From e1f37bfd938bb4312f8625d8337aec2ec2d8c06a Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Tue, 16 May 2023 10:34:23 -0700 Subject: [PATCH] massive refactoring for new feature-sets system --- .editorconfig | 3 +- .efrocachemap | 8183 +++++++++-------- .github/workflows/ci.yml | 6 +- .gitignore | 64 +- .../{ballisticacore.iml => ballisticakit.iml} | 47 +- .idea/dictionaries/ericf.xml | 52 +- .idea/inspectionProfiles/Default.xml | 1 + .idea/jsonSchemas.xml | 5 +- .idea/misc.xml | 2 +- .idea/modules.xml | 2 +- .idea/scopes/PyIgnoreProtectedAccess.xml | 4 +- .idea/scopes/PyIgnoreUnresolved.xml | 4 +- .idea/scopes/UncheckedPython.xml | 4 +- CHANGELOG.md | 1609 +++- Makefile | 634 +- README.md | 70 +- assets/.asset_manifest_public.json | 572 -- assets/Makefile | 7328 --------------- assets/src/ba_data/python/._ba_sources_hash | 1 - .../ba_data/python/._bainternal_sources_hash | 1 - assets/src/ba_data/python/_ba.py | 3410 ------- assets/src/ba_data/python/_bainternal.py | 277 - assets/src/ba_data/python/ba/__init__.py | 396 - assets/src/ba_data/python/ba/_analytics.py | 83 - assets/src/ba_data/python/ba/_app.py | 769 -- assets/src/ba_data/python/ba/_appmode.py | 3 - assets/src/ba_data/python/ba/_bootstrap.py | 192 - assets/src/ba_data/python/ba/_hooks.py | 498 - assets/src/ba_data/python/ba/_internal.py | 381 - assets/src/ba_data/python/ba/_store.py | 498 - assets/src/ba_data/python/ba/internal.py | 339 - .../src/ba_data/python/bastd/appdelegate.py | 37 - assets/src/ba_data/python/bastd/stdmap.py | 39 - .../src/ba_data/python/bastd/ui/appinvite.py | 449 - .../bastd/ui/settings/xbox360controller.py | 138 - assets/src/ba_data/python/bastd/ui/telnet.py | 67 - .../server/launch_ballisticacore_server.bat | 3 - ballisticacore-cmake/.idea/.name | 1 - ballisticacore-cmake/.idea/BallisticaCore.iml | 2 - .../.idea/scopes/External.xml | 3 - ballisticacore-cmake/.idea/scopes/Ignored.xml | 3 - ballisticacore-cmake/CMakeLists.txt | 688 -- .../Generic/BallisticaCoreGeneric.vcxproj | 758 -- .../BallisticaCoreGeneric.vcxproj.filters | 1691 ---- .../Headless/BallisticaCoreHeadless.vcxproj | 746 -- .../BallisticaCoreHeadless.vcxproj.filters | 1691 ---- .../.idea/.gitignore | 0 ballisticakit-cmake/.idea/.name | 1 + ballisticakit-cmake/.idea/BallisticaKit.iml | 8 + .../.idea/cmake.xml | 0 .../.idea/codeStyles/Project.xml | 1 + .../.idea/codeStyles/codeStyleConfig.xml | 0 .../.idea/dictionaries/ericf.xml | 58 +- .../inspectionProfiles/Project_Default.xml | 0 .../.idea/misc.xml | 57 +- .../.idea/modules.xml | 2 +- .../.idea/runConfigurations/ballisticakit.xml | 7 + ballisticakit-cmake/.idea/scopes/External.xml | 3 + ballisticakit-cmake/.idea/scopes/Ignored.xml | 3 + .../.idea/vcs.xml | 0 ballisticakit-cmake/CMakeLists.txt | 773 ++ .../Generic/BallisticaKit.rc | Bin 6966 -> 6948 bytes .../Generic/BallisticaKitGeneric.vcxproj | 839 ++ .../BallisticaKitGeneric.vcxproj.filters | 1970 ++++ .../Generic/Resource.h | 8 +- .../Generic/stdafx.cpp | 0 .../Generic/stdafx.h | 2 +- .../Generic/targetver.h | 0 .../Headless/BallisticaKitHeadless.vcxproj | 827 ++ .../BallisticaKitHeadless.vcxproj.filters | 1970 ++++ .../Headless/stdafx.cpp | 0 .../Headless/stdafx.h | 2 +- .../Headless/targetver.h | 0 config/README.md | 9 +- config/config.json | 54 - config/featuresets/README.md | 5 + config/featuresets/featureset_base.py | 15 + config/featuresets/featureset_classic.py | 15 + config/featuresets/featureset_core.py | 25 + config/featuresets/featureset_plus.py | 16 + config/featuresets/featureset_scene_v1.py | 15 + config/featuresets/featureset_template_fs.py | 15 + config/featuresets/featureset_ui_v1.py | 18 + config/projectconfig.json | 59 + config/spinoffconfig.py | 284 + config/toolconfigsrc/{README => README.md} | 2 + config/toolconfigsrc/clang-format | 4 + config/toolconfigsrc/dir-locals.el | 8 +- config/toolconfigsrc/editorconfig | 3 +- config/toolconfigsrc/mypy.ini | 19 +- resources/Makefile | 29 - src/README.md | 10 + .../assets}/.asset_manifest_private.json | 4784 +++++----- src/assets/.asset_manifest_public.json | 594 ++ src/assets/Makefile | 7505 +++++++++++++++ src/assets/README.md | 4 + src/assets/ba_data/python/babase/__init__.py | 184 + .../ba_data/python/babase}/_accountv2.py | 98 +- src/assets/ba_data/python/babase/_app.py | 519 ++ .../ba_data/python/babase}/_appcomponent.py | 12 +- .../ba_data/python/babase}/_appconfig.py | 49 +- .../ba_data/python/babase}/_apputils.py | 228 +- .../ba_data/python/babase}/_assetmanager.py | 6 +- .../assets/ba_data/python/babase}/_asyncio.py | 20 +- .../assets/ba_data/python/babase}/_cloud.py | 12 +- .../assets/ba_data/python/babase}/_error.py | 55 +- .../assets/ba_data/python/babase}/_general.py | 58 +- src/assets/ba_data/python/babase/_hooks.py | 415 + .../ba_data/python/babase}/_keyboard.py | 2 +- .../ba_data/python/babase}/_language.py | 91 +- .../assets/ba_data/python/babase}/_login.py | 62 +- .../assets/ba_data/python/babase}/_math.py | 0 .../assets/ba_data/python/babase}/_meta.py | 41 +- src/assets/ba_data/python/babase/_net.py | 76 + .../assets/ba_data/python/babase}/_plugin.py | 79 +- src/assets/ba_data/python/babase/_text.py | 90 + .../ba_data/python/babase}/_workspace.py | 41 +- src/assets/ba_data/python/babase/internal.py | 80 + .../ba_data/python/baclassic/__init__.py | 55 + .../ba_data/python/baclassic}/_accountv1.py | 162 +- .../ba_data/python/baclassic}/_achievement.py | 260 +- .../assets/ba_data/python/baclassic}/_ads.py | 97 +- .../ba_data/python/baclassic/_analytics.py | 97 + .../ba_data/python/baclassic}/_appdelegate.py | 24 +- .../ba_data/python/baclassic}/_benchmark.py | 129 +- .../ba_data/python/baclassic}/_campaign.py | 48 +- .../ba_data/python/baclassic}/_input.py | 97 +- .../ba_data/python/baclassic}/_level.py | 33 +- .../ba_data/python/baclassic}/_lobby.py | 227 +- .../ba_data/python/baclassic}/_music.py | 154 +- .../assets/ba_data/python/baclassic}/_net.py | 129 +- .../ba_data/python/baclassic}/_profile.py | 14 +- .../ba_data/python/baclassic}/_servermode.py | 146 +- src/assets/ba_data/python/baclassic/_store.py | 558 ++ .../ba_data/python/baclassic/_subsystem.py | 639 ++ .../assets/ba_data/python/baclassic}/_tips.py | 32 +- .../ba_data/python/baclassic}/_tournament.py | 12 +- .../assets/ba_data/python/baclassic}/_ui.py | 46 +- .../ba_data/python/baclassic}/macmusicapp.py | 70 +- .../ba_data/python/baclassic}/osmusic.py | 34 +- src/assets/ba_data/python/baenv.py | 419 + src/assets/ba_data/python/baplus/__init__.py | 30 + src/assets/ba_data/python/baplus/_hooks.py | 15 + .../ba_data/python/baplus/_subsystem.py | 248 + .../ba_data/python/bascenev1/__init__.py | 400 + .../ba_data/python/bascenev1}/_activity.py | 251 +- .../python/bascenev1}/_activitytypes.py | 73 +- .../ba_data/python/bascenev1}/_actor.py | 98 +- .../ba_data/python/bascenev1}/_collision.py | 34 +- .../ba_data/python/bascenev1}/_coopgame.py | 81 +- .../ba_data/python/bascenev1}/_coopsession.py | 118 +- src/assets/ba_data/python/bascenev1/_debug.py | 69 + .../ba_data/python/bascenev1}/_dependency.py | 96 +- .../python/bascenev1}/_dualteamsession.py | 20 +- .../ba_data/python/bascenev1/_featureset.py | 8 + .../python/bascenev1}/_freeforallsession.py | 22 +- .../python/bascenev1}/_gameactivity.py | 368 +- .../ba_data/python/bascenev1}/_gameresults.py | 76 +- .../ba_data/python/bascenev1}/_gameutils.py | 243 +- src/assets/ba_data/python/bascenev1/_hooks.py | 53 + .../assets/ba_data/python/bascenev1}/_map.py | 131 +- .../ba_data/python/bascenev1}/_messages.py | 57 +- .../python/bascenev1}/_multiteamsession.py | 82 +- src/assets/ba_data/python/bascenev1/_music.py | 73 + .../ba_data/python/bascenev1}/_nodeactor.py | 11 +- .../ba_data/python/bascenev1}/_player.py | 113 +- .../ba_data/python/bascenev1}/_playlist.py | 25 +- .../ba_data/python/bascenev1}/_powerup.py | 20 +- .../ba_data/python/bascenev1}/_score.py | 4 +- .../ba_data/python/bascenev1}/_session.py | 243 +- .../ba_data/python/bascenev1}/_settings.py | 0 .../ba_data/python/bascenev1}/_stats.py | 131 +- .../assets/ba_data/python/bascenev1}/_team.py | 65 +- .../ba_data/python/bascenev1}/_teamgame.py | 62 +- .../ba_data/python/bascenev1/internal.py | 43 + .../assets}/ba_data/python/bastd/__init__.py | 2 +- .../ba_data/python/bastd/activity/__init__.py | 0 .../ba_data/python/bastd/activity/coopjoin.py | 35 +- .../python/bastd/activity/coopscore.py | 546 +- .../python/bastd/activity/drawscore.py | 9 +- .../python/bastd/activity/dualteamscore.py | 47 +- .../bastd/activity/freeforallvictory.py | 89 +- .../python/bastd/activity/multiteamjoin.py | 26 +- .../python/bastd/activity/multiteamscore.py | 61 +- .../python/bastd/activity/multiteamvictory.py | 95 +- .../ba_data/python/bastd/actor/__init__.py | 0 .../ba_data/python/bastd/actor/background.py | 49 +- .../ba_data/python/bastd/actor/bomb.py | 452 +- .../python/bastd/actor/controlsguide.py | 158 +- .../ba_data/python/bastd/actor/flag.py | 91 +- .../ba_data/python/bastd/actor/image.py | 54 +- .../python/bastd/actor/onscreencountdown.py | 47 +- .../python/bastd/actor/onscreentimer.py | 62 +- .../ba_data/python/bastd/actor/playerspaz.py | 89 +- .../ba_data/python/bastd/actor/popuptext.py | 27 +- .../ba_data/python/bastd/actor/powerupbox.py | 179 +- .../ba_data/python/bastd/actor/respawnicon.py | 57 +- .../ba_data/python/bastd/actor/scoreboard.py | 83 +- .../ba_data/python/bastd/actor/spawner.py | 18 +- .../ba_data/python/bastd/actor/spaz.py | 378 +- .../python/bastd/actor/spazappearance.py | 585 +- .../ba_data/python/bastd/actor/spazbot.py | 200 +- .../ba_data/python/bastd/actor/spazfactory.py | 211 +- .../ba_data/python/bastd/actor/text.py | 49 +- .../ba_data/python/bastd/actor/tipstext.py | 37 +- .../ba_data/python/bastd/actor/zoomtext.py | 61 +- .../ba_data/python/bastd/game/__init__.py | 0 .../ba_data/python/bastd/game/assault.py | 82 +- .../python/bastd/game/capturetheflag.py | 151 +- .../ba_data/python/bastd/game/chosenone.py | 113 +- .../ba_data/python/bastd/game/conquest.py | 66 +- .../ba_data/python/bastd/game/deathmatch.py | 56 +- .../python/bastd/game/easteregghunt.py | 88 +- .../ba_data/python/bastd/game/elimination.py | 121 +- .../ba_data/python/bastd/game/football.py | 265 +- .../ba_data/python/bastd/game/hockey.py | 118 +- .../ba_data/python/bastd/game/keepaway.py | 92 +- .../python/bastd/game/kingofthehill.py | 88 +- .../ba_data/python/bastd/game/meteorshower.py | 67 +- .../ba_data/python/bastd/game/ninjafight.py | 59 +- .../ba_data/python/bastd/game/onslaught.py | 215 +- .../assets}/ba_data/python/bastd/game/race.py | 218 +- .../ba_data/python/bastd/game/runaround.py | 272 +- .../python/bastd/game/targetpractice.py | 116 +- .../ba_data/python/bastd/game/thelaststand.py | 69 +- .../assets}/ba_data/python/bastd/gameutils.py | 79 +- .../ba_data/python/bastd/keyboard/__init__.py | 0 .../python/bastd/keyboard/englishkeyboard.py | 8 +- .../assets}/ba_data/python/bastd/mainmenu.py | 383 +- .../ba_data/python/bastd/mapdata/__init__.py | 0 .../ba_data/python/bastd/mapdata/big_g.py | 0 .../ba_data/python/bastd/mapdata/bridgit.py | 0 .../ba_data/python/bastd/mapdata/courtyard.py | 0 .../python/bastd/mapdata/crag_castle.py | 0 .../python/bastd/mapdata/doom_shroom.py | 0 .../python/bastd/mapdata/football_stadium.py | 0 .../python/bastd/mapdata/happy_thoughts.py | 0 .../python/bastd/mapdata/hockey_stadium.py | 0 .../python/bastd/mapdata/lake_frigid.py | 0 .../python/bastd/mapdata/monkey_face.py | 0 .../ba_data/python/bastd/mapdata/rampage.py | 0 .../python/bastd/mapdata/roundabout.py | 0 .../python/bastd/mapdata/step_right_up.py | 0 .../ba_data/python/bastd/mapdata/the_pad.py | 0 .../ba_data/python/bastd/mapdata/tip_top.py | 0 .../ba_data/python/bastd/mapdata/tower_d.py | 0 .../ba_data/python/bastd/mapdata/zig_zag.py | 0 .../assets}/ba_data/python/bastd/maps.py | 804 +- .../ba_data/python/bastd/session/__init__.py | 0 .../assets}/ba_data/python/bastd/tutorial.py | 359 +- .../ba_data/python/bastd/ui/__init__.py | 0 .../python/bastd/ui/account/__init__.py | 19 +- .../ba_data/python/bastd/ui/account/link.py | 87 +- .../python/bastd/ui/account/settings.py | 681 +- .../ba_data/python/bastd/ui/account/unlink.py | 68 +- .../python/bastd/ui/account/v2proxy.py | 99 +- .../ba_data/python/bastd/ui/account/viewer.py | 202 +- .../ba_data/python/bastd/ui/achievements.py | 75 +- .../ba_data/python/bastd/ui/appinvite.py | 243 + .../python/bastd/ui/characterpicker.py | 65 +- .../ba_data/python/bastd/ui/colorpicker.py | 110 +- .../assets}/ba_data/python/bastd/ui/config.py | 52 +- .../ba_data/python/bastd/ui/configerror.py | 44 +- .../ba_data/python/bastd/ui/confirm.py | 84 +- .../ba_data/python/bastd/ui/continues.py | 107 +- .../ba_data/python/bastd/ui/coop/__init__.py | 0 .../ba_data/python/bastd/ui/coop/browser.py | 542 +- .../python/bastd/ui/coop/gamebutton.py | 99 +- .../ba_data/python/bastd/ui/coop/level.py | 39 +- .../python/bastd/ui/coop/tournamentbutton.py | 219 +- .../ba_data/python/bastd/ui/creditslist.py | 129 +- .../assets}/ba_data/python/bastd/ui/debug.py | 163 +- .../ba_data/python/bastd/ui/feedback.py | 58 +- .../ba_data/python/bastd/ui/fileselector.py | 146 +- .../python/bastd/ui/gather/__init__.py | 154 +- .../python/bastd/ui/gather/abouttab.py | 51 +- .../python/bastd/ui/gather/manualtab.py | 489 +- .../python/bastd/ui/gather/nearbytab.py | 61 +- .../python/bastd/ui/gather/privatetab.py | 323 +- .../python/bastd/ui/gather/publictab.py | 424 +- .../ba_data/python/bastd/ui/getcurrency.py | 317 +- .../ba_data/python/bastd/ui/getremote.py | 41 +- .../assets}/ba_data/python/bastd/ui/helpui.py | 254 +- .../ba_data/python/bastd/ui/iconpicker.py | 57 +- .../assets}/ba_data/python/bastd/ui/kiosk.py | 218 +- .../python/bastd/ui/league/__init__.py | 0 .../python/bastd/ui/league/rankbutton.py | 162 +- .../python/bastd/ui/league/rankwindow.py | 406 +- .../ba_data/python/bastd/ui/mainmenu.py | 496 +- .../assets}/ba_data/python/bastd/ui/party.py | 200 +- .../ba_data/python/bastd/ui/partyqueue.py | 217 +- .../assets}/ba_data/python/bastd/ui/play.py | 222 +- .../python/bastd/ui/playlist/__init__.py | 32 +- .../python/bastd/ui/playlist/addgame.py | 117 +- .../python/bastd/ui/playlist/browser.py | 262 +- .../bastd/ui/playlist/customizebrowser.py | 299 +- .../ba_data/python/bastd/ui/playlist/edit.py | 190 +- .../bastd/ui/playlist/editcontroller.py | 65 +- .../python/bastd/ui/playlist/editgame.py | 198 +- .../python/bastd/ui/playlist/mapselect.py | 131 +- .../ba_data/python/bastd/ui/playlist/share.py | 81 +- .../ba_data/python/bastd/ui/playoptions.py | 191 +- .../assets}/ba_data/python/bastd/ui/popup.py | 80 +- .../python/bastd/ui/profile/__init__.py | 0 .../python/bastd/ui/profile/browser.py | 206 +- .../ba_data/python/bastd/ui/profile/edit.py | 303 +- .../python/bastd/ui/profile/upgrade.py | 147 +- .../ba_data/python/bastd/ui/promocode.py | 60 +- .../ba_data/python/bastd/ui/purchase.py | 87 +- .../assets}/ba_data/python/bastd/ui/qrcode.py | 29 +- .../ba_data/python/bastd/ui/radiogroup.py | 12 +- .../assets}/ba_data/python/bastd/ui/report.py | 60 +- .../python/bastd/ui/resourcetypeinfo.py | 26 +- .../ba_data/python/bastd/ui/serverdialog.py | 70 +- .../python/bastd/ui/settings/__init__.py | 0 .../python/bastd/ui/settings/advanced.py | 357 +- .../python/bastd/ui/settings/allsettings.py | 148 +- .../ba_data/python/bastd/ui/settings/audio.py | 123 +- .../python/bastd/ui/settings/controls.py | 224 +- .../python/bastd/ui/settings/gamepad.py | 299 +- .../bastd/ui/settings/gamepadadvanced.py | 211 +- .../python/bastd/ui/settings/gamepadselect.py | 98 +- .../python/bastd/ui/settings/graphics.py | 227 +- .../python/bastd/ui/settings/keyboard.py | 154 +- .../python/bastd/ui/settings/nettesting.py | 124 +- .../python/bastd/ui/settings/plugins.py | 120 +- .../bastd/ui/settings/pluginsettings.py | 88 +- .../python/bastd/ui/settings/remoteapp.py | 60 +- .../python/bastd/ui/settings/testing.py | 107 +- .../python/bastd/ui/settings/touchscreen.py | 103 +- .../python/bastd/ui/settings/vrtesting.py | 14 +- .../python/bastd/ui/soundtrack/__init__.py | 0 .../python/bastd/ui/soundtrack/browser.py | 240 +- .../python/bastd/ui/soundtrack/edit.py | 210 +- .../bastd/ui/soundtrack/entrytypeselect.py | 89 +- .../python/bastd/ui/soundtrack/macmusicapp.py | 50 +- .../ba_data/python/bastd/ui/specialoffer.py | 249 +- .../ba_data/python/bastd/ui/store/__init__.py | 0 .../ba_data/python/bastd/ui/store/browser.py | 540 +- .../ba_data/python/bastd/ui/store/button.py | 128 +- .../ba_data/python/bastd/ui/store/item.py | 195 +- .../assets}/ba_data/python/bastd/ui/tabs.py | 19 +- .../python/bastd/ui/teamnamescolors.py | 85 +- .../python/bastd/ui/tournamententry.py | 325 +- .../python/bastd/ui/tournamentscores.py | 88 +- .../ba_data/python/bastd/ui/trophies.py | 50 +- .../assets}/ba_data/python/bastd/ui/url.py | 49 +- .../ba_data/python/bastd/ui/v2upgrade.py | 57 +- .../assets}/ba_data/python/bastd/ui/watch.py | 325 +- .../ba_data/python/batemplatefs/__init__.py | 11 + .../ba_data/python/batemplatefs/_hooks.py | 12 + .../ba_data/python/batemplatefs/_subsystem.py | 19 + src/assets/ba_data/python/bauiv1/__init__.py | 218 + src/assets/ba_data/python/bauiv1/_hooks.py | 93 + .../assets/ba_data/python/bauiv1}/modutils.py | 62 +- .../python/bauiv1}/onscreenkeyboard.py | 195 +- .../ba_data/python/bauiv1}/ui/__init__.py | 40 +- .../assets}/pdoc/templates/custom.css | 0 .../assets}/pdoc/templates/index.html.jinja2 | 0 .../assets}/pdoc/templates/module.html.jinja2 | 0 {assets/src => src/assets}/server/README.txt | 4 +- .../assets/server/ballisticakit_server.py | 32 +- .../assets}/server/config_template.yaml | 2 +- .../server/launch_ballisticakit_server.bat | 3 + src/ballistica/README.md | 86 + src/ballistica/app/app.cc | 18 - src/ballistica/app/app.h | 85 - src/ballistica/app/app_flavor.cc | 450 - src/ballistica/app/app_flavor_headless.cc | 24 - src/ballistica/app/app_flavor_headless.h | 20 - src/ballistica/app/app_flavor_vr.cc | 112 - src/ballistica/app/app_flavor_vr.h | 50 - src/ballistica/app/stress_test.cc | 114 - src/ballistica/app/stress_test.h | 31 - src/ballistica/assets/assets.cc | 1238 --- src/ballistica/assets/assets.h | 210 - src/ballistica/assets/assets_server.h | 42 - .../assets/component/asset_component.cc | 34 - .../assets/component/asset_component.h | 63 - .../assets/component/collide_model.cc | 44 - .../assets/component/collide_model.h | 41 - .../assets/component/cube_map_texture.cc | 21 - .../assets/component/cube_map_texture.h | 32 - src/ballistica/assets/component/data.cc | 45 - src/ballistica/assets/component/data.h | 43 - src/ballistica/assets/component/model.cc | 45 - src/ballistica/assets/component/model.h | 44 - src/ballistica/assets/component/sound.cc | 44 - src/ballistica/assets/component/sound.h | 37 - src/ballistica/assets/component/texture.cc | 57 - src/ballistica/assets/component/texture.h | 39 - .../assets/data/collide_model_data.h | 47 - src/ballistica/assets/data/data_data.h | 47 - .../assets/data/model_renderer_data.h | 21 - src/ballistica/assets/data/sound_data.h | 58 - src/ballistica/assets/data/texture_data.h | 62 - .../assets/data/texture_renderer_data.h | 27 - src/ballistica/ballistica.cc | 353 - src/ballistica/base/README.md | 3 + src/ballistica/base/app/app.cc | 413 + .../{app/app_flavor.h => base/app/app.h} | 119 +- src/ballistica/{ => base}/app/app_config.cc | 44 +- src/ballistica/{ => base}/app/app_config.h | 27 +- src/ballistica/base/app/app_headless.cc | 24 + src/ballistica/base/app/app_headless.h | 20 + src/ballistica/base/app/app_mode.cc | 67 + src/ballistica/base/app/app_mode.h | 102 + src/ballistica/base/app/app_mode_empty.cc | 21 + src/ballistica/base/app/app_mode_empty.h | 21 + src/ballistica/base/app/app_vr.cc | 122 + src/ballistica/base/app/app_vr.h | 49 + .../{platform/sdl => base/app}/sdl_app.cc | 240 +- src/ballistica/base/app/sdl_app.h | 66 + src/ballistica/base/app/stress_test.cc | 101 + src/ballistica/base/app/stress_test.h | 25 + .../assets/asset.cc} | 62 +- .../assets/asset.h} | 52 +- src/ballistica/base/assets/assets.cc | 1627 ++++ src/ballistica/base/assets/assets.h | 181 + .../{ => base}/assets/assets_server.cc | 141 +- src/ballistica/base/assets/assets_server.h | 42 + .../assets/collision_mesh_asset.cc} | 53 +- .../base/assets/collision_mesh_asset.h | 41 + .../assets/data_asset.cc} | 44 +- src/ballistica/base/assets/data_asset.h | 41 + .../assets/mesh_asset.cc} | 42 +- .../model_data.h => base/assets/mesh_asset.h} | 43 +- .../base/assets/mesh_asset_renderer_data.h | 21 + .../assets/sound_asset.cc} | 74 +- src/ballistica/base/assets/sound_asset.h | 53 + .../assets/texture_asset.cc} | 114 +- src/ballistica/base/assets/texture_asset.h | 59 + .../assets/texture_asset_preload_data.cc} | 17 +- .../assets/texture_asset_preload_data.h} | 20 +- .../base/assets/texture_asset_renderer_data.h | 29 + src/ballistica/{ => base}/audio/al_sys.cc | 12 +- src/ballistica/{ => base}/audio/al_sys.h | 12 +- src/ballistica/{ => base}/audio/audio.cc | 88 +- src/ballistica/{ => base}/audio/audio.h | 49 +- .../{ => base}/audio/audio_server.cc | 512 +- .../{ => base}/audio/audio_server.h | 100 +- .../{ => base}/audio/audio_source.cc | 63 +- .../{ => base}/audio/audio_source.h | 29 +- .../{ => base}/audio/audio_streamer.cc | 14 +- .../{ => base}/audio/audio_streamer.h | 18 +- src/ballistica/{ => base}/audio/ogg_stream.cc | 12 +- src/ballistica/{ => base}/audio/ogg_stream.h | 12 +- src/ballistica/base/base.cc | 516 ++ src/ballistica/base/base.h | 726 ++ .../{ => base}/dynamics/bg/bg_dynamics.cc | 172 +- .../{ => base}/dynamics/bg/bg_dynamics.h | 21 +- .../dynamics/bg/bg_dynamics_draw_snapshot.h | 34 +- .../dynamics/bg/bg_dynamics_fuse.cc | 24 +- .../{ => base}/dynamics/bg/bg_dynamics_fuse.h | 12 +- .../dynamics/bg/bg_dynamics_fuse_data.h | 13 +- .../dynamics/bg/bg_dynamics_height_cache.cc | 6 +- .../dynamics/bg/bg_dynamics_height_cache.h | 12 +- .../dynamics/bg/bg_dynamics_server.cc | 313 +- .../dynamics/bg/bg_dynamics_server.h | 95 +- .../dynamics/bg/bg_dynamics_shadow.cc | 32 +- .../dynamics/bg/bg_dynamics_shadow.h | 14 +- .../dynamics/bg/bg_dynamics_shadow_data.h | 10 +- .../dynamics/bg/bg_dynamics_volume_light.cc | 26 +- .../dynamics/bg/bg_dynamics_volume_light.h | 13 +- .../bg/bg_dynamics_volume_light_data.h | 12 +- .../{ => base}/dynamics/collision_cache.cc | 38 +- .../{ => base}/dynamics/collision_cache.h | 28 +- .../graphics/component/empty_component.h | 12 +- .../graphics/component/object_component.cc | 64 +- .../graphics/component/object_component.h | 38 +- .../component/post_process_component.cc | 6 +- .../component/post_process_component.h | 12 +- .../graphics/component/render_component.cc | 39 + .../graphics/component/render_component.h | 30 +- .../graphics/component/shield_component.cc | 6 +- .../graphics/component/shield_component.h | 21 + .../graphics/component/simple_component.cc | 58 +- .../graphics/component/simple_component.h | 60 +- .../graphics/component/smoke_component.cc | 10 +- .../graphics/component/smoke_component.h | 12 +- .../graphics/component/special_component.cc | 6 +- .../graphics/component/special_component.h | 12 +- .../graphics/component/sprite_component.cc | 10 +- .../graphics/component/sprite_component.h | 24 +- .../{ => base}/graphics/gl/gl_sys.cc | 26 +- .../{ => base}/graphics/gl/gl_sys.h | 14 +- .../{ => base}/graphics/gl/renderer_gl.cc | 429 +- .../{ => base}/graphics/gl/renderer_gl.h | 33 +- .../{ => base}/graphics/graphics.cc | 738 +- src/ballistica/{ => base}/graphics/graphics.h | 225 +- .../{ => base}/graphics/graphics_server.cc | 344 +- .../{ => base}/graphics/graphics_server.h | 168 +- .../graphics/graphics_vr.cc} | 82 +- .../graphics/graphics_vr.h} | 41 +- .../{ => base}/graphics/mesh/image_mesh.cc | 6 +- .../{ => base}/graphics/mesh/image_mesh.h | 12 +- .../{ => base}/graphics/mesh/mesh.h | 18 +- .../{ => base}/graphics/mesh/mesh_buffer.h | 12 +- .../graphics/mesh/mesh_buffer_base.h | 12 +- .../mesh/mesh_buffer_vertex_simple_full.h | 18 + .../mesh/mesh_buffer_vertex_smoke_full.h | 18 + .../graphics/mesh/mesh_buffer_vertex_sprite.h | 18 + .../{ => base}/graphics/mesh/mesh_data.cc | 12 +- .../{ => base}/graphics/mesh/mesh_data.h | 12 +- .../graphics/mesh/mesh_data_client_handle.cc | 17 + .../graphics/mesh/mesh_data_client_handle.h | 13 +- .../base/graphics/mesh/mesh_index_buffer_16.h | 17 + .../base/graphics/mesh/mesh_index_buffer_32.h | 17 + .../{ => base}/graphics/mesh/mesh_indexed.h | 14 +- .../graphics/mesh/mesh_indexed_base.h | 36 +- .../mesh/mesh_indexed_dual_texture_full.h | 18 + .../graphics/mesh/mesh_indexed_object_split.h | 12 +- .../graphics/mesh/mesh_indexed_simple_full.h | 18 + .../graphics/mesh/mesh_indexed_simple_split.h | 12 +- .../graphics/mesh/mesh_indexed_smoke_full.h | 18 + .../mesh/mesh_indexed_static_dynamic.h | 16 +- .../graphics/mesh/mesh_non_indexed.h | 12 +- .../base/graphics/mesh/mesh_renderer_data.h | 15 + .../base/graphics/mesh/sprite_mesh.h | 17 + .../{ => base}/graphics/mesh/text_mesh.cc | 31 +- .../{ => base}/graphics/mesh/text_mesh.h | 12 +- .../base/graphics/renderer/framebuffer.h | 19 + .../graphics/renderer}/render_pass.cc | 117 +- .../graphics/renderer}/render_pass.h | 13 +- .../graphics/renderer}/render_target.cc | 58 +- .../graphics/renderer}/render_target.h | 18 +- .../graphics/renderer}/renderer.cc | 111 +- .../graphics/renderer}/renderer.h | 122 +- .../graphics/support}/area_of_interest.cc | 10 +- .../graphics/support}/area_of_interest.h | 12 +- .../graphics/support}/camera.cc | 72 +- .../graphics/support}/camera.h | 15 +- .../graphics/support}/frame_def.cc | 65 +- .../graphics/support}/frame_def.h | 45 +- .../graphics/support}/net_graph.cc | 18 +- .../base/graphics/support/net_graph.h | 33 + .../graphics/support}/render_command_buffer.h | 70 +- .../graphics/text/font_page_map_data.h | 14 +- .../{ => base}/graphics/text/text_graphics.cc | 22 +- .../{ => base}/graphics/text/text_graphics.h | 14 +- .../{ => base}/graphics/text/text_group.cc | 59 +- .../{ => base}/graphics/text/text_group.h | 36 +- .../{ => base}/graphics/text/text_packer.cc | 6 +- .../{ => base}/graphics/text/text_packer.h | 14 +- .../{ => base}/graphics/texture/dds.cc | 11 +- .../{ => base}/graphics/texture/dds.h | 12 +- .../{ => base}/graphics/texture/ktx.cc | 11 +- .../{ => base}/graphics/texture/ktx.h | 12 +- .../{ => base}/graphics/texture/pvr.cc | 10 +- .../{ => base}/graphics/texture/pvr.h | 12 +- .../base/input/device/input_device.cc | 93 + .../base/input/device/input_device.h | 152 + .../input/device/input_device_delegate.cc | 35 + .../base/input/device/input_device_delegate.h | 57 + .../input/device/joystick_input.cc} | 317 +- .../input/device/joystick_input.h} | 31 +- .../{ => base}/input/device/keyboard_input.cc | 56 +- .../{ => base}/input/device/keyboard_input.h | 22 +- .../{ => base}/input/device/test_input.cc | 44 +- .../{ => base}/input/device/test_input.h | 14 +- .../{ => base}/input/device/touch_input.cc | 186 +- .../{ => base}/input/device/touch_input.h | 14 +- src/ballistica/{ => base}/input/input.cc | 1011 +- src/ballistica/{ => base}/input/input.h | 160 +- .../input/support/remote_app_server.cc} | 110 +- .../input/support/remote_app_server.h} | 20 +- src/ballistica/base/logic/logic.cc | 567 ++ src/ballistica/base/logic/logic.h | 104 + .../{ => base}/networking/network_reader.cc | 223 +- .../{ => base}/networking/network_reader.h | 27 +- .../base/networking/network_writer.cc | 35 + .../base/networking/network_writer.h | 27 + src/ballistica/base/networking/networking.cc | 58 + .../{ => base}/networking/networking.h | 92 +- .../android/amazon/base_plat_andr_amazon.cc | 18 + .../android/amazon/base_plat_andr_amazon.h | 21 + .../platform/android/base_platform_android.cc | 54 + .../platform/android/base_platform_android.h | 28 + .../android/cardboard/base_pl_an_cardboard.cc | 9 + .../android/cardboard/base_pl_an_cardboard.h | 20 + .../android/google/base_plat_andr_google.cc | 12 + .../android/google/base_plat_andr_google.h | 22 + .../platform/apple/base_platform_apple.cc | 68 + .../base/platform/apple/base_platform_apple.h | 28 + src/ballistica/base/platform/base_platform.cc | 299 + src/ballistica/base/platform/base_platform.h | 91 + .../platform/linux/base_platform_linux.cc | 22 + .../base/platform/linux/base_platform_linux.h | 22 + .../base/platform/oculus/main_rift.cc | 1180 +++ .../base/platform/support/min_sdl_key_names.h | 407 + .../platform/windows/base_platform_windows.cc | 67 + .../platform/windows/base_platform_windows.h | 24 + .../windows/base_platform_windows_oculus.cc | 23 + .../windows/base_platform_windows_oculus.h | 22 + src/ballistica/base/python/base_python.cc | 540 ++ src/ballistica/base/python/base_python.h | 168 + .../python/class/python_class_app_timer.cc | 123 + .../python/class/python_class_app_timer.h | 30 + .../python/class/python_class_context_call.cc | 120 +- .../python/class/python_class_context_call.h | 21 +- .../python/class/python_class_context_ref.cc | 229 + .../python/class/python_class_context_ref.h | 41 + .../class/python_class_display_timer.cc | 132 + .../python/class/python_class_display_timer.h | 30 + .../class/python_class_feature_set_data.cc | 52 + .../class/python_class_feature_set_data.h | 53 + .../python/class/python_class_simple_sound.cc | 112 + .../python/class/python_class_simple_sound.h | 55 + .../python/class/python_class_vec3.cc | 115 +- .../python/class/python_class_vec3.h | 18 +- .../base/python/methods/python_methods_app.cc | 1289 +++ .../base/python/methods/python_methods_app.h | 20 + .../python/methods/python_methods_graphics.cc | 648 ++ .../python/methods/python_methods_graphics.h | 20 + .../python/methods/python_methods_misc.cc | 1444 +++ .../base/python/methods/python_methods_misc.h | 20 + .../python/support/python_context_call.cc | 169 + .../base/python/support/python_context_call.h | 74 + .../support/python_context_call_runnable.h | 26 + src/ballistica/base/support/app_timer.h | 44 + src/ballistica/base/support/context.cc | 66 + src/ballistica/base/support/context.h | 157 + .../{generic => base/support}/huffman.cc | 8 +- .../{generic => base/support}/huffman.h | 12 +- src/ballistica/base/support/plus_soft.h | 65 + .../support}/stdio_console.cc | 86 +- src/ballistica/base/support/stdio_console.h | 24 + src/ballistica/{ => base}/ui/console.cc | 110 +- src/ballistica/{ => base}/ui/console.h | 22 +- src/ballistica/{ => base}/ui/ui.cc | 366 +- src/ballistica/base/ui/ui.h | 147 + src/ballistica/base/ui/widget_message.h | 71 + src/ballistica/classic/README.md | 4 + src/ballistica/classic/classic.cc | 59 + src/ballistica/classic/classic.h | 78 + .../classic/python/classic_python.cc | 94 + .../classic/python/classic_python.h | 40 + .../python/methods/python_methods_classic.cc | 156 + .../python/methods/python_methods_classic.h | 19 + .../{logic => classic/support}/v1_account.cc | 60 +- .../{logic => classic/support}/v1_account.h | 32 +- .../config/config_windows_generic.h | 19 - .../config/config_windows_headless.h | 18 - src/ballistica/core/README.md | 15 + src/ballistica/core/context.cc | 132 - src/ballistica/core/context.h | 126 - src/ballistica/core/core.cc | 247 + src/ballistica/core/core.h | 155 + src/ballistica/core/logging.cc | 109 - src/ballistica/core/logging.h | 39 - src/ballistica/core/macros.cc | 108 - src/ballistica/core/macros.h | 166 - .../core/platform/apple/core_platform_apple.h | 82 + .../platform/core_platform.cc} | 912 +- .../platform/core_platform.h} | 438 +- .../platform/linux/core_platform_linux.cc} | 30 +- .../core/platform/linux/core_platform_linux.h | 29 + .../platform/support}/min_sdl.h | 6 +- .../windows/core_platform_windows.cc} | 259 +- .../platform/windows/core_platform_windows.h} | 29 +- src/ballistica/core/python/core_python.cc | 355 + src/ballistica/core/python/core_python.h | 84 + src/ballistica/core/support/base_soft.h | 50 + src/ballistica/core/support/core_config.cc | 175 + src/ballistica/core/support/core_config.h | 61 + src/ballistica/core/types.h | 1074 --- .../dynamics/material/sound_material_action.h | 32 - src/ballistica/generic/real_timer.h | 49 - src/ballistica/generic/timer.cc | 55 - src/ballistica/generic/timer.h | 41 - src/ballistica/generic/timer_list.h | 68 - .../graphics/component/render_component.cc | 82 - .../graphics/component/shield_component.h | 21 - src/ballistica/graphics/framebuffer.h | 19 - .../mesh/mesh_buffer_vertex_simple_full.h | 18 - .../mesh/mesh_buffer_vertex_smoke_full.h | 18 - .../graphics/mesh/mesh_buffer_vertex_sprite.h | 18 - .../graphics/mesh/mesh_data_client_handle.cc | 17 - .../graphics/mesh/mesh_index_buffer_16.h | 17 - .../graphics/mesh/mesh_index_buffer_32.h | 17 - .../mesh/mesh_indexed_dual_texture_full.h | 18 - .../graphics/mesh/mesh_indexed_simple_full.h | 18 - .../graphics/mesh/mesh_indexed_smoke_full.h | 18 - .../graphics/mesh/mesh_renderer_data.h | 15 - src/ballistica/graphics/mesh/sprite_mesh.h | 17 - src/ballistica/graphics/net_graph.h | 32 - .../input/device/client_input_device.cc | 100 - .../input/device/client_input_device.h | 44 - src/ballistica/input/device/input_device.cc | 327 - src/ballistica/input/device/input_device.h | 187 - src/ballistica/internal/app_internal.h | 57 - .../logic/client_controller_interface.h | 22 - src/ballistica/logic/host_activity.h | 123 - src/ballistica/logic/logic.cc | 2183 ----- src/ballistica/logic/logic.h | 339 - src/ballistica/logic/session/client_session.h | 138 - .../logic/session/replay_client_session.h | 42 - src/ballistica/logic/session/session.cc | 38 - src/ballistica/logic/session/session.h | 44 - src/ballistica/math/random.h | 17 - src/ballistica/networking/network_writer.cc | 35 - src/ballistica/networking/network_writer.h | 25 - src/ballistica/networking/networking.cc | 270 - src/ballistica/networking/telnet_server.cc | 247 - src/ballistica/networking/telnet_server.h | 49 - .../platform/apple/platform_apple.h | 85 - .../platform/linux/platform_linux.h | 30 - src/ballistica/platform/sdl/sdl_app.h | 66 - src/ballistica/platform/stdio_console.h | 23 - src/ballistica/plus/README.md | 6 + src/ballistica/python/class/python_class.h | 28 - .../python/class/python_class_activity_data.h | 39 - .../class/python_class_collide_model.cc | 119 - .../python/class/python_class_collide_model.h | 34 - .../python/class/python_class_context.cc | 228 - .../python/class/python_class_context.h | 37 - .../python/class/python_class_data.cc | 146 - .../python/class/python_class_data.h | 36 - .../python/class/python_class_model.cc | 117 - .../python/class/python_class_model.h | 34 - .../python/class/python_class_sound.cc | 116 - .../python/class/python_class_sound.h | 34 - .../python/class/python_class_texture.cc | 109 - .../python/class/python_class_texture.h | 34 - .../python/class/python_class_timer.cc | 182 - .../python/class/python_class_timer.h | 35 - .../python/methods/python_methods_app.cc | 1260 --- .../python/methods/python_methods_app.h | 20 - .../python/methods/python_methods_assets.cc | 546 -- .../python/methods/python_methods_assets.h | 20 - .../python/methods/python_methods_gameplay.cc | 739 -- .../python/methods/python_methods_gameplay.h | 20 - .../python/methods/python_methods_graphics.cc | 433 - .../python/methods/python_methods_graphics.h | 20 - .../python/methods/python_methods_input.cc | 306 - .../python/methods/python_methods_input.h | 20 - .../methods/python_methods_networking.cc | 559 -- .../methods/python_methods_networking.h | 20 - .../python/methods/python_methods_system.cc | 1126 --- .../python/methods/python_methods_system.h | 20 - .../python/methods/python_methods_ui.h | 20 - src/ballistica/python/python.cc | 2881 ------ src/ballistica/python/python.h | 453 - src/ballistica/python/python_context_call.cc | 136 - src/ballistica/python/python_context_call.h | 54 - .../python/python_context_call_runnable.h | 25 - .../scene/node/session_globals_node.h | 22 - .../scene/node/texture_sequence_node.h | 33 - src/ballistica/scene/scene_stream.h | 172 - src/ballistica/scene/v1/scene_v1.cc | 96 - src/ballistica/scene/v1/scene_v1.h | 16 - src/ballistica/scene_v1/README.md | 3 + src/ballistica/scene_v1/assets/scene_asset.cc | 39 + src/ballistica/scene_v1/assets/scene_asset.h | 84 + .../scene_v1/assets/scene_collision_mesh.cc | 50 + .../scene_v1/assets/scene_collision_mesh.h | 37 + .../scene_v1/assets/scene_cube_map_texture.cc | 21 + .../scene_v1/assets/scene_cube_map_texture.h | 33 + .../scene_v1/assets/scene_data_asset.cc | 51 + .../scene_v1/assets/scene_data_asset.h | 40 + src/ballistica/scene_v1/assets/scene_mesh.cc | 50 + src/ballistica/scene_v1/assets/scene_mesh.h | 38 + src/ballistica/scene_v1/assets/scene_sound.cc | 53 + src/ballistica/scene_v1/assets/scene_sound.h | 35 + .../scene_v1/assets/scene_texture.cc | 64 + .../scene_v1/assets/scene_texture.h | 38 + .../connection/connection.cc | 71 +- .../connection/connection.h | 16 +- .../connection/connection_set.cc | 192 +- .../connection/connection_set.h | 72 +- .../connection/connection_to_client.cc | 228 +- .../connection/connection_to_client.h | 15 +- .../connection/connection_to_client_udp.cc | 36 +- .../connection/connection_to_client_udp.h | 21 +- .../connection/connection_to_host.cc | 179 +- .../connection/connection_to_host.h | 13 +- .../connection/connection_to_host_udp.cc | 76 +- .../connection/connection_to_host_udp.h | 16 +- .../{ => scene_v1}/dynamics/collision.h | 16 +- .../{ => scene_v1}/dynamics/dynamics.cc | 108 +- .../{ => scene_v1}/dynamics/dynamics.h | 53 +- .../material/impact_sound_material_action.cc | 30 +- .../material/impact_sound_material_action.h | 22 +- .../dynamics/material/material.cc | 32 +- .../dynamics/material/material.h | 19 +- .../dynamics/material/material_action.h | 15 +- .../dynamics/material/material_component.cc | 48 +- .../dynamics/material/material_component.h | 19 +- .../material/material_condition_node.cc | 19 +- .../material/material_condition_node.h | 17 +- .../dynamics/material/material_context.cc | 40 +- .../dynamics/material/material_context.h | 29 +- .../material/node_message_material_action.cc | 13 +- .../material/node_message_material_action.h | 19 +- .../material/node_mod_material_action.cc | 13 +- .../material/node_mod_material_action.h | 14 +- .../material/node_user_msg_mat_action.cc} | 18 +- .../material/node_user_msg_mat_action.h} | 16 +- .../material/part_mod_material_action.cc | 13 +- .../material/part_mod_material_action.h | 14 +- .../material/python_call_material_action.cc | 14 +- .../material/python_call_material_action.h | 18 +- .../material/roll_sound_material_action.cc | 30 +- .../material/roll_sound_material_action.h | 22 +- .../material/skid_sound_material_action.cc | 30 +- .../material/skid_sound_material_action.h | 22 +- .../material/sound_material_action.cc | 20 +- .../dynamics/material/sound_material_action.h | 32 + .../{ => scene_v1}/dynamics/part.cc | 24 +- src/ballistica/{ => scene_v1}/dynamics/part.h | 18 +- .../{ => scene_v1}/dynamics/rigid_body.cc | 118 +- .../{ => scene_v1}/dynamics/rigid_body.h | 67 +- .../node/anim_curve_node.cc | 12 +- .../node/anim_curve_node.h | 12 +- .../{scene => scene_v1}/node/bomb_node.cc | 22 +- .../{scene => scene_v1}/node/bomb_node.h | 18 +- .../{scene => scene_v1}/node/combine_node.cc | 10 +- .../{scene => scene_v1}/node/combine_node.h | 12 +- .../node/explosion_node.cc | 74 +- .../{scene => scene_v1}/node/explosion_node.h | 14 +- .../{scene => scene_v1}/node/flag_node.cc | 179 +- .../{scene => scene_v1}/node/flag_node.h | 34 +- .../{scene => scene_v1}/node/flash_node.cc | 21 +- .../{scene => scene_v1}/node/flash_node.h | 14 +- .../{scene => scene_v1}/node/globals_node.cc | 148 +- .../{scene => scene_v1}/node/globals_node.h | 16 +- .../{scene => scene_v1}/node/image_node.cc | 107 +- .../{scene => scene_v1}/node/image_node.h | 52 +- .../{scene => scene_v1}/node/light_node.cc | 47 +- .../{scene => scene_v1}/node/light_node.h | 20 +- .../{scene => scene_v1}/node/locator_node.cc | 52 +- .../{scene => scene_v1}/node/locator_node.h | 14 +- .../{scene => scene_v1}/node/math_node.cc | 10 +- .../{scene => scene_v1}/node/math_node.h | 12 +- .../{scene => scene_v1}/node/node.cc | 115 +- .../{scene => scene_v1}/node/node.h | 30 +- .../node/node_attribute.cc | 159 +- .../{scene => scene_v1}/node/node_attribute.h | 293 +- .../node/node_attribute_connection.cc | 62 +- .../node/node_attribute_connection.h | 13 +- .../{scene => scene_v1}/node/node_type.h | 12 +- .../{scene => scene_v1}/node/null_node.cc | 8 +- .../{scene => scene_v1}/node/null_node.h | 12 +- .../{scene => scene_v1}/node/player_node.cc | 12 +- .../{scene => scene_v1}/node/player_node.h | 12 +- .../{scene => scene_v1}/node/prop_node.cc | 127 +- .../{scene => scene_v1}/node/prop_node.h | 70 +- .../{scene => scene_v1}/node/region_node.cc | 23 +- .../{scene => scene_v1}/node/region_node.h | 16 +- .../{scene => scene_v1}/node/scorch_node.cc | 28 +- .../{scene => scene_v1}/node/scorch_node.h | 14 +- .../node/session_globals_node.cc | 16 +- .../scene_v1/node/session_globals_node.h | 22 + .../{scene => scene_v1}/node/shield_node.cc | 73 +- .../{scene => scene_v1}/node/shield_node.h | 18 +- .../{scene => scene_v1}/node/sound_node.cc | 55 +- .../{scene => scene_v1}/node/sound_node.h | 18 +- .../{scene => scene_v1}/node/spaz_node.cc | 824 +- .../{scene => scene_v1}/node/spaz_node.h | 198 +- .../{scene => scene_v1}/node/terrain_node.cc | 112 +- .../{scene => scene_v1}/node/terrain_node.h | 44 +- .../{scene => scene_v1}/node/text_node.cc | 116 +- .../{scene => scene_v1}/node/text_node.h | 18 +- .../node/texture_sequence_node.cc | 21 +- .../scene_v1/node/texture_sequence_node.h | 33 + .../node/time_display_node.cc | 24 +- .../node/time_display_node.h | 12 +- .../class/python_class_activity_data.cc | 128 +- .../python/class/python_class_activity_data.h | 42 + .../python/class/python_class_base_timer.cc | 135 + .../python/class/python_class_base_timer.h | 31 + .../python/class/python_class_input_device.cc | 218 +- .../python/class/python_class_input_device.h | 31 +- .../python/class/python_class_material.cc | 244 +- .../python/class/python_class_material.h | 21 +- .../python/class/python_class_node.cc | 179 +- .../python/class/python_class_node.h | 20 +- .../python_class_scene_collision_mesh.cc | 116 + .../class/python_class_scene_collision_mesh.h | 34 + .../class/python_class_scene_data_asset.cc | 141 + .../class/python_class_scene_data_asset.h | 36 + .../python/class/python_class_scene_mesh.cc | 109 + .../python/class/python_class_scene_mesh.h | 34 + .../python/class/python_class_scene_sound.cc | 167 + .../python/class/python_class_scene_sound.h | 37 + .../class/python_class_scene_texture.cc | 107 + .../python/class/python_class_scene_texture.h | 34 + .../python/class/python_class_scene_timer.cc | 141 + .../python/class/python_class_scene_timer.h | 31 + .../python/class/python_class_session_data.cc | 96 +- .../python/class/python_class_session_data.h | 25 +- .../class/python_class_session_player.cc | 252 +- .../class/python_class_session_player.h | 19 +- .../python/methods/python_methods_assets.cc | 343 + .../python/methods/python_methods_assets.h | 20 + .../python/methods/python_methods_input.cc | 346 + .../python/methods/python_methods_input.h | 20 + .../methods/python_methods_networking.cc | 797 ++ .../methods/python_methods_networking.h | 19 + .../python/methods/python_methods_scene.cc | 1654 ++++ .../python/methods/python_methods_scene.h | 20 + .../scene_v1/python/scene_v1_python.cc | 1490 +++ .../scene_v1/python/scene_v1_python.h | 121 + src/ballistica/scene_v1/scene_v1.cc | 189 + src/ballistica/scene_v1/scene_v1.h | 353 + .../support/client_controller_interface.h | 22 + .../scene_v1/support/client_input_device.cc | 24 + .../scene_v1/support/client_input_device.h | 38 + .../support/client_input_device_delegate.cc | 95 + .../support/client_input_device_delegate.h | 33 + .../support}/client_session.cc | 308 +- .../scene_v1/support/client_session.h | 140 + .../support/client_session_net.cc} | 93 +- .../support/client_session_net.h} | 35 +- .../support/client_session_replay.cc} | 76 +- .../scene_v1/support/client_session_replay.h | 43 + .../support}/host_activity.cc | 355 +- .../scene_v1/support/host_activity.h | 123 + .../support}/host_session.cc | 418 +- .../support}/host_session.h | 93 +- .../{logic => scene_v1/support}/player.cc | 98 +- .../{logic => scene_v1/support}/player.h | 87 +- .../support}/player_spec.cc | 44 +- .../{logic => scene_v1/support}/player_spec.h | 14 +- .../{scene => scene_v1/support}/scene.cc | 173 +- .../{scene => scene_v1/support}/scene.h | 78 +- .../scene_v1/support/scene_v1_app_mode.cc | 1463 +++ .../scene_v1/support/scene_v1_app_mode.h | 258 + .../scene_v1/support/scene_v1_context.cc | 111 + .../scene_v1/support/scene_v1_context.h | 84 + .../support/scene_v1_input_device_delegate.cc | 289 + .../support/scene_v1_input_device_delegate.h | 83 + src/ballistica/scene_v1/support/session.cc | 39 + src/ballistica/scene_v1/support/session.h | 46 + .../support/session_stream.cc} | 394 +- .../scene_v1/support/session_stream.h | 165 + src/ballistica/shared/README.md | 8 + src/ballistica/shared/ballistica.cc | 196 + src/ballistica/{ => shared}/ballistica.h | 125 +- .../buildconfig/buildconfig_cmake.h} | 8 +- .../buildconfig/buildconfig_common.h} | 33 +- .../buildconfig/buildconfig_windows_common.h} | 9 +- .../buildconfig/buildconfig_windows_generic.h | 19 + .../buildconfig_windows_headless.h | 18 + .../foundation/event_loop.cc} | 685 +- .../foundation/event_loop.h} | 122 +- .../{core => shared/foundation}/exception.cc | 24 +- .../{core => shared/foundation}/exception.h | 14 +- .../foundation}/fatal_error.cc | 113 +- .../{core => shared/foundation}/fatal_error.h | 12 +- .../foundation/feature_set_front_end.cc | 78 + .../shared/foundation/feature_set_front_end.h | 46 + .../{core => shared/foundation}/inline.cc | 2 +- .../{core => shared/foundation}/inline.h | 17 +- src/ballistica/shared/foundation/logging.cc | 65 + src/ballistica/shared/foundation/logging.h | 46 + src/ballistica/shared/foundation/macros.cc | 140 + src/ballistica/shared/foundation/macros.h | 182 + .../{core => shared/foundation}/object.cc | 263 +- .../{core => shared/foundation}/object.h | 276 +- src/ballistica/shared/foundation/types.h | 321 + src/ballistica/{ => shared}/generic/base64.cc | 2 +- src/ballistica/{ => shared}/generic/base64.h | 6 +- src/ballistica/{ => shared}/generic/buffer.h | 10 +- src/ballistica/{ => shared}/generic/json.cc | 2 +- src/ballistica/{ => shared}/generic/json.h | 8 +- .../{ => shared}/generic/lambda_runnable.h | 18 +- .../{ => shared}/generic/runnable.cc | 2 +- .../{ => shared}/generic/runnable.h | 8 +- .../{ => shared}/generic/timer_list.cc | 64 +- src/ballistica/shared/generic/timer_list.h | 95 + src/ballistica/{ => shared}/generic/utf8.cc | 2 +- src/ballistica/{ => shared}/generic/utf8.h | 8 +- src/ballistica/{ => shared}/generic/utils.cc | 81 +- src/ballistica/{ => shared}/generic/utils.h | 25 +- src/ballistica/{ => shared}/math/matrix44f.cc | 2 +- src/ballistica/{ => shared}/math/matrix44f.h | 8 +- src/ballistica/{ => shared}/math/point2d.h | 6 +- src/ballistica/{ => shared}/math/random.cc | 2 +- src/ballistica/shared/math/random.h | 25 + src/ballistica/{ => shared}/math/rect.h | 6 +- src/ballistica/{ => shared}/math/vector2f.h | 6 +- src/ballistica/{ => shared}/math/vector3f.cc | 2 +- src/ballistica/{ => shared}/math/vector3f.h | 10 +- src/ballistica/{ => shared}/math/vector4f.h | 8 +- .../{ => shared}/networking/networking_sys.h | 8 +- .../{ => shared}/networking/sockaddr.cc | 2 +- .../{ => shared}/networking/sockaddr.h | 10 +- src/ballistica/shared/python/python.cc | 516 ++ src/ballistica/shared/python/python.h | 126 + .../class => shared/python}/python_class.cc | 47 +- src/ballistica/shared/python/python_class.h | 48 + .../{ => shared}/python/python_command.cc | 151 +- .../{ => shared}/python/python_command.h | 28 +- .../shared/python/python_module_builder.h | 72 + .../shared/python/python_object_set.cc | 97 + .../shared/python/python_object_set.h | 107 + .../{ => shared}/python/python_ref.cc | 132 +- .../{ => shared}/python/python_ref.h | 84 +- .../{ => shared}/python/python_sys.h | 14 +- src/ballistica/template_fs/README.md | 3 + .../python/class/python_class_hello.cc | 64 + .../python/class/python_class_hello.h | 45 + .../methods/python_methods_template_fs.cc | 42 + .../methods/python_methods_template_fs.h | 19 + .../template_fs/python/template_fs_python.cc | 52 + .../template_fs/python/template_fs_python.h | 33 + src/ballistica/template_fs/template_fs.cc | 55 + src/ballistica/template_fs/template_fs.h | 59 + src/ballistica/ui/ui.h | 165 - src/ballistica/ui/widget/row_widget.h | 26 - .../python/class/python_class_ui_mesh.cc | 84 + .../ui_v1/python/class/python_class_ui_mesh.h | 50 + .../python/class/python_class_ui_sound.cc | 138 + .../python/class/python_class_ui_sound.h | 57 + .../python/class/python_class_ui_texture.cc | 84 + .../python/class/python_class_ui_texture.h | 51 + .../python/class/python_class_widget.cc | 115 +- .../python/class/python_class_widget.h | 19 +- .../python/methods/python_methods_ui_v1.cc} | 1956 ++-- .../python/methods/python_methods_ui_v1.h | 20 + src/ballistica/ui_v1/python/ui_v1_python.cc | 119 + src/ballistica/ui_v1/python/ui_v1_python.h | 51 + .../{ui => ui_v1/support}/root_ui.cc | 170 +- .../{ui => ui_v1/support}/root_ui.h | 20 +- src/ballistica/ui_v1/ui_v1.cc | 57 + src/ballistica/ui_v1/ui_v1.h | 62 + .../{ui => ui_v1}/widget/button_widget.cc | 293 +- .../{ui => ui_v1}/widget/button_widget.h | 84 +- .../{ui => ui_v1}/widget/check_box_widget.cc | 85 +- .../{ui => ui_v1}/widget/check_box_widget.h | 22 +- .../{ui => ui_v1}/widget/column_widget.cc | 12 +- .../{ui => ui_v1}/widget/column_widget.h | 14 +- .../{ui => ui_v1}/widget/container_widget.cc | 389 +- .../{ui => ui_v1}/widget/container_widget.h | 39 +- .../{ui => ui_v1}/widget/h_scroll_widget.cc | 124 +- .../{ui => ui_v1}/widget/h_scroll_widget.h | 25 +- .../{ui => ui_v1}/widget/image_widget.cc | 103 +- .../{ui => ui_v1}/widget/image_widget.h | 70 +- .../{ui => ui_v1}/widget/root_widget.cc | 166 +- .../{ui => ui_v1}/widget/root_widget.h | 16 +- .../{ui => ui_v1}/widget/row_widget.cc | 12 +- src/ballistica/ui_v1/widget/row_widget.h | 26 + .../{ui => ui_v1}/widget/scroll_widget.cc | 143 +- .../{ui => ui_v1}/widget/scroll_widget.h | 35 +- .../{ui => ui_v1}/widget/stack_widget.cc | 8 +- .../{ui => ui_v1}/widget/stack_widget.h | 12 +- .../{ui => ui_v1}/widget/text_widget.cc | 210 +- .../{ui => ui_v1}/widget/text_widget.h | 30 +- src/ballistica/{ui => ui_v1}/widget/widget.cc | 45 +- src/ballistica/{ui => ui_v1}/widget/widget.h | 92 +- src/external/README.md | 4 + .../ode/ode_collision_space.cpp | 2 +- src/external/windows/include/python/Python.h | 97 +- .../windows/include/python/abstract.h | 2 +- .../windows/include/python/boolobject.h | 4 +- .../windows/include/python/bytearrayobject.h | 4 +- .../windows/include/python/bytesobject.h | 4 +- src/external/windows/include/python/ceval.h | 14 +- src/external/windows/include/python/code.h | 20 - .../windows/include/python/complexobject.h | 45 +- .../windows/include/python/cpython/abstract.h | 210 +- .../include/python/cpython/bytearrayobject.h | 30 +- .../include/python/cpython/bytesobject.h | 25 +- .../include/python/{ => cpython}/cellobject.h | 6 +- .../windows/include/python/cpython/ceval.h | 8 +- .../python/{ => cpython}/classobject.h | 4 +- .../windows/include/python/cpython/code.h | 198 +- .../include/python/cpython/complexobject.h | 44 + .../include/python/{ => cpython}/context.h | 7 +- .../include/python/cpython/descrobject.h | 64 + .../include/python/cpython/dictobject.h | 20 +- .../include/python/cpython/fileutils.h | 166 +- .../include/python/cpython/floatobject.h | 21 + .../include/python/cpython/frameobject.h | 85 +- .../include/python/{ => cpython}/funcobject.h | 15 +- .../include/python/{ => cpython}/genobject.h | 42 +- .../windows/include/python/cpython/import.h | 10 +- .../include/python/cpython/initconfig.h | 8 + .../python/cpython/interpreteridobject.h | 11 - .../include/python/cpython/listobject.h | 29 +- .../python/{ => cpython}/longintrepr.h | 9 +- .../include/python/cpython/longobject.h | 95 + .../include/python/cpython/methodobject.h | 73 +- .../include/python/cpython/modsupport.h | 107 + .../windows/include/python/cpython/object.h | 131 +- .../windows/include/python/cpython/objimpl.h | 15 +- .../include/python/cpython/pthread_stubs.h | 88 + .../windows/include/python/cpython/pydebug.h | 2 +- .../windows/include/python/cpython/pyerrors.h | 53 +- .../windows/include/python/cpython/pyframe.h | 17 + .../include/python/cpython/pylifecycle.h | 7 +- .../windows/include/python/cpython/pystate.h | 129 +- .../windows/include/python/cpython/pythread.h | 42 + .../windows/include/python/cpython/pytime.h | 92 +- .../include/python/cpython/setobject.h | 67 + .../include/python/cpython/sysmodule.h | 4 +- .../include/python/cpython/traceback.h | 10 +- .../include/python/cpython/tupleobject.h | 29 +- .../include/python/cpython/unicodeobject.h | 608 +- .../windows/include/python/cpython/warnings.h | 20 + .../include/python/cpython/weakrefobject.h | 58 + .../windows/include/python/descrobject.h | 82 +- .../windows/include/python/dictobject.h | 2 +- .../include/python/dynamic_annotations.h | 4 +- src/external/windows/include/python/eval.h | 27 - .../windows/include/python/fileobject.h | 2 +- .../windows/include/python/fileutils.h | 2 +- .../windows/include/python/floatobject.h | 96 +- .../windows/include/python/frameobject.h | 2 +- src/external/windows/include/python/import.h | 2 +- .../include/python/internal/pycore_abstract.h | 3 + .../include/python/internal/pycore_asdl.h | 10 +- .../include/python/internal/pycore_ast.h | 17 +- .../python/internal/pycore_ast_state.h | 3 + .../include/python/internal/pycore_atomic.h | 2 +- .../include/python/internal/pycore_bitutils.h | 18 +- .../python/internal/pycore_bytesobject.h | 52 + .../include/python/internal/pycore_call.h | 84 +- .../include/python/internal/pycore_ceval.h | 88 +- .../include/python/internal/pycore_code.h | 562 +- .../include/python/internal/pycore_condvar.h | 6 +- .../include/python/internal/pycore_context.h | 31 +- .../include/python/internal/pycore_dict.h | 178 + .../include/python/internal/pycore_dtoa.h | 9 +- .../internal/pycore_emscripten_signal.h | 25 + .../python/internal/pycore_exceptions.h | 37 + .../python/internal/pycore_fileutils.h | 204 + .../python/internal/pycore_floatobject.h | 59 + .../include/python/internal/pycore_format.h | 2 + .../include/python/internal/pycore_frame.h | 240 + .../include/python/internal/pycore_function.h | 18 + .../include/python/internal/pycore_gc.h | 8 +- .../python/internal/pycore_genobject.h | 49 + .../python/internal/pycore_global_objects.h | 54 + .../python/internal/pycore_global_strings.h | 395 + .../include/python/internal/pycore_hamt.h | 41 +- .../include/python/internal/pycore_import.h | 10 + .../python/internal/pycore_initconfig.h | 7 + .../include/python/internal/pycore_interp.h | 242 +- .../internal/pycore_interpreteridobject.h | 22 + .../include/python/internal/pycore_list.h | 42 + .../include/python/internal/pycore_long.h | 106 +- .../python/internal/pycore_moduleobject.h | 2 +- .../python/internal/pycore_namespace.h | 20 + .../include/python/internal/pycore_object.h | 128 +- .../include/python/internal/pycore_opcode.h | 581 ++ .../python/internal/pycore_pathconfig.h | 57 +- .../include/python/internal/pycore_pyerrors.h | 30 +- .../python/internal/pycore_pylifecycle.h | 52 +- .../include/python/internal/pycore_pymath.h | 224 + .../include/python/internal/pycore_pymem.h | 15 +- .../include/python/internal/pycore_pystate.h | 60 +- .../include/python/internal/pycore_runtime.h | 54 +- .../python/internal/pycore_runtime_init.h | 1256 +++ .../include/python/internal/pycore_signal.h | 35 + .../python/internal/pycore_sliceobject.h | 20 + .../include/python/internal/pycore_strhex.h | 36 + .../python/internal/pycore_structseq.h | 7 + .../include/python/internal/pycore_symtable.h | 1 + .../python/internal/pycore_sysmodule.h | 2 + .../python/internal/pycore_traceback.h | 16 +- .../include/python/internal/pycore_tuple.h | 55 +- .../python/internal/pycore_typeobject.h | 50 + .../python/internal/pycore_unicodeobject.h | 62 + .../python/internal/pycore_unionobject.h | 1 + .../include/python/internal/pycore_warnings.h | 4 + .../include/python/interpreteridobject.h | 17 - .../windows/include/python/listobject.h | 2 +- .../windows/include/python/longobject.h | 135 +- src/external/windows/include/python/marshal.h | 19 +- .../windows/include/python/memoryobject.h | 4 +- .../windows/include/python/methodobject.h | 28 +- .../windows/include/python/modsupport.h | 113 +- .../windows/include/python/moduleobject.h | 25 +- .../windows/include/python/namespaceobject.h | 19 - src/external/windows/include/python/object.h | 190 +- src/external/windows/include/python/objimpl.h | 6 +- src/external/windows/include/python/opcode.h | 378 +- .../windows/include/python/patchlevel.h | 6 +- .../windows/include/python/py_curses.h | 2 +- .../windows/include/python/pybuffer.h | 142 + .../windows/include/python/pyconfig.h | 78 +- .../windows/include/python/pyerrors.h | 9 +- src/external/windows/include/python/pyframe.h | 8 +- src/external/windows/include/python/pyhash.h | 8 +- .../windows/include/python/pylifecycle.h | 12 +- src/external/windows/include/python/pymacro.h | 23 + src/external/windows/include/python/pymath.h | 205 +- src/external/windows/include/python/pymem.h | 2 +- src/external/windows/include/python/pyport.h | 365 +- src/external/windows/include/python/pystate.h | 30 +- .../windows/include/python/pystrhex.h | 22 - .../windows/include/python/pystrtod.h | 1 + .../windows/include/python/pythonrun.h | 3 +- .../windows/include/python/pythread.h | 58 +- .../windows/include/python/pytypedefs.h | 30 + .../windows/include/python/setobject.h | 73 +- .../windows/include/python/structmember.h | 8 +- .../windows/include/python/structseq.h | 4 +- .../windows/include/python/sysmodule.h | 16 +- .../windows/include/python/traceback.h | 2 +- .../windows/include/python/tupleobject.h | 2 +- .../windows/include/python/typeslots.h | 6 - .../windows/include/python/unicodeobject.h | 10 +- .../windows/include/python/warnings.h | 32 +- .../windows/include/python/weakrefobject.h | 56 +- src/external/windows/lib/Win32/python310.lib | Bin 363654 -> 0 bytes .../windows/lib/Win32/python310_d.lib | Bin 367998 -> 0 bytes src/external/windows/lib/Win32/python311.lib | Bin 0 -> 368002 bytes .../windows/lib/Win32/python311_d.lib | Bin 0 -> 372392 bytes src/external/windows/lib/x64/python310.lib | Bin 355924 -> 0 bytes src/external/windows/lib/x64/python310_d.lib | Bin 360248 -> 0 bytes src/external/windows/lib/x64/python311.lib | Bin 0 -> 360148 bytes src/external/windows/lib/x64/python311_d.lib | Bin 0 -> 364518 bytes src/meta/.meta_manifest_public.json | 15 +- src/meta/Makefile | 77 +- src/meta/README.md | 7 +- src/meta/{bameta => babasemeta}/__init__.py | 0 src/meta/babasemeta/pyembed/__init__.py | 3 + src/meta/babasemeta/pyembed/binding_base.py | 88 + .../__init__.py | 0 src/meta/baclassicmeta/pyembed/__init__.py | 3 + .../baclassicmeta/pyembed/binding_classic.py | 15 + src/meta/bacoremeta/__init__.py | 3 + src/meta/bacoremeta/pyembed/__init__.py | 3 + src/meta/bacoremeta/pyembed/binding_core.py | 26 + src/meta/bacoremeta/pyembed/env.py | 56 + src/meta/bameta/python_embedded/binding.py | 150 - src/meta/bameta/python_embedded/bootstrap.py | 32 - .../python_embedded/bootstrap_monolithic.py | 22 - src/meta/bascenev1meta/__init__.py | 3 + src/meta/bascenev1meta/pyembed/__init__.py | 3 + .../bascenev1meta/pyembed/binding_scene_v1.py | 34 + src/meta/batemplatefsmeta/__init__.py | 3 + src/meta/batemplatefsmeta/pyembed/__init__.py | 3 + .../pyembed/binding_template_fs.py | 14 + src/meta/bauiv1meta/__init__.py | 3 + src/meta/bauiv1meta/pyembed/__init__.py | 3 + src/meta/bauiv1meta/pyembed/binding_ui_v1.py | 25 + src/resources/Makefile | 48 + {resources => src/resources}/README.md | 2 +- tests/{test_ba => test_babase}/__init__.py | 0 .../test_assetmanager.py | 6 +- tests/test_efro/test_message.py | 1 - tests/test_efro/test_rpc.py | 6 - tools/README.md | 8 + tools/bacloud | 3 +- tools/bacommon/transfer.py | 2 +- tools/batools/androidsdkutils.py | 5 +- tools/batools/assetsmakefile.py | 315 +- tools/batools/assetstaging.py | 66 +- tools/batools/build.py | 146 +- tools/batools/docs.py | 9 +- tools/batools/dummymodule.py | 708 +- tools/batools/featureset.py | 268 + tools/batools/{meta.py => metabuild.py} | 76 +- tools/batools/metamakefile.py | 633 +- tools/batools/pcommand.py | 371 +- tools/batools/pcommand2.py | 304 + tools/batools/project.py | 800 -- tools/batools/project/__init__.py | 12 + tools/batools/project/_checks.py | 487 + tools/batools/project/_updater.py | 689 ++ tools/batools/pruneincludes.py | 209 + tools/batools/pythonenumsmodule.py | 2 - tools/batools/resourcesmakefile.py | 923 +- tools/batools/spinoff/__init__.py | 23 + tools/batools/spinoff/_config_template.py | 59 + tools/batools/spinoff/_context.py | 2031 ++++ tools/batools/spinoff/_main.py | 338 + tools/batools/spinoff/_state.py | 68 + tools/batools/toplevelmakefile.py | 98 + tools/batools/version.py | 32 +- tools/batools/xcodeproject.py | 428 + tools/efro/cloudshell.py | 2 +- tools/efro/dataclassio/_base.py | 2 - tools/efro/dataclassio/_inputter.py | 7 +- tools/efro/dataclassio/_outputter.py | 4 +- tools/efro/dataclassio/_pathcapture.py | 1 - tools/efro/dataclassio/_prep.py | 7 +- tools/efro/debug.py | 3 +- tools/efro/error.py | 2 - tools/efro/log.py | 82 +- tools/efro/message/_protocol.py | 4 +- tools/efro/message/_receiver.py | 3 +- tools/efro/message/_sender.py | 2 - tools/efro/rpc.py | 9 +- tools/efro/util.py | 120 +- tools/efrotools/__init__.py | 77 +- tools/efrotools/buildlock.py | 48 + tools/efrotools/code.py | 73 +- tools/efrotools/efrocache.py | 8 +- tools/efrotools/filecommand.py | 2 - tools/efrotools/ios.py | 23 +- tools/efrotools/{build.py => lazybuild.py} | 202 +- tools/efrotools/makefile.py | 3 - tools/efrotools/pcommand.py | 151 +- tools/efrotools/pcommand2.py | 31 + tools/efrotools/pybuild.py | 1564 ++-- tools/efrotools/pylintplugins.py | 5 +- tools/efrotools/sync.py | 2 - tools/efrotools/{xcode.py => xcodebuild.py} | 618 +- tools/pcommand | 31 +- tools/spinoff | 13 + 1305 files changed, 102706 insertions(+), 83000 deletions(-) rename .idea/{ballisticacore.iml => ballisticakit.iml} (62%) delete mode 100644 assets/.asset_manifest_public.json delete mode 100644 assets/Makefile delete mode 100644 assets/src/ba_data/python/._ba_sources_hash delete mode 100644 assets/src/ba_data/python/._bainternal_sources_hash delete mode 100644 assets/src/ba_data/python/_ba.py delete mode 100644 assets/src/ba_data/python/_bainternal.py delete mode 100644 assets/src/ba_data/python/ba/__init__.py delete mode 100644 assets/src/ba_data/python/ba/_analytics.py delete mode 100644 assets/src/ba_data/python/ba/_app.py delete mode 100644 assets/src/ba_data/python/ba/_appmode.py delete mode 100644 assets/src/ba_data/python/ba/_bootstrap.py delete mode 100644 assets/src/ba_data/python/ba/_hooks.py delete mode 100644 assets/src/ba_data/python/ba/_internal.py delete mode 100644 assets/src/ba_data/python/ba/_store.py delete mode 100644 assets/src/ba_data/python/ba/internal.py delete mode 100644 assets/src/ba_data/python/bastd/appdelegate.py delete mode 100644 assets/src/ba_data/python/bastd/stdmap.py delete mode 100644 assets/src/ba_data/python/bastd/ui/appinvite.py delete mode 100644 assets/src/ba_data/python/bastd/ui/settings/xbox360controller.py delete mode 100644 assets/src/ba_data/python/bastd/ui/telnet.py delete mode 100644 assets/src/server/launch_ballisticacore_server.bat delete mode 100644 ballisticacore-cmake/.idea/.name delete mode 100644 ballisticacore-cmake/.idea/BallisticaCore.iml delete mode 100644 ballisticacore-cmake/.idea/scopes/External.xml delete mode 100644 ballisticacore-cmake/.idea/scopes/Ignored.xml delete mode 100644 ballisticacore-cmake/CMakeLists.txt delete mode 100644 ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj delete mode 100644 ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj.filters delete mode 100644 ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj delete mode 100644 ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj.filters rename {ballisticacore-cmake => ballisticakit-cmake}/.idea/.gitignore (100%) create mode 100644 ballisticakit-cmake/.idea/.name create mode 100644 ballisticakit-cmake/.idea/BallisticaKit.iml rename {ballisticacore-cmake => ballisticakit-cmake}/.idea/cmake.xml (100%) rename {ballisticacore-cmake => ballisticakit-cmake}/.idea/codeStyles/Project.xml (85%) rename {ballisticacore-cmake => ballisticakit-cmake}/.idea/codeStyles/codeStyleConfig.xml (100%) rename {ballisticacore-cmake => ballisticakit-cmake}/.idea/dictionaries/ericf.xml (96%) rename {ballisticacore-cmake => ballisticakit-cmake}/.idea/inspectionProfiles/Project_Default.xml (100%) rename {ballisticacore-cmake => ballisticakit-cmake}/.idea/misc.xml (86%) rename {ballisticacore-cmake => ballisticakit-cmake}/.idea/modules.xml (56%) create mode 100644 ballisticakit-cmake/.idea/runConfigurations/ballisticakit.xml create mode 100644 ballisticakit-cmake/.idea/scopes/External.xml create mode 100644 ballisticakit-cmake/.idea/scopes/Ignored.xml rename {ballisticacore-cmake => ballisticakit-cmake}/.idea/vcs.xml (100%) create mode 100644 ballisticakit-cmake/CMakeLists.txt rename ballisticacore-windows/Generic/BallisticaCore.rc => ballisticakit-windows/Generic/BallisticaKit.rc (91%) create mode 100644 ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj create mode 100644 ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters rename {ballisticacore-windows => ballisticakit-windows}/Generic/Resource.h (78%) rename {ballisticacore-windows => ballisticakit-windows}/Generic/stdafx.cpp (100%) rename {ballisticacore-windows => ballisticakit-windows}/Generic/stdafx.h (71%) rename {ballisticacore-windows => ballisticakit-windows}/Generic/targetver.h (100%) create mode 100644 ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj create mode 100644 ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters rename {ballisticacore-windows => ballisticakit-windows}/Headless/stdafx.cpp (100%) rename {ballisticacore-windows => ballisticakit-windows}/Headless/stdafx.h (70%) rename {ballisticacore-windows => ballisticakit-windows}/Headless/targetver.h (100%) delete mode 100644 config/config.json create mode 100644 config/featuresets/README.md create mode 100644 config/featuresets/featureset_base.py create mode 100644 config/featuresets/featureset_classic.py create mode 100644 config/featuresets/featureset_core.py create mode 100644 config/featuresets/featureset_plus.py create mode 100644 config/featuresets/featureset_scene_v1.py create mode 100644 config/featuresets/featureset_template_fs.py create mode 100644 config/featuresets/featureset_ui_v1.py create mode 100644 config/projectconfig.json create mode 100644 config/spinoffconfig.py rename config/toolconfigsrc/{README => README.md} (90%) delete mode 100644 resources/Makefile create mode 100644 src/README.md rename {assets => src/assets}/.asset_manifest_private.json (55%) create mode 100644 src/assets/.asset_manifest_public.json create mode 100644 src/assets/Makefile create mode 100644 src/assets/README.md create mode 100644 src/assets/ba_data/python/babase/__init__.py rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_accountv2.py (85%) create mode 100644 src/assets/ba_data/python/babase/_app.py rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_appcomponent.py (91%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_appconfig.py (78%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_apputils.py (69%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_assetmanager.py (98%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_asyncio.py (87%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_cloud.py (95%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_error.py (73%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_general.py (87%) create mode 100644 src/assets/ba_data/python/babase/_hooks.py rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_keyboard.py (91%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_language.py (90%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_login.py (90%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_math.py (100%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_meta.py (94%) create mode 100644 src/assets/ba_data/python/babase/_net.py rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_plugin.py (78%) create mode 100644 src/assets/ba_data/python/babase/_text.py rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/babase}/_workspace.py (87%) create mode 100644 src/assets/ba_data/python/babase/internal.py create mode 100644 src/assets/ba_data/python/baclassic/__init__.py rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_accountv1.py (68%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_achievement.py (87%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_ads.py (69%) create mode 100644 src/assets/ba_data/python/baclassic/_analytics.py rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_appdelegate.py (52%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_benchmark.py (55%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_campaign.py (92%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_input.py (91%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_level.py (85%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_lobby.py (85%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_music.py (81%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_net.py (53%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_profile.py (91%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_servermode.py (78%) create mode 100644 src/assets/ba_data/python/baclassic/_store.py create mode 100644 src/assets/ba_data/python/baclassic/_subsystem.py rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_tips.py (90%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_tournament.py (84%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/_ui.py (85%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/macmusicapp.py (82%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/baclassic}/osmusic.py (87%) create mode 100644 src/assets/ba_data/python/baenv.py create mode 100644 src/assets/ba_data/python/baplus/__init__.py create mode 100644 src/assets/ba_data/python/baplus/_hooks.py create mode 100644 src/assets/ba_data/python/baplus/_subsystem.py create mode 100644 src/assets/ba_data/python/bascenev1/__init__.py rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_activity.py (79%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_activitytypes.py (78%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_actor.py (62%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_collision.py (58%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_coopgame.py (76%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_coopsession.py (82%) create mode 100644 src/assets/ba_data/python/bascenev1/_debug.py rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_dependency.py (83%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_dualteamsession.py (76%) create mode 100644 src/assets/ba_data/python/bascenev1/_featureset.py rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_freeforallsession.py (86%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_gameactivity.py (81%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_gameresults.py (75%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_gameutils.py (54%) create mode 100644 src/assets/ba_data/python/bascenev1/_hooks.py rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_map.py (77%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_messages.py (84%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_multiteamsession.py (82%) create mode 100644 src/assets/ba_data/python/bascenev1/_music.py rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_nodeactor.py (77%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_player.py (72%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_playlist.py (96%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_powerup.py (62%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_score.py (93%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_session.py (78%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_settings.py (100%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_stats.py (81%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_team.py (74%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bascenev1}/_teamgame.py (75%) create mode 100644 src/assets/ba_data/python/bascenev1/internal.py rename {assets/src => src/assets}/ba_data/python/bastd/__init__.py (82%) rename {assets/src => src/assets}/ba_data/python/bastd/activity/__init__.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/activity/coopjoin.py (77%) rename {assets/src => src/assets}/ba_data/python/bastd/activity/coopscore.py (79%) rename {assets/src => src/assets}/ba_data/python/bastd/activity/drawscore.py (81%) rename {assets/src => src/assets}/ba_data/python/bastd/activity/dualteamscore.py (80%) rename {assets/src => src/assets}/ba_data/python/bastd/activity/freeforallvictory.py (85%) rename {assets/src => src/assets}/ba_data/python/bastd/activity/multiteamjoin.py (80%) rename {assets/src => src/assets}/ba_data/python/bastd/activity/multiteamscore.py (83%) rename {assets/src => src/assets}/ba_data/python/bastd/activity/multiteamvictory.py (86%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/__init__.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/background.py (80%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/bomb.py (75%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/controlsguide.py (82%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/flag.py (83%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/image.py (82%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/onscreencountdown.py (69%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/onscreentimer.py (57%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/playerspaz.py (80%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/popuptext.py (87%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/powerupbox.py (60%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/respawnicon.py (72%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/scoreboard.py (88%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/spawner.py (89%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/spaz.py (83%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/spazappearance.py (70%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/spazbot.py (85%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/spazfactory.py (53%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/text.py (87%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/tipstext.py (76%) rename {assets/src => src/assets}/ba_data/python/bastd/actor/zoomtext.py (82%) rename {assets/src => src/assets}/ba_data/python/bastd/game/__init__.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/game/assault.py (78%) rename {assets/src => src/assets}/ba_data/python/bastd/game/capturetheflag.py (84%) rename {assets/src => src/assets}/ba_data/python/bastd/game/chosenone.py (79%) rename {assets/src => src/assets}/ba_data/python/bastd/game/conquest.py (87%) rename {assets/src => src/assets}/ba_data/python/bastd/game/deathmatch.py (82%) rename {assets/src => src/assets}/ba_data/python/bastd/game/easteregghunt.py (80%) rename {assets/src => src/assets}/ba_data/python/bastd/game/elimination.py (86%) rename {assets/src => src/assets}/ba_data/python/bastd/game/football.py (83%) rename {assets/src => src/assets}/ba_data/python/bastd/game/hockey.py (81%) rename {assets/src => src/assets}/ba_data/python/bastd/game/keepaway.py (78%) rename {assets/src => src/assets}/ba_data/python/bastd/game/kingofthehill.py (79%) rename {assets/src => src/assets}/ba_data/python/bastd/game/meteorshower.py (82%) rename {assets/src => src/assets}/ba_data/python/bastd/game/ninjafight.py (82%) rename {assets/src => src/assets}/ba_data/python/bastd/game/onslaught.py (91%) rename {assets/src => src/assets}/ba_data/python/bastd/game/race.py (82%) rename {assets/src => src/assets}/ba_data/python/bastd/game/runaround.py (87%) rename {assets/src => src/assets}/ba_data/python/bastd/game/targetpractice.py (79%) rename {assets/src => src/assets}/ba_data/python/bastd/game/thelaststand.py (86%) rename {assets/src => src/assets}/ba_data/python/bastd/gameutils.py (62%) rename {assets/src => src/assets}/ba_data/python/bastd/keyboard/__init__.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/keyboard/englishkeyboard.py (92%) rename {assets/src => src/assets}/ba_data/python/bastd/mainmenu.py (78%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/__init__.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/big_g.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/bridgit.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/courtyard.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/crag_castle.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/doom_shroom.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/football_stadium.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/happy_thoughts.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/hockey_stadium.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/lake_frigid.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/monkey_face.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/rampage.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/roundabout.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/step_right_up.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/the_pad.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/tip_top.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/tower_d.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/mapdata/zig_zag.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/maps.py (64%) rename {assets/src => src/assets}/ba_data/python/bastd/session/__init__.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/tutorial.py (90%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/__init__.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/account/__init__.py (59%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/account/link.py (71%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/account/settings.py (70%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/account/unlink.py (69%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/account/v2proxy.py (71%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/account/viewer.py (78%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/achievements.py (80%) create mode 100644 src/assets/ba_data/python/bastd/ui/appinvite.py rename {assets/src => src/assets}/ba_data/python/bastd/ui/characterpicker.py (78%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/colorpicker.py (78%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/config.py (78%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/configerror.py (65%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/confirm.py (70%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/continues.py (71%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/coop/__init__.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/coop/browser.py (72%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/coop/gamebutton.py (77%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/coop/level.py (67%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/coop/tournamentbutton.py (77%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/creditslist.py (72%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/debug.py (70%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/feedback.py (61%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/fileselector.py (78%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/gather/__init__.py (71%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/gather/abouttab.py (77%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/gather/manualtab.py (66%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/gather/nearbytab.py (73%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/gather/privatetab.py (79%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/gather/publictab.py (80%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/getcurrency.py (71%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/getremote.py (69%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/helpui.py (75%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/iconpicker.py (78%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/kiosk.py (73%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/league/__init__.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/league/rankbutton.py (76%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/league/rankwindow.py (73%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/mainmenu.py (74%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/party.py (75%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/partyqueue.py (77%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/play.py (77%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/playlist/__init__.py (68%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/playlist/addgame.py (70%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/playlist/browser.py (77%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/playlist/customizebrowser.py (70%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/playlist/edit.py (70%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/playlist/editcontroller.py (79%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/playlist/editgame.py (77%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/playlist/mapselect.py (70%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/playlist/share.py (63%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/playoptions.py (74%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/popup.py (87%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/profile/__init__.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/profile/browser.py (67%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/profile/edit.py (73%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/profile/upgrade.py (66%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/promocode.py (72%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/purchase.py (65%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/qrcode.py (69%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/radiogroup.py (77%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/report.py (65%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/resourcetypeinfo.py (68%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/serverdialog.py (70%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/__init__.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/advanced.py (72%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/allsettings.py (65%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/audio.py (70%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/controls.py (67%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/gamepad.py (80%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/gamepadadvanced.py (74%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/gamepadselect.py (62%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/graphics.py (68%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/keyboard.py (69%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/nettesting.py (78%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/plugins.py (67%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/pluginsettings.py (64%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/remoteapp.py (70%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/testing.py (64%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/touchscreen.py (73%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/settings/vrtesting.py (90%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/soundtrack/__init__.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/soundtrack/browser.py (72%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/soundtrack/edit.py (68%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py (70%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/soundtrack/macmusicapp.py (68%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/specialoffer.py (71%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/store/__init__.py (100%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/store/browser.py (75%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/store/button.py (73%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/store/item.py (80%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/tabs.py (88%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/teamnamescolors.py (73%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/tournamententry.py (71%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/tournamentscores.py (76%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/trophies.py (84%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/url.py (70%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/v2upgrade.py (67%) rename {assets/src => src/assets}/ba_data/python/bastd/ui/watch.py (65%) create mode 100644 src/assets/ba_data/python/batemplatefs/__init__.py create mode 100644 src/assets/ba_data/python/batemplatefs/_hooks.py create mode 100644 src/assets/ba_data/python/batemplatefs/_subsystem.py create mode 100644 src/assets/ba_data/python/bauiv1/__init__.py create mode 100644 src/assets/ba_data/python/bauiv1/_hooks.py rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bauiv1}/modutils.py (75%) rename {assets/src/ba_data/python/bastd/ui => src/assets/ba_data/python/bauiv1}/onscreenkeyboard.py (70%) rename {assets/src/ba_data/python/ba => src/assets/ba_data/python/bauiv1}/ui/__init__.py (87%) rename {assets/src => src/assets}/pdoc/templates/custom.css (100%) rename {assets/src => src/assets}/pdoc/templates/index.html.jinja2 (100%) rename {assets/src => src/assets}/pdoc/templates/module.html.jinja2 (100%) rename {assets/src => src/assets}/server/README.txt (86%) rename assets/src/server/ballisticacore_server.py => src/assets/server/ballisticakit_server.py (98%) rename {assets/src => src/assets}/server/config_template.yaml (73%) create mode 100644 src/assets/server/launch_ballisticakit_server.bat create mode 100644 src/ballistica/README.md delete mode 100644 src/ballistica/app/app.cc delete mode 100644 src/ballistica/app/app.h delete mode 100644 src/ballistica/app/app_flavor.cc delete mode 100644 src/ballistica/app/app_flavor_headless.cc delete mode 100644 src/ballistica/app/app_flavor_headless.h delete mode 100644 src/ballistica/app/app_flavor_vr.cc delete mode 100644 src/ballistica/app/app_flavor_vr.h delete mode 100644 src/ballistica/app/stress_test.cc delete mode 100644 src/ballistica/app/stress_test.h delete mode 100644 src/ballistica/assets/assets.cc delete mode 100644 src/ballistica/assets/assets.h delete mode 100644 src/ballistica/assets/assets_server.h delete mode 100644 src/ballistica/assets/component/asset_component.cc delete mode 100644 src/ballistica/assets/component/asset_component.h delete mode 100644 src/ballistica/assets/component/collide_model.cc delete mode 100644 src/ballistica/assets/component/collide_model.h delete mode 100644 src/ballistica/assets/component/cube_map_texture.cc delete mode 100644 src/ballistica/assets/component/cube_map_texture.h delete mode 100644 src/ballistica/assets/component/data.cc delete mode 100644 src/ballistica/assets/component/data.h delete mode 100644 src/ballistica/assets/component/model.cc delete mode 100644 src/ballistica/assets/component/model.h delete mode 100644 src/ballistica/assets/component/sound.cc delete mode 100644 src/ballistica/assets/component/sound.h delete mode 100644 src/ballistica/assets/component/texture.cc delete mode 100644 src/ballistica/assets/component/texture.h delete mode 100644 src/ballistica/assets/data/collide_model_data.h delete mode 100644 src/ballistica/assets/data/data_data.h delete mode 100644 src/ballistica/assets/data/model_renderer_data.h delete mode 100644 src/ballistica/assets/data/sound_data.h delete mode 100644 src/ballistica/assets/data/texture_data.h delete mode 100644 src/ballistica/assets/data/texture_renderer_data.h delete mode 100644 src/ballistica/ballistica.cc create mode 100644 src/ballistica/base/README.md create mode 100644 src/ballistica/base/app/app.cc rename src/ballistica/{app/app_flavor.h => base/app/app.h} (50%) rename src/ballistica/{ => base}/app/app_config.cc (88%) rename src/ballistica/{ => base}/app/app_config.h (85%) create mode 100644 src/ballistica/base/app/app_headless.cc create mode 100644 src/ballistica/base/app/app_headless.h create mode 100644 src/ballistica/base/app/app_mode.cc create mode 100644 src/ballistica/base/app/app_mode.h create mode 100644 src/ballistica/base/app/app_mode_empty.cc create mode 100644 src/ballistica/base/app/app_mode_empty.h create mode 100644 src/ballistica/base/app/app_vr.cc create mode 100644 src/ballistica/base/app/app_vr.h rename src/ballistica/{platform/sdl => base/app}/sdl_app.cc (69%) create mode 100644 src/ballistica/base/app/sdl_app.h create mode 100644 src/ballistica/base/app/stress_test.cc create mode 100644 src/ballistica/base/app/stress_test.h rename src/ballistica/{assets/data/asset_component_data.cc => base/assets/asset.cc} (64%) rename src/ballistica/{assets/data/asset_component_data.h => base/assets/asset.h} (78%) create mode 100644 src/ballistica/base/assets/assets.cc create mode 100644 src/ballistica/base/assets/assets.h rename src/ballistica/{ => base}/assets/assets_server.cc (59%) create mode 100644 src/ballistica/base/assets/assets_server.h rename src/ballistica/{assets/data/collide_model_data.cc => base/assets/collision_mesh_asset.cc} (70%) create mode 100644 src/ballistica/base/assets/collision_mesh_asset.h rename src/ballistica/{assets/data/data_data.cc => base/assets/data_asset.cc} (50%) create mode 100644 src/ballistica/base/assets/data_asset.h rename src/ballistica/{assets/data/model_data.cc => base/assets/mesh_asset.cc} (75%) rename src/ballistica/{assets/data/model_data.h => base/assets/mesh_asset.h} (53%) create mode 100644 src/ballistica/base/assets/mesh_asset_renderer_data.h rename src/ballistica/{assets/data/sound_data.cc => base/assets/sound_asset.cc} (83%) create mode 100644 src/ballistica/base/assets/sound_asset.h rename src/ballistica/{assets/data/texture_data.cc => base/assets/texture_asset.cc} (82%) create mode 100644 src/ballistica/base/assets/texture_asset.h rename src/ballistica/{assets/data/texture_preload_data.cc => base/assets/texture_asset_preload_data.cc} (97%) rename src/ballistica/{assets/data/texture_preload_data.h => base/assets/texture_asset_preload_data.h} (67%) create mode 100644 src/ballistica/base/assets/texture_asset_renderer_data.h rename src/ballistica/{ => base}/audio/al_sys.cc (84%) rename src/ballistica/{ => base}/audio/al_sys.h (74%) rename src/ballistica/{ => base}/audio/audio.cc (63%) rename src/ballistica/{ => base}/audio/audio.h (60%) rename src/ballistica/{ => base}/audio/audio_server.cc (81%) rename src/ballistica/{ => base}/audio/audio_server.h (52%) rename src/ballistica/{ => base}/audio/audio_source.cc (61%) rename src/ballistica/{ => base}/audio/audio_source.h (77%) rename src/ballistica/{ => base}/audio/audio_streamer.cc (92%) rename src/ballistica/{ => base}/audio/audio_streamer.h (74%) rename src/ballistica/{ => base}/audio/ogg_stream.cc (93%) rename src/ballistica/{ => base}/audio/ogg_stream.h (75%) create mode 100644 src/ballistica/base/base.cc create mode 100644 src/ballistica/base/base.h rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics.cc (65%) rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics.h (78%) rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics_draw_snapshot.h (61%) rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics_fuse.cc (57%) rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics_fuse.h (50%) rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics_fuse_data.h (90%) rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics_height_cache.cc (97%) rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics_height_cache.h (71%) rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics_server.cc (91%) rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics_server.h (65%) rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics_shadow.cc (57%) rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics_shadow.h (73%) rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics_shadow_data.h (82%) rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics_volume_light.cc (59%) rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics_volume_light.h (53%) rename src/ballistica/{ => base}/dynamics/bg/bg_dynamics_volume_light_data.h (58%) rename src/ballistica/{ => base}/dynamics/collision_cache.cc (91%) rename src/ballistica/{ => base}/dynamics/collision_cache.h (61%) rename src/ballistica/{ => base}/graphics/component/empty_component.h (62%) rename src/ballistica/{ => base}/graphics/component/object_component.cc (82%) rename src/ballistica/{ => base}/graphics/component/object_component.h (80%) rename src/ballistica/{ => base}/graphics/component/post_process_component.cc (77%) rename src/ballistica/{ => base}/graphics/component/post_process_component.h (59%) create mode 100644 src/ballistica/base/graphics/component/render_component.cc rename src/ballistica/{ => base}/graphics/component/render_component.h (91%) rename src/ballistica/{ => base}/graphics/component/shield_component.cc (53%) create mode 100644 src/ballistica/base/graphics/component/shield_component.h rename src/ballistica/{ => base}/graphics/component/simple_component.cc (86%) rename src/ballistica/{ => base}/graphics/component/simple_component.h (73%) rename src/ballistica/{ => base}/graphics/component/smoke_component.cc (56%) rename src/ballistica/{ => base}/graphics/component/smoke_component.h (68%) rename src/ballistica/{ => base}/graphics/component/special_component.cc (60%) rename src/ballistica/{ => base}/graphics/component/special_component.h (57%) rename src/ballistica/{ => base}/graphics/component/sprite_component.cc (72%) rename src/ballistica/{ => base}/graphics/component/sprite_component.h (63%) rename src/ballistica/{ => base}/graphics/gl/gl_sys.cc (96%) rename src/ballistica/{ => base}/graphics/gl/gl_sys.h (95%) rename src/ballistica/{ => base}/graphics/gl/renderer_gl.cc (95%) rename src/ballistica/{ => base}/graphics/gl/renderer_gl.h (92%) rename src/ballistica/{ => base}/graphics/graphics.cc (72%) rename src/ballistica/{ => base}/graphics/graphics.h (66%) rename src/ballistica/{ => base}/graphics/graphics_server.cc (69%) rename src/ballistica/{ => base}/graphics/graphics_server.h (67%) rename src/ballistica/{graphics/vr_graphics.cc => base/graphics/graphics_vr.cc} (81%) rename src/ballistica/{graphics/vr_graphics.h => base/graphics/graphics_vr.h} (69%) rename src/ballistica/{ => base}/graphics/mesh/image_mesh.cc (79%) rename src/ballistica/{ => base}/graphics/mesh/image_mesh.h (68%) rename src/ballistica/{ => base}/graphics/mesh/mesh.h (71%) rename src/ballistica/{ => base}/graphics/mesh/mesh_buffer.h (63%) rename src/ballistica/{ => base}/graphics/mesh/mesh_buffer_base.h (63%) create mode 100644 src/ballistica/base/graphics/mesh/mesh_buffer_vertex_simple_full.h create mode 100644 src/ballistica/base/graphics/mesh/mesh_buffer_vertex_smoke_full.h create mode 100644 src/ballistica/base/graphics/mesh/mesh_buffer_vertex_sprite.h rename src/ballistica/{ => base}/graphics/mesh/mesh_data.cc (59%) rename src/ballistica/{ => base}/graphics/mesh/mesh_data.h (78%) create mode 100644 src/ballistica/base/graphics/mesh/mesh_data_client_handle.cc rename src/ballistica/{ => base}/graphics/mesh/mesh_data_client_handle.h (55%) create mode 100644 src/ballistica/base/graphics/mesh/mesh_index_buffer_16.h create mode 100644 src/ballistica/base/graphics/mesh/mesh_index_buffer_32.h rename src/ballistica/{ => base}/graphics/mesh/mesh_indexed.h (73%) rename src/ballistica/{ => base}/graphics/mesh/mesh_indexed_base.h (74%) create mode 100644 src/ballistica/base/graphics/mesh/mesh_indexed_dual_texture_full.h rename src/ballistica/{ => base}/graphics/mesh/mesh_indexed_object_split.h (57%) create mode 100644 src/ballistica/base/graphics/mesh/mesh_indexed_simple_full.h rename src/ballistica/{ => base}/graphics/mesh/mesh_indexed_simple_split.h (56%) create mode 100644 src/ballistica/base/graphics/mesh/mesh_indexed_smoke_full.h rename src/ballistica/{ => base}/graphics/mesh/mesh_indexed_static_dynamic.h (77%) rename src/ballistica/{ => base}/graphics/mesh/mesh_non_indexed.h (76%) create mode 100644 src/ballistica/base/graphics/mesh/mesh_renderer_data.h create mode 100644 src/ballistica/base/graphics/mesh/sprite_mesh.h rename src/ballistica/{ => base}/graphics/mesh/text_mesh.cc (95%) rename src/ballistica/{ => base}/graphics/mesh/text_mesh.h (72%) create mode 100644 src/ballistica/base/graphics/renderer/framebuffer.h rename src/ballistica/{graphics => base/graphics/renderer}/render_pass.cc (82%) rename src/ballistica/{graphics => base/graphics/renderer}/render_pass.h (94%) rename src/ballistica/{graphics => base/graphics/renderer}/render_target.cc (50%) rename src/ballistica/{graphics => base/graphics/renderer}/render_target.h (71%) rename src/ballistica/{graphics => base/graphics/renderer}/renderer.cc (91%) rename src/ballistica/{graphics => base/graphics/renderer}/renderer.h (74%) rename src/ballistica/{graphics => base/graphics/support}/area_of_interest.cc (56%) rename src/ballistica/{graphics => base/graphics/support}/area_of_interest.h (71%) rename src/ballistica/{graphics => base/graphics/support}/camera.cc (95%) rename src/ballistica/{graphics => base/graphics/support}/camera.h (93%) rename src/ballistica/{graphics => base/graphics/support}/frame_def.cc (76%) rename src/ballistica/{graphics => base/graphics/support}/frame_def.h (88%) rename src/ballistica/{graphics => base/graphics/support}/net_graph.cc (92%) create mode 100644 src/ballistica/base/graphics/support/net_graph.h rename src/ballistica/{graphics => base/graphics/support}/render_command_buffer.h (90%) rename src/ballistica/{ => base}/graphics/text/font_page_map_data.h (93%) rename src/ballistica/{ => base}/graphics/text/text_graphics.cc (97%) rename src/ballistica/{ => base}/graphics/text/text_graphics.h (90%) rename src/ballistica/{ => base}/graphics/text/text_group.cc (84%) rename src/ballistica/{ => base}/graphics/text/text_group.h (71%) rename src/ballistica/{ => base}/graphics/text/text_packer.cc (98%) rename src/ballistica/{ => base}/graphics/text/text_packer.h (83%) rename src/ballistica/{ => base}/graphics/texture/dds.cc (94%) rename src/ballistica/{ => base}/graphics/texture/dds.h (95%) rename src/ballistica/{ => base}/graphics/texture/ktx.cc (99%) rename src/ballistica/{ => base}/graphics/texture/ktx.h (75%) rename src/ballistica/{ => base}/graphics/texture/pvr.cc (97%) rename src/ballistica/{ => base}/graphics/texture/pvr.h (59%) create mode 100644 src/ballistica/base/input/device/input_device.cc create mode 100644 src/ballistica/base/input/device/input_device.h create mode 100644 src/ballistica/base/input/device/input_device_delegate.cc create mode 100644 src/ballistica/base/input/device/input_device_delegate.h rename src/ballistica/{input/device/joystick.cc => base/input/device/joystick_input.cc} (82%) rename src/ballistica/{input/device/joystick.h => base/input/device/joystick_input.h} (88%) rename src/ballistica/{ => base}/input/device/keyboard_input.cc (90%) rename src/ballistica/{ => base}/input/device/keyboard_input.h (74%) rename src/ballistica/{ => base}/input/device/test_input.cc (76%) rename src/ballistica/{ => base}/input/device/test_input.h (66%) rename src/ballistica/{ => base}/input/device/touch_input.cc (85%) rename src/ballistica/{ => base}/input/device/touch_input.h (89%) rename src/ballistica/{ => base}/input/input.cc (61%) rename src/ballistica/{ => base}/input/input.h (54%) rename src/ballistica/{input/remote_app.cc => base/input/support/remote_app_server.cc} (85%) rename src/ballistica/{input/remote_app.h => base/input/support/remote_app_server.h} (76%) create mode 100644 src/ballistica/base/logic/logic.cc create mode 100644 src/ballistica/base/logic/logic.h rename src/ballistica/{ => base}/networking/network_reader.cc (71%) rename src/ballistica/{ => base}/networking/network_reader.h (74%) create mode 100644 src/ballistica/base/networking/network_writer.cc create mode 100644 src/ballistica/base/networking/network_writer.h create mode 100644 src/ballistica/base/networking/networking.cc rename src/ballistica/{ => base}/networking/networking.h (63%) create mode 100644 src/ballistica/base/platform/android/amazon/base_plat_andr_amazon.cc create mode 100644 src/ballistica/base/platform/android/amazon/base_plat_andr_amazon.h create mode 100644 src/ballistica/base/platform/android/base_platform_android.cc create mode 100644 src/ballistica/base/platform/android/base_platform_android.h create mode 100644 src/ballistica/base/platform/android/cardboard/base_pl_an_cardboard.cc create mode 100644 src/ballistica/base/platform/android/cardboard/base_pl_an_cardboard.h create mode 100644 src/ballistica/base/platform/android/google/base_plat_andr_google.cc create mode 100644 src/ballistica/base/platform/android/google/base_plat_andr_google.h create mode 100644 src/ballistica/base/platform/apple/base_platform_apple.cc create mode 100644 src/ballistica/base/platform/apple/base_platform_apple.h create mode 100644 src/ballistica/base/platform/base_platform.cc create mode 100644 src/ballistica/base/platform/base_platform.h create mode 100644 src/ballistica/base/platform/linux/base_platform_linux.cc create mode 100644 src/ballistica/base/platform/linux/base_platform_linux.h create mode 100644 src/ballistica/base/platform/oculus/main_rift.cc create mode 100644 src/ballistica/base/platform/support/min_sdl_key_names.h create mode 100644 src/ballistica/base/platform/windows/base_platform_windows.cc create mode 100644 src/ballistica/base/platform/windows/base_platform_windows.h create mode 100644 src/ballistica/base/platform/windows/base_platform_windows_oculus.cc create mode 100644 src/ballistica/base/platform/windows/base_platform_windows_oculus.h create mode 100644 src/ballistica/base/python/base_python.cc create mode 100644 src/ballistica/base/python/base_python.h create mode 100644 src/ballistica/base/python/class/python_class_app_timer.cc create mode 100644 src/ballistica/base/python/class/python_class_app_timer.h rename src/ballistica/{ => base}/python/class/python_class_context_call.cc (56%) rename src/ballistica/{ => base}/python/class/python_class_context_call.h (54%) create mode 100644 src/ballistica/base/python/class/python_class_context_ref.cc create mode 100644 src/ballistica/base/python/class/python_class_context_ref.h create mode 100644 src/ballistica/base/python/class/python_class_display_timer.cc create mode 100644 src/ballistica/base/python/class/python_class_display_timer.h create mode 100644 src/ballistica/base/python/class/python_class_feature_set_data.cc create mode 100644 src/ballistica/base/python/class/python_class_feature_set_data.h create mode 100644 src/ballistica/base/python/class/python_class_simple_sound.cc create mode 100644 src/ballistica/base/python/class/python_class_simple_sound.h rename src/ballistica/{ => base}/python/class/python_class_vec3.cc (76%) rename src/ballistica/{ => base}/python/class/python_class_vec3.h (79%) create mode 100644 src/ballistica/base/python/methods/python_methods_app.cc create mode 100644 src/ballistica/base/python/methods/python_methods_app.h create mode 100644 src/ballistica/base/python/methods/python_methods_graphics.cc create mode 100644 src/ballistica/base/python/methods/python_methods_graphics.h create mode 100644 src/ballistica/base/python/methods/python_methods_misc.cc create mode 100644 src/ballistica/base/python/methods/python_methods_misc.h create mode 100644 src/ballistica/base/python/support/python_context_call.cc create mode 100644 src/ballistica/base/python/support/python_context_call.h create mode 100644 src/ballistica/base/python/support/python_context_call_runnable.h create mode 100644 src/ballistica/base/support/app_timer.h create mode 100644 src/ballistica/base/support/context.cc create mode 100644 src/ballistica/base/support/context.h rename src/ballistica/{generic => base/support}/huffman.cc (99%) rename src/ballistica/{generic => base/support}/huffman.h (82%) create mode 100644 src/ballistica/base/support/plus_soft.h rename src/ballistica/{platform => base/support}/stdio_console.cc (52%) create mode 100644 src/ballistica/base/support/stdio_console.h rename src/ballistica/{ => base}/ui/console.cc (76%) rename src/ballistica/{ => base}/ui/console.h (79%) rename src/ballistica/{ => base}/ui/ui.cc (51%) create mode 100644 src/ballistica/base/ui/ui.h create mode 100644 src/ballistica/base/ui/widget_message.h create mode 100644 src/ballistica/classic/README.md create mode 100644 src/ballistica/classic/classic.cc create mode 100644 src/ballistica/classic/classic.h create mode 100644 src/ballistica/classic/python/classic_python.cc create mode 100644 src/ballistica/classic/python/classic_python.h create mode 100644 src/ballistica/classic/python/methods/python_methods_classic.cc create mode 100644 src/ballistica/classic/python/methods/python_methods_classic.h rename src/ballistica/{logic => classic/support}/v1_account.cc (72%) rename src/ballistica/{logic => classic/support}/v1_account.h (65%) delete mode 100644 src/ballistica/config/config_windows_generic.h delete mode 100644 src/ballistica/config/config_windows_headless.h create mode 100644 src/ballistica/core/README.md delete mode 100644 src/ballistica/core/context.cc delete mode 100644 src/ballistica/core/context.h create mode 100644 src/ballistica/core/core.cc create mode 100644 src/ballistica/core/core.h delete mode 100644 src/ballistica/core/logging.cc delete mode 100644 src/ballistica/core/logging.h delete mode 100644 src/ballistica/core/macros.cc delete mode 100644 src/ballistica/core/macros.h create mode 100644 src/ballistica/core/platform/apple/core_platform_apple.h rename src/ballistica/{platform/platform.cc => core/platform/core_platform.cc} (50%) rename src/ballistica/{platform/platform.h => core/platform/core_platform.h} (57%) rename src/ballistica/{platform/linux/platform_linux.cc => core/platform/linux/core_platform_linux.cc} (66%) create mode 100644 src/ballistica/core/platform/linux/core_platform_linux.h rename src/ballistica/{platform => core/platform/support}/min_sdl.h (99%) rename src/ballistica/{platform/windows/platform_windows.cc => core/platform/windows/core_platform_windows.cc} (77%) rename src/ballistica/{platform/windows/platform_windows.h => core/platform/windows/core_platform_windows.h} (70%) create mode 100644 src/ballistica/core/python/core_python.cc create mode 100644 src/ballistica/core/python/core_python.h create mode 100644 src/ballistica/core/support/base_soft.h create mode 100644 src/ballistica/core/support/core_config.cc create mode 100644 src/ballistica/core/support/core_config.h delete mode 100644 src/ballistica/core/types.h delete mode 100644 src/ballistica/dynamics/material/sound_material_action.h delete mode 100644 src/ballistica/generic/real_timer.h delete mode 100644 src/ballistica/generic/timer.cc delete mode 100644 src/ballistica/generic/timer.h delete mode 100644 src/ballistica/generic/timer_list.h delete mode 100644 src/ballistica/graphics/component/render_component.cc delete mode 100644 src/ballistica/graphics/component/shield_component.h delete mode 100644 src/ballistica/graphics/framebuffer.h delete mode 100644 src/ballistica/graphics/mesh/mesh_buffer_vertex_simple_full.h delete mode 100644 src/ballistica/graphics/mesh/mesh_buffer_vertex_smoke_full.h delete mode 100644 src/ballistica/graphics/mesh/mesh_buffer_vertex_sprite.h delete mode 100644 src/ballistica/graphics/mesh/mesh_data_client_handle.cc delete mode 100644 src/ballistica/graphics/mesh/mesh_index_buffer_16.h delete mode 100644 src/ballistica/graphics/mesh/mesh_index_buffer_32.h delete mode 100644 src/ballistica/graphics/mesh/mesh_indexed_dual_texture_full.h delete mode 100644 src/ballistica/graphics/mesh/mesh_indexed_simple_full.h delete mode 100644 src/ballistica/graphics/mesh/mesh_indexed_smoke_full.h delete mode 100644 src/ballistica/graphics/mesh/mesh_renderer_data.h delete mode 100644 src/ballistica/graphics/mesh/sprite_mesh.h delete mode 100644 src/ballistica/graphics/net_graph.h delete mode 100644 src/ballistica/input/device/client_input_device.cc delete mode 100644 src/ballistica/input/device/client_input_device.h delete mode 100644 src/ballistica/input/device/input_device.cc delete mode 100644 src/ballistica/input/device/input_device.h delete mode 100644 src/ballistica/internal/app_internal.h delete mode 100644 src/ballistica/logic/client_controller_interface.h delete mode 100644 src/ballistica/logic/host_activity.h delete mode 100644 src/ballistica/logic/logic.cc delete mode 100644 src/ballistica/logic/logic.h delete mode 100644 src/ballistica/logic/session/client_session.h delete mode 100644 src/ballistica/logic/session/replay_client_session.h delete mode 100644 src/ballistica/logic/session/session.cc delete mode 100644 src/ballistica/logic/session/session.h delete mode 100644 src/ballistica/math/random.h delete mode 100644 src/ballistica/networking/network_writer.cc delete mode 100644 src/ballistica/networking/network_writer.h delete mode 100644 src/ballistica/networking/networking.cc delete mode 100644 src/ballistica/networking/telnet_server.cc delete mode 100644 src/ballistica/networking/telnet_server.h delete mode 100644 src/ballistica/platform/apple/platform_apple.h delete mode 100644 src/ballistica/platform/linux/platform_linux.h delete mode 100644 src/ballistica/platform/sdl/sdl_app.h delete mode 100644 src/ballistica/platform/stdio_console.h create mode 100644 src/ballistica/plus/README.md delete mode 100644 src/ballistica/python/class/python_class.h delete mode 100644 src/ballistica/python/class/python_class_activity_data.h delete mode 100644 src/ballistica/python/class/python_class_collide_model.cc delete mode 100644 src/ballistica/python/class/python_class_collide_model.h delete mode 100644 src/ballistica/python/class/python_class_context.cc delete mode 100644 src/ballistica/python/class/python_class_context.h delete mode 100644 src/ballistica/python/class/python_class_data.cc delete mode 100644 src/ballistica/python/class/python_class_data.h delete mode 100644 src/ballistica/python/class/python_class_model.cc delete mode 100644 src/ballistica/python/class/python_class_model.h delete mode 100644 src/ballistica/python/class/python_class_sound.cc delete mode 100644 src/ballistica/python/class/python_class_sound.h delete mode 100644 src/ballistica/python/class/python_class_texture.cc delete mode 100644 src/ballistica/python/class/python_class_texture.h delete mode 100644 src/ballistica/python/class/python_class_timer.cc delete mode 100644 src/ballistica/python/class/python_class_timer.h delete mode 100644 src/ballistica/python/methods/python_methods_app.cc delete mode 100644 src/ballistica/python/methods/python_methods_app.h delete mode 100644 src/ballistica/python/methods/python_methods_assets.cc delete mode 100644 src/ballistica/python/methods/python_methods_assets.h delete mode 100644 src/ballistica/python/methods/python_methods_gameplay.cc delete mode 100644 src/ballistica/python/methods/python_methods_gameplay.h delete mode 100644 src/ballistica/python/methods/python_methods_graphics.cc delete mode 100644 src/ballistica/python/methods/python_methods_graphics.h delete mode 100644 src/ballistica/python/methods/python_methods_input.cc delete mode 100644 src/ballistica/python/methods/python_methods_input.h delete mode 100644 src/ballistica/python/methods/python_methods_networking.cc delete mode 100644 src/ballistica/python/methods/python_methods_networking.h delete mode 100644 src/ballistica/python/methods/python_methods_system.cc delete mode 100644 src/ballistica/python/methods/python_methods_system.h delete mode 100644 src/ballistica/python/methods/python_methods_ui.h delete mode 100644 src/ballistica/python/python.cc delete mode 100644 src/ballistica/python/python.h delete mode 100644 src/ballistica/python/python_context_call.cc delete mode 100644 src/ballistica/python/python_context_call.h delete mode 100644 src/ballistica/python/python_context_call_runnable.h delete mode 100644 src/ballistica/scene/node/session_globals_node.h delete mode 100644 src/ballistica/scene/node/texture_sequence_node.h delete mode 100644 src/ballistica/scene/scene_stream.h delete mode 100644 src/ballistica/scene/v1/scene_v1.cc delete mode 100644 src/ballistica/scene/v1/scene_v1.h create mode 100644 src/ballistica/scene_v1/README.md create mode 100644 src/ballistica/scene_v1/assets/scene_asset.cc create mode 100644 src/ballistica/scene_v1/assets/scene_asset.h create mode 100644 src/ballistica/scene_v1/assets/scene_collision_mesh.cc create mode 100644 src/ballistica/scene_v1/assets/scene_collision_mesh.h create mode 100644 src/ballistica/scene_v1/assets/scene_cube_map_texture.cc create mode 100644 src/ballistica/scene_v1/assets/scene_cube_map_texture.h create mode 100644 src/ballistica/scene_v1/assets/scene_data_asset.cc create mode 100644 src/ballistica/scene_v1/assets/scene_data_asset.h create mode 100644 src/ballistica/scene_v1/assets/scene_mesh.cc create mode 100644 src/ballistica/scene_v1/assets/scene_mesh.h create mode 100644 src/ballistica/scene_v1/assets/scene_sound.cc create mode 100644 src/ballistica/scene_v1/assets/scene_sound.h create mode 100644 src/ballistica/scene_v1/assets/scene_texture.cc create mode 100644 src/ballistica/scene_v1/assets/scene_texture.h rename src/ballistica/{logic => scene_v1}/connection/connection.cc (89%) rename src/ballistica/{logic => scene_v1}/connection/connection.h (92%) rename src/ballistica/{logic => scene_v1}/connection/connection_set.cc (82%) rename src/ballistica/{logic => scene_v1}/connection/connection_set.h (62%) rename src/ballistica/{logic => scene_v1}/connection/connection_to_client.cc (78%) rename src/ballistica/{logic => scene_v1}/connection/connection_to_client.h (89%) rename src/ballistica/{logic => scene_v1}/connection/connection_to_client_udp.cc (71%) rename src/ballistica/{logic => scene_v1}/connection/connection_to_client_udp.h (60%) rename src/ballistica/{logic => scene_v1}/connection/connection_to_host.cc (73%) rename src/ballistica/{logic => scene_v1}/connection/connection_to_host.h (79%) rename src/ballistica/{logic => scene_v1}/connection/connection_to_host_udp.cc (68%) rename src/ballistica/{logic => scene_v1}/connection/connection_to_host_udp.h (72%) rename src/ballistica/{ => scene_v1}/dynamics/collision.h (74%) rename src/ballistica/{ => scene_v1}/dynamics/dynamics.cc (92%) rename src/ballistica/{ => scene_v1}/dynamics/dynamics.h (72%) rename src/ballistica/{ => scene_v1}/dynamics/material/impact_sound_material_action.cc (71%) rename src/ballistica/{ => scene_v1}/dynamics/material/impact_sound_material_action.h (56%) rename src/ballistica/{ => scene_v1}/dynamics/material/material.cc (63%) rename src/ballistica/{ => scene_v1}/dynamics/material/material.h (77%) rename src/ballistica/{ => scene_v1}/dynamics/material/material_action.h (71%) rename src/ballistica/{ => scene_v1}/dynamics/material/material_component.cc (84%) rename src/ballistica/{ => scene_v1}/dynamics/material/material_component.h (68%) rename src/ballistica/{ => scene_v1}/dynamics/material/material_condition_node.cc (84%) rename src/ballistica/{ => scene_v1}/dynamics/material/material_condition_node.h (77%) rename src/ballistica/{ => scene_v1}/dynamics/material/material_context.cc (70%) rename src/ballistica/{ => scene_v1}/dynamics/material/material_context.h (71%) rename src/ballistica/{ => scene_v1}/dynamics/material/node_message_material_action.cc (80%) rename src/ballistica/{ => scene_v1}/dynamics/material/node_message_material_action.h (65%) rename src/ballistica/{ => scene_v1}/dynamics/material/node_mod_material_action.cc (72%) rename src/ballistica/{ => scene_v1}/dynamics/material/node_mod_material_action.h (59%) rename src/ballistica/{dynamics/material/node_user_message_material_action.cc => scene_v1/dynamics/material/node_user_msg_mat_action.cc} (78%) rename src/ballistica/{dynamics/material/node_user_message_material_action.h => scene_v1/dynamics/material/node_user_msg_mat_action.h} (63%) rename src/ballistica/{ => scene_v1}/dynamics/material/part_mod_material_action.cc (80%) rename src/ballistica/{ => scene_v1}/dynamics/material/part_mod_material_action.h (59%) rename src/ballistica/{ => scene_v1}/dynamics/material/python_call_material_action.cc (77%) rename src/ballistica/{ => scene_v1}/dynamics/material/python_call_material_action.h (50%) rename src/ballistica/{ => scene_v1}/dynamics/material/roll_sound_material_action.cc (64%) rename src/ballistica/{ => scene_v1}/dynamics/material/roll_sound_material_action.h (54%) rename src/ballistica/{ => scene_v1}/dynamics/material/skid_sound_material_action.cc (64%) rename src/ballistica/{ => scene_v1}/dynamics/material/skid_sound_material_action.h (54%) rename src/ballistica/{ => scene_v1}/dynamics/material/sound_material_action.cc (58%) create mode 100644 src/ballistica/scene_v1/dynamics/material/sound_material_action.h rename src/ballistica/{ => scene_v1}/dynamics/part.cc (87%) rename src/ballistica/{ => scene_v1}/dynamics/part.h (92%) rename src/ballistica/{ => scene_v1}/dynamics/rigid_body.cc (85%) rename src/ballistica/{ => scene_v1}/dynamics/rigid_body.h (81%) rename src/ballistica/{scene => scene_v1}/node/anim_curve_node.cc (92%) rename src/ballistica/{scene => scene_v1}/node/anim_curve_node.h (84%) rename src/ballistica/{scene => scene_v1}/node/bomb_node.cc (79%) rename src/ballistica/{scene => scene_v1}/node/bomb_node.h (54%) rename src/ballistica/{scene => scene_v1}/node/combine_node.cc (87%) rename src/ballistica/{scene => scene_v1}/node/combine_node.h (82%) rename src/ballistica/{scene => scene_v1}/node/explosion_node.cc (71%) rename src/ballistica/{scene => scene_v1}/node/explosion_node.h (77%) rename src/ballistica/{scene => scene_v1}/node/flag_node.cc (80%) rename src/ballistica/{scene => scene_v1}/node/flag_node.h (69%) rename src/ballistica/{scene => scene_v1}/node/flash_node.cc (68%) rename src/ballistica/{scene => scene_v1}/node/flash_node.h (69%) rename src/ballistica/{scene => scene_v1}/node/globals_node.cc (70%) rename src/ballistica/{scene => scene_v1}/node/globals_node.h (92%) rename src/ballistica/{scene => scene_v1}/node/image_node.cc (78%) rename src/ballistica/{scene => scene_v1}/node/image_node.h (71%) rename src/ballistica/{scene => scene_v1}/node/light_node.cc (75%) rename src/ballistica/{scene => scene_v1}/node/light_node.h (74%) rename src/ballistica/{scene => scene_v1}/node/locator_node.cc (77%) rename src/ballistica/{scene => scene_v1}/node/locator_node.h (83%) rename src/ballistica/{scene => scene_v1}/node/math_node.cc (92%) rename src/ballistica/{scene => scene_v1}/node/math_node.h (78%) rename src/ballistica/{scene => scene_v1}/node/node.cc (78%) rename src/ballistica/{scene => scene_v1}/node/node.h (89%) rename src/ballistica/{scene => scene_v1}/node/node_attribute.cc (77%) rename src/ballistica/{scene => scene_v1}/node/node_attribute.h (85%) rename src/ballistica/{scene => scene_v1}/node/node_attribute_connection.cc (59%) rename src/ballistica/{scene => scene_v1}/node/node_attribute_connection.h (54%) rename src/ballistica/{scene => scene_v1}/node/node_type.h (88%) rename src/ballistica/{scene => scene_v1}/node/null_node.cc (75%) rename src/ballistica/{scene => scene_v1}/node/null_node.h (51%) rename src/ballistica/{scene => scene_v1}/node/player_node.cc (79%) rename src/ballistica/{scene => scene_v1}/node/player_node.h (66%) rename src/ballistica/{scene => scene_v1}/node/prop_node.cc (86%) rename src/ballistica/{scene => scene_v1}/node/prop_node.h (78%) rename src/ballistica/{scene => scene_v1}/node/region_node.cc (84%) rename src/ballistica/{scene => scene_v1}/node/region_node.h (73%) rename src/ballistica/{scene => scene_v1}/node/scorch_node.cc (69%) rename src/ballistica/{scene => scene_v1}/node/scorch_node.h (75%) rename src/ballistica/{scene => scene_v1}/node/session_globals_node.cc (75%) create mode 100644 src/ballistica/scene_v1/node/session_globals_node.h rename src/ballistica/{scene => scene_v1}/node/shield_node.cc (77%) rename src/ballistica/{scene => scene_v1}/node/shield_node.h (75%) rename src/ballistica/{scene => scene_v1}/node/sound_node.cc (74%) rename src/ballistica/{scene => scene_v1}/node/sound_node.h (71%) rename src/ballistica/{scene => scene_v1}/node/spaz_node.cc (90%) rename src/ballistica/{scene => scene_v1}/node/spaz_node.h (75%) rename src/ballistica/{scene => scene_v1}/node/terrain_node.cc (65%) rename src/ballistica/{scene => scene_v1}/node/terrain_node.h (68%) rename src/ballistica/{scene => scene_v1}/node/text_node.cc (85%) rename src/ballistica/{scene => scene_v1}/node/text_node.h (91%) rename src/ballistica/{scene => scene_v1}/node/texture_sequence_node.cc (78%) create mode 100644 src/ballistica/scene_v1/node/texture_sequence_node.h rename src/ballistica/{scene => scene_v1}/node/time_display_node.cc (85%) rename src/ballistica/{scene => scene_v1}/node/time_display_node.h (85%) rename src/ballistica/{ => scene_v1}/python/class/python_class_activity_data.cc (52%) create mode 100644 src/ballistica/scene_v1/python/class/python_class_activity_data.h create mode 100644 src/ballistica/scene_v1/python/class/python_class_base_timer.cc create mode 100644 src/ballistica/scene_v1/python/class/python_class_base_timer.h rename src/ballistica/{ => scene_v1}/python/class/python_class_input_device.cc (67%) rename src/ballistica/{ => scene_v1}/python/class/python_class_input_device.h (62%) rename src/ballistica/{ => scene_v1}/python/class/python_class_material.cc (79%) rename src/ballistica/{ => scene_v1}/python/class/python_class_material.h (68%) rename src/ballistica/{ => scene_v1}/python/class/python_class_node.cc (74%) rename src/ballistica/{ => scene_v1}/python/class/python_class_node.h (76%) create mode 100644 src/ballistica/scene_v1/python/class/python_class_scene_collision_mesh.cc create mode 100644 src/ballistica/scene_v1/python/class/python_class_scene_collision_mesh.h create mode 100644 src/ballistica/scene_v1/python/class/python_class_scene_data_asset.cc create mode 100644 src/ballistica/scene_v1/python/class/python_class_scene_data_asset.h create mode 100644 src/ballistica/scene_v1/python/class/python_class_scene_mesh.cc create mode 100644 src/ballistica/scene_v1/python/class/python_class_scene_mesh.h create mode 100644 src/ballistica/scene_v1/python/class/python_class_scene_sound.cc create mode 100644 src/ballistica/scene_v1/python/class/python_class_scene_sound.h create mode 100644 src/ballistica/scene_v1/python/class/python_class_scene_texture.cc create mode 100644 src/ballistica/scene_v1/python/class/python_class_scene_texture.h create mode 100644 src/ballistica/scene_v1/python/class/python_class_scene_timer.cc create mode 100644 src/ballistica/scene_v1/python/class/python_class_scene_timer.h rename src/ballistica/{ => scene_v1}/python/class/python_class_session_data.cc (53%) rename src/ballistica/{ => scene_v1}/python/class/python_class_session_data.h (58%) rename src/ballistica/{ => scene_v1}/python/class/python_class_session_player.cc (78%) rename src/ballistica/{ => scene_v1}/python/class/python_class_session_player.h (82%) create mode 100644 src/ballistica/scene_v1/python/methods/python_methods_assets.cc create mode 100644 src/ballistica/scene_v1/python/methods/python_methods_assets.h create mode 100644 src/ballistica/scene_v1/python/methods/python_methods_input.cc create mode 100644 src/ballistica/scene_v1/python/methods/python_methods_input.h create mode 100644 src/ballistica/scene_v1/python/methods/python_methods_networking.cc create mode 100644 src/ballistica/scene_v1/python/methods/python_methods_networking.h create mode 100644 src/ballistica/scene_v1/python/methods/python_methods_scene.cc create mode 100644 src/ballistica/scene_v1/python/methods/python_methods_scene.h create mode 100644 src/ballistica/scene_v1/python/scene_v1_python.cc create mode 100644 src/ballistica/scene_v1/python/scene_v1_python.h create mode 100644 src/ballistica/scene_v1/scene_v1.cc create mode 100644 src/ballistica/scene_v1/scene_v1.h create mode 100644 src/ballistica/scene_v1/support/client_controller_interface.h create mode 100644 src/ballistica/scene_v1/support/client_input_device.cc create mode 100644 src/ballistica/scene_v1/support/client_input_device.h create mode 100644 src/ballistica/scene_v1/support/client_input_device_delegate.cc create mode 100644 src/ballistica/scene_v1/support/client_input_device_delegate.h rename src/ballistica/{logic/session => scene_v1/support}/client_session.cc (79%) create mode 100644 src/ballistica/scene_v1/support/client_session.h rename src/ballistica/{logic/session/net_client_session.cc => scene_v1/support/client_session_net.cc} (67%) rename src/ballistica/{logic/session/net_client_session.h => scene_v1/support/client_session_net.h} (57%) rename src/ballistica/{logic/session/replay_client_session.cc => scene_v1/support/client_session_replay.cc} (75%) create mode 100644 src/ballistica/scene_v1/support/client_session_replay.h rename src/ballistica/{logic => scene_v1/support}/host_activity.cc (54%) create mode 100644 src/ballistica/scene_v1/support/host_activity.h rename src/ballistica/{logic/session => scene_v1/support}/host_session.cc (61%) rename src/ballistica/{logic/session => scene_v1/support}/host_session.h (54%) rename src/ballistica/{logic => scene_v1/support}/player.cc (81%) rename src/ballistica/{logic => scene_v1/support}/player.h (65%) rename src/ballistica/{logic => scene_v1/support}/player_spec.cc (71%) rename src/ballistica/{logic => scene_v1/support}/player_spec.h (83%) rename src/ballistica/{scene => scene_v1/support}/scene.cc (76%) rename src/ballistica/{scene => scene_v1/support}/scene.h (56%) create mode 100644 src/ballistica/scene_v1/support/scene_v1_app_mode.cc create mode 100644 src/ballistica/scene_v1/support/scene_v1_app_mode.h create mode 100644 src/ballistica/scene_v1/support/scene_v1_context.cc create mode 100644 src/ballistica/scene_v1/support/scene_v1_context.h create mode 100644 src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc create mode 100644 src/ballistica/scene_v1/support/scene_v1_input_device_delegate.h create mode 100644 src/ballistica/scene_v1/support/session.cc create mode 100644 src/ballistica/scene_v1/support/session.h rename src/ballistica/{scene/scene_stream.cc => scene_v1/support/session_stream.cc} (72%) create mode 100644 src/ballistica/scene_v1/support/session_stream.h create mode 100644 src/ballistica/shared/README.md create mode 100644 src/ballistica/shared/ballistica.cc rename src/ballistica/{ => shared}/ballistica.h (51%) rename src/ballistica/{config/config_cmake.h => shared/buildconfig/buildconfig_cmake.h} (87%) rename src/ballistica/{config/config_common.h => shared/buildconfig/buildconfig_common.h} (85%) rename src/ballistica/{config/config_windows_common.h => shared/buildconfig/buildconfig_windows_common.h} (87%) create mode 100644 src/ballistica/shared/buildconfig/buildconfig_windows_generic.h create mode 100644 src/ballistica/shared/buildconfig/buildconfig_windows_headless.h rename src/ballistica/{core/thread.cc => shared/foundation/event_loop.cc} (65%) rename src/ballistica/{core/thread.h => shared/foundation/event_loop.h} (54%) rename src/ballistica/{core => shared/foundation}/exception.cc (76%) rename src/ballistica/{core => shared/foundation}/exception.h (89%) rename src/ballistica/{core => shared/foundation}/fatal_error.cc (59%) rename src/ballistica/{core => shared/foundation}/fatal_error.h (77%) create mode 100644 src/ballistica/shared/foundation/feature_set_front_end.cc create mode 100644 src/ballistica/shared/foundation/feature_set_front_end.h rename src/ballistica/{core => shared/foundation}/inline.cc (82%) rename src/ballistica/{core => shared/foundation}/inline.h (93%) create mode 100644 src/ballistica/shared/foundation/logging.cc create mode 100644 src/ballistica/shared/foundation/logging.h create mode 100644 src/ballistica/shared/foundation/macros.cc create mode 100644 src/ballistica/shared/foundation/macros.h rename src/ballistica/{core => shared/foundation}/object.cc (50%) rename src/ballistica/{core => shared/foundation}/object.h (68%) create mode 100644 src/ballistica/shared/foundation/types.h rename src/ballistica/{ => shared}/generic/base64.cc (99%) rename src/ballistica/{ => shared}/generic/base64.h (69%) rename src/ballistica/{ => shared}/generic/buffer.h (91%) rename src/ballistica/{ => shared}/generic/json.cc (99%) rename src/ballistica/{ => shared}/generic/json.h (98%) rename src/ballistica/{ => shared}/generic/lambda_runnable.h (56%) rename src/ballistica/{ => shared}/generic/runnable.cc (82%) rename src/ballistica/{ => shared}/generic/runnable.h (66%) rename src/ballistica/{ => shared}/generic/timer_list.cc (81%) create mode 100644 src/ballistica/shared/generic/timer_list.h rename src/ballistica/{ => shared}/generic/utf8.cc (99%) rename src/ballistica/{ => shared}/generic/utf8.h (94%) rename src/ballistica/{ => shared}/generic/utils.cc (86%) rename src/ballistica/{ => shared}/generic/utils.h (94%) rename src/ballistica/{ => shared}/math/matrix44f.cc (99%) rename src/ballistica/{ => shared}/math/matrix44f.h (96%) rename src/ballistica/{ => shared}/math/point2d.h (68%) rename src/ballistica/{ => shared}/math/random.cc (99%) create mode 100644 src/ballistica/shared/math/random.h rename src/ballistica/{ => shared}/math/rect.h (77%) rename src/ballistica/{ => shared}/math/vector2f.h (76%) rename src/ballistica/{ => shared}/math/vector3f.cc (96%) rename src/ballistica/{ => shared}/math/vector3f.h (95%) rename src/ballistica/{ => shared}/math/vector4f.h (75%) rename src/ballistica/{ => shared}/networking/networking_sys.h (72%) rename src/ballistica/{ => shared}/networking/sockaddr.cc (94%) rename src/ballistica/{ => shared}/networking/sockaddr.h (89%) create mode 100644 src/ballistica/shared/python/python.cc create mode 100644 src/ballistica/shared/python/python.h rename src/ballistica/{python/class => shared/python}/python_class.cc (59%) create mode 100644 src/ballistica/shared/python/python_class.h rename src/ballistica/{ => shared}/python/python_command.cc (58%) rename src/ballistica/{ => shared}/python/python_command.h (71%) create mode 100644 src/ballistica/shared/python/python_module_builder.h create mode 100644 src/ballistica/shared/python/python_object_set.cc create mode 100644 src/ballistica/shared/python/python_object_set.h rename src/ballistica/{ => shared}/python/python_ref.cc (56%) rename src/ballistica/{ => shared}/python/python_ref.h (58%) rename src/ballistica/{ => shared}/python/python_sys.h (87%) create mode 100644 src/ballistica/template_fs/README.md create mode 100644 src/ballistica/template_fs/python/class/python_class_hello.cc create mode 100644 src/ballistica/template_fs/python/class/python_class_hello.h create mode 100644 src/ballistica/template_fs/python/methods/python_methods_template_fs.cc create mode 100644 src/ballistica/template_fs/python/methods/python_methods_template_fs.h create mode 100644 src/ballistica/template_fs/python/template_fs_python.cc create mode 100644 src/ballistica/template_fs/python/template_fs_python.h create mode 100644 src/ballistica/template_fs/template_fs.cc create mode 100644 src/ballistica/template_fs/template_fs.h delete mode 100644 src/ballistica/ui/ui.h delete mode 100644 src/ballistica/ui/widget/row_widget.h create mode 100644 src/ballistica/ui_v1/python/class/python_class_ui_mesh.cc create mode 100644 src/ballistica/ui_v1/python/class/python_class_ui_mesh.h create mode 100644 src/ballistica/ui_v1/python/class/python_class_ui_sound.cc create mode 100644 src/ballistica/ui_v1/python/class/python_class_ui_sound.h create mode 100644 src/ballistica/ui_v1/python/class/python_class_ui_texture.cc create mode 100644 src/ballistica/ui_v1/python/class/python_class_ui_texture.h rename src/ballistica/{ => ui_v1}/python/class/python_class_widget.cc (75%) rename src/ballistica/{ => ui_v1}/python/class/python_class_widget.h (74%) rename src/ballistica/{python/methods/python_methods_ui.cc => ui_v1/python/methods/python_methods_ui_v1.cc} (60%) create mode 100644 src/ballistica/ui_v1/python/methods/python_methods_ui_v1.h create mode 100644 src/ballistica/ui_v1/python/ui_v1_python.cc create mode 100644 src/ballistica/ui_v1/python/ui_v1_python.h rename src/ballistica/{ui => ui_v1/support}/root_ui.cc (67%) rename src/ballistica/{ui => ui_v1/support}/root_ui.h (71%) create mode 100644 src/ballistica/ui_v1/ui_v1.cc create mode 100644 src/ballistica/ui_v1/ui_v1.h rename src/ballistica/{ui => ui_v1}/widget/button_widget.cc (62%) rename src/ballistica/{ui => ui_v1}/widget/button_widget.h (59%) rename src/ballistica/{ui => ui_v1}/widget/check_box_widget.cc (76%) rename src/ballistica/{ui => ui_v1}/widget/check_box_widget.h (79%) rename src/ballistica/{ui => ui_v1}/widget/column_widget.cc (82%) rename src/ballistica/{ui => ui_v1}/widget/column_widget.h (74%) rename src/ballistica/{ui => ui_v1}/widget/container_widget.cc (83%) rename src/ballistica/{ui => ui_v1}/widget/container_widget.h (89%) rename src/ballistica/{ui => ui_v1}/widget/h_scroll_widget.cc (87%) rename src/ballistica/{ui => ui_v1}/widget/h_scroll_widget.h (83%) rename src/ballistica/{ui => ui_v1}/widget/image_widget.cc (56%) rename src/ballistica/{ui => ui_v1}/widget/image_widget.h (51%) rename src/ballistica/{ui => ui_v1}/widget/root_widget.cc (87%) rename src/ballistica/{ui => ui_v1}/widget/root_widget.h (82%) rename src/ballistica/{ui => ui_v1}/widget/row_widget.cc (75%) create mode 100644 src/ballistica/ui_v1/widget/row_widget.h rename src/ballistica/{ui => ui_v1}/widget/scroll_widget.cc (85%) rename src/ballistica/{ui => ui_v1}/widget/scroll_widget.h (77%) rename src/ballistica/{ui => ui_v1}/widget/stack_widget.cc (83%) rename src/ballistica/{ui => ui_v1}/widget/stack_widget.h (75%) rename src/ballistica/{ui => ui_v1}/widget/text_widget.cc (81%) rename src/ballistica/{ui => ui_v1}/widget/text_widget.h (86%) rename src/ballistica/{ui => ui_v1}/widget/widget.cc (83%) rename src/ballistica/{ui => ui_v1}/widget/widget.h (82%) create mode 100644 src/external/README.md delete mode 100755 src/external/windows/include/python/code.h rename src/external/windows/include/python/{ => cpython}/cellobject.h (76%) mode change 100755 => 100644 rename src/external/windows/include/python/{ => cpython}/classobject.h (93%) mode change 100755 => 100644 create mode 100644 src/external/windows/include/python/cpython/complexobject.h rename src/external/windows/include/python/{ => cpython}/context.h (95%) mode change 100755 => 100644 create mode 100644 src/external/windows/include/python/cpython/descrobject.h create mode 100644 src/external/windows/include/python/cpython/floatobject.h rename src/external/windows/include/python/{ => cpython}/funcobject.h (90%) mode change 100755 => 100644 rename src/external/windows/include/python/{ => cpython}/genobject.h (69%) mode change 100755 => 100644 delete mode 100755 src/external/windows/include/python/cpython/interpreteridobject.h rename src/external/windows/include/python/{ => cpython}/longintrepr.h (92%) mode change 100755 => 100644 create mode 100644 src/external/windows/include/python/cpython/longobject.h create mode 100644 src/external/windows/include/python/cpython/modsupport.h create mode 100644 src/external/windows/include/python/cpython/pthread_stubs.h create mode 100644 src/external/windows/include/python/cpython/pyframe.h create mode 100644 src/external/windows/include/python/cpython/pythread.h create mode 100644 src/external/windows/include/python/cpython/setobject.h create mode 100644 src/external/windows/include/python/cpython/warnings.h create mode 100644 src/external/windows/include/python/cpython/weakrefobject.h delete mode 100755 src/external/windows/include/python/eval.h create mode 100644 src/external/windows/include/python/internal/pycore_bytesobject.h create mode 100644 src/external/windows/include/python/internal/pycore_dict.h create mode 100644 src/external/windows/include/python/internal/pycore_emscripten_signal.h create mode 100644 src/external/windows/include/python/internal/pycore_exceptions.h create mode 100644 src/external/windows/include/python/internal/pycore_floatobject.h create mode 100644 src/external/windows/include/python/internal/pycore_frame.h create mode 100644 src/external/windows/include/python/internal/pycore_function.h create mode 100644 src/external/windows/include/python/internal/pycore_genobject.h create mode 100644 src/external/windows/include/python/internal/pycore_global_objects.h create mode 100644 src/external/windows/include/python/internal/pycore_global_strings.h create mode 100644 src/external/windows/include/python/internal/pycore_interpreteridobject.h create mode 100644 src/external/windows/include/python/internal/pycore_namespace.h create mode 100644 src/external/windows/include/python/internal/pycore_opcode.h create mode 100644 src/external/windows/include/python/internal/pycore_pymath.h create mode 100644 src/external/windows/include/python/internal/pycore_runtime_init.h create mode 100644 src/external/windows/include/python/internal/pycore_signal.h create mode 100644 src/external/windows/include/python/internal/pycore_sliceobject.h create mode 100644 src/external/windows/include/python/internal/pycore_strhex.h create mode 100644 src/external/windows/include/python/internal/pycore_typeobject.h create mode 100644 src/external/windows/include/python/internal/pycore_unicodeobject.h delete mode 100755 src/external/windows/include/python/interpreteridobject.h delete mode 100755 src/external/windows/include/python/namespaceobject.h create mode 100644 src/external/windows/include/python/pybuffer.h delete mode 100755 src/external/windows/include/python/pystrhex.h create mode 100644 src/external/windows/include/python/pytypedefs.h delete mode 100755 src/external/windows/lib/Win32/python310.lib delete mode 100755 src/external/windows/lib/Win32/python310_d.lib create mode 100644 src/external/windows/lib/Win32/python311.lib create mode 100644 src/external/windows/lib/Win32/python311_d.lib delete mode 100755 src/external/windows/lib/x64/python310.lib delete mode 100755 src/external/windows/lib/x64/python310_d.lib create mode 100644 src/external/windows/lib/x64/python311.lib create mode 100644 src/external/windows/lib/x64/python311_d.lib rename src/meta/{bameta => babasemeta}/__init__.py (100%) create mode 100644 src/meta/babasemeta/pyembed/__init__.py create mode 100644 src/meta/babasemeta/pyembed/binding_base.py rename src/meta/{bameta/python_embedded => baclassicmeta}/__init__.py (100%) create mode 100644 src/meta/baclassicmeta/pyembed/__init__.py create mode 100644 src/meta/baclassicmeta/pyembed/binding_classic.py create mode 100644 src/meta/bacoremeta/__init__.py create mode 100644 src/meta/bacoremeta/pyembed/__init__.py create mode 100644 src/meta/bacoremeta/pyembed/binding_core.py create mode 100644 src/meta/bacoremeta/pyembed/env.py delete mode 100644 src/meta/bameta/python_embedded/binding.py delete mode 100644 src/meta/bameta/python_embedded/bootstrap.py delete mode 100644 src/meta/bameta/python_embedded/bootstrap_monolithic.py create mode 100644 src/meta/bascenev1meta/__init__.py create mode 100644 src/meta/bascenev1meta/pyembed/__init__.py create mode 100644 src/meta/bascenev1meta/pyembed/binding_scene_v1.py create mode 100644 src/meta/batemplatefsmeta/__init__.py create mode 100644 src/meta/batemplatefsmeta/pyembed/__init__.py create mode 100644 src/meta/batemplatefsmeta/pyembed/binding_template_fs.py create mode 100644 src/meta/bauiv1meta/__init__.py create mode 100644 src/meta/bauiv1meta/pyembed/__init__.py create mode 100644 src/meta/bauiv1meta/pyembed/binding_ui_v1.py create mode 100644 src/resources/Makefile rename {resources => src/resources}/README.md (86%) rename tests/{test_ba => test_babase}/__init__.py (100%) rename tests/{test_ba => test_babase}/test_assetmanager.py (91%) create mode 100644 tools/README.md create mode 100644 tools/batools/featureset.py rename tools/batools/{meta.py => metabuild.py} (57%) create mode 100644 tools/batools/pcommand2.py delete mode 100755 tools/batools/project.py create mode 100644 tools/batools/project/__init__.py create mode 100755 tools/batools/project/_checks.py create mode 100755 tools/batools/project/_updater.py create mode 100755 tools/batools/pruneincludes.py create mode 100644 tools/batools/spinoff/__init__.py create mode 100644 tools/batools/spinoff/_config_template.py create mode 100644 tools/batools/spinoff/_context.py create mode 100644 tools/batools/spinoff/_main.py create mode 100644 tools/batools/spinoff/_state.py create mode 100755 tools/batools/toplevelmakefile.py create mode 100644 tools/batools/xcodeproject.py create mode 100644 tools/efrotools/buildlock.py rename tools/efrotools/{build.py => lazybuild.py} (51%) create mode 100644 tools/efrotools/pcommand2.py rename tools/efrotools/{xcode.py => xcodebuild.py} (50%) create mode 100755 tools/spinoff diff --git a/.editorconfig b/.editorconfig index 71da63f2..439c21cc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,12 +16,13 @@ root = true end_of_line = lf trim_trailing_whitespace = true insert_final_newline = true +max_line_length = 80 # Python overrides. [*.py] indent_style = space indent_size = 4 -max_line_length = 79 +max_line_length = 80 charset = utf-8 # Makefile overrides. diff --git a/.efrocachemap b/.efrocachemap index f74d9a0a..45ef339f 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -1,4058 +1,4129 @@ { - "assets/build/ba_data/audio/achievement.ogg": "https://files.ballistica.net/cache/ba1/58/60/72ea79f720fe3025edccb074795c", - "assets/build/ba_data/audio/actionHero1.ogg": "https://files.ballistica.net/cache/ba1/df/19/3dca52242f89fea536675460662d", - "assets/build/ba_data/audio/actionHero2.ogg": "https://files.ballistica.net/cache/ba1/cf/59/e9370637605e17e06782a3ded253", - "assets/build/ba_data/audio/actionHero3.ogg": "https://files.ballistica.net/cache/ba1/28/90/1bcbfedccddb1dbb75f1be534a40", - "assets/build/ba_data/audio/actionHero4.ogg": "https://files.ballistica.net/cache/ba1/4e/40/b163b9e464e3ad61fbb1669faccf", - "assets/build/ba_data/audio/actionHeroDeath.ogg": "https://files.ballistica.net/cache/ba1/ab/59/f8c8b4374989b7f4291897d12136", - "assets/build/ba_data/audio/actionHeroFall.ogg": "https://files.ballistica.net/cache/ba1/67/10/fbf1be54bb261de5be005b698ac6", - "assets/build/ba_data/audio/actionHeroHit1.ogg": "https://files.ballistica.net/cache/ba1/5b/6f/f33a2387e511c98990b503120332", - "assets/build/ba_data/audio/actionHeroHit2.ogg": "https://files.ballistica.net/cache/ba1/d0/50/1a0773c6d9f75f5a59397cc7f570", - "assets/build/ba_data/audio/activateBeep.ogg": "https://files.ballistica.net/cache/ba1/5a/e7/f46f191ed3bf9141ce584670f5ef", - "assets/build/ba_data/audio/agent1.ogg": "https://files.ballistica.net/cache/ba1/5f/1d/56573ce369bab1278f8f5ca3df20", - "assets/build/ba_data/audio/agent2.ogg": "https://files.ballistica.net/cache/ba1/96/ce/9d2fa9e025c18f6a549308398622", - "assets/build/ba_data/audio/agent3.ogg": "https://files.ballistica.net/cache/ba1/7a/02/192379d0855a384a6f4602070d2f", - "assets/build/ba_data/audio/agent4.ogg": "https://files.ballistica.net/cache/ba1/d6/c2/ded31e0ca74e18fb1cc9b2848eac", - "assets/build/ba_data/audio/agentDeath.ogg": "https://files.ballistica.net/cache/ba1/37/e2/799c72c85d5580ed91f3d07f353f", - "assets/build/ba_data/audio/agentFall.ogg": "https://files.ballistica.net/cache/ba1/f7/2d/b0f45c9b63c00011d0d0726e99dc", - "assets/build/ba_data/audio/agentHit1.ogg": "https://files.ballistica.net/cache/ba1/0d/62/79bcab98bfc875de8caf8cb584ce", - "assets/build/ba_data/audio/agentHit2.ogg": "https://files.ballistica.net/cache/ba1/15/bc/08460f83e57a794f273ca6b6a0ec", - "assets/build/ba_data/audio/alarm.ogg": "https://files.ballistica.net/cache/ba1/ad/60/b59451d596fcc3f01c74622fe91b", - "assets/build/ba_data/audio/ali1.ogg": "https://files.ballistica.net/cache/ba1/75/6f/f1db853370363e61b4b59fdc34f1", - "assets/build/ba_data/audio/ali2.ogg": "https://files.ballistica.net/cache/ba1/47/dd/5fbbbf4636d3f5f1b1ccdbcd7ee9", - "assets/build/ba_data/audio/ali3.ogg": "https://files.ballistica.net/cache/ba1/e7/e2/3d9421fe70a347e421817a2b7281", - "assets/build/ba_data/audio/ali4.ogg": "https://files.ballistica.net/cache/ba1/75/6d/3986f030b46a23325ffe590ae864", - "assets/build/ba_data/audio/aliDeath.ogg": "https://files.ballistica.net/cache/ba1/0d/55/3b5715fc528462a85857209bddf5", - "assets/build/ba_data/audio/aliFall.ogg": "https://files.ballistica.net/cache/ba1/9b/44/4ae14fa4e34460eff4ff79adca82", - "assets/build/ba_data/audio/aliHit1.ogg": "https://files.ballistica.net/cache/ba1/69/7f/34fddf7c9c23d5bdb88765f246f3", - "assets/build/ba_data/audio/aliHit2.ogg": "https://files.ballistica.net/cache/ba1/d3/67/871f6ee7fbe3c0b079c1c376cad4", - "assets/build/ba_data/audio/alien1.ogg": "https://files.ballistica.net/cache/ba1/8d/25/7e7e6484081f461a3c835a40466a", - "assets/build/ba_data/audio/alien2.ogg": "https://files.ballistica.net/cache/ba1/27/17/aeabc20283ce7542d5e5dc959efb", - "assets/build/ba_data/audio/alien3.ogg": "https://files.ballistica.net/cache/ba1/83/2e/46a27a8d9650cb64fd6cd2df865f", - "assets/build/ba_data/audio/alien4.ogg": "https://files.ballistica.net/cache/ba1/37/33/4f765d486286c1f998ae4b68d98e", - "assets/build/ba_data/audio/alienDeath.ogg": "https://files.ballistica.net/cache/ba1/92/99/e9fe339c6cb5d5d91d653f2a2894", - "assets/build/ba_data/audio/alienFall.ogg": "https://files.ballistica.net/cache/ba1/70/8e/8e2fb00c82452632854959c664ce", - "assets/build/ba_data/audio/alienHit1.ogg": "https://files.ballistica.net/cache/ba1/c4/9c/181dd79a43686ebb8cc05851d7ea", - "assets/build/ba_data/audio/alienHit2.ogg": "https://files.ballistica.net/cache/ba1/bb/3e/4238ca124fdf0bdbb9aac13549f8", - "assets/build/ba_data/audio/announceEight.ogg": "https://files.ballistica.net/cache/ba1/27/6a/574c3185d7c35d9480a839c8888a", - "assets/build/ba_data/audio/announceFive.ogg": "https://files.ballistica.net/cache/ba1/11/6f/072ad78bb410d30cf26d6e511d67", - "assets/build/ba_data/audio/announceFour.ogg": "https://files.ballistica.net/cache/ba1/32/f2/c02875245b900e5fbb7a384f2c17", - "assets/build/ba_data/audio/announceNine.ogg": "https://files.ballistica.net/cache/ba1/11/c0/47a7d6d8e578229db28b604932cb", - "assets/build/ba_data/audio/announceOne.ogg": "https://files.ballistica.net/cache/ba1/96/5f/5985e6a5cc5596d507d64908ecfa", - "assets/build/ba_data/audio/announceSeven.ogg": "https://files.ballistica.net/cache/ba1/3a/11/dcc1c4c6573f29852ebe55e5b549", - "assets/build/ba_data/audio/announceSix.ogg": "https://files.ballistica.net/cache/ba1/2a/22/a966bc15bab28ed56f8ff7e5bbfa", - "assets/build/ba_data/audio/announceTen.ogg": "https://files.ballistica.net/cache/ba1/e0/2e/5c614c252c2b3c24e63ae8498dbf", - "assets/build/ba_data/audio/announceThree.ogg": "https://files.ballistica.net/cache/ba1/dc/29/3172eee392826010e9d2fa9c138e", - "assets/build/ba_data/audio/announceTwo.ogg": "https://files.ballistica.net/cache/ba1/43/18/098574d833d394808a07ba36cede", - "assets/build/ba_data/audio/assassin1.ogg": "https://files.ballistica.net/cache/ba1/c3/5f/6598616f4f1f96c40413b71de9e8", - "assets/build/ba_data/audio/assassin2.ogg": "https://files.ballistica.net/cache/ba1/a4/8d/4116a1ea3715f1c4bb788819cd18", - "assets/build/ba_data/audio/assassin3.ogg": "https://files.ballistica.net/cache/ba1/4e/92/ab9b6565d11b89b73aaa265eb8f0", - "assets/build/ba_data/audio/assassin4.ogg": "https://files.ballistica.net/cache/ba1/7b/e0/a40c91b027bace02667c0fcf158c", - "assets/build/ba_data/audio/assassinDeath.ogg": "https://files.ballistica.net/cache/ba1/05/88/f550d7014923a2c6134f12628098", - "assets/build/ba_data/audio/assassinFall.ogg": "https://files.ballistica.net/cache/ba1/dd/df/94392604b6a346f8b11b696c8f1d", - "assets/build/ba_data/audio/assassinHit1.ogg": "https://files.ballistica.net/cache/ba1/c0/6f/de6f72745e850af65cf9a5dbb3a6", - "assets/build/ba_data/audio/assassinHit2.ogg": "https://files.ballistica.net/cache/ba1/9e/e2/a69db7c14065ee8d570a100a263b", - "assets/build/ba_data/audio/bear1.ogg": "https://files.ballistica.net/cache/ba1/72/49/9551e6ff43e4fdd863cca11134f4", - "assets/build/ba_data/audio/bear2.ogg": "https://files.ballistica.net/cache/ba1/d7/1f/2c830ac5e0a0c43f145a20fa2c25", - "assets/build/ba_data/audio/bear3.ogg": "https://files.ballistica.net/cache/ba1/aa/98/9f1a21aa9dac07d05085e8687068", - "assets/build/ba_data/audio/bear4.ogg": "https://files.ballistica.net/cache/ba1/c7/26/69891bac03ddbea748f9baec9f95", - "assets/build/ba_data/audio/bearDeath.ogg": "https://files.ballistica.net/cache/ba1/e9/eb/39834342979b9340776d814902bb", - "assets/build/ba_data/audio/bearFall.ogg": "https://files.ballistica.net/cache/ba1/15/6b/2be51a0c48077b44166ce9b5a133", - "assets/build/ba_data/audio/bearHit1.ogg": "https://files.ballistica.net/cache/ba1/96/22/6f56ec5c0b58ce8faf959c766a61", - "assets/build/ba_data/audio/bearHit2.ogg": "https://files.ballistica.net/cache/ba1/4f/98/5366c6a26a83e9618b8eca54780a", - "assets/build/ba_data/audio/bellHigh.ogg": "https://files.ballistica.net/cache/ba1/7c/02/4bc38047fc4301eb3c195e0539b9", - "assets/build/ba_data/audio/bellLow.ogg": "https://files.ballistica.net/cache/ba1/85/e0/6ebea452dc2a1284e6668f18e6ec", - "assets/build/ba_data/audio/bellMed.ogg": "https://files.ballistica.net/cache/ba1/a2/d0/57fc42d7e5b76024f0221b43cdfe", - "assets/build/ba_data/audio/bigImpact.ogg": "https://files.ballistica.net/cache/ba1/90/6c/07bb89e1e1d59f927c7530c88fe8", - "assets/build/ba_data/audio/bigImpact2.ogg": "https://files.ballistica.net/cache/ba1/04/81/87864481b02fecb9914e3b50e2a6", - "assets/build/ba_data/audio/blank.ogg": "https://files.ballistica.net/cache/ba1/55/70/90a49806365698031bd7c2a1e55f", - "assets/build/ba_data/audio/blip.ogg": "https://files.ballistica.net/cache/ba1/be/d9/c7bae6f1b9900727cdcffe70adad", - "assets/build/ba_data/audio/block.ogg": "https://files.ballistica.net/cache/ba1/f6/5e/e27553fb56edd40e854a8310ce6b", - "assets/build/ba_data/audio/bombDrop01.ogg": "https://files.ballistica.net/cache/ba1/1a/e8/2bd59450bb142dd8edb7fc875337", - "assets/build/ba_data/audio/bombDrop02.ogg": "https://files.ballistica.net/cache/ba1/28/f5/f7601c05201d1cb088f7b01dd244", - "assets/build/ba_data/audio/bombRoll01.ogg": "https://files.ballistica.net/cache/ba1/4b/64/e0c24177793c47547e3f57c7273b", - "assets/build/ba_data/audio/bones1.ogg": "https://files.ballistica.net/cache/ba1/66/99/fc98f957a6db4a93b5fbb5786b4e", - "assets/build/ba_data/audio/bones2.ogg": "https://files.ballistica.net/cache/ba1/a1/38/064ad0b952104f931c1bcc34d747", - "assets/build/ba_data/audio/bones3.ogg": "https://files.ballistica.net/cache/ba1/20/6e/665a3bd95dc7769c95938da89a2b", - "assets/build/ba_data/audio/bonesDeath.ogg": "https://files.ballistica.net/cache/ba1/2c/53/450b0d309913653756599fb54f25", - "assets/build/ba_data/audio/bonesFall.ogg": "https://files.ballistica.net/cache/ba1/b5/94/d157d5a2b02016caf4a3b40c358c", - "assets/build/ba_data/audio/boo.ogg": "https://files.ballistica.net/cache/ba1/9e/b6/e71d3e3cb099fd93034adc78d079", - "assets/build/ba_data/audio/boxDrop.ogg": "https://files.ballistica.net/cache/ba1/4a/b6/c9f4c108be9808134c15cf44e4b2", - "assets/build/ba_data/audio/boxingBell.ogg": "https://files.ballistica.net/cache/ba1/34/e1/05c5c1400662d2fb3a07594db5b7", - "assets/build/ba_data/audio/bunny1.ogg": "https://files.ballistica.net/cache/ba1/24/8f/bb616af82630e9dc7b98f29318a9", - "assets/build/ba_data/audio/bunny2.ogg": "https://files.ballistica.net/cache/ba1/9d/63/3b247d4a4de0a673008868342765", - "assets/build/ba_data/audio/bunny3.ogg": "https://files.ballistica.net/cache/ba1/0e/08/6490c22fe15808b214068bdc7753", - "assets/build/ba_data/audio/bunny4.ogg": "https://files.ballistica.net/cache/ba1/2a/b7/c480591a278edfe518df5e12ec6a", - "assets/build/ba_data/audio/bunnyDeath.ogg": "https://files.ballistica.net/cache/ba1/93/7d/152e5bf4fdd7880c3f0b9abf178d", - "assets/build/ba_data/audio/bunnyFall.ogg": "https://files.ballistica.net/cache/ba1/0c/31/97752e51b1ab8f581d467b453d29", - "assets/build/ba_data/audio/bunnyHit1.ogg": "https://files.ballistica.net/cache/ba1/1a/e9/7b62eeb9abb596b89bfe53c41df2", - "assets/build/ba_data/audio/bunnyHit2.ogg": "https://files.ballistica.net/cache/ba1/37/56/f44055d623b9f16a6d3ea52cf95d", - "assets/build/ba_data/audio/bunnyJump.ogg": "https://files.ballistica.net/cache/ba1/be/92/b63a629718a81effe5858530a2be", - "assets/build/ba_data/audio/cashRegister.ogg": "https://files.ballistica.net/cache/ba1/91/07/33e1d7d98479aeb0386898bc633e", - "assets/build/ba_data/audio/cashRegister2.ogg": "https://files.ballistica.net/cache/ba1/f8/12/74edab7ebda050f913428793f477", - "assets/build/ba_data/audio/charSelectMusic.ogg": "https://files.ballistica.net/cache/ba1/a7/6d/53b41cd118b34d9ab8254dbd2ff1", - "assets/build/ba_data/audio/cheer.ogg": "https://files.ballistica.net/cache/ba1/c7/ff/c962b9c39a35de2004198e9f03f1", - "assets/build/ba_data/audio/click01.ogg": "https://files.ballistica.net/cache/ba1/7e/a6/42511f8b555d460156424bc53f47", - "assets/build/ba_data/audio/corkPop.ogg": "https://files.ballistica.net/cache/ba1/68/a4/2b7c6d678fe1fa09cde1081c83ef", - "assets/build/ba_data/audio/cowboy1.ogg": "https://files.ballistica.net/cache/ba1/1b/0f/a0bea97d774aa221a3941f0e0af2", - "assets/build/ba_data/audio/cowboy2.ogg": "https://files.ballistica.net/cache/ba1/eb/da/b25db53f6a6d4871db7a7829a8b3", - "assets/build/ba_data/audio/cowboy3.ogg": "https://files.ballistica.net/cache/ba1/42/7c/8eeaec50726c9f6bbd62694bffc5", - "assets/build/ba_data/audio/cowboy4.ogg": "https://files.ballistica.net/cache/ba1/c6/f4/a0f56e61df27ced1d7f03b1dc458", - "assets/build/ba_data/audio/cowboyDeath.ogg": "https://files.ballistica.net/cache/ba1/cb/0e/abb677f438cd64b63dd6fc363d4a", - "assets/build/ba_data/audio/cowboyFall.ogg": "https://files.ballistica.net/cache/ba1/1d/af/c302b9caba1d7f736a6dd9f52268", - "assets/build/ba_data/audio/cowboyHit1.ogg": "https://files.ballistica.net/cache/ba1/71/57/0a9213ba8dea0404318893385068", - "assets/build/ba_data/audio/cowboyHit2.ogg": "https://files.ballistica.net/cache/ba1/cb/13/afa6a5a87981e947177695af64d5", - "assets/build/ba_data/audio/crowdChant.ogg": "https://files.ballistica.net/cache/ba1/f5/c0/92c75181f783dae40075d9c2b689", - "assets/build/ba_data/audio/cyborg1.ogg": "https://files.ballistica.net/cache/ba1/47/15/b4930affbf40421447247546fe99", - "assets/build/ba_data/audio/cyborg2.ogg": "https://files.ballistica.net/cache/ba1/b2/af/626e8e08ab19d5854e474b9195aa", - "assets/build/ba_data/audio/cyborg3.ogg": "https://files.ballistica.net/cache/ba1/8c/0c/b25cf168c367368689c2830361ba", - "assets/build/ba_data/audio/cyborg4.ogg": "https://files.ballistica.net/cache/ba1/38/85/714969eb77f14b8b89c1fdf4bb54", - "assets/build/ba_data/audio/cyborgDeath.ogg": "https://files.ballistica.net/cache/ba1/2f/16/437f9756452e7386a6f737e957db", - "assets/build/ba_data/audio/cyborgFall.ogg": "https://files.ballistica.net/cache/ba1/77/db/a912a8c114f815799738da41ad5f", - "assets/build/ba_data/audio/cyborgHit1.ogg": "https://files.ballistica.net/cache/ba1/86/0c/c988a0ec4cf50cadbb3617b51547", - "assets/build/ba_data/audio/cyborgHit2.ogg": "https://files.ballistica.net/cache/ba1/30/86/9a0fd20c447caa136f902c0995aa", - "assets/build/ba_data/audio/cymbal.ogg": "https://files.ballistica.net/cache/ba1/74/c6/0f51346d8bd9ca177d20c913d4bc", - "assets/build/ba_data/audio/debrisFall.ogg": "https://files.ballistica.net/cache/ba1/82/8a/cc77cccd8f885307237e3091a43f", - "assets/build/ba_data/audio/deek.ogg": "https://files.ballistica.net/cache/ba1/4c/cf/d44e49553a8f590e6044d5a29c40", - "assets/build/ba_data/audio/deek2.ogg": "https://files.ballistica.net/cache/ba1/a1/9d/6feb67617bd1d6b0e116ba8f4051", - "assets/build/ba_data/audio/ding.ogg": "https://files.ballistica.net/cache/ba1/49/44/70398a2997c7238349abd56f64e9", - "assets/build/ba_data/audio/dingSmall.ogg": "https://files.ballistica.net/cache/ba1/ce/5d/6de4581beee72fd2dbf78e884f6d", - "assets/build/ba_data/audio/dingSmallHigh.ogg": "https://files.ballistica.net/cache/ba1/d8/6c/c290d65aa888309b14172b4550f5", - "assets/build/ba_data/audio/dripity.ogg": "https://files.ballistica.net/cache/ba1/5a/c7/2012a0547b43f4fe22300ba1e435", - "assets/build/ba_data/audio/drumRoll.ogg": "https://files.ballistica.net/cache/ba1/f9/c9/eb8cdc6911207e1b9fc37c856550", - "assets/build/ba_data/audio/error.ogg": "https://files.ballistica.net/cache/ba1/f0/fb/8db7d282d17f6549439472a17443", - "assets/build/ba_data/audio/explosion01.ogg": "https://files.ballistica.net/cache/ba1/7e/5c/60d2827cf919ce3a2a742c13656d", - "assets/build/ba_data/audio/explosion02.ogg": "https://files.ballistica.net/cache/ba1/40/5b/0ad2e9b46bdbeb5117c6c35d0c06", - "assets/build/ba_data/audio/explosion03.ogg": "https://files.ballistica.net/cache/ba1/fc/55/a9abae0b44d7a38e8965dbe0e82d", - "assets/build/ba_data/audio/explosion04.ogg": "https://files.ballistica.net/cache/ba1/3e/1d/6956a0ebd528c85d928f64e986ad", - "assets/build/ba_data/audio/explosion05.ogg": "https://files.ballistica.net/cache/ba1/1f/6a/ad37d5a4a16390ff4984008c4529", - "assets/build/ba_data/audio/fanfare.ogg": "https://files.ballistica.net/cache/ba1/65/89/78fb925efb8e70a3a7b3b8e6eb22", - "assets/build/ba_data/audio/flagCatcherMusic.ogg": "https://files.ballistica.net/cache/ba1/d2/63/1214d61047ae93562e81ebb5ff31", - "assets/build/ba_data/audio/flyingMusic.ogg": "https://files.ballistica.net/cache/ba1/84/62/853c3e325b4709e59e4d08c05f71", - "assets/build/ba_data/audio/foghorn.ogg": "https://files.ballistica.net/cache/ba1/74/26/2338cc1801cfa18e1e106aeb4ae6", - "assets/build/ba_data/audio/footImpact01.ogg": "https://files.ballistica.net/cache/ba1/d2/86/b525c0f04e5118b20d55cd075b8d", - "assets/build/ba_data/audio/footImpact02.ogg": "https://files.ballistica.net/cache/ba1/cc/c9/3a00e1b3f628412877c88f1be1dd", - "assets/build/ba_data/audio/footImpact03.ogg": "https://files.ballistica.net/cache/ba1/15/43/3e555fb6ea381f384d3ac1646707", - "assets/build/ba_data/audio/forwardMarchMusic.ogg": "https://files.ballistica.net/cache/ba1/4f/bd/c9a96a8949fca5328df20bf6c160", - "assets/build/ba_data/audio/freeze.ogg": "https://files.ballistica.net/cache/ba1/02/ab/e598b181e844a065925f2bf97963", - "assets/build/ba_data/audio/frosty01.ogg": "https://files.ballistica.net/cache/ba1/ef/6d/3400f25717f7f442dd8d1419033e", - "assets/build/ba_data/audio/frosty02.ogg": "https://files.ballistica.net/cache/ba1/76/38/5854a95ec204f41e43b50d263409", - "assets/build/ba_data/audio/frosty03.ogg": "https://files.ballistica.net/cache/ba1/a8/16/60fc348f2edc081c4c338e7956e6", - "assets/build/ba_data/audio/frosty04.ogg": "https://files.ballistica.net/cache/ba1/11/f0/7860caaada9174a47251c9c66018", - "assets/build/ba_data/audio/frosty05.ogg": "https://files.ballistica.net/cache/ba1/45/8f/def4659833a1dbd76f14b13d2bff", - "assets/build/ba_data/audio/frostyDeath.ogg": "https://files.ballistica.net/cache/ba1/d4/bd/d214a161586e03503c21d859b615", - "assets/build/ba_data/audio/frostyFall.ogg": "https://files.ballistica.net/cache/ba1/e9/44/98873e7072b05ab1fda1f35ee9cf", - "assets/build/ba_data/audio/frostyHit01.ogg": "https://files.ballistica.net/cache/ba1/7d/c9/5a6686a9f625692a51845f8b1a9e", - "assets/build/ba_data/audio/frostyHit02.ogg": "https://files.ballistica.net/cache/ba1/65/d3/bd26b06bc0d3840d12971a8ff7a3", - "assets/build/ba_data/audio/frostyHit03.ogg": "https://files.ballistica.net/cache/ba1/62/b8/ff9ae4125d95afff1eabfa70e85f", - "assets/build/ba_data/audio/fuse01.ogg": "https://files.ballistica.net/cache/ba1/e0/33/05a6deab05d5fc024078294911e6", - "assets/build/ba_data/audio/gladiator1.ogg": "https://files.ballistica.net/cache/ba1/62/c9/9a1677c7ac53aa1e2d86b6a41cb8", - "assets/build/ba_data/audio/gladiator2.ogg": "https://files.ballistica.net/cache/ba1/01/03/826a4dc462a2ffde058c229563c4", - "assets/build/ba_data/audio/gladiator3.ogg": "https://files.ballistica.net/cache/ba1/75/ac/4153c1bae224e124e6a0c14f3767", - "assets/build/ba_data/audio/gladiator4.ogg": "https://files.ballistica.net/cache/ba1/41/57/9d42f96a7b4f2501516c76acd8eb", - "assets/build/ba_data/audio/gladiatorDeath.ogg": "https://files.ballistica.net/cache/ba1/0b/3f/7b823002ac4823e692f3e3772e6d", - "assets/build/ba_data/audio/gladiatorFall.ogg": "https://files.ballistica.net/cache/ba1/59/52/bd80868601bbe451ea2b8bbedd3b", - "assets/build/ba_data/audio/gladiatorHit1.ogg": "https://files.ballistica.net/cache/ba1/d5/f1/eec7713ca5613b27827f2cecd23e", - "assets/build/ba_data/audio/gladiatorHit2.ogg": "https://files.ballistica.net/cache/ba1/8d/f6/65d8116517cedabd94498fad52f5", - "assets/build/ba_data/audio/gong.ogg": "https://files.ballistica.net/cache/ba1/22/74/cbebc4aea2e05e0b04acc6505247", - "assets/build/ba_data/audio/grandRompMusic.ogg": "https://files.ballistica.net/cache/ba1/d8/8d/c58f032720cf364033eca76836db", - "assets/build/ba_data/audio/gravelSkid.ogg": "https://files.ballistica.net/cache/ba1/53/45/06c085914c1f9faef2486bb97437", - "assets/build/ba_data/audio/gunCocking.ogg": "https://files.ballistica.net/cache/ba1/d8/ce/edd7befc975296aef73d732bb663", - "assets/build/ba_data/audio/healthPowerup.ogg": "https://files.ballistica.net/cache/ba1/f0/34/72c44aef2241471debca16f03870", - "assets/build/ba_data/audio/hiss.ogg": "https://files.ballistica.net/cache/ba1/04/3a/9b820db7a76beb8271c9737558ba", - "assets/build/ba_data/audio/impactHard.ogg": "https://files.ballistica.net/cache/ba1/30/e7/9e78011593669448536cfc97cfd2", - "assets/build/ba_data/audio/impactHard2.ogg": "https://files.ballistica.net/cache/ba1/3e/5f/4da1dd1467abf87db71303958898", - "assets/build/ba_data/audio/impactHard3.ogg": "https://files.ballistica.net/cache/ba1/19/b6/3350b18401d089a06ed953b664d8", - "assets/build/ba_data/audio/impactMedium.ogg": "https://files.ballistica.net/cache/ba1/fc/50/e6d167d4c54f01a23a8a509c0033", - "assets/build/ba_data/audio/impactMedium2.ogg": "https://files.ballistica.net/cache/ba1/d9/c9/594cee989be6e1101e5b81f84e21", - "assets/build/ba_data/audio/jack01.ogg": "https://files.ballistica.net/cache/ba1/e9/1d/924768dd46f3ebba712f7c4671fd", - "assets/build/ba_data/audio/jack02.ogg": "https://files.ballistica.net/cache/ba1/bd/6b/6f1071d7151a0c2cfb58203d3b0a", - "assets/build/ba_data/audio/jack03.ogg": "https://files.ballistica.net/cache/ba1/fe/2e/ee03645ecc98b87351b36dc8bad8", - "assets/build/ba_data/audio/jack04.ogg": "https://files.ballistica.net/cache/ba1/04/eb/2b842246184c956b63e03a030eb0", - "assets/build/ba_data/audio/jack05.ogg": "https://files.ballistica.net/cache/ba1/ee/9b/e9505d780f8df0b7f52521633033", - "assets/build/ba_data/audio/jack06.ogg": "https://files.ballistica.net/cache/ba1/28/c1/fc96bb381c38fe24db32ce943d39", - "assets/build/ba_data/audio/jackDeath01.ogg": "https://files.ballistica.net/cache/ba1/10/ad/a44b04f740f29112e146f61d08dd", - "assets/build/ba_data/audio/jackFall01.ogg": "https://files.ballistica.net/cache/ba1/d8/69/8185e28dedad83a6ce42412eca0b", - "assets/build/ba_data/audio/jackHit01.ogg": "https://files.ballistica.net/cache/ba1/c9/7d/076eb7ef2e45129e8f7870d2b5fa", - "assets/build/ba_data/audio/jackHit02.ogg": "https://files.ballistica.net/cache/ba1/cc/8c/8196a164dfa4a7304753767bc806", - "assets/build/ba_data/audio/jackHit03.ogg": "https://files.ballistica.net/cache/ba1/c0/93/280556ce2e97fbb3cfd43d4cc6f8", - "assets/build/ba_data/audio/jackHit04.ogg": "https://files.ballistica.net/cache/ba1/7d/2e/59fb210c91477798962076d40bed", - "assets/build/ba_data/audio/jackHit05.ogg": "https://files.ballistica.net/cache/ba1/d9/15/8db623ab332a7adf3087c54eb439", - "assets/build/ba_data/audio/jackHit06.ogg": "https://files.ballistica.net/cache/ba1/14/25/d85c7443244ff9c4b762cfba7d45", - "assets/build/ba_data/audio/jackHit07.ogg": "https://files.ballistica.net/cache/ba1/c3/92/70e73537e642c3de7c0d1514c62c", - "assets/build/ba_data/audio/jumpsuit1.ogg": "https://files.ballistica.net/cache/ba1/1d/e6/caeb8f3d75ad8d00a896493d4981", - "assets/build/ba_data/audio/jumpsuit2.ogg": "https://files.ballistica.net/cache/ba1/9d/65/5bada79073612f81dada09cdd669", - "assets/build/ba_data/audio/jumpsuit3.ogg": "https://files.ballistica.net/cache/ba1/ec/97/1807c9196930f55ed29080374395", - "assets/build/ba_data/audio/jumpsuit4.ogg": "https://files.ballistica.net/cache/ba1/3c/84/d62da6a915e3255434a0929e4ec8", - "assets/build/ba_data/audio/jumpsuitDeath.ogg": "https://files.ballistica.net/cache/ba1/8c/b3/525216f8302f9d948095bfa9799a", - "assets/build/ba_data/audio/jumpsuitFall.ogg": "https://files.ballistica.net/cache/ba1/7f/af/6b411b9a46ad8be33e15dc9e6429", - "assets/build/ba_data/audio/jumpsuitHit1.ogg": "https://files.ballistica.net/cache/ba1/a6/29/bed67bd2fa608427b5b27703a369", - "assets/build/ba_data/audio/jumpsuitHit2.ogg": "https://files.ballistica.net/cache/ba1/de/ff/aac029f85323dfde023607bd5b98", - "assets/build/ba_data/audio/kronk1.ogg": "https://files.ballistica.net/cache/ba1/a5/25/6ffeb4f7c0b479f6ea331fad4cb0", - "assets/build/ba_data/audio/kronk10.ogg": "https://files.ballistica.net/cache/ba1/e0/72/50b27a07c338b3ea9467877fcba6", - "assets/build/ba_data/audio/kronk2.ogg": "https://files.ballistica.net/cache/ba1/d3/4a/ab1f9a3af8ec7a35dc4c84ebec93", - "assets/build/ba_data/audio/kronk3.ogg": "https://files.ballistica.net/cache/ba1/78/3e/04ed2f524aa8c9d7cc20240dd102", - "assets/build/ba_data/audio/kronk4.ogg": "https://files.ballistica.net/cache/ba1/a7/90/67fc1bcc1e29225c6560b22dc797", - "assets/build/ba_data/audio/kronk5.ogg": "https://files.ballistica.net/cache/ba1/47/73/ebadf9e3e762761b486574f92ae5", - "assets/build/ba_data/audio/kronk6.ogg": "https://files.ballistica.net/cache/ba1/d7/12/eb34c17c7eae64861019d8b9d0e4", - "assets/build/ba_data/audio/kronk7.ogg": "https://files.ballistica.net/cache/ba1/50/12/4bbf79acb96de2891d0c04793dd8", - "assets/build/ba_data/audio/kronk8.ogg": "https://files.ballistica.net/cache/ba1/fb/3d/cf3678af97b1e9453b2a5ccf6556", - "assets/build/ba_data/audio/kronk9.ogg": "https://files.ballistica.net/cache/ba1/2b/5d/80ac256b9a0395ce0c1df07198b7", - "assets/build/ba_data/audio/kronkDeath.ogg": "https://files.ballistica.net/cache/ba1/a8/86/adb0bcf982f41aef1266bfef4ac3", - "assets/build/ba_data/audio/kronkFall.ogg": "https://files.ballistica.net/cache/ba1/01/bc/933e24668420f71aa5a7f405166e", - "assets/build/ba_data/audio/laser.ogg": "https://files.ballistica.net/cache/ba1/9e/ef/c95d203792e7c2410cf48b8e6242", - "assets/build/ba_data/audio/laserReverse.ogg": "https://files.ballistica.net/cache/ba1/ee/de/e4e5513a1e0b45bd7c21802a0680", - "assets/build/ba_data/audio/mel01.ogg": "https://files.ballistica.net/cache/ba1/df/e6/9597698cb9abe30ceade8ab682e1", - "assets/build/ba_data/audio/mel02.ogg": "https://files.ballistica.net/cache/ba1/28/0e/41a16319712fc1c0d7999d439dff", - "assets/build/ba_data/audio/mel03.ogg": "https://files.ballistica.net/cache/ba1/bf/b0/5ae8a0a1c92664a078869125aa33", - "assets/build/ba_data/audio/mel04.ogg": "https://files.ballistica.net/cache/ba1/62/8d/30104cf07b07d7f8044324587f8b", - "assets/build/ba_data/audio/mel05.ogg": "https://files.ballistica.net/cache/ba1/a3/ba/7e268b1bf136ab431ed8aa600c92", - "assets/build/ba_data/audio/mel06.ogg": "https://files.ballistica.net/cache/ba1/18/e1/f9a292341b7ae940dcdfc9af6dde", - "assets/build/ba_data/audio/mel07.ogg": "https://files.ballistica.net/cache/ba1/4e/42/c512f97edc448246a89e4c92f9c8", - "assets/build/ba_data/audio/mel08.ogg": "https://files.ballistica.net/cache/ba1/73/52/3930c7eb099dfe89fbd09c90309c", - "assets/build/ba_data/audio/mel09.ogg": "https://files.ballistica.net/cache/ba1/22/79/82f9d313915592a142e8cf0c6e99", - "assets/build/ba_data/audio/mel10.ogg": "https://files.ballistica.net/cache/ba1/a0/3a/067da88e50b7fb576cb30441065e", - "assets/build/ba_data/audio/melDeath01.ogg": "https://files.ballistica.net/cache/ba1/cd/e1/b3a3cf043eba71e1f6fdecb010f2", - "assets/build/ba_data/audio/melFall01.ogg": "https://files.ballistica.net/cache/ba1/fd/48/d9903063f4a5cd445f140280faba", - "assets/build/ba_data/audio/menuMusic.ogg": "https://files.ballistica.net/cache/ba1/61/25/949add05fbdc5d6d163192a5bf96", - "assets/build/ba_data/audio/metalHit.ogg": "https://files.ballistica.net/cache/ba1/ee/80/b0305bfb666556e8931ec5729088", - "assets/build/ba_data/audio/metalSkid.ogg": "https://files.ballistica.net/cache/ba1/de/81/6d1597d6bb6ae20d6715c52b8757", - "assets/build/ba_data/audio/ninjaAttack1.ogg": "https://files.ballistica.net/cache/ba1/b0/ec/7e6ea6898010fc117fe7ab3cfd14", - "assets/build/ba_data/audio/ninjaAttack2.ogg": "https://files.ballistica.net/cache/ba1/b9/2a/1b3045380c083e67252cade17f60", - "assets/build/ba_data/audio/ninjaAttack3.ogg": "https://files.ballistica.net/cache/ba1/83/fe/6854cffa06c237e8ba11aa1c3811", - "assets/build/ba_data/audio/ninjaAttack4.ogg": "https://files.ballistica.net/cache/ba1/09/f7/5b3de6c0c466c3fc6d2f6b19102c", - "assets/build/ba_data/audio/ninjaAttack5.ogg": "https://files.ballistica.net/cache/ba1/ec/42/e0784271bec8a82dfb54201c2e1c", - "assets/build/ba_data/audio/ninjaAttack6.ogg": "https://files.ballistica.net/cache/ba1/b2/e6/20743d838354bbc70b0eab0942e6", - "assets/build/ba_data/audio/ninjaAttack7.ogg": "https://files.ballistica.net/cache/ba1/9e/bb/e7002c6810d6a66d1a9779091364", - "assets/build/ba_data/audio/ninjaDeath1.ogg": "https://files.ballistica.net/cache/ba1/b8/62/c9f0b5aed52a44cda7c1d874f096", - "assets/build/ba_data/audio/ninjaFall1.ogg": "https://files.ballistica.net/cache/ba1/45/f8/461537290f94f4e93e6d127e73eb", - "assets/build/ba_data/audio/ninjaHit1.ogg": "https://files.ballistica.net/cache/ba1/b5/4d/0acb8fe85d7c67ec8afe3005ae2a", - "assets/build/ba_data/audio/ninjaHit2.ogg": "https://files.ballistica.net/cache/ba1/41/7b/bc492022c86f91cd38a203a7dbc8", - "assets/build/ba_data/audio/ninjaHit3.ogg": "https://files.ballistica.net/cache/ba1/21/a9/4bf5331435ab7f0118a769efc82e", - "assets/build/ba_data/audio/ninjaHit4.ogg": "https://files.ballistica.net/cache/ba1/90/9a/6a012a5e6782726a5e2d6fe91f44", - "assets/build/ba_data/audio/ninjaHit5.ogg": "https://files.ballistica.net/cache/ba1/30/ef/7d13306f36cb7827273062c3f2a4", - "assets/build/ba_data/audio/ninjaHit6.ogg": "https://files.ballistica.net/cache/ba1/5f/fe/fb2532d2e6de096a9feeab7f6c17", - "assets/build/ba_data/audio/ninjaHit7.ogg": "https://files.ballistica.net/cache/ba1/57/56/0ba4ab0f1233ef72108ccda9ccfa", - "assets/build/ba_data/audio/ninjaHit8.ogg": "https://files.ballistica.net/cache/ba1/d2/d9/b1fbfc0a864161fc879785b544c5", - "assets/build/ba_data/audio/oldLady1.ogg": "https://files.ballistica.net/cache/ba1/27/9c/4ae89ba2d90c5491a05a9098dfb7", - "assets/build/ba_data/audio/oldLady2.ogg": "https://files.ballistica.net/cache/ba1/6d/55/0f3a7a838905dde023bd93775ebb", - "assets/build/ba_data/audio/oldLady3.ogg": "https://files.ballistica.net/cache/ba1/d8/77/08e31296569e99eaea6e3335dac3", - "assets/build/ba_data/audio/oldLady4.ogg": "https://files.ballistica.net/cache/ba1/b7/96/5f7dd93a643a3ab1f5d6a0a4e8c3", - "assets/build/ba_data/audio/oldLadyDeath.ogg": "https://files.ballistica.net/cache/ba1/71/f0/ee089bbd54bb57734d09477cc2d9", - "assets/build/ba_data/audio/oldLadyFall.ogg": "https://files.ballistica.net/cache/ba1/b3/4b/a3dc06498650caee6290e364f2d6", - "assets/build/ba_data/audio/oldLadyHit1.ogg": "https://files.ballistica.net/cache/ba1/4a/66/358ad6790d13b4b2edcfc3e0f271", - "assets/build/ba_data/audio/oldLadyHit2.ogg": "https://files.ballistica.net/cache/ba1/18/cb/d7c3037b62cd77781fe4f12691fa", - "assets/build/ba_data/audio/ooh.ogg": "https://files.ballistica.net/cache/ba1/b7/28/6cefdb25ff8d908cc3feae409db3", - "assets/build/ba_data/audio/operaSinger1.ogg": "https://files.ballistica.net/cache/ba1/1f/ae/192442b203a9ea76ede63f8c6703", - "assets/build/ba_data/audio/operaSinger2.ogg": "https://files.ballistica.net/cache/ba1/ec/bd/5f82598f65e996cd38808750d31e", - "assets/build/ba_data/audio/operaSinger3.ogg": "https://files.ballistica.net/cache/ba1/6f/05/650ce355834ff955bd92e826bf61", - "assets/build/ba_data/audio/operaSinger4.ogg": "https://files.ballistica.net/cache/ba1/d9/7b/912192363ea7bdefad644d581031", - "assets/build/ba_data/audio/operaSingerDeath.ogg": "https://files.ballistica.net/cache/ba1/80/3b/9f46ff5e3935ef65a5fc241f6f69", - "assets/build/ba_data/audio/operaSingerFall.ogg": "https://files.ballistica.net/cache/ba1/31/87/7a3b7d9a0f30b97ea4c9465b1ed6", - "assets/build/ba_data/audio/operaSingerHit1.ogg": "https://files.ballistica.net/cache/ba1/af/f5/eed5e1c078a8ebb42da718d8eb2c", - "assets/build/ba_data/audio/operaSingerHit2.ogg": "https://files.ballistica.net/cache/ba1/81/9d/d2d943aec38c910aec3cd37ca8ce", - "assets/build/ba_data/audio/orchestraHit.ogg": "https://files.ballistica.net/cache/ba1/10/52/c067809e0e67ada9f51b7df4f905", - "assets/build/ba_data/audio/orchestraHit2.ogg": "https://files.ballistica.net/cache/ba1/cf/c5/f176dba87e6b8e9d8b23f79b6875", - "assets/build/ba_data/audio/orchestraHit3.ogg": "https://files.ballistica.net/cache/ba1/22/db/069cd352d268b36532fe44863a2c", - "assets/build/ba_data/audio/orchestraHit4.ogg": "https://files.ballistica.net/cache/ba1/0d/ca/e27abfc328e28c6ef394d7ae8820", - "assets/build/ba_data/audio/orchestraHitBig1.ogg": "https://files.ballistica.net/cache/ba1/b6/8f/310a8e3b4584274806ec4a623a37", - "assets/build/ba_data/audio/orchestraHitBig2.ogg": "https://files.ballistica.net/cache/ba1/36/7f/4d6affb3c51d5ab2d1f2c8161fbf", - "assets/build/ba_data/audio/penguin1.ogg": "https://files.ballistica.net/cache/ba1/05/e8/6a85b96b711a24d6b3ed85754de6", - "assets/build/ba_data/audio/penguin2.ogg": "https://files.ballistica.net/cache/ba1/a6/1c/684a4571e1e99b214ca21d600e41", - "assets/build/ba_data/audio/penguin3.ogg": "https://files.ballistica.net/cache/ba1/79/13/d728dd568a344cc7383a88a0f05a", - "assets/build/ba_data/audio/penguin4.ogg": "https://files.ballistica.net/cache/ba1/2d/f2/5b11c310268ef32a62b2ba4dcea2", - "assets/build/ba_data/audio/penguinDeath.ogg": "https://files.ballistica.net/cache/ba1/df/68/cd8363872bcef0b18d509685e3d0", - "assets/build/ba_data/audio/penguinFall.ogg": "https://files.ballistica.net/cache/ba1/a3/bb/28be81b10af2e47443e55e114878", - "assets/build/ba_data/audio/penguinHit1.ogg": "https://files.ballistica.net/cache/ba1/11/3a/9cd37ea4d2d61015c395d0e33815", - "assets/build/ba_data/audio/penguinHit2.ogg": "https://files.ballistica.net/cache/ba1/d2/5f/115f8eb45fff640967df5749383c", - "assets/build/ba_data/audio/pixie1.ogg": "https://files.ballistica.net/cache/ba1/f6/d6/8a1395d4d7305e8633f6e1caf0fb", - "assets/build/ba_data/audio/pixie2.ogg": "https://files.ballistica.net/cache/ba1/c0/5b/d0d2547b0887b7d7bbd81500b89a", - "assets/build/ba_data/audio/pixie3.ogg": "https://files.ballistica.net/cache/ba1/95/23/77e22310dd9377bb7a3444ac2bd7", - "assets/build/ba_data/audio/pixie4.ogg": "https://files.ballistica.net/cache/ba1/05/8b/81cf7b359746d76c762e2fd6004d", - "assets/build/ba_data/audio/pixieDeath.ogg": "https://files.ballistica.net/cache/ba1/e7/be/8a191fa33b425029a7e13625fa91", - "assets/build/ba_data/audio/pixieFall.ogg": "https://files.ballistica.net/cache/ba1/d5/64/34231aa7e9d336fa462cfaaec999", - "assets/build/ba_data/audio/pixieHit1.ogg": "https://files.ballistica.net/cache/ba1/e3/05/08b1c6f7e5906e6cb992dfbe514b", - "assets/build/ba_data/audio/pixieHit2.ogg": "https://files.ballistica.net/cache/ba1/ec/3f/c663c857d7530f216052ce814e97", - "assets/build/ba_data/audio/playerDeath.ogg": "https://files.ballistica.net/cache/ba1/96/a2/1e632a728ef300bcae2ff2d717a6", - "assets/build/ba_data/audio/playerLeft.ogg": "https://files.ballistica.net/cache/ba1/79/d8/7713140a3ede3b65e3bcdcba7d26", - "assets/build/ba_data/audio/pop01.ogg": "https://files.ballistica.net/cache/ba1/cc/48/4c7972a8ec6e7dbecc50debf05c9", - "assets/build/ba_data/audio/powerdown01.ogg": "https://files.ballistica.net/cache/ba1/a8/6d/9302b1a73bcbd9764f41875d7380", - "assets/build/ba_data/audio/powerup01.ogg": "https://files.ballistica.net/cache/ba1/fb/d4/deac2edc909c60ca651080a53794", - "assets/build/ba_data/audio/punch01.ogg": "https://files.ballistica.net/cache/ba1/55/4c/c31706350cfbb2a86af697848dab", - "assets/build/ba_data/audio/punchStrong01.ogg": "https://files.ballistica.net/cache/ba1/0d/7a/f1da7cbbd0597e5854a66efc2a88", - "assets/build/ba_data/audio/punchStrong02.ogg": "https://files.ballistica.net/cache/ba1/4e/c0/e100a891c673a56deeee605602b3", - "assets/build/ba_data/audio/punchSwish.ogg": "https://files.ballistica.net/cache/ba1/11/0c/d80e4d0842595fdc0923c4195ceb", - "assets/build/ba_data/audio/punchWeak01.ogg": "https://files.ballistica.net/cache/ba1/20/d3/08715bb8a195bc632c477469aed1", - "assets/build/ba_data/audio/raceBeep1.ogg": "https://files.ballistica.net/cache/ba1/53/5d/c54ae886ee8f56d3bb060fb0dfcb", - "assets/build/ba_data/audio/raceBeep2.ogg": "https://files.ballistica.net/cache/ba1/96/bc/455cf8d28acb205d13f01d4900bc", - "assets/build/ba_data/audio/refWhistle.ogg": "https://files.ballistica.net/cache/ba1/87/77/008ed6d8eac37f8b5aeabd8879c7", - "assets/build/ba_data/audio/robot1.ogg": "https://files.ballistica.net/cache/ba1/18/86/1ff23c315df417652e854b440c34", - "assets/build/ba_data/audio/robot2.ogg": "https://files.ballistica.net/cache/ba1/88/85/9f0ccaf6facb14a0a17d5ec89160", - "assets/build/ba_data/audio/robot3.ogg": "https://files.ballistica.net/cache/ba1/ac/92/647c8b99c83c1230d0709a27107b", - "assets/build/ba_data/audio/robot4.ogg": "https://files.ballistica.net/cache/ba1/ac/1e/6d1decef47d1fe68c36373f44cb4", - "assets/build/ba_data/audio/robotDeath.ogg": "https://files.ballistica.net/cache/ba1/0e/89/8e8dc3869dfa1349cfe6f400bd82", - "assets/build/ba_data/audio/robotFall.ogg": "https://files.ballistica.net/cache/ba1/02/d9/60eddc36b7ee6fbb02ae1ddb6b9e", - "assets/build/ba_data/audio/robotHit1.ogg": "https://files.ballistica.net/cache/ba1/eb/e2/f7f1492bd1ef17599b4253dea8e1", - "assets/build/ba_data/audio/robotHit2.ogg": "https://files.ballistica.net/cache/ba1/72/c3/3c9343953f884808f5452e6c7a1e", - "assets/build/ba_data/audio/runAwayMusic.ogg": "https://files.ballistica.net/cache/ba1/5a/d2/8dd3ec953d5a99a454250cee0cf2", - "assets/build/ba_data/audio/santa01.ogg": "https://files.ballistica.net/cache/ba1/f4/a4/18b599e1a8d2030e5ac71f57caad", - "assets/build/ba_data/audio/santa02.ogg": "https://files.ballistica.net/cache/ba1/c2/68/5bf7fc6c40706a830fa8feafd76d", - "assets/build/ba_data/audio/santa03.ogg": "https://files.ballistica.net/cache/ba1/0f/79/5ba921fe0af18aadc66b56443cd1", - "assets/build/ba_data/audio/santa04.ogg": "https://files.ballistica.net/cache/ba1/c7/7c/99c0b38cd9b457eafcc933efe0c4", - "assets/build/ba_data/audio/santa05.ogg": "https://files.ballistica.net/cache/ba1/e3/2f/03b8d771be498553d76dc3e1ca77", - "assets/build/ba_data/audio/santaDeath.ogg": "https://files.ballistica.net/cache/ba1/af/d2/17ef3bd225d342711fdaeaa13292", - "assets/build/ba_data/audio/santaFall.ogg": "https://files.ballistica.net/cache/ba1/3a/35/e7fd2c83bfa02fb4ef12226093bc", - "assets/build/ba_data/audio/santaHit01.ogg": "https://files.ballistica.net/cache/ba1/e5/a3/8212320d1f05b0abc8219f15283f", - "assets/build/ba_data/audio/santaHit02.ogg": "https://files.ballistica.net/cache/ba1/3f/56/d4f4afae2fe9f7d47fa57b2234c4", - "assets/build/ba_data/audio/santaHit03.ogg": "https://files.ballistica.net/cache/ba1/1d/63/5ba4653014fbc2885db708afd1ae", - "assets/build/ba_data/audio/santaHit04.ogg": "https://files.ballistica.net/cache/ba1/9c/fe/f0a5d119bf94ecd4547e81f12d7d", - "assets/build/ba_data/audio/scamper01.ogg": "https://files.ballistica.net/cache/ba1/b0/c0/77de611e53981f058e85d9d747b3", - "assets/build/ba_data/audio/scaryMusic.ogg": "https://files.ballistica.net/cache/ba1/62/a4/3f9af814e9a4796e994bde555a8f", - "assets/build/ba_data/audio/score.ogg": "https://files.ballistica.net/cache/ba1/5e/94/05dc50f166c99202a4aa2d106fff", - "assets/build/ba_data/audio/scoreHit01.ogg": "https://files.ballistica.net/cache/ba1/81/97/4ffd5ba4357e44df8f3ec37d75ed", - "assets/build/ba_data/audio/scoreHit02.ogg": "https://files.ballistica.net/cache/ba1/74/eb/220feb610faafde1e4601ff1dc65", - "assets/build/ba_data/audio/scoreIncrease.ogg": "https://files.ballistica.net/cache/ba1/cd/31/7ea93db05ec08d109bc53ff27114", - "assets/build/ba_data/audio/scoresEpicMusic.ogg": "https://files.ballistica.net/cache/ba1/16/63/0f6f47b8335573ae995d48671761", - "assets/build/ba_data/audio/shatter.ogg": "https://files.ballistica.net/cache/ba1/8c/10/27917e07d416be160445fbc51b30", - "assets/build/ba_data/audio/shieldDown.ogg": "https://files.ballistica.net/cache/ba1/f3/d8/039af57511dcf0c6a357a8607e31", - "assets/build/ba_data/audio/shieldHit.ogg": "https://files.ballistica.net/cache/ba1/ac/d8/191e528070dcfae914b1cd3a9ac3", - "assets/build/ba_data/audio/shieldUp.ogg": "https://files.ballistica.net/cache/ba1/eb/26/0d5329d330ee90542e8f7827ae9f", - "assets/build/ba_data/audio/skid01.ogg": "https://files.ballistica.net/cache/ba1/4f/0b/e12f1bd0b6e6b6246d5367ba492b", - "assets/build/ba_data/audio/slowEpicMusic.ogg": "https://files.ballistica.net/cache/ba1/5c/2e/7aa87fc4557c43009247e89d1a7e", - "assets/build/ba_data/audio/sparkle01.ogg": "https://files.ballistica.net/cache/ba1/06/97/865ba23d8393f0a6856e5ff9fa9f", - "assets/build/ba_data/audio/sparkle02.ogg": "https://files.ballistica.net/cache/ba1/e9/34/c7306677c23abe96b25005c4b227", - "assets/build/ba_data/audio/sparkle03.ogg": "https://files.ballistica.net/cache/ba1/fc/40/5883d06dc893f796276288494cda", - "assets/build/ba_data/audio/spawn.ogg": "https://files.ballistica.net/cache/ba1/1c/ad/832c7ca8efbd1933d05994b7f97c", - "assets/build/ba_data/audio/spazAttack01.ogg": "https://files.ballistica.net/cache/ba1/d0/af/5bba53f113f3ddd418da226a6383", - "assets/build/ba_data/audio/spazAttack02.ogg": "https://files.ballistica.net/cache/ba1/5c/99/cc38e0ef3afdce2e8957af33b100", - "assets/build/ba_data/audio/spazAttack03.ogg": "https://files.ballistica.net/cache/ba1/3c/1d/8c2d8fb2481e760da082cb976b40", - "assets/build/ba_data/audio/spazAttack04.ogg": "https://files.ballistica.net/cache/ba1/18/7c/d502771339dd6e0ac3425325ef7c", - "assets/build/ba_data/audio/spazDeath01.ogg": "https://files.ballistica.net/cache/ba1/41/96/bef46473014670f54ef9ea16df16", - "assets/build/ba_data/audio/spazEff.ogg": "https://files.ballistica.net/cache/ba1/70/72/d84808938005033bba5ca6955fef", - "assets/build/ba_data/audio/spazFall01.ogg": "https://files.ballistica.net/cache/ba1/aa/76/a7b3444ea49ff74f509108a22a66", - "assets/build/ba_data/audio/spazImpact01.ogg": "https://files.ballistica.net/cache/ba1/47/f1/58a6e673edfee76a535959ac574b", - "assets/build/ba_data/audio/spazImpact02.ogg": "https://files.ballistica.net/cache/ba1/1f/ca/ceb0cfe82f3269ecb595647d01f5", - "assets/build/ba_data/audio/spazImpact03.ogg": "https://files.ballistica.net/cache/ba1/7d/8a/6b5cb7d301808eb014a37e9b6d8f", - "assets/build/ba_data/audio/spazImpact04.ogg": "https://files.ballistica.net/cache/ba1/ad/32/6a3f980d5336cee41807efe6a9d6", - "assets/build/ba_data/audio/spazJump01.ogg": "https://files.ballistica.net/cache/ba1/77/b1/1478ab4c054f555d83f1a49d64b2", - "assets/build/ba_data/audio/spazJump02.ogg": "https://files.ballistica.net/cache/ba1/c3/70/f0acbf2c0916b2ac67b821c91372", - "assets/build/ba_data/audio/spazJump03.ogg": "https://files.ballistica.net/cache/ba1/cc/2c/66a30feb9b4781c8fbde12010ef5", - "assets/build/ba_data/audio/spazJump04.ogg": "https://files.ballistica.net/cache/ba1/91/83/cdcc15feadbd08dd4d899534ecf7", - "assets/build/ba_data/audio/spazOw.ogg": "https://files.ballistica.net/cache/ba1/1e/81/dc0302757a2d0ce8efbed226ce9e", - "assets/build/ba_data/audio/spazPickup01.ogg": "https://files.ballistica.net/cache/ba1/93/bf/c9b53b241bbec978c772c7e14caa", - "assets/build/ba_data/audio/spazScream01.ogg": "https://files.ballistica.net/cache/ba1/79/20/eb98e70397b0cd98149952753ef1", - "assets/build/ba_data/audio/splatter.ogg": "https://files.ballistica.net/cache/ba1/be/05/35059cebfb5658f7d434d32c94d1", - "assets/build/ba_data/audio/sportsMusic.ogg": "https://files.ballistica.net/cache/ba1/ff/cc/ba30edcc58ecc75ad4cf9f13e987", - "assets/build/ba_data/audio/stickyImpact.ogg": "https://files.ballistica.net/cache/ba1/5a/1c/bcfb5574c3ba2b8aec3d04d5937b", - "assets/build/ba_data/audio/superPunch.ogg": "https://files.ballistica.net/cache/ba1/ab/05/7ccdbb7ec4dc2da7e519644b7ab1", - "assets/build/ba_data/audio/superhero1.ogg": "https://files.ballistica.net/cache/ba1/2d/4b/d07a04dba349620a7b4f22e2340c", - "assets/build/ba_data/audio/superhero2.ogg": "https://files.ballistica.net/cache/ba1/35/b3/cf4479013786a22f2d378a998a15", - "assets/build/ba_data/audio/superhero3.ogg": "https://files.ballistica.net/cache/ba1/09/24/9aaf558d4eefad33845179b001f1", - "assets/build/ba_data/audio/superhero4.ogg": "https://files.ballistica.net/cache/ba1/3f/8e/219bab660637500a61f5be205b99", - "assets/build/ba_data/audio/superheroDeath.ogg": "https://files.ballistica.net/cache/ba1/d8/65/a0c7dc72b3a900ee3e9fc3a6c703", - "assets/build/ba_data/audio/superheroFall.ogg": "https://files.ballistica.net/cache/ba1/ba/21/c92b72275739cf682fe687fff10a", - "assets/build/ba_data/audio/superheroHit1.ogg": "https://files.ballistica.net/cache/ba1/fc/23/e15bb8d3e6daf77b4e393123d6f7", - "assets/build/ba_data/audio/superheroHit2.ogg": "https://files.ballistica.net/cache/ba1/f4/05/e2719c56ac77f3db1be03a0feac8", - "assets/build/ba_data/audio/survivalMusic.ogg": "https://files.ballistica.net/cache/ba1/69/45/5993fb4a9b6caba465feae60e3aa", - "assets/build/ba_data/audio/swip.ogg": "https://files.ballistica.net/cache/ba1/b2/fe/1a0796807eeb42160aeed59622f7", - "assets/build/ba_data/audio/swip2.ogg": "https://files.ballistica.net/cache/ba1/08/3d/12d4291235d240dd7e44dbe7f283", - "assets/build/ba_data/audio/swish.ogg": "https://files.ballistica.net/cache/ba1/33/41/3648e08fa50e2cf876f6ffa7fb3e", - "assets/build/ba_data/audio/swish2.ogg": "https://files.ballistica.net/cache/ba1/cc/67/8294681a9e9a88d055f33022a41f", - "assets/build/ba_data/audio/swish3.ogg": "https://files.ballistica.net/cache/ba1/06/35/7e03c8300a493d9c6ec4185b62fb", - "assets/build/ba_data/audio/tap.ogg": "https://files.ballistica.net/cache/ba1/30/7f/9f2a2aabfc63dfdb3e93106f3835", - "assets/build/ba_data/audio/technoHit01.ogg": "https://files.ballistica.net/cache/ba1/61/8a/d322dc58f7f41f34f2937a3879cf", - "assets/build/ba_data/audio/tick.ogg": "https://files.ballistica.net/cache/ba1/f5/cf/722bc46de90a550cab9ca11dd076", - "assets/build/ba_data/audio/ticking.ogg": "https://files.ballistica.net/cache/ba1/fb/55/55e5508fc180a67b2730d731856b", - "assets/build/ba_data/audio/tickingCrazy.ogg": "https://files.ballistica.net/cache/ba1/75/1c/43cea75988866fe0674883d70e08", - "assets/build/ba_data/audio/toTheDeathMusic.ogg": "https://files.ballistica.net/cache/ba1/ee/9a/9d2459da2a3fa0fbc977c639c5c6", - "assets/build/ba_data/audio/trashRummage.ogg": "https://files.ballistica.net/cache/ba1/ce/93/0e1b7d2155bcaccb585b3bcbf8b6", - "assets/build/ba_data/audio/victoryMusic.ogg": "https://files.ballistica.net/cache/ba1/6a/a7/0cf20971661de58dd3ee118856dc", - "assets/build/ba_data/audio/warnBeep.ogg": "https://files.ballistica.net/cache/ba1/51/ab/710c0882ab078f155ee574e124f4", - "assets/build/ba_data/audio/warnBeeps.ogg": "https://files.ballistica.net/cache/ba1/bc/b4/fed2b34f7ec47465b637b49e69fa", - "assets/build/ba_data/audio/warrior1.ogg": "https://files.ballistica.net/cache/ba1/fc/29/a88557dabe502eaa1f4e61e66050", - "assets/build/ba_data/audio/warrior2.ogg": "https://files.ballistica.net/cache/ba1/f1/37/cb391e6b920f278c5b007b381388", - "assets/build/ba_data/audio/warrior3.ogg": "https://files.ballistica.net/cache/ba1/8c/2e/d7c4c7a599a3a368f2d95f247127", - "assets/build/ba_data/audio/warrior4.ogg": "https://files.ballistica.net/cache/ba1/07/51/4796e3f9f9de9c5566740e951222", - "assets/build/ba_data/audio/warriorDeath.ogg": "https://files.ballistica.net/cache/ba1/d9/b6/c2e7f4a83928534e285f7ce1c922", - "assets/build/ba_data/audio/warriorFall.ogg": "https://files.ballistica.net/cache/ba1/2d/c5/ba39d507aa20f054feda013a9f21", - "assets/build/ba_data/audio/warriorHit1.ogg": "https://files.ballistica.net/cache/ba1/4f/83/1cbd4d2fb3e94e5e25979ed097ca", - "assets/build/ba_data/audio/warriorHit2.ogg": "https://files.ballistica.net/cache/ba1/be/ac/36bf2226134a7d319fd12fd6b28a", - "assets/build/ba_data/audio/whenJohnnyComesMarchingHomeMusic.ogg": "https://files.ballistica.net/cache/ba1/08/b1/21ee18cb2db336861c5a04d86f45", - "assets/build/ba_data/audio/witch1.ogg": "https://files.ballistica.net/cache/ba1/67/4e/05132bdb446fbdc1e1915089763b", - "assets/build/ba_data/audio/witch2.ogg": "https://files.ballistica.net/cache/ba1/57/a2/141d27861ab6a6177337d422e428", - "assets/build/ba_data/audio/witch3.ogg": "https://files.ballistica.net/cache/ba1/41/13/88fe3efdbc6b300ea74b8db0c816", - "assets/build/ba_data/audio/witch4.ogg": "https://files.ballistica.net/cache/ba1/1e/6a/67aaa75635a97116ca7bb5761051", - "assets/build/ba_data/audio/witchDeath.ogg": "https://files.ballistica.net/cache/ba1/22/33/2c7098263df931564f14815be5e8", - "assets/build/ba_data/audio/witchFall.ogg": "https://files.ballistica.net/cache/ba1/54/1c/d2de34a6985d56b469f10b7560ef", - "assets/build/ba_data/audio/witchHit1.ogg": "https://files.ballistica.net/cache/ba1/ae/24/430389e6bafba567a096b55228e6", - "assets/build/ba_data/audio/witchHit2.ogg": "https://files.ballistica.net/cache/ba1/c1/56/89955e8ab2fabaa5ff989bdf299e", - "assets/build/ba_data/audio/wizard1.ogg": "https://files.ballistica.net/cache/ba1/ff/6a/f81dc1a4656c6dffd1a36216d1b8", - "assets/build/ba_data/audio/wizard2.ogg": "https://files.ballistica.net/cache/ba1/52/43/f0e9af1d49b8d20bf561f8f9d3a9", - "assets/build/ba_data/audio/wizard3.ogg": "https://files.ballistica.net/cache/ba1/50/ba/716520f2f0137d8f86410a6089fb", - "assets/build/ba_data/audio/wizard4.ogg": "https://files.ballistica.net/cache/ba1/50/cc/2d239102cdc2ace02e79d8b0879c", - "assets/build/ba_data/audio/wizardDeath.ogg": "https://files.ballistica.net/cache/ba1/bb/4c/5e4dc54df830187c63a2bc7922b6", - "assets/build/ba_data/audio/wizardFall.ogg": "https://files.ballistica.net/cache/ba1/25/a5/21d69464843cb9f067541383468d", - "assets/build/ba_data/audio/wizardHit1.ogg": "https://files.ballistica.net/cache/ba1/10/47/be3966c620333be4ca9c30e1a2c4", - "assets/build/ba_data/audio/wizardHit2.ogg": "https://files.ballistica.net/cache/ba1/40/9a/0aed7b1bff3e69c257099ac26c1d", - "assets/build/ba_data/audio/woodDebrisFall.ogg": "https://files.ballistica.net/cache/ba1/94/03/59046559bc61e1e4b01078e778dd", - "assets/build/ba_data/audio/wrestler1.ogg": "https://files.ballistica.net/cache/ba1/52/71/c84d34f895c64d2fb38d880c737e", - "assets/build/ba_data/audio/wrestler2.ogg": "https://files.ballistica.net/cache/ba1/81/1f/c93c1b89d8d434fe84985d7d1af0", - "assets/build/ba_data/audio/wrestler3.ogg": "https://files.ballistica.net/cache/ba1/d8/2e/af82c6b47d77e9fa91b431ae03d1", - "assets/build/ba_data/audio/wrestler4.ogg": "https://files.ballistica.net/cache/ba1/7b/28/3aab8e16de76fc7453ed1e0886a8", - "assets/build/ba_data/audio/wrestlerDeath.ogg": "https://files.ballistica.net/cache/ba1/ae/b3/090c2525c3f697450a142458f6dc", - "assets/build/ba_data/audio/wrestlerFall.ogg": "https://files.ballistica.net/cache/ba1/1f/15/15fb12bb80fa7363b2ba88a4dce3", - "assets/build/ba_data/audio/wrestlerHit1.ogg": "https://files.ballistica.net/cache/ba1/90/b6/403ede16b60bdf089ce92079ef65", - "assets/build/ba_data/audio/wrestlerHit2.ogg": "https://files.ballistica.net/cache/ba1/d8/f7/4b4104842d9a408a9bb757cebabf", - "assets/build/ba_data/audio/zoeAttack01.ogg": "https://files.ballistica.net/cache/ba1/22/66/786bb399ac6bfc406f8fb0f6b3b4", - "assets/build/ba_data/audio/zoeAttack02.ogg": "https://files.ballistica.net/cache/ba1/37/42/60be96146c62bb29b10efae99fbe", - "assets/build/ba_data/audio/zoeAttack03.ogg": "https://files.ballistica.net/cache/ba1/0d/1f/9a71c8ce2aa31ffc2bdaa3341aed", - "assets/build/ba_data/audio/zoeAttack04.ogg": "https://files.ballistica.net/cache/ba1/52/e2/97d0299a564d2b7454fb742d5aab", - "assets/build/ba_data/audio/zoeDeath01.ogg": "https://files.ballistica.net/cache/ba1/fa/aa/f622e55e348e145955e27a4c3e57", - "assets/build/ba_data/audio/zoeEff.ogg": "https://files.ballistica.net/cache/ba1/42/c3/0bf8991471b650a1d2cd320c5ce3", - "assets/build/ba_data/audio/zoeFall01.ogg": "https://files.ballistica.net/cache/ba1/9e/12/ce19febfcbf2b43eb733dbd1626b", - "assets/build/ba_data/audio/zoeImpact01.ogg": "https://files.ballistica.net/cache/ba1/01/8c/90ab117e9c73fc33d556d18b079a", - "assets/build/ba_data/audio/zoeImpact02.ogg": "https://files.ballistica.net/cache/ba1/01/69/21c09400f109285b9d8cb8814d69", - "assets/build/ba_data/audio/zoeImpact03.ogg": "https://files.ballistica.net/cache/ba1/61/72/5bf11492feba95994f75040e36ce", - "assets/build/ba_data/audio/zoeImpact04.ogg": "https://files.ballistica.net/cache/ba1/c1/b1/a1159e8caeb1f5b80fcff55e8244", - "assets/build/ba_data/audio/zoeJump01.ogg": "https://files.ballistica.net/cache/ba1/b1/f0/5220fdb723a42e9177a99f9f5bdc", - "assets/build/ba_data/audio/zoeJump02.ogg": "https://files.ballistica.net/cache/ba1/71/01/4b2e5d9e2fcdfba254290c71a002", - "assets/build/ba_data/audio/zoeJump03.ogg": "https://files.ballistica.net/cache/ba1/66/3b/8ffff9c3d0280b7d1a15c92dfa94", - "assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/60/ad/38269b7f1c7dc20cb9a506cd0681", - "assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/72/85/d6fc4d16b7081d91fba2850b5b10", - "assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/e9/ae/1d674d0c086eaa0bd1c3b1db0505", - "assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/d2/88/6b51163789d96e3a6d0341680d6b", - "assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/92/43/36b34307575f6d6219bdf4898e18", - "assets/build/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/61/03/89070ca765e06da3a419a579f503", - "assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/f5/32/293ca14171b8e7e7d38f377e87fb", - "assets/build/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/f7/b0/191439142c6d6da4a056edc98b38", - "assets/build/ba_data/data/languages/croatian.json": "https://files.ballistica.net/cache/ba1/c9/73/01a1343af814131b1ee96af0b687", - "assets/build/ba_data/data/languages/czech.json": "https://files.ballistica.net/cache/ba1/4e/8c/72ddb584856a15dfb11df95f9283", - "assets/build/ba_data/data/languages/danish.json": "https://files.ballistica.net/cache/ba1/6a/fa/fcf4a804beaff927b0f12c179eaa", - "assets/build/ba_data/data/languages/dutch.json": "https://files.ballistica.net/cache/ba1/68/93/da8e9874f41a786edf52ba4ccaad", - "assets/build/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/15/9d/5199b6a053922c2f9cc0f705e113", - "assets/build/ba_data/data/languages/esperanto.json": "https://files.ballistica.net/cache/ba1/ac/f5/c0922a99e40dfc9f5e026d43b533", - "assets/build/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/dc/50/c109f475599608a7e8fdacea667c", - "assets/build/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/4f/4d/b259b145a69db1d34052281bb3bb", - "assets/build/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/6a/76/95a0d9f4615b23f12aa3a1a7e047", - "assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/09/c3/029c8d6357aa2dc5a3ac3560265a", - "assets/build/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/8a/82/392f2a4e0e6e5a5e367f19e49538", - "assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/2e/d1/b506ddf3d27af7be1ae6159531a3", - "assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/b2/81/53c8cd7617d649403e539c3a6171", - "assets/build/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/ff/81/d8ded0d4747336d1db29dcd048ef", - "assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/dd/be/1ae65f073d563e69c0c5246d20d7", - "assets/build/ba_data/data/languages/korean.json": "https://files.ballistica.net/cache/ba1/32/97/4c61425d7c200a5c4c9ae3a94a0e", - "assets/build/ba_data/data/languages/malay.json": "https://files.ballistica.net/cache/ba1/b4/02/f686ce008312e74d5953a511c8b2", - "assets/build/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/c5/35/5f12439b51df61451a884ecc0c0a", - "assets/build/ba_data/data/languages/polish.json": "https://files.ballistica.net/cache/ba1/b5/9b/8a3f4b32c7578ec5b1199755f08a", - "assets/build/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/4c/bc/b131de7b46d24f79767219959e0f", - "assets/build/ba_data/data/languages/romanian.json": "https://files.ballistica.net/cache/ba1/d7/06/9d70642d0a4d1e3b1c2149d7a17c", - "assets/build/ba_data/data/languages/russian.json": "https://files.ballistica.net/cache/ba1/01/97/624b3ee62979002d2385ed58abda", - "assets/build/ba_data/data/languages/serbian.json": "https://files.ballistica.net/cache/ba1/4e/91/6f2a9a3ce733908e91377a6ddb9a", - "assets/build/ba_data/data/languages/slovak.json": "https://files.ballistica.net/cache/ba1/20/a9/163d189884edf802636bf291e432", - "assets/build/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/a6/84/20a8d97b55785f6f995dadd88f4c", - "assets/build/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/3b/9f/d40c1423d260784970fd7364ca30", - "assets/build/ba_data/data/languages/tamil.json": "https://files.ballistica.net/cache/ba1/3d/83/e1bb0a664d1c14c41b1a083acf0d", - "assets/build/ba_data/data/languages/thai.json": "https://files.ballistica.net/cache/ba1/d6/16/523c643358880b03b233ed88e557", - "assets/build/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/4b/28/b62e4c928ee4ddb4120303cc5f62", - "assets/build/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/1c/3e/f264a8b00509c5b33a9abb02b25f", - "assets/build/ba_data/data/languages/venetian.json": "https://files.ballistica.net/cache/ba1/a6/ed/416638d46950c9ab4f6155b9c334", - "assets/build/ba_data/data/languages/vietnamese.json": "https://files.ballistica.net/cache/ba1/1f/ae/abe3f105b3c4b51f6b7942773305", - "assets/build/ba_data/data/maps/big_g.json": "https://files.ballistica.net/cache/ba1/47/0a/a617cc85d927b576c4e6fc1091ed", - "assets/build/ba_data/data/maps/bridgit.json": "https://files.ballistica.net/cache/ba1/03/4b/57ee9b42854b26f23f81bd8c58ef", - "assets/build/ba_data/data/maps/courtyard.json": "https://files.ballistica.net/cache/ba1/03/38/344dd05bfef7bbdf464035ec5aa2", - "assets/build/ba_data/data/maps/crag_castle.json": "https://files.ballistica.net/cache/ba1/53/89/0d4a1202902b73a1419a65a5056b", - "assets/build/ba_data/data/maps/doom_shroom.json": "https://files.ballistica.net/cache/ba1/46/76/1d748f6e867a4ca95a130417238d", - "assets/build/ba_data/data/maps/football_stadium.json": "https://files.ballistica.net/cache/ba1/36/86/b464844e6f35a28c9632667a4630", - "assets/build/ba_data/data/maps/happy_thoughts.json": "https://files.ballistica.net/cache/ba1/74/06/7faae161870162f2988575bbe59f", - "assets/build/ba_data/data/maps/hockey_stadium.json": "https://files.ballistica.net/cache/ba1/3f/91/a548331df9666e37d06d631d0c3e", - "assets/build/ba_data/data/maps/lake_frigid.json": "https://files.ballistica.net/cache/ba1/c4/b6/c75f3051e0f51304c2cc26d276cc", - "assets/build/ba_data/data/maps/monkey_face.json": "https://files.ballistica.net/cache/ba1/fc/b2/09c629850d9c1e3db403fe03aedf", - "assets/build/ba_data/data/maps/rampage.json": "https://files.ballistica.net/cache/ba1/e9/48/2747992ec7afbf2cb19ae602ccdb", - "assets/build/ba_data/data/maps/roundabout.json": "https://files.ballistica.net/cache/ba1/43/0d/342009523674c4425fc3ff6b72d6", - "assets/build/ba_data/data/maps/step_right_up.json": "https://files.ballistica.net/cache/ba1/de/07/f30afb17b149ed87dbfdce481686", - "assets/build/ba_data/data/maps/the_pad.json": "https://files.ballistica.net/cache/ba1/6a/18/ea01513ee2642fd75285f41f9080", - "assets/build/ba_data/data/maps/tip_top.json": "https://files.ballistica.net/cache/ba1/de/7f/b54d73c22546dd2784655c091c8e", - "assets/build/ba_data/data/maps/tower_d.json": "https://files.ballistica.net/cache/ba1/bd/6f/dc9b3208dc5b3966c33f83fa9b40", - "assets/build/ba_data/data/maps/zig_zag.json": "https://files.ballistica.net/cache/ba1/88/9b/e6e3aa1aca627e00443eae684e6d", - "assets/build/ba_data/fonts/fontSmall0.fdata": "https://files.ballistica.net/cache/ba1/42/b3/6c0466c7ce7c8476fbdbfbf221c7", - "assets/build/ba_data/fonts/fontSmall1.fdata": "https://files.ballistica.net/cache/ba1/f9/4a/dfc62e8ac13d84afbde66ee874ba", - "assets/build/ba_data/fonts/fontSmall2.fdata": "https://files.ballistica.net/cache/ba1/ec/ff/08c71b50c327ba18b6d296857972", - "assets/build/ba_data/fonts/fontSmall3.fdata": "https://files.ballistica.net/cache/ba1/ef/5c/37432c2f55739dc3cc2003027862", - "assets/build/ba_data/fonts/fontSmall4.fdata": "https://files.ballistica.net/cache/ba1/28/a1/067c2552014e5148cd604e8b12c6", - "assets/build/ba_data/fonts/fontSmall5.fdata": "https://files.ballistica.net/cache/ba1/4e/fd/9160326cc16916928e5677887346", - "assets/build/ba_data/fonts/fontSmall6.fdata": "https://files.ballistica.net/cache/ba1/42/42/a1096df1d5a21f20bbac239dcf42", - "assets/build/ba_data/fonts/fontSmall7.fdata": "https://files.ballistica.net/cache/ba1/3a/05/9f5f296e103d0c05a7bb0cb9dd67", - "assets/build/ba_data/models/achievementOutline.bob": "https://files.ballistica.net/cache/ba1/ac/ef/54aa9b5434dd39cbac3a018e3f06", - "assets/build/ba_data/models/actionButtonBottom.bob": "https://files.ballistica.net/cache/ba1/d4/98/5bd4ab46f7b2aea14b75df1b9b19", - "assets/build/ba_data/models/actionButtonLeft.bob": "https://files.ballistica.net/cache/ba1/e9/a9/c42904233f541a0215dbac68ad17", - "assets/build/ba_data/models/actionButtonRight.bob": "https://files.ballistica.net/cache/ba1/9c/0b/f6f76625035ab47b9e124530af9e", - "assets/build/ba_data/models/actionButtonTop.bob": "https://files.ballistica.net/cache/ba1/30/4e/913a172579d7692a313b2bb8501b", - "assets/build/ba_data/models/actionHeroForeArm.bob": "https://files.ballistica.net/cache/ba1/fe/74/fe8c20b35cd01b2a1489e05a1535", - "assets/build/ba_data/models/actionHeroHand.bob": "https://files.ballistica.net/cache/ba1/29/60/577e2e1d5e1dc17279d0c3afe637", - "assets/build/ba_data/models/actionHeroHead.bob": "https://files.ballistica.net/cache/ba1/84/8c/65dbd8897914b1c40f880b4ab549", - "assets/build/ba_data/models/actionHeroLowerLeg.bob": "https://files.ballistica.net/cache/ba1/03/8e/ef1ac85bc5a04121e4b5928e2ad2", - "assets/build/ba_data/models/actionHeroPelvis.bob": "https://files.ballistica.net/cache/ba1/5b/63/feb5a579153caf8cd4a0bd55f7a2", - "assets/build/ba_data/models/actionHeroToes.bob": "https://files.ballistica.net/cache/ba1/66/27/2f382432e5c11a1bc63ebda8d7d7", - "assets/build/ba_data/models/actionHeroTorso.bob": "https://files.ballistica.net/cache/ba1/0f/a0/83b2a0e63595f4c3b41203677b70", - "assets/build/ba_data/models/actionHeroUpperArm.bob": "https://files.ballistica.net/cache/ba1/7a/fd/c5dca43b907f7692d2838b00caed", - "assets/build/ba_data/models/actionHeroUpperLeg.bob": "https://files.ballistica.net/cache/ba1/cb/50/e8fff7c2e28bb75020607ed964eb", - "assets/build/ba_data/models/agentForeArm.bob": "https://files.ballistica.net/cache/ba1/a9/f1/c2a45d3a4f4d57b3720a5fcdf754", - "assets/build/ba_data/models/agentHand.bob": "https://files.ballistica.net/cache/ba1/e9/89/61cb35b8e35e0cd90b8795e24ce5", - "assets/build/ba_data/models/agentHead.bob": "https://files.ballistica.net/cache/ba1/ea/22/4694c4c01ed333a02338042f4610", - "assets/build/ba_data/models/agentLowerLeg.bob": "https://files.ballistica.net/cache/ba1/f6/fa/ccaa383cdf5949c8e4cfbfe02c57", - "assets/build/ba_data/models/agentPelvis.bob": "https://files.ballistica.net/cache/ba1/e7/a5/44d8148988efed59583c1fa8833c", - "assets/build/ba_data/models/agentToes.bob": "https://files.ballistica.net/cache/ba1/de/bd/bbb7ebb58ed3a6f263b298c78635", - "assets/build/ba_data/models/agentTorso.bob": "https://files.ballistica.net/cache/ba1/35/35/91e117e1d7c379c7a4c0fa107125", - "assets/build/ba_data/models/agentUpperArm.bob": "https://files.ballistica.net/cache/ba1/62/7f/cbf6fbc48f7c628ec9dcb60a1e74", - "assets/build/ba_data/models/agentUpperLeg.bob": "https://files.ballistica.net/cache/ba1/f4/fb/1d75e7d7ce1cb34701968b6fbd60", - "assets/build/ba_data/models/aliForeArm.bob": "https://files.ballistica.net/cache/ba1/d8/2b/91297d3ffcee56a2cd365e1a6b58", - "assets/build/ba_data/models/aliHand.bob": "https://files.ballistica.net/cache/ba1/75/bd/d3899fe0cf9b5f32e52d19b73e6a", - "assets/build/ba_data/models/aliHead.bob": "https://files.ballistica.net/cache/ba1/a5/5b/1c9d1d5f4a54f0a826723b3acd47", - "assets/build/ba_data/models/aliLowerLeg.bob": "https://files.ballistica.net/cache/ba1/47/98/0942fd603521cef80f1a09d3d274", - "assets/build/ba_data/models/aliPelvis.bob": "https://files.ballistica.net/cache/ba1/92/c6/a7cb156b5d8a3ae4f76486ad6759", - "assets/build/ba_data/models/aliToes.bob": "https://files.ballistica.net/cache/ba1/85/97/3611128c3a48e9191de7c4d705d5", - "assets/build/ba_data/models/aliTorso.bob": "https://files.ballistica.net/cache/ba1/e5/52/0c0a55557d0d674eeab7b144f1a8", - "assets/build/ba_data/models/aliUpperArm.bob": "https://files.ballistica.net/cache/ba1/a8/ea/aa5a052bba81996a6b79410583f6", - "assets/build/ba_data/models/aliUpperLeg.bob": "https://files.ballistica.net/cache/ba1/2d/6d/838e44bdf7e9b7f9ded9a5a8c57c", - "assets/build/ba_data/models/alienForeArm.bob": "https://files.ballistica.net/cache/ba1/8a/d0/e6864047ac3cb57bd16cfffbc354", - "assets/build/ba_data/models/alienHand.bob": "https://files.ballistica.net/cache/ba1/2c/60/48c63052bb2740cbebcc232de7e1", - "assets/build/ba_data/models/alienHead.bob": "https://files.ballistica.net/cache/ba1/d0/fc/65dd09c354e26754b7e9dfd81c4c", - "assets/build/ba_data/models/alienLowerLeg.bob": "https://files.ballistica.net/cache/ba1/6b/af/1125f3e4a368d419658b6fc53603", - "assets/build/ba_data/models/alienPelvis.bob": "https://files.ballistica.net/cache/ba1/d2/fc/51a7bd9f557bbf383d58e9f4509d", - "assets/build/ba_data/models/alienToes.bob": "https://files.ballistica.net/cache/ba1/e4/eb/5bd842e1951af6b530ba0ef50ab3", - "assets/build/ba_data/models/alienTorso.bob": "https://files.ballistica.net/cache/ba1/4b/94/002dafc392d294a07e7909a4eed7", - "assets/build/ba_data/models/alienUpperArm.bob": "https://files.ballistica.net/cache/ba1/1e/49/495c945f04563d05253a76ef9681", - "assets/build/ba_data/models/alienUpperLeg.bob": "https://files.ballistica.net/cache/ba1/93/3a/d05c6105c879077cc01f5b298807", - "assets/build/ba_data/models/alwaysLandBG.bob": "https://files.ballistica.net/cache/ba1/27/17/12b017d08aab5abf7d332431741d", - "assets/build/ba_data/models/alwaysLandLevel.bob": "https://files.ballistica.net/cache/ba1/1a/1e/d547bcd9ab32e428ac4f5bd644b2", - "assets/build/ba_data/models/alwaysLandLevelBottom.bob": "https://files.ballistica.net/cache/ba1/b3/25/891a3f36a523199dfa6f2e0613fd", - "assets/build/ba_data/models/alwaysLandLevelCollide.cob": "https://files.ballistica.net/cache/ba1/a7/bd/11be2244499eed41759318ce990c", - "assets/build/ba_data/models/alwaysLandVRFillMound.bob": "https://files.ballistica.net/cache/ba1/a7/1e/9b16be3deec55757de084d4c3812", - "assets/build/ba_data/models/angryComputerTransparent.bob": "https://files.ballistica.net/cache/ba1/02/fe/ac980c006aec43a0a6ad53e29add", - "assets/build/ba_data/models/arrowBack.bob": "https://files.ballistica.net/cache/ba1/1d/4a/a7fab16227111fa8a7dbea3eba2f", - "assets/build/ba_data/models/arrowFront.bob": "https://files.ballistica.net/cache/ba1/35/b5/a036e23c222d05725471ede6fdc6", - "assets/build/ba_data/models/assassinForeArm.bob": "https://files.ballistica.net/cache/ba1/75/27/68f3e1948f5d75129311f0456237", - "assets/build/ba_data/models/assassinHand.bob": "https://files.ballistica.net/cache/ba1/46/d3/fbc13f59c261b7efea481446f64e", - "assets/build/ba_data/models/assassinHead.bob": "https://files.ballistica.net/cache/ba1/6b/9d/3a6704195cb0fe4d79a0b215a3bc", - "assets/build/ba_data/models/assassinLowerLeg.bob": "https://files.ballistica.net/cache/ba1/3f/69/96b74a337b79c34c222116c51b8b", - "assets/build/ba_data/models/assassinPelvis.bob": "https://files.ballistica.net/cache/ba1/20/dc/0c395dae695984e27b61a8bf1084", - "assets/build/ba_data/models/assassinToes.bob": "https://files.ballistica.net/cache/ba1/83/04/773370e26fbdd8c0990aeb330808", - "assets/build/ba_data/models/assassinTorso.bob": "https://files.ballistica.net/cache/ba1/c4/05/feeb9368f6ba47288614efd34473", - "assets/build/ba_data/models/assassinUpperArm.bob": "https://files.ballistica.net/cache/ba1/63/72/9683f04f7a7b106f0361165ce963", - "assets/build/ba_data/models/assassinUpperLeg.bob": "https://files.ballistica.net/cache/ba1/1d/19/0a3aa57aa093fda8366f27ffc410", - "assets/build/ba_data/models/bearForeArm.bob": "https://files.ballistica.net/cache/ba1/ae/41/bdbf3e0a6caf6f93eec74ede0a00", - "assets/build/ba_data/models/bearHand.bob": "https://files.ballistica.net/cache/ba1/4a/5e/740a745a025bda5091069db58c24", - "assets/build/ba_data/models/bearHead.bob": "https://files.ballistica.net/cache/ba1/b2/c2/447fdf41c6ea09c60e4e86429971", - "assets/build/ba_data/models/bearLowerLeg.bob": "https://files.ballistica.net/cache/ba1/86/6a/a351bdf44353d67076be4061b792", - "assets/build/ba_data/models/bearPelvis.bob": "https://files.ballistica.net/cache/ba1/e3/d4/8753fd0236c7b09dfd60ec1e4a1c", - "assets/build/ba_data/models/bearToes.bob": "https://files.ballistica.net/cache/ba1/66/a1/36b2a3d2ddf662632f67244ff9e5", - "assets/build/ba_data/models/bearTorso.bob": "https://files.ballistica.net/cache/ba1/8d/0f/5a7e83aeb9a62f2c36cba9746072", - "assets/build/ba_data/models/bearUpperArm.bob": "https://files.ballistica.net/cache/ba1/4a/95/9bdece3e5b9eb332e923183dd022", - "assets/build/ba_data/models/bearUpperLeg.bob": "https://files.ballistica.net/cache/ba1/a4/b8/6be1f58b494893f181968888edba", - "assets/build/ba_data/models/bigG.bob": "https://files.ballistica.net/cache/ba1/af/0e/29f285ab1a1fa82ef8dbdf8429b8", - "assets/build/ba_data/models/bigGBottom.bob": "https://files.ballistica.net/cache/ba1/0d/ea/87870c1b4b3ed39018955efc0c49", - "assets/build/ba_data/models/bigGBumper.cob": "https://files.ballistica.net/cache/ba1/07/94/b428d062090f6fb542b6c9e18c8f", - "assets/build/ba_data/models/bigGCollide.cob": "https://files.ballistica.net/cache/ba1/e3/d5/3631af851fcd3f983ed6006e66d8", - "assets/build/ba_data/models/bomb.bob": "https://files.ballistica.net/cache/ba1/2f/9d/fe513bdca41e0c807b7b3d8ffbac", - "assets/build/ba_data/models/bombSticky.bob": "https://files.ballistica.net/cache/ba1/ed/5c/ca154d84938857373719526f58ec", - "assets/build/ba_data/models/bonesForeArm.bob": "https://files.ballistica.net/cache/ba1/90/e9/523d7d5b85e7e26337d914e5fda6", - "assets/build/ba_data/models/bonesHand.bob": "https://files.ballistica.net/cache/ba1/8a/03/2d93d23bc11f55b1899efb5f5c65", - "assets/build/ba_data/models/bonesHead.bob": "https://files.ballistica.net/cache/ba1/ab/eb/24f746a387249600b1f9b930f168", - "assets/build/ba_data/models/bonesLowerLeg.bob": "https://files.ballistica.net/cache/ba1/08/eb/7a0c1fd2ce600031d8343cf20cd2", - "assets/build/ba_data/models/bonesPelvis.bob": "https://files.ballistica.net/cache/ba1/11/c0/1a497586990488e69c868a9f4be9", - "assets/build/ba_data/models/bonesToes.bob": "https://files.ballistica.net/cache/ba1/48/3f/85e8cd2d3bc8ee5893223447ef4f", - "assets/build/ba_data/models/bonesTorso.bob": "https://files.ballistica.net/cache/ba1/86/e1/8c4bb500488f1c5d59a2e902fba8", - "assets/build/ba_data/models/bonesUpperArm.bob": "https://files.ballistica.net/cache/ba1/bb/ea/f09c9f485e510d3cc8be62e17681", - "assets/build/ba_data/models/bonesUpperLeg.bob": "https://files.ballistica.net/cache/ba1/30/ad/e9283168851fefdfcfe283443ebd", - "assets/build/ba_data/models/box.bob": "https://files.ballistica.net/cache/ba1/fe/71/d9cc843a9a7b11d3691beb63ae21", - "assets/build/ba_data/models/boxingGlove.bob": "https://files.ballistica.net/cache/ba1/ff/df/b5f186a797251d27bb6bb3a42804", - "assets/build/ba_data/models/bridgitLevelBottom.bob": "https://files.ballistica.net/cache/ba1/1e/f4/8e8c6b86081aecc89888677b1dda", - "assets/build/ba_data/models/bridgitLevelCollide.cob": "https://files.ballistica.net/cache/ba1/9c/19/b3f44e34a9153e645ae6e05bed9e", - "assets/build/ba_data/models/bridgitLevelRailingCollide.cob": "https://files.ballistica.net/cache/ba1/91/18/1fe3cd097d0b1e01ff76a531990f", - "assets/build/ba_data/models/bridgitLevelTop.bob": "https://files.ballistica.net/cache/ba1/8a/ff/e930989beb9e741c86fbc9d8b9c5", - "assets/build/ba_data/models/bunnyForeArm.bob": "https://files.ballistica.net/cache/ba1/7f/0c/31eb734e202a206b27df16f0bce3", - "assets/build/ba_data/models/bunnyHand.bob": "https://files.ballistica.net/cache/ba1/48/36/a7172c3d3660811d3c660719ab9a", - "assets/build/ba_data/models/bunnyHead.bob": "https://files.ballistica.net/cache/ba1/81/1e/999f5c55663b73c3a3506864f226", - "assets/build/ba_data/models/bunnyLowerLeg.bob": "https://files.ballistica.net/cache/ba1/fe/0e/b84726d48e86e7655c5d853b5822", - "assets/build/ba_data/models/bunnyPelvis.bob": "https://files.ballistica.net/cache/ba1/56/9b/5b3e450eb292e58bade935db0b21", - "assets/build/ba_data/models/bunnyToes.bob": "https://files.ballistica.net/cache/ba1/d3/c0/16045df2cbed07e24f20abf139de", - "assets/build/ba_data/models/bunnyTorso.bob": "https://files.ballistica.net/cache/ba1/cd/8a/3fc46cd107d4627f0d6c74d2fc87", - "assets/build/ba_data/models/bunnyUpperArm.bob": "https://files.ballistica.net/cache/ba1/6a/4b/25192fc23a57f2e8c13970044ca8", - "assets/build/ba_data/models/bunnyUpperLeg.bob": "https://files.ballistica.net/cache/ba1/c4/4a/55cc875677ecb3d688b3e60fb822", - "assets/build/ba_data/models/buttonBackOpaque.bob": "https://files.ballistica.net/cache/ba1/92/b4/c5eac2e2496fe314915121e33d39", - "assets/build/ba_data/models/buttonBackSmallOpaque.bob": "https://files.ballistica.net/cache/ba1/f3/46/19a96352948fec1944ce0b76b33a", - "assets/build/ba_data/models/buttonBackSmallTransparent.bob": "https://files.ballistica.net/cache/ba1/4d/76/a375906aac113e0acac493dba0e0", - "assets/build/ba_data/models/buttonBackTransparent.bob": "https://files.ballistica.net/cache/ba1/2e/a5/b96da3a8ea0d194b2ca3b0928cbc", - "assets/build/ba_data/models/buttonLargeOpaque.bob": "https://files.ballistica.net/cache/ba1/d8/75/98a29c9890f720a25f07e7788f4d", - "assets/build/ba_data/models/buttonLargeTransparent.bob": "https://files.ballistica.net/cache/ba1/7c/46/510aa2711832632a63a5a486ac32", - "assets/build/ba_data/models/buttonLargerOpaque.bob": "https://files.ballistica.net/cache/ba1/70/d4/4835f37b2163d23ff939be019d1d", - "assets/build/ba_data/models/buttonLargerTransparent.bob": "https://files.ballistica.net/cache/ba1/e9/ab/feae7436f2674109f18baa9c4cbb", - "assets/build/ba_data/models/buttonMediumOpaque.bob": "https://files.ballistica.net/cache/ba1/71/bb/fb57736376615cabaac0d572ea65", - "assets/build/ba_data/models/buttonMediumTransparent.bob": "https://files.ballistica.net/cache/ba1/10/54/d354ad07bc2f2b11d45b223fb2a7", - "assets/build/ba_data/models/buttonNull.bob": "https://files.ballistica.net/cache/ba1/1a/94/2fd82c3edb6e5d8e07e4b9cc6528", - "assets/build/ba_data/models/buttonSmallOpaque.bob": "https://files.ballistica.net/cache/ba1/d2/64/d9698c50a765624c4995e85b44cc", - "assets/build/ba_data/models/buttonSmallTransparent.bob": "https://files.ballistica.net/cache/ba1/00/cb/e454acb4912571c4815e2e5e1096", - "assets/build/ba_data/models/buttonSquareOpaque.bob": "https://files.ballistica.net/cache/ba1/56/c6/f29189a7c8471a8917edb5374e8f", - "assets/build/ba_data/models/buttonSquareTransparent.bob": "https://files.ballistica.net/cache/ba1/9a/4b/327c1da73ecac7d4f3cf3b6e55ec", - "assets/build/ba_data/models/buttonTabOpaque.bob": "https://files.ballistica.net/cache/ba1/81/26/05a4fb48165fed5ee7937175ec76", - "assets/build/ba_data/models/buttonTabTransparent.bob": "https://files.ballistica.net/cache/ba1/1c/68/2b0852c0ce3cd1406115883859ee", - "assets/build/ba_data/models/checkTransparent.bob": "https://files.ballistica.net/cache/ba1/e5/89/051a96f74be2fd32ae186c55fdf4", - "assets/build/ba_data/models/courtyardLevel.bob": "https://files.ballistica.net/cache/ba1/d2/ff/8f2f82f5513506d41c20868123a6", - "assets/build/ba_data/models/courtyardLevelBottom.bob": "https://files.ballistica.net/cache/ba1/82/d8/b88df1cd7539d336009c67185786", - "assets/build/ba_data/models/courtyardLevelCollide.cob": "https://files.ballistica.net/cache/ba1/7f/5b/796069ddf5fe0e9d0461b84558f2", - "assets/build/ba_data/models/courtyardPlayerWall.cob": "https://files.ballistica.net/cache/ba1/76/98/6a4deab0d2363405a1268a53ae6b", - "assets/build/ba_data/models/cowboyForeArm.bob": "https://files.ballistica.net/cache/ba1/e8/63/b7c8ac77d39b248b2979aa26f099", - "assets/build/ba_data/models/cowboyHand.bob": "https://files.ballistica.net/cache/ba1/ff/6d/a7678f469bf6e142ed08c10d0509", - "assets/build/ba_data/models/cowboyHead.bob": "https://files.ballistica.net/cache/ba1/06/54/f8f676a9300bc4efa8c44c81b77f", - "assets/build/ba_data/models/cowboyLowerLeg.bob": "https://files.ballistica.net/cache/ba1/5b/af/1d3a7786fa4e017d10e8399e37e3", - "assets/build/ba_data/models/cowboyPelvis.bob": "https://files.ballistica.net/cache/ba1/37/6f/0a97c4d6455336a728bb6cb50b64", - "assets/build/ba_data/models/cowboyToes.bob": "https://files.ballistica.net/cache/ba1/05/64/21f6af2230628ff5d5067753918a", - "assets/build/ba_data/models/cowboyTorso.bob": "https://files.ballistica.net/cache/ba1/be/29/e459674612f7fcd0f79c2e44b54d", - "assets/build/ba_data/models/cowboyUpperArm.bob": "https://files.ballistica.net/cache/ba1/6b/b7/8ba9ae2c1bb039ff10ad33fe5121", - "assets/build/ba_data/models/cowboyUpperLeg.bob": "https://files.ballistica.net/cache/ba1/57/80/7507422ac4810e9811ef67103644", - "assets/build/ba_data/models/cragCastleLevel.bob": "https://files.ballistica.net/cache/ba1/35/87/7d6a5810173b24469f34500a406a", - "assets/build/ba_data/models/cragCastleLevelBottom.bob": "https://files.ballistica.net/cache/ba1/dc/64/dd0060aeb53ccd59cbc6f3297d69", - "assets/build/ba_data/models/cragCastleLevelBumper.cob": "https://files.ballistica.net/cache/ba1/72/a6/e561fc7884e4dd385f7427d6db67", - "assets/build/ba_data/models/cragCastleLevelCollide.cob": "https://files.ballistica.net/cache/ba1/62/ac/2ecb3c12cdd5418b55423828a353", - "assets/build/ba_data/models/cragCastleVRFillMound.bob": "https://files.ballistica.net/cache/ba1/f3/cc/0aa2afd8dfc82f596ff613034362", - "assets/build/ba_data/models/crossOut.bob": "https://files.ballistica.net/cache/ba1/db/bb/bbfb6904c7fea43b409139a90c3c", - "assets/build/ba_data/models/currencyMeter.bob": "https://files.ballistica.net/cache/ba1/6b/bf/0216f0d851491af2eb988db02d43", - "assets/build/ba_data/models/currencyPlusButton.bob": "https://files.ballistica.net/cache/ba1/6f/87/fbb9e93d3b14a435947a05595699", - "assets/build/ba_data/models/cyborgForeArm.bob": "https://files.ballistica.net/cache/ba1/65/52/c130436bb43a91eae9054d41c695", - "assets/build/ba_data/models/cyborgHand.bob": "https://files.ballistica.net/cache/ba1/81/14/35da8e5eb95c4c4313ded5bb8f21", - "assets/build/ba_data/models/cyborgHead.bob": "https://files.ballistica.net/cache/ba1/80/ca/0ba89c1b7ade4f0b31d26ee8a8b3", - "assets/build/ba_data/models/cyborgLowerLeg.bob": "https://files.ballistica.net/cache/ba1/b5/86/26707de14c5c96106c5c3fb36f46", - "assets/build/ba_data/models/cyborgPelvis.bob": "https://files.ballistica.net/cache/ba1/c3/cb/711572bd44f4a8534496a5097ec1", - "assets/build/ba_data/models/cyborgToes.bob": "https://files.ballistica.net/cache/ba1/9f/15/ce8023286bbddcb63a712375ec41", - "assets/build/ba_data/models/cyborgTorso.bob": "https://files.ballistica.net/cache/ba1/65/dc/45703248862d09b15e385f0d4d20", - "assets/build/ba_data/models/cyborgUpperArm.bob": "https://files.ballistica.net/cache/ba1/df/f4/6da42b145a3e8cf1319d095c33e9", - "assets/build/ba_data/models/cyborgUpperLeg.bob": "https://files.ballistica.net/cache/ba1/81/1e/9436a22e1a4bc947978b00508cd5", - "assets/build/ba_data/models/cylinder.bob": "https://files.ballistica.net/cache/ba1/0e/0a/c5d4ce4e8692c44fd26973fb1564", - "assets/build/ba_data/models/doomShroomBG.bob": "https://files.ballistica.net/cache/ba1/11/14/3667770fd40f2774f3ec91ffe590", - "assets/build/ba_data/models/doomShroomLevel.bob": "https://files.ballistica.net/cache/ba1/ac/d0/0f600b128f1021947b48da6db90f", - "assets/build/ba_data/models/doomShroomLevelCollide.cob": "https://files.ballistica.net/cache/ba1/87/49/a950834fa46c6a6ce5642beb3307", - "assets/build/ba_data/models/doomShroomStem.bob": "https://files.ballistica.net/cache/ba1/bd/17/ee161e6ba819758a90b24ce21be6", - "assets/build/ba_data/models/doomShroomStemCollide.cob": "https://files.ballistica.net/cache/ba1/00/b6/2acf0290d13d1a1cc54c74ac34fe", - "assets/build/ba_data/models/doomShroomVRFill.bob": "https://files.ballistica.net/cache/ba1/95/13/187a686ace48b969b05be1b314bf", - "assets/build/ba_data/models/egg.bob": "https://files.ballistica.net/cache/ba1/59/18/6c8002191b391fee1fd29b375aa1", - "assets/build/ba_data/models/eyeBall.bob": "https://files.ballistica.net/cache/ba1/1f/f9/7e0f0e96b074914ddd2f212b1be3", - "assets/build/ba_data/models/eyeBallIris.bob": "https://files.ballistica.net/cache/ba1/2d/e9/a8e717cfe545064f65813fd63128", - "assets/build/ba_data/models/eyeLid.bob": "https://files.ballistica.net/cache/ba1/1b/ca/9b8f8956e0ad9490a7d25e3e839d", - "assets/build/ba_data/models/flagPole.bob": "https://files.ballistica.net/cache/ba1/bf/0c/257215f153949ec3b2ef60387725", - "assets/build/ba_data/models/flagStand.bob": "https://files.ballistica.net/cache/ba1/92/df/1a39c25b76219274d309c2ff1d7e", - "assets/build/ba_data/models/flash.bob": "https://files.ballistica.net/cache/ba1/01/29/a0c4f48110242fb43aeaf85f2418", - "assets/build/ba_data/models/footballStadium.bob": "https://files.ballistica.net/cache/ba1/a8/2b/fe85f838148527f5b5cabad4897a", - "assets/build/ba_data/models/footballStadiumCollide.cob": "https://files.ballistica.net/cache/ba1/3d/72/a9a3fa2bdae80a799e00d6ad2c0e", - "assets/build/ba_data/models/footballStadiumVRFill.bob": "https://files.ballistica.net/cache/ba1/77/8f/c8ef24b5a50c951c6dbccc6d4c1d", - "assets/build/ba_data/models/frameInset.bob": "https://files.ballistica.net/cache/ba1/2e/6f/a6d43b734aacc15da631ec6fda85", - "assets/build/ba_data/models/frostyForeArm.bob": "https://files.ballistica.net/cache/ba1/05/9d/8e3257d982a9080155383f612585", - "assets/build/ba_data/models/frostyHand.bob": "https://files.ballistica.net/cache/ba1/ba/ea/ad61ff7228766ea3074746c44095", - "assets/build/ba_data/models/frostyHead.bob": "https://files.ballistica.net/cache/ba1/dc/00/665d719a8e75cfedc1177228d627", - "assets/build/ba_data/models/frostyLowerLeg.bob": "https://files.ballistica.net/cache/ba1/6e/7d/512aaccb697cb33ed8c1e2c72332", - "assets/build/ba_data/models/frostyPelvis.bob": "https://files.ballistica.net/cache/ba1/59/a6/0b2b83d91e25a1b8238a76f88257", - "assets/build/ba_data/models/frostyToes.bob": "https://files.ballistica.net/cache/ba1/15/a7/5e684d8882358d7fd6358a255877", - "assets/build/ba_data/models/frostyTorso.bob": "https://files.ballistica.net/cache/ba1/25/57/96baca0e72cde9055e9e5b5f1e4a", - "assets/build/ba_data/models/frostyUpperArm.bob": "https://files.ballistica.net/cache/ba1/bb/9a/9977f9b066ac6175dadfdac487ee", - "assets/build/ba_data/models/frostyUpperLeg.bob": "https://files.ballistica.net/cache/ba1/94/e8/88587ffd97d0b60e38a098c9139d", - "assets/build/ba_data/models/gladiatorForeArm.bob": "https://files.ballistica.net/cache/ba1/c7/bb/b3e26ea7e3cbb14b058c7c77d34f", - "assets/build/ba_data/models/gladiatorHand.bob": "https://files.ballistica.net/cache/ba1/4a/26/13d8e097138e97f4b1c4ff3fd9fe", - "assets/build/ba_data/models/gladiatorHead.bob": "https://files.ballistica.net/cache/ba1/b5/6b/5f342b038d70e4b548256288f1f8", - "assets/build/ba_data/models/gladiatorLowerLeg.bob": "https://files.ballistica.net/cache/ba1/23/43/f3fa2b8d1ad314c204e1611be20d", - "assets/build/ba_data/models/gladiatorPelvis.bob": "https://files.ballistica.net/cache/ba1/7f/9d/623fa866e6f333513ea2e6239c4f", - "assets/build/ba_data/models/gladiatorToes.bob": "https://files.ballistica.net/cache/ba1/f5/f4/d4fa94222b3a9b1957026376373d", - "assets/build/ba_data/models/gladiatorTorso.bob": "https://files.ballistica.net/cache/ba1/0f/9e/0c30965ff4c265b1d2e69d468396", - "assets/build/ba_data/models/gladiatorUpperArm.bob": "https://files.ballistica.net/cache/ba1/c2/c8/9dffb3be2640f32c9e449197b253", - "assets/build/ba_data/models/gladiatorUpperLeg.bob": "https://files.ballistica.net/cache/ba1/bd/95/93628f804ce1133e2e238f8da683", - "assets/build/ba_data/models/hairTuft1.bob": "https://files.ballistica.net/cache/ba1/e8/89/e9f7ccdd9059c70fb7e253eafb86", - "assets/build/ba_data/models/hairTuft1b.bob": "https://files.ballistica.net/cache/ba1/b2/11/e487745b135373f767d7f9dfe7b9", - "assets/build/ba_data/models/hairTuft2.bob": "https://files.ballistica.net/cache/ba1/2d/7f/ac297d7bf0ecfc937ba1ea4da0b6", - "assets/build/ba_data/models/hairTuft3.bob": "https://files.ballistica.net/cache/ba1/38/d2/54987d1b166aba183e995013e63b", - "assets/build/ba_data/models/hairTuft4.bob": "https://files.ballistica.net/cache/ba1/24/b7/6935536499343ea8c169452ad10c", - "assets/build/ba_data/models/heartOpaque.bob": "https://files.ballistica.net/cache/ba1/37/11/35880dd5663712538211ded9c1ba", - "assets/build/ba_data/models/heartTransparent.bob": "https://files.ballistica.net/cache/ba1/60/86/3f943b3722fe2e722917240882be", - "assets/build/ba_data/models/hockeyStadiumCollide.cob": "https://files.ballistica.net/cache/ba1/c6/a7/386ed085840a6c9365bbb1b0a5e8", - "assets/build/ba_data/models/hockeyStadiumInner.bob": "https://files.ballistica.net/cache/ba1/1c/c7/8a4f141205124ee201ec48bc5f75", - "assets/build/ba_data/models/hockeyStadiumOuter.bob": "https://files.ballistica.net/cache/ba1/94/70/0d2a05f378ad134019995114f6b0", - "assets/build/ba_data/models/hockeyStadiumStands.bob": "https://files.ballistica.net/cache/ba1/b5/e1/4ea82fe914f2bffeeea4a6baaa5e", - "assets/build/ba_data/models/image16x1.bob": "https://files.ballistica.net/cache/ba1/37/ac/d832969df62f297386cca9875051", - "assets/build/ba_data/models/image1x1.bob": "https://files.ballistica.net/cache/ba1/ad/cd/fcfbbd82d2c34ef6ce35641cc007", - "assets/build/ba_data/models/image1x1FullScreen.bob": "https://files.ballistica.net/cache/ba1/ce/5e/1d12ea61fe184cb6933903c2e978", - "assets/build/ba_data/models/image1x1VRFullScreen.bob": "https://files.ballistica.net/cache/ba1/13/94/6ab9d97850406199a80e2f6be421", - "assets/build/ba_data/models/image2x1.bob": "https://files.ballistica.net/cache/ba1/98/a6/fdb586052e51da3c193fefbb7aff", - "assets/build/ba_data/models/image2x1Vertical.bob": "https://files.ballistica.net/cache/ba1/d7/7b/80d00b556d2d8f2f7a847ab2f995", - "assets/build/ba_data/models/image4x1.bob": "https://files.ballistica.net/cache/ba1/75/8c/91bae8fe9b0e73d5ce5ef4dcba0d", - "assets/build/ba_data/models/impactBomb.bob": "https://files.ballistica.net/cache/ba1/7f/ec/e8b7036bdfd8b6c2e7eee26b4731", - "assets/build/ba_data/models/jackForeArm.bob": "https://files.ballistica.net/cache/ba1/43/32/c16c4e353b484e39cab1ee9f1bd3", - "assets/build/ba_data/models/jackHand.bob": "https://files.ballistica.net/cache/ba1/b1/5c/200f77d5ed9cfd0b65789fadd9f2", - "assets/build/ba_data/models/jackHead.bob": "https://files.ballistica.net/cache/ba1/37/aa/48651c39464c44ea69d283ad6528", - "assets/build/ba_data/models/jackLowerLeg.bob": "https://files.ballistica.net/cache/ba1/e8/14/a42f6571d14e295804bfcfe9f820", - "assets/build/ba_data/models/jackToes.bob": "https://files.ballistica.net/cache/ba1/9d/1b/a598c1fa4528e1cb1025315ec364", - "assets/build/ba_data/models/jackTorso.bob": "https://files.ballistica.net/cache/ba1/7c/40/c99e9bf7ae066fa76cbbb7ded89c", - "assets/build/ba_data/models/jackUpperArm.bob": "https://files.ballistica.net/cache/ba1/0b/b9/5d7f5ed9a6c189a7d00fae8ca20e", - "assets/build/ba_data/models/jackUpperLeg.bob": "https://files.ballistica.net/cache/ba1/8d/02/164344ad761c5b9ea45932cdbddf", - "assets/build/ba_data/models/jumpsuitForeArm.bob": "https://files.ballistica.net/cache/ba1/bb/44/be0b24e51133bb1f181570b96757", - "assets/build/ba_data/models/jumpsuitHand.bob": "https://files.ballistica.net/cache/ba1/82/d7/2cac44a664ac7943a194cfd10d99", - "assets/build/ba_data/models/jumpsuitHead.bob": "https://files.ballistica.net/cache/ba1/4f/1e/d1f88135594dda40966937a521d8", - "assets/build/ba_data/models/jumpsuitLowerLeg.bob": "https://files.ballistica.net/cache/ba1/c7/18/2f1f3f42f237c4e615f52f0d7036", - "assets/build/ba_data/models/jumpsuitPelvis.bob": "https://files.ballistica.net/cache/ba1/3d/f9/df57134f7a02d139aa7c1e354c1a", - "assets/build/ba_data/models/jumpsuitToes.bob": "https://files.ballistica.net/cache/ba1/29/23/41e869144666788b3a61abbfee01", - "assets/build/ba_data/models/jumpsuitTorso.bob": "https://files.ballistica.net/cache/ba1/90/2f/038769fabe3d9dda43f469017f51", - "assets/build/ba_data/models/jumpsuitUpperArm.bob": "https://files.ballistica.net/cache/ba1/e6/f1/5d24173e7f8b12222f57ff5e3142", - "assets/build/ba_data/models/jumpsuitUpperLeg.bob": "https://files.ballistica.net/cache/ba1/b1/90/57ffc4ea8e1613001929c47616c4", - "assets/build/ba_data/models/kronkForeArm.bob": "https://files.ballistica.net/cache/ba1/0a/c2/8c9a7af9ba920ed2cafbe6d00280", - "assets/build/ba_data/models/kronkHand.bob": "https://files.ballistica.net/cache/ba1/50/19/e768d8dd22eaf42a3978a53902a8", - "assets/build/ba_data/models/kronkHead.bob": "https://files.ballistica.net/cache/ba1/49/de/01e960a18f2e28c4f29d234771b9", - "assets/build/ba_data/models/kronkLowerLeg.bob": "https://files.ballistica.net/cache/ba1/87/32/6925c01563acf7c6c40c00fecd61", - "assets/build/ba_data/models/kronkPelvis.bob": "https://files.ballistica.net/cache/ba1/3c/00/3051f1785c0cb35d9b95919f1980", - "assets/build/ba_data/models/kronkToes.bob": "https://files.ballistica.net/cache/ba1/83/54/197dcfa3d7eed36efcdd4b8c4833", - "assets/build/ba_data/models/kronkTorso.bob": "https://files.ballistica.net/cache/ba1/a8/c1/89f39fec0ab9c46831b1991e2119", - "assets/build/ba_data/models/kronkUpperArm.bob": "https://files.ballistica.net/cache/ba1/97/1f/f3b3d28f08df7497b97de49dc041", - "assets/build/ba_data/models/kronkUpperLeg.bob": "https://files.ballistica.net/cache/ba1/7a/9e/ef637444aac2c20065746b29b683", - "assets/build/ba_data/models/lakeFrigid.bob": "https://files.ballistica.net/cache/ba1/7f/9f/422435b2ff3df7489de9a15efeb1", - "assets/build/ba_data/models/lakeFrigidCollide.cob": "https://files.ballistica.net/cache/ba1/24/74/b916ad4ac2fbcc8ff2f7f0fe44bd", - "assets/build/ba_data/models/lakeFrigidReflections.bob": "https://files.ballistica.net/cache/ba1/2b/ad/428e550977687e38485f49bf7d0a", - "assets/build/ba_data/models/lakeFrigidTop.bob": "https://files.ballistica.net/cache/ba1/d3/f8/99fde49f7b00ece9332a7ae9e06d", - "assets/build/ba_data/models/lakeFrigidVRFill.bob": "https://files.ballistica.net/cache/ba1/3d/a9/8cf330087c54082a4d882efe5b21", - "assets/build/ba_data/models/landMine.bob": "https://files.ballistica.net/cache/ba1/22/6d/3135f746f59f7e936a846081a86c", - "assets/build/ba_data/models/level_select_button_opaque.bob": "https://files.ballistica.net/cache/ba1/08/8a/2add1e682a7caafec36bfc48ec9e", - "assets/build/ba_data/models/level_select_button_transparent.bob": "https://files.ballistica.net/cache/ba1/31/9e/b115c06f4294b7c36cd3faf4aefb", - "assets/build/ba_data/models/locator.bob": "https://files.ballistica.net/cache/ba1/81/df/6b71d389c629d7e6c40cdd08340d", - "assets/build/ba_data/models/locatorBox.bob": "https://files.ballistica.net/cache/ba1/20/b0/0d63d9dcd2c5214753d169c69a59", - "assets/build/ba_data/models/locatorCircle.bob": "https://files.ballistica.net/cache/ba1/88/57/8d8697e73e4ed06efb17b7cb01c7", - "assets/build/ba_data/models/locatorCircleOutline.bob": "https://files.ballistica.net/cache/ba1/88/ed/cebd7de8e856e3f2ccb9af02f21b", - "assets/build/ba_data/models/logo.bob": "https://files.ballistica.net/cache/ba1/23/de/aec4078f52db613f942bf1550589", - "assets/build/ba_data/models/logoTransparent.bob": "https://files.ballistica.net/cache/ba1/ea/9a/b1dc54665e65f3f66845836d4173", - "assets/build/ba_data/models/melForeArm.bob": "https://files.ballistica.net/cache/ba1/df/45/21c25e914563d86b299677000c06", - "assets/build/ba_data/models/melHand.bob": "https://files.ballistica.net/cache/ba1/cf/de/d5b50cb4588e6fac284d4441bd2d", - "assets/build/ba_data/models/melHead.bob": "https://files.ballistica.net/cache/ba1/94/01/52fd952c92dae1f1c30ad84f049c", - "assets/build/ba_data/models/melLowerLeg.bob": "https://files.ballistica.net/cache/ba1/0b/b8/4213b3993e6d40f81f8cf61b4d8d", - "assets/build/ba_data/models/melToes.bob": "https://files.ballistica.net/cache/ba1/34/2c/170e6513e4fe1f5d9bb967eb192c", - "assets/build/ba_data/models/melTorso.bob": "https://files.ballistica.net/cache/ba1/f6/a8/d3f96eb0160efee2851a99b782ea", - "assets/build/ba_data/models/melUpperArm.bob": "https://files.ballistica.net/cache/ba1/93/99/2291094cbe3c210968b6526689c3", - "assets/build/ba_data/models/melUpperLeg.bob": "https://files.ballistica.net/cache/ba1/f5/7a/e55ee441ff80f8569a53d0dcfca9", - "assets/build/ba_data/models/meterTransparent.bob": "https://files.ballistica.net/cache/ba1/3d/79/0d1c1144f47197600d82e167e6c4", - "assets/build/ba_data/models/monkeyFaceLevel.bob": "https://files.ballistica.net/cache/ba1/df/2e/33ba114953fad6849407bfdf3ed3", - "assets/build/ba_data/models/monkeyFaceLevelBottom.bob": "https://files.ballistica.net/cache/ba1/f8/55/6f8ed5baa4700c4c2a48773fb066", - "assets/build/ba_data/models/monkeyFaceLevelBumper.cob": "https://files.ballistica.net/cache/ba1/91/1c/c753259db7961625fbe366fec7ba", - "assets/build/ba_data/models/monkeyFaceLevelCollide.cob": "https://files.ballistica.net/cache/ba1/c7/5b/078f986cb5ce190eaca02d09311e", - "assets/build/ba_data/models/natureBackground.bob": "https://files.ballistica.net/cache/ba1/0d/61/0bd4c025a65ea55342f6accbc65a", - "assets/build/ba_data/models/natureBackgroundCollide.cob": "https://files.ballistica.net/cache/ba1/28/f6/b587286d5a6f48251ccebfdfe9be", - "assets/build/ba_data/models/natureBackgroundVRFill.bob": "https://files.ballistica.net/cache/ba1/ab/1a/dc94843b65838d42b084deea9f3e", - "assets/build/ba_data/models/neoSpazForeArm.bob": "https://files.ballistica.net/cache/ba1/c0/57/9bedbf53b420c70e7e3f6e2b796d", - "assets/build/ba_data/models/neoSpazHand.bob": "https://files.ballistica.net/cache/ba1/59/94/cf564045e45478977b6d03221018", - "assets/build/ba_data/models/neoSpazHead.bob": "https://files.ballistica.net/cache/ba1/4f/f5/050477686a968511476c45fc0f36", - "assets/build/ba_data/models/neoSpazLowerLeg.bob": "https://files.ballistica.net/cache/ba1/c9/da/1d11d5025cc10320ebd17f5a4e70", - "assets/build/ba_data/models/neoSpazPelvis.bob": "https://files.ballistica.net/cache/ba1/6c/eb/b644d5d5f3e1e55879585e24727a", - "assets/build/ba_data/models/neoSpazToes.bob": "https://files.ballistica.net/cache/ba1/fa/09/3201fd91a7215408b2ee98cf0ee9", - "assets/build/ba_data/models/neoSpazTorso.bob": "https://files.ballistica.net/cache/ba1/ac/8d/aec90ef483ecac258f54bfd5c1ab", - "assets/build/ba_data/models/neoSpazUpperArm.bob": "https://files.ballistica.net/cache/ba1/82/ce/103cc35018868b7751fa6fee9ed0", - "assets/build/ba_data/models/neoSpazUpperLeg.bob": "https://files.ballistica.net/cache/ba1/79/11/2ffb8406f502f58b5682eb51ab01", - "assets/build/ba_data/models/ninjaForeArm.bob": "https://files.ballistica.net/cache/ba1/b8/59/7b1bd544d349ccc8f521037c862c", - "assets/build/ba_data/models/ninjaHand.bob": "https://files.ballistica.net/cache/ba1/f1/ab/5680a3782bfc8cfce07966eec205", - "assets/build/ba_data/models/ninjaHead.bob": "https://files.ballistica.net/cache/ba1/b5/a8/2c478221c59e5d6ebd7de031875a", - "assets/build/ba_data/models/ninjaLowerLeg.bob": "https://files.ballistica.net/cache/ba1/7a/14/3b0d906e00f84967ffb439d76e6b", - "assets/build/ba_data/models/ninjaPelvis.bob": "https://files.ballistica.net/cache/ba1/a5/32/d011aa4dfeb277bbfca79311d87c", - "assets/build/ba_data/models/ninjaToes.bob": "https://files.ballistica.net/cache/ba1/ce/00/8515e5dda839b2700b07e1614b00", - "assets/build/ba_data/models/ninjaTorso.bob": "https://files.ballistica.net/cache/ba1/48/a0/7051842294e158133fadf787aa29", - "assets/build/ba_data/models/ninjaUpperArm.bob": "https://files.ballistica.net/cache/ba1/01/89/f4b16114fe2124e49bfaabe89bf1", - "assets/build/ba_data/models/ninjaUpperLeg.bob": "https://files.ballistica.net/cache/ba1/d8/a9/ae3e7d19c703a901a8312d2856b2", - "assets/build/ba_data/models/oldLadyForeArm.bob": "https://files.ballistica.net/cache/ba1/4d/79/b5a1d99f3c00f4ba943400c11146", - "assets/build/ba_data/models/oldLadyHand.bob": "https://files.ballistica.net/cache/ba1/68/c9/ff1392e4cd64e63ea998f147b3a0", - "assets/build/ba_data/models/oldLadyHead.bob": "https://files.ballistica.net/cache/ba1/a4/b6/f129cea985624029fc83fbeb342c", - "assets/build/ba_data/models/oldLadyLowerLeg.bob": "https://files.ballistica.net/cache/ba1/b9/b9/4a04c8bac7c2919796820570ab91", - "assets/build/ba_data/models/oldLadyPelvis.bob": "https://files.ballistica.net/cache/ba1/fc/b6/68fdabc14183d7402fc241cad73d", - "assets/build/ba_data/models/oldLadyToes.bob": "https://files.ballistica.net/cache/ba1/4f/26/e43feeaabc81db1c7fa218f5a18c", - "assets/build/ba_data/models/oldLadyTorso.bob": "https://files.ballistica.net/cache/ba1/73/24/18a1734c7ce97866c71043228318", - "assets/build/ba_data/models/oldLadyUpperArm.bob": "https://files.ballistica.net/cache/ba1/f3/a2/b08213f5c7c2b76c3b8049bdbcd6", - "assets/build/ba_data/models/oldLadyUpperLeg.bob": "https://files.ballistica.net/cache/ba1/34/a5/28933dd4781eab86e70268aa140b", - "assets/build/ba_data/models/operaSingerForeArm.bob": "https://files.ballistica.net/cache/ba1/5b/f9/45dac44de66ddec3d3dc9d940665", - "assets/build/ba_data/models/operaSingerHand.bob": "https://files.ballistica.net/cache/ba1/97/44/3e6b06b97a47e9d064b8de1fdaee", - "assets/build/ba_data/models/operaSingerHead.bob": "https://files.ballistica.net/cache/ba1/20/00/db5c96ba6cdfaf483d9d5096cfde", - "assets/build/ba_data/models/operaSingerLowerLeg.bob": "https://files.ballistica.net/cache/ba1/33/17/ad861186dc9ce690b47785bcf5bf", - "assets/build/ba_data/models/operaSingerPelvis.bob": "https://files.ballistica.net/cache/ba1/e5/90/a77e6d1564d5dc87496f0875d9a6", - "assets/build/ba_data/models/operaSingerToes.bob": "https://files.ballistica.net/cache/ba1/34/32/75ee366de6d085ef63015ffce108", - "assets/build/ba_data/models/operaSingerTorso.bob": "https://files.ballistica.net/cache/ba1/45/22/f1cd80a3f78ff982db77493e6ba3", - "assets/build/ba_data/models/operaSingerUpperArm.bob": "https://files.ballistica.net/cache/ba1/56/30/afe26dcf97af0d0b51970a5a232d", - "assets/build/ba_data/models/operaSingerUpperLeg.bob": "https://files.ballistica.net/cache/ba1/3b/2b/732e60d3546c21c9af6c4130be81", - "assets/build/ba_data/models/overlayGuide.bob": "https://files.ballistica.net/cache/ba1/cb/5f/49f570ee207f7103f7eecb42f7db", - "assets/build/ba_data/models/penguinForeArm.bob": "https://files.ballistica.net/cache/ba1/de/62/89c72525487626c566b122d33780", - "assets/build/ba_data/models/penguinHand.bob": "https://files.ballistica.net/cache/ba1/ce/49/ebb9f9eed6e0e131503f600dc59b", - "assets/build/ba_data/models/penguinHead.bob": "https://files.ballistica.net/cache/ba1/c2/7e/33c221f7beff699295f0f7cb8e80", - "assets/build/ba_data/models/penguinLowerLeg.bob": "https://files.ballistica.net/cache/ba1/ca/98/2a4b7820b3de3ca04e9b09c4b2fc", - "assets/build/ba_data/models/penguinPelvis.bob": "https://files.ballistica.net/cache/ba1/ec/39/aa8725e319f2a2dbfedb28167596", - "assets/build/ba_data/models/penguinToes.bob": "https://files.ballistica.net/cache/ba1/3c/4a/14321baabc6bc499d3821f66631c", - "assets/build/ba_data/models/penguinTorso.bob": "https://files.ballistica.net/cache/ba1/c2/a5/19c58a0f33143563565f568c8d18", - "assets/build/ba_data/models/penguinUpperArm.bob": "https://files.ballistica.net/cache/ba1/4f/30/0a9c388f4d32b45598756437fc02", - "assets/build/ba_data/models/penguinUpperLeg.bob": "https://files.ballistica.net/cache/ba1/5e/4f/b4d2efb1295b051c666fc30cf25d", - "assets/build/ba_data/models/pixieForeArm.bob": "https://files.ballistica.net/cache/ba1/9d/7c/6e2a30d84bfa4a137e5dd4d77f84", - "assets/build/ba_data/models/pixieHand.bob": "https://files.ballistica.net/cache/ba1/11/9a/7f48c12e50fbcdc8bee795e9b714", - "assets/build/ba_data/models/pixieHead.bob": "https://files.ballistica.net/cache/ba1/0c/c3/be8b8c4b8b83a4519dc390fbb016", - "assets/build/ba_data/models/pixieLowerLeg.bob": "https://files.ballistica.net/cache/ba1/59/5f/27f215a51b0d88d0d2b4e7b34e09", - "assets/build/ba_data/models/pixiePelvis.bob": "https://files.ballistica.net/cache/ba1/55/26/6f97f1e0f17e01e270c440e985e6", - "assets/build/ba_data/models/pixieToes.bob": "https://files.ballistica.net/cache/ba1/2a/1a/cc8221884da30ba5fe834755c8de", - "assets/build/ba_data/models/pixieTorso.bob": "https://files.ballistica.net/cache/ba1/dd/a4/3cc625e7568671af9d91a2752add", - "assets/build/ba_data/models/pixieUpperArm.bob": "https://files.ballistica.net/cache/ba1/f9/e5/3d2df15668bebe6577b78398eae5", - "assets/build/ba_data/models/pixieUpperLeg.bob": "https://files.ballistica.net/cache/ba1/68/90/fae0f02685cd8d556cd2ce199edc", - "assets/build/ba_data/models/plasticEyesTransparent.bob": "https://files.ballistica.net/cache/ba1/80/b3/edd8666a01eed3f57136968bdce9", - "assets/build/ba_data/models/playerLineup1Transparent.bob": "https://files.ballistica.net/cache/ba1/64/d0/500eb683997cc3806984dce6a53d", - "assets/build/ba_data/models/playerLineup2Transparent.bob": "https://files.ballistica.net/cache/ba1/e2/fa/d6d2b9cd093dc5511485c8342296", - "assets/build/ba_data/models/playerLineup3Transparent.bob": "https://files.ballistica.net/cache/ba1/b5/65/255f3879a356079d3598a70682d9", - "assets/build/ba_data/models/playerLineup4Transparent.bob": "https://files.ballistica.net/cache/ba1/6f/7e/1aa5ea1982b1b3df5c9aa173cf84", - "assets/build/ba_data/models/powerup.bob": "https://files.ballistica.net/cache/ba1/c4/02/8217cecd1331942a3e4cfab8ccee", - "assets/build/ba_data/models/powerupSimple.bob": "https://files.ballistica.net/cache/ba1/67/10/479cd32d3984bdb60e4d8f918542", - "assets/build/ba_data/models/puck.bob": "https://files.ballistica.net/cache/ba1/7b/b4/bf5b423328f1875137e0d2f8a4de", - "assets/build/ba_data/models/rampageBG.bob": "https://files.ballistica.net/cache/ba1/22/a5/fc017f2d0143ebc60643b3dd3188", - "assets/build/ba_data/models/rampageBG2.bob": "https://files.ballistica.net/cache/ba1/72/40/5aa50ddfd6274987f56d75c092f9", - "assets/build/ba_data/models/rampageBumper.cob": "https://files.ballistica.net/cache/ba1/db/07/6c2fb30ff93b1827a5831b823aec", - "assets/build/ba_data/models/rampageLevel.bob": "https://files.ballistica.net/cache/ba1/5c/fc/ae639969212b5c3820fa809b5a2b", - "assets/build/ba_data/models/rampageLevelBottom.bob": "https://files.ballistica.net/cache/ba1/8d/c6/84d3e18e748c1016a39ee817c53b", - "assets/build/ba_data/models/rampageLevelCollide.cob": "https://files.ballistica.net/cache/ba1/af/c1/16888b04bfd64b9451640dae3c73", - "assets/build/ba_data/models/rampageVRFill.bob": "https://files.ballistica.net/cache/ba1/39/6e/1ac1a86ea64700cbbf90567ecaf7", - "assets/build/ba_data/models/robotForeArm.bob": "https://files.ballistica.net/cache/ba1/8a/e3/2f4f1d27133cd5d7ada42aa01998", - "assets/build/ba_data/models/robotHand.bob": "https://files.ballistica.net/cache/ba1/f4/44/497ca858aa131d3319667ecca0cd", - "assets/build/ba_data/models/robotHead.bob": "https://files.ballistica.net/cache/ba1/82/ce/75496f4b3d256b5eae984516218b", - "assets/build/ba_data/models/robotLowerLeg.bob": "https://files.ballistica.net/cache/ba1/9a/5d/23ad0b9ee083ead0ab2128598998", - "assets/build/ba_data/models/robotPelvis.bob": "https://files.ballistica.net/cache/ba1/ae/0c/00b79ef1f1dc52e0e66d1b7c6d10", - "assets/build/ba_data/models/robotToes.bob": "https://files.ballistica.net/cache/ba1/68/10/b8e01022caf8fdd19c63b9296646", - "assets/build/ba_data/models/robotTorso.bob": "https://files.ballistica.net/cache/ba1/b5/20/d93cccb8fab6345256e39a0979e5", - "assets/build/ba_data/models/robotUpperArm.bob": "https://files.ballistica.net/cache/ba1/73/c6/068669bf776f763010fdfd96f6ad", - "assets/build/ba_data/models/robotUpperLeg.bob": "https://files.ballistica.net/cache/ba1/5f/58/a1f41b024125cfe1973d4d0bacc2", - "assets/build/ba_data/models/roundaboutLevel.bob": "https://files.ballistica.net/cache/ba1/54/20/5ea868c4e7bed162a74695e7d5cf", - "assets/build/ba_data/models/roundaboutLevelBottom.bob": "https://files.ballistica.net/cache/ba1/e1/96/57e22f2b2d4e422f45c718bc49c7", - "assets/build/ba_data/models/roundaboutLevelBumper.cob": "https://files.ballistica.net/cache/ba1/7e/0a/67dfa4982ae32efe1fd3aa8d7553", - "assets/build/ba_data/models/roundaboutLevelCollide.cob": "https://files.ballistica.net/cache/ba1/a8/c4/7364b72e0647974d095dda8fa367", - "assets/build/ba_data/models/runningShoes.bob": "https://files.ballistica.net/cache/ba1/d9/05/b65c13dc594722bfbaffd7ca4b9a", - "assets/build/ba_data/models/santaForeArm.bob": "https://files.ballistica.net/cache/ba1/cc/16/afd5e1052d32919bc83190b472f0", - "assets/build/ba_data/models/santaHand.bob": "https://files.ballistica.net/cache/ba1/29/86/4a3f22f46fa6a66c21442d7765c5", - "assets/build/ba_data/models/santaHead.bob": "https://files.ballistica.net/cache/ba1/82/88/d911c23a0a43daf23bbdd5ca2a9a", - "assets/build/ba_data/models/santaLowerLeg.bob": "https://files.ballistica.net/cache/ba1/d3/18/d55f0812dede0c4d188bab4b0113", - "assets/build/ba_data/models/santaToes.bob": "https://files.ballistica.net/cache/ba1/48/2e/f226da167cd99080eda6deb43a36", - "assets/build/ba_data/models/santaTorso.bob": "https://files.ballistica.net/cache/ba1/fa/97/5412251561c506ccaa8f4b1493d2", - "assets/build/ba_data/models/santaUpperArm.bob": "https://files.ballistica.net/cache/ba1/96/bd/8efa21d6d224bca2c9971f2a0a4e", - "assets/build/ba_data/models/santaUpperLeg.bob": "https://files.ballistica.net/cache/ba1/db/11/60bcc01580eeb8030eb773739165", - "assets/build/ba_data/models/scorch.bob": "https://files.ballistica.net/cache/ba1/cb/90/c0f843e51f7f66a9bb30004c9b94", - "assets/build/ba_data/models/scrollBarThumbOpaque.bob": "https://files.ballistica.net/cache/ba1/2a/07/d7b44476348ecbf1ecf65b896f68", - "assets/build/ba_data/models/scrollBarThumbShortOpaque.bob": "https://files.ballistica.net/cache/ba1/25/e2/0ffed8edf1d67c2a06f746f150b2", - "assets/build/ba_data/models/scrollBarThumbShortSimple.bob": "https://files.ballistica.net/cache/ba1/fe/c2/51ec1d5a72abe04b4308ba2a78ba", - "assets/build/ba_data/models/scrollBarThumbShortTransparent.bob": "https://files.ballistica.net/cache/ba1/35/75/b3d68a5fe3091463acf9efa9d7e7", - "assets/build/ba_data/models/scrollBarThumbSimple.bob": "https://files.ballistica.net/cache/ba1/c9/f6/804de0e69f8a31570b6426990450", - "assets/build/ba_data/models/scrollBarThumbTransparent.bob": "https://files.ballistica.net/cache/ba1/d7/ad/5dadefee0cf46dcfe2edd162cc88", - "assets/build/ba_data/models/scrollBarTroughTransparent.bob": "https://files.ballistica.net/cache/ba1/77/99/65f8b205649691cf7f121c896a22", - "assets/build/ba_data/models/scrollWidgetShort.bob": "https://files.ballistica.net/cache/ba1/c0/71/0767fce83c9e97e99d0fa34f70ef", - "assets/build/ba_data/models/shield.bob": "https://files.ballistica.net/cache/ba1/49/2a/e4e85b8c3a09405dc01e53576be0", - "assets/build/ba_data/models/shockWave.bob": "https://files.ballistica.net/cache/ba1/25/cf/eb455291ba2995cae44c23d14b46", - "assets/build/ba_data/models/shrapnel1.bob": "https://files.ballistica.net/cache/ba1/92/c9/39560da6db5e000f6835bffb297a", - "assets/build/ba_data/models/shrapnelBoard.bob": "https://files.ballistica.net/cache/ba1/c5/a5/b5ccc527086a11c2d5ce6c07f904", - "assets/build/ba_data/models/shrapnelSlime.bob": "https://files.ballistica.net/cache/ba1/88/79/ae9a5d2e583bcbfba23eddf56776", - "assets/build/ba_data/models/softEdgeInside.bob": "https://files.ballistica.net/cache/ba1/6d/b5/de80530e6bfefedcd9b79d947e03", - "assets/build/ba_data/models/softEdgeOutside.bob": "https://files.ballistica.net/cache/ba1/7c/13/2faa1a560289aedad08a92c2c4da", - "assets/build/ba_data/models/stepRightUpLevel.bob": "https://files.ballistica.net/cache/ba1/2d/d5/f11cecfb99a5aa7a95ba9d4c0cd9", - "assets/build/ba_data/models/stepRightUpLevelBottom.bob": "https://files.ballistica.net/cache/ba1/92/d8/b1a119d433320446f3caff09ff0a", - "assets/build/ba_data/models/stepRightUpLevelCollide.cob": "https://files.ballistica.net/cache/ba1/12/c9/76e4180a09a5bc45614dfc874d9f", - "assets/build/ba_data/models/stepRightUpVRFillMound.bob": "https://files.ballistica.net/cache/ba1/f5/f4/0b870f134842f7aa5d918b550614", - "assets/build/ba_data/models/superheroForeArm.bob": "https://files.ballistica.net/cache/ba1/1c/41/7e2c6833ea37819b5583ec3c7674", - "assets/build/ba_data/models/superheroHand.bob": "https://files.ballistica.net/cache/ba1/8b/90/9e52ba9483c8b38d5d540b6d9a33", - "assets/build/ba_data/models/superheroHead.bob": "https://files.ballistica.net/cache/ba1/ab/04/63b19bebb1b19054881b141aab73", - "assets/build/ba_data/models/superheroLowerLeg.bob": "https://files.ballistica.net/cache/ba1/82/fa/3b47b2da208c64bf91f038deb734", - "assets/build/ba_data/models/superheroPelvis.bob": "https://files.ballistica.net/cache/ba1/cc/20/e5e57c5d976883b65d7ed1cce3e8", - "assets/build/ba_data/models/superheroToes.bob": "https://files.ballistica.net/cache/ba1/b1/e8/b091f65ab911e2fa4fe28a135676", - "assets/build/ba_data/models/superheroTorso.bob": "https://files.ballistica.net/cache/ba1/65/5e/e74fe653b0a749a6dde2f79b671f", - "assets/build/ba_data/models/superheroUpperArm.bob": "https://files.ballistica.net/cache/ba1/ff/b5/63372861b856d0252bf3367c4517", - "assets/build/ba_data/models/superheroUpperLeg.bob": "https://files.ballistica.net/cache/ba1/47/4d/a0891687b3ab9b74c84ef1db5407", - "assets/build/ba_data/models/textBoxTransparent.bob": "https://files.ballistica.net/cache/ba1/ab/e6/358caf8eb80bbaeda086e8d48a4d", - "assets/build/ba_data/models/thePadBG.bob": "https://files.ballistica.net/cache/ba1/36/7b/f5295c9dd67f898b75300401728c", - "assets/build/ba_data/models/thePadBGSmall.bob": "https://files.ballistica.net/cache/ba1/f5/ff/01fcd28863a759807ca079c0bd77", - "assets/build/ba_data/models/thePadLevel.bob": "https://files.ballistica.net/cache/ba1/54/d2/d3f0956fa2e1f186f32bfc48bb68", - "assets/build/ba_data/models/thePadLevelBottom.bob": "https://files.ballistica.net/cache/ba1/69/d9/a7cc958480ecacee72c462feec85", - "assets/build/ba_data/models/thePadLevelBumper.cob": "https://files.ballistica.net/cache/ba1/71/2b/2f4b38db1adcaff7149436953baf", - "assets/build/ba_data/models/thePadLevelCollide.cob": "https://files.ballistica.net/cache/ba1/99/e6/8fcbe6dd5ecbab8a8e27b38cbad2", - "assets/build/ba_data/models/thePadVRFillBottom.bob": "https://files.ballistica.net/cache/ba1/74/17/b240306aa7716afbad23990081e7", - "assets/build/ba_data/models/thePadVRFillMound.bob": "https://files.ballistica.net/cache/ba1/04/f2/3811a62f5363004d6f91adb95373", - "assets/build/ba_data/models/thePadVRFillTop.bob": "https://files.ballistica.net/cache/ba1/77/a0/979a4b2c178938ab54fea47cb342", - "assets/build/ba_data/models/tipTopBG.bob": "https://files.ballistica.net/cache/ba1/c3/d0/d170b81d66265412bda9bd32322d", - "assets/build/ba_data/models/tipTopLevel.bob": "https://files.ballistica.net/cache/ba1/8f/81/03a14674551f3f8fd32eb0aee0fd", - "assets/build/ba_data/models/tipTopLevelBottom.bob": "https://files.ballistica.net/cache/ba1/38/f2/db7a3e29a13c0b09943ff2dca1a2", - "assets/build/ba_data/models/tipTopLevelBumper.cob": "https://files.ballistica.net/cache/ba1/b4/21/fb3664c4afae81aee7d11ce483c8", - "assets/build/ba_data/models/tipTopLevelCollide.cob": "https://files.ballistica.net/cache/ba1/f7/11/3e368ce2b132c85a6e1c0ed1dcaa", - "assets/build/ba_data/models/tnt.bob": "https://files.ballistica.net/cache/ba1/86/41/fed2ad95fb6292313861446ebb56", - "assets/build/ba_data/models/toolbarBacking.bob": "https://files.ballistica.net/cache/ba1/77/bf/2168fed3b1d3dcd1dc25f297e3db", - "assets/build/ba_data/models/toolbarBackingBottom.bob": "https://files.ballistica.net/cache/ba1/91/0a/0fddbc031e2a685c3dbfec86ce0e", - "assets/build/ba_data/models/toolbarBackingBottom2.bob": "https://files.ballistica.net/cache/ba1/4b/d4/ad47a1c316a114699d8c49472bfa", - "assets/build/ba_data/models/toolbarBackingOpaque.bob": "https://files.ballistica.net/cache/ba1/2b/74/b99209b2e4b225b716d734f2bb91", - "assets/build/ba_data/models/toolbarBackingTop.bob": "https://files.ballistica.net/cache/ba1/24/1c/cd6625a0c3d70c63d620cc37531f", - "assets/build/ba_data/models/toolbarBackingTop2.bob": "https://files.ballistica.net/cache/ba1/f1/1a/ecb9d61fa5d36324f82fccb6b38b", - "assets/build/ba_data/models/toolbarBackingTransparent.bob": "https://files.ballistica.net/cache/ba1/40/c8/defe35ce061da13f84bdeacff0b2", - "assets/build/ba_data/models/towerDLevel.bob": "https://files.ballistica.net/cache/ba1/a7/87/fb0d14ea35a11fbc6a17d13d5f35", - "assets/build/ba_data/models/towerDLevelBottom.bob": "https://files.ballistica.net/cache/ba1/80/cc/0c938ec519ba9c3bc9aeb51c11c6", - "assets/build/ba_data/models/towerDLevelCollide.cob": "https://files.ballistica.net/cache/ba1/66/83/af98677836422135d93a378867a4", - "assets/build/ba_data/models/towerDPlayerWall.cob": "https://files.ballistica.net/cache/ba1/5a/0a/cf6a24d5003e5b0a4fb4f1103695", - "assets/build/ba_data/models/trees.bob": "https://files.ballistica.net/cache/ba1/72/2e/709d5217f70a8e9f871f9cb36765", - "assets/build/ba_data/models/vrFade.bob": "https://files.ballistica.net/cache/ba1/3a/39/0b46b332f9099b5c0087f4fff67f", - "assets/build/ba_data/models/vrOverlay.bob": "https://files.ballistica.net/cache/ba1/c1/07/c75f8388c32635734f2c43af2885", - "assets/build/ba_data/models/warriorForeArm.bob": "https://files.ballistica.net/cache/ba1/c5/11/a2a7105d75fbb3da89739d0c398c", - "assets/build/ba_data/models/warriorHand.bob": "https://files.ballistica.net/cache/ba1/98/57/93fb838b1d140df444f3d29a829f", - "assets/build/ba_data/models/warriorHead.bob": "https://files.ballistica.net/cache/ba1/d9/0b/7c74624b44c795f00aae46042bdf", - "assets/build/ba_data/models/warriorLowerLeg.bob": "https://files.ballistica.net/cache/ba1/14/1a/3fd21e798814a8ba095f3e6cd4b0", - "assets/build/ba_data/models/warriorPelvis.bob": "https://files.ballistica.net/cache/ba1/36/19/f9f61a6225dd591b2a562eb61ea6", - "assets/build/ba_data/models/warriorToes.bob": "https://files.ballistica.net/cache/ba1/12/a9/2947b1d60aca8f0bae7ae57dfd0e", - "assets/build/ba_data/models/warriorTorso.bob": "https://files.ballistica.net/cache/ba1/68/95/942ca8f6d1a1fabb74c57c919709", - "assets/build/ba_data/models/warriorUpperArm.bob": "https://files.ballistica.net/cache/ba1/8d/13/012542d9b1f126346c99660a7e2b", - "assets/build/ba_data/models/warriorUpperLeg.bob": "https://files.ballistica.net/cache/ba1/66/91/7eff24024cb925b7d14e89a143bb", - "assets/build/ba_data/models/windowBGBlotch.bob": "https://files.ballistica.net/cache/ba1/9c/0d/41000cefdc36ccfe76b46754208c", - "assets/build/ba_data/models/windowHSmallVMedOpaque.bob": "https://files.ballistica.net/cache/ba1/30/a1/358be1aa3077582976b1f8b38167", - "assets/build/ba_data/models/windowHSmallVMedTransparent.bob": "https://files.ballistica.net/cache/ba1/31/f4/60098a4fc56a3afb9495bcc9a801", - "assets/build/ba_data/models/windowHSmallVSmallOpaque.bob": "https://files.ballistica.net/cache/ba1/3c/b5/557f9ef70365cebff6aba217f75e", - "assets/build/ba_data/models/windowHSmallVSmallTransparent.bob": "https://files.ballistica.net/cache/ba1/1c/55/8eceda6b2399eb499719432978b8", - "assets/build/ba_data/models/wing.bob": "https://files.ballistica.net/cache/ba1/78/54/c01fbbba6bef88468386218e76b9", - "assets/build/ba_data/models/witchForeArm.bob": "https://files.ballistica.net/cache/ba1/56/cf/aff82765c210be005dd7af43b9c0", - "assets/build/ba_data/models/witchHand.bob": "https://files.ballistica.net/cache/ba1/b0/f7/bfb73ddf006b5c7b07c44a8166eb", - "assets/build/ba_data/models/witchHead.bob": "https://files.ballistica.net/cache/ba1/44/b2/e95dd3a1c3b8685d91784b679418", - "assets/build/ba_data/models/witchLowerLeg.bob": "https://files.ballistica.net/cache/ba1/40/1b/690036b992eca29f9457dae140fa", - "assets/build/ba_data/models/witchPelvis.bob": "https://files.ballistica.net/cache/ba1/c2/60/b73ae4d8e0adcde627d2aea63194", - "assets/build/ba_data/models/witchToes.bob": "https://files.ballistica.net/cache/ba1/31/14/51653f3141aad8d796d8012dc817", - "assets/build/ba_data/models/witchTorso.bob": "https://files.ballistica.net/cache/ba1/75/d3/ac21ef8bb915bcc30bda47e5e3ea", - "assets/build/ba_data/models/witchUpperArm.bob": "https://files.ballistica.net/cache/ba1/a4/8b/e95f1ba13689886bae98a813a3fb", - "assets/build/ba_data/models/witchUpperLeg.bob": "https://files.ballistica.net/cache/ba1/da/f3/83bf5f77abce88e0ba7ddd3bfbde", - "assets/build/ba_data/models/wizardForeArm.bob": "https://files.ballistica.net/cache/ba1/7e/20/824b45fd56ef923d61ea7b91a904", - "assets/build/ba_data/models/wizardHand.bob": "https://files.ballistica.net/cache/ba1/66/bd/6059a0dcf1c14eb4dc47904aec94", - "assets/build/ba_data/models/wizardHead.bob": "https://files.ballistica.net/cache/ba1/8b/64/918cea6808d80f53a57ccee2029b", - "assets/build/ba_data/models/wizardLowerLeg.bob": "https://files.ballistica.net/cache/ba1/f4/9a/d8f380aba0775bff0fd35f98ba9a", - "assets/build/ba_data/models/wizardPelvis.bob": "https://files.ballistica.net/cache/ba1/63/e7/f5940cc4598d1dbeece261ea6c82", - "assets/build/ba_data/models/wizardToes.bob": "https://files.ballistica.net/cache/ba1/a2/c0/9b550223f1df470b6bfc2df23f71", - "assets/build/ba_data/models/wizardTorso.bob": "https://files.ballistica.net/cache/ba1/e7/de/40ed0fe87a04dcff8167185aae82", - "assets/build/ba_data/models/wizardUpperArm.bob": "https://files.ballistica.net/cache/ba1/c3/36/edd82626eaece0540499f4bef58b", - "assets/build/ba_data/models/wizardUpperLeg.bob": "https://files.ballistica.net/cache/ba1/5b/cc/8a55165ee3e89a8a12b930fe9259", - "assets/build/ba_data/models/wrestlerForeArm.bob": "https://files.ballistica.net/cache/ba1/3d/d8/c7dc3f23954cf73611e3f843df65", - "assets/build/ba_data/models/wrestlerHand.bob": "https://files.ballistica.net/cache/ba1/34/dd/018da291336b1db8531205756b9e", - "assets/build/ba_data/models/wrestlerHead.bob": "https://files.ballistica.net/cache/ba1/26/39/bd606fc2c06f6b42c0ea91d1c0ae", - "assets/build/ba_data/models/wrestlerLowerLeg.bob": "https://files.ballistica.net/cache/ba1/9d/b7/428464bf2b21e1942b4211dc1c03", - "assets/build/ba_data/models/wrestlerPelvis.bob": "https://files.ballistica.net/cache/ba1/5e/6c/4fc75a3ad6ec4b84cce04236688f", - "assets/build/ba_data/models/wrestlerToes.bob": "https://files.ballistica.net/cache/ba1/17/21/260e90fa37946b810bdb1a3dddeb", - "assets/build/ba_data/models/wrestlerTorso.bob": "https://files.ballistica.net/cache/ba1/25/cb/78cd940ded69c1a3abd3c1d9d9d5", - "assets/build/ba_data/models/wrestlerUpperArm.bob": "https://files.ballistica.net/cache/ba1/66/20/5b2c138ed007027af4b44a5720bd", - "assets/build/ba_data/models/wrestlerUpperLeg.bob": "https://files.ballistica.net/cache/ba1/dd/c8/e9377bafef0b51417e3b6ad33291", - "assets/build/ba_data/models/zigZagLevel.bob": "https://files.ballistica.net/cache/ba1/4a/a3/903c652d6ef5262ad760d697fd8d", - "assets/build/ba_data/models/zigZagLevelBottom.bob": "https://files.ballistica.net/cache/ba1/46/06/7bb8afa448c61f3307bca7e358ab", - "assets/build/ba_data/models/zigZagLevelBumper.cob": "https://files.ballistica.net/cache/ba1/34/b8/f96d8ec97403ad393e40382332f3", - "assets/build/ba_data/models/zigZagLevelCollide.cob": "https://files.ballistica.net/cache/ba1/93/c8/b044042c482d1e53a5384969624d", - "assets/build/ba_data/models/zoeForeArm.bob": "https://files.ballistica.net/cache/ba1/17/27/ef435bee72da504382311590577d", - "assets/build/ba_data/models/zoeHand.bob": "https://files.ballistica.net/cache/ba1/72/fe/e93690c16e5937eabd938df0a1fe", - "assets/build/ba_data/models/zoeHead.bob": "https://files.ballistica.net/cache/ba1/c1/7a/68d264712df92e22da89f5ae5324", - "assets/build/ba_data/models/zoeLowerLeg.bob": "https://files.ballistica.net/cache/ba1/8f/a2/6f358d24c474b4c8dccc5b5e591e", - "assets/build/ba_data/models/zoePelvis.bob": "https://files.ballistica.net/cache/ba1/ae/92/0aecd44c739165e97e55f3e529d8", - "assets/build/ba_data/models/zoeToes.bob": "https://files.ballistica.net/cache/ba1/41/69/96fb4ed28eed3bf8a704a256558c", - "assets/build/ba_data/models/zoeTorso.bob": "https://files.ballistica.net/cache/ba1/88/66/74a21f09ca6cfbfe7352219e43e6", - "assets/build/ba_data/models/zoeUpperArm.bob": "https://files.ballistica.net/cache/ba1/99/38/b7694cae0804260eeb337aa1676a", - "assets/build/ba_data/models/zoeUpperLeg.bob": "https://files.ballistica.net/cache/ba1/83/4f/28b2202d0109fa93272c0b09fa2d", - "assets/build/ba_data/python-site-packages/_yaml/__init__.py": "https://files.ballistica.net/cache/ba1/0d/45/65ba92f51d411dcffac8835b6130", - "assets/build/ba_data/python-site-packages/certifi/__init__.py": "https://files.ballistica.net/cache/ba1/1b/3d/98d25cd57f325c7dcda2d6f9054d", - "assets/build/ba_data/python-site-packages/certifi/__main__.py": "https://files.ballistica.net/cache/ba1/b2/bb/d7d8216212bcf66cdc3067700fb7", - "assets/build/ba_data/python-site-packages/certifi/cacert.pem": "https://files.ballistica.net/cache/ba1/33/4f/e92ea306a333f3c82d95b577653e", - "assets/build/ba_data/python-site-packages/certifi/core.py": "https://files.ballistica.net/cache/ba1/ac/28/37f05b52df3806856bce2c0b9772", - "assets/build/ba_data/python-site-packages/typing_extensions.py": "https://files.ballistica.net/cache/ba1/77/43/af9b9eedb8efd97e5818a2655a44", - "assets/build/ba_data/python-site-packages/yaml/__init__.py": "https://files.ballistica.net/cache/ba1/e5/47/17715ca7620f3b9749558b9dcb2d", - "assets/build/ba_data/python-site-packages/yaml/composer.py": "https://files.ballistica.net/cache/ba1/3e/aa/d7fcfc4707ad19a6964d72654b82", - "assets/build/ba_data/python-site-packages/yaml/constructor.py": "https://files.ballistica.net/cache/ba1/f4/29/cd8c7f5a2296d8f1715ad49b5797", - "assets/build/ba_data/python-site-packages/yaml/cyaml.py": "https://files.ballistica.net/cache/ba1/b4/98/dcdd4c31e2ff2c7a938170694020", - "assets/build/ba_data/python-site-packages/yaml/dumper.py": "https://files.ballistica.net/cache/ba1/10/86/350a48501c2ef0e9e0e524412eab", - "assets/build/ba_data/python-site-packages/yaml/emitter.py": "https://files.ballistica.net/cache/ba1/f2/68/4697cd68fb49c8ef517197535ba0", - "assets/build/ba_data/python-site-packages/yaml/error.py": "https://files.ballistica.net/cache/ba1/7b/d6/a06f6f135d32d5e96d727188346f", - "assets/build/ba_data/python-site-packages/yaml/events.py": "https://files.ballistica.net/cache/ba1/27/8e/94bb286138b923b47154842b5347", - "assets/build/ba_data/python-site-packages/yaml/loader.py": "https://files.ballistica.net/cache/ba1/71/dd/b4170459e37bafff6c0fe0d054b5", - "assets/build/ba_data/python-site-packages/yaml/nodes.py": "https://files.ballistica.net/cache/ba1/e5/f1/1d2e0edb5460dff0ebb10a92fe24", - "assets/build/ba_data/python-site-packages/yaml/parser.py": "https://files.ballistica.net/cache/ba1/c0/a6/e94c7a3f8f4f08803f68f9387731", - "assets/build/ba_data/python-site-packages/yaml/reader.py": "https://files.ballistica.net/cache/ba1/0d/58/9196a3104da22ed15afe27f67387", - "assets/build/ba_data/python-site-packages/yaml/representer.py": "https://files.ballistica.net/cache/ba1/81/c0/f24b6b2f2c777f9467bd24aa2789", - "assets/build/ba_data/python-site-packages/yaml/resolver.py": "https://files.ballistica.net/cache/ba1/e6/98/e6145dd5cfebbc456fb3881ce523", - "assets/build/ba_data/python-site-packages/yaml/scanner.py": "https://files.ballistica.net/cache/ba1/de/13/75284ee45274f24637d9c79bd0cb", - "assets/build/ba_data/python-site-packages/yaml/serializer.py": "https://files.ballistica.net/cache/ba1/19/a9/f996e312d2ca2d4cb974bcc4f5bc", - "assets/build/ba_data/python-site-packages/yaml/tokens.py": "https://files.ballistica.net/cache/ba1/93/58/d4c7731a1b991391782441f2ba8e", - "assets/build/ba_data/textures/achievementBoxer.dds": "https://files.ballistica.net/cache/ba1/ad/42/9203f2d7fac0185e04d5f163901d", - "assets/build/ba_data/textures/achievementBoxer.ktx": "https://files.ballistica.net/cache/ba1/79/76/18850573cd8bf8aae2a0bece21c7", - "assets/build/ba_data/textures/achievementBoxer.pvr": "https://files.ballistica.net/cache/ba1/5e/68/11b5ea9be250623669983832952a", - "assets/build/ba_data/textures/achievementBoxer_preview.png": "https://files.ballistica.net/cache/ba1/e8/2f/73bca43e56c91a742949f7cc7c16", - "assets/build/ba_data/textures/achievementCrossHair.dds": "https://files.ballistica.net/cache/ba1/e6/a2/f945b03f24b4ec71d5d5629f88fc", - "assets/build/ba_data/textures/achievementCrossHair.ktx": "https://files.ballistica.net/cache/ba1/49/1c/638c0be579433a86772d11c82e8c", - "assets/build/ba_data/textures/achievementCrossHair.pvr": "https://files.ballistica.net/cache/ba1/a1/f0/85f12820d1c81c839e91e09c829c", - "assets/build/ba_data/textures/achievementCrossHair_preview.png": "https://files.ballistica.net/cache/ba1/59/b8/4ffbe9e9ff74bc6cecafdacb544d", - "assets/build/ba_data/textures/achievementDualWielding.dds": "https://files.ballistica.net/cache/ba1/60/58/af4b45e593f9a421d45f2a7a15f5", - "assets/build/ba_data/textures/achievementDualWielding.ktx": "https://files.ballistica.net/cache/ba1/8d/6d/9ab5f1c59ada7e2cdd9db83cfa7c", - "assets/build/ba_data/textures/achievementDualWielding.pvr": "https://files.ballistica.net/cache/ba1/7c/21/8f6959d7196bb1623ae65c25d804", - "assets/build/ba_data/textures/achievementDualWielding_preview.png": "https://files.ballistica.net/cache/ba1/f8/61/072614c48ec86483f8c902cd98dc", - "assets/build/ba_data/textures/achievementEmpty.dds": "https://files.ballistica.net/cache/ba1/80/35/ca22cefd3a4b6a315ff9268012c3", - "assets/build/ba_data/textures/achievementEmpty.ktx": "https://files.ballistica.net/cache/ba1/81/47/24fda69d6c5fd3a2736d6b77f082", - "assets/build/ba_data/textures/achievementEmpty.pvr": "https://files.ballistica.net/cache/ba1/81/47/7f464160a45e7f8df551f02b8ea6", - "assets/build/ba_data/textures/achievementEmpty_preview.png": "https://files.ballistica.net/cache/ba1/ce/f6/086bc605cbece840527b51fc3f47", - "assets/build/ba_data/textures/achievementFlawlessVictory.dds": "https://files.ballistica.net/cache/ba1/9d/62/067d46adea91bfcc9d4dc9ce8b7c", - "assets/build/ba_data/textures/achievementFlawlessVictory.ktx": "https://files.ballistica.net/cache/ba1/ca/77/af2f650d4cf117ec705ad6ee3af3", - "assets/build/ba_data/textures/achievementFlawlessVictory.pvr": "https://files.ballistica.net/cache/ba1/97/c2/65ce9370f1e39382654179cda394", - "assets/build/ba_data/textures/achievementFlawlessVictory_preview.png": "https://files.ballistica.net/cache/ba1/2e/78/371b8389258ff18c36524b7ef5f9", - "assets/build/ba_data/textures/achievementFootballShutout.dds": "https://files.ballistica.net/cache/ba1/6c/3e/f9e2e1be4e45304a2ef768ec9dfe", - "assets/build/ba_data/textures/achievementFootballShutout.ktx": "https://files.ballistica.net/cache/ba1/b5/7d/8b2e3de3b8f8e017d9244d091272", - "assets/build/ba_data/textures/achievementFootballShutout.pvr": "https://files.ballistica.net/cache/ba1/e1/73/fb16eaa57ad397443176f508851c", - "assets/build/ba_data/textures/achievementFootballShutout_preview.png": "https://files.ballistica.net/cache/ba1/2d/3e/7a12d7c449bed234ac257365c157", - "assets/build/ba_data/textures/achievementFootballVictory.dds": "https://files.ballistica.net/cache/ba1/bb/06/01643c69642b093e2ad508f968c2", - "assets/build/ba_data/textures/achievementFootballVictory.ktx": "https://files.ballistica.net/cache/ba1/b4/0b/a9ee1ab1a283dcd897cc993167bd", - "assets/build/ba_data/textures/achievementFootballVictory.pvr": "https://files.ballistica.net/cache/ba1/ad/82/bdde642000f0477f34e34a2965cb", - "assets/build/ba_data/textures/achievementFootballVictory_preview.png": "https://files.ballistica.net/cache/ba1/7d/f4/d8451bd9a50c2f36b909b54e8d4c", - "assets/build/ba_data/textures/achievementFreeLoader.dds": "https://files.ballistica.net/cache/ba1/fe/76/5d65964da11b0cfed5611f89b42b", - "assets/build/ba_data/textures/achievementFreeLoader.ktx": "https://files.ballistica.net/cache/ba1/6c/72/9aff51cf52f9c745d9c7db7ee8c7", - "assets/build/ba_data/textures/achievementFreeLoader.pvr": "https://files.ballistica.net/cache/ba1/92/cd/0033e59c541525ad2e5826f80881", - "assets/build/ba_data/textures/achievementFreeLoader_preview.png": "https://files.ballistica.net/cache/ba1/bb/89/7249f9f958099ed9a571fd953598", - "assets/build/ba_data/textures/achievementGotTheMoves.dds": "https://files.ballistica.net/cache/ba1/53/2a/b68c18982566692260d311456fa9", - "assets/build/ba_data/textures/achievementGotTheMoves.ktx": "https://files.ballistica.net/cache/ba1/5c/63/68b26e53d4157cfd5dff4caa2468", - "assets/build/ba_data/textures/achievementGotTheMoves.pvr": "https://files.ballistica.net/cache/ba1/ac/e8/be25c2141bf80fc236470a9f7a3f", - "assets/build/ba_data/textures/achievementGotTheMoves_preview.png": "https://files.ballistica.net/cache/ba1/d5/4a/3d6763b76daa83d722c589546c93", - "assets/build/ba_data/textures/achievementInControl.dds": "https://files.ballistica.net/cache/ba1/89/21/1f3ff8714c0908a8b7a1a3987ab0", - "assets/build/ba_data/textures/achievementInControl.ktx": "https://files.ballistica.net/cache/ba1/f7/b9/20fd9a4bacdc295d447c04e535e9", - "assets/build/ba_data/textures/achievementInControl.pvr": "https://files.ballistica.net/cache/ba1/87/ec/cb7322e750f0bf739397d9857bca", - "assets/build/ba_data/textures/achievementInControl_preview.png": "https://files.ballistica.net/cache/ba1/47/20/e928b8b653e36ff609036a652fc4", - "assets/build/ba_data/textures/achievementMedalLarge.dds": "https://files.ballistica.net/cache/ba1/49/31/5256be4187fe4bfbfd9886a84b9c", - "assets/build/ba_data/textures/achievementMedalLarge.ktx": "https://files.ballistica.net/cache/ba1/03/5e/06cf7b25b844ebc77660d9d0d3a8", - "assets/build/ba_data/textures/achievementMedalLarge.pvr": "https://files.ballistica.net/cache/ba1/55/51/0dbc6c4c2afa7b45bfdcb7c94d4f", - "assets/build/ba_data/textures/achievementMedalLarge_preview.png": "https://files.ballistica.net/cache/ba1/da/7a/8f64578db4201112f7c17d1cccfd", - "assets/build/ba_data/textures/achievementMedalMedium.dds": "https://files.ballistica.net/cache/ba1/45/1a/a4398412861ecc9820cb40f70116", - "assets/build/ba_data/textures/achievementMedalMedium.ktx": "https://files.ballistica.net/cache/ba1/39/a3/15b3fe4c2a967679acea72de9015", - "assets/build/ba_data/textures/achievementMedalMedium.pvr": "https://files.ballistica.net/cache/ba1/89/10/83fcb47692f8f7c71956fdb764a8", - "assets/build/ba_data/textures/achievementMedalMedium_preview.png": "https://files.ballistica.net/cache/ba1/c8/2a/3674182a81c946da4891b46fda0a", - "assets/build/ba_data/textures/achievementMedalSmall.dds": "https://files.ballistica.net/cache/ba1/5c/77/52f04246ce41af04b6bdd26d35ae", - "assets/build/ba_data/textures/achievementMedalSmall.ktx": "https://files.ballistica.net/cache/ba1/b6/af/cb4844ad3ed67f7e9871b22648c1", - "assets/build/ba_data/textures/achievementMedalSmall.pvr": "https://files.ballistica.net/cache/ba1/b1/e6/ca154ff868d521e3a0378cdc332f", - "assets/build/ba_data/textures/achievementMedalSmall_preview.png": "https://files.ballistica.net/cache/ba1/ce/1f/8e744e4aba8cb1acbb723978c7b4", - "assets/build/ba_data/textures/achievementMine.dds": "https://files.ballistica.net/cache/ba1/99/b6/fcc7c9a6cb72864085b1b37e803d", - "assets/build/ba_data/textures/achievementMine.ktx": "https://files.ballistica.net/cache/ba1/68/b3/c4de57981d0e8049a2c8b4bc6023", - "assets/build/ba_data/textures/achievementMine.pvr": "https://files.ballistica.net/cache/ba1/cc/7e/52356e107a2d6ac1710ce87da3b0", - "assets/build/ba_data/textures/achievementMine_preview.png": "https://files.ballistica.net/cache/ba1/09/17/3875ae175e4f75a98901a1cae1c2", - "assets/build/ba_data/textures/achievementOffYouGo.dds": "https://files.ballistica.net/cache/ba1/19/52/02539a9ea21a141af006c1413105", - "assets/build/ba_data/textures/achievementOffYouGo.ktx": "https://files.ballistica.net/cache/ba1/c8/6a/5b36bf83c29cec49da39d3d20798", - "assets/build/ba_data/textures/achievementOffYouGo.pvr": "https://files.ballistica.net/cache/ba1/37/32/422cd6be997356efcad0d8432016", - "assets/build/ba_data/textures/achievementOffYouGo_preview.png": "https://files.ballistica.net/cache/ba1/4a/16/d567ae4e6be716735d45e93bb588", - "assets/build/ba_data/textures/achievementOnslaught.dds": "https://files.ballistica.net/cache/ba1/24/8e/65514203296fcac62b5a79c54fd8", - "assets/build/ba_data/textures/achievementOnslaught.ktx": "https://files.ballistica.net/cache/ba1/bc/c1/e7cf9a694dce071462d1694f6db8", - "assets/build/ba_data/textures/achievementOnslaught.pvr": "https://files.ballistica.net/cache/ba1/5b/0a/71ec4dc8e4e623aaeee1f3c439d8", - "assets/build/ba_data/textures/achievementOnslaught_preview.png": "https://files.ballistica.net/cache/ba1/7c/7f/bacf5219bfb04134f9831651857f", - "assets/build/ba_data/textures/achievementOutline.dds": "https://files.ballistica.net/cache/ba1/7e/c9/f7851c7991c80a5b561f15dcfc39", - "assets/build/ba_data/textures/achievementOutline.ktx": "https://files.ballistica.net/cache/ba1/e2/6a/e79197efcfe63222e1e7141ce304", - "assets/build/ba_data/textures/achievementOutline.pvr": "https://files.ballistica.net/cache/ba1/13/e4/b69213ff2c69f73c66166e19dd78", - "assets/build/ba_data/textures/achievementOutline_preview.png": "https://files.ballistica.net/cache/ba1/6c/0d/255bbc9e7dc0be253373991f024a", - "assets/build/ba_data/textures/achievementRunaround.dds": "https://files.ballistica.net/cache/ba1/87/bf/86f99a080429357462ad8b9194d3", - "assets/build/ba_data/textures/achievementRunaround.ktx": "https://files.ballistica.net/cache/ba1/96/eb/9d1428723f92631702ad24037c68", - "assets/build/ba_data/textures/achievementRunaround.pvr": "https://files.ballistica.net/cache/ba1/7f/94/e6c828e8b4decf80e9136ab19b6e", - "assets/build/ba_data/textures/achievementRunaround_preview.png": "https://files.ballistica.net/cache/ba1/eb/5f/2986ded7ae0d4d04f5e32d9d9841", - "assets/build/ba_data/textures/achievementSharingIsCaring.dds": "https://files.ballistica.net/cache/ba1/90/0a/0db3027d0ff01f332a39f6468e16", - "assets/build/ba_data/textures/achievementSharingIsCaring.ktx": "https://files.ballistica.net/cache/ba1/4f/cf/e5484cf614b9e167afd31eebfd67", - "assets/build/ba_data/textures/achievementSharingIsCaring.pvr": "https://files.ballistica.net/cache/ba1/35/7b/49e28bce72a35e1b232ac73db9c3", - "assets/build/ba_data/textures/achievementSharingIsCaring_preview.png": "https://files.ballistica.net/cache/ba1/01/17/25759ce0ed30eaa6b159673a5416", - "assets/build/ba_data/textures/achievementStayinAlive.dds": "https://files.ballistica.net/cache/ba1/59/5e/54920807ea580ea3238a01d967c0", - "assets/build/ba_data/textures/achievementStayinAlive.ktx": "https://files.ballistica.net/cache/ba1/a7/80/de91f9aafce8b8515f99695d1513", - "assets/build/ba_data/textures/achievementStayinAlive.pvr": "https://files.ballistica.net/cache/ba1/a4/76/b12f94a07b1e8a17410accc03fe2", - "assets/build/ba_data/textures/achievementStayinAlive_preview.png": "https://files.ballistica.net/cache/ba1/47/b0/75a2c0b763703a8a27d296d101e1", - "assets/build/ba_data/textures/achievementSuperPunch.dds": "https://files.ballistica.net/cache/ba1/89/6a/366fb62423f583b80edbeb5bd484", - "assets/build/ba_data/textures/achievementSuperPunch.ktx": "https://files.ballistica.net/cache/ba1/e7/b5/d5100520da3d7a71681c088841e7", - "assets/build/ba_data/textures/achievementSuperPunch.pvr": "https://files.ballistica.net/cache/ba1/a9/60/b823d6cd268f204478d0bd8ae6d2", - "assets/build/ba_data/textures/achievementSuperPunch_preview.png": "https://files.ballistica.net/cache/ba1/34/49/4b89dde93d979b275748d53f2c3d", - "assets/build/ba_data/textures/achievementTNT.dds": "https://files.ballistica.net/cache/ba1/40/eb/8abb772e903295b5dab0193b95ae", - "assets/build/ba_data/textures/achievementTNT.ktx": "https://files.ballistica.net/cache/ba1/2f/90/63ed59b9a0874432cf0d560d326e", - "assets/build/ba_data/textures/achievementTNT.pvr": "https://files.ballistica.net/cache/ba1/2b/53/d7c75d8daa4a9e4615c000a42e90", - "assets/build/ba_data/textures/achievementTNT_preview.png": "https://files.ballistica.net/cache/ba1/e2/0b/1160a26bf4b2c463d935fcbefbd6", - "assets/build/ba_data/textures/achievementTeamPlayer.dds": "https://files.ballistica.net/cache/ba1/70/ec/4db3493b54ba614003cd6863caaa", - "assets/build/ba_data/textures/achievementTeamPlayer.ktx": "https://files.ballistica.net/cache/ba1/94/c0/89e489392a06b3608ba2c093c017", - "assets/build/ba_data/textures/achievementTeamPlayer.pvr": "https://files.ballistica.net/cache/ba1/b4/06/00c51b533aae5bfdcb3a23278a17", - "assets/build/ba_data/textures/achievementTeamPlayer_preview.png": "https://files.ballistica.net/cache/ba1/20/3c/815cd0588da3bf5c7d6065d6ebfa", - "assets/build/ba_data/textures/achievementWall.dds": "https://files.ballistica.net/cache/ba1/c6/8f/b7bf511dbd48d43d8533c8f7e45a", - "assets/build/ba_data/textures/achievementWall.ktx": "https://files.ballistica.net/cache/ba1/7f/ca/aa47adef8b6fe89872ac572866ae", - "assets/build/ba_data/textures/achievementWall.pvr": "https://files.ballistica.net/cache/ba1/5c/ac/b6ce3bed59b17a5bdea28adaab9f", - "assets/build/ba_data/textures/achievementWall_preview.png": "https://files.ballistica.net/cache/ba1/87/a0/43c771d262bcce2a2417fd3295d6", - "assets/build/ba_data/textures/achievementsIcon.dds": "https://files.ballistica.net/cache/ba1/8b/0c/bffd8ad7a051002337a84f59805c", - "assets/build/ba_data/textures/achievementsIcon.ktx": "https://files.ballistica.net/cache/ba1/00/b7/afa2fd4c91c56a1b5877c5cfc57c", - "assets/build/ba_data/textures/achievementsIcon.pvr": "https://files.ballistica.net/cache/ba1/9b/2f/0defbe0d752d6ae83fcd4cc7cc14", - "assets/build/ba_data/textures/achievementsIcon_preview.png": "https://files.ballistica.net/cache/ba1/86/93/b4ff0e6ec3c1cc77d387bd20ee86", - "assets/build/ba_data/textures/actionButtons.dds": "https://files.ballistica.net/cache/ba1/cd/46/f18798621256a2a727b3966d8357", - "assets/build/ba_data/textures/actionButtons.ktx": "https://files.ballistica.net/cache/ba1/b6/f3/dd97cc67fe595f5d130aca945e19", - "assets/build/ba_data/textures/actionButtons.pvr": "https://files.ballistica.net/cache/ba1/a0/b7/6593e03be85043106b7faed5f34b", - "assets/build/ba_data/textures/actionButtons_preview.png": "https://files.ballistica.net/cache/ba1/ed/79/fdbaf987f172674f30102c3f2c13", - "assets/build/ba_data/textures/actionHeroColor.dds": "https://files.ballistica.net/cache/ba1/62/91/355fa26219c26750e46ad358dbc7", - "assets/build/ba_data/textures/actionHeroColor.ktx": "https://files.ballistica.net/cache/ba1/01/ce/95415b6b29f60015900b2f33db42", - "assets/build/ba_data/textures/actionHeroColor.pvr": "https://files.ballistica.net/cache/ba1/0d/03/832309d85531d4be991b6f7a9107", - "assets/build/ba_data/textures/actionHeroColorMask.dds": "https://files.ballistica.net/cache/ba1/5c/c0/7f52de795a1f08ffa72030cf9178", - "assets/build/ba_data/textures/actionHeroColorMask.ktx": "https://files.ballistica.net/cache/ba1/07/ed/6791e01ac69bb61cdffade59808f", - "assets/build/ba_data/textures/actionHeroColorMask.pvr": "https://files.ballistica.net/cache/ba1/22/40/d50cbe4fa13a3294638bb1b42c72", - "assets/build/ba_data/textures/actionHeroColorMask_preview.png": "https://files.ballistica.net/cache/ba1/83/2f/9a009e8f2cc9b956043511db105c", - "assets/build/ba_data/textures/actionHeroColor_preview.png": "https://files.ballistica.net/cache/ba1/ba/30/6903150a2c99ac224897828a7fd8", - "assets/build/ba_data/textures/actionHeroIcon.dds": "https://files.ballistica.net/cache/ba1/90/ba/4c98578717e53cb9e7e3ee3d610f", - "assets/build/ba_data/textures/actionHeroIcon.ktx": "https://files.ballistica.net/cache/ba1/8b/ed/d4ce9568146182986f10d2c50f0b", - "assets/build/ba_data/textures/actionHeroIcon.pvr": "https://files.ballistica.net/cache/ba1/b1/d3/8bb9354ed4db9e806d6013e0af25", - "assets/build/ba_data/textures/actionHeroIconColorMask.dds": "https://files.ballistica.net/cache/ba1/0f/1f/11bde7c17293557c4bfec985ebff", - "assets/build/ba_data/textures/actionHeroIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/2d/ef/175d5f2b5b8303ddd1eed4968ecd", - "assets/build/ba_data/textures/actionHeroIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/6d/a3/47482b9f9f2d566211b3a9adb7d6", - "assets/build/ba_data/textures/actionHeroIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/7d/9b/6df54a2a8afddfca0c56308e2a33", - "assets/build/ba_data/textures/actionHeroIcon_preview.png": "https://files.ballistica.net/cache/ba1/a5/18/07b3ed00815389169a271aed9402", - "assets/build/ba_data/textures/advancedIcon.dds": "https://files.ballistica.net/cache/ba1/6b/4f/825ecc4d72a391d50d8b7db33b1b", - "assets/build/ba_data/textures/advancedIcon.ktx": "https://files.ballistica.net/cache/ba1/87/6d/1758aaf14120f6c418e2ecdb410f", - "assets/build/ba_data/textures/advancedIcon.pvr": "https://files.ballistica.net/cache/ba1/39/00/237c07cd4a239ca69f90ba571e07", - "assets/build/ba_data/textures/advancedIcon_preview.png": "https://files.ballistica.net/cache/ba1/21/d5/3a3a834112b84befe910c15df2b6", - "assets/build/ba_data/textures/agentColor.dds": "https://files.ballistica.net/cache/ba1/05/19/15d35284e062e8d037dfece71af5", - "assets/build/ba_data/textures/agentColor.ktx": "https://files.ballistica.net/cache/ba1/ff/8a/3e375971f932cba1e4c8800129e8", - "assets/build/ba_data/textures/agentColor.pvr": "https://files.ballistica.net/cache/ba1/f8/3a/438fae77e7ad607345ff883c272d", - "assets/build/ba_data/textures/agentColorMask.dds": "https://files.ballistica.net/cache/ba1/dc/b1/a5a98e27864377a91452abcfba66", - "assets/build/ba_data/textures/agentColorMask.ktx": "https://files.ballistica.net/cache/ba1/64/9a/255eeabf2f4a5626d478b41f2268", - "assets/build/ba_data/textures/agentColorMask.pvr": "https://files.ballistica.net/cache/ba1/6a/d2/7a311a8525c02f7c8edb974e9651", - "assets/build/ba_data/textures/agentColorMask_preview.png": "https://files.ballistica.net/cache/ba1/78/ec/c4ddf22b95d893fa467412334ff5", - "assets/build/ba_data/textures/agentColor_preview.png": "https://files.ballistica.net/cache/ba1/9f/1b/1c128a69900d438c220ca7885dc7", - "assets/build/ba_data/textures/agentIcon.dds": "https://files.ballistica.net/cache/ba1/b4/ea/bc2ed456f6536711e03d292121fa", - "assets/build/ba_data/textures/agentIcon.ktx": "https://files.ballistica.net/cache/ba1/eb/1e/e73199f7c33e9002e92fe363a23b", - "assets/build/ba_data/textures/agentIcon.pvr": "https://files.ballistica.net/cache/ba1/5e/2d/e18e2133517fdcc8cb7ac28b31a8", - "assets/build/ba_data/textures/agentIconColorMask.dds": "https://files.ballistica.net/cache/ba1/b1/9a/ecb2300afababd460ca4bae36d96", - "assets/build/ba_data/textures/agentIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/79/e6/4f01fdb2751db2c57ef8828e0fce", - "assets/build/ba_data/textures/agentIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/22/24/8aadc852761e3f2c0fbf9d2f025a", - "assets/build/ba_data/textures/agentIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/e2/02/85032b1257accbb85ad1904018ef", - "assets/build/ba_data/textures/agentIcon_preview.png": "https://files.ballistica.net/cache/ba1/49/70/68d243aeff2de4e474e9a22ee6ef", - "assets/build/ba_data/textures/aliBSRemoteIOSQR.dds": "https://files.ballistica.net/cache/ba1/b7/69/9c4a96ceb3f84c6670355cac136d", - "assets/build/ba_data/textures/aliBSRemoteIOSQR.ktx": "https://files.ballistica.net/cache/ba1/10/a6/2cc21c0bae6eb45208974d8b1f60", - "assets/build/ba_data/textures/aliBSRemoteIOSQR.pvr": "https://files.ballistica.net/cache/ba1/56/8f/3f31426a93b70d4c1c7c54050473", - "assets/build/ba_data/textures/aliBSRemoteIOSQR_preview.png": "https://files.ballistica.net/cache/ba1/96/d8/d37b845efc618616a8e1fd0dd04b", - "assets/build/ba_data/textures/aliColor.dds": "https://files.ballistica.net/cache/ba1/54/17/1d3b7c4d9577908ed773ad11dad2", - "assets/build/ba_data/textures/aliColor.ktx": "https://files.ballistica.net/cache/ba1/ed/f3/3005b276e2b231be8170102c1cd7", - "assets/build/ba_data/textures/aliColor.pvr": "https://files.ballistica.net/cache/ba1/f3/bd/2c82648f6ef242ca4b34fd278629", - "assets/build/ba_data/textures/aliColorMask.dds": "https://files.ballistica.net/cache/ba1/68/bb/c7db9f1886a6d3fd5cdb308f3cef", - "assets/build/ba_data/textures/aliColorMask.ktx": "https://files.ballistica.net/cache/ba1/4c/a3/992550084846cbc6ca1dbfcc9a4d", - "assets/build/ba_data/textures/aliColorMask.pvr": "https://files.ballistica.net/cache/ba1/31/2a/ddb2a68bdfb3b175be2d3d6bcade", - "assets/build/ba_data/textures/aliColorMask_preview.png": "https://files.ballistica.net/cache/ba1/3b/05/0dd47cdf3a3ac437e5e276f5fd86", - "assets/build/ba_data/textures/aliColor_preview.png": "https://files.ballistica.net/cache/ba1/cb/93/d9230bee0324b2eec4bb8519dae3", - "assets/build/ba_data/textures/aliControllerQR.dds": "https://files.ballistica.net/cache/ba1/55/0a/44ecb9f5cf4e5c02b83089d0d13b", - "assets/build/ba_data/textures/aliControllerQR.ktx": "https://files.ballistica.net/cache/ba1/56/50/6d84e5b788b25b4e7275d20ce635", - "assets/build/ba_data/textures/aliControllerQR.pvr": "https://files.ballistica.net/cache/ba1/22/8d/34c3d66662866cc806c81ee13517", - "assets/build/ba_data/textures/aliControllerQR_preview.png": "https://files.ballistica.net/cache/ba1/59/83/a2dce8f9ec55f87cf450813a31c1", - "assets/build/ba_data/textures/aliIcon.dds": "https://files.ballistica.net/cache/ba1/bf/c9/ad39ef333825bbd5d6f5462d1a11", - "assets/build/ba_data/textures/aliIcon.ktx": "https://files.ballistica.net/cache/ba1/d1/ee/7c1c37d57cdb70d854ecdfb235ed", - "assets/build/ba_data/textures/aliIcon.pvr": "https://files.ballistica.net/cache/ba1/b6/04/931fbed4b2a9c75ae7745a88ed40", - "assets/build/ba_data/textures/aliIconColorMask.dds": "https://files.ballistica.net/cache/ba1/58/bc/04f52451d42f051acb8409ab965b", - "assets/build/ba_data/textures/aliIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/78/89/c3e95a7c3794e4c86ece540aa3a3", - "assets/build/ba_data/textures/aliIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/7a/81/d269ece740203c2e1a9a3c9523af", - "assets/build/ba_data/textures/aliIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/0f/8d/203f6ec0ff5a8dd380330a0f3853", - "assets/build/ba_data/textures/aliIcon_preview.png": "https://files.ballistica.net/cache/ba1/b7/fc/f8f833a902bcb8f62f38b12365bd", - "assets/build/ba_data/textures/aliSplash.dds": "https://files.ballistica.net/cache/ba1/dc/b7/a91fa91497e15ffb176bb92c2928", - "assets/build/ba_data/textures/aliSplash.ktx": "https://files.ballistica.net/cache/ba1/e8/8b/d1e42f05af2373326127f5a06c2c", - "assets/build/ba_data/textures/aliSplash.pvr": "https://files.ballistica.net/cache/ba1/20/f8/04ae497bd7764a6bb626da069092", - "assets/build/ba_data/textures/aliSplash_preview.png": "https://files.ballistica.net/cache/ba1/67/7e/a9f0874b74986a4d76c718a4f3f5", - "assets/build/ba_data/textures/alienColor.dds": "https://files.ballistica.net/cache/ba1/b3/63/1ec79e210306f871f4d24b7a005e", - "assets/build/ba_data/textures/alienColor.ktx": "https://files.ballistica.net/cache/ba1/97/fe/96c9ab440866a08d2510109eaaff", - "assets/build/ba_data/textures/alienColor.pvr": "https://files.ballistica.net/cache/ba1/39/dc/bb457bc12cd61482e2c7edfa457c", - "assets/build/ba_data/textures/alienColorMask.dds": "https://files.ballistica.net/cache/ba1/ce/51/a41c4af1205bd33dc8647898e4b6", - "assets/build/ba_data/textures/alienColorMask.ktx": "https://files.ballistica.net/cache/ba1/a7/3e/7759a1566e2dacd35c0a23304fc7", - "assets/build/ba_data/textures/alienColorMask.pvr": "https://files.ballistica.net/cache/ba1/70/70/49e327a70992f6f2cd57a69e182b", - "assets/build/ba_data/textures/alienColorMask_preview.png": "https://files.ballistica.net/cache/ba1/6a/7c/7e2fe077f46778b639b58d533ef8", - "assets/build/ba_data/textures/alienColor_preview.png": "https://files.ballistica.net/cache/ba1/82/47/527c4d25c4a4d7ab7379cef645e2", - "assets/build/ba_data/textures/alienIcon.dds": "https://files.ballistica.net/cache/ba1/f0/26/8006c3bf165f2264e17f386f0b81", - "assets/build/ba_data/textures/alienIcon.ktx": "https://files.ballistica.net/cache/ba1/e2/1c/24e6309c2c8bf1b56b2ee8c3fdc2", - "assets/build/ba_data/textures/alienIcon.pvr": "https://files.ballistica.net/cache/ba1/a2/e3/2b1e7a36caf44ce30bdfc4dcca55", - "assets/build/ba_data/textures/alienIconColorMask.dds": "https://files.ballistica.net/cache/ba1/e4/d0/9a495289d4d7f910e8af16f6a8bd", - "assets/build/ba_data/textures/alienIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/33/18/6001520bfded073b3b57493baf27", - "assets/build/ba_data/textures/alienIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/0b/8a/54fa4a11a7d066ff0cbc6c741451", - "assets/build/ba_data/textures/alienIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/55/bc/686e349c8557f232c6404bc22371", - "assets/build/ba_data/textures/alienIcon_preview.png": "https://files.ballistica.net/cache/ba1/4c/c9/12ab7e3cf9f1af93c608ce1bcefb", - "assets/build/ba_data/textures/alwaysLandBGColor.dds": "https://files.ballistica.net/cache/ba1/88/d6/3afdd31b573a08eb0cf3771f86bb", - "assets/build/ba_data/textures/alwaysLandBGColor.ktx": "https://files.ballistica.net/cache/ba1/5a/80/c2ff48e9f0f3e549f105e926dd3d", - "assets/build/ba_data/textures/alwaysLandBGColor.pvr": "https://files.ballistica.net/cache/ba1/2d/c0/c236df146c22606a3d290139e5b3", - "assets/build/ba_data/textures/alwaysLandBGColor_preview.png": "https://files.ballistica.net/cache/ba1/59/9c/933c8322bf93c02968e6f440ae59", - "assets/build/ba_data/textures/alwaysLandLevelColor.dds": "https://files.ballistica.net/cache/ba1/1e/da/7e0f8f4f82a9728a9996fe4b76bc", - "assets/build/ba_data/textures/alwaysLandLevelColor.ktx": "https://files.ballistica.net/cache/ba1/13/bd/5458fc8068b06a20ec00d2e35c2e", - "assets/build/ba_data/textures/alwaysLandLevelColor.pvr": "https://files.ballistica.net/cache/ba1/29/12/7c1b247f977a20c2d70069f95787", - "assets/build/ba_data/textures/alwaysLandLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/4a/61/15580dd303e1682c7c2f63e890da", - "assets/build/ba_data/textures/alwaysLandPreview.dds": "https://files.ballistica.net/cache/ba1/de/5e/62854d25f37a4f2151716dcbb5e0", - "assets/build/ba_data/textures/alwaysLandPreview.ktx": "https://files.ballistica.net/cache/ba1/44/1b/8985da2a7c0670089ec086237de2", - "assets/build/ba_data/textures/alwaysLandPreview.pvr": "https://files.ballistica.net/cache/ba1/8b/bb/db84d43e0acd251940d39ad52622", - "assets/build/ba_data/textures/alwaysLandPreview_preview.png": "https://files.ballistica.net/cache/ba1/c7/6d/d633bbe5ad1fb1478a49cc51f702", - "assets/build/ba_data/textures/analogStick.dds": "https://files.ballistica.net/cache/ba1/43/4a/2122f06d757d26e069f3cce65263", - "assets/build/ba_data/textures/analogStick.ktx": "https://files.ballistica.net/cache/ba1/94/7f/d7ef5eca17e0dea73ac52f5e4786", - "assets/build/ba_data/textures/analogStick.pvr": "https://files.ballistica.net/cache/ba1/96/47/5f2fec9aacd09aff811bf1dbcec5", - "assets/build/ba_data/textures/analogStick_preview.png": "https://files.ballistica.net/cache/ba1/74/c6/3cf900aee1a67ca1c0fc94f37278", - "assets/build/ba_data/textures/arrow.dds": "https://files.ballistica.net/cache/ba1/fd/f4/1f701b511b86fe3c97e7b24ffdc1", - "assets/build/ba_data/textures/arrow.ktx": "https://files.ballistica.net/cache/ba1/91/ea/0dbc5131961233ca8e36c0457310", - "assets/build/ba_data/textures/arrow.pvr": "https://files.ballistica.net/cache/ba1/e7/4d/b5fde84e482c3f37854089eb2388", - "assets/build/ba_data/textures/arrow_preview.png": "https://files.ballistica.net/cache/ba1/14/e7/364396f22047676ac53683567246", - "assets/build/ba_data/textures/assassinColor.dds": "https://files.ballistica.net/cache/ba1/74/4c/15d8400e13ddef0e6712bf52dd6e", - "assets/build/ba_data/textures/assassinColor.ktx": "https://files.ballistica.net/cache/ba1/77/1e/340bae7b1759427c0cf4ef6f200a", - "assets/build/ba_data/textures/assassinColor.pvr": "https://files.ballistica.net/cache/ba1/88/27/2a6734a37b350a8fe9d4313169ae", - "assets/build/ba_data/textures/assassinColorMask.dds": "https://files.ballistica.net/cache/ba1/b9/72/87c5ff1b72de68f5751140508d0e", - "assets/build/ba_data/textures/assassinColorMask.ktx": "https://files.ballistica.net/cache/ba1/52/6b/126ba01cd6e6f883e0a6695aec9c", - "assets/build/ba_data/textures/assassinColorMask.pvr": "https://files.ballistica.net/cache/ba1/c0/2c/5b137a905b0719022101f82f581d", - "assets/build/ba_data/textures/assassinColorMask_preview.png": "https://files.ballistica.net/cache/ba1/b0/c5/cb9321a0aca799d83159479d602c", - "assets/build/ba_data/textures/assassinColor_preview.png": "https://files.ballistica.net/cache/ba1/dc/a3/e7dc273da8100f7df28fc8bb0cba", - "assets/build/ba_data/textures/assassinIcon.dds": "https://files.ballistica.net/cache/ba1/7f/77/4986af4e74b8cf271bd1be24d9ee", - "assets/build/ba_data/textures/assassinIcon.ktx": "https://files.ballistica.net/cache/ba1/f1/20/4101b22fd53bd3b32a9d59f7c21d", - "assets/build/ba_data/textures/assassinIcon.pvr": "https://files.ballistica.net/cache/ba1/8f/58/3006c5ef5d2a5312c2b9af288749", - "assets/build/ba_data/textures/assassinIconColorMask.dds": "https://files.ballistica.net/cache/ba1/ba/51/73c42c8801d22d3c7187cfa344df", - "assets/build/ba_data/textures/assassinIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/23/5c/a673051a793a3ca931062e3a70fd", - "assets/build/ba_data/textures/assassinIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/60/7b/8d10bbf1078ba5b0b4ea11978211", - "assets/build/ba_data/textures/assassinIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/c9/f9/32933a0bcc71c4d0a0bc20983355", - "assets/build/ba_data/textures/assassinIcon_preview.png": "https://files.ballistica.net/cache/ba1/55/e7/8f38bf1720bdbd9aa982935476ab", - "assets/build/ba_data/textures/audioIcon.dds": "https://files.ballistica.net/cache/ba1/bf/42/85a551c4c543da64b2dba477a37b", - "assets/build/ba_data/textures/audioIcon.ktx": "https://files.ballistica.net/cache/ba1/56/9c/ad6926be8575babb47018fe8fca0", - "assets/build/ba_data/textures/audioIcon.pvr": "https://files.ballistica.net/cache/ba1/13/9e/06cdda82c2416814a0f7add829b8", - "assets/build/ba_data/textures/audioIcon_preview.png": "https://files.ballistica.net/cache/ba1/e3/31/f32c08bf00eae0fa27d28b024c2f", - "assets/build/ba_data/textures/backIcon.dds": "https://files.ballistica.net/cache/ba1/e6/48/aa6e719ea1f48f7d0aee158b4235", - "assets/build/ba_data/textures/backIcon.ktx": "https://files.ballistica.net/cache/ba1/2f/06/7f53ab639e9ce2301b9d80b1b235", - "assets/build/ba_data/textures/backIcon.pvr": "https://files.ballistica.net/cache/ba1/c9/75/aa03196694e61989f7ea091b8700", - "assets/build/ba_data/textures/backIcon_preview.png": "https://files.ballistica.net/cache/ba1/b6/31/c0b1e958662b364c6e52fc10dd4f", - "assets/build/ba_data/textures/bar.dds": "https://files.ballistica.net/cache/ba1/cf/e7/7b1a5ef3dd8790f64b1d889c78ea", - "assets/build/ba_data/textures/bar.ktx": "https://files.ballistica.net/cache/ba1/69/25/7c811b9c7e77e6c7d1df1a047991", - "assets/build/ba_data/textures/bar.pvr": "https://files.ballistica.net/cache/ba1/1c/b7/605cdb8193d4190f3a90a173cc90", - "assets/build/ba_data/textures/bar_preview.png": "https://files.ballistica.net/cache/ba1/b6/63/94b82a98ae1305b0f6b57fe32e8b", - "assets/build/ba_data/textures/bearColor.dds": "https://files.ballistica.net/cache/ba1/e3/9a/f6b129a0fe2933fb23643e71d9ef", - "assets/build/ba_data/textures/bearColor.ktx": "https://files.ballistica.net/cache/ba1/62/fc/be9341da4cfa66a8ce63807a3a8e", - "assets/build/ba_data/textures/bearColor.pvr": "https://files.ballistica.net/cache/ba1/c3/f7/1773a5033ab0a6689d01e0683a63", - "assets/build/ba_data/textures/bearColorMask.dds": "https://files.ballistica.net/cache/ba1/38/04/166ea3d73ed0af9b285db1b7e6c0", - "assets/build/ba_data/textures/bearColorMask.ktx": "https://files.ballistica.net/cache/ba1/6d/fa/f8715cf7e43136c9517b73354c97", - "assets/build/ba_data/textures/bearColorMask.pvr": "https://files.ballistica.net/cache/ba1/8e/df/cf06039b8152fd6e4c856d3bb10b", - "assets/build/ba_data/textures/bearColorMask_preview.png": "https://files.ballistica.net/cache/ba1/b7/fe/2967bb4af05b1f36e8b7be7d2e83", - "assets/build/ba_data/textures/bearColor_preview.png": "https://files.ballistica.net/cache/ba1/06/40/3a19c93262e5a2182fb4de97ad3e", - "assets/build/ba_data/textures/bearIcon.dds": "https://files.ballistica.net/cache/ba1/cd/80/163ac24028fd5d8ce484040d4176", - "assets/build/ba_data/textures/bearIcon.ktx": "https://files.ballistica.net/cache/ba1/01/f7/c2ef1c82862e74f74386c6341cb8", - "assets/build/ba_data/textures/bearIcon.pvr": "https://files.ballistica.net/cache/ba1/87/83/c856c8fb48ba9d7a7fd9869746f2", - "assets/build/ba_data/textures/bearIconColorMask.dds": "https://files.ballistica.net/cache/ba1/78/c9/123f72ce037631bb8f1906b8fd92", - "assets/build/ba_data/textures/bearIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/ad/75/804e6901f09e75348f1d480c5ae8", - "assets/build/ba_data/textures/bearIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/ce/23/1fd7ce03b72db0dc6b3903d8146f", - "assets/build/ba_data/textures/bearIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/72/1c/9775c5e7acec13a491299b998703", - "assets/build/ba_data/textures/bearIcon_preview.png": "https://files.ballistica.net/cache/ba1/d9/d2/b74a4de7ca068c378d78c745331b", - "assets/build/ba_data/textures/bg.dds": "https://files.ballistica.net/cache/ba1/92/e3/7beb6a9b50408b92e02a94965664", - "assets/build/ba_data/textures/bg.ktx": "https://files.ballistica.net/cache/ba1/97/6c/b965850bfa25cf9db805b921d291", - "assets/build/ba_data/textures/bg.pvr": "https://files.ballistica.net/cache/ba1/11/84/be878cc8fbe77a3ffdb4cd4ce125", - "assets/build/ba_data/textures/bg_preview.png": "https://files.ballistica.net/cache/ba1/6a/77/7613140be4610b02dcb0372111ed", - "assets/build/ba_data/textures/bigG.dds": "https://files.ballistica.net/cache/ba1/49/34/fe6162a12af04927e4c3489dcfff", - "assets/build/ba_data/textures/bigG.ktx": "https://files.ballistica.net/cache/ba1/df/1c/526de060409ebbd5235592943e94", - "assets/build/ba_data/textures/bigG.pvr": "https://files.ballistica.net/cache/ba1/41/82/532ef83baefbde887504c11ce140", - "assets/build/ba_data/textures/bigGPreview.dds": "https://files.ballistica.net/cache/ba1/2b/1c/8fb6a40e7e08419473d22e23cfcc", - "assets/build/ba_data/textures/bigGPreview.ktx": "https://files.ballistica.net/cache/ba1/4a/28/d3c6f8ad9f0c65cd7a422d9177dd", - "assets/build/ba_data/textures/bigGPreview.pvr": "https://files.ballistica.net/cache/ba1/4f/b4/9b4069351815040b2af7c4c9b498", - "assets/build/ba_data/textures/bigGPreview_preview.png": "https://files.ballistica.net/cache/ba1/4b/24/f4b442bc8ed68d6f3f740ffb19fd", - "assets/build/ba_data/textures/bigG_preview.png": "https://files.ballistica.net/cache/ba1/0c/ab/049fbd4f509e4ef20ebe01094ac4", - "assets/build/ba_data/textures/black.dds": "https://files.ballistica.net/cache/ba1/fd/94/5cc65cb4653a94e0f1dfab09c150", - "assets/build/ba_data/textures/black.ktx": "https://files.ballistica.net/cache/ba1/61/89/d92ad39d0e515d22fe32137277c1", - "assets/build/ba_data/textures/black.pvr": "https://files.ballistica.net/cache/ba1/2e/94/1b6b37f6a20b7b20f3eb2af56d40", - "assets/build/ba_data/textures/black_preview.png": "https://files.ballistica.net/cache/ba1/5b/e4/0eb70f5a035fda63964394d26306", - "assets/build/ba_data/textures/bombButton.dds": "https://files.ballistica.net/cache/ba1/c0/09/a6a373f26d16094793465e3344ce", - "assets/build/ba_data/textures/bombButton.ktx": "https://files.ballistica.net/cache/ba1/43/1c/3faa35dcc3f0077fabb6da1e0ab6", - "assets/build/ba_data/textures/bombButton.pvr": "https://files.ballistica.net/cache/ba1/d4/ea/6ab05c2618044b9946968cb26489", - "assets/build/ba_data/textures/bombButton_preview.png": "https://files.ballistica.net/cache/ba1/02/38/4e7f65791bc68b2f5fd279ab1245", - "assets/build/ba_data/textures/bombColor.dds": "https://files.ballistica.net/cache/ba1/5e/2b/492e68d77e6be9d6399e49435a8d", - "assets/build/ba_data/textures/bombColor.ktx": "https://files.ballistica.net/cache/ba1/bc/9f/c7a76334cf56bb0bb93d1f720b40", - "assets/build/ba_data/textures/bombColor.pvr": "https://files.ballistica.net/cache/ba1/c7/bb/89159008de3b624bcb90b3c25568", - "assets/build/ba_data/textures/bombColorIce.dds": "https://files.ballistica.net/cache/ba1/ab/ed/3f5a8f9f2fb8d1e60f34f1f1701b", - "assets/build/ba_data/textures/bombColorIce.ktx": "https://files.ballistica.net/cache/ba1/4b/2d/c8cfa1990fa1f211de36679a3665", - "assets/build/ba_data/textures/bombColorIce.pvr": "https://files.ballistica.net/cache/ba1/71/aa/3127d32782835de9fa0fab8c7395", - "assets/build/ba_data/textures/bombColorIce_preview.png": "https://files.ballistica.net/cache/ba1/ec/3a/96b5191c5e7662be273c311fa826", - "assets/build/ba_data/textures/bombColor_preview.png": "https://files.ballistica.net/cache/ba1/a5/65/878c8aa7876031426f5adf3aeb79", - "assets/build/ba_data/textures/bombStickyColor.dds": "https://files.ballistica.net/cache/ba1/c2/2d/cf8b630173546f03926c56258724", - "assets/build/ba_data/textures/bombStickyColor.ktx": "https://files.ballistica.net/cache/ba1/be/83/f3b7ca04ca377e2df7359784d6cf", - "assets/build/ba_data/textures/bombStickyColor.pvr": "https://files.ballistica.net/cache/ba1/14/a3/90a633e9edd9f826d89d2f610a0d", - "assets/build/ba_data/textures/bombStickyColor_preview.png": "https://files.ballistica.net/cache/ba1/c0/98/05587d954ce791ba5caa41af943f", - "assets/build/ba_data/textures/bonesColor.dds": "https://files.ballistica.net/cache/ba1/02/23/f9f6293644faaff5d3fe2f49408f", - "assets/build/ba_data/textures/bonesColor.ktx": "https://files.ballistica.net/cache/ba1/51/24/beed81e702c15d04b4f94388a328", - "assets/build/ba_data/textures/bonesColor.pvr": "https://files.ballistica.net/cache/ba1/84/85/d3822cbf93732d312b64dd0d2b26", - "assets/build/ba_data/textures/bonesColorMask.dds": "https://files.ballistica.net/cache/ba1/9e/57/bc0c41cf259d94e11814170d0137", - "assets/build/ba_data/textures/bonesColorMask.ktx": "https://files.ballistica.net/cache/ba1/0e/70/eb39f12338c2212e02b4ffdaffb3", - "assets/build/ba_data/textures/bonesColorMask.pvr": "https://files.ballistica.net/cache/ba1/ef/5d/f5687a88ac101ecf3de903f90cfe", - "assets/build/ba_data/textures/bonesColorMask_preview.png": "https://files.ballistica.net/cache/ba1/dc/39/6e8b4a8ac53f887b26dd26336905", - "assets/build/ba_data/textures/bonesColor_preview.png": "https://files.ballistica.net/cache/ba1/d5/8a/dd482e6a09b1ca281f134822e8df", - "assets/build/ba_data/textures/bonesIcon.dds": "https://files.ballistica.net/cache/ba1/35/26/052a5342663ba22e4d9857cc147b", - "assets/build/ba_data/textures/bonesIcon.ktx": "https://files.ballistica.net/cache/ba1/61/07/8dd831fcba29793682401d5c7be5", - "assets/build/ba_data/textures/bonesIcon.pvr": "https://files.ballistica.net/cache/ba1/1a/30/7569bf2de2c9850dde52d6789fc3", - "assets/build/ba_data/textures/bonesIconColorMask.dds": "https://files.ballistica.net/cache/ba1/7e/25/ba43056dc6fe81c715a157d3789d", - "assets/build/ba_data/textures/bonesIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/df/01/1a3673bac25a5dfdac3349b0f947", - "assets/build/ba_data/textures/bonesIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/61/76/93f6a96de783d69eac1d91037d75", - "assets/build/ba_data/textures/bonesIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/be/0d/51a457bc21e776f7be0ddf88dfe5", - "assets/build/ba_data/textures/bonesIcon_preview.png": "https://files.ballistica.net/cache/ba1/ee/a2/2af6c354b4241900afd661af36b0", - "assets/build/ba_data/textures/boxingGlovesColor.dds": "https://files.ballistica.net/cache/ba1/08/63/796e2222816c5594e70b08f669ef", - "assets/build/ba_data/textures/boxingGlovesColor.ktx": "https://files.ballistica.net/cache/ba1/9c/c1/fcb445a18de7bb2adb9c93644840", - "assets/build/ba_data/textures/boxingGlovesColor.pvr": "https://files.ballistica.net/cache/ba1/93/a9/c6d48979dc6c72c70f5c39ccabff", - "assets/build/ba_data/textures/boxingGlovesColor_preview.png": "https://files.ballistica.net/cache/ba1/75/d2/6056b445e3c8da206f4d7574c623", - "assets/build/ba_data/textures/bridgitLevelColor.dds": "https://files.ballistica.net/cache/ba1/91/ab/184a0bbeba1c1bb79d28168709be", - "assets/build/ba_data/textures/bridgitLevelColor.ktx": "https://files.ballistica.net/cache/ba1/30/08/4f5fec91381abc98c3382763cad9", - "assets/build/ba_data/textures/bridgitLevelColor.pvr": "https://files.ballistica.net/cache/ba1/6f/89/65a75daf364c9f3aec77c73bc343", - "assets/build/ba_data/textures/bridgitLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/c9/fd/7e9daa40ebce9e08d2daf07aef49", - "assets/build/ba_data/textures/bridgitPreview.dds": "https://files.ballistica.net/cache/ba1/97/40/31ae0bcfaeec410fc1460cfd4205", - "assets/build/ba_data/textures/bridgitPreview.ktx": "https://files.ballistica.net/cache/ba1/d4/d9/6f936f48e988e335ee1949c2219d", - "assets/build/ba_data/textures/bridgitPreview.pvr": "https://files.ballistica.net/cache/ba1/51/18/4716016fd0d40a0bb94cdac9c627", - "assets/build/ba_data/textures/bridgitPreview_preview.png": "https://files.ballistica.net/cache/ba1/c8/77/01fcaf507ca7c4575a76bff62ff0", - "assets/build/ba_data/textures/bunnyColor.dds": "https://files.ballistica.net/cache/ba1/fc/32/5452de45237f804355667a2584ba", - "assets/build/ba_data/textures/bunnyColor.ktx": "https://files.ballistica.net/cache/ba1/52/88/9b31fa2ce14ef7b98631cb36e769", - "assets/build/ba_data/textures/bunnyColor.pvr": "https://files.ballistica.net/cache/ba1/6b/33/e48393656bbbf7a146d4e246fdc9", - "assets/build/ba_data/textures/bunnyColorMask.dds": "https://files.ballistica.net/cache/ba1/b8/f2/7570c919e1f06c66c7236524d53f", - "assets/build/ba_data/textures/bunnyColorMask.ktx": "https://files.ballistica.net/cache/ba1/f2/bb/05f6af1df50332a10126072e233f", - "assets/build/ba_data/textures/bunnyColorMask.pvr": "https://files.ballistica.net/cache/ba1/db/91/d3ad07cabd17cffa0d9591532477", - "assets/build/ba_data/textures/bunnyColorMask_preview.png": "https://files.ballistica.net/cache/ba1/72/b8/27c36d27c73d19f496f955fa50e2", - "assets/build/ba_data/textures/bunnyColor_preview.png": "https://files.ballistica.net/cache/ba1/cb/f2/861b06f9154cd59a8e20802a0875", - "assets/build/ba_data/textures/bunnyIcon.dds": "https://files.ballistica.net/cache/ba1/ca/2e/0352302668a840b50cf83b7d69b3", - "assets/build/ba_data/textures/bunnyIcon.ktx": "https://files.ballistica.net/cache/ba1/da/92/e1ce1ba464ffc9ce57bfe87dd792", - "assets/build/ba_data/textures/bunnyIcon.pvr": "https://files.ballistica.net/cache/ba1/6e/03/4848e2435ac4f9938a98a7adaf25", - "assets/build/ba_data/textures/bunnyIconColorMask.dds": "https://files.ballistica.net/cache/ba1/d2/5d/7427e249e21960ab7f5e738f833d", - "assets/build/ba_data/textures/bunnyIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/97/5a/daaf489712ad1639b87c8937c43f", - "assets/build/ba_data/textures/bunnyIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/f9/ab/f94dc5a0933e23f8eccc30149b40", - "assets/build/ba_data/textures/bunnyIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/1c/46/c75c9b6ed6e93785d717ba5ee06c", - "assets/build/ba_data/textures/bunnyIcon_preview.png": "https://files.ballistica.net/cache/ba1/1b/cf/847d55686d755b5fd3d313c2224f", - "assets/build/ba_data/textures/buttonBomb.dds": "https://files.ballistica.net/cache/ba1/68/dc/973486e06cdfb2e47c19f2b99034", - "assets/build/ba_data/textures/buttonBomb.ktx": "https://files.ballistica.net/cache/ba1/12/75/f232b20371701bf2dbc4d431fda6", - "assets/build/ba_data/textures/buttonBomb.pvr": "https://files.ballistica.net/cache/ba1/cb/35/aecccd7979dfde1ad253bf2b9909", - "assets/build/ba_data/textures/buttonBomb_preview.png": "https://files.ballistica.net/cache/ba1/dd/11/26cf5767f57eb84d1e1cc2ebb5e0", - "assets/build/ba_data/textures/buttonJump.dds": "https://files.ballistica.net/cache/ba1/d0/25/32aeb74aa51e8f5d54db4c7c4092", - "assets/build/ba_data/textures/buttonJump.ktx": "https://files.ballistica.net/cache/ba1/c8/3d/0b23064948bb8d3139efd8e3dfc6", - "assets/build/ba_data/textures/buttonJump.pvr": "https://files.ballistica.net/cache/ba1/08/e1/8a75d25f2fad28fdff5fe667dc1e", - "assets/build/ba_data/textures/buttonJump_preview.png": "https://files.ballistica.net/cache/ba1/5a/bc/8bae06c91ce1b2708f506a41c87c", - "assets/build/ba_data/textures/buttonPickUp.dds": "https://files.ballistica.net/cache/ba1/f4/60/b28025a41f234c9df6fa0b343e01", - "assets/build/ba_data/textures/buttonPickUp.ktx": "https://files.ballistica.net/cache/ba1/4a/38/fcae119adaea87f5e6552829d771", - "assets/build/ba_data/textures/buttonPickUp.pvr": "https://files.ballistica.net/cache/ba1/cf/78/860d4e1eca6a6c8d3e312cfd70ca", - "assets/build/ba_data/textures/buttonPickUp_preview.png": "https://files.ballistica.net/cache/ba1/43/d2/a33943870c1d1d1842bf6f6af78f", - "assets/build/ba_data/textures/buttonPunch.dds": "https://files.ballistica.net/cache/ba1/e3/0a/346c024a2c075fb195b628630a54", - "assets/build/ba_data/textures/buttonPunch.ktx": "https://files.ballistica.net/cache/ba1/9b/44/cf2c01bb4b05a7e16f69063b17eb", - "assets/build/ba_data/textures/buttonPunch.pvr": "https://files.ballistica.net/cache/ba1/27/88/444dbee243df0c4d65f60c7894ab", - "assets/build/ba_data/textures/buttonPunch_preview.png": "https://files.ballistica.net/cache/ba1/58/86/89718927ebd6386a28a5f825c5c0", - "assets/build/ba_data/textures/buttonSquare.dds": "https://files.ballistica.net/cache/ba1/88/2a/8c5dd1960f8113891fb4156710fc", - "assets/build/ba_data/textures/buttonSquare.ktx": "https://files.ballistica.net/cache/ba1/cf/bc/cfb5c0d801790f60990cb3af5584", - "assets/build/ba_data/textures/buttonSquare.pvr": "https://files.ballistica.net/cache/ba1/e8/87/b49ff6380494c811d9fea334cf1c", - "assets/build/ba_data/textures/buttonSquare_preview.png": "https://files.ballistica.net/cache/ba1/c4/69/eed372e755f72c39f9697739e88a", - "assets/build/ba_data/textures/chTitleChar1.dds": "https://files.ballistica.net/cache/ba1/22/45/151a7fd0032a2d5eab9d47507c91", - "assets/build/ba_data/textures/chTitleChar1.ktx": "https://files.ballistica.net/cache/ba1/80/6c/ffc8f869efcf04f8cf9192e46cf1", - "assets/build/ba_data/textures/chTitleChar1.pvr": "https://files.ballistica.net/cache/ba1/33/b1/24cfd977d307461e38ce26db8ee2", - "assets/build/ba_data/textures/chTitleChar1_preview.png": "https://files.ballistica.net/cache/ba1/c8/dd/809dfb62ff0930d59f5a46f09a62", - "assets/build/ba_data/textures/chTitleChar2.dds": "https://files.ballistica.net/cache/ba1/aa/36/054a732f71305808d5eb1db8fc1c", - "assets/build/ba_data/textures/chTitleChar2.ktx": "https://files.ballistica.net/cache/ba1/4f/b2/18c10b2e15b827ae6e5d0a3be823", - "assets/build/ba_data/textures/chTitleChar2.pvr": "https://files.ballistica.net/cache/ba1/8d/38/f0e6c66d44720be37c209f8968bf", - "assets/build/ba_data/textures/chTitleChar2_preview.png": "https://files.ballistica.net/cache/ba1/d1/4b/52e03c996d49c2ae9d8a64c3bb33", - "assets/build/ba_data/textures/chTitleChar3.dds": "https://files.ballistica.net/cache/ba1/d5/27/15c11d3406b1da68389f80e9a660", - "assets/build/ba_data/textures/chTitleChar3.ktx": "https://files.ballistica.net/cache/ba1/da/d4/64d85d849c0e5f1afbb0f26dd285", - "assets/build/ba_data/textures/chTitleChar3.pvr": "https://files.ballistica.net/cache/ba1/45/9c/f8c45a056658261ae17790d71639", - "assets/build/ba_data/textures/chTitleChar3_preview.png": "https://files.ballistica.net/cache/ba1/20/07/d3f6b95754fe33980ec28b12851a", - "assets/build/ba_data/textures/chTitleChar4.dds": "https://files.ballistica.net/cache/ba1/70/2d/1ec47cbf431d7e55f6b7476fdeda", - "assets/build/ba_data/textures/chTitleChar4.ktx": "https://files.ballistica.net/cache/ba1/70/b6/ec1b4beabc6ed67369b4fefc608b", - "assets/build/ba_data/textures/chTitleChar4.pvr": "https://files.ballistica.net/cache/ba1/a7/78/79fa84a4d68ade6eabf5e73e59d8", - "assets/build/ba_data/textures/chTitleChar4_preview.png": "https://files.ballistica.net/cache/ba1/ca/b9/9be5d4ea038c27ac633cd07fcb07", - "assets/build/ba_data/textures/chTitleChar5.dds": "https://files.ballistica.net/cache/ba1/d1/be/30eb6a51e7add4bf46d7100513ca", - "assets/build/ba_data/textures/chTitleChar5.ktx": "https://files.ballistica.net/cache/ba1/46/6e/9a60af1c802efb6120375c5123c9", - "assets/build/ba_data/textures/chTitleChar5.pvr": "https://files.ballistica.net/cache/ba1/cb/73/234ebe7a23ffbffcd258a27a5826", - "assets/build/ba_data/textures/chTitleChar5_preview.png": "https://files.ballistica.net/cache/ba1/e9/1e/9872f4109d7b1136aef723397ccb", - "assets/build/ba_data/textures/characterIconMask.dds": "https://files.ballistica.net/cache/ba1/e1/6a/d53904143678474043cfa5996766", - "assets/build/ba_data/textures/characterIconMask.ktx": "https://files.ballistica.net/cache/ba1/b9/0b/7caee1441e6b158de60b875ff782", - "assets/build/ba_data/textures/characterIconMask.pvr": "https://files.ballistica.net/cache/ba1/37/a5/66ada64b7ed7dd38fc9a566d1eed", - "assets/build/ba_data/textures/characterIconMask_preview.png": "https://files.ballistica.net/cache/ba1/9c/91/60d86f236b34ef1bc97ab0890176", - "assets/build/ba_data/textures/chestIcon.dds": "https://files.ballistica.net/cache/ba1/70/65/106c130fe12a9e616b222d73be05", - "assets/build/ba_data/textures/chestIcon.ktx": "https://files.ballistica.net/cache/ba1/ee/bd/dda8522f6d80b51dbbed078131cb", - "assets/build/ba_data/textures/chestIcon.pvr": "https://files.ballistica.net/cache/ba1/71/e8/72034ff5819264815dee5e719a9e", - "assets/build/ba_data/textures/chestIconEmpty.dds": "https://files.ballistica.net/cache/ba1/98/be/e7abb99373e15428134bd283a40b", - "assets/build/ba_data/textures/chestIconEmpty.ktx": "https://files.ballistica.net/cache/ba1/56/18/a620612d3157ca4faf298fef45da", - "assets/build/ba_data/textures/chestIconEmpty.pvr": "https://files.ballistica.net/cache/ba1/9a/4d/b80057f50cc173cd91ba960f20c0", - "assets/build/ba_data/textures/chestIconEmpty_preview.png": "https://files.ballistica.net/cache/ba1/e0/58/183d1f1d5cba5c93cb140bb22e81", - "assets/build/ba_data/textures/chestIconMulti.dds": "https://files.ballistica.net/cache/ba1/04/eb/9d7358ff4717d1383e00cda4f2e3", - "assets/build/ba_data/textures/chestIconMulti.ktx": "https://files.ballistica.net/cache/ba1/b6/0d/5763fe330b4057f2a256707bebca", - "assets/build/ba_data/textures/chestIconMulti.pvr": "https://files.ballistica.net/cache/ba1/f8/ee/9e9fe9569e425c2b17952d0e24a2", - "assets/build/ba_data/textures/chestIconMulti_preview.png": "https://files.ballistica.net/cache/ba1/4e/ca/fa54cd541cf1f50f044448ad1119", - "assets/build/ba_data/textures/chestIcon_preview.png": "https://files.ballistica.net/cache/ba1/ae/bf/b9cd9471fad126dd50cb8fae9f90", - "assets/build/ba_data/textures/chestOpenIcon.dds": "https://files.ballistica.net/cache/ba1/56/d1/3b67beeb1f3031847d87e64a47a4", - "assets/build/ba_data/textures/chestOpenIcon.ktx": "https://files.ballistica.net/cache/ba1/41/1e/f0509cabb00dcbc53f32e57b5c34", - "assets/build/ba_data/textures/chestOpenIcon.pvr": "https://files.ballistica.net/cache/ba1/f8/70/8383cf98acc16c251d8514640837", - "assets/build/ba_data/textures/chestOpenIcon_preview.png": "https://files.ballistica.net/cache/ba1/9d/40/b80e6031abd38123d612b1c6a998", - "assets/build/ba_data/textures/circle.dds": "https://files.ballistica.net/cache/ba1/88/a6/c2e3db7c1c6aab2ad9b05d7074b4", - "assets/build/ba_data/textures/circle.ktx": "https://files.ballistica.net/cache/ba1/85/cd/0d9bd306a8f91a9ed661f99529b2", - "assets/build/ba_data/textures/circle.pvr": "https://files.ballistica.net/cache/ba1/3a/a2/d61927bc0770e37848ddd6712876", - "assets/build/ba_data/textures/circleNoAlpha.dds": "https://files.ballistica.net/cache/ba1/90/8b/7c1942e8bd58a3f2f20fc4be7173", - "assets/build/ba_data/textures/circleNoAlpha.ktx": "https://files.ballistica.net/cache/ba1/e4/83/17f847e9f832201929a30f1ea535", - "assets/build/ba_data/textures/circleNoAlpha.pvr": "https://files.ballistica.net/cache/ba1/23/d1/6d053de8f4ab7871c3275920cf18", - "assets/build/ba_data/textures/circleNoAlpha_preview.png": "https://files.ballistica.net/cache/ba1/5c/9d/0ffc70edd5440cea9d5b22810aa2", - "assets/build/ba_data/textures/circleOutline.dds": "https://files.ballistica.net/cache/ba1/a4/c2/e0a5bf011be78509dbef899b8f28", - "assets/build/ba_data/textures/circleOutline.ktx": "https://files.ballistica.net/cache/ba1/ce/91/5fe4f6f2ade655564632f78ddb32", - "assets/build/ba_data/textures/circleOutline.pvr": "https://files.ballistica.net/cache/ba1/df/7a/6de156c06892d833f7ebd264e772", - "assets/build/ba_data/textures/circleOutlineNoAlpha.dds": "https://files.ballistica.net/cache/ba1/60/0e/c7d019fe5cd651e26967c97c4969", - "assets/build/ba_data/textures/circleOutlineNoAlpha.ktx": "https://files.ballistica.net/cache/ba1/3d/19/19c290ceb02d8d41740c864e7793", - "assets/build/ba_data/textures/circleOutlineNoAlpha.pvr": "https://files.ballistica.net/cache/ba1/b0/56/02950e983c0fb55e9bc491b8860c", - "assets/build/ba_data/textures/circleOutlineNoAlpha_preview.png": "https://files.ballistica.net/cache/ba1/fc/b5/7d4368c1ff5655ecd7b22234cf54", - "assets/build/ba_data/textures/circleOutline_preview.png": "https://files.ballistica.net/cache/ba1/51/dc/40e925dfa6f8115626a5712a933a", - "assets/build/ba_data/textures/circleShadow.dds": "https://files.ballistica.net/cache/ba1/8f/1a/a37c8a74227e85c886ee560bf1fb", - "assets/build/ba_data/textures/circleShadow.ktx": "https://files.ballistica.net/cache/ba1/6b/9e/2709a68be7db63c8786a124f1004", - "assets/build/ba_data/textures/circleShadow.pvr": "https://files.ballistica.net/cache/ba1/1c/2a/e0002a4fbcdf6a559b8478090c8e", - "assets/build/ba_data/textures/circleShadow_preview.png": "https://files.ballistica.net/cache/ba1/21/d3/b7d17f7a93166e1cf030e93b79e7", - "assets/build/ba_data/textures/circleZigZag.dds": "https://files.ballistica.net/cache/ba1/74/02/3be9104333361a787a9817d6039f", - "assets/build/ba_data/textures/circleZigZag.ktx": "https://files.ballistica.net/cache/ba1/bc/0d/9bcb6d7b2f55057eab96e68d9da2", - "assets/build/ba_data/textures/circleZigZag.pvr": "https://files.ballistica.net/cache/ba1/1d/67/3d23ea4a8cca8bd4e0dd53eb1bad", - "assets/build/ba_data/textures/circleZigZag_preview.png": "https://files.ballistica.net/cache/ba1/92/f5/ff661dc2d999740c9f9f7db9f9ef", - "assets/build/ba_data/textures/circle_preview.png": "https://files.ballistica.net/cache/ba1/4f/ef/cd527a18eddcfe4fcd6eb066f6ba", - "assets/build/ba_data/textures/coin.dds": "https://files.ballistica.net/cache/ba1/d7/64/953b223ef1ffb051dc94271872d4", - "assets/build/ba_data/textures/coin.ktx": "https://files.ballistica.net/cache/ba1/6a/a5/2807ff07c8c50a48e0dc02815a43", - "assets/build/ba_data/textures/coin.pvr": "https://files.ballistica.net/cache/ba1/d0/24/663d15c1aeeb47729977ccdfb0c1", - "assets/build/ba_data/textures/coin_preview.png": "https://files.ballistica.net/cache/ba1/d3/d1/c5d202b1d50c91739d9063308a32", - "assets/build/ba_data/textures/controllerIcon.dds": "https://files.ballistica.net/cache/ba1/bf/c3/49b705d1b56af9d1233170b812f3", - "assets/build/ba_data/textures/controllerIcon.ktx": "https://files.ballistica.net/cache/ba1/14/16/a15ad29481944559f81f54fdfe20", - "assets/build/ba_data/textures/controllerIcon.pvr": "https://files.ballistica.net/cache/ba1/fe/01/befdbde27b052c35d3b12d80e80f", - "assets/build/ba_data/textures/controllerIcon_preview.png": "https://files.ballistica.net/cache/ba1/59/a5/1465197ee1267ba0e0b25c43d80f", - "assets/build/ba_data/textures/courtyardLevelColor.dds": "https://files.ballistica.net/cache/ba1/b2/e1/fb40e54fac2969cbaa71682af9ce", - "assets/build/ba_data/textures/courtyardLevelColor.ktx": "https://files.ballistica.net/cache/ba1/bd/56/4b6d7c0144fc37f99f35c52ab009", - "assets/build/ba_data/textures/courtyardLevelColor.pvr": "https://files.ballistica.net/cache/ba1/77/97/bf195499e7e18777f02bb87061d7", - "assets/build/ba_data/textures/courtyardLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/91/bf/617c0d77529aa079200fdf75173d", - "assets/build/ba_data/textures/courtyardPreview.dds": "https://files.ballistica.net/cache/ba1/cf/c7/5e5a25f7b6b1c0840d66d203e030", - "assets/build/ba_data/textures/courtyardPreview.ktx": "https://files.ballistica.net/cache/ba1/58/1c/ceeccf17dd5f3f1beb9af1ab720a", - "assets/build/ba_data/textures/courtyardPreview.pvr": "https://files.ballistica.net/cache/ba1/a4/e8/9f1afe13ee2ebb9f6754a5f00f26", - "assets/build/ba_data/textures/courtyardPreview_preview.png": "https://files.ballistica.net/cache/ba1/cb/40/06faf314dd35177a6d0e2a041bb7", - "assets/build/ba_data/textures/cowboyColor.dds": "https://files.ballistica.net/cache/ba1/a4/49/f004e03fe36543d89dd8d1a6ed7f", - "assets/build/ba_data/textures/cowboyColor.ktx": "https://files.ballistica.net/cache/ba1/00/30/f2ae84970bd0697725b821f2f152", - "assets/build/ba_data/textures/cowboyColor.pvr": "https://files.ballistica.net/cache/ba1/45/1a/5bbcf702a33fd48e2b9a7016323f", - "assets/build/ba_data/textures/cowboyColorMask.dds": "https://files.ballistica.net/cache/ba1/52/2f/874896a060d55f6498656cb8ba1d", - "assets/build/ba_data/textures/cowboyColorMask.ktx": "https://files.ballistica.net/cache/ba1/51/68/9145727dc989afded72318e673c7", - "assets/build/ba_data/textures/cowboyColorMask.pvr": "https://files.ballistica.net/cache/ba1/69/44/41ab993ebfc0a1aa32e431582fb4", - "assets/build/ba_data/textures/cowboyColorMask_preview.png": "https://files.ballistica.net/cache/ba1/93/43/7bb7d6df6b89daa507ead29d4f2a", - "assets/build/ba_data/textures/cowboyColor_preview.png": "https://files.ballistica.net/cache/ba1/47/7d/976c046c643e65797b217b43f32e", - "assets/build/ba_data/textures/cowboyIcon.dds": "https://files.ballistica.net/cache/ba1/24/26/65f2ba80626ab6e3622e4056e663", - "assets/build/ba_data/textures/cowboyIcon.ktx": "https://files.ballistica.net/cache/ba1/53/27/f3c3a51c019bb969e62b34b178c0", - "assets/build/ba_data/textures/cowboyIcon.pvr": "https://files.ballistica.net/cache/ba1/ec/34/245e858ff1cb4638da9def9d7fe9", - "assets/build/ba_data/textures/cowboyIconColorMask.dds": "https://files.ballistica.net/cache/ba1/06/da/5179411aee8509904104925d1958", - "assets/build/ba_data/textures/cowboyIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/5d/78/dfd36d09ea5e8d0901345ef478e9", - "assets/build/ba_data/textures/cowboyIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/b9/bf/be1704729e7714781060d58d65c2", - "assets/build/ba_data/textures/cowboyIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/ca/e7/0bc54a6b3fd60923c951fc97d790", - "assets/build/ba_data/textures/cowboyIcon_preview.png": "https://files.ballistica.net/cache/ba1/1c/67/f2c47c042d89c220e78e7ebce5ed", - "assets/build/ba_data/textures/cragCastleLevelColor.dds": "https://files.ballistica.net/cache/ba1/fe/9a/3d8efc6b02fe3f071b832d41dacb", - "assets/build/ba_data/textures/cragCastleLevelColor.ktx": "https://files.ballistica.net/cache/ba1/ea/44/3b99b25e09c05fc6e85583866694", - "assets/build/ba_data/textures/cragCastleLevelColor.pvr": "https://files.ballistica.net/cache/ba1/1e/9a/9b5125e165f5ebaecc8f1934a2c1", - "assets/build/ba_data/textures/cragCastleLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/b4/6c/6b8f525be822459dd4bd6548b2a8", - "assets/build/ba_data/textures/cragCastlePreview.dds": "https://files.ballistica.net/cache/ba1/fb/22/e6c8a874a13b94a688ba5421141a", - "assets/build/ba_data/textures/cragCastlePreview.ktx": "https://files.ballistica.net/cache/ba1/0c/b2/890cb8167c41cd2c220daa3db7a9", - "assets/build/ba_data/textures/cragCastlePreview.pvr": "https://files.ballistica.net/cache/ba1/c9/61/1c57e31b464b26c5e6c125d38944", - "assets/build/ba_data/textures/cragCastlePreview_preview.png": "https://files.ballistica.net/cache/ba1/b1/b2/d49537dbba63c3cbc25b3989a2e2", - "assets/build/ba_data/textures/crossOut.dds": "https://files.ballistica.net/cache/ba1/66/60/a08c6f11994af6dd12c14c5d7ced", - "assets/build/ba_data/textures/crossOut.ktx": "https://files.ballistica.net/cache/ba1/0a/65/af269970081b96379de81841be5b", - "assets/build/ba_data/textures/crossOut.pvr": "https://files.ballistica.net/cache/ba1/b5/5f/704daeb25b1c9674e06e470fd139", - "assets/build/ba_data/textures/crossOutMask.dds": "https://files.ballistica.net/cache/ba1/23/13/c7ab25d561fd29831f536f9fb7de", - "assets/build/ba_data/textures/crossOutMask.ktx": "https://files.ballistica.net/cache/ba1/1a/af/3b975e21848895d49c4490f32e65", - "assets/build/ba_data/textures/crossOutMask.pvr": "https://files.ballistica.net/cache/ba1/6d/d7/9c73928b7957aa74115eb3b14369", - "assets/build/ba_data/textures/crossOutMask_preview.png": "https://files.ballistica.net/cache/ba1/be/e9/5a87e37369a3d84df670c192bdb7", - "assets/build/ba_data/textures/crossOut_preview.png": "https://files.ballistica.net/cache/ba1/0c/50/367904317e0437b723ff096bc104", - "assets/build/ba_data/textures/cursor.dds": "https://files.ballistica.net/cache/ba1/29/c5/f9a5623ac3af827041213a7ec713", - "assets/build/ba_data/textures/cursor.ktx": "https://files.ballistica.net/cache/ba1/71/6f/153abc5679aba409e92cec36c9ab", - "assets/build/ba_data/textures/cursor.pvr": "https://files.ballistica.net/cache/ba1/02/9c/301af4494408fcbd81967eb61bd8", - "assets/build/ba_data/textures/cursor_preview.png": "https://files.ballistica.net/cache/ba1/15/4f/b093d49826968d0d7000ee6dfc4b", - "assets/build/ba_data/textures/cuteSpaz.dds": "https://files.ballistica.net/cache/ba1/23/4a/532f8bf15d5a0fb587c9a3b90242", - "assets/build/ba_data/textures/cuteSpaz.ktx": "https://files.ballistica.net/cache/ba1/9a/65/5bfc1d1ef2bb7ac9b767e70d6015", - "assets/build/ba_data/textures/cuteSpaz.pvr": "https://files.ballistica.net/cache/ba1/ff/ec/9172f64f96803393de8da58f7566", - "assets/build/ba_data/textures/cuteSpaz_preview.png": "https://files.ballistica.net/cache/ba1/e9/74/364547fef623e7bbbbc8a92a9efa", - "assets/build/ba_data/textures/cyborgColor.dds": "https://files.ballistica.net/cache/ba1/93/d4/75c140eff1c3cc45e1766fc0e95c", - "assets/build/ba_data/textures/cyborgColor.ktx": "https://files.ballistica.net/cache/ba1/d0/d6/e611760948207bebc5cec40721ee", - "assets/build/ba_data/textures/cyborgColor.pvr": "https://files.ballistica.net/cache/ba1/3b/b4/14499682862c916ba7e6df183445", - "assets/build/ba_data/textures/cyborgColorMask.dds": "https://files.ballistica.net/cache/ba1/34/e0/24881a8cabf3b48f1e68635b0096", - "assets/build/ba_data/textures/cyborgColorMask.ktx": "https://files.ballistica.net/cache/ba1/09/49/64a4b4bd11d7f43ee3e3b4557ccb", - "assets/build/ba_data/textures/cyborgColorMask.pvr": "https://files.ballistica.net/cache/ba1/aa/8f/2017c219d46766b0a8a58b12ef80", - "assets/build/ba_data/textures/cyborgColorMask_preview.png": "https://files.ballistica.net/cache/ba1/b2/c8/1ca1bb73d93dd93169977aef26eb", - "assets/build/ba_data/textures/cyborgColor_preview.png": "https://files.ballistica.net/cache/ba1/23/68/de2999bc4db759030c77dd372e23", - "assets/build/ba_data/textures/cyborgIcon.dds": "https://files.ballistica.net/cache/ba1/5a/55/c63bdf0b63c76da7b387b4bebecb", - "assets/build/ba_data/textures/cyborgIcon.ktx": "https://files.ballistica.net/cache/ba1/de/ab/4d5b2163047d3fb9b05d43c9183d", - "assets/build/ba_data/textures/cyborgIcon.pvr": "https://files.ballistica.net/cache/ba1/0c/7b/b47a4bfbbfd8607ea06375963485", - "assets/build/ba_data/textures/cyborgIconColorMask.dds": "https://files.ballistica.net/cache/ba1/ad/20/71a5de426d081b99c1abe672474a", - "assets/build/ba_data/textures/cyborgIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/20/95/85096b550149fb17500c673a9589", - "assets/build/ba_data/textures/cyborgIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/1f/70/35e302e1a91d78ab23cb3e936bde", - "assets/build/ba_data/textures/cyborgIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/09/bb/e6563f614b2cda0ca5da89b80b2a", - "assets/build/ba_data/textures/cyborgIcon_preview.png": "https://files.ballistica.net/cache/ba1/4e/af/0d54cad1089f73c91ed766519d77", - "assets/build/ba_data/textures/discordLogo.dds": "https://files.ballistica.net/cache/ba1/74/50/01043d907a39a088c30a8eaeab46", - "assets/build/ba_data/textures/discordLogo.ktx": "https://files.ballistica.net/cache/ba1/a2/ad/24c11330f25cd763640872fbd16e", - "assets/build/ba_data/textures/discordLogo.pvr": "https://files.ballistica.net/cache/ba1/2b/41/7b2e332fbfe4c74cc8c76be16fd0", - "assets/build/ba_data/textures/discordLogo_preview.png": "https://files.ballistica.net/cache/ba1/ad/88/2fc2f22f8ee9925df99dbc47d315", - "assets/build/ba_data/textures/doomShroomBGColor.dds": "https://files.ballistica.net/cache/ba1/5c/2b/88deea7de0cf4b7e3250ca76bcc1", - "assets/build/ba_data/textures/doomShroomBGColor.ktx": "https://files.ballistica.net/cache/ba1/db/35/7f5884df9de36467179b45e8e241", - "assets/build/ba_data/textures/doomShroomBGColor.pvr": "https://files.ballistica.net/cache/ba1/65/5d/fc508d48f3703e91cfbfce0187ed", - "assets/build/ba_data/textures/doomShroomBGColor_preview.png": "https://files.ballistica.net/cache/ba1/b5/7e/995e3c648eb68d4c72c4e50be7e6", - "assets/build/ba_data/textures/doomShroomLevelColor.dds": "https://files.ballistica.net/cache/ba1/cd/1e/f297f1c37249810e7aaffca591a4", - "assets/build/ba_data/textures/doomShroomLevelColor.ktx": "https://files.ballistica.net/cache/ba1/6c/96/d887b3d3cf79d2264ad2e601193d", - "assets/build/ba_data/textures/doomShroomLevelColor.pvr": "https://files.ballistica.net/cache/ba1/e2/15/8303117a33a96a61376dad6f5e9d", - "assets/build/ba_data/textures/doomShroomLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/61/cf/327dd838ebab6875b92ee28c3bcd", - "assets/build/ba_data/textures/doomShroomPreview.dds": "https://files.ballistica.net/cache/ba1/d7/c7/888bb15afe907f3a37006e76b3af", - "assets/build/ba_data/textures/doomShroomPreview.ktx": "https://files.ballistica.net/cache/ba1/f6/3c/20abd773a8502a0954d62c0917c7", - "assets/build/ba_data/textures/doomShroomPreview.pvr": "https://files.ballistica.net/cache/ba1/d3/92/aca35e87cb29f4da683e88859a21", - "assets/build/ba_data/textures/doomShroomPreview_preview.png": "https://files.ballistica.net/cache/ba1/19/85/7ec1b3b28c5bc564fd0a106d2423", - "assets/build/ba_data/textures/downButton.dds": "https://files.ballistica.net/cache/ba1/d2/0b/a76ea6cde9bce99c8e1185e97ace", - "assets/build/ba_data/textures/downButton.ktx": "https://files.ballistica.net/cache/ba1/e3/15/138ffe770433ea52179453fc6d27", - "assets/build/ba_data/textures/downButton.pvr": "https://files.ballistica.net/cache/ba1/3d/1b/8e5d5ded931ecedc2b8ccd8fe950", - "assets/build/ba_data/textures/downButton_preview.png": "https://files.ballistica.net/cache/ba1/8f/24/196b63faa311f316a78ad4409c31", - "assets/build/ba_data/textures/egg1.dds": "https://files.ballistica.net/cache/ba1/c7/a4/23e89269b6549f8e29cd2c20ef22", - "assets/build/ba_data/textures/egg1.ktx": "https://files.ballistica.net/cache/ba1/97/a4/213e80c093c9dc7ba34598d2a109", - "assets/build/ba_data/textures/egg1.pvr": "https://files.ballistica.net/cache/ba1/4f/39/b60d509019ce0c87bdd16a0f9e02", - "assets/build/ba_data/textures/egg1_preview.png": "https://files.ballistica.net/cache/ba1/d6/e4/6e902373b3f408275dd118436fe1", - "assets/build/ba_data/textures/egg2.dds": "https://files.ballistica.net/cache/ba1/6e/47/a29102f4cbfefe5a9a4965465066", - "assets/build/ba_data/textures/egg2.ktx": "https://files.ballistica.net/cache/ba1/25/73/c41cf45274182d05f0efe4dbeb45", - "assets/build/ba_data/textures/egg2.pvr": "https://files.ballistica.net/cache/ba1/7c/fb/6640d57563b108e1a8a172ad87d1", - "assets/build/ba_data/textures/egg2_preview.png": "https://files.ballistica.net/cache/ba1/8b/f9/5f105ad7952e151f79815e098952", - "assets/build/ba_data/textures/egg3.dds": "https://files.ballistica.net/cache/ba1/a5/ea/902d7c1fa4f7b81e8e70b28fb02f", - "assets/build/ba_data/textures/egg3.ktx": "https://files.ballistica.net/cache/ba1/6f/0a/5fafe7481e012ef9138dfe3ca73f", - "assets/build/ba_data/textures/egg3.pvr": "https://files.ballistica.net/cache/ba1/5e/8e/49cf5ee2892c41e2250ff36a1b5d", - "assets/build/ba_data/textures/egg3_preview.png": "https://files.ballistica.net/cache/ba1/e5/6f/0d483ac7302c91b0db6c3ea700b8", - "assets/build/ba_data/textures/egg4.dds": "https://files.ballistica.net/cache/ba1/ba/c7/588dd63c114ae666302f3b3822b4", - "assets/build/ba_data/textures/egg4.ktx": "https://files.ballistica.net/cache/ba1/88/b3/9cb3c996af3da387157f8173605c", - "assets/build/ba_data/textures/egg4.pvr": "https://files.ballistica.net/cache/ba1/27/37/6c99967d3c687d504b3cd7f89db1", - "assets/build/ba_data/textures/egg4_preview.png": "https://files.ballistica.net/cache/ba1/76/2a/5e6c70da2b54b037b613c48a5451", - "assets/build/ba_data/textures/eggTex1.dds": "https://files.ballistica.net/cache/ba1/3b/b1/3dc4330c30c22ca811e4114ec3ab", - "assets/build/ba_data/textures/eggTex1.ktx": "https://files.ballistica.net/cache/ba1/85/7c/659f38191c02a46eeb3cbf1661a9", - "assets/build/ba_data/textures/eggTex1.pvr": "https://files.ballistica.net/cache/ba1/8a/79/8f94e389565f68b8ddf5a0967935", - "assets/build/ba_data/textures/eggTex1_preview.png": "https://files.ballistica.net/cache/ba1/e9/73/f9b572f6afada4767164594db56d", - "assets/build/ba_data/textures/eggTex2.dds": "https://files.ballistica.net/cache/ba1/ec/1a/5195c5a5d6b5e33d388653c3c47b", - "assets/build/ba_data/textures/eggTex2.ktx": "https://files.ballistica.net/cache/ba1/ce/10/d8b82b76527bd6957fc4f2a2c118", - "assets/build/ba_data/textures/eggTex2.pvr": "https://files.ballistica.net/cache/ba1/ea/57/de7f1aeef07079b8f25741d20a95", - "assets/build/ba_data/textures/eggTex2_preview.png": "https://files.ballistica.net/cache/ba1/83/fb/e80aaf8ed938c6ce7323d1daecfc", - "assets/build/ba_data/textures/eggTex3.dds": "https://files.ballistica.net/cache/ba1/1b/7a/64e2f7402909907d7e38f37bd810", - "assets/build/ba_data/textures/eggTex3.ktx": "https://files.ballistica.net/cache/ba1/8a/72/983df85568f8bcdc3b6220c60d0c", - "assets/build/ba_data/textures/eggTex3.pvr": "https://files.ballistica.net/cache/ba1/fe/be/bb6a99c72bc58a45304788fa7c11", - "assets/build/ba_data/textures/eggTex3_preview.png": "https://files.ballistica.net/cache/ba1/04/05/c23e2aadf5e86edfbe2962b8d717", - "assets/build/ba_data/textures/empty.dds": "https://files.ballistica.net/cache/ba1/a1/6b/ac21f65c7c8e8f3492005a6f2108", - "assets/build/ba_data/textures/empty.ktx": "https://files.ballistica.net/cache/ba1/1c/1a/c944b4615540bebbd2337f37a36e", - "assets/build/ba_data/textures/empty.pvr": "https://files.ballistica.net/cache/ba1/e1/85/0e5d594e57336cf14b4914ebb37c", - "assets/build/ba_data/textures/empty_preview.png": "https://files.ballistica.net/cache/ba1/27/1d/936488a18c78f387f2f4ec562545", - "assets/build/ba_data/textures/explosion.dds": "https://files.ballistica.net/cache/ba1/e7/95/a6d20541c8ffff02583799ab77ba", - "assets/build/ba_data/textures/explosion.ktx": "https://files.ballistica.net/cache/ba1/72/e7/7db06c8ab6f0dd8dfc48162824b1", - "assets/build/ba_data/textures/explosion.pvr": "https://files.ballistica.net/cache/ba1/38/42/e4de72086e36187dcb5e4005f4e9", - "assets/build/ba_data/textures/explosion_preview.png": "https://files.ballistica.net/cache/ba1/6f/01/8a6cf54d2e9b24c87c00ea2ca9c3", - "assets/build/ba_data/textures/eyeColor.dds": "https://files.ballistica.net/cache/ba1/27/85/c9d65823523146dc7b06595d9574", - "assets/build/ba_data/textures/eyeColor.ktx": "https://files.ballistica.net/cache/ba1/7b/af/27f0a7628a0ff7ebc5bfe40df515", - "assets/build/ba_data/textures/eyeColor.pvr": "https://files.ballistica.net/cache/ba1/ee/a1/fd29f544917a08e6630c35bb19ac", - "assets/build/ba_data/textures/eyeColorTintMask.dds": "https://files.ballistica.net/cache/ba1/97/0a/59c0b1f11f974f926eda806c2312", - "assets/build/ba_data/textures/eyeColorTintMask.ktx": "https://files.ballistica.net/cache/ba1/5c/a2/af8bceabb21f343d932c4a5efa34", - "assets/build/ba_data/textures/eyeColorTintMask.pvr": "https://files.ballistica.net/cache/ba1/06/85/3069e8a3d29b7d252b9c98b93008", - "assets/build/ba_data/textures/eyeColorTintMask_preview.png": "https://files.ballistica.net/cache/ba1/d4/ee/8888120c27e400b67d8e3fce89c2", - "assets/build/ba_data/textures/eyeColor_preview.png": "https://files.ballistica.net/cache/ba1/0c/f2/010df146e4efb60f5073928e6116", - "assets/build/ba_data/textures/file.dds": "https://files.ballistica.net/cache/ba1/06/d9/c2f4e94248ed8ef8a5d03f1228e6", - "assets/build/ba_data/textures/file.ktx": "https://files.ballistica.net/cache/ba1/d5/f6/a80ff58c84435b33a05a1e61016e", - "assets/build/ba_data/textures/file.pvr": "https://files.ballistica.net/cache/ba1/5f/25/e6079cc54364d95d6101fc74bdba", - "assets/build/ba_data/textures/file_preview.png": "https://files.ballistica.net/cache/ba1/ef/c6/71a8f503449908b15a40ce3b5b03", - "assets/build/ba_data/textures/flagColor.dds": "https://files.ballistica.net/cache/ba1/4a/ce/2682d3720bff14ec43bdfcaca346", - "assets/build/ba_data/textures/flagColor.ktx": "https://files.ballistica.net/cache/ba1/81/3c/f16914e9d68d64ce59a1f7681bff", - "assets/build/ba_data/textures/flagColor.pvr": "https://files.ballistica.net/cache/ba1/e0/b0/06fd752a0f3563252d6dee64dcb3", - "assets/build/ba_data/textures/flagColor_preview.png": "https://files.ballistica.net/cache/ba1/0b/33/50db4a9487b0ef8d65f2628776e5", - "assets/build/ba_data/textures/flagPoleColor.dds": "https://files.ballistica.net/cache/ba1/22/e0/43555f1a8a4bff7088c33ccd179e", - "assets/build/ba_data/textures/flagPoleColor.ktx": "https://files.ballistica.net/cache/ba1/0f/56/7d16d9b2af848d004da2012c2cf7", - "assets/build/ba_data/textures/flagPoleColor.pvr": "https://files.ballistica.net/cache/ba1/7b/63/e163c96ff68bb242e8b9ef339711", - "assets/build/ba_data/textures/flagPoleColor_preview.png": "https://files.ballistica.net/cache/ba1/e0/e4/6c41b0bfd6796344ba60d0bdf189", - "assets/build/ba_data/textures/folder.dds": "https://files.ballistica.net/cache/ba1/12/f8/c4c6f047ac5b4e27b7ef6b102f2c", - "assets/build/ba_data/textures/folder.ktx": "https://files.ballistica.net/cache/ba1/80/26/60783c3b9dd188cd8dad726e520f", - "assets/build/ba_data/textures/folder.pvr": "https://files.ballistica.net/cache/ba1/51/8f/d8613fde2a95ce07b51e562eb65f", - "assets/build/ba_data/textures/folder_preview.png": "https://files.ballistica.net/cache/ba1/d4/4a/fd7f70b9963b51a317a934b26475", - "assets/build/ba_data/textures/fontBig.dds": "https://files.ballistica.net/cache/ba1/aa/a3/ff04a31180583dabce65d9dde806", - "assets/build/ba_data/textures/fontBig.ktx": "https://files.ballistica.net/cache/ba1/77/07/c97451e120a6a63cfc8425e43ef7", - "assets/build/ba_data/textures/fontBig.pvr": "https://files.ballistica.net/cache/ba1/20/89/beb82c386bc52a5c8d3cee142157", - "assets/build/ba_data/textures/fontBig_preview.png": "https://files.ballistica.net/cache/ba1/57/70/b08333a6691253f57f099bc80537", - "assets/build/ba_data/textures/fontExtras.dds": "https://files.ballistica.net/cache/ba1/57/19/c260d3eac3526c2a908e7d761520", - "assets/build/ba_data/textures/fontExtras.ktx": "https://files.ballistica.net/cache/ba1/43/23/d4af9f534f4f4be439df7f3de1bb", - "assets/build/ba_data/textures/fontExtras.pvr": "https://files.ballistica.net/cache/ba1/e3/68/8f7d535c92171f84365b6f87e0db", - "assets/build/ba_data/textures/fontExtras2.dds": "https://files.ballistica.net/cache/ba1/ed/af/090eaf3565b507f7bc3f2e762ea5", - "assets/build/ba_data/textures/fontExtras2.ktx": "https://files.ballistica.net/cache/ba1/75/a6/82de83bd315ec87d8d10e4207ed4", - "assets/build/ba_data/textures/fontExtras2.pvr": "https://files.ballistica.net/cache/ba1/28/c4/622d70995847920286d904d5e224", - "assets/build/ba_data/textures/fontExtras2_preview.png": "https://files.ballistica.net/cache/ba1/8a/e2/78355c9c236bb04737bfa6cf2f57", - "assets/build/ba_data/textures/fontExtras3.dds": "https://files.ballistica.net/cache/ba1/09/42/6d3701e1371cbde3586415de9b1a", - "assets/build/ba_data/textures/fontExtras3.ktx": "https://files.ballistica.net/cache/ba1/e4/b7/bd74b3f7e1c0d36524d979531557", - "assets/build/ba_data/textures/fontExtras3.pvr": "https://files.ballistica.net/cache/ba1/1a/bb/1523d014c9179f4c1774699ac5c5", - "assets/build/ba_data/textures/fontExtras3_preview.png": "https://files.ballistica.net/cache/ba1/42/da/9519da17994c22d3558a3cdddd6f", - "assets/build/ba_data/textures/fontExtras4.dds": "https://files.ballistica.net/cache/ba1/56/48/053ec471cc9628c24480d5c70b8b", - "assets/build/ba_data/textures/fontExtras4.ktx": "https://files.ballistica.net/cache/ba1/07/be/fe8e0dc0109813b69b564ccff2b7", - "assets/build/ba_data/textures/fontExtras4.pvr": "https://files.ballistica.net/cache/ba1/54/18/67d0b2bb733df19c7a46797552b0", - "assets/build/ba_data/textures/fontExtras4_preview.png": "https://files.ballistica.net/cache/ba1/2f/6d/0c490d3a8ffacb3f901bf7041bf6", - "assets/build/ba_data/textures/fontExtras_preview.png": "https://files.ballistica.net/cache/ba1/72/44/13f75510e9979f56c78e35f3b983", - "assets/build/ba_data/textures/fontSmall0.dds": "https://files.ballistica.net/cache/ba1/b8/40/2c95a5004a333f0b364dae081d78", - "assets/build/ba_data/textures/fontSmall0.ktx": "https://files.ballistica.net/cache/ba1/ea/7b/7a23fa7856a2712b408869113d3a", - "assets/build/ba_data/textures/fontSmall0.pvr": "https://files.ballistica.net/cache/ba1/03/89/0ceb5a3d36462ccbdc988ff54790", - "assets/build/ba_data/textures/fontSmall0_preview.png": "https://files.ballistica.net/cache/ba1/f8/57/c48097f2ffede6b37e1800300e39", - "assets/build/ba_data/textures/fontSmall1.dds": "https://files.ballistica.net/cache/ba1/68/8b/886ec9e6921e1d3aa40483910846", - "assets/build/ba_data/textures/fontSmall1.ktx": "https://files.ballistica.net/cache/ba1/11/fd/1ad432fba92775bc35d1f4d1cdce", - "assets/build/ba_data/textures/fontSmall1.pvr": "https://files.ballistica.net/cache/ba1/fb/71/d7975c22f30bc1e73c3d684f3db9", - "assets/build/ba_data/textures/fontSmall1_preview.png": "https://files.ballistica.net/cache/ba1/5c/02/997d7b821b12e7c2a3080108d167", - "assets/build/ba_data/textures/fontSmall2.dds": "https://files.ballistica.net/cache/ba1/91/7d/454f884507c70062299d6dfdbf8c", - "assets/build/ba_data/textures/fontSmall2.ktx": "https://files.ballistica.net/cache/ba1/99/81/025ab8448aeb9815d48bf0962ae6", - "assets/build/ba_data/textures/fontSmall2.pvr": "https://files.ballistica.net/cache/ba1/1d/cd/19232cf351985397c454c8dbdce6", - "assets/build/ba_data/textures/fontSmall2_preview.png": "https://files.ballistica.net/cache/ba1/c1/f1/c2602b14b6ae48e2aedcec058915", - "assets/build/ba_data/textures/fontSmall3.dds": "https://files.ballistica.net/cache/ba1/a0/4f/2f50fef4bf2f7214a07a95fed604", - "assets/build/ba_data/textures/fontSmall3.ktx": "https://files.ballistica.net/cache/ba1/9a/6d/79661bb5a228b818b4b4fb6613db", - "assets/build/ba_data/textures/fontSmall3.pvr": "https://files.ballistica.net/cache/ba1/bf/39/4475817d59fe8abe60dfba413467", - "assets/build/ba_data/textures/fontSmall3_preview.png": "https://files.ballistica.net/cache/ba1/d1/34/ccb65f0c449d485b35c0fbc7c2d5", - "assets/build/ba_data/textures/fontSmall4.dds": "https://files.ballistica.net/cache/ba1/6c/5d/abe0be3c3b06e8b7162285f63985", - "assets/build/ba_data/textures/fontSmall4.ktx": "https://files.ballistica.net/cache/ba1/7e/fe/8123b662f322f5150432607bcd49", - "assets/build/ba_data/textures/fontSmall4.pvr": "https://files.ballistica.net/cache/ba1/4d/68/af39872854f2e9b7805c7f981c00", - "assets/build/ba_data/textures/fontSmall4_preview.png": "https://files.ballistica.net/cache/ba1/fe/70/fd58e9c2acbf7b127be9bbedc365", - "assets/build/ba_data/textures/fontSmall5.dds": "https://files.ballistica.net/cache/ba1/1d/fc/7302cb4e031defd71fdda9bfff2a", - "assets/build/ba_data/textures/fontSmall5.ktx": "https://files.ballistica.net/cache/ba1/17/97/9c1eaaa27d8209e8adcd170d43cf", - "assets/build/ba_data/textures/fontSmall5.pvr": "https://files.ballistica.net/cache/ba1/6e/dc/3b0e6fb9d631e4dbe907e033a820", - "assets/build/ba_data/textures/fontSmall5_preview.png": "https://files.ballistica.net/cache/ba1/40/58/9b7b926f4f41ec7c0db47598f993", - "assets/build/ba_data/textures/fontSmall6.dds": "https://files.ballistica.net/cache/ba1/83/29/0785e9ea14ee2ce0e8eeceebe6c8", - "assets/build/ba_data/textures/fontSmall6.ktx": "https://files.ballistica.net/cache/ba1/61/ab/9d23c00dd382b99c7534e089abc2", - "assets/build/ba_data/textures/fontSmall6.pvr": "https://files.ballistica.net/cache/ba1/2f/79/e9836b4d8451c97c7b4ac2f48ba4", - "assets/build/ba_data/textures/fontSmall6_preview.png": "https://files.ballistica.net/cache/ba1/5d/54/cad076f46b1daa2d85a39959c60b", - "assets/build/ba_data/textures/fontSmall7.dds": "https://files.ballistica.net/cache/ba1/6b/59/f359ca5f1ec9d732d31a534dd413", - "assets/build/ba_data/textures/fontSmall7.ktx": "https://files.ballistica.net/cache/ba1/14/d0/23e59a31103533757dea7ffae94e", - "assets/build/ba_data/textures/fontSmall7.pvr": "https://files.ballistica.net/cache/ba1/48/b5/26321b2643219162a713170ab9b8", - "assets/build/ba_data/textures/fontSmall7_preview.png": "https://files.ballistica.net/cache/ba1/70/56/13b940fb6d0ba48023e7fac6f184", - "assets/build/ba_data/textures/footballStadium.dds": "https://files.ballistica.net/cache/ba1/c6/f3/1906912b8e4d923a0ec975de5e8f", - "assets/build/ba_data/textures/footballStadium.ktx": "https://files.ballistica.net/cache/ba1/00/bf/6b8e37071ad1d6e33f30f1998bed", - "assets/build/ba_data/textures/footballStadium.pvr": "https://files.ballistica.net/cache/ba1/92/69/f2c5577a1de103343ddcb01d6104", - "assets/build/ba_data/textures/footballStadiumPreview.dds": "https://files.ballistica.net/cache/ba1/d9/7f/b42a16567ea5ae5a85faa027e4ce", - "assets/build/ba_data/textures/footballStadiumPreview.ktx": "https://files.ballistica.net/cache/ba1/96/f2/6cf9b95e27aeca2d8be08feedaef", - "assets/build/ba_data/textures/footballStadiumPreview.pvr": "https://files.ballistica.net/cache/ba1/3f/1b/ce5faa6a8678c3b7d0a461c9509b", - "assets/build/ba_data/textures/footballStadiumPreview_preview.png": "https://files.ballistica.net/cache/ba1/a7/c8/393d085e92e185e6c43ab28cd5ee", - "assets/build/ba_data/textures/footballStadium_preview.png": "https://files.ballistica.net/cache/ba1/87/be/77e85341774c0d7cc21fc182714e", - "assets/build/ba_data/textures/frameInset.dds": "https://files.ballistica.net/cache/ba1/f4/83/a99269f57f98b30585ce3568b94a", - "assets/build/ba_data/textures/frameInset.ktx": "https://files.ballistica.net/cache/ba1/61/d7/264cfcdd0d07d67ac90827969caa", - "assets/build/ba_data/textures/frameInset.pvr": "https://files.ballistica.net/cache/ba1/d1/ef/b0dd66aab3c8cf21933dc53f0c8f", - "assets/build/ba_data/textures/frameInset_preview.png": "https://files.ballistica.net/cache/ba1/51/2f/a18329872f0d0b71f0a333c0db81", - "assets/build/ba_data/textures/frostyColor.dds": "https://files.ballistica.net/cache/ba1/6d/c6/2a49a0ec7810e7431c35752d3d37", - "assets/build/ba_data/textures/frostyColor.ktx": "https://files.ballistica.net/cache/ba1/17/6c/fe178ee04831b332ff771bade627", - "assets/build/ba_data/textures/frostyColor.pvr": "https://files.ballistica.net/cache/ba1/f8/fb/83779ec75b8113a47b9368ad6992", - "assets/build/ba_data/textures/frostyColorMask.dds": "https://files.ballistica.net/cache/ba1/7c/a3/3cd619ea33d9e222af6da8ce7ccc", - "assets/build/ba_data/textures/frostyColorMask.ktx": "https://files.ballistica.net/cache/ba1/5d/ff/5a82bf1855c957357d277dec18c7", - "assets/build/ba_data/textures/frostyColorMask.pvr": "https://files.ballistica.net/cache/ba1/d2/e3/5ccf3bc21b48180ef890cfd1ebab", - "assets/build/ba_data/textures/frostyColorMask_preview.png": "https://files.ballistica.net/cache/ba1/44/6f/795803f91fbf1628e708a6a293a4", - "assets/build/ba_data/textures/frostyColor_preview.png": "https://files.ballistica.net/cache/ba1/cd/3c/f7e49e7cf8164f754c2384f35e55", - "assets/build/ba_data/textures/frostyIcon.dds": "https://files.ballistica.net/cache/ba1/17/76/45c5a0a3d416259aab598bad0303", - "assets/build/ba_data/textures/frostyIcon.ktx": "https://files.ballistica.net/cache/ba1/74/b4/d498eb65a6a3b562e9eb6195131b", - "assets/build/ba_data/textures/frostyIcon.pvr": "https://files.ballistica.net/cache/ba1/a2/a6/589107d7ab94e8ba9dec5895ba6d", - "assets/build/ba_data/textures/frostyIconColorMask.dds": "https://files.ballistica.net/cache/ba1/af/d5/5421823e52ef7bb86d1e24a610c4", - "assets/build/ba_data/textures/frostyIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/10/a1/a114299df41eeb379db1b61182db", - "assets/build/ba_data/textures/frostyIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/8e/98/89e07ef5493074a2c4cbb7aa1592", - "assets/build/ba_data/textures/frostyIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/ef/77/6b17bc9b8d8d3fa37760ed4d96ac", - "assets/build/ba_data/textures/frostyIcon_preview.png": "https://files.ballistica.net/cache/ba1/3e/dd/9cc6d8f3f494b586237b050a4e06", - "assets/build/ba_data/textures/fuse.dds": "https://files.ballistica.net/cache/ba1/9a/72/5eb4b3235d38ec3f69c61fdb0445", - "assets/build/ba_data/textures/fuse.ktx": "https://files.ballistica.net/cache/ba1/49/ea/71c5c7dcbdcb213c036460d59117", - "assets/build/ba_data/textures/fuse.pvr": "https://files.ballistica.net/cache/ba1/48/3a/710945756ee605974820d4c98e49", - "assets/build/ba_data/textures/fuse_preview.png": "https://files.ballistica.net/cache/ba1/f1/69/975f7d19d7db9a01d78ecb33bfe2", - "assets/build/ba_data/textures/gameCenterIcon.dds": "https://files.ballistica.net/cache/ba1/8d/a8/e6d4fd229fd8248c6c3f0ba56e66", - "assets/build/ba_data/textures/gameCenterIcon.ktx": "https://files.ballistica.net/cache/ba1/7b/76/d5ec355de01b49cacd32621c84d3", - "assets/build/ba_data/textures/gameCenterIcon.pvr": "https://files.ballistica.net/cache/ba1/29/9d/2da58debae87bd7f933c862f70ad", - "assets/build/ba_data/textures/gameCenterIcon_preview.png": "https://files.ballistica.net/cache/ba1/16/d2/bc4424ea7d585521272629343d91", - "assets/build/ba_data/textures/gameCircleIcon.dds": "https://files.ballistica.net/cache/ba1/dd/74/99a4fedcc41cbb4c43ca3f619c80", - "assets/build/ba_data/textures/gameCircleIcon.ktx": "https://files.ballistica.net/cache/ba1/b3/2a/a5101200951b4349651bf32fa132", - "assets/build/ba_data/textures/gameCircleIcon.pvr": "https://files.ballistica.net/cache/ba1/ff/32/be54b0eb46c48665edb0669dcf0e", - "assets/build/ba_data/textures/gameCircleIcon_preview.png": "https://files.ballistica.net/cache/ba1/27/21/ddc0b407d57dd98170cbe4c54538", - "assets/build/ba_data/textures/githubLogo.dds": "https://files.ballistica.net/cache/ba1/aa/33/c411e5325264a7594eccee7a7f0f", - "assets/build/ba_data/textures/githubLogo.ktx": "https://files.ballistica.net/cache/ba1/48/0a/1244a43926b6efff0e2a6f316649", - "assets/build/ba_data/textures/githubLogo.pvr": "https://files.ballistica.net/cache/ba1/46/7a/d19365277cbc21601a023f38876b", - "assets/build/ba_data/textures/githubLogo_preview.png": "https://files.ballistica.net/cache/ba1/3e/03/0b2b3dfc5e8a779f15caa83b5240", - "assets/build/ba_data/textures/gladiatorColor.dds": "https://files.ballistica.net/cache/ba1/a8/6a/2b8eb1e0f4c15993e98be5395e4c", - "assets/build/ba_data/textures/gladiatorColor.ktx": "https://files.ballistica.net/cache/ba1/20/13/e491823b8cc38a52400f9d74a209", - "assets/build/ba_data/textures/gladiatorColor.pvr": "https://files.ballistica.net/cache/ba1/14/60/1d4a45b0036538368348613ad01f", - "assets/build/ba_data/textures/gladiatorColorMask.dds": "https://files.ballistica.net/cache/ba1/d2/ac/02d7c9a4320ed6a8b416d48f2665", - "assets/build/ba_data/textures/gladiatorColorMask.ktx": "https://files.ballistica.net/cache/ba1/1e/ae/f91222e92e938c0fc8be66103cb5", - "assets/build/ba_data/textures/gladiatorColorMask.pvr": "https://files.ballistica.net/cache/ba1/46/18/4ad8171ff791a86b03160648afcf", - "assets/build/ba_data/textures/gladiatorColorMask_preview.png": "https://files.ballistica.net/cache/ba1/87/45/0621a7a40782a938a8f66f8985ae", - "assets/build/ba_data/textures/gladiatorColor_preview.png": "https://files.ballistica.net/cache/ba1/31/d7/818975e4c91ab431f8ec4a74b6c7", - "assets/build/ba_data/textures/gladiatorIcon.dds": "https://files.ballistica.net/cache/ba1/b1/96/58005519916a97f4b836910de650", - "assets/build/ba_data/textures/gladiatorIcon.ktx": "https://files.ballistica.net/cache/ba1/2f/b2/e027d617c427b83adff1f96b1b86", - "assets/build/ba_data/textures/gladiatorIcon.pvr": "https://files.ballistica.net/cache/ba1/3d/bf/e2544a50428465b94f17b3ceaa83", - "assets/build/ba_data/textures/gladiatorIconColorMask.dds": "https://files.ballistica.net/cache/ba1/94/36/28392d1044d62faded6e46373dae", - "assets/build/ba_data/textures/gladiatorIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/a1/90/22783b8b7bc60e3378f494b4573e", - "assets/build/ba_data/textures/gladiatorIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/66/62/e09f708fd6c7dd771764cc5f52f6", - "assets/build/ba_data/textures/gladiatorIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/d6/83/bce44d9deb8684946cea60c5c1f2", - "assets/build/ba_data/textures/gladiatorIcon_preview.png": "https://files.ballistica.net/cache/ba1/35/dd/225a603dc87ddd7408491d190c32", - "assets/build/ba_data/textures/glow.dds": "https://files.ballistica.net/cache/ba1/69/31/1ec3d8d62342f80016d060f0fc62", - "assets/build/ba_data/textures/glow.ktx": "https://files.ballistica.net/cache/ba1/27/cd/5fdebf8efa1cf4cffc26f4e9e705", - "assets/build/ba_data/textures/glow.pvr": "https://files.ballistica.net/cache/ba1/f6/d2/c9e8d482a7c23feb12cc14fc66a1", - "assets/build/ba_data/textures/glow_preview.png": "https://files.ballistica.net/cache/ba1/20/64/6e31d3b70a268f1cce74890ffdc6", - "assets/build/ba_data/textures/googlePlayAchievementsIcon.dds": "https://files.ballistica.net/cache/ba1/1d/bf/fe4f20f4a047fd3b813e60ac69dc", - "assets/build/ba_data/textures/googlePlayAchievementsIcon.ktx": "https://files.ballistica.net/cache/ba1/72/a2/859eb350e8e94e00dd7b3b0c5f57", - "assets/build/ba_data/textures/googlePlayAchievementsIcon.pvr": "https://files.ballistica.net/cache/ba1/18/00/10cc6f22adb874d89a0b1299c69d", - "assets/build/ba_data/textures/googlePlayAchievementsIcon_preview.png": "https://files.ballistica.net/cache/ba1/2e/ed/2fafb0a5f024c0744c30721f6da2", - "assets/build/ba_data/textures/googlePlayGamesIcon.dds": "https://files.ballistica.net/cache/ba1/0a/2f/97625f280600e4e1ce93c5caf0d7", - "assets/build/ba_data/textures/googlePlayGamesIcon.ktx": "https://files.ballistica.net/cache/ba1/cc/ae/68a0fcdbd49859da266929c917b3", - "assets/build/ba_data/textures/googlePlayGamesIcon.pvr": "https://files.ballistica.net/cache/ba1/f8/00/462c1819a3f16d3729d385c04282", - "assets/build/ba_data/textures/googlePlayGamesIcon_preview.png": "https://files.ballistica.net/cache/ba1/22/02/8c32ccc28b653fcc8c3523091f4c", - "assets/build/ba_data/textures/googlePlayLeaderboardsIcon.dds": "https://files.ballistica.net/cache/ba1/d6/fd/6bf30aa5676be60e79478e26ec86", - "assets/build/ba_data/textures/googlePlayLeaderboardsIcon.ktx": "https://files.ballistica.net/cache/ba1/99/17/4ffda9bc95998087f82574e22fc1", - "assets/build/ba_data/textures/googlePlayLeaderboardsIcon.pvr": "https://files.ballistica.net/cache/ba1/8d/81/878aa26b60979750fdc7d8646005", - "assets/build/ba_data/textures/googlePlayLeaderboardsIcon_preview.png": "https://files.ballistica.net/cache/ba1/1d/16/2a55dbeab6b5a89b028c8b5b872b", - "assets/build/ba_data/textures/googlePlusIcon.dds": "https://files.ballistica.net/cache/ba1/93/c4/d6ecf43d90959a201fb331de2511", - "assets/build/ba_data/textures/googlePlusIcon.ktx": "https://files.ballistica.net/cache/ba1/fc/c3/6deb8f41d90732c27759e3f38197", - "assets/build/ba_data/textures/googlePlusIcon.pvr": "https://files.ballistica.net/cache/ba1/06/5f/18bd05025a93e0a3312de2488ed7", - "assets/build/ba_data/textures/googlePlusIcon_preview.png": "https://files.ballistica.net/cache/ba1/8b/f2/f070fe3fdc89a5f1881a203bdef6", - "assets/build/ba_data/textures/googlePlusSignInButton.dds": "https://files.ballistica.net/cache/ba1/b2/44/6d1e10115b5db3a5ca27400c63bf", - "assets/build/ba_data/textures/googlePlusSignInButton.ktx": "https://files.ballistica.net/cache/ba1/6b/73/5e33a3bddc4ccd639e6f440ad9d4", - "assets/build/ba_data/textures/googlePlusSignInButton.pvr": "https://files.ballistica.net/cache/ba1/c1/05/17843386f42ab9d34bedea86aca4", - "assets/build/ba_data/textures/googlePlusSignInButton_preview.png": "https://files.ballistica.net/cache/ba1/19/5e/ad5f3b8c700444ea58660b5547dc", - "assets/build/ba_data/textures/graphicsIcon.dds": "https://files.ballistica.net/cache/ba1/34/c6/abfc4f939b3afda74743e420a17e", - "assets/build/ba_data/textures/graphicsIcon.ktx": "https://files.ballistica.net/cache/ba1/ba/cb/4111bf28f1dabaec5b25a0883e7a", - "assets/build/ba_data/textures/graphicsIcon.pvr": "https://files.ballistica.net/cache/ba1/72/16/fecf118aa64b28ca61f2a7024209", - "assets/build/ba_data/textures/graphicsIcon_preview.png": "https://files.ballistica.net/cache/ba1/14/24/97e0374888661715dead8f0d6948", - "assets/build/ba_data/textures/heart.dds": "https://files.ballistica.net/cache/ba1/11/a7/5a1c140a974905db06f33f1031f3", - "assets/build/ba_data/textures/heart.ktx": "https://files.ballistica.net/cache/ba1/b7/b1/8db1d5e585578b16ad0e7dac922e", - "assets/build/ba_data/textures/heart.pvr": "https://files.ballistica.net/cache/ba1/b1/78/46eb38e941aa94b7a8c654c1ef38", - "assets/build/ba_data/textures/heart_preview.png": "https://files.ballistica.net/cache/ba1/3c/d8/bc7b73e6920382202f1bf27b7b40", - "assets/build/ba_data/textures/hockeyStadium.dds": "https://files.ballistica.net/cache/ba1/bc/a0/0ddbb1707aab5481582f40b5042c", - "assets/build/ba_data/textures/hockeyStadium.ktx": "https://files.ballistica.net/cache/ba1/57/14/4a768367b5a3ada80b8b866320ae", - "assets/build/ba_data/textures/hockeyStadium.pvr": "https://files.ballistica.net/cache/ba1/f4/e4/65792658b9ef354781472d5f82ba", - "assets/build/ba_data/textures/hockeyStadiumPreview.dds": "https://files.ballistica.net/cache/ba1/3c/d6/a0f39559e614781ef7590cf98826", - "assets/build/ba_data/textures/hockeyStadiumPreview.ktx": "https://files.ballistica.net/cache/ba1/af/05/69b7f2c83e332b6ec410163a59fd", - "assets/build/ba_data/textures/hockeyStadiumPreview.pvr": "https://files.ballistica.net/cache/ba1/4c/fb/7bcd3e1d5f830558cd01206c57e6", - "assets/build/ba_data/textures/hockeyStadiumPreview_preview.png": "https://files.ballistica.net/cache/ba1/d8/99/5d6922a337009674a13b647e06c5", - "assets/build/ba_data/textures/hockeyStadium_preview.png": "https://files.ballistica.net/cache/ba1/e4/6f/a27e76ee2c59c218495c817b3c08", - "assets/build/ba_data/textures/iconOnslaught.dds": "https://files.ballistica.net/cache/ba1/8b/5d/4588721810c23a9b9248c50d4fa6", - "assets/build/ba_data/textures/iconOnslaught.ktx": "https://files.ballistica.net/cache/ba1/d6/ec/a0dbcc5fc2f3bcc811575faf40e1", - "assets/build/ba_data/textures/iconOnslaught.pvr": "https://files.ballistica.net/cache/ba1/8a/c8/193972105d61be48027b522f79e9", - "assets/build/ba_data/textures/iconOnslaught_preview.png": "https://files.ballistica.net/cache/ba1/e2/c8/eae940dda4bb4fff4d0a0d11c161", - "assets/build/ba_data/textures/iconRunaround.dds": "https://files.ballistica.net/cache/ba1/65/f2/e8ad92ce2668b50cdd10ad8d2f12", - "assets/build/ba_data/textures/iconRunaround.ktx": "https://files.ballistica.net/cache/ba1/6e/66/c5741e7805801c46c18df2f86c9a", - "assets/build/ba_data/textures/iconRunaround.pvr": "https://files.ballistica.net/cache/ba1/54/e9/55a73bde307c0c7cb386c06837ac", - "assets/build/ba_data/textures/iconRunaround_preview.png": "https://files.ballistica.net/cache/ba1/2b/09/82799554bb223e4ffba1aeb3dc81", - "assets/build/ba_data/textures/iircadeLogo.dds": "https://files.ballistica.net/cache/ba1/ad/ca/23c18300668408ec8709e16c1ef3", - "assets/build/ba_data/textures/iircadeLogo.ktx": "https://files.ballistica.net/cache/ba1/58/b9/848fbac7a1c7ad6a9b2efd758ff1", - "assets/build/ba_data/textures/iircadeLogo.pvr": "https://files.ballistica.net/cache/ba1/19/ba/66f832c978d9cf46f991e4811401", - "assets/build/ba_data/textures/iircadeLogo_preview.png": "https://files.ballistica.net/cache/ba1/c7/d6/0740136951cbc17907f6192357b2", - "assets/build/ba_data/textures/impactBombColor.dds": "https://files.ballistica.net/cache/ba1/a6/70/8486c52fb904e18e9b13f4850b5e", - "assets/build/ba_data/textures/impactBombColor.ktx": "https://files.ballistica.net/cache/ba1/fc/3b/e1051061b1ed03bcbfdc6b9f8c79", - "assets/build/ba_data/textures/impactBombColor.pvr": "https://files.ballistica.net/cache/ba1/d6/d9/0f81193db5aa44780ee8bb0943b9", - "assets/build/ba_data/textures/impactBombColorLit.dds": "https://files.ballistica.net/cache/ba1/fe/65/a55aba9c983239516c96f28fd4b2", - "assets/build/ba_data/textures/impactBombColorLit.ktx": "https://files.ballistica.net/cache/ba1/c3/1a/f688099b25688e988a90a574b7f7", - "assets/build/ba_data/textures/impactBombColorLit.pvr": "https://files.ballistica.net/cache/ba1/c5/95/68500c3eb7dd2c0420741885e293", - "assets/build/ba_data/textures/impactBombColorLit_preview.png": "https://files.ballistica.net/cache/ba1/47/c4/f32573512376c6077c42521e1e80", - "assets/build/ba_data/textures/impactBombColor_preview.png": "https://files.ballistica.net/cache/ba1/a5/66/daf001fe21d3c4d756f60f33fd1e", - "assets/build/ba_data/textures/inventoryIcon.dds": "https://files.ballistica.net/cache/ba1/2d/10/0a1a55bcad86b2d5a80eba231de4", - "assets/build/ba_data/textures/inventoryIcon.ktx": "https://files.ballistica.net/cache/ba1/3c/ca/1a61ddc33fbae07060b3ffe24531", - "assets/build/ba_data/textures/inventoryIcon.pvr": "https://files.ballistica.net/cache/ba1/a7/e3/6fe1d29dd307067b5b42aa1e78ca", - "assets/build/ba_data/textures/inventoryIcon_preview.png": "https://files.ballistica.net/cache/ba1/5d/95/fc2f9cf29120f6e5714a82b49762", - "assets/build/ba_data/textures/jackColor.dds": "https://files.ballistica.net/cache/ba1/9c/16/c4c568eec16b4ac15110e2213107", - "assets/build/ba_data/textures/jackColor.ktx": "https://files.ballistica.net/cache/ba1/b4/92/c66d4bd99efdd17578c1ab40eb38", - "assets/build/ba_data/textures/jackColor.pvr": "https://files.ballistica.net/cache/ba1/93/f0/b67389b8a52446f9d60119b4e4b0", - "assets/build/ba_data/textures/jackColorMask.dds": "https://files.ballistica.net/cache/ba1/33/86/4e8b48b1274373302fed850899f3", - "assets/build/ba_data/textures/jackColorMask.ktx": "https://files.ballistica.net/cache/ba1/c9/40/a44ca36c9fcb3ca51fb61dbaa2cf", - "assets/build/ba_data/textures/jackColorMask.pvr": "https://files.ballistica.net/cache/ba1/32/89/9243f17fdd9d8c3d7c9cc8a26191", - "assets/build/ba_data/textures/jackColorMask_preview.png": "https://files.ballistica.net/cache/ba1/71/92/54cc17903955e0b923f64bb746a7", - "assets/build/ba_data/textures/jackColor_preview.png": "https://files.ballistica.net/cache/ba1/83/f8/daf0dc5014e0220a8e4bc8d1113a", - "assets/build/ba_data/textures/jackIcon.dds": "https://files.ballistica.net/cache/ba1/42/a1/c6f5a537e23a509a22a13870470f", - "assets/build/ba_data/textures/jackIcon.ktx": "https://files.ballistica.net/cache/ba1/e8/5d/e5e222130b790f5239089333fd92", - "assets/build/ba_data/textures/jackIcon.pvr": "https://files.ballistica.net/cache/ba1/da/f9/29d46bb6b052d5aad5a81efa994e", - "assets/build/ba_data/textures/jackIconColorMask.dds": "https://files.ballistica.net/cache/ba1/1e/8e/395cb09dbe9ca002ad0d5532f6bd", - "assets/build/ba_data/textures/jackIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/e5/1b/e113913efca8af6c7b4dd07ceb9c", - "assets/build/ba_data/textures/jackIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/d6/7a/7756102c66959cf110698cf7ac03", - "assets/build/ba_data/textures/jackIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/d3/d2/de8f97d370d350cf8925c9a79f69", - "assets/build/ba_data/textures/jackIcon_preview.png": "https://files.ballistica.net/cache/ba1/ba/40/eb3cfd3c3a5ec35f15ebba2bbe59", - "assets/build/ba_data/textures/jumpsuitColor.dds": "https://files.ballistica.net/cache/ba1/5b/9c/4761afb2e6fc3e0f8ef028d317be", - "assets/build/ba_data/textures/jumpsuitColor.ktx": "https://files.ballistica.net/cache/ba1/45/25/6084698fbdd038fbd750fc72dd28", - "assets/build/ba_data/textures/jumpsuitColor.pvr": "https://files.ballistica.net/cache/ba1/6c/77/39fe91748b79f2aabc640f38fd2a", - "assets/build/ba_data/textures/jumpsuitColorMask.dds": "https://files.ballistica.net/cache/ba1/04/00/8fbe70c2d5bc499f842e76b6a71d", - "assets/build/ba_data/textures/jumpsuitColorMask.ktx": "https://files.ballistica.net/cache/ba1/50/c2/79341c7081f28d70de09cb6db116", - "assets/build/ba_data/textures/jumpsuitColorMask.pvr": "https://files.ballistica.net/cache/ba1/51/1e/5f4580d17068abebb0f8d2609c3c", - "assets/build/ba_data/textures/jumpsuitColorMask_preview.png": "https://files.ballistica.net/cache/ba1/6f/05/15f3184259761adc208e2dd659ca", - "assets/build/ba_data/textures/jumpsuitColor_preview.png": "https://files.ballistica.net/cache/ba1/b2/7e/9f91058cc4f333fc3928a3918e7d", - "assets/build/ba_data/textures/jumpsuitIcon.dds": "https://files.ballistica.net/cache/ba1/b7/85/615da57672deee78c8836446bf3a", - "assets/build/ba_data/textures/jumpsuitIcon.ktx": "https://files.ballistica.net/cache/ba1/63/46/2675c91ece41bc941a11cddac40c", - "assets/build/ba_data/textures/jumpsuitIcon.pvr": "https://files.ballistica.net/cache/ba1/a7/29/9aad9b8a44fa81465859a24e437d", - "assets/build/ba_data/textures/jumpsuitIconColorMask.dds": "https://files.ballistica.net/cache/ba1/8c/39/8c135a187f18f662a569cb37961d", - "assets/build/ba_data/textures/jumpsuitIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/d8/ac/a2d68d816df690e8699da8d8b5e8", - "assets/build/ba_data/textures/jumpsuitIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/77/e0/1326a95629aec5d0a447f4694dc8", - "assets/build/ba_data/textures/jumpsuitIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/d0/81/c975a77166d8bac47d63340960b4", - "assets/build/ba_data/textures/jumpsuitIcon_preview.png": "https://files.ballistica.net/cache/ba1/06/1f/0f16b73da3b140ae91a645ad3920", - "assets/build/ba_data/textures/kronk.dds": "https://files.ballistica.net/cache/ba1/79/99/217e4d40c7107337bf4ff402e020", - "assets/build/ba_data/textures/kronk.ktx": "https://files.ballistica.net/cache/ba1/80/d7/0f40cc83961456cca52f50d87657", - "assets/build/ba_data/textures/kronk.pvr": "https://files.ballistica.net/cache/ba1/4d/74/fb56c0df66aa36551ae4ac73a141", - "assets/build/ba_data/textures/kronkColorMask.dds": "https://files.ballistica.net/cache/ba1/f7/e2/92c6c2179b0ea3dcaa85f357e3d7", - "assets/build/ba_data/textures/kronkColorMask.ktx": "https://files.ballistica.net/cache/ba1/de/23/7ab48fef2af0a1253b2eb6a083ab", - "assets/build/ba_data/textures/kronkColorMask.pvr": "https://files.ballistica.net/cache/ba1/d4/ab/0df43e24f6aa11608a2f9b2bda6d", - "assets/build/ba_data/textures/kronkColorMask_preview.png": "https://files.ballistica.net/cache/ba1/5c/14/80f2148210e89e8ca6b3b03b6e10", - "assets/build/ba_data/textures/kronkIcon.dds": "https://files.ballistica.net/cache/ba1/d2/66/367fb5a09d45419be710683f0b8a", - "assets/build/ba_data/textures/kronkIcon.ktx": "https://files.ballistica.net/cache/ba1/0b/23/ca03de39efd26cc2ca53a46f70b4", - "assets/build/ba_data/textures/kronkIcon.pvr": "https://files.ballistica.net/cache/ba1/d7/2c/17a833f010265c66243ec90fcf09", - "assets/build/ba_data/textures/kronkIconColorMask.dds": "https://files.ballistica.net/cache/ba1/ec/4f/d9c86c700f2feaf28767f666a836", - "assets/build/ba_data/textures/kronkIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/41/41/5c76cd5af36248308d96cf1b4bcd", - "assets/build/ba_data/textures/kronkIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/4c/0c/41d95110ecda25e0e66a3876cce4", - "assets/build/ba_data/textures/kronkIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/a3/24/7b66c17a8bae4ef0f9f3ca4a95ca", - "assets/build/ba_data/textures/kronkIcon_preview.png": "https://files.ballistica.net/cache/ba1/5f/28/4ca750d54dcb57ad82f3bbacd2e5", - "assets/build/ba_data/textures/kronk_preview.png": "https://files.ballistica.net/cache/ba1/0b/2e/083f0a0c74bec4b4f629e2bdd2a0", - "assets/build/ba_data/textures/lakeFrigid.dds": "https://files.ballistica.net/cache/ba1/de/16/41f8895b19d81123ab48ad72f38d", - "assets/build/ba_data/textures/lakeFrigid.ktx": "https://files.ballistica.net/cache/ba1/d2/ad/c28e092d9eeea2c5643d1d89613f", - "assets/build/ba_data/textures/lakeFrigid.pvr": "https://files.ballistica.net/cache/ba1/f8/c0/53f0c6ec9f5ec37514b07d0c3506", - "assets/build/ba_data/textures/lakeFrigidPreview.dds": "https://files.ballistica.net/cache/ba1/eb/fd/9d832e847a72de6f62a02403abc5", - "assets/build/ba_data/textures/lakeFrigidPreview.ktx": "https://files.ballistica.net/cache/ba1/8f/d5/185af314124e8ca312475205704e", - "assets/build/ba_data/textures/lakeFrigidPreview.pvr": "https://files.ballistica.net/cache/ba1/9b/2d/8b05ecb7e85b4dd42613e3e0ff89", - "assets/build/ba_data/textures/lakeFrigidPreview_preview.png": "https://files.ballistica.net/cache/ba1/80/ef/8283d6c6c7f144b1dd187262b661", - "assets/build/ba_data/textures/lakeFrigidReflections.dds": "https://files.ballistica.net/cache/ba1/50/18/f8b726d49e1814cbddbe0f05d3b4", - "assets/build/ba_data/textures/lakeFrigidReflections.ktx": "https://files.ballistica.net/cache/ba1/5c/8f/c8630c9984abaf5a05efc1c4a4c8", - "assets/build/ba_data/textures/lakeFrigidReflections.pvr": "https://files.ballistica.net/cache/ba1/9d/c3/24697f8177bd10fb415713f0fd2b", - "assets/build/ba_data/textures/lakeFrigidReflections_preview.png": "https://files.ballistica.net/cache/ba1/eb/1e/4ca92ed77a0a482fcea8fb57f690", - "assets/build/ba_data/textures/lakeFrigid_preview.png": "https://files.ballistica.net/cache/ba1/90/cd/8056102a056f66c85920b093c1ec", - "assets/build/ba_data/textures/landMine.dds": "https://files.ballistica.net/cache/ba1/e9/e1/d83b85ee7b4701d4c62da33201cf", - "assets/build/ba_data/textures/landMine.ktx": "https://files.ballistica.net/cache/ba1/9e/c5/b0174682cde437186b10564c3a1c", - "assets/build/ba_data/textures/landMine.pvr": "https://files.ballistica.net/cache/ba1/60/2a/d26da34c7bc13784e096ff3d2386", - "assets/build/ba_data/textures/landMineLit.dds": "https://files.ballistica.net/cache/ba1/ae/e8/cece9037d9737df840fc5e42b84e", - "assets/build/ba_data/textures/landMineLit.ktx": "https://files.ballistica.net/cache/ba1/45/7a/814e683e9b0d51bd358eee47847f", - "assets/build/ba_data/textures/landMineLit.pvr": "https://files.ballistica.net/cache/ba1/19/dc/243609824c27a77cdb7b04f2f2a9", - "assets/build/ba_data/textures/landMineLit_preview.png": "https://files.ballistica.net/cache/ba1/b2/13/487a27957749d3e21cf12bec52ba", - "assets/build/ba_data/textures/landMine_preview.png": "https://files.ballistica.net/cache/ba1/c0/63/d8803b975e4f0fb642516ac00bef", - "assets/build/ba_data/textures/leaderboardsIcon.dds": "https://files.ballistica.net/cache/ba1/0a/5d/a1189204352e020d4f361e72a3a1", - "assets/build/ba_data/textures/leaderboardsIcon.ktx": "https://files.ballistica.net/cache/ba1/bd/40/6d241dc5c4e4f972418e31bd384a", - "assets/build/ba_data/textures/leaderboardsIcon.pvr": "https://files.ballistica.net/cache/ba1/00/96/dd32ec8f5d1e0edc40ba4265d397", - "assets/build/ba_data/textures/leaderboardsIcon_preview.png": "https://files.ballistica.net/cache/ba1/fe/44/e21b917f0bf1731600bd90b610de", - "assets/build/ba_data/textures/leftButton.dds": "https://files.ballistica.net/cache/ba1/1c/7b/f78dd39bc41f15a8747fe7f92228", - "assets/build/ba_data/textures/leftButton.ktx": "https://files.ballistica.net/cache/ba1/47/39/9ea0c222646706b992c16cb95ea2", - "assets/build/ba_data/textures/leftButton.pvr": "https://files.ballistica.net/cache/ba1/b5/0f/582a10c181c142e5130807b84168", - "assets/build/ba_data/textures/leftButton_preview.png": "https://files.ballistica.net/cache/ba1/58/f3/e968aab65efc3d7de9128665507c", - "assets/build/ba_data/textures/levelIcon.dds": "https://files.ballistica.net/cache/ba1/dd/5c/afcd702fe35c842a1e64ed88d085", - "assets/build/ba_data/textures/levelIcon.ktx": "https://files.ballistica.net/cache/ba1/6d/8d/3425b9e667807837b9d41292b9df", - "assets/build/ba_data/textures/levelIcon.pvr": "https://files.ballistica.net/cache/ba1/99/7a/29e41d86f108556decc4cbb32ef0", - "assets/build/ba_data/textures/levelIcon_preview.png": "https://files.ballistica.net/cache/ba1/52/20/20a7e9735c0d5eccbc3c2758365e", - "assets/build/ba_data/textures/light.dds": "https://files.ballistica.net/cache/ba1/77/75/225392101e2d867d064090a7437e", - "assets/build/ba_data/textures/light.ktx": "https://files.ballistica.net/cache/ba1/7e/d1/f2c7dda03ab4eccf54a50c70f21e", - "assets/build/ba_data/textures/light.pvr": "https://files.ballistica.net/cache/ba1/5d/27/1882a456ef41670a1197dc6c9b60", - "assets/build/ba_data/textures/lightSharp.dds": "https://files.ballistica.net/cache/ba1/57/c7/6d024d5912d13d6c21a5e1b7a40a", - "assets/build/ba_data/textures/lightSharp.ktx": "https://files.ballistica.net/cache/ba1/da/c4/cdb75a82b01b2f5114ea6b7e4ba0", - "assets/build/ba_data/textures/lightSharp.pvr": "https://files.ballistica.net/cache/ba1/e4/a9/9c3bd2721a8c46c464419df0679a", - "assets/build/ba_data/textures/lightSharp_preview.png": "https://files.ballistica.net/cache/ba1/df/ff/c198a21dfac888ab41d434d60c20", - "assets/build/ba_data/textures/lightSoft.dds": "https://files.ballistica.net/cache/ba1/1b/da/98826db203344f86c8e519dccdbf", - "assets/build/ba_data/textures/lightSoft.ktx": "https://files.ballistica.net/cache/ba1/ab/81/8fd830c7e7b2989035ba708a5116", - "assets/build/ba_data/textures/lightSoft.pvr": "https://files.ballistica.net/cache/ba1/45/26/25e399868b21bfa707b251329c24", - "assets/build/ba_data/textures/lightSoft_preview.png": "https://files.ballistica.net/cache/ba1/8c/39/c3379ded855747257ef416cb1d67", - "assets/build/ba_data/textures/light_preview.png": "https://files.ballistica.net/cache/ba1/45/bb/facfaef674fca7553ae08bc5d98d", - "assets/build/ba_data/textures/lock.dds": "https://files.ballistica.net/cache/ba1/8b/27/ca57a1cb2d136d0fa6ac5a612bde", - "assets/build/ba_data/textures/lock.ktx": "https://files.ballistica.net/cache/ba1/e0/53/145812f48f7c9f75a25c59b5ac81", - "assets/build/ba_data/textures/lock.pvr": "https://files.ballistica.net/cache/ba1/ac/eb/efdbd0d90a48464d37e664a69f2d", - "assets/build/ba_data/textures/lock_preview.png": "https://files.ballistica.net/cache/ba1/86/d2/977dcede9cd700689bed0e243deb", - "assets/build/ba_data/textures/logIcon.dds": "https://files.ballistica.net/cache/ba1/2d/c4/94708c2846f696bab3456ff37bb2", - "assets/build/ba_data/textures/logIcon.ktx": "https://files.ballistica.net/cache/ba1/1d/63/f9c329c6bf80dfa8cccb7c9d043b", - "assets/build/ba_data/textures/logIcon.pvr": "https://files.ballistica.net/cache/ba1/c1/25/484397535ac76ba0a5a3b11f7d17", - "assets/build/ba_data/textures/logIcon_preview.png": "https://files.ballistica.net/cache/ba1/f7/b6/2bdaf79ef397ee247a287a214fb6", - "assets/build/ba_data/textures/logo.dds": "https://files.ballistica.net/cache/ba1/69/11/f529875cf6cd088f0f3511af831a", - "assets/build/ba_data/textures/logo.ktx": "https://files.ballistica.net/cache/ba1/40/89/974ac20dd2607c0c4df23ef00481", - "assets/build/ba_data/textures/logo.pvr": "https://files.ballistica.net/cache/ba1/96/bf/9b2a19301d9429cc82e4415b6edd", - "assets/build/ba_data/textures/logoEaster.dds": "https://files.ballistica.net/cache/ba1/88/09/3c6e64b17aed09a3064bf4fe4173", - "assets/build/ba_data/textures/logoEaster.ktx": "https://files.ballistica.net/cache/ba1/ec/72/e7a6ae904d026e28bfbc5ef6fbe4", - "assets/build/ba_data/textures/logoEaster.pvr": "https://files.ballistica.net/cache/ba1/9a/59/b126c7953f1abaaa9fc3f4fccdaa", - "assets/build/ba_data/textures/logoEaster_preview.png": "https://files.ballistica.net/cache/ba1/40/32/4d90968a7b8cd9bd39bb60794691", - "assets/build/ba_data/textures/logo_preview.png": "https://files.ballistica.net/cache/ba1/51/0a/78daa538bd44cfc777479762a5db", - "assets/build/ba_data/textures/mapPreviewMask.dds": "https://files.ballistica.net/cache/ba1/b9/24/5e52331229c0c5b365b78134fea3", - "assets/build/ba_data/textures/mapPreviewMask.ktx": "https://files.ballistica.net/cache/ba1/0c/0a/09bdabfbf3f6a8d00e1e05dbf548", - "assets/build/ba_data/textures/mapPreviewMask.pvr": "https://files.ballistica.net/cache/ba1/fd/1c/6ceafbf1ccd997315ede21546fa6", - "assets/build/ba_data/textures/mapPreviewMask_preview.png": "https://files.ballistica.net/cache/ba1/be/32/ba7c14389312749b8412beb33748", - "assets/build/ba_data/textures/medalBronze.dds": "https://files.ballistica.net/cache/ba1/ae/fb/0f7ece0774e06cb1eb38acd0847a", - "assets/build/ba_data/textures/medalBronze.ktx": "https://files.ballistica.net/cache/ba1/aa/98/5fa94e94bab13a1d4f121c48d761", - "assets/build/ba_data/textures/medalBronze.pvr": "https://files.ballistica.net/cache/ba1/4f/e2/408cdc56c306996c2c63e5bd278e", - "assets/build/ba_data/textures/medalBronze_preview.png": "https://files.ballistica.net/cache/ba1/9b/3f/2424f04db3b2e85c526bd0fa1984", - "assets/build/ba_data/textures/medalComplete.dds": "https://files.ballistica.net/cache/ba1/dd/1f/95c307f86a28b2a8c3958ab1c380", - "assets/build/ba_data/textures/medalComplete.ktx": "https://files.ballistica.net/cache/ba1/4f/d6/4d496551fac1a82b759d66b0f706", - "assets/build/ba_data/textures/medalComplete.pvr": "https://files.ballistica.net/cache/ba1/67/7d/28dcf95961d2a7cab0d3f2232dd9", - "assets/build/ba_data/textures/medalComplete_preview.png": "https://files.ballistica.net/cache/ba1/6d/2f/befc33eab88272506a3cd2841b74", - "assets/build/ba_data/textures/medalGold.dds": "https://files.ballistica.net/cache/ba1/b9/1a/f3b9bdb521f2956f2d729a7df026", - "assets/build/ba_data/textures/medalGold.ktx": "https://files.ballistica.net/cache/ba1/fe/86/6658a587d719d0260967cbaee333", - "assets/build/ba_data/textures/medalGold.pvr": "https://files.ballistica.net/cache/ba1/30/bc/ebc80b89f646fa3201070c5d6af8", - "assets/build/ba_data/textures/medalGold_preview.png": "https://files.ballistica.net/cache/ba1/72/b0/a31647390470ae7c3c7993cd8273", - "assets/build/ba_data/textures/medalSilver.dds": "https://files.ballistica.net/cache/ba1/61/21/4d91b01feb44c1ad366f84e4c8c2", - "assets/build/ba_data/textures/medalSilver.ktx": "https://files.ballistica.net/cache/ba1/68/84/934ab97c1014e98ffd6bbb24ad89", - "assets/build/ba_data/textures/medalSilver.pvr": "https://files.ballistica.net/cache/ba1/37/e8/0f2d99b80e98d8388cbb1fb81aed", - "assets/build/ba_data/textures/medalSilver_preview.png": "https://files.ballistica.net/cache/ba1/1c/38/7e840a4f3b6f7c15e21b22628603", - "assets/build/ba_data/textures/melColor.dds": "https://files.ballistica.net/cache/ba1/c6/f1/ef8f3740635ec2b601f73afe0424", - "assets/build/ba_data/textures/melColor.ktx": "https://files.ballistica.net/cache/ba1/fa/a1/645eb759ffd7b046fe91ff4f9f8b", - "assets/build/ba_data/textures/melColor.pvr": "https://files.ballistica.net/cache/ba1/b1/90/45696ef2a4717ce5f3e1838e31f1", - "assets/build/ba_data/textures/melColorMask.dds": "https://files.ballistica.net/cache/ba1/b7/64/9ae23dc4897fd612952d3193c023", - "assets/build/ba_data/textures/melColorMask.ktx": "https://files.ballistica.net/cache/ba1/bb/ed/2e666f6e2d647b45874645084028", - "assets/build/ba_data/textures/melColorMask.pvr": "https://files.ballistica.net/cache/ba1/af/a4/002bc32b803774a668ce8953014e", - "assets/build/ba_data/textures/melColorMask_preview.png": "https://files.ballistica.net/cache/ba1/58/ee/347860a4fc2faf37f1fa3013ce18", - "assets/build/ba_data/textures/melColor_preview.png": "https://files.ballistica.net/cache/ba1/00/c3/3211716f8ebdc059d3adacd5eab1", - "assets/build/ba_data/textures/melIcon.dds": "https://files.ballistica.net/cache/ba1/0e/b2/683438456569eacf65b24f18b66f", - "assets/build/ba_data/textures/melIcon.ktx": "https://files.ballistica.net/cache/ba1/a6/fb/e0f4945bea0edf83729dadf52f24", - "assets/build/ba_data/textures/melIcon.pvr": "https://files.ballistica.net/cache/ba1/b6/ec/ed46dda142a62f35c28720878c8f", - "assets/build/ba_data/textures/melIconColorMask.dds": "https://files.ballistica.net/cache/ba1/61/b4/637cf663cd1df1d6adacfcbc148c", - "assets/build/ba_data/textures/melIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/64/e0/2f0de4aca98350b0cecbaea53cda", - "assets/build/ba_data/textures/melIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/ba/94/c1955a495fcb9156ef4415105fa3", - "assets/build/ba_data/textures/melIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/60/9b/38b259d63b5b8c17e0e8f4a09682", - "assets/build/ba_data/textures/melIcon_preview.png": "https://files.ballistica.net/cache/ba1/cf/1d/5fd4405703e177ab90cc331e2809", - "assets/build/ba_data/textures/menuBG.dds": "https://files.ballistica.net/cache/ba1/66/38/e19d5d26e0c3b7750ad612288f51", - "assets/build/ba_data/textures/menuBG.ktx": "https://files.ballistica.net/cache/ba1/f9/f7/a09f825ab514e1a57fcf67149798", - "assets/build/ba_data/textures/menuBG.pvr": "https://files.ballistica.net/cache/ba1/2e/8a/11960533b874cb081c384a521710", - "assets/build/ba_data/textures/menuBG_preview.png": "https://files.ballistica.net/cache/ba1/03/1c/9a07cf577bdf39e58effdbc2e242", - "assets/build/ba_data/textures/menuButton.dds": "https://files.ballistica.net/cache/ba1/d7/e7/1493a065ea5be1c1ff8629d91718", - "assets/build/ba_data/textures/menuButton.ktx": "https://files.ballistica.net/cache/ba1/8a/34/48a24ec0395bef4b95c63e611c59", - "assets/build/ba_data/textures/menuButton.pvr": "https://files.ballistica.net/cache/ba1/24/33/ae87a6b6091cb6dc1f9f1601995d", - "assets/build/ba_data/textures/menuButton_preview.png": "https://files.ballistica.net/cache/ba1/d9/a4/9558342d65b20fd7e363248f6673", - "assets/build/ba_data/textures/menuIcon.dds": "https://files.ballistica.net/cache/ba1/6a/2a/4545e1f02a312471ff91c17fe116", - "assets/build/ba_data/textures/menuIcon.ktx": "https://files.ballistica.net/cache/ba1/e7/8c/dde40b132e096841299505e2b665", - "assets/build/ba_data/textures/menuIcon.pvr": "https://files.ballistica.net/cache/ba1/91/d3/6e3b38a70caa2bc68ddff50e3421", - "assets/build/ba_data/textures/menuIcon_preview.png": "https://files.ballistica.net/cache/ba1/1f/22/69a013729949501405ea342a1a8b", - "assets/build/ba_data/textures/merch.dds": "https://files.ballistica.net/cache/ba1/da/6a/87779603f295cf30fcec2d3a4e9f", - "assets/build/ba_data/textures/merch.ktx": "https://files.ballistica.net/cache/ba1/5b/c4/2df30594a9e7c1988d92435d7bd0", - "assets/build/ba_data/textures/merch.pvr": "https://files.ballistica.net/cache/ba1/20/76/b8da7a2dded8c137c8388bcb6986", - "assets/build/ba_data/textures/merch_preview.png": "https://files.ballistica.net/cache/ba1/31/ad/9ffc829f1fcff718106d129ac062", - "assets/build/ba_data/textures/meter.dds": "https://files.ballistica.net/cache/ba1/52/a8/00244236f0e6126e2e14bd1f3106", - "assets/build/ba_data/textures/meter.ktx": "https://files.ballistica.net/cache/ba1/b5/e7/3ba3d4c7c7e05081eb033ae5da8f", - "assets/build/ba_data/textures/meter.pvr": "https://files.ballistica.net/cache/ba1/58/de/ab83ad9e0d9b434ca42811030ef9", - "assets/build/ba_data/textures/meter_preview.png": "https://files.ballistica.net/cache/ba1/09/b7/40a1a187d7b057defa6f32c6b372", - "assets/build/ba_data/textures/monkeyFaceLevelColor.dds": "https://files.ballistica.net/cache/ba1/e8/41/cfcdc67b1adbf50621ae9ebdafef", - "assets/build/ba_data/textures/monkeyFaceLevelColor.ktx": "https://files.ballistica.net/cache/ba1/33/d0/e2600748ebab847b7a146350a3c9", - "assets/build/ba_data/textures/monkeyFaceLevelColor.pvr": "https://files.ballistica.net/cache/ba1/09/98/9491294da842980b75739e8d1c59", - "assets/build/ba_data/textures/monkeyFaceLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/2f/84/fd142ba957ead4acb7b4081a3771", - "assets/build/ba_data/textures/monkeyFacePreview.dds": "https://files.ballistica.net/cache/ba1/7d/53/6c90a28662f70e03ae967a63151c", - "assets/build/ba_data/textures/monkeyFacePreview.ktx": "https://files.ballistica.net/cache/ba1/ff/83/d340f1495861ebb97bd7f766ccbb", - "assets/build/ba_data/textures/monkeyFacePreview.pvr": "https://files.ballistica.net/cache/ba1/85/e7/807edc04137ee1a3b7767e90d692", - "assets/build/ba_data/textures/monkeyFacePreview_preview.png": "https://files.ballistica.net/cache/ba1/5c/62/929429eb7fd47b21edf6dd8d8f63", - "assets/build/ba_data/textures/multiplayerExamples.dds": "https://files.ballistica.net/cache/ba1/20/c0/99dc44c35a1b3025877bc300399c", - "assets/build/ba_data/textures/multiplayerExamples.ktx": "https://files.ballistica.net/cache/ba1/48/2c/081cf255b138824720c0b49d2e1e", - "assets/build/ba_data/textures/multiplayerExamples.pvr": "https://files.ballistica.net/cache/ba1/d1/db/8902cdbf501bf919e5f9f0ada2b4", - "assets/build/ba_data/textures/multiplayerExamples_preview.png": "https://files.ballistica.net/cache/ba1/ea/7c/acf4240ca1fa313126060a8258f0", - "assets/build/ba_data/textures/natureBackgroundColor.dds": "https://files.ballistica.net/cache/ba1/41/5a/2821b371bfb514531fd617be39cd", - "assets/build/ba_data/textures/natureBackgroundColor.ktx": "https://files.ballistica.net/cache/ba1/60/09/50676eca6d0599eabf895f18aa21", - "assets/build/ba_data/textures/natureBackgroundColor.pvr": "https://files.ballistica.net/cache/ba1/1f/37/b4950cf37311fc39f53235adb190", - "assets/build/ba_data/textures/natureBackgroundColor_preview.png": "https://files.ballistica.net/cache/ba1/61/91/2af689aba61ee35bc281a8c2fc88", - "assets/build/ba_data/textures/neoSpazColor.dds": "https://files.ballistica.net/cache/ba1/e7/77/ba83d0d9a1472d495d3ebaa182c4", - "assets/build/ba_data/textures/neoSpazColor.ktx": "https://files.ballistica.net/cache/ba1/d1/d7/797152df3d1260338405ea0ea59f", - "assets/build/ba_data/textures/neoSpazColor.pvr": "https://files.ballistica.net/cache/ba1/8b/68/60b1e0bc0c6295fba84e493c313d", - "assets/build/ba_data/textures/neoSpazColorMask.dds": "https://files.ballistica.net/cache/ba1/05/ff/08bba0e201d0eea3ead5746ca96f", - "assets/build/ba_data/textures/neoSpazColorMask.ktx": "https://files.ballistica.net/cache/ba1/ef/f6/84779b7eb27ccbbcdb77f3b6f2fb", - "assets/build/ba_data/textures/neoSpazColorMask.pvr": "https://files.ballistica.net/cache/ba1/a2/a8/4aa145599bd4b8c53d87624553e2", - "assets/build/ba_data/textures/neoSpazColorMask_preview.png": "https://files.ballistica.net/cache/ba1/62/88/1239b81fe081de88c08ac7434bc3", - "assets/build/ba_data/textures/neoSpazColor_preview.png": "https://files.ballistica.net/cache/ba1/23/2c/b541bf04f85d1aa5de3b475b82d2", - "assets/build/ba_data/textures/neoSpazIcon.dds": "https://files.ballistica.net/cache/ba1/e0/6e/59525196623212ac76d96d9cd3bb", - "assets/build/ba_data/textures/neoSpazIcon.ktx": "https://files.ballistica.net/cache/ba1/d2/b5/f7c3badf697bc9dd82cffd17352c", - "assets/build/ba_data/textures/neoSpazIcon.pvr": "https://files.ballistica.net/cache/ba1/47/c1/5ee2e1475e142aa8468b2d50c752", - "assets/build/ba_data/textures/neoSpazIconColorMask.dds": "https://files.ballistica.net/cache/ba1/60/46/4e61726cb5e2c81b0e0af5a7cd5e", - "assets/build/ba_data/textures/neoSpazIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/ca/5e/ab5d29947e33f331ac116c106548", - "assets/build/ba_data/textures/neoSpazIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/75/2e/1a3cb1b5fb28b5ea31cda4a02923", - "assets/build/ba_data/textures/neoSpazIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/2f/3d/c8cb5b27023e0fc12e7dccffe363", - "assets/build/ba_data/textures/neoSpazIcon_preview.png": "https://files.ballistica.net/cache/ba1/f8/d9/4b9d8688b3e4e8c1a9ef816cbcd1", - "assets/build/ba_data/textures/nextLevelIcon.dds": "https://files.ballistica.net/cache/ba1/1f/3f/28de59b7eee20eb5e560ae135bea", - "assets/build/ba_data/textures/nextLevelIcon.ktx": "https://files.ballistica.net/cache/ba1/b2/84/0bb105ab76cc15c66f52ecbefcf7", - "assets/build/ba_data/textures/nextLevelIcon.pvr": "https://files.ballistica.net/cache/ba1/0b/8d/087ad0de136250c54e42f4dca92f", - "assets/build/ba_data/textures/nextLevelIcon_preview.png": "https://files.ballistica.net/cache/ba1/76/5f/ffae5ff97775780301b96a1a3f52", - "assets/build/ba_data/textures/ninjaColor.dds": "https://files.ballistica.net/cache/ba1/e7/25/60195fddd30365324328f09e1230", - "assets/build/ba_data/textures/ninjaColor.ktx": "https://files.ballistica.net/cache/ba1/2a/a1/22d137826995da608c2e3898083e", - "assets/build/ba_data/textures/ninjaColor.pvr": "https://files.ballistica.net/cache/ba1/2d/0c/6a9364e38112591a5f77de741ca2", - "assets/build/ba_data/textures/ninjaColorMask.dds": "https://files.ballistica.net/cache/ba1/db/da/51021e6f1482e5664af808b29b00", - "assets/build/ba_data/textures/ninjaColorMask.ktx": "https://files.ballistica.net/cache/ba1/1d/68/ee8c4f656d583b49638f3cb40b4e", - "assets/build/ba_data/textures/ninjaColorMask.pvr": "https://files.ballistica.net/cache/ba1/06/0a/58e0e29f5b9f503afcfc1a93f052", - "assets/build/ba_data/textures/ninjaColorMask_preview.png": "https://files.ballistica.net/cache/ba1/8c/22/0103111a22390dd49615eaf2a4c3", - "assets/build/ba_data/textures/ninjaColor_preview.png": "https://files.ballistica.net/cache/ba1/d9/d9/52cf79858329ee664417fa86325a", - "assets/build/ba_data/textures/ninjaIcon.dds": "https://files.ballistica.net/cache/ba1/a4/34/ac0523aa0d224b5668966a1ce6cd", - "assets/build/ba_data/textures/ninjaIcon.ktx": "https://files.ballistica.net/cache/ba1/fb/be/7acdbf4b9e212c3a20bf7b7ffe1a", - "assets/build/ba_data/textures/ninjaIcon.pvr": "https://files.ballistica.net/cache/ba1/1a/c3/a5843f74115717fcbeb44cb792df", - "assets/build/ba_data/textures/ninjaIconColorMask.dds": "https://files.ballistica.net/cache/ba1/b8/eb/6b4811f664e79059a8604db820d6", - "assets/build/ba_data/textures/ninjaIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/1f/ad/82a7821b603e0bef6a23d288cab2", - "assets/build/ba_data/textures/ninjaIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/68/ca/2ae393fd37581875ed88b7084432", - "assets/build/ba_data/textures/ninjaIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/94/69/c47c81659d8d4765e93ad05385e8", - "assets/build/ba_data/textures/ninjaIcon_preview.png": "https://files.ballistica.net/cache/ba1/4e/67/123a6db51b454ebd93230d50c854", - "assets/build/ba_data/textures/nub.dds": "https://files.ballistica.net/cache/ba1/52/3a/8441765b5691981f10853f85c5ad", - "assets/build/ba_data/textures/nub.ktx": "https://files.ballistica.net/cache/ba1/66/1b/ba9878126f377eba687e72260398", - "assets/build/ba_data/textures/nub.pvr": "https://files.ballistica.net/cache/ba1/e6/6e/9d1989b80e55baf405151c0d45ed", - "assets/build/ba_data/textures/nub_preview.png": "https://files.ballistica.net/cache/ba1/98/cf/36351dd620110735041d651d60ea", - "assets/build/ba_data/textures/null.dds": "https://files.ballistica.net/cache/ba1/31/59/e52c794185cbd8d47eaf8f69526a", - "assets/build/ba_data/textures/null.ktx": "https://files.ballistica.net/cache/ba1/cd/99/4617510724525d3ffc5672e8a035", - "assets/build/ba_data/textures/null.pvr": "https://files.ballistica.net/cache/ba1/a0/6c/3751879b7145688c9a426e48482d", - "assets/build/ba_data/textures/null_preview.png": "https://files.ballistica.net/cache/ba1/b9/59/575b0a5a0937e0471d332a0e0b2c", - "assets/build/ba_data/textures/oldLadyColor.dds": "https://files.ballistica.net/cache/ba1/9c/9a/7b82b343061f5b2f8f2b281abf07", - "assets/build/ba_data/textures/oldLadyColor.ktx": "https://files.ballistica.net/cache/ba1/e9/a6/b2c434cba993f176a1fbce5e017c", - "assets/build/ba_data/textures/oldLadyColor.pvr": "https://files.ballistica.net/cache/ba1/49/6e/5d95e58db32fafff298781d3c379", - "assets/build/ba_data/textures/oldLadyColorMask.dds": "https://files.ballistica.net/cache/ba1/e5/91/354e94e932df33f458ee0bf6e1a0", - "assets/build/ba_data/textures/oldLadyColorMask.ktx": "https://files.ballistica.net/cache/ba1/81/da/80cd97c5f4c228ea585d4c6b1906", - "assets/build/ba_data/textures/oldLadyColorMask.pvr": "https://files.ballistica.net/cache/ba1/b7/fc/328a89dfc0578184d0cd9be05e5f", - "assets/build/ba_data/textures/oldLadyColorMask_preview.png": "https://files.ballistica.net/cache/ba1/f7/23/f290703d2c69f7f115151907afdd", - "assets/build/ba_data/textures/oldLadyColor_preview.png": "https://files.ballistica.net/cache/ba1/d8/25/7e3e13e51d0362ebb9b3e5d268ac", - "assets/build/ba_data/textures/oldLadyIcon.dds": "https://files.ballistica.net/cache/ba1/b4/68/f71eb708c6f0287aefa71463823e", - "assets/build/ba_data/textures/oldLadyIcon.ktx": "https://files.ballistica.net/cache/ba1/ea/c8/525ad8caec840e40ba36e4055786", - "assets/build/ba_data/textures/oldLadyIcon.pvr": "https://files.ballistica.net/cache/ba1/7c/23/b2b6b4da74205d68c519dd671d11", - "assets/build/ba_data/textures/oldLadyIconColorMask.dds": "https://files.ballistica.net/cache/ba1/c9/35/22ca9cd9082c510d252a545f6399", - "assets/build/ba_data/textures/oldLadyIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/08/98/41f1830a8f435be07754779eeae0", - "assets/build/ba_data/textures/oldLadyIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/5e/12/2b0765387d1a6637b6e130bdb107", - "assets/build/ba_data/textures/oldLadyIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/ef/b2/0c2a20cf7f647014ccf712c55627", - "assets/build/ba_data/textures/oldLadyIcon_preview.png": "https://files.ballistica.net/cache/ba1/1d/bc/22b7f44ba975e6ffe06390596fa2", - "assets/build/ba_data/textures/operaSingerColor.dds": "https://files.ballistica.net/cache/ba1/06/40/1d46db18bbf308f1e8e88ed25c7d", - "assets/build/ba_data/textures/operaSingerColor.ktx": "https://files.ballistica.net/cache/ba1/a9/34/bac2cb5bfa5cfac1b5a209e77a36", - "assets/build/ba_data/textures/operaSingerColor.pvr": "https://files.ballistica.net/cache/ba1/1b/e3/4a871e36791bf302c0ecb68426a7", - "assets/build/ba_data/textures/operaSingerColorMask.dds": "https://files.ballistica.net/cache/ba1/a6/c8/eb903a977a1c83792b5a9485357a", - "assets/build/ba_data/textures/operaSingerColorMask.ktx": "https://files.ballistica.net/cache/ba1/ad/76/24839951a638bcdbd2f399e26b96", - "assets/build/ba_data/textures/operaSingerColorMask.pvr": "https://files.ballistica.net/cache/ba1/1e/56/ce51acda0e9f77cf8450c4871503", - "assets/build/ba_data/textures/operaSingerColorMask_preview.png": "https://files.ballistica.net/cache/ba1/99/5e/a20ff9e112ae711dff9d9b0602c0", - "assets/build/ba_data/textures/operaSingerColor_preview.png": "https://files.ballistica.net/cache/ba1/58/d4/d9b0fc00f61615b987066343bafe", - "assets/build/ba_data/textures/operaSingerIcon.dds": "https://files.ballistica.net/cache/ba1/0e/09/f7f4bdda80d12b4238c062002223", - "assets/build/ba_data/textures/operaSingerIcon.ktx": "https://files.ballistica.net/cache/ba1/d6/46/816407493b140ba4e83955494a87", - "assets/build/ba_data/textures/operaSingerIcon.pvr": "https://files.ballistica.net/cache/ba1/df/bb/f2374801859d2153ce50b7aa257a", - "assets/build/ba_data/textures/operaSingerIconColorMask.dds": "https://files.ballistica.net/cache/ba1/8a/3b/ee091fdba058cefb653217f397ad", - "assets/build/ba_data/textures/operaSingerIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/fa/88/894da3fbc5741dadf5233358f57f", - "assets/build/ba_data/textures/operaSingerIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/45/1e/88de934268e453c77e456011a1b6", - "assets/build/ba_data/textures/operaSingerIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/2c/3e/2ec5a15e23282df2623d374766d4", - "assets/build/ba_data/textures/operaSingerIcon_preview.png": "https://files.ballistica.net/cache/ba1/de/61/b0960d2d528d6c9a09a76305ac9f", - "assets/build/ba_data/textures/ouyaAButton.dds": "https://files.ballistica.net/cache/ba1/cf/08/30e74b960a5554c966bf1cc3e21e", - "assets/build/ba_data/textures/ouyaAButton.ktx": "https://files.ballistica.net/cache/ba1/75/27/2c9ec1a1a7415cc1f69f3d5159d2", - "assets/build/ba_data/textures/ouyaAButton.pvr": "https://files.ballistica.net/cache/ba1/b0/1c/4d8b9ff84b9ab1d8f760516f3f6b", - "assets/build/ba_data/textures/ouyaAButton_preview.png": "https://files.ballistica.net/cache/ba1/56/c6/9a82eaaccee6341b7b0d04ae26c1", - "assets/build/ba_data/textures/ouyaIcon.dds": "https://files.ballistica.net/cache/ba1/ab/a9/77033a693c2a3969959a49c5e44f", - "assets/build/ba_data/textures/ouyaIcon.ktx": "https://files.ballistica.net/cache/ba1/20/25/5e42a140e2ea1e256324adac68c4", - "assets/build/ba_data/textures/ouyaIcon.pvr": "https://files.ballistica.net/cache/ba1/2b/d3/edf40ba84fdc993d531378fb8500", - "assets/build/ba_data/textures/ouyaIcon_preview.png": "https://files.ballistica.net/cache/ba1/5e/e5/8ad6c0c46ddd24cd4c3a99eb44aa", - "assets/build/ba_data/textures/ouyaOButton.dds": "https://files.ballistica.net/cache/ba1/1f/30/f24647c4a0ca09c2d1fed30f47be", - "assets/build/ba_data/textures/ouyaOButton.ktx": "https://files.ballistica.net/cache/ba1/97/e9/7a6e2393d8ae92e1e4f3d0a37e5d", - "assets/build/ba_data/textures/ouyaOButton.pvr": "https://files.ballistica.net/cache/ba1/73/20/bf23ffeb3188634648a19a50d9b6", - "assets/build/ba_data/textures/ouyaOButton_preview.png": "https://files.ballistica.net/cache/ba1/bc/96/736d157640bb8fee48930a5b1871", - "assets/build/ba_data/textures/ouyaUButton.dds": "https://files.ballistica.net/cache/ba1/a7/1c/f10466ef7d619b52283738bf4f14", - "assets/build/ba_data/textures/ouyaUButton.ktx": "https://files.ballistica.net/cache/ba1/b0/60/3a718821f6629410655c2cd4d924", - "assets/build/ba_data/textures/ouyaUButton.pvr": "https://files.ballistica.net/cache/ba1/2e/2d/494862c43fceb762966a0932a488", - "assets/build/ba_data/textures/ouyaUButton_preview.png": "https://files.ballistica.net/cache/ba1/54/61/7935f7916871a4fc57436bdae4ab", - "assets/build/ba_data/textures/ouyaYButton.dds": "https://files.ballistica.net/cache/ba1/b3/0e/ce60a3f163edd9f7d943a4a52830", - "assets/build/ba_data/textures/ouyaYButton.ktx": "https://files.ballistica.net/cache/ba1/dd/99/0a8364d55243d59174e449445c40", - "assets/build/ba_data/textures/ouyaYButton.pvr": "https://files.ballistica.net/cache/ba1/2d/f8/ebd32f8ca2f65f450641eda0e4b3", - "assets/build/ba_data/textures/ouyaYButton_preview.png": "https://files.ballistica.net/cache/ba1/a0/39/27eb09f33ca04d133a2c86d30f87", - "assets/build/ba_data/textures/penguinColor.dds": "https://files.ballistica.net/cache/ba1/46/06/98a0d4e3a61c17eabca8a5addac9", - "assets/build/ba_data/textures/penguinColor.ktx": "https://files.ballistica.net/cache/ba1/e8/a4/4bebf5671504129dd5daa25a8eaf", - "assets/build/ba_data/textures/penguinColor.pvr": "https://files.ballistica.net/cache/ba1/5c/7b/af36d581b9c5e920609861c4fab0", - "assets/build/ba_data/textures/penguinColorMask.dds": "https://files.ballistica.net/cache/ba1/ea/71/c0d2e8b03f5e02a17e884c332691", - "assets/build/ba_data/textures/penguinColorMask.ktx": "https://files.ballistica.net/cache/ba1/8a/10/60999fce550b8de1444f1d05f263", - "assets/build/ba_data/textures/penguinColorMask.pvr": "https://files.ballistica.net/cache/ba1/46/27/d3978fdfac00e02373d9298e755f", - "assets/build/ba_data/textures/penguinColorMask_preview.png": "https://files.ballistica.net/cache/ba1/a9/f6/99954cafdabea10227ac0943a206", - "assets/build/ba_data/textures/penguinColor_preview.png": "https://files.ballistica.net/cache/ba1/d5/4d/bb509ed353b4dd0e1e5231ddb59d", - "assets/build/ba_data/textures/penguinIcon.dds": "https://files.ballistica.net/cache/ba1/8b/01/d805bef78053a8bc2980fa9e987d", - "assets/build/ba_data/textures/penguinIcon.ktx": "https://files.ballistica.net/cache/ba1/5a/d8/daadfc2654dd77d31caf4e0796d0", - "assets/build/ba_data/textures/penguinIcon.pvr": "https://files.ballistica.net/cache/ba1/87/1c/dcbc92cc2fe862c43e81d587b1fe", - "assets/build/ba_data/textures/penguinIconColorMask.dds": "https://files.ballistica.net/cache/ba1/f0/0d/71a45142684d98ec7004eab7cfcd", - "assets/build/ba_data/textures/penguinIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/b2/10/73b6efac0d2df908570c53ded344", - "assets/build/ba_data/textures/penguinIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/25/26/0551afc2a52c6a6426ff3c21e4f4", - "assets/build/ba_data/textures/penguinIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/7c/0e/7df9e0157fd40450e0b5935cdc41", - "assets/build/ba_data/textures/penguinIcon_preview.png": "https://files.ballistica.net/cache/ba1/db/bf/98796d0f0d33bc200c896ba82109", - "assets/build/ba_data/textures/pixieColor.dds": "https://files.ballistica.net/cache/ba1/75/cc/389a63b73cfcbae4aed11b8b710a", - "assets/build/ba_data/textures/pixieColor.ktx": "https://files.ballistica.net/cache/ba1/23/90/c836828f9342e6bd21612ff78938", - "assets/build/ba_data/textures/pixieColor.pvr": "https://files.ballistica.net/cache/ba1/3d/ce/acca60b5cde2111bd62d90adc41e", - "assets/build/ba_data/textures/pixieColorMask.dds": "https://files.ballistica.net/cache/ba1/82/27/8c1fe9eec5b984f42fca3d3e9381", - "assets/build/ba_data/textures/pixieColorMask.ktx": "https://files.ballistica.net/cache/ba1/59/fe/138ccc612be62ef4a30b9adfbcf9", - "assets/build/ba_data/textures/pixieColorMask.pvr": "https://files.ballistica.net/cache/ba1/57/84/72b8fc64aad487d406fc876b1b42", - "assets/build/ba_data/textures/pixieColorMask_preview.png": "https://files.ballistica.net/cache/ba1/8a/7e/51ba798eac9278727df8a804ba35", - "assets/build/ba_data/textures/pixieColor_preview.png": "https://files.ballistica.net/cache/ba1/07/86/d9bab7a777b2e38107403edfcd50", - "assets/build/ba_data/textures/pixieIcon.dds": "https://files.ballistica.net/cache/ba1/68/32/7d731f50bf51e633df18254034a3", - "assets/build/ba_data/textures/pixieIcon.ktx": "https://files.ballistica.net/cache/ba1/e8/25/576bf3a4e9e2dba964c5f18b6622", - "assets/build/ba_data/textures/pixieIcon.pvr": "https://files.ballistica.net/cache/ba1/21/72/ba1ca96c4f017c11cffaec11d788", - "assets/build/ba_data/textures/pixieIconColorMask.dds": "https://files.ballistica.net/cache/ba1/3c/25/1f57bdf36447463ad1065a614c7f", - "assets/build/ba_data/textures/pixieIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/76/3f/caf7f06633052fa327db6eaa39d6", - "assets/build/ba_data/textures/pixieIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/1f/1e/da8a3c0f1a42acbe09140e29609d", - "assets/build/ba_data/textures/pixieIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/18/f4/e96160f142153a37f9e0919c883e", - "assets/build/ba_data/textures/pixieIcon_preview.png": "https://files.ballistica.net/cache/ba1/69/1d/8508a054ad2621ac40d7379e69e6", - "assets/build/ba_data/textures/playerLineup.dds": "https://files.ballistica.net/cache/ba1/0c/3a/a8b5e29db530709810235bf6919f", - "assets/build/ba_data/textures/playerLineup.ktx": "https://files.ballistica.net/cache/ba1/bb/71/f59d3e79b772ff5da78634ee306b", - "assets/build/ba_data/textures/playerLineup.pvr": "https://files.ballistica.net/cache/ba1/da/48/1aa1cb51345839048650a41f7629", - "assets/build/ba_data/textures/playerLineup_preview.png": "https://files.ballistica.net/cache/ba1/4b/b0/2bcfd64d689b0975fc32c8377e0c", - "assets/build/ba_data/textures/powerupBomb.dds": "https://files.ballistica.net/cache/ba1/c9/fe/63a3694ea2d5aaeb0fdd0b5b5e41", - "assets/build/ba_data/textures/powerupBomb.ktx": "https://files.ballistica.net/cache/ba1/1b/bc/5bf3f52c9f880476221d63a41ee1", - "assets/build/ba_data/textures/powerupBomb.pvr": "https://files.ballistica.net/cache/ba1/99/05/7144537430a306d45efbd75f733f", - "assets/build/ba_data/textures/powerupBomb_preview.png": "https://files.ballistica.net/cache/ba1/75/17/31e7f1997e0f3304b597a062efa7", - "assets/build/ba_data/textures/powerupCurse.dds": "https://files.ballistica.net/cache/ba1/6f/cd/50c78074de3f655afe1d029ffce8", - "assets/build/ba_data/textures/powerupCurse.ktx": "https://files.ballistica.net/cache/ba1/40/4f/0648e5b36ad56442fdba6e07ce6c", - "assets/build/ba_data/textures/powerupCurse.pvr": "https://files.ballistica.net/cache/ba1/31/2b/01f49d263a0915691651ae6b8196", - "assets/build/ba_data/textures/powerupCurse_preview.png": "https://files.ballistica.net/cache/ba1/07/21/962169962af41bec8c43cf3d01a1", - "assets/build/ba_data/textures/powerupHealth.dds": "https://files.ballistica.net/cache/ba1/fb/ed/5e71839b30ac420507a0e7944071", - "assets/build/ba_data/textures/powerupHealth.ktx": "https://files.ballistica.net/cache/ba1/be/3e/5bd4f2ac8d7bf972571f89992264", - "assets/build/ba_data/textures/powerupHealth.pvr": "https://files.ballistica.net/cache/ba1/64/ec/f1507c9ed94d0bd4b0c9ab8569da", - "assets/build/ba_data/textures/powerupHealth_preview.png": "https://files.ballistica.net/cache/ba1/42/11/e09c9daf2dadf44be3b9249e3fa4", - "assets/build/ba_data/textures/powerupIceBombs.dds": "https://files.ballistica.net/cache/ba1/9f/4d/af9b3707142271e4b6090c19f330", - "assets/build/ba_data/textures/powerupIceBombs.ktx": "https://files.ballistica.net/cache/ba1/27/6e/3b2b5fa2e68a073db9ed5c67da39", - "assets/build/ba_data/textures/powerupIceBombs.pvr": "https://files.ballistica.net/cache/ba1/b3/32/b9c0df53502d4402b36b8bdba951", - "assets/build/ba_data/textures/powerupIceBombs_preview.png": "https://files.ballistica.net/cache/ba1/61/8b/7f43a09b4ca30edef5099cecbbb1", - "assets/build/ba_data/textures/powerupImpactBombs.dds": "https://files.ballistica.net/cache/ba1/b2/c9/dba45ac8567744dc0be7c1eb58a3", - "assets/build/ba_data/textures/powerupImpactBombs.ktx": "https://files.ballistica.net/cache/ba1/49/0c/df4b313585f897592ccee9d5c542", - "assets/build/ba_data/textures/powerupImpactBombs.pvr": "https://files.ballistica.net/cache/ba1/f2/1b/8b6c447613d2bbec20cff630b1f1", - "assets/build/ba_data/textures/powerupImpactBombs_preview.png": "https://files.ballistica.net/cache/ba1/0c/33/5d041471446792bb5da31291b869", - "assets/build/ba_data/textures/powerupLandMines.dds": "https://files.ballistica.net/cache/ba1/99/f8/ea06e3d99018797fe82841988cce", - "assets/build/ba_data/textures/powerupLandMines.ktx": "https://files.ballistica.net/cache/ba1/79/ac/333130d040444c03fbcf7302e35b", - "assets/build/ba_data/textures/powerupLandMines.pvr": "https://files.ballistica.net/cache/ba1/2b/7a/4ac9f04cfa3a59e19fd3ddd72b2e", - "assets/build/ba_data/textures/powerupLandMines_preview.png": "https://files.ballistica.net/cache/ba1/21/bc/b619e59db015ed4cc8020972a336", - "assets/build/ba_data/textures/powerupPunch.dds": "https://files.ballistica.net/cache/ba1/3b/b7/918d87e1f8bfdf1b2b24b1f42d7d", - "assets/build/ba_data/textures/powerupPunch.ktx": "https://files.ballistica.net/cache/ba1/c6/fe/9ef3dca6a71621271fb0af12f997", - "assets/build/ba_data/textures/powerupPunch.pvr": "https://files.ballistica.net/cache/ba1/f4/a4/4af3558efd5c01c65eb4d7b48bd2", - "assets/build/ba_data/textures/powerupPunch_preview.png": "https://files.ballistica.net/cache/ba1/bb/bb/bf0d84327c82af9b5cc4fac4ef7e", - "assets/build/ba_data/textures/powerupShield.dds": "https://files.ballistica.net/cache/ba1/d5/3a/62b1c0ce13d5f05bcdc43c563490", - "assets/build/ba_data/textures/powerupShield.ktx": "https://files.ballistica.net/cache/ba1/8e/82/a0c0db06de8bfbe18e34fc0f4ffa", - "assets/build/ba_data/textures/powerupShield.pvr": "https://files.ballistica.net/cache/ba1/26/f4/4281dc052db319f01549a2b70e94", - "assets/build/ba_data/textures/powerupShield_preview.png": "https://files.ballistica.net/cache/ba1/af/4d/8b674d02851fdbd6221430d4d983", - "assets/build/ba_data/textures/powerupSpeed.dds": "https://files.ballistica.net/cache/ba1/ec/c7/1b65f2ed04112283515cc6478920", - "assets/build/ba_data/textures/powerupSpeed.ktx": "https://files.ballistica.net/cache/ba1/2e/fc/63e2a260e55d0d33ed0dc4400cb0", - "assets/build/ba_data/textures/powerupSpeed.pvr": "https://files.ballistica.net/cache/ba1/35/c1/657cc9772d575786c21d2575034d", - "assets/build/ba_data/textures/powerupSpeed_preview.png": "https://files.ballistica.net/cache/ba1/f1/2a/278da1921f939809afad254fbb03", - "assets/build/ba_data/textures/powerupStickyBombs.dds": "https://files.ballistica.net/cache/ba1/02/93/92f9be86367f44404b8257bdbf6c", - "assets/build/ba_data/textures/powerupStickyBombs.ktx": "https://files.ballistica.net/cache/ba1/fd/1b/0a64a4e0149e6c9acc3bd5710283", - "assets/build/ba_data/textures/powerupStickyBombs.pvr": "https://files.ballistica.net/cache/ba1/f5/44/ea00c858e1fdd2c73ae953b6fbe5", - "assets/build/ba_data/textures/powerupStickyBombs_preview.png": "https://files.ballistica.net/cache/ba1/76/67/7e11feb9a7f94a68be36128e9afd", - "assets/build/ba_data/textures/puckColor.dds": "https://files.ballistica.net/cache/ba1/14/5f/893c54c1ccb320272450c24d232b", - "assets/build/ba_data/textures/puckColor.ktx": "https://files.ballistica.net/cache/ba1/91/a8/49adfe0386637b17fd0d5df798d5", - "assets/build/ba_data/textures/puckColor.pvr": "https://files.ballistica.net/cache/ba1/00/54/b9d50d210bfba69387909e6e91de", - "assets/build/ba_data/textures/puckColor_preview.png": "https://files.ballistica.net/cache/ba1/bd/c4/4a3a3dcb8c1084a34479d43dcaed", - "assets/build/ba_data/textures/rampageBGColor.dds": "https://files.ballistica.net/cache/ba1/37/a1/a11281d3002afc84c3d28db58395", - "assets/build/ba_data/textures/rampageBGColor.ktx": "https://files.ballistica.net/cache/ba1/b0/39/acaf74037621c74a29a72576ee20", - "assets/build/ba_data/textures/rampageBGColor.pvr": "https://files.ballistica.net/cache/ba1/6b/9d/54521c22a5e2de98eabfb985fa85", - "assets/build/ba_data/textures/rampageBGColor2.dds": "https://files.ballistica.net/cache/ba1/08/35/b2aca6e05626aaf870cceb35ba8e", - "assets/build/ba_data/textures/rampageBGColor2.ktx": "https://files.ballistica.net/cache/ba1/60/23/d866c382fc22fdea03bf2527d147", - "assets/build/ba_data/textures/rampageBGColor2.pvr": "https://files.ballistica.net/cache/ba1/3e/69/d3c1a9d7f8910199598f010efd8a", - "assets/build/ba_data/textures/rampageBGColor2_preview.png": "https://files.ballistica.net/cache/ba1/79/f6/068a9185d32b580a4e73806d8d12", - "assets/build/ba_data/textures/rampageBGColor_preview.png": "https://files.ballistica.net/cache/ba1/38/39/0a2366c421eacc7c5a051c430e15", - "assets/build/ba_data/textures/rampageLevelColor.dds": "https://files.ballistica.net/cache/ba1/fe/40/41c878b5db3ebe47686d8a605cf6", - "assets/build/ba_data/textures/rampageLevelColor.ktx": "https://files.ballistica.net/cache/ba1/ef/06/d2ac2ec0f775180b62e341a5890f", - "assets/build/ba_data/textures/rampageLevelColor.pvr": "https://files.ballistica.net/cache/ba1/8a/74/ff813c81875c81cd8ccd345c5a27", - "assets/build/ba_data/textures/rampageLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/b9/db/0920892160b677dac33844acb9e3", - "assets/build/ba_data/textures/rampagePreview.dds": "https://files.ballistica.net/cache/ba1/15/af/3089f17f4e969b3780c92ef1ba58", - "assets/build/ba_data/textures/rampagePreview.ktx": "https://files.ballistica.net/cache/ba1/b3/80/783d2507b45af55ebe7d9e4aa7b1", - "assets/build/ba_data/textures/rampagePreview.pvr": "https://files.ballistica.net/cache/ba1/97/d2/99028f0da86ef4d61a70da709c29", - "assets/build/ba_data/textures/rampagePreview_preview.png": "https://files.ballistica.net/cache/ba1/4f/64/6469bc8cfe52cdd9590c1edbc89c", - "assets/build/ba_data/textures/reflectionChar_+x.dds": "https://files.ballistica.net/cache/ba1/5b/6d/76948a60ad687d91d4db8696ff35", - "assets/build/ba_data/textures/reflectionChar_+x.ktx": "https://files.ballistica.net/cache/ba1/d2/31/6eb2af3d54a5a9b194f5fd5a0cd7", - "assets/build/ba_data/textures/reflectionChar_+x.pvr": "https://files.ballistica.net/cache/ba1/45/72/1bf60fc63ee6024e2c62026e1d13", - "assets/build/ba_data/textures/reflectionChar_+x_preview.png": "https://files.ballistica.net/cache/ba1/3e/19/1545732b54357b959468baeadb05", - "assets/build/ba_data/textures/reflectionChar_+y.dds": "https://files.ballistica.net/cache/ba1/fd/31/fd0b92139768e26d31c26907809e", - "assets/build/ba_data/textures/reflectionChar_+y.ktx": "https://files.ballistica.net/cache/ba1/ca/e6/faa2b43131568d419fcdb9e11805", - "assets/build/ba_data/textures/reflectionChar_+y.pvr": "https://files.ballistica.net/cache/ba1/22/64/8713ed6cb45016a93c67650b6fa0", - "assets/build/ba_data/textures/reflectionChar_+y_preview.png": "https://files.ballistica.net/cache/ba1/38/7e/8e1d596068bc9a4527fc92d3f6f9", - "assets/build/ba_data/textures/reflectionChar_+z.dds": "https://files.ballistica.net/cache/ba1/f1/36/289bb673b07c3778dc59904d768c", - "assets/build/ba_data/textures/reflectionChar_+z.ktx": "https://files.ballistica.net/cache/ba1/dc/23/bc1efe8d9ef05512d2fddfcf6500", - "assets/build/ba_data/textures/reflectionChar_+z.pvr": "https://files.ballistica.net/cache/ba1/2e/f7/06d8945c252c76c071514bb13f5f", - "assets/build/ba_data/textures/reflectionChar_+z_preview.png": "https://files.ballistica.net/cache/ba1/6a/f6/1f5096572ded42d43788bf450d81", - "assets/build/ba_data/textures/reflectionChar_-x.dds": "https://files.ballistica.net/cache/ba1/ec/f2/aa706d24766b52454ee37f225d62", - "assets/build/ba_data/textures/reflectionChar_-x.ktx": "https://files.ballistica.net/cache/ba1/7b/a7/6592e876e7c31687e11e019a305d", - "assets/build/ba_data/textures/reflectionChar_-x.pvr": "https://files.ballistica.net/cache/ba1/88/fb/372bde1b68e238da396a7576088f", - "assets/build/ba_data/textures/reflectionChar_-x_preview.png": "https://files.ballistica.net/cache/ba1/81/8f/422c1c54b3021abc70a4aa64b3fd", - "assets/build/ba_data/textures/reflectionChar_-y.dds": "https://files.ballistica.net/cache/ba1/25/03/628a5da0305261041300f75ef756", - "assets/build/ba_data/textures/reflectionChar_-y.ktx": "https://files.ballistica.net/cache/ba1/25/1f/4e5b9240dcf6ea6639fb81f3579a", - "assets/build/ba_data/textures/reflectionChar_-y.pvr": "https://files.ballistica.net/cache/ba1/4e/ee/7cdca1ae6b1458a443262c5a4e9c", - "assets/build/ba_data/textures/reflectionChar_-y_preview.png": "https://files.ballistica.net/cache/ba1/ba/70/f2f188b95e2735467e823c94504a", - "assets/build/ba_data/textures/reflectionChar_-z.dds": "https://files.ballistica.net/cache/ba1/b3/c9/dffe62c38d26de73df27ee0c5978", - "assets/build/ba_data/textures/reflectionChar_-z.ktx": "https://files.ballistica.net/cache/ba1/96/56/0f9aef42c876c3d2832ba570de79", - "assets/build/ba_data/textures/reflectionChar_-z.pvr": "https://files.ballistica.net/cache/ba1/60/68/cb7fefc89119f14265c58200afde", - "assets/build/ba_data/textures/reflectionChar_-z_preview.png": "https://files.ballistica.net/cache/ba1/b5/10/70b59c9cd551b44f8422867a38c2", - "assets/build/ba_data/textures/reflectionPowerup_+x.dds": "https://files.ballistica.net/cache/ba1/4b/3d/76538e3f18edb2a373bb3b591314", - "assets/build/ba_data/textures/reflectionPowerup_+x.ktx": "https://files.ballistica.net/cache/ba1/a6/9b/682e676c993ae660475b4467fa6a", - "assets/build/ba_data/textures/reflectionPowerup_+x.pvr": "https://files.ballistica.net/cache/ba1/f9/45/2b58be3bf14314607a25143fb477", - "assets/build/ba_data/textures/reflectionPowerup_+x_preview.png": "https://files.ballistica.net/cache/ba1/66/cc/5021c0239527286f8c8f63aad21a", - "assets/build/ba_data/textures/reflectionPowerup_+y.dds": "https://files.ballistica.net/cache/ba1/a2/5e/7f4c545969afdc667bace945c626", - "assets/build/ba_data/textures/reflectionPowerup_+y.ktx": "https://files.ballistica.net/cache/ba1/37/05/8c7cfcfe1fcd01708853ffe6c59c", - "assets/build/ba_data/textures/reflectionPowerup_+y.pvr": "https://files.ballistica.net/cache/ba1/d6/77/baa16732e3e3c39e8c014e9ec114", - "assets/build/ba_data/textures/reflectionPowerup_+y_preview.png": "https://files.ballistica.net/cache/ba1/56/d2/117f205246d5cbbe0ecb8de7c72d", - "assets/build/ba_data/textures/reflectionPowerup_+z.dds": "https://files.ballistica.net/cache/ba1/3a/1e/e4c31ca8164064de3ceb5ffd6082", - "assets/build/ba_data/textures/reflectionPowerup_+z.ktx": "https://files.ballistica.net/cache/ba1/80/ec/f15758f7b4fd8ab758f545bb70f4", - "assets/build/ba_data/textures/reflectionPowerup_+z.pvr": "https://files.ballistica.net/cache/ba1/93/17/c73728bc925830830d5b9705d370", - "assets/build/ba_data/textures/reflectionPowerup_+z_preview.png": "https://files.ballistica.net/cache/ba1/d1/86/881cf0b330b75e7eb99433232f7f", - "assets/build/ba_data/textures/reflectionPowerup_-x.dds": "https://files.ballistica.net/cache/ba1/37/35/daf5ab77f00a65c49571088876cd", - "assets/build/ba_data/textures/reflectionPowerup_-x.ktx": "https://files.ballistica.net/cache/ba1/37/f2/a1c931a9035fe4181dea9324bc67", - "assets/build/ba_data/textures/reflectionPowerup_-x.pvr": "https://files.ballistica.net/cache/ba1/df/1f/354844bf13241972966a812e9628", - "assets/build/ba_data/textures/reflectionPowerup_-x_preview.png": "https://files.ballistica.net/cache/ba1/10/f6/55fd2539f303d0e32dfcb4327f7b", - "assets/build/ba_data/textures/reflectionPowerup_-y.dds": "https://files.ballistica.net/cache/ba1/e5/65/19b710093067f7534d598177dfed", - "assets/build/ba_data/textures/reflectionPowerup_-y.ktx": "https://files.ballistica.net/cache/ba1/18/11/f0cd7ee2426de574d7f203e52dc0", - "assets/build/ba_data/textures/reflectionPowerup_-y.pvr": "https://files.ballistica.net/cache/ba1/f7/7d/9dfe6ca108a76252f004bd082365", - "assets/build/ba_data/textures/reflectionPowerup_-y_preview.png": "https://files.ballistica.net/cache/ba1/44/94/82e4a0e30a92545d5d9e4eeef459", - "assets/build/ba_data/textures/reflectionPowerup_-z.dds": "https://files.ballistica.net/cache/ba1/48/70/fbc7b6a46abbae8f9f5f3bb9575a", - "assets/build/ba_data/textures/reflectionPowerup_-z.ktx": "https://files.ballistica.net/cache/ba1/9e/5b/0f56f679dbfa594c7a7d3ecad89b", - "assets/build/ba_data/textures/reflectionPowerup_-z.pvr": "https://files.ballistica.net/cache/ba1/df/a7/a93e173a8d969ea9b2982540fff4", - "assets/build/ba_data/textures/reflectionPowerup_-z_preview.png": "https://files.ballistica.net/cache/ba1/af/2d/ca5bd32fb36746e9adb02ef4ecc2", - "assets/build/ba_data/textures/reflectionSharp_+x.dds": "https://files.ballistica.net/cache/ba1/8d/11/4bb36f619c98bedd1ce6df6de207", - "assets/build/ba_data/textures/reflectionSharp_+x.ktx": "https://files.ballistica.net/cache/ba1/9c/35/fd637b51757b550f420cebb996e3", - "assets/build/ba_data/textures/reflectionSharp_+x.pvr": "https://files.ballistica.net/cache/ba1/c6/01/9352c2b1e183f5239c925841dc2a", - "assets/build/ba_data/textures/reflectionSharp_+x_preview.png": "https://files.ballistica.net/cache/ba1/b0/2c/8c98cdd9aa750f9f9d3df49f96aa", - "assets/build/ba_data/textures/reflectionSharp_+y.dds": "https://files.ballistica.net/cache/ba1/c1/a0/c28fcb31236e079a394d12c36a5f", - "assets/build/ba_data/textures/reflectionSharp_+y.ktx": "https://files.ballistica.net/cache/ba1/2c/a5/141e266b590226ee4a3474ac874e", - "assets/build/ba_data/textures/reflectionSharp_+y.pvr": "https://files.ballistica.net/cache/ba1/a5/10/cdd96a99404584f090032ca60e62", - "assets/build/ba_data/textures/reflectionSharp_+y_preview.png": "https://files.ballistica.net/cache/ba1/0f/58/f04fac2b705b92c8b0e11036a8ff", - "assets/build/ba_data/textures/reflectionSharp_+z.dds": "https://files.ballistica.net/cache/ba1/2a/be/a4ec2fb348452797276aa8a7ea52", - "assets/build/ba_data/textures/reflectionSharp_+z.ktx": "https://files.ballistica.net/cache/ba1/21/2c/51df5254932dac26b7068bc1e044", - "assets/build/ba_data/textures/reflectionSharp_+z.pvr": "https://files.ballistica.net/cache/ba1/e4/87/2eaf17f6a15512b4326c7a2ae395", - "assets/build/ba_data/textures/reflectionSharp_+z_preview.png": "https://files.ballistica.net/cache/ba1/1f/14/cd9fa509c22fea0f73a5eb76ac54", - "assets/build/ba_data/textures/reflectionSharp_-x.dds": "https://files.ballistica.net/cache/ba1/8f/2f/def7cd6049055701946f6ba7efcb", - "assets/build/ba_data/textures/reflectionSharp_-x.ktx": "https://files.ballistica.net/cache/ba1/04/cf/03b8b252c6d0a14b4a74df1697b2", - "assets/build/ba_data/textures/reflectionSharp_-x.pvr": "https://files.ballistica.net/cache/ba1/f3/5b/36f37a9ca99d555c8950524ca118", - "assets/build/ba_data/textures/reflectionSharp_-x_preview.png": "https://files.ballistica.net/cache/ba1/80/18/f2f8c9ce1f0dc8f297e0b29def1b", - "assets/build/ba_data/textures/reflectionSharp_-y.dds": "https://files.ballistica.net/cache/ba1/a2/57/1e8c7061bc1b03b7ffee5afcdb2d", - "assets/build/ba_data/textures/reflectionSharp_-y.ktx": "https://files.ballistica.net/cache/ba1/cb/54/3a5f84df622046484f39404d7f21", - "assets/build/ba_data/textures/reflectionSharp_-y.pvr": "https://files.ballistica.net/cache/ba1/48/eb/957459860d397758e50492e248e8", - "assets/build/ba_data/textures/reflectionSharp_-y_preview.png": "https://files.ballistica.net/cache/ba1/a6/cf/77325e3e26738b5db479324f4a2e", - "assets/build/ba_data/textures/reflectionSharp_-z.dds": "https://files.ballistica.net/cache/ba1/4f/0c/e24aa8048e5d8d62cd47f48a4ca2", - "assets/build/ba_data/textures/reflectionSharp_-z.ktx": "https://files.ballistica.net/cache/ba1/09/79/7c34e93c1453fa653098644dc902", - "assets/build/ba_data/textures/reflectionSharp_-z.pvr": "https://files.ballistica.net/cache/ba1/08/d2/fa84664b3558ce68269581d52ab3", - "assets/build/ba_data/textures/reflectionSharp_-z_preview.png": "https://files.ballistica.net/cache/ba1/e0/6e/de9a4cfdcb483bf92a63ac64e435", - "assets/build/ba_data/textures/reflectionSharper_+x.dds": "https://files.ballistica.net/cache/ba1/ed/05/68ad6e013833ba230bee61037591", - "assets/build/ba_data/textures/reflectionSharper_+x.ktx": "https://files.ballistica.net/cache/ba1/ab/9b/243e80e52d6f837f206472054489", - "assets/build/ba_data/textures/reflectionSharper_+x.pvr": "https://files.ballistica.net/cache/ba1/70/29/f1847b9d9c4892e3d383c8136b28", - "assets/build/ba_data/textures/reflectionSharper_+x_preview.png": "https://files.ballistica.net/cache/ba1/22/db/1afb62440951a98fcf57663d5884", - "assets/build/ba_data/textures/reflectionSharper_+y.dds": "https://files.ballistica.net/cache/ba1/f1/f7/1b17c9ebe9a881936666433eed27", - "assets/build/ba_data/textures/reflectionSharper_+y.ktx": "https://files.ballistica.net/cache/ba1/e9/bd/eb4084babeada5804cade1a3580d", - "assets/build/ba_data/textures/reflectionSharper_+y.pvr": "https://files.ballistica.net/cache/ba1/7d/2c/262c842aedcf93e5ed23c7c07cc3", - "assets/build/ba_data/textures/reflectionSharper_+y_preview.png": "https://files.ballistica.net/cache/ba1/25/c4/d112b3d51a2a29b05975b8deb606", - "assets/build/ba_data/textures/reflectionSharper_+z.dds": "https://files.ballistica.net/cache/ba1/54/f0/c26df410c19770e458a51aba67b0", - "assets/build/ba_data/textures/reflectionSharper_+z.ktx": "https://files.ballistica.net/cache/ba1/b7/74/06d4ba338e40f9de58f3d1ebf60c", - "assets/build/ba_data/textures/reflectionSharper_+z.pvr": "https://files.ballistica.net/cache/ba1/7c/81/66ce270040a7f9b9281a98b23cd7", - "assets/build/ba_data/textures/reflectionSharper_+z_preview.png": "https://files.ballistica.net/cache/ba1/eb/03/94a437173ef4b65e8290c19d51c9", - "assets/build/ba_data/textures/reflectionSharper_-x.dds": "https://files.ballistica.net/cache/ba1/a2/13/43f58a06c021f325162420549182", - "assets/build/ba_data/textures/reflectionSharper_-x.ktx": "https://files.ballistica.net/cache/ba1/d6/c4/d160cae43319dd50e691ad531198", - "assets/build/ba_data/textures/reflectionSharper_-x.pvr": "https://files.ballistica.net/cache/ba1/6b/46/9661bbf39e793b4a465a0dd93aac", - "assets/build/ba_data/textures/reflectionSharper_-x_preview.png": "https://files.ballistica.net/cache/ba1/b4/44/7052d607b344dee0b8bc72650521", - "assets/build/ba_data/textures/reflectionSharper_-y.dds": "https://files.ballistica.net/cache/ba1/95/85/7aef8657eca1ea7ece33aa7207a4", - "assets/build/ba_data/textures/reflectionSharper_-y.ktx": "https://files.ballistica.net/cache/ba1/6f/ac/3a7e188caf86c02b002124103c2c", - "assets/build/ba_data/textures/reflectionSharper_-y.pvr": "https://files.ballistica.net/cache/ba1/01/92/cf6f8cfea73d43c8eeec431d3c1e", - "assets/build/ba_data/textures/reflectionSharper_-y_preview.png": "https://files.ballistica.net/cache/ba1/6d/f4/0d3e1e51d5628481c43494b92834", - "assets/build/ba_data/textures/reflectionSharper_-z.dds": "https://files.ballistica.net/cache/ba1/fd/1e/b926e166ee4a099b85569ae6132a", - "assets/build/ba_data/textures/reflectionSharper_-z.ktx": "https://files.ballistica.net/cache/ba1/8a/ee/fdf7edf9161ccdfb3c7295a734ef", - "assets/build/ba_data/textures/reflectionSharper_-z.pvr": "https://files.ballistica.net/cache/ba1/03/1e/e4c38f9e8adeda5ac56d4a7d88c9", - "assets/build/ba_data/textures/reflectionSharper_-z_preview.png": "https://files.ballistica.net/cache/ba1/67/8f/5c3a0d7c4496c42b21a1900e2a86", - "assets/build/ba_data/textures/reflectionSharpest_+x.dds": "https://files.ballistica.net/cache/ba1/17/44/0f4e24def4a0896592f0afff49e4", - "assets/build/ba_data/textures/reflectionSharpest_+x.ktx": "https://files.ballistica.net/cache/ba1/52/26/a205c298acfb396559acf7fbd28f", - "assets/build/ba_data/textures/reflectionSharpest_+x.pvr": "https://files.ballistica.net/cache/ba1/53/fb/d812f17a354a24b4b9191bf8f5d7", - "assets/build/ba_data/textures/reflectionSharpest_+x_preview.png": "https://files.ballistica.net/cache/ba1/07/be/5bca6066c0973702683ec0a99330", - "assets/build/ba_data/textures/reflectionSharpest_+y.dds": "https://files.ballistica.net/cache/ba1/ff/81/9a178345d2eed88c90bb69093f5b", - "assets/build/ba_data/textures/reflectionSharpest_+y.ktx": "https://files.ballistica.net/cache/ba1/88/73/328a52723fd9b9f977bc1e315239", - "assets/build/ba_data/textures/reflectionSharpest_+y.pvr": "https://files.ballistica.net/cache/ba1/f9/1a/9e683aa5fbf37c9a0298010de27b", - "assets/build/ba_data/textures/reflectionSharpest_+y_preview.png": "https://files.ballistica.net/cache/ba1/a5/11/31293f29f0e24c3d3dfac5e9ebfe", - "assets/build/ba_data/textures/reflectionSharpest_+z.dds": "https://files.ballistica.net/cache/ba1/51/7f/2e3707014a13f7875ade21e14442", - "assets/build/ba_data/textures/reflectionSharpest_+z.ktx": "https://files.ballistica.net/cache/ba1/1c/7c/a0e34522c75d1a3f758d6c750490", - "assets/build/ba_data/textures/reflectionSharpest_+z.pvr": "https://files.ballistica.net/cache/ba1/b9/35/fcf58878dd114712af9c76062b57", - "assets/build/ba_data/textures/reflectionSharpest_+z_preview.png": "https://files.ballistica.net/cache/ba1/ca/bd/573ee0db128781842ae9175bc68b", - "assets/build/ba_data/textures/reflectionSharpest_-x.dds": "https://files.ballistica.net/cache/ba1/8c/79/b5c2e9d321f91aaeced39fb3298f", - "assets/build/ba_data/textures/reflectionSharpest_-x.ktx": "https://files.ballistica.net/cache/ba1/bf/46/c66d2f9e1b0a419f415b89c111e8", - "assets/build/ba_data/textures/reflectionSharpest_-x.pvr": "https://files.ballistica.net/cache/ba1/2b/25/52ecf66c7bb04d938204c2b5991e", - "assets/build/ba_data/textures/reflectionSharpest_-x_preview.png": "https://files.ballistica.net/cache/ba1/62/57/327933d1145709c17ab13a7903b0", - "assets/build/ba_data/textures/reflectionSharpest_-y.dds": "https://files.ballistica.net/cache/ba1/b0/9f/bf10bd449682084e9ca121b7e79c", - "assets/build/ba_data/textures/reflectionSharpest_-y.ktx": "https://files.ballistica.net/cache/ba1/e7/95/1a8b8ef2b1232e03bed5d8fee261", - "assets/build/ba_data/textures/reflectionSharpest_-y.pvr": "https://files.ballistica.net/cache/ba1/8d/20/f29d7e44570fe26189eabd4886fb", - "assets/build/ba_data/textures/reflectionSharpest_-y_preview.png": "https://files.ballistica.net/cache/ba1/15/1b/4d2181bb165063f6e12f97f305b2", - "assets/build/ba_data/textures/reflectionSharpest_-z.dds": "https://files.ballistica.net/cache/ba1/97/95/f7c7c436daa327b86416182cd008", - "assets/build/ba_data/textures/reflectionSharpest_-z.ktx": "https://files.ballistica.net/cache/ba1/b6/c5/1d465cc976b44b0bbaec7970c7b7", - "assets/build/ba_data/textures/reflectionSharpest_-z.pvr": "https://files.ballistica.net/cache/ba1/ab/96/fbaf0c6cade6e062d3c5a6c7af77", - "assets/build/ba_data/textures/reflectionSharpest_-z_preview.png": "https://files.ballistica.net/cache/ba1/70/7e/5e7a77571e140d18a2ca283bd9f4", - "assets/build/ba_data/textures/reflectionSoft_+x.dds": "https://files.ballistica.net/cache/ba1/1a/18/1be4680a8e1840307030430d6523", - "assets/build/ba_data/textures/reflectionSoft_+x.ktx": "https://files.ballistica.net/cache/ba1/66/d1/69ba40388be73c520a3fb075a456", - "assets/build/ba_data/textures/reflectionSoft_+x.pvr": "https://files.ballistica.net/cache/ba1/c1/8b/abc031f2aeee298e62218704a510", - "assets/build/ba_data/textures/reflectionSoft_+x_preview.png": "https://files.ballistica.net/cache/ba1/37/c3/db59fbc767c72d92ab97409cc5e1", - "assets/build/ba_data/textures/reflectionSoft_+y.dds": "https://files.ballistica.net/cache/ba1/8a/ff/30ee20fa5f94290a8bf6b40340d7", - "assets/build/ba_data/textures/reflectionSoft_+y.ktx": "https://files.ballistica.net/cache/ba1/6e/40/24e61248fb9b3342fd4a17af345f", - "assets/build/ba_data/textures/reflectionSoft_+y.pvr": "https://files.ballistica.net/cache/ba1/07/df/de5b19dcabf072a6de0970721871", - "assets/build/ba_data/textures/reflectionSoft_+y_preview.png": "https://files.ballistica.net/cache/ba1/3f/69/034beea8a394d163eac57dabb103", - "assets/build/ba_data/textures/reflectionSoft_+z.dds": "https://files.ballistica.net/cache/ba1/bf/f7/f5f65b434feb487fc6712e4d4ddd", - "assets/build/ba_data/textures/reflectionSoft_+z.ktx": "https://files.ballistica.net/cache/ba1/69/e3/7a6ae07ad6b9e50b278f760dea72", - "assets/build/ba_data/textures/reflectionSoft_+z.pvr": "https://files.ballistica.net/cache/ba1/ca/b1/2b331f5a2afd9928b411edbf4be7", - "assets/build/ba_data/textures/reflectionSoft_+z_preview.png": "https://files.ballistica.net/cache/ba1/85/63/3cf7229f4dcbd960498b0605883a", - "assets/build/ba_data/textures/reflectionSoft_-x.dds": "https://files.ballistica.net/cache/ba1/27/ee/af69589b4af6d9b0832a68249224", - "assets/build/ba_data/textures/reflectionSoft_-x.ktx": "https://files.ballistica.net/cache/ba1/67/22/3bbe97760bfca0fcaee114ba6bcf", - "assets/build/ba_data/textures/reflectionSoft_-x.pvr": "https://files.ballistica.net/cache/ba1/47/f7/510f7979be0e686007e28c0ec60e", - "assets/build/ba_data/textures/reflectionSoft_-x_preview.png": "https://files.ballistica.net/cache/ba1/4d/2e/0e35cdf46b8347bc620c53e935a7", - "assets/build/ba_data/textures/reflectionSoft_-y.dds": "https://files.ballistica.net/cache/ba1/b6/6c/4f2fd95892eb67be6cddbe2d1a5f", - "assets/build/ba_data/textures/reflectionSoft_-y.ktx": "https://files.ballistica.net/cache/ba1/c7/af/7e09853ae6f0487e3a4122ac31d5", - "assets/build/ba_data/textures/reflectionSoft_-y.pvr": "https://files.ballistica.net/cache/ba1/80/81/e5428d62dd5a31f708a68b258366", - "assets/build/ba_data/textures/reflectionSoft_-y_preview.png": "https://files.ballistica.net/cache/ba1/2f/e1/b0cee0f161188752abcba04c9291", - "assets/build/ba_data/textures/reflectionSoft_-z.dds": "https://files.ballistica.net/cache/ba1/14/be/6bab80deb23a5105dd1ddd57740b", - "assets/build/ba_data/textures/reflectionSoft_-z.ktx": "https://files.ballistica.net/cache/ba1/55/b2/45ec181ec07cd28be1a089b6bb79", - "assets/build/ba_data/textures/reflectionSoft_-z.pvr": "https://files.ballistica.net/cache/ba1/67/6e/271b323497b797718b7d9bd3f939", - "assets/build/ba_data/textures/reflectionSoft_-z_preview.png": "https://files.ballistica.net/cache/ba1/6f/38/dce6d5c26bc45005b9355b030c94", - "assets/build/ba_data/textures/replayIcon.dds": "https://files.ballistica.net/cache/ba1/43/34/02394e8694a01522b45033b487ff", - "assets/build/ba_data/textures/replayIcon.ktx": "https://files.ballistica.net/cache/ba1/14/da/81114669b844b40303806265f2f0", - "assets/build/ba_data/textures/replayIcon.pvr": "https://files.ballistica.net/cache/ba1/46/ef/5b515de5bf82ab8615db9b447b31", - "assets/build/ba_data/textures/replayIcon_preview.png": "https://files.ballistica.net/cache/ba1/d4/96/581653f1f55f7e2ab843e64fb0f7", - "assets/build/ba_data/textures/rgbStripes.dds": "https://files.ballistica.net/cache/ba1/a2/10/d7b6b75f5d39273521846074dd8c", - "assets/build/ba_data/textures/rgbStripes.ktx": "https://files.ballistica.net/cache/ba1/ee/c9/ebcb759bc7ba6cec340e872729b6", - "assets/build/ba_data/textures/rgbStripes.pvr": "https://files.ballistica.net/cache/ba1/9c/67/efacc6e2fd0b60aee7060c84abd9", - "assets/build/ba_data/textures/rgbStripes_preview.png": "https://files.ballistica.net/cache/ba1/00/65/28c41d12263eb1602dc7fe110039", - "assets/build/ba_data/textures/rightButton.dds": "https://files.ballistica.net/cache/ba1/dd/fa/b985c3e01dd9ba92aa7c58dc12c0", - "assets/build/ba_data/textures/rightButton.ktx": "https://files.ballistica.net/cache/ba1/42/a1/254b87902fd9a98ec882d97f1ee6", - "assets/build/ba_data/textures/rightButton.pvr": "https://files.ballistica.net/cache/ba1/3d/87/7a1a1134f1bb67c78883d542d622", - "assets/build/ba_data/textures/rightButton_preview.png": "https://files.ballistica.net/cache/ba1/73/f7/c5d247f3dd191d070d4dad6ae1d4", - "assets/build/ba_data/textures/robotColor.dds": "https://files.ballistica.net/cache/ba1/a3/c0/2f7089d363c3e22b4ab10cd1c589", - "assets/build/ba_data/textures/robotColor.ktx": "https://files.ballistica.net/cache/ba1/9a/9d/dbdec15776fd014e03eb1808ad28", - "assets/build/ba_data/textures/robotColor.pvr": "https://files.ballistica.net/cache/ba1/9d/24/cc381a8ff6e80947618a97d36107", - "assets/build/ba_data/textures/robotColorMask.dds": "https://files.ballistica.net/cache/ba1/ec/0e/d1b4b152415679464cd0bf773b94", - "assets/build/ba_data/textures/robotColorMask.ktx": "https://files.ballistica.net/cache/ba1/19/4e/6bf15a0ac4ba4ad3bedd5eb3a3f3", - "assets/build/ba_data/textures/robotColorMask.pvr": "https://files.ballistica.net/cache/ba1/ee/88/47d89b21335c4ba8916ff4eb11fd", - "assets/build/ba_data/textures/robotColorMask_preview.png": "https://files.ballistica.net/cache/ba1/03/fd/c13045e5beed778e19f9a70f81ea", - "assets/build/ba_data/textures/robotColor_preview.png": "https://files.ballistica.net/cache/ba1/0d/e9/682a50a2ce23c9b452ecc9d26560", - "assets/build/ba_data/textures/robotIcon.dds": "https://files.ballistica.net/cache/ba1/dc/7f/3b863a323abb6e0cbee247a25ebb", - "assets/build/ba_data/textures/robotIcon.ktx": "https://files.ballistica.net/cache/ba1/0a/1c/3f7d51624ac9509dbd4670941610", - "assets/build/ba_data/textures/robotIcon.pvr": "https://files.ballistica.net/cache/ba1/4d/e4/58246ebff305173b97f05d3c2ec8", - "assets/build/ba_data/textures/robotIconColorMask.dds": "https://files.ballistica.net/cache/ba1/e4/83/39a6d429498d748990c386c3019b", - "assets/build/ba_data/textures/robotIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/30/20/af96df8d4fc271ce3ee630a0895c", - "assets/build/ba_data/textures/robotIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/b8/65/40642024959c225ce4704b954f8f", - "assets/build/ba_data/textures/robotIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/3a/c2/771b5f4aa159f61383b8bbefa291", - "assets/build/ba_data/textures/robotIcon_preview.png": "https://files.ballistica.net/cache/ba1/7a/b8/1dad64c97fa4655f70490004cedc", - "assets/build/ba_data/textures/roundaboutLevelColor.dds": "https://files.ballistica.net/cache/ba1/69/bd/5a0e33f022abb454eebf4b9668ab", - "assets/build/ba_data/textures/roundaboutLevelColor.ktx": "https://files.ballistica.net/cache/ba1/19/f8/d8ea41097c6dfe7bec255d85ea0a", - "assets/build/ba_data/textures/roundaboutLevelColor.pvr": "https://files.ballistica.net/cache/ba1/0a/5a/19e1f33492b7ae2a9f100cf26fff", - "assets/build/ba_data/textures/roundaboutLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/18/c7/7dbe95a06d9e877fbe848f415141", - "assets/build/ba_data/textures/roundaboutPreview.dds": "https://files.ballistica.net/cache/ba1/a8/c2/d800722b8b7e172fcf19227abf05", - "assets/build/ba_data/textures/roundaboutPreview.ktx": "https://files.ballistica.net/cache/ba1/1f/63/e4776ec44e00a0d736aec31f1dfb", - "assets/build/ba_data/textures/roundaboutPreview.pvr": "https://files.ballistica.net/cache/ba1/65/1a/019c03931b513047b9a5fcb7e8af", - "assets/build/ba_data/textures/roundaboutPreview_preview.png": "https://files.ballistica.net/cache/ba1/d4/df/d26e050aac7285a6f453185f0046", - "assets/build/ba_data/textures/santaColor.dds": "https://files.ballistica.net/cache/ba1/5f/a1/91e228dda47c28f430ec2ff226b8", - "assets/build/ba_data/textures/santaColor.ktx": "https://files.ballistica.net/cache/ba1/cc/f4/de2aaaf8cc855afaaceb1e618409", - "assets/build/ba_data/textures/santaColor.pvr": "https://files.ballistica.net/cache/ba1/b6/22/85dfce13ccc2a531a42a91d4dd89", - "assets/build/ba_data/textures/santaColorMask.dds": "https://files.ballistica.net/cache/ba1/8a/9c/db332211cdc1deb5d60ce035d5a8", - "assets/build/ba_data/textures/santaColorMask.ktx": "https://files.ballistica.net/cache/ba1/b7/b0/9ebd903f0d9e491fa6a65181de19", - "assets/build/ba_data/textures/santaColorMask.pvr": "https://files.ballistica.net/cache/ba1/a8/8c/6ab8ad9a2ea2e907635286163e57", - "assets/build/ba_data/textures/santaColorMask_preview.png": "https://files.ballistica.net/cache/ba1/01/c4/436aca37ae86e73db9b7c856056a", - "assets/build/ba_data/textures/santaColor_preview.png": "https://files.ballistica.net/cache/ba1/7c/76/77d68b3c0127948c6ad22a76b61b", - "assets/build/ba_data/textures/santaIcon.dds": "https://files.ballistica.net/cache/ba1/50/10/67dc3f02df57883342cc345cf72e", - "assets/build/ba_data/textures/santaIcon.ktx": "https://files.ballistica.net/cache/ba1/b7/6c/f22cde80b89403aeb4e950d8e2d2", - "assets/build/ba_data/textures/santaIcon.pvr": "https://files.ballistica.net/cache/ba1/a0/4c/24e260da5e86b10a86049503b986", - "assets/build/ba_data/textures/santaIconColorMask.dds": "https://files.ballistica.net/cache/ba1/9e/36/dbeba27f8008aa3404864070f5bc", - "assets/build/ba_data/textures/santaIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/32/7a/c2d2cc16c7be82dbcff72a376f70", - "assets/build/ba_data/textures/santaIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/25/db/3dfff995ae0b87511a6bb7cc8b79", - "assets/build/ba_data/textures/santaIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/ca/f2/a9f44952811c708bd50525d4da30", - "assets/build/ba_data/textures/santaIcon_preview.png": "https://files.ballistica.net/cache/ba1/f4/bd/727d788b339822959eef8ea72b27", - "assets/build/ba_data/textures/scorch.dds": "https://files.ballistica.net/cache/ba1/f4/c0/8621a536f0e83f4b648d70797ffd", - "assets/build/ba_data/textures/scorch.ktx": "https://files.ballistica.net/cache/ba1/ef/bd/3e003dbc735890b066abc0974a09", - "assets/build/ba_data/textures/scorch.pvr": "https://files.ballistica.net/cache/ba1/94/cf/788d8e6fbffce922ef9b6f5afc26", - "assets/build/ba_data/textures/scorchBig.dds": "https://files.ballistica.net/cache/ba1/03/5f/83268fe9d027a53c99a80691cea8", - "assets/build/ba_data/textures/scorchBig.ktx": "https://files.ballistica.net/cache/ba1/21/e8/c6a1f4916a31daf84c7fa8f5c9e6", - "assets/build/ba_data/textures/scorchBig.pvr": "https://files.ballistica.net/cache/ba1/79/6d/f8f707a58c3170a8bac6da6914a2", - "assets/build/ba_data/textures/scorchBig_preview.png": "https://files.ballistica.net/cache/ba1/71/ed/b36c3e8416f81b5078b25a44e915", - "assets/build/ba_data/textures/scorch_preview.png": "https://files.ballistica.net/cache/ba1/6a/59/97cf3dc2f3831c8a02b084bfccfb", - "assets/build/ba_data/textures/scrollWidget.dds": "https://files.ballistica.net/cache/ba1/3c/67/44d52b736dd880f765efd0be8015", - "assets/build/ba_data/textures/scrollWidget.ktx": "https://files.ballistica.net/cache/ba1/ef/25/428f343cf6d6741f16053fd91381", - "assets/build/ba_data/textures/scrollWidget.pvr": "https://files.ballistica.net/cache/ba1/31/83/27985545a5bff8f0625904d64404", - "assets/build/ba_data/textures/scrollWidgetGlow.dds": "https://files.ballistica.net/cache/ba1/8f/d0/5769eefbf3be5a73b28f968d3bb9", - "assets/build/ba_data/textures/scrollWidgetGlow.ktx": "https://files.ballistica.net/cache/ba1/95/35/1f6462e42c0e12c2abeb03974116", - "assets/build/ba_data/textures/scrollWidgetGlow.pvr": "https://files.ballistica.net/cache/ba1/ef/d1/9895a2dabfed25cd57ddad7cd585", - "assets/build/ba_data/textures/scrollWidgetGlow_preview.png": "https://files.ballistica.net/cache/ba1/38/d5/348f1f35709857ab36bdf6af3a4a", - "assets/build/ba_data/textures/scrollWidget_preview.png": "https://files.ballistica.net/cache/ba1/10/1f/9a169fb4644118efc5df208b1010", - "assets/build/ba_data/textures/settingsIcon.dds": "https://files.ballistica.net/cache/ba1/e2/84/e74bb763397e643a6149979507e9", - "assets/build/ba_data/textures/settingsIcon.ktx": "https://files.ballistica.net/cache/ba1/0d/65/2e6aba54c6e01df88121266fbd2a", - "assets/build/ba_data/textures/settingsIcon.pvr": "https://files.ballistica.net/cache/ba1/fb/20/d92e144c2f7de0086913801c326f", - "assets/build/ba_data/textures/settingsIcon_preview.png": "https://files.ballistica.net/cache/ba1/ec/79/32e7ac9c0acfa1300fc3cacbd2d5", - "assets/build/ba_data/textures/shadow.dds": "https://files.ballistica.net/cache/ba1/be/7f/db0422e1acb9c6c403a0df2f9112", - "assets/build/ba_data/textures/shadow.ktx": "https://files.ballistica.net/cache/ba1/7e/d1/f80312a5ee5a318f520f91e1f06b", - "assets/build/ba_data/textures/shadow.pvr": "https://files.ballistica.net/cache/ba1/6c/a7/1dd4ba46ebec036269f61008eb2f", - "assets/build/ba_data/textures/shadowSharp.dds": "https://files.ballistica.net/cache/ba1/ac/b4/dfdeea0de1a7af65c17d5e465530", - "assets/build/ba_data/textures/shadowSharp.ktx": "https://files.ballistica.net/cache/ba1/4e/90/f0f2717aa966bd7e8e253f18fbfc", - "assets/build/ba_data/textures/shadowSharp.pvr": "https://files.ballistica.net/cache/ba1/b3/16/f729ae6ad253b5dfcbb5934e50be", - "assets/build/ba_data/textures/shadowSharp_preview.png": "https://files.ballistica.net/cache/ba1/da/cf/5f11f492623705b1fe0b656596ff", - "assets/build/ba_data/textures/shadowSoft.dds": "https://files.ballistica.net/cache/ba1/57/90/38f0dc70ed00f6a137e086f3a232", - "assets/build/ba_data/textures/shadowSoft.ktx": "https://files.ballistica.net/cache/ba1/9d/75/0871a90922b28e5182ab639a97c3", - "assets/build/ba_data/textures/shadowSoft.pvr": "https://files.ballistica.net/cache/ba1/c7/c6/1c1cc5b1cdc862e853e90d8680c5", - "assets/build/ba_data/textures/shadowSoft_preview.png": "https://files.ballistica.net/cache/ba1/32/37/96875a5ea694d32bddf6cf576eeb", - "assets/build/ba_data/textures/shadow_preview.png": "https://files.ballistica.net/cache/ba1/8b/40/be763db3e91a708bc4121089a467", - "assets/build/ba_data/textures/shield.dds": "https://files.ballistica.net/cache/ba1/4f/43/6880fcf3a1428bcd732f31f7dc47", - "assets/build/ba_data/textures/shield.ktx": "https://files.ballistica.net/cache/ba1/b6/13/83622be0f8e61625d03e25011a35", - "assets/build/ba_data/textures/shield.pvr": "https://files.ballistica.net/cache/ba1/b8/bf/7c76d23cd2b49b017bcc89f886d2", - "assets/build/ba_data/textures/shield_preview.png": "https://files.ballistica.net/cache/ba1/2f/18/cc00f9603c2310b8ea330419195a", - "assets/build/ba_data/textures/shrapnel1Color.dds": "https://files.ballistica.net/cache/ba1/b2/ba/f43694f9ba54082d75718e149205", - "assets/build/ba_data/textures/shrapnel1Color.ktx": "https://files.ballistica.net/cache/ba1/d1/68/3ddc2586e27e2bcbc784f09cba13", - "assets/build/ba_data/textures/shrapnel1Color.pvr": "https://files.ballistica.net/cache/ba1/9c/6e/9cb2515cef17164ebc77149d955b", - "assets/build/ba_data/textures/shrapnel1Color_preview.png": "https://files.ballistica.net/cache/ba1/17/47/6ca7a7d4fd811c3d9137e76d33a2", - "assets/build/ba_data/textures/slash.dds": "https://files.ballistica.net/cache/ba1/73/bd/69f7db030178996db0368d1e5694", - "assets/build/ba_data/textures/slash.ktx": "https://files.ballistica.net/cache/ba1/54/cb/a35780a274f021fc1044b45daf99", - "assets/build/ba_data/textures/slash.pvr": "https://files.ballistica.net/cache/ba1/b6/d2/fa0617f78a81fc0eacb962484729", - "assets/build/ba_data/textures/slash_preview.png": "https://files.ballistica.net/cache/ba1/33/2f/060dad5afdba8162d7a3ad082df0", - "assets/build/ba_data/textures/smoke.dds": "https://files.ballistica.net/cache/ba1/c1/b5/f1c19535edd6f9abc74b85a9673a", - "assets/build/ba_data/textures/smoke.ktx": "https://files.ballistica.net/cache/ba1/8c/d8/74ac19c7930bbd695f533ec09fa3", - "assets/build/ba_data/textures/smoke.pvr": "https://files.ballistica.net/cache/ba1/d7/a5/014c007053212e96096b545626ea", - "assets/build/ba_data/textures/smoke_preview.png": "https://files.ballistica.net/cache/ba1/b1/c6/52443210c0229920389219868dcc", - "assets/build/ba_data/textures/softRect.dds": "https://files.ballistica.net/cache/ba1/82/63/9c9e85113c0ea9d5beb654bfeabf", - "assets/build/ba_data/textures/softRect.ktx": "https://files.ballistica.net/cache/ba1/65/47/7373e4b04ed08cdf4580af89357f", - "assets/build/ba_data/textures/softRect.pvr": "https://files.ballistica.net/cache/ba1/ee/6c/89928ef4cb0175259bf134bb25f9", - "assets/build/ba_data/textures/softRect2.dds": "https://files.ballistica.net/cache/ba1/fc/f8/c50c7d2429b8f0304b1ed9a706f1", - "assets/build/ba_data/textures/softRect2.ktx": "https://files.ballistica.net/cache/ba1/5e/35/ea781ec64d8d2bdbaf09fbd2177c", - "assets/build/ba_data/textures/softRect2.pvr": "https://files.ballistica.net/cache/ba1/66/9c/f65316d6e670fb2a6e77842403aa", - "assets/build/ba_data/textures/softRect2_preview.png": "https://files.ballistica.net/cache/ba1/97/3a/fbe21bc3113f852570dcc3b556bd", - "assets/build/ba_data/textures/softRectVertical.dds": "https://files.ballistica.net/cache/ba1/3e/3b/bd50d9d297790b438081fb0dd04b", - "assets/build/ba_data/textures/softRectVertical.ktx": "https://files.ballistica.net/cache/ba1/8a/f7/6321bcd88495f6c5aa720c077217", - "assets/build/ba_data/textures/softRectVertical.pvr": "https://files.ballistica.net/cache/ba1/4d/19/857d457d9b5a0279a80dceea2cd3", - "assets/build/ba_data/textures/softRectVertical_preview.png": "https://files.ballistica.net/cache/ba1/56/0f/524a88de51160a96224fb40249c1", - "assets/build/ba_data/textures/softRect_preview.png": "https://files.ballistica.net/cache/ba1/5e/31/512ca0d886a0d866f194f875c3db", - "assets/build/ba_data/textures/sparks.dds": "https://files.ballistica.net/cache/ba1/89/77/00c8ba51a9cf06bfc0a0a427efc3", - "assets/build/ba_data/textures/sparks.ktx": "https://files.ballistica.net/cache/ba1/3d/6d/beee025480e5711ecd52bc5f6b07", - "assets/build/ba_data/textures/sparks.pvr": "https://files.ballistica.net/cache/ba1/39/af/5b9e1081c0973f5e853c11fa06bc", - "assets/build/ba_data/textures/sparks_preview.png": "https://files.ballistica.net/cache/ba1/19/fc/4a53d76b56ea0f0c3d5eaeed93b4", - "assets/build/ba_data/textures/star.dds": "https://files.ballistica.net/cache/ba1/a6/03/f94f82c4f7133859a16f6986207a", - "assets/build/ba_data/textures/star.ktx": "https://files.ballistica.net/cache/ba1/50/9e/d926091f490525e600094dfc4f30", - "assets/build/ba_data/textures/star.pvr": "https://files.ballistica.net/cache/ba1/c6/32/f8e33f7610aa730610e20635e85e", - "assets/build/ba_data/textures/star_preview.png": "https://files.ballistica.net/cache/ba1/c6/f8/cd2c3ae95fa277ff0adf080ace00", - "assets/build/ba_data/textures/startButton.dds": "https://files.ballistica.net/cache/ba1/9e/d0/98c62c6a6ef3db1e49328e5b66fc", - "assets/build/ba_data/textures/startButton.ktx": "https://files.ballistica.net/cache/ba1/ef/9a/9323a447cb7cf8fa119bbeadbf7c", - "assets/build/ba_data/textures/startButton.pvr": "https://files.ballistica.net/cache/ba1/a8/25/74bd9ee857764fba362a0ecdc9b3", - "assets/build/ba_data/textures/startButton_preview.png": "https://files.ballistica.net/cache/ba1/ba/d5/48858f0b1a1e0e4043762d4b0216", - "assets/build/ba_data/textures/stepRightUpLevelColor.dds": "https://files.ballistica.net/cache/ba1/aa/27/277f7741294d367810f01ef150f8", - "assets/build/ba_data/textures/stepRightUpLevelColor.ktx": "https://files.ballistica.net/cache/ba1/6f/35/3380ac50585bce22311666d28782", - "assets/build/ba_data/textures/stepRightUpLevelColor.pvr": "https://files.ballistica.net/cache/ba1/8b/fb/eba3c0faa304123c784c888d0c65", - "assets/build/ba_data/textures/stepRightUpLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/5d/b9/bcdca38ad1d808b541b8008d5555", - "assets/build/ba_data/textures/stepRightUpPreview.dds": "https://files.ballistica.net/cache/ba1/ce/8f/8843ebbc561cf5dd44eb657b797d", - "assets/build/ba_data/textures/stepRightUpPreview.ktx": "https://files.ballistica.net/cache/ba1/bd/75/7b19bfb14c8a73ae9a42156d70f2", - "assets/build/ba_data/textures/stepRightUpPreview.pvr": "https://files.ballistica.net/cache/ba1/fe/44/e09c5a523cb3e8576709256c79a9", - "assets/build/ba_data/textures/stepRightUpPreview_preview.png": "https://files.ballistica.net/cache/ba1/ad/ff/086bca9f87fdc17bae4bd55e904f", - "assets/build/ba_data/textures/storeCharacter.dds": "https://files.ballistica.net/cache/ba1/77/4b/f7ffac20e9247906054b1c63e4ab", - "assets/build/ba_data/textures/storeCharacter.ktx": "https://files.ballistica.net/cache/ba1/7b/bb/22aedb0bfa853cf7663e1a513691", - "assets/build/ba_data/textures/storeCharacter.pvr": "https://files.ballistica.net/cache/ba1/4b/39/1dddef9e334be55da422c5c94118", - "assets/build/ba_data/textures/storeCharacterEaster.dds": "https://files.ballistica.net/cache/ba1/89/62/00eb56d6dbc1c41d61dcd7216175", - "assets/build/ba_data/textures/storeCharacterEaster.ktx": "https://files.ballistica.net/cache/ba1/0e/e1/3d5ad2134337c854026ca656235a", - "assets/build/ba_data/textures/storeCharacterEaster.pvr": "https://files.ballistica.net/cache/ba1/52/b3/27d1607624af268cedce8fede721", - "assets/build/ba_data/textures/storeCharacterEaster_preview.png": "https://files.ballistica.net/cache/ba1/ee/bc/3092b89e330c40e1872d4a11514c", - "assets/build/ba_data/textures/storeCharacterXmas.dds": "https://files.ballistica.net/cache/ba1/fc/7c/3819628e17226f080b037a44f35c", - "assets/build/ba_data/textures/storeCharacterXmas.ktx": "https://files.ballistica.net/cache/ba1/bd/86/fcd3fd5d69900896d0c0b01ff8d2", - "assets/build/ba_data/textures/storeCharacterXmas.pvr": "https://files.ballistica.net/cache/ba1/70/7a/918331c596c9794f1e81116251a3", - "assets/build/ba_data/textures/storeCharacterXmas_preview.png": "https://files.ballistica.net/cache/ba1/82/75/e4337c2dd8c6b2ff63261c160334", - "assets/build/ba_data/textures/storeCharacter_preview.png": "https://files.ballistica.net/cache/ba1/54/46/d74b5e6e2a3ac7335b5cf746c2e7", - "assets/build/ba_data/textures/storeIcon.dds": "https://files.ballistica.net/cache/ba1/93/16/4593854e269f21add762f954e086", - "assets/build/ba_data/textures/storeIcon.ktx": "https://files.ballistica.net/cache/ba1/aa/27/1abbc5d0d3f8d9f6ff4d884d713c", - "assets/build/ba_data/textures/storeIcon.pvr": "https://files.ballistica.net/cache/ba1/ae/69/7d6fa92bf2b2198098915bf2c4e7", - "assets/build/ba_data/textures/storeIcon_preview.png": "https://files.ballistica.net/cache/ba1/8f/ed/852fdb4bbb6a0d7253398fee2588", - "assets/build/ba_data/textures/superheroColor.dds": "https://files.ballistica.net/cache/ba1/20/22/1e1f70c67cdbf49ce120b454f76a", - "assets/build/ba_data/textures/superheroColor.ktx": "https://files.ballistica.net/cache/ba1/6d/7c/bc6c4f9b237301fefea90f27f9a0", - "assets/build/ba_data/textures/superheroColor.pvr": "https://files.ballistica.net/cache/ba1/48/12/34feeaf8bb9724e3d084b96cda3b", - "assets/build/ba_data/textures/superheroColorMask.dds": "https://files.ballistica.net/cache/ba1/68/a5/b09635c375edc462cf4b102c3236", - "assets/build/ba_data/textures/superheroColorMask.ktx": "https://files.ballistica.net/cache/ba1/a5/19/f7169d492c759f9f3416887df9d5", - "assets/build/ba_data/textures/superheroColorMask.pvr": "https://files.ballistica.net/cache/ba1/39/dc/256d7b9203f90032bbe21cec3a03", - "assets/build/ba_data/textures/superheroColorMask_preview.png": "https://files.ballistica.net/cache/ba1/7d/f0/d05466477b420b25ff7dd881c87b", - "assets/build/ba_data/textures/superheroColor_preview.png": "https://files.ballistica.net/cache/ba1/4a/29/8ed3945a6704359718dd5e8e19e6", - "assets/build/ba_data/textures/superheroIcon.dds": "https://files.ballistica.net/cache/ba1/a0/60/29203cd8faeca978f499c408dc1c", - "assets/build/ba_data/textures/superheroIcon.ktx": "https://files.ballistica.net/cache/ba1/f5/bf/dc3b8af3d9261086b944d5ea6ed5", - "assets/build/ba_data/textures/superheroIcon.pvr": "https://files.ballistica.net/cache/ba1/1f/24/e946a1ff6ebb1d7902380d3cf182", - "assets/build/ba_data/textures/superheroIconColorMask.dds": "https://files.ballistica.net/cache/ba1/41/6b/338f6247bded99b89e98d19fc09b", - "assets/build/ba_data/textures/superheroIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/78/0f/deeffafbedd0b34c66f484480524", - "assets/build/ba_data/textures/superheroIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/b2/cc/b9ea2112f299abfb1c6b9bf29fca", - "assets/build/ba_data/textures/superheroIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/61/f0/e955384c22df0e681609f7e848b3", - "assets/build/ba_data/textures/superheroIcon_preview.png": "https://files.ballistica.net/cache/ba1/9f/b2/ae6714ea26630691a40a2bf2a67b", - "assets/build/ba_data/textures/textClearButton.dds": "https://files.ballistica.net/cache/ba1/80/24/dff04c82690248c24ca3d5739278", - "assets/build/ba_data/textures/textClearButton.ktx": "https://files.ballistica.net/cache/ba1/05/60/2ae6636eb10907f2973342229949", - "assets/build/ba_data/textures/textClearButton.pvr": "https://files.ballistica.net/cache/ba1/b1/32/dc45a735786df68c442b16a62a57", - "assets/build/ba_data/textures/textClearButton_preview.png": "https://files.ballistica.net/cache/ba1/12/64/67e74f9e5d77ba1230304ea40497", - "assets/build/ba_data/textures/thePadLevelColor.dds": "https://files.ballistica.net/cache/ba1/61/17/6fb9d7fef2499c9ef5e3eb7ac928", - "assets/build/ba_data/textures/thePadLevelColor.ktx": "https://files.ballistica.net/cache/ba1/4e/34/bf0d6a8b0e64cf7fef14b1ec9da5", - "assets/build/ba_data/textures/thePadLevelColor.pvr": "https://files.ballistica.net/cache/ba1/27/ad/471ab1086db8d6669040fc48742a", - "assets/build/ba_data/textures/thePadLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/88/1d/42616ff95265cb0d0db6c353a044", - "assets/build/ba_data/textures/thePadPreview.dds": "https://files.ballistica.net/cache/ba1/12/67/c64f2c3a5726712453400859e0f3", - "assets/build/ba_data/textures/thePadPreview.ktx": "https://files.ballistica.net/cache/ba1/5e/11/ff9caf9657a7c439b2e0fab39049", - "assets/build/ba_data/textures/thePadPreview.pvr": "https://files.ballistica.net/cache/ba1/5a/8f/4061a1deafca6ba331738f6c9db8", - "assets/build/ba_data/textures/thePadPreview_preview.png": "https://files.ballistica.net/cache/ba1/94/12/ec4cc8f0378cf62f811122e6faec", - "assets/build/ba_data/textures/ticketRoll.dds": "https://files.ballistica.net/cache/ba1/a3/b2/52d33da46ef8b5ec849143a85afc", - "assets/build/ba_data/textures/ticketRoll.ktx": "https://files.ballistica.net/cache/ba1/ac/ad/f0d78264544012f85f0a59f56890", - "assets/build/ba_data/textures/ticketRoll.pvr": "https://files.ballistica.net/cache/ba1/3d/62/ea117b377d6080af47722a08c8a0", - "assets/build/ba_data/textures/ticketRollBig.dds": "https://files.ballistica.net/cache/ba1/53/83/fea4e115c631dfb5430336665cae", - "assets/build/ba_data/textures/ticketRollBig.ktx": "https://files.ballistica.net/cache/ba1/27/36/106fa492322b0e7f480864186a03", - "assets/build/ba_data/textures/ticketRollBig.pvr": "https://files.ballistica.net/cache/ba1/54/a5/f09a5b96913a934114b400e8e875", - "assets/build/ba_data/textures/ticketRollBig_preview.png": "https://files.ballistica.net/cache/ba1/f3/b1/7a5ce67d0520ada71249594f1411", - "assets/build/ba_data/textures/ticketRoll_preview.png": "https://files.ballistica.net/cache/ba1/81/4f/fc77c14b0588257893c441060cbc", - "assets/build/ba_data/textures/ticketRolls.dds": "https://files.ballistica.net/cache/ba1/a1/12/0eb9317dd289b7fc4bca0df550d7", - "assets/build/ba_data/textures/ticketRolls.ktx": "https://files.ballistica.net/cache/ba1/c6/05/21e919e290075fc32ef7f5ef9caf", - "assets/build/ba_data/textures/ticketRolls.pvr": "https://files.ballistica.net/cache/ba1/ec/8d/17f3547a523ef56c3c4d8e666883", - "assets/build/ba_data/textures/ticketRolls_preview.png": "https://files.ballistica.net/cache/ba1/7c/22/3c6d77d46c6de99c72c705bd1bf3", - "assets/build/ba_data/textures/tickets.dds": "https://files.ballistica.net/cache/ba1/cd/2a/829d9fea89e769bb5ee73fcc09e0", - "assets/build/ba_data/textures/tickets.ktx": "https://files.ballistica.net/cache/ba1/e7/2c/f8c96f368da08044edd9501f97e5", - "assets/build/ba_data/textures/tickets.pvr": "https://files.ballistica.net/cache/ba1/ef/89/1246259d01f65535c867f0556b02", - "assets/build/ba_data/textures/ticketsMore.dds": "https://files.ballistica.net/cache/ba1/90/ac/03647336f11983c22f1c47284df7", - "assets/build/ba_data/textures/ticketsMore.ktx": "https://files.ballistica.net/cache/ba1/db/86/1a3c160a72a8cca63cfe7fe125a9", - "assets/build/ba_data/textures/ticketsMore.pvr": "https://files.ballistica.net/cache/ba1/59/1a/73c4109d96e85316c2c46ff2c025", - "assets/build/ba_data/textures/ticketsMore_preview.png": "https://files.ballistica.net/cache/ba1/24/01/32ba85edd6394dd56562f64eee09", - "assets/build/ba_data/textures/tickets_preview.png": "https://files.ballistica.net/cache/ba1/90/ce/b693dd116e50d1b0eff488cfdd98", - "assets/build/ba_data/textures/tipTopBGColor.dds": "https://files.ballistica.net/cache/ba1/19/bf/258c624e1b3e3e3efabeefe3dd20", - "assets/build/ba_data/textures/tipTopBGColor.ktx": "https://files.ballistica.net/cache/ba1/39/8c/a9f1ba489b23f5d899bb3f9941b1", - "assets/build/ba_data/textures/tipTopBGColor.pvr": "https://files.ballistica.net/cache/ba1/1b/0d/89ecc99bc58aa985f00f7097177a", - "assets/build/ba_data/textures/tipTopBGColor_preview.png": "https://files.ballistica.net/cache/ba1/54/0b/9978639a58e703cece49de6523b7", - "assets/build/ba_data/textures/tipTopLevelColor.dds": "https://files.ballistica.net/cache/ba1/d9/9d/5887220819a9bceaf547349cc1f3", - "assets/build/ba_data/textures/tipTopLevelColor.ktx": "https://files.ballistica.net/cache/ba1/4f/77/17f60fceaa239805af2bb7a169be", - "assets/build/ba_data/textures/tipTopLevelColor.pvr": "https://files.ballistica.net/cache/ba1/8b/20/240ddac5158fb481ce41c50a86ce", - "assets/build/ba_data/textures/tipTopLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/75/88/019f15db873f836c5651b1b12130", - "assets/build/ba_data/textures/tipTopPreview.dds": "https://files.ballistica.net/cache/ba1/81/53/3d173f18e5d307e9efd40e1dcd60", - "assets/build/ba_data/textures/tipTopPreview.ktx": "https://files.ballistica.net/cache/ba1/be/66/1fcdc84b3bd06378c3d5d52760ca", - "assets/build/ba_data/textures/tipTopPreview.pvr": "https://files.ballistica.net/cache/ba1/e5/7e/b2c75f4d11a9f19909f844860d6a", - "assets/build/ba_data/textures/tipTopPreview_preview.png": "https://files.ballistica.net/cache/ba1/f1/1f/793055f92784d8387b374bcf171a", - "assets/build/ba_data/textures/tnt.dds": "https://files.ballistica.net/cache/ba1/35/cd/ba58e52e3960ce072f0760be53c6", - "assets/build/ba_data/textures/tnt.ktx": "https://files.ballistica.net/cache/ba1/cd/d9/e282eeb5645e113c7d2496c08a9f", - "assets/build/ba_data/textures/tnt.pvr": "https://files.ballistica.net/cache/ba1/44/f4/e850b0fd867d4647417a5ab31fea", - "assets/build/ba_data/textures/tnt_preview.png": "https://files.ballistica.net/cache/ba1/dd/19/bb4ee173f7ec0269d0b1b1663a23", - "assets/build/ba_data/textures/touchArrows.dds": "https://files.ballistica.net/cache/ba1/e5/a4/678fa4d88fcf53ad0b69f870dba6", - "assets/build/ba_data/textures/touchArrows.ktx": "https://files.ballistica.net/cache/ba1/be/ab/d8fe216ec8e5ec5f70ae1a652238", - "assets/build/ba_data/textures/touchArrows.pvr": "https://files.ballistica.net/cache/ba1/5a/ab/901601ec1e654bf8e4822cd09a0e", - "assets/build/ba_data/textures/touchArrowsActions.dds": "https://files.ballistica.net/cache/ba1/7e/4c/8119db45dd1d43e6dd65d5befe4c", - "assets/build/ba_data/textures/touchArrowsActions.ktx": "https://files.ballistica.net/cache/ba1/53/1a/8eef6cef97525fa9469cffa606f2", - "assets/build/ba_data/textures/touchArrowsActions.pvr": "https://files.ballistica.net/cache/ba1/74/1b/7642cfb899d1337073b10855d01d", - "assets/build/ba_data/textures/touchArrowsActions_preview.png": "https://files.ballistica.net/cache/ba1/3c/21/1933b01847e20ed77164097aaea5", - "assets/build/ba_data/textures/touchArrows_preview.png": "https://files.ballistica.net/cache/ba1/14/bd/595019a7e3b87bd844f8a60bf04c", - "assets/build/ba_data/textures/towerDLevelColor.dds": "https://files.ballistica.net/cache/ba1/8a/1f/997dafb004fe5b2122c66ae66302", - "assets/build/ba_data/textures/towerDLevelColor.ktx": "https://files.ballistica.net/cache/ba1/8d/42/47252953095838210efdf6abc37d", - "assets/build/ba_data/textures/towerDLevelColor.pvr": "https://files.ballistica.net/cache/ba1/9f/18/de2d35f9737871b3a1a914561428", - "assets/build/ba_data/textures/towerDLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/d4/e2/96ed47c7c856cf772a74f7385fab", - "assets/build/ba_data/textures/towerDPreview.dds": "https://files.ballistica.net/cache/ba1/4f/91/7de86add4cdc30060477c93e2aa6", - "assets/build/ba_data/textures/towerDPreview.ktx": "https://files.ballistica.net/cache/ba1/e9/3a/d62eb672f3f26757b59f4d6d9e48", - "assets/build/ba_data/textures/towerDPreview.pvr": "https://files.ballistica.net/cache/ba1/be/40/cc2ad06c0c08a1b937a2165f3bdb", - "assets/build/ba_data/textures/towerDPreview_preview.png": "https://files.ballistica.net/cache/ba1/20/80/2314d36626f44f2ded99773ce267", - "assets/build/ba_data/textures/treesColor.dds": "https://files.ballistica.net/cache/ba1/a9/c6/e78e8aecedb379288c5a2d6e41f0", - "assets/build/ba_data/textures/treesColor.ktx": "https://files.ballistica.net/cache/ba1/e7/19/2b8b0c8cfd26839ebd1d514354b7", - "assets/build/ba_data/textures/treesColor.pvr": "https://files.ballistica.net/cache/ba1/03/b2/159518817084766a2cc464ba0a1b", - "assets/build/ba_data/textures/treesColor_preview.png": "https://files.ballistica.net/cache/ba1/72/2b/7addb4861a6a8cd09748248e4de4", - "assets/build/ba_data/textures/trophy.dds": "https://files.ballistica.net/cache/ba1/83/38/bd610211c05d8c2a244d1ab6e492", - "assets/build/ba_data/textures/trophy.ktx": "https://files.ballistica.net/cache/ba1/d2/fe/64262bd1e59afeff4f38f090690d", - "assets/build/ba_data/textures/trophy.pvr": "https://files.ballistica.net/cache/ba1/eb/65/5503d0d4954783f066cc79bdaa02", - "assets/build/ba_data/textures/trophy_preview.png": "https://files.ballistica.net/cache/ba1/f7/cd/37f86f57794f3199e375c6cfb88c", - "assets/build/ba_data/textures/tv.dds": "https://files.ballistica.net/cache/ba1/a6/a9/8b0d9f5e3958dd7d311c13819b5f", - "assets/build/ba_data/textures/tv.ktx": "https://files.ballistica.net/cache/ba1/3d/c7/8f8b90c74b46bf408f14ae2a0aad", - "assets/build/ba_data/textures/tv.pvr": "https://files.ballistica.net/cache/ba1/b5/ad/fe4e26229c5cf8f8532a72a6dd37", - "assets/build/ba_data/textures/tv_preview.png": "https://files.ballistica.net/cache/ba1/98/bc/72d7e3a1c4087d4811df1ae4cef2", - "assets/build/ba_data/textures/uiAtlas.dds": "https://files.ballistica.net/cache/ba1/20/f4/8cfeb68c7077288da98c6262f1cf", - "assets/build/ba_data/textures/uiAtlas.ktx": "https://files.ballistica.net/cache/ba1/29/ba/5f94e1887795038a5bc2f5bfa967", - "assets/build/ba_data/textures/uiAtlas.pvr": "https://files.ballistica.net/cache/ba1/9f/33/7dabb3b9b1fb9a07309f85c3f7b4", - "assets/build/ba_data/textures/uiAtlas2.dds": "https://files.ballistica.net/cache/ba1/d0/d1/7255e0d8330f4c372dc152dc12e6", - "assets/build/ba_data/textures/uiAtlas2.ktx": "https://files.ballistica.net/cache/ba1/56/bb/466f856993c4c74028c92746122b", - "assets/build/ba_data/textures/uiAtlas2.pvr": "https://files.ballistica.net/cache/ba1/99/de/c3d554c708fde26429d03e75cefc", - "assets/build/ba_data/textures/uiAtlas2_preview.png": "https://files.ballistica.net/cache/ba1/d6/d0/6ec9d6076c8e7cabce7798c0833f", - "assets/build/ba_data/textures/uiAtlas_preview.png": "https://files.ballistica.net/cache/ba1/09/6c/eed1bc24e3a64c6af05d796488a7", - "assets/build/ba_data/textures/upButton.dds": "https://files.ballistica.net/cache/ba1/92/4c/e3725df2f8d9b95587cc4e7fc0c8", - "assets/build/ba_data/textures/upButton.ktx": "https://files.ballistica.net/cache/ba1/45/af/e9633125cf4432943e090099845e", - "assets/build/ba_data/textures/upButton.pvr": "https://files.ballistica.net/cache/ba1/8d/ce/03b48a5e66c87e551e899ee33e60", - "assets/build/ba_data/textures/upButton_preview.png": "https://files.ballistica.net/cache/ba1/99/19/275e0bebf05e3761cedb802abd9d", - "assets/build/ba_data/textures/usersButton.dds": "https://files.ballistica.net/cache/ba1/34/f0/cc97153e2ab23996227a507d8044", - "assets/build/ba_data/textures/usersButton.ktx": "https://files.ballistica.net/cache/ba1/5b/e6/34fb5d6f905c30b3a2b9aa9e5734", - "assets/build/ba_data/textures/usersButton.pvr": "https://files.ballistica.net/cache/ba1/0a/de/0f45f12e34557fb1141d7a266a0d", - "assets/build/ba_data/textures/usersButton_preview.png": "https://files.ballistica.net/cache/ba1/1a/fc/7d74541cd06ad5db3de119418f81", - "assets/build/ba_data/textures/vrFillMound.dds": "https://files.ballistica.net/cache/ba1/bc/b3/8e14500bb766cf3639ea003cd9af", - "assets/build/ba_data/textures/vrFillMound.ktx": "https://files.ballistica.net/cache/ba1/5d/1e/e86c34fd33355e21955d3d48bfe6", - "assets/build/ba_data/textures/vrFillMound.pvr": "https://files.ballistica.net/cache/ba1/e8/da/46ed26badb88b6fd10abc37f35f6", - "assets/build/ba_data/textures/vrFillMound_preview.png": "https://files.ballistica.net/cache/ba1/fd/33/4f817cfe06d2f8a04c395a9dc960", - "assets/build/ba_data/textures/warriorColor.dds": "https://files.ballistica.net/cache/ba1/f2/51/d42750c9df1f55f39c134c925c7f", - "assets/build/ba_data/textures/warriorColor.ktx": "https://files.ballistica.net/cache/ba1/e7/72/6f9a3314300c12b5ffd99a4d0000", - "assets/build/ba_data/textures/warriorColor.pvr": "https://files.ballistica.net/cache/ba1/f2/63/d8bf3ba74b47932b4465847ef133", - "assets/build/ba_data/textures/warriorColorMask.dds": "https://files.ballistica.net/cache/ba1/e4/4c/e0ccd356704be279c55ad5d7bc68", - "assets/build/ba_data/textures/warriorColorMask.ktx": "https://files.ballistica.net/cache/ba1/d4/22/c8c01a12e9ec10f214659ce9faa4", - "assets/build/ba_data/textures/warriorColorMask.pvr": "https://files.ballistica.net/cache/ba1/17/c5/87586c89935377cb8d4336713094", - "assets/build/ba_data/textures/warriorColorMask_preview.png": "https://files.ballistica.net/cache/ba1/d9/97/16b01e9e4bf3bdc8c57672526158", - "assets/build/ba_data/textures/warriorColor_preview.png": "https://files.ballistica.net/cache/ba1/d1/c5/4b06d9c4016d084db3a3bb670dab", - "assets/build/ba_data/textures/warriorIcon.dds": "https://files.ballistica.net/cache/ba1/f4/df/ebdb667c745daf4a7d3b055110d7", - "assets/build/ba_data/textures/warriorIcon.ktx": "https://files.ballistica.net/cache/ba1/35/1c/5299fc16cdf1aad691436c4d89d0", - "assets/build/ba_data/textures/warriorIcon.pvr": "https://files.ballistica.net/cache/ba1/65/1c/c0e4b97c35ad6fda74538bf6bf59", - "assets/build/ba_data/textures/warriorIconColorMask.dds": "https://files.ballistica.net/cache/ba1/4f/d5/8346f68d2255e1d4c98082ee317e", - "assets/build/ba_data/textures/warriorIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/63/b4/da013ddfebbcf50d74a2a2785d86", - "assets/build/ba_data/textures/warriorIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/c6/53/1b3fcbbfd4db0e84d91399668889", - "assets/build/ba_data/textures/warriorIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/cb/f7/8f01e49a7282fd5901171d83bb11", - "assets/build/ba_data/textures/warriorIcon_preview.png": "https://files.ballistica.net/cache/ba1/fc/74/111b5efb60fedf6292351833d4f4", - "assets/build/ba_data/textures/white.dds": "https://files.ballistica.net/cache/ba1/fe/14/c0df2ba99acd5a4f970fd61eb485", - "assets/build/ba_data/textures/white.ktx": "https://files.ballistica.net/cache/ba1/f7/31/d8fc06c73f3afe55e1d5d24e86d7", - "assets/build/ba_data/textures/white.pvr": "https://files.ballistica.net/cache/ba1/75/6d/503ec6037fbdcc16c13b76967d02", - "assets/build/ba_data/textures/white_preview.png": "https://files.ballistica.net/cache/ba1/fe/c1/329a4b69f6d61cfc154523a65971", - "assets/build/ba_data/textures/windowHSmallVMed.dds": "https://files.ballistica.net/cache/ba1/57/6d/b8cbcdd63aef0a0559b12413fa55", - "assets/build/ba_data/textures/windowHSmallVMed.ktx": "https://files.ballistica.net/cache/ba1/df/19/de5c615c5defa8f8b66b57a679ba", - "assets/build/ba_data/textures/windowHSmallVMed.pvr": "https://files.ballistica.net/cache/ba1/17/a4/96527bea6af5a8e1a4003029c252", - "assets/build/ba_data/textures/windowHSmallVMed_preview.png": "https://files.ballistica.net/cache/ba1/12/34/368442aa7327e8d4612e7dc40599", - "assets/build/ba_data/textures/windowHSmallVSmall.dds": "https://files.ballistica.net/cache/ba1/57/75/44ace81722a8a2d39ebd7daa4c4c", - "assets/build/ba_data/textures/windowHSmallVSmall.ktx": "https://files.ballistica.net/cache/ba1/f7/2f/356dd5e43fe278682d1286aa9967", - "assets/build/ba_data/textures/windowHSmallVSmall.pvr": "https://files.ballistica.net/cache/ba1/d3/99/65ebc77a05d7852ab557d056f4e8", - "assets/build/ba_data/textures/windowHSmallVSmall_preview.png": "https://files.ballistica.net/cache/ba1/66/76/1f945114178854fc2583440ba44a", - "assets/build/ba_data/textures/wings.dds": "https://files.ballistica.net/cache/ba1/5e/9d/730e2aa43faa8800d585ff76541c", - "assets/build/ba_data/textures/wings.ktx": "https://files.ballistica.net/cache/ba1/cb/2d/b0fc916fb6dfa9530fbf78c4f091", - "assets/build/ba_data/textures/wings.pvr": "https://files.ballistica.net/cache/ba1/e5/b6/74308840b937937642056b542db4", - "assets/build/ba_data/textures/wings_preview.png": "https://files.ballistica.net/cache/ba1/90/4f/65d3a72f69d0f9a97f88b938c6d8", - "assets/build/ba_data/textures/witchColor.dds": "https://files.ballistica.net/cache/ba1/41/36/4b562ef0323d0b3ad88fea31bec8", - "assets/build/ba_data/textures/witchColor.ktx": "https://files.ballistica.net/cache/ba1/f2/32/d61ee331287b1c4e508a1f6ea317", - "assets/build/ba_data/textures/witchColor.pvr": "https://files.ballistica.net/cache/ba1/e5/f4/1ae61921f42efa27ed72120337a1", - "assets/build/ba_data/textures/witchColorMask.dds": "https://files.ballistica.net/cache/ba1/85/13/5d498983b93a3b27565525e60e03", - "assets/build/ba_data/textures/witchColorMask.ktx": "https://files.ballistica.net/cache/ba1/e0/23/4fb45223a969626489041af0de9f", - "assets/build/ba_data/textures/witchColorMask.pvr": "https://files.ballistica.net/cache/ba1/ab/d9/d03a2366b07854e7c82cd0e1e443", - "assets/build/ba_data/textures/witchColorMask_preview.png": "https://files.ballistica.net/cache/ba1/49/7f/61faa30b3b091c142c1212a85019", - "assets/build/ba_data/textures/witchColor_preview.png": "https://files.ballistica.net/cache/ba1/30/af/1ea77ea44e26d4d1b8e3f18b5fe8", - "assets/build/ba_data/textures/witchIcon.dds": "https://files.ballistica.net/cache/ba1/2d/46/b81308e041dd3442b258eb5a255e", - "assets/build/ba_data/textures/witchIcon.ktx": "https://files.ballistica.net/cache/ba1/1e/a3/6fd1e0fe25729b379adf1ab90ea9", - "assets/build/ba_data/textures/witchIcon.pvr": "https://files.ballistica.net/cache/ba1/9f/e4/dae655bd20c438d7aaab3fbe5bf9", - "assets/build/ba_data/textures/witchIconColorMask.dds": "https://files.ballistica.net/cache/ba1/6b/f8/2dd2b87ea79e14b528035eb5ff88", - "assets/build/ba_data/textures/witchIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/42/7c/2289f08d34b803aff4bef07e9847", - "assets/build/ba_data/textures/witchIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/2e/76/a2b8bfd7868a47c11b2c5fd1660b", - "assets/build/ba_data/textures/witchIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/49/bc/8479175256d1e62e607b2c392f2d", - "assets/build/ba_data/textures/witchIcon_preview.png": "https://files.ballistica.net/cache/ba1/5f/96/cb2eb1c3a9fb0240f5748c5c1da5", - "assets/build/ba_data/textures/wizardColor.dds": "https://files.ballistica.net/cache/ba1/ec/e9/fcbc0fc4d1fc397f8ec5b498337f", - "assets/build/ba_data/textures/wizardColor.ktx": "https://files.ballistica.net/cache/ba1/50/da/d0569142ec7924ca40eccf82a930", - "assets/build/ba_data/textures/wizardColor.pvr": "https://files.ballistica.net/cache/ba1/11/dc/b7f4a083ef2a93d7834b3d841443", - "assets/build/ba_data/textures/wizardColorMask.dds": "https://files.ballistica.net/cache/ba1/b4/32/3d46d2e5920e7530b384d7626617", - "assets/build/ba_data/textures/wizardColorMask.ktx": "https://files.ballistica.net/cache/ba1/0d/0d/80e2b947ebd3dbde1330682f62a4", - "assets/build/ba_data/textures/wizardColorMask.pvr": "https://files.ballistica.net/cache/ba1/5a/7c/9933e396902aca69b786468d1c38", - "assets/build/ba_data/textures/wizardColorMask_preview.png": "https://files.ballistica.net/cache/ba1/44/93/65a9e806485cd757e6a05655916a", - "assets/build/ba_data/textures/wizardColor_preview.png": "https://files.ballistica.net/cache/ba1/cf/3c/3db3b3f5b6dab73f8b8a5353fe1a", - "assets/build/ba_data/textures/wizardIcon.dds": "https://files.ballistica.net/cache/ba1/e6/2f/74df3fe2e97c960ff1ca64aed588", - "assets/build/ba_data/textures/wizardIcon.ktx": "https://files.ballistica.net/cache/ba1/b9/3d/2c10de406177de22b8e89c9cb3b3", - "assets/build/ba_data/textures/wizardIcon.pvr": "https://files.ballistica.net/cache/ba1/e3/c0/7d62f35ec06d5c4442a92b175cc0", - "assets/build/ba_data/textures/wizardIconColorMask.dds": "https://files.ballistica.net/cache/ba1/b5/16/7f556e4b852a94400388b4c96db4", - "assets/build/ba_data/textures/wizardIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/91/17/307bfa8af7dc435aa92dd38158f6", - "assets/build/ba_data/textures/wizardIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/3e/31/19cb4995ec02f1884bd35b29a464", - "assets/build/ba_data/textures/wizardIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/20/24/700221546a08f3bc9c31e751ebcc", - "assets/build/ba_data/textures/wizardIcon_preview.png": "https://files.ballistica.net/cache/ba1/6d/eb/6ad88e8d30bebe61fd853c9de377", - "assets/build/ba_data/textures/wrestlerColor.dds": "https://files.ballistica.net/cache/ba1/1f/d5/d21adac8982ad713b0bbdbb7ee6c", - "assets/build/ba_data/textures/wrestlerColor.ktx": "https://files.ballistica.net/cache/ba1/d7/22/bf15886b227531620eec6462be2d", - "assets/build/ba_data/textures/wrestlerColor.pvr": "https://files.ballistica.net/cache/ba1/ab/76/9f573919ed6b9e635f4a014584ba", - "assets/build/ba_data/textures/wrestlerColorMask.dds": "https://files.ballistica.net/cache/ba1/fb/a5/472ae89a40a52d189c9337c179fa", - "assets/build/ba_data/textures/wrestlerColorMask.ktx": "https://files.ballistica.net/cache/ba1/21/1f/eebe5e1fe740412aad15026d21b5", - "assets/build/ba_data/textures/wrestlerColorMask.pvr": "https://files.ballistica.net/cache/ba1/36/43/ef6da6107379ac3f74204a34c529", - "assets/build/ba_data/textures/wrestlerColorMask_preview.png": "https://files.ballistica.net/cache/ba1/0c/5f/8619903636053dc0ff1858bf3451", - "assets/build/ba_data/textures/wrestlerColor_preview.png": "https://files.ballistica.net/cache/ba1/88/53/ce4cdad4460ecdf972ac95ac2b2a", - "assets/build/ba_data/textures/wrestlerIcon.dds": "https://files.ballistica.net/cache/ba1/46/86/e478e00dced7a8dbfb67559c514c", - "assets/build/ba_data/textures/wrestlerIcon.ktx": "https://files.ballistica.net/cache/ba1/5c/27/c297371a5ac7ba70c5e5c5a67b2f", - "assets/build/ba_data/textures/wrestlerIcon.pvr": "https://files.ballistica.net/cache/ba1/27/e5/c0883e5f69758c93dbfbc2f0af8c", - "assets/build/ba_data/textures/wrestlerIconColorMask.dds": "https://files.ballistica.net/cache/ba1/47/da/41298df953d7cc8b01dedc90a97b", - "assets/build/ba_data/textures/wrestlerIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/7f/66/69bd4ebcbba15b8c49734ead9d49", - "assets/build/ba_data/textures/wrestlerIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/7b/5d/83a1a0f4d7b06303d5da137e5847", - "assets/build/ba_data/textures/wrestlerIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/b5/74/774b56f03e583df47388c552c63e", - "assets/build/ba_data/textures/wrestlerIcon_preview.png": "https://files.ballistica.net/cache/ba1/38/71/8b5f4d69dde24101ce0d69747486", - "assets/build/ba_data/textures/zigZagLevelColor.dds": "https://files.ballistica.net/cache/ba1/df/17/9994816ad67f481800bae16f2db2", - "assets/build/ba_data/textures/zigZagLevelColor.ktx": "https://files.ballistica.net/cache/ba1/c1/1e/82884d1423ce4a7d641003e84bfb", - "assets/build/ba_data/textures/zigZagLevelColor.pvr": "https://files.ballistica.net/cache/ba1/56/df/c53d264147677138e47799ea990e", - "assets/build/ba_data/textures/zigZagLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/cd/d7/b45e95db0117c4fe5d957136b4bb", - "assets/build/ba_data/textures/zigzagPreview.dds": "https://files.ballistica.net/cache/ba1/6c/13/64e4a3570343f8a8965b8c6bb318", - "assets/build/ba_data/textures/zigzagPreview.ktx": "https://files.ballistica.net/cache/ba1/33/ca/b022d38b1306d8382c2ecdb270b9", - "assets/build/ba_data/textures/zigzagPreview.pvr": "https://files.ballistica.net/cache/ba1/0e/21/4f296b4440f8467c904221d18400", - "assets/build/ba_data/textures/zigzagPreview_preview.png": "https://files.ballistica.net/cache/ba1/f4/32/1f69e507893d054aca0371c31248", - "assets/build/ba_data/textures/zoeColor.dds": "https://files.ballistica.net/cache/ba1/05/e4/93016a653b2bb3433138594c9c0e", - "assets/build/ba_data/textures/zoeColor.ktx": "https://files.ballistica.net/cache/ba1/8d/80/906fb08660e1fcde3f5997108abf", - "assets/build/ba_data/textures/zoeColor.pvr": "https://files.ballistica.net/cache/ba1/4a/74/8ca47effb87502b7334398e967be", - "assets/build/ba_data/textures/zoeColorMask.dds": "https://files.ballistica.net/cache/ba1/b6/06/e4086e65b697d7de1c8864f49029", - "assets/build/ba_data/textures/zoeColorMask.ktx": "https://files.ballistica.net/cache/ba1/b9/32/91c389da7403df50125cddc37575", - "assets/build/ba_data/textures/zoeColorMask.pvr": "https://files.ballistica.net/cache/ba1/f4/6a/4cbdbcf47abdb7f8fdc8d283451e", - "assets/build/ba_data/textures/zoeColorMask_preview.png": "https://files.ballistica.net/cache/ba1/ca/0e/66481610316607e9adcb456467d1", - "assets/build/ba_data/textures/zoeColor_preview.png": "https://files.ballistica.net/cache/ba1/3b/3e/91abd548d271a97df63b327a1f82", - "assets/build/ba_data/textures/zoeIcon.dds": "https://files.ballistica.net/cache/ba1/da/63/063fc70b4a22b7a0f32ca8e9f8b4", - "assets/build/ba_data/textures/zoeIcon.ktx": "https://files.ballistica.net/cache/ba1/fd/75/88d1a4355980906272cf904bd722", - "assets/build/ba_data/textures/zoeIcon.pvr": "https://files.ballistica.net/cache/ba1/7c/66/7959cdfaa884068bdb11db1ef330", - "assets/build/ba_data/textures/zoeIconColorMask.dds": "https://files.ballistica.net/cache/ba1/75/06/c186a1ad6e54ba6d8862b23c2136", - "assets/build/ba_data/textures/zoeIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/bd/eb/deb5899cffa81a665248e3062363", - "assets/build/ba_data/textures/zoeIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/2b/dc/22df1ef245a7f368060d2eecb839", - "assets/build/ba_data/textures/zoeIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/d5/08/7d5e28abf51591fb4923892f43dd", - "assets/build/ba_data/textures/zoeIcon_preview.png": "https://files.ballistica.net/cache/ba1/e2/af/ab381c9d7242aedf8535fc90252f", - "assets/build/pylib-android/__future__.py": "https://files.ballistica.net/cache/ba1/ca/fd/945e2163508dd4f4ab07c3f0f86c", - "assets/build/pylib-android/__phello__.foo.py": "https://files.ballistica.net/cache/ba1/18/38/a9706423d7445928a1c07345b100", - "assets/build/pylib-android/_aix_support.py": "https://files.ballistica.net/cache/ba1/8b/e1/ab1dea018409e398e2369eac2382", - "assets/build/pylib-android/_bootsubprocess.py": "https://files.ballistica.net/cache/ba1/bc/ce/d576eeb751cae56049dc067454fa", - "assets/build/pylib-android/_collections_abc.py": "https://files.ballistica.net/cache/ba1/0b/1f/e12b8485d524d08721f9af15da93", - "assets/build/pylib-android/_compat_pickle.py": "https://files.ballistica.net/cache/ba1/03/70/f2d2ed965337f8ed80f6fffb37e1", - "assets/build/pylib-android/_compression.py": "https://files.ballistica.net/cache/ba1/ce/ba/40525417342351d2b68ee0441840", - "assets/build/pylib-android/_markupbase.py": "https://files.ballistica.net/cache/ba1/6e/fa/843094e4e2260704331a98140aea", - "assets/build/pylib-android/_osx_support.py": "https://files.ballistica.net/cache/ba1/94/0a/b6a52752bc81475844f07f9274e3", - "assets/build/pylib-android/_py_abc.py": "https://files.ballistica.net/cache/ba1/5d/06/d6095f475b0d26f707d3d7484080", - "assets/build/pylib-android/_pydecimal.py": "https://files.ballistica.net/cache/ba1/f4/6a/4823eec7b012aa7e246a2f76ab56", - "assets/build/pylib-android/_pyio.py": "https://files.ballistica.net/cache/ba1/3b/32/cd02a387a3b28c59703a2a248f54", - "assets/build/pylib-android/_sitebuiltins.py": "https://files.ballistica.net/cache/ba1/d9/a6/6f567c74f87bb337ce42cff16d47", - "assets/build/pylib-android/_strptime.py": "https://files.ballistica.net/cache/ba1/03/b9/20bbd93982e9d620d42cacd1f4a2", - "assets/build/pylib-android/_threading_local.py": "https://files.ballistica.net/cache/ba1/da/4d/144f886b39ebc097fdcea22eecc8", - "assets/build/pylib-android/_weakrefset.py": "https://files.ballistica.net/cache/ba1/51/e9/e3d953fd714d8b74c65b708529d7", - "assets/build/pylib-android/abc.py": "https://files.ballistica.net/cache/ba1/dd/5d/479e9cf06248ed39cf30d7849609", - "assets/build/pylib-android/aifc.py": "https://files.ballistica.net/cache/ba1/e4/58/a75af0c597af1ed6a69b0d0408f3", - "assets/build/pylib-android/antigravity.py": "https://files.ballistica.net/cache/ba1/bc/5c/f8951b7abdac55a1a32d74b2fe50", - "assets/build/pylib-android/argparse.py": "https://files.ballistica.net/cache/ba1/a1/a6/6f3a5154526eaa600b76a533b126", - "assets/build/pylib-android/ast.py": "https://files.ballistica.net/cache/ba1/e4/5b/b25a82736f1a9f4e1f1f8b451a3a", - "assets/build/pylib-android/asynchat.py": "https://files.ballistica.net/cache/ba1/6a/73/7ba88b8e06566c40987426d73894", - "assets/build/pylib-android/asyncio/__init__.py": "https://files.ballistica.net/cache/ba1/03/13/e0b34767cf91bd9a931a7b5fa7a9", - "assets/build/pylib-android/asyncio/__main__.py": "https://files.ballistica.net/cache/ba1/00/05/038811ebbd5ac847aaf99cd8f2f9", - "assets/build/pylib-android/asyncio/base_events.py": "https://files.ballistica.net/cache/ba1/55/96/175d7ac6b04ae9b66a5e00ec6139", - "assets/build/pylib-android/asyncio/base_futures.py": "https://files.ballistica.net/cache/ba1/89/ee/3f980b3d7b480c7fe3d34ff876f5", - "assets/build/pylib-android/asyncio/base_subprocess.py": "https://files.ballistica.net/cache/ba1/72/c7/ccf47dbfa076fdf5dd38474a18dc", - "assets/build/pylib-android/asyncio/base_tasks.py": "https://files.ballistica.net/cache/ba1/f0/b8/3f75f12f1851216b9a16772dfa63", - "assets/build/pylib-android/asyncio/constants.py": "https://files.ballistica.net/cache/ba1/b6/63/66f781190cbd10a134616e67b516", - "assets/build/pylib-android/asyncio/coroutines.py": "https://files.ballistica.net/cache/ba1/72/cd/f838323eebe2eeb8f5ea19e5cebe", - "assets/build/pylib-android/asyncio/events.py": "https://files.ballistica.net/cache/ba1/1b/09/eabe0eff9865da97298147b689c1", - "assets/build/pylib-android/asyncio/exceptions.py": "https://files.ballistica.net/cache/ba1/aa/89/66db5cb3f71048a541224585f7fa", - "assets/build/pylib-android/asyncio/format_helpers.py": "https://files.ballistica.net/cache/ba1/ff/c2/2be0f7aa8dc71aa3a7cee83ed5c7", - "assets/build/pylib-android/asyncio/futures.py": "https://files.ballistica.net/cache/ba1/1a/0c/91c20706e3b9d5b21ab624dfefde", - "assets/build/pylib-android/asyncio/locks.py": "https://files.ballistica.net/cache/ba1/66/32/04fe594b13e982fa87ec267e2ca9", - "assets/build/pylib-android/asyncio/log.py": "https://files.ballistica.net/cache/ba1/da/7f/235e1251f8838a239dd3ec9e78c3", - "assets/build/pylib-android/asyncio/mixins.py": "https://files.ballistica.net/cache/ba1/54/f0/ccb908c33095088f37bd9920d65c", - "assets/build/pylib-android/asyncio/proactor_events.py": "https://files.ballistica.net/cache/ba1/4c/e2/bac13a2bebd4e78fa7f0db20ffc9", - "assets/build/pylib-android/asyncio/protocols.py": "https://files.ballistica.net/cache/ba1/17/b6/4a3fe4965cd04ad1092399ce7316", - "assets/build/pylib-android/asyncio/queues.py": "https://files.ballistica.net/cache/ba1/43/4d/26f1ed380b3bda3e2df2e37ca877", - "assets/build/pylib-android/asyncio/runners.py": "https://files.ballistica.net/cache/ba1/7f/48/6292d754af0b0344f39a9adc31c5", - "assets/build/pylib-android/asyncio/selector_events.py": "https://files.ballistica.net/cache/ba1/9e/0b/f3f5ae0768044d1335791ad7e194", - "assets/build/pylib-android/asyncio/sslproto.py": "https://files.ballistica.net/cache/ba1/c5/2b/1050d935d2f32dcac57b7ac22ef7", - "assets/build/pylib-android/asyncio/staggered.py": "https://files.ballistica.net/cache/ba1/f5/44/45851798434a9265934b4bb83368", - "assets/build/pylib-android/asyncio/streams.py": "https://files.ballistica.net/cache/ba1/2d/04/1224d72abd97b32e32663254accd", - "assets/build/pylib-android/asyncio/subprocess.py": "https://files.ballistica.net/cache/ba1/7b/26/00bb632c760f5d902729a0fa0d36", - "assets/build/pylib-android/asyncio/tasks.py": "https://files.ballistica.net/cache/ba1/9b/1e/bd8e3a36d0fd40fa6120acab3e13", - "assets/build/pylib-android/asyncio/threads.py": "https://files.ballistica.net/cache/ba1/86/be/92839ce16818b1b99703722fbfb7", - "assets/build/pylib-android/asyncio/transports.py": "https://files.ballistica.net/cache/ba1/cd/a4/36c4f1d9a91720d82dc74aa3d0d3", - "assets/build/pylib-android/asyncio/trsock.py": "https://files.ballistica.net/cache/ba1/52/c8/e4d2abe8aaccd35c8f79d5b77431", - "assets/build/pylib-android/asyncio/unix_events.py": "https://files.ballistica.net/cache/ba1/b8/35/a02541f68ac609779462a32ad2b8", - "assets/build/pylib-android/asyncio/windows_events.py": "https://files.ballistica.net/cache/ba1/b5/bb/b2455b4ff3c0fd6b5a6358f25e12", - "assets/build/pylib-android/asyncio/windows_utils.py": "https://files.ballistica.net/cache/ba1/2d/e4/dbd2379dd123a69aae5aa15ae6c2", - "assets/build/pylib-android/asyncore.py": "https://files.ballistica.net/cache/ba1/14/dc/4bce809a68a37364579f33753306", - "assets/build/pylib-android/base64.py": "https://files.ballistica.net/cache/ba1/3f/d9/3365630816a5133288401700e42f", - "assets/build/pylib-android/bdb.py": "https://files.ballistica.net/cache/ba1/f1/2f/c3fb18dddedb1f7dac7433463638", - "assets/build/pylib-android/binhex.py": "https://files.ballistica.net/cache/ba1/4b/e7/63c6bfa21977b4167847714df8f2", - "assets/build/pylib-android/bisect.py": "https://files.ballistica.net/cache/ba1/76/48/33d66068f92fe7b914bbaac8c943", - "assets/build/pylib-android/bz2.py": "https://files.ballistica.net/cache/ba1/ce/b2/0873f1ae536f0f5d9625b50b72aa", - "assets/build/pylib-android/cProfile.py": "https://files.ballistica.net/cache/ba1/c2/f9/c4bdc9b8caf07e4f005b01c42187", - "assets/build/pylib-android/calendar.py": "https://files.ballistica.net/cache/ba1/43/6b/8e77c82323313c9065b1f243abdc", - "assets/build/pylib-android/cgi.py": "https://files.ballistica.net/cache/ba1/48/15/cf7b543747c2185b2369fb63ebdd", - "assets/build/pylib-android/cgitb.py": "https://files.ballistica.net/cache/ba1/f5/d9/08ac5f41d9e716a5c21b7712b348", - "assets/build/pylib-android/chunk.py": "https://files.ballistica.net/cache/ba1/f6/fe/3c43d1dc84ee74b8a170c61271a3", - "assets/build/pylib-android/cmd.py": "https://files.ballistica.net/cache/ba1/f0/d9/8cec4bcbbfd195d46c3ad637df71", - "assets/build/pylib-android/code.py": "https://files.ballistica.net/cache/ba1/7a/a4/ee660f11ad995354a3b21efbfb1c", - "assets/build/pylib-android/codecs.py": "https://files.ballistica.net/cache/ba1/8b/ad/dfed9caf9c30c18782a581ff17d7", - "assets/build/pylib-android/codeop.py": "https://files.ballistica.net/cache/ba1/e9/e0/46dceb3902ba6466a7df6ac35f9f", - "assets/build/pylib-android/collections/__init__.py": "https://files.ballistica.net/cache/ba1/a3/1d/dc4bf9cc251a555a9b9e10c437d9", - "assets/build/pylib-android/collections/abc.py": "https://files.ballistica.net/cache/ba1/f3/59/731e8f57c99391bcb31da8916e19", - "assets/build/pylib-android/colorsys.py": "https://files.ballistica.net/cache/ba1/42/f9/07a6c13adc97bf5289d95a7c9127", - "assets/build/pylib-android/compileall.py": "https://files.ballistica.net/cache/ba1/18/01/0974dc4f20e7417e91459b15952f", - "assets/build/pylib-android/concurrent/__init__.py": "https://files.ballistica.net/cache/ba1/37/3e/87f9ab4111608e0442bc82ff572f", - "assets/build/pylib-android/concurrent/futures/__init__.py": "https://files.ballistica.net/cache/ba1/0a/eb/2954f0e71c4a1e71db5c13548aec", - "assets/build/pylib-android/concurrent/futures/_base.py": "https://files.ballistica.net/cache/ba1/77/02/cd3c13ded2262bb023361a801345", - "assets/build/pylib-android/concurrent/futures/process.py": "https://files.ballistica.net/cache/ba1/fe/ed/9bd137c04de2fa8045a307c50626", - "assets/build/pylib-android/concurrent/futures/thread.py": "https://files.ballistica.net/cache/ba1/54/c0/f14c081285722cd0bfdf7a6a3990", - "assets/build/pylib-android/configparser.py": "https://files.ballistica.net/cache/ba1/db/70/a2ddcd9e05e9fb0d6d10afc21510", - "assets/build/pylib-android/contextlib.py": "https://files.ballistica.net/cache/ba1/4a/ed/5ea5c6e0272355d280fbe712dfb8", - "assets/build/pylib-android/contextvars.py": "https://files.ballistica.net/cache/ba1/ed/ff/2f1089520caf4910564799a71d33", - "assets/build/pylib-android/copy.py": "https://files.ballistica.net/cache/ba1/39/e2/4cd03fd5fa181c2e8b0864da780a", - "assets/build/pylib-android/copyreg.py": "https://files.ballistica.net/cache/ba1/3f/c8/f272ff19185f60feb87b4bb95314", - "assets/build/pylib-android/crypt.py": "https://files.ballistica.net/cache/ba1/bf/26/6a8ef29965b7a22c9a5ad042b8b0", - "assets/build/pylib-android/csv.py": "https://files.ballistica.net/cache/ba1/37/81/c6e12b2dc921131d87070ead7ff8", - "assets/build/pylib-android/ctypes/__init__.py": "https://files.ballistica.net/cache/ba1/71/ad/62f870b48def255b2904695874b9", - "assets/build/pylib-android/ctypes/_aix.py": "https://files.ballistica.net/cache/ba1/73/e6/416ed3219c8fb6396a6d189ab99f", - "assets/build/pylib-android/ctypes/_endian.py": "https://files.ballistica.net/cache/ba1/4c/82/46e7f99faf6d1fac55192c5d06e1", - "assets/build/pylib-android/ctypes/macholib/__init__.py": "https://files.ballistica.net/cache/ba1/44/d7/a915d5da7e8ef1030b43bb9b51ab", - "assets/build/pylib-android/ctypes/macholib/dyld.py": "https://files.ballistica.net/cache/ba1/f9/66/e601b611dac985d7a89b84e739ba", - "assets/build/pylib-android/ctypes/macholib/dylib.py": "https://files.ballistica.net/cache/ba1/70/15/1df06874f788afb7b6742a5812a7", - "assets/build/pylib-android/ctypes/macholib/framework.py": "https://files.ballistica.net/cache/ba1/86/80/861e32730cec812366d7d06f6185", - "assets/build/pylib-android/ctypes/util.py": "https://files.ballistica.net/cache/ba1/c5/37/5bc64177f2b03f1b240712b330d5", - "assets/build/pylib-android/ctypes/wintypes.py": "https://files.ballistica.net/cache/ba1/48/c4/069f51da9b065dad73b199acb71b", - "assets/build/pylib-android/curses/__init__.py": "https://files.ballistica.net/cache/ba1/0e/8e/5e9ad5cbe8d8e09a61e5c186ccfb", - "assets/build/pylib-android/curses/ascii.py": "https://files.ballistica.net/cache/ba1/d9/f8/0a6587dae44d1694145b0dc96bc1", - "assets/build/pylib-android/curses/has_key.py": "https://files.ballistica.net/cache/ba1/a0/10/afbfbd5688090da7ea41e933174b", - "assets/build/pylib-android/curses/panel.py": "https://files.ballistica.net/cache/ba1/42/0f/580d5d6de90a64ade37f0a8e4696", - "assets/build/pylib-android/curses/textpad.py": "https://files.ballistica.net/cache/ba1/a9/3f/ac729e39c1c9fcecd8e3525c7079", - "assets/build/pylib-android/dataclasses.py": "https://files.ballistica.net/cache/ba1/76/e6/8a82672bae89691b0a0dabd15cc7", - "assets/build/pylib-android/datetime.py": "https://files.ballistica.net/cache/ba1/31/dc/651eaa03a087f60726a944a2ee92", - "assets/build/pylib-android/decimal.py": "https://files.ballistica.net/cache/ba1/92/94/b8be378718b3ede8f05f07aa257b", - "assets/build/pylib-android/difflib.py": "https://files.ballistica.net/cache/ba1/b6/80/e34a54d317e56d1cd9778457afc5", - "assets/build/pylib-android/dis.py": "https://files.ballistica.net/cache/ba1/99/a5/3074e813c5188b690b6bac77f7f4", - "assets/build/pylib-android/doctest.py": "https://files.ballistica.net/cache/ba1/dd/f2/64a5506888615fe045917a322b80", - "assets/build/pylib-android/email/__init__.py": "https://files.ballistica.net/cache/ba1/2b/f0/8c85ab15e7cdbdaa0e1705223012", - "assets/build/pylib-android/email/_encoded_words.py": "https://files.ballistica.net/cache/ba1/f1/20/54b74af7ab4f03f89f54cfa29b1b", - "assets/build/pylib-android/email/_header_value_parser.py": "https://files.ballistica.net/cache/ba1/31/3a/dbc2461fc26b2e3adf28c4796b1c", - "assets/build/pylib-android/email/_parseaddr.py": "https://files.ballistica.net/cache/ba1/ff/35/b427b328dc4c49aae4db0e2bfc72", - "assets/build/pylib-android/email/_policybase.py": "https://files.ballistica.net/cache/ba1/19/f9/844a8a848bc5670a810d06f0a6de", - "assets/build/pylib-android/email/base64mime.py": "https://files.ballistica.net/cache/ba1/e0/5f/25fbb5f0dde9de81ef3a6177a01f", - "assets/build/pylib-android/email/charset.py": "https://files.ballistica.net/cache/ba1/d0/28/dac9a2adff3763f6ae413f26df95", - "assets/build/pylib-android/email/contentmanager.py": "https://files.ballistica.net/cache/ba1/c2/1d/0acd924b8731b932e4a6ced58d77", - "assets/build/pylib-android/email/encoders.py": "https://files.ballistica.net/cache/ba1/7a/9c/eca8d9e60fa733457fc32facd2fe", - "assets/build/pylib-android/email/errors.py": "https://files.ballistica.net/cache/ba1/93/32/f046674ffc01766eeb9c54dd0830", - "assets/build/pylib-android/email/feedparser.py": "https://files.ballistica.net/cache/ba1/98/6d/61c614d442ca8451320ca7fe4e86", - "assets/build/pylib-android/email/generator.py": "https://files.ballistica.net/cache/ba1/f6/2c/862c0a2aa12d0e1c0ce1ca305299", - "assets/build/pylib-android/email/header.py": "https://files.ballistica.net/cache/ba1/bb/cb/194570894c14063cd85ea2d8ab6a", - "assets/build/pylib-android/email/headerregistry.py": "https://files.ballistica.net/cache/ba1/f3/df/8cc39142f0c83f927d3d5977d8fb", - "assets/build/pylib-android/email/iterators.py": "https://files.ballistica.net/cache/ba1/a5/02/2f56787a3fb91547c61284d7facd", - "assets/build/pylib-android/email/message.py": "https://files.ballistica.net/cache/ba1/5d/5f/3a462d2034f763839e73f4286d85", - "assets/build/pylib-android/email/mime/__init__.py": "https://files.ballistica.net/cache/ba1/42/fb/835abe12a4e1e72a5d1711d12cde", - "assets/build/pylib-android/email/mime/application.py": "https://files.ballistica.net/cache/ba1/47/49/cb957a7a159b5f2259115b7ea27a", - "assets/build/pylib-android/email/mime/audio.py": "https://files.ballistica.net/cache/ba1/67/1e/7ba0091888a55ada476f1300e686", - "assets/build/pylib-android/email/mime/base.py": "https://files.ballistica.net/cache/ba1/aa/67/3e867e08727884c5b438498bb3f6", - "assets/build/pylib-android/email/mime/image.py": "https://files.ballistica.net/cache/ba1/aa/86/4966d5e74352b6445ff06e5e6307", - "assets/build/pylib-android/email/mime/message.py": "https://files.ballistica.net/cache/ba1/0c/9f/264b0e5723c16702abcda09fb6de", - "assets/build/pylib-android/email/mime/multipart.py": "https://files.ballistica.net/cache/ba1/10/5f/9a485ba2efc7d5ccf77cbccf4318", - "assets/build/pylib-android/email/mime/nonmultipart.py": "https://files.ballistica.net/cache/ba1/aa/0a/e63093210171b2557041ba09bf9f", - "assets/build/pylib-android/email/mime/text.py": "https://files.ballistica.net/cache/ba1/07/ca/b27257a73ac29c22e0cd6fe664b1", - "assets/build/pylib-android/email/parser.py": "https://files.ballistica.net/cache/ba1/56/8f/cfad8593bd540c4c2d1b9ab9e133", - "assets/build/pylib-android/email/policy.py": "https://files.ballistica.net/cache/ba1/a9/f3/301e5f1e73bed7a7bfa4a113ab14", - "assets/build/pylib-android/email/quoprimime.py": "https://files.ballistica.net/cache/ba1/aa/32/7c1d81e4b7b757020947292f4031", - "assets/build/pylib-android/email/utils.py": "https://files.ballistica.net/cache/ba1/43/de/398b63a9a976feaf5e9e9edc15f8", - "assets/build/pylib-android/encodings/__init__.py": "https://files.ballistica.net/cache/ba1/35/20/c5afc6ea1e780c897a2d4f3f5613", - "assets/build/pylib-android/encodings/aliases.py": "https://files.ballistica.net/cache/ba1/6e/5c/2f911ed1bbeec1fce518171c32bc", - "assets/build/pylib-android/encodings/ascii.py": "https://files.ballistica.net/cache/ba1/f6/30/d35b4c5d478856f618208177d679", - "assets/build/pylib-android/encodings/base64_codec.py": "https://files.ballistica.net/cache/ba1/7f/03/88c0997433cffad3d142857389b8", - "assets/build/pylib-android/encodings/big5.py": "https://files.ballistica.net/cache/ba1/3c/a3/0310d813b4a6822b0affdec62f00", - "assets/build/pylib-android/encodings/big5hkscs.py": "https://files.ballistica.net/cache/ba1/b7/8c/350bfcbf5b599cb3aa8d974736df", - "assets/build/pylib-android/encodings/bz2_codec.py": "https://files.ballistica.net/cache/ba1/bc/55/37b8e3a0b64ee576b844e95d907c", - "assets/build/pylib-android/encodings/charmap.py": "https://files.ballistica.net/cache/ba1/b8/0d/c1d2b8de9161eb06eb891eac3207", - "assets/build/pylib-android/encodings/cp037.py": "https://files.ballistica.net/cache/ba1/b9/1d/d51031deb90c819a68084050da37", - "assets/build/pylib-android/encodings/cp1006.py": "https://files.ballistica.net/cache/ba1/b3/f2/7862d3a1bb6128723619157701c2", - "assets/build/pylib-android/encodings/cp1026.py": "https://files.ballistica.net/cache/ba1/bb/6e/f016cd0b1d89af6f1906459b7007", - "assets/build/pylib-android/encodings/cp1125.py": "https://files.ballistica.net/cache/ba1/eb/57/5c9b8e2a6ef4c396fd75204777a6", - "assets/build/pylib-android/encodings/cp1140.py": "https://files.ballistica.net/cache/ba1/ba/df/df5731bb8844cbd6a21365ace4bd", - "assets/build/pylib-android/encodings/cp1250.py": "https://files.ballistica.net/cache/ba1/b1/ef/596fa6b94f9e646e763fd2ad4161", - "assets/build/pylib-android/encodings/cp1251.py": "https://files.ballistica.net/cache/ba1/2e/33/5c09f90b327a501d3fd1e7df6948", - "assets/build/pylib-android/encodings/cp1252.py": "https://files.ballistica.net/cache/ba1/45/a6/991d629c2f9d265eccc4ae558858", - "assets/build/pylib-android/encodings/cp1253.py": "https://files.ballistica.net/cache/ba1/cd/07/fbdfc8dcbc1d43203d36a4450e14", - "assets/build/pylib-android/encodings/cp1254.py": "https://files.ballistica.net/cache/ba1/22/1c/1e39b6e07068514db856667becc2", - "assets/build/pylib-android/encodings/cp1255.py": "https://files.ballistica.net/cache/ba1/18/7e/36bf478804f3e39284a88b3ff815", - "assets/build/pylib-android/encodings/cp1256.py": "https://files.ballistica.net/cache/ba1/ee/e9/2501fe4b6099b83058bed96d9fb6", - "assets/build/pylib-android/encodings/cp1257.py": "https://files.ballistica.net/cache/ba1/65/2d/2f467440a4c2b693a41bb56ef7bb", - "assets/build/pylib-android/encodings/cp1258.py": "https://files.ballistica.net/cache/ba1/79/45/4bbfe196a886f18e89696a44d0e6", - "assets/build/pylib-android/encodings/cp273.py": "https://files.ballistica.net/cache/ba1/21/a8/4e95608a9042c4af19dad02b1063", - "assets/build/pylib-android/encodings/cp424.py": "https://files.ballistica.net/cache/ba1/01/ae/24924c4383da3eea407dd9a76ce6", - "assets/build/pylib-android/encodings/cp437.py": "https://files.ballistica.net/cache/ba1/11/44/0a615f0e2c8dd60863f351c25493", - "assets/build/pylib-android/encodings/cp500.py": "https://files.ballistica.net/cache/ba1/b5/69/54dcc4070d53074fc8912b290160", - "assets/build/pylib-android/encodings/cp720.py": "https://files.ballistica.net/cache/ba1/5d/f1/fd1d890b59934c857cc3f7ca6a4c", - "assets/build/pylib-android/encodings/cp737.py": "https://files.ballistica.net/cache/ba1/85/b5/a9c80dfd08a1e52ebff80411998a", - "assets/build/pylib-android/encodings/cp775.py": "https://files.ballistica.net/cache/ba1/8d/8f/21ec77013920bcd41253555b5474", - "assets/build/pylib-android/encodings/cp850.py": "https://files.ballistica.net/cache/ba1/34/11/29fcdecce75d3198996e119f9008", - "assets/build/pylib-android/encodings/cp852.py": "https://files.ballistica.net/cache/ba1/21/22/df8b2e0ec53243bdfc91fe6c9a54", - "assets/build/pylib-android/encodings/cp855.py": "https://files.ballistica.net/cache/ba1/24/ff/ae62b0d02f3a6cba83c6bfa7be42", - "assets/build/pylib-android/encodings/cp856.py": "https://files.ballistica.net/cache/ba1/4a/4c/0124250ad2bd7695a6dada284379", - "assets/build/pylib-android/encodings/cp857.py": "https://files.ballistica.net/cache/ba1/6b/6d/60857b6f246c03118016070c7d87", - "assets/build/pylib-android/encodings/cp858.py": "https://files.ballistica.net/cache/ba1/e0/d0/16984bc761be984273fca6960cb9", - "assets/build/pylib-android/encodings/cp860.py": "https://files.ballistica.net/cache/ba1/c1/21/2bfabb4de3ce47023f563126d8b9", - "assets/build/pylib-android/encodings/cp861.py": "https://files.ballistica.net/cache/ba1/98/60/8e0ad05ead55348d9d119fb9d4f6", - "assets/build/pylib-android/encodings/cp862.py": "https://files.ballistica.net/cache/ba1/1e/c0/d598b42de9ed150add9793625d77", - "assets/build/pylib-android/encodings/cp863.py": "https://files.ballistica.net/cache/ba1/02/e9/6143ea052557e93b8fa0dea4a6c4", - "assets/build/pylib-android/encodings/cp864.py": "https://files.ballistica.net/cache/ba1/e8/cb/0064d19f7914047da4da7bddaa90", - "assets/build/pylib-android/encodings/cp865.py": "https://files.ballistica.net/cache/ba1/23/20/e562252314b216ce9a5b6a8edff8", - "assets/build/pylib-android/encodings/cp866.py": "https://files.ballistica.net/cache/ba1/ee/a2/bb6e0d81c1dbc9945ff4910d5e85", - "assets/build/pylib-android/encodings/cp869.py": "https://files.ballistica.net/cache/ba1/17/f8/6154bc87f25c60141a253594e3cb", - "assets/build/pylib-android/encodings/cp874.py": "https://files.ballistica.net/cache/ba1/2c/b5/7ac9271844af85abe89b9ef1da6c", - "assets/build/pylib-android/encodings/cp875.py": "https://files.ballistica.net/cache/ba1/9d/3a/720f75fa5208cc3ab1218a5f2bf6", - "assets/build/pylib-android/encodings/cp932.py": "https://files.ballistica.net/cache/ba1/7b/16/da44f5555bcd4fa0cdaafc86c6f6", - "assets/build/pylib-android/encodings/cp949.py": "https://files.ballistica.net/cache/ba1/e1/75/487d58284f755d72234a07497294", - "assets/build/pylib-android/encodings/cp950.py": "https://files.ballistica.net/cache/ba1/37/d7/27225c2ed97911fe299c0fb2c70a", - "assets/build/pylib-android/encodings/euc_jis_2004.py": "https://files.ballistica.net/cache/ba1/f8/31/3adb9eca03c8442d56ec44135b74", - "assets/build/pylib-android/encodings/euc_jisx0213.py": "https://files.ballistica.net/cache/ba1/31/69/17fe1ff05de8be710e7dc8480e06", - "assets/build/pylib-android/encodings/euc_jp.py": "https://files.ballistica.net/cache/ba1/ee/36/8dc49cd39cf75056dacf3f88fda7", - "assets/build/pylib-android/encodings/euc_kr.py": "https://files.ballistica.net/cache/ba1/76/ee/b673e34e5cd785131312b6158bf9", - "assets/build/pylib-android/encodings/gb18030.py": "https://files.ballistica.net/cache/ba1/f0/ea/874727b04c8b63613a83f83262e8", - "assets/build/pylib-android/encodings/gb2312.py": "https://files.ballistica.net/cache/ba1/06/9f/57af60a0e4cf61969676e17d8c26", - "assets/build/pylib-android/encodings/gbk.py": "https://files.ballistica.net/cache/ba1/f0/3c/103d181c0c97ca442d558489cb27", - "assets/build/pylib-android/encodings/hex_codec.py": "https://files.ballistica.net/cache/ba1/da/02/4653fafb4df0fb804f00856045de", - "assets/build/pylib-android/encodings/hp_roman8.py": "https://files.ballistica.net/cache/ba1/c3/76/cc370f0ce146dadb10fe1e0c5a93", - "assets/build/pylib-android/encodings/hz.py": "https://files.ballistica.net/cache/ba1/36/64/8a8471f0e6933b5367550c58bdbe", - "assets/build/pylib-android/encodings/idna.py": "https://files.ballistica.net/cache/ba1/0a/2b/fb5496dae088a1a3e9115ea50c98", - "assets/build/pylib-android/encodings/iso2022_jp.py": "https://files.ballistica.net/cache/ba1/f8/e0/0abd46c92c6451376c5a4548fd63", - "assets/build/pylib-android/encodings/iso2022_jp_1.py": "https://files.ballistica.net/cache/ba1/99/2a/b67952ec3202eb7832ea66ccb08d", - "assets/build/pylib-android/encodings/iso2022_jp_2.py": "https://files.ballistica.net/cache/ba1/0a/67/f27daab54ac7fba55d627b706bd8", - "assets/build/pylib-android/encodings/iso2022_jp_2004.py": "https://files.ballistica.net/cache/ba1/80/cf/8d804bed4e8b233f363defa4bc59", - "assets/build/pylib-android/encodings/iso2022_jp_3.py": "https://files.ballistica.net/cache/ba1/8e/95/8e81cd7972033ec7e261b7cdb099", - "assets/build/pylib-android/encodings/iso2022_jp_ext.py": "https://files.ballistica.net/cache/ba1/e1/28/460a70694e338d6b37a06359ee66", - "assets/build/pylib-android/encodings/iso2022_kr.py": "https://files.ballistica.net/cache/ba1/c9/31/13978e225b6f69ace3684003800f", - "assets/build/pylib-android/encodings/iso8859_1.py": "https://files.ballistica.net/cache/ba1/46/fc/00bf161e7844447cf464dc493052", - "assets/build/pylib-android/encodings/iso8859_10.py": "https://files.ballistica.net/cache/ba1/41/41/fb70b027b6445586cd7a5fe07cb0", - "assets/build/pylib-android/encodings/iso8859_11.py": "https://files.ballistica.net/cache/ba1/c5/df/457883123ad1849baea3fa6b7c96", - "assets/build/pylib-android/encodings/iso8859_13.py": "https://files.ballistica.net/cache/ba1/83/20/af9d1178005ca789c1ce9021ce68", - "assets/build/pylib-android/encodings/iso8859_14.py": "https://files.ballistica.net/cache/ba1/e9/ff/df220ef37d6a4d500ad976e2bb4b", - "assets/build/pylib-android/encodings/iso8859_15.py": "https://files.ballistica.net/cache/ba1/17/0a/9385fa20fb06d13645d0f0da8c01", - "assets/build/pylib-android/encodings/iso8859_16.py": "https://files.ballistica.net/cache/ba1/c2/5d/ddf2a9e44571834c9edec7bd8f7c", - "assets/build/pylib-android/encodings/iso8859_2.py": "https://files.ballistica.net/cache/ba1/a7/3a/b1fb5790e68a43ce295a6620c731", - "assets/build/pylib-android/encodings/iso8859_3.py": "https://files.ballistica.net/cache/ba1/03/76/f59da12e66ef8c09c74003e7eb98", - "assets/build/pylib-android/encodings/iso8859_4.py": "https://files.ballistica.net/cache/ba1/a8/08/ee448053772dfe98d03cd3d0e0a3", - "assets/build/pylib-android/encodings/iso8859_5.py": "https://files.ballistica.net/cache/ba1/9a/23/8a5d429194d565a1acbc57b6d7f9", - "assets/build/pylib-android/encodings/iso8859_6.py": "https://files.ballistica.net/cache/ba1/f4/67/ae7a87259197da1b617cc6b208fe", - "assets/build/pylib-android/encodings/iso8859_7.py": "https://files.ballistica.net/cache/ba1/75/53/6f20d390025bf78ae3c1db3af017", - "assets/build/pylib-android/encodings/iso8859_8.py": "https://files.ballistica.net/cache/ba1/7e/19/d45e4927c8f56d2debf42f065bbc", - "assets/build/pylib-android/encodings/iso8859_9.py": "https://files.ballistica.net/cache/ba1/9d/cc/ff2a611f34a6146598f0de26a754", - "assets/build/pylib-android/encodings/johab.py": "https://files.ballistica.net/cache/ba1/fe/7d/f9fc279e2a233ec97677b6ea2857", - "assets/build/pylib-android/encodings/koi8_r.py": "https://files.ballistica.net/cache/ba1/9f/2f/334208f46f4ac179dfbd96af5644", - "assets/build/pylib-android/encodings/koi8_t.py": "https://files.ballistica.net/cache/ba1/62/d7/57e50f075fcf38d4f390a924aec2", - "assets/build/pylib-android/encodings/koi8_u.py": "https://files.ballistica.net/cache/ba1/ee/d4/faf13b503979f16200525ed9aa86", - "assets/build/pylib-android/encodings/kz1048.py": "https://files.ballistica.net/cache/ba1/0e/48/fc8cea0eb08e128d18afbd21bc40", - "assets/build/pylib-android/encodings/latin_1.py": "https://files.ballistica.net/cache/ba1/be/e2/98028955fa7784eabd731554df96", - "assets/build/pylib-android/encodings/mac_arabic.py": "https://files.ballistica.net/cache/ba1/4e/3d/c348df138dd892e764c653a5e3ad", - "assets/build/pylib-android/encodings/mac_croatian.py": "https://files.ballistica.net/cache/ba1/b7/1c/2e050fc360dc363d976321d0196b", - "assets/build/pylib-android/encodings/mac_cyrillic.py": "https://files.ballistica.net/cache/ba1/ce/05/72375c004b9fbce45e1ae2ed9fa3", - "assets/build/pylib-android/encodings/mac_farsi.py": "https://files.ballistica.net/cache/ba1/fb/60/2864afabbe0f4ef2d0da202b40d8", - "assets/build/pylib-android/encodings/mac_greek.py": "https://files.ballistica.net/cache/ba1/9a/f2/17bce804fdcd4d972ce0a4b7c01d", - "assets/build/pylib-android/encodings/mac_iceland.py": "https://files.ballistica.net/cache/ba1/10/2d/e73e6e2588ab5ebce82cad367e0d", - "assets/build/pylib-android/encodings/mac_latin2.py": "https://files.ballistica.net/cache/ba1/97/f1/f6dac533cab8ddf7a7a09323b907", - "assets/build/pylib-android/encodings/mac_roman.py": "https://files.ballistica.net/cache/ba1/40/3c/ed4c5767a8af2d4b68a6a02a8516", - "assets/build/pylib-android/encodings/mac_romanian.py": "https://files.ballistica.net/cache/ba1/3a/dc/0ba9e70ecafba40628e7148a6e3a", - "assets/build/pylib-android/encodings/mac_turkish.py": "https://files.ballistica.net/cache/ba1/2c/f8/792e0c8f6e8a81cc28db3aba4721", - "assets/build/pylib-android/encodings/mbcs.py": "https://files.ballistica.net/cache/ba1/8c/a3/08588ac5ea8a1ee393e375b07824", - "assets/build/pylib-android/encodings/oem.py": "https://files.ballistica.net/cache/ba1/89/7c/80a3ecd4520886dc361cfc656f3c", - "assets/build/pylib-android/encodings/palmos.py": "https://files.ballistica.net/cache/ba1/78/a9/9ef953f611452d621d44ac394e79", - "assets/build/pylib-android/encodings/ptcp154.py": "https://files.ballistica.net/cache/ba1/1c/c2/03b9c034f875cb7ec719396258d0", - "assets/build/pylib-android/encodings/punycode.py": "https://files.ballistica.net/cache/ba1/5c/08/5f08b470b9dfeca97acb5e500fab", - "assets/build/pylib-android/encodings/quopri_codec.py": "https://files.ballistica.net/cache/ba1/75/d7/28a861eed016c9c3054a32732575", - "assets/build/pylib-android/encodings/raw_unicode_escape.py": "https://files.ballistica.net/cache/ba1/4e/bd/9895c1da6a2e3e90a37e30dfd669", - "assets/build/pylib-android/encodings/rot_13.py": "https://files.ballistica.net/cache/ba1/38/24/555e226e476d804d5f79ca0e65b8", - "assets/build/pylib-android/encodings/shift_jis.py": "https://files.ballistica.net/cache/ba1/35/51/a11ba9f3b641f18467e3a5b557df", - "assets/build/pylib-android/encodings/shift_jis_2004.py": "https://files.ballistica.net/cache/ba1/5a/33/b98501b09860200cc99e829adbf9", - "assets/build/pylib-android/encodings/shift_jisx0213.py": "https://files.ballistica.net/cache/ba1/a5/df/cf6603600412c2511c7bb512f3bb", - "assets/build/pylib-android/encodings/tis_620.py": "https://files.ballistica.net/cache/ba1/c3/b4/fd078646ff6494baaf0453092c79", - "assets/build/pylib-android/encodings/undefined.py": "https://files.ballistica.net/cache/ba1/09/43/e8b79735ec6bdc629c2a45264e8e", - "assets/build/pylib-android/encodings/unicode_escape.py": "https://files.ballistica.net/cache/ba1/7a/f2/81db4fe366bc9a62fe2a4b1119d3", - "assets/build/pylib-android/encodings/utf_16.py": "https://files.ballistica.net/cache/ba1/e8/96/2999d7838c6ed0f435e3910ca1ee", - "assets/build/pylib-android/encodings/utf_16_be.py": "https://files.ballistica.net/cache/ba1/c4/ee/77e5c5cb580218e139226a79416e", - "assets/build/pylib-android/encodings/utf_16_le.py": "https://files.ballistica.net/cache/ba1/fe/89/c8a5cf4f08828b14a9911e2534a7", - "assets/build/pylib-android/encodings/utf_32.py": "https://files.ballistica.net/cache/ba1/cd/2f/9e28ef078388ba5055fda11b4e5b", - "assets/build/pylib-android/encodings/utf_32_be.py": "https://files.ballistica.net/cache/ba1/ca/d4/68f9f9d87eb1a3c9c88e9885c09b", - "assets/build/pylib-android/encodings/utf_32_le.py": "https://files.ballistica.net/cache/ba1/c1/c4/e348a6e590891289dd1f3382c4e8", - "assets/build/pylib-android/encodings/utf_7.py": "https://files.ballistica.net/cache/ba1/c4/e5/e4f1f3d9b6c4e1d4844dff2526c2", - "assets/build/pylib-android/encodings/utf_8.py": "https://files.ballistica.net/cache/ba1/2a/79/82132c5fdae014fd3ab6513bd39b", - "assets/build/pylib-android/encodings/utf_8_sig.py": "https://files.ballistica.net/cache/ba1/c5/56/c3d98c0cb4cf569fb833ed919cc1", - "assets/build/pylib-android/encodings/uu_codec.py": "https://files.ballistica.net/cache/ba1/f9/64/0107520eca9130ca870cec675bf0", - "assets/build/pylib-android/encodings/zlib_codec.py": "https://files.ballistica.net/cache/ba1/b4/3f/7369ee7aa1aa36b098c3b33ea31b", - "assets/build/pylib-android/enum.py": "https://files.ballistica.net/cache/ba1/56/4c/86116596f3af3892c7d77f5a34b2", - "assets/build/pylib-android/filecmp.py": "https://files.ballistica.net/cache/ba1/0e/1e/8f865dd6bcc2d60880839762e007", - "assets/build/pylib-android/fileinput.py": "https://files.ballistica.net/cache/ba1/9e/5c/ff3df618103ee025749af647a0d8", - "assets/build/pylib-android/fnmatch.py": "https://files.ballistica.net/cache/ba1/7d/7a/889383848f167117d0b473e1bc39", - "assets/build/pylib-android/fractions.py": "https://files.ballistica.net/cache/ba1/77/8a/fa5448bc945714954bd2e4bd0fc1", - "assets/build/pylib-android/ftplib.py": "https://files.ballistica.net/cache/ba1/15/f9/66c87baf6cd7be154d0b904a131a", - "assets/build/pylib-android/functools.py": "https://files.ballistica.net/cache/ba1/5b/5c/09cccab90e637eb0de81deb20cad", - "assets/build/pylib-android/genericpath.py": "https://files.ballistica.net/cache/ba1/a4/d1/a132fc4c20d49468d9aee1667a18", - "assets/build/pylib-android/getopt.py": "https://files.ballistica.net/cache/ba1/5c/25/34e54811bd07a3b7a15e60c67094", - "assets/build/pylib-android/getpass.py": "https://files.ballistica.net/cache/ba1/7f/27/2225631bb706fa77edbb0870f96d", - "assets/build/pylib-android/gettext.py": "https://files.ballistica.net/cache/ba1/06/75/330b5daf8b0c62599c1cb5d39e74", - "assets/build/pylib-android/glob.py": "https://files.ballistica.net/cache/ba1/6f/b8/cdaa5dc01bbd128f1b0cc90d7df2", - "assets/build/pylib-android/graphlib.py": "https://files.ballistica.net/cache/ba1/31/cc/8f2e48dec338077a792d79a319ca", - "assets/build/pylib-android/gzip.py": "https://files.ballistica.net/cache/ba1/b9/cf/001b29feb9bff1e4be7c571f40e9", - "assets/build/pylib-android/hashlib.py": "https://files.ballistica.net/cache/ba1/4f/33/7de78cc92126b1ba3f5242b1803c", - "assets/build/pylib-android/heapq.py": "https://files.ballistica.net/cache/ba1/63/79/bb9abc2fb3665fffc59e588aba1f", - "assets/build/pylib-android/hmac.py": "https://files.ballistica.net/cache/ba1/1d/a6/4a78ec8815727fd28b0e8c10ef59", - "assets/build/pylib-android/html/__init__.py": "https://files.ballistica.net/cache/ba1/63/0b/9695269a02f0ec6d8b2b928d1f3f", - "assets/build/pylib-android/html/entities.py": "https://files.ballistica.net/cache/ba1/51/10/faad91b2e1fe51ade90c4618e88f", - "assets/build/pylib-android/html/parser.py": "https://files.ballistica.net/cache/ba1/f3/63/57d38fd5ba0432c401807fdd216e", - "assets/build/pylib-android/http/__init__.py": "https://files.ballistica.net/cache/ba1/80/a7/800c5f996f53155c5a626cdd6613", - "assets/build/pylib-android/http/client.py": "https://files.ballistica.net/cache/ba1/73/6e/d63677c78de7bebec669debb7d79", - "assets/build/pylib-android/http/cookiejar.py": "https://files.ballistica.net/cache/ba1/fa/f7/b408a492958fa81d4446239ec972", - "assets/build/pylib-android/http/cookies.py": "https://files.ballistica.net/cache/ba1/d3/8d/e6c3a78a0d420510ff0d88980dc0", - "assets/build/pylib-android/http/server.py": "https://files.ballistica.net/cache/ba1/85/54/cd529a8abb29aeeabd9d8c294e67", - "assets/build/pylib-android/imghdr.py": "https://files.ballistica.net/cache/ba1/3d/d0/4f7452be4865bbf2e54e67fff577", - "assets/build/pylib-android/imp.py": "https://files.ballistica.net/cache/ba1/fc/eb/0ca6343cfee8e4f7b6675ed0877d", - "assets/build/pylib-android/importlib/__init__.py": "https://files.ballistica.net/cache/ba1/7e/46/5e49e3ef0c37b4b67313cb07ceb1", - "assets/build/pylib-android/importlib/_abc.py": "https://files.ballistica.net/cache/ba1/02/ec/866c0f5a975adc181c96f07af1f5", - "assets/build/pylib-android/importlib/_adapters.py": "https://files.ballistica.net/cache/ba1/8d/c9/c3d06e3dca42bc340b700760c067", - "assets/build/pylib-android/importlib/_bootstrap.py": "https://files.ballistica.net/cache/ba1/3a/17/a55cbb66adb160250e72c548d5ac", - "assets/build/pylib-android/importlib/_bootstrap_external.py": "https://files.ballistica.net/cache/ba1/b0/30/67df0b31966cf5acf76b9a674ddc", - "assets/build/pylib-android/importlib/_common.py": "https://files.ballistica.net/cache/ba1/ca/8b/5730716057f9c7b7914367f423ae", - "assets/build/pylib-android/importlib/abc.py": "https://files.ballistica.net/cache/ba1/ba/6d/e10d561a1f3e6963f5f8ce8d8ce8", - "assets/build/pylib-android/importlib/machinery.py": "https://files.ballistica.net/cache/ba1/2e/7f/fa3ad6f35b9068e0a8ba20ef4b43", - "assets/build/pylib-android/importlib/metadata/__init__.py": "https://files.ballistica.net/cache/ba1/b1/96/b3ace3e0e9e8309acdf4456a2934", - "assets/build/pylib-android/importlib/metadata/_adapters.py": "https://files.ballistica.net/cache/ba1/56/38/6942e168cf31d2a758076e800a96", - "assets/build/pylib-android/importlib/metadata/_collections.py": "https://files.ballistica.net/cache/ba1/05/3a/f8b42e30d0a9e9b9d65285c70351", - "assets/build/pylib-android/importlib/metadata/_functools.py": "https://files.ballistica.net/cache/ba1/b0/96/8a2d3de8bc61d461ab2ded56c4ef", - "assets/build/pylib-android/importlib/metadata/_itertools.py": "https://files.ballistica.net/cache/ba1/13/d5/5b7b88945dc57e66519ac059c92f", - "assets/build/pylib-android/importlib/metadata/_meta.py": "https://files.ballistica.net/cache/ba1/e8/f8/f08c9487023a82ba193b51a2b45b", - "assets/build/pylib-android/importlib/metadata/_text.py": "https://files.ballistica.net/cache/ba1/5f/7a/212c41092c65ce7a005129d1bdab", - "assets/build/pylib-android/importlib/readers.py": "https://files.ballistica.net/cache/ba1/24/6d/fe072d3040ea41549870da83daa5", - "assets/build/pylib-android/importlib/resources.py": "https://files.ballistica.net/cache/ba1/72/69/2f0d57b718e2c763fa850503f6a2", - "assets/build/pylib-android/importlib/util.py": "https://files.ballistica.net/cache/ba1/5c/13/dc6eb9c324c3a7a30680f56b2712", - "assets/build/pylib-android/inspect.py": "https://files.ballistica.net/cache/ba1/68/27/da7d2295c7fa8d60d529f7155d9f", - "assets/build/pylib-android/io.py": "https://files.ballistica.net/cache/ba1/9b/0e/b9fb6e1de0991b21e97e9d1997e1", - "assets/build/pylib-android/ipaddress.py": "https://files.ballistica.net/cache/ba1/8e/de/a27a5427d9ef1bb7758e50ba20eb", - "assets/build/pylib-android/json/__init__.py": "https://files.ballistica.net/cache/ba1/0a/6b/bb8407abb743492bf84a2e472f7c", - "assets/build/pylib-android/json/decoder.py": "https://files.ballistica.net/cache/ba1/3f/bf/6fd2a01d31cd85e4c21cf2c0a5c8", - "assets/build/pylib-android/json/encoder.py": "https://files.ballistica.net/cache/ba1/57/77/3f0ccd21441273ff30116fa76ba3", - "assets/build/pylib-android/json/scanner.py": "https://files.ballistica.net/cache/ba1/c8/4b/bcc458a5047e9ac8064a607ee231", - "assets/build/pylib-android/json/tool.py": "https://files.ballistica.net/cache/ba1/1a/d8/4680b29889d3c98ef28c72b4b1fa", - "assets/build/pylib-android/keyword.py": "https://files.ballistica.net/cache/ba1/1b/14/61bda5d55c8fa1ca9e30b020dd9d", - "assets/build/pylib-android/linecache.py": "https://files.ballistica.net/cache/ba1/aa/b3/84866ba897948d79b4ba5228e5d4", - "assets/build/pylib-android/locale.py": "https://files.ballistica.net/cache/ba1/26/3c/de2979ddf0116302e13bafc208d0", - "assets/build/pylib-android/logging/__init__.py": "https://files.ballistica.net/cache/ba1/fb/d2/7c8decb22308e028679eb6a27fe9", - "assets/build/pylib-android/logging/config.py": "https://files.ballistica.net/cache/ba1/e5/25/56879b22e752cc2d0d0d995cb48e", - "assets/build/pylib-android/logging/handlers.py": "https://files.ballistica.net/cache/ba1/92/f2/6b975782b00ec1cdf04e2aebc90a", - "assets/build/pylib-android/lzma.py": "https://files.ballistica.net/cache/ba1/d4/77/1a5a8675c4abeddc0f27dee304d3", - "assets/build/pylib-android/mailbox.py": "https://files.ballistica.net/cache/ba1/e2/5b/852981ddeb6c208455146b9d65df", - "assets/build/pylib-android/mailcap.py": "https://files.ballistica.net/cache/ba1/4a/a2/61dfae45beaf22c9696d069ee06c", - "assets/build/pylib-android/mimetypes.py": "https://files.ballistica.net/cache/ba1/42/77/78a9acf6e152b54c179af11f745d", - "assets/build/pylib-android/modulefinder.py": "https://files.ballistica.net/cache/ba1/bd/bb/c27cf6cf4d87fd4557f19c549ab6", - "assets/build/pylib-android/netrc.py": "https://files.ballistica.net/cache/ba1/96/fd/ee323a4a15bd853ec33dbcb07389", - "assets/build/pylib-android/nntplib.py": "https://files.ballistica.net/cache/ba1/e9/97/b8c183562190b711e14163b213d8", - "assets/build/pylib-android/ntpath.py": "https://files.ballistica.net/cache/ba1/98/f2/5fbb3769aff111281ef928b7626a", - "assets/build/pylib-android/nturl2path.py": "https://files.ballistica.net/cache/ba1/a9/59/0611204983d7eeed580ab3c56e4d", - "assets/build/pylib-android/numbers.py": "https://files.ballistica.net/cache/ba1/9e/75/45a922718c7dfcd1b68948fb74a3", - "assets/build/pylib-android/opcode.py": "https://files.ballistica.net/cache/ba1/05/c9/19fdbc51b3644b3666598f2661b2", - "assets/build/pylib-android/operator.py": "https://files.ballistica.net/cache/ba1/c7/e1/faab3be799d4a4bfd844c2425b04", - "assets/build/pylib-android/optparse.py": "https://files.ballistica.net/cache/ba1/0f/08/ec8ce5e48392a3f9bdc3c07f49cc", - "assets/build/pylib-android/os.py": "https://files.ballistica.net/cache/ba1/98/a1/d44b87acd1e25e7fe895f0d7ce86", - "assets/build/pylib-android/pathlib.py": "https://files.ballistica.net/cache/ba1/8a/c7/07ab66e2e8fc2ca436d4c129377f", - "assets/build/pylib-android/pdb.py": "https://files.ballistica.net/cache/ba1/bf/d0/f2d06639315624e00183fd69c46d", - "assets/build/pylib-android/pickle.py": "https://files.ballistica.net/cache/ba1/b5/c5/6a5af50d16fc23f35633e8c3722b", - "assets/build/pylib-android/pickletools.py": "https://files.ballistica.net/cache/ba1/72/c5/182517538ac4e0c1b7d25bde80e4", - "assets/build/pylib-android/pipes.py": "https://files.ballistica.net/cache/ba1/ea/55/1be5012381daf85c7fecffa89825", - "assets/build/pylib-android/pkgutil.py": "https://files.ballistica.net/cache/ba1/0c/e5/32774efc6108dfbbff81db9859ab", - "assets/build/pylib-android/platform.py": "https://files.ballistica.net/cache/ba1/91/dd/1b8e043da292c0e9755de9eea097", - "assets/build/pylib-android/plistlib.py": "https://files.ballistica.net/cache/ba1/e4/b4/c991bcd1812d6ba77181a37a4cd9", - "assets/build/pylib-android/poplib.py": "https://files.ballistica.net/cache/ba1/45/2e/413a448cee745616d9b2a3be1678", - "assets/build/pylib-android/posixpath.py": "https://files.ballistica.net/cache/ba1/94/cd/03db2674af399101e65ae870188f", - "assets/build/pylib-android/pprint.py": "https://files.ballistica.net/cache/ba1/e0/ca/b8cf3ccff0e23256fff16566ad5b", - "assets/build/pylib-android/profile.py": "https://files.ballistica.net/cache/ba1/2c/5f/fe19381e5041a1bc781b35aeb5e8", - "assets/build/pylib-android/pstats.py": "https://files.ballistica.net/cache/ba1/9f/9c/4b003a13a13f588bfee2c1427f79", - "assets/build/pylib-android/pty.py": "https://files.ballistica.net/cache/ba1/db/f1/1d638ed363309a1eb5d487ca9205", - "assets/build/pylib-android/py_compile.py": "https://files.ballistica.net/cache/ba1/a7/9e/7f4e8da084e2d0761352120a331c", - "assets/build/pylib-android/pyclbr.py": "https://files.ballistica.net/cache/ba1/e2/ed/7a1e35b368c6de0b51b9d9edc4a1", - "assets/build/pylib-android/pydoc.py": "https://files.ballistica.net/cache/ba1/aa/e3/6409048f80e37ae5737adc9eafc9", - "assets/build/pylib-android/queue.py": "https://files.ballistica.net/cache/ba1/6e/d7/04bd9d41783a2e75af05e14241a9", - "assets/build/pylib-android/quopri.py": "https://files.ballistica.net/cache/ba1/0f/ad/93ff84558a4d8ca0607ab2c35df2", - "assets/build/pylib-android/random.py": "https://files.ballistica.net/cache/ba1/a8/ed/a10fe4ba8a637263efa5d6b5fec7", - "assets/build/pylib-android/re.py": "https://files.ballistica.net/cache/ba1/28/6f/c6088e235e06ea35a34bdd28f73f", - "assets/build/pylib-android/reprlib.py": "https://files.ballistica.net/cache/ba1/81/66/44ee9dceee6943006c4500ee3303", - "assets/build/pylib-android/rlcompleter.py": "https://files.ballistica.net/cache/ba1/41/1a/6e68fca92c466f362f227621ed81", - "assets/build/pylib-android/runpy.py": "https://files.ballistica.net/cache/ba1/f1/db/abb67fceafd6b354c3851f069884", - "assets/build/pylib-android/sched.py": "https://files.ballistica.net/cache/ba1/48/ff/80ac9aee80b53416c702c2a856d4", - "assets/build/pylib-android/secrets.py": "https://files.ballistica.net/cache/ba1/e2/1b/ffc4c1422b6ac0eda4924d2f241a", - "assets/build/pylib-android/selectors.py": "https://files.ballistica.net/cache/ba1/ba/53/3b7515983548e5de15ccd7d37fb7", - "assets/build/pylib-android/shelve.py": "https://files.ballistica.net/cache/ba1/70/fa/16d6647f0b0c8d1db51e54e5db92", - "assets/build/pylib-android/shlex.py": "https://files.ballistica.net/cache/ba1/bf/d2/45bdf01bfb2a8d468d6ff0534fb6", - "assets/build/pylib-android/shutil.py": "https://files.ballistica.net/cache/ba1/a6/a2/068d0972d8c54309226a95a62f2c", - "assets/build/pylib-android/signal.py": "https://files.ballistica.net/cache/ba1/49/e4/1b6cac607c01ffaccfc26f21eede", - "assets/build/pylib-android/site.py": "https://files.ballistica.net/cache/ba1/f0/15/6dc1d610a17dcd53be135b7d57a3", - "assets/build/pylib-android/smtpd.py": "https://files.ballistica.net/cache/ba1/d3/7b/4881d213384e3e4df8358906ea26", - "assets/build/pylib-android/smtplib.py": "https://files.ballistica.net/cache/ba1/e2/a8/0694cc406f334eaf4d6bf094ffaf", - "assets/build/pylib-android/sndhdr.py": "https://files.ballistica.net/cache/ba1/d8/db/7f9e8c520cd68d3cf3308e3a9923", - "assets/build/pylib-android/socket.py": "https://files.ballistica.net/cache/ba1/b5/38/017a990c761d8d75af5a969661f3", - "assets/build/pylib-android/socketserver.py": "https://files.ballistica.net/cache/ba1/0f/54/a474bf8ddc82d8155de1344e1318", - "assets/build/pylib-android/sqlite3/__init__.py": "https://files.ballistica.net/cache/ba1/7a/d3/a97bad7e5ce8d0e9cde349b1181e", - "assets/build/pylib-android/sqlite3/dbapi2.py": "https://files.ballistica.net/cache/ba1/6a/2b/5618418e0c8edfc04b6ff2f1058c", - "assets/build/pylib-android/sqlite3/dump.py": "https://files.ballistica.net/cache/ba1/1c/d8/dc3fb256cbb05c8fffb52c52404a", - "assets/build/pylib-android/sre_compile.py": "https://files.ballistica.net/cache/ba1/fc/be/bb3ebf34da4f6222e45f1e2e7a65", - "assets/build/pylib-android/sre_constants.py": "https://files.ballistica.net/cache/ba1/fc/54/c33aefcbc2dab38c050d444a3548", - "assets/build/pylib-android/sre_parse.py": "https://files.ballistica.net/cache/ba1/09/5d/b45c4c248ea9c70430f858e950f9", - "assets/build/pylib-android/ssl.py": "https://files.ballistica.net/cache/ba1/6d/a5/0555a143cd530ef6d04dfcfdf278", - "assets/build/pylib-android/stat.py": "https://files.ballistica.net/cache/ba1/03/1d/acbfa83d8d94d8a483c6a884b589", - "assets/build/pylib-android/statistics.py": "https://files.ballistica.net/cache/ba1/58/75/7788301683ae47efa836ef9cdb7c", - "assets/build/pylib-android/string.py": "https://files.ballistica.net/cache/ba1/71/7f/8d19b04aea3ea1fc6bb49534d244", - "assets/build/pylib-android/stringprep.py": "https://files.ballistica.net/cache/ba1/20/41/fcfc5f510286ead5f7f4678ac9ec", - "assets/build/pylib-android/struct.py": "https://files.ballistica.net/cache/ba1/37/67/74dea8e8f3831e802c3b5288e901", - "assets/build/pylib-android/subprocess.py": "https://files.ballistica.net/cache/ba1/49/04/1b24f46851ca9d92a4c7559ae349", - "assets/build/pylib-android/sunau.py": "https://files.ballistica.net/cache/ba1/e5/20/6b38eb6fd8a07086d219d9ab269b", - "assets/build/pylib-android/symtable.py": "https://files.ballistica.net/cache/ba1/51/1e/6efd09781ab558d4ccfdfda3204f", - "assets/build/pylib-android/sysconfig.py": "https://files.ballistica.net/cache/ba1/ab/ca/56bd2cc4682412d0ed87ce887e3e", - "assets/build/pylib-android/tabnanny.py": "https://files.ballistica.net/cache/ba1/f3/7e/b463d5f4ead23d34a36d0e559447", - "assets/build/pylib-android/tarfile.py": "https://files.ballistica.net/cache/ba1/bf/34/4f0dc6c91854780a4dfdd68ca04b", - "assets/build/pylib-android/telnetlib.py": "https://files.ballistica.net/cache/ba1/a5/81/6133a7707e3de363ddc642944e52", - "assets/build/pylib-android/tempfile.py": "https://files.ballistica.net/cache/ba1/67/78/34399b155c0db5f48a0ec8ea5c92", - "assets/build/pylib-android/textwrap.py": "https://files.ballistica.net/cache/ba1/3e/86/05b5dfd9776a28ada091b543077c", - "assets/build/pylib-android/this.py": "https://files.ballistica.net/cache/ba1/a8/fa/4d1152b689d75bc1a997ff34b799", - "assets/build/pylib-android/threading.py": "https://files.ballistica.net/cache/ba1/e4/09/0ae2163973a67d4b686c199caec2", - "assets/build/pylib-android/timeit.py": "https://files.ballistica.net/cache/ba1/68/56/ef8e9c86fdd966f0e8fcc03160ac", - "assets/build/pylib-android/token.py": "https://files.ballistica.net/cache/ba1/de/5c/e38eb5b2f3f3f4e01dbce7f25c86", - "assets/build/pylib-android/tokenize.py": "https://files.ballistica.net/cache/ba1/88/72/7d33fc2a2e89d0eeb2f2c4c20b66", - "assets/build/pylib-android/trace.py": "https://files.ballistica.net/cache/ba1/f3/54/8ae4070c15a5390f186c009c3180", - "assets/build/pylib-android/traceback.py": "https://files.ballistica.net/cache/ba1/d6/c1/1dacaf151acf74cf3b9a71b96846", - "assets/build/pylib-android/tracemalloc.py": "https://files.ballistica.net/cache/ba1/2c/4a/ea13ebd9c04d1db131d62880b2c6", - "assets/build/pylib-android/tty.py": "https://files.ballistica.net/cache/ba1/ad/19/a6ad29b8958fa9f5acc3cf71d3b2", - "assets/build/pylib-android/types.py": "https://files.ballistica.net/cache/ba1/ea/c2/ff8d0bb0c35a348b6ebeffb99631", - "assets/build/pylib-android/typing.py": "https://files.ballistica.net/cache/ba1/12/67/d943ce0b0e1b88e6f59ab97c9696", - "assets/build/pylib-android/urllib/__init__.py": "https://files.ballistica.net/cache/ba1/b0/56/87601ed47a5181d1e6a40eb4ea40", - "assets/build/pylib-android/urllib/error.py": "https://files.ballistica.net/cache/ba1/07/8c/573897fc3bdc6d3e2e8d449f17c7", - "assets/build/pylib-android/urllib/parse.py": "https://files.ballistica.net/cache/ba1/b8/d9/6cf19b9e635a9a8dcd9ac2e397de", - "assets/build/pylib-android/urllib/request.py": "https://files.ballistica.net/cache/ba1/ac/b2/4f16fc515abc96d77d3eb3666cc5", - "assets/build/pylib-android/urllib/response.py": "https://files.ballistica.net/cache/ba1/ac/f3/2d5bd4fe5cfcf640e8be71dec9a4", - "assets/build/pylib-android/urllib/robotparser.py": "https://files.ballistica.net/cache/ba1/a4/52/f0d03835bb08e08195aeebd05e04", - "assets/build/pylib-android/uu.py": "https://files.ballistica.net/cache/ba1/a1/89/070ed8553858a75fcafae4b7bd37", - "assets/build/pylib-android/uuid.py": "https://files.ballistica.net/cache/ba1/6c/3b/c018c6e9883e5218175592ec3343", - "assets/build/pylib-android/warnings.py": "https://files.ballistica.net/cache/ba1/23/25/0ed7968cb91c285e031fa93614db", - "assets/build/pylib-android/wave.py": "https://files.ballistica.net/cache/ba1/bf/f7/89182d61e9f3eb100e5efbff4243", - "assets/build/pylib-android/weakref.py": "https://files.ballistica.net/cache/ba1/3a/34/42234c435d2e753407b0c068a271", - "assets/build/pylib-android/webbrowser.py": "https://files.ballistica.net/cache/ba1/1d/62/609608141e8ce5c553b603b1e536", - "assets/build/pylib-android/xdrlib.py": "https://files.ballistica.net/cache/ba1/ec/bf/84d830dca1231ec1a67d8ccbb21f", - "assets/build/pylib-android/xml/__init__.py": "https://files.ballistica.net/cache/ba1/ba/67/bbd97e53f3db5ebc3abd3fef2275", - "assets/build/pylib-android/xml/dom/NodeFilter.py": "https://files.ballistica.net/cache/ba1/36/76/2a47e7bc727db1c44d157b23d2c3", - "assets/build/pylib-android/xml/dom/__init__.py": "https://files.ballistica.net/cache/ba1/4b/7b/185f8e04797815876d9a16210af7", - "assets/build/pylib-android/xml/dom/domreg.py": "https://files.ballistica.net/cache/ba1/db/f3/81553e1167748f7d22152ac83eba", - "assets/build/pylib-android/xml/dom/expatbuilder.py": "https://files.ballistica.net/cache/ba1/8e/f0/26f7a8d646c051dc29134411e5a3", - "assets/build/pylib-android/xml/dom/minicompat.py": "https://files.ballistica.net/cache/ba1/27/a6/6caea719e99063ca274ba3dd9883", - "assets/build/pylib-android/xml/dom/minidom.py": "https://files.ballistica.net/cache/ba1/53/8e/fa86c93dc783f50139a6df679f70", - "assets/build/pylib-android/xml/dom/pulldom.py": "https://files.ballistica.net/cache/ba1/69/93/695baf453ca1030e7f6a62eb977b", - "assets/build/pylib-android/xml/dom/xmlbuilder.py": "https://files.ballistica.net/cache/ba1/e2/28/cb659e6d308904b5b8349b2fff52", - "assets/build/pylib-android/xml/etree/ElementInclude.py": "https://files.ballistica.net/cache/ba1/bf/14/b35813d60c28bb05f1528c80f062", - "assets/build/pylib-android/xml/etree/ElementPath.py": "https://files.ballistica.net/cache/ba1/37/66/1c977331552663a6d344885a2253", - "assets/build/pylib-android/xml/etree/ElementTree.py": "https://files.ballistica.net/cache/ba1/25/f0/aecd1da0aba9b78b4fa3134ae257", - "assets/build/pylib-android/xml/etree/__init__.py": "https://files.ballistica.net/cache/ba1/38/44/1792ae5542c1ae5248568ef5abbd", - "assets/build/pylib-android/xml/etree/cElementTree.py": "https://files.ballistica.net/cache/ba1/6f/39/4c840e5eb0e09f709569154159ba", - "assets/build/pylib-android/xml/parsers/__init__.py": "https://files.ballistica.net/cache/ba1/db/92/7283051e5e1c7426985835f506b9", - "assets/build/pylib-android/xml/parsers/expat.py": "https://files.ballistica.net/cache/ba1/39/6c/fac98453c28f626916837c8a1e6b", - "assets/build/pylib-android/xml/sax/__init__.py": "https://files.ballistica.net/cache/ba1/91/57/68039f41084d369567cdad6f02f1", - "assets/build/pylib-android/xml/sax/_exceptions.py": "https://files.ballistica.net/cache/ba1/2c/42/ba99070cafd736b165b825f1df53", - "assets/build/pylib-android/xml/sax/expatreader.py": "https://files.ballistica.net/cache/ba1/84/07/34199c53cc1f9d91c835af86b47c", - "assets/build/pylib-android/xml/sax/handler.py": "https://files.ballistica.net/cache/ba1/c9/fb/412eb34bc87dffc11dd00ce0edbd", - "assets/build/pylib-android/xml/sax/saxutils.py": "https://files.ballistica.net/cache/ba1/13/27/66fe0ad3aa764ba65643f5fcbc37", - "assets/build/pylib-android/xml/sax/xmlreader.py": "https://files.ballistica.net/cache/ba1/0d/90/edc37e1a4f436d853c6d90263ae3", - "assets/build/pylib-android/xmlrpc/__init__.py": "https://files.ballistica.net/cache/ba1/fd/fd/acc2dbaec2abc4a76071ce7ed7ab", - "assets/build/pylib-android/xmlrpc/client.py": "https://files.ballistica.net/cache/ba1/4c/e4/4a92e3e83fdbf8e1578c726fd438", - "assets/build/pylib-android/xmlrpc/server.py": "https://files.ballistica.net/cache/ba1/eb/1a/fae274a8e90aa97ade20712bfe2e", - "assets/build/pylib-android/zipapp.py": "https://files.ballistica.net/cache/ba1/40/ae/178305c27d72cc46ad2220b504ec", - "assets/build/pylib-android/zipfile.py": "https://files.ballistica.net/cache/ba1/a2/9f/b75ac1015178f4186f59cf7b1adc", - "assets/build/pylib-android/zipimport.py": "https://files.ballistica.net/cache/ba1/20/ce/c481892e53a1422e4b87119b1acf", - "assets/build/pylib-android/zoneinfo/__init__.py": "https://files.ballistica.net/cache/ba1/b7/4e/bea553b4c2a2e63902c5cb446579", - "assets/build/pylib-android/zoneinfo/_common.py": "https://files.ballistica.net/cache/ba1/ae/e7/6145bfdca63840d5168082339fe0", - "assets/build/pylib-android/zoneinfo/_tzpath.py": "https://files.ballistica.net/cache/ba1/64/69/98dfdf2ecf82fa172ac717c2e8f0", - "assets/build/pylib-android/zoneinfo/_zoneinfo.py": "https://files.ballistica.net/cache/ba1/44/e9/20edcf6696e38c3314d888f5100a", - "assets/build/pylib-apple/__future__.py": "https://files.ballistica.net/cache/ba1/06/10/071659c8db50b919057e978d4806", - "assets/build/pylib-apple/__phello__.foo.py": "https://files.ballistica.net/cache/ba1/c1/42/8c4e5889af4acd69a6e866ea360f", - "assets/build/pylib-apple/_aix_support.py": "https://files.ballistica.net/cache/ba1/0b/b4/6291cf49c7fe55f3c3c4683f49a7", - "assets/build/pylib-apple/_bootsubprocess.py": "https://files.ballistica.net/cache/ba1/e8/c2/bd1aeca19799926a926a2f5ba3fd", - "assets/build/pylib-apple/_collections_abc.py": "https://files.ballistica.net/cache/ba1/04/c7/cd3e51b6f4108aee8e64f34e6181", - "assets/build/pylib-apple/_compat_pickle.py": "https://files.ballistica.net/cache/ba1/5a/19/9dbf1e881a6483e3a268e5f8ea7d", - "assets/build/pylib-apple/_compression.py": "https://files.ballistica.net/cache/ba1/c0/41/81f9725a28dceaf77a248756c3a3", - "assets/build/pylib-apple/_markupbase.py": "https://files.ballistica.net/cache/ba1/31/fc/6482fb9688b8c2ac4dc983f95452", - "assets/build/pylib-apple/_osx_support.py": "https://files.ballistica.net/cache/ba1/87/33/5b1564e8cfbdda7fbd63bad35282", - "assets/build/pylib-apple/_py_abc.py": "https://files.ballistica.net/cache/ba1/be/e3/d1030ff81e5440dc12c13a6730f7", - "assets/build/pylib-apple/_pydecimal.py": "https://files.ballistica.net/cache/ba1/33/1d/1b9d5dc8866de6ba171f526278f9", - "assets/build/pylib-apple/_pyio.py": "https://files.ballistica.net/cache/ba1/9a/10/f2f06f8bd0b762963df2e11aaeb0", - "assets/build/pylib-apple/_sitebuiltins.py": "https://files.ballistica.net/cache/ba1/14/86/8398345100fd934e4ce3a534b09a", - "assets/build/pylib-apple/_strptime.py": "https://files.ballistica.net/cache/ba1/47/5a/feef7100cfb5a7c285130e2b18f2", - "assets/build/pylib-apple/_threading_local.py": "https://files.ballistica.net/cache/ba1/94/a6/3e202c4309c356baf71a34e9325e", - "assets/build/pylib-apple/_weakrefset.py": "https://files.ballistica.net/cache/ba1/fb/58/b70b767c53b294e2932547e00a82", - "assets/build/pylib-apple/abc.py": "https://files.ballistica.net/cache/ba1/8e/b6/582479158d9819a2d6d5c15fe199", - "assets/build/pylib-apple/aifc.py": "https://files.ballistica.net/cache/ba1/13/f1/c7846c59f3a3a3f5ac2bfcb5c29a", - "assets/build/pylib-apple/antigravity.py": "https://files.ballistica.net/cache/ba1/6f/c8/f009e5a528fadaa1d92edd2b117d", - "assets/build/pylib-apple/argparse.py": "https://files.ballistica.net/cache/ba1/8f/ae/61ea4327f64e8003e3b1a5644739", - "assets/build/pylib-apple/ast.py": "https://files.ballistica.net/cache/ba1/4d/1d/aba66b672a24bd2e2009cd5af464", - "assets/build/pylib-apple/asynchat.py": "https://files.ballistica.net/cache/ba1/e9/b2/00e25081b297efd1a9c5f55e16b9", - "assets/build/pylib-apple/asyncio/__init__.py": "https://files.ballistica.net/cache/ba1/bd/b4/654cc05e142420b24c79d9613cb4", - "assets/build/pylib-apple/asyncio/__main__.py": "https://files.ballistica.net/cache/ba1/10/db/493c9bfaae1f8953435bf7aed8d2", - "assets/build/pylib-apple/asyncio/base_events.py": "https://files.ballistica.net/cache/ba1/b2/b1/a460e3a401efc6a3626157741cf8", - "assets/build/pylib-apple/asyncio/base_futures.py": "https://files.ballistica.net/cache/ba1/59/67/37e8f4938af7ebdea7da3fdef34c", - "assets/build/pylib-apple/asyncio/base_subprocess.py": "https://files.ballistica.net/cache/ba1/88/cc/f422549995be43e7b17d9830ee38", - "assets/build/pylib-apple/asyncio/base_tasks.py": "https://files.ballistica.net/cache/ba1/c5/8f/0a5cbd7b5a6f2c8531af87a09469", - "assets/build/pylib-apple/asyncio/constants.py": "https://files.ballistica.net/cache/ba1/bc/0e/b8a63e47d3d3d73b198bf52019e5", - "assets/build/pylib-apple/asyncio/coroutines.py": "https://files.ballistica.net/cache/ba1/c3/d7/e58784180e8e61785d455f7e3679", - "assets/build/pylib-apple/asyncio/events.py": "https://files.ballistica.net/cache/ba1/7c/89/6e7409ff078fe651c8e416fef5e9", - "assets/build/pylib-apple/asyncio/exceptions.py": "https://files.ballistica.net/cache/ba1/e1/81/9fcc3367d8ebba74819157777a25", - "assets/build/pylib-apple/asyncio/format_helpers.py": "https://files.ballistica.net/cache/ba1/2d/0c/03cf213d7bc7bcc0a6ee0e7310dc", - "assets/build/pylib-apple/asyncio/futures.py": "https://files.ballistica.net/cache/ba1/4a/06/1735cf6be1281f45d1347d54d0be", - "assets/build/pylib-apple/asyncio/locks.py": "https://files.ballistica.net/cache/ba1/67/d1/2a18e6b3bd5b38b2077f1998c7a3", - "assets/build/pylib-apple/asyncio/log.py": "https://files.ballistica.net/cache/ba1/0f/5a/f419c17cbe08eddfa90f314b8a6c", - "assets/build/pylib-apple/asyncio/mixins.py": "https://files.ballistica.net/cache/ba1/c2/96/55f157872a1cf80478280d98b50e", - "assets/build/pylib-apple/asyncio/proactor_events.py": "https://files.ballistica.net/cache/ba1/9c/8f/3939c63512c7f2083501b928a161", - "assets/build/pylib-apple/asyncio/protocols.py": "https://files.ballistica.net/cache/ba1/ba/74/d55d794d40457f97fc09287c1b9a", - "assets/build/pylib-apple/asyncio/queues.py": "https://files.ballistica.net/cache/ba1/3b/9d/f511b128421c13506b7e943b838a", - "assets/build/pylib-apple/asyncio/runners.py": "https://files.ballistica.net/cache/ba1/79/67/8e3adc65e74dd3fa09547507941f", - "assets/build/pylib-apple/asyncio/selector_events.py": "https://files.ballistica.net/cache/ba1/98/42/03fac39c6a19988e2367edd6bc45", - "assets/build/pylib-apple/asyncio/sslproto.py": "https://files.ballistica.net/cache/ba1/88/ac/7d5895b4917b574a9beb9aff8c19", - "assets/build/pylib-apple/asyncio/staggered.py": "https://files.ballistica.net/cache/ba1/92/3a/0de93c93afe7311a68a615fb2b97", - "assets/build/pylib-apple/asyncio/streams.py": "https://files.ballistica.net/cache/ba1/79/64/e068ea39e822c333f4636a4b19b9", - "assets/build/pylib-apple/asyncio/subprocess.py": "https://files.ballistica.net/cache/ba1/f6/8a/20579c462e5861d846cd36ac1a2d", - "assets/build/pylib-apple/asyncio/tasks.py": "https://files.ballistica.net/cache/ba1/bb/96/37cc5c6aeefa3c0b61f46fc6ca3c", - "assets/build/pylib-apple/asyncio/threads.py": "https://files.ballistica.net/cache/ba1/9d/3f/a1aef3cbccdd7da182b5adda824e", - "assets/build/pylib-apple/asyncio/transports.py": "https://files.ballistica.net/cache/ba1/45/b8/6e2420686a0d562e566f4fe37960", - "assets/build/pylib-apple/asyncio/trsock.py": "https://files.ballistica.net/cache/ba1/19/74/be437ee7d795f7c6106ef8fbf2db", - "assets/build/pylib-apple/asyncio/unix_events.py": "https://files.ballistica.net/cache/ba1/e3/93/473c75e7d314ae8f634bbe486328", - "assets/build/pylib-apple/asyncio/windows_events.py": "https://files.ballistica.net/cache/ba1/db/d8/1053535a4d6cebc504b51654bc94", - "assets/build/pylib-apple/asyncio/windows_utils.py": "https://files.ballistica.net/cache/ba1/c3/9a/4bc6c5e7ffe8c0298bbae63157c0", - "assets/build/pylib-apple/asyncore.py": "https://files.ballistica.net/cache/ba1/e2/8f/b4eacf0a51f03614679e2bc49272", - "assets/build/pylib-apple/base64.py": "https://files.ballistica.net/cache/ba1/7c/ef/6f6619359347005296a638194c5f", - "assets/build/pylib-apple/bdb.py": "https://files.ballistica.net/cache/ba1/b6/77/2e17dfa73c13c59f1721262d2dad", - "assets/build/pylib-apple/binhex.py": "https://files.ballistica.net/cache/ba1/55/2a/cd97de9d7e92a69ea9fd26be5498", - "assets/build/pylib-apple/bisect.py": "https://files.ballistica.net/cache/ba1/41/d5/0b0bbedf69c235b62667bc6ee11f", - "assets/build/pylib-apple/bz2.py": "https://files.ballistica.net/cache/ba1/a7/03/070f6bfcf4b59a703d7e8a556a54", - "assets/build/pylib-apple/cProfile.py": "https://files.ballistica.net/cache/ba1/d9/b8/bd483641d5eb3292a4ef38771c04", - "assets/build/pylib-apple/calendar.py": "https://files.ballistica.net/cache/ba1/26/7e/834553849b988c69b1f6f4b5d4cd", - "assets/build/pylib-apple/cgi.py": "https://files.ballistica.net/cache/ba1/2a/e7/17d38c48ac2b7b27bd6449de9ecb", - "assets/build/pylib-apple/cgitb.py": "https://files.ballistica.net/cache/ba1/ea/fa/4ac55d7852f61b8c42d82b690fef", - "assets/build/pylib-apple/chunk.py": "https://files.ballistica.net/cache/ba1/e0/4d/8609a028d890841ff867e97f0869", - "assets/build/pylib-apple/cmd.py": "https://files.ballistica.net/cache/ba1/33/25/43fd9394378dd3db266dd35af46e", - "assets/build/pylib-apple/code.py": "https://files.ballistica.net/cache/ba1/18/fc/d667016222e466707ec5d0991810", - "assets/build/pylib-apple/codecs.py": "https://files.ballistica.net/cache/ba1/b9/f7/9767012d288c3072ee2d4d156c4f", - "assets/build/pylib-apple/codeop.py": "https://files.ballistica.net/cache/ba1/14/57/3fe55c95397aca03243a759c5b70", - "assets/build/pylib-apple/collections/__init__.py": "https://files.ballistica.net/cache/ba1/b8/7e/811d5b51927f457f1d8a9321bda7", - "assets/build/pylib-apple/collections/abc.py": "https://files.ballistica.net/cache/ba1/63/ca/9d22346f8750ef62088de3c25c0a", - "assets/build/pylib-apple/colorsys.py": "https://files.ballistica.net/cache/ba1/40/fa/e7ec73644cb26848318f54ab49d2", - "assets/build/pylib-apple/compileall.py": "https://files.ballistica.net/cache/ba1/01/29/691fa9dd9624d7fb077118f15b96", - "assets/build/pylib-apple/concurrent/__init__.py": "https://files.ballistica.net/cache/ba1/f8/0b/346441ef94908fb806338d0510b6", - "assets/build/pylib-apple/concurrent/futures/__init__.py": "https://files.ballistica.net/cache/ba1/6b/26/78ac686a2335efb18fa8ed0e5d6a", - "assets/build/pylib-apple/concurrent/futures/_base.py": "https://files.ballistica.net/cache/ba1/ef/36/a117832c70a34fd8ea798f4cb994", - "assets/build/pylib-apple/concurrent/futures/process.py": "https://files.ballistica.net/cache/ba1/a7/35/e4d1c9e1f656d2ffb6d07331a618", - "assets/build/pylib-apple/concurrent/futures/thread.py": "https://files.ballistica.net/cache/ba1/af/72/4903f475286e1bb37c0549e7122f", - "assets/build/pylib-apple/configparser.py": "https://files.ballistica.net/cache/ba1/5c/7b/92556ff0a68f66a8340ee71cfd59", - "assets/build/pylib-apple/contextlib.py": "https://files.ballistica.net/cache/ba1/8c/a7/4aaebb725005dde583b8cc3a1c24", - "assets/build/pylib-apple/contextvars.py": "https://files.ballistica.net/cache/ba1/02/52/e520b59b10124c813468252fee2a", - "assets/build/pylib-apple/copy.py": "https://files.ballistica.net/cache/ba1/d0/ca/960d55e257f617b9e85a9a7c6658", - "assets/build/pylib-apple/copyreg.py": "https://files.ballistica.net/cache/ba1/06/49/c1e6f2f9fef5a786241efc149241", - "assets/build/pylib-apple/crypt.py": "https://files.ballistica.net/cache/ba1/a8/21/905bf832e4a54f0efde53d431f2f", - "assets/build/pylib-apple/csv.py": "https://files.ballistica.net/cache/ba1/94/09/3e900377e27a1cfb0be4b57ed3bc", - "assets/build/pylib-apple/ctypes/__init__.py": "https://files.ballistica.net/cache/ba1/68/b7/87f1ceae16da8b248a771145e8e4", - "assets/build/pylib-apple/ctypes/_aix.py": "https://files.ballistica.net/cache/ba1/de/74/20f2f83f65af1bf8ee53bcf3d89a", - "assets/build/pylib-apple/ctypes/_endian.py": "https://files.ballistica.net/cache/ba1/f7/ff/297cd3308735876b3d7ac54172e0", - "assets/build/pylib-apple/ctypes/macholib/__init__.py": "https://files.ballistica.net/cache/ba1/cd/67/df2ef87d5f3411077030882bcae3", - "assets/build/pylib-apple/ctypes/macholib/dyld.py": "https://files.ballistica.net/cache/ba1/26/85/489db3f032a47813958e08c687d9", - "assets/build/pylib-apple/ctypes/macholib/dylib.py": "https://files.ballistica.net/cache/ba1/bd/b1/b431a9e977498c9c5945a3f63da9", - "assets/build/pylib-apple/ctypes/macholib/framework.py": "https://files.ballistica.net/cache/ba1/97/8b/19a7d679d50c0983f9c4b2f37d5e", - "assets/build/pylib-apple/ctypes/util.py": "https://files.ballistica.net/cache/ba1/80/95/f30336080c30fcb7d5db549ef68b", - "assets/build/pylib-apple/ctypes/wintypes.py": "https://files.ballistica.net/cache/ba1/09/43/f7e315d732918308ea4e4c197c5a", - "assets/build/pylib-apple/curses/__init__.py": "https://files.ballistica.net/cache/ba1/02/0b/75d9b2291f8ce6b7c06003397aba", - "assets/build/pylib-apple/curses/ascii.py": "https://files.ballistica.net/cache/ba1/97/27/94584e1dcd1637dfdce80efa78eb", - "assets/build/pylib-apple/curses/has_key.py": "https://files.ballistica.net/cache/ba1/60/17/fde8d2bfabfe53298f936ebdec88", - "assets/build/pylib-apple/curses/panel.py": "https://files.ballistica.net/cache/ba1/41/42/ef63abf4b18669a22ab9141bcb88", - "assets/build/pylib-apple/curses/textpad.py": "https://files.ballistica.net/cache/ba1/34/78/bc5c856e5f1d8439d4e191bc4f75", - "assets/build/pylib-apple/dataclasses.py": "https://files.ballistica.net/cache/ba1/29/75/8808e373b093d0747576d2a44a37", - "assets/build/pylib-apple/datetime.py": "https://files.ballistica.net/cache/ba1/18/38/ffdbe4da2f437314df6659baa791", - "assets/build/pylib-apple/decimal.py": "https://files.ballistica.net/cache/ba1/dd/5d/8d0f90ec4e20c613b6ce2a88bc60", - "assets/build/pylib-apple/difflib.py": "https://files.ballistica.net/cache/ba1/b6/30/794d2ce6c6c67a341a398475e0fa", - "assets/build/pylib-apple/dis.py": "https://files.ballistica.net/cache/ba1/75/04/b55a421d349ed7703bbd0919e9a7", - "assets/build/pylib-apple/doctest.py": "https://files.ballistica.net/cache/ba1/71/4c/3104a26c2eaef0c6c3be3573fc39", - "assets/build/pylib-apple/email/__init__.py": "https://files.ballistica.net/cache/ba1/2f/8e/c14225900357ac302213f5b4d674", - "assets/build/pylib-apple/email/_encoded_words.py": "https://files.ballistica.net/cache/ba1/c6/3d/d686aa9a7ddbee790ad558b25661", - "assets/build/pylib-apple/email/_header_value_parser.py": "https://files.ballistica.net/cache/ba1/fb/2a/efe05a41d7fda3cba1d9a68b31b2", - "assets/build/pylib-apple/email/_parseaddr.py": "https://files.ballistica.net/cache/ba1/c8/86/7a4355da8ada8f4142c159a2eb86", - "assets/build/pylib-apple/email/_policybase.py": "https://files.ballistica.net/cache/ba1/6c/38/90cc9fd748e60e442565a4a1d88a", - "assets/build/pylib-apple/email/base64mime.py": "https://files.ballistica.net/cache/ba1/f6/9d/5e1ba6328ee09574b348aee7441c", - "assets/build/pylib-apple/email/charset.py": "https://files.ballistica.net/cache/ba1/7e/a5/dd4804d6ab73949fc8b905377163", - "assets/build/pylib-apple/email/contentmanager.py": "https://files.ballistica.net/cache/ba1/c7/48/5f45e0d18b7b600de87759d118be", - "assets/build/pylib-apple/email/encoders.py": "https://files.ballistica.net/cache/ba1/93/57/13dcc149580825b133b08fbc6a87", - "assets/build/pylib-apple/email/errors.py": "https://files.ballistica.net/cache/ba1/77/99/46ff11eb98aaf4155d27a9017248", - "assets/build/pylib-apple/email/feedparser.py": "https://files.ballistica.net/cache/ba1/5e/34/32eb942596826dd2cb845e9d2a2e", - "assets/build/pylib-apple/email/generator.py": "https://files.ballistica.net/cache/ba1/09/86/f9ebd02a5a8a21bd66927b50fffe", - "assets/build/pylib-apple/email/header.py": "https://files.ballistica.net/cache/ba1/21/e7/9bb3af5ad0b37941d9c73020cccf", - "assets/build/pylib-apple/email/headerregistry.py": "https://files.ballistica.net/cache/ba1/08/87/e111222315f16f7ce523bc375747", - "assets/build/pylib-apple/email/iterators.py": "https://files.ballistica.net/cache/ba1/2f/8c/6a1dc186422beb8546721f22b72b", - "assets/build/pylib-apple/email/message.py": "https://files.ballistica.net/cache/ba1/85/b7/5f68f74ba619a5b78507b73e0e4e", - "assets/build/pylib-apple/email/mime/__init__.py": "https://files.ballistica.net/cache/ba1/e0/cb/20c79c7faa724bdfeebae99795aa", - "assets/build/pylib-apple/email/mime/application.py": "https://files.ballistica.net/cache/ba1/45/a9/97edc07b0a7c7e3e004422b674c2", - "assets/build/pylib-apple/email/mime/audio.py": "https://files.ballistica.net/cache/ba1/c8/25/9973c555726af5e265c0f459e8b2", - "assets/build/pylib-apple/email/mime/base.py": "https://files.ballistica.net/cache/ba1/21/85/f780318011c30d785eb5af82e321", - "assets/build/pylib-apple/email/mime/image.py": "https://files.ballistica.net/cache/ba1/9c/ed/b556808bf0a49f3f813a4e49c5e8", - "assets/build/pylib-apple/email/mime/message.py": "https://files.ballistica.net/cache/ba1/69/9d/acd10bbbf439917af6079c703f3c", - "assets/build/pylib-apple/email/mime/multipart.py": "https://files.ballistica.net/cache/ba1/72/35/31f1f48461e387ca0479a159b9d2", - "assets/build/pylib-apple/email/mime/nonmultipart.py": "https://files.ballistica.net/cache/ba1/45/85/597c6acf7574876fa2b5fee716d9", - "assets/build/pylib-apple/email/mime/text.py": "https://files.ballistica.net/cache/ba1/f2/d3/f8bad4747bbb3fa12295dc8998f4", - "assets/build/pylib-apple/email/parser.py": "https://files.ballistica.net/cache/ba1/86/8c/8030eb141d7713a36befee9736ef", - "assets/build/pylib-apple/email/policy.py": "https://files.ballistica.net/cache/ba1/a5/64/8c59670fdf565988c72f21040ed5", - "assets/build/pylib-apple/email/quoprimime.py": "https://files.ballistica.net/cache/ba1/4a/85/aa0a24ec3a4f407d18aadf357bad", - "assets/build/pylib-apple/email/utils.py": "https://files.ballistica.net/cache/ba1/5b/4c/63d0c1c19f597fdbab52d121d6a5", - "assets/build/pylib-apple/encodings/__init__.py": "https://files.ballistica.net/cache/ba1/69/21/c4db58e0cc01b6ea6301b2a03bb6", - "assets/build/pylib-apple/encodings/aliases.py": "https://files.ballistica.net/cache/ba1/d8/a9/a7cd663238f58ba364d163ab0caf", - "assets/build/pylib-apple/encodings/ascii.py": "https://files.ballistica.net/cache/ba1/ae/eb/d4efe1172c154ed19f3ed9c3e0d8", - "assets/build/pylib-apple/encodings/base64_codec.py": "https://files.ballistica.net/cache/ba1/ac/9d/63f89df311a1a5cfb3c0e8cedcce", - "assets/build/pylib-apple/encodings/big5.py": "https://files.ballistica.net/cache/ba1/78/ea/960502c422e3c224c8ad449fd429", - "assets/build/pylib-apple/encodings/big5hkscs.py": "https://files.ballistica.net/cache/ba1/fb/f9/4f749e7f48a1114433afdb8af502", - "assets/build/pylib-apple/encodings/bz2_codec.py": "https://files.ballistica.net/cache/ba1/3d/93/41f39520984ad411f757b65b8977", - "assets/build/pylib-apple/encodings/charmap.py": "https://files.ballistica.net/cache/ba1/e1/53/ca2d28bc8d30977cc4de2bfc33f7", - "assets/build/pylib-apple/encodings/cp037.py": "https://files.ballistica.net/cache/ba1/25/dd/ad20754fdad86ed4be3d59488009", - "assets/build/pylib-apple/encodings/cp1006.py": "https://files.ballistica.net/cache/ba1/69/ce/f83926840de2454836bbfbd32a77", - "assets/build/pylib-apple/encodings/cp1026.py": "https://files.ballistica.net/cache/ba1/de/05/586d3c4a59fd4853c28ec146ecb9", - "assets/build/pylib-apple/encodings/cp1125.py": "https://files.ballistica.net/cache/ba1/09/60/fd864f7a251007862db28c82fdfa", - "assets/build/pylib-apple/encodings/cp1140.py": "https://files.ballistica.net/cache/ba1/b4/0b/614164146086a0cc1af2fcac6c69", - "assets/build/pylib-apple/encodings/cp1250.py": "https://files.ballistica.net/cache/ba1/fe/8d/fddf21cd1bbbe7ddc78de2c7cc33", - "assets/build/pylib-apple/encodings/cp1251.py": "https://files.ballistica.net/cache/ba1/e5/18/df35c74bea8421ae0c2ab5707c74", - "assets/build/pylib-apple/encodings/cp1252.py": "https://files.ballistica.net/cache/ba1/40/c2/746c9a90cfe5f5ac07ac9a8a88fa", - "assets/build/pylib-apple/encodings/cp1253.py": "https://files.ballistica.net/cache/ba1/d2/e5/53002db6d562b8d88faa4b13c869", - "assets/build/pylib-apple/encodings/cp1254.py": "https://files.ballistica.net/cache/ba1/89/e4/0398c76cdd84466bc2b49e472fb8", - "assets/build/pylib-apple/encodings/cp1255.py": "https://files.ballistica.net/cache/ba1/b7/ad/6ddb21baa421426e66fbb2a56f51", - "assets/build/pylib-apple/encodings/cp1256.py": "https://files.ballistica.net/cache/ba1/74/46/fb558d724ef9a8334e492522b80a", - "assets/build/pylib-apple/encodings/cp1257.py": "https://files.ballistica.net/cache/ba1/3b/0c/28a4e931808376142bcec043d491", - "assets/build/pylib-apple/encodings/cp1258.py": "https://files.ballistica.net/cache/ba1/fe/07/b219b4c9d25c8f1496a3b3134662", - "assets/build/pylib-apple/encodings/cp273.py": "https://files.ballistica.net/cache/ba1/42/92/702f1990093c01c940e43b5aaf09", - "assets/build/pylib-apple/encodings/cp424.py": "https://files.ballistica.net/cache/ba1/1a/2d/a446a54a2d76c587b34eb915f127", - "assets/build/pylib-apple/encodings/cp437.py": "https://files.ballistica.net/cache/ba1/46/c2/29b4a8a21a9bb96be0e203ad6ec4", - "assets/build/pylib-apple/encodings/cp500.py": "https://files.ballistica.net/cache/ba1/a9/11/e8f4418506106a071feef14b3382", - "assets/build/pylib-apple/encodings/cp720.py": "https://files.ballistica.net/cache/ba1/53/18/99c144337822c63b123971dd1779", - "assets/build/pylib-apple/encodings/cp737.py": "https://files.ballistica.net/cache/ba1/08/a7/8eab04dab573b446dba438e51467", - "assets/build/pylib-apple/encodings/cp775.py": "https://files.ballistica.net/cache/ba1/5d/a9/7de5f8fd96e66e4bd2caa6ff84b1", - "assets/build/pylib-apple/encodings/cp850.py": "https://files.ballistica.net/cache/ba1/c6/b7/8f097103438e1385f2c3ba2c3392", - "assets/build/pylib-apple/encodings/cp852.py": "https://files.ballistica.net/cache/ba1/d9/9e/253ef33683f232283796e05f3fa8", - "assets/build/pylib-apple/encodings/cp855.py": "https://files.ballistica.net/cache/ba1/ef/35/2aa5cbfc44502e75d39ea4295267", - "assets/build/pylib-apple/encodings/cp856.py": "https://files.ballistica.net/cache/ba1/b1/2d/2c2040e8118786b75941313b6698", - "assets/build/pylib-apple/encodings/cp857.py": "https://files.ballistica.net/cache/ba1/d7/20/6cbf378cbd0ad1a37fce5ccf8fbf", - "assets/build/pylib-apple/encodings/cp858.py": "https://files.ballistica.net/cache/ba1/f4/d4/4a38f70cf8b71967537ce9f6989a", - "assets/build/pylib-apple/encodings/cp860.py": "https://files.ballistica.net/cache/ba1/a8/28/b3b895da8024df535b642186b940", - "assets/build/pylib-apple/encodings/cp861.py": "https://files.ballistica.net/cache/ba1/ce/57/318e7c9bea4cb927f76490ac438c", - "assets/build/pylib-apple/encodings/cp862.py": "https://files.ballistica.net/cache/ba1/f8/19/98e8dd96582970971f60575f2949", - "assets/build/pylib-apple/encodings/cp863.py": "https://files.ballistica.net/cache/ba1/c5/dc/cb7f39bdab80037f29647d00b13b", - "assets/build/pylib-apple/encodings/cp864.py": "https://files.ballistica.net/cache/ba1/f9/c8/99920e492f4cbf84d3dbf7bfa854", - "assets/build/pylib-apple/encodings/cp865.py": "https://files.ballistica.net/cache/ba1/51/2b/ecf20198542e4bbe678677a16f96", - "assets/build/pylib-apple/encodings/cp866.py": "https://files.ballistica.net/cache/ba1/27/40/a0a2bfc8682c41d6bae37ba1e625", - "assets/build/pylib-apple/encodings/cp869.py": "https://files.ballistica.net/cache/ba1/98/4b/7c6b0a6e43441b86031f027b7f99", - "assets/build/pylib-apple/encodings/cp874.py": "https://files.ballistica.net/cache/ba1/c4/3c/d5b1bfeffd7f22ef7c4310b692de", - "assets/build/pylib-apple/encodings/cp875.py": "https://files.ballistica.net/cache/ba1/a7/54/9f2d97b9882914db7b3bcffba3b3", - "assets/build/pylib-apple/encodings/cp932.py": "https://files.ballistica.net/cache/ba1/5d/ee/be474addb3592a7c52de1924ca89", - "assets/build/pylib-apple/encodings/cp949.py": "https://files.ballistica.net/cache/ba1/b8/b5/a4af998d516c5aa2285f562d248c", - "assets/build/pylib-apple/encodings/cp950.py": "https://files.ballistica.net/cache/ba1/33/d7/5e2b2e506a2cc222206b3d85e348", - "assets/build/pylib-apple/encodings/euc_jis_2004.py": "https://files.ballistica.net/cache/ba1/fc/ef/0d88c32abb38d2afae17137a7279", - "assets/build/pylib-apple/encodings/euc_jisx0213.py": "https://files.ballistica.net/cache/ba1/dc/a3/3ea79baddf0883b7ad2a6d81ba25", - "assets/build/pylib-apple/encodings/euc_jp.py": "https://files.ballistica.net/cache/ba1/f4/13/77d5b52d8e958eebe527c31af80f", - "assets/build/pylib-apple/encodings/euc_kr.py": "https://files.ballistica.net/cache/ba1/df/6b/b1054fd07bb1add4c9006a4d4c83", - "assets/build/pylib-apple/encodings/gb18030.py": "https://files.ballistica.net/cache/ba1/f8/40/2306ae4d7767a47de24e8b402979", - "assets/build/pylib-apple/encodings/gb2312.py": "https://files.ballistica.net/cache/ba1/8a/2e/1b172623956bda98c58ce6ab8b0d", - "assets/build/pylib-apple/encodings/gbk.py": "https://files.ballistica.net/cache/ba1/72/bf/4af21356063e8e8cb050e79da213", - "assets/build/pylib-apple/encodings/hex_codec.py": "https://files.ballistica.net/cache/ba1/61/f1/0f3c128dd009b1745b17b71728c1", - "assets/build/pylib-apple/encodings/hp_roman8.py": "https://files.ballistica.net/cache/ba1/66/6f/156aec24cbb96c9477e902e8f334", - "assets/build/pylib-apple/encodings/hz.py": "https://files.ballistica.net/cache/ba1/ac/59/43bcce12ae925e0c23b6eb26ef7b", - "assets/build/pylib-apple/encodings/idna.py": "https://files.ballistica.net/cache/ba1/d7/a4/5719c8cc2cb8e5ba6283cff451ad", - "assets/build/pylib-apple/encodings/iso2022_jp.py": "https://files.ballistica.net/cache/ba1/5a/0a/81cfe5eb6752664de42600812b3f", - "assets/build/pylib-apple/encodings/iso2022_jp_1.py": "https://files.ballistica.net/cache/ba1/a8/4c/bf8d714cc72c571544c55044ac87", - "assets/build/pylib-apple/encodings/iso2022_jp_2.py": "https://files.ballistica.net/cache/ba1/9f/da/c2cab5ac51f515b6b0767cbeab4e", - "assets/build/pylib-apple/encodings/iso2022_jp_2004.py": "https://files.ballistica.net/cache/ba1/cf/61/ccb42c9bf314df5c218dba9ee916", - "assets/build/pylib-apple/encodings/iso2022_jp_3.py": "https://files.ballistica.net/cache/ba1/f9/c3/9e2b380bfcc803e41053b52497c1", - "assets/build/pylib-apple/encodings/iso2022_jp_ext.py": "https://files.ballistica.net/cache/ba1/fe/e8/9bab22097ffff2c09ac781bfb180", - "assets/build/pylib-apple/encodings/iso2022_kr.py": "https://files.ballistica.net/cache/ba1/4f/a2/2c7de113ce9a4a2423a2594c78ff", - "assets/build/pylib-apple/encodings/iso8859_1.py": "https://files.ballistica.net/cache/ba1/20/44/07c49c28439727ebb8d33ab1e757", - "assets/build/pylib-apple/encodings/iso8859_10.py": "https://files.ballistica.net/cache/ba1/87/bc/158914ff0958afe8a7dd6849c5df", - "assets/build/pylib-apple/encodings/iso8859_11.py": "https://files.ballistica.net/cache/ba1/e0/b8/44c0637f01f2bb42b22325c0aa5b", - "assets/build/pylib-apple/encodings/iso8859_13.py": "https://files.ballistica.net/cache/ba1/1f/c8/e34ba6dc590a3f183dc718fd43b6", - "assets/build/pylib-apple/encodings/iso8859_14.py": "https://files.ballistica.net/cache/ba1/1a/a5/34e62f9c962c6501d0ad5048490b", - "assets/build/pylib-apple/encodings/iso8859_15.py": "https://files.ballistica.net/cache/ba1/43/71/e5947b63082f3e520ec2f889a018", - "assets/build/pylib-apple/encodings/iso8859_16.py": "https://files.ballistica.net/cache/ba1/e9/62/0cd582023c5dbfe4ccd84bf927f6", - "assets/build/pylib-apple/encodings/iso8859_2.py": "https://files.ballistica.net/cache/ba1/5c/79/8e1928d513581c9fc02977cf4076", - "assets/build/pylib-apple/encodings/iso8859_3.py": "https://files.ballistica.net/cache/ba1/d1/fe/567648bf798bbf0c0bc917ca42e2", - "assets/build/pylib-apple/encodings/iso8859_4.py": "https://files.ballistica.net/cache/ba1/89/dc/60aece6808b33abe95b3870255d3", - "assets/build/pylib-apple/encodings/iso8859_5.py": "https://files.ballistica.net/cache/ba1/90/b1/82e7ef509f627410cafd031a9d94", - "assets/build/pylib-apple/encodings/iso8859_6.py": "https://files.ballistica.net/cache/ba1/5c/95/6f9a5d2953e03c3a98e6e6bcfb54", - "assets/build/pylib-apple/encodings/iso8859_7.py": "https://files.ballistica.net/cache/ba1/39/99/024e8aa0b3ec17a0412c668f7fda", - "assets/build/pylib-apple/encodings/iso8859_8.py": "https://files.ballistica.net/cache/ba1/35/ff/79f3f101252e0cbe0a6f0e875c4e", - "assets/build/pylib-apple/encodings/iso8859_9.py": "https://files.ballistica.net/cache/ba1/c7/88/440faf74054c3f974fbd6879d6de", - "assets/build/pylib-apple/encodings/johab.py": "https://files.ballistica.net/cache/ba1/3c/f1/52cbc472f6365e168380030d36f4", - "assets/build/pylib-apple/encodings/koi8_r.py": "https://files.ballistica.net/cache/ba1/7d/ae/32766b835c27743d8e8886b8671f", - "assets/build/pylib-apple/encodings/koi8_t.py": "https://files.ballistica.net/cache/ba1/f5/ef/f1cda88378dfe2a7cb61d6c2171a", - "assets/build/pylib-apple/encodings/koi8_u.py": "https://files.ballistica.net/cache/ba1/66/ee/0492ca895ab6daf6876d77bb5d27", - "assets/build/pylib-apple/encodings/kz1048.py": "https://files.ballistica.net/cache/ba1/f5/2a/2b0dcc29c907c6a518497c7f48c7", - "assets/build/pylib-apple/encodings/latin_1.py": "https://files.ballistica.net/cache/ba1/59/fe/91ae90c6e9dd63531cb3d312100b", - "assets/build/pylib-apple/encodings/mac_arabic.py": "https://files.ballistica.net/cache/ba1/68/fe/d6a7084555b675b9c484ce28b0b7", - "assets/build/pylib-apple/encodings/mac_croatian.py": "https://files.ballistica.net/cache/ba1/b7/55/5934baacacdbab3fc78aab842577", - "assets/build/pylib-apple/encodings/mac_cyrillic.py": "https://files.ballistica.net/cache/ba1/4a/4a/56eb674ccc125d9f75c14283d979", - "assets/build/pylib-apple/encodings/mac_farsi.py": "https://files.ballistica.net/cache/ba1/f1/d3/6f3a3bc64f3dee0db82072cd1578", - "assets/build/pylib-apple/encodings/mac_greek.py": "https://files.ballistica.net/cache/ba1/8d/53/188243bd31341d3f1a94f6d41d63", - "assets/build/pylib-apple/encodings/mac_iceland.py": "https://files.ballistica.net/cache/ba1/26/7d/0ffe4f053f958d57dc4c4e631396", - "assets/build/pylib-apple/encodings/mac_latin2.py": "https://files.ballistica.net/cache/ba1/a3/a4/3ebce6fe56f0e1d612b783d8297b", - "assets/build/pylib-apple/encodings/mac_roman.py": "https://files.ballistica.net/cache/ba1/2c/d9/18b69e51f0c13523e2b1fc8e43ae", - "assets/build/pylib-apple/encodings/mac_romanian.py": "https://files.ballistica.net/cache/ba1/0f/b3/3f49eea2b96705960adbc36d1d50", - "assets/build/pylib-apple/encodings/mac_turkish.py": "https://files.ballistica.net/cache/ba1/b3/8a/866e2a9c999dd3fe41dace01c497", - "assets/build/pylib-apple/encodings/mbcs.py": "https://files.ballistica.net/cache/ba1/48/6d/4e1f27265d6c3cd9ee6b1ada42e1", - "assets/build/pylib-apple/encodings/oem.py": "https://files.ballistica.net/cache/ba1/95/72/044008306f724b5b7ed437b33345", - "assets/build/pylib-apple/encodings/palmos.py": "https://files.ballistica.net/cache/ba1/6a/11/ea725dae02d1d9efe32deeddfa6f", - "assets/build/pylib-apple/encodings/ptcp154.py": "https://files.ballistica.net/cache/ba1/64/3f/a54d9272029e4ea2c2c334eefe5e", - "assets/build/pylib-apple/encodings/punycode.py": "https://files.ballistica.net/cache/ba1/cf/31/c8cf7953762f98ac90c097a4b865", - "assets/build/pylib-apple/encodings/quopri_codec.py": "https://files.ballistica.net/cache/ba1/7c/99/e1ba630b5466e0b9b1cd6ff779f7", - "assets/build/pylib-apple/encodings/raw_unicode_escape.py": "https://files.ballistica.net/cache/ba1/a4/38/aabcae8f196b2af32b4a5ca84027", - "assets/build/pylib-apple/encodings/rot_13.py": "https://files.ballistica.net/cache/ba1/ef/32/5aa7862c190b7246f8592bdeca9e", - "assets/build/pylib-apple/encodings/shift_jis.py": "https://files.ballistica.net/cache/ba1/ec/50/2383e5c96f87aae39321c274309d", - "assets/build/pylib-apple/encodings/shift_jis_2004.py": "https://files.ballistica.net/cache/ba1/5f/73/e624c75440c616f02be55164301e", - "assets/build/pylib-apple/encodings/shift_jisx0213.py": "https://files.ballistica.net/cache/ba1/2f/42/52a2a9c22027c372a9a8c496736f", - "assets/build/pylib-apple/encodings/tis_620.py": "https://files.ballistica.net/cache/ba1/f5/97/5265d287599954e9edc9d136c2eb", - "assets/build/pylib-apple/encodings/undefined.py": "https://files.ballistica.net/cache/ba1/e0/30/aa4fc6031370f50a2c5b895f2bd8", - "assets/build/pylib-apple/encodings/unicode_escape.py": "https://files.ballistica.net/cache/ba1/6b/c2/6df64e57e6c237e8a8f9b7961983", - "assets/build/pylib-apple/encodings/utf_16.py": "https://files.ballistica.net/cache/ba1/0f/27/bd0dcbe7be3cf0daf600f720c24a", - "assets/build/pylib-apple/encodings/utf_16_be.py": "https://files.ballistica.net/cache/ba1/25/40/ffe71632b66c6d5a08b4ce945790", - "assets/build/pylib-apple/encodings/utf_16_le.py": "https://files.ballistica.net/cache/ba1/64/28/e9b6c773570ea8510c2acf0b7cd8", - "assets/build/pylib-apple/encodings/utf_32.py": "https://files.ballistica.net/cache/ba1/65/b5/1b632a75766f5a1c0591620a23d2", - "assets/build/pylib-apple/encodings/utf_32_be.py": "https://files.ballistica.net/cache/ba1/b4/81/5426c7fb7e052f681ee9a16f0816", - "assets/build/pylib-apple/encodings/utf_32_le.py": "https://files.ballistica.net/cache/ba1/c2/12/0ed4da51da1dc8f9f2b9f850555b", - "assets/build/pylib-apple/encodings/utf_7.py": "https://files.ballistica.net/cache/ba1/b9/f2/824e852768e1ee545ebadaa4a73d", - "assets/build/pylib-apple/encodings/utf_8.py": "https://files.ballistica.net/cache/ba1/9c/68/6756078d5a53aab89c88e31c73ad", - "assets/build/pylib-apple/encodings/utf_8_sig.py": "https://files.ballistica.net/cache/ba1/1b/c8/1ffa26b3c2a650efae206d27b6f3", - "assets/build/pylib-apple/encodings/uu_codec.py": "https://files.ballistica.net/cache/ba1/40/6c/279145a291f59642952ea9880b9d", - "assets/build/pylib-apple/encodings/zlib_codec.py": "https://files.ballistica.net/cache/ba1/1f/c9/57406baceeabe0a0e201a81833d5", - "assets/build/pylib-apple/enum.py": "https://files.ballistica.net/cache/ba1/f9/f0/29d3b69d70c786415b626919820f", - "assets/build/pylib-apple/filecmp.py": "https://files.ballistica.net/cache/ba1/d0/d5/60a99803a250a9a1ea3452fe4936", - "assets/build/pylib-apple/fileinput.py": "https://files.ballistica.net/cache/ba1/a1/36/0c41daeac3e75e270cc57cc68a18", - "assets/build/pylib-apple/fnmatch.py": "https://files.ballistica.net/cache/ba1/c7/a6/5036cd268eee8504116945ea8d02", - "assets/build/pylib-apple/fractions.py": "https://files.ballistica.net/cache/ba1/0d/63/5012c1eef74766170e9d8bbbb19c", - "assets/build/pylib-apple/ftplib.py": "https://files.ballistica.net/cache/ba1/6c/7f/c59704a4093cecace0737ff19134", - "assets/build/pylib-apple/functools.py": "https://files.ballistica.net/cache/ba1/5c/3a/139c52884ed8d5608bae463d1a40", - "assets/build/pylib-apple/genericpath.py": "https://files.ballistica.net/cache/ba1/b3/cd/0b7bb99d3aa5a41b69a4b62cd3cf", - "assets/build/pylib-apple/getopt.py": "https://files.ballistica.net/cache/ba1/46/47/d33382d447d398923f4c0c0f87c1", - "assets/build/pylib-apple/getpass.py": "https://files.ballistica.net/cache/ba1/4e/67/45234cbaae5e74cdd50bab4423bd", - "assets/build/pylib-apple/gettext.py": "https://files.ballistica.net/cache/ba1/f7/39/95601dd0780dd07609592b57dda6", - "assets/build/pylib-apple/glob.py": "https://files.ballistica.net/cache/ba1/2f/7a/28d3950e212c8185465084e4a9ff", - "assets/build/pylib-apple/graphlib.py": "https://files.ballistica.net/cache/ba1/aa/06/0daf2f1e46fce128caae2d0750ee", - "assets/build/pylib-apple/gzip.py": "https://files.ballistica.net/cache/ba1/44/7f/0d7609112f9531b39690a1a887a2", - "assets/build/pylib-apple/hashlib.py": "https://files.ballistica.net/cache/ba1/4d/52/2a4a1e8939b41e3fd648353d0199", - "assets/build/pylib-apple/heapq.py": "https://files.ballistica.net/cache/ba1/cf/ff/e3aa597a8669e6030a2b8ab06228", - "assets/build/pylib-apple/hmac.py": "https://files.ballistica.net/cache/ba1/da/db/160f7bfb3e95c19c81ab6672f30c", - "assets/build/pylib-apple/html/__init__.py": "https://files.ballistica.net/cache/ba1/cd/32/56a082769e4cd6fb8de604e0a1aa", - "assets/build/pylib-apple/html/entities.py": "https://files.ballistica.net/cache/ba1/98/95/6e96db7b66edc0178c1680bbd561", - "assets/build/pylib-apple/html/parser.py": "https://files.ballistica.net/cache/ba1/93/ab/c4572ff7b241d7d5ca88911614f3", - "assets/build/pylib-apple/http/__init__.py": "https://files.ballistica.net/cache/ba1/7e/f4/397bb11e9690b2c5ff940d68549a", - "assets/build/pylib-apple/http/client.py": "https://files.ballistica.net/cache/ba1/33/34/bcca7942c19b4aa6eabf55c18c89", - "assets/build/pylib-apple/http/cookiejar.py": "https://files.ballistica.net/cache/ba1/da/02/a7986f4c3c87f92625bdcac67bec", - "assets/build/pylib-apple/http/cookies.py": "https://files.ballistica.net/cache/ba1/4f/b7/ec4c23a01fb3b7a7a8c88239fb17", - "assets/build/pylib-apple/http/server.py": "https://files.ballistica.net/cache/ba1/cd/b9/43d4719015c9a1659f2f39bbc8cb", - "assets/build/pylib-apple/imghdr.py": "https://files.ballistica.net/cache/ba1/58/22/50ad8d061dc8e6c9d59e96bf04fe", - "assets/build/pylib-apple/imp.py": "https://files.ballistica.net/cache/ba1/8d/31/b012ed0e21c6309c3fdc5dbae44f", - "assets/build/pylib-apple/importlib/__init__.py": "https://files.ballistica.net/cache/ba1/b7/b5/e94307e5d07218f0c7a7a941821f", - "assets/build/pylib-apple/importlib/_abc.py": "https://files.ballistica.net/cache/ba1/ae/10/1ffaabbe74b543934f44cb47d30c", - "assets/build/pylib-apple/importlib/_adapters.py": "https://files.ballistica.net/cache/ba1/b3/e4/3e0b59f3b419d01cfd4746873d93", - "assets/build/pylib-apple/importlib/_bootstrap.py": "https://files.ballistica.net/cache/ba1/e1/7c/19e42bde13539df6bc9a318bb09a", - "assets/build/pylib-apple/importlib/_bootstrap_external.py": "https://files.ballistica.net/cache/ba1/60/fa/c33e0a343361393633e6b8751260", - "assets/build/pylib-apple/importlib/_common.py": "https://files.ballistica.net/cache/ba1/81/1c/b4d831086d6f51fed5e2c698abdf", - "assets/build/pylib-apple/importlib/abc.py": "https://files.ballistica.net/cache/ba1/f0/d8/420f0375bc62e2a22639120bf74b", - "assets/build/pylib-apple/importlib/machinery.py": "https://files.ballistica.net/cache/ba1/55/07/02156726660c760b1a3127f8b79e", - "assets/build/pylib-apple/importlib/metadata/__init__.py": "https://files.ballistica.net/cache/ba1/e9/3e/b0496c4b3374831026709acc5b47", - "assets/build/pylib-apple/importlib/metadata/_adapters.py": "https://files.ballistica.net/cache/ba1/d4/ad/fc75a32c673dc5bb8b3d117086bb", - "assets/build/pylib-apple/importlib/metadata/_collections.py": "https://files.ballistica.net/cache/ba1/d6/7d/c6dddd0da7f8ecb2eb36d55c17a2", - "assets/build/pylib-apple/importlib/metadata/_functools.py": "https://files.ballistica.net/cache/ba1/a8/e0/d3820e359eb5828bd9dff08d3bc7", - "assets/build/pylib-apple/importlib/metadata/_itertools.py": "https://files.ballistica.net/cache/ba1/55/ad/142be4b8de7bdd7820de3ef6086f", - "assets/build/pylib-apple/importlib/metadata/_meta.py": "https://files.ballistica.net/cache/ba1/03/8e/71d8927f7125c82ea2940b495ada", - "assets/build/pylib-apple/importlib/metadata/_text.py": "https://files.ballistica.net/cache/ba1/03/20/c9072c8f9202724cc428558f7558", - "assets/build/pylib-apple/importlib/readers.py": "https://files.ballistica.net/cache/ba1/06/eb/6483f0e8cbe22cbd4a9333c2f683", - "assets/build/pylib-apple/importlib/resources.py": "https://files.ballistica.net/cache/ba1/95/25/315eed219dfc9bc485ec73eb5052", - "assets/build/pylib-apple/importlib/util.py": "https://files.ballistica.net/cache/ba1/88/71/be34273b25a0f80605ec06f1bb45", - "assets/build/pylib-apple/inspect.py": "https://files.ballistica.net/cache/ba1/18/dd/e369140bef7caad4a373b7b64aaa", - "assets/build/pylib-apple/io.py": "https://files.ballistica.net/cache/ba1/ad/15/9f735ac895b10c4f5692d417a948", - "assets/build/pylib-apple/ipaddress.py": "https://files.ballistica.net/cache/ba1/bc/b5/fd6c9c55a2a8ca6d31950e4cd5b4", - "assets/build/pylib-apple/json/__init__.py": "https://files.ballistica.net/cache/ba1/55/c1/e41c17454f7e7b82dcef5e9a2e6f", - "assets/build/pylib-apple/json/decoder.py": "https://files.ballistica.net/cache/ba1/6f/c8/ddcd524fd23ec4c64a11c80905da", - "assets/build/pylib-apple/json/encoder.py": "https://files.ballistica.net/cache/ba1/cc/15/86c577cfb53a1051cecf453532fe", - "assets/build/pylib-apple/json/scanner.py": "https://files.ballistica.net/cache/ba1/81/7c/03b7f8d8f18de6a1b12debd594db", - "assets/build/pylib-apple/json/tool.py": "https://files.ballistica.net/cache/ba1/8d/9f/ad5a8bcd985f3499534bd5bb478a", - "assets/build/pylib-apple/keyword.py": "https://files.ballistica.net/cache/ba1/cb/28/c7bb1f719407191745a8cfbefeef", - "assets/build/pylib-apple/linecache.py": "https://files.ballistica.net/cache/ba1/a0/af/54839d7556f5bc793a4678fa279e", - "assets/build/pylib-apple/locale.py": "https://files.ballistica.net/cache/ba1/33/5e/185036ad08d331a3d9c8a3cfb241", - "assets/build/pylib-apple/logging/__init__.py": "https://files.ballistica.net/cache/ba1/c7/b3/9f056c40375851784445e6168d0e", - "assets/build/pylib-apple/logging/config.py": "https://files.ballistica.net/cache/ba1/01/aa/66a1f2e1e84afe6171c73c985b1d", - "assets/build/pylib-apple/logging/handlers.py": "https://files.ballistica.net/cache/ba1/f7/7a/d855cb881c1ac0ad38e7bde7af57", - "assets/build/pylib-apple/lzma.py": "https://files.ballistica.net/cache/ba1/f0/40/fda7cb5d02583cd1b4a715d1b227", - "assets/build/pylib-apple/mailbox.py": "https://files.ballistica.net/cache/ba1/e3/28/f0d20f21f0858b3e730ebebdfef3", - "assets/build/pylib-apple/mailcap.py": "https://files.ballistica.net/cache/ba1/4d/23/f74089a34cbd2edc6c11ebb790aa", - "assets/build/pylib-apple/mimetypes.py": "https://files.ballistica.net/cache/ba1/73/cc/e95bf43b632f49bfacc548934ed5", - "assets/build/pylib-apple/modulefinder.py": "https://files.ballistica.net/cache/ba1/88/34/e12f85d48d954a45ecf42c243c21", - "assets/build/pylib-apple/msilib/__init__.py": "https://files.ballistica.net/cache/ba1/aa/ff/356e0db47c4c1c06f8e37bcd38a7", - "assets/build/pylib-apple/msilib/schema.py": "https://files.ballistica.net/cache/ba1/04/e9/d46bdc70946e76fd396d55ab02ac", - "assets/build/pylib-apple/msilib/sequence.py": "https://files.ballistica.net/cache/ba1/94/25/0e90328d0fb33814334a3ec64472", - "assets/build/pylib-apple/msilib/text.py": "https://files.ballistica.net/cache/ba1/39/0a/1a182e532d3f0574298120dad765", - "assets/build/pylib-apple/netrc.py": "https://files.ballistica.net/cache/ba1/b3/cc/70c4c51975dde5c270b2c6289fb0", - "assets/build/pylib-apple/nntplib.py": "https://files.ballistica.net/cache/ba1/fa/d5/e0f012ba98a3069ced1c5115c043", - "assets/build/pylib-apple/ntpath.py": "https://files.ballistica.net/cache/ba1/ef/bd/aca7f7b6aaac14452dc89984307b", - "assets/build/pylib-apple/nturl2path.py": "https://files.ballistica.net/cache/ba1/cc/79/e930bdf392702ad2da57f10c8a7a", - "assets/build/pylib-apple/numbers.py": "https://files.ballistica.net/cache/ba1/45/40/fe6afa3e354c7bc5a9a8c3700659", - "assets/build/pylib-apple/opcode.py": "https://files.ballistica.net/cache/ba1/02/9e/38414f54f79c978ccb9b6b12dcf3", - "assets/build/pylib-apple/operator.py": "https://files.ballistica.net/cache/ba1/bf/b2/a75d14b6a59138611ec10490af03", - "assets/build/pylib-apple/optparse.py": "https://files.ballistica.net/cache/ba1/a0/0b/202b321b2eddb9b62f30d58fd1f6", - "assets/build/pylib-apple/os.py": "https://files.ballistica.net/cache/ba1/9b/3f/17b75f3efee27bc95ac835903751", - "assets/build/pylib-apple/pathlib.py": "https://files.ballistica.net/cache/ba1/9c/32/031308f47036d4eb048e5af7c7b3", - "assets/build/pylib-apple/pdb.py": "https://files.ballistica.net/cache/ba1/58/34/e65e4c1be07f9950dc1eba292046", - "assets/build/pylib-apple/pickle.py": "https://files.ballistica.net/cache/ba1/f7/32/0a56be2517d25f3e563e8a26b827", - "assets/build/pylib-apple/pickletools.py": "https://files.ballistica.net/cache/ba1/b0/a2/0655f9bef1f950e8f678be867bec", - "assets/build/pylib-apple/pipes.py": "https://files.ballistica.net/cache/ba1/94/fa/a131498d2c4eb9f055cc22a8d793", - "assets/build/pylib-apple/pkgutil.py": "https://files.ballistica.net/cache/ba1/d3/8a/602831a1f75d847c02ad7078a924", - "assets/build/pylib-apple/platform.py": "https://files.ballistica.net/cache/ba1/19/dc/b254c97465419dc13083ce99537e", - "assets/build/pylib-apple/plistlib.py": "https://files.ballistica.net/cache/ba1/e0/71/58498a0c336ab3a26bfe82db1c37", - "assets/build/pylib-apple/poplib.py": "https://files.ballistica.net/cache/ba1/50/c6/d2c55762b3e2e06754cdec4aad0b", - "assets/build/pylib-apple/posixpath.py": "https://files.ballistica.net/cache/ba1/1a/17/817635fb37731755599525dd9aa9", - "assets/build/pylib-apple/pprint.py": "https://files.ballistica.net/cache/ba1/59/8f/cbbd822ed0c8cd80d379303bb76e", - "assets/build/pylib-apple/profile.py": "https://files.ballistica.net/cache/ba1/87/07/7fc7d9607ba2dd3eace3de730d5a", - "assets/build/pylib-apple/pstats.py": "https://files.ballistica.net/cache/ba1/90/cd/9f43fa464fdfc86a1c817cefb0cf", - "assets/build/pylib-apple/pty.py": "https://files.ballistica.net/cache/ba1/70/a5/cff6f8f395cd37fbd3fb80170e3f", - "assets/build/pylib-apple/py_compile.py": "https://files.ballistica.net/cache/ba1/48/75/32e8b9d382231f378c54ff7eefd3", - "assets/build/pylib-apple/pyclbr.py": "https://files.ballistica.net/cache/ba1/50/1a/b8f037dae6bdd061d070b225d68a", - "assets/build/pylib-apple/pydoc.py": "https://files.ballistica.net/cache/ba1/c2/62/153b2eaa414807b13598a734ddc8", - "assets/build/pylib-apple/queue.py": "https://files.ballistica.net/cache/ba1/bc/12/ad8745f4cd033dce64b4169d4998", - "assets/build/pylib-apple/quopri.py": "https://files.ballistica.net/cache/ba1/71/23/921fa527149e18b7dd43d408f104", - "assets/build/pylib-apple/random.py": "https://files.ballistica.net/cache/ba1/f2/35/d95ff56d8e6699745a83674ca254", - "assets/build/pylib-apple/re.py": "https://files.ballistica.net/cache/ba1/41/e6/e3ea4fc70eb41718a31b4d324cc5", - "assets/build/pylib-apple/reprlib.py": "https://files.ballistica.net/cache/ba1/25/3c/b07febf734908722d45da1ea6c57", - "assets/build/pylib-apple/rlcompleter.py": "https://files.ballistica.net/cache/ba1/62/52/ec8d35309b3eb4a7c7e4c3530f32", - "assets/build/pylib-apple/runpy.py": "https://files.ballistica.net/cache/ba1/7a/9a/ea34ae4cc4c091cb533db1b66b59", - "assets/build/pylib-apple/sched.py": "https://files.ballistica.net/cache/ba1/06/91/a7b4f4f3431caf08d7fb94fd6308", - "assets/build/pylib-apple/secrets.py": "https://files.ballistica.net/cache/ba1/fb/3f/59ef3e01440f29a2d3eb193cb873", - "assets/build/pylib-apple/selectors.py": "https://files.ballistica.net/cache/ba1/4e/d1/ceb7bbee7484d8a49e8a3619766c", - "assets/build/pylib-apple/shelve.py": "https://files.ballistica.net/cache/ba1/f6/29/ddbb59e7d6f704d6717e4d111c32", - "assets/build/pylib-apple/shlex.py": "https://files.ballistica.net/cache/ba1/9c/99/a93fe9fb09dbba3272bb202aa7eb", - "assets/build/pylib-apple/shutil.py": "https://files.ballistica.net/cache/ba1/3d/8e/63d8991b4631064b1859501e3715", - "assets/build/pylib-apple/signal.py": "https://files.ballistica.net/cache/ba1/0c/da/b612cfd20949985f65d4eeddb0cc", - "assets/build/pylib-apple/site.py": "https://files.ballistica.net/cache/ba1/ff/48/d021ba40d26d798872d1bc0271c6", - "assets/build/pylib-apple/smtpd.py": "https://files.ballistica.net/cache/ba1/b8/0c/89117001b5fb9c8f071861c9fdea", - "assets/build/pylib-apple/smtplib.py": "https://files.ballistica.net/cache/ba1/7f/51/bfd11363e815d8d4eb2bc1d86efc", - "assets/build/pylib-apple/sndhdr.py": "https://files.ballistica.net/cache/ba1/14/6a/c95a945f5f442e33567069ff95bd", - "assets/build/pylib-apple/socket.py": "https://files.ballistica.net/cache/ba1/4a/82/d898106fd89274a03a7f67632906", - "assets/build/pylib-apple/socketserver.py": "https://files.ballistica.net/cache/ba1/e4/81/ae642b90684451a011e516f74241", - "assets/build/pylib-apple/sqlite3/__init__.py": "https://files.ballistica.net/cache/ba1/2a/0e/a5d905511bdb3337a6c72e450fc8", - "assets/build/pylib-apple/sqlite3/dbapi2.py": "https://files.ballistica.net/cache/ba1/31/32/b4f26475e8260a6d452b35910e9e", - "assets/build/pylib-apple/sqlite3/dump.py": "https://files.ballistica.net/cache/ba1/cb/c9/2ea904e0824aefc3d9524174fd29", - "assets/build/pylib-apple/sre_compile.py": "https://files.ballistica.net/cache/ba1/7d/b1/e88453af530e58284561945056c4", - "assets/build/pylib-apple/sre_constants.py": "https://files.ballistica.net/cache/ba1/95/ef/a427cef2c7463b6906f53e8811b8", - "assets/build/pylib-apple/sre_parse.py": "https://files.ballistica.net/cache/ba1/d4/30/2b0f7916003e13fcc90c80d10703", - "assets/build/pylib-apple/ssl.py": "https://files.ballistica.net/cache/ba1/8c/4f/13ecae34b3a6868be5a32e912dd5", - "assets/build/pylib-apple/stat.py": "https://files.ballistica.net/cache/ba1/9b/79/01acd372faf539e10c6f87803eae", - "assets/build/pylib-apple/statistics.py": "https://files.ballistica.net/cache/ba1/ca/99/f9a5420e036fa3be8c71535b465e", - "assets/build/pylib-apple/string.py": "https://files.ballistica.net/cache/ba1/26/91/1f4fa0849214246a5befd79ffa18", - "assets/build/pylib-apple/stringprep.py": "https://files.ballistica.net/cache/ba1/f0/9b/77cc5580b139f527ee84fff812fc", - "assets/build/pylib-apple/struct.py": "https://files.ballistica.net/cache/ba1/10/6d/7a6c0fbac83b2680bbeda8585f8f", - "assets/build/pylib-apple/subprocess.py": "https://files.ballistica.net/cache/ba1/bc/3d/e391c3bcdf75e729a22bbf2b3547", - "assets/build/pylib-apple/sunau.py": "https://files.ballistica.net/cache/ba1/e0/49/c14a20e198761c7dfa423f09941e", - "assets/build/pylib-apple/symtable.py": "https://files.ballistica.net/cache/ba1/02/3b/237d9ffb4d8fb0b4773fbcb0f32b", - "assets/build/pylib-apple/sysconfig.py": "https://files.ballistica.net/cache/ba1/a7/46/c150a36353b7e0c64e4c3664e24d", - "assets/build/pylib-apple/tabnanny.py": "https://files.ballistica.net/cache/ba1/f7/ba/da1e12d53ebdf326581c99c7d29b", - "assets/build/pylib-apple/tarfile.py": "https://files.ballistica.net/cache/ba1/42/16/0a2a06a78fb2b7faa62bb3f5ce63", - "assets/build/pylib-apple/telnetlib.py": "https://files.ballistica.net/cache/ba1/48/28/786c9600155c60407fe91045d587", - "assets/build/pylib-apple/tempfile.py": "https://files.ballistica.net/cache/ba1/3e/0f/88ce7fa67d687d1fa4eae8a8c47e", - "assets/build/pylib-apple/textwrap.py": "https://files.ballistica.net/cache/ba1/db/d0/5d253847b4dd8a9e7dd052f99b76", - "assets/build/pylib-apple/this.py": "https://files.ballistica.net/cache/ba1/ae/6a/c4cfb10d365db8ca16afef89958e", - "assets/build/pylib-apple/threading.py": "https://files.ballistica.net/cache/ba1/d2/27/8093fc76b70739b09f8902008873", - "assets/build/pylib-apple/timeit.py": "https://files.ballistica.net/cache/ba1/fd/db/62a5917e9d91cafdeb540a63d4ac", - "assets/build/pylib-apple/token.py": "https://files.ballistica.net/cache/ba1/2f/4a/9422c7f7050f45769ce6b6bd38c2", - "assets/build/pylib-apple/tokenize.py": "https://files.ballistica.net/cache/ba1/dd/76/2f02ffb84f770949f448add95220", - "assets/build/pylib-apple/trace.py": "https://files.ballistica.net/cache/ba1/8f/2e/ebf6b39addebbcbe2a77147fe715", - "assets/build/pylib-apple/traceback.py": "https://files.ballistica.net/cache/ba1/f1/0d/d50f1e128bb36993445cecfb187f", - "assets/build/pylib-apple/tracemalloc.py": "https://files.ballistica.net/cache/ba1/77/0e/a83629cfbc0a9560ace295d5c403", - "assets/build/pylib-apple/tty.py": "https://files.ballistica.net/cache/ba1/ec/ea/2421fecb0e38e38d55cf0ce2b0e2", - "assets/build/pylib-apple/types.py": "https://files.ballistica.net/cache/ba1/69/b0/986b7f46a7af6c1b5a28758b647b", - "assets/build/pylib-apple/typing.py": "https://files.ballistica.net/cache/ba1/d9/c9/a8355ac260e10e6b7a6b0afad6a8", - "assets/build/pylib-apple/urllib/__init__.py": "https://files.ballistica.net/cache/ba1/a2/c9/6d1cda1b043897ad0b5b043e7112", - "assets/build/pylib-apple/urllib/error.py": "https://files.ballistica.net/cache/ba1/09/dd/15e4e9e675bd3242b0d5fb0f2707", - "assets/build/pylib-apple/urllib/parse.py": "https://files.ballistica.net/cache/ba1/53/7e/3996c67b09f3610e23343f5cfad9", - "assets/build/pylib-apple/urllib/request.py": "https://files.ballistica.net/cache/ba1/ac/b1/49252679b259379e0647a2663a85", - "assets/build/pylib-apple/urllib/response.py": "https://files.ballistica.net/cache/ba1/80/27/642b1adb216af47e5401647276e7", - "assets/build/pylib-apple/urllib/robotparser.py": "https://files.ballistica.net/cache/ba1/ba/83/b9c99d5b11514b827d64f9fd6d33", - "assets/build/pylib-apple/uu.py": "https://files.ballistica.net/cache/ba1/02/7a/d6fed645dcff0d4aff84e3cea58e", - "assets/build/pylib-apple/uuid.py": "https://files.ballistica.net/cache/ba1/f1/2a/9e44739fed968c3b750184ff1b65", - "assets/build/pylib-apple/warnings.py": "https://files.ballistica.net/cache/ba1/65/5c/5e6cf7bf573f4fae2344c68bed04", - "assets/build/pylib-apple/wave.py": "https://files.ballistica.net/cache/ba1/fe/5d/63cca128c210230749e7ab44d94c", - "assets/build/pylib-apple/weakref.py": "https://files.ballistica.net/cache/ba1/c0/ca/94e82ef3b761a544339a2403763b", - "assets/build/pylib-apple/webbrowser.py": "https://files.ballistica.net/cache/ba1/b5/be/ac8ace18e1d9c9d12e2e9e81a038", - "assets/build/pylib-apple/xdrlib.py": "https://files.ballistica.net/cache/ba1/b7/83/ac6e63a15cead601475a09350849", - "assets/build/pylib-apple/xml/__init__.py": "https://files.ballistica.net/cache/ba1/3f/bd/6072ff48fc04c3af1dcbb8005adf", - "assets/build/pylib-apple/xml/dom/NodeFilter.py": "https://files.ballistica.net/cache/ba1/97/e8/e3ea178b500cab89a64c7e5d3d81", - "assets/build/pylib-apple/xml/dom/__init__.py": "https://files.ballistica.net/cache/ba1/31/12/a48c1565e682174e10bbbcf5b819", - "assets/build/pylib-apple/xml/dom/domreg.py": "https://files.ballistica.net/cache/ba1/7b/7d/a7cea1700813d14a0909085369b0", - "assets/build/pylib-apple/xml/dom/expatbuilder.py": "https://files.ballistica.net/cache/ba1/91/0e/e275bf104b202bf61654bd70d525", - "assets/build/pylib-apple/xml/dom/minicompat.py": "https://files.ballistica.net/cache/ba1/97/0c/a28bca0a0bd221a70cb6377f1bef", - "assets/build/pylib-apple/xml/dom/minidom.py": "https://files.ballistica.net/cache/ba1/cb/1c/c49400fc90343e994e3fcf2363d0", - "assets/build/pylib-apple/xml/dom/pulldom.py": "https://files.ballistica.net/cache/ba1/bc/df/46a7e10fac8b5dd6c0361aaef6a2", - "assets/build/pylib-apple/xml/dom/xmlbuilder.py": "https://files.ballistica.net/cache/ba1/40/f2/d8f3279b82679d625fc279e557f3", - "assets/build/pylib-apple/xml/etree/ElementInclude.py": "https://files.ballistica.net/cache/ba1/ec/fa/a81b10846111c57b947032e0c585", - "assets/build/pylib-apple/xml/etree/ElementPath.py": "https://files.ballistica.net/cache/ba1/46/4e/0afb87cd583de1a4bb2433d5407b", - "assets/build/pylib-apple/xml/etree/ElementTree.py": "https://files.ballistica.net/cache/ba1/bb/e6/5906765a97ff0eca34e5651a8e3d", - "assets/build/pylib-apple/xml/etree/__init__.py": "https://files.ballistica.net/cache/ba1/39/a0/bc399cd9767de7f639505ec0a600", - "assets/build/pylib-apple/xml/etree/cElementTree.py": "https://files.ballistica.net/cache/ba1/50/ec/0cd835d512fa5ca9bcada07c27ab", - "assets/build/pylib-apple/xml/parsers/__init__.py": "https://files.ballistica.net/cache/ba1/2c/3e/7f79fe325f250709ab2c01bedada", - "assets/build/pylib-apple/xml/parsers/expat.py": "https://files.ballistica.net/cache/ba1/58/65/6ea61e8a28312897baa67deeac2e", - "assets/build/pylib-apple/xml/sax/__init__.py": "https://files.ballistica.net/cache/ba1/7c/a4/ee44447f0bf4dab1fdaa5401a41c", - "assets/build/pylib-apple/xml/sax/_exceptions.py": "https://files.ballistica.net/cache/ba1/82/3b/a63de9807fe5698ad1283e70e261", - "assets/build/pylib-apple/xml/sax/expatreader.py": "https://files.ballistica.net/cache/ba1/c1/7b/bffc2cc52335fab24863a87692a9", - "assets/build/pylib-apple/xml/sax/handler.py": "https://files.ballistica.net/cache/ba1/fd/72/3f8119ef19fec55632fb5e435ac7", - "assets/build/pylib-apple/xml/sax/saxutils.py": "https://files.ballistica.net/cache/ba1/2f/a5/38f2998b6ed24c674aa04a32d899", - "assets/build/pylib-apple/xml/sax/xmlreader.py": "https://files.ballistica.net/cache/ba1/41/e7/9fcf670b326880d5452a7cc4cc7c", - "assets/build/pylib-apple/xmlrpc/__init__.py": "https://files.ballistica.net/cache/ba1/7a/ee/61deeb7b264890b54b1cbb894cf8", - "assets/build/pylib-apple/xmlrpc/client.py": "https://files.ballistica.net/cache/ba1/38/45/b4f956357fbe44d40c132ad27751", - "assets/build/pylib-apple/xmlrpc/server.py": "https://files.ballistica.net/cache/ba1/9d/72/33aae04c01bfc089940204313847", - "assets/build/pylib-apple/zipapp.py": "https://files.ballistica.net/cache/ba1/bc/39/2d745b00133cddd197c3a4ee400e", - "assets/build/pylib-apple/zipfile.py": "https://files.ballistica.net/cache/ba1/55/11/3e268f39098567f143bab0da1264", - "assets/build/pylib-apple/zipimport.py": "https://files.ballistica.net/cache/ba1/f1/cb/34ae54fc9a0d3c1fc44e0e4329bf", - "assets/build/pylib-apple/zoneinfo/__init__.py": "https://files.ballistica.net/cache/ba1/0d/98/0cf009982c33ab3cfec1cd201628", - "assets/build/pylib-apple/zoneinfo/_common.py": "https://files.ballistica.net/cache/ba1/a9/4e/df4e928cc6457870c159ede106f0", - "assets/build/pylib-apple/zoneinfo/_tzpath.py": "https://files.ballistica.net/cache/ba1/23/43/a1f97306955413a1100b70e04d10", - "assets/build/pylib-apple/zoneinfo/_zoneinfo.py": "https://files.ballistica.net/cache/ba1/c9/e2/93f31abbcd1e93a9be9a300469ee", - "assets/build/windows/Win32/DLLs/_asyncio.pyd": "https://files.ballistica.net/cache/ba1/bf/da/e699583d49bd93d6aa47911a16ed", - "assets/build/windows/Win32/DLLs/_asyncio_d.pyd": "https://files.ballistica.net/cache/ba1/23/9a/183eeda462608eda29febfc6c4da", - "assets/build/windows/Win32/DLLs/_bz2.pyd": "https://files.ballistica.net/cache/ba1/db/f5/fb6a7c3e930381470fd06134aae5", - "assets/build/windows/Win32/DLLs/_bz2_d.pyd": "https://files.ballistica.net/cache/ba1/3e/51/e332d9a2a7be60bbf8196a3253c7", - "assets/build/windows/Win32/DLLs/_ctypes.pyd": "https://files.ballistica.net/cache/ba1/26/30/42591f183474a3b0aa9c87fe0a5a", - "assets/build/windows/Win32/DLLs/_ctypes_d.pyd": "https://files.ballistica.net/cache/ba1/86/07/f69d56ac5059ac7f49c28962d236", - "assets/build/windows/Win32/DLLs/_ctypes_test.pyd": "https://files.ballistica.net/cache/ba1/10/3f/e40142d5e90aa0f3db82c8e430a6", - "assets/build/windows/Win32/DLLs/_ctypes_test_d.pyd": "https://files.ballistica.net/cache/ba1/78/0e/1a5b0162f4edf7b530c796761ca4", - "assets/build/windows/Win32/DLLs/_decimal.pyd": "https://files.ballistica.net/cache/ba1/72/4a/ca5c44f0d8e5f5fe8d82ed353d72", - "assets/build/windows/Win32/DLLs/_decimal_d.pyd": "https://files.ballistica.net/cache/ba1/3b/e8/9c853af841f6ac51a27f31d15acd", - "assets/build/windows/Win32/DLLs/_elementtree.pyd": "https://files.ballistica.net/cache/ba1/25/90/9d2d80cb8e0fb75cd4cca35f1b56", - "assets/build/windows/Win32/DLLs/_elementtree_d.pyd": "https://files.ballistica.net/cache/ba1/b6/2e/42fb179f6d81a41b280b78cf1b76", - "assets/build/windows/Win32/DLLs/_hashlib.pyd": "https://files.ballistica.net/cache/ba1/6f/4a/2786921b043b2aedaa5e2a87aa9a", - "assets/build/windows/Win32/DLLs/_hashlib_d.pyd": "https://files.ballistica.net/cache/ba1/b1/5e/daafdcbbb8bea93a33740e79ab61", - "assets/build/windows/Win32/DLLs/_lzma.pyd": "https://files.ballistica.net/cache/ba1/2d/85/9606b23546b1bf88f101ee8dae49", - "assets/build/windows/Win32/DLLs/_lzma_d.pyd": "https://files.ballistica.net/cache/ba1/40/20/73e7745f7bba86d03f2b7311058b", - "assets/build/windows/Win32/DLLs/_msi.pyd": "https://files.ballistica.net/cache/ba1/28/d4/4d149bf2972eb2944ebc730959ad", - "assets/build/windows/Win32/DLLs/_msi_d.pyd": "https://files.ballistica.net/cache/ba1/b9/b7/f196a189b7701b0afac9e7cb545c", - "assets/build/windows/Win32/DLLs/_multiprocessing.pyd": "https://files.ballistica.net/cache/ba1/30/80/66cadf6832ddfd922e39e9c6f586", - "assets/build/windows/Win32/DLLs/_multiprocessing_d.pyd": "https://files.ballistica.net/cache/ba1/e0/f7/92f033608974a809d5515fa97a59", - "assets/build/windows/Win32/DLLs/_overlapped.pyd": "https://files.ballistica.net/cache/ba1/47/e5/e609bbb2f5b21851f5b5912e7d23", - "assets/build/windows/Win32/DLLs/_overlapped_d.pyd": "https://files.ballistica.net/cache/ba1/dd/df/f7e511499fedd2a8b8d3ad6224be", - "assets/build/windows/Win32/DLLs/_queue.pyd": "https://files.ballistica.net/cache/ba1/7c/6d/b00c0348542d2718a211fff93d66", - "assets/build/windows/Win32/DLLs/_queue_d.pyd": "https://files.ballistica.net/cache/ba1/f4/0d/ca7b5681c717303ad71a40bc8e50", - "assets/build/windows/Win32/DLLs/_socket.pyd": "https://files.ballistica.net/cache/ba1/79/51/2775e17b15cd45a84c467c9fe29f", - "assets/build/windows/Win32/DLLs/_socket_d.pyd": "https://files.ballistica.net/cache/ba1/48/ef/c1ab6c24512720c3556310eb9546", - "assets/build/windows/Win32/DLLs/_sqlite3.pyd": "https://files.ballistica.net/cache/ba1/10/9f/75ec3ea1562f13eb8bd6e03ae8b3", - "assets/build/windows/Win32/DLLs/_sqlite3_d.pyd": "https://files.ballistica.net/cache/ba1/d9/c0/3800e59780afcbbc0c22559ae94d", - "assets/build/windows/Win32/DLLs/_ssl.pyd": "https://files.ballistica.net/cache/ba1/33/2f/dc884702b753197052e5acd720ac", - "assets/build/windows/Win32/DLLs/_ssl_d.pyd": "https://files.ballistica.net/cache/ba1/1a/ff/80563968a8bdada77f4896a2f2a9", - "assets/build/windows/Win32/DLLs/_testbuffer.pyd": "https://files.ballistica.net/cache/ba1/01/da/e81a4e00f26ecbd4d71085357f41", - "assets/build/windows/Win32/DLLs/_testbuffer_d.pyd": "https://files.ballistica.net/cache/ba1/53/ee/35c525b253aef7c589d5488d2640", - "assets/build/windows/Win32/DLLs/_testcapi.pyd": "https://files.ballistica.net/cache/ba1/03/2e/19d4df0a63060383188c20aa3058", - "assets/build/windows/Win32/DLLs/_testcapi_d.pyd": "https://files.ballistica.net/cache/ba1/60/ff/1e55a5a241707e24b0babb473767", - "assets/build/windows/Win32/DLLs/_testconsole.pyd": "https://files.ballistica.net/cache/ba1/ac/32/e385833067a7dd028ac83b38bec6", - "assets/build/windows/Win32/DLLs/_testconsole_d.pyd": "https://files.ballistica.net/cache/ba1/1d/40/3a9cf567c58fb2503435704e8537", - "assets/build/windows/Win32/DLLs/_testimportmultiple.pyd": "https://files.ballistica.net/cache/ba1/9e/97/9f75f4bd187ba2c6aa1e0a84915f", - "assets/build/windows/Win32/DLLs/_testimportmultiple_d.pyd": "https://files.ballistica.net/cache/ba1/63/b3/0c269237df5c520a27d54a0e1e61", - "assets/build/windows/Win32/DLLs/_testinternalcapi.pyd": "https://files.ballistica.net/cache/ba1/23/1e/5dbf87362e9a17166f4b0e3708c9", - "assets/build/windows/Win32/DLLs/_testinternalcapi_d.pyd": "https://files.ballistica.net/cache/ba1/f3/88/a729e10ae4ae34e4934a2ab153db", - "assets/build/windows/Win32/DLLs/_testmultiphase.pyd": "https://files.ballistica.net/cache/ba1/4f/24/5406b0e4624e71259885469bb136", - "assets/build/windows/Win32/DLLs/_testmultiphase_d.pyd": "https://files.ballistica.net/cache/ba1/d4/cf/0a4f6158cdbb22a25a6c01296227", - "assets/build/windows/Win32/DLLs/_tkinter.pyd": "https://files.ballistica.net/cache/ba1/df/71/186adca16a1c49bd990b6c17b8c8", - "assets/build/windows/Win32/DLLs/_tkinter_d.lib": "https://files.ballistica.net/cache/ba1/97/16/569ec65b87b47719c832bc080856", - "assets/build/windows/Win32/DLLs/_tkinter_d.pyd": "https://files.ballistica.net/cache/ba1/6e/c9/5379e078bc473e7ac089e7bdb071", - "assets/build/windows/Win32/DLLs/_uuid.pyd": "https://files.ballistica.net/cache/ba1/ca/cb/d8e6871daeb8f77877ca5cd433b0", - "assets/build/windows/Win32/DLLs/_uuid_d.pyd": "https://files.ballistica.net/cache/ba1/a2/fc/6bb81642e6eda3706173b09eb202", - "assets/build/windows/Win32/DLLs/_zoneinfo.pyd": "https://files.ballistica.net/cache/ba1/7f/dc/2c0e618167a0715a6b07ece79116", - "assets/build/windows/Win32/DLLs/_zoneinfo_d.pyd": "https://files.ballistica.net/cache/ba1/ab/16/7f8d19ef3f9a8471482bb1367c8b", - "assets/build/windows/Win32/DLLs/libcrypto-1_1.dll": "https://files.ballistica.net/cache/ba1/f9/aa/5f3439fc824a3e47bf5db60b0f23", - "assets/build/windows/Win32/DLLs/libffi-7.dll": "https://files.ballistica.net/cache/ba1/0a/62/9908b69e4aace0659b7ba62772bf", - "assets/build/windows/Win32/DLLs/libssl-1_1.dll": "https://files.ballistica.net/cache/ba1/a5/00/7bb314df13207b184bc95a436045", - "assets/build/windows/Win32/DLLs/pyexpat.pyd": "https://files.ballistica.net/cache/ba1/5b/63/2723f9b9199c0e9009e58d1d6ab6", - "assets/build/windows/Win32/DLLs/pyexpat_d.pyd": "https://files.ballistica.net/cache/ba1/89/05/734741f95ee7506da7e5c4fbda47", - "assets/build/windows/Win32/DLLs/python_lib.cat": "https://files.ballistica.net/cache/ba1/82/ce/61c7fdb6c6938d3e0407874ef45c", - "assets/build/windows/Win32/DLLs/python_tools.cat": "https://files.ballistica.net/cache/ba1/89/be/54a26d57f2e9b528a835263018d1", - "assets/build/windows/Win32/DLLs/select.pyd": "https://files.ballistica.net/cache/ba1/33/de/1cf474cd6f742b1a6065b293899b", - "assets/build/windows/Win32/DLLs/select_d.pyd": "https://files.ballistica.net/cache/ba1/2e/1a/a4fc07d5ed08e0eb57ebfd85d399", - "assets/build/windows/Win32/DLLs/sqlite3.dll": "https://files.ballistica.net/cache/ba1/bb/0b/9312c372a96f0849792157f52efd", - "assets/build/windows/Win32/DLLs/sqlite3_d.dll": "https://files.ballistica.net/cache/ba1/7a/ff/53634bbe2da6927789e8a5bda97b", - "assets/build/windows/Win32/DLLs/tcl86t.dll": "https://files.ballistica.net/cache/ba1/60/a1/0555f843f1a7038166d85d577793", - "assets/build/windows/Win32/DLLs/tk86t.dll": "https://files.ballistica.net/cache/ba1/b7/09/d55c684787da522106213f1f5cda", - "assets/build/windows/Win32/DLLs/unicodedata.pyd": "https://files.ballistica.net/cache/ba1/e3/33/4cc36d9dd067b5240fd7cee890a6", - "assets/build/windows/Win32/DLLs/unicodedata_d.pyd": "https://files.ballistica.net/cache/ba1/23/98/d91874caecc6815b971e5bcf0684", - "assets/build/windows/Win32/DLLs/winsound.pyd": "https://files.ballistica.net/cache/ba1/19/8c/d0ed2552a057e8bdf4c3d639a7cb", - "assets/build/windows/Win32/DLLs/winsound_d.pyd": "https://files.ballistica.net/cache/ba1/30/43/7975ef43dcd2e76e20530f804628", - "assets/build/windows/Win32/Lib/__future__.py": "https://files.ballistica.net/cache/ba1/17/df/d92237082c55e04318c4b548ead9", - "assets/build/windows/Win32/Lib/__phello__.foo.py": "https://files.ballistica.net/cache/ba1/3b/8b/939d78ee0764fdf52f3098127d6c", - "assets/build/windows/Win32/Lib/_aix_support.py": "https://files.ballistica.net/cache/ba1/39/f1/66fd3be8a8a2ff3b7b9659cf2cbe", - "assets/build/windows/Win32/Lib/_bootsubprocess.py": "https://files.ballistica.net/cache/ba1/7b/c0/88d4a649d94551456dc8702f3739", - "assets/build/windows/Win32/Lib/_collections_abc.py": "https://files.ballistica.net/cache/ba1/08/a4/e7b2a3795c5711bc530f1b931c7a", - "assets/build/windows/Win32/Lib/_compat_pickle.py": "https://files.ballistica.net/cache/ba1/46/06/1015248f3c4416edb60e7830aecb", - "assets/build/windows/Win32/Lib/_compression.py": "https://files.ballistica.net/cache/ba1/cb/30/b1588586156c30a8060b26bced00", - "assets/build/windows/Win32/Lib/_markupbase.py": "https://files.ballistica.net/cache/ba1/00/4a/d605432f43bda241dd39ff23f33c", - "assets/build/windows/Win32/Lib/_osx_support.py": "https://files.ballistica.net/cache/ba1/07/15/fc55a31ea0a03ad61ece475ae825", - "assets/build/windows/Win32/Lib/_py_abc.py": "https://files.ballistica.net/cache/ba1/ad/d8/684169061fcf843ea3541d4a27a6", - "assets/build/windows/Win32/Lib/_pydecimal.py": "https://files.ballistica.net/cache/ba1/f6/dd/b35b32270e6b49474a2ba848e406", - "assets/build/windows/Win32/Lib/_pyio.py": "https://files.ballistica.net/cache/ba1/6b/ae/7aa136c2b4e7d41fe3054ab5ae06", - "assets/build/windows/Win32/Lib/_sitebuiltins.py": "https://files.ballistica.net/cache/ba1/05/be/b187e9ac9417a2493879b08a52a8", - "assets/build/windows/Win32/Lib/_strptime.py": "https://files.ballistica.net/cache/ba1/1f/d3/d9d48711d7c2f8272d85172bc142", - "assets/build/windows/Win32/Lib/_threading_local.py": "https://files.ballistica.net/cache/ba1/6c/8a/9ac70e582f8ec4da33694a8a2409", - "assets/build/windows/Win32/Lib/_weakrefset.py": "https://files.ballistica.net/cache/ba1/98/8f/0eefe23bddf158278317f3c713a9", - "assets/build/windows/Win32/Lib/abc.py": "https://files.ballistica.net/cache/ba1/1e/08/8dd2b52b5e0dff1f9ea873050c5a", - "assets/build/windows/Win32/Lib/aifc.py": "https://files.ballistica.net/cache/ba1/c8/7e/7a303b0e1b32c42dc2878e80c931", - "assets/build/windows/Win32/Lib/antigravity.py": "https://files.ballistica.net/cache/ba1/06/68/e45f30bcc009d8e4647b2d881cf8", - "assets/build/windows/Win32/Lib/argparse.py": "https://files.ballistica.net/cache/ba1/12/33/d25ba7b03386dc952ad40cc24c23", - "assets/build/windows/Win32/Lib/ast.py": "https://files.ballistica.net/cache/ba1/8c/5e/4a442df5dece78166af96d3b8b49", - "assets/build/windows/Win32/Lib/asynchat.py": "https://files.ballistica.net/cache/ba1/51/11/4ee307f675b2ff3ebae52d870f6f", - "assets/build/windows/Win32/Lib/asyncio/__init__.py": "https://files.ballistica.net/cache/ba1/51/60/b751f463f7866ca08575c7a04518", - "assets/build/windows/Win32/Lib/asyncio/__main__.py": "https://files.ballistica.net/cache/ba1/fb/f0/a4fad40aea4845a907af269fa159", - "assets/build/windows/Win32/Lib/asyncio/base_events.py": "https://files.ballistica.net/cache/ba1/87/86/7629cb9027dae148cf35a784eee8", - "assets/build/windows/Win32/Lib/asyncio/base_futures.py": "https://files.ballistica.net/cache/ba1/c8/13/786974d187c40e2b50174f73f040", - "assets/build/windows/Win32/Lib/asyncio/base_subprocess.py": "https://files.ballistica.net/cache/ba1/0e/c0/3384fbba49486643e507a49770ee", - "assets/build/windows/Win32/Lib/asyncio/base_tasks.py": "https://files.ballistica.net/cache/ba1/28/9d/b3adf943122909eb73dd216ef179", - "assets/build/windows/Win32/Lib/asyncio/constants.py": "https://files.ballistica.net/cache/ba1/28/3e/49ec61050acc59c2bb5761d5332e", - "assets/build/windows/Win32/Lib/asyncio/coroutines.py": "https://files.ballistica.net/cache/ba1/22/0f/65ce7b4328dbdf3a300616755b5f", - "assets/build/windows/Win32/Lib/asyncio/events.py": "https://files.ballistica.net/cache/ba1/09/2f/a1022a33d59f2649e85c098f8cfe", - "assets/build/windows/Win32/Lib/asyncio/exceptions.py": "https://files.ballistica.net/cache/ba1/8e/d5/3836a190b2e188ab2889cfd572b9", - "assets/build/windows/Win32/Lib/asyncio/format_helpers.py": "https://files.ballistica.net/cache/ba1/40/98/952c80350fd35c81680c0d565aa1", - "assets/build/windows/Win32/Lib/asyncio/futures.py": "https://files.ballistica.net/cache/ba1/7d/12/d81053535161b19e9a573fd9e445", - "assets/build/windows/Win32/Lib/asyncio/locks.py": "https://files.ballistica.net/cache/ba1/ea/18/689d9939acdee8367036c9020fb5", - "assets/build/windows/Win32/Lib/asyncio/log.py": "https://files.ballistica.net/cache/ba1/d6/d3/380f88b21d3b8ef14f758f283af0", - "assets/build/windows/Win32/Lib/asyncio/mixins.py": "https://files.ballistica.net/cache/ba1/5f/2e/e79239baecdb7bbc35dc15d86a7c", - "assets/build/windows/Win32/Lib/asyncio/proactor_events.py": "https://files.ballistica.net/cache/ba1/72/95/b62df145f7b28eaffc9a73fc1b9b", - "assets/build/windows/Win32/Lib/asyncio/protocols.py": "https://files.ballistica.net/cache/ba1/22/2d/29111353bfcd153bc4cb43083c13", - "assets/build/windows/Win32/Lib/asyncio/queues.py": "https://files.ballistica.net/cache/ba1/1c/ed/c4a96f969effd307a58983afa338", - "assets/build/windows/Win32/Lib/asyncio/runners.py": "https://files.ballistica.net/cache/ba1/99/ce/3c89bb13bc3acf9572d3bc20e246", - "assets/build/windows/Win32/Lib/asyncio/selector_events.py": "https://files.ballistica.net/cache/ba1/8c/8b/e1417d83936cd01aeb431ac84357", - "assets/build/windows/Win32/Lib/asyncio/sslproto.py": "https://files.ballistica.net/cache/ba1/24/88/46522ced6a1a2f482d42dbe9a9b5", - "assets/build/windows/Win32/Lib/asyncio/staggered.py": "https://files.ballistica.net/cache/ba1/9f/52/01396863292b0b31dfbc92c03907", - "assets/build/windows/Win32/Lib/asyncio/streams.py": "https://files.ballistica.net/cache/ba1/59/f6/b7acdf85fb9a3fb405f2ed7f7fd9", - "assets/build/windows/Win32/Lib/asyncio/subprocess.py": "https://files.ballistica.net/cache/ba1/a3/fe/e7f5f439a52bb7f8a9b39d941be0", - "assets/build/windows/Win32/Lib/asyncio/tasks.py": "https://files.ballistica.net/cache/ba1/90/90/6001aac1c4d26b31b5d681a2b053", - "assets/build/windows/Win32/Lib/asyncio/threads.py": "https://files.ballistica.net/cache/ba1/8b/ec/747fd095b73a211ce132a135270d", - "assets/build/windows/Win32/Lib/asyncio/transports.py": "https://files.ballistica.net/cache/ba1/2d/85/081c056105f6b8af8cd0c9c8ad81", - "assets/build/windows/Win32/Lib/asyncio/trsock.py": "https://files.ballistica.net/cache/ba1/46/88/ab7c4a2f09e5a3b9f675d5f89b2e", - "assets/build/windows/Win32/Lib/asyncio/unix_events.py": "https://files.ballistica.net/cache/ba1/60/a8/90320d7fc93e3480bfac154350ae", - "assets/build/windows/Win32/Lib/asyncio/windows_events.py": "https://files.ballistica.net/cache/ba1/4d/5a/89eb0a1181fe8eb02bc4975188bf", - "assets/build/windows/Win32/Lib/asyncio/windows_utils.py": "https://files.ballistica.net/cache/ba1/bd/ed/ddb1d357625e17352ed9928cb9e5", - "assets/build/windows/Win32/Lib/asyncore.py": "https://files.ballistica.net/cache/ba1/49/79/f5f41e4ef573f6326adf5e1e7989", - "assets/build/windows/Win32/Lib/base64.py": "https://files.ballistica.net/cache/ba1/c0/bd/3e27bf842999ff245775aef24309", - "assets/build/windows/Win32/Lib/bdb.py": "https://files.ballistica.net/cache/ba1/fe/45/416917a5738c262c618d5e6971f5", - "assets/build/windows/Win32/Lib/binhex.py": "https://files.ballistica.net/cache/ba1/ea/3b/4934f59948cd40a4d47462ae8a7f", - "assets/build/windows/Win32/Lib/bisect.py": "https://files.ballistica.net/cache/ba1/7f/81/4f58373ce27024b4de482a0d1ecd", - "assets/build/windows/Win32/Lib/bz2.py": "https://files.ballistica.net/cache/ba1/d7/9e/7a792bf73940671a8a0d41ac64b9", - "assets/build/windows/Win32/Lib/cProfile.py": "https://files.ballistica.net/cache/ba1/c3/00/d6ba42740a6a153aa1c5bf6e15e5", - "assets/build/windows/Win32/Lib/calendar.py": "https://files.ballistica.net/cache/ba1/33/ee/b3e80b98ba71fa279e211e79890e", - "assets/build/windows/Win32/Lib/cgi.py": "https://files.ballistica.net/cache/ba1/b2/07/a811efeff5f1835c050908564464", - "assets/build/windows/Win32/Lib/cgitb.py": "https://files.ballistica.net/cache/ba1/ff/e6/b30e42109a8bfd593a30daa12579", - "assets/build/windows/Win32/Lib/chunk.py": "https://files.ballistica.net/cache/ba1/16/08/2708ae495aab5e54fe27da06f633", - "assets/build/windows/Win32/Lib/cmd.py": "https://files.ballistica.net/cache/ba1/cc/83/f3046ec22ee06c45649da6add0c7", - "assets/build/windows/Win32/Lib/code.py": "https://files.ballistica.net/cache/ba1/61/31/f1ff9d938a5f29efe83838362b52", - "assets/build/windows/Win32/Lib/codecs.py": "https://files.ballistica.net/cache/ba1/82/fe/f735f8bb999fa25bff976a682067", - "assets/build/windows/Win32/Lib/codeop.py": "https://files.ballistica.net/cache/ba1/db/94/8e70bf73babb77f92633ecc1bcb7", - "assets/build/windows/Win32/Lib/collections/__init__.py": "https://files.ballistica.net/cache/ba1/9f/8c/8315cadaadb0d5088aaafefdf48c", - "assets/build/windows/Win32/Lib/collections/abc.py": "https://files.ballistica.net/cache/ba1/47/0c/2287da3638e7910fecb7b32504a0", - "assets/build/windows/Win32/Lib/colorsys.py": "https://files.ballistica.net/cache/ba1/8e/ce/47d3813a9a095c1ea478afb2051f", - "assets/build/windows/Win32/Lib/compileall.py": "https://files.ballistica.net/cache/ba1/dc/8b/23a9d181608da93acb2bdbe5c636", - "assets/build/windows/Win32/Lib/concurrent/__init__.py": "https://files.ballistica.net/cache/ba1/5b/ec/08df2761a442b8ff6fe7d52fcc89", - "assets/build/windows/Win32/Lib/concurrent/futures/__init__.py": "https://files.ballistica.net/cache/ba1/c7/46/341d04b8d611753ebc06780081ea", - "assets/build/windows/Win32/Lib/concurrent/futures/_base.py": "https://files.ballistica.net/cache/ba1/8f/2e/868d89f12fd90adce6b1dec5f0f3", - "assets/build/windows/Win32/Lib/concurrent/futures/process.py": "https://files.ballistica.net/cache/ba1/0c/00/007e443d3725f007b1f406569a71", - "assets/build/windows/Win32/Lib/concurrent/futures/thread.py": "https://files.ballistica.net/cache/ba1/62/4c/93529859d652ad821a02a636a82e", - "assets/build/windows/Win32/Lib/configparser.py": "https://files.ballistica.net/cache/ba1/b6/1b/92944d1dfd0b6785ea20112266d8", - "assets/build/windows/Win32/Lib/contextlib.py": "https://files.ballistica.net/cache/ba1/38/11/8e4f422a65cadc6550232594ea6f", - "assets/build/windows/Win32/Lib/contextvars.py": "https://files.ballistica.net/cache/ba1/97/a6/19610cddd01bb44cc6f9d3a21293", - "assets/build/windows/Win32/Lib/copy.py": "https://files.ballistica.net/cache/ba1/72/18/3ed75c0be7ede66b0dc3abade794", - "assets/build/windows/Win32/Lib/copyreg.py": "https://files.ballistica.net/cache/ba1/25/ad/4e004c7ebe2b3fb9025c479b8ce3", - "assets/build/windows/Win32/Lib/crypt.py": "https://files.ballistica.net/cache/ba1/cb/33/c3831d873f0e168596fe6d6d0a6a", - "assets/build/windows/Win32/Lib/csv.py": "https://files.ballistica.net/cache/ba1/1e/ef/9976fe877f15f3933437f7d9d946", - "assets/build/windows/Win32/Lib/ctypes/__init__.py": "https://files.ballistica.net/cache/ba1/1b/69/8e9072eb16430d7ac77bb3065d8c", - "assets/build/windows/Win32/Lib/ctypes/_aix.py": "https://files.ballistica.net/cache/ba1/b9/22/2fe85ab7ebc62f2ccc9919797c99", - "assets/build/windows/Win32/Lib/ctypes/_endian.py": "https://files.ballistica.net/cache/ba1/04/c7/1775ac390854c9015be8e834ff50", - "assets/build/windows/Win32/Lib/ctypes/macholib/README.ctypes": "https://files.ballistica.net/cache/ba1/90/bf/d7c620c1dec8a9219b27e1cfa6f4", - "assets/build/windows/Win32/Lib/ctypes/macholib/__init__.py": "https://files.ballistica.net/cache/ba1/a7/68/4d72c2a8db47c671575650daa0e6", - "assets/build/windows/Win32/Lib/ctypes/macholib/dyld.py": "https://files.ballistica.net/cache/ba1/5a/b0/dbb8fe9518edf3a514f5fcdf7d09", - "assets/build/windows/Win32/Lib/ctypes/macholib/dylib.py": "https://files.ballistica.net/cache/ba1/c2/c2/547efc609d150143701b892bc5ae", - "assets/build/windows/Win32/Lib/ctypes/macholib/fetch_macholib": "https://files.ballistica.net/cache/ba1/4d/fb/26d07e6522338f7fc233734f8807", - "assets/build/windows/Win32/Lib/ctypes/macholib/fetch_macholib.bat": "https://files.ballistica.net/cache/ba1/ab/58/c056f07d65b7abf4ac2fc3598947", - "assets/build/windows/Win32/Lib/ctypes/macholib/framework.py": "https://files.ballistica.net/cache/ba1/fb/23/90116831cb2f6d105bc3cd65559c", - "assets/build/windows/Win32/Lib/ctypes/util.py": "https://files.ballistica.net/cache/ba1/8b/ca/74691e74841c822d74843c8cb1fe", - "assets/build/windows/Win32/Lib/ctypes/wintypes.py": "https://files.ballistica.net/cache/ba1/25/1d/9a7dfb9d5c76744c30ffcde3b6eb", - "assets/build/windows/Win32/Lib/curses/__init__.py": "https://files.ballistica.net/cache/ba1/83/2b/9028d278d47bc11d9ad0326f68a9", - "assets/build/windows/Win32/Lib/curses/ascii.py": "https://files.ballistica.net/cache/ba1/fd/14/86316d73c170437831841f44f410", - "assets/build/windows/Win32/Lib/curses/has_key.py": "https://files.ballistica.net/cache/ba1/39/59/8a09c722d5a9c762fe51d6bf827a", - "assets/build/windows/Win32/Lib/curses/panel.py": "https://files.ballistica.net/cache/ba1/48/9c/133d9a244f62e3739cb392d1a096", - "assets/build/windows/Win32/Lib/curses/textpad.py": "https://files.ballistica.net/cache/ba1/ea/c0/e047229f762662427d6c64dd3c61", - "assets/build/windows/Win32/Lib/dataclasses.py": "https://files.ballistica.net/cache/ba1/88/32/531d81103649f955ddfb1b364d0b", - "assets/build/windows/Win32/Lib/datetime.py": "https://files.ballistica.net/cache/ba1/7f/79/b01c809f151c7e81af2e570585bc", - "assets/build/windows/Win32/Lib/decimal.py": "https://files.ballistica.net/cache/ba1/d5/55/9ae7a36d41bfe37b2d67ce3599d0", - "assets/build/windows/Win32/Lib/difflib.py": "https://files.ballistica.net/cache/ba1/20/03/db382a894bda88eae71fd5b9e8bd", - "assets/build/windows/Win32/Lib/dis.py": "https://files.ballistica.net/cache/ba1/c7/19/7e34c76ca046e180a9040faf3dc5", - "assets/build/windows/Win32/Lib/doctest.py": "https://files.ballistica.net/cache/ba1/d9/ce/8774a0e5f472b9cdadb4df68efac", - "assets/build/windows/Win32/Lib/email/__init__.py": "https://files.ballistica.net/cache/ba1/ed/7d/64247a9b90f5c7b3f577b0e28ca0", - "assets/build/windows/Win32/Lib/email/_encoded_words.py": "https://files.ballistica.net/cache/ba1/05/81/408bbbd16a07d3c6473ff0ce523b", - "assets/build/windows/Win32/Lib/email/_header_value_parser.py": "https://files.ballistica.net/cache/ba1/79/74/8b39d9278c30a5113a6285f68c13", - "assets/build/windows/Win32/Lib/email/_parseaddr.py": "https://files.ballistica.net/cache/ba1/be/c5/2704a23f1f46c8fd23eb2a1c5281", - "assets/build/windows/Win32/Lib/email/_policybase.py": "https://files.ballistica.net/cache/ba1/06/37/302137642525762bee6ce4a09cf1", - "assets/build/windows/Win32/Lib/email/architecture.rst": "https://files.ballistica.net/cache/ba1/78/7c/c4274166d5aa06c20c2c0d391104", - "assets/build/windows/Win32/Lib/email/base64mime.py": "https://files.ballistica.net/cache/ba1/36/80/75cd2619e0d4d0ec3a478a5c92b2", - "assets/build/windows/Win32/Lib/email/charset.py": "https://files.ballistica.net/cache/ba1/85/85/2e724aa519d670805839deb3415f", - "assets/build/windows/Win32/Lib/email/contentmanager.py": "https://files.ballistica.net/cache/ba1/bf/e7/b88cafef90aa8b96d6169743b08b", - "assets/build/windows/Win32/Lib/email/encoders.py": "https://files.ballistica.net/cache/ba1/05/5e/1da72e6b33454bc00ccc75bae468", - "assets/build/windows/Win32/Lib/email/errors.py": "https://files.ballistica.net/cache/ba1/23/52/8e6ed97e557a3dd518c6bfbbea32", - "assets/build/windows/Win32/Lib/email/feedparser.py": "https://files.ballistica.net/cache/ba1/aa/d0/f54e9f077a1a3a69295932c21353", - "assets/build/windows/Win32/Lib/email/generator.py": "https://files.ballistica.net/cache/ba1/ee/1e/0cd02341e34d798770b3ac88d9d4", - "assets/build/windows/Win32/Lib/email/header.py": "https://files.ballistica.net/cache/ba1/d8/53/2ad4aea28a0f2fb1dcdbaca1d8e8", - "assets/build/windows/Win32/Lib/email/headerregistry.py": "https://files.ballistica.net/cache/ba1/28/f2/075c25ba04029da1141612cdf9ad", - "assets/build/windows/Win32/Lib/email/iterators.py": "https://files.ballistica.net/cache/ba1/90/09/e8c04371be81b7ab0a11be68784d", - "assets/build/windows/Win32/Lib/email/message.py": "https://files.ballistica.net/cache/ba1/37/dd/436fd62061b38bfdd6279d235bae", - "assets/build/windows/Win32/Lib/email/mime/__init__.py": "https://files.ballistica.net/cache/ba1/b5/ea/80f195a1c0d100480897a83a4da4", - "assets/build/windows/Win32/Lib/email/mime/application.py": "https://files.ballistica.net/cache/ba1/a6/b1/f129c2517c74d85f48087f80824d", - "assets/build/windows/Win32/Lib/email/mime/audio.py": "https://files.ballistica.net/cache/ba1/29/39/306f35115c997946975ac7c0191b", - "assets/build/windows/Win32/Lib/email/mime/base.py": "https://files.ballistica.net/cache/ba1/2b/03/f95d91c9d3b4c9c90010e58d6502", - "assets/build/windows/Win32/Lib/email/mime/image.py": "https://files.ballistica.net/cache/ba1/4e/b4/f4ac0b2fa30f88de156e3969685e", - "assets/build/windows/Win32/Lib/email/mime/message.py": "https://files.ballistica.net/cache/ba1/2a/bd/1ee232c948a87e6f1ec58a8694d8", - "assets/build/windows/Win32/Lib/email/mime/multipart.py": "https://files.ballistica.net/cache/ba1/48/c9/3beb25ea74084fd2ab4b2c86c37b", - "assets/build/windows/Win32/Lib/email/mime/nonmultipart.py": "https://files.ballistica.net/cache/ba1/0b/e2/82ca4668d9286af8b2dcffa0b6bc", - "assets/build/windows/Win32/Lib/email/mime/text.py": "https://files.ballistica.net/cache/ba1/cb/bb/02e93c2f1c2cce4b255ae7b1e482", - "assets/build/windows/Win32/Lib/email/parser.py": "https://files.ballistica.net/cache/ba1/4f/06/6bf636bb70433a9a62c41bca8406", - "assets/build/windows/Win32/Lib/email/policy.py": "https://files.ballistica.net/cache/ba1/77/b1/8a70ba209a24931675bff0a345cd", - "assets/build/windows/Win32/Lib/email/quoprimime.py": "https://files.ballistica.net/cache/ba1/76/74/92b4640edaa325ff338c5affb245", - "assets/build/windows/Win32/Lib/email/utils.py": "https://files.ballistica.net/cache/ba1/c0/1a/41f606db68e72065d7620e071970", - "assets/build/windows/Win32/Lib/encodings/__init__.py": "https://files.ballistica.net/cache/ba1/96/f3/58865e6f078884cc6e23953a5e62", - "assets/build/windows/Win32/Lib/encodings/aliases.py": "https://files.ballistica.net/cache/ba1/6b/e8/e4a9550b7bba748fef8f65526066", - "assets/build/windows/Win32/Lib/encodings/ascii.py": "https://files.ballistica.net/cache/ba1/41/44/3c51a65e96fdbbdfc71983863cf5", - "assets/build/windows/Win32/Lib/encodings/base64_codec.py": "https://files.ballistica.net/cache/ba1/f8/5d/78e448a54324be27c57960c3ef8f", - "assets/build/windows/Win32/Lib/encodings/big5.py": "https://files.ballistica.net/cache/ba1/59/23/27486e3ee84ed7e8f3dfcba5497d", - "assets/build/windows/Win32/Lib/encodings/big5hkscs.py": "https://files.ballistica.net/cache/ba1/af/6d/cc95b1377b36ea595a6e7856b257", - "assets/build/windows/Win32/Lib/encodings/bz2_codec.py": "https://files.ballistica.net/cache/ba1/a9/7a/81d7ce81b9963f1bac848daed4aa", - "assets/build/windows/Win32/Lib/encodings/charmap.py": "https://files.ballistica.net/cache/ba1/3a/c4/a547fd6bbfff9a192fcdaaf04572", - "assets/build/windows/Win32/Lib/encodings/cp037.py": "https://files.ballistica.net/cache/ba1/d0/04/e8657144c01a9e55127f528a446e", - "assets/build/windows/Win32/Lib/encodings/cp1006.py": "https://files.ballistica.net/cache/ba1/3f/da/b8d4ce156a61f2741d26467759f0", - "assets/build/windows/Win32/Lib/encodings/cp1026.py": "https://files.ballistica.net/cache/ba1/f1/5d/e4fc4dde67b14f08135293d54fa5", - "assets/build/windows/Win32/Lib/encodings/cp1125.py": "https://files.ballistica.net/cache/ba1/4d/b5/90438fa1429f755c665b3e070437", - "assets/build/windows/Win32/Lib/encodings/cp1140.py": "https://files.ballistica.net/cache/ba1/50/9f/9fb7e2a356d6ba5a1b3d149c1f19", - "assets/build/windows/Win32/Lib/encodings/cp1250.py": "https://files.ballistica.net/cache/ba1/95/ec/6f472fd71c04ac97eeec9a385096", - "assets/build/windows/Win32/Lib/encodings/cp1251.py": "https://files.ballistica.net/cache/ba1/f6/6c/742c74e44015474a31c5abce598a", - "assets/build/windows/Win32/Lib/encodings/cp1252.py": "https://files.ballistica.net/cache/ba1/f8/e1/9306eb6a5c391e481b696f09442a", - "assets/build/windows/Win32/Lib/encodings/cp1253.py": "https://files.ballistica.net/cache/ba1/fe/0e/742893c2dbfbb7ca205fbca56c68", - "assets/build/windows/Win32/Lib/encodings/cp1254.py": "https://files.ballistica.net/cache/ba1/ba/d3/0cdc819b2db190ca8d8c1243bbb1", - "assets/build/windows/Win32/Lib/encodings/cp1255.py": "https://files.ballistica.net/cache/ba1/f3/2e/93096169d97b764d23900e4c4036", - "assets/build/windows/Win32/Lib/encodings/cp1256.py": "https://files.ballistica.net/cache/ba1/b9/fe/a8d6e2a6e1e82123929122ae7bdf", - "assets/build/windows/Win32/Lib/encodings/cp1257.py": "https://files.ballistica.net/cache/ba1/11/13/1f6503b747032c8a09cb4fb52562", - "assets/build/windows/Win32/Lib/encodings/cp1258.py": "https://files.ballistica.net/cache/ba1/5c/0c/73e8f1da40cf731a0682b210f23f", - "assets/build/windows/Win32/Lib/encodings/cp273.py": "https://files.ballistica.net/cache/ba1/16/17/a651036f942e64c98c1e76237c98", - "assets/build/windows/Win32/Lib/encodings/cp424.py": "https://files.ballistica.net/cache/ba1/dd/f9/afd1ad2a6a709a2f6024854b6bf1", - "assets/build/windows/Win32/Lib/encodings/cp437.py": "https://files.ballistica.net/cache/ba1/39/01/ca310570ec583d62401b6468b6f5", - "assets/build/windows/Win32/Lib/encodings/cp500.py": "https://files.ballistica.net/cache/ba1/bb/19/e298405e000df436dee9331327cd", - "assets/build/windows/Win32/Lib/encodings/cp720.py": "https://files.ballistica.net/cache/ba1/54/06/8dbf13e0ba1e6ebb13249dfbe449", - "assets/build/windows/Win32/Lib/encodings/cp737.py": "https://files.ballistica.net/cache/ba1/56/c1/cf08397bfbac35ca9a26951af2e2", - "assets/build/windows/Win32/Lib/encodings/cp775.py": "https://files.ballistica.net/cache/ba1/d0/d4/e0a4547c5853e36e8496ebcca08f", - "assets/build/windows/Win32/Lib/encodings/cp850.py": "https://files.ballistica.net/cache/ba1/74/7d/482f2d10fd45b5481c6fd8e0d000", - "assets/build/windows/Win32/Lib/encodings/cp852.py": "https://files.ballistica.net/cache/ba1/be/ea/645638e25c84ab1eefe98a017c44", - "assets/build/windows/Win32/Lib/encodings/cp855.py": "https://files.ballistica.net/cache/ba1/be/ff/27d738b63cbdb0a088d61e469f2d", - "assets/build/windows/Win32/Lib/encodings/cp856.py": "https://files.ballistica.net/cache/ba1/70/a9/8039d96806c15cd41ee0cf3c129e", - "assets/build/windows/Win32/Lib/encodings/cp857.py": "https://files.ballistica.net/cache/ba1/e1/42/b6db24f7914142da06f25070763c", - "assets/build/windows/Win32/Lib/encodings/cp858.py": "https://files.ballistica.net/cache/ba1/2d/76/2fc64c375b5d044508f61d68f345", - "assets/build/windows/Win32/Lib/encodings/cp860.py": "https://files.ballistica.net/cache/ba1/56/46/f37d63fc3725291352dd942690af", - "assets/build/windows/Win32/Lib/encodings/cp861.py": "https://files.ballistica.net/cache/ba1/e0/78/71599103be5608aae7b418b7c1f4", - "assets/build/windows/Win32/Lib/encodings/cp862.py": "https://files.ballistica.net/cache/ba1/d7/bb/6ebbe8ad5bcbe6dccb7516b173bd", - "assets/build/windows/Win32/Lib/encodings/cp863.py": "https://files.ballistica.net/cache/ba1/61/bb/44577e96baeb76ee9634d7dd72e7", - "assets/build/windows/Win32/Lib/encodings/cp864.py": "https://files.ballistica.net/cache/ba1/1c/61/3fd4c19c2f079bb3f2f889f24214", - "assets/build/windows/Win32/Lib/encodings/cp865.py": "https://files.ballistica.net/cache/ba1/cb/1e/2808aa8d5dfeebbaa85ec6391028", - "assets/build/windows/Win32/Lib/encodings/cp866.py": "https://files.ballistica.net/cache/ba1/b5/fd/1fae6d351f9f56453af776aa3178", - "assets/build/windows/Win32/Lib/encodings/cp869.py": "https://files.ballistica.net/cache/ba1/59/1f/928020788fef916505477a1af38a", - "assets/build/windows/Win32/Lib/encodings/cp874.py": "https://files.ballistica.net/cache/ba1/c2/fb/4fb9e0253a0003f8ef3d06092c47", - "assets/build/windows/Win32/Lib/encodings/cp875.py": "https://files.ballistica.net/cache/ba1/c2/c2/5b98ded094a33895e936e51bef3c", - "assets/build/windows/Win32/Lib/encodings/cp932.py": "https://files.ballistica.net/cache/ba1/30/8a/b850c7131b835043070dd23cb448", - "assets/build/windows/Win32/Lib/encodings/cp949.py": "https://files.ballistica.net/cache/ba1/c1/f0/3125394eb5c65fd69a3727bf5d24", - "assets/build/windows/Win32/Lib/encodings/cp950.py": "https://files.ballistica.net/cache/ba1/fa/9e/18fad07c67b69a92de36307ea755", - "assets/build/windows/Win32/Lib/encodings/euc_jis_2004.py": "https://files.ballistica.net/cache/ba1/54/91/7bd52fc806dae474183e780ca737", - "assets/build/windows/Win32/Lib/encodings/euc_jisx0213.py": "https://files.ballistica.net/cache/ba1/0c/08/e3660d7e1c63519d844461bf6cc5", - "assets/build/windows/Win32/Lib/encodings/euc_jp.py": "https://files.ballistica.net/cache/ba1/cb/27/12fd7ed4f52ed310ecd698d4afa0", - "assets/build/windows/Win32/Lib/encodings/euc_kr.py": "https://files.ballistica.net/cache/ba1/27/e1/0791e94e9fbf1c775822db3203f5", - "assets/build/windows/Win32/Lib/encodings/gb18030.py": "https://files.ballistica.net/cache/ba1/55/a4/3a0baefee1510dab0a10ce7be07f", - "assets/build/windows/Win32/Lib/encodings/gb2312.py": "https://files.ballistica.net/cache/ba1/18/89/82f27a14c4e50438daf7a1b43c7a", - "assets/build/windows/Win32/Lib/encodings/gbk.py": "https://files.ballistica.net/cache/ba1/c2/e5/89158266cac7e905aec39b3a386d", - "assets/build/windows/Win32/Lib/encodings/hex_codec.py": "https://files.ballistica.net/cache/ba1/f9/f0/99096817ec50a43574f5b13324e9", - "assets/build/windows/Win32/Lib/encodings/hp_roman8.py": "https://files.ballistica.net/cache/ba1/c3/a5/f69c9ab78d48a231843c8f442ee2", - "assets/build/windows/Win32/Lib/encodings/hz.py": "https://files.ballistica.net/cache/ba1/18/42/04846c7ccfc6f1dd92bd738ef3c2", - "assets/build/windows/Win32/Lib/encodings/idna.py": "https://files.ballistica.net/cache/ba1/bb/5a/adf3a73d954cd499cda1cbaa4c8e", - "assets/build/windows/Win32/Lib/encodings/iso2022_jp.py": "https://files.ballistica.net/cache/ba1/52/d1/dfd6413d34e9a3d9828cac952498", - "assets/build/windows/Win32/Lib/encodings/iso2022_jp_1.py": "https://files.ballistica.net/cache/ba1/e3/bd/75f5029b160812d3467b1deb71a0", - "assets/build/windows/Win32/Lib/encodings/iso2022_jp_2.py": "https://files.ballistica.net/cache/ba1/86/49/e1b42c251a8a428d4d15c3582ded", - "assets/build/windows/Win32/Lib/encodings/iso2022_jp_2004.py": "https://files.ballistica.net/cache/ba1/b3/18/abab0add51e2735cae631192a108", - "assets/build/windows/Win32/Lib/encodings/iso2022_jp_3.py": "https://files.ballistica.net/cache/ba1/02/99/66ce9f543edc09ab9ee0dd365a5a", - "assets/build/windows/Win32/Lib/encodings/iso2022_jp_ext.py": "https://files.ballistica.net/cache/ba1/4a/fa/de3068d1b82201597129c96d2402", - "assets/build/windows/Win32/Lib/encodings/iso2022_kr.py": "https://files.ballistica.net/cache/ba1/c1/3f/0084a5f33f40ac249d7a7f42ba6c", - "assets/build/windows/Win32/Lib/encodings/iso8859_1.py": "https://files.ballistica.net/cache/ba1/05/cc/1be2d57d81b08ca7c45f37887990", - "assets/build/windows/Win32/Lib/encodings/iso8859_10.py": "https://files.ballistica.net/cache/ba1/53/de/825c0e81ca85801a4a487b59e649", - "assets/build/windows/Win32/Lib/encodings/iso8859_11.py": "https://files.ballistica.net/cache/ba1/3a/b7/fa02f87fcbfac1c857e6eea35be4", - "assets/build/windows/Win32/Lib/encodings/iso8859_13.py": "https://files.ballistica.net/cache/ba1/6e/67/1452d5cd3b5150504574bf82cc65", - "assets/build/windows/Win32/Lib/encodings/iso8859_14.py": "https://files.ballistica.net/cache/ba1/41/df/6d0aafe8f31df2477102ceb578c1", - "assets/build/windows/Win32/Lib/encodings/iso8859_15.py": "https://files.ballistica.net/cache/ba1/c0/32/d256cfe344d7bf1bcce4562383eb", - "assets/build/windows/Win32/Lib/encodings/iso8859_16.py": "https://files.ballistica.net/cache/ba1/21/3f/3ec08c7249fe62ea857671765761", - "assets/build/windows/Win32/Lib/encodings/iso8859_2.py": "https://files.ballistica.net/cache/ba1/13/7f/aa4831a981e2d24fc004e07c1f4b", - "assets/build/windows/Win32/Lib/encodings/iso8859_3.py": "https://files.ballistica.net/cache/ba1/d2/70/e9e11fc724bb7a5de274ba845ef5", - "assets/build/windows/Win32/Lib/encodings/iso8859_4.py": "https://files.ballistica.net/cache/ba1/36/76/8143407a078308fe11a93f08ae89", - "assets/build/windows/Win32/Lib/encodings/iso8859_5.py": "https://files.ballistica.net/cache/ba1/46/6f/563d5a496dfb2459bc5c7b08ebcd", - "assets/build/windows/Win32/Lib/encodings/iso8859_6.py": "https://files.ballistica.net/cache/ba1/71/ac/874b7e441d428cf5ae181bd5bb41", - "assets/build/windows/Win32/Lib/encodings/iso8859_7.py": "https://files.ballistica.net/cache/ba1/7c/16/8fa091513c28d0727576988ca2d1", - "assets/build/windows/Win32/Lib/encodings/iso8859_8.py": "https://files.ballistica.net/cache/ba1/70/60/c888d1f1d4347914bd689e75eb09", - "assets/build/windows/Win32/Lib/encodings/iso8859_9.py": "https://files.ballistica.net/cache/ba1/02/18/f5a7e43238cc3035d3a1fbc01fb1", - "assets/build/windows/Win32/Lib/encodings/johab.py": "https://files.ballistica.net/cache/ba1/cf/d4/034a613a3b12d7c1dbc97b439441", - "assets/build/windows/Win32/Lib/encodings/koi8_r.py": "https://files.ballistica.net/cache/ba1/72/5e/fd8aaa704c7902394cd521cad6eb", - "assets/build/windows/Win32/Lib/encodings/koi8_t.py": "https://files.ballistica.net/cache/ba1/d8/43/ec2e92a189a5c353fac8b1a016bd", - "assets/build/windows/Win32/Lib/encodings/koi8_u.py": "https://files.ballistica.net/cache/ba1/b3/59/47c7bd9e465176ba19587528a2ad", - "assets/build/windows/Win32/Lib/encodings/kz1048.py": "https://files.ballistica.net/cache/ba1/d3/5b/422fa8a73c17bb05fdff6f663827", - "assets/build/windows/Win32/Lib/encodings/latin_1.py": "https://files.ballistica.net/cache/ba1/41/45/1c7f9d33cc7a8febf0a0071808cc", - "assets/build/windows/Win32/Lib/encodings/mac_arabic.py": "https://files.ballistica.net/cache/ba1/0a/8a/ab8900ad1b09cd5faeeb6eb6b5c6", - "assets/build/windows/Win32/Lib/encodings/mac_croatian.py": "https://files.ballistica.net/cache/ba1/a5/01/ae139e112718536de88fe63f5bbf", - "assets/build/windows/Win32/Lib/encodings/mac_cyrillic.py": "https://files.ballistica.net/cache/ba1/01/84/40472ce95eaf2c04d3387a021eeb", - "assets/build/windows/Win32/Lib/encodings/mac_farsi.py": "https://files.ballistica.net/cache/ba1/04/9c/0b1bfb3fa6b4580cbc0c95f05f3d", - "assets/build/windows/Win32/Lib/encodings/mac_greek.py": "https://files.ballistica.net/cache/ba1/9c/d0/840650e734e4b7bd55c5e8322714", - "assets/build/windows/Win32/Lib/encodings/mac_iceland.py": "https://files.ballistica.net/cache/ba1/79/94/22a1bfb52d879c48afb0d7fb3960", - "assets/build/windows/Win32/Lib/encodings/mac_latin2.py": "https://files.ballistica.net/cache/ba1/e4/14/bb21b2f80597573dde201a847e4f", - "assets/build/windows/Win32/Lib/encodings/mac_roman.py": "https://files.ballistica.net/cache/ba1/ca/7f/68b9a04e47c09881a88351a7a924", - "assets/build/windows/Win32/Lib/encodings/mac_romanian.py": "https://files.ballistica.net/cache/ba1/75/9a/f10a4f64f31715a9ab7320e982f4", - "assets/build/windows/Win32/Lib/encodings/mac_turkish.py": "https://files.ballistica.net/cache/ba1/bc/e1/fd00b935519743331f28cf142b30", - "assets/build/windows/Win32/Lib/encodings/mbcs.py": "https://files.ballistica.net/cache/ba1/7f/c3/d5b7c74f6b1a2f476c0ef640c987", - "assets/build/windows/Win32/Lib/encodings/oem.py": "https://files.ballistica.net/cache/ba1/fa/02/08370f780f841c23b7a8dd8f25aa", - "assets/build/windows/Win32/Lib/encodings/palmos.py": "https://files.ballistica.net/cache/ba1/7b/fd/8cd9337594e60b0feddcf25c368d", - "assets/build/windows/Win32/Lib/encodings/ptcp154.py": "https://files.ballistica.net/cache/ba1/20/3b/47719c175fdfe43538c5e9792d24", - "assets/build/windows/Win32/Lib/encodings/punycode.py": "https://files.ballistica.net/cache/ba1/81/cf/8fe2dc639f26d7cb00ff0ce7e1ea", - "assets/build/windows/Win32/Lib/encodings/quopri_codec.py": "https://files.ballistica.net/cache/ba1/73/b5/88317f8c11128b5797b6b282b22a", - "assets/build/windows/Win32/Lib/encodings/raw_unicode_escape.py": "https://files.ballistica.net/cache/ba1/bf/25/70f8fc0d5950a6aca63d6300ce7a", - "assets/build/windows/Win32/Lib/encodings/rot_13.py": "https://files.ballistica.net/cache/ba1/a9/86/d0e282a103b6005c7eba393c2865", - "assets/build/windows/Win32/Lib/encodings/shift_jis.py": "https://files.ballistica.net/cache/ba1/ba/2c/2ef82e17969f3b47e0dfe36f8439", - "assets/build/windows/Win32/Lib/encodings/shift_jis_2004.py": "https://files.ballistica.net/cache/ba1/e4/7f/191f32888ecc24da183a30be5976", - "assets/build/windows/Win32/Lib/encodings/shift_jisx0213.py": "https://files.ballistica.net/cache/ba1/9d/3e/acee612d961a29508e4fc5405d9d", - "assets/build/windows/Win32/Lib/encodings/tis_620.py": "https://files.ballistica.net/cache/ba1/11/e1/d45a248d14f218f1f4b35b46f949", - "assets/build/windows/Win32/Lib/encodings/undefined.py": "https://files.ballistica.net/cache/ba1/31/c6/571a6f1a9c7aa0d26e721ba4fc65", - "assets/build/windows/Win32/Lib/encodings/unicode_escape.py": "https://files.ballistica.net/cache/ba1/e7/2e/af7cee329e2d8823ef86c081d241", - "assets/build/windows/Win32/Lib/encodings/utf_16.py": "https://files.ballistica.net/cache/ba1/2c/d5/a7818c23518a9bb340183dd05c1d", - "assets/build/windows/Win32/Lib/encodings/utf_16_be.py": "https://files.ballistica.net/cache/ba1/7f/2c/d6f996bcb15cda0a566bd3517239", - "assets/build/windows/Win32/Lib/encodings/utf_16_le.py": "https://files.ballistica.net/cache/ba1/8b/92/2014625fc6fc4012468d4fcc1551", - "assets/build/windows/Win32/Lib/encodings/utf_32.py": "https://files.ballistica.net/cache/ba1/71/bb/dd68363e86733e927523d710ba8c", - "assets/build/windows/Win32/Lib/encodings/utf_32_be.py": "https://files.ballistica.net/cache/ba1/1c/87/c323363c9f47092c236783f2e0e3", - "assets/build/windows/Win32/Lib/encodings/utf_32_le.py": "https://files.ballistica.net/cache/ba1/38/df/5b501debcfe2f050060ffe14b70c", - "assets/build/windows/Win32/Lib/encodings/utf_7.py": "https://files.ballistica.net/cache/ba1/fe/2c/cc363eceaec22d46829d0b3748e5", - "assets/build/windows/Win32/Lib/encodings/utf_8.py": "https://files.ballistica.net/cache/ba1/82/0e/a8bd3ac0e209ec22903f36c7e743", - "assets/build/windows/Win32/Lib/encodings/utf_8_sig.py": "https://files.ballistica.net/cache/ba1/02/57/abf1662be43acd806d712d09ad92", - "assets/build/windows/Win32/Lib/encodings/uu_codec.py": "https://files.ballistica.net/cache/ba1/ca/1c/8b4574b02be8387d5b8818d1399d", - "assets/build/windows/Win32/Lib/encodings/zlib_codec.py": "https://files.ballistica.net/cache/ba1/ca/b7/a919c6be178102f90d97879e61ec", - "assets/build/windows/Win32/Lib/enum.py": "https://files.ballistica.net/cache/ba1/36/83/c096089b8f0c196e577b35e8f451", - "assets/build/windows/Win32/Lib/filecmp.py": "https://files.ballistica.net/cache/ba1/a8/88/df1eac15604e399603d3d727582c", - "assets/build/windows/Win32/Lib/fileinput.py": "https://files.ballistica.net/cache/ba1/97/e8/c39a202550652cabe1c519def2f8", - "assets/build/windows/Win32/Lib/fnmatch.py": "https://files.ballistica.net/cache/ba1/e9/b4/9afa7e0e63d5fc0cba43ee03189b", - "assets/build/windows/Win32/Lib/fractions.py": "https://files.ballistica.net/cache/ba1/1e/f4/5b5b01442b7363b61791b580272a", - "assets/build/windows/Win32/Lib/ftplib.py": "https://files.ballistica.net/cache/ba1/cd/ab/0156861df1e160c17b79454c7b72", - "assets/build/windows/Win32/Lib/functools.py": "https://files.ballistica.net/cache/ba1/57/b6/8817d2f94b5bf3ee6d612d0e041e", - "assets/build/windows/Win32/Lib/genericpath.py": "https://files.ballistica.net/cache/ba1/51/fc/1a323017e8b06cdc6327c524fe66", - "assets/build/windows/Win32/Lib/getopt.py": "https://files.ballistica.net/cache/ba1/c8/63/dfdbb68d2e67c3bae4f9dcc3f0f5", - "assets/build/windows/Win32/Lib/getpass.py": "https://files.ballistica.net/cache/ba1/72/31/d1e384b10905ca750c1ed8c0b84a", - "assets/build/windows/Win32/Lib/gettext.py": "https://files.ballistica.net/cache/ba1/0e/42/9e44277c3e9347fa5401959e3705", - "assets/build/windows/Win32/Lib/glob.py": "https://files.ballistica.net/cache/ba1/d4/88/2bc8a7585f8318dcaf0844d6a4ca", - "assets/build/windows/Win32/Lib/graphlib.py": "https://files.ballistica.net/cache/ba1/30/11/196fba08556068f7510a3ad1c219", - "assets/build/windows/Win32/Lib/gzip.py": "https://files.ballistica.net/cache/ba1/4f/b5/7c327e425a1451e3acb485b5bcdd", - "assets/build/windows/Win32/Lib/hashlib.py": "https://files.ballistica.net/cache/ba1/bf/28/ae3367a32bd1e8647c19cad6a4d9", - "assets/build/windows/Win32/Lib/heapq.py": "https://files.ballistica.net/cache/ba1/34/2f/f8ff15aedf39c36b2f6412af45fe", - "assets/build/windows/Win32/Lib/hmac.py": "https://files.ballistica.net/cache/ba1/47/30/7c82906f5f048a804b39f0c52bfd", - "assets/build/windows/Win32/Lib/html/__init__.py": "https://files.ballistica.net/cache/ba1/8c/08/c638db74e5e5979dea109da1f68b", - "assets/build/windows/Win32/Lib/html/entities.py": "https://files.ballistica.net/cache/ba1/52/69/e7311caea2fbfdfef9c05515de4b", - "assets/build/windows/Win32/Lib/html/parser.py": "https://files.ballistica.net/cache/ba1/67/69/0bd4c96809a146bac423dd581aa5", - "assets/build/windows/Win32/Lib/http/__init__.py": "https://files.ballistica.net/cache/ba1/e2/22/904f7ffdc8017e302b097e234176", - "assets/build/windows/Win32/Lib/http/client.py": "https://files.ballistica.net/cache/ba1/9a/3a/b80c43bcb3cecbb16f5247c25717", - "assets/build/windows/Win32/Lib/http/cookiejar.py": "https://files.ballistica.net/cache/ba1/93/f5/30a584d2a12a2bd549de1876c729", - "assets/build/windows/Win32/Lib/http/cookies.py": "https://files.ballistica.net/cache/ba1/4a/49/a309c5a835f284a508f1a621ba61", - "assets/build/windows/Win32/Lib/http/server.py": "https://files.ballistica.net/cache/ba1/cd/da/73228acde2ebcc27110681656a52", - "assets/build/windows/Win32/Lib/imghdr.py": "https://files.ballistica.net/cache/ba1/a8/49/6afa9e88ee446fadd95254fc159d", - "assets/build/windows/Win32/Lib/imp.py": "https://files.ballistica.net/cache/ba1/ea/42/6ea1c4f2b5a5b1991072d07abf70", - "assets/build/windows/Win32/Lib/importlib/__init__.py": "https://files.ballistica.net/cache/ba1/45/29/a30575bd78441793ea479465b11a", - "assets/build/windows/Win32/Lib/importlib/_abc.py": "https://files.ballistica.net/cache/ba1/58/51/1c5b3083b96dcc01824280485089", - "assets/build/windows/Win32/Lib/importlib/_adapters.py": "https://files.ballistica.net/cache/ba1/2d/32/6b02e2f92ee2ac8061f51e2f0981", - "assets/build/windows/Win32/Lib/importlib/_bootstrap.py": "https://files.ballistica.net/cache/ba1/80/ac/cf20977149c77ce7117f9bc9f0a1", - "assets/build/windows/Win32/Lib/importlib/_bootstrap_external.py": "https://files.ballistica.net/cache/ba1/4c/fd/6d4c0838e760815c1738d2608ee2", - "assets/build/windows/Win32/Lib/importlib/_common.py": "https://files.ballistica.net/cache/ba1/39/11/98d16202977d90980fa30a9a5e97", - "assets/build/windows/Win32/Lib/importlib/abc.py": "https://files.ballistica.net/cache/ba1/c3/bd/4decc4d69fa369b74808d5b07e32", - "assets/build/windows/Win32/Lib/importlib/machinery.py": "https://files.ballistica.net/cache/ba1/33/24/8dd2949980782fc0f6a277132afd", - "assets/build/windows/Win32/Lib/importlib/metadata/__init__.py": "https://files.ballistica.net/cache/ba1/15/9a/59e2ffc03543582dae8ebdd423cd", - "assets/build/windows/Win32/Lib/importlib/metadata/_adapters.py": "https://files.ballistica.net/cache/ba1/0d/d7/a20a69f98708485c5e290b3aa88f", - "assets/build/windows/Win32/Lib/importlib/metadata/_collections.py": "https://files.ballistica.net/cache/ba1/aa/1d/2b82eda6cd6738839fe5910874f4", - "assets/build/windows/Win32/Lib/importlib/metadata/_functools.py": "https://files.ballistica.net/cache/ba1/5b/e4/be18f8b33e21622d01ea79926af9", - "assets/build/windows/Win32/Lib/importlib/metadata/_itertools.py": "https://files.ballistica.net/cache/ba1/3e/ca/d3bab2a64aa9665fb66a848e2d9a", - "assets/build/windows/Win32/Lib/importlib/metadata/_meta.py": "https://files.ballistica.net/cache/ba1/7f/08/f32de9a3a65b9920118e4a7830b9", - "assets/build/windows/Win32/Lib/importlib/metadata/_text.py": "https://files.ballistica.net/cache/ba1/0d/0f/1466972f4195b28ea9f57c2f8a8e", - "assets/build/windows/Win32/Lib/importlib/readers.py": "https://files.ballistica.net/cache/ba1/88/25/67ba1bd9663133a4d8fb00e266ce", - "assets/build/windows/Win32/Lib/importlib/resources.py": "https://files.ballistica.net/cache/ba1/8e/1b/2a9e9e4c1a7bab7daf8a535c5797", - "assets/build/windows/Win32/Lib/importlib/util.py": "https://files.ballistica.net/cache/ba1/bb/27/11df8cc40b948582250e429eb726", - "assets/build/windows/Win32/Lib/inspect.py": "https://files.ballistica.net/cache/ba1/6d/bd/0cfec2a6769a7cf93f058053f0df", - "assets/build/windows/Win32/Lib/io.py": "https://files.ballistica.net/cache/ba1/bc/81/445d2700a1ceef23a4e345493f6d", - "assets/build/windows/Win32/Lib/ipaddress.py": "https://files.ballistica.net/cache/ba1/70/b4/678515d2cf181fae5bcf00baf2d6", - "assets/build/windows/Win32/Lib/json/__init__.py": "https://files.ballistica.net/cache/ba1/a5/fb/b9112182b4acc08872d7420ffb12", - "assets/build/windows/Win32/Lib/json/decoder.py": "https://files.ballistica.net/cache/ba1/f5/44/19f6e70ef50bed1f318027bbf9aa", - "assets/build/windows/Win32/Lib/json/encoder.py": "https://files.ballistica.net/cache/ba1/fd/09/c8714243af885e6315349b999bde", - "assets/build/windows/Win32/Lib/json/scanner.py": "https://files.ballistica.net/cache/ba1/a7/9e/0fdf34c72293733a58f0dd2677fa", - "assets/build/windows/Win32/Lib/json/tool.py": "https://files.ballistica.net/cache/ba1/84/94/1f619ad14ffcfc52382c2004766b", - "assets/build/windows/Win32/Lib/keyword.py": "https://files.ballistica.net/cache/ba1/aa/66/332e4147e0ebe89ae70b5c6f3f9b", - "assets/build/windows/Win32/Lib/linecache.py": "https://files.ballistica.net/cache/ba1/87/6d/a4551bb2aee3db12040deba09949", - "assets/build/windows/Win32/Lib/locale.py": "https://files.ballistica.net/cache/ba1/dc/0c/d7563e1b3b46ae22755f32541dd9", - "assets/build/windows/Win32/Lib/logging/__init__.py": "https://files.ballistica.net/cache/ba1/0a/18/14a20443809edb4240922a3523f2", - "assets/build/windows/Win32/Lib/logging/config.py": "https://files.ballistica.net/cache/ba1/8f/9b/1a32fbbb9f7975352c0a0179d1c7", - "assets/build/windows/Win32/Lib/logging/handlers.py": "https://files.ballistica.net/cache/ba1/f1/cd/b811bfa5a0cac11b0baf23213cdf", - "assets/build/windows/Win32/Lib/lzma.py": "https://files.ballistica.net/cache/ba1/4c/6f/d2797205aaaf73cbe7a88ecb96ac", - "assets/build/windows/Win32/Lib/mailbox.py": "https://files.ballistica.net/cache/ba1/f5/47/ec4fe120a31bc22ee4160ee2b628", - "assets/build/windows/Win32/Lib/mailcap.py": "https://files.ballistica.net/cache/ba1/39/3c/ba855852479d4b38bff59e22f7c9", - "assets/build/windows/Win32/Lib/mimetypes.py": "https://files.ballistica.net/cache/ba1/b8/a7/d61ea8d3f5680952183a7df517b5", - "assets/build/windows/Win32/Lib/modulefinder.py": "https://files.ballistica.net/cache/ba1/63/85/9cd35229337868fce8bd0456d505", - "assets/build/windows/Win32/Lib/msilib/__init__.py": "https://files.ballistica.net/cache/ba1/54/95/b1f45d5d58bdb568be4fb1c9d7ad", - "assets/build/windows/Win32/Lib/msilib/schema.py": "https://files.ballistica.net/cache/ba1/0f/8f/2699306b3baf09d04fabd1b9a5a6", - "assets/build/windows/Win32/Lib/msilib/sequence.py": "https://files.ballistica.net/cache/ba1/5e/c4/a7a6140c298325ee69a8958b40d6", - "assets/build/windows/Win32/Lib/msilib/text.py": "https://files.ballistica.net/cache/ba1/49/b2/15871bd0d47d347c9371490eb35f", - "assets/build/windows/Win32/Lib/netrc.py": "https://files.ballistica.net/cache/ba1/9b/36/42c141a9cf55a8e33ca539b6e4fa", - "assets/build/windows/Win32/Lib/nntplib.py": "https://files.ballistica.net/cache/ba1/84/ff/9ca203896a4c7f75d09bbbb49f23", - "assets/build/windows/Win32/Lib/ntpath.py": "https://files.ballistica.net/cache/ba1/88/02/6db850090c7d53304ce5214f9e49", - "assets/build/windows/Win32/Lib/nturl2path.py": "https://files.ballistica.net/cache/ba1/6c/d4/b0ad03a05c03e8a672239a8d3c22", - "assets/build/windows/Win32/Lib/numbers.py": "https://files.ballistica.net/cache/ba1/4c/14/0c9d8a9af8c19d9639c4bf46470f", - "assets/build/windows/Win32/Lib/opcode.py": "https://files.ballistica.net/cache/ba1/e1/62/314d6e7237bbacf56d8a2ec3c8d8", - "assets/build/windows/Win32/Lib/operator.py": "https://files.ballistica.net/cache/ba1/62/02/2ae2e6306970768ece225d61e8f6", - "assets/build/windows/Win32/Lib/optparse.py": "https://files.ballistica.net/cache/ba1/08/d8/10bbc27b50836a1329ca9a8c76d9", - "assets/build/windows/Win32/Lib/os.py": "https://files.ballistica.net/cache/ba1/0b/04/2653ad211134b99ecd0c2f79dd87", - "assets/build/windows/Win32/Lib/pathlib.py": "https://files.ballistica.net/cache/ba1/eb/2d/64eeec552be0e216df1268c6f781", - "assets/build/windows/Win32/Lib/pdb.py": "https://files.ballistica.net/cache/ba1/33/89/c22c604d4cbdb6b855fa6b1bd46b", - "assets/build/windows/Win32/Lib/pickle.py": "https://files.ballistica.net/cache/ba1/df/68/587598ccfbcd3e351673b2ea183d", - "assets/build/windows/Win32/Lib/pickletools.py": "https://files.ballistica.net/cache/ba1/2e/45/eddff9ff648e35265541ce3296aa", - "assets/build/windows/Win32/Lib/pipes.py": "https://files.ballistica.net/cache/ba1/54/90/bf2ecb88ae5afd28847f16ed6101", - "assets/build/windows/Win32/Lib/pkgutil.py": "https://files.ballistica.net/cache/ba1/d1/cc/459a67b674171371912b6a80f496", - "assets/build/windows/Win32/Lib/platform.py": "https://files.ballistica.net/cache/ba1/c5/23/3b33655542952865393804cf2016", - "assets/build/windows/Win32/Lib/plistlib.py": "https://files.ballistica.net/cache/ba1/6a/e8/4d5c747e45f582db11ffa511d651", - "assets/build/windows/Win32/Lib/poplib.py": "https://files.ballistica.net/cache/ba1/68/71/845ce32e039b1d7c9fe9dc07e059", - "assets/build/windows/Win32/Lib/posixpath.py": "https://files.ballistica.net/cache/ba1/73/6b/d62b11159bb9f5d29bb14e66c930", - "assets/build/windows/Win32/Lib/pprint.py": "https://files.ballistica.net/cache/ba1/fd/d3/c9a25f6c135851e21f6105d344e8", - "assets/build/windows/Win32/Lib/profile.py": "https://files.ballistica.net/cache/ba1/27/f7/fecf77603def6be7e272ecf879b1", - "assets/build/windows/Win32/Lib/pstats.py": "https://files.ballistica.net/cache/ba1/8e/3e/2e0ba82e95a5ea276ade31824e46", - "assets/build/windows/Win32/Lib/pty.py": "https://files.ballistica.net/cache/ba1/51/f8/072c0d92f609d48cff2292081b87", - "assets/build/windows/Win32/Lib/py_compile.py": "https://files.ballistica.net/cache/ba1/35/2b/4363fd179d3492fcddc0c18931cf", - "assets/build/windows/Win32/Lib/pyclbr.py": "https://files.ballistica.net/cache/ba1/a9/d0/e9373e0037303c06dc42c75c548a", - "assets/build/windows/Win32/Lib/pydoc.py": "https://files.ballistica.net/cache/ba1/1b/99/ed86db97f223466a82588ac1805f", - "assets/build/windows/Win32/Lib/queue.py": "https://files.ballistica.net/cache/ba1/ad/d9/dccb193c1f5b918c874573949a7c", - "assets/build/windows/Win32/Lib/quopri.py": "https://files.ballistica.net/cache/ba1/01/61/77a2bd9e234c35d4e5e3dca41474", - "assets/build/windows/Win32/Lib/random.py": "https://files.ballistica.net/cache/ba1/9d/98/687d246e31ca3eed473ceb61f6c7", - "assets/build/windows/Win32/Lib/re.py": "https://files.ballistica.net/cache/ba1/d1/6c/b6e9c875a7e34eb1d040af7d0a9e", - "assets/build/windows/Win32/Lib/reprlib.py": "https://files.ballistica.net/cache/ba1/a8/ba/d93d9b48a6c8e84421dc29a7a356", - "assets/build/windows/Win32/Lib/rlcompleter.py": "https://files.ballistica.net/cache/ba1/bc/73/496e8c60f4cb9a5faaca4e1dc03e", - "assets/build/windows/Win32/Lib/runpy.py": "https://files.ballistica.net/cache/ba1/b1/bd/d09b34c85e459b14b4afc8c0511c", - "assets/build/windows/Win32/Lib/sched.py": "https://files.ballistica.net/cache/ba1/4b/63/7bd55e4f1bee9ac30a99dabec23f", - "assets/build/windows/Win32/Lib/secrets.py": "https://files.ballistica.net/cache/ba1/3a/08/35ad5fa7b2674b557d6877856e57", - "assets/build/windows/Win32/Lib/selectors.py": "https://files.ballistica.net/cache/ba1/e7/e0/5974f4d22db37ed06a4a88c3982f", - "assets/build/windows/Win32/Lib/shelve.py": "https://files.ballistica.net/cache/ba1/c6/0d/cbdd81146ddf9b5ed9473eb451a6", - "assets/build/windows/Win32/Lib/shlex.py": "https://files.ballistica.net/cache/ba1/8b/f0/0626cd9774fc74dbdd57171b6bd0", - "assets/build/windows/Win32/Lib/shutil.py": "https://files.ballistica.net/cache/ba1/95/b7/545514d65303f0528fb8314fbaf4", - "assets/build/windows/Win32/Lib/signal.py": "https://files.ballistica.net/cache/ba1/b1/a4/133c77450fbeea335140ad2c0fa9", - "assets/build/windows/Win32/Lib/site.py": "https://files.ballistica.net/cache/ba1/c1/1a/0ea3d0b25ef32da156325786184c", - "assets/build/windows/Win32/Lib/smtpd.py": "https://files.ballistica.net/cache/ba1/c6/1b/6bc1d115fcf64bc74c500f0fd1a1", - "assets/build/windows/Win32/Lib/smtplib.py": "https://files.ballistica.net/cache/ba1/fc/2d/ab49bf28843b86688d235fbbfda1", - "assets/build/windows/Win32/Lib/sndhdr.py": "https://files.ballistica.net/cache/ba1/48/bd/cf83c27bc7f72e2c5b5d5497aeb3", - "assets/build/windows/Win32/Lib/socket.py": "https://files.ballistica.net/cache/ba1/d1/81/c54ab46bf90b4e2b9c9807b565e1", - "assets/build/windows/Win32/Lib/socketserver.py": "https://files.ballistica.net/cache/ba1/c6/b6/45fd1ececd3b4272f1f0674ce05c", - "assets/build/windows/Win32/Lib/sqlite3/__init__.py": "https://files.ballistica.net/cache/ba1/41/76/dcb7c8db6e13c942c164e3549b1e", - "assets/build/windows/Win32/Lib/sqlite3/dbapi2.py": "https://files.ballistica.net/cache/ba1/79/5f/4ca5b9628e35c49e13d1b0d5d895", - "assets/build/windows/Win32/Lib/sqlite3/dump.py": "https://files.ballistica.net/cache/ba1/25/0e/52f5b1972488978dae3361460bec", - "assets/build/windows/Win32/Lib/sre_compile.py": "https://files.ballistica.net/cache/ba1/ad/7b/8ab12949ef2e5f8da457fac53624", - "assets/build/windows/Win32/Lib/sre_constants.py": "https://files.ballistica.net/cache/ba1/5e/cd/1e06950ce526150b57e13943b117", - "assets/build/windows/Win32/Lib/sre_parse.py": "https://files.ballistica.net/cache/ba1/68/33/658802a2b2c4c3345ad942a739c4", - "assets/build/windows/Win32/Lib/ssl.py": "https://files.ballistica.net/cache/ba1/4c/9e/2f733a06f99c2c7b561e53ec9f64", - "assets/build/windows/Win32/Lib/stat.py": "https://files.ballistica.net/cache/ba1/f5/ba/9a795715bfa80417f6b90fc4d5eb", - "assets/build/windows/Win32/Lib/statistics.py": "https://files.ballistica.net/cache/ba1/35/38/2c54e929340a175416c085060705", - "assets/build/windows/Win32/Lib/string.py": "https://files.ballistica.net/cache/ba1/e7/b7/56d0ff953973eb37ef38328f75e2", - "assets/build/windows/Win32/Lib/stringprep.py": "https://files.ballistica.net/cache/ba1/5b/42/39b8e69a2979e4f553bb6908b545", - "assets/build/windows/Win32/Lib/struct.py": "https://files.ballistica.net/cache/ba1/8e/da/3a3bdc58e197050b1906c1c86266", - "assets/build/windows/Win32/Lib/subprocess.py": "https://files.ballistica.net/cache/ba1/d6/5e/d24995fabaea4b7bea9595054b7d", - "assets/build/windows/Win32/Lib/sunau.py": "https://files.ballistica.net/cache/ba1/c5/ea/8efd34c174ebbf6e721eb2c4854b", - "assets/build/windows/Win32/Lib/symtable.py": "https://files.ballistica.net/cache/ba1/48/8e/754cef371072cae03b71059bbc55", - "assets/build/windows/Win32/Lib/sysconfig.py": "https://files.ballistica.net/cache/ba1/1e/16/a562edf0bab3eb205bc86d17c4d2", - "assets/build/windows/Win32/Lib/tabnanny.py": "https://files.ballistica.net/cache/ba1/f0/6a/00c300ad5404f3bbbe4fe723a466", - "assets/build/windows/Win32/Lib/tarfile.py": "https://files.ballistica.net/cache/ba1/3a/c4/6947caf4273d79405a6ae2165f5a", - "assets/build/windows/Win32/Lib/telnetlib.py": "https://files.ballistica.net/cache/ba1/29/35/bdff1814c1cdba6b4ae5cb2f8675", - "assets/build/windows/Win32/Lib/tempfile.py": "https://files.ballistica.net/cache/ba1/7d/e4/ee39de062784f8f9b1de87ea4908", - "assets/build/windows/Win32/Lib/textwrap.py": "https://files.ballistica.net/cache/ba1/6a/a9/b4ab6d566de6e345fe9ee847b9d2", - "assets/build/windows/Win32/Lib/this.py": "https://files.ballistica.net/cache/ba1/b0/f9/1eb227ba1d4d069da408b12e8312", - "assets/build/windows/Win32/Lib/threading.py": "https://files.ballistica.net/cache/ba1/ca/76/e6266096e3480843c67bf9b625b6", - "assets/build/windows/Win32/Lib/timeit.py": "https://files.ballistica.net/cache/ba1/70/79/0180b0c63115665fd121b5b77be4", - "assets/build/windows/Win32/Lib/token.py": "https://files.ballistica.net/cache/ba1/c1/50/44f912d10d22b4881917803bdf8e", - "assets/build/windows/Win32/Lib/tokenize.py": "https://files.ballistica.net/cache/ba1/3d/92/6c659e32eec0aca98e10f253c4f8", - "assets/build/windows/Win32/Lib/trace.py": "https://files.ballistica.net/cache/ba1/fb/73/bd58d56bde10b7865935dbf5835d", - "assets/build/windows/Win32/Lib/traceback.py": "https://files.ballistica.net/cache/ba1/aa/76/d6f3a5c0fc4d1337ebdbc39b95ce", - "assets/build/windows/Win32/Lib/tracemalloc.py": "https://files.ballistica.net/cache/ba1/18/0d/5faf4c360c505d08b7613f5417d7", - "assets/build/windows/Win32/Lib/tty.py": "https://files.ballistica.net/cache/ba1/66/b3/bb1684cec763502fdf418909eedc", - "assets/build/windows/Win32/Lib/types.py": "https://files.ballistica.net/cache/ba1/72/fa/2bac246211c89d02a96046cd38f4", - "assets/build/windows/Win32/Lib/typing.py": "https://files.ballistica.net/cache/ba1/8c/34/7c8d2ce62806892002ae807c9a26", - "assets/build/windows/Win32/Lib/urllib/__init__.py": "https://files.ballistica.net/cache/ba1/48/ca/f840c02dd0e7222236a872a7f278", - "assets/build/windows/Win32/Lib/urllib/error.py": "https://files.ballistica.net/cache/ba1/c9/c8/9d133fc217803023dff6faed8681", - "assets/build/windows/Win32/Lib/urllib/parse.py": "https://files.ballistica.net/cache/ba1/19/6a/66026aaca0d3e1df3de0262226e7", - "assets/build/windows/Win32/Lib/urllib/request.py": "https://files.ballistica.net/cache/ba1/8c/86/cf629e2ea35ba525744ccff3918a", - "assets/build/windows/Win32/Lib/urllib/response.py": "https://files.ballistica.net/cache/ba1/30/32/b589f1002cf402e632315e473447", - "assets/build/windows/Win32/Lib/urllib/robotparser.py": "https://files.ballistica.net/cache/ba1/ef/f6/c7fd5ad82a88e56463f9d1c1b8d1", - "assets/build/windows/Win32/Lib/uu.py": "https://files.ballistica.net/cache/ba1/40/20/7ae03397a7bf1f182a9e5463047f", - "assets/build/windows/Win32/Lib/uuid.py": "https://files.ballistica.net/cache/ba1/15/d4/3cbe0a49dbc49c4579f174635806", - "assets/build/windows/Win32/Lib/warnings.py": "https://files.ballistica.net/cache/ba1/4f/c2/55e364e63a35cfada42a1c0b9504", - "assets/build/windows/Win32/Lib/wave.py": "https://files.ballistica.net/cache/ba1/86/67/3fc7f739b871c1d3f9e479a8c4f5", - "assets/build/windows/Win32/Lib/weakref.py": "https://files.ballistica.net/cache/ba1/ea/d2/894a2e873428b59c4fba411773e4", - "assets/build/windows/Win32/Lib/webbrowser.py": "https://files.ballistica.net/cache/ba1/58/5c/1dbfc5ed3f73cac0f1e5b40cf3c4", - "assets/build/windows/Win32/Lib/xdrlib.py": "https://files.ballistica.net/cache/ba1/35/a6/f6e14e4fb6beae1f68ec2a4be852", - "assets/build/windows/Win32/Lib/xml/__init__.py": "https://files.ballistica.net/cache/ba1/02/9f/a002b4cb540d2e1c2b68edb44b3c", - "assets/build/windows/Win32/Lib/xml/dom/NodeFilter.py": "https://files.ballistica.net/cache/ba1/70/fb/e0e98f50e21c494e53869d4d7d32", - "assets/build/windows/Win32/Lib/xml/dom/__init__.py": "https://files.ballistica.net/cache/ba1/da/c6/e03fc8651c8453f97f28b59c39e6", - "assets/build/windows/Win32/Lib/xml/dom/domreg.py": "https://files.ballistica.net/cache/ba1/24/0b/7c0c234f022ab835c9de2ebeae0b", - "assets/build/windows/Win32/Lib/xml/dom/expatbuilder.py": "https://files.ballistica.net/cache/ba1/b7/72/a2efe64e9d3d6d69be97b30e7bff", - "assets/build/windows/Win32/Lib/xml/dom/minicompat.py": "https://files.ballistica.net/cache/ba1/00/0c/03191b6f36ce83e51030448fae74", - "assets/build/windows/Win32/Lib/xml/dom/minidom.py": "https://files.ballistica.net/cache/ba1/c3/90/41746421ae51e1a26eb6c6baac49", - "assets/build/windows/Win32/Lib/xml/dom/pulldom.py": "https://files.ballistica.net/cache/ba1/de/17/036dd0486e71bd9257e4d083b21b", - "assets/build/windows/Win32/Lib/xml/dom/xmlbuilder.py": "https://files.ballistica.net/cache/ba1/ec/9e/09de652d2787b926de856d50485e", - "assets/build/windows/Win32/Lib/xml/etree/ElementInclude.py": "https://files.ballistica.net/cache/ba1/f7/70/75a58d2a3125230578bc521d3994", - "assets/build/windows/Win32/Lib/xml/etree/ElementPath.py": "https://files.ballistica.net/cache/ba1/0f/2f/991723cfc14290b2fef87324ce08", - "assets/build/windows/Win32/Lib/xml/etree/ElementTree.py": "https://files.ballistica.net/cache/ba1/fc/e3/bae0fb7c91232656adf53d1c6971", - "assets/build/windows/Win32/Lib/xml/etree/__init__.py": "https://files.ballistica.net/cache/ba1/27/f3/261a335507cb7fee52f2afb65af3", - "assets/build/windows/Win32/Lib/xml/etree/cElementTree.py": "https://files.ballistica.net/cache/ba1/50/3c/23d2072448e974423bb11e396ab6", - "assets/build/windows/Win32/Lib/xml/parsers/__init__.py": "https://files.ballistica.net/cache/ba1/19/bf/5493270c481d02ed233d2119653c", - "assets/build/windows/Win32/Lib/xml/parsers/expat.py": "https://files.ballistica.net/cache/ba1/6e/f1/a6aad9b9297dd20b5c24a224eb87", - "assets/build/windows/Win32/Lib/xml/sax/__init__.py": "https://files.ballistica.net/cache/ba1/5d/e2/7b4012ae334633fd73e6f15735a8", - "assets/build/windows/Win32/Lib/xml/sax/_exceptions.py": "https://files.ballistica.net/cache/ba1/47/64/9b8c6da80774cf676c116c42c779", - "assets/build/windows/Win32/Lib/xml/sax/expatreader.py": "https://files.ballistica.net/cache/ba1/75/23/e1f38824e9a56ce646de9ccc92a2", - "assets/build/windows/Win32/Lib/xml/sax/handler.py": "https://files.ballistica.net/cache/ba1/f3/af/49a619fedab9020bc4e13a91452d", - "assets/build/windows/Win32/Lib/xml/sax/saxutils.py": "https://files.ballistica.net/cache/ba1/b8/1c/2ca4b0f80078729b5e76b6d0250b", - "assets/build/windows/Win32/Lib/xml/sax/xmlreader.py": "https://files.ballistica.net/cache/ba1/05/f9/eeb1c32818d63991e806451dad45", - "assets/build/windows/Win32/Lib/xmlrpc/__init__.py": "https://files.ballistica.net/cache/ba1/84/79/19c277eda21caa83389ff1c2258f", - "assets/build/windows/Win32/Lib/xmlrpc/client.py": "https://files.ballistica.net/cache/ba1/fa/41/52ab64dfb57c001c9bd647a0509a", - "assets/build/windows/Win32/Lib/xmlrpc/server.py": "https://files.ballistica.net/cache/ba1/25/9c/bb429d6083ec5e53cfe18d7eb955", - "assets/build/windows/Win32/Lib/zipapp.py": "https://files.ballistica.net/cache/ba1/92/65/287c3d27f14ea2956622e136f71b", - "assets/build/windows/Win32/Lib/zipfile.py": "https://files.ballistica.net/cache/ba1/89/e8/10b4ee21b1262dffb996dcc48806", - "assets/build/windows/Win32/Lib/zipimport.py": "https://files.ballistica.net/cache/ba1/5d/50/cf06beb082b87fa337bb17462887", - "assets/build/windows/Win32/Lib/zoneinfo/__init__.py": "https://files.ballistica.net/cache/ba1/2d/ff/48364d3203172aef5248f5645e6f", - "assets/build/windows/Win32/Lib/zoneinfo/_common.py": "https://files.ballistica.net/cache/ba1/9e/30/8ad12b05547e9099933346f25c32", - "assets/build/windows/Win32/Lib/zoneinfo/_tzpath.py": "https://files.ballistica.net/cache/ba1/d3/be/2eed03b48558c92499af86cd48a8", - "assets/build/windows/Win32/Lib/zoneinfo/_zoneinfo.py": "https://files.ballistica.net/cache/ba1/e3/c4/139b66cf700ff6e81d25cfc5cb43", - "assets/build/windows/Win32/OpenAL32.dll": "https://files.ballistica.net/cache/ba1/7b/0f/4349781f2e7ea0ced321fd0b9c45", - "assets/build/windows/Win32/SDL2.dll": "https://files.ballistica.net/cache/ba1/c5/7d/e8943b5eda2472a308b63f938b1c", - "assets/build/windows/Win32/libvorbis.dll": "https://files.ballistica.net/cache/ba1/1b/d1/75cc9eb21373659c8baacbdeb080", - "assets/build/windows/Win32/libvorbisfile.dll": "https://files.ballistica.net/cache/ba1/c6/e1/f52f5d6c78f0ba497f0c8cd1c630", - "assets/build/windows/Win32/msvcp140d.dll": "https://files.ballistica.net/cache/ba1/4e/48/54d72587b4bd07abcad311523685", - "assets/build/windows/Win32/ogg.dll": "https://files.ballistica.net/cache/ba1/f2/0c/469bdc2148ad7d94232023da2cc6", - "assets/build/windows/Win32/python.exe": "https://files.ballistica.net/cache/ba1/c6/cd/aa726d9ddec5dfb40de8e2f35ef6", - "assets/build/windows/Win32/python310.dll": "https://files.ballistica.net/cache/ba1/13/56/ff85c22dbf7de5ed75f4a60e1062", - "assets/build/windows/Win32/python310_d.dll": "https://files.ballistica.net/cache/ba1/0b/58/277d0deb50e6bd6f206c5f440cf6", - "assets/build/windows/Win32/python_d.exe": "https://files.ballistica.net/cache/ba1/ad/63/7c257a3497e0af2eab3de0ebc929", - "assets/build/windows/Win32/pythonw.exe": "https://files.ballistica.net/cache/ba1/e9/c6/ff2e30816b91b3abf63e92163098", - "assets/build/windows/Win32/pythonw_d.exe": "https://files.ballistica.net/cache/ba1/a8/d6/149b6e3f44e96c97dedbb9f5352c", - "assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/f5/8b/14895df9caf46f326a3c939b34a4", - "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", - "assets/build/workspace/ninjafightplug.py": "https://files.ballistica.net/cache/ba1/f1/8d/934610811db8bdd58ea84a8f4408", - "assets/build/workspace/onslaughtplug.py": "https://files.ballistica.net/cache/ba1/08/ed/d671c39a3ece6362a6d985112c8e", - "assets/build/workspace/runaroundplug.py": "https://files.ballistica.net/cache/ba1/4d/71/1292911f5369bdb83ef6a34921c0", - "assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e", - "assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/1c/77/ac670a5118abdf8a7687af0e159b", - "ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a", - "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/75/cb/d0257897d9230af60b8265dd49b3", - "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d9/69/54f46438b5c9e5159c342b2f2efe", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2b/17/0b3ec1eab3ff8be3bf20ac14b048", - "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/55/af/00171ed0d6e78a72f7b928a2fbea", - "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/7d/a8/e3e3d130cc77e4070d64b630e2b5", - "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/af/a2/821b7cd09fa5178a8aa52df3ca39", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/47/09/50585aaa691782553aa2c5675b31", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/27/42/18e141f733dfd0233fce159e05e5", - "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/a7/77/dd0b4658d5e0de27f1c3900e777a", - "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d5/67/53cc21710083adc2318860e81c96", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1e/5d/1ad092e3af8455f4d23cc649a737", - "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/52/79/94f1e48fe80359b8e84d9f405f5f", - "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e3/1c/569425522cc0af062aa01d7010eb", - "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/83/67/2f62e2bcc3126bf09bd6066f51ca", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d7/41/b2afb28d9d6d5eca30a40c25a9cf", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/33/05/894c4682febb13c0862a6dba669c", - "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/4e/c6/3b0e683a07ec04a1c975ab09dc9b", - "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/ce/be/450ca52c3de6fb3f9bf37155fea2", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/bb/a4/065dea281d8dac0686ae54ff46bc", - "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/24/5b/3533fde7a47702d923c963163939", - "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/22/b9/efe2e0a58bc65d852104552d67cf", - "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b0/68/12b83d2fb0d9303f77349dcc3e2b", - "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d3/2c/943673be99c77c8c052845fe79fc", - "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2e/07/b739ef23ff55fb3fd45a5434cc46", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/24/eb/8f58ad33181a61dc128229219b55", - "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/74/90/6b7b9259633c55e1e23041bd6fdd", - "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/48/52/5d677ac97fd41b6b55627085ab5c", - "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/01/fd/a66b6ee0cff76d9eb27190ee1f08", - "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2e/b9/7c47db5c03dd72d015ee19d1fdc8", - "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3d/c9/52598eebc9079a275963ce372272", - "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/15/5a/4a90da5f1b681f6bdd6214673e1a", - "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ab/97/449b885954e49816dc53e5112839", - "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/df/4c/e6ac16fd511c196eb6adea36affd", - "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d6/3f/f865c2ac0183040993db5d68ee71", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ae/31/34f17c4dbd9d0c1036541bb8919a", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/91/56/8cf9ffe11c29cf297fddb4b359d7", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/1b/f0/87ceb3ec480dac9ca6365d6cf4c7", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/ff/d3/a94e83a99a3c6df253a9ce750b00", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/f3/bc/7237ac5bfe9308c39157a5e8648b", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/24/0c/6bab1b51d397c084cf6714cf262e", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/bf/d0/d906b7f2b752ef8b1148ebcc8c13", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/63/d3/bf61061c5270e265f68c4f8d13ee", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/9b/0e/575279f8955d4c36ebcda8eba3bd", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/39/3e/759f28bb0fead4a42132ce9954bf", - "src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/23/ce/68396b1b7ec6d2f8425902148140", - "src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/2d/4f/f4fe67827f36cd59cd5193333a02", - "src/ballistica/generated/python_embedded/bootstrap_monolithic.inc": "https://files.ballistica.net/cache/ba1/ef/c1/aa5f1aa10af89f5c0b1e616355fd" + "ballisticakit-windows/Generic/BallisticaKit.ico": "https://files.ballistica.net/cache/ba1/ea/5b/8f39e7538f8abf09a1779b7ac5f6", + "build/assets/ba_data/audio/achievement.ogg": "https://files.ballistica.net/cache/ba1/fd/de/b837150af5100b074f3926e3eb14", + "build/assets/ba_data/audio/actionHero1.ogg": "https://files.ballistica.net/cache/ba1/8c/2f/2072df2a8bc1ad54ff4fbc3b268f", + "build/assets/ba_data/audio/actionHero2.ogg": "https://files.ballistica.net/cache/ba1/54/4a/24d7eaf46f5f84b34327493dba49", + "build/assets/ba_data/audio/actionHero3.ogg": "https://files.ballistica.net/cache/ba1/ca/6e/130c8a02a50e9baed9b0956dd1da", + "build/assets/ba_data/audio/actionHero4.ogg": "https://files.ballistica.net/cache/ba1/a0/44/5303c5c39ab3297c5b04514c5908", + "build/assets/ba_data/audio/actionHeroDeath.ogg": "https://files.ballistica.net/cache/ba1/a4/41/e2065fac295dcfeaccab114773fc", + "build/assets/ba_data/audio/actionHeroFall.ogg": "https://files.ballistica.net/cache/ba1/d3/bd/4c62058c029df53baa270c417ad1", + "build/assets/ba_data/audio/actionHeroHit1.ogg": "https://files.ballistica.net/cache/ba1/b5/7f/a10a58a17d1cc98ad9bf20fa5b79", + "build/assets/ba_data/audio/actionHeroHit2.ogg": "https://files.ballistica.net/cache/ba1/43/1a/d17d8fe51b2e8b041851c7c840cf", + "build/assets/ba_data/audio/activateBeep.ogg": "https://files.ballistica.net/cache/ba1/c5/e1/581055c8e20f315c8433f75e1ab9", + "build/assets/ba_data/audio/agent1.ogg": "https://files.ballistica.net/cache/ba1/5d/22/09ff4ee52984a87fd58f1861ca7c", + "build/assets/ba_data/audio/agent2.ogg": "https://files.ballistica.net/cache/ba1/c3/e9/a2dd53a95749f248c46ccfc3ef0e", + "build/assets/ba_data/audio/agent3.ogg": "https://files.ballistica.net/cache/ba1/c3/ef/f7c12ddb83fba0a9a42f2ded6a01", + "build/assets/ba_data/audio/agent4.ogg": "https://files.ballistica.net/cache/ba1/99/d3/e218f05f0e7158e34080e426dfd2", + "build/assets/ba_data/audio/agentDeath.ogg": "https://files.ballistica.net/cache/ba1/b8/8d/322f9c74cf61328354a5ea8a9535", + "build/assets/ba_data/audio/agentFall.ogg": "https://files.ballistica.net/cache/ba1/7d/19/09e299d16a71ec348d0926e3f6b5", + "build/assets/ba_data/audio/agentHit1.ogg": "https://files.ballistica.net/cache/ba1/69/4d/8ca8dd1a0c58f63fad33d8be5c01", + "build/assets/ba_data/audio/agentHit2.ogg": "https://files.ballistica.net/cache/ba1/7d/bc/c7ee1e3bc1836511e3c679a94a1f", + "build/assets/ba_data/audio/alarm.ogg": "https://files.ballistica.net/cache/ba1/7f/99/43c8aac29b33d4ddc87068be8448", + "build/assets/ba_data/audio/ali1.ogg": "https://files.ballistica.net/cache/ba1/64/a2/816b305ec393be65a70158f90853", + "build/assets/ba_data/audio/ali2.ogg": "https://files.ballistica.net/cache/ba1/30/30/24641305137ff2768763c796e2ea", + "build/assets/ba_data/audio/ali3.ogg": "https://files.ballistica.net/cache/ba1/82/e5/fdc78ffaf50bfc2be9904c3ce2d6", + "build/assets/ba_data/audio/ali4.ogg": "https://files.ballistica.net/cache/ba1/b7/9b/80a12e2be99f205eea57e48afd50", + "build/assets/ba_data/audio/aliDeath.ogg": "https://files.ballistica.net/cache/ba1/83/56/03df9487ae5f33fb679d5f7095ff", + "build/assets/ba_data/audio/aliFall.ogg": "https://files.ballistica.net/cache/ba1/7f/4d/6a5cf2054c96d0e868536105c619", + "build/assets/ba_data/audio/aliHit1.ogg": "https://files.ballistica.net/cache/ba1/e1/34/1fc99f12c570224a06de1ca319b6", + "build/assets/ba_data/audio/aliHit2.ogg": "https://files.ballistica.net/cache/ba1/40/25/622ad2a4d2c39f4cc18348ecff95", + "build/assets/ba_data/audio/alien1.ogg": "https://files.ballistica.net/cache/ba1/20/33/437b68f6ac7e2ddfcbb6805fab6e", + "build/assets/ba_data/audio/alien2.ogg": "https://files.ballistica.net/cache/ba1/0e/db/c09dca9f411250e4ddbca340b108", + "build/assets/ba_data/audio/alien3.ogg": "https://files.ballistica.net/cache/ba1/71/ad/cc20b881e9f3627bbb4b52714754", + "build/assets/ba_data/audio/alien4.ogg": "https://files.ballistica.net/cache/ba1/e6/9c/e29865d73c4f6e57c22fb7eae397", + "build/assets/ba_data/audio/alienDeath.ogg": "https://files.ballistica.net/cache/ba1/53/d6/58e89caf141c80929900f8c1cd27", + "build/assets/ba_data/audio/alienFall.ogg": "https://files.ballistica.net/cache/ba1/43/2c/406e959355b04c0f88b5203aca62", + "build/assets/ba_data/audio/alienHit1.ogg": "https://files.ballistica.net/cache/ba1/55/d1/4492d7ff7223609e62b5fd4117d4", + "build/assets/ba_data/audio/alienHit2.ogg": "https://files.ballistica.net/cache/ba1/5b/6c/a5f7c9b02f7a1de95c15de311779", + "build/assets/ba_data/audio/announceEight.ogg": "https://files.ballistica.net/cache/ba1/2f/07/2813453ae575fe0b3124e1f74ab5", + "build/assets/ba_data/audio/announceFive.ogg": "https://files.ballistica.net/cache/ba1/46/c0/a425676b69f2cc61d09666982aab", + "build/assets/ba_data/audio/announceFour.ogg": "https://files.ballistica.net/cache/ba1/2d/11/7021c7f52478f098bffe8e79cbdc", + "build/assets/ba_data/audio/announceNine.ogg": "https://files.ballistica.net/cache/ba1/0c/2d/b2c4a30686e50b6f2fbb0b62d0b7", + "build/assets/ba_data/audio/announceOne.ogg": "https://files.ballistica.net/cache/ba1/15/5f/697f860e6b8917b98e89cbfd0f7e", + "build/assets/ba_data/audio/announceSeven.ogg": "https://files.ballistica.net/cache/ba1/4a/53/c9a111e4f40d77d889528507f66d", + "build/assets/ba_data/audio/announceSix.ogg": "https://files.ballistica.net/cache/ba1/44/7a/8739d57a288a61ea7f3bd95df421", + "build/assets/ba_data/audio/announceTen.ogg": "https://files.ballistica.net/cache/ba1/73/d9/c969d29d880fb1c86ae0a66df6e7", + "build/assets/ba_data/audio/announceThree.ogg": "https://files.ballistica.net/cache/ba1/27/38/4af86211600c4aee6de0bda3abc1", + "build/assets/ba_data/audio/announceTwo.ogg": "https://files.ballistica.net/cache/ba1/34/c4/09254db34e3d9a4f4afc76c53b9e", + "build/assets/ba_data/audio/assassin1.ogg": "https://files.ballistica.net/cache/ba1/79/a8/64ad5985421df6079cbf4317efed", + "build/assets/ba_data/audio/assassin2.ogg": "https://files.ballistica.net/cache/ba1/91/5e/5a4c9fb112d9bf87afedcbfc8938", + "build/assets/ba_data/audio/assassin3.ogg": "https://files.ballistica.net/cache/ba1/60/2d/d726e27e93af4914b640c5d53cc3", + "build/assets/ba_data/audio/assassin4.ogg": "https://files.ballistica.net/cache/ba1/ba/5e/d781994b6b13268518f5c04f3484", + "build/assets/ba_data/audio/assassinDeath.ogg": "https://files.ballistica.net/cache/ba1/a0/04/cf5e435d1aa9ab6b590957f656a1", + "build/assets/ba_data/audio/assassinFall.ogg": "https://files.ballistica.net/cache/ba1/2c/63/ac78147752b302ca0931ba4716f0", + "build/assets/ba_data/audio/assassinHit1.ogg": "https://files.ballistica.net/cache/ba1/59/e9/48aadecf18e8bfbd8c96be3eeb06", + "build/assets/ba_data/audio/assassinHit2.ogg": "https://files.ballistica.net/cache/ba1/ae/6f/66bcbe392c27174e55f196ec6296", + "build/assets/ba_data/audio/bear1.ogg": "https://files.ballistica.net/cache/ba1/d6/35/0673e42cb8d8d4d44afc2da48039", + "build/assets/ba_data/audio/bear2.ogg": "https://files.ballistica.net/cache/ba1/e3/0f/1b8b7a4bbd40e2f8c0ad4e8262d8", + "build/assets/ba_data/audio/bear3.ogg": "https://files.ballistica.net/cache/ba1/4b/17/f52b72d0f58e0cb2e65b73e9edd1", + "build/assets/ba_data/audio/bear4.ogg": "https://files.ballistica.net/cache/ba1/c0/02/26f5c13ef9a40705018422dbd9e1", + "build/assets/ba_data/audio/bearDeath.ogg": "https://files.ballistica.net/cache/ba1/04/7d/efccbfab80d2579ff771d98e20c4", + "build/assets/ba_data/audio/bearFall.ogg": "https://files.ballistica.net/cache/ba1/bf/c8/8bf48a002785dda5e4c8e0df0f15", + "build/assets/ba_data/audio/bearHit1.ogg": "https://files.ballistica.net/cache/ba1/37/9a/0606e60f38b57fc44df3703c98c9", + "build/assets/ba_data/audio/bearHit2.ogg": "https://files.ballistica.net/cache/ba1/ec/18/ba2fed8e0f4c3c301cdf5cc0d061", + "build/assets/ba_data/audio/bellHigh.ogg": "https://files.ballistica.net/cache/ba1/a0/d5/d6b1e4ec9eba220263971c8211b9", + "build/assets/ba_data/audio/bellLow.ogg": "https://files.ballistica.net/cache/ba1/9e/8e/084009ead597bc1cc10aa366b428", + "build/assets/ba_data/audio/bellMed.ogg": "https://files.ballistica.net/cache/ba1/45/72/ffceccc22b6cf239baefbe039a3e", + "build/assets/ba_data/audio/bigImpact.ogg": "https://files.ballistica.net/cache/ba1/c4/24/6f61a1e850748ecfdae82ace9446", + "build/assets/ba_data/audio/bigImpact2.ogg": "https://files.ballistica.net/cache/ba1/f4/8f/c30ba716d2e9a7d2c8a1c495a221", + "build/assets/ba_data/audio/blank.ogg": "https://files.ballistica.net/cache/ba1/9e/ac/94f36889aa8afe479861d84fb99a", + "build/assets/ba_data/audio/blip.ogg": "https://files.ballistica.net/cache/ba1/c7/67/0c2ca98d50b1835c4d3d9cf95420", + "build/assets/ba_data/audio/block.ogg": "https://files.ballistica.net/cache/ba1/ee/6f/84a7cb4cde51694ae2a0786bf65c", + "build/assets/ba_data/audio/bombDrop01.ogg": "https://files.ballistica.net/cache/ba1/72/0c/0c18bd17ec0ceba0d266142c51c1", + "build/assets/ba_data/audio/bombDrop02.ogg": "https://files.ballistica.net/cache/ba1/27/10/d41f158b0f5deee429f0bae4d52c", + "build/assets/ba_data/audio/bombRoll01.ogg": "https://files.ballistica.net/cache/ba1/93/b0/d84650bf108f6c6c71ebb1aba58c", + "build/assets/ba_data/audio/bones1.ogg": "https://files.ballistica.net/cache/ba1/86/f4/4b5952731e4fbfae31d4e998a7f5", + "build/assets/ba_data/audio/bones2.ogg": "https://files.ballistica.net/cache/ba1/34/fe/170bfe0cb7cea2994f9bbf40e40a", + "build/assets/ba_data/audio/bones3.ogg": "https://files.ballistica.net/cache/ba1/7a/cf/5d838b71ad3c1a4ec29e4747c806", + "build/assets/ba_data/audio/bonesDeath.ogg": "https://files.ballistica.net/cache/ba1/c1/23/ba2781621cf23021603e928f3ff2", + "build/assets/ba_data/audio/bonesFall.ogg": "https://files.ballistica.net/cache/ba1/4e/c8/a958f19c35bfcca8001441812762", + "build/assets/ba_data/audio/boo.ogg": "https://files.ballistica.net/cache/ba1/30/dc/03fef3870100bfb1245530f9db80", + "build/assets/ba_data/audio/boxDrop.ogg": "https://files.ballistica.net/cache/ba1/52/cc/2bf1fccb463348723fa3d250d0cf", + "build/assets/ba_data/audio/boxingBell.ogg": "https://files.ballistica.net/cache/ba1/3f/dd/f0048eac8da79a661e1d1220749e", + "build/assets/ba_data/audio/bunny1.ogg": "https://files.ballistica.net/cache/ba1/fa/8f/5b7e62fb8347703433e6688d8b51", + "build/assets/ba_data/audio/bunny2.ogg": "https://files.ballistica.net/cache/ba1/05/d7/098a3c5eeec0c9643bbd6ab1fa23", + "build/assets/ba_data/audio/bunny3.ogg": "https://files.ballistica.net/cache/ba1/9a/74/97f10f372e9c8a2a7d601b4d3fd1", + "build/assets/ba_data/audio/bunny4.ogg": "https://files.ballistica.net/cache/ba1/6a/50/7daf80d3c47ab1434b261f2d49e5", + "build/assets/ba_data/audio/bunnyDeath.ogg": "https://files.ballistica.net/cache/ba1/af/51/c50a4aadd8b2f1eed87e476665bb", + "build/assets/ba_data/audio/bunnyFall.ogg": "https://files.ballistica.net/cache/ba1/7f/7d/62cc47b9e6f7711c78d251a8dc2a", + "build/assets/ba_data/audio/bunnyHit1.ogg": "https://files.ballistica.net/cache/ba1/b3/ab/b77f008dbda6a12930ea6c61002c", + "build/assets/ba_data/audio/bunnyHit2.ogg": "https://files.ballistica.net/cache/ba1/c2/b8/b951ef772b7caf889a85f6dd8efb", + "build/assets/ba_data/audio/bunnyJump.ogg": "https://files.ballistica.net/cache/ba1/98/7d/f86c3d8c10f5164b054462588fdc", + "build/assets/ba_data/audio/cashRegister.ogg": "https://files.ballistica.net/cache/ba1/92/b4/d6ad80d0941e8b369dae943c5e64", + "build/assets/ba_data/audio/cashRegister2.ogg": "https://files.ballistica.net/cache/ba1/ff/e3/82b3f554001be251f39bd03058d4", + "build/assets/ba_data/audio/charSelectMusic.ogg": "https://files.ballistica.net/cache/ba1/f7/1f/c9028d82951324ba798858603a77", + "build/assets/ba_data/audio/cheer.ogg": "https://files.ballistica.net/cache/ba1/70/d2/45076dec69105b3becd8374c1138", + "build/assets/ba_data/audio/click01.ogg": "https://files.ballistica.net/cache/ba1/23/c5/fb6d53b35e9b70007a72b497abe3", + "build/assets/ba_data/audio/corkPop.ogg": "https://files.ballistica.net/cache/ba1/77/3f/492ea98192f2d5cce07af401f9cc", + "build/assets/ba_data/audio/cowboy1.ogg": "https://files.ballistica.net/cache/ba1/24/ab/e2ecfdad6aa7707a7b567ccf84e6", + "build/assets/ba_data/audio/cowboy2.ogg": "https://files.ballistica.net/cache/ba1/71/98/0d752328e837bb9a4109cd730e90", + "build/assets/ba_data/audio/cowboy3.ogg": "https://files.ballistica.net/cache/ba1/5b/4f/6b94cc899457ba8438c8f3cfbaff", + "build/assets/ba_data/audio/cowboy4.ogg": "https://files.ballistica.net/cache/ba1/d8/f3/b4fa737d6b035b2755bda5c56879", + "build/assets/ba_data/audio/cowboyDeath.ogg": "https://files.ballistica.net/cache/ba1/81/b2/8bf4eb24f23ea98b472fb90f9cb5", + "build/assets/ba_data/audio/cowboyFall.ogg": "https://files.ballistica.net/cache/ba1/c3/46/bbdcb4bac02503962625404da810", + "build/assets/ba_data/audio/cowboyHit1.ogg": "https://files.ballistica.net/cache/ba1/99/4e/e75145e8f0f3f07f9ab122625e0c", + "build/assets/ba_data/audio/cowboyHit2.ogg": "https://files.ballistica.net/cache/ba1/92/df/e2acae87f545f4b4005e8cd2879b", + "build/assets/ba_data/audio/crowdChant.ogg": "https://files.ballistica.net/cache/ba1/e9/a7/3b4834bbac314faad09809a48e72", + "build/assets/ba_data/audio/cyborg1.ogg": "https://files.ballistica.net/cache/ba1/94/83/d967bbd29e4f2d58526d58ec8f0b", + "build/assets/ba_data/audio/cyborg2.ogg": "https://files.ballistica.net/cache/ba1/36/9b/19e6357b335458824b893d4699ba", + "build/assets/ba_data/audio/cyborg3.ogg": "https://files.ballistica.net/cache/ba1/ee/7f/b0f73d5101137d8331806837f4dd", + "build/assets/ba_data/audio/cyborg4.ogg": "https://files.ballistica.net/cache/ba1/83/88/5888b9237f53c24c12fb9242b97f", + "build/assets/ba_data/audio/cyborgDeath.ogg": "https://files.ballistica.net/cache/ba1/52/2b/e717b7371dedef2232ebe5ffb89e", + "build/assets/ba_data/audio/cyborgFall.ogg": "https://files.ballistica.net/cache/ba1/7c/8e/5a92a2762b273214a62dfb7daef6", + "build/assets/ba_data/audio/cyborgHit1.ogg": "https://files.ballistica.net/cache/ba1/50/13/0d46af7ec249c9415dd0fd45bb4c", + "build/assets/ba_data/audio/cyborgHit2.ogg": "https://files.ballistica.net/cache/ba1/01/05/7bcc7a5414df8cde912085189c44", + "build/assets/ba_data/audio/cymbal.ogg": "https://files.ballistica.net/cache/ba1/76/2e/7b1f80cb41454805bca4a01ab91d", + "build/assets/ba_data/audio/debrisFall.ogg": "https://files.ballistica.net/cache/ba1/85/ea/3af46b081128c3dd37af04dbf38d", + "build/assets/ba_data/audio/deek.ogg": "https://files.ballistica.net/cache/ba1/fd/1a/ed14dc1b082bedd08d4715848e86", + "build/assets/ba_data/audio/deek2.ogg": "https://files.ballistica.net/cache/ba1/f3/e0/0fe7adbe189ff4299f5c1dc136fe", + "build/assets/ba_data/audio/ding.ogg": "https://files.ballistica.net/cache/ba1/98/cf/cedcb021e0eeac1caca9eb3de270", + "build/assets/ba_data/audio/dingSmall.ogg": "https://files.ballistica.net/cache/ba1/f7/f3/831276189fc88289fd955a013fed", + "build/assets/ba_data/audio/dingSmallHigh.ogg": "https://files.ballistica.net/cache/ba1/3a/b4/2897572e7602d9968ae728bb38eb", + "build/assets/ba_data/audio/dripity.ogg": "https://files.ballistica.net/cache/ba1/83/c4/632325125757f05d28b6bf4260e3", + "build/assets/ba_data/audio/drumRoll.ogg": "https://files.ballistica.net/cache/ba1/dc/dc/1f52151d6e423e767e4bad7cb094", + "build/assets/ba_data/audio/error.ogg": "https://files.ballistica.net/cache/ba1/60/e6/d8a3089d86833c85dc155779977c", + "build/assets/ba_data/audio/explosion01.ogg": "https://files.ballistica.net/cache/ba1/c1/d9/605970513b253268b86724927274", + "build/assets/ba_data/audio/explosion02.ogg": "https://files.ballistica.net/cache/ba1/ea/b8/d82480ee08063b9f331934e5262b", + "build/assets/ba_data/audio/explosion03.ogg": "https://files.ballistica.net/cache/ba1/3d/e8/cab8864e8200f21a8c88fb3e6cc4", + "build/assets/ba_data/audio/explosion04.ogg": "https://files.ballistica.net/cache/ba1/27/bd/d7fe2e33a501675f0bed62aa37df", + "build/assets/ba_data/audio/explosion05.ogg": "https://files.ballistica.net/cache/ba1/3e/2a/db895f6b8889419169939c194e96", + "build/assets/ba_data/audio/fanfare.ogg": "https://files.ballistica.net/cache/ba1/00/12/1ece0a1ef68d67d1f5bdc632ab73", + "build/assets/ba_data/audio/flagCatcherMusic.ogg": "https://files.ballistica.net/cache/ba1/6a/6b/dadc874e7a36d7bc070d09c88d42", + "build/assets/ba_data/audio/flyingMusic.ogg": "https://files.ballistica.net/cache/ba1/8a/00/ec8157f5be3d9c0b40bd6ecfb97f", + "build/assets/ba_data/audio/foghorn.ogg": "https://files.ballistica.net/cache/ba1/ab/94/75f87bcfadd7244f779f826526b3", + "build/assets/ba_data/audio/footImpact01.ogg": "https://files.ballistica.net/cache/ba1/a6/cd/583c4a6979a04fa9dbdc5e3b61e2", + "build/assets/ba_data/audio/footImpact02.ogg": "https://files.ballistica.net/cache/ba1/c9/c4/4d9f5a128bed821d7c176319e7ab", + "build/assets/ba_data/audio/footImpact03.ogg": "https://files.ballistica.net/cache/ba1/e1/ba/334df4e0b899199227181664fa1b", + "build/assets/ba_data/audio/forwardMarchMusic.ogg": "https://files.ballistica.net/cache/ba1/91/b0/50e9673705c43b42fb3c9ac4f444", + "build/assets/ba_data/audio/freeze.ogg": "https://files.ballistica.net/cache/ba1/18/7e/adeb4a2b9f04cf919bb10dbcba54", + "build/assets/ba_data/audio/frosty01.ogg": "https://files.ballistica.net/cache/ba1/9f/15/0b59175e8ccaee89f8bcc085e207", + "build/assets/ba_data/audio/frosty02.ogg": "https://files.ballistica.net/cache/ba1/92/1f/587ed76ce5a3480e253bb56ab76e", + "build/assets/ba_data/audio/frosty03.ogg": "https://files.ballistica.net/cache/ba1/41/33/6e40b79692851b652c45ff2d1da6", + "build/assets/ba_data/audio/frosty04.ogg": "https://files.ballistica.net/cache/ba1/29/5c/b061e0c280623af0b8dfbda71fd4", + "build/assets/ba_data/audio/frosty05.ogg": "https://files.ballistica.net/cache/ba1/69/16/7cd788907d2b360761af291428e3", + "build/assets/ba_data/audio/frostyDeath.ogg": "https://files.ballistica.net/cache/ba1/32/e6/70f76016b3c2cc9d0ab4ee81f735", + "build/assets/ba_data/audio/frostyFall.ogg": "https://files.ballistica.net/cache/ba1/94/a9/6f0cd55d5cd3788771bc4ade217f", + "build/assets/ba_data/audio/frostyHit01.ogg": "https://files.ballistica.net/cache/ba1/a0/25/9bdcfc81c582a3cd800509a1b3e6", + "build/assets/ba_data/audio/frostyHit02.ogg": "https://files.ballistica.net/cache/ba1/b4/e2/1d1cc6c8d75754b91299211d9b50", + "build/assets/ba_data/audio/frostyHit03.ogg": "https://files.ballistica.net/cache/ba1/51/ad/92338fd0a8f8d9859f74a2f180d5", + "build/assets/ba_data/audio/fuse01.ogg": "https://files.ballistica.net/cache/ba1/29/82/7064dea5890e39a5abe6103b8454", + "build/assets/ba_data/audio/gladiator1.ogg": "https://files.ballistica.net/cache/ba1/e4/89/93b352efc81e606fff8337d765cc", + "build/assets/ba_data/audio/gladiator2.ogg": "https://files.ballistica.net/cache/ba1/c3/3f/81a4886f5267ff7a8fcaf3255b19", + "build/assets/ba_data/audio/gladiator3.ogg": "https://files.ballistica.net/cache/ba1/df/7d/35d408af3a3dfc05f525698cf203", + "build/assets/ba_data/audio/gladiator4.ogg": "https://files.ballistica.net/cache/ba1/06/66/c1aa2c2cda561f70c197825ea02e", + "build/assets/ba_data/audio/gladiatorDeath.ogg": "https://files.ballistica.net/cache/ba1/69/c3/5c8fd9d4654d7f51d559eac95878", + "build/assets/ba_data/audio/gladiatorFall.ogg": "https://files.ballistica.net/cache/ba1/15/4f/98b554f3eff3cb5e52bcd61046fe", + "build/assets/ba_data/audio/gladiatorHit1.ogg": "https://files.ballistica.net/cache/ba1/a3/4b/372a3832d03ee8e9226ca12ac2a8", + "build/assets/ba_data/audio/gladiatorHit2.ogg": "https://files.ballistica.net/cache/ba1/61/7e/53481c14a737cc63b1667fa971e4", + "build/assets/ba_data/audio/gong.ogg": "https://files.ballistica.net/cache/ba1/57/ab/0c05bfa996272436253745304373", + "build/assets/ba_data/audio/grandRompMusic.ogg": "https://files.ballistica.net/cache/ba1/82/c6/20b35823538efd2a2e9600b8b6ab", + "build/assets/ba_data/audio/gravelSkid.ogg": "https://files.ballistica.net/cache/ba1/85/46/1d7001817133fe6f22bb7d3d0b70", + "build/assets/ba_data/audio/gunCocking.ogg": "https://files.ballistica.net/cache/ba1/98/71/8c3536d374615a9ef1781ce96337", + "build/assets/ba_data/audio/healthPowerup.ogg": "https://files.ballistica.net/cache/ba1/56/e4/bc661f7f35cd4f46df4637863606", + "build/assets/ba_data/audio/hiss.ogg": "https://files.ballistica.net/cache/ba1/df/8d/669b2759c741f92fa741686ee5d1", + "build/assets/ba_data/audio/impactHard.ogg": "https://files.ballistica.net/cache/ba1/4a/40/ed7a0fd282b596571fff68b30f06", + "build/assets/ba_data/audio/impactHard2.ogg": "https://files.ballistica.net/cache/ba1/f9/d9/03b62ab0127730f4a49722a65bf1", + "build/assets/ba_data/audio/impactHard3.ogg": "https://files.ballistica.net/cache/ba1/d4/9e/781bae7d18beca2f5c3aabccf5d6", + "build/assets/ba_data/audio/impactMedium.ogg": "https://files.ballistica.net/cache/ba1/ed/7d/17ce075b05e2aca8c37b7a6d11ea", + "build/assets/ba_data/audio/impactMedium2.ogg": "https://files.ballistica.net/cache/ba1/42/e5/f6bb327eb3d98094052612ee5c7f", + "build/assets/ba_data/audio/jack01.ogg": "https://files.ballistica.net/cache/ba1/cf/1c/0917396bec0b0eff6c569543b947", + "build/assets/ba_data/audio/jack02.ogg": "https://files.ballistica.net/cache/ba1/46/ec/b47b588b4361922e3613137cf86f", + "build/assets/ba_data/audio/jack03.ogg": "https://files.ballistica.net/cache/ba1/7a/3f/30701ee093f8d9edc3689bfb442b", + "build/assets/ba_data/audio/jack04.ogg": "https://files.ballistica.net/cache/ba1/64/3b/142ce814f8acce20a5783de919d7", + "build/assets/ba_data/audio/jack05.ogg": "https://files.ballistica.net/cache/ba1/48/97/ae160a91f9421c01965c5ec9311b", + "build/assets/ba_data/audio/jack06.ogg": "https://files.ballistica.net/cache/ba1/54/35/bcb2956b3c80e834720c4731ec81", + "build/assets/ba_data/audio/jackDeath01.ogg": "https://files.ballistica.net/cache/ba1/98/0c/c1b5781fc29b14e38b34251482f6", + "build/assets/ba_data/audio/jackFall01.ogg": "https://files.ballistica.net/cache/ba1/33/f4/fd9eac23dcb7a12482c945e0b988", + "build/assets/ba_data/audio/jackHit01.ogg": "https://files.ballistica.net/cache/ba1/8f/5d/df097fde7c2ba684ced5811b9cc6", + "build/assets/ba_data/audio/jackHit02.ogg": "https://files.ballistica.net/cache/ba1/d2/78/b9beda2d17779f5b3060277e65d8", + "build/assets/ba_data/audio/jackHit03.ogg": "https://files.ballistica.net/cache/ba1/fc/d0/aba07ffb129cb0b99eb64dec6e23", + "build/assets/ba_data/audio/jackHit04.ogg": "https://files.ballistica.net/cache/ba1/26/34/ca2334f1bc12a979929add2abcbc", + "build/assets/ba_data/audio/jackHit05.ogg": "https://files.ballistica.net/cache/ba1/db/4a/dfc93517cd301f2a65b2ae6aa4af", + "build/assets/ba_data/audio/jackHit06.ogg": "https://files.ballistica.net/cache/ba1/f2/5c/05676fea2263178122c293358084", + "build/assets/ba_data/audio/jackHit07.ogg": "https://files.ballistica.net/cache/ba1/25/67/77d329ab6717a76c97fc17108bb1", + "build/assets/ba_data/audio/jumpsuit1.ogg": "https://files.ballistica.net/cache/ba1/6f/79/92ec66104ad1171a5bfcd2928ce7", + "build/assets/ba_data/audio/jumpsuit2.ogg": "https://files.ballistica.net/cache/ba1/da/76/7eb974bed29376ff5f46da6e52f5", + "build/assets/ba_data/audio/jumpsuit3.ogg": "https://files.ballistica.net/cache/ba1/e4/49/1b11b8961f9e599cdb65e568fa19", + "build/assets/ba_data/audio/jumpsuit4.ogg": "https://files.ballistica.net/cache/ba1/70/50/0fab4bc83d4ebf034b8e94dd9d89", + "build/assets/ba_data/audio/jumpsuitDeath.ogg": "https://files.ballistica.net/cache/ba1/55/b9/33815d38a8bd9521bf0c34e5d962", + "build/assets/ba_data/audio/jumpsuitFall.ogg": "https://files.ballistica.net/cache/ba1/49/d1/9bced0869569510e47052d8d2d80", + "build/assets/ba_data/audio/jumpsuitHit1.ogg": "https://files.ballistica.net/cache/ba1/5d/1c/8b7131fb555bcf3f6786f874d502", + "build/assets/ba_data/audio/jumpsuitHit2.ogg": "https://files.ballistica.net/cache/ba1/57/a3/1a440777e5b0a68761fd3d685781", + "build/assets/ba_data/audio/kronk1.ogg": "https://files.ballistica.net/cache/ba1/4b/7a/e4aba3bb6f5f3f1e079b41fbe128", + "build/assets/ba_data/audio/kronk10.ogg": "https://files.ballistica.net/cache/ba1/2a/c7/3278798bf83d028082f76f4bb266", + "build/assets/ba_data/audio/kronk2.ogg": "https://files.ballistica.net/cache/ba1/37/63/bc27bb40bb3cec2249f6a4b1f6db", + "build/assets/ba_data/audio/kronk3.ogg": "https://files.ballistica.net/cache/ba1/7d/7c/dd1267438d7fe5a03381595643c9", + "build/assets/ba_data/audio/kronk4.ogg": "https://files.ballistica.net/cache/ba1/37/5c/f7ea3a2686cd4f0dcb8f1f7d72d2", + "build/assets/ba_data/audio/kronk5.ogg": "https://files.ballistica.net/cache/ba1/52/fe/62fc3f2c8f8fbd8cf5ef70f06e5d", + "build/assets/ba_data/audio/kronk6.ogg": "https://files.ballistica.net/cache/ba1/19/a5/e606e889615171528e063fd65583", + "build/assets/ba_data/audio/kronk7.ogg": "https://files.ballistica.net/cache/ba1/cd/92/982b535be005eb5113f64a0d53e4", + "build/assets/ba_data/audio/kronk8.ogg": "https://files.ballistica.net/cache/ba1/41/3b/b4e57a04c07c40c7ac4d75f84c1e", + "build/assets/ba_data/audio/kronk9.ogg": "https://files.ballistica.net/cache/ba1/9c/9d/5bd11e9a204dfe537e24312fd168", + "build/assets/ba_data/audio/kronkDeath.ogg": "https://files.ballistica.net/cache/ba1/59/bd/1ee1a59a8ec7dc320b756094ea26", + "build/assets/ba_data/audio/kronkFall.ogg": "https://files.ballistica.net/cache/ba1/5f/60/21484b877b7b4853cac2570e17d6", + "build/assets/ba_data/audio/laser.ogg": "https://files.ballistica.net/cache/ba1/be/32/5e1619b55386bd34fcc9b960450d", + "build/assets/ba_data/audio/laserReverse.ogg": "https://files.ballistica.net/cache/ba1/9d/fe/1d3e3407a9ab9978fa982cfaa6f6", + "build/assets/ba_data/audio/mel01.ogg": "https://files.ballistica.net/cache/ba1/31/dc/8566fd755c935bd7915b5809dd95", + "build/assets/ba_data/audio/mel02.ogg": "https://files.ballistica.net/cache/ba1/e1/7f/8d4dfec6f72272cd852ad5ddbf3f", + "build/assets/ba_data/audio/mel03.ogg": "https://files.ballistica.net/cache/ba1/85/c6/a6e77c40f52c474d2a15f4180ca3", + "build/assets/ba_data/audio/mel04.ogg": "https://files.ballistica.net/cache/ba1/6d/8c/53c4ded7b2575a507982f85340ee", + "build/assets/ba_data/audio/mel05.ogg": "https://files.ballistica.net/cache/ba1/3b/cd/4195749cc58073f0d36454748e9f", + "build/assets/ba_data/audio/mel06.ogg": "https://files.ballistica.net/cache/ba1/cd/d3/72c78aa4d5647ce0b704c43ad92a", + "build/assets/ba_data/audio/mel07.ogg": "https://files.ballistica.net/cache/ba1/24/2b/d642d0c7c2d399fc40a9ebb47acd", + "build/assets/ba_data/audio/mel08.ogg": "https://files.ballistica.net/cache/ba1/a5/9e/c9620c2b624ed396ea79ce48b648", + "build/assets/ba_data/audio/mel09.ogg": "https://files.ballistica.net/cache/ba1/e9/68/f0a2787a309815b094df3a46d9ab", + "build/assets/ba_data/audio/mel10.ogg": "https://files.ballistica.net/cache/ba1/61/31/f6f66683876a4140ed76d1aa82f1", + "build/assets/ba_data/audio/melDeath01.ogg": "https://files.ballistica.net/cache/ba1/65/c4/19829d04e9677a47c857415577fb", + "build/assets/ba_data/audio/melFall01.ogg": "https://files.ballistica.net/cache/ba1/3a/0a/77bf218fb798e17054e6a94aa6f2", + "build/assets/ba_data/audio/menuMusic.ogg": "https://files.ballistica.net/cache/ba1/69/49/9866a5dd9d1f4b5ade4d3e35e57a", + "build/assets/ba_data/audio/metalHit.ogg": "https://files.ballistica.net/cache/ba1/b6/9d/535026e2cc905f60b9ef725436f3", + "build/assets/ba_data/audio/metalSkid.ogg": "https://files.ballistica.net/cache/ba1/6d/1c/cac86d42cefefd50fe432699bc12", + "build/assets/ba_data/audio/ninjaAttack1.ogg": "https://files.ballistica.net/cache/ba1/c2/2d/6685a73bb9fb04a595138c889049", + "build/assets/ba_data/audio/ninjaAttack2.ogg": "https://files.ballistica.net/cache/ba1/2b/70/89f5a8d09958cb5bb12eb5a32e62", + "build/assets/ba_data/audio/ninjaAttack3.ogg": "https://files.ballistica.net/cache/ba1/a3/8b/98df90a9ec07345f9354b8b2c5af", + "build/assets/ba_data/audio/ninjaAttack4.ogg": "https://files.ballistica.net/cache/ba1/92/32/1875e23eab0ede2db0eafd8b4a44", + "build/assets/ba_data/audio/ninjaAttack5.ogg": "https://files.ballistica.net/cache/ba1/a0/92/fe2933d37c587ef90b77779e307d", + "build/assets/ba_data/audio/ninjaAttack6.ogg": "https://files.ballistica.net/cache/ba1/6f/a1/9c9d2e5cd1d0e4f104665a3abf14", + "build/assets/ba_data/audio/ninjaAttack7.ogg": "https://files.ballistica.net/cache/ba1/24/08/852441624e57a78ec3c69e85ae39", + "build/assets/ba_data/audio/ninjaDeath1.ogg": "https://files.ballistica.net/cache/ba1/25/db/88826aa0d547497dea74687f53e6", + "build/assets/ba_data/audio/ninjaFall1.ogg": "https://files.ballistica.net/cache/ba1/dd/28/96ccf67566a7d4593d15c0f32fb2", + "build/assets/ba_data/audio/ninjaHit1.ogg": "https://files.ballistica.net/cache/ba1/39/d1/a78bccb81909e51fae2e81158c11", + "build/assets/ba_data/audio/ninjaHit2.ogg": "https://files.ballistica.net/cache/ba1/a3/74/afdf3f603354b68ed48a84c9be45", + "build/assets/ba_data/audio/ninjaHit3.ogg": "https://files.ballistica.net/cache/ba1/04/81/480f0f3f156a045cc67837a8108a", + "build/assets/ba_data/audio/ninjaHit4.ogg": "https://files.ballistica.net/cache/ba1/c4/13/b5406760df9ce07d4b2b81f84177", + "build/assets/ba_data/audio/ninjaHit5.ogg": "https://files.ballistica.net/cache/ba1/18/61/90103177f66554160231826e7597", + "build/assets/ba_data/audio/ninjaHit6.ogg": "https://files.ballistica.net/cache/ba1/1e/49/fd620696c44ea76ff4a10b582bb8", + "build/assets/ba_data/audio/ninjaHit7.ogg": "https://files.ballistica.net/cache/ba1/8f/13/e849a604afb6cb21244a98cb365d", + "build/assets/ba_data/audio/ninjaHit8.ogg": "https://files.ballistica.net/cache/ba1/06/86/3da41a4a8a068e1c40d91791bce6", + "build/assets/ba_data/audio/oldLady1.ogg": "https://files.ballistica.net/cache/ba1/13/aa/59b87d8f97c50fe6932b4a54c5b5", + "build/assets/ba_data/audio/oldLady2.ogg": "https://files.ballistica.net/cache/ba1/39/8c/0754c50c9a8c804424359327515c", + "build/assets/ba_data/audio/oldLady3.ogg": "https://files.ballistica.net/cache/ba1/fb/39/b66c763460404c010672242a5dd3", + "build/assets/ba_data/audio/oldLady4.ogg": "https://files.ballistica.net/cache/ba1/92/c3/39363c9c28731fa53a860f0917ce", + "build/assets/ba_data/audio/oldLadyDeath.ogg": "https://files.ballistica.net/cache/ba1/83/ad/3e5a8431795d25650bc76fee3905", + "build/assets/ba_data/audio/oldLadyFall.ogg": "https://files.ballistica.net/cache/ba1/e0/7c/f8dd788d236507dd773de3dc984a", + "build/assets/ba_data/audio/oldLadyHit1.ogg": "https://files.ballistica.net/cache/ba1/53/9e/67d9ed9b26ef8186dffa0d7bb2ae", + "build/assets/ba_data/audio/oldLadyHit2.ogg": "https://files.ballistica.net/cache/ba1/12/fd/1356cdcb4d4da314957d2c1193e1", + "build/assets/ba_data/audio/ooh.ogg": "https://files.ballistica.net/cache/ba1/ef/9d/e08d3c2d003e35ddf6b9d3415f2b", + "build/assets/ba_data/audio/operaSinger1.ogg": "https://files.ballistica.net/cache/ba1/53/a8/588fc68c348aa0d99d696857ae24", + "build/assets/ba_data/audio/operaSinger2.ogg": "https://files.ballistica.net/cache/ba1/57/7b/9fb9089c35b3dddfa9ffa0cc82cf", + "build/assets/ba_data/audio/operaSinger3.ogg": "https://files.ballistica.net/cache/ba1/16/7f/2980a2d24c82f4a3cc7815ebdf97", + "build/assets/ba_data/audio/operaSinger4.ogg": "https://files.ballistica.net/cache/ba1/ec/81/0b642ed2aa6b66a5fb33c905c6ba", + "build/assets/ba_data/audio/operaSingerDeath.ogg": "https://files.ballistica.net/cache/ba1/b9/e1/d5f272a093176993e5e02c813fe5", + "build/assets/ba_data/audio/operaSingerFall.ogg": "https://files.ballistica.net/cache/ba1/5e/56/f28c5ae057f4985275936b230def", + "build/assets/ba_data/audio/operaSingerHit1.ogg": "https://files.ballistica.net/cache/ba1/15/26/2f6a2d01d81ddeec9dbb5801f761", + "build/assets/ba_data/audio/operaSingerHit2.ogg": "https://files.ballistica.net/cache/ba1/fe/64/03aed39dfcaf0a212001ee736846", + "build/assets/ba_data/audio/orchestraHit.ogg": "https://files.ballistica.net/cache/ba1/c9/70/d9c6095de0eff1c12317f78ca404", + "build/assets/ba_data/audio/orchestraHit2.ogg": "https://files.ballistica.net/cache/ba1/69/4e/470ef36c9613920e7c5e4b6e8440", + "build/assets/ba_data/audio/orchestraHit3.ogg": "https://files.ballistica.net/cache/ba1/3a/4e/44550b845b7adbe8ca4bd80f5698", + "build/assets/ba_data/audio/orchestraHit4.ogg": "https://files.ballistica.net/cache/ba1/3c/83/2ab6e6116c5710b9c5e546790930", + "build/assets/ba_data/audio/orchestraHitBig1.ogg": "https://files.ballistica.net/cache/ba1/2f/ab/468ce69bdd1b78e22719f7207198", + "build/assets/ba_data/audio/orchestraHitBig2.ogg": "https://files.ballistica.net/cache/ba1/16/d5/51a1766181c4993008d001110a2f", + "build/assets/ba_data/audio/penguin1.ogg": "https://files.ballistica.net/cache/ba1/2c/b3/1646831bbe907f737e495e28751d", + "build/assets/ba_data/audio/penguin2.ogg": "https://files.ballistica.net/cache/ba1/d3/8d/0ecbc1987986196003d25248c9bb", + "build/assets/ba_data/audio/penguin3.ogg": "https://files.ballistica.net/cache/ba1/3f/c7/170248cb3b4980562595ab4b6874", + "build/assets/ba_data/audio/penguin4.ogg": "https://files.ballistica.net/cache/ba1/5c/06/fc177266d7db226fd4597ff66036", + "build/assets/ba_data/audio/penguinDeath.ogg": "https://files.ballistica.net/cache/ba1/4f/b4/1ca6450511184387d61fa83dd8c1", + "build/assets/ba_data/audio/penguinFall.ogg": "https://files.ballistica.net/cache/ba1/35/61/ebee7c78642478d0a78ff3baf24e", + "build/assets/ba_data/audio/penguinHit1.ogg": "https://files.ballistica.net/cache/ba1/1a/1a/a66998a5f0766420ab221552310a", + "build/assets/ba_data/audio/penguinHit2.ogg": "https://files.ballistica.net/cache/ba1/f0/e3/9c2571186b969cc64eaa04871e60", + "build/assets/ba_data/audio/pixie1.ogg": "https://files.ballistica.net/cache/ba1/37/9f/9f1b9232ea1b3b0b0c649894ea31", + "build/assets/ba_data/audio/pixie2.ogg": "https://files.ballistica.net/cache/ba1/17/55/8e27b5fc064c671f54dd636275ac", + "build/assets/ba_data/audio/pixie3.ogg": "https://files.ballistica.net/cache/ba1/42/d5/9c3c3ae8b9e3d61c8772e8d9a50c", + "build/assets/ba_data/audio/pixie4.ogg": "https://files.ballistica.net/cache/ba1/21/72/650ee9b45da0b190f9579f506e29", + "build/assets/ba_data/audio/pixieDeath.ogg": "https://files.ballistica.net/cache/ba1/14/e7/5a2dc91889d0ca9cbd4d7d8fc0b5", + "build/assets/ba_data/audio/pixieFall.ogg": "https://files.ballistica.net/cache/ba1/14/d8/ae062d563e30aaf27287f4dc6004", + "build/assets/ba_data/audio/pixieHit1.ogg": "https://files.ballistica.net/cache/ba1/09/6e/f43916bd67df3bfd9921902bdcb8", + "build/assets/ba_data/audio/pixieHit2.ogg": "https://files.ballistica.net/cache/ba1/52/51/9f21b08635b1b87751f315cf0e3d", + "build/assets/ba_data/audio/playerDeath.ogg": "https://files.ballistica.net/cache/ba1/e9/4d/03e407534897efcaf3286fa3bf3e", + "build/assets/ba_data/audio/playerLeft.ogg": "https://files.ballistica.net/cache/ba1/f6/a9/7868feb5942a7849b7201c3d3d5d", + "build/assets/ba_data/audio/pop01.ogg": "https://files.ballistica.net/cache/ba1/d4/5c/5607ee1c5d826c13939a82508b23", + "build/assets/ba_data/audio/powerdown01.ogg": "https://files.ballistica.net/cache/ba1/a8/e6/cfde1513008f5cd0716c693682c0", + "build/assets/ba_data/audio/powerup01.ogg": "https://files.ballistica.net/cache/ba1/f2/06/de5632f1c649a081bbafc50aea5b", + "build/assets/ba_data/audio/punch01.ogg": "https://files.ballistica.net/cache/ba1/0d/9b/9cbb77fd90ef782fc486b67b1dd4", + "build/assets/ba_data/audio/punchStrong01.ogg": "https://files.ballistica.net/cache/ba1/04/b1/c1fd724125c0b7f0f3abefbb9bea", + "build/assets/ba_data/audio/punchStrong02.ogg": "https://files.ballistica.net/cache/ba1/4b/df/5b1218aaa178f7b37bf6bf95c416", + "build/assets/ba_data/audio/punchSwish.ogg": "https://files.ballistica.net/cache/ba1/37/70/eb887ba259d5ef06fb0f28fd036b", + "build/assets/ba_data/audio/punchWeak01.ogg": "https://files.ballistica.net/cache/ba1/f8/16/8fbe1c43cf6a8f220c5beaa3fc8e", + "build/assets/ba_data/audio/raceBeep1.ogg": "https://files.ballistica.net/cache/ba1/b5/95/27aa8d0bf4b230d8e736b5940b72", + "build/assets/ba_data/audio/raceBeep2.ogg": "https://files.ballistica.net/cache/ba1/e0/aa/d8d993eb1730dffb4d26b3263a49", + "build/assets/ba_data/audio/refWhistle.ogg": "https://files.ballistica.net/cache/ba1/8b/fa/5b109fe3eea2577ac4d92c092747", + "build/assets/ba_data/audio/robot1.ogg": "https://files.ballistica.net/cache/ba1/96/45/4493573de6a0d69d7250558be69b", + "build/assets/ba_data/audio/robot2.ogg": "https://files.ballistica.net/cache/ba1/60/6e/d473ea6aa4f9f6a70c16e7dd8c29", + "build/assets/ba_data/audio/robot3.ogg": "https://files.ballistica.net/cache/ba1/6a/59/321f6e8e6b7bbab4872daafda5bd", + "build/assets/ba_data/audio/robot4.ogg": "https://files.ballistica.net/cache/ba1/bc/cf/d9827cb81459ebea674810ced7c0", + "build/assets/ba_data/audio/robotDeath.ogg": "https://files.ballistica.net/cache/ba1/3c/74/0b504a8215100384c5226e30c948", + "build/assets/ba_data/audio/robotFall.ogg": "https://files.ballistica.net/cache/ba1/b9/e6/0d5bbd049c6688a680dd87037d6e", + "build/assets/ba_data/audio/robotHit1.ogg": "https://files.ballistica.net/cache/ba1/b4/9e/4b2da2a75726dcff331cd3c12c9d", + "build/assets/ba_data/audio/robotHit2.ogg": "https://files.ballistica.net/cache/ba1/e2/71/94dd57f4c9698100a8cded834025", + "build/assets/ba_data/audio/runAwayMusic.ogg": "https://files.ballistica.net/cache/ba1/15/d9/29f7a470e858fbff392b0b10c5ba", + "build/assets/ba_data/audio/santa01.ogg": "https://files.ballistica.net/cache/ba1/29/75/c32a777c8d578f97d2f056721de0", + "build/assets/ba_data/audio/santa02.ogg": "https://files.ballistica.net/cache/ba1/68/c6/fe77c231113b5394a155609281fb", + "build/assets/ba_data/audio/santa03.ogg": "https://files.ballistica.net/cache/ba1/3a/3d/8b986e7cf008688f61b979069bd0", + "build/assets/ba_data/audio/santa04.ogg": "https://files.ballistica.net/cache/ba1/ab/d9/38739263f0314607671a8a517b10", + "build/assets/ba_data/audio/santa05.ogg": "https://files.ballistica.net/cache/ba1/dc/69/5078f91fc2afb040c41d24177567", + "build/assets/ba_data/audio/santaDeath.ogg": "https://files.ballistica.net/cache/ba1/2c/cb/045b34b4c1245e7c6ac7f85c60c0", + "build/assets/ba_data/audio/santaFall.ogg": "https://files.ballistica.net/cache/ba1/1f/14/be78056289678f1f97c15ff4278d", + "build/assets/ba_data/audio/santaHit01.ogg": "https://files.ballistica.net/cache/ba1/db/06/b2b0e00037c90fb742ff1712a8d9", + "build/assets/ba_data/audio/santaHit02.ogg": "https://files.ballistica.net/cache/ba1/39/5b/c458a5f1d2265a7828c7b458d238", + "build/assets/ba_data/audio/santaHit03.ogg": "https://files.ballistica.net/cache/ba1/8c/cf/987c58522df5a76927dfcae893f9", + "build/assets/ba_data/audio/santaHit04.ogg": "https://files.ballistica.net/cache/ba1/00/15/62528d4cc62c999a078a55309b31", + "build/assets/ba_data/audio/scamper01.ogg": "https://files.ballistica.net/cache/ba1/b0/20/eee34b248060b84c14d7f6066611", + "build/assets/ba_data/audio/scaryMusic.ogg": "https://files.ballistica.net/cache/ba1/1d/8a/9ae9c2649b88dd7c004055a51a4d", + "build/assets/ba_data/audio/score.ogg": "https://files.ballistica.net/cache/ba1/6c/7c/c802f3d0965a46a5f21d3722013f", + "build/assets/ba_data/audio/scoreHit01.ogg": "https://files.ballistica.net/cache/ba1/1d/85/ed1ac17c2f593d2c157edb553ba9", + "build/assets/ba_data/audio/scoreHit02.ogg": "https://files.ballistica.net/cache/ba1/11/55/b3c84bddd7cab4ee1be499aa750d", + "build/assets/ba_data/audio/scoreIncrease.ogg": "https://files.ballistica.net/cache/ba1/da/3a/2b30c5d1cfcb40aba5435d91d74a", + "build/assets/ba_data/audio/scoresEpicMusic.ogg": "https://files.ballistica.net/cache/ba1/8b/9b/ee9d8894cda47a9ef689f0452196", + "build/assets/ba_data/audio/shatter.ogg": "https://files.ballistica.net/cache/ba1/f6/6a/80c9cd14443d3fc97096645240ad", + "build/assets/ba_data/audio/shieldDown.ogg": "https://files.ballistica.net/cache/ba1/9f/5b/22c3f9bb0aa104166c4d5f1de618", + "build/assets/ba_data/audio/shieldHit.ogg": "https://files.ballistica.net/cache/ba1/35/06/0cb69bf0f0d444069769c0798398", + "build/assets/ba_data/audio/shieldUp.ogg": "https://files.ballistica.net/cache/ba1/fd/bf/e3e75a4072b913d1dd8b7b7caeb1", + "build/assets/ba_data/audio/skid01.ogg": "https://files.ballistica.net/cache/ba1/3d/d3/407c656a7947021d64f2c4b69875", + "build/assets/ba_data/audio/slowEpicMusic.ogg": "https://files.ballistica.net/cache/ba1/20/0c/9b1f02820e133e4065ccbb21e0f3", + "build/assets/ba_data/audio/sparkle01.ogg": "https://files.ballistica.net/cache/ba1/83/21/6da0d8f351c2361880e464cc122c", + "build/assets/ba_data/audio/sparkle02.ogg": "https://files.ballistica.net/cache/ba1/14/c6/a6d67dc8a374233987e5501feb83", + "build/assets/ba_data/audio/sparkle03.ogg": "https://files.ballistica.net/cache/ba1/54/c0/b1b0bfbc42f36bb91f8f1820c2d3", + "build/assets/ba_data/audio/spawn.ogg": "https://files.ballistica.net/cache/ba1/20/ef/418ad54088b3dc3bdaeb6b998737", + "build/assets/ba_data/audio/spazAttack01.ogg": "https://files.ballistica.net/cache/ba1/46/9c/3f0dfcce5bfc1b280a8ea5fd9842", + "build/assets/ba_data/audio/spazAttack02.ogg": "https://files.ballistica.net/cache/ba1/b1/c7/dcfb077812ac5da130e256adea84", + "build/assets/ba_data/audio/spazAttack03.ogg": "https://files.ballistica.net/cache/ba1/a2/9f/6c817b541d14c17c2cb267d1ea47", + "build/assets/ba_data/audio/spazAttack04.ogg": "https://files.ballistica.net/cache/ba1/f8/25/600c794be6b0c394a8454d83faf2", + "build/assets/ba_data/audio/spazDeath01.ogg": "https://files.ballistica.net/cache/ba1/d5/14/a1af1a1d6655474d2ffd23106882", + "build/assets/ba_data/audio/spazEff.ogg": "https://files.ballistica.net/cache/ba1/27/e0/fe9eef7ec78a37f2be0861905d49", + "build/assets/ba_data/audio/spazFall01.ogg": "https://files.ballistica.net/cache/ba1/24/4c/90687b59b839530241317a07b75e", + "build/assets/ba_data/audio/spazImpact01.ogg": "https://files.ballistica.net/cache/ba1/ea/01/49d17a623e4cd1cf0e00c510c365", + "build/assets/ba_data/audio/spazImpact02.ogg": "https://files.ballistica.net/cache/ba1/3a/90/ea63feefde701bb23f983ee033bd", + "build/assets/ba_data/audio/spazImpact03.ogg": "https://files.ballistica.net/cache/ba1/17/bd/e4ac84d7d836341701ed6e3c6735", + "build/assets/ba_data/audio/spazImpact04.ogg": "https://files.ballistica.net/cache/ba1/01/81/be2111efa6afceeb9cb1b32a13b1", + "build/assets/ba_data/audio/spazJump01.ogg": "https://files.ballistica.net/cache/ba1/3f/e0/3d11f1ccad9055474fa60a337271", + "build/assets/ba_data/audio/spazJump02.ogg": "https://files.ballistica.net/cache/ba1/17/5d/64c8180f234c1998099192673e2d", + "build/assets/ba_data/audio/spazJump03.ogg": "https://files.ballistica.net/cache/ba1/d0/70/ff3f62e2712e919d9f1579c35bea", + "build/assets/ba_data/audio/spazJump04.ogg": "https://files.ballistica.net/cache/ba1/ff/82/ba5cd72a7dd257bfeed2fdc7de82", + "build/assets/ba_data/audio/spazOw.ogg": "https://files.ballistica.net/cache/ba1/97/49/1b1591ffabbe6ae9d128ab7cbac7", + "build/assets/ba_data/audio/spazPickup01.ogg": "https://files.ballistica.net/cache/ba1/b4/a5/ac0f2efa686dc8b702e942148e23", + "build/assets/ba_data/audio/spazScream01.ogg": "https://files.ballistica.net/cache/ba1/8d/7c/a47f9aacfb74dd4a5c72b337ef74", + "build/assets/ba_data/audio/splatter.ogg": "https://files.ballistica.net/cache/ba1/3c/5e/e6a0e069c4bca0a7f895496775bc", + "build/assets/ba_data/audio/sportsMusic.ogg": "https://files.ballistica.net/cache/ba1/82/5b/d2a228f5c57cc5bb234afa4930e3", + "build/assets/ba_data/audio/stickyImpact.ogg": "https://files.ballistica.net/cache/ba1/bb/1d/dd0791497125b84e5498868cc7ad", + "build/assets/ba_data/audio/superPunch.ogg": "https://files.ballistica.net/cache/ba1/ad/6c/a21cef3856a40f86525d7da887da", + "build/assets/ba_data/audio/superhero1.ogg": "https://files.ballistica.net/cache/ba1/2f/20/037d40aa01dfc2774ca57a1d9ebe", + "build/assets/ba_data/audio/superhero2.ogg": "https://files.ballistica.net/cache/ba1/08/e6/03331d66944cf6db5a9c313ee3b1", + "build/assets/ba_data/audio/superhero3.ogg": "https://files.ballistica.net/cache/ba1/bc/5a/d605d408163ecf03e2f53724953e", + "build/assets/ba_data/audio/superhero4.ogg": "https://files.ballistica.net/cache/ba1/1d/9a/586899614e536eb4b14b462bb63a", + "build/assets/ba_data/audio/superheroDeath.ogg": "https://files.ballistica.net/cache/ba1/80/7d/046b00dbec9233b2311334310dec", + "build/assets/ba_data/audio/superheroFall.ogg": "https://files.ballistica.net/cache/ba1/5d/60/8b7765a10df8ea7d39789b854313", + "build/assets/ba_data/audio/superheroHit1.ogg": "https://files.ballistica.net/cache/ba1/d4/63/e01f5698367335767a0e14391583", + "build/assets/ba_data/audio/superheroHit2.ogg": "https://files.ballistica.net/cache/ba1/5d/62/51bfcbdf2f20f983e99a5dc2efa9", + "build/assets/ba_data/audio/survivalMusic.ogg": "https://files.ballistica.net/cache/ba1/13/51/f5218883a6285da610fb39092ad3", + "build/assets/ba_data/audio/swip.ogg": "https://files.ballistica.net/cache/ba1/3b/8a/6796906c3b37d76b3aa2d6478dda", + "build/assets/ba_data/audio/swip2.ogg": "https://files.ballistica.net/cache/ba1/85/32/230876a0a63faf643b6ac0935f4b", + "build/assets/ba_data/audio/swish.ogg": "https://files.ballistica.net/cache/ba1/06/10/14dca42b52f9acdeebd8b9ae8c5d", + "build/assets/ba_data/audio/swish2.ogg": "https://files.ballistica.net/cache/ba1/c7/75/4a41476441fce615a17cf28f90bd", + "build/assets/ba_data/audio/swish3.ogg": "https://files.ballistica.net/cache/ba1/87/68/a1db69842f18f87d814a08d2562c", + "build/assets/ba_data/audio/tap.ogg": "https://files.ballistica.net/cache/ba1/65/0a/cc199295961d6f7c0bf6ebe3d099", + "build/assets/ba_data/audio/technoHit01.ogg": "https://files.ballistica.net/cache/ba1/09/8d/04d5a37397582cd826e55d51ede3", + "build/assets/ba_data/audio/tick.ogg": "https://files.ballistica.net/cache/ba1/bb/94/a7506b990622329a5207ffd481fa", + "build/assets/ba_data/audio/ticking.ogg": "https://files.ballistica.net/cache/ba1/13/c3/b547fb7c0f950457c1d9130b6164", + "build/assets/ba_data/audio/tickingCrazy.ogg": "https://files.ballistica.net/cache/ba1/2d/6f/d6ed9eab2149ba6e775b53f5c7bf", + "build/assets/ba_data/audio/toTheDeathMusic.ogg": "https://files.ballistica.net/cache/ba1/a0/50/080c13c597b284b80c17d16234f5", + "build/assets/ba_data/audio/trashRummage.ogg": "https://files.ballistica.net/cache/ba1/56/09/33cab3a1603d4d2ad11acd8d4a0b", + "build/assets/ba_data/audio/victoryMusic.ogg": "https://files.ballistica.net/cache/ba1/a6/0b/cce7b1a2ab00d8ef7972bacb9843", + "build/assets/ba_data/audio/warnBeep.ogg": "https://files.ballistica.net/cache/ba1/57/8b/cc8f942bcca085d095ac24259541", + "build/assets/ba_data/audio/warnBeeps.ogg": "https://files.ballistica.net/cache/ba1/e1/dd/b8f7ab32709f6a53ef4ff7d1fcf0", + "build/assets/ba_data/audio/warrior1.ogg": "https://files.ballistica.net/cache/ba1/d0/d2/e356bd61d151d44bfc38a5d54f8f", + "build/assets/ba_data/audio/warrior2.ogg": "https://files.ballistica.net/cache/ba1/fc/8a/6b72ebc0c723a5bb4db370f733bc", + "build/assets/ba_data/audio/warrior3.ogg": "https://files.ballistica.net/cache/ba1/e6/24/cfa32219e8872295721ca6525a18", + "build/assets/ba_data/audio/warrior4.ogg": "https://files.ballistica.net/cache/ba1/62/dc/45942c5a9a4d42ec3e326591661d", + "build/assets/ba_data/audio/warriorDeath.ogg": "https://files.ballistica.net/cache/ba1/67/61/1ae029292532b672eee37ea77b9f", + "build/assets/ba_data/audio/warriorFall.ogg": "https://files.ballistica.net/cache/ba1/45/8d/a25f1d4bf825e91cd052e8ae7e04", + "build/assets/ba_data/audio/warriorHit1.ogg": "https://files.ballistica.net/cache/ba1/85/52/84a6e0e7d8e0185ff03264a4161b", + "build/assets/ba_data/audio/warriorHit2.ogg": "https://files.ballistica.net/cache/ba1/13/1a/56ac6f094cb6158669c060c8d45b", + "build/assets/ba_data/audio/whenJohnnyComesMarchingHomeMusic.ogg": "https://files.ballistica.net/cache/ba1/8b/41/73102128b1807ea93180b122d627", + "build/assets/ba_data/audio/witch1.ogg": "https://files.ballistica.net/cache/ba1/2c/b0/57c8a097ddb7142153a36a1889de", + "build/assets/ba_data/audio/witch2.ogg": "https://files.ballistica.net/cache/ba1/d0/cc/766d16e2444c6e93a09d5456c517", + "build/assets/ba_data/audio/witch3.ogg": "https://files.ballistica.net/cache/ba1/d2/ed/7c60b2bcf8f0ba5274408f83a1a3", + "build/assets/ba_data/audio/witch4.ogg": "https://files.ballistica.net/cache/ba1/f1/ce/e4e0a75598d7ee2badba1eb17f6a", + "build/assets/ba_data/audio/witchDeath.ogg": "https://files.ballistica.net/cache/ba1/17/77/bbe66562c290e9fe4607a4499428", + "build/assets/ba_data/audio/witchFall.ogg": "https://files.ballistica.net/cache/ba1/7f/6b/0a3db7ce7dcb30943a70af352d06", + "build/assets/ba_data/audio/witchHit1.ogg": "https://files.ballistica.net/cache/ba1/ef/46/742453abce37a1927dabd8607d71", + "build/assets/ba_data/audio/witchHit2.ogg": "https://files.ballistica.net/cache/ba1/04/0a/85f022c0ccc82be483dab5bb6a30", + "build/assets/ba_data/audio/wizard1.ogg": "https://files.ballistica.net/cache/ba1/57/b5/7679c7b7d0235faab50397f64f32", + "build/assets/ba_data/audio/wizard2.ogg": "https://files.ballistica.net/cache/ba1/34/b3/1d8ec1948e1800158c7457e05f37", + "build/assets/ba_data/audio/wizard3.ogg": "https://files.ballistica.net/cache/ba1/f4/16/ac503074c540dd2801b41b5e07fc", + "build/assets/ba_data/audio/wizard4.ogg": "https://files.ballistica.net/cache/ba1/7e/12/103709b79587db6503fa13c4a230", + "build/assets/ba_data/audio/wizardDeath.ogg": "https://files.ballistica.net/cache/ba1/e6/fd/dd5ca5d44f341d987390868b613d", + "build/assets/ba_data/audio/wizardFall.ogg": "https://files.ballistica.net/cache/ba1/f6/07/d9579dfc752251a99983cf7904ff", + "build/assets/ba_data/audio/wizardHit1.ogg": "https://files.ballistica.net/cache/ba1/58/95/5df481ae0e04a7dacfa659427277", + "build/assets/ba_data/audio/wizardHit2.ogg": "https://files.ballistica.net/cache/ba1/ae/f5/bb6a7bcef67426fca41b7fc76e37", + "build/assets/ba_data/audio/woodDebrisFall.ogg": "https://files.ballistica.net/cache/ba1/98/ca/c04bb85360b7df42c29e29f90f38", + "build/assets/ba_data/audio/wrestler1.ogg": "https://files.ballistica.net/cache/ba1/67/9b/a47ec608b5453c7b2b0402674cd7", + "build/assets/ba_data/audio/wrestler2.ogg": "https://files.ballistica.net/cache/ba1/0d/4c/d28686a669023a553333c965d1b8", + "build/assets/ba_data/audio/wrestler3.ogg": "https://files.ballistica.net/cache/ba1/f2/bd/c9b8cdf67fb0116af3a34c37abef", + "build/assets/ba_data/audio/wrestler4.ogg": "https://files.ballistica.net/cache/ba1/9b/09/d254166c41d7f4042306a1343588", + "build/assets/ba_data/audio/wrestlerDeath.ogg": "https://files.ballistica.net/cache/ba1/8b/a5/d2e56c34b3d9392feb857d74f053", + "build/assets/ba_data/audio/wrestlerFall.ogg": "https://files.ballistica.net/cache/ba1/89/cf/d1bc0265f1e3e89f3d31f2ffd03d", + "build/assets/ba_data/audio/wrestlerHit1.ogg": "https://files.ballistica.net/cache/ba1/c4/39/e4452c4ca40e8206da9b7e463ded", + "build/assets/ba_data/audio/wrestlerHit2.ogg": "https://files.ballistica.net/cache/ba1/3c/aa/a0d1188abd7301a93d98d7ace7e9", + "build/assets/ba_data/audio/zoeAttack01.ogg": "https://files.ballistica.net/cache/ba1/b1/10/dc96af2dbaa6618fb9f23a86ef0a", + "build/assets/ba_data/audio/zoeAttack02.ogg": "https://files.ballistica.net/cache/ba1/91/d4/d2d8d2867af72efc7e413387989a", + "build/assets/ba_data/audio/zoeAttack03.ogg": "https://files.ballistica.net/cache/ba1/05/8c/06f976ccac1fb0859986dd2b5a6f", + "build/assets/ba_data/audio/zoeAttack04.ogg": "https://files.ballistica.net/cache/ba1/a8/6b/9fb4a1d62f3fb9224003720cbd52", + "build/assets/ba_data/audio/zoeDeath01.ogg": "https://files.ballistica.net/cache/ba1/4d/2a/86020fee3988a7e8a898a60dd3e7", + "build/assets/ba_data/audio/zoeEff.ogg": "https://files.ballistica.net/cache/ba1/c9/c3/711e8c61bbab1568f5f5a23bb697", + "build/assets/ba_data/audio/zoeFall01.ogg": "https://files.ballistica.net/cache/ba1/0e/2d/f1975ca9f56d315010ba8414674f", + "build/assets/ba_data/audio/zoeImpact01.ogg": "https://files.ballistica.net/cache/ba1/06/aa/f40eb55d6fec3ff37f18b0169221", + "build/assets/ba_data/audio/zoeImpact02.ogg": "https://files.ballistica.net/cache/ba1/46/3d/ad94e05091e09fd29b120e6f6c19", + "build/assets/ba_data/audio/zoeImpact03.ogg": "https://files.ballistica.net/cache/ba1/c4/17/dbacc0b0b8b2818df90cb61f885d", + "build/assets/ba_data/audio/zoeImpact04.ogg": "https://files.ballistica.net/cache/ba1/89/35/8203edb0f4e939ccc338c8a824da", + "build/assets/ba_data/audio/zoeJump01.ogg": "https://files.ballistica.net/cache/ba1/de/37/668f43b941dcb473cd12c5cbed5b", + "build/assets/ba_data/audio/zoeJump02.ogg": "https://files.ballistica.net/cache/ba1/b5/38/3e3cf6f98fae9979bca95898f12e", + "build/assets/ba_data/audio/zoeJump03.ogg": "https://files.ballistica.net/cache/ba1/29/4e/0aa406b574476526b8c48dc6938b", + "build/assets/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/87/22/97ab37683ee4398d3b1f187920c5", + "build/assets/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/05/c2/713c072f4e329a41d96a869bd8f3", + "build/assets/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/7b/22/3a63851f20d331c61f8a9f241e03", + "build/assets/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/3b/0c/e201c5f31d19e0b93c5e5af81a54", + "build/assets/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/f1/33/bee4a5dd13bf8a0b8566e2907876", + "build/assets/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/c0/82/9e3dfce6e766f015c10a2064278e", + "build/assets/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/aa/5e/bebd1031bb1b78c6fcbd18b9f3c0", + "build/assets/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/fe/7f/7b58735c3a52167fb4f8c598d032", + "build/assets/ba_data/data/languages/croatian.json": "https://files.ballistica.net/cache/ba1/fb/02/bf4d9f8db85a43967319916ada0b", + "build/assets/ba_data/data/languages/czech.json": "https://files.ballistica.net/cache/ba1/46/49/8aad582c97def1b45cdae73a7445", + "build/assets/ba_data/data/languages/danish.json": "https://files.ballistica.net/cache/ba1/58/a5/b47cbedf0089f2a91533d35f11c4", + "build/assets/ba_data/data/languages/dutch.json": "https://files.ballistica.net/cache/ba1/8d/eb/98377780a8ca10fad206039a9855", + "build/assets/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/9f/98/06b9c62fc92ef12c1d9db45628ef", + "build/assets/ba_data/data/languages/esperanto.json": "https://files.ballistica.net/cache/ba1/53/13/3bfa0a5c743cd62a6049081ca8bb", + "build/assets/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/4f/f7/3d14933afd164cc01595be97b5db", + "build/assets/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/9d/76/22ed68d506d347d4765398ea24c0", + "build/assets/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/5f/d9/862fc0f299a87be14a53eb4bc712", + "build/assets/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/3c/1c/16c6ca71928720e1e3e0d3fdbe90", + "build/assets/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/93/72/b773ef21cfaaa47c59a606ad7cc9", + "build/assets/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/ab/60/1b460ab890a5dd45bfcbd4ff1d43", + "build/assets/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/e1/5e/65e12a78bdd09a6091c9b9e546fc", + "build/assets/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/2b/f2/a339f8634e8477504f64e6c85f6c", + "build/assets/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/54/31/7327631db1e56b644ab673b8a62b", + "build/assets/ba_data/data/languages/korean.json": "https://files.ballistica.net/cache/ba1/a3/a9/c85ae5914d23c4ca6fb830f7048a", + "build/assets/ba_data/data/languages/malay.json": "https://files.ballistica.net/cache/ba1/32/51/ed33be67091f4d23de7a0531c3d1", + "build/assets/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/b3/7a/0c82beaff067bdeac2914c5e3a95", + "build/assets/ba_data/data/languages/polish.json": "https://files.ballistica.net/cache/ba1/d9/74/93c94e24c480b50058fbf8c29575", + "build/assets/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/8e/c2/839e099cbbc420a7fd04c823bc4f", + "build/assets/ba_data/data/languages/romanian.json": "https://files.ballistica.net/cache/ba1/2d/e9/daebca347a9450266a1c4f9c0fa5", + "build/assets/ba_data/data/languages/russian.json": "https://files.ballistica.net/cache/ba1/f3/d3/8c0e0965d88361c4efcdc4c4389c", + "build/assets/ba_data/data/languages/serbian.json": "https://files.ballistica.net/cache/ba1/5f/a9/3a0bd59533cf6cea2f3463b0f16f", + "build/assets/ba_data/data/languages/slovak.json": "https://files.ballistica.net/cache/ba1/23/a2/7cdf2946254cce58bcd37dd126a5", + "build/assets/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/91/4e/1c5d58f9333fac73048b6f5dfa80", + "build/assets/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/87/cb/692935d8462ea4f0843e0e42582c", + "build/assets/ba_data/data/languages/tamil.json": "https://files.ballistica.net/cache/ba1/7d/1e/7895607192bd7967736cec5bfce6", + "build/assets/ba_data/data/languages/thai.json": "https://files.ballistica.net/cache/ba1/02/8a/a4c87d7e13f280dc6b18848f1058", + "build/assets/ba_data/data/languages/turkish.json": "https://files.ballistica.net/cache/ba1/39/53/c09ecffb5bf04c59ccb9f08ab835", + "build/assets/ba_data/data/languages/ukrainian.json": "https://files.ballistica.net/cache/ba1/f7/55/8362c390108bbe7eef4411c706a9", + "build/assets/ba_data/data/languages/venetian.json": "https://files.ballistica.net/cache/ba1/d4/19/e1308b440afe2eacc05ed310ddc6", + "build/assets/ba_data/data/languages/vietnamese.json": "https://files.ballistica.net/cache/ba1/dc/dd/13c188ea58ee0632e03e0790a879", + "build/assets/ba_data/data/maps/big_g.json": "https://files.ballistica.net/cache/ba1/02/1c/90eb398befa794d5adca8d52f1d7", + "build/assets/ba_data/data/maps/bridgit.json": "https://files.ballistica.net/cache/ba1/ed/92/0ab320e39416f20157707f9ef270", + "build/assets/ba_data/data/maps/courtyard.json": "https://files.ballistica.net/cache/ba1/be/25/4e9664634d8bec130185b774ebb5", + "build/assets/ba_data/data/maps/crag_castle.json": "https://files.ballistica.net/cache/ba1/fb/86/9442585a60fe9209e619dd2f911b", + "build/assets/ba_data/data/maps/doom_shroom.json": "https://files.ballistica.net/cache/ba1/b7/91/1afef8dac4c67360e5d7672ea46c", + "build/assets/ba_data/data/maps/football_stadium.json": "https://files.ballistica.net/cache/ba1/88/34/9facc05212cb6d06037d5cecde5f", + "build/assets/ba_data/data/maps/happy_thoughts.json": "https://files.ballistica.net/cache/ba1/d1/da/6e6369ae0fe02be68b04dd42ace4", + "build/assets/ba_data/data/maps/hockey_stadium.json": "https://files.ballistica.net/cache/ba1/1c/e5/87124b8cb84d6e6901fb5cabd4f5", + "build/assets/ba_data/data/maps/lake_frigid.json": "https://files.ballistica.net/cache/ba1/25/46/d62988351a99dc06633e50f98ded", + "build/assets/ba_data/data/maps/monkey_face.json": "https://files.ballistica.net/cache/ba1/97/f1/e730ad1bfd2d0c99ee72d1ed9c5f", + "build/assets/ba_data/data/maps/rampage.json": "https://files.ballistica.net/cache/ba1/14/b0/6c9a2531cfaeab005a42758d1ec1", + "build/assets/ba_data/data/maps/roundabout.json": "https://files.ballistica.net/cache/ba1/9f/fd/755e7786627ff7b386932dd9cac1", + "build/assets/ba_data/data/maps/step_right_up.json": "https://files.ballistica.net/cache/ba1/5a/73/de0489d9562262a52f1d3fc2a05c", + "build/assets/ba_data/data/maps/the_pad.json": "https://files.ballistica.net/cache/ba1/eb/fd/c76f033f723e073e8f316e922f4b", + "build/assets/ba_data/data/maps/tip_top.json": "https://files.ballistica.net/cache/ba1/df/75/a05201b41533b693e230e5b85303", + "build/assets/ba_data/data/maps/tower_d.json": "https://files.ballistica.net/cache/ba1/ff/d2/bb31fd710e3d8d225ce80f418d49", + "build/assets/ba_data/data/maps/zig_zag.json": "https://files.ballistica.net/cache/ba1/19/93/da9f8c255d6e88085060e6fbff10", + "build/assets/ba_data/fonts/fontSmall0.fdata": "https://files.ballistica.net/cache/ba1/73/a2/8e2243cf1d16f0048e8dd4b5f7f3", + "build/assets/ba_data/fonts/fontSmall1.fdata": "https://files.ballistica.net/cache/ba1/93/64/2fa8008cd40467fa602f941add95", + "build/assets/ba_data/fonts/fontSmall2.fdata": "https://files.ballistica.net/cache/ba1/ea/09/d6eef0745d3a546b5c37d67c4497", + "build/assets/ba_data/fonts/fontSmall3.fdata": "https://files.ballistica.net/cache/ba1/28/b3/f2eedd6462b492aaec92ed319089", + "build/assets/ba_data/fonts/fontSmall4.fdata": "https://files.ballistica.net/cache/ba1/39/0a/59f4debc14352dfec5988092d937", + "build/assets/ba_data/fonts/fontSmall5.fdata": "https://files.ballistica.net/cache/ba1/d0/30/d916244383318ec62f02f4bacf13", + "build/assets/ba_data/fonts/fontSmall6.fdata": "https://files.ballistica.net/cache/ba1/e3/1c/2d5e8e91f38cd05291086693bf31", + "build/assets/ba_data/fonts/fontSmall7.fdata": "https://files.ballistica.net/cache/ba1/75/8b/26b8e5e6aa9f2a7c9c3aaf69bec4", + "build/assets/ba_data/meshes/achievementOutline.bob": "https://files.ballistica.net/cache/ba1/78/b1/58ed9e1ba9cd3db3497c69080b00", + "build/assets/ba_data/meshes/actionButtonBottom.bob": "https://files.ballistica.net/cache/ba1/3b/f8/0c30d09e33e3df8781df98223745", + "build/assets/ba_data/meshes/actionButtonLeft.bob": "https://files.ballistica.net/cache/ba1/64/6d/6cd269637a002157d77d5b08c0b7", + "build/assets/ba_data/meshes/actionButtonRight.bob": "https://files.ballistica.net/cache/ba1/a0/00/d5682c7381066086cc5bde56401d", + "build/assets/ba_data/meshes/actionButtonTop.bob": "https://files.ballistica.net/cache/ba1/f4/ed/a489444007d237409beb4bb0f9b1", + "build/assets/ba_data/meshes/actionHeroForeArm.bob": "https://files.ballistica.net/cache/ba1/48/e9/89198f799c8ea79b8ea573893ff7", + "build/assets/ba_data/meshes/actionHeroHand.bob": "https://files.ballistica.net/cache/ba1/82/ef/f37ca063b933498d3e2faef6d0c5", + "build/assets/ba_data/meshes/actionHeroHead.bob": "https://files.ballistica.net/cache/ba1/72/ef/385f21bf5d7d830db33a172e8991", + "build/assets/ba_data/meshes/actionHeroLowerLeg.bob": "https://files.ballistica.net/cache/ba1/2c/e8/1d594a839112b76511aebb7622d7", + "build/assets/ba_data/meshes/actionHeroPelvis.bob": "https://files.ballistica.net/cache/ba1/f4/ac/a23cb431eaf7c6c94ceb82ee12d0", + "build/assets/ba_data/meshes/actionHeroToes.bob": "https://files.ballistica.net/cache/ba1/4e/50/7054b0456223372496c0224acf08", + "build/assets/ba_data/meshes/actionHeroTorso.bob": "https://files.ballistica.net/cache/ba1/0d/fc/61d4c0c811e247566e11d48557f7", + "build/assets/ba_data/meshes/actionHeroUpperArm.bob": "https://files.ballistica.net/cache/ba1/54/94/ceeb1bcc25e28750fd721945618a", + "build/assets/ba_data/meshes/actionHeroUpperLeg.bob": "https://files.ballistica.net/cache/ba1/53/81/37063cb392ccb7c7cc2d1e68da8f", + "build/assets/ba_data/meshes/agentForeArm.bob": "https://files.ballistica.net/cache/ba1/f1/94/0e41a7c12c2a91273ec4bb79a38c", + "build/assets/ba_data/meshes/agentHand.bob": "https://files.ballistica.net/cache/ba1/af/9c/218d24df785bd28308e9d277ee77", + "build/assets/ba_data/meshes/agentHead.bob": "https://files.ballistica.net/cache/ba1/e8/d1/6e8728e99181fc568085b5171ed7", + "build/assets/ba_data/meshes/agentLowerLeg.bob": "https://files.ballistica.net/cache/ba1/c4/a8/11b20ab514cd7f79800f78fd2f01", + "build/assets/ba_data/meshes/agentPelvis.bob": "https://files.ballistica.net/cache/ba1/ad/0b/7df5c693c364f09db3c9131c4c6e", + "build/assets/ba_data/meshes/agentToes.bob": "https://files.ballistica.net/cache/ba1/f9/93/c7f63177e1c998eee2e86894bdf0", + "build/assets/ba_data/meshes/agentTorso.bob": "https://files.ballistica.net/cache/ba1/4f/53/9d4666f3f1e13a7cf4ed6b8029dd", + "build/assets/ba_data/meshes/agentUpperArm.bob": "https://files.ballistica.net/cache/ba1/b3/c7/243c247af8ddd4f9d2a907d0a97c", + "build/assets/ba_data/meshes/agentUpperLeg.bob": "https://files.ballistica.net/cache/ba1/49/44/bbf85b01e3816056e4a93e878517", + "build/assets/ba_data/meshes/aliForeArm.bob": "https://files.ballistica.net/cache/ba1/12/d9/f63ade93ca82c6902978fbaee096", + "build/assets/ba_data/meshes/aliHand.bob": "https://files.ballistica.net/cache/ba1/0b/4f/d1998d952783ba163db3a760c6fb", + "build/assets/ba_data/meshes/aliHead.bob": "https://files.ballistica.net/cache/ba1/2d/83/27a65b5a3ff7af5d5b2f3f85b896", + "build/assets/ba_data/meshes/aliLowerLeg.bob": "https://files.ballistica.net/cache/ba1/cf/b7/590a3573983749355e1faba3ec66", + "build/assets/ba_data/meshes/aliPelvis.bob": "https://files.ballistica.net/cache/ba1/9c/ad/7c3fd01dabff0d3d2a4a4ef5f33b", + "build/assets/ba_data/meshes/aliToes.bob": "https://files.ballistica.net/cache/ba1/32/73/fabe64f8511cee392a5598186f68", + "build/assets/ba_data/meshes/aliTorso.bob": "https://files.ballistica.net/cache/ba1/4f/74/f6712af5a8b294d1e3a705977bc6", + "build/assets/ba_data/meshes/aliUpperArm.bob": "https://files.ballistica.net/cache/ba1/bc/ae/df5a212f2fa54df987017d8cf36d", + "build/assets/ba_data/meshes/aliUpperLeg.bob": "https://files.ballistica.net/cache/ba1/88/7e/806f18f21230c3572e40a5a37861", + "build/assets/ba_data/meshes/alienForeArm.bob": "https://files.ballistica.net/cache/ba1/31/6e/f026afe8795d4d234f40a63a7e4b", + "build/assets/ba_data/meshes/alienHand.bob": "https://files.ballistica.net/cache/ba1/ba/fc/8bde4af36464116b36f42c3b3d7b", + "build/assets/ba_data/meshes/alienHead.bob": "https://files.ballistica.net/cache/ba1/98/4b/b4e753c51140940f0beb21be00b1", + "build/assets/ba_data/meshes/alienLowerLeg.bob": "https://files.ballistica.net/cache/ba1/98/df/f9b4e1109339615dc36bd13c52c0", + "build/assets/ba_data/meshes/alienPelvis.bob": "https://files.ballistica.net/cache/ba1/e8/b5/0534dd4b0634eda155d7e4856a42", + "build/assets/ba_data/meshes/alienToes.bob": "https://files.ballistica.net/cache/ba1/eb/15/7a4367378eac73bafec9156d43dd", + "build/assets/ba_data/meshes/alienTorso.bob": "https://files.ballistica.net/cache/ba1/2f/f7/d7dcc0ff3ea8761098ef81c404fd", + "build/assets/ba_data/meshes/alienUpperArm.bob": "https://files.ballistica.net/cache/ba1/35/ac/d0e56a7ca3c7c2be1eca27bb849f", + "build/assets/ba_data/meshes/alienUpperLeg.bob": "https://files.ballistica.net/cache/ba1/d8/f5/a1eede68aab9040af9dd7139089b", + "build/assets/ba_data/meshes/alwaysLandBG.bob": "https://files.ballistica.net/cache/ba1/2a/5c/7fa8e95104e98429843b34300087", + "build/assets/ba_data/meshes/alwaysLandLevel.bob": "https://files.ballistica.net/cache/ba1/13/88/1c5c23fb4164aceeb440383fbb30", + "build/assets/ba_data/meshes/alwaysLandLevelBottom.bob": "https://files.ballistica.net/cache/ba1/9f/65/def03897c69a822dff5f000202d8", + "build/assets/ba_data/meshes/alwaysLandLevelCollide.cob": "https://files.ballistica.net/cache/ba1/96/34/f7bf867072fe6b1ef229bbac2516", + "build/assets/ba_data/meshes/alwaysLandVRFillMound.bob": "https://files.ballistica.net/cache/ba1/08/2a/9a264c75d6ed58827d1cafc00b3f", + "build/assets/ba_data/meshes/angryComputerTransparent.bob": "https://files.ballistica.net/cache/ba1/68/e8/075b5df01435ce61bf03f7d12438", + "build/assets/ba_data/meshes/arrowBack.bob": "https://files.ballistica.net/cache/ba1/5f/71/c5be36d5432ca7938362dc9f50be", + "build/assets/ba_data/meshes/arrowFront.bob": "https://files.ballistica.net/cache/ba1/43/0c/a7443951f1b473ac85fa07da971a", + "build/assets/ba_data/meshes/assassinForeArm.bob": "https://files.ballistica.net/cache/ba1/4e/79/f9f7d4928c68ffea368b50726cee", + "build/assets/ba_data/meshes/assassinHand.bob": "https://files.ballistica.net/cache/ba1/50/2e/545758b09a884d3b1a2443ecf367", + "build/assets/ba_data/meshes/assassinHead.bob": "https://files.ballistica.net/cache/ba1/21/b5/4cacedb3129125bbe3c11e77dfdb", + "build/assets/ba_data/meshes/assassinLowerLeg.bob": "https://files.ballistica.net/cache/ba1/b5/e9/4cf530c6cf150ac8d46649b6134c", + "build/assets/ba_data/meshes/assassinPelvis.bob": "https://files.ballistica.net/cache/ba1/c1/29/dae642593678117ca1a090d32c9b", + "build/assets/ba_data/meshes/assassinToes.bob": "https://files.ballistica.net/cache/ba1/d4/d7/cddc3792822c2e0422568dfe1816", + "build/assets/ba_data/meshes/assassinTorso.bob": "https://files.ballistica.net/cache/ba1/b9/36/23151dd0d0621fe78ceea5634dea", + "build/assets/ba_data/meshes/assassinUpperArm.bob": "https://files.ballistica.net/cache/ba1/4c/56/70a7b7fe6a4e75ad532fcca6f194", + "build/assets/ba_data/meshes/assassinUpperLeg.bob": "https://files.ballistica.net/cache/ba1/91/27/c290f0d552aef9327443c634f812", + "build/assets/ba_data/meshes/bearForeArm.bob": "https://files.ballistica.net/cache/ba1/d9/84/8998db9df0884fa32e591f32cfdd", + "build/assets/ba_data/meshes/bearHand.bob": "https://files.ballistica.net/cache/ba1/79/71/4688820584398169c7ace2d2365b", + "build/assets/ba_data/meshes/bearHead.bob": "https://files.ballistica.net/cache/ba1/13/c1/c9193c67ef699eab5cd5ce023b8a", + "build/assets/ba_data/meshes/bearLowerLeg.bob": "https://files.ballistica.net/cache/ba1/4a/5b/5642d946fd13baf5502bc257f6ff", + "build/assets/ba_data/meshes/bearPelvis.bob": "https://files.ballistica.net/cache/ba1/2a/6b/e60715ea64d510648b0b181ce2b7", + "build/assets/ba_data/meshes/bearToes.bob": "https://files.ballistica.net/cache/ba1/83/bd/e44b7a3ac8961304e94a2fa97c74", + "build/assets/ba_data/meshes/bearTorso.bob": "https://files.ballistica.net/cache/ba1/5c/dd/bd442e9f8555be0a0e2687b7564e", + "build/assets/ba_data/meshes/bearUpperArm.bob": "https://files.ballistica.net/cache/ba1/ba/3c/8a0c2f2f3c2b34f632ad516fe01f", + "build/assets/ba_data/meshes/bearUpperLeg.bob": "https://files.ballistica.net/cache/ba1/9d/3a/6a4d5efa795f086994a01e04d2df", + "build/assets/ba_data/meshes/bigG.bob": "https://files.ballistica.net/cache/ba1/0b/68/06819832d1003f3449b062b663c9", + "build/assets/ba_data/meshes/bigGBottom.bob": "https://files.ballistica.net/cache/ba1/16/d0/c2eb14bfbcf7f3dff611bca72c18", + "build/assets/ba_data/meshes/bigGBumper.cob": "https://files.ballistica.net/cache/ba1/62/10/67bb191b614d7fc376557c85758d", + "build/assets/ba_data/meshes/bigGCollide.cob": "https://files.ballistica.net/cache/ba1/2b/5f/c619f2388f7193907ad5f7c00f22", + "build/assets/ba_data/meshes/bomb.bob": "https://files.ballistica.net/cache/ba1/5a/f5/e539299c62261c377f6c10605c71", + "build/assets/ba_data/meshes/bombSticky.bob": "https://files.ballistica.net/cache/ba1/b6/34/faa80e9f8cebfd7e4c3ea6fa5632", + "build/assets/ba_data/meshes/bonesForeArm.bob": "https://files.ballistica.net/cache/ba1/4f/4f/3f21c7fd3e401c64c702865a3b6d", + "build/assets/ba_data/meshes/bonesHand.bob": "https://files.ballistica.net/cache/ba1/5f/e4/236b3da96995cbbcc8a0595a4a67", + "build/assets/ba_data/meshes/bonesHead.bob": "https://files.ballistica.net/cache/ba1/8e/cc/dcccbe1645fc9a439fdcd3154434", + "build/assets/ba_data/meshes/bonesLowerLeg.bob": "https://files.ballistica.net/cache/ba1/65/74/89fdc8b451907979f469a48af0b8", + "build/assets/ba_data/meshes/bonesPelvis.bob": "https://files.ballistica.net/cache/ba1/12/18/c7a9ac1c0e55a0cec2ac765bfb3c", + "build/assets/ba_data/meshes/bonesToes.bob": "https://files.ballistica.net/cache/ba1/b7/b6/1fe2319ce16856344973872fac4b", + "build/assets/ba_data/meshes/bonesTorso.bob": "https://files.ballistica.net/cache/ba1/15/ae/4a8368225c6a2f98ee6f6c128ba3", + "build/assets/ba_data/meshes/bonesUpperArm.bob": "https://files.ballistica.net/cache/ba1/78/5a/67594957e875a04aed672a2fa8d9", + "build/assets/ba_data/meshes/bonesUpperLeg.bob": "https://files.ballistica.net/cache/ba1/55/5d/9ebc219ce767eeb8f3cca6c4b28d", + "build/assets/ba_data/meshes/box.bob": "https://files.ballistica.net/cache/ba1/09/c0/9174d98c0c92333d195eea26842d", + "build/assets/ba_data/meshes/boxingGlove.bob": "https://files.ballistica.net/cache/ba1/45/dc/be158c2c45c1263d49042a7aae7c", + "build/assets/ba_data/meshes/bridgitLevelBottom.bob": "https://files.ballistica.net/cache/ba1/07/90/9c5c5f3a7a5f2b297d718b722768", + "build/assets/ba_data/meshes/bridgitLevelCollide.cob": "https://files.ballistica.net/cache/ba1/3b/ba/6183474c4079853e47e79bf62fb3", + "build/assets/ba_data/meshes/bridgitLevelRailingCollide.cob": "https://files.ballistica.net/cache/ba1/b3/fe/f9c59439ab8ec9e4927683c8a782", + "build/assets/ba_data/meshes/bridgitLevelTop.bob": "https://files.ballistica.net/cache/ba1/e7/5c/bf39261d20e8c23ffe53da63d28f", + "build/assets/ba_data/meshes/bunnyForeArm.bob": "https://files.ballistica.net/cache/ba1/e0/c5/51ecbe030e8334bdd5fa146111da", + "build/assets/ba_data/meshes/bunnyHand.bob": "https://files.ballistica.net/cache/ba1/d0/68/2fba3bb5987f9c4645118e279757", + "build/assets/ba_data/meshes/bunnyHead.bob": "https://files.ballistica.net/cache/ba1/59/d0/e0279d01db7a43175816695e485e", + "build/assets/ba_data/meshes/bunnyLowerLeg.bob": "https://files.ballistica.net/cache/ba1/ef/7a/4653ba3f2c5bebf21586745e8cd1", + "build/assets/ba_data/meshes/bunnyPelvis.bob": "https://files.ballistica.net/cache/ba1/25/72/4c7d0a081c5d4f4d4ad0ddfb572d", + "build/assets/ba_data/meshes/bunnyToes.bob": "https://files.ballistica.net/cache/ba1/3d/4d/fd4b8a74ff9a94ad77d75fd3ef5b", + "build/assets/ba_data/meshes/bunnyTorso.bob": "https://files.ballistica.net/cache/ba1/ba/03/7311cf28058fb5645ab04e07ccea", + "build/assets/ba_data/meshes/bunnyUpperArm.bob": "https://files.ballistica.net/cache/ba1/6f/d6/469fe4255824f40ac401b75807c7", + "build/assets/ba_data/meshes/bunnyUpperLeg.bob": "https://files.ballistica.net/cache/ba1/2e/0e/bd04ca9145a009a040f750fef4f7", + "build/assets/ba_data/meshes/buttonBackOpaque.bob": "https://files.ballistica.net/cache/ba1/92/76/7889bc788bcbfad6b7f66a48621e", + "build/assets/ba_data/meshes/buttonBackSmallOpaque.bob": "https://files.ballistica.net/cache/ba1/11/df/5b8c6f6771fb3ec40733bdf87fc4", + "build/assets/ba_data/meshes/buttonBackSmallTransparent.bob": "https://files.ballistica.net/cache/ba1/dd/8e/166a2f268190cf7cf29d31d25624", + "build/assets/ba_data/meshes/buttonBackTransparent.bob": "https://files.ballistica.net/cache/ba1/ae/db/45fff9ed76b48d42e21bf3a2ad0d", + "build/assets/ba_data/meshes/buttonLargeOpaque.bob": "https://files.ballistica.net/cache/ba1/76/3f/26112a6c6820e937190aac126ee8", + "build/assets/ba_data/meshes/buttonLargeTransparent.bob": "https://files.ballistica.net/cache/ba1/46/af/a4687dbf7a453235185d5d4c97e5", + "build/assets/ba_data/meshes/buttonLargerOpaque.bob": "https://files.ballistica.net/cache/ba1/2f/92/5a1ff624a5ac7beacb0bd6c154f8", + "build/assets/ba_data/meshes/buttonLargerTransparent.bob": "https://files.ballistica.net/cache/ba1/92/b0/574059e987a5140a11bd73713ddf", + "build/assets/ba_data/meshes/buttonMediumOpaque.bob": "https://files.ballistica.net/cache/ba1/80/08/170f53be60d02afa7f68583e2c46", + "build/assets/ba_data/meshes/buttonMediumTransparent.bob": "https://files.ballistica.net/cache/ba1/1a/68/568397a2d8bba20e44e3bb57c6e8", + "build/assets/ba_data/meshes/buttonNull.bob": "https://files.ballistica.net/cache/ba1/a3/78/a8bcb120cae92d503fd9c36f53d9", + "build/assets/ba_data/meshes/buttonSmallOpaque.bob": "https://files.ballistica.net/cache/ba1/cd/fc/e384f1f1fcbdd617aa50a3f6077c", + "build/assets/ba_data/meshes/buttonSmallTransparent.bob": "https://files.ballistica.net/cache/ba1/50/8f/abb83b55ff5564655e42edf47715", + "build/assets/ba_data/meshes/buttonSquareOpaque.bob": "https://files.ballistica.net/cache/ba1/1c/53/2aeb60e7b231836bb9fb2fedc0c2", + "build/assets/ba_data/meshes/buttonSquareTransparent.bob": "https://files.ballistica.net/cache/ba1/38/4b/464e4458b334618bcc32d1056f4d", + "build/assets/ba_data/meshes/buttonTabOpaque.bob": "https://files.ballistica.net/cache/ba1/6a/8d/509c8cd51afbd3225e228f9ba7bb", + "build/assets/ba_data/meshes/buttonTabTransparent.bob": "https://files.ballistica.net/cache/ba1/2d/c2/f5f31ce6beae9eac81b626e66009", + "build/assets/ba_data/meshes/checkTransparent.bob": "https://files.ballistica.net/cache/ba1/42/87/c9e00391bf084e3e78f35fd46afd", + "build/assets/ba_data/meshes/courtyardLevel.bob": "https://files.ballistica.net/cache/ba1/b5/ed/5a667beec21c06f1ebc7f6c58894", + "build/assets/ba_data/meshes/courtyardLevelBottom.bob": "https://files.ballistica.net/cache/ba1/45/30/a5c262fd67a0d485ac1f529b8de1", + "build/assets/ba_data/meshes/courtyardLevelCollide.cob": "https://files.ballistica.net/cache/ba1/ea/99/35dc78cad906b02dc407094c7471", + "build/assets/ba_data/meshes/courtyardPlayerWall.cob": "https://files.ballistica.net/cache/ba1/ff/88/0f077a5f490533c5ae2ecc0240f5", + "build/assets/ba_data/meshes/cowboyForeArm.bob": "https://files.ballistica.net/cache/ba1/f0/71/2de879f1ddd7efce5c5e712d463d", + "build/assets/ba_data/meshes/cowboyHand.bob": "https://files.ballistica.net/cache/ba1/f9/e8/4cc941d0e09a137598ba2adb9f58", + "build/assets/ba_data/meshes/cowboyHead.bob": "https://files.ballistica.net/cache/ba1/ff/f8/6084f69a80f63392397f3cd22130", + "build/assets/ba_data/meshes/cowboyLowerLeg.bob": "https://files.ballistica.net/cache/ba1/0c/1f/c3181bbe6e55898410eef5752db0", + "build/assets/ba_data/meshes/cowboyPelvis.bob": "https://files.ballistica.net/cache/ba1/9c/2f/407e53e3b1336f84e437299fec2b", + "build/assets/ba_data/meshes/cowboyToes.bob": "https://files.ballistica.net/cache/ba1/9d/3e/27997ca4ecaeb1937576523d1caf", + "build/assets/ba_data/meshes/cowboyTorso.bob": "https://files.ballistica.net/cache/ba1/98/c8/c362996965bc0220f1c509031356", + "build/assets/ba_data/meshes/cowboyUpperArm.bob": "https://files.ballistica.net/cache/ba1/56/74/a25868e05fce62b850bd5a71d012", + "build/assets/ba_data/meshes/cowboyUpperLeg.bob": "https://files.ballistica.net/cache/ba1/65/6a/265cc9bf33a12d361db78286b357", + "build/assets/ba_data/meshes/cragCastleLevel.bob": "https://files.ballistica.net/cache/ba1/d9/a0/76a4817c6fbae9bb4b1857eaea3e", + "build/assets/ba_data/meshes/cragCastleLevelBottom.bob": "https://files.ballistica.net/cache/ba1/28/bb/45eca041210b9d40fe604191e7b0", + "build/assets/ba_data/meshes/cragCastleLevelBumper.cob": "https://files.ballistica.net/cache/ba1/2f/03/8494c62361516c77009b948e23f6", + "build/assets/ba_data/meshes/cragCastleLevelCollide.cob": "https://files.ballistica.net/cache/ba1/a7/e4/f805cb3326ae17a80e61b785a9bd", + "build/assets/ba_data/meshes/cragCastleVRFillMound.bob": "https://files.ballistica.net/cache/ba1/e5/42/43a588e9b5d402e475e870c4a0a6", + "build/assets/ba_data/meshes/crossOut.bob": "https://files.ballistica.net/cache/ba1/4b/53/7fd41f5a5fb0660a5679867c7b55", + "build/assets/ba_data/meshes/currencyMeter.bob": "https://files.ballistica.net/cache/ba1/53/36/f3756ef61943941edc52f2047387", + "build/assets/ba_data/meshes/currencyPlusButton.bob": "https://files.ballistica.net/cache/ba1/0a/24/010fd0199d9a9e43b8a72ac06f02", + "build/assets/ba_data/meshes/cyborgForeArm.bob": "https://files.ballistica.net/cache/ba1/a0/0f/782a4306ff135a13ba7ce35f77db", + "build/assets/ba_data/meshes/cyborgHand.bob": "https://files.ballistica.net/cache/ba1/b1/b8/ff8c4752223023047ab827098c90", + "build/assets/ba_data/meshes/cyborgHead.bob": "https://files.ballistica.net/cache/ba1/a1/09/3af3addbbacec75fb92c17980bf0", + "build/assets/ba_data/meshes/cyborgLowerLeg.bob": "https://files.ballistica.net/cache/ba1/46/1a/0584b03607d55005edf39dfb71b3", + "build/assets/ba_data/meshes/cyborgPelvis.bob": "https://files.ballistica.net/cache/ba1/cf/85/b84c7d430fdae884b5555b47665d", + "build/assets/ba_data/meshes/cyborgToes.bob": "https://files.ballistica.net/cache/ba1/55/41/de1326e169f9bda3cbdda4fa0d54", + "build/assets/ba_data/meshes/cyborgTorso.bob": "https://files.ballistica.net/cache/ba1/6b/81/37949384bb01ebe4e22e69766f5f", + "build/assets/ba_data/meshes/cyborgUpperArm.bob": "https://files.ballistica.net/cache/ba1/79/c3/350f8e34a9824ea67891c4645757", + "build/assets/ba_data/meshes/cyborgUpperLeg.bob": "https://files.ballistica.net/cache/ba1/ab/25/3ee6572e04befb328f6d888b82a1", + "build/assets/ba_data/meshes/cylinder.bob": "https://files.ballistica.net/cache/ba1/91/62/fec93d45961e4fc7cc7409bc339a", + "build/assets/ba_data/meshes/doomShroomBG.bob": "https://files.ballistica.net/cache/ba1/c4/40/8ee26353a239acbe767096e6c5bf", + "build/assets/ba_data/meshes/doomShroomLevel.bob": "https://files.ballistica.net/cache/ba1/6d/af/22716e09867e3520a6ffeebedccf", + "build/assets/ba_data/meshes/doomShroomLevelCollide.cob": "https://files.ballistica.net/cache/ba1/6e/9f/7af9ed8b1009a843e326d703bbac", + "build/assets/ba_data/meshes/doomShroomStem.bob": "https://files.ballistica.net/cache/ba1/49/ad/ab259313643528eeeeade2d1bccf", + "build/assets/ba_data/meshes/doomShroomStemCollide.cob": "https://files.ballistica.net/cache/ba1/11/03/b83b176ed8213efe5d38d02ab4f5", + "build/assets/ba_data/meshes/doomShroomVRFill.bob": "https://files.ballistica.net/cache/ba1/ae/11/bc045d1aafd1f697dc931daa1e8a", + "build/assets/ba_data/meshes/egg.bob": "https://files.ballistica.net/cache/ba1/4a/80/c7f795533fc8dfe0e091cb85cc74", + "build/assets/ba_data/meshes/eyeBall.bob": "https://files.ballistica.net/cache/ba1/ae/c1/227fccfeaef31f481b41f6761208", + "build/assets/ba_data/meshes/eyeBallIris.bob": "https://files.ballistica.net/cache/ba1/19/e4/9867735a4bde5acf1bec2c88e7fc", + "build/assets/ba_data/meshes/eyeLid.bob": "https://files.ballistica.net/cache/ba1/ed/ca/6f02e93893a3e2d2adb04146ac21", + "build/assets/ba_data/meshes/flagPole.bob": "https://files.ballistica.net/cache/ba1/3b/36/f7fa246da30482d0c94f06c163fa", + "build/assets/ba_data/meshes/flagStand.bob": "https://files.ballistica.net/cache/ba1/27/7a/4edbc5a49a36cc2961a9b968c441", + "build/assets/ba_data/meshes/flash.bob": "https://files.ballistica.net/cache/ba1/28/71/584e09e2ba30154d67948444d0b4", + "build/assets/ba_data/meshes/footballStadium.bob": "https://files.ballistica.net/cache/ba1/9c/c9/119225eaa521b82a9a148a5dcdfa", + "build/assets/ba_data/meshes/footballStadiumCollide.cob": "https://files.ballistica.net/cache/ba1/7e/24/95721e9aaadaaf7b742ec2da4c8f", + "build/assets/ba_data/meshes/footballStadiumVRFill.bob": "https://files.ballistica.net/cache/ba1/fd/ad/958daaa6a35b975e9c4c3f1abcc5", + "build/assets/ba_data/meshes/frameInset.bob": "https://files.ballistica.net/cache/ba1/f1/82/79eeadef5090266dd23524567fe4", + "build/assets/ba_data/meshes/frostyForeArm.bob": "https://files.ballistica.net/cache/ba1/30/a2/c34b176452f3e4372b68e0b6a9a4", + "build/assets/ba_data/meshes/frostyHand.bob": "https://files.ballistica.net/cache/ba1/14/ec/a6a7ee0ade214cd4c37ddb1bf60a", + "build/assets/ba_data/meshes/frostyHead.bob": "https://files.ballistica.net/cache/ba1/37/3d/ec849f028e28dd25b756b39e53a9", + "build/assets/ba_data/meshes/frostyLowerLeg.bob": "https://files.ballistica.net/cache/ba1/e3/7a/3cee40823b665f73712876e3b147", + "build/assets/ba_data/meshes/frostyPelvis.bob": "https://files.ballistica.net/cache/ba1/30/ff/0dd1b17b9a9568e0cc4766b72b6c", + "build/assets/ba_data/meshes/frostyToes.bob": "https://files.ballistica.net/cache/ba1/44/c9/e272515d085c88dff481c379e6df", + "build/assets/ba_data/meshes/frostyTorso.bob": "https://files.ballistica.net/cache/ba1/56/6b/d400985b1c97b2b35e667f9ea755", + "build/assets/ba_data/meshes/frostyUpperArm.bob": "https://files.ballistica.net/cache/ba1/39/91/c068725897bc0f75a7abd99926ce", + "build/assets/ba_data/meshes/frostyUpperLeg.bob": "https://files.ballistica.net/cache/ba1/99/28/2b4f52bf460a009dfe13725fc022", + "build/assets/ba_data/meshes/gladiatorForeArm.bob": "https://files.ballistica.net/cache/ba1/8c/42/776c21e59ffd5a1dad3f0c591eb7", + "build/assets/ba_data/meshes/gladiatorHand.bob": "https://files.ballistica.net/cache/ba1/50/6f/1f42ce8238407f358c2f9ab4083f", + "build/assets/ba_data/meshes/gladiatorHead.bob": "https://files.ballistica.net/cache/ba1/e8/48/1798866f106f860b3b2e95e35817", + "build/assets/ba_data/meshes/gladiatorLowerLeg.bob": "https://files.ballistica.net/cache/ba1/7d/69/79298eea009aadfe25547523db63", + "build/assets/ba_data/meshes/gladiatorPelvis.bob": "https://files.ballistica.net/cache/ba1/78/34/c1078d272cff3c1f4dc65a6bdcbd", + "build/assets/ba_data/meshes/gladiatorToes.bob": "https://files.ballistica.net/cache/ba1/f4/e7/b048f533aff6a245142cba193339", + "build/assets/ba_data/meshes/gladiatorTorso.bob": "https://files.ballistica.net/cache/ba1/fb/20/f137cb40ae4b266e07b81547d271", + "build/assets/ba_data/meshes/gladiatorUpperArm.bob": "https://files.ballistica.net/cache/ba1/3c/39/1cd0b59591423808ae3290bab5fc", + "build/assets/ba_data/meshes/gladiatorUpperLeg.bob": "https://files.ballistica.net/cache/ba1/74/1e/b383ce80c0a3350e57d7cfd73946", + "build/assets/ba_data/meshes/hairTuft1.bob": "https://files.ballistica.net/cache/ba1/ac/34/e269bf98d26ffc0300ab9c3564d5", + "build/assets/ba_data/meshes/hairTuft1b.bob": "https://files.ballistica.net/cache/ba1/5c/d0/9d6a00c637ed0bb2c18ad87529b2", + "build/assets/ba_data/meshes/hairTuft2.bob": "https://files.ballistica.net/cache/ba1/62/85/9e63ec6ea2a8ac70f5b61aa10443", + "build/assets/ba_data/meshes/hairTuft3.bob": "https://files.ballistica.net/cache/ba1/63/25/2fad67ae24557e7f6cec5cfda1dd", + "build/assets/ba_data/meshes/hairTuft4.bob": "https://files.ballistica.net/cache/ba1/80/cf/540970d8b85f19a2e43f7fc7a82d", + "build/assets/ba_data/meshes/heartOpaque.bob": "https://files.ballistica.net/cache/ba1/50/45/45b7d67a768eb8c58447d7139092", + "build/assets/ba_data/meshes/heartTransparent.bob": "https://files.ballistica.net/cache/ba1/9d/e3/08db7ec7965a1a4f7b11d62a686a", + "build/assets/ba_data/meshes/hockeyStadiumCollide.cob": "https://files.ballistica.net/cache/ba1/d1/2e/28e9cb5ea38e649e600ddb93565a", + "build/assets/ba_data/meshes/hockeyStadiumInner.bob": "https://files.ballistica.net/cache/ba1/fe/33/363972b00bd4041d107252b6b79a", + "build/assets/ba_data/meshes/hockeyStadiumOuter.bob": "https://files.ballistica.net/cache/ba1/5a/1b/c31072a3e28ae648f3c4b61b1824", + "build/assets/ba_data/meshes/hockeyStadiumStands.bob": "https://files.ballistica.net/cache/ba1/ba/e1/3e600b3c6c56f48f747a1e5e92ea", + "build/assets/ba_data/meshes/image16x1.bob": "https://files.ballistica.net/cache/ba1/2e/5a/0f9b44ca826443f8466477f13660", + "build/assets/ba_data/meshes/image1x1.bob": "https://files.ballistica.net/cache/ba1/56/b7/71ee477cf965cf049966388f5506", + "build/assets/ba_data/meshes/image1x1FullScreen.bob": "https://files.ballistica.net/cache/ba1/f8/6b/352157bce51cf90e2e104e08994d", + "build/assets/ba_data/meshes/image1x1VRFullScreen.bob": "https://files.ballistica.net/cache/ba1/09/a6/912810076d16e59a1e996cbd79fd", + "build/assets/ba_data/meshes/image2x1.bob": "https://files.ballistica.net/cache/ba1/0c/99/3d6470af64e18b1c6b456b0ae6b4", + "build/assets/ba_data/meshes/image2x1Vertical.bob": "https://files.ballistica.net/cache/ba1/eb/f4/36a6def37b50eab93d38f25c0684", + "build/assets/ba_data/meshes/image4x1.bob": "https://files.ballistica.net/cache/ba1/a5/7c/b17bdff2cf4659a5424e06dfdd87", + "build/assets/ba_data/meshes/impactBomb.bob": "https://files.ballistica.net/cache/ba1/85/8d/39f618765a5cee459022b664ed91", + "build/assets/ba_data/meshes/jackForeArm.bob": "https://files.ballistica.net/cache/ba1/ac/ff/120f2b2174abef43715209211ecc", + "build/assets/ba_data/meshes/jackHand.bob": "https://files.ballistica.net/cache/ba1/ca/a5/b2cb952a60457bacd1b9c5436ab8", + "build/assets/ba_data/meshes/jackHead.bob": "https://files.ballistica.net/cache/ba1/31/26/af85e1ec603a1f4c5efff9fd225a", + "build/assets/ba_data/meshes/jackLowerLeg.bob": "https://files.ballistica.net/cache/ba1/cc/7d/5e70cad41ab74bef6204419bbc88", + "build/assets/ba_data/meshes/jackToes.bob": "https://files.ballistica.net/cache/ba1/75/48/e79acb34130b66fd6d5056ab60c0", + "build/assets/ba_data/meshes/jackTorso.bob": "https://files.ballistica.net/cache/ba1/4e/66/cda1cbf818b9b74abaa02d5644c9", + "build/assets/ba_data/meshes/jackUpperArm.bob": "https://files.ballistica.net/cache/ba1/07/10/369eba62bb7005c615e46b985587", + "build/assets/ba_data/meshes/jackUpperLeg.bob": "https://files.ballistica.net/cache/ba1/2a/fe/82832f6215d6dc4f35c753d6037c", + "build/assets/ba_data/meshes/jumpsuitForeArm.bob": "https://files.ballistica.net/cache/ba1/bd/52/8d7f919cee8ea787072d4a07ffda", + "build/assets/ba_data/meshes/jumpsuitHand.bob": "https://files.ballistica.net/cache/ba1/6b/49/4f4e306810fc4dcaab68c67c468d", + "build/assets/ba_data/meshes/jumpsuitHead.bob": "https://files.ballistica.net/cache/ba1/96/aa/fe0621a8b514fe2ab71348b72f16", + "build/assets/ba_data/meshes/jumpsuitLowerLeg.bob": "https://files.ballistica.net/cache/ba1/fa/c5/566201b33390c08776973a9aa941", + "build/assets/ba_data/meshes/jumpsuitPelvis.bob": "https://files.ballistica.net/cache/ba1/56/03/a343ebdcd1c21e3e02c8d5c3b05f", + "build/assets/ba_data/meshes/jumpsuitToes.bob": "https://files.ballistica.net/cache/ba1/0d/29/f98d9570a6dd7e073b24f0df951f", + "build/assets/ba_data/meshes/jumpsuitTorso.bob": "https://files.ballistica.net/cache/ba1/df/ca/b44d65feac3facf10e9026b03b61", + "build/assets/ba_data/meshes/jumpsuitUpperArm.bob": "https://files.ballistica.net/cache/ba1/9f/d3/8a076d1c98fc9198a7b5c1a345a8", + "build/assets/ba_data/meshes/jumpsuitUpperLeg.bob": "https://files.ballistica.net/cache/ba1/e3/d9/a02c63cc1eff4514552f91182618", + "build/assets/ba_data/meshes/kronkForeArm.bob": "https://files.ballistica.net/cache/ba1/a5/d7/7e3b8438249bf583b93ec9e8c4e2", + "build/assets/ba_data/meshes/kronkHand.bob": "https://files.ballistica.net/cache/ba1/82/17/e091af444a1be02266eb4e91696f", + "build/assets/ba_data/meshes/kronkHead.bob": "https://files.ballistica.net/cache/ba1/5d/b1/5c68300047c3b1b03c05d41b72a4", + "build/assets/ba_data/meshes/kronkLowerLeg.bob": "https://files.ballistica.net/cache/ba1/31/58/ae6d789ee9f01509daab13283ff9", + "build/assets/ba_data/meshes/kronkPelvis.bob": "https://files.ballistica.net/cache/ba1/8f/17/063a74ba3c18ce4513464c7df5ca", + "build/assets/ba_data/meshes/kronkToes.bob": "https://files.ballistica.net/cache/ba1/4c/0b/c9c3520d0c1308258ff6402537bc", + "build/assets/ba_data/meshes/kronkTorso.bob": "https://files.ballistica.net/cache/ba1/9d/9d/7721cf9843b0cbaf3c1d94afecc7", + "build/assets/ba_data/meshes/kronkUpperArm.bob": "https://files.ballistica.net/cache/ba1/2d/c4/5adb06b3d17c4632652f9e7d3fef", + "build/assets/ba_data/meshes/kronkUpperLeg.bob": "https://files.ballistica.net/cache/ba1/14/f7/9262d2d3c294bd4ff53b01934a43", + "build/assets/ba_data/meshes/lakeFrigid.bob": "https://files.ballistica.net/cache/ba1/53/c2/85df7d65df2fb86f5b7ac986fc86", + "build/assets/ba_data/meshes/lakeFrigidCollide.cob": "https://files.ballistica.net/cache/ba1/69/d1/b602b284213040dff934847c8e8d", + "build/assets/ba_data/meshes/lakeFrigidReflections.bob": "https://files.ballistica.net/cache/ba1/ce/82/a25e11e21fcac658604e7c5adcb9", + "build/assets/ba_data/meshes/lakeFrigidTop.bob": "https://files.ballistica.net/cache/ba1/39/8f/d5af8037cf5e8f4cd8662b051b00", + "build/assets/ba_data/meshes/lakeFrigidVRFill.bob": "https://files.ballistica.net/cache/ba1/4e/f5/2079bae4a040caacbe0c5a51b972", + "build/assets/ba_data/meshes/landMine.bob": "https://files.ballistica.net/cache/ba1/4d/f6/37573da68536291c701143e63599", + "build/assets/ba_data/meshes/level_select_button_opaque.bob": "https://files.ballistica.net/cache/ba1/a9/c0/87c21cd9cc694871a4b69684a6d0", + "build/assets/ba_data/meshes/level_select_button_transparent.bob": "https://files.ballistica.net/cache/ba1/1f/28/38a3a07b80a71bf1df29bbedeb18", + "build/assets/ba_data/meshes/locator.bob": "https://files.ballistica.net/cache/ba1/c0/72/7d88a8a6f7b2e35656761b44396e", + "build/assets/ba_data/meshes/locatorBox.bob": "https://files.ballistica.net/cache/ba1/58/39/6884d2c4005b69b99a3179dab167", + "build/assets/ba_data/meshes/locatorCircle.bob": "https://files.ballistica.net/cache/ba1/ee/9f/937d2a8d9a5ebeeeed7fb7e0d0bc", + "build/assets/ba_data/meshes/locatorCircleOutline.bob": "https://files.ballistica.net/cache/ba1/e9/07/65bdcdfd314cb19f34d90726fd60", + "build/assets/ba_data/meshes/logo.bob": "https://files.ballistica.net/cache/ba1/ea/d8/d9a64b4275d443396422153fd214", + "build/assets/ba_data/meshes/logoTransparent.bob": "https://files.ballistica.net/cache/ba1/22/ad/76ed2b702477aaca2536887551a4", + "build/assets/ba_data/meshes/melForeArm.bob": "https://files.ballistica.net/cache/ba1/2d/d1/f1dd768d0a0d13e849794be9053a", + "build/assets/ba_data/meshes/melHand.bob": "https://files.ballistica.net/cache/ba1/0d/a8/23196a6451522b44afb266d5dcc7", + "build/assets/ba_data/meshes/melHead.bob": "https://files.ballistica.net/cache/ba1/f3/4a/f1072e942750445f87d236c1782a", + "build/assets/ba_data/meshes/melLowerLeg.bob": "https://files.ballistica.net/cache/ba1/de/ab/17e73b8b6ee69c90d438848e44ae", + "build/assets/ba_data/meshes/melToes.bob": "https://files.ballistica.net/cache/ba1/15/58/2ccee25b1627cae0757ea4118409", + "build/assets/ba_data/meshes/melTorso.bob": "https://files.ballistica.net/cache/ba1/d0/4b/d734903cef2af23f225693cee805", + "build/assets/ba_data/meshes/melUpperArm.bob": "https://files.ballistica.net/cache/ba1/03/c5/6fffb649f2c1a9facf222e3aa0aa", + "build/assets/ba_data/meshes/melUpperLeg.bob": "https://files.ballistica.net/cache/ba1/58/89/ee7a133f406334ea1b4474d3a1f0", + "build/assets/ba_data/meshes/meterTransparent.bob": "https://files.ballistica.net/cache/ba1/46/c8/0d15c914f929230e46acce3cb363", + "build/assets/ba_data/meshes/monkeyFaceLevel.bob": "https://files.ballistica.net/cache/ba1/06/ce/5ba6b00ce6dca051c96f320be16c", + "build/assets/ba_data/meshes/monkeyFaceLevelBottom.bob": "https://files.ballistica.net/cache/ba1/de/f4/c4147c7000ebb95975f123cacaef", + "build/assets/ba_data/meshes/monkeyFaceLevelBumper.cob": "https://files.ballistica.net/cache/ba1/ee/10/4cceafa27dc230cfd48e35c5543f", + "build/assets/ba_data/meshes/monkeyFaceLevelCollide.cob": "https://files.ballistica.net/cache/ba1/f4/2c/061c2566b07f08d48d41fc747788", + "build/assets/ba_data/meshes/natureBackground.bob": "https://files.ballistica.net/cache/ba1/26/ff/337bf63a2cda73c705386695c7a4", + "build/assets/ba_data/meshes/natureBackgroundCollide.cob": "https://files.ballistica.net/cache/ba1/ca/d5/2fffac60f0e7927b46c6e044406a", + "build/assets/ba_data/meshes/natureBackgroundVRFill.bob": "https://files.ballistica.net/cache/ba1/4b/42/66addd025fa4f8f4b4e17fc64a96", + "build/assets/ba_data/meshes/neoSpazForeArm.bob": "https://files.ballistica.net/cache/ba1/a9/3f/893f420625e44baaba4ed81ff460", + "build/assets/ba_data/meshes/neoSpazHand.bob": "https://files.ballistica.net/cache/ba1/80/6e/4bb8f91227deac2f9a3879a6c40d", + "build/assets/ba_data/meshes/neoSpazHead.bob": "https://files.ballistica.net/cache/ba1/d8/fd/c696e2e21c7854ec9eca85e05730", + "build/assets/ba_data/meshes/neoSpazLowerLeg.bob": "https://files.ballistica.net/cache/ba1/e0/2e/ea4465f7ef5e54d0dd832e8f4316", + "build/assets/ba_data/meshes/neoSpazPelvis.bob": "https://files.ballistica.net/cache/ba1/14/4c/2c93303d6a345b92d87d292ae58d", + "build/assets/ba_data/meshes/neoSpazToes.bob": "https://files.ballistica.net/cache/ba1/7d/4c/25073b93fe5c4e73c9848f4130c0", + "build/assets/ba_data/meshes/neoSpazTorso.bob": "https://files.ballistica.net/cache/ba1/ce/fa/e751fc048ba6274c496c423a2cde", + "build/assets/ba_data/meshes/neoSpazUpperArm.bob": "https://files.ballistica.net/cache/ba1/34/59/869eb4798cb5f9be0754eb96a997", + "build/assets/ba_data/meshes/neoSpazUpperLeg.bob": "https://files.ballistica.net/cache/ba1/89/ac/959e211adb64305346cc888cad44", + "build/assets/ba_data/meshes/ninjaForeArm.bob": "https://files.ballistica.net/cache/ba1/4a/42/abcf55aedf5c30ad92c94af5449e", + "build/assets/ba_data/meshes/ninjaHand.bob": "https://files.ballistica.net/cache/ba1/d3/4f/f404683bbcf3f277eb97c58cae42", + "build/assets/ba_data/meshes/ninjaHead.bob": "https://files.ballistica.net/cache/ba1/f8/12/dfd7622271f010ea3f0605a0dc9c", + "build/assets/ba_data/meshes/ninjaLowerLeg.bob": "https://files.ballistica.net/cache/ba1/02/68/bb789073aee507917e37f2c5597f", + "build/assets/ba_data/meshes/ninjaPelvis.bob": "https://files.ballistica.net/cache/ba1/cc/72/39605878bf0cd89d7e6df40b03b7", + "build/assets/ba_data/meshes/ninjaToes.bob": "https://files.ballistica.net/cache/ba1/b8/64/d2fc7e8b12c9474cb88e00233047", + "build/assets/ba_data/meshes/ninjaTorso.bob": "https://files.ballistica.net/cache/ba1/d1/df/5f7823a5b42a19f2dbe3c98c90c5", + "build/assets/ba_data/meshes/ninjaUpperArm.bob": "https://files.ballistica.net/cache/ba1/15/db/932ddf4c575ebff971388b0b6c5e", + "build/assets/ba_data/meshes/ninjaUpperLeg.bob": "https://files.ballistica.net/cache/ba1/a0/d7/508e858284df1b0a5893470e2801", + "build/assets/ba_data/meshes/oldLadyForeArm.bob": "https://files.ballistica.net/cache/ba1/e0/91/74f0abbb1cc9f05470e881c72760", + "build/assets/ba_data/meshes/oldLadyHand.bob": "https://files.ballistica.net/cache/ba1/0a/6c/d801e4e37db8b1ed5f27af0e3bd4", + "build/assets/ba_data/meshes/oldLadyHead.bob": "https://files.ballistica.net/cache/ba1/e0/f5/a38f5df14998a5c267f1159c26a4", + "build/assets/ba_data/meshes/oldLadyLowerLeg.bob": "https://files.ballistica.net/cache/ba1/51/72/76987540fc85416e00f6cd716da8", + "build/assets/ba_data/meshes/oldLadyPelvis.bob": "https://files.ballistica.net/cache/ba1/6e/8a/3081e80998341c01686587a1ede3", + "build/assets/ba_data/meshes/oldLadyToes.bob": "https://files.ballistica.net/cache/ba1/8e/8f/6f65cc98f710a1eaa9ca042bf3ea", + "build/assets/ba_data/meshes/oldLadyTorso.bob": "https://files.ballistica.net/cache/ba1/c6/c6/b4dfce1c29431bc9a3d299e23d9e", + "build/assets/ba_data/meshes/oldLadyUpperArm.bob": "https://files.ballistica.net/cache/ba1/a5/6f/75ef15645faea6e0271518e7230a", + "build/assets/ba_data/meshes/oldLadyUpperLeg.bob": "https://files.ballistica.net/cache/ba1/d8/88/7a4707060f1350cef63ad66c4c13", + "build/assets/ba_data/meshes/operaSingerForeArm.bob": "https://files.ballistica.net/cache/ba1/21/f8/0b54525cfa4b13289c7fcc2dad14", + "build/assets/ba_data/meshes/operaSingerHand.bob": "https://files.ballistica.net/cache/ba1/af/c0/f8466a2e17fa0c98d717982e14c0", + "build/assets/ba_data/meshes/operaSingerHead.bob": "https://files.ballistica.net/cache/ba1/b9/0d/5f50a59e0c9fc6f5fe2a9a8ac5c4", + "build/assets/ba_data/meshes/operaSingerLowerLeg.bob": "https://files.ballistica.net/cache/ba1/25/1d/335711f7a61ccb9bf0ded11fd5d3", + "build/assets/ba_data/meshes/operaSingerPelvis.bob": "https://files.ballistica.net/cache/ba1/d4/bd/2f8ea13413c1a0113ad088b5bfad", + "build/assets/ba_data/meshes/operaSingerToes.bob": "https://files.ballistica.net/cache/ba1/15/2c/0bf5f4f1445d235e5ef744949cd3", + "build/assets/ba_data/meshes/operaSingerTorso.bob": "https://files.ballistica.net/cache/ba1/7c/e2/1bc8aae266958212632f3679044f", + "build/assets/ba_data/meshes/operaSingerUpperArm.bob": "https://files.ballistica.net/cache/ba1/b7/b7/c78aa2a7225e3bd66bcae5a4d920", + "build/assets/ba_data/meshes/operaSingerUpperLeg.bob": "https://files.ballistica.net/cache/ba1/cc/de/6772608d58526c71328826a20ac0", + "build/assets/ba_data/meshes/overlayGuide.bob": "https://files.ballistica.net/cache/ba1/35/0a/4cea31fc04a36f90e2c2a955d36a", + "build/assets/ba_data/meshes/penguinForeArm.bob": "https://files.ballistica.net/cache/ba1/5e/17/fd96027ac5b175c819402e3c8416", + "build/assets/ba_data/meshes/penguinHand.bob": "https://files.ballistica.net/cache/ba1/09/38/5cd2d773f9b15f9143394be0320f", + "build/assets/ba_data/meshes/penguinHead.bob": "https://files.ballistica.net/cache/ba1/c4/be/4eb3b46d8639dc2b52e998eccaa0", + "build/assets/ba_data/meshes/penguinLowerLeg.bob": "https://files.ballistica.net/cache/ba1/b5/0e/67d3adae78ee043c319cfc3f55cc", + "build/assets/ba_data/meshes/penguinPelvis.bob": "https://files.ballistica.net/cache/ba1/a3/69/22f8d27ce7318f8bc779c50cd69a", + "build/assets/ba_data/meshes/penguinToes.bob": "https://files.ballistica.net/cache/ba1/d3/7c/ecb2f17eca0a4bc37e58c681561b", + "build/assets/ba_data/meshes/penguinTorso.bob": "https://files.ballistica.net/cache/ba1/0e/3f/70d4083926f277e2da59508b66ea", + "build/assets/ba_data/meshes/penguinUpperArm.bob": "https://files.ballistica.net/cache/ba1/ac/2a/4dd5c8a29f12031d636c1dae8040", + "build/assets/ba_data/meshes/penguinUpperLeg.bob": "https://files.ballistica.net/cache/ba1/3c/25/817bdc3b0776268d936d1576df56", + "build/assets/ba_data/meshes/pixieForeArm.bob": "https://files.ballistica.net/cache/ba1/e5/52/85a734767a0032521d58a8bfd50f", + "build/assets/ba_data/meshes/pixieHand.bob": "https://files.ballistica.net/cache/ba1/3f/6b/8be487f2d89ecdbcac872dd47c8f", + "build/assets/ba_data/meshes/pixieHead.bob": "https://files.ballistica.net/cache/ba1/90/15/5fff3479709945d15e212c9f85f0", + "build/assets/ba_data/meshes/pixieLowerLeg.bob": "https://files.ballistica.net/cache/ba1/b2/ca/948e4848c611e262b34ed7c5ceba", + "build/assets/ba_data/meshes/pixiePelvis.bob": "https://files.ballistica.net/cache/ba1/38/4b/e19396302a8c44955c9d830cd95e", + "build/assets/ba_data/meshes/pixieToes.bob": "https://files.ballistica.net/cache/ba1/aa/d5/d81ea854cf00fd6fd178be138b57", + "build/assets/ba_data/meshes/pixieTorso.bob": "https://files.ballistica.net/cache/ba1/4e/36/bcbc95d8d5536424f3535263e74c", + "build/assets/ba_data/meshes/pixieUpperArm.bob": "https://files.ballistica.net/cache/ba1/0f/a7/69a7edc055b58e26f4c65278a356", + "build/assets/ba_data/meshes/pixieUpperLeg.bob": "https://files.ballistica.net/cache/ba1/ea/44/20c0bd07b75abb6fc468cbe94522", + "build/assets/ba_data/meshes/plasticEyesTransparent.bob": "https://files.ballistica.net/cache/ba1/a1/1f/77054b25533a337811b82f9067d5", + "build/assets/ba_data/meshes/playerLineup1Transparent.bob": "https://files.ballistica.net/cache/ba1/ab/98/d250d78ac0c63904c4e38540879c", + "build/assets/ba_data/meshes/playerLineup2Transparent.bob": "https://files.ballistica.net/cache/ba1/fc/42/943401c2a53e443c6a075de468ef", + "build/assets/ba_data/meshes/playerLineup3Transparent.bob": "https://files.ballistica.net/cache/ba1/0e/ed/4cea76fea875acd86abf099406ed", + "build/assets/ba_data/meshes/playerLineup4Transparent.bob": "https://files.ballistica.net/cache/ba1/6f/cc/7174da5db7e3b0ab2d9312b5cdc5", + "build/assets/ba_data/meshes/powerup.bob": "https://files.ballistica.net/cache/ba1/98/ea/03170b70480a8a36cfdbdf409e71", + "build/assets/ba_data/meshes/powerupSimple.bob": "https://files.ballistica.net/cache/ba1/60/4c/a3510afeea4c375790077d062824", + "build/assets/ba_data/meshes/puck.bob": "https://files.ballistica.net/cache/ba1/91/5a/8531a23b2f420965b9c2fa6156e3", + "build/assets/ba_data/meshes/rampageBG.bob": "https://files.ballistica.net/cache/ba1/b9/b8/4168f099304100ba4926bb7b9034", + "build/assets/ba_data/meshes/rampageBG2.bob": "https://files.ballistica.net/cache/ba1/81/b0/36841e6c26c7e8af5ca545b3fc8b", + "build/assets/ba_data/meshes/rampageBumper.cob": "https://files.ballistica.net/cache/ba1/8a/a0/9060967222f82fdaf7b4454cc448", + "build/assets/ba_data/meshes/rampageLevel.bob": "https://files.ballistica.net/cache/ba1/4d/d7/64b35c24222b1ca926c9f3ad693e", + "build/assets/ba_data/meshes/rampageLevelBottom.bob": "https://files.ballistica.net/cache/ba1/a5/74/207928041f94b15f0d9347d4a6d6", + "build/assets/ba_data/meshes/rampageLevelCollide.cob": "https://files.ballistica.net/cache/ba1/8e/6b/ab30fc0b9b9728c5e5fe5f12ddae", + "build/assets/ba_data/meshes/rampageVRFill.bob": "https://files.ballistica.net/cache/ba1/ff/3a/5498dec434f1f431900d80d9bf52", + "build/assets/ba_data/meshes/robotForeArm.bob": "https://files.ballistica.net/cache/ba1/6f/59/36175f508230988cecabf9505aca", + "build/assets/ba_data/meshes/robotHand.bob": "https://files.ballistica.net/cache/ba1/2a/a0/a484109d461d3be6c6431c921e8c", + "build/assets/ba_data/meshes/robotHead.bob": "https://files.ballistica.net/cache/ba1/0b/3b/dd9ae3dad96c3bef77b52f585233", + "build/assets/ba_data/meshes/robotLowerLeg.bob": "https://files.ballistica.net/cache/ba1/c6/35/9e9042659011b30cf0b0d1eb59c5", + "build/assets/ba_data/meshes/robotPelvis.bob": "https://files.ballistica.net/cache/ba1/99/88/85785c28ae28f95720887cf295b5", + "build/assets/ba_data/meshes/robotToes.bob": "https://files.ballistica.net/cache/ba1/80/ec/953fa2ad01aa5c72fff04ac5037f", + "build/assets/ba_data/meshes/robotTorso.bob": "https://files.ballistica.net/cache/ba1/92/7f/0a5613e18e7aeedb5655b76327ff", + "build/assets/ba_data/meshes/robotUpperArm.bob": "https://files.ballistica.net/cache/ba1/41/83/b8484c4d7c4357ec1d0efc1e60c9", + "build/assets/ba_data/meshes/robotUpperLeg.bob": "https://files.ballistica.net/cache/ba1/1f/2e/bbfad5cb8803a1da7b3908b47202", + "build/assets/ba_data/meshes/roundaboutLevel.bob": "https://files.ballistica.net/cache/ba1/2e/57/7ccc32dc95158488bb15b0c5f5f4", + "build/assets/ba_data/meshes/roundaboutLevelBottom.bob": "https://files.ballistica.net/cache/ba1/4b/fc/ee506e4e2c5af4c4b32f27404c70", + "build/assets/ba_data/meshes/roundaboutLevelBumper.cob": "https://files.ballistica.net/cache/ba1/00/02/27949020fb1bbc58d7342ca39475", + "build/assets/ba_data/meshes/roundaboutLevelCollide.cob": "https://files.ballistica.net/cache/ba1/69/69/f3477b735119ff0b28eba0fa00a5", + "build/assets/ba_data/meshes/runningShoes.bob": "https://files.ballistica.net/cache/ba1/b9/e9/28b5210cf9a460d838a7d0c946c4", + "build/assets/ba_data/meshes/santaForeArm.bob": "https://files.ballistica.net/cache/ba1/d9/eb/8fe56aadeab7cdbd3c8f1860b384", + "build/assets/ba_data/meshes/santaHand.bob": "https://files.ballistica.net/cache/ba1/39/7e/5afa90de580c005a43a97f42926b", + "build/assets/ba_data/meshes/santaHead.bob": "https://files.ballistica.net/cache/ba1/3a/00/53735bcdff4d9d04ae7e5f0608a4", + "build/assets/ba_data/meshes/santaLowerLeg.bob": "https://files.ballistica.net/cache/ba1/d7/44/a9f09ed156550e540356996346e7", + "build/assets/ba_data/meshes/santaToes.bob": "https://files.ballistica.net/cache/ba1/ac/42/4924659b9ee036c135f57fe3769a", + "build/assets/ba_data/meshes/santaTorso.bob": "https://files.ballistica.net/cache/ba1/ff/ac/28daf1103dd95b45e74aa202e667", + "build/assets/ba_data/meshes/santaUpperArm.bob": "https://files.ballistica.net/cache/ba1/30/aa/130b79ea29c2e8d96c8feeecbb7e", + "build/assets/ba_data/meshes/santaUpperLeg.bob": "https://files.ballistica.net/cache/ba1/24/ab/71c2a2f661210798a3adcb413e8b", + "build/assets/ba_data/meshes/scorch.bob": "https://files.ballistica.net/cache/ba1/3b/d7/883f0fc748984b2cfef7e15ff0d3", + "build/assets/ba_data/meshes/scrollBarThumbOpaque.bob": "https://files.ballistica.net/cache/ba1/cd/84/bd513541ac33417b7a7abf255424", + "build/assets/ba_data/meshes/scrollBarThumbShortOpaque.bob": "https://files.ballistica.net/cache/ba1/7d/fc/a2447e7f4c095f8f033cc9dcb842", + "build/assets/ba_data/meshes/scrollBarThumbShortSimple.bob": "https://files.ballistica.net/cache/ba1/d0/a3/6527f1f40933bc673c4aa3025b77", + "build/assets/ba_data/meshes/scrollBarThumbShortTransparent.bob": "https://files.ballistica.net/cache/ba1/bc/2f/99c367802d376b83f7452e92fc86", + "build/assets/ba_data/meshes/scrollBarThumbSimple.bob": "https://files.ballistica.net/cache/ba1/af/38/7c48921014d2f22e24f6ba6f58f8", + "build/assets/ba_data/meshes/scrollBarThumbTransparent.bob": "https://files.ballistica.net/cache/ba1/19/7d/651127bab4f7c96fcd811c66d72b", + "build/assets/ba_data/meshes/scrollBarTroughTransparent.bob": "https://files.ballistica.net/cache/ba1/bd/3d/049bea239f484603e8dcdf52cb75", + "build/assets/ba_data/meshes/scrollWidgetShort.bob": "https://files.ballistica.net/cache/ba1/97/0e/56410c31a386f3975bf1bdfc099c", + "build/assets/ba_data/meshes/shield.bob": "https://files.ballistica.net/cache/ba1/90/9f/322273c9e3461ab61440fea8bf19", + "build/assets/ba_data/meshes/shockWave.bob": "https://files.ballistica.net/cache/ba1/7f/23/b3eac941334261d7b74633758ef1", + "build/assets/ba_data/meshes/shrapnel1.bob": "https://files.ballistica.net/cache/ba1/7b/7f/a3957a89b6663e6ec4c2c1dbd868", + "build/assets/ba_data/meshes/shrapnelBoard.bob": "https://files.ballistica.net/cache/ba1/bf/aa/27a1a9f066335db33c8bc2347938", + "build/assets/ba_data/meshes/shrapnelSlime.bob": "https://files.ballistica.net/cache/ba1/c0/c0/e3a898ae0897cf007bfd8ee3a3a4", + "build/assets/ba_data/meshes/softEdgeInside.bob": "https://files.ballistica.net/cache/ba1/3f/af/3562e46d7ef18838c74be7ed076f", + "build/assets/ba_data/meshes/softEdgeOutside.bob": "https://files.ballistica.net/cache/ba1/fd/26/bea925a87e7bd896c0f9104c6bef", + "build/assets/ba_data/meshes/stepRightUpLevel.bob": "https://files.ballistica.net/cache/ba1/52/0d/913819f138a168f14c656a8317e8", + "build/assets/ba_data/meshes/stepRightUpLevelBottom.bob": "https://files.ballistica.net/cache/ba1/1f/05/fdfb676dba82cea246ee2f626bf0", + "build/assets/ba_data/meshes/stepRightUpLevelCollide.cob": "https://files.ballistica.net/cache/ba1/b3/d4/ef2f40cb14a127489dc6c396895e", + "build/assets/ba_data/meshes/stepRightUpVRFillMound.bob": "https://files.ballistica.net/cache/ba1/af/29/29b2c4d83dbd2a3c7c1c6b34e4a8", + "build/assets/ba_data/meshes/superheroForeArm.bob": "https://files.ballistica.net/cache/ba1/5d/4c/d5cfb05a0a827054a42b83e41e9a", + "build/assets/ba_data/meshes/superheroHand.bob": "https://files.ballistica.net/cache/ba1/c6/e4/11d9beee6c2b6a00ec13b9ec4bb9", + "build/assets/ba_data/meshes/superheroHead.bob": "https://files.ballistica.net/cache/ba1/89/cd/c89f31c82a7fa9f24d243a3aa8f1", + "build/assets/ba_data/meshes/superheroLowerLeg.bob": "https://files.ballistica.net/cache/ba1/f7/3d/2ab68cca6c986e46c76b129efd36", + "build/assets/ba_data/meshes/superheroPelvis.bob": "https://files.ballistica.net/cache/ba1/1f/1b/8cf7e4ff465bd542de6658ebed86", + "build/assets/ba_data/meshes/superheroToes.bob": "https://files.ballistica.net/cache/ba1/4e/fb/fc9608558ba281ca7a4ef472e62a", + "build/assets/ba_data/meshes/superheroTorso.bob": "https://files.ballistica.net/cache/ba1/29/f7/5c9811ce160fa17a7dfdc0bc7ac1", + "build/assets/ba_data/meshes/superheroUpperArm.bob": "https://files.ballistica.net/cache/ba1/81/1b/6d37e502fbe2a83e6d88bb5f6e17", + "build/assets/ba_data/meshes/superheroUpperLeg.bob": "https://files.ballistica.net/cache/ba1/47/1c/d63243db78bfbc5bd50244b4db27", + "build/assets/ba_data/meshes/textBoxTransparent.bob": "https://files.ballistica.net/cache/ba1/a7/18/306615c36006730c0c116cf7abf7", + "build/assets/ba_data/meshes/thePadBG.bob": "https://files.ballistica.net/cache/ba1/30/0b/70087d5a48db98cf700c27080f6a", + "build/assets/ba_data/meshes/thePadBGSmall.bob": "https://files.ballistica.net/cache/ba1/25/dd/ebb3ec67e221ee8ee6ad8fefe63b", + "build/assets/ba_data/meshes/thePadLevel.bob": "https://files.ballistica.net/cache/ba1/27/56/bd4e6398e7c9610cecebed185b4a", + "build/assets/ba_data/meshes/thePadLevelBottom.bob": "https://files.ballistica.net/cache/ba1/0b/4c/e5dec0f91e9c42c7f5c916b2c4af", + "build/assets/ba_data/meshes/thePadLevelBumper.cob": "https://files.ballistica.net/cache/ba1/eb/f0/0274160803cd53db3ffc22a2307b", + "build/assets/ba_data/meshes/thePadLevelCollide.cob": "https://files.ballistica.net/cache/ba1/81/91/e3f51f09149bbfbe0332f3f2a371", + "build/assets/ba_data/meshes/thePadVRFillBottom.bob": "https://files.ballistica.net/cache/ba1/64/c0/a8a9f7fd5a231432f34188db0f81", + "build/assets/ba_data/meshes/thePadVRFillMound.bob": "https://files.ballistica.net/cache/ba1/6a/12/2caf46a76a624b692a80cab3e3bb", + "build/assets/ba_data/meshes/thePadVRFillTop.bob": "https://files.ballistica.net/cache/ba1/a2/de/5874d658fa7686a78a6afd868a2b", + "build/assets/ba_data/meshes/tipTopBG.bob": "https://files.ballistica.net/cache/ba1/a3/04/e1897f5a45781a84a7005b457e6b", + "build/assets/ba_data/meshes/tipTopLevel.bob": "https://files.ballistica.net/cache/ba1/51/c4/2a21a559d98a7ade54dc178fb9a5", + "build/assets/ba_data/meshes/tipTopLevelBottom.bob": "https://files.ballistica.net/cache/ba1/bd/aa/8ce9f4c2cd0fed5b1779af7c373d", + "build/assets/ba_data/meshes/tipTopLevelBumper.cob": "https://files.ballistica.net/cache/ba1/14/1b/a82df13f9ed092da0b6f607cce23", + "build/assets/ba_data/meshes/tipTopLevelCollide.cob": "https://files.ballistica.net/cache/ba1/a0/15/cf78880e0d5e0357dac767820c2d", + "build/assets/ba_data/meshes/tnt.bob": "https://files.ballistica.net/cache/ba1/83/04/98c7a6f4e596d535d7dbf1a87651", + "build/assets/ba_data/meshes/toolbarBacking.bob": "https://files.ballistica.net/cache/ba1/fd/a0/ff5898cbbf651ccf84e840034043", + "build/assets/ba_data/meshes/toolbarBackingBottom.bob": "https://files.ballistica.net/cache/ba1/0c/74/a1da95fbb72308258f2534fceaba", + "build/assets/ba_data/meshes/toolbarBackingBottom2.bob": "https://files.ballistica.net/cache/ba1/8b/27/f81c59265650b767463684d86d23", + "build/assets/ba_data/meshes/toolbarBackingOpaque.bob": "https://files.ballistica.net/cache/ba1/34/e0/c8fd9b3fb176126a33c642d96ea7", + "build/assets/ba_data/meshes/toolbarBackingTop.bob": "https://files.ballistica.net/cache/ba1/82/e6/f8d49ce86431c2b6b1d41036cc4c", + "build/assets/ba_data/meshes/toolbarBackingTop2.bob": "https://files.ballistica.net/cache/ba1/95/b4/1d5ccc019ea084ff82b481efd498", + "build/assets/ba_data/meshes/toolbarBackingTransparent.bob": "https://files.ballistica.net/cache/ba1/c5/2a/a816b511f56048f6cf67d824ae59", + "build/assets/ba_data/meshes/towerDLevel.bob": "https://files.ballistica.net/cache/ba1/75/c9/7dabfa7c5e6f1f541d84b775e742", + "build/assets/ba_data/meshes/towerDLevelBottom.bob": "https://files.ballistica.net/cache/ba1/23/57/f4e564001d05b00e7f42fe4b9376", + "build/assets/ba_data/meshes/towerDLevelCollide.cob": "https://files.ballistica.net/cache/ba1/54/96/4d692f8a17addc3fc2d62ab7d894", + "build/assets/ba_data/meshes/towerDPlayerWall.cob": "https://files.ballistica.net/cache/ba1/5a/f1/992ab378baa10ea54e6816a058b6", + "build/assets/ba_data/meshes/trees.bob": "https://files.ballistica.net/cache/ba1/ce/d9/4a0bd6cf2fffae26995e84ee3f79", + "build/assets/ba_data/meshes/vrFade.bob": "https://files.ballistica.net/cache/ba1/ad/f0/a4b74eb08e62bd6f161196342182", + "build/assets/ba_data/meshes/vrOverlay.bob": "https://files.ballistica.net/cache/ba1/dd/4e/def7b2df9e27da144cc58160384c", + "build/assets/ba_data/meshes/warriorForeArm.bob": "https://files.ballistica.net/cache/ba1/a5/c5/140880c5a6b8b30d6d16f52f7a00", + "build/assets/ba_data/meshes/warriorHand.bob": "https://files.ballistica.net/cache/ba1/b6/a9/0702b2a368f44a54e9161ef6c1c8", + "build/assets/ba_data/meshes/warriorHead.bob": "https://files.ballistica.net/cache/ba1/b4/c4/8f789b56cce1b38a992acacd36b0", + "build/assets/ba_data/meshes/warriorLowerLeg.bob": "https://files.ballistica.net/cache/ba1/55/2c/2edc8156a47bb547791fd750face", + "build/assets/ba_data/meshes/warriorPelvis.bob": "https://files.ballistica.net/cache/ba1/f3/df/9b7da21b871d948c8f133d4bd15b", + "build/assets/ba_data/meshes/warriorToes.bob": "https://files.ballistica.net/cache/ba1/ee/29/07958611a577ae98a636bdeda4fd", + "build/assets/ba_data/meshes/warriorTorso.bob": "https://files.ballistica.net/cache/ba1/78/e0/25ff42f02c236d6955b61363e8f2", + "build/assets/ba_data/meshes/warriorUpperArm.bob": "https://files.ballistica.net/cache/ba1/a2/a2/088d957701a5b2436b5c658c4a27", + "build/assets/ba_data/meshes/warriorUpperLeg.bob": "https://files.ballistica.net/cache/ba1/e8/ea/3491e52c6343617fda5f81312a15", + "build/assets/ba_data/meshes/windowBGBlotch.bob": "https://files.ballistica.net/cache/ba1/70/b0/ae47e4912c9c39ca67e8349cfda9", + "build/assets/ba_data/meshes/windowHSmallVMedOpaque.bob": "https://files.ballistica.net/cache/ba1/fb/b0/39e5c2d075ca3b4b1832ae3b7208", + "build/assets/ba_data/meshes/windowHSmallVMedTransparent.bob": "https://files.ballistica.net/cache/ba1/d2/24/a33fc23ed54deb5425c53a0a4d06", + "build/assets/ba_data/meshes/windowHSmallVSmallOpaque.bob": "https://files.ballistica.net/cache/ba1/a3/93/d212d6f66675b5426c44785f96d7", + "build/assets/ba_data/meshes/windowHSmallVSmallTransparent.bob": "https://files.ballistica.net/cache/ba1/b5/26/c34f0df07e64c6dee28b5456d92a", + "build/assets/ba_data/meshes/wing.bob": "https://files.ballistica.net/cache/ba1/6b/41/12d162ef5bf35f54ca769802ced5", + "build/assets/ba_data/meshes/witchForeArm.bob": "https://files.ballistica.net/cache/ba1/db/89/4a473f6011163d04da489013b91c", + "build/assets/ba_data/meshes/witchHand.bob": "https://files.ballistica.net/cache/ba1/2e/55/39f9397f0c3cee2b79dab374c239", + "build/assets/ba_data/meshes/witchHead.bob": "https://files.ballistica.net/cache/ba1/90/67/d85a960ccb8908c3482f0babc0ca", + "build/assets/ba_data/meshes/witchLowerLeg.bob": "https://files.ballistica.net/cache/ba1/ca/a9/9d488f4d0e33887631b9fc55feb4", + "build/assets/ba_data/meshes/witchPelvis.bob": "https://files.ballistica.net/cache/ba1/b4/33/97d2405309a64e021d1c2922daf5", + "build/assets/ba_data/meshes/witchToes.bob": "https://files.ballistica.net/cache/ba1/1e/5c/cc36a2a575a0f06518e95b5105d8", + "build/assets/ba_data/meshes/witchTorso.bob": "https://files.ballistica.net/cache/ba1/aa/2e/b48cc7226d06f60e4678c786d8e4", + "build/assets/ba_data/meshes/witchUpperArm.bob": "https://files.ballistica.net/cache/ba1/f2/2a/a72cab42ca6279d3328f6cdf34b0", + "build/assets/ba_data/meshes/witchUpperLeg.bob": "https://files.ballistica.net/cache/ba1/a9/f6/7db3f5a26d5c4c4bb188a7a9654f", + "build/assets/ba_data/meshes/wizardForeArm.bob": "https://files.ballistica.net/cache/ba1/33/b5/81e7a78a6ef6631be1d605263caa", + "build/assets/ba_data/meshes/wizardHand.bob": "https://files.ballistica.net/cache/ba1/e0/52/3daca91dc0052a9216090200f9b9", + "build/assets/ba_data/meshes/wizardHead.bob": "https://files.ballistica.net/cache/ba1/e3/e5/25086df6f6ff1a4dbbd9ae0a31e9", + "build/assets/ba_data/meshes/wizardLowerLeg.bob": "https://files.ballistica.net/cache/ba1/6a/fa/1061016199fcc397612b6540a454", + "build/assets/ba_data/meshes/wizardPelvis.bob": "https://files.ballistica.net/cache/ba1/23/2d/d879c6eccd9c5b1ec4cb475ed65d", + "build/assets/ba_data/meshes/wizardToes.bob": "https://files.ballistica.net/cache/ba1/06/5c/f27225653a7f52340c91c3cdd5e1", + "build/assets/ba_data/meshes/wizardTorso.bob": "https://files.ballistica.net/cache/ba1/a5/d8/76175bb0201903f367b1b2c78b2a", + "build/assets/ba_data/meshes/wizardUpperArm.bob": "https://files.ballistica.net/cache/ba1/1f/c6/87bc0d6626c9d8c6f48989a48593", + "build/assets/ba_data/meshes/wizardUpperLeg.bob": "https://files.ballistica.net/cache/ba1/ec/c7/baf1882b7fc3919076d4fa09b593", + "build/assets/ba_data/meshes/wrestlerForeArm.bob": "https://files.ballistica.net/cache/ba1/a2/80/4a1fcdcc88e76dcea628b1839b50", + "build/assets/ba_data/meshes/wrestlerHand.bob": "https://files.ballistica.net/cache/ba1/6d/9d/6b6c3c827cd6ca69fec3c5db4aff", + "build/assets/ba_data/meshes/wrestlerHead.bob": "https://files.ballistica.net/cache/ba1/a5/55/d34ea949ee2f690828ee3a2e3669", + "build/assets/ba_data/meshes/wrestlerLowerLeg.bob": "https://files.ballistica.net/cache/ba1/c5/d0/904a48dd029ed824f0952fd31c7a", + "build/assets/ba_data/meshes/wrestlerPelvis.bob": "https://files.ballistica.net/cache/ba1/bf/4c/7b19887e183b5b195739a254e505", + "build/assets/ba_data/meshes/wrestlerToes.bob": "https://files.ballistica.net/cache/ba1/29/c2/af4527a3591df988e562b4fb23a4", + "build/assets/ba_data/meshes/wrestlerTorso.bob": "https://files.ballistica.net/cache/ba1/db/d4/831cec8ad3674e74a693a288b08f", + "build/assets/ba_data/meshes/wrestlerUpperArm.bob": "https://files.ballistica.net/cache/ba1/1d/13/2d5bc20b9cb59cf26b46268e26c3", + "build/assets/ba_data/meshes/wrestlerUpperLeg.bob": "https://files.ballistica.net/cache/ba1/9e/61/bccb2c32c7f67fca9987a3ccc3e8", + "build/assets/ba_data/meshes/zigZagLevel.bob": "https://files.ballistica.net/cache/ba1/a1/b1/f7b2f5afaf661fc7bbf27ffbc3a0", + "build/assets/ba_data/meshes/zigZagLevelBottom.bob": "https://files.ballistica.net/cache/ba1/51/56/0e1f71e24281612c8336735bcf9e", + "build/assets/ba_data/meshes/zigZagLevelBumper.cob": "https://files.ballistica.net/cache/ba1/21/4b/a8826c6440ad8ba00555e9859969", + "build/assets/ba_data/meshes/zigZagLevelCollide.cob": "https://files.ballistica.net/cache/ba1/34/dd/6e4078bb656380c2957407a65b5a", + "build/assets/ba_data/meshes/zoeForeArm.bob": "https://files.ballistica.net/cache/ba1/8f/db/8d342078352776373309d1fdac7d", + "build/assets/ba_data/meshes/zoeHand.bob": "https://files.ballistica.net/cache/ba1/fe/71/f21d96b0eda6813e19cf602000f9", + "build/assets/ba_data/meshes/zoeHead.bob": "https://files.ballistica.net/cache/ba1/6e/2b/57c8ec54682f00d87be3f14ebf0f", + "build/assets/ba_data/meshes/zoeLowerLeg.bob": "https://files.ballistica.net/cache/ba1/ac/3a/b595eb37bc2248cd9693ecbc300c", + "build/assets/ba_data/meshes/zoePelvis.bob": "https://files.ballistica.net/cache/ba1/76/fa/e59ecb05cbb372d4027d4ab38ab5", + "build/assets/ba_data/meshes/zoeToes.bob": "https://files.ballistica.net/cache/ba1/5f/4c/5ba3c7d2687230768c4bf3ab5ce0", + "build/assets/ba_data/meshes/zoeTorso.bob": "https://files.ballistica.net/cache/ba1/d1/ab/a863e69bef6751aa7553913ffac2", + "build/assets/ba_data/meshes/zoeUpperArm.bob": "https://files.ballistica.net/cache/ba1/33/87/7b5cd2778024e9ecfc7de79fa272", + "build/assets/ba_data/meshes/zoeUpperLeg.bob": "https://files.ballistica.net/cache/ba1/55/9c/ca1a1dcf9a4152463fff953ce3bc", + "build/assets/ba_data/python-site-packages/_yaml/__init__.py": "https://files.ballistica.net/cache/ba1/10/e2/33d720d96d8cb8845f263fa770e6", + "build/assets/ba_data/python-site-packages/certifi/__init__.py": "https://files.ballistica.net/cache/ba1/17/16/8c50bb571c66fd9e52c49c1205d2", + "build/assets/ba_data/python-site-packages/certifi/__main__.py": "https://files.ballistica.net/cache/ba1/16/9f/c2603f93ff256c915b721966ab7b", + "build/assets/ba_data/python-site-packages/certifi/cacert.pem": "https://files.ballistica.net/cache/ba1/43/33/f2953fd79a3e22e8fa8a521eaaf1", + "build/assets/ba_data/python-site-packages/certifi/core.py": "https://files.ballistica.net/cache/ba1/ad/fc/1254120c9a4ad8b016f6e6aae7ec", + "build/assets/ba_data/python-site-packages/typing_extensions.py": "https://files.ballistica.net/cache/ba1/2d/1f/48dc111fc94b7ad3de033867cee8", + "build/assets/ba_data/python-site-packages/yaml/__init__.py": "https://files.ballistica.net/cache/ba1/eb/6b/24e3019e43bc5c6b396705d5b180", + "build/assets/ba_data/python-site-packages/yaml/composer.py": "https://files.ballistica.net/cache/ba1/7c/f3/ac0de2bfa8809789d512e52c859f", + "build/assets/ba_data/python-site-packages/yaml/constructor.py": "https://files.ballistica.net/cache/ba1/c9/e0/0e95838925d2f3c495b94080930d", + "build/assets/ba_data/python-site-packages/yaml/cyaml.py": "https://files.ballistica.net/cache/ba1/ed/e7/79425b3cac19a4608f56130c6a0f", + "build/assets/ba_data/python-site-packages/yaml/dumper.py": "https://files.ballistica.net/cache/ba1/a9/38/6418d198f1ebeddcb7d0823c1d11", + "build/assets/ba_data/python-site-packages/yaml/emitter.py": "https://files.ballistica.net/cache/ba1/23/ef/abb46e17daef353d3766f01f4b8c", + "build/assets/ba_data/python-site-packages/yaml/error.py": "https://files.ballistica.net/cache/ba1/cb/65/36aa4ec95fef831bf7a8f1b5e0f7", + "build/assets/ba_data/python-site-packages/yaml/events.py": "https://files.ballistica.net/cache/ba1/ae/54/f95a199c3cf16264a5c768ffc17e", + "build/assets/ba_data/python-site-packages/yaml/loader.py": "https://files.ballistica.net/cache/ba1/7f/d9/ae018b4953c1c91756280ecb8b92", + "build/assets/ba_data/python-site-packages/yaml/nodes.py": "https://files.ballistica.net/cache/ba1/90/83/c24bfcc541acf7020544eecd037a", + "build/assets/ba_data/python-site-packages/yaml/parser.py": "https://files.ballistica.net/cache/ba1/1e/5b/bf56d0e64f2f7ee0ce639fd2a9b9", + "build/assets/ba_data/python-site-packages/yaml/reader.py": "https://files.ballistica.net/cache/ba1/ad/be/f89faea8adb3e6fe560aa6276af1", + "build/assets/ba_data/python-site-packages/yaml/representer.py": "https://files.ballistica.net/cache/ba1/87/14/e57791427b6db65cf224a643f363", + "build/assets/ba_data/python-site-packages/yaml/resolver.py": "https://files.ballistica.net/cache/ba1/74/75/d04af13c16c742593af3e404ce79", + "build/assets/ba_data/python-site-packages/yaml/scanner.py": "https://files.ballistica.net/cache/ba1/72/31/f7504e738d5e48563e28d8b13d44", + "build/assets/ba_data/python-site-packages/yaml/serializer.py": "https://files.ballistica.net/cache/ba1/4e/8b/d81dfcd71708585d03e03d908d1b", + "build/assets/ba_data/python-site-packages/yaml/tokens.py": "https://files.ballistica.net/cache/ba1/0c/79/c2f50aa2c5bfbf4657915ac683e0", + "build/assets/ba_data/textures/achievementBoxer.dds": "https://files.ballistica.net/cache/ba1/ba/02/b41354bb6475995d94f89c71e93f", + "build/assets/ba_data/textures/achievementBoxer.ktx": "https://files.ballistica.net/cache/ba1/ad/8b/7428f158b9d3d6b9e0417d5e3a94", + "build/assets/ba_data/textures/achievementBoxer.pvr": "https://files.ballistica.net/cache/ba1/b8/14/fb1c5251af94359ed718037259fa", + "build/assets/ba_data/textures/achievementBoxer_preview.png": "https://files.ballistica.net/cache/ba1/73/c2/ff5780b6b468d922cb9073550397", + "build/assets/ba_data/textures/achievementCrossHair.dds": "https://files.ballistica.net/cache/ba1/bf/c2/62d00115b84f65544440d24df299", + "build/assets/ba_data/textures/achievementCrossHair.ktx": "https://files.ballistica.net/cache/ba1/86/9a/d047b4a25277da752c9c260fa67a", + "build/assets/ba_data/textures/achievementCrossHair.pvr": "https://files.ballistica.net/cache/ba1/69/52/577c4c50d59254db7e0f7077bc2f", + "build/assets/ba_data/textures/achievementCrossHair_preview.png": "https://files.ballistica.net/cache/ba1/6e/81/f87e101e539ed05ea0770a535d8d", + "build/assets/ba_data/textures/achievementDualWielding.dds": "https://files.ballistica.net/cache/ba1/e9/13/81de9164a3752d4397f81b1e307a", + "build/assets/ba_data/textures/achievementDualWielding.ktx": "https://files.ballistica.net/cache/ba1/f5/2d/9797c9933aea7526973e179693cf", + "build/assets/ba_data/textures/achievementDualWielding.pvr": "https://files.ballistica.net/cache/ba1/b7/98/f6581b297a2a111dea8093318f86", + "build/assets/ba_data/textures/achievementDualWielding_preview.png": "https://files.ballistica.net/cache/ba1/59/24/eff18d53214ef2429e361ff7fd0c", + "build/assets/ba_data/textures/achievementEmpty.dds": "https://files.ballistica.net/cache/ba1/1c/d8/b8b04851e93301044d3e669e6f3c", + "build/assets/ba_data/textures/achievementEmpty.ktx": "https://files.ballistica.net/cache/ba1/96/8e/3d4836da8f98406f90a04262c167", + "build/assets/ba_data/textures/achievementEmpty.pvr": "https://files.ballistica.net/cache/ba1/19/bf/4e867089cda29016635744ab99f2", + "build/assets/ba_data/textures/achievementEmpty_preview.png": "https://files.ballistica.net/cache/ba1/67/38/d949b88b29dc90a6c13dadc89375", + "build/assets/ba_data/textures/achievementFlawlessVictory.dds": "https://files.ballistica.net/cache/ba1/db/92/838523af48f5646c07cf8afb9f35", + "build/assets/ba_data/textures/achievementFlawlessVictory.ktx": "https://files.ballistica.net/cache/ba1/a9/6b/5e7a92379aa2ff29079430da78ea", + "build/assets/ba_data/textures/achievementFlawlessVictory.pvr": "https://files.ballistica.net/cache/ba1/f3/9f/86a45f579f2a8561c2c597cddac3", + "build/assets/ba_data/textures/achievementFlawlessVictory_preview.png": "https://files.ballistica.net/cache/ba1/3c/32/1afb2ae006ed9d1ddbb9912a9763", + "build/assets/ba_data/textures/achievementFootballShutout.dds": "https://files.ballistica.net/cache/ba1/52/77/934791d499021024625b56c77167", + "build/assets/ba_data/textures/achievementFootballShutout.ktx": "https://files.ballistica.net/cache/ba1/6c/ed/c234c5464c0171b13cf5ceda5ec4", + "build/assets/ba_data/textures/achievementFootballShutout.pvr": "https://files.ballistica.net/cache/ba1/b2/86/563bef70563f7aa9f33e96743b42", + "build/assets/ba_data/textures/achievementFootballShutout_preview.png": "https://files.ballistica.net/cache/ba1/54/39/42ca97f7d20742c2ee7dcdf7a597", + "build/assets/ba_data/textures/achievementFootballVictory.dds": "https://files.ballistica.net/cache/ba1/13/e1/4fe72cf77791901c64e2f4cd8fa9", + "build/assets/ba_data/textures/achievementFootballVictory.ktx": "https://files.ballistica.net/cache/ba1/3a/3c/bda3f6f213e35d2c159cbadc8c24", + "build/assets/ba_data/textures/achievementFootballVictory.pvr": "https://files.ballistica.net/cache/ba1/7a/57/0ae2adcdfcdb7c9a4ce0b3ae3b5b", + "build/assets/ba_data/textures/achievementFootballVictory_preview.png": "https://files.ballistica.net/cache/ba1/64/79/b5d796b36e90ea0acfb5cc302da0", + "build/assets/ba_data/textures/achievementFreeLoader.dds": "https://files.ballistica.net/cache/ba1/0b/70/ad0143c5e9885cc992220d022f08", + "build/assets/ba_data/textures/achievementFreeLoader.ktx": "https://files.ballistica.net/cache/ba1/73/fd/98a3b2576ec05da45a75c47ff868", + "build/assets/ba_data/textures/achievementFreeLoader.pvr": "https://files.ballistica.net/cache/ba1/f3/31/b2d7b1d4ef707bddf4e3fbf6f02d", + "build/assets/ba_data/textures/achievementFreeLoader_preview.png": "https://files.ballistica.net/cache/ba1/5d/a5/beb2369580a32c14b61f6d86d5c0", + "build/assets/ba_data/textures/achievementGotTheMoves.dds": "https://files.ballistica.net/cache/ba1/ad/20/957172510833598a0a68e0e702c1", + "build/assets/ba_data/textures/achievementGotTheMoves.ktx": "https://files.ballistica.net/cache/ba1/9b/1c/ef408c8cf60830675be39c3596d9", + "build/assets/ba_data/textures/achievementGotTheMoves.pvr": "https://files.ballistica.net/cache/ba1/8d/3b/795f3e565f982ec27738bd5e86da", + "build/assets/ba_data/textures/achievementGotTheMoves_preview.png": "https://files.ballistica.net/cache/ba1/c8/12/a2b96c127cd281c9bd27cd603b85", + "build/assets/ba_data/textures/achievementInControl.dds": "https://files.ballistica.net/cache/ba1/a7/bc/20ccc85d654c2ded1b5cd8442388", + "build/assets/ba_data/textures/achievementInControl.ktx": "https://files.ballistica.net/cache/ba1/d8/88/13ec13d6ed5abb2e0a2dc316639c", + "build/assets/ba_data/textures/achievementInControl.pvr": "https://files.ballistica.net/cache/ba1/cd/a5/2436542f19d7bbcd1f0e1176c803", + "build/assets/ba_data/textures/achievementInControl_preview.png": "https://files.ballistica.net/cache/ba1/cc/30/01f234170830fd7cf4e856fabe37", + "build/assets/ba_data/textures/achievementMedalLarge.dds": "https://files.ballistica.net/cache/ba1/d5/70/3fb1affea21993bf15b1c3d72d87", + "build/assets/ba_data/textures/achievementMedalLarge.ktx": "https://files.ballistica.net/cache/ba1/70/f4/ddb1944bff5048e4e99e1db28de1", + "build/assets/ba_data/textures/achievementMedalLarge.pvr": "https://files.ballistica.net/cache/ba1/e6/4d/50e8b77768056792fc3f1430c130", + "build/assets/ba_data/textures/achievementMedalLarge_preview.png": "https://files.ballistica.net/cache/ba1/d4/96/b45f39b5176b8e6d5f78ec40913b", + "build/assets/ba_data/textures/achievementMedalMedium.dds": "https://files.ballistica.net/cache/ba1/8a/8f/4c51bdcf60f3adc1632356c97ecb", + "build/assets/ba_data/textures/achievementMedalMedium.ktx": "https://files.ballistica.net/cache/ba1/b5/7a/e16b8625a06858bd7fa4056b2de8", + "build/assets/ba_data/textures/achievementMedalMedium.pvr": "https://files.ballistica.net/cache/ba1/7d/0c/4b8b396cb3038bc75b151356ae0b", + "build/assets/ba_data/textures/achievementMedalMedium_preview.png": "https://files.ballistica.net/cache/ba1/ce/7b/445ee7cefd0058b44ba935d450e6", + "build/assets/ba_data/textures/achievementMedalSmall.dds": "https://files.ballistica.net/cache/ba1/96/d1/fe12ff5809da0abce179b798ae91", + "build/assets/ba_data/textures/achievementMedalSmall.ktx": "https://files.ballistica.net/cache/ba1/7c/77/387e5522a668fb093b3652d5413c", + "build/assets/ba_data/textures/achievementMedalSmall.pvr": "https://files.ballistica.net/cache/ba1/21/dd/0abeeee92257c08db6df671e43ac", + "build/assets/ba_data/textures/achievementMedalSmall_preview.png": "https://files.ballistica.net/cache/ba1/1e/57/0285045b920083546ec275e1d3a7", + "build/assets/ba_data/textures/achievementMine.dds": "https://files.ballistica.net/cache/ba1/2c/61/7f64e326ed12cfbbe6acfe284ad9", + "build/assets/ba_data/textures/achievementMine.ktx": "https://files.ballistica.net/cache/ba1/e6/4e/70f19f9e9caf5bd351f25ef8f8c1", + "build/assets/ba_data/textures/achievementMine.pvr": "https://files.ballistica.net/cache/ba1/df/6f/c88d75b0b51d54a07f139e1f80ee", + "build/assets/ba_data/textures/achievementMine_preview.png": "https://files.ballistica.net/cache/ba1/3f/c6/f12c73cc23266c0177ad9d91a0cb", + "build/assets/ba_data/textures/achievementOffYouGo.dds": "https://files.ballistica.net/cache/ba1/2f/38/3026e5f9d320c184cff9b718a9f6", + "build/assets/ba_data/textures/achievementOffYouGo.ktx": "https://files.ballistica.net/cache/ba1/7c/b4/0859af62ed367d7186fc9b668182", + "build/assets/ba_data/textures/achievementOffYouGo.pvr": "https://files.ballistica.net/cache/ba1/47/6b/63f4523fe29b6fe48465a2e3c227", + "build/assets/ba_data/textures/achievementOffYouGo_preview.png": "https://files.ballistica.net/cache/ba1/0b/d7/de6babc6f77145af4437c19d8ca3", + "build/assets/ba_data/textures/achievementOnslaught.dds": "https://files.ballistica.net/cache/ba1/bd/c6/ebf792c492b8fe247d9c4335a3a9", + "build/assets/ba_data/textures/achievementOnslaught.ktx": "https://files.ballistica.net/cache/ba1/a8/ab/c2da4d52f2cf0436979f18f229e3", + "build/assets/ba_data/textures/achievementOnslaught.pvr": "https://files.ballistica.net/cache/ba1/a0/fe/35a863b52a7984973d15db9157f4", + "build/assets/ba_data/textures/achievementOnslaught_preview.png": "https://files.ballistica.net/cache/ba1/ca/fa/cfaacacba4f4de354e6e5919165a", + "build/assets/ba_data/textures/achievementOutline.dds": "https://files.ballistica.net/cache/ba1/f1/bd/cdc09fd12161cea9af79ee4b46cf", + "build/assets/ba_data/textures/achievementOutline.ktx": "https://files.ballistica.net/cache/ba1/17/ca/107aee4503efef3d4748dddd0714", + "build/assets/ba_data/textures/achievementOutline.pvr": "https://files.ballistica.net/cache/ba1/d9/b4/a2cc91e6df285b4e5ee9191af774", + "build/assets/ba_data/textures/achievementOutline_preview.png": "https://files.ballistica.net/cache/ba1/d0/cf/55410a526332201621e5f7618a31", + "build/assets/ba_data/textures/achievementRunaround.dds": "https://files.ballistica.net/cache/ba1/ae/a0/88496eb6c679530f60b92d5b9f64", + "build/assets/ba_data/textures/achievementRunaround.ktx": "https://files.ballistica.net/cache/ba1/a6/6a/07553dc47acb31f273423992b000", + "build/assets/ba_data/textures/achievementRunaround.pvr": "https://files.ballistica.net/cache/ba1/a6/5a/4459d97a0c64300c5f2eceba2ca4", + "build/assets/ba_data/textures/achievementRunaround_preview.png": "https://files.ballistica.net/cache/ba1/5f/c3/76af67208b7c469ea8d58b202e2b", + "build/assets/ba_data/textures/achievementSharingIsCaring.dds": "https://files.ballistica.net/cache/ba1/d4/f8/b414c538c1accdff709dd1673d23", + "build/assets/ba_data/textures/achievementSharingIsCaring.ktx": "https://files.ballistica.net/cache/ba1/86/86/0cd1d4782cb23987ec1aecc03fce", + "build/assets/ba_data/textures/achievementSharingIsCaring.pvr": "https://files.ballistica.net/cache/ba1/ff/39/7c43dd2b29d6e08f11a3bc9bf759", + "build/assets/ba_data/textures/achievementSharingIsCaring_preview.png": "https://files.ballistica.net/cache/ba1/79/40/32966af9f907fc58e49928112381", + "build/assets/ba_data/textures/achievementStayinAlive.dds": "https://files.ballistica.net/cache/ba1/3c/10/5bbf07f69b69b32364070d2d0ee5", + "build/assets/ba_data/textures/achievementStayinAlive.ktx": "https://files.ballistica.net/cache/ba1/bd/45/0602b41597955910d955db86e525", + "build/assets/ba_data/textures/achievementStayinAlive.pvr": "https://files.ballistica.net/cache/ba1/dd/0e/1d4b7207239d59ffa2675f9e00de", + "build/assets/ba_data/textures/achievementStayinAlive_preview.png": "https://files.ballistica.net/cache/ba1/58/b4/fb0b6bda408828f3e76eab500e16", + "build/assets/ba_data/textures/achievementSuperPunch.dds": "https://files.ballistica.net/cache/ba1/a5/6a/5be5f8a53b1ff16e675a374cbd9c", + "build/assets/ba_data/textures/achievementSuperPunch.ktx": "https://files.ballistica.net/cache/ba1/82/6b/9cc79df5eacdbcde55c813204e53", + "build/assets/ba_data/textures/achievementSuperPunch.pvr": "https://files.ballistica.net/cache/ba1/17/fd/2772fd47655eaeea8fa6ca29906a", + "build/assets/ba_data/textures/achievementSuperPunch_preview.png": "https://files.ballistica.net/cache/ba1/d5/67/fe93ff9ce01a5c659368f13d07b3", + "build/assets/ba_data/textures/achievementTNT.dds": "https://files.ballistica.net/cache/ba1/db/68/79064a703e8a49c3f259b2963dcf", + "build/assets/ba_data/textures/achievementTNT.ktx": "https://files.ballistica.net/cache/ba1/a3/f1/2d343fbf8747d50652265ed42a64", + "build/assets/ba_data/textures/achievementTNT.pvr": "https://files.ballistica.net/cache/ba1/56/89/09d8ec644a91d570c28a112a9486", + "build/assets/ba_data/textures/achievementTNT_preview.png": "https://files.ballistica.net/cache/ba1/2c/ce/4547d109b965043c12544a3480ed", + "build/assets/ba_data/textures/achievementTeamPlayer.dds": "https://files.ballistica.net/cache/ba1/31/73/7006b32a9678bcddb0b5446a31d7", + "build/assets/ba_data/textures/achievementTeamPlayer.ktx": "https://files.ballistica.net/cache/ba1/3b/48/cf7e994befe13bc0d6c7ae987816", + "build/assets/ba_data/textures/achievementTeamPlayer.pvr": "https://files.ballistica.net/cache/ba1/23/4f/58f8b6873a549719c601c0698f37", + "build/assets/ba_data/textures/achievementTeamPlayer_preview.png": "https://files.ballistica.net/cache/ba1/a6/35/702f60b7a50661a3ace1f82f6d26", + "build/assets/ba_data/textures/achievementWall.dds": "https://files.ballistica.net/cache/ba1/3d/7a/b820daac58d2aa2c82bcaedceef9", + "build/assets/ba_data/textures/achievementWall.ktx": "https://files.ballistica.net/cache/ba1/28/b0/1490868ee3f567159e812a53bbb8", + "build/assets/ba_data/textures/achievementWall.pvr": "https://files.ballistica.net/cache/ba1/12/6c/78c2b0225439454e541a1468b5f4", + "build/assets/ba_data/textures/achievementWall_preview.png": "https://files.ballistica.net/cache/ba1/70/1f/6db5ef09014f63409258477a32d4", + "build/assets/ba_data/textures/achievementsIcon.dds": "https://files.ballistica.net/cache/ba1/3f/e1/ed01593777845adfeffe54ff1594", + "build/assets/ba_data/textures/achievementsIcon.ktx": "https://files.ballistica.net/cache/ba1/41/0b/4b131cf20d8dcadc0ad6b22c23ae", + "build/assets/ba_data/textures/achievementsIcon.pvr": "https://files.ballistica.net/cache/ba1/aa/6f/8b85d4afc6516c7f9242cc060dea", + "build/assets/ba_data/textures/achievementsIcon_preview.png": "https://files.ballistica.net/cache/ba1/20/4f/e7c8b7b40666d19a129de315c152", + "build/assets/ba_data/textures/actionButtons.dds": "https://files.ballistica.net/cache/ba1/90/6e/e9c1e89547a7290cd489584cfe1a", + "build/assets/ba_data/textures/actionButtons.ktx": "https://files.ballistica.net/cache/ba1/6c/24/f538480ec0d6a1280222a79abb48", + "build/assets/ba_data/textures/actionButtons.pvr": "https://files.ballistica.net/cache/ba1/d3/ba/11de1dc0fb3e9a7bf03ec4ffbd53", + "build/assets/ba_data/textures/actionButtons_preview.png": "https://files.ballistica.net/cache/ba1/4d/0b/a49338de6840d82c7df4d10fb9a2", + "build/assets/ba_data/textures/actionHeroColor.dds": "https://files.ballistica.net/cache/ba1/8a/bf/d9f0b835dbb0d01236313211793d", + "build/assets/ba_data/textures/actionHeroColor.ktx": "https://files.ballistica.net/cache/ba1/a0/aa/9f9a30ea4b985fbc15168cdcb20b", + "build/assets/ba_data/textures/actionHeroColor.pvr": "https://files.ballistica.net/cache/ba1/46/07/a7fd9af2e3e009f9e684ad4e5d5d", + "build/assets/ba_data/textures/actionHeroColorMask.dds": "https://files.ballistica.net/cache/ba1/a7/62/7cd76f6f06caff611b659be6216f", + "build/assets/ba_data/textures/actionHeroColorMask.ktx": "https://files.ballistica.net/cache/ba1/53/89/a1aa78de294daacba539155aa8e2", + "build/assets/ba_data/textures/actionHeroColorMask.pvr": "https://files.ballistica.net/cache/ba1/ab/b1/eca07711bf206df6f91f00fcdece", + "build/assets/ba_data/textures/actionHeroColorMask_preview.png": "https://files.ballistica.net/cache/ba1/33/16/94d4afa282b53092dd0848032f87", + "build/assets/ba_data/textures/actionHeroColor_preview.png": "https://files.ballistica.net/cache/ba1/46/83/6f24d87d918e1bb3c0fbf19d554a", + "build/assets/ba_data/textures/actionHeroIcon.dds": "https://files.ballistica.net/cache/ba1/fe/f8/6d9d9f588a97d306d44276351a99", + "build/assets/ba_data/textures/actionHeroIcon.ktx": "https://files.ballistica.net/cache/ba1/68/1a/0bd67a189944159fee424becfbbf", + "build/assets/ba_data/textures/actionHeroIcon.pvr": "https://files.ballistica.net/cache/ba1/94/ed/ac12633ac7c6137921d6f680906f", + "build/assets/ba_data/textures/actionHeroIconColorMask.dds": "https://files.ballistica.net/cache/ba1/2c/28/bd8efbfe9ccf2d1371ce8ec0357a", + "build/assets/ba_data/textures/actionHeroIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/e2/74/aca46bf59a2d9ab7a22ea2d85a98", + "build/assets/ba_data/textures/actionHeroIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/63/c4/a013089abf20fbf404fca8d44088", + "build/assets/ba_data/textures/actionHeroIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/c9/27/15f63d60aa0b0ce16992ebb20dba", + "build/assets/ba_data/textures/actionHeroIcon_preview.png": "https://files.ballistica.net/cache/ba1/91/27/34996ced0a88912bb3d57321f853", + "build/assets/ba_data/textures/advancedIcon.dds": "https://files.ballistica.net/cache/ba1/ce/74/2bd1785687193caad48123283f99", + "build/assets/ba_data/textures/advancedIcon.ktx": "https://files.ballistica.net/cache/ba1/90/8f/ed9082b7e811f7354ac2d67aa5f5", + "build/assets/ba_data/textures/advancedIcon.pvr": "https://files.ballistica.net/cache/ba1/1c/c0/6c3ef2e9bb43e1af3ec0dfb8b2e6", + "build/assets/ba_data/textures/advancedIcon_preview.png": "https://files.ballistica.net/cache/ba1/6c/ed/d44a6cf5e7b60a2735e8f1633ae4", + "build/assets/ba_data/textures/agentColor.dds": "https://files.ballistica.net/cache/ba1/d4/4d/f1759122e6b8ee52c4ee304aa7ec", + "build/assets/ba_data/textures/agentColor.ktx": "https://files.ballistica.net/cache/ba1/79/87/834c2d43dca5f0ae61392e538fc3", + "build/assets/ba_data/textures/agentColor.pvr": "https://files.ballistica.net/cache/ba1/a0/20/aede1430ff6c57d3fbc9ba310eab", + "build/assets/ba_data/textures/agentColorMask.dds": "https://files.ballistica.net/cache/ba1/63/48/ff80c09bace380ed8808acb04542", + "build/assets/ba_data/textures/agentColorMask.ktx": "https://files.ballistica.net/cache/ba1/83/0b/01eb3c031a726b6bb2d4b4f256b9", + "build/assets/ba_data/textures/agentColorMask.pvr": "https://files.ballistica.net/cache/ba1/e7/5b/86cecbdae80973573e9b6425c878", + "build/assets/ba_data/textures/agentColorMask_preview.png": "https://files.ballistica.net/cache/ba1/f1/a0/4db6402922fc1c786f4afaa59f58", + "build/assets/ba_data/textures/agentColor_preview.png": "https://files.ballistica.net/cache/ba1/bb/16/3c1e14372176faf7d4c7e6ec5afe", + "build/assets/ba_data/textures/agentIcon.dds": "https://files.ballistica.net/cache/ba1/eb/84/97b6df1d5696d947da81d53d971e", + "build/assets/ba_data/textures/agentIcon.ktx": "https://files.ballistica.net/cache/ba1/b1/76/6f475f637e8cfcc22c55a3875218", + "build/assets/ba_data/textures/agentIcon.pvr": "https://files.ballistica.net/cache/ba1/cb/e7/cc28d1b828134ab85056afa7b854", + "build/assets/ba_data/textures/agentIconColorMask.dds": "https://files.ballistica.net/cache/ba1/1e/82/b2cecb4f6929b091ff110873e759", + "build/assets/ba_data/textures/agentIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/52/a0/80f954996161ff2d451e1787fb31", + "build/assets/ba_data/textures/agentIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/68/d8/0c2e31bb9d5c37a3e0fe3c6c5f63", + "build/assets/ba_data/textures/agentIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/11/c8/53fdd77d5c7ce1194c6043516d3b", + "build/assets/ba_data/textures/agentIcon_preview.png": "https://files.ballistica.net/cache/ba1/39/09/ddecf28721fa8de79e9b2399b607", + "build/assets/ba_data/textures/aliBSRemoteIOSQR.dds": "https://files.ballistica.net/cache/ba1/b5/99/a278926345c6e7c7be1ed1a03ae7", + "build/assets/ba_data/textures/aliBSRemoteIOSQR.ktx": "https://files.ballistica.net/cache/ba1/b8/09/52c31cfa4d68ca6a2f8ca0e37640", + "build/assets/ba_data/textures/aliBSRemoteIOSQR.pvr": "https://files.ballistica.net/cache/ba1/12/3f/2ad8174c0e60063c37f01ba78330", + "build/assets/ba_data/textures/aliBSRemoteIOSQR_preview.png": "https://files.ballistica.net/cache/ba1/1c/aa/9a14b7e31ad19ed8d7050dbab1d0", + "build/assets/ba_data/textures/aliColor.dds": "https://files.ballistica.net/cache/ba1/59/31/c45a19f87f854f23225d019a4ac4", + "build/assets/ba_data/textures/aliColor.ktx": "https://files.ballistica.net/cache/ba1/44/bc/3777cd0aa6052aa6059ce9d12129", + "build/assets/ba_data/textures/aliColor.pvr": "https://files.ballistica.net/cache/ba1/d5/3f/e2da32ef5b3f4b4f0a3ad6b594ba", + "build/assets/ba_data/textures/aliColorMask.dds": "https://files.ballistica.net/cache/ba1/af/ae/5c7e388f71a0677f0b017c6fa1b1", + "build/assets/ba_data/textures/aliColorMask.ktx": "https://files.ballistica.net/cache/ba1/31/90/7c3502e7114ecdf4a424ef94126d", + "build/assets/ba_data/textures/aliColorMask.pvr": "https://files.ballistica.net/cache/ba1/30/61/f2207f03b1da77b64faa51705545", + "build/assets/ba_data/textures/aliColorMask_preview.png": "https://files.ballistica.net/cache/ba1/98/2c/4ba17e5be48a3c56138d9dbaa8d0", + "build/assets/ba_data/textures/aliColor_preview.png": "https://files.ballistica.net/cache/ba1/25/11/a33c2851908a8e1ec751305de64d", + "build/assets/ba_data/textures/aliControllerQR.dds": "https://files.ballistica.net/cache/ba1/d1/f0/1d0c807dd41dbc12b2fbc2abe5e1", + "build/assets/ba_data/textures/aliControllerQR.ktx": "https://files.ballistica.net/cache/ba1/f0/f9/d726755511b29bb6140855c6f54b", + "build/assets/ba_data/textures/aliControllerQR.pvr": "https://files.ballistica.net/cache/ba1/8c/0f/a585d9028632c4e0e404e96cb7a1", + "build/assets/ba_data/textures/aliControllerQR_preview.png": "https://files.ballistica.net/cache/ba1/26/db/85eb1fb8c3062c75f08daec5d9f2", + "build/assets/ba_data/textures/aliIcon.dds": "https://files.ballistica.net/cache/ba1/c9/99/d3f9a8dece7a30e759b61bd2c8c5", + "build/assets/ba_data/textures/aliIcon.ktx": "https://files.ballistica.net/cache/ba1/12/29/bccc5c8bc7b8420fc98e1d30e9b3", + "build/assets/ba_data/textures/aliIcon.pvr": "https://files.ballistica.net/cache/ba1/0e/b3/8130fa6a1c6ec319dafb0d9a29d1", + "build/assets/ba_data/textures/aliIconColorMask.dds": "https://files.ballistica.net/cache/ba1/2d/ca/4fcd521e1b6732c7e6a8f06738b2", + "build/assets/ba_data/textures/aliIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/9f/9e/21a2b0ef2aff946e388d54c21a6d", + "build/assets/ba_data/textures/aliIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/e1/3a/e2dce2961a29e98414c615c4d5ec", + "build/assets/ba_data/textures/aliIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/9a/af/ad1fff0f5c7c88a052053d084764", + "build/assets/ba_data/textures/aliIcon_preview.png": "https://files.ballistica.net/cache/ba1/61/de/c01770ff74130fc5f8c2b2f625bc", + "build/assets/ba_data/textures/aliSplash.dds": "https://files.ballistica.net/cache/ba1/8a/34/040f8bce29e9f30b6d4437052d41", + "build/assets/ba_data/textures/aliSplash.ktx": "https://files.ballistica.net/cache/ba1/3a/7a/de5dc21792b664b76cd82b21cc59", + "build/assets/ba_data/textures/aliSplash.pvr": "https://files.ballistica.net/cache/ba1/6a/17/2db5f772608c581e1f6e5d58f023", + "build/assets/ba_data/textures/aliSplash_preview.png": "https://files.ballistica.net/cache/ba1/1e/95/4240769b72679f1b006066de6376", + "build/assets/ba_data/textures/alienColor.dds": "https://files.ballistica.net/cache/ba1/8c/51/e0c0270d6219f98a2b32cde5aa0d", + "build/assets/ba_data/textures/alienColor.ktx": "https://files.ballistica.net/cache/ba1/ad/ea/c791d9693cea416128a9fffc0f12", + "build/assets/ba_data/textures/alienColor.pvr": "https://files.ballistica.net/cache/ba1/37/f7/0015c5b4ea0e5c20fd16f0c24060", + "build/assets/ba_data/textures/alienColorMask.dds": "https://files.ballistica.net/cache/ba1/4e/a6/f6e7ea952a6a1f598faeef850ca5", + "build/assets/ba_data/textures/alienColorMask.ktx": "https://files.ballistica.net/cache/ba1/5f/bc/528b6cb47bacbd4e910f518e8f2e", + "build/assets/ba_data/textures/alienColorMask.pvr": "https://files.ballistica.net/cache/ba1/6b/67/c745022d83870b695d41e54e0384", + "build/assets/ba_data/textures/alienColorMask_preview.png": "https://files.ballistica.net/cache/ba1/40/75/74616c2406927f0ed23ddc3ff5be", + "build/assets/ba_data/textures/alienColor_preview.png": "https://files.ballistica.net/cache/ba1/3a/51/6767b55667c3348f544e3e423573", + "build/assets/ba_data/textures/alienIcon.dds": "https://files.ballistica.net/cache/ba1/89/22/08bb3a97f9fd0f6560d046fb3ddc", + "build/assets/ba_data/textures/alienIcon.ktx": "https://files.ballistica.net/cache/ba1/c6/0d/4237dc5a450bbaa8e02334aee0f5", + "build/assets/ba_data/textures/alienIcon.pvr": "https://files.ballistica.net/cache/ba1/c7/91/bf8f10db48eb7d42d986771de592", + "build/assets/ba_data/textures/alienIconColorMask.dds": "https://files.ballistica.net/cache/ba1/8e/1f/4a300d570b739ab24dd5c53999dc", + "build/assets/ba_data/textures/alienIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/ba/4c/503433f562681e4448a5f83f9394", + "build/assets/ba_data/textures/alienIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/d4/76/c51478609f2ded2f9fe5aaa5cb5e", + "build/assets/ba_data/textures/alienIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/02/6b/ac9fb3afb762d8a92aaec2611010", + "build/assets/ba_data/textures/alienIcon_preview.png": "https://files.ballistica.net/cache/ba1/5a/ba/bddc46ce188de67a0d2937df884d", + "build/assets/ba_data/textures/alwaysLandBGColor.dds": "https://files.ballistica.net/cache/ba1/d9/52/61de0beabd1e1689fc29d0fd3859", + "build/assets/ba_data/textures/alwaysLandBGColor.ktx": "https://files.ballistica.net/cache/ba1/74/1e/9551db4fb2ca65da8c057f34517d", + "build/assets/ba_data/textures/alwaysLandBGColor.pvr": "https://files.ballistica.net/cache/ba1/ad/6f/f79533d806507af0dbf421d7a409", + "build/assets/ba_data/textures/alwaysLandBGColor_preview.png": "https://files.ballistica.net/cache/ba1/a7/17/89ace7136a850bd9cdad77c74ee2", + "build/assets/ba_data/textures/alwaysLandLevelColor.dds": "https://files.ballistica.net/cache/ba1/d5/95/06ee3d0738856a5221c0bd1dfb18", + "build/assets/ba_data/textures/alwaysLandLevelColor.ktx": "https://files.ballistica.net/cache/ba1/86/80/b3d16e223eab485d629622a81129", + "build/assets/ba_data/textures/alwaysLandLevelColor.pvr": "https://files.ballistica.net/cache/ba1/24/7a/cf16068c397830cd062b396f9a38", + "build/assets/ba_data/textures/alwaysLandLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/73/d2/2d89f86a43b431f2e584a8398c15", + "build/assets/ba_data/textures/alwaysLandPreview.dds": "https://files.ballistica.net/cache/ba1/da/b0/99c7f7a0ef9584a2456d8667c08e", + "build/assets/ba_data/textures/alwaysLandPreview.ktx": "https://files.ballistica.net/cache/ba1/b0/c5/932d6c73d7f0939c1bf15e5e19fa", + "build/assets/ba_data/textures/alwaysLandPreview.pvr": "https://files.ballistica.net/cache/ba1/cc/cb/0fadd85741cf6bfdcf60625ae253", + "build/assets/ba_data/textures/alwaysLandPreview_preview.png": "https://files.ballistica.net/cache/ba1/60/ce/0af46d85fa2f0d99385e81b954e7", + "build/assets/ba_data/textures/analogStick.dds": "https://files.ballistica.net/cache/ba1/9f/34/590c94111a6213186ef23b0d31cb", + "build/assets/ba_data/textures/analogStick.ktx": "https://files.ballistica.net/cache/ba1/49/cd/9618cd9fe41015b7029fc5268477", + "build/assets/ba_data/textures/analogStick.pvr": "https://files.ballistica.net/cache/ba1/32/8c/8abd323c2f7b58c717864e388d86", + "build/assets/ba_data/textures/analogStick_preview.png": "https://files.ballistica.net/cache/ba1/b5/6d/20aa7583bb5022c4c07628311f6e", + "build/assets/ba_data/textures/arrow.dds": "https://files.ballistica.net/cache/ba1/ae/06/85fa720639751cea026d93c896ee", + "build/assets/ba_data/textures/arrow.ktx": "https://files.ballistica.net/cache/ba1/ba/ce/98c7469dc85755f9c2f7955f044c", + "build/assets/ba_data/textures/arrow.pvr": "https://files.ballistica.net/cache/ba1/3d/9a/84a87862c7bf4bd499badd216854", + "build/assets/ba_data/textures/arrow_preview.png": "https://files.ballistica.net/cache/ba1/86/04/6333616b4756b8f490647f71441b", + "build/assets/ba_data/textures/assassinColor.dds": "https://files.ballistica.net/cache/ba1/88/c7/e1391f1bf20223cf7e60b7e10b74", + "build/assets/ba_data/textures/assassinColor.ktx": "https://files.ballistica.net/cache/ba1/2e/f2/592cd199d66596358fbd366eb167", + "build/assets/ba_data/textures/assassinColor.pvr": "https://files.ballistica.net/cache/ba1/fe/e2/de2411137dbb8814b688d54e1bfa", + "build/assets/ba_data/textures/assassinColorMask.dds": "https://files.ballistica.net/cache/ba1/72/01/b2f51cea6c81473ff03f17cc8d84", + "build/assets/ba_data/textures/assassinColorMask.ktx": "https://files.ballistica.net/cache/ba1/1b/52/c979efb609e7d105868bbcc50457", + "build/assets/ba_data/textures/assassinColorMask.pvr": "https://files.ballistica.net/cache/ba1/5b/1d/dff938ca8a9c6f51a1e9166429fa", + "build/assets/ba_data/textures/assassinColorMask_preview.png": "https://files.ballistica.net/cache/ba1/be/be/fa1d84ffd441be0902f481154a09", + "build/assets/ba_data/textures/assassinColor_preview.png": "https://files.ballistica.net/cache/ba1/36/1c/9a2f08ac660b901b21aa4842ebb7", + "build/assets/ba_data/textures/assassinIcon.dds": "https://files.ballistica.net/cache/ba1/b9/ac/222e50311c882d4897b0b6d8c3c0", + "build/assets/ba_data/textures/assassinIcon.ktx": "https://files.ballistica.net/cache/ba1/27/eb/3c845021c1eb0084c494c17249fb", + "build/assets/ba_data/textures/assassinIcon.pvr": "https://files.ballistica.net/cache/ba1/55/c6/1d1b95eee7c9dc2dbdff92e6c2c4", + "build/assets/ba_data/textures/assassinIconColorMask.dds": "https://files.ballistica.net/cache/ba1/19/c3/f3bbe2b2dfbc1de8dcc881c8b3ff", + "build/assets/ba_data/textures/assassinIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/3c/ad/c071820bae32471248cca663fb0d", + "build/assets/ba_data/textures/assassinIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/2d/27/cdcd7253459eecf2d6f181ee83fe", + "build/assets/ba_data/textures/assassinIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/c6/4b/fb4b5be34d3a31a21b3d517de96e", + "build/assets/ba_data/textures/assassinIcon_preview.png": "https://files.ballistica.net/cache/ba1/4a/3d/62135be3ebaaa16b8ffcf910c6f0", + "build/assets/ba_data/textures/audioIcon.dds": "https://files.ballistica.net/cache/ba1/61/b9/d633a6a0f57c9dc5f11b463ed1f6", + "build/assets/ba_data/textures/audioIcon.ktx": "https://files.ballistica.net/cache/ba1/d7/96/6403d5fdbc1afffafa2b5877e88d", + "build/assets/ba_data/textures/audioIcon.pvr": "https://files.ballistica.net/cache/ba1/fb/61/0dcab7a3df253b1f2fb041bfd434", + "build/assets/ba_data/textures/audioIcon_preview.png": "https://files.ballistica.net/cache/ba1/73/34/a492a32c30162b7fd317f25ff955", + "build/assets/ba_data/textures/backIcon.dds": "https://files.ballistica.net/cache/ba1/f3/7f/968e645439cc0efeb645d1c08df2", + "build/assets/ba_data/textures/backIcon.ktx": "https://files.ballistica.net/cache/ba1/0c/87/609dce2d0d3e9568697efd00f6a8", + "build/assets/ba_data/textures/backIcon.pvr": "https://files.ballistica.net/cache/ba1/13/a7/46a6b84d3e4d231b526f4fb9db60", + "build/assets/ba_data/textures/backIcon_preview.png": "https://files.ballistica.net/cache/ba1/2a/87/058f77ba5f6cc9dda9b82c853902", + "build/assets/ba_data/textures/bar.dds": "https://files.ballistica.net/cache/ba1/a8/fb/0f958008f32255fb33293629fcca", + "build/assets/ba_data/textures/bar.ktx": "https://files.ballistica.net/cache/ba1/96/0f/10d07e58b8ac4f617d6429bcc95e", + "build/assets/ba_data/textures/bar.pvr": "https://files.ballistica.net/cache/ba1/e6/3c/e6be20d8ab857faf001d2fafd92b", + "build/assets/ba_data/textures/bar_preview.png": "https://files.ballistica.net/cache/ba1/53/c7/ed74296590b52723f07302395c1c", + "build/assets/ba_data/textures/bearColor.dds": "https://files.ballistica.net/cache/ba1/4a/fe/d73823239f341a0292dd0acaa014", + "build/assets/ba_data/textures/bearColor.ktx": "https://files.ballistica.net/cache/ba1/81/47/5b26bad87a4cdfccc87ae15e4c01", + "build/assets/ba_data/textures/bearColor.pvr": "https://files.ballistica.net/cache/ba1/a2/a4/9902745e576cf0dfd30934ad06dd", + "build/assets/ba_data/textures/bearColorMask.dds": "https://files.ballistica.net/cache/ba1/2b/30/11d93a9ab40002343767b018690e", + "build/assets/ba_data/textures/bearColorMask.ktx": "https://files.ballistica.net/cache/ba1/b8/ae/ee2ff1d43dbd8d249522aa25de77", + "build/assets/ba_data/textures/bearColorMask.pvr": "https://files.ballistica.net/cache/ba1/7e/85/5be03243c046e83a8bcb9d3d7eaf", + "build/assets/ba_data/textures/bearColorMask_preview.png": "https://files.ballistica.net/cache/ba1/23/e6/477406d1e4bfaa6609c8faa2fcde", + "build/assets/ba_data/textures/bearColor_preview.png": "https://files.ballistica.net/cache/ba1/4f/ad/3e7b557f10b50445be4180b1b09e", + "build/assets/ba_data/textures/bearIcon.dds": "https://files.ballistica.net/cache/ba1/cb/b8/823b31302432e3855505ab31598a", + "build/assets/ba_data/textures/bearIcon.ktx": "https://files.ballistica.net/cache/ba1/08/8a/a3e0c34a2b35ca984a9148feea79", + "build/assets/ba_data/textures/bearIcon.pvr": "https://files.ballistica.net/cache/ba1/8b/ee/9141c837f25546ee1c3afd054942", + "build/assets/ba_data/textures/bearIconColorMask.dds": "https://files.ballistica.net/cache/ba1/ef/1e/0bbc17756f3df9759b588886ac4e", + "build/assets/ba_data/textures/bearIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/62/ab/c7f153d8d86e4cba02057bb9db0d", + "build/assets/ba_data/textures/bearIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/c1/64/dd837c9dc8dde4d741842e89a4b8", + "build/assets/ba_data/textures/bearIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/d0/13/24f83d5d3950a05b3134b143e19b", + "build/assets/ba_data/textures/bearIcon_preview.png": "https://files.ballistica.net/cache/ba1/88/b8/cf612a5e532c3a8ccd52d9489da5", + "build/assets/ba_data/textures/bg.dds": "https://files.ballistica.net/cache/ba1/3d/b0/5db6d28d77a9ed9716412fe2a8e9", + "build/assets/ba_data/textures/bg.ktx": "https://files.ballistica.net/cache/ba1/92/16/ab3a3d57d039f2811a7fcff11434", + "build/assets/ba_data/textures/bg.pvr": "https://files.ballistica.net/cache/ba1/76/cc/4e920bdd90d4a26b8e185ba0793b", + "build/assets/ba_data/textures/bg_preview.png": "https://files.ballistica.net/cache/ba1/99/bc/a7bb9b51c3c7085193e45837c818", + "build/assets/ba_data/textures/bigG.dds": "https://files.ballistica.net/cache/ba1/7b/10/817589b282af819d678d345c2cfb", + "build/assets/ba_data/textures/bigG.ktx": "https://files.ballistica.net/cache/ba1/44/31/8e656874423be2d3154723deb897", + "build/assets/ba_data/textures/bigG.pvr": "https://files.ballistica.net/cache/ba1/90/4b/340c88475bf2d45f45553cbe87fd", + "build/assets/ba_data/textures/bigGPreview.dds": "https://files.ballistica.net/cache/ba1/ac/b6/759ef931c85e2289af9b922951c9", + "build/assets/ba_data/textures/bigGPreview.ktx": "https://files.ballistica.net/cache/ba1/29/17/5be0b485c9afd616e91a8cd095f0", + "build/assets/ba_data/textures/bigGPreview.pvr": "https://files.ballistica.net/cache/ba1/23/c8/4fb12018dd5963abd9588d1479f8", + "build/assets/ba_data/textures/bigGPreview_preview.png": "https://files.ballistica.net/cache/ba1/2a/ff/523c1c60ae4f2f1e0e3f51b11fd2", + "build/assets/ba_data/textures/bigG_preview.png": "https://files.ballistica.net/cache/ba1/a9/5c/f746db7a99e41111974ce92560fe", + "build/assets/ba_data/textures/black.dds": "https://files.ballistica.net/cache/ba1/87/32/b3f53394addf4b9a4cb23e6be694", + "build/assets/ba_data/textures/black.ktx": "https://files.ballistica.net/cache/ba1/f0/f3/ed208dc52d8b42621637d33ba4cf", + "build/assets/ba_data/textures/black.pvr": "https://files.ballistica.net/cache/ba1/e6/0a/c85b4b85fec551447b4367efdff1", + "build/assets/ba_data/textures/black_preview.png": "https://files.ballistica.net/cache/ba1/17/d2/ddc038dfd3b6d7754b3ed386cc42", + "build/assets/ba_data/textures/bombButton.dds": "https://files.ballistica.net/cache/ba1/9c/dc/dde5d13bf1dd6847c213776e6d70", + "build/assets/ba_data/textures/bombButton.ktx": "https://files.ballistica.net/cache/ba1/e3/ad/c2d9bfcb5bb30971b9defdd04d56", + "build/assets/ba_data/textures/bombButton.pvr": "https://files.ballistica.net/cache/ba1/93/27/1e6612372c8ae9284356fa8db595", + "build/assets/ba_data/textures/bombButton_preview.png": "https://files.ballistica.net/cache/ba1/5f/c9/73eeabc67b4cd9475e33a44cd441", + "build/assets/ba_data/textures/bombColor.dds": "https://files.ballistica.net/cache/ba1/f8/62/e1b0d0fb171d4d2cc4451f7f7c54", + "build/assets/ba_data/textures/bombColor.ktx": "https://files.ballistica.net/cache/ba1/77/55/8a0232b1d5856517939b4d038578", + "build/assets/ba_data/textures/bombColor.pvr": "https://files.ballistica.net/cache/ba1/7a/c1/6ac7491ed3d50a2a08bb8f07e0c1", + "build/assets/ba_data/textures/bombColorIce.dds": "https://files.ballistica.net/cache/ba1/36/bd/ade4f74b2365e54616eacd854adf", + "build/assets/ba_data/textures/bombColorIce.ktx": "https://files.ballistica.net/cache/ba1/33/87/3c268ef65d0cd3189d2cfc6b310a", + "build/assets/ba_data/textures/bombColorIce.pvr": "https://files.ballistica.net/cache/ba1/9c/fb/f42cb597d263b0b2e57b8bccc6f9", + "build/assets/ba_data/textures/bombColorIce_preview.png": "https://files.ballistica.net/cache/ba1/9e/5a/b593b7c1f2d48db66d75476d3a54", + "build/assets/ba_data/textures/bombColor_preview.png": "https://files.ballistica.net/cache/ba1/64/18/f138e27af2f8abca392b2ee8a766", + "build/assets/ba_data/textures/bombStickyColor.dds": "https://files.ballistica.net/cache/ba1/76/eb/60177ea973b85645b8bf859e1568", + "build/assets/ba_data/textures/bombStickyColor.ktx": "https://files.ballistica.net/cache/ba1/fe/c6/6e93bc8ab3a126ff92b2982da93b", + "build/assets/ba_data/textures/bombStickyColor.pvr": "https://files.ballistica.net/cache/ba1/e4/cb/91104ff5f2f6919b551ce6081f90", + "build/assets/ba_data/textures/bombStickyColor_preview.png": "https://files.ballistica.net/cache/ba1/14/95/204097c0390b265b799dea259376", + "build/assets/ba_data/textures/bonesColor.dds": "https://files.ballistica.net/cache/ba1/ad/95/bb6b3e80c8c57adbd34e9ac27d32", + "build/assets/ba_data/textures/bonesColor.ktx": "https://files.ballistica.net/cache/ba1/3c/d6/8dc66b6fa87bd13aead80d017850", + "build/assets/ba_data/textures/bonesColor.pvr": "https://files.ballistica.net/cache/ba1/f2/93/63037f6da7ce8d6202b594b74230", + "build/assets/ba_data/textures/bonesColorMask.dds": "https://files.ballistica.net/cache/ba1/30/5e/b6a147139616aecc2d3fc3986420", + "build/assets/ba_data/textures/bonesColorMask.ktx": "https://files.ballistica.net/cache/ba1/78/fb/6fd2eea647cc23bdc2ac2272809a", + "build/assets/ba_data/textures/bonesColorMask.pvr": "https://files.ballistica.net/cache/ba1/9d/de/b1ddb509770ee4ae670ecaba735f", + "build/assets/ba_data/textures/bonesColorMask_preview.png": "https://files.ballistica.net/cache/ba1/6c/a1/8a39f4fc976122ac45c12a6391f4", + "build/assets/ba_data/textures/bonesColor_preview.png": "https://files.ballistica.net/cache/ba1/0a/7e/5191ad47ecd3de3b6098dd85066b", + "build/assets/ba_data/textures/bonesIcon.dds": "https://files.ballistica.net/cache/ba1/dc/ae/2feb5330ca425b4318e097ea7a95", + "build/assets/ba_data/textures/bonesIcon.ktx": "https://files.ballistica.net/cache/ba1/8f/7d/5d53798196058fc01721decf42d4", + "build/assets/ba_data/textures/bonesIcon.pvr": "https://files.ballistica.net/cache/ba1/14/fa/d446595678cb32971b1de1d085cc", + "build/assets/ba_data/textures/bonesIconColorMask.dds": "https://files.ballistica.net/cache/ba1/b0/0f/8a33bc6680488e3343eb3fe7d16c", + "build/assets/ba_data/textures/bonesIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/cc/28/91a076e08b2a234d8638e795f8ea", + "build/assets/ba_data/textures/bonesIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/b2/77/e2b400fa415e857234df3410593f", + "build/assets/ba_data/textures/bonesIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/dc/47/fd088d1f35cd86b5a488bb91a926", + "build/assets/ba_data/textures/bonesIcon_preview.png": "https://files.ballistica.net/cache/ba1/66/32/c133a86382341b485b3c402347ad", + "build/assets/ba_data/textures/boxingGlovesColor.dds": "https://files.ballistica.net/cache/ba1/b6/a4/599da0c733c1315dce62a19a0d43", + "build/assets/ba_data/textures/boxingGlovesColor.ktx": "https://files.ballistica.net/cache/ba1/4c/07/5536900bb50c87d9ffef83d9e8ae", + "build/assets/ba_data/textures/boxingGlovesColor.pvr": "https://files.ballistica.net/cache/ba1/fc/81/5e868455032e99b93f221278255b", + "build/assets/ba_data/textures/boxingGlovesColor_preview.png": "https://files.ballistica.net/cache/ba1/c8/e1/2bbac7129a948f81d5534c4ebba5", + "build/assets/ba_data/textures/bridgitLevelColor.dds": "https://files.ballistica.net/cache/ba1/9f/9e/db8b8b95b875a870e39a207808ca", + "build/assets/ba_data/textures/bridgitLevelColor.ktx": "https://files.ballistica.net/cache/ba1/70/fa/bc6f8b64efb376c5b3d9e4f8027a", + "build/assets/ba_data/textures/bridgitLevelColor.pvr": "https://files.ballistica.net/cache/ba1/94/89/6e1a0933dd81240ebedd7eb14ddf", + "build/assets/ba_data/textures/bridgitLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/f1/cb/4e46767fa42f6040052c3ae705de", + "build/assets/ba_data/textures/bridgitPreview.dds": "https://files.ballistica.net/cache/ba1/d0/25/69532eb8bd98442fe0813ff68035", + "build/assets/ba_data/textures/bridgitPreview.ktx": "https://files.ballistica.net/cache/ba1/b2/a8/f06c88d54892ed1aef92d22aeb26", + "build/assets/ba_data/textures/bridgitPreview.pvr": "https://files.ballistica.net/cache/ba1/53/ed/880b320ec72ce2526245102ed52f", + "build/assets/ba_data/textures/bridgitPreview_preview.png": "https://files.ballistica.net/cache/ba1/f6/8e/8fa4f979fda2b0d9b4aa78736717", + "build/assets/ba_data/textures/bunnyColor.dds": "https://files.ballistica.net/cache/ba1/0b/b8/9093653e66aa39f1f12b81d71011", + "build/assets/ba_data/textures/bunnyColor.ktx": "https://files.ballistica.net/cache/ba1/e0/d6/acfc83747128d66d6106e7110bf9", + "build/assets/ba_data/textures/bunnyColor.pvr": "https://files.ballistica.net/cache/ba1/2b/2f/673e0a70847cb5318ef2a5c8950b", + "build/assets/ba_data/textures/bunnyColorMask.dds": "https://files.ballistica.net/cache/ba1/5d/97/6976450e804dec9c4313813163c9", + "build/assets/ba_data/textures/bunnyColorMask.ktx": "https://files.ballistica.net/cache/ba1/2f/e3/1933e869b1bee9d4f286745a043b", + "build/assets/ba_data/textures/bunnyColorMask.pvr": "https://files.ballistica.net/cache/ba1/18/a8/80cd7f67ec75120067715160c039", + "build/assets/ba_data/textures/bunnyColorMask_preview.png": "https://files.ballistica.net/cache/ba1/19/fa/e83b822dec9d4289180a8cad719d", + "build/assets/ba_data/textures/bunnyColor_preview.png": "https://files.ballistica.net/cache/ba1/39/09/1ec82c507355c313fd991affd148", + "build/assets/ba_data/textures/bunnyIcon.dds": "https://files.ballistica.net/cache/ba1/af/f3/61bfc788818228824d0276f9b3c1", + "build/assets/ba_data/textures/bunnyIcon.ktx": "https://files.ballistica.net/cache/ba1/f5/2f/91b22cc9b61855a0b3ed3be2a7ee", + "build/assets/ba_data/textures/bunnyIcon.pvr": "https://files.ballistica.net/cache/ba1/9f/3e/b21d141004f5810406bd93cbb452", + "build/assets/ba_data/textures/bunnyIconColorMask.dds": "https://files.ballistica.net/cache/ba1/d1/08/120b128fb23403ec28601a9d52e7", + "build/assets/ba_data/textures/bunnyIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/56/57/35aea42954b379dfdeb67d6fd424", + "build/assets/ba_data/textures/bunnyIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/cd/de/9dbbadafb2073d421c4ef7226f9a", + "build/assets/ba_data/textures/bunnyIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/c1/8b/cdd960ff8af4f808214c6d7e8b3b", + "build/assets/ba_data/textures/bunnyIcon_preview.png": "https://files.ballistica.net/cache/ba1/9c/35/28a5b8b4a88c1823f46e8b883773", + "build/assets/ba_data/textures/buttonBomb.dds": "https://files.ballistica.net/cache/ba1/9b/67/454226c00678e9171a2ee0f2f762", + "build/assets/ba_data/textures/buttonBomb.ktx": "https://files.ballistica.net/cache/ba1/98/4b/a4fdcece8b299e8dbbc3da772f22", + "build/assets/ba_data/textures/buttonBomb.pvr": "https://files.ballistica.net/cache/ba1/a8/5c/bcf9dbc2955a5f8a5c6dfce348b1", + "build/assets/ba_data/textures/buttonBomb_preview.png": "https://files.ballistica.net/cache/ba1/07/51/9e0bbbd9163fe457fa93c53d5ee2", + "build/assets/ba_data/textures/buttonJump.dds": "https://files.ballistica.net/cache/ba1/a7/5a/126a3fd41f0f780f79cddc9af62b", + "build/assets/ba_data/textures/buttonJump.ktx": "https://files.ballistica.net/cache/ba1/8a/fe/f26e292fb412f155dd30ddf1503b", + "build/assets/ba_data/textures/buttonJump.pvr": "https://files.ballistica.net/cache/ba1/a7/7b/af65741acce5750aed8ef3021000", + "build/assets/ba_data/textures/buttonJump_preview.png": "https://files.ballistica.net/cache/ba1/a7/26/8d16ca8ba430243ebecda700bd7a", + "build/assets/ba_data/textures/buttonPickUp.dds": "https://files.ballistica.net/cache/ba1/c8/1d/4cf0b74196d3d1b2e9edcb26a54f", + "build/assets/ba_data/textures/buttonPickUp.ktx": "https://files.ballistica.net/cache/ba1/6e/53/5ed06b158970cece32fb26a3d3a2", + "build/assets/ba_data/textures/buttonPickUp.pvr": "https://files.ballistica.net/cache/ba1/16/78/a0d2a0783aa8df0e86756945ef4d", + "build/assets/ba_data/textures/buttonPickUp_preview.png": "https://files.ballistica.net/cache/ba1/2a/43/d83464c960085f25bbc00e1387db", + "build/assets/ba_data/textures/buttonPunch.dds": "https://files.ballistica.net/cache/ba1/ac/67/12807cee3dab67e6d6a4385e3bf4", + "build/assets/ba_data/textures/buttonPunch.ktx": "https://files.ballistica.net/cache/ba1/0c/8f/6ad61abdb5eaa89cbec54590bdc7", + "build/assets/ba_data/textures/buttonPunch.pvr": "https://files.ballistica.net/cache/ba1/ff/45/5a885403b5f832e59913facd5bd6", + "build/assets/ba_data/textures/buttonPunch_preview.png": "https://files.ballistica.net/cache/ba1/01/34/107dfe52184076638b046f4f7974", + "build/assets/ba_data/textures/buttonSquare.dds": "https://files.ballistica.net/cache/ba1/cc/71/da7db6ebe660e1b120cef0690533", + "build/assets/ba_data/textures/buttonSquare.ktx": "https://files.ballistica.net/cache/ba1/40/fe/addba91142cd63e118d0be14bf3e", + "build/assets/ba_data/textures/buttonSquare.pvr": "https://files.ballistica.net/cache/ba1/82/a8/7e82a822843712336f6e3aa5d821", + "build/assets/ba_data/textures/buttonSquare_preview.png": "https://files.ballistica.net/cache/ba1/ec/9f/09a2ad16a97d6f955a91520e47b9", + "build/assets/ba_data/textures/chTitleChar1.dds": "https://files.ballistica.net/cache/ba1/3b/ce/cea7014fc6a7a0bcb34bdfefc61a", + "build/assets/ba_data/textures/chTitleChar1.ktx": "https://files.ballistica.net/cache/ba1/9e/d7/02d972c07846010e38af871ea2a3", + "build/assets/ba_data/textures/chTitleChar1.pvr": "https://files.ballistica.net/cache/ba1/74/df/c768d75b415ff2543da749b4ba12", + "build/assets/ba_data/textures/chTitleChar1_preview.png": "https://files.ballistica.net/cache/ba1/06/3e/642a6c81fa7e75d609b80330a254", + "build/assets/ba_data/textures/chTitleChar2.dds": "https://files.ballistica.net/cache/ba1/b0/ec/05bce4a548bddf67a0f8e7ee83da", + "build/assets/ba_data/textures/chTitleChar2.ktx": "https://files.ballistica.net/cache/ba1/80/d0/100b5632ea7fee7246756c35eda9", + "build/assets/ba_data/textures/chTitleChar2.pvr": "https://files.ballistica.net/cache/ba1/6a/e0/2d1d9807f4d1ad49979a053f3f35", + "build/assets/ba_data/textures/chTitleChar2_preview.png": "https://files.ballistica.net/cache/ba1/e4/38/bacae444e5331f02bb142dd29c1e", + "build/assets/ba_data/textures/chTitleChar3.dds": "https://files.ballistica.net/cache/ba1/a1/2a/e16d8bb698c42b953fb5594e9546", + "build/assets/ba_data/textures/chTitleChar3.ktx": "https://files.ballistica.net/cache/ba1/34/bb/8f70f5c019878be99362722b2c59", + "build/assets/ba_data/textures/chTitleChar3.pvr": "https://files.ballistica.net/cache/ba1/d2/88/e5d8d5737b56c9e799574ba3329c", + "build/assets/ba_data/textures/chTitleChar3_preview.png": "https://files.ballistica.net/cache/ba1/bc/3d/a3536d1d2543a6e85bfd2418bd0c", + "build/assets/ba_data/textures/chTitleChar4.dds": "https://files.ballistica.net/cache/ba1/2d/cc/73cf734bb777bb6c428b71c50ee5", + "build/assets/ba_data/textures/chTitleChar4.ktx": "https://files.ballistica.net/cache/ba1/96/a8/5bc9ddbee37a62a3458d548348ae", + "build/assets/ba_data/textures/chTitleChar4.pvr": "https://files.ballistica.net/cache/ba1/79/c5/387d002727a4d83123eff96960c3", + "build/assets/ba_data/textures/chTitleChar4_preview.png": "https://files.ballistica.net/cache/ba1/97/6e/5a35fccdeb9c28b07e3c3cf6997c", + "build/assets/ba_data/textures/chTitleChar5.dds": "https://files.ballistica.net/cache/ba1/48/52/fe11785a8fb28da888dd0386402a", + "build/assets/ba_data/textures/chTitleChar5.ktx": "https://files.ballistica.net/cache/ba1/21/f0/a01ac847425b9467ca2196e5c45b", + "build/assets/ba_data/textures/chTitleChar5.pvr": "https://files.ballistica.net/cache/ba1/f6/e1/6ae509b021022643955081c72984", + "build/assets/ba_data/textures/chTitleChar5_preview.png": "https://files.ballistica.net/cache/ba1/19/87/26fd103ea09ff77b6d28c073f4cd", + "build/assets/ba_data/textures/characterIconMask.dds": "https://files.ballistica.net/cache/ba1/e7/71/799b2de7b95d330901adfc805054", + "build/assets/ba_data/textures/characterIconMask.ktx": "https://files.ballistica.net/cache/ba1/98/2c/fd6d6f1e3f1d65ac1bc538a2ec25", + "build/assets/ba_data/textures/characterIconMask.pvr": "https://files.ballistica.net/cache/ba1/33/71/2413101781f947ad827b1535e08d", + "build/assets/ba_data/textures/characterIconMask_preview.png": "https://files.ballistica.net/cache/ba1/f7/b4/330726f0976378753321fdef9e2e", + "build/assets/ba_data/textures/chestIcon.dds": "https://files.ballistica.net/cache/ba1/c5/20/b6aa5053305ade29700b06b5004d", + "build/assets/ba_data/textures/chestIcon.ktx": "https://files.ballistica.net/cache/ba1/96/8f/ca775cbd04bae5b3fb990e6e90fd", + "build/assets/ba_data/textures/chestIcon.pvr": "https://files.ballistica.net/cache/ba1/96/c0/d10dc212db4deb6142aab0714c3c", + "build/assets/ba_data/textures/chestIconEmpty.dds": "https://files.ballistica.net/cache/ba1/5f/a3/23d09954f4e10834a559767ff699", + "build/assets/ba_data/textures/chestIconEmpty.ktx": "https://files.ballistica.net/cache/ba1/76/4b/85f824591681a29fc891caeac69c", + "build/assets/ba_data/textures/chestIconEmpty.pvr": "https://files.ballistica.net/cache/ba1/72/56/19e70527ee4840fcc87414c53700", + "build/assets/ba_data/textures/chestIconEmpty_preview.png": "https://files.ballistica.net/cache/ba1/1b/9a/f1b04ef2e0dbf739e9c7b8d3d79a", + "build/assets/ba_data/textures/chestIconMulti.dds": "https://files.ballistica.net/cache/ba1/e0/c4/eb403a8c04a54b5faa3ee239050d", + "build/assets/ba_data/textures/chestIconMulti.ktx": "https://files.ballistica.net/cache/ba1/c7/db/58a58ffb7729dfe9535b13d7ed72", + "build/assets/ba_data/textures/chestIconMulti.pvr": "https://files.ballistica.net/cache/ba1/be/d5/b435176fd415a241fa9dd136f76f", + "build/assets/ba_data/textures/chestIconMulti_preview.png": "https://files.ballistica.net/cache/ba1/e6/82/d748ef4e502f2e19075a92acad30", + "build/assets/ba_data/textures/chestIcon_preview.png": "https://files.ballistica.net/cache/ba1/90/50/1822507fd4793bbe655db749d965", + "build/assets/ba_data/textures/chestOpenIcon.dds": "https://files.ballistica.net/cache/ba1/44/f1/f136b988842b284fafb6873bb334", + "build/assets/ba_data/textures/chestOpenIcon.ktx": "https://files.ballistica.net/cache/ba1/7f/bf/19fabe1f71df6d06be8761838e2f", + "build/assets/ba_data/textures/chestOpenIcon.pvr": "https://files.ballistica.net/cache/ba1/40/80/4c25f8f89abc021441aaceb5abf0", + "build/assets/ba_data/textures/chestOpenIcon_preview.png": "https://files.ballistica.net/cache/ba1/c1/73/3682af54647684bf9436d99c53a6", + "build/assets/ba_data/textures/circle.dds": "https://files.ballistica.net/cache/ba1/ff/36/1fe5974cf800ce88e365015512d6", + "build/assets/ba_data/textures/circle.ktx": "https://files.ballistica.net/cache/ba1/d0/42/e1152b773d0ca758d14d2c8be2e4", + "build/assets/ba_data/textures/circle.pvr": "https://files.ballistica.net/cache/ba1/5d/75/2df73a6a458cdedf19880825ce36", + "build/assets/ba_data/textures/circleNoAlpha.dds": "https://files.ballistica.net/cache/ba1/25/12/6080f085de19c9bf794e9b250ffa", + "build/assets/ba_data/textures/circleNoAlpha.ktx": "https://files.ballistica.net/cache/ba1/52/b5/9239f6d763d1d773563c3cb03fe3", + "build/assets/ba_data/textures/circleNoAlpha.pvr": "https://files.ballistica.net/cache/ba1/7f/c4/a1581e4885d17326be7a4511f3cc", + "build/assets/ba_data/textures/circleNoAlpha_preview.png": "https://files.ballistica.net/cache/ba1/4f/19/6afc6a7a073ed0b3a14b3f545960", + "build/assets/ba_data/textures/circleOutline.dds": "https://files.ballistica.net/cache/ba1/73/f8/b79eaab0b77ee2436ac800b899fd", + "build/assets/ba_data/textures/circleOutline.ktx": "https://files.ballistica.net/cache/ba1/0d/2b/335dd4011ad9f400bea6db6827f4", + "build/assets/ba_data/textures/circleOutline.pvr": "https://files.ballistica.net/cache/ba1/1b/5f/c252df132fd547cbe2ec7ae9105a", + "build/assets/ba_data/textures/circleOutlineNoAlpha.dds": "https://files.ballistica.net/cache/ba1/5a/51/03d7e16f31b9af1c225e1b18b1c0", + "build/assets/ba_data/textures/circleOutlineNoAlpha.ktx": "https://files.ballistica.net/cache/ba1/92/2d/4b40eb5ff82590818fe84fccbe94", + "build/assets/ba_data/textures/circleOutlineNoAlpha.pvr": "https://files.ballistica.net/cache/ba1/13/48/2ccb6ed57b35cfda2f19b6ac6a1b", + "build/assets/ba_data/textures/circleOutlineNoAlpha_preview.png": "https://files.ballistica.net/cache/ba1/92/43/bea6f979b3477761a04ec52bc05c", + "build/assets/ba_data/textures/circleOutline_preview.png": "https://files.ballistica.net/cache/ba1/b5/d7/698eb681065efc063be855c03c6b", + "build/assets/ba_data/textures/circleShadow.dds": "https://files.ballistica.net/cache/ba1/61/28/6d92e966122325e0ce314d8ad589", + "build/assets/ba_data/textures/circleShadow.ktx": "https://files.ballistica.net/cache/ba1/83/62/4c0581181a1cbb28e2db7324c5f4", + "build/assets/ba_data/textures/circleShadow.pvr": "https://files.ballistica.net/cache/ba1/af/77/33bc2a5f53bc919a853aeca82c6c", + "build/assets/ba_data/textures/circleShadow_preview.png": "https://files.ballistica.net/cache/ba1/61/49/38d24aa900f672506f9f0496383c", + "build/assets/ba_data/textures/circleZigZag.dds": "https://files.ballistica.net/cache/ba1/c2/80/4cd87346b9a426e245f420b3cbb2", + "build/assets/ba_data/textures/circleZigZag.ktx": "https://files.ballistica.net/cache/ba1/b8/97/4f3aab1fad878d833c6d0e381bb2", + "build/assets/ba_data/textures/circleZigZag.pvr": "https://files.ballistica.net/cache/ba1/3a/0f/dc8aacced53f0d76534e03048653", + "build/assets/ba_data/textures/circleZigZag_preview.png": "https://files.ballistica.net/cache/ba1/7b/9e/781ae8fa62569cbfe893bb1a28c2", + "build/assets/ba_data/textures/circle_preview.png": "https://files.ballistica.net/cache/ba1/4c/f2/7ddb1f50b7fad1a71b2abefc1aa0", + "build/assets/ba_data/textures/coin.dds": "https://files.ballistica.net/cache/ba1/55/8e/a653aefb1dbe7ab8dd2697c9b349", + "build/assets/ba_data/textures/coin.ktx": "https://files.ballistica.net/cache/ba1/6f/c3/2256756577d9f1348c3e9e029520", + "build/assets/ba_data/textures/coin.pvr": "https://files.ballistica.net/cache/ba1/d7/ca/5fe74a039cd8220fd804148bd34e", + "build/assets/ba_data/textures/coin_preview.png": "https://files.ballistica.net/cache/ba1/5a/53/5a2916c7048424927b13134b69cc", + "build/assets/ba_data/textures/controllerIcon.dds": "https://files.ballistica.net/cache/ba1/8d/50/79a179f63f5ca7486bb316277456", + "build/assets/ba_data/textures/controllerIcon.ktx": "https://files.ballistica.net/cache/ba1/23/0c/0ea5883fc425a19b43498bc45020", + "build/assets/ba_data/textures/controllerIcon.pvr": "https://files.ballistica.net/cache/ba1/fb/d0/8c08402b52e284a5088fbf3a5521", + "build/assets/ba_data/textures/controllerIcon_preview.png": "https://files.ballistica.net/cache/ba1/11/29/3f155d67eeaef097d36a22ddb260", + "build/assets/ba_data/textures/courtyardLevelColor.dds": "https://files.ballistica.net/cache/ba1/82/2a/623e82289dac3733541671ac8525", + "build/assets/ba_data/textures/courtyardLevelColor.ktx": "https://files.ballistica.net/cache/ba1/c3/c9/815db19141c1bd35eb431629f359", + "build/assets/ba_data/textures/courtyardLevelColor.pvr": "https://files.ballistica.net/cache/ba1/30/81/3d48f3afbb083b8928ce25a334a8", + "build/assets/ba_data/textures/courtyardLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/05/09/f15f41ba3692519ce725a34e2b5e", + "build/assets/ba_data/textures/courtyardPreview.dds": "https://files.ballistica.net/cache/ba1/7e/64/dd7807ce3d2c852507f4fa3e5c4a", + "build/assets/ba_data/textures/courtyardPreview.ktx": "https://files.ballistica.net/cache/ba1/ef/69/6e6836d43d470b57b3dbcfda054e", + "build/assets/ba_data/textures/courtyardPreview.pvr": "https://files.ballistica.net/cache/ba1/72/51/8b5ee7762caf264cee6a2c374956", + "build/assets/ba_data/textures/courtyardPreview_preview.png": "https://files.ballistica.net/cache/ba1/19/b6/e86ee3609c93eb80536485c9297f", + "build/assets/ba_data/textures/cowboyColor.dds": "https://files.ballistica.net/cache/ba1/e8/cf/be66361b493c5a18dd5f6c557d9c", + "build/assets/ba_data/textures/cowboyColor.ktx": "https://files.ballistica.net/cache/ba1/6f/6e/8a347075c69a33caedfd1b67a2c0", + "build/assets/ba_data/textures/cowboyColor.pvr": "https://files.ballistica.net/cache/ba1/c8/9c/2b0b7fd25f28a2f38017562fd311", + "build/assets/ba_data/textures/cowboyColorMask.dds": "https://files.ballistica.net/cache/ba1/78/35/222c20734aacc1abaa8f947eddab", + "build/assets/ba_data/textures/cowboyColorMask.ktx": "https://files.ballistica.net/cache/ba1/1c/de/b6d8d28df76bb41f22fd68e2eb74", + "build/assets/ba_data/textures/cowboyColorMask.pvr": "https://files.ballistica.net/cache/ba1/83/67/cf6b4cdd62b1129c20fbc86e76e9", + "build/assets/ba_data/textures/cowboyColorMask_preview.png": "https://files.ballistica.net/cache/ba1/b2/1f/9d3ab0b45776b8717363c075dd12", + "build/assets/ba_data/textures/cowboyColor_preview.png": "https://files.ballistica.net/cache/ba1/a9/aa/6bfdaa005caba7ed2abeaba9e2ea", + "build/assets/ba_data/textures/cowboyIcon.dds": "https://files.ballistica.net/cache/ba1/65/f0/4f2259d843b47b048a0233fcd51b", + "build/assets/ba_data/textures/cowboyIcon.ktx": "https://files.ballistica.net/cache/ba1/47/29/e828792364dc77bd0ea05998ad38", + "build/assets/ba_data/textures/cowboyIcon.pvr": "https://files.ballistica.net/cache/ba1/f5/a1/a70a6a3fbba63ff4302d4c1bea2c", + "build/assets/ba_data/textures/cowboyIconColorMask.dds": "https://files.ballistica.net/cache/ba1/5a/e7/0d80eccdac219d6688e30255833d", + "build/assets/ba_data/textures/cowboyIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/6e/85/b4ecb01ca5d14627068f672db065", + "build/assets/ba_data/textures/cowboyIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/e7/a6/636b0944f1a2e8daf6cbbd487e9a", + "build/assets/ba_data/textures/cowboyIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/ca/98/7d6fe6f02110da6abb65b31cb654", + "build/assets/ba_data/textures/cowboyIcon_preview.png": "https://files.ballistica.net/cache/ba1/3d/0f/db57d8208d95b37ce2496e8d8478", + "build/assets/ba_data/textures/cragCastleLevelColor.dds": "https://files.ballistica.net/cache/ba1/34/ed/b16b37cfe31d1a9138ba889e7aff", + "build/assets/ba_data/textures/cragCastleLevelColor.ktx": "https://files.ballistica.net/cache/ba1/a9/67/c4bcede98269c2940a7b3c91eed8", + "build/assets/ba_data/textures/cragCastleLevelColor.pvr": "https://files.ballistica.net/cache/ba1/6c/1d/18b445899b829dd05fcb6182d582", + "build/assets/ba_data/textures/cragCastleLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/09/ad/953f80749011d7c3d29dd9e011bf", + "build/assets/ba_data/textures/cragCastlePreview.dds": "https://files.ballistica.net/cache/ba1/5a/e1/6b6554e1028d0cf3a1500f560c26", + "build/assets/ba_data/textures/cragCastlePreview.ktx": "https://files.ballistica.net/cache/ba1/62/07/13b6797cdc410b0aece9a8c9bd42", + "build/assets/ba_data/textures/cragCastlePreview.pvr": "https://files.ballistica.net/cache/ba1/a4/0c/6b0d5f394a8d58bc1340e0d26144", + "build/assets/ba_data/textures/cragCastlePreview_preview.png": "https://files.ballistica.net/cache/ba1/1a/df/2d270217ae843282d87e3b04417f", + "build/assets/ba_data/textures/crossOut.dds": "https://files.ballistica.net/cache/ba1/ed/6c/7c3ed0d5077a311fac57e6a841c1", + "build/assets/ba_data/textures/crossOut.ktx": "https://files.ballistica.net/cache/ba1/e7/2f/6e45adaa46069d71e6aa41f63161", + "build/assets/ba_data/textures/crossOut.pvr": "https://files.ballistica.net/cache/ba1/65/94/9cea748b0904ed83a15cf1d53e32", + "build/assets/ba_data/textures/crossOutMask.dds": "https://files.ballistica.net/cache/ba1/d3/b3/b4c01c896a97b186996fed036a88", + "build/assets/ba_data/textures/crossOutMask.ktx": "https://files.ballistica.net/cache/ba1/4e/33/2992ae7898eaf2cf23091bb19107", + "build/assets/ba_data/textures/crossOutMask.pvr": "https://files.ballistica.net/cache/ba1/fe/ed/0a8fdf337f068bd2e856dc797739", + "build/assets/ba_data/textures/crossOutMask_preview.png": "https://files.ballistica.net/cache/ba1/42/be/c791a34696ca07a83030fa819ebd", + "build/assets/ba_data/textures/crossOut_preview.png": "https://files.ballistica.net/cache/ba1/45/1d/d402491fd081cd16110a4201f979", + "build/assets/ba_data/textures/cursor.dds": "https://files.ballistica.net/cache/ba1/12/8b/4bb6e16df2e6f6b7bab126c54569", + "build/assets/ba_data/textures/cursor.ktx": "https://files.ballistica.net/cache/ba1/6c/ec/babbc4dded2ea7a96c14ad28f3c2", + "build/assets/ba_data/textures/cursor.pvr": "https://files.ballistica.net/cache/ba1/7e/c1/c713edd0c98aa0deb963c1bf6c8d", + "build/assets/ba_data/textures/cursor_preview.png": "https://files.ballistica.net/cache/ba1/46/21/b99f3970c92ae3a7e74bcadc4248", + "build/assets/ba_data/textures/cuteSpaz.dds": "https://files.ballistica.net/cache/ba1/d9/69/1041af8c895a35613f9d66294b4e", + "build/assets/ba_data/textures/cuteSpaz.ktx": "https://files.ballistica.net/cache/ba1/d7/84/48cc94dd0ab78c8fa58b23b119cf", + "build/assets/ba_data/textures/cuteSpaz.pvr": "https://files.ballistica.net/cache/ba1/f4/3b/09db0ff8bd3deb432d9cdded83f8", + "build/assets/ba_data/textures/cuteSpaz_preview.png": "https://files.ballistica.net/cache/ba1/03/27/cae4336f94ebee0d752ea9fa3d0d", + "build/assets/ba_data/textures/cyborgColor.dds": "https://files.ballistica.net/cache/ba1/37/c7/57014f0e0e57eebb4023bcf22d87", + "build/assets/ba_data/textures/cyborgColor.ktx": "https://files.ballistica.net/cache/ba1/46/2e/0e1eda0b25375c99bbe5feb238ba", + "build/assets/ba_data/textures/cyborgColor.pvr": "https://files.ballistica.net/cache/ba1/36/a3/9e715fb9f8290208b16c8c4be52d", + "build/assets/ba_data/textures/cyborgColorMask.dds": "https://files.ballistica.net/cache/ba1/8f/eb/97b6625e35fa9d0a57d9617a2cda", + "build/assets/ba_data/textures/cyborgColorMask.ktx": "https://files.ballistica.net/cache/ba1/01/ae/d033bf17e0d044c87138b8ffca87", + "build/assets/ba_data/textures/cyborgColorMask.pvr": "https://files.ballistica.net/cache/ba1/53/cf/0565d88ef56a3a685d5540ba12a0", + "build/assets/ba_data/textures/cyborgColorMask_preview.png": "https://files.ballistica.net/cache/ba1/7e/e7/7c5534965160625987e8e518fb3e", + "build/assets/ba_data/textures/cyborgColor_preview.png": "https://files.ballistica.net/cache/ba1/5f/44/37151e5fa27c53edf6d452ef38f3", + "build/assets/ba_data/textures/cyborgIcon.dds": "https://files.ballistica.net/cache/ba1/57/4b/feab9c1161639656ce4915dcf245", + "build/assets/ba_data/textures/cyborgIcon.ktx": "https://files.ballistica.net/cache/ba1/fc/bc/93320bcb2d37bfe03340ca7a4981", + "build/assets/ba_data/textures/cyborgIcon.pvr": "https://files.ballistica.net/cache/ba1/54/6d/50b5fab22f82c459b6a464c66eaa", + "build/assets/ba_data/textures/cyborgIconColorMask.dds": "https://files.ballistica.net/cache/ba1/43/ba/60217c2ac5e520191e36a12d5c15", + "build/assets/ba_data/textures/cyborgIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/da/98/2a0d017ba3477126362c24d0224c", + "build/assets/ba_data/textures/cyborgIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/dd/53/eef7c2fa08301788fe031a223c68", + "build/assets/ba_data/textures/cyborgIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/46/4d/d35c97866ae47813205ae0fd050f", + "build/assets/ba_data/textures/cyborgIcon_preview.png": "https://files.ballistica.net/cache/ba1/15/5e/9998d2b921accde0135b319aab0e", + "build/assets/ba_data/textures/discordLogo.dds": "https://files.ballistica.net/cache/ba1/4f/71/d2065d87f0757d101ae3e1ec97c6", + "build/assets/ba_data/textures/discordLogo.ktx": "https://files.ballistica.net/cache/ba1/05/66/51fcf6c8d668942d5fdce58b5bf8", + "build/assets/ba_data/textures/discordLogo.pvr": "https://files.ballistica.net/cache/ba1/2e/8e/379a5289e582ca4af1ee36a63b89", + "build/assets/ba_data/textures/discordLogo_preview.png": "https://files.ballistica.net/cache/ba1/56/29/3ae4082d12484d7658e4db914f2b", + "build/assets/ba_data/textures/doomShroomBGColor.dds": "https://files.ballistica.net/cache/ba1/b5/9d/cda147d436355113171901245107", + "build/assets/ba_data/textures/doomShroomBGColor.ktx": "https://files.ballistica.net/cache/ba1/3d/ec/b2011bce98b2fa12e0c7c39fa414", + "build/assets/ba_data/textures/doomShroomBGColor.pvr": "https://files.ballistica.net/cache/ba1/d6/5c/017301bf5f34c96e29237987c336", + "build/assets/ba_data/textures/doomShroomBGColor_preview.png": "https://files.ballistica.net/cache/ba1/bf/1a/1ef94eb0b8d7fa625b97f1d51b40", + "build/assets/ba_data/textures/doomShroomLevelColor.dds": "https://files.ballistica.net/cache/ba1/f9/de/3a1383cc6bd7b65465c6932c3175", + "build/assets/ba_data/textures/doomShroomLevelColor.ktx": "https://files.ballistica.net/cache/ba1/dc/22/390453522bf123bbe583fbecbca8", + "build/assets/ba_data/textures/doomShroomLevelColor.pvr": "https://files.ballistica.net/cache/ba1/bf/53/e6844c1fc4b4ad93b446d0149ef7", + "build/assets/ba_data/textures/doomShroomLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/6f/e9/8583c14c018bca8e708e218fe0aa", + "build/assets/ba_data/textures/doomShroomPreview.dds": "https://files.ballistica.net/cache/ba1/21/32/8f514b0158ffd92d1649edc251de", + "build/assets/ba_data/textures/doomShroomPreview.ktx": "https://files.ballistica.net/cache/ba1/73/ef/b402a56aeda3c48ddc55a8b49d97", + "build/assets/ba_data/textures/doomShroomPreview.pvr": "https://files.ballistica.net/cache/ba1/8d/62/11a47179dd01d6fcf4292ccc1315", + "build/assets/ba_data/textures/doomShroomPreview_preview.png": "https://files.ballistica.net/cache/ba1/4e/a3/51da5912e52bfa226b63338a71d2", + "build/assets/ba_data/textures/downButton.dds": "https://files.ballistica.net/cache/ba1/55/c6/993f3b850ffc39549671c0782fdd", + "build/assets/ba_data/textures/downButton.ktx": "https://files.ballistica.net/cache/ba1/6b/e6/6cf9bf6b5b290813db1bc59476c9", + "build/assets/ba_data/textures/downButton.pvr": "https://files.ballistica.net/cache/ba1/ea/af/69c079160782f409fd726fb3e587", + "build/assets/ba_data/textures/downButton_preview.png": "https://files.ballistica.net/cache/ba1/9b/2f/4853a5351071c0874daa14fc5575", + "build/assets/ba_data/textures/egg1.dds": "https://files.ballistica.net/cache/ba1/ee/b1/ef7a2ceabf35e2ddc537c3a14d28", + "build/assets/ba_data/textures/egg1.ktx": "https://files.ballistica.net/cache/ba1/0c/4a/634e1863908c6c3566abda449560", + "build/assets/ba_data/textures/egg1.pvr": "https://files.ballistica.net/cache/ba1/a3/f5/7d1046ae148488e00fd90ac69e19", + "build/assets/ba_data/textures/egg1_preview.png": "https://files.ballistica.net/cache/ba1/53/6f/deb64b55401e2ca17d86de6f0e8d", + "build/assets/ba_data/textures/egg2.dds": "https://files.ballistica.net/cache/ba1/a5/29/1fc934339712642eedb75a8a17db", + "build/assets/ba_data/textures/egg2.ktx": "https://files.ballistica.net/cache/ba1/12/1d/8ecb69281f92d065943854897b3e", + "build/assets/ba_data/textures/egg2.pvr": "https://files.ballistica.net/cache/ba1/53/9e/8e8936a1358a3902bcca5fb00746", + "build/assets/ba_data/textures/egg2_preview.png": "https://files.ballistica.net/cache/ba1/6f/19/bf3d34bc8b1d25e5c867e60b0c84", + "build/assets/ba_data/textures/egg3.dds": "https://files.ballistica.net/cache/ba1/c5/e1/feaf2bd5a983e08e233d04ac8eb0", + "build/assets/ba_data/textures/egg3.ktx": "https://files.ballistica.net/cache/ba1/87/10/df5bb2f0d079a77285b84d3bf3aa", + "build/assets/ba_data/textures/egg3.pvr": "https://files.ballistica.net/cache/ba1/8c/e5/051214224a1cfa4922781696d565", + "build/assets/ba_data/textures/egg3_preview.png": "https://files.ballistica.net/cache/ba1/b8/d3/ec0eed3d9658d85ed717bcc83505", + "build/assets/ba_data/textures/egg4.dds": "https://files.ballistica.net/cache/ba1/57/a0/b0fd9d017fdde2863fadbdc4270a", + "build/assets/ba_data/textures/egg4.ktx": "https://files.ballistica.net/cache/ba1/68/50/0199a2a9e71589921cc6f91b46dc", + "build/assets/ba_data/textures/egg4.pvr": "https://files.ballistica.net/cache/ba1/c2/ea/3f94c8a20eae60006eee113f022e", + "build/assets/ba_data/textures/egg4_preview.png": "https://files.ballistica.net/cache/ba1/4c/e6/8344374ee33fb241d8dedb04f380", + "build/assets/ba_data/textures/eggTex1.dds": "https://files.ballistica.net/cache/ba1/dc/6e/41fad9bdd243ab53adf35edf77f6", + "build/assets/ba_data/textures/eggTex1.ktx": "https://files.ballistica.net/cache/ba1/c2/92/88e2504de402cd62e7c9f498870a", + "build/assets/ba_data/textures/eggTex1.pvr": "https://files.ballistica.net/cache/ba1/b8/c1/08e874f297046a1703cb6c434bc1", + "build/assets/ba_data/textures/eggTex1_preview.png": "https://files.ballistica.net/cache/ba1/d1/bb/9444a5d21778b3be1db04ad8fe0d", + "build/assets/ba_data/textures/eggTex2.dds": "https://files.ballistica.net/cache/ba1/23/53/5daa4691d360039ea0b84288d29b", + "build/assets/ba_data/textures/eggTex2.ktx": "https://files.ballistica.net/cache/ba1/31/78/03471200120ff6346fed0034695e", + "build/assets/ba_data/textures/eggTex2.pvr": "https://files.ballistica.net/cache/ba1/58/dc/68e865937a9f20ad8e06ec77dda4", + "build/assets/ba_data/textures/eggTex2_preview.png": "https://files.ballistica.net/cache/ba1/5b/df/8c2f124a540fbf93040c75f0539d", + "build/assets/ba_data/textures/eggTex3.dds": "https://files.ballistica.net/cache/ba1/6a/1c/aae1b64ae01fcdd8c1cebcc708de", + "build/assets/ba_data/textures/eggTex3.ktx": "https://files.ballistica.net/cache/ba1/3e/44/386c84a7c9641ef5905734baeaeb", + "build/assets/ba_data/textures/eggTex3.pvr": "https://files.ballistica.net/cache/ba1/bb/a0/9cd54c962727fab07124210d40e8", + "build/assets/ba_data/textures/eggTex3_preview.png": "https://files.ballistica.net/cache/ba1/ba/5d/33c757a56cc10587328ec19b379c", + "build/assets/ba_data/textures/empty.dds": "https://files.ballistica.net/cache/ba1/a5/d8/0399fc979de802124f4be846d4da", + "build/assets/ba_data/textures/empty.ktx": "https://files.ballistica.net/cache/ba1/9b/9d/b236c45edebb7f194352036679e9", + "build/assets/ba_data/textures/empty.pvr": "https://files.ballistica.net/cache/ba1/9c/6e/34655e7707d27243b02d7e45a36f", + "build/assets/ba_data/textures/empty_preview.png": "https://files.ballistica.net/cache/ba1/5c/8d/8197cc2fb6d95d9e3d20e984f67a", + "build/assets/ba_data/textures/explosion.dds": "https://files.ballistica.net/cache/ba1/69/32/5b1838cb4c8ce746a0a4c0b4122b", + "build/assets/ba_data/textures/explosion.ktx": "https://files.ballistica.net/cache/ba1/f9/84/2c7ed662f0975abc564918b78487", + "build/assets/ba_data/textures/explosion.pvr": "https://files.ballistica.net/cache/ba1/1c/c0/de1fd3b2289df0cc6dd5cf31418d", + "build/assets/ba_data/textures/explosion_preview.png": "https://files.ballistica.net/cache/ba1/f7/48/2ce29ecf7ba7278b1724b18245f1", + "build/assets/ba_data/textures/eyeColor.dds": "https://files.ballistica.net/cache/ba1/64/45/a1d9b447f78d6ed5e4d9160a0e0c", + "build/assets/ba_data/textures/eyeColor.ktx": "https://files.ballistica.net/cache/ba1/1c/a2/c1d8f9bdd7d686317d792f2c714a", + "build/assets/ba_data/textures/eyeColor.pvr": "https://files.ballistica.net/cache/ba1/6b/1c/1d2a558e17789ffcac881ef73302", + "build/assets/ba_data/textures/eyeColorTintMask.dds": "https://files.ballistica.net/cache/ba1/e0/2c/f0bc8a0972149b449470b9876cae", + "build/assets/ba_data/textures/eyeColorTintMask.ktx": "https://files.ballistica.net/cache/ba1/aa/a1/2962b44579f9b28824b1cad7370c", + "build/assets/ba_data/textures/eyeColorTintMask.pvr": "https://files.ballistica.net/cache/ba1/26/af/9ac99ac54c9bc689de07cbff3e11", + "build/assets/ba_data/textures/eyeColorTintMask_preview.png": "https://files.ballistica.net/cache/ba1/db/8e/c1293880a7c0964ef30800519cac", + "build/assets/ba_data/textures/eyeColor_preview.png": "https://files.ballistica.net/cache/ba1/e9/1e/bd0205125cc16a2f4c16f774a9ff", + "build/assets/ba_data/textures/file.dds": "https://files.ballistica.net/cache/ba1/e9/18/ec4cf459bb1b3a77b050f0e24e5d", + "build/assets/ba_data/textures/file.ktx": "https://files.ballistica.net/cache/ba1/cf/a1/ea4036c8a363ac7a1599fe785dd3", + "build/assets/ba_data/textures/file.pvr": "https://files.ballistica.net/cache/ba1/60/89/8931d474063673ac1f7d55328f8b", + "build/assets/ba_data/textures/file_preview.png": "https://files.ballistica.net/cache/ba1/ad/74/d976d0351b5fbfbc8edba86cc040", + "build/assets/ba_data/textures/flagColor.dds": "https://files.ballistica.net/cache/ba1/d8/21/8a7441317db81eb42c7368362f0e", + "build/assets/ba_data/textures/flagColor.ktx": "https://files.ballistica.net/cache/ba1/bb/da/2cd38c0fffc39d44f6cc010c1fcd", + "build/assets/ba_data/textures/flagColor.pvr": "https://files.ballistica.net/cache/ba1/d3/e8/3bfa62a4f6ae403c4ed532abbdbf", + "build/assets/ba_data/textures/flagColor_preview.png": "https://files.ballistica.net/cache/ba1/16/51/610084b028cd994bf42e1964c945", + "build/assets/ba_data/textures/flagPoleColor.dds": "https://files.ballistica.net/cache/ba1/14/45/7e743f3841c4c0d37b1431e326a4", + "build/assets/ba_data/textures/flagPoleColor.ktx": "https://files.ballistica.net/cache/ba1/94/db/e16d5231bf7b24a027a147a4b389", + "build/assets/ba_data/textures/flagPoleColor.pvr": "https://files.ballistica.net/cache/ba1/12/7b/7e0d44c848e369557a6a59839a05", + "build/assets/ba_data/textures/flagPoleColor_preview.png": "https://files.ballistica.net/cache/ba1/e2/5a/f8f3fb698b400064df017800ece7", + "build/assets/ba_data/textures/folder.dds": "https://files.ballistica.net/cache/ba1/38/12/cc5f1a1e8ea12987c4d650c7567d", + "build/assets/ba_data/textures/folder.ktx": "https://files.ballistica.net/cache/ba1/b5/30/9f8fb1bba7ee4ce42145fd1d6afb", + "build/assets/ba_data/textures/folder.pvr": "https://files.ballistica.net/cache/ba1/48/a4/82376dc3a3a224ccfebd619f5b65", + "build/assets/ba_data/textures/folder_preview.png": "https://files.ballistica.net/cache/ba1/4d/7d/4cff78443183a5eb8313743b4cdc", + "build/assets/ba_data/textures/fontBig.dds": "https://files.ballistica.net/cache/ba1/95/51/54ae3ef5097bf69f37560f315df3", + "build/assets/ba_data/textures/fontBig.ktx": "https://files.ballistica.net/cache/ba1/04/fb/b9689f16463c20e916508c5d381b", + "build/assets/ba_data/textures/fontBig.pvr": "https://files.ballistica.net/cache/ba1/32/51/fd3857fd7fbd7de0fef8317406d0", + "build/assets/ba_data/textures/fontBig_preview.png": "https://files.ballistica.net/cache/ba1/ad/5c/288695c5d0286e7f4bd011fbfcc6", + "build/assets/ba_data/textures/fontExtras.dds": "https://files.ballistica.net/cache/ba1/fe/84/5ba50f4c2c434a3322a46df22c88", + "build/assets/ba_data/textures/fontExtras.ktx": "https://files.ballistica.net/cache/ba1/24/0d/ae209f8be961be62ebbccbf88cee", + "build/assets/ba_data/textures/fontExtras.pvr": "https://files.ballistica.net/cache/ba1/b2/15/06a4cec8ffaa7445281aae174296", + "build/assets/ba_data/textures/fontExtras2.dds": "https://files.ballistica.net/cache/ba1/a9/03/2320762b4883c2472f7581905b7c", + "build/assets/ba_data/textures/fontExtras2.ktx": "https://files.ballistica.net/cache/ba1/4d/bb/b6db0a25d7ce2a599768234e1fe1", + "build/assets/ba_data/textures/fontExtras2.pvr": "https://files.ballistica.net/cache/ba1/31/26/ad77cfb9befedcfb7a0c673d62cc", + "build/assets/ba_data/textures/fontExtras2_preview.png": "https://files.ballistica.net/cache/ba1/af/06/c2c8439cb97f05803f4d7ed6701f", + "build/assets/ba_data/textures/fontExtras3.dds": "https://files.ballistica.net/cache/ba1/08/80/0927ca927dfec052868043f409a3", + "build/assets/ba_data/textures/fontExtras3.ktx": "https://files.ballistica.net/cache/ba1/fc/84/15847ca7f0a3bc1286c19ef07d8b", + "build/assets/ba_data/textures/fontExtras3.pvr": "https://files.ballistica.net/cache/ba1/8e/bd/b24fc1774b15b29731695497695c", + "build/assets/ba_data/textures/fontExtras3_preview.png": "https://files.ballistica.net/cache/ba1/aa/28/e9fd0779072eb6319dc8e9061a56", + "build/assets/ba_data/textures/fontExtras4.dds": "https://files.ballistica.net/cache/ba1/b7/95/0e750cddb0608e8b2cbc015c5d47", + "build/assets/ba_data/textures/fontExtras4.ktx": "https://files.ballistica.net/cache/ba1/16/d1/129f78859591a8e5640f4849f1e5", + "build/assets/ba_data/textures/fontExtras4.pvr": "https://files.ballistica.net/cache/ba1/c9/22/7af2fb9de29a7dd147c8a8c9e422", + "build/assets/ba_data/textures/fontExtras4_preview.png": "https://files.ballistica.net/cache/ba1/66/91/7bb9dfd3fcadfe5b4f6170cd30f4", + "build/assets/ba_data/textures/fontExtras_preview.png": "https://files.ballistica.net/cache/ba1/48/37/0e42a51547abac7631483edb599e", + "build/assets/ba_data/textures/fontSmall0.dds": "https://files.ballistica.net/cache/ba1/f0/02/17adbc12ad051df787981edd1e19", + "build/assets/ba_data/textures/fontSmall0.ktx": "https://files.ballistica.net/cache/ba1/cd/8f/17cbbef97995f52e706563d70cad", + "build/assets/ba_data/textures/fontSmall0.pvr": "https://files.ballistica.net/cache/ba1/79/75/c5fc56941f9f11056a3f5de6c450", + "build/assets/ba_data/textures/fontSmall0_preview.png": "https://files.ballistica.net/cache/ba1/96/68/dd267f67ae391bcf288f7c5ca3ae", + "build/assets/ba_data/textures/fontSmall1.dds": "https://files.ballistica.net/cache/ba1/ba/69/e001ce799412902481e7b3ca6b04", + "build/assets/ba_data/textures/fontSmall1.ktx": "https://files.ballistica.net/cache/ba1/30/f4/623af2fc01f51673881a88fe73f9", + "build/assets/ba_data/textures/fontSmall1.pvr": "https://files.ballistica.net/cache/ba1/3e/58/773e2538202e2b101f3668f26225", + "build/assets/ba_data/textures/fontSmall1_preview.png": "https://files.ballistica.net/cache/ba1/c0/31/d68f7fdc70ec4ec6ffdf74176843", + "build/assets/ba_data/textures/fontSmall2.dds": "https://files.ballistica.net/cache/ba1/47/b1/3b972c010072bf33ed754c725213", + "build/assets/ba_data/textures/fontSmall2.ktx": "https://files.ballistica.net/cache/ba1/f7/f3/8b2cec447c5c835d4beaeccd78bf", + "build/assets/ba_data/textures/fontSmall2.pvr": "https://files.ballistica.net/cache/ba1/3e/68/ed3e8287e4f1a29bd1c89398acd2", + "build/assets/ba_data/textures/fontSmall2_preview.png": "https://files.ballistica.net/cache/ba1/52/93/e76c1252fcff227cedf917870223", + "build/assets/ba_data/textures/fontSmall3.dds": "https://files.ballistica.net/cache/ba1/1e/cb/c08b7bea005b8aba058b58990da1", + "build/assets/ba_data/textures/fontSmall3.ktx": "https://files.ballistica.net/cache/ba1/b8/10/24fed1cc9e7ccd0f9fdfebf3696b", + "build/assets/ba_data/textures/fontSmall3.pvr": "https://files.ballistica.net/cache/ba1/29/87/db0c9f9643d878123c3ddede4290", + "build/assets/ba_data/textures/fontSmall3_preview.png": "https://files.ballistica.net/cache/ba1/ed/f6/318b942f27431b81c92674b6000d", + "build/assets/ba_data/textures/fontSmall4.dds": "https://files.ballistica.net/cache/ba1/bb/5b/d61959082e31545ef14630a65363", + "build/assets/ba_data/textures/fontSmall4.ktx": "https://files.ballistica.net/cache/ba1/7d/d5/05564b2d849af26a9b772953633e", + "build/assets/ba_data/textures/fontSmall4.pvr": "https://files.ballistica.net/cache/ba1/98/65/89808ab17a8aba98e527d4de0ac8", + "build/assets/ba_data/textures/fontSmall4_preview.png": "https://files.ballistica.net/cache/ba1/6a/b3/38c52d4c39f86ef394f7f350eaaa", + "build/assets/ba_data/textures/fontSmall5.dds": "https://files.ballistica.net/cache/ba1/a0/73/d622bbee755622911b5e0df7cca7", + "build/assets/ba_data/textures/fontSmall5.ktx": "https://files.ballistica.net/cache/ba1/98/a2/6d6f8c68558b9de401ef365be62f", + "build/assets/ba_data/textures/fontSmall5.pvr": "https://files.ballistica.net/cache/ba1/ee/fd/934eaa4b8c091b80a9bd106a2202", + "build/assets/ba_data/textures/fontSmall5_preview.png": "https://files.ballistica.net/cache/ba1/27/04/a50bf3b4e14b0fa0f8dfa02ffc7b", + "build/assets/ba_data/textures/fontSmall6.dds": "https://files.ballistica.net/cache/ba1/bd/3a/205edce8e62f45fcaa37b22ad727", + "build/assets/ba_data/textures/fontSmall6.ktx": "https://files.ballistica.net/cache/ba1/be/e0/24c19e3cb9b6aa7ff72fee7a2bd6", + "build/assets/ba_data/textures/fontSmall6.pvr": "https://files.ballistica.net/cache/ba1/eb/97/c6a035e261e31ebcd9603e73848e", + "build/assets/ba_data/textures/fontSmall6_preview.png": "https://files.ballistica.net/cache/ba1/21/26/f54a1e69f57c2fad7e2f3394b7ec", + "build/assets/ba_data/textures/fontSmall7.dds": "https://files.ballistica.net/cache/ba1/8d/3f/0308f68d374127176526547480db", + "build/assets/ba_data/textures/fontSmall7.ktx": "https://files.ballistica.net/cache/ba1/8e/6c/22e051382e9bad2cce2202517b2d", + "build/assets/ba_data/textures/fontSmall7.pvr": "https://files.ballistica.net/cache/ba1/c7/b2/65d7b2b3a8df4e44101695be3590", + "build/assets/ba_data/textures/fontSmall7_preview.png": "https://files.ballistica.net/cache/ba1/08/97/f529d326307ba0a343d5c1206fb4", + "build/assets/ba_data/textures/footballStadium.dds": "https://files.ballistica.net/cache/ba1/4a/48/376fbe1149141ec10071c92b2621", + "build/assets/ba_data/textures/footballStadium.ktx": "https://files.ballistica.net/cache/ba1/16/d3/73a987fb336d672833cb83e66916", + "build/assets/ba_data/textures/footballStadium.pvr": "https://files.ballistica.net/cache/ba1/54/72/4faa7592894dd21224589c5a5e83", + "build/assets/ba_data/textures/footballStadiumPreview.dds": "https://files.ballistica.net/cache/ba1/9e/3f/318d553d0ccf6c64fe63a10b1924", + "build/assets/ba_data/textures/footballStadiumPreview.ktx": "https://files.ballistica.net/cache/ba1/65/62/81797e751f4cb17b4585f221ebc3", + "build/assets/ba_data/textures/footballStadiumPreview.pvr": "https://files.ballistica.net/cache/ba1/da/7b/3a17dfc9e2f0feb718b3df20700a", + "build/assets/ba_data/textures/footballStadiumPreview_preview.png": "https://files.ballistica.net/cache/ba1/8f/2f/fad28ff515afd94d9c3cf57fa268", + "build/assets/ba_data/textures/footballStadium_preview.png": "https://files.ballistica.net/cache/ba1/eb/ef/6dfc816bfb99eb94831d5c9dd361", + "build/assets/ba_data/textures/frameInset.dds": "https://files.ballistica.net/cache/ba1/ec/b5/0aa39f8bba05ec5238aee87158a6", + "build/assets/ba_data/textures/frameInset.ktx": "https://files.ballistica.net/cache/ba1/b8/a6/be5a184900792d997ace29d58e23", + "build/assets/ba_data/textures/frameInset.pvr": "https://files.ballistica.net/cache/ba1/74/dc/78183b36e0d90902507af4c2eb1b", + "build/assets/ba_data/textures/frameInset_preview.png": "https://files.ballistica.net/cache/ba1/1e/41/b2a275595beda042a6c38922431c", + "build/assets/ba_data/textures/frostyColor.dds": "https://files.ballistica.net/cache/ba1/fa/8d/50666401f15e092e527932b5a737", + "build/assets/ba_data/textures/frostyColor.ktx": "https://files.ballistica.net/cache/ba1/9e/a0/61b2262e6cc1e730220908d981e1", + "build/assets/ba_data/textures/frostyColor.pvr": "https://files.ballistica.net/cache/ba1/0e/17/f40c37b80cbedbb5727ecd8f4ecb", + "build/assets/ba_data/textures/frostyColorMask.dds": "https://files.ballistica.net/cache/ba1/c1/7d/0e88ff4d542ba156fd3a19ddf52b", + "build/assets/ba_data/textures/frostyColorMask.ktx": "https://files.ballistica.net/cache/ba1/cc/d0/8de0117232aaf7b2ebab61bab5e7", + "build/assets/ba_data/textures/frostyColorMask.pvr": "https://files.ballistica.net/cache/ba1/04/2f/95a4baa57ded745d6a9defe9afc5", + "build/assets/ba_data/textures/frostyColorMask_preview.png": "https://files.ballistica.net/cache/ba1/43/3b/0d2a27bef8c6663a3bb9a240c6ef", + "build/assets/ba_data/textures/frostyColor_preview.png": "https://files.ballistica.net/cache/ba1/34/33/41c244aaf815f82f4ddfa0d14b39", + "build/assets/ba_data/textures/frostyIcon.dds": "https://files.ballistica.net/cache/ba1/02/be/6842fbd688b64067e8f0ee9288ea", + "build/assets/ba_data/textures/frostyIcon.ktx": "https://files.ballistica.net/cache/ba1/7b/c8/02eb3e4bd3cfe4808ba7def7c8b4", + "build/assets/ba_data/textures/frostyIcon.pvr": "https://files.ballistica.net/cache/ba1/0b/d2/3aec4aabf7a95a27f2e1731af4a7", + "build/assets/ba_data/textures/frostyIconColorMask.dds": "https://files.ballistica.net/cache/ba1/7d/b7/c5bf5b6b3e223b4bc80f5e48bf69", + "build/assets/ba_data/textures/frostyIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/7e/1e/a2ee6ed58e93109d759e63c5c775", + "build/assets/ba_data/textures/frostyIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/cc/77/e609f82627eab4e701b3c93ab770", + "build/assets/ba_data/textures/frostyIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/93/9f/c79222de2f9a932abbeac0994a9f", + "build/assets/ba_data/textures/frostyIcon_preview.png": "https://files.ballistica.net/cache/ba1/82/34/0dfe2ef349b824c3d2424e0106aa", + "build/assets/ba_data/textures/fuse.dds": "https://files.ballistica.net/cache/ba1/c3/60/c27374beb55fbb485cea9b8b0848", + "build/assets/ba_data/textures/fuse.ktx": "https://files.ballistica.net/cache/ba1/04/65/b8a7c2c26b78b5d1fccf19a48652", + "build/assets/ba_data/textures/fuse.pvr": "https://files.ballistica.net/cache/ba1/0c/39/004ad2acafb4cf77565c4c03341d", + "build/assets/ba_data/textures/fuse_preview.png": "https://files.ballistica.net/cache/ba1/5d/5f/06722e81651886e832bdbf4955ef", + "build/assets/ba_data/textures/gameCenterIcon.dds": "https://files.ballistica.net/cache/ba1/49/78/13927f60baa1261f0dbd08306083", + "build/assets/ba_data/textures/gameCenterIcon.ktx": "https://files.ballistica.net/cache/ba1/0a/9d/6b988f62c7bdfceb0c96ad48071a", + "build/assets/ba_data/textures/gameCenterIcon.pvr": "https://files.ballistica.net/cache/ba1/a0/ba/c7cc47acd01ff020408bc417ae89", + "build/assets/ba_data/textures/gameCenterIcon_preview.png": "https://files.ballistica.net/cache/ba1/47/5e/5ec037be23fe266facb411a5a91f", + "build/assets/ba_data/textures/gameCircleIcon.dds": "https://files.ballistica.net/cache/ba1/52/1b/6069ab1a33c8000df0088d438368", + "build/assets/ba_data/textures/gameCircleIcon.ktx": "https://files.ballistica.net/cache/ba1/d7/f3/c3b83e3b488122668cc4bdf17ace", + "build/assets/ba_data/textures/gameCircleIcon.pvr": "https://files.ballistica.net/cache/ba1/9e/0a/5aa043c52a7a3929ef3d8862854a", + "build/assets/ba_data/textures/gameCircleIcon_preview.png": "https://files.ballistica.net/cache/ba1/bd/c3/5cc0fc6ef066c8a09f5221bbd383", + "build/assets/ba_data/textures/githubLogo.dds": "https://files.ballistica.net/cache/ba1/9c/99/abae42606e1c5080a5d3f02ed07f", + "build/assets/ba_data/textures/githubLogo.ktx": "https://files.ballistica.net/cache/ba1/e8/21/3564e51c363a12329b62af5bac1f", + "build/assets/ba_data/textures/githubLogo.pvr": "https://files.ballistica.net/cache/ba1/5a/51/76830629ac69415934c573894895", + "build/assets/ba_data/textures/githubLogo_preview.png": "https://files.ballistica.net/cache/ba1/15/e6/85e366ef08672768c7b1788cff74", + "build/assets/ba_data/textures/gladiatorColor.dds": "https://files.ballistica.net/cache/ba1/21/57/0ae6bab600d41138e772c7e2ff53", + "build/assets/ba_data/textures/gladiatorColor.ktx": "https://files.ballistica.net/cache/ba1/8b/52/6567e09d2e4eb970ef8775455ccf", + "build/assets/ba_data/textures/gladiatorColor.pvr": "https://files.ballistica.net/cache/ba1/c5/d1/e025362f9bf82e957f3868bdab82", + "build/assets/ba_data/textures/gladiatorColorMask.dds": "https://files.ballistica.net/cache/ba1/21/af/a420693245f7d4991a9828a1b85e", + "build/assets/ba_data/textures/gladiatorColorMask.ktx": "https://files.ballistica.net/cache/ba1/ed/e9/adc5ae416de77dc3ac0e68224035", + "build/assets/ba_data/textures/gladiatorColorMask.pvr": "https://files.ballistica.net/cache/ba1/07/d8/2f82288b704135ec7db776102c56", + "build/assets/ba_data/textures/gladiatorColorMask_preview.png": "https://files.ballistica.net/cache/ba1/0f/02/cc9d1234cd196b5af61b2efa6ab5", + "build/assets/ba_data/textures/gladiatorColor_preview.png": "https://files.ballistica.net/cache/ba1/5d/f6/cc1c859ac80aa4975bb372abfa08", + "build/assets/ba_data/textures/gladiatorIcon.dds": "https://files.ballistica.net/cache/ba1/ac/3f/f908d82fcb20912c11922b1cf491", + "build/assets/ba_data/textures/gladiatorIcon.ktx": "https://files.ballistica.net/cache/ba1/b1/8b/618b681ecc9628d6e5fb0ba14365", + "build/assets/ba_data/textures/gladiatorIcon.pvr": "https://files.ballistica.net/cache/ba1/a4/7e/5e4b6ce4644b01222b20ba48e596", + "build/assets/ba_data/textures/gladiatorIconColorMask.dds": "https://files.ballistica.net/cache/ba1/c2/c4/61f126bf4ea37710337ac4767d8c", + "build/assets/ba_data/textures/gladiatorIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/a6/29/ab7334927ed2f64ed6a870a217f2", + "build/assets/ba_data/textures/gladiatorIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/08/eb/d0652292b87c5991efc9c33f65ed", + "build/assets/ba_data/textures/gladiatorIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/39/58/246f55bc2ceaa0966ad40e28be0a", + "build/assets/ba_data/textures/gladiatorIcon_preview.png": "https://files.ballistica.net/cache/ba1/12/ea/4f0de9fafdf694324d6f3c3a89b5", + "build/assets/ba_data/textures/glow.dds": "https://files.ballistica.net/cache/ba1/59/e3/8a251643356272a14523048304c9", + "build/assets/ba_data/textures/glow.ktx": "https://files.ballistica.net/cache/ba1/9b/ec/f0de6dae4339f32aef39fdd54cfb", + "build/assets/ba_data/textures/glow.pvr": "https://files.ballistica.net/cache/ba1/a7/6f/b72ccd8a97008dd629259a2c8562", + "build/assets/ba_data/textures/glow_preview.png": "https://files.ballistica.net/cache/ba1/76/37/3095e7f87b547b252afb7a8ebb46", + "build/assets/ba_data/textures/googlePlayAchievementsIcon.dds": "https://files.ballistica.net/cache/ba1/26/c3/bcd104c1b80ab126694ad3b93d2c", + "build/assets/ba_data/textures/googlePlayAchievementsIcon.ktx": "https://files.ballistica.net/cache/ba1/4d/b4/921bd7e417bdc919d5dccbe5c917", + "build/assets/ba_data/textures/googlePlayAchievementsIcon.pvr": "https://files.ballistica.net/cache/ba1/42/fc/8a848d2d5830995ef17d993ebb60", + "build/assets/ba_data/textures/googlePlayAchievementsIcon_preview.png": "https://files.ballistica.net/cache/ba1/ac/c2/4d4e662be6dbd5f7d6d661e3bee8", + "build/assets/ba_data/textures/googlePlayGamesIcon.dds": "https://files.ballistica.net/cache/ba1/7e/e5/9c92ae4b959f96db86b68fa37570", + "build/assets/ba_data/textures/googlePlayGamesIcon.ktx": "https://files.ballistica.net/cache/ba1/aa/7d/3ba76b5455d2d31c73ba3b31b36a", + "build/assets/ba_data/textures/googlePlayGamesIcon.pvr": "https://files.ballistica.net/cache/ba1/48/8a/c58e633a85e514256eb89e032949", + "build/assets/ba_data/textures/googlePlayGamesIcon_preview.png": "https://files.ballistica.net/cache/ba1/14/2a/51177bd1bc49c1dbe138a9cacd78", + "build/assets/ba_data/textures/googlePlayLeaderboardsIcon.dds": "https://files.ballistica.net/cache/ba1/0d/a9/b865f5924b71c1da470f66c080f6", + "build/assets/ba_data/textures/googlePlayLeaderboardsIcon.ktx": "https://files.ballistica.net/cache/ba1/a5/19/e6da8cf114f93cb7b34bc3e1446f", + "build/assets/ba_data/textures/googlePlayLeaderboardsIcon.pvr": "https://files.ballistica.net/cache/ba1/48/74/99a52b3a96f379fdee78a384e182", + "build/assets/ba_data/textures/googlePlayLeaderboardsIcon_preview.png": "https://files.ballistica.net/cache/ba1/a8/30/0d592541f22551563a83d54fffa2", + "build/assets/ba_data/textures/googlePlusIcon.dds": "https://files.ballistica.net/cache/ba1/39/24/c63c160eeb71ca9917c9327e9aba", + "build/assets/ba_data/textures/googlePlusIcon.ktx": "https://files.ballistica.net/cache/ba1/e2/05/583f90dafef9c9aed0f16d72b8dd", + "build/assets/ba_data/textures/googlePlusIcon.pvr": "https://files.ballistica.net/cache/ba1/b1/30/e3ec98607f1101692319a0149acb", + "build/assets/ba_data/textures/googlePlusIcon_preview.png": "https://files.ballistica.net/cache/ba1/3b/0d/482ac1d274381f13c1a58f5740b4", + "build/assets/ba_data/textures/googlePlusSignInButton.dds": "https://files.ballistica.net/cache/ba1/14/1a/926e322288987e1889fe49f5a61c", + "build/assets/ba_data/textures/googlePlusSignInButton.ktx": "https://files.ballistica.net/cache/ba1/2c/fa/1246c2cc0e7852761b9e87e500b8", + "build/assets/ba_data/textures/googlePlusSignInButton.pvr": "https://files.ballistica.net/cache/ba1/ef/25/72679e7c1ccb2d18946f02596d78", + "build/assets/ba_data/textures/googlePlusSignInButton_preview.png": "https://files.ballistica.net/cache/ba1/09/65/552c5cff3545d498851da5d9c564", + "build/assets/ba_data/textures/graphicsIcon.dds": "https://files.ballistica.net/cache/ba1/9a/4f/b8b87672606464014f24ec380cfa", + "build/assets/ba_data/textures/graphicsIcon.ktx": "https://files.ballistica.net/cache/ba1/e3/6d/2a948b15410af68e5a26a4b9ae3f", + "build/assets/ba_data/textures/graphicsIcon.pvr": "https://files.ballistica.net/cache/ba1/d2/ec/8c15c08c9f42b58f11c18bf8cdc1", + "build/assets/ba_data/textures/graphicsIcon_preview.png": "https://files.ballistica.net/cache/ba1/45/7b/fc3d296e3b70419e2230dce4a8af", + "build/assets/ba_data/textures/heart.dds": "https://files.ballistica.net/cache/ba1/9f/cc/7bee81fbb35812fd9e8b62c7ee2e", + "build/assets/ba_data/textures/heart.ktx": "https://files.ballistica.net/cache/ba1/ac/58/a03d1588a8931fbcccdfbefb8a31", + "build/assets/ba_data/textures/heart.pvr": "https://files.ballistica.net/cache/ba1/a5/ee/293da2bf70e256e7b362a2e7542f", + "build/assets/ba_data/textures/heart_preview.png": "https://files.ballistica.net/cache/ba1/10/64/eec838eda431f10b999f692b82ec", + "build/assets/ba_data/textures/hockeyStadium.dds": "https://files.ballistica.net/cache/ba1/01/9e/fc2a1f1650a3aeabcc4aabae6184", + "build/assets/ba_data/textures/hockeyStadium.ktx": "https://files.ballistica.net/cache/ba1/b0/10/6f56051f4b914f82a60b4011d8f4", + "build/assets/ba_data/textures/hockeyStadium.pvr": "https://files.ballistica.net/cache/ba1/6b/73/a29776f424a932cb66e7a17fe966", + "build/assets/ba_data/textures/hockeyStadiumPreview.dds": "https://files.ballistica.net/cache/ba1/b8/b9/a12c04ed62f227ee3dc92d6474cc", + "build/assets/ba_data/textures/hockeyStadiumPreview.ktx": "https://files.ballistica.net/cache/ba1/f8/f2/825951df04208e83fbbbb528312a", + "build/assets/ba_data/textures/hockeyStadiumPreview.pvr": "https://files.ballistica.net/cache/ba1/c0/54/4828fc8a82307e5c72612a1c51fd", + "build/assets/ba_data/textures/hockeyStadiumPreview_preview.png": "https://files.ballistica.net/cache/ba1/96/16/fde78fc0763a3049f1d14906d64b", + "build/assets/ba_data/textures/hockeyStadium_preview.png": "https://files.ballistica.net/cache/ba1/69/fc/5723570e43a7f1b64e821d396a88", + "build/assets/ba_data/textures/iconOnslaught.dds": "https://files.ballistica.net/cache/ba1/5a/d7/857f7b6f4d717154cf04307b99b8", + "build/assets/ba_data/textures/iconOnslaught.ktx": "https://files.ballistica.net/cache/ba1/3a/da/c927326082eefaa82632f8a04130", + "build/assets/ba_data/textures/iconOnslaught.pvr": "https://files.ballistica.net/cache/ba1/1a/02/dec0257f80ea7eb80e253a5718c2", + "build/assets/ba_data/textures/iconOnslaught_preview.png": "https://files.ballistica.net/cache/ba1/72/a4/6c789970ee77788d4c393ba944d9", + "build/assets/ba_data/textures/iconRunaround.dds": "https://files.ballistica.net/cache/ba1/18/c8/833894eb7703fc0c5a42c9cbbbf9", + "build/assets/ba_data/textures/iconRunaround.ktx": "https://files.ballistica.net/cache/ba1/48/a1/b85ea44e736a6ef44c9515b31ed1", + "build/assets/ba_data/textures/iconRunaround.pvr": "https://files.ballistica.net/cache/ba1/fc/8f/bfe77ebac2b326fad8cc0c1e5826", + "build/assets/ba_data/textures/iconRunaround_preview.png": "https://files.ballistica.net/cache/ba1/58/7a/ab959948169a9783df8fe0ee81af", + "build/assets/ba_data/textures/iircadeLogo.dds": "https://files.ballistica.net/cache/ba1/6c/f6/a78b90cf96a4d07bab7c41a9b756", + "build/assets/ba_data/textures/iircadeLogo.ktx": "https://files.ballistica.net/cache/ba1/4c/94/2be886aa2307baf45cda3d5db029", + "build/assets/ba_data/textures/iircadeLogo.pvr": "https://files.ballistica.net/cache/ba1/a6/92/f8c7ac56a7592edcbf0fe5da1960", + "build/assets/ba_data/textures/iircadeLogo_preview.png": "https://files.ballistica.net/cache/ba1/25/2e/fc851dc741f5a65038b7bad48394", + "build/assets/ba_data/textures/impactBombColor.dds": "https://files.ballistica.net/cache/ba1/e3/db/c77036eeac25bc6d604f9482f850", + "build/assets/ba_data/textures/impactBombColor.ktx": "https://files.ballistica.net/cache/ba1/3d/cc/76793bfa5a2096ede4a824d958d5", + "build/assets/ba_data/textures/impactBombColor.pvr": "https://files.ballistica.net/cache/ba1/a0/8a/a06751a26e906b372e77de3e34a4", + "build/assets/ba_data/textures/impactBombColorLit.dds": "https://files.ballistica.net/cache/ba1/97/cf/b851e73b5c31ad2ee7e22216d836", + "build/assets/ba_data/textures/impactBombColorLit.ktx": "https://files.ballistica.net/cache/ba1/41/11/30b46d8dbcefb0ec3fc32d0db68a", + "build/assets/ba_data/textures/impactBombColorLit.pvr": "https://files.ballistica.net/cache/ba1/10/17/6bf7725b1d9e726be0337b64d5dd", + "build/assets/ba_data/textures/impactBombColorLit_preview.png": "https://files.ballistica.net/cache/ba1/c1/6f/ffbdd206b2998a3e131ffecaf156", + "build/assets/ba_data/textures/impactBombColor_preview.png": "https://files.ballistica.net/cache/ba1/99/4d/3f8f1d4f120b579d6608a2599a55", + "build/assets/ba_data/textures/inventoryIcon.dds": "https://files.ballistica.net/cache/ba1/e5/dd/245c494857d4ef967711d9e473ab", + "build/assets/ba_data/textures/inventoryIcon.ktx": "https://files.ballistica.net/cache/ba1/ce/e8/8140f93f75071cab6759fee942e4", + "build/assets/ba_data/textures/inventoryIcon.pvr": "https://files.ballistica.net/cache/ba1/12/0d/5a8288f8c1d4e49a82b66257c493", + "build/assets/ba_data/textures/inventoryIcon_preview.png": "https://files.ballistica.net/cache/ba1/18/6a/faccadf393f3cb56ab09c2c52b92", + "build/assets/ba_data/textures/jackColor.dds": "https://files.ballistica.net/cache/ba1/bc/ac/95ef23d8b19de20cac837e5950c0", + "build/assets/ba_data/textures/jackColor.ktx": "https://files.ballistica.net/cache/ba1/de/fc/96d685fe51e3c1152c60e40ebdb7", + "build/assets/ba_data/textures/jackColor.pvr": "https://files.ballistica.net/cache/ba1/d8/70/86eac5aeadec4425da390a80bfb4", + "build/assets/ba_data/textures/jackColorMask.dds": "https://files.ballistica.net/cache/ba1/fc/99/e9fb5c81905b7a7be8ccf8c31fc4", + "build/assets/ba_data/textures/jackColorMask.ktx": "https://files.ballistica.net/cache/ba1/19/af/d6845a2f9b3bc2c23b12a8f4f966", + "build/assets/ba_data/textures/jackColorMask.pvr": "https://files.ballistica.net/cache/ba1/48/c3/b78d7bc23806bb0b86927d2ced93", + "build/assets/ba_data/textures/jackColorMask_preview.png": "https://files.ballistica.net/cache/ba1/df/48/51187a519c275944d5086ebf6707", + "build/assets/ba_data/textures/jackColor_preview.png": "https://files.ballistica.net/cache/ba1/7d/c8/fbf892e881edaf0a0c9934de9ede", + "build/assets/ba_data/textures/jackIcon.dds": "https://files.ballistica.net/cache/ba1/e6/e5/bbce282559330c7636876f851826", + "build/assets/ba_data/textures/jackIcon.ktx": "https://files.ballistica.net/cache/ba1/e2/50/2441627bcc27f9da285c84d11327", + "build/assets/ba_data/textures/jackIcon.pvr": "https://files.ballistica.net/cache/ba1/66/49/2f1ccd85aa6a53c71fc59c5c0b82", + "build/assets/ba_data/textures/jackIconColorMask.dds": "https://files.ballistica.net/cache/ba1/eb/ac/ba760a778ad2465b18c079be11e7", + "build/assets/ba_data/textures/jackIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/5f/77/65356e8ad76886fbc228753f3efc", + "build/assets/ba_data/textures/jackIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/3a/53/e2e605ec380a6fe3dc3d363f5ec4", + "build/assets/ba_data/textures/jackIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/ac/67/666e1b7b4d29f308d31d17053761", + "build/assets/ba_data/textures/jackIcon_preview.png": "https://files.ballistica.net/cache/ba1/8c/f3/10f293573cb1443154ec47b08121", + "build/assets/ba_data/textures/jumpsuitColor.dds": "https://files.ballistica.net/cache/ba1/78/8b/d52c4c915e8253831b99dc34f181", + "build/assets/ba_data/textures/jumpsuitColor.ktx": "https://files.ballistica.net/cache/ba1/6a/4f/3d7d3ed5c94a77a17698db66ba96", + "build/assets/ba_data/textures/jumpsuitColor.pvr": "https://files.ballistica.net/cache/ba1/64/ec/994c04df40685e1a782c2e2d6266", + "build/assets/ba_data/textures/jumpsuitColorMask.dds": "https://files.ballistica.net/cache/ba1/06/ac/d9e409ec3d380ad9a97da0294e0a", + "build/assets/ba_data/textures/jumpsuitColorMask.ktx": "https://files.ballistica.net/cache/ba1/21/8c/e82dc32b8c285c0883f2ab32af3e", + "build/assets/ba_data/textures/jumpsuitColorMask.pvr": "https://files.ballistica.net/cache/ba1/fe/ea/40001c511514875c6ff67b161925", + "build/assets/ba_data/textures/jumpsuitColorMask_preview.png": "https://files.ballistica.net/cache/ba1/1a/bb/fdc5550ad1b8f8d5ac692d0b75d6", + "build/assets/ba_data/textures/jumpsuitColor_preview.png": "https://files.ballistica.net/cache/ba1/16/8e/1782f66f94aef9c8a1dee9c2fbe3", + "build/assets/ba_data/textures/jumpsuitIcon.dds": "https://files.ballistica.net/cache/ba1/86/a2/b8234baf8b272286ef4dac43773a", + "build/assets/ba_data/textures/jumpsuitIcon.ktx": "https://files.ballistica.net/cache/ba1/90/b4/8ff7352a4c78275d67fe72537b3f", + "build/assets/ba_data/textures/jumpsuitIcon.pvr": "https://files.ballistica.net/cache/ba1/64/2f/2703398942dddb5bea678d994a34", + "build/assets/ba_data/textures/jumpsuitIconColorMask.dds": "https://files.ballistica.net/cache/ba1/e8/30/1d8b215c4afcbef1a18aa5d203ca", + "build/assets/ba_data/textures/jumpsuitIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/d8/c3/973bbac13f85ebf6ac4dca1c6200", + "build/assets/ba_data/textures/jumpsuitIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/2a/3a/bdd7b5cb2acca72e2af79eed023a", + "build/assets/ba_data/textures/jumpsuitIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/50/07/ca5f84483837c43656ce63c0e644", + "build/assets/ba_data/textures/jumpsuitIcon_preview.png": "https://files.ballistica.net/cache/ba1/9e/6b/eff786f8f9b450cefe56961e608b", + "build/assets/ba_data/textures/kronk.dds": "https://files.ballistica.net/cache/ba1/8d/3f/096389a4d3dec942715032875ef6", + "build/assets/ba_data/textures/kronk.ktx": "https://files.ballistica.net/cache/ba1/23/91/3a70868121cdeae3bbb87a5e722d", + "build/assets/ba_data/textures/kronk.pvr": "https://files.ballistica.net/cache/ba1/c3/5f/3a37f9a5b60f027bf9f9b23e21f4", + "build/assets/ba_data/textures/kronkColorMask.dds": "https://files.ballistica.net/cache/ba1/a5/19/4fc29c1990ff31c9fbe0c5e7da11", + "build/assets/ba_data/textures/kronkColorMask.ktx": "https://files.ballistica.net/cache/ba1/bf/77/dff40d74a4a704ea93c2ba7baccb", + "build/assets/ba_data/textures/kronkColorMask.pvr": "https://files.ballistica.net/cache/ba1/3f/4e/d78a3b4154361ec283f9f7f03754", + "build/assets/ba_data/textures/kronkColorMask_preview.png": "https://files.ballistica.net/cache/ba1/ce/9a/f7e6f99b9f23cc420923630767e2", + "build/assets/ba_data/textures/kronkIcon.dds": "https://files.ballistica.net/cache/ba1/67/aa/9bc6db670f259ab07665df2228a4", + "build/assets/ba_data/textures/kronkIcon.ktx": "https://files.ballistica.net/cache/ba1/a9/d0/c883b2e9cfe06de9fb04956277bc", + "build/assets/ba_data/textures/kronkIcon.pvr": "https://files.ballistica.net/cache/ba1/55/0f/8675f07dcaecb7bfbd464686ddc3", + "build/assets/ba_data/textures/kronkIconColorMask.dds": "https://files.ballistica.net/cache/ba1/eb/31/37f22f78e7a467e9b8306325526f", + "build/assets/ba_data/textures/kronkIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/48/6d/ee0ce3cd0a601bea6d34ff0d0b05", + "build/assets/ba_data/textures/kronkIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/9d/44/920eb9fc365c7d166a4bd316d8d1", + "build/assets/ba_data/textures/kronkIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/fd/9d/bf1af62c2297f82047332eb705bd", + "build/assets/ba_data/textures/kronkIcon_preview.png": "https://files.ballistica.net/cache/ba1/be/91/167c833234dd787779b111eaca1d", + "build/assets/ba_data/textures/kronk_preview.png": "https://files.ballistica.net/cache/ba1/75/73/4465163ee2e81cfb579da3faca41", + "build/assets/ba_data/textures/lakeFrigid.dds": "https://files.ballistica.net/cache/ba1/14/52/9ecfbcfe8bb6b0ef87d8750fecd7", + "build/assets/ba_data/textures/lakeFrigid.ktx": "https://files.ballistica.net/cache/ba1/81/5f/16f82996a090018dacd812220345", + "build/assets/ba_data/textures/lakeFrigid.pvr": "https://files.ballistica.net/cache/ba1/7e/c4/d8e74f041c4c859896e2219cd9d6", + "build/assets/ba_data/textures/lakeFrigidPreview.dds": "https://files.ballistica.net/cache/ba1/c4/23/fef4b75b3cf2d96eff238a1d0ce0", + "build/assets/ba_data/textures/lakeFrigidPreview.ktx": "https://files.ballistica.net/cache/ba1/36/a8/9eb2147aed38e45e4842b9d712ea", + "build/assets/ba_data/textures/lakeFrigidPreview.pvr": "https://files.ballistica.net/cache/ba1/5c/88/1031ca9daa7b455aa601411dddbb", + "build/assets/ba_data/textures/lakeFrigidPreview_preview.png": "https://files.ballistica.net/cache/ba1/5a/a7/50a175b959bc60fc7616fc0ba8f0", + "build/assets/ba_data/textures/lakeFrigidReflections.dds": "https://files.ballistica.net/cache/ba1/ba/ae/18df2885af864b9bcceb22da18f7", + "build/assets/ba_data/textures/lakeFrigidReflections.ktx": "https://files.ballistica.net/cache/ba1/eb/f3/171fa2a9f21782afd5c85fd45b81", + "build/assets/ba_data/textures/lakeFrigidReflections.pvr": "https://files.ballistica.net/cache/ba1/c6/d8/8dbb90304231dd5c110c5e7e9da0", + "build/assets/ba_data/textures/lakeFrigidReflections_preview.png": "https://files.ballistica.net/cache/ba1/f8/0a/cb8db69366fdce4d712a65a9413c", + "build/assets/ba_data/textures/lakeFrigid_preview.png": "https://files.ballistica.net/cache/ba1/80/b8/1ded414e45928f442937ced5f22f", + "build/assets/ba_data/textures/landMine.dds": "https://files.ballistica.net/cache/ba1/15/50/8e4dfd2c4f4e72f01abca9c0dced", + "build/assets/ba_data/textures/landMine.ktx": "https://files.ballistica.net/cache/ba1/d0/05/402af0b8dd4d3a4c96ab6e02b9c2", + "build/assets/ba_data/textures/landMine.pvr": "https://files.ballistica.net/cache/ba1/c3/89/6f31b73d4f5f7d344f7ce10a9de2", + "build/assets/ba_data/textures/landMineLit.dds": "https://files.ballistica.net/cache/ba1/38/97/f4ca6317af169ab54b9e774d8584", + "build/assets/ba_data/textures/landMineLit.ktx": "https://files.ballistica.net/cache/ba1/e3/4d/e2a4df893f400bf5a8214faa6d7e", + "build/assets/ba_data/textures/landMineLit.pvr": "https://files.ballistica.net/cache/ba1/e5/34/81629aafba00215b40452cf6b8f0", + "build/assets/ba_data/textures/landMineLit_preview.png": "https://files.ballistica.net/cache/ba1/06/33/fe3d40dfdb2bf4ae4cb8b89c8de7", + "build/assets/ba_data/textures/landMine_preview.png": "https://files.ballistica.net/cache/ba1/64/3e/f0b897524f838de9e7b0fae84c92", + "build/assets/ba_data/textures/leaderboardsIcon.dds": "https://files.ballistica.net/cache/ba1/c9/89/fa5f58da9fbb6546a702ca1fe669", + "build/assets/ba_data/textures/leaderboardsIcon.ktx": "https://files.ballistica.net/cache/ba1/3a/31/a84c0ef9d6005bab8f9f6d2b495e", + "build/assets/ba_data/textures/leaderboardsIcon.pvr": "https://files.ballistica.net/cache/ba1/7b/2b/9e265487f5cb00abc6f8cc59f08c", + "build/assets/ba_data/textures/leaderboardsIcon_preview.png": "https://files.ballistica.net/cache/ba1/4d/bd/93ce86b14f4e301e8da1cee56277", + "build/assets/ba_data/textures/leftButton.dds": "https://files.ballistica.net/cache/ba1/81/74/1a46a914a34108656d91761562d4", + "build/assets/ba_data/textures/leftButton.ktx": "https://files.ballistica.net/cache/ba1/84/76/ce904aef7da1a5098fee5877eefa", + "build/assets/ba_data/textures/leftButton.pvr": "https://files.ballistica.net/cache/ba1/2d/17/82029e912a3c27a01825f72e057f", + "build/assets/ba_data/textures/leftButton_preview.png": "https://files.ballistica.net/cache/ba1/25/8f/0461920c7de5d1c067bb6587279a", + "build/assets/ba_data/textures/levelIcon.dds": "https://files.ballistica.net/cache/ba1/4b/3e/31a522ce121b279a88bf1b9c3f56", + "build/assets/ba_data/textures/levelIcon.ktx": "https://files.ballistica.net/cache/ba1/6d/db/f705533eef108febc5743a310406", + "build/assets/ba_data/textures/levelIcon.pvr": "https://files.ballistica.net/cache/ba1/e1/bf/2f91ee82156de71593d5af18b626", + "build/assets/ba_data/textures/levelIcon_preview.png": "https://files.ballistica.net/cache/ba1/31/a4/f1306c2ddc798ab643e53f9a69d7", + "build/assets/ba_data/textures/light.dds": "https://files.ballistica.net/cache/ba1/0f/4e/c1c633087d93f575fcb179ee6368", + "build/assets/ba_data/textures/light.ktx": "https://files.ballistica.net/cache/ba1/66/00/dc51d2fe99293eeab46ac546f0a2", + "build/assets/ba_data/textures/light.pvr": "https://files.ballistica.net/cache/ba1/26/9c/6762bdeb370618524972a69087ff", + "build/assets/ba_data/textures/lightSharp.dds": "https://files.ballistica.net/cache/ba1/c1/99/81da8723084c7e8af834c14900bf", + "build/assets/ba_data/textures/lightSharp.ktx": "https://files.ballistica.net/cache/ba1/a5/8c/42ebcecbb05da99869c8d6e606f5", + "build/assets/ba_data/textures/lightSharp.pvr": "https://files.ballistica.net/cache/ba1/f9/8d/336bee97498a1f2392425974c497", + "build/assets/ba_data/textures/lightSharp_preview.png": "https://files.ballistica.net/cache/ba1/f9/73/62d7733e482d757010930e8bf3ae", + "build/assets/ba_data/textures/lightSoft.dds": "https://files.ballistica.net/cache/ba1/c9/8a/c3d40ff39d28886e8aeb569fbaeb", + "build/assets/ba_data/textures/lightSoft.ktx": "https://files.ballistica.net/cache/ba1/a1/d0/8bd2561c5f62dd825642ff101b6f", + "build/assets/ba_data/textures/lightSoft.pvr": "https://files.ballistica.net/cache/ba1/dd/37/b1a92a96d22866ee63a932252b88", + "build/assets/ba_data/textures/lightSoft_preview.png": "https://files.ballistica.net/cache/ba1/bf/68/02d9c6b2a26df56a814315d465d4", + "build/assets/ba_data/textures/light_preview.png": "https://files.ballistica.net/cache/ba1/b9/e3/61a15c104a78c3db67df69168d36", + "build/assets/ba_data/textures/lock.dds": "https://files.ballistica.net/cache/ba1/50/c6/a83422dbbe4603f4a0cf3989c068", + "build/assets/ba_data/textures/lock.ktx": "https://files.ballistica.net/cache/ba1/bb/c8/5d8e4d8a1b72ea4accc06a645861", + "build/assets/ba_data/textures/lock.pvr": "https://files.ballistica.net/cache/ba1/1d/44/9f8634042b214fcb7c35c0e28661", + "build/assets/ba_data/textures/lock_preview.png": "https://files.ballistica.net/cache/ba1/26/4d/8c475722321576767dc7aff51f46", + "build/assets/ba_data/textures/logIcon.dds": "https://files.ballistica.net/cache/ba1/0f/81/2318c6b5c5342f7738a86a500b99", + "build/assets/ba_data/textures/logIcon.ktx": "https://files.ballistica.net/cache/ba1/fa/1d/a0f815d51cb25eae435c5c2f7076", + "build/assets/ba_data/textures/logIcon.pvr": "https://files.ballistica.net/cache/ba1/0c/37/f2d8f1e522d2c24fc125ce093a80", + "build/assets/ba_data/textures/logIcon_preview.png": "https://files.ballistica.net/cache/ba1/af/0e/882a4609f983fd1e167d05cf08ef", + "build/assets/ba_data/textures/logo.dds": "https://files.ballistica.net/cache/ba1/97/4a/045fc5b4eca6aaf635d090bfd420", + "build/assets/ba_data/textures/logo.ktx": "https://files.ballistica.net/cache/ba1/39/e9/392891898cee10b3f16dd4ce1242", + "build/assets/ba_data/textures/logo.pvr": "https://files.ballistica.net/cache/ba1/ad/94/578ad756bc3b2fa05742c6ea4ab1", + "build/assets/ba_data/textures/logoEaster.dds": "https://files.ballistica.net/cache/ba1/28/8f/3a1048ee8620986dc2a972a85448", + "build/assets/ba_data/textures/logoEaster.ktx": "https://files.ballistica.net/cache/ba1/37/ec/8cf3638d47d95bfd5441baeca036", + "build/assets/ba_data/textures/logoEaster.pvr": "https://files.ballistica.net/cache/ba1/12/e0/8491108bff044dc37fc8c0df4f14", + "build/assets/ba_data/textures/logoEaster_preview.png": "https://files.ballistica.net/cache/ba1/27/36/2473a27cd5f3a11d685ee7a572ca", + "build/assets/ba_data/textures/logo_preview.png": "https://files.ballistica.net/cache/ba1/af/b4/f01b8ba0d8c6509ffc23d7f8663d", + "build/assets/ba_data/textures/mapPreviewMask.dds": "https://files.ballistica.net/cache/ba1/81/ac/bfb7eb8f724a28dd656c7952dc71", + "build/assets/ba_data/textures/mapPreviewMask.ktx": "https://files.ballistica.net/cache/ba1/c8/0f/6e7060c34c22cf7ddad4a9c3d9b2", + "build/assets/ba_data/textures/mapPreviewMask.pvr": "https://files.ballistica.net/cache/ba1/d2/56/0ab7046535f4334a36407a195687", + "build/assets/ba_data/textures/mapPreviewMask_preview.png": "https://files.ballistica.net/cache/ba1/14/36/60bb6483dddb1df387301ba454ff", + "build/assets/ba_data/textures/medalBronze.dds": "https://files.ballistica.net/cache/ba1/b1/3c/974c016b7412b99b4cfbe2003e30", + "build/assets/ba_data/textures/medalBronze.ktx": "https://files.ballistica.net/cache/ba1/0c/81/2200511fcf027c73b35ce3d73820", + "build/assets/ba_data/textures/medalBronze.pvr": "https://files.ballistica.net/cache/ba1/9e/4f/cb85ed8ae91fb5064db00a2d4b40", + "build/assets/ba_data/textures/medalBronze_preview.png": "https://files.ballistica.net/cache/ba1/39/50/7c04b0e77e8d93f65549082c74dc", + "build/assets/ba_data/textures/medalComplete.dds": "https://files.ballistica.net/cache/ba1/f0/9b/b82c8eb1d4ab1b06855089706640", + "build/assets/ba_data/textures/medalComplete.ktx": "https://files.ballistica.net/cache/ba1/53/f7/c2f9e2a19c31414cbddec6355fbf", + "build/assets/ba_data/textures/medalComplete.pvr": "https://files.ballistica.net/cache/ba1/cb/df/fa511352559e8f83d78e6255cbcd", + "build/assets/ba_data/textures/medalComplete_preview.png": "https://files.ballistica.net/cache/ba1/ea/6b/c5bacf35405848c68e80135318a5", + "build/assets/ba_data/textures/medalGold.dds": "https://files.ballistica.net/cache/ba1/34/3b/0d1853672ffd88c2f38e03314850", + "build/assets/ba_data/textures/medalGold.ktx": "https://files.ballistica.net/cache/ba1/b8/73/1e1630ba5a2f58fa6069eddeb1cd", + "build/assets/ba_data/textures/medalGold.pvr": "https://files.ballistica.net/cache/ba1/87/66/59f70c1db296fb9ee509f2ca4dac", + "build/assets/ba_data/textures/medalGold_preview.png": "https://files.ballistica.net/cache/ba1/cb/03/c778e1d9b38b62f9974d31344505", + "build/assets/ba_data/textures/medalSilver.dds": "https://files.ballistica.net/cache/ba1/08/a5/07fb88b4f8719a280c9e691ba205", + "build/assets/ba_data/textures/medalSilver.ktx": "https://files.ballistica.net/cache/ba1/2a/01/5144ac0ed4698b62d0c023225347", + "build/assets/ba_data/textures/medalSilver.pvr": "https://files.ballistica.net/cache/ba1/aa/d2/aec9f3c4d143dcf391bb0b379219", + "build/assets/ba_data/textures/medalSilver_preview.png": "https://files.ballistica.net/cache/ba1/09/dc/af7553a84b320c8ed781bec532dc", + "build/assets/ba_data/textures/melColor.dds": "https://files.ballistica.net/cache/ba1/38/fe/dabba9604f9aac49a0ea29474cbd", + "build/assets/ba_data/textures/melColor.ktx": "https://files.ballistica.net/cache/ba1/0e/53/ce02e76a4be8b83a12c07aa32214", + "build/assets/ba_data/textures/melColor.pvr": "https://files.ballistica.net/cache/ba1/7c/d9/1d0d0b190e18577d91e2063ea3a9", + "build/assets/ba_data/textures/melColorMask.dds": "https://files.ballistica.net/cache/ba1/94/25/5d5cc59c7e4e4c3c35fd6bf8f782", + "build/assets/ba_data/textures/melColorMask.ktx": "https://files.ballistica.net/cache/ba1/3e/64/8c8848274f6f55730b3e1ed36bec", + "build/assets/ba_data/textures/melColorMask.pvr": "https://files.ballistica.net/cache/ba1/5f/15/cffc8add8c897d129ab333c024b5", + "build/assets/ba_data/textures/melColorMask_preview.png": "https://files.ballistica.net/cache/ba1/fe/5c/43fa88edb520203cab3e45d055ef", + "build/assets/ba_data/textures/melColor_preview.png": "https://files.ballistica.net/cache/ba1/cf/fa/2f517f6e2708c01220feb7e69529", + "build/assets/ba_data/textures/melIcon.dds": "https://files.ballistica.net/cache/ba1/81/d7/c090efb6a4680c1df3c3de219679", + "build/assets/ba_data/textures/melIcon.ktx": "https://files.ballistica.net/cache/ba1/66/80/5a190ba6ec9c6746c52f3cd43425", + "build/assets/ba_data/textures/melIcon.pvr": "https://files.ballistica.net/cache/ba1/23/c0/1dd86062f28d371f5efceb989de4", + "build/assets/ba_data/textures/melIconColorMask.dds": "https://files.ballistica.net/cache/ba1/d5/35/a0a2ba3d044c27b2404731291df7", + "build/assets/ba_data/textures/melIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/ff/6b/15a379a22879df9bcf7526b4e524", + "build/assets/ba_data/textures/melIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/47/02/4f1c6d28b0c065b7ae6ef758737b", + "build/assets/ba_data/textures/melIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/6a/45/13a050673a9c0479fb2d253452b8", + "build/assets/ba_data/textures/melIcon_preview.png": "https://files.ballistica.net/cache/ba1/d8/5a/8d086212095e4c7e229fccd1d6fd", + "build/assets/ba_data/textures/menuBG.dds": "https://files.ballistica.net/cache/ba1/8a/6d/655d3f6b7c1dc4ca2ec898a4e164", + "build/assets/ba_data/textures/menuBG.ktx": "https://files.ballistica.net/cache/ba1/2a/9d/86d534d975c36986667ffc9937b8", + "build/assets/ba_data/textures/menuBG.pvr": "https://files.ballistica.net/cache/ba1/fd/db/b12f53de737bf396cdb35215b5f6", + "build/assets/ba_data/textures/menuBG_preview.png": "https://files.ballistica.net/cache/ba1/bd/a7/39c2b5cef5ada831fb4aea7de827", + "build/assets/ba_data/textures/menuButton.dds": "https://files.ballistica.net/cache/ba1/82/fd/26f8e50ea4a236ebe4a9556fdd17", + "build/assets/ba_data/textures/menuButton.ktx": "https://files.ballistica.net/cache/ba1/00/13/ad5bf92876ee22fa9705098f077e", + "build/assets/ba_data/textures/menuButton.pvr": "https://files.ballistica.net/cache/ba1/2c/83/f6b5abbab02b6653208aea7b42cd", + "build/assets/ba_data/textures/menuButton_preview.png": "https://files.ballistica.net/cache/ba1/48/63/95ec339513aba2c687018f9bf5df", + "build/assets/ba_data/textures/menuIcon.dds": "https://files.ballistica.net/cache/ba1/cf/6c/6c03e857723ec694e3ff5f46b851", + "build/assets/ba_data/textures/menuIcon.ktx": "https://files.ballistica.net/cache/ba1/00/6f/657b6b8d26f85dbd6d6c53d4fc54", + "build/assets/ba_data/textures/menuIcon.pvr": "https://files.ballistica.net/cache/ba1/22/34/309e41cbc7ba2f25b65dcfa6b3ac", + "build/assets/ba_data/textures/menuIcon_preview.png": "https://files.ballistica.net/cache/ba1/99/61/3d059a177c16091157f1643e9b46", + "build/assets/ba_data/textures/merch.dds": "https://files.ballistica.net/cache/ba1/15/c5/4e9059d46701506bf626b4ee995c", + "build/assets/ba_data/textures/merch.ktx": "https://files.ballistica.net/cache/ba1/62/bb/a262a01850a145a2083e1118234d", + "build/assets/ba_data/textures/merch.pvr": "https://files.ballistica.net/cache/ba1/dd/0e/7a581109fa57193282f86eb8a0d5", + "build/assets/ba_data/textures/merch_preview.png": "https://files.ballistica.net/cache/ba1/23/78/761cce15f89b93e4182b03c5ab4e", + "build/assets/ba_data/textures/meter.dds": "https://files.ballistica.net/cache/ba1/02/84/b17fdc96392d131e3b6d17376120", + "build/assets/ba_data/textures/meter.ktx": "https://files.ballistica.net/cache/ba1/cd/c2/416941aa442b00f79108a940be4a", + "build/assets/ba_data/textures/meter.pvr": "https://files.ballistica.net/cache/ba1/9f/8c/626f37c08b7635176c9c3e296521", + "build/assets/ba_data/textures/meter_preview.png": "https://files.ballistica.net/cache/ba1/c7/98/08b8b87fe2b20c2612e497f2baba", + "build/assets/ba_data/textures/monkeyFaceLevelColor.dds": "https://files.ballistica.net/cache/ba1/29/5d/67c2630f2b0f31baa25e452e8697", + "build/assets/ba_data/textures/monkeyFaceLevelColor.ktx": "https://files.ballistica.net/cache/ba1/b3/16/85e41a92b0a167ace0d585692539", + "build/assets/ba_data/textures/monkeyFaceLevelColor.pvr": "https://files.ballistica.net/cache/ba1/d1/82/47fb84896a45f41d1ce594e0a4c6", + "build/assets/ba_data/textures/monkeyFaceLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/65/36/d6ad68a2a63495e56387b58e7be8", + "build/assets/ba_data/textures/monkeyFacePreview.dds": "https://files.ballistica.net/cache/ba1/00/2b/e2779af7516f947fba2e1e094d79", + "build/assets/ba_data/textures/monkeyFacePreview.ktx": "https://files.ballistica.net/cache/ba1/3f/6a/7109310a62bc3880ce56f72fdce0", + "build/assets/ba_data/textures/monkeyFacePreview.pvr": "https://files.ballistica.net/cache/ba1/46/95/69a76d4d57aa6345f158c2bb3dd6", + "build/assets/ba_data/textures/monkeyFacePreview_preview.png": "https://files.ballistica.net/cache/ba1/9d/1b/0c59ad9c3e6d404038fce49b2250", + "build/assets/ba_data/textures/multiplayerExamples.dds": "https://files.ballistica.net/cache/ba1/e4/65/c48c264905a7a06d4488fec5d1c0", + "build/assets/ba_data/textures/multiplayerExamples.ktx": "https://files.ballistica.net/cache/ba1/ec/ac/993ee75f751c9390e281d1364547", + "build/assets/ba_data/textures/multiplayerExamples.pvr": "https://files.ballistica.net/cache/ba1/e1/73/6689fbe171259096eb16f16a0099", + "build/assets/ba_data/textures/multiplayerExamples_preview.png": "https://files.ballistica.net/cache/ba1/35/24/1da1b05ca163dc72741c9bc3a14e", + "build/assets/ba_data/textures/natureBackgroundColor.dds": "https://files.ballistica.net/cache/ba1/25/ec/dca3988ced11548f7b4fb065fa5a", + "build/assets/ba_data/textures/natureBackgroundColor.ktx": "https://files.ballistica.net/cache/ba1/98/5e/9126fe0570f5a4c6801814a11aa6", + "build/assets/ba_data/textures/natureBackgroundColor.pvr": "https://files.ballistica.net/cache/ba1/46/c2/6bbfeeaca5da06c1200b64561008", + "build/assets/ba_data/textures/natureBackgroundColor_preview.png": "https://files.ballistica.net/cache/ba1/f2/b8/68f44eb059ca1c70a65464700da1", + "build/assets/ba_data/textures/neoSpazColor.dds": "https://files.ballistica.net/cache/ba1/9e/93/d2c42e9ef2ee802387f40c950369", + "build/assets/ba_data/textures/neoSpazColor.ktx": "https://files.ballistica.net/cache/ba1/07/af/67ebbca9f6f9c078cc9103e999d7", + "build/assets/ba_data/textures/neoSpazColor.pvr": "https://files.ballistica.net/cache/ba1/4a/8b/f40f2ef992283863ba06d2bd97dc", + "build/assets/ba_data/textures/neoSpazColorMask.dds": "https://files.ballistica.net/cache/ba1/08/cb/5ed944bb9c0de86769023b851b7e", + "build/assets/ba_data/textures/neoSpazColorMask.ktx": "https://files.ballistica.net/cache/ba1/4d/91/72091cb2bf284192ce4757b6db28", + "build/assets/ba_data/textures/neoSpazColorMask.pvr": "https://files.ballistica.net/cache/ba1/b3/cf/16db52c0828bd344b17629e9551e", + "build/assets/ba_data/textures/neoSpazColorMask_preview.png": "https://files.ballistica.net/cache/ba1/b2/0a/d389a3973cfc91181979d31e0cb0", + "build/assets/ba_data/textures/neoSpazColor_preview.png": "https://files.ballistica.net/cache/ba1/d1/ce/783d339763045fe984aeb381aeea", + "build/assets/ba_data/textures/neoSpazIcon.dds": "https://files.ballistica.net/cache/ba1/65/64/b21283208f233a4cea6f69321c99", + "build/assets/ba_data/textures/neoSpazIcon.ktx": "https://files.ballistica.net/cache/ba1/40/ad/696eab8158b253f8f7e85c492dee", + "build/assets/ba_data/textures/neoSpazIcon.pvr": "https://files.ballistica.net/cache/ba1/2f/1b/b8496bf38f7e1ade4c237ddaee30", + "build/assets/ba_data/textures/neoSpazIconColorMask.dds": "https://files.ballistica.net/cache/ba1/ba/3f/f979dda39a6c3a795d9a452c81a5", + "build/assets/ba_data/textures/neoSpazIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/8c/aa/d224cbfe0dac993e46fffb65666b", + "build/assets/ba_data/textures/neoSpazIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/ca/47/75cc7f3a8e6d92101758b8754e92", + "build/assets/ba_data/textures/neoSpazIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/2f/0f/f12fb054a87ff7ad088b8c5e736e", + "build/assets/ba_data/textures/neoSpazIcon_preview.png": "https://files.ballistica.net/cache/ba1/1a/34/b6143a0700276c3b348c69a15f65", + "build/assets/ba_data/textures/nextLevelIcon.dds": "https://files.ballistica.net/cache/ba1/56/15/5915c5e479c1f1085802e51eaf2b", + "build/assets/ba_data/textures/nextLevelIcon.ktx": "https://files.ballistica.net/cache/ba1/0e/b1/c0e7fc213ce99e09e172cc817287", + "build/assets/ba_data/textures/nextLevelIcon.pvr": "https://files.ballistica.net/cache/ba1/f0/68/afb5d4c9ff62f373aea6a43a8093", + "build/assets/ba_data/textures/nextLevelIcon_preview.png": "https://files.ballistica.net/cache/ba1/49/2c/f179fd4a60956299964162d6b5f8", + "build/assets/ba_data/textures/ninjaColor.dds": "https://files.ballistica.net/cache/ba1/a6/12/0a55db709abc0649d255fbf555fb", + "build/assets/ba_data/textures/ninjaColor.ktx": "https://files.ballistica.net/cache/ba1/ef/3a/2ed2ea1e8e857a2b89b030fb7123", + "build/assets/ba_data/textures/ninjaColor.pvr": "https://files.ballistica.net/cache/ba1/d8/4a/1d20273f26a3cc1f93057a98218a", + "build/assets/ba_data/textures/ninjaColorMask.dds": "https://files.ballistica.net/cache/ba1/0d/08/cdd6bba64f42f87ddbe5df475d43", + "build/assets/ba_data/textures/ninjaColorMask.ktx": "https://files.ballistica.net/cache/ba1/dc/79/ab14d3cf5c646b63e82f37f66291", + "build/assets/ba_data/textures/ninjaColorMask.pvr": "https://files.ballistica.net/cache/ba1/5c/e7/506fe27a3742e4183389df2dc1f4", + "build/assets/ba_data/textures/ninjaColorMask_preview.png": "https://files.ballistica.net/cache/ba1/27/34/653996a6f47ae151811bb8ab73cd", + "build/assets/ba_data/textures/ninjaColor_preview.png": "https://files.ballistica.net/cache/ba1/9f/3d/109dc5d318fb655b2979073a62cc", + "build/assets/ba_data/textures/ninjaIcon.dds": "https://files.ballistica.net/cache/ba1/78/ae/27521fe22b486af3d9b725bfe140", + "build/assets/ba_data/textures/ninjaIcon.ktx": "https://files.ballistica.net/cache/ba1/df/07/6f687937bbfd21dbdc513af6f31a", + "build/assets/ba_data/textures/ninjaIcon.pvr": "https://files.ballistica.net/cache/ba1/47/97/d71332db97180a6c46123c9cfd34", + "build/assets/ba_data/textures/ninjaIconColorMask.dds": "https://files.ballistica.net/cache/ba1/3b/3d/3531e20d35e1d7731be6c9419df7", + "build/assets/ba_data/textures/ninjaIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/a9/4b/4390d920527178efa8df4814de77", + "build/assets/ba_data/textures/ninjaIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/c1/71/71980983c8b005885943c6dce710", + "build/assets/ba_data/textures/ninjaIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/92/cc/2a2c3b31c72329af65da3b5ba763", + "build/assets/ba_data/textures/ninjaIcon_preview.png": "https://files.ballistica.net/cache/ba1/0b/87/70d70de030a02aecabea55a9a80c", + "build/assets/ba_data/textures/nub.dds": "https://files.ballistica.net/cache/ba1/ae/92/b0778866ef9809782f0af18f5172", + "build/assets/ba_data/textures/nub.ktx": "https://files.ballistica.net/cache/ba1/2d/91/af72f467f936f82103f6ca38498d", + "build/assets/ba_data/textures/nub.pvr": "https://files.ballistica.net/cache/ba1/d3/6a/b817e713cbd93578671609b72652", + "build/assets/ba_data/textures/nub_preview.png": "https://files.ballistica.net/cache/ba1/35/72/e9b5436f306b59e7aa9f50c2da48", + "build/assets/ba_data/textures/null.dds": "https://files.ballistica.net/cache/ba1/d4/b8/3e5ec1536cfdc1d4485a48b1bafb", + "build/assets/ba_data/textures/null.ktx": "https://files.ballistica.net/cache/ba1/54/35/cfe53efcd8e28f2ec59be9369661", + "build/assets/ba_data/textures/null.pvr": "https://files.ballistica.net/cache/ba1/5c/25/75f292d60a6fb1e142b1580bba85", + "build/assets/ba_data/textures/null_preview.png": "https://files.ballistica.net/cache/ba1/7c/aa/69765c194c8d1abad6478587c09e", + "build/assets/ba_data/textures/oldLadyColor.dds": "https://files.ballistica.net/cache/ba1/14/0e/21b42fd725b0fca0fcc244f7bc4a", + "build/assets/ba_data/textures/oldLadyColor.ktx": "https://files.ballistica.net/cache/ba1/10/8b/6fa6b1d6762e263c884169b94642", + "build/assets/ba_data/textures/oldLadyColor.pvr": "https://files.ballistica.net/cache/ba1/b1/1c/7c2e964ad618e34a2f107ff0d32d", + "build/assets/ba_data/textures/oldLadyColorMask.dds": "https://files.ballistica.net/cache/ba1/29/68/8f228431f14aee727e9389b31ea5", + "build/assets/ba_data/textures/oldLadyColorMask.ktx": "https://files.ballistica.net/cache/ba1/14/a8/3db1d3fc7c070f0171b541bbcd7f", + "build/assets/ba_data/textures/oldLadyColorMask.pvr": "https://files.ballistica.net/cache/ba1/2d/2a/9192363346dffcde5c3dd9da674b", + "build/assets/ba_data/textures/oldLadyColorMask_preview.png": "https://files.ballistica.net/cache/ba1/52/8b/1f50f30cfdc13eb73f1cc0b22bac", + "build/assets/ba_data/textures/oldLadyColor_preview.png": "https://files.ballistica.net/cache/ba1/c9/db/5975ed0bd87a42e7e9f9f5a42f83", + "build/assets/ba_data/textures/oldLadyIcon.dds": "https://files.ballistica.net/cache/ba1/37/34/8a4de56230aeff35fc1ba4da4696", + "build/assets/ba_data/textures/oldLadyIcon.ktx": "https://files.ballistica.net/cache/ba1/9e/ab/0d314f72cd906ab7107e3abadc7d", + "build/assets/ba_data/textures/oldLadyIcon.pvr": "https://files.ballistica.net/cache/ba1/3e/07/be0a9400ace970b9ca06b263cbaf", + "build/assets/ba_data/textures/oldLadyIconColorMask.dds": "https://files.ballistica.net/cache/ba1/0a/ff/6246442462ca2e153a443f5cad18", + "build/assets/ba_data/textures/oldLadyIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/a1/e3/f3e254d707047ffb36c318ec59e3", + "build/assets/ba_data/textures/oldLadyIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/61/7f/c64400b0f72e21d7deefb056bf45", + "build/assets/ba_data/textures/oldLadyIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/9b/2e/d0050bb447b331709b1a930695bd", + "build/assets/ba_data/textures/oldLadyIcon_preview.png": "https://files.ballistica.net/cache/ba1/3a/4f/e1e5a303c4f7bdb7fbadf18541e2", + "build/assets/ba_data/textures/operaSingerColor.dds": "https://files.ballistica.net/cache/ba1/4d/71/a45b41d88cfb0acd9f5e377bbe02", + "build/assets/ba_data/textures/operaSingerColor.ktx": "https://files.ballistica.net/cache/ba1/db/0b/96ce94531b0b4673530ae342f7ec", + "build/assets/ba_data/textures/operaSingerColor.pvr": "https://files.ballistica.net/cache/ba1/1d/14/515c261f8d45ca6bd68a39c04ddf", + "build/assets/ba_data/textures/operaSingerColorMask.dds": "https://files.ballistica.net/cache/ba1/6f/24/65b48bf163bd0f29f16993e7a656", + "build/assets/ba_data/textures/operaSingerColorMask.ktx": "https://files.ballistica.net/cache/ba1/74/1b/a996e83a5c1ea3bce2fb6aa1787f", + "build/assets/ba_data/textures/operaSingerColorMask.pvr": "https://files.ballistica.net/cache/ba1/1d/f9/731d7b46c8b9010462371b7fbe28", + "build/assets/ba_data/textures/operaSingerColorMask_preview.png": "https://files.ballistica.net/cache/ba1/6b/7a/206c1c0fa44718d2f5ff917d0f9c", + "build/assets/ba_data/textures/operaSingerColor_preview.png": "https://files.ballistica.net/cache/ba1/5b/65/4d0672df7f44b8b7f338473ad487", + "build/assets/ba_data/textures/operaSingerIcon.dds": "https://files.ballistica.net/cache/ba1/02/99/1503de3822cffbd63533a7749925", + "build/assets/ba_data/textures/operaSingerIcon.ktx": "https://files.ballistica.net/cache/ba1/cd/85/34b3dde8be5af1b82973bf0110f4", + "build/assets/ba_data/textures/operaSingerIcon.pvr": "https://files.ballistica.net/cache/ba1/39/50/e07aacc46afd2dd66b5a60a5b294", + "build/assets/ba_data/textures/operaSingerIconColorMask.dds": "https://files.ballistica.net/cache/ba1/49/a2/e34dc247514813deda74f4213a03", + "build/assets/ba_data/textures/operaSingerIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/b7/6e/6e779dd2afb7e4baff902ff3f928", + "build/assets/ba_data/textures/operaSingerIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/00/66/c60bafadbc24397fc5c61f4f1a85", + "build/assets/ba_data/textures/operaSingerIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/60/1b/7730d01cfeadef70d1c7fdc7a759", + "build/assets/ba_data/textures/operaSingerIcon_preview.png": "https://files.ballistica.net/cache/ba1/31/fc/9b34025a59b9b2be174f07eea7db", + "build/assets/ba_data/textures/ouyaAButton.dds": "https://files.ballistica.net/cache/ba1/8f/04/249dceb323fe6f65317aacd69b68", + "build/assets/ba_data/textures/ouyaAButton.ktx": "https://files.ballistica.net/cache/ba1/a3/bf/15b831c5c5f12324449da47d8fb2", + "build/assets/ba_data/textures/ouyaAButton.pvr": "https://files.ballistica.net/cache/ba1/08/bc/0918e308da78e59f8dfa2f9504cb", + "build/assets/ba_data/textures/ouyaAButton_preview.png": "https://files.ballistica.net/cache/ba1/40/a4/2f5366bddf74244fd3f764242fc2", + "build/assets/ba_data/textures/ouyaIcon.dds": "https://files.ballistica.net/cache/ba1/75/03/442b97a97bf860578e9078e113cd", + "build/assets/ba_data/textures/ouyaIcon.ktx": "https://files.ballistica.net/cache/ba1/77/1d/a9758b432f7adc7b6fcbf4fb4970", + "build/assets/ba_data/textures/ouyaIcon.pvr": "https://files.ballistica.net/cache/ba1/cc/92/60e908f2e7f4ddcfdb85805d9dfe", + "build/assets/ba_data/textures/ouyaIcon_preview.png": "https://files.ballistica.net/cache/ba1/e7/62/eb808ffb9b0fdfaf7b5d1a23318c", + "build/assets/ba_data/textures/ouyaOButton.dds": "https://files.ballistica.net/cache/ba1/b7/03/e2d535b98b236e8f596f3de964da", + "build/assets/ba_data/textures/ouyaOButton.ktx": "https://files.ballistica.net/cache/ba1/a1/02/07a2c82536e0261cdeb06c4c51e9", + "build/assets/ba_data/textures/ouyaOButton.pvr": "https://files.ballistica.net/cache/ba1/3b/12/844e94ec0ae683aaa37670d57a15", + "build/assets/ba_data/textures/ouyaOButton_preview.png": "https://files.ballistica.net/cache/ba1/67/4b/c130f560121ffa232b3ea1d4ff0e", + "build/assets/ba_data/textures/ouyaUButton.dds": "https://files.ballistica.net/cache/ba1/7b/5a/bebe0fdc8495cb5551a4b56be6b2", + "build/assets/ba_data/textures/ouyaUButton.ktx": "https://files.ballistica.net/cache/ba1/8b/52/9e1c9dbe9af488b387ca77ca7da1", + "build/assets/ba_data/textures/ouyaUButton.pvr": "https://files.ballistica.net/cache/ba1/2b/87/9c73774ddfc3d2efa7077fc64b12", + "build/assets/ba_data/textures/ouyaUButton_preview.png": "https://files.ballistica.net/cache/ba1/d3/5f/e77a27f2d7c87e34376cbd1a006f", + "build/assets/ba_data/textures/ouyaYButton.dds": "https://files.ballistica.net/cache/ba1/c6/bf/4d390df2963d87124c877ab61765", + "build/assets/ba_data/textures/ouyaYButton.ktx": "https://files.ballistica.net/cache/ba1/f2/f7/37c87ca972259ff6cb1effe355bc", + "build/assets/ba_data/textures/ouyaYButton.pvr": "https://files.ballistica.net/cache/ba1/86/97/ebcd024b40f5883ac0ec9994658b", + "build/assets/ba_data/textures/ouyaYButton_preview.png": "https://files.ballistica.net/cache/ba1/ed/df/1d061046a637a55842bc39dc8365", + "build/assets/ba_data/textures/penguinColor.dds": "https://files.ballistica.net/cache/ba1/cd/e4/47ce1bed8413410d7ba31e579527", + "build/assets/ba_data/textures/penguinColor.ktx": "https://files.ballistica.net/cache/ba1/5d/a9/e64a6ce63f0bee9de4dba526510b", + "build/assets/ba_data/textures/penguinColor.pvr": "https://files.ballistica.net/cache/ba1/32/45/61a61f56944fee4a61c47b9f826b", + "build/assets/ba_data/textures/penguinColorMask.dds": "https://files.ballistica.net/cache/ba1/1d/00/628e2de4633092b1ac998c1bd921", + "build/assets/ba_data/textures/penguinColorMask.ktx": "https://files.ballistica.net/cache/ba1/8e/53/be3d750c81c7fa95d65755921a39", + "build/assets/ba_data/textures/penguinColorMask.pvr": "https://files.ballistica.net/cache/ba1/e9/e6/c39bb65c79d771aa7eef0af897bf", + "build/assets/ba_data/textures/penguinColorMask_preview.png": "https://files.ballistica.net/cache/ba1/5c/bd/583e086c64520e5ed619aced364b", + "build/assets/ba_data/textures/penguinColor_preview.png": "https://files.ballistica.net/cache/ba1/53/9c/4da562a683bd39f805dd159d63da", + "build/assets/ba_data/textures/penguinIcon.dds": "https://files.ballistica.net/cache/ba1/79/af/9b65a37ad93beb8261db32e6f8be", + "build/assets/ba_data/textures/penguinIcon.ktx": "https://files.ballistica.net/cache/ba1/aa/b3/2c323260119a57a69cbff07d05fc", + "build/assets/ba_data/textures/penguinIcon.pvr": "https://files.ballistica.net/cache/ba1/83/9a/e53827174929f07dd8acdb9e515f", + "build/assets/ba_data/textures/penguinIconColorMask.dds": "https://files.ballistica.net/cache/ba1/ea/89/7940924c4c1dcb55f116102f947b", + "build/assets/ba_data/textures/penguinIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/a2/63/fb1c0bc7e55c0b4e8d45c81aeaff", + "build/assets/ba_data/textures/penguinIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/e2/4c/69b3a43f3cfba82b9ac25b7193bc", + "build/assets/ba_data/textures/penguinIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/89/16/31baf29bcbce290cee85c8749c51", + "build/assets/ba_data/textures/penguinIcon_preview.png": "https://files.ballistica.net/cache/ba1/55/28/8c1f6fdee52728e224639725c2f7", + "build/assets/ba_data/textures/pixieColor.dds": "https://files.ballistica.net/cache/ba1/4c/60/5325351a0cd75a381bb326e2e999", + "build/assets/ba_data/textures/pixieColor.ktx": "https://files.ballistica.net/cache/ba1/a9/2b/554e2bc4a9d4b0cf6a62936f55cc", + "build/assets/ba_data/textures/pixieColor.pvr": "https://files.ballistica.net/cache/ba1/17/f6/3fe6240e4612bea96a29253fcb6a", + "build/assets/ba_data/textures/pixieColorMask.dds": "https://files.ballistica.net/cache/ba1/91/2b/0b86d6fd2e14b8549d0ee9e04632", + "build/assets/ba_data/textures/pixieColorMask.ktx": "https://files.ballistica.net/cache/ba1/93/81/3d666da2d541f58b736523c17f7e", + "build/assets/ba_data/textures/pixieColorMask.pvr": "https://files.ballistica.net/cache/ba1/e7/61/30db6c8321a9b7bffd85969d56ea", + "build/assets/ba_data/textures/pixieColorMask_preview.png": "https://files.ballistica.net/cache/ba1/01/1e/4053d469f77676a26eadfef01b04", + "build/assets/ba_data/textures/pixieColor_preview.png": "https://files.ballistica.net/cache/ba1/58/48/4b6af62286aa5797224739cfea7e", + "build/assets/ba_data/textures/pixieIcon.dds": "https://files.ballistica.net/cache/ba1/b5/f9/f30b7633ffb790c7afa50f15ca21", + "build/assets/ba_data/textures/pixieIcon.ktx": "https://files.ballistica.net/cache/ba1/ba/50/814e155a61ff7d1f7603d999fc7c", + "build/assets/ba_data/textures/pixieIcon.pvr": "https://files.ballistica.net/cache/ba1/c2/b5/01ce9496378f618fe9d5f0f24ac7", + "build/assets/ba_data/textures/pixieIconColorMask.dds": "https://files.ballistica.net/cache/ba1/21/87/b271609c765e55fa3cf4b9d89a2f", + "build/assets/ba_data/textures/pixieIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/c3/4c/51ed34761afadf25dde8a4932f3b", + "build/assets/ba_data/textures/pixieIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/2c/9a/2dc13e367e254e20e83a595b5b4f", + "build/assets/ba_data/textures/pixieIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/a7/7d/a3bcb3cbc236de84a661c9ca3f76", + "build/assets/ba_data/textures/pixieIcon_preview.png": "https://files.ballistica.net/cache/ba1/79/d2/be8c833f49793f475208eb97309c", + "build/assets/ba_data/textures/playerLineup.dds": "https://files.ballistica.net/cache/ba1/14/14/b99dfa468ed72598950b42889781", + "build/assets/ba_data/textures/playerLineup.ktx": "https://files.ballistica.net/cache/ba1/e1/49/fd6f78ddb5f62863e2023fcebb02", + "build/assets/ba_data/textures/playerLineup.pvr": "https://files.ballistica.net/cache/ba1/e0/c9/cb984eae0f2c7ad1204890c60f17", + "build/assets/ba_data/textures/playerLineup_preview.png": "https://files.ballistica.net/cache/ba1/a9/66/b2ef7427fb3493bb313410d3b3ae", + "build/assets/ba_data/textures/powerupBomb.dds": "https://files.ballistica.net/cache/ba1/fc/cd/d03d92d24a47ec30d647c296bfdb", + "build/assets/ba_data/textures/powerupBomb.ktx": "https://files.ballistica.net/cache/ba1/68/32/649f3ec1db1da233f20faf317377", + "build/assets/ba_data/textures/powerupBomb.pvr": "https://files.ballistica.net/cache/ba1/87/cf/b0d60ec93e455c394498b18ffb78", + "build/assets/ba_data/textures/powerupBomb_preview.png": "https://files.ballistica.net/cache/ba1/22/5b/1c1f944a5d58e65b3d097eee111f", + "build/assets/ba_data/textures/powerupCurse.dds": "https://files.ballistica.net/cache/ba1/56/11/d907d63ca3fd93bb97539000e109", + "build/assets/ba_data/textures/powerupCurse.ktx": "https://files.ballistica.net/cache/ba1/74/8a/97ab73d3b525026bb28c96abce42", + "build/assets/ba_data/textures/powerupCurse.pvr": "https://files.ballistica.net/cache/ba1/05/e1/527e5ea984debf96bb47fdaa3099", + "build/assets/ba_data/textures/powerupCurse_preview.png": "https://files.ballistica.net/cache/ba1/ee/98/df9817f23e1ae5f9045c9d435042", + "build/assets/ba_data/textures/powerupHealth.dds": "https://files.ballistica.net/cache/ba1/97/e6/70d925ad80cd446c122e108ddfa7", + "build/assets/ba_data/textures/powerupHealth.ktx": "https://files.ballistica.net/cache/ba1/bd/d3/d96b9561ab6da6bada6c5d3b98fb", + "build/assets/ba_data/textures/powerupHealth.pvr": "https://files.ballistica.net/cache/ba1/c8/52/d72afde1930ff40f6d1b6ca52ac5", + "build/assets/ba_data/textures/powerupHealth_preview.png": "https://files.ballistica.net/cache/ba1/23/73/5456b590f9dd7ead5595dec549c8", + "build/assets/ba_data/textures/powerupIceBombs.dds": "https://files.ballistica.net/cache/ba1/f8/0b/c0a704590e2cafd591936327fd08", + "build/assets/ba_data/textures/powerupIceBombs.ktx": "https://files.ballistica.net/cache/ba1/f2/67/2db8feeaa99644a7c319b39354d0", + "build/assets/ba_data/textures/powerupIceBombs.pvr": "https://files.ballistica.net/cache/ba1/5f/04/ccc2f4c775d316cad0c98a974d57", + "build/assets/ba_data/textures/powerupIceBombs_preview.png": "https://files.ballistica.net/cache/ba1/39/b1/65a463bf9cfc8b373fed498f15da", + "build/assets/ba_data/textures/powerupImpactBombs.dds": "https://files.ballistica.net/cache/ba1/cf/b8/ce70712e337593d325b79cc5fa89", + "build/assets/ba_data/textures/powerupImpactBombs.ktx": "https://files.ballistica.net/cache/ba1/a5/18/c781691b5a7eacd7879bbd8bda8c", + "build/assets/ba_data/textures/powerupImpactBombs.pvr": "https://files.ballistica.net/cache/ba1/10/ba/02bb149da101a2c3d24cad91e990", + "build/assets/ba_data/textures/powerupImpactBombs_preview.png": "https://files.ballistica.net/cache/ba1/7d/e4/e481b464418e4fd8e0e422d778f1", + "build/assets/ba_data/textures/powerupLandMines.dds": "https://files.ballistica.net/cache/ba1/36/f2/52159bf1380556f4b2a8dad6260b", + "build/assets/ba_data/textures/powerupLandMines.ktx": "https://files.ballistica.net/cache/ba1/47/e7/56ae78fc9ba456588f53c6752ee9", + "build/assets/ba_data/textures/powerupLandMines.pvr": "https://files.ballistica.net/cache/ba1/dc/36/f967220ba94fb721637c487645b5", + "build/assets/ba_data/textures/powerupLandMines_preview.png": "https://files.ballistica.net/cache/ba1/ec/ff/3f8da95b3b7b7867498863dcbd0d", + "build/assets/ba_data/textures/powerupPunch.dds": "https://files.ballistica.net/cache/ba1/48/fa/504599744b5ca5ecaa300af6ff3e", + "build/assets/ba_data/textures/powerupPunch.ktx": "https://files.ballistica.net/cache/ba1/2e/f5/e972cd7f7cac4c8c9e6694ed8472", + "build/assets/ba_data/textures/powerupPunch.pvr": "https://files.ballistica.net/cache/ba1/9f/e7/d53c74e8a5c8bb7c46c24056ecb6", + "build/assets/ba_data/textures/powerupPunch_preview.png": "https://files.ballistica.net/cache/ba1/d3/dc/8b6981f896d78dca3ae917d5b64f", + "build/assets/ba_data/textures/powerupShield.dds": "https://files.ballistica.net/cache/ba1/d0/03/9c3090719e3db5db6abfe1b55731", + "build/assets/ba_data/textures/powerupShield.ktx": "https://files.ballistica.net/cache/ba1/1a/d3/3d1bde66c825ab44b9457cfbdec8", + "build/assets/ba_data/textures/powerupShield.pvr": "https://files.ballistica.net/cache/ba1/e7/c2/2ee8bb33f1e7f16c760205ef2efb", + "build/assets/ba_data/textures/powerupShield_preview.png": "https://files.ballistica.net/cache/ba1/59/b7/b73455e5a1c37eaf3eed8699b419", + "build/assets/ba_data/textures/powerupSpeed.dds": "https://files.ballistica.net/cache/ba1/7c/7e/af09a30194e039e17f835c844d46", + "build/assets/ba_data/textures/powerupSpeed.ktx": "https://files.ballistica.net/cache/ba1/df/43/774a601d75223b93abcee822f493", + "build/assets/ba_data/textures/powerupSpeed.pvr": "https://files.ballistica.net/cache/ba1/e7/d5/e1b69ca67f054d0c002a93e9f194", + "build/assets/ba_data/textures/powerupSpeed_preview.png": "https://files.ballistica.net/cache/ba1/75/b8/14dd947be1229f655c6466b5388a", + "build/assets/ba_data/textures/powerupStickyBombs.dds": "https://files.ballistica.net/cache/ba1/bc/4e/5b96d371761a6eff1a4ed2b197bf", + "build/assets/ba_data/textures/powerupStickyBombs.ktx": "https://files.ballistica.net/cache/ba1/a2/64/d31c373a69ca64070459a142e7b2", + "build/assets/ba_data/textures/powerupStickyBombs.pvr": "https://files.ballistica.net/cache/ba1/11/6e/a194101845bfb39f120975f07462", + "build/assets/ba_data/textures/powerupStickyBombs_preview.png": "https://files.ballistica.net/cache/ba1/ad/67/cac4f163289cce2df78bb4945e3e", + "build/assets/ba_data/textures/puckColor.dds": "https://files.ballistica.net/cache/ba1/54/b2/f6abdefc7c89058af90e5980cc2c", + "build/assets/ba_data/textures/puckColor.ktx": "https://files.ballistica.net/cache/ba1/3a/7b/57198468ef370d9407a1abb6cba5", + "build/assets/ba_data/textures/puckColor.pvr": "https://files.ballistica.net/cache/ba1/4e/98/979b29d786fc5c4814004596b759", + "build/assets/ba_data/textures/puckColor_preview.png": "https://files.ballistica.net/cache/ba1/3a/54/d44e5d26d459ee72ba21c695894a", + "build/assets/ba_data/textures/rampageBGColor.dds": "https://files.ballistica.net/cache/ba1/94/61/5741c11090af17ead40e09f1eb8c", + "build/assets/ba_data/textures/rampageBGColor.ktx": "https://files.ballistica.net/cache/ba1/b0/bd/b3a662a4e2101d0e32aae42d42d5", + "build/assets/ba_data/textures/rampageBGColor.pvr": "https://files.ballistica.net/cache/ba1/d7/8f/380c6ce089e35914cb3209446bdf", + "build/assets/ba_data/textures/rampageBGColor2.dds": "https://files.ballistica.net/cache/ba1/e4/d4/9df47a75ca07e371ad639fcacebb", + "build/assets/ba_data/textures/rampageBGColor2.ktx": "https://files.ballistica.net/cache/ba1/cf/32/116df1238a04afcb09529dee9462", + "build/assets/ba_data/textures/rampageBGColor2.pvr": "https://files.ballistica.net/cache/ba1/b4/b0/4e6a57e71108ba9d9fd0ba8dcb21", + "build/assets/ba_data/textures/rampageBGColor2_preview.png": "https://files.ballistica.net/cache/ba1/ac/f5/d06049a17d55cb10e200f5efe6e1", + "build/assets/ba_data/textures/rampageBGColor_preview.png": "https://files.ballistica.net/cache/ba1/77/c3/967691d4fefa76b4d8b6def1a6e5", + "build/assets/ba_data/textures/rampageLevelColor.dds": "https://files.ballistica.net/cache/ba1/b3/67/2e3cf0e17e5326aa98759dcd768e", + "build/assets/ba_data/textures/rampageLevelColor.ktx": "https://files.ballistica.net/cache/ba1/a2/cb/8e331b0a09f8e236a4d6c90d366f", + "build/assets/ba_data/textures/rampageLevelColor.pvr": "https://files.ballistica.net/cache/ba1/23/51/c2e9f680d0f33b20cf2ea2fb04f3", + "build/assets/ba_data/textures/rampageLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/62/68/229d5cb94a31d1ef860935f03347", + "build/assets/ba_data/textures/rampagePreview.dds": "https://files.ballistica.net/cache/ba1/5f/71/f5f5c3b7f032437c4843fd694f95", + "build/assets/ba_data/textures/rampagePreview.ktx": "https://files.ballistica.net/cache/ba1/b7/f7/c02607b302f2ed7d266ee2e0009a", + "build/assets/ba_data/textures/rampagePreview.pvr": "https://files.ballistica.net/cache/ba1/04/c8/4556cd33441405c47d6fb6f3652b", + "build/assets/ba_data/textures/rampagePreview_preview.png": "https://files.ballistica.net/cache/ba1/cb/ec/fffa4f29096ac14ec71a22b8dea3", + "build/assets/ba_data/textures/reflectionChar_+x.dds": "https://files.ballistica.net/cache/ba1/f7/50/d8947860fda2d27100977d47deab", + "build/assets/ba_data/textures/reflectionChar_+x.ktx": "https://files.ballistica.net/cache/ba1/5f/e7/365590d7cbfef588dbb167c29643", + "build/assets/ba_data/textures/reflectionChar_+x.pvr": "https://files.ballistica.net/cache/ba1/b6/bb/88a4fca76be9e479fd84bbb31f79", + "build/assets/ba_data/textures/reflectionChar_+x_preview.png": "https://files.ballistica.net/cache/ba1/cb/d4/fec6e4fc94ba882de18ec8209393", + "build/assets/ba_data/textures/reflectionChar_+y.dds": "https://files.ballistica.net/cache/ba1/c1/0e/2cb467d9a07924879464cedeae4e", + "build/assets/ba_data/textures/reflectionChar_+y.ktx": "https://files.ballistica.net/cache/ba1/63/53/be1f9d395fa9663c48a8b5bf827a", + "build/assets/ba_data/textures/reflectionChar_+y.pvr": "https://files.ballistica.net/cache/ba1/51/c0/a6fe38eeb6c19406d5dce4ea6c40", + "build/assets/ba_data/textures/reflectionChar_+y_preview.png": "https://files.ballistica.net/cache/ba1/d1/12/33207089a989438478075e48b268", + "build/assets/ba_data/textures/reflectionChar_+z.dds": "https://files.ballistica.net/cache/ba1/76/c9/0303189bd6689a9045639dddab96", + "build/assets/ba_data/textures/reflectionChar_+z.ktx": "https://files.ballistica.net/cache/ba1/7d/2e/b875d4f3963248bf25c2a0b0519f", + "build/assets/ba_data/textures/reflectionChar_+z.pvr": "https://files.ballistica.net/cache/ba1/11/b8/722fbc40c9bd1f911948d13b11c8", + "build/assets/ba_data/textures/reflectionChar_+z_preview.png": "https://files.ballistica.net/cache/ba1/ca/d3/24e408ef6f4cb10c705e72c34349", + "build/assets/ba_data/textures/reflectionChar_-x.dds": "https://files.ballistica.net/cache/ba1/47/3c/b93aa5702416329684178394dfc9", + "build/assets/ba_data/textures/reflectionChar_-x.ktx": "https://files.ballistica.net/cache/ba1/af/a5/d905271db1c5abc7f23fe1eb68fe", + "build/assets/ba_data/textures/reflectionChar_-x.pvr": "https://files.ballistica.net/cache/ba1/4d/f6/8a8be07f8b3064d0bfda628eb38b", + "build/assets/ba_data/textures/reflectionChar_-x_preview.png": "https://files.ballistica.net/cache/ba1/ac/ea/6d7a7b245562c53bc9076a97f935", + "build/assets/ba_data/textures/reflectionChar_-y.dds": "https://files.ballistica.net/cache/ba1/93/8d/3884deb9cd475060cfa876daeb2f", + "build/assets/ba_data/textures/reflectionChar_-y.ktx": "https://files.ballistica.net/cache/ba1/30/0d/0b29867edfc8487022573da99da5", + "build/assets/ba_data/textures/reflectionChar_-y.pvr": "https://files.ballistica.net/cache/ba1/89/b8/a56996c6edde15b4b7d7b0eaa943", + "build/assets/ba_data/textures/reflectionChar_-y_preview.png": "https://files.ballistica.net/cache/ba1/13/54/8a34eac73ea6f9f30215dba5c1e3", + "build/assets/ba_data/textures/reflectionChar_-z.dds": "https://files.ballistica.net/cache/ba1/79/25/a2c5da69e63ac02f64d0bac4c0f5", + "build/assets/ba_data/textures/reflectionChar_-z.ktx": "https://files.ballistica.net/cache/ba1/3e/8c/03d0be48f4dd850a40661c62e173", + "build/assets/ba_data/textures/reflectionChar_-z.pvr": "https://files.ballistica.net/cache/ba1/13/c6/707859f1820e9f7aacd7cc070bea", + "build/assets/ba_data/textures/reflectionChar_-z_preview.png": "https://files.ballistica.net/cache/ba1/a3/14/9e067ed84cb6a0e565f642534099", + "build/assets/ba_data/textures/reflectionPowerup_+x.dds": "https://files.ballistica.net/cache/ba1/b1/5a/66307afbe74178bbcb8d594b141c", + "build/assets/ba_data/textures/reflectionPowerup_+x.ktx": "https://files.ballistica.net/cache/ba1/2e/57/59b1d2bc4786426f3192b14aa836", + "build/assets/ba_data/textures/reflectionPowerup_+x.pvr": "https://files.ballistica.net/cache/ba1/a3/1a/d0359e55c462397e079fed9f22e9", + "build/assets/ba_data/textures/reflectionPowerup_+x_preview.png": "https://files.ballistica.net/cache/ba1/c1/0b/3e8a4ceaa4ffbe7182e10fb7180a", + "build/assets/ba_data/textures/reflectionPowerup_+y.dds": "https://files.ballistica.net/cache/ba1/d9/6b/79515951452da5856cce0811f6bc", + "build/assets/ba_data/textures/reflectionPowerup_+y.ktx": "https://files.ballistica.net/cache/ba1/f7/e3/52d325e0c495e0c55a37fcd4e4a1", + "build/assets/ba_data/textures/reflectionPowerup_+y.pvr": "https://files.ballistica.net/cache/ba1/91/3c/1ffc155e5a0478638e31e4bc06d5", + "build/assets/ba_data/textures/reflectionPowerup_+y_preview.png": "https://files.ballistica.net/cache/ba1/47/20/678f7a8b07f74dc09c56a5e0d676", + "build/assets/ba_data/textures/reflectionPowerup_+z.dds": "https://files.ballistica.net/cache/ba1/be/29/438d2ed36c6aa4e9cecb9884b275", + "build/assets/ba_data/textures/reflectionPowerup_+z.ktx": "https://files.ballistica.net/cache/ba1/1a/56/0123af56ceb3d4dc474fac4217a4", + "build/assets/ba_data/textures/reflectionPowerup_+z.pvr": "https://files.ballistica.net/cache/ba1/0d/5f/ef81383de9b3e33e1df852c9f41e", + "build/assets/ba_data/textures/reflectionPowerup_+z_preview.png": "https://files.ballistica.net/cache/ba1/aa/c0/e9fcf567c57209c69d7a423de16f", + "build/assets/ba_data/textures/reflectionPowerup_-x.dds": "https://files.ballistica.net/cache/ba1/54/eb/4408071a42a455f3f5651b832276", + "build/assets/ba_data/textures/reflectionPowerup_-x.ktx": "https://files.ballistica.net/cache/ba1/7a/40/baf2a65439ec26fd00ea9a2dfe0f", + "build/assets/ba_data/textures/reflectionPowerup_-x.pvr": "https://files.ballistica.net/cache/ba1/6c/ac/bf3a970db00510b735ca25c78e7f", + "build/assets/ba_data/textures/reflectionPowerup_-x_preview.png": "https://files.ballistica.net/cache/ba1/8c/f4/6b49ebe7a2dcd07e6fe67d30adb3", + "build/assets/ba_data/textures/reflectionPowerup_-y.dds": "https://files.ballistica.net/cache/ba1/8d/5b/2de8698c84f9510ae524fed9c7c1", + "build/assets/ba_data/textures/reflectionPowerup_-y.ktx": "https://files.ballistica.net/cache/ba1/ed/29/fe038b278eeeb400f941e1e867f2", + "build/assets/ba_data/textures/reflectionPowerup_-y.pvr": "https://files.ballistica.net/cache/ba1/7b/f9/23673984bd98440a49bc8957f4fe", + "build/assets/ba_data/textures/reflectionPowerup_-y_preview.png": "https://files.ballistica.net/cache/ba1/fc/66/b1df994f4c3d274549f56b4250d7", + "build/assets/ba_data/textures/reflectionPowerup_-z.dds": "https://files.ballistica.net/cache/ba1/0f/ca/7443437368473654d5660e339f1c", + "build/assets/ba_data/textures/reflectionPowerup_-z.ktx": "https://files.ballistica.net/cache/ba1/09/6b/f55d4f732f4cc4ff42076e625354", + "build/assets/ba_data/textures/reflectionPowerup_-z.pvr": "https://files.ballistica.net/cache/ba1/a5/73/414d2173bb1fdb0ac32681701f98", + "build/assets/ba_data/textures/reflectionPowerup_-z_preview.png": "https://files.ballistica.net/cache/ba1/0d/f4/ed45ff45072cc4467829a8ba3779", + "build/assets/ba_data/textures/reflectionSharp_+x.dds": "https://files.ballistica.net/cache/ba1/62/94/23d24efbd4458737babecd6b6408", + "build/assets/ba_data/textures/reflectionSharp_+x.ktx": "https://files.ballistica.net/cache/ba1/ef/66/2c7ad34c054200b0c5660936453e", + "build/assets/ba_data/textures/reflectionSharp_+x.pvr": "https://files.ballistica.net/cache/ba1/a6/c4/255fe005be28ef1b28124f64329a", + "build/assets/ba_data/textures/reflectionSharp_+x_preview.png": "https://files.ballistica.net/cache/ba1/fb/48/46199abdfbf6462e557853770997", + "build/assets/ba_data/textures/reflectionSharp_+y.dds": "https://files.ballistica.net/cache/ba1/4f/63/f7c094171f7e1f20980f8d9e5bb7", + "build/assets/ba_data/textures/reflectionSharp_+y.ktx": "https://files.ballistica.net/cache/ba1/b3/f7/7128c087ec4dd86bd5003d691c14", + "build/assets/ba_data/textures/reflectionSharp_+y.pvr": "https://files.ballistica.net/cache/ba1/f2/65/c576b3888020f684c1df1fe0bba4", + "build/assets/ba_data/textures/reflectionSharp_+y_preview.png": "https://files.ballistica.net/cache/ba1/35/d4/40c7d921539db990cb50dd9d412d", + "build/assets/ba_data/textures/reflectionSharp_+z.dds": "https://files.ballistica.net/cache/ba1/80/96/432d58350e331ff20578d88bfe07", + "build/assets/ba_data/textures/reflectionSharp_+z.ktx": "https://files.ballistica.net/cache/ba1/63/93/29ecf3f920135d7b9bdcb6182154", + "build/assets/ba_data/textures/reflectionSharp_+z.pvr": "https://files.ballistica.net/cache/ba1/73/c4/0f84f86e8a128594fea6d69d8277", + "build/assets/ba_data/textures/reflectionSharp_+z_preview.png": "https://files.ballistica.net/cache/ba1/29/be/a4c14444521a80c74f540e0b2f30", + "build/assets/ba_data/textures/reflectionSharp_-x.dds": "https://files.ballistica.net/cache/ba1/e1/f9/2e3f5e24b87b73cbaa409a47751d", + "build/assets/ba_data/textures/reflectionSharp_-x.ktx": "https://files.ballistica.net/cache/ba1/87/91/e8c6983d9cf5fae1d424987b16d0", + "build/assets/ba_data/textures/reflectionSharp_-x.pvr": "https://files.ballistica.net/cache/ba1/c7/fd/d3adfc6404936686e846e5c33834", + "build/assets/ba_data/textures/reflectionSharp_-x_preview.png": "https://files.ballistica.net/cache/ba1/32/3d/e534385a575434e5a8428f9d6f61", + "build/assets/ba_data/textures/reflectionSharp_-y.dds": "https://files.ballistica.net/cache/ba1/a3/9d/ca4bc6d20aa0e8635013dac8a6e2", + "build/assets/ba_data/textures/reflectionSharp_-y.ktx": "https://files.ballistica.net/cache/ba1/64/b4/3952f530fad80625f5b6c97e0a63", + "build/assets/ba_data/textures/reflectionSharp_-y.pvr": "https://files.ballistica.net/cache/ba1/3b/59/5ec90f8cc789ab686ae9fe5c231b", + "build/assets/ba_data/textures/reflectionSharp_-y_preview.png": "https://files.ballistica.net/cache/ba1/4d/a1/8e2ad1409ca6fad653bc05b79c89", + "build/assets/ba_data/textures/reflectionSharp_-z.dds": "https://files.ballistica.net/cache/ba1/e7/dd/bff1bb85f165ca0eaea24a68fa9b", + "build/assets/ba_data/textures/reflectionSharp_-z.ktx": "https://files.ballistica.net/cache/ba1/0d/1d/02a236075ffd3a2680f9139d3699", + "build/assets/ba_data/textures/reflectionSharp_-z.pvr": "https://files.ballistica.net/cache/ba1/f8/5d/012d37dcb722bd01689c066e0615", + "build/assets/ba_data/textures/reflectionSharp_-z_preview.png": "https://files.ballistica.net/cache/ba1/cb/87/920cc8acef2f61a9200ca414a6f5", + "build/assets/ba_data/textures/reflectionSharper_+x.dds": "https://files.ballistica.net/cache/ba1/8b/2e/0a257ed7eb39479dbefce10504d8", + "build/assets/ba_data/textures/reflectionSharper_+x.ktx": "https://files.ballistica.net/cache/ba1/ba/38/e74d9da6593a6e1963e2078f1fa9", + "build/assets/ba_data/textures/reflectionSharper_+x.pvr": "https://files.ballistica.net/cache/ba1/f1/9d/65f08738abed9cc5ac20e9ee3adc", + "build/assets/ba_data/textures/reflectionSharper_+x_preview.png": "https://files.ballistica.net/cache/ba1/5d/2c/8db067c4600929791f44cfa62d03", + "build/assets/ba_data/textures/reflectionSharper_+y.dds": "https://files.ballistica.net/cache/ba1/e4/75/c90bb182933c46cfbbc577adb80b", + "build/assets/ba_data/textures/reflectionSharper_+y.ktx": "https://files.ballistica.net/cache/ba1/d9/06/7cf50791e4cce878b47b78cba23a", + "build/assets/ba_data/textures/reflectionSharper_+y.pvr": "https://files.ballistica.net/cache/ba1/70/a8/0d50eb917cf3264a75ce522758a1", + "build/assets/ba_data/textures/reflectionSharper_+y_preview.png": "https://files.ballistica.net/cache/ba1/87/3a/04bbfbee2c71b8bf747b695448fb", + "build/assets/ba_data/textures/reflectionSharper_+z.dds": "https://files.ballistica.net/cache/ba1/64/1e/abfeef31bebc51b33880c9a870e5", + "build/assets/ba_data/textures/reflectionSharper_+z.ktx": "https://files.ballistica.net/cache/ba1/10/99/01abf6b1acb47f8ea7e30e614e67", + "build/assets/ba_data/textures/reflectionSharper_+z.pvr": "https://files.ballistica.net/cache/ba1/19/56/81cfc29ac520fc8f99467feed331", + "build/assets/ba_data/textures/reflectionSharper_+z_preview.png": "https://files.ballistica.net/cache/ba1/6d/8d/565985180de03a795bfd9bd212d2", + "build/assets/ba_data/textures/reflectionSharper_-x.dds": "https://files.ballistica.net/cache/ba1/36/c3/a150c81e21ba14b75134c95319a7", + "build/assets/ba_data/textures/reflectionSharper_-x.ktx": "https://files.ballistica.net/cache/ba1/57/e6/e16b38edbed2af1803a0eb9418db", + "build/assets/ba_data/textures/reflectionSharper_-x.pvr": "https://files.ballistica.net/cache/ba1/75/c9/e6f392c4f0190b2ab1851b7509f7", + "build/assets/ba_data/textures/reflectionSharper_-x_preview.png": "https://files.ballistica.net/cache/ba1/94/59/927601569ab601572076c9ce168b", + "build/assets/ba_data/textures/reflectionSharper_-y.dds": "https://files.ballistica.net/cache/ba1/78/37/d017389d415773a4d1e95c41367f", + "build/assets/ba_data/textures/reflectionSharper_-y.ktx": "https://files.ballistica.net/cache/ba1/b2/93/bb8319311f36795927b100cbffed", + "build/assets/ba_data/textures/reflectionSharper_-y.pvr": "https://files.ballistica.net/cache/ba1/24/84/1c3a209e8b89c7044c1c3e48985d", + "build/assets/ba_data/textures/reflectionSharper_-y_preview.png": "https://files.ballistica.net/cache/ba1/b2/1d/52d78c7c66cf7632b3ccd5ba3795", + "build/assets/ba_data/textures/reflectionSharper_-z.dds": "https://files.ballistica.net/cache/ba1/60/f7/a947942da55b4f1e364f58b2b455", + "build/assets/ba_data/textures/reflectionSharper_-z.ktx": "https://files.ballistica.net/cache/ba1/71/a0/7e957f45694e4817e51458a019be", + "build/assets/ba_data/textures/reflectionSharper_-z.pvr": "https://files.ballistica.net/cache/ba1/67/72/76ee42deb4ea909cba8951ae761c", + "build/assets/ba_data/textures/reflectionSharper_-z_preview.png": "https://files.ballistica.net/cache/ba1/ca/90/fbb127f6cc11ca1ff9d397e2778f", + "build/assets/ba_data/textures/reflectionSharpest_+x.dds": "https://files.ballistica.net/cache/ba1/40/b6/5ffd5bd8f2735441dda0b335d041", + "build/assets/ba_data/textures/reflectionSharpest_+x.ktx": "https://files.ballistica.net/cache/ba1/cd/99/9fe3f3e8801fc62a3b407b499a12", + "build/assets/ba_data/textures/reflectionSharpest_+x.pvr": "https://files.ballistica.net/cache/ba1/46/60/f605adaa3981ea9913fe77141322", + "build/assets/ba_data/textures/reflectionSharpest_+x_preview.png": "https://files.ballistica.net/cache/ba1/b7/a7/0401315e7b1a6d505611ca9da475", + "build/assets/ba_data/textures/reflectionSharpest_+y.dds": "https://files.ballistica.net/cache/ba1/74/2f/5f518ecc87c10ac8a5e004014914", + "build/assets/ba_data/textures/reflectionSharpest_+y.ktx": "https://files.ballistica.net/cache/ba1/9f/bc/1fddf329db3efaf9d31bcf730567", + "build/assets/ba_data/textures/reflectionSharpest_+y.pvr": "https://files.ballistica.net/cache/ba1/e8/94/552448b0993fe3d521ff473f1653", + "build/assets/ba_data/textures/reflectionSharpest_+y_preview.png": "https://files.ballistica.net/cache/ba1/93/52/8e1aaae41d773ac63503170a9054", + "build/assets/ba_data/textures/reflectionSharpest_+z.dds": "https://files.ballistica.net/cache/ba1/9f/45/ae3540518f20067b39e79f3ac8b5", + "build/assets/ba_data/textures/reflectionSharpest_+z.ktx": "https://files.ballistica.net/cache/ba1/98/29/a144bd6517ed0e1f97ca7ab952c3", + "build/assets/ba_data/textures/reflectionSharpest_+z.pvr": "https://files.ballistica.net/cache/ba1/4b/3d/c5d0be748523b5e39e7364c5a9eb", + "build/assets/ba_data/textures/reflectionSharpest_+z_preview.png": "https://files.ballistica.net/cache/ba1/60/f1/4cbf58cef81caeb092e6b3087bfd", + "build/assets/ba_data/textures/reflectionSharpest_-x.dds": "https://files.ballistica.net/cache/ba1/2b/23/003be933f39e155dfb5e21671fce", + "build/assets/ba_data/textures/reflectionSharpest_-x.ktx": "https://files.ballistica.net/cache/ba1/27/ce/8ff7ae3b7bc2a9cb6e78f2fad912", + "build/assets/ba_data/textures/reflectionSharpest_-x.pvr": "https://files.ballistica.net/cache/ba1/f1/6a/cbd92a31fb9cd4332a808652a2b8", + "build/assets/ba_data/textures/reflectionSharpest_-x_preview.png": "https://files.ballistica.net/cache/ba1/47/cf/7c97260371978c12c72bf727d09c", + "build/assets/ba_data/textures/reflectionSharpest_-y.dds": "https://files.ballistica.net/cache/ba1/ab/85/3b270e14210604d5688639cdfea6", + "build/assets/ba_data/textures/reflectionSharpest_-y.ktx": "https://files.ballistica.net/cache/ba1/b8/82/96972f72cc505e4ec270f7ab0db6", + "build/assets/ba_data/textures/reflectionSharpest_-y.pvr": "https://files.ballistica.net/cache/ba1/ad/c2/23a5cf239959f6cf68ed02ae2cd3", + "build/assets/ba_data/textures/reflectionSharpest_-y_preview.png": "https://files.ballistica.net/cache/ba1/38/99/d1a1b33fc1dcb476321fa26dfdf1", + "build/assets/ba_data/textures/reflectionSharpest_-z.dds": "https://files.ballistica.net/cache/ba1/7d/53/c050ed054db5a1145d02c6ee39e4", + "build/assets/ba_data/textures/reflectionSharpest_-z.ktx": "https://files.ballistica.net/cache/ba1/24/9f/548f2b801f9a20f424ae93f10366", + "build/assets/ba_data/textures/reflectionSharpest_-z.pvr": "https://files.ballistica.net/cache/ba1/23/44/7c2db0fb12bc568c4b63bc3a8157", + "build/assets/ba_data/textures/reflectionSharpest_-z_preview.png": "https://files.ballistica.net/cache/ba1/69/64/e1fee5493719b15fc2e72ab1f6b1", + "build/assets/ba_data/textures/reflectionSoft_+x.dds": "https://files.ballistica.net/cache/ba1/8a/de/59285bfd71abf4b9a090a9f25bc6", + "build/assets/ba_data/textures/reflectionSoft_+x.ktx": "https://files.ballistica.net/cache/ba1/f9/0d/b1a0763945ec85a3a8cf53511f0a", + "build/assets/ba_data/textures/reflectionSoft_+x.pvr": "https://files.ballistica.net/cache/ba1/0f/dc/6dff5b8e5b25a40d2869d3b68d5b", + "build/assets/ba_data/textures/reflectionSoft_+x_preview.png": "https://files.ballistica.net/cache/ba1/32/07/6fab8a4e4296430a574045656404", + "build/assets/ba_data/textures/reflectionSoft_+y.dds": "https://files.ballistica.net/cache/ba1/92/d6/bd81458dcc86c23daa7a791431f7", + "build/assets/ba_data/textures/reflectionSoft_+y.ktx": "https://files.ballistica.net/cache/ba1/35/e3/e9d31e6ca61f0445b9e0e77c3080", + "build/assets/ba_data/textures/reflectionSoft_+y.pvr": "https://files.ballistica.net/cache/ba1/27/34/d758c53f205c3ba85b1a7c85b6c1", + "build/assets/ba_data/textures/reflectionSoft_+y_preview.png": "https://files.ballistica.net/cache/ba1/04/fa/f97602445c808f2f7b394ab53483", + "build/assets/ba_data/textures/reflectionSoft_+z.dds": "https://files.ballistica.net/cache/ba1/99/2f/f6d63a93cc29751108e23a13c88e", + "build/assets/ba_data/textures/reflectionSoft_+z.ktx": "https://files.ballistica.net/cache/ba1/40/0f/fc5b721b2633791f232c87bd9039", + "build/assets/ba_data/textures/reflectionSoft_+z.pvr": "https://files.ballistica.net/cache/ba1/54/76/579e1ded7b5977034212b335984b", + "build/assets/ba_data/textures/reflectionSoft_+z_preview.png": "https://files.ballistica.net/cache/ba1/e6/a1/6556f25f9487c075cf5f5a5572f0", + "build/assets/ba_data/textures/reflectionSoft_-x.dds": "https://files.ballistica.net/cache/ba1/d4/b2/25edf29cd27e7412856afc4357f9", + "build/assets/ba_data/textures/reflectionSoft_-x.ktx": "https://files.ballistica.net/cache/ba1/cb/ec/5e767bcea68f27110abaac76981a", + "build/assets/ba_data/textures/reflectionSoft_-x.pvr": "https://files.ballistica.net/cache/ba1/9d/ec/3523607c49fe88b4af80de21a7bd", + "build/assets/ba_data/textures/reflectionSoft_-x_preview.png": "https://files.ballistica.net/cache/ba1/ed/e6/52cb468da32bfea2cf288568a9c3", + "build/assets/ba_data/textures/reflectionSoft_-y.dds": "https://files.ballistica.net/cache/ba1/52/78/c482cdf7af5ed961aa3dd0dd6565", + "build/assets/ba_data/textures/reflectionSoft_-y.ktx": "https://files.ballistica.net/cache/ba1/58/3e/e1bf329d40591663b4e3d3329c51", + "build/assets/ba_data/textures/reflectionSoft_-y.pvr": "https://files.ballistica.net/cache/ba1/26/40/254010efdc7d1b1408767ff29eb2", + "build/assets/ba_data/textures/reflectionSoft_-y_preview.png": "https://files.ballistica.net/cache/ba1/41/e2/6c9c9840c5b9af0a5b8c290ac98b", + "build/assets/ba_data/textures/reflectionSoft_-z.dds": "https://files.ballistica.net/cache/ba1/81/c3/30bf1d23d23642ba3afa830074a4", + "build/assets/ba_data/textures/reflectionSoft_-z.ktx": "https://files.ballistica.net/cache/ba1/50/b2/c8626bd6b5d3be421fb437fff116", + "build/assets/ba_data/textures/reflectionSoft_-z.pvr": "https://files.ballistica.net/cache/ba1/fb/2e/01d7f0aaa93daf2d2dad1c9d51d1", + "build/assets/ba_data/textures/reflectionSoft_-z_preview.png": "https://files.ballistica.net/cache/ba1/b7/30/0f98d08c4e26976b9eb0e9043fca", + "build/assets/ba_data/textures/replayIcon.dds": "https://files.ballistica.net/cache/ba1/b0/23/67c94a59c236a98f8ff0979d3cce", + "build/assets/ba_data/textures/replayIcon.ktx": "https://files.ballistica.net/cache/ba1/ca/db/a9977ec396a22c2d9dbef3601050", + "build/assets/ba_data/textures/replayIcon.pvr": "https://files.ballistica.net/cache/ba1/39/16/cb7c798272965b558352b7c5029b", + "build/assets/ba_data/textures/replayIcon_preview.png": "https://files.ballistica.net/cache/ba1/ca/a7/8e2367d0e07aeecdf7b71b7d7a70", + "build/assets/ba_data/textures/rgbStripes.dds": "https://files.ballistica.net/cache/ba1/9d/6f/7e34a6e18ddcdda9facd979a789b", + "build/assets/ba_data/textures/rgbStripes.ktx": "https://files.ballistica.net/cache/ba1/20/1b/4957139848ef63663acd2d014fac", + "build/assets/ba_data/textures/rgbStripes.pvr": "https://files.ballistica.net/cache/ba1/08/c7/c830bf8e2a49d4c51cb3de1e0f92", + "build/assets/ba_data/textures/rgbStripes_preview.png": "https://files.ballistica.net/cache/ba1/f2/d3/57f8bae92481031e92f2412c96ad", + "build/assets/ba_data/textures/rightButton.dds": "https://files.ballistica.net/cache/ba1/f7/ba/4e3cc2a78ae63a72a312cbeddb4e", + "build/assets/ba_data/textures/rightButton.ktx": "https://files.ballistica.net/cache/ba1/5c/02/49b7dab4e3e4c5d4c2be94596d55", + "build/assets/ba_data/textures/rightButton.pvr": "https://files.ballistica.net/cache/ba1/71/b5/72c0feaacc8a989f41513ca0237f", + "build/assets/ba_data/textures/rightButton_preview.png": "https://files.ballistica.net/cache/ba1/21/10/1486b562de6fa323389a0d478525", + "build/assets/ba_data/textures/robotColor.dds": "https://files.ballistica.net/cache/ba1/ce/0e/5ccb3b5f99957244b8721ec7ff37", + "build/assets/ba_data/textures/robotColor.ktx": "https://files.ballistica.net/cache/ba1/c3/e7/d53687c3eed408d351cedba5331a", + "build/assets/ba_data/textures/robotColor.pvr": "https://files.ballistica.net/cache/ba1/dc/7c/7fb60475b4a56b5ca7860daedddf", + "build/assets/ba_data/textures/robotColorMask.dds": "https://files.ballistica.net/cache/ba1/4a/02/9cd159f7b9eec3b259d31886bdf8", + "build/assets/ba_data/textures/robotColorMask.ktx": "https://files.ballistica.net/cache/ba1/2a/76/04956a7dc9f3e2afcec27a614c2a", + "build/assets/ba_data/textures/robotColorMask.pvr": "https://files.ballistica.net/cache/ba1/43/93/d3907edaa3812288dc1ba918f8de", + "build/assets/ba_data/textures/robotColorMask_preview.png": "https://files.ballistica.net/cache/ba1/69/e0/5b17803503cf4a9547615a1c5c64", + "build/assets/ba_data/textures/robotColor_preview.png": "https://files.ballistica.net/cache/ba1/77/57/f2ab61d5ff3da71d5d03f48fa04c", + "build/assets/ba_data/textures/robotIcon.dds": "https://files.ballistica.net/cache/ba1/c3/6d/9080cd4b90a30a499657efbc301d", + "build/assets/ba_data/textures/robotIcon.ktx": "https://files.ballistica.net/cache/ba1/27/ae/7c48802d63a4b3aa17b5f7a1dd81", + "build/assets/ba_data/textures/robotIcon.pvr": "https://files.ballistica.net/cache/ba1/b7/80/7a3793b466a81d5b738d265b907c", + "build/assets/ba_data/textures/robotIconColorMask.dds": "https://files.ballistica.net/cache/ba1/cb/04/fdd52b4d047edd83d590c951e775", + "build/assets/ba_data/textures/robotIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/89/77/5e384eb9e73e248a98f313f6a02f", + "build/assets/ba_data/textures/robotIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/7c/4d/26be40e69aa222aa05c87ea079ec", + "build/assets/ba_data/textures/robotIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/21/22/39569e526cd9fa13a72bad9cbb63", + "build/assets/ba_data/textures/robotIcon_preview.png": "https://files.ballistica.net/cache/ba1/09/56/d339719ec405c9622525cf6caf74", + "build/assets/ba_data/textures/roundaboutLevelColor.dds": "https://files.ballistica.net/cache/ba1/6e/bb/c727f3feb70222555159679d1d28", + "build/assets/ba_data/textures/roundaboutLevelColor.ktx": "https://files.ballistica.net/cache/ba1/2b/ed/a2ffc57649163341e9b659a0a341", + "build/assets/ba_data/textures/roundaboutLevelColor.pvr": "https://files.ballistica.net/cache/ba1/cb/a9/5ca43724ef62a9829b0e55169671", + "build/assets/ba_data/textures/roundaboutLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/19/cf/7a636036118b55c8ba24638da923", + "build/assets/ba_data/textures/roundaboutPreview.dds": "https://files.ballistica.net/cache/ba1/a2/32/2ec0e80061943c5c7b67161e36be", + "build/assets/ba_data/textures/roundaboutPreview.ktx": "https://files.ballistica.net/cache/ba1/1c/c3/93b47837712116e2baadae25971d", + "build/assets/ba_data/textures/roundaboutPreview.pvr": "https://files.ballistica.net/cache/ba1/e2/29/51c7b310e96b4db97cb6f92b182d", + "build/assets/ba_data/textures/roundaboutPreview_preview.png": "https://files.ballistica.net/cache/ba1/13/95/efb16647f78df6c3f6e4e43983b0", + "build/assets/ba_data/textures/santaColor.dds": "https://files.ballistica.net/cache/ba1/e1/49/e17fcc4d1deeb1fe14f242dd2df7", + "build/assets/ba_data/textures/santaColor.ktx": "https://files.ballistica.net/cache/ba1/e3/9c/c34b0a416010ee86f991448a3e41", + "build/assets/ba_data/textures/santaColor.pvr": "https://files.ballistica.net/cache/ba1/d3/f1/062b23313cbea4eda19c6e9e08a0", + "build/assets/ba_data/textures/santaColorMask.dds": "https://files.ballistica.net/cache/ba1/c6/90/22994fe821b9eb1d182753e0035d", + "build/assets/ba_data/textures/santaColorMask.ktx": "https://files.ballistica.net/cache/ba1/0f/8b/7966dc9851ef64b184622bb14a6d", + "build/assets/ba_data/textures/santaColorMask.pvr": "https://files.ballistica.net/cache/ba1/3e/fb/9fb134b391fc9b4688dc4a7a9719", + "build/assets/ba_data/textures/santaColorMask_preview.png": "https://files.ballistica.net/cache/ba1/f9/98/682826e2ee0958955b1f68dd3329", + "build/assets/ba_data/textures/santaColor_preview.png": "https://files.ballistica.net/cache/ba1/85/95/606673c3a96c65a962df2519f594", + "build/assets/ba_data/textures/santaIcon.dds": "https://files.ballistica.net/cache/ba1/00/93/a71bc7080cb3f14e85a8993a421c", + "build/assets/ba_data/textures/santaIcon.ktx": "https://files.ballistica.net/cache/ba1/ac/fc/98d671b527c8bf74ea1d00b11c07", + "build/assets/ba_data/textures/santaIcon.pvr": "https://files.ballistica.net/cache/ba1/89/61/9fe45a5da2e43703c1abea5c9e4d", + "build/assets/ba_data/textures/santaIconColorMask.dds": "https://files.ballistica.net/cache/ba1/45/ae/969cc8b4925fa1ed26173aaff6e3", + "build/assets/ba_data/textures/santaIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/80/f3/f1cd4485991168f96d620cb92219", + "build/assets/ba_data/textures/santaIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/f1/96/a56368b4f2451e1c2d5a5287aa65", + "build/assets/ba_data/textures/santaIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/11/72/836e91fbc9a6661cb93fef9afce1", + "build/assets/ba_data/textures/santaIcon_preview.png": "https://files.ballistica.net/cache/ba1/cd/19/8f202957547423eda591fc1252d6", + "build/assets/ba_data/textures/scorch.dds": "https://files.ballistica.net/cache/ba1/cd/9b/71806259d2898ecdaff002a8dade", + "build/assets/ba_data/textures/scorch.ktx": "https://files.ballistica.net/cache/ba1/92/92/5081dbfd19e51022e3a8679f19f7", + "build/assets/ba_data/textures/scorch.pvr": "https://files.ballistica.net/cache/ba1/11/16/29df8644432447a0b3b1b7f55c85", + "build/assets/ba_data/textures/scorchBig.dds": "https://files.ballistica.net/cache/ba1/84/d4/80d570c0b779c5885f7d307b1949", + "build/assets/ba_data/textures/scorchBig.ktx": "https://files.ballistica.net/cache/ba1/38/fa/a5c9f40edba611b74dc8b97079ff", + "build/assets/ba_data/textures/scorchBig.pvr": "https://files.ballistica.net/cache/ba1/37/74/fc9d9832937767ff56927ab2abb6", + "build/assets/ba_data/textures/scorchBig_preview.png": "https://files.ballistica.net/cache/ba1/17/08/fb38d27bdc969db4fac7326b553a", + "build/assets/ba_data/textures/scorch_preview.png": "https://files.ballistica.net/cache/ba1/c2/94/3aabb015f92afcb891911e0b5a12", + "build/assets/ba_data/textures/scrollWidget.dds": "https://files.ballistica.net/cache/ba1/1b/bd/9207aba53d9d1f92de085ca35f79", + "build/assets/ba_data/textures/scrollWidget.ktx": "https://files.ballistica.net/cache/ba1/08/51/6b437300126918f01aaa8bf120c0", + "build/assets/ba_data/textures/scrollWidget.pvr": "https://files.ballistica.net/cache/ba1/8a/54/f3fd3b9c9d2f9f14f6b6d9a88fce", + "build/assets/ba_data/textures/scrollWidgetGlow.dds": "https://files.ballistica.net/cache/ba1/0e/d6/8c8b6c276ca6a11179b198085aae", + "build/assets/ba_data/textures/scrollWidgetGlow.ktx": "https://files.ballistica.net/cache/ba1/6e/6f/b1c17b8e55ae33799e3640f2d495", + "build/assets/ba_data/textures/scrollWidgetGlow.pvr": "https://files.ballistica.net/cache/ba1/21/c3/7f273da7aa7ef8a10ee2cde9e48b", + "build/assets/ba_data/textures/scrollWidgetGlow_preview.png": "https://files.ballistica.net/cache/ba1/92/21/2fb6812d44213c11cb16170606dd", + "build/assets/ba_data/textures/scrollWidget_preview.png": "https://files.ballistica.net/cache/ba1/79/f7/c74f512e0f02584e455f69bea95b", + "build/assets/ba_data/textures/settingsIcon.dds": "https://files.ballistica.net/cache/ba1/95/82/5a1a3a8b043924bd74f05c16fe1e", + "build/assets/ba_data/textures/settingsIcon.ktx": "https://files.ballistica.net/cache/ba1/29/43/79d51487d8855380665a82d4e98a", + "build/assets/ba_data/textures/settingsIcon.pvr": "https://files.ballistica.net/cache/ba1/a8/71/354e314fc92d696a06c6f50941e7", + "build/assets/ba_data/textures/settingsIcon_preview.png": "https://files.ballistica.net/cache/ba1/9c/90/f6b0c2175baed6590b71a879bc36", + "build/assets/ba_data/textures/shadow.dds": "https://files.ballistica.net/cache/ba1/06/c0/de130371102d11c6e1b3adb7a76c", + "build/assets/ba_data/textures/shadow.ktx": "https://files.ballistica.net/cache/ba1/e2/57/9008afe55395653ba66cafdf8822", + "build/assets/ba_data/textures/shadow.pvr": "https://files.ballistica.net/cache/ba1/63/0d/3777bd3a0442835d99cb58f1172a", + "build/assets/ba_data/textures/shadowSharp.dds": "https://files.ballistica.net/cache/ba1/0a/20/ec43c7a50eeb652f103e1bd3d942", + "build/assets/ba_data/textures/shadowSharp.ktx": "https://files.ballistica.net/cache/ba1/92/61/97bcb59648001d8f1d71d5c50464", + "build/assets/ba_data/textures/shadowSharp.pvr": "https://files.ballistica.net/cache/ba1/62/63/5ad85a0efe5895de2055be31e1eb", + "build/assets/ba_data/textures/shadowSharp_preview.png": "https://files.ballistica.net/cache/ba1/72/52/d3636de1f0596412d0c610eb346a", + "build/assets/ba_data/textures/shadowSoft.dds": "https://files.ballistica.net/cache/ba1/3a/29/086f699636407ea85c32e9b61241", + "build/assets/ba_data/textures/shadowSoft.ktx": "https://files.ballistica.net/cache/ba1/fd/c5/e60bd291ac80cb8664719060f659", + "build/assets/ba_data/textures/shadowSoft.pvr": "https://files.ballistica.net/cache/ba1/a8/b8/8a96ed68816718d9a92c2c3e9d0e", + "build/assets/ba_data/textures/shadowSoft_preview.png": "https://files.ballistica.net/cache/ba1/32/9b/91f725c104e808cbf9f3ca165f67", + "build/assets/ba_data/textures/shadow_preview.png": "https://files.ballistica.net/cache/ba1/87/45/a88d820540f6dcb5c5bf7cfafc02", + "build/assets/ba_data/textures/shield.dds": "https://files.ballistica.net/cache/ba1/7c/6a/5ecbef8cc895b4ec563b80879132", + "build/assets/ba_data/textures/shield.ktx": "https://files.ballistica.net/cache/ba1/df/19/64acf34ef888a63394ba5daf5eec", + "build/assets/ba_data/textures/shield.pvr": "https://files.ballistica.net/cache/ba1/8c/21/382dd62330775450f3fb6e29fc3b", + "build/assets/ba_data/textures/shield_preview.png": "https://files.ballistica.net/cache/ba1/a5/7c/1defc477c928ff9aa9396822ea42", + "build/assets/ba_data/textures/shrapnel1Color.dds": "https://files.ballistica.net/cache/ba1/f6/ef/ec1df8174e2f4f0731cf10ffeb83", + "build/assets/ba_data/textures/shrapnel1Color.ktx": "https://files.ballistica.net/cache/ba1/08/ab/433427c0942c58bdbe90ae304250", + "build/assets/ba_data/textures/shrapnel1Color.pvr": "https://files.ballistica.net/cache/ba1/0f/0b/2299436c571ab41eabf71e11e482", + "build/assets/ba_data/textures/shrapnel1Color_preview.png": "https://files.ballistica.net/cache/ba1/01/3c/8c7c17226e2f4a4e7a8243dbcb12", + "build/assets/ba_data/textures/slash.dds": "https://files.ballistica.net/cache/ba1/34/7b/7d3ecbb943cf34381309c8029d7b", + "build/assets/ba_data/textures/slash.ktx": "https://files.ballistica.net/cache/ba1/30/67/a03ac5db3c0e4cf340e02062b417", + "build/assets/ba_data/textures/slash.pvr": "https://files.ballistica.net/cache/ba1/27/27/ac451c6fe68087e7d63b70b5938b", + "build/assets/ba_data/textures/slash_preview.png": "https://files.ballistica.net/cache/ba1/3d/1d/601b1e6d62cc517e934d66a23769", + "build/assets/ba_data/textures/smoke.dds": "https://files.ballistica.net/cache/ba1/9d/a3/edb4304aa6766654b5bb70f418bc", + "build/assets/ba_data/textures/smoke.ktx": "https://files.ballistica.net/cache/ba1/16/ea/acf9dea76ec6f95e356c0bacd88e", + "build/assets/ba_data/textures/smoke.pvr": "https://files.ballistica.net/cache/ba1/3c/f3/af02a85c8c592407621d64e87d31", + "build/assets/ba_data/textures/smoke_preview.png": "https://files.ballistica.net/cache/ba1/e7/2d/11262da5f7e44eada2d07713545b", + "build/assets/ba_data/textures/softRect.dds": "https://files.ballistica.net/cache/ba1/b8/31/ecf4ff71997c0735cc6eb0b3bb98", + "build/assets/ba_data/textures/softRect.ktx": "https://files.ballistica.net/cache/ba1/e7/c1/f20af66d49675189362395988681", + "build/assets/ba_data/textures/softRect.pvr": "https://files.ballistica.net/cache/ba1/6f/4d/2114de974e9da458df831c1f0f81", + "build/assets/ba_data/textures/softRect2.dds": "https://files.ballistica.net/cache/ba1/db/ee/1b6a0cf414ca39fbb97a6dc4e1d1", + "build/assets/ba_data/textures/softRect2.ktx": "https://files.ballistica.net/cache/ba1/1a/24/d17fc63783328e08a8b2300e3f31", + "build/assets/ba_data/textures/softRect2.pvr": "https://files.ballistica.net/cache/ba1/67/08/cc1ee35ad388cab1acfe25b031d8", + "build/assets/ba_data/textures/softRect2_preview.png": "https://files.ballistica.net/cache/ba1/30/11/2eff62f29595d7fac00551b1b082", + "build/assets/ba_data/textures/softRectVertical.dds": "https://files.ballistica.net/cache/ba1/9e/9d/05b5e0f3f86fc8553193e7e656a7", + "build/assets/ba_data/textures/softRectVertical.ktx": "https://files.ballistica.net/cache/ba1/4b/15/1e0c7a85506210386b46af7b5eae", + "build/assets/ba_data/textures/softRectVertical.pvr": "https://files.ballistica.net/cache/ba1/9d/4d/9379c94b397edee6c1b2fd091247", + "build/assets/ba_data/textures/softRectVertical_preview.png": "https://files.ballistica.net/cache/ba1/1b/9d/b422f5cdf8dbd01580e9bc796372", + "build/assets/ba_data/textures/softRect_preview.png": "https://files.ballistica.net/cache/ba1/67/42/f657ca2d4ee9dee1bb3d92446048", + "build/assets/ba_data/textures/sparks.dds": "https://files.ballistica.net/cache/ba1/7f/49/567f1a500245b026146969608361", + "build/assets/ba_data/textures/sparks.ktx": "https://files.ballistica.net/cache/ba1/0f/fc/985d27461ff681001fa2e5fce008", + "build/assets/ba_data/textures/sparks.pvr": "https://files.ballistica.net/cache/ba1/3c/59/54c400ce70054bd53004cb6622fd", + "build/assets/ba_data/textures/sparks_preview.png": "https://files.ballistica.net/cache/ba1/7b/f8/6c8f11c621a19ff29dbcde338bb2", + "build/assets/ba_data/textures/star.dds": "https://files.ballistica.net/cache/ba1/99/f3/0de823009b309e583faa939b341f", + "build/assets/ba_data/textures/star.ktx": "https://files.ballistica.net/cache/ba1/e8/5b/7126deaa363b170d6333bb4b33c1", + "build/assets/ba_data/textures/star.pvr": "https://files.ballistica.net/cache/ba1/90/44/3fb4ca13f8adaad3e755f73926ea", + "build/assets/ba_data/textures/star_preview.png": "https://files.ballistica.net/cache/ba1/0e/eb/0d09e6e87607eb2750ef0e38cc1e", + "build/assets/ba_data/textures/startButton.dds": "https://files.ballistica.net/cache/ba1/75/9a/4319ba5be7843e6095fcb366d998", + "build/assets/ba_data/textures/startButton.ktx": "https://files.ballistica.net/cache/ba1/74/06/eef2865e6c75caeb39f3db5fbb0a", + "build/assets/ba_data/textures/startButton.pvr": "https://files.ballistica.net/cache/ba1/24/b2/5644225437efe7fcefd3918e4445", + "build/assets/ba_data/textures/startButton_preview.png": "https://files.ballistica.net/cache/ba1/54/08/e98efcb4eac546dcb1e4b8ae16c1", + "build/assets/ba_data/textures/stepRightUpLevelColor.dds": "https://files.ballistica.net/cache/ba1/f0/66/9342f37825061f9b03d3464365ec", + "build/assets/ba_data/textures/stepRightUpLevelColor.ktx": "https://files.ballistica.net/cache/ba1/ee/a5/2b5b2543a21c0fc1412fe74dc53e", + "build/assets/ba_data/textures/stepRightUpLevelColor.pvr": "https://files.ballistica.net/cache/ba1/24/53/b96a0a501e2be92ff4757be359f3", + "build/assets/ba_data/textures/stepRightUpLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/af/51/2547aaea3d9d95c1d45ac96dd623", + "build/assets/ba_data/textures/stepRightUpPreview.dds": "https://files.ballistica.net/cache/ba1/74/f5/224a79689fc3acb022acfc089dc6", + "build/assets/ba_data/textures/stepRightUpPreview.ktx": "https://files.ballistica.net/cache/ba1/c4/e6/bb4f9e75ab1e152e4497d2850077", + "build/assets/ba_data/textures/stepRightUpPreview.pvr": "https://files.ballistica.net/cache/ba1/e2/ab/d2387c89a1620097d9c88c58109c", + "build/assets/ba_data/textures/stepRightUpPreview_preview.png": "https://files.ballistica.net/cache/ba1/ea/77/15350c0802f26b9c36885169f40d", + "build/assets/ba_data/textures/storeCharacter.dds": "https://files.ballistica.net/cache/ba1/2d/36/5046eaacc9956e9c19e1f311368b", + "build/assets/ba_data/textures/storeCharacter.ktx": "https://files.ballistica.net/cache/ba1/6b/6c/401f27e18fdd79e89f75a7537c8a", + "build/assets/ba_data/textures/storeCharacter.pvr": "https://files.ballistica.net/cache/ba1/62/da/1d26ad5ca865032b36854ecdfe42", + "build/assets/ba_data/textures/storeCharacterEaster.dds": "https://files.ballistica.net/cache/ba1/19/26/cbf2cf1669d74262976b0c8ddfc7", + "build/assets/ba_data/textures/storeCharacterEaster.ktx": "https://files.ballistica.net/cache/ba1/2e/40/2368317543b7f8e43bf5e51cafe9", + "build/assets/ba_data/textures/storeCharacterEaster.pvr": "https://files.ballistica.net/cache/ba1/ef/88/f0b7c18bcd0a9e0036a7b503b719", + "build/assets/ba_data/textures/storeCharacterEaster_preview.png": "https://files.ballistica.net/cache/ba1/83/2d/3bfd01c9af19612a80250649b551", + "build/assets/ba_data/textures/storeCharacterXmas.dds": "https://files.ballistica.net/cache/ba1/10/c6/69ac564a70265e81bf26adf3714a", + "build/assets/ba_data/textures/storeCharacterXmas.ktx": "https://files.ballistica.net/cache/ba1/f7/8a/c77aedfca4dd8d97f03129686971", + "build/assets/ba_data/textures/storeCharacterXmas.pvr": "https://files.ballistica.net/cache/ba1/45/a6/fd73f1312184d083646a654a54ff", + "build/assets/ba_data/textures/storeCharacterXmas_preview.png": "https://files.ballistica.net/cache/ba1/81/aa/e19a9999cdce3951c9e84946c45d", + "build/assets/ba_data/textures/storeCharacter_preview.png": "https://files.ballistica.net/cache/ba1/93/22/c6f2a81f92d0122130c28131b0b6", + "build/assets/ba_data/textures/storeIcon.dds": "https://files.ballistica.net/cache/ba1/fd/b5/77d60589f1d68fdaee1fae8ede4f", + "build/assets/ba_data/textures/storeIcon.ktx": "https://files.ballistica.net/cache/ba1/fb/ff/5a633f704b932d357df2259d1ce2", + "build/assets/ba_data/textures/storeIcon.pvr": "https://files.ballistica.net/cache/ba1/43/54/fb34a670bd4d2490df90e8978f54", + "build/assets/ba_data/textures/storeIcon_preview.png": "https://files.ballistica.net/cache/ba1/4f/fd/b8df2631ef7565f4e8c281f0913e", + "build/assets/ba_data/textures/superheroColor.dds": "https://files.ballistica.net/cache/ba1/40/56/66136eb4010b3bbf80385f6dbd0a", + "build/assets/ba_data/textures/superheroColor.ktx": "https://files.ballistica.net/cache/ba1/d0/07/86c1a34fd126d04fabbf5929903c", + "build/assets/ba_data/textures/superheroColor.pvr": "https://files.ballistica.net/cache/ba1/96/6a/4136562b4e59ad63c06acc6e9a76", + "build/assets/ba_data/textures/superheroColorMask.dds": "https://files.ballistica.net/cache/ba1/8f/c4/eb4d2edf43b22eda30b751bcbc91", + "build/assets/ba_data/textures/superheroColorMask.ktx": "https://files.ballistica.net/cache/ba1/b9/a8/50015172ccd6972165fb6dd14f2d", + "build/assets/ba_data/textures/superheroColorMask.pvr": "https://files.ballistica.net/cache/ba1/ef/e1/863fba40e4020d1b22b2648c9d74", + "build/assets/ba_data/textures/superheroColorMask_preview.png": "https://files.ballistica.net/cache/ba1/7d/60/813951c40d892a3ee81fe3feb082", + "build/assets/ba_data/textures/superheroColor_preview.png": "https://files.ballistica.net/cache/ba1/28/89/816997095963d9900a09684c37a3", + "build/assets/ba_data/textures/superheroIcon.dds": "https://files.ballistica.net/cache/ba1/53/b0/59d057da07b3055cc1dba4cccabf", + "build/assets/ba_data/textures/superheroIcon.ktx": "https://files.ballistica.net/cache/ba1/45/45/973b9eea0f2792ba1eb5fc93259f", + "build/assets/ba_data/textures/superheroIcon.pvr": "https://files.ballistica.net/cache/ba1/6b/9b/20e85ed0c4c166a5f40fca060de1", + "build/assets/ba_data/textures/superheroIconColorMask.dds": "https://files.ballistica.net/cache/ba1/5e/1a/80da0bc9363928aa93efef4603eb", + "build/assets/ba_data/textures/superheroIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/39/db/969a765d9c52090230925e3c358f", + "build/assets/ba_data/textures/superheroIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/c6/45/e7b0b9e69ebc29e51f6f87e9b8ab", + "build/assets/ba_data/textures/superheroIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/f8/21/b7875be86db17ab9c495d243dc99", + "build/assets/ba_data/textures/superheroIcon_preview.png": "https://files.ballistica.net/cache/ba1/d3/9c/c4f2110f8a1e0b02bc1f6bbc675a", + "build/assets/ba_data/textures/textClearButton.dds": "https://files.ballistica.net/cache/ba1/0e/3d/509e0e55031325a46cafd0591c0b", + "build/assets/ba_data/textures/textClearButton.ktx": "https://files.ballistica.net/cache/ba1/ff/5a/2e7cce60e4fcfc82f0220c05be05", + "build/assets/ba_data/textures/textClearButton.pvr": "https://files.ballistica.net/cache/ba1/83/5a/6787e2a3e35f876a5d740bb7d144", + "build/assets/ba_data/textures/textClearButton_preview.png": "https://files.ballistica.net/cache/ba1/57/b2/4c14987dc28c897a44c817759813", + "build/assets/ba_data/textures/thePadLevelColor.dds": "https://files.ballistica.net/cache/ba1/7d/43/abfa1e7aa719bed5d90287ca1e97", + "build/assets/ba_data/textures/thePadLevelColor.ktx": "https://files.ballistica.net/cache/ba1/e4/12/03b43b778a9e1e6c8bf32636b828", + "build/assets/ba_data/textures/thePadLevelColor.pvr": "https://files.ballistica.net/cache/ba1/5f/38/c58f3d1aab24862e5816c38768f2", + "build/assets/ba_data/textures/thePadLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/6e/44/4c72408642ebb4575a28254ee423", + "build/assets/ba_data/textures/thePadPreview.dds": "https://files.ballistica.net/cache/ba1/e0/4f/3961a84e193f76cfd259c4cbc828", + "build/assets/ba_data/textures/thePadPreview.ktx": "https://files.ballistica.net/cache/ba1/41/8b/55e3bb862bcc0ab6a277ae9ecdbc", + "build/assets/ba_data/textures/thePadPreview.pvr": "https://files.ballistica.net/cache/ba1/cf/e3/7a45c8733a6d48589239bc7e4751", + "build/assets/ba_data/textures/thePadPreview_preview.png": "https://files.ballistica.net/cache/ba1/59/77/a1fce650a0b74ef5c0419dce08b6", + "build/assets/ba_data/textures/ticketRoll.dds": "https://files.ballistica.net/cache/ba1/1e/8d/78c3794a4cb98dc570bc2c2e7a24", + "build/assets/ba_data/textures/ticketRoll.ktx": "https://files.ballistica.net/cache/ba1/76/d6/7f857dac93b1290306c16e393e8f", + "build/assets/ba_data/textures/ticketRoll.pvr": "https://files.ballistica.net/cache/ba1/82/9a/5b1cc03517edb6c4cc60bca5a170", + "build/assets/ba_data/textures/ticketRollBig.dds": "https://files.ballistica.net/cache/ba1/ec/13/7238172042c28458350a9c971955", + "build/assets/ba_data/textures/ticketRollBig.ktx": "https://files.ballistica.net/cache/ba1/e6/a2/732eae49e25db36294660e327e58", + "build/assets/ba_data/textures/ticketRollBig.pvr": "https://files.ballistica.net/cache/ba1/ae/2c/68f6a7187c3bb1d696c353288a04", + "build/assets/ba_data/textures/ticketRollBig_preview.png": "https://files.ballistica.net/cache/ba1/07/34/9181e04734c562c782051d9dda56", + "build/assets/ba_data/textures/ticketRoll_preview.png": "https://files.ballistica.net/cache/ba1/5a/36/134ad00a84bae33533bc3c4df3e8", + "build/assets/ba_data/textures/ticketRolls.dds": "https://files.ballistica.net/cache/ba1/2c/00/d2e2ab1acde8993b81668f71bb7b", + "build/assets/ba_data/textures/ticketRolls.ktx": "https://files.ballistica.net/cache/ba1/f3/dd/acce2e47eb76f0fb9a0733b66876", + "build/assets/ba_data/textures/ticketRolls.pvr": "https://files.ballistica.net/cache/ba1/4b/af/021333d9dd7f6765df041f448338", + "build/assets/ba_data/textures/ticketRolls_preview.png": "https://files.ballistica.net/cache/ba1/8b/15/78c4b2d615254466466e766e6281", + "build/assets/ba_data/textures/tickets.dds": "https://files.ballistica.net/cache/ba1/8d/2c/15324edc27f4c055394fdca68f0d", + "build/assets/ba_data/textures/tickets.ktx": "https://files.ballistica.net/cache/ba1/a4/f0/649eefc10067d324089cc22f23de", + "build/assets/ba_data/textures/tickets.pvr": "https://files.ballistica.net/cache/ba1/ab/2f/d3e7fa14f6f8b3b680323e5f9e30", + "build/assets/ba_data/textures/ticketsMore.dds": "https://files.ballistica.net/cache/ba1/cc/83/b25fbc327d0aadd1432a7335a83b", + "build/assets/ba_data/textures/ticketsMore.ktx": "https://files.ballistica.net/cache/ba1/b7/3c/e76102ced37d7f126c8839f6b211", + "build/assets/ba_data/textures/ticketsMore.pvr": "https://files.ballistica.net/cache/ba1/26/d8/8fb19218bdc098db5cbb6c8c11a7", + "build/assets/ba_data/textures/ticketsMore_preview.png": "https://files.ballistica.net/cache/ba1/8b/2a/1776719c22d702eeb67d4b36132f", + "build/assets/ba_data/textures/tickets_preview.png": "https://files.ballistica.net/cache/ba1/71/53/a2c2108a6e44bdaa138662575af5", + "build/assets/ba_data/textures/tipTopBGColor.dds": "https://files.ballistica.net/cache/ba1/27/7f/b4feda02f9b060f0ffcc96b7e11d", + "build/assets/ba_data/textures/tipTopBGColor.ktx": "https://files.ballistica.net/cache/ba1/ea/71/98c507dec94e386bd66c44e46071", + "build/assets/ba_data/textures/tipTopBGColor.pvr": "https://files.ballistica.net/cache/ba1/7f/04/6b0b678766cf27efddd2bf9b04c7", + "build/assets/ba_data/textures/tipTopBGColor_preview.png": "https://files.ballistica.net/cache/ba1/5b/82/27ba38842f3c4c4970eb00f17037", + "build/assets/ba_data/textures/tipTopLevelColor.dds": "https://files.ballistica.net/cache/ba1/50/2d/982298d8a59c566ec556b22c93ab", + "build/assets/ba_data/textures/tipTopLevelColor.ktx": "https://files.ballistica.net/cache/ba1/cc/01/79bdde7e9abbb67246b69dd88404", + "build/assets/ba_data/textures/tipTopLevelColor.pvr": "https://files.ballistica.net/cache/ba1/54/86/7a8aac3e2c23a669b1f428189ba0", + "build/assets/ba_data/textures/tipTopLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/cf/f9/a31b35f61d73902dcfdfc935aa78", + "build/assets/ba_data/textures/tipTopPreview.dds": "https://files.ballistica.net/cache/ba1/72/c2/a629a91786c9998f6f410b7c3d66", + "build/assets/ba_data/textures/tipTopPreview.ktx": "https://files.ballistica.net/cache/ba1/e5/7a/557b3725042dacc4605ed9305a24", + "build/assets/ba_data/textures/tipTopPreview.pvr": "https://files.ballistica.net/cache/ba1/76/27/b2c1ce76595a8ec8a8fc34713b93", + "build/assets/ba_data/textures/tipTopPreview_preview.png": "https://files.ballistica.net/cache/ba1/ae/75/d25721c47a4b7563c3f001c87cbb", + "build/assets/ba_data/textures/tnt.dds": "https://files.ballistica.net/cache/ba1/d7/67/e9be081bd8b1b364f09214803c69", + "build/assets/ba_data/textures/tnt.ktx": "https://files.ballistica.net/cache/ba1/c4/f9/303441c3d60f54b7a969b47baa8c", + "build/assets/ba_data/textures/tnt.pvr": "https://files.ballistica.net/cache/ba1/e1/7e/b61413bf9ad2ba457b483705f9da", + "build/assets/ba_data/textures/tnt_preview.png": "https://files.ballistica.net/cache/ba1/53/d3/c59c83474be47f1ba95b632c3633", + "build/assets/ba_data/textures/touchArrows.dds": "https://files.ballistica.net/cache/ba1/96/f2/dee69fbb686e4808cfa2dca10ea1", + "build/assets/ba_data/textures/touchArrows.ktx": "https://files.ballistica.net/cache/ba1/5e/23/76aa254ee232000ce148f7820055", + "build/assets/ba_data/textures/touchArrows.pvr": "https://files.ballistica.net/cache/ba1/1e/d5/9554e7c340dcdaf89dadfaad0501", + "build/assets/ba_data/textures/touchArrowsActions.dds": "https://files.ballistica.net/cache/ba1/53/da/e36ff52f215e2e230f0ab69b5c51", + "build/assets/ba_data/textures/touchArrowsActions.ktx": "https://files.ballistica.net/cache/ba1/98/6e/726a279be3192b6d5fccf7bcf354", + "build/assets/ba_data/textures/touchArrowsActions.pvr": "https://files.ballistica.net/cache/ba1/a3/86/171a55b885b5645adb504468de89", + "build/assets/ba_data/textures/touchArrowsActions_preview.png": "https://files.ballistica.net/cache/ba1/d5/3a/831b6b79a99c113eb635652b8f0f", + "build/assets/ba_data/textures/touchArrows_preview.png": "https://files.ballistica.net/cache/ba1/89/63/52770edc60b4fc195ed10beefe65", + "build/assets/ba_data/textures/towerDLevelColor.dds": "https://files.ballistica.net/cache/ba1/26/cb/4c155cc09641590625da1f0d904f", + "build/assets/ba_data/textures/towerDLevelColor.ktx": "https://files.ballistica.net/cache/ba1/52/a9/a9aaa9fec7c0e512ba96f8bbd5a0", + "build/assets/ba_data/textures/towerDLevelColor.pvr": "https://files.ballistica.net/cache/ba1/a9/48/dfcc3ba427542888dd3f2d655063", + "build/assets/ba_data/textures/towerDLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/0a/ed/191f071d75d68f8a78502153df11", + "build/assets/ba_data/textures/towerDPreview.dds": "https://files.ballistica.net/cache/ba1/95/b4/6d948bc1a86845879c821a645486", + "build/assets/ba_data/textures/towerDPreview.ktx": "https://files.ballistica.net/cache/ba1/aa/9e/dcd2508a3cd1a36cfd4757c62901", + "build/assets/ba_data/textures/towerDPreview.pvr": "https://files.ballistica.net/cache/ba1/9d/73/009927f61e52988f2677199f0977", + "build/assets/ba_data/textures/towerDPreview_preview.png": "https://files.ballistica.net/cache/ba1/e6/96/fd996e82a190c5100c8cfe59cf31", + "build/assets/ba_data/textures/treesColor.dds": "https://files.ballistica.net/cache/ba1/7a/28/6fcc7e65bd6fcde0d472c980f60a", + "build/assets/ba_data/textures/treesColor.ktx": "https://files.ballistica.net/cache/ba1/f3/bf/d21cfeaa7069bd1c835915a3e54a", + "build/assets/ba_data/textures/treesColor.pvr": "https://files.ballistica.net/cache/ba1/04/af/b7d382924721d6853d7705a73147", + "build/assets/ba_data/textures/treesColor_preview.png": "https://files.ballistica.net/cache/ba1/63/f5/344cce011e17762da0bf77c58f86", + "build/assets/ba_data/textures/trophy.dds": "https://files.ballistica.net/cache/ba1/8f/a5/d55001e06d21b4271afdc0863b67", + "build/assets/ba_data/textures/trophy.ktx": "https://files.ballistica.net/cache/ba1/84/e1/161381dd787c99f1403a3a73b5e5", + "build/assets/ba_data/textures/trophy.pvr": "https://files.ballistica.net/cache/ba1/01/7e/d9d6dd4ffe1870949920f59d2d4c", + "build/assets/ba_data/textures/trophy_preview.png": "https://files.ballistica.net/cache/ba1/08/47/ad83d48fa245a9b5150804183bca", + "build/assets/ba_data/textures/tv.dds": "https://files.ballistica.net/cache/ba1/31/e7/de2d8e4c81716da04011984008e1", + "build/assets/ba_data/textures/tv.ktx": "https://files.ballistica.net/cache/ba1/ee/f5/5757971e2ccd4921aa68f11a9d67", + "build/assets/ba_data/textures/tv.pvr": "https://files.ballistica.net/cache/ba1/dd/42/6fe402f8619bfe055385cac8f30d", + "build/assets/ba_data/textures/tv_preview.png": "https://files.ballistica.net/cache/ba1/ba/aa/da2f47203a8e7b9db93da0015f75", + "build/assets/ba_data/textures/uiAtlas.dds": "https://files.ballistica.net/cache/ba1/c4/d2/3baede4524f7279bb6eeb255ddd9", + "build/assets/ba_data/textures/uiAtlas.ktx": "https://files.ballistica.net/cache/ba1/20/38/80bfa0fb6c8a120483b1a9f18ca3", + "build/assets/ba_data/textures/uiAtlas.pvr": "https://files.ballistica.net/cache/ba1/33/92/cb1d47735168f1706cbeba25b28c", + "build/assets/ba_data/textures/uiAtlas2.dds": "https://files.ballistica.net/cache/ba1/72/e8/5a7655ab13e156b4a0a0b831452d", + "build/assets/ba_data/textures/uiAtlas2.ktx": "https://files.ballistica.net/cache/ba1/f6/73/2be359c331fc65fbc166becf3a9f", + "build/assets/ba_data/textures/uiAtlas2.pvr": "https://files.ballistica.net/cache/ba1/36/84/c841437b2514bdb9de21e0dcd164", + "build/assets/ba_data/textures/uiAtlas2_preview.png": "https://files.ballistica.net/cache/ba1/00/67/cfe76fd349a07b34a6ac01512896", + "build/assets/ba_data/textures/uiAtlas_preview.png": "https://files.ballistica.net/cache/ba1/78/5b/9cecd805c151a36c8e9f5606f4fc", + "build/assets/ba_data/textures/upButton.dds": "https://files.ballistica.net/cache/ba1/ef/70/ebbc73f2bf6ba792be72ba1beee3", + "build/assets/ba_data/textures/upButton.ktx": "https://files.ballistica.net/cache/ba1/4a/40/bb945f04a6b2b29b3f784a70cc19", + "build/assets/ba_data/textures/upButton.pvr": "https://files.ballistica.net/cache/ba1/fe/30/79c7fc8a7e425ca93bf653f4af19", + "build/assets/ba_data/textures/upButton_preview.png": "https://files.ballistica.net/cache/ba1/9e/48/034f192b59955a401cb1bfc3ee01", + "build/assets/ba_data/textures/usersButton.dds": "https://files.ballistica.net/cache/ba1/01/7b/1639c70002d993b3757ebdb98f67", + "build/assets/ba_data/textures/usersButton.ktx": "https://files.ballistica.net/cache/ba1/3d/92/8cafa18ea1083f5c4d9cab680d1e", + "build/assets/ba_data/textures/usersButton.pvr": "https://files.ballistica.net/cache/ba1/1a/85/7ae5b9f1e6c881827452a32d8699", + "build/assets/ba_data/textures/usersButton_preview.png": "https://files.ballistica.net/cache/ba1/b8/97/6ee834976707ecfecff5163a3374", + "build/assets/ba_data/textures/vrFillMound.dds": "https://files.ballistica.net/cache/ba1/ff/9f/5c4f5ca8bc4d87b356071c737e53", + "build/assets/ba_data/textures/vrFillMound.ktx": "https://files.ballistica.net/cache/ba1/38/a2/f749a21bede7e87e9a49844053e0", + "build/assets/ba_data/textures/vrFillMound.pvr": "https://files.ballistica.net/cache/ba1/75/9f/4c1a04ceb4b57a17336cde5b43cb", + "build/assets/ba_data/textures/vrFillMound_preview.png": "https://files.ballistica.net/cache/ba1/a0/e7/22e4e439b46216072832431174de", + "build/assets/ba_data/textures/warriorColor.dds": "https://files.ballistica.net/cache/ba1/cb/d0/291b1b78a0485c53340c01e15f0b", + "build/assets/ba_data/textures/warriorColor.ktx": "https://files.ballistica.net/cache/ba1/d7/06/0814282f11c0af322f9604c0db17", + "build/assets/ba_data/textures/warriorColor.pvr": "https://files.ballistica.net/cache/ba1/b2/86/15b1acc1093c5b40f7297b8b5ed7", + "build/assets/ba_data/textures/warriorColorMask.dds": "https://files.ballistica.net/cache/ba1/6c/27/4dbe13726c1c8615f57d28c4052c", + "build/assets/ba_data/textures/warriorColorMask.ktx": "https://files.ballistica.net/cache/ba1/30/b3/a7eca5cb5ea8b83bfaf35f975546", + "build/assets/ba_data/textures/warriorColorMask.pvr": "https://files.ballistica.net/cache/ba1/31/59/194441055f8bd68fe1c38762ca2e", + "build/assets/ba_data/textures/warriorColorMask_preview.png": "https://files.ballistica.net/cache/ba1/a6/74/9288b0b9e8fa87b242f8cf4b085a", + "build/assets/ba_data/textures/warriorColor_preview.png": "https://files.ballistica.net/cache/ba1/05/bc/4df9b082010ced35f32275c6f48e", + "build/assets/ba_data/textures/warriorIcon.dds": "https://files.ballistica.net/cache/ba1/a3/21/2ddb8e4306409a4b54f43d81e3e3", + "build/assets/ba_data/textures/warriorIcon.ktx": "https://files.ballistica.net/cache/ba1/40/62/b8cc35a00c52bbbea8e10a221c9e", + "build/assets/ba_data/textures/warriorIcon.pvr": "https://files.ballistica.net/cache/ba1/54/ba/6cc766962111c64d6705764a2643", + "build/assets/ba_data/textures/warriorIconColorMask.dds": "https://files.ballistica.net/cache/ba1/2f/0d/1ca9d332cd5b8b62b4b8afe62e6e", + "build/assets/ba_data/textures/warriorIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/64/eb/cac158fa53d7d0777c82ca0c3bea", + "build/assets/ba_data/textures/warriorIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/19/f7/8c87516a00f35059001cb24322e3", + "build/assets/ba_data/textures/warriorIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/20/2a/188d5ecba6ad9af19a2118dae668", + "build/assets/ba_data/textures/warriorIcon_preview.png": "https://files.ballistica.net/cache/ba1/a2/79/b5fac05a2bdf3d07c94f22c43475", + "build/assets/ba_data/textures/white.dds": "https://files.ballistica.net/cache/ba1/94/fd/bdb684cfd24dbe93fe76be2069fe", + "build/assets/ba_data/textures/white.ktx": "https://files.ballistica.net/cache/ba1/84/52/3d7ca1cdd84e1a156ef059369231", + "build/assets/ba_data/textures/white.pvr": "https://files.ballistica.net/cache/ba1/4a/28/7f42b31e8686f612465c16e56fa7", + "build/assets/ba_data/textures/white_preview.png": "https://files.ballistica.net/cache/ba1/27/45/bca00432f51c6a0d1b939ad48f0d", + "build/assets/ba_data/textures/windowHSmallVMed.dds": "https://files.ballistica.net/cache/ba1/2b/68/7ee5b1254eb97416d55e6c9bd2f3", + "build/assets/ba_data/textures/windowHSmallVMed.ktx": "https://files.ballistica.net/cache/ba1/c8/64/523de890504eec633f7038d5de23", + "build/assets/ba_data/textures/windowHSmallVMed.pvr": "https://files.ballistica.net/cache/ba1/21/c4/53aea2949583fed4416a9d0f0dd4", + "build/assets/ba_data/textures/windowHSmallVMed_preview.png": "https://files.ballistica.net/cache/ba1/44/53/43f278da1697604dcd78a89ad7ab", + "build/assets/ba_data/textures/windowHSmallVSmall.dds": "https://files.ballistica.net/cache/ba1/f3/06/bc8dd16fbc614dc40a0cd40cfbb0", + "build/assets/ba_data/textures/windowHSmallVSmall.ktx": "https://files.ballistica.net/cache/ba1/84/41/6b95d1e5f51eab240ad74a3e7ae4", + "build/assets/ba_data/textures/windowHSmallVSmall.pvr": "https://files.ballistica.net/cache/ba1/55/22/a732b3292299ef2ebd33c94439bb", + "build/assets/ba_data/textures/windowHSmallVSmall_preview.png": "https://files.ballistica.net/cache/ba1/85/0f/6126f80e08796fbc49c6c75ef4bd", + "build/assets/ba_data/textures/wings.dds": "https://files.ballistica.net/cache/ba1/66/d2/0725df10db1e3e0d9c6734c8029c", + "build/assets/ba_data/textures/wings.ktx": "https://files.ballistica.net/cache/ba1/60/06/74ac032e08f47d72de16a5427f33", + "build/assets/ba_data/textures/wings.pvr": "https://files.ballistica.net/cache/ba1/fd/e3/3f939a18c2006256d3ab826fe18c", + "build/assets/ba_data/textures/wings_preview.png": "https://files.ballistica.net/cache/ba1/b6/de/2a230a097c7ac7e9d93399daa78a", + "build/assets/ba_data/textures/witchColor.dds": "https://files.ballistica.net/cache/ba1/4b/6b/d76a671dbe41c47c55b6ad1e6bc0", + "build/assets/ba_data/textures/witchColor.ktx": "https://files.ballistica.net/cache/ba1/d2/12/960daffffb9fc181f314fcd87dab", + "build/assets/ba_data/textures/witchColor.pvr": "https://files.ballistica.net/cache/ba1/b1/03/5a5764dc78e20205c205766147c3", + "build/assets/ba_data/textures/witchColorMask.dds": "https://files.ballistica.net/cache/ba1/0f/9e/c7f6bf545593255c7d3899905930", + "build/assets/ba_data/textures/witchColorMask.ktx": "https://files.ballistica.net/cache/ba1/db/c8/5ada77b76a9edbb6f28f361191eb", + "build/assets/ba_data/textures/witchColorMask.pvr": "https://files.ballistica.net/cache/ba1/0b/08/21836f134c6a98419bc3ada0e2f4", + "build/assets/ba_data/textures/witchColorMask_preview.png": "https://files.ballistica.net/cache/ba1/eb/7e/5eba03eb820a9afb2c0ef5df97f6", + "build/assets/ba_data/textures/witchColor_preview.png": "https://files.ballistica.net/cache/ba1/6d/24/cc1d8a0d5848d37983a5064f331a", + "build/assets/ba_data/textures/witchIcon.dds": "https://files.ballistica.net/cache/ba1/3e/7d/e9bcece8b75d2c02aa112fadd1f5", + "build/assets/ba_data/textures/witchIcon.ktx": "https://files.ballistica.net/cache/ba1/87/76/f4af3c9b5a2f9711cd401f8b3af3", + "build/assets/ba_data/textures/witchIcon.pvr": "https://files.ballistica.net/cache/ba1/d6/d9/db32d32c3de1f976f60befb7bed4", + "build/assets/ba_data/textures/witchIconColorMask.dds": "https://files.ballistica.net/cache/ba1/9c/86/ea6163a79f66d36f8c883f64e3dd", + "build/assets/ba_data/textures/witchIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/87/a1/ef221c727c2a9d2550b104cca854", + "build/assets/ba_data/textures/witchIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/32/f1/b600f21cb868b856fbdff9a39be7", + "build/assets/ba_data/textures/witchIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/f1/49/89263a7f78333cf3e87817c97015", + "build/assets/ba_data/textures/witchIcon_preview.png": "https://files.ballistica.net/cache/ba1/b3/a4/dda0f283cfe790e63dd27ec51540", + "build/assets/ba_data/textures/wizardColor.dds": "https://files.ballistica.net/cache/ba1/53/32/f55a27d45edb76502223248433fc", + "build/assets/ba_data/textures/wizardColor.ktx": "https://files.ballistica.net/cache/ba1/2e/58/149c43c84eb3979d5941aba4e64a", + "build/assets/ba_data/textures/wizardColor.pvr": "https://files.ballistica.net/cache/ba1/0a/8e/a18f02eabce668882dadfbe4f5be", + "build/assets/ba_data/textures/wizardColorMask.dds": "https://files.ballistica.net/cache/ba1/d6/e6/7e3d950d0983fec473def8e1c93e", + "build/assets/ba_data/textures/wizardColorMask.ktx": "https://files.ballistica.net/cache/ba1/34/4e/ce0a5df69f6f9ff9d37fcfba71f2", + "build/assets/ba_data/textures/wizardColorMask.pvr": "https://files.ballistica.net/cache/ba1/54/89/1ae79d4d2cbb4e5ac7d48f60130b", + "build/assets/ba_data/textures/wizardColorMask_preview.png": "https://files.ballistica.net/cache/ba1/a8/f6/cb036a1ec50ebb960a9f17323c56", + "build/assets/ba_data/textures/wizardColor_preview.png": "https://files.ballistica.net/cache/ba1/4d/a1/668609acc23169f9ba22cccb3191", + "build/assets/ba_data/textures/wizardIcon.dds": "https://files.ballistica.net/cache/ba1/45/33/e5001519fb9af5c6bf10ea07d115", + "build/assets/ba_data/textures/wizardIcon.ktx": "https://files.ballistica.net/cache/ba1/ac/e9/0f776b1908bb6b6f42a23917680b", + "build/assets/ba_data/textures/wizardIcon.pvr": "https://files.ballistica.net/cache/ba1/c4/4a/e7e1f192497392079630efc0cbdd", + "build/assets/ba_data/textures/wizardIconColorMask.dds": "https://files.ballistica.net/cache/ba1/19/05/fb8432f5bc239074cd5e48e0c1e4", + "build/assets/ba_data/textures/wizardIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/97/3b/3b48992448b8e03057694ee7f042", + "build/assets/ba_data/textures/wizardIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/3a/28/e1d6a66ba94949e9f6c1dd350c6b", + "build/assets/ba_data/textures/wizardIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/56/4c/e2782db7a49aa7b53328d71864a6", + "build/assets/ba_data/textures/wizardIcon_preview.png": "https://files.ballistica.net/cache/ba1/9e/80/317b06471afbf3021dfb9bdab0fe", + "build/assets/ba_data/textures/wrestlerColor.dds": "https://files.ballistica.net/cache/ba1/4c/b5/ce330d5e96bdf6b9b41951dbd479", + "build/assets/ba_data/textures/wrestlerColor.ktx": "https://files.ballistica.net/cache/ba1/f7/ce/b5ecf16fb1560c4603a71039fbe1", + "build/assets/ba_data/textures/wrestlerColor.pvr": "https://files.ballistica.net/cache/ba1/63/ca/7d14c5ee91d016d7278c7976e0ee", + "build/assets/ba_data/textures/wrestlerColorMask.dds": "https://files.ballistica.net/cache/ba1/1d/a2/93ad8149cf9a597065ef0e80dd1a", + "build/assets/ba_data/textures/wrestlerColorMask.ktx": "https://files.ballistica.net/cache/ba1/e1/7f/2a9cc52aedec57444e9ed4f8f340", + "build/assets/ba_data/textures/wrestlerColorMask.pvr": "https://files.ballistica.net/cache/ba1/00/dd/4fccfba7224977a79acbe0da969a", + "build/assets/ba_data/textures/wrestlerColorMask_preview.png": "https://files.ballistica.net/cache/ba1/6d/b3/ddf185b936c8979281eb1dc197c4", + "build/assets/ba_data/textures/wrestlerColor_preview.png": "https://files.ballistica.net/cache/ba1/07/d5/86806db2fbc46b2427b7087f0024", + "build/assets/ba_data/textures/wrestlerIcon.dds": "https://files.ballistica.net/cache/ba1/61/19/1c2dc35ef32ce9c52432b599a8dd", + "build/assets/ba_data/textures/wrestlerIcon.ktx": "https://files.ballistica.net/cache/ba1/a2/fa/61af9ffb12153ec883be5d678c49", + "build/assets/ba_data/textures/wrestlerIcon.pvr": "https://files.ballistica.net/cache/ba1/d0/a3/b41bf5639da897df6d8aca7280d8", + "build/assets/ba_data/textures/wrestlerIconColorMask.dds": "https://files.ballistica.net/cache/ba1/03/67/c65f270554066da5a9401f54bb58", + "build/assets/ba_data/textures/wrestlerIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/12/c6/b9a28e5f1b3b5f062825667ac65a", + "build/assets/ba_data/textures/wrestlerIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/94/23/a90f55b7fbcc0d38e59485af48e0", + "build/assets/ba_data/textures/wrestlerIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/34/c0/45b125a3fa7052c5d7b6c9c4abb3", + "build/assets/ba_data/textures/wrestlerIcon_preview.png": "https://files.ballistica.net/cache/ba1/24/c2/bf9df719a3cca488db207cd967a0", + "build/assets/ba_data/textures/zigZagLevelColor.dds": "https://files.ballistica.net/cache/ba1/76/b5/4e86f064cacea2398a8e144cc4f2", + "build/assets/ba_data/textures/zigZagLevelColor.ktx": "https://files.ballistica.net/cache/ba1/a9/40/0118ae1fa53f65b0aa7b1480d9db", + "build/assets/ba_data/textures/zigZagLevelColor.pvr": "https://files.ballistica.net/cache/ba1/96/17/5d7ccb9747c28fb3ab1475966006", + "build/assets/ba_data/textures/zigZagLevelColor_preview.png": "https://files.ballistica.net/cache/ba1/18/16/c35ecedf0ffa59304ad92c7dff8d", + "build/assets/ba_data/textures/zigzagPreview.dds": "https://files.ballistica.net/cache/ba1/7d/20/4f43ce1d45e53d330e2155957c37", + "build/assets/ba_data/textures/zigzagPreview.ktx": "https://files.ballistica.net/cache/ba1/49/49/a15007ba3ea7cd72a2845def3daf", + "build/assets/ba_data/textures/zigzagPreview.pvr": "https://files.ballistica.net/cache/ba1/d8/3c/a19d415a0eb32548026677162cfc", + "build/assets/ba_data/textures/zigzagPreview_preview.png": "https://files.ballistica.net/cache/ba1/82/e6/24bac386287abba7259ac60df5c8", + "build/assets/ba_data/textures/zoeColor.dds": "https://files.ballistica.net/cache/ba1/1c/25/bdac2e93639374c0ad185fc95886", + "build/assets/ba_data/textures/zoeColor.ktx": "https://files.ballistica.net/cache/ba1/cd/c6/fa0e81bf8e6903c8a556c5dcad77", + "build/assets/ba_data/textures/zoeColor.pvr": "https://files.ballistica.net/cache/ba1/50/6f/1085e50229cad30a0ef6053972cf", + "build/assets/ba_data/textures/zoeColorMask.dds": "https://files.ballistica.net/cache/ba1/50/ef/da44679ce4c788ed5cf6e108eff6", + "build/assets/ba_data/textures/zoeColorMask.ktx": "https://files.ballistica.net/cache/ba1/0e/c0/f21678f3f3a8110e470be34435f7", + "build/assets/ba_data/textures/zoeColorMask.pvr": "https://files.ballistica.net/cache/ba1/09/ce/4562d81ece75b5ac86db03e4b93f", + "build/assets/ba_data/textures/zoeColorMask_preview.png": "https://files.ballistica.net/cache/ba1/e2/03/f3c4f191762fe1c43d4c07c7fae5", + "build/assets/ba_data/textures/zoeColor_preview.png": "https://files.ballistica.net/cache/ba1/3a/7f/410e8fc40753cccd12ea8294bc75", + "build/assets/ba_data/textures/zoeIcon.dds": "https://files.ballistica.net/cache/ba1/1a/f2/1d4acd568d69c33145a227eaefa7", + "build/assets/ba_data/textures/zoeIcon.ktx": "https://files.ballistica.net/cache/ba1/37/86/557b7c333f23e1ef95219cc8ba6e", + "build/assets/ba_data/textures/zoeIcon.pvr": "https://files.ballistica.net/cache/ba1/c2/ab/aebf72f80b9cb7c87d25cf9b24e6", + "build/assets/ba_data/textures/zoeIconColorMask.dds": "https://files.ballistica.net/cache/ba1/3e/a2/033fa783d11c3d2a983bef93e703", + "build/assets/ba_data/textures/zoeIconColorMask.ktx": "https://files.ballistica.net/cache/ba1/49/22/418c0f683b30bf69c77d5b7fe79c", + "build/assets/ba_data/textures/zoeIconColorMask.pvr": "https://files.ballistica.net/cache/ba1/0a/8d/df4804e5754b784269a07c1ddf93", + "build/assets/ba_data/textures/zoeIconColorMask_preview.png": "https://files.ballistica.net/cache/ba1/47/fd/459aad49c8af5d1e8bd778b863c3", + "build/assets/ba_data/textures/zoeIcon_preview.png": "https://files.ballistica.net/cache/ba1/ff/85/bc6154691d91a10bd05eb6553988", + "build/assets/mac_disk_image/dmgbuild_settings.py": "https://files.ballistica.net/cache/ba1/52/80/5abc16af08f61367f56fa557b4a7", + "build/assets/pylib-android/__future__.py": "https://files.ballistica.net/cache/ba1/e4/c1/8204c9a658ed7a66e73578e1aec3", + "build/assets/pylib-android/__hello__.py": "https://files.ballistica.net/cache/ba1/22/0b/ed30fa72b96613d06c660d4383d0", + "build/assets/pylib-android/_aix_support.py": "https://files.ballistica.net/cache/ba1/27/f4/57854d91066a4a0035edc1c74a29", + "build/assets/pylib-android/_bootsubprocess.py": "https://files.ballistica.net/cache/ba1/7c/b1/2b42edf5a88de5af4a3e830b8d8f", + "build/assets/pylib-android/_collections_abc.py": "https://files.ballistica.net/cache/ba1/82/e7/0d01fbbeb6b2bd43b972f9b4d103", + "build/assets/pylib-android/_compat_pickle.py": "https://files.ballistica.net/cache/ba1/c7/34/0b970e31c2f1cca6e5f9faa6c132", + "build/assets/pylib-android/_compression.py": "https://files.ballistica.net/cache/ba1/be/28/a9ccf1b2d6349fb6863920412e35", + "build/assets/pylib-android/_markupbase.py": "https://files.ballistica.net/cache/ba1/22/9e/5c473c35ac1b7b0f5ee4ed3ea020", + "build/assets/pylib-android/_osx_support.py": "https://files.ballistica.net/cache/ba1/84/ce/dd78a10efcf4f0b54539c34e980e", + "build/assets/pylib-android/_py_abc.py": "https://files.ballistica.net/cache/ba1/e3/ad/dc1a2ad6db4cdd86de93bcd750f6", + "build/assets/pylib-android/_pydecimal.py": "https://files.ballistica.net/cache/ba1/67/41/0fcb353d5894bd18ede6c8a4256a", + "build/assets/pylib-android/_pyio.py": "https://files.ballistica.net/cache/ba1/11/4e/7f9d7424429b2e785beba09c9389", + "build/assets/pylib-android/_sitebuiltins.py": "https://files.ballistica.net/cache/ba1/44/3f/7be1842543d123b8d15b274d3649", + "build/assets/pylib-android/_strptime.py": "https://files.ballistica.net/cache/ba1/49/1f/7f01498352a4857febe591e3f832", + "build/assets/pylib-android/_sysconfigdata__linux_aarch64-linux-android.py": "https://files.ballistica.net/cache/ba1/27/7f/a35a77a16679c94d6d06ce0ba0e4", + "build/assets/pylib-android/_sysconfigdata__linux_arm-linux-androideabi.py": "https://files.ballistica.net/cache/ba1/b7/7d/502892b5862387914156ba1a7bd8", + "build/assets/pylib-android/_sysconfigdata__linux_i686-linux-android.py": "https://files.ballistica.net/cache/ba1/c1/b3/62641d60f5af3f6ea16e4cce77e7", + "build/assets/pylib-android/_sysconfigdata__linux_x86_64-linux-android.py": "https://files.ballistica.net/cache/ba1/b0/08/c3bbaf3d5ceadbf8ec7a2073df81", + "build/assets/pylib-android/_sysconfigdata_d_linux_aarch64-linux-android.py": "https://files.ballistica.net/cache/ba1/20/02/16d7431980122d50dd604235d66d", + "build/assets/pylib-android/_sysconfigdata_d_linux_arm-linux-androideabi.py": "https://files.ballistica.net/cache/ba1/8d/eb/1e0dbe82e1e113fec42d7dfb9d38", + "build/assets/pylib-android/_sysconfigdata_d_linux_i686-linux-android.py": "https://files.ballistica.net/cache/ba1/48/0a/0997f41083f25b6b9b4cfffbb6bf", + "build/assets/pylib-android/_sysconfigdata_d_linux_x86_64-linux-android.py": "https://files.ballistica.net/cache/ba1/79/38/a968f84c6b5392821c5eacc0435b", + "build/assets/pylib-android/_threading_local.py": "https://files.ballistica.net/cache/ba1/fd/eb/14e56bbec0816a3846feabb8bb0e", + "build/assets/pylib-android/_weakrefset.py": "https://files.ballistica.net/cache/ba1/76/cb/236078c553733f5b4d632fe22efe", + "build/assets/pylib-android/abc.py": "https://files.ballistica.net/cache/ba1/14/96/4280bbc8f6c7dee386ebab59930d", + "build/assets/pylib-android/aifc.py": "https://files.ballistica.net/cache/ba1/a9/49/cbf98060f598ff3309f541e12d89", + "build/assets/pylib-android/antigravity.py": "https://files.ballistica.net/cache/ba1/07/0d/e63763fd59023915560e53a11975", + "build/assets/pylib-android/argparse.py": "https://files.ballistica.net/cache/ba1/b8/ad/bd0f7211433aba2b78cb7353135f", + "build/assets/pylib-android/ast.py": "https://files.ballistica.net/cache/ba1/ab/6b/b5c69c13d82467eb6af8b897c3d9", + "build/assets/pylib-android/asynchat.py": "https://files.ballistica.net/cache/ba1/e5/7d/2001540fbf4de672a05519612bf0", + "build/assets/pylib-android/asyncio/__init__.py": "https://files.ballistica.net/cache/ba1/5b/0b/eedecb3057822d54e0add447d548", + "build/assets/pylib-android/asyncio/__main__.py": "https://files.ballistica.net/cache/ba1/bc/29/c2dfccf4ec11545c8f1e05746536", + "build/assets/pylib-android/asyncio/base_events.py": "https://files.ballistica.net/cache/ba1/cc/99/711f3aa7928f5f1fa370766845f4", + "build/assets/pylib-android/asyncio/base_futures.py": "https://files.ballistica.net/cache/ba1/dd/39/4886f50da2d72a54622001934aef", + "build/assets/pylib-android/asyncio/base_subprocess.py": "https://files.ballistica.net/cache/ba1/e0/52/36ab1b6c82f0405dafc2b424e40f", + "build/assets/pylib-android/asyncio/base_tasks.py": "https://files.ballistica.net/cache/ba1/bd/14/7d545db669eb422cd156af53e4ff", + "build/assets/pylib-android/asyncio/constants.py": "https://files.ballistica.net/cache/ba1/fb/95/ba4f9e8e99499952ba0e8b876484", + "build/assets/pylib-android/asyncio/coroutines.py": "https://files.ballistica.net/cache/ba1/4e/b8/8ccd1c102dab9a9a61bee5a20e3f", + "build/assets/pylib-android/asyncio/events.py": "https://files.ballistica.net/cache/ba1/b3/fc/d819c61f8385f03386fcb6fd9841", + "build/assets/pylib-android/asyncio/exceptions.py": "https://files.ballistica.net/cache/ba1/d2/9b/c08e932b004c66ea08a8bbfd05fc", + "build/assets/pylib-android/asyncio/format_helpers.py": "https://files.ballistica.net/cache/ba1/46/0d/5b2ba14f5d3aae8e026d6be14374", + "build/assets/pylib-android/asyncio/futures.py": "https://files.ballistica.net/cache/ba1/66/5b/6075334f76af87bf03bdf67afc05", + "build/assets/pylib-android/asyncio/locks.py": "https://files.ballistica.net/cache/ba1/94/60/2b359df88e479e063f5879e6f289", + "build/assets/pylib-android/asyncio/log.py": "https://files.ballistica.net/cache/ba1/04/2f/2be12fc22426b458cd898e70b3ae", + "build/assets/pylib-android/asyncio/mixins.py": "https://files.ballistica.net/cache/ba1/52/80/bbfce19ed99e20bddd086a07a5e6", + "build/assets/pylib-android/asyncio/proactor_events.py": "https://files.ballistica.net/cache/ba1/ad/19/43173218d53a1afcc05f20d1897e", + "build/assets/pylib-android/asyncio/protocols.py": "https://files.ballistica.net/cache/ba1/e9/d3/c06de2cf3539236af7d774828368", + "build/assets/pylib-android/asyncio/queues.py": "https://files.ballistica.net/cache/ba1/9b/ff/9654727f9cdbb8cb750cf8599822", + "build/assets/pylib-android/asyncio/runners.py": "https://files.ballistica.net/cache/ba1/e0/18/d2901d4eafea1d1d184ab801e326", + "build/assets/pylib-android/asyncio/selector_events.py": "https://files.ballistica.net/cache/ba1/e7/13/4de61e244b833c3f2fc16ac12a4b", + "build/assets/pylib-android/asyncio/sslproto.py": "https://files.ballistica.net/cache/ba1/43/38/6a5917e4bae773357adf4271705a", + "build/assets/pylib-android/asyncio/staggered.py": "https://files.ballistica.net/cache/ba1/a5/78/f03914202593ad187dfd71798ef6", + "build/assets/pylib-android/asyncio/streams.py": "https://files.ballistica.net/cache/ba1/93/eb/cc1ba4a5652812834ccc8ac364c7", + "build/assets/pylib-android/asyncio/subprocess.py": "https://files.ballistica.net/cache/ba1/99/43/92c57000e0df74dd9db6867eb4ca", + "build/assets/pylib-android/asyncio/taskgroups.py": "https://files.ballistica.net/cache/ba1/99/23/2849fa35da58b3cc52cb6bcb4736", + "build/assets/pylib-android/asyncio/tasks.py": "https://files.ballistica.net/cache/ba1/af/c3/8b588cd6543c22c594ec1039070f", + "build/assets/pylib-android/asyncio/threads.py": "https://files.ballistica.net/cache/ba1/87/38/383a3389e151ad087870efb99211", + "build/assets/pylib-android/asyncio/timeouts.py": "https://files.ballistica.net/cache/ba1/02/1e/68c679179870df894499743f934b", + "build/assets/pylib-android/asyncio/transports.py": "https://files.ballistica.net/cache/ba1/39/a9/c8c7bc9c7ac80733af0fa240f594", + "build/assets/pylib-android/asyncio/trsock.py": "https://files.ballistica.net/cache/ba1/20/d5/1e33d2e394fb0861bc996a631cae", + "build/assets/pylib-android/asyncio/unix_events.py": "https://files.ballistica.net/cache/ba1/e7/1a/8b408a13b4a4f493fc626382c4ea", + "build/assets/pylib-android/asyncio/windows_events.py": "https://files.ballistica.net/cache/ba1/4d/60/ef7d918fad7de1398a3614d93703", + "build/assets/pylib-android/asyncio/windows_utils.py": "https://files.ballistica.net/cache/ba1/45/76/58cda709d839fd485decc1fcbb94", + "build/assets/pylib-android/asyncore.py": "https://files.ballistica.net/cache/ba1/6c/5e/72f045c5480e4dbeb3157e25cb28", + "build/assets/pylib-android/base64.py": "https://files.ballistica.net/cache/ba1/5a/52/a00e4013833a4c7a8d3ff86fe410", + "build/assets/pylib-android/bdb.py": "https://files.ballistica.net/cache/ba1/23/8a/73a32635135073d78ac22264ee65", + "build/assets/pylib-android/bisect.py": "https://files.ballistica.net/cache/ba1/07/2a/3ac9abb24e89003a9f9da08ad772", + "build/assets/pylib-android/bz2.py": "https://files.ballistica.net/cache/ba1/7c/c8/d02015e48a44a1cf7f1250a2965f", + "build/assets/pylib-android/cProfile.py": "https://files.ballistica.net/cache/ba1/71/fc/eb5b22a69841d606bf95086bad5e", + "build/assets/pylib-android/calendar.py": "https://files.ballistica.net/cache/ba1/f6/a4/730a6b9451ef1f70cc2caa5bc3bb", + "build/assets/pylib-android/cgi.py": "https://files.ballistica.net/cache/ba1/df/31/7b5112ec3a0e03e7264342bf01b9", + "build/assets/pylib-android/cgitb.py": "https://files.ballistica.net/cache/ba1/1a/4c/5755dc0a2efc87327c26f2bd3b2d", + "build/assets/pylib-android/chunk.py": "https://files.ballistica.net/cache/ba1/f6/a6/beb25bd2e55c53feebf5fe4179e2", + "build/assets/pylib-android/cmd.py": "https://files.ballistica.net/cache/ba1/18/93/29c3662754a676381523a47a2fab", + "build/assets/pylib-android/code.py": "https://files.ballistica.net/cache/ba1/32/0d/593177f2c8a3c6c736406cf638ac", + "build/assets/pylib-android/codecs.py": "https://files.ballistica.net/cache/ba1/eb/d6/3906ebb0476c9387f3c74d638ce8", + "build/assets/pylib-android/codeop.py": "https://files.ballistica.net/cache/ba1/81/e8/8c7a8aa7ad5873f6e74a4f67d0df", + "build/assets/pylib-android/collections/__init__.py": "https://files.ballistica.net/cache/ba1/99/68/47af9ffee79634ca235a75cd556e", + "build/assets/pylib-android/collections/abc.py": "https://files.ballistica.net/cache/ba1/59/d8/90d436b81cb5fda187cc75d3eb0c", + "build/assets/pylib-android/colorsys.py": "https://files.ballistica.net/cache/ba1/de/d7/d53f3575485a6a70b7b936fba373", + "build/assets/pylib-android/compileall.py": "https://files.ballistica.net/cache/ba1/37/d9/c05b0f6557b4db08ac5b69771221", + "build/assets/pylib-android/concurrent/__init__.py": "https://files.ballistica.net/cache/ba1/b7/a0/2383c2489329efedfe1ab87c7d2a", + "build/assets/pylib-android/concurrent/futures/__init__.py": "https://files.ballistica.net/cache/ba1/49/3f/c139f8c80fa7e21b906c25c8b0a2", + "build/assets/pylib-android/concurrent/futures/_base.py": "https://files.ballistica.net/cache/ba1/b8/68/ad3bb5eaff8e56d71adc5a073ce8", + "build/assets/pylib-android/concurrent/futures/process.py": "https://files.ballistica.net/cache/ba1/52/b1/bd0a3a4bce96b8a5c3f2b3f314ef", + "build/assets/pylib-android/concurrent/futures/thread.py": "https://files.ballistica.net/cache/ba1/0d/d7/5526b756b835c6bae332c73a5208", + "build/assets/pylib-android/configparser.py": "https://files.ballistica.net/cache/ba1/3c/76/3fa755f7b7b4b1354a54d9ed0261", + "build/assets/pylib-android/contextlib.py": "https://files.ballistica.net/cache/ba1/b4/b2/f229d3c09e470da8d5acc135aaab", + "build/assets/pylib-android/contextvars.py": "https://files.ballistica.net/cache/ba1/4b/cc/ee67fc51b0691c036962b941e7e8", + "build/assets/pylib-android/copy.py": "https://files.ballistica.net/cache/ba1/39/55/c392475868507b097e0d71547b3f", + "build/assets/pylib-android/copyreg.py": "https://files.ballistica.net/cache/ba1/3b/3d/3b54f8d5ca1adbd928479b177c85", + "build/assets/pylib-android/crypt.py": "https://files.ballistica.net/cache/ba1/5f/bc/06dac11faf4f6cebae4a0ec29df9", + "build/assets/pylib-android/csv.py": "https://files.ballistica.net/cache/ba1/dd/af/26ed466f7b00e1665f0b456c7b31", + "build/assets/pylib-android/ctypes/__init__.py": "https://files.ballistica.net/cache/ba1/f8/b2/ae09611e462ace390ace198cf952", + "build/assets/pylib-android/ctypes/_aix.py": "https://files.ballistica.net/cache/ba1/74/06/ee924338327662178d1a674fab05", + "build/assets/pylib-android/ctypes/_endian.py": "https://files.ballistica.net/cache/ba1/8a/02/f79ee98228241f3c7a499c0c2054", + "build/assets/pylib-android/ctypes/macholib/__init__.py": "https://files.ballistica.net/cache/ba1/df/d1/262902242f6ac014d53c742dfc61", + "build/assets/pylib-android/ctypes/macholib/dyld.py": "https://files.ballistica.net/cache/ba1/1a/49/680c919f636b6c8b4c4bfc95152e", + "build/assets/pylib-android/ctypes/macholib/dylib.py": "https://files.ballistica.net/cache/ba1/16/42/2f2a19b5837529c0a362eb85f07a", + "build/assets/pylib-android/ctypes/macholib/framework.py": "https://files.ballistica.net/cache/ba1/55/bc/7c9c8a6be6b51ce0c2f28d477af4", + "build/assets/pylib-android/ctypes/util.py": "https://files.ballistica.net/cache/ba1/c1/3e/12615ed0f4a029bc44d5742f1fc4", + "build/assets/pylib-android/ctypes/wintypes.py": "https://files.ballistica.net/cache/ba1/8d/79/52bb4fe8904dafd4611a2ff883eb", + "build/assets/pylib-android/curses/__init__.py": "https://files.ballistica.net/cache/ba1/de/8b/a6552285f6ddfb31b87cbf57bbce", + "build/assets/pylib-android/curses/ascii.py": "https://files.ballistica.net/cache/ba1/a1/ba/73d00b55f782db49b0912206f298", + "build/assets/pylib-android/curses/has_key.py": "https://files.ballistica.net/cache/ba1/4e/dc/1bf5497a7bb4ceb828d6ecf53d21", + "build/assets/pylib-android/curses/panel.py": "https://files.ballistica.net/cache/ba1/cc/b9/5cc708922caca56354569966d6e2", + "build/assets/pylib-android/curses/textpad.py": "https://files.ballistica.net/cache/ba1/a7/9c/e3d7d752e5796bed64144f142d61", + "build/assets/pylib-android/dataclasses.py": "https://files.ballistica.net/cache/ba1/d1/c6/e98351580e40c3004953927e1121", + "build/assets/pylib-android/datetime.py": "https://files.ballistica.net/cache/ba1/bb/7d/ae538850412171e7ff941ff58875", + "build/assets/pylib-android/decimal.py": "https://files.ballistica.net/cache/ba1/d1/ba/13fa9977bb4d079e442f8955d263", + "build/assets/pylib-android/difflib.py": "https://files.ballistica.net/cache/ba1/93/83/4990ceb26dec9593f5a3b775feac", + "build/assets/pylib-android/dis.py": "https://files.ballistica.net/cache/ba1/be/db/11983c5d58d2d3b28ed72fc3cee7", + "build/assets/pylib-android/doctest.py": "https://files.ballistica.net/cache/ba1/05/ca/760f1e3f0801d97cb521e6f904e3", + "build/assets/pylib-android/email/__init__.py": "https://files.ballistica.net/cache/ba1/34/74/90966c40e3a3f49559621c629840", + "build/assets/pylib-android/email/_encoded_words.py": "https://files.ballistica.net/cache/ba1/96/d0/47819c2975167c0031fab901cbbf", + "build/assets/pylib-android/email/_header_value_parser.py": "https://files.ballistica.net/cache/ba1/92/02/6857e47732bbf278dc2c66be92d2", + "build/assets/pylib-android/email/_parseaddr.py": "https://files.ballistica.net/cache/ba1/9c/cb/85f2cbe5130b00fa95a2bf4537de", + "build/assets/pylib-android/email/_policybase.py": "https://files.ballistica.net/cache/ba1/67/45/64ef42d536b468e5e791b65d918d", + "build/assets/pylib-android/email/base64mime.py": "https://files.ballistica.net/cache/ba1/ed/70/f0e5612e3790075209de41847ef1", + "build/assets/pylib-android/email/charset.py": "https://files.ballistica.net/cache/ba1/52/63/e188c29c9e7c0ae42f1e9169e2ae", + "build/assets/pylib-android/email/contentmanager.py": "https://files.ballistica.net/cache/ba1/d8/d4/c3fd4b648b3e3ee0efaf0aa70b3e", + "build/assets/pylib-android/email/encoders.py": "https://files.ballistica.net/cache/ba1/e1/e7/840c66a3384a17153084325a17f6", + "build/assets/pylib-android/email/errors.py": "https://files.ballistica.net/cache/ba1/6d/95/3614fb2a595565f2370ede6df55b", + "build/assets/pylib-android/email/feedparser.py": "https://files.ballistica.net/cache/ba1/9b/86/939b157119501de21c78cc065e83", + "build/assets/pylib-android/email/generator.py": "https://files.ballistica.net/cache/ba1/12/ec/f50dde06bdade7153ac78e1e4aea", + "build/assets/pylib-android/email/header.py": "https://files.ballistica.net/cache/ba1/dc/0a/ae17e0627a1f83c7138418e57d0c", + "build/assets/pylib-android/email/headerregistry.py": "https://files.ballistica.net/cache/ba1/9f/a8/1ca733696ba7f627cd6423af36de", + "build/assets/pylib-android/email/iterators.py": "https://files.ballistica.net/cache/ba1/b8/ae/07f1c6ac5beb02cc2e45a501f790", + "build/assets/pylib-android/email/message.py": "https://files.ballistica.net/cache/ba1/22/90/fcfeb386f851100025af666c1b02", + "build/assets/pylib-android/email/mime/__init__.py": "https://files.ballistica.net/cache/ba1/5c/ba/dda4003487ea0108ba746ba0c746", + "build/assets/pylib-android/email/mime/application.py": "https://files.ballistica.net/cache/ba1/70/69/0798dae8781bc31d8d2a38534594", + "build/assets/pylib-android/email/mime/audio.py": "https://files.ballistica.net/cache/ba1/e4/83/371585a0917ed3579e1b028392a3", + "build/assets/pylib-android/email/mime/base.py": "https://files.ballistica.net/cache/ba1/64/ed/801678370b80151bd92a2d501c0f", + "build/assets/pylib-android/email/mime/image.py": "https://files.ballistica.net/cache/ba1/84/15/0c0126b836df1bd39ce8e4e95d90", + "build/assets/pylib-android/email/mime/message.py": "https://files.ballistica.net/cache/ba1/e0/fe/0714de4eabe65851c72e0c4459d1", + "build/assets/pylib-android/email/mime/multipart.py": "https://files.ballistica.net/cache/ba1/7a/87/98d99a8c104615fd4f958b9033e5", + "build/assets/pylib-android/email/mime/nonmultipart.py": "https://files.ballistica.net/cache/ba1/5c/41/9af530140459c379072ff2e56681", + "build/assets/pylib-android/email/mime/text.py": "https://files.ballistica.net/cache/ba1/66/4f/3c050fee345b053b3649053a93a3", + "build/assets/pylib-android/email/parser.py": "https://files.ballistica.net/cache/ba1/ef/5e/2d5460305454d41dfb24c0dbea51", + "build/assets/pylib-android/email/policy.py": "https://files.ballistica.net/cache/ba1/db/1e/883d7a85d371d4e7236e59792465", + "build/assets/pylib-android/email/quoprimime.py": "https://files.ballistica.net/cache/ba1/e9/16/dd86e23437291a0a7e353f11b577", + "build/assets/pylib-android/email/utils.py": "https://files.ballistica.net/cache/ba1/1d/da/d8446d2cf02bc94fa863d06aa4e8", + "build/assets/pylib-android/encodings/__init__.py": "https://files.ballistica.net/cache/ba1/aa/c4/e6833090545036b7dc0893b5f945", + "build/assets/pylib-android/encodings/aliases.py": "https://files.ballistica.net/cache/ba1/53/ea/7712e64ad2d759fe7111447061f8", + "build/assets/pylib-android/encodings/ascii.py": "https://files.ballistica.net/cache/ba1/c0/02/ccfe85017b6b3d2bd696cad8803f", + "build/assets/pylib-android/encodings/base64_codec.py": "https://files.ballistica.net/cache/ba1/39/0f/0c65e3db96c2e50e25b61755521b", + "build/assets/pylib-android/encodings/big5.py": "https://files.ballistica.net/cache/ba1/59/59/26f03c4a96d58f1e96deaf665126", + "build/assets/pylib-android/encodings/big5hkscs.py": "https://files.ballistica.net/cache/ba1/22/4d/5c97dfef1552c7cfbf85a7ea4c37", + "build/assets/pylib-android/encodings/bz2_codec.py": "https://files.ballistica.net/cache/ba1/a2/f4/9193fc92a019142822dfbd30aab0", + "build/assets/pylib-android/encodings/charmap.py": "https://files.ballistica.net/cache/ba1/55/ea/6ce417fc2359057fba31dddd73db", + "build/assets/pylib-android/encodings/cp037.py": "https://files.ballistica.net/cache/ba1/03/67/54ef0ecd89312c58187224bf71e5", + "build/assets/pylib-android/encodings/cp1006.py": "https://files.ballistica.net/cache/ba1/7c/aa/bd9d9e3d0839e0c1483ef4d9a566", + "build/assets/pylib-android/encodings/cp1026.py": "https://files.ballistica.net/cache/ba1/c9/0c/882cad803b883972a79bd4df24f4", + "build/assets/pylib-android/encodings/cp1125.py": "https://files.ballistica.net/cache/ba1/93/cb/98f4691912e92263da6d89ad1717", + "build/assets/pylib-android/encodings/cp1140.py": "https://files.ballistica.net/cache/ba1/58/31/a30bf81dff7b0fb61534709a4557", + "build/assets/pylib-android/encodings/cp1250.py": "https://files.ballistica.net/cache/ba1/17/cf/eae1f772fe150e1b29c30264a61b", + "build/assets/pylib-android/encodings/cp1251.py": "https://files.ballistica.net/cache/ba1/b2/73/53ecfaee7653d6627205084f8fb4", + "build/assets/pylib-android/encodings/cp1252.py": "https://files.ballistica.net/cache/ba1/2d/7b/7dbfc5624a221a5516e860854488", + "build/assets/pylib-android/encodings/cp1253.py": "https://files.ballistica.net/cache/ba1/9f/13/296bcd130a2423718a03f7f55ac0", + "build/assets/pylib-android/encodings/cp1254.py": "https://files.ballistica.net/cache/ba1/72/cd/8029073351b6cc99674696453c3a", + "build/assets/pylib-android/encodings/cp1255.py": "https://files.ballistica.net/cache/ba1/f3/b4/b0c956d383a3d044c1c24949bdfc", + "build/assets/pylib-android/encodings/cp1256.py": "https://files.ballistica.net/cache/ba1/a9/7c/f46990082b2611dac6188210c643", + "build/assets/pylib-android/encodings/cp1257.py": "https://files.ballistica.net/cache/ba1/72/03/c3206e0fee6c3715cdb52a97e254", + "build/assets/pylib-android/encodings/cp1258.py": "https://files.ballistica.net/cache/ba1/43/74/3981801ae5da2f22f9d9a0e929d4", + "build/assets/pylib-android/encodings/cp273.py": "https://files.ballistica.net/cache/ba1/17/b8/7c2401621609b7b1845da23d5b12", + "build/assets/pylib-android/encodings/cp424.py": "https://files.ballistica.net/cache/ba1/d5/fc/a26d0b74bd09567d82e965aa076d", + "build/assets/pylib-android/encodings/cp437.py": "https://files.ballistica.net/cache/ba1/ea/f9/5d495b3701fc3a7dc1d518646945", + "build/assets/pylib-android/encodings/cp500.py": "https://files.ballistica.net/cache/ba1/fc/8c/9ee8617fd50751366b905a6b5eb3", + "build/assets/pylib-android/encodings/cp720.py": "https://files.ballistica.net/cache/ba1/51/49/8a5754fe1310564a434227b2c8a5", + "build/assets/pylib-android/encodings/cp737.py": "https://files.ballistica.net/cache/ba1/16/d4/ae5e794924ffc81f5dc65a502662", + "build/assets/pylib-android/encodings/cp775.py": "https://files.ballistica.net/cache/ba1/14/d8/6e94e42c8aab17553d9945f657cf", + "build/assets/pylib-android/encodings/cp850.py": "https://files.ballistica.net/cache/ba1/04/a4/7f097ce9a5909fc6e67b67d2dde9", + "build/assets/pylib-android/encodings/cp852.py": "https://files.ballistica.net/cache/ba1/1c/59/80573e9dc2ad0498f4b7c80b727d", + "build/assets/pylib-android/encodings/cp855.py": "https://files.ballistica.net/cache/ba1/7c/f7/2b6631c115ca27ae2675366b4a6c", + "build/assets/pylib-android/encodings/cp856.py": "https://files.ballistica.net/cache/ba1/8e/b2/09645c7eacaa144d747a012b8b8b", + "build/assets/pylib-android/encodings/cp857.py": "https://files.ballistica.net/cache/ba1/53/46/a703b2d4a531f23fa84da9846470", + "build/assets/pylib-android/encodings/cp858.py": "https://files.ballistica.net/cache/ba1/0a/a2/3859912e1e6f37472ace75914558", + "build/assets/pylib-android/encodings/cp860.py": "https://files.ballistica.net/cache/ba1/19/98/912d90ea80163dee2e7a5827f3b3", + "build/assets/pylib-android/encodings/cp861.py": "https://files.ballistica.net/cache/ba1/54/5d/bb920712b2c7eb1106ec1a532694", + "build/assets/pylib-android/encodings/cp862.py": "https://files.ballistica.net/cache/ba1/a4/2b/43d590358199c33ff9e7dc686aa2", + "build/assets/pylib-android/encodings/cp863.py": "https://files.ballistica.net/cache/ba1/d5/f3/5645ad987d8e79a833d51f871643", + "build/assets/pylib-android/encodings/cp864.py": "https://files.ballistica.net/cache/ba1/a4/12/8893c8f89669495bcd10670d2ece", + "build/assets/pylib-android/encodings/cp865.py": "https://files.ballistica.net/cache/ba1/2d/ea/46cb176b44dded00b942dee0c1a2", + "build/assets/pylib-android/encodings/cp866.py": "https://files.ballistica.net/cache/ba1/72/04/a9ea54ea7e965f382ac8c2a6af5e", + "build/assets/pylib-android/encodings/cp869.py": "https://files.ballistica.net/cache/ba1/bc/1c/7d4f35949b518618d592be1dc6ed", + "build/assets/pylib-android/encodings/cp874.py": "https://files.ballistica.net/cache/ba1/c5/13/96daf62fb6e095ab81f19d59ebdd", + "build/assets/pylib-android/encodings/cp875.py": "https://files.ballistica.net/cache/ba1/d4/2b/7afff0060476b063d13bce0532c6", + "build/assets/pylib-android/encodings/cp932.py": "https://files.ballistica.net/cache/ba1/1a/d5/4213ed599528d53fc3b1499122e1", + "build/assets/pylib-android/encodings/cp949.py": "https://files.ballistica.net/cache/ba1/49/63/6f67fb0e350bd0eb282c7e3e8d2d", + "build/assets/pylib-android/encodings/cp950.py": "https://files.ballistica.net/cache/ba1/a7/dd/9056612f3ac3804c622619c05a79", + "build/assets/pylib-android/encodings/euc_jis_2004.py": "https://files.ballistica.net/cache/ba1/5c/bb/e1007a29a6f1b98200a2c2f71e32", + "build/assets/pylib-android/encodings/euc_jisx0213.py": "https://files.ballistica.net/cache/ba1/79/b0/446bb8d2603825b13e121fc59491", + "build/assets/pylib-android/encodings/euc_jp.py": "https://files.ballistica.net/cache/ba1/d1/a4/f6002bb5e61db8f37273bd4937ef", + "build/assets/pylib-android/encodings/euc_kr.py": "https://files.ballistica.net/cache/ba1/00/cc/9c90fb513d557c40a83e4c124951", + "build/assets/pylib-android/encodings/gb18030.py": "https://files.ballistica.net/cache/ba1/78/cd/78fa536a7e812fa063c31fc3a8ad", + "build/assets/pylib-android/encodings/gb2312.py": "https://files.ballistica.net/cache/ba1/85/da/e5f6af6fd5a54b3a7e66bf433971", + "build/assets/pylib-android/encodings/gbk.py": "https://files.ballistica.net/cache/ba1/fe/8a/c4065bc1fac235b271b48c1e73a7", + "build/assets/pylib-android/encodings/hex_codec.py": "https://files.ballistica.net/cache/ba1/b9/dc/c4b1faa6bb777c08674d488c322f", + "build/assets/pylib-android/encodings/hp_roman8.py": "https://files.ballistica.net/cache/ba1/57/3f/ed2abf1396bd3c84066d3873c9de", + "build/assets/pylib-android/encodings/hz.py": "https://files.ballistica.net/cache/ba1/08/f4/25701af21562d5e8f6f7b43aa330", + "build/assets/pylib-android/encodings/idna.py": "https://files.ballistica.net/cache/ba1/2e/a1/b959b98354a98d237fd59410d389", + "build/assets/pylib-android/encodings/iso2022_jp.py": "https://files.ballistica.net/cache/ba1/ca/40/b881ae7331b74358cea7c4c54d17", + "build/assets/pylib-android/encodings/iso2022_jp_1.py": "https://files.ballistica.net/cache/ba1/43/54/25c35cce54c878073fe8dddf3b58", + "build/assets/pylib-android/encodings/iso2022_jp_2.py": "https://files.ballistica.net/cache/ba1/a3/0b/229f12bf2513e5868da27c0bcd21", + "build/assets/pylib-android/encodings/iso2022_jp_2004.py": "https://files.ballistica.net/cache/ba1/3d/a2/aab10ccd8df5635bbf298784a294", + "build/assets/pylib-android/encodings/iso2022_jp_3.py": "https://files.ballistica.net/cache/ba1/a5/8e/de160e60f329ee02a9d79a2177c3", + "build/assets/pylib-android/encodings/iso2022_jp_ext.py": "https://files.ballistica.net/cache/ba1/fe/30/04ddff2740fd71496dbc0b66979f", + "build/assets/pylib-android/encodings/iso2022_kr.py": "https://files.ballistica.net/cache/ba1/26/83/ec97e87874c05fc1f0c0cb00a09d", + "build/assets/pylib-android/encodings/iso8859_1.py": "https://files.ballistica.net/cache/ba1/4e/4d/e2e4ef5282252fbe6f69d6db1bc6", + "build/assets/pylib-android/encodings/iso8859_10.py": "https://files.ballistica.net/cache/ba1/77/df/ff8f74466bdeb00b392a3a6788be", + "build/assets/pylib-android/encodings/iso8859_11.py": "https://files.ballistica.net/cache/ba1/8a/57/9bf9b94f6af5df178f460851d044", + "build/assets/pylib-android/encodings/iso8859_13.py": "https://files.ballistica.net/cache/ba1/06/ac/2d74cf87021a0c9e30c33bdeaebb", + "build/assets/pylib-android/encodings/iso8859_14.py": "https://files.ballistica.net/cache/ba1/03/7c/5f12280532f09c0d6d8c82fc3cf3", + "build/assets/pylib-android/encodings/iso8859_15.py": "https://files.ballistica.net/cache/ba1/d5/16/d68605df7f73e18acf0caba448e8", + "build/assets/pylib-android/encodings/iso8859_16.py": "https://files.ballistica.net/cache/ba1/e9/34/7ab8d930518065b6edce728f4237", + "build/assets/pylib-android/encodings/iso8859_2.py": "https://files.ballistica.net/cache/ba1/78/e0/fc3a795819cbc7a090549273e0f5", + "build/assets/pylib-android/encodings/iso8859_3.py": "https://files.ballistica.net/cache/ba1/4a/52/8ca198b60adeee1add8b083859f2", + "build/assets/pylib-android/encodings/iso8859_4.py": "https://files.ballistica.net/cache/ba1/f6/99/85db9de14b17ee10daabb398bdc8", + "build/assets/pylib-android/encodings/iso8859_5.py": "https://files.ballistica.net/cache/ba1/1a/a4/d8bc9c338dbe86a8a720333215d5", + "build/assets/pylib-android/encodings/iso8859_6.py": "https://files.ballistica.net/cache/ba1/a3/0f/7a379b7cea7bd4dcb18b6f4fb26c", + "build/assets/pylib-android/encodings/iso8859_7.py": "https://files.ballistica.net/cache/ba1/8c/3a/b456b4db46c734fc98650f594555", + "build/assets/pylib-android/encodings/iso8859_8.py": "https://files.ballistica.net/cache/ba1/49/3c/33d8832207485bfc7c9cad74f85d", + "build/assets/pylib-android/encodings/iso8859_9.py": "https://files.ballistica.net/cache/ba1/ee/d3/34b7dd6fb481b835085b088c0f19", + "build/assets/pylib-android/encodings/johab.py": "https://files.ballistica.net/cache/ba1/49/84/d693186864e88eec1caa1889037a", + "build/assets/pylib-android/encodings/koi8_r.py": "https://files.ballistica.net/cache/ba1/46/74/734033d0457db92b930982fc6bf6", + "build/assets/pylib-android/encodings/koi8_t.py": "https://files.ballistica.net/cache/ba1/ef/4c/3e58a795c36b757f631ee11fef36", + "build/assets/pylib-android/encodings/koi8_u.py": "https://files.ballistica.net/cache/ba1/35/c7/ad39c6aaef56f7e83f9bf36a3bf3", + "build/assets/pylib-android/encodings/kz1048.py": "https://files.ballistica.net/cache/ba1/dd/d0/7b309d9d50da1889746bf92a4838", + "build/assets/pylib-android/encodings/latin_1.py": "https://files.ballistica.net/cache/ba1/e8/17/e1a1d5ea15d47117ee03c6cdae20", + "build/assets/pylib-android/encodings/mac_arabic.py": "https://files.ballistica.net/cache/ba1/1d/25/aa3a26b9b7301c1b2ae6665321de", + "build/assets/pylib-android/encodings/mac_croatian.py": "https://files.ballistica.net/cache/ba1/ad/08/ef4de44543e0da393775cf747456", + "build/assets/pylib-android/encodings/mac_cyrillic.py": "https://files.ballistica.net/cache/ba1/6f/17/22b0912e0fd60f6e69fecb95f754", + "build/assets/pylib-android/encodings/mac_farsi.py": "https://files.ballistica.net/cache/ba1/81/3e/e1234e1e4ee9c7003ea20964db08", + "build/assets/pylib-android/encodings/mac_greek.py": "https://files.ballistica.net/cache/ba1/42/eb/e9025babaae75c5836c5ac619088", + "build/assets/pylib-android/encodings/mac_iceland.py": "https://files.ballistica.net/cache/ba1/dd/29/0e23f3868827ce0d23ea1d003f4a", + "build/assets/pylib-android/encodings/mac_latin2.py": "https://files.ballistica.net/cache/ba1/b8/3e/25116270cb6c6e186b5deeceaa01", + "build/assets/pylib-android/encodings/mac_roman.py": "https://files.ballistica.net/cache/ba1/4e/ed/bb78db4b91209312f8b23dfc560d", + "build/assets/pylib-android/encodings/mac_romanian.py": "https://files.ballistica.net/cache/ba1/88/bb/7adabc10164104518321b5cedbf4", + "build/assets/pylib-android/encodings/mac_turkish.py": "https://files.ballistica.net/cache/ba1/17/82/7b9cc5635ce63f7605e87b8a684a", + "build/assets/pylib-android/encodings/mbcs.py": "https://files.ballistica.net/cache/ba1/bc/4b/3b87a50ba093b3b6f4dfacfb52f0", + "build/assets/pylib-android/encodings/oem.py": "https://files.ballistica.net/cache/ba1/d8/94/965dd2bdba833008182619c18ddc", + "build/assets/pylib-android/encodings/palmos.py": "https://files.ballistica.net/cache/ba1/5a/0b/be8eb706b9d09d4e230b9e2e16e9", + "build/assets/pylib-android/encodings/ptcp154.py": "https://files.ballistica.net/cache/ba1/9b/77/0fc17dd7d7b123ef943b938e0708", + "build/assets/pylib-android/encodings/punycode.py": "https://files.ballistica.net/cache/ba1/9c/71/8c4bbf54905e3604ad4e57503fe4", + "build/assets/pylib-android/encodings/quopri_codec.py": "https://files.ballistica.net/cache/ba1/c4/13/d58409f806377ed2dd3857cf119c", + "build/assets/pylib-android/encodings/raw_unicode_escape.py": "https://files.ballistica.net/cache/ba1/e5/e3/a5597b4ee4bfe101687e5e75253e", + "build/assets/pylib-android/encodings/rot_13.py": "https://files.ballistica.net/cache/ba1/50/0d/726517b327c64224fff7b36a7c68", + "build/assets/pylib-android/encodings/shift_jis.py": "https://files.ballistica.net/cache/ba1/ba/7f/ca5a9bb1423e7909151420691df2", + "build/assets/pylib-android/encodings/shift_jis_2004.py": "https://files.ballistica.net/cache/ba1/31/78/07ad62cecf6e051d1dc95084066b", + "build/assets/pylib-android/encodings/shift_jisx0213.py": "https://files.ballistica.net/cache/ba1/06/32/5ba01dd78752697a3b611b809ec3", + "build/assets/pylib-android/encodings/tis_620.py": "https://files.ballistica.net/cache/ba1/da/1c/cc052b3d0e604876a6514f2f3549", + "build/assets/pylib-android/encodings/undefined.py": "https://files.ballistica.net/cache/ba1/bc/a9/511dfb43dddee15d6dadade563a2", + "build/assets/pylib-android/encodings/unicode_escape.py": "https://files.ballistica.net/cache/ba1/b9/ce/9c47fe9841a311615a2c89e7d100", + "build/assets/pylib-android/encodings/utf_16.py": "https://files.ballistica.net/cache/ba1/f5/98/40f92147f1f8273c4fbcc1bd8059", + "build/assets/pylib-android/encodings/utf_16_be.py": "https://files.ballistica.net/cache/ba1/0f/fe/a0c737729c1b935bac5f0d35cc7d", + "build/assets/pylib-android/encodings/utf_16_le.py": "https://files.ballistica.net/cache/ba1/31/48/1d28038a1d4d42beae915097e61d", + "build/assets/pylib-android/encodings/utf_32.py": "https://files.ballistica.net/cache/ba1/2d/05/30f686ab12e35b3292c923ee14ac", + "build/assets/pylib-android/encodings/utf_32_be.py": "https://files.ballistica.net/cache/ba1/0e/fd/9145398f14a6abe6b4c20a34d353", + "build/assets/pylib-android/encodings/utf_32_le.py": "https://files.ballistica.net/cache/ba1/2f/7f/5a7a416eaad439c447d99479de57", + "build/assets/pylib-android/encodings/utf_7.py": "https://files.ballistica.net/cache/ba1/ef/fc/f1a54204efc8a898c29eea6c45d0", + "build/assets/pylib-android/encodings/utf_8.py": "https://files.ballistica.net/cache/ba1/95/ec/b68e2505f91398a1454507315b81", + "build/assets/pylib-android/encodings/utf_8_sig.py": "https://files.ballistica.net/cache/ba1/a0/87/b1b21658d459b81d4b4f3eba5fda", + "build/assets/pylib-android/encodings/uu_codec.py": "https://files.ballistica.net/cache/ba1/81/f2/699b65f2fbfe2269b019d53638d4", + "build/assets/pylib-android/encodings/zlib_codec.py": "https://files.ballistica.net/cache/ba1/ae/b0/03f9878279ca750627da8a9286f9", + "build/assets/pylib-android/enum.py": "https://files.ballistica.net/cache/ba1/53/c5/7ff1762c1f25ba48f84dadfd5441", + "build/assets/pylib-android/filecmp.py": "https://files.ballistica.net/cache/ba1/75/1b/23213df0cd5330b4c0dcd3767af3", + "build/assets/pylib-android/fileinput.py": "https://files.ballistica.net/cache/ba1/bc/39/80230e16c3a78116438779c45371", + "build/assets/pylib-android/fnmatch.py": "https://files.ballistica.net/cache/ba1/8b/87/dc294f9f38ebdcbf8d9277ac2a85", + "build/assets/pylib-android/fractions.py": "https://files.ballistica.net/cache/ba1/04/49/d558379f23d4f94ec6735a097ded", + "build/assets/pylib-android/ftplib.py": "https://files.ballistica.net/cache/ba1/3a/ec/77a401ecbed6081a4684d69ada2e", + "build/assets/pylib-android/functools.py": "https://files.ballistica.net/cache/ba1/28/54/6b09496199dec466c74172f30d97", + "build/assets/pylib-android/genericpath.py": "https://files.ballistica.net/cache/ba1/21/98/a18c1526a17e6e91608196fb78b6", + "build/assets/pylib-android/getopt.py": "https://files.ballistica.net/cache/ba1/f4/df/08539701ee0170385793181efe65", + "build/assets/pylib-android/getpass.py": "https://files.ballistica.net/cache/ba1/f6/fa/acea25348bd0bf8ecbb5196cda17", + "build/assets/pylib-android/gettext.py": "https://files.ballistica.net/cache/ba1/fa/ec/f8eafce8f56d09a17d91c72428f6", + "build/assets/pylib-android/glob.py": "https://files.ballistica.net/cache/ba1/01/98/f0e817f4cf5cbfb8852e11516672", + "build/assets/pylib-android/graphlib.py": "https://files.ballistica.net/cache/ba1/fd/fe/8d2d1d1cd3b293424a6abf875898", + "build/assets/pylib-android/gzip.py": "https://files.ballistica.net/cache/ba1/03/d1/9f5cc17300434742fa65fababef3", + "build/assets/pylib-android/hashlib.py": "https://files.ballistica.net/cache/ba1/6f/1d/5649138c98b31a9014a8a5c7b556", + "build/assets/pylib-android/heapq.py": "https://files.ballistica.net/cache/ba1/3f/97/a85187af6f8e6483a5d9f97823a7", + "build/assets/pylib-android/hmac.py": "https://files.ballistica.net/cache/ba1/20/66/7a741af1d3d54917b19a03291f0d", + "build/assets/pylib-android/html/__init__.py": "https://files.ballistica.net/cache/ba1/98/29/1450470d5bbb985758672c9a976a", + "build/assets/pylib-android/html/entities.py": "https://files.ballistica.net/cache/ba1/21/4e/ca7db3103ec5430020eb6983a52a", + "build/assets/pylib-android/html/parser.py": "https://files.ballistica.net/cache/ba1/c9/c3/612563c206b602c386e4dc74bf8b", + "build/assets/pylib-android/http/__init__.py": "https://files.ballistica.net/cache/ba1/ab/a8/60f6e02e3057238cbd7faeb24a5d", + "build/assets/pylib-android/http/client.py": "https://files.ballistica.net/cache/ba1/c0/c5/cf40ebe4646690b870da0dbcf4bb", + "build/assets/pylib-android/http/cookiejar.py": "https://files.ballistica.net/cache/ba1/f9/98/2fe9f4d37c778b20eb61a01dff54", + "build/assets/pylib-android/http/cookies.py": "https://files.ballistica.net/cache/ba1/31/84/71c2f2759c2c96897238705328db", + "build/assets/pylib-android/http/server.py": "https://files.ballistica.net/cache/ba1/c2/af/92575cb4550157107b76f2ea355f", + "build/assets/pylib-android/imghdr.py": "https://files.ballistica.net/cache/ba1/c1/36/f12e13fc3353afe57107cc284321", + "build/assets/pylib-android/imp.py": "https://files.ballistica.net/cache/ba1/f1/d0/9fa34389681937d0ee5f62c9e207", + "build/assets/pylib-android/importlib/__init__.py": "https://files.ballistica.net/cache/ba1/bc/bf/52f17481ca5939f0dfa4bca4479a", + "build/assets/pylib-android/importlib/_abc.py": "https://files.ballistica.net/cache/ba1/1a/0c/83058b821d97b39a9323fefa7428", + "build/assets/pylib-android/importlib/_bootstrap.py": "https://files.ballistica.net/cache/ba1/7b/a0/6c6992f8834255259277822dab9d", + "build/assets/pylib-android/importlib/_bootstrap_external.py": "https://files.ballistica.net/cache/ba1/b4/c5/0ea0af304a714347a7c387abf801", + "build/assets/pylib-android/importlib/abc.py": "https://files.ballistica.net/cache/ba1/8d/79/841a5156187a336494465cd07937", + "build/assets/pylib-android/importlib/machinery.py": "https://files.ballistica.net/cache/ba1/99/7a/2a3d5b34e17bdea66844e5386492", + "build/assets/pylib-android/importlib/metadata/__init__.py": "https://files.ballistica.net/cache/ba1/aa/35/634de42f73f839565fd12a4caa15", + "build/assets/pylib-android/importlib/metadata/_adapters.py": "https://files.ballistica.net/cache/ba1/4e/61/25070def9cf30d5bae2d523ef76a", + "build/assets/pylib-android/importlib/metadata/_collections.py": "https://files.ballistica.net/cache/ba1/b9/c7/ade5baccac840ca3f37223fb0683", + "build/assets/pylib-android/importlib/metadata/_functools.py": "https://files.ballistica.net/cache/ba1/f1/40/0e02db2c114e3fca8940fddb7bdd", + "build/assets/pylib-android/importlib/metadata/_itertools.py": "https://files.ballistica.net/cache/ba1/51/0f/6a58f6e7963c7815ed6d8aac5e24", + "build/assets/pylib-android/importlib/metadata/_meta.py": "https://files.ballistica.net/cache/ba1/72/d9/feb60bf8f350cfee8dccde45b438", + "build/assets/pylib-android/importlib/metadata/_text.py": "https://files.ballistica.net/cache/ba1/d8/44/1bd3e7f6f46dce59b1d5a28d861d", + "build/assets/pylib-android/importlib/readers.py": "https://files.ballistica.net/cache/ba1/a3/a2/bccafd3eb836e36e7924db593898", + "build/assets/pylib-android/importlib/resources/__init__.py": "https://files.ballistica.net/cache/ba1/de/30/5cf534e41b591bc183748ce57026", + "build/assets/pylib-android/importlib/resources/_adapters.py": "https://files.ballistica.net/cache/ba1/56/a8/b63be4a0c7cfee6fe6eed7f169cd", + "build/assets/pylib-android/importlib/resources/_common.py": "https://files.ballistica.net/cache/ba1/2b/0c/c5591d8e465fca5c9752c39a7ac8", + "build/assets/pylib-android/importlib/resources/_itertools.py": "https://files.ballistica.net/cache/ba1/b3/b5/64484ce4f2ba88bc6cf14f4e2307", + "build/assets/pylib-android/importlib/resources/_legacy.py": "https://files.ballistica.net/cache/ba1/c5/3d/773a324cc910f4450030d052ecd7", + "build/assets/pylib-android/importlib/resources/abc.py": "https://files.ballistica.net/cache/ba1/e2/92/0ae14c9d68bfcb16a4306e2467aa", + "build/assets/pylib-android/importlib/resources/readers.py": "https://files.ballistica.net/cache/ba1/d1/d7/3c4fe435968eed76ac7f54efcd25", + "build/assets/pylib-android/importlib/resources/simple.py": "https://files.ballistica.net/cache/ba1/68/6f/220edb0ad3f1ae2c7b6983719300", + "build/assets/pylib-android/importlib/simple.py": "https://files.ballistica.net/cache/ba1/4e/60/69c044faccf4d24dfba6e548fd17", + "build/assets/pylib-android/importlib/util.py": "https://files.ballistica.net/cache/ba1/58/05/3acef7c0df3c760e3bd2d030c682", + "build/assets/pylib-android/inspect.py": "https://files.ballistica.net/cache/ba1/6d/8d/6215b0fb8758e81dcd46f0ceaacd", + "build/assets/pylib-android/io.py": "https://files.ballistica.net/cache/ba1/32/77/0725623b196167edb03028831eff", + "build/assets/pylib-android/ipaddress.py": "https://files.ballistica.net/cache/ba1/6e/99/78fe432980b893d35672ef501261", + "build/assets/pylib-android/json/__init__.py": "https://files.ballistica.net/cache/ba1/f9/5d/4ff63f138982a594966942d9bbb1", + "build/assets/pylib-android/json/decoder.py": "https://files.ballistica.net/cache/ba1/27/15/414888bb7c29f424847fea9b30b7", + "build/assets/pylib-android/json/encoder.py": "https://files.ballistica.net/cache/ba1/b2/c8/d69b7b68ee1d13f86c552fa7942e", + "build/assets/pylib-android/json/scanner.py": "https://files.ballistica.net/cache/ba1/7f/61/8f406cf4a3f53afb9c7a383426fd", + "build/assets/pylib-android/json/tool.py": "https://files.ballistica.net/cache/ba1/28/1f/7436fdee541ebbb8e27660c884bc", + "build/assets/pylib-android/keyword.py": "https://files.ballistica.net/cache/ba1/b3/87/956382148882b8e55de176d2b0f8", + "build/assets/pylib-android/linecache.py": "https://files.ballistica.net/cache/ba1/7d/ae/39fa9b35188a87e1810c4b90edce", + "build/assets/pylib-android/locale.py": "https://files.ballistica.net/cache/ba1/d4/4a/f94e9e568851d22c3aaa488f7854", + "build/assets/pylib-android/logging/__init__.py": "https://files.ballistica.net/cache/ba1/9b/b0/8faf58afc9c32f6971e191462f17", + "build/assets/pylib-android/logging/config.py": "https://files.ballistica.net/cache/ba1/44/ba/f0fb127840c33252a9efc64275c5", + "build/assets/pylib-android/logging/handlers.py": "https://files.ballistica.net/cache/ba1/7a/5e/387613e4b85840f534f1deab7986", + "build/assets/pylib-android/lzma.py": "https://files.ballistica.net/cache/ba1/76/62/b505ad06344d5897139a40d52d6d", + "build/assets/pylib-android/mailbox.py": "https://files.ballistica.net/cache/ba1/c6/68/bc900fcbd1b40c71ba98ae342730", + "build/assets/pylib-android/mailcap.py": "https://files.ballistica.net/cache/ba1/33/54/76f07b53b10e74b1064a42071848", + "build/assets/pylib-android/mimetypes.py": "https://files.ballistica.net/cache/ba1/e9/fc/67eb6795f2fd93c2b59a7948a367", + "build/assets/pylib-android/modulefinder.py": "https://files.ballistica.net/cache/ba1/67/1c/c5c15c8f4ed76772dd7f5d47f75d", + "build/assets/pylib-android/netrc.py": "https://files.ballistica.net/cache/ba1/24/ec/3d79f43a69b88195bb0033e6d150", + "build/assets/pylib-android/nntplib.py": "https://files.ballistica.net/cache/ba1/c3/e1/e6d5129fb4cf875b110dcc3eff45", + "build/assets/pylib-android/ntpath.py": "https://files.ballistica.net/cache/ba1/87/a6/9e53e3cc295480e3004315b1c38f", + "build/assets/pylib-android/nturl2path.py": "https://files.ballistica.net/cache/ba1/7c/7c/5a358f3716295903727657b7ffcd", + "build/assets/pylib-android/numbers.py": "https://files.ballistica.net/cache/ba1/49/0f/5b4f9dbd7970dee16dd5438daccf", + "build/assets/pylib-android/opcode.py": "https://files.ballistica.net/cache/ba1/c9/24/aa1231c1c0ca6407df6f4e13f6ee", + "build/assets/pylib-android/operator.py": "https://files.ballistica.net/cache/ba1/32/9b/a80a3f6d0415923600d968bfe526", + "build/assets/pylib-android/optparse.py": "https://files.ballistica.net/cache/ba1/60/d1/3c90fd50743a69425c7a40abf5f8", + "build/assets/pylib-android/os.py": "https://files.ballistica.net/cache/ba1/f4/58/e3e43f7b5f8482e9bb3d4e26802b", + "build/assets/pylib-android/pathlib.py": "https://files.ballistica.net/cache/ba1/83/bc/a10f85d027655f588b4b0a3cdbcc", + "build/assets/pylib-android/pdb.py": "https://files.ballistica.net/cache/ba1/d1/51/8f85a264bc8ff33c7a2f5f216f89", + "build/assets/pylib-android/pickle.py": "https://files.ballistica.net/cache/ba1/56/c8/4480e9d305f0ab1888989b79ba79", + "build/assets/pylib-android/pickletools.py": "https://files.ballistica.net/cache/ba1/09/b8/03acf4fb90321f6bcb1dffacf95e", + "build/assets/pylib-android/pipes.py": "https://files.ballistica.net/cache/ba1/a6/ac/524d28aecee445b00f5c167d79a2", + "build/assets/pylib-android/pkgutil.py": "https://files.ballistica.net/cache/ba1/1c/f4/5f26a59d1132671ca084d5155641", + "build/assets/pylib-android/platform.py": "https://files.ballistica.net/cache/ba1/50/58/a4bb984ce89a890ff9288ac3dba8", + "build/assets/pylib-android/plistlib.py": "https://files.ballistica.net/cache/ba1/c6/7a/002fb72735b5376d967b8fa9cbb6", + "build/assets/pylib-android/poplib.py": "https://files.ballistica.net/cache/ba1/ce/ce/201eb4c9e1efba11225172e9d28a", + "build/assets/pylib-android/posixpath.py": "https://files.ballistica.net/cache/ba1/cc/ff/d7b2d646ef74228f2db64689202a", + "build/assets/pylib-android/pprint.py": "https://files.ballistica.net/cache/ba1/37/94/be4cd4d59e62bbbcf9eec57d382b", + "build/assets/pylib-android/profile.py": "https://files.ballistica.net/cache/ba1/da/f9/3d6ac7f390636dca72f38c039711", + "build/assets/pylib-android/pstats.py": "https://files.ballistica.net/cache/ba1/fc/e3/26760eea795e0d27a4e7e872aad9", + "build/assets/pylib-android/pty.py": "https://files.ballistica.net/cache/ba1/ec/f2/9f3bd6278eb619477297c33f190c", + "build/assets/pylib-android/py_compile.py": "https://files.ballistica.net/cache/ba1/fe/79/528c79fc920a57b2df6c66c7c82d", + "build/assets/pylib-android/pyclbr.py": "https://files.ballistica.net/cache/ba1/8f/c0/dbb16fd7cf37d091494a1e1e8353", + "build/assets/pylib-android/pydoc.py": "https://files.ballistica.net/cache/ba1/91/f4/81dc97ab0abec5dff260792c5a15", + "build/assets/pylib-android/queue.py": "https://files.ballistica.net/cache/ba1/8f/de/830a86df78d44899a4b310c03ddd", + "build/assets/pylib-android/quopri.py": "https://files.ballistica.net/cache/ba1/77/45/f9f488fa13422afb7dd016f24148", + "build/assets/pylib-android/random.py": "https://files.ballistica.net/cache/ba1/99/8d/b9aaa4a32d99177e8ecb685a6e71", + "build/assets/pylib-android/re/__init__.py": "https://files.ballistica.net/cache/ba1/0f/44/c673cd698279d44478bcb9ebf66e", + "build/assets/pylib-android/re/_casefix.py": "https://files.ballistica.net/cache/ba1/d9/95/037bcd42132b82608102a43e1f0b", + "build/assets/pylib-android/re/_compiler.py": "https://files.ballistica.net/cache/ba1/a9/47/1b9771d3eb29dd936bfa74c703b9", + "build/assets/pylib-android/re/_constants.py": "https://files.ballistica.net/cache/ba1/e4/00/3c6d85e3faa6e291b56802b0ae77", + "build/assets/pylib-android/re/_parser.py": "https://files.ballistica.net/cache/ba1/21/60/3106b28386eb81710ee525e873d8", + "build/assets/pylib-android/reprlib.py": "https://files.ballistica.net/cache/ba1/66/23/1e3bc2f61285f85c773bd1c5075b", + "build/assets/pylib-android/rlcompleter.py": "https://files.ballistica.net/cache/ba1/76/5e/a1161ef963ddb70eac6971393023", + "build/assets/pylib-android/runpy.py": "https://files.ballistica.net/cache/ba1/a0/f6/47e6523cbc323b870a308b6dacf5", + "build/assets/pylib-android/sched.py": "https://files.ballistica.net/cache/ba1/d8/5c/ad937058b2d86337b8f25955635d", + "build/assets/pylib-android/secrets.py": "https://files.ballistica.net/cache/ba1/e7/e5/17a8aa20e547d6b00e33868b0c1b", + "build/assets/pylib-android/selectors.py": "https://files.ballistica.net/cache/ba1/29/70/701f1dc15d960ddbdb173e2ad15a", + "build/assets/pylib-android/shelve.py": "https://files.ballistica.net/cache/ba1/4a/29/679f70785058deb9673be087781b", + "build/assets/pylib-android/shlex.py": "https://files.ballistica.net/cache/ba1/b4/86/5c4942ead90ee153a0cfd7db59e7", + "build/assets/pylib-android/shutil.py": "https://files.ballistica.net/cache/ba1/19/91/02364228e97892aabdc718ed6b0a", + "build/assets/pylib-android/signal.py": "https://files.ballistica.net/cache/ba1/4e/d0/e28909e4ba4ed0ff14a5d8b6a334", + "build/assets/pylib-android/site.py": "https://files.ballistica.net/cache/ba1/43/dc/e1b762037fdfe5f7c0ca619d30a5", + "build/assets/pylib-android/smtpd.py": "https://files.ballistica.net/cache/ba1/28/d1/42d510bf914f547f71173d95fbf2", + "build/assets/pylib-android/smtplib.py": "https://files.ballistica.net/cache/ba1/24/b7/570f1db878df0d5d1e57f681f9e7", + "build/assets/pylib-android/sndhdr.py": "https://files.ballistica.net/cache/ba1/e0/5f/20e17d82515bbc77a56bea545aa8", + "build/assets/pylib-android/socket.py": "https://files.ballistica.net/cache/ba1/6a/c0/d1921984235ba74200d18c76e28c", + "build/assets/pylib-android/socketserver.py": "https://files.ballistica.net/cache/ba1/88/6a/6a78b0ac4d0e87bb879e14c0bb87", + "build/assets/pylib-android/sqlite3/__init__.py": "https://files.ballistica.net/cache/ba1/f4/db/b520c3f25fe5ab90f1c25a075395", + "build/assets/pylib-android/sqlite3/dbapi2.py": "https://files.ballistica.net/cache/ba1/fc/6e/d874d427c273374b06a6a8eca2ae", + "build/assets/pylib-android/sqlite3/dump.py": "https://files.ballistica.net/cache/ba1/f5/e8/7af1014950763f22e0f06448c9f1", + "build/assets/pylib-android/sre_compile.py": "https://files.ballistica.net/cache/ba1/65/a7/ff14bb856fe8350a52b5b40fe18f", + "build/assets/pylib-android/sre_constants.py": "https://files.ballistica.net/cache/ba1/74/71/4957946e475a892b97c1c75233a6", + "build/assets/pylib-android/sre_parse.py": "https://files.ballistica.net/cache/ba1/7e/be/498cee60aae4c9c0c097d5006a1d", + "build/assets/pylib-android/ssl.py": "https://files.ballistica.net/cache/ba1/2b/27/a8d691b7e5a36da6c748bd21d4bf", + "build/assets/pylib-android/stat.py": "https://files.ballistica.net/cache/ba1/6b/57/1f2535476027b293cabfa7028f99", + "build/assets/pylib-android/statistics.py": "https://files.ballistica.net/cache/ba1/b3/64/b7b0af25358b9a09454f1856332a", + "build/assets/pylib-android/string.py": "https://files.ballistica.net/cache/ba1/ef/13/2ce40878621ef55d7ff91788794d", + "build/assets/pylib-android/stringprep.py": "https://files.ballistica.net/cache/ba1/73/36/72c617e2fa6ebb4dcd56e6d1bfd3", + "build/assets/pylib-android/struct.py": "https://files.ballistica.net/cache/ba1/b5/65/28d3c888541c45e0fa26999f59b4", + "build/assets/pylib-android/subprocess.py": "https://files.ballistica.net/cache/ba1/12/b7/528dfa8935ea415d3949183c05af", + "build/assets/pylib-android/sunau.py": "https://files.ballistica.net/cache/ba1/96/1d/e2279e74a12f965eac6e5ca23cab", + "build/assets/pylib-android/symtable.py": "https://files.ballistica.net/cache/ba1/93/58/5cbabba2ba6a7f33f822f9a7a529", + "build/assets/pylib-android/sysconfig.py": "https://files.ballistica.net/cache/ba1/08/3a/7b9d4bd098bf177ef71484a43aea", + "build/assets/pylib-android/tabnanny.py": "https://files.ballistica.net/cache/ba1/e4/da/288591c481c3bb632a4b0561b241", + "build/assets/pylib-android/tarfile.py": "https://files.ballistica.net/cache/ba1/84/0a/faa3ad5287d627dcc72b411db083", + "build/assets/pylib-android/telnetlib.py": "https://files.ballistica.net/cache/ba1/c2/34/6c1e6f4c6d95743666eeaa6d054e", + "build/assets/pylib-android/tempfile.py": "https://files.ballistica.net/cache/ba1/f1/7c/74e6b18f230d21e98a8f32f25697", + "build/assets/pylib-android/textwrap.py": "https://files.ballistica.net/cache/ba1/66/2b/8cb816cd75bd9a029f8088f1d0a0", + "build/assets/pylib-android/this.py": "https://files.ballistica.net/cache/ba1/ca/2a/ff0e5dcac20428bd96ad24662853", + "build/assets/pylib-android/threading.py": "https://files.ballistica.net/cache/ba1/b9/3c/883e63f8cb98c8f87051eda10e04", + "build/assets/pylib-android/timeit.py": "https://files.ballistica.net/cache/ba1/bc/1d/5d9e5188d01da7504a94034e7307", + "build/assets/pylib-android/token.py": "https://files.ballistica.net/cache/ba1/9c/54/aa994fd4ae774207727ab68f6ea8", + "build/assets/pylib-android/tokenize.py": "https://files.ballistica.net/cache/ba1/89/85/5496bce392b43435a3a1481a770f", + "build/assets/pylib-android/tomllib/__init__.py": "https://files.ballistica.net/cache/ba1/95/8f/801c692372537fd03c5b783a6158", + "build/assets/pylib-android/tomllib/_parser.py": "https://files.ballistica.net/cache/ba1/32/ec/cb2f06d613fd29f78ad931f23c4f", + "build/assets/pylib-android/tomllib/_re.py": "https://files.ballistica.net/cache/ba1/b2/75/34d09c059944a53dca471550ca44", + "build/assets/pylib-android/tomllib/_types.py": "https://files.ballistica.net/cache/ba1/f0/ec/c94c592886eb18b3249e056d6666", + "build/assets/pylib-android/trace.py": "https://files.ballistica.net/cache/ba1/2d/d3/6d3f0c8bc86695e1bacf8a3f83a8", + "build/assets/pylib-android/traceback.py": "https://files.ballistica.net/cache/ba1/67/eb/312dfd2ceda7e64814f9370d499c", + "build/assets/pylib-android/tracemalloc.py": "https://files.ballistica.net/cache/ba1/0d/40/f46807fdc83c72ff96e981113e2e", + "build/assets/pylib-android/tty.py": "https://files.ballistica.net/cache/ba1/fd/f1/56604db1c20236b280c4348149a4", + "build/assets/pylib-android/types.py": "https://files.ballistica.net/cache/ba1/ad/e3/8b9019c8ec8e9de9f1221c16fc17", + "build/assets/pylib-android/typing.py": "https://files.ballistica.net/cache/ba1/31/88/e43b1970ee293758879c511d060e", + "build/assets/pylib-android/urllib/__init__.py": "https://files.ballistica.net/cache/ba1/75/89/662268489c303f52307a27af2450", + "build/assets/pylib-android/urllib/error.py": "https://files.ballistica.net/cache/ba1/d9/09/60d240697dbf66101eb7b5a7f334", + "build/assets/pylib-android/urllib/parse.py": "https://files.ballistica.net/cache/ba1/34/cd/b432595cdca2526de7ea6f3f530e", + "build/assets/pylib-android/urllib/request.py": "https://files.ballistica.net/cache/ba1/c6/df/5ffbcc892b0e7b1bec7f64bcdb97", + "build/assets/pylib-android/urllib/response.py": "https://files.ballistica.net/cache/ba1/76/9e/fd7f6038b304ba7326d70d3545b4", + "build/assets/pylib-android/urllib/robotparser.py": "https://files.ballistica.net/cache/ba1/69/dc/f3dfa2f2cdc4f34cc57a3679fabf", + "build/assets/pylib-android/uu.py": "https://files.ballistica.net/cache/ba1/cf/58/ae214afee2e9d8a9b8f404eae696", + "build/assets/pylib-android/uuid.py": "https://files.ballistica.net/cache/ba1/fb/64/209cd33405f033e79a1448eb2bcd", + "build/assets/pylib-android/warnings.py": "https://files.ballistica.net/cache/ba1/83/b1/bdb64efd4c030e26f72bbe89a4e7", + "build/assets/pylib-android/wave.py": "https://files.ballistica.net/cache/ba1/15/33/3464f7fedaf9b32971565898a093", + "build/assets/pylib-android/weakref.py": "https://files.ballistica.net/cache/ba1/4e/75/588e9a04172e84d79431ec3b91c5", + "build/assets/pylib-android/webbrowser.py": "https://files.ballistica.net/cache/ba1/9a/ba/2b811e113df36764154b48688b9f", + "build/assets/pylib-android/xdrlib.py": "https://files.ballistica.net/cache/ba1/c8/f8/daead2c847afbc51eb8e2feb71bf", + "build/assets/pylib-android/xml/__init__.py": "https://files.ballistica.net/cache/ba1/40/83/b412dfe0b3eca3a890866b7a78f3", + "build/assets/pylib-android/xml/dom/NodeFilter.py": "https://files.ballistica.net/cache/ba1/6f/2b/c9f51382c39a5e535dc81c8d02d0", + "build/assets/pylib-android/xml/dom/__init__.py": "https://files.ballistica.net/cache/ba1/4b/0f/656e1a02a78afd09e11ee2e0ff1b", + "build/assets/pylib-android/xml/dom/domreg.py": "https://files.ballistica.net/cache/ba1/37/f2/0f8f581b02c85689ca1dc71f5da6", + "build/assets/pylib-android/xml/dom/expatbuilder.py": "https://files.ballistica.net/cache/ba1/46/5a/156a1601e861597f0b6a7f6a1a1e", + "build/assets/pylib-android/xml/dom/minicompat.py": "https://files.ballistica.net/cache/ba1/cf/99/971fbbd2707ea2dacb8b92fe9546", + "build/assets/pylib-android/xml/dom/minidom.py": "https://files.ballistica.net/cache/ba1/83/eb/4821714212f71e489594bfeb4af4", + "build/assets/pylib-android/xml/dom/pulldom.py": "https://files.ballistica.net/cache/ba1/6c/e2/d5078673cdb0023e1f0368b62f6b", + "build/assets/pylib-android/xml/dom/xmlbuilder.py": "https://files.ballistica.net/cache/ba1/a9/22/d0bdad5d0a87b1381669e2cd59eb", + "build/assets/pylib-android/xml/etree/ElementInclude.py": "https://files.ballistica.net/cache/ba1/a3/bc/d17af57dfb3b77bdc278d8985e53", + "build/assets/pylib-android/xml/etree/ElementPath.py": "https://files.ballistica.net/cache/ba1/0b/8c/8273fe93520b7343c7d04e1051c5", + "build/assets/pylib-android/xml/etree/ElementTree.py": "https://files.ballistica.net/cache/ba1/84/52/1dc4c04d62f8959178654a164d1c", + "build/assets/pylib-android/xml/etree/__init__.py": "https://files.ballistica.net/cache/ba1/63/16/9c82d3917a179f20d9e57fcbb208", + "build/assets/pylib-android/xml/etree/cElementTree.py": "https://files.ballistica.net/cache/ba1/5c/93/41ef23871901e5fa93247b3a14aa", + "build/assets/pylib-android/xml/parsers/__init__.py": "https://files.ballistica.net/cache/ba1/74/8c/b5ba94c235a3065dffddbde3db8f", + "build/assets/pylib-android/xml/parsers/expat.py": "https://files.ballistica.net/cache/ba1/9b/87/8bd708e9ffc734623d711a35a9e1", + "build/assets/pylib-android/xml/sax/__init__.py": "https://files.ballistica.net/cache/ba1/0f/8e/df400d81abf03f7150a4dc25b906", + "build/assets/pylib-android/xml/sax/_exceptions.py": "https://files.ballistica.net/cache/ba1/89/c0/5d04f5a7c20eeff6720ecca557f5", + "build/assets/pylib-android/xml/sax/expatreader.py": "https://files.ballistica.net/cache/ba1/be/63/b1e1bdbc7a9f3ef18a46a866b7d0", + "build/assets/pylib-android/xml/sax/handler.py": "https://files.ballistica.net/cache/ba1/20/52/b1c654fbafe536cd3b6baad978a5", + "build/assets/pylib-android/xml/sax/saxutils.py": "https://files.ballistica.net/cache/ba1/e1/a0/567425288fbb46ca1ca122a37989", + "build/assets/pylib-android/xml/sax/xmlreader.py": "https://files.ballistica.net/cache/ba1/79/2f/33f4dda1a7f4ae416e1ea5dfed7a", + "build/assets/pylib-android/xmlrpc/__init__.py": "https://files.ballistica.net/cache/ba1/26/e0/d50d2b09954e70df3f6103771a6a", + "build/assets/pylib-android/xmlrpc/client.py": "https://files.ballistica.net/cache/ba1/99/9e/3ce7fd5979e14474bfb58f785025", + "build/assets/pylib-android/xmlrpc/server.py": "https://files.ballistica.net/cache/ba1/04/88/2bcc20caec1d4074fbf1cbe5886e", + "build/assets/pylib-android/zipapp.py": "https://files.ballistica.net/cache/ba1/b3/8e/aec28096190b6dfb1f73fc5c9351", + "build/assets/pylib-android/zipfile.py": "https://files.ballistica.net/cache/ba1/9d/96/2769a54efed3f179c722bb3e3238", + "build/assets/pylib-android/zipimport.py": "https://files.ballistica.net/cache/ba1/85/79/e5d60b513a0f5f2793747295b081", + "build/assets/pylib-android/zoneinfo/__init__.py": "https://files.ballistica.net/cache/ba1/df/ee/927882c75e6c5969c8115dc734ab", + "build/assets/pylib-android/zoneinfo/_common.py": "https://files.ballistica.net/cache/ba1/72/1d/5c4770adce2c36594379cecc77ac", + "build/assets/pylib-android/zoneinfo/_tzpath.py": "https://files.ballistica.net/cache/ba1/a8/14/0bef1d1cec01def80bfb1a4d497e", + "build/assets/pylib-android/zoneinfo/_zoneinfo.py": "https://files.ballistica.net/cache/ba1/33/d7/f54f02ab6d71644dcdfc84eb229d", + "build/assets/pylib-apple/__future__.py": "https://files.ballistica.net/cache/ba1/6b/1e/102d75672ef5bf1a26c2ad87e7b3", + "build/assets/pylib-apple/__hello__.py": "https://files.ballistica.net/cache/ba1/fa/66/6cefabeadb9cf4df7f656ce513ba", + "build/assets/pylib-apple/_aix_support.py": "https://files.ballistica.net/cache/ba1/34/d4/602e20d0eec44fb6c50837cda3b6", + "build/assets/pylib-apple/_bootsubprocess.py": "https://files.ballistica.net/cache/ba1/fc/55/26cf31325c079fffcc04ba947509", + "build/assets/pylib-apple/_collections_abc.py": "https://files.ballistica.net/cache/ba1/87/6e/393c9bb7819345396f52b4e37698", + "build/assets/pylib-apple/_compat_pickle.py": "https://files.ballistica.net/cache/ba1/42/bf/2b6b300ed24cbc40a3a0b5454211", + "build/assets/pylib-apple/_compression.py": "https://files.ballistica.net/cache/ba1/66/2f/3b388b3ff1809779557fe3cfa7ea", + "build/assets/pylib-apple/_ios_support.py": "https://files.ballistica.net/cache/ba1/11/6a/5a467e0f9e7c6cd5685cd734a1a5", + "build/assets/pylib-apple/_markupbase.py": "https://files.ballistica.net/cache/ba1/ec/0c/2908468fa52281c543402df924a5", + "build/assets/pylib-apple/_osx_support.py": "https://files.ballistica.net/cache/ba1/8d/0e/3b4d8db6e20abaa9ccfd27faf682", + "build/assets/pylib-apple/_py_abc.py": "https://files.ballistica.net/cache/ba1/4c/d6/0be7dfa8711e09fc5967b5ff34e3", + "build/assets/pylib-apple/_pydecimal.py": "https://files.ballistica.net/cache/ba1/f0/37/a282c7517b4ac1aabe58c6a569d8", + "build/assets/pylib-apple/_pyio.py": "https://files.ballistica.net/cache/ba1/52/bb/ba702272a13b5acfd5e1ed9afb73", + "build/assets/pylib-apple/_sitebuiltins.py": "https://files.ballistica.net/cache/ba1/f8/67/825e01d342cfcf3acea501b35f5b", + "build/assets/pylib-apple/_strptime.py": "https://files.ballistica.net/cache/ba1/66/81/26118909f96dd491426e45ea48ce", + "build/assets/pylib-apple/_sysconfigdata__darwin_darwin.py": "https://files.ballistica.net/cache/ba1/08/fb/47b9094e98159dc76d384aedc55f", + "build/assets/pylib-apple/_sysconfigdata__ios_iphoneos.py": "https://files.ballistica.net/cache/ba1/50/27/0a3d84dc3bc29bdd5f313adfccb6", + "build/assets/pylib-apple/_sysconfigdata__ios_iphoneos_arm64.py": "https://files.ballistica.net/cache/ba1/9b/4d/81147c7f184168fc1d09dbc3d136", + "build/assets/pylib-apple/_sysconfigdata__ios_iphonesimulator.py": "https://files.ballistica.net/cache/ba1/5f/b2/958abd2d0f6a2763652d07c2e780", + "build/assets/pylib-apple/_sysconfigdata__ios_iphonesimulator_arm64.py": "https://files.ballistica.net/cache/ba1/74/7a/359c84792f5c0180175082400973", + "build/assets/pylib-apple/_sysconfigdata__ios_iphonesimulator_x86_64.py": "https://files.ballistica.net/cache/ba1/df/35/cf3c011a55fb1f909c89bc4461cc", + "build/assets/pylib-apple/_sysconfigdata__tvos_appletvos.py": "https://files.ballistica.net/cache/ba1/43/74/dfa7ac1fd4c9e358b5c6b5d777ef", + "build/assets/pylib-apple/_sysconfigdata__tvos_appletvos_arm64.py": "https://files.ballistica.net/cache/ba1/5c/1c/40d7083ff0cb19fed0d2908fb255", + "build/assets/pylib-apple/_sysconfigdata__tvos_appletvsimulator.py": "https://files.ballistica.net/cache/ba1/e6/2d/ae74b1f948b584ae4b0d195bc674", + "build/assets/pylib-apple/_sysconfigdata__tvos_appletvsimulator_arm64.py": "https://files.ballistica.net/cache/ba1/05/cc/632e7bf770ed4dd300f13e9950b3", + "build/assets/pylib-apple/_sysconfigdata__tvos_appletvsimulator_x86_64.py": "https://files.ballistica.net/cache/ba1/31/02/00ec7a4436a1c0c7d76ab91b5e44", + "build/assets/pylib-apple/_sysconfigdata_d_darwin_darwin.py": "https://files.ballistica.net/cache/ba1/a9/2e/74600dd74ae742e9ac6bdc528f33", + "build/assets/pylib-apple/_sysconfigdata_d_ios_iphoneos.py": "https://files.ballistica.net/cache/ba1/1e/b1/d7fadd60aa4012b3acb6871a1eff", + "build/assets/pylib-apple/_sysconfigdata_d_ios_iphoneos_arm64.py": "https://files.ballistica.net/cache/ba1/60/0c/2a9e13dfcdc3d7f9f43020cf9413", + "build/assets/pylib-apple/_sysconfigdata_d_ios_iphonesimulator.py": "https://files.ballistica.net/cache/ba1/b4/50/2fab9b1ec896451b8bba207e5588", + "build/assets/pylib-apple/_sysconfigdata_d_ios_iphonesimulator_arm64.py": "https://files.ballistica.net/cache/ba1/63/61/afa07eb2e403519623b619c885c1", + "build/assets/pylib-apple/_sysconfigdata_d_ios_iphonesimulator_x86_64.py": "https://files.ballistica.net/cache/ba1/cc/cf/2a486577733b82d5f114b9777f5c", + "build/assets/pylib-apple/_sysconfigdata_d_tvos_appletvos.py": "https://files.ballistica.net/cache/ba1/5a/df/d7a9481ba11f94b386266c40565f", + "build/assets/pylib-apple/_sysconfigdata_d_tvos_appletvos_arm64.py": "https://files.ballistica.net/cache/ba1/65/fd/f52cb3d6f9f2eacc8bdefbd851b1", + "build/assets/pylib-apple/_sysconfigdata_d_tvos_appletvsimulator.py": "https://files.ballistica.net/cache/ba1/14/12/654dfe0840fefff24a047183b73e", + "build/assets/pylib-apple/_sysconfigdata_d_tvos_appletvsimulator_arm64.py": "https://files.ballistica.net/cache/ba1/0c/d6/bab3ca6e30d97d6bd3f37b1ac36f", + "build/assets/pylib-apple/_sysconfigdata_d_tvos_appletvsimulator_x86_64.py": "https://files.ballistica.net/cache/ba1/ce/00/9a44545de86a62c3143c99db3404", + "build/assets/pylib-apple/_threading_local.py": "https://files.ballistica.net/cache/ba1/22/80/23639c6caa13f99cee20992c0e77", + "build/assets/pylib-apple/_weakrefset.py": "https://files.ballistica.net/cache/ba1/ed/ad/9fe6413d9dbaf927daf237d27570", + "build/assets/pylib-apple/abc.py": "https://files.ballistica.net/cache/ba1/70/9e/8062bae76bd0ee89d12a7b7d8ef3", + "build/assets/pylib-apple/aifc.py": "https://files.ballistica.net/cache/ba1/6e/4e/2fda13fa194bc71d738696c98370", + "build/assets/pylib-apple/antigravity.py": "https://files.ballistica.net/cache/ba1/69/23/6f0a71652be7c013ffc949e00868", + "build/assets/pylib-apple/argparse.py": "https://files.ballistica.net/cache/ba1/95/57/1bfd2e66c58d815e9ac0f29a3ddd", + "build/assets/pylib-apple/ast.py": "https://files.ballistica.net/cache/ba1/9d/e4/b581eec4d2876d550488e4e3884d", + "build/assets/pylib-apple/asynchat.py": "https://files.ballistica.net/cache/ba1/d1/4a/b3d9be8700b24d2e51ca181ccf36", + "build/assets/pylib-apple/asyncio/__init__.py": "https://files.ballistica.net/cache/ba1/a9/c8/a7ea80c6cce571f7d6964afa83d2", + "build/assets/pylib-apple/asyncio/__main__.py": "https://files.ballistica.net/cache/ba1/12/33/52fc6700602f97d4c3c48f0f482a", + "build/assets/pylib-apple/asyncio/base_events.py": "https://files.ballistica.net/cache/ba1/5a/cf/600ccce816a083143d86fba1e2b4", + "build/assets/pylib-apple/asyncio/base_futures.py": "https://files.ballistica.net/cache/ba1/c8/ab/bac1edb71f53aad77b9362f767ae", + "build/assets/pylib-apple/asyncio/base_subprocess.py": "https://files.ballistica.net/cache/ba1/78/cc/5e88b0269027f71285ba6ee7308f", + "build/assets/pylib-apple/asyncio/base_tasks.py": "https://files.ballistica.net/cache/ba1/37/d7/5b4f362bc12cc0e4c7e5ec9621b0", + "build/assets/pylib-apple/asyncio/constants.py": "https://files.ballistica.net/cache/ba1/8c/48/8696de041fa4bb218ea9f329aa6f", + "build/assets/pylib-apple/asyncio/coroutines.py": "https://files.ballistica.net/cache/ba1/e8/30/f09c05f9be9cc326555531e18f39", + "build/assets/pylib-apple/asyncio/events.py": "https://files.ballistica.net/cache/ba1/58/59/abfcffd67d5f6f601ad148b70040", + "build/assets/pylib-apple/asyncio/exceptions.py": "https://files.ballistica.net/cache/ba1/20/bd/ae76cab225708b0020c94384ea75", + "build/assets/pylib-apple/asyncio/format_helpers.py": "https://files.ballistica.net/cache/ba1/12/62/ffc54305f738db4745828ed03443", + "build/assets/pylib-apple/asyncio/futures.py": "https://files.ballistica.net/cache/ba1/49/03/6b954437e9297811c84b794036fc", + "build/assets/pylib-apple/asyncio/locks.py": "https://files.ballistica.net/cache/ba1/82/83/9b0547b2b716eb560550182b2472", + "build/assets/pylib-apple/asyncio/log.py": "https://files.ballistica.net/cache/ba1/25/7d/1baf23cd89a6960c092ec4692219", + "build/assets/pylib-apple/asyncio/mixins.py": "https://files.ballistica.net/cache/ba1/ca/ea/57965f7f5a6b42595ee26692f41d", + "build/assets/pylib-apple/asyncio/proactor_events.py": "https://files.ballistica.net/cache/ba1/a9/2c/525c6ac9c90a0b6a67f81887b70e", + "build/assets/pylib-apple/asyncio/protocols.py": "https://files.ballistica.net/cache/ba1/63/84/cc27efd70e73da5dce1ae21b2d94", + "build/assets/pylib-apple/asyncio/queues.py": "https://files.ballistica.net/cache/ba1/73/7d/596b8b6cc54068eb86e12cb498b9", + "build/assets/pylib-apple/asyncio/runners.py": "https://files.ballistica.net/cache/ba1/af/45/ce88f654c3a605c5c638456aa5e3", + "build/assets/pylib-apple/asyncio/selector_events.py": "https://files.ballistica.net/cache/ba1/40/74/dfc2e299f9baac5590535a8449f3", + "build/assets/pylib-apple/asyncio/sslproto.py": "https://files.ballistica.net/cache/ba1/e7/34/c01d73988e6942ac3abd2eba33ef", + "build/assets/pylib-apple/asyncio/staggered.py": "https://files.ballistica.net/cache/ba1/49/67/46d65e8b0ab3a594f14345a74c9e", + "build/assets/pylib-apple/asyncio/streams.py": "https://files.ballistica.net/cache/ba1/2e/24/1c796eddd4c0ba30eb662b68cd4c", + "build/assets/pylib-apple/asyncio/subprocess.py": "https://files.ballistica.net/cache/ba1/aa/94/6c38cf7a6fe5e554a398fe23d9cf", + "build/assets/pylib-apple/asyncio/taskgroups.py": "https://files.ballistica.net/cache/ba1/7b/73/9ea38f3f9bad97fd45144f74699a", + "build/assets/pylib-apple/asyncio/tasks.py": "https://files.ballistica.net/cache/ba1/74/21/1fb1b15e53595fa1f433b59ac9e4", + "build/assets/pylib-apple/asyncio/threads.py": "https://files.ballistica.net/cache/ba1/c4/15/e194c8e88d742c4209f8ceb29e8a", + "build/assets/pylib-apple/asyncio/timeouts.py": "https://files.ballistica.net/cache/ba1/e1/13/f1d9575de93b0b44db7564f3e785", + "build/assets/pylib-apple/asyncio/transports.py": "https://files.ballistica.net/cache/ba1/f5/c2/26ef957e09846f22dcd8f4ee67b6", + "build/assets/pylib-apple/asyncio/trsock.py": "https://files.ballistica.net/cache/ba1/a0/db/12804978d1611d16216f5d0be2e7", + "build/assets/pylib-apple/asyncio/unix_events.py": "https://files.ballistica.net/cache/ba1/04/57/bba9763febfa4951ba8bc22907e0", + "build/assets/pylib-apple/asyncio/windows_events.py": "https://files.ballistica.net/cache/ba1/17/89/7be6c65747f35805df10d642e2a1", + "build/assets/pylib-apple/asyncio/windows_utils.py": "https://files.ballistica.net/cache/ba1/14/17/95caff1f5c29f894395f8afb39e1", + "build/assets/pylib-apple/asyncore.py": "https://files.ballistica.net/cache/ba1/ce/5a/a50dd09d7545cc48295406d1dfe9", + "build/assets/pylib-apple/base64.py": "https://files.ballistica.net/cache/ba1/1e/56/f4fc5ec97e04d0285a78751660ef", + "build/assets/pylib-apple/bdb.py": "https://files.ballistica.net/cache/ba1/95/7d/b2e5b1c191bb861c9ee772c09b2f", + "build/assets/pylib-apple/bisect.py": "https://files.ballistica.net/cache/ba1/8c/e0/927f86b9aa9b2837ea3ad43096f5", + "build/assets/pylib-apple/bz2.py": "https://files.ballistica.net/cache/ba1/7b/15/0c9fa75db1eddd93ad02bff724b1", + "build/assets/pylib-apple/cProfile.py": "https://files.ballistica.net/cache/ba1/17/eb/a2f2bba7bf6528ffc1a842cda932", + "build/assets/pylib-apple/calendar.py": "https://files.ballistica.net/cache/ba1/8c/d8/0729a061d1015ebf110d57058104", + "build/assets/pylib-apple/cgi.py": "https://files.ballistica.net/cache/ba1/79/41/54b20df815ee7b781cb1c6ad71de", + "build/assets/pylib-apple/cgitb.py": "https://files.ballistica.net/cache/ba1/2d/74/8972d91cc10853e8168a19f59157", + "build/assets/pylib-apple/chunk.py": "https://files.ballistica.net/cache/ba1/05/5a/5d5fb20ff56a11db528b29861219", + "build/assets/pylib-apple/cmd.py": "https://files.ballistica.net/cache/ba1/76/ef/c18cf2270200bdeef1e6e344a8d0", + "build/assets/pylib-apple/code.py": "https://files.ballistica.net/cache/ba1/16/12/5c4e1fd3e172ec310c12ede8b9ee", + "build/assets/pylib-apple/codecs.py": "https://files.ballistica.net/cache/ba1/86/8a/437bd5cee709e7c0115b9eda654e", + "build/assets/pylib-apple/codeop.py": "https://files.ballistica.net/cache/ba1/0e/c3/750f6dae8bea415343626b2868e6", + "build/assets/pylib-apple/collections/__init__.py": "https://files.ballistica.net/cache/ba1/d2/fd/f7ef203d26b693ce62a554186bbd", + "build/assets/pylib-apple/collections/abc.py": "https://files.ballistica.net/cache/ba1/66/33/6ba0abc93b5d2ddf5b24c0e7ceec", + "build/assets/pylib-apple/colorsys.py": "https://files.ballistica.net/cache/ba1/7d/08/846a051326cdbb8012cd04c3170e", + "build/assets/pylib-apple/compileall.py": "https://files.ballistica.net/cache/ba1/19/46/20b1d34cabd3f7fc94a377790b2d", + "build/assets/pylib-apple/concurrent/__init__.py": "https://files.ballistica.net/cache/ba1/4e/6e/ef2f26c7fb93905cf801fe1dfb19", + "build/assets/pylib-apple/concurrent/futures/__init__.py": "https://files.ballistica.net/cache/ba1/44/46/38a048926a857b4d5d8e66288956", + "build/assets/pylib-apple/concurrent/futures/_base.py": "https://files.ballistica.net/cache/ba1/9b/01/cf1dc6b636a9e55ee7c38fab459f", + "build/assets/pylib-apple/concurrent/futures/process.py": "https://files.ballistica.net/cache/ba1/4b/8a/715baae4bb1fc82a5a47eb78084a", + "build/assets/pylib-apple/concurrent/futures/thread.py": "https://files.ballistica.net/cache/ba1/6f/67/e242e9d64d4926c0f2a0f764affd", + "build/assets/pylib-apple/configparser.py": "https://files.ballistica.net/cache/ba1/b9/2d/c586de202da604332f9d1163b6bc", + "build/assets/pylib-apple/contextlib.py": "https://files.ballistica.net/cache/ba1/7a/c5/64037035814cadb201f77a98d87d", + "build/assets/pylib-apple/contextvars.py": "https://files.ballistica.net/cache/ba1/ab/2b/1593de4d9e2ee765fab208b2b858", + "build/assets/pylib-apple/copy.py": "https://files.ballistica.net/cache/ba1/96/f1/7b53ba36cf2dba9719ea59d74336", + "build/assets/pylib-apple/copyreg.py": "https://files.ballistica.net/cache/ba1/1c/f2/8faf51c0601c5635b15eaf7f12d3", + "build/assets/pylib-apple/crypt.py": "https://files.ballistica.net/cache/ba1/2e/97/911bd28fbab679f9f9935ed0ad17", + "build/assets/pylib-apple/csv.py": "https://files.ballistica.net/cache/ba1/34/20/ba261d740478e59854bc548ffe66", + "build/assets/pylib-apple/ctypes/__init__.py": "https://files.ballistica.net/cache/ba1/8d/9d/5d3d4d362dcea51a79a41302ea8e", + "build/assets/pylib-apple/ctypes/_aix.py": "https://files.ballistica.net/cache/ba1/09/63/53d1c1b917119ed5f55c885a4afa", + "build/assets/pylib-apple/ctypes/_endian.py": "https://files.ballistica.net/cache/ba1/73/1f/e04ad4adfebdeaa5f3e5a3f2eb82", + "build/assets/pylib-apple/ctypes/macholib/__init__.py": "https://files.ballistica.net/cache/ba1/63/88/0a9db1810209a3e9fda6abaf7e03", + "build/assets/pylib-apple/ctypes/macholib/dyld.py": "https://files.ballistica.net/cache/ba1/6d/6d/b79b81f377a4f8b99cb6345ffa39", + "build/assets/pylib-apple/ctypes/macholib/dylib.py": "https://files.ballistica.net/cache/ba1/52/03/83068688251769e7d60d1fa1ad00", + "build/assets/pylib-apple/ctypes/macholib/framework.py": "https://files.ballistica.net/cache/ba1/f8/34/5533749c5999d5e80c803e8b5b00", + "build/assets/pylib-apple/ctypes/util.py": "https://files.ballistica.net/cache/ba1/e0/8d/801271c7ba84e66a58b362eee556", + "build/assets/pylib-apple/ctypes/wintypes.py": "https://files.ballistica.net/cache/ba1/55/e3/55803b6822669b8fc99bacf7a4ce", + "build/assets/pylib-apple/curses/__init__.py": "https://files.ballistica.net/cache/ba1/cf/31/2ed64e2f7d45c73d6f6f9d56c2d9", + "build/assets/pylib-apple/curses/ascii.py": "https://files.ballistica.net/cache/ba1/2a/8a/4b66e68da0df06b4e69779d9f121", + "build/assets/pylib-apple/curses/has_key.py": "https://files.ballistica.net/cache/ba1/1d/00/1bce48b4810e1b36b512233bb249", + "build/assets/pylib-apple/curses/panel.py": "https://files.ballistica.net/cache/ba1/14/ff/fbf7c6dc94206d60b8bfaddda086", + "build/assets/pylib-apple/curses/textpad.py": "https://files.ballistica.net/cache/ba1/0f/01/807ee892c8d66c6eb2dbb4486742", + "build/assets/pylib-apple/dataclasses.py": "https://files.ballistica.net/cache/ba1/b6/3e/df6b547026aaf16c711ba51beec5", + "build/assets/pylib-apple/datetime.py": "https://files.ballistica.net/cache/ba1/c9/e6/d09b5bcbf752e9abd62c7b9bf5b7", + "build/assets/pylib-apple/decimal.py": "https://files.ballistica.net/cache/ba1/af/42/6b12167bb2b5ef1b07ffa4779b5f", + "build/assets/pylib-apple/difflib.py": "https://files.ballistica.net/cache/ba1/d2/40/2c3f4c502ba76c4b5c350a2df292", + "build/assets/pylib-apple/dis.py": "https://files.ballistica.net/cache/ba1/1d/c5/13f5fd42bc382212cd4eff5d6a1a", + "build/assets/pylib-apple/doctest.py": "https://files.ballistica.net/cache/ba1/a4/ad/6ded1043af2650c90eb6616c08a3", + "build/assets/pylib-apple/email/__init__.py": "https://files.ballistica.net/cache/ba1/7b/87/2aa098490b4b45eb185a693bea4f", + "build/assets/pylib-apple/email/_encoded_words.py": "https://files.ballistica.net/cache/ba1/56/f2/bd7e95217f4404af92d7569c0492", + "build/assets/pylib-apple/email/_header_value_parser.py": "https://files.ballistica.net/cache/ba1/66/cd/a94fac226b6b49d5769d5adbc467", + "build/assets/pylib-apple/email/_parseaddr.py": "https://files.ballistica.net/cache/ba1/df/08/e161e8eca7410e66c2459da9e48a", + "build/assets/pylib-apple/email/_policybase.py": "https://files.ballistica.net/cache/ba1/77/47/57861786a7aa1f07732deb1c46a6", + "build/assets/pylib-apple/email/base64mime.py": "https://files.ballistica.net/cache/ba1/b1/65/2172bd301f1468b31a36126cebde", + "build/assets/pylib-apple/email/charset.py": "https://files.ballistica.net/cache/ba1/f3/4c/4738cf5451d996ab8de78ed69ea5", + "build/assets/pylib-apple/email/contentmanager.py": "https://files.ballistica.net/cache/ba1/7d/b2/7c9906866ae8fe1be9c7fc126d32", + "build/assets/pylib-apple/email/encoders.py": "https://files.ballistica.net/cache/ba1/89/a1/ce0930b00a1c100eff09e82fce43", + "build/assets/pylib-apple/email/errors.py": "https://files.ballistica.net/cache/ba1/76/4e/946ac4464bfcedec70cc98bab8f1", + "build/assets/pylib-apple/email/feedparser.py": "https://files.ballistica.net/cache/ba1/ce/0d/dffb8f9167c3f6fdc74864fd4a4d", + "build/assets/pylib-apple/email/generator.py": "https://files.ballistica.net/cache/ba1/9a/88/bb6e9956f5ab89d751e06007da5e", + "build/assets/pylib-apple/email/header.py": "https://files.ballistica.net/cache/ba1/18/b2/244a9b5187afecb33372f8b74465", + "build/assets/pylib-apple/email/headerregistry.py": "https://files.ballistica.net/cache/ba1/43/f9/408b3c7758b76bda29dcaaa0c4d8", + "build/assets/pylib-apple/email/iterators.py": "https://files.ballistica.net/cache/ba1/82/63/2191d958b39b33efc51b92ec5ade", + "build/assets/pylib-apple/email/message.py": "https://files.ballistica.net/cache/ba1/22/5f/94ffba693e7b5ed5dd6e3c788c18", + "build/assets/pylib-apple/email/mime/__init__.py": "https://files.ballistica.net/cache/ba1/ed/33/4918affb2faabaa7b2323c99763e", + "build/assets/pylib-apple/email/mime/application.py": "https://files.ballistica.net/cache/ba1/cb/66/f166d69f1b140ca9c86f48a2d68e", + "build/assets/pylib-apple/email/mime/audio.py": "https://files.ballistica.net/cache/ba1/b3/ae/7fed6ebf4d270a4fcb06877554b8", + "build/assets/pylib-apple/email/mime/base.py": "https://files.ballistica.net/cache/ba1/aa/a6/e0775cdf8cab154d10f50f79301a", + "build/assets/pylib-apple/email/mime/image.py": "https://files.ballistica.net/cache/ba1/b6/64/8079b455f6344adb0fe2eed102be", + "build/assets/pylib-apple/email/mime/message.py": "https://files.ballistica.net/cache/ba1/a1/cb/e247c688c11e7dc2681542aad678", + "build/assets/pylib-apple/email/mime/multipart.py": "https://files.ballistica.net/cache/ba1/b3/c7/1a101b548e1b24243a424e08b37b", + "build/assets/pylib-apple/email/mime/nonmultipart.py": "https://files.ballistica.net/cache/ba1/e8/80/0ee119412ecb3f6fcff81d32ccb3", + "build/assets/pylib-apple/email/mime/text.py": "https://files.ballistica.net/cache/ba1/30/d0/fb4aff3efb7f7d1e882b39cc12f6", + "build/assets/pylib-apple/email/parser.py": "https://files.ballistica.net/cache/ba1/41/21/40c6e18fa15ac4a57437f332ce1c", + "build/assets/pylib-apple/email/policy.py": "https://files.ballistica.net/cache/ba1/d8/9d/6b3438d1468ed515b778948ba1a6", + "build/assets/pylib-apple/email/quoprimime.py": "https://files.ballistica.net/cache/ba1/dd/73/352548416b0436833cd880553f55", + "build/assets/pylib-apple/email/utils.py": "https://files.ballistica.net/cache/ba1/f4/54/14b68e85101cda0df37f12bd4819", + "build/assets/pylib-apple/encodings/__init__.py": "https://files.ballistica.net/cache/ba1/c4/1b/df2a05d2ffe64d2c65bc5d7795bd", + "build/assets/pylib-apple/encodings/aliases.py": "https://files.ballistica.net/cache/ba1/c5/bd/232040ec866ae94f1d73ed2dcf47", + "build/assets/pylib-apple/encodings/ascii.py": "https://files.ballistica.net/cache/ba1/ec/a1/f7c4566e12fa88d743ad400ebdd5", + "build/assets/pylib-apple/encodings/base64_codec.py": "https://files.ballistica.net/cache/ba1/dd/17/343e9f54d9d4804d237b14f8697f", + "build/assets/pylib-apple/encodings/big5.py": "https://files.ballistica.net/cache/ba1/56/31/c25e9d6279b50d85a2b051a1711e", + "build/assets/pylib-apple/encodings/big5hkscs.py": "https://files.ballistica.net/cache/ba1/af/89/fafbc806e71dbee9f636198c4b3e", + "build/assets/pylib-apple/encodings/bz2_codec.py": "https://files.ballistica.net/cache/ba1/51/fa/6b5df1a9c13134270dd373527bc2", + "build/assets/pylib-apple/encodings/charmap.py": "https://files.ballistica.net/cache/ba1/8b/0f/ab006dd3e88fe98eec5c7af597d5", + "build/assets/pylib-apple/encodings/cp037.py": "https://files.ballistica.net/cache/ba1/65/82/d25eb620ecc6f2cb4281d1e85c76", + "build/assets/pylib-apple/encodings/cp1006.py": "https://files.ballistica.net/cache/ba1/10/ac/c40956aa6b9fccafd596b644d1fc", + "build/assets/pylib-apple/encodings/cp1026.py": "https://files.ballistica.net/cache/ba1/37/c2/017b75821c21cbda91edc8570201", + "build/assets/pylib-apple/encodings/cp1125.py": "https://files.ballistica.net/cache/ba1/e6/df/a09b65eaabd6f2f594a4c164aad3", + "build/assets/pylib-apple/encodings/cp1140.py": "https://files.ballistica.net/cache/ba1/25/4c/ebd0e58001f4c4d663579daccb2f", + "build/assets/pylib-apple/encodings/cp1250.py": "https://files.ballistica.net/cache/ba1/cb/9d/9af0bcc05a94f4b966f65fe93012", + "build/assets/pylib-apple/encodings/cp1251.py": "https://files.ballistica.net/cache/ba1/d8/3c/ca0a97cee92fd11d8064760b914a", + "build/assets/pylib-apple/encodings/cp1252.py": "https://files.ballistica.net/cache/ba1/c0/0d/3cb842c57dbcfb3db9f079bbb380", + "build/assets/pylib-apple/encodings/cp1253.py": "https://files.ballistica.net/cache/ba1/b1/48/e785eadce2a71cdb5e94cd5b3780", + "build/assets/pylib-apple/encodings/cp1254.py": "https://files.ballistica.net/cache/ba1/b9/e4/ddaed77dc4c74ce9a5646c8ef430", + "build/assets/pylib-apple/encodings/cp1255.py": "https://files.ballistica.net/cache/ba1/8c/36/2a72335c189c23e21204e09719d1", + "build/assets/pylib-apple/encodings/cp1256.py": "https://files.ballistica.net/cache/ba1/b1/47/2e2ced1b2b8cc726d520252bcb09", + "build/assets/pylib-apple/encodings/cp1257.py": "https://files.ballistica.net/cache/ba1/9a/f5/04cabf8a5278e2eca1b9bb82b8ca", + "build/assets/pylib-apple/encodings/cp1258.py": "https://files.ballistica.net/cache/ba1/77/dc/786d547668e3b9a2f146cc73c335", + "build/assets/pylib-apple/encodings/cp273.py": "https://files.ballistica.net/cache/ba1/c8/46/ab71a93511e074e1284211cce43c", + "build/assets/pylib-apple/encodings/cp424.py": "https://files.ballistica.net/cache/ba1/56/04/c5b3c6b76f4030deb0f0291c3f37", + "build/assets/pylib-apple/encodings/cp437.py": "https://files.ballistica.net/cache/ba1/07/18/e598a34a098eb3eda22a98eadd2d", + "build/assets/pylib-apple/encodings/cp500.py": "https://files.ballistica.net/cache/ba1/d5/33/f6545b3df78956780edd8348c26d", + "build/assets/pylib-apple/encodings/cp720.py": "https://files.ballistica.net/cache/ba1/b4/ab/0eae0009d3bd01986ae9b2b2238c", + "build/assets/pylib-apple/encodings/cp737.py": "https://files.ballistica.net/cache/ba1/13/87/09c2bd4e55ac2871145a9c7fec5b", + "build/assets/pylib-apple/encodings/cp775.py": "https://files.ballistica.net/cache/ba1/9d/1b/71c7c0eaf88c67aaa1d2723f1b76", + "build/assets/pylib-apple/encodings/cp850.py": "https://files.ballistica.net/cache/ba1/d7/a8/f6a2d373813a751f78ac00f02697", + "build/assets/pylib-apple/encodings/cp852.py": "https://files.ballistica.net/cache/ba1/dd/0e/6cabcc4cba4f8e670845347acfe4", + "build/assets/pylib-apple/encodings/cp855.py": "https://files.ballistica.net/cache/ba1/0e/f7/f671154fcc8d3875657664596034", + "build/assets/pylib-apple/encodings/cp856.py": "https://files.ballistica.net/cache/ba1/3d/5a/6ad9695c73a29d883c094efbb88f", + "build/assets/pylib-apple/encodings/cp857.py": "https://files.ballistica.net/cache/ba1/6b/00/f2523219c9a55a3fafef216583d1", + "build/assets/pylib-apple/encodings/cp858.py": "https://files.ballistica.net/cache/ba1/0e/d2/4a6a6e0cff33570043bda256ab09", + "build/assets/pylib-apple/encodings/cp860.py": "https://files.ballistica.net/cache/ba1/a2/7d/74b8dcaebd1f2282373ac8e6924f", + "build/assets/pylib-apple/encodings/cp861.py": "https://files.ballistica.net/cache/ba1/b7/d9/0c78b17e57de0fce971b1c586296", + "build/assets/pylib-apple/encodings/cp862.py": "https://files.ballistica.net/cache/ba1/06/fd/3d283ea35c0af4ac606037b486f3", + "build/assets/pylib-apple/encodings/cp863.py": "https://files.ballistica.net/cache/ba1/67/3b/d617c8a84e51d0b88948ae4bc6df", + "build/assets/pylib-apple/encodings/cp864.py": "https://files.ballistica.net/cache/ba1/f9/13/bf28252db9a0fe044b4814651501", + "build/assets/pylib-apple/encodings/cp865.py": "https://files.ballistica.net/cache/ba1/2c/33/674ca4090eee763e8f593e808110", + "build/assets/pylib-apple/encodings/cp866.py": "https://files.ballistica.net/cache/ba1/82/8f/668b67ff306376ba0348c03598eb", + "build/assets/pylib-apple/encodings/cp869.py": "https://files.ballistica.net/cache/ba1/07/09/b882da4e7dc442fd9d40b0c57a52", + "build/assets/pylib-apple/encodings/cp874.py": "https://files.ballistica.net/cache/ba1/4b/77/67d05a01a96b3cc1c80ed6292576", + "build/assets/pylib-apple/encodings/cp875.py": "https://files.ballistica.net/cache/ba1/fb/e2/cea4302850e2099f12f52adb8383", + "build/assets/pylib-apple/encodings/cp932.py": "https://files.ballistica.net/cache/ba1/04/ec/9936d88184632b03fc57aae66c15", + "build/assets/pylib-apple/encodings/cp949.py": "https://files.ballistica.net/cache/ba1/10/6f/f48153040490c1128bc993d51a1d", + "build/assets/pylib-apple/encodings/cp950.py": "https://files.ballistica.net/cache/ba1/34/55/ae68985ea5f579a6b3e61afe942d", + "build/assets/pylib-apple/encodings/euc_jis_2004.py": "https://files.ballistica.net/cache/ba1/a0/3c/34973ad9045a4c127638854ad0df", + "build/assets/pylib-apple/encodings/euc_jisx0213.py": "https://files.ballistica.net/cache/ba1/51/3e/92520e0c7ea747158ae7447dd9be", + "build/assets/pylib-apple/encodings/euc_jp.py": "https://files.ballistica.net/cache/ba1/17/ae/44ee27d1a55284e095a59d269230", + "build/assets/pylib-apple/encodings/euc_kr.py": "https://files.ballistica.net/cache/ba1/65/2e/54bfe9068ef6c562d98813cc58d4", + "build/assets/pylib-apple/encodings/gb18030.py": "https://files.ballistica.net/cache/ba1/78/ea/69bfeb991a8c483e83ec4993302d", + "build/assets/pylib-apple/encodings/gb2312.py": "https://files.ballistica.net/cache/ba1/01/c2/e1fdd10025ee35e6acf7255d5711", + "build/assets/pylib-apple/encodings/gbk.py": "https://files.ballistica.net/cache/ba1/bc/0d/b841141d19d85122500451bcb2d5", + "build/assets/pylib-apple/encodings/hex_codec.py": "https://files.ballistica.net/cache/ba1/3e/43/77a03b918b85788cda8655f1edca", + "build/assets/pylib-apple/encodings/hp_roman8.py": "https://files.ballistica.net/cache/ba1/b2/69/29e6f85fab353bea887fd0349674", + "build/assets/pylib-apple/encodings/hz.py": "https://files.ballistica.net/cache/ba1/f6/10/96d43be02259e8bb773fad3eab89", + "build/assets/pylib-apple/encodings/idna.py": "https://files.ballistica.net/cache/ba1/3d/67/9baf708367d1768d85378a207388", + "build/assets/pylib-apple/encodings/iso2022_jp.py": "https://files.ballistica.net/cache/ba1/ad/2b/051bb23e97ba1218f189cba44c42", + "build/assets/pylib-apple/encodings/iso2022_jp_1.py": "https://files.ballistica.net/cache/ba1/68/40/c9829c6bf6a7d9a0dfb69be875d4", + "build/assets/pylib-apple/encodings/iso2022_jp_2.py": "https://files.ballistica.net/cache/ba1/05/0a/0c26964823e893b9b140c0b49c26", + "build/assets/pylib-apple/encodings/iso2022_jp_2004.py": "https://files.ballistica.net/cache/ba1/98/e0/7e079264c4fd7e11ed7b6b2ba1df", + "build/assets/pylib-apple/encodings/iso2022_jp_3.py": "https://files.ballistica.net/cache/ba1/04/52/d9dc69cf32339038c7476b376108", + "build/assets/pylib-apple/encodings/iso2022_jp_ext.py": "https://files.ballistica.net/cache/ba1/2b/8e/8c750781f75946d295652458e9c6", + "build/assets/pylib-apple/encodings/iso2022_kr.py": "https://files.ballistica.net/cache/ba1/99/97/67057c562517753755c579fb0695", + "build/assets/pylib-apple/encodings/iso8859_1.py": "https://files.ballistica.net/cache/ba1/9e/12/a52f6614139a81c99a8c391c41c8", + "build/assets/pylib-apple/encodings/iso8859_10.py": "https://files.ballistica.net/cache/ba1/7d/89/1323d14bd39071682abf9ef150c3", + "build/assets/pylib-apple/encodings/iso8859_11.py": "https://files.ballistica.net/cache/ba1/b5/e8/df49b3adabed4b7ba04814bc6758", + "build/assets/pylib-apple/encodings/iso8859_13.py": "https://files.ballistica.net/cache/ba1/f1/2d/96e1e6befa8a7f671da144c24f06", + "build/assets/pylib-apple/encodings/iso8859_14.py": "https://files.ballistica.net/cache/ba1/d3/29/c0028d2341bf26a918f5d7724929", + "build/assets/pylib-apple/encodings/iso8859_15.py": "https://files.ballistica.net/cache/ba1/cd/a9/be741d3ca4418794a373a9bed2e8", + "build/assets/pylib-apple/encodings/iso8859_16.py": "https://files.ballistica.net/cache/ba1/82/c3/434e7b2fefa96b3894e2e346cb07", + "build/assets/pylib-apple/encodings/iso8859_2.py": "https://files.ballistica.net/cache/ba1/2d/81/3562ac283e38bbed8bb76d412b5b", + "build/assets/pylib-apple/encodings/iso8859_3.py": "https://files.ballistica.net/cache/ba1/58/df/0a07f7807ea342b7221a0a7d7dcf", + "build/assets/pylib-apple/encodings/iso8859_4.py": "https://files.ballistica.net/cache/ba1/97/2e/fd3e6866e4b2c743f2512ec862a1", + "build/assets/pylib-apple/encodings/iso8859_5.py": "https://files.ballistica.net/cache/ba1/95/37/2d88d26d1c75630c9b1f13b257d9", + "build/assets/pylib-apple/encodings/iso8859_6.py": "https://files.ballistica.net/cache/ba1/9e/c8/7cd4e3a8962f2d5c5f5033a44210", + "build/assets/pylib-apple/encodings/iso8859_7.py": "https://files.ballistica.net/cache/ba1/0d/56/1f0e50855365039ea3ad283e13c9", + "build/assets/pylib-apple/encodings/iso8859_8.py": "https://files.ballistica.net/cache/ba1/8f/dc/849fdc4c14d78436795905c86c66", + "build/assets/pylib-apple/encodings/iso8859_9.py": "https://files.ballistica.net/cache/ba1/af/2d/56c6a4f51cf9e14abe295237cf3f", + "build/assets/pylib-apple/encodings/johab.py": "https://files.ballistica.net/cache/ba1/a4/7f/5a4335a2cb40205fa7a313d57753", + "build/assets/pylib-apple/encodings/koi8_r.py": "https://files.ballistica.net/cache/ba1/10/36/151af008a932ac0d7da52bba47cc", + "build/assets/pylib-apple/encodings/koi8_t.py": "https://files.ballistica.net/cache/ba1/ff/ca/dc64dfcb075a5592f24225687a96", + "build/assets/pylib-apple/encodings/koi8_u.py": "https://files.ballistica.net/cache/ba1/3a/8c/514ae8e70329706ad72f32c1f392", + "build/assets/pylib-apple/encodings/kz1048.py": "https://files.ballistica.net/cache/ba1/bd/8d/1bcd553ef41e0e2b9bcae51fe900", + "build/assets/pylib-apple/encodings/latin_1.py": "https://files.ballistica.net/cache/ba1/af/9d/e100c978d87476d8728198c9442f", + "build/assets/pylib-apple/encodings/mac_arabic.py": "https://files.ballistica.net/cache/ba1/9e/ca/eb14a6e27328b030625c1161a5ed", + "build/assets/pylib-apple/encodings/mac_croatian.py": "https://files.ballistica.net/cache/ba1/da/78/94a9863f3742538da72808940112", + "build/assets/pylib-apple/encodings/mac_cyrillic.py": "https://files.ballistica.net/cache/ba1/59/45/505e30dc613529ac99caf4472d7a", + "build/assets/pylib-apple/encodings/mac_farsi.py": "https://files.ballistica.net/cache/ba1/95/c1/fb4b46d4b9488bc7031dc1ac0c71", + "build/assets/pylib-apple/encodings/mac_greek.py": "https://files.ballistica.net/cache/ba1/77/80/88f6975d06356c85c0cb6a148416", + "build/assets/pylib-apple/encodings/mac_iceland.py": "https://files.ballistica.net/cache/ba1/c4/25/7e7dff77ea92041f8a5179a16f46", + "build/assets/pylib-apple/encodings/mac_latin2.py": "https://files.ballistica.net/cache/ba1/ec/55/90896a1796b850c82ca95beb8a55", + "build/assets/pylib-apple/encodings/mac_roman.py": "https://files.ballistica.net/cache/ba1/79/d3/0ce20a6dc4df28aa97fd31af0fc4", + "build/assets/pylib-apple/encodings/mac_romanian.py": "https://files.ballistica.net/cache/ba1/97/48/acf1aa733a3133c9ab335a3c2c63", + "build/assets/pylib-apple/encodings/mac_turkish.py": "https://files.ballistica.net/cache/ba1/0a/ef/2c6f78227282ae49095881a15f67", + "build/assets/pylib-apple/encodings/mbcs.py": "https://files.ballistica.net/cache/ba1/c9/a4/8e24fb6c5af88c1ccd783e906dd1", + "build/assets/pylib-apple/encodings/oem.py": "https://files.ballistica.net/cache/ba1/c4/d1/d3b5e75a1253f49b7520c897ebfd", + "build/assets/pylib-apple/encodings/palmos.py": "https://files.ballistica.net/cache/ba1/9e/78/4faa9aa5bafe031270780cfc769a", + "build/assets/pylib-apple/encodings/ptcp154.py": "https://files.ballistica.net/cache/ba1/eb/29/8ca043dd400328cc502b5d314a93", + "build/assets/pylib-apple/encodings/punycode.py": "https://files.ballistica.net/cache/ba1/db/e3/df25c06b454fcee1b53ad3dbc371", + "build/assets/pylib-apple/encodings/quopri_codec.py": "https://files.ballistica.net/cache/ba1/b0/ab/236f133a1c818a6a2f443eb8d302", + "build/assets/pylib-apple/encodings/raw_unicode_escape.py": "https://files.ballistica.net/cache/ba1/d0/42/5d492d5edbc5b742677e97b3de92", + "build/assets/pylib-apple/encodings/rot_13.py": "https://files.ballistica.net/cache/ba1/1d/b4/54d65d3ea4b4d2da85e3b1f6dd44", + "build/assets/pylib-apple/encodings/shift_jis.py": "https://files.ballistica.net/cache/ba1/d7/a5/38cd449083f6d7d0da7ea1d11c34", + "build/assets/pylib-apple/encodings/shift_jis_2004.py": "https://files.ballistica.net/cache/ba1/d0/30/763c1150f7b9cb38386e2f5fe5e0", + "build/assets/pylib-apple/encodings/shift_jisx0213.py": "https://files.ballistica.net/cache/ba1/79/7c/55e21d505533067358c0fa24a964", + "build/assets/pylib-apple/encodings/tis_620.py": "https://files.ballistica.net/cache/ba1/67/db/3ca8484202ef22809626b9de6dbc", + "build/assets/pylib-apple/encodings/undefined.py": "https://files.ballistica.net/cache/ba1/c8/4a/17c9ad9cd3e38516d3299ef1cf49", + "build/assets/pylib-apple/encodings/unicode_escape.py": "https://files.ballistica.net/cache/ba1/91/37/83c21f9e8514ebd9e855d4770be2", + "build/assets/pylib-apple/encodings/utf_16.py": "https://files.ballistica.net/cache/ba1/45/a4/4fc814a667480da139187aa5608c", + "build/assets/pylib-apple/encodings/utf_16_be.py": "https://files.ballistica.net/cache/ba1/99/50/79ae98c30dcff85d8b2e75f1cbe3", + "build/assets/pylib-apple/encodings/utf_16_le.py": "https://files.ballistica.net/cache/ba1/8b/0d/ff87c36745e02412dc6e8e4b1caf", + "build/assets/pylib-apple/encodings/utf_32.py": "https://files.ballistica.net/cache/ba1/96/b7/d922f508bdd0740e3c8bba9c0179", + "build/assets/pylib-apple/encodings/utf_32_be.py": "https://files.ballistica.net/cache/ba1/86/22/467586215b9551ac46bde73a7b53", + "build/assets/pylib-apple/encodings/utf_32_le.py": "https://files.ballistica.net/cache/ba1/bf/b8/19ed5403caf19aa7ee5de22737bf", + "build/assets/pylib-apple/encodings/utf_7.py": "https://files.ballistica.net/cache/ba1/2a/b8/a5cfc846f0c7868c26135d7d8ae6", + "build/assets/pylib-apple/encodings/utf_8.py": "https://files.ballistica.net/cache/ba1/f4/a7/c86c4747d9114e098f6021582ff8", + "build/assets/pylib-apple/encodings/utf_8_sig.py": "https://files.ballistica.net/cache/ba1/3d/ed/2a41d258f2a0c13eb6415fe2acc5", + "build/assets/pylib-apple/encodings/uu_codec.py": "https://files.ballistica.net/cache/ba1/b6/9e/69134a3d87739bc2006ba5ea6453", + "build/assets/pylib-apple/encodings/zlib_codec.py": "https://files.ballistica.net/cache/ba1/0a/bd/70ee88c017f969993a048c251b51", + "build/assets/pylib-apple/enum.py": "https://files.ballistica.net/cache/ba1/6d/f4/3e65ad04605b379375058c46d655", + "build/assets/pylib-apple/filecmp.py": "https://files.ballistica.net/cache/ba1/0c/03/be22b2361949206ba161413d8ddc", + "build/assets/pylib-apple/fileinput.py": "https://files.ballistica.net/cache/ba1/71/78/3a798f754042c541a177e9fffd17", + "build/assets/pylib-apple/fnmatch.py": "https://files.ballistica.net/cache/ba1/aa/08/e6cbb9ef4bf5faa28898c5fb76e9", + "build/assets/pylib-apple/fractions.py": "https://files.ballistica.net/cache/ba1/be/ae/18c0b944e984b97c5a36812e39aa", + "build/assets/pylib-apple/ftplib.py": "https://files.ballistica.net/cache/ba1/4b/a6/bfcb52429a9c65fa85c2585533c5", + "build/assets/pylib-apple/functools.py": "https://files.ballistica.net/cache/ba1/44/f2/499e55e6e2c5d47642fe1486e5f0", + "build/assets/pylib-apple/genericpath.py": "https://files.ballistica.net/cache/ba1/83/44/ae09357b9a6038d473c0d73e70d0", + "build/assets/pylib-apple/getopt.py": "https://files.ballistica.net/cache/ba1/a3/b2/298089f7b4d7c0a993e8817dceb4", + "build/assets/pylib-apple/getpass.py": "https://files.ballistica.net/cache/ba1/4f/10/7b211c00ad736c166a8bcc927593", + "build/assets/pylib-apple/gettext.py": "https://files.ballistica.net/cache/ba1/9f/3b/688361fc019e44a6445668e7759f", + "build/assets/pylib-apple/glob.py": "https://files.ballistica.net/cache/ba1/de/64/b7ad568271ffa094685c5f1267c2", + "build/assets/pylib-apple/graphlib.py": "https://files.ballistica.net/cache/ba1/49/65/d98b7a230804f8be38ba8c7955b0", + "build/assets/pylib-apple/gzip.py": "https://files.ballistica.net/cache/ba1/72/2a/84b7bede9d8d1b8e9cc1fa8fb14f", + "build/assets/pylib-apple/hashlib.py": "https://files.ballistica.net/cache/ba1/55/b2/0c6cfcac549f79e020c6baac371a", + "build/assets/pylib-apple/heapq.py": "https://files.ballistica.net/cache/ba1/ea/5d/895a88dcd19de60bff27b6e31f07", + "build/assets/pylib-apple/hmac.py": "https://files.ballistica.net/cache/ba1/c9/ea/fa197872fdd20d4c5fd9a1d9c4ab", + "build/assets/pylib-apple/html/__init__.py": "https://files.ballistica.net/cache/ba1/32/14/de22de0d7d19cc5ddeacf9e724a3", + "build/assets/pylib-apple/html/entities.py": "https://files.ballistica.net/cache/ba1/a4/a2/e5a48cef1dc155f42ce1c57e5e70", + "build/assets/pylib-apple/html/parser.py": "https://files.ballistica.net/cache/ba1/37/69/f9426429e848c5dc2c0f5c1a58f5", + "build/assets/pylib-apple/http/__init__.py": "https://files.ballistica.net/cache/ba1/55/75/2ab3057800f8f5d0f11ecdb63458", + "build/assets/pylib-apple/http/client.py": "https://files.ballistica.net/cache/ba1/c8/db/3007c1316760c88956d229153f76", + "build/assets/pylib-apple/http/cookiejar.py": "https://files.ballistica.net/cache/ba1/8f/58/bfa499ad930b82abd59da93d558e", + "build/assets/pylib-apple/http/cookies.py": "https://files.ballistica.net/cache/ba1/5b/b2/7d4f7a0e1db013853b96ce961726", + "build/assets/pylib-apple/http/server.py": "https://files.ballistica.net/cache/ba1/59/93/5b505c2217f4ab6696c3dbac7682", + "build/assets/pylib-apple/imghdr.py": "https://files.ballistica.net/cache/ba1/c6/e4/a9aec2ad5c2cb4c25fde7f514895", + "build/assets/pylib-apple/imp.py": "https://files.ballistica.net/cache/ba1/ff/35/9f914fffd815bbbecc2dab3d9f92", + "build/assets/pylib-apple/importlib/__init__.py": "https://files.ballistica.net/cache/ba1/33/cd/bf4c0d2cf5940e205e06428d2fb3", + "build/assets/pylib-apple/importlib/_abc.py": "https://files.ballistica.net/cache/ba1/5d/df/779b9f4f29fdb7153f8db67d8f59", + "build/assets/pylib-apple/importlib/_bootstrap.py": "https://files.ballistica.net/cache/ba1/b9/ad/18609eb3ab806b2970bfb3c2c7c9", + "build/assets/pylib-apple/importlib/_bootstrap_external.py": "https://files.ballistica.net/cache/ba1/0a/20/5037e6f6d20be7964bc0c4882985", + "build/assets/pylib-apple/importlib/abc.py": "https://files.ballistica.net/cache/ba1/4a/6f/03750a0802d43a74cf653d4f4695", + "build/assets/pylib-apple/importlib/machinery.py": "https://files.ballistica.net/cache/ba1/0c/d4/49c0627533b501a6b1afa6904dfe", + "build/assets/pylib-apple/importlib/metadata/__init__.py": "https://files.ballistica.net/cache/ba1/ea/b1/70e776f5911181442e5b6abbb06e", + "build/assets/pylib-apple/importlib/metadata/_adapters.py": "https://files.ballistica.net/cache/ba1/7e/1d/2c2f9177168febb20d631d18a4f0", + "build/assets/pylib-apple/importlib/metadata/_collections.py": "https://files.ballistica.net/cache/ba1/86/a4/a2d68a69fd047d1a13746e0e2c49", + "build/assets/pylib-apple/importlib/metadata/_functools.py": "https://files.ballistica.net/cache/ba1/06/b2/8ad3886e0aff681c77392aab42e1", + "build/assets/pylib-apple/importlib/metadata/_itertools.py": "https://files.ballistica.net/cache/ba1/ea/ef/76b8345fa2356663fb282d772ef1", + "build/assets/pylib-apple/importlib/metadata/_meta.py": "https://files.ballistica.net/cache/ba1/39/4f/fa307e369d40270c317e06f1500f", + "build/assets/pylib-apple/importlib/metadata/_text.py": "https://files.ballistica.net/cache/ba1/9b/6f/23aab0396c41b7d3258b7cad55c2", + "build/assets/pylib-apple/importlib/readers.py": "https://files.ballistica.net/cache/ba1/08/c6/96175fc8a7f29823884cfacdb9c6", + "build/assets/pylib-apple/importlib/resources/__init__.py": "https://files.ballistica.net/cache/ba1/23/73/fa33ad408e29588edd848622cf92", + "build/assets/pylib-apple/importlib/resources/_adapters.py": "https://files.ballistica.net/cache/ba1/7d/6a/90b3720bf60ad1a650c7ffe47790", + "build/assets/pylib-apple/importlib/resources/_common.py": "https://files.ballistica.net/cache/ba1/3c/af/290d83077ff789a0b7662fa7a973", + "build/assets/pylib-apple/importlib/resources/_itertools.py": "https://files.ballistica.net/cache/ba1/fe/fd/66369623e156c515c0d64774229a", + "build/assets/pylib-apple/importlib/resources/_legacy.py": "https://files.ballistica.net/cache/ba1/0b/7c/60e346a0d6ce2597309a04bd2bde", + "build/assets/pylib-apple/importlib/resources/abc.py": "https://files.ballistica.net/cache/ba1/b2/8c/4056ac2cf997a18e3bd3ff6e2927", + "build/assets/pylib-apple/importlib/resources/readers.py": "https://files.ballistica.net/cache/ba1/33/9c/193f87cd8a079e164eae47610f47", + "build/assets/pylib-apple/importlib/resources/simple.py": "https://files.ballistica.net/cache/ba1/66/ee/aa35afd8e14aa4edd32e2ed73d2a", + "build/assets/pylib-apple/importlib/simple.py": "https://files.ballistica.net/cache/ba1/5e/46/ca632547028d40229bf52449c77c", + "build/assets/pylib-apple/importlib/util.py": "https://files.ballistica.net/cache/ba1/49/75/574f0b86e9198ad44ec80b39c92b", + "build/assets/pylib-apple/inspect.py": "https://files.ballistica.net/cache/ba1/36/bf/105b9ba50e0efa2608c9218aea2c", + "build/assets/pylib-apple/io.py": "https://files.ballistica.net/cache/ba1/01/29/2304c7652902c84c44da99f6c517", + "build/assets/pylib-apple/ipaddress.py": "https://files.ballistica.net/cache/ba1/e3/46/442e42d3d845c85903ea6bfc8f2e", + "build/assets/pylib-apple/json/__init__.py": "https://files.ballistica.net/cache/ba1/6f/84/b386ae49e407ee85ea54aa157cfa", + "build/assets/pylib-apple/json/decoder.py": "https://files.ballistica.net/cache/ba1/20/dc/cb4ae07c5524c41c9d395c84e81d", + "build/assets/pylib-apple/json/encoder.py": "https://files.ballistica.net/cache/ba1/48/f8/243d565329324ecce2d03eff8838", + "build/assets/pylib-apple/json/scanner.py": "https://files.ballistica.net/cache/ba1/d7/70/c7b4871004e2a563561f93c7d82b", + "build/assets/pylib-apple/json/tool.py": "https://files.ballistica.net/cache/ba1/48/52/4c084bb9e4bbdd13fab01ab3d32a", + "build/assets/pylib-apple/keyword.py": "https://files.ballistica.net/cache/ba1/fc/4a/a7f72adcb7e64cbcd77802403174", + "build/assets/pylib-apple/linecache.py": "https://files.ballistica.net/cache/ba1/99/4a/7527568ad8a363028512822059b8", + "build/assets/pylib-apple/locale.py": "https://files.ballistica.net/cache/ba1/9c/0d/0a41771ad9d9e062b5c39c8af785", + "build/assets/pylib-apple/logging/__init__.py": "https://files.ballistica.net/cache/ba1/8c/6a/8b37c995fe415d911e236c7171fb", + "build/assets/pylib-apple/logging/config.py": "https://files.ballistica.net/cache/ba1/ad/06/7c0c0094d607025f779b9d2bf147", + "build/assets/pylib-apple/logging/handlers.py": "https://files.ballistica.net/cache/ba1/e0/93/d7c9acc411016dc8505fb03eaabc", + "build/assets/pylib-apple/lzma.py": "https://files.ballistica.net/cache/ba1/cd/4c/4028e618c402b9f7c296493c1c85", + "build/assets/pylib-apple/mailbox.py": "https://files.ballistica.net/cache/ba1/ed/90/f232860c293ac05d5edf2bf81d8e", + "build/assets/pylib-apple/mailcap.py": "https://files.ballistica.net/cache/ba1/76/0f/c7f7e390466358536c3917ba7fbe", + "build/assets/pylib-apple/mimetypes.py": "https://files.ballistica.net/cache/ba1/a7/47/3b4cd9aa5c4167fa98348d6a4346", + "build/assets/pylib-apple/modulefinder.py": "https://files.ballistica.net/cache/ba1/19/81/c5bae60023384849c22022853d5e", + "build/assets/pylib-apple/netrc.py": "https://files.ballistica.net/cache/ba1/b0/20/8c33034a7b52f86e4824850bd123", + "build/assets/pylib-apple/nntplib.py": "https://files.ballistica.net/cache/ba1/fd/fc/cadc63bee8c89e98a1a60fd81483", + "build/assets/pylib-apple/ntpath.py": "https://files.ballistica.net/cache/ba1/0a/93/66bd2db410e935426a526efafc7d", + "build/assets/pylib-apple/nturl2path.py": "https://files.ballistica.net/cache/ba1/1b/dc/e092479da52b6922c5b7222b0942", + "build/assets/pylib-apple/numbers.py": "https://files.ballistica.net/cache/ba1/68/65/7e02085137037b2106ac44540228", + "build/assets/pylib-apple/opcode.py": "https://files.ballistica.net/cache/ba1/2c/34/88e3a36749af112c94d51b65aa00", + "build/assets/pylib-apple/operator.py": "https://files.ballistica.net/cache/ba1/03/4e/aefacab00f48e67700c870a81dc7", + "build/assets/pylib-apple/optparse.py": "https://files.ballistica.net/cache/ba1/c5/2b/9271ffc8cf42577cf43ee6992b61", + "build/assets/pylib-apple/os.py": "https://files.ballistica.net/cache/ba1/39/57/e190ebfc4aa7015deb297a53a053", + "build/assets/pylib-apple/pathlib.py": "https://files.ballistica.net/cache/ba1/fa/ca/7c008c0043d8e1ff64f39ac86dd0", + "build/assets/pylib-apple/pdb.py": "https://files.ballistica.net/cache/ba1/17/24/1ba66a7ded2d7fd5dbfb7c5d0a6a", + "build/assets/pylib-apple/pickle.py": "https://files.ballistica.net/cache/ba1/47/c1/e1c92f9aa3cb9ac8ac539840678f", + "build/assets/pylib-apple/pickletools.py": "https://files.ballistica.net/cache/ba1/5d/4c/8b0fd2ffbc696282d253e742ed9c", + "build/assets/pylib-apple/pipes.py": "https://files.ballistica.net/cache/ba1/e2/81/44f45eb3d1ac3cf5516fe660fcac", + "build/assets/pylib-apple/pkgutil.py": "https://files.ballistica.net/cache/ba1/ea/78/6ee2045486f68262e01e57a3a145", + "build/assets/pylib-apple/platform.py": "https://files.ballistica.net/cache/ba1/02/41/d126db358ee9ffdccdb2b58d8ccb", + "build/assets/pylib-apple/plistlib.py": "https://files.ballistica.net/cache/ba1/39/be/5a5414376c906d40cdb05baa3c1a", + "build/assets/pylib-apple/poplib.py": "https://files.ballistica.net/cache/ba1/17/c5/72bfb3fd7c1a65cfdd57b0cd547d", + "build/assets/pylib-apple/posixpath.py": "https://files.ballistica.net/cache/ba1/87/4f/fbf2b5c996bf1809ca0c01bd30e4", + "build/assets/pylib-apple/pprint.py": "https://files.ballistica.net/cache/ba1/2a/b6/12363a8a788002d9603ed821a1fa", + "build/assets/pylib-apple/profile.py": "https://files.ballistica.net/cache/ba1/cc/c3/119b945ccf8765f9b468194773d9", + "build/assets/pylib-apple/pstats.py": "https://files.ballistica.net/cache/ba1/40/07/f043f5d335dbdcc4c6cdc03941a1", + "build/assets/pylib-apple/pty.py": "https://files.ballistica.net/cache/ba1/f9/6e/1cb31a0f40fea526ea2e49ab62cb", + "build/assets/pylib-apple/py_compile.py": "https://files.ballistica.net/cache/ba1/3f/cd/8266cfe2a938fbe565479762967c", + "build/assets/pylib-apple/pyclbr.py": "https://files.ballistica.net/cache/ba1/b9/d0/fed3452bf2a94f2e593a841de023", + "build/assets/pylib-apple/pydoc.py": "https://files.ballistica.net/cache/ba1/eb/82/492c4494eb7552f5ff15b0ef16cd", + "build/assets/pylib-apple/queue.py": "https://files.ballistica.net/cache/ba1/8c/03/03bd9af274b77740503aaa1aea9a", + "build/assets/pylib-apple/quopri.py": "https://files.ballistica.net/cache/ba1/bc/4a/4eca80303439219220c716b6b36a", + "build/assets/pylib-apple/random.py": "https://files.ballistica.net/cache/ba1/dd/2f/c6a8d9fb9e5dfe490e74c906a994", + "build/assets/pylib-apple/re/__init__.py": "https://files.ballistica.net/cache/ba1/4d/0c/ac19f4d6801ceada9e899b1eb25a", + "build/assets/pylib-apple/re/_casefix.py": "https://files.ballistica.net/cache/ba1/ca/8e/9a6bbf6a4f2126c49e628761587a", + "build/assets/pylib-apple/re/_compiler.py": "https://files.ballistica.net/cache/ba1/b7/a6/7ca95ea39b3deb38f4237a642c72", + "build/assets/pylib-apple/re/_constants.py": "https://files.ballistica.net/cache/ba1/bb/ee/3a8ec22cef6968314a3a5e7a747f", + "build/assets/pylib-apple/re/_parser.py": "https://files.ballistica.net/cache/ba1/d7/c5/518c215ffc0509a37bcbb9fbeba0", + "build/assets/pylib-apple/reprlib.py": "https://files.ballistica.net/cache/ba1/c3/a3/381a5559c150b626b6faf9b1f0fc", + "build/assets/pylib-apple/rlcompleter.py": "https://files.ballistica.net/cache/ba1/f6/12/0c9a29ced2e9662c57b0934aa5c3", + "build/assets/pylib-apple/runpy.py": "https://files.ballistica.net/cache/ba1/99/73/cb7de79a675192ff0acbe1e6e5c6", + "build/assets/pylib-apple/sched.py": "https://files.ballistica.net/cache/ba1/3b/20/dbbb99728812cbc7a98241eb3459", + "build/assets/pylib-apple/secrets.py": "https://files.ballistica.net/cache/ba1/bd/54/13395dfe610c5320e066c487d984", + "build/assets/pylib-apple/selectors.py": "https://files.ballistica.net/cache/ba1/5e/f2/6599c264e044dc137249a8d98966", + "build/assets/pylib-apple/shelve.py": "https://files.ballistica.net/cache/ba1/79/97/daf1a632f384e53d93df7d85b968", + "build/assets/pylib-apple/shlex.py": "https://files.ballistica.net/cache/ba1/45/89/e3cba8b776a79b5e62c100ba2881", + "build/assets/pylib-apple/shutil.py": "https://files.ballistica.net/cache/ba1/30/50/c117ca48188a5c4e0cc3fcd86664", + "build/assets/pylib-apple/signal.py": "https://files.ballistica.net/cache/ba1/2e/65/bf956b8b53c01e329f9ae28b6681", + "build/assets/pylib-apple/site.py": "https://files.ballistica.net/cache/ba1/0d/3e/7f5dcba31ff02b5aaab6d7105804", + "build/assets/pylib-apple/smtpd.py": "https://files.ballistica.net/cache/ba1/9f/f0/f2d65d6d5e02b9d9820189f463f6", + "build/assets/pylib-apple/smtplib.py": "https://files.ballistica.net/cache/ba1/26/53/882312ec369f9521a0d272b6c268", + "build/assets/pylib-apple/sndhdr.py": "https://files.ballistica.net/cache/ba1/4f/3d/72e31266abd894037854c76747e3", + "build/assets/pylib-apple/socket.py": "https://files.ballistica.net/cache/ba1/fe/2d/fbc9acb4e736bb41bb3cfcc26aa4", + "build/assets/pylib-apple/socketserver.py": "https://files.ballistica.net/cache/ba1/41/84/93095cce96e01bded86c932128ba", + "build/assets/pylib-apple/sqlite3/__init__.py": "https://files.ballistica.net/cache/ba1/d8/d1/7c80b4f18c7f49057994e134edb5", + "build/assets/pylib-apple/sqlite3/dbapi2.py": "https://files.ballistica.net/cache/ba1/6c/09/171fe9b8f3d07693e53d297dcb9a", + "build/assets/pylib-apple/sqlite3/dump.py": "https://files.ballistica.net/cache/ba1/ca/0c/d4fae4d467bc8136ceee40860f0f", + "build/assets/pylib-apple/sre_compile.py": "https://files.ballistica.net/cache/ba1/22/75/092541083ea44e681d94abe0da40", + "build/assets/pylib-apple/sre_constants.py": "https://files.ballistica.net/cache/ba1/16/39/5c020754b900f36c912e3775a2ae", + "build/assets/pylib-apple/sre_parse.py": "https://files.ballistica.net/cache/ba1/c1/f2/81135dd2d8ac77edeeb0fb69a125", + "build/assets/pylib-apple/ssl.py": "https://files.ballistica.net/cache/ba1/a3/04/0396acbb44ebf5f53e87f7dee4b9", + "build/assets/pylib-apple/stat.py": "https://files.ballistica.net/cache/ba1/29/69/d60626d0a5d8d2611abf7b552fcb", + "build/assets/pylib-apple/statistics.py": "https://files.ballistica.net/cache/ba1/d6/53/e15579c1dad9d8d71ef6cc69af9f", + "build/assets/pylib-apple/string.py": "https://files.ballistica.net/cache/ba1/8a/95/059848b0f4f965b6085fb432cc95", + "build/assets/pylib-apple/stringprep.py": "https://files.ballistica.net/cache/ba1/e5/92/1d26505afd8fa7ab39b96c3d7ea6", + "build/assets/pylib-apple/struct.py": "https://files.ballistica.net/cache/ba1/ac/a3/53766959e368b021f8e013ab6d1e", + "build/assets/pylib-apple/subprocess.py": "https://files.ballistica.net/cache/ba1/69/10/af12445fe9a6619d313917c286cb", + "build/assets/pylib-apple/sunau.py": "https://files.ballistica.net/cache/ba1/ed/a3/1d3106fb8c6d1dc69f14b07d993f", + "build/assets/pylib-apple/symtable.py": "https://files.ballistica.net/cache/ba1/00/f3/2c637f7a8e8541b5707214737be7", + "build/assets/pylib-apple/sysconfig.py": "https://files.ballistica.net/cache/ba1/9a/9d/96261ff34eb64cfccdcafeb460c1", + "build/assets/pylib-apple/tabnanny.py": "https://files.ballistica.net/cache/ba1/58/e2/0c30424b599169f74d4e6d2f3371", + "build/assets/pylib-apple/tarfile.py": "https://files.ballistica.net/cache/ba1/fe/82/85efa7fcd561c3041921fdd7950b", + "build/assets/pylib-apple/telnetlib.py": "https://files.ballistica.net/cache/ba1/fb/cc/c423c7ca43fe6d22308f53623057", + "build/assets/pylib-apple/tempfile.py": "https://files.ballistica.net/cache/ba1/d5/fe/fa22e900262fede184e6ecf45baf", + "build/assets/pylib-apple/textwrap.py": "https://files.ballistica.net/cache/ba1/52/1b/29b249f2a92948503c487e49707a", + "build/assets/pylib-apple/this.py": "https://files.ballistica.net/cache/ba1/ae/8e/569f571572c774d1a897e853ddd2", + "build/assets/pylib-apple/threading.py": "https://files.ballistica.net/cache/ba1/7b/ee/d1e19d5458cc1f8d2c336821a21b", + "build/assets/pylib-apple/timeit.py": "https://files.ballistica.net/cache/ba1/ec/38/ad27fa2c4518233cc57d4ee5c442", + "build/assets/pylib-apple/token.py": "https://files.ballistica.net/cache/ba1/39/cf/39b7c46fbffa4f585c764564f24e", + "build/assets/pylib-apple/tokenize.py": "https://files.ballistica.net/cache/ba1/40/6b/69bf9a95fac7126eb6239309f912", + "build/assets/pylib-apple/tomllib/__init__.py": "https://files.ballistica.net/cache/ba1/5b/5b/647eced87b2954a20fdd42df4af3", + "build/assets/pylib-apple/tomllib/_parser.py": "https://files.ballistica.net/cache/ba1/e9/e8/61b39aef70961ac8ca2690f1fe31", + "build/assets/pylib-apple/tomllib/_re.py": "https://files.ballistica.net/cache/ba1/fe/ac/d009152d20e48244c37862a97456", + "build/assets/pylib-apple/tomllib/_types.py": "https://files.ballistica.net/cache/ba1/43/de/aecefbf8e81a00e11cdda537139c", + "build/assets/pylib-apple/trace.py": "https://files.ballistica.net/cache/ba1/99/05/4061251d0325e586d7e0957fca4f", + "build/assets/pylib-apple/traceback.py": "https://files.ballistica.net/cache/ba1/01/70/3ffa82be7d6e2710e91824283ee1", + "build/assets/pylib-apple/tracemalloc.py": "https://files.ballistica.net/cache/ba1/45/39/2d506bca6ad13582bd82bd7faa3e", + "build/assets/pylib-apple/tty.py": "https://files.ballistica.net/cache/ba1/cc/1b/895c0dfa607feedd2ffdc470b8d9", + "build/assets/pylib-apple/types.py": "https://files.ballistica.net/cache/ba1/1d/16/3ba7f88028393906815ef22b1aac", + "build/assets/pylib-apple/typing.py": "https://files.ballistica.net/cache/ba1/d7/54/6dea9e36e739d16dc2f3064611b2", + "build/assets/pylib-apple/urllib/__init__.py": "https://files.ballistica.net/cache/ba1/62/b5/65af5cd536424dcb96fe531e75ad", + "build/assets/pylib-apple/urllib/error.py": "https://files.ballistica.net/cache/ba1/f2/28/04dde91111fbd80c215ae1d8bd9f", + "build/assets/pylib-apple/urllib/parse.py": "https://files.ballistica.net/cache/ba1/18/16/d7b7b19b7728db0c3f7fe2499fe4", + "build/assets/pylib-apple/urllib/request.py": "https://files.ballistica.net/cache/ba1/81/e9/223a7434765c67cdaa75ec1b76d2", + "build/assets/pylib-apple/urllib/response.py": "https://files.ballistica.net/cache/ba1/ae/97/7e2863452e6f9ffef1bbca660025", + "build/assets/pylib-apple/urllib/robotparser.py": "https://files.ballistica.net/cache/ba1/06/fc/04f384443f19489a022c90b0f66b", + "build/assets/pylib-apple/uu.py": "https://files.ballistica.net/cache/ba1/c3/8c/81a5b7c885d50b924ac141ca336d", + "build/assets/pylib-apple/uuid.py": "https://files.ballistica.net/cache/ba1/8f/2e/c57653bb977b8047a40c07546752", + "build/assets/pylib-apple/warnings.py": "https://files.ballistica.net/cache/ba1/ec/5a/ac149144ce58037bb8995d579645", + "build/assets/pylib-apple/wave.py": "https://files.ballistica.net/cache/ba1/2d/7e/695e7c295e34fec22e5958599601", + "build/assets/pylib-apple/weakref.py": "https://files.ballistica.net/cache/ba1/0b/65/99759b3cd0a255e1030e76046105", + "build/assets/pylib-apple/webbrowser.py": "https://files.ballistica.net/cache/ba1/20/75/dbf9de49a9d90d2b21d100d9519c", + "build/assets/pylib-apple/xdrlib.py": "https://files.ballistica.net/cache/ba1/d3/b1/640c80dd9fb2a3e692dc5de690e0", + "build/assets/pylib-apple/xml/__init__.py": "https://files.ballistica.net/cache/ba1/cb/4a/17f052386e05feb3237acc97c2b0", + "build/assets/pylib-apple/xml/dom/NodeFilter.py": "https://files.ballistica.net/cache/ba1/fb/bd/7108b27d748f761483221993dc4f", + "build/assets/pylib-apple/xml/dom/__init__.py": "https://files.ballistica.net/cache/ba1/de/b8/2189238720c1b38fb525cad4e302", + "build/assets/pylib-apple/xml/dom/domreg.py": "https://files.ballistica.net/cache/ba1/58/47/e7556f90fcd4e52415d72b43c5f2", + "build/assets/pylib-apple/xml/dom/expatbuilder.py": "https://files.ballistica.net/cache/ba1/6d/cc/c8b3d86c58985cdad1a545def8dc", + "build/assets/pylib-apple/xml/dom/minicompat.py": "https://files.ballistica.net/cache/ba1/25/43/c3b20ee4519aa073cee43fca8b91", + "build/assets/pylib-apple/xml/dom/minidom.py": "https://files.ballistica.net/cache/ba1/34/e8/0d604a18109b58cb3bd46f892aa0", + "build/assets/pylib-apple/xml/dom/pulldom.py": "https://files.ballistica.net/cache/ba1/e5/4d/1a278012bf38ac6a65c4491353c5", + "build/assets/pylib-apple/xml/dom/xmlbuilder.py": "https://files.ballistica.net/cache/ba1/1f/d4/e30b5086cdff8a1f917559097a8a", + "build/assets/pylib-apple/xml/etree/ElementInclude.py": "https://files.ballistica.net/cache/ba1/f1/08/16b797bb623ca179a2b3746ab8f0", + "build/assets/pylib-apple/xml/etree/ElementPath.py": "https://files.ballistica.net/cache/ba1/d3/ed/188b60453f0c2a5e708e4fda66ac", + "build/assets/pylib-apple/xml/etree/ElementTree.py": "https://files.ballistica.net/cache/ba1/ce/6e/db109b83e3374294c323b9dfb6fd", + "build/assets/pylib-apple/xml/etree/__init__.py": "https://files.ballistica.net/cache/ba1/34/e1/92476f7867927e4985ec2a2ec0c2", + "build/assets/pylib-apple/xml/etree/cElementTree.py": "https://files.ballistica.net/cache/ba1/dc/b3/e9f76f662952aa0395ffe048fbed", + "build/assets/pylib-apple/xml/parsers/__init__.py": "https://files.ballistica.net/cache/ba1/7b/47/cbd75f4254f35e4a176777c1b7a0", + "build/assets/pylib-apple/xml/parsers/expat.py": "https://files.ballistica.net/cache/ba1/42/e5/96e0f3a602b076ee1c4ca566f728", + "build/assets/pylib-apple/xml/sax/__init__.py": "https://files.ballistica.net/cache/ba1/76/21/5989c8aff2043d0fa836073993e1", + "build/assets/pylib-apple/xml/sax/_exceptions.py": "https://files.ballistica.net/cache/ba1/8f/46/a3fdfa28f34449c20bfc110c31ba", + "build/assets/pylib-apple/xml/sax/expatreader.py": "https://files.ballistica.net/cache/ba1/5a/ca/c9128042344132bcc2eae3ed7b2c", + "build/assets/pylib-apple/xml/sax/handler.py": "https://files.ballistica.net/cache/ba1/4c/e8/3e35359b5a9011b240bcfb40b5ec", + "build/assets/pylib-apple/xml/sax/saxutils.py": "https://files.ballistica.net/cache/ba1/fd/e1/c132a620c895c56923e0be513170", + "build/assets/pylib-apple/xml/sax/xmlreader.py": "https://files.ballistica.net/cache/ba1/ba/db/bf1d97073fdee0b048074acb603d", + "build/assets/pylib-apple/xmlrpc/__init__.py": "https://files.ballistica.net/cache/ba1/7a/a4/025d973f3f7bb19c38b014a2bc4b", + "build/assets/pylib-apple/xmlrpc/client.py": "https://files.ballistica.net/cache/ba1/2e/3d/5a3349e4528f7c57b84ab63947cd", + "build/assets/pylib-apple/xmlrpc/server.py": "https://files.ballistica.net/cache/ba1/a8/99/1a153dd7a17bab4bc34bfa5e7b9d", + "build/assets/pylib-apple/zipapp.py": "https://files.ballistica.net/cache/ba1/35/fc/552e41154bcc922337ed2299d083", + "build/assets/pylib-apple/zipfile.py": "https://files.ballistica.net/cache/ba1/f4/5f/bbcba646d0c397b4a35dd7b86b56", + "build/assets/pylib-apple/zipimport.py": "https://files.ballistica.net/cache/ba1/36/b7/a46907426899e73c56ae46d2e314", + "build/assets/pylib-apple/zoneinfo/__init__.py": "https://files.ballistica.net/cache/ba1/86/b1/20201d445b0b54435890a2957ea1", + "build/assets/pylib-apple/zoneinfo/_common.py": "https://files.ballistica.net/cache/ba1/9e/5e/d2d140e92f155f566ec391a11d6f", + "build/assets/pylib-apple/zoneinfo/_tzpath.py": "https://files.ballistica.net/cache/ba1/ce/cf/839c848b5b1060fe04c4a609b475", + "build/assets/pylib-apple/zoneinfo/_zoneinfo.py": "https://files.ballistica.net/cache/ba1/54/5b/796adf9673253fd3de80140902f1", + "build/assets/windows/Win32/DLLs/_asyncio.pyd": "https://files.ballistica.net/cache/ba1/cd/c2/5e53ae0b05fb09ea4ff94b71d944", + "build/assets/windows/Win32/DLLs/_asyncio_d.pyd": "https://files.ballistica.net/cache/ba1/95/d8/1bb860cfdcc63e7a27497d9bf94e", + "build/assets/windows/Win32/DLLs/_bz2.pyd": "https://files.ballistica.net/cache/ba1/4e/6b/2246d3194b065518ce98010eeeea", + "build/assets/windows/Win32/DLLs/_bz2_d.pyd": "https://files.ballistica.net/cache/ba1/3d/ea/99258e54a0efda5ac88fc13f2a9c", + "build/assets/windows/Win32/DLLs/_ctypes.pyd": "https://files.ballistica.net/cache/ba1/b6/06/2789386aecade0e0c29b163e0b76", + "build/assets/windows/Win32/DLLs/_ctypes_d.pyd": "https://files.ballistica.net/cache/ba1/0e/ee/c7794b0155ee96d3cd3e59a684ad", + "build/assets/windows/Win32/DLLs/_ctypes_test.pyd": "https://files.ballistica.net/cache/ba1/4e/b8/5bfbf41d8fa14688755474a967db", + "build/assets/windows/Win32/DLLs/_ctypes_test_d.pyd": "https://files.ballistica.net/cache/ba1/19/30/df8230cfd27aa2d300e744d32497", + "build/assets/windows/Win32/DLLs/_decimal.pyd": "https://files.ballistica.net/cache/ba1/fb/de/170b2779ea337332324603e441ea", + "build/assets/windows/Win32/DLLs/_decimal_d.pyd": "https://files.ballistica.net/cache/ba1/aa/bf/1a90db10c73585af9dba3e5b47b3", + "build/assets/windows/Win32/DLLs/_elementtree.pyd": "https://files.ballistica.net/cache/ba1/83/95/1433f8ed83cc177dc9e0638fd1a1", + "build/assets/windows/Win32/DLLs/_elementtree_d.pyd": "https://files.ballistica.net/cache/ba1/ed/11/da41cec31e5f7c198c69f4d03588", + "build/assets/windows/Win32/DLLs/_hashlib.pyd": "https://files.ballistica.net/cache/ba1/c3/37/41a82daf444791889a960bccaf48", + "build/assets/windows/Win32/DLLs/_hashlib_d.pyd": "https://files.ballistica.net/cache/ba1/e3/4a/ac2d2f5f1145dc5c34eccd88ee94", + "build/assets/windows/Win32/DLLs/_lzma.pyd": "https://files.ballistica.net/cache/ba1/6c/da/3f21b4bfa4b0e8036d68cf2e5376", + "build/assets/windows/Win32/DLLs/_lzma_d.pyd": "https://files.ballistica.net/cache/ba1/e5/c7/29aaaecbb9386bf88cece90f53bf", + "build/assets/windows/Win32/DLLs/_msi.pyd": "https://files.ballistica.net/cache/ba1/ab/39/1ea917d648e893d4518988788754", + "build/assets/windows/Win32/DLLs/_msi_d.pyd": "https://files.ballistica.net/cache/ba1/f0/38/09bf5ab95e3588d153c80819cfb2", + "build/assets/windows/Win32/DLLs/_multiprocessing.pyd": "https://files.ballistica.net/cache/ba1/bb/41/0b73a8177b09780fde83654858e9", + "build/assets/windows/Win32/DLLs/_multiprocessing_d.pyd": "https://files.ballistica.net/cache/ba1/3a/45/0db0a0e55618f2b9b467f0e683d5", + "build/assets/windows/Win32/DLLs/_overlapped.pyd": "https://files.ballistica.net/cache/ba1/50/3b/8e9bc6073959773d927f184a7b0d", + "build/assets/windows/Win32/DLLs/_overlapped_d.pyd": "https://files.ballistica.net/cache/ba1/c9/36/34ca4953e5982f6e85c9d950a929", + "build/assets/windows/Win32/DLLs/_queue.pyd": "https://files.ballistica.net/cache/ba1/0a/b0/35f2566ba4cc503d12c907744a96", + "build/assets/windows/Win32/DLLs/_queue_d.pyd": "https://files.ballistica.net/cache/ba1/cf/0f/6e3d266975276b0e43260d7f5d7c", + "build/assets/windows/Win32/DLLs/_socket.pyd": "https://files.ballistica.net/cache/ba1/dc/0b/7be6665577d1ed68255245438d95", + "build/assets/windows/Win32/DLLs/_socket_d.pyd": "https://files.ballistica.net/cache/ba1/e4/51/fb4e12f66daf1a327dd9e8a64782", + "build/assets/windows/Win32/DLLs/_sqlite3.pyd": "https://files.ballistica.net/cache/ba1/78/fc/af854dfd8a5c2e1f32783d4493fc", + "build/assets/windows/Win32/DLLs/_sqlite3_d.pyd": "https://files.ballistica.net/cache/ba1/09/c8/d5836de5d01e019aa8af3be2c835", + "build/assets/windows/Win32/DLLs/_ssl.pyd": "https://files.ballistica.net/cache/ba1/f8/b7/d7f024b904ae4c740d54cce95d6d", + "build/assets/windows/Win32/DLLs/_ssl_d.pyd": "https://files.ballistica.net/cache/ba1/53/39/4630f3bb03a8daa04788cecf3e0e", + "build/assets/windows/Win32/DLLs/_testbuffer.pyd": "https://files.ballistica.net/cache/ba1/e9/ca/020424259e7ab2adce2690456a34", + "build/assets/windows/Win32/DLLs/_testbuffer_d.pyd": "https://files.ballistica.net/cache/ba1/d9/2d/1d8177d6b2c47ad5bf4827742f57", + "build/assets/windows/Win32/DLLs/_testcapi.pyd": "https://files.ballistica.net/cache/ba1/e7/42/b30814bfce0977ffdae7297c800e", + "build/assets/windows/Win32/DLLs/_testcapi_d.pyd": "https://files.ballistica.net/cache/ba1/d8/06/7de5c2b7ac509c759c523cdb92a0", + "build/assets/windows/Win32/DLLs/_testconsole.pyd": "https://files.ballistica.net/cache/ba1/7a/8b/af7a09b7e6e3a9bba380f8191d8d", + "build/assets/windows/Win32/DLLs/_testconsole_d.pyd": "https://files.ballistica.net/cache/ba1/d2/fb/65633127961115d22a725432319c", + "build/assets/windows/Win32/DLLs/_testimportmultiple.pyd": "https://files.ballistica.net/cache/ba1/ed/83/8c2e5c77d0af7b572074deb9fba6", + "build/assets/windows/Win32/DLLs/_testimportmultiple_d.pyd": "https://files.ballistica.net/cache/ba1/30/93/1a2a01042b1cbebf09acd6e25412", + "build/assets/windows/Win32/DLLs/_testinternalcapi.pyd": "https://files.ballistica.net/cache/ba1/98/09/802b6190a88fe58ef604c4c93af6", + "build/assets/windows/Win32/DLLs/_testinternalcapi_d.pyd": "https://files.ballistica.net/cache/ba1/79/54/915901bd2c08914b79d500f1dcc2", + "build/assets/windows/Win32/DLLs/_testmultiphase.pyd": "https://files.ballistica.net/cache/ba1/25/bf/6fb58588dc25cf40068ba107af56", + "build/assets/windows/Win32/DLLs/_testmultiphase_d.pyd": "https://files.ballistica.net/cache/ba1/e5/53/e11ffde22e8856d2bc202bde571a", + "build/assets/windows/Win32/DLLs/_tkinter.pyd": "https://files.ballistica.net/cache/ba1/42/88/5de4d844e3968bd1079504beb4fa", + "build/assets/windows/Win32/DLLs/_tkinter_d.lib": "https://files.ballistica.net/cache/ba1/57/4b/14dfe707c0a18a061460845662f2", + "build/assets/windows/Win32/DLLs/_tkinter_d.pyd": "https://files.ballistica.net/cache/ba1/ff/7a/b65f9a0563f1c083f2286f2a7e2e", + "build/assets/windows/Win32/DLLs/_uuid.pyd": "https://files.ballistica.net/cache/ba1/bd/97/f7600ef6476cd286922ea1ab5932", + "build/assets/windows/Win32/DLLs/_uuid_d.pyd": "https://files.ballistica.net/cache/ba1/95/8f/8548d20f41d6a78adc25fb34bc87", + "build/assets/windows/Win32/DLLs/_zoneinfo.pyd": "https://files.ballistica.net/cache/ba1/a4/af/35350a062b3d0ea4fccc1d9d0249", + "build/assets/windows/Win32/DLLs/_zoneinfo_d.pyd": "https://files.ballistica.net/cache/ba1/ac/5b/09d46278587cbf146f9ffbe55ad7", + "build/assets/windows/Win32/DLLs/libcrypto-1_1.dll": "https://files.ballistica.net/cache/ba1/84/59/f59e6a074d54ab909c66f7f13a28", + "build/assets/windows/Win32/DLLs/libffi-8.dll": "https://files.ballistica.net/cache/ba1/62/d5/87bdfc426241eac132884282a10c", + "build/assets/windows/Win32/DLLs/libssl-1_1.dll": "https://files.ballistica.net/cache/ba1/22/1f/be9f67e0eb31db6600b432eaa89b", + "build/assets/windows/Win32/DLLs/pyexpat.pyd": "https://files.ballistica.net/cache/ba1/f6/3f/a65d8ab224e6e181639d8bbfa768", + "build/assets/windows/Win32/DLLs/pyexpat_d.pyd": "https://files.ballistica.net/cache/ba1/c9/d2/6c1d8a3fa669c7cd0e56743c7cbf", + "build/assets/windows/Win32/DLLs/python_lib.cat": "https://files.ballistica.net/cache/ba1/29/aa/f44be08758a522e3459fef09368d", + "build/assets/windows/Win32/DLLs/python_tools.cat": "https://files.ballistica.net/cache/ba1/b3/e6/90904619c68a528df2b3247e58ad", + "build/assets/windows/Win32/DLLs/select.pyd": "https://files.ballistica.net/cache/ba1/32/d2/52b82098a220c4024b0e4c1c6219", + "build/assets/windows/Win32/DLLs/select_d.pyd": "https://files.ballistica.net/cache/ba1/77/39/8c6474a19e2e06b8d127cb2ac238", + "build/assets/windows/Win32/DLLs/sqlite3.dll": "https://files.ballistica.net/cache/ba1/9c/15/bc88a5d4983a6cbc085b8dc5ef78", + "build/assets/windows/Win32/DLLs/sqlite3_d.dll": "https://files.ballistica.net/cache/ba1/8d/b1/c2c97464e28a2b9c8f3fc7e8334e", + "build/assets/windows/Win32/DLLs/tcl86t.dll": "https://files.ballistica.net/cache/ba1/f5/05/98a8f87528d619774d91ead2abfe", + "build/assets/windows/Win32/DLLs/tk86t.dll": "https://files.ballistica.net/cache/ba1/67/ab/1fd3383291c10cf287e98127bee2", + "build/assets/windows/Win32/DLLs/unicodedata.pyd": "https://files.ballistica.net/cache/ba1/2c/2a/16d071c8385bf6e08d4b5d77cad5", + "build/assets/windows/Win32/DLLs/unicodedata_d.pyd": "https://files.ballistica.net/cache/ba1/40/4c/a605842f8b2751d10026caec98f3", + "build/assets/windows/Win32/DLLs/winsound.pyd": "https://files.ballistica.net/cache/ba1/c6/4e/c3403226f6ccaf1876360fde41cc", + "build/assets/windows/Win32/DLLs/winsound_d.pyd": "https://files.ballistica.net/cache/ba1/05/b0/de2c035b8e8849fe998d97937780", + "build/assets/windows/Win32/Lib/__future__.py": "https://files.ballistica.net/cache/ba1/6b/00/c49b7c519511a282dd876e7761cb", + "build/assets/windows/Win32/Lib/__hello__.py": "https://files.ballistica.net/cache/ba1/86/4b/46d2d7afa24fd91bccff91dc19eb", + "build/assets/windows/Win32/Lib/_aix_support.py": "https://files.ballistica.net/cache/ba1/d8/37/7f4b09e1e00caca436a30e1a7b47", + "build/assets/windows/Win32/Lib/_bootsubprocess.py": "https://files.ballistica.net/cache/ba1/7d/fe/6f98c190e3b5d2355c96ffafb84d", + "build/assets/windows/Win32/Lib/_collections_abc.py": "https://files.ballistica.net/cache/ba1/6c/00/a552369f050ed59e49fcd68c7494", + "build/assets/windows/Win32/Lib/_compat_pickle.py": "https://files.ballistica.net/cache/ba1/b5/bf/110a0b84f35be7de1388d826114a", + "build/assets/windows/Win32/Lib/_compression.py": "https://files.ballistica.net/cache/ba1/6d/d1/39efb99ef34b58cd8f39524325e1", + "build/assets/windows/Win32/Lib/_markupbase.py": "https://files.ballistica.net/cache/ba1/df/9a/53920f05402bf3363d2578417327", + "build/assets/windows/Win32/Lib/_osx_support.py": "https://files.ballistica.net/cache/ba1/8d/f3/0190c46b7d967ac1db943712d19f", + "build/assets/windows/Win32/Lib/_py_abc.py": "https://files.ballistica.net/cache/ba1/43/91/29c3d440f1bba5bc7cecc72e04fd", + "build/assets/windows/Win32/Lib/_pydecimal.py": "https://files.ballistica.net/cache/ba1/87/59/d49cec8cf1843c9a863e649a4141", + "build/assets/windows/Win32/Lib/_pyio.py": "https://files.ballistica.net/cache/ba1/50/12/568efb82f8f5e7bdee205d974e71", + "build/assets/windows/Win32/Lib/_sitebuiltins.py": "https://files.ballistica.net/cache/ba1/16/c3/2d3e8497087edc5f170386a8c7a2", + "build/assets/windows/Win32/Lib/_strptime.py": "https://files.ballistica.net/cache/ba1/35/ea/342528b4da3337bbe20f5903833e", + "build/assets/windows/Win32/Lib/_threading_local.py": "https://files.ballistica.net/cache/ba1/b0/0f/bccd89d32a39b28f3ec8ec0c82db", + "build/assets/windows/Win32/Lib/_weakrefset.py": "https://files.ballistica.net/cache/ba1/d7/8b/383d6e769dd80f2c2d32a862f675", + "build/assets/windows/Win32/Lib/abc.py": "https://files.ballistica.net/cache/ba1/f6/05/4ee13d7ac1f09456b8294c4aa23b", + "build/assets/windows/Win32/Lib/aifc.py": "https://files.ballistica.net/cache/ba1/81/3e/79ddb8f2c0a299fc32e3bdc27b6f", + "build/assets/windows/Win32/Lib/antigravity.py": "https://files.ballistica.net/cache/ba1/98/18/1f95b170664cd921b3d412eda241", + "build/assets/windows/Win32/Lib/argparse.py": "https://files.ballistica.net/cache/ba1/6b/c5/31775abf64c6550d445a20a491a3", + "build/assets/windows/Win32/Lib/ast.py": "https://files.ballistica.net/cache/ba1/77/30/22e7e0b69ea53042ff6fce1b2d39", + "build/assets/windows/Win32/Lib/asynchat.py": "https://files.ballistica.net/cache/ba1/af/33/990d5f83cb6d3ade6effd9340fae", + "build/assets/windows/Win32/Lib/asyncio/__init__.py": "https://files.ballistica.net/cache/ba1/1b/42/f26170e14e26b5f44a46380303f9", + "build/assets/windows/Win32/Lib/asyncio/__main__.py": "https://files.ballistica.net/cache/ba1/37/e1/fbdb45033041b5ae63b60bd19a4c", + "build/assets/windows/Win32/Lib/asyncio/base_events.py": "https://files.ballistica.net/cache/ba1/80/cb/577a6dee12cf565679584d9df203", + "build/assets/windows/Win32/Lib/asyncio/base_futures.py": "https://files.ballistica.net/cache/ba1/fe/cb/f22287b48cf4b5e52131ef21a0a5", + "build/assets/windows/Win32/Lib/asyncio/base_subprocess.py": "https://files.ballistica.net/cache/ba1/b6/b5/cd633f72bec1e3e14cb57e78514f", + "build/assets/windows/Win32/Lib/asyncio/base_tasks.py": "https://files.ballistica.net/cache/ba1/cf/7a/655c14a759c3a942f63bdd68e4e6", + "build/assets/windows/Win32/Lib/asyncio/constants.py": "https://files.ballistica.net/cache/ba1/77/62/b6a951c11dac04231e613b962df7", + "build/assets/windows/Win32/Lib/asyncio/coroutines.py": "https://files.ballistica.net/cache/ba1/7e/7c/ee98a7e2f16c35b0f8e7a886ff21", + "build/assets/windows/Win32/Lib/asyncio/events.py": "https://files.ballistica.net/cache/ba1/fc/92/c4634681fa1452c8bc9ab1b4ae10", + "build/assets/windows/Win32/Lib/asyncio/exceptions.py": "https://files.ballistica.net/cache/ba1/a5/a8/8e49f5376755662c1a800a3586be", + "build/assets/windows/Win32/Lib/asyncio/format_helpers.py": "https://files.ballistica.net/cache/ba1/57/47/07a6cfe26d6ac4000fd89183521d", + "build/assets/windows/Win32/Lib/asyncio/futures.py": "https://files.ballistica.net/cache/ba1/3d/5a/54ba0b6734ff5300f9009e956dca", + "build/assets/windows/Win32/Lib/asyncio/locks.py": "https://files.ballistica.net/cache/ba1/f2/22/5cbc9288c0988f5623e3bc481d5e", + "build/assets/windows/Win32/Lib/asyncio/log.py": "https://files.ballistica.net/cache/ba1/e0/42/7b4051adce1ebd7b0273bdb83e60", + "build/assets/windows/Win32/Lib/asyncio/mixins.py": "https://files.ballistica.net/cache/ba1/e9/10/9b991bda62c832aa2fc05c470727", + "build/assets/windows/Win32/Lib/asyncio/proactor_events.py": "https://files.ballistica.net/cache/ba1/e5/ec/4673bd7a39dd4886f1d99c81b637", + "build/assets/windows/Win32/Lib/asyncio/protocols.py": "https://files.ballistica.net/cache/ba1/d2/d3/22672be271d41cbfef87b2b6f11c", + "build/assets/windows/Win32/Lib/asyncio/queues.py": "https://files.ballistica.net/cache/ba1/c4/69/bd2e8161de689e99284719a99323", + "build/assets/windows/Win32/Lib/asyncio/runners.py": "https://files.ballistica.net/cache/ba1/20/d2/349fc3e2a4c57ec2b8a91bb4467e", + "build/assets/windows/Win32/Lib/asyncio/selector_events.py": "https://files.ballistica.net/cache/ba1/e6/e2/0d3465c66774a9b65136427759e6", + "build/assets/windows/Win32/Lib/asyncio/sslproto.py": "https://files.ballistica.net/cache/ba1/3f/16/21406c634207458d3b049104670c", + "build/assets/windows/Win32/Lib/asyncio/staggered.py": "https://files.ballistica.net/cache/ba1/d6/ca/d17d07a3f924c93dd226aa25ffe9", + "build/assets/windows/Win32/Lib/asyncio/streams.py": "https://files.ballistica.net/cache/ba1/df/35/9bdf4359a1d765bd656bb8195b05", + "build/assets/windows/Win32/Lib/asyncio/subprocess.py": "https://files.ballistica.net/cache/ba1/71/62/b4a6d837a8754eea955b4ee81f2b", + "build/assets/windows/Win32/Lib/asyncio/taskgroups.py": "https://files.ballistica.net/cache/ba1/41/74/8edb83029af3cbacd221b00aecff", + "build/assets/windows/Win32/Lib/asyncio/tasks.py": "https://files.ballistica.net/cache/ba1/3f/e7/e5e98b5b913916cbfc0f3c7bdd45", + "build/assets/windows/Win32/Lib/asyncio/threads.py": "https://files.ballistica.net/cache/ba1/46/5b/4a2bda605e3a90cb01d206a162fe", + "build/assets/windows/Win32/Lib/asyncio/timeouts.py": "https://files.ballistica.net/cache/ba1/20/a5/2d1355e9d2a8d6a251e22dea7064", + "build/assets/windows/Win32/Lib/asyncio/transports.py": "https://files.ballistica.net/cache/ba1/f9/22/1954b7ef40714b2e17e227427d80", + "build/assets/windows/Win32/Lib/asyncio/trsock.py": "https://files.ballistica.net/cache/ba1/11/f0/c23d72ef4b5ae8f8f355d3f7f6f9", + "build/assets/windows/Win32/Lib/asyncio/unix_events.py": "https://files.ballistica.net/cache/ba1/f8/41/74139ababb3f8c8559b0aaa99552", + "build/assets/windows/Win32/Lib/asyncio/windows_events.py": "https://files.ballistica.net/cache/ba1/55/ed/fa12d38b6a0a24a5d79593086404", + "build/assets/windows/Win32/Lib/asyncio/windows_utils.py": "https://files.ballistica.net/cache/ba1/5a/7b/0f2ee904cbb9bdb05acea92da346", + "build/assets/windows/Win32/Lib/asyncore.py": "https://files.ballistica.net/cache/ba1/fc/0b/8a496ada76ef448ae6f3f163b1d7", + "build/assets/windows/Win32/Lib/base64.py": "https://files.ballistica.net/cache/ba1/a3/8f/c40fe061c55d4af7d8b71e449a14", + "build/assets/windows/Win32/Lib/bdb.py": "https://files.ballistica.net/cache/ba1/ce/72/c8d563aef5b1f27832fb67cac59f", + "build/assets/windows/Win32/Lib/bisect.py": "https://files.ballistica.net/cache/ba1/7c/d6/7f78945500ea8cfbad7fd9b1360f", + "build/assets/windows/Win32/Lib/bz2.py": "https://files.ballistica.net/cache/ba1/9b/60/e576cfe7b85159b40dce0a102b20", + "build/assets/windows/Win32/Lib/cProfile.py": "https://files.ballistica.net/cache/ba1/d6/e2/1a67b45e502cd1d0c470f931c94e", + "build/assets/windows/Win32/Lib/calendar.py": "https://files.ballistica.net/cache/ba1/f6/4a/224ba4ae62c2658013d87aa015c0", + "build/assets/windows/Win32/Lib/cgi.py": "https://files.ballistica.net/cache/ba1/39/f8/cb1ae1db248a859a598324002447", + "build/assets/windows/Win32/Lib/cgitb.py": "https://files.ballistica.net/cache/ba1/57/67/c187e740ae9b986755c078e45091", + "build/assets/windows/Win32/Lib/chunk.py": "https://files.ballistica.net/cache/ba1/39/02/60861f5db7ef763567ff78726c77", + "build/assets/windows/Win32/Lib/cmd.py": "https://files.ballistica.net/cache/ba1/89/74/b5d654a01b6ad2477d4b20410f5b", + "build/assets/windows/Win32/Lib/code.py": "https://files.ballistica.net/cache/ba1/9c/1f/5e23ab3361ed5154f3354532ed9f", + "build/assets/windows/Win32/Lib/codecs.py": "https://files.ballistica.net/cache/ba1/6a/79/062528023d8e4a62d2d5f0bb26fe", + "build/assets/windows/Win32/Lib/codeop.py": "https://files.ballistica.net/cache/ba1/bc/9b/428910b7cde0ed3bf96038438c58", + "build/assets/windows/Win32/Lib/collections/__init__.py": "https://files.ballistica.net/cache/ba1/3a/73/0b12fc9b7fb79d40d9748df5ba08", + "build/assets/windows/Win32/Lib/collections/abc.py": "https://files.ballistica.net/cache/ba1/ee/3d/356c3fb36b907b3786d3f4b7b2cb", + "build/assets/windows/Win32/Lib/colorsys.py": "https://files.ballistica.net/cache/ba1/a4/be/6d12c63a17ea74dd7321e9362abf", + "build/assets/windows/Win32/Lib/compileall.py": "https://files.ballistica.net/cache/ba1/10/b8/77c8ef03cdc403d4224bfa227f60", + "build/assets/windows/Win32/Lib/concurrent/__init__.py": "https://files.ballistica.net/cache/ba1/3b/d4/13d3aaf3d7aebd985b2d523f9b05", + "build/assets/windows/Win32/Lib/concurrent/futures/__init__.py": "https://files.ballistica.net/cache/ba1/8a/4e/4ca1f0e16f245025eaebb999e044", + "build/assets/windows/Win32/Lib/concurrent/futures/_base.py": "https://files.ballistica.net/cache/ba1/b0/ae/abe7123e3544410985a972ea5f2a", + "build/assets/windows/Win32/Lib/concurrent/futures/process.py": "https://files.ballistica.net/cache/ba1/02/c0/0277c07df4a5b8be0f8bf94f40cf", + "build/assets/windows/Win32/Lib/concurrent/futures/thread.py": "https://files.ballistica.net/cache/ba1/8e/fc/602dc723a4f3d4b63e03fb549144", + "build/assets/windows/Win32/Lib/configparser.py": "https://files.ballistica.net/cache/ba1/86/a3/ea5af1abc064a24b5203d6a45af0", + "build/assets/windows/Win32/Lib/contextlib.py": "https://files.ballistica.net/cache/ba1/d9/ff/e9f44996416fc0f0529c19ff0dee", + "build/assets/windows/Win32/Lib/contextvars.py": "https://files.ballistica.net/cache/ba1/3b/2c/2a178cce209bd89cd6b12c6bf934", + "build/assets/windows/Win32/Lib/copy.py": "https://files.ballistica.net/cache/ba1/eb/78/7240820d40c49960b4d0e0fe565a", + "build/assets/windows/Win32/Lib/copyreg.py": "https://files.ballistica.net/cache/ba1/b4/7f/a7a0b30cb2b3407e67b73734e80d", + "build/assets/windows/Win32/Lib/crypt.py": "https://files.ballistica.net/cache/ba1/b1/bc/dd3023b6281cc3fcabce0343e95c", + "build/assets/windows/Win32/Lib/csv.py": "https://files.ballistica.net/cache/ba1/1e/d8/a18bc8af9f321c95362ad1c71d8f", + "build/assets/windows/Win32/Lib/ctypes/__init__.py": "https://files.ballistica.net/cache/ba1/dd/90/648c91924935fa738543c8456247", + "build/assets/windows/Win32/Lib/ctypes/_aix.py": "https://files.ballistica.net/cache/ba1/c6/a3/6b19ec1193ea7e96587958ab9779", + "build/assets/windows/Win32/Lib/ctypes/_endian.py": "https://files.ballistica.net/cache/ba1/1a/44/6625fa4190e305c5abe346fd13d5", + "build/assets/windows/Win32/Lib/ctypes/macholib/__init__.py": "https://files.ballistica.net/cache/ba1/85/ae/8a88ff9e951ee18b0cf60ef62ada", + "build/assets/windows/Win32/Lib/ctypes/macholib/dyld.py": "https://files.ballistica.net/cache/ba1/4a/f9/6ce4b98a04a07317ee2e196fc43d", + "build/assets/windows/Win32/Lib/ctypes/macholib/dylib.py": "https://files.ballistica.net/cache/ba1/d2/6f/a6c2aaa55baacf2a388103fd5507", + "build/assets/windows/Win32/Lib/ctypes/macholib/framework.py": "https://files.ballistica.net/cache/ba1/58/ff/644855f70548088dbad370ccedde", + "build/assets/windows/Win32/Lib/ctypes/util.py": "https://files.ballistica.net/cache/ba1/94/1c/e5ed7d85473ea21d1a981c2dea62", + "build/assets/windows/Win32/Lib/ctypes/wintypes.py": "https://files.ballistica.net/cache/ba1/6d/6e/bb8b182340a1b669a2250c61926f", + "build/assets/windows/Win32/Lib/curses/__init__.py": "https://files.ballistica.net/cache/ba1/5f/d6/df09df30472647095d9233107f40", + "build/assets/windows/Win32/Lib/curses/ascii.py": "https://files.ballistica.net/cache/ba1/2b/54/e76ec11362e874b4e5d737d1af7f", + "build/assets/windows/Win32/Lib/curses/has_key.py": "https://files.ballistica.net/cache/ba1/97/28/1faf82b121f75d54e5e2f3f3d739", + "build/assets/windows/Win32/Lib/curses/panel.py": "https://files.ballistica.net/cache/ba1/73/a8/a35b8b3f88abf9da8ad8444d0baa", + "build/assets/windows/Win32/Lib/curses/textpad.py": "https://files.ballistica.net/cache/ba1/f3/4a/d5c35fae65400667a6d0d1ebd3ae", + "build/assets/windows/Win32/Lib/dataclasses.py": "https://files.ballistica.net/cache/ba1/4c/dc/4610faa5caf962dfccea3412a40d", + "build/assets/windows/Win32/Lib/datetime.py": "https://files.ballistica.net/cache/ba1/9e/82/449d1de863e36020c66f792368d5", + "build/assets/windows/Win32/Lib/decimal.py": "https://files.ballistica.net/cache/ba1/c5/f0/8006683130a8c5498f72c5e90bf6", + "build/assets/windows/Win32/Lib/difflib.py": "https://files.ballistica.net/cache/ba1/5e/24/24529f6d5a4825af1a6237d52153", + "build/assets/windows/Win32/Lib/dis.py": "https://files.ballistica.net/cache/ba1/a9/97/6cd704a4ee7d4e66a1d74e58fa23", + "build/assets/windows/Win32/Lib/doctest.py": "https://files.ballistica.net/cache/ba1/b8/53/65773a110217436085a2483648a6", + "build/assets/windows/Win32/Lib/email/__init__.py": "https://files.ballistica.net/cache/ba1/f4/38/3416aac09ae2cf762ab5141436e0", + "build/assets/windows/Win32/Lib/email/_encoded_words.py": "https://files.ballistica.net/cache/ba1/56/76/d1e4cdea8cfb61823a416ab9e5d9", + "build/assets/windows/Win32/Lib/email/_header_value_parser.py": "https://files.ballistica.net/cache/ba1/e1/9c/5d993e3987c8de1f845396814a2c", + "build/assets/windows/Win32/Lib/email/_parseaddr.py": "https://files.ballistica.net/cache/ba1/15/6c/5a5bc0f17972fe43070a073fa220", + "build/assets/windows/Win32/Lib/email/_policybase.py": "https://files.ballistica.net/cache/ba1/e0/a4/45f7e774ffa554ee855d23128046", + "build/assets/windows/Win32/Lib/email/architecture.rst": "https://files.ballistica.net/cache/ba1/63/e6/2f14d3d49c3fd5853510eba22e69", + "build/assets/windows/Win32/Lib/email/base64mime.py": "https://files.ballistica.net/cache/ba1/d6/e9/72e37c42998766edd315316d3211", + "build/assets/windows/Win32/Lib/email/charset.py": "https://files.ballistica.net/cache/ba1/bf/92/99425a3bf754ca14474a86e6aea2", + "build/assets/windows/Win32/Lib/email/contentmanager.py": "https://files.ballistica.net/cache/ba1/36/dd/6dac89fe89366521e4e1d80890c6", + "build/assets/windows/Win32/Lib/email/encoders.py": "https://files.ballistica.net/cache/ba1/fd/6e/f52d465e046fbd8a12451c18c21d", + "build/assets/windows/Win32/Lib/email/errors.py": "https://files.ballistica.net/cache/ba1/7a/ac/bd473c3a224764f7692369598e8f", + "build/assets/windows/Win32/Lib/email/feedparser.py": "https://files.ballistica.net/cache/ba1/00/50/a0e748ffccf6ebf37415dfd75b47", + "build/assets/windows/Win32/Lib/email/generator.py": "https://files.ballistica.net/cache/ba1/2e/e4/a3d5447dfc53d19e872e836b50d2", + "build/assets/windows/Win32/Lib/email/header.py": "https://files.ballistica.net/cache/ba1/16/42/e0abe3c43df699f7bb9d64f22d6f", + "build/assets/windows/Win32/Lib/email/headerregistry.py": "https://files.ballistica.net/cache/ba1/9d/20/89da7c35a4c53b3816a967acc475", + "build/assets/windows/Win32/Lib/email/iterators.py": "https://files.ballistica.net/cache/ba1/95/66/8a2076bec3753756f4e2ed89686f", + "build/assets/windows/Win32/Lib/email/message.py": "https://files.ballistica.net/cache/ba1/09/59/612e5c522aabf3fcad55b7af8e33", + "build/assets/windows/Win32/Lib/email/mime/__init__.py": "https://files.ballistica.net/cache/ba1/10/8f/8733420290ad331d78f37a853d65", + "build/assets/windows/Win32/Lib/email/mime/application.py": "https://files.ballistica.net/cache/ba1/66/bd/85c3de50dc71f64b314aae69f93a", + "build/assets/windows/Win32/Lib/email/mime/audio.py": "https://files.ballistica.net/cache/ba1/62/00/cf0067fc0349fece4eb08ba59f80", + "build/assets/windows/Win32/Lib/email/mime/base.py": "https://files.ballistica.net/cache/ba1/34/b7/35135cc7aa66fef8e78d1842af67", + "build/assets/windows/Win32/Lib/email/mime/image.py": "https://files.ballistica.net/cache/ba1/4f/31/94e7a30b422941d8b966a6436fbd", + "build/assets/windows/Win32/Lib/email/mime/message.py": "https://files.ballistica.net/cache/ba1/fd/d5/5f321f98416cf66f0ef93ee270c6", + "build/assets/windows/Win32/Lib/email/mime/multipart.py": "https://files.ballistica.net/cache/ba1/20/55/cd9c2cefb3e17610c09340414f7d", + "build/assets/windows/Win32/Lib/email/mime/nonmultipart.py": "https://files.ballistica.net/cache/ba1/c8/95/7609cc961cc7d3fdedee83658d1a", + "build/assets/windows/Win32/Lib/email/mime/text.py": "https://files.ballistica.net/cache/ba1/ab/9d/2a9dfe1cd17feb88530d0afc7ba4", + "build/assets/windows/Win32/Lib/email/parser.py": "https://files.ballistica.net/cache/ba1/ab/40/9463af2f8e857f7ef6248262ed16", + "build/assets/windows/Win32/Lib/email/policy.py": "https://files.ballistica.net/cache/ba1/66/50/2cee477b4e97285575401942720f", + "build/assets/windows/Win32/Lib/email/quoprimime.py": "https://files.ballistica.net/cache/ba1/26/5a/9a8e8e195e5621599d850eb8edd6", + "build/assets/windows/Win32/Lib/email/utils.py": "https://files.ballistica.net/cache/ba1/97/83/d6f90e21d99435ffe6e4e528b0a0", + "build/assets/windows/Win32/Lib/encodings/__init__.py": "https://files.ballistica.net/cache/ba1/7f/3b/f55537edc282df60a4f6eeaf69b3", + "build/assets/windows/Win32/Lib/encodings/aliases.py": "https://files.ballistica.net/cache/ba1/bc/87/5140b47e884d7ee5ab4be5992292", + "build/assets/windows/Win32/Lib/encodings/ascii.py": "https://files.ballistica.net/cache/ba1/f1/bb/3984a7eee0dcbb01c8b948c15c6e", + "build/assets/windows/Win32/Lib/encodings/base64_codec.py": "https://files.ballistica.net/cache/ba1/c1/d5/a073b5321ad2708d770e843089b0", + "build/assets/windows/Win32/Lib/encodings/big5.py": "https://files.ballistica.net/cache/ba1/80/37/645ef63b0590cbede4777195772e", + "build/assets/windows/Win32/Lib/encodings/big5hkscs.py": "https://files.ballistica.net/cache/ba1/5f/48/ba205d64c83cbdf9b66d40a64594", + "build/assets/windows/Win32/Lib/encodings/bz2_codec.py": "https://files.ballistica.net/cache/ba1/07/8a/48091c672ae2d2815ee4a8a146ee", + "build/assets/windows/Win32/Lib/encodings/charmap.py": "https://files.ballistica.net/cache/ba1/2a/a8/5f93a84c9c8d9839acb7bd866cae", + "build/assets/windows/Win32/Lib/encodings/cp037.py": "https://files.ballistica.net/cache/ba1/8c/bb/bf0b46b9da09f370115709009f58", + "build/assets/windows/Win32/Lib/encodings/cp1006.py": "https://files.ballistica.net/cache/ba1/0c/64/4c098d86f5bf584ce58e1d911abb", + "build/assets/windows/Win32/Lib/encodings/cp1026.py": "https://files.ballistica.net/cache/ba1/20/ad/9b79b62fa44faf12cbf34dc7deea", + "build/assets/windows/Win32/Lib/encodings/cp1125.py": "https://files.ballistica.net/cache/ba1/a5/57/dbcacab17322dd3692344610c0a2", + "build/assets/windows/Win32/Lib/encodings/cp1140.py": "https://files.ballistica.net/cache/ba1/53/05/8070677c4c400bd7a7f66760b2ae", + "build/assets/windows/Win32/Lib/encodings/cp1250.py": "https://files.ballistica.net/cache/ba1/18/bf/e95a7d6f4da8c477a40ef8a3e3df", + "build/assets/windows/Win32/Lib/encodings/cp1251.py": "https://files.ballistica.net/cache/ba1/58/ff/38971efa38018ca14ab800b35e0d", + "build/assets/windows/Win32/Lib/encodings/cp1252.py": "https://files.ballistica.net/cache/ba1/91/d5/dd59658e917532cd509e7d9bd116", + "build/assets/windows/Win32/Lib/encodings/cp1253.py": "https://files.ballistica.net/cache/ba1/fe/a0/52fce210be36dcd2bcfaeb0c5e89", + "build/assets/windows/Win32/Lib/encodings/cp1254.py": "https://files.ballistica.net/cache/ba1/c0/2e/3c2d71995cd71e0c6bc979d1e4a4", + "build/assets/windows/Win32/Lib/encodings/cp1255.py": "https://files.ballistica.net/cache/ba1/ef/f9/a4db04166db1393f9ac5f3518a6f", + "build/assets/windows/Win32/Lib/encodings/cp1256.py": "https://files.ballistica.net/cache/ba1/36/cd/e3da78b12acdccbcf0d28877c06d", + "build/assets/windows/Win32/Lib/encodings/cp1257.py": "https://files.ballistica.net/cache/ba1/64/d7/26aab866979fe2ee326cc7e1d467", + "build/assets/windows/Win32/Lib/encodings/cp1258.py": "https://files.ballistica.net/cache/ba1/f1/5c/7e2b1933b24f8f8f473eb641e517", + "build/assets/windows/Win32/Lib/encodings/cp273.py": "https://files.ballistica.net/cache/ba1/88/d8/82649f4a49533c1a5044130ce41d", + "build/assets/windows/Win32/Lib/encodings/cp424.py": "https://files.ballistica.net/cache/ba1/f4/67/b6f868260912da6bbc1e1bebddbc", + "build/assets/windows/Win32/Lib/encodings/cp437.py": "https://files.ballistica.net/cache/ba1/ec/24/e877060ed1fef592f6edfa962825", + "build/assets/windows/Win32/Lib/encodings/cp500.py": "https://files.ballistica.net/cache/ba1/b5/4c/5cda7107d446baf72596f771054a", + "build/assets/windows/Win32/Lib/encodings/cp720.py": "https://files.ballistica.net/cache/ba1/95/fe/f7b6cc9d1ebf1d0d61094eee0638", + "build/assets/windows/Win32/Lib/encodings/cp737.py": "https://files.ballistica.net/cache/ba1/c7/09/67cdb189d874ff7df7f5048078a3", + "build/assets/windows/Win32/Lib/encodings/cp775.py": "https://files.ballistica.net/cache/ba1/da/ac/36ba47b56ec8f7c4100ab3265007", + "build/assets/windows/Win32/Lib/encodings/cp850.py": "https://files.ballistica.net/cache/ba1/4e/cf/6267c5dcb982e8577fe5186260d7", + "build/assets/windows/Win32/Lib/encodings/cp852.py": "https://files.ballistica.net/cache/ba1/45/81/e2c8e5be20b4ebb9a16af22ddd7e", + "build/assets/windows/Win32/Lib/encodings/cp855.py": "https://files.ballistica.net/cache/ba1/47/29/12f47a23bca94451745f0f8cc606", + "build/assets/windows/Win32/Lib/encodings/cp856.py": "https://files.ballistica.net/cache/ba1/fc/b6/cdddcab3958a04797e60f1e60020", + "build/assets/windows/Win32/Lib/encodings/cp857.py": "https://files.ballistica.net/cache/ba1/65/c3/14e9d215921d9b10944dc8c746fa", + "build/assets/windows/Win32/Lib/encodings/cp858.py": "https://files.ballistica.net/cache/ba1/1d/43/a93d269e2a3a0c7fb8a272118926", + "build/assets/windows/Win32/Lib/encodings/cp860.py": "https://files.ballistica.net/cache/ba1/27/05/2bb1b7e96c46ac4c219af286d0c7", + "build/assets/windows/Win32/Lib/encodings/cp861.py": "https://files.ballistica.net/cache/ba1/5a/ff/1af2cdda19cbef7bb0f290333399", + "build/assets/windows/Win32/Lib/encodings/cp862.py": "https://files.ballistica.net/cache/ba1/a1/c4/1f0899757cfc56a06a2ebe3f0bd1", + "build/assets/windows/Win32/Lib/encodings/cp863.py": "https://files.ballistica.net/cache/ba1/6c/36/f7c5d92795f13a459a8537f25d15", + "build/assets/windows/Win32/Lib/encodings/cp864.py": "https://files.ballistica.net/cache/ba1/3a/76/cc71d5c831200341f8e89bf83960", + "build/assets/windows/Win32/Lib/encodings/cp865.py": "https://files.ballistica.net/cache/ba1/db/56/5427a763d45a4888ca5f27f013e6", + "build/assets/windows/Win32/Lib/encodings/cp866.py": "https://files.ballistica.net/cache/ba1/1f/5e/6b065fefa5266e75209c38efcec6", + "build/assets/windows/Win32/Lib/encodings/cp869.py": "https://files.ballistica.net/cache/ba1/f1/59/2b89a39ded9bab41d67431b95ba2", + "build/assets/windows/Win32/Lib/encodings/cp874.py": "https://files.ballistica.net/cache/ba1/15/e2/f57493cf6a296dfdc2a332909032", + "build/assets/windows/Win32/Lib/encodings/cp875.py": "https://files.ballistica.net/cache/ba1/d5/75/4c60226c3755beb5925baeb4cea9", + "build/assets/windows/Win32/Lib/encodings/cp932.py": "https://files.ballistica.net/cache/ba1/e8/aa/fc76fda975327dcf20f07d1ace7d", + "build/assets/windows/Win32/Lib/encodings/cp949.py": "https://files.ballistica.net/cache/ba1/d6/03/f43a9bf6c281d73896d2c6c35acc", + "build/assets/windows/Win32/Lib/encodings/cp950.py": "https://files.ballistica.net/cache/ba1/a8/8e/11b8f69d0f070a0916d32a0aee4f", + "build/assets/windows/Win32/Lib/encodings/euc_jis_2004.py": "https://files.ballistica.net/cache/ba1/53/82/f9d2fa3024b832ad1234441bc201", + "build/assets/windows/Win32/Lib/encodings/euc_jisx0213.py": "https://files.ballistica.net/cache/ba1/f7/56/113baeb43e94755c42e6a0ebbe6f", + "build/assets/windows/Win32/Lib/encodings/euc_jp.py": "https://files.ballistica.net/cache/ba1/34/d4/d2d53a386d370d23b37d31d190ba", + "build/assets/windows/Win32/Lib/encodings/euc_kr.py": "https://files.ballistica.net/cache/ba1/e7/61/216f428aa857e6224447bee18aad", + "build/assets/windows/Win32/Lib/encodings/gb18030.py": "https://files.ballistica.net/cache/ba1/43/86/d94992d4ab1856f17922cd7a8795", + "build/assets/windows/Win32/Lib/encodings/gb2312.py": "https://files.ballistica.net/cache/ba1/44/f8/ed257968e742de1e761abc95a2c3", + "build/assets/windows/Win32/Lib/encodings/gbk.py": "https://files.ballistica.net/cache/ba1/a9/c6/75bde1a6aa77261584117811cc73", + "build/assets/windows/Win32/Lib/encodings/hex_codec.py": "https://files.ballistica.net/cache/ba1/e9/40/bc5fe974769fbd3393fe84b89f73", + "build/assets/windows/Win32/Lib/encodings/hp_roman8.py": "https://files.ballistica.net/cache/ba1/ca/43/0bcff3cab3d7c5c1310476093b8c", + "build/assets/windows/Win32/Lib/encodings/hz.py": "https://files.ballistica.net/cache/ba1/03/36/2e52da054dcd148f382a9f709db2", + "build/assets/windows/Win32/Lib/encodings/idna.py": "https://files.ballistica.net/cache/ba1/9f/36/4363632cdbcb6a0fa58a6e8bd900", + "build/assets/windows/Win32/Lib/encodings/iso2022_jp.py": "https://files.ballistica.net/cache/ba1/91/2a/84e45b80abfac2371fabcbc163f7", + "build/assets/windows/Win32/Lib/encodings/iso2022_jp_1.py": "https://files.ballistica.net/cache/ba1/01/dd/dc0f6ec18c0b0fd89f2d22dc9cb8", + "build/assets/windows/Win32/Lib/encodings/iso2022_jp_2.py": "https://files.ballistica.net/cache/ba1/e2/ab/fb3bd3b266140d3f0d6d9d19fa44", + "build/assets/windows/Win32/Lib/encodings/iso2022_jp_2004.py": "https://files.ballistica.net/cache/ba1/ce/ea/07ca2726c9cdd1bf3ec85d3551f8", + "build/assets/windows/Win32/Lib/encodings/iso2022_jp_3.py": "https://files.ballistica.net/cache/ba1/ff/ab/634e48b570f7b6daef0040c8a7ba", + "build/assets/windows/Win32/Lib/encodings/iso2022_jp_ext.py": "https://files.ballistica.net/cache/ba1/b0/2d/35a071b8f84c013885b0e765da8f", + "build/assets/windows/Win32/Lib/encodings/iso2022_kr.py": "https://files.ballistica.net/cache/ba1/25/ea/463c68f528839af09b104a30a90f", + "build/assets/windows/Win32/Lib/encodings/iso8859_1.py": "https://files.ballistica.net/cache/ba1/c1/a9/c7c4ee8125f0d6a4ab998e408665", + "build/assets/windows/Win32/Lib/encodings/iso8859_10.py": "https://files.ballistica.net/cache/ba1/5d/d0/5c91f9747a0c7ff675ef1cb13ece", + "build/assets/windows/Win32/Lib/encodings/iso8859_11.py": "https://files.ballistica.net/cache/ba1/18/b2/68947a783164505edb129228fdb1", + "build/assets/windows/Win32/Lib/encodings/iso8859_13.py": "https://files.ballistica.net/cache/ba1/22/3c/ab3309d88e2b2bd066619d2fb4d3", + "build/assets/windows/Win32/Lib/encodings/iso8859_14.py": "https://files.ballistica.net/cache/ba1/ac/17/0759e88079cecb57f9c59ac03d5d", + "build/assets/windows/Win32/Lib/encodings/iso8859_15.py": "https://files.ballistica.net/cache/ba1/e4/e8/027f91dd077d23309610a78dcf25", + "build/assets/windows/Win32/Lib/encodings/iso8859_16.py": "https://files.ballistica.net/cache/ba1/21/d1/96c7c8ac6f2ffee585a48ac00a01", + "build/assets/windows/Win32/Lib/encodings/iso8859_2.py": "https://files.ballistica.net/cache/ba1/05/30/f9e9ca5e74afd672235f386fe82e", + "build/assets/windows/Win32/Lib/encodings/iso8859_3.py": "https://files.ballistica.net/cache/ba1/7d/bb/3d0951926093da6319c8692d79a3", + "build/assets/windows/Win32/Lib/encodings/iso8859_4.py": "https://files.ballistica.net/cache/ba1/d7/23/51e1b65871e7ada43adb1d3846fe", + "build/assets/windows/Win32/Lib/encodings/iso8859_5.py": "https://files.ballistica.net/cache/ba1/34/c6/a8b7364cfd41ba66440418abcf97", + "build/assets/windows/Win32/Lib/encodings/iso8859_6.py": "https://files.ballistica.net/cache/ba1/11/25/efdef7f92fef54cabbf41ad9caeb", + "build/assets/windows/Win32/Lib/encodings/iso8859_7.py": "https://files.ballistica.net/cache/ba1/96/a3/34672944c199cd4faea569691855", + "build/assets/windows/Win32/Lib/encodings/iso8859_8.py": "https://files.ballistica.net/cache/ba1/86/7b/3aa9b7915391d6f7964543438d36", + "build/assets/windows/Win32/Lib/encodings/iso8859_9.py": "https://files.ballistica.net/cache/ba1/35/4e/032a8774ff0c73ac6f025f021af5", + "build/assets/windows/Win32/Lib/encodings/johab.py": "https://files.ballistica.net/cache/ba1/c4/0b/f66a781962979db53ccbd098ae70", + "build/assets/windows/Win32/Lib/encodings/koi8_r.py": "https://files.ballistica.net/cache/ba1/fa/34/20ea9735ca38d99d02b13ab06dbb", + "build/assets/windows/Win32/Lib/encodings/koi8_t.py": "https://files.ballistica.net/cache/ba1/f6/18/a41304b181bd6c50f1d57140dd40", + "build/assets/windows/Win32/Lib/encodings/koi8_u.py": "https://files.ballistica.net/cache/ba1/60/b6/5435316ebf39dc10ed4f4b11143d", + "build/assets/windows/Win32/Lib/encodings/kz1048.py": "https://files.ballistica.net/cache/ba1/f4/8a/a7aec6f8679cec8a0b56cda56fbc", + "build/assets/windows/Win32/Lib/encodings/latin_1.py": "https://files.ballistica.net/cache/ba1/c2/d6/8a6ae3e87f5fb0978df5f5b97e37", + "build/assets/windows/Win32/Lib/encodings/mac_arabic.py": "https://files.ballistica.net/cache/ba1/d0/2f/dc3f7c351f30cbe298c509a5d310", + "build/assets/windows/Win32/Lib/encodings/mac_croatian.py": "https://files.ballistica.net/cache/ba1/76/aa/c46b28e28beac969b7cf614623ad", + "build/assets/windows/Win32/Lib/encodings/mac_cyrillic.py": "https://files.ballistica.net/cache/ba1/3e/10/33bf50fbeb50f208262f24574d82", + "build/assets/windows/Win32/Lib/encodings/mac_farsi.py": "https://files.ballistica.net/cache/ba1/ef/da/e5b2e11da2a95925ab08edd9cf24", + "build/assets/windows/Win32/Lib/encodings/mac_greek.py": "https://files.ballistica.net/cache/ba1/a7/fb/e5920266aabd980d7f3d883395dd", + "build/assets/windows/Win32/Lib/encodings/mac_iceland.py": "https://files.ballistica.net/cache/ba1/b5/d6/6674beaefa7cb1c6828b73a15379", + "build/assets/windows/Win32/Lib/encodings/mac_latin2.py": "https://files.ballistica.net/cache/ba1/2f/35/dad6b80b2cd623236bf300ff78d0", + "build/assets/windows/Win32/Lib/encodings/mac_roman.py": "https://files.ballistica.net/cache/ba1/14/1c/6bf7b1bd433974b1133b252b0b66", + "build/assets/windows/Win32/Lib/encodings/mac_romanian.py": "https://files.ballistica.net/cache/ba1/a7/7b/7b9dfc69bbf24244430befed27cd", + "build/assets/windows/Win32/Lib/encodings/mac_turkish.py": "https://files.ballistica.net/cache/ba1/b0/0f/011f4ed8fe10f88ce5ecb82a1449", + "build/assets/windows/Win32/Lib/encodings/mbcs.py": "https://files.ballistica.net/cache/ba1/69/ce/3cc6082fa1afbd9e2cc1831ee5c4", + "build/assets/windows/Win32/Lib/encodings/oem.py": "https://files.ballistica.net/cache/ba1/01/fd/deb6d3409fe28278807a97c75d3c", + "build/assets/windows/Win32/Lib/encodings/palmos.py": "https://files.ballistica.net/cache/ba1/a8/aa/61bbd1cb6d3dd1e0dae9f6897ddd", + "build/assets/windows/Win32/Lib/encodings/ptcp154.py": "https://files.ballistica.net/cache/ba1/10/8c/e5f30d97e95c32d3e7b59d2f943b", + "build/assets/windows/Win32/Lib/encodings/punycode.py": "https://files.ballistica.net/cache/ba1/c7/b6/10380c329f603e522e709cae61d8", + "build/assets/windows/Win32/Lib/encodings/quopri_codec.py": "https://files.ballistica.net/cache/ba1/bf/a8/9a2372eb226ad1972d91617a9698", + "build/assets/windows/Win32/Lib/encodings/raw_unicode_escape.py": "https://files.ballistica.net/cache/ba1/fe/82/e89eac337214448cad7ed13c7b4f", + "build/assets/windows/Win32/Lib/encodings/rot_13.py": "https://files.ballistica.net/cache/ba1/dd/c2/b6fcee683ca8fb1e46c6bf47f6d2", + "build/assets/windows/Win32/Lib/encodings/shift_jis.py": "https://files.ballistica.net/cache/ba1/30/46/add51e8feed385c835a19e4c6c1d", + "build/assets/windows/Win32/Lib/encodings/shift_jis_2004.py": "https://files.ballistica.net/cache/ba1/c9/89/cdc0bb427dc3ff82d82087bcba01", + "build/assets/windows/Win32/Lib/encodings/shift_jisx0213.py": "https://files.ballistica.net/cache/ba1/ff/f1/085e1bbe15776dfb50dda22f7d36", + "build/assets/windows/Win32/Lib/encodings/tis_620.py": "https://files.ballistica.net/cache/ba1/99/d5/a0e9ef729f2e8c3590fed011ec1b", + "build/assets/windows/Win32/Lib/encodings/undefined.py": "https://files.ballistica.net/cache/ba1/0f/f3/92b6bf746181cdcb37d0e7356349", + "build/assets/windows/Win32/Lib/encodings/unicode_escape.py": "https://files.ballistica.net/cache/ba1/76/a3/fa731c784c72c8d880dc19af1a36", + "build/assets/windows/Win32/Lib/encodings/utf_16.py": "https://files.ballistica.net/cache/ba1/5c/b7/e7843ae1c9d92c55c6f97e8dbae8", + "build/assets/windows/Win32/Lib/encodings/utf_16_be.py": "https://files.ballistica.net/cache/ba1/c3/42/af94343dda4b9540bc4ff6d2fd5f", + "build/assets/windows/Win32/Lib/encodings/utf_16_le.py": "https://files.ballistica.net/cache/ba1/9a/fb/3870300dd9bf794531a0a2f8a720", + "build/assets/windows/Win32/Lib/encodings/utf_32.py": "https://files.ballistica.net/cache/ba1/f2/b0/fb3ff01d4f436414d17dbd9ab91a", + "build/assets/windows/Win32/Lib/encodings/utf_32_be.py": "https://files.ballistica.net/cache/ba1/04/51/9e922f1879c4045657371711f800", + "build/assets/windows/Win32/Lib/encodings/utf_32_le.py": "https://files.ballistica.net/cache/ba1/e8/be/dad52b4cc301bd4d91397c098012", + "build/assets/windows/Win32/Lib/encodings/utf_7.py": "https://files.ballistica.net/cache/ba1/a0/ff/500db9c22e29fbdc34d65fd58066", + "build/assets/windows/Win32/Lib/encodings/utf_8.py": "https://files.ballistica.net/cache/ba1/69/e6/38e7054c7a2a587c0c0a21796170", + "build/assets/windows/Win32/Lib/encodings/utf_8_sig.py": "https://files.ballistica.net/cache/ba1/a0/4c/457fbb612098b7f0ff1fd89afde1", + "build/assets/windows/Win32/Lib/encodings/uu_codec.py": "https://files.ballistica.net/cache/ba1/3c/8d/0e550c83fb1821a08bf2dad8ca61", + "build/assets/windows/Win32/Lib/encodings/zlib_codec.py": "https://files.ballistica.net/cache/ba1/a6/ff/937639be46951310751125f73fa8", + "build/assets/windows/Win32/Lib/enum.py": "https://files.ballistica.net/cache/ba1/ca/8d/8963d0a1c68f88186651e7500e98", + "build/assets/windows/Win32/Lib/filecmp.py": "https://files.ballistica.net/cache/ba1/64/04/67a01002aae322fd24ebd99ba8df", + "build/assets/windows/Win32/Lib/fileinput.py": "https://files.ballistica.net/cache/ba1/01/fc/5cee5fe51b5e7c2105e2fd6da208", + "build/assets/windows/Win32/Lib/fnmatch.py": "https://files.ballistica.net/cache/ba1/2d/a3/ecf4c695a38fc7060d2009fb2041", + "build/assets/windows/Win32/Lib/fractions.py": "https://files.ballistica.net/cache/ba1/c8/15/a58de6d8573bfec9c4521ed0ed89", + "build/assets/windows/Win32/Lib/ftplib.py": "https://files.ballistica.net/cache/ba1/2b/b3/f48274da9799013b87ebdce464f2", + "build/assets/windows/Win32/Lib/functools.py": "https://files.ballistica.net/cache/ba1/2e/5e/9ff7ee92c6dbb977469d60e61f74", + "build/assets/windows/Win32/Lib/genericpath.py": "https://files.ballistica.net/cache/ba1/1e/89/16baae3fe29f6f7616f2d6435a3d", + "build/assets/windows/Win32/Lib/getopt.py": "https://files.ballistica.net/cache/ba1/e6/5e/d915d71edfe25beb993db55e0838", + "build/assets/windows/Win32/Lib/getpass.py": "https://files.ballistica.net/cache/ba1/02/99/a9522a099cd42ae81935266f5dea", + "build/assets/windows/Win32/Lib/gettext.py": "https://files.ballistica.net/cache/ba1/6b/d0/223bf0eb5494f08aa02c8f112595", + "build/assets/windows/Win32/Lib/glob.py": "https://files.ballistica.net/cache/ba1/c0/30/1dee0150b7bb25b98b7be7bb083c", + "build/assets/windows/Win32/Lib/graphlib.py": "https://files.ballistica.net/cache/ba1/28/f0/15e32f58289b23c2e02336557205", + "build/assets/windows/Win32/Lib/gzip.py": "https://files.ballistica.net/cache/ba1/84/79/1cb90ae83ab49f449d63a7e220fb", + "build/assets/windows/Win32/Lib/hashlib.py": "https://files.ballistica.net/cache/ba1/65/74/12ba62d70aa3d4e8e2b1784cf0e2", + "build/assets/windows/Win32/Lib/heapq.py": "https://files.ballistica.net/cache/ba1/2d/a5/11967af19c95b070cfef6cddcec9", + "build/assets/windows/Win32/Lib/hmac.py": "https://files.ballistica.net/cache/ba1/60/40/6a55df1d5c2f4bfa4151dc37c997", + "build/assets/windows/Win32/Lib/html/__init__.py": "https://files.ballistica.net/cache/ba1/96/95/cd516ef5ef35804eb01971d80ea4", + "build/assets/windows/Win32/Lib/html/entities.py": "https://files.ballistica.net/cache/ba1/29/06/5b2c8642c8c7a95a5be69742c6cf", + "build/assets/windows/Win32/Lib/html/parser.py": "https://files.ballistica.net/cache/ba1/a9/b4/938196a5ee4e5fc356df98b51901", + "build/assets/windows/Win32/Lib/http/__init__.py": "https://files.ballistica.net/cache/ba1/e8/86/ebdccc938f0dece4d172c93a390e", + "build/assets/windows/Win32/Lib/http/client.py": "https://files.ballistica.net/cache/ba1/ef/9c/07be59ed485d4c56759fb6216000", + "build/assets/windows/Win32/Lib/http/cookiejar.py": "https://files.ballistica.net/cache/ba1/06/c6/b5a6ff45e46c7607f0b7dd01fe12", + "build/assets/windows/Win32/Lib/http/cookies.py": "https://files.ballistica.net/cache/ba1/b2/5e/e267e04a3b3ef82a31ca33c9c602", + "build/assets/windows/Win32/Lib/http/server.py": "https://files.ballistica.net/cache/ba1/72/3e/ad29eb3c4494571953ffa8f64e3b", + "build/assets/windows/Win32/Lib/imghdr.py": "https://files.ballistica.net/cache/ba1/b7/b5/d9047ef2783c7786761840d421a0", + "build/assets/windows/Win32/Lib/imp.py": "https://files.ballistica.net/cache/ba1/b5/f1/9e8842b505914e1389d2263d2bd2", + "build/assets/windows/Win32/Lib/importlib/__init__.py": "https://files.ballistica.net/cache/ba1/c0/7e/65dcf3ab4e4358b5761f3a4dae75", + "build/assets/windows/Win32/Lib/importlib/_abc.py": "https://files.ballistica.net/cache/ba1/77/65/2a197760e6b4de66a785054e8998", + "build/assets/windows/Win32/Lib/importlib/_bootstrap.py": "https://files.ballistica.net/cache/ba1/89/81/8c5adcd95b24f08e3701001b1e6f", + "build/assets/windows/Win32/Lib/importlib/_bootstrap_external.py": "https://files.ballistica.net/cache/ba1/d6/89/378ef410da2b873baf9e0d2e1202", + "build/assets/windows/Win32/Lib/importlib/abc.py": "https://files.ballistica.net/cache/ba1/3d/ca/d67f4678b627c1469f074ecbee2c", + "build/assets/windows/Win32/Lib/importlib/machinery.py": "https://files.ballistica.net/cache/ba1/10/6c/200375e407830d01f614203c46ce", + "build/assets/windows/Win32/Lib/importlib/metadata/__init__.py": "https://files.ballistica.net/cache/ba1/d0/9e/3aa5a01e3dc0ff57f68b985ab3de", + "build/assets/windows/Win32/Lib/importlib/metadata/_adapters.py": "https://files.ballistica.net/cache/ba1/7b/e1/2a0932c5c7281c9726521638f8be", + "build/assets/windows/Win32/Lib/importlib/metadata/_collections.py": "https://files.ballistica.net/cache/ba1/57/af/17a24adea34aacf62a9e69dec19d", + "build/assets/windows/Win32/Lib/importlib/metadata/_functools.py": "https://files.ballistica.net/cache/ba1/4e/73/fd1cd9a7c29f3c1b5eaa8b129576", + "build/assets/windows/Win32/Lib/importlib/metadata/_itertools.py": "https://files.ballistica.net/cache/ba1/80/7e/51420c7d3bd569239724f7396487", + "build/assets/windows/Win32/Lib/importlib/metadata/_meta.py": "https://files.ballistica.net/cache/ba1/71/80/2a51861876fd8f1756c20fc9753a", + "build/assets/windows/Win32/Lib/importlib/metadata/_text.py": "https://files.ballistica.net/cache/ba1/60/01/f3f8eb9c75debeb1144410aa0711", + "build/assets/windows/Win32/Lib/importlib/readers.py": "https://files.ballistica.net/cache/ba1/14/54/4cbaf6a1dab56432cf1d3b975749", + "build/assets/windows/Win32/Lib/importlib/resources/__init__.py": "https://files.ballistica.net/cache/ba1/d5/55/1b050d1c82f5536e905d71009cd8", + "build/assets/windows/Win32/Lib/importlib/resources/_adapters.py": "https://files.ballistica.net/cache/ba1/f6/e4/2d8b1ad603bb85c6d566c799ed18", + "build/assets/windows/Win32/Lib/importlib/resources/_common.py": "https://files.ballistica.net/cache/ba1/d7/b0/df8e3c5b9c06c7b6f10a2af059ef", + "build/assets/windows/Win32/Lib/importlib/resources/_itertools.py": "https://files.ballistica.net/cache/ba1/32/4c/c78fc8b7600f3e97d1989442fdec", + "build/assets/windows/Win32/Lib/importlib/resources/_legacy.py": "https://files.ballistica.net/cache/ba1/77/c1/3b3705ecf61125cc53b3d15c81f4", + "build/assets/windows/Win32/Lib/importlib/resources/abc.py": "https://files.ballistica.net/cache/ba1/9e/e3/e893666dfb2e0acb2e1841f5eead", + "build/assets/windows/Win32/Lib/importlib/resources/readers.py": "https://files.ballistica.net/cache/ba1/57/6d/0de58f9116ebc0c60cec03cc4298", + "build/assets/windows/Win32/Lib/importlib/resources/simple.py": "https://files.ballistica.net/cache/ba1/8b/df/5d60fe6f1809abf39944fa954213", + "build/assets/windows/Win32/Lib/importlib/simple.py": "https://files.ballistica.net/cache/ba1/80/a6/afe1e3ecc7f66557621bd4141e16", + "build/assets/windows/Win32/Lib/importlib/util.py": "https://files.ballistica.net/cache/ba1/bb/37/88643e29ec8d883a6954f8bc236f", + "build/assets/windows/Win32/Lib/inspect.py": "https://files.ballistica.net/cache/ba1/55/33/536c0284619ddd44a95e77a4f20d", + "build/assets/windows/Win32/Lib/io.py": "https://files.ballistica.net/cache/ba1/61/a3/2ed5232952b2d8924369b5d3d3fa", + "build/assets/windows/Win32/Lib/ipaddress.py": "https://files.ballistica.net/cache/ba1/10/94/580af524b36f8e3b1511391a2cf4", + "build/assets/windows/Win32/Lib/json/__init__.py": "https://files.ballistica.net/cache/ba1/41/dc/d6ae024898bfc598e885a86a03c8", + "build/assets/windows/Win32/Lib/json/decoder.py": "https://files.ballistica.net/cache/ba1/55/90/4a8b78dcdd8462d938a5fee8c49f", + "build/assets/windows/Win32/Lib/json/encoder.py": "https://files.ballistica.net/cache/ba1/43/86/4bc2287976ee6334729566437a98", + "build/assets/windows/Win32/Lib/json/scanner.py": "https://files.ballistica.net/cache/ba1/25/43/067f0878cb9b99117c4d94f1ae72", + "build/assets/windows/Win32/Lib/json/tool.py": "https://files.ballistica.net/cache/ba1/8e/bb/6b9cee97dd32c61804f4b48b06e3", + "build/assets/windows/Win32/Lib/keyword.py": "https://files.ballistica.net/cache/ba1/e1/b0/0ce4a134f48a7afe6dae67c84413", + "build/assets/windows/Win32/Lib/linecache.py": "https://files.ballistica.net/cache/ba1/69/91/805c5ae2a9a369fa66ade96b37d7", + "build/assets/windows/Win32/Lib/locale.py": "https://files.ballistica.net/cache/ba1/57/3f/7eb9bb3a11172c3d8df0be59dc19", + "build/assets/windows/Win32/Lib/logging/__init__.py": "https://files.ballistica.net/cache/ba1/2c/61/90582c1b163a512ca946b9328087", + "build/assets/windows/Win32/Lib/logging/config.py": "https://files.ballistica.net/cache/ba1/b6/d1/fc3b183a34596d2462134deda770", + "build/assets/windows/Win32/Lib/logging/handlers.py": "https://files.ballistica.net/cache/ba1/ee/85/89411ab020a8d8732a122ea41d5f", + "build/assets/windows/Win32/Lib/lzma.py": "https://files.ballistica.net/cache/ba1/d0/57/e806e27f9915cddc18f3cb7df736", + "build/assets/windows/Win32/Lib/mailbox.py": "https://files.ballistica.net/cache/ba1/a5/7d/72fc98d8fd77726c05084a2b2c3b", + "build/assets/windows/Win32/Lib/mailcap.py": "https://files.ballistica.net/cache/ba1/69/f2/c0b81602fa3af52cb4dee8aa4dfd", + "build/assets/windows/Win32/Lib/mimetypes.py": "https://files.ballistica.net/cache/ba1/ec/c4/fdb11bc67e37df4461a39207ae3c", + "build/assets/windows/Win32/Lib/modulefinder.py": "https://files.ballistica.net/cache/ba1/b0/fc/84546cd7a7ce5a9b366cde06fad4", + "build/assets/windows/Win32/Lib/netrc.py": "https://files.ballistica.net/cache/ba1/f5/0d/08387614a2dd6e938dfc30b13dad", + "build/assets/windows/Win32/Lib/nntplib.py": "https://files.ballistica.net/cache/ba1/48/df/f6c8730c0c9be3b4d8c20cbe967e", + "build/assets/windows/Win32/Lib/ntpath.py": "https://files.ballistica.net/cache/ba1/e8/1a/aed82bb1f3044b85f5a88b4192e4", + "build/assets/windows/Win32/Lib/nturl2path.py": "https://files.ballistica.net/cache/ba1/f7/bd/f7e4d07cb78dc383166dafe1346f", + "build/assets/windows/Win32/Lib/numbers.py": "https://files.ballistica.net/cache/ba1/c1/be/2d600f0cdadd39a078b313a9fb89", + "build/assets/windows/Win32/Lib/opcode.py": "https://files.ballistica.net/cache/ba1/c8/79/f7a61a981efdbfbb92f1c85bcce0", + "build/assets/windows/Win32/Lib/operator.py": "https://files.ballistica.net/cache/ba1/87/a8/b85f70d70f815d50de13c31cfe49", + "build/assets/windows/Win32/Lib/optparse.py": "https://files.ballistica.net/cache/ba1/80/73/8c55859d0f496df6f2acf6871996", + "build/assets/windows/Win32/Lib/os.py": "https://files.ballistica.net/cache/ba1/a1/a3/d74010d5a666a2a490ceab93f3d0", + "build/assets/windows/Win32/Lib/pathlib.py": "https://files.ballistica.net/cache/ba1/57/e2/f1997aeaa609959f99e11c3c5a6b", + "build/assets/windows/Win32/Lib/pdb.py": "https://files.ballistica.net/cache/ba1/78/52/164fba34dc098d85b8d6ea5af25f", + "build/assets/windows/Win32/Lib/pickle.py": "https://files.ballistica.net/cache/ba1/fb/04/4e406cdb9e62646b51d545a2ecc4", + "build/assets/windows/Win32/Lib/pickletools.py": "https://files.ballistica.net/cache/ba1/c6/24/cd032489b0167c5b22480775f8d0", + "build/assets/windows/Win32/Lib/pipes.py": "https://files.ballistica.net/cache/ba1/96/f2/602eecc76845de9814f6e65c0f8e", + "build/assets/windows/Win32/Lib/pkgutil.py": "https://files.ballistica.net/cache/ba1/0a/2b/3b5cb98a1d403448100a22ca7e6f", + "build/assets/windows/Win32/Lib/platform.py": "https://files.ballistica.net/cache/ba1/e0/23/2c62c839db899c01c4a1e51703cc", + "build/assets/windows/Win32/Lib/plistlib.py": "https://files.ballistica.net/cache/ba1/14/b7/85b0391281001476acc3ad68da8a", + "build/assets/windows/Win32/Lib/poplib.py": "https://files.ballistica.net/cache/ba1/3d/f8/8b27bfbbd6955a1a3e4af0d68eed", + "build/assets/windows/Win32/Lib/posixpath.py": "https://files.ballistica.net/cache/ba1/85/2e/4572bd249b4f7e68a4a6e4500f10", + "build/assets/windows/Win32/Lib/pprint.py": "https://files.ballistica.net/cache/ba1/c3/ca/aad7b6c532e0a39e4d5b8202be8a", + "build/assets/windows/Win32/Lib/profile.py": "https://files.ballistica.net/cache/ba1/75/1f/000ec649766993f81ae724b71d78", + "build/assets/windows/Win32/Lib/pstats.py": "https://files.ballistica.net/cache/ba1/34/08/7bdfb536f67c2d7ddc8e83e8aa4c", + "build/assets/windows/Win32/Lib/pty.py": "https://files.ballistica.net/cache/ba1/02/f1/28248c2076d9e0cfb43c32a7680f", + "build/assets/windows/Win32/Lib/py_compile.py": "https://files.ballistica.net/cache/ba1/1b/f5/56f84f9a0a0debaca0d3771e49a5", + "build/assets/windows/Win32/Lib/pyclbr.py": "https://files.ballistica.net/cache/ba1/57/5f/71c1689c6e3697f1cdd5dd27e521", + "build/assets/windows/Win32/Lib/pydoc.py": "https://files.ballistica.net/cache/ba1/20/dd/14173e5cf536386db54a15f4e921", + "build/assets/windows/Win32/Lib/queue.py": "https://files.ballistica.net/cache/ba1/67/df/7e4ab428281188fa4c3d74fe9513", + "build/assets/windows/Win32/Lib/quopri.py": "https://files.ballistica.net/cache/ba1/93/69/b72ff6e157ca3e7a1be6f8c9af98", + "build/assets/windows/Win32/Lib/random.py": "https://files.ballistica.net/cache/ba1/98/cc/74c2829169d6e941957c012171c2", + "build/assets/windows/Win32/Lib/re/__init__.py": "https://files.ballistica.net/cache/ba1/d3/c2/d51ea4d9868ddb6f98b9c4a01a51", + "build/assets/windows/Win32/Lib/re/_casefix.py": "https://files.ballistica.net/cache/ba1/93/72/06a856cd994b8cff04251859347a", + "build/assets/windows/Win32/Lib/re/_compiler.py": "https://files.ballistica.net/cache/ba1/42/9c/e78978724ff679bfaa8bc034d06b", + "build/assets/windows/Win32/Lib/re/_constants.py": "https://files.ballistica.net/cache/ba1/1c/d0/a20a893f7d2e29162a4cab1ccd04", + "build/assets/windows/Win32/Lib/re/_parser.py": "https://files.ballistica.net/cache/ba1/65/6a/46da910e7963651e099225494b4c", + "build/assets/windows/Win32/Lib/reprlib.py": "https://files.ballistica.net/cache/ba1/2d/50/0594ca73bf9ca74ee8c312441450", + "build/assets/windows/Win32/Lib/rlcompleter.py": "https://files.ballistica.net/cache/ba1/a3/ba/c00825a094539a6531d45df5b172", + "build/assets/windows/Win32/Lib/runpy.py": "https://files.ballistica.net/cache/ba1/19/46/bae97e8940aa9252ca4cbee2f8b7", + "build/assets/windows/Win32/Lib/sched.py": "https://files.ballistica.net/cache/ba1/62/3d/bae0051726709beb46c6e6fb5356", + "build/assets/windows/Win32/Lib/secrets.py": "https://files.ballistica.net/cache/ba1/d8/c3/8502e7e5c0b9a792746816df6108", + "build/assets/windows/Win32/Lib/selectors.py": "https://files.ballistica.net/cache/ba1/38/e8/d0b78d2d44e8ca2c314c26c2c813", + "build/assets/windows/Win32/Lib/shelve.py": "https://files.ballistica.net/cache/ba1/89/18/2f3f856aae146f39e16132be5d1f", + "build/assets/windows/Win32/Lib/shlex.py": "https://files.ballistica.net/cache/ba1/db/1c/a991dedfdd6d957cbb46b1d6bf48", + "build/assets/windows/Win32/Lib/shutil.py": "https://files.ballistica.net/cache/ba1/7c/58/99cdc9c9a4f11d3b92b94b9ea71a", + "build/assets/windows/Win32/Lib/signal.py": "https://files.ballistica.net/cache/ba1/9e/f5/a53fe648dc8e0dfd914bfe48ead7", + "build/assets/windows/Win32/Lib/site.py": "https://files.ballistica.net/cache/ba1/1e/53/b8641f116b0926c7fb42bf2ad820", + "build/assets/windows/Win32/Lib/smtpd.py": "https://files.ballistica.net/cache/ba1/b7/35/a0805f2deda6b8e8f2c5e32111f5", + "build/assets/windows/Win32/Lib/smtplib.py": "https://files.ballistica.net/cache/ba1/86/e2/fcb2e447865b68d38cb422bf4890", + "build/assets/windows/Win32/Lib/sndhdr.py": "https://files.ballistica.net/cache/ba1/8b/1b/314049b94b1a6fbd6bca6d486b52", + "build/assets/windows/Win32/Lib/socket.py": "https://files.ballistica.net/cache/ba1/6f/21/a555ec7d876bdce52fc45e663b70", + "build/assets/windows/Win32/Lib/socketserver.py": "https://files.ballistica.net/cache/ba1/69/64/9b16b8200f78161a00825c1e9bea", + "build/assets/windows/Win32/Lib/sqlite3/__init__.py": "https://files.ballistica.net/cache/ba1/e1/f1/a468933863b15eb72120af0703c5", + "build/assets/windows/Win32/Lib/sqlite3/dbapi2.py": "https://files.ballistica.net/cache/ba1/dd/71/1a1a8f102d1f362ca5e6d1ea156a", + "build/assets/windows/Win32/Lib/sqlite3/dump.py": "https://files.ballistica.net/cache/ba1/9e/1b/30748df99a49027ebee3a8d5169c", + "build/assets/windows/Win32/Lib/sre_compile.py": "https://files.ballistica.net/cache/ba1/e9/c1/e52ef83f005286ae75cb2b33f32e", + "build/assets/windows/Win32/Lib/sre_constants.py": "https://files.ballistica.net/cache/ba1/4e/c4/a3f77b4754f22b94b16659e9d8d2", + "build/assets/windows/Win32/Lib/sre_parse.py": "https://files.ballistica.net/cache/ba1/f6/f7/f80a633f9376542e65cae03d5bd9", + "build/assets/windows/Win32/Lib/ssl.py": "https://files.ballistica.net/cache/ba1/6b/0e/3e3274c1183e8c5c26cb5a3a9017", + "build/assets/windows/Win32/Lib/stat.py": "https://files.ballistica.net/cache/ba1/4a/8c/7346d77d38b60632952e2e0ba7f0", + "build/assets/windows/Win32/Lib/statistics.py": "https://files.ballistica.net/cache/ba1/7a/32/8ca515ff9cf89710cbae711f271b", + "build/assets/windows/Win32/Lib/string.py": "https://files.ballistica.net/cache/ba1/04/1d/6ff0fcc9d7f1df37c3c57969d3b3", + "build/assets/windows/Win32/Lib/stringprep.py": "https://files.ballistica.net/cache/ba1/73/68/2555e359e99f628e59de8ebf9d6b", + "build/assets/windows/Win32/Lib/struct.py": "https://files.ballistica.net/cache/ba1/85/bc/4c5d4aa147458e62bd1660ea3805", + "build/assets/windows/Win32/Lib/subprocess.py": "https://files.ballistica.net/cache/ba1/6f/d2/5eabc7c3792307b5ca3ebfe2dabb", + "build/assets/windows/Win32/Lib/sunau.py": "https://files.ballistica.net/cache/ba1/e0/e7/bb9db658907c6a6a84a5e33dfc53", + "build/assets/windows/Win32/Lib/symtable.py": "https://files.ballistica.net/cache/ba1/48/cd/600b71a7f7d34efb7ea3e85533b8", + "build/assets/windows/Win32/Lib/sysconfig.py": "https://files.ballistica.net/cache/ba1/d0/c6/6b3625484d4f0b8376296c7dee8a", + "build/assets/windows/Win32/Lib/tabnanny.py": "https://files.ballistica.net/cache/ba1/2c/a6/dedc5352df8bed307564ebb34586", + "build/assets/windows/Win32/Lib/tarfile.py": "https://files.ballistica.net/cache/ba1/6f/ac/a6023083ed0e09ec3203687fe24d", + "build/assets/windows/Win32/Lib/telnetlib.py": "https://files.ballistica.net/cache/ba1/76/be/120cceabccdc87d6cb0e882bdac6", + "build/assets/windows/Win32/Lib/tempfile.py": "https://files.ballistica.net/cache/ba1/63/8f/21536eb7a39244391c3f76eed03f", + "build/assets/windows/Win32/Lib/textwrap.py": "https://files.ballistica.net/cache/ba1/bf/20/ff252c32e72ce1d2e22a5de3cf1a", + "build/assets/windows/Win32/Lib/this.py": "https://files.ballistica.net/cache/ba1/c0/f6/54a840f2882c525c9dafe4086be3", + "build/assets/windows/Win32/Lib/threading.py": "https://files.ballistica.net/cache/ba1/3e/64/5509d5e785ef5b738419b31c6b57", + "build/assets/windows/Win32/Lib/timeit.py": "https://files.ballistica.net/cache/ba1/df/55/63a5732bf089f2a17314d9c54a4d", + "build/assets/windows/Win32/Lib/token.py": "https://files.ballistica.net/cache/ba1/ab/f7/508cbcd30e087591b976198d004a", + "build/assets/windows/Win32/Lib/tokenize.py": "https://files.ballistica.net/cache/ba1/37/cf/5f61a513df3cfc6ffaf3a74848b2", + "build/assets/windows/Win32/Lib/tomllib/__init__.py": "https://files.ballistica.net/cache/ba1/0d/b9/988c7eba9a7b38eb749e08bcb892", + "build/assets/windows/Win32/Lib/tomllib/_parser.py": "https://files.ballistica.net/cache/ba1/b6/4e/d56a4c77f9c9ef1967f87646f08e", + "build/assets/windows/Win32/Lib/tomllib/_re.py": "https://files.ballistica.net/cache/ba1/d7/77/d117701e7aaf75174cd139727b4c", + "build/assets/windows/Win32/Lib/tomllib/_types.py": "https://files.ballistica.net/cache/ba1/0b/7a/5933581476604f75f905a670beda", + "build/assets/windows/Win32/Lib/trace.py": "https://files.ballistica.net/cache/ba1/b1/2d/1122924f7b87bf25958bad30a26d", + "build/assets/windows/Win32/Lib/traceback.py": "https://files.ballistica.net/cache/ba1/bf/be/a04fb4c1cd0bf28290e412d7fcf0", + "build/assets/windows/Win32/Lib/tracemalloc.py": "https://files.ballistica.net/cache/ba1/b1/34/aa2a096b675effe273332d84f9b5", + "build/assets/windows/Win32/Lib/tty.py": "https://files.ballistica.net/cache/ba1/57/f9/c2322a8cc77d80f3baa6e000fffb", + "build/assets/windows/Win32/Lib/types.py": "https://files.ballistica.net/cache/ba1/d0/5d/3aa5fa4eab6044f19d1810a2b0cc", + "build/assets/windows/Win32/Lib/typing.py": "https://files.ballistica.net/cache/ba1/36/03/a18d38477cab6e10950be7b49041", + "build/assets/windows/Win32/Lib/urllib/__init__.py": "https://files.ballistica.net/cache/ba1/f9/b3/aeccc8dc49bf365eeed014143e69", + "build/assets/windows/Win32/Lib/urllib/error.py": "https://files.ballistica.net/cache/ba1/ba/b7/dfdeeffcb1b3b4589804606d4393", + "build/assets/windows/Win32/Lib/urllib/parse.py": "https://files.ballistica.net/cache/ba1/f9/98/4e204b25629ab397a03127e7e08c", + "build/assets/windows/Win32/Lib/urllib/request.py": "https://files.ballistica.net/cache/ba1/46/b3/ffaaf449e74c2e3f56613e438795", + "build/assets/windows/Win32/Lib/urllib/response.py": "https://files.ballistica.net/cache/ba1/c3/78/753539d454b9dce2e6a8158bb368", + "build/assets/windows/Win32/Lib/urllib/robotparser.py": "https://files.ballistica.net/cache/ba1/b2/2a/8282c342e4a6037621d26d26bd43", + "build/assets/windows/Win32/Lib/uu.py": "https://files.ballistica.net/cache/ba1/fe/e7/c3faf031723d6719269d1865b3b4", + "build/assets/windows/Win32/Lib/uuid.py": "https://files.ballistica.net/cache/ba1/3b/eb/3f62030a98aca904c4fccdb1c14d", + "build/assets/windows/Win32/Lib/warnings.py": "https://files.ballistica.net/cache/ba1/59/74/8e282fd462d332ee86485155625a", + "build/assets/windows/Win32/Lib/wave.py": "https://files.ballistica.net/cache/ba1/88/a0/e4af93d7e5287746c70500b2ac5e", + "build/assets/windows/Win32/Lib/weakref.py": "https://files.ballistica.net/cache/ba1/3e/5f/35b5dd22a4ae49649c20b77026b7", + "build/assets/windows/Win32/Lib/webbrowser.py": "https://files.ballistica.net/cache/ba1/f0/fa/f3e2fb138650b6e3ca1b112b8fa5", + "build/assets/windows/Win32/Lib/xdrlib.py": "https://files.ballistica.net/cache/ba1/0e/ef/577eb3579a1d2bc52aa2b5a2e0f2", + "build/assets/windows/Win32/Lib/xml/__init__.py": "https://files.ballistica.net/cache/ba1/b7/93/2c8c4fd09e88e54280ea6dad932e", + "build/assets/windows/Win32/Lib/xml/dom/NodeFilter.py": "https://files.ballistica.net/cache/ba1/f3/80/d1bc28b75e960b975d5961ab44b5", + "build/assets/windows/Win32/Lib/xml/dom/__init__.py": "https://files.ballistica.net/cache/ba1/c0/d4/a73f219b37e24ac6f5625198e489", + "build/assets/windows/Win32/Lib/xml/dom/domreg.py": "https://files.ballistica.net/cache/ba1/14/82/f9c256ae172eeb316443a9bd89b6", + "build/assets/windows/Win32/Lib/xml/dom/expatbuilder.py": "https://files.ballistica.net/cache/ba1/7e/bb/5c690622612810a3276f6489fdba", + "build/assets/windows/Win32/Lib/xml/dom/minicompat.py": "https://files.ballistica.net/cache/ba1/a0/a5/d1ca96bf070fe430d5050d567eac", + "build/assets/windows/Win32/Lib/xml/dom/minidom.py": "https://files.ballistica.net/cache/ba1/2d/bf/03b4ba41d8a28b07f907e476913a", + "build/assets/windows/Win32/Lib/xml/dom/pulldom.py": "https://files.ballistica.net/cache/ba1/d5/b7/164d0aec27a680cd3762d361e591", + "build/assets/windows/Win32/Lib/xml/dom/xmlbuilder.py": "https://files.ballistica.net/cache/ba1/ba/74/da46f3c075e47b4640bcf2f37821", + "build/assets/windows/Win32/Lib/xml/etree/ElementInclude.py": "https://files.ballistica.net/cache/ba1/0c/da/2693de30db3f21eaec902c8187ab", + "build/assets/windows/Win32/Lib/xml/etree/ElementPath.py": "https://files.ballistica.net/cache/ba1/b5/83/353355d7b3b6e56f99dc3e208997", + "build/assets/windows/Win32/Lib/xml/etree/ElementTree.py": "https://files.ballistica.net/cache/ba1/32/b6/021a41613ca19c2e44b08007e0e6", + "build/assets/windows/Win32/Lib/xml/etree/__init__.py": "https://files.ballistica.net/cache/ba1/68/00/c6e87f3959178d26e6c4a95329ec", + "build/assets/windows/Win32/Lib/xml/etree/cElementTree.py": "https://files.ballistica.net/cache/ba1/55/ed/4c1bcd2ca9275d72fd54530b3928", + "build/assets/windows/Win32/Lib/xml/parsers/__init__.py": "https://files.ballistica.net/cache/ba1/b6/03/a844d2cedf6e29b8f08a4ffba0f7", + "build/assets/windows/Win32/Lib/xml/parsers/expat.py": "https://files.ballistica.net/cache/ba1/e5/b8/bbb925c79d6b23ea4847635ef168", + "build/assets/windows/Win32/Lib/xml/sax/__init__.py": "https://files.ballistica.net/cache/ba1/ca/99/79f7a1b836d2d3569295df3647ac", + "build/assets/windows/Win32/Lib/xml/sax/_exceptions.py": "https://files.ballistica.net/cache/ba1/af/5d/62cf9f1325a355df8fac0bffad8c", + "build/assets/windows/Win32/Lib/xml/sax/expatreader.py": "https://files.ballistica.net/cache/ba1/41/9d/bdb85ddf12cfb2cb7415604cc5da", + "build/assets/windows/Win32/Lib/xml/sax/handler.py": "https://files.ballistica.net/cache/ba1/36/74/44eaf79baa7c5a720753cf6f7f8c", + "build/assets/windows/Win32/Lib/xml/sax/saxutils.py": "https://files.ballistica.net/cache/ba1/06/b0/34251e0659cdd59fb14b088944ed", + "build/assets/windows/Win32/Lib/xml/sax/xmlreader.py": "https://files.ballistica.net/cache/ba1/4f/f9/45548142552d384f9bdade5b5542", + "build/assets/windows/Win32/Lib/xmlrpc/__init__.py": "https://files.ballistica.net/cache/ba1/f2/65/b217306f59a04de91e1b22da8315", + "build/assets/windows/Win32/Lib/xmlrpc/client.py": "https://files.ballistica.net/cache/ba1/ef/cb/761c61c1b345fe6927be572f48ff", + "build/assets/windows/Win32/Lib/xmlrpc/server.py": "https://files.ballistica.net/cache/ba1/c8/b5/77ef627928d38ae1ec0cbf2e5510", + "build/assets/windows/Win32/Lib/zipapp.py": "https://files.ballistica.net/cache/ba1/84/00/6030a277f2a87589b8a1788b1077", + "build/assets/windows/Win32/Lib/zipfile.py": "https://files.ballistica.net/cache/ba1/d0/72/7560a29a895dd3015f206d22190f", + "build/assets/windows/Win32/Lib/zipimport.py": "https://files.ballistica.net/cache/ba1/70/b8/e4559f0013e577b586e6b7c93082", + "build/assets/windows/Win32/Lib/zoneinfo/__init__.py": "https://files.ballistica.net/cache/ba1/6e/88/47246fb1f58dd354764ec3f7b67b", + "build/assets/windows/Win32/Lib/zoneinfo/_common.py": "https://files.ballistica.net/cache/ba1/73/7e/88c0b4baa195f642a7af9542baf0", + "build/assets/windows/Win32/Lib/zoneinfo/_tzpath.py": "https://files.ballistica.net/cache/ba1/3b/62/ec8effa785199b04553c52cfbccc", + "build/assets/windows/Win32/Lib/zoneinfo/_zoneinfo.py": "https://files.ballistica.net/cache/ba1/17/02/a78c6841f8d0fa426fbff7863664", + "build/assets/windows/Win32/OpenAL32.dll": "https://files.ballistica.net/cache/ba1/80/cc/249c55f23b9ce8f0c4f76b394adb", + "build/assets/windows/Win32/SDL2.dll": "https://files.ballistica.net/cache/ba1/b1/35/ed31cd199266219e53701d6f61e8", + "build/assets/windows/Win32/libvorbis.dll": "https://files.ballistica.net/cache/ba1/1e/30/2a6c4c4d5356541af0aaf0104127", + "build/assets/windows/Win32/libvorbisfile.dll": "https://files.ballistica.net/cache/ba1/c5/f9/ca30fd076b67c0edfe44450445f9", + "build/assets/windows/Win32/msvcp140d.dll": "https://files.ballistica.net/cache/ba1/68/d7/f1d7fffe20a7649f713e970999ca", + "build/assets/windows/Win32/ogg.dll": "https://files.ballistica.net/cache/ba1/3e/59/11a76156a0ab3e0463f7b0a90cfb", + "build/assets/windows/Win32/python.exe": "https://files.ballistica.net/cache/ba1/03/bf/fbd714902e772c6e9c4fe520ea5c", + "build/assets/windows/Win32/python311.dll": "https://files.ballistica.net/cache/ba1/c9/6a/c5e0b41afda0ae14f1b7bb5b5efa", + "build/assets/windows/Win32/python311_d.dll": "https://files.ballistica.net/cache/ba1/6c/f0/9fab77e6d1bf969e58f0815c3376", + "build/assets/windows/Win32/python_d.exe": "https://files.ballistica.net/cache/ba1/2f/0e/ebb942fc44fbce31f241baaadbb5", + "build/assets/windows/Win32/pythonw.exe": "https://files.ballistica.net/cache/ba1/93/5b/2bcc9306a82d9c68acdcc51a59e2", + "build/assets/windows/Win32/pythonw_d.exe": "https://files.ballistica.net/cache/ba1/f7/3e/c5b8ea54a63e532b3aa70fb56ed0", + "build/assets/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/cd/ea/b7fb7c2ffc222f289d10cea32600", + "build/assets/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/74/28/01d4092e952681f6e2717c277719", + "build/assets/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/cc/ad/6d8f08f6ba82073e77b43b720073", + "build/assets/workspace/ninjafightplug.py": "https://files.ballistica.net/cache/ba1/5c/81/461b88dcd868f050d85aa8ac3bc1", + "build/assets/workspace/onslaughtplug.py": "https://files.ballistica.net/cache/ba1/1c/02/97064ab6cd7f4e0018c4e7fa8282", + "build/assets/workspace/runaroundplug.py": "https://files.ballistica.net/cache/ba1/68/b6/f21cabe2df5f604ef02fee02082e", + "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/74/32/3753792e68f5dcc0a6e21178b79e", + "build/prefab/full/linux_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/4c/df/7ad49eb63fd93903a561639f0464", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/55/e3/053ff9dc235170fcf431d69ee4d2", + "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/ea/c0/cd7b9932a75e88c5531dd677c083", + "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/71/f0/aeadd7f99b8dc64b48bc9c926ebe", + "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/00/49/e47efddc17c6668697ad9779615c", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/e4/e7/3ec91b98817f29d17c95ee7bdee3", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/4a/45/0299b8fe6a228b7ae3bd91edc606", + "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/cb/e2/ce3e89dbfbdc312d4e539829292e", + "build/prefab/full/mac_arm64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/fa/47/e5979c9a446e163af5758605d3ef", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/ea/70/f351a3472d7737b896e27be0b0ce", + "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/09/52/fce56e0d6da0d2017b577a625eb5", + "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "https://files.ballistica.net/cache/ba1/cf/77/dff517033f2cf52abd760846e3be", + "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "https://files.ballistica.net/cache/ba1/83/ee/5e815a581095bc5cc4e9f4b0c520", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/40/a7/2b8acce6af212a196392d86d50c2", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "https://files.ballistica.net/cache/ba1/36/17/ea670f5195251b7b926b7b271488", + "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/f3/08/b1b63814b180a7b5f507c474cf1c", + "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "https://files.ballistica.net/cache/ba1/6f/a1/ac1dcfc0b1b8c6c55fb770224e74", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/1e/7d/e043c1244cae9148ae1c070daa6c", + "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "https://files.ballistica.net/cache/ba1/ac/34/50aa60e470ebdfcf0ffb10577088", + "build/prefab/lib/linux_arm64_gui/debug/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/df/11/f2087bd3edce7376be6e6113bfe0", + "build/prefab/lib/linux_arm64_gui/release/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/aa/29/08d7c589821a466b966f79249394", + "build/prefab/lib/linux_arm64_server/debug/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/97/f2/7c6e1c10b9677759ee72d73added", + "build/prefab/lib/linux_arm64_server/release/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/a5/9d/24b5abd63de894c4096b24265648", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/41/e3/f96e9c1d349d602f66325e85f435", + "build/prefab/lib/linux_x86_64_gui/release/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/8d/be/b718641a2c1c3f931dcf0d88065c", + "build/prefab/lib/linux_x86_64_server/debug/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/01/f0/9e76e99a3284dfdde4cfc6622273", + "build/prefab/lib/linux_x86_64_server/release/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/7d/92/b9df2f5c52fd94a0c23d1ae98f3b", + "build/prefab/lib/mac_arm64_gui/debug/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/a9/4e/87c56e3ce65dd27bc8a446475e10", + "build/prefab/lib/mac_arm64_gui/release/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/7c/15/629fd97be2cd9765da062ac74224", + "build/prefab/lib/mac_arm64_server/debug/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/90/e6/7e488e65e5f6bbaf513ac93610dd", + "build/prefab/lib/mac_arm64_server/release/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/cb/ce/d167f9973a701af55e3d5cbc9ef8", + "build/prefab/lib/mac_x86_64_gui/debug/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/7d/46/4026492e6490314f1bf84dce63fc", + "build/prefab/lib/mac_x86_64_gui/release/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/d7/c4/ff0ab9a031720bd1889970f95f94", + "build/prefab/lib/mac_x86_64_server/debug/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/00/23/6094e9f386f10c2608835d0d46bc", + "build/prefab/lib/mac_x86_64_server/release/libballisticakit_internal.a": "https://files.ballistica.net/cache/ba1/61/2a/d5ed8af50aaa5093fa93f45db8fc", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericInternal.lib": "https://files.ballistica.net/cache/ba1/2c/77/67476d4a50afcebe5ddaaa119ac6", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/00/52/d70b1c8a9ca6f3b880e1c4949667", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/ce/54/3fcdb89a95a44b08a68e9d496810", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/e5/1c/ff55fbe2c6974309d4fd63a5cb92", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericInternal.lib": "https://files.ballistica.net/cache/ba1/bb/52/b845911893d44827c2c53033adc6", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/53/c4/e3235f843f36dc5da105141c794c", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/2d/61/d3810db88e77eb5f546ed4129978", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/5d/32/e4c3b0fc1fe33da29ab9e0babe36", + "src/assets/ba_data/python/babase/_mgen/__init__.py": "https://files.ballistica.net/cache/ba1/52/c6/c11130af7b10d6c0321add5518fa", + "src/assets/ba_data/python/babase/_mgen/enums.py": "https://files.ballistica.net/cache/ba1/38/c3/1dedd5e74f2508efc5974c8815a1", + "src/ballistica/base/mgen/pyembed/binding_base.inc": "https://files.ballistica.net/cache/ba1/b4/3d/e352190a0e5673d101c0f3ee3ad2", + "src/ballistica/classic/mgen/pyembed/binding_classic.inc": "https://files.ballistica.net/cache/ba1/b2/fd/f5c362e9093b2d0721fb59fc0056", + "src/ballistica/core/mgen/pyembed/binding_core.inc": "https://files.ballistica.net/cache/ba1/71/b8/c020cbb7f9a7ce3f347ccadd3df1", + "src/ballistica/core/mgen/pyembed/env.inc": "https://files.ballistica.net/cache/ba1/7c/74/9b014c32c937757bf3ed36a4f848", + "src/ballistica/core/mgen/python_modules_monolithic.h": "https://files.ballistica.net/cache/ba1/e4/87/ca448e63b14d8c292e2c32c35d5d", + "src/ballistica/scene_v1/mgen/pyembed/binding_scene_v1.inc": "https://files.ballistica.net/cache/ba1/4d/d5/a6e9feabfdf60ac6d78ee7983427", + "src/ballistica/template_fs/mgen/pyembed/binding_template_fs.inc": "https://files.ballistica.net/cache/ba1/fe/97/a8e045105bf910b8921daf3fad83", + "src/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc": "https://files.ballistica.net/cache/ba1/df/8d/fb958de2a7592fc5b4d2d3f7a81f" } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 068328e0..e76b0d37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Install dependencies run: tools/pcommand install_pip_reqs - name: Run checks and tests @@ -35,7 +35,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Compile binary run: make _cmake-simple-ci-server-build @@ -51,7 +51,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.gitignore b/.gitignore index 2768560b..d5e260ad 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,6 @@ gen/ # ew - want to ignore libs built by NDK but not a few specific others.. libs/ -!**/GameCircleSDK/libs !**/PlatformSDK/Android/libs # Visual Studio @@ -81,31 +80,19 @@ libs/ !pythonw_d.exe !**/OculusSDK/Tools/**/*.exe -# Note: specifying exact Debug/Release dirs for now; we wind up ignoring -# some files under LibOVR if we try to glob these together. -ballisticacore-windows/Debug -ballisticacore-windows/Release -ballisticacore-windows/BallisticaCore/Debug -ballisticacore-windows/BallisticaCore/Release -ballisticacore-windows/BallisticaCoreHeadless/Debug -ballisticacore-windows/BallisticaCoreHeadless/Release -ballisticacore-windows-oculus/BallisticaCoreVR/Projects/Windows/VS2015/Debug -ballisticacore-windows-oculus/BallisticaCoreVR/Projects/Windows/VS2015/Release - # Android Studio # I'm keeping most IDEA related files out of git and only including gradle stuff; # It's nice to be able to cleanly re-import the project. # As time goes on, I'm making explicit exceptions for some key files (dictionaries, etc) # which are nice to keep around. -ballisticacore-android/**/*.iml +ballisticakit-android/**/*.iml .gradle -ballisticacore-android/.idea/* +ballisticakit-android/.idea/* # Android Studio Stuff we *DO* want to keep -!ballisticacore-android/.idea/dictionaries -!ballisticacore-android/.idea/inspectionProfiles -!ballisticacore-android/.idea/codeStyles - +!ballisticakit-android/.idea/dictionaries +!ballisticakit-android/.idea/inspectionProfiles +!ballisticakit-android/.idea/codeStyles # NDK .cxx @@ -114,31 +101,28 @@ ballisticacore-android/.idea/* Thumbs.db # Asset master source link -assets/src_master - -# Asset build cache -.assetcache +src/assets/src_master # XCode xcuserdata/ -# Generated sources -/src/ballistica/generated -/assets/src/ba_data/python/ba/_generated -/src/meta/bametainternal/generated +# Meta-generated sources +/src/ballistica/mgen +/src/ballistica/*/mgen +/src/assets/ba_data/python/*/_mgen +/src/meta/*/mgen # Dynamically generated resource files -/ballisticacore-android/BallisticaCore/src/cardboard/res/drawable-*/vr_icon.png -/ballisticacore-android/BallisticaCore/src/cardboard/res/drawable-*/vr_icon_background.png -/ballisticacore-android/BallisticaCore/src/cardboard/res/drawable-*/icon.png -/ballisticacore-android/BallisticaCore/src/main/res/drawable-*/banner.png -/ballisticacore-android/BallisticaCore/src/main/res/mipmap-*/ic_launcher*.png -/ballisticacore-android/BallisticaCore/src/cardboard/res/mipmap-*/ic_launcher*.png -BallisticaCore.ico -/ballisticacore-xcode/BallisticaCore Shared/Assets.xcassets/AppIcon iOS.appiconset/icon_*.png -/ballisticacore-xcode/BallisticaCore Shared/Assets.xcassets/AppIcon macOS.appiconset/icon_*.png -/ballisticacore-xcode/BallisticaCore Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Layer*.imagestacklayer/Content.imageset/*.png -/ballisticacore-xcode/BallisticaCore Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Layer*.imagestacklayer/Content.imageset/*.png -/ballisticacore-xcode/BallisticaCore Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/*.png -/ballisticacore-xcode/BallisticaCore Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/*.png - +/ballisticakit-android/BallisticaKit/src/cardboard/res/drawable-*/vr_icon.png +/ballisticakit-android/BallisticaKit/src/cardboard/res/drawable-*/vr_icon_background.png +/ballisticakit-android/BallisticaKit/src/cardboard/res/drawable-*/icon.png +/ballisticakit-android/BallisticaKit/src/main/res/drawable-*/banner.png +/ballisticakit-android/BallisticaKit/src/main/res/mipmap-*/ic_launcher*.png +/ballisticakit-android/BallisticaKit/src/cardboard/res/mipmap-*/ic_launcher*.png +BallisticaKit.ico +/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/AppIcon iOS.appiconset/icon_*.png +/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/AppIcon macOS.appiconset/icon_*.png +/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Layer*.imagestacklayer/Content.imageset/*.png +/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Layer*.imagestacklayer/Content.imageset/*.png +/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/*.png +/ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets/tvOS App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/*.png diff --git a/.idea/ballisticacore.iml b/.idea/ballisticakit.iml similarity index 62% rename from .idea/ballisticacore.iml rename to .idea/ballisticakit.iml index 50ee3594..e0bf8510 100644 --- a/.idea/ballisticacore.iml +++ b/.idea/ballisticakit.iml @@ -2,31 +2,29 @@ - + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + @@ -65,7 +63,8 @@ - + + @@ -74,4 +73,4 @@ - \ No newline at end of file + diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index a1be411a..7752a7dd 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -1,6 +1,7 @@ + _babase aaaa aaab aaac @@ -121,6 +122,8 @@ appstate appstore apptime + apptimer + apptimers apputils archivepath archivepathbase @@ -189,29 +192,35 @@ awaitable awaitables axismotion + babase + babasemeta bacfg backgrounded backgrounding backporting backwin + baclassic bacloud bacloudcmd bacommon badguy + baenv bafoundation bainternal ballistica ballistica's - ballisticacore - ballisticacorecb + ballisticakit + ballisticakitcb bamaster bamasteraddr bamasterlegacy bameta bametainternal + baplus barcolor barebones bargs + bascenev baseaddr baseh baseimps @@ -220,6 +229,7 @@ baservnode basespaz basetime + basetimer baseurl baseval basew @@ -229,9 +239,11 @@ basnmessagesender basntoclient bastd + batemplatefs batocloud batools batoolsinternal + bauiv baworker bbbb bblk @@ -272,6 +284,7 @@ bmag bmas bmasl + bmcjphh bname bndl boffs @@ -281,6 +294,7 @@ bombsquadgame bools bootlocale + bootlog bootsubprocess borhani bot's @@ -530,6 +544,7 @@ copyswiftlibs cornerpin coroutines + correcthash countdownsounds cpbd cpnf @@ -605,10 +620,12 @@ dcls dcmake deathmatch + debugger's deek defs defsline deivit + depchecks depcls depdata depdatas @@ -651,6 +668,8 @@ disllowed dispatchmethod displayname + displaytime + displaytimer distro distroot distros @@ -871,6 +890,7 @@ fdesc fdict fdout + featureset fecfc feedparser fentry @@ -1050,6 +1070,7 @@ getcollide getcollidemodel getcollision + getcollisionmesh getconf getconfig getcurrency @@ -1068,6 +1089,7 @@ getlog getlogin getmaps + getmesh getmodel getname getnodes @@ -1084,6 +1106,7 @@ getscanresults getscoreconfig getsession + getsimplesound getsockname getsound getspaz @@ -1091,6 +1114,7 @@ gettext gettexture gettime + getuisound gfile gfxb ghnc @@ -1145,6 +1169,7 @@ hashtype hashval hasstarted + hastouchscreen hatmotion hattach hcalc @@ -1180,6 +1205,7 @@ hostconfig hostingconfig hostingstate + hostsession hostuser hout howtoplay @@ -1205,6 +1231,7 @@ ifeq ifneq iiarcade + iiiiisssi iircade ilang ilck @@ -1357,6 +1384,7 @@ langtarget langutils langval + lanst larmbeast lasthash lastline @@ -1387,7 +1415,7 @@ levelstr lfull lfval - libballisticacore + libballisticakit libbz libbzip libcrypto @@ -1556,6 +1584,7 @@ memfunctions menubar meryu + mesh messagetype metallink metamakefile @@ -1568,6 +1597,7 @@ mhend mhsh microprotocols + microsecs mikirog millisecs mimetypes @@ -1761,9 +1791,11 @@ nyko obj's objb + objcount objid objname objs + objset objsizes objt objtoyaml @@ -1785,6 +1817,7 @@ oival okbtn oldbook + olde oldlady oldtoken onln @@ -1964,6 +1997,7 @@ poweruptype powervr ppos + pppp ppre pproxy pptabcom @@ -1974,6 +2008,7 @@ precommand precompiling preconfig + predeclares preexec prefablib preflightfast @@ -2030,6 +2065,7 @@ proxykey prtb prunedir + prunetask prval pstats pstr @@ -2100,6 +2136,7 @@ pygram pyio pylib + pylibpath pylibs pylint pylintfast @@ -2141,6 +2178,7 @@ rawkey rawpath rawpaths + rayd rcade rcfile rcva @@ -2153,6 +2191,7 @@ realpath realsies recache + reconverge recursed recursing recv @@ -2172,6 +2211,7 @@ rentry representer reprlib + repro reqs reqtype reqtypes @@ -2264,6 +2304,7 @@ scenefile scenefiles scenename + scenepacket sched sclx scly @@ -2601,6 +2642,7 @@ tempfile tempfilepath templatecb + templatefs tenum termcolors termios @@ -2628,9 +2670,11 @@ testpath testpt testresponse + testrunnable testsealable testsentinel testsoundtrack + testtask testthread testval testwith @@ -2699,6 +2743,7 @@ tournamententry tournamentscores tpartial + tpexl tpimport tpimportex tpimports @@ -2886,6 +2931,7 @@ whatevs whatisv wheee + wheeee whos widgetdeathtime wiimote diff --git a/.idea/inspectionProfiles/Default.xml b/.idea/inspectionProfiles/Default.xml index 9bcf896e..4e8b4a5b 100644 --- a/.idea/inspectionProfiles/Default.xml +++ b/.idea/inspectionProfiles/Default.xml @@ -49,6 +49,7 @@ diff --git a/.idea/jsonSchemas.xml b/.idea/jsonSchemas.xml index 5dd84805..59f321a2 100644 --- a/.idea/jsonSchemas.xml +++ b/.idea/jsonSchemas.xml @@ -13,7 +13,8 @@ @@ -23,4 +24,4 @@ - \ No newline at end of file + diff --git a/.idea/misc.xml b/.idea/misc.xml index bc77e743..3ea7c1cb 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/.idea/modules.xml b/.idea/modules.xml index 23614f17..33afc9e9 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + diff --git a/.idea/scopes/PyIgnoreProtectedAccess.xml b/.idea/scopes/PyIgnoreProtectedAccess.xml index 3b276099..fda03c3e 100644 --- a/.idea/scopes/PyIgnoreProtectedAccess.xml +++ b/.idea/scopes/PyIgnoreProtectedAccess.xml @@ -1,3 +1,3 @@ - - \ No newline at end of file + + diff --git a/.idea/scopes/PyIgnoreUnresolved.xml b/.idea/scopes/PyIgnoreUnresolved.xml index 9adf83e2..25bc847f 100644 --- a/.idea/scopes/PyIgnoreUnresolved.xml +++ b/.idea/scopes/PyIgnoreUnresolved.xml @@ -1,3 +1,3 @@ - - \ No newline at end of file + + diff --git a/.idea/scopes/UncheckedPython.xml b/.idea/scopes/UncheckedPython.xml index 7dbb3a2b..95e415de 100644 --- a/.idea/scopes/UncheckedPython.xml +++ b/.idea/scopes/UncheckedPython.xml @@ -1,3 +1,3 @@ - - \ No newline at end of file + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 082b936b..a047a33d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,214 +1,759 @@ -### 1.7.20 (build 21005, api 7, 2023-01-21) -- Started work on the ba.app.components subsystem which will be used by different app-modes, plugins, etc. to override various app functionality. +### 1.7.20 (build 21015, api 8, 2023-05-05) + +- This seems like a good time for a `refactoring` release in anticipation of + changes coming in 1.8. Basically this means that a lot of things will be + getting moved or renamed, though their actual functionality should remain + mostly the same. This will allow modders to prepare for some of what is coming + in 1.8 without having to worry about functionality changing also. Hopefully + this will be easier than dumping everything at once when 1.8.0 drops. +- Bumped api-version from 7 to 8. There will be enough breaking changes here + that I think it's a good thing to force modders to explicitly check/update + their stuff. +- Started work on the `ba.app.components` subsystem which will be used by + different app-modes, plugins, etc. to override various app functionality. +- Removed telnet support. This never worked great, has been disabled in server + builds for a while now, and cloud console mostly eliminates its use case. +- Added the `baclassic` package. As more modern stuff like app-modes, + squads-mode, and scene-versions start to come online, code specific to more + hard-coded classic ways of doing things will get migrated here to keep things + clean and maintainable. Though there are no plans to remove classic + functionality from the game anytime soon, this functionality may become + unavailable in some contexts such as when modding cloud servers. +- Added a `baclassic.ClassicSubsystem` singleton accessible as `ba.app.classic`. + Various older bits from `ba.app` and elsewhere will start to be migrated + there. Note that the value for `ba.app.classic` can be None if classic is not + present, so code should try to handle that case cleanly when possible. +- Moved a number of attributes and methods from `ba.app` to `ba.app.classic`. + This includes things like `spaz_appearances`, `campaigns`, and `maps`. +- `ba.app.accounts_v1` is now `ba.app.classic.accounts`. +- `ba.app.accounts_v2` is now `ba.app.accounts`. Going forward, most all account + functionality should go through this native v2 stuff. +- 'Model' and 'CollideModel' are now known as 'Mesh' and 'CollisionMesh' + respectively. I've been wanting to make this change for a while since that + more accurately describes what is currently stored in a .bob/.cob file. I + would like to reserve the term 'Model' for use in the future; probably for + something that can represent multiple meshes, collision-meshes, shading, etc. + wrapped up into a single unit. To update your code for this change, just + search for all variations of 'model', 'Model', 'collide_model', ' + CollideModel', etc. and replace them with the equivalent ' Mesh' or + 'CollisionMesh' forms. There should be no remaining uses of 'model' in + ballistica currently so you should be able to track everything down this way. +- Added the `bascenev1` package. Scene-versions are a major upcoming feature in + 1.8 which for the first time will allow us to make substantial additions and + changes to low-level game-related types such as nodes, models, and textures + without breaking backwards compatibility. (bascenev2, etc.) The first step of + this process will be to move all existing gameplay types into `bascenev1`. So + instead of looking like: `import ba; ba.newnode()`, gameplay code might look + more like `import bascenev1 as bs; bs.newnode()` (Wheeee 'bs' is back!). +- Added the `bauiv1` package. This contains all of the existing user-interface + functionality. Similar to `bascenev1`, most existing UI code now uses the + convention `import bauiv1 as bui`. This versioning will allow us to evolve + nicer UI systems in the future (bauiv2, etc.) while keeping existing UIs + functional. +- Many common bits from `ba` are now available from `bascenev1` and/or `bauiv1`. + For instance, `bascenev1.app` and `bauiv1.app` are the same as `ba.app`. The + goal is that most gameplay related modules should only need to import + `bascenev1` and most UI related modules only `bauiv1` to keep things as simple + as possible. The `ba` package is now mainly a common repository of + functionality for these client-facing packages to pull from and should not + often need to be used directly. +- There is no longer a 'ui' context. Previously, lots of common functionality + would differ in behavior when executed under the 'ui' context. For example, + `ba.screenmessage()` under the 'ui' context would simply print to the local + device's screen, whereas when called under a game hosting context it would + result in messages sent to all game clients. Now, however, there are instead + unique versions for gameplay (`bascenev1.screenmessage()`) and ui + (`bauiv1.screenmessage()`). These versions may differ in arguments and + functionality; see docs for details. In general, the `ui` versions no longer + care what context they are running under; their results will always just apply + to the local device. +- The `ba.Context` class has been reworked a bit and is now `ba.ContextRef` to + more accurately describe what it actually is. The default constructor + (`ba.ContextRef()`) will grab a reference to the current context. To get a + context-ref pointing to *no* context, do `ba.ContextRef.empty()`. UI stuff + will now insist on being run with no context set. To get references to + Activity/Session contexts, use the context() methods they provide. +- The following have been split into `bascenev1` and `bauiv1` versions: + `screenmessage()`, `gettexture()`, `getsound()`, `getmesh()`, + `getcollisionmesh()`, `getdata()`. +- The `_bainternal` module (the closed source parts of the app) is now the + `baplus` package. There were too many things with 'internal' in the name and + it was starting to get confusing. Also, the goal is for this to be an optional + thing at some point and I feel 'plus' better fits that role; ' internal' + sounds like something that is always required. +- Added the general concept of 'feature-sets' to the build system. A feature-set + consists of a high level subset of the app source that can be included or + excluded for different builds. The current list of feature-sets is `scene_v1`, + `ui_v1`, `classic`, and `plus`. Tests are being added to ensure that + feature-sets remain cleanly independent of eachother and also that the app can + be built and run without *any* feature-sets present. Stay tuned for more info + as things evolve, but the general idea is that feature-sets will allow us to + isolate and test new functionality in an efficient way and also will allow + 'spinoff' projects to strip out parts of the app they don't need or add in new + custom parts on the top of the base set. Modders interested in ' total + conversions' may want to keep an eye on this. +- There is no longer a standalone `ba.playsound()` function. Both ui-sounds ( + acquired via `bauiv1.getsound()` and scene-sounds (acquired via + `bascenev1.getsound()`) now have a play() method on them for this purpose. So + just search for any instances of 'playsound' in your code and change stuff + like `ba.playsound(ba.getsound('error'))` to `bs.getsound('error').play()`. + Playing sounds in timers is now especially nicer looking; instead of + `ba.timer(1.0, ba.Call(ba.playsound, my_sound))` you can now simply do + `bs.timer(1.0, my_sound.play)` +- Since time functionality needs to be split between ui and scene versions + anyway, I'm taking the opportunity to revise ballistica's time concepts. I + revamped these in 1.5, and, after working with them for a few years, I feel + that having a single time(), timer(), and Timer() call with a variety of + arguments influencing behavior is unwieldy, so I'll be splitting things out + into a few seperate and simplified versions. Details follow. +- There is now the concept of 'app-time'. This was previously called ' + real-time'. It is basically time that has elapsed while the app is actively + running. It never jumps ahead or goes backwards and it stops progressing while + the app is suspended (which is why I feel the term 'real-time' was a bit + misleading). +- `ballistica::GetRealTime()` in the C++ layer is now + `ballistica::GetAppTimeMillisecs()`. +- App-time is now stored internally in microseconds instead of milliseconds, and + there is a `ballistica::GetAppTimeMicrosecs()` call to retrieve the full + resolution value. +- A number of calls, including the various time/timer functions listed below, + now always accept time as float seconds and no longer accept a ba.TimeFormat + value to do otherwise. TimeFormat was basically a way to transition elegantly + from milliseconds to seconds everywhere, but it has been long enough now that + we should simplify things. If you are passing + timeformat=ba.TimeFormat.MILLISECONDS anywhere, simply divide your value by + 1000 now instead to make it seconds. +- In Python there is now an `apptime()` function to get current app-time in + seconds, an `apptimer()` function to set a timer based on app-time, and an + `AppTimer()` class to get an adjustable/cancelable timer. There is also an + `AppTime` type which is technically just float but which can be used by + type-checkers to keep these time values from being accidentally mixed with + others. All of these are available in `ba`, `bauiv1`, and `bascenev1`. +- There is now the concept of `display-time` which is a value that progresses + *mostly* at the same speed as app-time, but in a way that tries to advance at + a constant rate per local frame drawn, which is useful for visual purposes + such as UI animations. Trying to instead use app-time in these situations may + lead to visual jitters since actual times between frame draws may not always + be constant. Display-time avoids this problem, trading off technical time + accuracy for visual smoothness. Be aware that display-time updates may be very + sparse (like 10 per second) if the app is running in headless mode. +- There is now a `displaytime()` function to get current display-time in + seconds, a `displaytimer()` function to set a timer based on display-time, and + a `DisplayTimer()` class for an adjustable timer. `DisplayTime` is the custom + type for these time values (though again, outside of the type-checker it is + simply a float). +- Within scenes, there is the concept of 'scene-time' or simply just 'time'. + This was previously called 'sim-time' and is the default time value that most + gameplay code should deal with. When speeding up or slowing down or pausing a + game, it is the rate of scene-time progression that is actually changing. +- The `bascenev1.time()` function now gets the current scene-time in seconds, + the `bascenev1.timer()` function sets a timer based on scene-time, and + `bascenev1.Timer()` class gives an adjustable timer. `bascenev1.Time` is the + custom type for these time values (though again, outside of the type-checker, + it is simply a float). These names are the same as ballistica's previous + unified time calls, but they no longer have the options to return values in + milliseconds or operate on other time types. Just do `int(bs.time() * 1000)` + if you need milliseconds. +- The 'base-time' concept within scenes remains. Base-time can be thought of as + a metronome - it progresses constantly for a scene even if the scene is paused + or sped up or slowed down. Some factors, however, can still cause it to speed + up or slow down, including changing playback rate in a replay or excess cpu + load causing it to progress slower than normal. +- There is now a `bascenev1.basetime()` function to get the current base-time in + seconds, a `bascenev1.basetimer()` call to set a timer using base-time, and a + `bascenev1.BaseTimer` class for an adjustable timer. `bascenev1.BaseTime` is + the custom type for these values, though again it is simply a float outside of + the type checker. +- Reworked frame scheduling to be much more general and no longer assume 60fps ( + basically using the new 'display-time' concept). The engine should now be + better at maintaining smooth looking animation at other frame-rates. Please + holler if you see otherwise. Note this doesn't affect the issue where pure SDL + builds like PC/Linux are locked to 60fps; that's a seperate thing. +- You can set env-var `BA_DEBUG_LOG_DISPLAY_TIME=1` to get display-time stat + logs to make sure things are working smoothly on your setup. +- The engine no longer requires that ba_data and other required files exist in + the current working dir. This assumption meant the engine would at some point + `chdir()` to where those files live, which felt dirty and complicated passing + command line args or using ballistica functionality as part of scripts. There + is now `ba.app.data_directory` which shows where the app is looking for its + stuff. It is also now possible to specify this directory on the command-line + via `--data-dir` or `-d`. Note that some platforms/setups may choose to + `chdir()` to that dir *before* spinning up the engine (to get clean relative + paths in stack traces/etc.), but the engine itself no longer forces this. +- The `-cfgdir` command-line arg has been renamed to be either `--config-dir` or + `-C`. +- The `-exec` command-line arg has been renamed to be either `--exec` or `-e`. +- Added a command arg accessible via `--command` or `-c`. Unlike the exec arg + which runs as part of the app event loop, this command runs *instead* of the + normal event loop. It can be thought of as analagous to the `-c` arg for the + Python interpreter. This provides a clean way to do things like introspect + ballistica's binary modules without having to worry about data files being + present or about exiting the app after the command runs. The app simply + bootstraps its Python interpreter, runs this command, and then exits. +- Moved bootstrapping code from a few different places such as ba._bootstrap to + a standalone `baenv` module. Calling `baenv.configure()` will set up various + Python things such as script paths, logging, stdout redirection, and signal + handling. Default runs of the app will do this as the very first thing, but it + will also be possible to skip this and use ballistica functionality in a more + 'vanilla' Python environment. Running ballistica in the following way should + be essentially the same as a 'default' run: `PYTHONPATH=ba_data/python + ./ballisticakit -c 'import baenv; baenv.configure(); import ba; + ba.app.run()'`. +- Related to the above, it is now possible for `ba.app.python_directory_app`, + `ba.app.python_directory_user`, and `ba.app.python_directory_app_site` to be + None if ballisticia is being run in a non-standard environment setup. Just + something to watch out for. +- The `ba` module is no longer imported by default. Since most modding will go + through other modules now such as `bascenev` or `bauiv1` it seemed odd to be + importing only `ba`. +- Starting to move the 'spinoff' system into the public repo (things like + tools/spinoff and tools/batools/spinoff). This is what will be used to make + filtered standalone versions of ballistica. More on this soon. +- Python dummy-modules are now always generated on an as-needed basis (when + running things like `make mypy`) and live in build/dummymodules instead of + under assets src. +- Added a help command accessible via '--help' or '-h'. Prints available command + line args/etc. +- Mods dir can now be overridden via '--mods-dir' or '-m' command line args. +- Ballistica has been updated to use Python 3.11 (with all bundled Python + versions set to 3.11.3). +- Cleaned up bundled Python builds a bit; they now include a number of + previously-not-included modules such as the rather tricky to compile 'ctypes'. + Also some modules that relied on native parts that we are not building have + been filtered out. If you come across any bundled modules that don't import or + there are any standard modules that you would like to have which are currently + excluded, please holler. +- Fixes an issue where holding a key while bringing up the chat window could + leave the player moving in the same direction. +- The ballistica project config file has been renamed from 'config/config.json' + to 'config/projectconfig.json'. There's a fair amount of other stuff in the + config dir these days so this helps keep things clear. +- The resources directory is now 'src/resources' (though this is mostly not + present in the public repo as of yet, but it should appear at some point). +- Similarly, the assets directory is now 'src/assets' and assets get built to ' + build/assets'. This simplifies a lot of project logic in terms of which files + get blown away during cleans, which get ignored by syncs, etc. (a single big + src and build dir is simpler than lots of little ones). +- Shortened some names for meta-generated sources. Something like ' + ballistica/generated/python_embedded/foo.inc' might now look more like ' + ballistica/mgen/pyembed/foo.inc'. Some include paths were starting to get + ridiculously long so this will save a bit of space, especially as + meta-generated code is set to become a bigger deal soon. +- Renamed BallisticaCore to BallisticaKit. This is simply the default project + name and is replaced by an actual project name such as 'BombSquad' in spun-off + projects. Because there is now a 'core' feature set, this name was feeling a + bit ambiguous. I also feel 'core' sounds like a small subset of a project and + 'kit' more accurately sounds like the entirety of a project. Also, in the + future, the default BallisticaKit app may be expanded with editing + functionality and I feel the name 'Kit' fits better for something used that + way than 'Core' does. ### 1.7.19 (build 20997, api 7, 2023-01-19) -- Fixes an issue where repeated curses could use incorrect countdown times (Thanks EraOSBeta!). -- Last manual party connect port is now saved. Previously, it always assumed the port to be 43210 (Thanks ritiek!). -- Added a plugin-settings window under the plugins UI which allows enabling/disabling all plugins and setting whether new plugins are auto-enabled (Thanks vishal332008!). -- Missing maps are now cleanly filtered out of playlists instead of causing errors/hangs (Thanks imayushsaini!). + +- Fixes an issue where repeated curses could use incorrect countdown times ( + Thanks EraOSBeta!). +- Last manual party connect port is now saved. Previously, it always assumed the + port to be 43210 (Thanks ritiek!). +- Added a plugin-settings window under the plugins UI which allows + enabling/disabling all plugins and setting whether new plugins are + auto-enabled (Thanks vishal332008!). +- Missing maps are now cleanly filtered out of playlists instead of causing + errors/hangs (Thanks imayushsaini!). - Added in-game-ping option under advanced settings (Thanks imayushsaini!). -- `BA_DEVICE_NAME` environment variable can now be used to change the name the local device shows up as. Handy if running multiple servers so you can tell them apart in cloud-console/etc. (Thanks imayushsaini!). +- `BA_DEVICE_NAME` environment variable can now be used to change the name the + local device shows up as. Handy if running multiple servers so you can tell + them apart in cloud-console/etc. (Thanks imayushsaini!). ### 1.7.18 (build 20989, api 7, 2023-01-16) -- Reworked some low level asynchronous messaging functionality in efro.message and efro.rpc. Previously these were a little *too* asynchronous which could lead to messages being received in a different order than they were sent, which is not desirable. -- Added a way to suppress 'Your build is outdated' messages at launch (see `ba._hooks.show_client_too_old_error()`). + +- Reworked some low level asynchronous messaging functionality in efro.message + and efro.rpc. Previously these were a little *too* asynchronous which could + lead to messages being received in a different order than they were sent, + which is not desirable. +- Added a way to suppress 'Your build is outdated' messages at launch ( see + `ba._hooks.show_client_too_old_error()`). ### 1.7.17 (build 20983, api 7, 2023-01-09) -- V2 accounts now show a 'Unlink Legacy (V1) Accounts' button in account settings if they have any old V1 links present. This can be used to clear out old links to replace them with V2 links which work correctly with V2 accounts. -- `ba.internal.dump_tracebacks()` is now `ba.internal.dump_app_state()` and `ba.internal.log_dumped_tracebacks()` is now `ba.internal.log_dumped_app_state()`. This reflects the fact that these calls may be expanded to include other app state in the future (C++ layer thread states, etc.). -- Added `ba.app.health_monitor` which will dump app state if the logic thread ever stops responding for 5+ seconds while the app is running (to help diagnose deadlock situations). -- Various extra logging and bug fixes related to V2 accounts and master server communication (trying to get this stuff working as smoothly as possible now that it is feature-complete). + +- V2 accounts now show a 'Unlink Legacy (V1) Accounts' button in account + settings if they have any old V1 links present. This can be used to clear out + old links to replace them with V2 links which work correctly with V2 accounts. +- `ba.internal.dump_tracebacks()` is now `ba.internal.dump_app_state()` and + `ba.internal.log_dumped_tracebacks()` is now + `ba.internal.log_dumped_app_state()`. This reflects the fact that these calls + may be expanded to include other app state in the future (C++ layer thread + states, etc.). +- Added `ba.app.health_monitor` which will dump app state if the logic thread + ever stops responding for 5+ seconds while the app is running (to help + diagnose deadlock situations). +- Various extra logging and bug fixes related to V2 accounts and master server + communication (trying to get this stuff working as smoothly as possible now + that it is feature-complete). ### 1.7.16 (build 20969, api 7, 2022-12-18) -- Fixed a bug where profile names encased in curly brackets could cause harmless error messages. -- Android will no longer log errors on ba.open_url() calls if a browser is not available (it still just falls back to the in-app dialog in that case). -- The 'Upgrade' button for device accounts now signs you out and closes the upgrade window to hopefully make it more clear that you need to sign in with your newly created/upgraded BombSquad account. -- Fixed a bug where the remote app could not connect for the first 5 seconds after launching the app. -- Added Malay language. Ick; apparently its been sitting done for a while and I hadn't realized it wasn't added to the game yet. Apologies!. And thanks to all contributors! -- Added 'enable_queue' server config setting. This defaults to True but can be turned off as a workaround for server owners targeted by queue spam attacks. -- The public party list no longer sorts servers without queues at the end of the list. This sorting was put there long ago to prioritize fancy new queue-supporting servers but now it would just make the few that opt out of queues hard to find. Doh. So opting out of queues is probably not a great idea until this build is widespread. -- Public uuids now only change once every 6 months or so instead of with every version bump. This way periods of heavy development won't put added strain on server owners trying to keep ban lists up to date and whatnot. -- Added a merch button in the in-game store that goes to the ballistica.net merch page (though it only shows up in the few countries where merch is available). + +- Fixed a bug where profile names encased in curly brackets could cause harmless + error messages. +- Android will no longer log errors on ba.open_url() calls if a browser is not + available (it still just falls back to the in-app dialog in that case). +- The 'Upgrade' button for device accounts now signs you out and closes the + upgrade window to hopefully make it more clear that you need to sign in with + your newly created/upgraded BombSquad account. +- Fixed a bug where the remote app could not connect for the first 5 seconds + after launching the app. +- Added Malay language. Ick; apparently its been sitting done for a while and I + hadn't realized it wasn't added to the game yet. Apologies!. And thanks to all + contributors! +- Added 'enable_queue' server config setting. This defaults to True but can be + turned off as a workaround for server owners targeted by queue spam attacks. +- The public party list no longer sorts servers without queues at the end of the + list. This sorting was put there long ago to prioritize fancy new + queue-supporting servers but now it would just make the few that opt out of + queues hard to find. Doh. So opting out of queues is probably not a great idea + until this build is widespread. +- Public uuids now only change once every 6 months or so instead of with every + version bump. This way periods of heavy development won't put added strain on + server owners trying to keep ban lists up to date and whatnot. +- Added a merch button in the in-game store that goes to the ballistica.net + merch page (though it only shows up in the few countries where merch is + available). ### 1.7.15 (build 20960, api 7, 2022-12-04) -- The cancel button on the 'Sign in with a Bombsquad Account' popup no longer respond to system cancel buttons (escape key, android back button, etc). Turns out some Android people were pressing back repeatedly to come back from a browser after signing in and immediately canceling their sign in attempts in the game before they completed. Hopefully this will avoid some frustration. -- Fixed an issue where back presses could result in multiple main menu windows appearing. + +- The cancel button on the 'Sign in with a Bombsquad Account' popup no longer + respond to system cancel buttons (escape key, android back button, etc). Turns + out some Android people were pressing back repeatedly to come back from a + browser after signing in and immediately canceling their sign in attempts in + the game before they completed. Hopefully this will avoid some frustration. +- Fixed an issue where back presses could result in multiple main menu windows + appearing. ### 1.7.14 (build 20958, api 7, 2022-12-03) -- Android Google Play logins now provide V2 accounts with access to all V2 features such as a globally-unique account tag, cloud-console, and workspaces. They should still retain their V1 data as well. -- V2 accounts now have a 'Manage Account' button in the app account window which will sign you into a browser with your current account. -- Removed Google App Invite functionality which has been deprecated for a while now. Google Play users can still get tickets by sharing the app via codes (same as other platforms). -- Updated Android root-detection library to the latest version. Please holler if you are getting new false 'your device is rooted' errors when trying to play tournaments or anything like that. -- Removed a few obsolete internal functions: `_ba.is_ouya_build()`, `_ba.android_media_scan_file()`. -- Renaming some methods/data to disambiguate 'login' vs 'sign-in', both in the app and on ballistica.net. Those two terms are somewhat ambiguous and interchangeable in English and can either be a verb or a noun. I'd like to keep things clear in Ballistica by always using 'sign-in' for the verb form and 'login' for the noun. For example: 'You can now sign in to your account using your Google Play login'. -- Fixed the 'your config is broken' dialog that shows on desktop builds if the game's config file is corrupt and can't be read. It should let you edit the config or replace it with a default. -- `ba.printobjects()` is now `ba.ls_objects()`. It technically logs and doesn't print so the former name was a bit misleading. -- Added `ba.ls_input_devices()` to dump debug info about the current set of input devices. Can be helpful to diagnose mysterious devices joining games unintentionally and things like that. -- Added 'raw' bool arg to `ba.pushcall()`. Passing True for it disables context save/restore and thread checks. -- Added `ba.internal.dump_tracebacks()` which can be used to dump the stack state of all Python threads after some delay. Useful for debugging deadlock; just call right before said deadlock occurs. Results will be logged on the next app launch if they cannot be immediately. -- Fixed a low level event-loop issue that in some cases was preventing the Android version from properly pausing/resuming the app or managing connections while in the background. If you look at the devices section on ballistica.net you should now see your device disappear when you background the app and reappear when you foreground it. Please holler if not. -- Device accounts are now marked as deprecated, and signing in with one now brings up an 'upgrade' UI which allows converting it to a V2 account. It is my hope to push the entire client ecosystem to V2 accounts as quickly as possible since trying to support both independent V1 accounts and V2 accounts is a substantial technical burden. -- Fixed an issue where Log calls made within `Thread::PushThreadMessage()` could result in deadlock. -- Fixed an issue where some Android hardware buttons could theoretically cause rogue game controller button presses (due to downcasting int values > 255 into a uint8 value). + +- Android Google Play logins now provide V2 accounts with access to all V2 + features such as a globally-unique account tag, cloud-console, and workspaces. + They should still retain their V1 data as well. +- V2 accounts now have a 'Manage Account' button in the app account window which + will sign you into a browser with your current account. +- Removed Google App Invite functionality which has been deprecated for a while + now. Google Play users can still get tickets by sharing the app via codes ( + same as other platforms). +- Updated Android root-detection library to the latest version. Please holler if + you are getting new false 'your device is rooted' errors when trying to play + tournaments or anything like that. +- Removed a few obsolete internal functions: `_ba.is_ouya_build()`, + `_ba.android_media_scan_file()`. +- Renaming some methods/data to disambiguate 'login' vs 'sign-in', both in the + app and on ballistica.net. Those two terms are somewhat ambiguous and + interchangeable in English and can either be a verb or a noun. I'd like to + keep things clear in Ballistica by always using 'sign-in' for the verb form + and 'login' for the noun. For example: 'You can now sign in to your account + using your Google Play login'. +- Fixed the 'your config is broken' dialog that shows on desktop builds if the + game's config file is corrupt and can't be read. It should let you edit the + config or replace it with a default. +- `ba.printobjects()` is now `ba.ls_objects()`. It technically logs and doesn't + print so the former name was a bit misleading. +- Added `ba.ls_input_devices()` to dump debug info about the current set of + input devices. Can be helpful to diagnose mysterious devices joining games + unintentionally and things like that. +- Added 'raw' bool arg to `ba.pushcall()`. Passing True for it disables + context_ref save/restore and thread checks. +- Added `ba.internal.dump_tracebacks()` which can be used to dump the stack + state of all Python threads after some delay. Useful for debugging deadlock; + just call right before said deadlock occurs. Results will be logged on the + next app launch if they cannot be immediately. +- Fixed a low level event-loop issue that in some cases was preventing the + Android version from properly pausing/resuming the app or managing connections + while in the background. If you look at the devices section on ballistica.net + you should now see your device disappear when you background the app and + reappear when you foreground it. Please holler if not. +- Device accounts are now marked as deprecated, and signing in with one now + brings up an 'upgrade' UI which allows converting it to a V2 account. It is my + hope to push the entire client ecosystem to V2 accounts as quickly as possible + since trying to support both independent V1 accounts and V2 accounts is a + substantial technical burden. +- Fixed an issue where Log calls made within + `EventLoopThread::PushThreadMessage()` could result in deadlock. +- Fixed an issue where some Android hardware buttons could theoretically cause + rogue game controller button presses (due to downcasting int values > 255 into + a uint8 value). ### 1.7.13 (build 20919, api 7, 2022-11-03) -- Android target-sdk has been updated to 33 (Android 13). Please holler if anything seems broken or is behaving differently than before on Android. -- Android back-button handling code had to be reworked a bit for sdk 33 (see https://developer.android.com/guide/navigation/predictive-back-gesture). Because of this, back buttons on gamepads or other special cases behave slightly differently, but hopefully still in a reasonable way. Please holler if you find otherwise. + +- Android target-sdk has been updated to 33 (Android 13). Please holler if + anything seems broken or is behaving differently than before on Android. +- Android back-button handling code had to be reworked a bit for sdk 33 ( see + https://developer.android.com/guide/navigation/predictive-back-gesture). + Because of this, back buttons on gamepads or other special cases behave + slightly differently, but hopefully still in a reasonable way. Please holler + if you find otherwise. ### 1.7.12 (build 20914, api 7, 2022-10-18) -- Disabled some live-objects warnings as it seems their use of certain gc module functionality might be causing some rare errors/crashes. On further inspection, it turns out that is technically expected. Basically those calls are useful for debugging but can break things. Added a note at the top of efro.debug elaborating on the situation. We can reimplement similar warnings later in a safe manner. -- Removed `ba._general.print_active_refs()` because the newer stuff in efro.debug does the same thing better. + +- Disabled some live-objects warnings as it seems their use of certain gc module + functionality might be causing some rare errors/crashes. On further + inspection, it turns out that is technically expected. Basically those calls + are useful for debugging but can break things. Added a note at the top of + efro.debug elaborating on the situation. We can reimplement similar warnings + later in a safe manner. +- Removed `ba._general.print_active_refs()` because the newer stuff in + efro.debug does the same thing better. - Bug fixes related to v2 account connections. ### 1.7.11 (build 20909, api 7, 2022-10-15) -- Switched our Python autoformatting from yapf to black. The yapf project seems to be mostly dead whereas black seems to be thriving. The final straw was yapf not supporting the `match` statement in Python 3.10. -- Added `has_settings_ui()` and `show_settings_ui()` methods to ba.Plugin. Plugins can use these to enable a 'Settings' button next to them in the plugin manager that brings up a custom UI. -- Fixed workspaces functionality, which I broke rather terribly in 1.7.10 when I forgot to test it against all the internal changes there (sorry). Note that there is a slight downside to having workspace syncing enabled now in that it turns off the fast-v2-relaunch-login optimization from 1.7.10. -- App should now show a message when workspace has been changed and a restart is needed for it to take effect. -- Fixed an issue where `ba.open_url()` would fall back to internal url display window on some newer Android versions instead of opening a browser. It should now correctly open a browser on regular Android. On AndroidTV/iiRcade/VR it will now always display the internal pop-up. It was trying to use fancy logic before to determine if a browser was available but this seemed to be flaky. Holler if this is not working well on your device/situation. -- The internal 'fallback' `ba.open_url()` window which shows a url string when a system browser is not available now has a qrcode and a copy button (where copy/paste is supported). -- Added a 'force_internal' arg to `ba.open_url()` if you would like to always use the internal window instead of attempting to open a browser. Now that we show a copy button and qr code there are some cases where this may be desirable. + +- Switched our Python autoformatting from yapf to black. The yapf project seems + to be mostly dead whereas black seems to be thriving. The final straw was yapf + not supporting the `match` statement in Python 3.10. +- Added `has_settings_ui()` and `show_settings_ui()` methods to ba.Plugin. + Plugins can use these to enable a 'Settings' button next to them in the plugin + manager that brings up a custom UI. +- Fixed workspaces functionality, which I broke rather terribly in 1.7.10 when I + forgot to test it against all the internal changes there (sorry). Note that + there is a slight downside to having workspace syncing enabled now in that it + turns off the fast-v2-relaunch-login optimization from 1.7.10. +- App should now show a message when workspace has been changed and a restart is + needed for it to take effect. +- Fixed an issue where `ba.open_url()` would fall back to internal url display + window on some newer Android versions instead of opening a browser. It should + now correctly open a browser on regular Android. On AndroidTV/iiRcade/VR it + will now always display the internal pop-up. It was trying to use fancy logic + before to determine if a browser was available but this seemed to be flaky. + Holler if this is not working well on your device/situation. +- The internal 'fallback' `ba.open_url()` window which shows a url string when a + system browser is not available now has a qrcode and a copy button (where + copy/paste is supported). +- Added a 'force_internal' arg to `ba.open_url()` if you would like to always + use the internal window instead of attempting to open a browser. Now that we + show a copy button and qr code there are some cases where this may be + desirable. ### 1.7.10 (build 20895, api 7, 2022-10-09) -- Added eval support for cloud-console. This means you can type something like '1+1' in the console and see '2' printed. This is how Python behaves in the stdin console or in-game console or the standard Python interpreter. -- Exceptions in the cloud-console now print to stderr instead of logging.exception(). This means they aren't a pretty red color anymore, but this will keep cloud-console behaving well with things like servers where logging.exception() might trigger alarms or otherwise. This is also consistent with standard interactive Python behavior. -- Cloud console now shows the device name at the top instead of simply 'Console' while connected. -- Moved the function that actually runs cloud console code to `ba._cloud.cloud_console_exec()`. -- Added efro.debug which contains useful functionality for debugging object reference issues and memory leaks on live app instances (via cloud shell or whatever). -- Lots of reworking/polishing in general on communication between the game and v2 regional/master servers in preparation of upgrading Google Play accounts to V2. Please holler if anything is not working smoothly with a V2 account. -- When establishing V2 master-server communication, if the closest regional server is down or too busy, will now fall back to farther ones instead of giving up. You can follow this process by setting env var `BA_DEBUG_PRINT_V2_TRANSPORT` to 1 when running the app. -- Network testing now skips the alternate v1 master server addr if the primary succeeded. The alternate often fails which makes things look broken even though the game is ok as long as primary works. -- The v2-transport system will now properly reestablish account connectivity when asked to refresh its connection (the cloud does this periodically so regional cloud servers can be restarted as needed). Practically this means your app won't stop showing up under the ballistica.net devices section after its been running for a while; a problem previous builds had. -- The v2-transport system can now establish more than one connection at a time, which allows the app to gracefully transition to a new connection when the old is about to expire without any period of no connectivity. To test this functionality, set env var `BA_DEBUG_PRINT_V2_TRANSPORT=1` to see transport debug messages and `BA_DEBUG_V2_TRANSPORT_SHORT_DURATION=1` to cause the cloud to request a connection-refresh every 30 seconds or so. -- V2 accounts now consider themselves instantly signed in if they were signed in when the app last ran. They still need to contact the master-server before anything important can happen, but this should help keep things feel faster in general. -- Due to v2-transport improvements, pressing the 'End Session Now' button in ballistica.net account settings should now instantly log you out of all apps using that session (ones that are online at least). Previously this would often not take effect until something like an app relaunch. -- Fixes an issue where the tournament entry window could remain stuck on top when following a 'get more tickets' link. (Thanks itsre3!) -- The main menu now says 'End Test' when in a stress test instead of 'End Game' (Thanks vishal332008!) -- Added 'discordLogo' and 'githubLogo' textures for anyone who wants to use those for UIs. + +- Added eval support for cloud-console. This means you can type something like ' + 1+1' in the console and see '2' printed. This is how Python behaves in the + stdin console or in-game console or the standard Python interpreter. +- Exceptions in the cloud-console now print to stderr instead of + logging.exception(). This means they aren't a pretty red color anymore, but + this will keep cloud-console behaving well with things like servers where + logging.exception() might trigger alarms or otherwise. This is also consistent + with standard interactive Python behavior. +- Cloud console now shows the device name at the top instead of simply 'Console' + while connected. +- Moved the function that actually runs cloud console code to + `ba._cloud.cloud_console_exec()`. +- Added efro.debug which contains useful functionality for debugging object + reference issues and memory leaks on live app instances (via cloud shell or + whatever). +- Lots of reworking/polishing in general on communication between the game and + v2 regional/master servers in preparation of upgrading Google Play accounts to + V2. Please holler if anything is not working smoothly with a V2 account. +- When establishing V2 master-server communication, if the closest regional + server is down or too busy, will now fall back to farther ones instead of + giving up. You can follow this process by setting env var + `BA_DEBUG_PRINT_V2_TRANSPORT` to 1 when running the app. +- Network testing now skips the alternate v1 master server addr if the primary + succeeded. The alternate often fails which makes things look broken even + though the game is ok as long as primary works. +- The v2-transport system will now properly reestablish account connectivity + when asked to refresh its connection (the cloud does this periodically so + regional cloud servers can be restarted as needed). Practically this means + your app won't stop showing up under the ballistica.net devices section after + its been running for a while; a problem previous builds had. +- The v2-transport system can now establish more than one connection at a time, + which allows the app to gracefully transition to a new connection when the old + is about to expire without any period of no connectivity. To test this + functionality, set env var `BA_DEBUG_PRINT_V2_TRANSPORT=1` to see transport + debug messages and `BA_DEBUG_V2_TRANSPORT_SHORT_DURATION=1` to cause the cloud + to request a connection-refresh every 30 seconds or so. +- V2 accounts now consider themselves instantly signed in if they were signed in + when the app last ran. They still need to contact the master-server before + anything important can happen, but this should help keep things feel faster in + general. +- Due to v2-transport improvements, pressing the 'End Session Now' button in + ballistica.net account settings should now instantly log you out of all apps + using that session (ones that are online at least). Previously this would + often not take effect until something like an app relaunch. +- Fixes an issue where the tournament entry window could remain stuck on top + when following a 'get more tickets' link. (Thanks itsre3!) +- The main menu now says 'End Test' when in a stress test instead of 'End Game' + (Thanks vishal332008!) +- Added 'discordLogo' and 'githubLogo' textures for anyone who wants to use + those for UIs. ### 1.7.9 (build 20880, api 7, 2022-09-24) -- Cleaned up the efro.message system to isolate response types that are used purely internally (via a new SysResponse type). + +- Cleaned up the efro.message system to isolate response types that are used + purely internally (via a new SysResponse type). - Fixed bug with 'Disable Camera Shake' option. (GitHub #511) (thanks Dliwk!) - Fixed an issue where Co-op football would play no music. - Accept "fairydust" as an emit type in `ba.emitfx()` (thanks ritiek!). - Added epic mode option to Easter Egg Hunt (thanks itsre3!). -- The game no longer auto-signs-in to a device account when first run since we want to start encouraging people to use V2 accounts. -- Removed support for GameCircle in Amazon builds (which has been discontinued for years at this point). +- The game no longer auto-signs-in to a device account when first run since we + want to start encouraging people to use V2 accounts. +- Removed support for GameCircle in Amazon builds (which has been discontinued + for years at this point). ### 1.7.8 (build 20871, api 7, 2022-09-21) + - Fixed tournament scores submits which were broken in 1.7.7 (oops). - Added @clear command to stdin command reader. ### 1.7.7 (build 20868, api 7, 2022-09-20) -- Added `ba.app.meta.load_exported_classes()` for loading classes discovered by the meta subsystem cleanly in a background thread. + +- Added `ba.app.meta.load_exported_classes()` for loading classes discovered by + the meta subsystem cleanly in a background thread. - Improved logging of missing playlist game types. - Some ba.Lstr functionality can now be used in background threads. -- Added simple check for incoming packets (should increase security level a bit). -- Simplified logic for C++ `Platform::GetDeviceName()` and made it accessible to Python via `ba.app.device_name`. -- Default device name now uses gethostname() instead of being hard coded to 'Untitled Device' (though many platforms override this). -- Added support for the console tool in the new devices section on ballistica.net. -- Increased timeouts in net-testing gui and a few other places to be able to better diagnose/handle places with very poor connectivity. -- Removed `Platform::SetLastPyCall()` which was just for debugging and which has not been useful in a while. +- Added simple check for incoming packets (should increase security level a + bit). +- Simplified logic for C++ `Platform::GetDeviceName()` and made it accessible to + Python via `ba.app.device_name`. +- Default device name now uses gethostname() instead of being hard coded to ' + Untitled Device' (though many platforms override this). +- Added support for the console tool in the new devices section on + ballistica.net. +- Increased timeouts in net-testing gui and a few other places to be able to + better diagnose/handle places with very poor connectivity. +- Removed `Platform::SetLastPyCall()` which was just for debugging and which has + not been useful in a while. - Moved some app bootstrapping from the C++ layer to the `ba._bootstrap` module. -- The game will now properly return to the stress-test window after a stress test finishes (thanks vishal332008!) -- Continue window will now pause the game to avoid running up times in the background (thanks vishal332008!) +- The game will now properly return to the stress-test window after a stress + test finishes (thanks vishal332008!) +- Continue window will now pause the game to avoid running up times in the + background (thanks vishal332008!) - Keepaway and KingOfTheHill now have epic options (thanks FAL-Guys!) -- Spaz starting with gloves no longer loses it after picking up an expiring gloves powerup (thanks itsre3!) -- Starting to rename the 'game' thread to the 'logic' thread. This is the thread where most high level app logic happen, not only game logic. +- Spaz starting with gloves no longer loses it after picking up an expiring + gloves powerup (thanks itsre3!) +- Starting to rename the 'game' thread to the 'logic' thread. This is the thread + where most high level app logic happen, not only game logic. - `_ba.in_game_thread()` is now `_ba.in_logic_thread()`. - Misc C++ layer tidying/refactoring. -- Split out the `_ba` binary module into `_ba` and `_bainternal`. This will eventually allow running without the closed-source parts (`_bainternal`) present at all. -- There is now a `_bainternal.py` dummy-module alongside the existing `_ba.py` one. Be sure to exclude it from any script collections used by the game (the same as `_ba.py`). -- Added checks to make sure `_ba` or `_bainternal` arent used outside of ba. Any 'internal' functionality needed outside of ba should be exposed through ba.internal. `_ba` and `_bainternal` are internal implementation details. -- Removed C++ Module class and simplified Thread class. The Module class was an old relic of long ago before C++ had lambdas and its existence was pretty pointless and confusing these days. +- Split out the `_ba` binary module into `_ba` and `_bainternal`. This will + eventually allow running without the closed-source parts (`_bainternal`) + present at all. +- There is now a `_bainternal.py` dummy-module alongside the existing `_ba.py` + one. Be sure to exclude it from any script collections used by the game (the + same as `_ba.py`). +- Added checks to make sure `_ba` or `_bainternal` arent used outside of ba. + Any 'internal' functionality needed outside of ba should be exposed through + ba.internal. `_ba` and `_bainternal` are internal implementation details. +- Removed C++ Module class and simplified EventLoopThread class. The Module + class was an old relic of long ago before C++ had lambdas and its existence + was pretty pointless and confusing these days. - Renamed C++ App to AppFlavor and AppGlobals to App. - Renamed C++ Media to Assets. -- Removed 'scores to beat' list in coop which was only ever functional in limited cases on the Mac version. Perhaps that feature can reappear in a cross-platform way sometime. +- Removed 'scores to beat' list in coop which was only ever functional in + limited cases on the Mac version. Perhaps that feature can reappear in a + cross-platform way sometime. - Simplified C++ bootstrapping to allocate all globals in one place. - Renamed C++ Game classes to Logic. -- The app now bootstraps Python in the main thread instead of the logic thread. This will keep things more consistent later when we are able to run under an already-existing Python interpreter. -- As a side-effect of initing Python in the main thread, it seems that Python now catches segfaults in our debug builds and prints Python stack traces. (see https://docs.python.org/3/library/faulthandler.html). We'll have to experiment and see if this is a net positive or something we want to disable or make optional. -- Python and `_ba` are now completely initialized in public source code. Now we just need to enable the app to survive without `_bainternal` and it'll be possible to build a 100% open source app. -- `Logging::Log()` in the C++ layer now takes a LogLevel arg (kDebug, kWarning, kError, etc.) and simply calls the equivalent Python logging.XXX call. This unifies our C++ and Python logging to go through the same place. -- `ba.log()` is no more. Instead just use standard Python logging functions (logging.info(), logging.error(), etc.). -- `_ba.getlog()` is now `_ba.get_v1_cloud_log()`. Note that this functionality will go away eventually so you should use `ba.app.log_handler` and/or standard Python logging functions to get at app logs. -- Along the same lines, `_ba.get_log_file_path()` is now `_ba.get_v1_cloud_log_file_path()`. -- Added `_ba.display_log()` function which ships a log message to the in-game terminal and platform-specific places like the Android log. The engine wires up standard Python logging output to go through this. -- Added `_ba.v1_cloud_log()` which ships a message to the old v1-cloud-log (the log which is gathered and sent to the v1 master server to help me identify problems people are seeing). This is presently wired up to a subset of Python logging output to approximate how it used to work. -- Note: Previously in the C++ layer some code would mix Python print calls (such as `PyErr_PrintEx()`) with ballistica::Log() calls. Previously these all wound up going to the same place (Python's sys.stderr) so it worked, but now they no longer do and so this sort of mixing should be avoided. So if you see a weird combination of colored log output lines with non-colored lines that seem to go together, please holler as it means something needs to be fixed. -- Builds for Apple devices now explicitly set a thread stack size of 1MB. The default there is 512k and I was seeing some stack overflows for heavy physics sims or very recursive Python stuff. -- If you want to grab recent logs, you can now use `ba.app.log_handler.get_cached()`. This will give you everything that has gone through Python logging, Python stdout/stderr, and the C++ Log() call (up to the max cache size that is). -- LogHandler output now ALWAYS goes to stderr. Previously it only would if an interactive terminal was detected. This should make the binary easier to debug if run from scripts/etc. We can add a `--quiet` option if needed or whatnot. -- (build 20859) Fixed an error setting up asyncio loops under Windows related to the fact that Python is now inited in the main thread. -- (build 20864) Fatal-error message/traceback now properly prints to stderr again (I think the recent logging rejiggering caused it to stop). -- (build 20864) Fixed an issue where the app could crash when connected to the cloud console while in a network game. -- Added a simplified help() command which behaves reasonably under the in-game console or cloud-console. - +- The app now bootstraps Python in the main thread instead of the logic thread. + This will keep things more consistent later when we are able to run under an + already-existing Python interpreter. +- As a side-effect of initing Python in the main thread, it seems that Python + now catches segfaults in our debug builds and prints Python stack traces. ( + see https://docs.python.org/3/library/faulthandler.html). We'll have to + experiment and see if this is a net positive or something we want to disable + or make optional. +- Python and `_ba` are now completely initialized in public source code. Now we + just need to enable the app to survive without `_bainternal` and it'll be + possible to build a 100% open source app. +- `Logging::Log()` in the C++ layer now takes a LogLevel arg (kDebug, kWarning, + kError, etc.) and simply calls the equivalent Python logging.XXX call. This + unifies our C++ and Python logging to go through the same place. +- `ba.log()` is no more. Instead just use standard Python logging functions ( + logging.info(), logging.error(), etc.). +- `_ba.getlog()` is now `_ba.get_v1_cloud_log()`. Note that this functionality + will go away eventually so you should use `ba.app.log_handler` and/or standard + Python logging functions to get at app logs. +- Along the same lines, `_ba.get_log_file_path()` is now + `_ba.get_v1_cloud_log_file_path()`. +- Added `_ba.display_log()` function which ships a log message to the in-game + terminal and platform-specific places like the Android log. The engine wires + up standard Python logging output to go through this. +- Added `_ba.v1_cloud_log()` which ships a message to the old v1-cloud-log (the + log which is gathered and sent to the v1 master server to help me identify + problems people are seeing). This is presently wired up to a subset of Python + logging output to approximate how it used to work. +- Note: Previously in the C++ layer some code would mix Python print calls (such + as `PyErr_PrintEx()`) with ballistica::Log() calls. Previously these all wound + up going to the same place (Python's sys.stderr) so it worked, but now they no + longer do and so this sort of mixing should be avoided. So if you see a weird + combination of colored log output lines with non-colored lines that seem to go + together, please holler as it means something needs to be fixed. +- Builds for Apple devices now explicitly set a thread stack size of 1MB. The + default there is 512k and I was seeing some stack overflows for heavy physics + sims or very recursive Python stuff. +- If you want to grab recent logs, you can now use + `ba.app.log_handler.get_cached()`. This will give you everything that has gone + through Python logging, Python stdout/stderr, and the C++ Log() call (up to + the max cache size that is). +- LogHandler output now ALWAYS goes to stderr. Previously it only would if an + interactive terminal was detected. This should make the binary easier to debug + if run from scripts/etc. We can add a `--quiet` option if needed or whatnot. +- (build 20859) Fixed an error setting up asyncio loops under Windows related to + the fact that Python is now inited in the main thread. +- (build 20864) Fatal-error message/traceback now properly prints to stderr + again (I think the recent logging rejiggering caused it to stop). +- (build 20864) Fixed an issue where the app could crash when connected to the + cloud console while in a network game. +- Added a simplified help() command which behaves reasonably under the in-game + console or cloud-console. ### 1.7.6 (build 20687, api 7, 2022-08-11) + - Cleaned up the MetaSubsystem code. -- It is now possible to tell the meta system about arbitrary classes (ba\_meta export foo.bar.Class) instead of just the preset types 'plugin', 'game', etc. -- Newly discovered plugins are now activated immediately instead of requiring a restart. +- It is now possible to tell the meta system about arbitrary classes (ba\_meta + export foo.bar.Class) instead of just the preset types 'plugin', 'game', etc. +- Newly discovered plugins are now activated immediately instead of requiring a + restart. ### 1.7.5 (build 20672, api 7, 2022-07-25) -- Android build now uses the ReLinker library to load the native main.so, which will (hopefully) avoid some random load failures on older Android versions. -- Android Google Play build now prints a message at launch if the billing library isn't available or needs to be updated (explaining why purchases won't work in that case). + +- Android build now uses the ReLinker library to load the native main.so, which + will (hopefully) avoid some random load failures on older Android versions. +- Android Google Play build now prints a message at launch if the billing + library isn't available or needs to be updated (explaining why purchases won't + work in that case). - Various minor bug fixes (mostly cleaning up unnecessary error logging) - Updated Android builds to use the new NDK 25 release - Added a warning when trying to play a tournament with a workspace active - Added api-version to changelog headers and `pcommand version` command. ### 1.7.4 (20646, 2022-07-12) + - Fixed the trophies list showing an incorrect total (Thanks itsre3!) - ba.app.meta.metascan is now ba.app.meta.scanresults - Cleaned up co-op ui code a bit -- Added a utility function to add custom co-op games in the practice section: `ba.app.add_coop_practice_level`. Also added new workspace template script which uses it to define a new co-op game type. -- Removed some spammy debug timing logging I added for tracking down a recent bug (can be reenabled by setting env var `BA_DEBUG_TIMING=1`) -- Updated the 'Show Mods Folder' to properly show the path to the mods folder. Before it would unhelpfully show something like `/BombSquad` but now it should be something more useful like `Android/data/net.froemling.bombsquad/files/mods`. -- Android user scripts dir is now called 'mods' instead of 'BombSquad'. The name 'BombSquad' made sense when it was located in a truly shared area of storage but now that it is in the app-specific area (something like Android/data/net.froemling.bombsquad/files) it makes sense to just use 'mods' like other platforms. -- Updated the Modding Guide button in advanced settings to point to the new ballistica wiki stuff instead of the old out-of-date 1.4 modding docs. -- Added ba.app.net.sslcontext which is a shared SSLContext we can recycle for our https requests. It turns out it can take upwards of 1 second on older Android devices to create a default SSLContext, so this can provide a nice speedup compared to the default behavior of creating a new default one for each request. -- Rewrote Google Play version purchasing code using Google's newest libraries (Google Play Billing 5.0). This should make everything more reliable, but please holler if you try to purchase anything in the game and run into problems. -- It is now possible on the Google Play version to purchase things like Pro more than once for different accounts. +- Added a utility function to add custom co-op games in the practice section: + `ba.app.add_coop_practice_level`. Also added new workspace template script + which uses it to define a new co-op game type. +- Removed some spammy debug timing logging I added for tracking down a recent + bug (can be reenabled by setting env var `BA_DEBUG_TIMING=1`) +- Updated the 'Show Mods Folder' to properly show the path to the mods folder. + Before it would unhelpfully show something like `/BombSquad` + but now it should be something more useful like + `Android/data/net.froemling.bombsquad/files/mods`. +- Android user scripts dir is now called 'mods' instead of 'BombSquad'. The name + 'BombSquad' made sense when it was located in a truly shared area of storage + but now that it is in the app-specific area (something like + Android/data/net.froemling.bombsquad/files) it makes sense to just use 'mods' + like other platforms. +- Updated the Modding Guide button in advanced settings to point to the new + ballistica wiki stuff instead of the old out-of-date 1.4 modding docs. +- Added ba.app.net.sslcontext which is a shared SSLContext we can recycle for + our https requests. It turns out it can take upwards of 1 second on older + Android devices to create a default SSLContext, so this can provide a nice + speedup compared to the default behavior of creating a new default one for + each request. +- Rewrote Google Play version purchasing code using Google's newest libraries ( + Google Play Billing 5.0). This should make everything more reliable, but + please holler if you try to purchase anything in the game and run into + problems. +- It is now possible on the Google Play version to purchase things like Pro more + than once for different accounts. ### 1.7.3 (20634, 2022-07-06) -- Fixed an issue with King of the Hill flag regions not working when players entered them (Thanks itsre3!) -- Fixed an issue in Chosen One where the flag resetting on top of a player would not cause them to become the chosen one (Thanks Dliwk!) -- Fixed an issue where triple-bomb powerup would not flash before wearing off (Thanks Juleskie!). + +- Fixed an issue with King of the Hill flag regions not working when players + entered them (Thanks itsre3!) +- Fixed an issue in Chosen One where the flag resetting on top of a player would + not cause them to become the chosen one (Thanks Dliwk!) +- Fixed an issue where triple-bomb powerup would not flash before wearing off ( + Thanks Juleskie!). - Fixed an issue where syncing workspaces containing large files could error. -- Net-testing window now requires you to be signed in instead of giving an error result in that case. -- The app now issues a gentle notice if plugins are removed instead of erroring and continuing to look for them on subsequent launches. This makes things much smoother when switching between workspaces or users. +- Net-testing window now requires you to be signed in instead of giving an error + result in that case. +- The app now issues a gentle notice if plugins are removed instead of erroring + and continuing to look for them on subsequent launches. This makes things much + smoother when switching between workspaces or users. - Added new translation entries for Workspace/Plugin stuff. -- tools/bacloud workspace get/put commands are now functional (wiki page with instructions coming soon). -- `_ba.android_get_external_storage_path` is now `_ba.android_get_external_files_dir` which maps to the actual call it makes under the hood these days. -- Android logging now breaks up long entries such as stack-traces into multiple log entries so they should not get truncated. -- The app now issues a warning if device time varies significantly from actual world time. This can lead to things like the app incorrectly treating SSL certificates as not yet valid and network functionality failing. -- The app now issues a warning if unable to establish secure connections to cloud servers (which can be due to aforementioned issue, but could also stem from other network problems). -- The Network Testing utility (Settings->Advanced->Network Testing) now tests for more potential issues including ones mentioned above. -- The Android version now stores files such as extracted assets and audio caches in the non-backed-up files dir (Android's Context.getNoBackupFilesDir()). These files can always be recreated by the app so they don't need backups, and this makes it more likely that Android will back up what's left in the regular files dir (the app config, etc). -- Fixed an issue causing hitches during background SSL network operations (manifesting on the Android version but theoretically possibly anywhere). +- tools/bacloud workspace get/put commands are now functional (wiki page with + instructions coming soon). +- `_ba.android_get_external_storage_path` is now + `_ba.android_get_external_files_dir` which maps to the actual call it makes + under the hood these days. +- Android logging now breaks up long entries such as stack-traces into multiple + log entries so they should not get truncated. +- The app now issues a warning if device time varies significantly from actual + world time. This can lead to things like the app incorrectly treating SSL + certificates as not yet valid and network functionality failing. +- The app now issues a warning if unable to establish secure connections to + cloud servers (which can be due to aforementioned issue, but could also stem + from other network problems). +- The Network Testing utility (Settings->Advanced->Network Testing) now tests + for more potential issues including ones mentioned above. +- The Android version now stores files such as extracted assets and audio caches + in the non-backed-up files dir (Android's Context.getNoBackupFilesDir()). + These files can always be recreated by the app so they don't need backups, and + this makes it more likely that Android will back up what's left in the regular + files dir (the app config, etc). +- Fixed an issue causing hitches during background SSL network operations ( + manifesting on the Android version but theoretically possibly anywhere). ### 1.7.2 (20620, 2022-06-25) + - Minor fixes in some minigames (Thanks Droopy!) -- Fixed a bug preventing 'clients' arg from working in `_ba.chatmessage` (Thanks imayushsaini!) -- Fixed a bug where ba.Player.getdelegate(doraise=True) could return None instead of raising a ba.DelegateNotFoundError (thanks Dliwk!) +- Fixed a bug preventing 'clients' arg from working in `_ba.chatmessage` (Thanks + imayushsaini!) +- Fixed a bug where ba.Player.getdelegate(doraise=True) could return None + instead of raising a ba.DelegateNotFoundError (thanks Dliwk!) - Lots of Romanian language improvements (Thanks Meryu!) -- Workspaces are now functional. They require signing in with a V2 account, which currently is limited to explicitly created email/password logins. See ballistica.net to create such an account or create/edit a workspace. This is bleeding edge stuff so please holler with any bugs you come across or if anything seems unintuitive. -- Newly detected Plugins are now enabled by default in all cases; not just headless builds. (Though a restart is still required before they run). Some builds (headless, iiRcade) can't easily access gui settings so this makes Plugins more usable there and keeps things consistent. The user still has the opportunity to deactivate newly detected plugins before restarting if they don't want to use them. -- Reworked app states for the new workspace system, with a new `loading` stage that comes after `launching` and before `running`. The `loading` stage consists of an initial account log-in (or lack thereof) and any workspace/asset downloading related to that. This allows the app to ensure that the latest workspace state is synced for the active account before running plugin loads and meta scans, allowing those bits to work as seamlessly in workspaces as they do for traditional local manual installs. -- Plugins now have an `on_app_running` call instead of `on_app_launch`, allowing them to work seamlessly with workspaces (see previous entry). -- Errors running/loading plugins now show up as screen-messages. This can be ugly but hopefully provides a bit of debugging capability for anyone testing code on a phone or somewhere with no access to full log output. Once we can add logging features to the workspaces web ui we can perhaps scale back on this. -- Api version increased from 6 to 7 due to the aforementioned plugin changes (`on_app_launch` becoming `on_app_running`, etc.) +- Workspaces are now functional. They require signing in with a V2 account, + which currently is limited to explicitly created email/password logins. See + ballistica.net to create such an account or create/edit a workspace. This is + bleeding edge stuff so please holler with any bugs you come across or if + anything seems unintuitive. +- Newly detected Plugins are now enabled by default in all cases; not just + headless builds. (Though a restart is still required before they run). Some + builds (headless, iiRcade) can't easily access gui settings so this makes + Plugins more usable there and keeps things consistent. The user still has the + opportunity to deactivate newly detected plugins before restarting if they + don't want to use them. +- Reworked app states for the new workspace system, with a new `loading` stage + that comes after `launching` and before `running`. The `loading` stage + consists of an initial account log-in (or lack thereof) and any + workspace/asset downloading related to that. This allows the app to ensure + that the latest workspace state is synced for the active account before + running plugin loads and meta scans, allowing those bits to work as seamlessly + in workspaces as they do for traditional local manual installs. +- Plugins now have an `on_app_running` call instead of `on_app_launch`, allowing + them to work seamlessly with workspaces (see previous entry). +- Errors running/loading plugins now show up as screen-messages. This can be + ugly but hopefully provides a bit of debugging capability for anyone testing + code on a phone or somewhere with no access to full log output. Once we can + add logging features to the workspaces web ui we can perhaps scale back on + this. +- Api version increased from 6 to 7 due to the aforementioned plugin changes + (`on_app_launch` becoming `on_app_running`, etc.) ### 1.7.1 (20597, 2022-06-04) + - V2 account logic fixes - Polishing V2 web-based login flow ### 1.7.0 (20591, 2022-06-02) -- V2 accounts are now available (woohoo!). These are called 'BombSquad Accounts' in the account section. V2 accounts communicate with a completely new server and will be the foundation for lots of new functionality in the future. However they also function as a V1 account so existing functionality should still work. Note that the new 'workspaces' feature for V2-accounts is not yet enabled in this build, but it will be in the next few builds. Also note that account types such as GameCenter and Google-Play will be 'upgraded' to V2 accounts in the future so there is no need to try this out if you use one of those. But if you use device-accounts you might want to create yourself a V2 account, since device-accounts will remain V1-only (though you can link an old device-account to a v2-enabled account if you want to keep your progress). Getting a V2 account now also gives you a chance to reserve a nice account-tag before all the good ones are taken. -- Legacy account subsystem has been renamed from `ba.app.accounts` to `ba.app.accounts_v1` + +- V2 accounts are now available (woohoo!). These are called 'BombSquad Accounts' + in the account section. V2 accounts communicate with a completely new server + and will be the foundation for lots of new functionality in the future. + However they also function as a V1 account so existing functionality should + still work. Note that the new 'workspaces' feature for V2-accounts is not yet + enabled in this build, but it will be in the next few builds. Also note that + account types such as GameCenter and Google-Play will be 'upgraded' to V2 + accounts in the future so there is no need to try this out if you use one of + those. But if you use device-accounts you might want to create yourself a V2 + account, since device-accounts will remain V1-only (though you can link an old + device-account to a v2-enabled account if you want to keep your progress). + Getting a V2 account now also gives you a chance to reserve a nice account-tag + before all the good ones are taken. +- Legacy account subsystem has been renamed from `ba.app.accounts` to + `ba.app.accounts_v1` - Added `ba.app.accounts_v2` subsystem for working with V2 accounts. -- `ba.SessionPlayer.get_account_id()` is now `ba.SessionPlayer.get_v1_account_id()` +- `ba.SessionPlayer.get_account_id()` is now + `ba.SessionPlayer.get_v1_account_id()` - `ba.InputDevice.get_account_id()` is now `ba.InputDevice.get_v1_account_id()` - `_ba.sign_in()` is now `_ba.sign_in_v1()` - `_ba.sign_out()` is now `_ba.sign_out_v1()` @@ -216,101 +761,178 @@ - `_ba.get_account_type()` is now `_ba.get_v1_account_type()` - `_ba.get_account_state()` is now `_ba.get_v1_account_state()` - `_ba.get_account_state_num()` is now `_ba.get_v1_account_state_num()` -- `_ba.get_account_display_string()` is now `_ba.get_v1_account_display_string()` +- `_ba.get_account_display_string()` is now + `_ba.get_v1_account_display_string()` - `_ba.get_account_misc_val()` is now `_ba.get_v1_account_misc_val()` - `_ba.get_account_misc_read_val()` is now `_ba.get_v1_account_misc_read_val()` -- `_ba.get_account_misc_read_val_2()` is now `_ba.get_v1_account_misc_read_val_2()` +- `_ba.get_account_misc_read_val_2()` is now + `_ba.get_v1_account_misc_read_val_2()` - `_ba.get_account_ticket_count()` is now `_ba.get_v1_account_ticket_count()` -- Exposing more sources in the public repo; namely networking stuff. I realize this probably opens up some attack vectors for hackers but also opens up options for server-owners to add their own defenses without having to wait on me. Hopefully this won't prove to be a bad idea. -- V2 master server addr is now simply https://ballistica.net. If you had saved links to the previous address, https://tools.ballistica.net, please update them, as the old address may stop working at some point. -- Upgraded everything to Python 3.10. The upgrade process is pretty smooth at this point so we should be able to upgrade yearly now once each new Python version has had some time to mature. - +- Exposing more sources in the public repo; namely networking stuff. I realize + this probably opens up some attack vectors for hackers but also opens up + options for server-owners to add their own defenses without having to wait on + me. Hopefully this won't prove to be a bad idea. +- V2 master server addr is now simply https://ballistica.net. If you had saved + links to the previous address, https://tools.ballistica.net, please update + them, as the old address may stop working at some point. +- Upgraded everything to Python 3.10. The upgrade process is pretty smooth at + this point so we should be able to upgrade yearly now once each new Python + version has had some time to mature. ### 1.6.12 (20567, 2022-05-04) + - More internal work on V2 master-server communication ### 1.6.11 (20539, 2022-03-23) -- Documentation is now generated using pdoc . Thanks Dliwk!! (I'll get it wired up to auto-update to a webpage soon). -- Players who connect to authenticated servers impersonating someone else are now simply kicked; not banned. The old behavior was being intentionally exploited to ban people from their own servers/etc. I may revert to bans once I can do it in a way that is not exploitable. -- The game now establishes a V2 master-server connection (which will soon be used for lots of cool functionality). For this version it is mainly enabled for testing purposes; please holler if you see any odd warning messages or behavior. + +- Documentation is now generated using pdoc . Thanks Dliwk!! + ( I'll get it wired up to auto-update to a webpage soon). +- Players who connect to authenticated servers impersonating someone else are + now simply kicked; not banned. The old behavior was being intentionally + exploited to ban people from their own servers/etc. I may revert to bans once + I can do it in a way that is not exploitable. +- The game now establishes a V2 master-server connection (which will soon be + used for lots of cool functionality). For this version it is mainly enabled + for testing purposes; please holler if you see any odd warning messages or + behavior. ### 1.6.10 (20511, 2022-03-20) -- Added `_ba.get_client_public_device_uuid` function which returns a semi-permanent device id for a connected client running 1.6.10 or newer. Can be useful to combat spam attacks or other mischief. -- Fixed an issue with `make update` not properly rewriting Visual Studio project files to account for new/deleted source files. -- Removed various bits of code associated with the (no-longer-functional) Google Play Games multiplayer connections. -- Added lots of foundation code for v2 master-server connections (not yet enabled). + +- Added `_ba.get_client_public_device_uuid` function which returns a + semi-permanent device id for a connected client running 1.6.10 or newer. Can + be useful to combat spam attacks or other mischief. +- Fixed an issue with `make update` not properly rewriting Visual Studio project + files to account for new/deleted source files. +- Removed various bits of code associated with the (no-longer-functional) Google + Play Games multiplayer connections. +- Added lots of foundation code for v2 master-server connections (not yet + enabled). ### 1.6.9 (20486, 2022-02-22) + - Upgraded Android Python to 3.9.10 -- Fixed an issue with SSL in Android builds that was preventing communication with the master-server in 1.6.8 -- Added a new network-diagnostics tool at 'Settings->Advanced->Network Testing'. Can be used to diagnose issues talking to master-servers/etc. (especially useful now that SSL can factor in) -- Added clipboard support to Mac test build (thought pasting currently requires ctrl-v instead of cmd-v). -- Fixed an issue where non-ascii characters in device names could break network communication. +- Fixed an issue with SSL in Android builds that was preventing communication + with the master-server in 1.6.8 +- Added a new network-diagnostics tool at 'Settings->Advanced->Network Testing'. + Can be used to diagnose issues talking to master-servers/etc. (especially + useful now that SSL can factor in) +- Added clipboard support to Mac test build (thought pasting currently requires + ctrl-v instead of cmd-v). +- Fixed an issue where non-ascii characters in device names could break network + communication. ### 1.6.8 (20458, 2022-02-16) + - Added Filipino language (Thanks David!) - Restored pre-v1.5 jump behaviour. -- All communication with the master-server should now be secure (https) using root certificates from the [certifi](https://github.com/certifi/python-certifi) project. Please holler if you run into any connection issues with this version. +- All communication with the master-server should now be secure (https) using + root certificates from the + [certifi](https://github.com/certifi/python-certifi) project. Please holler if + you run into any connection issues with this version. ### 1.6.7 (20436) + - Fixed a vulnerability which could expose device-account uuids. -- Now generating Linux Arm64 server and test builds (currently built against Ubuntu 20). -- Mac test builds are now Universal binaries (Arm64 & x86-64 versions bundled together). -- Mac test builds are now notarized and distributed via a snazzy .dmg instead of a zip file, so the OS should no longer try to prevent you from running them. -- Test builds can now be found at - this page shows more info about the builds, including file checksums (stored on a separate server from the actual files for increased security). +- Now generating Linux Arm64 server and test builds (currently built against + Ubuntu 20). +- Mac test builds are now Universal binaries (Arm64 & x86-64 versions bundled + together). +- Mac test builds are now notarized and distributed via a snazzy .dmg instead of + a zip file, so the OS should no longer try to prevent you from running them. +- Test builds can now be found at - this page + shows more info about the builds, including file checksums (stored on a + separate server from the actual files for increased security). ### 1.6.6 (20394) + - Beginning work on moving to new asset system. - Added Tamil language (Thanks Ryan!) - Added methods for changing camera attributes to the `_ba` module. ### 1.6.5 (20394) + - Added co-op support to server builds (thanks Dliwk!) -- Updated everything from Python 3.8 to Python 3.9. The biggest immediate impact to our code is that basic types such as list, dict, and tuple can be used in annotations, eliminating the need to import typing.Dict, typing.List, etc. See python.org for more changes. -- Note: accessing mods on external storage on Android will not work in this release. This functionality has not been working in recent versions of Android due to increased security features anyway and I am in the process of replacing it with a cloud based system for installing mods. More on this soon. -- Python 3.9 no longer supports Windows 7 or earlier (according to ) so if you are running such a version of Windows you will need to stick to older builds. +- Updated everything from Python 3.8 to Python 3.9. The biggest immediate impact + to our code is that basic types such as list, dict, and tuple can be used in + annotations, eliminating the need to import typing.Dict, typing.List, etc. See + python.org for more changes. +- Note: accessing mods on external storage on Android will not work in this + release. This functionality has not been working in recent versions of Android + due to increased security features anyway and I am in the process of replacing + it with a cloud based system for installing mods. More on this soon. +- Python 3.9 no longer supports Windows 7 or earlier (according to + ) so if you are running such a + version of Windows you will need to stick to older builds. ### 1.6.4 (20382) + - Some cleanups in the Favorites tab of the gather window. -- Reorganized prefab target names; some targets such as `prefab-debug` are now `prefab-gui-debug` (more consistent with the existing `prefab-server-debug` targets). -- Windows builds now go to build/windows instead of `ballisticacore_windows/build`. -- Lots of project reorganization to allow things such as documentation or the dummy `_ba.py` module to be rebuilt from the public repo. +- Reorganized prefab target names; some targets such as `prefab-debug` are now + `prefab-gui-debug` (more consistent with the existing `prefab-server-debug` + targets). +- Windows builds now go to build/windows instead of + `ballisticakit_windows/build`. +- Lots of project reorganization to allow things such as documentation or the + dummy `_ba.py` module to be rebuilt from the public repo. - Added network flood attack mitigation. ### 1.6.3 (20366) -- Telnet access works again for gui builds without requiring a password (access must still be granted via the gui). + +- Telnet access works again for gui builds without requiring a password (access + must still be granted via the gui). ### 1.6.2 (20365) -- Declare opponent team as the winner if a player with their final turn leaves an elimination game. -- Fix for certain cases when trying to host a private game where no available nearby servers could be found. + +- Declare opponent team as the winner if a player with their final turn leaves + an elimination game. +- Fix for certain cases when trying to host a private game where no available + nearby servers could be found. - Enabling per-architecture apk splitting for smaller download sizes on Android. ### 1.6.1 (20362) -- Some clean-up on Android builds, including simplifying ad-networks. No longer should ever show rewarded ads in between game rounds (only when actual rewards are involved). + +- Some clean-up on Android builds, including simplifying ad-networks. No longer + should ever show rewarded ads in between game rounds (only when actual rewards + are involved). ### 1.6.0 (20357) -- Revamped netcode significantly. We still don't have client-prediction, but things should (hopefully) feel much lower latency now. + +- Revamped netcode significantly. We still don't have client-prediction, but + things should (hopefully) feel much lower latency now. - Added network debug graphs accessible by hitting F8. -- Added private parties functionality (cloud hosted parties with associated codes making it easier to play with friends) +- 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. +- 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. -- Server-wrapper improvements allowing config path and `ba_root` path to be passed explicitly. +- Fixed an issue where click locations on scaled text fields could be + incorrectly calculated. +- Server-wrapper improvements allowing config path and `ba_root` path to be + passed explicitly. - Binary -cfgdir option now properly allows any path, not just `./ba_root`. -- Additional server-wrapper options such as disabling auto-restart and automatic restarts on config file changes. -- Running a `_ba.connect_to_party` command via the -exec arg should now do the right thing. +- Additional server-wrapper options such as disabling auto-restart and automatic + restarts on config file changes. +- Running a `_ba.connect_to_party` command via the -exec arg should now do the + right thing. - Fixed possible crash due to buffer under/overruns in `Utils::precalc_rands_*`. -- Fixed a potential crash-on-exit due to statically allocated colliders/caches in `ode_collision_trimesh.cpp` getting torn down while in use +- Fixed a potential crash-on-exit due to statically allocated colliders/caches + in `ode_collision_trimesh.cpp` getting torn down while in use - Better randomization for player free-for-all starting locations -- Plugins can now register to be called for pause, resume, and shutdown events in addition to launch -- Added ba.app.state holding the overall state of the app (running, paused, etc.) -- renamed the efro.dataclasses module to efro.dataclassio and added significant functionality +- Plugins can now register to be called for pause, resume, and shutdown events + in addition to launch +- Added ba.app.state holding the overall state of the app (running, paused, + etc.) +- renamed the efro.dataclasses module to efro.dataclassio and added significant + functionality - command-line input no longer errors on commands longer than 4k bytes. - added show-tutorial option to the server wrapper config - added custom-team-names option to the server wrapper config @@ -318,402 +940,701 @@ - added inline-playlist option to the server wrapper config ### 1.5.29 (20246) + - Exposed ba method/class initing in public C++ layer. -- The 'restart' and 'shutdown' commands in the server script now default to immediate=True -- Wired up `clean_exit_minutes`, `unclean_exit_minutes`, and `idle_exit_minutes` options in the server config -- Removed remains of the google-real-time-multiplayer stuff from the android/java layer. +- The 'restart' and 'shutdown' commands in the server script now default to + immediate=True +- Wired up `clean_exit_minutes`, `unclean_exit_minutes`, and `idle_exit_minutes` + options in the server config +- Removed remains of the google-real-time-multiplayer stuff from the + android/java layer. ### 1.5.28 (20239) + - Simplified `ba.enum_by_value()` -- Updated Google Play version to hopefully show friend high scores again on score screens (at least for levels that have an associated Google Play leaderboard). -- Public-party-list now properly shows an error instead of 'loading...' when not signed in. -- Heavily reworked public party list display code to be more efficient and avoid hitches even with large numbers of servers. +- Updated Google Play version to hopefully show friend high scores again on + score screens (at least for levels that have an associated Google Play + leaderboard). +- Public-party-list now properly shows an error instead of 'loading...' when not + signed in. +- Heavily reworked public party list display code to be more efficient and avoid + hitches even with large numbers of servers. ### 1.5.27 (20238) -- Language functionality has been consolidated into a LanguageSubsystem object at ba.app.lang + +- Language functionality has been consolidated into a LanguageSubsystem object + at ba.app.lang - `ba.get_valid_languages()` is now an attr: `ba.app.lang.available_languages` -- Achievement functionality has been consolidated into an AchievementSubsystem object at ba.app.ach -- Plugin functionality has been consolidated into a PluginSubsystem obj at ba.app.plugins +- Achievement functionality has been consolidated into an AchievementSubsystem + object at ba.app.ach +- Plugin functionality has been consolidated into a PluginSubsystem obj at + ba.app.plugins - Ditto with AccountV1Subsystem and ba.app.accounts - Ditto with MetadataSubsystem and ba.app.meta - Ditto with AdsSubsystem and ba.app.ads -- Revamped tab-button functionality into a cleaner type-safe class (bastd.ui.tabs.TabRow) -- Split Gather-Window tabs out into individual classes for future improvements (bastd.ui.gather.\*) -- Added the ability to disable ticket-purchasing UIs for builds (`ba.app.allow_ticket_purchases`) -- Reworked the public party gather section to perform better; it should no longer have to rebuild the list from scratch each time the UI is visited. +- Revamped tab-button functionality into a cleaner type-safe class ( + bastd.ui.tabs.TabRow) +- Split Gather-Window tabs out into individual classes for future improvements ( + bastd.ui.gather.\*) +- Added the ability to disable ticket-purchasing UIs for builds + (`ba.app.allow_ticket_purchases`) +- Reworked the public party gather section to perform better; it should no + longer have to rebuild the list from scratch each time the UI is visited. - Added a filter option to the public party list (sorry it has taken so long). ### 1.5.26 (20217) + - Simplified licensing header on python scripts. - General project refactoring in order to open source most of the C++ layer. ### 1.5.25 (20176) + - Added Venetian language (thanks Federico!) -- Fixed an issue where chosen-one flashes would remain if the player leaves the game +- Fixed an issue where chosen-one flashes would remain if the player leaves the + game - Added android input-device detection log messages for debugging -- Android asset-sync phase (completing install...) now emits log output for debugging. +- Android asset-sync phase (completing install...) now emits log output for + debugging. ### 1.5.24 (20163) -- Upgraded Python from version 3.7 to 3.8. This is a substantial change (though nothing like the previous update from 2.7 to 3.7) so please holler if anything is broken. These updates will happen once every year or two now... -- Windows debug builds now use Python debug libraries. This should hopefully catch more errors that would otherwise go undetected and potentially cause crashes. -- Switched windows builds to use 'fast' mode math instead of 'strict'. This should make the game run more efficiently (similar modes are already in use on other platforms) but holler if any odd breakage happens such as things falling through floors (more often than the occasional random fluke-y case that happens now). -- Added `_ba.can_display_full_unicode()` for any code that wants to avoid printing things that won't show up locally. -- Now pulling some classes such as Literal and Protocol from typing instead of `typing_extensions` (they were officially added to Python in 3.8) -- Double taps/clicks now work properly on widgets nested under a scroll-widget on mobile (so, for example, replays can now be double-clicked to view them) + +- Upgraded Python from version 3.7 to 3.8. This is a substantial change (though + nothing like the previous update from 2.7 to 3.7) so please holler if anything + is broken. These updates will happen once every year or two now... +- Windows debug builds now use Python debug libraries. This should hopefully + catch more errors that would otherwise go undetected and potentially cause + crashes. +- Switched windows builds to use 'fast' mode math instead of 'strict'. This + should make the game run more efficiently (similar modes are already in use on + other platforms) but holler if any odd breakage happens such as things falling + through floors (more often than the occasional random fluke-y case that + happens now). +- Added `_ba.can_display_full_unicode()` for any code that wants to avoid + printing things that won't show up locally. +- Now pulling some classes such as Literal and Protocol from typing instead of + `typing_extensions` (they were officially added to Python in 3.8) +- Double taps/clicks now work properly on widgets nested under a scroll-widget + on mobile (so, for example, replays can now be double-clicked to view them) ### 1.5.23 (20146) -- Fixed the shebang line in `bombsquad_server` file by using `-S` flag for `/usr/bin/env`. -- Fixed a bug with hardware keyboards emitting extra characters in the in-game console (~ or F2) -- Added support for 'plugin' mods and user controls to configure them in settings-\>advanced-\>plugins. -- Renamed `selection_loop_to_parent` to `selection_loops_to_parent` in widget calls. -- Added `selection_loops_to_parent`, `border`, `margin`, `claims_left_right`, and `claims_tab` args to ba.columnwidget(). -- Column-widget now has a default `border` of 0 (explicitly pass 2 to get the old look). -- Column-widget now has a default `margin` of 10 (explicitly pass 0 to get the old look). -- Added `selection_loops_to_parent`, `claims_left_right`, and `claims_tab` args to ba.scrollwidget. -- Added `selection_loops_to_parent`, `claims_left_right`, and `claims_tab` args to ba.rowwidget. + +- Fixed the shebang line in `bombsquad_server` file by using `-S` flag for + `/usr/bin/env`. +- Fixed a bug with hardware keyboards emitting extra characters in the in-game + console (~ or F2) +- Added support for 'plugin' mods and user controls to configure them in + settings-\>advanced-\>plugins. +- Renamed `selection_loop_to_parent` to `selection_loops_to_parent` in widget + calls. +- Added `selection_loops_to_parent`, `border`, `margin`, `claims_left_right`, + and `claims_tab` args to ba.columnwidget(). +- Column-widget now has a default `border` of 0 (explicitly pass 2 to get the + old look). +- Column-widget now has a default `margin` of 10 (explicitly pass 0 to get the + old look). +- Added `selection_loops_to_parent`, `claims_left_right`, and `claims_tab` args + to ba.scrollwidget. +- Added `selection_loops_to_parent`, `claims_left_right`, and `claims_tab` args + to ba.rowwidget. - Added `claims_left_right` and `claims_tab` to ba.hscrollwidget(). -- Default widget `show_buffer` is now 20 instead of 0 (causes scrolling to stay slightly ahead of widget selection). This can be overridden with the ba.widget() call if anything breaks. +- Default widget `show_buffer` is now 20 instead of 0 (causes scrolling to stay + slightly ahead of widget selection). This can be overridden with the + ba.widget() call if anything breaks. - Relocated ba.app.uiscale to ba.app.ui.uiscale. - Top level settings window now properly saves/restores its state again. - Added Emojis to the Internal Game Keyboard. - Added continuous CAPITAL letters typing feature in the Internal Game Keyboard. ### 1.5.22 (20139) -- Button and key names now display correctly again on Android (and are cleaned up on other platforms too). + +- Button and key names now display correctly again on Android (and are cleaned + up on other platforms too). ### 1.5.21 (20138) -- Added a UI subsystem at ba.app.ui (containing globals/functionality that was previously directly under ba.app). And hopefully added a fix for rare state of two main menus appearing on-screen at once. -- Added options in the 'Advanced' section to disable camera shake and camera gyroscope motion. + +- Added a UI subsystem at ba.app.ui (containing globals/functionality that was + previously directly under ba.app). And hopefully added a fix for rare state of + two main menus appearing on-screen at once. +- Added options in the 'Advanced' section to disable camera shake and camera + gyroscope motion. ### 1.5.20 (20126) -- The ba.Session.teams and ba.Session.players lists are now ba.Session.sessionteams and ba.Session.sessionplayers. This is to help keep it clear that a Team/Player and a SessionTeam/SessionPlayer are different things now. -- Disconnecting an input-device now immediately removes the player instead of doing so in the next cycle; this prevents possible issues where code would try to access player.inputdevice before the removal happens which would lead to errors. -- Updated mac prefab builds to point at homebrew's python@3.7 package now that 3.8 has been made the default. -- Fixed an issue where adding/deleting UI widgets within certain callbacks could cause a crash. -- Fixed a case where an early fatal error could lead to a hung app and no error dialog. -- Added environment variables which can override UI scale for testing. Set `BA_FORCE_UI_SCALE` to small, medium or large. -- Added a ba.UIScale enum. The value at ba.app.uiscale replaces the old `ba.app.interface_type`, `ba.app.small_ui`, and `ba.app.med_ui` values. -- Emoji no longer display in-game with a washed-out appearance. If there are any places in-game where bright-colored emoji become distracting, please holler. -- `_ba.get_game_roster()` now includes `account_id` which is the validated account id of all clients (will be None until completes). Also, a few keys are renamed: `specString->spec_string` and `displayString->display_string`. + +- The ba.Session.teams and ba.Session.players lists are now + ba.Session.sessionteams and ba.Session.sessionplayers. This is to help keep it + clear that a Team/Player and a SessionTeam/SessionPlayer are different things + now. +- Disconnecting an input-device now immediately removes the player instead of + doing so in the next cycle; this prevents possible issues where code would try + to access player.inputdevice before the removal happens which would lead to + errors. +- Updated mac prefab builds to point at homebrew's python@3.7 package now that + 3.8 has been made the default. +- Fixed an issue where adding/deleting UI widgets within certain callbacks could + cause a crash. +- Fixed a case where an early fatal error could lead to a hung app and no error + dialog. +- Added environment variables which can override UI scale for testing. Set + `BA_FORCE_UI_SCALE` to small, medium or large. +- Added a ba.UIScale enum. The value at ba.app.uiscale replaces the old + `ba.app.interface_type`, `ba.app.small_ui`, and `ba.app.med_ui` values. +- Emoji no longer display in-game with a washed-out appearance. If there are any + places in-game where bright-colored emoji become distracting, please holler. +- `_ba.get_game_roster()` now includes `account_id` which is the validated + account id of all clients (will be None until completes). Also, a few keys are + renamed: `specString->spec_string` and `displayString->display_string`. ### 1.5.19 (20123) -- Cleaned up some bomb logic to avoid weird corner-cases such as land-mine explosions behaving like punches when set off by punches or bombs potentially resulting in multiple explosions when triggered by multiple other bombs simultaneously. Holler if anything explosion-related seems off now. -- Reactivated and cleaned up fatal-error message dialogs; they should now show up more consistently and on more platforms when something catastrophic happens instead of getting a silent crash. -- Certain hardware buttons on Android which stopped working in 1.5 should now be working again... + +- Cleaned up some bomb logic to avoid weird corner-cases such as land-mine + explosions behaving like punches when set off by punches or bombs potentially + resulting in multiple explosions when triggered by multiple other bombs + simultaneously. Holler if anything explosion-related seems off now. +- Reactivated and cleaned up fatal-error message dialogs; they should now show + up more consistently and on more platforms when something catastrophic happens + instead of getting a silent crash. +- Certain hardware buttons on Android which stopped working in 1.5 should now be + working again... ### 1.5.18 (20108) + - A bit of project cleanup; tools/snippets is now tools/pcommand, etc. - More minor bug fixes and crash/bug-logging improvements. ### 1.5.17 (20102) + - More cleanup to logging and crash reporting system. - Various other minor bug fixes... ### 1.5.16 (20099) + - Hopefully finally fixed that pesky crash bug on score submissions. ### 1.5.14 (20096) + - Fixed Android VR version failing to launch. - More bug fixing and crash reporting improvements. ### 1.5.13 (20095) + - Hopefully fixed an elusive random crash on android that popped up recently. - Misc bug fixes. ### 1.5.12 (20087) + - Improved exception handling and crash reporting. - Misc bug fixes. ### 1.5.11 (20083) + - Fixed a freeze in the local network browser. ### 1.5.10 (20083) + - Streamlined C++ layer bootstrapping process a bit. - Creating sys scripts via ba.modutils now works properly. - Custom soundtracks should now work again under Android 10. - Misc other bug fixes. ### 1.5.9 (20082) + - Reduced some hitches when clicking on certain buttons in the UI -- Fixed an issue where very early keyboard/controller connects/disconnects could get lost on android. -- `ba._modutils` is now ba.modutils since it is intended to be publicly accessible. -- drop-down console is now properly accessible again via android hardware keyboards (\` key) +- Fixed an issue where very early keyboard/controller connects/disconnects could + get lost on android. +- `ba._modutils` is now ba.modutils since it is intended to be publicly + accessible. +- drop-down console is now properly accessible again via android hardware + keyboards (\` key) - Other minor bug fixes.. ### 1.5.8 (20079) -- Fixed an issue where touch controls or sound settings values could look like 0.8999999999. Please holler if you see this anywhere else. -- Fixed a potential crash when tapping the screen before the game is fully inited. -- Restored the correct error message in the 'Google Play' connection tab from 1.4 (I am actively working on a replacement) + +- Fixed an issue where touch controls or sound settings values could look like + 0.8999999999. Please holler if you see this anywhere else. +- Fixed a potential crash when tapping the screen before the game is fully + inited. +- Restored the correct error message in the 'Google Play' connection tab from + 1.4 (I am actively working on a replacement) - Other minor bug fixes. ### 1.5.7 (20077) -- Fixed an issue where co-op score screen rating could look like '3.9999999999999' + +- Fixed an issue where co-op score screen rating could look like ' + 3.9999999999999' - Other minor bug fixes. ### 1.5.6 (20075) -- Lots of internal event-handling cleanup/reorganization in preparation for Android 1.5 update. -- Lots of low level input handling cleanup, also related to Android 1.5 version. Please holler if keyboard/game-controllers/etc. are behaving odd on any platforms. -- Now including Android test builds for the first time since 1.5. These have not been thoroughly tested yet so please holler with anything that is obviously broken. + +- Lots of internal event-handling cleanup/reorganization in preparation for + Android 1.5 update. +- Lots of low level input handling cleanup, also related to Android 1.5 version. + Please holler if keyboard/game-controllers/etc. are behaving odd on any + platforms. +- Now including Android test builds for the first time since 1.5. These have not + been thoroughly tested yet so please holler with anything that is obviously + broken. - Mouse wheel now works in manual camera mode on more platforms. -- Server scripts now run in opt mode in release builds, so they can use bundled .opt-1.pyc files. +- Server scripts now run in opt mode in release builds, so they can use bundled + .opt-1.pyc files. - Fixes a potential crash in the local network browser. - Fixes an issue where Hockey Pucks would not show up in network games. - More misc bug fixes and tidying. ### 1.5.5 (20069) + - Cleaned up Windows version packaging. - More misc bug fixes. ### 1.5.4 (20067) + - Should now work properly with non-ascii paths on Windows (for real this time). -- Note that Windows game data is now stored under 'Local' appdata instead of 'Roaming'; if you have an old installation with data you want to preserve, you may want to move it over manually. +- Note that Windows game data is now stored under 'Local' appdata instead of ' + Roaming'; if you have an old installation with data you want to preserve, you + may want to move it over manually. - Misc cleanup and minor bug fixes. ### 1.5.3 (20065) + - Improved handling of non-ascii characters in file paths on Windows. ### 1.5.2 (20063) -- Fixes an issue with controls not working correctly in net-play between 1.4.x and 1.5.x. + +- Fixes an issue with controls not working correctly in net-play between 1.4.x + and 1.5.x. - Tidied up onslaught code a bit. - Fixes various other minor bugs. ### 1.5.1 (20062) -- Windows server now properly displays color when run by double-clicking the .bat file. + +- Windows server now properly displays color when run by double-clicking the + .bat file. - Misc bug fixes. ### 1.5.0 (20001) -- This build contains about 2 years worth of MAJOR internal refactoring to prepare for the future of BombSquad. As a player this should not (yet) look different from 1.4, but for modders there is a lot new. See the rest of these change entries or visit [ballistica.net](https://ballistica.net) for more info. -- Ported the entire scripting layer from Python 2 to Python 3 (currently at 3.7, and I intend to keep this updated to the latest widely-available release). There's some significant changes going from python 2 to 3 (new print statement, string behavior, etc.), but these are well documented online, so please read up as needed. This should provide us some nice benefits and future-proofs everything. (my janky 2.7 custom Python builds were getting a little long in the tooth). -- Refactored all script code to be PEP8 compliant (Python coding standards). Basically, this means that stuff that was camel-case (fooBar) is now a single word or underscores (`foobar` / `foo_bar`). There are a few minor exceptions such as existing resource and media filenames, but in general old code can be ported by taking a pass through and killing the camel-case. I know this is a bit of a pain in the ass, but it'll let us use things like Pylint and just be more consistent with the rest of the Python world. -- On a related note, I'm now using 'yapf' to keep my Python code formatted nicely (using pep8 style); I'd recommend checking it out if you're doing a lot of scripting as it's a great time-saver. -- On another related note, I'm trying to confirm to Google's recommendations for Python code (search 'Google Python Style Guide'). There are some good bits of wisdom in there, so I recommend at least skimming through it. -- And as one last related note, I'm now running Pylint on all my own Python code. Highly recommended if you are doing serious scripting, as it can make Python almost feel as type-safe as C++. -- The minimum required android version will now be 5.0 (a requirement of the Python 3 builds I'm using) + +- This build contains about 2 years worth of MAJOR internal refactoring to + prepare for the future of BombSquad. As a player this should not (yet) look + different from 1.4, but for modders there is a lot new. See the rest of these + change entries or visit [ballistica.net](https://ballistica.net) for more + info. +- Ported the entire scripting layer from Python 2 to Python 3 (currently at 3.7, + and I intend to keep this updated to the latest widely-available release). + There's some significant changes going from python 2 to 3 (new print + statement, string behavior, etc.), but these are well documented online, so + please read up as needed. This should provide us some nice benefits and + future-proofs everything. (my janky 2.7 custom Python builds were getting a + little long in the tooth). +- Refactored all script code to be PEP8 compliant (Python coding standards). + Basically, this means that stuff that was camel-case (fooBar) is now a single + word or underscores (`foobar` / `foo_bar`). There are a few minor exceptions + such as existing resource and media filenames, but in general old code can be + ported by taking a pass through and killing the camel-case. I know this is a + bit of a pain in the ass, but it'll let us use things like Pylint and just be + more consistent with the rest of the Python world. +- On a related note, I'm now using 'yapf' to keep my Python code formatted + nicely (using pep8 style); I'd recommend checking it out if you're doing a lot + of scripting as it's a great time-saver. +- On another related note, I'm trying to confirm to Google's recommendations for + Python code (search 'Google Python Style Guide'). There are some good bits of + wisdom in there, so I recommend at least skimming through it. +- And as one last related note, I'm now running Pylint on all my own Python + code. Highly recommended if you are doing serious scripting, as it can make + Python almost feel as type-safe as C++. +- The minimum required android version will now be 5.0 (a requirement of the + Python 3 builds I'm using) - Minimum required macOS version is now 10.13 (for similar reasons) -- 'bsInternal' module is now `_ba` (better lines up with standard Python practices) -- bs.writeConfig() and bs.applySettings() are no more. There is now ba.app.config which is basically a fancy dict class with some methods added such as commit() and apply() -- bs.getEnvironment() is no more; the values there are now available through ba.app (see notes down further) -- Fixed the mac build so command line input works again when launched from a terminal +- 'bsInternal' module is now `_ba` (better lines up with standard Python + practices) +- bs.writeConfig() and bs.applySettings() are no more. There is now + ba.app.config which is basically a fancy dict class with some methods added + such as commit() and apply() +- bs.getEnvironment() is no more; the values there are now available through + ba.app (see notes down further) +- Fixed the mac build so command line input works again when launched from a + terminal - Renamed 'exceptionOnNone' arg to 'doraise' in various calls. - bs.emitBGDynamics() is now ba.emitfx() - bs.shakeCamera() is now ba.camerashake() -- Various other minor name changes (bs.getUIBounds() -> ba.app.ui_bounds, etc.). I'm keeping old and new Python API docs around for now, so you can compare as needed. -- Renamed bot classes based on their actions instead of their appearances (ie: PirateBot -> ExplodeyBot) +- Various other minor name changes (bs.getUIBounds() -> ba.app.ui_bounds, etc.). + I'm keeping old and new Python API docs around for now, so you can compare as + needed. +- Renamed bot classes based on their actions instead of their appearances (ie: + PirateBot -> ExplodeyBot) - bs.getSharedObject() is now ba.stdobj() -- Removed bs.uni(), bs.utf8(), `bs.uni_to_ints()`, and `bs.uni_from_ints()` which are no longer needed due to Python 3's better string handling. -- Removed bs.SecureInt since it didn't do much to slow down hackers and hurts code readability. -- Renamed 'finalize' to 'expire' for actors and activities. 'Finalize' sounds too much like a destructor, which is not really what that is. -- bs.getMapsSupportingPlayType() is now simply ba.getmaps(). I might want to add more filter options to it besides just play-type, hence the renaming. -- Changed the concept of 'game', 'net', and 'real' times to 'sim', 'base', and 'real'. See time function docs for specifics. Also cleared up a few ambiguities over what can be used where. -- I'm converting all scripting functions to operate on floating-point seconds by default instead of integer milliseconds. This will let us support more accurate simulations later and is just cleaner I feel. To keep existing calls working you should be able to add timeformat='ms' and you'll get the old behavior (or multiply your time values by 0.001). Specific notes listed below. -- ba.Timer now takes its 'time' arg as seconds instead of milliseconds. To port old calls, add: timeformat='ms' to each call (or multiply your input by 0.001) -- ba.animate() now takes times in seconds and its 'driver' arg is now 'timetype' for consistency with other time functions. To port existing code you can pass timeformat='ms' to keep the old milliseconds based behavior. +- Removed bs.uni(), bs.utf8(), `bs.uni_to_ints()`, and `bs.uni_from_ints()` + which are no longer needed due to Python 3's better string handling. +- Removed bs.SecureInt since it didn't do much to slow down hackers and hurts + code readability. +- Renamed 'finalize' to 'expire' for actors and activities. 'Finalize' sounds + too much like a destructor, which is not really what that is. +- bs.getMapsSupportingPlayType() is now simply ba.getmaps(). I might want to add + more filter options to it besides just play-type, hence the renaming. +- Changed the concept of 'game', 'net', and 'real' times to 'sim', 'base', and ' + real'. See time function docs for specifics. Also cleared up a few ambiguities + over what can be used where. +- I'm converting all scripting functions to operate on floating-point seconds by + default instead of integer milliseconds. This will let us support more + accurate simulations later and is just cleaner I feel. To keep existing calls + working you should be able to add timeformat='ms' and you'll get the old + behavior (or multiply your time values by 0.001). Specific notes listed below. +- ba.Timer now takes its 'time' arg as seconds instead of milliseconds. To port + old calls, add: timeformat='ms' to each call (or multiply your input by 0.001) +- ba.animate() now takes times in seconds and its 'driver' arg is now 'timetype' + for consistency with other time functions. To port existing code you can pass + timeformat='ms' to keep the old milliseconds based behavior. - ditto for `ba.animate_array()` - ba.Activity.end() now takes seconds instead of milliseconds as its delay arg. - TNTSpawner now also takes seconds instead of milliseconds for `respawn_time`. -- There is a new ba.timer() function which is used for all one-off timer creation. It has the same args as the ba.Timer() class constructor. -- bs.gameTimer() is no more. Pass timeformat='ms' to ba.timer() if you need to recreate its behavior. -- bs.netTimer() is no more. Pass timetype='base' and timeformat='ms' to ba.timer() if you need to recreate its behavior. -- bs.realTimer() is no more. Pass timetype='real' and timeformat='ms' to ba.timer() if you need to recreate its behavior. -- There is a new ba.time() function for getting time values; it has consistent args with the new ba.timer() and ba.Timer() calls. -- bs.getGameTime() is no more. Pass timeformat='ms' to ba.time() if you need to recreate its behavior. -- bs.getNetTime() is no more. Pass timetype='base' and timeformat='ms' to ba.time() if you need to recreate its behavior. -- bs.getRealTime() is no more. Pass timetype='real' and timeformat='ms' to ba.time() if you need to recreate its behavior. -- bs.getTimeString() is now just ba.timestring(), and accepts seconds by default (pass timeformat='ms' to keep old calls working). -- bs.callInGameThread() has been replaced by an optional `from_other_thread` arg for ba.pushcall() -- There is now a special `ba.UNHANDLED` value that handlemessage() calls should return any time they don't handle a passed message. This will allow fallback message types and other nice things in the future. -- Wired the boolean operator up to ba.Actor's exists() method, so now a simple "if mynode" will do the right thing for both Actors and None values instead of having to explicitly check for both. -- Ditto for ba.Node; you can now just do 'if mynode' which will do the right thing for both a dead Node or None. +- There is a new ba.timer() function which is used for all one-off timer + creation. It has the same args as the ba.Timer() class constructor. +- bs.gameTimer() is no more. Pass timeformat='ms' to ba.timer() if you need to + recreate its behavior. +- bs.netTimer() is no more. Pass timetype='base' and timeformat='ms' to + ba.timer() if you need to recreate its behavior. +- bs.realTimer() is no more. Pass timetype='real' and timeformat='ms' to + ba.timer() if you need to recreate its behavior. +- There is a new ba.time() function for getting time values; it has consistent + args with the new ba.timer() and ba.Timer() calls. +- bs.getGameTime() is no more. Pass timeformat='ms' to ba.time() if you need to + recreate its behavior. +- bs.getNetTime() is no more. Pass timetype='base' and timeformat='ms' to + ba.time() if you need to recreate its behavior. +- bs.getRealTime() is no more. Pass timetype='real' and timeformat='ms' to + ba.time() if you need to recreate its behavior. +- bs.getTimeString() is now just ba.timestring(), and accepts seconds by default + (pass timeformat='ms' to keep old calls working). +- bs.callInGameThread() has been replaced by an optional `from_other_thread` arg + for ba.pushcall() +- There is now a special `ba.UNHANDLED` value that handlemessage() calls should + return any time they don't handle a passed message. This will allow fallback + message types and other nice things in the future. +- Wired the boolean operator up to ba.Actor's exists() method, so now a simple " + if mynode" will do the right thing for both Actors and None values instead of + having to explicitly check for both. +- Ditto for ba.Node; you can now just do 'if mynode' which will do the right + thing for both a dead Node or None. - Ditto for ba.InputDevice, ba.Widget, ba.Player -- Added a bs.App class accessible via ba.app; will be migrating global app values there instead of littering python modules with globals. The only remaining module globals should be all-caps public 'constants' -- 'Internal' methods and classes living in `_ba` and elsewhere no longer start with underscores. They are now simply marked with '(internal)' in their docstrings. 'Internal' bits are likely to have janky interfaces and can change without warning, so be wary of using them. If you find yourself depending on some internal thing often, please let me know, and I can try to clean it up and make it 'public'. +- Added a bs.App class accessible via ba.app; will be migrating global app + values there instead of littering python modules with globals. The only + remaining module globals should be all-caps public 'constants' +- 'Internal' methods and classes living in `_ba` and elsewhere no longer start + with underscores. They are now simply marked with '(internal)' in their + docstrings. 'Internal' bits are likely to have janky interfaces and can + change without warning, so be wary of using them. If you find yourself + depending on some internal thing often, please let me know, and I can try to + clean it up and make it 'public'. - bs.getLanguage() is no more; that value is now accessible via ba.app.language -- bs.Actor now accepts an optional 'node' arg which it will store as `self.node` if passed. Its default DieMessage() and exists() handlers will use `self.node` if it exists. This removes the need for a separate NodeActor() for simple cases. +- bs.Actor now accepts an optional 'node' arg which it will store as `self.node` + if passed. Its default DieMessage() and exists() handlers will use `self.node` + if it exists. This removes the need for a separate NodeActor() for simple + cases. - bs.NodeActor is no more (it can simply be replaced with ba.Actor()) -- bs.playMusic() is now ba.setmusic() which better fits its functionality (it sometimes just continues playing or stops playing). -- The bs.Vector class is no more; in its place is a shiny new ba.Vec3 which is implemented internally in C++ so its nice and speedy. Will probably update certain things like vector node attrs to support this class in the future since it makes vector math nice and convenient. -- Ok you get the point... see [ballistica.net](https://ballistica.net) for more info on these changes. +- bs.playMusic() is now ba.setmusic() which better fits its functionality (it + sometimes just continues playing or stops playing). +- The bs.Vector class is no more; in its place is a shiny new ba.Vec3 which is + implemented internally in C++ so its nice and speedy. Will probably update + certain things like vector node attrs to support this class in the future + since it makes vector math nice and convenient. +- Ok you get the point... see [ballistica.net](https://ballistica.net) for more + info on these changes. ### 1.4.155 (14377) + - Added protection against a repeated-input attack in lobbies. ### 1.4.151 (14371) + - Added Chinese-Traditional language and improved translations for others. ### 1.4.150 (14369) + - Telnet port can now be specified in the config -- Telnet socket no longer opens on headless build when telnet access is off (reduces DoS attack potential) -- Added a `filter_chat_message()` call which can be used by servers to intercept/modify/block all chat messages. -- `bsInternal._disconnectClient()` now takes an optional banTime arg (in seconds, defaults to old value of 300). +- Telnet socket no longer opens on headless build when telnet access is off ( + reduces DoS attack potential) +- Added a `filter_chat_message()` call which can be used by servers to + intercept/modify/block all chat messages. +- `bsInternal._disconnectClient()` now takes an optional banTime arg (in + seconds, defaults to old value of 300). ### 1.4.148 (14365) + - Added a password option for telnet access on server builds ### 1.4.147 (14364) -- Fixes an issue where a client rejoining a server after being kicked could get stuck in limbo + +- Fixes an issue where a client rejoining a server after being kicked could get + stuck in limbo - Language updates -- Increased security on games that list themselves as public. All joining players must now be validated by the master server, or they will be kicked. This will let me globally ban accounts or ip addresses from joining games to avoid things like ad spam-bots (which has been a problem this week). +- Increased security on games that list themselves as public. All joining + players must now be validated by the master server, or they will be kicked. + This will let me globally ban accounts or ip addresses from joining games to + avoid things like ad spam-bots (which has been a problem this week). - Added a max chat message length of 100 - Clients sending abnormal amounts of data to the server will now be auto-kicked ### 1.4.145 (14351) -- Mostly a maintenance release (real work is happening in the 1.5/2.0 branch) - minor bug fixes and a few language updates. -- Google deprecated some older SDKs, so the minimum Android supported by this build is now 4.4 + +- Mostly a maintenance release (real work is happening in the 1.5/2.0 branch) - + minor bug fixes and a few language updates. +- Google deprecated some older SDKs, so the minimum Android supported by this + build is now 4.4 ### 1.4.144 (14350) + - Added Greek translation ### 1.4.143 (14347) -- Fixed an issue where server names starting and ending with curly brackets would display incorrectly -- Fixed an issue where an android back-button press very soon after launch could lead to a crash -- Fixed a potential crash if a remove-player call is made for a player that has already left + +- Fixed an issue where server names starting and ending with curly brackets + would display incorrectly +- Fixed an issue where an android back-button press very soon after launch could + lead to a crash +- Fixed a potential crash if a remove-player call is made for a player that has + already left ### 1.4.142 (14346) -- Fixed an issue in my rigid body simulation code which could lead to crashes when large numbers of bodies are present + +- Fixed an issue in my rigid body simulation code which could lead to crashes + when large numbers of bodies are present ### 1.4.141 (14344) -- Fixed a longstanding bug in my huffman compression code that could cause an extra byte of unallocated memory to be read, leading to occasional crashes + +- Fixed a longstanding bug in my huffman compression code that could cause an + extra byte of unallocated memory to be read, leading to occasional crashes ### 1.4.140 (14343) + - Fixed a few minor outstanding bugs from the 1.4.139 update ### 1.4.139 (14340) -- Added an option to the server builds to point to a server-stats webpage that will show up as an extra link in the server browser (in client 1.4.139+) -- Removed the language column from the server browser. This was more relevant back when all clients saw the game in the server's language, and is nowadays largely just hijacked for silly purposes. Holler if you miss it. -- Server list now re-pings servers less often and averages ping results to reduce the amount of jumping around in the list. Please holler if this feels off. -- Added some slick new client-verification tech. Going forward it should be pretty much impossible to fool a server into thinking you are using a different account than you really are. -- Added a `get_account_id()` method to the bs.Player class. This will return a player's signed-in account-id (when it can be verified for certain) + +- Added an option to the server builds to point to a server-stats webpage that + will show up as an extra link in the server browser (in client 1.4.139+) +- Removed the language column from the server browser. This was more relevant + back when all clients saw the game in the server's language, and is nowadays + largely just hijacked for silly purposes. Holler if you miss it. +- Server list now re-pings servers less often and averages ping results to + reduce the amount of jumping around in the list. Please holler if this feels + off. +- Added some slick new client-verification tech. Going forward it should be + pretty much impossible to fool a server into thinking you are using a + different account than you really are. +- Added a `get_account_id()` method to the bs.Player class. This will return a + player's signed-in account-id (when it can be verified for certain) ### 1.4.138 (14336) -- Removed SDL library from the server builds, so that's one less dependency that needs to be installed when setting up a linux server + +- Removed SDL library from the server builds, so that's one less dependency that + needs to be installed when setting up a linux server ### 1.4.137 (14331) -- Lots of internal code cleanup and reorganization before I dig into networking rework (hopefully didn't break too much) -- Slowly cleaning up Python files (hoping to move closer to PEP 8 standards and eventually Python 3) + +- Lots of internal code cleanup and reorganization before I dig into networking + rework (hopefully didn't break too much) +- Slowly cleaning up Python files (hoping to move closer to PEP 8 standards and + eventually Python 3) - Added Hindi language - Cleared out some old build types (farewell OUYA; thanks for the memories) -- Added support for meshes with > 65535 verts (though turns out OpenGL ES2 doesn't support this so moot at the moment) +- Added support for meshes with > 65535 verts (though turns out OpenGL ES2 + doesn't support this so moot at the moment) ### 1.4.136 (14327) + - Updated 'kiosk mode' functionality (used for simple demo versions of the game) - Lots of work getting VR builds up to date -- Fixed an issue where 'report this player' window would show up behind the window that spawned it +- Fixed an issue where 'report this player' window would show up behind the + window that spawned it ### 1.4.135 (14324) -- Updated various SDKs for the android build (now building against api 27, removed inmobi ads, etc.) + +- Updated various SDKs for the android build (now building against api 27, + removed inmobi ads, etc.) ### 1.4.134 (14322) -- Fixed an issue where the internal keyboard would sometimes show up behind game windows -- Fixed an issue where UI widget selection would sometimes loop incorrectly at window edges -- Fixed an issue where overlay windows such as the quit dialog would allow clicks to pass through to regular windows under them + +- Fixed an issue where the internal keyboard would sometimes show up behind game + windows +- Fixed an issue where UI widget selection would sometimes loop incorrectly at + window edges +- Fixed an issue where overlay windows such as the quit dialog would allow + clicks to pass through to regular windows under them - Work on 2.0 UI (not yet enabled) ### 1.4.133 (14318) + - Pro upgrade now unlocks custom team names and colors in teams mode - Added a 'Mute Chat' option for completely ignoring incoming chat messages - Fixed a longstanding issue where player-selectors could get 'stuck' -- Pro upgrade now unlocks a new exact color picker option for character colors/highlights/etc. -- Added new flag icons to the store: Iran, Poland, Argentina, Philippines, and Chile -- Added an option for translators to be notified by the game whenever there are new phrases to translate (under settings->advanced) +- Pro upgrade now unlocks a new exact color picker option for character + colors/highlights/etc. +- Added new flag icons to the store: Iran, Poland, Argentina, Philippines, and + Chile +- Added an option for translators to be notified by the game whenever there are + new phrases to translate (under settings->advanced) - Increased quality on some models, textures and audio - Assault no longer counts dead bodies hitting the flag as scores - Replay speed can now be controlled with -/= keys (on devices with keyboards) - Added Serbian language - Remote app connections are now disabled by default on server builds -- Server wrapper script now supports python 3 in addition to python 2. (Python 3 support in the actual game will still be awhile) -- Added better crash reporting on Android, so I can hopefully fix bugs more quickly. -- bs.Lstr() can now take a 'fallbackResource' or 'fallbackValue' argument; the old 'fallback' argument is deprecated -- Removed the long-since-deprecated bs.translate() and bs.getResource() calls (bs.Lstr() should be used for all that stuff now) -- Removed some deprecated functions from GameActivity: getInstanceScoreBoardNameLocalized(), getInstanceNameLocalized(), getConfigDescriptionLocalized() +- Server wrapper script now supports python 3 in addition to python 2. (Python 3 + support in the actual game will still be awhile) +- Added better crash reporting on Android, so I can hopefully fix bugs more + quickly. +- bs.Lstr() can now take a 'fallbackResource' or 'fallbackValue' argument; the + old 'fallback' argument is deprecated +- Removed the long-since-deprecated bs.translate() and bs.getResource() calls ( + bs.Lstr() should be used for all that stuff now) +- Removed some deprecated functions from GameActivity: + getInstanceScoreBoardNameLocalized(), getInstanceNameLocalized(), + getConfigDescriptionLocalized() ### 1.4.132 (14316) -- Fixed an issue where the game could get stuck in a black screen after resuming on Android + +- Fixed an issue where the game could get stuck in a black screen after resuming + on Android ### 1.4.131 (14315) + - Replay playback speed can now be adjusted in the menu - Fixed an issue with touch controls showing up while in party chat - Fixed issues with the new anti-turbo logic when hosting ### 1.4.130 (14313) + - New character: Grumbledorf the Wizard - Improved public party browsing performance - Added protections against turbo exploits when hosting - Fixed issues with some Android controllers not being recognized ### 1.4.126 (14307) + - Improved keyboard and mouse support on Android ### 1.4.125 (14306) + - Added support for keyboards on Android -- Added support for desktop-like environments such as Samsung DeX and Chromebooks on Android +- Added support for desktop-like environments such as Samsung DeX and + Chromebooks on Android - Optimized game UI for wide-screen layouts such as the Galaxy Note 8 ### 1.4.121 (14302) + - Added support for account unlinking ### 1.4.118 (14298) + - Added 64-bit arm binary to Android builds ### 1.4.111 (14286) -- BallisticaCore Pro now unlocks 2 additional characters -- multi-line chat messages are now clamped down to 1 line; should prevent annoying multi-line fullscreen message spam + +- BallisticaKit Pro now unlocks 2 additional characters +- multi-line chat messages are now clamped down to 1 line; should prevent + annoying multi-line fullscreen message spam ### 1.4.106 (14280) -- the game will now only print 'game full' player-rejection messages to the client attempting to join; should reduce annoying message spam. + +- the game will now only print 'game full' player-rejection messages to the + client attempting to join; should reduce annoying message spam. ### 1.4.101 (14268) -- the game will now attempt to load connecting players' profiles and info from my master-server instead of trusting the player; should reduce cheating + +- the game will now attempt to load connecting players' profiles and info from + my master-server instead of trusting the player; should reduce cheating ### 1.4.100 (14264) -- added a 'playlistCode' option in the server config which corresponds with playlist codes added in Ballisticacore 1.4.100 (used for sharing playlists with friends). Now you can create a custom playlist, grab a code for it, and easily use it in a dedicated server. + +- added a 'playlistCode' option in the server config which corresponds with + playlist codes added in Ballisticakit 1.4.100 (used for sharing playlists with + friends). Now you can create a custom playlist, grab a code for it, and easily + use it in a dedicated server. ### 1.4.99 (14252) -- there is now a forced 10-second delay between a player leaving the game and another player from that same client joining the game. This should fix the exploit where players were leaving and re-joining to avoid spawn times. -- most in-game text is now set as bs.Lstr() values so that they show up in the client's own language instead of the server's There are currently a few exceptions such as time values which I need to address. + +- there is now a forced 10-second delay between a player leaving the game and + another player from that same client joining the game. This should fix the + exploit where players were leaving and re-joining to avoid spawn times. +- most in-game text is now set as bs.Lstr() values so that they show up in the + client's own language instead of the server's There are currently a few + exceptions such as time values which I need to address. ### 1.4.98 (14248) -- added kick-votes that can be started by any client. Currently, a client must type '0' or '1' in chat to vote, but I'll add buttons for them soon. -- modified text nodes so that they can display in each client's own language. (most text nodes don't do this yet but the capability is there). However, this means older clients can't connect to 1.4.98 servers, so you may want to stick with an older server for a bit until the userbase gets more updated. + +- added kick-votes that can be started by any client. Currently, a client must + type '0' or '1' in chat to vote, but I'll add buttons for them soon. +- modified text nodes so that they can display in each client's own language. ( + most text nodes don't do this yet but the capability is there). However, this + means older clients can't connect to 1.4.98 servers, so you may want to stick + with an older server for a bit until the userbase gets more updated. ### 1.4.97 (14247) -- back to displaying long names in more places; mainly just the in-game ones are clamped... trying to find a good balance... + +- back to displaying long names in more places; mainly just the in-game ones are + clamped... trying to find a good balance... ### 1.4.97 (14246) -- public party names will now show up for clients as the title of their party windows instead of "My Party" and also during connect/disconnect (requires client 14246+) -- server now ignores 'locked' states on maps/game-types, so meteor-shower, target-practice, etc. should work now + +- public party names will now show up for clients as the title of their party + windows instead of "My Party" and also during connect/disconnect (requires + client 14246+) +- server now ignores 'locked' states on maps/game-types, so meteor-shower, + target-practice, etc. should work now ### 1.4.97 (14244) + - kicked players are now unable to rejoin for a several minutes ### 1.4.96 (14242) -- chat messages and the party window now show player names instead of account names when possible -- server now clamps in-game names to 8 characters so there's some hope of reading them in-game. Can loosen this or add controls for how clamping happens if need be. + +- chat messages and the party window now show player names instead of account + names when possible +- server now clamps in-game names to 8 characters so there's some hope of + reading them in-game. Can loosen this or add controls for how clamping happens + if need be. ### 1.4.96 (14241) -- added an automatic chat-block to combat chat spammers. Block durations start at 10 seconds and double with each repeat offense + +- added an automatic chat-block to combat chat spammers. Block durations start + at 10 seconds and double with each repeat offense ### 1.4.95 (14240) -- fixed an issue where a single account could not be used to host multiple parties at once + +- fixed an issue where a single account could not be used to host multiple + parties at once ### 1.4.95 (14236) -- added a port option to the config, so it's now possible to host multiple parties on one machine (note that ballisticacore 1.4.95+ is required to connect ports aside from 43210) + +- added a port option to the config, so it's now possible to host multiple + parties on one machine (note that ballisticakit 1.4.95+ is required to connect + ports aside from 43210) ### 1.4.95 (14234) -- fixed a bug that could cause the Windows version to freeze randomly after a while + +- fixed a bug that could cause the Windows version to freeze randomly after a + while ### 1.4.95 (14233) -- ballisticacore (both `bs_headless` and regular) now reads commands from standard input, making it easier to run commands via scripts or the terminal -- server now runs using a 'server' account-type instead of the local 'device' account. (avoids daily-ticket-reward messages and other stuff that's not relevant to servers) -- the server script now passes args to the game as a json file instead of individual args; this should keep things cleaner and more expandable -- the `ballisticacore_server` script also now reads commands from stdin, allowing reconfiguring server settings on the fly -- added more options such as the ability to set game series lengths and to host a non-public party + +- ballisticakit (both `bs_headless` and regular) now reads commands from + standard input, making it easier to run commands via scripts or the terminal +- server now runs using a 'server' account-type instead of the local 'device' + account. (avoids daily-ticket-reward messages and other stuff that's not + relevant to servers) +- the server script now passes args to the game as a json file instead of + individual args; this should keep things cleaner and more expandable +- the `ballisticakit_server` script also now reads commands from stdin, allowing + reconfiguring server settings on the fly +- added more options such as the ability to set game series lengths and to host + a non-public party ### 1.4.94 + - now have mac, windows, and both 32 and 64-bit linux server builds -- added an optional config.py file that can be used instead of modifying the server script itself +- added an optional config.py file that can be used instead of modifying the + server script itself - added an autoBalanceTeams option for teams games - people joining and leaving the party are no longer announced (too much noise) ### 1.4.93 + - should now properly allow clients to use their unlocked characters - added an option to enable telnet access diff --git a/Makefile b/Makefile index 59935c80..f45cc78c 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,18 @@ # working with Ballistica. These build rules are also handy as reference or a # starting point if you need specific funtionality beyond that exposed here. # Targets in this top level Makefile do not expect -jX to be passed to them -# and generally handle spawning an appropriate number of child jobs themselves. +# and generally handle spawning an appropriate number of child jobs +# themselves. +# +# Note: Some of these targets use the lazybuild system to keep things extra +# efficient. This means, for example, that if nothing in asset sources has +# changed since the last successful asset build, we won't even dive into the +# src/assets dir and trigger the Makefile there the next time the 'assets' +# target here gets built. However, if you manually clear out built assets or +# muck around in src/assets yourself you may need to blow away the lazybuild +# states to get things building again. These live in '.cache/lazybuild'. You +# can blow away individual files there or the whole directory to force +# lazybuild to run builds it wouldn't otherwise run. ################################################################################ @@ -17,111 +28,138 @@ help: @tools/pcommand makefile_target_list Makefile -# Set env-var BA_ENABLE_IRONY_BUILD_DB=1 to enable creating/updating a -# cmake compile-commands database for use with irony for emacs (and possibly -# other tools). -# FIXME - this can break if we move/rename files and then run 'make update' -# because it tries to use the previous cmakelist with the no-longer-valid -# filename *before* it updates the cmakelist. Need to rethink order of -# operations there. +# Set env-var BA_ENABLE_IRONY_BUILD_DB=1 to enable creating/updating a cmake +# compile-commands database for use with irony for emacs (and possibly other +# tools). ifeq ($(BA_ENABLE_IRONY_BUILD_DB),1) - PREREQ_IRONY = .cache/irony/compile_commands.json + PREREQ_IRONY_BUILD_DB = .cache/irony/compile_commands.json endif -PREREQS = .cache/checkenv $(PREREQ_IRONY) .dir-locals.el \ - .mypy.ini .pycheckers .pylintrc .style.yapf .clang-format \ - ballisticacore-cmake/.clang-format .editorconfig +# Prereq targets that should be safe to run anytime; even if project-files +# are out of date. +PREREQS_SAFE = .cache/checkenv .dir-locals.el .mypy.ini .pycheckers .pylintrc \ + .style.yapf .clang-format ballisticakit-cmake/.clang-format .editorconfig + +# Prereq targets that may break if the project needs updating should go here. +# An example is compile-command-databases; these might try to run cmake and +# fail if the CMakeList files don't match what's on disk. If such a target was +# included in PREREQS_SAFE it would try to build *before* project updates +# which would leave us stuck in a broken state. +PREREQS_POST_UPDATE_ONLY = $(PREREQ_IRONY_BUILD_DB) # Target that should be built before running most any other build. # This installs tool config files, runs environment checks, etc. -prereqs: ${PREREQS} +prereqs: $(PREREQS_SAFE) $(PREREQS_POST_UPDATE_ONLY) + +# Set of prereqs that is safe to run if the project state is dirty. +prereqs-pre-update: $(PREREQS_SAFE) prereqs-clean: - @rm -rf ${PREREQS} + rm -rf $(PREREQS_SAFE) $(PREREQS_POST_UPDATE_ONLY) # Build all assets for all platforms. assets: prereqs meta - cd assets && $(MAKE) -j$(CPUS) + @tools/pcommand lazybuild assets_src $(LAZYBUILDDIR)/$@ \ + cd src/assets \&\& $(MAKE) -j$(CPUS) # Build assets required for cmake builds (linux, mac) assets-cmake: prereqs meta - cd assets && $(MAKE) -j$(CPUS) cmake + @tools/pcommand lazybuild assets_src $(LAZYBUILDDIR)/$@ \ + cd src/assets \&\& $(MAKE) -j$(CPUS) cmake + +# Build only script assets required for cmake builds (linux, mac) +assets-cmake-scripts: prereqs meta + @tools/pcommand lazybuild assets_src $(LAZYBUILDDIR)/$@ \ + cd src/assets \&\& $(MAKE) -j$(CPUS) scripts-cmake # Build assets required for WINDOWS_PLATFORM windows builds. assets-windows: prereqs meta - cd assets && $(MAKE) -j$(CPUS) win-${WINDOWS_PLATFORM} + @tools/pcommand lazybuild assets_src $(LAZYBUILDDIR)/$@ \ + cd src/assets \&\& $(MAKE) -j$(CPUS) win-$(WINDOWS_PLATFORM) # Build assets required for Win32 windows builds. assets-windows-Win32: prereqs meta - cd assets && $(MAKE) -j$(CPUS) win-Win32 + @tools/pcommand lazybuild assets_src $(LAZYBUILDDIR)/$@ \ + cd src/assets \&\& $(MAKE) -j$(CPUS) win-Win32 # Build assets required for x64 windows builds. assets-windows-x64: prereqs meta - cd assets && $(MAKE) -j$(CPUS) win-x64 + @tools/pcommand lazybuild assets_src $(LAZYBUILDDIR)/$@ \ + cd src/assets \&\& $(MAKE) -j$(CPUS) win-x64 # Build assets required for mac xcode builds assets-mac: prereqs meta - cd assets && $(MAKE) -j$(CPUS) mac + @tools/pcommand lazybuild assets_src $(LAZYBUILDDIR)/$@ \ + cd src/assets \&\& $(MAKE) -j$(CPUS) mac # Build assets required for ios. assets-ios: prereqs meta - cd assets && $(MAKE) -j$(CPUS) ios + @tools/pcommand lazybuild assets_src $(LAZYBUILDDIR)/$@ \ + cd src/assets \&\& $(MAKE) -j$(CPUS) ios # Build assets required for android. assets-android: prereqs meta - cd assets && $(MAKE) -j$(CPUS) android + @tools/pcommand lazybuild assets_src $(LAZYBUILDDIR)/$@ \ + cd src/assets \&\& $(MAKE) -j$(CPUS) android # Clean all assets. assets-clean: - cd assets && $(MAKE) clean + @rm -f $(LAZYBUILDDIR)/assets* + cd src/assets && $(MAKE) clean # Build resources. resources: prereqs meta - tools/pcommand lazybuild resources_src ${LAZYBUILDDIR}/resources \ - cd resources \&\& $(MAKE) -j$(CPUS) resources + @tools/pcommand lazybuild resources_src $(LAZYBUILDDIR)/$@ \ + cd src/resources \&\& $(MAKE) -j$(CPUS) # Clean resources. resources-clean: - cd resources && $(MAKE) clean - rm -f ${LAZYBUILDDIR}/resources + rm -f $(LAZYBUILDDIR)/resources + cd src/resources && $(MAKE) clean # Build our generated sources. # Meta builds can affect sources used by asset builds, resource builds, and # compiles, so it should be listed as a dependency of any of those. meta: prereqs - tools/pcommand lazybuild meta_src ${LAZYBUILDDIR}/meta \ + @tools/pcommand lazybuild meta_src $(LAZYBUILDDIR)/$@ \ cd src/meta \&\& $(MAKE) -j$(CPUS) # Clean our generated sources. meta-clean: + rm -f $(LAZYBUILDDIR)/meta cd src/meta && $(MAKE) clean - rm -f ${LAZYBUILDDIR}/meta # Remove ALL files and directories that aren't managed by git # (except for a few things such as localconfig.json). clean: - ${CHECK_CLEAN_SAFETY} - git clean -dfx ${ROOT_CLEAN_IGNORES} + $(CHECK_CLEAN_SAFETY) + rm -rf build + git clean -dfx $(ROOT_CLEAN_IGNORES) # Show what clean would delete without actually deleting it. clean-list: - ${CHECK_CLEAN_SAFETY} - git clean -dnx ${ROOT_CLEAN_IGNORES} + $(CHECK_CLEAN_SAFETY) + @echo Would remove build # We do this ourself; not git. + git clean -dnx $(ROOT_CLEAN_IGNORES) -# Force regenerate the dummy module. -dummymodules: - ./tools/pcommand update_dummy_modules --force +# Build/update dummy python modules. +# IMPORTANT - building this target can kick off full builds/cleans and so +# it should not be built in parallel with other targets. +# See py_check_prepass target for more info. +dummymodules: build/dummymodules/.dummy_modules_state + +dummymodules-clean: + rm -rf build/dummymodules # Generate docs. docs: assets-cmake @tools/pcommand gendocs # Tell make which of these targets don't represent files. -.PHONY: help prereqs prereqs-clean assets assets-cmake assets-windows \ - assets-windows-Win32 assets-windows-x64 \ - assets-mac assets-ios assets-android assets-clean \ - resources resources-clean meta meta-clean \ - clean clean-list dummymodules docs +.PHONY: help prereqs prereqs-pre-update prereqs-clean assets assets-cmake \ + assets-cmake-scripts assets-windows assets-windows-Win32 assets-windows-x64 \ + assets-mac assets-ios assets-android assets-clean resources resources-clean \ + meta meta-clean clean clean-list dummymodules docs ################################################################################ @@ -176,344 +214,344 @@ WINPLAT_X86 = Win32 # Mac gui debug: RUN_PREFAB_MAC_X86_64_GUI_DEBUG = cd build/prefab/full/mac_x86_64_gui/debug \ - && ./ballisticacore + && ./ballisticakit RUN_PREFAB_MAC_ARM64_GUI_DEBUG = cd build/prefab/full/mac_arm64_gui/debug \ - && ./ballisticacore + && ./ballisticakit prefab-mac-x86-64-gui-debug: prefab-mac-x86-64-gui-debug-build @tools/pcommand ensure_prefab_platform mac_x86_64 - @${RUN_PREFAB_MAC_X86_64_GUI_DEBUG} + @$(RUN_PREFAB_MAC_X86_64_GUI_DEBUG) prefab-mac-arm64-gui-debug: prefab-mac-arm64-gui-debug-build @tools/pcommand ensure_prefab_platform mac_arm64 - @${RUN_PREFAB_MAC_ARM64_GUI_DEBUG} + @$(RUN_PREFAB_MAC_ARM64_GUI_DEBUG) prefab-mac-x86-64-gui-debug-build: prereqs assets-cmake \ - build/prefab/full/mac_x86_64_gui/debug/ballisticacore - @${STAGE_ASSETS} -cmake build/prefab/full/mac_x86_64_gui/debug + build/prefab/full/mac_x86_64_gui/debug/ballisticakit + @$(STAGE_ASSETS) -cmake build/prefab/full/mac_x86_64_gui/debug prefab-mac-arm64-gui-debug-build: prereqs assets-cmake \ - build/prefab/full/mac_arm64_gui/debug/ballisticacore - @${STAGE_ASSETS} -cmake build/prefab/full/mac_arm64_gui/debug + build/prefab/full/mac_arm64_gui/debug/ballisticakit + @$(STAGE_ASSETS) -cmake build/prefab/full/mac_arm64_gui/debug -build/prefab/full/mac_%_gui/debug/ballisticacore: .efrocachemap +build/prefab/full/mac_%_gui/debug/ballisticakit: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/mac_%_gui/debug/libballisticacore_internal.a: .efrocachemap +build/prefab/lib/mac_%_gui/debug/libballisticakit_internal.a: .efrocachemap @tools/pcommand efrocache_get $@ # Mac gui release: RUN_PREFAB_MAC_X86_64_GUI_RELEASE = cd \ - build/prefab/full/mac_x86_64_gui/release && ./ballisticacore + build/prefab/full/mac_x86_64_gui/release && ./ballisticakit RUN_PREFAB_MAC_ARM64_GUI_RELEASE = cd build/prefab/full/mac_arm64_gui/release \ - && ./ballisticacore + && ./ballisticakit prefab-mac-x86-64-gui-release: prefab-mac-x86-64-gui-release-build @tools/pcommand ensure_prefab_platform mac_x86_64 - @${RUN_PREFAB_MAC_X86_64_GUI_RELEASE} + @$(RUN_PREFAB_MAC_X86_64_GUI_RELEASE) prefab-mac-arm64-gui-release: prefab-mac-arm64-gui_release-build @tools/pcommand ensure_prefab_platform mac_arm64 - @${RUN_PREFAB_MAC_ARM64_GUI_RELEASE} + @$(RUN_PREFAB_MAC_ARM64_GUI_RELEASE) prefab-mac-x86-64-gui-release-build: prereqs assets-cmake \ - build/prefab/full/mac_x86_64_gui/release/ballisticacore - @${STAGE_ASSETS} -cmake build/prefab/full/mac_x86_64_gui/release + build/prefab/full/mac_x86_64_gui/release/ballisticakit + @$(STAGE_ASSETS) -cmake build/prefab/full/mac_x86_64_gui/release prefab-mac-arm64-gui-release-build: prereqs assets-cmake \ - build/prefab/full/mac_arm64_gui/release/ballisticacore - @${STAGE_ASSETS} -cmake build/prefab/full/mac_arm64_gui/release + build/prefab/full/mac_arm64_gui/release/ballisticakit + @$(STAGE_ASSETS) -cmake build/prefab/full/mac_arm64_gui/release -build/prefab/full/mac_%_gui/release/ballisticacore: .efrocachemap +build/prefab/full/mac_%_gui/release/ballisticakit: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/mac_%_gui/release/libballisticacore_internal.a: .efrocachemap +build/prefab/lib/mac_%_gui/release/libballisticakit_internal.a: .efrocachemap @tools/pcommand efrocache_get $@ # Mac server debug: RUN_PREFAB_MAC_X86_64_SERVER_DEBUG = cd \ - build/prefab/full/mac_x86_64_server/debug && ./ballisticacore_server + build/prefab/full/mac_x86_64_server/debug && ./ballisticakit_server RUN_PREFAB_MAC_ARM64_SERVER_DEBUG = cd \ - build/prefab/full/mac_arm64_server/debug && ./ballisticacore_server + build/prefab/full/mac_arm64_server/debug && ./ballisticakit_server prefab-mac-x86-64-server-debug: prefab-mac-x86-64-server-debug-build @tools/pcommand ensure_prefab_platform mac_x86_64 - @${RUN_PREFAB_MAC_X86_64_SERVER_DEBUG} + @$(RUN_PREFAB_MAC_X86_64_SERVER_DEBUG) prefab-mac-arm64-server-debug: prefab-mac-arm64-server-debug-build @tools/pcommand ensure_prefab_platform mac_arm64 - @${RUN_PREFAB_MAC_ARM64_SERVER_DEBUG} + @$(RUN_PREFAB_MAC_ARM64_SERVER_DEBUG) prefab-mac-x86-64-server-debug-build: prereqs assets-cmake \ - build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless - @${STAGE_ASSETS} -cmakeserver -debug build/prefab/full/mac_x86_64_server/debug + build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless + @$(STAGE_ASSETS) -cmakeserver -debug build/prefab/full/mac_x86_64_server/debug prefab-mac-arm64-server-debug-build: prereqs assets-cmake \ - build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless - @${STAGE_ASSETS} -cmakeserver -debug build/prefab/full/mac_arm64_server/debug + build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless + @$(STAGE_ASSETS) -cmakeserver -debug build/prefab/full/mac_arm64_server/debug -build/prefab/full/mac_%_server/debug/dist/ballisticacore_headless: .efrocachemap +build/prefab/full/mac_%_server/debug/dist/ballisticakit_headless: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/mac_%_server/debug/libballisticacore_internal.a: .efrocachemap +build/prefab/lib/mac_%_server/debug/libballisticakit_internal.a: .efrocachemap @tools/pcommand efrocache_get $@ # Mac server release: RUN_PREFAB_MAC_X86_64_SERVER_RELEASE = cd \ - build/prefab/full/mac_x86_64_server/release && ./ballisticacore_server + build/prefab/full/mac_x86_64_server/release && ./ballisticakit_server RUN_PREFAB_MAC_ARM64_SERVER_RELEASE = cd \ - build/prefab/full/mac_arm64_server/release && ./ballisticacore_server + build/prefab/full/mac_arm64_server/release && ./ballisticakit_server prefab-mac-x86-64-server-release: prefab-mac-x86-64-server-release-build @tools/pcommand ensure_prefab_platform mac_x86_64 - @${RUN_PREFAB_MAC_X86_64_SERVER_RELEASE} + @$(RUN_PREFAB_MAC_X86_64_SERVER_RELEASE) prefab-mac-arm64-server-release: prefab-mac-arm64-server-release-build @tools/pcommand ensure_prefab_platform mac_arm64 - @${RUN_PREFAB_MAC_ARM64_SERVER_RELEASE} + @$(RUN_PREFAB_MAC_ARM64_SERVER_RELEASE) prefab-mac-x86-64-server-release-build: prereqs assets-cmake \ - build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless - @${STAGE_ASSETS} -cmakeserver -release \ + build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless + @$(STAGE_ASSETS) -cmakeserver -release \ build/prefab/full/mac_x86_64_server/release prefab-mac-arm64-server-release-build: prereqs assets-cmake \ - build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless - @${STAGE_ASSETS} -cmakeserver -release \ + build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless + @$(STAGE_ASSETS) -cmakeserver -release \ build/prefab/full/mac_arm64_server/release -build/prefab/full/mac_%_server/release/dist/ballisticacore_headless: .efrocachemap +build/prefab/full/mac_%_server/release/dist/ballisticakit_headless: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/mac_%_server/release/libballisticacore_internal.a: .efrocachemap +build/prefab/lib/mac_%_server/release/libballisticakit_internal.a: .efrocachemap @tools/pcommand efrocache_get $@ # Linux gui debug: RUN_PREFAB_LINUX_X86_64_GUI_DEBUG = cd \ - build/prefab/full/linux_x86_64_gui/debug && ./ballisticacore + build/prefab/full/linux_x86_64_gui/debug && ./ballisticakit RUN_PREFAB_LINUX_ARM64_GUI_DEBUG = cd \ - build/prefab/full/linux_arm64_gui/debug && ./ballisticacore + build/prefab/full/linux_arm64_gui/debug && ./ballisticakit prefab-linux-x86-64-gui-debug: prefab-linux-x86-64-gui-debug-build @tools/pcommand ensure_prefab_platform linux_x86_64 - @${RUN_PREFAB_LINUX_X86_64_GUI_DEBUG} + @$(RUN_PREFAB_LINUX_X86_64_GUI_DEBUG) prefab-linux-arm64-gui-debug: prefab-linux-arm64-gui-debug-build @tools/pcommand ensure_prefab_platform linux_arm64 - @${RUN_PREFAB_LINUX_ARM64_GUI_DEBUG} + @$(RUN_PREFAB_LINUX_ARM64_GUI_DEBUG) prefab-linux-x86-64-gui-debug-build: prereqs assets-cmake \ - build/prefab/full/linux_x86_64_gui/debug/ballisticacore - @${STAGE_ASSETS} -cmake build/prefab/full/linux_x86_64_gui/debug + build/prefab/full/linux_x86_64_gui/debug/ballisticakit + @$(STAGE_ASSETS) -cmake build/prefab/full/linux_x86_64_gui/debug prefab-linux-arm64-gui-debug-build: prereqs assets-cmake \ - build/prefab/full/linux_arm64_gui/debug/ballisticacore - @${STAGE_ASSETS} -cmake build/prefab/full/linux_arm64_gui/debug + build/prefab/full/linux_arm64_gui/debug/ballisticakit + @$(STAGE_ASSETS) -cmake build/prefab/full/linux_arm64_gui/debug -build/prefab/full/linux_%_gui/debug/ballisticacore: .efrocachemap +build/prefab/full/linux_%_gui/debug/ballisticakit: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/linux_%_gui/debug/libballisticacore_internal.a: .efrocachemap +build/prefab/lib/linux_%_gui/debug/libballisticakit_internal.a: .efrocachemap @tools/pcommand efrocache_get $@ # Linux gui release: RUN_PREFAB_LINUX_X86_64_GUI_RELEASE = cd \ - build/prefab/full/linux_x86_64_gui/release && ./ballisticacore + build/prefab/full/linux_x86_64_gui/release && ./ballisticakit RUN_PREFAB_LINUX_ARM64_GUI_RELEASE = cd \ - build/prefab/full/linux_arm64_gui/release && ./ballisticacore + build/prefab/full/linux_arm64_gui/release && ./ballisticakit prefab-linux-x86-64-gui-release: prefab-linux-x86-64-gui-release-build @tools/pcommand ensure_prefab_platform linux_x86_64 - @${RUN_PREFAB_LINUX_X86_64_GUI_RELEASE} + @$(RUN_PREFAB_LINUX_X86_64_GUI_RELEASE) prefab-linux-arm64-gui-release: prefab-linux-arm64-gui-release-build @tools/pcommand ensure_prefab_platform linux_arm64 - @${RUN_PREFAB_LINUX_ARM64_GUI_RELEASE} + @$(RUN_PREFAB_LINUX_ARM64_GUI_RELEASE) prefab-linux-x86-64-gui-release-build: prereqs assets-cmake \ - build/prefab/full/linux_x86_64_gui/release/ballisticacore - @${STAGE_ASSETS} -cmake build/prefab/full/linux_x86_64_gui/release + build/prefab/full/linux_x86_64_gui/release/ballisticakit + @$(STAGE_ASSETS) -cmake build/prefab/full/linux_x86_64_gui/release prefab-linux-arm64-gui-release-build: prereqs assets-cmake \ - build/prefab/full/linux_arm64_gui/release/ballisticacore - @${STAGE_ASSETS} -cmake build/prefab/full/linux_arm64_gui/release + build/prefab/full/linux_arm64_gui/release/ballisticakit + @$(STAGE_ASSETS) -cmake build/prefab/full/linux_arm64_gui/release -build/prefab/full/linux_%_gui/release/ballisticacore: .efrocachemap +build/prefab/full/linux_%_gui/release/ballisticakit: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/linux_%_gui/release/libballisticacore_internal.a: .efrocachemap +build/prefab/lib/linux_%_gui/release/libballisticakit_internal.a: .efrocachemap @tools/pcommand efrocache_get $@ # Linux server debug: RUN_PREFAB_LINUX_X86_64_SERVER_DEBUG = cd \ - build/prefab/full/linux_x86_64_server/debug && ./ballisticacore_server + build/prefab/full/linux_x86_64_server/debug && ./ballisticakit_server RUN_PREFAB_LINUX_ARM64_SERVER_DEBUG = cd \ - build/prefab/full/linux_arm64_server/debug && ./ballisticacore_server + build/prefab/full/linux_arm64_server/debug && ./ballisticakit_server prefab-linux-x86-64-server-debug: prefab-linux-x86-64-server-debug-build @tools/pcommand ensure_prefab_platform linux_x86_64 - @${RUN_PREFAB_LINUX_X86_64_SERVER_DEBUG} + @$(RUN_PREFAB_LINUX_X86_64_SERVER_DEBUG) prefab-linux-arm64-server-debug: prefab-linux-arm64-server-debug-build @tools/pcommand ensure_prefab_platform linux_arm64 - @${RUN_PREFAB_LINUX_ARM64_SERVER_DEBUG} + @$(RUN_PREFAB_LINUX_ARM64_SERVER_DEBUG) prefab-linux-x86-64-server-debug-build: prereqs assets-cmake \ - build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless - @${STAGE_ASSETS} -cmakeserver -debug \ + build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless + @$(STAGE_ASSETS) -cmakeserver -debug \ build/prefab/full/linux_x86_64_server/debug prefab-linux-arm64-server-debug-build: prereqs assets-cmake \ - build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless - @${STAGE_ASSETS} -cmakeserver -debug \ + build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless + @$(STAGE_ASSETS) -cmakeserver -debug \ build/prefab/full/linux_arm64_server/debug -build/prefab/full/linux_%_server/debug/dist/ballisticacore_headless: .efrocachemap +build/prefab/full/linux_%_server/debug/dist/ballisticakit_headless: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/linux_%_server/debug/libballisticacore_internal.a: .efrocachemap +build/prefab/lib/linux_%_server/debug/libballisticakit_internal.a: .efrocachemap @tools/pcommand efrocache_get $@ # Linux server release: RUN_PREFAB_LINUX_X86_64_SERVER_RELEASE = cd \ - build/prefab/full/linux_x86_64_server/release && ./ballisticacore_server + build/prefab/full/linux_x86_64_server/release && ./ballisticakit_server RUN_PREFAB_LINUX_ARM64_SERVER_RELEASE = cd \ - build/prefab/full/linux_arm64_server/release && ./ballisticacore_server + build/prefab/full/linux_arm64_server/release && ./ballisticakit_server prefab-linux-x86-64-server-release: prefab-linux-x86-64-server-release-build @tools/pcommand ensure_prefab_platform linux_x86_64 - @${RUN_PREFAB_LINUX_X86_64_SERVER_RELEASE} + @$(RUN_PREFAB_LINUX_X86_64_SERVER_RELEASE) prefab-linux-arm64-server-release: prefab-linux-arm64-server-release-build @tools/pcommand ensure_prefab_platform linux_arm64 - @${RUN_PREFAB_LINUX_ARM64_SERVER_RELEASE} + @$(RUN_PREFAB_LINUX_ARM64_SERVER_RELEASE) prefab-linux-x86-64-server-release-build: prereqs assets-cmake \ - build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless - @${STAGE_ASSETS} -cmakeserver -release \ + build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless + @$(STAGE_ASSETS) -cmakeserver -release \ build/prefab/full/linux_x86_64_server/release prefab-linux-arm64-server-release-build: prereqs assets-cmake \ - build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless - @${STAGE_ASSETS} -cmakeserver -release \ + build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless + @$(STAGE_ASSETS) -cmakeserver -release \ build/prefab/full/linux_arm64_server/release -build/prefab/full/linux_%_server/release/dist/ballisticacore_headless: .efrocachemap +build/prefab/full/linux_%_server/release/dist/ballisticakit_headless: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/linux_%_server/release/libballisticacore_internal.a: .efrocachemap +build/prefab/lib/linux_%_server/release/libballisticakit_internal.a: .efrocachemap @tools/pcommand efrocache_get $@ # Windows gui debug: RUN_PREFAB_WINDOWS_X86_GUI_DEBUG = cd build/prefab/full/windows_x86_gui/debug \ - && ./BallisticaCore.exe + && ./BallisticaKit.exe prefab-windows-x86-gui-debug: prefab-windows-x86-gui-debug-build @tools/pcommand ensure_prefab_platform windows_x86 - @{RUN_PREFAB_WINDOWS_X86_GUI_DEBUG} + @$(RUN_PREFAB_WINDOWS_X86_GUI_DEBUG) -prefab-windows-x86-gui-debug-build: prereqs assets-windows-${WINPLAT_X86} \ - build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe - @${STAGE_ASSETS} -win-${WINPLAT_X86}-Debug \ +prefab-windows-x86-gui-debug-build: prereqs assets-windows-$(WINPLAT_X86) \ + build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe + @$(STAGE_ASSETS) -win-$(WINPLAT_X86)-Debug \ build/prefab/full/windows_x86_gui/debug -build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe: .efrocachemap +build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/windows/Debug_%/BallisticaCoreGenericInternal.lib: .efrocachemap +build/prefab/lib/windows/Debug_%/BallisticaKitGenericInternal.lib: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/windows/Debug_%/BallisticaCoreGenericInternal.pdb: .efrocachemap +build/prefab/lib/windows/Debug_%/BallisticaKitGenericInternal.pdb: .efrocachemap @tools/pcommand efrocache_get $@ # Windows gui release: RUN_PREFAB_WINDOWS_X86_GUI_RELEASE = cd \ - build/prefab/full/windows_x86_gui/release && ./BallisticaCore.exe + build/prefab/full/windows_x86_gui/release && ./BallisticaKit.exe prefab-windows-x86-gui-release: prefab-windows-x86-gui-release-build @tools/pcommand ensure_prefab_platform windows_x86 - @{RUN_PREFAB_WINDOWS_X86_GUI_RELEASE} + @$(RUN_PREFAB_WINDOWS_X86_GUI_RELEASE) prefab-windows-x86-gui-release-build: prereqs \ - assets-windows-${WINPLAT_X86} \ - build/prefab/full/windows_x86_gui/release/BallisticaCore.exe - @${STAGE_ASSETS} -win-${WINPLAT_X86}-Release \ + assets-windows-$(WINPLAT_X86) \ + build/prefab/full/windows_x86_gui/release/BallisticaKit.exe + @$(STAGE_ASSETS) -win-$(WINPLAT_X86)-Release \ build/prefab/full/windows_x86_gui/release -build/prefab/full/windows_x86_gui/release/BallisticaCore.exe: .efrocachemap +build/prefab/full/windows_x86_gui/release/BallisticaKit.exe: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/windows/Release_%/BallisticaCoreGenericInternal.lib: .efrocachemap +build/prefab/lib/windows/Release_%/BallisticaKitGenericInternal.lib: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/windows/Release_%/BallisticaCoreGenericInternal.pdb: .efrocachemap +build/prefab/lib/windows/Release_%/BallisticaKitGenericInternal.pdb: .efrocachemap @tools/pcommand efrocache_get $@ # Windows server debug: RUN_PREFAB_WINDOWS_X86_SERVER_DEBUG = cd \ build/prefab/full/windows_x86_server/debug \ - && dist/python_d.exe ballisticacore_server.py + && dist/python_d.exe ballisticakit_server.py prefab-windows-x86-server-debug: prefab-windows-x86-server-debug-build @tools/pcommand ensure_prefab_platform windows_x86 - @{RUN_PREFAB_WINDOWS_X86_SERVER_DEBUG} + @$(RUN_PREFAB_WINDOWS_X86_SERVER_DEBUG) prefab-windows-x86-server-debug-build: prereqs \ - assets-windows-${WINPLAT_X86} \ - build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe - @${STAGE_ASSETS} -winserver-${WINPLAT_X86}-Debug \ + assets-windows-$(WINPLAT_X86) \ + build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe + @$(STAGE_ASSETS) -winserver-$(WINPLAT_X86)-Debug \ build/prefab/full/windows_x86_server/debug -build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe: .efrocachemap +build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/windows/Debug_%/BallisticaCoreHeadlessInternal.lib: .efrocachemap +build/prefab/lib/windows/Debug_%/BallisticaKitHeadlessInternal.lib: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/windows/Debug_%/BallisticaCoreHeadlessInternal.pdb: .efrocachemap +build/prefab/lib/windows/Debug_%/BallisticaKitHeadlessInternal.pdb: .efrocachemap @tools/pcommand efrocache_get $@ # Windows server release: RUN_PREFAB_WINDOWS_X86_SERVER_RELEASE = cd \ build/prefab/full/windows_x86_server/release \ - && dist/python.exe -O ballisticacore_server.py + && dist/python.exe -O ballisticakit_server.py prefab-windows-x86-server-release: prefab-windows-x86-server-release-build @tools/pcommand ensure_prefab_platform windows_x86 - @{RUN_PREFAB_WINDOWS_X86_SERVER_RELEASE} + @$(RUN_PREFAB_WINDOWS_X86_SERVER_RELEASE) prefab-windows-x86-server-release-build: prereqs \ - assets-windows-${WINPLAT_X86} \ - build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe - @${STAGE_ASSETS} -winserver-${WINPLAT_X86}-Release \ + assets-windows-$(WINPLAT_X86) \ + build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe + @$(STAGE_ASSETS) -winserver-$(WINPLAT_X86)-Release \ build/prefab/full/windows_x86_server/release -build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe: .efrocachemap +build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/windows/Release_%/BallisticaCoreHeadlessInternal.lib: .efrocachemap +build/prefab/lib/windows/Release_%/BallisticaKitHeadlessInternal.lib: .efrocachemap @tools/pcommand efrocache_get $@ -build/prefab/lib/windows/Release_%/BallisticaCoreHeadlessInternal.pdb: .efrocachemap +build/prefab/lib/windows/Release_%/BallisticaKitHeadlessInternal.pdb: .efrocachemap @tools/pcommand efrocache_get $@ # Tell make which of these targets don't represent files. @@ -551,12 +589,17 @@ build/prefab/lib/windows/Release_%/BallisticaCoreHeadlessInternal.pdb: .efrocach ################################################################################ # Update any project files that need it (does NOT build projects). -update: prereqs +update: prereqs-pre-update @tools/pcommand update_project +# Though not technically necessary, let's keep things like tool-configs +# immediately updated so our editors/etc. better reflect the current state. + @$(MAKE) -j$(CPUS) prereqs + @tools/pcommand echo GRN Update-Project: SUCCESS! # Don't update but fail if anything needs it. -update-check: prereqs +update-check: prereqs-pre-update @tools/pcommand update_project --check + @tools/pcommand echo GRN Check-Project: Everything up to date. # Tell make which of these targets don't represent files. .PHONY: update update-check @@ -609,24 +652,32 @@ format-makefile: prereqs ################################################################################ # Run all project checks. (static analysis) -check: +check: py_check_prepass @$(DMAKE) -j$(CPUS) update-check cpplint pylint mypy @tools/pcommand echo SGRN BLD ALL CHECKS PASSED! # Same as check but no caching (all files are checked). -check-full: +check-full: py_check_prepass @$(DMAKE) -j$(CPUS) update-check cpplint-full pylint-full mypy-full @tools/pcommand echo SGRN BLD ALL CHECKS PASSED! # Same as 'check' plus optional/slow extra checks. -check2: - @$(DMAKE) -j$(CPUS) update-check cpplint pylint mypy pycharm +# check2: +# @$(DMAKE) -j$(CPUS) update-check cpplint pylint mypy pycharm depcheck +# @tools/pcommand echo SGRN BLD ALL CHECKS PASSED! +# TEMP - disabling some during refactor. +check2: py_check_prepass + @$(DMAKE) -j$(CPUS) update-check cpplint mypy @tools/pcommand echo SGRN BLD ALL CHECKS PASSED! # Same as check2 but no caching (all files are checked). -check2-full: - @$(DMAKE) -j$(CPUS) update-check cpplint-full pylint-full mypy-full \ - pycharm-full +# check2-full: +# @$(DMAKE) -j$(CPUS) update-check cpplint-full pylint-full mypy-full \ +# pycharm-full +# @tools/pcommand echo SGRN BLD ALL CHECKS PASSED! +# TEMP - disabling some during refactor. +check2-full: py_check_prepass + @$(DMAKE) -j$(CPUS) update-check cpplint-full mypy-full @tools/pcommand echo SGRN BLD ALL CHECKS PASSED! # Run Cpplint checks on all C/C++ code. @@ -638,41 +689,114 @@ cpplint-full: prereqs meta @tools/pcommand cpplint -full # Run Pylint checks on all Python Code. -pylint: prereqs meta +pylint: py_check_prepass @tools/pcommand pylint # Run Pylint checks without caching (all files are checked). -pylint-full: prereqs meta +pylint-full: py_check_prepass @tools/pcommand pylint -full # Run Mypy checks on all Python code. -mypy: prereqs meta +mypy: py_check_prepass @tools/pcommand mypy # Run Mypy checks without caching (all files are checked). -mypy-full: prereqs meta +mypy-full: py_check_prepass @tools/pcommand mypy -full # Run Mypy checks on all Python code using daemon mode. -dmypy: prereqs meta +dmypy: py_check_prepass @tools/pcommand dmypy # Stop the mypy daemon -dmypy-stop: prereqs meta +dmypy-stop: py_check_prepass @tools/pcommand dmypy -stop # Run PyCharm checks on all Python code. -pycharm: prereqs meta +pycharm: py_check_prepass @tools/pcommand pycharm # Run PyCharm checks without caching (all files are checked). -pycharm-full: prereqs meta +pycharm-full: py_check_prepass @tools/pcommand pycharm -full +# Run extra mypy checks with various dependency permutations. +# ensures packages don't depend on thing they're not supposed to. +depchecks: py_check_prepass + @tools/pcommand depchecks + +featuresettest: py_check_prepass + echo WOULD DO FS + +# Build prerequisites needed for python checks. +# +# IMPORTANT - this target may kick off new meta/asset/binary builds/cleans as +# part of doing its thing. For this reason, be sure this target gets built +# alone in a make process and not mixed in with others as it would likely +# stomp on them or their dependencies. +# +# Practically, this means any target depending on this should list it as its +# one and only dependency. And when any such target gets built alongside +# others (such as by the 'check-full' target) the parent target should +# explicitly built this beforehand to ensure it does not happen during the +# parallel part. +# Note to self: Originally prereqs and meta were listed as deps of +# .dummy_modules_state, but because prereqs has no output dependencies +# it always fires which meant dummy modules would get rebuilt every run. +# This config should do the right thing without violating the above rules. +py_check_prepass: prereqs meta + @$(MAKE) dummymodules + +# The following section is auto-generated; do not edit by hand. +# __AUTOGENERATED_DUMMY_MODULES_BEGIN__ + +# Update dummy Python modules when source files contributing to them change. +build/dummymodules/.dummy_modules_state: \ + src/ballistica/base/python/class/python_class_app_timer.cc \ + src/ballistica/base/python/class/python_class_context_call.cc \ + src/ballistica/base/python/class/python_class_context_ref.cc \ + src/ballistica/base/python/class/python_class_display_timer.cc \ + src/ballistica/base/python/class/python_class_feature_set_data.cc \ + src/ballistica/base/python/class/python_class_simple_sound.cc \ + src/ballistica/base/python/class/python_class_vec3.cc \ + src/ballistica/base/python/methods/python_methods_app.cc \ + src/ballistica/base/python/methods/python_methods_graphics.cc \ + src/ballistica/base/python/methods/python_methods_misc.cc \ + src/ballistica/classic/python/methods/python_methods_classic.cc \ + src/ballistica/scene_v1/python/class/python_class_activity_data.cc \ + src/ballistica/scene_v1/python/class/python_class_base_timer.cc \ + src/ballistica/scene_v1/python/class/python_class_input_device.cc \ + src/ballistica/scene_v1/python/class/python_class_material.cc \ + src/ballistica/scene_v1/python/class/python_class_node.cc \ + src/ballistica/scene_v1/python/class/python_class_scene_collision_mesh.cc \ + src/ballistica/scene_v1/python/class/python_class_scene_data_asset.cc \ + src/ballistica/scene_v1/python/class/python_class_scene_mesh.cc \ + src/ballistica/scene_v1/python/class/python_class_scene_sound.cc \ + src/ballistica/scene_v1/python/class/python_class_scene_texture.cc \ + src/ballistica/scene_v1/python/class/python_class_scene_timer.cc \ + src/ballistica/scene_v1/python/class/python_class_session_data.cc \ + src/ballistica/scene_v1/python/class/python_class_session_player.cc \ + src/ballistica/scene_v1/python/methods/python_methods_assets.cc \ + src/ballistica/scene_v1/python/methods/python_methods_input.cc \ + src/ballistica/scene_v1/python/methods/python_methods_networking.cc \ + src/ballistica/scene_v1/python/methods/python_methods_scene.cc \ + src/ballistica/template_fs/python/class/python_class_hello.cc \ + src/ballistica/template_fs/python/methods/python_methods_template_fs.cc \ + src/ballistica/ui_v1/python/class/python_class_ui_mesh.cc \ + src/ballistica/ui_v1/python/class/python_class_ui_sound.cc \ + src/ballistica/ui_v1/python/class/python_class_ui_texture.cc \ + src/ballistica/ui_v1/python/class/python_class_widget.cc \ + src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc + @tools/pcommand with_build_lock gen_dummy_modules_lock \ + rm -rf build/dummymodules \&\& mkdir -p build/dummymodules \ + \&\& ./tools/pcommand gen_dummy_modules \ + \&\& touch build/dummymodules/.dummy_modules_state +# __AUTOGENERATED_DUMMY_MODULES_END__ + # Tell make which of these targets don't represent files. .PHONY: check check-full check2 check2-full \ cpplint cpplint-full pylint pylint-full mypy \ - mypy-full pycharm pycharm-full + mypy-full dmypy dmypy-stop pycharm pycharm-full py_check_prepass ################################################################################ @@ -682,7 +806,7 @@ pycharm-full: prereqs meta ################################################################################ # Run all tests. (live execution verification) -test: prereqs meta +test: py_check_prepass @tools/pcommand echo BLU Running all tests... @tools/pcommand pytest -v tests @@ -692,7 +816,7 @@ test-full: test # Individual test with extra output enabled. test-assetmanager: @tools/pcommand pytest -o log_cli=true -o log_cli_level=debug -s -vv \ - tests/test_ba/test_assetmanager.py::test_assetmanager + tests/test_babase/test_assetmanager.py::test_assetmanager # Individual test with extra output enabled. test-message: @@ -770,23 +894,23 @@ WINDOWS_CONFIGURATION ?= Debug # Stage assets and other files so a built binary will run. windows-staging: assets-windows resources meta - ${STAGE_ASSETS} -win-${WINPLT}-${WINCFG} \ + $(STAGE_ASSETS) -win-$(WINPLT)-$(WINCFG) \ build/windows/$(WINCFG)_$(WINPLT) # Build and run a debug windows build (from WSL). windows-debug: windows-debug-build @tools/pcommand ensure_prefab_platform windows_x86 - build/windows/Debug_Win32/BallisticaCoreGeneric.exe + build/windows/Debug_Win32/BallisticaKitGeneric.exe # Build and run a release windows build (from WSL). windows-release: windows-release-build @tools/pcommand ensure_prefab_platform windows_x86 - build/windows/Release_Win32/BallisticaCoreGeneric.exe + build/windows/Release_Win32/BallisticaKitGeneric.exe # Build a debug windows build (from WSL). windows-debug-build: \ - build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib \ - build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb + build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericInternal.lib \ + build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericInternal.pdb @tools/pcommand ensure_prefab_platform windows_x86 @tools/pcommand wsl_build_check_win_drive WINDOWS_CONFIGURATION=Debug WINDOWS_PLATFORM=Win32 $(MAKE) windows-staging @@ -795,8 +919,8 @@ windows-debug-build: \ # Rebuild a debug windows build (from WSL). windows-debug-rebuild: \ - build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib \ - build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb + build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericInternal.lib \ + build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericInternal.pdb @tools/pcommand ensure_prefab_platform windows_x86 @tools/pcommand wsl_build_check_win_drive WINDOWS_CONFIGURATION=Debug WINDOWS_PLATFORM=Win32 $(MAKE) windows-staging @@ -805,8 +929,8 @@ windows-debug-rebuild: \ # Build a release windows build (from WSL). windows-release-build: \ - build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib \ - build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb + build/prefab/lib/windows/Release_Win32/BallisticaKitGenericInternal.lib \ + build/prefab/lib/windows/Release_Win32/BallisticaKitGenericInternal.pdb @tools/pcommand ensure_prefab_platform windows_x86 @tools/pcommand wsl_build_check_win_drive WINDOWS_CONFIGURATION=Release WINDOWS_PLATFORM=Win32 $(MAKE) windows-staging @@ -815,8 +939,8 @@ windows-release-build: \ # Rebuild a release windows build (from WSL). windows-release-rebuild: \ - build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib \ - build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb + build/prefab/lib/windows/Release_Win32/BallisticaKitGenericInternal.lib \ + build/prefab/lib/windows/Release_Win32/BallisticaKitGenericInternal.pdb @tools/pcommand ensure_prefab_platform windows_x86 @tools/pcommand wsl_build_check_win_drive WINDOWS_CONFIGURATION=Release WINDOWS_PLATFORM=Win32 $(MAKE) windows-staging @@ -825,15 +949,15 @@ windows-release-rebuild: \ # Remove all non-git-managed files in windows subdir. windows-clean: - @${CHECK_CLEAN_SAFETY} - git clean -dfx ballisticacore-windows - rm -rf build/windows ${LAZYBUILDDIR} + @$(CHECK_CLEAN_SAFETY) + git clean -dfx ballisticakit-windows + rm -rf build/windows $(LAZYBUILDDIR) # Show what would be cleaned. windows-clean-list: - @${CHECK_CLEAN_SAFETY} - git clean -dnx ballisticacore-windows - echo would also remove build/windows ${LAZYBUILDDIR} + @$(CHECK_CLEAN_SAFETY) + git clean -dnx ballisticakit-windows + echo would also remove build/windows $(LAZYBUILDDIR) ################################################################################ @@ -849,42 +973,50 @@ CMAKE_BUILD_TYPE ?= Debug # Build and run the cmake build. cmake: cmake-build - @cd build/cmake/$(CM_BT_LC) && ./ballisticacore + @cd build/cmake/$(CM_BT_LC) && ./ballisticakit + +# Build and run the cmake build under lldb. +cmake-lldb: cmake-build + @cd build/cmake/$(CM_BT_LC) && BA_DEBUGGER_ATTACHED=1 lldb ./ballisticakit # Build but don't run it. -cmake-build: assets-cmake resources meta +cmake-build: assets-cmake resources cmake-binary + @$(STAGE_ASSETS) -cmake build/cmake/$(CM_BT_LC) + +cmake-binary: meta @tools/pcommand cmake_prep_dir build/cmake/$(CM_BT_LC) - @tools/pcommand update_cmake_prefab_lib standard ${CM_BT_LC} build/cmake/${CM_BT_LC} - @${STAGE_ASSETS} -cmake build/cmake/$(CM_BT_LC) @cd build/cmake/$(CM_BT_LC) && test -f Makefile \ || cmake -DCMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) \ - ${PWD}/ballisticacore-cmake - @cd build/cmake/$(CM_BT_LC) && $(MAKE) -j$(CPUS) ballisticacore + $(PWD)/ballisticakit-cmake + @tools/pcommand update_cmake_prefab_lib standard $(CM_BT_LC) build/cmake/$(CM_BT_LC) + @cd build/cmake/$(CM_BT_LC) && $(MAKE) -j$(CPUS) ballisticakit cmake-clean: rm -rf build/cmake/$(CM_BT_LC) cmake-server: cmake-server-build - @cd build/cmake/server-$(CM_BT_LC) && ./ballisticacore_server + @cd build/cmake/server-$(CM_BT_LC) && ./ballisticakit_server -cmake-server-build: assets-cmake resources meta +cmake-server-build: assets-cmake resources meta cmake-server-binary + @$(STAGE_ASSETS) -cmakeserver -$(CM_BT_LC) build/cmake/server-$(CM_BT_LC) + +# Build just the headless binary. +# Note: We currently symlink FOO_headless. In packaged builds we rename it. +cmake-server-binary: meta @tools/pcommand cmake_prep_dir build/cmake/server-$(CM_BT_LC)/dist - @tools/pcommand update_cmake_prefab_lib server ${CM_BT_LC} build/cmake/server-${CM_BT_LC}/dist - @${STAGE_ASSETS} -cmakeserver -${CM_BT_LC} build/cmake/server-$(CM_BT_LC) @cd build/cmake/server-$(CM_BT_LC)/dist && test -f Makefile \ || cmake -DCMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) -DHEADLESS=true \ - ${PWD}/ballisticacore-cmake + $(PWD)/ballisticakit-cmake + @tools/pcommand update_cmake_prefab_lib server $(CM_BT_LC) build/cmake/server-$(CM_BT_LC)/dist @cd build/cmake/server-$(CM_BT_LC)/dist && $(MAKE) -j$(CPUS) - @cd build/cmake/server-$(CM_BT_LC)/dist && test -f ballisticacore_headless \ - || ln -sf ballisticacore ballisticacore_headless cmake-server-clean: rm -rf build/cmake/server-$(CM_BT_LC) # Stage assets for building/running within CLion. clion-staging: assets-cmake resources meta - ${STAGE_ASSETS} -cmake build/clion_debug - ${STAGE_ASSETS} -cmake build/clion_release + $(STAGE_ASSETS) -cmake build/clion_debug + $(STAGE_ASSETS) -cmake build/clion_release # Tell make which of these targets don't represent files. .PHONY: cmake cmake-build cmake-clean cmake-server cmake-server-build \ @@ -897,29 +1029,32 @@ clion-staging: assets-cmake resources meta # # ################################################################################ -# Can be used in place of the recommended $(MAKE) to suppress various -# warnings about the jobserver being disabled due to -jX options being -# passed to sub-makes. Generally passing -jX as part of a recursive make -# instantiation is frowned upon, but we treat this Makefile as a high level -# orchestration layer and we want to handle details like that for the user, -# so we try to pick smart -j values ourselves. We can't really rely on the -# jobserver anyway to balance our loads since we often call out to other -# systems (XCode, Gradle, Visual Studio, etc.) which wrangle jobs in their -# own ways. +# Can be used in place of the recommended $(MAKE) to suppress various warnings +# about the jobserver being disabled due to -jX options being passed to +# sub-makes. Generally passing -jX as part of a recursive make instantiation +# is frowned upon, but we treat this Makefile as a high level orchestration +# layer and we want to handle details like that for the user, so we try to +# pick smart -j values ourselves. We can't really rely on the jobserver anyway +# to balance our loads since we often call out to other systems (XCode, +# Gradle, Visual Studio, etc.) which wrangle jobs in their own ways. DMAKE = $(MAKE) MAKEFLAGS= MKFLAGS= MAKELEVEL= # This should give the cpu count on linux and mac; may need to expand this # if using this on other platforms. CPUS = $(shell getconf _NPROCESSORS_ONLN || echo 8) -PROJ_DIR = ${abspath ${CURDIR}} +PROJ_DIR = $(abspath $(CURDIR)) VERSION = $(shell tools/pcommand version version) BUILD_NUMBER = $(shell tools/pcommand version build) -BUILD_DIR = ${PROJ_DIR}/build +BUILD_DIR = $(PROJ_DIR)/build LAZYBUILDDIR = .cache/lazybuild -STAGE_ASSETS = ${PROJ_DIR}/tools/pcommand stage_assets +STAGE_ASSETS = $(PROJ_DIR)/tools/pcommand stage_assets -# Things to ignore when doing root level cleans. -ROOT_CLEAN_IGNORES = --exclude=config/localconfig.json +# Things to ignore when doing root level cleans. Note that we exclude build +# and just blow that away manually; it might contain git repos or other things +# that can confuse git. +ROOT_CLEAN_IGNORES = --exclude=config/localconfig.json \ + --exclude=.spinoffdata \ + --exclude=/build CHECK_CLEAN_SAFETY = tools/pcommand check_clean_safety @@ -927,40 +1062,40 @@ CHECK_CLEAN_SAFETY = tools/pcommand check_clean_safety TOOL_CFG_INST = tools/pcommand tool_config_install # Anything that affects tool-config generation. -TOOL_CFG_SRC = tools/efrotools/pcommand.py config/config.json +TOOL_CFG_SRC = tools/efrotools/pcommand.py config/projectconfig.json # Anything that should trigger an environment-check when changed. ENV_SRC = tools/pcommand tools/batools/build.py -.clang-format: config/toolconfigsrc/clang-format ${TOOL_CFG_SRC} - @${TOOL_CFG_INST} $< $@ +.clang-format: config/toolconfigsrc/clang-format $(TOOL_CFG_SRC) + @$(TOOL_CFG_INST) $< $@ -.style.yapf: config/toolconfigsrc/style.yapf ${TOOL_CFG_SRC} - @${TOOL_CFG_INST} $< $@ +.style.yapf: config/toolconfigsrc/style.yapf $(TOOL_CFG_SRC) + @$(TOOL_CFG_INST) $< $@ -.pylintrc: config/toolconfigsrc/pylintrc ${TOOL_CFG_SRC} - @${TOOL_CFG_INST} $< $@ +.pylintrc: config/toolconfigsrc/pylintrc $(TOOL_CFG_SRC) + @$(TOOL_CFG_INST) $< $@ -.projectile: config/toolconfigsrc/projectile ${TOOL_CFG_SRC} - @${TOOL_CFG_INST} $< $@ +.projectile: config/toolconfigsrc/projectile $(TOOL_CFG_SRC) + @$(TOOL_CFG_INST) $< $@ -.editorconfig: config/toolconfigsrc/editorconfig ${TOOL_CFG_SRC} - @${TOOL_CFG_INST} $< $@ +.editorconfig: config/toolconfigsrc/editorconfig $(TOOL_CFG_SRC) + @$(TOOL_CFG_INST) $< $@ -.dir-locals.el: config/toolconfigsrc/dir-locals.el ${TOOL_CFG_SRC} - @${TOOL_CFG_INST} $< $@ +.dir-locals.el: config/toolconfigsrc/dir-locals.el $(TOOL_CFG_SRC) + @$(TOOL_CFG_INST) $< $@ -.mypy.ini: config/toolconfigsrc/mypy.ini ${TOOL_CFG_SRC} - @${TOOL_CFG_INST} $< $@ +.mypy.ini: config/toolconfigsrc/mypy.ini $(TOOL_CFG_SRC) + @$(TOOL_CFG_INST) $< $@ -.pycheckers: config/toolconfigsrc/pycheckers ${TOOL_CFG_SRC} - @${TOOL_CFG_INST} $< $@ +.pycheckers: config/toolconfigsrc/pycheckers $(TOOL_CFG_SRC) + @$(TOOL_CFG_INST) $< $@ # Set this to 1 to skip environment checks. SKIP_ENV_CHECKS ?= 0 -.cache/checkenv: ${ENV_SRC} - @if [ ${SKIP_ENV_CHECKS} -ne 1 ]; then \ +.cache/checkenv: $(ENV_SRC) + @if [ $(SKIP_ENV_CHECKS) -ne 1 ]; then \ tools/pcommand checkenv && mkdir -p .cache && touch .cache/checkenv; \ fi @@ -974,15 +1109,16 @@ _WMSBE_1B = /mnt/c/Program Files (x86)/Microsoft Visual Studio/2019 _WMSBE_2B = /Community/MSBuild/Current/Bin/MSBuild.exe VISUAL_STUDIO_VERSION = -property:VisualStudioVersion=16 -WIN_MSBUILD_EXE = ${_WMSBE_1}${_WMSBE_2} -WIN_MSBUILD_EXE_B = "${_WMSBE_1B}${_WMSBE_2B}" +WIN_MSBUILD_EXE = $(_WMSBE_1)$(_WMSBE_2) +WIN_MSBUILD_EXE_B = "$(_WMSBE_1B)$(_WMSBE_2B)" WINPRJ = $(WINDOWS_PROJECT) WINPLT = $(WINDOWS_PLATFORM) WINCFG = $(WINDOWS_CONFIGURATION) # When using CLion, our cmake dir is root. Expose .clang-format there too. -ballisticacore-cmake/.clang-format: .clang-format - @cd ballisticacore-cmake && ln -sf ../.clang-format . +ballisticakit-cmake/.clang-format: .clang-format + @mkdir -p ballisticakit-cmake + @cd ballisticakit-cmake && ln -sf ../.clang-format . # Simple target for CI to build a binary but not download/assemble assets/etc. _cmake-simple-ci-server-build: @@ -992,19 +1128,19 @@ _cmake-simple-ci-server-build: tools/pcommand update_cmake_prefab_lib \ server debug build/cmake_simple_ci_server_build cd build/cmake_simple_ci_server_build && \ - cmake -DCMAKE_BUILD_TYPE=Debug -DHEADLESS=true ${PWD}/ballisticacore-cmake + cmake -DCMAKE_BUILD_TYPE=Debug -DHEADLESS=true $(PWD)/ballisticakit-cmake cd build/cmake_simple_ci_server_build && $(MAKE) -j$(CPUS) # Irony in emacs requires us to use cmake to generate a full # list of compile commands for all files; lets try to keep it up to date # whenever CMakeLists changes. -.cache/irony/compile_commands.json: ballisticacore-cmake/CMakeLists.txt +.cache/irony/compile_commands.json: ballisticakit-cmake/CMakeLists.txt @tools/pcommand echo BLU Updating Irony build commands db... @echo Generating Irony compile-commands-list... @mkdir -p .cache/irony @cd .cache/irony \ && cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Debug \ - ${PWD}/ballisticacore-cmake + $(PWD)/ballisticakit-cmake @mv .cache/irony/compile_commands.json . \ && rm -rf .cache/irony \ && mkdir .cache/irony \ @@ -1015,23 +1151,23 @@ _windows-wsl-build: @tools/pcommand wsl_build_check_win_drive $(WIN_MSBUILD_EXE_B) \ $(shell tools/pcommand wsl_path_to_win --escape \ - ballisticacore-windows/$(WINPRJ)/BallisticaCore${WINPRJ}.vcxproj) \ + ballisticakit-windows/$(WINPRJ)/BallisticaKit$(WINPRJ).vcxproj) \ -target:Build \ -property:Configuration=$(WINCFG) \ -property:Platform=$(WINPLT) \ $(VISUAL_STUDIO_VERSION) - @tools/pcommand echo BLU BLD Built build/windows/BallisticaCore$(WINPRJ).exe. + @tools/pcommand echo BLU BLD Built build/windows/BallisticaKit$(WINPRJ).exe. _windows-wsl-rebuild: @tools/pcommand wsl_build_check_win_drive $(WIN_MSBUILD_EXE_B) \ $(shell tools/pcommand wsl_path_to_win --escape \ - ballisticacore-windows/$(WINPRJ)/BallisticaCore${WINPRJ}.vcxproj) \ + ballisticakit-windows/$(WINPRJ)/BallisticaKit$(WINPRJ).vcxproj) \ -target:Rebuild \ -property:Configuration=$(WINCFG) \ -property:Platform=$(WINPLT) \ $(VISUAL_STUDIO_VERSION) - @tools/pcommand echo BLU BLD Built build/windows/BallisticaCore$(WINPRJ).exe. + @tools/pcommand echo BLU BLD Built build/windows/BallisticaKit$(WINPRJ).exe. # Tell make which of these targets don't represent files. .PHONY: _cmake-simple-ci-server-build _windows-wsl-build _windows-wsl-rebuild diff --git a/README.md b/README.md index 5eed326d..af3fecab 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ - +logo ***bal·lis·tic***: physics of an object in motion; behaving like a projectile. @@ -6,38 +8,82 @@ ![](https://github.com/efroemling/ballistica/workflows/CI/badge.svg) -The Ballistica project is the foundation for [BombSquad](https://www.froemling.net/apps/bombsquad) and potentially other future projects. +The Ballistica project is the foundation for +[BombSquad](https://www.froemling.net/apps/bombsquad) and potentially other +future projects. -[Head to the project wiki to get started](https://github.com/efroemling/ballistica/wiki), or learn more about the project below. +[Head to the project wiki to get started +](https://github.com/efroemling/ballistica/wiki), or learn more about the +project below. ### Project Goals * **Do one thing and do it well** - Ballistica is not aiming to be a general purpose game engine. Rather, its goal is to support creating one particular type of experience: 'physics based multiplayer action on small diorama-like environments built from real-world objects'. If you've got something you'd like to create that can fit within that box (as BombSquad itself does), give Ballistica a look. Of course, there is nothing preventing you from going and building an FPS out of this stuff, but I wouldn't recommend it. + Ballistica is not aiming to be a general purpose game engine. Rather, its goal +is to support creating one particular type of experience: 'physics based +multiplayer action on small diorama-like environments built from real-world +objects'. If you've got something you'd like to create that can fit within that +box (as BombSquad itself does), give Ballistica a look. Of course, there is +nothing preventing you from going and building a first person shooter out of +this stuff, but I wouldn't recommend it. * **Python tomfoolery** - Ballistica is built on a C++ core for performance-sensitive code with a Python layer for high level game/app logic. This Python layer is designed to be mucked with. Users can override core game functionality, write their own mini games, or anything else they can dream up, either by directly accessing files on disk or by working through an integrated web-based editor. It can be a fun way to learn Python without the danger of getting actual work done. + Ballistica is built on a C++ core for performance-sensitive code with a +Python layer for high level game/app logic. This Python layer is designed to be +mucked with. Users can override core game functionality, write their own mini +games, or anything else they can dream up, either by directly accessing files on +disk or by working through an integrated web-based editor. It can be a fun way +to learn Python without the danger of getting actual work done. * **Physics-y goodness** - I love playing with physics simulations, and Ballistica was built partly to scratch this itch. Though the game physics in BombSquad have stayed largely unchanged for a while, my future plans for the engine lean heavily on making this more flexible and open-ended, opening up lots of fun multiplayer physics-y potential. Stay tuned... + I love playing with physics simulations, and Ballistica was built partly to +scratch this itch. Though the game physics in BombSquad have stayed largely +unchanged for a while, my future plans for the engine lean heavily on making +this more flexible and open-ended, opening up lots of fun multiplayer physics-y +potential. Stay tuned... * **Community** - BombSquad started as a 'just for fun' project to play with my friends, and I want to keep that spirit alive as the Ballistica project moves forward. Whether this means making it easier to share mods, organize tournaments, join up with friends, teach each other some Python, or whatever else. Life is short; let's play some games. Or make them. Maybe both. + BombSquad started as a 'just for fun' project to play with my friends, and I +want to keep that spirit alive as the Ballistica project moves forward. Whether +this means making it easier to share mods, organize tournaments, join up with +friends, teach each other some Python, or whatever else. Life is short; let's +play some games. Or make them. Maybe both. ### Frequently Asked Questions + * **Q: What's with this name? Is it BombSquad or Ballistica?** * A: BombSquad is the game. Ballistica is the engine. * **Q: Does this mean BombSquad is open source?** - * A: Yes and no. All code contained in this repo is MIT licensed and free for use anywhere. This includes game scripts, pipeline tools, and most of the binary engine sources. Anything not directly contained in this repository, however, even if automatically downloaded by build scripts, is still proprietary and cannot be redistributed without explicit consent. This includes assets and prebuilt libraries/binaries. So in a nutshell: create and share mods or use any of this code in your own projects, but please don't distribute your own complete copies of BombSquad without permission. Please email support@froemling.net if you have any questions about this. + * A: Yes and no. All code contained in this repo is MIT licensed and free for + use anywhere. This includes game scripts, pipeline tools, and most of the + binary engine sources. Anything not directly contained in this repository, + however, even if automatically downloaded by build scripts, is still + proprietary and cannot be redistributed without explicit consent. This + includes assets and prebuilt libraries/binaries. So in a nutshell: create and + share mods or use any of this code in your own projects, but please don't + distribute your own complete copies of BombSquad without permission. Please + email support@froemling.net if you have any questions about this. -* **Q: When are you adding more maps/characters/minigames/etc. to BombSquad!?!?** - * A: Check out the [Ballistica Roadmap](https://github.com/efroemling/ballistica/wiki/Roadmap) to get a sense of what's planned for when. And for the record, the answer to that particular question is basically '1.8'. +* **Q: When are you adding more maps/characters/minigames/etc. to + BombSquad!?!?** + * A: Check out the [Ballistica + Roadmap](https://github.com/efroemling/ballistica/wiki/Roadmap) to get a sense + of what's planned for when. And for the record, the answer to that particular + question is basically '1.8'. -* **Q: When will BombSquad be released on iOS / Steam / Switch / XBox / Playstation / My Toaster??** - * A: The 2.0 update will be the big 'relaunch' release and the plan is to launch on at least iOS and Steam at that time. I'm trying to get there as fast as I can. As far as consoles, I'd love to and hope to at some point but have nothing to announce just yet. See the [Ballistica Roadmap](https://github.com/efroemling/ballistica/wiki/Roadmap) for more details or check the [Ballistica Downloads](https://ballistica.net/downloads) page for early test builds on some platforms. +* **Q: When will BombSquad be released on iOS / Steam / Switch / XBox / +Playstation / My Toaster??** + * A: The 2.0 update will be the big 'relaunch' release and the plan is to + launch on at least iOS and Steam at that time. I'm trying to get there as fast + as I can. As far as consoles, I'd love to and hope to at some point but have + nothing to announce just yet. See the + * [Ballistica Roadmap](https://github.com/efroemling/ballistica/wiki/Roadmap) + for more details or check the [Ballistica + Downloads](https://ballistica.net/downloads) page for early test builds on + some platforms. diff --git a/assets/.asset_manifest_public.json b/assets/.asset_manifest_public.json deleted file mode 100644 index fb7c3b94..00000000 --- a/assets/.asset_manifest_public.json +++ /dev/null @@ -1,572 +0,0 @@ -[ - "ba_data/python/__pycache__/_bainternal.cpython-310.opt-1.pyc", - "ba_data/python/_bainternal.py", - "ba_data/python/ba/__init__.py", - "ba_data/python/ba/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_accountv1.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_accountv2.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_achievement.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_activity.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_activitytypes.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_actor.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_ads.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_analytics.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_app.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_appcomponent.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_appconfig.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_appdelegate.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_appmode.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_apputils.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_assetmanager.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_asyncio.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_benchmark.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_bootstrap.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_campaign.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_cloud.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_collision.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_coopgame.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_coopsession.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_dependency.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_dualteamsession.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_error.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_freeforallsession.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_gameactivity.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_gameresults.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_gameutils.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_general.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_hooks.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_input.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_internal.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_keyboard.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_language.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_level.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_lobby.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_login.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_map.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_math.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_messages.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_meta.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_multiteamsession.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_music.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_net.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_nodeactor.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_player.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_playlist.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_plugin.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_powerup.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_profile.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_score.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_servermode.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_session.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_settings.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_stats.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_store.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_team.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_teamgame.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_tips.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_tournament.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_ui.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/_workspace.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/internal.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/macmusicapp.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/modutils.cpython-310.opt-1.pyc", - "ba_data/python/ba/__pycache__/osmusic.cpython-310.opt-1.pyc", - "ba_data/python/ba/_accountv1.py", - "ba_data/python/ba/_accountv2.py", - "ba_data/python/ba/_achievement.py", - "ba_data/python/ba/_activity.py", - "ba_data/python/ba/_activitytypes.py", - "ba_data/python/ba/_actor.py", - "ba_data/python/ba/_ads.py", - "ba_data/python/ba/_analytics.py", - "ba_data/python/ba/_app.py", - "ba_data/python/ba/_appcomponent.py", - "ba_data/python/ba/_appconfig.py", - "ba_data/python/ba/_appdelegate.py", - "ba_data/python/ba/_appmode.py", - "ba_data/python/ba/_apputils.py", - "ba_data/python/ba/_assetmanager.py", - "ba_data/python/ba/_asyncio.py", - "ba_data/python/ba/_benchmark.py", - "ba_data/python/ba/_bootstrap.py", - "ba_data/python/ba/_campaign.py", - "ba_data/python/ba/_cloud.py", - "ba_data/python/ba/_collision.py", - "ba_data/python/ba/_coopgame.py", - "ba_data/python/ba/_coopsession.py", - "ba_data/python/ba/_dependency.py", - "ba_data/python/ba/_dualteamsession.py", - "ba_data/python/ba/_error.py", - "ba_data/python/ba/_freeforallsession.py", - "ba_data/python/ba/_gameactivity.py", - "ba_data/python/ba/_gameresults.py", - "ba_data/python/ba/_gameutils.py", - "ba_data/python/ba/_general.py", - "ba_data/python/ba/_generated/__init__.py", - "ba_data/python/ba/_generated/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/ba/_generated/__pycache__/enums.cpython-310.opt-1.pyc", - "ba_data/python/ba/_generated/enums.py", - "ba_data/python/ba/_hooks.py", - "ba_data/python/ba/_input.py", - "ba_data/python/ba/_internal.py", - "ba_data/python/ba/_keyboard.py", - "ba_data/python/ba/_language.py", - "ba_data/python/ba/_level.py", - "ba_data/python/ba/_lobby.py", - "ba_data/python/ba/_login.py", - "ba_data/python/ba/_map.py", - "ba_data/python/ba/_math.py", - "ba_data/python/ba/_messages.py", - "ba_data/python/ba/_meta.py", - "ba_data/python/ba/_multiteamsession.py", - "ba_data/python/ba/_music.py", - "ba_data/python/ba/_net.py", - "ba_data/python/ba/_nodeactor.py", - "ba_data/python/ba/_player.py", - "ba_data/python/ba/_playlist.py", - "ba_data/python/ba/_plugin.py", - "ba_data/python/ba/_powerup.py", - "ba_data/python/ba/_profile.py", - "ba_data/python/ba/_score.py", - "ba_data/python/ba/_servermode.py", - "ba_data/python/ba/_session.py", - "ba_data/python/ba/_settings.py", - "ba_data/python/ba/_stats.py", - "ba_data/python/ba/_store.py", - "ba_data/python/ba/_team.py", - "ba_data/python/ba/_teamgame.py", - "ba_data/python/ba/_tips.py", - "ba_data/python/ba/_tournament.py", - "ba_data/python/ba/_ui.py", - "ba_data/python/ba/_workspace.py", - "ba_data/python/ba/internal.py", - "ba_data/python/ba/macmusicapp.py", - "ba_data/python/ba/modutils.py", - "ba_data/python/ba/osmusic.py", - "ba_data/python/ba/ui/__init__.py", - "ba_data/python/ba/ui/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bacommon/__init__.py", - "ba_data/python/bacommon/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bacommon/__pycache__/assets.cpython-310.opt-1.pyc", - "ba_data/python/bacommon/__pycache__/bacloud.cpython-310.opt-1.pyc", - "ba_data/python/bacommon/__pycache__/build.cpython-310.opt-1.pyc", - "ba_data/python/bacommon/__pycache__/cloud.cpython-310.opt-1.pyc", - "ba_data/python/bacommon/__pycache__/login.cpython-310.opt-1.pyc", - "ba_data/python/bacommon/__pycache__/net.cpython-310.opt-1.pyc", - "ba_data/python/bacommon/__pycache__/servermanager.cpython-310.opt-1.pyc", - "ba_data/python/bacommon/__pycache__/transfer.cpython-310.opt-1.pyc", - "ba_data/python/bacommon/assets.py", - "ba_data/python/bacommon/bacloud.py", - "ba_data/python/bacommon/build.py", - "ba_data/python/bacommon/cloud.py", - "ba_data/python/bacommon/login.py", - "ba_data/python/bacommon/net.py", - "ba_data/python/bacommon/servermanager.py", - "ba_data/python/bacommon/transfer.py", - "ba_data/python/bastd/__init__.py", - "ba_data/python/bastd/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/__pycache__/appdelegate.cpython-310.opt-1.pyc", - "ba_data/python/bastd/__pycache__/gameutils.cpython-310.opt-1.pyc", - "ba_data/python/bastd/__pycache__/mainmenu.cpython-310.opt-1.pyc", - "ba_data/python/bastd/__pycache__/maps.cpython-310.opt-1.pyc", - "ba_data/python/bastd/__pycache__/stdmap.cpython-310.opt-1.pyc", - "ba_data/python/bastd/__pycache__/tutorial.cpython-310.opt-1.pyc", - "ba_data/python/bastd/activity/__init__.py", - "ba_data/python/bastd/activity/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/activity/__pycache__/coopjoin.cpython-310.opt-1.pyc", - "ba_data/python/bastd/activity/__pycache__/coopscore.cpython-310.opt-1.pyc", - "ba_data/python/bastd/activity/__pycache__/drawscore.cpython-310.opt-1.pyc", - "ba_data/python/bastd/activity/__pycache__/dualteamscore.cpython-310.opt-1.pyc", - "ba_data/python/bastd/activity/__pycache__/freeforallvictory.cpython-310.opt-1.pyc", - "ba_data/python/bastd/activity/__pycache__/multiteamjoin.cpython-310.opt-1.pyc", - "ba_data/python/bastd/activity/__pycache__/multiteamscore.cpython-310.opt-1.pyc", - "ba_data/python/bastd/activity/__pycache__/multiteamvictory.cpython-310.opt-1.pyc", - "ba_data/python/bastd/activity/coopjoin.py", - "ba_data/python/bastd/activity/coopscore.py", - "ba_data/python/bastd/activity/drawscore.py", - "ba_data/python/bastd/activity/dualteamscore.py", - "ba_data/python/bastd/activity/freeforallvictory.py", - "ba_data/python/bastd/activity/multiteamjoin.py", - "ba_data/python/bastd/activity/multiteamscore.py", - "ba_data/python/bastd/activity/multiteamvictory.py", - "ba_data/python/bastd/actor/__init__.py", - "ba_data/python/bastd/actor/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/background.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/bomb.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/controlsguide.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/flag.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/image.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/onscreencountdown.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/onscreentimer.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/playerspaz.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/popuptext.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/powerupbox.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/respawnicon.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/scoreboard.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/spawner.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/spaz.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/spazappearance.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/spazbot.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/spazfactory.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/text.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/tipstext.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/__pycache__/zoomtext.cpython-310.opt-1.pyc", - "ba_data/python/bastd/actor/background.py", - "ba_data/python/bastd/actor/bomb.py", - "ba_data/python/bastd/actor/controlsguide.py", - "ba_data/python/bastd/actor/flag.py", - "ba_data/python/bastd/actor/image.py", - "ba_data/python/bastd/actor/onscreencountdown.py", - "ba_data/python/bastd/actor/onscreentimer.py", - "ba_data/python/bastd/actor/playerspaz.py", - "ba_data/python/bastd/actor/popuptext.py", - "ba_data/python/bastd/actor/powerupbox.py", - "ba_data/python/bastd/actor/respawnicon.py", - "ba_data/python/bastd/actor/scoreboard.py", - "ba_data/python/bastd/actor/spawner.py", - "ba_data/python/bastd/actor/spaz.py", - "ba_data/python/bastd/actor/spazappearance.py", - "ba_data/python/bastd/actor/spazbot.py", - "ba_data/python/bastd/actor/spazfactory.py", - "ba_data/python/bastd/actor/text.py", - "ba_data/python/bastd/actor/tipstext.py", - "ba_data/python/bastd/actor/zoomtext.py", - "ba_data/python/bastd/appdelegate.py", - "ba_data/python/bastd/game/__init__.py", - "ba_data/python/bastd/game/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/assault.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/capturetheflag.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/chosenone.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/conquest.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/deathmatch.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/easteregghunt.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/elimination.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/football.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/hockey.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/keepaway.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/kingofthehill.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/meteorshower.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/ninjafight.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/onslaught.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/race.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/runaround.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/targetpractice.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/__pycache__/thelaststand.cpython-310.opt-1.pyc", - "ba_data/python/bastd/game/assault.py", - "ba_data/python/bastd/game/capturetheflag.py", - "ba_data/python/bastd/game/chosenone.py", - "ba_data/python/bastd/game/conquest.py", - "ba_data/python/bastd/game/deathmatch.py", - "ba_data/python/bastd/game/easteregghunt.py", - "ba_data/python/bastd/game/elimination.py", - "ba_data/python/bastd/game/football.py", - "ba_data/python/bastd/game/hockey.py", - "ba_data/python/bastd/game/keepaway.py", - "ba_data/python/bastd/game/kingofthehill.py", - "ba_data/python/bastd/game/meteorshower.py", - "ba_data/python/bastd/game/ninjafight.py", - "ba_data/python/bastd/game/onslaught.py", - "ba_data/python/bastd/game/race.py", - "ba_data/python/bastd/game/runaround.py", - "ba_data/python/bastd/game/targetpractice.py", - "ba_data/python/bastd/game/thelaststand.py", - "ba_data/python/bastd/gameutils.py", - "ba_data/python/bastd/keyboard/__init__.py", - "ba_data/python/bastd/keyboard/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/keyboard/__pycache__/englishkeyboard.cpython-310.opt-1.pyc", - "ba_data/python/bastd/keyboard/englishkeyboard.py", - "ba_data/python/bastd/mainmenu.py", - "ba_data/python/bastd/mapdata/__init__.py", - "ba_data/python/bastd/mapdata/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/big_g.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/bridgit.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/courtyard.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/crag_castle.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/doom_shroom.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/football_stadium.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/happy_thoughts.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/hockey_stadium.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/lake_frigid.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/monkey_face.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/rampage.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/roundabout.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/step_right_up.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/the_pad.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/tip_top.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/tower_d.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/__pycache__/zig_zag.cpython-310.opt-1.pyc", - "ba_data/python/bastd/mapdata/big_g.py", - "ba_data/python/bastd/mapdata/bridgit.py", - "ba_data/python/bastd/mapdata/courtyard.py", - "ba_data/python/bastd/mapdata/crag_castle.py", - "ba_data/python/bastd/mapdata/doom_shroom.py", - "ba_data/python/bastd/mapdata/football_stadium.py", - "ba_data/python/bastd/mapdata/happy_thoughts.py", - "ba_data/python/bastd/mapdata/hockey_stadium.py", - "ba_data/python/bastd/mapdata/lake_frigid.py", - "ba_data/python/bastd/mapdata/monkey_face.py", - "ba_data/python/bastd/mapdata/rampage.py", - "ba_data/python/bastd/mapdata/roundabout.py", - "ba_data/python/bastd/mapdata/step_right_up.py", - "ba_data/python/bastd/mapdata/the_pad.py", - "ba_data/python/bastd/mapdata/tip_top.py", - "ba_data/python/bastd/mapdata/tower_d.py", - "ba_data/python/bastd/mapdata/zig_zag.py", - "ba_data/python/bastd/maps.py", - "ba_data/python/bastd/session/__init__.py", - "ba_data/python/bastd/session/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/stdmap.py", - "ba_data/python/bastd/tutorial.py", - "ba_data/python/bastd/ui/__init__.py", - "ba_data/python/bastd/ui/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/achievements.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/appinvite.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/characterpicker.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/colorpicker.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/config.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/configerror.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/confirm.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/continues.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/creditslist.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/debug.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/feedback.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/fileselector.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/getcurrency.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/getremote.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/helpui.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/iconpicker.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/kiosk.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/mainmenu.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/onscreenkeyboard.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/party.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/partyqueue.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/play.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/playoptions.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/popup.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/promocode.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/purchase.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/qrcode.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/radiogroup.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/report.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/resourcetypeinfo.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/serverdialog.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/specialoffer.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/tabs.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/teamnamescolors.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/telnet.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/tournamententry.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/tournamentscores.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/trophies.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/url.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/v2upgrade.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/__pycache__/watch.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/account/__init__.py", - "ba_data/python/bastd/ui/account/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/account/__pycache__/link.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/account/__pycache__/settings.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/account/__pycache__/unlink.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/account/__pycache__/v2proxy.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/account/__pycache__/viewer.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/account/link.py", - "ba_data/python/bastd/ui/account/settings.py", - "ba_data/python/bastd/ui/account/unlink.py", - "ba_data/python/bastd/ui/account/v2proxy.py", - "ba_data/python/bastd/ui/account/viewer.py", - "ba_data/python/bastd/ui/achievements.py", - "ba_data/python/bastd/ui/appinvite.py", - "ba_data/python/bastd/ui/characterpicker.py", - "ba_data/python/bastd/ui/colorpicker.py", - "ba_data/python/bastd/ui/config.py", - "ba_data/python/bastd/ui/configerror.py", - "ba_data/python/bastd/ui/confirm.py", - "ba_data/python/bastd/ui/continues.py", - "ba_data/python/bastd/ui/coop/__init__.py", - "ba_data/python/bastd/ui/coop/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/coop/__pycache__/browser.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/coop/__pycache__/gamebutton.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/coop/__pycache__/level.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/coop/__pycache__/tournamentbutton.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/coop/browser.py", - "ba_data/python/bastd/ui/coop/gamebutton.py", - "ba_data/python/bastd/ui/coop/level.py", - "ba_data/python/bastd/ui/coop/tournamentbutton.py", - "ba_data/python/bastd/ui/creditslist.py", - "ba_data/python/bastd/ui/debug.py", - "ba_data/python/bastd/ui/feedback.py", - "ba_data/python/bastd/ui/fileselector.py", - "ba_data/python/bastd/ui/gather/__init__.py", - "ba_data/python/bastd/ui/gather/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/gather/__pycache__/abouttab.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/gather/__pycache__/manualtab.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/gather/__pycache__/nearbytab.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/gather/__pycache__/privatetab.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/gather/__pycache__/publictab.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/gather/abouttab.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", - "ba_data/python/bastd/ui/helpui.py", - "ba_data/python/bastd/ui/iconpicker.py", - "ba_data/python/bastd/ui/kiosk.py", - "ba_data/python/bastd/ui/league/__init__.py", - "ba_data/python/bastd/ui/league/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/league/__pycache__/rankbutton.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/league/__pycache__/rankwindow.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/league/rankbutton.py", - "ba_data/python/bastd/ui/league/rankwindow.py", - "ba_data/python/bastd/ui/mainmenu.py", - "ba_data/python/bastd/ui/onscreenkeyboard.py", - "ba_data/python/bastd/ui/party.py", - "ba_data/python/bastd/ui/partyqueue.py", - "ba_data/python/bastd/ui/play.py", - "ba_data/python/bastd/ui/playlist/__init__.py", - "ba_data/python/bastd/ui/playlist/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/playlist/__pycache__/addgame.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/playlist/__pycache__/browser.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/playlist/__pycache__/customizebrowser.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/playlist/__pycache__/edit.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/playlist/__pycache__/editcontroller.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/playlist/__pycache__/editgame.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/playlist/__pycache__/mapselect.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/playlist/__pycache__/share.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/playlist/addgame.py", - "ba_data/python/bastd/ui/playlist/browser.py", - "ba_data/python/bastd/ui/playlist/customizebrowser.py", - "ba_data/python/bastd/ui/playlist/edit.py", - "ba_data/python/bastd/ui/playlist/editcontroller.py", - "ba_data/python/bastd/ui/playlist/editgame.py", - "ba_data/python/bastd/ui/playlist/mapselect.py", - "ba_data/python/bastd/ui/playlist/share.py", - "ba_data/python/bastd/ui/playoptions.py", - "ba_data/python/bastd/ui/popup.py", - "ba_data/python/bastd/ui/profile/__init__.py", - "ba_data/python/bastd/ui/profile/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/profile/__pycache__/browser.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/profile/__pycache__/edit.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/profile/__pycache__/upgrade.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/profile/browser.py", - "ba_data/python/bastd/ui/profile/edit.py", - "ba_data/python/bastd/ui/profile/upgrade.py", - "ba_data/python/bastd/ui/promocode.py", - "ba_data/python/bastd/ui/purchase.py", - "ba_data/python/bastd/ui/qrcode.py", - "ba_data/python/bastd/ui/radiogroup.py", - "ba_data/python/bastd/ui/report.py", - "ba_data/python/bastd/ui/resourcetypeinfo.py", - "ba_data/python/bastd/ui/serverdialog.py", - "ba_data/python/bastd/ui/settings/__init__.py", - "ba_data/python/bastd/ui/settings/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/advanced.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/allsettings.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/audio.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/controls.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/gamepad.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/gamepadadvanced.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/gamepadselect.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/graphics.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/keyboard.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/nettesting.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/plugins.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/pluginsettings.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/remoteapp.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/testing.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/touchscreen.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/vrtesting.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/__pycache__/xbox360controller.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/settings/advanced.py", - "ba_data/python/bastd/ui/settings/allsettings.py", - "ba_data/python/bastd/ui/settings/audio.py", - "ba_data/python/bastd/ui/settings/controls.py", - "ba_data/python/bastd/ui/settings/gamepad.py", - "ba_data/python/bastd/ui/settings/gamepadadvanced.py", - "ba_data/python/bastd/ui/settings/gamepadselect.py", - "ba_data/python/bastd/ui/settings/graphics.py", - "ba_data/python/bastd/ui/settings/keyboard.py", - "ba_data/python/bastd/ui/settings/nettesting.py", - "ba_data/python/bastd/ui/settings/plugins.py", - "ba_data/python/bastd/ui/settings/pluginsettings.py", - "ba_data/python/bastd/ui/settings/remoteapp.py", - "ba_data/python/bastd/ui/settings/testing.py", - "ba_data/python/bastd/ui/settings/touchscreen.py", - "ba_data/python/bastd/ui/settings/vrtesting.py", - "ba_data/python/bastd/ui/settings/xbox360controller.py", - "ba_data/python/bastd/ui/soundtrack/__init__.py", - "ba_data/python/bastd/ui/soundtrack/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/soundtrack/__pycache__/browser.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/soundtrack/__pycache__/edit.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/soundtrack/__pycache__/entrytypeselect.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/soundtrack/__pycache__/macmusicapp.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/soundtrack/browser.py", - "ba_data/python/bastd/ui/soundtrack/edit.py", - "ba_data/python/bastd/ui/soundtrack/entrytypeselect.py", - "ba_data/python/bastd/ui/soundtrack/macmusicapp.py", - "ba_data/python/bastd/ui/specialoffer.py", - "ba_data/python/bastd/ui/store/__init__.py", - "ba_data/python/bastd/ui/store/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/store/__pycache__/browser.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/store/__pycache__/button.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/store/__pycache__/item.cpython-310.opt-1.pyc", - "ba_data/python/bastd/ui/store/browser.py", - "ba_data/python/bastd/ui/store/button.py", - "ba_data/python/bastd/ui/store/item.py", - "ba_data/python/bastd/ui/tabs.py", - "ba_data/python/bastd/ui/teamnamescolors.py", - "ba_data/python/bastd/ui/telnet.py", - "ba_data/python/bastd/ui/tournamententry.py", - "ba_data/python/bastd/ui/tournamentscores.py", - "ba_data/python/bastd/ui/trophies.py", - "ba_data/python/bastd/ui/url.py", - "ba_data/python/bastd/ui/v2upgrade.py", - "ba_data/python/bastd/ui/watch.py", - "ba_data/python/efro/__init__.py", - "ba_data/python/efro/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/efro/__pycache__/call.cpython-310.opt-1.pyc", - "ba_data/python/efro/__pycache__/cloudshell.cpython-310.opt-1.pyc", - "ba_data/python/efro/__pycache__/debug.cpython-310.opt-1.pyc", - "ba_data/python/efro/__pycache__/error.cpython-310.opt-1.pyc", - "ba_data/python/efro/__pycache__/log.cpython-310.opt-1.pyc", - "ba_data/python/efro/__pycache__/rpc.cpython-310.opt-1.pyc", - "ba_data/python/efro/__pycache__/terminal.cpython-310.opt-1.pyc", - "ba_data/python/efro/__pycache__/util.cpython-310.opt-1.pyc", - "ba_data/python/efro/call.py", - "ba_data/python/efro/cloudshell.py", - "ba_data/python/efro/dataclassio/__init__.py", - "ba_data/python/efro/dataclassio/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/efro/dataclassio/__pycache__/_api.cpython-310.opt-1.pyc", - "ba_data/python/efro/dataclassio/__pycache__/_base.cpython-310.opt-1.pyc", - "ba_data/python/efro/dataclassio/__pycache__/_inputter.cpython-310.opt-1.pyc", - "ba_data/python/efro/dataclassio/__pycache__/_outputter.cpython-310.opt-1.pyc", - "ba_data/python/efro/dataclassio/__pycache__/_pathcapture.cpython-310.opt-1.pyc", - "ba_data/python/efro/dataclassio/__pycache__/_prep.cpython-310.opt-1.pyc", - "ba_data/python/efro/dataclassio/__pycache__/extras.cpython-310.opt-1.pyc", - "ba_data/python/efro/dataclassio/_api.py", - "ba_data/python/efro/dataclassio/_base.py", - "ba_data/python/efro/dataclassio/_inputter.py", - "ba_data/python/efro/dataclassio/_outputter.py", - "ba_data/python/efro/dataclassio/_pathcapture.py", - "ba_data/python/efro/dataclassio/_prep.py", - "ba_data/python/efro/dataclassio/extras.py", - "ba_data/python/efro/debug.py", - "ba_data/python/efro/error.py", - "ba_data/python/efro/log.py", - "ba_data/python/efro/message/__init__.py", - "ba_data/python/efro/message/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python/efro/message/__pycache__/_message.cpython-310.opt-1.pyc", - "ba_data/python/efro/message/__pycache__/_module.cpython-310.opt-1.pyc", - "ba_data/python/efro/message/__pycache__/_protocol.cpython-310.opt-1.pyc", - "ba_data/python/efro/message/__pycache__/_receiver.cpython-310.opt-1.pyc", - "ba_data/python/efro/message/__pycache__/_sender.cpython-310.opt-1.pyc", - "ba_data/python/efro/message/_message.py", - "ba_data/python/efro/message/_module.py", - "ba_data/python/efro/message/_protocol.py", - "ba_data/python/efro/message/_receiver.py", - "ba_data/python/efro/message/_sender.py", - "ba_data/python/efro/rpc.py", - "ba_data/python/efro/terminal.py", - "ba_data/python/efro/util.py", - "server/__pycache__/ballisticacore_server.cpython-310.opt-1.pyc", - "server/ballisticacore_server.py" -] \ No newline at end of file diff --git a/assets/Makefile b/assets/Makefile deleted file mode 100644 index 019d13e5..00000000 --- a/assets/Makefile +++ /dev/null @@ -1,7328 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -################################################################################ -# # -# Asset Generation # -# # -################################################################################ - -# Generally this Makefile should not be modified by hand. To add or remove -# assets, add or remove files under the assets/src directory and then run -# "make update" from the project root. That will update this file as well as -# other relevant files such as assets/manifest.json - -# Generally you shouldn't need to use this Makefile directly. Targets in the -# root level Makefile should handle asset building for you. If you do use -# this Makefile directly for whatever reason, remember to pass -jX on the -# command line to speed up your builds. (with X being the number of procs -# in your machine) - -TOOLS_DIR = ../tools -PROJ_DIR = .. - -# High level targets: generally these are what should be used here. - -# Build everything needed for all platforms. -all: - @${TOOLS_DIR}/pcommand warm_start_asset_build - @$(MAKE) assets - @${TOOLS_DIR}/pcommand clean_orphaned_assets - -# Build everything needed for our cmake builds (linux, mac). -cmake: - @${TOOLS_DIR}/pcommand warm_start_asset_build - @$(MAKE) assets-cmake - @${TOOLS_DIR}/pcommand clean_orphaned_assets - -# Build everything needed for x86 windows builds. -win-Win32: - @${TOOLS_DIR}/pcommand warm_start_asset_build - @$(MAKE) assets-win-Win32 - @${TOOLS_DIR}/pcommand clean_orphaned_assets - -# Build everything needed for x86-64 windows builds. -win-x64: - @${TOOLS_DIR}/pcommand warm_start_asset_build - @$(MAKE) assets-win-x64 - @${TOOLS_DIR}/pcommand clean_orphaned_assets - -# Build everything needed for our mac xcode builds. -mac: - @${TOOLS_DIR}/pcommand warm_start_asset_build - @$(MAKE) assets-mac - @${TOOLS_DIR}/pcommand clean_orphaned_assets - -# Build everything needed for our ios/tvos builds. -ios: - @${TOOLS_DIR}/pcommand warm_start_asset_build - @$(MAKE) assets-ios - @${TOOLS_DIR}/pcommand clean_orphaned_assets - -# Build everything needed for android. -android: - @${TOOLS_DIR}/pcommand warm_start_asset_build - @$(MAKE) assets-android - @${TOOLS_DIR}/pcommand clean_orphaned_assets - -MAKE_AUDIO = 1 -MAKE_TEXTURES = 1 -MAKE_SCRIPTS = 1 -MAKE_FONTS = 1 -MAKE_DATA = 1 - -ASSET_TARGETS_COMMON = -ASSET_TARGETS_CMAKE = -ASSET_TARGETS_MAC = -ASSET_TARGETS_WIN_WIN32 = -ASSET_TARGETS_WIN_X64 = -ASSET_TARGETS_IOS = -ASSET_TARGETS_ANDROID = - -# Audio. -ifeq ($(MAKE_AUDIO),1) -ASSET_TARGETS_COMMON += $(AUDIO_TARGETS) -endif - -# FontData. -ifeq ($(MAKE_FONTS),1) -ASSET_TARGETS_COMMON += $(FONT_TARGETS) -endif - -# Data. -ifeq ($(MAKE_DATA),1) -ASSET_TARGETS_COMMON += $(DATA_TARGETS) -endif - -# Textures. -ifeq ($(MAKE_TEXTURES),1) - -ASSET_TARGETS_CMAKE += $(TEXTURE_TARGETS_CMAKE) -ASSET_TARGETS_MAC += $(TEXTURE_TARGETS_MAC) -ASSET_TARGETS_WIN_WIN32 += $(TEXTURE_TARGETS_WIN) -ASSET_TARGETS_WIN_X64 += $(TEXTURE_TARGETS_WIN) -ASSET_TARGETS_IOS += $(TEXTURE_TARGETS_IOS) -ASSET_TARGETS_ANDROID += $(TEXTURE_TARGETS_ANDROID) -ASSET_TARGETS_COMMON += $(TEXTURE_TARGETS_COMMON) - -endif # Textures - -# Scripts -ifeq ($(MAKE_SCRIPTS),1) -ASSET_TARGETS_CMAKE += $(SCRIPT_TARGETS_CMAKE) -ASSET_TARGETS_MAC += $(SCRIPT_TARGETS_MAC) -ASSET_TARGETS_WIN_WIN32 += $(SCRIPT_TARGETS_WIN_WIN32) -ASSET_TARGETS_WIN_X64 += $(SCRIPT_TARGETS_WIN_X64) -ASSET_TARGETS_IOS += $(SCRIPT_TARGETS_IOS) -ASSET_TARGETS_ANDROID += $(SCRIPT_TARGETS_ANDROID) -ASSET_TARGETS_COMMON += $(SCRIPT_TARGETS_COMMON) -endif - -# Extras -ASSET_TARGETS_WIN_WIN32 += $(EXTRAS_TARGETS_WIN_WIN32) -ASSET_TARGETS_WIN_X64 += $(EXTRAS_TARGETS_WIN_X64) - -# Note: Code below needs updating when Python version changes (currently 3.10) -define make-opt-pyc-target -$1: $$(subst /__pycache__,,$$(subst .cpython-310.opt-1.pyc,.py,$1)) - @echo Compiling script: $$^ - @rm -rf $$@ && PYTHONHASHSEED=1 \ - $$(TOOLS_DIR)/pcommand compile_python_files $$^ && chmod 444 $$@ -endef - -# This section is generated by batools.assetsmakefile; do not edit by hand. -# __AUTOGENERATED_PUBLIC_BEGIN__ - -SCRIPT_TARGETS_PY_PUBLIC = \ - build/ba_data/python/_bainternal.py \ - build/ba_data/python/ba/__init__.py \ - build/ba_data/python/ba/_accountv1.py \ - build/ba_data/python/ba/_accountv2.py \ - build/ba_data/python/ba/_achievement.py \ - build/ba_data/python/ba/_activity.py \ - build/ba_data/python/ba/_activitytypes.py \ - build/ba_data/python/ba/_actor.py \ - build/ba_data/python/ba/_ads.py \ - build/ba_data/python/ba/_analytics.py \ - build/ba_data/python/ba/_app.py \ - build/ba_data/python/ba/_appcomponent.py \ - build/ba_data/python/ba/_appconfig.py \ - build/ba_data/python/ba/_appdelegate.py \ - build/ba_data/python/ba/_appmode.py \ - build/ba_data/python/ba/_apputils.py \ - build/ba_data/python/ba/_assetmanager.py \ - build/ba_data/python/ba/_asyncio.py \ - build/ba_data/python/ba/_benchmark.py \ - build/ba_data/python/ba/_bootstrap.py \ - build/ba_data/python/ba/_campaign.py \ - build/ba_data/python/ba/_cloud.py \ - build/ba_data/python/ba/_collision.py \ - build/ba_data/python/ba/_coopgame.py \ - build/ba_data/python/ba/_coopsession.py \ - build/ba_data/python/ba/_dependency.py \ - build/ba_data/python/ba/_dualteamsession.py \ - build/ba_data/python/ba/_error.py \ - build/ba_data/python/ba/_freeforallsession.py \ - build/ba_data/python/ba/_gameactivity.py \ - build/ba_data/python/ba/_gameresults.py \ - build/ba_data/python/ba/_gameutils.py \ - build/ba_data/python/ba/_general.py \ - build/ba_data/python/ba/_generated/__init__.py \ - build/ba_data/python/ba/_generated/enums.py \ - build/ba_data/python/ba/_hooks.py \ - build/ba_data/python/ba/_input.py \ - build/ba_data/python/ba/_internal.py \ - build/ba_data/python/ba/_keyboard.py \ - build/ba_data/python/ba/_language.py \ - build/ba_data/python/ba/_level.py \ - build/ba_data/python/ba/_lobby.py \ - build/ba_data/python/ba/_login.py \ - build/ba_data/python/ba/_map.py \ - build/ba_data/python/ba/_math.py \ - build/ba_data/python/ba/_messages.py \ - build/ba_data/python/ba/_meta.py \ - build/ba_data/python/ba/_multiteamsession.py \ - build/ba_data/python/ba/_music.py \ - build/ba_data/python/ba/_net.py \ - build/ba_data/python/ba/_nodeactor.py \ - build/ba_data/python/ba/_player.py \ - build/ba_data/python/ba/_playlist.py \ - build/ba_data/python/ba/_plugin.py \ - build/ba_data/python/ba/_powerup.py \ - build/ba_data/python/ba/_profile.py \ - build/ba_data/python/ba/_score.py \ - build/ba_data/python/ba/_servermode.py \ - build/ba_data/python/ba/_session.py \ - build/ba_data/python/ba/_settings.py \ - build/ba_data/python/ba/_stats.py \ - build/ba_data/python/ba/_store.py \ - build/ba_data/python/ba/_team.py \ - build/ba_data/python/ba/_teamgame.py \ - build/ba_data/python/ba/_tips.py \ - build/ba_data/python/ba/_tournament.py \ - build/ba_data/python/ba/_ui.py \ - build/ba_data/python/ba/_workspace.py \ - build/ba_data/python/ba/internal.py \ - build/ba_data/python/ba/macmusicapp.py \ - build/ba_data/python/ba/modutils.py \ - build/ba_data/python/ba/osmusic.py \ - build/ba_data/python/ba/ui/__init__.py \ - build/ba_data/python/bastd/__init__.py \ - build/ba_data/python/bastd/activity/__init__.py \ - build/ba_data/python/bastd/activity/coopjoin.py \ - build/ba_data/python/bastd/activity/coopscore.py \ - build/ba_data/python/bastd/activity/drawscore.py \ - build/ba_data/python/bastd/activity/dualteamscore.py \ - build/ba_data/python/bastd/activity/freeforallvictory.py \ - build/ba_data/python/bastd/activity/multiteamjoin.py \ - build/ba_data/python/bastd/activity/multiteamscore.py \ - build/ba_data/python/bastd/activity/multiteamvictory.py \ - build/ba_data/python/bastd/actor/__init__.py \ - build/ba_data/python/bastd/actor/background.py \ - build/ba_data/python/bastd/actor/bomb.py \ - build/ba_data/python/bastd/actor/controlsguide.py \ - build/ba_data/python/bastd/actor/flag.py \ - build/ba_data/python/bastd/actor/image.py \ - build/ba_data/python/bastd/actor/onscreencountdown.py \ - build/ba_data/python/bastd/actor/onscreentimer.py \ - build/ba_data/python/bastd/actor/playerspaz.py \ - build/ba_data/python/bastd/actor/popuptext.py \ - build/ba_data/python/bastd/actor/powerupbox.py \ - build/ba_data/python/bastd/actor/respawnicon.py \ - build/ba_data/python/bastd/actor/scoreboard.py \ - build/ba_data/python/bastd/actor/spawner.py \ - build/ba_data/python/bastd/actor/spaz.py \ - build/ba_data/python/bastd/actor/spazappearance.py \ - build/ba_data/python/bastd/actor/spazbot.py \ - build/ba_data/python/bastd/actor/spazfactory.py \ - build/ba_data/python/bastd/actor/text.py \ - build/ba_data/python/bastd/actor/tipstext.py \ - build/ba_data/python/bastd/actor/zoomtext.py \ - build/ba_data/python/bastd/appdelegate.py \ - build/ba_data/python/bastd/game/__init__.py \ - build/ba_data/python/bastd/game/assault.py \ - build/ba_data/python/bastd/game/capturetheflag.py \ - build/ba_data/python/bastd/game/chosenone.py \ - build/ba_data/python/bastd/game/conquest.py \ - build/ba_data/python/bastd/game/deathmatch.py \ - build/ba_data/python/bastd/game/easteregghunt.py \ - build/ba_data/python/bastd/game/elimination.py \ - build/ba_data/python/bastd/game/football.py \ - build/ba_data/python/bastd/game/hockey.py \ - build/ba_data/python/bastd/game/keepaway.py \ - build/ba_data/python/bastd/game/kingofthehill.py \ - build/ba_data/python/bastd/game/meteorshower.py \ - build/ba_data/python/bastd/game/ninjafight.py \ - build/ba_data/python/bastd/game/onslaught.py \ - build/ba_data/python/bastd/game/race.py \ - build/ba_data/python/bastd/game/runaround.py \ - build/ba_data/python/bastd/game/targetpractice.py \ - build/ba_data/python/bastd/game/thelaststand.py \ - build/ba_data/python/bastd/gameutils.py \ - build/ba_data/python/bastd/keyboard/__init__.py \ - build/ba_data/python/bastd/keyboard/englishkeyboard.py \ - build/ba_data/python/bastd/mainmenu.py \ - build/ba_data/python/bastd/mapdata/__init__.py \ - build/ba_data/python/bastd/mapdata/big_g.py \ - build/ba_data/python/bastd/mapdata/bridgit.py \ - build/ba_data/python/bastd/mapdata/courtyard.py \ - build/ba_data/python/bastd/mapdata/crag_castle.py \ - build/ba_data/python/bastd/mapdata/doom_shroom.py \ - build/ba_data/python/bastd/mapdata/football_stadium.py \ - build/ba_data/python/bastd/mapdata/happy_thoughts.py \ - build/ba_data/python/bastd/mapdata/hockey_stadium.py \ - build/ba_data/python/bastd/mapdata/lake_frigid.py \ - build/ba_data/python/bastd/mapdata/monkey_face.py \ - build/ba_data/python/bastd/mapdata/rampage.py \ - build/ba_data/python/bastd/mapdata/roundabout.py \ - build/ba_data/python/bastd/mapdata/step_right_up.py \ - build/ba_data/python/bastd/mapdata/the_pad.py \ - build/ba_data/python/bastd/mapdata/tip_top.py \ - build/ba_data/python/bastd/mapdata/tower_d.py \ - build/ba_data/python/bastd/mapdata/zig_zag.py \ - build/ba_data/python/bastd/maps.py \ - build/ba_data/python/bastd/session/__init__.py \ - build/ba_data/python/bastd/stdmap.py \ - build/ba_data/python/bastd/tutorial.py \ - build/ba_data/python/bastd/ui/__init__.py \ - build/ba_data/python/bastd/ui/account/__init__.py \ - build/ba_data/python/bastd/ui/account/link.py \ - build/ba_data/python/bastd/ui/account/settings.py \ - build/ba_data/python/bastd/ui/account/unlink.py \ - build/ba_data/python/bastd/ui/account/v2proxy.py \ - build/ba_data/python/bastd/ui/account/viewer.py \ - build/ba_data/python/bastd/ui/achievements.py \ - build/ba_data/python/bastd/ui/appinvite.py \ - build/ba_data/python/bastd/ui/characterpicker.py \ - build/ba_data/python/bastd/ui/colorpicker.py \ - build/ba_data/python/bastd/ui/config.py \ - build/ba_data/python/bastd/ui/configerror.py \ - build/ba_data/python/bastd/ui/confirm.py \ - build/ba_data/python/bastd/ui/continues.py \ - build/ba_data/python/bastd/ui/coop/__init__.py \ - build/ba_data/python/bastd/ui/coop/browser.py \ - build/ba_data/python/bastd/ui/coop/gamebutton.py \ - build/ba_data/python/bastd/ui/coop/level.py \ - build/ba_data/python/bastd/ui/coop/tournamentbutton.py \ - build/ba_data/python/bastd/ui/creditslist.py \ - build/ba_data/python/bastd/ui/debug.py \ - build/ba_data/python/bastd/ui/feedback.py \ - 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/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 \ - build/ba_data/python/bastd/ui/helpui.py \ - build/ba_data/python/bastd/ui/iconpicker.py \ - build/ba_data/python/bastd/ui/kiosk.py \ - build/ba_data/python/bastd/ui/league/__init__.py \ - build/ba_data/python/bastd/ui/league/rankbutton.py \ - build/ba_data/python/bastd/ui/league/rankwindow.py \ - build/ba_data/python/bastd/ui/mainmenu.py \ - build/ba_data/python/bastd/ui/onscreenkeyboard.py \ - build/ba_data/python/bastd/ui/party.py \ - build/ba_data/python/bastd/ui/partyqueue.py \ - build/ba_data/python/bastd/ui/play.py \ - build/ba_data/python/bastd/ui/playlist/__init__.py \ - build/ba_data/python/bastd/ui/playlist/addgame.py \ - build/ba_data/python/bastd/ui/playlist/browser.py \ - build/ba_data/python/bastd/ui/playlist/customizebrowser.py \ - build/ba_data/python/bastd/ui/playlist/edit.py \ - build/ba_data/python/bastd/ui/playlist/editcontroller.py \ - build/ba_data/python/bastd/ui/playlist/editgame.py \ - build/ba_data/python/bastd/ui/playlist/mapselect.py \ - build/ba_data/python/bastd/ui/playlist/share.py \ - build/ba_data/python/bastd/ui/playoptions.py \ - build/ba_data/python/bastd/ui/popup.py \ - build/ba_data/python/bastd/ui/profile/__init__.py \ - build/ba_data/python/bastd/ui/profile/browser.py \ - build/ba_data/python/bastd/ui/profile/edit.py \ - build/ba_data/python/bastd/ui/profile/upgrade.py \ - build/ba_data/python/bastd/ui/promocode.py \ - build/ba_data/python/bastd/ui/purchase.py \ - build/ba_data/python/bastd/ui/qrcode.py \ - build/ba_data/python/bastd/ui/radiogroup.py \ - build/ba_data/python/bastd/ui/report.py \ - build/ba_data/python/bastd/ui/resourcetypeinfo.py \ - build/ba_data/python/bastd/ui/serverdialog.py \ - build/ba_data/python/bastd/ui/settings/__init__.py \ - build/ba_data/python/bastd/ui/settings/advanced.py \ - build/ba_data/python/bastd/ui/settings/allsettings.py \ - build/ba_data/python/bastd/ui/settings/audio.py \ - build/ba_data/python/bastd/ui/settings/controls.py \ - build/ba_data/python/bastd/ui/settings/gamepad.py \ - build/ba_data/python/bastd/ui/settings/gamepadadvanced.py \ - build/ba_data/python/bastd/ui/settings/gamepadselect.py \ - build/ba_data/python/bastd/ui/settings/graphics.py \ - build/ba_data/python/bastd/ui/settings/keyboard.py \ - build/ba_data/python/bastd/ui/settings/nettesting.py \ - build/ba_data/python/bastd/ui/settings/plugins.py \ - build/ba_data/python/bastd/ui/settings/pluginsettings.py \ - build/ba_data/python/bastd/ui/settings/remoteapp.py \ - build/ba_data/python/bastd/ui/settings/testing.py \ - build/ba_data/python/bastd/ui/settings/touchscreen.py \ - build/ba_data/python/bastd/ui/settings/vrtesting.py \ - build/ba_data/python/bastd/ui/settings/xbox360controller.py \ - build/ba_data/python/bastd/ui/soundtrack/__init__.py \ - build/ba_data/python/bastd/ui/soundtrack/browser.py \ - build/ba_data/python/bastd/ui/soundtrack/edit.py \ - build/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py \ - build/ba_data/python/bastd/ui/soundtrack/macmusicapp.py \ - build/ba_data/python/bastd/ui/specialoffer.py \ - build/ba_data/python/bastd/ui/store/__init__.py \ - build/ba_data/python/bastd/ui/store/browser.py \ - build/ba_data/python/bastd/ui/store/button.py \ - build/ba_data/python/bastd/ui/store/item.py \ - build/ba_data/python/bastd/ui/tabs.py \ - build/ba_data/python/bastd/ui/teamnamescolors.py \ - build/ba_data/python/bastd/ui/telnet.py \ - build/ba_data/python/bastd/ui/tournamententry.py \ - build/ba_data/python/bastd/ui/tournamentscores.py \ - build/ba_data/python/bastd/ui/trophies.py \ - build/ba_data/python/bastd/ui/url.py \ - build/ba_data/python/bastd/ui/v2upgrade.py \ - build/ba_data/python/bastd/ui/watch.py \ - build/server/ballisticacore_server.py - -SCRIPT_TARGETS_PYC_PUBLIC = \ - build/ba_data/python/__pycache__/_bainternal.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_accountv1.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_accountv2.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_achievement.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_activity.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_activitytypes.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_actor.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_ads.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_analytics.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_app.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_appcomponent.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_appconfig.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_appdelegate.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_appmode.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_apputils.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_assetmanager.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_asyncio.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_benchmark.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_bootstrap.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_campaign.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_cloud.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_collision.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_coopgame.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_coopsession.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_dependency.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_dualteamsession.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_error.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_freeforallsession.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_gameactivity.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_gameresults.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_gameutils.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_general.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/_generated/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/_generated/__pycache__/enums.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_hooks.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_input.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_internal.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_keyboard.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_language.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_level.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_lobby.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_login.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_map.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_math.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_messages.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_meta.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_multiteamsession.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_music.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_net.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_nodeactor.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_player.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_playlist.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_plugin.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_powerup.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_profile.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_score.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_servermode.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_session.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_settings.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_stats.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_store.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_team.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_teamgame.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_tips.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_tournament.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_ui.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/_workspace.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/internal.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/macmusicapp.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/modutils.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/__pycache__/osmusic.cpython-310.opt-1.pyc \ - build/ba_data/python/ba/ui/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/activity/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/activity/__pycache__/coopjoin.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/activity/__pycache__/coopscore.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/activity/__pycache__/drawscore.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/activity/__pycache__/dualteamscore.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/activity/__pycache__/freeforallvictory.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/activity/__pycache__/multiteamjoin.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/activity/__pycache__/multiteamscore.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/activity/__pycache__/multiteamvictory.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/background.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/bomb.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/controlsguide.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/flag.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/image.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/onscreencountdown.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/onscreentimer.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/playerspaz.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/popuptext.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/powerupbox.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/respawnicon.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/scoreboard.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/spawner.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/spaz.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/spazappearance.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/spazbot.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/spazfactory.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/text.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/tipstext.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/actor/__pycache__/zoomtext.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/__pycache__/appdelegate.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/assault.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/capturetheflag.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/chosenone.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/conquest.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/deathmatch.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/easteregghunt.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/elimination.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/football.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/hockey.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/keepaway.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/kingofthehill.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/meteorshower.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/ninjafight.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/onslaught.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/race.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/runaround.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/targetpractice.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/game/__pycache__/thelaststand.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/__pycache__/gameutils.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/keyboard/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/keyboard/__pycache__/englishkeyboard.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/__pycache__/mainmenu.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/big_g.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/bridgit.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/courtyard.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/crag_castle.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/doom_shroom.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/football_stadium.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/happy_thoughts.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/hockey_stadium.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/lake_frigid.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/monkey_face.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/rampage.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/roundabout.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/step_right_up.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/the_pad.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/tip_top.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/tower_d.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/mapdata/__pycache__/zig_zag.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/__pycache__/maps.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/session/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/__pycache__/stdmap.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/__pycache__/tutorial.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/account/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/account/__pycache__/link.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/account/__pycache__/settings.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/account/__pycache__/unlink.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/account/__pycache__/v2proxy.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/account/__pycache__/viewer.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/achievements.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/appinvite.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/characterpicker.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/colorpicker.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/config.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/configerror.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/confirm.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/continues.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/coop/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/coop/__pycache__/browser.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/coop/__pycache__/gamebutton.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/coop/__pycache__/level.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/coop/__pycache__/tournamentbutton.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/creditslist.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/debug.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/feedback.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/fileselector.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/gather/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/gather/__pycache__/abouttab.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/gather/__pycache__/manualtab.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/gather/__pycache__/nearbytab.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/gather/__pycache__/privatetab.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/gather/__pycache__/publictab.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/getcurrency.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/getremote.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/helpui.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/iconpicker.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/kiosk.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/league/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/league/__pycache__/rankbutton.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/league/__pycache__/rankwindow.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/mainmenu.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/onscreenkeyboard.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/party.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/partyqueue.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/play.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/playlist/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/playlist/__pycache__/addgame.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/playlist/__pycache__/browser.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/playlist/__pycache__/customizebrowser.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/playlist/__pycache__/edit.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/playlist/__pycache__/editcontroller.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/playlist/__pycache__/editgame.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/playlist/__pycache__/mapselect.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/playlist/__pycache__/share.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/playoptions.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/popup.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/profile/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/profile/__pycache__/browser.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/profile/__pycache__/edit.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/profile/__pycache__/upgrade.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/promocode.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/purchase.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/qrcode.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/radiogroup.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/report.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/resourcetypeinfo.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/serverdialog.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/advanced.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/allsettings.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/audio.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/controls.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/gamepad.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/gamepadadvanced.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/gamepadselect.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/graphics.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/keyboard.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/nettesting.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/plugins.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/pluginsettings.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/remoteapp.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/testing.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/touchscreen.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/vrtesting.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/settings/__pycache__/xbox360controller.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/soundtrack/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/soundtrack/__pycache__/browser.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/soundtrack/__pycache__/edit.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/soundtrack/__pycache__/entrytypeselect.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/soundtrack/__pycache__/macmusicapp.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/specialoffer.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/store/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/store/__pycache__/browser.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/store/__pycache__/button.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/store/__pycache__/item.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/tabs.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/teamnamescolors.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/telnet.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/tournamententry.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/tournamentscores.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/trophies.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/url.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/v2upgrade.cpython-310.opt-1.pyc \ - build/ba_data/python/bastd/ui/__pycache__/watch.cpython-310.opt-1.pyc \ - build/server/__pycache__/ballisticacore_server.cpython-310.opt-1.pyc - -# Rule to copy src asset scripts to dst. -# (and make non-writable so I'm less likely to accidentally edit them there) -$(SCRIPT_TARGETS_PY_PUBLIC) : build/%.py : src/%.py - @echo Copying script: $@ - @mkdir -p $(dir $@) - @rm -f $@ - @cp $^ $@ - @chmod 444 $@ - -# These are too complex to define in a pattern rule; -# Instead we generate individual targets in a loop. -$(foreach element,$(SCRIPT_TARGETS_PYC_PUBLIC),\ -$(eval $(call make-opt-pyc-target,$(element)))) - -SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \ - build/ba_data/python/bacommon/__init__.py \ - build/ba_data/python/bacommon/assets.py \ - build/ba_data/python/bacommon/bacloud.py \ - build/ba_data/python/bacommon/build.py \ - build/ba_data/python/bacommon/cloud.py \ - build/ba_data/python/bacommon/login.py \ - build/ba_data/python/bacommon/net.py \ - build/ba_data/python/bacommon/servermanager.py \ - build/ba_data/python/bacommon/transfer.py \ - build/ba_data/python/efro/__init__.py \ - build/ba_data/python/efro/call.py \ - build/ba_data/python/efro/cloudshell.py \ - build/ba_data/python/efro/dataclassio/__init__.py \ - build/ba_data/python/efro/dataclassio/_api.py \ - build/ba_data/python/efro/dataclassio/_base.py \ - build/ba_data/python/efro/dataclassio/_inputter.py \ - build/ba_data/python/efro/dataclassio/_outputter.py \ - build/ba_data/python/efro/dataclassio/_pathcapture.py \ - build/ba_data/python/efro/dataclassio/_prep.py \ - build/ba_data/python/efro/dataclassio/extras.py \ - build/ba_data/python/efro/debug.py \ - build/ba_data/python/efro/error.py \ - build/ba_data/python/efro/log.py \ - build/ba_data/python/efro/message/__init__.py \ - build/ba_data/python/efro/message/_message.py \ - build/ba_data/python/efro/message/_module.py \ - build/ba_data/python/efro/message/_protocol.py \ - build/ba_data/python/efro/message/_receiver.py \ - build/ba_data/python/efro/message/_sender.py \ - build/ba_data/python/efro/rpc.py \ - build/ba_data/python/efro/terminal.py \ - build/ba_data/python/efro/util.py - -SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \ - build/ba_data/python/bacommon/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/bacommon/__pycache__/assets.cpython-310.opt-1.pyc \ - build/ba_data/python/bacommon/__pycache__/bacloud.cpython-310.opt-1.pyc \ - build/ba_data/python/bacommon/__pycache__/build.cpython-310.opt-1.pyc \ - build/ba_data/python/bacommon/__pycache__/cloud.cpython-310.opt-1.pyc \ - build/ba_data/python/bacommon/__pycache__/login.cpython-310.opt-1.pyc \ - build/ba_data/python/bacommon/__pycache__/net.cpython-310.opt-1.pyc \ - build/ba_data/python/bacommon/__pycache__/servermanager.cpython-310.opt-1.pyc \ - build/ba_data/python/bacommon/__pycache__/transfer.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/__pycache__/call.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/__pycache__/cloudshell.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/dataclassio/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/dataclassio/__pycache__/_api.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/dataclassio/__pycache__/_base.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/dataclassio/__pycache__/_inputter.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/dataclassio/__pycache__/_outputter.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/dataclassio/__pycache__/_pathcapture.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/dataclassio/__pycache__/_prep.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/dataclassio/__pycache__/extras.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/__pycache__/debug.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/__pycache__/error.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/__pycache__/log.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/message/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/message/__pycache__/_message.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/message/__pycache__/_module.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/message/__pycache__/_protocol.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/message/__pycache__/_receiver.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/message/__pycache__/_sender.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/__pycache__/rpc.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/__pycache__/terminal.cpython-310.opt-1.pyc \ - build/ba_data/python/efro/__pycache__/util.cpython-310.opt-1.pyc - -# Rule to copy src asset scripts to dst. -# (and make non-writable so I'm less likely to accidentally edit them there) -$(SCRIPT_TARGETS_PY_PUBLIC_TOOLS) : build/ba_data/python/%.py : ../tools/%.py - @echo Copying script: $@ - @mkdir -p $(dir $@) - @rm -f $@ - @cp $^ $@ - @chmod 444 $@ - -# These are too complex to define in a pattern rule; -# Instead we generate individual targets in a loop. -$(foreach element,$(SCRIPT_TARGETS_PYC_PUBLIC_TOOLS),\ -$(eval $(call make-opt-pyc-target,$(element)))) -# __AUTOGENERATED_PUBLIC_END__ - -# This section is generated by batools.assetsmakefile; do not edit by hand. -# __AUTOGENERATED_PRIVATE_BEGIN__ - -SCRIPT_TARGETS_PY_PRIVATE_APPLE = \ - build/pylib-apple/__future__.py \ - build/pylib-apple/__phello__.foo.py \ - build/pylib-apple/_aix_support.py \ - build/pylib-apple/_bootsubprocess.py \ - build/pylib-apple/_collections_abc.py \ - build/pylib-apple/_compat_pickle.py \ - build/pylib-apple/_compression.py \ - build/pylib-apple/_markupbase.py \ - build/pylib-apple/_osx_support.py \ - build/pylib-apple/_py_abc.py \ - build/pylib-apple/_pydecimal.py \ - build/pylib-apple/_pyio.py \ - build/pylib-apple/_sitebuiltins.py \ - build/pylib-apple/_strptime.py \ - build/pylib-apple/_threading_local.py \ - build/pylib-apple/_weakrefset.py \ - build/pylib-apple/abc.py \ - build/pylib-apple/aifc.py \ - build/pylib-apple/antigravity.py \ - build/pylib-apple/argparse.py \ - build/pylib-apple/ast.py \ - build/pylib-apple/asynchat.py \ - build/pylib-apple/asyncio/__init__.py \ - build/pylib-apple/asyncio/__main__.py \ - build/pylib-apple/asyncio/base_events.py \ - build/pylib-apple/asyncio/base_futures.py \ - build/pylib-apple/asyncio/base_subprocess.py \ - build/pylib-apple/asyncio/base_tasks.py \ - build/pylib-apple/asyncio/constants.py \ - build/pylib-apple/asyncio/coroutines.py \ - build/pylib-apple/asyncio/events.py \ - build/pylib-apple/asyncio/exceptions.py \ - build/pylib-apple/asyncio/format_helpers.py \ - build/pylib-apple/asyncio/futures.py \ - build/pylib-apple/asyncio/locks.py \ - build/pylib-apple/asyncio/log.py \ - build/pylib-apple/asyncio/mixins.py \ - build/pylib-apple/asyncio/proactor_events.py \ - build/pylib-apple/asyncio/protocols.py \ - build/pylib-apple/asyncio/queues.py \ - build/pylib-apple/asyncio/runners.py \ - build/pylib-apple/asyncio/selector_events.py \ - build/pylib-apple/asyncio/sslproto.py \ - build/pylib-apple/asyncio/staggered.py \ - build/pylib-apple/asyncio/streams.py \ - build/pylib-apple/asyncio/subprocess.py \ - build/pylib-apple/asyncio/tasks.py \ - build/pylib-apple/asyncio/threads.py \ - build/pylib-apple/asyncio/transports.py \ - build/pylib-apple/asyncio/trsock.py \ - build/pylib-apple/asyncio/unix_events.py \ - build/pylib-apple/asyncio/windows_events.py \ - build/pylib-apple/asyncio/windows_utils.py \ - build/pylib-apple/asyncore.py \ - build/pylib-apple/base64.py \ - build/pylib-apple/bdb.py \ - build/pylib-apple/binhex.py \ - build/pylib-apple/bisect.py \ - build/pylib-apple/bz2.py \ - build/pylib-apple/cProfile.py \ - build/pylib-apple/calendar.py \ - build/pylib-apple/cgi.py \ - build/pylib-apple/cgitb.py \ - build/pylib-apple/chunk.py \ - build/pylib-apple/cmd.py \ - build/pylib-apple/code.py \ - build/pylib-apple/codecs.py \ - build/pylib-apple/codeop.py \ - build/pylib-apple/collections/__init__.py \ - build/pylib-apple/collections/abc.py \ - build/pylib-apple/colorsys.py \ - build/pylib-apple/compileall.py \ - build/pylib-apple/concurrent/__init__.py \ - build/pylib-apple/concurrent/futures/__init__.py \ - build/pylib-apple/concurrent/futures/_base.py \ - build/pylib-apple/concurrent/futures/process.py \ - build/pylib-apple/concurrent/futures/thread.py \ - build/pylib-apple/configparser.py \ - build/pylib-apple/contextlib.py \ - build/pylib-apple/contextvars.py \ - build/pylib-apple/copy.py \ - build/pylib-apple/copyreg.py \ - build/pylib-apple/crypt.py \ - build/pylib-apple/csv.py \ - build/pylib-apple/ctypes/__init__.py \ - build/pylib-apple/ctypes/_aix.py \ - build/pylib-apple/ctypes/_endian.py \ - build/pylib-apple/ctypes/macholib/__init__.py \ - build/pylib-apple/ctypes/macholib/dyld.py \ - build/pylib-apple/ctypes/macholib/dylib.py \ - build/pylib-apple/ctypes/macholib/framework.py \ - build/pylib-apple/ctypes/util.py \ - build/pylib-apple/ctypes/wintypes.py \ - build/pylib-apple/curses/__init__.py \ - build/pylib-apple/curses/ascii.py \ - build/pylib-apple/curses/has_key.py \ - build/pylib-apple/curses/panel.py \ - build/pylib-apple/curses/textpad.py \ - build/pylib-apple/dataclasses.py \ - build/pylib-apple/datetime.py \ - build/pylib-apple/decimal.py \ - build/pylib-apple/difflib.py \ - build/pylib-apple/dis.py \ - build/pylib-apple/doctest.py \ - build/pylib-apple/email/__init__.py \ - build/pylib-apple/email/_encoded_words.py \ - build/pylib-apple/email/_header_value_parser.py \ - build/pylib-apple/email/_parseaddr.py \ - build/pylib-apple/email/_policybase.py \ - build/pylib-apple/email/base64mime.py \ - build/pylib-apple/email/charset.py \ - build/pylib-apple/email/contentmanager.py \ - build/pylib-apple/email/encoders.py \ - build/pylib-apple/email/errors.py \ - build/pylib-apple/email/feedparser.py \ - build/pylib-apple/email/generator.py \ - build/pylib-apple/email/header.py \ - build/pylib-apple/email/headerregistry.py \ - build/pylib-apple/email/iterators.py \ - build/pylib-apple/email/message.py \ - build/pylib-apple/email/mime/__init__.py \ - build/pylib-apple/email/mime/application.py \ - build/pylib-apple/email/mime/audio.py \ - build/pylib-apple/email/mime/base.py \ - build/pylib-apple/email/mime/image.py \ - build/pylib-apple/email/mime/message.py \ - build/pylib-apple/email/mime/multipart.py \ - build/pylib-apple/email/mime/nonmultipart.py \ - build/pylib-apple/email/mime/text.py \ - build/pylib-apple/email/parser.py \ - build/pylib-apple/email/policy.py \ - build/pylib-apple/email/quoprimime.py \ - build/pylib-apple/email/utils.py \ - build/pylib-apple/encodings/__init__.py \ - build/pylib-apple/encodings/aliases.py \ - build/pylib-apple/encodings/ascii.py \ - build/pylib-apple/encodings/base64_codec.py \ - build/pylib-apple/encodings/big5.py \ - build/pylib-apple/encodings/big5hkscs.py \ - build/pylib-apple/encodings/bz2_codec.py \ - build/pylib-apple/encodings/charmap.py \ - build/pylib-apple/encodings/cp037.py \ - build/pylib-apple/encodings/cp1006.py \ - build/pylib-apple/encodings/cp1026.py \ - build/pylib-apple/encodings/cp1125.py \ - build/pylib-apple/encodings/cp1140.py \ - build/pylib-apple/encodings/cp1250.py \ - build/pylib-apple/encodings/cp1251.py \ - build/pylib-apple/encodings/cp1252.py \ - build/pylib-apple/encodings/cp1253.py \ - build/pylib-apple/encodings/cp1254.py \ - build/pylib-apple/encodings/cp1255.py \ - build/pylib-apple/encodings/cp1256.py \ - build/pylib-apple/encodings/cp1257.py \ - build/pylib-apple/encodings/cp1258.py \ - build/pylib-apple/encodings/cp273.py \ - build/pylib-apple/encodings/cp424.py \ - build/pylib-apple/encodings/cp437.py \ - build/pylib-apple/encodings/cp500.py \ - build/pylib-apple/encodings/cp720.py \ - build/pylib-apple/encodings/cp737.py \ - build/pylib-apple/encodings/cp775.py \ - build/pylib-apple/encodings/cp850.py \ - build/pylib-apple/encodings/cp852.py \ - build/pylib-apple/encodings/cp855.py \ - build/pylib-apple/encodings/cp856.py \ - build/pylib-apple/encodings/cp857.py \ - build/pylib-apple/encodings/cp858.py \ - build/pylib-apple/encodings/cp860.py \ - build/pylib-apple/encodings/cp861.py \ - build/pylib-apple/encodings/cp862.py \ - build/pylib-apple/encodings/cp863.py \ - build/pylib-apple/encodings/cp864.py \ - build/pylib-apple/encodings/cp865.py \ - build/pylib-apple/encodings/cp866.py \ - build/pylib-apple/encodings/cp869.py \ - build/pylib-apple/encodings/cp874.py \ - build/pylib-apple/encodings/cp875.py \ - build/pylib-apple/encodings/cp932.py \ - build/pylib-apple/encodings/cp949.py \ - build/pylib-apple/encodings/cp950.py \ - build/pylib-apple/encodings/euc_jis_2004.py \ - build/pylib-apple/encodings/euc_jisx0213.py \ - build/pylib-apple/encodings/euc_jp.py \ - build/pylib-apple/encodings/euc_kr.py \ - build/pylib-apple/encodings/gb18030.py \ - build/pylib-apple/encodings/gb2312.py \ - build/pylib-apple/encodings/gbk.py \ - build/pylib-apple/encodings/hex_codec.py \ - build/pylib-apple/encodings/hp_roman8.py \ - build/pylib-apple/encodings/hz.py \ - build/pylib-apple/encodings/idna.py \ - build/pylib-apple/encodings/iso2022_jp.py \ - build/pylib-apple/encodings/iso2022_jp_1.py \ - build/pylib-apple/encodings/iso2022_jp_2.py \ - build/pylib-apple/encodings/iso2022_jp_2004.py \ - build/pylib-apple/encodings/iso2022_jp_3.py \ - build/pylib-apple/encodings/iso2022_jp_ext.py \ - build/pylib-apple/encodings/iso2022_kr.py \ - build/pylib-apple/encodings/iso8859_1.py \ - build/pylib-apple/encodings/iso8859_10.py \ - build/pylib-apple/encodings/iso8859_11.py \ - build/pylib-apple/encodings/iso8859_13.py \ - build/pylib-apple/encodings/iso8859_14.py \ - build/pylib-apple/encodings/iso8859_15.py \ - build/pylib-apple/encodings/iso8859_16.py \ - build/pylib-apple/encodings/iso8859_2.py \ - build/pylib-apple/encodings/iso8859_3.py \ - build/pylib-apple/encodings/iso8859_4.py \ - build/pylib-apple/encodings/iso8859_5.py \ - build/pylib-apple/encodings/iso8859_6.py \ - build/pylib-apple/encodings/iso8859_7.py \ - build/pylib-apple/encodings/iso8859_8.py \ - build/pylib-apple/encodings/iso8859_9.py \ - build/pylib-apple/encodings/johab.py \ - build/pylib-apple/encodings/koi8_r.py \ - build/pylib-apple/encodings/koi8_t.py \ - build/pylib-apple/encodings/koi8_u.py \ - build/pylib-apple/encodings/kz1048.py \ - build/pylib-apple/encodings/latin_1.py \ - build/pylib-apple/encodings/mac_arabic.py \ - build/pylib-apple/encodings/mac_croatian.py \ - build/pylib-apple/encodings/mac_cyrillic.py \ - build/pylib-apple/encodings/mac_farsi.py \ - build/pylib-apple/encodings/mac_greek.py \ - build/pylib-apple/encodings/mac_iceland.py \ - build/pylib-apple/encodings/mac_latin2.py \ - build/pylib-apple/encodings/mac_roman.py \ - build/pylib-apple/encodings/mac_romanian.py \ - build/pylib-apple/encodings/mac_turkish.py \ - build/pylib-apple/encodings/mbcs.py \ - build/pylib-apple/encodings/oem.py \ - build/pylib-apple/encodings/palmos.py \ - build/pylib-apple/encodings/ptcp154.py \ - build/pylib-apple/encodings/punycode.py \ - build/pylib-apple/encodings/quopri_codec.py \ - build/pylib-apple/encodings/raw_unicode_escape.py \ - build/pylib-apple/encodings/rot_13.py \ - build/pylib-apple/encodings/shift_jis.py \ - build/pylib-apple/encodings/shift_jis_2004.py \ - build/pylib-apple/encodings/shift_jisx0213.py \ - build/pylib-apple/encodings/tis_620.py \ - build/pylib-apple/encodings/undefined.py \ - build/pylib-apple/encodings/unicode_escape.py \ - build/pylib-apple/encodings/utf_16.py \ - build/pylib-apple/encodings/utf_16_be.py \ - build/pylib-apple/encodings/utf_16_le.py \ - build/pylib-apple/encodings/utf_32.py \ - build/pylib-apple/encodings/utf_32_be.py \ - build/pylib-apple/encodings/utf_32_le.py \ - build/pylib-apple/encodings/utf_7.py \ - build/pylib-apple/encodings/utf_8.py \ - build/pylib-apple/encodings/utf_8_sig.py \ - build/pylib-apple/encodings/uu_codec.py \ - build/pylib-apple/encodings/zlib_codec.py \ - build/pylib-apple/enum.py \ - build/pylib-apple/filecmp.py \ - build/pylib-apple/fileinput.py \ - build/pylib-apple/fnmatch.py \ - build/pylib-apple/fractions.py \ - build/pylib-apple/ftplib.py \ - build/pylib-apple/functools.py \ - build/pylib-apple/genericpath.py \ - build/pylib-apple/getopt.py \ - build/pylib-apple/getpass.py \ - build/pylib-apple/gettext.py \ - build/pylib-apple/glob.py \ - build/pylib-apple/graphlib.py \ - build/pylib-apple/gzip.py \ - build/pylib-apple/hashlib.py \ - build/pylib-apple/heapq.py \ - build/pylib-apple/hmac.py \ - build/pylib-apple/html/__init__.py \ - build/pylib-apple/html/entities.py \ - build/pylib-apple/html/parser.py \ - build/pylib-apple/http/__init__.py \ - build/pylib-apple/http/client.py \ - build/pylib-apple/http/cookiejar.py \ - build/pylib-apple/http/cookies.py \ - build/pylib-apple/http/server.py \ - build/pylib-apple/imghdr.py \ - build/pylib-apple/imp.py \ - build/pylib-apple/importlib/__init__.py \ - build/pylib-apple/importlib/_abc.py \ - build/pylib-apple/importlib/_adapters.py \ - build/pylib-apple/importlib/_bootstrap.py \ - build/pylib-apple/importlib/_bootstrap_external.py \ - build/pylib-apple/importlib/_common.py \ - build/pylib-apple/importlib/abc.py \ - build/pylib-apple/importlib/machinery.py \ - build/pylib-apple/importlib/metadata/__init__.py \ - build/pylib-apple/importlib/metadata/_adapters.py \ - build/pylib-apple/importlib/metadata/_collections.py \ - build/pylib-apple/importlib/metadata/_functools.py \ - build/pylib-apple/importlib/metadata/_itertools.py \ - build/pylib-apple/importlib/metadata/_meta.py \ - build/pylib-apple/importlib/metadata/_text.py \ - build/pylib-apple/importlib/readers.py \ - build/pylib-apple/importlib/resources.py \ - build/pylib-apple/importlib/util.py \ - build/pylib-apple/inspect.py \ - build/pylib-apple/io.py \ - build/pylib-apple/ipaddress.py \ - build/pylib-apple/json/__init__.py \ - build/pylib-apple/json/decoder.py \ - build/pylib-apple/json/encoder.py \ - build/pylib-apple/json/scanner.py \ - build/pylib-apple/json/tool.py \ - build/pylib-apple/keyword.py \ - build/pylib-apple/linecache.py \ - build/pylib-apple/locale.py \ - build/pylib-apple/logging/__init__.py \ - build/pylib-apple/logging/config.py \ - build/pylib-apple/logging/handlers.py \ - build/pylib-apple/lzma.py \ - build/pylib-apple/mailbox.py \ - build/pylib-apple/mailcap.py \ - build/pylib-apple/mimetypes.py \ - build/pylib-apple/modulefinder.py \ - build/pylib-apple/msilib/__init__.py \ - build/pylib-apple/msilib/schema.py \ - build/pylib-apple/msilib/sequence.py \ - build/pylib-apple/msilib/text.py \ - build/pylib-apple/netrc.py \ - build/pylib-apple/nntplib.py \ - build/pylib-apple/ntpath.py \ - build/pylib-apple/nturl2path.py \ - build/pylib-apple/numbers.py \ - build/pylib-apple/opcode.py \ - build/pylib-apple/operator.py \ - build/pylib-apple/optparse.py \ - build/pylib-apple/os.py \ - build/pylib-apple/pathlib.py \ - build/pylib-apple/pdb.py \ - build/pylib-apple/pickle.py \ - build/pylib-apple/pickletools.py \ - build/pylib-apple/pipes.py \ - build/pylib-apple/pkgutil.py \ - build/pylib-apple/platform.py \ - build/pylib-apple/plistlib.py \ - build/pylib-apple/poplib.py \ - build/pylib-apple/posixpath.py \ - build/pylib-apple/pprint.py \ - build/pylib-apple/profile.py \ - build/pylib-apple/pstats.py \ - build/pylib-apple/pty.py \ - build/pylib-apple/py_compile.py \ - build/pylib-apple/pyclbr.py \ - build/pylib-apple/pydoc.py \ - build/pylib-apple/queue.py \ - build/pylib-apple/quopri.py \ - build/pylib-apple/random.py \ - build/pylib-apple/re.py \ - build/pylib-apple/reprlib.py \ - build/pylib-apple/rlcompleter.py \ - build/pylib-apple/runpy.py \ - build/pylib-apple/sched.py \ - build/pylib-apple/secrets.py \ - build/pylib-apple/selectors.py \ - build/pylib-apple/shelve.py \ - build/pylib-apple/shlex.py \ - build/pylib-apple/shutil.py \ - build/pylib-apple/signal.py \ - build/pylib-apple/site.py \ - build/pylib-apple/smtpd.py \ - build/pylib-apple/smtplib.py \ - build/pylib-apple/sndhdr.py \ - build/pylib-apple/socket.py \ - build/pylib-apple/socketserver.py \ - build/pylib-apple/sqlite3/__init__.py \ - build/pylib-apple/sqlite3/dbapi2.py \ - build/pylib-apple/sqlite3/dump.py \ - build/pylib-apple/sre_compile.py \ - build/pylib-apple/sre_constants.py \ - build/pylib-apple/sre_parse.py \ - build/pylib-apple/ssl.py \ - build/pylib-apple/stat.py \ - build/pylib-apple/statistics.py \ - build/pylib-apple/string.py \ - build/pylib-apple/stringprep.py \ - build/pylib-apple/struct.py \ - build/pylib-apple/subprocess.py \ - build/pylib-apple/sunau.py \ - build/pylib-apple/symtable.py \ - build/pylib-apple/sysconfig.py \ - build/pylib-apple/tabnanny.py \ - build/pylib-apple/tarfile.py \ - build/pylib-apple/telnetlib.py \ - build/pylib-apple/tempfile.py \ - build/pylib-apple/textwrap.py \ - build/pylib-apple/this.py \ - build/pylib-apple/threading.py \ - build/pylib-apple/timeit.py \ - build/pylib-apple/token.py \ - build/pylib-apple/tokenize.py \ - build/pylib-apple/trace.py \ - build/pylib-apple/traceback.py \ - build/pylib-apple/tracemalloc.py \ - build/pylib-apple/tty.py \ - build/pylib-apple/types.py \ - build/pylib-apple/typing.py \ - build/pylib-apple/urllib/__init__.py \ - build/pylib-apple/urllib/error.py \ - build/pylib-apple/urllib/parse.py \ - build/pylib-apple/urllib/request.py \ - build/pylib-apple/urllib/response.py \ - build/pylib-apple/urllib/robotparser.py \ - build/pylib-apple/uu.py \ - build/pylib-apple/uuid.py \ - build/pylib-apple/warnings.py \ - build/pylib-apple/wave.py \ - build/pylib-apple/weakref.py \ - build/pylib-apple/webbrowser.py \ - build/pylib-apple/xdrlib.py \ - build/pylib-apple/xml/__init__.py \ - build/pylib-apple/xml/dom/NodeFilter.py \ - build/pylib-apple/xml/dom/__init__.py \ - build/pylib-apple/xml/dom/domreg.py \ - build/pylib-apple/xml/dom/expatbuilder.py \ - build/pylib-apple/xml/dom/minicompat.py \ - build/pylib-apple/xml/dom/minidom.py \ - build/pylib-apple/xml/dom/pulldom.py \ - build/pylib-apple/xml/dom/xmlbuilder.py \ - build/pylib-apple/xml/etree/ElementInclude.py \ - build/pylib-apple/xml/etree/ElementPath.py \ - build/pylib-apple/xml/etree/ElementTree.py \ - build/pylib-apple/xml/etree/__init__.py \ - build/pylib-apple/xml/etree/cElementTree.py \ - build/pylib-apple/xml/parsers/__init__.py \ - build/pylib-apple/xml/parsers/expat.py \ - build/pylib-apple/xml/sax/__init__.py \ - build/pylib-apple/xml/sax/_exceptions.py \ - build/pylib-apple/xml/sax/expatreader.py \ - build/pylib-apple/xml/sax/handler.py \ - build/pylib-apple/xml/sax/saxutils.py \ - build/pylib-apple/xml/sax/xmlreader.py \ - build/pylib-apple/xmlrpc/__init__.py \ - build/pylib-apple/xmlrpc/client.py \ - build/pylib-apple/xmlrpc/server.py \ - build/pylib-apple/zipapp.py \ - build/pylib-apple/zipfile.py \ - build/pylib-apple/zipimport.py \ - build/pylib-apple/zoneinfo/__init__.py \ - build/pylib-apple/zoneinfo/_common.py \ - build/pylib-apple/zoneinfo/_tzpath.py \ - build/pylib-apple/zoneinfo/_zoneinfo.py - -SCRIPT_TARGETS_PYC_PRIVATE_APPLE = \ - build/pylib-apple/__pycache__/__future__.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/__phello__.foo.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/_aix_support.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/_bootsubprocess.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/_collections_abc.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/_compat_pickle.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/_compression.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/_markupbase.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/_osx_support.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/_py_abc.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/_pydecimal.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/_pyio.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/_sitebuiltins.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/_strptime.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/_threading_local.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/_weakrefset.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/abc.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/aifc.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/antigravity.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/argparse.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/ast.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/asynchat.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/__main__.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/base_events.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/base_futures.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/base_subprocess.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/base_tasks.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/constants.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/coroutines.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/events.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/exceptions.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/format_helpers.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/futures.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/locks.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/log.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/mixins.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/proactor_events.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/protocols.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/queues.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/runners.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/selector_events.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/sslproto.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/staggered.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/streams.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/subprocess.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/tasks.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/threads.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/transports.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/trsock.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/unix_events.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/windows_events.cpython-310.opt-1.pyc \ - build/pylib-apple/asyncio/__pycache__/windows_utils.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/asyncore.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/base64.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/bdb.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/binhex.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/bisect.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/bz2.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/cProfile.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/calendar.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/cgi.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/cgitb.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/chunk.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/cmd.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/code.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/codecs.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/codeop.cpython-310.opt-1.pyc \ - build/pylib-apple/collections/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/collections/__pycache__/abc.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/colorsys.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/compileall.cpython-310.opt-1.pyc \ - build/pylib-apple/concurrent/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/concurrent/futures/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/concurrent/futures/__pycache__/_base.cpython-310.opt-1.pyc \ - build/pylib-apple/concurrent/futures/__pycache__/process.cpython-310.opt-1.pyc \ - build/pylib-apple/concurrent/futures/__pycache__/thread.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/configparser.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/contextlib.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/contextvars.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/copy.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/copyreg.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/crypt.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/csv.cpython-310.opt-1.pyc \ - build/pylib-apple/ctypes/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/ctypes/__pycache__/_aix.cpython-310.opt-1.pyc \ - build/pylib-apple/ctypes/__pycache__/_endian.cpython-310.opt-1.pyc \ - build/pylib-apple/ctypes/macholib/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/ctypes/macholib/__pycache__/dyld.cpython-310.opt-1.pyc \ - build/pylib-apple/ctypes/macholib/__pycache__/dylib.cpython-310.opt-1.pyc \ - build/pylib-apple/ctypes/macholib/__pycache__/framework.cpython-310.opt-1.pyc \ - build/pylib-apple/ctypes/__pycache__/util.cpython-310.opt-1.pyc \ - build/pylib-apple/ctypes/__pycache__/wintypes.cpython-310.opt-1.pyc \ - build/pylib-apple/curses/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/curses/__pycache__/ascii.cpython-310.opt-1.pyc \ - build/pylib-apple/curses/__pycache__/has_key.cpython-310.opt-1.pyc \ - build/pylib-apple/curses/__pycache__/panel.cpython-310.opt-1.pyc \ - build/pylib-apple/curses/__pycache__/textpad.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/dataclasses.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/datetime.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/decimal.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/difflib.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/dis.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/doctest.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/_encoded_words.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/_header_value_parser.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/_parseaddr.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/_policybase.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/base64mime.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/charset.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/contentmanager.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/encoders.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/errors.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/feedparser.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/generator.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/header.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/headerregistry.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/iterators.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/message.cpython-310.opt-1.pyc \ - build/pylib-apple/email/mime/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/email/mime/__pycache__/application.cpython-310.opt-1.pyc \ - build/pylib-apple/email/mime/__pycache__/audio.cpython-310.opt-1.pyc \ - build/pylib-apple/email/mime/__pycache__/base.cpython-310.opt-1.pyc \ - build/pylib-apple/email/mime/__pycache__/image.cpython-310.opt-1.pyc \ - build/pylib-apple/email/mime/__pycache__/message.cpython-310.opt-1.pyc \ - build/pylib-apple/email/mime/__pycache__/multipart.cpython-310.opt-1.pyc \ - build/pylib-apple/email/mime/__pycache__/nonmultipart.cpython-310.opt-1.pyc \ - build/pylib-apple/email/mime/__pycache__/text.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/parser.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/policy.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/quoprimime.cpython-310.opt-1.pyc \ - build/pylib-apple/email/__pycache__/utils.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/aliases.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/ascii.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/base64_codec.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/big5.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/big5hkscs.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/bz2_codec.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/charmap.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp037.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp1006.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp1026.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp1125.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp1140.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp1250.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp1251.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp1252.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp1253.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp1254.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp1255.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp1256.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp1257.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp1258.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp273.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp424.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp437.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp500.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp720.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp737.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp775.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp850.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp852.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp855.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp856.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp857.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp858.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp860.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp861.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp862.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp863.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp864.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp865.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp866.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp869.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp874.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp875.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp932.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp949.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/cp950.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/euc_jis_2004.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/euc_jisx0213.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/euc_jp.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/euc_kr.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/gb18030.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/gb2312.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/gbk.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/hex_codec.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/hp_roman8.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/hz.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/idna.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso2022_jp.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso2022_jp_1.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso2022_jp_2.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso2022_jp_2004.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso2022_jp_3.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso2022_jp_ext.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso2022_kr.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso8859_1.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso8859_10.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso8859_11.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso8859_13.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso8859_14.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso8859_15.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso8859_16.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso8859_2.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso8859_3.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso8859_4.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso8859_5.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso8859_6.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso8859_7.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso8859_8.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/iso8859_9.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/johab.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/koi8_r.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/koi8_t.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/koi8_u.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/kz1048.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/latin_1.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/mac_arabic.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/mac_croatian.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/mac_cyrillic.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/mac_farsi.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/mac_greek.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/mac_iceland.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/mac_latin2.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/mac_roman.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/mac_romanian.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/mac_turkish.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/mbcs.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/oem.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/palmos.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/ptcp154.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/punycode.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/quopri_codec.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/raw_unicode_escape.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/rot_13.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/shift_jis.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/shift_jis_2004.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/shift_jisx0213.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/tis_620.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/undefined.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/unicode_escape.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/utf_16.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/utf_16_be.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/utf_16_le.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/utf_32.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/utf_32_be.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/utf_32_le.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/utf_7.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/utf_8.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/utf_8_sig.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/uu_codec.cpython-310.opt-1.pyc \ - build/pylib-apple/encodings/__pycache__/zlib_codec.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/enum.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/filecmp.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/fileinput.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/fnmatch.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/fractions.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/ftplib.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/functools.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/genericpath.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/getopt.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/getpass.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/gettext.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/glob.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/graphlib.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/gzip.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/hashlib.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/heapq.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/hmac.cpython-310.opt-1.pyc \ - build/pylib-apple/html/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/html/__pycache__/entities.cpython-310.opt-1.pyc \ - build/pylib-apple/html/__pycache__/parser.cpython-310.opt-1.pyc \ - build/pylib-apple/http/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/http/__pycache__/client.cpython-310.opt-1.pyc \ - build/pylib-apple/http/__pycache__/cookiejar.cpython-310.opt-1.pyc \ - build/pylib-apple/http/__pycache__/cookies.cpython-310.opt-1.pyc \ - build/pylib-apple/http/__pycache__/server.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/imghdr.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/imp.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/__pycache__/_abc.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/__pycache__/_adapters.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/__pycache__/_bootstrap.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/__pycache__/_bootstrap_external.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/__pycache__/_common.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/__pycache__/abc.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/__pycache__/machinery.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/metadata/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/metadata/__pycache__/_adapters.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/metadata/__pycache__/_collections.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/metadata/__pycache__/_functools.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/metadata/__pycache__/_itertools.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/metadata/__pycache__/_meta.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/metadata/__pycache__/_text.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/__pycache__/readers.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/__pycache__/resources.cpython-310.opt-1.pyc \ - build/pylib-apple/importlib/__pycache__/util.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/inspect.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/io.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/ipaddress.cpython-310.opt-1.pyc \ - build/pylib-apple/json/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/json/__pycache__/decoder.cpython-310.opt-1.pyc \ - build/pylib-apple/json/__pycache__/encoder.cpython-310.opt-1.pyc \ - build/pylib-apple/json/__pycache__/scanner.cpython-310.opt-1.pyc \ - build/pylib-apple/json/__pycache__/tool.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/keyword.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/linecache.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/locale.cpython-310.opt-1.pyc \ - build/pylib-apple/logging/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/logging/__pycache__/config.cpython-310.opt-1.pyc \ - build/pylib-apple/logging/__pycache__/handlers.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/lzma.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/mailbox.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/mailcap.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/mimetypes.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/modulefinder.cpython-310.opt-1.pyc \ - build/pylib-apple/msilib/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/msilib/__pycache__/schema.cpython-310.opt-1.pyc \ - build/pylib-apple/msilib/__pycache__/sequence.cpython-310.opt-1.pyc \ - build/pylib-apple/msilib/__pycache__/text.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/netrc.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/nntplib.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/ntpath.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/nturl2path.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/numbers.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/opcode.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/operator.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/optparse.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/os.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/pathlib.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/pdb.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/pickle.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/pickletools.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/pipes.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/pkgutil.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/platform.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/plistlib.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/poplib.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/posixpath.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/pprint.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/profile.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/pstats.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/pty.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/py_compile.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/pyclbr.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/pydoc.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/queue.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/quopri.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/random.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/re.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/reprlib.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/rlcompleter.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/runpy.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/sched.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/secrets.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/selectors.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/shelve.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/shlex.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/shutil.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/signal.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/site.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/smtpd.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/smtplib.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/sndhdr.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/socket.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/socketserver.cpython-310.opt-1.pyc \ - build/pylib-apple/sqlite3/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/sqlite3/__pycache__/dbapi2.cpython-310.opt-1.pyc \ - build/pylib-apple/sqlite3/__pycache__/dump.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/sre_compile.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/sre_constants.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/sre_parse.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/ssl.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/stat.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/statistics.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/string.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/stringprep.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/struct.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/subprocess.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/sunau.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/symtable.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/sysconfig.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/tabnanny.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/tarfile.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/telnetlib.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/tempfile.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/textwrap.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/this.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/threading.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/timeit.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/token.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/tokenize.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/trace.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/traceback.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/tracemalloc.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/tty.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/types.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/typing.cpython-310.opt-1.pyc \ - build/pylib-apple/urllib/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/urllib/__pycache__/error.cpython-310.opt-1.pyc \ - build/pylib-apple/urllib/__pycache__/parse.cpython-310.opt-1.pyc \ - build/pylib-apple/urllib/__pycache__/request.cpython-310.opt-1.pyc \ - build/pylib-apple/urllib/__pycache__/response.cpython-310.opt-1.pyc \ - build/pylib-apple/urllib/__pycache__/robotparser.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/uu.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/uuid.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/warnings.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/wave.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/weakref.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/webbrowser.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/xdrlib.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/dom/__pycache__/NodeFilter.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/dom/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/dom/__pycache__/domreg.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/dom/__pycache__/expatbuilder.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/dom/__pycache__/minicompat.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/dom/__pycache__/minidom.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/dom/__pycache__/pulldom.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/dom/__pycache__/xmlbuilder.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/etree/__pycache__/ElementInclude.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/etree/__pycache__/ElementPath.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/etree/__pycache__/ElementTree.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/etree/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/etree/__pycache__/cElementTree.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/parsers/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/parsers/__pycache__/expat.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/sax/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/sax/__pycache__/_exceptions.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/sax/__pycache__/expatreader.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/sax/__pycache__/handler.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/sax/__pycache__/saxutils.cpython-310.opt-1.pyc \ - build/pylib-apple/xml/sax/__pycache__/xmlreader.cpython-310.opt-1.pyc \ - build/pylib-apple/xmlrpc/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/xmlrpc/__pycache__/client.cpython-310.opt-1.pyc \ - build/pylib-apple/xmlrpc/__pycache__/server.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/zipapp.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/zipfile.cpython-310.opt-1.pyc \ - build/pylib-apple/__pycache__/zipimport.cpython-310.opt-1.pyc \ - build/pylib-apple/zoneinfo/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-apple/zoneinfo/__pycache__/_common.cpython-310.opt-1.pyc \ - build/pylib-apple/zoneinfo/__pycache__/_tzpath.cpython-310.opt-1.pyc \ - build/pylib-apple/zoneinfo/__pycache__/_zoneinfo.cpython-310.opt-1.pyc - -# Rule to copy src asset scripts to dst. -# (and make non-writable so I'm less likely to accidentally edit them there) -$(SCRIPT_TARGETS_PY_PRIVATE_APPLE) : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -# These are too complex to define in a pattern rule; -# Instead we generate individual targets in a loop. -$(foreach element,$(SCRIPT_TARGETS_PYC_PRIVATE_APPLE),\ -$(eval $(call make-opt-pyc-target,$(element)))) - -SCRIPT_TARGETS_PY_PRIVATE_ANDROID = \ - build/pylib-android/__future__.py \ - build/pylib-android/__phello__.foo.py \ - build/pylib-android/_aix_support.py \ - build/pylib-android/_bootsubprocess.py \ - build/pylib-android/_collections_abc.py \ - build/pylib-android/_compat_pickle.py \ - build/pylib-android/_compression.py \ - build/pylib-android/_markupbase.py \ - build/pylib-android/_osx_support.py \ - build/pylib-android/_py_abc.py \ - build/pylib-android/_pydecimal.py \ - build/pylib-android/_pyio.py \ - build/pylib-android/_sitebuiltins.py \ - build/pylib-android/_strptime.py \ - build/pylib-android/_threading_local.py \ - build/pylib-android/_weakrefset.py \ - build/pylib-android/abc.py \ - build/pylib-android/aifc.py \ - build/pylib-android/antigravity.py \ - build/pylib-android/argparse.py \ - build/pylib-android/ast.py \ - build/pylib-android/asynchat.py \ - build/pylib-android/asyncio/__init__.py \ - build/pylib-android/asyncio/__main__.py \ - build/pylib-android/asyncio/base_events.py \ - build/pylib-android/asyncio/base_futures.py \ - build/pylib-android/asyncio/base_subprocess.py \ - build/pylib-android/asyncio/base_tasks.py \ - build/pylib-android/asyncio/constants.py \ - build/pylib-android/asyncio/coroutines.py \ - build/pylib-android/asyncio/events.py \ - build/pylib-android/asyncio/exceptions.py \ - build/pylib-android/asyncio/format_helpers.py \ - build/pylib-android/asyncio/futures.py \ - build/pylib-android/asyncio/locks.py \ - build/pylib-android/asyncio/log.py \ - build/pylib-android/asyncio/mixins.py \ - build/pylib-android/asyncio/proactor_events.py \ - build/pylib-android/asyncio/protocols.py \ - build/pylib-android/asyncio/queues.py \ - build/pylib-android/asyncio/runners.py \ - build/pylib-android/asyncio/selector_events.py \ - build/pylib-android/asyncio/sslproto.py \ - build/pylib-android/asyncio/staggered.py \ - build/pylib-android/asyncio/streams.py \ - build/pylib-android/asyncio/subprocess.py \ - build/pylib-android/asyncio/tasks.py \ - build/pylib-android/asyncio/threads.py \ - build/pylib-android/asyncio/transports.py \ - build/pylib-android/asyncio/trsock.py \ - build/pylib-android/asyncio/unix_events.py \ - build/pylib-android/asyncio/windows_events.py \ - build/pylib-android/asyncio/windows_utils.py \ - build/pylib-android/asyncore.py \ - build/pylib-android/base64.py \ - build/pylib-android/bdb.py \ - build/pylib-android/binhex.py \ - build/pylib-android/bisect.py \ - build/pylib-android/bz2.py \ - build/pylib-android/cProfile.py \ - build/pylib-android/calendar.py \ - build/pylib-android/cgi.py \ - build/pylib-android/cgitb.py \ - build/pylib-android/chunk.py \ - build/pylib-android/cmd.py \ - build/pylib-android/code.py \ - build/pylib-android/codecs.py \ - build/pylib-android/codeop.py \ - build/pylib-android/collections/__init__.py \ - build/pylib-android/collections/abc.py \ - build/pylib-android/colorsys.py \ - build/pylib-android/compileall.py \ - build/pylib-android/concurrent/__init__.py \ - build/pylib-android/concurrent/futures/__init__.py \ - build/pylib-android/concurrent/futures/_base.py \ - build/pylib-android/concurrent/futures/process.py \ - build/pylib-android/concurrent/futures/thread.py \ - build/pylib-android/configparser.py \ - build/pylib-android/contextlib.py \ - build/pylib-android/contextvars.py \ - build/pylib-android/copy.py \ - build/pylib-android/copyreg.py \ - build/pylib-android/crypt.py \ - build/pylib-android/csv.py \ - build/pylib-android/ctypes/__init__.py \ - build/pylib-android/ctypes/_aix.py \ - build/pylib-android/ctypes/_endian.py \ - build/pylib-android/ctypes/macholib/__init__.py \ - build/pylib-android/ctypes/macholib/dyld.py \ - build/pylib-android/ctypes/macholib/dylib.py \ - build/pylib-android/ctypes/macholib/framework.py \ - build/pylib-android/ctypes/util.py \ - build/pylib-android/ctypes/wintypes.py \ - build/pylib-android/curses/__init__.py \ - build/pylib-android/curses/ascii.py \ - build/pylib-android/curses/has_key.py \ - build/pylib-android/curses/panel.py \ - build/pylib-android/curses/textpad.py \ - build/pylib-android/dataclasses.py \ - build/pylib-android/datetime.py \ - build/pylib-android/decimal.py \ - build/pylib-android/difflib.py \ - build/pylib-android/dis.py \ - build/pylib-android/doctest.py \ - build/pylib-android/email/__init__.py \ - build/pylib-android/email/_encoded_words.py \ - build/pylib-android/email/_header_value_parser.py \ - build/pylib-android/email/_parseaddr.py \ - build/pylib-android/email/_policybase.py \ - build/pylib-android/email/base64mime.py \ - build/pylib-android/email/charset.py \ - build/pylib-android/email/contentmanager.py \ - build/pylib-android/email/encoders.py \ - build/pylib-android/email/errors.py \ - build/pylib-android/email/feedparser.py \ - build/pylib-android/email/generator.py \ - build/pylib-android/email/header.py \ - build/pylib-android/email/headerregistry.py \ - build/pylib-android/email/iterators.py \ - build/pylib-android/email/message.py \ - build/pylib-android/email/mime/__init__.py \ - build/pylib-android/email/mime/application.py \ - build/pylib-android/email/mime/audio.py \ - build/pylib-android/email/mime/base.py \ - build/pylib-android/email/mime/image.py \ - build/pylib-android/email/mime/message.py \ - build/pylib-android/email/mime/multipart.py \ - build/pylib-android/email/mime/nonmultipart.py \ - build/pylib-android/email/mime/text.py \ - build/pylib-android/email/parser.py \ - build/pylib-android/email/policy.py \ - build/pylib-android/email/quoprimime.py \ - build/pylib-android/email/utils.py \ - build/pylib-android/encodings/__init__.py \ - build/pylib-android/encodings/aliases.py \ - build/pylib-android/encodings/ascii.py \ - build/pylib-android/encodings/base64_codec.py \ - build/pylib-android/encodings/big5.py \ - build/pylib-android/encodings/big5hkscs.py \ - build/pylib-android/encodings/bz2_codec.py \ - build/pylib-android/encodings/charmap.py \ - build/pylib-android/encodings/cp037.py \ - build/pylib-android/encodings/cp1006.py \ - build/pylib-android/encodings/cp1026.py \ - build/pylib-android/encodings/cp1125.py \ - build/pylib-android/encodings/cp1140.py \ - build/pylib-android/encodings/cp1250.py \ - build/pylib-android/encodings/cp1251.py \ - build/pylib-android/encodings/cp1252.py \ - build/pylib-android/encodings/cp1253.py \ - build/pylib-android/encodings/cp1254.py \ - build/pylib-android/encodings/cp1255.py \ - build/pylib-android/encodings/cp1256.py \ - build/pylib-android/encodings/cp1257.py \ - build/pylib-android/encodings/cp1258.py \ - build/pylib-android/encodings/cp273.py \ - build/pylib-android/encodings/cp424.py \ - build/pylib-android/encodings/cp437.py \ - build/pylib-android/encodings/cp500.py \ - build/pylib-android/encodings/cp720.py \ - build/pylib-android/encodings/cp737.py \ - build/pylib-android/encodings/cp775.py \ - build/pylib-android/encodings/cp850.py \ - build/pylib-android/encodings/cp852.py \ - build/pylib-android/encodings/cp855.py \ - build/pylib-android/encodings/cp856.py \ - build/pylib-android/encodings/cp857.py \ - build/pylib-android/encodings/cp858.py \ - build/pylib-android/encodings/cp860.py \ - build/pylib-android/encodings/cp861.py \ - build/pylib-android/encodings/cp862.py \ - build/pylib-android/encodings/cp863.py \ - build/pylib-android/encodings/cp864.py \ - build/pylib-android/encodings/cp865.py \ - build/pylib-android/encodings/cp866.py \ - build/pylib-android/encodings/cp869.py \ - build/pylib-android/encodings/cp874.py \ - build/pylib-android/encodings/cp875.py \ - build/pylib-android/encodings/cp932.py \ - build/pylib-android/encodings/cp949.py \ - build/pylib-android/encodings/cp950.py \ - build/pylib-android/encodings/euc_jis_2004.py \ - build/pylib-android/encodings/euc_jisx0213.py \ - build/pylib-android/encodings/euc_jp.py \ - build/pylib-android/encodings/euc_kr.py \ - build/pylib-android/encodings/gb18030.py \ - build/pylib-android/encodings/gb2312.py \ - build/pylib-android/encodings/gbk.py \ - build/pylib-android/encodings/hex_codec.py \ - build/pylib-android/encodings/hp_roman8.py \ - build/pylib-android/encodings/hz.py \ - build/pylib-android/encodings/idna.py \ - build/pylib-android/encodings/iso2022_jp.py \ - build/pylib-android/encodings/iso2022_jp_1.py \ - build/pylib-android/encodings/iso2022_jp_2.py \ - build/pylib-android/encodings/iso2022_jp_2004.py \ - build/pylib-android/encodings/iso2022_jp_3.py \ - build/pylib-android/encodings/iso2022_jp_ext.py \ - build/pylib-android/encodings/iso2022_kr.py \ - build/pylib-android/encodings/iso8859_1.py \ - build/pylib-android/encodings/iso8859_10.py \ - build/pylib-android/encodings/iso8859_11.py \ - build/pylib-android/encodings/iso8859_13.py \ - build/pylib-android/encodings/iso8859_14.py \ - build/pylib-android/encodings/iso8859_15.py \ - build/pylib-android/encodings/iso8859_16.py \ - build/pylib-android/encodings/iso8859_2.py \ - build/pylib-android/encodings/iso8859_3.py \ - build/pylib-android/encodings/iso8859_4.py \ - build/pylib-android/encodings/iso8859_5.py \ - build/pylib-android/encodings/iso8859_6.py \ - build/pylib-android/encodings/iso8859_7.py \ - build/pylib-android/encodings/iso8859_8.py \ - build/pylib-android/encodings/iso8859_9.py \ - build/pylib-android/encodings/johab.py \ - build/pylib-android/encodings/koi8_r.py \ - build/pylib-android/encodings/koi8_t.py \ - build/pylib-android/encodings/koi8_u.py \ - build/pylib-android/encodings/kz1048.py \ - build/pylib-android/encodings/latin_1.py \ - build/pylib-android/encodings/mac_arabic.py \ - build/pylib-android/encodings/mac_croatian.py \ - build/pylib-android/encodings/mac_cyrillic.py \ - build/pylib-android/encodings/mac_farsi.py \ - build/pylib-android/encodings/mac_greek.py \ - build/pylib-android/encodings/mac_iceland.py \ - build/pylib-android/encodings/mac_latin2.py \ - build/pylib-android/encodings/mac_roman.py \ - build/pylib-android/encodings/mac_romanian.py \ - build/pylib-android/encodings/mac_turkish.py \ - build/pylib-android/encodings/mbcs.py \ - build/pylib-android/encodings/oem.py \ - build/pylib-android/encodings/palmos.py \ - build/pylib-android/encodings/ptcp154.py \ - build/pylib-android/encodings/punycode.py \ - build/pylib-android/encodings/quopri_codec.py \ - build/pylib-android/encodings/raw_unicode_escape.py \ - build/pylib-android/encodings/rot_13.py \ - build/pylib-android/encodings/shift_jis.py \ - build/pylib-android/encodings/shift_jis_2004.py \ - build/pylib-android/encodings/shift_jisx0213.py \ - build/pylib-android/encodings/tis_620.py \ - build/pylib-android/encodings/undefined.py \ - build/pylib-android/encodings/unicode_escape.py \ - build/pylib-android/encodings/utf_16.py \ - build/pylib-android/encodings/utf_16_be.py \ - build/pylib-android/encodings/utf_16_le.py \ - build/pylib-android/encodings/utf_32.py \ - build/pylib-android/encodings/utf_32_be.py \ - build/pylib-android/encodings/utf_32_le.py \ - build/pylib-android/encodings/utf_7.py \ - build/pylib-android/encodings/utf_8.py \ - build/pylib-android/encodings/utf_8_sig.py \ - build/pylib-android/encodings/uu_codec.py \ - build/pylib-android/encodings/zlib_codec.py \ - build/pylib-android/enum.py \ - build/pylib-android/filecmp.py \ - build/pylib-android/fileinput.py \ - build/pylib-android/fnmatch.py \ - build/pylib-android/fractions.py \ - build/pylib-android/ftplib.py \ - build/pylib-android/functools.py \ - build/pylib-android/genericpath.py \ - build/pylib-android/getopt.py \ - build/pylib-android/getpass.py \ - build/pylib-android/gettext.py \ - build/pylib-android/glob.py \ - build/pylib-android/graphlib.py \ - build/pylib-android/gzip.py \ - build/pylib-android/hashlib.py \ - build/pylib-android/heapq.py \ - build/pylib-android/hmac.py \ - build/pylib-android/html/__init__.py \ - build/pylib-android/html/entities.py \ - build/pylib-android/html/parser.py \ - build/pylib-android/http/__init__.py \ - build/pylib-android/http/client.py \ - build/pylib-android/http/cookiejar.py \ - build/pylib-android/http/cookies.py \ - build/pylib-android/http/server.py \ - build/pylib-android/imghdr.py \ - build/pylib-android/imp.py \ - build/pylib-android/importlib/__init__.py \ - build/pylib-android/importlib/_abc.py \ - build/pylib-android/importlib/_adapters.py \ - build/pylib-android/importlib/_bootstrap.py \ - build/pylib-android/importlib/_bootstrap_external.py \ - build/pylib-android/importlib/_common.py \ - build/pylib-android/importlib/abc.py \ - build/pylib-android/importlib/machinery.py \ - build/pylib-android/importlib/metadata/__init__.py \ - build/pylib-android/importlib/metadata/_adapters.py \ - build/pylib-android/importlib/metadata/_collections.py \ - build/pylib-android/importlib/metadata/_functools.py \ - build/pylib-android/importlib/metadata/_itertools.py \ - build/pylib-android/importlib/metadata/_meta.py \ - build/pylib-android/importlib/metadata/_text.py \ - build/pylib-android/importlib/readers.py \ - build/pylib-android/importlib/resources.py \ - build/pylib-android/importlib/util.py \ - build/pylib-android/inspect.py \ - build/pylib-android/io.py \ - build/pylib-android/ipaddress.py \ - build/pylib-android/json/__init__.py \ - build/pylib-android/json/decoder.py \ - build/pylib-android/json/encoder.py \ - build/pylib-android/json/scanner.py \ - build/pylib-android/json/tool.py \ - build/pylib-android/keyword.py \ - build/pylib-android/linecache.py \ - build/pylib-android/locale.py \ - build/pylib-android/logging/__init__.py \ - build/pylib-android/logging/config.py \ - build/pylib-android/logging/handlers.py \ - build/pylib-android/lzma.py \ - build/pylib-android/mailbox.py \ - build/pylib-android/mailcap.py \ - build/pylib-android/mimetypes.py \ - build/pylib-android/modulefinder.py \ - build/pylib-android/netrc.py \ - build/pylib-android/nntplib.py \ - build/pylib-android/ntpath.py \ - build/pylib-android/nturl2path.py \ - build/pylib-android/numbers.py \ - build/pylib-android/opcode.py \ - build/pylib-android/operator.py \ - build/pylib-android/optparse.py \ - build/pylib-android/os.py \ - build/pylib-android/pathlib.py \ - build/pylib-android/pdb.py \ - build/pylib-android/pickle.py \ - build/pylib-android/pickletools.py \ - build/pylib-android/pipes.py \ - build/pylib-android/pkgutil.py \ - build/pylib-android/platform.py \ - build/pylib-android/plistlib.py \ - build/pylib-android/poplib.py \ - build/pylib-android/posixpath.py \ - build/pylib-android/pprint.py \ - build/pylib-android/profile.py \ - build/pylib-android/pstats.py \ - build/pylib-android/pty.py \ - build/pylib-android/py_compile.py \ - build/pylib-android/pyclbr.py \ - build/pylib-android/pydoc.py \ - build/pylib-android/queue.py \ - build/pylib-android/quopri.py \ - build/pylib-android/random.py \ - build/pylib-android/re.py \ - build/pylib-android/reprlib.py \ - build/pylib-android/rlcompleter.py \ - build/pylib-android/runpy.py \ - build/pylib-android/sched.py \ - build/pylib-android/secrets.py \ - build/pylib-android/selectors.py \ - build/pylib-android/shelve.py \ - build/pylib-android/shlex.py \ - build/pylib-android/shutil.py \ - build/pylib-android/signal.py \ - build/pylib-android/site.py \ - build/pylib-android/smtpd.py \ - build/pylib-android/smtplib.py \ - build/pylib-android/sndhdr.py \ - build/pylib-android/socket.py \ - build/pylib-android/socketserver.py \ - build/pylib-android/sqlite3/__init__.py \ - build/pylib-android/sqlite3/dbapi2.py \ - build/pylib-android/sqlite3/dump.py \ - build/pylib-android/sre_compile.py \ - build/pylib-android/sre_constants.py \ - build/pylib-android/sre_parse.py \ - build/pylib-android/ssl.py \ - build/pylib-android/stat.py \ - build/pylib-android/statistics.py \ - build/pylib-android/string.py \ - build/pylib-android/stringprep.py \ - build/pylib-android/struct.py \ - build/pylib-android/subprocess.py \ - build/pylib-android/sunau.py \ - build/pylib-android/symtable.py \ - build/pylib-android/sysconfig.py \ - build/pylib-android/tabnanny.py \ - build/pylib-android/tarfile.py \ - build/pylib-android/telnetlib.py \ - build/pylib-android/tempfile.py \ - build/pylib-android/textwrap.py \ - build/pylib-android/this.py \ - build/pylib-android/threading.py \ - build/pylib-android/timeit.py \ - build/pylib-android/token.py \ - build/pylib-android/tokenize.py \ - build/pylib-android/trace.py \ - build/pylib-android/traceback.py \ - build/pylib-android/tracemalloc.py \ - build/pylib-android/tty.py \ - build/pylib-android/types.py \ - build/pylib-android/typing.py \ - build/pylib-android/urllib/__init__.py \ - build/pylib-android/urllib/error.py \ - build/pylib-android/urllib/parse.py \ - build/pylib-android/urllib/request.py \ - build/pylib-android/urllib/response.py \ - build/pylib-android/urllib/robotparser.py \ - build/pylib-android/uu.py \ - build/pylib-android/uuid.py \ - build/pylib-android/warnings.py \ - build/pylib-android/wave.py \ - build/pylib-android/weakref.py \ - build/pylib-android/webbrowser.py \ - build/pylib-android/xdrlib.py \ - build/pylib-android/xml/__init__.py \ - build/pylib-android/xml/dom/NodeFilter.py \ - build/pylib-android/xml/dom/__init__.py \ - build/pylib-android/xml/dom/domreg.py \ - build/pylib-android/xml/dom/expatbuilder.py \ - build/pylib-android/xml/dom/minicompat.py \ - build/pylib-android/xml/dom/minidom.py \ - build/pylib-android/xml/dom/pulldom.py \ - build/pylib-android/xml/dom/xmlbuilder.py \ - build/pylib-android/xml/etree/ElementInclude.py \ - build/pylib-android/xml/etree/ElementPath.py \ - build/pylib-android/xml/etree/ElementTree.py \ - build/pylib-android/xml/etree/__init__.py \ - build/pylib-android/xml/etree/cElementTree.py \ - build/pylib-android/xml/parsers/__init__.py \ - build/pylib-android/xml/parsers/expat.py \ - build/pylib-android/xml/sax/__init__.py \ - build/pylib-android/xml/sax/_exceptions.py \ - build/pylib-android/xml/sax/expatreader.py \ - build/pylib-android/xml/sax/handler.py \ - build/pylib-android/xml/sax/saxutils.py \ - build/pylib-android/xml/sax/xmlreader.py \ - build/pylib-android/xmlrpc/__init__.py \ - build/pylib-android/xmlrpc/client.py \ - build/pylib-android/xmlrpc/server.py \ - build/pylib-android/zipapp.py \ - build/pylib-android/zipfile.py \ - build/pylib-android/zipimport.py \ - build/pylib-android/zoneinfo/__init__.py \ - build/pylib-android/zoneinfo/_common.py \ - build/pylib-android/zoneinfo/_tzpath.py \ - build/pylib-android/zoneinfo/_zoneinfo.py - -SCRIPT_TARGETS_PYC_PRIVATE_ANDROID = \ - build/pylib-android/__pycache__/__future__.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/__phello__.foo.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/_aix_support.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/_bootsubprocess.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/_collections_abc.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/_compat_pickle.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/_compression.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/_markupbase.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/_osx_support.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/_py_abc.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/_pydecimal.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/_pyio.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/_sitebuiltins.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/_strptime.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/_threading_local.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/_weakrefset.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/abc.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/aifc.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/antigravity.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/argparse.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/ast.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/asynchat.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/__main__.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/base_events.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/base_futures.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/base_subprocess.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/base_tasks.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/constants.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/coroutines.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/events.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/exceptions.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/format_helpers.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/futures.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/locks.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/log.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/mixins.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/proactor_events.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/protocols.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/queues.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/runners.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/selector_events.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/sslproto.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/staggered.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/streams.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/subprocess.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/tasks.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/threads.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/transports.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/trsock.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/unix_events.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/windows_events.cpython-310.opt-1.pyc \ - build/pylib-android/asyncio/__pycache__/windows_utils.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/asyncore.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/base64.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/bdb.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/binhex.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/bisect.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/bz2.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/cProfile.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/calendar.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/cgi.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/cgitb.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/chunk.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/cmd.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/code.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/codecs.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/codeop.cpython-310.opt-1.pyc \ - build/pylib-android/collections/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/collections/__pycache__/abc.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/colorsys.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/compileall.cpython-310.opt-1.pyc \ - build/pylib-android/concurrent/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/concurrent/futures/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/concurrent/futures/__pycache__/_base.cpython-310.opt-1.pyc \ - build/pylib-android/concurrent/futures/__pycache__/process.cpython-310.opt-1.pyc \ - build/pylib-android/concurrent/futures/__pycache__/thread.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/configparser.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/contextlib.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/contextvars.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/copy.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/copyreg.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/crypt.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/csv.cpython-310.opt-1.pyc \ - build/pylib-android/ctypes/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/ctypes/__pycache__/_aix.cpython-310.opt-1.pyc \ - build/pylib-android/ctypes/__pycache__/_endian.cpython-310.opt-1.pyc \ - build/pylib-android/ctypes/macholib/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/ctypes/macholib/__pycache__/dyld.cpython-310.opt-1.pyc \ - build/pylib-android/ctypes/macholib/__pycache__/dylib.cpython-310.opt-1.pyc \ - build/pylib-android/ctypes/macholib/__pycache__/framework.cpython-310.opt-1.pyc \ - build/pylib-android/ctypes/__pycache__/util.cpython-310.opt-1.pyc \ - build/pylib-android/ctypes/__pycache__/wintypes.cpython-310.opt-1.pyc \ - build/pylib-android/curses/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/curses/__pycache__/ascii.cpython-310.opt-1.pyc \ - build/pylib-android/curses/__pycache__/has_key.cpython-310.opt-1.pyc \ - build/pylib-android/curses/__pycache__/panel.cpython-310.opt-1.pyc \ - build/pylib-android/curses/__pycache__/textpad.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/dataclasses.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/datetime.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/decimal.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/difflib.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/dis.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/doctest.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/_encoded_words.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/_header_value_parser.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/_parseaddr.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/_policybase.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/base64mime.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/charset.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/contentmanager.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/encoders.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/errors.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/feedparser.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/generator.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/header.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/headerregistry.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/iterators.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/message.cpython-310.opt-1.pyc \ - build/pylib-android/email/mime/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/email/mime/__pycache__/application.cpython-310.opt-1.pyc \ - build/pylib-android/email/mime/__pycache__/audio.cpython-310.opt-1.pyc \ - build/pylib-android/email/mime/__pycache__/base.cpython-310.opt-1.pyc \ - build/pylib-android/email/mime/__pycache__/image.cpython-310.opt-1.pyc \ - build/pylib-android/email/mime/__pycache__/message.cpython-310.opt-1.pyc \ - build/pylib-android/email/mime/__pycache__/multipart.cpython-310.opt-1.pyc \ - build/pylib-android/email/mime/__pycache__/nonmultipart.cpython-310.opt-1.pyc \ - build/pylib-android/email/mime/__pycache__/text.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/parser.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/policy.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/quoprimime.cpython-310.opt-1.pyc \ - build/pylib-android/email/__pycache__/utils.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/aliases.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/ascii.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/base64_codec.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/big5.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/big5hkscs.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/bz2_codec.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/charmap.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp037.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp1006.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp1026.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp1125.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp1140.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp1250.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp1251.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp1252.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp1253.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp1254.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp1255.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp1256.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp1257.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp1258.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp273.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp424.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp437.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp500.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp720.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp737.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp775.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp850.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp852.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp855.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp856.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp857.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp858.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp860.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp861.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp862.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp863.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp864.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp865.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp866.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp869.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp874.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp875.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp932.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp949.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/cp950.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/euc_jis_2004.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/euc_jisx0213.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/euc_jp.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/euc_kr.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/gb18030.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/gb2312.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/gbk.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/hex_codec.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/hp_roman8.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/hz.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/idna.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso2022_jp.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso2022_jp_1.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso2022_jp_2.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso2022_jp_2004.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso2022_jp_3.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso2022_jp_ext.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso2022_kr.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso8859_1.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso8859_10.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso8859_11.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso8859_13.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso8859_14.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso8859_15.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso8859_16.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso8859_2.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso8859_3.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso8859_4.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso8859_5.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso8859_6.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso8859_7.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso8859_8.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/iso8859_9.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/johab.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/koi8_r.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/koi8_t.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/koi8_u.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/kz1048.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/latin_1.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/mac_arabic.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/mac_croatian.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/mac_cyrillic.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/mac_farsi.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/mac_greek.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/mac_iceland.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/mac_latin2.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/mac_roman.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/mac_romanian.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/mac_turkish.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/mbcs.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/oem.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/palmos.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/ptcp154.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/punycode.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/quopri_codec.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/raw_unicode_escape.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/rot_13.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/shift_jis.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/shift_jis_2004.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/shift_jisx0213.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/tis_620.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/undefined.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/unicode_escape.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/utf_16.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/utf_16_be.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/utf_16_le.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/utf_32.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/utf_32_be.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/utf_32_le.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/utf_7.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/utf_8.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/utf_8_sig.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/uu_codec.cpython-310.opt-1.pyc \ - build/pylib-android/encodings/__pycache__/zlib_codec.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/enum.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/filecmp.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/fileinput.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/fnmatch.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/fractions.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/ftplib.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/functools.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/genericpath.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/getopt.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/getpass.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/gettext.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/glob.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/graphlib.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/gzip.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/hashlib.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/heapq.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/hmac.cpython-310.opt-1.pyc \ - build/pylib-android/html/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/html/__pycache__/entities.cpython-310.opt-1.pyc \ - build/pylib-android/html/__pycache__/parser.cpython-310.opt-1.pyc \ - build/pylib-android/http/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/http/__pycache__/client.cpython-310.opt-1.pyc \ - build/pylib-android/http/__pycache__/cookiejar.cpython-310.opt-1.pyc \ - build/pylib-android/http/__pycache__/cookies.cpython-310.opt-1.pyc \ - build/pylib-android/http/__pycache__/server.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/imghdr.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/imp.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/__pycache__/_abc.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/__pycache__/_adapters.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/__pycache__/_bootstrap.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/__pycache__/_bootstrap_external.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/__pycache__/_common.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/__pycache__/abc.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/__pycache__/machinery.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/metadata/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/metadata/__pycache__/_adapters.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/metadata/__pycache__/_collections.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/metadata/__pycache__/_functools.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/metadata/__pycache__/_itertools.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/metadata/__pycache__/_meta.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/metadata/__pycache__/_text.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/__pycache__/readers.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/__pycache__/resources.cpython-310.opt-1.pyc \ - build/pylib-android/importlib/__pycache__/util.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/inspect.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/io.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/ipaddress.cpython-310.opt-1.pyc \ - build/pylib-android/json/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/json/__pycache__/decoder.cpython-310.opt-1.pyc \ - build/pylib-android/json/__pycache__/encoder.cpython-310.opt-1.pyc \ - build/pylib-android/json/__pycache__/scanner.cpython-310.opt-1.pyc \ - build/pylib-android/json/__pycache__/tool.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/keyword.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/linecache.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/locale.cpython-310.opt-1.pyc \ - build/pylib-android/logging/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/logging/__pycache__/config.cpython-310.opt-1.pyc \ - build/pylib-android/logging/__pycache__/handlers.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/lzma.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/mailbox.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/mailcap.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/mimetypes.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/modulefinder.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/netrc.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/nntplib.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/ntpath.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/nturl2path.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/numbers.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/opcode.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/operator.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/optparse.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/os.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/pathlib.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/pdb.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/pickle.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/pickletools.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/pipes.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/pkgutil.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/platform.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/plistlib.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/poplib.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/posixpath.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/pprint.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/profile.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/pstats.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/pty.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/py_compile.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/pyclbr.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/pydoc.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/queue.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/quopri.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/random.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/re.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/reprlib.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/rlcompleter.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/runpy.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/sched.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/secrets.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/selectors.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/shelve.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/shlex.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/shutil.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/signal.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/site.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/smtpd.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/smtplib.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/sndhdr.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/socket.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/socketserver.cpython-310.opt-1.pyc \ - build/pylib-android/sqlite3/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/sqlite3/__pycache__/dbapi2.cpython-310.opt-1.pyc \ - build/pylib-android/sqlite3/__pycache__/dump.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/sre_compile.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/sre_constants.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/sre_parse.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/ssl.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/stat.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/statistics.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/string.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/stringprep.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/struct.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/subprocess.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/sunau.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/symtable.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/sysconfig.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/tabnanny.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/tarfile.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/telnetlib.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/tempfile.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/textwrap.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/this.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/threading.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/timeit.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/token.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/tokenize.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/trace.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/traceback.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/tracemalloc.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/tty.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/types.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/typing.cpython-310.opt-1.pyc \ - build/pylib-android/urllib/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/urllib/__pycache__/error.cpython-310.opt-1.pyc \ - build/pylib-android/urllib/__pycache__/parse.cpython-310.opt-1.pyc \ - build/pylib-android/urllib/__pycache__/request.cpython-310.opt-1.pyc \ - build/pylib-android/urllib/__pycache__/response.cpython-310.opt-1.pyc \ - build/pylib-android/urllib/__pycache__/robotparser.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/uu.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/uuid.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/warnings.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/wave.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/weakref.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/webbrowser.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/xdrlib.cpython-310.opt-1.pyc \ - build/pylib-android/xml/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/xml/dom/__pycache__/NodeFilter.cpython-310.opt-1.pyc \ - build/pylib-android/xml/dom/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/xml/dom/__pycache__/domreg.cpython-310.opt-1.pyc \ - build/pylib-android/xml/dom/__pycache__/expatbuilder.cpython-310.opt-1.pyc \ - build/pylib-android/xml/dom/__pycache__/minicompat.cpython-310.opt-1.pyc \ - build/pylib-android/xml/dom/__pycache__/minidom.cpython-310.opt-1.pyc \ - build/pylib-android/xml/dom/__pycache__/pulldom.cpython-310.opt-1.pyc \ - build/pylib-android/xml/dom/__pycache__/xmlbuilder.cpython-310.opt-1.pyc \ - build/pylib-android/xml/etree/__pycache__/ElementInclude.cpython-310.opt-1.pyc \ - build/pylib-android/xml/etree/__pycache__/ElementPath.cpython-310.opt-1.pyc \ - build/pylib-android/xml/etree/__pycache__/ElementTree.cpython-310.opt-1.pyc \ - build/pylib-android/xml/etree/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/xml/etree/__pycache__/cElementTree.cpython-310.opt-1.pyc \ - build/pylib-android/xml/parsers/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/xml/parsers/__pycache__/expat.cpython-310.opt-1.pyc \ - build/pylib-android/xml/sax/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/xml/sax/__pycache__/_exceptions.cpython-310.opt-1.pyc \ - build/pylib-android/xml/sax/__pycache__/expatreader.cpython-310.opt-1.pyc \ - build/pylib-android/xml/sax/__pycache__/handler.cpython-310.opt-1.pyc \ - build/pylib-android/xml/sax/__pycache__/saxutils.cpython-310.opt-1.pyc \ - build/pylib-android/xml/sax/__pycache__/xmlreader.cpython-310.opt-1.pyc \ - build/pylib-android/xmlrpc/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/xmlrpc/__pycache__/client.cpython-310.opt-1.pyc \ - build/pylib-android/xmlrpc/__pycache__/server.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/zipapp.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/zipfile.cpython-310.opt-1.pyc \ - build/pylib-android/__pycache__/zipimport.cpython-310.opt-1.pyc \ - build/pylib-android/zoneinfo/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/pylib-android/zoneinfo/__pycache__/_common.cpython-310.opt-1.pyc \ - build/pylib-android/zoneinfo/__pycache__/_tzpath.cpython-310.opt-1.pyc \ - build/pylib-android/zoneinfo/__pycache__/_zoneinfo.cpython-310.opt-1.pyc - -# Rule to copy src asset scripts to dst. -# (and make non-writable so I'm less likely to accidentally edit them there) -$(SCRIPT_TARGETS_PY_PRIVATE_ANDROID) : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -# These are too complex to define in a pattern rule; -# Instead we generate individual targets in a loop. -$(foreach element,$(SCRIPT_TARGETS_PYC_PRIVATE_ANDROID),\ -$(eval $(call make-opt-pyc-target,$(element)))) - -SCRIPT_TARGETS_PY_PRIVATE_COMMON = \ - build/ba_data/python-site-packages/_yaml/__init__.py \ - build/ba_data/python-site-packages/certifi/__init__.py \ - build/ba_data/python-site-packages/certifi/__main__.py \ - build/ba_data/python-site-packages/certifi/core.py \ - build/ba_data/python-site-packages/typing_extensions.py \ - build/ba_data/python-site-packages/yaml/__init__.py \ - build/ba_data/python-site-packages/yaml/composer.py \ - build/ba_data/python-site-packages/yaml/constructor.py \ - build/ba_data/python-site-packages/yaml/cyaml.py \ - build/ba_data/python-site-packages/yaml/dumper.py \ - build/ba_data/python-site-packages/yaml/emitter.py \ - build/ba_data/python-site-packages/yaml/error.py \ - build/ba_data/python-site-packages/yaml/events.py \ - build/ba_data/python-site-packages/yaml/loader.py \ - build/ba_data/python-site-packages/yaml/nodes.py \ - build/ba_data/python-site-packages/yaml/parser.py \ - build/ba_data/python-site-packages/yaml/reader.py \ - build/ba_data/python-site-packages/yaml/representer.py \ - build/ba_data/python-site-packages/yaml/resolver.py \ - build/ba_data/python-site-packages/yaml/scanner.py \ - build/ba_data/python-site-packages/yaml/serializer.py \ - build/ba_data/python-site-packages/yaml/tokens.py \ - build/workspace/ninjafightplug.py \ - build/workspace/onslaughtplug.py \ - build/workspace/runaroundplug.py - -SCRIPT_TARGETS_PYC_PRIVATE_COMMON = \ - build/ba_data/python-site-packages/_yaml/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/certifi/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/certifi/__pycache__/__main__.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/certifi/__pycache__/core.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/__pycache__/typing_extensions.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/composer.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/constructor.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/cyaml.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/dumper.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/emitter.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/error.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/events.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/loader.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/nodes.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/parser.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/reader.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/representer.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/resolver.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/scanner.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/serializer.cpython-310.opt-1.pyc \ - build/ba_data/python-site-packages/yaml/__pycache__/tokens.cpython-310.opt-1.pyc \ - build/workspace/__pycache__/ninjafightplug.cpython-310.opt-1.pyc \ - build/workspace/__pycache__/onslaughtplug.cpython-310.opt-1.pyc \ - build/workspace/__pycache__/runaroundplug.cpython-310.opt-1.pyc - -# Rule to copy src asset scripts to dst. -# (and make non-writable so I'm less likely to accidentally edit them there) -$(SCRIPT_TARGETS_PY_PRIVATE_COMMON) : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -# These are too complex to define in a pattern rule; -# Instead we generate individual targets in a loop. -$(foreach element,$(SCRIPT_TARGETS_PYC_PRIVATE_COMMON),\ -$(eval $(call make-opt-pyc-target,$(element)))) - -SCRIPT_TARGETS_PY_PRIVATE_WIN_WIN32 = \ - build/windows/Win32/Lib/__future__.py \ - build/windows/Win32/Lib/__phello__.foo.py \ - build/windows/Win32/Lib/_aix_support.py \ - build/windows/Win32/Lib/_bootsubprocess.py \ - build/windows/Win32/Lib/_collections_abc.py \ - build/windows/Win32/Lib/_compat_pickle.py \ - build/windows/Win32/Lib/_compression.py \ - build/windows/Win32/Lib/_markupbase.py \ - build/windows/Win32/Lib/_osx_support.py \ - build/windows/Win32/Lib/_py_abc.py \ - build/windows/Win32/Lib/_pydecimal.py \ - build/windows/Win32/Lib/_pyio.py \ - build/windows/Win32/Lib/_sitebuiltins.py \ - build/windows/Win32/Lib/_strptime.py \ - build/windows/Win32/Lib/_threading_local.py \ - build/windows/Win32/Lib/_weakrefset.py \ - build/windows/Win32/Lib/abc.py \ - build/windows/Win32/Lib/aifc.py \ - build/windows/Win32/Lib/antigravity.py \ - build/windows/Win32/Lib/argparse.py \ - build/windows/Win32/Lib/ast.py \ - build/windows/Win32/Lib/asynchat.py \ - build/windows/Win32/Lib/asyncio/__init__.py \ - build/windows/Win32/Lib/asyncio/__main__.py \ - build/windows/Win32/Lib/asyncio/base_events.py \ - build/windows/Win32/Lib/asyncio/base_futures.py \ - build/windows/Win32/Lib/asyncio/base_subprocess.py \ - build/windows/Win32/Lib/asyncio/base_tasks.py \ - build/windows/Win32/Lib/asyncio/constants.py \ - build/windows/Win32/Lib/asyncio/coroutines.py \ - build/windows/Win32/Lib/asyncio/events.py \ - build/windows/Win32/Lib/asyncio/exceptions.py \ - build/windows/Win32/Lib/asyncio/format_helpers.py \ - build/windows/Win32/Lib/asyncio/futures.py \ - build/windows/Win32/Lib/asyncio/locks.py \ - build/windows/Win32/Lib/asyncio/log.py \ - build/windows/Win32/Lib/asyncio/mixins.py \ - build/windows/Win32/Lib/asyncio/proactor_events.py \ - build/windows/Win32/Lib/asyncio/protocols.py \ - build/windows/Win32/Lib/asyncio/queues.py \ - build/windows/Win32/Lib/asyncio/runners.py \ - build/windows/Win32/Lib/asyncio/selector_events.py \ - build/windows/Win32/Lib/asyncio/sslproto.py \ - build/windows/Win32/Lib/asyncio/staggered.py \ - build/windows/Win32/Lib/asyncio/streams.py \ - build/windows/Win32/Lib/asyncio/subprocess.py \ - build/windows/Win32/Lib/asyncio/tasks.py \ - build/windows/Win32/Lib/asyncio/threads.py \ - build/windows/Win32/Lib/asyncio/transports.py \ - build/windows/Win32/Lib/asyncio/trsock.py \ - build/windows/Win32/Lib/asyncio/unix_events.py \ - build/windows/Win32/Lib/asyncio/windows_events.py \ - build/windows/Win32/Lib/asyncio/windows_utils.py \ - build/windows/Win32/Lib/asyncore.py \ - build/windows/Win32/Lib/base64.py \ - build/windows/Win32/Lib/bdb.py \ - build/windows/Win32/Lib/binhex.py \ - build/windows/Win32/Lib/bisect.py \ - build/windows/Win32/Lib/bz2.py \ - build/windows/Win32/Lib/cProfile.py \ - build/windows/Win32/Lib/calendar.py \ - build/windows/Win32/Lib/cgi.py \ - build/windows/Win32/Lib/cgitb.py \ - build/windows/Win32/Lib/chunk.py \ - build/windows/Win32/Lib/cmd.py \ - build/windows/Win32/Lib/code.py \ - build/windows/Win32/Lib/codecs.py \ - build/windows/Win32/Lib/codeop.py \ - build/windows/Win32/Lib/collections/__init__.py \ - build/windows/Win32/Lib/collections/abc.py \ - build/windows/Win32/Lib/colorsys.py \ - build/windows/Win32/Lib/compileall.py \ - build/windows/Win32/Lib/concurrent/__init__.py \ - build/windows/Win32/Lib/concurrent/futures/__init__.py \ - build/windows/Win32/Lib/concurrent/futures/_base.py \ - build/windows/Win32/Lib/concurrent/futures/process.py \ - build/windows/Win32/Lib/concurrent/futures/thread.py \ - build/windows/Win32/Lib/configparser.py \ - build/windows/Win32/Lib/contextlib.py \ - build/windows/Win32/Lib/contextvars.py \ - build/windows/Win32/Lib/copy.py \ - build/windows/Win32/Lib/copyreg.py \ - build/windows/Win32/Lib/crypt.py \ - build/windows/Win32/Lib/csv.py \ - build/windows/Win32/Lib/ctypes/__init__.py \ - build/windows/Win32/Lib/ctypes/_aix.py \ - build/windows/Win32/Lib/ctypes/_endian.py \ - build/windows/Win32/Lib/ctypes/macholib/__init__.py \ - build/windows/Win32/Lib/ctypes/macholib/dyld.py \ - build/windows/Win32/Lib/ctypes/macholib/dylib.py \ - build/windows/Win32/Lib/ctypes/macholib/framework.py \ - build/windows/Win32/Lib/ctypes/util.py \ - build/windows/Win32/Lib/ctypes/wintypes.py \ - build/windows/Win32/Lib/curses/__init__.py \ - build/windows/Win32/Lib/curses/ascii.py \ - build/windows/Win32/Lib/curses/has_key.py \ - build/windows/Win32/Lib/curses/panel.py \ - build/windows/Win32/Lib/curses/textpad.py \ - build/windows/Win32/Lib/dataclasses.py \ - build/windows/Win32/Lib/datetime.py \ - build/windows/Win32/Lib/decimal.py \ - build/windows/Win32/Lib/difflib.py \ - build/windows/Win32/Lib/dis.py \ - build/windows/Win32/Lib/doctest.py \ - build/windows/Win32/Lib/email/__init__.py \ - build/windows/Win32/Lib/email/_encoded_words.py \ - build/windows/Win32/Lib/email/_header_value_parser.py \ - build/windows/Win32/Lib/email/_parseaddr.py \ - build/windows/Win32/Lib/email/_policybase.py \ - build/windows/Win32/Lib/email/base64mime.py \ - build/windows/Win32/Lib/email/charset.py \ - build/windows/Win32/Lib/email/contentmanager.py \ - build/windows/Win32/Lib/email/encoders.py \ - build/windows/Win32/Lib/email/errors.py \ - build/windows/Win32/Lib/email/feedparser.py \ - build/windows/Win32/Lib/email/generator.py \ - build/windows/Win32/Lib/email/header.py \ - build/windows/Win32/Lib/email/headerregistry.py \ - build/windows/Win32/Lib/email/iterators.py \ - build/windows/Win32/Lib/email/message.py \ - build/windows/Win32/Lib/email/mime/__init__.py \ - build/windows/Win32/Lib/email/mime/application.py \ - build/windows/Win32/Lib/email/mime/audio.py \ - build/windows/Win32/Lib/email/mime/base.py \ - build/windows/Win32/Lib/email/mime/image.py \ - build/windows/Win32/Lib/email/mime/message.py \ - build/windows/Win32/Lib/email/mime/multipart.py \ - build/windows/Win32/Lib/email/mime/nonmultipart.py \ - build/windows/Win32/Lib/email/mime/text.py \ - build/windows/Win32/Lib/email/parser.py \ - build/windows/Win32/Lib/email/policy.py \ - build/windows/Win32/Lib/email/quoprimime.py \ - build/windows/Win32/Lib/email/utils.py \ - build/windows/Win32/Lib/encodings/__init__.py \ - build/windows/Win32/Lib/encodings/aliases.py \ - build/windows/Win32/Lib/encodings/ascii.py \ - build/windows/Win32/Lib/encodings/base64_codec.py \ - build/windows/Win32/Lib/encodings/big5.py \ - build/windows/Win32/Lib/encodings/big5hkscs.py \ - build/windows/Win32/Lib/encodings/bz2_codec.py \ - build/windows/Win32/Lib/encodings/charmap.py \ - build/windows/Win32/Lib/encodings/cp037.py \ - build/windows/Win32/Lib/encodings/cp1006.py \ - build/windows/Win32/Lib/encodings/cp1026.py \ - build/windows/Win32/Lib/encodings/cp1125.py \ - build/windows/Win32/Lib/encodings/cp1140.py \ - build/windows/Win32/Lib/encodings/cp1250.py \ - build/windows/Win32/Lib/encodings/cp1251.py \ - build/windows/Win32/Lib/encodings/cp1252.py \ - build/windows/Win32/Lib/encodings/cp1253.py \ - build/windows/Win32/Lib/encodings/cp1254.py \ - build/windows/Win32/Lib/encodings/cp1255.py \ - build/windows/Win32/Lib/encodings/cp1256.py \ - build/windows/Win32/Lib/encodings/cp1257.py \ - build/windows/Win32/Lib/encodings/cp1258.py \ - build/windows/Win32/Lib/encodings/cp273.py \ - build/windows/Win32/Lib/encodings/cp424.py \ - build/windows/Win32/Lib/encodings/cp437.py \ - build/windows/Win32/Lib/encodings/cp500.py \ - build/windows/Win32/Lib/encodings/cp720.py \ - build/windows/Win32/Lib/encodings/cp737.py \ - build/windows/Win32/Lib/encodings/cp775.py \ - build/windows/Win32/Lib/encodings/cp850.py \ - build/windows/Win32/Lib/encodings/cp852.py \ - build/windows/Win32/Lib/encodings/cp855.py \ - build/windows/Win32/Lib/encodings/cp856.py \ - build/windows/Win32/Lib/encodings/cp857.py \ - build/windows/Win32/Lib/encodings/cp858.py \ - build/windows/Win32/Lib/encodings/cp860.py \ - build/windows/Win32/Lib/encodings/cp861.py \ - build/windows/Win32/Lib/encodings/cp862.py \ - build/windows/Win32/Lib/encodings/cp863.py \ - build/windows/Win32/Lib/encodings/cp864.py \ - build/windows/Win32/Lib/encodings/cp865.py \ - build/windows/Win32/Lib/encodings/cp866.py \ - build/windows/Win32/Lib/encodings/cp869.py \ - build/windows/Win32/Lib/encodings/cp874.py \ - build/windows/Win32/Lib/encodings/cp875.py \ - build/windows/Win32/Lib/encodings/cp932.py \ - build/windows/Win32/Lib/encodings/cp949.py \ - build/windows/Win32/Lib/encodings/cp950.py \ - build/windows/Win32/Lib/encodings/euc_jis_2004.py \ - build/windows/Win32/Lib/encodings/euc_jisx0213.py \ - build/windows/Win32/Lib/encodings/euc_jp.py \ - build/windows/Win32/Lib/encodings/euc_kr.py \ - build/windows/Win32/Lib/encodings/gb18030.py \ - build/windows/Win32/Lib/encodings/gb2312.py \ - build/windows/Win32/Lib/encodings/gbk.py \ - build/windows/Win32/Lib/encodings/hex_codec.py \ - build/windows/Win32/Lib/encodings/hp_roman8.py \ - build/windows/Win32/Lib/encodings/hz.py \ - build/windows/Win32/Lib/encodings/idna.py \ - build/windows/Win32/Lib/encodings/iso2022_jp.py \ - build/windows/Win32/Lib/encodings/iso2022_jp_1.py \ - build/windows/Win32/Lib/encodings/iso2022_jp_2.py \ - build/windows/Win32/Lib/encodings/iso2022_jp_2004.py \ - build/windows/Win32/Lib/encodings/iso2022_jp_3.py \ - build/windows/Win32/Lib/encodings/iso2022_jp_ext.py \ - build/windows/Win32/Lib/encodings/iso2022_kr.py \ - build/windows/Win32/Lib/encodings/iso8859_1.py \ - build/windows/Win32/Lib/encodings/iso8859_10.py \ - build/windows/Win32/Lib/encodings/iso8859_11.py \ - build/windows/Win32/Lib/encodings/iso8859_13.py \ - build/windows/Win32/Lib/encodings/iso8859_14.py \ - build/windows/Win32/Lib/encodings/iso8859_15.py \ - build/windows/Win32/Lib/encodings/iso8859_16.py \ - build/windows/Win32/Lib/encodings/iso8859_2.py \ - build/windows/Win32/Lib/encodings/iso8859_3.py \ - build/windows/Win32/Lib/encodings/iso8859_4.py \ - build/windows/Win32/Lib/encodings/iso8859_5.py \ - build/windows/Win32/Lib/encodings/iso8859_6.py \ - build/windows/Win32/Lib/encodings/iso8859_7.py \ - build/windows/Win32/Lib/encodings/iso8859_8.py \ - build/windows/Win32/Lib/encodings/iso8859_9.py \ - build/windows/Win32/Lib/encodings/johab.py \ - build/windows/Win32/Lib/encodings/koi8_r.py \ - build/windows/Win32/Lib/encodings/koi8_t.py \ - build/windows/Win32/Lib/encodings/koi8_u.py \ - build/windows/Win32/Lib/encodings/kz1048.py \ - build/windows/Win32/Lib/encodings/latin_1.py \ - build/windows/Win32/Lib/encodings/mac_arabic.py \ - build/windows/Win32/Lib/encodings/mac_croatian.py \ - build/windows/Win32/Lib/encodings/mac_cyrillic.py \ - build/windows/Win32/Lib/encodings/mac_farsi.py \ - build/windows/Win32/Lib/encodings/mac_greek.py \ - build/windows/Win32/Lib/encodings/mac_iceland.py \ - build/windows/Win32/Lib/encodings/mac_latin2.py \ - build/windows/Win32/Lib/encodings/mac_roman.py \ - build/windows/Win32/Lib/encodings/mac_romanian.py \ - build/windows/Win32/Lib/encodings/mac_turkish.py \ - build/windows/Win32/Lib/encodings/mbcs.py \ - build/windows/Win32/Lib/encodings/oem.py \ - build/windows/Win32/Lib/encodings/palmos.py \ - build/windows/Win32/Lib/encodings/ptcp154.py \ - build/windows/Win32/Lib/encodings/punycode.py \ - build/windows/Win32/Lib/encodings/quopri_codec.py \ - build/windows/Win32/Lib/encodings/raw_unicode_escape.py \ - build/windows/Win32/Lib/encodings/rot_13.py \ - build/windows/Win32/Lib/encodings/shift_jis.py \ - build/windows/Win32/Lib/encodings/shift_jis_2004.py \ - build/windows/Win32/Lib/encodings/shift_jisx0213.py \ - build/windows/Win32/Lib/encodings/tis_620.py \ - build/windows/Win32/Lib/encodings/undefined.py \ - build/windows/Win32/Lib/encodings/unicode_escape.py \ - build/windows/Win32/Lib/encodings/utf_16.py \ - build/windows/Win32/Lib/encodings/utf_16_be.py \ - build/windows/Win32/Lib/encodings/utf_16_le.py \ - build/windows/Win32/Lib/encodings/utf_32.py \ - build/windows/Win32/Lib/encodings/utf_32_be.py \ - build/windows/Win32/Lib/encodings/utf_32_le.py \ - build/windows/Win32/Lib/encodings/utf_7.py \ - build/windows/Win32/Lib/encodings/utf_8.py \ - build/windows/Win32/Lib/encodings/utf_8_sig.py \ - build/windows/Win32/Lib/encodings/uu_codec.py \ - build/windows/Win32/Lib/encodings/zlib_codec.py \ - build/windows/Win32/Lib/enum.py \ - build/windows/Win32/Lib/filecmp.py \ - build/windows/Win32/Lib/fileinput.py \ - build/windows/Win32/Lib/fnmatch.py \ - build/windows/Win32/Lib/fractions.py \ - build/windows/Win32/Lib/ftplib.py \ - build/windows/Win32/Lib/functools.py \ - build/windows/Win32/Lib/genericpath.py \ - build/windows/Win32/Lib/getopt.py \ - build/windows/Win32/Lib/getpass.py \ - build/windows/Win32/Lib/gettext.py \ - build/windows/Win32/Lib/glob.py \ - build/windows/Win32/Lib/graphlib.py \ - build/windows/Win32/Lib/gzip.py \ - build/windows/Win32/Lib/hashlib.py \ - build/windows/Win32/Lib/heapq.py \ - build/windows/Win32/Lib/hmac.py \ - build/windows/Win32/Lib/html/__init__.py \ - build/windows/Win32/Lib/html/entities.py \ - build/windows/Win32/Lib/html/parser.py \ - build/windows/Win32/Lib/http/__init__.py \ - build/windows/Win32/Lib/http/client.py \ - build/windows/Win32/Lib/http/cookiejar.py \ - build/windows/Win32/Lib/http/cookies.py \ - build/windows/Win32/Lib/http/server.py \ - build/windows/Win32/Lib/imghdr.py \ - build/windows/Win32/Lib/imp.py \ - build/windows/Win32/Lib/importlib/__init__.py \ - build/windows/Win32/Lib/importlib/_abc.py \ - build/windows/Win32/Lib/importlib/_adapters.py \ - build/windows/Win32/Lib/importlib/_bootstrap.py \ - build/windows/Win32/Lib/importlib/_bootstrap_external.py \ - build/windows/Win32/Lib/importlib/_common.py \ - build/windows/Win32/Lib/importlib/abc.py \ - build/windows/Win32/Lib/importlib/machinery.py \ - build/windows/Win32/Lib/importlib/metadata/__init__.py \ - build/windows/Win32/Lib/importlib/metadata/_adapters.py \ - build/windows/Win32/Lib/importlib/metadata/_collections.py \ - build/windows/Win32/Lib/importlib/metadata/_functools.py \ - build/windows/Win32/Lib/importlib/metadata/_itertools.py \ - build/windows/Win32/Lib/importlib/metadata/_meta.py \ - build/windows/Win32/Lib/importlib/metadata/_text.py \ - build/windows/Win32/Lib/importlib/readers.py \ - build/windows/Win32/Lib/importlib/resources.py \ - build/windows/Win32/Lib/importlib/util.py \ - build/windows/Win32/Lib/inspect.py \ - build/windows/Win32/Lib/io.py \ - build/windows/Win32/Lib/ipaddress.py \ - build/windows/Win32/Lib/json/__init__.py \ - build/windows/Win32/Lib/json/decoder.py \ - build/windows/Win32/Lib/json/encoder.py \ - build/windows/Win32/Lib/json/scanner.py \ - build/windows/Win32/Lib/json/tool.py \ - build/windows/Win32/Lib/keyword.py \ - build/windows/Win32/Lib/linecache.py \ - build/windows/Win32/Lib/locale.py \ - build/windows/Win32/Lib/logging/__init__.py \ - build/windows/Win32/Lib/logging/config.py \ - build/windows/Win32/Lib/logging/handlers.py \ - build/windows/Win32/Lib/lzma.py \ - build/windows/Win32/Lib/mailbox.py \ - build/windows/Win32/Lib/mailcap.py \ - build/windows/Win32/Lib/mimetypes.py \ - build/windows/Win32/Lib/modulefinder.py \ - build/windows/Win32/Lib/msilib/__init__.py \ - build/windows/Win32/Lib/msilib/schema.py \ - build/windows/Win32/Lib/msilib/sequence.py \ - build/windows/Win32/Lib/msilib/text.py \ - build/windows/Win32/Lib/netrc.py \ - build/windows/Win32/Lib/nntplib.py \ - build/windows/Win32/Lib/ntpath.py \ - build/windows/Win32/Lib/nturl2path.py \ - build/windows/Win32/Lib/numbers.py \ - build/windows/Win32/Lib/opcode.py \ - build/windows/Win32/Lib/operator.py \ - build/windows/Win32/Lib/optparse.py \ - build/windows/Win32/Lib/os.py \ - build/windows/Win32/Lib/pathlib.py \ - build/windows/Win32/Lib/pdb.py \ - build/windows/Win32/Lib/pickle.py \ - build/windows/Win32/Lib/pickletools.py \ - build/windows/Win32/Lib/pipes.py \ - build/windows/Win32/Lib/pkgutil.py \ - build/windows/Win32/Lib/platform.py \ - build/windows/Win32/Lib/plistlib.py \ - build/windows/Win32/Lib/poplib.py \ - build/windows/Win32/Lib/posixpath.py \ - build/windows/Win32/Lib/pprint.py \ - build/windows/Win32/Lib/profile.py \ - build/windows/Win32/Lib/pstats.py \ - build/windows/Win32/Lib/pty.py \ - build/windows/Win32/Lib/py_compile.py \ - build/windows/Win32/Lib/pyclbr.py \ - build/windows/Win32/Lib/pydoc.py \ - build/windows/Win32/Lib/queue.py \ - build/windows/Win32/Lib/quopri.py \ - build/windows/Win32/Lib/random.py \ - build/windows/Win32/Lib/re.py \ - build/windows/Win32/Lib/reprlib.py \ - build/windows/Win32/Lib/rlcompleter.py \ - build/windows/Win32/Lib/runpy.py \ - build/windows/Win32/Lib/sched.py \ - build/windows/Win32/Lib/secrets.py \ - build/windows/Win32/Lib/selectors.py \ - build/windows/Win32/Lib/shelve.py \ - build/windows/Win32/Lib/shlex.py \ - build/windows/Win32/Lib/shutil.py \ - build/windows/Win32/Lib/signal.py \ - build/windows/Win32/Lib/site.py \ - build/windows/Win32/Lib/smtpd.py \ - build/windows/Win32/Lib/smtplib.py \ - build/windows/Win32/Lib/sndhdr.py \ - build/windows/Win32/Lib/socket.py \ - build/windows/Win32/Lib/socketserver.py \ - build/windows/Win32/Lib/sqlite3/__init__.py \ - build/windows/Win32/Lib/sqlite3/dbapi2.py \ - build/windows/Win32/Lib/sqlite3/dump.py \ - build/windows/Win32/Lib/sre_compile.py \ - build/windows/Win32/Lib/sre_constants.py \ - build/windows/Win32/Lib/sre_parse.py \ - build/windows/Win32/Lib/ssl.py \ - build/windows/Win32/Lib/stat.py \ - build/windows/Win32/Lib/statistics.py \ - build/windows/Win32/Lib/string.py \ - build/windows/Win32/Lib/stringprep.py \ - build/windows/Win32/Lib/struct.py \ - build/windows/Win32/Lib/subprocess.py \ - build/windows/Win32/Lib/sunau.py \ - build/windows/Win32/Lib/symtable.py \ - build/windows/Win32/Lib/sysconfig.py \ - build/windows/Win32/Lib/tabnanny.py \ - build/windows/Win32/Lib/tarfile.py \ - build/windows/Win32/Lib/telnetlib.py \ - build/windows/Win32/Lib/tempfile.py \ - build/windows/Win32/Lib/textwrap.py \ - build/windows/Win32/Lib/this.py \ - build/windows/Win32/Lib/threading.py \ - build/windows/Win32/Lib/timeit.py \ - build/windows/Win32/Lib/token.py \ - build/windows/Win32/Lib/tokenize.py \ - build/windows/Win32/Lib/trace.py \ - build/windows/Win32/Lib/traceback.py \ - build/windows/Win32/Lib/tracemalloc.py \ - build/windows/Win32/Lib/tty.py \ - build/windows/Win32/Lib/types.py \ - build/windows/Win32/Lib/typing.py \ - build/windows/Win32/Lib/urllib/__init__.py \ - build/windows/Win32/Lib/urllib/error.py \ - build/windows/Win32/Lib/urllib/parse.py \ - build/windows/Win32/Lib/urllib/request.py \ - build/windows/Win32/Lib/urllib/response.py \ - build/windows/Win32/Lib/urllib/robotparser.py \ - build/windows/Win32/Lib/uu.py \ - build/windows/Win32/Lib/uuid.py \ - build/windows/Win32/Lib/warnings.py \ - build/windows/Win32/Lib/wave.py \ - build/windows/Win32/Lib/weakref.py \ - build/windows/Win32/Lib/webbrowser.py \ - build/windows/Win32/Lib/xdrlib.py \ - build/windows/Win32/Lib/xml/__init__.py \ - build/windows/Win32/Lib/xml/dom/NodeFilter.py \ - build/windows/Win32/Lib/xml/dom/__init__.py \ - build/windows/Win32/Lib/xml/dom/domreg.py \ - build/windows/Win32/Lib/xml/dom/expatbuilder.py \ - build/windows/Win32/Lib/xml/dom/minicompat.py \ - build/windows/Win32/Lib/xml/dom/minidom.py \ - build/windows/Win32/Lib/xml/dom/pulldom.py \ - build/windows/Win32/Lib/xml/dom/xmlbuilder.py \ - build/windows/Win32/Lib/xml/etree/ElementInclude.py \ - build/windows/Win32/Lib/xml/etree/ElementPath.py \ - build/windows/Win32/Lib/xml/etree/ElementTree.py \ - build/windows/Win32/Lib/xml/etree/__init__.py \ - build/windows/Win32/Lib/xml/etree/cElementTree.py \ - build/windows/Win32/Lib/xml/parsers/__init__.py \ - build/windows/Win32/Lib/xml/parsers/expat.py \ - build/windows/Win32/Lib/xml/sax/__init__.py \ - build/windows/Win32/Lib/xml/sax/_exceptions.py \ - build/windows/Win32/Lib/xml/sax/expatreader.py \ - build/windows/Win32/Lib/xml/sax/handler.py \ - build/windows/Win32/Lib/xml/sax/saxutils.py \ - build/windows/Win32/Lib/xml/sax/xmlreader.py \ - build/windows/Win32/Lib/xmlrpc/__init__.py \ - build/windows/Win32/Lib/xmlrpc/client.py \ - build/windows/Win32/Lib/xmlrpc/server.py \ - build/windows/Win32/Lib/zipapp.py \ - build/windows/Win32/Lib/zipfile.py \ - build/windows/Win32/Lib/zipimport.py \ - build/windows/Win32/Lib/zoneinfo/__init__.py \ - build/windows/Win32/Lib/zoneinfo/_common.py \ - build/windows/Win32/Lib/zoneinfo/_tzpath.py \ - build/windows/Win32/Lib/zoneinfo/_zoneinfo.py - -SCRIPT_TARGETS_PYC_PRIVATE_WIN_WIN32 = \ - build/windows/Win32/Lib/__pycache__/__future__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/__phello__.foo.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/_aix_support.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/_bootsubprocess.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/_collections_abc.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/_compat_pickle.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/_compression.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/_markupbase.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/_osx_support.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/_py_abc.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/_pydecimal.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/_pyio.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/_sitebuiltins.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/_strptime.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/_threading_local.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/_weakrefset.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/abc.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/aifc.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/antigravity.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/argparse.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/ast.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/asynchat.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/__main__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/base_events.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/base_futures.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/base_subprocess.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/base_tasks.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/constants.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/coroutines.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/events.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/exceptions.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/format_helpers.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/futures.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/locks.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/log.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/mixins.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/proactor_events.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/protocols.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/queues.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/runners.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/selector_events.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/sslproto.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/staggered.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/streams.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/subprocess.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/tasks.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/threads.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/transports.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/trsock.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/unix_events.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/windows_events.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/asyncio/__pycache__/windows_utils.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/asyncore.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/base64.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/bdb.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/binhex.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/bisect.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/bz2.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/cProfile.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/calendar.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/cgi.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/cgitb.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/chunk.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/cmd.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/code.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/codecs.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/codeop.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/collections/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/collections/__pycache__/abc.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/colorsys.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/compileall.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/concurrent/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/concurrent/futures/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/concurrent/futures/__pycache__/_base.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/concurrent/futures/__pycache__/process.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/concurrent/futures/__pycache__/thread.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/configparser.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/contextlib.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/contextvars.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/copy.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/copyreg.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/crypt.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/csv.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/ctypes/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/ctypes/__pycache__/_aix.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/ctypes/__pycache__/_endian.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/ctypes/macholib/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/ctypes/macholib/__pycache__/dyld.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/ctypes/macholib/__pycache__/dylib.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/ctypes/macholib/__pycache__/framework.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/ctypes/__pycache__/util.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/ctypes/__pycache__/wintypes.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/curses/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/curses/__pycache__/ascii.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/curses/__pycache__/has_key.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/curses/__pycache__/panel.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/curses/__pycache__/textpad.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/dataclasses.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/datetime.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/decimal.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/difflib.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/dis.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/doctest.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/_encoded_words.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/_header_value_parser.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/_parseaddr.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/_policybase.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/base64mime.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/charset.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/contentmanager.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/encoders.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/errors.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/feedparser.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/generator.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/header.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/headerregistry.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/iterators.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/message.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/mime/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/mime/__pycache__/application.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/mime/__pycache__/audio.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/mime/__pycache__/base.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/mime/__pycache__/image.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/mime/__pycache__/message.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/mime/__pycache__/multipart.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/mime/__pycache__/nonmultipart.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/mime/__pycache__/text.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/parser.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/policy.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/quoprimime.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/email/__pycache__/utils.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/aliases.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/ascii.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/base64_codec.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/big5.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/big5hkscs.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/bz2_codec.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/charmap.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp037.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp1006.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp1026.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp1125.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp1140.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp1250.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp1251.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp1252.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp1253.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp1254.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp1255.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp1256.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp1257.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp1258.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp273.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp424.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp437.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp500.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp720.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp737.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp775.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp850.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp852.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp855.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp856.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp857.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp858.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp860.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp861.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp862.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp863.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp864.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp865.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp866.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp869.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp874.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp875.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp932.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp949.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/cp950.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/euc_jis_2004.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/euc_jisx0213.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/euc_jp.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/euc_kr.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/gb18030.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/gb2312.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/gbk.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/hex_codec.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/hp_roman8.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/hz.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/idna.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso2022_jp.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso2022_jp_1.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso2022_jp_2.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso2022_jp_2004.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso2022_jp_3.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso2022_jp_ext.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso2022_kr.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso8859_1.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso8859_10.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso8859_11.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso8859_13.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso8859_14.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso8859_15.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso8859_16.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso8859_2.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso8859_3.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso8859_4.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso8859_5.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso8859_6.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso8859_7.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso8859_8.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/iso8859_9.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/johab.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/koi8_r.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/koi8_t.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/koi8_u.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/kz1048.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/latin_1.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/mac_arabic.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/mac_croatian.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/mac_cyrillic.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/mac_farsi.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/mac_greek.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/mac_iceland.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/mac_latin2.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/mac_roman.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/mac_romanian.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/mac_turkish.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/mbcs.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/oem.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/palmos.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/ptcp154.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/punycode.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/quopri_codec.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/raw_unicode_escape.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/rot_13.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/shift_jis.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/shift_jis_2004.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/shift_jisx0213.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/tis_620.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/undefined.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/unicode_escape.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/utf_16.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/utf_16_be.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/utf_16_le.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/utf_32.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/utf_32_be.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/utf_32_le.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/utf_7.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/utf_8.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/utf_8_sig.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/uu_codec.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/encodings/__pycache__/zlib_codec.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/enum.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/filecmp.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/fileinput.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/fnmatch.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/fractions.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/ftplib.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/functools.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/genericpath.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/getopt.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/getpass.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/gettext.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/glob.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/graphlib.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/gzip.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/hashlib.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/heapq.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/hmac.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/html/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/html/__pycache__/entities.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/html/__pycache__/parser.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/http/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/http/__pycache__/client.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/http/__pycache__/cookiejar.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/http/__pycache__/cookies.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/http/__pycache__/server.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/imghdr.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/imp.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/__pycache__/_abc.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/__pycache__/_adapters.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/__pycache__/_bootstrap.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/__pycache__/_bootstrap_external.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/__pycache__/_common.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/__pycache__/abc.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/__pycache__/machinery.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/metadata/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/metadata/__pycache__/_adapters.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/metadata/__pycache__/_collections.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/metadata/__pycache__/_functools.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/metadata/__pycache__/_itertools.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/metadata/__pycache__/_meta.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/metadata/__pycache__/_text.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/__pycache__/readers.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/__pycache__/resources.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/importlib/__pycache__/util.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/inspect.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/io.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/ipaddress.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/json/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/json/__pycache__/decoder.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/json/__pycache__/encoder.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/json/__pycache__/scanner.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/json/__pycache__/tool.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/keyword.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/linecache.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/locale.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/logging/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/logging/__pycache__/config.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/logging/__pycache__/handlers.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/lzma.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/mailbox.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/mailcap.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/mimetypes.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/modulefinder.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/msilib/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/msilib/__pycache__/schema.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/msilib/__pycache__/sequence.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/msilib/__pycache__/text.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/netrc.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/nntplib.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/ntpath.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/nturl2path.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/numbers.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/opcode.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/operator.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/optparse.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/os.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/pathlib.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/pdb.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/pickle.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/pickletools.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/pipes.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/pkgutil.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/platform.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/plistlib.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/poplib.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/posixpath.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/pprint.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/profile.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/pstats.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/pty.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/py_compile.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/pyclbr.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/pydoc.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/queue.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/quopri.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/random.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/re.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/reprlib.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/rlcompleter.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/runpy.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/sched.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/secrets.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/selectors.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/shelve.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/shlex.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/shutil.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/signal.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/site.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/smtpd.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/smtplib.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/sndhdr.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/socket.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/socketserver.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/sqlite3/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/sqlite3/__pycache__/dbapi2.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/sqlite3/__pycache__/dump.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/sre_compile.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/sre_constants.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/sre_parse.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/ssl.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/stat.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/statistics.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/string.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/stringprep.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/struct.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/subprocess.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/sunau.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/symtable.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/sysconfig.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/tabnanny.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/tarfile.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/telnetlib.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/tempfile.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/textwrap.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/this.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/threading.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/timeit.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/token.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/tokenize.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/trace.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/traceback.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/tracemalloc.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/tty.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/types.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/typing.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/urllib/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/urllib/__pycache__/error.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/urllib/__pycache__/parse.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/urllib/__pycache__/request.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/urllib/__pycache__/response.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/urllib/__pycache__/robotparser.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/uu.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/uuid.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/warnings.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/wave.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/weakref.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/webbrowser.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/xdrlib.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/dom/__pycache__/NodeFilter.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/dom/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/dom/__pycache__/domreg.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/dom/__pycache__/expatbuilder.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/dom/__pycache__/minicompat.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/dom/__pycache__/minidom.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/dom/__pycache__/pulldom.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/dom/__pycache__/xmlbuilder.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/etree/__pycache__/ElementInclude.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/etree/__pycache__/ElementPath.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/etree/__pycache__/ElementTree.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/etree/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/etree/__pycache__/cElementTree.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/parsers/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/parsers/__pycache__/expat.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/sax/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/sax/__pycache__/_exceptions.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/sax/__pycache__/expatreader.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/sax/__pycache__/handler.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/sax/__pycache__/saxutils.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xml/sax/__pycache__/xmlreader.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xmlrpc/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xmlrpc/__pycache__/client.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/xmlrpc/__pycache__/server.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/zipapp.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/zipfile.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/__pycache__/zipimport.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/zoneinfo/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/zoneinfo/__pycache__/_common.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/zoneinfo/__pycache__/_tzpath.cpython-310.opt-1.pyc \ - build/windows/Win32/Lib/zoneinfo/__pycache__/_zoneinfo.cpython-310.opt-1.pyc - -# Rule to copy src asset scripts to dst. -# (and make non-writable so I'm less likely to accidentally edit them there) -$(SCRIPT_TARGETS_PY_PRIVATE_WIN_WIN32) : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -# These are too complex to define in a pattern rule; -# Instead we generate individual targets in a loop. -$(foreach element,$(SCRIPT_TARGETS_PYC_PRIVATE_WIN_WIN32),\ -$(eval $(call make-opt-pyc-target,$(element)))) - -SCRIPT_TARGETS_PY_PRIVATE_WIN_X64 = \ - build/windows/x64/Lib/__future__.py \ - build/windows/x64/Lib/__phello__.foo.py \ - build/windows/x64/Lib/_aix_support.py \ - build/windows/x64/Lib/_bootsubprocess.py \ - build/windows/x64/Lib/_collections_abc.py \ - build/windows/x64/Lib/_compat_pickle.py \ - build/windows/x64/Lib/_compression.py \ - build/windows/x64/Lib/_markupbase.py \ - build/windows/x64/Lib/_osx_support.py \ - build/windows/x64/Lib/_py_abc.py \ - build/windows/x64/Lib/_pydecimal.py \ - build/windows/x64/Lib/_pyio.py \ - build/windows/x64/Lib/_sitebuiltins.py \ - build/windows/x64/Lib/_strptime.py \ - build/windows/x64/Lib/_threading_local.py \ - build/windows/x64/Lib/_weakrefset.py \ - build/windows/x64/Lib/abc.py \ - build/windows/x64/Lib/aifc.py \ - build/windows/x64/Lib/antigravity.py \ - build/windows/x64/Lib/argparse.py \ - build/windows/x64/Lib/ast.py \ - build/windows/x64/Lib/asynchat.py \ - build/windows/x64/Lib/asyncio/__init__.py \ - build/windows/x64/Lib/asyncio/__main__.py \ - build/windows/x64/Lib/asyncio/base_events.py \ - build/windows/x64/Lib/asyncio/base_futures.py \ - build/windows/x64/Lib/asyncio/base_subprocess.py \ - build/windows/x64/Lib/asyncio/base_tasks.py \ - build/windows/x64/Lib/asyncio/constants.py \ - build/windows/x64/Lib/asyncio/coroutines.py \ - build/windows/x64/Lib/asyncio/events.py \ - build/windows/x64/Lib/asyncio/exceptions.py \ - build/windows/x64/Lib/asyncio/format_helpers.py \ - build/windows/x64/Lib/asyncio/futures.py \ - build/windows/x64/Lib/asyncio/locks.py \ - build/windows/x64/Lib/asyncio/log.py \ - build/windows/x64/Lib/asyncio/mixins.py \ - build/windows/x64/Lib/asyncio/proactor_events.py \ - build/windows/x64/Lib/asyncio/protocols.py \ - build/windows/x64/Lib/asyncio/queues.py \ - build/windows/x64/Lib/asyncio/runners.py \ - build/windows/x64/Lib/asyncio/selector_events.py \ - build/windows/x64/Lib/asyncio/sslproto.py \ - build/windows/x64/Lib/asyncio/staggered.py \ - build/windows/x64/Lib/asyncio/streams.py \ - build/windows/x64/Lib/asyncio/subprocess.py \ - build/windows/x64/Lib/asyncio/tasks.py \ - build/windows/x64/Lib/asyncio/threads.py \ - build/windows/x64/Lib/asyncio/transports.py \ - build/windows/x64/Lib/asyncio/trsock.py \ - build/windows/x64/Lib/asyncio/unix_events.py \ - build/windows/x64/Lib/asyncio/windows_events.py \ - build/windows/x64/Lib/asyncio/windows_utils.py \ - build/windows/x64/Lib/asyncore.py \ - build/windows/x64/Lib/base64.py \ - build/windows/x64/Lib/bdb.py \ - build/windows/x64/Lib/binhex.py \ - build/windows/x64/Lib/bisect.py \ - build/windows/x64/Lib/bz2.py \ - build/windows/x64/Lib/cProfile.py \ - build/windows/x64/Lib/calendar.py \ - build/windows/x64/Lib/cgi.py \ - build/windows/x64/Lib/cgitb.py \ - build/windows/x64/Lib/chunk.py \ - build/windows/x64/Lib/cmd.py \ - build/windows/x64/Lib/code.py \ - build/windows/x64/Lib/codecs.py \ - build/windows/x64/Lib/codeop.py \ - build/windows/x64/Lib/collections/__init__.py \ - build/windows/x64/Lib/collections/abc.py \ - build/windows/x64/Lib/colorsys.py \ - build/windows/x64/Lib/compileall.py \ - build/windows/x64/Lib/concurrent/__init__.py \ - build/windows/x64/Lib/concurrent/futures/__init__.py \ - build/windows/x64/Lib/concurrent/futures/_base.py \ - build/windows/x64/Lib/concurrent/futures/process.py \ - build/windows/x64/Lib/concurrent/futures/thread.py \ - build/windows/x64/Lib/configparser.py \ - build/windows/x64/Lib/contextlib.py \ - build/windows/x64/Lib/contextvars.py \ - build/windows/x64/Lib/copy.py \ - build/windows/x64/Lib/copyreg.py \ - build/windows/x64/Lib/crypt.py \ - build/windows/x64/Lib/csv.py \ - build/windows/x64/Lib/ctypes/__init__.py \ - build/windows/x64/Lib/ctypes/_aix.py \ - build/windows/x64/Lib/ctypes/_endian.py \ - build/windows/x64/Lib/ctypes/macholib/__init__.py \ - build/windows/x64/Lib/ctypes/macholib/dyld.py \ - build/windows/x64/Lib/ctypes/macholib/dylib.py \ - build/windows/x64/Lib/ctypes/macholib/framework.py \ - build/windows/x64/Lib/ctypes/util.py \ - build/windows/x64/Lib/ctypes/wintypes.py \ - build/windows/x64/Lib/curses/__init__.py \ - build/windows/x64/Lib/curses/ascii.py \ - build/windows/x64/Lib/curses/has_key.py \ - build/windows/x64/Lib/curses/panel.py \ - build/windows/x64/Lib/curses/textpad.py \ - build/windows/x64/Lib/dataclasses.py \ - build/windows/x64/Lib/datetime.py \ - build/windows/x64/Lib/decimal.py \ - build/windows/x64/Lib/difflib.py \ - build/windows/x64/Lib/dis.py \ - build/windows/x64/Lib/doctest.py \ - build/windows/x64/Lib/email/__init__.py \ - build/windows/x64/Lib/email/_encoded_words.py \ - build/windows/x64/Lib/email/_header_value_parser.py \ - build/windows/x64/Lib/email/_parseaddr.py \ - build/windows/x64/Lib/email/_policybase.py \ - build/windows/x64/Lib/email/base64mime.py \ - build/windows/x64/Lib/email/charset.py \ - build/windows/x64/Lib/email/contentmanager.py \ - build/windows/x64/Lib/email/encoders.py \ - build/windows/x64/Lib/email/errors.py \ - build/windows/x64/Lib/email/feedparser.py \ - build/windows/x64/Lib/email/generator.py \ - build/windows/x64/Lib/email/header.py \ - build/windows/x64/Lib/email/headerregistry.py \ - build/windows/x64/Lib/email/iterators.py \ - build/windows/x64/Lib/email/message.py \ - build/windows/x64/Lib/email/mime/__init__.py \ - build/windows/x64/Lib/email/mime/application.py \ - build/windows/x64/Lib/email/mime/audio.py \ - build/windows/x64/Lib/email/mime/base.py \ - build/windows/x64/Lib/email/mime/image.py \ - build/windows/x64/Lib/email/mime/message.py \ - build/windows/x64/Lib/email/mime/multipart.py \ - build/windows/x64/Lib/email/mime/nonmultipart.py \ - build/windows/x64/Lib/email/mime/text.py \ - build/windows/x64/Lib/email/parser.py \ - build/windows/x64/Lib/email/policy.py \ - build/windows/x64/Lib/email/quoprimime.py \ - build/windows/x64/Lib/email/utils.py \ - build/windows/x64/Lib/encodings/__init__.py \ - build/windows/x64/Lib/encodings/aliases.py \ - build/windows/x64/Lib/encodings/ascii.py \ - build/windows/x64/Lib/encodings/base64_codec.py \ - build/windows/x64/Lib/encodings/big5.py \ - build/windows/x64/Lib/encodings/big5hkscs.py \ - build/windows/x64/Lib/encodings/bz2_codec.py \ - build/windows/x64/Lib/encodings/charmap.py \ - build/windows/x64/Lib/encodings/cp037.py \ - build/windows/x64/Lib/encodings/cp1006.py \ - build/windows/x64/Lib/encodings/cp1026.py \ - build/windows/x64/Lib/encodings/cp1125.py \ - build/windows/x64/Lib/encodings/cp1140.py \ - build/windows/x64/Lib/encodings/cp1250.py \ - build/windows/x64/Lib/encodings/cp1251.py \ - build/windows/x64/Lib/encodings/cp1252.py \ - build/windows/x64/Lib/encodings/cp1253.py \ - build/windows/x64/Lib/encodings/cp1254.py \ - build/windows/x64/Lib/encodings/cp1255.py \ - build/windows/x64/Lib/encodings/cp1256.py \ - build/windows/x64/Lib/encodings/cp1257.py \ - build/windows/x64/Lib/encodings/cp1258.py \ - build/windows/x64/Lib/encodings/cp273.py \ - build/windows/x64/Lib/encodings/cp424.py \ - build/windows/x64/Lib/encodings/cp437.py \ - build/windows/x64/Lib/encodings/cp500.py \ - build/windows/x64/Lib/encodings/cp720.py \ - build/windows/x64/Lib/encodings/cp737.py \ - build/windows/x64/Lib/encodings/cp775.py \ - build/windows/x64/Lib/encodings/cp850.py \ - build/windows/x64/Lib/encodings/cp852.py \ - build/windows/x64/Lib/encodings/cp855.py \ - build/windows/x64/Lib/encodings/cp856.py \ - build/windows/x64/Lib/encodings/cp857.py \ - build/windows/x64/Lib/encodings/cp858.py \ - build/windows/x64/Lib/encodings/cp860.py \ - build/windows/x64/Lib/encodings/cp861.py \ - build/windows/x64/Lib/encodings/cp862.py \ - build/windows/x64/Lib/encodings/cp863.py \ - build/windows/x64/Lib/encodings/cp864.py \ - build/windows/x64/Lib/encodings/cp865.py \ - build/windows/x64/Lib/encodings/cp866.py \ - build/windows/x64/Lib/encodings/cp869.py \ - build/windows/x64/Lib/encodings/cp874.py \ - build/windows/x64/Lib/encodings/cp875.py \ - build/windows/x64/Lib/encodings/cp932.py \ - build/windows/x64/Lib/encodings/cp949.py \ - build/windows/x64/Lib/encodings/cp950.py \ - build/windows/x64/Lib/encodings/euc_jis_2004.py \ - build/windows/x64/Lib/encodings/euc_jisx0213.py \ - build/windows/x64/Lib/encodings/euc_jp.py \ - build/windows/x64/Lib/encodings/euc_kr.py \ - build/windows/x64/Lib/encodings/gb18030.py \ - build/windows/x64/Lib/encodings/gb2312.py \ - build/windows/x64/Lib/encodings/gbk.py \ - build/windows/x64/Lib/encodings/hex_codec.py \ - build/windows/x64/Lib/encodings/hp_roman8.py \ - build/windows/x64/Lib/encodings/hz.py \ - build/windows/x64/Lib/encodings/idna.py \ - build/windows/x64/Lib/encodings/iso2022_jp.py \ - build/windows/x64/Lib/encodings/iso2022_jp_1.py \ - build/windows/x64/Lib/encodings/iso2022_jp_2.py \ - build/windows/x64/Lib/encodings/iso2022_jp_2004.py \ - build/windows/x64/Lib/encodings/iso2022_jp_3.py \ - build/windows/x64/Lib/encodings/iso2022_jp_ext.py \ - build/windows/x64/Lib/encodings/iso2022_kr.py \ - build/windows/x64/Lib/encodings/iso8859_1.py \ - build/windows/x64/Lib/encodings/iso8859_10.py \ - build/windows/x64/Lib/encodings/iso8859_11.py \ - build/windows/x64/Lib/encodings/iso8859_13.py \ - build/windows/x64/Lib/encodings/iso8859_14.py \ - build/windows/x64/Lib/encodings/iso8859_15.py \ - build/windows/x64/Lib/encodings/iso8859_16.py \ - build/windows/x64/Lib/encodings/iso8859_2.py \ - build/windows/x64/Lib/encodings/iso8859_3.py \ - build/windows/x64/Lib/encodings/iso8859_4.py \ - build/windows/x64/Lib/encodings/iso8859_5.py \ - build/windows/x64/Lib/encodings/iso8859_6.py \ - build/windows/x64/Lib/encodings/iso8859_7.py \ - build/windows/x64/Lib/encodings/iso8859_8.py \ - build/windows/x64/Lib/encodings/iso8859_9.py \ - build/windows/x64/Lib/encodings/johab.py \ - build/windows/x64/Lib/encodings/koi8_r.py \ - build/windows/x64/Lib/encodings/koi8_t.py \ - build/windows/x64/Lib/encodings/koi8_u.py \ - build/windows/x64/Lib/encodings/kz1048.py \ - build/windows/x64/Lib/encodings/latin_1.py \ - build/windows/x64/Lib/encodings/mac_arabic.py \ - build/windows/x64/Lib/encodings/mac_croatian.py \ - build/windows/x64/Lib/encodings/mac_cyrillic.py \ - build/windows/x64/Lib/encodings/mac_farsi.py \ - build/windows/x64/Lib/encodings/mac_greek.py \ - build/windows/x64/Lib/encodings/mac_iceland.py \ - build/windows/x64/Lib/encodings/mac_latin2.py \ - build/windows/x64/Lib/encodings/mac_roman.py \ - build/windows/x64/Lib/encodings/mac_romanian.py \ - build/windows/x64/Lib/encodings/mac_turkish.py \ - build/windows/x64/Lib/encodings/mbcs.py \ - build/windows/x64/Lib/encodings/oem.py \ - build/windows/x64/Lib/encodings/palmos.py \ - build/windows/x64/Lib/encodings/ptcp154.py \ - build/windows/x64/Lib/encodings/punycode.py \ - build/windows/x64/Lib/encodings/quopri_codec.py \ - build/windows/x64/Lib/encodings/raw_unicode_escape.py \ - build/windows/x64/Lib/encodings/rot_13.py \ - build/windows/x64/Lib/encodings/shift_jis.py \ - build/windows/x64/Lib/encodings/shift_jis_2004.py \ - build/windows/x64/Lib/encodings/shift_jisx0213.py \ - build/windows/x64/Lib/encodings/tis_620.py \ - build/windows/x64/Lib/encodings/undefined.py \ - build/windows/x64/Lib/encodings/unicode_escape.py \ - build/windows/x64/Lib/encodings/utf_16.py \ - build/windows/x64/Lib/encodings/utf_16_be.py \ - build/windows/x64/Lib/encodings/utf_16_le.py \ - build/windows/x64/Lib/encodings/utf_32.py \ - build/windows/x64/Lib/encodings/utf_32_be.py \ - build/windows/x64/Lib/encodings/utf_32_le.py \ - build/windows/x64/Lib/encodings/utf_7.py \ - build/windows/x64/Lib/encodings/utf_8.py \ - build/windows/x64/Lib/encodings/utf_8_sig.py \ - build/windows/x64/Lib/encodings/uu_codec.py \ - build/windows/x64/Lib/encodings/zlib_codec.py \ - build/windows/x64/Lib/enum.py \ - build/windows/x64/Lib/filecmp.py \ - build/windows/x64/Lib/fileinput.py \ - build/windows/x64/Lib/fnmatch.py \ - build/windows/x64/Lib/fractions.py \ - build/windows/x64/Lib/ftplib.py \ - build/windows/x64/Lib/functools.py \ - build/windows/x64/Lib/genericpath.py \ - build/windows/x64/Lib/getopt.py \ - build/windows/x64/Lib/getpass.py \ - build/windows/x64/Lib/gettext.py \ - build/windows/x64/Lib/glob.py \ - build/windows/x64/Lib/graphlib.py \ - build/windows/x64/Lib/gzip.py \ - build/windows/x64/Lib/hashlib.py \ - build/windows/x64/Lib/heapq.py \ - build/windows/x64/Lib/hmac.py \ - build/windows/x64/Lib/html/__init__.py \ - build/windows/x64/Lib/html/entities.py \ - build/windows/x64/Lib/html/parser.py \ - build/windows/x64/Lib/http/__init__.py \ - build/windows/x64/Lib/http/client.py \ - build/windows/x64/Lib/http/cookiejar.py \ - build/windows/x64/Lib/http/cookies.py \ - build/windows/x64/Lib/http/server.py \ - build/windows/x64/Lib/imghdr.py \ - build/windows/x64/Lib/imp.py \ - build/windows/x64/Lib/importlib/__init__.py \ - build/windows/x64/Lib/importlib/_abc.py \ - build/windows/x64/Lib/importlib/_adapters.py \ - build/windows/x64/Lib/importlib/_bootstrap.py \ - build/windows/x64/Lib/importlib/_bootstrap_external.py \ - build/windows/x64/Lib/importlib/_common.py \ - build/windows/x64/Lib/importlib/abc.py \ - build/windows/x64/Lib/importlib/machinery.py \ - build/windows/x64/Lib/importlib/metadata/__init__.py \ - build/windows/x64/Lib/importlib/metadata/_adapters.py \ - build/windows/x64/Lib/importlib/metadata/_collections.py \ - build/windows/x64/Lib/importlib/metadata/_functools.py \ - build/windows/x64/Lib/importlib/metadata/_itertools.py \ - build/windows/x64/Lib/importlib/metadata/_meta.py \ - build/windows/x64/Lib/importlib/metadata/_text.py \ - build/windows/x64/Lib/importlib/readers.py \ - build/windows/x64/Lib/importlib/resources.py \ - build/windows/x64/Lib/importlib/util.py \ - build/windows/x64/Lib/inspect.py \ - build/windows/x64/Lib/io.py \ - build/windows/x64/Lib/ipaddress.py \ - build/windows/x64/Lib/json/__init__.py \ - build/windows/x64/Lib/json/decoder.py \ - build/windows/x64/Lib/json/encoder.py \ - build/windows/x64/Lib/json/scanner.py \ - build/windows/x64/Lib/json/tool.py \ - build/windows/x64/Lib/keyword.py \ - build/windows/x64/Lib/linecache.py \ - build/windows/x64/Lib/locale.py \ - build/windows/x64/Lib/logging/__init__.py \ - build/windows/x64/Lib/logging/config.py \ - build/windows/x64/Lib/logging/handlers.py \ - build/windows/x64/Lib/lzma.py \ - build/windows/x64/Lib/mailbox.py \ - build/windows/x64/Lib/mailcap.py \ - build/windows/x64/Lib/mimetypes.py \ - build/windows/x64/Lib/modulefinder.py \ - build/windows/x64/Lib/msilib/__init__.py \ - build/windows/x64/Lib/msilib/schema.py \ - build/windows/x64/Lib/msilib/sequence.py \ - build/windows/x64/Lib/msilib/text.py \ - build/windows/x64/Lib/netrc.py \ - build/windows/x64/Lib/nntplib.py \ - build/windows/x64/Lib/ntpath.py \ - build/windows/x64/Lib/nturl2path.py \ - build/windows/x64/Lib/numbers.py \ - build/windows/x64/Lib/opcode.py \ - build/windows/x64/Lib/operator.py \ - build/windows/x64/Lib/optparse.py \ - build/windows/x64/Lib/os.py \ - build/windows/x64/Lib/pathlib.py \ - build/windows/x64/Lib/pdb.py \ - build/windows/x64/Lib/pickle.py \ - build/windows/x64/Lib/pickletools.py \ - build/windows/x64/Lib/pipes.py \ - build/windows/x64/Lib/pkgutil.py \ - build/windows/x64/Lib/platform.py \ - build/windows/x64/Lib/plistlib.py \ - build/windows/x64/Lib/poplib.py \ - build/windows/x64/Lib/posixpath.py \ - build/windows/x64/Lib/pprint.py \ - build/windows/x64/Lib/profile.py \ - build/windows/x64/Lib/pstats.py \ - build/windows/x64/Lib/pty.py \ - build/windows/x64/Lib/py_compile.py \ - build/windows/x64/Lib/pyclbr.py \ - build/windows/x64/Lib/pydoc.py \ - build/windows/x64/Lib/queue.py \ - build/windows/x64/Lib/quopri.py \ - build/windows/x64/Lib/random.py \ - build/windows/x64/Lib/re.py \ - build/windows/x64/Lib/reprlib.py \ - build/windows/x64/Lib/rlcompleter.py \ - build/windows/x64/Lib/runpy.py \ - build/windows/x64/Lib/sched.py \ - build/windows/x64/Lib/secrets.py \ - build/windows/x64/Lib/selectors.py \ - build/windows/x64/Lib/shelve.py \ - build/windows/x64/Lib/shlex.py \ - build/windows/x64/Lib/shutil.py \ - build/windows/x64/Lib/signal.py \ - build/windows/x64/Lib/site.py \ - build/windows/x64/Lib/smtpd.py \ - build/windows/x64/Lib/smtplib.py \ - build/windows/x64/Lib/sndhdr.py \ - build/windows/x64/Lib/socket.py \ - build/windows/x64/Lib/socketserver.py \ - build/windows/x64/Lib/sqlite3/__init__.py \ - build/windows/x64/Lib/sqlite3/dbapi2.py \ - build/windows/x64/Lib/sqlite3/dump.py \ - build/windows/x64/Lib/sre_compile.py \ - build/windows/x64/Lib/sre_constants.py \ - build/windows/x64/Lib/sre_parse.py \ - build/windows/x64/Lib/ssl.py \ - build/windows/x64/Lib/stat.py \ - build/windows/x64/Lib/statistics.py \ - build/windows/x64/Lib/string.py \ - build/windows/x64/Lib/stringprep.py \ - build/windows/x64/Lib/struct.py \ - build/windows/x64/Lib/subprocess.py \ - build/windows/x64/Lib/sunau.py \ - build/windows/x64/Lib/symtable.py \ - build/windows/x64/Lib/sysconfig.py \ - build/windows/x64/Lib/tabnanny.py \ - build/windows/x64/Lib/tarfile.py \ - build/windows/x64/Lib/telnetlib.py \ - build/windows/x64/Lib/tempfile.py \ - build/windows/x64/Lib/textwrap.py \ - build/windows/x64/Lib/this.py \ - build/windows/x64/Lib/threading.py \ - build/windows/x64/Lib/timeit.py \ - build/windows/x64/Lib/token.py \ - build/windows/x64/Lib/tokenize.py \ - build/windows/x64/Lib/trace.py \ - build/windows/x64/Lib/traceback.py \ - build/windows/x64/Lib/tracemalloc.py \ - build/windows/x64/Lib/tty.py \ - build/windows/x64/Lib/types.py \ - build/windows/x64/Lib/typing.py \ - build/windows/x64/Lib/urllib/__init__.py \ - build/windows/x64/Lib/urllib/error.py \ - build/windows/x64/Lib/urllib/parse.py \ - build/windows/x64/Lib/urllib/request.py \ - build/windows/x64/Lib/urllib/response.py \ - build/windows/x64/Lib/urllib/robotparser.py \ - build/windows/x64/Lib/uu.py \ - build/windows/x64/Lib/uuid.py \ - build/windows/x64/Lib/warnings.py \ - build/windows/x64/Lib/wave.py \ - build/windows/x64/Lib/weakref.py \ - build/windows/x64/Lib/webbrowser.py \ - build/windows/x64/Lib/xdrlib.py \ - build/windows/x64/Lib/xml/__init__.py \ - build/windows/x64/Lib/xml/dom/NodeFilter.py \ - build/windows/x64/Lib/xml/dom/__init__.py \ - build/windows/x64/Lib/xml/dom/domreg.py \ - build/windows/x64/Lib/xml/dom/expatbuilder.py \ - build/windows/x64/Lib/xml/dom/minicompat.py \ - build/windows/x64/Lib/xml/dom/minidom.py \ - build/windows/x64/Lib/xml/dom/pulldom.py \ - build/windows/x64/Lib/xml/dom/xmlbuilder.py \ - build/windows/x64/Lib/xml/etree/ElementInclude.py \ - build/windows/x64/Lib/xml/etree/ElementPath.py \ - build/windows/x64/Lib/xml/etree/ElementTree.py \ - build/windows/x64/Lib/xml/etree/__init__.py \ - build/windows/x64/Lib/xml/etree/cElementTree.py \ - build/windows/x64/Lib/xml/parsers/__init__.py \ - build/windows/x64/Lib/xml/parsers/expat.py \ - build/windows/x64/Lib/xml/sax/__init__.py \ - build/windows/x64/Lib/xml/sax/_exceptions.py \ - build/windows/x64/Lib/xml/sax/expatreader.py \ - build/windows/x64/Lib/xml/sax/handler.py \ - build/windows/x64/Lib/xml/sax/saxutils.py \ - build/windows/x64/Lib/xml/sax/xmlreader.py \ - build/windows/x64/Lib/xmlrpc/__init__.py \ - build/windows/x64/Lib/xmlrpc/client.py \ - build/windows/x64/Lib/xmlrpc/server.py \ - build/windows/x64/Lib/zipapp.py \ - build/windows/x64/Lib/zipfile.py \ - build/windows/x64/Lib/zipimport.py \ - build/windows/x64/Lib/zoneinfo/__init__.py \ - build/windows/x64/Lib/zoneinfo/_common.py \ - build/windows/x64/Lib/zoneinfo/_tzpath.py \ - build/windows/x64/Lib/zoneinfo/_zoneinfo.py - -SCRIPT_TARGETS_PYC_PRIVATE_WIN_X64 = \ - build/windows/x64/Lib/__pycache__/__future__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/__phello__.foo.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/_aix_support.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/_bootsubprocess.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/_collections_abc.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/_compat_pickle.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/_compression.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/_markupbase.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/_osx_support.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/_py_abc.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/_pydecimal.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/_pyio.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/_sitebuiltins.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/_strptime.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/_threading_local.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/_weakrefset.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/abc.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/aifc.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/antigravity.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/argparse.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/ast.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/asynchat.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/__main__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/base_events.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/base_futures.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/base_subprocess.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/base_tasks.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/constants.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/coroutines.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/events.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/exceptions.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/format_helpers.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/futures.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/locks.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/log.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/mixins.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/proactor_events.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/protocols.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/queues.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/runners.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/selector_events.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/sslproto.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/staggered.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/streams.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/subprocess.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/tasks.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/threads.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/transports.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/trsock.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/unix_events.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/windows_events.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/asyncio/__pycache__/windows_utils.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/asyncore.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/base64.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/bdb.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/binhex.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/bisect.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/bz2.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/cProfile.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/calendar.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/cgi.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/cgitb.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/chunk.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/cmd.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/code.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/codecs.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/codeop.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/collections/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/collections/__pycache__/abc.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/colorsys.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/compileall.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/concurrent/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/concurrent/futures/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/concurrent/futures/__pycache__/_base.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/concurrent/futures/__pycache__/process.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/concurrent/futures/__pycache__/thread.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/configparser.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/contextlib.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/contextvars.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/copy.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/copyreg.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/crypt.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/csv.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/ctypes/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/ctypes/__pycache__/_aix.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/ctypes/__pycache__/_endian.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/ctypes/macholib/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/ctypes/macholib/__pycache__/dyld.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/ctypes/macholib/__pycache__/dylib.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/ctypes/macholib/__pycache__/framework.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/ctypes/__pycache__/util.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/ctypes/__pycache__/wintypes.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/curses/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/curses/__pycache__/ascii.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/curses/__pycache__/has_key.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/curses/__pycache__/panel.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/curses/__pycache__/textpad.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/dataclasses.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/datetime.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/decimal.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/difflib.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/dis.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/doctest.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/_encoded_words.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/_header_value_parser.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/_parseaddr.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/_policybase.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/base64mime.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/charset.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/contentmanager.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/encoders.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/errors.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/feedparser.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/generator.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/header.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/headerregistry.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/iterators.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/message.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/mime/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/mime/__pycache__/application.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/mime/__pycache__/audio.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/mime/__pycache__/base.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/mime/__pycache__/image.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/mime/__pycache__/message.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/mime/__pycache__/multipart.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/mime/__pycache__/nonmultipart.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/mime/__pycache__/text.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/parser.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/policy.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/quoprimime.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/email/__pycache__/utils.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/aliases.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/ascii.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/base64_codec.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/big5.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/big5hkscs.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/bz2_codec.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/charmap.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp037.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp1006.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp1026.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp1125.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp1140.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp1250.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp1251.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp1252.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp1253.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp1254.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp1255.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp1256.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp1257.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp1258.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp273.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp424.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp437.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp500.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp720.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp737.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp775.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp850.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp852.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp855.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp856.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp857.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp858.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp860.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp861.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp862.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp863.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp864.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp865.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp866.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp869.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp874.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp875.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp932.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp949.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/cp950.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/euc_jis_2004.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/euc_jisx0213.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/euc_jp.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/euc_kr.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/gb18030.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/gb2312.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/gbk.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/hex_codec.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/hp_roman8.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/hz.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/idna.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso2022_jp.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso2022_jp_1.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso2022_jp_2.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso2022_jp_2004.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso2022_jp_3.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso2022_jp_ext.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso2022_kr.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso8859_1.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso8859_10.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso8859_11.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso8859_13.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso8859_14.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso8859_15.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso8859_16.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso8859_2.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso8859_3.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso8859_4.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso8859_5.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso8859_6.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso8859_7.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso8859_8.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/iso8859_9.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/johab.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/koi8_r.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/koi8_t.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/koi8_u.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/kz1048.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/latin_1.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/mac_arabic.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/mac_croatian.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/mac_cyrillic.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/mac_farsi.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/mac_greek.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/mac_iceland.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/mac_latin2.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/mac_roman.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/mac_romanian.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/mac_turkish.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/mbcs.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/oem.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/palmos.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/ptcp154.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/punycode.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/quopri_codec.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/raw_unicode_escape.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/rot_13.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/shift_jis.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/shift_jis_2004.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/shift_jisx0213.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/tis_620.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/undefined.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/unicode_escape.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/utf_16.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/utf_16_be.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/utf_16_le.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/utf_32.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/utf_32_be.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/utf_32_le.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/utf_7.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/utf_8.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/utf_8_sig.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/uu_codec.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/encodings/__pycache__/zlib_codec.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/enum.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/filecmp.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/fileinput.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/fnmatch.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/fractions.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/ftplib.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/functools.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/genericpath.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/getopt.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/getpass.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/gettext.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/glob.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/graphlib.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/gzip.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/hashlib.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/heapq.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/hmac.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/html/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/html/__pycache__/entities.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/html/__pycache__/parser.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/http/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/http/__pycache__/client.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/http/__pycache__/cookiejar.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/http/__pycache__/cookies.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/http/__pycache__/server.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/imghdr.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/imp.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/__pycache__/_abc.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/__pycache__/_adapters.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/__pycache__/_bootstrap.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/__pycache__/_bootstrap_external.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/__pycache__/_common.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/__pycache__/abc.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/__pycache__/machinery.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/metadata/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/metadata/__pycache__/_adapters.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/metadata/__pycache__/_collections.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/metadata/__pycache__/_functools.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/metadata/__pycache__/_itertools.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/metadata/__pycache__/_meta.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/metadata/__pycache__/_text.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/__pycache__/readers.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/__pycache__/resources.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/importlib/__pycache__/util.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/inspect.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/io.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/ipaddress.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/json/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/json/__pycache__/decoder.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/json/__pycache__/encoder.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/json/__pycache__/scanner.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/json/__pycache__/tool.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/keyword.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/linecache.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/locale.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/logging/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/logging/__pycache__/config.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/logging/__pycache__/handlers.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/lzma.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/mailbox.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/mailcap.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/mimetypes.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/modulefinder.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/msilib/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/msilib/__pycache__/schema.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/msilib/__pycache__/sequence.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/msilib/__pycache__/text.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/netrc.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/nntplib.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/ntpath.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/nturl2path.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/numbers.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/opcode.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/operator.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/optparse.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/os.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/pathlib.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/pdb.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/pickle.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/pickletools.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/pipes.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/pkgutil.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/platform.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/plistlib.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/poplib.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/posixpath.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/pprint.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/profile.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/pstats.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/pty.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/py_compile.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/pyclbr.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/pydoc.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/queue.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/quopri.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/random.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/re.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/reprlib.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/rlcompleter.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/runpy.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/sched.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/secrets.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/selectors.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/shelve.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/shlex.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/shutil.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/signal.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/site.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/smtpd.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/smtplib.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/sndhdr.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/socket.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/socketserver.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/sqlite3/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/sqlite3/__pycache__/dbapi2.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/sqlite3/__pycache__/dump.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/sre_compile.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/sre_constants.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/sre_parse.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/ssl.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/stat.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/statistics.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/string.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/stringprep.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/struct.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/subprocess.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/sunau.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/symtable.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/sysconfig.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/tabnanny.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/tarfile.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/telnetlib.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/tempfile.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/textwrap.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/this.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/threading.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/timeit.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/token.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/tokenize.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/trace.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/traceback.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/tracemalloc.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/tty.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/types.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/typing.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/urllib/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/urllib/__pycache__/error.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/urllib/__pycache__/parse.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/urllib/__pycache__/request.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/urllib/__pycache__/response.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/urllib/__pycache__/robotparser.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/uu.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/uuid.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/warnings.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/wave.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/weakref.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/webbrowser.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/xdrlib.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/dom/__pycache__/NodeFilter.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/dom/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/dom/__pycache__/domreg.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/dom/__pycache__/expatbuilder.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/dom/__pycache__/minicompat.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/dom/__pycache__/minidom.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/dom/__pycache__/pulldom.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/dom/__pycache__/xmlbuilder.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/etree/__pycache__/ElementInclude.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/etree/__pycache__/ElementPath.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/etree/__pycache__/ElementTree.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/etree/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/etree/__pycache__/cElementTree.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/parsers/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/parsers/__pycache__/expat.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/sax/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/sax/__pycache__/_exceptions.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/sax/__pycache__/expatreader.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/sax/__pycache__/handler.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/sax/__pycache__/saxutils.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xml/sax/__pycache__/xmlreader.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xmlrpc/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xmlrpc/__pycache__/client.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/xmlrpc/__pycache__/server.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/zipapp.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/zipfile.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/__pycache__/zipimport.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/zoneinfo/__pycache__/__init__.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/zoneinfo/__pycache__/_common.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/zoneinfo/__pycache__/_tzpath.cpython-310.opt-1.pyc \ - build/windows/x64/Lib/zoneinfo/__pycache__/_zoneinfo.cpython-310.opt-1.pyc - -# Rule to copy src asset scripts to dst. -# (and make non-writable so I'm less likely to accidentally edit them there) -$(SCRIPT_TARGETS_PY_PRIVATE_WIN_X64) : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -# These are too complex to define in a pattern rule; -# Instead we generate individual targets in a loop. -$(foreach element,$(SCRIPT_TARGETS_PYC_PRIVATE_WIN_X64),\ -$(eval $(call make-opt-pyc-target,$(element)))) - -COB_TARGETS = \ - build/ba_data/models/alwaysLandLevelCollide.cob \ - build/ba_data/models/bigGBumper.cob \ - build/ba_data/models/bigGCollide.cob \ - build/ba_data/models/bridgitLevelCollide.cob \ - build/ba_data/models/bridgitLevelRailingCollide.cob \ - build/ba_data/models/courtyardLevelCollide.cob \ - build/ba_data/models/courtyardPlayerWall.cob \ - build/ba_data/models/cragCastleLevelBumper.cob \ - build/ba_data/models/cragCastleLevelCollide.cob \ - build/ba_data/models/doomShroomLevelCollide.cob \ - build/ba_data/models/doomShroomStemCollide.cob \ - build/ba_data/models/footballStadiumCollide.cob \ - build/ba_data/models/hockeyStadiumCollide.cob \ - build/ba_data/models/lakeFrigidCollide.cob \ - build/ba_data/models/monkeyFaceLevelBumper.cob \ - build/ba_data/models/monkeyFaceLevelCollide.cob \ - build/ba_data/models/natureBackgroundCollide.cob \ - build/ba_data/models/rampageBumper.cob \ - build/ba_data/models/rampageLevelCollide.cob \ - build/ba_data/models/roundaboutLevelBumper.cob \ - build/ba_data/models/roundaboutLevelCollide.cob \ - build/ba_data/models/stepRightUpLevelCollide.cob \ - build/ba_data/models/thePadLevelBumper.cob \ - build/ba_data/models/thePadLevelCollide.cob \ - build/ba_data/models/tipTopLevelBumper.cob \ - build/ba_data/models/tipTopLevelCollide.cob \ - build/ba_data/models/towerDLevelCollide.cob \ - build/ba_data/models/towerDPlayerWall.cob \ - build/ba_data/models/zigZagLevelBumper.cob \ - build/ba_data/models/zigZagLevelCollide.cob - -BOB_TARGETS = \ - build/ba_data/models/achievementOutline.bob \ - build/ba_data/models/actionButtonBottom.bob \ - build/ba_data/models/actionButtonLeft.bob \ - build/ba_data/models/actionButtonRight.bob \ - build/ba_data/models/actionButtonTop.bob \ - build/ba_data/models/actionHeroForeArm.bob \ - build/ba_data/models/actionHeroHand.bob \ - build/ba_data/models/actionHeroHead.bob \ - build/ba_data/models/actionHeroLowerLeg.bob \ - build/ba_data/models/actionHeroPelvis.bob \ - build/ba_data/models/actionHeroToes.bob \ - build/ba_data/models/actionHeroTorso.bob \ - build/ba_data/models/actionHeroUpperArm.bob \ - build/ba_data/models/actionHeroUpperLeg.bob \ - build/ba_data/models/agentForeArm.bob \ - build/ba_data/models/agentHand.bob \ - build/ba_data/models/agentHead.bob \ - build/ba_data/models/agentLowerLeg.bob \ - build/ba_data/models/agentPelvis.bob \ - build/ba_data/models/agentToes.bob \ - build/ba_data/models/agentTorso.bob \ - build/ba_data/models/agentUpperArm.bob \ - build/ba_data/models/agentUpperLeg.bob \ - build/ba_data/models/aliForeArm.bob \ - build/ba_data/models/aliHand.bob \ - build/ba_data/models/aliHead.bob \ - build/ba_data/models/aliLowerLeg.bob \ - build/ba_data/models/aliPelvis.bob \ - build/ba_data/models/aliToes.bob \ - build/ba_data/models/aliTorso.bob \ - build/ba_data/models/aliUpperArm.bob \ - build/ba_data/models/aliUpperLeg.bob \ - build/ba_data/models/alienForeArm.bob \ - build/ba_data/models/alienHand.bob \ - build/ba_data/models/alienHead.bob \ - build/ba_data/models/alienLowerLeg.bob \ - build/ba_data/models/alienPelvis.bob \ - build/ba_data/models/alienToes.bob \ - build/ba_data/models/alienTorso.bob \ - build/ba_data/models/alienUpperArm.bob \ - build/ba_data/models/alienUpperLeg.bob \ - build/ba_data/models/alwaysLandBG.bob \ - build/ba_data/models/alwaysLandLevel.bob \ - build/ba_data/models/alwaysLandLevelBottom.bob \ - build/ba_data/models/alwaysLandVRFillMound.bob \ - build/ba_data/models/angryComputerTransparent.bob \ - build/ba_data/models/arrowBack.bob \ - build/ba_data/models/arrowFront.bob \ - build/ba_data/models/assassinForeArm.bob \ - build/ba_data/models/assassinHand.bob \ - build/ba_data/models/assassinHead.bob \ - build/ba_data/models/assassinLowerLeg.bob \ - build/ba_data/models/assassinPelvis.bob \ - build/ba_data/models/assassinToes.bob \ - build/ba_data/models/assassinTorso.bob \ - build/ba_data/models/assassinUpperArm.bob \ - build/ba_data/models/assassinUpperLeg.bob \ - build/ba_data/models/bearForeArm.bob \ - build/ba_data/models/bearHand.bob \ - build/ba_data/models/bearHead.bob \ - build/ba_data/models/bearLowerLeg.bob \ - build/ba_data/models/bearPelvis.bob \ - build/ba_data/models/bearToes.bob \ - build/ba_data/models/bearTorso.bob \ - build/ba_data/models/bearUpperArm.bob \ - build/ba_data/models/bearUpperLeg.bob \ - build/ba_data/models/bigG.bob \ - build/ba_data/models/bigGBottom.bob \ - build/ba_data/models/bomb.bob \ - build/ba_data/models/bombSticky.bob \ - build/ba_data/models/bonesForeArm.bob \ - build/ba_data/models/bonesHand.bob \ - build/ba_data/models/bonesHead.bob \ - build/ba_data/models/bonesLowerLeg.bob \ - build/ba_data/models/bonesPelvis.bob \ - build/ba_data/models/bonesToes.bob \ - build/ba_data/models/bonesTorso.bob \ - build/ba_data/models/bonesUpperArm.bob \ - build/ba_data/models/bonesUpperLeg.bob \ - build/ba_data/models/box.bob \ - build/ba_data/models/boxingGlove.bob \ - build/ba_data/models/bridgitLevelBottom.bob \ - build/ba_data/models/bridgitLevelTop.bob \ - build/ba_data/models/bunnyForeArm.bob \ - build/ba_data/models/bunnyHand.bob \ - build/ba_data/models/bunnyHead.bob \ - build/ba_data/models/bunnyLowerLeg.bob \ - build/ba_data/models/bunnyPelvis.bob \ - build/ba_data/models/bunnyToes.bob \ - build/ba_data/models/bunnyTorso.bob \ - build/ba_data/models/bunnyUpperArm.bob \ - build/ba_data/models/bunnyUpperLeg.bob \ - build/ba_data/models/buttonBackOpaque.bob \ - build/ba_data/models/buttonBackSmallOpaque.bob \ - build/ba_data/models/buttonBackSmallTransparent.bob \ - build/ba_data/models/buttonBackTransparent.bob \ - build/ba_data/models/buttonLargeOpaque.bob \ - build/ba_data/models/buttonLargeTransparent.bob \ - build/ba_data/models/buttonLargerOpaque.bob \ - build/ba_data/models/buttonLargerTransparent.bob \ - build/ba_data/models/buttonMediumOpaque.bob \ - build/ba_data/models/buttonMediumTransparent.bob \ - build/ba_data/models/buttonNull.bob \ - build/ba_data/models/buttonSmallOpaque.bob \ - build/ba_data/models/buttonSmallTransparent.bob \ - build/ba_data/models/buttonSquareOpaque.bob \ - build/ba_data/models/buttonSquareTransparent.bob \ - build/ba_data/models/buttonTabOpaque.bob \ - build/ba_data/models/buttonTabTransparent.bob \ - build/ba_data/models/checkTransparent.bob \ - build/ba_data/models/courtyardLevel.bob \ - build/ba_data/models/courtyardLevelBottom.bob \ - build/ba_data/models/cowboyForeArm.bob \ - build/ba_data/models/cowboyHand.bob \ - build/ba_data/models/cowboyHead.bob \ - build/ba_data/models/cowboyLowerLeg.bob \ - build/ba_data/models/cowboyPelvis.bob \ - build/ba_data/models/cowboyToes.bob \ - build/ba_data/models/cowboyTorso.bob \ - build/ba_data/models/cowboyUpperArm.bob \ - build/ba_data/models/cowboyUpperLeg.bob \ - build/ba_data/models/cragCastleLevel.bob \ - build/ba_data/models/cragCastleLevelBottom.bob \ - build/ba_data/models/cragCastleVRFillMound.bob \ - build/ba_data/models/crossOut.bob \ - build/ba_data/models/currencyMeter.bob \ - build/ba_data/models/currencyPlusButton.bob \ - build/ba_data/models/cyborgForeArm.bob \ - build/ba_data/models/cyborgHand.bob \ - build/ba_data/models/cyborgHead.bob \ - build/ba_data/models/cyborgLowerLeg.bob \ - build/ba_data/models/cyborgPelvis.bob \ - build/ba_data/models/cyborgToes.bob \ - build/ba_data/models/cyborgTorso.bob \ - build/ba_data/models/cyborgUpperArm.bob \ - build/ba_data/models/cyborgUpperLeg.bob \ - build/ba_data/models/cylinder.bob \ - build/ba_data/models/doomShroomBG.bob \ - build/ba_data/models/doomShroomLevel.bob \ - build/ba_data/models/doomShroomStem.bob \ - build/ba_data/models/doomShroomVRFill.bob \ - build/ba_data/models/egg.bob \ - build/ba_data/models/eyeBall.bob \ - build/ba_data/models/eyeBallIris.bob \ - build/ba_data/models/eyeLid.bob \ - build/ba_data/models/flagPole.bob \ - build/ba_data/models/flagStand.bob \ - build/ba_data/models/flash.bob \ - build/ba_data/models/footballStadium.bob \ - build/ba_data/models/footballStadiumVRFill.bob \ - build/ba_data/models/frameInset.bob \ - build/ba_data/models/frostyForeArm.bob \ - build/ba_data/models/frostyHand.bob \ - build/ba_data/models/frostyHead.bob \ - build/ba_data/models/frostyLowerLeg.bob \ - build/ba_data/models/frostyPelvis.bob \ - build/ba_data/models/frostyToes.bob \ - build/ba_data/models/frostyTorso.bob \ - build/ba_data/models/frostyUpperArm.bob \ - build/ba_data/models/frostyUpperLeg.bob \ - build/ba_data/models/gladiatorForeArm.bob \ - build/ba_data/models/gladiatorHand.bob \ - build/ba_data/models/gladiatorHead.bob \ - build/ba_data/models/gladiatorLowerLeg.bob \ - build/ba_data/models/gladiatorPelvis.bob \ - build/ba_data/models/gladiatorToes.bob \ - build/ba_data/models/gladiatorTorso.bob \ - build/ba_data/models/gladiatorUpperArm.bob \ - build/ba_data/models/gladiatorUpperLeg.bob \ - build/ba_data/models/hairTuft1.bob \ - build/ba_data/models/hairTuft1b.bob \ - build/ba_data/models/hairTuft2.bob \ - build/ba_data/models/hairTuft3.bob \ - build/ba_data/models/hairTuft4.bob \ - build/ba_data/models/heartOpaque.bob \ - build/ba_data/models/heartTransparent.bob \ - build/ba_data/models/hockeyStadiumInner.bob \ - build/ba_data/models/hockeyStadiumOuter.bob \ - build/ba_data/models/hockeyStadiumStands.bob \ - build/ba_data/models/image16x1.bob \ - build/ba_data/models/image1x1.bob \ - build/ba_data/models/image1x1FullScreen.bob \ - build/ba_data/models/image1x1VRFullScreen.bob \ - build/ba_data/models/image2x1.bob \ - build/ba_data/models/image2x1Vertical.bob \ - build/ba_data/models/image4x1.bob \ - build/ba_data/models/impactBomb.bob \ - build/ba_data/models/jackForeArm.bob \ - build/ba_data/models/jackHand.bob \ - build/ba_data/models/jackHead.bob \ - build/ba_data/models/jackLowerLeg.bob \ - build/ba_data/models/jackToes.bob \ - build/ba_data/models/jackTorso.bob \ - build/ba_data/models/jackUpperArm.bob \ - build/ba_data/models/jackUpperLeg.bob \ - build/ba_data/models/jumpsuitForeArm.bob \ - build/ba_data/models/jumpsuitHand.bob \ - build/ba_data/models/jumpsuitHead.bob \ - build/ba_data/models/jumpsuitLowerLeg.bob \ - build/ba_data/models/jumpsuitPelvis.bob \ - build/ba_data/models/jumpsuitToes.bob \ - build/ba_data/models/jumpsuitTorso.bob \ - build/ba_data/models/jumpsuitUpperArm.bob \ - build/ba_data/models/jumpsuitUpperLeg.bob \ - build/ba_data/models/kronkForeArm.bob \ - build/ba_data/models/kronkHand.bob \ - build/ba_data/models/kronkHead.bob \ - build/ba_data/models/kronkLowerLeg.bob \ - build/ba_data/models/kronkPelvis.bob \ - build/ba_data/models/kronkToes.bob \ - build/ba_data/models/kronkTorso.bob \ - build/ba_data/models/kronkUpperArm.bob \ - build/ba_data/models/kronkUpperLeg.bob \ - build/ba_data/models/lakeFrigid.bob \ - build/ba_data/models/lakeFrigidReflections.bob \ - build/ba_data/models/lakeFrigidTop.bob \ - build/ba_data/models/lakeFrigidVRFill.bob \ - build/ba_data/models/landMine.bob \ - build/ba_data/models/level_select_button_opaque.bob \ - build/ba_data/models/level_select_button_transparent.bob \ - build/ba_data/models/locator.bob \ - build/ba_data/models/locatorBox.bob \ - build/ba_data/models/locatorCircle.bob \ - build/ba_data/models/locatorCircleOutline.bob \ - build/ba_data/models/logo.bob \ - build/ba_data/models/logoTransparent.bob \ - build/ba_data/models/melForeArm.bob \ - build/ba_data/models/melHand.bob \ - build/ba_data/models/melHead.bob \ - build/ba_data/models/melLowerLeg.bob \ - build/ba_data/models/melToes.bob \ - build/ba_data/models/melTorso.bob \ - build/ba_data/models/melUpperArm.bob \ - build/ba_data/models/melUpperLeg.bob \ - build/ba_data/models/meterTransparent.bob \ - build/ba_data/models/monkeyFaceLevel.bob \ - build/ba_data/models/monkeyFaceLevelBottom.bob \ - build/ba_data/models/natureBackground.bob \ - build/ba_data/models/natureBackgroundVRFill.bob \ - build/ba_data/models/neoSpazForeArm.bob \ - build/ba_data/models/neoSpazHand.bob \ - build/ba_data/models/neoSpazHead.bob \ - build/ba_data/models/neoSpazLowerLeg.bob \ - build/ba_data/models/neoSpazPelvis.bob \ - build/ba_data/models/neoSpazToes.bob \ - build/ba_data/models/neoSpazTorso.bob \ - build/ba_data/models/neoSpazUpperArm.bob \ - build/ba_data/models/neoSpazUpperLeg.bob \ - build/ba_data/models/ninjaForeArm.bob \ - build/ba_data/models/ninjaHand.bob \ - build/ba_data/models/ninjaHead.bob \ - build/ba_data/models/ninjaLowerLeg.bob \ - build/ba_data/models/ninjaPelvis.bob \ - build/ba_data/models/ninjaToes.bob \ - build/ba_data/models/ninjaTorso.bob \ - build/ba_data/models/ninjaUpperArm.bob \ - build/ba_data/models/ninjaUpperLeg.bob \ - build/ba_data/models/oldLadyForeArm.bob \ - build/ba_data/models/oldLadyHand.bob \ - build/ba_data/models/oldLadyHead.bob \ - build/ba_data/models/oldLadyLowerLeg.bob \ - build/ba_data/models/oldLadyPelvis.bob \ - build/ba_data/models/oldLadyToes.bob \ - build/ba_data/models/oldLadyTorso.bob \ - build/ba_data/models/oldLadyUpperArm.bob \ - build/ba_data/models/oldLadyUpperLeg.bob \ - build/ba_data/models/operaSingerForeArm.bob \ - build/ba_data/models/operaSingerHand.bob \ - build/ba_data/models/operaSingerHead.bob \ - build/ba_data/models/operaSingerLowerLeg.bob \ - build/ba_data/models/operaSingerPelvis.bob \ - build/ba_data/models/operaSingerToes.bob \ - build/ba_data/models/operaSingerTorso.bob \ - build/ba_data/models/operaSingerUpperArm.bob \ - build/ba_data/models/operaSingerUpperLeg.bob \ - build/ba_data/models/overlayGuide.bob \ - build/ba_data/models/penguinForeArm.bob \ - build/ba_data/models/penguinHand.bob \ - build/ba_data/models/penguinHead.bob \ - build/ba_data/models/penguinLowerLeg.bob \ - build/ba_data/models/penguinPelvis.bob \ - build/ba_data/models/penguinToes.bob \ - build/ba_data/models/penguinTorso.bob \ - build/ba_data/models/penguinUpperArm.bob \ - build/ba_data/models/penguinUpperLeg.bob \ - build/ba_data/models/pixieForeArm.bob \ - build/ba_data/models/pixieHand.bob \ - build/ba_data/models/pixieHead.bob \ - build/ba_data/models/pixieLowerLeg.bob \ - build/ba_data/models/pixiePelvis.bob \ - build/ba_data/models/pixieToes.bob \ - build/ba_data/models/pixieTorso.bob \ - build/ba_data/models/pixieUpperArm.bob \ - build/ba_data/models/pixieUpperLeg.bob \ - build/ba_data/models/plasticEyesTransparent.bob \ - build/ba_data/models/playerLineup1Transparent.bob \ - build/ba_data/models/playerLineup2Transparent.bob \ - build/ba_data/models/playerLineup3Transparent.bob \ - build/ba_data/models/playerLineup4Transparent.bob \ - build/ba_data/models/powerup.bob \ - build/ba_data/models/powerupSimple.bob \ - build/ba_data/models/puck.bob \ - build/ba_data/models/rampageBG.bob \ - build/ba_data/models/rampageBG2.bob \ - build/ba_data/models/rampageLevel.bob \ - build/ba_data/models/rampageLevelBottom.bob \ - build/ba_data/models/rampageVRFill.bob \ - build/ba_data/models/robotForeArm.bob \ - build/ba_data/models/robotHand.bob \ - build/ba_data/models/robotHead.bob \ - build/ba_data/models/robotLowerLeg.bob \ - build/ba_data/models/robotPelvis.bob \ - build/ba_data/models/robotToes.bob \ - build/ba_data/models/robotTorso.bob \ - build/ba_data/models/robotUpperArm.bob \ - build/ba_data/models/robotUpperLeg.bob \ - build/ba_data/models/roundaboutLevel.bob \ - build/ba_data/models/roundaboutLevelBottom.bob \ - build/ba_data/models/runningShoes.bob \ - build/ba_data/models/santaForeArm.bob \ - build/ba_data/models/santaHand.bob \ - build/ba_data/models/santaHead.bob \ - build/ba_data/models/santaLowerLeg.bob \ - build/ba_data/models/santaToes.bob \ - build/ba_data/models/santaTorso.bob \ - build/ba_data/models/santaUpperArm.bob \ - build/ba_data/models/santaUpperLeg.bob \ - build/ba_data/models/scorch.bob \ - build/ba_data/models/scrollBarThumbOpaque.bob \ - build/ba_data/models/scrollBarThumbShortOpaque.bob \ - build/ba_data/models/scrollBarThumbShortSimple.bob \ - build/ba_data/models/scrollBarThumbShortTransparent.bob \ - build/ba_data/models/scrollBarThumbSimple.bob \ - build/ba_data/models/scrollBarThumbTransparent.bob \ - build/ba_data/models/scrollBarTroughTransparent.bob \ - build/ba_data/models/scrollWidgetShort.bob \ - build/ba_data/models/shield.bob \ - build/ba_data/models/shockWave.bob \ - build/ba_data/models/shrapnel1.bob \ - build/ba_data/models/shrapnelBoard.bob \ - build/ba_data/models/shrapnelSlime.bob \ - build/ba_data/models/softEdgeInside.bob \ - build/ba_data/models/softEdgeOutside.bob \ - build/ba_data/models/stepRightUpLevel.bob \ - build/ba_data/models/stepRightUpLevelBottom.bob \ - build/ba_data/models/stepRightUpVRFillMound.bob \ - build/ba_data/models/superheroForeArm.bob \ - build/ba_data/models/superheroHand.bob \ - build/ba_data/models/superheroHead.bob \ - build/ba_data/models/superheroLowerLeg.bob \ - build/ba_data/models/superheroPelvis.bob \ - build/ba_data/models/superheroToes.bob \ - build/ba_data/models/superheroTorso.bob \ - build/ba_data/models/superheroUpperArm.bob \ - build/ba_data/models/superheroUpperLeg.bob \ - build/ba_data/models/textBoxTransparent.bob \ - build/ba_data/models/thePadBG.bob \ - build/ba_data/models/thePadBGSmall.bob \ - build/ba_data/models/thePadLevel.bob \ - build/ba_data/models/thePadLevelBottom.bob \ - build/ba_data/models/thePadVRFillBottom.bob \ - build/ba_data/models/thePadVRFillMound.bob \ - build/ba_data/models/thePadVRFillTop.bob \ - build/ba_data/models/tipTopBG.bob \ - build/ba_data/models/tipTopLevel.bob \ - build/ba_data/models/tipTopLevelBottom.bob \ - build/ba_data/models/tnt.bob \ - build/ba_data/models/toolbarBacking.bob \ - build/ba_data/models/toolbarBackingBottom.bob \ - build/ba_data/models/toolbarBackingBottom2.bob \ - build/ba_data/models/toolbarBackingOpaque.bob \ - build/ba_data/models/toolbarBackingTop.bob \ - build/ba_data/models/toolbarBackingTop2.bob \ - build/ba_data/models/toolbarBackingTransparent.bob \ - build/ba_data/models/towerDLevel.bob \ - build/ba_data/models/towerDLevelBottom.bob \ - build/ba_data/models/trees.bob \ - build/ba_data/models/vrFade.bob \ - build/ba_data/models/vrOverlay.bob \ - build/ba_data/models/warriorForeArm.bob \ - build/ba_data/models/warriorHand.bob \ - build/ba_data/models/warriorHead.bob \ - build/ba_data/models/warriorLowerLeg.bob \ - build/ba_data/models/warriorPelvis.bob \ - build/ba_data/models/warriorToes.bob \ - build/ba_data/models/warriorTorso.bob \ - build/ba_data/models/warriorUpperArm.bob \ - build/ba_data/models/warriorUpperLeg.bob \ - build/ba_data/models/windowBGBlotch.bob \ - build/ba_data/models/windowHSmallVMedOpaque.bob \ - build/ba_data/models/windowHSmallVMedTransparent.bob \ - build/ba_data/models/windowHSmallVSmallOpaque.bob \ - build/ba_data/models/windowHSmallVSmallTransparent.bob \ - build/ba_data/models/wing.bob \ - build/ba_data/models/witchForeArm.bob \ - build/ba_data/models/witchHand.bob \ - build/ba_data/models/witchHead.bob \ - build/ba_data/models/witchLowerLeg.bob \ - build/ba_data/models/witchPelvis.bob \ - build/ba_data/models/witchToes.bob \ - build/ba_data/models/witchTorso.bob \ - build/ba_data/models/witchUpperArm.bob \ - build/ba_data/models/witchUpperLeg.bob \ - build/ba_data/models/wizardForeArm.bob \ - build/ba_data/models/wizardHand.bob \ - build/ba_data/models/wizardHead.bob \ - build/ba_data/models/wizardLowerLeg.bob \ - build/ba_data/models/wizardPelvis.bob \ - build/ba_data/models/wizardToes.bob \ - build/ba_data/models/wizardTorso.bob \ - build/ba_data/models/wizardUpperArm.bob \ - build/ba_data/models/wizardUpperLeg.bob \ - build/ba_data/models/wrestlerForeArm.bob \ - build/ba_data/models/wrestlerHand.bob \ - build/ba_data/models/wrestlerHead.bob \ - build/ba_data/models/wrestlerLowerLeg.bob \ - build/ba_data/models/wrestlerPelvis.bob \ - build/ba_data/models/wrestlerToes.bob \ - build/ba_data/models/wrestlerTorso.bob \ - build/ba_data/models/wrestlerUpperArm.bob \ - build/ba_data/models/wrestlerUpperLeg.bob \ - build/ba_data/models/zigZagLevel.bob \ - build/ba_data/models/zigZagLevelBottom.bob \ - build/ba_data/models/zoeForeArm.bob \ - build/ba_data/models/zoeHand.bob \ - build/ba_data/models/zoeHead.bob \ - build/ba_data/models/zoeLowerLeg.bob \ - build/ba_data/models/zoePelvis.bob \ - build/ba_data/models/zoeToes.bob \ - build/ba_data/models/zoeTorso.bob \ - build/ba_data/models/zoeUpperArm.bob \ - build/ba_data/models/zoeUpperLeg.bob - -FONT_TARGETS = \ - build/ba_data/fonts/fontSmall0.fdata \ - build/ba_data/fonts/fontSmall1.fdata \ - build/ba_data/fonts/fontSmall2.fdata \ - build/ba_data/fonts/fontSmall3.fdata \ - build/ba_data/fonts/fontSmall4.fdata \ - build/ba_data/fonts/fontSmall5.fdata \ - build/ba_data/fonts/fontSmall6.fdata \ - build/ba_data/fonts/fontSmall7.fdata - -PEM_TARGETS = \ - build/ba_data/python-site-packages/certifi/cacert.pem - -DATA_TARGETS = \ - build/ba_data/data/langdata.json \ - build/ba_data/data/languages/arabic.json \ - build/ba_data/data/languages/belarussian.json \ - build/ba_data/data/languages/chinese.json \ - build/ba_data/data/languages/chinesetraditional.json \ - build/ba_data/data/languages/croatian.json \ - build/ba_data/data/languages/czech.json \ - build/ba_data/data/languages/danish.json \ - build/ba_data/data/languages/dutch.json \ - build/ba_data/data/languages/english.json \ - build/ba_data/data/languages/esperanto.json \ - build/ba_data/data/languages/filipino.json \ - build/ba_data/data/languages/french.json \ - build/ba_data/data/languages/german.json \ - build/ba_data/data/languages/gibberish.json \ - build/ba_data/data/languages/greek.json \ - build/ba_data/data/languages/hindi.json \ - build/ba_data/data/languages/hungarian.json \ - build/ba_data/data/languages/indonesian.json \ - build/ba_data/data/languages/italian.json \ - build/ba_data/data/languages/korean.json \ - build/ba_data/data/languages/malay.json \ - build/ba_data/data/languages/persian.json \ - build/ba_data/data/languages/polish.json \ - build/ba_data/data/languages/portuguese.json \ - build/ba_data/data/languages/romanian.json \ - build/ba_data/data/languages/russian.json \ - build/ba_data/data/languages/serbian.json \ - build/ba_data/data/languages/slovak.json \ - build/ba_data/data/languages/spanish.json \ - build/ba_data/data/languages/swedish.json \ - build/ba_data/data/languages/tamil.json \ - build/ba_data/data/languages/thai.json \ - build/ba_data/data/languages/turkish.json \ - build/ba_data/data/languages/ukrainian.json \ - build/ba_data/data/languages/venetian.json \ - build/ba_data/data/languages/vietnamese.json \ - build/ba_data/data/maps/big_g.json \ - build/ba_data/data/maps/bridgit.json \ - build/ba_data/data/maps/courtyard.json \ - build/ba_data/data/maps/crag_castle.json \ - build/ba_data/data/maps/doom_shroom.json \ - build/ba_data/data/maps/football_stadium.json \ - build/ba_data/data/maps/happy_thoughts.json \ - build/ba_data/data/maps/hockey_stadium.json \ - build/ba_data/data/maps/lake_frigid.json \ - build/ba_data/data/maps/monkey_face.json \ - build/ba_data/data/maps/rampage.json \ - build/ba_data/data/maps/roundabout.json \ - build/ba_data/data/maps/step_right_up.json \ - build/ba_data/data/maps/the_pad.json \ - build/ba_data/data/maps/tip_top.json \ - build/ba_data/data/maps/tower_d.json \ - build/ba_data/data/maps/zig_zag.json - -AUDIO_TARGETS = \ - build/ba_data/audio/achievement.ogg \ - build/ba_data/audio/actionHero1.ogg \ - build/ba_data/audio/actionHero2.ogg \ - build/ba_data/audio/actionHero3.ogg \ - build/ba_data/audio/actionHero4.ogg \ - build/ba_data/audio/actionHeroDeath.ogg \ - build/ba_data/audio/actionHeroFall.ogg \ - build/ba_data/audio/actionHeroHit1.ogg \ - build/ba_data/audio/actionHeroHit2.ogg \ - build/ba_data/audio/activateBeep.ogg \ - build/ba_data/audio/agent1.ogg \ - build/ba_data/audio/agent2.ogg \ - build/ba_data/audio/agent3.ogg \ - build/ba_data/audio/agent4.ogg \ - build/ba_data/audio/agentDeath.ogg \ - build/ba_data/audio/agentFall.ogg \ - build/ba_data/audio/agentHit1.ogg \ - build/ba_data/audio/agentHit2.ogg \ - build/ba_data/audio/alarm.ogg \ - build/ba_data/audio/ali1.ogg \ - build/ba_data/audio/ali2.ogg \ - build/ba_data/audio/ali3.ogg \ - build/ba_data/audio/ali4.ogg \ - build/ba_data/audio/aliDeath.ogg \ - build/ba_data/audio/aliFall.ogg \ - build/ba_data/audio/aliHit1.ogg \ - build/ba_data/audio/aliHit2.ogg \ - build/ba_data/audio/alien1.ogg \ - build/ba_data/audio/alien2.ogg \ - build/ba_data/audio/alien3.ogg \ - build/ba_data/audio/alien4.ogg \ - build/ba_data/audio/alienDeath.ogg \ - build/ba_data/audio/alienFall.ogg \ - build/ba_data/audio/alienHit1.ogg \ - build/ba_data/audio/alienHit2.ogg \ - build/ba_data/audio/announceEight.ogg \ - build/ba_data/audio/announceFive.ogg \ - build/ba_data/audio/announceFour.ogg \ - build/ba_data/audio/announceNine.ogg \ - build/ba_data/audio/announceOne.ogg \ - build/ba_data/audio/announceSeven.ogg \ - build/ba_data/audio/announceSix.ogg \ - build/ba_data/audio/announceTen.ogg \ - build/ba_data/audio/announceThree.ogg \ - build/ba_data/audio/announceTwo.ogg \ - build/ba_data/audio/assassin1.ogg \ - build/ba_data/audio/assassin2.ogg \ - build/ba_data/audio/assassin3.ogg \ - build/ba_data/audio/assassin4.ogg \ - build/ba_data/audio/assassinDeath.ogg \ - build/ba_data/audio/assassinFall.ogg \ - build/ba_data/audio/assassinHit1.ogg \ - build/ba_data/audio/assassinHit2.ogg \ - build/ba_data/audio/bear1.ogg \ - build/ba_data/audio/bear2.ogg \ - build/ba_data/audio/bear3.ogg \ - build/ba_data/audio/bear4.ogg \ - build/ba_data/audio/bearDeath.ogg \ - build/ba_data/audio/bearFall.ogg \ - build/ba_data/audio/bearHit1.ogg \ - build/ba_data/audio/bearHit2.ogg \ - build/ba_data/audio/bellHigh.ogg \ - build/ba_data/audio/bellLow.ogg \ - build/ba_data/audio/bellMed.ogg \ - build/ba_data/audio/bigImpact.ogg \ - build/ba_data/audio/bigImpact2.ogg \ - build/ba_data/audio/blank.ogg \ - build/ba_data/audio/blip.ogg \ - build/ba_data/audio/block.ogg \ - build/ba_data/audio/bombDrop01.ogg \ - build/ba_data/audio/bombDrop02.ogg \ - build/ba_data/audio/bombRoll01.ogg \ - build/ba_data/audio/bones1.ogg \ - build/ba_data/audio/bones2.ogg \ - build/ba_data/audio/bones3.ogg \ - build/ba_data/audio/bonesDeath.ogg \ - build/ba_data/audio/bonesFall.ogg \ - build/ba_data/audio/boo.ogg \ - build/ba_data/audio/boxDrop.ogg \ - build/ba_data/audio/boxingBell.ogg \ - build/ba_data/audio/bunny1.ogg \ - build/ba_data/audio/bunny2.ogg \ - build/ba_data/audio/bunny3.ogg \ - build/ba_data/audio/bunny4.ogg \ - build/ba_data/audio/bunnyDeath.ogg \ - build/ba_data/audio/bunnyFall.ogg \ - build/ba_data/audio/bunnyHit1.ogg \ - build/ba_data/audio/bunnyHit2.ogg \ - build/ba_data/audio/bunnyJump.ogg \ - build/ba_data/audio/cashRegister.ogg \ - build/ba_data/audio/cashRegister2.ogg \ - build/ba_data/audio/charSelectMusic.ogg \ - build/ba_data/audio/cheer.ogg \ - build/ba_data/audio/click01.ogg \ - build/ba_data/audio/corkPop.ogg \ - build/ba_data/audio/cowboy1.ogg \ - build/ba_data/audio/cowboy2.ogg \ - build/ba_data/audio/cowboy3.ogg \ - build/ba_data/audio/cowboy4.ogg \ - build/ba_data/audio/cowboyDeath.ogg \ - build/ba_data/audio/cowboyFall.ogg \ - build/ba_data/audio/cowboyHit1.ogg \ - build/ba_data/audio/cowboyHit2.ogg \ - build/ba_data/audio/crowdChant.ogg \ - build/ba_data/audio/cyborg1.ogg \ - build/ba_data/audio/cyborg2.ogg \ - build/ba_data/audio/cyborg3.ogg \ - build/ba_data/audio/cyborg4.ogg \ - build/ba_data/audio/cyborgDeath.ogg \ - build/ba_data/audio/cyborgFall.ogg \ - build/ba_data/audio/cyborgHit1.ogg \ - build/ba_data/audio/cyborgHit2.ogg \ - build/ba_data/audio/cymbal.ogg \ - build/ba_data/audio/debrisFall.ogg \ - build/ba_data/audio/deek.ogg \ - build/ba_data/audio/deek2.ogg \ - build/ba_data/audio/ding.ogg \ - build/ba_data/audio/dingSmall.ogg \ - build/ba_data/audio/dingSmallHigh.ogg \ - build/ba_data/audio/dripity.ogg \ - build/ba_data/audio/drumRoll.ogg \ - build/ba_data/audio/error.ogg \ - build/ba_data/audio/explosion01.ogg \ - build/ba_data/audio/explosion02.ogg \ - build/ba_data/audio/explosion03.ogg \ - build/ba_data/audio/explosion04.ogg \ - build/ba_data/audio/explosion05.ogg \ - build/ba_data/audio/fanfare.ogg \ - build/ba_data/audio/flagCatcherMusic.ogg \ - build/ba_data/audio/flyingMusic.ogg \ - build/ba_data/audio/foghorn.ogg \ - build/ba_data/audio/footImpact01.ogg \ - build/ba_data/audio/footImpact02.ogg \ - build/ba_data/audio/footImpact03.ogg \ - build/ba_data/audio/forwardMarchMusic.ogg \ - build/ba_data/audio/freeze.ogg \ - build/ba_data/audio/frosty01.ogg \ - build/ba_data/audio/frosty02.ogg \ - build/ba_data/audio/frosty03.ogg \ - build/ba_data/audio/frosty04.ogg \ - build/ba_data/audio/frosty05.ogg \ - build/ba_data/audio/frostyDeath.ogg \ - build/ba_data/audio/frostyFall.ogg \ - build/ba_data/audio/frostyHit01.ogg \ - build/ba_data/audio/frostyHit02.ogg \ - build/ba_data/audio/frostyHit03.ogg \ - build/ba_data/audio/fuse01.ogg \ - build/ba_data/audio/gladiator1.ogg \ - build/ba_data/audio/gladiator2.ogg \ - build/ba_data/audio/gladiator3.ogg \ - build/ba_data/audio/gladiator4.ogg \ - build/ba_data/audio/gladiatorDeath.ogg \ - build/ba_data/audio/gladiatorFall.ogg \ - build/ba_data/audio/gladiatorHit1.ogg \ - build/ba_data/audio/gladiatorHit2.ogg \ - build/ba_data/audio/gong.ogg \ - build/ba_data/audio/grandRompMusic.ogg \ - build/ba_data/audio/gravelSkid.ogg \ - build/ba_data/audio/gunCocking.ogg \ - build/ba_data/audio/healthPowerup.ogg \ - build/ba_data/audio/hiss.ogg \ - build/ba_data/audio/impactHard.ogg \ - build/ba_data/audio/impactHard2.ogg \ - build/ba_data/audio/impactHard3.ogg \ - build/ba_data/audio/impactMedium.ogg \ - build/ba_data/audio/impactMedium2.ogg \ - build/ba_data/audio/jack01.ogg \ - build/ba_data/audio/jack02.ogg \ - build/ba_data/audio/jack03.ogg \ - build/ba_data/audio/jack04.ogg \ - build/ba_data/audio/jack05.ogg \ - build/ba_data/audio/jack06.ogg \ - build/ba_data/audio/jackDeath01.ogg \ - build/ba_data/audio/jackFall01.ogg \ - build/ba_data/audio/jackHit01.ogg \ - build/ba_data/audio/jackHit02.ogg \ - build/ba_data/audio/jackHit03.ogg \ - build/ba_data/audio/jackHit04.ogg \ - build/ba_data/audio/jackHit05.ogg \ - build/ba_data/audio/jackHit06.ogg \ - build/ba_data/audio/jackHit07.ogg \ - build/ba_data/audio/jumpsuit1.ogg \ - build/ba_data/audio/jumpsuit2.ogg \ - build/ba_data/audio/jumpsuit3.ogg \ - build/ba_data/audio/jumpsuit4.ogg \ - build/ba_data/audio/jumpsuitDeath.ogg \ - build/ba_data/audio/jumpsuitFall.ogg \ - build/ba_data/audio/jumpsuitHit1.ogg \ - build/ba_data/audio/jumpsuitHit2.ogg \ - build/ba_data/audio/kronk1.ogg \ - build/ba_data/audio/kronk10.ogg \ - build/ba_data/audio/kronk2.ogg \ - build/ba_data/audio/kronk3.ogg \ - build/ba_data/audio/kronk4.ogg \ - build/ba_data/audio/kronk5.ogg \ - build/ba_data/audio/kronk6.ogg \ - build/ba_data/audio/kronk7.ogg \ - build/ba_data/audio/kronk8.ogg \ - build/ba_data/audio/kronk9.ogg \ - build/ba_data/audio/kronkDeath.ogg \ - build/ba_data/audio/kronkFall.ogg \ - build/ba_data/audio/laser.ogg \ - build/ba_data/audio/laserReverse.ogg \ - build/ba_data/audio/mel01.ogg \ - build/ba_data/audio/mel02.ogg \ - build/ba_data/audio/mel03.ogg \ - build/ba_data/audio/mel04.ogg \ - build/ba_data/audio/mel05.ogg \ - build/ba_data/audio/mel06.ogg \ - build/ba_data/audio/mel07.ogg \ - build/ba_data/audio/mel08.ogg \ - build/ba_data/audio/mel09.ogg \ - build/ba_data/audio/mel10.ogg \ - build/ba_data/audio/melDeath01.ogg \ - build/ba_data/audio/melFall01.ogg \ - build/ba_data/audio/menuMusic.ogg \ - build/ba_data/audio/metalHit.ogg \ - build/ba_data/audio/metalSkid.ogg \ - build/ba_data/audio/ninjaAttack1.ogg \ - build/ba_data/audio/ninjaAttack2.ogg \ - build/ba_data/audio/ninjaAttack3.ogg \ - build/ba_data/audio/ninjaAttack4.ogg \ - build/ba_data/audio/ninjaAttack5.ogg \ - build/ba_data/audio/ninjaAttack6.ogg \ - build/ba_data/audio/ninjaAttack7.ogg \ - build/ba_data/audio/ninjaDeath1.ogg \ - build/ba_data/audio/ninjaFall1.ogg \ - build/ba_data/audio/ninjaHit1.ogg \ - build/ba_data/audio/ninjaHit2.ogg \ - build/ba_data/audio/ninjaHit3.ogg \ - build/ba_data/audio/ninjaHit4.ogg \ - build/ba_data/audio/ninjaHit5.ogg \ - build/ba_data/audio/ninjaHit6.ogg \ - build/ba_data/audio/ninjaHit7.ogg \ - build/ba_data/audio/ninjaHit8.ogg \ - build/ba_data/audio/oldLady1.ogg \ - build/ba_data/audio/oldLady2.ogg \ - build/ba_data/audio/oldLady3.ogg \ - build/ba_data/audio/oldLady4.ogg \ - build/ba_data/audio/oldLadyDeath.ogg \ - build/ba_data/audio/oldLadyFall.ogg \ - build/ba_data/audio/oldLadyHit1.ogg \ - build/ba_data/audio/oldLadyHit2.ogg \ - build/ba_data/audio/ooh.ogg \ - build/ba_data/audio/operaSinger1.ogg \ - build/ba_data/audio/operaSinger2.ogg \ - build/ba_data/audio/operaSinger3.ogg \ - build/ba_data/audio/operaSinger4.ogg \ - build/ba_data/audio/operaSingerDeath.ogg \ - build/ba_data/audio/operaSingerFall.ogg \ - build/ba_data/audio/operaSingerHit1.ogg \ - build/ba_data/audio/operaSingerHit2.ogg \ - build/ba_data/audio/orchestraHit.ogg \ - build/ba_data/audio/orchestraHit2.ogg \ - build/ba_data/audio/orchestraHit3.ogg \ - build/ba_data/audio/orchestraHit4.ogg \ - build/ba_data/audio/orchestraHitBig1.ogg \ - build/ba_data/audio/orchestraHitBig2.ogg \ - build/ba_data/audio/penguin1.ogg \ - build/ba_data/audio/penguin2.ogg \ - build/ba_data/audio/penguin3.ogg \ - build/ba_data/audio/penguin4.ogg \ - build/ba_data/audio/penguinDeath.ogg \ - build/ba_data/audio/penguinFall.ogg \ - build/ba_data/audio/penguinHit1.ogg \ - build/ba_data/audio/penguinHit2.ogg \ - build/ba_data/audio/pixie1.ogg \ - build/ba_data/audio/pixie2.ogg \ - build/ba_data/audio/pixie3.ogg \ - build/ba_data/audio/pixie4.ogg \ - build/ba_data/audio/pixieDeath.ogg \ - build/ba_data/audio/pixieFall.ogg \ - build/ba_data/audio/pixieHit1.ogg \ - build/ba_data/audio/pixieHit2.ogg \ - build/ba_data/audio/playerDeath.ogg \ - build/ba_data/audio/playerLeft.ogg \ - build/ba_data/audio/pop01.ogg \ - build/ba_data/audio/powerdown01.ogg \ - build/ba_data/audio/powerup01.ogg \ - build/ba_data/audio/punch01.ogg \ - build/ba_data/audio/punchStrong01.ogg \ - build/ba_data/audio/punchStrong02.ogg \ - build/ba_data/audio/punchSwish.ogg \ - build/ba_data/audio/punchWeak01.ogg \ - build/ba_data/audio/raceBeep1.ogg \ - build/ba_data/audio/raceBeep2.ogg \ - build/ba_data/audio/refWhistle.ogg \ - build/ba_data/audio/robot1.ogg \ - build/ba_data/audio/robot2.ogg \ - build/ba_data/audio/robot3.ogg \ - build/ba_data/audio/robot4.ogg \ - build/ba_data/audio/robotDeath.ogg \ - build/ba_data/audio/robotFall.ogg \ - build/ba_data/audio/robotHit1.ogg \ - build/ba_data/audio/robotHit2.ogg \ - build/ba_data/audio/runAwayMusic.ogg \ - build/ba_data/audio/santa01.ogg \ - build/ba_data/audio/santa02.ogg \ - build/ba_data/audio/santa03.ogg \ - build/ba_data/audio/santa04.ogg \ - build/ba_data/audio/santa05.ogg \ - build/ba_data/audio/santaDeath.ogg \ - build/ba_data/audio/santaFall.ogg \ - build/ba_data/audio/santaHit01.ogg \ - build/ba_data/audio/santaHit02.ogg \ - build/ba_data/audio/santaHit03.ogg \ - build/ba_data/audio/santaHit04.ogg \ - build/ba_data/audio/scamper01.ogg \ - build/ba_data/audio/scaryMusic.ogg \ - build/ba_data/audio/score.ogg \ - build/ba_data/audio/scoreHit01.ogg \ - build/ba_data/audio/scoreHit02.ogg \ - build/ba_data/audio/scoreIncrease.ogg \ - build/ba_data/audio/scoresEpicMusic.ogg \ - build/ba_data/audio/shatter.ogg \ - build/ba_data/audio/shieldDown.ogg \ - build/ba_data/audio/shieldHit.ogg \ - build/ba_data/audio/shieldUp.ogg \ - build/ba_data/audio/skid01.ogg \ - build/ba_data/audio/slowEpicMusic.ogg \ - build/ba_data/audio/sparkle01.ogg \ - build/ba_data/audio/sparkle02.ogg \ - build/ba_data/audio/sparkle03.ogg \ - build/ba_data/audio/spawn.ogg \ - build/ba_data/audio/spazAttack01.ogg \ - build/ba_data/audio/spazAttack02.ogg \ - build/ba_data/audio/spazAttack03.ogg \ - build/ba_data/audio/spazAttack04.ogg \ - build/ba_data/audio/spazDeath01.ogg \ - build/ba_data/audio/spazEff.ogg \ - build/ba_data/audio/spazFall01.ogg \ - build/ba_data/audio/spazImpact01.ogg \ - build/ba_data/audio/spazImpact02.ogg \ - build/ba_data/audio/spazImpact03.ogg \ - build/ba_data/audio/spazImpact04.ogg \ - build/ba_data/audio/spazJump01.ogg \ - build/ba_data/audio/spazJump02.ogg \ - build/ba_data/audio/spazJump03.ogg \ - build/ba_data/audio/spazJump04.ogg \ - build/ba_data/audio/spazOw.ogg \ - build/ba_data/audio/spazPickup01.ogg \ - build/ba_data/audio/spazScream01.ogg \ - build/ba_data/audio/splatter.ogg \ - build/ba_data/audio/sportsMusic.ogg \ - build/ba_data/audio/stickyImpact.ogg \ - build/ba_data/audio/superPunch.ogg \ - build/ba_data/audio/superhero1.ogg \ - build/ba_data/audio/superhero2.ogg \ - build/ba_data/audio/superhero3.ogg \ - build/ba_data/audio/superhero4.ogg \ - build/ba_data/audio/superheroDeath.ogg \ - build/ba_data/audio/superheroFall.ogg \ - build/ba_data/audio/superheroHit1.ogg \ - build/ba_data/audio/superheroHit2.ogg \ - build/ba_data/audio/survivalMusic.ogg \ - build/ba_data/audio/swip.ogg \ - build/ba_data/audio/swip2.ogg \ - build/ba_data/audio/swish.ogg \ - build/ba_data/audio/swish2.ogg \ - build/ba_data/audio/swish3.ogg \ - build/ba_data/audio/tap.ogg \ - build/ba_data/audio/technoHit01.ogg \ - build/ba_data/audio/tick.ogg \ - build/ba_data/audio/ticking.ogg \ - build/ba_data/audio/tickingCrazy.ogg \ - build/ba_data/audio/toTheDeathMusic.ogg \ - build/ba_data/audio/trashRummage.ogg \ - build/ba_data/audio/victoryMusic.ogg \ - build/ba_data/audio/warnBeep.ogg \ - build/ba_data/audio/warnBeeps.ogg \ - build/ba_data/audio/warrior1.ogg \ - build/ba_data/audio/warrior2.ogg \ - build/ba_data/audio/warrior3.ogg \ - build/ba_data/audio/warrior4.ogg \ - build/ba_data/audio/warriorDeath.ogg \ - build/ba_data/audio/warriorFall.ogg \ - build/ba_data/audio/warriorHit1.ogg \ - build/ba_data/audio/warriorHit2.ogg \ - build/ba_data/audio/whenJohnnyComesMarchingHomeMusic.ogg \ - build/ba_data/audio/witch1.ogg \ - build/ba_data/audio/witch2.ogg \ - build/ba_data/audio/witch3.ogg \ - build/ba_data/audio/witch4.ogg \ - build/ba_data/audio/witchDeath.ogg \ - build/ba_data/audio/witchFall.ogg \ - build/ba_data/audio/witchHit1.ogg \ - build/ba_data/audio/witchHit2.ogg \ - build/ba_data/audio/wizard1.ogg \ - build/ba_data/audio/wizard2.ogg \ - build/ba_data/audio/wizard3.ogg \ - build/ba_data/audio/wizard4.ogg \ - build/ba_data/audio/wizardDeath.ogg \ - build/ba_data/audio/wizardFall.ogg \ - build/ba_data/audio/wizardHit1.ogg \ - build/ba_data/audio/wizardHit2.ogg \ - build/ba_data/audio/woodDebrisFall.ogg \ - build/ba_data/audio/wrestler1.ogg \ - build/ba_data/audio/wrestler2.ogg \ - build/ba_data/audio/wrestler3.ogg \ - build/ba_data/audio/wrestler4.ogg \ - build/ba_data/audio/wrestlerDeath.ogg \ - build/ba_data/audio/wrestlerFall.ogg \ - build/ba_data/audio/wrestlerHit1.ogg \ - build/ba_data/audio/wrestlerHit2.ogg \ - build/ba_data/audio/zoeAttack01.ogg \ - build/ba_data/audio/zoeAttack02.ogg \ - build/ba_data/audio/zoeAttack03.ogg \ - build/ba_data/audio/zoeAttack04.ogg \ - build/ba_data/audio/zoeDeath01.ogg \ - build/ba_data/audio/zoeEff.ogg \ - build/ba_data/audio/zoeFall01.ogg \ - build/ba_data/audio/zoeImpact01.ogg \ - build/ba_data/audio/zoeImpact02.ogg \ - build/ba_data/audio/zoeImpact03.ogg \ - build/ba_data/audio/zoeImpact04.ogg \ - build/ba_data/audio/zoeJump01.ogg \ - build/ba_data/audio/zoeJump02.ogg \ - build/ba_data/audio/zoeJump03.ogg \ - build/ba_data/audio/zoeOw.ogg \ - build/ba_data/audio/zoePickup01.ogg \ - build/ba_data/audio/zoeScream01.ogg - -TEX2D_DDS_TARGETS = \ - build/ba_data/textures/achievementBoxer.dds \ - build/ba_data/textures/achievementCrossHair.dds \ - build/ba_data/textures/achievementDualWielding.dds \ - build/ba_data/textures/achievementEmpty.dds \ - build/ba_data/textures/achievementFlawlessVictory.dds \ - build/ba_data/textures/achievementFootballShutout.dds \ - build/ba_data/textures/achievementFootballVictory.dds \ - build/ba_data/textures/achievementFreeLoader.dds \ - build/ba_data/textures/achievementGotTheMoves.dds \ - build/ba_data/textures/achievementInControl.dds \ - build/ba_data/textures/achievementMedalLarge.dds \ - build/ba_data/textures/achievementMedalMedium.dds \ - build/ba_data/textures/achievementMedalSmall.dds \ - build/ba_data/textures/achievementMine.dds \ - build/ba_data/textures/achievementOffYouGo.dds \ - build/ba_data/textures/achievementOnslaught.dds \ - build/ba_data/textures/achievementOutline.dds \ - build/ba_data/textures/achievementRunaround.dds \ - build/ba_data/textures/achievementSharingIsCaring.dds \ - build/ba_data/textures/achievementStayinAlive.dds \ - build/ba_data/textures/achievementSuperPunch.dds \ - build/ba_data/textures/achievementTNT.dds \ - build/ba_data/textures/achievementTeamPlayer.dds \ - build/ba_data/textures/achievementWall.dds \ - build/ba_data/textures/achievementsIcon.dds \ - build/ba_data/textures/actionButtons.dds \ - build/ba_data/textures/actionHeroColor.dds \ - build/ba_data/textures/actionHeroColorMask.dds \ - build/ba_data/textures/actionHeroIcon.dds \ - build/ba_data/textures/actionHeroIconColorMask.dds \ - build/ba_data/textures/advancedIcon.dds \ - build/ba_data/textures/agentColor.dds \ - build/ba_data/textures/agentColorMask.dds \ - build/ba_data/textures/agentIcon.dds \ - build/ba_data/textures/agentIconColorMask.dds \ - build/ba_data/textures/aliBSRemoteIOSQR.dds \ - build/ba_data/textures/aliColor.dds \ - build/ba_data/textures/aliColorMask.dds \ - build/ba_data/textures/aliControllerQR.dds \ - build/ba_data/textures/aliIcon.dds \ - build/ba_data/textures/aliIconColorMask.dds \ - build/ba_data/textures/aliSplash.dds \ - build/ba_data/textures/alienColor.dds \ - build/ba_data/textures/alienColorMask.dds \ - build/ba_data/textures/alienIcon.dds \ - build/ba_data/textures/alienIconColorMask.dds \ - build/ba_data/textures/alwaysLandBGColor.dds \ - build/ba_data/textures/alwaysLandLevelColor.dds \ - build/ba_data/textures/alwaysLandPreview.dds \ - build/ba_data/textures/analogStick.dds \ - build/ba_data/textures/arrow.dds \ - build/ba_data/textures/assassinColor.dds \ - build/ba_data/textures/assassinColorMask.dds \ - build/ba_data/textures/assassinIcon.dds \ - build/ba_data/textures/assassinIconColorMask.dds \ - build/ba_data/textures/audioIcon.dds \ - build/ba_data/textures/backIcon.dds \ - build/ba_data/textures/bar.dds \ - build/ba_data/textures/bearColor.dds \ - build/ba_data/textures/bearColorMask.dds \ - build/ba_data/textures/bearIcon.dds \ - build/ba_data/textures/bearIconColorMask.dds \ - build/ba_data/textures/bg.dds \ - build/ba_data/textures/bigG.dds \ - build/ba_data/textures/bigGPreview.dds \ - build/ba_data/textures/black.dds \ - build/ba_data/textures/bombButton.dds \ - build/ba_data/textures/bombColor.dds \ - build/ba_data/textures/bombColorIce.dds \ - build/ba_data/textures/bombStickyColor.dds \ - build/ba_data/textures/bonesColor.dds \ - build/ba_data/textures/bonesColorMask.dds \ - build/ba_data/textures/bonesIcon.dds \ - build/ba_data/textures/bonesIconColorMask.dds \ - build/ba_data/textures/boxingGlovesColor.dds \ - build/ba_data/textures/bridgitLevelColor.dds \ - build/ba_data/textures/bridgitPreview.dds \ - build/ba_data/textures/bunnyColor.dds \ - build/ba_data/textures/bunnyColorMask.dds \ - build/ba_data/textures/bunnyIcon.dds \ - build/ba_data/textures/bunnyIconColorMask.dds \ - build/ba_data/textures/buttonBomb.dds \ - build/ba_data/textures/buttonJump.dds \ - build/ba_data/textures/buttonPickUp.dds \ - build/ba_data/textures/buttonPunch.dds \ - build/ba_data/textures/buttonSquare.dds \ - build/ba_data/textures/chTitleChar1.dds \ - build/ba_data/textures/chTitleChar2.dds \ - build/ba_data/textures/chTitleChar3.dds \ - build/ba_data/textures/chTitleChar4.dds \ - build/ba_data/textures/chTitleChar5.dds \ - build/ba_data/textures/characterIconMask.dds \ - build/ba_data/textures/chestIcon.dds \ - build/ba_data/textures/chestIconEmpty.dds \ - build/ba_data/textures/chestIconMulti.dds \ - build/ba_data/textures/chestOpenIcon.dds \ - build/ba_data/textures/circle.dds \ - build/ba_data/textures/circleNoAlpha.dds \ - build/ba_data/textures/circleOutline.dds \ - build/ba_data/textures/circleOutlineNoAlpha.dds \ - build/ba_data/textures/circleShadow.dds \ - build/ba_data/textures/circleZigZag.dds \ - build/ba_data/textures/coin.dds \ - build/ba_data/textures/controllerIcon.dds \ - build/ba_data/textures/courtyardLevelColor.dds \ - build/ba_data/textures/courtyardPreview.dds \ - build/ba_data/textures/cowboyColor.dds \ - build/ba_data/textures/cowboyColorMask.dds \ - build/ba_data/textures/cowboyIcon.dds \ - build/ba_data/textures/cowboyIconColorMask.dds \ - build/ba_data/textures/cragCastleLevelColor.dds \ - build/ba_data/textures/cragCastlePreview.dds \ - build/ba_data/textures/crossOut.dds \ - build/ba_data/textures/crossOutMask.dds \ - build/ba_data/textures/cursor.dds \ - build/ba_data/textures/cuteSpaz.dds \ - build/ba_data/textures/cyborgColor.dds \ - build/ba_data/textures/cyborgColorMask.dds \ - build/ba_data/textures/cyborgIcon.dds \ - build/ba_data/textures/cyborgIconColorMask.dds \ - build/ba_data/textures/discordLogo.dds \ - build/ba_data/textures/doomShroomBGColor.dds \ - build/ba_data/textures/doomShroomLevelColor.dds \ - build/ba_data/textures/doomShroomPreview.dds \ - build/ba_data/textures/downButton.dds \ - build/ba_data/textures/egg1.dds \ - build/ba_data/textures/egg2.dds \ - build/ba_data/textures/egg3.dds \ - build/ba_data/textures/egg4.dds \ - build/ba_data/textures/eggTex1.dds \ - build/ba_data/textures/eggTex2.dds \ - build/ba_data/textures/eggTex3.dds \ - build/ba_data/textures/empty.dds \ - build/ba_data/textures/explosion.dds \ - build/ba_data/textures/eyeColor.dds \ - build/ba_data/textures/eyeColorTintMask.dds \ - build/ba_data/textures/file.dds \ - build/ba_data/textures/flagColor.dds \ - build/ba_data/textures/flagPoleColor.dds \ - build/ba_data/textures/folder.dds \ - build/ba_data/textures/fontBig.dds \ - build/ba_data/textures/fontExtras.dds \ - build/ba_data/textures/fontExtras2.dds \ - build/ba_data/textures/fontExtras3.dds \ - build/ba_data/textures/fontExtras4.dds \ - build/ba_data/textures/fontSmall0.dds \ - build/ba_data/textures/fontSmall1.dds \ - build/ba_data/textures/fontSmall2.dds \ - build/ba_data/textures/fontSmall3.dds \ - build/ba_data/textures/fontSmall4.dds \ - build/ba_data/textures/fontSmall5.dds \ - build/ba_data/textures/fontSmall6.dds \ - build/ba_data/textures/fontSmall7.dds \ - build/ba_data/textures/footballStadium.dds \ - build/ba_data/textures/footballStadiumPreview.dds \ - build/ba_data/textures/frameInset.dds \ - build/ba_data/textures/frostyColor.dds \ - build/ba_data/textures/frostyColorMask.dds \ - build/ba_data/textures/frostyIcon.dds \ - build/ba_data/textures/frostyIconColorMask.dds \ - build/ba_data/textures/fuse.dds \ - build/ba_data/textures/gameCenterIcon.dds \ - build/ba_data/textures/gameCircleIcon.dds \ - build/ba_data/textures/githubLogo.dds \ - build/ba_data/textures/gladiatorColor.dds \ - build/ba_data/textures/gladiatorColorMask.dds \ - build/ba_data/textures/gladiatorIcon.dds \ - build/ba_data/textures/gladiatorIconColorMask.dds \ - build/ba_data/textures/glow.dds \ - build/ba_data/textures/googlePlayAchievementsIcon.dds \ - build/ba_data/textures/googlePlayGamesIcon.dds \ - build/ba_data/textures/googlePlayLeaderboardsIcon.dds \ - build/ba_data/textures/googlePlusIcon.dds \ - build/ba_data/textures/googlePlusSignInButton.dds \ - build/ba_data/textures/graphicsIcon.dds \ - build/ba_data/textures/heart.dds \ - build/ba_data/textures/hockeyStadium.dds \ - build/ba_data/textures/hockeyStadiumPreview.dds \ - build/ba_data/textures/iconOnslaught.dds \ - build/ba_data/textures/iconRunaround.dds \ - build/ba_data/textures/iircadeLogo.dds \ - build/ba_data/textures/impactBombColor.dds \ - build/ba_data/textures/impactBombColorLit.dds \ - build/ba_data/textures/inventoryIcon.dds \ - build/ba_data/textures/jackColor.dds \ - build/ba_data/textures/jackColorMask.dds \ - build/ba_data/textures/jackIcon.dds \ - build/ba_data/textures/jackIconColorMask.dds \ - build/ba_data/textures/jumpsuitColor.dds \ - build/ba_data/textures/jumpsuitColorMask.dds \ - build/ba_data/textures/jumpsuitIcon.dds \ - build/ba_data/textures/jumpsuitIconColorMask.dds \ - build/ba_data/textures/kronk.dds \ - build/ba_data/textures/kronkColorMask.dds \ - build/ba_data/textures/kronkIcon.dds \ - build/ba_data/textures/kronkIconColorMask.dds \ - build/ba_data/textures/lakeFrigid.dds \ - build/ba_data/textures/lakeFrigidPreview.dds \ - build/ba_data/textures/lakeFrigidReflections.dds \ - build/ba_data/textures/landMine.dds \ - build/ba_data/textures/landMineLit.dds \ - build/ba_data/textures/leaderboardsIcon.dds \ - build/ba_data/textures/leftButton.dds \ - build/ba_data/textures/levelIcon.dds \ - build/ba_data/textures/light.dds \ - build/ba_data/textures/lightSharp.dds \ - build/ba_data/textures/lightSoft.dds \ - build/ba_data/textures/lock.dds \ - build/ba_data/textures/logIcon.dds \ - build/ba_data/textures/logo.dds \ - build/ba_data/textures/logoEaster.dds \ - build/ba_data/textures/mapPreviewMask.dds \ - build/ba_data/textures/medalBronze.dds \ - build/ba_data/textures/medalComplete.dds \ - build/ba_data/textures/medalGold.dds \ - build/ba_data/textures/medalSilver.dds \ - build/ba_data/textures/melColor.dds \ - build/ba_data/textures/melColorMask.dds \ - build/ba_data/textures/melIcon.dds \ - build/ba_data/textures/melIconColorMask.dds \ - build/ba_data/textures/menuBG.dds \ - build/ba_data/textures/menuButton.dds \ - build/ba_data/textures/menuIcon.dds \ - build/ba_data/textures/merch.dds \ - build/ba_data/textures/meter.dds \ - build/ba_data/textures/monkeyFaceLevelColor.dds \ - build/ba_data/textures/monkeyFacePreview.dds \ - build/ba_data/textures/multiplayerExamples.dds \ - build/ba_data/textures/natureBackgroundColor.dds \ - build/ba_data/textures/neoSpazColor.dds \ - build/ba_data/textures/neoSpazColorMask.dds \ - build/ba_data/textures/neoSpazIcon.dds \ - build/ba_data/textures/neoSpazIconColorMask.dds \ - build/ba_data/textures/nextLevelIcon.dds \ - build/ba_data/textures/ninjaColor.dds \ - build/ba_data/textures/ninjaColorMask.dds \ - build/ba_data/textures/ninjaIcon.dds \ - build/ba_data/textures/ninjaIconColorMask.dds \ - build/ba_data/textures/nub.dds \ - build/ba_data/textures/null.dds \ - build/ba_data/textures/oldLadyColor.dds \ - build/ba_data/textures/oldLadyColorMask.dds \ - build/ba_data/textures/oldLadyIcon.dds \ - build/ba_data/textures/oldLadyIconColorMask.dds \ - build/ba_data/textures/operaSingerColor.dds \ - build/ba_data/textures/operaSingerColorMask.dds \ - build/ba_data/textures/operaSingerIcon.dds \ - build/ba_data/textures/operaSingerIconColorMask.dds \ - build/ba_data/textures/ouyaAButton.dds \ - build/ba_data/textures/ouyaIcon.dds \ - build/ba_data/textures/ouyaOButton.dds \ - build/ba_data/textures/ouyaUButton.dds \ - build/ba_data/textures/ouyaYButton.dds \ - build/ba_data/textures/penguinColor.dds \ - build/ba_data/textures/penguinColorMask.dds \ - build/ba_data/textures/penguinIcon.dds \ - build/ba_data/textures/penguinIconColorMask.dds \ - build/ba_data/textures/pixieColor.dds \ - build/ba_data/textures/pixieColorMask.dds \ - build/ba_data/textures/pixieIcon.dds \ - build/ba_data/textures/pixieIconColorMask.dds \ - build/ba_data/textures/playerLineup.dds \ - build/ba_data/textures/powerupBomb.dds \ - build/ba_data/textures/powerupCurse.dds \ - build/ba_data/textures/powerupHealth.dds \ - build/ba_data/textures/powerupIceBombs.dds \ - build/ba_data/textures/powerupImpactBombs.dds \ - build/ba_data/textures/powerupLandMines.dds \ - build/ba_data/textures/powerupPunch.dds \ - build/ba_data/textures/powerupShield.dds \ - build/ba_data/textures/powerupSpeed.dds \ - build/ba_data/textures/powerupStickyBombs.dds \ - build/ba_data/textures/puckColor.dds \ - build/ba_data/textures/rampageBGColor.dds \ - build/ba_data/textures/rampageBGColor2.dds \ - build/ba_data/textures/rampageLevelColor.dds \ - build/ba_data/textures/rampagePreview.dds \ - build/ba_data/textures/reflectionChar_+x.dds \ - build/ba_data/textures/reflectionChar_+y.dds \ - build/ba_data/textures/reflectionChar_+z.dds \ - build/ba_data/textures/reflectionChar_-x.dds \ - build/ba_data/textures/reflectionChar_-y.dds \ - build/ba_data/textures/reflectionChar_-z.dds \ - build/ba_data/textures/reflectionPowerup_+x.dds \ - build/ba_data/textures/reflectionPowerup_+y.dds \ - build/ba_data/textures/reflectionPowerup_+z.dds \ - build/ba_data/textures/reflectionPowerup_-x.dds \ - build/ba_data/textures/reflectionPowerup_-y.dds \ - build/ba_data/textures/reflectionPowerup_-z.dds \ - build/ba_data/textures/reflectionSharp_+x.dds \ - build/ba_data/textures/reflectionSharp_+y.dds \ - build/ba_data/textures/reflectionSharp_+z.dds \ - build/ba_data/textures/reflectionSharp_-x.dds \ - build/ba_data/textures/reflectionSharp_-y.dds \ - build/ba_data/textures/reflectionSharp_-z.dds \ - build/ba_data/textures/reflectionSharper_+x.dds \ - build/ba_data/textures/reflectionSharper_+y.dds \ - build/ba_data/textures/reflectionSharper_+z.dds \ - build/ba_data/textures/reflectionSharper_-x.dds \ - build/ba_data/textures/reflectionSharper_-y.dds \ - build/ba_data/textures/reflectionSharper_-z.dds \ - build/ba_data/textures/reflectionSharpest_+x.dds \ - build/ba_data/textures/reflectionSharpest_+y.dds \ - build/ba_data/textures/reflectionSharpest_+z.dds \ - build/ba_data/textures/reflectionSharpest_-x.dds \ - build/ba_data/textures/reflectionSharpest_-y.dds \ - build/ba_data/textures/reflectionSharpest_-z.dds \ - build/ba_data/textures/reflectionSoft_+x.dds \ - build/ba_data/textures/reflectionSoft_+y.dds \ - build/ba_data/textures/reflectionSoft_+z.dds \ - build/ba_data/textures/reflectionSoft_-x.dds \ - build/ba_data/textures/reflectionSoft_-y.dds \ - build/ba_data/textures/reflectionSoft_-z.dds \ - build/ba_data/textures/replayIcon.dds \ - build/ba_data/textures/rgbStripes.dds \ - build/ba_data/textures/rightButton.dds \ - build/ba_data/textures/robotColor.dds \ - build/ba_data/textures/robotColorMask.dds \ - build/ba_data/textures/robotIcon.dds \ - build/ba_data/textures/robotIconColorMask.dds \ - build/ba_data/textures/roundaboutLevelColor.dds \ - build/ba_data/textures/roundaboutPreview.dds \ - build/ba_data/textures/santaColor.dds \ - build/ba_data/textures/santaColorMask.dds \ - build/ba_data/textures/santaIcon.dds \ - build/ba_data/textures/santaIconColorMask.dds \ - build/ba_data/textures/scorch.dds \ - build/ba_data/textures/scorchBig.dds \ - build/ba_data/textures/scrollWidget.dds \ - build/ba_data/textures/scrollWidgetGlow.dds \ - build/ba_data/textures/settingsIcon.dds \ - build/ba_data/textures/shadow.dds \ - build/ba_data/textures/shadowSharp.dds \ - build/ba_data/textures/shadowSoft.dds \ - build/ba_data/textures/shield.dds \ - build/ba_data/textures/shrapnel1Color.dds \ - build/ba_data/textures/slash.dds \ - build/ba_data/textures/smoke.dds \ - build/ba_data/textures/softRect.dds \ - build/ba_data/textures/softRect2.dds \ - build/ba_data/textures/softRectVertical.dds \ - build/ba_data/textures/sparks.dds \ - build/ba_data/textures/star.dds \ - build/ba_data/textures/startButton.dds \ - build/ba_data/textures/stepRightUpLevelColor.dds \ - build/ba_data/textures/stepRightUpPreview.dds \ - build/ba_data/textures/storeCharacter.dds \ - build/ba_data/textures/storeCharacterEaster.dds \ - build/ba_data/textures/storeCharacterXmas.dds \ - build/ba_data/textures/storeIcon.dds \ - build/ba_data/textures/superheroColor.dds \ - build/ba_data/textures/superheroColorMask.dds \ - build/ba_data/textures/superheroIcon.dds \ - build/ba_data/textures/superheroIconColorMask.dds \ - build/ba_data/textures/textClearButton.dds \ - build/ba_data/textures/thePadLevelColor.dds \ - build/ba_data/textures/thePadPreview.dds \ - build/ba_data/textures/ticketRoll.dds \ - build/ba_data/textures/ticketRollBig.dds \ - build/ba_data/textures/ticketRolls.dds \ - build/ba_data/textures/tickets.dds \ - build/ba_data/textures/ticketsMore.dds \ - build/ba_data/textures/tipTopBGColor.dds \ - build/ba_data/textures/tipTopLevelColor.dds \ - build/ba_data/textures/tipTopPreview.dds \ - build/ba_data/textures/tnt.dds \ - build/ba_data/textures/touchArrows.dds \ - build/ba_data/textures/touchArrowsActions.dds \ - build/ba_data/textures/towerDLevelColor.dds \ - build/ba_data/textures/towerDPreview.dds \ - build/ba_data/textures/treesColor.dds \ - build/ba_data/textures/trophy.dds \ - build/ba_data/textures/tv.dds \ - build/ba_data/textures/uiAtlas.dds \ - build/ba_data/textures/uiAtlas2.dds \ - build/ba_data/textures/upButton.dds \ - build/ba_data/textures/usersButton.dds \ - build/ba_data/textures/vrFillMound.dds \ - build/ba_data/textures/warriorColor.dds \ - build/ba_data/textures/warriorColorMask.dds \ - build/ba_data/textures/warriorIcon.dds \ - build/ba_data/textures/warriorIconColorMask.dds \ - build/ba_data/textures/white.dds \ - build/ba_data/textures/windowHSmallVMed.dds \ - build/ba_data/textures/windowHSmallVSmall.dds \ - build/ba_data/textures/wings.dds \ - build/ba_data/textures/witchColor.dds \ - build/ba_data/textures/witchColorMask.dds \ - build/ba_data/textures/witchIcon.dds \ - build/ba_data/textures/witchIconColorMask.dds \ - build/ba_data/textures/wizardColor.dds \ - build/ba_data/textures/wizardColorMask.dds \ - build/ba_data/textures/wizardIcon.dds \ - build/ba_data/textures/wizardIconColorMask.dds \ - build/ba_data/textures/wrestlerColor.dds \ - build/ba_data/textures/wrestlerColorMask.dds \ - build/ba_data/textures/wrestlerIcon.dds \ - build/ba_data/textures/wrestlerIconColorMask.dds \ - build/ba_data/textures/zigZagLevelColor.dds \ - build/ba_data/textures/zigzagPreview.dds \ - build/ba_data/textures/zoeColor.dds \ - build/ba_data/textures/zoeColorMask.dds \ - build/ba_data/textures/zoeIcon.dds \ - build/ba_data/textures/zoeIconColorMask.dds - -TEX2D_PVR_TARGETS = \ - build/ba_data/textures/achievementBoxer.pvr \ - build/ba_data/textures/achievementCrossHair.pvr \ - build/ba_data/textures/achievementDualWielding.pvr \ - build/ba_data/textures/achievementEmpty.pvr \ - build/ba_data/textures/achievementFlawlessVictory.pvr \ - build/ba_data/textures/achievementFootballShutout.pvr \ - build/ba_data/textures/achievementFootballVictory.pvr \ - build/ba_data/textures/achievementFreeLoader.pvr \ - build/ba_data/textures/achievementGotTheMoves.pvr \ - build/ba_data/textures/achievementInControl.pvr \ - build/ba_data/textures/achievementMedalLarge.pvr \ - build/ba_data/textures/achievementMedalMedium.pvr \ - build/ba_data/textures/achievementMedalSmall.pvr \ - build/ba_data/textures/achievementMine.pvr \ - build/ba_data/textures/achievementOffYouGo.pvr \ - build/ba_data/textures/achievementOnslaught.pvr \ - build/ba_data/textures/achievementOutline.pvr \ - build/ba_data/textures/achievementRunaround.pvr \ - build/ba_data/textures/achievementSharingIsCaring.pvr \ - build/ba_data/textures/achievementStayinAlive.pvr \ - build/ba_data/textures/achievementSuperPunch.pvr \ - build/ba_data/textures/achievementTNT.pvr \ - build/ba_data/textures/achievementTeamPlayer.pvr \ - build/ba_data/textures/achievementWall.pvr \ - build/ba_data/textures/achievementsIcon.pvr \ - build/ba_data/textures/actionButtons.pvr \ - build/ba_data/textures/actionHeroColor.pvr \ - build/ba_data/textures/actionHeroColorMask.pvr \ - build/ba_data/textures/actionHeroIcon.pvr \ - build/ba_data/textures/actionHeroIconColorMask.pvr \ - build/ba_data/textures/advancedIcon.pvr \ - build/ba_data/textures/agentColor.pvr \ - build/ba_data/textures/agentColorMask.pvr \ - build/ba_data/textures/agentIcon.pvr \ - build/ba_data/textures/agentIconColorMask.pvr \ - build/ba_data/textures/aliBSRemoteIOSQR.pvr \ - build/ba_data/textures/aliColor.pvr \ - build/ba_data/textures/aliColorMask.pvr \ - build/ba_data/textures/aliControllerQR.pvr \ - build/ba_data/textures/aliIcon.pvr \ - build/ba_data/textures/aliIconColorMask.pvr \ - build/ba_data/textures/aliSplash.pvr \ - build/ba_data/textures/alienColor.pvr \ - build/ba_data/textures/alienColorMask.pvr \ - build/ba_data/textures/alienIcon.pvr \ - build/ba_data/textures/alienIconColorMask.pvr \ - build/ba_data/textures/alwaysLandBGColor.pvr \ - build/ba_data/textures/alwaysLandLevelColor.pvr \ - build/ba_data/textures/alwaysLandPreview.pvr \ - build/ba_data/textures/analogStick.pvr \ - build/ba_data/textures/arrow.pvr \ - build/ba_data/textures/assassinColor.pvr \ - build/ba_data/textures/assassinColorMask.pvr \ - build/ba_data/textures/assassinIcon.pvr \ - build/ba_data/textures/assassinIconColorMask.pvr \ - build/ba_data/textures/audioIcon.pvr \ - build/ba_data/textures/backIcon.pvr \ - build/ba_data/textures/bar.pvr \ - build/ba_data/textures/bearColor.pvr \ - build/ba_data/textures/bearColorMask.pvr \ - build/ba_data/textures/bearIcon.pvr \ - build/ba_data/textures/bearIconColorMask.pvr \ - build/ba_data/textures/bg.pvr \ - build/ba_data/textures/bigG.pvr \ - build/ba_data/textures/bigGPreview.pvr \ - build/ba_data/textures/black.pvr \ - build/ba_data/textures/bombButton.pvr \ - build/ba_data/textures/bombColor.pvr \ - build/ba_data/textures/bombColorIce.pvr \ - build/ba_data/textures/bombStickyColor.pvr \ - build/ba_data/textures/bonesColor.pvr \ - build/ba_data/textures/bonesColorMask.pvr \ - build/ba_data/textures/bonesIcon.pvr \ - build/ba_data/textures/bonesIconColorMask.pvr \ - build/ba_data/textures/boxingGlovesColor.pvr \ - build/ba_data/textures/bridgitLevelColor.pvr \ - build/ba_data/textures/bridgitPreview.pvr \ - build/ba_data/textures/bunnyColor.pvr \ - build/ba_data/textures/bunnyColorMask.pvr \ - build/ba_data/textures/bunnyIcon.pvr \ - build/ba_data/textures/bunnyIconColorMask.pvr \ - build/ba_data/textures/buttonBomb.pvr \ - build/ba_data/textures/buttonJump.pvr \ - build/ba_data/textures/buttonPickUp.pvr \ - build/ba_data/textures/buttonPunch.pvr \ - build/ba_data/textures/buttonSquare.pvr \ - build/ba_data/textures/chTitleChar1.pvr \ - build/ba_data/textures/chTitleChar2.pvr \ - build/ba_data/textures/chTitleChar3.pvr \ - build/ba_data/textures/chTitleChar4.pvr \ - build/ba_data/textures/chTitleChar5.pvr \ - build/ba_data/textures/characterIconMask.pvr \ - build/ba_data/textures/chestIcon.pvr \ - build/ba_data/textures/chestIconEmpty.pvr \ - build/ba_data/textures/chestIconMulti.pvr \ - build/ba_data/textures/chestOpenIcon.pvr \ - build/ba_data/textures/circle.pvr \ - build/ba_data/textures/circleNoAlpha.pvr \ - build/ba_data/textures/circleOutline.pvr \ - build/ba_data/textures/circleOutlineNoAlpha.pvr \ - build/ba_data/textures/circleShadow.pvr \ - build/ba_data/textures/circleZigZag.pvr \ - build/ba_data/textures/coin.pvr \ - build/ba_data/textures/controllerIcon.pvr \ - build/ba_data/textures/courtyardLevelColor.pvr \ - build/ba_data/textures/courtyardPreview.pvr \ - build/ba_data/textures/cowboyColor.pvr \ - build/ba_data/textures/cowboyColorMask.pvr \ - build/ba_data/textures/cowboyIcon.pvr \ - build/ba_data/textures/cowboyIconColorMask.pvr \ - build/ba_data/textures/cragCastleLevelColor.pvr \ - build/ba_data/textures/cragCastlePreview.pvr \ - build/ba_data/textures/crossOut.pvr \ - build/ba_data/textures/crossOutMask.pvr \ - build/ba_data/textures/cursor.pvr \ - build/ba_data/textures/cuteSpaz.pvr \ - build/ba_data/textures/cyborgColor.pvr \ - build/ba_data/textures/cyborgColorMask.pvr \ - build/ba_data/textures/cyborgIcon.pvr \ - build/ba_data/textures/cyborgIconColorMask.pvr \ - build/ba_data/textures/discordLogo.pvr \ - build/ba_data/textures/doomShroomBGColor.pvr \ - build/ba_data/textures/doomShroomLevelColor.pvr \ - build/ba_data/textures/doomShroomPreview.pvr \ - build/ba_data/textures/downButton.pvr \ - build/ba_data/textures/egg1.pvr \ - build/ba_data/textures/egg2.pvr \ - build/ba_data/textures/egg3.pvr \ - build/ba_data/textures/egg4.pvr \ - build/ba_data/textures/eggTex1.pvr \ - build/ba_data/textures/eggTex2.pvr \ - build/ba_data/textures/eggTex3.pvr \ - build/ba_data/textures/empty.pvr \ - build/ba_data/textures/explosion.pvr \ - build/ba_data/textures/eyeColor.pvr \ - build/ba_data/textures/eyeColorTintMask.pvr \ - build/ba_data/textures/file.pvr \ - build/ba_data/textures/flagColor.pvr \ - build/ba_data/textures/flagPoleColor.pvr \ - build/ba_data/textures/folder.pvr \ - build/ba_data/textures/fontBig.pvr \ - build/ba_data/textures/fontExtras.pvr \ - build/ba_data/textures/fontExtras2.pvr \ - build/ba_data/textures/fontExtras3.pvr \ - build/ba_data/textures/fontExtras4.pvr \ - build/ba_data/textures/fontSmall0.pvr \ - build/ba_data/textures/fontSmall1.pvr \ - build/ba_data/textures/fontSmall2.pvr \ - build/ba_data/textures/fontSmall3.pvr \ - build/ba_data/textures/fontSmall4.pvr \ - build/ba_data/textures/fontSmall5.pvr \ - build/ba_data/textures/fontSmall6.pvr \ - build/ba_data/textures/fontSmall7.pvr \ - build/ba_data/textures/footballStadium.pvr \ - build/ba_data/textures/footballStadiumPreview.pvr \ - build/ba_data/textures/frameInset.pvr \ - build/ba_data/textures/frostyColor.pvr \ - build/ba_data/textures/frostyColorMask.pvr \ - build/ba_data/textures/frostyIcon.pvr \ - build/ba_data/textures/frostyIconColorMask.pvr \ - build/ba_data/textures/fuse.pvr \ - build/ba_data/textures/gameCenterIcon.pvr \ - build/ba_data/textures/gameCircleIcon.pvr \ - build/ba_data/textures/githubLogo.pvr \ - build/ba_data/textures/gladiatorColor.pvr \ - build/ba_data/textures/gladiatorColorMask.pvr \ - build/ba_data/textures/gladiatorIcon.pvr \ - build/ba_data/textures/gladiatorIconColorMask.pvr \ - build/ba_data/textures/glow.pvr \ - build/ba_data/textures/googlePlayAchievementsIcon.pvr \ - build/ba_data/textures/googlePlayGamesIcon.pvr \ - build/ba_data/textures/googlePlayLeaderboardsIcon.pvr \ - build/ba_data/textures/googlePlusIcon.pvr \ - build/ba_data/textures/googlePlusSignInButton.pvr \ - build/ba_data/textures/graphicsIcon.pvr \ - build/ba_data/textures/heart.pvr \ - build/ba_data/textures/hockeyStadium.pvr \ - build/ba_data/textures/hockeyStadiumPreview.pvr \ - build/ba_data/textures/iconOnslaught.pvr \ - build/ba_data/textures/iconRunaround.pvr \ - build/ba_data/textures/iircadeLogo.pvr \ - build/ba_data/textures/impactBombColor.pvr \ - build/ba_data/textures/impactBombColorLit.pvr \ - build/ba_data/textures/inventoryIcon.pvr \ - build/ba_data/textures/jackColor.pvr \ - build/ba_data/textures/jackColorMask.pvr \ - build/ba_data/textures/jackIcon.pvr \ - build/ba_data/textures/jackIconColorMask.pvr \ - build/ba_data/textures/jumpsuitColor.pvr \ - build/ba_data/textures/jumpsuitColorMask.pvr \ - build/ba_data/textures/jumpsuitIcon.pvr \ - build/ba_data/textures/jumpsuitIconColorMask.pvr \ - build/ba_data/textures/kronk.pvr \ - build/ba_data/textures/kronkColorMask.pvr \ - build/ba_data/textures/kronkIcon.pvr \ - build/ba_data/textures/kronkIconColorMask.pvr \ - build/ba_data/textures/lakeFrigid.pvr \ - build/ba_data/textures/lakeFrigidPreview.pvr \ - build/ba_data/textures/lakeFrigidReflections.pvr \ - build/ba_data/textures/landMine.pvr \ - build/ba_data/textures/landMineLit.pvr \ - build/ba_data/textures/leaderboardsIcon.pvr \ - build/ba_data/textures/leftButton.pvr \ - build/ba_data/textures/levelIcon.pvr \ - build/ba_data/textures/light.pvr \ - build/ba_data/textures/lightSharp.pvr \ - build/ba_data/textures/lightSoft.pvr \ - build/ba_data/textures/lock.pvr \ - build/ba_data/textures/logIcon.pvr \ - build/ba_data/textures/logo.pvr \ - build/ba_data/textures/logoEaster.pvr \ - build/ba_data/textures/mapPreviewMask.pvr \ - build/ba_data/textures/medalBronze.pvr \ - build/ba_data/textures/medalComplete.pvr \ - build/ba_data/textures/medalGold.pvr \ - build/ba_data/textures/medalSilver.pvr \ - build/ba_data/textures/melColor.pvr \ - build/ba_data/textures/melColorMask.pvr \ - build/ba_data/textures/melIcon.pvr \ - build/ba_data/textures/melIconColorMask.pvr \ - build/ba_data/textures/menuBG.pvr \ - build/ba_data/textures/menuButton.pvr \ - build/ba_data/textures/menuIcon.pvr \ - build/ba_data/textures/merch.pvr \ - build/ba_data/textures/meter.pvr \ - build/ba_data/textures/monkeyFaceLevelColor.pvr \ - build/ba_data/textures/monkeyFacePreview.pvr \ - build/ba_data/textures/multiplayerExamples.pvr \ - build/ba_data/textures/natureBackgroundColor.pvr \ - build/ba_data/textures/neoSpazColor.pvr \ - build/ba_data/textures/neoSpazColorMask.pvr \ - build/ba_data/textures/neoSpazIcon.pvr \ - build/ba_data/textures/neoSpazIconColorMask.pvr \ - build/ba_data/textures/nextLevelIcon.pvr \ - build/ba_data/textures/ninjaColor.pvr \ - build/ba_data/textures/ninjaColorMask.pvr \ - build/ba_data/textures/ninjaIcon.pvr \ - build/ba_data/textures/ninjaIconColorMask.pvr \ - build/ba_data/textures/nub.pvr \ - build/ba_data/textures/null.pvr \ - build/ba_data/textures/oldLadyColor.pvr \ - build/ba_data/textures/oldLadyColorMask.pvr \ - build/ba_data/textures/oldLadyIcon.pvr \ - build/ba_data/textures/oldLadyIconColorMask.pvr \ - build/ba_data/textures/operaSingerColor.pvr \ - build/ba_data/textures/operaSingerColorMask.pvr \ - build/ba_data/textures/operaSingerIcon.pvr \ - build/ba_data/textures/operaSingerIconColorMask.pvr \ - build/ba_data/textures/ouyaAButton.pvr \ - build/ba_data/textures/ouyaIcon.pvr \ - build/ba_data/textures/ouyaOButton.pvr \ - build/ba_data/textures/ouyaUButton.pvr \ - build/ba_data/textures/ouyaYButton.pvr \ - build/ba_data/textures/penguinColor.pvr \ - build/ba_data/textures/penguinColorMask.pvr \ - build/ba_data/textures/penguinIcon.pvr \ - build/ba_data/textures/penguinIconColorMask.pvr \ - build/ba_data/textures/pixieColor.pvr \ - build/ba_data/textures/pixieColorMask.pvr \ - build/ba_data/textures/pixieIcon.pvr \ - build/ba_data/textures/pixieIconColorMask.pvr \ - build/ba_data/textures/playerLineup.pvr \ - build/ba_data/textures/powerupBomb.pvr \ - build/ba_data/textures/powerupCurse.pvr \ - build/ba_data/textures/powerupHealth.pvr \ - build/ba_data/textures/powerupIceBombs.pvr \ - build/ba_data/textures/powerupImpactBombs.pvr \ - build/ba_data/textures/powerupLandMines.pvr \ - build/ba_data/textures/powerupPunch.pvr \ - build/ba_data/textures/powerupShield.pvr \ - build/ba_data/textures/powerupSpeed.pvr \ - build/ba_data/textures/powerupStickyBombs.pvr \ - build/ba_data/textures/puckColor.pvr \ - build/ba_data/textures/rampageBGColor.pvr \ - build/ba_data/textures/rampageBGColor2.pvr \ - build/ba_data/textures/rampageLevelColor.pvr \ - build/ba_data/textures/rampagePreview.pvr \ - build/ba_data/textures/reflectionChar_+x.pvr \ - build/ba_data/textures/reflectionChar_+y.pvr \ - build/ba_data/textures/reflectionChar_+z.pvr \ - build/ba_data/textures/reflectionChar_-x.pvr \ - build/ba_data/textures/reflectionChar_-y.pvr \ - build/ba_data/textures/reflectionChar_-z.pvr \ - build/ba_data/textures/reflectionPowerup_+x.pvr \ - build/ba_data/textures/reflectionPowerup_+y.pvr \ - build/ba_data/textures/reflectionPowerup_+z.pvr \ - build/ba_data/textures/reflectionPowerup_-x.pvr \ - build/ba_data/textures/reflectionPowerup_-y.pvr \ - build/ba_data/textures/reflectionPowerup_-z.pvr \ - build/ba_data/textures/reflectionSharp_+x.pvr \ - build/ba_data/textures/reflectionSharp_+y.pvr \ - build/ba_data/textures/reflectionSharp_+z.pvr \ - build/ba_data/textures/reflectionSharp_-x.pvr \ - build/ba_data/textures/reflectionSharp_-y.pvr \ - build/ba_data/textures/reflectionSharp_-z.pvr \ - build/ba_data/textures/reflectionSharper_+x.pvr \ - build/ba_data/textures/reflectionSharper_+y.pvr \ - build/ba_data/textures/reflectionSharper_+z.pvr \ - build/ba_data/textures/reflectionSharper_-x.pvr \ - build/ba_data/textures/reflectionSharper_-y.pvr \ - build/ba_data/textures/reflectionSharper_-z.pvr \ - build/ba_data/textures/reflectionSharpest_+x.pvr \ - build/ba_data/textures/reflectionSharpest_+y.pvr \ - build/ba_data/textures/reflectionSharpest_+z.pvr \ - build/ba_data/textures/reflectionSharpest_-x.pvr \ - build/ba_data/textures/reflectionSharpest_-y.pvr \ - build/ba_data/textures/reflectionSharpest_-z.pvr \ - build/ba_data/textures/reflectionSoft_+x.pvr \ - build/ba_data/textures/reflectionSoft_+y.pvr \ - build/ba_data/textures/reflectionSoft_+z.pvr \ - build/ba_data/textures/reflectionSoft_-x.pvr \ - build/ba_data/textures/reflectionSoft_-y.pvr \ - build/ba_data/textures/reflectionSoft_-z.pvr \ - build/ba_data/textures/replayIcon.pvr \ - build/ba_data/textures/rgbStripes.pvr \ - build/ba_data/textures/rightButton.pvr \ - build/ba_data/textures/robotColor.pvr \ - build/ba_data/textures/robotColorMask.pvr \ - build/ba_data/textures/robotIcon.pvr \ - build/ba_data/textures/robotIconColorMask.pvr \ - build/ba_data/textures/roundaboutLevelColor.pvr \ - build/ba_data/textures/roundaboutPreview.pvr \ - build/ba_data/textures/santaColor.pvr \ - build/ba_data/textures/santaColorMask.pvr \ - build/ba_data/textures/santaIcon.pvr \ - build/ba_data/textures/santaIconColorMask.pvr \ - build/ba_data/textures/scorch.pvr \ - build/ba_data/textures/scorchBig.pvr \ - build/ba_data/textures/scrollWidget.pvr \ - build/ba_data/textures/scrollWidgetGlow.pvr \ - build/ba_data/textures/settingsIcon.pvr \ - build/ba_data/textures/shadow.pvr \ - build/ba_data/textures/shadowSharp.pvr \ - build/ba_data/textures/shadowSoft.pvr \ - build/ba_data/textures/shield.pvr \ - build/ba_data/textures/shrapnel1Color.pvr \ - build/ba_data/textures/slash.pvr \ - build/ba_data/textures/smoke.pvr \ - build/ba_data/textures/softRect.pvr \ - build/ba_data/textures/softRect2.pvr \ - build/ba_data/textures/softRectVertical.pvr \ - build/ba_data/textures/sparks.pvr \ - build/ba_data/textures/star.pvr \ - build/ba_data/textures/startButton.pvr \ - build/ba_data/textures/stepRightUpLevelColor.pvr \ - build/ba_data/textures/stepRightUpPreview.pvr \ - build/ba_data/textures/storeCharacter.pvr \ - build/ba_data/textures/storeCharacterEaster.pvr \ - build/ba_data/textures/storeCharacterXmas.pvr \ - build/ba_data/textures/storeIcon.pvr \ - build/ba_data/textures/superheroColor.pvr \ - build/ba_data/textures/superheroColorMask.pvr \ - build/ba_data/textures/superheroIcon.pvr \ - build/ba_data/textures/superheroIconColorMask.pvr \ - build/ba_data/textures/textClearButton.pvr \ - build/ba_data/textures/thePadLevelColor.pvr \ - build/ba_data/textures/thePadPreview.pvr \ - build/ba_data/textures/ticketRoll.pvr \ - build/ba_data/textures/ticketRollBig.pvr \ - build/ba_data/textures/ticketRolls.pvr \ - build/ba_data/textures/tickets.pvr \ - build/ba_data/textures/ticketsMore.pvr \ - build/ba_data/textures/tipTopBGColor.pvr \ - build/ba_data/textures/tipTopLevelColor.pvr \ - build/ba_data/textures/tipTopPreview.pvr \ - build/ba_data/textures/tnt.pvr \ - build/ba_data/textures/touchArrows.pvr \ - build/ba_data/textures/touchArrowsActions.pvr \ - build/ba_data/textures/towerDLevelColor.pvr \ - build/ba_data/textures/towerDPreview.pvr \ - build/ba_data/textures/treesColor.pvr \ - build/ba_data/textures/trophy.pvr \ - build/ba_data/textures/tv.pvr \ - build/ba_data/textures/uiAtlas.pvr \ - build/ba_data/textures/uiAtlas2.pvr \ - build/ba_data/textures/upButton.pvr \ - build/ba_data/textures/usersButton.pvr \ - build/ba_data/textures/vrFillMound.pvr \ - build/ba_data/textures/warriorColor.pvr \ - build/ba_data/textures/warriorColorMask.pvr \ - build/ba_data/textures/warriorIcon.pvr \ - build/ba_data/textures/warriorIconColorMask.pvr \ - build/ba_data/textures/white.pvr \ - build/ba_data/textures/windowHSmallVMed.pvr \ - build/ba_data/textures/windowHSmallVSmall.pvr \ - build/ba_data/textures/wings.pvr \ - build/ba_data/textures/witchColor.pvr \ - build/ba_data/textures/witchColorMask.pvr \ - build/ba_data/textures/witchIcon.pvr \ - build/ba_data/textures/witchIconColorMask.pvr \ - build/ba_data/textures/wizardColor.pvr \ - build/ba_data/textures/wizardColorMask.pvr \ - build/ba_data/textures/wizardIcon.pvr \ - build/ba_data/textures/wizardIconColorMask.pvr \ - build/ba_data/textures/wrestlerColor.pvr \ - build/ba_data/textures/wrestlerColorMask.pvr \ - build/ba_data/textures/wrestlerIcon.pvr \ - build/ba_data/textures/wrestlerIconColorMask.pvr \ - build/ba_data/textures/zigZagLevelColor.pvr \ - build/ba_data/textures/zigzagPreview.pvr \ - build/ba_data/textures/zoeColor.pvr \ - build/ba_data/textures/zoeColorMask.pvr \ - build/ba_data/textures/zoeIcon.pvr \ - build/ba_data/textures/zoeIconColorMask.pvr - -TEX2D_KTX_TARGETS = \ - build/ba_data/textures/achievementBoxer.ktx \ - build/ba_data/textures/achievementCrossHair.ktx \ - build/ba_data/textures/achievementDualWielding.ktx \ - build/ba_data/textures/achievementEmpty.ktx \ - build/ba_data/textures/achievementFlawlessVictory.ktx \ - build/ba_data/textures/achievementFootballShutout.ktx \ - build/ba_data/textures/achievementFootballVictory.ktx \ - build/ba_data/textures/achievementFreeLoader.ktx \ - build/ba_data/textures/achievementGotTheMoves.ktx \ - build/ba_data/textures/achievementInControl.ktx \ - build/ba_data/textures/achievementMedalLarge.ktx \ - build/ba_data/textures/achievementMedalMedium.ktx \ - build/ba_data/textures/achievementMedalSmall.ktx \ - build/ba_data/textures/achievementMine.ktx \ - build/ba_data/textures/achievementOffYouGo.ktx \ - build/ba_data/textures/achievementOnslaught.ktx \ - build/ba_data/textures/achievementOutline.ktx \ - build/ba_data/textures/achievementRunaround.ktx \ - build/ba_data/textures/achievementSharingIsCaring.ktx \ - build/ba_data/textures/achievementStayinAlive.ktx \ - build/ba_data/textures/achievementSuperPunch.ktx \ - build/ba_data/textures/achievementTNT.ktx \ - build/ba_data/textures/achievementTeamPlayer.ktx \ - build/ba_data/textures/achievementWall.ktx \ - build/ba_data/textures/achievementsIcon.ktx \ - build/ba_data/textures/actionButtons.ktx \ - build/ba_data/textures/actionHeroColor.ktx \ - build/ba_data/textures/actionHeroColorMask.ktx \ - build/ba_data/textures/actionHeroIcon.ktx \ - build/ba_data/textures/actionHeroIconColorMask.ktx \ - build/ba_data/textures/advancedIcon.ktx \ - build/ba_data/textures/agentColor.ktx \ - build/ba_data/textures/agentColorMask.ktx \ - build/ba_data/textures/agentIcon.ktx \ - build/ba_data/textures/agentIconColorMask.ktx \ - build/ba_data/textures/aliBSRemoteIOSQR.ktx \ - build/ba_data/textures/aliColor.ktx \ - build/ba_data/textures/aliColorMask.ktx \ - build/ba_data/textures/aliControllerQR.ktx \ - build/ba_data/textures/aliIcon.ktx \ - build/ba_data/textures/aliIconColorMask.ktx \ - build/ba_data/textures/aliSplash.ktx \ - build/ba_data/textures/alienColor.ktx \ - build/ba_data/textures/alienColorMask.ktx \ - build/ba_data/textures/alienIcon.ktx \ - build/ba_data/textures/alienIconColorMask.ktx \ - build/ba_data/textures/alwaysLandBGColor.ktx \ - build/ba_data/textures/alwaysLandLevelColor.ktx \ - build/ba_data/textures/alwaysLandPreview.ktx \ - build/ba_data/textures/analogStick.ktx \ - build/ba_data/textures/arrow.ktx \ - build/ba_data/textures/assassinColor.ktx \ - build/ba_data/textures/assassinColorMask.ktx \ - build/ba_data/textures/assassinIcon.ktx \ - build/ba_data/textures/assassinIconColorMask.ktx \ - build/ba_data/textures/audioIcon.ktx \ - build/ba_data/textures/backIcon.ktx \ - build/ba_data/textures/bar.ktx \ - build/ba_data/textures/bearColor.ktx \ - build/ba_data/textures/bearColorMask.ktx \ - build/ba_data/textures/bearIcon.ktx \ - build/ba_data/textures/bearIconColorMask.ktx \ - build/ba_data/textures/bg.ktx \ - build/ba_data/textures/bigG.ktx \ - build/ba_data/textures/bigGPreview.ktx \ - build/ba_data/textures/black.ktx \ - build/ba_data/textures/bombButton.ktx \ - build/ba_data/textures/bombColor.ktx \ - build/ba_data/textures/bombColorIce.ktx \ - build/ba_data/textures/bombStickyColor.ktx \ - build/ba_data/textures/bonesColor.ktx \ - build/ba_data/textures/bonesColorMask.ktx \ - build/ba_data/textures/bonesIcon.ktx \ - build/ba_data/textures/bonesIconColorMask.ktx \ - build/ba_data/textures/boxingGlovesColor.ktx \ - build/ba_data/textures/bridgitLevelColor.ktx \ - build/ba_data/textures/bridgitPreview.ktx \ - build/ba_data/textures/bunnyColor.ktx \ - build/ba_data/textures/bunnyColorMask.ktx \ - build/ba_data/textures/bunnyIcon.ktx \ - build/ba_data/textures/bunnyIconColorMask.ktx \ - build/ba_data/textures/buttonBomb.ktx \ - build/ba_data/textures/buttonJump.ktx \ - build/ba_data/textures/buttonPickUp.ktx \ - build/ba_data/textures/buttonPunch.ktx \ - build/ba_data/textures/buttonSquare.ktx \ - build/ba_data/textures/chTitleChar1.ktx \ - build/ba_data/textures/chTitleChar2.ktx \ - build/ba_data/textures/chTitleChar3.ktx \ - build/ba_data/textures/chTitleChar4.ktx \ - build/ba_data/textures/chTitleChar5.ktx \ - build/ba_data/textures/characterIconMask.ktx \ - build/ba_data/textures/chestIcon.ktx \ - build/ba_data/textures/chestIconEmpty.ktx \ - build/ba_data/textures/chestIconMulti.ktx \ - build/ba_data/textures/chestOpenIcon.ktx \ - build/ba_data/textures/circle.ktx \ - build/ba_data/textures/circleNoAlpha.ktx \ - build/ba_data/textures/circleOutline.ktx \ - build/ba_data/textures/circleOutlineNoAlpha.ktx \ - build/ba_data/textures/circleShadow.ktx \ - build/ba_data/textures/circleZigZag.ktx \ - build/ba_data/textures/coin.ktx \ - build/ba_data/textures/controllerIcon.ktx \ - build/ba_data/textures/courtyardLevelColor.ktx \ - build/ba_data/textures/courtyardPreview.ktx \ - build/ba_data/textures/cowboyColor.ktx \ - build/ba_data/textures/cowboyColorMask.ktx \ - build/ba_data/textures/cowboyIcon.ktx \ - build/ba_data/textures/cowboyIconColorMask.ktx \ - build/ba_data/textures/cragCastleLevelColor.ktx \ - build/ba_data/textures/cragCastlePreview.ktx \ - build/ba_data/textures/crossOut.ktx \ - build/ba_data/textures/crossOutMask.ktx \ - build/ba_data/textures/cursor.ktx \ - build/ba_data/textures/cuteSpaz.ktx \ - build/ba_data/textures/cyborgColor.ktx \ - build/ba_data/textures/cyborgColorMask.ktx \ - build/ba_data/textures/cyborgIcon.ktx \ - build/ba_data/textures/cyborgIconColorMask.ktx \ - build/ba_data/textures/discordLogo.ktx \ - build/ba_data/textures/doomShroomBGColor.ktx \ - build/ba_data/textures/doomShroomLevelColor.ktx \ - build/ba_data/textures/doomShroomPreview.ktx \ - build/ba_data/textures/downButton.ktx \ - build/ba_data/textures/egg1.ktx \ - build/ba_data/textures/egg2.ktx \ - build/ba_data/textures/egg3.ktx \ - build/ba_data/textures/egg4.ktx \ - build/ba_data/textures/eggTex1.ktx \ - build/ba_data/textures/eggTex2.ktx \ - build/ba_data/textures/eggTex3.ktx \ - build/ba_data/textures/empty.ktx \ - build/ba_data/textures/explosion.ktx \ - build/ba_data/textures/eyeColor.ktx \ - build/ba_data/textures/eyeColorTintMask.ktx \ - build/ba_data/textures/file.ktx \ - build/ba_data/textures/flagColor.ktx \ - build/ba_data/textures/flagPoleColor.ktx \ - build/ba_data/textures/folder.ktx \ - build/ba_data/textures/fontBig.ktx \ - build/ba_data/textures/fontExtras.ktx \ - build/ba_data/textures/fontExtras2.ktx \ - build/ba_data/textures/fontExtras3.ktx \ - build/ba_data/textures/fontExtras4.ktx \ - build/ba_data/textures/fontSmall0.ktx \ - build/ba_data/textures/fontSmall1.ktx \ - build/ba_data/textures/fontSmall2.ktx \ - build/ba_data/textures/fontSmall3.ktx \ - build/ba_data/textures/fontSmall4.ktx \ - build/ba_data/textures/fontSmall5.ktx \ - build/ba_data/textures/fontSmall6.ktx \ - build/ba_data/textures/fontSmall7.ktx \ - build/ba_data/textures/footballStadium.ktx \ - build/ba_data/textures/footballStadiumPreview.ktx \ - build/ba_data/textures/frameInset.ktx \ - build/ba_data/textures/frostyColor.ktx \ - build/ba_data/textures/frostyColorMask.ktx \ - build/ba_data/textures/frostyIcon.ktx \ - build/ba_data/textures/frostyIconColorMask.ktx \ - build/ba_data/textures/fuse.ktx \ - build/ba_data/textures/gameCenterIcon.ktx \ - build/ba_data/textures/gameCircleIcon.ktx \ - build/ba_data/textures/githubLogo.ktx \ - build/ba_data/textures/gladiatorColor.ktx \ - build/ba_data/textures/gladiatorColorMask.ktx \ - build/ba_data/textures/gladiatorIcon.ktx \ - build/ba_data/textures/gladiatorIconColorMask.ktx \ - build/ba_data/textures/glow.ktx \ - build/ba_data/textures/googlePlayAchievementsIcon.ktx \ - build/ba_data/textures/googlePlayGamesIcon.ktx \ - build/ba_data/textures/googlePlayLeaderboardsIcon.ktx \ - build/ba_data/textures/googlePlusIcon.ktx \ - build/ba_data/textures/googlePlusSignInButton.ktx \ - build/ba_data/textures/graphicsIcon.ktx \ - build/ba_data/textures/heart.ktx \ - build/ba_data/textures/hockeyStadium.ktx \ - build/ba_data/textures/hockeyStadiumPreview.ktx \ - build/ba_data/textures/iconOnslaught.ktx \ - build/ba_data/textures/iconRunaround.ktx \ - build/ba_data/textures/iircadeLogo.ktx \ - build/ba_data/textures/impactBombColor.ktx \ - build/ba_data/textures/impactBombColorLit.ktx \ - build/ba_data/textures/inventoryIcon.ktx \ - build/ba_data/textures/jackColor.ktx \ - build/ba_data/textures/jackColorMask.ktx \ - build/ba_data/textures/jackIcon.ktx \ - build/ba_data/textures/jackIconColorMask.ktx \ - build/ba_data/textures/jumpsuitColor.ktx \ - build/ba_data/textures/jumpsuitColorMask.ktx \ - build/ba_data/textures/jumpsuitIcon.ktx \ - build/ba_data/textures/jumpsuitIconColorMask.ktx \ - build/ba_data/textures/kronk.ktx \ - build/ba_data/textures/kronkColorMask.ktx \ - build/ba_data/textures/kronkIcon.ktx \ - build/ba_data/textures/kronkIconColorMask.ktx \ - build/ba_data/textures/lakeFrigid.ktx \ - build/ba_data/textures/lakeFrigidPreview.ktx \ - build/ba_data/textures/lakeFrigidReflections.ktx \ - build/ba_data/textures/landMine.ktx \ - build/ba_data/textures/landMineLit.ktx \ - build/ba_data/textures/leaderboardsIcon.ktx \ - build/ba_data/textures/leftButton.ktx \ - build/ba_data/textures/levelIcon.ktx \ - build/ba_data/textures/light.ktx \ - build/ba_data/textures/lightSharp.ktx \ - build/ba_data/textures/lightSoft.ktx \ - build/ba_data/textures/lock.ktx \ - build/ba_data/textures/logIcon.ktx \ - build/ba_data/textures/logo.ktx \ - build/ba_data/textures/logoEaster.ktx \ - build/ba_data/textures/mapPreviewMask.ktx \ - build/ba_data/textures/medalBronze.ktx \ - build/ba_data/textures/medalComplete.ktx \ - build/ba_data/textures/medalGold.ktx \ - build/ba_data/textures/medalSilver.ktx \ - build/ba_data/textures/melColor.ktx \ - build/ba_data/textures/melColorMask.ktx \ - build/ba_data/textures/melIcon.ktx \ - build/ba_data/textures/melIconColorMask.ktx \ - build/ba_data/textures/menuBG.ktx \ - build/ba_data/textures/menuButton.ktx \ - build/ba_data/textures/menuIcon.ktx \ - build/ba_data/textures/merch.ktx \ - build/ba_data/textures/meter.ktx \ - build/ba_data/textures/monkeyFaceLevelColor.ktx \ - build/ba_data/textures/monkeyFacePreview.ktx \ - build/ba_data/textures/multiplayerExamples.ktx \ - build/ba_data/textures/natureBackgroundColor.ktx \ - build/ba_data/textures/neoSpazColor.ktx \ - build/ba_data/textures/neoSpazColorMask.ktx \ - build/ba_data/textures/neoSpazIcon.ktx \ - build/ba_data/textures/neoSpazIconColorMask.ktx \ - build/ba_data/textures/nextLevelIcon.ktx \ - build/ba_data/textures/ninjaColor.ktx \ - build/ba_data/textures/ninjaColorMask.ktx \ - build/ba_data/textures/ninjaIcon.ktx \ - build/ba_data/textures/ninjaIconColorMask.ktx \ - build/ba_data/textures/nub.ktx \ - build/ba_data/textures/null.ktx \ - build/ba_data/textures/oldLadyColor.ktx \ - build/ba_data/textures/oldLadyColorMask.ktx \ - build/ba_data/textures/oldLadyIcon.ktx \ - build/ba_data/textures/oldLadyIconColorMask.ktx \ - build/ba_data/textures/operaSingerColor.ktx \ - build/ba_data/textures/operaSingerColorMask.ktx \ - build/ba_data/textures/operaSingerIcon.ktx \ - build/ba_data/textures/operaSingerIconColorMask.ktx \ - build/ba_data/textures/ouyaAButton.ktx \ - build/ba_data/textures/ouyaIcon.ktx \ - build/ba_data/textures/ouyaOButton.ktx \ - build/ba_data/textures/ouyaUButton.ktx \ - build/ba_data/textures/ouyaYButton.ktx \ - build/ba_data/textures/penguinColor.ktx \ - build/ba_data/textures/penguinColorMask.ktx \ - build/ba_data/textures/penguinIcon.ktx \ - build/ba_data/textures/penguinIconColorMask.ktx \ - build/ba_data/textures/pixieColor.ktx \ - build/ba_data/textures/pixieColorMask.ktx \ - build/ba_data/textures/pixieIcon.ktx \ - build/ba_data/textures/pixieIconColorMask.ktx \ - build/ba_data/textures/playerLineup.ktx \ - build/ba_data/textures/powerupBomb.ktx \ - build/ba_data/textures/powerupCurse.ktx \ - build/ba_data/textures/powerupHealth.ktx \ - build/ba_data/textures/powerupIceBombs.ktx \ - build/ba_data/textures/powerupImpactBombs.ktx \ - build/ba_data/textures/powerupLandMines.ktx \ - build/ba_data/textures/powerupPunch.ktx \ - build/ba_data/textures/powerupShield.ktx \ - build/ba_data/textures/powerupSpeed.ktx \ - build/ba_data/textures/powerupStickyBombs.ktx \ - build/ba_data/textures/puckColor.ktx \ - build/ba_data/textures/rampageBGColor.ktx \ - build/ba_data/textures/rampageBGColor2.ktx \ - build/ba_data/textures/rampageLevelColor.ktx \ - build/ba_data/textures/rampagePreview.ktx \ - build/ba_data/textures/reflectionChar_+x.ktx \ - build/ba_data/textures/reflectionChar_+y.ktx \ - build/ba_data/textures/reflectionChar_+z.ktx \ - build/ba_data/textures/reflectionChar_-x.ktx \ - build/ba_data/textures/reflectionChar_-y.ktx \ - build/ba_data/textures/reflectionChar_-z.ktx \ - build/ba_data/textures/reflectionPowerup_+x.ktx \ - build/ba_data/textures/reflectionPowerup_+y.ktx \ - build/ba_data/textures/reflectionPowerup_+z.ktx \ - build/ba_data/textures/reflectionPowerup_-x.ktx \ - build/ba_data/textures/reflectionPowerup_-y.ktx \ - build/ba_data/textures/reflectionPowerup_-z.ktx \ - build/ba_data/textures/reflectionSharp_+x.ktx \ - build/ba_data/textures/reflectionSharp_+y.ktx \ - build/ba_data/textures/reflectionSharp_+z.ktx \ - build/ba_data/textures/reflectionSharp_-x.ktx \ - build/ba_data/textures/reflectionSharp_-y.ktx \ - build/ba_data/textures/reflectionSharp_-z.ktx \ - build/ba_data/textures/reflectionSharper_+x.ktx \ - build/ba_data/textures/reflectionSharper_+y.ktx \ - build/ba_data/textures/reflectionSharper_+z.ktx \ - build/ba_data/textures/reflectionSharper_-x.ktx \ - build/ba_data/textures/reflectionSharper_-y.ktx \ - build/ba_data/textures/reflectionSharper_-z.ktx \ - build/ba_data/textures/reflectionSharpest_+x.ktx \ - build/ba_data/textures/reflectionSharpest_+y.ktx \ - build/ba_data/textures/reflectionSharpest_+z.ktx \ - build/ba_data/textures/reflectionSharpest_-x.ktx \ - build/ba_data/textures/reflectionSharpest_-y.ktx \ - build/ba_data/textures/reflectionSharpest_-z.ktx \ - build/ba_data/textures/reflectionSoft_+x.ktx \ - build/ba_data/textures/reflectionSoft_+y.ktx \ - build/ba_data/textures/reflectionSoft_+z.ktx \ - build/ba_data/textures/reflectionSoft_-x.ktx \ - build/ba_data/textures/reflectionSoft_-y.ktx \ - build/ba_data/textures/reflectionSoft_-z.ktx \ - build/ba_data/textures/replayIcon.ktx \ - build/ba_data/textures/rgbStripes.ktx \ - build/ba_data/textures/rightButton.ktx \ - build/ba_data/textures/robotColor.ktx \ - build/ba_data/textures/robotColorMask.ktx \ - build/ba_data/textures/robotIcon.ktx \ - build/ba_data/textures/robotIconColorMask.ktx \ - build/ba_data/textures/roundaboutLevelColor.ktx \ - build/ba_data/textures/roundaboutPreview.ktx \ - build/ba_data/textures/santaColor.ktx \ - build/ba_data/textures/santaColorMask.ktx \ - build/ba_data/textures/santaIcon.ktx \ - build/ba_data/textures/santaIconColorMask.ktx \ - build/ba_data/textures/scorch.ktx \ - build/ba_data/textures/scorchBig.ktx \ - build/ba_data/textures/scrollWidget.ktx \ - build/ba_data/textures/scrollWidgetGlow.ktx \ - build/ba_data/textures/settingsIcon.ktx \ - build/ba_data/textures/shadow.ktx \ - build/ba_data/textures/shadowSharp.ktx \ - build/ba_data/textures/shadowSoft.ktx \ - build/ba_data/textures/shield.ktx \ - build/ba_data/textures/shrapnel1Color.ktx \ - build/ba_data/textures/slash.ktx \ - build/ba_data/textures/smoke.ktx \ - build/ba_data/textures/softRect.ktx \ - build/ba_data/textures/softRect2.ktx \ - build/ba_data/textures/softRectVertical.ktx \ - build/ba_data/textures/sparks.ktx \ - build/ba_data/textures/star.ktx \ - build/ba_data/textures/startButton.ktx \ - build/ba_data/textures/stepRightUpLevelColor.ktx \ - build/ba_data/textures/stepRightUpPreview.ktx \ - build/ba_data/textures/storeCharacter.ktx \ - build/ba_data/textures/storeCharacterEaster.ktx \ - build/ba_data/textures/storeCharacterXmas.ktx \ - build/ba_data/textures/storeIcon.ktx \ - build/ba_data/textures/superheroColor.ktx \ - build/ba_data/textures/superheroColorMask.ktx \ - build/ba_data/textures/superheroIcon.ktx \ - build/ba_data/textures/superheroIconColorMask.ktx \ - build/ba_data/textures/textClearButton.ktx \ - build/ba_data/textures/thePadLevelColor.ktx \ - build/ba_data/textures/thePadPreview.ktx \ - build/ba_data/textures/ticketRoll.ktx \ - build/ba_data/textures/ticketRollBig.ktx \ - build/ba_data/textures/ticketRolls.ktx \ - build/ba_data/textures/tickets.ktx \ - build/ba_data/textures/ticketsMore.ktx \ - build/ba_data/textures/tipTopBGColor.ktx \ - build/ba_data/textures/tipTopLevelColor.ktx \ - build/ba_data/textures/tipTopPreview.ktx \ - build/ba_data/textures/tnt.ktx \ - build/ba_data/textures/touchArrows.ktx \ - build/ba_data/textures/touchArrowsActions.ktx \ - build/ba_data/textures/towerDLevelColor.ktx \ - build/ba_data/textures/towerDPreview.ktx \ - build/ba_data/textures/treesColor.ktx \ - build/ba_data/textures/trophy.ktx \ - build/ba_data/textures/tv.ktx \ - build/ba_data/textures/uiAtlas.ktx \ - build/ba_data/textures/uiAtlas2.ktx \ - build/ba_data/textures/upButton.ktx \ - build/ba_data/textures/usersButton.ktx \ - build/ba_data/textures/vrFillMound.ktx \ - build/ba_data/textures/warriorColor.ktx \ - build/ba_data/textures/warriorColorMask.ktx \ - build/ba_data/textures/warriorIcon.ktx \ - build/ba_data/textures/warriorIconColorMask.ktx \ - build/ba_data/textures/white.ktx \ - build/ba_data/textures/windowHSmallVMed.ktx \ - build/ba_data/textures/windowHSmallVSmall.ktx \ - build/ba_data/textures/wings.ktx \ - build/ba_data/textures/witchColor.ktx \ - build/ba_data/textures/witchColorMask.ktx \ - build/ba_data/textures/witchIcon.ktx \ - build/ba_data/textures/witchIconColorMask.ktx \ - build/ba_data/textures/wizardColor.ktx \ - build/ba_data/textures/wizardColorMask.ktx \ - build/ba_data/textures/wizardIcon.ktx \ - build/ba_data/textures/wizardIconColorMask.ktx \ - build/ba_data/textures/wrestlerColor.ktx \ - build/ba_data/textures/wrestlerColorMask.ktx \ - build/ba_data/textures/wrestlerIcon.ktx \ - build/ba_data/textures/wrestlerIconColorMask.ktx \ - build/ba_data/textures/zigZagLevelColor.ktx \ - build/ba_data/textures/zigzagPreview.ktx \ - build/ba_data/textures/zoeColor.ktx \ - build/ba_data/textures/zoeColorMask.ktx \ - build/ba_data/textures/zoeIcon.ktx \ - build/ba_data/textures/zoeIconColorMask.ktx - -TEX2D_PREVIEW_PNG_TARGETS = \ - build/ba_data/textures/achievementBoxer_preview.png \ - build/ba_data/textures/achievementCrossHair_preview.png \ - build/ba_data/textures/achievementDualWielding_preview.png \ - build/ba_data/textures/achievementEmpty_preview.png \ - build/ba_data/textures/achievementFlawlessVictory_preview.png \ - build/ba_data/textures/achievementFootballShutout_preview.png \ - build/ba_data/textures/achievementFootballVictory_preview.png \ - build/ba_data/textures/achievementFreeLoader_preview.png \ - build/ba_data/textures/achievementGotTheMoves_preview.png \ - build/ba_data/textures/achievementInControl_preview.png \ - build/ba_data/textures/achievementMedalLarge_preview.png \ - build/ba_data/textures/achievementMedalMedium_preview.png \ - build/ba_data/textures/achievementMedalSmall_preview.png \ - build/ba_data/textures/achievementMine_preview.png \ - build/ba_data/textures/achievementOffYouGo_preview.png \ - build/ba_data/textures/achievementOnslaught_preview.png \ - build/ba_data/textures/achievementOutline_preview.png \ - build/ba_data/textures/achievementRunaround_preview.png \ - build/ba_data/textures/achievementSharingIsCaring_preview.png \ - build/ba_data/textures/achievementStayinAlive_preview.png \ - build/ba_data/textures/achievementSuperPunch_preview.png \ - build/ba_data/textures/achievementTNT_preview.png \ - build/ba_data/textures/achievementTeamPlayer_preview.png \ - build/ba_data/textures/achievementWall_preview.png \ - build/ba_data/textures/achievementsIcon_preview.png \ - build/ba_data/textures/actionButtons_preview.png \ - build/ba_data/textures/actionHeroColorMask_preview.png \ - build/ba_data/textures/actionHeroColor_preview.png \ - build/ba_data/textures/actionHeroIconColorMask_preview.png \ - build/ba_data/textures/actionHeroIcon_preview.png \ - build/ba_data/textures/advancedIcon_preview.png \ - build/ba_data/textures/agentColorMask_preview.png \ - build/ba_data/textures/agentColor_preview.png \ - build/ba_data/textures/agentIconColorMask_preview.png \ - build/ba_data/textures/agentIcon_preview.png \ - build/ba_data/textures/aliBSRemoteIOSQR_preview.png \ - build/ba_data/textures/aliColorMask_preview.png \ - build/ba_data/textures/aliColor_preview.png \ - build/ba_data/textures/aliControllerQR_preview.png \ - build/ba_data/textures/aliIconColorMask_preview.png \ - build/ba_data/textures/aliIcon_preview.png \ - build/ba_data/textures/aliSplash_preview.png \ - build/ba_data/textures/alienColorMask_preview.png \ - build/ba_data/textures/alienColor_preview.png \ - build/ba_data/textures/alienIconColorMask_preview.png \ - build/ba_data/textures/alienIcon_preview.png \ - build/ba_data/textures/alwaysLandBGColor_preview.png \ - build/ba_data/textures/alwaysLandLevelColor_preview.png \ - build/ba_data/textures/alwaysLandPreview_preview.png \ - build/ba_data/textures/analogStick_preview.png \ - build/ba_data/textures/arrow_preview.png \ - build/ba_data/textures/assassinColorMask_preview.png \ - build/ba_data/textures/assassinColor_preview.png \ - build/ba_data/textures/assassinIconColorMask_preview.png \ - build/ba_data/textures/assassinIcon_preview.png \ - build/ba_data/textures/audioIcon_preview.png \ - build/ba_data/textures/backIcon_preview.png \ - build/ba_data/textures/bar_preview.png \ - build/ba_data/textures/bearColorMask_preview.png \ - build/ba_data/textures/bearColor_preview.png \ - build/ba_data/textures/bearIconColorMask_preview.png \ - build/ba_data/textures/bearIcon_preview.png \ - build/ba_data/textures/bg_preview.png \ - build/ba_data/textures/bigGPreview_preview.png \ - build/ba_data/textures/bigG_preview.png \ - build/ba_data/textures/black_preview.png \ - build/ba_data/textures/bombButton_preview.png \ - build/ba_data/textures/bombColorIce_preview.png \ - build/ba_data/textures/bombColor_preview.png \ - build/ba_data/textures/bombStickyColor_preview.png \ - build/ba_data/textures/bonesColorMask_preview.png \ - build/ba_data/textures/bonesColor_preview.png \ - build/ba_data/textures/bonesIconColorMask_preview.png \ - build/ba_data/textures/bonesIcon_preview.png \ - build/ba_data/textures/boxingGlovesColor_preview.png \ - build/ba_data/textures/bridgitLevelColor_preview.png \ - build/ba_data/textures/bridgitPreview_preview.png \ - build/ba_data/textures/bunnyColorMask_preview.png \ - build/ba_data/textures/bunnyColor_preview.png \ - build/ba_data/textures/bunnyIconColorMask_preview.png \ - build/ba_data/textures/bunnyIcon_preview.png \ - build/ba_data/textures/buttonBomb_preview.png \ - build/ba_data/textures/buttonJump_preview.png \ - build/ba_data/textures/buttonPickUp_preview.png \ - build/ba_data/textures/buttonPunch_preview.png \ - build/ba_data/textures/buttonSquare_preview.png \ - build/ba_data/textures/chTitleChar1_preview.png \ - build/ba_data/textures/chTitleChar2_preview.png \ - build/ba_data/textures/chTitleChar3_preview.png \ - build/ba_data/textures/chTitleChar4_preview.png \ - build/ba_data/textures/chTitleChar5_preview.png \ - build/ba_data/textures/characterIconMask_preview.png \ - build/ba_data/textures/chestIconEmpty_preview.png \ - build/ba_data/textures/chestIconMulti_preview.png \ - build/ba_data/textures/chestIcon_preview.png \ - build/ba_data/textures/chestOpenIcon_preview.png \ - build/ba_data/textures/circleNoAlpha_preview.png \ - build/ba_data/textures/circleOutlineNoAlpha_preview.png \ - build/ba_data/textures/circleOutline_preview.png \ - build/ba_data/textures/circleShadow_preview.png \ - build/ba_data/textures/circleZigZag_preview.png \ - build/ba_data/textures/circle_preview.png \ - build/ba_data/textures/coin_preview.png \ - build/ba_data/textures/controllerIcon_preview.png \ - build/ba_data/textures/courtyardLevelColor_preview.png \ - build/ba_data/textures/courtyardPreview_preview.png \ - build/ba_data/textures/cowboyColorMask_preview.png \ - build/ba_data/textures/cowboyColor_preview.png \ - build/ba_data/textures/cowboyIconColorMask_preview.png \ - build/ba_data/textures/cowboyIcon_preview.png \ - build/ba_data/textures/cragCastleLevelColor_preview.png \ - build/ba_data/textures/cragCastlePreview_preview.png \ - build/ba_data/textures/crossOutMask_preview.png \ - build/ba_data/textures/crossOut_preview.png \ - build/ba_data/textures/cursor_preview.png \ - build/ba_data/textures/cuteSpaz_preview.png \ - build/ba_data/textures/cyborgColorMask_preview.png \ - build/ba_data/textures/cyborgColor_preview.png \ - build/ba_data/textures/cyborgIconColorMask_preview.png \ - build/ba_data/textures/cyborgIcon_preview.png \ - build/ba_data/textures/discordLogo_preview.png \ - build/ba_data/textures/doomShroomBGColor_preview.png \ - build/ba_data/textures/doomShroomLevelColor_preview.png \ - build/ba_data/textures/doomShroomPreview_preview.png \ - build/ba_data/textures/downButton_preview.png \ - build/ba_data/textures/egg1_preview.png \ - build/ba_data/textures/egg2_preview.png \ - build/ba_data/textures/egg3_preview.png \ - build/ba_data/textures/egg4_preview.png \ - build/ba_data/textures/eggTex1_preview.png \ - build/ba_data/textures/eggTex2_preview.png \ - build/ba_data/textures/eggTex3_preview.png \ - build/ba_data/textures/empty_preview.png \ - build/ba_data/textures/explosion_preview.png \ - build/ba_data/textures/eyeColorTintMask_preview.png \ - build/ba_data/textures/eyeColor_preview.png \ - build/ba_data/textures/file_preview.png \ - build/ba_data/textures/flagColor_preview.png \ - build/ba_data/textures/flagPoleColor_preview.png \ - build/ba_data/textures/folder_preview.png \ - build/ba_data/textures/fontBig_preview.png \ - build/ba_data/textures/fontExtras2_preview.png \ - build/ba_data/textures/fontExtras3_preview.png \ - build/ba_data/textures/fontExtras4_preview.png \ - build/ba_data/textures/fontExtras_preview.png \ - build/ba_data/textures/fontSmall0_preview.png \ - build/ba_data/textures/fontSmall1_preview.png \ - build/ba_data/textures/fontSmall2_preview.png \ - build/ba_data/textures/fontSmall3_preview.png \ - build/ba_data/textures/fontSmall4_preview.png \ - build/ba_data/textures/fontSmall5_preview.png \ - build/ba_data/textures/fontSmall6_preview.png \ - build/ba_data/textures/fontSmall7_preview.png \ - build/ba_data/textures/footballStadiumPreview_preview.png \ - build/ba_data/textures/footballStadium_preview.png \ - build/ba_data/textures/frameInset_preview.png \ - build/ba_data/textures/frostyColorMask_preview.png \ - build/ba_data/textures/frostyColor_preview.png \ - build/ba_data/textures/frostyIconColorMask_preview.png \ - build/ba_data/textures/frostyIcon_preview.png \ - build/ba_data/textures/fuse_preview.png \ - build/ba_data/textures/gameCenterIcon_preview.png \ - build/ba_data/textures/gameCircleIcon_preview.png \ - build/ba_data/textures/githubLogo_preview.png \ - build/ba_data/textures/gladiatorColorMask_preview.png \ - build/ba_data/textures/gladiatorColor_preview.png \ - build/ba_data/textures/gladiatorIconColorMask_preview.png \ - build/ba_data/textures/gladiatorIcon_preview.png \ - build/ba_data/textures/glow_preview.png \ - build/ba_data/textures/googlePlayAchievementsIcon_preview.png \ - build/ba_data/textures/googlePlayGamesIcon_preview.png \ - build/ba_data/textures/googlePlayLeaderboardsIcon_preview.png \ - build/ba_data/textures/googlePlusIcon_preview.png \ - build/ba_data/textures/googlePlusSignInButton_preview.png \ - build/ba_data/textures/graphicsIcon_preview.png \ - build/ba_data/textures/heart_preview.png \ - build/ba_data/textures/hockeyStadiumPreview_preview.png \ - build/ba_data/textures/hockeyStadium_preview.png \ - build/ba_data/textures/iconOnslaught_preview.png \ - build/ba_data/textures/iconRunaround_preview.png \ - build/ba_data/textures/iircadeLogo_preview.png \ - build/ba_data/textures/impactBombColorLit_preview.png \ - build/ba_data/textures/impactBombColor_preview.png \ - build/ba_data/textures/inventoryIcon_preview.png \ - build/ba_data/textures/jackColorMask_preview.png \ - build/ba_data/textures/jackColor_preview.png \ - build/ba_data/textures/jackIconColorMask_preview.png \ - build/ba_data/textures/jackIcon_preview.png \ - build/ba_data/textures/jumpsuitColorMask_preview.png \ - build/ba_data/textures/jumpsuitColor_preview.png \ - build/ba_data/textures/jumpsuitIconColorMask_preview.png \ - build/ba_data/textures/jumpsuitIcon_preview.png \ - build/ba_data/textures/kronkColorMask_preview.png \ - build/ba_data/textures/kronkIconColorMask_preview.png \ - build/ba_data/textures/kronkIcon_preview.png \ - build/ba_data/textures/kronk_preview.png \ - build/ba_data/textures/lakeFrigidPreview_preview.png \ - build/ba_data/textures/lakeFrigidReflections_preview.png \ - build/ba_data/textures/lakeFrigid_preview.png \ - build/ba_data/textures/landMineLit_preview.png \ - build/ba_data/textures/landMine_preview.png \ - build/ba_data/textures/leaderboardsIcon_preview.png \ - build/ba_data/textures/leftButton_preview.png \ - build/ba_data/textures/levelIcon_preview.png \ - build/ba_data/textures/lightSharp_preview.png \ - build/ba_data/textures/lightSoft_preview.png \ - build/ba_data/textures/light_preview.png \ - build/ba_data/textures/lock_preview.png \ - build/ba_data/textures/logIcon_preview.png \ - build/ba_data/textures/logoEaster_preview.png \ - build/ba_data/textures/logo_preview.png \ - build/ba_data/textures/mapPreviewMask_preview.png \ - build/ba_data/textures/medalBronze_preview.png \ - build/ba_data/textures/medalComplete_preview.png \ - build/ba_data/textures/medalGold_preview.png \ - build/ba_data/textures/medalSilver_preview.png \ - build/ba_data/textures/melColorMask_preview.png \ - build/ba_data/textures/melColor_preview.png \ - build/ba_data/textures/melIconColorMask_preview.png \ - build/ba_data/textures/melIcon_preview.png \ - build/ba_data/textures/menuBG_preview.png \ - build/ba_data/textures/menuButton_preview.png \ - build/ba_data/textures/menuIcon_preview.png \ - build/ba_data/textures/merch_preview.png \ - build/ba_data/textures/meter_preview.png \ - build/ba_data/textures/monkeyFaceLevelColor_preview.png \ - build/ba_data/textures/monkeyFacePreview_preview.png \ - build/ba_data/textures/multiplayerExamples_preview.png \ - build/ba_data/textures/natureBackgroundColor_preview.png \ - build/ba_data/textures/neoSpazColorMask_preview.png \ - build/ba_data/textures/neoSpazColor_preview.png \ - build/ba_data/textures/neoSpazIconColorMask_preview.png \ - build/ba_data/textures/neoSpazIcon_preview.png \ - build/ba_data/textures/nextLevelIcon_preview.png \ - build/ba_data/textures/ninjaColorMask_preview.png \ - build/ba_data/textures/ninjaColor_preview.png \ - build/ba_data/textures/ninjaIconColorMask_preview.png \ - build/ba_data/textures/ninjaIcon_preview.png \ - build/ba_data/textures/nub_preview.png \ - build/ba_data/textures/null_preview.png \ - build/ba_data/textures/oldLadyColorMask_preview.png \ - build/ba_data/textures/oldLadyColor_preview.png \ - build/ba_data/textures/oldLadyIconColorMask_preview.png \ - build/ba_data/textures/oldLadyIcon_preview.png \ - build/ba_data/textures/operaSingerColorMask_preview.png \ - build/ba_data/textures/operaSingerColor_preview.png \ - build/ba_data/textures/operaSingerIconColorMask_preview.png \ - build/ba_data/textures/operaSingerIcon_preview.png \ - build/ba_data/textures/ouyaAButton_preview.png \ - build/ba_data/textures/ouyaIcon_preview.png \ - build/ba_data/textures/ouyaOButton_preview.png \ - build/ba_data/textures/ouyaUButton_preview.png \ - build/ba_data/textures/ouyaYButton_preview.png \ - build/ba_data/textures/penguinColorMask_preview.png \ - build/ba_data/textures/penguinColor_preview.png \ - build/ba_data/textures/penguinIconColorMask_preview.png \ - build/ba_data/textures/penguinIcon_preview.png \ - build/ba_data/textures/pixieColorMask_preview.png \ - build/ba_data/textures/pixieColor_preview.png \ - build/ba_data/textures/pixieIconColorMask_preview.png \ - build/ba_data/textures/pixieIcon_preview.png \ - build/ba_data/textures/playerLineup_preview.png \ - build/ba_data/textures/powerupBomb_preview.png \ - build/ba_data/textures/powerupCurse_preview.png \ - build/ba_data/textures/powerupHealth_preview.png \ - build/ba_data/textures/powerupIceBombs_preview.png \ - build/ba_data/textures/powerupImpactBombs_preview.png \ - build/ba_data/textures/powerupLandMines_preview.png \ - build/ba_data/textures/powerupPunch_preview.png \ - build/ba_data/textures/powerupShield_preview.png \ - build/ba_data/textures/powerupSpeed_preview.png \ - build/ba_data/textures/powerupStickyBombs_preview.png \ - build/ba_data/textures/puckColor_preview.png \ - build/ba_data/textures/rampageBGColor2_preview.png \ - build/ba_data/textures/rampageBGColor_preview.png \ - build/ba_data/textures/rampageLevelColor_preview.png \ - build/ba_data/textures/rampagePreview_preview.png \ - build/ba_data/textures/reflectionChar_+x_preview.png \ - build/ba_data/textures/reflectionChar_+y_preview.png \ - build/ba_data/textures/reflectionChar_+z_preview.png \ - build/ba_data/textures/reflectionChar_-x_preview.png \ - build/ba_data/textures/reflectionChar_-y_preview.png \ - build/ba_data/textures/reflectionChar_-z_preview.png \ - build/ba_data/textures/reflectionPowerup_+x_preview.png \ - build/ba_data/textures/reflectionPowerup_+y_preview.png \ - build/ba_data/textures/reflectionPowerup_+z_preview.png \ - build/ba_data/textures/reflectionPowerup_-x_preview.png \ - build/ba_data/textures/reflectionPowerup_-y_preview.png \ - build/ba_data/textures/reflectionPowerup_-z_preview.png \ - build/ba_data/textures/reflectionSharp_+x_preview.png \ - build/ba_data/textures/reflectionSharp_+y_preview.png \ - build/ba_data/textures/reflectionSharp_+z_preview.png \ - build/ba_data/textures/reflectionSharp_-x_preview.png \ - build/ba_data/textures/reflectionSharp_-y_preview.png \ - build/ba_data/textures/reflectionSharp_-z_preview.png \ - build/ba_data/textures/reflectionSharper_+x_preview.png \ - build/ba_data/textures/reflectionSharper_+y_preview.png \ - build/ba_data/textures/reflectionSharper_+z_preview.png \ - build/ba_data/textures/reflectionSharper_-x_preview.png \ - build/ba_data/textures/reflectionSharper_-y_preview.png \ - build/ba_data/textures/reflectionSharper_-z_preview.png \ - build/ba_data/textures/reflectionSharpest_+x_preview.png \ - build/ba_data/textures/reflectionSharpest_+y_preview.png \ - build/ba_data/textures/reflectionSharpest_+z_preview.png \ - build/ba_data/textures/reflectionSharpest_-x_preview.png \ - build/ba_data/textures/reflectionSharpest_-y_preview.png \ - build/ba_data/textures/reflectionSharpest_-z_preview.png \ - build/ba_data/textures/reflectionSoft_+x_preview.png \ - build/ba_data/textures/reflectionSoft_+y_preview.png \ - build/ba_data/textures/reflectionSoft_+z_preview.png \ - build/ba_data/textures/reflectionSoft_-x_preview.png \ - build/ba_data/textures/reflectionSoft_-y_preview.png \ - build/ba_data/textures/reflectionSoft_-z_preview.png \ - build/ba_data/textures/replayIcon_preview.png \ - build/ba_data/textures/rgbStripes_preview.png \ - build/ba_data/textures/rightButton_preview.png \ - build/ba_data/textures/robotColorMask_preview.png \ - build/ba_data/textures/robotColor_preview.png \ - build/ba_data/textures/robotIconColorMask_preview.png \ - build/ba_data/textures/robotIcon_preview.png \ - build/ba_data/textures/roundaboutLevelColor_preview.png \ - build/ba_data/textures/roundaboutPreview_preview.png \ - build/ba_data/textures/santaColorMask_preview.png \ - build/ba_data/textures/santaColor_preview.png \ - build/ba_data/textures/santaIconColorMask_preview.png \ - build/ba_data/textures/santaIcon_preview.png \ - build/ba_data/textures/scorchBig_preview.png \ - build/ba_data/textures/scorch_preview.png \ - build/ba_data/textures/scrollWidgetGlow_preview.png \ - build/ba_data/textures/scrollWidget_preview.png \ - build/ba_data/textures/settingsIcon_preview.png \ - build/ba_data/textures/shadowSharp_preview.png \ - build/ba_data/textures/shadowSoft_preview.png \ - build/ba_data/textures/shadow_preview.png \ - build/ba_data/textures/shield_preview.png \ - build/ba_data/textures/shrapnel1Color_preview.png \ - build/ba_data/textures/slash_preview.png \ - build/ba_data/textures/smoke_preview.png \ - build/ba_data/textures/softRect2_preview.png \ - build/ba_data/textures/softRectVertical_preview.png \ - build/ba_data/textures/softRect_preview.png \ - build/ba_data/textures/sparks_preview.png \ - build/ba_data/textures/star_preview.png \ - build/ba_data/textures/startButton_preview.png \ - build/ba_data/textures/stepRightUpLevelColor_preview.png \ - build/ba_data/textures/stepRightUpPreview_preview.png \ - build/ba_data/textures/storeCharacterEaster_preview.png \ - build/ba_data/textures/storeCharacterXmas_preview.png \ - build/ba_data/textures/storeCharacter_preview.png \ - build/ba_data/textures/storeIcon_preview.png \ - build/ba_data/textures/superheroColorMask_preview.png \ - build/ba_data/textures/superheroColor_preview.png \ - build/ba_data/textures/superheroIconColorMask_preview.png \ - build/ba_data/textures/superheroIcon_preview.png \ - build/ba_data/textures/textClearButton_preview.png \ - build/ba_data/textures/thePadLevelColor_preview.png \ - build/ba_data/textures/thePadPreview_preview.png \ - build/ba_data/textures/ticketRollBig_preview.png \ - build/ba_data/textures/ticketRoll_preview.png \ - build/ba_data/textures/ticketRolls_preview.png \ - build/ba_data/textures/ticketsMore_preview.png \ - build/ba_data/textures/tickets_preview.png \ - build/ba_data/textures/tipTopBGColor_preview.png \ - build/ba_data/textures/tipTopLevelColor_preview.png \ - build/ba_data/textures/tipTopPreview_preview.png \ - build/ba_data/textures/tnt_preview.png \ - build/ba_data/textures/touchArrowsActions_preview.png \ - build/ba_data/textures/touchArrows_preview.png \ - build/ba_data/textures/towerDLevelColor_preview.png \ - build/ba_data/textures/towerDPreview_preview.png \ - build/ba_data/textures/treesColor_preview.png \ - build/ba_data/textures/trophy_preview.png \ - build/ba_data/textures/tv_preview.png \ - build/ba_data/textures/uiAtlas2_preview.png \ - build/ba_data/textures/uiAtlas_preview.png \ - build/ba_data/textures/upButton_preview.png \ - build/ba_data/textures/usersButton_preview.png \ - build/ba_data/textures/vrFillMound_preview.png \ - build/ba_data/textures/warriorColorMask_preview.png \ - build/ba_data/textures/warriorColor_preview.png \ - build/ba_data/textures/warriorIconColorMask_preview.png \ - build/ba_data/textures/warriorIcon_preview.png \ - build/ba_data/textures/white_preview.png \ - build/ba_data/textures/windowHSmallVMed_preview.png \ - build/ba_data/textures/windowHSmallVSmall_preview.png \ - build/ba_data/textures/wings_preview.png \ - build/ba_data/textures/witchColorMask_preview.png \ - build/ba_data/textures/witchColor_preview.png \ - build/ba_data/textures/witchIconColorMask_preview.png \ - build/ba_data/textures/witchIcon_preview.png \ - build/ba_data/textures/wizardColorMask_preview.png \ - build/ba_data/textures/wizardColor_preview.png \ - build/ba_data/textures/wizardIconColorMask_preview.png \ - build/ba_data/textures/wizardIcon_preview.png \ - build/ba_data/textures/wrestlerColorMask_preview.png \ - build/ba_data/textures/wrestlerColor_preview.png \ - build/ba_data/textures/wrestlerIconColorMask_preview.png \ - build/ba_data/textures/wrestlerIcon_preview.png \ - build/ba_data/textures/zigZagLevelColor_preview.png \ - build/ba_data/textures/zigzagPreview_preview.png \ - build/ba_data/textures/zoeColorMask_preview.png \ - build/ba_data/textures/zoeColor_preview.png \ - build/ba_data/textures/zoeIconColorMask_preview.png \ - build/ba_data/textures/zoeIcon_preview.png - -EXTRAS_TARGETS_WIN_WIN32 = \ - build/windows/Win32/DLLs/_asyncio.pyd \ - build/windows/Win32/DLLs/_asyncio_d.pyd \ - build/windows/Win32/DLLs/_bz2.pyd \ - build/windows/Win32/DLLs/_bz2_d.pyd \ - build/windows/Win32/DLLs/_ctypes.pyd \ - build/windows/Win32/DLLs/_ctypes_d.pyd \ - build/windows/Win32/DLLs/_ctypes_test.pyd \ - build/windows/Win32/DLLs/_ctypes_test_d.pyd \ - build/windows/Win32/DLLs/_decimal.pyd \ - build/windows/Win32/DLLs/_decimal_d.pyd \ - build/windows/Win32/DLLs/_elementtree.pyd \ - build/windows/Win32/DLLs/_elementtree_d.pyd \ - build/windows/Win32/DLLs/_hashlib.pyd \ - build/windows/Win32/DLLs/_hashlib_d.pyd \ - build/windows/Win32/DLLs/_lzma.pyd \ - build/windows/Win32/DLLs/_lzma_d.pyd \ - build/windows/Win32/DLLs/_msi.pyd \ - build/windows/Win32/DLLs/_msi_d.pyd \ - build/windows/Win32/DLLs/_multiprocessing.pyd \ - build/windows/Win32/DLLs/_multiprocessing_d.pyd \ - build/windows/Win32/DLLs/_overlapped.pyd \ - build/windows/Win32/DLLs/_overlapped_d.pyd \ - build/windows/Win32/DLLs/_queue.pyd \ - build/windows/Win32/DLLs/_queue_d.pyd \ - build/windows/Win32/DLLs/_socket.pyd \ - build/windows/Win32/DLLs/_socket_d.pyd \ - build/windows/Win32/DLLs/_sqlite3.pyd \ - build/windows/Win32/DLLs/_sqlite3_d.pyd \ - build/windows/Win32/DLLs/_ssl.pyd \ - build/windows/Win32/DLLs/_ssl_d.pyd \ - build/windows/Win32/DLLs/_testbuffer.pyd \ - build/windows/Win32/DLLs/_testbuffer_d.pyd \ - build/windows/Win32/DLLs/_testcapi.pyd \ - build/windows/Win32/DLLs/_testcapi_d.pyd \ - build/windows/Win32/DLLs/_testconsole.pyd \ - build/windows/Win32/DLLs/_testconsole_d.pyd \ - build/windows/Win32/DLLs/_testimportmultiple.pyd \ - build/windows/Win32/DLLs/_testimportmultiple_d.pyd \ - build/windows/Win32/DLLs/_testinternalcapi.pyd \ - build/windows/Win32/DLLs/_testinternalcapi_d.pyd \ - build/windows/Win32/DLLs/_testmultiphase.pyd \ - build/windows/Win32/DLLs/_testmultiphase_d.pyd \ - build/windows/Win32/DLLs/_tkinter.pyd \ - build/windows/Win32/DLLs/_tkinter_d.lib \ - build/windows/Win32/DLLs/_tkinter_d.pyd \ - build/windows/Win32/DLLs/_uuid.pyd \ - build/windows/Win32/DLLs/_uuid_d.pyd \ - build/windows/Win32/DLLs/_zoneinfo.pyd \ - build/windows/Win32/DLLs/_zoneinfo_d.pyd \ - build/windows/Win32/DLLs/libcrypto-1_1.dll \ - build/windows/Win32/DLLs/libffi-7.dll \ - build/windows/Win32/DLLs/libssl-1_1.dll \ - build/windows/Win32/DLLs/pyexpat.pyd \ - build/windows/Win32/DLLs/pyexpat_d.pyd \ - build/windows/Win32/DLLs/python_lib.cat \ - build/windows/Win32/DLLs/python_tools.cat \ - build/windows/Win32/DLLs/select.pyd \ - build/windows/Win32/DLLs/select_d.pyd \ - build/windows/Win32/DLLs/sqlite3.dll \ - build/windows/Win32/DLLs/sqlite3_d.dll \ - build/windows/Win32/DLLs/tcl86t.dll \ - build/windows/Win32/DLLs/tk86t.dll \ - build/windows/Win32/DLLs/unicodedata.pyd \ - build/windows/Win32/DLLs/unicodedata_d.pyd \ - build/windows/Win32/DLLs/winsound.pyd \ - build/windows/Win32/DLLs/winsound_d.pyd \ - build/windows/Win32/Lib/ctypes/macholib/README.ctypes \ - build/windows/Win32/Lib/ctypes/macholib/fetch_macholib \ - build/windows/Win32/Lib/ctypes/macholib/fetch_macholib.bat \ - build/windows/Win32/Lib/email/architecture.rst \ - build/windows/Win32/OpenAL32.dll \ - build/windows/Win32/SDL2.dll \ - build/windows/Win32/libvorbis.dll \ - build/windows/Win32/libvorbisfile.dll \ - build/windows/Win32/msvcp140d.dll \ - build/windows/Win32/ogg.dll \ - build/windows/Win32/python.exe \ - build/windows/Win32/python310.dll \ - build/windows/Win32/python310_d.dll \ - build/windows/Win32/python_d.exe \ - build/windows/Win32/pythonw.exe \ - build/windows/Win32/pythonw_d.exe \ - build/windows/Win32/ucrtbased.dll \ - build/windows/Win32/vc_redist.x86.exe \ - build/windows/Win32/vcruntime140d.dll - -# Rule to copy src extras to build. -$(EXTRAS_TARGETS_WIN_WIN32) : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - - -EXTRAS_TARGETS_WIN_X64 = \ - build/windows/x64/DLLs/_asyncio.pyd \ - build/windows/x64/DLLs/_asyncio_d.pyd \ - build/windows/x64/DLLs/_bz2.pyd \ - build/windows/x64/DLLs/_bz2_d.pyd \ - build/windows/x64/DLLs/_ctypes.pyd \ - build/windows/x64/DLLs/_ctypes_d.pyd \ - build/windows/x64/DLLs/_ctypes_test.pyd \ - build/windows/x64/DLLs/_ctypes_test_d.pyd \ - build/windows/x64/DLLs/_decimal.pyd \ - build/windows/x64/DLLs/_decimal_d.pyd \ - build/windows/x64/DLLs/_elementtree.pyd \ - build/windows/x64/DLLs/_elementtree_d.pyd \ - build/windows/x64/DLLs/_hashlib.pyd \ - build/windows/x64/DLLs/_hashlib_d.pyd \ - build/windows/x64/DLLs/_lzma.pyd \ - build/windows/x64/DLLs/_lzma_d.pyd \ - build/windows/x64/DLLs/_msi.pyd \ - build/windows/x64/DLLs/_msi_d.pyd \ - build/windows/x64/DLLs/_multiprocessing.pyd \ - build/windows/x64/DLLs/_multiprocessing_d.pyd \ - build/windows/x64/DLLs/_overlapped.pyd \ - build/windows/x64/DLLs/_overlapped_d.pyd \ - build/windows/x64/DLLs/_queue.pyd \ - build/windows/x64/DLLs/_queue_d.pyd \ - build/windows/x64/DLLs/_socket.pyd \ - build/windows/x64/DLLs/_socket_d.pyd \ - build/windows/x64/DLLs/_sqlite3.pyd \ - build/windows/x64/DLLs/_sqlite3_d.pyd \ - build/windows/x64/DLLs/_ssl.pyd \ - build/windows/x64/DLLs/_ssl_d.pyd \ - build/windows/x64/DLLs/_testbuffer.pyd \ - build/windows/x64/DLLs/_testbuffer_d.pyd \ - build/windows/x64/DLLs/_testcapi.pyd \ - build/windows/x64/DLLs/_testcapi_d.pyd \ - build/windows/x64/DLLs/_testconsole.pyd \ - build/windows/x64/DLLs/_testconsole_d.pyd \ - build/windows/x64/DLLs/_testimportmultiple.pyd \ - build/windows/x64/DLLs/_testimportmultiple_d.pyd \ - build/windows/x64/DLLs/_testinternalcapi.pyd \ - build/windows/x64/DLLs/_testinternalcapi_d.pyd \ - build/windows/x64/DLLs/_testmultiphase.pyd \ - build/windows/x64/DLLs/_testmultiphase_d.pyd \ - build/windows/x64/DLLs/_tkinter.pyd \ - build/windows/x64/DLLs/_tkinter_d.lib \ - build/windows/x64/DLLs/_tkinter_d.pyd \ - build/windows/x64/DLLs/_uuid.pyd \ - build/windows/x64/DLLs/_uuid_d.pyd \ - build/windows/x64/DLLs/_zoneinfo.pyd \ - build/windows/x64/DLLs/_zoneinfo_d.pyd \ - build/windows/x64/DLLs/libcrypto-1_1.dll \ - build/windows/x64/DLLs/libffi-7.dll \ - build/windows/x64/DLLs/libssl-1_1.dll \ - build/windows/x64/DLLs/pyexpat.pyd \ - build/windows/x64/DLLs/pyexpat_d.pyd \ - build/windows/x64/DLLs/python_lib.cat \ - build/windows/x64/DLLs/python_tools.cat \ - build/windows/x64/DLLs/select.pyd \ - build/windows/x64/DLLs/select_d.pyd \ - build/windows/x64/DLLs/sqlite3.dll \ - build/windows/x64/DLLs/sqlite3_d.dll \ - build/windows/x64/DLLs/tcl86t.dll \ - build/windows/x64/DLLs/tk86t.dll \ - build/windows/x64/DLLs/unicodedata.pyd \ - build/windows/x64/DLLs/unicodedata_d.pyd \ - build/windows/x64/DLLs/winsound.pyd \ - build/windows/x64/DLLs/winsound_d.pyd \ - build/windows/x64/Lib/ctypes/macholib/README.ctypes \ - build/windows/x64/Lib/ctypes/macholib/fetch_macholib \ - build/windows/x64/Lib/ctypes/macholib/fetch_macholib.bat \ - build/windows/x64/Lib/email/architecture.rst \ - build/windows/x64/OpenAL32.dll \ - build/windows/x64/SDL2.dll \ - build/windows/x64/libvorbis.dll \ - build/windows/x64/libvorbisfile.dll \ - build/windows/x64/msvcp140d.dll \ - build/windows/x64/ogg.dll \ - build/windows/x64/python.exe \ - build/windows/x64/python310.dll \ - build/windows/x64/python310_d.dll \ - build/windows/x64/python_d.exe \ - build/windows/x64/pythonw.exe \ - build/windows/x64/pythonw_d.exe \ - build/windows/x64/ucrtbased.dll \ - build/windows/x64/vc_redist.x64.exe \ - build/windows/x64/vcruntime140_1d.dll \ - build/windows/x64/vcruntime140d.dll - -# Rule to copy src extras to build. -$(EXTRAS_TARGETS_WIN_X64) : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -# __AUTOGENERATED_PRIVATE_END__ - -ASSET_TARGETS_COMMON += $(MODEL_TARGETS) - -build/%.bob : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -build/%.cob : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -build/%.ogg : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -build/%.fdata : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -build/%.pem : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -# Langdata one-off json file. -build/ba_data/data/langdata.json : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -# Languages json files. -build/ba_data/data/languages/%.json : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -# Map json files. -build/ba_data/data/maps/%.json : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -src/ba_data/%.tex2d.png : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -src/ba_data/%_+x.tex2d.png : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -build/ba_data/%_preview.png : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -build/%.dds : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -build/%.pvr : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -build/%.ktx : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get assets/$@ - -audio: $(AUDIO_TARGETS) -clean-audio: - rm -rf build/ba_data/audio - -data: $(DATA_TARGETS) -clean-data: - rm -rf build/ba_data/data - -fonts: $(FONT_TARGETS) -clean-fonts: - rm -rf build/ba_data/fonts - -# Model targets needed for all platforms. -MODEL_TARGETS = ${BOB_TARGETS} ${COB_TARGETS} - -models: $(MODEL_TARGETS) -clean-models: - rm -rf build/ba_data/models - -# Texture targets needed per-platform (may overlap) -TEXTURE_TARGETS_CMAKE = $(TEX2D_DDS_TARGETS) -TEXTURE_TARGETS_WIN = $(TEX2D_DDS_TARGETS) -TEXTURE_TARGETS_MAC = $(TEX2D_DDS_TARGETS) -TEXTURE_TARGETS_IOS = $(TEX2D_PVR_TARGETS) -TEXTURE_TARGETS_ANDROID = $(TEX2D_KTX_TARGETS) - -# Texture targets needed for all platforms. -TEXTURE_TARGETS_COMMON = $(TEX2D_PREVIEW_PNG_TARGETS) \ - $(TEX2D_PREVIEW_PNG_TARGETS_2) - -clean-textures: - rm -rf build/ba_data/textures - -# Script targets needed per-platform (may overlap) -SCRIPT_TARGETS_CMAKE = -SCRIPT_TARGETS_WIN_WIN32 = $(SCRIPT_TARGETS_PY_PRIVATE_WIN_WIN32) \ - $(SCRIPT_TARGETS_PYC_PRIVATE_WIN_WIN32) -SCRIPT_TARGETS_WIN_X64 = $(SCRIPT_TARGETS_PY_PRIVATE_WIN_X64) \ - $(SCRIPT_TARGETS_PYC_PRIVATE_WIN_X64) -SCRIPT_TARGETS_MAC = $(SCRIPT_TARGETS_PY_PRIVATE_APPLE) \ - $(SCRIPT_TARGETS_PYC_PRIVATE_APPLE) -SCRIPT_TARGETS_IOS = $(SCRIPT_TARGETS_PY_PRIVATE_APPLE) \ - $(SCRIPT_TARGETS_PYC_PRIVATE_APPLE) -SCRIPT_TARGETS_ANDROID = $(SCRIPT_TARGETS_PY_PRIVATE_ANDROID) \ - $(SCRIPT_TARGETS_PYC_PRIVATE_ANDROID) - -# Script targets needed by all platforms. -SCRIPT_TARGETS_COMMON = $(SCRIPT_TARGETS_PY_PUBLIC) \ - $(SCRIPT_TARGETS_PYC_PUBLIC) $(SCRIPT_TARGETS_PY_PUBLIC_TOOLS) \ - $(SCRIPT_TARGETS_PYC_PUBLIC_TOOLS) $(SCRIPT_TARGETS_PY_PRIVATE_COMMON) \ - $(SCRIPT_TARGETS_PYC_PRIVATE_COMMON) $(PEM_TARGETS) - -# Build scripts for a specific platform. -scripts-cmake: $(SCRIPT_TARGETS_CMAKE) $(SCRIPT_TARGETS_COMMON) -scripts-win-Win32: $(SCRIPT_TARGETS_WIN_WIN32) $(SCRIPT_TARGETS_COMMON) -scripts-win-x64: $(SCRIPT_TARGETS_WIN_X64) $(SCRIPT_TARGETS_COMMON) -scripts-mac: $(SCRIPT_TARGETS_MAC) $(SCRIPT_TARGETS_COMMON) -scripts-ios: $(SCRIPT_TARGETS_IOS) $(SCRIPT_TARGETS_COMMON) -scripts-android: $(SCRIPT_TARGETS_ANDROID) $(SCRIPT_TARGETS_COMMON) - -# Build scripts for all platforms -scripts: scripts-cmake scripts-win-Win32 scripts-win-x64 scripts-mac \ - scripts-ios scripts-android -clean-scripts: - rm -rf build/ba_data/python build/ba_data/python-site-packages \ - build/pylib-android build/pylib-apple build/windows/Win32/Lib \ - build/windows/x64/Lib - -# Build all required assets for a specific platform. -assets-cmake: $(ASSET_TARGETS_CMAKE) $(ASSET_TARGETS_COMMON) -assets-win-Win32: $(ASSET_TARGETS_WIN_WIN32) $(ASSET_TARGETS_COMMON) -assets-win-x64: $(ASSET_TARGETS_WIN_X64) $(ASSET_TARGETS_COMMON) -assets-mac: $(ASSET_TARGETS_MAC) $(ASSET_TARGETS_COMMON) -assets-ios: $(ASSET_TARGETS_IOS) $(ASSET_TARGETS_COMMON) -assets-android: $(ASSET_TARGETS_ANDROID) $(ASSET_TARGETS_COMMON) - -# Build all assets for all platforms. -assets: assets-cmake assets-win-Win32 assets-win-x64 assets-mac assets-ios \ - assets-android - -clean: - @rm -rf build - -# None of these targets correspond to actual files; make sure make knows that. -.PHONY: cmake win mac ios android audio clean-audio fonts clean-fonts \ - data clean-data models clean-models \ - clean-textures scripts clean-scripts \ - assets assets-cmake assets-win-Win32 assets-win-x64 assets-mac \ - assets-ios assets-android asset_sources clean diff --git a/assets/src/ba_data/python/._ba_sources_hash b/assets/src/ba_data/python/._ba_sources_hash deleted file mode 100644 index 71902207..00000000 --- a/assets/src/ba_data/python/._ba_sources_hash +++ /dev/null @@ -1 +0,0 @@ -71864217068887285722858773141608052966 \ No newline at end of file diff --git a/assets/src/ba_data/python/._bainternal_sources_hash b/assets/src/ba_data/python/._bainternal_sources_hash deleted file mode 100644 index 99a731ce..00000000 --- a/assets/src/ba_data/python/._bainternal_sources_hash +++ /dev/null @@ -1 +0,0 @@ -139020022013133168311319486434408589898 diff --git a/assets/src/ba_data/python/_ba.py b/assets/src/ba_data/python/_ba.py deleted file mode 100644 index eb4a7c75..00000000 --- a/assets/src/ba_data/python/_ba.py +++ /dev/null @@ -1,3410 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""A dummy stub module for the real _ba. - -The real _ba is a compiled extension module and only available -in the live engine. This dummy-module allows Pylint/Mypy/etc. to -function reasonably well outside of that environment. - -Make sure this file is never included in dirs seen by the engine! - -In the future perhaps this can be a stub (.pyi) file, but we will need -to make sure that it works with all our tools (mypy, pylint, pycharm). - -NOTE: This file was autogenerated by batools.dummymodule; do not edit by hand. -""" - -# I'm sorry Pylint. I know this file saddens you. Be strong. -# pylint: disable=useless-suppression -# pylint: disable=unnecessary-pass -# pylint: disable=use-dict-literal -# pylint: disable=use-list-literal -# pylint: disable=unused-argument -# pylint: disable=missing-docstring -# pylint: disable=too-many-locals -# pylint: disable=redefined-builtin -# pylint: disable=too-many-lines -# pylint: disable=redefined-outer-name -# pylint: disable=invalid-name -# pylint: disable=no-value-for-parameter - -from __future__ import annotations - -from typing import TYPE_CHECKING, overload, Sequence, TypeVar - -from ba._generated.enums import TimeFormat, TimeType - -if TYPE_CHECKING: - from typing import Any, Callable, Literal - from ba._app import App - import ba - - -_T = TypeVar('_T') - -app: App - - -def _uninferrable() -> Any: - """Get an "Any" in mypy and "uninferrable" in Pylint.""" - # pylint: disable=undefined-variable - return _not_a_real_variable # type: ignore - - -class ActivityData: - - """(internal)""" - - def exists(self) -> bool: - - """Returns whether the ActivityData still exists. - Most functionality will fail on a nonexistent instance. - """ - return bool() - - def expire(self) -> None: - - """Expires the internal data for the activity""" - return None - - def make_foreground(self) -> None: - - """Sets this activity as the foreground one in its session.""" - return None - - def start(self) -> None: - - """Begins the activity running""" - return None - - -class CollideModel: - - """A reference to a collide-model. - - Category: **Asset Classes** - - Use ba.getcollidemodel() to instantiate one. - """ - - pass - - -class Context: - - """A game context state. - - Category: **General Utility Classes** - - Many operations such as ba.newnode() or ba.gettexture() operate - implicitly on the current context. Each ba.Activity has its own - Context and objects within that activity (nodes, media, etc) can only - interact with other objects from that context. - - In general, as a modder, you should not need to worry about contexts, - since timers and other callbacks will take care of saving and - restoring the context automatically, but there may be rare cases where - you need to deal with them, such as when loading media in for use in - the UI (there is a special `'ui'` context for all - user-interface-related functionality). - - When instantiating a ba.Context instance, a single `'source'` argument - is passed, which can be one of the following strings/objects: - - ###### `'empty'` - > Gives an empty context; it can be handy to run code here to ensure - it does no loading of media, creation of nodes, etc. - - ###### `'current'` - > Sets the context object to the current context. - - ###### `'ui'` - > Sets to the UI context. UI functions as well as loading of media to - be used in said functions must happen in the UI context. - - ###### A ba.Activity instance - > Gives the context for the provided ba.Activity. - Most all code run during a game happens in an Activity's Context. - - ###### A ba.Session instance - > Gives the context for the provided ba.Session. - Generally a user should not need to run anything here. - - - ##### Usage - Contexts are generally used with the python 'with' statement, which - sets the context as current on entry and resets it to the previous - value on exit. - - ##### Example - Load a few textures into the UI context - (for use in widgets, etc): - >>> with ba.Context('ui'): - ... tex1 = ba.gettexture('foo_tex_1') - ... tex2 = ba.gettexture('foo_tex_2') - """ - - def __init__(self, source: Any): - pass - - def __enter__(self) -> None: - """Support for "with" statement.""" - pass - - def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any: - """Support for "with" statement.""" - pass - - -class ContextCall: - - """A context-preserving callable. - - Category: **General Utility Classes** - - A ContextCall wraps a callable object along with a reference - to the current context (see ba.Context); it handles restoring the - context when run and automatically clears itself if the context - it belongs to shuts down. - - Generally you should not need to use this directly; all standard - Ballistica callbacks involved with timers, materials, UI functions, - etc. handle this under-the-hood you don't have to worry about it. - The only time it may be necessary is if you are implementing your - own callbacks, such as a worker thread that does some action and then - runs some game code when done. By wrapping said callback in one of - these, you can ensure that you will not inadvertently be keeping the - current activity alive or running code in a torn-down (expired) - context. - - You can also use ba.WeakCall for similar functionality, but - ContextCall has the added bonus that it will not run during context - shutdown, whereas ba.WeakCall simply looks at whether the target - object still exists. - - ##### Examples - **Example A:** code like this can inadvertently prevent our activity - (self) from ending until the operation completes, since the bound - method we're passing (self.dosomething) contains a strong-reference - to self). - >>> start_some_long_action(callback_when_done=self.dosomething) - - **Example B:** in this case our activity (self) can still die - properly; the callback will clear itself when the activity starts - shutting down, becoming a harmless no-op and releasing the reference - to our activity. - - >>> start_long_action( - ... callback_when_done=ba.ContextCall(self.mycallback)) - """ - - def __init__(self, call: Callable): - pass - - -class Data: - - """A reference to a data object. - - Category: **Asset Classes** - - Use ba.getdata() to instantiate one. - """ - - def getvalue(self) -> Any: - - """Return the data object's value. - - This can consist of anything representable by json (dicts, lists, - numbers, bools, None, etc). - Note that this call will block if the data has not yet been loaded, - so it can be beneficial to plan a short bit of time between when - the data object is requested and when it's value is accessed. - """ - return _uninferrable() - - -class InputDevice: - - """An input-device such as a gamepad, touchscreen, or keyboard. - - Category: **Gameplay Classes** - """ - - allows_configuring: bool - - """Whether the input-device can be configured.""" - - has_meaningful_button_names: bool - - """Whether button names returned by this instance match labels - on the actual device. (Can be used to determine whether to show - them in controls-overlays, etc.).""" - - player: ba.SessionPlayer | None - - """The player associated with this input device.""" - - client_id: int - - """The numeric client-id this device is associated with. - This is only meaningful for remote client inputs; for - all local devices this will be -1.""" - - name: str - - """The name of the device.""" - - unique_identifier: str - - """A string that can be used to persistently identify the device, - even among other devices of the same type. Used for saving - prefs, etc.""" - - id: int - - """The unique numeric id of this device.""" - - instance_number: int - - """The number of this device among devices of the same type.""" - - is_controller_app: bool - - """Whether this input-device represents a locally-connected - controller-app.""" - - is_remote_client: bool - - """Whether this input-device represents a remotely-connected - client.""" - - def exists(self) -> bool: - - """Return whether the underlying device for this object is - still present. - """ - return bool() - - def get_axis_name(self, axis_id: int) -> str: - - """Given an axis ID, return the name of the axis on this device. - - Can return an empty string if the value is not meaningful to humans. - """ - return str() - - def get_button_name(self, button_id: int) -> ba.Lstr: - - """Given a button ID, return a human-readable name for that key/button. - - Can return an empty string if the value is not meaningful to humans. - """ - import ba # pylint: disable=cyclic-import - - return ba.Lstr(value='') - - def get_default_player_name(self) -> str: - - """(internal) - - Returns the default player name for this device. (used for the 'random' - profile) - """ - return str() - - def get_player_profiles(self) -> dict: - - """(internal)""" - return dict() - - def get_v1_account_name(self, full: bool) -> str: - - """Returns the account name associated with this device. - - (can be used to get account names for remote players) - """ - return str() - - def is_connected_to_remote_player(self) -> bool: - - """(internal)""" - return bool() - - def remove_remote_player_from_game(self) -> None: - - """(internal)""" - return None - - -class Material: - - """An entity applied to game objects to modify collision behavior. - - Category: **Gameplay Classes** - - A material can affect physical characteristics, generate sounds, - or trigger callback functions when collisions occur. - - Materials are applied to 'parts', which are groups of one or more - rigid bodies created as part of a ba.Node. Nodes can have any number - of parts, each with its own set of materials. Generally materials are - specified as array attributes on the Node. The `spaz` node, for - example, has various attributes such as `materials`, - `roller_materials`, and `punch_materials`, which correspond - to the various parts it creates. - - Use ba.Material to instantiate a blank material, and then use its - ba.Material.add_actions() method to define what the material does. - """ - - def __init__(self, label: str | None = None): - pass - - label: str - - """A label for the material; only used for debugging.""" - - def add_actions( - self, actions: tuple, conditions: tuple | None = None - ) -> None: - - """Add one or more actions to the material, optionally with conditions. - - ##### Conditions - Conditions are provided as tuples which can be combined - to form boolean logic. A single condition might look like - `('condition_name', cond_arg)`, or a more complex nested one - might look like `(('some_condition', cond_arg), 'or', - ('another_condition', cond2_arg))`. - - `'and'`, `'or'`, and `'xor'` are available to chain - together 2 conditions, as seen above. - - ##### Available Conditions - ###### `('they_have_material', material)` - > Does the part we're hitting have a given ba.Material? - - ###### `('they_dont_have_material', material)` - > Does the part we're hitting not have a given ba.Material? - - ###### `('eval_colliding')` - > Is `'collide'` true at this point - in material evaluation? (see the `modify_part_collision` action) - - ###### `('eval_not_colliding')` - > Is 'collide' false at this point - in material evaluation? (see the `modify_part_collision` action) - - ###### `('we_are_younger_than', age)` - > Is our part younger than `age` (in milliseconds)? - - ###### `('we_are_older_than', age)` - > Is our part older than `age` (in milliseconds)? - - ###### `('they_are_younger_than', age)` - > Is the part we're hitting younger than `age` (in milliseconds)? - - ###### `('they_are_older_than', age)` - > Is the part we're hitting older than `age` (in milliseconds)? - - ###### `('they_are_same_node_as_us')` - > Does the part we're hitting belong to the same ba.Node as us? - - ###### `('they_are_different_node_than_us')` - > Does the part we're hitting belong to a different ba.Node than us? - - ##### Actions - In a similar manner, actions are specified as tuples. - Multiple actions can be specified by providing a tuple - of tuples. - - ##### Available Actions - ###### `('call', when, callable)` - > Calls the provided callable; - `when` can be either `'at_connect'` or `'at_disconnect'`. - `'at_connect'` means to fire - when the two parts first come in contact; `'at_disconnect'` - means to fire once they cease being in contact. - - ###### `('message', who, when, message_obj)` - > Sends a message object; - `who` can be either `'our_node'` or `'their_node'`, `when` can be - `'at_connect'` or `'at_disconnect'`, and `message_obj` is the message - object to send. - This has the same effect as calling the node's - ba.Node.handlemessage() method. - - ###### `('modify_part_collision', attr, value)` - > Changes some - characteristic of the physical collision that will occur between - our part and their part. This change will remain in effect as - long as the two parts remain overlapping. This means if you have a - part with a material that turns `'collide'` off against parts - younger than 100ms, and it touches another part that is 50ms old, - it will continue to not collide with that part until they separate, - even if the 100ms threshold is passed. Options for attr/value are: - `'physical'` (boolean value; whether a *physical* response will - occur at all), `'friction'` (float value; how friction-y the - physical response will be), `'collide'` (boolean value; - whether *any* collision will occur at all, including non-physical - stuff like callbacks), `'use_node_collide'` - (boolean value; whether to honor modify_node_collision - overrides for this collision), `'stiffness'` (float value, - how springy the physical response is), `'damping'` (float - value, how damped the physical response is), `'bounce'` (float - value; how bouncy the physical response is). - - ###### `('modify_node_collision', attr, value)` - > Similar to - `modify_part_collision`, but operates at a node-level. - collision attributes set here will remain in effect as long as - *anything* from our part's node and their part's node overlap. - A key use of this functionality is to prevent new nodes from - colliding with each other if they appear overlapped; - if `modify_part_collision` is used, only the individual - parts that were overlapping would avoid contact, but other parts - could still contact leaving the two nodes 'tangled up'. Using - `modify_node_collision` ensures that the nodes must completely - separate before they can start colliding. Currently the only attr - available here is `'collide'` (a boolean value). - - ###### `('sound', sound, volume)` - > Plays a ba.Sound when a collision - occurs, at a given volume, regardless of the collision speed/etc. - - ###### `('impact_sound', sound, targetImpulse, volume)` - > Plays a sound - when a collision occurs, based on the speed of impact. - Provide a ba.Sound, a target-impulse, and a volume. - - ###### `('skid_sound', sound, targetImpulse, volume)` - > Plays a sound - during a collision when parts are 'scraping' against each other. - Provide a ba.Sound, a target-impulse, and a volume. - - ###### `('roll_sound', sound, targetImpulse, volume)` - > Plays a sound - during a collision when parts are 'rolling' against each other. - Provide a ba.Sound, a target-impulse, and a volume. - - ##### Examples - **Example 1:** create a material that lets us ignore - collisions against any nodes we touch in the first - 100 ms of our existence; handy for preventing us from - exploding outward if we spawn on top of another object: - >>> m = ba.Material() - ... m.add_actions( - ... conditions=(('we_are_younger_than', 100), - ... 'or', ('they_are_younger_than', 100)), - ... actions=('modify_node_collision', 'collide', False)) - - **Example 2:** send a ba.DieMessage to anything we touch, but cause - no physical response. This should cause any ba.Actor to drop dead: - >>> m = ba.Material() - ... m.add_actions( - ... actions=(('modify_part_collision', 'physical', False), - ... ('message', 'their_node', 'at_connect', - ... ba.DieMessage()))) - - **Example 3:** play some sounds when we're contacting the ground: - >>> m = ba.Material() - ... m.add_actions( - ... conditions=('they_have_material', - ... shared.footing_material), - ... actions=(('impact_sound', ba.getsound('metalHit'), 2, 5), - ... ('skid_sound', ba.getsound('metalSkid'), 2, 5))) - """ - return None - - -class Model: - - """A reference to a model. - - Category: **Asset Classes** - - Models are used for drawing. - Use ba.getmodel() to instantiate one. - """ - - pass - - -class Node: - - """Reference to a Node; the low level building block of the game. - - Category: **Gameplay Classes** - - At its core, a game is nothing more than a scene of Nodes - with attributes getting interconnected or set over time. - - A ba.Node instance should be thought of as a weak-reference - to a game node; *not* the node itself. This means a Node's - lifecycle is completely independent of how many Python references - to it exist. To explicitly add a new node to the game, use - ba.newnode(), and to explicitly delete one, use ba.Node.delete(). - ba.Node.exists() can be used to determine if a Node still points to - a live node in the game. - - You can use `ba.Node(None)` to instantiate an invalid - Node reference (sometimes used as attr values/etc). - """ - - # Note attributes: - # NOTE: I'm just adding *all* possible node attrs here - # now now since we have a single ba.Node type; in the - # future I hope to create proper individual classes - # corresponding to different node types with correct - # attributes per node-type. - color: Sequence[float] = (0.0, 0.0, 0.0) - size: Sequence[float] = (0.0, 0.0, 0.0) - position: Sequence[float] = (0.0, 0.0, 0.0) - position_center: Sequence[float] = (0.0, 0.0, 0.0) - position_forward: Sequence[float] = (0.0, 0.0, 0.0) - punch_position: Sequence[float] = (0.0, 0.0, 0.0) - punch_velocity: Sequence[float] = (0.0, 0.0, 0.0) - velocity: Sequence[float] = (0.0, 0.0, 0.0) - name_color: Sequence[float] = (0.0, 0.0, 0.0) - tint_color: Sequence[float] = (0.0, 0.0, 0.0) - tint2_color: Sequence[float] = (0.0, 0.0, 0.0) - text: ba.Lstr | str = '' - texture: ba.Texture | None = None - tint_texture: ba.Texture | None = None - times: Sequence[int] = (1, 2, 3, 4, 5) - values: Sequence[float] = (1.0, 2.0, 3.0, 4.0) - offset: float = 0.0 - input0: float = 0.0 - input1: float = 0.0 - input2: float = 0.0 - input3: float = 0.0 - flashing: bool = False - scale: float | Sequence[float] = 0.0 - opacity: float = 0.0 - loop: bool = False - time1: int = 0 - time2: int = 0 - timemax: int = 0 - client_only: bool = False - materials: Sequence[Material] = () - roller_materials: Sequence[Material] = () - name: str = '' - punch_materials: Sequence[ba.Material] = () - pickup_materials: Sequence[ba.Material] = () - extras_material: Sequence[ba.Material] = () - rotate: float = 0.0 - hold_node: ba.Node | None = None - hold_body: int = 0 - host_only: bool = False - premultiplied: bool = False - source_player: ba.Player | None = None - model_opaque: ba.Model | None = None - model_transparent: ba.Model | None = None - damage_smoothed: float = 0.0 - gravity_scale: float = 1.0 - punch_power: float = 0.0 - punch_momentum_linear: Sequence[float] = (0.0, 0.0, 0.0) - punch_momentum_angular: float = 0.0 - rate: int = 0 - vr_depth: float = 0.0 - is_area_of_interest: bool = False - jump_pressed: bool = False - pickup_pressed: bool = False - punch_pressed: bool = False - bomb_pressed: bool = False - fly_pressed: bool = False - hold_position_pressed: bool = False - knockout: float = 0.0 - invincible: bool = False - stick_to_owner: bool = False - damage: int = 0 - run: float = 0.0 - move_up_down: float = 0.0 - move_left_right: float = 0.0 - curse_death_time: int = 0 - boxing_gloves: bool = False - hockey: bool = False - use_fixed_vr_overlay: bool = False - allow_kick_idle_players: bool = False - music_continuous: bool = False - music_count: int = 0 - hurt: float = 0.0 - always_show_health_bar: bool = False - mini_billboard_1_texture: ba.Texture | None = None - mini_billboard_1_start_time: int = 0 - mini_billboard_1_end_time: int = 0 - mini_billboard_2_texture: ba.Texture | None = None - mini_billboard_2_start_time: int = 0 - mini_billboard_2_end_time: int = 0 - mini_billboard_3_texture: ba.Texture | None = None - mini_billboard_3_start_time: int = 0 - mini_billboard_3_end_time: int = 0 - boxing_gloves_flashing: bool = False - dead: bool = False - floor_reflection: bool = False - debris_friction: float = 0.0 - debris_kill_height: float = 0.0 - vr_near_clip: float = 0.0 - shadow_ortho: bool = False - happy_thoughts_mode: bool = False - shadow_offset: Sequence[float] = (0.0, 0.0) - paused: bool = False - time: int = 0 - ambient_color: Sequence[float] = (1.0, 1.0, 1.0) - camera_mode: str = 'rotate' - frozen: bool = False - area_of_interest_bounds: Sequence[float] = (-1, -1, -1, 1, 1, 1) - shadow_range: Sequence[float] = (0, 0, 0, 0) - counter_text: str = '' - counter_texture: ba.Texture | None = None - shattered: int = 0 - billboard_texture: ba.Texture | None = None - billboard_cross_out: bool = False - billboard_opacity: float = 0.0 - slow_motion: bool = False - music: str = '' - vr_camera_offset: Sequence[float] = (0.0, 0.0, 0.0) - vr_overlay_center: Sequence[float] = (0.0, 0.0, 0.0) - vr_overlay_center_enabled: bool = False - vignette_outer: Sequence[float] = (0.0, 0.0) - vignette_inner: Sequence[float] = (0.0, 0.0) - tint: Sequence[float] = (1.0, 1.0, 1.0) - - def add_death_action(self, action: Callable[[], None]) -> None: - - """Add a callable object to be called upon this node's death. - Note that these actions are run just after the node dies, not before. - """ - return None - - def connectattr(self, srcattr: str, dstnode: Node, dstattr: str) -> None: - - """Connect one of this node's attributes to an attribute on another - node. This will immediately set the target attribute's value to that - of the source attribute, and will continue to do so once per step - as long as the two nodes exist. The connection can be severed by - setting the target attribute to any value or connecting another - node attribute to it. - - ##### Example - Create a locator and attach a light to it: - >>> light = ba.newnode('light') - ... loc = ba.newnode('locator', attrs={'position': (0, 10, 0)}) - ... loc.connectattr('position', light, 'position') - """ - return None - - def delete(self, ignore_missing: bool = True) -> None: - - """Delete the node. Ignores already-deleted nodes if `ignore_missing` - is True; otherwise a ba.NodeNotFoundError is thrown. - """ - return None - - def exists(self) -> bool: - - """Returns whether the Node still exists. - Most functionality will fail on a nonexistent Node, so it's never a bad - idea to check this. - - Note that you can also use the boolean operator for this same - functionality, so a statement such as "if mynode" will do - the right thing both for Node objects and values of None. - """ - return bool() - - # Show that ur return type varies based on "doraise" value: - @overload - def getdelegate( - self, type: type[_T], doraise: Literal[False] = False - ) -> _T | None: - ... - - @overload - def getdelegate(self, type: type[_T], doraise: Literal[True]) -> _T: - ... - - def getdelegate(self, type: Any, doraise: bool = False) -> Any: - - """Return the node's current delegate object if it matches - a certain type. - - If the node has no delegate or it is not an instance of the passed - type, then None will be returned. If 'doraise' is True, then an - ba.DelegateNotFoundError will be raised instead. - """ - return None - - def getname(self) -> str: - - """Return the name assigned to a Node; used mainly for debugging""" - return str() - - def getnodetype(self) -> str: - - """Return the type of Node referenced by this object as a string. - (Note this is different from the Python type which is always ba.Node) - """ - return str() - - def handlemessage(self, *args: Any) -> None: - - """General message handling; can be passed any message object. - - All standard message objects are forwarded along to the ba.Node's - delegate for handling (generally the ba.Actor that made the node). - - ba.Node-s are unique, however, in that they can be passed a second - form of message; 'node-messages'. These consist of a string type-name - as a first argument along with the args specific to that type name - as additional arguments. - Node-messages communicate directly with the low-level node layer - and are delivered simultaneously on all game clients, - acting as an alternative to setting node attributes. - """ - return None - - -class SessionData: - - """(internal)""" - - def exists(self) -> bool: - - """Returns whether the SessionData still exists. - Most functionality will fail on a nonexistent instance. - """ - return bool() - - -class SessionPlayer: - - """A reference to a player in the ba.Session. - - Category: **Gameplay Classes** - - These are created and managed internally and - provided to your ba.Session/ba.Activity instances. - Be aware that, like `ba.Node`s, ba.SessionPlayer objects are 'weak' - references under-the-hood; a player can leave the game at - any point. For this reason, you should make judicious use of the - ba.SessionPlayer.exists() method (or boolean operator) to ensure - that a SessionPlayer is still present if retaining references to one - for any length of time. - """ - - id: int - - """The unique numeric ID of the Player. - - Note that you can also use the boolean operator for this same - functionality, so a statement such as "if player" will do - the right thing both for Player objects and values of None.""" - - in_game: bool - - """This bool value will be True once the Player has completed - any lobby character/team selection.""" - - sessionteam: ba.SessionTeam - - """The ba.SessionTeam this Player is on. If the SessionPlayer - is still in its lobby selecting a team/etc. then a - ba.SessionTeamNotFoundError will be raised.""" - - inputdevice: ba.InputDevice - - """The input device associated with the player.""" - - color: Sequence[float] - - """The base color for this Player. - In team games this will match the ba.SessionTeam's color.""" - - highlight: Sequence[float] - - """A secondary color for this player. - This is used for minor highlights and accents - to allow a player to stand apart from his teammates - who may all share the same team (primary) color.""" - - character: str - - """The character this player has selected in their profile.""" - - activityplayer: ba.Player | None - - """The current game-specific instance for this player.""" - - def assigninput( - self, type: ba.InputType | tuple[ba.InputType, ...], call: Callable - ) -> None: - - """Set the python callable to be run for one or more types of input.""" - return None - - def exists(self) -> bool: - - """Return whether the underlying player is still in the game.""" - return bool() - - def get_icon(self) -> dict[str, Any]: - - """Returns the character's icon (images, colors, etc contained - in a dict. - """ - return {'foo': 'bar'} - - def get_icon_info(self) -> dict[str, Any]: - - """(internal)""" - return {'foo': 'bar'} - - def get_v1_account_id(self) -> str: - - """Return the V1 Account ID this player is signed in under, if - there is one and it can be determined with relative certainty. - Returns None otherwise. Note that this may require an active - internet connection (especially for network-connected players) - and may return None for a short while after a player initially - joins (while verification occurs). - """ - return str() - - def getname(self, full: bool = False, icon: bool = True) -> str: - - """Returns the player's name. If icon is True, the long version of the - name may include an icon. - """ - return str() - - def remove_from_game(self) -> None: - - """Removes the player from the game.""" - return None - - def resetinput(self) -> None: - - """Clears out the player's assigned input actions.""" - return None - - def set_icon_info( - self, - texture: str, - tint_texture: str, - tint_color: Sequence[float], - tint2_color: Sequence[float], - ) -> None: - - """(internal)""" - return None - - def setactivity(self, activity: ba.Activity | None) -> None: - - """(internal)""" - return None - - def setdata( - self, - team: ba.SessionTeam, - character: str, - color: Sequence[float], - highlight: Sequence[float], - ) -> None: - - """(internal)""" - return None - - def setname( - self, name: str, full_name: str | None = None, real: bool = True - ) -> None: - - """Set the player's name to the provided string. - A number will automatically be appended if the name is not unique from - other players. - """ - return None - - def setnode(self, node: Node | None) -> None: - - """(internal)""" - return None - - -class Sound: - - """A reference to a sound. - - Category: **Asset Classes** - - Use ba.getsound() to instantiate one. - """ - - pass - - -class Texture: - - """A reference to a texture. - - Category: **Asset Classes** - - Use ba.gettexture() to instantiate one. - """ - - pass - - -class Timer: - - """Timers are used to run code at later points in time. - - Category: **General Utility Classes** - - This class encapsulates a timer in the current ba.Context. - The underlying timer will be destroyed when either this object is - no longer referenced or when its Context (Activity, etc.) dies. If you - do not want to worry about keeping a reference to your timer around, - 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) - - ###### 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 - may want to look into concepts such as ba.WeakCall if that is not - desired. - - ###### repeat - > If True, the timer will fire repeatedly, with each successive - firing having the same delay as the first. - - ###### timetype - > A ba.TimeType value determining which timeline the timer is - placed onto. - - ###### 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(): - ... ba.screenmessage('BADGER!') - ... def stop_saying_it(): - ... self.t = None - ... ba.screenmessage('MUSHROOM MUSHROOM!') - ... # 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. - ... ba.timer(3.89, stop_saying_it) - """ - - def __init__( - self, - time: float, - call: Callable[[], Any], - repeat: bool = False, - timetype: ba.TimeType = TimeType.SIM, - timeformat: ba.TimeFormat = TimeFormat.SECONDS, - suppress_format_warning: bool = False, - ): - pass - - -class Vec3(Sequence[float]): - - """A vector of 3 floats. - - Category: **General Utility Classes** - - These can be created the following ways (checked in this order): - - with no args, all values are set to 0 - - with a single numeric arg, all values are set to that value - - with a single three-member sequence arg, sequence values are copied - - otherwise assumes individual x/y/z args (positional or keywords) - """ - - x: float - - """The vector's X component.""" - - y: float - - """The vector's Y component.""" - - z: float - - """The vector's Z component.""" - - # pylint: disable=function-redefined - - @overload - def __init__(self) -> None: - pass - - @overload - def __init__(self, value: float): - pass - - @overload - def __init__(self, values: Sequence[float]): - pass - - @overload - def __init__(self, x: float, y: float, z: float): - pass - - def __init__(self, *args: Any, **kwds: Any): - pass - - def __add__(self, other: Vec3) -> Vec3: - return self - - def __sub__(self, other: Vec3) -> Vec3: - return self - - @overload - def __mul__(self, other: float) -> Vec3: - return self - - @overload - def __mul__(self, other: Sequence[float]) -> Vec3: - return self - - def __mul__(self, other: Any) -> Any: - return self - - @overload - def __rmul__(self, other: float) -> Vec3: - return self - - @overload - def __rmul__(self, other: Sequence[float]) -> Vec3: - return self - - def __rmul__(self, other: Any) -> Any: - return self - - # (for index access) - def __getitem__(self, typeargs: Any) -> Any: - return 0.0 - - def __len__(self) -> int: - return 3 - - # (for iterator access) - def __iter__(self) -> Any: - return self - - def __next__(self) -> float: - return 0.0 - - def __neg__(self) -> Vec3: - return self - - def __setitem__(self, index: int, val: float) -> None: - pass - - def cross(self, other: Vec3) -> Vec3: - - """Returns the cross product of this vector and another.""" - return Vec3() - - def dot(self, other: Vec3) -> float: - - """Returns the dot product of this vector and another.""" - return float() - - def length(self) -> float: - - """Returns the length of the vector.""" - return float() - - def normalized(self) -> Vec3: - - """Returns a normalized version of the vector.""" - return Vec3() - - -class Widget: - - """Internal type for low level UI elements; buttons, windows, etc. - - Category: **User Interface Classes** - - This class represents a weak reference to a widget object - in the internal C++ layer. Currently, functions such as - ba.buttonwidget() must be used to instantiate or edit these. - """ - - def activate(self) -> None: - - """Activates a widget; the same as if it had been clicked.""" - return None - - def add_delete_callback(self, call: Callable) -> None: - - """Add a call to be run immediately after this widget is destroyed.""" - return None - - def delete(self, ignore_missing: bool = True) -> None: - - """Delete the Widget. Ignores already-deleted Widgets if ignore_missing - is True; otherwise an Exception is thrown. - """ - return None - - def exists(self) -> bool: - - """Returns whether the Widget still exists. - Most functionality will fail on a nonexistent widget. - - Note that you can also use the boolean operator for this same - functionality, so a statement such as "if mywidget" will do - the right thing both for Widget objects and values of None. - """ - return bool() - - def get_children(self) -> list[ba.Widget]: - - """Returns any child Widgets of this Widget.""" - return [Widget()] - - def get_screen_space_center(self) -> tuple[float, float]: - - """Returns the coords of the ba.Widget center relative to the center - of the screen. This can be useful for placing pop-up windows and other - special cases. - """ - return (0.0, 0.0) - - def get_selected_child(self) -> ba.Widget | None: - - """Returns the selected child Widget or None if nothing is selected.""" - return Widget() - - def get_widget_type(self) -> str: - - """Return the internal type of the Widget as a string. Note that this - is different from the Python ba.Widget type, which is the same for - all widgets. - """ - return str() - - -def _app() -> ba.App: - - """(internal)""" - import ba # pylint: disable=cyclic-import - - return ba.App() - - -def add_clean_frame_callback(call: Callable) -> None: - - """(internal) - - Provide an object to be called once the next non-progress-bar-frame has - been rendered. Useful for queueing things to load in the background - without elongating any current progress-bar-load. - """ - return None - - -def android_get_external_files_dir() -> str: - - """(internal) - - Returns the android external storage path, or None if there is none on - this device - """ - return str() - - -def android_show_wifi_settings() -> None: - - """(internal)""" - return None - - -def app_instance_uuid() -> str: - - """(internal)""" - return str() - - -def apply_config() -> None: - - """(internal)""" - return None - - -def appname() -> str: - - """(internal)""" - return str() - - -def appnameupper() -> str: - - """(internal) - - Return whether this build of the game can display full unicode such as - Emoji, Asian languages, etc. - """ - return str() - - -def back_press() -> None: - - """(internal)""" - return None - - -def bless() -> None: - - """(internal)""" - return None - - -def buttonwidget( - edit: ba.Widget | None = None, - parent: ba.Widget | None = None, - size: Sequence[float] | None = None, - position: Sequence[float] | None = None, - on_activate_call: Callable | None = None, - label: str | ba.Lstr | None = None, - color: Sequence[float] | None = None, - down_widget: ba.Widget | None = None, - up_widget: ba.Widget | None = None, - left_widget: ba.Widget | None = None, - right_widget: ba.Widget | None = None, - texture: ba.Texture | None = None, - text_scale: float | None = None, - textcolor: Sequence[float] | None = None, - enable_sound: bool | None = None, - model_transparent: ba.Model | None = None, - model_opaque: ba.Model | None = None, - repeat: bool | None = None, - scale: float | None = None, - transition_delay: float | None = None, - on_select_call: Callable | None = None, - button_type: str | None = None, - extra_touch_border_scale: float | None = None, - selectable: bool | None = None, - show_buffer_top: float | None = None, - icon: ba.Texture | None = None, - iconscale: float | None = None, - icon_tint: float | None = None, - icon_color: Sequence[float] | None = None, - autoselect: bool | None = None, - mask_texture: ba.Texture | None = None, - tint_texture: ba.Texture | None = None, - tint_color: Sequence[float] | None = None, - tint2_color: Sequence[float] | None = None, - text_flatness: float | None = None, - text_res_scale: float | None = None, - enabled: bool | None = None, -) -> ba.Widget: - - """Create or edit a button widget. - - Category: **User Interface Functions** - - Pass a valid existing ba.Widget as 'edit' to modify it; otherwise - a new one is created and returned. Arguments that are not set to None - are applied to the Widget. - """ - import ba # pylint: disable=cyclic-import - - return ba.Widget() - - -def camerashake(intensity: float = 1.0) -> None: - - """Shake the camera. - - Category: **Gameplay Functions** - - Note that some cameras and/or platforms (such as VR) may not display - camera-shake, so do not rely on this always being visible to the - player as a gameplay cue. - """ - return None - - -def can_display_full_unicode() -> bool: - - """(internal)""" - return bool() - - -def can_show_ad() -> bool: - - """(internal)""" - return bool() - - -def capture_gamepad_input(call: Callable[[dict], None]) -> None: - - """(internal) - - Add a callable to be called for subsequent gamepad events. - The method is passed a dict containing info about the event. - """ - return None - - -def capture_keyboard_input(call: Callable[[dict], None]) -> None: - - """(internal) - - Add a callable to be called for subsequent keyboard-game-pad events. - The method is passed a dict containing info about the event. - """ - return None - - -def charstr(char_id: ba.SpecialChar) -> str: - - """Get a unicode string representing a special character. - - Category: **General Utility Functions** - - Note that these utilize the private-use block of unicode characters - (U+E000-U+F8FF) and are specific to the game; exporting or rendering - them elsewhere will be meaningless. - - See ba.SpecialChar for the list of available characters. - """ - return str() - - -def chatmessage( - message: str | ba.Lstr, - clients: Sequence[int] | None = None, - sender_override: str | None = None, -) -> None: - - """(internal)""" - return None - - -def checkboxwidget( - edit: ba.Widget | None = None, - parent: ba.Widget | None = None, - size: Sequence[float] | None = None, - position: Sequence[float] | None = None, - text: str | ba.Lstr | None = None, - value: bool | None = None, - on_value_change_call: Callable[[bool], None] | None = None, - on_select_call: Callable[[], None] | None = None, - text_scale: float | None = None, - textcolor: Sequence[float] | None = None, - scale: float | None = None, - is_radio_button: bool | None = None, - maxwidth: float | None = None, - autoselect: bool | None = None, - color: Sequence[float] | None = None, -) -> ba.Widget: - - """Create or edit a check-box widget. - - Category: **User Interface Functions** - - Pass a valid existing ba.Widget as 'edit' to modify it; otherwise - a new one is created and returned. Arguments that are not set to None - are applied to the Widget. - """ - import ba # pylint: disable=cyclic-import - - return ba.Widget() - - -def client_info_query_response(token: str, response: Any) -> None: - - """(internal)""" - return None - - -def 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: - - """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_is_supported() separately. - """ - return bool() - - -def 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: - - """Copy a string to the system clipboard. - - Category: **General Utility Functions** - - Ensure that ba.clipboard_is_supported() returns True before adding - buttons/etc. that make use of this functionality. - """ - return None - - -def columnwidget( - edit: ba.Widget | None = None, - parent: ba.Widget | None = None, - size: Sequence[float] | None = None, - position: Sequence[float] | None = None, - background: bool | None = None, - selected_child: ba.Widget | None = None, - visible_child: ba.Widget | None = None, - single_depth: bool | None = None, - print_list_exit_instructions: bool | None = None, - left_border: float | None = None, - top_border: float | None = None, - bottom_border: float | None = None, - selection_loops_to_parent: bool | None = None, - border: float | None = None, - margin: float | None = None, - claims_left_right: bool | None = None, - claims_tab: bool | None = None, -) -> ba.Widget: - - """Create or edit a column widget. - - Category: **User Interface Functions** - - Pass a valid existing ba.Widget as 'edit' to modify it; otherwise - a new one is created and returned. Arguments that are not set to None - are applied to the Widget. - """ - import ba # pylint: disable=cyclic-import - - return ba.Widget() - - -def commit_config(config: str) -> None: - - """(internal)""" - return None - - -def connect_to_party( - address: str, port: int | None = None, print_progress: bool = True -) -> None: - - """(internal)""" - return None - - -def console_print(*args: Any) -> None: - - """(internal) - - Print the provided args to the game console (using str()). - For most debugging/info purposes you should just use Python's standard - print, which will show up in the game console as well. - """ - return None - - -def containerwidget( - edit: ba.Widget | None = None, - parent: ba.Widget | None = None, - size: Sequence[float] | None = None, - position: Sequence[float] | None = None, - background: bool | None = None, - selected_child: ba.Widget | None = None, - transition: str | None = None, - cancel_button: ba.Widget | None = None, - start_button: ba.Widget | None = None, - root_selectable: bool | None = None, - on_activate_call: Callable[[], None] | None = None, - claims_left_right: bool | None = None, - claims_tab: bool | None = None, - selection_loops: bool | None = None, - selection_loops_to_parent: bool | None = None, - scale: float | None = None, - on_outside_click_call: Callable[[], None] | None = None, - single_depth: bool | None = None, - visible_child: ba.Widget | None = None, - stack_offset: Sequence[float] | None = None, - color: Sequence[float] | None = None, - on_cancel_call: Callable[[], None] | None = None, - print_list_exit_instructions: bool | None = None, - click_activate: bool | None = None, - always_highlight: bool | None = None, - selectable: bool | None = None, - scale_origin_stack_offset: Sequence[float] | None = None, - toolbar_visibility: str | None = None, - on_select_call: Callable[[], None] | None = None, - claim_outside_clicks: bool | None = None, - claims_up_down: bool | None = None, -) -> ba.Widget: - - """Create or edit a container widget. - - Category: **User Interface Functions** - - Pass a valid existing ba.Widget as 'edit' to modify it; otherwise - a new one is created and returned. Arguments that are not set to None - are applied to the Widget. - """ - import ba # pylint: disable=cyclic-import - - return ba.Widget() - - -def contains_python_dist() -> bool: - - """(internal)""" - return bool() - - -def debug_print_py_err() -> None: - - """(internal) - - Debugging func for tracking leaked Python errors in the C++ layer.. - """ - return None - - -def disconnect_client(client_id: int, ban_time: int = 300) -> bool: - - """(internal)""" - return bool() - - -def disconnect_from_host() -> None: - - """(internal) - - Category: General Utility Functions - """ - return None - - -def display_log(name: str, level: str, message: str) -> None: - - """(internal) - - Sends a log message to the in-game console and any per-platform - log destinations (Android log, etc.). This generally is not called - directly and should instead be fed Python logging output. - """ - return None - - -def do_once() -> bool: - - """Return whether this is the first time running a line of code. - - Category: **General Utility Functions** - - This is used by 'print_once()' type calls to keep from overflowing - logs. The call functions by registering the filename and line where - The call is made from. Returns True if this location has not been - registered already, and False if it has. - - ##### Example - This print will only fire for the first loop iteration: - >>> for i in range(10): - ... if ba.do_once(): - ... print('Hello once from loop!') - """ - return bool() - - -def ehv() -> None: - - """(internal)""" - return None - - -def emitfx( - position: Sequence[float], - velocity: Sequence[float] | None = None, - count: int = 10, - scale: float = 1.0, - spread: float = 1.0, - chunk_type: str = 'rock', - emit_type: str = 'chunks', - tendril_type: str = 'smoke', -) -> None: - - """Emit particles, smoke, etc. into the fx sim layer. - - Category: **Gameplay Functions** - - The fx sim layer is a secondary dynamics simulation that runs in - the background and just looks pretty; it does not affect gameplay. - Note that the actual amount emitted may vary depending on graphics - settings, exiting element counts, or other factors. - """ - return None - - -def end_host_scanning() -> None: - - """(internal) - - Category: General Utility Functions - """ - return None - - -def env() -> dict: - - """(internal) - - Returns a dict containing general info about the operating environment - such as version, platform, etc. - This info is now exposed through ba.App; refer to those docs for - info on specific elements. - """ - return dict() - - -def evaluate_lstr(value: str) -> str: - - """(internal)""" - return str() - - -def fade_screen( - to: int = 0, time: float = 0.25, endcall: Callable[[], None] | None = None -) -> None: - - """(internal) - - Fade the local game screen in our out from black over a duration of - time. if "to" is 0, the screen will fade out to black. Otherwise it - will fade in from black. If endcall is provided, it will be run after a - completely faded frame is drawn. - """ - return None - - -def focus_window() -> None: - - """(internal) - - A workaround for some unintentional backgrounding that occurs on mac - """ - return None - - -def get_appconfig_builtin_keys() -> list[str]: - - """(internal)""" - return ['blah', 'blah2'] - - -def get_appconfig_default_value(key: str) -> Any: - - """(internal)""" - return _uninferrable() - - -def get_camera_position() -> tuple[float, ...]: - - """(internal) - - WARNING: these camera controls will not apply to network clients - and may behave unpredictably in other ways. Use them only for - tinkering. - """ - return (0.0, 0.0, 0.0) - - -def get_camera_target() -> tuple[float, ...]: - - """(internal) - - WARNING: these camera controls will not apply to network clients - and may behave unpredictably in other ways. Use them only for - tinkering. - """ - return (0.0, 0.0, 0.0) - - -def get_chat_messages() -> list[str]: - - """(internal)""" - return ['blah', 'blah2'] - - -def get_client_public_device_uuid(client_id: int) -> str | None: - - """(internal) - - Category: General Utility Functions - - Return a public device UUID for a client. If the client does not - exist or is running a version older than 1.6.10, returns None. - Public device UUID uniquely identifies the device the client is - using in a semi-permanent way. The UUID value will change - periodically with updates to the game or operating system. - """ - return '' - - -def get_collision_info(*args: Any) -> Any: - - """Return collision related values - - Category: **Gameplay Functions** - - Returns a single collision value or tuple of values such as location, - depth, nodes involved, etc. Only call this in the handler of a - collision-triggered callback or message - """ - return _uninferrable() - - -def get_configurable_game_pads() -> list: - - """(internal) - - Returns a list of the currently connected gamepads that can be - configured. - """ - return list() - - -def get_connection_to_host_info() -> dict: - - """(internal)""" - return dict() - - -def get_display_resolution() -> tuple[int, int] | None: - - """(internal) - - Return the currently selected display resolution for fullscreen - display. Returns None if resolutions cannot be directly set. - """ - return (0, 0) - - -def get_foreground_host_activity() -> ba.Activity | None: - - """(internal) - - Returns the ba.Activity currently in the foreground, or None if there - is none. - """ - import ba # pylint: disable=cyclic-import - - return ba.Activity(settings={}) - - -def get_foreground_host_session() -> ba.Session | None: - - """(internal) - - Return the ba.Session currently being displayed, or None if there is - none. - """ - import ba # pylint: disable=cyclic-import - - return ba.Session([]) - - -def get_game_port() -> int: - - """(internal) - - Return the port ballistica is hosting on. - """ - return int() - - -def get_game_roster() -> list[dict[str, Any]]: - - """(internal)""" - return [{'foo': 'bar'}] - - -def get_idle_time() -> int: - - """(internal) - - Returns the amount of time since any game input has been received. - """ - return int() - - -def get_local_active_input_devices_count() -> int: - - """(internal)""" - return int() - - -def get_low_level_config_value(key: str, default_value: int) -> int: - - """(internal)""" - return int() - - -def get_max_graphics_quality() -> str: - - """(internal) - - Return the max graphics-quality supported on the current hardware. - """ - return str() - - -def get_package_collide_model( - package: ba.AssetPackage, name: str -) -> ba.CollideModel: - - """(internal)""" - import ba # pylint: disable=cyclic-import - - return ba.CollideModel() - - -def get_package_data(package: ba.AssetPackage, name: str) -> ba.Data: - - """(internal).""" - import ba # pylint: disable=cyclic-import - - return ba.Data() - - -def get_package_model(package: ba.AssetPackage, name: str) -> ba.Model: - - """(internal)""" - import ba # pylint: disable=cyclic-import - - return ba.Model() - - -def get_package_sound(package: ba.AssetPackage, name: str) -> ba.Sound: - - """(internal).""" - import ba # pylint: disable=cyclic-import - - return ba.Sound() - - -def get_package_texture(package: ba.AssetPackage, name: str) -> ba.Texture: - - """(internal)""" - import ba # pylint: disable=cyclic-import - - return ba.Texture() - - -def get_public_party_enabled() -> bool: - - """(internal)""" - return bool() - - -def get_public_party_max_size() -> int: - - """(internal)""" - return int() - - -def get_qrcode_texture(url: str) -> ba.Texture: - - """(internal)""" - import ba # pylint: disable=cyclic-import - - return ba.Texture() - - -def get_random_names() -> list: - - """(internal) - - Returns the random names used by the game. - """ - return list() - - -def get_replay_speed_exponent() -> int: - - """(internal) - - Returns current replay speed value. Actual displayed speed is pow(2,speed). - """ - return int() - - -def get_replays_dir() -> str: - - """(internal)""" - return str() - - -def get_special_widget(name: str) -> Widget: - - """(internal)""" - return Widget() - - -def get_string_height(string: str, suppress_warning: bool = False) -> float: - - """(internal) - - Given a string, returns its height using the standard small app - font. - """ - return float() - - -def get_string_width(string: str, suppress_warning: bool = False) -> float: - - """(internal) - - Given a string, returns its width using the standard small app - font. - """ - return float() - - -def get_thread_name() -> str: - - """(internal) - - Returns the name of the current thread. - This may vary depending on platform and should not be used in logic; - only for debugging. - """ - return str() - - -def get_ui_input_device() -> ba.InputDevice: - - """(internal) - - Returns the input-device that currently owns the user interface, or - None if there is none. - """ - import ba # pylint: disable=cyclic-import - - return ba.InputDevice() - - -def get_v1_cloud_log() -> str: - - """(internal)""" - return str() - - -def get_v1_cloud_log_file_path() -> str: - - """(internal) - - Return the path to the app log file. - """ - return str() - - -def get_volatile_data_directory() -> str: - - """(internal) - - Return the path to the app volatile data directory. - This directory is for data generated by the app that does not - need to be backed up and can be recreated if necessary. - """ - return str() - - -# Show that our return type varies based on "doraise" value: -@overload -def getactivity(doraise: Literal[True] = True) -> ba.Activity: - ... - - -@overload -def getactivity(doraise: Literal[False]) -> ba.Activity | None: - ... - - -def getactivity(doraise: bool = True) -> ba.Activity | None: - """Return the current ba.Activity instance. - - Category: **Gameplay Functions** - - Note that this is based on context; thus code run in a timer generated - in Activity 'foo' will properly return 'foo' here, even if another - Activity has since been created or is transitioning in. - If there is no current Activity, raises a ba.ActivityNotFoundError. - If doraise is False, None will be returned instead in that case. - """ - return None - - -def getcollidemodel(name: str) -> ba.CollideModel: - - """Return a collide-model, loading it if necessary. - - Category: **Asset Functions** - - Collide-models are used in physics calculations for such things as - terrain. - - Note that this function returns immediately even if the media has yet - to be loaded. To avoid hitches, instantiate your media objects in - advance of when you will be using them, allowing time for them to load - in the background if necessary. - """ - import ba # pylint: disable=cyclic-import - - return ba.CollideModel() - - -def getdata(name: str) -> ba.Data: - - """Return a data, loading it if necessary. - - Category: **Asset Functions** - - Note that this function returns immediately even if the media has yet - to be loaded. To avoid hitches, instantiate your media objects in - advance of when you will be using them, allowing time for them to load - in the background if necessary. - """ - import ba # pylint: disable=cyclic-import - - return ba.Data() - - -# Show that our return type varies based on "doraise" value: -@overload -def getinputdevice( - name: str, unique_id: str, doraise: Literal[True] = True -) -> ba.InputDevice: - ... - - -@overload -def getinputdevice( - name: str, unique_id: str, doraise: Literal[False] -) -> ba.InputDevice | None: - ... - - -def getinputdevice(name: str, unique_id: str, doraise: bool = True) -> Any: - """(internal) - - Given a type name and a unique identifier, returns an InputDevice. - Throws an Exception if the input-device is not found, or returns None - if 'doraise' is False. - """ - return None - - -def getmodel(name: str) -> ba.Model: - - """Return a model, loading it if necessary. - - Category: **Asset Functions** - - Note that this function returns immediately even if the media has yet - to be loaded. To avoid hitches, instantiate your media objects in - advance of when you will be using them, allowing time for them to load - in the background if necessary. - """ - import ba # pylint: disable=cyclic-import - - return ba.Model() - - -def getnodes() -> list: - - """Return all nodes in the current ba.Context. - - Category: **Gameplay Functions** - """ - return list() - - -# Show that our return type varies based on "doraise" value: -@overload -def getsession(doraise: Literal[True] = True) -> ba.Session: - ... - - -@overload -def getsession(doraise: Literal[False]) -> ba.Session | None: - ... - - -def getsession(doraise: bool = True) -> ba.Session | None: - """Category: **Gameplay Functions** - - Returns the current ba.Session instance. - Note that this is based on context; thus code being run in the UI - context will return the UI context here even if a game Session also - exists, etc. If there is no current Session, an Exception is raised, or - if doraise is False then None is returned instead. - """ - return None - - -def getsound(name: str) -> ba.Sound: - - """Return a sound, loading it if necessary. - - Category: **Asset Functions** - - Note that this function returns immediately even if the media has yet - to be loaded. To avoid hitches, instantiate your media objects in - advance of when you will be using them, allowing time for them to load - in the background if necessary. - """ - import ba # pylint: disable=cyclic-import - - return ba.Sound() - - -def gettexture(name: str) -> ba.Texture: - - """Return a texture, loading it if necessary. - - Category: **Asset Functions** - - Note that this function returns immediately even if the media has yet - to be loaded. To avoid hitches, instantiate your media objects in - advance of when you will be using them, allowing time for them to load - in the background if necessary. - """ - import ba # pylint: disable=cyclic-import - - return ba.Texture() - - -def has_gamma_control() -> bool: - - """(internal) - - Returns whether the system can adjust overall screen gamma) - """ - return bool() - - -def has_user_run_commands() -> bool: - - """(internal)""" - return bool() - - -def has_video_ads() -> bool: - - """(internal)""" - return bool() - - -def have_chars(text: str) -> bool: - - """(internal)""" - return bool() - - -def have_connected_clients() -> bool: - - """(internal) - - Category: General Utility Functions - """ - return bool() - - -def have_incentivized_ad() -> bool: - - """(internal)""" - return bool() - - -def have_permission(permission: ba.Permission) -> bool: - - """(internal)""" - return bool() - - -def have_touchscreen_input() -> bool: - - """(internal) - - Returns whether or not a touch-screen input is present - """ - return bool() - - -def host_scan_cycle() -> list: - - """(internal)""" - return list() - - -def hscrollwidget( - edit: ba.Widget | None = None, - parent: ba.Widget | None = None, - size: Sequence[float] | None = None, - position: Sequence[float] | None = None, - background: bool | None = None, - selected_child: ba.Widget | None = None, - capture_arrows: bool | None = None, - on_select_call: Callable[[], None] | None = None, - center_small_content: bool | None = None, - color: Sequence[float] | None = None, - highlight: bool | None = None, - border_opacity: float | None = None, - simple_culling_h: float | None = None, - claims_left_right: bool | None = None, - claims_up_down: bool | None = None, - claims_tab: bool | None = None, -) -> ba.Widget: - - """Create or edit a horizontal scroll widget. - - Category: **User Interface Functions** - - Pass a valid existing ba.Widget as 'edit' to modify it; otherwise - a new one is created and returned. Arguments that are not set to None - are applied to the Widget. - """ - import ba # pylint: disable=cyclic-import - - return ba.Widget() - - -def imagewidget( - edit: ba.Widget | None = None, - parent: ba.Widget | None = None, - size: Sequence[float] | None = None, - position: Sequence[float] | None = None, - color: Sequence[float] | None = None, - texture: ba.Texture | None = None, - opacity: float | None = None, - model_transparent: ba.Model | None = None, - model_opaque: ba.Model | None = None, - has_alpha_channel: bool = True, - tint_texture: ba.Texture | None = None, - tint_color: Sequence[float] | None = None, - transition_delay: float | None = None, - draw_controller: ba.Widget | None = None, - tint2_color: Sequence[float] | None = None, - tilt_scale: float | None = None, - mask_texture: ba.Texture | None = None, - radial_amount: float | None = None, -) -> ba.Widget: - - """Create or edit an image widget. - - Category: **User Interface Functions** - - Pass a valid existing ba.Widget as 'edit' to modify it; otherwise - a new one is created and returned. Arguments that are not set to None - are applied to the Widget. - """ - import ba # pylint: disable=cyclic-import - - return ba.Widget() - - -def in_logic_thread() -> bool: - - """(internal) - - Returns whether or not the current thread is the logic thread. - """ - return bool() - - -def increment_analytics_count(name: str, increment: int = 1) -> None: - - """(internal)""" - return None - - -def increment_analytics_count_raw_2( - name: str, uses_increment: bool = True, increment: int = 1 -) -> None: - - """(internal)""" - return None - - -def increment_analytics_counts_raw(name: str, increment: int = 1) -> None: - - """(internal)""" - return None - - -def is_in_replay() -> bool: - - """(internal)""" - return bool() - - -def is_log_full() -> bool: - - """(internal)""" - return bool() - - -def is_os_playing_music() -> bool: - - """(internal) - - Tells whether the OS is currently playing music of some sort. - - (Used to determine whether the game should avoid playing its own) - """ - return bool() - - -def is_party_icon_visible() -> bool: - - """(internal)""" - return bool() - - -def is_running_on_fire_tv() -> bool: - - """(internal)""" - return bool() - - -def is_running_on_ouya() -> bool: - - """(internal)""" - return bool() - - -def is_xcode_build() -> bool: - - """(internal)""" - return bool() - - -def lock_all_input() -> None: - - """(internal) - - Prevents all keyboard, mouse, and gamepad events from being processed. - """ - return None - - -def login_adapter_back_end_active_change(login_type: str, active: bool) -> None: - - """(internal)""" - return None - - -def login_adapter_get_sign_in_token(login_type: str, attempt_id: int) -> None: - - """(internal)""" - return None - - -def ls_input_devices() -> None: - - """Print debugging info about game objects. - - Category: **General Utility Functions** - - This call only functions in debug builds of the game. - It prints various info about the current object count, etc. - """ - return None - - -def ls_objects() -> None: - - """Log debugging info about C++ level objects. - - Category: **General Utility Functions** - - This call only functions in debug builds of the game. - It prints various info about the current object count, etc. - """ - return None - - -def mac_music_app_get_library_source() -> None: - - """(internal)""" - return None - - -def mac_music_app_get_playlists() -> list[str]: - - """(internal)""" - return ['blah', 'blah2'] - - -def mac_music_app_get_volume() -> int: - - """(internal)""" - return int() - - -def mac_music_app_init() -> None: - - """(internal)""" - return None - - -def mac_music_app_play_playlist(playlist: str) -> bool: - - """(internal)""" - return bool() - - -def mac_music_app_set_volume(volume: int) -> None: - - """(internal)""" - return None - - -def mac_music_app_stop() -> None: - - """(internal)""" - return None - - -def mark_log_sent() -> None: - - """(internal)""" - return None - - -def music_player_play(files: Any) -> None: - - """(internal) - - Starts internal music file playback (for internal use) - """ - return None - - -def music_player_set_volume(volume: float) -> None: - - """(internal) - - Sets internal music player volume (for internal use) - """ - return None - - -def music_player_shutdown() -> None: - - """(internal) - - Finalizes internal music file playback (for internal use) - """ - return None - - -def music_player_stop() -> None: - - """(internal) - - Stops internal music file playback (for internal use) - """ - return None - - -def new_host_session( - sessiontype: type[ba.Session], benchmark_type: str | None = None -) -> None: - - """(internal)""" - return None - - -def new_replay_session(file_name: str) -> None: - - """(internal)""" - return None - - -def newactivity( - activity_type: type[ba.Activity], settings: dict | None = None -) -> ba.Activity: - - """Instantiates a ba.Activity given a type object. - - Category: **General Utility Functions** - - Activities require special setup and thus cannot be directly - instantiated; you must go through this function. - """ - import ba # pylint: disable=cyclic-import - - return ba.Activity(settings={}) - - -def newnode( - type: str, - owner: ba.Node | None = None, - attrs: dict | None = None, - name: str | None = None, - delegate: Any = None, -) -> Node: - - """Add a node of the given type to the game. - - Category: **Gameplay Functions** - - If a dict is provided for 'attributes', the node's initial attributes - will be set based on them. - - 'name', if provided, will be stored with the node purely for debugging - purposes. If no name is provided, an automatic one will be generated - such as 'terrain@foo.py:30'. - - If 'delegate' is provided, Python messages sent to the node will go to - that object's handlemessage() method. Note that the delegate is stored - as a weak-ref, so the node itself will not keep the object alive. - - if 'owner' is provided, the node will be automatically killed when that - object dies. 'owner' can be another node or a ba.Actor - """ - return Node() - - -def open_dir_externally(path: str) -> None: - - """(internal) - - Open the provided dir in the default external app. - """ - return None - - -def open_file_externally(path: str) -> None: - - """(internal) - - Open the provided file in the default external app. - """ - return None - - -def open_url(address: str, force_internal: bool = False) -> None: - - """Open a provided URL. - - Category: **General Utility Functions** - - Open the provided url in a web-browser, or display the URL - string in a window if that isn't possible (or if force_internal - is True). - """ - return None - - -def playsound( - sound: Sound, - volume: float = 1.0, - position: Sequence[float] | None = None, - host_only: bool = False, -) -> None: - - """Play a ba.Sound a single time. - - Category: **Gameplay Functions** - - If position is not provided, the sound will be at a constant volume - everywhere. Position should be a float tuple of size 3. - """ - return None - - -def print_context() -> None: - - """(internal) - - Prints info about the current context state; for debugging. - """ - return None - - -def print_load_info() -> None: - - """(internal) - - Category: **General Utility Functions** - """ - return None - - -def printnodes() -> None: - - """Print various info about existing nodes; useful for debugging. - - Category: **Gameplay Functions** - """ - return None - - -def pushcall( - call: Callable, - from_other_thread: bool = False, - suppress_other_thread_warning: bool = False, - other_thread_use_fg_context: bool = False, - raw: bool = False, -) -> None: - - """Push a call to the logic event-loop. - Category: **General Utility Functions** - - This call expects to be used in the logic thread, and will automatically - save and restore the ba.Context to behave seamlessly. - - If you want to push a call from outside of the logic thread, - however, you can pass 'from_other_thread' as True. In this case - the call will always run in the UI context on the logic thread - or whichever context is in the foreground if - other_thread_use_fg_context is True. - Passing raw=True will disable thread checks and context sets/restores. - """ - return None - - -def quit(soft: bool = False, back: bool = False) -> None: - - """Quit the game. - - Category: **General Utility Functions** - - On systems like android, 'soft' will end the activity but keep the - app running. - """ - return None - - -def register_activity(activity: ba.Activity) -> ActivityData: - - """(internal)""" - return ActivityData() - - -def register_session(session: ba.Session) -> SessionData: - - """(internal)""" - return SessionData() - - -def release_gamepad_input() -> None: - - """(internal) - - Resumes normal gamepad event processing. - """ - return None - - -def release_keyboard_input() -> None: - - """(internal) - - Resumes normal keyboard event processing. - """ - return None - - -def reload_media() -> None: - - """(internal) - - Reload all currently loaded game media; useful for - development/debugging. - """ - return None - - -def request_permission(permission: ba.Permission) -> None: - - """(internal)""" - return None - - -def reset_game_activity_tracking() -> None: - - """(internal)""" - return None - - -def reset_random_player_names() -> None: - - """(internal)""" - return None - - -def resolve_appconfig_value(key: str) -> Any: - - """(internal)""" - return _uninferrable() - - -def rowwidget( - edit: ba.Widget | None = None, - parent: ba.Widget | None = None, - size: Sequence[float] | None = None, - position: Sequence[float] | None = None, - background: bool | None = None, - selected_child: ba.Widget | None = None, - visible_child: ba.Widget | None = None, - claims_left_right: bool | None = None, - claims_tab: bool | None = None, - selection_loops_to_parent: bool | None = None, -) -> ba.Widget: - - """Create or edit a row widget. - - Category: **User Interface Functions** - - Pass a valid existing ba.Widget as 'edit' to modify it; otherwise - a new one is created and returned. Arguments that are not set to None - are applied to the Widget. - """ - import ba # pylint: disable=cyclic-import - - return ba.Widget() - - -def safecolor( - color: Sequence[float], target_intensity: float = 0.6 -) -> tuple[float, ...]: - - """Given a color tuple, return a color safe to display as text. - - Category: **General Utility Functions** - - Accepts tuples of length 3 or 4. This will slightly brighten very - dark colors, etc. - """ - return (0.0, 0.0, 0.0) - - -def screenmessage( - message: str | ba.Lstr, - color: Sequence[float] | None = None, - top: bool = False, - image: dict[str, Any] | None = None, - log: bool = False, - clients: Sequence[int] | None = None, - transient: bool = False, -) -> None: - - """Print a message to the local client's screen, in a given color. - - Category: **General Utility Functions** - - If 'top' is True, the message will go to the top message area. - For 'top' messages, 'image' must be a dict containing 'texture' - and 'tint_texture' textures and 'tint_color' and 'tint2_color' - colors. This defines an icon to display alongside the message. - If 'log' is True, the message will also be submitted to the log. - 'clients' can be a list of client-ids the message should be sent - to, or None to specify that everyone should receive it. - If 'transient' is True, the message will not be included in the - game-stream and thus will not show up when viewing replays. - Currently the 'clients' option only works for transient messages. - """ - return None - - -def scrollwidget( - edit: ba.Widget | None = None, - parent: ba.Widget | None = None, - size: Sequence[float] | None = None, - position: Sequence[float] | None = None, - background: bool | None = None, - selected_child: ba.Widget | None = None, - capture_arrows: bool = False, - on_select_call: Callable | None = None, - center_small_content: bool | None = None, - color: Sequence[float] | None = None, - highlight: bool | None = None, - border_opacity: float | None = None, - simple_culling_v: float | None = None, - selection_loops_to_parent: bool | None = None, - claims_left_right: bool | None = None, - claims_up_down: bool | None = None, - claims_tab: bool | None = None, - autoselect: bool | None = None, -) -> ba.Widget: - - """Create or edit a scroll widget. - - Category: **User Interface Functions** - - Pass a valid existing ba.Widget as 'edit' to modify it; otherwise - a new one is created and returned. Arguments that are not set to None - are applied to the Widget. - """ - import ba # pylint: disable=cyclic-import - - return ba.Widget() - - -def set_admins(admins: list[str]) -> None: - - """(internal)""" - return None - - -def set_analytics_screen(screen: str) -> None: - - """Used for analytics to see where in the app players spend their time. - - Category: **General Utility Functions** - - Generally called when opening a new window or entering some UI. - 'screen' should be a string description of an app location - ('Main Menu', etc.) - """ - return None - - -def set_authenticate_clients(enable: bool) -> None: - - """(internal)""" - return None - - -def set_camera_manual(value: bool) -> None: - - """(internal) - - WARNING: these camera controls will not apply to network clients - and may behave unpredictably in other ways. Use them only for - tinkering. - """ - return None - - -def set_camera_position(x: float, y: float, z: float) -> None: - - """(internal) - - WARNING: these camera controls will not apply to network clients - and may behave unpredictably in other ways. Use them only for - tinkering. - """ - return None - - -def set_camera_target(x: float, y: float, z: float) -> None: - - """(internal) - - WARNING: these camera controls will not apply to network clients - and may behave unpredictably in other ways. Use them only for - tinkering. - """ - return None - - -def set_debug_speed_exponent(speed: int) -> None: - - """(internal) - - Sets the debug speed scale for the game. Actual speed is pow(2,speed). - """ - return None - - -def set_enable_default_kick_voting(enable: bool) -> None: - - """(internal)""" - return None - - -def set_internal_language_keys( - listobj: list[tuple[str, str]], random_names_list: list[tuple[str, str]] -) -> None: - - """(internal)""" - return None - - -def set_low_level_config_value(key: str, value: int) -> None: - - """(internal)""" - return None - - -def set_map_bounds( - bounds: tuple[float, float, float, float, float, float] -) -> None: - - """(internal) - - Set map bounds. Generally nodes that go outside of this box are killed. - """ - return None - - -def set_master_server_source(source: int) -> None: - - """(internal)""" - return None - - -def set_party_icon_always_visible(value: bool) -> None: - - """(internal)""" - return None - - -def set_party_window_open(value: bool) -> None: - - """(internal)""" - return None - - -def set_platform_misc_read_vals(mode: str) -> None: - - """(internal)""" - return None - - -def set_public_party_enabled(enabled: bool) -> None: - - """(internal)""" - return None - - -def set_public_party_max_size(max_size: int) -> None: - - """(internal)""" - return None - - -def set_public_party_name(name: str) -> None: - - """(internal)""" - return None - - -def set_public_party_queue_enabled(max_size: bool) -> None: - - """(internal)""" - return None - - -def set_public_party_stats_url(url: str | None) -> None: - - """(internal)""" - return None - - -def set_replay_speed_exponent(speed: int) -> None: - - """(internal) - - Set replay speed. Actual displayed speed is pow(2, speed). - """ - return None - - -def set_stress_testing(testing: bool, player_count: int) -> None: - - """(internal)""" - return None - - -def set_telnet_access_enabled(enable: bool) -> None: - - """(internal)""" - return None - - -def set_thread_name(name: str) -> None: - - """(internal) - - Sets the name of the current thread (on platforms where this is - available). Thread names are only for debugging and should not be - used in logic, as naming behavior can vary across platforms. - """ - return None - - -def set_touchscreen_editing(editing: bool) -> None: - - """(internal)""" - return None - - -def set_ui_input_device(input_device: ba.InputDevice | None) -> None: - - """(internal) - - Sets the input-device that currently owns the user interface. - """ - return None - - -def setup_sigint() -> None: - - """(internal)""" - return None - - -def show_ad( - purpose: str, on_completion_call: Callable[[], None] | None = None -) -> None: - - """(internal)""" - return None - - -def show_ad_2( - purpose: str, on_completion_call: Callable[[bool], None] | None = None -) -> None: - - """(internal)""" - return None - - -def show_app_invite( - title: str | ba.Lstr, message: str | ba.Lstr, code: str -) -> None: - - """(internal) - - Category: **General Utility Functions** - """ - return None - - -def show_online_score_ui( - show: str = 'general', - game: str | None = None, - game_version: str | None = None, -) -> None: - - """(internal)""" - return None - - -def show_progress_bar() -> None: - - """(internal) - - Category: **General Utility Functions** - """ - return None - - -def submit_analytics_counts() -> None: - - """(internal)""" - return None - - -def textwidget( - edit: ba.Widget | None = None, - parent: ba.Widget | None = None, - size: Sequence[float] | None = None, - position: Sequence[float] | None = None, - text: str | ba.Lstr | None = None, - v_align: str | None = None, - h_align: str | None = None, - editable: bool | None = None, - padding: float | None = None, - on_return_press_call: Callable[[], None] | None = None, - on_activate_call: Callable[[], None] | None = None, - selectable: bool | None = None, - query: ba.Widget | None = None, - max_chars: int | None = None, - color: Sequence[float] | None = None, - click_activate: bool | None = None, - on_select_call: Callable[[], None] | None = None, - always_highlight: bool | None = None, - draw_controller: ba.Widget | None = None, - scale: float | None = None, - corner_scale: float | None = None, - description: str | ba.Lstr | None = None, - transition_delay: float | None = None, - maxwidth: float | None = None, - max_height: float | None = None, - flatness: float | None = None, - shadow: float | None = None, - autoselect: bool | None = None, - rotate: float | None = None, - enabled: bool | None = None, - force_internal_editing: bool | None = None, - always_show_carat: bool | None = None, - big: bool | None = None, - extra_touch_border_scale: float | None = None, - res_scale: float | None = None, -) -> Widget: - - """Create or edit a text widget. - - Category: **User Interface Functions** - - Pass a valid existing ba.Widget as 'edit' to modify it; otherwise - a new one is created and returned. Arguments that are not set to None - are applied to the Widget. - """ - return Widget() - - -# Overloads to return a type based on requested format. - - -@overload -def time( - timetype: ba.TimeType = TimeType.SIM, - timeformat: Literal[TimeFormat.SECONDS] = TimeFormat.SECONDS, -) -> float: - ... - - -# This "*" keyword-only hack lets us accept 1 arg (timeformat=MILLISECS) forms. -@overload -def time( - timetype: ba.TimeType = TimeType.SIM, - *, - timeformat: Literal[TimeFormat.MILLISECONDS], -) -> int: - ... - - -@overload -def time( - timetype: ba.TimeType, timeformat: Literal[TimeFormat.MILLISECONDS] -) -> int: - ... - - -def time( - timetype: ba.TimeType = TimeType.SIM, - timeformat: ba.TimeFormat = TimeFormat.SECONDS, -) -> Any: - - """Return the current time. - - Category: **General Utility Functions** - - The time returned depends on the current ba.Context and timetype. - - timetype can be either SIM, BASE, or REAL. It defaults to - SIM. Types are explained below: - - - 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 which returns float seconds, - but it can also be MILLISECONDS to return integer milliseconds. - - Note: If you need pure unfiltered clock time, just use the standard - Python functions such as time.time(). - """ - return None - - -def time_format_check(time_format: ba.TimeFormat, length: float | int) -> None: - - """(internal) - - Logs suspicious time values for timers or animate calls. - - (for helping with the transition from milliseconds-based time calls - to seconds-based ones) - """ - return None - - -def timer( - time: float, - call: Callable[[], Any], - repeat: bool = False, - timetype: ba.TimeType = TimeType.SIM, - timeformat: ba.TimeFormat = TimeFormat.SECONDS, - suppress_format_warning: bool = False, -) -> None: - - """Schedule a call to run at a later point in time. - - Category: **General Utility Functions** - - This function adds a timer to the current ba.Context. - This timer cannot be canceled or modified once created. If you - require the ability to do so, use the ba.Timer class instead. - - ##### Arguments - ###### time (float) - > 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) - - ###### call (Callable[[], Any]) - > A callable Python object. Note that the timer will retain a - strong reference to the callable for as long as it exists, so you - may want to look into concepts such as ba.WeakCall if that is not - desired. - - ###### repeat (bool) - > If True, the timer will fire repeatedly, with each successive - firing having the same delay as the first. - - ###### timetype (ba.TimeType) - > Can be either `SIM`, `BASE`, or `REAL`. It defaults to - `SIM`. - - ###### timeformat (ba.TimeFormat) - > Defaults to seconds but can also be milliseconds. - - - 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. - - ##### Examples - Print some stuff through time: - >>> ba.screenmessage('hello from now!') - >>> ba.timer(1.0, ba.Call(ba.screenmessage, 'hello from the future!')) - >>> ba.timer(2.0, ba.Call(ba.screenmessage, - ... 'hello from the future 2!')) - """ - return None - - -def uibounds() -> tuple[float, float, float, float]: - - """(internal) - - Returns a tuple of 4 values: (x-min, x-max, y-min, y-max) representing - the range of values that can be plugged into a root level - ba.ContainerWidget's stack_offset value while guaranteeing that its - center remains onscreen. - """ - return (0.0, 0.0, 0.0, 0.0) - - -def unlock_all_input() -> None: - - """(internal) - - Resumes normal keyboard, mouse, and gamepad event processing. - """ - return None - - -def user_ran_commands() -> None: - - """(internal)""" - return None - - -def v1_cloud_log(message: str) -> None: - - """(internal) - - Push messages to the old v1 cloud log. - """ - return None - - -def value_test( - arg: str, change: float | None = None, absolute: float | None = None -) -> float: - - """(internal)""" - return float() - - -def widget( - edit: ba.Widget | None = None, - up_widget: ba.Widget | None = None, - down_widget: ba.Widget | None = None, - left_widget: ba.Widget | None = None, - right_widget: ba.Widget | None = None, - show_buffer_top: float | None = None, - show_buffer_bottom: float | None = None, - show_buffer_left: float | None = None, - show_buffer_right: float | None = None, - autoselect: bool | None = None, -) -> None: - - """Edit common attributes of any widget. - - Category: **User Interface Functions** - - Unlike other UI calls, this can only be used to edit, not to create. - """ - return None - - -def workspaces_in_use() -> bool: - - """(internal) - - Returns whether workspaces functionality has been enabled at - any point this run. - """ - return bool() diff --git a/assets/src/ba_data/python/_bainternal.py b/assets/src/ba_data/python/_bainternal.py deleted file mode 100644 index 99fd8ee4..00000000 --- a/assets/src/ba_data/python/_bainternal.py +++ /dev/null @@ -1,277 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""A dummy stub module for the real _bainternal. - -The real _bainternal is a compiled extension module and only available -in the live engine. This dummy-module allows Pylint/Mypy/etc. to -function reasonably well outside of that environment. - -Make sure this file is never included in dirs seen by the engine! - -In the future perhaps this can be a stub (.pyi) file, but we will need -to make sure that it works with all our tools (mypy, pylint, pycharm). - -NOTE: This file was autogenerated by batools.dummymodule; do not edit by hand. -""" - -# I'm sorry Pylint. I know this file saddens you. Be strong. -# pylint: disable=useless-suppression -# pylint: disable=unnecessary-pass -# pylint: disable=use-dict-literal -# pylint: disable=use-list-literal -# pylint: disable=unused-argument -# pylint: disable=missing-docstring -# pylint: disable=too-many-locals -# pylint: disable=redefined-builtin -# pylint: disable=too-many-lines -# pylint: disable=redefined-outer-name -# pylint: disable=invalid-name -# pylint: disable=no-value-for-parameter - -from __future__ import annotations - -from typing import TYPE_CHECKING, TypeVar - -if TYPE_CHECKING: - from typing import Any, Callable - - -_T = TypeVar('_T') - - -def _uninferrable() -> Any: - """Get an "Any" in mypy and "uninferrable" in Pylint.""" - # pylint: disable=undefined-variable - return _not_a_real_variable # type: ignore - - -def add_transaction( - transaction: dict, callback: Callable | None = None -) -> None: - - """(internal)""" - return None - - -def game_service_has_leaderboard(game: str, config: str) -> bool: - - """(internal) - - Given a game and config string, returns whether there is a leaderboard - for it on the game service. - """ - return bool() - - -def get_master_server_address(source: int = -1, version: int = 1) -> str: - - """(internal) - - Return the address of the master server. - """ - return str() - - -def get_news_show() -> str: - - """(internal)""" - return str() - - -def get_price(item: str) -> str | None: - - """(internal)""" - return '' - - -def get_public_login_id() -> str | None: - - """(internal)""" - return '' - - -def get_purchased(item: str) -> bool: - - """(internal)""" - return bool() - - -def get_purchases_state() -> int: - - """(internal)""" - return int() - - -def get_v1_account_display_string(full: bool = True) -> str: - - """(internal)""" - return str() - - -def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any: - - """(internal)""" - return _uninferrable() - - -def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any: - - """(internal)""" - return _uninferrable() - - -def get_v1_account_misc_val(name: str, default_value: Any) -> Any: - - """(internal)""" - return _uninferrable() - - -def get_v1_account_name() -> str: - - """(internal)""" - return str() - - -def get_v1_account_state() -> str: - - """(internal)""" - return str() - - -def get_v1_account_state_num() -> int: - - """(internal)""" - return int() - - -def get_v1_account_ticket_count() -> int: - - """(internal) - - Returns the number of tickets for the current account. - """ - return int() - - -def get_v1_account_type() -> str: - - """(internal)""" - return str() - - -def get_v2_fleet() -> str: - - """(internal)""" - return str() - - -def have_outstanding_transactions() -> bool: - - """(internal)""" - return bool() - - -def in_game_purchase(item: str, price: int) -> None: - - """(internal)""" - return None - - -def is_blessed() -> bool: - - """(internal)""" - return bool() - - -def mark_config_dirty() -> None: - - """(internal) - - Category: General Utility Functions - """ - return None - - -def power_ranking_query(callback: Callable, season: Any = None) -> None: - - """(internal)""" - return None - - -def purchase(item: str) -> None: - - """(internal)""" - return None - - -def report_achievement(achievement: str, pass_to_account: bool = True) -> None: - - """(internal)""" - return None - - -def reset_achievements() -> None: - - """(internal)""" - return None - - -def restore_purchases() -> None: - - """(internal)""" - return None - - -def run_transactions() -> None: - - """(internal)""" - return None - - -def sign_in_v1(account_type: str) -> None: - - """(internal) - - Category: General Utility Functions - """ - return None - - -def sign_out_v1(v2_embedded: bool = False) -> None: - - """(internal) - - Category: General Utility Functions - """ - return None - - -def submit_score( - game: str, - config: str, - name: Any, - score: int | None, - callback: Callable, - order: str = 'increasing', - tournament_id: str | None = None, - score_type: str = 'points', - campaign: str | None = None, - level: str | None = None, -) -> None: - - """(internal) - - Submit a score to the server; callback will be called with the results. - As a courtesy, please don't send fake scores to the server. I'd prefer - to devote my time to improving the game instead of trying to make the - score server more mischief-proof. - """ - return None - - -def tournament_query( - callback: Callable[[dict | None], None], args: dict -) -> None: - - """(internal)""" - return None diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py deleted file mode 100644 index 54be9f96..00000000 --- a/assets/src/ba_data/python/ba/__init__.py +++ /dev/null @@ -1,396 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""The public face of Ballistica. - -This top level module is a collection of most commonly used functionality. -For many modding purposes, the bits exposed here are all you'll need. -In some specific cases you may need to pull in individual submodules instead. -""" -# 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, - newactivity, - newnode, - playsound, - printnodes, - ls_objects, - ls_input_devices, - 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, - getdata, - in_logic_thread, -) -from ba._accountv2 import AccountV2Handle -from ba._activity import Activity -from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem -from ba._actor import Actor -from ba._player import PlayerInfo, Player, EmptyPlayer, StandLocation -from ba._nodeactor import NodeActor -from ba._app import App -from ba._cloud import CloudSubsystem -from ba._coopgame import CoopGameActivity -from ba._coopsession import CoopSession -from ba._dependency import ( - Dependency, - DependencyComponent, - DependencySet, - AssetPackage, -) -from ba._generated.enums import ( - TimeType, - Permission, - TimeFormat, - SpecialChar, - InputType, - UIScale, -) -from ba._error import ( - print_exception, - print_error, - ContextError, - NotFoundError, - PlayerNotFoundError, - SessionPlayerNotFoundError, - NodeNotFoundError, - ActorNotFoundError, - InputDeviceNotFoundError, - WidgetNotFoundError, - ActivityNotFoundError, - TeamNotFoundError, - MapNotFoundError, - SessionTeamNotFoundError, - SessionNotFoundError, - DelegateNotFoundError, - DependencyError, -) -from ba._freeforallsession import FreeForAllSession -from ba._gameactivity import GameActivity -from ba._gameresults import GameResults -from ba._settings import ( - Setting, - IntSetting, - FloatSetting, - ChoiceSetting, - BoolSetting, - IntChoiceSetting, - FloatChoiceSetting, -) -from ba._language import Lstr, LanguageSubsystem -from ba._map import Map, getmaps -from ba._session import Session -from ba._ui import UISubsystem -from ba._servermode import ServerController -from ba._score import ScoreType, ScoreConfig -from ba._stats import PlayerScoredMessage, PlayerRecord, Stats -from ba._team import SessionTeam, Team, EmptyTeam -from ba._teamgame import TeamGameActivity -from ba._dualteamsession import DualTeamSession -from ba._achievement import Achievement, AchievementSubsystem -from ba._appconfig import AppConfig -from ba._appdelegate import AppDelegate -from ba._apputils import is_browser_likely_available, garbage_collect -from ba._campaign import Campaign -from ba._gameutils import ( - GameTip, - animate, - animate_array, - show_damage_count, - timestring, - cameraflash, -) -from ba._general import ( - WeakCall, - Call, - existing, - Existable, - verify_object_death, - storagename, - getclass, -) -from ba._keyboard import Keyboard -from ba._level import Level -from ba._lobby import Lobby, Chooser -from ba._math import normalized_color, is_point_in_box, vec3validate -from ba._meta import MetadataSubsystem -from ba._messages import ( - UNHANDLED, - OutOfBoundsMessage, - DeathType, - DieMessage, - PlayerDiedMessage, - StandMessage, - PickUpMessage, - DropMessage, - PickedUpMessage, - DroppedMessage, - ShouldShatterMessage, - ImpactDamageMessage, - FreezeMessage, - ThawMessage, - HitMessage, - CelebrateMessage, -) -from ba._music import ( - setmusic, - MusicPlayer, - MusicType, - MusicPlayMode, - MusicSubsystem, -) -from ba._powerup import PowerupMessage, PowerupAcceptMessage -from ba._multiteamsession import MultiTeamSession -from ba.ui import Window, UIController, uicleanupcheck -from ba._collision import Collision, getcollision - -app: App - -__all__ = [ - 'AccountV2Handle', - 'Achievement', - 'AchievementSubsystem', - 'Activity', - 'ActivityNotFoundError', - 'Actor', - 'ActorNotFoundError', - 'animate', - 'animate_array', - 'app', - 'App', - 'AppConfig', - 'AppDelegate', - 'AssetPackage', - 'BoolSetting', - 'buttonwidget', - 'Call', - 'cameraflash', - 'camerashake', - 'Campaign', - 'CelebrateMessage', - 'charstr', - 'checkboxwidget', - 'ChoiceSetting', - 'Chooser', - 'clipboard_get_text', - 'clipboard_has_text', - 'clipboard_is_supported', - 'clipboard_set_text', - 'CollideModel', - 'Collision', - 'columnwidget', - 'containerwidget', - 'Context', - 'ContextCall', - 'ContextError', - 'CloudSubsystem', - 'CoopGameActivity', - 'CoopSession', - 'Data', - 'DeathType', - 'DelegateNotFoundError', - 'Dependency', - 'DependencyComponent', - 'DependencyError', - 'DependencySet', - 'DieMessage', - 'do_once', - 'DropMessage', - 'DroppedMessage', - 'DualTeamSession', - 'emitfx', - 'EmptyPlayer', - 'EmptyTeam', - 'Existable', - 'existing', - 'FloatChoiceSetting', - 'FloatSetting', - 'FreeForAllSession', - 'FreezeMessage', - 'GameActivity', - 'GameResults', - 'GameTip', - 'garbage_collect', - 'getactivity', - 'getclass', - 'getcollidemodel', - 'getcollision', - 'getdata', - 'getmaps', - 'getmodel', - 'getnodes', - 'getsession', - 'getsound', - 'gettexture', - 'HitMessage', - 'hscrollwidget', - 'imagewidget', - 'ImpactDamageMessage', - 'in_logic_thread', - 'InputDevice', - 'InputDeviceNotFoundError', - 'InputType', - 'IntChoiceSetting', - 'IntSetting', - 'is_browser_likely_available', - 'is_point_in_box', - 'Keyboard', - 'LanguageSubsystem', - 'Level', - 'Lobby', - 'Lstr', - 'Map', - 'MapNotFoundError', - 'Material', - 'MetadataSubsystem', - 'Model', - 'MultiTeamSession', - 'MusicPlayer', - 'MusicPlayMode', - 'MusicSubsystem', - 'MusicType', - 'newactivity', - 'newnode', - 'Node', - 'NodeActor', - 'NodeNotFoundError', - 'normalized_color', - 'NotFoundError', - 'open_url', - 'OutOfBoundsMessage', - 'Permission', - 'PickedUpMessage', - 'PickUpMessage', - 'Player', - 'PlayerDiedMessage', - 'PlayerInfo', - 'PlayerNotFoundError', - 'PlayerRecord', - 'PlayerScoredMessage', - 'playsound', - 'Plugin', - 'PluginSubsystem', - 'PotentialPlugin', - 'PowerupAcceptMessage', - 'PowerupMessage', - 'print_error', - 'print_exception', - 'printnodes', - 'ls_objects', - 'ls_input_devices', - 'pushcall', - 'quit', - 'rowwidget', - 'safecolor', - 'ScoreConfig', - 'ScoreType', - 'screenmessage', - 'scrollwidget', - 'ServerController', - 'Session', - 'SessionNotFoundError', - 'SessionPlayer', - 'SessionPlayerNotFoundError', - 'SessionTeam', - 'SessionTeamNotFoundError', - 'set_analytics_screen', - 'setmusic', - 'Setting', - 'ShouldShatterMessage', - 'show_damage_count', - 'Sound', - 'SpecialChar', - 'StandLocation', - 'StandMessage', - 'Stats', - 'storagename', - 'Team', - 'TeamGameActivity', - 'TeamNotFoundError', - 'Texture', - 'textwidget', - 'ThawMessage', - 'time', - 'TimeFormat', - 'Timer', - 'timer', - 'timestring', - 'TimeType', - 'uicleanupcheck', - 'UIController', - 'UIScale', - 'UISubsystem', - 'UNHANDLED', - 'Vec3', - 'vec3validate', - 'verify_object_death', - 'WeakCall', - 'Widget', - 'widget', - 'WidgetNotFoundError', - 'Window', -] - - -# Have these things present themselves cleanly as 'ba.Foo' -# instead of 'ba._submodule.Foo' -def _simplify_module_names() -> None: - import os - - # Though pdoc gets confused when we override __module__, - # so let's make an exception for it. - if os.environ.get('BA_DOCS_GENERATION', '0') != '1': - from efro.util import set_canonical_module - - globs = globals() - set_canonical_module( - module_globals=globs, - names=[n for n in globs.keys() if not n.startswith('_')], - ) - - -_simplify_module_names() -del _simplify_module_names diff --git a/assets/src/ba_data/python/ba/_analytics.py b/assets/src/ba_data/python/ba/_analytics.py deleted file mode 100644 index a3c25e81..00000000 --- a/assets/src/ba_data/python/ba/_analytics.py +++ /dev/null @@ -1,83 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Functionality related to analytics.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import _ba - -if TYPE_CHECKING: - pass - - -def game_begin_analytics() -> None: - """Update analytics events for the start of a game.""" - # pylint: disable=too-many-branches - # pylint: disable=cyclic-import - from ba._dualteamsession import DualTeamSession - from ba._freeforallsession import FreeForAllSession - from ba._coopsession import CoopSession - from ba._gameactivity import GameActivity - - activity = _ba.getactivity(False) - session = _ba.getsession(False) - - # Fail gracefully if we didn't cleanly get a session and game activity. - if not activity or not session or not isinstance(activity, GameActivity): - return - - if isinstance(session, CoopSession): - campaign = session.campaign - assert campaign is not None - _ba.set_analytics_screen( - 'Coop Game: ' - + campaign.name - + ' ' - + campaign.getlevel(_ba.app.coop_session_args['level']).name - ) - _ba.increment_analytics_count('Co-op round start') - if len(activity.players) == 1: - _ba.increment_analytics_count('Co-op round start 1 human player') - elif len(activity.players) == 2: - _ba.increment_analytics_count('Co-op round start 2 human players') - elif len(activity.players) == 3: - _ba.increment_analytics_count('Co-op round start 3 human players') - elif len(activity.players) >= 4: - _ba.increment_analytics_count('Co-op round start 4+ human players') - - elif isinstance(session, DualTeamSession): - _ba.set_analytics_screen('Teams Game: ' + activity.getname()) - _ba.increment_analytics_count('Teams round start') - if len(activity.players) == 1: - _ba.increment_analytics_count('Teams round start 1 human player') - elif 1 < len(activity.players) < 8: - _ba.increment_analytics_count( - 'Teams round start ' - + str(len(activity.players)) - + ' human players' - ) - elif len(activity.players) >= 8: - _ba.increment_analytics_count('Teams round start 8+ human players') - - elif isinstance(session, FreeForAllSession): - _ba.set_analytics_screen('FreeForAll Game: ' + activity.getname()) - _ba.increment_analytics_count('Free-for-all round start') - if len(activity.players) == 1: - _ba.increment_analytics_count( - 'Free-for-all round start 1 human player' - ) - elif 1 < len(activity.players) < 8: - _ba.increment_analytics_count( - 'Free-for-all round start ' - + str(len(activity.players)) - + ' human players' - ) - elif len(activity.players) >= 8: - _ba.increment_analytics_count( - 'Free-for-all round start 8+ human players' - ) - - # For some analytics tracking on the c layer. - _ba.reset_game_activity_tracking() diff --git a/assets/src/ba_data/python/ba/_app.py b/assets/src/ba_data/python/ba/_app.py deleted file mode 100644 index ddc18131..00000000 --- a/assets/src/ba_data/python/ba/_app.py +++ /dev/null @@ -1,769 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Functionality related to the high level state of the app.""" -from __future__ import annotations - -import random -import logging -from enum import Enum -from typing import TYPE_CHECKING -from concurrent.futures import ThreadPoolExecutor - -import _ba -from ba._music import MusicSubsystem -from ba._language import LanguageSubsystem -from ba._ui import UISubsystem -from ba._achievement import AchievementSubsystem -from ba._plugin import PluginSubsystem -from ba._accountv1 import AccountV1Subsystem -from ba._meta import MetadataSubsystem -from ba._ads import AdsSubsystem -from ba._net import NetworkSubsystem -from ba._workspace import WorkspaceSubsystem -from ba._appcomponent import AppComponentSubsystem -from ba import _internal - -if TYPE_CHECKING: - import asyncio - from typing import Any, Callable - - import efro.log - import ba - from ba._cloud import CloudSubsystem - from bastd.actor import spazappearance - from ba._accountv2 import AccountV2Subsystem - from ba._level import Level - from ba._apputils import AppHealthMonitor - - -class App: - """A class for high level app functionality and state. - - Category: **App Classes** - - Use ba.app to access the single shared instance of this class. - - Note that properties not documented here should be considered internal - and subject to change without warning. - """ - - # pylint: disable=too-many-public-methods - - # Implementations for these will be filled in by internal libs. - accounts_v2: AccountV2Subsystem - cloud: CloudSubsystem - - log_handler: efro.log.LogHandler - health_monitor: AppHealthMonitor - - class State(Enum): - """High level state the app can be in.""" - - # The launch process has not yet begun. - INITIAL = 0 - - # Our app subsystems are being inited but should not yet interact. - LAUNCHING = 1 - - # App subsystems are inited and interacting, but the app has not - # yet embarked on a high level course of action. It is doing initial - # account logins, workspace & asset downloads, etc. in order to - # prepare for this. - LOADING = 2 - - # All pieces are in place and the app is now doing its thing. - RUNNING = 3 - - # The app is backgrounded or otherwise suspended. - PAUSED = 4 - - # The app is shutting down. - SHUTTING_DOWN = 5 - - @property - def aioloop(self) -> asyncio.AbstractEventLoop: - """The Logic Thread's Asyncio Event Loop. - - This allow async tasks to be run in the logic thread. - Note that, at this time, the asyncio loop is encapsulated - and explicitly stepped by the engine's logic thread loop and - thus things like asyncio.get_running_loop() will not return this - loop from most places in the logic thread; only from within a - task explicitly created in this loop. - """ - assert self._aioloop is not None - return self._aioloop - - @property - def build_number(self) -> int: - """Integer build number. - - This value increases by at least 1 with each release of the game. - It is independent of the human readable ba.App.version string. - """ - assert isinstance(self._env['build_number'], int) - return self._env['build_number'] - - @property - def device_name(self) -> str: - """Name of the device running the game.""" - assert isinstance(self._env['device_name'], str) - return self._env['device_name'] - - @property - def config_file_path(self) -> str: - """Where the game's config file is stored on disk.""" - assert isinstance(self._env['config_file_path'], str) - return self._env['config_file_path'] - - @property - def user_agent_string(self) -> str: - """String containing various bits of info about OS/device/etc.""" - assert isinstance(self._env['user_agent_string'], str) - return self._env['user_agent_string'] - - @property - def version(self) -> str: - """Human-readable version string; something like '1.3.24'. - - This should not be interpreted as a number; it may contain - string elements such as 'alpha', 'beta', 'test', etc. - If a numeric version is needed, use 'ba.App.build_number'. - """ - assert isinstance(self._env['version'], str) - return self._env['version'] - - @property - def debug_build(self) -> bool: - """Whether the app was compiled in debug mode. - - Debug builds generally run substantially slower than non-debug - builds due to compiler optimizations being disabled and extra - checks being run. - """ - assert isinstance(self._env['debug_build'], bool) - return self._env['debug_build'] - - @property - def test_build(self) -> bool: - """Whether the game was compiled in test mode. - - Test mode enables extra checks and features that are useful for - release testing but which do not slow the game down significantly. - """ - assert isinstance(self._env['test_build'], bool) - return self._env['test_build'] - - @property - def python_directory_user(self) -> str: - """Path where the app looks for custom user scripts.""" - assert isinstance(self._env['python_directory_user'], str) - return self._env['python_directory_user'] - - @property - def python_directory_app(self) -> str: - """Path where the app looks for its bundled scripts.""" - assert isinstance(self._env['python_directory_app'], str) - return self._env['python_directory_app'] - - @property - def python_directory_app_site(self) -> str: - """Path containing pip packages bundled with the app.""" - assert isinstance(self._env['python_directory_app_site'], str) - return self._env['python_directory_app_site'] - - @property - def config(self) -> ba.AppConfig: - """The ba.AppConfig instance representing the app's config state.""" - assert self._config is not None - return self._config - - @property - def platform(self) -> str: - """Name of the current platform. - - Examples are: 'mac', 'windows', android'. - """ - assert isinstance(self._env['platform'], str) - return self._env['platform'] - - @property - def subplatform(self) -> str: - """String for subplatform. - - Can be empty. For the 'android' platform, subplatform may - be 'google', 'amazon', etc. - """ - assert isinstance(self._env['subplatform'], str) - return self._env['subplatform'] - - @property - def api_version(self) -> int: - """The game's api version. - - Only Python modules and packages associated with the current API - version number will be detected by the game (see the ba_meta tag). - This value will change whenever backward-incompatible changes are - introduced to game APIs. When that happens, scripts should be updated - accordingly and set to target the new API version number. - """ - from ba._meta import CURRENT_API_VERSION - - return CURRENT_API_VERSION - - @property - def on_tv(self) -> bool: - """Whether the game is currently running on a TV.""" - assert isinstance(self._env['on_tv'], bool) - return self._env['on_tv'] - - @property - def vr_mode(self) -> bool: - """Whether the game is currently running in VR.""" - assert isinstance(self._env['vr_mode'], bool) - return self._env['vr_mode'] - - @property - def ui_bounds(self) -> tuple[float, float, float, float]: - """Bounds of the 'safe' screen area in ui space. - - This tuple contains: (x-min, x-max, y-min, y-max) - """ - return _ba.uibounds() - - def __init__(self) -> None: - """(internal) - - Do not instantiate this class; use ba.app to access - the single shared instance. - """ - # pylint: disable=too-many-statements - - self.state = self.State.INITIAL - - self._bootstrapping_completed = False - self._called_on_app_launching = False - self._launch_completed = False - self._initial_sign_in_completed = False - self._meta_scan_completed = False - self._called_on_app_loading = False - self._called_on_app_running = False - self._app_paused = False - - # Config. - self.config_file_healthy = False - - # This is incremented any time the app is backgrounded/foregrounded; - # can be a simple way to determine if network data should be - # refreshed/etc. - self.fg_state = 0 - - self._aioloop: asyncio.AbstractEventLoop | None = None - - self._env = _ba.env() - self.protocol_version: int = self._env['protocol_version'] - assert isinstance(self.protocol_version, int) - self.toolbar_test: bool = self._env['toolbar_test'] - assert isinstance(self.toolbar_test, bool) - self.demo_mode: bool = self._env['demo_mode'] - assert isinstance(self.demo_mode, bool) - self.arcade_mode: bool = self._env['arcade_mode'] - assert isinstance(self.arcade_mode, bool) - self.headless_mode: bool = self._env['headless_mode'] - assert isinstance(self.headless_mode, bool) - self.iircade_mode: bool = self._env['iircade_mode'] - assert isinstance(self.iircade_mode, bool) - self.allow_ticket_purchases: bool = not self.iircade_mode - - # Default executor which can be used for misc background processing. - # It should also be passed to any asyncio loops we create so that - # everything shares the same single set of threads. - self.threadpool = ThreadPoolExecutor(thread_name_prefix='baworker') - - # Misc. - self.tips: list[str] = [] - self.stress_test_reset_timer: ba.Timer | None = None - self.did_weak_call_warning = False - - self.log_have_new = False - self.log_upload_timer_started = False - self._config: ba.AppConfig | None = None - self.printed_live_object_warning = False - - # We include this extra hash with shared input-mapping names so - # that we don't share mappings between differently-configured - # systems. For instance, different android devices may give different - # key values for the same controller type so we keep their mappings - # distinct. - self.input_map_hash: str | None = None - - # Co-op Campaigns. - self.campaigns: dict[str, ba.Campaign] = {} - self.custom_coop_practice_games: list[str] = [] - - # Server Mode. - self.server: ba.ServerController | None = None - - self.components = AppComponentSubsystem() - self.meta = MetadataSubsystem() - self.accounts_v1 = AccountV1Subsystem() - self.plugins = PluginSubsystem() - self.music = MusicSubsystem() - self.lang = LanguageSubsystem() - self.ach = AchievementSubsystem() - self.ui = UISubsystem() - self.ads = AdsSubsystem() - self.net = NetworkSubsystem() - self.workspaces = WorkspaceSubsystem() - - # Lobby. - self.lobby_random_profile_index: int = 1 - self.lobby_random_char_index_offset = random.randrange(1000) - self.lobby_account_profile_device_id: int | None = None - - # Main Menu. - self.main_menu_did_initial_transition = False - self.main_menu_last_news_fetch_time: float | None = None - - # Spaz. - self.spaz_appearances: dict[str, spazappearance.Appearance] = {} - self.last_spaz_turbo_warn_time: float = -99999.0 - - # Maps. - self.maps: dict[str, type[ba.Map]] = {} - - # Gameplay. - self.teams_series_length = 7 - self.ffa_series_length = 24 - self.coop_session_args: dict = {} - - self.value_test_defaults: dict = {} - self.first_main_menu = True # FIXME: Move to mainmenu class. - self.did_menu_intro = False # FIXME: Move to mainmenu class. - self.main_menu_window_refresh_check_count = 0 # FIXME: Mv to mainmenu. - self.main_menu_resume_callbacks: list = [] # Can probably go away. - self.special_offer: dict | None = None - self.ping_thread_count = 0 - self.invite_confirm_windows: list[Any] = [] # FIXME: Don't use Any. - self.store_layout: dict[str, list[dict[str, Any]]] | None = None - self.store_items: dict[str, dict] | None = None - self.pro_sale_start_time: int | None = None - self.pro_sale_start_val: int | None = None - - self.delegate: ba.AppDelegate | None = None - self._asyncio_timer: ba.Timer | None = None - - def on_app_launching(self) -> None: - """Called when the app is first entering the launching state.""" - # pylint: disable=cyclic-import - # pylint: disable=too-many-locals - from ba import _asyncio - from ba import _appconfig - from ba import _map - from ba import _campaign - from bastd import appdelegate - from bastd import maps as stdmaps - from bastd.actor import spazappearance - from ba._generated.enums import TimeType - from ba._apputils import ( - log_dumped_app_state, - handle_leftover_v1_cloud_log_file, - AppHealthMonitor, - ) - - assert _ba.in_logic_thread() - - self._aioloop = _asyncio.setup_asyncio() - self.health_monitor = AppHealthMonitor() - - cfg = self.config - - self.delegate = appdelegate.AppDelegate() - - self.ui.on_app_launch() - - spazappearance.register_appearances() - _campaign.init_campaigns() - - # FIXME: This should not be hard-coded. - for maptype in [ - stdmaps.HockeyStadium, - stdmaps.FootballStadium, - stdmaps.Bridgit, - stdmaps.BigG, - stdmaps.Roundabout, - stdmaps.MonkeyFace, - stdmaps.ZigZag, - stdmaps.ThePad, - stdmaps.DoomShroom, - stdmaps.LakeFrigid, - stdmaps.TipTop, - stdmaps.CragCastle, - stdmaps.TowerD, - stdmaps.HappyThoughts, - stdmaps.StepRightUp, - stdmaps.Courtyard, - stdmaps.Rampage, - ]: - _map.register_map(maptype) - - # Non-test, non-debug builds should generally be blessed; warn if not. - # (so I don't accidentally release a build that can't play tourneys) - if ( - not self.debug_build - and not self.test_build - and not _internal.is_blessed() - ): - _ba.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0)) - - # If there's a leftover log file, attempt to upload it to the - # master-server and/or get rid of it. - handle_leftover_v1_cloud_log_file() - - # Only do this stuff if our config file is healthy so we don't - # overwrite a broken one or whatnot and wipe out data. - if not self.config_file_healthy: - if self.platform in ('mac', 'linux', 'windows'): - from bastd.ui.configerror import ConfigErrorWindow - - _ba.pushcall(ConfigErrorWindow) - return - - # For now on other systems we just overwrite the bum config. - # At this point settings are already set; lets just commit them - # to disk. - _appconfig.commit_app_config(force=True) - - self.music.on_app_launch() - - launch_count = cfg.get('launchCount', 0) - launch_count += 1 - - # So we know how many times we've run the game at various - # version milestones. - for key in ('lc14173', 'lc14292'): - cfg.setdefault(key, launch_count) - - cfg['launchCount'] = launch_count - cfg.commit() - - # Run a test in a few seconds to see if we should pop up an existing - # pending special offer. - def check_special_offer() -> None: - from bastd.ui.specialoffer import show_offer - - config = self.config - if ( - 'pendingSpecialOffer' in config - and _internal.get_public_login_id() - == config['pendingSpecialOffer']['a'] - ): - self.special_offer = config['pendingSpecialOffer']['o'] - show_offer() - - if not self.headless_mode: - _ba.timer(3.0, check_special_offer, timetype=TimeType.REAL) - - # Get meta-system scanning built-in stuff in the bg. - self.meta.start_scan(scan_complete_cb=self.on_meta_scan_complete) - - self.accounts_v2.on_app_launch() - self.accounts_v1.on_app_launch() - - # See note below in on_app_pause. - if self.state != self.State.LAUNCHING: - logging.error( - 'on_app_launch found state %s; expected LAUNCHING.', self.state - ) - - # If any traceback dumps happened last run, log and clear them. - log_dumped_app_state() - - self._launch_completed = True - self._update_state() - - def on_app_loading(self) -> None: - """Called when initially entering the loading state.""" - - def on_app_running(self) -> None: - """Called when initially entering the running state.""" - - self.plugins.on_app_running() - - # from ba._dependency import test_depset - # test_depset() - - def on_bootstrapping_completed(self) -> None: - """Called by the C++ layer once its ready to rock.""" - assert _ba.in_logic_thread() - assert not self._bootstrapping_completed - self._bootstrapping_completed = True - self._update_state() - - def on_meta_scan_complete(self) -> None: - """Called by meta-scan when it is done doing its thing.""" - assert _ba.in_logic_thread() - self.plugins.on_meta_scan_complete() - - assert not self._meta_scan_completed - self._meta_scan_completed = True - self._update_state() - - def _update_state(self) -> None: - assert _ba.in_logic_thread() - - if self._app_paused: - # Entering paused state: - if self.state is not self.State.PAUSED: - self.state = self.State.PAUSED - self.cloud.on_app_pause() - self.accounts_v1.on_app_pause() - self.plugins.on_app_pause() - self.health_monitor.on_app_pause() - else: - # Leaving paused state: - if self.state is self.State.PAUSED: - self.fg_state += 1 - self.cloud.on_app_resume() - self.accounts_v1.on_app_resume() - self.music.on_app_resume() - self.plugins.on_app_resume() - self.health_monitor.on_app_resume() - - # Handle initially entering or returning to other states. - if self._initial_sign_in_completed and self._meta_scan_completed: - self.state = self.State.RUNNING - if not self._called_on_app_running: - self._called_on_app_running = True - self.on_app_running() - elif self._launch_completed: - self.state = self.State.LOADING - if not self._called_on_app_loading: - self._called_on_app_loading = True - self.on_app_loading() - else: - # Only thing left is launching. We shouldn't be getting - # called before at least that is complete. - assert self._bootstrapping_completed - self.state = self.State.LAUNCHING - if not self._called_on_app_launching: - self._called_on_app_launching = True - self.on_app_launching() - - def on_app_pause(self) -> None: - """Called when the app goes to a suspended state.""" - - assert not self._app_paused # Should avoid redundant calls. - self._app_paused = True - self._update_state() - - def on_app_resume(self) -> None: - """Run when the app resumes from a suspended state.""" - - assert self._app_paused # Should avoid redundant calls. - self._app_paused = False - self._update_state() - - def on_app_shutdown(self) -> None: - """(internal)""" - self.state = self.State.SHUTTING_DOWN - self.music.on_app_shutdown() - self.plugins.on_app_shutdown() - - def read_config(self) -> None: - """(internal)""" - from ba._appconfig import read_config - - self._config, self.config_file_healthy = read_config() - - def pause(self) -> None: - """Pause the game due to a user request or menu popping up. - - If there's a foreground host-activity that says it's pausable, tell it - to pause ..we now no longer pause if there are connected clients. - """ - activity: ba.Activity | None = _ba.get_foreground_host_activity() - if ( - activity is not None - and activity.allow_pausing - and not _ba.have_connected_clients() - ): - from ba._language import Lstr - from ba._nodeactor import NodeActor - - # FIXME: Shouldn't be touching scene stuff here; - # should just pass the request on to the host-session. - with _ba.Context(activity): - globs = activity.globalsnode - if not globs.paused: - _ba.playsound(_ba.getsound('refWhistle')) - globs.paused = True - - # FIXME: This should not be an attr on Actor. - activity.paused_text = NodeActor( - _ba.newnode( - 'text', - attrs={ - 'text': Lstr(resource='pausedByHostText'), - 'client_only': True, - 'flatness': 1.0, - 'h_align': 'center', - }, - ) - ) - - def resume(self) -> None: - """Resume the game due to a user request or menu closing. - - If there's a foreground host-activity that's currently paused, tell it - to resume. - """ - - # FIXME: Shouldn't be touching scene stuff here; - # should just pass the request on to the host-session. - activity = _ba.get_foreground_host_activity() - if activity is not None: - with _ba.Context(activity): - globs = activity.globalsnode - if globs.paused: - _ba.playsound(_ba.getsound('refWhistle')) - globs.paused = False - - # FIXME: This should not be an actor attr. - activity.paused_text = None - - def add_coop_practice_level(self, level: Level) -> None: - """Adds an individual level to the 'practice' section in Co-op.""" - - # Assign this level to our catch-all campaign. - self.campaigns['Challenges'].addlevel(level) - - # Make note to add it to our challenges UI. - self.custom_coop_practice_games.append(f'Challenges:{level.name}') - - def return_to_main_menu_session_gracefully( - self, reset_ui: bool = True - ) -> None: - """Attempt to cleanly get back to the main menu.""" - # pylint: disable=cyclic-import - from ba import _benchmark - from ba._general import Call - from bastd.mainmenu import MainMenuSession - - if reset_ui: - _ba.app.ui.clear_main_menu_window() - - if isinstance(_ba.get_foreground_host_session(), MainMenuSession): - # It may be possible we're on the main menu but the screen is faded - # so fade back in. - _ba.fade_screen(True) - return - - _benchmark.stop_stress_test() # Stop stress-test if in progress. - - # If we're in a host-session, tell them to end. - # This lets them tear themselves down gracefully. - host_session: ba.Session | None = _ba.get_foreground_host_session() - if host_session is not None: - - # Kick off a little transaction so we'll hopefully have all the - # latest account state when we get back to the menu. - _internal.add_transaction( - {'type': 'END_SESSION', 'sType': str(type(host_session))} - ) - _internal.run_transactions() - - host_session.end() - - # Otherwise just force the issue. - else: - _ba.pushcall(Call(_ba.new_host_session, MainMenuSession)) - - def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None: - """(internal)""" - - # If there's no main menu up, just call immediately. - if not self.ui.has_main_menu_window(): - with _ba.Context('ui'): - call() - else: - self.main_menu_resume_callbacks.append(call) - - def launch_coop_game( - self, game: str, force: bool = False, args: dict | None = None - ) -> bool: - """High level way to launch a local co-op session.""" - # pylint: disable=cyclic-import - from ba._campaign import getcampaign - from bastd.ui.coop.level import CoopLevelLockedWindow - - if args is None: - args = {} - if game == '': - raise ValueError('empty game name') - campaignname, levelname = game.split(':') - campaign = getcampaign(campaignname) - - # If this campaign is sequential, make sure we've completed the - # one before this. - if campaign.sequential and not force: - for level in campaign.levels: - if level.name == levelname: - break - if not level.complete: - CoopLevelLockedWindow( - campaign.getlevel(levelname).displayname, - campaign.getlevel(level.name).displayname, - ) - return False - - # Ok, we're good to go. - self.coop_session_args = { - 'campaign': campaignname, - 'level': levelname, - } - for arg_name, arg_val in list(args.items()): - self.coop_session_args[arg_name] = arg_val - - def _fade_end() -> None: - from ba import _coopsession - - try: - _ba.new_host_session(_coopsession.CoopSession) - except Exception: - from ba import _error - - _error.print_exception() - from bastd.mainmenu import MainMenuSession - - _ba.new_host_session(MainMenuSession) - - _ba.fade_screen(False, endcall=_fade_end) - return True - - def handle_deep_link(self, url: str) -> None: - """Handle a deep link URL.""" - from ba._language import Lstr - - appname = _ba.appname() - if url.startswith(f'{appname}://code/'): - code = url.replace(f'{appname}://code/', '') - self.accounts_v1.add_pending_promo_code(code) - else: - _ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) - _ba.playsound(_ba.getsound('error')) - - def on_initial_sign_in_completed(self) -> None: - """Callback to be run after initial sign-in (or lack thereof). - - This period includes things such as syncing account workspaces - or other data so it may take a substantial amount of time. - This should also run after a short amount of time if no login - has occurred. - """ - # Tell meta it can start scanning extra stuff that just showed up - # (account workspaces). - self.meta.start_extra_scan() - - self._initial_sign_in_completed = True - self._update_state() diff --git a/assets/src/ba_data/python/ba/_appmode.py b/assets/src/ba_data/python/ba/_appmode.py deleted file mode 100644 index ccec7ab7..00000000 --- a/assets/src/ba_data/python/ba/_appmode.py +++ /dev/null @@ -1,3 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Functionality related to the high level state of the app.""" diff --git a/assets/src/ba_data/python/ba/_bootstrap.py b/assets/src/ba_data/python/ba/_bootstrap.py deleted file mode 100644 index 0b41cbbb..00000000 --- a/assets/src/ba_data/python/ba/_bootstrap.py +++ /dev/null @@ -1,192 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Bootstrapping.""" -from __future__ import annotations - -import os -import sys -from typing import TYPE_CHECKING - -from efro.log import setup_logging, LogLevel -import _ba - -if TYPE_CHECKING: - from typing import Any - from efro.log import LogEntry - -_g_did_bootstrap = False # pylint: disable=invalid-name - - -def bootstrap() -> None: - """Run bootstrapping logic. - - This is the very first ballistica code that runs (aside from imports). - It sets up low level environment bits and creates the app instance. - """ - - global _g_did_bootstrap # pylint: disable=global-statement, invalid-name - if _g_did_bootstrap: - raise RuntimeError('Bootstrap has already been called.') - _g_did_bootstrap = True - - # The first thing we do is set up our logging system and feed - # Python's stdout/stderr into it. Then we can at least debug problems - # on systems where native stdout/stderr is not easily accessible - # such as Android. - log_handler = setup_logging( - log_path=None, - level=LogLevel.DEBUG, - suppress_non_root_debug=True, - log_stdout_stderr=True, - cache_size_limit=1024 * 1024, - ) - - log_handler.add_callback(_on_log) - - env = _ba.env() - - # Give a soft warning if we're being used with a different binary - # version than we expect. - expected_build = 21005 - running_build: int = env['build_number'] - if running_build != expected_build: - print( - f'WARNING: These script files are meant to be used with' - f' Ballistica build {expected_build}.\n' - f' You are running build {running_build}.' - f' This might cause the app to error or misbehave.', - file=sys.stderr, - ) - - # In bootstrap_monolithic.py we told Python not to handle SIGINT itself - # (because that must be done in the main thread). Now we finish the - # job by adding our own handler to replace it. - - # Note: I've found we need to set up our C signal handling AFTER - # we've told Python to disable its own; otherwise (on Mac at least) it - # wipes out our existing C handler. - _ba.setup_sigint() - - # Sanity check: we should always be run in UTF-8 mode. - if sys.flags.utf8_mode != 1: - print( - 'ERROR: Python\'s UTF-8 mode is not set.' - ' This will likely result in errors.', - file=sys.stderr, - ) - - debug_build = env['debug_build'] - - # We expect dev_mode on in debug builds and off otherwise. - if debug_build != sys.flags.dev_mode: - print( - f'WARNING: Mismatch in debug_build {debug_build}' - f' and sys.flags.dev_mode {sys.flags.dev_mode}', - file=sys.stderr, - ) - - # In embedded situations (when we're providing our own Python) let's - # also provide our own root certs so ssl works. We can consider overriding - # this in particular embedded cases if we can verify that system certs - # are working. - # (We also allow forcing this via an env var if the user desires) - if ( - _ba.contains_python_dist() - or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1' - ): - import certifi - - # Let both OpenSSL and requests (if present) know to use this. - os.environ['SSL_CERT_FILE'] = os.environ[ - 'REQUESTS_CA_BUNDLE' - ] = certifi.where() - - # On Windows I'm seeing the following error creating asyncio loops in - # background threads with the default proactor setup: - # ValueError: set_wakeup_fd only works in main thread of the main - # interpreter - # So let's explicitly request selector loops. - # Interestingly this error only started showing up once I moved - # Python init to the main thread; previously the various asyncio - # bg thread loops were working fine (maybe something caused them - # to default to selector in that case?.. - if sys.platform == 'win32': - import asyncio - - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - - # pylint: disable=c-extension-no-member - if not TYPE_CHECKING: - import __main__ - - # Clear out the standard quit/exit messages since they don't - # work in our embedded situation (should revisit this once we're - # usable from a standard interpreter). - del __main__.__builtins__.quit - del __main__.__builtins__.exit - - # Also replace standard interactive help with our simplified - # one which is more friendly to cloud/in-game console situations. - __main__.__builtins__.help = _CustomHelper() - - # Now spin up our App instance and store it on both _ba and ba. - from ba._app import App - import ba - - _ba.app = ba.app = App() - _ba.app.log_handler = log_handler - - -class _CustomHelper: - """Replacement 'help' that behaves better for our setup.""" - - def __repr__(self) -> str: - return 'Type help(object) for help about object.' - - def __call__(self, *args: Any, **kwds: Any) -> Any: - # We get an ugly error importing pydoc on our embedded - # platforms due to _sysconfigdata_xxx.py not being present - # (but then things mostly work). Let's get the ugly error out - # of the way explicitly. - import sysconfig - - try: - # This errors once but seems to run cleanly after, so let's - # get the error out of the way. - sysconfig.get_path('stdlib') - except ModuleNotFoundError: - pass - - import pydoc - - # Disable pager and interactive help since neither works well - # with our funky multi-threaded setup or in-game/cloud consoles. - # Let's just do simple text dumps. - pydoc.pager = pydoc.plainpager - if not args and not kwds: - print( - 'Interactive help is not available in this environment.\n' - 'Type help(object) for help about object.' - ) - return None - return pydoc.help(*args, **kwds) - - -def _on_log(entry: LogEntry) -> None: - - # Just forward this along to the engine to display in the in-game console, - # in the Android log, etc. - _ba.display_log( - name=entry.name, - level=entry.level.name, - message=entry.message, - ) - - # We also want to feed some logs to the old V1-cloud-log system. - # Let's go with anything warning or higher as well as the stdout/stderr - # log messages that ba.app.log_handler creates for us. - if entry.level.value >= LogLevel.WARNING.value or entry.name in ( - 'stdout', - 'stderr', - ): - _ba.v1_cloud_log(entry.message) diff --git a/assets/src/ba_data/python/ba/_hooks.py b/assets/src/ba_data/python/ba/_hooks.py deleted file mode 100644 index 67749df3..00000000 --- a/assets/src/ba_data/python/ba/_hooks.py +++ /dev/null @@ -1,498 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Snippets of code for use by the internal layer. - -History: originally the engine would dynamically compile/eval various Python -code from within C++ code, but the major downside there was that none of it -was type-checked so if names or arguments changed it would go unnoticed -until it broke at runtime. By instead defining such snippets here and then -capturing references to them all at launch it is possible to allow linting -and type-checking magic to happen and most issues will be caught immediately. -""" -# (most of these are self-explanatory) -# pylint: disable=missing-function-docstring -from __future__ import annotations - -from typing import TYPE_CHECKING - -import _ba -from ba import _internal - -if TYPE_CHECKING: - from typing import Sequence, Any - import ba - - -def finish_bootstrapping() -> None: - """Do final bootstrapping related bits.""" - assert _ba.in_logic_thread() - - # Ok, low level bootstrapping is done; time to get Python stuff started. - _ba.app.on_bootstrapping_completed() - - -def reset_to_main_menu() -> None: - """Reset the game to the main menu gracefully.""" - _ba.app.return_to_main_menu_session_gracefully() - - -def set_config_fullscreen_on() -> None: - """The app has set fullscreen on its own and we should note it.""" - _ba.app.config['Fullscreen'] = True - _ba.app.config.commit() - - -def set_config_fullscreen_off() -> None: - """The app has set fullscreen on its own and we should note it.""" - _ba.app.config['Fullscreen'] = False - _ba.app.config.commit() - - -def not_signed_in_screen_message() -> None: - from ba._language import Lstr - - _ba.screenmessage(Lstr(resource='notSignedInErrorText')) - - -def connecting_to_party_message() -> None: - from ba._language import Lstr - - _ba.screenmessage( - Lstr(resource='internal.connectingToPartyText'), color=(1, 1, 1) - ) - - -def rejecting_invite_already_in_party_message() -> None: - from ba._language import Lstr - - _ba.screenmessage( - Lstr(resource='internal.rejectingInviteAlreadyInPartyText'), - color=(1, 0.5, 0), - ) - - -def connection_failed_message() -> None: - from ba._language import Lstr - - _ba.screenmessage( - Lstr(resource='internal.connectionFailedText'), color=(1, 0.5, 0) - ) - - -def temporarily_unavailable_message() -> None: - from ba._language import Lstr - - _ba.playsound(_ba.getsound('error')) - _ba.screenmessage( - Lstr(resource='getTicketsWindow.unavailableTemporarilyText'), - color=(1, 0, 0), - ) - - -def in_progress_message() -> None: - from ba._language import Lstr - - _ba.playsound(_ba.getsound('error')) - _ba.screenmessage( - Lstr(resource='getTicketsWindow.inProgressText'), color=(1, 0, 0) - ) - - -def error_message() -> None: - from ba._language import Lstr - - _ba.playsound(_ba.getsound('error')) - _ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) - - -def purchase_not_valid_error() -> None: - from ba._language import Lstr - - _ba.playsound(_ba.getsound('error')) - _ba.screenmessage( - Lstr( - resource='store.purchaseNotValidError', - subs=[('${EMAIL}', 'support@froemling.net')], - ), - color=(1, 0, 0), - ) - - -def purchase_already_in_progress_error() -> None: - from ba._language import Lstr - - _ba.playsound(_ba.getsound('error')) - _ba.screenmessage( - Lstr(resource='store.purchaseAlreadyInProgressText'), color=(1, 0, 0) - ) - - -def gear_vr_controller_warning() -> None: - from ba._language import Lstr - - _ba.playsound(_ba.getsound('error')) - _ba.screenmessage( - Lstr(resource='usesExternalControllerText'), color=(1, 0, 0) - ) - - -def uuid_str() -> str: - import uuid - - return str(uuid.uuid4()) - - -def orientation_reset_cb_message() -> None: - from ba._language import Lstr - - _ba.screenmessage( - Lstr(resource='internal.vrOrientationResetCardboardText'), - color=(0, 1, 0), - ) - - -def orientation_reset_message() -> None: - from ba._language import Lstr - - _ba.screenmessage( - Lstr(resource='internal.vrOrientationResetText'), color=(0, 1, 0) - ) - - -def on_app_pause() -> None: - _ba.app.on_app_pause() - - -def on_app_resume() -> None: - _ba.app.on_app_resume() - - -def launch_main_menu_session() -> None: - from bastd.mainmenu import MainMenuSession - - _ba.new_host_session(MainMenuSession) - - -def language_test_toggle() -> None: - _ba.app.lang.setlanguage( - 'Gibberish' if _ba.app.lang.language == 'English' else 'English' - ) - - -def award_in_control_achievement() -> None: - _ba.app.ach.award_local_achievement('In Control') - - -def award_dual_wielding_achievement() -> None: - _ba.app.ach.award_local_achievement('Dual Wielding') - - -def play_gong_sound() -> None: - _ba.playsound(_ba.getsound('gong')) - - -def launch_coop_game(name: str) -> None: - _ba.app.launch_coop_game(name) - - -def purchases_restored_message() -> None: - from ba._language import Lstr - - _ba.screenmessage( - Lstr(resource='getTicketsWindow.purchasesRestoredText'), color=(0, 1, 0) - ) - - -def dismiss_wii_remotes_window() -> None: - call = _ba.app.ui.dismiss_wii_remotes_window_call - if call is not None: - call() - - -def unavailable_message() -> None: - from ba._language import Lstr - - _ba.screenmessage( - Lstr(resource='getTicketsWindow.unavailableText'), color=(1, 0, 0) - ) - - -def submit_analytics_counts(sval: str) -> None: - _internal.add_transaction({'type': 'ANALYTICS_COUNTS', 'values': sval}) - _internal.run_transactions() - - -def set_last_ad_network(sval: str) -> None: - import time - - _ba.app.ads.last_ad_network = sval - _ba.app.ads.last_ad_network_set_time = time.time() - - -def no_game_circle_message() -> None: - from ba._language import Lstr - - _ba.screenmessage(Lstr(resource='noGameCircleText'), color=(1, 0, 0)) - - -def google_play_purchases_not_available_message() -> None: - from ba._language import Lstr - - _ba.screenmessage( - Lstr(resource='googlePlayPurchasesNotAvailableText'), color=(1, 0, 0) - ) - - -def google_play_services_not_available_message() -> None: - from ba._language import Lstr - - _ba.screenmessage( - Lstr(resource='googlePlayServicesNotAvailableText'), color=(1, 0, 0) - ) - - -def empty_call() -> None: - pass - - -def level_icon_press() -> None: - print('LEVEL ICON PRESSED') - - -def trophy_icon_press() -> None: - print('TROPHY ICON PRESSED') - - -def coin_icon_press() -> None: - print('COIN ICON PRESSED') - - -def ticket_icon_press() -> None: - from bastd.ui.resourcetypeinfo import ResourceTypeInfoWindow - - ResourceTypeInfoWindow( - origin_widget=_ba.get_special_widget('tickets_info_button') - ) - - -def back_button_press() -> None: - _ba.back_press() - - -def friends_button_press() -> None: - print('FRIEND BUTTON PRESSED!') - - -def print_trace() -> None: - import traceback - - print('Python Traceback (most recent call last):') - traceback.print_stack() - - -def toggle_fullscreen() -> None: - cfg = _ba.app.config - cfg['Fullscreen'] = not cfg.resolve('Fullscreen') - cfg.apply_and_commit() - - -def party_icon_activate(origin: Sequence[float]) -> None: - import weakref - from bastd.ui.party import PartyWindow - - app = _ba.app - _ba.playsound(_ba.getsound('swish')) - - # If it exists, dismiss it; otherwise make a new one. - if app.ui.party_window is not None and app.ui.party_window() is not None: - app.ui.party_window().close() - else: - app.ui.party_window = weakref.ref(PartyWindow(origin=origin)) - - -def read_config() -> None: - _ba.app.read_config() - - -def ui_remote_press() -> None: - """Handle a press by a remote device that is only usable for nav.""" - from ba._language import Lstr - - # Can be called without a context; need a context for getsound. - with _ba.Context('ui'): - _ba.screenmessage( - Lstr(resource='internal.controllerForMenusOnlyText'), - color=(1, 0, 0), - ) - _ba.playsound(_ba.getsound('error')) - - -def quit_window() -> None: - from bastd.ui.confirm import QuitWindow - - QuitWindow() - - -def remove_in_game_ads_message() -> None: - _ba.app.ads.do_remove_in_game_ads_message() - - -def telnet_access_request() -> None: - from bastd.ui.telnet import TelnetAccessRequestWindow - - TelnetAccessRequestWindow() - - -def do_quit() -> None: - _ba.quit() - - -def shutdown() -> None: - _ba.app.on_app_shutdown() - - -def gc_disable() -> None: - import gc - - gc.disable() - - -def device_menu_press(device: ba.InputDevice) -> None: - from bastd.ui.mainmenu import MainMenuWindow - - in_main_menu = _ba.app.ui.has_main_menu_window() - if not in_main_menu: - _ba.set_ui_input_device(device) - _ba.playsound(_ba.getsound('swish')) - _ba.app.ui.set_main_menu_window(MainMenuWindow().get_root_widget()) - - -def show_url_window(address: str) -> None: - from bastd.ui.url import ShowURLWindow - - ShowURLWindow(address) - - -def party_invite_revoke(invite_id: str) -> None: - # If there's a confirm window up for joining this particular - # invite, kill it. - for winref in _ba.app.invite_confirm_windows: - win = winref() - if win is not None and win.ew_party_invite_id == invite_id: - _ba.containerwidget( - edit=win.get_root_widget(), transition='out_right' - ) - - -def filter_chat_message(msg: str, client_id: int) -> str | None: - """Intercept/filter chat messages. - - Called for all chat messages while hosting. - Messages originating from the host will have clientID -1. - Should filter and return the string to be displayed, or return None - to ignore the message. - """ - del client_id # Unused by default. - return msg - - -def local_chat_message(msg: str) -> None: - if ( - _ba.app.ui.party_window is not None - and _ba.app.ui.party_window() is not None - ): - _ba.app.ui.party_window().on_chat_message(msg) - - -def get_player_icon(sessionplayer: ba.SessionPlayer) -> dict[str, Any]: - info = sessionplayer.get_icon_info() - return { - 'texture': _ba.gettexture(info['texture']), - 'tint_texture': _ba.gettexture(info['tint_texture']), - 'tint_color': info['tint_color'], - 'tint2_color': info['tint2_color'], - } - - -def hash_strings(inputs: list[str]) -> str: - """Hash provided strings into a short output string.""" - import hashlib - - sha = hashlib.sha1() - for inp in inputs: - sha.update(inp.encode()) - - return sha.hexdigest() - - -def have_account_v2_credentials() -> bool: - """Do we have primary account-v2 credentials set?""" - return _ba.app.accounts_v2.have_primary_credentials() - - -def implicit_sign_in( - login_type_str: str, login_id: str, display_name: str -) -> None: - """An implicit login happened.""" - from bacommon.login import LoginType - - _ba.app.accounts_v2.on_implicit_sign_in( - login_type=LoginType(login_type_str), - login_id=login_id, - display_name=display_name, - ) - - -def implicit_sign_out(login_type_str: str) -> None: - """An implicit logout happened.""" - from bacommon.login import LoginType - - _ba.app.accounts_v2.on_implicit_sign_out( - login_type=LoginType(login_type_str) - ) - - -def login_adapter_get_sign_in_token_response( - login_type_str: str, attempt_id_str: str, result_str: str -) -> None: - """Login adapter do-sign-in completed.""" - from bacommon.login import LoginType - from ba._login import LoginAdapterNative - - login_type = LoginType(login_type_str) - attempt_id = int(attempt_id_str) - result = None if result_str == '' else result_str - with _ba.Context('ui'): - adapter = _ba.app.accounts_v2.login_adapters[login_type] - assert isinstance(adapter, LoginAdapterNative) - adapter.on_sign_in_complete(attempt_id=attempt_id, result=result) - - -def show_client_too_old_error() -> None: - """Called at launch if the server tells us we're too old to talk to it.""" - from ba._language import Lstr - - # If you are using an old build of the app and would like to stop - # seeing this error at launch, do: - # ba.app.config['SuppressClientTooOldErrorForBuild'] = ba.app.build_number - # ba.app.config.commit() - # Note that you will have to do that again later if you update to - # a newer build. - if ( - _ba.app.config.get('SuppressClientTooOldErrorForBuild') - == _ba.app.build_number - ): - return - - _ba.playsound(_ba.getsound('error')) - _ba.screenmessage( - Lstr( - translate=( - 'serverResponses', - 'Server functionality is no longer supported' - ' in this version of the game;\n' - 'Please update to a newer version.', - ) - ), - color=(1, 0, 0), - ) diff --git a/assets/src/ba_data/python/ba/_internal.py b/assets/src/ba_data/python/ba/_internal.py deleted file mode 100644 index 4fb62ba2..00000000 --- a/assets/src/ba_data/python/ba/_internal.py +++ /dev/null @@ -1,381 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""A soft wrapper around _bainternal. - -This allows code to use _bainternal functionality and get warnings -or fallbacks in some cases instead of hard errors. Code that absolutely -relies on the presence of _bainternal can just use that module directly. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -try: - # noinspection PyUnresolvedReferences - import _bainternal - - HAVE_INTERNAL = True -except ImportError: - HAVE_INTERNAL = False - -if TYPE_CHECKING: - from typing import Callable, Any - - -# Code that will function without _bainternal but which should be updated -# to account for its absence should call this to draw attention to itself. -def _no_bainternal_warning() -> None: - import logging - - logging.warning('INTERNAL CALL RUN WITHOUT INTERNAL PRESENT.') - - -# Code that won't work without _bainternal should raise these errors. -def _no_bainternal_error() -> RuntimeError: - raise RuntimeError('_bainternal is not present') - - -def get_v2_fleet() -> str: - """(internal)""" - if HAVE_INTERNAL: - return _bainternal.get_v2_fleet() - raise _no_bainternal_error() - - -def get_master_server_address(source: int = -1, version: int = 1) -> str: - """(internal) - - Return the address of the master server. - """ - if HAVE_INTERNAL: - return _bainternal.get_master_server_address( - source=source, version=version - ) - raise _no_bainternal_error() - - -def is_blessed() -> bool: - """(internal)""" - if HAVE_INTERNAL: - return _bainternal.is_blessed() - - # Harmless to always just say no here. - return False - - -def get_news_show() -> str: - """(internal)""" - if HAVE_INTERNAL: - return _bainternal.get_news_show() - raise _no_bainternal_error() - - -def game_service_has_leaderboard(game: str, config: str) -> bool: - """(internal) - - Given a game and config string, returns whether there is a leaderboard - for it on the game service. - """ - if HAVE_INTERNAL: - return _bainternal.game_service_has_leaderboard( - game=game, config=config - ) - # Harmless to always just say no here. - return False - - -def report_achievement(achievement: str, pass_to_account: bool = True) -> None: - """(internal)""" - if HAVE_INTERNAL: - _bainternal.report_achievement( - achievement=achievement, pass_to_account=pass_to_account - ) - return - - # Need to see if this actually still works as expected.. warning for now. - _no_bainternal_warning() - - -# noinspection PyUnresolvedReferences -def submit_score( - game: str, - config: str, - name: Any, - score: int | None, - callback: Callable, - order: str = 'increasing', - tournament_id: str | None = None, - score_type: str = 'points', - campaign: str | None = None, - level: str | None = None, -) -> None: - """(internal) - - Submit a score to the server; callback will be called with the results. - As a courtesy, please don't send fake scores to the server. I'd prefer - to devote my time to improving the game instead of trying to make the - score server more mischief-proof. - """ - if HAVE_INTERNAL: - _bainternal.submit_score( - game=game, - config=config, - name=name, - score=score, - callback=callback, - order=order, - tournament_id=tournament_id, - score_type=score_type, - campaign=campaign, - level=level, - ) - return - # This technically breaks since callback will never be called/etc. - raise _no_bainternal_error() - - -def tournament_query( - callback: Callable[[dict | None], None], args: dict -) -> None: - """(internal)""" - if HAVE_INTERNAL: - _bainternal.tournament_query(callback=callback, args=args) - return - - # This technically breaks since callback will never be called/etc. - raise _no_bainternal_error() - - -def power_ranking_query(callback: Callable, season: Any = None) -> None: - """(internal)""" - if HAVE_INTERNAL: - _bainternal.power_ranking_query(callback=callback, season=season) - return - - # This technically breaks since callback will never be called/etc. - raise _no_bainternal_error() - - -def restore_purchases() -> None: - """(internal)""" - if HAVE_INTERNAL: - _bainternal.restore_purchases() - return - - # This shouldn't break anything but should try to avoid calling it. - _no_bainternal_warning() - - -def purchase(item: str) -> None: - """(internal)""" - - if HAVE_INTERNAL: - _bainternal.purchase(item) - return - - # This won't break messily but won't function as intended. - _no_bainternal_warning() - - -def get_purchases_state() -> int: - """(internal)""" - - if HAVE_INTERNAL: - return _bainternal.get_purchases_state() - - # This won't function correctly without internal. - raise _no_bainternal_error() - - -def get_purchased(item: str) -> bool: - """(internal)""" - - if HAVE_INTERNAL: - return _bainternal.get_purchased(item) - - # Without internal we can just assume no purchases. - return False - - -def get_price(item: str) -> str | None: - """(internal)""" - - if HAVE_INTERNAL: - return _bainternal.get_price(item) - - # Without internal we can just assume no prices. - return None - - -def in_game_purchase(item: str, price: int) -> None: - """(internal)""" - - if HAVE_INTERNAL: - _bainternal.in_game_purchase(item=item, price=price) - return - - # Without internal this doesn't function as expected. - raise _no_bainternal_error() - - -# noinspection PyUnresolvedReferences -def add_transaction( - transaction: dict, callback: Callable | None = None -) -> None: - """(internal)""" - if HAVE_INTERNAL: - _bainternal.add_transaction(transaction=transaction, callback=callback) - return - - # This won't function correctly without internal (callback never called). - raise _no_bainternal_error() - - -def reset_achievements() -> None: - """(internal)""" - if HAVE_INTERNAL: - _bainternal.reset_achievements() - return - - # Technically doesnt break but won't do anything. - _no_bainternal_warning() - - -def get_public_login_id() -> str | None: - """(internal)""" - - if HAVE_INTERNAL: - return _bainternal.get_public_login_id() - - # Harmless to return nothing in this case. - return None - - -def have_outstanding_transactions() -> bool: - """(internal)""" - - if HAVE_INTERNAL: - return _bainternal.have_outstanding_transactions() - - # Harmless to return False here. - return False - - -def run_transactions() -> None: - """(internal)""" - if HAVE_INTERNAL: - _bainternal.run_transactions() - - # Harmless no-op in this case. - - -def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any: - """(internal)""" - if HAVE_INTERNAL: - return _bainternal.get_v1_account_misc_read_val( - name=name, default_value=default_value - ) - raise _no_bainternal_error() - - -def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any: - """(internal)""" - if HAVE_INTERNAL: - return _bainternal.get_v1_account_misc_read_val_2( - name=name, default_value=default_value - ) - raise _no_bainternal_error() - - -def get_v1_account_misc_val(name: str, default_value: Any) -> Any: - """(internal)""" - if HAVE_INTERNAL: - return _bainternal.get_v1_account_misc_val( - name=name, default_value=default_value - ) - raise _no_bainternal_error() - - -def get_v1_account_ticket_count() -> int: - """(internal) - - Returns the number of tickets for the current account. - """ - - if HAVE_INTERNAL: - return _bainternal.get_v1_account_ticket_count() - return 0 - - -def get_v1_account_state_num() -> int: - """(internal)""" - if HAVE_INTERNAL: - return _bainternal.get_v1_account_state_num() - return 0 - - -def get_v1_account_state() -> str: - """(internal)""" - if HAVE_INTERNAL: - return _bainternal.get_v1_account_state() - - # Without internal present just consider ourself always signed out. - return 'signed_out' - - -def get_v1_account_display_string(full: bool = True) -> str: - """(internal)""" - if HAVE_INTERNAL: - return _bainternal.get_v1_account_display_string(full=full) - raise _no_bainternal_error() - - -def get_v1_account_type() -> str: - """(internal)""" - if HAVE_INTERNAL: - return _bainternal.get_v1_account_type() - raise _no_bainternal_error() - - -def get_v1_account_name() -> str: - """(internal)""" - if HAVE_INTERNAL: - return _bainternal.get_v1_account_name() - raise _no_bainternal_error() - - -def sign_out_v1(v2_embedded: bool = False) -> None: - """(internal) - - Category: General Utility Functions - """ - if HAVE_INTERNAL: - _bainternal.sign_out_v1(v2_embedded=v2_embedded) - return - raise _no_bainternal_error() - - -def sign_in_v1(account_type: str) -> None: - """(internal) - - Category: General Utility Functions - """ - if HAVE_INTERNAL: - _bainternal.sign_in_v1(account_type=account_type) - return - raise _no_bainternal_error() - - -def mark_config_dirty() -> None: - """(internal) - - Category: General Utility Functions - """ - if HAVE_INTERNAL: - _bainternal.mark_config_dirty() - return - - # Note to self - need to fix config writing to not rely on - # internal lib. - _no_bainternal_warning() diff --git a/assets/src/ba_data/python/ba/_store.py b/assets/src/ba_data/python/ba/_store.py deleted file mode 100644 index ef8385d9..00000000 --- a/assets/src/ba_data/python/ba/_store.py +++ /dev/null @@ -1,498 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Store related functionality for classic mode.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import _ba -from ba import _internal - -if TYPE_CHECKING: - from typing import Any - import ba - - -def get_store_item(item: str) -> dict[str, Any]: - """(internal)""" - return get_store_items()[item] - - -def get_store_item_name_translated(item_name: str) -> ba.Lstr: - """Return a ba.Lstr for a store item name.""" - # pylint: disable=cyclic-import - from ba import _language - from ba import _map - - item_info = get_store_item(item_name) - if item_name.startswith('characters.'): - return _language.Lstr( - translate=('characterNames', item_info['character']) - ) - if item_name in ['merch']: - return _language.Lstr(resource='merchText') - if item_name in ['upgrades.pro', 'pro']: - return _language.Lstr( - resource='store.bombSquadProNameText', - subs=[('${APP_NAME}', _language.Lstr(resource='titleText'))], - ) - if item_name.startswith('maps.'): - map_type: type[ba.Map] = item_info['map_type'] - return _map.get_map_display_string(map_type.name) - if item_name.startswith('games.'): - gametype: type[ba.GameActivity] = item_info['gametype'] - return gametype.get_display_string() - if item_name.startswith('icons.'): - return _language.Lstr(resource='editProfileWindow.iconText') - raise ValueError('unrecognized item: ' + item_name) - - -def get_store_item_display_size(item_name: str) -> tuple[float, float]: - """(internal)""" - if item_name.startswith('characters.'): - return 340 * 0.6, 430 * 0.6 - if item_name in ['pro', 'upgrades.pro', 'merch']: - from ba._generated.enums import UIScale - - return 650 * 0.9, 500 * ( - 0.72 - if ( - _ba.app.config.get('Merch Link') - and _ba.app.ui.uiscale is UIScale.SMALL - ) - else 0.85 - ) - if item_name.startswith('maps.'): - return 510 * 0.6, 450 * 0.6 - if item_name.startswith('icons.'): - return 265 * 0.6, 250 * 0.6 - return 450 * 0.6, 450 * 0.6 - - -def get_store_items() -> dict[str, dict]: - """Returns info about purchasable items. - - (internal) - """ - # pylint: disable=cyclic-import - from ba._generated.enums import SpecialChar - from bastd import maps - - if _ba.app.store_items is None: - from bastd.game import ninjafight - from bastd.game import meteorshower - from bastd.game import targetpractice - from bastd.game import easteregghunt - - # IMPORTANT - need to keep this synced with the master server. - # (doing so manually for now) - _ba.app.store_items = { - 'characters.kronk': {'character': 'Kronk'}, - 'characters.zoe': {'character': 'Zoe'}, - 'characters.jackmorgan': {'character': 'Jack Morgan'}, - 'characters.mel': {'character': 'Mel'}, - 'characters.snakeshadow': {'character': 'Snake Shadow'}, - 'characters.bones': {'character': 'Bones'}, - 'characters.bernard': { - 'character': 'Bernard', - 'highlight': (0.6, 0.5, 0.8), - }, - 'characters.pixie': {'character': 'Pixel'}, - 'characters.wizard': {'character': 'Grumbledorf'}, - 'characters.frosty': {'character': 'Frosty'}, - 'characters.pascal': {'character': 'Pascal'}, - 'characters.cyborg': {'character': 'B-9000'}, - 'characters.agent': {'character': 'Agent Johnson'}, - 'characters.taobaomascot': {'character': 'Taobao Mascot'}, - 'characters.santa': {'character': 'Santa Claus'}, - 'characters.bunny': {'character': 'Easter Bunny'}, - 'merch': {}, - 'pro': {}, - 'maps.lake_frigid': {'map_type': maps.LakeFrigid}, - 'games.ninja_fight': { - 'gametype': ninjafight.NinjaFightGame, - 'previewTex': 'courtyardPreview', - }, - 'games.meteor_shower': { - 'gametype': meteorshower.MeteorShowerGame, - 'previewTex': 'rampagePreview', - }, - 'games.target_practice': { - 'gametype': targetpractice.TargetPracticeGame, - 'previewTex': 'doomShroomPreview', - }, - 'games.easter_egg_hunt': { - 'gametype': easteregghunt.EasterEggHuntGame, - 'previewTex': 'towerDPreview', - }, - 'icons.flag_us': { - 'icon': _ba.charstr(SpecialChar.FLAG_UNITED_STATES) - }, - 'icons.flag_mexico': {'icon': _ba.charstr(SpecialChar.FLAG_MEXICO)}, - 'icons.flag_germany': { - 'icon': _ba.charstr(SpecialChar.FLAG_GERMANY) - }, - 'icons.flag_brazil': {'icon': _ba.charstr(SpecialChar.FLAG_BRAZIL)}, - 'icons.flag_russia': {'icon': _ba.charstr(SpecialChar.FLAG_RUSSIA)}, - 'icons.flag_china': {'icon': _ba.charstr(SpecialChar.FLAG_CHINA)}, - 'icons.flag_uk': { - 'icon': _ba.charstr(SpecialChar.FLAG_UNITED_KINGDOM) - }, - 'icons.flag_canada': {'icon': _ba.charstr(SpecialChar.FLAG_CANADA)}, - 'icons.flag_india': {'icon': _ba.charstr(SpecialChar.FLAG_INDIA)}, - 'icons.flag_japan': {'icon': _ba.charstr(SpecialChar.FLAG_JAPAN)}, - 'icons.flag_france': {'icon': _ba.charstr(SpecialChar.FLAG_FRANCE)}, - 'icons.flag_indonesia': { - 'icon': _ba.charstr(SpecialChar.FLAG_INDONESIA) - }, - 'icons.flag_italy': {'icon': _ba.charstr(SpecialChar.FLAG_ITALY)}, - 'icons.flag_south_korea': { - 'icon': _ba.charstr(SpecialChar.FLAG_SOUTH_KOREA) - }, - 'icons.flag_netherlands': { - 'icon': _ba.charstr(SpecialChar.FLAG_NETHERLANDS) - }, - 'icons.flag_uae': { - 'icon': _ba.charstr(SpecialChar.FLAG_UNITED_ARAB_EMIRATES) - }, - 'icons.flag_qatar': {'icon': _ba.charstr(SpecialChar.FLAG_QATAR)}, - 'icons.flag_egypt': {'icon': _ba.charstr(SpecialChar.FLAG_EGYPT)}, - 'icons.flag_kuwait': {'icon': _ba.charstr(SpecialChar.FLAG_KUWAIT)}, - 'icons.flag_algeria': { - 'icon': _ba.charstr(SpecialChar.FLAG_ALGERIA) - }, - 'icons.flag_saudi_arabia': { - 'icon': _ba.charstr(SpecialChar.FLAG_SAUDI_ARABIA) - }, - 'icons.flag_malaysia': { - 'icon': _ba.charstr(SpecialChar.FLAG_MALAYSIA) - }, - 'icons.flag_czech_republic': { - 'icon': _ba.charstr(SpecialChar.FLAG_CZECH_REPUBLIC) - }, - 'icons.flag_australia': { - 'icon': _ba.charstr(SpecialChar.FLAG_AUSTRALIA) - }, - 'icons.flag_singapore': { - 'icon': _ba.charstr(SpecialChar.FLAG_SINGAPORE) - }, - 'icons.flag_iran': {'icon': _ba.charstr(SpecialChar.FLAG_IRAN)}, - 'icons.flag_poland': {'icon': _ba.charstr(SpecialChar.FLAG_POLAND)}, - 'icons.flag_argentina': { - 'icon': _ba.charstr(SpecialChar.FLAG_ARGENTINA) - }, - 'icons.flag_philippines': { - 'icon': _ba.charstr(SpecialChar.FLAG_PHILIPPINES) - }, - 'icons.flag_chile': {'icon': _ba.charstr(SpecialChar.FLAG_CHILE)}, - 'icons.fedora': {'icon': _ba.charstr(SpecialChar.FEDORA)}, - 'icons.hal': {'icon': _ba.charstr(SpecialChar.HAL)}, - 'icons.crown': {'icon': _ba.charstr(SpecialChar.CROWN)}, - 'icons.yinyang': {'icon': _ba.charstr(SpecialChar.YIN_YANG)}, - 'icons.eyeball': {'icon': _ba.charstr(SpecialChar.EYE_BALL)}, - 'icons.skull': {'icon': _ba.charstr(SpecialChar.SKULL)}, - 'icons.heart': {'icon': _ba.charstr(SpecialChar.HEART)}, - 'icons.dragon': {'icon': _ba.charstr(SpecialChar.DRAGON)}, - 'icons.helmet': {'icon': _ba.charstr(SpecialChar.HELMET)}, - 'icons.mushroom': {'icon': _ba.charstr(SpecialChar.MUSHROOM)}, - 'icons.ninja_star': {'icon': _ba.charstr(SpecialChar.NINJA_STAR)}, - 'icons.viking_helmet': { - 'icon': _ba.charstr(SpecialChar.VIKING_HELMET) - }, - 'icons.moon': {'icon': _ba.charstr(SpecialChar.MOON)}, - 'icons.spider': {'icon': _ba.charstr(SpecialChar.SPIDER)}, - 'icons.fireball': {'icon': _ba.charstr(SpecialChar.FIREBALL)}, - 'icons.mikirog': {'icon': _ba.charstr(SpecialChar.MIKIROG)}, - } - return _ba.app.store_items - - -def get_store_layout() -> dict[str, list[dict[str, Any]]]: - """Return what's available in the store at a given time. - - Categorized by tab and by section.""" - if _ba.app.store_layout is None: - _ba.app.store_layout = { - 'characters': [{'items': []}], - 'extras': [{'items': ['pro']}], - 'maps': [{'items': ['maps.lake_frigid']}], - 'minigames': [], - 'icons': [ - { - 'items': [ - 'icons.mushroom', - 'icons.heart', - 'icons.eyeball', - 'icons.yinyang', - 'icons.hal', - 'icons.flag_us', - 'icons.flag_mexico', - 'icons.flag_germany', - 'icons.flag_brazil', - 'icons.flag_russia', - 'icons.flag_china', - 'icons.flag_uk', - 'icons.flag_canada', - 'icons.flag_india', - 'icons.flag_japan', - 'icons.flag_france', - 'icons.flag_indonesia', - 'icons.flag_italy', - 'icons.flag_south_korea', - 'icons.flag_netherlands', - 'icons.flag_uae', - 'icons.flag_qatar', - 'icons.flag_egypt', - 'icons.flag_kuwait', - 'icons.flag_algeria', - 'icons.flag_saudi_arabia', - 'icons.flag_malaysia', - 'icons.flag_czech_republic', - 'icons.flag_australia', - 'icons.flag_singapore', - 'icons.flag_iran', - 'icons.flag_poland', - 'icons.flag_argentina', - 'icons.flag_philippines', - 'icons.flag_chile', - 'icons.moon', - 'icons.fedora', - 'icons.spider', - 'icons.ninja_star', - 'icons.skull', - 'icons.dragon', - 'icons.viking_helmet', - 'icons.fireball', - 'icons.helmet', - 'icons.crown', - ] - } - ], - } - store_layout = _ba.app.store_layout - store_layout['characters'] = [ - { - 'items': [ - 'characters.kronk', - 'characters.zoe', - 'characters.jackmorgan', - 'characters.mel', - 'characters.snakeshadow', - 'characters.bones', - 'characters.bernard', - 'characters.agent', - 'characters.frosty', - 'characters.pascal', - 'characters.pixie', - ] - } - ] - store_layout['minigames'] = [ - { - 'items': [ - 'games.ninja_fight', - 'games.meteor_shower', - 'games.target_practice', - ] - } - ] - if _internal.get_v1_account_misc_read_val('xmas', False): - store_layout['characters'][0]['items'].append('characters.santa') - store_layout['characters'][0]['items'].append('characters.wizard') - store_layout['characters'][0]['items'].append('characters.cyborg') - if _internal.get_v1_account_misc_read_val('easter', False): - store_layout['characters'].append( - {'title': 'store.holidaySpecialText', 'items': ['characters.bunny']} - ) - store_layout['minigames'].append( - { - 'title': 'store.holidaySpecialText', - 'items': ['games.easter_egg_hunt'], - } - ) - - # This will cause merch to show only if the master-server has - # given us a link (which means merch is available in our region). - store_layout['extras'] = [{'items': ['pro']}] - if _ba.app.config.get('Merch Link'): - store_layout['extras'][0]['items'].append('merch') - return store_layout - - -def get_clean_price(price_string: str) -> str: - """(internal)""" - - # I'm not brave enough to try and do any numerical - # manipulation on formatted price strings, but lets do a - # few swap-outs to tidy things up a bit. - psubs = { - '$2.99': '$3.00', - '$4.99': '$5.00', - '$9.99': '$10.00', - '$19.99': '$20.00', - '$49.99': '$50.00', - } - return psubs.get(price_string, price_string) - - -def get_available_purchase_count(tab: str | None = None) -> int: - """(internal)""" - try: - if _internal.get_v1_account_state() != 'signed_in': - return 0 - count = 0 - our_tickets = _internal.get_v1_account_ticket_count() - store_data = get_store_layout() - if tab is not None: - tabs = [(tab, store_data[tab])] - else: - tabs = list(store_data.items()) - for tab_name, tabval in tabs: - if tab_name == 'icons': - continue # too many of these; don't show.. - count = _calc_count_for_tab(tabval, our_tickets, count) - return count - except Exception: - from ba import _error - - _error.print_exception('error calcing available purchases') - return 0 - - -def _calc_count_for_tab( - tabval: list[dict[str, Any]], our_tickets: int, count: int -) -> int: - for section in tabval: - for item in section['items']: - ticket_cost = _internal.get_v1_account_misc_read_val( - 'price.' + item, None - ) - if ticket_cost is not None: - if our_tickets >= ticket_cost and not _internal.get_purchased( - item - ): - count += 1 - return count - - -def get_available_sale_time(tab: str) -> int | None: - """(internal)""" - # pylint: disable=too-many-branches - # pylint: disable=too-many-nested-blocks - # pylint: disable=too-many-locals - try: - import datetime - from ba._generated.enums import TimeType, TimeFormat - - app = _ba.app - sale_times: list[int | None] = [] - - # Calc time for our pro sale (old special case). - if tab == 'extras': - config = app.config - if app.accounts_v1.have_pro(): - return None - - # If we haven't calced/loaded start times yet. - if app.pro_sale_start_time is None: - - # If we've got a time-remaining in our config, start there. - if 'PSTR' in config: - app.pro_sale_start_time = int( - _ba.time(TimeType.REAL, TimeFormat.MILLISECONDS) - ) - app.pro_sale_start_val = config['PSTR'] - else: - - # We start the timer once we get the duration from - # the server. - start_duration = _internal.get_v1_account_misc_read_val( - 'proSaleDurationMinutes', None - ) - if start_duration is not None: - app.pro_sale_start_time = int( - _ba.time(TimeType.REAL, TimeFormat.MILLISECONDS) - ) - app.pro_sale_start_val = 60000 * start_duration - - # If we haven't heard from the server yet, no sale.. - else: - return None - - assert app.pro_sale_start_val is not None - val: int | None = max( - 0, - app.pro_sale_start_val - - ( - _ba.time(TimeType.REAL, TimeFormat.MILLISECONDS) - - app.pro_sale_start_time - ), - ) - - # Keep the value in the config up to date. I suppose we should - # write the config occasionally but it should happen often enough - # for other reasons. - config['PSTR'] = val - if val == 0: - val = None - sale_times.append(val) - - # Now look for sales in this tab. - sales_raw = _internal.get_v1_account_misc_read_val('sales', {}) - store_layout = get_store_layout() - for section in store_layout[tab]: - for item in section['items']: - if item in sales_raw: - if not _internal.get_purchased(item): - to_end = ( - datetime.datetime.utcfromtimestamp( - sales_raw[item]['e'] - ) - - datetime.datetime.utcnow() - ).total_seconds() - if to_end > 0: - sale_times.append(int(to_end * 1000)) - - # Return the smallest time I guess? - sale_times_int = [t for t in sale_times if isinstance(t, int)] - return min(sale_times_int) if sale_times_int else None - - except Exception: - from ba import _error - - _error.print_exception('error calcing sale time') - return None - - -def get_unowned_maps() -> list[str]: - """Return the list of local maps not owned by the current account. - - Category: **Asset Functions** - """ - unowned_maps: set[str] = set() - if not _ba.app.headless_mode: - for map_section in get_store_layout()['maps']: - for mapitem in map_section['items']: - if not _internal.get_purchased(mapitem): - m_info = get_store_item(mapitem) - unowned_maps.add(m_info['map_type'].name) - return sorted(unowned_maps) - - -def get_unowned_game_types() -> set[type[ba.GameActivity]]: - """Return present game types not owned by the current account.""" - try: - unowned_games: set[type[ba.GameActivity]] = set() - if not _ba.app.headless_mode: - for section in get_store_layout()['minigames']: - for mname in section['items']: - if not _internal.get_purchased(mname): - m_info = get_store_item(mname) - unowned_games.add(m_info['gametype']) - return unowned_games - except Exception: - from ba import _error - - _error.print_exception('error calcing un-owned games') - return set() diff --git a/assets/src/ba_data/python/ba/internal.py b/assets/src/ba_data/python/ba/internal.py deleted file mode 100644 index 4b04a9a7..00000000 --- a/assets/src/ba_data/python/ba/internal.py +++ /dev/null @@ -1,339 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Exposed functionality not intended for full public use. - -Classes and functions contained here, while technically 'public', may change -or disappear without warning, so should be avoided (or used sparingly and -defensively) in mods. -""" -from __future__ import annotations - -from _ba import ( - show_online_score_ui, - set_ui_input_device, - is_party_icon_visible, - getinputdevice, - add_clean_frame_callback, - unlock_all_input, - increment_analytics_count, - set_debug_speed_exponent, - get_special_widget, - get_qrcode_texture, - get_string_height, - get_string_width, - show_app_invite, - appnameupper, - lock_all_input, - open_file_externally, - fade_screen, - appname, - have_incentivized_ad, - has_video_ads, - workspaces_in_use, - set_party_icon_always_visible, - connect_to_party, - get_game_port, - end_host_scanning, - host_scan_cycle, - charstr, - get_public_party_enabled, - get_public_party_max_size, - set_public_party_name, - set_public_party_max_size, - set_public_party_queue_enabled, - set_authenticate_clients, - set_public_party_enabled, - reset_random_player_names, - new_host_session, - get_foreground_host_session, - get_local_active_input_devices_count, - get_ui_input_device, - is_in_replay, - set_replay_speed_exponent, - get_replay_speed_exponent, - disconnect_from_host, - set_party_window_open, - get_connection_to_host_info, - get_chat_messages, - get_game_roster, - disconnect_client, - chatmessage, - get_random_names, - have_permission, - request_permission, - have_touchscreen_input, - is_xcode_build, - set_low_level_config_value, - get_low_level_config_value, - capture_gamepad_input, - release_gamepad_input, - has_gamma_control, - get_max_graphics_quality, - get_display_resolution, - capture_keyboard_input, - release_keyboard_input, - value_test, - set_touchscreen_editing, - is_running_on_fire_tv, - android_get_external_files_dir, - set_telnet_access_enabled, - new_replay_session, - get_replays_dir, -) - -from ba._login import LoginAdapter -from ba._map import ( - get_map_class, - register_map, - preload_map_preview_media, - get_map_display_string, - get_filtered_map_name, -) -from ba._appconfig import commit_app_config -from ba._input import ( - get_device_value, - get_input_map_hash, - get_input_device_config, -) -from ba._general import getclass, json_prep, get_type_name -from ba._activitytypes import JoinActivity, ScoreScreenActivity -from ba._apputils import ( - is_browser_likely_available, - get_remote_app_name, - should_submit_debug_info, - dump_app_state, - log_dumped_app_state, -) -from ba._benchmark import ( - run_gpu_benchmark, - run_cpu_benchmark, - run_media_reload_benchmark, - run_stress_test, -) -from ba._campaign import getcampaign -from ba._messages import PlayerProfilesChangedMessage -from ba._multiteamsession import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES -from ba._music import do_play_music -from ba._net import ( - master_server_get, - master_server_post, - get_ip_address_type, - DEFAULT_REQUEST_TIMEOUT_SECONDS, -) -from ba._powerup import get_default_powerup_distribution -from ba._profile import ( - get_player_profile_colors, - get_player_profile_icon, - get_player_colors, -) -from ba._tips import get_next_tip -from ba._playlist import ( - get_default_free_for_all_playlist, - get_default_teams_playlist, - filter_playlist, -) -from ba._store import ( - get_available_sale_time, - get_available_purchase_count, - get_store_item_name_translated, - get_store_item_display_size, - get_store_layout, - get_store_item, - get_clean_price, - get_unowned_maps, - get_unowned_game_types, -) -from ba._tournament import get_tournament_prize_strings -from ba._gameutils import get_trophy_string - -from ba._internal import ( - get_v2_fleet, - get_master_server_address, - is_blessed, - get_news_show, - game_service_has_leaderboard, - report_achievement, - submit_score, - tournament_query, - power_ranking_query, - restore_purchases, - purchase, - get_purchases_state, - get_purchased, - get_price, - in_game_purchase, - add_transaction, - reset_achievements, - get_public_login_id, - have_outstanding_transactions, - run_transactions, - get_v1_account_misc_read_val, - get_v1_account_misc_read_val_2, - get_v1_account_misc_val, - get_v1_account_ticket_count, - get_v1_account_state_num, - get_v1_account_state, - get_v1_account_display_string, - get_v1_account_type, - get_v1_account_name, - sign_out_v1, - sign_in_v1, - mark_config_dirty, -) - -__all__ = [ - 'LoginAdapter', - 'show_online_score_ui', - 'set_ui_input_device', - 'is_party_icon_visible', - 'getinputdevice', - 'add_clean_frame_callback', - 'unlock_all_input', - 'increment_analytics_count', - 'set_debug_speed_exponent', - 'get_special_widget', - 'get_qrcode_texture', - 'get_string_height', - 'get_string_width', - 'show_app_invite', - 'appnameupper', - 'lock_all_input', - 'open_file_externally', - 'fade_screen', - 'appname', - 'have_incentivized_ad', - 'has_video_ads', - 'workspaces_in_use', - 'set_party_icon_always_visible', - 'connect_to_party', - 'get_game_port', - 'end_host_scanning', - 'host_scan_cycle', - 'charstr', - 'get_public_party_enabled', - 'get_public_party_max_size', - 'set_public_party_name', - 'set_public_party_max_size', - 'set_public_party_queue_enabled', - 'set_authenticate_clients', - 'set_public_party_enabled', - 'reset_random_player_names', - 'new_host_session', - 'get_foreground_host_session', - 'get_local_active_input_devices_count', - 'get_ui_input_device', - 'is_in_replay', - 'set_replay_speed_exponent', - 'get_replay_speed_exponent', - 'disconnect_from_host', - 'set_party_window_open', - 'get_connection_to_host_info', - 'get_chat_messages', - 'get_game_roster', - 'disconnect_client', - 'chatmessage', - 'get_random_names', - 'have_permission', - 'request_permission', - 'have_touchscreen_input', - 'is_xcode_build', - 'set_low_level_config_value', - 'get_low_level_config_value', - 'capture_gamepad_input', - 'release_gamepad_input', - 'has_gamma_control', - 'get_max_graphics_quality', - 'get_display_resolution', - 'capture_keyboard_input', - 'release_keyboard_input', - 'value_test', - 'set_touchscreen_editing', - 'is_running_on_fire_tv', - 'android_get_external_files_dir', - 'set_telnet_access_enabled', - 'new_replay_session', - 'get_replays_dir', - 'get_unowned_maps', - 'get_unowned_game_types', - 'get_map_class', - 'register_map', - 'preload_map_preview_media', - 'get_map_display_string', - 'get_filtered_map_name', - 'commit_app_config', - 'get_device_value', - 'get_input_map_hash', - 'get_input_device_config', - 'getclass', - 'json_prep', - 'get_type_name', - 'JoinActivity', - 'ScoreScreenActivity', - 'is_browser_likely_available', - 'get_remote_app_name', - 'should_submit_debug_info', - 'run_gpu_benchmark', - 'run_cpu_benchmark', - 'run_media_reload_benchmark', - 'run_stress_test', - 'getcampaign', - 'PlayerProfilesChangedMessage', - 'DEFAULT_TEAM_COLORS', - 'DEFAULT_TEAM_NAMES', - 'do_play_music', - 'master_server_get', - 'master_server_post', - 'get_ip_address_type', - 'DEFAULT_REQUEST_TIMEOUT_SECONDS', - 'get_default_powerup_distribution', - 'get_player_profile_colors', - 'get_player_profile_icon', - 'get_player_colors', - 'get_next_tip', - 'get_default_free_for_all_playlist', - 'get_default_teams_playlist', - 'filter_playlist', - 'get_available_sale_time', - 'get_available_purchase_count', - 'get_store_item_name_translated', - 'get_store_item_display_size', - 'get_store_layout', - 'get_store_item', - 'get_clean_price', - 'get_tournament_prize_strings', - 'get_trophy_string', - 'get_v2_fleet', - 'get_master_server_address', - 'is_blessed', - 'get_news_show', - 'game_service_has_leaderboard', - 'report_achievement', - 'submit_score', - 'tournament_query', - 'power_ranking_query', - 'restore_purchases', - 'purchase', - 'get_purchases_state', - 'get_purchased', - 'get_price', - 'in_game_purchase', - 'add_transaction', - 'reset_achievements', - 'get_public_login_id', - 'have_outstanding_transactions', - 'run_transactions', - 'get_v1_account_misc_read_val', - 'get_v1_account_misc_read_val_2', - 'get_v1_account_misc_val', - 'get_v1_account_ticket_count', - 'get_v1_account_state_num', - 'get_v1_account_state', - 'get_v1_account_display_string', - 'get_v1_account_type', - 'get_v1_account_name', - 'sign_out_v1', - 'sign_in_v1', - 'mark_config_dirty', - 'dump_app_state', - 'log_dumped_app_state', -] diff --git a/assets/src/ba_data/python/bastd/appdelegate.py b/assets/src/ba_data/python/bastd/appdelegate.py deleted file mode 100644 index 746aa5d8..00000000 --- a/assets/src/ba_data/python/bastd/appdelegate.py +++ /dev/null @@ -1,37 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Provide our delegate for high level app functionality.""" -from __future__ import annotations - -from typing import TYPE_CHECKING - -import ba - -if TYPE_CHECKING: - from typing import Any, Callable - - -class AppDelegate(ba.AppDelegate): - """Defines handlers for high level app functionality.""" - - def create_default_game_settings_ui( - self, - gameclass: type[ba.GameActivity], - sessiontype: type[ba.Session], - settings: dict | None, - completion_call: Callable[[dict | None], Any], - ) -> None: - """(internal)""" - - # Replace the main window once we come up successfully. - from bastd.ui.playlist.editgame import PlaylistEditGameWindow - - ba.app.ui.clear_main_menu_window(transition='out_left') - ba.app.ui.set_main_menu_window( - PlaylistEditGameWindow( - gameclass, - sessiontype, - settings, - completion_call=completion_call, - ).get_root_widget() - ) diff --git a/assets/src/ba_data/python/bastd/stdmap.py b/assets/src/ba_data/python/bastd/stdmap.py deleted file mode 100644 index 505705ac..00000000 --- a/assets/src/ba_data/python/bastd/stdmap.py +++ /dev/null @@ -1,39 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Defines standard map type.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import ba - -if TYPE_CHECKING: - from typing import Any - - -def _get_map_data(name: str) -> dict[str, Any]: - import json - - print('Would get map data', name) - with open( - 'ba_data/data/maps/' + name + '.json', encoding='utf-8' - ) as infile: - mapdata = json.loads(infile.read()) - assert isinstance(mapdata, dict) - return mapdata - - -class StdMap(ba.Map): - """A map completely defined by asset data.""" - - _data: dict[str, Any] | None = None - - @classmethod - def _getdata(cls) -> dict[str, Any]: - if cls._data is None: - cls._data = _get_map_data('bridgit') - return cls._data - - def __init__(self) -> None: - super().__init__() diff --git a/assets/src/ba_data/python/bastd/ui/appinvite.py b/assets/src/ba_data/python/bastd/ui/appinvite.py deleted file mode 100644 index 525e8711..00000000 --- a/assets/src/ba_data/python/bastd/ui/appinvite.py +++ /dev/null @@ -1,449 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""UI functionality related to inviting people to try the game.""" - -from __future__ import annotations - -import copy -import time -from typing import TYPE_CHECKING - -import ba -import ba.internal - -if TYPE_CHECKING: - from typing import Any - - -class AppInviteWindow(ba.Window): - """Window for showing different ways to invite people to try the game.""" - - def __init__(self) -> None: - ba.set_analytics_screen('AppInviteWindow') - self._data: dict[str, Any] | None = None - self._width = 650 - self._height = 400 - - uiscale = ba.app.ui.uiscale - super().__init__( - root_widget=ba.containerwidget( - size=(self._width, self._height), - transition='in_scale', - scale=( - 1.8 - if uiscale is ba.UIScale.SMALL - else 1.35 - if uiscale is ba.UIScale.MEDIUM - else 1.0 - ), - ) - ) - - self._cancel_button = ba.buttonwidget( - parent=self._root_widget, - scale=0.8, - position=(60, self._height - 50), - size=(50, 50), - label='', - on_activate_call=self.close, - autoselect=True, - color=(0.4, 0.4, 0.6), - icon=ba.gettexture('crossOut'), - iconscale=1.2, - ) - - ba.containerwidget( - edit=self._root_widget, cancel_button=self._cancel_button - ) - - ba.textwidget( - parent=self._root_widget, - size=(0, 0), - position=(self._width * 0.5, self._height * 0.5 + 110), - autoselect=True, - scale=0.8, - maxwidth=self._width * 0.9, - h_align='center', - v_align='center', - color=(0.3, 0.8, 0.3), - flatness=1.0, - text=ba.Lstr( - resource='gatherWindow.earnTicketsForRecommendingAmountText', - fallback_resource=( - 'gatherWindow.earnTicketsForRecommendingText' - ), - subs=[ - ( - '${COUNT}', - str( - ba.internal.get_v1_account_misc_read_val( - 'friendTryTickets', 300 - ) - ), - ), - ( - '${YOU_COUNT}', - str( - ba.internal.get_v1_account_misc_read_val( - 'friendTryAwardTickets', 100 - ) - ), - ), - ], - ), - ) - - or_text = ( - ba.Lstr(resource='orText', subs=[('${A}', ''), ('${B}', '')]) - .evaluate() - .strip() - ) - ba.buttonwidget( - parent=self._root_widget, - size=(250, 150), - position=(self._width * 0.5 - 125, self._height * 0.5 - 80), - autoselect=True, - button_type='square', - label=ba.Lstr(resource='gatherWindow.inviteFriendsText'), - on_activate_call=ba.WeakCall(self._google_invites), - ) - - ba.textwidget( - parent=self._root_widget, - size=(0, 0), - position=(self._width * 0.5, self._height * 0.5 - 94), - autoselect=True, - scale=0.9, - h_align='center', - v_align='center', - color=(0.5, 0.5, 0.5), - flatness=1.0, - text=or_text, - ) - - ba.buttonwidget( - parent=self._root_widget, - size=(180, 50), - position=(self._width * 0.5 - 90, self._height * 0.5 - 170), - autoselect=True, - color=(0.5, 0.5, 0.6), - textcolor=(0.7, 0.7, 0.8), - text_scale=0.8, - label=ba.Lstr(resource='gatherWindow.appInviteSendACodeText'), - on_activate_call=ba.WeakCall(self._send_code), - ) - - # kick off a transaction to get our code - ba.internal.add_transaction( - { - 'type': 'FRIEND_PROMO_CODE_REQUEST', - 'ali': False, - 'expire_time': time.time() + 20, - }, - callback=ba.WeakCall(self._on_code_result), - ) - ba.internal.run_transactions() - - def _on_code_result(self, result: dict[str, Any] | None) -> None: - if result is not None: - self._data = result - - def _send_code(self) -> None: - handle_app_invites_press(force_code=True) - - def _google_invites(self) -> None: - if self._data is None: - ba.screenmessage( - ba.Lstr(resource='getTicketsWindow.unavailableTemporarilyText'), - color=(1, 0, 0), - ) - ba.playsound(ba.getsound('error')) - return - - if ba.internal.get_v1_account_state() == 'signed_in': - ba.set_analytics_screen('App Invite UI') - ba.internal.show_app_invite( - ba.Lstr( - resource='gatherWindow.appInviteTitleText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], - ).evaluate(), - ba.Lstr( - resource='gatherWindow.appInviteMessageText', - subs=[ - ('${COUNT}', str(self._data['tickets'])), - ( - '${NAME}', - ba.internal.get_v1_account_name().split()[0], - ), - ('${APP_NAME}', ba.Lstr(resource='titleText')), - ], - ).evaluate(), - self._data['code'], - ) - else: - ba.playsound(ba.getsound('error')) - - def close(self) -> None: - """Close the window.""" - ba.containerwidget(edit=self._root_widget, transition='out_scale') - - -class ShowFriendCodeWindow(ba.Window): - """Window showing a code for sharing with friends.""" - - def __init__(self, data: dict[str, Any]): - from ba.internal import is_browser_likely_available - - ba.set_analytics_screen('Friend Promo Code') - self._width = 650 - self._height = 400 - uiscale = ba.app.ui.uiscale - super().__init__( - root_widget=ba.containerwidget( - size=(self._width, self._height), - color=(0.45, 0.63, 0.15), - transition='in_scale', - scale=( - 1.7 - if uiscale is ba.UIScale.SMALL - else 1.35 - if uiscale is ba.UIScale.MEDIUM - else 1.0 - ), - ) - ) - self._data = copy.deepcopy(data) - ba.playsound(ba.getsound('cashRegister')) - ba.playsound(ba.getsound('swish')) - - self._cancel_button = ba.buttonwidget( - parent=self._root_widget, - scale=0.7, - position=(50, self._height - 50), - size=(60, 60), - label='', - on_activate_call=self.close, - autoselect=True, - color=(0.45, 0.63, 0.15), - icon=ba.gettexture('crossOut'), - iconscale=1.2, - ) - ba.containerwidget( - edit=self._root_widget, cancel_button=self._cancel_button - ) - - ba.textwidget( - parent=self._root_widget, - position=(self._width * 0.5, self._height * 0.8), - size=(0, 0), - color=ba.app.ui.infotextcolor, - scale=1.0, - flatness=1.0, - h_align='center', - v_align='center', - text=ba.Lstr(resource='gatherWindow.shareThisCodeWithFriendsText'), - maxwidth=self._width * 0.85, - ) - - ba.textwidget( - parent=self._root_widget, - position=(self._width * 0.5, self._height * 0.645), - size=(0, 0), - color=(1.0, 3.0, 1.0), - scale=2.0, - h_align='center', - v_align='center', - text=data['code'], - maxwidth=self._width * 0.85, - ) - - award_str: str | ba.Lstr | None - if self._data['awardTickets'] != 0: - award_str = ba.Lstr( - resource='gatherWindow.friendPromoCodeAwardText', - subs=[('${COUNT}', str(self._data['awardTickets']))], - ) - else: - award_str = '' - ba.textwidget( - parent=self._root_widget, - position=(self._width * 0.5, self._height * 0.37), - size=(0, 0), - color=ba.app.ui.infotextcolor, - scale=1.0, - flatness=1.0, - h_align='center', - v_align='center', - text=ba.Lstr( - value='${A}\n${B}\n${C}\n${D}', - subs=[ - ( - '${A}', - ba.Lstr( - resource=( - 'gatherWindow.friendPromoCodeRedeemLongText' - ), - subs=[ - ('${COUNT}', str(self._data['tickets'])), - ( - '${MAX_USES}', - str(self._data['usesRemaining']), - ), - ], - ), - ), - ( - '${B}', - ba.Lstr( - resource=( - 'gatherWindow.friendPromoCodeWhereToEnterText' - ) - ), - ), - ('${C}', award_str), - ( - '${D}', - ba.Lstr( - resource='gatherWindow.friendPromoCodeExpireText', - subs=[ - ( - '${EXPIRE_HOURS}', - str(self._data['expireHours']), - ) - ], - ), - ), - ], - ), - maxwidth=self._width * 0.9, - max_height=self._height * 0.35, - ) - - if is_browser_likely_available(): - xoffs = 0 - ba.buttonwidget( - parent=self._root_widget, - size=(200, 40), - position=(self._width * 0.5 - 100 + xoffs, 39), - autoselect=True, - label=ba.Lstr(resource='gatherWindow.emailItText'), - on_activate_call=ba.WeakCall(self._email), - ) - - def _google_invites(self) -> None: - ba.set_analytics_screen('App Invite UI') - ba.internal.show_app_invite( - ba.Lstr( - resource='gatherWindow.appInviteTitleText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], - ).evaluate(), - ba.Lstr( - resource='gatherWindow.appInviteMessageText', - subs=[ - ('${COUNT}', str(self._data['tickets'])), - ('${NAME}', ba.internal.get_v1_account_name().split()[0]), - ('${APP_NAME}', ba.Lstr(resource='titleText')), - ], - ).evaluate(), - self._data['code'], - ) - - def _email(self) -> None: - import urllib.parse - - # If somehow we got signed out. - if ba.internal.get_v1_account_state() != 'signed_in': - ba.screenmessage( - ba.Lstr(resource='notSignedInText'), color=(1, 0, 0) - ) - ba.playsound(ba.getsound('error')) - return - - ba.set_analytics_screen('Email Friend Code') - subject = ( - ba.Lstr(resource='gatherWindow.friendHasSentPromoCodeText') - .evaluate() - .replace('${NAME}', ba.internal.get_v1_account_name()) - .replace('${APP_NAME}', ba.Lstr(resource='titleText').evaluate()) - .replace('${COUNT}', str(self._data['tickets'])) - ) - body = ( - ba.Lstr(resource='gatherWindow.youHaveBeenSentAPromoCodeText') - .evaluate() - .replace('${APP_NAME}', ba.Lstr(resource='titleText').evaluate()) - + '\n\n' - + str(self._data['code']) - + '\n\n' - ) - body += ( - ( - ba.Lstr(resource='gatherWindow.friendPromoCodeRedeemShortText') - .evaluate() - .replace('${COUNT}', str(self._data['tickets'])) - ) - + '\n\n' - + ba.Lstr(resource='gatherWindow.friendPromoCodeInstructionsText') - .evaluate() - .replace('${APP_NAME}', ba.Lstr(resource='titleText').evaluate()) - + '\n' - + ba.Lstr(resource='gatherWindow.friendPromoCodeExpireText') - .evaluate() - .replace('${EXPIRE_HOURS}', str(self._data['expireHours'])) - + '\n' - + ba.Lstr(resource='enjoyText').evaluate() - ) - ba.open_url( - 'mailto:?subject=' - + urllib.parse.quote(subject) - + '&body=' - + urllib.parse.quote(body) - ) - - def close(self) -> None: - """Close the window.""" - ba.containerwidget(edit=self._root_widget, transition='out_scale') - - -def handle_app_invites_press(force_code: bool = False) -> None: - """(internal)""" - app = ba.app - do_app_invites = ( - app.platform == 'android' - and app.subplatform == 'google' - and ba.internal.get_v1_account_misc_read_val('enableAppInvites', False) - and not app.on_tv - ) - # Update: google's app invites are deprecated. - do_app_invites = False - - if force_code: - do_app_invites = False - - # FIXME: Should update this to grab a code before showing the invite UI. - if do_app_invites: - AppInviteWindow() - else: - ba.screenmessage( - ba.Lstr(resource='gatherWindow.requestingAPromoCodeText'), - color=(0, 1, 0), - ) - - def handle_result(result: dict[str, Any] | None) -> None: - with ba.Context('ui'): - if result is None: - ba.screenmessage( - ba.Lstr(resource='errorText'), color=(1, 0, 0) - ) - ba.playsound(ba.getsound('error')) - else: - ShowFriendCodeWindow(result) - - ba.internal.add_transaction( - { - 'type': 'FRIEND_PROMO_CODE_REQUEST', - 'ali': False, - 'expire_time': time.time() + 10, - }, - callback=handle_result, - ) - ba.internal.run_transactions() diff --git a/assets/src/ba_data/python/bastd/ui/settings/xbox360controller.py b/assets/src/ba_data/python/bastd/ui/settings/xbox360controller.py deleted file mode 100644 index 546f8f9a..00000000 --- a/assets/src/ba_data/python/bastd/ui/settings/xbox360controller.py +++ /dev/null @@ -1,138 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""UI functionality related to using xbox360 controllers.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import ba -import ba.internal - -if TYPE_CHECKING: - pass - - -class XBox360ControllerSettingsWindow(ba.Window): - """UI showing info about xbox 360 controllers.""" - - def __init__(self) -> None: - self._r = 'xbox360ControllersWindow' - width = 700 - height = 300 if ba.internal.is_running_on_fire_tv() else 485 - spacing = 40 - uiscale = ba.app.ui.uiscale - super().__init__( - root_widget=ba.containerwidget( - size=(width, height), - transition='in_right', - scale=( - 1.4 - if uiscale is ba.UIScale.SMALL - else 1.4 - if uiscale is ba.UIScale.MEDIUM - else 1.0 - ), - ) - ) - - btn = ba.buttonwidget( - parent=self._root_widget, - position=(35, height - 65), - size=(120, 60), - scale=0.84, - label=ba.Lstr(resource='backText'), - button_type='back', - autoselect=True, - on_activate_call=self._back, - ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) - - ba.textwidget( - parent=self._root_widget, - position=(width * 0.5, height - 42), - size=(0, 0), - scale=0.85, - text=ba.Lstr( - resource=self._r + '.titleText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], - ), - color=ba.app.ui.title_color, - maxwidth=400, - h_align='center', - v_align='center', - ) - - ba.buttonwidget( - edit=btn, - button_type='backSmall', - size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), - ) - - v = height - 70 - v -= spacing - - if ba.internal.is_running_on_fire_tv(): - ba.textwidget( - parent=self._root_widget, - position=(width * 0.5, height * 0.47), - size=(0, 0), - color=(0.7, 0.9, 0.7, 1.0), - maxwidth=width * 0.95, - max_height=height * 0.75, - scale=0.7, - text=ba.Lstr(resource=self._r + '.ouyaInstructionsText'), - h_align='center', - v_align='center', - ) - else: - ba.textwidget( - parent=self._root_widget, - position=(width * 0.5, v - 1), - size=(0, 0), - color=(0.7, 0.9, 0.7, 1.0), - maxwidth=width * 0.95, - max_height=height * 0.22, - text=ba.Lstr(resource=self._r + '.macInstructionsText'), - scale=0.7, - h_align='center', - v_align='center', - ) - v -= 90 - b_width = 300 - btn = ba.buttonwidget( - parent=self._root_widget, - position=((width - b_width) * 0.5, v - 10), - size=(b_width, 50), - label=ba.Lstr(resource=self._r + '.getDriverText'), - autoselect=True, - on_activate_call=ba.Call( - ba.open_url, - 'https://github.com/360Controller/360Controller/releases', - ), - ) - ba.containerwidget(edit=self._root_widget, start_button=btn) - v -= 60 - ba.textwidget( - parent=self._root_widget, - position=(width * 0.5, v - 85), - size=(0, 0), - color=(0.7, 0.9, 0.7, 1.0), - maxwidth=width * 0.95, - max_height=height * 0.46, - scale=0.7, - text=ba.Lstr(resource=self._r + '.macInstructions2Text'), - h_align='center', - v_align='center', - ) - - def _back(self) -> None: - from bastd.ui.settings import controls - - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.app.ui.set_main_menu_window( - controls.ControlsSettingsWindow( - transition='in_left' - ).get_root_widget() - ) diff --git a/assets/src/ba_data/python/bastd/ui/telnet.py b/assets/src/ba_data/python/bastd/ui/telnet.py deleted file mode 100644 index 80c01274..00000000 --- a/assets/src/ba_data/python/bastd/ui/telnet.py +++ /dev/null @@ -1,67 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""UI functionality for telnet access.""" - -from __future__ import annotations - -import ba -import ba.internal - - -class TelnetAccessRequestWindow(ba.Window): - """Window asking the user whether to allow a telnet connection.""" - - def __init__(self) -> None: - width = 400 - height = 100 - text = ba.Lstr(resource='telnetAccessText') - - uiscale = ba.app.ui.uiscale - super().__init__( - root_widget=ba.containerwidget( - size=(width, height + 40), - transition='in_right', - scale=( - 1.7 - if uiscale is ba.UIScale.SMALL - else 1.3 - if uiscale is ba.UIScale.MEDIUM - else 1.0 - ), - ) - ) - padding = 20 - ba.textwidget( - parent=self._root_widget, - position=(padding, padding + 33), - size=(width - 2 * padding, height - 2 * padding), - h_align='center', - v_align='top', - text=text, - ) - btn = ba.buttonwidget( - parent=self._root_widget, - position=(20, 20), - size=(140, 50), - label=ba.Lstr(resource='denyText'), - on_activate_call=self._cancel, - ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) - ba.containerwidget(edit=self._root_widget, selected_child=btn) - - ba.buttonwidget( - parent=self._root_widget, - position=(width - 155, 20), - size=(140, 50), - label=ba.Lstr(resource='allowText'), - on_activate_call=self._ok, - ) - - def _cancel(self) -> None: - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.internal.set_telnet_access_enabled(False) - - def _ok(self) -> None: - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.internal.set_telnet_access_enabled(True) - ba.screenmessage(ba.Lstr(resource='telnetAccessGrantedText')) diff --git a/assets/src/server/launch_ballisticacore_server.bat b/assets/src/server/launch_ballisticacore_server.bat deleted file mode 100644 index 547324d6..00000000 --- a/assets/src/server/launch_ballisticacore_server.bat +++ /dev/null @@ -1,3 +0,0 @@ -:: Simply run the ballisticacore_server.py script with the bundled -:: Python interpreter. -dist\\python.exe ballisticacore_server.py diff --git a/ballisticacore-cmake/.idea/.name b/ballisticacore-cmake/.idea/.name deleted file mode 100644 index 5aa978d3..00000000 --- a/ballisticacore-cmake/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -BallisticaCore \ No newline at end of file diff --git a/ballisticacore-cmake/.idea/BallisticaCore.iml b/ballisticacore-cmake/.idea/BallisticaCore.iml deleted file mode 100644 index f08604bb..00000000 --- a/ballisticacore-cmake/.idea/BallisticaCore.iml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/ballisticacore-cmake/.idea/scopes/External.xml b/ballisticacore-cmake/.idea/scopes/External.xml deleted file mode 100644 index a140a57c..00000000 --- a/ballisticacore-cmake/.idea/scopes/External.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/ballisticacore-cmake/.idea/scopes/Ignored.xml b/ballisticacore-cmake/.idea/scopes/Ignored.xml deleted file mode 100644 index 55960b0f..00000000 --- a/ballisticacore-cmake/.idea/scopes/Ignored.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/ballisticacore-cmake/CMakeLists.txt b/ballisticacore-cmake/CMakeLists.txt deleted file mode 100644 index 4b79cb15..00000000 --- a/ballisticacore-cmake/CMakeLists.txt +++ /dev/null @@ -1,688 +0,0 @@ -cmake_minimum_required(VERSION 3.12) -project(BallisticaCore) -include(CheckIncludeFile) - -option(HEADLESS "build headless server" OFF) -option(TEST_BUILD "include testing features" OFF) - -# Requiring minimum of C++17 currently. -set(CMAKE_CXX_STANDARD 17) - -if (APPLE) - # Seems as of Mojave we need to explicitly pull in homebrew paths. - # Just hard-coding recommended homebrew install paths for now. - # Is there a more elegant way to do this? - if (CMAKE_SYSTEM_PROCESSOR MATCHES arm64) - list(APPEND CMAKE_PREFIX_PATH /opt/homebrew) - include_directories("/opt/homebrew/include") - link_directories("/opt/homebrew/lib") - else() - list(APPEND CMAKE_PREFIX_PATH /usr/local) - include_directories("/usr/local/include") - link_directories("/usr/local/lib") - endif() - - # On Mac with homebrew it seems that Requesting 3.X when we've got - # 3.(X+1) installed will point us at the 3.(X+1) framework but will attempt - # to load a 3.X library from within it which doesn't exist. So we need - # to be a bit more explicit telling it where to look. Note: this was last - # tested with 3.7; should revisit sometime to make sure still applies. - execute_process(COMMAND "python3.10-config" "--prefix" - OUTPUT_VARIABLE Python_ROOT_DIR - OUTPUT_STRIP_TRAILING_WHITESPACE) - -endif () -find_package (Python 3.10 REQUIRED EXACT COMPONENTS Development) - - -if (HEADLESS) - add_definitions(-DBA_HEADLESS_BUILD=1) - else () - find_package(SDL2 QUIET) - if (SDL2_FOUND) - if ("${SDL2_LIBRARIES}" STREQUAL "") - message(WARNING "SDL2_LIBRARIES wasn't set, manually setting to SDL2::SDL2") - set(SDL2_LIBRARIES "SDL2::SDL2") - endif () - # Getting complaint about space at the end of this on ubuntu16. - string(STRIP "${SDL2_LIBRARIES}" SDL2_LIBRARIES) - else () - message(FATAL_ERROR "SDL2 not found") - endif () - find_package(OpenGL REQUIRED) - find_package(OpenAL REQUIRED) - if (APPLE) - # On mac this sets an include path that we don't need since - # we're using the system framework... should clean this up. - set(OPENAL_INCLUDE_DIR "") - endif () - find_library(OGG_LIBRARY ogg) - find_library(VORBISFILE_LIBRARY vorbisfile) - if (NOT OGG_LIBRARY) - message(FATAL_ERROR "ogg library not found") - endif () - if (NOT VORBISFILE_LIBRARY) - message(FATAL_ERROR "vorbisfile library not found") - endif () - set(EXTRA_INCLUDE_DIRS ${OPENGL_INCLUDE_DIRS} - ${OPENAL_INCLUDE_DIR} ${SDL2_INCLUDE_DIRS}) - set(EXTRA_LIBRARIES ogg vorbisfile ${OPENGL_LIBRARIES} ${OPENAL_LIBRARY}) -endif () - -if (TEST_BUILD) - add_definitions(-DBA_TEST_BUILD=1) -endif () - -# Currently seeing warnings about parameter order changing in GCC 7.1 -# on Raspberry Pi builds. We never need to care about C++ abi compatibility -# so just silencing them for now. Can maybe remove this later if they stop. -if (CMAKE_CXX_COMPILER_ID MATCHES GNU) - set(CMAKE_CXX_FLAGS - "${CMAKE_CXX_FLAGS} -Wno-psabi") -endif() - -set(BA_SRC_ROOT ../src) -include_directories(${BA_SRC_ROOT}) -add_compile_options(-include ballistica/config/config_cmake.h) - -if (CMAKE_BUILD_TYPE MATCHES Debug) - add_definitions(-DBA_DEBUG_BUILD=1) -else () - # It seems that cmake can choose -O2 sometimes and -O3 sometimes - # for release builds (depending on Release vs RelWithDebInfo, etc). - # Let's keep all our non-debug builds consistent at -O3 for now; can - # revisit if it causes problems. - add_definitions(-O3) -endif () - -set(ODE_SRC_ROOT ${BA_SRC_ROOT}/external/open_dynamics_engine-ef) - -add_library(ode - ${ODE_SRC_ROOT}/ode/IceAABB.cpp - ${ODE_SRC_ROOT}/ode/IceContainer.cpp - ${ODE_SRC_ROOT}/ode/IceHPoint.cpp - ${ODE_SRC_ROOT}/ode/IceIndexedTriangle.cpp - ${ODE_SRC_ROOT}/ode/IceMatrix3x3.cpp - ${ODE_SRC_ROOT}/ode/IceMatrix4x4.cpp - ${ODE_SRC_ROOT}/ode/IceOBB.cpp - ${ODE_SRC_ROOT}/ode/IcePlane.cpp - ${ODE_SRC_ROOT}/ode/IcePoint.cpp - ${ODE_SRC_ROOT}/ode/IceRandom.cpp - ${ODE_SRC_ROOT}/ode/IceRay.cpp - ${ODE_SRC_ROOT}/ode/IceRevisitedRadix.cpp - ${ODE_SRC_ROOT}/ode/IceSegment.cpp - ${ODE_SRC_ROOT}/ode/IceTriangle.cpp - ${ODE_SRC_ROOT}/ode/IceUtils.cpp - ${ODE_SRC_ROOT}/ode/ode.cpp - ${ODE_SRC_ROOT}/ode/ode_array.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_cylinder_box.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_cylinder_plane.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_cylinder_sphere.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_cylinder_trimesh.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_kernel.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_quadtreespace.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_sapspace.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_space.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_std.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_transform.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_trimesh.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_trimesh_box.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_trimesh_ccylinder.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_trimesh_distance.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_trimesh_plane.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_trimesh_ray.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_trimesh_sphere.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_trimesh_trimesh.cpp - ${ODE_SRC_ROOT}/ode/ode_collision_util.cpp - ${ODE_SRC_ROOT}/ode/ode_error.cpp - ${ODE_SRC_ROOT}/ode/ode_export-diff.cpp - ${ODE_SRC_ROOT}/ode/ode_fastdot.cpp - ${ODE_SRC_ROOT}/ode/ode_fastldlt.cpp - ${ODE_SRC_ROOT}/ode/ode_fastlsolve.cpp - ${ODE_SRC_ROOT}/ode/ode_fastltsolve.cpp - ${ODE_SRC_ROOT}/ode/ode_joint.cpp - ${ODE_SRC_ROOT}/ode/ode_lcp.cpp - ${ODE_SRC_ROOT}/ode/ode_mass.cpp - ${ODE_SRC_ROOT}/ode/ode_mat.cpp - ${ODE_SRC_ROOT}/ode/ode_math.cpp - ${ODE_SRC_ROOT}/ode/ode_matrix.cpp - ${ODE_SRC_ROOT}/ode/ode_memory.cpp - ${ODE_SRC_ROOT}/ode/ode_misc.cpp - ${ODE_SRC_ROOT}/ode/ode_obstack.cpp - ${ODE_SRC_ROOT}/ode/ode_quickstep.cpp - ${ODE_SRC_ROOT}/ode/ode_rotation.cpp - ${ODE_SRC_ROOT}/ode/ode_step.cpp - ${ODE_SRC_ROOT}/ode/ode_stepfast.cpp - ${ODE_SRC_ROOT}/ode/ode_timer.cpp - ${ODE_SRC_ROOT}/ode/ode_util.cpp - ${ODE_SRC_ROOT}/ode/OPC_AABBCollider.cpp - ${ODE_SRC_ROOT}/ode/OPC_AABBTree.cpp - ${ODE_SRC_ROOT}/ode/OPC_BaseModel.cpp - ${ODE_SRC_ROOT}/ode/OPC_BoxPruning.cpp - ${ODE_SRC_ROOT}/ode/OPC_Collider.cpp - ${ODE_SRC_ROOT}/ode/OPC_HybridModel.cpp - ${ODE_SRC_ROOT}/ode/OPC_LSSCollider.cpp - ${ODE_SRC_ROOT}/ode/OPC_MeshInterface.cpp - ${ODE_SRC_ROOT}/ode/OPC_Model.cpp - ${ODE_SRC_ROOT}/ode/OPC_OBBCollider.cpp - ${ODE_SRC_ROOT}/ode/OPC_OptimizedTree.cpp - ${ODE_SRC_ROOT}/ode/OPC_PlanesCollider.cpp - ${ODE_SRC_ROOT}/ode/OPC_RayCollider.cpp - ${ODE_SRC_ROOT}/ode/OPC_SphereCollider.cpp - ${ODE_SRC_ROOT}/ode/OPC_SweepAndPrune.cpp - ${ODE_SRC_ROOT}/ode/OPC_TreeBuilders.cpp - ${ODE_SRC_ROOT}/ode/OPC_TreeCollider.cpp - ${ODE_SRC_ROOT}/ode/OPC_VolumeCollider.cpp - ${ODE_SRC_ROOT}/ode/Opcode.cpp - ) -target_include_directories(ode PRIVATE ${ODE_SRC_ROOT}) - -# NOTE: There used to be an issue with optimized GCC builds where mesh -# collisions would fail randomly, leading to characters falling through -# floors somewhat regularly. For this reason I was limiting optimization to -# -O1 for the rigid body library. However, as of April 2021, all seems -# well when testing on arm64 and x86-64 linux builds. (I think) -# The last time I remember seeing this bug was around 2016 I believe, but I -# haven't looked for it since. Perhaps GCC was fixed or perhaps the error -# was limited to 32 bit x86 builds; in either case we should be good, as -# we're no longer building any 32 bit x86 builds using GCC. -# Keeping this in here commented out just in case it rears its ugly head -# again though. -# if (CMAKE_BUILD_TYPE MATCHES Release) -# if (CMAKE_CXX_COMPILER_ID MATCHES GNU) -# target_compile_options(ode PRIVATE -O1) -# endif() -# endif () - -# BallisticaCore binary. -add_executable(ballisticacore - ${BA_SRC_ROOT}/external/qr_code_generator/QrCode.cpp - # AUTOGENERATED_PUBLIC_BEGIN (this section is managed by the "update_project" tool) - ${BA_SRC_ROOT}/ballistica/app/app.cc - ${BA_SRC_ROOT}/ballistica/app/app.h - ${BA_SRC_ROOT}/ballistica/app/app_config.cc - ${BA_SRC_ROOT}/ballistica/app/app_config.h - ${BA_SRC_ROOT}/ballistica/app/app_flavor.cc - ${BA_SRC_ROOT}/ballistica/app/app_flavor.h - ${BA_SRC_ROOT}/ballistica/app/app_flavor_headless.cc - ${BA_SRC_ROOT}/ballistica/app/app_flavor_headless.h - ${BA_SRC_ROOT}/ballistica/app/app_flavor_vr.cc - ${BA_SRC_ROOT}/ballistica/app/app_flavor_vr.h - ${BA_SRC_ROOT}/ballistica/app/stress_test.cc - ${BA_SRC_ROOT}/ballistica/app/stress_test.h - ${BA_SRC_ROOT}/ballistica/assets/assets.cc - ${BA_SRC_ROOT}/ballistica/assets/assets.h - ${BA_SRC_ROOT}/ballistica/assets/assets_server.cc - ${BA_SRC_ROOT}/ballistica/assets/assets_server.h - ${BA_SRC_ROOT}/ballistica/assets/component/asset_component.cc - ${BA_SRC_ROOT}/ballistica/assets/component/asset_component.h - ${BA_SRC_ROOT}/ballistica/assets/component/collide_model.cc - ${BA_SRC_ROOT}/ballistica/assets/component/collide_model.h - ${BA_SRC_ROOT}/ballistica/assets/component/cube_map_texture.cc - ${BA_SRC_ROOT}/ballistica/assets/component/cube_map_texture.h - ${BA_SRC_ROOT}/ballistica/assets/component/data.cc - ${BA_SRC_ROOT}/ballistica/assets/component/data.h - ${BA_SRC_ROOT}/ballistica/assets/component/model.cc - ${BA_SRC_ROOT}/ballistica/assets/component/model.h - ${BA_SRC_ROOT}/ballistica/assets/component/sound.cc - ${BA_SRC_ROOT}/ballistica/assets/component/sound.h - ${BA_SRC_ROOT}/ballistica/assets/component/texture.cc - ${BA_SRC_ROOT}/ballistica/assets/component/texture.h - ${BA_SRC_ROOT}/ballistica/assets/data/asset_component_data.cc - ${BA_SRC_ROOT}/ballistica/assets/data/asset_component_data.h - ${BA_SRC_ROOT}/ballistica/assets/data/collide_model_data.cc - ${BA_SRC_ROOT}/ballistica/assets/data/collide_model_data.h - ${BA_SRC_ROOT}/ballistica/assets/data/data_data.cc - ${BA_SRC_ROOT}/ballistica/assets/data/data_data.h - ${BA_SRC_ROOT}/ballistica/assets/data/model_data.cc - ${BA_SRC_ROOT}/ballistica/assets/data/model_data.h - ${BA_SRC_ROOT}/ballistica/assets/data/model_renderer_data.h - ${BA_SRC_ROOT}/ballistica/assets/data/sound_data.cc - ${BA_SRC_ROOT}/ballistica/assets/data/sound_data.h - ${BA_SRC_ROOT}/ballistica/assets/data/texture_data.cc - ${BA_SRC_ROOT}/ballistica/assets/data/texture_data.h - ${BA_SRC_ROOT}/ballistica/assets/data/texture_preload_data.cc - ${BA_SRC_ROOT}/ballistica/assets/data/texture_preload_data.h - ${BA_SRC_ROOT}/ballistica/assets/data/texture_renderer_data.h - ${BA_SRC_ROOT}/ballistica/audio/al_sys.cc - ${BA_SRC_ROOT}/ballistica/audio/al_sys.h - ${BA_SRC_ROOT}/ballistica/audio/audio.cc - ${BA_SRC_ROOT}/ballistica/audio/audio.h - ${BA_SRC_ROOT}/ballistica/audio/audio_server.cc - ${BA_SRC_ROOT}/ballistica/audio/audio_server.h - ${BA_SRC_ROOT}/ballistica/audio/audio_source.cc - ${BA_SRC_ROOT}/ballistica/audio/audio_source.h - ${BA_SRC_ROOT}/ballistica/audio/audio_streamer.cc - ${BA_SRC_ROOT}/ballistica/audio/audio_streamer.h - ${BA_SRC_ROOT}/ballistica/audio/ogg_stream.cc - ${BA_SRC_ROOT}/ballistica/audio/ogg_stream.h - ${BA_SRC_ROOT}/ballistica/ballistica.cc - ${BA_SRC_ROOT}/ballistica/ballistica.h - ${BA_SRC_ROOT}/ballistica/config/config_cmake.h - ${BA_SRC_ROOT}/ballistica/config/config_common.h - ${BA_SRC_ROOT}/ballistica/config/config_windows_common.h - ${BA_SRC_ROOT}/ballistica/config/config_windows_generic.h - ${BA_SRC_ROOT}/ballistica/config/config_windows_headless.h - ${BA_SRC_ROOT}/ballistica/core/context.cc - ${BA_SRC_ROOT}/ballistica/core/context.h - ${BA_SRC_ROOT}/ballistica/core/exception.cc - ${BA_SRC_ROOT}/ballistica/core/exception.h - ${BA_SRC_ROOT}/ballistica/core/fatal_error.cc - ${BA_SRC_ROOT}/ballistica/core/fatal_error.h - ${BA_SRC_ROOT}/ballistica/core/inline.cc - ${BA_SRC_ROOT}/ballistica/core/inline.h - ${BA_SRC_ROOT}/ballistica/core/logging.cc - ${BA_SRC_ROOT}/ballistica/core/logging.h - ${BA_SRC_ROOT}/ballistica/core/macros.cc - ${BA_SRC_ROOT}/ballistica/core/macros.h - ${BA_SRC_ROOT}/ballistica/core/object.cc - ${BA_SRC_ROOT}/ballistica/core/object.h - ${BA_SRC_ROOT}/ballistica/core/thread.cc - ${BA_SRC_ROOT}/ballistica/core/thread.h - ${BA_SRC_ROOT}/ballistica/core/types.h - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics.cc - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics.h - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_draw_snapshot.h - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_fuse.cc - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_fuse.h - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_fuse_data.h - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_height_cache.cc - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_height_cache.h - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_server.cc - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_server.h - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_shadow.cc - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_shadow.h - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_shadow_data.h - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_volume_light.cc - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_volume_light.h - ${BA_SRC_ROOT}/ballistica/dynamics/bg/bg_dynamics_volume_light_data.h - ${BA_SRC_ROOT}/ballistica/dynamics/collision.h - ${BA_SRC_ROOT}/ballistica/dynamics/collision_cache.cc - ${BA_SRC_ROOT}/ballistica/dynamics/collision_cache.h - ${BA_SRC_ROOT}/ballistica/dynamics/dynamics.cc - ${BA_SRC_ROOT}/ballistica/dynamics/dynamics.h - ${BA_SRC_ROOT}/ballistica/dynamics/material/impact_sound_material_action.cc - ${BA_SRC_ROOT}/ballistica/dynamics/material/impact_sound_material_action.h - ${BA_SRC_ROOT}/ballistica/dynamics/material/material.cc - ${BA_SRC_ROOT}/ballistica/dynamics/material/material.h - ${BA_SRC_ROOT}/ballistica/dynamics/material/material_action.h - ${BA_SRC_ROOT}/ballistica/dynamics/material/material_component.cc - ${BA_SRC_ROOT}/ballistica/dynamics/material/material_component.h - ${BA_SRC_ROOT}/ballistica/dynamics/material/material_condition_node.cc - ${BA_SRC_ROOT}/ballistica/dynamics/material/material_condition_node.h - ${BA_SRC_ROOT}/ballistica/dynamics/material/material_context.cc - ${BA_SRC_ROOT}/ballistica/dynamics/material/material_context.h - ${BA_SRC_ROOT}/ballistica/dynamics/material/node_message_material_action.cc - ${BA_SRC_ROOT}/ballistica/dynamics/material/node_message_material_action.h - ${BA_SRC_ROOT}/ballistica/dynamics/material/node_mod_material_action.cc - ${BA_SRC_ROOT}/ballistica/dynamics/material/node_mod_material_action.h - ${BA_SRC_ROOT}/ballistica/dynamics/material/node_user_message_material_action.cc - ${BA_SRC_ROOT}/ballistica/dynamics/material/node_user_message_material_action.h - ${BA_SRC_ROOT}/ballistica/dynamics/material/part_mod_material_action.cc - ${BA_SRC_ROOT}/ballistica/dynamics/material/part_mod_material_action.h - ${BA_SRC_ROOT}/ballistica/dynamics/material/python_call_material_action.cc - ${BA_SRC_ROOT}/ballistica/dynamics/material/python_call_material_action.h - ${BA_SRC_ROOT}/ballistica/dynamics/material/roll_sound_material_action.cc - ${BA_SRC_ROOT}/ballistica/dynamics/material/roll_sound_material_action.h - ${BA_SRC_ROOT}/ballistica/dynamics/material/skid_sound_material_action.cc - ${BA_SRC_ROOT}/ballistica/dynamics/material/skid_sound_material_action.h - ${BA_SRC_ROOT}/ballistica/dynamics/material/sound_material_action.cc - ${BA_SRC_ROOT}/ballistica/dynamics/material/sound_material_action.h - ${BA_SRC_ROOT}/ballistica/dynamics/part.cc - ${BA_SRC_ROOT}/ballistica/dynamics/part.h - ${BA_SRC_ROOT}/ballistica/dynamics/rigid_body.cc - ${BA_SRC_ROOT}/ballistica/dynamics/rigid_body.h - ${BA_SRC_ROOT}/ballistica/generic/base64.cc - ${BA_SRC_ROOT}/ballistica/generic/base64.h - ${BA_SRC_ROOT}/ballistica/generic/buffer.h - ${BA_SRC_ROOT}/ballistica/generic/huffman.cc - ${BA_SRC_ROOT}/ballistica/generic/huffman.h - ${BA_SRC_ROOT}/ballistica/generic/json.cc - ${BA_SRC_ROOT}/ballistica/generic/json.h - ${BA_SRC_ROOT}/ballistica/generic/lambda_runnable.h - ${BA_SRC_ROOT}/ballistica/generic/real_timer.h - ${BA_SRC_ROOT}/ballistica/generic/runnable.cc - ${BA_SRC_ROOT}/ballistica/generic/runnable.h - ${BA_SRC_ROOT}/ballistica/generic/timer.cc - ${BA_SRC_ROOT}/ballistica/generic/timer.h - ${BA_SRC_ROOT}/ballistica/generic/timer_list.cc - ${BA_SRC_ROOT}/ballistica/generic/timer_list.h - ${BA_SRC_ROOT}/ballistica/generic/utf8.cc - ${BA_SRC_ROOT}/ballistica/generic/utf8.h - ${BA_SRC_ROOT}/ballistica/generic/utils.cc - ${BA_SRC_ROOT}/ballistica/generic/utils.h - ${BA_SRC_ROOT}/ballistica/graphics/area_of_interest.cc - ${BA_SRC_ROOT}/ballistica/graphics/area_of_interest.h - ${BA_SRC_ROOT}/ballistica/graphics/camera.cc - ${BA_SRC_ROOT}/ballistica/graphics/camera.h - ${BA_SRC_ROOT}/ballistica/graphics/component/empty_component.h - ${BA_SRC_ROOT}/ballistica/graphics/component/object_component.cc - ${BA_SRC_ROOT}/ballistica/graphics/component/object_component.h - ${BA_SRC_ROOT}/ballistica/graphics/component/post_process_component.cc - ${BA_SRC_ROOT}/ballistica/graphics/component/post_process_component.h - ${BA_SRC_ROOT}/ballistica/graphics/component/render_component.cc - ${BA_SRC_ROOT}/ballistica/graphics/component/render_component.h - ${BA_SRC_ROOT}/ballistica/graphics/component/shield_component.cc - ${BA_SRC_ROOT}/ballistica/graphics/component/shield_component.h - ${BA_SRC_ROOT}/ballistica/graphics/component/simple_component.cc - ${BA_SRC_ROOT}/ballistica/graphics/component/simple_component.h - ${BA_SRC_ROOT}/ballistica/graphics/component/smoke_component.cc - ${BA_SRC_ROOT}/ballistica/graphics/component/smoke_component.h - ${BA_SRC_ROOT}/ballistica/graphics/component/special_component.cc - ${BA_SRC_ROOT}/ballistica/graphics/component/special_component.h - ${BA_SRC_ROOT}/ballistica/graphics/component/sprite_component.cc - ${BA_SRC_ROOT}/ballistica/graphics/component/sprite_component.h - ${BA_SRC_ROOT}/ballistica/graphics/frame_def.cc - ${BA_SRC_ROOT}/ballistica/graphics/frame_def.h - ${BA_SRC_ROOT}/ballistica/graphics/framebuffer.h - ${BA_SRC_ROOT}/ballistica/graphics/gl/gl_sys.cc - ${BA_SRC_ROOT}/ballistica/graphics/gl/gl_sys.h - ${BA_SRC_ROOT}/ballistica/graphics/gl/renderer_gl.cc - ${BA_SRC_ROOT}/ballistica/graphics/gl/renderer_gl.h - ${BA_SRC_ROOT}/ballistica/graphics/graphics.cc - ${BA_SRC_ROOT}/ballistica/graphics/graphics.h - ${BA_SRC_ROOT}/ballistica/graphics/graphics_server.cc - ${BA_SRC_ROOT}/ballistica/graphics/graphics_server.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/image_mesh.cc - ${BA_SRC_ROOT}/ballistica/graphics/mesh/image_mesh.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_buffer.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_buffer_base.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_buffer_vertex_simple_full.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_buffer_vertex_smoke_full.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_buffer_vertex_sprite.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_data.cc - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_data.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_data_client_handle.cc - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_data_client_handle.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_index_buffer_16.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_index_buffer_32.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed_base.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed_dual_texture_full.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed_object_split.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed_simple_full.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed_simple_split.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed_smoke_full.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_indexed_static_dynamic.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_non_indexed.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/mesh_renderer_data.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/sprite_mesh.h - ${BA_SRC_ROOT}/ballistica/graphics/mesh/text_mesh.cc - ${BA_SRC_ROOT}/ballistica/graphics/mesh/text_mesh.h - ${BA_SRC_ROOT}/ballistica/graphics/net_graph.cc - ${BA_SRC_ROOT}/ballistica/graphics/net_graph.h - ${BA_SRC_ROOT}/ballistica/graphics/render_command_buffer.h - ${BA_SRC_ROOT}/ballistica/graphics/render_pass.cc - ${BA_SRC_ROOT}/ballistica/graphics/render_pass.h - ${BA_SRC_ROOT}/ballistica/graphics/render_target.cc - ${BA_SRC_ROOT}/ballistica/graphics/render_target.h - ${BA_SRC_ROOT}/ballistica/graphics/renderer.cc - ${BA_SRC_ROOT}/ballistica/graphics/renderer.h - ${BA_SRC_ROOT}/ballistica/graphics/text/font_page_map_data.h - ${BA_SRC_ROOT}/ballistica/graphics/text/text_graphics.cc - ${BA_SRC_ROOT}/ballistica/graphics/text/text_graphics.h - ${BA_SRC_ROOT}/ballistica/graphics/text/text_group.cc - ${BA_SRC_ROOT}/ballistica/graphics/text/text_group.h - ${BA_SRC_ROOT}/ballistica/graphics/text/text_packer.cc - ${BA_SRC_ROOT}/ballistica/graphics/text/text_packer.h - ${BA_SRC_ROOT}/ballistica/graphics/texture/dds.cc - ${BA_SRC_ROOT}/ballistica/graphics/texture/dds.h - ${BA_SRC_ROOT}/ballistica/graphics/texture/ktx.cc - ${BA_SRC_ROOT}/ballistica/graphics/texture/ktx.h - ${BA_SRC_ROOT}/ballistica/graphics/texture/pvr.cc - ${BA_SRC_ROOT}/ballistica/graphics/texture/pvr.h - ${BA_SRC_ROOT}/ballistica/graphics/vr_graphics.cc - ${BA_SRC_ROOT}/ballistica/graphics/vr_graphics.h - ${BA_SRC_ROOT}/ballistica/input/device/client_input_device.cc - ${BA_SRC_ROOT}/ballistica/input/device/client_input_device.h - ${BA_SRC_ROOT}/ballistica/input/device/input_device.cc - ${BA_SRC_ROOT}/ballistica/input/device/input_device.h - ${BA_SRC_ROOT}/ballistica/input/device/joystick.cc - ${BA_SRC_ROOT}/ballistica/input/device/joystick.h - ${BA_SRC_ROOT}/ballistica/input/device/keyboard_input.cc - ${BA_SRC_ROOT}/ballistica/input/device/keyboard_input.h - ${BA_SRC_ROOT}/ballistica/input/device/test_input.cc - ${BA_SRC_ROOT}/ballistica/input/device/test_input.h - ${BA_SRC_ROOT}/ballistica/input/device/touch_input.cc - ${BA_SRC_ROOT}/ballistica/input/device/touch_input.h - ${BA_SRC_ROOT}/ballistica/input/input.cc - ${BA_SRC_ROOT}/ballistica/input/input.h - ${BA_SRC_ROOT}/ballistica/input/remote_app.cc - ${BA_SRC_ROOT}/ballistica/input/remote_app.h - ${BA_SRC_ROOT}/ballistica/internal/app_internal.h - ${BA_SRC_ROOT}/ballistica/logic/client_controller_interface.h - ${BA_SRC_ROOT}/ballistica/logic/connection/connection.cc - ${BA_SRC_ROOT}/ballistica/logic/connection/connection.h - ${BA_SRC_ROOT}/ballistica/logic/connection/connection_set.cc - ${BA_SRC_ROOT}/ballistica/logic/connection/connection_set.h - ${BA_SRC_ROOT}/ballistica/logic/connection/connection_to_client.cc - ${BA_SRC_ROOT}/ballistica/logic/connection/connection_to_client.h - ${BA_SRC_ROOT}/ballistica/logic/connection/connection_to_client_udp.cc - ${BA_SRC_ROOT}/ballistica/logic/connection/connection_to_client_udp.h - ${BA_SRC_ROOT}/ballistica/logic/connection/connection_to_host.cc - ${BA_SRC_ROOT}/ballistica/logic/connection/connection_to_host.h - ${BA_SRC_ROOT}/ballistica/logic/connection/connection_to_host_udp.cc - ${BA_SRC_ROOT}/ballistica/logic/connection/connection_to_host_udp.h - ${BA_SRC_ROOT}/ballistica/logic/host_activity.cc - ${BA_SRC_ROOT}/ballistica/logic/host_activity.h - ${BA_SRC_ROOT}/ballistica/logic/logic.cc - ${BA_SRC_ROOT}/ballistica/logic/logic.h - ${BA_SRC_ROOT}/ballistica/logic/player.cc - ${BA_SRC_ROOT}/ballistica/logic/player.h - ${BA_SRC_ROOT}/ballistica/logic/player_spec.cc - ${BA_SRC_ROOT}/ballistica/logic/player_spec.h - ${BA_SRC_ROOT}/ballistica/logic/session/client_session.cc - ${BA_SRC_ROOT}/ballistica/logic/session/client_session.h - ${BA_SRC_ROOT}/ballistica/logic/session/host_session.cc - ${BA_SRC_ROOT}/ballistica/logic/session/host_session.h - ${BA_SRC_ROOT}/ballistica/logic/session/net_client_session.cc - ${BA_SRC_ROOT}/ballistica/logic/session/net_client_session.h - ${BA_SRC_ROOT}/ballistica/logic/session/replay_client_session.cc - ${BA_SRC_ROOT}/ballistica/logic/session/replay_client_session.h - ${BA_SRC_ROOT}/ballistica/logic/session/session.cc - ${BA_SRC_ROOT}/ballistica/logic/session/session.h - ${BA_SRC_ROOT}/ballistica/logic/v1_account.cc - ${BA_SRC_ROOT}/ballistica/logic/v1_account.h - ${BA_SRC_ROOT}/ballistica/math/matrix44f.cc - ${BA_SRC_ROOT}/ballistica/math/matrix44f.h - ${BA_SRC_ROOT}/ballistica/math/point2d.h - ${BA_SRC_ROOT}/ballistica/math/random.cc - ${BA_SRC_ROOT}/ballistica/math/random.h - ${BA_SRC_ROOT}/ballistica/math/rect.h - ${BA_SRC_ROOT}/ballistica/math/vector2f.h - ${BA_SRC_ROOT}/ballistica/math/vector3f.cc - ${BA_SRC_ROOT}/ballistica/math/vector3f.h - ${BA_SRC_ROOT}/ballistica/math/vector4f.h - ${BA_SRC_ROOT}/ballistica/networking/network_reader.cc - ${BA_SRC_ROOT}/ballistica/networking/network_reader.h - ${BA_SRC_ROOT}/ballistica/networking/network_writer.cc - ${BA_SRC_ROOT}/ballistica/networking/network_writer.h - ${BA_SRC_ROOT}/ballistica/networking/networking.cc - ${BA_SRC_ROOT}/ballistica/networking/networking.h - ${BA_SRC_ROOT}/ballistica/networking/networking_sys.h - ${BA_SRC_ROOT}/ballistica/networking/sockaddr.cc - ${BA_SRC_ROOT}/ballistica/networking/sockaddr.h - ${BA_SRC_ROOT}/ballistica/networking/telnet_server.cc - ${BA_SRC_ROOT}/ballistica/networking/telnet_server.h - ${BA_SRC_ROOT}/ballistica/platform/apple/platform_apple.h - ${BA_SRC_ROOT}/ballistica/platform/linux/platform_linux.cc - ${BA_SRC_ROOT}/ballistica/platform/linux/platform_linux.h - ${BA_SRC_ROOT}/ballistica/platform/min_sdl.h - ${BA_SRC_ROOT}/ballistica/platform/platform.cc - ${BA_SRC_ROOT}/ballistica/platform/platform.h - ${BA_SRC_ROOT}/ballistica/platform/sdl/sdl_app.cc - ${BA_SRC_ROOT}/ballistica/platform/sdl/sdl_app.h - ${BA_SRC_ROOT}/ballistica/platform/stdio_console.cc - ${BA_SRC_ROOT}/ballistica/platform/stdio_console.h - ${BA_SRC_ROOT}/ballistica/platform/windows/platform_windows.cc - ${BA_SRC_ROOT}/ballistica/platform/windows/platform_windows.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_activity_data.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_activity_data.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_collide_model.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_collide_model.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_context.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_context.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_context_call.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_context_call.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_data.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_data.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_input_device.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_input_device.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_material.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_material.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_model.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_model.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_node.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_node.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_session_data.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_session_data.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_session_player.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_session_player.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_sound.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_sound.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_texture.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_texture.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_timer.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_timer.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_vec3.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_vec3.h - ${BA_SRC_ROOT}/ballistica/python/class/python_class_widget.cc - ${BA_SRC_ROOT}/ballistica/python/class/python_class_widget.h - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_app.cc - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_app.h - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_assets.cc - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_assets.h - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_gameplay.cc - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_gameplay.h - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_graphics.cc - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_graphics.h - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_input.cc - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_input.h - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_networking.cc - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_networking.h - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_system.cc - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_system.h - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_ui.cc - ${BA_SRC_ROOT}/ballistica/python/methods/python_methods_ui.h - ${BA_SRC_ROOT}/ballistica/python/python.cc - ${BA_SRC_ROOT}/ballistica/python/python.h - ${BA_SRC_ROOT}/ballistica/python/python_command.cc - ${BA_SRC_ROOT}/ballistica/python/python_command.h - ${BA_SRC_ROOT}/ballistica/python/python_context_call.cc - ${BA_SRC_ROOT}/ballistica/python/python_context_call.h - ${BA_SRC_ROOT}/ballistica/python/python_context_call_runnable.h - ${BA_SRC_ROOT}/ballistica/python/python_ref.cc - ${BA_SRC_ROOT}/ballistica/python/python_ref.h - ${BA_SRC_ROOT}/ballistica/python/python_sys.h - ${BA_SRC_ROOT}/ballistica/scene/node/anim_curve_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/anim_curve_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/bomb_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/bomb_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/combine_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/combine_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/explosion_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/explosion_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/flag_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/flag_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/flash_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/flash_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/globals_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/globals_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/image_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/image_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/light_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/light_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/locator_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/locator_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/math_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/math_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/node.h - ${BA_SRC_ROOT}/ballistica/scene/node/node_attribute.cc - ${BA_SRC_ROOT}/ballistica/scene/node/node_attribute.h - ${BA_SRC_ROOT}/ballistica/scene/node/node_attribute_connection.cc - ${BA_SRC_ROOT}/ballistica/scene/node/node_attribute_connection.h - ${BA_SRC_ROOT}/ballistica/scene/node/node_type.h - ${BA_SRC_ROOT}/ballistica/scene/node/null_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/null_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/player_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/player_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/prop_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/prop_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/region_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/region_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/scorch_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/scorch_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/session_globals_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/session_globals_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/shield_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/shield_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/sound_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/sound_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/spaz_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/spaz_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/terrain_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/terrain_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/text_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/text_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/texture_sequence_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/texture_sequence_node.h - ${BA_SRC_ROOT}/ballistica/scene/node/time_display_node.cc - ${BA_SRC_ROOT}/ballistica/scene/node/time_display_node.h - ${BA_SRC_ROOT}/ballistica/scene/scene.cc - ${BA_SRC_ROOT}/ballistica/scene/scene.h - ${BA_SRC_ROOT}/ballistica/scene/scene_stream.cc - ${BA_SRC_ROOT}/ballistica/scene/scene_stream.h - ${BA_SRC_ROOT}/ballistica/scene/v1/scene_v1.cc - ${BA_SRC_ROOT}/ballistica/scene/v1/scene_v1.h - ${BA_SRC_ROOT}/ballistica/ui/console.cc - ${BA_SRC_ROOT}/ballistica/ui/console.h - ${BA_SRC_ROOT}/ballistica/ui/root_ui.cc - ${BA_SRC_ROOT}/ballistica/ui/root_ui.h - ${BA_SRC_ROOT}/ballistica/ui/ui.cc - ${BA_SRC_ROOT}/ballistica/ui/ui.h - ${BA_SRC_ROOT}/ballistica/ui/widget/button_widget.cc - ${BA_SRC_ROOT}/ballistica/ui/widget/button_widget.h - ${BA_SRC_ROOT}/ballistica/ui/widget/check_box_widget.cc - ${BA_SRC_ROOT}/ballistica/ui/widget/check_box_widget.h - ${BA_SRC_ROOT}/ballistica/ui/widget/column_widget.cc - ${BA_SRC_ROOT}/ballistica/ui/widget/column_widget.h - ${BA_SRC_ROOT}/ballistica/ui/widget/container_widget.cc - ${BA_SRC_ROOT}/ballistica/ui/widget/container_widget.h - ${BA_SRC_ROOT}/ballistica/ui/widget/h_scroll_widget.cc - ${BA_SRC_ROOT}/ballistica/ui/widget/h_scroll_widget.h - ${BA_SRC_ROOT}/ballistica/ui/widget/image_widget.cc - ${BA_SRC_ROOT}/ballistica/ui/widget/image_widget.h - ${BA_SRC_ROOT}/ballistica/ui/widget/root_widget.cc - ${BA_SRC_ROOT}/ballistica/ui/widget/root_widget.h - ${BA_SRC_ROOT}/ballistica/ui/widget/row_widget.cc - ${BA_SRC_ROOT}/ballistica/ui/widget/row_widget.h - ${BA_SRC_ROOT}/ballistica/ui/widget/scroll_widget.cc - ${BA_SRC_ROOT}/ballistica/ui/widget/scroll_widget.h - ${BA_SRC_ROOT}/ballistica/ui/widget/stack_widget.cc - ${BA_SRC_ROOT}/ballistica/ui/widget/stack_widget.h - ${BA_SRC_ROOT}/ballistica/ui/widget/text_widget.cc - ${BA_SRC_ROOT}/ballistica/ui/widget/text_widget.h - ${BA_SRC_ROOT}/ballistica/ui/widget/widget.cc - ${BA_SRC_ROOT}/ballistica/ui/widget/widget.h - # AUTOGENERATED_PUBLIC_END - ) - -target_include_directories(ballisticacore PRIVATE - ${Python_INCLUDE_DIRS} - ${BA_SRC_ROOT}/external/open_dynamics_engine-ef - ${EXTRA_INCLUDE_DIRS} - ) - -target_link_libraries(ballisticacore PRIVATE - ${CMAKE_CURRENT_BINARY_DIR}/prefablib/libballisticacore_internal.a ode pthread ${Python_LIBRARIES} - ${SDL2_LIBRARIES} ${EXTRA_LIBRARIES} dl) - -# Hack for building on rpi (might be due to my manually built Python 3.8) -# Hopefully can remove later... -if(EXISTS "/home/pi") -target_link_libraries(ballisticacore PRIVATE dl util) -endif() diff --git a/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj b/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj deleted file mode 100644 index 18e36464..00000000 --- a/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj +++ /dev/null @@ -1,758 +0,0 @@ - - - - - Debug - Win32 - - - Debug - x64 - - - Release - Win32 - - - Release - x64 - - - - {B0B090EA-92E7-457D-9785-ACDF917F9C93} - Win32Proj - BallisticaCoreGeneric - 10.0 - - - - Application - true - Unicode - v142 - - - Application - true - Unicode - v142 - - - Application - false - true - Unicode - v142 - - - Application - false - true - Unicode - v142 - - - - - - - - - - - - - - - - - - - true - $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ - $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ - - - true - $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ - $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ - - - false - $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ - $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ - - - false - $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ - $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ - - - - Use - Level3 - Disabled - WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) - stdafx.h - true - false - SyncCThrow - true - ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef;../../src/external/qrencode-3.4.4 - stdcpp17 - Fast - - - Console - true - - - Default - ../../src/external/windows/lib/$(Platform) - - - - - Use - Level3 - Disabled - WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) - stdafx.h - true - false - SyncCThrow - true - ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef - stdcpp17 - Fast - - - Console - true - - - Default - ../../src/external/windows/lib/$(Platform) - - - - - Level3 - Use - MaxSpeed - true - true - WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) - stdafx.h - true - SyncCThrow - false - ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef - stdcpp17 - Fast - - - Console - true - true - true - UseFastLinkTimeCodeGeneration - ../../src/external/windows/lib/$(Platform) - - - - - Level3 - Use - MaxSpeed - true - true - WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) - stdafx.h - true - SyncCThrow - false - ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef - stdcpp17 - Fast - - - Console - true - true - true - UseFastLinkTimeCodeGeneration - ../../src/external/windows/lib/$(Platform) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Create - Create - Create - Create - - - - - - - - - - - - - - - diff --git a/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj.filters b/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj.filters deleted file mode 100644 index 75bed1a1..00000000 --- a/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj.filters +++ /dev/null @@ -1,1691 +0,0 @@ - - - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\assets - - - ballistica\assets - - - ballistica\assets - - - ballistica\assets - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica - - - ballistica - - - ballistica\config - - - ballistica\config - - - ballistica\config - - - ballistica\config - - - ballistica\config - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics - - - ballistica\dynamics - - - ballistica\dynamics - - - ballistica\dynamics - - - ballistica\dynamics - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics - - - ballistica\dynamics - - - ballistica\dynamics - - - ballistica\dynamics - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics\gl - - - ballistica\graphics\gl - - - ballistica\graphics\gl - - - ballistica\graphics\gl - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics\text - - - ballistica\graphics\text - - - ballistica\graphics\text - - - ballistica\graphics\text - - - ballistica\graphics\text - - - ballistica\graphics\text - - - ballistica\graphics\text - - - ballistica\graphics\texture - - - ballistica\graphics\texture - - - ballistica\graphics\texture - - - ballistica\graphics\texture - - - ballistica\graphics\texture - - - ballistica\graphics\texture - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input - - - ballistica\input - - - ballistica\input - - - ballistica\input - - - ballistica\internal - - - ballistica\logic - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic - - - ballistica\logic - - - ballistica\logic - - - ballistica\logic - - - ballistica\logic - - - ballistica\logic - - - ballistica\logic - - - ballistica\logic - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic - - - ballistica\logic - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\platform\apple - - - ballistica\platform\linux - - - ballistica\platform\linux - - - ballistica\platform - - - ballistica\platform - - - ballistica\platform - - - ballistica\platform\sdl - - - ballistica\platform\sdl - - - ballistica\platform - - - ballistica\platform - - - ballistica\platform\windows - - - ballistica\platform\windows - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene - - - ballistica\scene - - - ballistica\scene - - - ballistica\scene - - - ballistica\scene\v1 - - - ballistica\scene\v1 - - - ballistica\ui - - - ballistica\ui - - - ballistica\ui - - - ballistica\ui - - - ballistica\ui - - - ballistica\ui - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\qr_code_generator - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj b/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj deleted file mode 100644 index 78353bdb..00000000 --- a/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj +++ /dev/null @@ -1,746 +0,0 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - Debug - x64 - - - Release - x64 - - - - {B9012642-2E6D-48A1-90EC-E91799B2198E} - Win32Proj - BallisticaCoreHeadless - 10.0 - - - - Application - true - v142 - Unicode - - - Application - false - v142 - true - Unicode - - - Application - true - v142 - Unicode - - - Application - false - v142 - true - Unicode - - - - - - - - - - - - - - - - - - - - - true - $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ - $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ - - - true - $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ - $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ - - - false - $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ - $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ - - - false - $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ - $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ - - - - Use - Level3 - Disabled - WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) - true - true - stdafx.h - false - SyncCThrow - ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef - stdcpp17 - Fast - - - Console - true - Default - ../../src/external/windows/lib/$(Platform) - - - - - Use - Level3 - Disabled - _DEBUG;_CONSOLE;%(PreprocessorDefinitions) - true - SyncCThrow - stdafx.h - true - false - ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef - stdcpp17 - Fast - - - Console - true - ../../src/external/windows/lib/$(Platform) - - - - - Level3 - Use - MaxSpeed - true - true - WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) - false - true - stdafx.h - SyncCThrow - ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef - stdcpp17 - Fast - - - Console - true - true - true - ../../src/external/windows/lib/$(Platform) - - - - - Level3 - Use - MaxSpeed - true - true - NDEBUG;_CONSOLE;%(PreprocessorDefinitions) - false - SyncCThrow - stdafx.h - true - ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef - stdcpp17 - Fast - - - Console - true - true - true - ../../src/external/windows/lib/$(Platform) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Create - Create - Create - Create - - - - - - - - diff --git a/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj.filters b/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj.filters deleted file mode 100644 index 75bed1a1..00000000 --- a/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj.filters +++ /dev/null @@ -1,1691 +0,0 @@ - - - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\app - - - ballistica\assets - - - ballistica\assets - - - ballistica\assets - - - ballistica\assets - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\component - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\assets\data - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica\audio - - - ballistica - - - ballistica - - - ballistica\config - - - ballistica\config - - - ballistica\config - - - ballistica\config - - - ballistica\config - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\core - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics\bg - - - ballistica\dynamics - - - ballistica\dynamics - - - ballistica\dynamics - - - ballistica\dynamics - - - ballistica\dynamics - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics\material - - - ballistica\dynamics - - - ballistica\dynamics - - - ballistica\dynamics - - - ballistica\dynamics - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\generic - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics\component - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics\gl - - - ballistica\graphics\gl - - - ballistica\graphics\gl - - - ballistica\graphics\gl - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics\mesh - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\graphics\text - - - ballistica\graphics\text - - - ballistica\graphics\text - - - ballistica\graphics\text - - - ballistica\graphics\text - - - ballistica\graphics\text - - - ballistica\graphics\text - - - ballistica\graphics\texture - - - ballistica\graphics\texture - - - ballistica\graphics\texture - - - ballistica\graphics\texture - - - ballistica\graphics\texture - - - ballistica\graphics\texture - - - ballistica\graphics - - - ballistica\graphics - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input\device - - - ballistica\input - - - ballistica\input - - - ballistica\input - - - ballistica\input - - - ballistica\internal - - - ballistica\logic - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic\connection - - - ballistica\logic - - - ballistica\logic - - - ballistica\logic - - - ballistica\logic - - - ballistica\logic - - - ballistica\logic - - - ballistica\logic - - - ballistica\logic - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic\session - - - ballistica\logic - - - ballistica\logic - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\math - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\networking - - - ballistica\platform\apple - - - ballistica\platform\linux - - - ballistica\platform\linux - - - ballistica\platform - - - ballistica\platform - - - ballistica\platform - - - ballistica\platform\sdl - - - ballistica\platform\sdl - - - ballistica\platform - - - ballistica\platform - - - ballistica\platform\windows - - - ballistica\platform\windows - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\class - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python\methods - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\python - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene\node - - - ballistica\scene - - - ballistica\scene - - - ballistica\scene - - - ballistica\scene - - - ballistica\scene\v1 - - - ballistica\scene\v1 - - - ballistica\ui - - - ballistica\ui - - - ballistica\ui - - - ballistica\ui - - - ballistica\ui - - - ballistica\ui - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - ballistica\ui\widget - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\open_dynamics_engine-ef\ode - - - external\qr_code_generator - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ballisticacore-cmake/.idea/.gitignore b/ballisticakit-cmake/.idea/.gitignore similarity index 100% rename from ballisticacore-cmake/.idea/.gitignore rename to ballisticakit-cmake/.idea/.gitignore diff --git a/ballisticakit-cmake/.idea/.name b/ballisticakit-cmake/.idea/.name new file mode 100644 index 00000000..e564eee5 --- /dev/null +++ b/ballisticakit-cmake/.idea/.name @@ -0,0 +1 @@ +BallisticaKit \ No newline at end of file diff --git a/ballisticakit-cmake/.idea/BallisticaKit.iml b/ballisticakit-cmake/.idea/BallisticaKit.iml new file mode 100644 index 00000000..0ec431be --- /dev/null +++ b/ballisticakit-cmake/.idea/BallisticaKit.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ballisticacore-cmake/.idea/cmake.xml b/ballisticakit-cmake/.idea/cmake.xml similarity index 100% rename from ballisticacore-cmake/.idea/cmake.xml rename to ballisticakit-cmake/.idea/cmake.xml diff --git a/ballisticacore-cmake/.idea/codeStyles/Project.xml b/ballisticakit-cmake/.idea/codeStyles/Project.xml similarity index 85% rename from ballisticacore-cmake/.idea/codeStyles/Project.xml rename to ballisticakit-cmake/.idea/codeStyles/Project.xml index 249efa28..6cf85b14 100644 --- a/ballisticacore-cmake/.idea/codeStyles/Project.xml +++ b/ballisticakit-cmake/.idea/codeStyles/Project.xml @@ -1,6 +1,7 @@ - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + @@ -48,14 +42,22 @@ - - - - - - - - + + + + + + + + + + + + + + + + @@ -63,4 +65,7 @@ + + diff --git a/ballisticacore-cmake/.idea/modules.xml b/ballisticakit-cmake/.idea/modules.xml similarity index 56% rename from ballisticacore-cmake/.idea/modules.xml rename to ballisticakit-cmake/.idea/modules.xml index bbbd75dc..2105983b 100644 --- a/ballisticacore-cmake/.idea/modules.xml +++ b/ballisticakit-cmake/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/ballisticakit-cmake/.idea/runConfigurations/ballisticakit.xml b/ballisticakit-cmake/.idea/runConfigurations/ballisticakit.xml new file mode 100644 index 00000000..2e94e7a1 --- /dev/null +++ b/ballisticakit-cmake/.idea/runConfigurations/ballisticakit.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/ballisticakit-cmake/.idea/scopes/External.xml b/ballisticakit-cmake/.idea/scopes/External.xml new file mode 100644 index 00000000..2a3dc5ab --- /dev/null +++ b/ballisticakit-cmake/.idea/scopes/External.xml @@ -0,0 +1,3 @@ + + + diff --git a/ballisticakit-cmake/.idea/scopes/Ignored.xml b/ballisticakit-cmake/.idea/scopes/Ignored.xml new file mode 100644 index 00000000..91975d44 --- /dev/null +++ b/ballisticakit-cmake/.idea/scopes/Ignored.xml @@ -0,0 +1,3 @@ + + + diff --git a/ballisticacore-cmake/.idea/vcs.xml b/ballisticakit-cmake/.idea/vcs.xml similarity index 100% rename from ballisticacore-cmake/.idea/vcs.xml rename to ballisticakit-cmake/.idea/vcs.xml diff --git a/ballisticakit-cmake/CMakeLists.txt b/ballisticakit-cmake/CMakeLists.txt new file mode 100644 index 00000000..1d1c3e84 --- /dev/null +++ b/ballisticakit-cmake/CMakeLists.txt @@ -0,0 +1,773 @@ +cmake_minimum_required(VERSION 3.12) +project(BallisticaKit) +include(CheckIncludeFile) + +option(HEADLESS "build headless server" OFF) +option(TEST_BUILD "include testing features" OFF) + +# Requiring minimum of C++17 currently. +set(CMAKE_CXX_STANDARD 17) + +if (APPLE) + # Seems as of Mojave we need to explicitly pull in homebrew paths. + # Just hard-coding recommended homebrew install paths for now. + # Is there a more elegant way to do this? + if (CMAKE_SYSTEM_PROCESSOR MATCHES arm64) + list(APPEND CMAKE_PREFIX_PATH /opt/homebrew) + include_directories("/opt/homebrew/include") + link_directories("/opt/homebrew/lib") + else() + list(APPEND CMAKE_PREFIX_PATH /usr/local) + include_directories("/usr/local/include") + link_directories("/usr/local/lib") + endif() + + # On Mac with homebrew it seems that Requesting 3.X when we've got + # 3.(X+1) installed will point us at the 3.(X+1) framework but will attempt + # to load a 3.X library from within it which doesn't exist. So we need + # to be a bit more explicit telling it where to look. Note: this was last + # tested with 3.7; should revisit sometime to make sure still applies. + execute_process(COMMAND "python3.11-config" "--prefix" + OUTPUT_VARIABLE Python_ROOT_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE) + +endif () +find_package (Python 3.11 REQUIRED EXACT COMPONENTS Development) + + +if (HEADLESS) + add_definitions(-DBA_HEADLESS_BUILD=1) + else () + find_package(SDL2 QUIET) + if (SDL2_FOUND) + if ("${SDL2_LIBRARIES}" STREQUAL "") + message(WARNING "SDL2_LIBRARIES wasn't set, manually setting to SDL2::SDL2") + set(SDL2_LIBRARIES "SDL2::SDL2") + endif () + # Getting complaint about space at the end of this on ubuntu16. + string(STRIP "${SDL2_LIBRARIES}" SDL2_LIBRARIES) + else () + message(FATAL_ERROR "SDL2 not found") + endif () + find_package(OpenGL REQUIRED) + find_package(OpenAL REQUIRED) + if (APPLE) + # On mac this sets an include path that we don't need since + # we're using the system framework... should clean this up. + set(OPENAL_INCLUDE_DIR "") + endif () + find_library(OGG_LIBRARY ogg) + find_library(VORBISFILE_LIBRARY vorbisfile) + if (NOT OGG_LIBRARY) + message(FATAL_ERROR "ogg library not found") + endif () + if (NOT VORBISFILE_LIBRARY) + message(FATAL_ERROR "vorbisfile library not found") + endif () + set(EXTRA_INCLUDE_DIRS ${OPENGL_INCLUDE_DIRS} + ${OPENAL_INCLUDE_DIR} ${SDL2_INCLUDE_DIRS}) + set(EXTRA_LIBRARIES ogg vorbisfile ${OPENGL_LIBRARIES} ${OPENAL_LIBRARY}) +endif () + +if (TEST_BUILD) + add_definitions(-DBA_TEST_BUILD=1) +endif () + +# Currently seeing warnings about parameter order changing in GCC 7.1 +# on Raspberry Pi builds. We never need to care about C++ abi compatibility +# so just silencing them for now. Can maybe remove this later if they stop. +if (CMAKE_CXX_COMPILER_ID MATCHES GNU) + set(CMAKE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} -Wno-psabi") +endif() + +set(BA_SRC_ROOT ../src) +include_directories(${BA_SRC_ROOT}) +add_compile_options(-include ballistica/shared/buildconfig/buildconfig_cmake.h) + +if (CMAKE_BUILD_TYPE MATCHES Debug) + add_definitions(-DBA_DEBUG_BUILD=1) +else () + # It seems that cmake can choose -O2 sometimes and -O3 sometimes + # for release builds (depending on Release vs RelWithDebInfo, etc). + # Let's keep all our non-debug builds consistent at -O3 for now; can + # revisit if it causes problems. + add_definitions(-O3) +endif () + +set(ODE_SRC_ROOT ${BA_SRC_ROOT}/external/open_dynamics_engine-ef) + +add_library(ode + ${ODE_SRC_ROOT}/ode/IceAABB.cpp + ${ODE_SRC_ROOT}/ode/IceContainer.cpp + ${ODE_SRC_ROOT}/ode/IceHPoint.cpp + ${ODE_SRC_ROOT}/ode/IceIndexedTriangle.cpp + ${ODE_SRC_ROOT}/ode/IceMatrix3x3.cpp + ${ODE_SRC_ROOT}/ode/IceMatrix4x4.cpp + ${ODE_SRC_ROOT}/ode/IceOBB.cpp + ${ODE_SRC_ROOT}/ode/IcePlane.cpp + ${ODE_SRC_ROOT}/ode/IcePoint.cpp + ${ODE_SRC_ROOT}/ode/IceRandom.cpp + ${ODE_SRC_ROOT}/ode/IceRay.cpp + ${ODE_SRC_ROOT}/ode/IceRevisitedRadix.cpp + ${ODE_SRC_ROOT}/ode/IceSegment.cpp + ${ODE_SRC_ROOT}/ode/IceTriangle.cpp + ${ODE_SRC_ROOT}/ode/IceUtils.cpp + ${ODE_SRC_ROOT}/ode/ode.cpp + ${ODE_SRC_ROOT}/ode/ode_array.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_cylinder_box.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_cylinder_plane.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_cylinder_sphere.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_cylinder_trimesh.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_kernel.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_quadtreespace.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_sapspace.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_space.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_std.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_transform.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_trimesh.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_trimesh_box.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_trimesh_ccylinder.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_trimesh_distance.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_trimesh_plane.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_trimesh_ray.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_trimesh_sphere.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_trimesh_trimesh.cpp + ${ODE_SRC_ROOT}/ode/ode_collision_util.cpp + ${ODE_SRC_ROOT}/ode/ode_error.cpp + ${ODE_SRC_ROOT}/ode/ode_export-diff.cpp + ${ODE_SRC_ROOT}/ode/ode_fastdot.cpp + ${ODE_SRC_ROOT}/ode/ode_fastldlt.cpp + ${ODE_SRC_ROOT}/ode/ode_fastlsolve.cpp + ${ODE_SRC_ROOT}/ode/ode_fastltsolve.cpp + ${ODE_SRC_ROOT}/ode/ode_joint.cpp + ${ODE_SRC_ROOT}/ode/ode_lcp.cpp + ${ODE_SRC_ROOT}/ode/ode_mass.cpp + ${ODE_SRC_ROOT}/ode/ode_mat.cpp + ${ODE_SRC_ROOT}/ode/ode_math.cpp + ${ODE_SRC_ROOT}/ode/ode_matrix.cpp + ${ODE_SRC_ROOT}/ode/ode_memory.cpp + ${ODE_SRC_ROOT}/ode/ode_misc.cpp + ${ODE_SRC_ROOT}/ode/ode_obstack.cpp + ${ODE_SRC_ROOT}/ode/ode_quickstep.cpp + ${ODE_SRC_ROOT}/ode/ode_rotation.cpp + ${ODE_SRC_ROOT}/ode/ode_step.cpp + ${ODE_SRC_ROOT}/ode/ode_stepfast.cpp + ${ODE_SRC_ROOT}/ode/ode_timer.cpp + ${ODE_SRC_ROOT}/ode/ode_util.cpp + ${ODE_SRC_ROOT}/ode/OPC_AABBCollider.cpp + ${ODE_SRC_ROOT}/ode/OPC_AABBTree.cpp + ${ODE_SRC_ROOT}/ode/OPC_BaseModel.cpp + ${ODE_SRC_ROOT}/ode/OPC_BoxPruning.cpp + ${ODE_SRC_ROOT}/ode/OPC_Collider.cpp + ${ODE_SRC_ROOT}/ode/OPC_HybridModel.cpp + ${ODE_SRC_ROOT}/ode/OPC_LSSCollider.cpp + ${ODE_SRC_ROOT}/ode/OPC_MeshInterface.cpp + ${ODE_SRC_ROOT}/ode/OPC_Model.cpp + ${ODE_SRC_ROOT}/ode/OPC_OBBCollider.cpp + ${ODE_SRC_ROOT}/ode/OPC_OptimizedTree.cpp + ${ODE_SRC_ROOT}/ode/OPC_PlanesCollider.cpp + ${ODE_SRC_ROOT}/ode/OPC_RayCollider.cpp + ${ODE_SRC_ROOT}/ode/OPC_SphereCollider.cpp + ${ODE_SRC_ROOT}/ode/OPC_SweepAndPrune.cpp + ${ODE_SRC_ROOT}/ode/OPC_TreeBuilders.cpp + ${ODE_SRC_ROOT}/ode/OPC_TreeCollider.cpp + ${ODE_SRC_ROOT}/ode/OPC_VolumeCollider.cpp + ${ODE_SRC_ROOT}/ode/Opcode.cpp + ) +target_include_directories(ode PRIVATE ${ODE_SRC_ROOT}) + +# NOTE: There used to be an issue with optimized GCC builds where mesh +# collisions would fail randomly, leading to characters falling through +# floors somewhat regularly. For this reason I was limiting optimization to +# -O1 for the rigid body library. However, as of April 2021, all seems +# well when testing on arm64 and x86-64 linux builds. (I think) +# The last time I remember seeing this bug was around 2016 I believe, but I +# haven't looked for it since. Perhaps GCC was fixed or perhaps the error +# was limited to 32 bit x86 builds; in either case we should be good, as +# we're no longer building any 32 bit x86 builds using GCC. +# Keeping this in here commented out just in case it rears its ugly head +# again though. +# if (CMAKE_BUILD_TYPE MATCHES Release) +# if (CMAKE_CXX_COMPILER_ID MATCHES GNU) +# target_compile_options(ode PRIVATE -O1) +# endif() +# endif () + +# BallisticaKit binary. +add_executable(ballisticakit + ${BA_SRC_ROOT}/external/qr_code_generator/QrCode.cpp + # AUTOGENERATED_PUBLIC_BEGIN (this section is managed by the "update_project" tool) + ${BA_SRC_ROOT}/ballistica/base/app/app.cc + ${BA_SRC_ROOT}/ballistica/base/app/app.h + ${BA_SRC_ROOT}/ballistica/base/app/app_config.cc + ${BA_SRC_ROOT}/ballistica/base/app/app_config.h + ${BA_SRC_ROOT}/ballistica/base/app/app_headless.cc + ${BA_SRC_ROOT}/ballistica/base/app/app_headless.h + ${BA_SRC_ROOT}/ballistica/base/app/app_mode.cc + ${BA_SRC_ROOT}/ballistica/base/app/app_mode.h + ${BA_SRC_ROOT}/ballistica/base/app/app_mode_empty.cc + ${BA_SRC_ROOT}/ballistica/base/app/app_mode_empty.h + ${BA_SRC_ROOT}/ballistica/base/app/app_vr.cc + ${BA_SRC_ROOT}/ballistica/base/app/app_vr.h + ${BA_SRC_ROOT}/ballistica/base/app/sdl_app.cc + ${BA_SRC_ROOT}/ballistica/base/app/sdl_app.h + ${BA_SRC_ROOT}/ballistica/base/app/stress_test.cc + ${BA_SRC_ROOT}/ballistica/base/app/stress_test.h + ${BA_SRC_ROOT}/ballistica/base/assets/asset.cc + ${BA_SRC_ROOT}/ballistica/base/assets/asset.h + ${BA_SRC_ROOT}/ballistica/base/assets/assets.cc + ${BA_SRC_ROOT}/ballistica/base/assets/assets.h + ${BA_SRC_ROOT}/ballistica/base/assets/assets_server.cc + ${BA_SRC_ROOT}/ballistica/base/assets/assets_server.h + ${BA_SRC_ROOT}/ballistica/base/assets/collision_mesh_asset.cc + ${BA_SRC_ROOT}/ballistica/base/assets/collision_mesh_asset.h + ${BA_SRC_ROOT}/ballistica/base/assets/data_asset.cc + ${BA_SRC_ROOT}/ballistica/base/assets/data_asset.h + ${BA_SRC_ROOT}/ballistica/base/assets/mesh_asset.cc + ${BA_SRC_ROOT}/ballistica/base/assets/mesh_asset.h + ${BA_SRC_ROOT}/ballistica/base/assets/mesh_asset_renderer_data.h + ${BA_SRC_ROOT}/ballistica/base/assets/sound_asset.cc + ${BA_SRC_ROOT}/ballistica/base/assets/sound_asset.h + ${BA_SRC_ROOT}/ballistica/base/assets/texture_asset.cc + ${BA_SRC_ROOT}/ballistica/base/assets/texture_asset.h + ${BA_SRC_ROOT}/ballistica/base/assets/texture_asset_preload_data.cc + ${BA_SRC_ROOT}/ballistica/base/assets/texture_asset_preload_data.h + ${BA_SRC_ROOT}/ballistica/base/assets/texture_asset_renderer_data.h + ${BA_SRC_ROOT}/ballistica/base/audio/al_sys.cc + ${BA_SRC_ROOT}/ballistica/base/audio/al_sys.h + ${BA_SRC_ROOT}/ballistica/base/audio/audio.cc + ${BA_SRC_ROOT}/ballistica/base/audio/audio.h + ${BA_SRC_ROOT}/ballistica/base/audio/audio_server.cc + ${BA_SRC_ROOT}/ballistica/base/audio/audio_server.h + ${BA_SRC_ROOT}/ballistica/base/audio/audio_source.cc + ${BA_SRC_ROOT}/ballistica/base/audio/audio_source.h + ${BA_SRC_ROOT}/ballistica/base/audio/audio_streamer.cc + ${BA_SRC_ROOT}/ballistica/base/audio/audio_streamer.h + ${BA_SRC_ROOT}/ballistica/base/audio/ogg_stream.cc + ${BA_SRC_ROOT}/ballistica/base/audio/ogg_stream.h + ${BA_SRC_ROOT}/ballistica/base/base.cc + ${BA_SRC_ROOT}/ballistica/base/base.h + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics.cc + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics.h + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics_draw_snapshot.h + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics_fuse.cc + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics_fuse.h + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics_fuse_data.h + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics_height_cache.cc + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics_height_cache.h + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics_server.cc + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics_server.h + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics_shadow.cc + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics_shadow.h + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics_shadow_data.h + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics_volume_light.cc + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics_volume_light.h + ${BA_SRC_ROOT}/ballistica/base/dynamics/bg/bg_dynamics_volume_light_data.h + ${BA_SRC_ROOT}/ballistica/base/dynamics/collision_cache.cc + ${BA_SRC_ROOT}/ballistica/base/dynamics/collision_cache.h + ${BA_SRC_ROOT}/ballistica/base/graphics/component/empty_component.h + ${BA_SRC_ROOT}/ballistica/base/graphics/component/object_component.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/component/object_component.h + ${BA_SRC_ROOT}/ballistica/base/graphics/component/post_process_component.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/component/post_process_component.h + ${BA_SRC_ROOT}/ballistica/base/graphics/component/render_component.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/component/render_component.h + ${BA_SRC_ROOT}/ballistica/base/graphics/component/shield_component.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/component/shield_component.h + ${BA_SRC_ROOT}/ballistica/base/graphics/component/simple_component.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/component/simple_component.h + ${BA_SRC_ROOT}/ballistica/base/graphics/component/smoke_component.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/component/smoke_component.h + ${BA_SRC_ROOT}/ballistica/base/graphics/component/special_component.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/component/special_component.h + ${BA_SRC_ROOT}/ballistica/base/graphics/component/sprite_component.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/component/sprite_component.h + ${BA_SRC_ROOT}/ballistica/base/graphics/gl/gl_sys.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/gl/gl_sys.h + ${BA_SRC_ROOT}/ballistica/base/graphics/gl/renderer_gl.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/gl/renderer_gl.h + ${BA_SRC_ROOT}/ballistica/base/graphics/graphics.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/graphics.h + ${BA_SRC_ROOT}/ballistica/base/graphics/graphics_server.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/graphics_server.h + ${BA_SRC_ROOT}/ballistica/base/graphics/graphics_vr.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/graphics_vr.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/image_mesh.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/image_mesh.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_buffer.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_buffer_base.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_buffer_vertex_simple_full.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_buffer_vertex_smoke_full.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_buffer_vertex_sprite.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_data.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_data.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_data_client_handle.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_data_client_handle.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_index_buffer_16.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_index_buffer_32.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_indexed.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_indexed_base.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_indexed_dual_texture_full.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_indexed_object_split.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_indexed_simple_full.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_indexed_simple_split.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_indexed_smoke_full.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_indexed_static_dynamic.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_non_indexed.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/mesh_renderer_data.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/sprite_mesh.h + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/text_mesh.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/mesh/text_mesh.h + ${BA_SRC_ROOT}/ballistica/base/graphics/renderer/framebuffer.h + ${BA_SRC_ROOT}/ballistica/base/graphics/renderer/render_pass.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/renderer/render_pass.h + ${BA_SRC_ROOT}/ballistica/base/graphics/renderer/render_target.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/renderer/render_target.h + ${BA_SRC_ROOT}/ballistica/base/graphics/renderer/renderer.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/renderer/renderer.h + ${BA_SRC_ROOT}/ballistica/base/graphics/support/area_of_interest.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/support/area_of_interest.h + ${BA_SRC_ROOT}/ballistica/base/graphics/support/camera.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/support/camera.h + ${BA_SRC_ROOT}/ballistica/base/graphics/support/frame_def.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/support/frame_def.h + ${BA_SRC_ROOT}/ballistica/base/graphics/support/net_graph.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/support/net_graph.h + ${BA_SRC_ROOT}/ballistica/base/graphics/support/render_command_buffer.h + ${BA_SRC_ROOT}/ballistica/base/graphics/text/font_page_map_data.h + ${BA_SRC_ROOT}/ballistica/base/graphics/text/text_graphics.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/text/text_graphics.h + ${BA_SRC_ROOT}/ballistica/base/graphics/text/text_group.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/text/text_group.h + ${BA_SRC_ROOT}/ballistica/base/graphics/text/text_packer.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/text/text_packer.h + ${BA_SRC_ROOT}/ballistica/base/graphics/texture/dds.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/texture/dds.h + ${BA_SRC_ROOT}/ballistica/base/graphics/texture/ktx.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/texture/ktx.h + ${BA_SRC_ROOT}/ballistica/base/graphics/texture/pvr.cc + ${BA_SRC_ROOT}/ballistica/base/graphics/texture/pvr.h + ${BA_SRC_ROOT}/ballistica/base/input/device/input_device.cc + ${BA_SRC_ROOT}/ballistica/base/input/device/input_device.h + ${BA_SRC_ROOT}/ballistica/base/input/device/input_device_delegate.cc + ${BA_SRC_ROOT}/ballistica/base/input/device/input_device_delegate.h + ${BA_SRC_ROOT}/ballistica/base/input/device/joystick_input.cc + ${BA_SRC_ROOT}/ballistica/base/input/device/joystick_input.h + ${BA_SRC_ROOT}/ballistica/base/input/device/keyboard_input.cc + ${BA_SRC_ROOT}/ballistica/base/input/device/keyboard_input.h + ${BA_SRC_ROOT}/ballistica/base/input/device/test_input.cc + ${BA_SRC_ROOT}/ballistica/base/input/device/test_input.h + ${BA_SRC_ROOT}/ballistica/base/input/device/touch_input.cc + ${BA_SRC_ROOT}/ballistica/base/input/device/touch_input.h + ${BA_SRC_ROOT}/ballistica/base/input/input.cc + ${BA_SRC_ROOT}/ballistica/base/input/input.h + ${BA_SRC_ROOT}/ballistica/base/input/support/remote_app_server.cc + ${BA_SRC_ROOT}/ballistica/base/input/support/remote_app_server.h + ${BA_SRC_ROOT}/ballistica/base/logic/logic.cc + ${BA_SRC_ROOT}/ballistica/base/logic/logic.h + ${BA_SRC_ROOT}/ballistica/base/networking/network_reader.cc + ${BA_SRC_ROOT}/ballistica/base/networking/network_reader.h + ${BA_SRC_ROOT}/ballistica/base/networking/network_writer.cc + ${BA_SRC_ROOT}/ballistica/base/networking/network_writer.h + ${BA_SRC_ROOT}/ballistica/base/networking/networking.cc + ${BA_SRC_ROOT}/ballistica/base/networking/networking.h + ${BA_SRC_ROOT}/ballistica/base/platform/android/amazon/base_plat_andr_amazon.cc + ${BA_SRC_ROOT}/ballistica/base/platform/android/amazon/base_plat_andr_amazon.h + ${BA_SRC_ROOT}/ballistica/base/platform/android/base_platform_android.cc + ${BA_SRC_ROOT}/ballistica/base/platform/android/base_platform_android.h + ${BA_SRC_ROOT}/ballistica/base/platform/android/cardboard/base_pl_an_cardboard.cc + ${BA_SRC_ROOT}/ballistica/base/platform/android/cardboard/base_pl_an_cardboard.h + ${BA_SRC_ROOT}/ballistica/base/platform/android/google/base_plat_andr_google.cc + ${BA_SRC_ROOT}/ballistica/base/platform/android/google/base_plat_andr_google.h + ${BA_SRC_ROOT}/ballistica/base/platform/apple/base_platform_apple.cc + ${BA_SRC_ROOT}/ballistica/base/platform/apple/base_platform_apple.h + ${BA_SRC_ROOT}/ballistica/base/platform/base_platform.cc + ${BA_SRC_ROOT}/ballistica/base/platform/base_platform.h + ${BA_SRC_ROOT}/ballistica/base/platform/linux/base_platform_linux.cc + ${BA_SRC_ROOT}/ballistica/base/platform/linux/base_platform_linux.h + ${BA_SRC_ROOT}/ballistica/base/platform/oculus/main_rift.cc + ${BA_SRC_ROOT}/ballistica/base/platform/support/min_sdl_key_names.h + ${BA_SRC_ROOT}/ballistica/base/platform/windows/base_platform_windows.cc + ${BA_SRC_ROOT}/ballistica/base/platform/windows/base_platform_windows.h + ${BA_SRC_ROOT}/ballistica/base/platform/windows/base_platform_windows_oculus.cc + ${BA_SRC_ROOT}/ballistica/base/platform/windows/base_platform_windows_oculus.h + ${BA_SRC_ROOT}/ballistica/base/python/base_python.cc + ${BA_SRC_ROOT}/ballistica/base/python/base_python.h + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_app_timer.cc + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_app_timer.h + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_context_call.cc + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_context_call.h + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_context_ref.cc + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_context_ref.h + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_display_timer.cc + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_display_timer.h + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_feature_set_data.cc + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_feature_set_data.h + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_simple_sound.cc + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_simple_sound.h + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_vec3.cc + ${BA_SRC_ROOT}/ballistica/base/python/class/python_class_vec3.h + ${BA_SRC_ROOT}/ballistica/base/python/methods/python_methods_app.cc + ${BA_SRC_ROOT}/ballistica/base/python/methods/python_methods_app.h + ${BA_SRC_ROOT}/ballistica/base/python/methods/python_methods_graphics.cc + ${BA_SRC_ROOT}/ballistica/base/python/methods/python_methods_graphics.h + ${BA_SRC_ROOT}/ballistica/base/python/methods/python_methods_misc.cc + ${BA_SRC_ROOT}/ballistica/base/python/methods/python_methods_misc.h + ${BA_SRC_ROOT}/ballistica/base/python/support/python_context_call.cc + ${BA_SRC_ROOT}/ballistica/base/python/support/python_context_call.h + ${BA_SRC_ROOT}/ballistica/base/python/support/python_context_call_runnable.h + ${BA_SRC_ROOT}/ballistica/base/support/app_timer.h + ${BA_SRC_ROOT}/ballistica/base/support/context.cc + ${BA_SRC_ROOT}/ballistica/base/support/context.h + ${BA_SRC_ROOT}/ballistica/base/support/huffman.cc + ${BA_SRC_ROOT}/ballistica/base/support/huffman.h + ${BA_SRC_ROOT}/ballistica/base/support/plus_soft.h + ${BA_SRC_ROOT}/ballistica/base/support/stdio_console.cc + ${BA_SRC_ROOT}/ballistica/base/support/stdio_console.h + ${BA_SRC_ROOT}/ballistica/base/ui/console.cc + ${BA_SRC_ROOT}/ballistica/base/ui/console.h + ${BA_SRC_ROOT}/ballistica/base/ui/ui.cc + ${BA_SRC_ROOT}/ballistica/base/ui/ui.h + ${BA_SRC_ROOT}/ballistica/base/ui/widget_message.h + ${BA_SRC_ROOT}/ballistica/classic/classic.cc + ${BA_SRC_ROOT}/ballistica/classic/classic.h + ${BA_SRC_ROOT}/ballistica/classic/python/classic_python.cc + ${BA_SRC_ROOT}/ballistica/classic/python/classic_python.h + ${BA_SRC_ROOT}/ballistica/classic/python/methods/python_methods_classic.cc + ${BA_SRC_ROOT}/ballistica/classic/python/methods/python_methods_classic.h + ${BA_SRC_ROOT}/ballistica/classic/support/v1_account.cc + ${BA_SRC_ROOT}/ballistica/classic/support/v1_account.h + ${BA_SRC_ROOT}/ballistica/core/core.cc + ${BA_SRC_ROOT}/ballistica/core/core.h + ${BA_SRC_ROOT}/ballistica/core/platform/apple/core_platform_apple.h + ${BA_SRC_ROOT}/ballistica/core/platform/core_platform.cc + ${BA_SRC_ROOT}/ballistica/core/platform/core_platform.h + ${BA_SRC_ROOT}/ballistica/core/platform/linux/core_platform_linux.cc + ${BA_SRC_ROOT}/ballistica/core/platform/linux/core_platform_linux.h + ${BA_SRC_ROOT}/ballistica/core/platform/support/min_sdl.h + ${BA_SRC_ROOT}/ballistica/core/platform/windows/core_platform_windows.cc + ${BA_SRC_ROOT}/ballistica/core/platform/windows/core_platform_windows.h + ${BA_SRC_ROOT}/ballistica/core/python/core_python.cc + ${BA_SRC_ROOT}/ballistica/core/python/core_python.h + ${BA_SRC_ROOT}/ballistica/core/support/base_soft.h + ${BA_SRC_ROOT}/ballistica/core/support/core_config.cc + ${BA_SRC_ROOT}/ballistica/core/support/core_config.h + ${BA_SRC_ROOT}/ballistica/scene_v1/assets/scene_asset.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/assets/scene_asset.h + ${BA_SRC_ROOT}/ballistica/scene_v1/assets/scene_collision_mesh.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/assets/scene_collision_mesh.h + ${BA_SRC_ROOT}/ballistica/scene_v1/assets/scene_cube_map_texture.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/assets/scene_cube_map_texture.h + ${BA_SRC_ROOT}/ballistica/scene_v1/assets/scene_data_asset.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/assets/scene_data_asset.h + ${BA_SRC_ROOT}/ballistica/scene_v1/assets/scene_mesh.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/assets/scene_mesh.h + ${BA_SRC_ROOT}/ballistica/scene_v1/assets/scene_sound.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/assets/scene_sound.h + ${BA_SRC_ROOT}/ballistica/scene_v1/assets/scene_texture.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/assets/scene_texture.h + ${BA_SRC_ROOT}/ballistica/scene_v1/connection/connection.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/connection/connection.h + ${BA_SRC_ROOT}/ballistica/scene_v1/connection/connection_set.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/connection/connection_set.h + ${BA_SRC_ROOT}/ballistica/scene_v1/connection/connection_to_client.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/connection/connection_to_client.h + ${BA_SRC_ROOT}/ballistica/scene_v1/connection/connection_to_client_udp.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/connection/connection_to_client_udp.h + ${BA_SRC_ROOT}/ballistica/scene_v1/connection/connection_to_host.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/connection/connection_to_host.h + ${BA_SRC_ROOT}/ballistica/scene_v1/connection/connection_to_host_udp.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/connection/connection_to_host_udp.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/collision.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/dynamics.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/dynamics.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/impact_sound_material_action.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/impact_sound_material_action.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/material.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/material.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/material_action.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/material_component.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/material_component.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/material_condition_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/material_condition_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/material_context.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/material_context.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/node_message_material_action.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/node_message_material_action.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/node_mod_material_action.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/node_mod_material_action.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/node_user_msg_mat_action.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/node_user_msg_mat_action.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/part_mod_material_action.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/part_mod_material_action.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/python_call_material_action.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/python_call_material_action.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/roll_sound_material_action.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/roll_sound_material_action.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/skid_sound_material_action.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/skid_sound_material_action.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/sound_material_action.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/material/sound_material_action.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/part.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/part.h + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/rigid_body.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/dynamics/rigid_body.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/anim_curve_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/anim_curve_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/bomb_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/bomb_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/combine_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/combine_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/explosion_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/explosion_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/flag_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/flag_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/flash_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/flash_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/globals_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/globals_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/image_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/image_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/light_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/light_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/locator_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/locator_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/math_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/math_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/node_attribute.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/node_attribute.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/node_attribute_connection.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/node_attribute_connection.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/node_type.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/null_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/null_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/player_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/player_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/prop_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/prop_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/region_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/region_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/scorch_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/scorch_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/session_globals_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/session_globals_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/shield_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/shield_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/sound_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/sound_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/spaz_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/spaz_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/terrain_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/terrain_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/text_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/text_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/texture_sequence_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/texture_sequence_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/node/time_display_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/node/time_display_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_activity_data.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_activity_data.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_base_timer.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_base_timer.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_input_device.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_input_device.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_material.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_material.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_node.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_node.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_scene_collision_mesh.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_scene_collision_mesh.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_scene_data_asset.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_scene_data_asset.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_scene_mesh.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_scene_mesh.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_scene_sound.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_scene_sound.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_scene_texture.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_scene_texture.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_scene_timer.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_scene_timer.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_session_data.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_session_data.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_session_player.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/class/python_class_session_player.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/methods/python_methods_assets.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/methods/python_methods_assets.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/methods/python_methods_input.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/methods/python_methods_input.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/methods/python_methods_networking.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/methods/python_methods_networking.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/methods/python_methods_scene.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/methods/python_methods_scene.h + ${BA_SRC_ROOT}/ballistica/scene_v1/python/scene_v1_python.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/python/scene_v1_python.h + ${BA_SRC_ROOT}/ballistica/scene_v1/scene_v1.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/scene_v1.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/client_controller_interface.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/client_input_device.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/support/client_input_device.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/client_input_device_delegate.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/support/client_input_device_delegate.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/client_session.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/support/client_session.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/client_session_net.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/support/client_session_net.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/client_session_replay.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/support/client_session_replay.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/host_activity.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/support/host_activity.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/host_session.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/support/host_session.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/player.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/support/player.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/player_spec.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/support/player_spec.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/scene.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/support/scene.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/scene_v1_app_mode.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/support/scene_v1_app_mode.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/scene_v1_context.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/support/scene_v1_context.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/support/scene_v1_input_device_delegate.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/session.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/support/session.h + ${BA_SRC_ROOT}/ballistica/scene_v1/support/session_stream.cc + ${BA_SRC_ROOT}/ballistica/scene_v1/support/session_stream.h + ${BA_SRC_ROOT}/ballistica/shared/ballistica.cc + ${BA_SRC_ROOT}/ballistica/shared/ballistica.h + ${BA_SRC_ROOT}/ballistica/shared/buildconfig/buildconfig_cmake.h + ${BA_SRC_ROOT}/ballistica/shared/buildconfig/buildconfig_common.h + ${BA_SRC_ROOT}/ballistica/shared/buildconfig/buildconfig_windows_common.h + ${BA_SRC_ROOT}/ballistica/shared/buildconfig/buildconfig_windows_generic.h + ${BA_SRC_ROOT}/ballistica/shared/buildconfig/buildconfig_windows_headless.h + ${BA_SRC_ROOT}/ballistica/shared/foundation/event_loop.cc + ${BA_SRC_ROOT}/ballistica/shared/foundation/event_loop.h + ${BA_SRC_ROOT}/ballistica/shared/foundation/exception.cc + ${BA_SRC_ROOT}/ballistica/shared/foundation/exception.h + ${BA_SRC_ROOT}/ballistica/shared/foundation/fatal_error.cc + ${BA_SRC_ROOT}/ballistica/shared/foundation/fatal_error.h + ${BA_SRC_ROOT}/ballistica/shared/foundation/feature_set_front_end.cc + ${BA_SRC_ROOT}/ballistica/shared/foundation/feature_set_front_end.h + ${BA_SRC_ROOT}/ballistica/shared/foundation/inline.cc + ${BA_SRC_ROOT}/ballistica/shared/foundation/inline.h + ${BA_SRC_ROOT}/ballistica/shared/foundation/logging.cc + ${BA_SRC_ROOT}/ballistica/shared/foundation/logging.h + ${BA_SRC_ROOT}/ballistica/shared/foundation/macros.cc + ${BA_SRC_ROOT}/ballistica/shared/foundation/macros.h + ${BA_SRC_ROOT}/ballistica/shared/foundation/object.cc + ${BA_SRC_ROOT}/ballistica/shared/foundation/object.h + ${BA_SRC_ROOT}/ballistica/shared/foundation/types.h + ${BA_SRC_ROOT}/ballistica/shared/generic/base64.cc + ${BA_SRC_ROOT}/ballistica/shared/generic/base64.h + ${BA_SRC_ROOT}/ballistica/shared/generic/buffer.h + ${BA_SRC_ROOT}/ballistica/shared/generic/json.cc + ${BA_SRC_ROOT}/ballistica/shared/generic/json.h + ${BA_SRC_ROOT}/ballistica/shared/generic/lambda_runnable.h + ${BA_SRC_ROOT}/ballistica/shared/generic/runnable.cc + ${BA_SRC_ROOT}/ballistica/shared/generic/runnable.h + ${BA_SRC_ROOT}/ballistica/shared/generic/timer_list.cc + ${BA_SRC_ROOT}/ballistica/shared/generic/timer_list.h + ${BA_SRC_ROOT}/ballistica/shared/generic/utf8.cc + ${BA_SRC_ROOT}/ballistica/shared/generic/utf8.h + ${BA_SRC_ROOT}/ballistica/shared/generic/utils.cc + ${BA_SRC_ROOT}/ballistica/shared/generic/utils.h + ${BA_SRC_ROOT}/ballistica/shared/math/matrix44f.cc + ${BA_SRC_ROOT}/ballistica/shared/math/matrix44f.h + ${BA_SRC_ROOT}/ballistica/shared/math/point2d.h + ${BA_SRC_ROOT}/ballistica/shared/math/random.cc + ${BA_SRC_ROOT}/ballistica/shared/math/random.h + ${BA_SRC_ROOT}/ballistica/shared/math/rect.h + ${BA_SRC_ROOT}/ballistica/shared/math/vector2f.h + ${BA_SRC_ROOT}/ballistica/shared/math/vector3f.cc + ${BA_SRC_ROOT}/ballistica/shared/math/vector3f.h + ${BA_SRC_ROOT}/ballistica/shared/math/vector4f.h + ${BA_SRC_ROOT}/ballistica/shared/networking/networking_sys.h + ${BA_SRC_ROOT}/ballistica/shared/networking/sockaddr.cc + ${BA_SRC_ROOT}/ballistica/shared/networking/sockaddr.h + ${BA_SRC_ROOT}/ballistica/shared/python/python.cc + ${BA_SRC_ROOT}/ballistica/shared/python/python.h + ${BA_SRC_ROOT}/ballistica/shared/python/python_class.cc + ${BA_SRC_ROOT}/ballistica/shared/python/python_class.h + ${BA_SRC_ROOT}/ballistica/shared/python/python_command.cc + ${BA_SRC_ROOT}/ballistica/shared/python/python_command.h + ${BA_SRC_ROOT}/ballistica/shared/python/python_module_builder.h + ${BA_SRC_ROOT}/ballistica/shared/python/python_object_set.cc + ${BA_SRC_ROOT}/ballistica/shared/python/python_object_set.h + ${BA_SRC_ROOT}/ballistica/shared/python/python_ref.cc + ${BA_SRC_ROOT}/ballistica/shared/python/python_ref.h + ${BA_SRC_ROOT}/ballistica/shared/python/python_sys.h + ${BA_SRC_ROOT}/ballistica/template_fs/python/class/python_class_hello.cc + ${BA_SRC_ROOT}/ballistica/template_fs/python/class/python_class_hello.h + ${BA_SRC_ROOT}/ballistica/template_fs/python/methods/python_methods_template_fs.cc + ${BA_SRC_ROOT}/ballistica/template_fs/python/methods/python_methods_template_fs.h + ${BA_SRC_ROOT}/ballistica/template_fs/python/template_fs_python.cc + ${BA_SRC_ROOT}/ballistica/template_fs/python/template_fs_python.h + ${BA_SRC_ROOT}/ballistica/template_fs/template_fs.cc + ${BA_SRC_ROOT}/ballistica/template_fs/template_fs.h + ${BA_SRC_ROOT}/ballistica/ui_v1/python/class/python_class_ui_mesh.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/python/class/python_class_ui_mesh.h + ${BA_SRC_ROOT}/ballistica/ui_v1/python/class/python_class_ui_sound.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/python/class/python_class_ui_sound.h + ${BA_SRC_ROOT}/ballistica/ui_v1/python/class/python_class_ui_texture.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/python/class/python_class_ui_texture.h + ${BA_SRC_ROOT}/ballistica/ui_v1/python/class/python_class_widget.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/python/class/python_class_widget.h + ${BA_SRC_ROOT}/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/python/methods/python_methods_ui_v1.h + ${BA_SRC_ROOT}/ballistica/ui_v1/python/ui_v1_python.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/python/ui_v1_python.h + ${BA_SRC_ROOT}/ballistica/ui_v1/support/root_ui.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/support/root_ui.h + ${BA_SRC_ROOT}/ballistica/ui_v1/ui_v1.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/ui_v1.h + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/button_widget.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/button_widget.h + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/check_box_widget.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/check_box_widget.h + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/column_widget.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/column_widget.h + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/container_widget.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/container_widget.h + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/h_scroll_widget.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/h_scroll_widget.h + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/image_widget.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/image_widget.h + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/root_widget.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/root_widget.h + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/row_widget.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/row_widget.h + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/scroll_widget.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/scroll_widget.h + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/stack_widget.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/stack_widget.h + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/text_widget.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/text_widget.h + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/widget.cc + ${BA_SRC_ROOT}/ballistica/ui_v1/widget/widget.h + # AUTOGENERATED_PUBLIC_END + ) + +if (HEADLESS) + set_target_properties(ballisticakit PROPERTIES OUTPUT_NAME "ballisticakit_headless") +endif () + +target_include_directories(ballisticakit PRIVATE + ${Python_INCLUDE_DIRS} + ${BA_SRC_ROOT}/external/open_dynamics_engine-ef + ${EXTRA_INCLUDE_DIRS} + ) + +target_link_libraries(ballisticakit PRIVATE + ${CMAKE_CURRENT_BINARY_DIR}/prefablib/libballisticakit_internal.a ode pthread ${Python_LIBRARIES} + ${SDL2_LIBRARIES} ${EXTRA_LIBRARIES} dl) + +# Hack for building on rpi (might be due to my manually built Python 3.8) +# Hopefully can remove later... +if(EXISTS "/home/pi") +target_link_libraries(ballisticakit PRIVATE dl util stdc++fs) +endif() diff --git a/ballisticacore-windows/Generic/BallisticaCore.rc b/ballisticakit-windows/Generic/BallisticaKit.rc similarity index 91% rename from ballisticacore-windows/Generic/BallisticaCore.rc rename to ballisticakit-windows/Generic/BallisticaKit.rc index c85e175f1c9afba449a477211e9a4147b2df685f..380c3633dce577f7ab000f80bcba01ee0f34efd0 100755 GIT binary patch delta 114 zcmdmHw#00M9~-MTgC|4CQSGr>8{T!PFn nWs_&}euuF)PvukOgsGamTU-{-zANqxQ#9F8OmVZH#5E=WTy-Gt diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj new file mode 100644 index 00000000..5f89f951 --- /dev/null +++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj @@ -0,0 +1,839 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {B0B090EA-92E7-457D-9785-ACDF917F9C93} + Win32Proj + BallisticaKitGeneric + 10.0 + + + + Application + true + Unicode + v142 + + + Application + true + Unicode + v142 + + + Application + false + true + Unicode + v142 + + + Application + false + true + Unicode + v142 + + + + + + + + + + + + + + + + + + + true + $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ + $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ + + + true + $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ + $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ + + + false + $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ + $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ + + + false + $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ + $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ + + + + Use + Level3 + Disabled + WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) + stdafx.h + true + false + SyncCThrow + true + ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef;../../src/external/qrencode-3.4.4 + stdcpp17 + Fast + + + Console + true + + + Default + ../../src/external/windows/lib/$(Platform) + + + + + Use + Level3 + Disabled + WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) + stdafx.h + true + false + SyncCThrow + true + ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef + stdcpp17 + Fast + + + Console + true + + + Default + ../../src/external/windows/lib/$(Platform) + + + + + Level3 + Use + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + stdafx.h + true + SyncCThrow + false + ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef + stdcpp17 + Fast + + + Console + true + true + true + UseFastLinkTimeCodeGeneration + ../../src/external/windows/lib/$(Platform) + + + + + Level3 + Use + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + stdafx.h + true + SyncCThrow + false + ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef + stdcpp17 + Fast + + + Console + true + true + true + UseFastLinkTimeCodeGeneration + ../../src/external/windows/lib/$(Platform) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + + + + + + + + + + + + + + + diff --git a/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters new file mode 100644 index 00000000..4ea8019d --- /dev/null +++ b/ballisticakit-windows/Generic/BallisticaKitGeneric.vcxproj.filters @@ -0,0 +1,1970 @@ + + + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base + + + ballistica\base + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics + + + ballistica\base\dynamics + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\gl + + + ballistica\base\graphics\gl + + + ballistica\base\graphics\gl + + + ballistica\base\graphics\gl + + + ballistica\base\graphics + + + ballistica\base\graphics + + + ballistica\base\graphics + + + ballistica\base\graphics + + + ballistica\base\graphics + + + ballistica\base\graphics + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\renderer + + + ballistica\base\graphics\renderer + + + ballistica\base\graphics\renderer + + + ballistica\base\graphics\renderer + + + ballistica\base\graphics\renderer + + + ballistica\base\graphics\renderer + + + ballistica\base\graphics\renderer + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\text + + + ballistica\base\graphics\text + + + ballistica\base\graphics\text + + + ballistica\base\graphics\text + + + ballistica\base\graphics\text + + + ballistica\base\graphics\text + + + ballistica\base\graphics\text + + + ballistica\base\graphics\texture + + + ballistica\base\graphics\texture + + + ballistica\base\graphics\texture + + + ballistica\base\graphics\texture + + + ballistica\base\graphics\texture + + + ballistica\base\graphics\texture + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input + + + ballistica\base\input + + + ballistica\base\input\support + + + ballistica\base\input\support + + + ballistica\base\logic + + + ballistica\base\logic + + + ballistica\base\networking + + + ballistica\base\networking + + + ballistica\base\networking + + + ballistica\base\networking + + + ballistica\base\networking + + + ballistica\base\networking + + + ballistica\base\platform\android\amazon + + + ballistica\base\platform\android\amazon + + + ballistica\base\platform\android + + + ballistica\base\platform\android + + + ballistica\base\platform\android\cardboard + + + ballistica\base\platform\android\cardboard + + + ballistica\base\platform\android\google + + + ballistica\base\platform\android\google + + + ballistica\base\platform\apple + + + ballistica\base\platform\apple + + + ballistica\base\platform + + + ballistica\base\platform + + + ballistica\base\platform\linux + + + ballistica\base\platform\linux + + + ballistica\base\platform\oculus + + + ballistica\base\platform\support + + + ballistica\base\platform\windows + + + ballistica\base\platform\windows + + + ballistica\base\platform\windows + + + ballistica\base\platform\windows + + + ballistica\base\python + + + ballistica\base\python + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\methods + + + ballistica\base\python\methods + + + ballistica\base\python\methods + + + ballistica\base\python\methods + + + ballistica\base\python\methods + + + ballistica\base\python\methods + + + ballistica\base\python\support + + + ballistica\base\python\support + + + ballistica\base\python\support + + + ballistica\base\support + + + ballistica\base\support + + + ballistica\base\support + + + ballistica\base\support + + + ballistica\base\support + + + ballistica\base\support + + + ballistica\base\support + + + ballistica\base\support + + + ballistica\base\ui + + + ballistica\base\ui + + + ballistica\base\ui + + + ballistica\base\ui + + + ballistica\base\ui + + + ballistica\classic + + + ballistica\classic + + + ballistica\classic\python + + + ballistica\classic\python + + + ballistica\classic\python\methods + + + ballistica\classic\python\methods + + + ballistica\classic\support + + + ballistica\classic\support + + + ballistica\core + + + ballistica\core + + + ballistica\core\platform\apple + + + ballistica\core\platform + + + ballistica\core\platform + + + ballistica\core\platform\linux + + + ballistica\core\platform\linux + + + ballistica\core\platform\support + + + ballistica\core\platform\windows + + + ballistica\core\platform\windows + + + ballistica\core\python + + + ballistica\core\python + + + ballistica\core\support + + + ballistica\core\support + + + ballistica\core\support + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\dynamics + + + ballistica\scene_v1\dynamics + + + ballistica\scene_v1\dynamics + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics + + + ballistica\scene_v1\dynamics + + + ballistica\scene_v1\dynamics + + + ballistica\scene_v1\dynamics + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python + + + ballistica\scene_v1\python + + + ballistica\scene_v1 + + + ballistica\scene_v1 + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\shared + + + ballistica\shared + + + ballistica\shared\buildconfig + + + ballistica\shared\buildconfig + + + ballistica\shared\buildconfig + + + ballistica\shared\buildconfig + + + ballistica\shared\buildconfig + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\networking + + + ballistica\shared\networking + + + ballistica\shared\networking + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\template_fs\python\class + + + ballistica\template_fs\python\class + + + ballistica\template_fs\python\methods + + + ballistica\template_fs\python\methods + + + ballistica\template_fs\python + + + ballistica\template_fs\python + + + ballistica\template_fs + + + ballistica\template_fs + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\methods + + + ballistica\ui_v1\python\methods + + + ballistica\ui_v1\python + + + ballistica\ui_v1\python + + + ballistica\ui_v1\support + + + ballistica\ui_v1\support + + + ballistica\ui_v1 + + + ballistica\ui_v1 + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\qr_code_generator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ballisticacore-windows/Generic/Resource.h b/ballisticakit-windows/Generic/Resource.h similarity index 78% rename from ballisticacore-windows/Generic/Resource.h rename to ballisticakit-windows/Generic/Resource.h index 75ce2eb4..4f3ef08f 100755 --- a/ballisticacore-windows/Generic/Resource.h +++ b/ballisticakit-windows/Generic/Resource.h @@ -1,18 +1,18 @@ //{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. -// Used by BallisticaCore.rc +// Used by BallisticaKit.rc // #define IDS_APP_TITLE 103 #define IDR_MAINFRAME 128 -#define IDD_BALLISTICACORE_DIALOG 102 +#define IDD_BALLISTICAKIT_DIALOG 102 #define IDD_ABOUTBOX 103 #define IDM_ABOUT 104 #define IDM_EXIT 105 -#define IDI_BALLISTICACORE 107 +#define IDI_BALLISTICAKIT 107 #define IDI_SMALL 108 -#define IDC_BALLISTICACORE 109 +#define IDC_BALLISTICAKIT 109 #define IDC_MYICON 2 #ifndef IDC_STATIC #define IDC_STATIC -1 diff --git a/ballisticacore-windows/Generic/stdafx.cpp b/ballisticakit-windows/Generic/stdafx.cpp similarity index 100% rename from ballisticacore-windows/Generic/stdafx.cpp rename to ballisticakit-windows/Generic/stdafx.cpp diff --git a/ballisticacore-windows/Generic/stdafx.h b/ballisticakit-windows/Generic/stdafx.h similarity index 71% rename from ballisticacore-windows/Generic/stdafx.h rename to ballisticakit-windows/Generic/stdafx.h index f15cf639..588fa633 100644 --- a/ballisticacore-windows/Generic/stdafx.h +++ b/ballisticakit-windows/Generic/stdafx.h @@ -5,4 +5,4 @@ #pragma once -#include "ballistica/config/config_windows_generic.h" +#include "ballistica/shared/buildconfig/buildconfig_windows_generic.h" diff --git a/ballisticacore-windows/Generic/targetver.h b/ballisticakit-windows/Generic/targetver.h similarity index 100% rename from ballisticacore-windows/Generic/targetver.h rename to ballisticakit-windows/Generic/targetver.h diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj new file mode 100644 index 00000000..5abdb89d --- /dev/null +++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj @@ -0,0 +1,827 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + {B9012642-2E6D-48A1-90EC-E91799B2198E} + Win32Proj + BallisticaKitHeadless + 10.0 + + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ + $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ + + + true + $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ + $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ + + + false + $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ + $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ + + + false + $(ProjectDir)..\..\build\windows\$(Configuration)_$(Platform)\ + $(ProjectDir)..\..\build\windows\obj\$(MSBuildProjectName)\$(Configuration)_$(Platform)\ + + + + Use + Level3 + Disabled + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + stdafx.h + false + SyncCThrow + ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef + stdcpp17 + Fast + + + Console + true + Default + ../../src/external/windows/lib/$(Platform) + + + + + Use + Level3 + Disabled + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + SyncCThrow + stdafx.h + true + false + ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef + stdcpp17 + Fast + + + Console + true + ../../src/external/windows/lib/$(Platform) + + + + + Level3 + Use + MaxSpeed + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + false + true + stdafx.h + SyncCThrow + ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef + stdcpp17 + Fast + + + Console + true + true + true + ../../src/external/windows/lib/$(Platform) + + + + + Level3 + Use + MaxSpeed + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + false + SyncCThrow + stdafx.h + true + ../../src;../../src/external/windows/include/SDL2;../../src/external/windows/include/python;../../src/external/windows/include;../../src/external/open_dynamics_engine-ef + stdcpp17 + Fast + + + Console + true + true + true + ../../src/external/windows/lib/$(Platform) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + + + + + + + + diff --git a/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters new file mode 100644 index 00000000..4ea8019d --- /dev/null +++ b/ballisticakit-windows/Headless/BallisticaKitHeadless.vcxproj.filters @@ -0,0 +1,1970 @@ + + + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\app + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\assets + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base\audio + + + ballistica\base + + + ballistica\base + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics\bg + + + ballistica\base\dynamics + + + ballistica\base\dynamics + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\component + + + ballistica\base\graphics\gl + + + ballistica\base\graphics\gl + + + ballistica\base\graphics\gl + + + ballistica\base\graphics\gl + + + ballistica\base\graphics + + + ballistica\base\graphics + + + ballistica\base\graphics + + + ballistica\base\graphics + + + ballistica\base\graphics + + + ballistica\base\graphics + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\mesh + + + ballistica\base\graphics\renderer + + + ballistica\base\graphics\renderer + + + ballistica\base\graphics\renderer + + + ballistica\base\graphics\renderer + + + ballistica\base\graphics\renderer + + + ballistica\base\graphics\renderer + + + ballistica\base\graphics\renderer + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\support + + + ballistica\base\graphics\text + + + ballistica\base\graphics\text + + + ballistica\base\graphics\text + + + ballistica\base\graphics\text + + + ballistica\base\graphics\text + + + ballistica\base\graphics\text + + + ballistica\base\graphics\text + + + ballistica\base\graphics\texture + + + ballistica\base\graphics\texture + + + ballistica\base\graphics\texture + + + ballistica\base\graphics\texture + + + ballistica\base\graphics\texture + + + ballistica\base\graphics\texture + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input\device + + + ballistica\base\input + + + ballistica\base\input + + + ballistica\base\input\support + + + ballistica\base\input\support + + + ballistica\base\logic + + + ballistica\base\logic + + + ballistica\base\networking + + + ballistica\base\networking + + + ballistica\base\networking + + + ballistica\base\networking + + + ballistica\base\networking + + + ballistica\base\networking + + + ballistica\base\platform\android\amazon + + + ballistica\base\platform\android\amazon + + + ballistica\base\platform\android + + + ballistica\base\platform\android + + + ballistica\base\platform\android\cardboard + + + ballistica\base\platform\android\cardboard + + + ballistica\base\platform\android\google + + + ballistica\base\platform\android\google + + + ballistica\base\platform\apple + + + ballistica\base\platform\apple + + + ballistica\base\platform + + + ballistica\base\platform + + + ballistica\base\platform\linux + + + ballistica\base\platform\linux + + + ballistica\base\platform\oculus + + + ballistica\base\platform\support + + + ballistica\base\platform\windows + + + ballistica\base\platform\windows + + + ballistica\base\platform\windows + + + ballistica\base\platform\windows + + + ballistica\base\python + + + ballistica\base\python + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\class + + + ballistica\base\python\methods + + + ballistica\base\python\methods + + + ballistica\base\python\methods + + + ballistica\base\python\methods + + + ballistica\base\python\methods + + + ballistica\base\python\methods + + + ballistica\base\python\support + + + ballistica\base\python\support + + + ballistica\base\python\support + + + ballistica\base\support + + + ballistica\base\support + + + ballistica\base\support + + + ballistica\base\support + + + ballistica\base\support + + + ballistica\base\support + + + ballistica\base\support + + + ballistica\base\support + + + ballistica\base\ui + + + ballistica\base\ui + + + ballistica\base\ui + + + ballistica\base\ui + + + ballistica\base\ui + + + ballistica\classic + + + ballistica\classic + + + ballistica\classic\python + + + ballistica\classic\python + + + ballistica\classic\python\methods + + + ballistica\classic\python\methods + + + ballistica\classic\support + + + ballistica\classic\support + + + ballistica\core + + + ballistica\core + + + ballistica\core\platform\apple + + + ballistica\core\platform + + + ballistica\core\platform + + + ballistica\core\platform\linux + + + ballistica\core\platform\linux + + + ballistica\core\platform\support + + + ballistica\core\platform\windows + + + ballistica\core\platform\windows + + + ballistica\core\python + + + ballistica\core\python + + + ballistica\core\support + + + ballistica\core\support + + + ballistica\core\support + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\assets + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\connection + + + ballistica\scene_v1\dynamics + + + ballistica\scene_v1\dynamics + + + ballistica\scene_v1\dynamics + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics\material + + + ballistica\scene_v1\dynamics + + + ballistica\scene_v1\dynamics + + + ballistica\scene_v1\dynamics + + + ballistica\scene_v1\dynamics + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\node + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\class + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python\methods + + + ballistica\scene_v1\python + + + ballistica\scene_v1\python + + + ballistica\scene_v1 + + + ballistica\scene_v1 + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\scene_v1\support + + + ballistica\shared + + + ballistica\shared + + + ballistica\shared\buildconfig + + + ballistica\shared\buildconfig + + + ballistica\shared\buildconfig + + + ballistica\shared\buildconfig + + + ballistica\shared\buildconfig + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\foundation + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\generic + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\math + + + ballistica\shared\networking + + + ballistica\shared\networking + + + ballistica\shared\networking + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\shared\python + + + ballistica\template_fs\python\class + + + ballistica\template_fs\python\class + + + ballistica\template_fs\python\methods + + + ballistica\template_fs\python\methods + + + ballistica\template_fs\python + + + ballistica\template_fs\python + + + ballistica\template_fs + + + ballistica\template_fs + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\class + + + ballistica\ui_v1\python\methods + + + ballistica\ui_v1\python\methods + + + ballistica\ui_v1\python + + + ballistica\ui_v1\python + + + ballistica\ui_v1\support + + + ballistica\ui_v1\support + + + ballistica\ui_v1 + + + ballistica\ui_v1 + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + ballistica\ui_v1\widget + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\open_dynamics_engine-ef\ode + + + external\qr_code_generator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ballisticacore-windows/Headless/stdafx.cpp b/ballisticakit-windows/Headless/stdafx.cpp similarity index 100% rename from ballisticacore-windows/Headless/stdafx.cpp rename to ballisticakit-windows/Headless/stdafx.cpp diff --git a/ballisticacore-windows/Headless/stdafx.h b/ballisticakit-windows/Headless/stdafx.h similarity index 70% rename from ballisticacore-windows/Headless/stdafx.h rename to ballisticakit-windows/Headless/stdafx.h index ced26a68..925f8d00 100644 --- a/ballisticacore-windows/Headless/stdafx.h +++ b/ballisticakit-windows/Headless/stdafx.h @@ -6,4 +6,4 @@ #pragma once -#include "ballistica/config/config_windows_headless.h" +#include "ballistica/shared/buildconfig/buildconfig_windows_headless.h" diff --git a/ballisticacore-windows/Headless/targetver.h b/ballisticakit-windows/Headless/targetver.h similarity index 100% rename from ballisticacore-windows/Headless/targetver.h rename to ballisticakit-windows/Headless/targetver.h diff --git a/config/README.md b/config/README.md index 3c6f0f4b..bb680f27 100644 --- a/config/README.md +++ b/config/README.md @@ -1 +1,8 @@ -This directory is for high level project configuration. +# Ballistica Project Configuration + +This directory contains overall configuration files for the project. + +Noteworthy files: +- **projectconfig.json**: Top level settings for the project. Various tools look for values here. +- **spinoffconfig.json**: Configures how this project can be spun off into other projects and/or what it inherits from a parent project. +- **localconfig.json**: Optional file influencing behavior only at this location. This file should not be stored in git/etc. diff --git a/config/config.json b/config/config.json deleted file mode 100644 index 1fcd9866..00000000 --- a/config/config.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "code_source_dirs": [ - "src/ballistica" - ], - "cpplint_blacklist": [ - "src/ballistica/generic/json.cc", - "src/ballistica/generic/json.h", - "src/ballistica/generic/utf8.cc", - "src/ballistica/graphics/texture/dds.h", - "src/ballistica/graphics/texture/ktx.cc", - "src/ballistica/platform/android/android_gl3.h", - "src/ballistica/platform/apple/app_delegate.h", - "src/ballistica/platform/apple/scripting_bridge_music.h", - "src/ballistica/platform/android/utf8/checked.h", - "src/ballistica/platform/android/utf8/unchecked.h", - "src/ballistica/platform/android/utf8/core.h", - "src/ballistica/platform/apple/sdl_main_mac.h", - "src/ballistica/platform/oculus/main_rift.cc", - "src/ballistica/platform/android/android_gl3.c" - ], - "name": "BallisticaCore", - "public": true, - "pylint_ignored_untracked_deps": [ - "__main__", - "astroid.modutils", - "astroid", - "pylint.lint", - "pytest", - "pytz", - "yaml", - "requests", - "typing_extensions", - "cpplint", - "ansiwrap", - "filelock", - "Cocoa", - "pdoc", - "certifi", - "psutil" - ], - "python_paths": [ - "assets/src/ba_data/python", - "src/meta", - "tools" - ], - "python_source_dirs": [ - "assets/src/ba_data/python", - "assets/src/server", - "assets/src/workspace", - "src/meta", - "tools", - "tests" - ] -} diff --git a/config/featuresets/README.md b/config/featuresets/README.md new file mode 100644 index 00000000..9158857d --- /dev/null +++ b/config/featuresets/README.md @@ -0,0 +1,5 @@ +# Ballistica Feature Sets + +This directory contains a config file for each feature-set in the project. +Feature sets are high level subsets of an engine or app which can be easily +added, removed, duplicated, etc. diff --git a/config/featuresets/featureset_base.py b/config/featuresets/featureset_base.py new file mode 100644 index 00000000..12b293fe --- /dev/null +++ b/config/featuresets/featureset_base.py @@ -0,0 +1,15 @@ +# Released under the MIT License. See LICENSE for details. +# +# pylint: disable=missing-docstring, invalid-name +from __future__ import annotations + +# This file is exec'ed by the spinoff system, allowing us to define +# values and behavior for this feature-set here in a way that can be +# type-checked alongside other project Python code. + +from batools.featureset import FeatureSet + +# Grab the FeatureSet we should apply to. +fset = FeatureSet.get_active() + +fset.requirements = {'core'} diff --git a/config/featuresets/featureset_classic.py b/config/featuresets/featureset_classic.py new file mode 100644 index 00000000..280994ea --- /dev/null +++ b/config/featuresets/featureset_classic.py @@ -0,0 +1,15 @@ +# Released under the MIT License. See LICENSE for details. +# +# pylint: disable=missing-docstring, invalid-name +from __future__ import annotations + +# This file is exec'ed by the spinoff system, allowing us to define +# values and behavior for this feature-set here in a way that can be +# type-checked alongside other project Python code. + +from batools.featureset import FeatureSet + +# Grab the FeatureSet we should apply to. +fset = FeatureSet.get_active() + +fset.requirements = {'base', 'scene_v1', 'ui_v1'} diff --git a/config/featuresets/featureset_core.py b/config/featuresets/featureset_core.py new file mode 100644 index 00000000..47f31147 --- /dev/null +++ b/config/featuresets/featureset_core.py @@ -0,0 +1,25 @@ +# Released under the MIT License. See LICENSE for details. +# +# pylint: disable=missing-docstring, invalid-name +from __future__ import annotations + +# This file is exec'ed by the spinoff system, allowing us to define +# values and behavior for this feature-set here in a way that can be +# type-checked alongside other project Python code. + +from batools.featureset import FeatureSet + +# Grab the FeatureSet we should apply to. +fset = FeatureSet.get_active() + +fset.requirements = set() + +fset.has_native_python_module = False + +# Bits of code we're using that don't conform to our feature-set based +# namespace scheme. +fset.cpp_namespace_check_disable_files = { + 'src/ballistica/core/platform/android/utf8/checked.h', + 'src/ballistica/core/platform/android/utf8/unchecked.h', + 'src/ballistica/core/platform/android/utf8/core.h', +} diff --git a/config/featuresets/featureset_plus.py b/config/featuresets/featureset_plus.py new file mode 100644 index 00000000..5fad68ad --- /dev/null +++ b/config/featuresets/featureset_plus.py @@ -0,0 +1,16 @@ +# Released under the MIT License. See LICENSE for details. +# +# pylint: disable=missing-docstring, invalid-name +from __future__ import annotations + +# This file is exec'ed by the spinoff system, allowing us to define +# values and behavior for this feature-set here in a way that can be +# type-checked alongside other project Python code. + +from batools.featureset import FeatureSet + +# Grab the FeatureSet we should apply to. +fset = FeatureSet.get_active() + +fset.requirements = {'base'} +fset.internal = True diff --git a/config/featuresets/featureset_scene_v1.py b/config/featuresets/featureset_scene_v1.py new file mode 100644 index 00000000..9773718f --- /dev/null +++ b/config/featuresets/featureset_scene_v1.py @@ -0,0 +1,15 @@ +# Released under the MIT License. See LICENSE for details. +# +# pylint: disable=missing-docstring, invalid-name +from __future__ import annotations + +# This file is exec'ed by the spinoff system, allowing us to define +# values and behavior for this feature-set here in a way that can be +# type-checked alongside other project Python code. + +from batools.featureset import FeatureSet + +# Grab the FeatureSet we should apply to. +fset = FeatureSet.get_active() + +fset.requirements = {'base'} diff --git a/config/featuresets/featureset_template_fs.py b/config/featuresets/featureset_template_fs.py new file mode 100644 index 00000000..9773718f --- /dev/null +++ b/config/featuresets/featureset_template_fs.py @@ -0,0 +1,15 @@ +# Released under the MIT License. See LICENSE for details. +# +# pylint: disable=missing-docstring, invalid-name +from __future__ import annotations + +# This file is exec'ed by the spinoff system, allowing us to define +# values and behavior for this feature-set here in a way that can be +# type-checked alongside other project Python code. + +from batools.featureset import FeatureSet + +# Grab the FeatureSet we should apply to. +fset = FeatureSet.get_active() + +fset.requirements = {'base'} diff --git a/config/featuresets/featureset_ui_v1.py b/config/featuresets/featureset_ui_v1.py new file mode 100644 index 00000000..c1883593 --- /dev/null +++ b/config/featuresets/featureset_ui_v1.py @@ -0,0 +1,18 @@ +# Released under the MIT License. See LICENSE for details. +# +# pylint: disable=missing-docstring, invalid-name +from __future__ import annotations + +# This file is exec'ed by the spinoff system, allowing us to define +# values and behavior for this feature-set here in a way that can be +# type-checked alongside other project Python code. + +from batools.featureset import FeatureSet + +# Grab the FeatureSet we should apply to. +fset = FeatureSet.get_active() + +fset.requirements = {'base'} + +# We'd prefer our name's title form to be 'UI V1', not the default 'Ui V1'. +fset.name_title = 'UI V1' diff --git a/config/projectconfig.json b/config/projectconfig.json new file mode 100644 index 00000000..2a988ec5 --- /dev/null +++ b/config/projectconfig.json @@ -0,0 +1,59 @@ +{ + "code_source_dirs": [ + "src/ballistica" + ], + "cpplint_blacklist": [ + "src/ballistica/shared/generic/json.cc", + "src/ballistica/shared/generic/json.h", + "src/ballistica/shared/generic/utf8.cc", + "src/ballistica/base/graphics/texture/dds.h", + "src/ballistica/base/graphics/texture/ktx.cc", + "src/ballistica/core/platform/android/android_gl3.h", + "src/ballistica/core/platform/apple/app_delegate.h", + "src/ballistica/core/platform/apple/scripting_bridge_music.h", + "src/ballistica/core/platform/android/utf8/checked.h", + "src/ballistica/core/platform/android/utf8/unchecked.h", + "src/ballistica/core/platform/android/utf8/core.h", + "src/ballistica/core/platform/apple/sdl_main_mac.h", + "src/ballistica/base/platform/oculus/main_rift.cc", + "src/ballistica/core/platform/android/android_gl3.c" + ], + "name": "BallisticaKit", + "public": true, + "pylint_ignored_untracked_deps": [ + "__main__", + "astroid.modutils", + "astroid", + "pylint.lint", + "pytest", + "pytz", + "yaml", + "requests", + "typing_extensions", + "cpplint", + "ansiwrap", + "filelock", + "Cocoa", + "pdoc", + "certifi", + "psutil", + "pbxproj.XcodeProject", + "pbxproj.pbxextensions", + "openstep_parser" + ], + "python_paths": [ + "src/assets/ba_data/python", + "build/dummymodules", + "src/meta", + "tools" + ], + "python_source_dirs": [ + "src/assets/ba_data/python", + "src/assets/server", + "src/assets/workspace", + "src/meta", + "tools", + "tests", + "config" + ] +} diff --git a/config/spinoffconfig.py b/config/spinoffconfig.py new file mode 100644 index 00000000..bf2f8aa9 --- /dev/null +++ b/config/spinoffconfig.py @@ -0,0 +1,284 @@ +# Released under the MIT License. See LICENSE for details. +# +# pylint: disable=missing-docstring, invalid-name +from __future__ import annotations + +# This file is exec'ed by tools/spinoff, allowing us to customize +# how this src project gits filtered into dst projects. + +from batools.spinoff import SpinoffContext + +# Grab the context we should apply to. +ctx = SpinoffContext.get_active() + +# As a src project, we set up a baseline set of rules based on what +# we contain. The dst project config (exec'ed after us) is then free +# to override based on what they want of ours or what they add +# themselves. + +# Any files/dirs with these base names will be ignored by spinoff +# on both src and dst. +ctx.ignore_names = { + '__pycache__', + '.git', + '.mypy_cache', +} + +# Special set of paths managed by spinoff but ALSO stored in git in +# the dst project. This is for bare minimum stuff needed to be always +# present in dst for bootstrapping, indexing by github, etc). Changes +# to these files in dst will be silently and happily overwritten by +# spinoff, so tread carefully. +ctx.git_mirrored_paths = { + '.gitignore', + '.gitattributes', + 'README.md', + 'config/jenkins', +} + +# File names that can be quietly ignored or cleared out when found. +# This should encompass things like .DS_Store files created by the +# Mac Finder when browsing directories. This helps spinoff remove +# empty directories when doing a 'clean', etc. +ctx.cruft_file_names = {'.DS_Store'} + +# These paths in the src project will be skipped over during updates and +# not synced into the dst project. The dst project can use this to +# trim out parts of the src project that it doesn't want or that it +# intends to 'override' with its own versions. +ctx.src_omit_paths = { + '.gitignore', + 'config/spinoffconfig.py', + 'ballisticakit-android/build', + 'tools/spinoff', + '.editorconfig', +} + +# Use this to 'carve out' directories or exact file paths which will be +# git-managed on dst. By default, spinoff will consider dirs containing +# the files it syncs from src as 'spinoff-managed'; it will set them as +# git-ignored and will complain if any files appear in them that it does +# not manage itself (to prevent accidentally doing work in such places). +# Note that adding a dir to src_write_paths does not prevent files +# within it from being synced by spinoff; it just means that each of +# those individual spinoff-managed files will have their own gitignore +# entry since there is no longer one covering the whole dir. So to keep +# things tidy, carve out the minimal set of exact file/dir paths that you +# need. +ctx.src_write_paths = { + 'tools/spinoff', + 'config/spinoffconfig.py', +} + +# Normally spinoff errors if it finds any files in its managed dirs +# that it did not put there. This is to prevent accidentally working +# in these parts of a dst project; since these sections are git-ignored, +# git itself won't raise any warnings in such cases and it would be easy +# to accidentally lose work otherwise. +# This list can be used to suppress spinoff's errors for specific +# locations. This is generally used to allow build output or other +# dynamically generated files to exist within spinoff-managed +# directories. It is possible to use src_write_paths for such purposes, +# but this has the side-effect of greatly complicating the dst +# project's gitignore list; selectively marking a few dirs as +# unchecked makes for a cleaner setup. Just be careful to not set +# excessively broad regions as unchecked; you don't want to mask +# actual useful error messages. +ctx.src_unchecked_paths = { + 'src/ballistica/mgen', + 'src/ballistica/*/mgen', + 'src/assets/ba_data/python/*/_mgen', + 'src/meta/*/mgen', + 'ballisticakit-cmake/.clang-format', + 'ballisticakit-android/BallisticaKit/src/cardboard/res', + 'ballisticakit-windows/*/BallisticaKit.ico', + 'ballisticakit-xcode/BallisticaKit Shared/Assets.xcassets', + 'ballisticakit-android/BallisticaKit/src/*/res', + 'ballisticakit-android/BallisticaKit/src/*/assets', +} + +# Files at or under these paths are considered 'project' files. +# These files are synced after all other files and go through +# batools.project.Updater class as part of their filtering. +# This allows them to update themselves in the same way as they +# do when running 'make update' for the project; adding the final +# filtered set of project source files to themself, etc. +ctx.project_file_paths = set() + +# Everything actually synced into dst will use the following filter rules: + +# If files are 'filtered' it means they will have all instances +# of BallisticaKit in their names and contents replaced with their +# project name. Other custom filtering can also be applied. Obviously +# filtering should not be run on certain files (binary data, etc.) +# and disabling it where not needed can improve efficiency and make +# backporting easier (editing spinoff-managed files in dst and getting +# those changes back into src). + +# Anything under these dirs WILL be filtered. +ctx.filter_dirs = { + 'ballisticakit-cmake', + 'ballisticakit-xcode/BallisticaKit.xcodeproj', + 'ballisticakit-ios.xcodeproj', + 'ballisticakit-mac.xcodeproj', + 'config', + 'src/assets/pdoc', +} + +# ELSE anything under these dirs will NOT be filtered. +ctx.no_filter_dirs = { + 'src/external', + 'src/assets/pylib-android', + 'src/assets/pylib-apple', + 'src/assets/ba_data/python-site-packages', + 'src/assets/windows', +} + +# ELSE files matching these exact base names WILL be filtered +# (so FOO matches a/b/FOO as well as just FOO). +ctx.filter_file_names = { + 'Makefile', + '.gitignore', + '.gitattributes', + 'README', + 'README.md', + 'bootstrap', + 'configure', + 'Makefile.am', + 'Makefile.in', + 'Jenkinsfile', + 'assets_phase_android', + 'testfoo.py', + 'testfoo2.py', + 'assets_phase_xcode', + 'ballistica_maya_tools.mel', + 'check_python_syntax', + 'compile_python_files', + 'pcommand', + 'vmshell', + 'cloudshell', + 'BUCK', + 'BUCK_WIN', + 'upgrade_vms', + 'flycheck-dir-locals.el', + '.projectile', + '.editorconfig', + 'LICENSE', + 'cloudtool', + 'bacloud', + 'config_template.yaml', +} + +# ELSE files matching these exact base names will NOT be filtered. +ctx.no_filter_file_names = { + 'PVRTexToolCLI', + 'composite', + 'etcpack', + 'astcenc', + 'convert', + 'make_bob', + 'nvcompress', + 'INSTALL', + 'install-sh', + 'LICENSE.txt', + 'gradlew', + '.style.yapf', + '.clang-format', + '.pylintrc', + 'CPPLINT.cfg', + '.mypy.ini', + '._ba_sources_hash', + '._baplus_sources_hash', + '._bascenev1_sources_hash', + '._bauiv1_sources_hash', +} + +# ELSE files with these extensions WILL be filtered. +ctx.filter_file_extensions = { + '.py', + '.md', + '.cpp', + '.cc', + '.c', + '.h', + '.m', + '.mm', + '.metal', + '.swift', + '.storyboard', + '.pbxproj', + '.xcworkspacedata', + '.xcscheme', + '.bat', + '.entitlements', + '.json', + '.plist', + '.strings', + '.ac', + '.m4', + '.txt', + '.settings', + '.in', + '.props', + '.html', + '.cmake', + '.sh', + '.sln', + '.vcxproj', + '.cmd', + '.hlsl', + '.gradle', + '.xml', + '.java', + '.kt', + '.pro', + '.aidl', + '.iml', + '.properties', + '.rc', + '.mk', + '.r', + '.frag', + '.vert', + '.xcsettings', + '.filters', +} + +# ELSE files with these extensions will NOT be filtered. +ctx.no_filter_file_extensions = { + '.png', + '.exe', + '.psd', + '.obj', + '.wav', + '.fdata', + '.icns', + '.ico', + '.lib', + '.dll', + '.dc', + '.generated', + '.default', + '.minimal', + '.spec', + '.x', + '.pl', + '.nib', + '.Porting', + '.touch', + '.MacOSX', + '.ds', + '.WinCE', + '.hgignore', + '.inl', + '.man', + '.BIN', + '.bin', + '.pyd', + '.jar', + '.aar', + '.zip', + '.keystore', + '.bmp', + '.pem', +} diff --git a/config/toolconfigsrc/README b/config/toolconfigsrc/README.md similarity index 90% rename from config/toolconfigsrc/README rename to config/toolconfigsrc/README.md index 7269548d..96e3757b 100644 --- a/config/toolconfigsrc/README +++ b/config/toolconfigsrc/README.md @@ -1,3 +1,5 @@ +# Ballistica Tool Config Source + These configs can be installed via 'make prereqs' in the project root. (this should automatically happen as part of other targets such as 'make check') diff --git a/config/toolconfigsrc/clang-format b/config/toolconfigsrc/clang-format index 7f292443..d28aa940 100644 --- a/config/toolconfigsrc/clang-format +++ b/config/toolconfigsrc/clang-format @@ -7,3 +7,7 @@ DerivePointerAlignment: false # want +, -, etc at beginning of split lines BreakBeforeBinaryOperators: NonAssignment + +# Testing; kinda looks cramped without space but function-call type +# initializations look odd *with* space. Guess I'll leave default for now. +# SpaceBeforeCpp11BracedList: true diff --git a/config/toolconfigsrc/dir-locals.el b/config/toolconfigsrc/dir-locals.el index 81c39f95..099927ae 100644 --- a/config/toolconfigsrc/dir-locals.el +++ b/config/toolconfigsrc/dir-locals.el @@ -6,7 +6,7 @@ (python-mode (jedi:server-args . ("--sys-path" "__EFRO_PROJECT_ROOT__/tools" - "--sys-path" "__EFRO_PROJECT_ROOT__/assets/src/ba_data/python")) + "--sys-path" "__EFRO_PROJECT_ROOT__/src/assets/ba_data/python")) (python-black-extra-args . __PYTHON_BLACK_EXTRA_ARGS__)) ;; Shorter name in projectile status bar to save valuable space. @@ -19,8 +19,8 @@ (nil . ((projectile-globally-ignored-directories . ("docs" "submodules" "src/external" - "assets/src/pylib-android" - "assets/src/pylib-apple" - "assets/src/windows")))) + "src/assets/pylib-android" + "src/assets/pylib-apple" + "src/assets/windows")))) ) diff --git a/config/toolconfigsrc/editorconfig b/config/toolconfigsrc/editorconfig index 8d9056d4..d4f37a4d 100644 --- a/config/toolconfigsrc/editorconfig +++ b/config/toolconfigsrc/editorconfig @@ -13,12 +13,13 @@ root = true end_of_line = lf trim_trailing_whitespace = true insert_final_newline = true +max_line_length = 80 # Python overrides. [*.py] indent_style = space indent_size = 4 -max_line_length = 79 +max_line_length = 80 charset = utf-8 # Makefile overrides. diff --git a/config/toolconfigsrc/mypy.ini b/config/toolconfigsrc/mypy.ini index dc7f3b06..61feb823 100644 --- a/config/toolconfigsrc/mypy.ini +++ b/config/toolconfigsrc/mypy.ini @@ -1,19 +1,8 @@ [mypy] -mypy_path = __EFRO_PROJECT_ROOT__/tools:__EFRO_PROJECT_ROOT__/assets/src/ba_data/python +mypy_path = __EFRO_PYTHON_PATHS__ __EFRO_MYPY_STANDARD_SETTINGS__ -# We have mypy alert us if we use any vars that have been imported -# by other modules; we want to import everything directly from its -# source. However there are some modules that are explicitly exist -# to reexport things, so let's let those pass. -# (we could also set __all__ in those modules, but that's a lot of -# repeating ourself) -[mypy-ba] -no_implicit_reexport = False -[mypy-ba.internal] -no_implicit_reexport = False - [mypy-psutil] ignore_missing_imports = True @@ -41,3 +30,9 @@ disallow_any_unimported = False [mypy-pdoc] ignore_missing_imports = True +[mypy-pbxproj.*] +ignore_missing_imports = True + +[mypy-openstep_parser.*] +ignore_missing_imports = True + diff --git a/resources/Makefile b/resources/Makefile deleted file mode 100644 index a93c1761..00000000 --- a/resources/Makefile +++ /dev/null @@ -1,29 +0,0 @@ -# Released under the MIT License. See LICENSE for details. - -all: resources - -# This section is generated by batools.resourcesmakefile; do not edit by hand. -# __AUTOGENERATED_PUBLIC_BEGIN__ -# __AUTOGENERATED_PUBLIC_END__ - - -# This section is generated by batools.resourcesmakefile; do not edit by hand. -# __AUTOGENERATED_PRIVATE_BEGIN__ - -resources: \ - ../ballisticacore-windows/Generic/BallisticaCore.ico - -clean: clean-private - -clean-private: - rm -f "../ballisticacore-windows/Generic/BallisticaCore.ico" - -../ballisticacore-windows/Generic/BallisticaCore.ico : ../.efrocachemap - @cd .. && tools/pcommand efrocache_get resources/$@ - -efrocache-list: - @echo "../ballisticacore-windows/Generic/BallisticaCore.ico" - -efrocache-build: resources - -# __AUTOGENERATED_PRIVATE_END__ diff --git a/src/README.md b/src/README.md new file mode 100644 index 00000000..a4b6554a --- /dev/null +++ b/src/README.md @@ -0,0 +1,10 @@ +# Ballistica Source + +(Mostly) everything contributing to Ballistica builds lives here. + +### Noteworthy Bits: + +- [assets/ba_data/python](assets/ba_data/python): Where most Python code going + in to the app lives. +- [ballistica](ballistica): Where the native (C++, etc.) layer of the app lives. +- [meta](meta): Code used to generate other code (metaprogramming). diff --git a/assets/.asset_manifest_private.json b/src/assets/.asset_manifest_private.json similarity index 55% rename from assets/.asset_manifest_private.json rename to src/assets/.asset_manifest_private.json index 6b62d1ec..6b57a6d2 100644 --- a/assets/.asset_manifest_private.json +++ b/src/assets/.asset_manifest_private.json @@ -482,497 +482,497 @@ "ba_data/fonts/fontSmall5.fdata", "ba_data/fonts/fontSmall6.fdata", "ba_data/fonts/fontSmall7.fdata", - "ba_data/models/achievementOutline.bob", - "ba_data/models/actionButtonBottom.bob", - "ba_data/models/actionButtonLeft.bob", - "ba_data/models/actionButtonRight.bob", - "ba_data/models/actionButtonTop.bob", - "ba_data/models/actionHeroForeArm.bob", - "ba_data/models/actionHeroHand.bob", - "ba_data/models/actionHeroHead.bob", - "ba_data/models/actionHeroLowerLeg.bob", - "ba_data/models/actionHeroPelvis.bob", - "ba_data/models/actionHeroToes.bob", - "ba_data/models/actionHeroTorso.bob", - "ba_data/models/actionHeroUpperArm.bob", - "ba_data/models/actionHeroUpperLeg.bob", - "ba_data/models/agentForeArm.bob", - "ba_data/models/agentHand.bob", - "ba_data/models/agentHead.bob", - "ba_data/models/agentLowerLeg.bob", - "ba_data/models/agentPelvis.bob", - "ba_data/models/agentToes.bob", - "ba_data/models/agentTorso.bob", - "ba_data/models/agentUpperArm.bob", - "ba_data/models/agentUpperLeg.bob", - "ba_data/models/aliForeArm.bob", - "ba_data/models/aliHand.bob", - "ba_data/models/aliHead.bob", - "ba_data/models/aliLowerLeg.bob", - "ba_data/models/aliPelvis.bob", - "ba_data/models/aliToes.bob", - "ba_data/models/aliTorso.bob", - "ba_data/models/aliUpperArm.bob", - "ba_data/models/aliUpperLeg.bob", - "ba_data/models/alienForeArm.bob", - "ba_data/models/alienHand.bob", - "ba_data/models/alienHead.bob", - "ba_data/models/alienLowerLeg.bob", - "ba_data/models/alienPelvis.bob", - "ba_data/models/alienToes.bob", - "ba_data/models/alienTorso.bob", - "ba_data/models/alienUpperArm.bob", - "ba_data/models/alienUpperLeg.bob", - "ba_data/models/alwaysLandBG.bob", - "ba_data/models/alwaysLandLevel.bob", - "ba_data/models/alwaysLandLevelBottom.bob", - "ba_data/models/alwaysLandLevelCollide.cob", - "ba_data/models/alwaysLandVRFillMound.bob", - "ba_data/models/angryComputerTransparent.bob", - "ba_data/models/arrowBack.bob", - "ba_data/models/arrowFront.bob", - "ba_data/models/assassinForeArm.bob", - "ba_data/models/assassinHand.bob", - "ba_data/models/assassinHead.bob", - "ba_data/models/assassinLowerLeg.bob", - "ba_data/models/assassinPelvis.bob", - "ba_data/models/assassinToes.bob", - "ba_data/models/assassinTorso.bob", - "ba_data/models/assassinUpperArm.bob", - "ba_data/models/assassinUpperLeg.bob", - "ba_data/models/bearForeArm.bob", - "ba_data/models/bearHand.bob", - "ba_data/models/bearHead.bob", - "ba_data/models/bearLowerLeg.bob", - "ba_data/models/bearPelvis.bob", - "ba_data/models/bearToes.bob", - "ba_data/models/bearTorso.bob", - "ba_data/models/bearUpperArm.bob", - "ba_data/models/bearUpperLeg.bob", - "ba_data/models/bigG.bob", - "ba_data/models/bigGBottom.bob", - "ba_data/models/bigGBumper.cob", - "ba_data/models/bigGCollide.cob", - "ba_data/models/bomb.bob", - "ba_data/models/bombSticky.bob", - "ba_data/models/bonesForeArm.bob", - "ba_data/models/bonesHand.bob", - "ba_data/models/bonesHead.bob", - "ba_data/models/bonesLowerLeg.bob", - "ba_data/models/bonesPelvis.bob", - "ba_data/models/bonesToes.bob", - "ba_data/models/bonesTorso.bob", - "ba_data/models/bonesUpperArm.bob", - "ba_data/models/bonesUpperLeg.bob", - "ba_data/models/box.bob", - "ba_data/models/boxingGlove.bob", - "ba_data/models/bridgitLevelBottom.bob", - "ba_data/models/bridgitLevelCollide.cob", - "ba_data/models/bridgitLevelRailingCollide.cob", - "ba_data/models/bridgitLevelTop.bob", - "ba_data/models/bunnyForeArm.bob", - "ba_data/models/bunnyHand.bob", - "ba_data/models/bunnyHead.bob", - "ba_data/models/bunnyLowerLeg.bob", - "ba_data/models/bunnyPelvis.bob", - "ba_data/models/bunnyToes.bob", - "ba_data/models/bunnyTorso.bob", - "ba_data/models/bunnyUpperArm.bob", - "ba_data/models/bunnyUpperLeg.bob", - "ba_data/models/buttonBackOpaque.bob", - "ba_data/models/buttonBackSmallOpaque.bob", - "ba_data/models/buttonBackSmallTransparent.bob", - "ba_data/models/buttonBackTransparent.bob", - "ba_data/models/buttonLargeOpaque.bob", - "ba_data/models/buttonLargeTransparent.bob", - "ba_data/models/buttonLargerOpaque.bob", - "ba_data/models/buttonLargerTransparent.bob", - "ba_data/models/buttonMediumOpaque.bob", - "ba_data/models/buttonMediumTransparent.bob", - "ba_data/models/buttonNull.bob", - "ba_data/models/buttonSmallOpaque.bob", - "ba_data/models/buttonSmallTransparent.bob", - "ba_data/models/buttonSquareOpaque.bob", - "ba_data/models/buttonSquareTransparent.bob", - "ba_data/models/buttonTabOpaque.bob", - "ba_data/models/buttonTabTransparent.bob", - "ba_data/models/checkTransparent.bob", - "ba_data/models/courtyardLevel.bob", - "ba_data/models/courtyardLevelBottom.bob", - "ba_data/models/courtyardLevelCollide.cob", - "ba_data/models/courtyardPlayerWall.cob", - "ba_data/models/cowboyForeArm.bob", - "ba_data/models/cowboyHand.bob", - "ba_data/models/cowboyHead.bob", - "ba_data/models/cowboyLowerLeg.bob", - "ba_data/models/cowboyPelvis.bob", - "ba_data/models/cowboyToes.bob", - "ba_data/models/cowboyTorso.bob", - "ba_data/models/cowboyUpperArm.bob", - "ba_data/models/cowboyUpperLeg.bob", - "ba_data/models/cragCastleLevel.bob", - "ba_data/models/cragCastleLevelBottom.bob", - "ba_data/models/cragCastleLevelBumper.cob", - "ba_data/models/cragCastleLevelCollide.cob", - "ba_data/models/cragCastleVRFillMound.bob", - "ba_data/models/crossOut.bob", - "ba_data/models/currencyMeter.bob", - "ba_data/models/currencyPlusButton.bob", - "ba_data/models/cyborgForeArm.bob", - "ba_data/models/cyborgHand.bob", - "ba_data/models/cyborgHead.bob", - "ba_data/models/cyborgLowerLeg.bob", - "ba_data/models/cyborgPelvis.bob", - "ba_data/models/cyborgToes.bob", - "ba_data/models/cyborgTorso.bob", - "ba_data/models/cyborgUpperArm.bob", - "ba_data/models/cyborgUpperLeg.bob", - "ba_data/models/cylinder.bob", - "ba_data/models/doomShroomBG.bob", - "ba_data/models/doomShroomLevel.bob", - "ba_data/models/doomShroomLevelCollide.cob", - "ba_data/models/doomShroomStem.bob", - "ba_data/models/doomShroomStemCollide.cob", - "ba_data/models/doomShroomVRFill.bob", - "ba_data/models/egg.bob", - "ba_data/models/eyeBall.bob", - "ba_data/models/eyeBallIris.bob", - "ba_data/models/eyeLid.bob", - "ba_data/models/flagPole.bob", - "ba_data/models/flagStand.bob", - "ba_data/models/flash.bob", - "ba_data/models/footballStadium.bob", - "ba_data/models/footballStadiumCollide.cob", - "ba_data/models/footballStadiumVRFill.bob", - "ba_data/models/frameInset.bob", - "ba_data/models/frostyForeArm.bob", - "ba_data/models/frostyHand.bob", - "ba_data/models/frostyHead.bob", - "ba_data/models/frostyLowerLeg.bob", - "ba_data/models/frostyPelvis.bob", - "ba_data/models/frostyToes.bob", - "ba_data/models/frostyTorso.bob", - "ba_data/models/frostyUpperArm.bob", - "ba_data/models/frostyUpperLeg.bob", - "ba_data/models/gladiatorForeArm.bob", - "ba_data/models/gladiatorHand.bob", - "ba_data/models/gladiatorHead.bob", - "ba_data/models/gladiatorLowerLeg.bob", - "ba_data/models/gladiatorPelvis.bob", - "ba_data/models/gladiatorToes.bob", - "ba_data/models/gladiatorTorso.bob", - "ba_data/models/gladiatorUpperArm.bob", - "ba_data/models/gladiatorUpperLeg.bob", - "ba_data/models/hairTuft1.bob", - "ba_data/models/hairTuft1b.bob", - "ba_data/models/hairTuft2.bob", - "ba_data/models/hairTuft3.bob", - "ba_data/models/hairTuft4.bob", - "ba_data/models/heartOpaque.bob", - "ba_data/models/heartTransparent.bob", - "ba_data/models/hockeyStadiumCollide.cob", - "ba_data/models/hockeyStadiumInner.bob", - "ba_data/models/hockeyStadiumOuter.bob", - "ba_data/models/hockeyStadiumStands.bob", - "ba_data/models/image16x1.bob", - "ba_data/models/image1x1.bob", - "ba_data/models/image1x1FullScreen.bob", - "ba_data/models/image1x1VRFullScreen.bob", - "ba_data/models/image2x1.bob", - "ba_data/models/image2x1Vertical.bob", - "ba_data/models/image4x1.bob", - "ba_data/models/impactBomb.bob", - "ba_data/models/jackForeArm.bob", - "ba_data/models/jackHand.bob", - "ba_data/models/jackHead.bob", - "ba_data/models/jackLowerLeg.bob", - "ba_data/models/jackToes.bob", - "ba_data/models/jackTorso.bob", - "ba_data/models/jackUpperArm.bob", - "ba_data/models/jackUpperLeg.bob", - "ba_data/models/jumpsuitForeArm.bob", - "ba_data/models/jumpsuitHand.bob", - "ba_data/models/jumpsuitHead.bob", - "ba_data/models/jumpsuitLowerLeg.bob", - "ba_data/models/jumpsuitPelvis.bob", - "ba_data/models/jumpsuitToes.bob", - "ba_data/models/jumpsuitTorso.bob", - "ba_data/models/jumpsuitUpperArm.bob", - "ba_data/models/jumpsuitUpperLeg.bob", - "ba_data/models/kronkForeArm.bob", - "ba_data/models/kronkHand.bob", - "ba_data/models/kronkHead.bob", - "ba_data/models/kronkLowerLeg.bob", - "ba_data/models/kronkPelvis.bob", - "ba_data/models/kronkToes.bob", - "ba_data/models/kronkTorso.bob", - "ba_data/models/kronkUpperArm.bob", - "ba_data/models/kronkUpperLeg.bob", - "ba_data/models/lakeFrigid.bob", - "ba_data/models/lakeFrigidCollide.cob", - "ba_data/models/lakeFrigidReflections.bob", - "ba_data/models/lakeFrigidTop.bob", - "ba_data/models/lakeFrigidVRFill.bob", - "ba_data/models/landMine.bob", - "ba_data/models/level_select_button_opaque.bob", - "ba_data/models/level_select_button_transparent.bob", - "ba_data/models/locator.bob", - "ba_data/models/locatorBox.bob", - "ba_data/models/locatorCircle.bob", - "ba_data/models/locatorCircleOutline.bob", - "ba_data/models/logo.bob", - "ba_data/models/logoTransparent.bob", - "ba_data/models/melForeArm.bob", - "ba_data/models/melHand.bob", - "ba_data/models/melHead.bob", - "ba_data/models/melLowerLeg.bob", - "ba_data/models/melToes.bob", - "ba_data/models/melTorso.bob", - "ba_data/models/melUpperArm.bob", - "ba_data/models/melUpperLeg.bob", - "ba_data/models/meterTransparent.bob", - "ba_data/models/monkeyFaceLevel.bob", - "ba_data/models/monkeyFaceLevelBottom.bob", - "ba_data/models/monkeyFaceLevelBumper.cob", - "ba_data/models/monkeyFaceLevelCollide.cob", - "ba_data/models/natureBackground.bob", - "ba_data/models/natureBackgroundCollide.cob", - "ba_data/models/natureBackgroundVRFill.bob", - "ba_data/models/neoSpazForeArm.bob", - "ba_data/models/neoSpazHand.bob", - "ba_data/models/neoSpazHead.bob", - "ba_data/models/neoSpazLowerLeg.bob", - "ba_data/models/neoSpazPelvis.bob", - "ba_data/models/neoSpazToes.bob", - "ba_data/models/neoSpazTorso.bob", - "ba_data/models/neoSpazUpperArm.bob", - "ba_data/models/neoSpazUpperLeg.bob", - "ba_data/models/ninjaForeArm.bob", - "ba_data/models/ninjaHand.bob", - "ba_data/models/ninjaHead.bob", - "ba_data/models/ninjaLowerLeg.bob", - "ba_data/models/ninjaPelvis.bob", - "ba_data/models/ninjaToes.bob", - "ba_data/models/ninjaTorso.bob", - "ba_data/models/ninjaUpperArm.bob", - "ba_data/models/ninjaUpperLeg.bob", - "ba_data/models/oldLadyForeArm.bob", - "ba_data/models/oldLadyHand.bob", - "ba_data/models/oldLadyHead.bob", - "ba_data/models/oldLadyLowerLeg.bob", - "ba_data/models/oldLadyPelvis.bob", - "ba_data/models/oldLadyToes.bob", - "ba_data/models/oldLadyTorso.bob", - "ba_data/models/oldLadyUpperArm.bob", - "ba_data/models/oldLadyUpperLeg.bob", - "ba_data/models/operaSingerForeArm.bob", - "ba_data/models/operaSingerHand.bob", - "ba_data/models/operaSingerHead.bob", - "ba_data/models/operaSingerLowerLeg.bob", - "ba_data/models/operaSingerPelvis.bob", - "ba_data/models/operaSingerToes.bob", - "ba_data/models/operaSingerTorso.bob", - "ba_data/models/operaSingerUpperArm.bob", - "ba_data/models/operaSingerUpperLeg.bob", - "ba_data/models/overlayGuide.bob", - "ba_data/models/penguinForeArm.bob", - "ba_data/models/penguinHand.bob", - "ba_data/models/penguinHead.bob", - "ba_data/models/penguinLowerLeg.bob", - "ba_data/models/penguinPelvis.bob", - "ba_data/models/penguinToes.bob", - "ba_data/models/penguinTorso.bob", - "ba_data/models/penguinUpperArm.bob", - "ba_data/models/penguinUpperLeg.bob", - "ba_data/models/pixieForeArm.bob", - "ba_data/models/pixieHand.bob", - "ba_data/models/pixieHead.bob", - "ba_data/models/pixieLowerLeg.bob", - "ba_data/models/pixiePelvis.bob", - "ba_data/models/pixieToes.bob", - "ba_data/models/pixieTorso.bob", - "ba_data/models/pixieUpperArm.bob", - "ba_data/models/pixieUpperLeg.bob", - "ba_data/models/plasticEyesTransparent.bob", - "ba_data/models/playerLineup1Transparent.bob", - "ba_data/models/playerLineup2Transparent.bob", - "ba_data/models/playerLineup3Transparent.bob", - "ba_data/models/playerLineup4Transparent.bob", - "ba_data/models/powerup.bob", - "ba_data/models/powerupSimple.bob", - "ba_data/models/puck.bob", - "ba_data/models/rampageBG.bob", - "ba_data/models/rampageBG2.bob", - "ba_data/models/rampageBumper.cob", - "ba_data/models/rampageLevel.bob", - "ba_data/models/rampageLevelBottom.bob", - "ba_data/models/rampageLevelCollide.cob", - "ba_data/models/rampageVRFill.bob", - "ba_data/models/robotForeArm.bob", - "ba_data/models/robotHand.bob", - "ba_data/models/robotHead.bob", - "ba_data/models/robotLowerLeg.bob", - "ba_data/models/robotPelvis.bob", - "ba_data/models/robotToes.bob", - "ba_data/models/robotTorso.bob", - "ba_data/models/robotUpperArm.bob", - "ba_data/models/robotUpperLeg.bob", - "ba_data/models/roundaboutLevel.bob", - "ba_data/models/roundaboutLevelBottom.bob", - "ba_data/models/roundaboutLevelBumper.cob", - "ba_data/models/roundaboutLevelCollide.cob", - "ba_data/models/runningShoes.bob", - "ba_data/models/santaForeArm.bob", - "ba_data/models/santaHand.bob", - "ba_data/models/santaHead.bob", - "ba_data/models/santaLowerLeg.bob", - "ba_data/models/santaToes.bob", - "ba_data/models/santaTorso.bob", - "ba_data/models/santaUpperArm.bob", - "ba_data/models/santaUpperLeg.bob", - "ba_data/models/scorch.bob", - "ba_data/models/scrollBarThumbOpaque.bob", - "ba_data/models/scrollBarThumbShortOpaque.bob", - "ba_data/models/scrollBarThumbShortSimple.bob", - "ba_data/models/scrollBarThumbShortTransparent.bob", - "ba_data/models/scrollBarThumbSimple.bob", - "ba_data/models/scrollBarThumbTransparent.bob", - "ba_data/models/scrollBarTroughTransparent.bob", - "ba_data/models/scrollWidgetShort.bob", - "ba_data/models/shield.bob", - "ba_data/models/shockWave.bob", - "ba_data/models/shrapnel1.bob", - "ba_data/models/shrapnelBoard.bob", - "ba_data/models/shrapnelSlime.bob", - "ba_data/models/softEdgeInside.bob", - "ba_data/models/softEdgeOutside.bob", - "ba_data/models/stepRightUpLevel.bob", - "ba_data/models/stepRightUpLevelBottom.bob", - "ba_data/models/stepRightUpLevelCollide.cob", - "ba_data/models/stepRightUpVRFillMound.bob", - "ba_data/models/superheroForeArm.bob", - "ba_data/models/superheroHand.bob", - "ba_data/models/superheroHead.bob", - "ba_data/models/superheroLowerLeg.bob", - "ba_data/models/superheroPelvis.bob", - "ba_data/models/superheroToes.bob", - "ba_data/models/superheroTorso.bob", - "ba_data/models/superheroUpperArm.bob", - "ba_data/models/superheroUpperLeg.bob", - "ba_data/models/textBoxTransparent.bob", - "ba_data/models/thePadBG.bob", - "ba_data/models/thePadBGSmall.bob", - "ba_data/models/thePadLevel.bob", - "ba_data/models/thePadLevelBottom.bob", - "ba_data/models/thePadLevelBumper.cob", - "ba_data/models/thePadLevelCollide.cob", - "ba_data/models/thePadVRFillBottom.bob", - "ba_data/models/thePadVRFillMound.bob", - "ba_data/models/thePadVRFillTop.bob", - "ba_data/models/tipTopBG.bob", - "ba_data/models/tipTopLevel.bob", - "ba_data/models/tipTopLevelBottom.bob", - "ba_data/models/tipTopLevelBumper.cob", - "ba_data/models/tipTopLevelCollide.cob", - "ba_data/models/tnt.bob", - "ba_data/models/toolbarBacking.bob", - "ba_data/models/toolbarBackingBottom.bob", - "ba_data/models/toolbarBackingBottom2.bob", - "ba_data/models/toolbarBackingOpaque.bob", - "ba_data/models/toolbarBackingTop.bob", - "ba_data/models/toolbarBackingTop2.bob", - "ba_data/models/toolbarBackingTransparent.bob", - "ba_data/models/towerDLevel.bob", - "ba_data/models/towerDLevelBottom.bob", - "ba_data/models/towerDLevelCollide.cob", - "ba_data/models/towerDPlayerWall.cob", - "ba_data/models/trees.bob", - "ba_data/models/vrFade.bob", - "ba_data/models/vrOverlay.bob", - "ba_data/models/warriorForeArm.bob", - "ba_data/models/warriorHand.bob", - "ba_data/models/warriorHead.bob", - "ba_data/models/warriorLowerLeg.bob", - "ba_data/models/warriorPelvis.bob", - "ba_data/models/warriorToes.bob", - "ba_data/models/warriorTorso.bob", - "ba_data/models/warriorUpperArm.bob", - "ba_data/models/warriorUpperLeg.bob", - "ba_data/models/windowBGBlotch.bob", - "ba_data/models/windowHSmallVMedOpaque.bob", - "ba_data/models/windowHSmallVMedTransparent.bob", - "ba_data/models/windowHSmallVSmallOpaque.bob", - "ba_data/models/windowHSmallVSmallTransparent.bob", - "ba_data/models/wing.bob", - "ba_data/models/witchForeArm.bob", - "ba_data/models/witchHand.bob", - "ba_data/models/witchHead.bob", - "ba_data/models/witchLowerLeg.bob", - "ba_data/models/witchPelvis.bob", - "ba_data/models/witchToes.bob", - "ba_data/models/witchTorso.bob", - "ba_data/models/witchUpperArm.bob", - "ba_data/models/witchUpperLeg.bob", - "ba_data/models/wizardForeArm.bob", - "ba_data/models/wizardHand.bob", - "ba_data/models/wizardHead.bob", - "ba_data/models/wizardLowerLeg.bob", - "ba_data/models/wizardPelvis.bob", - "ba_data/models/wizardToes.bob", - "ba_data/models/wizardTorso.bob", - "ba_data/models/wizardUpperArm.bob", - "ba_data/models/wizardUpperLeg.bob", - "ba_data/models/wrestlerForeArm.bob", - "ba_data/models/wrestlerHand.bob", - "ba_data/models/wrestlerHead.bob", - "ba_data/models/wrestlerLowerLeg.bob", - "ba_data/models/wrestlerPelvis.bob", - "ba_data/models/wrestlerToes.bob", - "ba_data/models/wrestlerTorso.bob", - "ba_data/models/wrestlerUpperArm.bob", - "ba_data/models/wrestlerUpperLeg.bob", - "ba_data/models/zigZagLevel.bob", - "ba_data/models/zigZagLevelBottom.bob", - "ba_data/models/zigZagLevelBumper.cob", - "ba_data/models/zigZagLevelCollide.cob", - "ba_data/models/zoeForeArm.bob", - "ba_data/models/zoeHand.bob", - "ba_data/models/zoeHead.bob", - "ba_data/models/zoeLowerLeg.bob", - "ba_data/models/zoePelvis.bob", - "ba_data/models/zoeToes.bob", - "ba_data/models/zoeTorso.bob", - "ba_data/models/zoeUpperArm.bob", - "ba_data/models/zoeUpperLeg.bob", - "ba_data/python-site-packages/__pycache__/typing_extensions.cpython-310.opt-1.pyc", + "ba_data/meshes/achievementOutline.bob", + "ba_data/meshes/actionButtonBottom.bob", + "ba_data/meshes/actionButtonLeft.bob", + "ba_data/meshes/actionButtonRight.bob", + "ba_data/meshes/actionButtonTop.bob", + "ba_data/meshes/actionHeroForeArm.bob", + "ba_data/meshes/actionHeroHand.bob", + "ba_data/meshes/actionHeroHead.bob", + "ba_data/meshes/actionHeroLowerLeg.bob", + "ba_data/meshes/actionHeroPelvis.bob", + "ba_data/meshes/actionHeroToes.bob", + "ba_data/meshes/actionHeroTorso.bob", + "ba_data/meshes/actionHeroUpperArm.bob", + "ba_data/meshes/actionHeroUpperLeg.bob", + "ba_data/meshes/agentForeArm.bob", + "ba_data/meshes/agentHand.bob", + "ba_data/meshes/agentHead.bob", + "ba_data/meshes/agentLowerLeg.bob", + "ba_data/meshes/agentPelvis.bob", + "ba_data/meshes/agentToes.bob", + "ba_data/meshes/agentTorso.bob", + "ba_data/meshes/agentUpperArm.bob", + "ba_data/meshes/agentUpperLeg.bob", + "ba_data/meshes/aliForeArm.bob", + "ba_data/meshes/aliHand.bob", + "ba_data/meshes/aliHead.bob", + "ba_data/meshes/aliLowerLeg.bob", + "ba_data/meshes/aliPelvis.bob", + "ba_data/meshes/aliToes.bob", + "ba_data/meshes/aliTorso.bob", + "ba_data/meshes/aliUpperArm.bob", + "ba_data/meshes/aliUpperLeg.bob", + "ba_data/meshes/alienForeArm.bob", + "ba_data/meshes/alienHand.bob", + "ba_data/meshes/alienHead.bob", + "ba_data/meshes/alienLowerLeg.bob", + "ba_data/meshes/alienPelvis.bob", + "ba_data/meshes/alienToes.bob", + "ba_data/meshes/alienTorso.bob", + "ba_data/meshes/alienUpperArm.bob", + "ba_data/meshes/alienUpperLeg.bob", + "ba_data/meshes/alwaysLandBG.bob", + "ba_data/meshes/alwaysLandLevel.bob", + "ba_data/meshes/alwaysLandLevelBottom.bob", + "ba_data/meshes/alwaysLandLevelCollide.cob", + "ba_data/meshes/alwaysLandVRFillMound.bob", + "ba_data/meshes/angryComputerTransparent.bob", + "ba_data/meshes/arrowBack.bob", + "ba_data/meshes/arrowFront.bob", + "ba_data/meshes/assassinForeArm.bob", + "ba_data/meshes/assassinHand.bob", + "ba_data/meshes/assassinHead.bob", + "ba_data/meshes/assassinLowerLeg.bob", + "ba_data/meshes/assassinPelvis.bob", + "ba_data/meshes/assassinToes.bob", + "ba_data/meshes/assassinTorso.bob", + "ba_data/meshes/assassinUpperArm.bob", + "ba_data/meshes/assassinUpperLeg.bob", + "ba_data/meshes/bearForeArm.bob", + "ba_data/meshes/bearHand.bob", + "ba_data/meshes/bearHead.bob", + "ba_data/meshes/bearLowerLeg.bob", + "ba_data/meshes/bearPelvis.bob", + "ba_data/meshes/bearToes.bob", + "ba_data/meshes/bearTorso.bob", + "ba_data/meshes/bearUpperArm.bob", + "ba_data/meshes/bearUpperLeg.bob", + "ba_data/meshes/bigG.bob", + "ba_data/meshes/bigGBottom.bob", + "ba_data/meshes/bigGBumper.cob", + "ba_data/meshes/bigGCollide.cob", + "ba_data/meshes/bomb.bob", + "ba_data/meshes/bombSticky.bob", + "ba_data/meshes/bonesForeArm.bob", + "ba_data/meshes/bonesHand.bob", + "ba_data/meshes/bonesHead.bob", + "ba_data/meshes/bonesLowerLeg.bob", + "ba_data/meshes/bonesPelvis.bob", + "ba_data/meshes/bonesToes.bob", + "ba_data/meshes/bonesTorso.bob", + "ba_data/meshes/bonesUpperArm.bob", + "ba_data/meshes/bonesUpperLeg.bob", + "ba_data/meshes/box.bob", + "ba_data/meshes/boxingGlove.bob", + "ba_data/meshes/bridgitLevelBottom.bob", + "ba_data/meshes/bridgitLevelCollide.cob", + "ba_data/meshes/bridgitLevelRailingCollide.cob", + "ba_data/meshes/bridgitLevelTop.bob", + "ba_data/meshes/bunnyForeArm.bob", + "ba_data/meshes/bunnyHand.bob", + "ba_data/meshes/bunnyHead.bob", + "ba_data/meshes/bunnyLowerLeg.bob", + "ba_data/meshes/bunnyPelvis.bob", + "ba_data/meshes/bunnyToes.bob", + "ba_data/meshes/bunnyTorso.bob", + "ba_data/meshes/bunnyUpperArm.bob", + "ba_data/meshes/bunnyUpperLeg.bob", + "ba_data/meshes/buttonBackOpaque.bob", + "ba_data/meshes/buttonBackSmallOpaque.bob", + "ba_data/meshes/buttonBackSmallTransparent.bob", + "ba_data/meshes/buttonBackTransparent.bob", + "ba_data/meshes/buttonLargeOpaque.bob", + "ba_data/meshes/buttonLargeTransparent.bob", + "ba_data/meshes/buttonLargerOpaque.bob", + "ba_data/meshes/buttonLargerTransparent.bob", + "ba_data/meshes/buttonMediumOpaque.bob", + "ba_data/meshes/buttonMediumTransparent.bob", + "ba_data/meshes/buttonNull.bob", + "ba_data/meshes/buttonSmallOpaque.bob", + "ba_data/meshes/buttonSmallTransparent.bob", + "ba_data/meshes/buttonSquareOpaque.bob", + "ba_data/meshes/buttonSquareTransparent.bob", + "ba_data/meshes/buttonTabOpaque.bob", + "ba_data/meshes/buttonTabTransparent.bob", + "ba_data/meshes/checkTransparent.bob", + "ba_data/meshes/courtyardLevel.bob", + "ba_data/meshes/courtyardLevelBottom.bob", + "ba_data/meshes/courtyardLevelCollide.cob", + "ba_data/meshes/courtyardPlayerWall.cob", + "ba_data/meshes/cowboyForeArm.bob", + "ba_data/meshes/cowboyHand.bob", + "ba_data/meshes/cowboyHead.bob", + "ba_data/meshes/cowboyLowerLeg.bob", + "ba_data/meshes/cowboyPelvis.bob", + "ba_data/meshes/cowboyToes.bob", + "ba_data/meshes/cowboyTorso.bob", + "ba_data/meshes/cowboyUpperArm.bob", + "ba_data/meshes/cowboyUpperLeg.bob", + "ba_data/meshes/cragCastleLevel.bob", + "ba_data/meshes/cragCastleLevelBottom.bob", + "ba_data/meshes/cragCastleLevelBumper.cob", + "ba_data/meshes/cragCastleLevelCollide.cob", + "ba_data/meshes/cragCastleVRFillMound.bob", + "ba_data/meshes/crossOut.bob", + "ba_data/meshes/currencyMeter.bob", + "ba_data/meshes/currencyPlusButton.bob", + "ba_data/meshes/cyborgForeArm.bob", + "ba_data/meshes/cyborgHand.bob", + "ba_data/meshes/cyborgHead.bob", + "ba_data/meshes/cyborgLowerLeg.bob", + "ba_data/meshes/cyborgPelvis.bob", + "ba_data/meshes/cyborgToes.bob", + "ba_data/meshes/cyborgTorso.bob", + "ba_data/meshes/cyborgUpperArm.bob", + "ba_data/meshes/cyborgUpperLeg.bob", + "ba_data/meshes/cylinder.bob", + "ba_data/meshes/doomShroomBG.bob", + "ba_data/meshes/doomShroomLevel.bob", + "ba_data/meshes/doomShroomLevelCollide.cob", + "ba_data/meshes/doomShroomStem.bob", + "ba_data/meshes/doomShroomStemCollide.cob", + "ba_data/meshes/doomShroomVRFill.bob", + "ba_data/meshes/egg.bob", + "ba_data/meshes/eyeBall.bob", + "ba_data/meshes/eyeBallIris.bob", + "ba_data/meshes/eyeLid.bob", + "ba_data/meshes/flagPole.bob", + "ba_data/meshes/flagStand.bob", + "ba_data/meshes/flash.bob", + "ba_data/meshes/footballStadium.bob", + "ba_data/meshes/footballStadiumCollide.cob", + "ba_data/meshes/footballStadiumVRFill.bob", + "ba_data/meshes/frameInset.bob", + "ba_data/meshes/frostyForeArm.bob", + "ba_data/meshes/frostyHand.bob", + "ba_data/meshes/frostyHead.bob", + "ba_data/meshes/frostyLowerLeg.bob", + "ba_data/meshes/frostyPelvis.bob", + "ba_data/meshes/frostyToes.bob", + "ba_data/meshes/frostyTorso.bob", + "ba_data/meshes/frostyUpperArm.bob", + "ba_data/meshes/frostyUpperLeg.bob", + "ba_data/meshes/gladiatorForeArm.bob", + "ba_data/meshes/gladiatorHand.bob", + "ba_data/meshes/gladiatorHead.bob", + "ba_data/meshes/gladiatorLowerLeg.bob", + "ba_data/meshes/gladiatorPelvis.bob", + "ba_data/meshes/gladiatorToes.bob", + "ba_data/meshes/gladiatorTorso.bob", + "ba_data/meshes/gladiatorUpperArm.bob", + "ba_data/meshes/gladiatorUpperLeg.bob", + "ba_data/meshes/hairTuft1.bob", + "ba_data/meshes/hairTuft1b.bob", + "ba_data/meshes/hairTuft2.bob", + "ba_data/meshes/hairTuft3.bob", + "ba_data/meshes/hairTuft4.bob", + "ba_data/meshes/heartOpaque.bob", + "ba_data/meshes/heartTransparent.bob", + "ba_data/meshes/hockeyStadiumCollide.cob", + "ba_data/meshes/hockeyStadiumInner.bob", + "ba_data/meshes/hockeyStadiumOuter.bob", + "ba_data/meshes/hockeyStadiumStands.bob", + "ba_data/meshes/image16x1.bob", + "ba_data/meshes/image1x1.bob", + "ba_data/meshes/image1x1FullScreen.bob", + "ba_data/meshes/image1x1VRFullScreen.bob", + "ba_data/meshes/image2x1.bob", + "ba_data/meshes/image2x1Vertical.bob", + "ba_data/meshes/image4x1.bob", + "ba_data/meshes/impactBomb.bob", + "ba_data/meshes/jackForeArm.bob", + "ba_data/meshes/jackHand.bob", + "ba_data/meshes/jackHead.bob", + "ba_data/meshes/jackLowerLeg.bob", + "ba_data/meshes/jackToes.bob", + "ba_data/meshes/jackTorso.bob", + "ba_data/meshes/jackUpperArm.bob", + "ba_data/meshes/jackUpperLeg.bob", + "ba_data/meshes/jumpsuitForeArm.bob", + "ba_data/meshes/jumpsuitHand.bob", + "ba_data/meshes/jumpsuitHead.bob", + "ba_data/meshes/jumpsuitLowerLeg.bob", + "ba_data/meshes/jumpsuitPelvis.bob", + "ba_data/meshes/jumpsuitToes.bob", + "ba_data/meshes/jumpsuitTorso.bob", + "ba_data/meshes/jumpsuitUpperArm.bob", + "ba_data/meshes/jumpsuitUpperLeg.bob", + "ba_data/meshes/kronkForeArm.bob", + "ba_data/meshes/kronkHand.bob", + "ba_data/meshes/kronkHead.bob", + "ba_data/meshes/kronkLowerLeg.bob", + "ba_data/meshes/kronkPelvis.bob", + "ba_data/meshes/kronkToes.bob", + "ba_data/meshes/kronkTorso.bob", + "ba_data/meshes/kronkUpperArm.bob", + "ba_data/meshes/kronkUpperLeg.bob", + "ba_data/meshes/lakeFrigid.bob", + "ba_data/meshes/lakeFrigidCollide.cob", + "ba_data/meshes/lakeFrigidReflections.bob", + "ba_data/meshes/lakeFrigidTop.bob", + "ba_data/meshes/lakeFrigidVRFill.bob", + "ba_data/meshes/landMine.bob", + "ba_data/meshes/level_select_button_opaque.bob", + "ba_data/meshes/level_select_button_transparent.bob", + "ba_data/meshes/locator.bob", + "ba_data/meshes/locatorBox.bob", + "ba_data/meshes/locatorCircle.bob", + "ba_data/meshes/locatorCircleOutline.bob", + "ba_data/meshes/logo.bob", + "ba_data/meshes/logoTransparent.bob", + "ba_data/meshes/melForeArm.bob", + "ba_data/meshes/melHand.bob", + "ba_data/meshes/melHead.bob", + "ba_data/meshes/melLowerLeg.bob", + "ba_data/meshes/melToes.bob", + "ba_data/meshes/melTorso.bob", + "ba_data/meshes/melUpperArm.bob", + "ba_data/meshes/melUpperLeg.bob", + "ba_data/meshes/meterTransparent.bob", + "ba_data/meshes/monkeyFaceLevel.bob", + "ba_data/meshes/monkeyFaceLevelBottom.bob", + "ba_data/meshes/monkeyFaceLevelBumper.cob", + "ba_data/meshes/monkeyFaceLevelCollide.cob", + "ba_data/meshes/natureBackground.bob", + "ba_data/meshes/natureBackgroundCollide.cob", + "ba_data/meshes/natureBackgroundVRFill.bob", + "ba_data/meshes/neoSpazForeArm.bob", + "ba_data/meshes/neoSpazHand.bob", + "ba_data/meshes/neoSpazHead.bob", + "ba_data/meshes/neoSpazLowerLeg.bob", + "ba_data/meshes/neoSpazPelvis.bob", + "ba_data/meshes/neoSpazToes.bob", + "ba_data/meshes/neoSpazTorso.bob", + "ba_data/meshes/neoSpazUpperArm.bob", + "ba_data/meshes/neoSpazUpperLeg.bob", + "ba_data/meshes/ninjaForeArm.bob", + "ba_data/meshes/ninjaHand.bob", + "ba_data/meshes/ninjaHead.bob", + "ba_data/meshes/ninjaLowerLeg.bob", + "ba_data/meshes/ninjaPelvis.bob", + "ba_data/meshes/ninjaToes.bob", + "ba_data/meshes/ninjaTorso.bob", + "ba_data/meshes/ninjaUpperArm.bob", + "ba_data/meshes/ninjaUpperLeg.bob", + "ba_data/meshes/oldLadyForeArm.bob", + "ba_data/meshes/oldLadyHand.bob", + "ba_data/meshes/oldLadyHead.bob", + "ba_data/meshes/oldLadyLowerLeg.bob", + "ba_data/meshes/oldLadyPelvis.bob", + "ba_data/meshes/oldLadyToes.bob", + "ba_data/meshes/oldLadyTorso.bob", + "ba_data/meshes/oldLadyUpperArm.bob", + "ba_data/meshes/oldLadyUpperLeg.bob", + "ba_data/meshes/operaSingerForeArm.bob", + "ba_data/meshes/operaSingerHand.bob", + "ba_data/meshes/operaSingerHead.bob", + "ba_data/meshes/operaSingerLowerLeg.bob", + "ba_data/meshes/operaSingerPelvis.bob", + "ba_data/meshes/operaSingerToes.bob", + "ba_data/meshes/operaSingerTorso.bob", + "ba_data/meshes/operaSingerUpperArm.bob", + "ba_data/meshes/operaSingerUpperLeg.bob", + "ba_data/meshes/overlayGuide.bob", + "ba_data/meshes/penguinForeArm.bob", + "ba_data/meshes/penguinHand.bob", + "ba_data/meshes/penguinHead.bob", + "ba_data/meshes/penguinLowerLeg.bob", + "ba_data/meshes/penguinPelvis.bob", + "ba_data/meshes/penguinToes.bob", + "ba_data/meshes/penguinTorso.bob", + "ba_data/meshes/penguinUpperArm.bob", + "ba_data/meshes/penguinUpperLeg.bob", + "ba_data/meshes/pixieForeArm.bob", + "ba_data/meshes/pixieHand.bob", + "ba_data/meshes/pixieHead.bob", + "ba_data/meshes/pixieLowerLeg.bob", + "ba_data/meshes/pixiePelvis.bob", + "ba_data/meshes/pixieToes.bob", + "ba_data/meshes/pixieTorso.bob", + "ba_data/meshes/pixieUpperArm.bob", + "ba_data/meshes/pixieUpperLeg.bob", + "ba_data/meshes/plasticEyesTransparent.bob", + "ba_data/meshes/playerLineup1Transparent.bob", + "ba_data/meshes/playerLineup2Transparent.bob", + "ba_data/meshes/playerLineup3Transparent.bob", + "ba_data/meshes/playerLineup4Transparent.bob", + "ba_data/meshes/powerup.bob", + "ba_data/meshes/powerupSimple.bob", + "ba_data/meshes/puck.bob", + "ba_data/meshes/rampageBG.bob", + "ba_data/meshes/rampageBG2.bob", + "ba_data/meshes/rampageBumper.cob", + "ba_data/meshes/rampageLevel.bob", + "ba_data/meshes/rampageLevelBottom.bob", + "ba_data/meshes/rampageLevelCollide.cob", + "ba_data/meshes/rampageVRFill.bob", + "ba_data/meshes/robotForeArm.bob", + "ba_data/meshes/robotHand.bob", + "ba_data/meshes/robotHead.bob", + "ba_data/meshes/robotLowerLeg.bob", + "ba_data/meshes/robotPelvis.bob", + "ba_data/meshes/robotToes.bob", + "ba_data/meshes/robotTorso.bob", + "ba_data/meshes/robotUpperArm.bob", + "ba_data/meshes/robotUpperLeg.bob", + "ba_data/meshes/roundaboutLevel.bob", + "ba_data/meshes/roundaboutLevelBottom.bob", + "ba_data/meshes/roundaboutLevelBumper.cob", + "ba_data/meshes/roundaboutLevelCollide.cob", + "ba_data/meshes/runningShoes.bob", + "ba_data/meshes/santaForeArm.bob", + "ba_data/meshes/santaHand.bob", + "ba_data/meshes/santaHead.bob", + "ba_data/meshes/santaLowerLeg.bob", + "ba_data/meshes/santaToes.bob", + "ba_data/meshes/santaTorso.bob", + "ba_data/meshes/santaUpperArm.bob", + "ba_data/meshes/santaUpperLeg.bob", + "ba_data/meshes/scorch.bob", + "ba_data/meshes/scrollBarThumbOpaque.bob", + "ba_data/meshes/scrollBarThumbShortOpaque.bob", + "ba_data/meshes/scrollBarThumbShortSimple.bob", + "ba_data/meshes/scrollBarThumbShortTransparent.bob", + "ba_data/meshes/scrollBarThumbSimple.bob", + "ba_data/meshes/scrollBarThumbTransparent.bob", + "ba_data/meshes/scrollBarTroughTransparent.bob", + "ba_data/meshes/scrollWidgetShort.bob", + "ba_data/meshes/shield.bob", + "ba_data/meshes/shockWave.bob", + "ba_data/meshes/shrapnel1.bob", + "ba_data/meshes/shrapnelBoard.bob", + "ba_data/meshes/shrapnelSlime.bob", + "ba_data/meshes/softEdgeInside.bob", + "ba_data/meshes/softEdgeOutside.bob", + "ba_data/meshes/stepRightUpLevel.bob", + "ba_data/meshes/stepRightUpLevelBottom.bob", + "ba_data/meshes/stepRightUpLevelCollide.cob", + "ba_data/meshes/stepRightUpVRFillMound.bob", + "ba_data/meshes/superheroForeArm.bob", + "ba_data/meshes/superheroHand.bob", + "ba_data/meshes/superheroHead.bob", + "ba_data/meshes/superheroLowerLeg.bob", + "ba_data/meshes/superheroPelvis.bob", + "ba_data/meshes/superheroToes.bob", + "ba_data/meshes/superheroTorso.bob", + "ba_data/meshes/superheroUpperArm.bob", + "ba_data/meshes/superheroUpperLeg.bob", + "ba_data/meshes/textBoxTransparent.bob", + "ba_data/meshes/thePadBG.bob", + "ba_data/meshes/thePadBGSmall.bob", + "ba_data/meshes/thePadLevel.bob", + "ba_data/meshes/thePadLevelBottom.bob", + "ba_data/meshes/thePadLevelBumper.cob", + "ba_data/meshes/thePadLevelCollide.cob", + "ba_data/meshes/thePadVRFillBottom.bob", + "ba_data/meshes/thePadVRFillMound.bob", + "ba_data/meshes/thePadVRFillTop.bob", + "ba_data/meshes/tipTopBG.bob", + "ba_data/meshes/tipTopLevel.bob", + "ba_data/meshes/tipTopLevelBottom.bob", + "ba_data/meshes/tipTopLevelBumper.cob", + "ba_data/meshes/tipTopLevelCollide.cob", + "ba_data/meshes/tnt.bob", + "ba_data/meshes/toolbarBacking.bob", + "ba_data/meshes/toolbarBackingBottom.bob", + "ba_data/meshes/toolbarBackingBottom2.bob", + "ba_data/meshes/toolbarBackingOpaque.bob", + "ba_data/meshes/toolbarBackingTop.bob", + "ba_data/meshes/toolbarBackingTop2.bob", + "ba_data/meshes/toolbarBackingTransparent.bob", + "ba_data/meshes/towerDLevel.bob", + "ba_data/meshes/towerDLevelBottom.bob", + "ba_data/meshes/towerDLevelCollide.cob", + "ba_data/meshes/towerDPlayerWall.cob", + "ba_data/meshes/trees.bob", + "ba_data/meshes/vrFade.bob", + "ba_data/meshes/vrOverlay.bob", + "ba_data/meshes/warriorForeArm.bob", + "ba_data/meshes/warriorHand.bob", + "ba_data/meshes/warriorHead.bob", + "ba_data/meshes/warriorLowerLeg.bob", + "ba_data/meshes/warriorPelvis.bob", + "ba_data/meshes/warriorToes.bob", + "ba_data/meshes/warriorTorso.bob", + "ba_data/meshes/warriorUpperArm.bob", + "ba_data/meshes/warriorUpperLeg.bob", + "ba_data/meshes/windowBGBlotch.bob", + "ba_data/meshes/windowHSmallVMedOpaque.bob", + "ba_data/meshes/windowHSmallVMedTransparent.bob", + "ba_data/meshes/windowHSmallVSmallOpaque.bob", + "ba_data/meshes/windowHSmallVSmallTransparent.bob", + "ba_data/meshes/wing.bob", + "ba_data/meshes/witchForeArm.bob", + "ba_data/meshes/witchHand.bob", + "ba_data/meshes/witchHead.bob", + "ba_data/meshes/witchLowerLeg.bob", + "ba_data/meshes/witchPelvis.bob", + "ba_data/meshes/witchToes.bob", + "ba_data/meshes/witchTorso.bob", + "ba_data/meshes/witchUpperArm.bob", + "ba_data/meshes/witchUpperLeg.bob", + "ba_data/meshes/wizardForeArm.bob", + "ba_data/meshes/wizardHand.bob", + "ba_data/meshes/wizardHead.bob", + "ba_data/meshes/wizardLowerLeg.bob", + "ba_data/meshes/wizardPelvis.bob", + "ba_data/meshes/wizardToes.bob", + "ba_data/meshes/wizardTorso.bob", + "ba_data/meshes/wizardUpperArm.bob", + "ba_data/meshes/wizardUpperLeg.bob", + "ba_data/meshes/wrestlerForeArm.bob", + "ba_data/meshes/wrestlerHand.bob", + "ba_data/meshes/wrestlerHead.bob", + "ba_data/meshes/wrestlerLowerLeg.bob", + "ba_data/meshes/wrestlerPelvis.bob", + "ba_data/meshes/wrestlerToes.bob", + "ba_data/meshes/wrestlerTorso.bob", + "ba_data/meshes/wrestlerUpperArm.bob", + "ba_data/meshes/wrestlerUpperLeg.bob", + "ba_data/meshes/zigZagLevel.bob", + "ba_data/meshes/zigZagLevelBottom.bob", + "ba_data/meshes/zigZagLevelBumper.cob", + "ba_data/meshes/zigZagLevelCollide.cob", + "ba_data/meshes/zoeForeArm.bob", + "ba_data/meshes/zoeHand.bob", + "ba_data/meshes/zoeHead.bob", + "ba_data/meshes/zoeLowerLeg.bob", + "ba_data/meshes/zoePelvis.bob", + "ba_data/meshes/zoeToes.bob", + "ba_data/meshes/zoeTorso.bob", + "ba_data/meshes/zoeUpperArm.bob", + "ba_data/meshes/zoeUpperLeg.bob", + "ba_data/python-site-packages/__pycache__/typing_extensions.cpython-311.opt-1.pyc", "ba_data/python-site-packages/_yaml/__init__.py", - "ba_data/python-site-packages/_yaml/__pycache__/__init__.cpython-310.opt-1.pyc", + "ba_data/python-site-packages/_yaml/__pycache__/__init__.cpython-311.opt-1.pyc", "ba_data/python-site-packages/certifi/__init__.py", "ba_data/python-site-packages/certifi/__main__.py", - "ba_data/python-site-packages/certifi/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/certifi/__pycache__/__main__.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/certifi/__pycache__/core.cpython-310.opt-1.pyc", + "ba_data/python-site-packages/certifi/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/certifi/__pycache__/__main__.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/certifi/__pycache__/core.cpython-311.opt-1.pyc", "ba_data/python-site-packages/certifi/cacert.pem", "ba_data/python-site-packages/certifi/core.py", "ba_data/python-site-packages/typing_extensions.py", "ba_data/python-site-packages/yaml/__init__.py", - "ba_data/python-site-packages/yaml/__pycache__/__init__.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/composer.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/constructor.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/cyaml.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/dumper.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/emitter.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/error.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/events.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/loader.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/nodes.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/parser.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/reader.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/representer.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/resolver.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/scanner.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/serializer.cpython-310.opt-1.pyc", - "ba_data/python-site-packages/yaml/__pycache__/tokens.cpython-310.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/composer.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/constructor.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/cyaml.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/dumper.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/emitter.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/error.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/events.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/loader.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/nodes.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/parser.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/reader.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/representer.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/resolver.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/scanner.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/serializer.cpython-311.opt-1.pyc", + "ba_data/python-site-packages/yaml/__pycache__/tokens.cpython-311.opt-1.pyc", "ba_data/python-site-packages/yaml/composer.py", "ba_data/python-site-packages/yaml/constructor.py", "ba_data/python-site-packages/yaml/cyaml.py", @@ -2605,175 +2605,183 @@ "ba_data/textures/zoeIconColorMask.pvr", "ba_data/textures/zoeIconColorMask_preview.png", "ba_data/textures/zoeIcon_preview.png", + "mac_disk_image/__pycache__/dmgbuild_settings.cpython-311.opt-1.pyc", + "mac_disk_image/dmgbuild_settings.py", "pylib-android/__future__.py", - "pylib-android/__phello__.foo.py", - "pylib-android/__pycache__/__future__.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/__phello__.foo.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/_aix_support.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/_bootsubprocess.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/_collections_abc.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/_compat_pickle.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/_compression.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/_markupbase.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/_osx_support.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/_py_abc.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/_pydecimal.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/_pyio.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/_sitebuiltins.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/_strptime.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/_threading_local.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/_weakrefset.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/abc.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/aifc.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/antigravity.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/argparse.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/ast.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/asynchat.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/asyncore.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/base64.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/bdb.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/binhex.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/bisect.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/bz2.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/cProfile.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/calendar.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/cgi.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/cgitb.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/chunk.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/cmd.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/code.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/codecs.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/codeop.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/colorsys.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/compileall.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/configparser.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/contextlib.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/contextvars.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/copy.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/copyreg.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/crypt.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/csv.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/dataclasses.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/datetime.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/decimal.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/difflib.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/dis.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/doctest.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/enum.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/filecmp.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/fileinput.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/fnmatch.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/fractions.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/ftplib.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/functools.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/genericpath.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/getopt.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/getpass.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/gettext.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/glob.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/graphlib.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/gzip.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/hashlib.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/heapq.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/hmac.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/imghdr.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/imp.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/inspect.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/io.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/ipaddress.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/keyword.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/linecache.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/locale.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/lzma.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/mailbox.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/mailcap.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/mimetypes.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/modulefinder.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/netrc.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/nntplib.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/ntpath.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/nturl2path.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/numbers.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/opcode.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/operator.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/optparse.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/os.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/pathlib.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/pdb.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/pickle.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/pickletools.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/pipes.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/pkgutil.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/platform.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/plistlib.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/poplib.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/posixpath.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/pprint.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/profile.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/pstats.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/pty.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/py_compile.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/pyclbr.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/pydoc.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/queue.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/quopri.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/random.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/re.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/reprlib.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/rlcompleter.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/runpy.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/sched.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/secrets.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/selectors.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/shelve.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/shlex.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/shutil.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/signal.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/site.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/smtpd.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/smtplib.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/sndhdr.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/socket.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/socketserver.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/sre_compile.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/sre_constants.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/sre_parse.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/ssl.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/stat.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/statistics.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/string.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/stringprep.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/struct.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/subprocess.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/sunau.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/symtable.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/sysconfig.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/tabnanny.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/tarfile.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/telnetlib.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/tempfile.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/textwrap.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/this.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/threading.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/timeit.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/token.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/tokenize.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/trace.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/traceback.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/tracemalloc.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/tty.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/types.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/typing.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/uu.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/uuid.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/warnings.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/wave.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/weakref.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/webbrowser.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/xdrlib.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/zipapp.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/zipfile.cpython-310.opt-1.pyc", - "pylib-android/__pycache__/zipimport.cpython-310.opt-1.pyc", + "pylib-android/__hello__.py", + "pylib-android/__pycache__/__future__.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/__hello__.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_aix_support.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_bootsubprocess.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_collections_abc.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_compat_pickle.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_compression.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_markupbase.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_osx_support.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_py_abc.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_pydecimal.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_pyio.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_sitebuiltins.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_strptime.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_sysconfigdata__linux_aarch64-linux-android.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_sysconfigdata__linux_arm-linux-androideabi.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_sysconfigdata__linux_i686-linux-android.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_sysconfigdata__linux_x86_64-linux-android.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_sysconfigdata_d_linux_aarch64-linux-android.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_sysconfigdata_d_linux_arm-linux-androideabi.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_sysconfigdata_d_linux_i686-linux-android.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_sysconfigdata_d_linux_x86_64-linux-android.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_threading_local.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/_weakrefset.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/abc.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/aifc.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/antigravity.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/argparse.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/ast.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/asynchat.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/asyncore.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/base64.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/bdb.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/bisect.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/bz2.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/cProfile.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/calendar.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/cgi.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/cgitb.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/chunk.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/cmd.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/code.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/codecs.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/codeop.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/colorsys.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/compileall.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/configparser.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/contextlib.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/contextvars.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/copy.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/copyreg.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/crypt.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/csv.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/dataclasses.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/datetime.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/decimal.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/difflib.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/dis.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/doctest.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/enum.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/filecmp.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/fileinput.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/fnmatch.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/fractions.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/ftplib.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/functools.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/genericpath.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/getopt.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/getpass.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/gettext.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/glob.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/graphlib.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/gzip.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/hashlib.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/heapq.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/hmac.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/imghdr.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/imp.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/inspect.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/io.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/ipaddress.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/keyword.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/linecache.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/locale.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/lzma.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/mailbox.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/mailcap.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/mimetypes.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/modulefinder.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/netrc.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/nntplib.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/ntpath.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/nturl2path.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/numbers.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/opcode.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/operator.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/optparse.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/os.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/pathlib.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/pdb.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/pickle.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/pickletools.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/pipes.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/pkgutil.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/platform.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/plistlib.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/poplib.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/posixpath.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/pprint.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/profile.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/pstats.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/pty.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/py_compile.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/pyclbr.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/pydoc.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/queue.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/quopri.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/random.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/reprlib.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/rlcompleter.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/runpy.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/sched.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/secrets.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/selectors.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/shelve.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/shlex.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/shutil.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/signal.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/site.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/smtpd.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/smtplib.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/sndhdr.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/socket.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/socketserver.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/sre_compile.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/sre_constants.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/sre_parse.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/ssl.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/stat.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/statistics.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/string.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/stringprep.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/struct.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/subprocess.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/sunau.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/symtable.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/sysconfig.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/tabnanny.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/tarfile.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/telnetlib.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/tempfile.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/textwrap.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/this.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/threading.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/timeit.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/token.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/tokenize.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/trace.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/traceback.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/tracemalloc.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/tty.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/types.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/typing.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/uu.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/uuid.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/warnings.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/wave.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/weakref.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/webbrowser.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/xdrlib.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/zipapp.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/zipfile.cpython-311.opt-1.pyc", + "pylib-android/__pycache__/zipimport.cpython-311.opt-1.pyc", "pylib-android/_aix_support.py", "pylib-android/_bootsubprocess.py", "pylib-android/_collections_abc.py", @@ -2786,6 +2794,14 @@ "pylib-android/_pyio.py", "pylib-android/_sitebuiltins.py", "pylib-android/_strptime.py", + "pylib-android/_sysconfigdata__linux_aarch64-linux-android.py", + "pylib-android/_sysconfigdata__linux_arm-linux-androideabi.py", + "pylib-android/_sysconfigdata__linux_i686-linux-android.py", + "pylib-android/_sysconfigdata__linux_x86_64-linux-android.py", + "pylib-android/_sysconfigdata_d_linux_aarch64-linux-android.py", + "pylib-android/_sysconfigdata_d_linux_arm-linux-androideabi.py", + "pylib-android/_sysconfigdata_d_linux_i686-linux-android.py", + "pylib-android/_sysconfigdata_d_linux_x86_64-linux-android.py", "pylib-android/_threading_local.py", "pylib-android/_weakrefset.py", "pylib-android/abc.py", @@ -2796,37 +2812,39 @@ "pylib-android/asynchat.py", "pylib-android/asyncio/__init__.py", "pylib-android/asyncio/__main__.py", - "pylib-android/asyncio/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/__main__.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/base_events.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/base_futures.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/base_subprocess.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/base_tasks.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/constants.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/coroutines.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/events.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/exceptions.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/format_helpers.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/futures.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/locks.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/log.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/mixins.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/proactor_events.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/protocols.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/queues.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/runners.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/selector_events.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/sslproto.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/staggered.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/streams.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/subprocess.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/tasks.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/threads.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/transports.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/trsock.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/unix_events.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/windows_events.cpython-310.opt-1.pyc", - "pylib-android/asyncio/__pycache__/windows_utils.cpython-310.opt-1.pyc", + "pylib-android/asyncio/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/__main__.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/base_events.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/base_futures.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/base_subprocess.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/base_tasks.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/constants.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/coroutines.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/events.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/exceptions.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/format_helpers.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/futures.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/locks.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/log.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/mixins.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/proactor_events.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/protocols.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/queues.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/runners.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/selector_events.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/sslproto.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/staggered.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/streams.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/subprocess.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/taskgroups.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/tasks.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/threads.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/timeouts.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/transports.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/trsock.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/unix_events.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/windows_events.cpython-311.opt-1.pyc", + "pylib-android/asyncio/__pycache__/windows_utils.cpython-311.opt-1.pyc", "pylib-android/asyncio/base_events.py", "pylib-android/asyncio/base_futures.py", "pylib-android/asyncio/base_subprocess.py", @@ -2849,8 +2867,10 @@ "pylib-android/asyncio/staggered.py", "pylib-android/asyncio/streams.py", "pylib-android/asyncio/subprocess.py", + "pylib-android/asyncio/taskgroups.py", "pylib-android/asyncio/tasks.py", "pylib-android/asyncio/threads.py", + "pylib-android/asyncio/timeouts.py", "pylib-android/asyncio/transports.py", "pylib-android/asyncio/trsock.py", "pylib-android/asyncio/unix_events.py", @@ -2859,7 +2879,6 @@ "pylib-android/asyncore.py", "pylib-android/base64.py", "pylib-android/bdb.py", - "pylib-android/binhex.py", "pylib-android/bisect.py", "pylib-android/bz2.py", "pylib-android/cProfile.py", @@ -2872,18 +2891,18 @@ "pylib-android/codecs.py", "pylib-android/codeop.py", "pylib-android/collections/__init__.py", - "pylib-android/collections/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/collections/__pycache__/abc.cpython-310.opt-1.pyc", + "pylib-android/collections/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/collections/__pycache__/abc.cpython-311.opt-1.pyc", "pylib-android/collections/abc.py", "pylib-android/colorsys.py", "pylib-android/compileall.py", "pylib-android/concurrent/__init__.py", - "pylib-android/concurrent/__pycache__/__init__.cpython-310.opt-1.pyc", + "pylib-android/concurrent/__pycache__/__init__.cpython-311.opt-1.pyc", "pylib-android/concurrent/futures/__init__.py", - "pylib-android/concurrent/futures/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/concurrent/futures/__pycache__/_base.cpython-310.opt-1.pyc", - "pylib-android/concurrent/futures/__pycache__/process.cpython-310.opt-1.pyc", - "pylib-android/concurrent/futures/__pycache__/thread.cpython-310.opt-1.pyc", + "pylib-android/concurrent/futures/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/concurrent/futures/__pycache__/_base.cpython-311.opt-1.pyc", + "pylib-android/concurrent/futures/__pycache__/process.cpython-311.opt-1.pyc", + "pylib-android/concurrent/futures/__pycache__/thread.cpython-311.opt-1.pyc", "pylib-android/concurrent/futures/_base.py", "pylib-android/concurrent/futures/process.py", "pylib-android/concurrent/futures/thread.py", @@ -2895,29 +2914,29 @@ "pylib-android/crypt.py", "pylib-android/csv.py", "pylib-android/ctypes/__init__.py", - "pylib-android/ctypes/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/ctypes/__pycache__/_aix.cpython-310.opt-1.pyc", - "pylib-android/ctypes/__pycache__/_endian.cpython-310.opt-1.pyc", - "pylib-android/ctypes/__pycache__/util.cpython-310.opt-1.pyc", - "pylib-android/ctypes/__pycache__/wintypes.cpython-310.opt-1.pyc", + "pylib-android/ctypes/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/ctypes/__pycache__/_aix.cpython-311.opt-1.pyc", + "pylib-android/ctypes/__pycache__/_endian.cpython-311.opt-1.pyc", + "pylib-android/ctypes/__pycache__/util.cpython-311.opt-1.pyc", + "pylib-android/ctypes/__pycache__/wintypes.cpython-311.opt-1.pyc", "pylib-android/ctypes/_aix.py", "pylib-android/ctypes/_endian.py", "pylib-android/ctypes/macholib/__init__.py", - "pylib-android/ctypes/macholib/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/ctypes/macholib/__pycache__/dyld.cpython-310.opt-1.pyc", - "pylib-android/ctypes/macholib/__pycache__/dylib.cpython-310.opt-1.pyc", - "pylib-android/ctypes/macholib/__pycache__/framework.cpython-310.opt-1.pyc", + "pylib-android/ctypes/macholib/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/ctypes/macholib/__pycache__/dyld.cpython-311.opt-1.pyc", + "pylib-android/ctypes/macholib/__pycache__/dylib.cpython-311.opt-1.pyc", + "pylib-android/ctypes/macholib/__pycache__/framework.cpython-311.opt-1.pyc", "pylib-android/ctypes/macholib/dyld.py", "pylib-android/ctypes/macholib/dylib.py", "pylib-android/ctypes/macholib/framework.py", "pylib-android/ctypes/util.py", "pylib-android/ctypes/wintypes.py", "pylib-android/curses/__init__.py", - "pylib-android/curses/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/curses/__pycache__/ascii.cpython-310.opt-1.pyc", - "pylib-android/curses/__pycache__/has_key.cpython-310.opt-1.pyc", - "pylib-android/curses/__pycache__/panel.cpython-310.opt-1.pyc", - "pylib-android/curses/__pycache__/textpad.cpython-310.opt-1.pyc", + "pylib-android/curses/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/curses/__pycache__/ascii.cpython-311.opt-1.pyc", + "pylib-android/curses/__pycache__/has_key.cpython-311.opt-1.pyc", + "pylib-android/curses/__pycache__/panel.cpython-311.opt-1.pyc", + "pylib-android/curses/__pycache__/textpad.cpython-311.opt-1.pyc", "pylib-android/curses/ascii.py", "pylib-android/curses/has_key.py", "pylib-android/curses/panel.py", @@ -2929,26 +2948,26 @@ "pylib-android/dis.py", "pylib-android/doctest.py", "pylib-android/email/__init__.py", - "pylib-android/email/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/_encoded_words.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/_header_value_parser.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/_parseaddr.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/_policybase.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/base64mime.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/charset.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/contentmanager.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/encoders.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/errors.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/feedparser.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/generator.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/header.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/headerregistry.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/iterators.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/message.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/parser.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/policy.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/quoprimime.cpython-310.opt-1.pyc", - "pylib-android/email/__pycache__/utils.cpython-310.opt-1.pyc", + "pylib-android/email/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/_encoded_words.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/_header_value_parser.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/_parseaddr.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/_policybase.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/base64mime.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/charset.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/contentmanager.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/encoders.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/errors.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/feedparser.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/generator.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/header.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/headerregistry.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/iterators.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/message.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/parser.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/policy.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/quoprimime.cpython-311.opt-1.pyc", + "pylib-android/email/__pycache__/utils.cpython-311.opt-1.pyc", "pylib-android/email/_encoded_words.py", "pylib-android/email/_header_value_parser.py", "pylib-android/email/_parseaddr.py", @@ -2965,15 +2984,15 @@ "pylib-android/email/iterators.py", "pylib-android/email/message.py", "pylib-android/email/mime/__init__.py", - "pylib-android/email/mime/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/email/mime/__pycache__/application.cpython-310.opt-1.pyc", - "pylib-android/email/mime/__pycache__/audio.cpython-310.opt-1.pyc", - "pylib-android/email/mime/__pycache__/base.cpython-310.opt-1.pyc", - "pylib-android/email/mime/__pycache__/image.cpython-310.opt-1.pyc", - "pylib-android/email/mime/__pycache__/message.cpython-310.opt-1.pyc", - "pylib-android/email/mime/__pycache__/multipart.cpython-310.opt-1.pyc", - "pylib-android/email/mime/__pycache__/nonmultipart.cpython-310.opt-1.pyc", - "pylib-android/email/mime/__pycache__/text.cpython-310.opt-1.pyc", + "pylib-android/email/mime/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/email/mime/__pycache__/application.cpython-311.opt-1.pyc", + "pylib-android/email/mime/__pycache__/audio.cpython-311.opt-1.pyc", + "pylib-android/email/mime/__pycache__/base.cpython-311.opt-1.pyc", + "pylib-android/email/mime/__pycache__/image.cpython-311.opt-1.pyc", + "pylib-android/email/mime/__pycache__/message.cpython-311.opt-1.pyc", + "pylib-android/email/mime/__pycache__/multipart.cpython-311.opt-1.pyc", + "pylib-android/email/mime/__pycache__/nonmultipart.cpython-311.opt-1.pyc", + "pylib-android/email/mime/__pycache__/text.cpython-311.opt-1.pyc", "pylib-android/email/mime/application.py", "pylib-android/email/mime/audio.py", "pylib-android/email/mime/base.py", @@ -2987,128 +3006,128 @@ "pylib-android/email/quoprimime.py", "pylib-android/email/utils.py", "pylib-android/encodings/__init__.py", - "pylib-android/encodings/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/aliases.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/ascii.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/base64_codec.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/big5.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/big5hkscs.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/bz2_codec.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/charmap.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp037.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp1006.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp1026.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp1125.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp1140.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp1250.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp1251.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp1252.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp1253.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp1254.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp1255.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp1256.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp1257.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp1258.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp273.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp424.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp437.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp500.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp720.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp737.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp775.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp850.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp852.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp855.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp856.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp857.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp858.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp860.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp861.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp862.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp863.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp864.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp865.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp866.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp869.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp874.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp875.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp932.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp949.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/cp950.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/euc_jis_2004.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/euc_jisx0213.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/euc_jp.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/euc_kr.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/gb18030.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/gb2312.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/gbk.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/hex_codec.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/hp_roman8.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/hz.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/idna.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso2022_jp.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso2022_jp_1.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso2022_jp_2.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso2022_jp_2004.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso2022_jp_3.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso2022_jp_ext.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso2022_kr.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso8859_1.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso8859_10.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso8859_11.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso8859_13.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso8859_14.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso8859_15.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso8859_16.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso8859_2.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso8859_3.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso8859_4.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso8859_5.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso8859_6.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso8859_7.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso8859_8.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/iso8859_9.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/johab.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/koi8_r.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/koi8_t.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/koi8_u.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/kz1048.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/latin_1.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/mac_arabic.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/mac_croatian.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/mac_cyrillic.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/mac_farsi.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/mac_greek.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/mac_iceland.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/mac_latin2.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/mac_roman.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/mac_romanian.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/mac_turkish.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/mbcs.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/oem.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/palmos.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/ptcp154.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/punycode.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/quopri_codec.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/raw_unicode_escape.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/rot_13.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/shift_jis.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/shift_jis_2004.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/shift_jisx0213.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/tis_620.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/undefined.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/unicode_escape.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/utf_16.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/utf_16_be.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/utf_16_le.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/utf_32.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/utf_32_be.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/utf_32_le.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/utf_7.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/utf_8.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/utf_8_sig.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/uu_codec.cpython-310.opt-1.pyc", - "pylib-android/encodings/__pycache__/zlib_codec.cpython-310.opt-1.pyc", + "pylib-android/encodings/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/aliases.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/ascii.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/base64_codec.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/big5.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/big5hkscs.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/bz2_codec.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/charmap.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp037.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp1006.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp1026.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp1125.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp1140.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp1250.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp1251.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp1252.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp1253.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp1254.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp1255.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp1256.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp1257.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp1258.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp273.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp424.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp437.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp500.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp720.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp737.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp775.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp850.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp852.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp855.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp856.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp857.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp858.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp860.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp861.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp862.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp863.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp864.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp865.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp866.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp869.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp874.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp875.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp932.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp949.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/cp950.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/euc_jis_2004.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/euc_jisx0213.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/euc_jp.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/euc_kr.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/gb18030.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/gb2312.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/gbk.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/hex_codec.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/hp_roman8.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/hz.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/idna.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso2022_jp.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso2022_jp_1.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso2022_jp_2.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso2022_jp_2004.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso2022_jp_3.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso2022_jp_ext.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso2022_kr.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso8859_1.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso8859_10.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso8859_11.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso8859_13.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso8859_14.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso8859_15.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso8859_16.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso8859_2.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso8859_3.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso8859_4.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso8859_5.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso8859_6.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso8859_7.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso8859_8.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/iso8859_9.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/johab.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/koi8_r.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/koi8_t.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/koi8_u.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/kz1048.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/latin_1.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/mac_arabic.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/mac_croatian.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/mac_cyrillic.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/mac_farsi.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/mac_greek.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/mac_iceland.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/mac_latin2.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/mac_roman.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/mac_romanian.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/mac_turkish.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/mbcs.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/oem.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/palmos.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/ptcp154.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/punycode.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/quopri_codec.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/raw_unicode_escape.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/rot_13.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/shift_jis.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/shift_jis_2004.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/shift_jisx0213.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/tis_620.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/undefined.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/unicode_escape.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/utf_16.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/utf_16_be.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/utf_16_le.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/utf_32.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/utf_32_be.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/utf_32_le.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/utf_7.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/utf_8.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/utf_8_sig.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/uu_codec.cpython-311.opt-1.pyc", + "pylib-android/encodings/__pycache__/zlib_codec.cpython-311.opt-1.pyc", "pylib-android/encodings/aliases.py", "pylib-android/encodings/ascii.py", "pylib-android/encodings/base64_codec.py", @@ -3248,17 +3267,17 @@ "pylib-android/heapq.py", "pylib-android/hmac.py", "pylib-android/html/__init__.py", - "pylib-android/html/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/html/__pycache__/entities.cpython-310.opt-1.pyc", - "pylib-android/html/__pycache__/parser.cpython-310.opt-1.pyc", + "pylib-android/html/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/html/__pycache__/entities.cpython-311.opt-1.pyc", + "pylib-android/html/__pycache__/parser.cpython-311.opt-1.pyc", "pylib-android/html/entities.py", "pylib-android/html/parser.py", "pylib-android/http/__init__.py", - "pylib-android/http/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/http/__pycache__/client.cpython-310.opt-1.pyc", - "pylib-android/http/__pycache__/cookiejar.cpython-310.opt-1.pyc", - "pylib-android/http/__pycache__/cookies.cpython-310.opt-1.pyc", - "pylib-android/http/__pycache__/server.cpython-310.opt-1.pyc", + "pylib-android/http/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/http/__pycache__/client.cpython-311.opt-1.pyc", + "pylib-android/http/__pycache__/cookiejar.cpython-311.opt-1.pyc", + "pylib-android/http/__pycache__/cookies.cpython-311.opt-1.pyc", + "pylib-android/http/__pycache__/server.cpython-311.opt-1.pyc", "pylib-android/http/client.py", "pylib-android/http/cookiejar.py", "pylib-android/http/cookies.py", @@ -3266,32 +3285,28 @@ "pylib-android/imghdr.py", "pylib-android/imp.py", "pylib-android/importlib/__init__.py", - "pylib-android/importlib/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/importlib/__pycache__/_abc.cpython-310.opt-1.pyc", - "pylib-android/importlib/__pycache__/_adapters.cpython-310.opt-1.pyc", - "pylib-android/importlib/__pycache__/_bootstrap.cpython-310.opt-1.pyc", - "pylib-android/importlib/__pycache__/_bootstrap_external.cpython-310.opt-1.pyc", - "pylib-android/importlib/__pycache__/_common.cpython-310.opt-1.pyc", - "pylib-android/importlib/__pycache__/abc.cpython-310.opt-1.pyc", - "pylib-android/importlib/__pycache__/machinery.cpython-310.opt-1.pyc", - "pylib-android/importlib/__pycache__/readers.cpython-310.opt-1.pyc", - "pylib-android/importlib/__pycache__/resources.cpython-310.opt-1.pyc", - "pylib-android/importlib/__pycache__/util.cpython-310.opt-1.pyc", + "pylib-android/importlib/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/importlib/__pycache__/_abc.cpython-311.opt-1.pyc", + "pylib-android/importlib/__pycache__/_bootstrap.cpython-311.opt-1.pyc", + "pylib-android/importlib/__pycache__/_bootstrap_external.cpython-311.opt-1.pyc", + "pylib-android/importlib/__pycache__/abc.cpython-311.opt-1.pyc", + "pylib-android/importlib/__pycache__/machinery.cpython-311.opt-1.pyc", + "pylib-android/importlib/__pycache__/readers.cpython-311.opt-1.pyc", + "pylib-android/importlib/__pycache__/simple.cpython-311.opt-1.pyc", + "pylib-android/importlib/__pycache__/util.cpython-311.opt-1.pyc", "pylib-android/importlib/_abc.py", - "pylib-android/importlib/_adapters.py", "pylib-android/importlib/_bootstrap.py", "pylib-android/importlib/_bootstrap_external.py", - "pylib-android/importlib/_common.py", "pylib-android/importlib/abc.py", "pylib-android/importlib/machinery.py", "pylib-android/importlib/metadata/__init__.py", - "pylib-android/importlib/metadata/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/importlib/metadata/__pycache__/_adapters.cpython-310.opt-1.pyc", - "pylib-android/importlib/metadata/__pycache__/_collections.cpython-310.opt-1.pyc", - "pylib-android/importlib/metadata/__pycache__/_functools.cpython-310.opt-1.pyc", - "pylib-android/importlib/metadata/__pycache__/_itertools.cpython-310.opt-1.pyc", - "pylib-android/importlib/metadata/__pycache__/_meta.cpython-310.opt-1.pyc", - "pylib-android/importlib/metadata/__pycache__/_text.cpython-310.opt-1.pyc", + "pylib-android/importlib/metadata/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/importlib/metadata/__pycache__/_adapters.cpython-311.opt-1.pyc", + "pylib-android/importlib/metadata/__pycache__/_collections.cpython-311.opt-1.pyc", + "pylib-android/importlib/metadata/__pycache__/_functools.cpython-311.opt-1.pyc", + "pylib-android/importlib/metadata/__pycache__/_itertools.cpython-311.opt-1.pyc", + "pylib-android/importlib/metadata/__pycache__/_meta.cpython-311.opt-1.pyc", + "pylib-android/importlib/metadata/__pycache__/_text.cpython-311.opt-1.pyc", "pylib-android/importlib/metadata/_adapters.py", "pylib-android/importlib/metadata/_collections.py", "pylib-android/importlib/metadata/_functools.py", @@ -3299,17 +3314,33 @@ "pylib-android/importlib/metadata/_meta.py", "pylib-android/importlib/metadata/_text.py", "pylib-android/importlib/readers.py", - "pylib-android/importlib/resources.py", + "pylib-android/importlib/resources/__init__.py", + "pylib-android/importlib/resources/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/importlib/resources/__pycache__/_adapters.cpython-311.opt-1.pyc", + "pylib-android/importlib/resources/__pycache__/_common.cpython-311.opt-1.pyc", + "pylib-android/importlib/resources/__pycache__/_itertools.cpython-311.opt-1.pyc", + "pylib-android/importlib/resources/__pycache__/_legacy.cpython-311.opt-1.pyc", + "pylib-android/importlib/resources/__pycache__/abc.cpython-311.opt-1.pyc", + "pylib-android/importlib/resources/__pycache__/readers.cpython-311.opt-1.pyc", + "pylib-android/importlib/resources/__pycache__/simple.cpython-311.opt-1.pyc", + "pylib-android/importlib/resources/_adapters.py", + "pylib-android/importlib/resources/_common.py", + "pylib-android/importlib/resources/_itertools.py", + "pylib-android/importlib/resources/_legacy.py", + "pylib-android/importlib/resources/abc.py", + "pylib-android/importlib/resources/readers.py", + "pylib-android/importlib/resources/simple.py", + "pylib-android/importlib/simple.py", "pylib-android/importlib/util.py", "pylib-android/inspect.py", "pylib-android/io.py", "pylib-android/ipaddress.py", "pylib-android/json/__init__.py", - "pylib-android/json/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/json/__pycache__/decoder.cpython-310.opt-1.pyc", - "pylib-android/json/__pycache__/encoder.cpython-310.opt-1.pyc", - "pylib-android/json/__pycache__/scanner.cpython-310.opt-1.pyc", - "pylib-android/json/__pycache__/tool.cpython-310.opt-1.pyc", + "pylib-android/json/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/json/__pycache__/decoder.cpython-311.opt-1.pyc", + "pylib-android/json/__pycache__/encoder.cpython-311.opt-1.pyc", + "pylib-android/json/__pycache__/scanner.cpython-311.opt-1.pyc", + "pylib-android/json/__pycache__/tool.cpython-311.opt-1.pyc", "pylib-android/json/decoder.py", "pylib-android/json/encoder.py", "pylib-android/json/scanner.py", @@ -3318,9 +3349,9 @@ "pylib-android/linecache.py", "pylib-android/locale.py", "pylib-android/logging/__init__.py", - "pylib-android/logging/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/logging/__pycache__/config.cpython-310.opt-1.pyc", - "pylib-android/logging/__pycache__/handlers.cpython-310.opt-1.pyc", + "pylib-android/logging/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/logging/__pycache__/config.cpython-311.opt-1.pyc", + "pylib-android/logging/__pycache__/handlers.cpython-311.opt-1.pyc", "pylib-android/logging/config.py", "pylib-android/logging/handlers.py", "pylib-android/lzma.py", @@ -3357,7 +3388,16 @@ "pylib-android/queue.py", "pylib-android/quopri.py", "pylib-android/random.py", - "pylib-android/re.py", + "pylib-android/re/__init__.py", + "pylib-android/re/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/re/__pycache__/_casefix.cpython-311.opt-1.pyc", + "pylib-android/re/__pycache__/_compiler.cpython-311.opt-1.pyc", + "pylib-android/re/__pycache__/_constants.cpython-311.opt-1.pyc", + "pylib-android/re/__pycache__/_parser.cpython-311.opt-1.pyc", + "pylib-android/re/_casefix.py", + "pylib-android/re/_compiler.py", + "pylib-android/re/_constants.py", + "pylib-android/re/_parser.py", "pylib-android/reprlib.py", "pylib-android/rlcompleter.py", "pylib-android/runpy.py", @@ -3375,9 +3415,9 @@ "pylib-android/socket.py", "pylib-android/socketserver.py", "pylib-android/sqlite3/__init__.py", - "pylib-android/sqlite3/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/sqlite3/__pycache__/dbapi2.cpython-310.opt-1.pyc", - "pylib-android/sqlite3/__pycache__/dump.cpython-310.opt-1.pyc", + "pylib-android/sqlite3/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/sqlite3/__pycache__/dbapi2.cpython-311.opt-1.pyc", + "pylib-android/sqlite3/__pycache__/dump.cpython-311.opt-1.pyc", "pylib-android/sqlite3/dbapi2.py", "pylib-android/sqlite3/dump.py", "pylib-android/sre_compile.py", @@ -3403,6 +3443,14 @@ "pylib-android/timeit.py", "pylib-android/token.py", "pylib-android/tokenize.py", + "pylib-android/tomllib/__init__.py", + "pylib-android/tomllib/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/tomllib/__pycache__/_parser.cpython-311.opt-1.pyc", + "pylib-android/tomllib/__pycache__/_re.cpython-311.opt-1.pyc", + "pylib-android/tomllib/__pycache__/_types.cpython-311.opt-1.pyc", + "pylib-android/tomllib/_parser.py", + "pylib-android/tomllib/_re.py", + "pylib-android/tomllib/_types.py", "pylib-android/trace.py", "pylib-android/traceback.py", "pylib-android/tracemalloc.py", @@ -3410,12 +3458,12 @@ "pylib-android/types.py", "pylib-android/typing.py", "pylib-android/urllib/__init__.py", - "pylib-android/urllib/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/urllib/__pycache__/error.cpython-310.opt-1.pyc", - "pylib-android/urllib/__pycache__/parse.cpython-310.opt-1.pyc", - "pylib-android/urllib/__pycache__/request.cpython-310.opt-1.pyc", - "pylib-android/urllib/__pycache__/response.cpython-310.opt-1.pyc", - "pylib-android/urllib/__pycache__/robotparser.cpython-310.opt-1.pyc", + "pylib-android/urllib/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/urllib/__pycache__/error.cpython-311.opt-1.pyc", + "pylib-android/urllib/__pycache__/parse.cpython-311.opt-1.pyc", + "pylib-android/urllib/__pycache__/request.cpython-311.opt-1.pyc", + "pylib-android/urllib/__pycache__/response.cpython-311.opt-1.pyc", + "pylib-android/urllib/__pycache__/robotparser.cpython-311.opt-1.pyc", "pylib-android/urllib/error.py", "pylib-android/urllib/parse.py", "pylib-android/urllib/request.py", @@ -3429,17 +3477,17 @@ "pylib-android/webbrowser.py", "pylib-android/xdrlib.py", "pylib-android/xml/__init__.py", - "pylib-android/xml/__pycache__/__init__.cpython-310.opt-1.pyc", + "pylib-android/xml/__pycache__/__init__.cpython-311.opt-1.pyc", "pylib-android/xml/dom/NodeFilter.py", "pylib-android/xml/dom/__init__.py", - "pylib-android/xml/dom/__pycache__/NodeFilter.cpython-310.opt-1.pyc", - "pylib-android/xml/dom/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/xml/dom/__pycache__/domreg.cpython-310.opt-1.pyc", - "pylib-android/xml/dom/__pycache__/expatbuilder.cpython-310.opt-1.pyc", - "pylib-android/xml/dom/__pycache__/minicompat.cpython-310.opt-1.pyc", - "pylib-android/xml/dom/__pycache__/minidom.cpython-310.opt-1.pyc", - "pylib-android/xml/dom/__pycache__/pulldom.cpython-310.opt-1.pyc", - "pylib-android/xml/dom/__pycache__/xmlbuilder.cpython-310.opt-1.pyc", + "pylib-android/xml/dom/__pycache__/NodeFilter.cpython-311.opt-1.pyc", + "pylib-android/xml/dom/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/xml/dom/__pycache__/domreg.cpython-311.opt-1.pyc", + "pylib-android/xml/dom/__pycache__/expatbuilder.cpython-311.opt-1.pyc", + "pylib-android/xml/dom/__pycache__/minicompat.cpython-311.opt-1.pyc", + "pylib-android/xml/dom/__pycache__/minidom.cpython-311.opt-1.pyc", + "pylib-android/xml/dom/__pycache__/pulldom.cpython-311.opt-1.pyc", + "pylib-android/xml/dom/__pycache__/xmlbuilder.cpython-311.opt-1.pyc", "pylib-android/xml/dom/domreg.py", "pylib-android/xml/dom/expatbuilder.py", "pylib-android/xml/dom/minicompat.py", @@ -3450,219 +3498,241 @@ "pylib-android/xml/etree/ElementPath.py", "pylib-android/xml/etree/ElementTree.py", "pylib-android/xml/etree/__init__.py", - "pylib-android/xml/etree/__pycache__/ElementInclude.cpython-310.opt-1.pyc", - "pylib-android/xml/etree/__pycache__/ElementPath.cpython-310.opt-1.pyc", - "pylib-android/xml/etree/__pycache__/ElementTree.cpython-310.opt-1.pyc", - "pylib-android/xml/etree/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/xml/etree/__pycache__/cElementTree.cpython-310.opt-1.pyc", + "pylib-android/xml/etree/__pycache__/ElementInclude.cpython-311.opt-1.pyc", + "pylib-android/xml/etree/__pycache__/ElementPath.cpython-311.opt-1.pyc", + "pylib-android/xml/etree/__pycache__/ElementTree.cpython-311.opt-1.pyc", + "pylib-android/xml/etree/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/xml/etree/__pycache__/cElementTree.cpython-311.opt-1.pyc", "pylib-android/xml/etree/cElementTree.py", "pylib-android/xml/parsers/__init__.py", - "pylib-android/xml/parsers/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/xml/parsers/__pycache__/expat.cpython-310.opt-1.pyc", + "pylib-android/xml/parsers/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/xml/parsers/__pycache__/expat.cpython-311.opt-1.pyc", "pylib-android/xml/parsers/expat.py", "pylib-android/xml/sax/__init__.py", - "pylib-android/xml/sax/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/xml/sax/__pycache__/_exceptions.cpython-310.opt-1.pyc", - "pylib-android/xml/sax/__pycache__/expatreader.cpython-310.opt-1.pyc", - "pylib-android/xml/sax/__pycache__/handler.cpython-310.opt-1.pyc", - "pylib-android/xml/sax/__pycache__/saxutils.cpython-310.opt-1.pyc", - "pylib-android/xml/sax/__pycache__/xmlreader.cpython-310.opt-1.pyc", + "pylib-android/xml/sax/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/xml/sax/__pycache__/_exceptions.cpython-311.opt-1.pyc", + "pylib-android/xml/sax/__pycache__/expatreader.cpython-311.opt-1.pyc", + "pylib-android/xml/sax/__pycache__/handler.cpython-311.opt-1.pyc", + "pylib-android/xml/sax/__pycache__/saxutils.cpython-311.opt-1.pyc", + "pylib-android/xml/sax/__pycache__/xmlreader.cpython-311.opt-1.pyc", "pylib-android/xml/sax/_exceptions.py", "pylib-android/xml/sax/expatreader.py", "pylib-android/xml/sax/handler.py", "pylib-android/xml/sax/saxutils.py", "pylib-android/xml/sax/xmlreader.py", "pylib-android/xmlrpc/__init__.py", - "pylib-android/xmlrpc/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/xmlrpc/__pycache__/client.cpython-310.opt-1.pyc", - "pylib-android/xmlrpc/__pycache__/server.cpython-310.opt-1.pyc", + "pylib-android/xmlrpc/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/xmlrpc/__pycache__/client.cpython-311.opt-1.pyc", + "pylib-android/xmlrpc/__pycache__/server.cpython-311.opt-1.pyc", "pylib-android/xmlrpc/client.py", "pylib-android/xmlrpc/server.py", "pylib-android/zipapp.py", "pylib-android/zipfile.py", "pylib-android/zipimport.py", "pylib-android/zoneinfo/__init__.py", - "pylib-android/zoneinfo/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-android/zoneinfo/__pycache__/_common.cpython-310.opt-1.pyc", - "pylib-android/zoneinfo/__pycache__/_tzpath.cpython-310.opt-1.pyc", - "pylib-android/zoneinfo/__pycache__/_zoneinfo.cpython-310.opt-1.pyc", + "pylib-android/zoneinfo/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-android/zoneinfo/__pycache__/_common.cpython-311.opt-1.pyc", + "pylib-android/zoneinfo/__pycache__/_tzpath.cpython-311.opt-1.pyc", + "pylib-android/zoneinfo/__pycache__/_zoneinfo.cpython-311.opt-1.pyc", "pylib-android/zoneinfo/_common.py", "pylib-android/zoneinfo/_tzpath.py", "pylib-android/zoneinfo/_zoneinfo.py", "pylib-apple/__future__.py", - "pylib-apple/__phello__.foo.py", - "pylib-apple/__pycache__/__future__.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/__phello__.foo.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/_aix_support.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/_bootsubprocess.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/_collections_abc.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/_compat_pickle.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/_compression.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/_markupbase.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/_osx_support.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/_py_abc.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/_pydecimal.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/_pyio.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/_sitebuiltins.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/_strptime.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/_threading_local.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/_weakrefset.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/abc.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/aifc.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/antigravity.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/argparse.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/ast.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/asynchat.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/asyncore.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/base64.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/bdb.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/binhex.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/bisect.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/bz2.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/cProfile.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/calendar.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/cgi.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/cgitb.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/chunk.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/cmd.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/code.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/codecs.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/codeop.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/colorsys.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/compileall.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/configparser.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/contextlib.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/contextvars.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/copy.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/copyreg.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/crypt.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/csv.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/dataclasses.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/datetime.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/decimal.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/difflib.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/dis.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/doctest.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/enum.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/filecmp.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/fileinput.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/fnmatch.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/fractions.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/ftplib.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/functools.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/genericpath.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/getopt.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/getpass.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/gettext.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/glob.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/graphlib.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/gzip.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/hashlib.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/heapq.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/hmac.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/imghdr.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/imp.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/inspect.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/io.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/ipaddress.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/keyword.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/linecache.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/locale.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/lzma.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/mailbox.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/mailcap.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/mimetypes.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/modulefinder.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/netrc.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/nntplib.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/ntpath.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/nturl2path.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/numbers.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/opcode.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/operator.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/optparse.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/os.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/pathlib.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/pdb.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/pickle.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/pickletools.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/pipes.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/pkgutil.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/platform.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/plistlib.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/poplib.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/posixpath.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/pprint.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/profile.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/pstats.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/pty.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/py_compile.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/pyclbr.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/pydoc.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/queue.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/quopri.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/random.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/re.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/reprlib.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/rlcompleter.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/runpy.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/sched.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/secrets.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/selectors.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/shelve.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/shlex.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/shutil.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/signal.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/site.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/smtpd.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/smtplib.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/sndhdr.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/socket.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/socketserver.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/sre_compile.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/sre_constants.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/sre_parse.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/ssl.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/stat.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/statistics.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/string.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/stringprep.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/struct.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/subprocess.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/sunau.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/symtable.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/sysconfig.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/tabnanny.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/tarfile.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/telnetlib.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/tempfile.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/textwrap.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/this.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/threading.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/timeit.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/token.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/tokenize.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/trace.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/traceback.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/tracemalloc.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/tty.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/types.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/typing.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/uu.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/uuid.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/warnings.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/wave.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/weakref.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/webbrowser.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/xdrlib.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/zipapp.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/zipfile.cpython-310.opt-1.pyc", - "pylib-apple/__pycache__/zipimport.cpython-310.opt-1.pyc", + "pylib-apple/__hello__.py", + "pylib-apple/__pycache__/__future__.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/__hello__.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_aix_support.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_bootsubprocess.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_collections_abc.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_compat_pickle.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_compression.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_ios_support.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_markupbase.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_osx_support.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_py_abc.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_pydecimal.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_pyio.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sitebuiltins.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_strptime.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata__darwin_darwin.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata__ios_iphoneos.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata__ios_iphoneos_arm64.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata__ios_iphonesimulator.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata__ios_iphonesimulator_arm64.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata__ios_iphonesimulator_x86_64.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata__tvos_appletvos.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata__tvos_appletvos_arm64.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata__tvos_appletvsimulator.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata__tvos_appletvsimulator_arm64.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata__tvos_appletvsimulator_x86_64.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata_d_darwin_darwin.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata_d_ios_iphoneos.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata_d_ios_iphoneos_arm64.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata_d_ios_iphonesimulator.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata_d_ios_iphonesimulator_arm64.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata_d_ios_iphonesimulator_x86_64.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata_d_tvos_appletvos.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata_d_tvos_appletvos_arm64.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata_d_tvos_appletvsimulator.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata_d_tvos_appletvsimulator_arm64.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_sysconfigdata_d_tvos_appletvsimulator_x86_64.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_threading_local.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/_weakrefset.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/abc.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/aifc.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/antigravity.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/argparse.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/ast.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/asynchat.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/asyncore.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/base64.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/bdb.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/bisect.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/bz2.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/cProfile.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/calendar.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/cgi.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/cgitb.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/chunk.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/cmd.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/code.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/codecs.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/codeop.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/colorsys.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/compileall.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/configparser.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/contextlib.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/contextvars.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/copy.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/copyreg.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/crypt.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/csv.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/dataclasses.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/datetime.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/decimal.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/difflib.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/dis.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/doctest.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/enum.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/filecmp.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/fileinput.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/fnmatch.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/fractions.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/ftplib.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/functools.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/genericpath.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/getopt.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/getpass.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/gettext.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/glob.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/graphlib.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/gzip.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/hashlib.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/heapq.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/hmac.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/imghdr.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/imp.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/inspect.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/io.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/ipaddress.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/keyword.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/linecache.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/locale.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/lzma.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/mailbox.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/mailcap.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/mimetypes.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/modulefinder.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/netrc.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/nntplib.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/ntpath.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/nturl2path.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/numbers.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/opcode.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/operator.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/optparse.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/os.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/pathlib.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/pdb.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/pickle.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/pickletools.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/pipes.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/pkgutil.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/platform.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/plistlib.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/poplib.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/posixpath.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/pprint.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/profile.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/pstats.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/pty.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/py_compile.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/pyclbr.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/pydoc.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/queue.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/quopri.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/random.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/reprlib.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/rlcompleter.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/runpy.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/sched.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/secrets.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/selectors.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/shelve.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/shlex.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/shutil.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/signal.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/site.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/smtpd.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/smtplib.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/sndhdr.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/socket.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/socketserver.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/sre_compile.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/sre_constants.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/sre_parse.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/ssl.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/stat.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/statistics.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/string.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/stringprep.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/struct.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/subprocess.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/sunau.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/symtable.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/sysconfig.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/tabnanny.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/tarfile.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/telnetlib.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/tempfile.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/textwrap.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/this.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/threading.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/timeit.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/token.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/tokenize.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/trace.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/traceback.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/tracemalloc.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/tty.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/types.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/typing.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/uu.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/uuid.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/warnings.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/wave.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/weakref.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/webbrowser.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/xdrlib.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/zipapp.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/zipfile.cpython-311.opt-1.pyc", + "pylib-apple/__pycache__/zipimport.cpython-311.opt-1.pyc", "pylib-apple/_aix_support.py", "pylib-apple/_bootsubprocess.py", "pylib-apple/_collections_abc.py", "pylib-apple/_compat_pickle.py", "pylib-apple/_compression.py", + "pylib-apple/_ios_support.py", "pylib-apple/_markupbase.py", "pylib-apple/_osx_support.py", "pylib-apple/_py_abc.py", @@ -3670,6 +3740,28 @@ "pylib-apple/_pyio.py", "pylib-apple/_sitebuiltins.py", "pylib-apple/_strptime.py", + "pylib-apple/_sysconfigdata__darwin_darwin.py", + "pylib-apple/_sysconfigdata__ios_iphoneos.py", + "pylib-apple/_sysconfigdata__ios_iphoneos_arm64.py", + "pylib-apple/_sysconfigdata__ios_iphonesimulator.py", + "pylib-apple/_sysconfigdata__ios_iphonesimulator_arm64.py", + "pylib-apple/_sysconfigdata__ios_iphonesimulator_x86_64.py", + "pylib-apple/_sysconfigdata__tvos_appletvos.py", + "pylib-apple/_sysconfigdata__tvos_appletvos_arm64.py", + "pylib-apple/_sysconfigdata__tvos_appletvsimulator.py", + "pylib-apple/_sysconfigdata__tvos_appletvsimulator_arm64.py", + "pylib-apple/_sysconfigdata__tvos_appletvsimulator_x86_64.py", + "pylib-apple/_sysconfigdata_d_darwin_darwin.py", + "pylib-apple/_sysconfigdata_d_ios_iphoneos.py", + "pylib-apple/_sysconfigdata_d_ios_iphoneos_arm64.py", + "pylib-apple/_sysconfigdata_d_ios_iphonesimulator.py", + "pylib-apple/_sysconfigdata_d_ios_iphonesimulator_arm64.py", + "pylib-apple/_sysconfigdata_d_ios_iphonesimulator_x86_64.py", + "pylib-apple/_sysconfigdata_d_tvos_appletvos.py", + "pylib-apple/_sysconfigdata_d_tvos_appletvos_arm64.py", + "pylib-apple/_sysconfigdata_d_tvos_appletvsimulator.py", + "pylib-apple/_sysconfigdata_d_tvos_appletvsimulator_arm64.py", + "pylib-apple/_sysconfigdata_d_tvos_appletvsimulator_x86_64.py", "pylib-apple/_threading_local.py", "pylib-apple/_weakrefset.py", "pylib-apple/abc.py", @@ -3680,37 +3772,39 @@ "pylib-apple/asynchat.py", "pylib-apple/asyncio/__init__.py", "pylib-apple/asyncio/__main__.py", - "pylib-apple/asyncio/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/__main__.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/base_events.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/base_futures.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/base_subprocess.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/base_tasks.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/constants.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/coroutines.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/events.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/exceptions.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/format_helpers.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/futures.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/locks.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/log.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/mixins.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/proactor_events.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/protocols.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/queues.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/runners.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/selector_events.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/sslproto.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/staggered.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/streams.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/subprocess.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/tasks.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/threads.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/transports.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/trsock.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/unix_events.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/windows_events.cpython-310.opt-1.pyc", - "pylib-apple/asyncio/__pycache__/windows_utils.cpython-310.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/__main__.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/base_events.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/base_futures.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/base_subprocess.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/base_tasks.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/constants.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/coroutines.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/events.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/exceptions.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/format_helpers.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/futures.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/locks.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/log.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/mixins.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/proactor_events.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/protocols.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/queues.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/runners.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/selector_events.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/sslproto.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/staggered.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/streams.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/subprocess.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/taskgroups.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/tasks.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/threads.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/timeouts.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/transports.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/trsock.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/unix_events.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/windows_events.cpython-311.opt-1.pyc", + "pylib-apple/asyncio/__pycache__/windows_utils.cpython-311.opt-1.pyc", "pylib-apple/asyncio/base_events.py", "pylib-apple/asyncio/base_futures.py", "pylib-apple/asyncio/base_subprocess.py", @@ -3733,8 +3827,10 @@ "pylib-apple/asyncio/staggered.py", "pylib-apple/asyncio/streams.py", "pylib-apple/asyncio/subprocess.py", + "pylib-apple/asyncio/taskgroups.py", "pylib-apple/asyncio/tasks.py", "pylib-apple/asyncio/threads.py", + "pylib-apple/asyncio/timeouts.py", "pylib-apple/asyncio/transports.py", "pylib-apple/asyncio/trsock.py", "pylib-apple/asyncio/unix_events.py", @@ -3743,7 +3839,6 @@ "pylib-apple/asyncore.py", "pylib-apple/base64.py", "pylib-apple/bdb.py", - "pylib-apple/binhex.py", "pylib-apple/bisect.py", "pylib-apple/bz2.py", "pylib-apple/cProfile.py", @@ -3756,18 +3851,18 @@ "pylib-apple/codecs.py", "pylib-apple/codeop.py", "pylib-apple/collections/__init__.py", - "pylib-apple/collections/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/collections/__pycache__/abc.cpython-310.opt-1.pyc", + "pylib-apple/collections/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/collections/__pycache__/abc.cpython-311.opt-1.pyc", "pylib-apple/collections/abc.py", "pylib-apple/colorsys.py", "pylib-apple/compileall.py", "pylib-apple/concurrent/__init__.py", - "pylib-apple/concurrent/__pycache__/__init__.cpython-310.opt-1.pyc", + "pylib-apple/concurrent/__pycache__/__init__.cpython-311.opt-1.pyc", "pylib-apple/concurrent/futures/__init__.py", - "pylib-apple/concurrent/futures/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/concurrent/futures/__pycache__/_base.cpython-310.opt-1.pyc", - "pylib-apple/concurrent/futures/__pycache__/process.cpython-310.opt-1.pyc", - "pylib-apple/concurrent/futures/__pycache__/thread.cpython-310.opt-1.pyc", + "pylib-apple/concurrent/futures/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/concurrent/futures/__pycache__/_base.cpython-311.opt-1.pyc", + "pylib-apple/concurrent/futures/__pycache__/process.cpython-311.opt-1.pyc", + "pylib-apple/concurrent/futures/__pycache__/thread.cpython-311.opt-1.pyc", "pylib-apple/concurrent/futures/_base.py", "pylib-apple/concurrent/futures/process.py", "pylib-apple/concurrent/futures/thread.py", @@ -3779,29 +3874,29 @@ "pylib-apple/crypt.py", "pylib-apple/csv.py", "pylib-apple/ctypes/__init__.py", - "pylib-apple/ctypes/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/ctypes/__pycache__/_aix.cpython-310.opt-1.pyc", - "pylib-apple/ctypes/__pycache__/_endian.cpython-310.opt-1.pyc", - "pylib-apple/ctypes/__pycache__/util.cpython-310.opt-1.pyc", - "pylib-apple/ctypes/__pycache__/wintypes.cpython-310.opt-1.pyc", + "pylib-apple/ctypes/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/ctypes/__pycache__/_aix.cpython-311.opt-1.pyc", + "pylib-apple/ctypes/__pycache__/_endian.cpython-311.opt-1.pyc", + "pylib-apple/ctypes/__pycache__/util.cpython-311.opt-1.pyc", + "pylib-apple/ctypes/__pycache__/wintypes.cpython-311.opt-1.pyc", "pylib-apple/ctypes/_aix.py", "pylib-apple/ctypes/_endian.py", "pylib-apple/ctypes/macholib/__init__.py", - "pylib-apple/ctypes/macholib/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/ctypes/macholib/__pycache__/dyld.cpython-310.opt-1.pyc", - "pylib-apple/ctypes/macholib/__pycache__/dylib.cpython-310.opt-1.pyc", - "pylib-apple/ctypes/macholib/__pycache__/framework.cpython-310.opt-1.pyc", + "pylib-apple/ctypes/macholib/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/ctypes/macholib/__pycache__/dyld.cpython-311.opt-1.pyc", + "pylib-apple/ctypes/macholib/__pycache__/dylib.cpython-311.opt-1.pyc", + "pylib-apple/ctypes/macholib/__pycache__/framework.cpython-311.opt-1.pyc", "pylib-apple/ctypes/macholib/dyld.py", "pylib-apple/ctypes/macholib/dylib.py", "pylib-apple/ctypes/macholib/framework.py", "pylib-apple/ctypes/util.py", "pylib-apple/ctypes/wintypes.py", "pylib-apple/curses/__init__.py", - "pylib-apple/curses/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/curses/__pycache__/ascii.cpython-310.opt-1.pyc", - "pylib-apple/curses/__pycache__/has_key.cpython-310.opt-1.pyc", - "pylib-apple/curses/__pycache__/panel.cpython-310.opt-1.pyc", - "pylib-apple/curses/__pycache__/textpad.cpython-310.opt-1.pyc", + "pylib-apple/curses/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/curses/__pycache__/ascii.cpython-311.opt-1.pyc", + "pylib-apple/curses/__pycache__/has_key.cpython-311.opt-1.pyc", + "pylib-apple/curses/__pycache__/panel.cpython-311.opt-1.pyc", + "pylib-apple/curses/__pycache__/textpad.cpython-311.opt-1.pyc", "pylib-apple/curses/ascii.py", "pylib-apple/curses/has_key.py", "pylib-apple/curses/panel.py", @@ -3813,26 +3908,26 @@ "pylib-apple/dis.py", "pylib-apple/doctest.py", "pylib-apple/email/__init__.py", - "pylib-apple/email/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/_encoded_words.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/_header_value_parser.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/_parseaddr.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/_policybase.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/base64mime.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/charset.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/contentmanager.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/encoders.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/errors.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/feedparser.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/generator.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/header.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/headerregistry.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/iterators.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/message.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/parser.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/policy.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/quoprimime.cpython-310.opt-1.pyc", - "pylib-apple/email/__pycache__/utils.cpython-310.opt-1.pyc", + "pylib-apple/email/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/_encoded_words.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/_header_value_parser.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/_parseaddr.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/_policybase.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/base64mime.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/charset.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/contentmanager.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/encoders.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/errors.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/feedparser.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/generator.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/header.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/headerregistry.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/iterators.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/message.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/parser.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/policy.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/quoprimime.cpython-311.opt-1.pyc", + "pylib-apple/email/__pycache__/utils.cpython-311.opt-1.pyc", "pylib-apple/email/_encoded_words.py", "pylib-apple/email/_header_value_parser.py", "pylib-apple/email/_parseaddr.py", @@ -3849,15 +3944,15 @@ "pylib-apple/email/iterators.py", "pylib-apple/email/message.py", "pylib-apple/email/mime/__init__.py", - "pylib-apple/email/mime/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/email/mime/__pycache__/application.cpython-310.opt-1.pyc", - "pylib-apple/email/mime/__pycache__/audio.cpython-310.opt-1.pyc", - "pylib-apple/email/mime/__pycache__/base.cpython-310.opt-1.pyc", - "pylib-apple/email/mime/__pycache__/image.cpython-310.opt-1.pyc", - "pylib-apple/email/mime/__pycache__/message.cpython-310.opt-1.pyc", - "pylib-apple/email/mime/__pycache__/multipart.cpython-310.opt-1.pyc", - "pylib-apple/email/mime/__pycache__/nonmultipart.cpython-310.opt-1.pyc", - "pylib-apple/email/mime/__pycache__/text.cpython-310.opt-1.pyc", + "pylib-apple/email/mime/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/email/mime/__pycache__/application.cpython-311.opt-1.pyc", + "pylib-apple/email/mime/__pycache__/audio.cpython-311.opt-1.pyc", + "pylib-apple/email/mime/__pycache__/base.cpython-311.opt-1.pyc", + "pylib-apple/email/mime/__pycache__/image.cpython-311.opt-1.pyc", + "pylib-apple/email/mime/__pycache__/message.cpython-311.opt-1.pyc", + "pylib-apple/email/mime/__pycache__/multipart.cpython-311.opt-1.pyc", + "pylib-apple/email/mime/__pycache__/nonmultipart.cpython-311.opt-1.pyc", + "pylib-apple/email/mime/__pycache__/text.cpython-311.opt-1.pyc", "pylib-apple/email/mime/application.py", "pylib-apple/email/mime/audio.py", "pylib-apple/email/mime/base.py", @@ -3871,128 +3966,128 @@ "pylib-apple/email/quoprimime.py", "pylib-apple/email/utils.py", "pylib-apple/encodings/__init__.py", - "pylib-apple/encodings/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/aliases.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/ascii.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/base64_codec.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/big5.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/big5hkscs.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/bz2_codec.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/charmap.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp037.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp1006.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp1026.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp1125.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp1140.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp1250.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp1251.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp1252.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp1253.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp1254.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp1255.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp1256.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp1257.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp1258.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp273.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp424.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp437.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp500.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp720.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp737.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp775.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp850.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp852.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp855.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp856.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp857.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp858.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp860.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp861.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp862.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp863.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp864.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp865.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp866.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp869.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp874.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp875.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp932.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp949.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/cp950.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/euc_jis_2004.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/euc_jisx0213.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/euc_jp.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/euc_kr.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/gb18030.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/gb2312.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/gbk.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/hex_codec.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/hp_roman8.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/hz.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/idna.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso2022_jp.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso2022_jp_1.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso2022_jp_2.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso2022_jp_2004.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso2022_jp_3.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso2022_jp_ext.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso2022_kr.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso8859_1.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso8859_10.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso8859_11.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso8859_13.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso8859_14.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso8859_15.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso8859_16.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso8859_2.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso8859_3.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso8859_4.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso8859_5.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso8859_6.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso8859_7.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso8859_8.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/iso8859_9.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/johab.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/koi8_r.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/koi8_t.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/koi8_u.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/kz1048.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/latin_1.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/mac_arabic.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/mac_croatian.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/mac_cyrillic.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/mac_farsi.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/mac_greek.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/mac_iceland.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/mac_latin2.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/mac_roman.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/mac_romanian.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/mac_turkish.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/mbcs.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/oem.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/palmos.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/ptcp154.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/punycode.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/quopri_codec.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/raw_unicode_escape.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/rot_13.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/shift_jis.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/shift_jis_2004.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/shift_jisx0213.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/tis_620.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/undefined.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/unicode_escape.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/utf_16.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/utf_16_be.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/utf_16_le.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/utf_32.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/utf_32_be.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/utf_32_le.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/utf_7.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/utf_8.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/utf_8_sig.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/uu_codec.cpython-310.opt-1.pyc", - "pylib-apple/encodings/__pycache__/zlib_codec.cpython-310.opt-1.pyc", + "pylib-apple/encodings/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/aliases.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/ascii.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/base64_codec.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/big5.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/big5hkscs.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/bz2_codec.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/charmap.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp037.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp1006.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp1026.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp1125.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp1140.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp1250.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp1251.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp1252.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp1253.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp1254.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp1255.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp1256.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp1257.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp1258.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp273.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp424.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp437.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp500.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp720.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp737.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp775.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp850.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp852.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp855.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp856.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp857.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp858.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp860.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp861.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp862.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp863.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp864.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp865.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp866.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp869.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp874.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp875.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp932.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp949.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/cp950.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/euc_jis_2004.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/euc_jisx0213.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/euc_jp.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/euc_kr.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/gb18030.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/gb2312.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/gbk.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/hex_codec.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/hp_roman8.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/hz.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/idna.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso2022_jp.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso2022_jp_1.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso2022_jp_2.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso2022_jp_2004.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso2022_jp_3.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso2022_jp_ext.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso2022_kr.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso8859_1.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso8859_10.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso8859_11.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso8859_13.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso8859_14.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso8859_15.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso8859_16.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso8859_2.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso8859_3.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso8859_4.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso8859_5.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso8859_6.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso8859_7.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso8859_8.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/iso8859_9.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/johab.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/koi8_r.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/koi8_t.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/koi8_u.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/kz1048.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/latin_1.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/mac_arabic.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/mac_croatian.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/mac_cyrillic.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/mac_farsi.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/mac_greek.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/mac_iceland.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/mac_latin2.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/mac_roman.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/mac_romanian.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/mac_turkish.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/mbcs.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/oem.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/palmos.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/ptcp154.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/punycode.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/quopri_codec.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/raw_unicode_escape.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/rot_13.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/shift_jis.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/shift_jis_2004.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/shift_jisx0213.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/tis_620.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/undefined.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/unicode_escape.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/utf_16.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/utf_16_be.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/utf_16_le.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/utf_32.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/utf_32_be.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/utf_32_le.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/utf_7.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/utf_8.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/utf_8_sig.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/uu_codec.cpython-311.opt-1.pyc", + "pylib-apple/encodings/__pycache__/zlib_codec.cpython-311.opt-1.pyc", "pylib-apple/encodings/aliases.py", "pylib-apple/encodings/ascii.py", "pylib-apple/encodings/base64_codec.py", @@ -4132,17 +4227,17 @@ "pylib-apple/heapq.py", "pylib-apple/hmac.py", "pylib-apple/html/__init__.py", - "pylib-apple/html/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/html/__pycache__/entities.cpython-310.opt-1.pyc", - "pylib-apple/html/__pycache__/parser.cpython-310.opt-1.pyc", + "pylib-apple/html/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/html/__pycache__/entities.cpython-311.opt-1.pyc", + "pylib-apple/html/__pycache__/parser.cpython-311.opt-1.pyc", "pylib-apple/html/entities.py", "pylib-apple/html/parser.py", "pylib-apple/http/__init__.py", - "pylib-apple/http/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/http/__pycache__/client.cpython-310.opt-1.pyc", - "pylib-apple/http/__pycache__/cookiejar.cpython-310.opt-1.pyc", - "pylib-apple/http/__pycache__/cookies.cpython-310.opt-1.pyc", - "pylib-apple/http/__pycache__/server.cpython-310.opt-1.pyc", + "pylib-apple/http/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/http/__pycache__/client.cpython-311.opt-1.pyc", + "pylib-apple/http/__pycache__/cookiejar.cpython-311.opt-1.pyc", + "pylib-apple/http/__pycache__/cookies.cpython-311.opt-1.pyc", + "pylib-apple/http/__pycache__/server.cpython-311.opt-1.pyc", "pylib-apple/http/client.py", "pylib-apple/http/cookiejar.py", "pylib-apple/http/cookies.py", @@ -4150,32 +4245,28 @@ "pylib-apple/imghdr.py", "pylib-apple/imp.py", "pylib-apple/importlib/__init__.py", - "pylib-apple/importlib/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/importlib/__pycache__/_abc.cpython-310.opt-1.pyc", - "pylib-apple/importlib/__pycache__/_adapters.cpython-310.opt-1.pyc", - "pylib-apple/importlib/__pycache__/_bootstrap.cpython-310.opt-1.pyc", - "pylib-apple/importlib/__pycache__/_bootstrap_external.cpython-310.opt-1.pyc", - "pylib-apple/importlib/__pycache__/_common.cpython-310.opt-1.pyc", - "pylib-apple/importlib/__pycache__/abc.cpython-310.opt-1.pyc", - "pylib-apple/importlib/__pycache__/machinery.cpython-310.opt-1.pyc", - "pylib-apple/importlib/__pycache__/readers.cpython-310.opt-1.pyc", - "pylib-apple/importlib/__pycache__/resources.cpython-310.opt-1.pyc", - "pylib-apple/importlib/__pycache__/util.cpython-310.opt-1.pyc", + "pylib-apple/importlib/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/importlib/__pycache__/_abc.cpython-311.opt-1.pyc", + "pylib-apple/importlib/__pycache__/_bootstrap.cpython-311.opt-1.pyc", + "pylib-apple/importlib/__pycache__/_bootstrap_external.cpython-311.opt-1.pyc", + "pylib-apple/importlib/__pycache__/abc.cpython-311.opt-1.pyc", + "pylib-apple/importlib/__pycache__/machinery.cpython-311.opt-1.pyc", + "pylib-apple/importlib/__pycache__/readers.cpython-311.opt-1.pyc", + "pylib-apple/importlib/__pycache__/simple.cpython-311.opt-1.pyc", + "pylib-apple/importlib/__pycache__/util.cpython-311.opt-1.pyc", "pylib-apple/importlib/_abc.py", - "pylib-apple/importlib/_adapters.py", "pylib-apple/importlib/_bootstrap.py", "pylib-apple/importlib/_bootstrap_external.py", - "pylib-apple/importlib/_common.py", "pylib-apple/importlib/abc.py", "pylib-apple/importlib/machinery.py", "pylib-apple/importlib/metadata/__init__.py", - "pylib-apple/importlib/metadata/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/importlib/metadata/__pycache__/_adapters.cpython-310.opt-1.pyc", - "pylib-apple/importlib/metadata/__pycache__/_collections.cpython-310.opt-1.pyc", - "pylib-apple/importlib/metadata/__pycache__/_functools.cpython-310.opt-1.pyc", - "pylib-apple/importlib/metadata/__pycache__/_itertools.cpython-310.opt-1.pyc", - "pylib-apple/importlib/metadata/__pycache__/_meta.cpython-310.opt-1.pyc", - "pylib-apple/importlib/metadata/__pycache__/_text.cpython-310.opt-1.pyc", + "pylib-apple/importlib/metadata/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/importlib/metadata/__pycache__/_adapters.cpython-311.opt-1.pyc", + "pylib-apple/importlib/metadata/__pycache__/_collections.cpython-311.opt-1.pyc", + "pylib-apple/importlib/metadata/__pycache__/_functools.cpython-311.opt-1.pyc", + "pylib-apple/importlib/metadata/__pycache__/_itertools.cpython-311.opt-1.pyc", + "pylib-apple/importlib/metadata/__pycache__/_meta.cpython-311.opt-1.pyc", + "pylib-apple/importlib/metadata/__pycache__/_text.cpython-311.opt-1.pyc", "pylib-apple/importlib/metadata/_adapters.py", "pylib-apple/importlib/metadata/_collections.py", "pylib-apple/importlib/metadata/_functools.py", @@ -4183,17 +4274,33 @@ "pylib-apple/importlib/metadata/_meta.py", "pylib-apple/importlib/metadata/_text.py", "pylib-apple/importlib/readers.py", - "pylib-apple/importlib/resources.py", + "pylib-apple/importlib/resources/__init__.py", + "pylib-apple/importlib/resources/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/importlib/resources/__pycache__/_adapters.cpython-311.opt-1.pyc", + "pylib-apple/importlib/resources/__pycache__/_common.cpython-311.opt-1.pyc", + "pylib-apple/importlib/resources/__pycache__/_itertools.cpython-311.opt-1.pyc", + "pylib-apple/importlib/resources/__pycache__/_legacy.cpython-311.opt-1.pyc", + "pylib-apple/importlib/resources/__pycache__/abc.cpython-311.opt-1.pyc", + "pylib-apple/importlib/resources/__pycache__/readers.cpython-311.opt-1.pyc", + "pylib-apple/importlib/resources/__pycache__/simple.cpython-311.opt-1.pyc", + "pylib-apple/importlib/resources/_adapters.py", + "pylib-apple/importlib/resources/_common.py", + "pylib-apple/importlib/resources/_itertools.py", + "pylib-apple/importlib/resources/_legacy.py", + "pylib-apple/importlib/resources/abc.py", + "pylib-apple/importlib/resources/readers.py", + "pylib-apple/importlib/resources/simple.py", + "pylib-apple/importlib/simple.py", "pylib-apple/importlib/util.py", "pylib-apple/inspect.py", "pylib-apple/io.py", "pylib-apple/ipaddress.py", "pylib-apple/json/__init__.py", - "pylib-apple/json/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/json/__pycache__/decoder.cpython-310.opt-1.pyc", - "pylib-apple/json/__pycache__/encoder.cpython-310.opt-1.pyc", - "pylib-apple/json/__pycache__/scanner.cpython-310.opt-1.pyc", - "pylib-apple/json/__pycache__/tool.cpython-310.opt-1.pyc", + "pylib-apple/json/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/json/__pycache__/decoder.cpython-311.opt-1.pyc", + "pylib-apple/json/__pycache__/encoder.cpython-311.opt-1.pyc", + "pylib-apple/json/__pycache__/scanner.cpython-311.opt-1.pyc", + "pylib-apple/json/__pycache__/tool.cpython-311.opt-1.pyc", "pylib-apple/json/decoder.py", "pylib-apple/json/encoder.py", "pylib-apple/json/scanner.py", @@ -4202,9 +4309,9 @@ "pylib-apple/linecache.py", "pylib-apple/locale.py", "pylib-apple/logging/__init__.py", - "pylib-apple/logging/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/logging/__pycache__/config.cpython-310.opt-1.pyc", - "pylib-apple/logging/__pycache__/handlers.cpython-310.opt-1.pyc", + "pylib-apple/logging/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/logging/__pycache__/config.cpython-311.opt-1.pyc", + "pylib-apple/logging/__pycache__/handlers.cpython-311.opt-1.pyc", "pylib-apple/logging/config.py", "pylib-apple/logging/handlers.py", "pylib-apple/lzma.py", @@ -4212,14 +4319,6 @@ "pylib-apple/mailcap.py", "pylib-apple/mimetypes.py", "pylib-apple/modulefinder.py", - "pylib-apple/msilib/__init__.py", - "pylib-apple/msilib/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/msilib/__pycache__/schema.cpython-310.opt-1.pyc", - "pylib-apple/msilib/__pycache__/sequence.cpython-310.opt-1.pyc", - "pylib-apple/msilib/__pycache__/text.cpython-310.opt-1.pyc", - "pylib-apple/msilib/schema.py", - "pylib-apple/msilib/sequence.py", - "pylib-apple/msilib/text.py", "pylib-apple/netrc.py", "pylib-apple/nntplib.py", "pylib-apple/ntpath.py", @@ -4249,7 +4348,16 @@ "pylib-apple/queue.py", "pylib-apple/quopri.py", "pylib-apple/random.py", - "pylib-apple/re.py", + "pylib-apple/re/__init__.py", + "pylib-apple/re/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/re/__pycache__/_casefix.cpython-311.opt-1.pyc", + "pylib-apple/re/__pycache__/_compiler.cpython-311.opt-1.pyc", + "pylib-apple/re/__pycache__/_constants.cpython-311.opt-1.pyc", + "pylib-apple/re/__pycache__/_parser.cpython-311.opt-1.pyc", + "pylib-apple/re/_casefix.py", + "pylib-apple/re/_compiler.py", + "pylib-apple/re/_constants.py", + "pylib-apple/re/_parser.py", "pylib-apple/reprlib.py", "pylib-apple/rlcompleter.py", "pylib-apple/runpy.py", @@ -4267,9 +4375,9 @@ "pylib-apple/socket.py", "pylib-apple/socketserver.py", "pylib-apple/sqlite3/__init__.py", - "pylib-apple/sqlite3/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/sqlite3/__pycache__/dbapi2.cpython-310.opt-1.pyc", - "pylib-apple/sqlite3/__pycache__/dump.cpython-310.opt-1.pyc", + "pylib-apple/sqlite3/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/sqlite3/__pycache__/dbapi2.cpython-311.opt-1.pyc", + "pylib-apple/sqlite3/__pycache__/dump.cpython-311.opt-1.pyc", "pylib-apple/sqlite3/dbapi2.py", "pylib-apple/sqlite3/dump.py", "pylib-apple/sre_compile.py", @@ -4295,6 +4403,14 @@ "pylib-apple/timeit.py", "pylib-apple/token.py", "pylib-apple/tokenize.py", + "pylib-apple/tomllib/__init__.py", + "pylib-apple/tomllib/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/tomllib/__pycache__/_parser.cpython-311.opt-1.pyc", + "pylib-apple/tomllib/__pycache__/_re.cpython-311.opt-1.pyc", + "pylib-apple/tomllib/__pycache__/_types.cpython-311.opt-1.pyc", + "pylib-apple/tomllib/_parser.py", + "pylib-apple/tomllib/_re.py", + "pylib-apple/tomllib/_types.py", "pylib-apple/trace.py", "pylib-apple/traceback.py", "pylib-apple/tracemalloc.py", @@ -4302,12 +4418,12 @@ "pylib-apple/types.py", "pylib-apple/typing.py", "pylib-apple/urllib/__init__.py", - "pylib-apple/urllib/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/urllib/__pycache__/error.cpython-310.opt-1.pyc", - "pylib-apple/urllib/__pycache__/parse.cpython-310.opt-1.pyc", - "pylib-apple/urllib/__pycache__/request.cpython-310.opt-1.pyc", - "pylib-apple/urllib/__pycache__/response.cpython-310.opt-1.pyc", - "pylib-apple/urllib/__pycache__/robotparser.cpython-310.opt-1.pyc", + "pylib-apple/urllib/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/urllib/__pycache__/error.cpython-311.opt-1.pyc", + "pylib-apple/urllib/__pycache__/parse.cpython-311.opt-1.pyc", + "pylib-apple/urllib/__pycache__/request.cpython-311.opt-1.pyc", + "pylib-apple/urllib/__pycache__/response.cpython-311.opt-1.pyc", + "pylib-apple/urllib/__pycache__/robotparser.cpython-311.opt-1.pyc", "pylib-apple/urllib/error.py", "pylib-apple/urllib/parse.py", "pylib-apple/urllib/request.py", @@ -4321,17 +4437,17 @@ "pylib-apple/webbrowser.py", "pylib-apple/xdrlib.py", "pylib-apple/xml/__init__.py", - "pylib-apple/xml/__pycache__/__init__.cpython-310.opt-1.pyc", + "pylib-apple/xml/__pycache__/__init__.cpython-311.opt-1.pyc", "pylib-apple/xml/dom/NodeFilter.py", "pylib-apple/xml/dom/__init__.py", - "pylib-apple/xml/dom/__pycache__/NodeFilter.cpython-310.opt-1.pyc", - "pylib-apple/xml/dom/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/xml/dom/__pycache__/domreg.cpython-310.opt-1.pyc", - "pylib-apple/xml/dom/__pycache__/expatbuilder.cpython-310.opt-1.pyc", - "pylib-apple/xml/dom/__pycache__/minicompat.cpython-310.opt-1.pyc", - "pylib-apple/xml/dom/__pycache__/minidom.cpython-310.opt-1.pyc", - "pylib-apple/xml/dom/__pycache__/pulldom.cpython-310.opt-1.pyc", - "pylib-apple/xml/dom/__pycache__/xmlbuilder.cpython-310.opt-1.pyc", + "pylib-apple/xml/dom/__pycache__/NodeFilter.cpython-311.opt-1.pyc", + "pylib-apple/xml/dom/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/xml/dom/__pycache__/domreg.cpython-311.opt-1.pyc", + "pylib-apple/xml/dom/__pycache__/expatbuilder.cpython-311.opt-1.pyc", + "pylib-apple/xml/dom/__pycache__/minicompat.cpython-311.opt-1.pyc", + "pylib-apple/xml/dom/__pycache__/minidom.cpython-311.opt-1.pyc", + "pylib-apple/xml/dom/__pycache__/pulldom.cpython-311.opt-1.pyc", + "pylib-apple/xml/dom/__pycache__/xmlbuilder.cpython-311.opt-1.pyc", "pylib-apple/xml/dom/domreg.py", "pylib-apple/xml/dom/expatbuilder.py", "pylib-apple/xml/dom/minicompat.py", @@ -4342,42 +4458,42 @@ "pylib-apple/xml/etree/ElementPath.py", "pylib-apple/xml/etree/ElementTree.py", "pylib-apple/xml/etree/__init__.py", - "pylib-apple/xml/etree/__pycache__/ElementInclude.cpython-310.opt-1.pyc", - "pylib-apple/xml/etree/__pycache__/ElementPath.cpython-310.opt-1.pyc", - "pylib-apple/xml/etree/__pycache__/ElementTree.cpython-310.opt-1.pyc", - "pylib-apple/xml/etree/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/xml/etree/__pycache__/cElementTree.cpython-310.opt-1.pyc", + "pylib-apple/xml/etree/__pycache__/ElementInclude.cpython-311.opt-1.pyc", + "pylib-apple/xml/etree/__pycache__/ElementPath.cpython-311.opt-1.pyc", + "pylib-apple/xml/etree/__pycache__/ElementTree.cpython-311.opt-1.pyc", + "pylib-apple/xml/etree/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/xml/etree/__pycache__/cElementTree.cpython-311.opt-1.pyc", "pylib-apple/xml/etree/cElementTree.py", "pylib-apple/xml/parsers/__init__.py", - "pylib-apple/xml/parsers/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/xml/parsers/__pycache__/expat.cpython-310.opt-1.pyc", + "pylib-apple/xml/parsers/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/xml/parsers/__pycache__/expat.cpython-311.opt-1.pyc", "pylib-apple/xml/parsers/expat.py", "pylib-apple/xml/sax/__init__.py", - "pylib-apple/xml/sax/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/xml/sax/__pycache__/_exceptions.cpython-310.opt-1.pyc", - "pylib-apple/xml/sax/__pycache__/expatreader.cpython-310.opt-1.pyc", - "pylib-apple/xml/sax/__pycache__/handler.cpython-310.opt-1.pyc", - "pylib-apple/xml/sax/__pycache__/saxutils.cpython-310.opt-1.pyc", - "pylib-apple/xml/sax/__pycache__/xmlreader.cpython-310.opt-1.pyc", + "pylib-apple/xml/sax/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/xml/sax/__pycache__/_exceptions.cpython-311.opt-1.pyc", + "pylib-apple/xml/sax/__pycache__/expatreader.cpython-311.opt-1.pyc", + "pylib-apple/xml/sax/__pycache__/handler.cpython-311.opt-1.pyc", + "pylib-apple/xml/sax/__pycache__/saxutils.cpython-311.opt-1.pyc", + "pylib-apple/xml/sax/__pycache__/xmlreader.cpython-311.opt-1.pyc", "pylib-apple/xml/sax/_exceptions.py", "pylib-apple/xml/sax/expatreader.py", "pylib-apple/xml/sax/handler.py", "pylib-apple/xml/sax/saxutils.py", "pylib-apple/xml/sax/xmlreader.py", "pylib-apple/xmlrpc/__init__.py", - "pylib-apple/xmlrpc/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/xmlrpc/__pycache__/client.cpython-310.opt-1.pyc", - "pylib-apple/xmlrpc/__pycache__/server.cpython-310.opt-1.pyc", + "pylib-apple/xmlrpc/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/xmlrpc/__pycache__/client.cpython-311.opt-1.pyc", + "pylib-apple/xmlrpc/__pycache__/server.cpython-311.opt-1.pyc", "pylib-apple/xmlrpc/client.py", "pylib-apple/xmlrpc/server.py", "pylib-apple/zipapp.py", "pylib-apple/zipfile.py", "pylib-apple/zipimport.py", "pylib-apple/zoneinfo/__init__.py", - "pylib-apple/zoneinfo/__pycache__/__init__.cpython-310.opt-1.pyc", - "pylib-apple/zoneinfo/__pycache__/_common.cpython-310.opt-1.pyc", - "pylib-apple/zoneinfo/__pycache__/_tzpath.cpython-310.opt-1.pyc", - "pylib-apple/zoneinfo/__pycache__/_zoneinfo.cpython-310.opt-1.pyc", + "pylib-apple/zoneinfo/__pycache__/__init__.cpython-311.opt-1.pyc", + "pylib-apple/zoneinfo/__pycache__/_common.cpython-311.opt-1.pyc", + "pylib-apple/zoneinfo/__pycache__/_tzpath.cpython-311.opt-1.pyc", + "pylib-apple/zoneinfo/__pycache__/_zoneinfo.cpython-311.opt-1.pyc", "pylib-apple/zoneinfo/_common.py", "pylib-apple/zoneinfo/_tzpath.py", "pylib-apple/zoneinfo/_zoneinfo.py", @@ -4431,7 +4547,7 @@ "windows/Win32/DLLs/_zoneinfo.pyd", "windows/Win32/DLLs/_zoneinfo_d.pyd", "windows/Win32/DLLs/libcrypto-1_1.dll", - "windows/Win32/DLLs/libffi-7.dll", + "windows/Win32/DLLs/libffi-8.dll", "windows/Win32/DLLs/libssl-1_1.dll", "windows/Win32/DLLs/pyexpat.pyd", "windows/Win32/DLLs/pyexpat_d.pyd", @@ -4448,174 +4564,172 @@ "windows/Win32/DLLs/winsound.pyd", "windows/Win32/DLLs/winsound_d.pyd", "windows/Win32/Lib/__future__.py", - "windows/Win32/Lib/__phello__.foo.py", - "windows/Win32/Lib/__pycache__/__future__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/__phello__.foo.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/_aix_support.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/_bootsubprocess.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/_collections_abc.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/_compat_pickle.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/_compression.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/_markupbase.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/_osx_support.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/_py_abc.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/_pydecimal.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/_pyio.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/_sitebuiltins.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/_strptime.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/_threading_local.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/_weakrefset.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/abc.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/aifc.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/antigravity.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/argparse.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/ast.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/asynchat.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/asyncore.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/base64.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/bdb.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/binhex.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/bisect.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/bz2.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/cProfile.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/calendar.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/cgi.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/cgitb.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/chunk.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/cmd.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/code.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/codecs.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/codeop.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/colorsys.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/compileall.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/configparser.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/contextlib.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/contextvars.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/copy.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/copyreg.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/crypt.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/csv.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/dataclasses.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/datetime.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/decimal.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/difflib.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/dis.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/doctest.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/enum.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/filecmp.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/fileinput.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/fnmatch.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/fractions.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/ftplib.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/functools.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/genericpath.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/getopt.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/getpass.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/gettext.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/glob.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/graphlib.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/gzip.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/hashlib.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/heapq.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/hmac.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/imghdr.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/imp.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/inspect.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/io.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/ipaddress.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/keyword.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/linecache.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/locale.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/lzma.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/mailbox.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/mailcap.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/mimetypes.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/modulefinder.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/netrc.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/nntplib.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/ntpath.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/nturl2path.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/numbers.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/opcode.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/operator.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/optparse.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/os.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/pathlib.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/pdb.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/pickle.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/pickletools.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/pipes.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/pkgutil.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/platform.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/plistlib.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/poplib.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/posixpath.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/pprint.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/profile.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/pstats.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/pty.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/py_compile.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/pyclbr.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/pydoc.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/queue.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/quopri.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/random.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/re.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/reprlib.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/rlcompleter.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/runpy.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/sched.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/secrets.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/selectors.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/shelve.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/shlex.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/shutil.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/signal.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/site.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/smtpd.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/smtplib.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/sndhdr.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/socket.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/socketserver.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/sre_compile.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/sre_constants.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/sre_parse.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/ssl.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/stat.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/statistics.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/string.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/stringprep.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/struct.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/subprocess.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/sunau.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/symtable.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/sysconfig.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/tabnanny.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/tarfile.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/telnetlib.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/tempfile.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/textwrap.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/this.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/threading.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/timeit.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/token.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/tokenize.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/trace.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/traceback.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/tracemalloc.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/tty.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/types.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/typing.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/uu.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/uuid.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/warnings.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/wave.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/weakref.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/webbrowser.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/xdrlib.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/zipapp.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/zipfile.cpython-310.opt-1.pyc", - "windows/Win32/Lib/__pycache__/zipimport.cpython-310.opt-1.pyc", + "windows/Win32/Lib/__hello__.py", + "windows/Win32/Lib/__pycache__/__future__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/__hello__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/_aix_support.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/_bootsubprocess.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/_collections_abc.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/_compat_pickle.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/_compression.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/_markupbase.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/_osx_support.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/_py_abc.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/_pydecimal.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/_pyio.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/_sitebuiltins.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/_strptime.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/_threading_local.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/_weakrefset.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/abc.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/aifc.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/antigravity.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/argparse.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/ast.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/asynchat.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/asyncore.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/base64.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/bdb.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/bisect.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/bz2.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/cProfile.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/calendar.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/cgi.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/cgitb.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/chunk.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/cmd.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/code.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/codecs.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/codeop.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/colorsys.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/compileall.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/configparser.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/contextlib.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/contextvars.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/copy.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/copyreg.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/crypt.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/csv.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/dataclasses.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/datetime.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/decimal.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/difflib.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/dis.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/doctest.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/enum.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/filecmp.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/fileinput.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/fnmatch.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/fractions.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/ftplib.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/functools.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/genericpath.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/getopt.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/getpass.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/gettext.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/glob.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/graphlib.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/gzip.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/hashlib.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/heapq.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/hmac.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/imghdr.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/imp.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/inspect.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/io.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/ipaddress.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/keyword.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/linecache.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/locale.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/lzma.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/mailbox.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/mailcap.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/mimetypes.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/modulefinder.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/netrc.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/nntplib.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/ntpath.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/nturl2path.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/numbers.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/opcode.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/operator.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/optparse.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/os.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/pathlib.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/pdb.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/pickle.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/pickletools.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/pipes.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/pkgutil.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/platform.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/plistlib.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/poplib.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/posixpath.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/pprint.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/profile.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/pstats.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/pty.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/py_compile.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/pyclbr.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/pydoc.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/queue.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/quopri.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/random.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/reprlib.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/rlcompleter.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/runpy.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/sched.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/secrets.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/selectors.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/shelve.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/shlex.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/shutil.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/signal.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/site.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/smtpd.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/smtplib.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/sndhdr.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/socket.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/socketserver.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/sre_compile.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/sre_constants.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/sre_parse.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/ssl.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/stat.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/statistics.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/string.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/stringprep.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/struct.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/subprocess.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/sunau.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/symtable.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/sysconfig.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/tabnanny.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/tarfile.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/telnetlib.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/tempfile.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/textwrap.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/this.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/threading.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/timeit.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/token.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/tokenize.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/trace.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/traceback.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/tracemalloc.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/tty.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/types.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/typing.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/uu.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/uuid.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/warnings.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/wave.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/weakref.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/webbrowser.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/xdrlib.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/zipapp.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/zipfile.cpython-311.opt-1.pyc", + "windows/Win32/Lib/__pycache__/zipimport.cpython-311.opt-1.pyc", "windows/Win32/Lib/_aix_support.py", "windows/Win32/Lib/_bootsubprocess.py", "windows/Win32/Lib/_collections_abc.py", @@ -4638,37 +4752,39 @@ "windows/Win32/Lib/asynchat.py", "windows/Win32/Lib/asyncio/__init__.py", "windows/Win32/Lib/asyncio/__main__.py", - "windows/Win32/Lib/asyncio/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/__main__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/base_events.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/base_futures.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/base_subprocess.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/base_tasks.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/constants.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/coroutines.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/events.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/exceptions.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/format_helpers.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/futures.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/locks.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/log.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/mixins.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/proactor_events.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/protocols.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/queues.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/runners.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/selector_events.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/sslproto.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/staggered.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/streams.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/subprocess.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/tasks.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/threads.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/transports.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/trsock.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/unix_events.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/windows_events.cpython-310.opt-1.pyc", - "windows/Win32/Lib/asyncio/__pycache__/windows_utils.cpython-310.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/__main__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/base_events.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/base_futures.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/base_subprocess.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/base_tasks.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/constants.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/coroutines.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/events.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/exceptions.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/format_helpers.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/futures.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/locks.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/log.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/mixins.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/proactor_events.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/protocols.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/queues.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/runners.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/selector_events.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/sslproto.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/staggered.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/streams.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/subprocess.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/taskgroups.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/tasks.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/threads.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/timeouts.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/transports.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/trsock.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/unix_events.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/windows_events.cpython-311.opt-1.pyc", + "windows/Win32/Lib/asyncio/__pycache__/windows_utils.cpython-311.opt-1.pyc", "windows/Win32/Lib/asyncio/base_events.py", "windows/Win32/Lib/asyncio/base_futures.py", "windows/Win32/Lib/asyncio/base_subprocess.py", @@ -4691,8 +4807,10 @@ "windows/Win32/Lib/asyncio/staggered.py", "windows/Win32/Lib/asyncio/streams.py", "windows/Win32/Lib/asyncio/subprocess.py", + "windows/Win32/Lib/asyncio/taskgroups.py", "windows/Win32/Lib/asyncio/tasks.py", "windows/Win32/Lib/asyncio/threads.py", + "windows/Win32/Lib/asyncio/timeouts.py", "windows/Win32/Lib/asyncio/transports.py", "windows/Win32/Lib/asyncio/trsock.py", "windows/Win32/Lib/asyncio/unix_events.py", @@ -4701,7 +4819,6 @@ "windows/Win32/Lib/asyncore.py", "windows/Win32/Lib/base64.py", "windows/Win32/Lib/bdb.py", - "windows/Win32/Lib/binhex.py", "windows/Win32/Lib/bisect.py", "windows/Win32/Lib/bz2.py", "windows/Win32/Lib/cProfile.py", @@ -4714,18 +4831,18 @@ "windows/Win32/Lib/codecs.py", "windows/Win32/Lib/codeop.py", "windows/Win32/Lib/collections/__init__.py", - "windows/Win32/Lib/collections/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/collections/__pycache__/abc.cpython-310.opt-1.pyc", + "windows/Win32/Lib/collections/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/collections/__pycache__/abc.cpython-311.opt-1.pyc", "windows/Win32/Lib/collections/abc.py", "windows/Win32/Lib/colorsys.py", "windows/Win32/Lib/compileall.py", "windows/Win32/Lib/concurrent/__init__.py", - "windows/Win32/Lib/concurrent/__pycache__/__init__.cpython-310.opt-1.pyc", + "windows/Win32/Lib/concurrent/__pycache__/__init__.cpython-311.opt-1.pyc", "windows/Win32/Lib/concurrent/futures/__init__.py", - "windows/Win32/Lib/concurrent/futures/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/concurrent/futures/__pycache__/_base.cpython-310.opt-1.pyc", - "windows/Win32/Lib/concurrent/futures/__pycache__/process.cpython-310.opt-1.pyc", - "windows/Win32/Lib/concurrent/futures/__pycache__/thread.cpython-310.opt-1.pyc", + "windows/Win32/Lib/concurrent/futures/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/concurrent/futures/__pycache__/_base.cpython-311.opt-1.pyc", + "windows/Win32/Lib/concurrent/futures/__pycache__/process.cpython-311.opt-1.pyc", + "windows/Win32/Lib/concurrent/futures/__pycache__/thread.cpython-311.opt-1.pyc", "windows/Win32/Lib/concurrent/futures/_base.py", "windows/Win32/Lib/concurrent/futures/process.py", "windows/Win32/Lib/concurrent/futures/thread.py", @@ -4737,32 +4854,29 @@ "windows/Win32/Lib/crypt.py", "windows/Win32/Lib/csv.py", "windows/Win32/Lib/ctypes/__init__.py", - "windows/Win32/Lib/ctypes/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/ctypes/__pycache__/_aix.cpython-310.opt-1.pyc", - "windows/Win32/Lib/ctypes/__pycache__/_endian.cpython-310.opt-1.pyc", - "windows/Win32/Lib/ctypes/__pycache__/util.cpython-310.opt-1.pyc", - "windows/Win32/Lib/ctypes/__pycache__/wintypes.cpython-310.opt-1.pyc", + "windows/Win32/Lib/ctypes/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/ctypes/__pycache__/_aix.cpython-311.opt-1.pyc", + "windows/Win32/Lib/ctypes/__pycache__/_endian.cpython-311.opt-1.pyc", + "windows/Win32/Lib/ctypes/__pycache__/util.cpython-311.opt-1.pyc", + "windows/Win32/Lib/ctypes/__pycache__/wintypes.cpython-311.opt-1.pyc", "windows/Win32/Lib/ctypes/_aix.py", "windows/Win32/Lib/ctypes/_endian.py", - "windows/Win32/Lib/ctypes/macholib/README.ctypes", "windows/Win32/Lib/ctypes/macholib/__init__.py", - "windows/Win32/Lib/ctypes/macholib/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/ctypes/macholib/__pycache__/dyld.cpython-310.opt-1.pyc", - "windows/Win32/Lib/ctypes/macholib/__pycache__/dylib.cpython-310.opt-1.pyc", - "windows/Win32/Lib/ctypes/macholib/__pycache__/framework.cpython-310.opt-1.pyc", + "windows/Win32/Lib/ctypes/macholib/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/ctypes/macholib/__pycache__/dyld.cpython-311.opt-1.pyc", + "windows/Win32/Lib/ctypes/macholib/__pycache__/dylib.cpython-311.opt-1.pyc", + "windows/Win32/Lib/ctypes/macholib/__pycache__/framework.cpython-311.opt-1.pyc", "windows/Win32/Lib/ctypes/macholib/dyld.py", "windows/Win32/Lib/ctypes/macholib/dylib.py", - "windows/Win32/Lib/ctypes/macholib/fetch_macholib", - "windows/Win32/Lib/ctypes/macholib/fetch_macholib.bat", "windows/Win32/Lib/ctypes/macholib/framework.py", "windows/Win32/Lib/ctypes/util.py", "windows/Win32/Lib/ctypes/wintypes.py", "windows/Win32/Lib/curses/__init__.py", - "windows/Win32/Lib/curses/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/curses/__pycache__/ascii.cpython-310.opt-1.pyc", - "windows/Win32/Lib/curses/__pycache__/has_key.cpython-310.opt-1.pyc", - "windows/Win32/Lib/curses/__pycache__/panel.cpython-310.opt-1.pyc", - "windows/Win32/Lib/curses/__pycache__/textpad.cpython-310.opt-1.pyc", + "windows/Win32/Lib/curses/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/curses/__pycache__/ascii.cpython-311.opt-1.pyc", + "windows/Win32/Lib/curses/__pycache__/has_key.cpython-311.opt-1.pyc", + "windows/Win32/Lib/curses/__pycache__/panel.cpython-311.opt-1.pyc", + "windows/Win32/Lib/curses/__pycache__/textpad.cpython-311.opt-1.pyc", "windows/Win32/Lib/curses/ascii.py", "windows/Win32/Lib/curses/has_key.py", "windows/Win32/Lib/curses/panel.py", @@ -4774,26 +4888,26 @@ "windows/Win32/Lib/dis.py", "windows/Win32/Lib/doctest.py", "windows/Win32/Lib/email/__init__.py", - "windows/Win32/Lib/email/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/_encoded_words.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/_header_value_parser.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/_parseaddr.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/_policybase.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/base64mime.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/charset.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/contentmanager.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/encoders.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/errors.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/feedparser.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/generator.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/header.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/headerregistry.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/iterators.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/message.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/parser.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/policy.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/quoprimime.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/__pycache__/utils.cpython-310.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/_encoded_words.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/_header_value_parser.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/_parseaddr.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/_policybase.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/base64mime.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/charset.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/contentmanager.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/encoders.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/errors.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/feedparser.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/generator.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/header.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/headerregistry.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/iterators.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/message.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/parser.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/policy.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/quoprimime.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/__pycache__/utils.cpython-311.opt-1.pyc", "windows/Win32/Lib/email/_encoded_words.py", "windows/Win32/Lib/email/_header_value_parser.py", "windows/Win32/Lib/email/_parseaddr.py", @@ -4811,15 +4925,15 @@ "windows/Win32/Lib/email/iterators.py", "windows/Win32/Lib/email/message.py", "windows/Win32/Lib/email/mime/__init__.py", - "windows/Win32/Lib/email/mime/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/mime/__pycache__/application.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/mime/__pycache__/audio.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/mime/__pycache__/base.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/mime/__pycache__/image.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/mime/__pycache__/message.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/mime/__pycache__/multipart.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/mime/__pycache__/nonmultipart.cpython-310.opt-1.pyc", - "windows/Win32/Lib/email/mime/__pycache__/text.cpython-310.opt-1.pyc", + "windows/Win32/Lib/email/mime/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/mime/__pycache__/application.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/mime/__pycache__/audio.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/mime/__pycache__/base.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/mime/__pycache__/image.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/mime/__pycache__/message.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/mime/__pycache__/multipart.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/mime/__pycache__/nonmultipart.cpython-311.opt-1.pyc", + "windows/Win32/Lib/email/mime/__pycache__/text.cpython-311.opt-1.pyc", "windows/Win32/Lib/email/mime/application.py", "windows/Win32/Lib/email/mime/audio.py", "windows/Win32/Lib/email/mime/base.py", @@ -4833,128 +4947,128 @@ "windows/Win32/Lib/email/quoprimime.py", "windows/Win32/Lib/email/utils.py", "windows/Win32/Lib/encodings/__init__.py", - "windows/Win32/Lib/encodings/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/aliases.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/ascii.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/base64_codec.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/big5.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/big5hkscs.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/bz2_codec.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/charmap.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp037.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp1006.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp1026.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp1125.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp1140.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp1250.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp1251.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp1252.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp1253.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp1254.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp1255.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp1256.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp1257.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp1258.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp273.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp424.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp437.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp500.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp720.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp737.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp775.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp850.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp852.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp855.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp856.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp857.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp858.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp860.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp861.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp862.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp863.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp864.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp865.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp866.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp869.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp874.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp875.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp932.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp949.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/cp950.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/euc_jis_2004.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/euc_jisx0213.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/euc_jp.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/euc_kr.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/gb18030.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/gb2312.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/gbk.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/hex_codec.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/hp_roman8.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/hz.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/idna.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso2022_jp.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso2022_jp_1.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso2022_jp_2.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso2022_jp_2004.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso2022_jp_3.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso2022_jp_ext.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso2022_kr.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso8859_1.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso8859_10.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso8859_11.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso8859_13.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso8859_14.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso8859_15.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso8859_16.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso8859_2.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso8859_3.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso8859_4.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso8859_5.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso8859_6.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso8859_7.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso8859_8.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/iso8859_9.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/johab.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/koi8_r.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/koi8_t.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/koi8_u.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/kz1048.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/latin_1.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/mac_arabic.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/mac_croatian.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/mac_cyrillic.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/mac_farsi.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/mac_greek.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/mac_iceland.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/mac_latin2.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/mac_roman.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/mac_romanian.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/mac_turkish.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/mbcs.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/oem.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/palmos.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/ptcp154.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/punycode.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/quopri_codec.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/raw_unicode_escape.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/rot_13.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/shift_jis.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/shift_jis_2004.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/shift_jisx0213.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/tis_620.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/undefined.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/unicode_escape.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/utf_16.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/utf_16_be.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/utf_16_le.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/utf_32.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/utf_32_be.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/utf_32_le.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/utf_7.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/utf_8.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/utf_8_sig.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/uu_codec.cpython-310.opt-1.pyc", - "windows/Win32/Lib/encodings/__pycache__/zlib_codec.cpython-310.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/aliases.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/ascii.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/base64_codec.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/big5.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/big5hkscs.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/bz2_codec.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/charmap.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp037.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp1006.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp1026.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp1125.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp1140.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp1250.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp1251.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp1252.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp1253.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp1254.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp1255.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp1256.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp1257.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp1258.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp273.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp424.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp437.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp500.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp720.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp737.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp775.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp850.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp852.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp855.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp856.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp857.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp858.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp860.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp861.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp862.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp863.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp864.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp865.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp866.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp869.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp874.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp875.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp932.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp949.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/cp950.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/euc_jis_2004.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/euc_jisx0213.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/euc_jp.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/euc_kr.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/gb18030.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/gb2312.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/gbk.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/hex_codec.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/hp_roman8.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/hz.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/idna.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso2022_jp.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso2022_jp_1.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso2022_jp_2.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso2022_jp_2004.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso2022_jp_3.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso2022_jp_ext.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso2022_kr.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso8859_1.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso8859_10.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso8859_11.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso8859_13.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso8859_14.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso8859_15.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso8859_16.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso8859_2.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso8859_3.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso8859_4.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso8859_5.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso8859_6.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso8859_7.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso8859_8.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/iso8859_9.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/johab.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/koi8_r.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/koi8_t.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/koi8_u.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/kz1048.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/latin_1.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/mac_arabic.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/mac_croatian.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/mac_cyrillic.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/mac_farsi.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/mac_greek.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/mac_iceland.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/mac_latin2.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/mac_roman.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/mac_romanian.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/mac_turkish.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/mbcs.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/oem.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/palmos.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/ptcp154.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/punycode.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/quopri_codec.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/raw_unicode_escape.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/rot_13.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/shift_jis.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/shift_jis_2004.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/shift_jisx0213.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/tis_620.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/undefined.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/unicode_escape.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/utf_16.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/utf_16_be.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/utf_16_le.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/utf_32.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/utf_32_be.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/utf_32_le.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/utf_7.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/utf_8.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/utf_8_sig.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/uu_codec.cpython-311.opt-1.pyc", + "windows/Win32/Lib/encodings/__pycache__/zlib_codec.cpython-311.opt-1.pyc", "windows/Win32/Lib/encodings/aliases.py", "windows/Win32/Lib/encodings/ascii.py", "windows/Win32/Lib/encodings/base64_codec.py", @@ -5094,17 +5208,17 @@ "windows/Win32/Lib/heapq.py", "windows/Win32/Lib/hmac.py", "windows/Win32/Lib/html/__init__.py", - "windows/Win32/Lib/html/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/html/__pycache__/entities.cpython-310.opt-1.pyc", - "windows/Win32/Lib/html/__pycache__/parser.cpython-310.opt-1.pyc", + "windows/Win32/Lib/html/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/html/__pycache__/entities.cpython-311.opt-1.pyc", + "windows/Win32/Lib/html/__pycache__/parser.cpython-311.opt-1.pyc", "windows/Win32/Lib/html/entities.py", "windows/Win32/Lib/html/parser.py", "windows/Win32/Lib/http/__init__.py", - "windows/Win32/Lib/http/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/http/__pycache__/client.cpython-310.opt-1.pyc", - "windows/Win32/Lib/http/__pycache__/cookiejar.cpython-310.opt-1.pyc", - "windows/Win32/Lib/http/__pycache__/cookies.cpython-310.opt-1.pyc", - "windows/Win32/Lib/http/__pycache__/server.cpython-310.opt-1.pyc", + "windows/Win32/Lib/http/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/http/__pycache__/client.cpython-311.opt-1.pyc", + "windows/Win32/Lib/http/__pycache__/cookiejar.cpython-311.opt-1.pyc", + "windows/Win32/Lib/http/__pycache__/cookies.cpython-311.opt-1.pyc", + "windows/Win32/Lib/http/__pycache__/server.cpython-311.opt-1.pyc", "windows/Win32/Lib/http/client.py", "windows/Win32/Lib/http/cookiejar.py", "windows/Win32/Lib/http/cookies.py", @@ -5112,32 +5226,28 @@ "windows/Win32/Lib/imghdr.py", "windows/Win32/Lib/imp.py", "windows/Win32/Lib/importlib/__init__.py", - "windows/Win32/Lib/importlib/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/__pycache__/_abc.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/__pycache__/_adapters.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/__pycache__/_bootstrap.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/__pycache__/_bootstrap_external.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/__pycache__/_common.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/__pycache__/abc.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/__pycache__/machinery.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/__pycache__/readers.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/__pycache__/resources.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/__pycache__/util.cpython-310.opt-1.pyc", + "windows/Win32/Lib/importlib/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/__pycache__/_abc.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/__pycache__/_bootstrap.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/__pycache__/_bootstrap_external.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/__pycache__/abc.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/__pycache__/machinery.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/__pycache__/readers.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/__pycache__/simple.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/__pycache__/util.cpython-311.opt-1.pyc", "windows/Win32/Lib/importlib/_abc.py", - "windows/Win32/Lib/importlib/_adapters.py", "windows/Win32/Lib/importlib/_bootstrap.py", "windows/Win32/Lib/importlib/_bootstrap_external.py", - "windows/Win32/Lib/importlib/_common.py", "windows/Win32/Lib/importlib/abc.py", "windows/Win32/Lib/importlib/machinery.py", "windows/Win32/Lib/importlib/metadata/__init__.py", - "windows/Win32/Lib/importlib/metadata/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/metadata/__pycache__/_adapters.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/metadata/__pycache__/_collections.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/metadata/__pycache__/_functools.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/metadata/__pycache__/_itertools.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/metadata/__pycache__/_meta.cpython-310.opt-1.pyc", - "windows/Win32/Lib/importlib/metadata/__pycache__/_text.cpython-310.opt-1.pyc", + "windows/Win32/Lib/importlib/metadata/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/metadata/__pycache__/_adapters.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/metadata/__pycache__/_collections.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/metadata/__pycache__/_functools.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/metadata/__pycache__/_itertools.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/metadata/__pycache__/_meta.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/metadata/__pycache__/_text.cpython-311.opt-1.pyc", "windows/Win32/Lib/importlib/metadata/_adapters.py", "windows/Win32/Lib/importlib/metadata/_collections.py", "windows/Win32/Lib/importlib/metadata/_functools.py", @@ -5145,17 +5255,33 @@ "windows/Win32/Lib/importlib/metadata/_meta.py", "windows/Win32/Lib/importlib/metadata/_text.py", "windows/Win32/Lib/importlib/readers.py", - "windows/Win32/Lib/importlib/resources.py", + "windows/Win32/Lib/importlib/resources/__init__.py", + "windows/Win32/Lib/importlib/resources/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/resources/__pycache__/_adapters.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/resources/__pycache__/_common.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/resources/__pycache__/_itertools.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/resources/__pycache__/_legacy.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/resources/__pycache__/abc.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/resources/__pycache__/readers.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/resources/__pycache__/simple.cpython-311.opt-1.pyc", + "windows/Win32/Lib/importlib/resources/_adapters.py", + "windows/Win32/Lib/importlib/resources/_common.py", + "windows/Win32/Lib/importlib/resources/_itertools.py", + "windows/Win32/Lib/importlib/resources/_legacy.py", + "windows/Win32/Lib/importlib/resources/abc.py", + "windows/Win32/Lib/importlib/resources/readers.py", + "windows/Win32/Lib/importlib/resources/simple.py", + "windows/Win32/Lib/importlib/simple.py", "windows/Win32/Lib/importlib/util.py", "windows/Win32/Lib/inspect.py", "windows/Win32/Lib/io.py", "windows/Win32/Lib/ipaddress.py", "windows/Win32/Lib/json/__init__.py", - "windows/Win32/Lib/json/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/json/__pycache__/decoder.cpython-310.opt-1.pyc", - "windows/Win32/Lib/json/__pycache__/encoder.cpython-310.opt-1.pyc", - "windows/Win32/Lib/json/__pycache__/scanner.cpython-310.opt-1.pyc", - "windows/Win32/Lib/json/__pycache__/tool.cpython-310.opt-1.pyc", + "windows/Win32/Lib/json/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/json/__pycache__/decoder.cpython-311.opt-1.pyc", + "windows/Win32/Lib/json/__pycache__/encoder.cpython-311.opt-1.pyc", + "windows/Win32/Lib/json/__pycache__/scanner.cpython-311.opt-1.pyc", + "windows/Win32/Lib/json/__pycache__/tool.cpython-311.opt-1.pyc", "windows/Win32/Lib/json/decoder.py", "windows/Win32/Lib/json/encoder.py", "windows/Win32/Lib/json/scanner.py", @@ -5164,9 +5290,9 @@ "windows/Win32/Lib/linecache.py", "windows/Win32/Lib/locale.py", "windows/Win32/Lib/logging/__init__.py", - "windows/Win32/Lib/logging/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/logging/__pycache__/config.cpython-310.opt-1.pyc", - "windows/Win32/Lib/logging/__pycache__/handlers.cpython-310.opt-1.pyc", + "windows/Win32/Lib/logging/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/logging/__pycache__/config.cpython-311.opt-1.pyc", + "windows/Win32/Lib/logging/__pycache__/handlers.cpython-311.opt-1.pyc", "windows/Win32/Lib/logging/config.py", "windows/Win32/Lib/logging/handlers.py", "windows/Win32/Lib/lzma.py", @@ -5174,14 +5300,6 @@ "windows/Win32/Lib/mailcap.py", "windows/Win32/Lib/mimetypes.py", "windows/Win32/Lib/modulefinder.py", - "windows/Win32/Lib/msilib/__init__.py", - "windows/Win32/Lib/msilib/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/msilib/__pycache__/schema.cpython-310.opt-1.pyc", - "windows/Win32/Lib/msilib/__pycache__/sequence.cpython-310.opt-1.pyc", - "windows/Win32/Lib/msilib/__pycache__/text.cpython-310.opt-1.pyc", - "windows/Win32/Lib/msilib/schema.py", - "windows/Win32/Lib/msilib/sequence.py", - "windows/Win32/Lib/msilib/text.py", "windows/Win32/Lib/netrc.py", "windows/Win32/Lib/nntplib.py", "windows/Win32/Lib/ntpath.py", @@ -5211,7 +5329,16 @@ "windows/Win32/Lib/queue.py", "windows/Win32/Lib/quopri.py", "windows/Win32/Lib/random.py", - "windows/Win32/Lib/re.py", + "windows/Win32/Lib/re/__init__.py", + "windows/Win32/Lib/re/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/re/__pycache__/_casefix.cpython-311.opt-1.pyc", + "windows/Win32/Lib/re/__pycache__/_compiler.cpython-311.opt-1.pyc", + "windows/Win32/Lib/re/__pycache__/_constants.cpython-311.opt-1.pyc", + "windows/Win32/Lib/re/__pycache__/_parser.cpython-311.opt-1.pyc", + "windows/Win32/Lib/re/_casefix.py", + "windows/Win32/Lib/re/_compiler.py", + "windows/Win32/Lib/re/_constants.py", + "windows/Win32/Lib/re/_parser.py", "windows/Win32/Lib/reprlib.py", "windows/Win32/Lib/rlcompleter.py", "windows/Win32/Lib/runpy.py", @@ -5229,9 +5356,9 @@ "windows/Win32/Lib/socket.py", "windows/Win32/Lib/socketserver.py", "windows/Win32/Lib/sqlite3/__init__.py", - "windows/Win32/Lib/sqlite3/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/sqlite3/__pycache__/dbapi2.cpython-310.opt-1.pyc", - "windows/Win32/Lib/sqlite3/__pycache__/dump.cpython-310.opt-1.pyc", + "windows/Win32/Lib/sqlite3/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/sqlite3/__pycache__/dbapi2.cpython-311.opt-1.pyc", + "windows/Win32/Lib/sqlite3/__pycache__/dump.cpython-311.opt-1.pyc", "windows/Win32/Lib/sqlite3/dbapi2.py", "windows/Win32/Lib/sqlite3/dump.py", "windows/Win32/Lib/sre_compile.py", @@ -5257,6 +5384,14 @@ "windows/Win32/Lib/timeit.py", "windows/Win32/Lib/token.py", "windows/Win32/Lib/tokenize.py", + "windows/Win32/Lib/tomllib/__init__.py", + "windows/Win32/Lib/tomllib/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/tomllib/__pycache__/_parser.cpython-311.opt-1.pyc", + "windows/Win32/Lib/tomllib/__pycache__/_re.cpython-311.opt-1.pyc", + "windows/Win32/Lib/tomllib/__pycache__/_types.cpython-311.opt-1.pyc", + "windows/Win32/Lib/tomllib/_parser.py", + "windows/Win32/Lib/tomllib/_re.py", + "windows/Win32/Lib/tomllib/_types.py", "windows/Win32/Lib/trace.py", "windows/Win32/Lib/traceback.py", "windows/Win32/Lib/tracemalloc.py", @@ -5264,12 +5399,12 @@ "windows/Win32/Lib/types.py", "windows/Win32/Lib/typing.py", "windows/Win32/Lib/urllib/__init__.py", - "windows/Win32/Lib/urllib/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/urllib/__pycache__/error.cpython-310.opt-1.pyc", - "windows/Win32/Lib/urllib/__pycache__/parse.cpython-310.opt-1.pyc", - "windows/Win32/Lib/urllib/__pycache__/request.cpython-310.opt-1.pyc", - "windows/Win32/Lib/urllib/__pycache__/response.cpython-310.opt-1.pyc", - "windows/Win32/Lib/urllib/__pycache__/robotparser.cpython-310.opt-1.pyc", + "windows/Win32/Lib/urllib/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/urllib/__pycache__/error.cpython-311.opt-1.pyc", + "windows/Win32/Lib/urllib/__pycache__/parse.cpython-311.opt-1.pyc", + "windows/Win32/Lib/urllib/__pycache__/request.cpython-311.opt-1.pyc", + "windows/Win32/Lib/urllib/__pycache__/response.cpython-311.opt-1.pyc", + "windows/Win32/Lib/urllib/__pycache__/robotparser.cpython-311.opt-1.pyc", "windows/Win32/Lib/urllib/error.py", "windows/Win32/Lib/urllib/parse.py", "windows/Win32/Lib/urllib/request.py", @@ -5283,17 +5418,17 @@ "windows/Win32/Lib/webbrowser.py", "windows/Win32/Lib/xdrlib.py", "windows/Win32/Lib/xml/__init__.py", - "windows/Win32/Lib/xml/__pycache__/__init__.cpython-310.opt-1.pyc", + "windows/Win32/Lib/xml/__pycache__/__init__.cpython-311.opt-1.pyc", "windows/Win32/Lib/xml/dom/NodeFilter.py", "windows/Win32/Lib/xml/dom/__init__.py", - "windows/Win32/Lib/xml/dom/__pycache__/NodeFilter.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/dom/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/dom/__pycache__/domreg.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/dom/__pycache__/expatbuilder.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/dom/__pycache__/minicompat.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/dom/__pycache__/minidom.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/dom/__pycache__/pulldom.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/dom/__pycache__/xmlbuilder.cpython-310.opt-1.pyc", + "windows/Win32/Lib/xml/dom/__pycache__/NodeFilter.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/dom/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/dom/__pycache__/domreg.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/dom/__pycache__/expatbuilder.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/dom/__pycache__/minicompat.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/dom/__pycache__/minidom.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/dom/__pycache__/pulldom.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/dom/__pycache__/xmlbuilder.cpython-311.opt-1.pyc", "windows/Win32/Lib/xml/dom/domreg.py", "windows/Win32/Lib/xml/dom/expatbuilder.py", "windows/Win32/Lib/xml/dom/minicompat.py", @@ -5304,42 +5439,42 @@ "windows/Win32/Lib/xml/etree/ElementPath.py", "windows/Win32/Lib/xml/etree/ElementTree.py", "windows/Win32/Lib/xml/etree/__init__.py", - "windows/Win32/Lib/xml/etree/__pycache__/ElementInclude.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/etree/__pycache__/ElementPath.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/etree/__pycache__/ElementTree.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/etree/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/etree/__pycache__/cElementTree.cpython-310.opt-1.pyc", + "windows/Win32/Lib/xml/etree/__pycache__/ElementInclude.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/etree/__pycache__/ElementPath.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/etree/__pycache__/ElementTree.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/etree/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/etree/__pycache__/cElementTree.cpython-311.opt-1.pyc", "windows/Win32/Lib/xml/etree/cElementTree.py", "windows/Win32/Lib/xml/parsers/__init__.py", - "windows/Win32/Lib/xml/parsers/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/parsers/__pycache__/expat.cpython-310.opt-1.pyc", + "windows/Win32/Lib/xml/parsers/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/parsers/__pycache__/expat.cpython-311.opt-1.pyc", "windows/Win32/Lib/xml/parsers/expat.py", "windows/Win32/Lib/xml/sax/__init__.py", - "windows/Win32/Lib/xml/sax/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/sax/__pycache__/_exceptions.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/sax/__pycache__/expatreader.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/sax/__pycache__/handler.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/sax/__pycache__/saxutils.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xml/sax/__pycache__/xmlreader.cpython-310.opt-1.pyc", + "windows/Win32/Lib/xml/sax/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/sax/__pycache__/_exceptions.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/sax/__pycache__/expatreader.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/sax/__pycache__/handler.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/sax/__pycache__/saxutils.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xml/sax/__pycache__/xmlreader.cpython-311.opt-1.pyc", "windows/Win32/Lib/xml/sax/_exceptions.py", "windows/Win32/Lib/xml/sax/expatreader.py", "windows/Win32/Lib/xml/sax/handler.py", "windows/Win32/Lib/xml/sax/saxutils.py", "windows/Win32/Lib/xml/sax/xmlreader.py", "windows/Win32/Lib/xmlrpc/__init__.py", - "windows/Win32/Lib/xmlrpc/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xmlrpc/__pycache__/client.cpython-310.opt-1.pyc", - "windows/Win32/Lib/xmlrpc/__pycache__/server.cpython-310.opt-1.pyc", + "windows/Win32/Lib/xmlrpc/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xmlrpc/__pycache__/client.cpython-311.opt-1.pyc", + "windows/Win32/Lib/xmlrpc/__pycache__/server.cpython-311.opt-1.pyc", "windows/Win32/Lib/xmlrpc/client.py", "windows/Win32/Lib/xmlrpc/server.py", "windows/Win32/Lib/zipapp.py", "windows/Win32/Lib/zipfile.py", "windows/Win32/Lib/zipimport.py", "windows/Win32/Lib/zoneinfo/__init__.py", - "windows/Win32/Lib/zoneinfo/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/Win32/Lib/zoneinfo/__pycache__/_common.cpython-310.opt-1.pyc", - "windows/Win32/Lib/zoneinfo/__pycache__/_tzpath.cpython-310.opt-1.pyc", - "windows/Win32/Lib/zoneinfo/__pycache__/_zoneinfo.cpython-310.opt-1.pyc", + "windows/Win32/Lib/zoneinfo/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/Win32/Lib/zoneinfo/__pycache__/_common.cpython-311.opt-1.pyc", + "windows/Win32/Lib/zoneinfo/__pycache__/_tzpath.cpython-311.opt-1.pyc", + "windows/Win32/Lib/zoneinfo/__pycache__/_zoneinfo.cpython-311.opt-1.pyc", "windows/Win32/Lib/zoneinfo/_common.py", "windows/Win32/Lib/zoneinfo/_tzpath.py", "windows/Win32/Lib/zoneinfo/_zoneinfo.py", @@ -5350,8 +5485,8 @@ "windows/Win32/msvcp140d.dll", "windows/Win32/ogg.dll", "windows/Win32/python.exe", - "windows/Win32/python310.dll", - "windows/Win32/python310_d.dll", + "windows/Win32/python311.dll", + "windows/Win32/python311_d.dll", "windows/Win32/python_d.exe", "windows/Win32/pythonw.exe", "windows/Win32/pythonw_d.exe", @@ -5408,7 +5543,7 @@ "windows/x64/DLLs/_zoneinfo.pyd", "windows/x64/DLLs/_zoneinfo_d.pyd", "windows/x64/DLLs/libcrypto-1_1.dll", - "windows/x64/DLLs/libffi-7.dll", + "windows/x64/DLLs/libffi-8.dll", "windows/x64/DLLs/libssl-1_1.dll", "windows/x64/DLLs/pyexpat.pyd", "windows/x64/DLLs/pyexpat_d.pyd", @@ -5425,174 +5560,172 @@ "windows/x64/DLLs/winsound.pyd", "windows/x64/DLLs/winsound_d.pyd", "windows/x64/Lib/__future__.py", - "windows/x64/Lib/__phello__.foo.py", - "windows/x64/Lib/__pycache__/__future__.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/__phello__.foo.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/_aix_support.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/_bootsubprocess.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/_collections_abc.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/_compat_pickle.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/_compression.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/_markupbase.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/_osx_support.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/_py_abc.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/_pydecimal.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/_pyio.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/_sitebuiltins.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/_strptime.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/_threading_local.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/_weakrefset.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/abc.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/aifc.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/antigravity.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/argparse.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/ast.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/asynchat.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/asyncore.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/base64.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/bdb.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/binhex.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/bisect.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/bz2.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/cProfile.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/calendar.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/cgi.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/cgitb.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/chunk.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/cmd.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/code.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/codecs.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/codeop.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/colorsys.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/compileall.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/configparser.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/contextlib.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/contextvars.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/copy.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/copyreg.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/crypt.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/csv.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/dataclasses.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/datetime.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/decimal.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/difflib.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/dis.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/doctest.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/enum.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/filecmp.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/fileinput.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/fnmatch.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/fractions.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/ftplib.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/functools.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/genericpath.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/getopt.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/getpass.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/gettext.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/glob.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/graphlib.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/gzip.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/hashlib.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/heapq.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/hmac.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/imghdr.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/imp.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/inspect.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/io.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/ipaddress.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/keyword.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/linecache.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/locale.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/lzma.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/mailbox.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/mailcap.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/mimetypes.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/modulefinder.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/netrc.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/nntplib.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/ntpath.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/nturl2path.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/numbers.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/opcode.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/operator.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/optparse.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/os.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/pathlib.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/pdb.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/pickle.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/pickletools.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/pipes.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/pkgutil.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/platform.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/plistlib.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/poplib.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/posixpath.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/pprint.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/profile.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/pstats.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/pty.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/py_compile.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/pyclbr.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/pydoc.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/queue.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/quopri.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/random.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/re.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/reprlib.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/rlcompleter.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/runpy.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/sched.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/secrets.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/selectors.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/shelve.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/shlex.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/shutil.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/signal.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/site.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/smtpd.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/smtplib.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/sndhdr.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/socket.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/socketserver.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/sre_compile.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/sre_constants.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/sre_parse.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/ssl.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/stat.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/statistics.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/string.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/stringprep.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/struct.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/subprocess.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/sunau.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/symtable.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/sysconfig.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/tabnanny.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/tarfile.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/telnetlib.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/tempfile.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/textwrap.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/this.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/threading.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/timeit.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/token.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/tokenize.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/trace.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/traceback.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/tracemalloc.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/tty.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/types.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/typing.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/uu.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/uuid.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/warnings.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/wave.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/weakref.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/webbrowser.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/xdrlib.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/zipapp.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/zipfile.cpython-310.opt-1.pyc", - "windows/x64/Lib/__pycache__/zipimport.cpython-310.opt-1.pyc", + "windows/x64/Lib/__hello__.py", + "windows/x64/Lib/__pycache__/__future__.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/__hello__.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/_aix_support.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/_bootsubprocess.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/_collections_abc.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/_compat_pickle.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/_compression.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/_markupbase.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/_osx_support.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/_py_abc.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/_pydecimal.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/_pyio.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/_sitebuiltins.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/_strptime.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/_threading_local.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/_weakrefset.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/abc.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/aifc.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/antigravity.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/argparse.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/ast.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/asynchat.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/asyncore.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/base64.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/bdb.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/bisect.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/bz2.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/cProfile.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/calendar.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/cgi.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/cgitb.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/chunk.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/cmd.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/code.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/codecs.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/codeop.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/colorsys.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/compileall.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/configparser.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/contextlib.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/contextvars.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/copy.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/copyreg.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/crypt.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/csv.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/dataclasses.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/datetime.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/decimal.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/difflib.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/dis.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/doctest.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/enum.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/filecmp.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/fileinput.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/fnmatch.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/fractions.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/ftplib.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/functools.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/genericpath.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/getopt.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/getpass.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/gettext.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/glob.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/graphlib.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/gzip.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/hashlib.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/heapq.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/hmac.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/imghdr.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/imp.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/inspect.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/io.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/ipaddress.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/keyword.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/linecache.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/locale.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/lzma.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/mailbox.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/mailcap.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/mimetypes.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/modulefinder.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/netrc.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/nntplib.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/ntpath.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/nturl2path.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/numbers.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/opcode.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/operator.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/optparse.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/os.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/pathlib.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/pdb.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/pickle.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/pickletools.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/pipes.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/pkgutil.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/platform.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/plistlib.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/poplib.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/posixpath.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/pprint.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/profile.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/pstats.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/pty.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/py_compile.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/pyclbr.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/pydoc.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/queue.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/quopri.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/random.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/reprlib.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/rlcompleter.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/runpy.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/sched.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/secrets.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/selectors.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/shelve.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/shlex.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/shutil.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/signal.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/site.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/smtpd.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/smtplib.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/sndhdr.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/socket.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/socketserver.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/sre_compile.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/sre_constants.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/sre_parse.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/ssl.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/stat.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/statistics.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/string.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/stringprep.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/struct.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/subprocess.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/sunau.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/symtable.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/sysconfig.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/tabnanny.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/tarfile.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/telnetlib.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/tempfile.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/textwrap.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/this.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/threading.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/timeit.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/token.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/tokenize.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/trace.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/traceback.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/tracemalloc.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/tty.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/types.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/typing.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/uu.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/uuid.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/warnings.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/wave.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/weakref.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/webbrowser.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/xdrlib.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/zipapp.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/zipfile.cpython-311.opt-1.pyc", + "windows/x64/Lib/__pycache__/zipimport.cpython-311.opt-1.pyc", "windows/x64/Lib/_aix_support.py", "windows/x64/Lib/_bootsubprocess.py", "windows/x64/Lib/_collections_abc.py", @@ -5615,37 +5748,39 @@ "windows/x64/Lib/asynchat.py", "windows/x64/Lib/asyncio/__init__.py", "windows/x64/Lib/asyncio/__main__.py", - "windows/x64/Lib/asyncio/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/__main__.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/base_events.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/base_futures.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/base_subprocess.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/base_tasks.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/constants.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/coroutines.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/events.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/exceptions.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/format_helpers.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/futures.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/locks.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/log.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/mixins.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/proactor_events.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/protocols.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/queues.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/runners.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/selector_events.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/sslproto.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/staggered.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/streams.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/subprocess.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/tasks.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/threads.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/transports.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/trsock.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/unix_events.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/windows_events.cpython-310.opt-1.pyc", - "windows/x64/Lib/asyncio/__pycache__/windows_utils.cpython-310.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/__main__.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/base_events.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/base_futures.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/base_subprocess.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/base_tasks.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/constants.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/coroutines.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/events.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/exceptions.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/format_helpers.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/futures.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/locks.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/log.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/mixins.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/proactor_events.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/protocols.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/queues.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/runners.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/selector_events.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/sslproto.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/staggered.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/streams.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/subprocess.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/taskgroups.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/tasks.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/threads.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/timeouts.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/transports.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/trsock.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/unix_events.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/windows_events.cpython-311.opt-1.pyc", + "windows/x64/Lib/asyncio/__pycache__/windows_utils.cpython-311.opt-1.pyc", "windows/x64/Lib/asyncio/base_events.py", "windows/x64/Lib/asyncio/base_futures.py", "windows/x64/Lib/asyncio/base_subprocess.py", @@ -5668,8 +5803,10 @@ "windows/x64/Lib/asyncio/staggered.py", "windows/x64/Lib/asyncio/streams.py", "windows/x64/Lib/asyncio/subprocess.py", + "windows/x64/Lib/asyncio/taskgroups.py", "windows/x64/Lib/asyncio/tasks.py", "windows/x64/Lib/asyncio/threads.py", + "windows/x64/Lib/asyncio/timeouts.py", "windows/x64/Lib/asyncio/transports.py", "windows/x64/Lib/asyncio/trsock.py", "windows/x64/Lib/asyncio/unix_events.py", @@ -5678,7 +5815,6 @@ "windows/x64/Lib/asyncore.py", "windows/x64/Lib/base64.py", "windows/x64/Lib/bdb.py", - "windows/x64/Lib/binhex.py", "windows/x64/Lib/bisect.py", "windows/x64/Lib/bz2.py", "windows/x64/Lib/cProfile.py", @@ -5691,18 +5827,18 @@ "windows/x64/Lib/codecs.py", "windows/x64/Lib/codeop.py", "windows/x64/Lib/collections/__init__.py", - "windows/x64/Lib/collections/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/collections/__pycache__/abc.cpython-310.opt-1.pyc", + "windows/x64/Lib/collections/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/collections/__pycache__/abc.cpython-311.opt-1.pyc", "windows/x64/Lib/collections/abc.py", "windows/x64/Lib/colorsys.py", "windows/x64/Lib/compileall.py", "windows/x64/Lib/concurrent/__init__.py", - "windows/x64/Lib/concurrent/__pycache__/__init__.cpython-310.opt-1.pyc", + "windows/x64/Lib/concurrent/__pycache__/__init__.cpython-311.opt-1.pyc", "windows/x64/Lib/concurrent/futures/__init__.py", - "windows/x64/Lib/concurrent/futures/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/concurrent/futures/__pycache__/_base.cpython-310.opt-1.pyc", - "windows/x64/Lib/concurrent/futures/__pycache__/process.cpython-310.opt-1.pyc", - "windows/x64/Lib/concurrent/futures/__pycache__/thread.cpython-310.opt-1.pyc", + "windows/x64/Lib/concurrent/futures/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/concurrent/futures/__pycache__/_base.cpython-311.opt-1.pyc", + "windows/x64/Lib/concurrent/futures/__pycache__/process.cpython-311.opt-1.pyc", + "windows/x64/Lib/concurrent/futures/__pycache__/thread.cpython-311.opt-1.pyc", "windows/x64/Lib/concurrent/futures/_base.py", "windows/x64/Lib/concurrent/futures/process.py", "windows/x64/Lib/concurrent/futures/thread.py", @@ -5714,32 +5850,29 @@ "windows/x64/Lib/crypt.py", "windows/x64/Lib/csv.py", "windows/x64/Lib/ctypes/__init__.py", - "windows/x64/Lib/ctypes/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/ctypes/__pycache__/_aix.cpython-310.opt-1.pyc", - "windows/x64/Lib/ctypes/__pycache__/_endian.cpython-310.opt-1.pyc", - "windows/x64/Lib/ctypes/__pycache__/util.cpython-310.opt-1.pyc", - "windows/x64/Lib/ctypes/__pycache__/wintypes.cpython-310.opt-1.pyc", + "windows/x64/Lib/ctypes/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/ctypes/__pycache__/_aix.cpython-311.opt-1.pyc", + "windows/x64/Lib/ctypes/__pycache__/_endian.cpython-311.opt-1.pyc", + "windows/x64/Lib/ctypes/__pycache__/util.cpython-311.opt-1.pyc", + "windows/x64/Lib/ctypes/__pycache__/wintypes.cpython-311.opt-1.pyc", "windows/x64/Lib/ctypes/_aix.py", "windows/x64/Lib/ctypes/_endian.py", - "windows/x64/Lib/ctypes/macholib/README.ctypes", "windows/x64/Lib/ctypes/macholib/__init__.py", - "windows/x64/Lib/ctypes/macholib/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/ctypes/macholib/__pycache__/dyld.cpython-310.opt-1.pyc", - "windows/x64/Lib/ctypes/macholib/__pycache__/dylib.cpython-310.opt-1.pyc", - "windows/x64/Lib/ctypes/macholib/__pycache__/framework.cpython-310.opt-1.pyc", + "windows/x64/Lib/ctypes/macholib/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/ctypes/macholib/__pycache__/dyld.cpython-311.opt-1.pyc", + "windows/x64/Lib/ctypes/macholib/__pycache__/dylib.cpython-311.opt-1.pyc", + "windows/x64/Lib/ctypes/macholib/__pycache__/framework.cpython-311.opt-1.pyc", "windows/x64/Lib/ctypes/macholib/dyld.py", "windows/x64/Lib/ctypes/macholib/dylib.py", - "windows/x64/Lib/ctypes/macholib/fetch_macholib", - "windows/x64/Lib/ctypes/macholib/fetch_macholib.bat", "windows/x64/Lib/ctypes/macholib/framework.py", "windows/x64/Lib/ctypes/util.py", "windows/x64/Lib/ctypes/wintypes.py", "windows/x64/Lib/curses/__init__.py", - "windows/x64/Lib/curses/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/curses/__pycache__/ascii.cpython-310.opt-1.pyc", - "windows/x64/Lib/curses/__pycache__/has_key.cpython-310.opt-1.pyc", - "windows/x64/Lib/curses/__pycache__/panel.cpython-310.opt-1.pyc", - "windows/x64/Lib/curses/__pycache__/textpad.cpython-310.opt-1.pyc", + "windows/x64/Lib/curses/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/curses/__pycache__/ascii.cpython-311.opt-1.pyc", + "windows/x64/Lib/curses/__pycache__/has_key.cpython-311.opt-1.pyc", + "windows/x64/Lib/curses/__pycache__/panel.cpython-311.opt-1.pyc", + "windows/x64/Lib/curses/__pycache__/textpad.cpython-311.opt-1.pyc", "windows/x64/Lib/curses/ascii.py", "windows/x64/Lib/curses/has_key.py", "windows/x64/Lib/curses/panel.py", @@ -5751,26 +5884,26 @@ "windows/x64/Lib/dis.py", "windows/x64/Lib/doctest.py", "windows/x64/Lib/email/__init__.py", - "windows/x64/Lib/email/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/_encoded_words.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/_header_value_parser.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/_parseaddr.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/_policybase.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/base64mime.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/charset.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/contentmanager.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/encoders.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/errors.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/feedparser.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/generator.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/header.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/headerregistry.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/iterators.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/message.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/parser.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/policy.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/quoprimime.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/__pycache__/utils.cpython-310.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/_encoded_words.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/_header_value_parser.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/_parseaddr.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/_policybase.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/base64mime.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/charset.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/contentmanager.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/encoders.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/errors.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/feedparser.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/generator.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/header.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/headerregistry.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/iterators.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/message.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/parser.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/policy.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/quoprimime.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/__pycache__/utils.cpython-311.opt-1.pyc", "windows/x64/Lib/email/_encoded_words.py", "windows/x64/Lib/email/_header_value_parser.py", "windows/x64/Lib/email/_parseaddr.py", @@ -5788,15 +5921,15 @@ "windows/x64/Lib/email/iterators.py", "windows/x64/Lib/email/message.py", "windows/x64/Lib/email/mime/__init__.py", - "windows/x64/Lib/email/mime/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/mime/__pycache__/application.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/mime/__pycache__/audio.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/mime/__pycache__/base.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/mime/__pycache__/image.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/mime/__pycache__/message.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/mime/__pycache__/multipart.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/mime/__pycache__/nonmultipart.cpython-310.opt-1.pyc", - "windows/x64/Lib/email/mime/__pycache__/text.cpython-310.opt-1.pyc", + "windows/x64/Lib/email/mime/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/mime/__pycache__/application.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/mime/__pycache__/audio.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/mime/__pycache__/base.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/mime/__pycache__/image.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/mime/__pycache__/message.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/mime/__pycache__/multipart.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/mime/__pycache__/nonmultipart.cpython-311.opt-1.pyc", + "windows/x64/Lib/email/mime/__pycache__/text.cpython-311.opt-1.pyc", "windows/x64/Lib/email/mime/application.py", "windows/x64/Lib/email/mime/audio.py", "windows/x64/Lib/email/mime/base.py", @@ -5810,128 +5943,128 @@ "windows/x64/Lib/email/quoprimime.py", "windows/x64/Lib/email/utils.py", "windows/x64/Lib/encodings/__init__.py", - "windows/x64/Lib/encodings/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/aliases.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/ascii.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/base64_codec.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/big5.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/big5hkscs.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/bz2_codec.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/charmap.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp037.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp1006.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp1026.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp1125.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp1140.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp1250.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp1251.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp1252.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp1253.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp1254.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp1255.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp1256.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp1257.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp1258.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp273.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp424.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp437.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp500.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp720.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp737.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp775.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp850.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp852.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp855.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp856.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp857.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp858.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp860.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp861.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp862.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp863.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp864.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp865.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp866.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp869.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp874.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp875.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp932.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp949.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/cp950.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/euc_jis_2004.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/euc_jisx0213.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/euc_jp.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/euc_kr.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/gb18030.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/gb2312.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/gbk.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/hex_codec.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/hp_roman8.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/hz.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/idna.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso2022_jp.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso2022_jp_1.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso2022_jp_2.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso2022_jp_2004.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso2022_jp_3.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso2022_jp_ext.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso2022_kr.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso8859_1.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso8859_10.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso8859_11.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso8859_13.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso8859_14.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso8859_15.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso8859_16.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso8859_2.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso8859_3.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso8859_4.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso8859_5.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso8859_6.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso8859_7.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso8859_8.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/iso8859_9.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/johab.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/koi8_r.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/koi8_t.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/koi8_u.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/kz1048.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/latin_1.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/mac_arabic.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/mac_croatian.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/mac_cyrillic.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/mac_farsi.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/mac_greek.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/mac_iceland.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/mac_latin2.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/mac_roman.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/mac_romanian.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/mac_turkish.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/mbcs.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/oem.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/palmos.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/ptcp154.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/punycode.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/quopri_codec.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/raw_unicode_escape.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/rot_13.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/shift_jis.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/shift_jis_2004.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/shift_jisx0213.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/tis_620.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/undefined.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/unicode_escape.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/utf_16.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/utf_16_be.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/utf_16_le.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/utf_32.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/utf_32_be.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/utf_32_le.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/utf_7.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/utf_8.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/utf_8_sig.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/uu_codec.cpython-310.opt-1.pyc", - "windows/x64/Lib/encodings/__pycache__/zlib_codec.cpython-310.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/aliases.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/ascii.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/base64_codec.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/big5.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/big5hkscs.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/bz2_codec.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/charmap.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp037.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp1006.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp1026.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp1125.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp1140.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp1250.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp1251.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp1252.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp1253.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp1254.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp1255.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp1256.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp1257.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp1258.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp273.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp424.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp437.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp500.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp720.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp737.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp775.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp850.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp852.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp855.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp856.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp857.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp858.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp860.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp861.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp862.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp863.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp864.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp865.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp866.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp869.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp874.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp875.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp932.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp949.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/cp950.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/euc_jis_2004.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/euc_jisx0213.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/euc_jp.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/euc_kr.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/gb18030.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/gb2312.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/gbk.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/hex_codec.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/hp_roman8.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/hz.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/idna.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso2022_jp.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso2022_jp_1.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso2022_jp_2.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso2022_jp_2004.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso2022_jp_3.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso2022_jp_ext.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso2022_kr.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso8859_1.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso8859_10.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso8859_11.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso8859_13.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso8859_14.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso8859_15.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso8859_16.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso8859_2.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso8859_3.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso8859_4.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso8859_5.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso8859_6.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso8859_7.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso8859_8.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/iso8859_9.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/johab.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/koi8_r.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/koi8_t.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/koi8_u.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/kz1048.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/latin_1.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/mac_arabic.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/mac_croatian.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/mac_cyrillic.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/mac_farsi.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/mac_greek.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/mac_iceland.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/mac_latin2.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/mac_roman.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/mac_romanian.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/mac_turkish.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/mbcs.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/oem.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/palmos.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/ptcp154.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/punycode.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/quopri_codec.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/raw_unicode_escape.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/rot_13.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/shift_jis.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/shift_jis_2004.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/shift_jisx0213.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/tis_620.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/undefined.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/unicode_escape.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/utf_16.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/utf_16_be.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/utf_16_le.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/utf_32.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/utf_32_be.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/utf_32_le.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/utf_7.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/utf_8.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/utf_8_sig.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/uu_codec.cpython-311.opt-1.pyc", + "windows/x64/Lib/encodings/__pycache__/zlib_codec.cpython-311.opt-1.pyc", "windows/x64/Lib/encodings/aliases.py", "windows/x64/Lib/encodings/ascii.py", "windows/x64/Lib/encodings/base64_codec.py", @@ -6071,17 +6204,17 @@ "windows/x64/Lib/heapq.py", "windows/x64/Lib/hmac.py", "windows/x64/Lib/html/__init__.py", - "windows/x64/Lib/html/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/html/__pycache__/entities.cpython-310.opt-1.pyc", - "windows/x64/Lib/html/__pycache__/parser.cpython-310.opt-1.pyc", + "windows/x64/Lib/html/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/html/__pycache__/entities.cpython-311.opt-1.pyc", + "windows/x64/Lib/html/__pycache__/parser.cpython-311.opt-1.pyc", "windows/x64/Lib/html/entities.py", "windows/x64/Lib/html/parser.py", "windows/x64/Lib/http/__init__.py", - "windows/x64/Lib/http/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/http/__pycache__/client.cpython-310.opt-1.pyc", - "windows/x64/Lib/http/__pycache__/cookiejar.cpython-310.opt-1.pyc", - "windows/x64/Lib/http/__pycache__/cookies.cpython-310.opt-1.pyc", - "windows/x64/Lib/http/__pycache__/server.cpython-310.opt-1.pyc", + "windows/x64/Lib/http/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/http/__pycache__/client.cpython-311.opt-1.pyc", + "windows/x64/Lib/http/__pycache__/cookiejar.cpython-311.opt-1.pyc", + "windows/x64/Lib/http/__pycache__/cookies.cpython-311.opt-1.pyc", + "windows/x64/Lib/http/__pycache__/server.cpython-311.opt-1.pyc", "windows/x64/Lib/http/client.py", "windows/x64/Lib/http/cookiejar.py", "windows/x64/Lib/http/cookies.py", @@ -6089,32 +6222,28 @@ "windows/x64/Lib/imghdr.py", "windows/x64/Lib/imp.py", "windows/x64/Lib/importlib/__init__.py", - "windows/x64/Lib/importlib/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/__pycache__/_abc.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/__pycache__/_adapters.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/__pycache__/_bootstrap.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/__pycache__/_bootstrap_external.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/__pycache__/_common.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/__pycache__/abc.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/__pycache__/machinery.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/__pycache__/readers.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/__pycache__/resources.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/__pycache__/util.cpython-310.opt-1.pyc", + "windows/x64/Lib/importlib/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/__pycache__/_abc.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/__pycache__/_bootstrap.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/__pycache__/_bootstrap_external.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/__pycache__/abc.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/__pycache__/machinery.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/__pycache__/readers.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/__pycache__/simple.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/__pycache__/util.cpython-311.opt-1.pyc", "windows/x64/Lib/importlib/_abc.py", - "windows/x64/Lib/importlib/_adapters.py", "windows/x64/Lib/importlib/_bootstrap.py", "windows/x64/Lib/importlib/_bootstrap_external.py", - "windows/x64/Lib/importlib/_common.py", "windows/x64/Lib/importlib/abc.py", "windows/x64/Lib/importlib/machinery.py", "windows/x64/Lib/importlib/metadata/__init__.py", - "windows/x64/Lib/importlib/metadata/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/metadata/__pycache__/_adapters.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/metadata/__pycache__/_collections.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/metadata/__pycache__/_functools.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/metadata/__pycache__/_itertools.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/metadata/__pycache__/_meta.cpython-310.opt-1.pyc", - "windows/x64/Lib/importlib/metadata/__pycache__/_text.cpython-310.opt-1.pyc", + "windows/x64/Lib/importlib/metadata/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/metadata/__pycache__/_adapters.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/metadata/__pycache__/_collections.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/metadata/__pycache__/_functools.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/metadata/__pycache__/_itertools.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/metadata/__pycache__/_meta.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/metadata/__pycache__/_text.cpython-311.opt-1.pyc", "windows/x64/Lib/importlib/metadata/_adapters.py", "windows/x64/Lib/importlib/metadata/_collections.py", "windows/x64/Lib/importlib/metadata/_functools.py", @@ -6122,17 +6251,33 @@ "windows/x64/Lib/importlib/metadata/_meta.py", "windows/x64/Lib/importlib/metadata/_text.py", "windows/x64/Lib/importlib/readers.py", - "windows/x64/Lib/importlib/resources.py", + "windows/x64/Lib/importlib/resources/__init__.py", + "windows/x64/Lib/importlib/resources/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/resources/__pycache__/_adapters.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/resources/__pycache__/_common.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/resources/__pycache__/_itertools.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/resources/__pycache__/_legacy.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/resources/__pycache__/abc.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/resources/__pycache__/readers.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/resources/__pycache__/simple.cpython-311.opt-1.pyc", + "windows/x64/Lib/importlib/resources/_adapters.py", + "windows/x64/Lib/importlib/resources/_common.py", + "windows/x64/Lib/importlib/resources/_itertools.py", + "windows/x64/Lib/importlib/resources/_legacy.py", + "windows/x64/Lib/importlib/resources/abc.py", + "windows/x64/Lib/importlib/resources/readers.py", + "windows/x64/Lib/importlib/resources/simple.py", + "windows/x64/Lib/importlib/simple.py", "windows/x64/Lib/importlib/util.py", "windows/x64/Lib/inspect.py", "windows/x64/Lib/io.py", "windows/x64/Lib/ipaddress.py", "windows/x64/Lib/json/__init__.py", - "windows/x64/Lib/json/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/json/__pycache__/decoder.cpython-310.opt-1.pyc", - "windows/x64/Lib/json/__pycache__/encoder.cpython-310.opt-1.pyc", - "windows/x64/Lib/json/__pycache__/scanner.cpython-310.opt-1.pyc", - "windows/x64/Lib/json/__pycache__/tool.cpython-310.opt-1.pyc", + "windows/x64/Lib/json/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/json/__pycache__/decoder.cpython-311.opt-1.pyc", + "windows/x64/Lib/json/__pycache__/encoder.cpython-311.opt-1.pyc", + "windows/x64/Lib/json/__pycache__/scanner.cpython-311.opt-1.pyc", + "windows/x64/Lib/json/__pycache__/tool.cpython-311.opt-1.pyc", "windows/x64/Lib/json/decoder.py", "windows/x64/Lib/json/encoder.py", "windows/x64/Lib/json/scanner.py", @@ -6141,9 +6286,9 @@ "windows/x64/Lib/linecache.py", "windows/x64/Lib/locale.py", "windows/x64/Lib/logging/__init__.py", - "windows/x64/Lib/logging/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/logging/__pycache__/config.cpython-310.opt-1.pyc", - "windows/x64/Lib/logging/__pycache__/handlers.cpython-310.opt-1.pyc", + "windows/x64/Lib/logging/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/logging/__pycache__/config.cpython-311.opt-1.pyc", + "windows/x64/Lib/logging/__pycache__/handlers.cpython-311.opt-1.pyc", "windows/x64/Lib/logging/config.py", "windows/x64/Lib/logging/handlers.py", "windows/x64/Lib/lzma.py", @@ -6151,14 +6296,6 @@ "windows/x64/Lib/mailcap.py", "windows/x64/Lib/mimetypes.py", "windows/x64/Lib/modulefinder.py", - "windows/x64/Lib/msilib/__init__.py", - "windows/x64/Lib/msilib/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/msilib/__pycache__/schema.cpython-310.opt-1.pyc", - "windows/x64/Lib/msilib/__pycache__/sequence.cpython-310.opt-1.pyc", - "windows/x64/Lib/msilib/__pycache__/text.cpython-310.opt-1.pyc", - "windows/x64/Lib/msilib/schema.py", - "windows/x64/Lib/msilib/sequence.py", - "windows/x64/Lib/msilib/text.py", "windows/x64/Lib/netrc.py", "windows/x64/Lib/nntplib.py", "windows/x64/Lib/ntpath.py", @@ -6188,7 +6325,16 @@ "windows/x64/Lib/queue.py", "windows/x64/Lib/quopri.py", "windows/x64/Lib/random.py", - "windows/x64/Lib/re.py", + "windows/x64/Lib/re/__init__.py", + "windows/x64/Lib/re/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/re/__pycache__/_casefix.cpython-311.opt-1.pyc", + "windows/x64/Lib/re/__pycache__/_compiler.cpython-311.opt-1.pyc", + "windows/x64/Lib/re/__pycache__/_constants.cpython-311.opt-1.pyc", + "windows/x64/Lib/re/__pycache__/_parser.cpython-311.opt-1.pyc", + "windows/x64/Lib/re/_casefix.py", + "windows/x64/Lib/re/_compiler.py", + "windows/x64/Lib/re/_constants.py", + "windows/x64/Lib/re/_parser.py", "windows/x64/Lib/reprlib.py", "windows/x64/Lib/rlcompleter.py", "windows/x64/Lib/runpy.py", @@ -6206,9 +6352,9 @@ "windows/x64/Lib/socket.py", "windows/x64/Lib/socketserver.py", "windows/x64/Lib/sqlite3/__init__.py", - "windows/x64/Lib/sqlite3/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/sqlite3/__pycache__/dbapi2.cpython-310.opt-1.pyc", - "windows/x64/Lib/sqlite3/__pycache__/dump.cpython-310.opt-1.pyc", + "windows/x64/Lib/sqlite3/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/sqlite3/__pycache__/dbapi2.cpython-311.opt-1.pyc", + "windows/x64/Lib/sqlite3/__pycache__/dump.cpython-311.opt-1.pyc", "windows/x64/Lib/sqlite3/dbapi2.py", "windows/x64/Lib/sqlite3/dump.py", "windows/x64/Lib/sre_compile.py", @@ -6234,6 +6380,14 @@ "windows/x64/Lib/timeit.py", "windows/x64/Lib/token.py", "windows/x64/Lib/tokenize.py", + "windows/x64/Lib/tomllib/__init__.py", + "windows/x64/Lib/tomllib/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/tomllib/__pycache__/_parser.cpython-311.opt-1.pyc", + "windows/x64/Lib/tomllib/__pycache__/_re.cpython-311.opt-1.pyc", + "windows/x64/Lib/tomllib/__pycache__/_types.cpython-311.opt-1.pyc", + "windows/x64/Lib/tomllib/_parser.py", + "windows/x64/Lib/tomllib/_re.py", + "windows/x64/Lib/tomllib/_types.py", "windows/x64/Lib/trace.py", "windows/x64/Lib/traceback.py", "windows/x64/Lib/tracemalloc.py", @@ -6241,12 +6395,12 @@ "windows/x64/Lib/types.py", "windows/x64/Lib/typing.py", "windows/x64/Lib/urllib/__init__.py", - "windows/x64/Lib/urllib/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/urllib/__pycache__/error.cpython-310.opt-1.pyc", - "windows/x64/Lib/urllib/__pycache__/parse.cpython-310.opt-1.pyc", - "windows/x64/Lib/urllib/__pycache__/request.cpython-310.opt-1.pyc", - "windows/x64/Lib/urllib/__pycache__/response.cpython-310.opt-1.pyc", - "windows/x64/Lib/urllib/__pycache__/robotparser.cpython-310.opt-1.pyc", + "windows/x64/Lib/urllib/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/urllib/__pycache__/error.cpython-311.opt-1.pyc", + "windows/x64/Lib/urllib/__pycache__/parse.cpython-311.opt-1.pyc", + "windows/x64/Lib/urllib/__pycache__/request.cpython-311.opt-1.pyc", + "windows/x64/Lib/urllib/__pycache__/response.cpython-311.opt-1.pyc", + "windows/x64/Lib/urllib/__pycache__/robotparser.cpython-311.opt-1.pyc", "windows/x64/Lib/urllib/error.py", "windows/x64/Lib/urllib/parse.py", "windows/x64/Lib/urllib/request.py", @@ -6260,17 +6414,17 @@ "windows/x64/Lib/webbrowser.py", "windows/x64/Lib/xdrlib.py", "windows/x64/Lib/xml/__init__.py", - "windows/x64/Lib/xml/__pycache__/__init__.cpython-310.opt-1.pyc", + "windows/x64/Lib/xml/__pycache__/__init__.cpython-311.opt-1.pyc", "windows/x64/Lib/xml/dom/NodeFilter.py", "windows/x64/Lib/xml/dom/__init__.py", - "windows/x64/Lib/xml/dom/__pycache__/NodeFilter.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/dom/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/dom/__pycache__/domreg.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/dom/__pycache__/expatbuilder.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/dom/__pycache__/minicompat.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/dom/__pycache__/minidom.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/dom/__pycache__/pulldom.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/dom/__pycache__/xmlbuilder.cpython-310.opt-1.pyc", + "windows/x64/Lib/xml/dom/__pycache__/NodeFilter.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/dom/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/dom/__pycache__/domreg.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/dom/__pycache__/expatbuilder.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/dom/__pycache__/minicompat.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/dom/__pycache__/minidom.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/dom/__pycache__/pulldom.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/dom/__pycache__/xmlbuilder.cpython-311.opt-1.pyc", "windows/x64/Lib/xml/dom/domreg.py", "windows/x64/Lib/xml/dom/expatbuilder.py", "windows/x64/Lib/xml/dom/minicompat.py", @@ -6281,42 +6435,42 @@ "windows/x64/Lib/xml/etree/ElementPath.py", "windows/x64/Lib/xml/etree/ElementTree.py", "windows/x64/Lib/xml/etree/__init__.py", - "windows/x64/Lib/xml/etree/__pycache__/ElementInclude.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/etree/__pycache__/ElementPath.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/etree/__pycache__/ElementTree.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/etree/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/etree/__pycache__/cElementTree.cpython-310.opt-1.pyc", + "windows/x64/Lib/xml/etree/__pycache__/ElementInclude.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/etree/__pycache__/ElementPath.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/etree/__pycache__/ElementTree.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/etree/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/etree/__pycache__/cElementTree.cpython-311.opt-1.pyc", "windows/x64/Lib/xml/etree/cElementTree.py", "windows/x64/Lib/xml/parsers/__init__.py", - "windows/x64/Lib/xml/parsers/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/parsers/__pycache__/expat.cpython-310.opt-1.pyc", + "windows/x64/Lib/xml/parsers/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/parsers/__pycache__/expat.cpython-311.opt-1.pyc", "windows/x64/Lib/xml/parsers/expat.py", "windows/x64/Lib/xml/sax/__init__.py", - "windows/x64/Lib/xml/sax/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/sax/__pycache__/_exceptions.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/sax/__pycache__/expatreader.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/sax/__pycache__/handler.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/sax/__pycache__/saxutils.cpython-310.opt-1.pyc", - "windows/x64/Lib/xml/sax/__pycache__/xmlreader.cpython-310.opt-1.pyc", + "windows/x64/Lib/xml/sax/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/sax/__pycache__/_exceptions.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/sax/__pycache__/expatreader.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/sax/__pycache__/handler.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/sax/__pycache__/saxutils.cpython-311.opt-1.pyc", + "windows/x64/Lib/xml/sax/__pycache__/xmlreader.cpython-311.opt-1.pyc", "windows/x64/Lib/xml/sax/_exceptions.py", "windows/x64/Lib/xml/sax/expatreader.py", "windows/x64/Lib/xml/sax/handler.py", "windows/x64/Lib/xml/sax/saxutils.py", "windows/x64/Lib/xml/sax/xmlreader.py", "windows/x64/Lib/xmlrpc/__init__.py", - "windows/x64/Lib/xmlrpc/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/xmlrpc/__pycache__/client.cpython-310.opt-1.pyc", - "windows/x64/Lib/xmlrpc/__pycache__/server.cpython-310.opt-1.pyc", + "windows/x64/Lib/xmlrpc/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/xmlrpc/__pycache__/client.cpython-311.opt-1.pyc", + "windows/x64/Lib/xmlrpc/__pycache__/server.cpython-311.opt-1.pyc", "windows/x64/Lib/xmlrpc/client.py", "windows/x64/Lib/xmlrpc/server.py", "windows/x64/Lib/zipapp.py", "windows/x64/Lib/zipfile.py", "windows/x64/Lib/zipimport.py", "windows/x64/Lib/zoneinfo/__init__.py", - "windows/x64/Lib/zoneinfo/__pycache__/__init__.cpython-310.opt-1.pyc", - "windows/x64/Lib/zoneinfo/__pycache__/_common.cpython-310.opt-1.pyc", - "windows/x64/Lib/zoneinfo/__pycache__/_tzpath.cpython-310.opt-1.pyc", - "windows/x64/Lib/zoneinfo/__pycache__/_zoneinfo.cpython-310.opt-1.pyc", + "windows/x64/Lib/zoneinfo/__pycache__/__init__.cpython-311.opt-1.pyc", + "windows/x64/Lib/zoneinfo/__pycache__/_common.cpython-311.opt-1.pyc", + "windows/x64/Lib/zoneinfo/__pycache__/_tzpath.cpython-311.opt-1.pyc", + "windows/x64/Lib/zoneinfo/__pycache__/_zoneinfo.cpython-311.opt-1.pyc", "windows/x64/Lib/zoneinfo/_common.py", "windows/x64/Lib/zoneinfo/_tzpath.py", "windows/x64/Lib/zoneinfo/_zoneinfo.py", @@ -6327,8 +6481,8 @@ "windows/x64/msvcp140d.dll", "windows/x64/ogg.dll", "windows/x64/python.exe", - "windows/x64/python310.dll", - "windows/x64/python310_d.dll", + "windows/x64/python311.dll", + "windows/x64/python311_d.dll", "windows/x64/python_d.exe", "windows/x64/pythonw.exe", "windows/x64/pythonw_d.exe", @@ -6336,9 +6490,9 @@ "windows/x64/vc_redist.x64.exe", "windows/x64/vcruntime140_1d.dll", "windows/x64/vcruntime140d.dll", - "workspace/__pycache__/ninjafightplug.cpython-310.opt-1.pyc", - "workspace/__pycache__/onslaughtplug.cpython-310.opt-1.pyc", - "workspace/__pycache__/runaroundplug.cpython-310.opt-1.pyc", + "workspace/__pycache__/ninjafightplug.cpython-311.opt-1.pyc", + "workspace/__pycache__/onslaughtplug.cpython-311.opt-1.pyc", + "workspace/__pycache__/runaroundplug.cpython-311.opt-1.pyc", "workspace/ninjafightplug.py", "workspace/onslaughtplug.py", "workspace/runaroundplug.py" diff --git a/src/assets/.asset_manifest_public.json b/src/assets/.asset_manifest_public.json new file mode 100644 index 00000000..f630e343 --- /dev/null +++ b/src/assets/.asset_manifest_public.json @@ -0,0 +1,594 @@ +[ + "ba_data/python/__pycache__/baenv.cpython-311.opt-1.pyc", + "ba_data/python/babase/__init__.py", + "ba_data/python/babase/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_accountv2.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_app.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_appcomponent.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_appconfig.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_apputils.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_assetmanager.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_asyncio.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_cloud.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_error.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_general.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_hooks.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_keyboard.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_language.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_login.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_math.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_meta.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_net.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_plugin.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_text.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_workspace.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/internal.cpython-311.opt-1.pyc", + "ba_data/python/babase/_accountv2.py", + "ba_data/python/babase/_app.py", + "ba_data/python/babase/_appcomponent.py", + "ba_data/python/babase/_appconfig.py", + "ba_data/python/babase/_apputils.py", + "ba_data/python/babase/_assetmanager.py", + "ba_data/python/babase/_asyncio.py", + "ba_data/python/babase/_cloud.py", + "ba_data/python/babase/_error.py", + "ba_data/python/babase/_general.py", + "ba_data/python/babase/_hooks.py", + "ba_data/python/babase/_keyboard.py", + "ba_data/python/babase/_language.py", + "ba_data/python/babase/_login.py", + "ba_data/python/babase/_math.py", + "ba_data/python/babase/_meta.py", + "ba_data/python/babase/_mgen/__init__.py", + "ba_data/python/babase/_mgen/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/babase/_mgen/__pycache__/enums.cpython-311.opt-1.pyc", + "ba_data/python/babase/_mgen/enums.py", + "ba_data/python/babase/_net.py", + "ba_data/python/babase/_plugin.py", + "ba_data/python/babase/_text.py", + "ba_data/python/babase/_workspace.py", + "ba_data/python/babase/internal.py", + "ba_data/python/baclassic/__init__.py", + "ba_data/python/baclassic/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_accountv1.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_achievement.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_ads.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_analytics.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_appdelegate.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_benchmark.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_campaign.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_input.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_level.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_lobby.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_music.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_net.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_profile.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_servermode.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_store.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_subsystem.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_tips.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_tournament.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/_ui.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/macmusicapp.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/__pycache__/osmusic.cpython-311.opt-1.pyc", + "ba_data/python/baclassic/_accountv1.py", + "ba_data/python/baclassic/_achievement.py", + "ba_data/python/baclassic/_ads.py", + "ba_data/python/baclassic/_analytics.py", + "ba_data/python/baclassic/_appdelegate.py", + "ba_data/python/baclassic/_benchmark.py", + "ba_data/python/baclassic/_campaign.py", + "ba_data/python/baclassic/_input.py", + "ba_data/python/baclassic/_level.py", + "ba_data/python/baclassic/_lobby.py", + "ba_data/python/baclassic/_music.py", + "ba_data/python/baclassic/_net.py", + "ba_data/python/baclassic/_profile.py", + "ba_data/python/baclassic/_servermode.py", + "ba_data/python/baclassic/_store.py", + "ba_data/python/baclassic/_subsystem.py", + "ba_data/python/baclassic/_tips.py", + "ba_data/python/baclassic/_tournament.py", + "ba_data/python/baclassic/_ui.py", + "ba_data/python/baclassic/macmusicapp.py", + "ba_data/python/baclassic/osmusic.py", + "ba_data/python/bacommon/__init__.py", + "ba_data/python/bacommon/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bacommon/__pycache__/assets.cpython-311.opt-1.pyc", + "ba_data/python/bacommon/__pycache__/bacloud.cpython-311.opt-1.pyc", + "ba_data/python/bacommon/__pycache__/build.cpython-311.opt-1.pyc", + "ba_data/python/bacommon/__pycache__/cloud.cpython-311.opt-1.pyc", + "ba_data/python/bacommon/__pycache__/login.cpython-311.opt-1.pyc", + "ba_data/python/bacommon/__pycache__/net.cpython-311.opt-1.pyc", + "ba_data/python/bacommon/__pycache__/servermanager.cpython-311.opt-1.pyc", + "ba_data/python/bacommon/__pycache__/transfer.cpython-311.opt-1.pyc", + "ba_data/python/bacommon/assets.py", + "ba_data/python/bacommon/bacloud.py", + "ba_data/python/bacommon/build.py", + "ba_data/python/bacommon/cloud.py", + "ba_data/python/bacommon/login.py", + "ba_data/python/bacommon/net.py", + "ba_data/python/bacommon/servermanager.py", + "ba_data/python/bacommon/transfer.py", + "ba_data/python/baenv.py", + "ba_data/python/baplus/__init__.py", + "ba_data/python/baplus/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/baplus/__pycache__/_hooks.cpython-311.opt-1.pyc", + "ba_data/python/baplus/__pycache__/_subsystem.cpython-311.opt-1.pyc", + "ba_data/python/baplus/_hooks.py", + "ba_data/python/baplus/_subsystem.py", + "ba_data/python/bascenev1/__init__.py", + "ba_data/python/bascenev1/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_activity.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_activitytypes.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_actor.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_collision.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_coopgame.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_coopsession.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_debug.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_dependency.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_dualteamsession.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_featureset.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_freeforallsession.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_gameactivity.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_gameresults.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_gameutils.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_hooks.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_map.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_messages.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_multiteamsession.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_music.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_nodeactor.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_player.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_playlist.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_powerup.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_score.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_session.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_settings.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_stats.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_team.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/_teamgame.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/__pycache__/internal.cpython-311.opt-1.pyc", + "ba_data/python/bascenev1/_activity.py", + "ba_data/python/bascenev1/_activitytypes.py", + "ba_data/python/bascenev1/_actor.py", + "ba_data/python/bascenev1/_collision.py", + "ba_data/python/bascenev1/_coopgame.py", + "ba_data/python/bascenev1/_coopsession.py", + "ba_data/python/bascenev1/_debug.py", + "ba_data/python/bascenev1/_dependency.py", + "ba_data/python/bascenev1/_dualteamsession.py", + "ba_data/python/bascenev1/_featureset.py", + "ba_data/python/bascenev1/_freeforallsession.py", + "ba_data/python/bascenev1/_gameactivity.py", + "ba_data/python/bascenev1/_gameresults.py", + "ba_data/python/bascenev1/_gameutils.py", + "ba_data/python/bascenev1/_hooks.py", + "ba_data/python/bascenev1/_map.py", + "ba_data/python/bascenev1/_messages.py", + "ba_data/python/bascenev1/_multiteamsession.py", + "ba_data/python/bascenev1/_music.py", + "ba_data/python/bascenev1/_nodeactor.py", + "ba_data/python/bascenev1/_player.py", + "ba_data/python/bascenev1/_playlist.py", + "ba_data/python/bascenev1/_powerup.py", + "ba_data/python/bascenev1/_score.py", + "ba_data/python/bascenev1/_session.py", + "ba_data/python/bascenev1/_settings.py", + "ba_data/python/bascenev1/_stats.py", + "ba_data/python/bascenev1/_team.py", + "ba_data/python/bascenev1/_teamgame.py", + "ba_data/python/bascenev1/internal.py", + "ba_data/python/bastd/__init__.py", + "ba_data/python/bastd/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/__pycache__/gameutils.cpython-311.opt-1.pyc", + "ba_data/python/bastd/__pycache__/mainmenu.cpython-311.opt-1.pyc", + "ba_data/python/bastd/__pycache__/maps.cpython-311.opt-1.pyc", + "ba_data/python/bastd/__pycache__/tutorial.cpython-311.opt-1.pyc", + "ba_data/python/bastd/activity/__init__.py", + "ba_data/python/bastd/activity/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/activity/__pycache__/coopjoin.cpython-311.opt-1.pyc", + "ba_data/python/bastd/activity/__pycache__/coopscore.cpython-311.opt-1.pyc", + "ba_data/python/bastd/activity/__pycache__/drawscore.cpython-311.opt-1.pyc", + "ba_data/python/bastd/activity/__pycache__/dualteamscore.cpython-311.opt-1.pyc", + "ba_data/python/bastd/activity/__pycache__/freeforallvictory.cpython-311.opt-1.pyc", + "ba_data/python/bastd/activity/__pycache__/multiteamjoin.cpython-311.opt-1.pyc", + "ba_data/python/bastd/activity/__pycache__/multiteamscore.cpython-311.opt-1.pyc", + "ba_data/python/bastd/activity/__pycache__/multiteamvictory.cpython-311.opt-1.pyc", + "ba_data/python/bastd/activity/coopjoin.py", + "ba_data/python/bastd/activity/coopscore.py", + "ba_data/python/bastd/activity/drawscore.py", + "ba_data/python/bastd/activity/dualteamscore.py", + "ba_data/python/bastd/activity/freeforallvictory.py", + "ba_data/python/bastd/activity/multiteamjoin.py", + "ba_data/python/bastd/activity/multiteamscore.py", + "ba_data/python/bastd/activity/multiteamvictory.py", + "ba_data/python/bastd/actor/__init__.py", + "ba_data/python/bastd/actor/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/background.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/bomb.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/controlsguide.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/flag.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/image.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/onscreencountdown.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/onscreentimer.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/playerspaz.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/popuptext.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/powerupbox.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/respawnicon.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/scoreboard.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/spawner.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/spaz.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/spazappearance.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/spazbot.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/spazfactory.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/text.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/tipstext.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/__pycache__/zoomtext.cpython-311.opt-1.pyc", + "ba_data/python/bastd/actor/background.py", + "ba_data/python/bastd/actor/bomb.py", + "ba_data/python/bastd/actor/controlsguide.py", + "ba_data/python/bastd/actor/flag.py", + "ba_data/python/bastd/actor/image.py", + "ba_data/python/bastd/actor/onscreencountdown.py", + "ba_data/python/bastd/actor/onscreentimer.py", + "ba_data/python/bastd/actor/playerspaz.py", + "ba_data/python/bastd/actor/popuptext.py", + "ba_data/python/bastd/actor/powerupbox.py", + "ba_data/python/bastd/actor/respawnicon.py", + "ba_data/python/bastd/actor/scoreboard.py", + "ba_data/python/bastd/actor/spawner.py", + "ba_data/python/bastd/actor/spaz.py", + "ba_data/python/bastd/actor/spazappearance.py", + "ba_data/python/bastd/actor/spazbot.py", + "ba_data/python/bastd/actor/spazfactory.py", + "ba_data/python/bastd/actor/text.py", + "ba_data/python/bastd/actor/tipstext.py", + "ba_data/python/bastd/actor/zoomtext.py", + "ba_data/python/bastd/game/__init__.py", + "ba_data/python/bastd/game/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/assault.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/capturetheflag.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/chosenone.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/conquest.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/deathmatch.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/easteregghunt.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/elimination.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/football.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/hockey.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/keepaway.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/kingofthehill.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/meteorshower.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/ninjafight.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/onslaught.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/race.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/runaround.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/targetpractice.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/__pycache__/thelaststand.cpython-311.opt-1.pyc", + "ba_data/python/bastd/game/assault.py", + "ba_data/python/bastd/game/capturetheflag.py", + "ba_data/python/bastd/game/chosenone.py", + "ba_data/python/bastd/game/conquest.py", + "ba_data/python/bastd/game/deathmatch.py", + "ba_data/python/bastd/game/easteregghunt.py", + "ba_data/python/bastd/game/elimination.py", + "ba_data/python/bastd/game/football.py", + "ba_data/python/bastd/game/hockey.py", + "ba_data/python/bastd/game/keepaway.py", + "ba_data/python/bastd/game/kingofthehill.py", + "ba_data/python/bastd/game/meteorshower.py", + "ba_data/python/bastd/game/ninjafight.py", + "ba_data/python/bastd/game/onslaught.py", + "ba_data/python/bastd/game/race.py", + "ba_data/python/bastd/game/runaround.py", + "ba_data/python/bastd/game/targetpractice.py", + "ba_data/python/bastd/game/thelaststand.py", + "ba_data/python/bastd/gameutils.py", + "ba_data/python/bastd/keyboard/__init__.py", + "ba_data/python/bastd/keyboard/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/keyboard/__pycache__/englishkeyboard.cpython-311.opt-1.pyc", + "ba_data/python/bastd/keyboard/englishkeyboard.py", + "ba_data/python/bastd/mainmenu.py", + "ba_data/python/bastd/mapdata/__init__.py", + "ba_data/python/bastd/mapdata/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/big_g.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/bridgit.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/courtyard.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/crag_castle.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/doom_shroom.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/football_stadium.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/happy_thoughts.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/hockey_stadium.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/lake_frigid.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/monkey_face.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/rampage.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/roundabout.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/step_right_up.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/the_pad.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/tip_top.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/tower_d.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/__pycache__/zig_zag.cpython-311.opt-1.pyc", + "ba_data/python/bastd/mapdata/big_g.py", + "ba_data/python/bastd/mapdata/bridgit.py", + "ba_data/python/bastd/mapdata/courtyard.py", + "ba_data/python/bastd/mapdata/crag_castle.py", + "ba_data/python/bastd/mapdata/doom_shroom.py", + "ba_data/python/bastd/mapdata/football_stadium.py", + "ba_data/python/bastd/mapdata/happy_thoughts.py", + "ba_data/python/bastd/mapdata/hockey_stadium.py", + "ba_data/python/bastd/mapdata/lake_frigid.py", + "ba_data/python/bastd/mapdata/monkey_face.py", + "ba_data/python/bastd/mapdata/rampage.py", + "ba_data/python/bastd/mapdata/roundabout.py", + "ba_data/python/bastd/mapdata/step_right_up.py", + "ba_data/python/bastd/mapdata/the_pad.py", + "ba_data/python/bastd/mapdata/tip_top.py", + "ba_data/python/bastd/mapdata/tower_d.py", + "ba_data/python/bastd/mapdata/zig_zag.py", + "ba_data/python/bastd/maps.py", + "ba_data/python/bastd/session/__init__.py", + "ba_data/python/bastd/session/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/tutorial.py", + "ba_data/python/bastd/ui/__init__.py", + "ba_data/python/bastd/ui/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/achievements.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/appinvite.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/characterpicker.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/colorpicker.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/config.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/configerror.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/confirm.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/continues.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/creditslist.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/debug.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/feedback.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/fileselector.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/getcurrency.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/getremote.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/helpui.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/iconpicker.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/kiosk.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/mainmenu.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/party.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/partyqueue.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/play.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/playoptions.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/popup.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/promocode.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/purchase.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/qrcode.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/radiogroup.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/report.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/resourcetypeinfo.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/serverdialog.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/specialoffer.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/tabs.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/teamnamescolors.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/tournamententry.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/tournamentscores.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/trophies.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/url.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/v2upgrade.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/__pycache__/watch.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/account/__init__.py", + "ba_data/python/bastd/ui/account/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/account/__pycache__/link.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/account/__pycache__/settings.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/account/__pycache__/unlink.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/account/__pycache__/v2proxy.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/account/__pycache__/viewer.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/account/link.py", + "ba_data/python/bastd/ui/account/settings.py", + "ba_data/python/bastd/ui/account/unlink.py", + "ba_data/python/bastd/ui/account/v2proxy.py", + "ba_data/python/bastd/ui/account/viewer.py", + "ba_data/python/bastd/ui/achievements.py", + "ba_data/python/bastd/ui/appinvite.py", + "ba_data/python/bastd/ui/characterpicker.py", + "ba_data/python/bastd/ui/colorpicker.py", + "ba_data/python/bastd/ui/config.py", + "ba_data/python/bastd/ui/configerror.py", + "ba_data/python/bastd/ui/confirm.py", + "ba_data/python/bastd/ui/continues.py", + "ba_data/python/bastd/ui/coop/__init__.py", + "ba_data/python/bastd/ui/coop/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/coop/__pycache__/browser.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/coop/__pycache__/gamebutton.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/coop/__pycache__/level.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/coop/__pycache__/tournamentbutton.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/coop/browser.py", + "ba_data/python/bastd/ui/coop/gamebutton.py", + "ba_data/python/bastd/ui/coop/level.py", + "ba_data/python/bastd/ui/coop/tournamentbutton.py", + "ba_data/python/bastd/ui/creditslist.py", + "ba_data/python/bastd/ui/debug.py", + "ba_data/python/bastd/ui/feedback.py", + "ba_data/python/bastd/ui/fileselector.py", + "ba_data/python/bastd/ui/gather/__init__.py", + "ba_data/python/bastd/ui/gather/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/gather/__pycache__/abouttab.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/gather/__pycache__/manualtab.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/gather/__pycache__/nearbytab.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/gather/__pycache__/privatetab.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/gather/__pycache__/publictab.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/gather/abouttab.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", + "ba_data/python/bastd/ui/helpui.py", + "ba_data/python/bastd/ui/iconpicker.py", + "ba_data/python/bastd/ui/kiosk.py", + "ba_data/python/bastd/ui/league/__init__.py", + "ba_data/python/bastd/ui/league/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/league/__pycache__/rankbutton.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/league/__pycache__/rankwindow.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/league/rankbutton.py", + "ba_data/python/bastd/ui/league/rankwindow.py", + "ba_data/python/bastd/ui/mainmenu.py", + "ba_data/python/bastd/ui/party.py", + "ba_data/python/bastd/ui/partyqueue.py", + "ba_data/python/bastd/ui/play.py", + "ba_data/python/bastd/ui/playlist/__init__.py", + "ba_data/python/bastd/ui/playlist/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/playlist/__pycache__/addgame.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/playlist/__pycache__/browser.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/playlist/__pycache__/customizebrowser.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/playlist/__pycache__/edit.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/playlist/__pycache__/editcontroller.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/playlist/__pycache__/editgame.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/playlist/__pycache__/mapselect.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/playlist/__pycache__/share.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/playlist/addgame.py", + "ba_data/python/bastd/ui/playlist/browser.py", + "ba_data/python/bastd/ui/playlist/customizebrowser.py", + "ba_data/python/bastd/ui/playlist/edit.py", + "ba_data/python/bastd/ui/playlist/editcontroller.py", + "ba_data/python/bastd/ui/playlist/editgame.py", + "ba_data/python/bastd/ui/playlist/mapselect.py", + "ba_data/python/bastd/ui/playlist/share.py", + "ba_data/python/bastd/ui/playoptions.py", + "ba_data/python/bastd/ui/popup.py", + "ba_data/python/bastd/ui/profile/__init__.py", + "ba_data/python/bastd/ui/profile/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/profile/__pycache__/browser.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/profile/__pycache__/edit.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/profile/__pycache__/upgrade.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/profile/browser.py", + "ba_data/python/bastd/ui/profile/edit.py", + "ba_data/python/bastd/ui/profile/upgrade.py", + "ba_data/python/bastd/ui/promocode.py", + "ba_data/python/bastd/ui/purchase.py", + "ba_data/python/bastd/ui/qrcode.py", + "ba_data/python/bastd/ui/radiogroup.py", + "ba_data/python/bastd/ui/report.py", + "ba_data/python/bastd/ui/resourcetypeinfo.py", + "ba_data/python/bastd/ui/serverdialog.py", + "ba_data/python/bastd/ui/settings/__init__.py", + "ba_data/python/bastd/ui/settings/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/advanced.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/allsettings.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/audio.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/controls.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/gamepad.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/gamepadadvanced.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/gamepadselect.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/graphics.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/keyboard.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/nettesting.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/plugins.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/pluginsettings.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/remoteapp.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/testing.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/touchscreen.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/__pycache__/vrtesting.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/settings/advanced.py", + "ba_data/python/bastd/ui/settings/allsettings.py", + "ba_data/python/bastd/ui/settings/audio.py", + "ba_data/python/bastd/ui/settings/controls.py", + "ba_data/python/bastd/ui/settings/gamepad.py", + "ba_data/python/bastd/ui/settings/gamepadadvanced.py", + "ba_data/python/bastd/ui/settings/gamepadselect.py", + "ba_data/python/bastd/ui/settings/graphics.py", + "ba_data/python/bastd/ui/settings/keyboard.py", + "ba_data/python/bastd/ui/settings/nettesting.py", + "ba_data/python/bastd/ui/settings/plugins.py", + "ba_data/python/bastd/ui/settings/pluginsettings.py", + "ba_data/python/bastd/ui/settings/remoteapp.py", + "ba_data/python/bastd/ui/settings/testing.py", + "ba_data/python/bastd/ui/settings/touchscreen.py", + "ba_data/python/bastd/ui/settings/vrtesting.py", + "ba_data/python/bastd/ui/soundtrack/__init__.py", + "ba_data/python/bastd/ui/soundtrack/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/soundtrack/__pycache__/browser.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/soundtrack/__pycache__/edit.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/soundtrack/__pycache__/entrytypeselect.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/soundtrack/__pycache__/macmusicapp.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/soundtrack/browser.py", + "ba_data/python/bastd/ui/soundtrack/edit.py", + "ba_data/python/bastd/ui/soundtrack/entrytypeselect.py", + "ba_data/python/bastd/ui/soundtrack/macmusicapp.py", + "ba_data/python/bastd/ui/specialoffer.py", + "ba_data/python/bastd/ui/store/__init__.py", + "ba_data/python/bastd/ui/store/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/store/__pycache__/browser.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/store/__pycache__/button.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/store/__pycache__/item.cpython-311.opt-1.pyc", + "ba_data/python/bastd/ui/store/browser.py", + "ba_data/python/bastd/ui/store/button.py", + "ba_data/python/bastd/ui/store/item.py", + "ba_data/python/bastd/ui/tabs.py", + "ba_data/python/bastd/ui/teamnamescolors.py", + "ba_data/python/bastd/ui/tournamententry.py", + "ba_data/python/bastd/ui/tournamentscores.py", + "ba_data/python/bastd/ui/trophies.py", + "ba_data/python/bastd/ui/url.py", + "ba_data/python/bastd/ui/v2upgrade.py", + "ba_data/python/bastd/ui/watch.py", + "ba_data/python/batemplatefs/__init__.py", + "ba_data/python/batemplatefs/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/batemplatefs/__pycache__/_hooks.cpython-311.opt-1.pyc", + "ba_data/python/batemplatefs/__pycache__/_subsystem.cpython-311.opt-1.pyc", + "ba_data/python/batemplatefs/_hooks.py", + "ba_data/python/batemplatefs/_subsystem.py", + "ba_data/python/bauiv1/__init__.py", + "ba_data/python/bauiv1/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/bauiv1/__pycache__/_hooks.cpython-311.opt-1.pyc", + "ba_data/python/bauiv1/__pycache__/modutils.cpython-311.opt-1.pyc", + "ba_data/python/bauiv1/__pycache__/onscreenkeyboard.cpython-311.opt-1.pyc", + "ba_data/python/bauiv1/_hooks.py", + "ba_data/python/bauiv1/modutils.py", + "ba_data/python/bauiv1/onscreenkeyboard.py", + "ba_data/python/bauiv1/ui/__init__.py", + "ba_data/python/bauiv1/ui/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/efro/__init__.py", + "ba_data/python/efro/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/efro/__pycache__/call.cpython-311.opt-1.pyc", + "ba_data/python/efro/__pycache__/cloudshell.cpython-311.opt-1.pyc", + "ba_data/python/efro/__pycache__/debug.cpython-311.opt-1.pyc", + "ba_data/python/efro/__pycache__/error.cpython-311.opt-1.pyc", + "ba_data/python/efro/__pycache__/log.cpython-311.opt-1.pyc", + "ba_data/python/efro/__pycache__/rpc.cpython-311.opt-1.pyc", + "ba_data/python/efro/__pycache__/terminal.cpython-311.opt-1.pyc", + "ba_data/python/efro/__pycache__/util.cpython-311.opt-1.pyc", + "ba_data/python/efro/call.py", + "ba_data/python/efro/cloudshell.py", + "ba_data/python/efro/dataclassio/__init__.py", + "ba_data/python/efro/dataclassio/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/efro/dataclassio/__pycache__/_api.cpython-311.opt-1.pyc", + "ba_data/python/efro/dataclassio/__pycache__/_base.cpython-311.opt-1.pyc", + "ba_data/python/efro/dataclassio/__pycache__/_inputter.cpython-311.opt-1.pyc", + "ba_data/python/efro/dataclassio/__pycache__/_outputter.cpython-311.opt-1.pyc", + "ba_data/python/efro/dataclassio/__pycache__/_pathcapture.cpython-311.opt-1.pyc", + "ba_data/python/efro/dataclassio/__pycache__/_prep.cpython-311.opt-1.pyc", + "ba_data/python/efro/dataclassio/__pycache__/extras.cpython-311.opt-1.pyc", + "ba_data/python/efro/dataclassio/_api.py", + "ba_data/python/efro/dataclassio/_base.py", + "ba_data/python/efro/dataclassio/_inputter.py", + "ba_data/python/efro/dataclassio/_outputter.py", + "ba_data/python/efro/dataclassio/_pathcapture.py", + "ba_data/python/efro/dataclassio/_prep.py", + "ba_data/python/efro/dataclassio/extras.py", + "ba_data/python/efro/debug.py", + "ba_data/python/efro/error.py", + "ba_data/python/efro/log.py", + "ba_data/python/efro/message/__init__.py", + "ba_data/python/efro/message/__pycache__/__init__.cpython-311.opt-1.pyc", + "ba_data/python/efro/message/__pycache__/_message.cpython-311.opt-1.pyc", + "ba_data/python/efro/message/__pycache__/_module.cpython-311.opt-1.pyc", + "ba_data/python/efro/message/__pycache__/_protocol.cpython-311.opt-1.pyc", + "ba_data/python/efro/message/__pycache__/_receiver.cpython-311.opt-1.pyc", + "ba_data/python/efro/message/__pycache__/_sender.cpython-311.opt-1.pyc", + "ba_data/python/efro/message/_message.py", + "ba_data/python/efro/message/_module.py", + "ba_data/python/efro/message/_protocol.py", + "ba_data/python/efro/message/_receiver.py", + "ba_data/python/efro/message/_sender.py", + "ba_data/python/efro/rpc.py", + "ba_data/python/efro/terminal.py", + "ba_data/python/efro/util.py", + "server/__pycache__/ballisticakit_server.cpython-311.opt-1.pyc", + "server/ballisticakit_server.py" +] \ No newline at end of file diff --git a/src/assets/Makefile b/src/assets/Makefile new file mode 100644 index 00000000..09dcc176 --- /dev/null +++ b/src/assets/Makefile @@ -0,0 +1,7505 @@ +# Released under the MIT License. See LICENSE for details. +# +################################################################################ +# # +# Asset Generation # +# # +################################################################################ + +# Generally this Makefile should not be modified by hand. To add or remove +# assets, add or remove files under the src/assets directory and then run +# "make update" from the project root. That will update this file as well as +# other relevant files such as assets/manifest.json + +# Generally you shouldn't need to use this Makefile directly. Targets in the +# root level Makefile should handle asset building for you. If you do use +# this Makefile directly for whatever reason, remember to pass -jX on the +# command line to speed up your builds. (with X being the number of procs +# in your machine) + +PROJ_DIR = ../.. +TOOLS_DIR = $(PROJ_DIR)/tools +BUILD_DIR = $(PROJ_DIR)/build/assets + +# High level targets: generally these are what should be used here. + +# Build everything needed for all platforms. +all: + @$(TOOLS_DIR)/pcommand warm_start_asset_build + @$(MAKE) assets + @$(TOOLS_DIR)/pcommand clean_orphaned_assets + +# Build everything needed for our cmake builds (linux, mac). +cmake: + @$(TOOLS_DIR)/pcommand warm_start_asset_build + @$(MAKE) assets-cmake + @$(TOOLS_DIR)/pcommand clean_orphaned_assets + +# Build everything needed for x86 windows builds. +win-Win32: + @$(TOOLS_DIR)/pcommand warm_start_asset_build + @$(MAKE) assets-win-Win32 + @$(TOOLS_DIR)/pcommand clean_orphaned_assets + +# Build everything needed for x86-64 windows builds. +win-x64: + @$(TOOLS_DIR)/pcommand warm_start_asset_build + @$(MAKE) assets-win-x64 + @$(TOOLS_DIR)/pcommand clean_orphaned_assets + +# Build everything needed for our mac xcode builds. +mac: + @$(TOOLS_DIR)/pcommand warm_start_asset_build + @$(MAKE) assets-mac + @$(TOOLS_DIR)/pcommand clean_orphaned_assets + +# Build everything needed for our ios/tvos builds. +ios: + @$(TOOLS_DIR)/pcommand warm_start_asset_build + @$(MAKE) assets-ios + @$(TOOLS_DIR)/pcommand clean_orphaned_assets + +# Build everything needed for android. +android: + @$(TOOLS_DIR)/pcommand warm_start_asset_build + @$(MAKE) assets-android + @$(TOOLS_DIR)/pcommand clean_orphaned_assets + +MAKE_AUDIO = 1 +MAKE_TEXTURES = 1 +MAKE_SCRIPTS = 1 +MAKE_FONTS = 1 +MAKE_DATA = 1 + +ASSET_TARGETS_COMMON = +ASSET_TARGETS_CMAKE = +ASSET_TARGETS_MAC = +ASSET_TARGETS_WIN_WIN32 = +ASSET_TARGETS_WIN_X64 = +ASSET_TARGETS_IOS = +ASSET_TARGETS_ANDROID = + +# Audio. +ifeq ($(MAKE_AUDIO),1) +ASSET_TARGETS_COMMON += $(AUDIO_TARGETS) +endif + +# FontData. +ifeq ($(MAKE_FONTS),1) +ASSET_TARGETS_COMMON += $(FONT_TARGETS) +endif + +# Data. +ifeq ($(MAKE_DATA),1) +ASSET_TARGETS_COMMON += $(DATA_TARGETS) +endif + +# Textures. +ifeq ($(MAKE_TEXTURES),1) + +ASSET_TARGETS_CMAKE += $(TEXTURE_TARGETS_CMAKE) +ASSET_TARGETS_MAC += $(TEXTURE_TARGETS_MAC) +ASSET_TARGETS_WIN_WIN32 += $(TEXTURE_TARGETS_WIN) +ASSET_TARGETS_WIN_X64 += $(TEXTURE_TARGETS_WIN) +ASSET_TARGETS_IOS += $(TEXTURE_TARGETS_IOS) +ASSET_TARGETS_ANDROID += $(TEXTURE_TARGETS_ANDROID) +ASSET_TARGETS_COMMON += $(TEXTURE_TARGETS_COMMON) + +endif # Textures + +# Scripts +ifeq ($(MAKE_SCRIPTS),1) +ASSET_TARGETS_CMAKE += $(SCRIPT_TARGETS_CMAKE) +ASSET_TARGETS_MAC += $(SCRIPT_TARGETS_MAC) +ASSET_TARGETS_WIN_WIN32 += $(SCRIPT_TARGETS_WIN_WIN32) +ASSET_TARGETS_WIN_X64 += $(SCRIPT_TARGETS_WIN_X64) +ASSET_TARGETS_IOS += $(SCRIPT_TARGETS_IOS) +ASSET_TARGETS_ANDROID += $(SCRIPT_TARGETS_ANDROID) +ASSET_TARGETS_COMMON += $(SCRIPT_TARGETS_COMMON) +endif + +# Extras +ASSET_TARGETS_WIN_WIN32 += $(EXTRAS_TARGETS_WIN_WIN32) +ASSET_TARGETS_WIN_X64 += $(EXTRAS_TARGETS_WIN_X64) + +# Note: Code below needs updating when Python version changes (currently 3.11) +define make-opt-pyc-target +$1: $$(subst /__pycache__,,$$(subst .cpython-311.opt-1.pyc,.py,$1)) + @echo Compiling script: $$(subst $(BUILD_DIR)/,,$$^) + @rm -rf $$@ && PYTHONHASHSEED=1 \ + $$(TOOLS_DIR)/pcommand compile_python_files $$^ && chmod 444 $$@ +endef + +# This section is generated by batools.assetsmakefile; do not edit by hand. +# __AUTOGENERATED_PUBLIC_BEGIN__ + +SCRIPT_TARGETS_PY_PUBLIC = \ + $(BUILD_DIR)/ba_data/python/babase/__init__.py \ + $(BUILD_DIR)/ba_data/python/babase/_accountv2.py \ + $(BUILD_DIR)/ba_data/python/babase/_app.py \ + $(BUILD_DIR)/ba_data/python/babase/_appcomponent.py \ + $(BUILD_DIR)/ba_data/python/babase/_appconfig.py \ + $(BUILD_DIR)/ba_data/python/babase/_apputils.py \ + $(BUILD_DIR)/ba_data/python/babase/_assetmanager.py \ + $(BUILD_DIR)/ba_data/python/babase/_asyncio.py \ + $(BUILD_DIR)/ba_data/python/babase/_cloud.py \ + $(BUILD_DIR)/ba_data/python/babase/_error.py \ + $(BUILD_DIR)/ba_data/python/babase/_general.py \ + $(BUILD_DIR)/ba_data/python/babase/_hooks.py \ + $(BUILD_DIR)/ba_data/python/babase/_keyboard.py \ + $(BUILD_DIR)/ba_data/python/babase/_language.py \ + $(BUILD_DIR)/ba_data/python/babase/_login.py \ + $(BUILD_DIR)/ba_data/python/babase/_math.py \ + $(BUILD_DIR)/ba_data/python/babase/_meta.py \ + $(BUILD_DIR)/ba_data/python/babase/_mgen/__init__.py \ + $(BUILD_DIR)/ba_data/python/babase/_mgen/enums.py \ + $(BUILD_DIR)/ba_data/python/babase/_net.py \ + $(BUILD_DIR)/ba_data/python/babase/_plugin.py \ + $(BUILD_DIR)/ba_data/python/babase/_text.py \ + $(BUILD_DIR)/ba_data/python/babase/_workspace.py \ + $(BUILD_DIR)/ba_data/python/babase/internal.py \ + $(BUILD_DIR)/ba_data/python/baclassic/__init__.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_accountv1.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_achievement.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_ads.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_analytics.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_appdelegate.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_benchmark.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_campaign.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_input.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_level.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_lobby.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_music.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_net.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_profile.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_servermode.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_store.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_subsystem.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_tips.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_tournament.py \ + $(BUILD_DIR)/ba_data/python/baclassic/_ui.py \ + $(BUILD_DIR)/ba_data/python/baclassic/macmusicapp.py \ + $(BUILD_DIR)/ba_data/python/baclassic/osmusic.py \ + $(BUILD_DIR)/ba_data/python/baenv.py \ + $(BUILD_DIR)/ba_data/python/baplus/__init__.py \ + $(BUILD_DIR)/ba_data/python/baplus/_hooks.py \ + $(BUILD_DIR)/ba_data/python/baplus/_subsystem.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/__init__.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_activity.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_activitytypes.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_actor.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_collision.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_coopgame.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_coopsession.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_debug.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_dependency.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_dualteamsession.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_featureset.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_freeforallsession.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_gameactivity.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_gameresults.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_gameutils.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_hooks.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_map.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_messages.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_multiteamsession.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_music.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_nodeactor.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_player.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_playlist.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_powerup.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_score.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_session.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_settings.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_stats.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_team.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/_teamgame.py \ + $(BUILD_DIR)/ba_data/python/bascenev1/internal.py \ + $(BUILD_DIR)/ba_data/python/bastd/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/activity/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/activity/coopjoin.py \ + $(BUILD_DIR)/ba_data/python/bastd/activity/coopscore.py \ + $(BUILD_DIR)/ba_data/python/bastd/activity/drawscore.py \ + $(BUILD_DIR)/ba_data/python/bastd/activity/dualteamscore.py \ + $(BUILD_DIR)/ba_data/python/bastd/activity/freeforallvictory.py \ + $(BUILD_DIR)/ba_data/python/bastd/activity/multiteamjoin.py \ + $(BUILD_DIR)/ba_data/python/bastd/activity/multiteamscore.py \ + $(BUILD_DIR)/ba_data/python/bastd/activity/multiteamvictory.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/background.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/bomb.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/controlsguide.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/flag.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/image.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/onscreencountdown.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/onscreentimer.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/playerspaz.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/popuptext.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/powerupbox.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/respawnicon.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/scoreboard.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/spawner.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/spaz.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/spazappearance.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/spazbot.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/spazfactory.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/text.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/tipstext.py \ + $(BUILD_DIR)/ba_data/python/bastd/actor/zoomtext.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/assault.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/capturetheflag.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/chosenone.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/conquest.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/deathmatch.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/easteregghunt.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/elimination.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/football.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/hockey.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/keepaway.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/kingofthehill.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/meteorshower.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/ninjafight.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/onslaught.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/race.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/runaround.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/targetpractice.py \ + $(BUILD_DIR)/ba_data/python/bastd/game/thelaststand.py \ + $(BUILD_DIR)/ba_data/python/bastd/gameutils.py \ + $(BUILD_DIR)/ba_data/python/bastd/keyboard/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/keyboard/englishkeyboard.py \ + $(BUILD_DIR)/ba_data/python/bastd/mainmenu.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/big_g.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/bridgit.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/courtyard.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/crag_castle.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/doom_shroom.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/football_stadium.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/happy_thoughts.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/hockey_stadium.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/lake_frigid.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/monkey_face.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/rampage.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/roundabout.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/step_right_up.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/the_pad.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/tip_top.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/tower_d.py \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/zig_zag.py \ + $(BUILD_DIR)/ba_data/python/bastd/maps.py \ + $(BUILD_DIR)/ba_data/python/bastd/session/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/tutorial.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/account/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/account/link.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/account/settings.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/account/unlink.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/account/v2proxy.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/account/viewer.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/achievements.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/appinvite.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/characterpicker.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/colorpicker.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/config.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/configerror.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/confirm.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/continues.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/coop/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/coop/browser.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/coop/gamebutton.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/coop/level.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/coop/tournamentbutton.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/creditslist.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/debug.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/feedback.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/fileselector.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/gather/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/gather/abouttab.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/gather/manualtab.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/gather/nearbytab.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/gather/privatetab.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/gather/publictab.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/getcurrency.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/getremote.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/helpui.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/iconpicker.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/kiosk.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/league/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/league/rankbutton.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/league/rankwindow.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/mainmenu.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/party.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/partyqueue.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/play.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/addgame.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/browser.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/customizebrowser.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/edit.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/editcontroller.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/editgame.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/mapselect.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/share.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playoptions.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/popup.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/profile/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/profile/browser.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/profile/edit.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/profile/upgrade.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/promocode.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/purchase.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/qrcode.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/radiogroup.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/report.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/resourcetypeinfo.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/serverdialog.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/advanced.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/allsettings.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/audio.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/controls.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/gamepad.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/gamepadadvanced.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/gamepadselect.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/graphics.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/keyboard.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/nettesting.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/plugins.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/pluginsettings.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/remoteapp.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/testing.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/touchscreen.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/vrtesting.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/soundtrack/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/soundtrack/browser.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/soundtrack/edit.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/soundtrack/macmusicapp.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/specialoffer.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/store/__init__.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/store/browser.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/store/button.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/store/item.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/tabs.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/teamnamescolors.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/tournamententry.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/tournamentscores.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/trophies.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/url.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/v2upgrade.py \ + $(BUILD_DIR)/ba_data/python/bastd/ui/watch.py \ + $(BUILD_DIR)/ba_data/python/batemplatefs/__init__.py \ + $(BUILD_DIR)/ba_data/python/batemplatefs/_hooks.py \ + $(BUILD_DIR)/ba_data/python/batemplatefs/_subsystem.py \ + $(BUILD_DIR)/ba_data/python/bauiv1/__init__.py \ + $(BUILD_DIR)/ba_data/python/bauiv1/_hooks.py \ + $(BUILD_DIR)/ba_data/python/bauiv1/modutils.py \ + $(BUILD_DIR)/ba_data/python/bauiv1/onscreenkeyboard.py \ + $(BUILD_DIR)/ba_data/python/bauiv1/ui/__init__.py \ + $(BUILD_DIR)/server/ballisticakit_server.py + +SCRIPT_TARGETS_PYC_PUBLIC = \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_accountv2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_app.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_appcomponent.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_appconfig.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_apputils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_assetmanager.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_asyncio.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_cloud.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_error.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_general.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_hooks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_keyboard.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_language.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_login.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_math.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_meta.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/_mgen/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/_mgen/__pycache__/enums.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_net.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_plugin.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_text.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_workspace.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/internal.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_accountv1.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_achievement.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_ads.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_analytics.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_appdelegate.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_benchmark.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_campaign.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_input.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_level.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_lobby.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_music.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_net.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_profile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_servermode.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_store.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_subsystem.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_tips.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_tournament.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/_ui.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/macmusicapp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/osmusic.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/__pycache__/baenv.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baplus/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baplus/__pycache__/_hooks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/baplus/__pycache__/_subsystem.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_activity.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_activitytypes.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_actor.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_collision.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_coopgame.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_coopsession.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_debug.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_dependency.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_dualteamsession.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_featureset.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_freeforallsession.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_gameactivity.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_gameresults.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_gameutils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_hooks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_map.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_messages.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_multiteamsession.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_music.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_nodeactor.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_player.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_playlist.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_powerup.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_score.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_session.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_settings.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_stats.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_team.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/_teamgame.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bascenev1/__pycache__/internal.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/activity/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/activity/__pycache__/coopjoin.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/activity/__pycache__/coopscore.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/activity/__pycache__/drawscore.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/activity/__pycache__/dualteamscore.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/activity/__pycache__/freeforallvictory.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/activity/__pycache__/multiteamjoin.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/activity/__pycache__/multiteamscore.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/activity/__pycache__/multiteamvictory.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/background.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/bomb.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/controlsguide.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/flag.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/image.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/onscreencountdown.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/onscreentimer.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/playerspaz.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/popuptext.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/powerupbox.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/respawnicon.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/scoreboard.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/spawner.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/spaz.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/spazappearance.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/spazbot.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/spazfactory.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/text.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/tipstext.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/actor/__pycache__/zoomtext.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/assault.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/capturetheflag.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/chosenone.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/conquest.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/deathmatch.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/easteregghunt.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/elimination.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/football.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/hockey.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/keepaway.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/kingofthehill.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/meteorshower.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/ninjafight.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/onslaught.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/race.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/runaround.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/targetpractice.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/game/__pycache__/thelaststand.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/__pycache__/gameutils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/keyboard/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/keyboard/__pycache__/englishkeyboard.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/__pycache__/mainmenu.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/big_g.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/bridgit.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/courtyard.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/crag_castle.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/doom_shroom.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/football_stadium.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/happy_thoughts.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/hockey_stadium.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/lake_frigid.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/monkey_face.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/rampage.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/roundabout.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/step_right_up.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/the_pad.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/tip_top.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/tower_d.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/mapdata/__pycache__/zig_zag.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/__pycache__/maps.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/session/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/__pycache__/tutorial.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/account/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/account/__pycache__/link.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/account/__pycache__/settings.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/account/__pycache__/unlink.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/account/__pycache__/v2proxy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/account/__pycache__/viewer.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/achievements.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/appinvite.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/characterpicker.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/colorpicker.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/config.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/configerror.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/confirm.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/continues.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/coop/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/coop/__pycache__/browser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/coop/__pycache__/gamebutton.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/coop/__pycache__/level.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/coop/__pycache__/tournamentbutton.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/creditslist.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/debug.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/feedback.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/fileselector.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/gather/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/gather/__pycache__/abouttab.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/gather/__pycache__/manualtab.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/gather/__pycache__/nearbytab.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/gather/__pycache__/privatetab.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/gather/__pycache__/publictab.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/getcurrency.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/getremote.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/helpui.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/iconpicker.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/kiosk.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/league/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/league/__pycache__/rankbutton.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/league/__pycache__/rankwindow.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/mainmenu.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/party.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/partyqueue.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/play.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/__pycache__/addgame.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/__pycache__/browser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/__pycache__/customizebrowser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/__pycache__/edit.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/__pycache__/editcontroller.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/__pycache__/editgame.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/__pycache__/mapselect.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/playlist/__pycache__/share.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/playoptions.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/popup.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/profile/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/profile/__pycache__/browser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/profile/__pycache__/edit.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/profile/__pycache__/upgrade.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/promocode.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/purchase.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/qrcode.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/radiogroup.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/report.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/resourcetypeinfo.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/serverdialog.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/advanced.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/allsettings.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/audio.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/controls.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/gamepad.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/gamepadadvanced.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/gamepadselect.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/graphics.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/keyboard.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/nettesting.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/plugins.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/pluginsettings.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/remoteapp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/testing.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/touchscreen.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/settings/__pycache__/vrtesting.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/soundtrack/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/soundtrack/__pycache__/browser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/soundtrack/__pycache__/edit.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/soundtrack/__pycache__/entrytypeselect.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/soundtrack/__pycache__/macmusicapp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/specialoffer.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/store/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/store/__pycache__/browser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/store/__pycache__/button.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/store/__pycache__/item.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/tabs.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/teamnamescolors.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/tournamententry.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/tournamentscores.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/trophies.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/url.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/v2upgrade.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bastd/ui/__pycache__/watch.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/batemplatefs/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/batemplatefs/__pycache__/_hooks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/batemplatefs/__pycache__/_subsystem.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/_hooks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/modutils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bauiv1/__pycache__/onscreenkeyboard.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bauiv1/ui/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/server/__pycache__/ballisticakit_server.cpython-311.opt-1.pyc + +# Rule to copy src asset scripts to dst. +# (and make non-writable so I'm less likely to accidentally edit them there) +$(SCRIPT_TARGETS_PY_PUBLIC) : $(BUILD_DIR)/%.py : %.py + @echo Copying script: $(subst $(BUILD_DIR)/,,$@) + @mkdir -p $(dir $@) + @rm -f $@ + @cp $^ $@ + @chmod 444 $@ + +# These are too complex to define in a pattern rule; +# Instead we generate individual targets in a loop. +$(foreach element,$(SCRIPT_TARGETS_PYC_PUBLIC),\ +$(eval $(call make-opt-pyc-target,$(element)))) + +SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \ + $(BUILD_DIR)/ba_data/python/bacommon/__init__.py \ + $(BUILD_DIR)/ba_data/python/bacommon/assets.py \ + $(BUILD_DIR)/ba_data/python/bacommon/bacloud.py \ + $(BUILD_DIR)/ba_data/python/bacommon/build.py \ + $(BUILD_DIR)/ba_data/python/bacommon/cloud.py \ + $(BUILD_DIR)/ba_data/python/bacommon/login.py \ + $(BUILD_DIR)/ba_data/python/bacommon/net.py \ + $(BUILD_DIR)/ba_data/python/bacommon/servermanager.py \ + $(BUILD_DIR)/ba_data/python/bacommon/transfer.py \ + $(BUILD_DIR)/ba_data/python/efro/__init__.py \ + $(BUILD_DIR)/ba_data/python/efro/call.py \ + $(BUILD_DIR)/ba_data/python/efro/cloudshell.py \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/__init__.py \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/_api.py \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/_base.py \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/_inputter.py \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/_outputter.py \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/_pathcapture.py \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/_prep.py \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/extras.py \ + $(BUILD_DIR)/ba_data/python/efro/debug.py \ + $(BUILD_DIR)/ba_data/python/efro/error.py \ + $(BUILD_DIR)/ba_data/python/efro/log.py \ + $(BUILD_DIR)/ba_data/python/efro/message/__init__.py \ + $(BUILD_DIR)/ba_data/python/efro/message/_message.py \ + $(BUILD_DIR)/ba_data/python/efro/message/_module.py \ + $(BUILD_DIR)/ba_data/python/efro/message/_protocol.py \ + $(BUILD_DIR)/ba_data/python/efro/message/_receiver.py \ + $(BUILD_DIR)/ba_data/python/efro/message/_sender.py \ + $(BUILD_DIR)/ba_data/python/efro/rpc.py \ + $(BUILD_DIR)/ba_data/python/efro/terminal.py \ + $(BUILD_DIR)/ba_data/python/efro/util.py + +SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \ + $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/assets.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/bacloud.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/build.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/cloud.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/login.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/net.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/servermanager.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/bacommon/__pycache__/transfer.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/__pycache__/call.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/__pycache__/cloudshell.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/_api.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/_base.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/_inputter.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/_outputter.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/_pathcapture.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/_prep.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/dataclassio/__pycache__/extras.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/__pycache__/debug.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/__pycache__/error.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/__pycache__/log.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/message/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/message/__pycache__/_message.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/message/__pycache__/_module.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/message/__pycache__/_protocol.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/message/__pycache__/_receiver.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/message/__pycache__/_sender.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/__pycache__/rpc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/__pycache__/terminal.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/efro/__pycache__/util.cpython-311.opt-1.pyc + +# Rule to copy src asset scripts to dst. +# (and make non-writable so I'm less likely to accidentally edit them there) +$(SCRIPT_TARGETS_PY_PUBLIC_TOOLS) : $(BUILD_DIR)/ba_data/python/%.py : $(TOOLS_DIR)/%.py + @echo Copying script: $(subst $(BUILD_DIR)/,,$@) + @mkdir -p $(dir $@) + @rm -f $@ + @cp $^ $@ + @chmod 444 $@ + +# These are too complex to define in a pattern rule; +# Instead we generate individual targets in a loop. +$(foreach element,$(SCRIPT_TARGETS_PYC_PUBLIC_TOOLS),\ +$(eval $(call make-opt-pyc-target,$(element)))) +# __AUTOGENERATED_PUBLIC_END__ + +# This section is generated by batools.assetsmakefile; do not edit by hand. +# __AUTOGENERATED_PRIVATE_BEGIN__ + +SCRIPT_TARGETS_PY_PRIVATE_APPLE = \ + $(BUILD_DIR)/pylib-apple/__future__.py \ + $(BUILD_DIR)/pylib-apple/__hello__.py \ + $(BUILD_DIR)/pylib-apple/_aix_support.py \ + $(BUILD_DIR)/pylib-apple/_bootsubprocess.py \ + $(BUILD_DIR)/pylib-apple/_collections_abc.py \ + $(BUILD_DIR)/pylib-apple/_compat_pickle.py \ + $(BUILD_DIR)/pylib-apple/_compression.py \ + $(BUILD_DIR)/pylib-apple/_ios_support.py \ + $(BUILD_DIR)/pylib-apple/_markupbase.py \ + $(BUILD_DIR)/pylib-apple/_osx_support.py \ + $(BUILD_DIR)/pylib-apple/_py_abc.py \ + $(BUILD_DIR)/pylib-apple/_pydecimal.py \ + $(BUILD_DIR)/pylib-apple/_pyio.py \ + $(BUILD_DIR)/pylib-apple/_sitebuiltins.py \ + $(BUILD_DIR)/pylib-apple/_strptime.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata__darwin_darwin.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata__ios_iphoneos.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata__ios_iphoneos_arm64.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata__ios_iphonesimulator.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata__ios_iphonesimulator_arm64.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata__ios_iphonesimulator_x86_64.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata__tvos_appletvos.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata__tvos_appletvos_arm64.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata__tvos_appletvsimulator.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata__tvos_appletvsimulator_arm64.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata__tvos_appletvsimulator_x86_64.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata_d_darwin_darwin.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata_d_ios_iphoneos.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata_d_ios_iphoneos_arm64.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata_d_ios_iphonesimulator.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata_d_ios_iphonesimulator_arm64.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata_d_ios_iphonesimulator_x86_64.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata_d_tvos_appletvos.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata_d_tvos_appletvos_arm64.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata_d_tvos_appletvsimulator.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata_d_tvos_appletvsimulator_arm64.py \ + $(BUILD_DIR)/pylib-apple/_sysconfigdata_d_tvos_appletvsimulator_x86_64.py \ + $(BUILD_DIR)/pylib-apple/_threading_local.py \ + $(BUILD_DIR)/pylib-apple/_weakrefset.py \ + $(BUILD_DIR)/pylib-apple/abc.py \ + $(BUILD_DIR)/pylib-apple/aifc.py \ + $(BUILD_DIR)/pylib-apple/antigravity.py \ + $(BUILD_DIR)/pylib-apple/argparse.py \ + $(BUILD_DIR)/pylib-apple/ast.py \ + $(BUILD_DIR)/pylib-apple/asynchat.py \ + $(BUILD_DIR)/pylib-apple/asyncio/__init__.py \ + $(BUILD_DIR)/pylib-apple/asyncio/__main__.py \ + $(BUILD_DIR)/pylib-apple/asyncio/base_events.py \ + $(BUILD_DIR)/pylib-apple/asyncio/base_futures.py \ + $(BUILD_DIR)/pylib-apple/asyncio/base_subprocess.py \ + $(BUILD_DIR)/pylib-apple/asyncio/base_tasks.py \ + $(BUILD_DIR)/pylib-apple/asyncio/constants.py \ + $(BUILD_DIR)/pylib-apple/asyncio/coroutines.py \ + $(BUILD_DIR)/pylib-apple/asyncio/events.py \ + $(BUILD_DIR)/pylib-apple/asyncio/exceptions.py \ + $(BUILD_DIR)/pylib-apple/asyncio/format_helpers.py \ + $(BUILD_DIR)/pylib-apple/asyncio/futures.py \ + $(BUILD_DIR)/pylib-apple/asyncio/locks.py \ + $(BUILD_DIR)/pylib-apple/asyncio/log.py \ + $(BUILD_DIR)/pylib-apple/asyncio/mixins.py \ + $(BUILD_DIR)/pylib-apple/asyncio/proactor_events.py \ + $(BUILD_DIR)/pylib-apple/asyncio/protocols.py \ + $(BUILD_DIR)/pylib-apple/asyncio/queues.py \ + $(BUILD_DIR)/pylib-apple/asyncio/runners.py \ + $(BUILD_DIR)/pylib-apple/asyncio/selector_events.py \ + $(BUILD_DIR)/pylib-apple/asyncio/sslproto.py \ + $(BUILD_DIR)/pylib-apple/asyncio/staggered.py \ + $(BUILD_DIR)/pylib-apple/asyncio/streams.py \ + $(BUILD_DIR)/pylib-apple/asyncio/subprocess.py \ + $(BUILD_DIR)/pylib-apple/asyncio/taskgroups.py \ + $(BUILD_DIR)/pylib-apple/asyncio/tasks.py \ + $(BUILD_DIR)/pylib-apple/asyncio/threads.py \ + $(BUILD_DIR)/pylib-apple/asyncio/timeouts.py \ + $(BUILD_DIR)/pylib-apple/asyncio/transports.py \ + $(BUILD_DIR)/pylib-apple/asyncio/trsock.py \ + $(BUILD_DIR)/pylib-apple/asyncio/unix_events.py \ + $(BUILD_DIR)/pylib-apple/asyncio/windows_events.py \ + $(BUILD_DIR)/pylib-apple/asyncio/windows_utils.py \ + $(BUILD_DIR)/pylib-apple/asyncore.py \ + $(BUILD_DIR)/pylib-apple/base64.py \ + $(BUILD_DIR)/pylib-apple/bdb.py \ + $(BUILD_DIR)/pylib-apple/bisect.py \ + $(BUILD_DIR)/pylib-apple/bz2.py \ + $(BUILD_DIR)/pylib-apple/cProfile.py \ + $(BUILD_DIR)/pylib-apple/calendar.py \ + $(BUILD_DIR)/pylib-apple/cgi.py \ + $(BUILD_DIR)/pylib-apple/cgitb.py \ + $(BUILD_DIR)/pylib-apple/chunk.py \ + $(BUILD_DIR)/pylib-apple/cmd.py \ + $(BUILD_DIR)/pylib-apple/code.py \ + $(BUILD_DIR)/pylib-apple/codecs.py \ + $(BUILD_DIR)/pylib-apple/codeop.py \ + $(BUILD_DIR)/pylib-apple/collections/__init__.py \ + $(BUILD_DIR)/pylib-apple/collections/abc.py \ + $(BUILD_DIR)/pylib-apple/colorsys.py \ + $(BUILD_DIR)/pylib-apple/compileall.py \ + $(BUILD_DIR)/pylib-apple/concurrent/__init__.py \ + $(BUILD_DIR)/pylib-apple/concurrent/futures/__init__.py \ + $(BUILD_DIR)/pylib-apple/concurrent/futures/_base.py \ + $(BUILD_DIR)/pylib-apple/concurrent/futures/process.py \ + $(BUILD_DIR)/pylib-apple/concurrent/futures/thread.py \ + $(BUILD_DIR)/pylib-apple/configparser.py \ + $(BUILD_DIR)/pylib-apple/contextlib.py \ + $(BUILD_DIR)/pylib-apple/contextvars.py \ + $(BUILD_DIR)/pylib-apple/copy.py \ + $(BUILD_DIR)/pylib-apple/copyreg.py \ + $(BUILD_DIR)/pylib-apple/crypt.py \ + $(BUILD_DIR)/pylib-apple/csv.py \ + $(BUILD_DIR)/pylib-apple/ctypes/__init__.py \ + $(BUILD_DIR)/pylib-apple/ctypes/_aix.py \ + $(BUILD_DIR)/pylib-apple/ctypes/_endian.py \ + $(BUILD_DIR)/pylib-apple/ctypes/macholib/__init__.py \ + $(BUILD_DIR)/pylib-apple/ctypes/macholib/dyld.py \ + $(BUILD_DIR)/pylib-apple/ctypes/macholib/dylib.py \ + $(BUILD_DIR)/pylib-apple/ctypes/macholib/framework.py \ + $(BUILD_DIR)/pylib-apple/ctypes/util.py \ + $(BUILD_DIR)/pylib-apple/ctypes/wintypes.py \ + $(BUILD_DIR)/pylib-apple/curses/__init__.py \ + $(BUILD_DIR)/pylib-apple/curses/ascii.py \ + $(BUILD_DIR)/pylib-apple/curses/has_key.py \ + $(BUILD_DIR)/pylib-apple/curses/panel.py \ + $(BUILD_DIR)/pylib-apple/curses/textpad.py \ + $(BUILD_DIR)/pylib-apple/dataclasses.py \ + $(BUILD_DIR)/pylib-apple/datetime.py \ + $(BUILD_DIR)/pylib-apple/decimal.py \ + $(BUILD_DIR)/pylib-apple/difflib.py \ + $(BUILD_DIR)/pylib-apple/dis.py \ + $(BUILD_DIR)/pylib-apple/doctest.py \ + $(BUILD_DIR)/pylib-apple/email/__init__.py \ + $(BUILD_DIR)/pylib-apple/email/_encoded_words.py \ + $(BUILD_DIR)/pylib-apple/email/_header_value_parser.py \ + $(BUILD_DIR)/pylib-apple/email/_parseaddr.py \ + $(BUILD_DIR)/pylib-apple/email/_policybase.py \ + $(BUILD_DIR)/pylib-apple/email/base64mime.py \ + $(BUILD_DIR)/pylib-apple/email/charset.py \ + $(BUILD_DIR)/pylib-apple/email/contentmanager.py \ + $(BUILD_DIR)/pylib-apple/email/encoders.py \ + $(BUILD_DIR)/pylib-apple/email/errors.py \ + $(BUILD_DIR)/pylib-apple/email/feedparser.py \ + $(BUILD_DIR)/pylib-apple/email/generator.py \ + $(BUILD_DIR)/pylib-apple/email/header.py \ + $(BUILD_DIR)/pylib-apple/email/headerregistry.py \ + $(BUILD_DIR)/pylib-apple/email/iterators.py \ + $(BUILD_DIR)/pylib-apple/email/message.py \ + $(BUILD_DIR)/pylib-apple/email/mime/__init__.py \ + $(BUILD_DIR)/pylib-apple/email/mime/application.py \ + $(BUILD_DIR)/pylib-apple/email/mime/audio.py \ + $(BUILD_DIR)/pylib-apple/email/mime/base.py \ + $(BUILD_DIR)/pylib-apple/email/mime/image.py \ + $(BUILD_DIR)/pylib-apple/email/mime/message.py \ + $(BUILD_DIR)/pylib-apple/email/mime/multipart.py \ + $(BUILD_DIR)/pylib-apple/email/mime/nonmultipart.py \ + $(BUILD_DIR)/pylib-apple/email/mime/text.py \ + $(BUILD_DIR)/pylib-apple/email/parser.py \ + $(BUILD_DIR)/pylib-apple/email/policy.py \ + $(BUILD_DIR)/pylib-apple/email/quoprimime.py \ + $(BUILD_DIR)/pylib-apple/email/utils.py \ + $(BUILD_DIR)/pylib-apple/encodings/__init__.py \ + $(BUILD_DIR)/pylib-apple/encodings/aliases.py \ + $(BUILD_DIR)/pylib-apple/encodings/ascii.py \ + $(BUILD_DIR)/pylib-apple/encodings/base64_codec.py \ + $(BUILD_DIR)/pylib-apple/encodings/big5.py \ + $(BUILD_DIR)/pylib-apple/encodings/big5hkscs.py \ + $(BUILD_DIR)/pylib-apple/encodings/bz2_codec.py \ + $(BUILD_DIR)/pylib-apple/encodings/charmap.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp037.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp1006.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp1026.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp1125.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp1140.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp1250.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp1251.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp1252.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp1253.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp1254.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp1255.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp1256.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp1257.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp1258.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp273.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp424.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp437.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp500.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp720.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp737.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp775.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp850.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp852.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp855.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp856.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp857.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp858.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp860.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp861.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp862.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp863.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp864.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp865.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp866.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp869.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp874.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp875.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp932.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp949.py \ + $(BUILD_DIR)/pylib-apple/encodings/cp950.py \ + $(BUILD_DIR)/pylib-apple/encodings/euc_jis_2004.py \ + $(BUILD_DIR)/pylib-apple/encodings/euc_jisx0213.py \ + $(BUILD_DIR)/pylib-apple/encodings/euc_jp.py \ + $(BUILD_DIR)/pylib-apple/encodings/euc_kr.py \ + $(BUILD_DIR)/pylib-apple/encodings/gb18030.py \ + $(BUILD_DIR)/pylib-apple/encodings/gb2312.py \ + $(BUILD_DIR)/pylib-apple/encodings/gbk.py \ + $(BUILD_DIR)/pylib-apple/encodings/hex_codec.py \ + $(BUILD_DIR)/pylib-apple/encodings/hp_roman8.py \ + $(BUILD_DIR)/pylib-apple/encodings/hz.py \ + $(BUILD_DIR)/pylib-apple/encodings/idna.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso2022_jp.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso2022_jp_1.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso2022_jp_2.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso2022_jp_2004.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso2022_jp_3.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso2022_jp_ext.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso2022_kr.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso8859_1.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso8859_10.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso8859_11.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso8859_13.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso8859_14.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso8859_15.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso8859_16.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso8859_2.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso8859_3.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso8859_4.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso8859_5.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso8859_6.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso8859_7.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso8859_8.py \ + $(BUILD_DIR)/pylib-apple/encodings/iso8859_9.py \ + $(BUILD_DIR)/pylib-apple/encodings/johab.py \ + $(BUILD_DIR)/pylib-apple/encodings/koi8_r.py \ + $(BUILD_DIR)/pylib-apple/encodings/koi8_t.py \ + $(BUILD_DIR)/pylib-apple/encodings/koi8_u.py \ + $(BUILD_DIR)/pylib-apple/encodings/kz1048.py \ + $(BUILD_DIR)/pylib-apple/encodings/latin_1.py \ + $(BUILD_DIR)/pylib-apple/encodings/mac_arabic.py \ + $(BUILD_DIR)/pylib-apple/encodings/mac_croatian.py \ + $(BUILD_DIR)/pylib-apple/encodings/mac_cyrillic.py \ + $(BUILD_DIR)/pylib-apple/encodings/mac_farsi.py \ + $(BUILD_DIR)/pylib-apple/encodings/mac_greek.py \ + $(BUILD_DIR)/pylib-apple/encodings/mac_iceland.py \ + $(BUILD_DIR)/pylib-apple/encodings/mac_latin2.py \ + $(BUILD_DIR)/pylib-apple/encodings/mac_roman.py \ + $(BUILD_DIR)/pylib-apple/encodings/mac_romanian.py \ + $(BUILD_DIR)/pylib-apple/encodings/mac_turkish.py \ + $(BUILD_DIR)/pylib-apple/encodings/mbcs.py \ + $(BUILD_DIR)/pylib-apple/encodings/oem.py \ + $(BUILD_DIR)/pylib-apple/encodings/palmos.py \ + $(BUILD_DIR)/pylib-apple/encodings/ptcp154.py \ + $(BUILD_DIR)/pylib-apple/encodings/punycode.py \ + $(BUILD_DIR)/pylib-apple/encodings/quopri_codec.py \ + $(BUILD_DIR)/pylib-apple/encodings/raw_unicode_escape.py \ + $(BUILD_DIR)/pylib-apple/encodings/rot_13.py \ + $(BUILD_DIR)/pylib-apple/encodings/shift_jis.py \ + $(BUILD_DIR)/pylib-apple/encodings/shift_jis_2004.py \ + $(BUILD_DIR)/pylib-apple/encodings/shift_jisx0213.py \ + $(BUILD_DIR)/pylib-apple/encodings/tis_620.py \ + $(BUILD_DIR)/pylib-apple/encodings/undefined.py \ + $(BUILD_DIR)/pylib-apple/encodings/unicode_escape.py \ + $(BUILD_DIR)/pylib-apple/encodings/utf_16.py \ + $(BUILD_DIR)/pylib-apple/encodings/utf_16_be.py \ + $(BUILD_DIR)/pylib-apple/encodings/utf_16_le.py \ + $(BUILD_DIR)/pylib-apple/encodings/utf_32.py \ + $(BUILD_DIR)/pylib-apple/encodings/utf_32_be.py \ + $(BUILD_DIR)/pylib-apple/encodings/utf_32_le.py \ + $(BUILD_DIR)/pylib-apple/encodings/utf_7.py \ + $(BUILD_DIR)/pylib-apple/encodings/utf_8.py \ + $(BUILD_DIR)/pylib-apple/encodings/utf_8_sig.py \ + $(BUILD_DIR)/pylib-apple/encodings/uu_codec.py \ + $(BUILD_DIR)/pylib-apple/encodings/zlib_codec.py \ + $(BUILD_DIR)/pylib-apple/enum.py \ + $(BUILD_DIR)/pylib-apple/filecmp.py \ + $(BUILD_DIR)/pylib-apple/fileinput.py \ + $(BUILD_DIR)/pylib-apple/fnmatch.py \ + $(BUILD_DIR)/pylib-apple/fractions.py \ + $(BUILD_DIR)/pylib-apple/ftplib.py \ + $(BUILD_DIR)/pylib-apple/functools.py \ + $(BUILD_DIR)/pylib-apple/genericpath.py \ + $(BUILD_DIR)/pylib-apple/getopt.py \ + $(BUILD_DIR)/pylib-apple/getpass.py \ + $(BUILD_DIR)/pylib-apple/gettext.py \ + $(BUILD_DIR)/pylib-apple/glob.py \ + $(BUILD_DIR)/pylib-apple/graphlib.py \ + $(BUILD_DIR)/pylib-apple/gzip.py \ + $(BUILD_DIR)/pylib-apple/hashlib.py \ + $(BUILD_DIR)/pylib-apple/heapq.py \ + $(BUILD_DIR)/pylib-apple/hmac.py \ + $(BUILD_DIR)/pylib-apple/html/__init__.py \ + $(BUILD_DIR)/pylib-apple/html/entities.py \ + $(BUILD_DIR)/pylib-apple/html/parser.py \ + $(BUILD_DIR)/pylib-apple/http/__init__.py \ + $(BUILD_DIR)/pylib-apple/http/client.py \ + $(BUILD_DIR)/pylib-apple/http/cookiejar.py \ + $(BUILD_DIR)/pylib-apple/http/cookies.py \ + $(BUILD_DIR)/pylib-apple/http/server.py \ + $(BUILD_DIR)/pylib-apple/imghdr.py \ + $(BUILD_DIR)/pylib-apple/imp.py \ + $(BUILD_DIR)/pylib-apple/importlib/__init__.py \ + $(BUILD_DIR)/pylib-apple/importlib/_abc.py \ + $(BUILD_DIR)/pylib-apple/importlib/_bootstrap.py \ + $(BUILD_DIR)/pylib-apple/importlib/_bootstrap_external.py \ + $(BUILD_DIR)/pylib-apple/importlib/abc.py \ + $(BUILD_DIR)/pylib-apple/importlib/machinery.py \ + $(BUILD_DIR)/pylib-apple/importlib/metadata/__init__.py \ + $(BUILD_DIR)/pylib-apple/importlib/metadata/_adapters.py \ + $(BUILD_DIR)/pylib-apple/importlib/metadata/_collections.py \ + $(BUILD_DIR)/pylib-apple/importlib/metadata/_functools.py \ + $(BUILD_DIR)/pylib-apple/importlib/metadata/_itertools.py \ + $(BUILD_DIR)/pylib-apple/importlib/metadata/_meta.py \ + $(BUILD_DIR)/pylib-apple/importlib/metadata/_text.py \ + $(BUILD_DIR)/pylib-apple/importlib/readers.py \ + $(BUILD_DIR)/pylib-apple/importlib/resources/__init__.py \ + $(BUILD_DIR)/pylib-apple/importlib/resources/_adapters.py \ + $(BUILD_DIR)/pylib-apple/importlib/resources/_common.py \ + $(BUILD_DIR)/pylib-apple/importlib/resources/_itertools.py \ + $(BUILD_DIR)/pylib-apple/importlib/resources/_legacy.py \ + $(BUILD_DIR)/pylib-apple/importlib/resources/abc.py \ + $(BUILD_DIR)/pylib-apple/importlib/resources/readers.py \ + $(BUILD_DIR)/pylib-apple/importlib/resources/simple.py \ + $(BUILD_DIR)/pylib-apple/importlib/simple.py \ + $(BUILD_DIR)/pylib-apple/importlib/util.py \ + $(BUILD_DIR)/pylib-apple/inspect.py \ + $(BUILD_DIR)/pylib-apple/io.py \ + $(BUILD_DIR)/pylib-apple/ipaddress.py \ + $(BUILD_DIR)/pylib-apple/json/__init__.py \ + $(BUILD_DIR)/pylib-apple/json/decoder.py \ + $(BUILD_DIR)/pylib-apple/json/encoder.py \ + $(BUILD_DIR)/pylib-apple/json/scanner.py \ + $(BUILD_DIR)/pylib-apple/json/tool.py \ + $(BUILD_DIR)/pylib-apple/keyword.py \ + $(BUILD_DIR)/pylib-apple/linecache.py \ + $(BUILD_DIR)/pylib-apple/locale.py \ + $(BUILD_DIR)/pylib-apple/logging/__init__.py \ + $(BUILD_DIR)/pylib-apple/logging/config.py \ + $(BUILD_DIR)/pylib-apple/logging/handlers.py \ + $(BUILD_DIR)/pylib-apple/lzma.py \ + $(BUILD_DIR)/pylib-apple/mailbox.py \ + $(BUILD_DIR)/pylib-apple/mailcap.py \ + $(BUILD_DIR)/pylib-apple/mimetypes.py \ + $(BUILD_DIR)/pylib-apple/modulefinder.py \ + $(BUILD_DIR)/pylib-apple/netrc.py \ + $(BUILD_DIR)/pylib-apple/nntplib.py \ + $(BUILD_DIR)/pylib-apple/ntpath.py \ + $(BUILD_DIR)/pylib-apple/nturl2path.py \ + $(BUILD_DIR)/pylib-apple/numbers.py \ + $(BUILD_DIR)/pylib-apple/opcode.py \ + $(BUILD_DIR)/pylib-apple/operator.py \ + $(BUILD_DIR)/pylib-apple/optparse.py \ + $(BUILD_DIR)/pylib-apple/os.py \ + $(BUILD_DIR)/pylib-apple/pathlib.py \ + $(BUILD_DIR)/pylib-apple/pdb.py \ + $(BUILD_DIR)/pylib-apple/pickle.py \ + $(BUILD_DIR)/pylib-apple/pickletools.py \ + $(BUILD_DIR)/pylib-apple/pipes.py \ + $(BUILD_DIR)/pylib-apple/pkgutil.py \ + $(BUILD_DIR)/pylib-apple/platform.py \ + $(BUILD_DIR)/pylib-apple/plistlib.py \ + $(BUILD_DIR)/pylib-apple/poplib.py \ + $(BUILD_DIR)/pylib-apple/posixpath.py \ + $(BUILD_DIR)/pylib-apple/pprint.py \ + $(BUILD_DIR)/pylib-apple/profile.py \ + $(BUILD_DIR)/pylib-apple/pstats.py \ + $(BUILD_DIR)/pylib-apple/pty.py \ + $(BUILD_DIR)/pylib-apple/py_compile.py \ + $(BUILD_DIR)/pylib-apple/pyclbr.py \ + $(BUILD_DIR)/pylib-apple/pydoc.py \ + $(BUILD_DIR)/pylib-apple/queue.py \ + $(BUILD_DIR)/pylib-apple/quopri.py \ + $(BUILD_DIR)/pylib-apple/random.py \ + $(BUILD_DIR)/pylib-apple/re/__init__.py \ + $(BUILD_DIR)/pylib-apple/re/_casefix.py \ + $(BUILD_DIR)/pylib-apple/re/_compiler.py \ + $(BUILD_DIR)/pylib-apple/re/_constants.py \ + $(BUILD_DIR)/pylib-apple/re/_parser.py \ + $(BUILD_DIR)/pylib-apple/reprlib.py \ + $(BUILD_DIR)/pylib-apple/rlcompleter.py \ + $(BUILD_DIR)/pylib-apple/runpy.py \ + $(BUILD_DIR)/pylib-apple/sched.py \ + $(BUILD_DIR)/pylib-apple/secrets.py \ + $(BUILD_DIR)/pylib-apple/selectors.py \ + $(BUILD_DIR)/pylib-apple/shelve.py \ + $(BUILD_DIR)/pylib-apple/shlex.py \ + $(BUILD_DIR)/pylib-apple/shutil.py \ + $(BUILD_DIR)/pylib-apple/signal.py \ + $(BUILD_DIR)/pylib-apple/site.py \ + $(BUILD_DIR)/pylib-apple/smtpd.py \ + $(BUILD_DIR)/pylib-apple/smtplib.py \ + $(BUILD_DIR)/pylib-apple/sndhdr.py \ + $(BUILD_DIR)/pylib-apple/socket.py \ + $(BUILD_DIR)/pylib-apple/socketserver.py \ + $(BUILD_DIR)/pylib-apple/sqlite3/__init__.py \ + $(BUILD_DIR)/pylib-apple/sqlite3/dbapi2.py \ + $(BUILD_DIR)/pylib-apple/sqlite3/dump.py \ + $(BUILD_DIR)/pylib-apple/sre_compile.py \ + $(BUILD_DIR)/pylib-apple/sre_constants.py \ + $(BUILD_DIR)/pylib-apple/sre_parse.py \ + $(BUILD_DIR)/pylib-apple/ssl.py \ + $(BUILD_DIR)/pylib-apple/stat.py \ + $(BUILD_DIR)/pylib-apple/statistics.py \ + $(BUILD_DIR)/pylib-apple/string.py \ + $(BUILD_DIR)/pylib-apple/stringprep.py \ + $(BUILD_DIR)/pylib-apple/struct.py \ + $(BUILD_DIR)/pylib-apple/subprocess.py \ + $(BUILD_DIR)/pylib-apple/sunau.py \ + $(BUILD_DIR)/pylib-apple/symtable.py \ + $(BUILD_DIR)/pylib-apple/sysconfig.py \ + $(BUILD_DIR)/pylib-apple/tabnanny.py \ + $(BUILD_DIR)/pylib-apple/tarfile.py \ + $(BUILD_DIR)/pylib-apple/telnetlib.py \ + $(BUILD_DIR)/pylib-apple/tempfile.py \ + $(BUILD_DIR)/pylib-apple/textwrap.py \ + $(BUILD_DIR)/pylib-apple/this.py \ + $(BUILD_DIR)/pylib-apple/threading.py \ + $(BUILD_DIR)/pylib-apple/timeit.py \ + $(BUILD_DIR)/pylib-apple/token.py \ + $(BUILD_DIR)/pylib-apple/tokenize.py \ + $(BUILD_DIR)/pylib-apple/tomllib/__init__.py \ + $(BUILD_DIR)/pylib-apple/tomllib/_parser.py \ + $(BUILD_DIR)/pylib-apple/tomllib/_re.py \ + $(BUILD_DIR)/pylib-apple/tomllib/_types.py \ + $(BUILD_DIR)/pylib-apple/trace.py \ + $(BUILD_DIR)/pylib-apple/traceback.py \ + $(BUILD_DIR)/pylib-apple/tracemalloc.py \ + $(BUILD_DIR)/pylib-apple/tty.py \ + $(BUILD_DIR)/pylib-apple/types.py \ + $(BUILD_DIR)/pylib-apple/typing.py \ + $(BUILD_DIR)/pylib-apple/urllib/__init__.py \ + $(BUILD_DIR)/pylib-apple/urllib/error.py \ + $(BUILD_DIR)/pylib-apple/urllib/parse.py \ + $(BUILD_DIR)/pylib-apple/urllib/request.py \ + $(BUILD_DIR)/pylib-apple/urllib/response.py \ + $(BUILD_DIR)/pylib-apple/urllib/robotparser.py \ + $(BUILD_DIR)/pylib-apple/uu.py \ + $(BUILD_DIR)/pylib-apple/uuid.py \ + $(BUILD_DIR)/pylib-apple/warnings.py \ + $(BUILD_DIR)/pylib-apple/wave.py \ + $(BUILD_DIR)/pylib-apple/weakref.py \ + $(BUILD_DIR)/pylib-apple/webbrowser.py \ + $(BUILD_DIR)/pylib-apple/xdrlib.py \ + $(BUILD_DIR)/pylib-apple/xml/__init__.py \ + $(BUILD_DIR)/pylib-apple/xml/dom/NodeFilter.py \ + $(BUILD_DIR)/pylib-apple/xml/dom/__init__.py \ + $(BUILD_DIR)/pylib-apple/xml/dom/domreg.py \ + $(BUILD_DIR)/pylib-apple/xml/dom/expatbuilder.py \ + $(BUILD_DIR)/pylib-apple/xml/dom/minicompat.py \ + $(BUILD_DIR)/pylib-apple/xml/dom/minidom.py \ + $(BUILD_DIR)/pylib-apple/xml/dom/pulldom.py \ + $(BUILD_DIR)/pylib-apple/xml/dom/xmlbuilder.py \ + $(BUILD_DIR)/pylib-apple/xml/etree/ElementInclude.py \ + $(BUILD_DIR)/pylib-apple/xml/etree/ElementPath.py \ + $(BUILD_DIR)/pylib-apple/xml/etree/ElementTree.py \ + $(BUILD_DIR)/pylib-apple/xml/etree/__init__.py \ + $(BUILD_DIR)/pylib-apple/xml/etree/cElementTree.py \ + $(BUILD_DIR)/pylib-apple/xml/parsers/__init__.py \ + $(BUILD_DIR)/pylib-apple/xml/parsers/expat.py \ + $(BUILD_DIR)/pylib-apple/xml/sax/__init__.py \ + $(BUILD_DIR)/pylib-apple/xml/sax/_exceptions.py \ + $(BUILD_DIR)/pylib-apple/xml/sax/expatreader.py \ + $(BUILD_DIR)/pylib-apple/xml/sax/handler.py \ + $(BUILD_DIR)/pylib-apple/xml/sax/saxutils.py \ + $(BUILD_DIR)/pylib-apple/xml/sax/xmlreader.py \ + $(BUILD_DIR)/pylib-apple/xmlrpc/__init__.py \ + $(BUILD_DIR)/pylib-apple/xmlrpc/client.py \ + $(BUILD_DIR)/pylib-apple/xmlrpc/server.py \ + $(BUILD_DIR)/pylib-apple/zipapp.py \ + $(BUILD_DIR)/pylib-apple/zipfile.py \ + $(BUILD_DIR)/pylib-apple/zipimport.py \ + $(BUILD_DIR)/pylib-apple/zoneinfo/__init__.py \ + $(BUILD_DIR)/pylib-apple/zoneinfo/_common.py \ + $(BUILD_DIR)/pylib-apple/zoneinfo/_tzpath.py \ + $(BUILD_DIR)/pylib-apple/zoneinfo/_zoneinfo.py + +SCRIPT_TARGETS_PYC_PRIVATE_APPLE = \ + $(BUILD_DIR)/pylib-apple/__pycache__/__future__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/__hello__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_aix_support.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_bootsubprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_collections_abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_compat_pickle.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_compression.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_ios_support.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_markupbase.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_osx_support.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_py_abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_pydecimal.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_pyio.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sitebuiltins.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_strptime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata__darwin_darwin.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata__ios_iphoneos.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata__ios_iphoneos_arm64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata__ios_iphonesimulator.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata__ios_iphonesimulator_arm64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata__ios_iphonesimulator_x86_64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata__tvos_appletvos.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata__tvos_appletvos_arm64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata__tvos_appletvsimulator.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata__tvos_appletvsimulator_arm64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata__tvos_appletvsimulator_x86_64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata_d_darwin_darwin.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata_d_ios_iphoneos.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata_d_ios_iphoneos_arm64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata_d_ios_iphonesimulator.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata_d_ios_iphonesimulator_arm64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata_d_ios_iphonesimulator_x86_64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata_d_tvos_appletvos.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata_d_tvos_appletvos_arm64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata_d_tvos_appletvsimulator.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata_d_tvos_appletvsimulator_arm64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_sysconfigdata_d_tvos_appletvsimulator_x86_64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_threading_local.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/_weakrefset.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/aifc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/antigravity.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/argparse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/ast.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/asynchat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/__main__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/base_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/base_futures.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/base_subprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/base_tasks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/constants.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/coroutines.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/exceptions.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/format_helpers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/futures.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/locks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/log.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/mixins.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/proactor_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/protocols.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/queues.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/runners.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/selector_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/sslproto.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/staggered.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/streams.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/subprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/taskgroups.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/tasks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/threads.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/timeouts.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/transports.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/trsock.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/unix_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/windows_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/asyncio/__pycache__/windows_utils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/asyncore.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/base64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/bdb.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/bisect.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/bz2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/cProfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/calendar.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/cgi.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/cgitb.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/chunk.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/cmd.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/code.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/codecs.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/codeop.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/collections/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/collections/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/colorsys.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/compileall.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/concurrent/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/concurrent/futures/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/concurrent/futures/__pycache__/_base.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/concurrent/futures/__pycache__/process.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/concurrent/futures/__pycache__/thread.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/configparser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/contextlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/contextvars.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/copy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/copyreg.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/crypt.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/csv.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/ctypes/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/ctypes/__pycache__/_aix.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/ctypes/__pycache__/_endian.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/ctypes/macholib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/ctypes/macholib/__pycache__/dyld.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/ctypes/macholib/__pycache__/dylib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/ctypes/macholib/__pycache__/framework.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/ctypes/__pycache__/util.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/ctypes/__pycache__/wintypes.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/curses/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/curses/__pycache__/ascii.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/curses/__pycache__/has_key.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/curses/__pycache__/panel.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/curses/__pycache__/textpad.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/dataclasses.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/datetime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/decimal.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/difflib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/dis.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/doctest.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/_encoded_words.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/_header_value_parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/_parseaddr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/_policybase.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/base64mime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/charset.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/contentmanager.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/encoders.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/errors.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/feedparser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/generator.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/header.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/headerregistry.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/iterators.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/message.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/mime/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/mime/__pycache__/application.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/mime/__pycache__/audio.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/mime/__pycache__/base.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/mime/__pycache__/image.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/mime/__pycache__/message.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/mime/__pycache__/multipart.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/mime/__pycache__/nonmultipart.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/mime/__pycache__/text.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/policy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/quoprimime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/email/__pycache__/utils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/aliases.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/ascii.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/base64_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/big5.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/big5hkscs.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/bz2_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/charmap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp037.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp1006.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp1026.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp1125.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp1140.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp1250.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp1251.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp1252.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp1253.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp1254.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp1255.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp1256.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp1257.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp1258.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp273.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp424.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp437.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp500.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp720.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp737.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp775.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp850.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp852.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp855.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp856.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp857.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp858.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp860.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp861.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp862.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp863.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp864.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp865.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp866.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp869.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp874.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp875.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp932.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp949.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/cp950.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/euc_jis_2004.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/euc_jisx0213.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/euc_jp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/euc_kr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/gb18030.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/gb2312.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/gbk.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/hex_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/hp_roman8.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/hz.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/idna.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso2022_jp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso2022_jp_1.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso2022_jp_2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso2022_jp_2004.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso2022_jp_3.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso2022_jp_ext.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso2022_kr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso8859_1.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso8859_10.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso8859_11.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso8859_13.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso8859_14.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso8859_15.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso8859_16.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso8859_2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso8859_3.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso8859_4.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso8859_5.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso8859_6.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso8859_7.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso8859_8.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/iso8859_9.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/johab.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/koi8_r.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/koi8_t.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/koi8_u.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/kz1048.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/latin_1.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/mac_arabic.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/mac_croatian.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/mac_cyrillic.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/mac_farsi.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/mac_greek.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/mac_iceland.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/mac_latin2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/mac_roman.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/mac_romanian.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/mac_turkish.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/mbcs.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/oem.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/palmos.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/ptcp154.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/punycode.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/quopri_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/raw_unicode_escape.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/rot_13.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/shift_jis.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/shift_jis_2004.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/shift_jisx0213.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/tis_620.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/undefined.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/unicode_escape.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/utf_16.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/utf_16_be.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/utf_16_le.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/utf_32.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/utf_32_be.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/utf_32_le.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/utf_7.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/utf_8.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/utf_8_sig.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/uu_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/encodings/__pycache__/zlib_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/enum.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/filecmp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/fileinput.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/fnmatch.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/fractions.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/ftplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/functools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/genericpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/getopt.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/getpass.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/gettext.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/glob.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/graphlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/gzip.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/hashlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/heapq.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/hmac.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/html/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/html/__pycache__/entities.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/html/__pycache__/parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/http/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/http/__pycache__/client.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/http/__pycache__/cookiejar.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/http/__pycache__/cookies.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/http/__pycache__/server.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/imghdr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/imp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/__pycache__/_abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/__pycache__/_bootstrap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/__pycache__/_bootstrap_external.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/__pycache__/machinery.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/metadata/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/metadata/__pycache__/_adapters.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/metadata/__pycache__/_collections.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/metadata/__pycache__/_functools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/metadata/__pycache__/_itertools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/metadata/__pycache__/_meta.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/metadata/__pycache__/_text.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/__pycache__/readers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/resources/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/resources/__pycache__/_adapters.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/resources/__pycache__/_common.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/resources/__pycache__/_itertools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/resources/__pycache__/_legacy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/resources/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/resources/__pycache__/readers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/resources/__pycache__/simple.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/__pycache__/simple.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/importlib/__pycache__/util.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/inspect.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/io.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/ipaddress.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/json/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/json/__pycache__/decoder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/json/__pycache__/encoder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/json/__pycache__/scanner.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/json/__pycache__/tool.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/keyword.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/linecache.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/locale.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/logging/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/logging/__pycache__/config.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/logging/__pycache__/handlers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/lzma.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/mailbox.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/mailcap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/mimetypes.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/modulefinder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/netrc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/nntplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/ntpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/nturl2path.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/numbers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/opcode.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/operator.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/optparse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/os.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/pathlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/pdb.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/pickle.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/pickletools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/pipes.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/pkgutil.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/platform.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/plistlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/poplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/posixpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/pprint.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/profile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/pstats.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/pty.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/py_compile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/pyclbr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/pydoc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/queue.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/quopri.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/random.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/re/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/re/__pycache__/_casefix.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/re/__pycache__/_compiler.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/re/__pycache__/_constants.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/re/__pycache__/_parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/reprlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/rlcompleter.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/runpy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/sched.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/secrets.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/selectors.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/shelve.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/shlex.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/shutil.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/signal.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/site.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/smtpd.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/smtplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/sndhdr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/socket.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/socketserver.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/sqlite3/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/sqlite3/__pycache__/dbapi2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/sqlite3/__pycache__/dump.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/sre_compile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/sre_constants.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/sre_parse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/ssl.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/stat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/statistics.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/string.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/stringprep.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/struct.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/subprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/sunau.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/symtable.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/sysconfig.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/tabnanny.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/tarfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/telnetlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/tempfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/textwrap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/this.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/threading.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/timeit.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/token.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/tokenize.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/tomllib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/tomllib/__pycache__/_parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/tomllib/__pycache__/_re.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/tomllib/__pycache__/_types.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/trace.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/traceback.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/tracemalloc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/tty.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/types.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/typing.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/urllib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/urllib/__pycache__/error.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/urllib/__pycache__/parse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/urllib/__pycache__/request.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/urllib/__pycache__/response.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/urllib/__pycache__/robotparser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/uu.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/uuid.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/warnings.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/wave.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/weakref.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/webbrowser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/xdrlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/dom/__pycache__/NodeFilter.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/dom/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/dom/__pycache__/domreg.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/dom/__pycache__/expatbuilder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/dom/__pycache__/minicompat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/dom/__pycache__/minidom.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/dom/__pycache__/pulldom.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/dom/__pycache__/xmlbuilder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/etree/__pycache__/ElementInclude.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/etree/__pycache__/ElementPath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/etree/__pycache__/ElementTree.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/etree/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/etree/__pycache__/cElementTree.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/parsers/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/parsers/__pycache__/expat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/sax/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/sax/__pycache__/_exceptions.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/sax/__pycache__/expatreader.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/sax/__pycache__/handler.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/sax/__pycache__/saxutils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xml/sax/__pycache__/xmlreader.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xmlrpc/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xmlrpc/__pycache__/client.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/xmlrpc/__pycache__/server.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/zipapp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/zipfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/__pycache__/zipimport.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/zoneinfo/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/zoneinfo/__pycache__/_common.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/zoneinfo/__pycache__/_tzpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-apple/zoneinfo/__pycache__/_zoneinfo.cpython-311.opt-1.pyc + +# Rule to copy src asset scripts to dst. +# (and make non-writable so I'm less likely to accidentally edit them there) +$(SCRIPT_TARGETS_PY_PRIVATE_APPLE) : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +# These are too complex to define in a pattern rule; +# Instead we generate individual targets in a loop. +$(foreach element,$(SCRIPT_TARGETS_PYC_PRIVATE_APPLE),\ +$(eval $(call make-opt-pyc-target,$(element)))) + +SCRIPT_TARGETS_PY_PRIVATE_ANDROID = \ + $(BUILD_DIR)/pylib-android/__future__.py \ + $(BUILD_DIR)/pylib-android/__hello__.py \ + $(BUILD_DIR)/pylib-android/_aix_support.py \ + $(BUILD_DIR)/pylib-android/_bootsubprocess.py \ + $(BUILD_DIR)/pylib-android/_collections_abc.py \ + $(BUILD_DIR)/pylib-android/_compat_pickle.py \ + $(BUILD_DIR)/pylib-android/_compression.py \ + $(BUILD_DIR)/pylib-android/_markupbase.py \ + $(BUILD_DIR)/pylib-android/_osx_support.py \ + $(BUILD_DIR)/pylib-android/_py_abc.py \ + $(BUILD_DIR)/pylib-android/_pydecimal.py \ + $(BUILD_DIR)/pylib-android/_pyio.py \ + $(BUILD_DIR)/pylib-android/_sitebuiltins.py \ + $(BUILD_DIR)/pylib-android/_strptime.py \ + $(BUILD_DIR)/pylib-android/_sysconfigdata__linux_aarch64-linux-android.py \ + $(BUILD_DIR)/pylib-android/_sysconfigdata__linux_arm-linux-androideabi.py \ + $(BUILD_DIR)/pylib-android/_sysconfigdata__linux_i686-linux-android.py \ + $(BUILD_DIR)/pylib-android/_sysconfigdata__linux_x86_64-linux-android.py \ + $(BUILD_DIR)/pylib-android/_sysconfigdata_d_linux_aarch64-linux-android.py \ + $(BUILD_DIR)/pylib-android/_sysconfigdata_d_linux_arm-linux-androideabi.py \ + $(BUILD_DIR)/pylib-android/_sysconfigdata_d_linux_i686-linux-android.py \ + $(BUILD_DIR)/pylib-android/_sysconfigdata_d_linux_x86_64-linux-android.py \ + $(BUILD_DIR)/pylib-android/_threading_local.py \ + $(BUILD_DIR)/pylib-android/_weakrefset.py \ + $(BUILD_DIR)/pylib-android/abc.py \ + $(BUILD_DIR)/pylib-android/aifc.py \ + $(BUILD_DIR)/pylib-android/antigravity.py \ + $(BUILD_DIR)/pylib-android/argparse.py \ + $(BUILD_DIR)/pylib-android/ast.py \ + $(BUILD_DIR)/pylib-android/asynchat.py \ + $(BUILD_DIR)/pylib-android/asyncio/__init__.py \ + $(BUILD_DIR)/pylib-android/asyncio/__main__.py \ + $(BUILD_DIR)/pylib-android/asyncio/base_events.py \ + $(BUILD_DIR)/pylib-android/asyncio/base_futures.py \ + $(BUILD_DIR)/pylib-android/asyncio/base_subprocess.py \ + $(BUILD_DIR)/pylib-android/asyncio/base_tasks.py \ + $(BUILD_DIR)/pylib-android/asyncio/constants.py \ + $(BUILD_DIR)/pylib-android/asyncio/coroutines.py \ + $(BUILD_DIR)/pylib-android/asyncio/events.py \ + $(BUILD_DIR)/pylib-android/asyncio/exceptions.py \ + $(BUILD_DIR)/pylib-android/asyncio/format_helpers.py \ + $(BUILD_DIR)/pylib-android/asyncio/futures.py \ + $(BUILD_DIR)/pylib-android/asyncio/locks.py \ + $(BUILD_DIR)/pylib-android/asyncio/log.py \ + $(BUILD_DIR)/pylib-android/asyncio/mixins.py \ + $(BUILD_DIR)/pylib-android/asyncio/proactor_events.py \ + $(BUILD_DIR)/pylib-android/asyncio/protocols.py \ + $(BUILD_DIR)/pylib-android/asyncio/queues.py \ + $(BUILD_DIR)/pylib-android/asyncio/runners.py \ + $(BUILD_DIR)/pylib-android/asyncio/selector_events.py \ + $(BUILD_DIR)/pylib-android/asyncio/sslproto.py \ + $(BUILD_DIR)/pylib-android/asyncio/staggered.py \ + $(BUILD_DIR)/pylib-android/asyncio/streams.py \ + $(BUILD_DIR)/pylib-android/asyncio/subprocess.py \ + $(BUILD_DIR)/pylib-android/asyncio/taskgroups.py \ + $(BUILD_DIR)/pylib-android/asyncio/tasks.py \ + $(BUILD_DIR)/pylib-android/asyncio/threads.py \ + $(BUILD_DIR)/pylib-android/asyncio/timeouts.py \ + $(BUILD_DIR)/pylib-android/asyncio/transports.py \ + $(BUILD_DIR)/pylib-android/asyncio/trsock.py \ + $(BUILD_DIR)/pylib-android/asyncio/unix_events.py \ + $(BUILD_DIR)/pylib-android/asyncio/windows_events.py \ + $(BUILD_DIR)/pylib-android/asyncio/windows_utils.py \ + $(BUILD_DIR)/pylib-android/asyncore.py \ + $(BUILD_DIR)/pylib-android/base64.py \ + $(BUILD_DIR)/pylib-android/bdb.py \ + $(BUILD_DIR)/pylib-android/bisect.py \ + $(BUILD_DIR)/pylib-android/bz2.py \ + $(BUILD_DIR)/pylib-android/cProfile.py \ + $(BUILD_DIR)/pylib-android/calendar.py \ + $(BUILD_DIR)/pylib-android/cgi.py \ + $(BUILD_DIR)/pylib-android/cgitb.py \ + $(BUILD_DIR)/pylib-android/chunk.py \ + $(BUILD_DIR)/pylib-android/cmd.py \ + $(BUILD_DIR)/pylib-android/code.py \ + $(BUILD_DIR)/pylib-android/codecs.py \ + $(BUILD_DIR)/pylib-android/codeop.py \ + $(BUILD_DIR)/pylib-android/collections/__init__.py \ + $(BUILD_DIR)/pylib-android/collections/abc.py \ + $(BUILD_DIR)/pylib-android/colorsys.py \ + $(BUILD_DIR)/pylib-android/compileall.py \ + $(BUILD_DIR)/pylib-android/concurrent/__init__.py \ + $(BUILD_DIR)/pylib-android/concurrent/futures/__init__.py \ + $(BUILD_DIR)/pylib-android/concurrent/futures/_base.py \ + $(BUILD_DIR)/pylib-android/concurrent/futures/process.py \ + $(BUILD_DIR)/pylib-android/concurrent/futures/thread.py \ + $(BUILD_DIR)/pylib-android/configparser.py \ + $(BUILD_DIR)/pylib-android/contextlib.py \ + $(BUILD_DIR)/pylib-android/contextvars.py \ + $(BUILD_DIR)/pylib-android/copy.py \ + $(BUILD_DIR)/pylib-android/copyreg.py \ + $(BUILD_DIR)/pylib-android/crypt.py \ + $(BUILD_DIR)/pylib-android/csv.py \ + $(BUILD_DIR)/pylib-android/ctypes/__init__.py \ + $(BUILD_DIR)/pylib-android/ctypes/_aix.py \ + $(BUILD_DIR)/pylib-android/ctypes/_endian.py \ + $(BUILD_DIR)/pylib-android/ctypes/macholib/__init__.py \ + $(BUILD_DIR)/pylib-android/ctypes/macholib/dyld.py \ + $(BUILD_DIR)/pylib-android/ctypes/macholib/dylib.py \ + $(BUILD_DIR)/pylib-android/ctypes/macholib/framework.py \ + $(BUILD_DIR)/pylib-android/ctypes/util.py \ + $(BUILD_DIR)/pylib-android/ctypes/wintypes.py \ + $(BUILD_DIR)/pylib-android/curses/__init__.py \ + $(BUILD_DIR)/pylib-android/curses/ascii.py \ + $(BUILD_DIR)/pylib-android/curses/has_key.py \ + $(BUILD_DIR)/pylib-android/curses/panel.py \ + $(BUILD_DIR)/pylib-android/curses/textpad.py \ + $(BUILD_DIR)/pylib-android/dataclasses.py \ + $(BUILD_DIR)/pylib-android/datetime.py \ + $(BUILD_DIR)/pylib-android/decimal.py \ + $(BUILD_DIR)/pylib-android/difflib.py \ + $(BUILD_DIR)/pylib-android/dis.py \ + $(BUILD_DIR)/pylib-android/doctest.py \ + $(BUILD_DIR)/pylib-android/email/__init__.py \ + $(BUILD_DIR)/pylib-android/email/_encoded_words.py \ + $(BUILD_DIR)/pylib-android/email/_header_value_parser.py \ + $(BUILD_DIR)/pylib-android/email/_parseaddr.py \ + $(BUILD_DIR)/pylib-android/email/_policybase.py \ + $(BUILD_DIR)/pylib-android/email/base64mime.py \ + $(BUILD_DIR)/pylib-android/email/charset.py \ + $(BUILD_DIR)/pylib-android/email/contentmanager.py \ + $(BUILD_DIR)/pylib-android/email/encoders.py \ + $(BUILD_DIR)/pylib-android/email/errors.py \ + $(BUILD_DIR)/pylib-android/email/feedparser.py \ + $(BUILD_DIR)/pylib-android/email/generator.py \ + $(BUILD_DIR)/pylib-android/email/header.py \ + $(BUILD_DIR)/pylib-android/email/headerregistry.py \ + $(BUILD_DIR)/pylib-android/email/iterators.py \ + $(BUILD_DIR)/pylib-android/email/message.py \ + $(BUILD_DIR)/pylib-android/email/mime/__init__.py \ + $(BUILD_DIR)/pylib-android/email/mime/application.py \ + $(BUILD_DIR)/pylib-android/email/mime/audio.py \ + $(BUILD_DIR)/pylib-android/email/mime/base.py \ + $(BUILD_DIR)/pylib-android/email/mime/image.py \ + $(BUILD_DIR)/pylib-android/email/mime/message.py \ + $(BUILD_DIR)/pylib-android/email/mime/multipart.py \ + $(BUILD_DIR)/pylib-android/email/mime/nonmultipart.py \ + $(BUILD_DIR)/pylib-android/email/mime/text.py \ + $(BUILD_DIR)/pylib-android/email/parser.py \ + $(BUILD_DIR)/pylib-android/email/policy.py \ + $(BUILD_DIR)/pylib-android/email/quoprimime.py \ + $(BUILD_DIR)/pylib-android/email/utils.py \ + $(BUILD_DIR)/pylib-android/encodings/__init__.py \ + $(BUILD_DIR)/pylib-android/encodings/aliases.py \ + $(BUILD_DIR)/pylib-android/encodings/ascii.py \ + $(BUILD_DIR)/pylib-android/encodings/base64_codec.py \ + $(BUILD_DIR)/pylib-android/encodings/big5.py \ + $(BUILD_DIR)/pylib-android/encodings/big5hkscs.py \ + $(BUILD_DIR)/pylib-android/encodings/bz2_codec.py \ + $(BUILD_DIR)/pylib-android/encodings/charmap.py \ + $(BUILD_DIR)/pylib-android/encodings/cp037.py \ + $(BUILD_DIR)/pylib-android/encodings/cp1006.py \ + $(BUILD_DIR)/pylib-android/encodings/cp1026.py \ + $(BUILD_DIR)/pylib-android/encodings/cp1125.py \ + $(BUILD_DIR)/pylib-android/encodings/cp1140.py \ + $(BUILD_DIR)/pylib-android/encodings/cp1250.py \ + $(BUILD_DIR)/pylib-android/encodings/cp1251.py \ + $(BUILD_DIR)/pylib-android/encodings/cp1252.py \ + $(BUILD_DIR)/pylib-android/encodings/cp1253.py \ + $(BUILD_DIR)/pylib-android/encodings/cp1254.py \ + $(BUILD_DIR)/pylib-android/encodings/cp1255.py \ + $(BUILD_DIR)/pylib-android/encodings/cp1256.py \ + $(BUILD_DIR)/pylib-android/encodings/cp1257.py \ + $(BUILD_DIR)/pylib-android/encodings/cp1258.py \ + $(BUILD_DIR)/pylib-android/encodings/cp273.py \ + $(BUILD_DIR)/pylib-android/encodings/cp424.py \ + $(BUILD_DIR)/pylib-android/encodings/cp437.py \ + $(BUILD_DIR)/pylib-android/encodings/cp500.py \ + $(BUILD_DIR)/pylib-android/encodings/cp720.py \ + $(BUILD_DIR)/pylib-android/encodings/cp737.py \ + $(BUILD_DIR)/pylib-android/encodings/cp775.py \ + $(BUILD_DIR)/pylib-android/encodings/cp850.py \ + $(BUILD_DIR)/pylib-android/encodings/cp852.py \ + $(BUILD_DIR)/pylib-android/encodings/cp855.py \ + $(BUILD_DIR)/pylib-android/encodings/cp856.py \ + $(BUILD_DIR)/pylib-android/encodings/cp857.py \ + $(BUILD_DIR)/pylib-android/encodings/cp858.py \ + $(BUILD_DIR)/pylib-android/encodings/cp860.py \ + $(BUILD_DIR)/pylib-android/encodings/cp861.py \ + $(BUILD_DIR)/pylib-android/encodings/cp862.py \ + $(BUILD_DIR)/pylib-android/encodings/cp863.py \ + $(BUILD_DIR)/pylib-android/encodings/cp864.py \ + $(BUILD_DIR)/pylib-android/encodings/cp865.py \ + $(BUILD_DIR)/pylib-android/encodings/cp866.py \ + $(BUILD_DIR)/pylib-android/encodings/cp869.py \ + $(BUILD_DIR)/pylib-android/encodings/cp874.py \ + $(BUILD_DIR)/pylib-android/encodings/cp875.py \ + $(BUILD_DIR)/pylib-android/encodings/cp932.py \ + $(BUILD_DIR)/pylib-android/encodings/cp949.py \ + $(BUILD_DIR)/pylib-android/encodings/cp950.py \ + $(BUILD_DIR)/pylib-android/encodings/euc_jis_2004.py \ + $(BUILD_DIR)/pylib-android/encodings/euc_jisx0213.py \ + $(BUILD_DIR)/pylib-android/encodings/euc_jp.py \ + $(BUILD_DIR)/pylib-android/encodings/euc_kr.py \ + $(BUILD_DIR)/pylib-android/encodings/gb18030.py \ + $(BUILD_DIR)/pylib-android/encodings/gb2312.py \ + $(BUILD_DIR)/pylib-android/encodings/gbk.py \ + $(BUILD_DIR)/pylib-android/encodings/hex_codec.py \ + $(BUILD_DIR)/pylib-android/encodings/hp_roman8.py \ + $(BUILD_DIR)/pylib-android/encodings/hz.py \ + $(BUILD_DIR)/pylib-android/encodings/idna.py \ + $(BUILD_DIR)/pylib-android/encodings/iso2022_jp.py \ + $(BUILD_DIR)/pylib-android/encodings/iso2022_jp_1.py \ + $(BUILD_DIR)/pylib-android/encodings/iso2022_jp_2.py \ + $(BUILD_DIR)/pylib-android/encodings/iso2022_jp_2004.py \ + $(BUILD_DIR)/pylib-android/encodings/iso2022_jp_3.py \ + $(BUILD_DIR)/pylib-android/encodings/iso2022_jp_ext.py \ + $(BUILD_DIR)/pylib-android/encodings/iso2022_kr.py \ + $(BUILD_DIR)/pylib-android/encodings/iso8859_1.py \ + $(BUILD_DIR)/pylib-android/encodings/iso8859_10.py \ + $(BUILD_DIR)/pylib-android/encodings/iso8859_11.py \ + $(BUILD_DIR)/pylib-android/encodings/iso8859_13.py \ + $(BUILD_DIR)/pylib-android/encodings/iso8859_14.py \ + $(BUILD_DIR)/pylib-android/encodings/iso8859_15.py \ + $(BUILD_DIR)/pylib-android/encodings/iso8859_16.py \ + $(BUILD_DIR)/pylib-android/encodings/iso8859_2.py \ + $(BUILD_DIR)/pylib-android/encodings/iso8859_3.py \ + $(BUILD_DIR)/pylib-android/encodings/iso8859_4.py \ + $(BUILD_DIR)/pylib-android/encodings/iso8859_5.py \ + $(BUILD_DIR)/pylib-android/encodings/iso8859_6.py \ + $(BUILD_DIR)/pylib-android/encodings/iso8859_7.py \ + $(BUILD_DIR)/pylib-android/encodings/iso8859_8.py \ + $(BUILD_DIR)/pylib-android/encodings/iso8859_9.py \ + $(BUILD_DIR)/pylib-android/encodings/johab.py \ + $(BUILD_DIR)/pylib-android/encodings/koi8_r.py \ + $(BUILD_DIR)/pylib-android/encodings/koi8_t.py \ + $(BUILD_DIR)/pylib-android/encodings/koi8_u.py \ + $(BUILD_DIR)/pylib-android/encodings/kz1048.py \ + $(BUILD_DIR)/pylib-android/encodings/latin_1.py \ + $(BUILD_DIR)/pylib-android/encodings/mac_arabic.py \ + $(BUILD_DIR)/pylib-android/encodings/mac_croatian.py \ + $(BUILD_DIR)/pylib-android/encodings/mac_cyrillic.py \ + $(BUILD_DIR)/pylib-android/encodings/mac_farsi.py \ + $(BUILD_DIR)/pylib-android/encodings/mac_greek.py \ + $(BUILD_DIR)/pylib-android/encodings/mac_iceland.py \ + $(BUILD_DIR)/pylib-android/encodings/mac_latin2.py \ + $(BUILD_DIR)/pylib-android/encodings/mac_roman.py \ + $(BUILD_DIR)/pylib-android/encodings/mac_romanian.py \ + $(BUILD_DIR)/pylib-android/encodings/mac_turkish.py \ + $(BUILD_DIR)/pylib-android/encodings/mbcs.py \ + $(BUILD_DIR)/pylib-android/encodings/oem.py \ + $(BUILD_DIR)/pylib-android/encodings/palmos.py \ + $(BUILD_DIR)/pylib-android/encodings/ptcp154.py \ + $(BUILD_DIR)/pylib-android/encodings/punycode.py \ + $(BUILD_DIR)/pylib-android/encodings/quopri_codec.py \ + $(BUILD_DIR)/pylib-android/encodings/raw_unicode_escape.py \ + $(BUILD_DIR)/pylib-android/encodings/rot_13.py \ + $(BUILD_DIR)/pylib-android/encodings/shift_jis.py \ + $(BUILD_DIR)/pylib-android/encodings/shift_jis_2004.py \ + $(BUILD_DIR)/pylib-android/encodings/shift_jisx0213.py \ + $(BUILD_DIR)/pylib-android/encodings/tis_620.py \ + $(BUILD_DIR)/pylib-android/encodings/undefined.py \ + $(BUILD_DIR)/pylib-android/encodings/unicode_escape.py \ + $(BUILD_DIR)/pylib-android/encodings/utf_16.py \ + $(BUILD_DIR)/pylib-android/encodings/utf_16_be.py \ + $(BUILD_DIR)/pylib-android/encodings/utf_16_le.py \ + $(BUILD_DIR)/pylib-android/encodings/utf_32.py \ + $(BUILD_DIR)/pylib-android/encodings/utf_32_be.py \ + $(BUILD_DIR)/pylib-android/encodings/utf_32_le.py \ + $(BUILD_DIR)/pylib-android/encodings/utf_7.py \ + $(BUILD_DIR)/pylib-android/encodings/utf_8.py \ + $(BUILD_DIR)/pylib-android/encodings/utf_8_sig.py \ + $(BUILD_DIR)/pylib-android/encodings/uu_codec.py \ + $(BUILD_DIR)/pylib-android/encodings/zlib_codec.py \ + $(BUILD_DIR)/pylib-android/enum.py \ + $(BUILD_DIR)/pylib-android/filecmp.py \ + $(BUILD_DIR)/pylib-android/fileinput.py \ + $(BUILD_DIR)/pylib-android/fnmatch.py \ + $(BUILD_DIR)/pylib-android/fractions.py \ + $(BUILD_DIR)/pylib-android/ftplib.py \ + $(BUILD_DIR)/pylib-android/functools.py \ + $(BUILD_DIR)/pylib-android/genericpath.py \ + $(BUILD_DIR)/pylib-android/getopt.py \ + $(BUILD_DIR)/pylib-android/getpass.py \ + $(BUILD_DIR)/pylib-android/gettext.py \ + $(BUILD_DIR)/pylib-android/glob.py \ + $(BUILD_DIR)/pylib-android/graphlib.py \ + $(BUILD_DIR)/pylib-android/gzip.py \ + $(BUILD_DIR)/pylib-android/hashlib.py \ + $(BUILD_DIR)/pylib-android/heapq.py \ + $(BUILD_DIR)/pylib-android/hmac.py \ + $(BUILD_DIR)/pylib-android/html/__init__.py \ + $(BUILD_DIR)/pylib-android/html/entities.py \ + $(BUILD_DIR)/pylib-android/html/parser.py \ + $(BUILD_DIR)/pylib-android/http/__init__.py \ + $(BUILD_DIR)/pylib-android/http/client.py \ + $(BUILD_DIR)/pylib-android/http/cookiejar.py \ + $(BUILD_DIR)/pylib-android/http/cookies.py \ + $(BUILD_DIR)/pylib-android/http/server.py \ + $(BUILD_DIR)/pylib-android/imghdr.py \ + $(BUILD_DIR)/pylib-android/imp.py \ + $(BUILD_DIR)/pylib-android/importlib/__init__.py \ + $(BUILD_DIR)/pylib-android/importlib/_abc.py \ + $(BUILD_DIR)/pylib-android/importlib/_bootstrap.py \ + $(BUILD_DIR)/pylib-android/importlib/_bootstrap_external.py \ + $(BUILD_DIR)/pylib-android/importlib/abc.py \ + $(BUILD_DIR)/pylib-android/importlib/machinery.py \ + $(BUILD_DIR)/pylib-android/importlib/metadata/__init__.py \ + $(BUILD_DIR)/pylib-android/importlib/metadata/_adapters.py \ + $(BUILD_DIR)/pylib-android/importlib/metadata/_collections.py \ + $(BUILD_DIR)/pylib-android/importlib/metadata/_functools.py \ + $(BUILD_DIR)/pylib-android/importlib/metadata/_itertools.py \ + $(BUILD_DIR)/pylib-android/importlib/metadata/_meta.py \ + $(BUILD_DIR)/pylib-android/importlib/metadata/_text.py \ + $(BUILD_DIR)/pylib-android/importlib/readers.py \ + $(BUILD_DIR)/pylib-android/importlib/resources/__init__.py \ + $(BUILD_DIR)/pylib-android/importlib/resources/_adapters.py \ + $(BUILD_DIR)/pylib-android/importlib/resources/_common.py \ + $(BUILD_DIR)/pylib-android/importlib/resources/_itertools.py \ + $(BUILD_DIR)/pylib-android/importlib/resources/_legacy.py \ + $(BUILD_DIR)/pylib-android/importlib/resources/abc.py \ + $(BUILD_DIR)/pylib-android/importlib/resources/readers.py \ + $(BUILD_DIR)/pylib-android/importlib/resources/simple.py \ + $(BUILD_DIR)/pylib-android/importlib/simple.py \ + $(BUILD_DIR)/pylib-android/importlib/util.py \ + $(BUILD_DIR)/pylib-android/inspect.py \ + $(BUILD_DIR)/pylib-android/io.py \ + $(BUILD_DIR)/pylib-android/ipaddress.py \ + $(BUILD_DIR)/pylib-android/json/__init__.py \ + $(BUILD_DIR)/pylib-android/json/decoder.py \ + $(BUILD_DIR)/pylib-android/json/encoder.py \ + $(BUILD_DIR)/pylib-android/json/scanner.py \ + $(BUILD_DIR)/pylib-android/json/tool.py \ + $(BUILD_DIR)/pylib-android/keyword.py \ + $(BUILD_DIR)/pylib-android/linecache.py \ + $(BUILD_DIR)/pylib-android/locale.py \ + $(BUILD_DIR)/pylib-android/logging/__init__.py \ + $(BUILD_DIR)/pylib-android/logging/config.py \ + $(BUILD_DIR)/pylib-android/logging/handlers.py \ + $(BUILD_DIR)/pylib-android/lzma.py \ + $(BUILD_DIR)/pylib-android/mailbox.py \ + $(BUILD_DIR)/pylib-android/mailcap.py \ + $(BUILD_DIR)/pylib-android/mimetypes.py \ + $(BUILD_DIR)/pylib-android/modulefinder.py \ + $(BUILD_DIR)/pylib-android/netrc.py \ + $(BUILD_DIR)/pylib-android/nntplib.py \ + $(BUILD_DIR)/pylib-android/ntpath.py \ + $(BUILD_DIR)/pylib-android/nturl2path.py \ + $(BUILD_DIR)/pylib-android/numbers.py \ + $(BUILD_DIR)/pylib-android/opcode.py \ + $(BUILD_DIR)/pylib-android/operator.py \ + $(BUILD_DIR)/pylib-android/optparse.py \ + $(BUILD_DIR)/pylib-android/os.py \ + $(BUILD_DIR)/pylib-android/pathlib.py \ + $(BUILD_DIR)/pylib-android/pdb.py \ + $(BUILD_DIR)/pylib-android/pickle.py \ + $(BUILD_DIR)/pylib-android/pickletools.py \ + $(BUILD_DIR)/pylib-android/pipes.py \ + $(BUILD_DIR)/pylib-android/pkgutil.py \ + $(BUILD_DIR)/pylib-android/platform.py \ + $(BUILD_DIR)/pylib-android/plistlib.py \ + $(BUILD_DIR)/pylib-android/poplib.py \ + $(BUILD_DIR)/pylib-android/posixpath.py \ + $(BUILD_DIR)/pylib-android/pprint.py \ + $(BUILD_DIR)/pylib-android/profile.py \ + $(BUILD_DIR)/pylib-android/pstats.py \ + $(BUILD_DIR)/pylib-android/pty.py \ + $(BUILD_DIR)/pylib-android/py_compile.py \ + $(BUILD_DIR)/pylib-android/pyclbr.py \ + $(BUILD_DIR)/pylib-android/pydoc.py \ + $(BUILD_DIR)/pylib-android/queue.py \ + $(BUILD_DIR)/pylib-android/quopri.py \ + $(BUILD_DIR)/pylib-android/random.py \ + $(BUILD_DIR)/pylib-android/re/__init__.py \ + $(BUILD_DIR)/pylib-android/re/_casefix.py \ + $(BUILD_DIR)/pylib-android/re/_compiler.py \ + $(BUILD_DIR)/pylib-android/re/_constants.py \ + $(BUILD_DIR)/pylib-android/re/_parser.py \ + $(BUILD_DIR)/pylib-android/reprlib.py \ + $(BUILD_DIR)/pylib-android/rlcompleter.py \ + $(BUILD_DIR)/pylib-android/runpy.py \ + $(BUILD_DIR)/pylib-android/sched.py \ + $(BUILD_DIR)/pylib-android/secrets.py \ + $(BUILD_DIR)/pylib-android/selectors.py \ + $(BUILD_DIR)/pylib-android/shelve.py \ + $(BUILD_DIR)/pylib-android/shlex.py \ + $(BUILD_DIR)/pylib-android/shutil.py \ + $(BUILD_DIR)/pylib-android/signal.py \ + $(BUILD_DIR)/pylib-android/site.py \ + $(BUILD_DIR)/pylib-android/smtpd.py \ + $(BUILD_DIR)/pylib-android/smtplib.py \ + $(BUILD_DIR)/pylib-android/sndhdr.py \ + $(BUILD_DIR)/pylib-android/socket.py \ + $(BUILD_DIR)/pylib-android/socketserver.py \ + $(BUILD_DIR)/pylib-android/sqlite3/__init__.py \ + $(BUILD_DIR)/pylib-android/sqlite3/dbapi2.py \ + $(BUILD_DIR)/pylib-android/sqlite3/dump.py \ + $(BUILD_DIR)/pylib-android/sre_compile.py \ + $(BUILD_DIR)/pylib-android/sre_constants.py \ + $(BUILD_DIR)/pylib-android/sre_parse.py \ + $(BUILD_DIR)/pylib-android/ssl.py \ + $(BUILD_DIR)/pylib-android/stat.py \ + $(BUILD_DIR)/pylib-android/statistics.py \ + $(BUILD_DIR)/pylib-android/string.py \ + $(BUILD_DIR)/pylib-android/stringprep.py \ + $(BUILD_DIR)/pylib-android/struct.py \ + $(BUILD_DIR)/pylib-android/subprocess.py \ + $(BUILD_DIR)/pylib-android/sunau.py \ + $(BUILD_DIR)/pylib-android/symtable.py \ + $(BUILD_DIR)/pylib-android/sysconfig.py \ + $(BUILD_DIR)/pylib-android/tabnanny.py \ + $(BUILD_DIR)/pylib-android/tarfile.py \ + $(BUILD_DIR)/pylib-android/telnetlib.py \ + $(BUILD_DIR)/pylib-android/tempfile.py \ + $(BUILD_DIR)/pylib-android/textwrap.py \ + $(BUILD_DIR)/pylib-android/this.py \ + $(BUILD_DIR)/pylib-android/threading.py \ + $(BUILD_DIR)/pylib-android/timeit.py \ + $(BUILD_DIR)/pylib-android/token.py \ + $(BUILD_DIR)/pylib-android/tokenize.py \ + $(BUILD_DIR)/pylib-android/tomllib/__init__.py \ + $(BUILD_DIR)/pylib-android/tomllib/_parser.py \ + $(BUILD_DIR)/pylib-android/tomllib/_re.py \ + $(BUILD_DIR)/pylib-android/tomllib/_types.py \ + $(BUILD_DIR)/pylib-android/trace.py \ + $(BUILD_DIR)/pylib-android/traceback.py \ + $(BUILD_DIR)/pylib-android/tracemalloc.py \ + $(BUILD_DIR)/pylib-android/tty.py \ + $(BUILD_DIR)/pylib-android/types.py \ + $(BUILD_DIR)/pylib-android/typing.py \ + $(BUILD_DIR)/pylib-android/urllib/__init__.py \ + $(BUILD_DIR)/pylib-android/urllib/error.py \ + $(BUILD_DIR)/pylib-android/urllib/parse.py \ + $(BUILD_DIR)/pylib-android/urllib/request.py \ + $(BUILD_DIR)/pylib-android/urllib/response.py \ + $(BUILD_DIR)/pylib-android/urllib/robotparser.py \ + $(BUILD_DIR)/pylib-android/uu.py \ + $(BUILD_DIR)/pylib-android/uuid.py \ + $(BUILD_DIR)/pylib-android/warnings.py \ + $(BUILD_DIR)/pylib-android/wave.py \ + $(BUILD_DIR)/pylib-android/weakref.py \ + $(BUILD_DIR)/pylib-android/webbrowser.py \ + $(BUILD_DIR)/pylib-android/xdrlib.py \ + $(BUILD_DIR)/pylib-android/xml/__init__.py \ + $(BUILD_DIR)/pylib-android/xml/dom/NodeFilter.py \ + $(BUILD_DIR)/pylib-android/xml/dom/__init__.py \ + $(BUILD_DIR)/pylib-android/xml/dom/domreg.py \ + $(BUILD_DIR)/pylib-android/xml/dom/expatbuilder.py \ + $(BUILD_DIR)/pylib-android/xml/dom/minicompat.py \ + $(BUILD_DIR)/pylib-android/xml/dom/minidom.py \ + $(BUILD_DIR)/pylib-android/xml/dom/pulldom.py \ + $(BUILD_DIR)/pylib-android/xml/dom/xmlbuilder.py \ + $(BUILD_DIR)/pylib-android/xml/etree/ElementInclude.py \ + $(BUILD_DIR)/pylib-android/xml/etree/ElementPath.py \ + $(BUILD_DIR)/pylib-android/xml/etree/ElementTree.py \ + $(BUILD_DIR)/pylib-android/xml/etree/__init__.py \ + $(BUILD_DIR)/pylib-android/xml/etree/cElementTree.py \ + $(BUILD_DIR)/pylib-android/xml/parsers/__init__.py \ + $(BUILD_DIR)/pylib-android/xml/parsers/expat.py \ + $(BUILD_DIR)/pylib-android/xml/sax/__init__.py \ + $(BUILD_DIR)/pylib-android/xml/sax/_exceptions.py \ + $(BUILD_DIR)/pylib-android/xml/sax/expatreader.py \ + $(BUILD_DIR)/pylib-android/xml/sax/handler.py \ + $(BUILD_DIR)/pylib-android/xml/sax/saxutils.py \ + $(BUILD_DIR)/pylib-android/xml/sax/xmlreader.py \ + $(BUILD_DIR)/pylib-android/xmlrpc/__init__.py \ + $(BUILD_DIR)/pylib-android/xmlrpc/client.py \ + $(BUILD_DIR)/pylib-android/xmlrpc/server.py \ + $(BUILD_DIR)/pylib-android/zipapp.py \ + $(BUILD_DIR)/pylib-android/zipfile.py \ + $(BUILD_DIR)/pylib-android/zipimport.py \ + $(BUILD_DIR)/pylib-android/zoneinfo/__init__.py \ + $(BUILD_DIR)/pylib-android/zoneinfo/_common.py \ + $(BUILD_DIR)/pylib-android/zoneinfo/_tzpath.py \ + $(BUILD_DIR)/pylib-android/zoneinfo/_zoneinfo.py + +SCRIPT_TARGETS_PYC_PRIVATE_ANDROID = \ + $(BUILD_DIR)/pylib-android/__pycache__/__future__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/__hello__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_aix_support.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_bootsubprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_collections_abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_compat_pickle.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_compression.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_markupbase.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_osx_support.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_py_abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_pydecimal.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_pyio.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_sitebuiltins.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_strptime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_sysconfigdata__linux_aarch64-linux-android.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_sysconfigdata__linux_arm-linux-androideabi.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_sysconfigdata__linux_i686-linux-android.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_sysconfigdata__linux_x86_64-linux-android.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_sysconfigdata_d_linux_aarch64-linux-android.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_sysconfigdata_d_linux_arm-linux-androideabi.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_sysconfigdata_d_linux_i686-linux-android.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_sysconfigdata_d_linux_x86_64-linux-android.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_threading_local.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/_weakrefset.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/aifc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/antigravity.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/argparse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/ast.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/asynchat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/__main__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/base_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/base_futures.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/base_subprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/base_tasks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/constants.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/coroutines.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/exceptions.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/format_helpers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/futures.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/locks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/log.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/mixins.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/proactor_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/protocols.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/queues.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/runners.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/selector_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/sslproto.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/staggered.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/streams.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/subprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/taskgroups.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/tasks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/threads.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/timeouts.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/transports.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/trsock.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/unix_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/windows_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/asyncio/__pycache__/windows_utils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/asyncore.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/base64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/bdb.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/bisect.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/bz2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/cProfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/calendar.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/cgi.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/cgitb.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/chunk.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/cmd.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/code.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/codecs.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/codeop.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/collections/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/collections/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/colorsys.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/compileall.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/concurrent/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/concurrent/futures/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/concurrent/futures/__pycache__/_base.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/concurrent/futures/__pycache__/process.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/concurrent/futures/__pycache__/thread.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/configparser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/contextlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/contextvars.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/copy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/copyreg.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/crypt.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/csv.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/ctypes/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/ctypes/__pycache__/_aix.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/ctypes/__pycache__/_endian.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/ctypes/macholib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/ctypes/macholib/__pycache__/dyld.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/ctypes/macholib/__pycache__/dylib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/ctypes/macholib/__pycache__/framework.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/ctypes/__pycache__/util.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/ctypes/__pycache__/wintypes.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/curses/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/curses/__pycache__/ascii.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/curses/__pycache__/has_key.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/curses/__pycache__/panel.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/curses/__pycache__/textpad.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/dataclasses.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/datetime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/decimal.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/difflib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/dis.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/doctest.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/_encoded_words.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/_header_value_parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/_parseaddr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/_policybase.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/base64mime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/charset.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/contentmanager.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/encoders.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/errors.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/feedparser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/generator.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/header.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/headerregistry.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/iterators.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/message.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/mime/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/mime/__pycache__/application.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/mime/__pycache__/audio.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/mime/__pycache__/base.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/mime/__pycache__/image.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/mime/__pycache__/message.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/mime/__pycache__/multipart.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/mime/__pycache__/nonmultipart.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/mime/__pycache__/text.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/policy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/quoprimime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/email/__pycache__/utils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/aliases.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/ascii.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/base64_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/big5.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/big5hkscs.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/bz2_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/charmap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp037.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp1006.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp1026.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp1125.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp1140.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp1250.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp1251.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp1252.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp1253.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp1254.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp1255.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp1256.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp1257.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp1258.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp273.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp424.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp437.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp500.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp720.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp737.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp775.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp850.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp852.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp855.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp856.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp857.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp858.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp860.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp861.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp862.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp863.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp864.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp865.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp866.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp869.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp874.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp875.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp932.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp949.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/cp950.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/euc_jis_2004.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/euc_jisx0213.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/euc_jp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/euc_kr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/gb18030.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/gb2312.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/gbk.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/hex_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/hp_roman8.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/hz.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/idna.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso2022_jp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso2022_jp_1.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso2022_jp_2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso2022_jp_2004.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso2022_jp_3.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso2022_jp_ext.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso2022_kr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso8859_1.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso8859_10.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso8859_11.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso8859_13.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso8859_14.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso8859_15.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso8859_16.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso8859_2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso8859_3.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso8859_4.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso8859_5.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso8859_6.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso8859_7.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso8859_8.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/iso8859_9.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/johab.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/koi8_r.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/koi8_t.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/koi8_u.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/kz1048.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/latin_1.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/mac_arabic.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/mac_croatian.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/mac_cyrillic.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/mac_farsi.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/mac_greek.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/mac_iceland.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/mac_latin2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/mac_roman.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/mac_romanian.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/mac_turkish.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/mbcs.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/oem.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/palmos.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/ptcp154.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/punycode.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/quopri_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/raw_unicode_escape.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/rot_13.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/shift_jis.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/shift_jis_2004.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/shift_jisx0213.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/tis_620.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/undefined.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/unicode_escape.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/utf_16.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/utf_16_be.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/utf_16_le.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/utf_32.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/utf_32_be.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/utf_32_le.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/utf_7.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/utf_8.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/utf_8_sig.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/uu_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/encodings/__pycache__/zlib_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/enum.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/filecmp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/fileinput.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/fnmatch.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/fractions.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/ftplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/functools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/genericpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/getopt.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/getpass.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/gettext.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/glob.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/graphlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/gzip.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/hashlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/heapq.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/hmac.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/html/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/html/__pycache__/entities.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/html/__pycache__/parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/http/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/http/__pycache__/client.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/http/__pycache__/cookiejar.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/http/__pycache__/cookies.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/http/__pycache__/server.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/imghdr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/imp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/__pycache__/_abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/__pycache__/_bootstrap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/__pycache__/_bootstrap_external.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/__pycache__/machinery.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/metadata/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/metadata/__pycache__/_adapters.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/metadata/__pycache__/_collections.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/metadata/__pycache__/_functools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/metadata/__pycache__/_itertools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/metadata/__pycache__/_meta.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/metadata/__pycache__/_text.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/__pycache__/readers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/resources/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/resources/__pycache__/_adapters.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/resources/__pycache__/_common.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/resources/__pycache__/_itertools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/resources/__pycache__/_legacy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/resources/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/resources/__pycache__/readers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/resources/__pycache__/simple.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/__pycache__/simple.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/importlib/__pycache__/util.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/inspect.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/io.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/ipaddress.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/json/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/json/__pycache__/decoder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/json/__pycache__/encoder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/json/__pycache__/scanner.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/json/__pycache__/tool.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/keyword.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/linecache.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/locale.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/logging/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/logging/__pycache__/config.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/logging/__pycache__/handlers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/lzma.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/mailbox.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/mailcap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/mimetypes.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/modulefinder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/netrc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/nntplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/ntpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/nturl2path.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/numbers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/opcode.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/operator.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/optparse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/os.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/pathlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/pdb.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/pickle.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/pickletools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/pipes.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/pkgutil.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/platform.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/plistlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/poplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/posixpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/pprint.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/profile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/pstats.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/pty.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/py_compile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/pyclbr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/pydoc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/queue.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/quopri.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/random.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/re/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/re/__pycache__/_casefix.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/re/__pycache__/_compiler.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/re/__pycache__/_constants.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/re/__pycache__/_parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/reprlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/rlcompleter.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/runpy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/sched.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/secrets.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/selectors.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/shelve.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/shlex.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/shutil.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/signal.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/site.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/smtpd.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/smtplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/sndhdr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/socket.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/socketserver.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/sqlite3/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/sqlite3/__pycache__/dbapi2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/sqlite3/__pycache__/dump.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/sre_compile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/sre_constants.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/sre_parse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/ssl.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/stat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/statistics.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/string.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/stringprep.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/struct.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/subprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/sunau.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/symtable.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/sysconfig.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/tabnanny.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/tarfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/telnetlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/tempfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/textwrap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/this.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/threading.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/timeit.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/token.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/tokenize.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/tomllib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/tomllib/__pycache__/_parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/tomllib/__pycache__/_re.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/tomllib/__pycache__/_types.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/trace.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/traceback.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/tracemalloc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/tty.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/types.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/typing.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/urllib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/urllib/__pycache__/error.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/urllib/__pycache__/parse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/urllib/__pycache__/request.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/urllib/__pycache__/response.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/urllib/__pycache__/robotparser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/uu.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/uuid.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/warnings.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/wave.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/weakref.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/webbrowser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/xdrlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/dom/__pycache__/NodeFilter.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/dom/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/dom/__pycache__/domreg.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/dom/__pycache__/expatbuilder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/dom/__pycache__/minicompat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/dom/__pycache__/minidom.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/dom/__pycache__/pulldom.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/dom/__pycache__/xmlbuilder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/etree/__pycache__/ElementInclude.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/etree/__pycache__/ElementPath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/etree/__pycache__/ElementTree.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/etree/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/etree/__pycache__/cElementTree.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/parsers/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/parsers/__pycache__/expat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/sax/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/sax/__pycache__/_exceptions.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/sax/__pycache__/expatreader.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/sax/__pycache__/handler.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/sax/__pycache__/saxutils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xml/sax/__pycache__/xmlreader.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xmlrpc/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xmlrpc/__pycache__/client.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/xmlrpc/__pycache__/server.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/zipapp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/zipfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/__pycache__/zipimport.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/zoneinfo/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/zoneinfo/__pycache__/_common.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/zoneinfo/__pycache__/_tzpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/pylib-android/zoneinfo/__pycache__/_zoneinfo.cpython-311.opt-1.pyc + +# Rule to copy src asset scripts to dst. +# (and make non-writable so I'm less likely to accidentally edit them there) +$(SCRIPT_TARGETS_PY_PRIVATE_ANDROID) : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +# These are too complex to define in a pattern rule; +# Instead we generate individual targets in a loop. +$(foreach element,$(SCRIPT_TARGETS_PYC_PRIVATE_ANDROID),\ +$(eval $(call make-opt-pyc-target,$(element)))) + +SCRIPT_TARGETS_PY_PRIVATE_COMMON = \ + $(BUILD_DIR)/ba_data/python-site-packages/_yaml/__init__.py \ + $(BUILD_DIR)/ba_data/python-site-packages/certifi/__init__.py \ + $(BUILD_DIR)/ba_data/python-site-packages/certifi/__main__.py \ + $(BUILD_DIR)/ba_data/python-site-packages/certifi/core.py \ + $(BUILD_DIR)/ba_data/python-site-packages/typing_extensions.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__init__.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/composer.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/constructor.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/cyaml.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/dumper.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/emitter.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/error.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/events.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/loader.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/nodes.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/parser.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/reader.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/representer.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/resolver.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/scanner.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/serializer.py \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/tokens.py \ + $(BUILD_DIR)/mac_disk_image/dmgbuild_settings.py \ + $(BUILD_DIR)/workspace/ninjafightplug.py \ + $(BUILD_DIR)/workspace/onslaughtplug.py \ + $(BUILD_DIR)/workspace/runaroundplug.py + +SCRIPT_TARGETS_PYC_PRIVATE_COMMON = \ + $(BUILD_DIR)/ba_data/python-site-packages/_yaml/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/certifi/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/certifi/__pycache__/__main__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/certifi/__pycache__/core.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/__pycache__/typing_extensions.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/composer.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/constructor.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/cyaml.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/dumper.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/emitter.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/error.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/loader.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/nodes.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/reader.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/representer.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/resolver.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/scanner.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/serializer.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python-site-packages/yaml/__pycache__/tokens.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/mac_disk_image/__pycache__/dmgbuild_settings.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/workspace/__pycache__/ninjafightplug.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/workspace/__pycache__/onslaughtplug.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/workspace/__pycache__/runaroundplug.cpython-311.opt-1.pyc + +# Rule to copy src asset scripts to dst. +# (and make non-writable so I'm less likely to accidentally edit them there) +$(SCRIPT_TARGETS_PY_PRIVATE_COMMON) : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +# These are too complex to define in a pattern rule; +# Instead we generate individual targets in a loop. +$(foreach element,$(SCRIPT_TARGETS_PYC_PRIVATE_COMMON),\ +$(eval $(call make-opt-pyc-target,$(element)))) + +SCRIPT_TARGETS_PY_PRIVATE_WIN_WIN32 = \ + $(BUILD_DIR)/windows/Win32/Lib/__future__.py \ + $(BUILD_DIR)/windows/Win32/Lib/__hello__.py \ + $(BUILD_DIR)/windows/Win32/Lib/_aix_support.py \ + $(BUILD_DIR)/windows/Win32/Lib/_bootsubprocess.py \ + $(BUILD_DIR)/windows/Win32/Lib/_collections_abc.py \ + $(BUILD_DIR)/windows/Win32/Lib/_compat_pickle.py \ + $(BUILD_DIR)/windows/Win32/Lib/_compression.py \ + $(BUILD_DIR)/windows/Win32/Lib/_markupbase.py \ + $(BUILD_DIR)/windows/Win32/Lib/_osx_support.py \ + $(BUILD_DIR)/windows/Win32/Lib/_py_abc.py \ + $(BUILD_DIR)/windows/Win32/Lib/_pydecimal.py \ + $(BUILD_DIR)/windows/Win32/Lib/_pyio.py \ + $(BUILD_DIR)/windows/Win32/Lib/_sitebuiltins.py \ + $(BUILD_DIR)/windows/Win32/Lib/_strptime.py \ + $(BUILD_DIR)/windows/Win32/Lib/_threading_local.py \ + $(BUILD_DIR)/windows/Win32/Lib/_weakrefset.py \ + $(BUILD_DIR)/windows/Win32/Lib/abc.py \ + $(BUILD_DIR)/windows/Win32/Lib/aifc.py \ + $(BUILD_DIR)/windows/Win32/Lib/antigravity.py \ + $(BUILD_DIR)/windows/Win32/Lib/argparse.py \ + $(BUILD_DIR)/windows/Win32/Lib/ast.py \ + $(BUILD_DIR)/windows/Win32/Lib/asynchat.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__main__.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/base_events.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/base_futures.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/base_subprocess.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/base_tasks.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/constants.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/coroutines.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/events.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/exceptions.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/format_helpers.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/futures.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/locks.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/log.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/mixins.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/proactor_events.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/protocols.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/queues.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/runners.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/selector_events.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/sslproto.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/staggered.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/streams.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/subprocess.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/taskgroups.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/tasks.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/threads.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/timeouts.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/transports.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/trsock.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/unix_events.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/windows_events.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/windows_utils.py \ + $(BUILD_DIR)/windows/Win32/Lib/asyncore.py \ + $(BUILD_DIR)/windows/Win32/Lib/base64.py \ + $(BUILD_DIR)/windows/Win32/Lib/bdb.py \ + $(BUILD_DIR)/windows/Win32/Lib/bisect.py \ + $(BUILD_DIR)/windows/Win32/Lib/bz2.py \ + $(BUILD_DIR)/windows/Win32/Lib/cProfile.py \ + $(BUILD_DIR)/windows/Win32/Lib/calendar.py \ + $(BUILD_DIR)/windows/Win32/Lib/cgi.py \ + $(BUILD_DIR)/windows/Win32/Lib/cgitb.py \ + $(BUILD_DIR)/windows/Win32/Lib/chunk.py \ + $(BUILD_DIR)/windows/Win32/Lib/cmd.py \ + $(BUILD_DIR)/windows/Win32/Lib/code.py \ + $(BUILD_DIR)/windows/Win32/Lib/codecs.py \ + $(BUILD_DIR)/windows/Win32/Lib/codeop.py \ + $(BUILD_DIR)/windows/Win32/Lib/collections/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/collections/abc.py \ + $(BUILD_DIR)/windows/Win32/Lib/colorsys.py \ + $(BUILD_DIR)/windows/Win32/Lib/compileall.py \ + $(BUILD_DIR)/windows/Win32/Lib/concurrent/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/concurrent/futures/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/concurrent/futures/_base.py \ + $(BUILD_DIR)/windows/Win32/Lib/concurrent/futures/process.py \ + $(BUILD_DIR)/windows/Win32/Lib/concurrent/futures/thread.py \ + $(BUILD_DIR)/windows/Win32/Lib/configparser.py \ + $(BUILD_DIR)/windows/Win32/Lib/contextlib.py \ + $(BUILD_DIR)/windows/Win32/Lib/contextvars.py \ + $(BUILD_DIR)/windows/Win32/Lib/copy.py \ + $(BUILD_DIR)/windows/Win32/Lib/copyreg.py \ + $(BUILD_DIR)/windows/Win32/Lib/crypt.py \ + $(BUILD_DIR)/windows/Win32/Lib/csv.py \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/_aix.py \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/_endian.py \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/macholib/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/macholib/dyld.py \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/macholib/dylib.py \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/macholib/framework.py \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/util.py \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/wintypes.py \ + $(BUILD_DIR)/windows/Win32/Lib/curses/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/curses/ascii.py \ + $(BUILD_DIR)/windows/Win32/Lib/curses/has_key.py \ + $(BUILD_DIR)/windows/Win32/Lib/curses/panel.py \ + $(BUILD_DIR)/windows/Win32/Lib/curses/textpad.py \ + $(BUILD_DIR)/windows/Win32/Lib/dataclasses.py \ + $(BUILD_DIR)/windows/Win32/Lib/datetime.py \ + $(BUILD_DIR)/windows/Win32/Lib/decimal.py \ + $(BUILD_DIR)/windows/Win32/Lib/difflib.py \ + $(BUILD_DIR)/windows/Win32/Lib/dis.py \ + $(BUILD_DIR)/windows/Win32/Lib/doctest.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/_encoded_words.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/_header_value_parser.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/_parseaddr.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/_policybase.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/base64mime.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/charset.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/contentmanager.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/encoders.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/errors.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/feedparser.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/generator.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/header.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/headerregistry.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/iterators.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/message.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/application.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/audio.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/base.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/image.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/message.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/multipart.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/nonmultipart.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/text.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/parser.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/policy.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/quoprimime.py \ + $(BUILD_DIR)/windows/Win32/Lib/email/utils.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/aliases.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/ascii.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/base64_codec.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/big5.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/big5hkscs.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/bz2_codec.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/charmap.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp037.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp1006.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp1026.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp1125.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp1140.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp1250.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp1251.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp1252.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp1253.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp1254.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp1255.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp1256.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp1257.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp1258.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp273.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp424.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp437.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp500.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp720.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp737.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp775.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp850.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp852.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp855.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp856.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp857.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp858.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp860.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp861.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp862.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp863.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp864.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp865.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp866.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp869.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp874.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp875.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp932.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp949.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/cp950.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/euc_jis_2004.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/euc_jisx0213.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/euc_jp.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/euc_kr.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/gb18030.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/gb2312.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/gbk.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/hex_codec.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/hp_roman8.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/hz.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/idna.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso2022_jp.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso2022_jp_1.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso2022_jp_2.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso2022_jp_2004.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso2022_jp_3.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso2022_jp_ext.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso2022_kr.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso8859_1.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso8859_10.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso8859_11.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso8859_13.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso8859_14.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso8859_15.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso8859_16.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso8859_2.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso8859_3.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso8859_4.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso8859_5.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso8859_6.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso8859_7.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso8859_8.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/iso8859_9.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/johab.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/koi8_r.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/koi8_t.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/koi8_u.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/kz1048.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/latin_1.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/mac_arabic.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/mac_croatian.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/mac_cyrillic.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/mac_farsi.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/mac_greek.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/mac_iceland.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/mac_latin2.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/mac_roman.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/mac_romanian.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/mac_turkish.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/mbcs.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/oem.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/palmos.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/ptcp154.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/punycode.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/quopri_codec.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/raw_unicode_escape.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/rot_13.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/shift_jis.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/shift_jis_2004.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/shift_jisx0213.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/tis_620.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/undefined.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/unicode_escape.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/utf_16.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/utf_16_be.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/utf_16_le.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/utf_32.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/utf_32_be.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/utf_32_le.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/utf_7.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/utf_8.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/utf_8_sig.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/uu_codec.py \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/zlib_codec.py \ + $(BUILD_DIR)/windows/Win32/Lib/enum.py \ + $(BUILD_DIR)/windows/Win32/Lib/filecmp.py \ + $(BUILD_DIR)/windows/Win32/Lib/fileinput.py \ + $(BUILD_DIR)/windows/Win32/Lib/fnmatch.py \ + $(BUILD_DIR)/windows/Win32/Lib/fractions.py \ + $(BUILD_DIR)/windows/Win32/Lib/ftplib.py \ + $(BUILD_DIR)/windows/Win32/Lib/functools.py \ + $(BUILD_DIR)/windows/Win32/Lib/genericpath.py \ + $(BUILD_DIR)/windows/Win32/Lib/getopt.py \ + $(BUILD_DIR)/windows/Win32/Lib/getpass.py \ + $(BUILD_DIR)/windows/Win32/Lib/gettext.py \ + $(BUILD_DIR)/windows/Win32/Lib/glob.py \ + $(BUILD_DIR)/windows/Win32/Lib/graphlib.py \ + $(BUILD_DIR)/windows/Win32/Lib/gzip.py \ + $(BUILD_DIR)/windows/Win32/Lib/hashlib.py \ + $(BUILD_DIR)/windows/Win32/Lib/heapq.py \ + $(BUILD_DIR)/windows/Win32/Lib/hmac.py \ + $(BUILD_DIR)/windows/Win32/Lib/html/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/html/entities.py \ + $(BUILD_DIR)/windows/Win32/Lib/html/parser.py \ + $(BUILD_DIR)/windows/Win32/Lib/http/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/http/client.py \ + $(BUILD_DIR)/windows/Win32/Lib/http/cookiejar.py \ + $(BUILD_DIR)/windows/Win32/Lib/http/cookies.py \ + $(BUILD_DIR)/windows/Win32/Lib/http/server.py \ + $(BUILD_DIR)/windows/Win32/Lib/imghdr.py \ + $(BUILD_DIR)/windows/Win32/Lib/imp.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/_abc.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/_bootstrap.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/_bootstrap_external.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/abc.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/machinery.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/metadata/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/metadata/_adapters.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/metadata/_collections.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/metadata/_functools.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/metadata/_itertools.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/metadata/_meta.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/metadata/_text.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/readers.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/_adapters.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/_common.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/_itertools.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/_legacy.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/abc.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/readers.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/simple.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/simple.py \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/util.py \ + $(BUILD_DIR)/windows/Win32/Lib/inspect.py \ + $(BUILD_DIR)/windows/Win32/Lib/io.py \ + $(BUILD_DIR)/windows/Win32/Lib/ipaddress.py \ + $(BUILD_DIR)/windows/Win32/Lib/json/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/json/decoder.py \ + $(BUILD_DIR)/windows/Win32/Lib/json/encoder.py \ + $(BUILD_DIR)/windows/Win32/Lib/json/scanner.py \ + $(BUILD_DIR)/windows/Win32/Lib/json/tool.py \ + $(BUILD_DIR)/windows/Win32/Lib/keyword.py \ + $(BUILD_DIR)/windows/Win32/Lib/linecache.py \ + $(BUILD_DIR)/windows/Win32/Lib/locale.py \ + $(BUILD_DIR)/windows/Win32/Lib/logging/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/logging/config.py \ + $(BUILD_DIR)/windows/Win32/Lib/logging/handlers.py \ + $(BUILD_DIR)/windows/Win32/Lib/lzma.py \ + $(BUILD_DIR)/windows/Win32/Lib/mailbox.py \ + $(BUILD_DIR)/windows/Win32/Lib/mailcap.py \ + $(BUILD_DIR)/windows/Win32/Lib/mimetypes.py \ + $(BUILD_DIR)/windows/Win32/Lib/modulefinder.py \ + $(BUILD_DIR)/windows/Win32/Lib/netrc.py \ + $(BUILD_DIR)/windows/Win32/Lib/nntplib.py \ + $(BUILD_DIR)/windows/Win32/Lib/ntpath.py \ + $(BUILD_DIR)/windows/Win32/Lib/nturl2path.py \ + $(BUILD_DIR)/windows/Win32/Lib/numbers.py \ + $(BUILD_DIR)/windows/Win32/Lib/opcode.py \ + $(BUILD_DIR)/windows/Win32/Lib/operator.py \ + $(BUILD_DIR)/windows/Win32/Lib/optparse.py \ + $(BUILD_DIR)/windows/Win32/Lib/os.py \ + $(BUILD_DIR)/windows/Win32/Lib/pathlib.py \ + $(BUILD_DIR)/windows/Win32/Lib/pdb.py \ + $(BUILD_DIR)/windows/Win32/Lib/pickle.py \ + $(BUILD_DIR)/windows/Win32/Lib/pickletools.py \ + $(BUILD_DIR)/windows/Win32/Lib/pipes.py \ + $(BUILD_DIR)/windows/Win32/Lib/pkgutil.py \ + $(BUILD_DIR)/windows/Win32/Lib/platform.py \ + $(BUILD_DIR)/windows/Win32/Lib/plistlib.py \ + $(BUILD_DIR)/windows/Win32/Lib/poplib.py \ + $(BUILD_DIR)/windows/Win32/Lib/posixpath.py \ + $(BUILD_DIR)/windows/Win32/Lib/pprint.py \ + $(BUILD_DIR)/windows/Win32/Lib/profile.py \ + $(BUILD_DIR)/windows/Win32/Lib/pstats.py \ + $(BUILD_DIR)/windows/Win32/Lib/pty.py \ + $(BUILD_DIR)/windows/Win32/Lib/py_compile.py \ + $(BUILD_DIR)/windows/Win32/Lib/pyclbr.py \ + $(BUILD_DIR)/windows/Win32/Lib/pydoc.py \ + $(BUILD_DIR)/windows/Win32/Lib/queue.py \ + $(BUILD_DIR)/windows/Win32/Lib/quopri.py \ + $(BUILD_DIR)/windows/Win32/Lib/random.py \ + $(BUILD_DIR)/windows/Win32/Lib/re/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/re/_casefix.py \ + $(BUILD_DIR)/windows/Win32/Lib/re/_compiler.py \ + $(BUILD_DIR)/windows/Win32/Lib/re/_constants.py \ + $(BUILD_DIR)/windows/Win32/Lib/re/_parser.py \ + $(BUILD_DIR)/windows/Win32/Lib/reprlib.py \ + $(BUILD_DIR)/windows/Win32/Lib/rlcompleter.py \ + $(BUILD_DIR)/windows/Win32/Lib/runpy.py \ + $(BUILD_DIR)/windows/Win32/Lib/sched.py \ + $(BUILD_DIR)/windows/Win32/Lib/secrets.py \ + $(BUILD_DIR)/windows/Win32/Lib/selectors.py \ + $(BUILD_DIR)/windows/Win32/Lib/shelve.py \ + $(BUILD_DIR)/windows/Win32/Lib/shlex.py \ + $(BUILD_DIR)/windows/Win32/Lib/shutil.py \ + $(BUILD_DIR)/windows/Win32/Lib/signal.py \ + $(BUILD_DIR)/windows/Win32/Lib/site.py \ + $(BUILD_DIR)/windows/Win32/Lib/smtpd.py \ + $(BUILD_DIR)/windows/Win32/Lib/smtplib.py \ + $(BUILD_DIR)/windows/Win32/Lib/sndhdr.py \ + $(BUILD_DIR)/windows/Win32/Lib/socket.py \ + $(BUILD_DIR)/windows/Win32/Lib/socketserver.py \ + $(BUILD_DIR)/windows/Win32/Lib/sqlite3/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/sqlite3/dbapi2.py \ + $(BUILD_DIR)/windows/Win32/Lib/sqlite3/dump.py \ + $(BUILD_DIR)/windows/Win32/Lib/sre_compile.py \ + $(BUILD_DIR)/windows/Win32/Lib/sre_constants.py \ + $(BUILD_DIR)/windows/Win32/Lib/sre_parse.py \ + $(BUILD_DIR)/windows/Win32/Lib/ssl.py \ + $(BUILD_DIR)/windows/Win32/Lib/stat.py \ + $(BUILD_DIR)/windows/Win32/Lib/statistics.py \ + $(BUILD_DIR)/windows/Win32/Lib/string.py \ + $(BUILD_DIR)/windows/Win32/Lib/stringprep.py \ + $(BUILD_DIR)/windows/Win32/Lib/struct.py \ + $(BUILD_DIR)/windows/Win32/Lib/subprocess.py \ + $(BUILD_DIR)/windows/Win32/Lib/sunau.py \ + $(BUILD_DIR)/windows/Win32/Lib/symtable.py \ + $(BUILD_DIR)/windows/Win32/Lib/sysconfig.py \ + $(BUILD_DIR)/windows/Win32/Lib/tabnanny.py \ + $(BUILD_DIR)/windows/Win32/Lib/tarfile.py \ + $(BUILD_DIR)/windows/Win32/Lib/telnetlib.py \ + $(BUILD_DIR)/windows/Win32/Lib/tempfile.py \ + $(BUILD_DIR)/windows/Win32/Lib/textwrap.py \ + $(BUILD_DIR)/windows/Win32/Lib/this.py \ + $(BUILD_DIR)/windows/Win32/Lib/threading.py \ + $(BUILD_DIR)/windows/Win32/Lib/timeit.py \ + $(BUILD_DIR)/windows/Win32/Lib/token.py \ + $(BUILD_DIR)/windows/Win32/Lib/tokenize.py \ + $(BUILD_DIR)/windows/Win32/Lib/tomllib/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/tomllib/_parser.py \ + $(BUILD_DIR)/windows/Win32/Lib/tomllib/_re.py \ + $(BUILD_DIR)/windows/Win32/Lib/tomllib/_types.py \ + $(BUILD_DIR)/windows/Win32/Lib/trace.py \ + $(BUILD_DIR)/windows/Win32/Lib/traceback.py \ + $(BUILD_DIR)/windows/Win32/Lib/tracemalloc.py \ + $(BUILD_DIR)/windows/Win32/Lib/tty.py \ + $(BUILD_DIR)/windows/Win32/Lib/types.py \ + $(BUILD_DIR)/windows/Win32/Lib/typing.py \ + $(BUILD_DIR)/windows/Win32/Lib/urllib/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/urllib/error.py \ + $(BUILD_DIR)/windows/Win32/Lib/urllib/parse.py \ + $(BUILD_DIR)/windows/Win32/Lib/urllib/request.py \ + $(BUILD_DIR)/windows/Win32/Lib/urllib/response.py \ + $(BUILD_DIR)/windows/Win32/Lib/urllib/robotparser.py \ + $(BUILD_DIR)/windows/Win32/Lib/uu.py \ + $(BUILD_DIR)/windows/Win32/Lib/uuid.py \ + $(BUILD_DIR)/windows/Win32/Lib/warnings.py \ + $(BUILD_DIR)/windows/Win32/Lib/wave.py \ + $(BUILD_DIR)/windows/Win32/Lib/weakref.py \ + $(BUILD_DIR)/windows/Win32/Lib/webbrowser.py \ + $(BUILD_DIR)/windows/Win32/Lib/xdrlib.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/NodeFilter.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/domreg.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/expatbuilder.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/minicompat.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/minidom.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/pulldom.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/xmlbuilder.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/etree/ElementInclude.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/etree/ElementPath.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/etree/ElementTree.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/etree/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/etree/cElementTree.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/parsers/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/parsers/expat.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/sax/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/sax/_exceptions.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/sax/expatreader.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/sax/handler.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/sax/saxutils.py \ + $(BUILD_DIR)/windows/Win32/Lib/xml/sax/xmlreader.py \ + $(BUILD_DIR)/windows/Win32/Lib/xmlrpc/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/xmlrpc/client.py \ + $(BUILD_DIR)/windows/Win32/Lib/xmlrpc/server.py \ + $(BUILD_DIR)/windows/Win32/Lib/zipapp.py \ + $(BUILD_DIR)/windows/Win32/Lib/zipfile.py \ + $(BUILD_DIR)/windows/Win32/Lib/zipimport.py \ + $(BUILD_DIR)/windows/Win32/Lib/zoneinfo/__init__.py \ + $(BUILD_DIR)/windows/Win32/Lib/zoneinfo/_common.py \ + $(BUILD_DIR)/windows/Win32/Lib/zoneinfo/_tzpath.py \ + $(BUILD_DIR)/windows/Win32/Lib/zoneinfo/_zoneinfo.py + +SCRIPT_TARGETS_PYC_PRIVATE_WIN_WIN32 = \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/__future__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/__hello__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/_aix_support.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/_bootsubprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/_collections_abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/_compat_pickle.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/_compression.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/_markupbase.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/_osx_support.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/_py_abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/_pydecimal.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/_pyio.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/_sitebuiltins.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/_strptime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/_threading_local.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/_weakrefset.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/aifc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/antigravity.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/argparse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/ast.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/asynchat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/__main__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/base_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/base_futures.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/base_subprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/base_tasks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/constants.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/coroutines.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/exceptions.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/format_helpers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/futures.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/locks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/log.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/mixins.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/proactor_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/protocols.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/queues.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/runners.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/selector_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/sslproto.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/staggered.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/streams.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/subprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/taskgroups.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/tasks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/threads.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/timeouts.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/transports.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/trsock.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/unix_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/windows_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/asyncio/__pycache__/windows_utils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/asyncore.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/base64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/bdb.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/bisect.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/bz2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/cProfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/calendar.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/cgi.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/cgitb.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/chunk.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/cmd.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/code.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/codecs.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/codeop.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/collections/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/collections/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/colorsys.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/compileall.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/concurrent/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/concurrent/futures/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/concurrent/futures/__pycache__/_base.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/concurrent/futures/__pycache__/process.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/concurrent/futures/__pycache__/thread.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/configparser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/contextlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/contextvars.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/copy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/copyreg.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/crypt.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/csv.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/__pycache__/_aix.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/__pycache__/_endian.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/macholib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/macholib/__pycache__/dyld.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/macholib/__pycache__/dylib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/macholib/__pycache__/framework.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/__pycache__/util.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/ctypes/__pycache__/wintypes.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/curses/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/curses/__pycache__/ascii.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/curses/__pycache__/has_key.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/curses/__pycache__/panel.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/curses/__pycache__/textpad.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/dataclasses.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/datetime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/decimal.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/difflib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/dis.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/doctest.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/_encoded_words.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/_header_value_parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/_parseaddr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/_policybase.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/base64mime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/charset.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/contentmanager.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/encoders.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/errors.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/feedparser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/generator.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/header.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/headerregistry.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/iterators.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/message.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/__pycache__/application.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/__pycache__/audio.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/__pycache__/base.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/__pycache__/image.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/__pycache__/message.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/__pycache__/multipart.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/__pycache__/nonmultipart.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/mime/__pycache__/text.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/policy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/quoprimime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/email/__pycache__/utils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/aliases.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/ascii.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/base64_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/big5.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/big5hkscs.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/bz2_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/charmap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp037.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp1006.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp1026.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp1125.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp1140.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp1250.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp1251.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp1252.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp1253.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp1254.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp1255.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp1256.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp1257.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp1258.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp273.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp424.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp437.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp500.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp720.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp737.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp775.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp850.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp852.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp855.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp856.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp857.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp858.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp860.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp861.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp862.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp863.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp864.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp865.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp866.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp869.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp874.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp875.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp932.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp949.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/cp950.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/euc_jis_2004.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/euc_jisx0213.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/euc_jp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/euc_kr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/gb18030.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/gb2312.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/gbk.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/hex_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/hp_roman8.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/hz.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/idna.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso2022_jp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso2022_jp_1.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso2022_jp_2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso2022_jp_2004.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso2022_jp_3.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso2022_jp_ext.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso2022_kr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso8859_1.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso8859_10.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso8859_11.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso8859_13.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso8859_14.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso8859_15.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso8859_16.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso8859_2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso8859_3.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso8859_4.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso8859_5.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso8859_6.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso8859_7.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso8859_8.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/iso8859_9.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/johab.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/koi8_r.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/koi8_t.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/koi8_u.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/kz1048.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/latin_1.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/mac_arabic.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/mac_croatian.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/mac_cyrillic.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/mac_farsi.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/mac_greek.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/mac_iceland.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/mac_latin2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/mac_roman.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/mac_romanian.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/mac_turkish.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/mbcs.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/oem.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/palmos.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/ptcp154.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/punycode.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/quopri_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/raw_unicode_escape.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/rot_13.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/shift_jis.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/shift_jis_2004.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/shift_jisx0213.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/tis_620.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/undefined.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/unicode_escape.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/utf_16.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/utf_16_be.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/utf_16_le.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/utf_32.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/utf_32_be.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/utf_32_le.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/utf_7.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/utf_8.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/utf_8_sig.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/uu_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/encodings/__pycache__/zlib_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/enum.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/filecmp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/fileinput.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/fnmatch.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/fractions.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/ftplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/functools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/genericpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/getopt.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/getpass.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/gettext.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/glob.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/graphlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/gzip.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/hashlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/heapq.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/hmac.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/html/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/html/__pycache__/entities.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/html/__pycache__/parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/http/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/http/__pycache__/client.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/http/__pycache__/cookiejar.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/http/__pycache__/cookies.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/http/__pycache__/server.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/imghdr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/imp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/__pycache__/_abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/__pycache__/_bootstrap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/__pycache__/_bootstrap_external.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/__pycache__/machinery.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/metadata/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/metadata/__pycache__/_adapters.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/metadata/__pycache__/_collections.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/metadata/__pycache__/_functools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/metadata/__pycache__/_itertools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/metadata/__pycache__/_meta.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/metadata/__pycache__/_text.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/__pycache__/readers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/__pycache__/_adapters.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/__pycache__/_common.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/__pycache__/_itertools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/__pycache__/_legacy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/__pycache__/readers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/resources/__pycache__/simple.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/__pycache__/simple.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/importlib/__pycache__/util.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/inspect.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/io.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/ipaddress.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/json/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/json/__pycache__/decoder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/json/__pycache__/encoder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/json/__pycache__/scanner.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/json/__pycache__/tool.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/keyword.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/linecache.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/locale.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/logging/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/logging/__pycache__/config.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/logging/__pycache__/handlers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/lzma.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/mailbox.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/mailcap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/mimetypes.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/modulefinder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/netrc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/nntplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/ntpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/nturl2path.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/numbers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/opcode.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/operator.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/optparse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/os.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/pathlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/pdb.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/pickle.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/pickletools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/pipes.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/pkgutil.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/platform.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/plistlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/poplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/posixpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/pprint.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/profile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/pstats.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/pty.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/py_compile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/pyclbr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/pydoc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/queue.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/quopri.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/random.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/re/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/re/__pycache__/_casefix.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/re/__pycache__/_compiler.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/re/__pycache__/_constants.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/re/__pycache__/_parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/reprlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/rlcompleter.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/runpy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/sched.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/secrets.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/selectors.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/shelve.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/shlex.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/shutil.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/signal.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/site.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/smtpd.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/smtplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/sndhdr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/socket.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/socketserver.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/sqlite3/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/sqlite3/__pycache__/dbapi2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/sqlite3/__pycache__/dump.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/sre_compile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/sre_constants.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/sre_parse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/ssl.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/stat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/statistics.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/string.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/stringprep.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/struct.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/subprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/sunau.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/symtable.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/sysconfig.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/tabnanny.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/tarfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/telnetlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/tempfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/textwrap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/this.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/threading.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/timeit.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/token.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/tokenize.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/tomllib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/tomllib/__pycache__/_parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/tomllib/__pycache__/_re.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/tomllib/__pycache__/_types.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/trace.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/traceback.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/tracemalloc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/tty.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/types.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/typing.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/urllib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/urllib/__pycache__/error.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/urllib/__pycache__/parse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/urllib/__pycache__/request.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/urllib/__pycache__/response.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/urllib/__pycache__/robotparser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/uu.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/uuid.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/warnings.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/wave.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/weakref.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/webbrowser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/xdrlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/__pycache__/NodeFilter.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/__pycache__/domreg.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/__pycache__/expatbuilder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/__pycache__/minicompat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/__pycache__/minidom.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/__pycache__/pulldom.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/dom/__pycache__/xmlbuilder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/etree/__pycache__/ElementInclude.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/etree/__pycache__/ElementPath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/etree/__pycache__/ElementTree.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/etree/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/etree/__pycache__/cElementTree.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/parsers/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/parsers/__pycache__/expat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/sax/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/sax/__pycache__/_exceptions.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/sax/__pycache__/expatreader.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/sax/__pycache__/handler.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/sax/__pycache__/saxutils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xml/sax/__pycache__/xmlreader.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xmlrpc/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xmlrpc/__pycache__/client.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/xmlrpc/__pycache__/server.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/zipapp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/zipfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/__pycache__/zipimport.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/zoneinfo/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/zoneinfo/__pycache__/_common.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/zoneinfo/__pycache__/_tzpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/Win32/Lib/zoneinfo/__pycache__/_zoneinfo.cpython-311.opt-1.pyc + +# Rule to copy src asset scripts to dst. +# (and make non-writable so I'm less likely to accidentally edit them there) +$(SCRIPT_TARGETS_PY_PRIVATE_WIN_WIN32) : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +# These are too complex to define in a pattern rule; +# Instead we generate individual targets in a loop. +$(foreach element,$(SCRIPT_TARGETS_PYC_PRIVATE_WIN_WIN32),\ +$(eval $(call make-opt-pyc-target,$(element)))) + +SCRIPT_TARGETS_PY_PRIVATE_WIN_X64 = \ + $(BUILD_DIR)/windows/x64/Lib/__future__.py \ + $(BUILD_DIR)/windows/x64/Lib/__hello__.py \ + $(BUILD_DIR)/windows/x64/Lib/_aix_support.py \ + $(BUILD_DIR)/windows/x64/Lib/_bootsubprocess.py \ + $(BUILD_DIR)/windows/x64/Lib/_collections_abc.py \ + $(BUILD_DIR)/windows/x64/Lib/_compat_pickle.py \ + $(BUILD_DIR)/windows/x64/Lib/_compression.py \ + $(BUILD_DIR)/windows/x64/Lib/_markupbase.py \ + $(BUILD_DIR)/windows/x64/Lib/_osx_support.py \ + $(BUILD_DIR)/windows/x64/Lib/_py_abc.py \ + $(BUILD_DIR)/windows/x64/Lib/_pydecimal.py \ + $(BUILD_DIR)/windows/x64/Lib/_pyio.py \ + $(BUILD_DIR)/windows/x64/Lib/_sitebuiltins.py \ + $(BUILD_DIR)/windows/x64/Lib/_strptime.py \ + $(BUILD_DIR)/windows/x64/Lib/_threading_local.py \ + $(BUILD_DIR)/windows/x64/Lib/_weakrefset.py \ + $(BUILD_DIR)/windows/x64/Lib/abc.py \ + $(BUILD_DIR)/windows/x64/Lib/aifc.py \ + $(BUILD_DIR)/windows/x64/Lib/antigravity.py \ + $(BUILD_DIR)/windows/x64/Lib/argparse.py \ + $(BUILD_DIR)/windows/x64/Lib/ast.py \ + $(BUILD_DIR)/windows/x64/Lib/asynchat.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__main__.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/base_events.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/base_futures.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/base_subprocess.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/base_tasks.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/constants.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/coroutines.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/events.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/exceptions.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/format_helpers.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/futures.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/locks.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/log.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/mixins.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/proactor_events.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/protocols.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/queues.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/runners.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/selector_events.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/sslproto.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/staggered.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/streams.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/subprocess.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/taskgroups.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/tasks.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/threads.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/timeouts.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/transports.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/trsock.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/unix_events.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/windows_events.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/windows_utils.py \ + $(BUILD_DIR)/windows/x64/Lib/asyncore.py \ + $(BUILD_DIR)/windows/x64/Lib/base64.py \ + $(BUILD_DIR)/windows/x64/Lib/bdb.py \ + $(BUILD_DIR)/windows/x64/Lib/bisect.py \ + $(BUILD_DIR)/windows/x64/Lib/bz2.py \ + $(BUILD_DIR)/windows/x64/Lib/cProfile.py \ + $(BUILD_DIR)/windows/x64/Lib/calendar.py \ + $(BUILD_DIR)/windows/x64/Lib/cgi.py \ + $(BUILD_DIR)/windows/x64/Lib/cgitb.py \ + $(BUILD_DIR)/windows/x64/Lib/chunk.py \ + $(BUILD_DIR)/windows/x64/Lib/cmd.py \ + $(BUILD_DIR)/windows/x64/Lib/code.py \ + $(BUILD_DIR)/windows/x64/Lib/codecs.py \ + $(BUILD_DIR)/windows/x64/Lib/codeop.py \ + $(BUILD_DIR)/windows/x64/Lib/collections/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/collections/abc.py \ + $(BUILD_DIR)/windows/x64/Lib/colorsys.py \ + $(BUILD_DIR)/windows/x64/Lib/compileall.py \ + $(BUILD_DIR)/windows/x64/Lib/concurrent/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/concurrent/futures/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/concurrent/futures/_base.py \ + $(BUILD_DIR)/windows/x64/Lib/concurrent/futures/process.py \ + $(BUILD_DIR)/windows/x64/Lib/concurrent/futures/thread.py \ + $(BUILD_DIR)/windows/x64/Lib/configparser.py \ + $(BUILD_DIR)/windows/x64/Lib/contextlib.py \ + $(BUILD_DIR)/windows/x64/Lib/contextvars.py \ + $(BUILD_DIR)/windows/x64/Lib/copy.py \ + $(BUILD_DIR)/windows/x64/Lib/copyreg.py \ + $(BUILD_DIR)/windows/x64/Lib/crypt.py \ + $(BUILD_DIR)/windows/x64/Lib/csv.py \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/_aix.py \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/_endian.py \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/macholib/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/macholib/dyld.py \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/macholib/dylib.py \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/macholib/framework.py \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/util.py \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/wintypes.py \ + $(BUILD_DIR)/windows/x64/Lib/curses/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/curses/ascii.py \ + $(BUILD_DIR)/windows/x64/Lib/curses/has_key.py \ + $(BUILD_DIR)/windows/x64/Lib/curses/panel.py \ + $(BUILD_DIR)/windows/x64/Lib/curses/textpad.py \ + $(BUILD_DIR)/windows/x64/Lib/dataclasses.py \ + $(BUILD_DIR)/windows/x64/Lib/datetime.py \ + $(BUILD_DIR)/windows/x64/Lib/decimal.py \ + $(BUILD_DIR)/windows/x64/Lib/difflib.py \ + $(BUILD_DIR)/windows/x64/Lib/dis.py \ + $(BUILD_DIR)/windows/x64/Lib/doctest.py \ + $(BUILD_DIR)/windows/x64/Lib/email/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/email/_encoded_words.py \ + $(BUILD_DIR)/windows/x64/Lib/email/_header_value_parser.py \ + $(BUILD_DIR)/windows/x64/Lib/email/_parseaddr.py \ + $(BUILD_DIR)/windows/x64/Lib/email/_policybase.py \ + $(BUILD_DIR)/windows/x64/Lib/email/base64mime.py \ + $(BUILD_DIR)/windows/x64/Lib/email/charset.py \ + $(BUILD_DIR)/windows/x64/Lib/email/contentmanager.py \ + $(BUILD_DIR)/windows/x64/Lib/email/encoders.py \ + $(BUILD_DIR)/windows/x64/Lib/email/errors.py \ + $(BUILD_DIR)/windows/x64/Lib/email/feedparser.py \ + $(BUILD_DIR)/windows/x64/Lib/email/generator.py \ + $(BUILD_DIR)/windows/x64/Lib/email/header.py \ + $(BUILD_DIR)/windows/x64/Lib/email/headerregistry.py \ + $(BUILD_DIR)/windows/x64/Lib/email/iterators.py \ + $(BUILD_DIR)/windows/x64/Lib/email/message.py \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/application.py \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/audio.py \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/base.py \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/image.py \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/message.py \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/multipart.py \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/nonmultipart.py \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/text.py \ + $(BUILD_DIR)/windows/x64/Lib/email/parser.py \ + $(BUILD_DIR)/windows/x64/Lib/email/policy.py \ + $(BUILD_DIR)/windows/x64/Lib/email/quoprimime.py \ + $(BUILD_DIR)/windows/x64/Lib/email/utils.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/aliases.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/ascii.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/base64_codec.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/big5.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/big5hkscs.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/bz2_codec.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/charmap.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp037.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp1006.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp1026.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp1125.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp1140.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp1250.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp1251.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp1252.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp1253.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp1254.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp1255.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp1256.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp1257.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp1258.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp273.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp424.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp437.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp500.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp720.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp737.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp775.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp850.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp852.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp855.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp856.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp857.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp858.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp860.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp861.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp862.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp863.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp864.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp865.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp866.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp869.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp874.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp875.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp932.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp949.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/cp950.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/euc_jis_2004.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/euc_jisx0213.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/euc_jp.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/euc_kr.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/gb18030.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/gb2312.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/gbk.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/hex_codec.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/hp_roman8.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/hz.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/idna.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso2022_jp.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso2022_jp_1.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso2022_jp_2.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso2022_jp_2004.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso2022_jp_3.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso2022_jp_ext.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso2022_kr.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso8859_1.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso8859_10.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso8859_11.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso8859_13.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso8859_14.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso8859_15.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso8859_16.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso8859_2.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso8859_3.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso8859_4.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso8859_5.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso8859_6.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso8859_7.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso8859_8.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/iso8859_9.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/johab.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/koi8_r.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/koi8_t.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/koi8_u.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/kz1048.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/latin_1.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/mac_arabic.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/mac_croatian.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/mac_cyrillic.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/mac_farsi.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/mac_greek.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/mac_iceland.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/mac_latin2.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/mac_roman.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/mac_romanian.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/mac_turkish.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/mbcs.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/oem.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/palmos.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/ptcp154.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/punycode.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/quopri_codec.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/raw_unicode_escape.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/rot_13.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/shift_jis.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/shift_jis_2004.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/shift_jisx0213.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/tis_620.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/undefined.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/unicode_escape.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/utf_16.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/utf_16_be.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/utf_16_le.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/utf_32.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/utf_32_be.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/utf_32_le.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/utf_7.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/utf_8.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/utf_8_sig.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/uu_codec.py \ + $(BUILD_DIR)/windows/x64/Lib/encodings/zlib_codec.py \ + $(BUILD_DIR)/windows/x64/Lib/enum.py \ + $(BUILD_DIR)/windows/x64/Lib/filecmp.py \ + $(BUILD_DIR)/windows/x64/Lib/fileinput.py \ + $(BUILD_DIR)/windows/x64/Lib/fnmatch.py \ + $(BUILD_DIR)/windows/x64/Lib/fractions.py \ + $(BUILD_DIR)/windows/x64/Lib/ftplib.py \ + $(BUILD_DIR)/windows/x64/Lib/functools.py \ + $(BUILD_DIR)/windows/x64/Lib/genericpath.py \ + $(BUILD_DIR)/windows/x64/Lib/getopt.py \ + $(BUILD_DIR)/windows/x64/Lib/getpass.py \ + $(BUILD_DIR)/windows/x64/Lib/gettext.py \ + $(BUILD_DIR)/windows/x64/Lib/glob.py \ + $(BUILD_DIR)/windows/x64/Lib/graphlib.py \ + $(BUILD_DIR)/windows/x64/Lib/gzip.py \ + $(BUILD_DIR)/windows/x64/Lib/hashlib.py \ + $(BUILD_DIR)/windows/x64/Lib/heapq.py \ + $(BUILD_DIR)/windows/x64/Lib/hmac.py \ + $(BUILD_DIR)/windows/x64/Lib/html/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/html/entities.py \ + $(BUILD_DIR)/windows/x64/Lib/html/parser.py \ + $(BUILD_DIR)/windows/x64/Lib/http/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/http/client.py \ + $(BUILD_DIR)/windows/x64/Lib/http/cookiejar.py \ + $(BUILD_DIR)/windows/x64/Lib/http/cookies.py \ + $(BUILD_DIR)/windows/x64/Lib/http/server.py \ + $(BUILD_DIR)/windows/x64/Lib/imghdr.py \ + $(BUILD_DIR)/windows/x64/Lib/imp.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/_abc.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/_bootstrap.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/_bootstrap_external.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/abc.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/machinery.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/metadata/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/metadata/_adapters.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/metadata/_collections.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/metadata/_functools.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/metadata/_itertools.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/metadata/_meta.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/metadata/_text.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/readers.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/_adapters.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/_common.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/_itertools.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/_legacy.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/abc.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/readers.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/simple.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/simple.py \ + $(BUILD_DIR)/windows/x64/Lib/importlib/util.py \ + $(BUILD_DIR)/windows/x64/Lib/inspect.py \ + $(BUILD_DIR)/windows/x64/Lib/io.py \ + $(BUILD_DIR)/windows/x64/Lib/ipaddress.py \ + $(BUILD_DIR)/windows/x64/Lib/json/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/json/decoder.py \ + $(BUILD_DIR)/windows/x64/Lib/json/encoder.py \ + $(BUILD_DIR)/windows/x64/Lib/json/scanner.py \ + $(BUILD_DIR)/windows/x64/Lib/json/tool.py \ + $(BUILD_DIR)/windows/x64/Lib/keyword.py \ + $(BUILD_DIR)/windows/x64/Lib/linecache.py \ + $(BUILD_DIR)/windows/x64/Lib/locale.py \ + $(BUILD_DIR)/windows/x64/Lib/logging/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/logging/config.py \ + $(BUILD_DIR)/windows/x64/Lib/logging/handlers.py \ + $(BUILD_DIR)/windows/x64/Lib/lzma.py \ + $(BUILD_DIR)/windows/x64/Lib/mailbox.py \ + $(BUILD_DIR)/windows/x64/Lib/mailcap.py \ + $(BUILD_DIR)/windows/x64/Lib/mimetypes.py \ + $(BUILD_DIR)/windows/x64/Lib/modulefinder.py \ + $(BUILD_DIR)/windows/x64/Lib/netrc.py \ + $(BUILD_DIR)/windows/x64/Lib/nntplib.py \ + $(BUILD_DIR)/windows/x64/Lib/ntpath.py \ + $(BUILD_DIR)/windows/x64/Lib/nturl2path.py \ + $(BUILD_DIR)/windows/x64/Lib/numbers.py \ + $(BUILD_DIR)/windows/x64/Lib/opcode.py \ + $(BUILD_DIR)/windows/x64/Lib/operator.py \ + $(BUILD_DIR)/windows/x64/Lib/optparse.py \ + $(BUILD_DIR)/windows/x64/Lib/os.py \ + $(BUILD_DIR)/windows/x64/Lib/pathlib.py \ + $(BUILD_DIR)/windows/x64/Lib/pdb.py \ + $(BUILD_DIR)/windows/x64/Lib/pickle.py \ + $(BUILD_DIR)/windows/x64/Lib/pickletools.py \ + $(BUILD_DIR)/windows/x64/Lib/pipes.py \ + $(BUILD_DIR)/windows/x64/Lib/pkgutil.py \ + $(BUILD_DIR)/windows/x64/Lib/platform.py \ + $(BUILD_DIR)/windows/x64/Lib/plistlib.py \ + $(BUILD_DIR)/windows/x64/Lib/poplib.py \ + $(BUILD_DIR)/windows/x64/Lib/posixpath.py \ + $(BUILD_DIR)/windows/x64/Lib/pprint.py \ + $(BUILD_DIR)/windows/x64/Lib/profile.py \ + $(BUILD_DIR)/windows/x64/Lib/pstats.py \ + $(BUILD_DIR)/windows/x64/Lib/pty.py \ + $(BUILD_DIR)/windows/x64/Lib/py_compile.py \ + $(BUILD_DIR)/windows/x64/Lib/pyclbr.py \ + $(BUILD_DIR)/windows/x64/Lib/pydoc.py \ + $(BUILD_DIR)/windows/x64/Lib/queue.py \ + $(BUILD_DIR)/windows/x64/Lib/quopri.py \ + $(BUILD_DIR)/windows/x64/Lib/random.py \ + $(BUILD_DIR)/windows/x64/Lib/re/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/re/_casefix.py \ + $(BUILD_DIR)/windows/x64/Lib/re/_compiler.py \ + $(BUILD_DIR)/windows/x64/Lib/re/_constants.py \ + $(BUILD_DIR)/windows/x64/Lib/re/_parser.py \ + $(BUILD_DIR)/windows/x64/Lib/reprlib.py \ + $(BUILD_DIR)/windows/x64/Lib/rlcompleter.py \ + $(BUILD_DIR)/windows/x64/Lib/runpy.py \ + $(BUILD_DIR)/windows/x64/Lib/sched.py \ + $(BUILD_DIR)/windows/x64/Lib/secrets.py \ + $(BUILD_DIR)/windows/x64/Lib/selectors.py \ + $(BUILD_DIR)/windows/x64/Lib/shelve.py \ + $(BUILD_DIR)/windows/x64/Lib/shlex.py \ + $(BUILD_DIR)/windows/x64/Lib/shutil.py \ + $(BUILD_DIR)/windows/x64/Lib/signal.py \ + $(BUILD_DIR)/windows/x64/Lib/site.py \ + $(BUILD_DIR)/windows/x64/Lib/smtpd.py \ + $(BUILD_DIR)/windows/x64/Lib/smtplib.py \ + $(BUILD_DIR)/windows/x64/Lib/sndhdr.py \ + $(BUILD_DIR)/windows/x64/Lib/socket.py \ + $(BUILD_DIR)/windows/x64/Lib/socketserver.py \ + $(BUILD_DIR)/windows/x64/Lib/sqlite3/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/sqlite3/dbapi2.py \ + $(BUILD_DIR)/windows/x64/Lib/sqlite3/dump.py \ + $(BUILD_DIR)/windows/x64/Lib/sre_compile.py \ + $(BUILD_DIR)/windows/x64/Lib/sre_constants.py \ + $(BUILD_DIR)/windows/x64/Lib/sre_parse.py \ + $(BUILD_DIR)/windows/x64/Lib/ssl.py \ + $(BUILD_DIR)/windows/x64/Lib/stat.py \ + $(BUILD_DIR)/windows/x64/Lib/statistics.py \ + $(BUILD_DIR)/windows/x64/Lib/string.py \ + $(BUILD_DIR)/windows/x64/Lib/stringprep.py \ + $(BUILD_DIR)/windows/x64/Lib/struct.py \ + $(BUILD_DIR)/windows/x64/Lib/subprocess.py \ + $(BUILD_DIR)/windows/x64/Lib/sunau.py \ + $(BUILD_DIR)/windows/x64/Lib/symtable.py \ + $(BUILD_DIR)/windows/x64/Lib/sysconfig.py \ + $(BUILD_DIR)/windows/x64/Lib/tabnanny.py \ + $(BUILD_DIR)/windows/x64/Lib/tarfile.py \ + $(BUILD_DIR)/windows/x64/Lib/telnetlib.py \ + $(BUILD_DIR)/windows/x64/Lib/tempfile.py \ + $(BUILD_DIR)/windows/x64/Lib/textwrap.py \ + $(BUILD_DIR)/windows/x64/Lib/this.py \ + $(BUILD_DIR)/windows/x64/Lib/threading.py \ + $(BUILD_DIR)/windows/x64/Lib/timeit.py \ + $(BUILD_DIR)/windows/x64/Lib/token.py \ + $(BUILD_DIR)/windows/x64/Lib/tokenize.py \ + $(BUILD_DIR)/windows/x64/Lib/tomllib/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/tomllib/_parser.py \ + $(BUILD_DIR)/windows/x64/Lib/tomllib/_re.py \ + $(BUILD_DIR)/windows/x64/Lib/tomllib/_types.py \ + $(BUILD_DIR)/windows/x64/Lib/trace.py \ + $(BUILD_DIR)/windows/x64/Lib/traceback.py \ + $(BUILD_DIR)/windows/x64/Lib/tracemalloc.py \ + $(BUILD_DIR)/windows/x64/Lib/tty.py \ + $(BUILD_DIR)/windows/x64/Lib/types.py \ + $(BUILD_DIR)/windows/x64/Lib/typing.py \ + $(BUILD_DIR)/windows/x64/Lib/urllib/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/urllib/error.py \ + $(BUILD_DIR)/windows/x64/Lib/urllib/parse.py \ + $(BUILD_DIR)/windows/x64/Lib/urllib/request.py \ + $(BUILD_DIR)/windows/x64/Lib/urllib/response.py \ + $(BUILD_DIR)/windows/x64/Lib/urllib/robotparser.py \ + $(BUILD_DIR)/windows/x64/Lib/uu.py \ + $(BUILD_DIR)/windows/x64/Lib/uuid.py \ + $(BUILD_DIR)/windows/x64/Lib/warnings.py \ + $(BUILD_DIR)/windows/x64/Lib/wave.py \ + $(BUILD_DIR)/windows/x64/Lib/weakref.py \ + $(BUILD_DIR)/windows/x64/Lib/webbrowser.py \ + $(BUILD_DIR)/windows/x64/Lib/xdrlib.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/NodeFilter.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/domreg.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/expatbuilder.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/minicompat.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/minidom.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/pulldom.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/xmlbuilder.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/etree/ElementInclude.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/etree/ElementPath.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/etree/ElementTree.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/etree/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/etree/cElementTree.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/parsers/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/parsers/expat.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/sax/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/sax/_exceptions.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/sax/expatreader.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/sax/handler.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/sax/saxutils.py \ + $(BUILD_DIR)/windows/x64/Lib/xml/sax/xmlreader.py \ + $(BUILD_DIR)/windows/x64/Lib/xmlrpc/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/xmlrpc/client.py \ + $(BUILD_DIR)/windows/x64/Lib/xmlrpc/server.py \ + $(BUILD_DIR)/windows/x64/Lib/zipapp.py \ + $(BUILD_DIR)/windows/x64/Lib/zipfile.py \ + $(BUILD_DIR)/windows/x64/Lib/zipimport.py \ + $(BUILD_DIR)/windows/x64/Lib/zoneinfo/__init__.py \ + $(BUILD_DIR)/windows/x64/Lib/zoneinfo/_common.py \ + $(BUILD_DIR)/windows/x64/Lib/zoneinfo/_tzpath.py \ + $(BUILD_DIR)/windows/x64/Lib/zoneinfo/_zoneinfo.py + +SCRIPT_TARGETS_PYC_PRIVATE_WIN_X64 = \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/__future__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/__hello__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/_aix_support.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/_bootsubprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/_collections_abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/_compat_pickle.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/_compression.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/_markupbase.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/_osx_support.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/_py_abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/_pydecimal.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/_pyio.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/_sitebuiltins.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/_strptime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/_threading_local.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/_weakrefset.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/aifc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/antigravity.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/argparse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/ast.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/asynchat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/__main__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/base_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/base_futures.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/base_subprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/base_tasks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/constants.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/coroutines.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/exceptions.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/format_helpers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/futures.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/locks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/log.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/mixins.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/proactor_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/protocols.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/queues.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/runners.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/selector_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/sslproto.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/staggered.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/streams.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/subprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/taskgroups.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/tasks.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/threads.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/timeouts.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/transports.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/trsock.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/unix_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/windows_events.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/asyncio/__pycache__/windows_utils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/asyncore.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/base64.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/bdb.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/bisect.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/bz2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/cProfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/calendar.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/cgi.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/cgitb.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/chunk.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/cmd.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/code.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/codecs.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/codeop.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/collections/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/collections/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/colorsys.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/compileall.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/concurrent/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/concurrent/futures/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/concurrent/futures/__pycache__/_base.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/concurrent/futures/__pycache__/process.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/concurrent/futures/__pycache__/thread.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/configparser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/contextlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/contextvars.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/copy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/copyreg.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/crypt.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/csv.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/__pycache__/_aix.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/__pycache__/_endian.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/macholib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/macholib/__pycache__/dyld.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/macholib/__pycache__/dylib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/macholib/__pycache__/framework.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/__pycache__/util.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/ctypes/__pycache__/wintypes.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/curses/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/curses/__pycache__/ascii.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/curses/__pycache__/has_key.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/curses/__pycache__/panel.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/curses/__pycache__/textpad.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/dataclasses.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/datetime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/decimal.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/difflib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/dis.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/doctest.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/_encoded_words.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/_header_value_parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/_parseaddr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/_policybase.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/base64mime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/charset.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/contentmanager.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/encoders.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/errors.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/feedparser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/generator.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/header.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/headerregistry.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/iterators.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/message.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/__pycache__/application.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/__pycache__/audio.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/__pycache__/base.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/__pycache__/image.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/__pycache__/message.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/__pycache__/multipart.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/__pycache__/nonmultipart.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/mime/__pycache__/text.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/policy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/quoprimime.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/email/__pycache__/utils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/aliases.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/ascii.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/base64_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/big5.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/big5hkscs.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/bz2_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/charmap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp037.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp1006.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp1026.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp1125.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp1140.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp1250.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp1251.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp1252.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp1253.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp1254.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp1255.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp1256.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp1257.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp1258.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp273.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp424.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp437.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp500.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp720.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp737.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp775.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp850.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp852.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp855.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp856.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp857.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp858.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp860.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp861.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp862.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp863.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp864.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp865.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp866.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp869.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp874.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp875.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp932.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp949.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/cp950.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/euc_jis_2004.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/euc_jisx0213.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/euc_jp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/euc_kr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/gb18030.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/gb2312.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/gbk.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/hex_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/hp_roman8.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/hz.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/idna.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso2022_jp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso2022_jp_1.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso2022_jp_2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso2022_jp_2004.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso2022_jp_3.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso2022_jp_ext.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso2022_kr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso8859_1.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso8859_10.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso8859_11.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso8859_13.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso8859_14.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso8859_15.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso8859_16.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso8859_2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso8859_3.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso8859_4.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso8859_5.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso8859_6.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso8859_7.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso8859_8.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/iso8859_9.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/johab.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/koi8_r.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/koi8_t.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/koi8_u.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/kz1048.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/latin_1.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/mac_arabic.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/mac_croatian.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/mac_cyrillic.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/mac_farsi.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/mac_greek.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/mac_iceland.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/mac_latin2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/mac_roman.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/mac_romanian.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/mac_turkish.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/mbcs.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/oem.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/palmos.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/ptcp154.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/punycode.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/quopri_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/raw_unicode_escape.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/rot_13.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/shift_jis.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/shift_jis_2004.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/shift_jisx0213.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/tis_620.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/undefined.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/unicode_escape.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/utf_16.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/utf_16_be.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/utf_16_le.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/utf_32.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/utf_32_be.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/utf_32_le.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/utf_7.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/utf_8.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/utf_8_sig.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/uu_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/encodings/__pycache__/zlib_codec.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/enum.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/filecmp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/fileinput.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/fnmatch.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/fractions.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/ftplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/functools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/genericpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/getopt.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/getpass.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/gettext.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/glob.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/graphlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/gzip.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/hashlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/heapq.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/hmac.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/html/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/html/__pycache__/entities.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/html/__pycache__/parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/http/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/http/__pycache__/client.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/http/__pycache__/cookiejar.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/http/__pycache__/cookies.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/http/__pycache__/server.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/imghdr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/imp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/__pycache__/_abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/__pycache__/_bootstrap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/__pycache__/_bootstrap_external.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/__pycache__/machinery.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/metadata/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/metadata/__pycache__/_adapters.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/metadata/__pycache__/_collections.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/metadata/__pycache__/_functools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/metadata/__pycache__/_itertools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/metadata/__pycache__/_meta.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/metadata/__pycache__/_text.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/__pycache__/readers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/__pycache__/_adapters.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/__pycache__/_common.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/__pycache__/_itertools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/__pycache__/_legacy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/__pycache__/abc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/__pycache__/readers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/resources/__pycache__/simple.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/__pycache__/simple.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/importlib/__pycache__/util.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/inspect.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/io.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/ipaddress.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/json/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/json/__pycache__/decoder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/json/__pycache__/encoder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/json/__pycache__/scanner.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/json/__pycache__/tool.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/keyword.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/linecache.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/locale.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/logging/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/logging/__pycache__/config.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/logging/__pycache__/handlers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/lzma.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/mailbox.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/mailcap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/mimetypes.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/modulefinder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/netrc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/nntplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/ntpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/nturl2path.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/numbers.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/opcode.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/operator.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/optparse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/os.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/pathlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/pdb.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/pickle.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/pickletools.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/pipes.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/pkgutil.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/platform.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/plistlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/poplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/posixpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/pprint.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/profile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/pstats.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/pty.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/py_compile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/pyclbr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/pydoc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/queue.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/quopri.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/random.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/re/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/re/__pycache__/_casefix.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/re/__pycache__/_compiler.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/re/__pycache__/_constants.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/re/__pycache__/_parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/reprlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/rlcompleter.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/runpy.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/sched.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/secrets.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/selectors.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/shelve.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/shlex.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/shutil.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/signal.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/site.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/smtpd.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/smtplib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/sndhdr.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/socket.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/socketserver.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/sqlite3/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/sqlite3/__pycache__/dbapi2.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/sqlite3/__pycache__/dump.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/sre_compile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/sre_constants.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/sre_parse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/ssl.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/stat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/statistics.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/string.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/stringprep.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/struct.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/subprocess.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/sunau.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/symtable.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/sysconfig.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/tabnanny.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/tarfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/telnetlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/tempfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/textwrap.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/this.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/threading.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/timeit.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/token.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/tokenize.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/tomllib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/tomllib/__pycache__/_parser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/tomllib/__pycache__/_re.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/tomllib/__pycache__/_types.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/trace.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/traceback.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/tracemalloc.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/tty.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/types.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/typing.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/urllib/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/urllib/__pycache__/error.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/urllib/__pycache__/parse.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/urllib/__pycache__/request.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/urllib/__pycache__/response.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/urllib/__pycache__/robotparser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/uu.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/uuid.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/warnings.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/wave.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/weakref.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/webbrowser.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/xdrlib.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/__pycache__/NodeFilter.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/__pycache__/domreg.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/__pycache__/expatbuilder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/__pycache__/minicompat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/__pycache__/minidom.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/__pycache__/pulldom.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/dom/__pycache__/xmlbuilder.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/etree/__pycache__/ElementInclude.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/etree/__pycache__/ElementPath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/etree/__pycache__/ElementTree.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/etree/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/etree/__pycache__/cElementTree.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/parsers/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/parsers/__pycache__/expat.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/sax/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/sax/__pycache__/_exceptions.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/sax/__pycache__/expatreader.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/sax/__pycache__/handler.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/sax/__pycache__/saxutils.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xml/sax/__pycache__/xmlreader.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xmlrpc/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xmlrpc/__pycache__/client.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/xmlrpc/__pycache__/server.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/zipapp.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/zipfile.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/__pycache__/zipimport.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/zoneinfo/__pycache__/__init__.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/zoneinfo/__pycache__/_common.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/zoneinfo/__pycache__/_tzpath.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/windows/x64/Lib/zoneinfo/__pycache__/_zoneinfo.cpython-311.opt-1.pyc + +# Rule to copy src asset scripts to dst. +# (and make non-writable so I'm less likely to accidentally edit them there) +$(SCRIPT_TARGETS_PY_PRIVATE_WIN_X64) : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +# These are too complex to define in a pattern rule; +# Instead we generate individual targets in a loop. +$(foreach element,$(SCRIPT_TARGETS_PYC_PRIVATE_WIN_X64),\ +$(eval $(call make-opt-pyc-target,$(element)))) + +COB_TARGETS = \ + $(BUILD_DIR)/ba_data/meshes/alwaysLandLevelCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/bigGBumper.cob \ + $(BUILD_DIR)/ba_data/meshes/bigGCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/bridgitLevelCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/bridgitLevelRailingCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/courtyardLevelCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/courtyardPlayerWall.cob \ + $(BUILD_DIR)/ba_data/meshes/cragCastleLevelBumper.cob \ + $(BUILD_DIR)/ba_data/meshes/cragCastleLevelCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/doomShroomLevelCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/doomShroomStemCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/footballStadiumCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/hockeyStadiumCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/lakeFrigidCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/monkeyFaceLevelBumper.cob \ + $(BUILD_DIR)/ba_data/meshes/monkeyFaceLevelCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/natureBackgroundCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/rampageBumper.cob \ + $(BUILD_DIR)/ba_data/meshes/rampageLevelCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/roundaboutLevelBumper.cob \ + $(BUILD_DIR)/ba_data/meshes/roundaboutLevelCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/stepRightUpLevelCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/thePadLevelBumper.cob \ + $(BUILD_DIR)/ba_data/meshes/thePadLevelCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/tipTopLevelBumper.cob \ + $(BUILD_DIR)/ba_data/meshes/tipTopLevelCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/towerDLevelCollide.cob \ + $(BUILD_DIR)/ba_data/meshes/towerDPlayerWall.cob \ + $(BUILD_DIR)/ba_data/meshes/zigZagLevelBumper.cob \ + $(BUILD_DIR)/ba_data/meshes/zigZagLevelCollide.cob + +BOB_TARGETS = \ + $(BUILD_DIR)/ba_data/meshes/achievementOutline.bob \ + $(BUILD_DIR)/ba_data/meshes/actionButtonBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/actionButtonLeft.bob \ + $(BUILD_DIR)/ba_data/meshes/actionButtonRight.bob \ + $(BUILD_DIR)/ba_data/meshes/actionButtonTop.bob \ + $(BUILD_DIR)/ba_data/meshes/actionHeroForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/actionHeroHand.bob \ + $(BUILD_DIR)/ba_data/meshes/actionHeroHead.bob \ + $(BUILD_DIR)/ba_data/meshes/actionHeroLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/actionHeroPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/actionHeroToes.bob \ + $(BUILD_DIR)/ba_data/meshes/actionHeroTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/actionHeroUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/actionHeroUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/agentForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/agentHand.bob \ + $(BUILD_DIR)/ba_data/meshes/agentHead.bob \ + $(BUILD_DIR)/ba_data/meshes/agentLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/agentPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/agentToes.bob \ + $(BUILD_DIR)/ba_data/meshes/agentTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/agentUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/agentUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/aliForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/aliHand.bob \ + $(BUILD_DIR)/ba_data/meshes/aliHead.bob \ + $(BUILD_DIR)/ba_data/meshes/aliLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/aliPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/aliToes.bob \ + $(BUILD_DIR)/ba_data/meshes/aliTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/aliUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/aliUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/alienForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/alienHand.bob \ + $(BUILD_DIR)/ba_data/meshes/alienHead.bob \ + $(BUILD_DIR)/ba_data/meshes/alienLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/alienPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/alienToes.bob \ + $(BUILD_DIR)/ba_data/meshes/alienTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/alienUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/alienUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/alwaysLandBG.bob \ + $(BUILD_DIR)/ba_data/meshes/alwaysLandLevel.bob \ + $(BUILD_DIR)/ba_data/meshes/alwaysLandLevelBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/alwaysLandVRFillMound.bob \ + $(BUILD_DIR)/ba_data/meshes/angryComputerTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/arrowBack.bob \ + $(BUILD_DIR)/ba_data/meshes/arrowFront.bob \ + $(BUILD_DIR)/ba_data/meshes/assassinForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/assassinHand.bob \ + $(BUILD_DIR)/ba_data/meshes/assassinHead.bob \ + $(BUILD_DIR)/ba_data/meshes/assassinLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/assassinPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/assassinToes.bob \ + $(BUILD_DIR)/ba_data/meshes/assassinTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/assassinUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/assassinUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/bearForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/bearHand.bob \ + $(BUILD_DIR)/ba_data/meshes/bearHead.bob \ + $(BUILD_DIR)/ba_data/meshes/bearLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/bearPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/bearToes.bob \ + $(BUILD_DIR)/ba_data/meshes/bearTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/bearUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/bearUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/bigG.bob \ + $(BUILD_DIR)/ba_data/meshes/bigGBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/bomb.bob \ + $(BUILD_DIR)/ba_data/meshes/bombSticky.bob \ + $(BUILD_DIR)/ba_data/meshes/bonesForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/bonesHand.bob \ + $(BUILD_DIR)/ba_data/meshes/bonesHead.bob \ + $(BUILD_DIR)/ba_data/meshes/bonesLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/bonesPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/bonesToes.bob \ + $(BUILD_DIR)/ba_data/meshes/bonesTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/bonesUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/bonesUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/box.bob \ + $(BUILD_DIR)/ba_data/meshes/boxingGlove.bob \ + $(BUILD_DIR)/ba_data/meshes/bridgitLevelBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/bridgitLevelTop.bob \ + $(BUILD_DIR)/ba_data/meshes/bunnyForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/bunnyHand.bob \ + $(BUILD_DIR)/ba_data/meshes/bunnyHead.bob \ + $(BUILD_DIR)/ba_data/meshes/bunnyLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/bunnyPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/bunnyToes.bob \ + $(BUILD_DIR)/ba_data/meshes/bunnyTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/bunnyUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/bunnyUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonBackOpaque.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonBackSmallOpaque.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonBackSmallTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonBackTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonLargeOpaque.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonLargeTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonLargerOpaque.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonLargerTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonMediumOpaque.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonMediumTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonNull.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonSmallOpaque.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonSmallTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonSquareOpaque.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonSquareTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonTabOpaque.bob \ + $(BUILD_DIR)/ba_data/meshes/buttonTabTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/checkTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/courtyardLevel.bob \ + $(BUILD_DIR)/ba_data/meshes/courtyardLevelBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/cowboyForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/cowboyHand.bob \ + $(BUILD_DIR)/ba_data/meshes/cowboyHead.bob \ + $(BUILD_DIR)/ba_data/meshes/cowboyLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/cowboyPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/cowboyToes.bob \ + $(BUILD_DIR)/ba_data/meshes/cowboyTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/cowboyUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/cowboyUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/cragCastleLevel.bob \ + $(BUILD_DIR)/ba_data/meshes/cragCastleLevelBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/cragCastleVRFillMound.bob \ + $(BUILD_DIR)/ba_data/meshes/crossOut.bob \ + $(BUILD_DIR)/ba_data/meshes/currencyMeter.bob \ + $(BUILD_DIR)/ba_data/meshes/currencyPlusButton.bob \ + $(BUILD_DIR)/ba_data/meshes/cyborgForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/cyborgHand.bob \ + $(BUILD_DIR)/ba_data/meshes/cyborgHead.bob \ + $(BUILD_DIR)/ba_data/meshes/cyborgLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/cyborgPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/cyborgToes.bob \ + $(BUILD_DIR)/ba_data/meshes/cyborgTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/cyborgUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/cyborgUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/cylinder.bob \ + $(BUILD_DIR)/ba_data/meshes/doomShroomBG.bob \ + $(BUILD_DIR)/ba_data/meshes/doomShroomLevel.bob \ + $(BUILD_DIR)/ba_data/meshes/doomShroomStem.bob \ + $(BUILD_DIR)/ba_data/meshes/doomShroomVRFill.bob \ + $(BUILD_DIR)/ba_data/meshes/egg.bob \ + $(BUILD_DIR)/ba_data/meshes/eyeBall.bob \ + $(BUILD_DIR)/ba_data/meshes/eyeBallIris.bob \ + $(BUILD_DIR)/ba_data/meshes/eyeLid.bob \ + $(BUILD_DIR)/ba_data/meshes/flagPole.bob \ + $(BUILD_DIR)/ba_data/meshes/flagStand.bob \ + $(BUILD_DIR)/ba_data/meshes/flash.bob \ + $(BUILD_DIR)/ba_data/meshes/footballStadium.bob \ + $(BUILD_DIR)/ba_data/meshes/footballStadiumVRFill.bob \ + $(BUILD_DIR)/ba_data/meshes/frameInset.bob \ + $(BUILD_DIR)/ba_data/meshes/frostyForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/frostyHand.bob \ + $(BUILD_DIR)/ba_data/meshes/frostyHead.bob \ + $(BUILD_DIR)/ba_data/meshes/frostyLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/frostyPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/frostyToes.bob \ + $(BUILD_DIR)/ba_data/meshes/frostyTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/frostyUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/frostyUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/gladiatorForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/gladiatorHand.bob \ + $(BUILD_DIR)/ba_data/meshes/gladiatorHead.bob \ + $(BUILD_DIR)/ba_data/meshes/gladiatorLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/gladiatorPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/gladiatorToes.bob \ + $(BUILD_DIR)/ba_data/meshes/gladiatorTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/gladiatorUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/gladiatorUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/hairTuft1.bob \ + $(BUILD_DIR)/ba_data/meshes/hairTuft1b.bob \ + $(BUILD_DIR)/ba_data/meshes/hairTuft2.bob \ + $(BUILD_DIR)/ba_data/meshes/hairTuft3.bob \ + $(BUILD_DIR)/ba_data/meshes/hairTuft4.bob \ + $(BUILD_DIR)/ba_data/meshes/heartOpaque.bob \ + $(BUILD_DIR)/ba_data/meshes/heartTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/hockeyStadiumInner.bob \ + $(BUILD_DIR)/ba_data/meshes/hockeyStadiumOuter.bob \ + $(BUILD_DIR)/ba_data/meshes/hockeyStadiumStands.bob \ + $(BUILD_DIR)/ba_data/meshes/image16x1.bob \ + $(BUILD_DIR)/ba_data/meshes/image1x1.bob \ + $(BUILD_DIR)/ba_data/meshes/image1x1FullScreen.bob \ + $(BUILD_DIR)/ba_data/meshes/image1x1VRFullScreen.bob \ + $(BUILD_DIR)/ba_data/meshes/image2x1.bob \ + $(BUILD_DIR)/ba_data/meshes/image2x1Vertical.bob \ + $(BUILD_DIR)/ba_data/meshes/image4x1.bob \ + $(BUILD_DIR)/ba_data/meshes/impactBomb.bob \ + $(BUILD_DIR)/ba_data/meshes/jackForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/jackHand.bob \ + $(BUILD_DIR)/ba_data/meshes/jackHead.bob \ + $(BUILD_DIR)/ba_data/meshes/jackLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/jackToes.bob \ + $(BUILD_DIR)/ba_data/meshes/jackTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/jackUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/jackUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/jumpsuitForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/jumpsuitHand.bob \ + $(BUILD_DIR)/ba_data/meshes/jumpsuitHead.bob \ + $(BUILD_DIR)/ba_data/meshes/jumpsuitLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/jumpsuitPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/jumpsuitToes.bob \ + $(BUILD_DIR)/ba_data/meshes/jumpsuitTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/jumpsuitUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/jumpsuitUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/kronkForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/kronkHand.bob \ + $(BUILD_DIR)/ba_data/meshes/kronkHead.bob \ + $(BUILD_DIR)/ba_data/meshes/kronkLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/kronkPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/kronkToes.bob \ + $(BUILD_DIR)/ba_data/meshes/kronkTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/kronkUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/kronkUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/lakeFrigid.bob \ + $(BUILD_DIR)/ba_data/meshes/lakeFrigidReflections.bob \ + $(BUILD_DIR)/ba_data/meshes/lakeFrigidTop.bob \ + $(BUILD_DIR)/ba_data/meshes/lakeFrigidVRFill.bob \ + $(BUILD_DIR)/ba_data/meshes/landMine.bob \ + $(BUILD_DIR)/ba_data/meshes/level_select_button_opaque.bob \ + $(BUILD_DIR)/ba_data/meshes/level_select_button_transparent.bob \ + $(BUILD_DIR)/ba_data/meshes/locator.bob \ + $(BUILD_DIR)/ba_data/meshes/locatorBox.bob \ + $(BUILD_DIR)/ba_data/meshes/locatorCircle.bob \ + $(BUILD_DIR)/ba_data/meshes/locatorCircleOutline.bob \ + $(BUILD_DIR)/ba_data/meshes/logo.bob \ + $(BUILD_DIR)/ba_data/meshes/logoTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/melForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/melHand.bob \ + $(BUILD_DIR)/ba_data/meshes/melHead.bob \ + $(BUILD_DIR)/ba_data/meshes/melLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/melToes.bob \ + $(BUILD_DIR)/ba_data/meshes/melTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/melUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/melUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/meterTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/monkeyFaceLevel.bob \ + $(BUILD_DIR)/ba_data/meshes/monkeyFaceLevelBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/natureBackground.bob \ + $(BUILD_DIR)/ba_data/meshes/natureBackgroundVRFill.bob \ + $(BUILD_DIR)/ba_data/meshes/neoSpazForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/neoSpazHand.bob \ + $(BUILD_DIR)/ba_data/meshes/neoSpazHead.bob \ + $(BUILD_DIR)/ba_data/meshes/neoSpazLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/neoSpazPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/neoSpazToes.bob \ + $(BUILD_DIR)/ba_data/meshes/neoSpazTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/neoSpazUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/neoSpazUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/ninjaForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/ninjaHand.bob \ + $(BUILD_DIR)/ba_data/meshes/ninjaHead.bob \ + $(BUILD_DIR)/ba_data/meshes/ninjaLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/ninjaPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/ninjaToes.bob \ + $(BUILD_DIR)/ba_data/meshes/ninjaTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/ninjaUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/ninjaUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/oldLadyForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/oldLadyHand.bob \ + $(BUILD_DIR)/ba_data/meshes/oldLadyHead.bob \ + $(BUILD_DIR)/ba_data/meshes/oldLadyLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/oldLadyPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/oldLadyToes.bob \ + $(BUILD_DIR)/ba_data/meshes/oldLadyTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/oldLadyUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/oldLadyUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/operaSingerForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/operaSingerHand.bob \ + $(BUILD_DIR)/ba_data/meshes/operaSingerHead.bob \ + $(BUILD_DIR)/ba_data/meshes/operaSingerLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/operaSingerPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/operaSingerToes.bob \ + $(BUILD_DIR)/ba_data/meshes/operaSingerTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/operaSingerUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/operaSingerUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/overlayGuide.bob \ + $(BUILD_DIR)/ba_data/meshes/penguinForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/penguinHand.bob \ + $(BUILD_DIR)/ba_data/meshes/penguinHead.bob \ + $(BUILD_DIR)/ba_data/meshes/penguinLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/penguinPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/penguinToes.bob \ + $(BUILD_DIR)/ba_data/meshes/penguinTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/penguinUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/penguinUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/pixieForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/pixieHand.bob \ + $(BUILD_DIR)/ba_data/meshes/pixieHead.bob \ + $(BUILD_DIR)/ba_data/meshes/pixieLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/pixiePelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/pixieToes.bob \ + $(BUILD_DIR)/ba_data/meshes/pixieTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/pixieUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/pixieUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/plasticEyesTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/playerLineup1Transparent.bob \ + $(BUILD_DIR)/ba_data/meshes/playerLineup2Transparent.bob \ + $(BUILD_DIR)/ba_data/meshes/playerLineup3Transparent.bob \ + $(BUILD_DIR)/ba_data/meshes/playerLineup4Transparent.bob \ + $(BUILD_DIR)/ba_data/meshes/powerup.bob \ + $(BUILD_DIR)/ba_data/meshes/powerupSimple.bob \ + $(BUILD_DIR)/ba_data/meshes/puck.bob \ + $(BUILD_DIR)/ba_data/meshes/rampageBG.bob \ + $(BUILD_DIR)/ba_data/meshes/rampageBG2.bob \ + $(BUILD_DIR)/ba_data/meshes/rampageLevel.bob \ + $(BUILD_DIR)/ba_data/meshes/rampageLevelBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/rampageVRFill.bob \ + $(BUILD_DIR)/ba_data/meshes/robotForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/robotHand.bob \ + $(BUILD_DIR)/ba_data/meshes/robotHead.bob \ + $(BUILD_DIR)/ba_data/meshes/robotLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/robotPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/robotToes.bob \ + $(BUILD_DIR)/ba_data/meshes/robotTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/robotUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/robotUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/roundaboutLevel.bob \ + $(BUILD_DIR)/ba_data/meshes/roundaboutLevelBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/runningShoes.bob \ + $(BUILD_DIR)/ba_data/meshes/santaForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/santaHand.bob \ + $(BUILD_DIR)/ba_data/meshes/santaHead.bob \ + $(BUILD_DIR)/ba_data/meshes/santaLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/santaToes.bob \ + $(BUILD_DIR)/ba_data/meshes/santaTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/santaUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/santaUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/scorch.bob \ + $(BUILD_DIR)/ba_data/meshes/scrollBarThumbOpaque.bob \ + $(BUILD_DIR)/ba_data/meshes/scrollBarThumbShortOpaque.bob \ + $(BUILD_DIR)/ba_data/meshes/scrollBarThumbShortSimple.bob \ + $(BUILD_DIR)/ba_data/meshes/scrollBarThumbShortTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/scrollBarThumbSimple.bob \ + $(BUILD_DIR)/ba_data/meshes/scrollBarThumbTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/scrollBarTroughTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/scrollWidgetShort.bob \ + $(BUILD_DIR)/ba_data/meshes/shield.bob \ + $(BUILD_DIR)/ba_data/meshes/shockWave.bob \ + $(BUILD_DIR)/ba_data/meshes/shrapnel1.bob \ + $(BUILD_DIR)/ba_data/meshes/shrapnelBoard.bob \ + $(BUILD_DIR)/ba_data/meshes/shrapnelSlime.bob \ + $(BUILD_DIR)/ba_data/meshes/softEdgeInside.bob \ + $(BUILD_DIR)/ba_data/meshes/softEdgeOutside.bob \ + $(BUILD_DIR)/ba_data/meshes/stepRightUpLevel.bob \ + $(BUILD_DIR)/ba_data/meshes/stepRightUpLevelBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/stepRightUpVRFillMound.bob \ + $(BUILD_DIR)/ba_data/meshes/superheroForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/superheroHand.bob \ + $(BUILD_DIR)/ba_data/meshes/superheroHead.bob \ + $(BUILD_DIR)/ba_data/meshes/superheroLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/superheroPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/superheroToes.bob \ + $(BUILD_DIR)/ba_data/meshes/superheroTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/superheroUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/superheroUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/textBoxTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/thePadBG.bob \ + $(BUILD_DIR)/ba_data/meshes/thePadBGSmall.bob \ + $(BUILD_DIR)/ba_data/meshes/thePadLevel.bob \ + $(BUILD_DIR)/ba_data/meshes/thePadLevelBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/thePadVRFillBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/thePadVRFillMound.bob \ + $(BUILD_DIR)/ba_data/meshes/thePadVRFillTop.bob \ + $(BUILD_DIR)/ba_data/meshes/tipTopBG.bob \ + $(BUILD_DIR)/ba_data/meshes/tipTopLevel.bob \ + $(BUILD_DIR)/ba_data/meshes/tipTopLevelBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/tnt.bob \ + $(BUILD_DIR)/ba_data/meshes/toolbarBacking.bob \ + $(BUILD_DIR)/ba_data/meshes/toolbarBackingBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/toolbarBackingBottom2.bob \ + $(BUILD_DIR)/ba_data/meshes/toolbarBackingOpaque.bob \ + $(BUILD_DIR)/ba_data/meshes/toolbarBackingTop.bob \ + $(BUILD_DIR)/ba_data/meshes/toolbarBackingTop2.bob \ + $(BUILD_DIR)/ba_data/meshes/toolbarBackingTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/towerDLevel.bob \ + $(BUILD_DIR)/ba_data/meshes/towerDLevelBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/trees.bob \ + $(BUILD_DIR)/ba_data/meshes/vrFade.bob \ + $(BUILD_DIR)/ba_data/meshes/vrOverlay.bob \ + $(BUILD_DIR)/ba_data/meshes/warriorForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/warriorHand.bob \ + $(BUILD_DIR)/ba_data/meshes/warriorHead.bob \ + $(BUILD_DIR)/ba_data/meshes/warriorLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/warriorPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/warriorToes.bob \ + $(BUILD_DIR)/ba_data/meshes/warriorTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/warriorUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/warriorUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/windowBGBlotch.bob \ + $(BUILD_DIR)/ba_data/meshes/windowHSmallVMedOpaque.bob \ + $(BUILD_DIR)/ba_data/meshes/windowHSmallVMedTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/windowHSmallVSmallOpaque.bob \ + $(BUILD_DIR)/ba_data/meshes/windowHSmallVSmallTransparent.bob \ + $(BUILD_DIR)/ba_data/meshes/wing.bob \ + $(BUILD_DIR)/ba_data/meshes/witchForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/witchHand.bob \ + $(BUILD_DIR)/ba_data/meshes/witchHead.bob \ + $(BUILD_DIR)/ba_data/meshes/witchLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/witchPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/witchToes.bob \ + $(BUILD_DIR)/ba_data/meshes/witchTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/witchUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/witchUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/wizardForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/wizardHand.bob \ + $(BUILD_DIR)/ba_data/meshes/wizardHead.bob \ + $(BUILD_DIR)/ba_data/meshes/wizardLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/wizardPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/wizardToes.bob \ + $(BUILD_DIR)/ba_data/meshes/wizardTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/wizardUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/wizardUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/wrestlerForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/wrestlerHand.bob \ + $(BUILD_DIR)/ba_data/meshes/wrestlerHead.bob \ + $(BUILD_DIR)/ba_data/meshes/wrestlerLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/wrestlerPelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/wrestlerToes.bob \ + $(BUILD_DIR)/ba_data/meshes/wrestlerTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/wrestlerUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/wrestlerUpperLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/zigZagLevel.bob \ + $(BUILD_DIR)/ba_data/meshes/zigZagLevelBottom.bob \ + $(BUILD_DIR)/ba_data/meshes/zoeForeArm.bob \ + $(BUILD_DIR)/ba_data/meshes/zoeHand.bob \ + $(BUILD_DIR)/ba_data/meshes/zoeHead.bob \ + $(BUILD_DIR)/ba_data/meshes/zoeLowerLeg.bob \ + $(BUILD_DIR)/ba_data/meshes/zoePelvis.bob \ + $(BUILD_DIR)/ba_data/meshes/zoeToes.bob \ + $(BUILD_DIR)/ba_data/meshes/zoeTorso.bob \ + $(BUILD_DIR)/ba_data/meshes/zoeUpperArm.bob \ + $(BUILD_DIR)/ba_data/meshes/zoeUpperLeg.bob + +FONT_TARGETS = \ + $(BUILD_DIR)/ba_data/fonts/fontSmall0.fdata \ + $(BUILD_DIR)/ba_data/fonts/fontSmall1.fdata \ + $(BUILD_DIR)/ba_data/fonts/fontSmall2.fdata \ + $(BUILD_DIR)/ba_data/fonts/fontSmall3.fdata \ + $(BUILD_DIR)/ba_data/fonts/fontSmall4.fdata \ + $(BUILD_DIR)/ba_data/fonts/fontSmall5.fdata \ + $(BUILD_DIR)/ba_data/fonts/fontSmall6.fdata \ + $(BUILD_DIR)/ba_data/fonts/fontSmall7.fdata + +PEM_TARGETS = \ + $(BUILD_DIR)/ba_data/python-site-packages/certifi/cacert.pem + +DATA_TARGETS = \ + $(BUILD_DIR)/ba_data/data/langdata.json \ + $(BUILD_DIR)/ba_data/data/languages/arabic.json \ + $(BUILD_DIR)/ba_data/data/languages/belarussian.json \ + $(BUILD_DIR)/ba_data/data/languages/chinese.json \ + $(BUILD_DIR)/ba_data/data/languages/chinesetraditional.json \ + $(BUILD_DIR)/ba_data/data/languages/croatian.json \ + $(BUILD_DIR)/ba_data/data/languages/czech.json \ + $(BUILD_DIR)/ba_data/data/languages/danish.json \ + $(BUILD_DIR)/ba_data/data/languages/dutch.json \ + $(BUILD_DIR)/ba_data/data/languages/english.json \ + $(BUILD_DIR)/ba_data/data/languages/esperanto.json \ + $(BUILD_DIR)/ba_data/data/languages/filipino.json \ + $(BUILD_DIR)/ba_data/data/languages/french.json \ + $(BUILD_DIR)/ba_data/data/languages/german.json \ + $(BUILD_DIR)/ba_data/data/languages/gibberish.json \ + $(BUILD_DIR)/ba_data/data/languages/greek.json \ + $(BUILD_DIR)/ba_data/data/languages/hindi.json \ + $(BUILD_DIR)/ba_data/data/languages/hungarian.json \ + $(BUILD_DIR)/ba_data/data/languages/indonesian.json \ + $(BUILD_DIR)/ba_data/data/languages/italian.json \ + $(BUILD_DIR)/ba_data/data/languages/korean.json \ + $(BUILD_DIR)/ba_data/data/languages/malay.json \ + $(BUILD_DIR)/ba_data/data/languages/persian.json \ + $(BUILD_DIR)/ba_data/data/languages/polish.json \ + $(BUILD_DIR)/ba_data/data/languages/portuguese.json \ + $(BUILD_DIR)/ba_data/data/languages/romanian.json \ + $(BUILD_DIR)/ba_data/data/languages/russian.json \ + $(BUILD_DIR)/ba_data/data/languages/serbian.json \ + $(BUILD_DIR)/ba_data/data/languages/slovak.json \ + $(BUILD_DIR)/ba_data/data/languages/spanish.json \ + $(BUILD_DIR)/ba_data/data/languages/swedish.json \ + $(BUILD_DIR)/ba_data/data/languages/tamil.json \ + $(BUILD_DIR)/ba_data/data/languages/thai.json \ + $(BUILD_DIR)/ba_data/data/languages/turkish.json \ + $(BUILD_DIR)/ba_data/data/languages/ukrainian.json \ + $(BUILD_DIR)/ba_data/data/languages/venetian.json \ + $(BUILD_DIR)/ba_data/data/languages/vietnamese.json \ + $(BUILD_DIR)/ba_data/data/maps/big_g.json \ + $(BUILD_DIR)/ba_data/data/maps/bridgit.json \ + $(BUILD_DIR)/ba_data/data/maps/courtyard.json \ + $(BUILD_DIR)/ba_data/data/maps/crag_castle.json \ + $(BUILD_DIR)/ba_data/data/maps/doom_shroom.json \ + $(BUILD_DIR)/ba_data/data/maps/football_stadium.json \ + $(BUILD_DIR)/ba_data/data/maps/happy_thoughts.json \ + $(BUILD_DIR)/ba_data/data/maps/hockey_stadium.json \ + $(BUILD_DIR)/ba_data/data/maps/lake_frigid.json \ + $(BUILD_DIR)/ba_data/data/maps/monkey_face.json \ + $(BUILD_DIR)/ba_data/data/maps/rampage.json \ + $(BUILD_DIR)/ba_data/data/maps/roundabout.json \ + $(BUILD_DIR)/ba_data/data/maps/step_right_up.json \ + $(BUILD_DIR)/ba_data/data/maps/the_pad.json \ + $(BUILD_DIR)/ba_data/data/maps/tip_top.json \ + $(BUILD_DIR)/ba_data/data/maps/tower_d.json \ + $(BUILD_DIR)/ba_data/data/maps/zig_zag.json + +AUDIO_TARGETS = \ + $(BUILD_DIR)/ba_data/audio/achievement.ogg \ + $(BUILD_DIR)/ba_data/audio/actionHero1.ogg \ + $(BUILD_DIR)/ba_data/audio/actionHero2.ogg \ + $(BUILD_DIR)/ba_data/audio/actionHero3.ogg \ + $(BUILD_DIR)/ba_data/audio/actionHero4.ogg \ + $(BUILD_DIR)/ba_data/audio/actionHeroDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/actionHeroFall.ogg \ + $(BUILD_DIR)/ba_data/audio/actionHeroHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/actionHeroHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/activateBeep.ogg \ + $(BUILD_DIR)/ba_data/audio/agent1.ogg \ + $(BUILD_DIR)/ba_data/audio/agent2.ogg \ + $(BUILD_DIR)/ba_data/audio/agent3.ogg \ + $(BUILD_DIR)/ba_data/audio/agent4.ogg \ + $(BUILD_DIR)/ba_data/audio/agentDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/agentFall.ogg \ + $(BUILD_DIR)/ba_data/audio/agentHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/agentHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/alarm.ogg \ + $(BUILD_DIR)/ba_data/audio/ali1.ogg \ + $(BUILD_DIR)/ba_data/audio/ali2.ogg \ + $(BUILD_DIR)/ba_data/audio/ali3.ogg \ + $(BUILD_DIR)/ba_data/audio/ali4.ogg \ + $(BUILD_DIR)/ba_data/audio/aliDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/aliFall.ogg \ + $(BUILD_DIR)/ba_data/audio/aliHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/aliHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/alien1.ogg \ + $(BUILD_DIR)/ba_data/audio/alien2.ogg \ + $(BUILD_DIR)/ba_data/audio/alien3.ogg \ + $(BUILD_DIR)/ba_data/audio/alien4.ogg \ + $(BUILD_DIR)/ba_data/audio/alienDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/alienFall.ogg \ + $(BUILD_DIR)/ba_data/audio/alienHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/alienHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/announceEight.ogg \ + $(BUILD_DIR)/ba_data/audio/announceFive.ogg \ + $(BUILD_DIR)/ba_data/audio/announceFour.ogg \ + $(BUILD_DIR)/ba_data/audio/announceNine.ogg \ + $(BUILD_DIR)/ba_data/audio/announceOne.ogg \ + $(BUILD_DIR)/ba_data/audio/announceSeven.ogg \ + $(BUILD_DIR)/ba_data/audio/announceSix.ogg \ + $(BUILD_DIR)/ba_data/audio/announceTen.ogg \ + $(BUILD_DIR)/ba_data/audio/announceThree.ogg \ + $(BUILD_DIR)/ba_data/audio/announceTwo.ogg \ + $(BUILD_DIR)/ba_data/audio/assassin1.ogg \ + $(BUILD_DIR)/ba_data/audio/assassin2.ogg \ + $(BUILD_DIR)/ba_data/audio/assassin3.ogg \ + $(BUILD_DIR)/ba_data/audio/assassin4.ogg \ + $(BUILD_DIR)/ba_data/audio/assassinDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/assassinFall.ogg \ + $(BUILD_DIR)/ba_data/audio/assassinHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/assassinHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/bear1.ogg \ + $(BUILD_DIR)/ba_data/audio/bear2.ogg \ + $(BUILD_DIR)/ba_data/audio/bear3.ogg \ + $(BUILD_DIR)/ba_data/audio/bear4.ogg \ + $(BUILD_DIR)/ba_data/audio/bearDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/bearFall.ogg \ + $(BUILD_DIR)/ba_data/audio/bearHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/bearHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/bellHigh.ogg \ + $(BUILD_DIR)/ba_data/audio/bellLow.ogg \ + $(BUILD_DIR)/ba_data/audio/bellMed.ogg \ + $(BUILD_DIR)/ba_data/audio/bigImpact.ogg \ + $(BUILD_DIR)/ba_data/audio/bigImpact2.ogg \ + $(BUILD_DIR)/ba_data/audio/blank.ogg \ + $(BUILD_DIR)/ba_data/audio/blip.ogg \ + $(BUILD_DIR)/ba_data/audio/block.ogg \ + $(BUILD_DIR)/ba_data/audio/bombDrop01.ogg \ + $(BUILD_DIR)/ba_data/audio/bombDrop02.ogg \ + $(BUILD_DIR)/ba_data/audio/bombRoll01.ogg \ + $(BUILD_DIR)/ba_data/audio/bones1.ogg \ + $(BUILD_DIR)/ba_data/audio/bones2.ogg \ + $(BUILD_DIR)/ba_data/audio/bones3.ogg \ + $(BUILD_DIR)/ba_data/audio/bonesDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/bonesFall.ogg \ + $(BUILD_DIR)/ba_data/audio/boo.ogg \ + $(BUILD_DIR)/ba_data/audio/boxDrop.ogg \ + $(BUILD_DIR)/ba_data/audio/boxingBell.ogg \ + $(BUILD_DIR)/ba_data/audio/bunny1.ogg \ + $(BUILD_DIR)/ba_data/audio/bunny2.ogg \ + $(BUILD_DIR)/ba_data/audio/bunny3.ogg \ + $(BUILD_DIR)/ba_data/audio/bunny4.ogg \ + $(BUILD_DIR)/ba_data/audio/bunnyDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/bunnyFall.ogg \ + $(BUILD_DIR)/ba_data/audio/bunnyHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/bunnyHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/bunnyJump.ogg \ + $(BUILD_DIR)/ba_data/audio/cashRegister.ogg \ + $(BUILD_DIR)/ba_data/audio/cashRegister2.ogg \ + $(BUILD_DIR)/ba_data/audio/charSelectMusic.ogg \ + $(BUILD_DIR)/ba_data/audio/cheer.ogg \ + $(BUILD_DIR)/ba_data/audio/click01.ogg \ + $(BUILD_DIR)/ba_data/audio/corkPop.ogg \ + $(BUILD_DIR)/ba_data/audio/cowboy1.ogg \ + $(BUILD_DIR)/ba_data/audio/cowboy2.ogg \ + $(BUILD_DIR)/ba_data/audio/cowboy3.ogg \ + $(BUILD_DIR)/ba_data/audio/cowboy4.ogg \ + $(BUILD_DIR)/ba_data/audio/cowboyDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/cowboyFall.ogg \ + $(BUILD_DIR)/ba_data/audio/cowboyHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/cowboyHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/crowdChant.ogg \ + $(BUILD_DIR)/ba_data/audio/cyborg1.ogg \ + $(BUILD_DIR)/ba_data/audio/cyborg2.ogg \ + $(BUILD_DIR)/ba_data/audio/cyborg3.ogg \ + $(BUILD_DIR)/ba_data/audio/cyborg4.ogg \ + $(BUILD_DIR)/ba_data/audio/cyborgDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/cyborgFall.ogg \ + $(BUILD_DIR)/ba_data/audio/cyborgHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/cyborgHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/cymbal.ogg \ + $(BUILD_DIR)/ba_data/audio/debrisFall.ogg \ + $(BUILD_DIR)/ba_data/audio/deek.ogg \ + $(BUILD_DIR)/ba_data/audio/deek2.ogg \ + $(BUILD_DIR)/ba_data/audio/ding.ogg \ + $(BUILD_DIR)/ba_data/audio/dingSmall.ogg \ + $(BUILD_DIR)/ba_data/audio/dingSmallHigh.ogg \ + $(BUILD_DIR)/ba_data/audio/dripity.ogg \ + $(BUILD_DIR)/ba_data/audio/drumRoll.ogg \ + $(BUILD_DIR)/ba_data/audio/error.ogg \ + $(BUILD_DIR)/ba_data/audio/explosion01.ogg \ + $(BUILD_DIR)/ba_data/audio/explosion02.ogg \ + $(BUILD_DIR)/ba_data/audio/explosion03.ogg \ + $(BUILD_DIR)/ba_data/audio/explosion04.ogg \ + $(BUILD_DIR)/ba_data/audio/explosion05.ogg \ + $(BUILD_DIR)/ba_data/audio/fanfare.ogg \ + $(BUILD_DIR)/ba_data/audio/flagCatcherMusic.ogg \ + $(BUILD_DIR)/ba_data/audio/flyingMusic.ogg \ + $(BUILD_DIR)/ba_data/audio/foghorn.ogg \ + $(BUILD_DIR)/ba_data/audio/footImpact01.ogg \ + $(BUILD_DIR)/ba_data/audio/footImpact02.ogg \ + $(BUILD_DIR)/ba_data/audio/footImpact03.ogg \ + $(BUILD_DIR)/ba_data/audio/forwardMarchMusic.ogg \ + $(BUILD_DIR)/ba_data/audio/freeze.ogg \ + $(BUILD_DIR)/ba_data/audio/frosty01.ogg \ + $(BUILD_DIR)/ba_data/audio/frosty02.ogg \ + $(BUILD_DIR)/ba_data/audio/frosty03.ogg \ + $(BUILD_DIR)/ba_data/audio/frosty04.ogg \ + $(BUILD_DIR)/ba_data/audio/frosty05.ogg \ + $(BUILD_DIR)/ba_data/audio/frostyDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/frostyFall.ogg \ + $(BUILD_DIR)/ba_data/audio/frostyHit01.ogg \ + $(BUILD_DIR)/ba_data/audio/frostyHit02.ogg \ + $(BUILD_DIR)/ba_data/audio/frostyHit03.ogg \ + $(BUILD_DIR)/ba_data/audio/fuse01.ogg \ + $(BUILD_DIR)/ba_data/audio/gladiator1.ogg \ + $(BUILD_DIR)/ba_data/audio/gladiator2.ogg \ + $(BUILD_DIR)/ba_data/audio/gladiator3.ogg \ + $(BUILD_DIR)/ba_data/audio/gladiator4.ogg \ + $(BUILD_DIR)/ba_data/audio/gladiatorDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/gladiatorFall.ogg \ + $(BUILD_DIR)/ba_data/audio/gladiatorHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/gladiatorHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/gong.ogg \ + $(BUILD_DIR)/ba_data/audio/grandRompMusic.ogg \ + $(BUILD_DIR)/ba_data/audio/gravelSkid.ogg \ + $(BUILD_DIR)/ba_data/audio/gunCocking.ogg \ + $(BUILD_DIR)/ba_data/audio/healthPowerup.ogg \ + $(BUILD_DIR)/ba_data/audio/hiss.ogg \ + $(BUILD_DIR)/ba_data/audio/impactHard.ogg \ + $(BUILD_DIR)/ba_data/audio/impactHard2.ogg \ + $(BUILD_DIR)/ba_data/audio/impactHard3.ogg \ + $(BUILD_DIR)/ba_data/audio/impactMedium.ogg \ + $(BUILD_DIR)/ba_data/audio/impactMedium2.ogg \ + $(BUILD_DIR)/ba_data/audio/jack01.ogg \ + $(BUILD_DIR)/ba_data/audio/jack02.ogg \ + $(BUILD_DIR)/ba_data/audio/jack03.ogg \ + $(BUILD_DIR)/ba_data/audio/jack04.ogg \ + $(BUILD_DIR)/ba_data/audio/jack05.ogg \ + $(BUILD_DIR)/ba_data/audio/jack06.ogg \ + $(BUILD_DIR)/ba_data/audio/jackDeath01.ogg \ + $(BUILD_DIR)/ba_data/audio/jackFall01.ogg \ + $(BUILD_DIR)/ba_data/audio/jackHit01.ogg \ + $(BUILD_DIR)/ba_data/audio/jackHit02.ogg \ + $(BUILD_DIR)/ba_data/audio/jackHit03.ogg \ + $(BUILD_DIR)/ba_data/audio/jackHit04.ogg \ + $(BUILD_DIR)/ba_data/audio/jackHit05.ogg \ + $(BUILD_DIR)/ba_data/audio/jackHit06.ogg \ + $(BUILD_DIR)/ba_data/audio/jackHit07.ogg \ + $(BUILD_DIR)/ba_data/audio/jumpsuit1.ogg \ + $(BUILD_DIR)/ba_data/audio/jumpsuit2.ogg \ + $(BUILD_DIR)/ba_data/audio/jumpsuit3.ogg \ + $(BUILD_DIR)/ba_data/audio/jumpsuit4.ogg \ + $(BUILD_DIR)/ba_data/audio/jumpsuitDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/jumpsuitFall.ogg \ + $(BUILD_DIR)/ba_data/audio/jumpsuitHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/jumpsuitHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/kronk1.ogg \ + $(BUILD_DIR)/ba_data/audio/kronk10.ogg \ + $(BUILD_DIR)/ba_data/audio/kronk2.ogg \ + $(BUILD_DIR)/ba_data/audio/kronk3.ogg \ + $(BUILD_DIR)/ba_data/audio/kronk4.ogg \ + $(BUILD_DIR)/ba_data/audio/kronk5.ogg \ + $(BUILD_DIR)/ba_data/audio/kronk6.ogg \ + $(BUILD_DIR)/ba_data/audio/kronk7.ogg \ + $(BUILD_DIR)/ba_data/audio/kronk8.ogg \ + $(BUILD_DIR)/ba_data/audio/kronk9.ogg \ + $(BUILD_DIR)/ba_data/audio/kronkDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/kronkFall.ogg \ + $(BUILD_DIR)/ba_data/audio/laser.ogg \ + $(BUILD_DIR)/ba_data/audio/laserReverse.ogg \ + $(BUILD_DIR)/ba_data/audio/mel01.ogg \ + $(BUILD_DIR)/ba_data/audio/mel02.ogg \ + $(BUILD_DIR)/ba_data/audio/mel03.ogg \ + $(BUILD_DIR)/ba_data/audio/mel04.ogg \ + $(BUILD_DIR)/ba_data/audio/mel05.ogg \ + $(BUILD_DIR)/ba_data/audio/mel06.ogg \ + $(BUILD_DIR)/ba_data/audio/mel07.ogg \ + $(BUILD_DIR)/ba_data/audio/mel08.ogg \ + $(BUILD_DIR)/ba_data/audio/mel09.ogg \ + $(BUILD_DIR)/ba_data/audio/mel10.ogg \ + $(BUILD_DIR)/ba_data/audio/melDeath01.ogg \ + $(BUILD_DIR)/ba_data/audio/melFall01.ogg \ + $(BUILD_DIR)/ba_data/audio/menuMusic.ogg \ + $(BUILD_DIR)/ba_data/audio/metalHit.ogg \ + $(BUILD_DIR)/ba_data/audio/metalSkid.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaAttack1.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaAttack2.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaAttack3.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaAttack4.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaAttack5.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaAttack6.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaAttack7.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaDeath1.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaFall1.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaHit3.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaHit4.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaHit5.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaHit6.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaHit7.ogg \ + $(BUILD_DIR)/ba_data/audio/ninjaHit8.ogg \ + $(BUILD_DIR)/ba_data/audio/oldLady1.ogg \ + $(BUILD_DIR)/ba_data/audio/oldLady2.ogg \ + $(BUILD_DIR)/ba_data/audio/oldLady3.ogg \ + $(BUILD_DIR)/ba_data/audio/oldLady4.ogg \ + $(BUILD_DIR)/ba_data/audio/oldLadyDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/oldLadyFall.ogg \ + $(BUILD_DIR)/ba_data/audio/oldLadyHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/oldLadyHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/ooh.ogg \ + $(BUILD_DIR)/ba_data/audio/operaSinger1.ogg \ + $(BUILD_DIR)/ba_data/audio/operaSinger2.ogg \ + $(BUILD_DIR)/ba_data/audio/operaSinger3.ogg \ + $(BUILD_DIR)/ba_data/audio/operaSinger4.ogg \ + $(BUILD_DIR)/ba_data/audio/operaSingerDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/operaSingerFall.ogg \ + $(BUILD_DIR)/ba_data/audio/operaSingerHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/operaSingerHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/orchestraHit.ogg \ + $(BUILD_DIR)/ba_data/audio/orchestraHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/orchestraHit3.ogg \ + $(BUILD_DIR)/ba_data/audio/orchestraHit4.ogg \ + $(BUILD_DIR)/ba_data/audio/orchestraHitBig1.ogg \ + $(BUILD_DIR)/ba_data/audio/orchestraHitBig2.ogg \ + $(BUILD_DIR)/ba_data/audio/penguin1.ogg \ + $(BUILD_DIR)/ba_data/audio/penguin2.ogg \ + $(BUILD_DIR)/ba_data/audio/penguin3.ogg \ + $(BUILD_DIR)/ba_data/audio/penguin4.ogg \ + $(BUILD_DIR)/ba_data/audio/penguinDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/penguinFall.ogg \ + $(BUILD_DIR)/ba_data/audio/penguinHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/penguinHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/pixie1.ogg \ + $(BUILD_DIR)/ba_data/audio/pixie2.ogg \ + $(BUILD_DIR)/ba_data/audio/pixie3.ogg \ + $(BUILD_DIR)/ba_data/audio/pixie4.ogg \ + $(BUILD_DIR)/ba_data/audio/pixieDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/pixieFall.ogg \ + $(BUILD_DIR)/ba_data/audio/pixieHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/pixieHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/playerDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/playerLeft.ogg \ + $(BUILD_DIR)/ba_data/audio/pop01.ogg \ + $(BUILD_DIR)/ba_data/audio/powerdown01.ogg \ + $(BUILD_DIR)/ba_data/audio/powerup01.ogg \ + $(BUILD_DIR)/ba_data/audio/punch01.ogg \ + $(BUILD_DIR)/ba_data/audio/punchStrong01.ogg \ + $(BUILD_DIR)/ba_data/audio/punchStrong02.ogg \ + $(BUILD_DIR)/ba_data/audio/punchSwish.ogg \ + $(BUILD_DIR)/ba_data/audio/punchWeak01.ogg \ + $(BUILD_DIR)/ba_data/audio/raceBeep1.ogg \ + $(BUILD_DIR)/ba_data/audio/raceBeep2.ogg \ + $(BUILD_DIR)/ba_data/audio/refWhistle.ogg \ + $(BUILD_DIR)/ba_data/audio/robot1.ogg \ + $(BUILD_DIR)/ba_data/audio/robot2.ogg \ + $(BUILD_DIR)/ba_data/audio/robot3.ogg \ + $(BUILD_DIR)/ba_data/audio/robot4.ogg \ + $(BUILD_DIR)/ba_data/audio/robotDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/robotFall.ogg \ + $(BUILD_DIR)/ba_data/audio/robotHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/robotHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/runAwayMusic.ogg \ + $(BUILD_DIR)/ba_data/audio/santa01.ogg \ + $(BUILD_DIR)/ba_data/audio/santa02.ogg \ + $(BUILD_DIR)/ba_data/audio/santa03.ogg \ + $(BUILD_DIR)/ba_data/audio/santa04.ogg \ + $(BUILD_DIR)/ba_data/audio/santa05.ogg \ + $(BUILD_DIR)/ba_data/audio/santaDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/santaFall.ogg \ + $(BUILD_DIR)/ba_data/audio/santaHit01.ogg \ + $(BUILD_DIR)/ba_data/audio/santaHit02.ogg \ + $(BUILD_DIR)/ba_data/audio/santaHit03.ogg \ + $(BUILD_DIR)/ba_data/audio/santaHit04.ogg \ + $(BUILD_DIR)/ba_data/audio/scamper01.ogg \ + $(BUILD_DIR)/ba_data/audio/scaryMusic.ogg \ + $(BUILD_DIR)/ba_data/audio/score.ogg \ + $(BUILD_DIR)/ba_data/audio/scoreHit01.ogg \ + $(BUILD_DIR)/ba_data/audio/scoreHit02.ogg \ + $(BUILD_DIR)/ba_data/audio/scoreIncrease.ogg \ + $(BUILD_DIR)/ba_data/audio/scoresEpicMusic.ogg \ + $(BUILD_DIR)/ba_data/audio/shatter.ogg \ + $(BUILD_DIR)/ba_data/audio/shieldDown.ogg \ + $(BUILD_DIR)/ba_data/audio/shieldHit.ogg \ + $(BUILD_DIR)/ba_data/audio/shieldUp.ogg \ + $(BUILD_DIR)/ba_data/audio/skid01.ogg \ + $(BUILD_DIR)/ba_data/audio/slowEpicMusic.ogg \ + $(BUILD_DIR)/ba_data/audio/sparkle01.ogg \ + $(BUILD_DIR)/ba_data/audio/sparkle02.ogg \ + $(BUILD_DIR)/ba_data/audio/sparkle03.ogg \ + $(BUILD_DIR)/ba_data/audio/spawn.ogg \ + $(BUILD_DIR)/ba_data/audio/spazAttack01.ogg \ + $(BUILD_DIR)/ba_data/audio/spazAttack02.ogg \ + $(BUILD_DIR)/ba_data/audio/spazAttack03.ogg \ + $(BUILD_DIR)/ba_data/audio/spazAttack04.ogg \ + $(BUILD_DIR)/ba_data/audio/spazDeath01.ogg \ + $(BUILD_DIR)/ba_data/audio/spazEff.ogg \ + $(BUILD_DIR)/ba_data/audio/spazFall01.ogg \ + $(BUILD_DIR)/ba_data/audio/spazImpact01.ogg \ + $(BUILD_DIR)/ba_data/audio/spazImpact02.ogg \ + $(BUILD_DIR)/ba_data/audio/spazImpact03.ogg \ + $(BUILD_DIR)/ba_data/audio/spazImpact04.ogg \ + $(BUILD_DIR)/ba_data/audio/spazJump01.ogg \ + $(BUILD_DIR)/ba_data/audio/spazJump02.ogg \ + $(BUILD_DIR)/ba_data/audio/spazJump03.ogg \ + $(BUILD_DIR)/ba_data/audio/spazJump04.ogg \ + $(BUILD_DIR)/ba_data/audio/spazOw.ogg \ + $(BUILD_DIR)/ba_data/audio/spazPickup01.ogg \ + $(BUILD_DIR)/ba_data/audio/spazScream01.ogg \ + $(BUILD_DIR)/ba_data/audio/splatter.ogg \ + $(BUILD_DIR)/ba_data/audio/sportsMusic.ogg \ + $(BUILD_DIR)/ba_data/audio/stickyImpact.ogg \ + $(BUILD_DIR)/ba_data/audio/superPunch.ogg \ + $(BUILD_DIR)/ba_data/audio/superhero1.ogg \ + $(BUILD_DIR)/ba_data/audio/superhero2.ogg \ + $(BUILD_DIR)/ba_data/audio/superhero3.ogg \ + $(BUILD_DIR)/ba_data/audio/superhero4.ogg \ + $(BUILD_DIR)/ba_data/audio/superheroDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/superheroFall.ogg \ + $(BUILD_DIR)/ba_data/audio/superheroHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/superheroHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/survivalMusic.ogg \ + $(BUILD_DIR)/ba_data/audio/swip.ogg \ + $(BUILD_DIR)/ba_data/audio/swip2.ogg \ + $(BUILD_DIR)/ba_data/audio/swish.ogg \ + $(BUILD_DIR)/ba_data/audio/swish2.ogg \ + $(BUILD_DIR)/ba_data/audio/swish3.ogg \ + $(BUILD_DIR)/ba_data/audio/tap.ogg \ + $(BUILD_DIR)/ba_data/audio/technoHit01.ogg \ + $(BUILD_DIR)/ba_data/audio/tick.ogg \ + $(BUILD_DIR)/ba_data/audio/ticking.ogg \ + $(BUILD_DIR)/ba_data/audio/tickingCrazy.ogg \ + $(BUILD_DIR)/ba_data/audio/toTheDeathMusic.ogg \ + $(BUILD_DIR)/ba_data/audio/trashRummage.ogg \ + $(BUILD_DIR)/ba_data/audio/victoryMusic.ogg \ + $(BUILD_DIR)/ba_data/audio/warnBeep.ogg \ + $(BUILD_DIR)/ba_data/audio/warnBeeps.ogg \ + $(BUILD_DIR)/ba_data/audio/warrior1.ogg \ + $(BUILD_DIR)/ba_data/audio/warrior2.ogg \ + $(BUILD_DIR)/ba_data/audio/warrior3.ogg \ + $(BUILD_DIR)/ba_data/audio/warrior4.ogg \ + $(BUILD_DIR)/ba_data/audio/warriorDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/warriorFall.ogg \ + $(BUILD_DIR)/ba_data/audio/warriorHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/warriorHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/whenJohnnyComesMarchingHomeMusic.ogg \ + $(BUILD_DIR)/ba_data/audio/witch1.ogg \ + $(BUILD_DIR)/ba_data/audio/witch2.ogg \ + $(BUILD_DIR)/ba_data/audio/witch3.ogg \ + $(BUILD_DIR)/ba_data/audio/witch4.ogg \ + $(BUILD_DIR)/ba_data/audio/witchDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/witchFall.ogg \ + $(BUILD_DIR)/ba_data/audio/witchHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/witchHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/wizard1.ogg \ + $(BUILD_DIR)/ba_data/audio/wizard2.ogg \ + $(BUILD_DIR)/ba_data/audio/wizard3.ogg \ + $(BUILD_DIR)/ba_data/audio/wizard4.ogg \ + $(BUILD_DIR)/ba_data/audio/wizardDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/wizardFall.ogg \ + $(BUILD_DIR)/ba_data/audio/wizardHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/wizardHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/woodDebrisFall.ogg \ + $(BUILD_DIR)/ba_data/audio/wrestler1.ogg \ + $(BUILD_DIR)/ba_data/audio/wrestler2.ogg \ + $(BUILD_DIR)/ba_data/audio/wrestler3.ogg \ + $(BUILD_DIR)/ba_data/audio/wrestler4.ogg \ + $(BUILD_DIR)/ba_data/audio/wrestlerDeath.ogg \ + $(BUILD_DIR)/ba_data/audio/wrestlerFall.ogg \ + $(BUILD_DIR)/ba_data/audio/wrestlerHit1.ogg \ + $(BUILD_DIR)/ba_data/audio/wrestlerHit2.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeAttack01.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeAttack02.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeAttack03.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeAttack04.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeDeath01.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeEff.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeFall01.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeImpact01.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeImpact02.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeImpact03.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeImpact04.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeJump01.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeJump02.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeJump03.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeOw.ogg \ + $(BUILD_DIR)/ba_data/audio/zoePickup01.ogg \ + $(BUILD_DIR)/ba_data/audio/zoeScream01.ogg + +TEX2D_DDS_TARGETS = \ + $(BUILD_DIR)/ba_data/textures/achievementBoxer.dds \ + $(BUILD_DIR)/ba_data/textures/achievementCrossHair.dds \ + $(BUILD_DIR)/ba_data/textures/achievementDualWielding.dds \ + $(BUILD_DIR)/ba_data/textures/achievementEmpty.dds \ + $(BUILD_DIR)/ba_data/textures/achievementFlawlessVictory.dds \ + $(BUILD_DIR)/ba_data/textures/achievementFootballShutout.dds \ + $(BUILD_DIR)/ba_data/textures/achievementFootballVictory.dds \ + $(BUILD_DIR)/ba_data/textures/achievementFreeLoader.dds \ + $(BUILD_DIR)/ba_data/textures/achievementGotTheMoves.dds \ + $(BUILD_DIR)/ba_data/textures/achievementInControl.dds \ + $(BUILD_DIR)/ba_data/textures/achievementMedalLarge.dds \ + $(BUILD_DIR)/ba_data/textures/achievementMedalMedium.dds \ + $(BUILD_DIR)/ba_data/textures/achievementMedalSmall.dds \ + $(BUILD_DIR)/ba_data/textures/achievementMine.dds \ + $(BUILD_DIR)/ba_data/textures/achievementOffYouGo.dds \ + $(BUILD_DIR)/ba_data/textures/achievementOnslaught.dds \ + $(BUILD_DIR)/ba_data/textures/achievementOutline.dds \ + $(BUILD_DIR)/ba_data/textures/achievementRunaround.dds \ + $(BUILD_DIR)/ba_data/textures/achievementSharingIsCaring.dds \ + $(BUILD_DIR)/ba_data/textures/achievementStayinAlive.dds \ + $(BUILD_DIR)/ba_data/textures/achievementSuperPunch.dds \ + $(BUILD_DIR)/ba_data/textures/achievementTNT.dds \ + $(BUILD_DIR)/ba_data/textures/achievementTeamPlayer.dds \ + $(BUILD_DIR)/ba_data/textures/achievementWall.dds \ + $(BUILD_DIR)/ba_data/textures/achievementsIcon.dds \ + $(BUILD_DIR)/ba_data/textures/actionButtons.dds \ + $(BUILD_DIR)/ba_data/textures/actionHeroColor.dds \ + $(BUILD_DIR)/ba_data/textures/actionHeroColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/actionHeroIcon.dds \ + $(BUILD_DIR)/ba_data/textures/actionHeroIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/advancedIcon.dds \ + $(BUILD_DIR)/ba_data/textures/agentColor.dds \ + $(BUILD_DIR)/ba_data/textures/agentColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/agentIcon.dds \ + $(BUILD_DIR)/ba_data/textures/agentIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/aliBSRemoteIOSQR.dds \ + $(BUILD_DIR)/ba_data/textures/aliColor.dds \ + $(BUILD_DIR)/ba_data/textures/aliColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/aliControllerQR.dds \ + $(BUILD_DIR)/ba_data/textures/aliIcon.dds \ + $(BUILD_DIR)/ba_data/textures/aliIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/aliSplash.dds \ + $(BUILD_DIR)/ba_data/textures/alienColor.dds \ + $(BUILD_DIR)/ba_data/textures/alienColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/alienIcon.dds \ + $(BUILD_DIR)/ba_data/textures/alienIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/alwaysLandBGColor.dds \ + $(BUILD_DIR)/ba_data/textures/alwaysLandLevelColor.dds \ + $(BUILD_DIR)/ba_data/textures/alwaysLandPreview.dds \ + $(BUILD_DIR)/ba_data/textures/analogStick.dds \ + $(BUILD_DIR)/ba_data/textures/arrow.dds \ + $(BUILD_DIR)/ba_data/textures/assassinColor.dds \ + $(BUILD_DIR)/ba_data/textures/assassinColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/assassinIcon.dds \ + $(BUILD_DIR)/ba_data/textures/assassinIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/audioIcon.dds \ + $(BUILD_DIR)/ba_data/textures/backIcon.dds \ + $(BUILD_DIR)/ba_data/textures/bar.dds \ + $(BUILD_DIR)/ba_data/textures/bearColor.dds \ + $(BUILD_DIR)/ba_data/textures/bearColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/bearIcon.dds \ + $(BUILD_DIR)/ba_data/textures/bearIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/bg.dds \ + $(BUILD_DIR)/ba_data/textures/bigG.dds \ + $(BUILD_DIR)/ba_data/textures/bigGPreview.dds \ + $(BUILD_DIR)/ba_data/textures/black.dds \ + $(BUILD_DIR)/ba_data/textures/bombButton.dds \ + $(BUILD_DIR)/ba_data/textures/bombColor.dds \ + $(BUILD_DIR)/ba_data/textures/bombColorIce.dds \ + $(BUILD_DIR)/ba_data/textures/bombStickyColor.dds \ + $(BUILD_DIR)/ba_data/textures/bonesColor.dds \ + $(BUILD_DIR)/ba_data/textures/bonesColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/bonesIcon.dds \ + $(BUILD_DIR)/ba_data/textures/bonesIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/boxingGlovesColor.dds \ + $(BUILD_DIR)/ba_data/textures/bridgitLevelColor.dds \ + $(BUILD_DIR)/ba_data/textures/bridgitPreview.dds \ + $(BUILD_DIR)/ba_data/textures/bunnyColor.dds \ + $(BUILD_DIR)/ba_data/textures/bunnyColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/bunnyIcon.dds \ + $(BUILD_DIR)/ba_data/textures/bunnyIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/buttonBomb.dds \ + $(BUILD_DIR)/ba_data/textures/buttonJump.dds \ + $(BUILD_DIR)/ba_data/textures/buttonPickUp.dds \ + $(BUILD_DIR)/ba_data/textures/buttonPunch.dds \ + $(BUILD_DIR)/ba_data/textures/buttonSquare.dds \ + $(BUILD_DIR)/ba_data/textures/chTitleChar1.dds \ + $(BUILD_DIR)/ba_data/textures/chTitleChar2.dds \ + $(BUILD_DIR)/ba_data/textures/chTitleChar3.dds \ + $(BUILD_DIR)/ba_data/textures/chTitleChar4.dds \ + $(BUILD_DIR)/ba_data/textures/chTitleChar5.dds \ + $(BUILD_DIR)/ba_data/textures/characterIconMask.dds \ + $(BUILD_DIR)/ba_data/textures/chestIcon.dds \ + $(BUILD_DIR)/ba_data/textures/chestIconEmpty.dds \ + $(BUILD_DIR)/ba_data/textures/chestIconMulti.dds \ + $(BUILD_DIR)/ba_data/textures/chestOpenIcon.dds \ + $(BUILD_DIR)/ba_data/textures/circle.dds \ + $(BUILD_DIR)/ba_data/textures/circleNoAlpha.dds \ + $(BUILD_DIR)/ba_data/textures/circleOutline.dds \ + $(BUILD_DIR)/ba_data/textures/circleOutlineNoAlpha.dds \ + $(BUILD_DIR)/ba_data/textures/circleShadow.dds \ + $(BUILD_DIR)/ba_data/textures/circleZigZag.dds \ + $(BUILD_DIR)/ba_data/textures/coin.dds \ + $(BUILD_DIR)/ba_data/textures/controllerIcon.dds \ + $(BUILD_DIR)/ba_data/textures/courtyardLevelColor.dds \ + $(BUILD_DIR)/ba_data/textures/courtyardPreview.dds \ + $(BUILD_DIR)/ba_data/textures/cowboyColor.dds \ + $(BUILD_DIR)/ba_data/textures/cowboyColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/cowboyIcon.dds \ + $(BUILD_DIR)/ba_data/textures/cowboyIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/cragCastleLevelColor.dds \ + $(BUILD_DIR)/ba_data/textures/cragCastlePreview.dds \ + $(BUILD_DIR)/ba_data/textures/crossOut.dds \ + $(BUILD_DIR)/ba_data/textures/crossOutMask.dds \ + $(BUILD_DIR)/ba_data/textures/cursor.dds \ + $(BUILD_DIR)/ba_data/textures/cuteSpaz.dds \ + $(BUILD_DIR)/ba_data/textures/cyborgColor.dds \ + $(BUILD_DIR)/ba_data/textures/cyborgColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/cyborgIcon.dds \ + $(BUILD_DIR)/ba_data/textures/cyborgIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/discordLogo.dds \ + $(BUILD_DIR)/ba_data/textures/doomShroomBGColor.dds \ + $(BUILD_DIR)/ba_data/textures/doomShroomLevelColor.dds \ + $(BUILD_DIR)/ba_data/textures/doomShroomPreview.dds \ + $(BUILD_DIR)/ba_data/textures/downButton.dds \ + $(BUILD_DIR)/ba_data/textures/egg1.dds \ + $(BUILD_DIR)/ba_data/textures/egg2.dds \ + $(BUILD_DIR)/ba_data/textures/egg3.dds \ + $(BUILD_DIR)/ba_data/textures/egg4.dds \ + $(BUILD_DIR)/ba_data/textures/eggTex1.dds \ + $(BUILD_DIR)/ba_data/textures/eggTex2.dds \ + $(BUILD_DIR)/ba_data/textures/eggTex3.dds \ + $(BUILD_DIR)/ba_data/textures/empty.dds \ + $(BUILD_DIR)/ba_data/textures/explosion.dds \ + $(BUILD_DIR)/ba_data/textures/eyeColor.dds \ + $(BUILD_DIR)/ba_data/textures/eyeColorTintMask.dds \ + $(BUILD_DIR)/ba_data/textures/file.dds \ + $(BUILD_DIR)/ba_data/textures/flagColor.dds \ + $(BUILD_DIR)/ba_data/textures/flagPoleColor.dds \ + $(BUILD_DIR)/ba_data/textures/folder.dds \ + $(BUILD_DIR)/ba_data/textures/fontBig.dds \ + $(BUILD_DIR)/ba_data/textures/fontExtras.dds \ + $(BUILD_DIR)/ba_data/textures/fontExtras2.dds \ + $(BUILD_DIR)/ba_data/textures/fontExtras3.dds \ + $(BUILD_DIR)/ba_data/textures/fontExtras4.dds \ + $(BUILD_DIR)/ba_data/textures/fontSmall0.dds \ + $(BUILD_DIR)/ba_data/textures/fontSmall1.dds \ + $(BUILD_DIR)/ba_data/textures/fontSmall2.dds \ + $(BUILD_DIR)/ba_data/textures/fontSmall3.dds \ + $(BUILD_DIR)/ba_data/textures/fontSmall4.dds \ + $(BUILD_DIR)/ba_data/textures/fontSmall5.dds \ + $(BUILD_DIR)/ba_data/textures/fontSmall6.dds \ + $(BUILD_DIR)/ba_data/textures/fontSmall7.dds \ + $(BUILD_DIR)/ba_data/textures/footballStadium.dds \ + $(BUILD_DIR)/ba_data/textures/footballStadiumPreview.dds \ + $(BUILD_DIR)/ba_data/textures/frameInset.dds \ + $(BUILD_DIR)/ba_data/textures/frostyColor.dds \ + $(BUILD_DIR)/ba_data/textures/frostyColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/frostyIcon.dds \ + $(BUILD_DIR)/ba_data/textures/frostyIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/fuse.dds \ + $(BUILD_DIR)/ba_data/textures/gameCenterIcon.dds \ + $(BUILD_DIR)/ba_data/textures/gameCircleIcon.dds \ + $(BUILD_DIR)/ba_data/textures/githubLogo.dds \ + $(BUILD_DIR)/ba_data/textures/gladiatorColor.dds \ + $(BUILD_DIR)/ba_data/textures/gladiatorColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/gladiatorIcon.dds \ + $(BUILD_DIR)/ba_data/textures/gladiatorIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/glow.dds \ + $(BUILD_DIR)/ba_data/textures/googlePlayAchievementsIcon.dds \ + $(BUILD_DIR)/ba_data/textures/googlePlayGamesIcon.dds \ + $(BUILD_DIR)/ba_data/textures/googlePlayLeaderboardsIcon.dds \ + $(BUILD_DIR)/ba_data/textures/googlePlusIcon.dds \ + $(BUILD_DIR)/ba_data/textures/googlePlusSignInButton.dds \ + $(BUILD_DIR)/ba_data/textures/graphicsIcon.dds \ + $(BUILD_DIR)/ba_data/textures/heart.dds \ + $(BUILD_DIR)/ba_data/textures/hockeyStadium.dds \ + $(BUILD_DIR)/ba_data/textures/hockeyStadiumPreview.dds \ + $(BUILD_DIR)/ba_data/textures/iconOnslaught.dds \ + $(BUILD_DIR)/ba_data/textures/iconRunaround.dds \ + $(BUILD_DIR)/ba_data/textures/iircadeLogo.dds \ + $(BUILD_DIR)/ba_data/textures/impactBombColor.dds \ + $(BUILD_DIR)/ba_data/textures/impactBombColorLit.dds \ + $(BUILD_DIR)/ba_data/textures/inventoryIcon.dds \ + $(BUILD_DIR)/ba_data/textures/jackColor.dds \ + $(BUILD_DIR)/ba_data/textures/jackColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/jackIcon.dds \ + $(BUILD_DIR)/ba_data/textures/jackIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/jumpsuitColor.dds \ + $(BUILD_DIR)/ba_data/textures/jumpsuitColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/jumpsuitIcon.dds \ + $(BUILD_DIR)/ba_data/textures/jumpsuitIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/kronk.dds \ + $(BUILD_DIR)/ba_data/textures/kronkColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/kronkIcon.dds \ + $(BUILD_DIR)/ba_data/textures/kronkIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/lakeFrigid.dds \ + $(BUILD_DIR)/ba_data/textures/lakeFrigidPreview.dds \ + $(BUILD_DIR)/ba_data/textures/lakeFrigidReflections.dds \ + $(BUILD_DIR)/ba_data/textures/landMine.dds \ + $(BUILD_DIR)/ba_data/textures/landMineLit.dds \ + $(BUILD_DIR)/ba_data/textures/leaderboardsIcon.dds \ + $(BUILD_DIR)/ba_data/textures/leftButton.dds \ + $(BUILD_DIR)/ba_data/textures/levelIcon.dds \ + $(BUILD_DIR)/ba_data/textures/light.dds \ + $(BUILD_DIR)/ba_data/textures/lightSharp.dds \ + $(BUILD_DIR)/ba_data/textures/lightSoft.dds \ + $(BUILD_DIR)/ba_data/textures/lock.dds \ + $(BUILD_DIR)/ba_data/textures/logIcon.dds \ + $(BUILD_DIR)/ba_data/textures/logo.dds \ + $(BUILD_DIR)/ba_data/textures/logoEaster.dds \ + $(BUILD_DIR)/ba_data/textures/mapPreviewMask.dds \ + $(BUILD_DIR)/ba_data/textures/medalBronze.dds \ + $(BUILD_DIR)/ba_data/textures/medalComplete.dds \ + $(BUILD_DIR)/ba_data/textures/medalGold.dds \ + $(BUILD_DIR)/ba_data/textures/medalSilver.dds \ + $(BUILD_DIR)/ba_data/textures/melColor.dds \ + $(BUILD_DIR)/ba_data/textures/melColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/melIcon.dds \ + $(BUILD_DIR)/ba_data/textures/melIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/menuBG.dds \ + $(BUILD_DIR)/ba_data/textures/menuButton.dds \ + $(BUILD_DIR)/ba_data/textures/menuIcon.dds \ + $(BUILD_DIR)/ba_data/textures/merch.dds \ + $(BUILD_DIR)/ba_data/textures/meter.dds \ + $(BUILD_DIR)/ba_data/textures/monkeyFaceLevelColor.dds \ + $(BUILD_DIR)/ba_data/textures/monkeyFacePreview.dds \ + $(BUILD_DIR)/ba_data/textures/multiplayerExamples.dds \ + $(BUILD_DIR)/ba_data/textures/natureBackgroundColor.dds \ + $(BUILD_DIR)/ba_data/textures/neoSpazColor.dds \ + $(BUILD_DIR)/ba_data/textures/neoSpazColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/neoSpazIcon.dds \ + $(BUILD_DIR)/ba_data/textures/neoSpazIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/nextLevelIcon.dds \ + $(BUILD_DIR)/ba_data/textures/ninjaColor.dds \ + $(BUILD_DIR)/ba_data/textures/ninjaColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/ninjaIcon.dds \ + $(BUILD_DIR)/ba_data/textures/ninjaIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/nub.dds \ + $(BUILD_DIR)/ba_data/textures/null.dds \ + $(BUILD_DIR)/ba_data/textures/oldLadyColor.dds \ + $(BUILD_DIR)/ba_data/textures/oldLadyColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/oldLadyIcon.dds \ + $(BUILD_DIR)/ba_data/textures/oldLadyIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/operaSingerColor.dds \ + $(BUILD_DIR)/ba_data/textures/operaSingerColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/operaSingerIcon.dds \ + $(BUILD_DIR)/ba_data/textures/operaSingerIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/ouyaAButton.dds \ + $(BUILD_DIR)/ba_data/textures/ouyaIcon.dds \ + $(BUILD_DIR)/ba_data/textures/ouyaOButton.dds \ + $(BUILD_DIR)/ba_data/textures/ouyaUButton.dds \ + $(BUILD_DIR)/ba_data/textures/ouyaYButton.dds \ + $(BUILD_DIR)/ba_data/textures/penguinColor.dds \ + $(BUILD_DIR)/ba_data/textures/penguinColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/penguinIcon.dds \ + $(BUILD_DIR)/ba_data/textures/penguinIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/pixieColor.dds \ + $(BUILD_DIR)/ba_data/textures/pixieColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/pixieIcon.dds \ + $(BUILD_DIR)/ba_data/textures/pixieIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/playerLineup.dds \ + $(BUILD_DIR)/ba_data/textures/powerupBomb.dds \ + $(BUILD_DIR)/ba_data/textures/powerupCurse.dds \ + $(BUILD_DIR)/ba_data/textures/powerupHealth.dds \ + $(BUILD_DIR)/ba_data/textures/powerupIceBombs.dds \ + $(BUILD_DIR)/ba_data/textures/powerupImpactBombs.dds \ + $(BUILD_DIR)/ba_data/textures/powerupLandMines.dds \ + $(BUILD_DIR)/ba_data/textures/powerupPunch.dds \ + $(BUILD_DIR)/ba_data/textures/powerupShield.dds \ + $(BUILD_DIR)/ba_data/textures/powerupSpeed.dds \ + $(BUILD_DIR)/ba_data/textures/powerupStickyBombs.dds \ + $(BUILD_DIR)/ba_data/textures/puckColor.dds \ + $(BUILD_DIR)/ba_data/textures/rampageBGColor.dds \ + $(BUILD_DIR)/ba_data/textures/rampageBGColor2.dds \ + $(BUILD_DIR)/ba_data/textures/rampageLevelColor.dds \ + $(BUILD_DIR)/ba_data/textures/rampagePreview.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_+x.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_+y.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_+z.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_-x.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_-y.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_-z.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_+x.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_+y.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_+z.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_-x.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_-y.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_-z.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_+x.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_+y.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_+z.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_-x.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_-y.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_-z.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_+x.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_+y.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_+z.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_-x.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_-y.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_-z.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_+x.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_+y.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_+z.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_-x.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_-y.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_-z.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_+x.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_+y.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_+z.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_-x.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_-y.dds \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_-z.dds \ + $(BUILD_DIR)/ba_data/textures/replayIcon.dds \ + $(BUILD_DIR)/ba_data/textures/rgbStripes.dds \ + $(BUILD_DIR)/ba_data/textures/rightButton.dds \ + $(BUILD_DIR)/ba_data/textures/robotColor.dds \ + $(BUILD_DIR)/ba_data/textures/robotColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/robotIcon.dds \ + $(BUILD_DIR)/ba_data/textures/robotIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/roundaboutLevelColor.dds \ + $(BUILD_DIR)/ba_data/textures/roundaboutPreview.dds \ + $(BUILD_DIR)/ba_data/textures/santaColor.dds \ + $(BUILD_DIR)/ba_data/textures/santaColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/santaIcon.dds \ + $(BUILD_DIR)/ba_data/textures/santaIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/scorch.dds \ + $(BUILD_DIR)/ba_data/textures/scorchBig.dds \ + $(BUILD_DIR)/ba_data/textures/scrollWidget.dds \ + $(BUILD_DIR)/ba_data/textures/scrollWidgetGlow.dds \ + $(BUILD_DIR)/ba_data/textures/settingsIcon.dds \ + $(BUILD_DIR)/ba_data/textures/shadow.dds \ + $(BUILD_DIR)/ba_data/textures/shadowSharp.dds \ + $(BUILD_DIR)/ba_data/textures/shadowSoft.dds \ + $(BUILD_DIR)/ba_data/textures/shield.dds \ + $(BUILD_DIR)/ba_data/textures/shrapnel1Color.dds \ + $(BUILD_DIR)/ba_data/textures/slash.dds \ + $(BUILD_DIR)/ba_data/textures/smoke.dds \ + $(BUILD_DIR)/ba_data/textures/softRect.dds \ + $(BUILD_DIR)/ba_data/textures/softRect2.dds \ + $(BUILD_DIR)/ba_data/textures/softRectVertical.dds \ + $(BUILD_DIR)/ba_data/textures/sparks.dds \ + $(BUILD_DIR)/ba_data/textures/star.dds \ + $(BUILD_DIR)/ba_data/textures/startButton.dds \ + $(BUILD_DIR)/ba_data/textures/stepRightUpLevelColor.dds \ + $(BUILD_DIR)/ba_data/textures/stepRightUpPreview.dds \ + $(BUILD_DIR)/ba_data/textures/storeCharacter.dds \ + $(BUILD_DIR)/ba_data/textures/storeCharacterEaster.dds \ + $(BUILD_DIR)/ba_data/textures/storeCharacterXmas.dds \ + $(BUILD_DIR)/ba_data/textures/storeIcon.dds \ + $(BUILD_DIR)/ba_data/textures/superheroColor.dds \ + $(BUILD_DIR)/ba_data/textures/superheroColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/superheroIcon.dds \ + $(BUILD_DIR)/ba_data/textures/superheroIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/textClearButton.dds \ + $(BUILD_DIR)/ba_data/textures/thePadLevelColor.dds \ + $(BUILD_DIR)/ba_data/textures/thePadPreview.dds \ + $(BUILD_DIR)/ba_data/textures/ticketRoll.dds \ + $(BUILD_DIR)/ba_data/textures/ticketRollBig.dds \ + $(BUILD_DIR)/ba_data/textures/ticketRolls.dds \ + $(BUILD_DIR)/ba_data/textures/tickets.dds \ + $(BUILD_DIR)/ba_data/textures/ticketsMore.dds \ + $(BUILD_DIR)/ba_data/textures/tipTopBGColor.dds \ + $(BUILD_DIR)/ba_data/textures/tipTopLevelColor.dds \ + $(BUILD_DIR)/ba_data/textures/tipTopPreview.dds \ + $(BUILD_DIR)/ba_data/textures/tnt.dds \ + $(BUILD_DIR)/ba_data/textures/touchArrows.dds \ + $(BUILD_DIR)/ba_data/textures/touchArrowsActions.dds \ + $(BUILD_DIR)/ba_data/textures/towerDLevelColor.dds \ + $(BUILD_DIR)/ba_data/textures/towerDPreview.dds \ + $(BUILD_DIR)/ba_data/textures/treesColor.dds \ + $(BUILD_DIR)/ba_data/textures/trophy.dds \ + $(BUILD_DIR)/ba_data/textures/tv.dds \ + $(BUILD_DIR)/ba_data/textures/uiAtlas.dds \ + $(BUILD_DIR)/ba_data/textures/uiAtlas2.dds \ + $(BUILD_DIR)/ba_data/textures/upButton.dds \ + $(BUILD_DIR)/ba_data/textures/usersButton.dds \ + $(BUILD_DIR)/ba_data/textures/vrFillMound.dds \ + $(BUILD_DIR)/ba_data/textures/warriorColor.dds \ + $(BUILD_DIR)/ba_data/textures/warriorColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/warriorIcon.dds \ + $(BUILD_DIR)/ba_data/textures/warriorIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/white.dds \ + $(BUILD_DIR)/ba_data/textures/windowHSmallVMed.dds \ + $(BUILD_DIR)/ba_data/textures/windowHSmallVSmall.dds \ + $(BUILD_DIR)/ba_data/textures/wings.dds \ + $(BUILD_DIR)/ba_data/textures/witchColor.dds \ + $(BUILD_DIR)/ba_data/textures/witchColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/witchIcon.dds \ + $(BUILD_DIR)/ba_data/textures/witchIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/wizardColor.dds \ + $(BUILD_DIR)/ba_data/textures/wizardColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/wizardIcon.dds \ + $(BUILD_DIR)/ba_data/textures/wizardIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/wrestlerColor.dds \ + $(BUILD_DIR)/ba_data/textures/wrestlerColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/wrestlerIcon.dds \ + $(BUILD_DIR)/ba_data/textures/wrestlerIconColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/zigZagLevelColor.dds \ + $(BUILD_DIR)/ba_data/textures/zigzagPreview.dds \ + $(BUILD_DIR)/ba_data/textures/zoeColor.dds \ + $(BUILD_DIR)/ba_data/textures/zoeColorMask.dds \ + $(BUILD_DIR)/ba_data/textures/zoeIcon.dds \ + $(BUILD_DIR)/ba_data/textures/zoeIconColorMask.dds + +TEX2D_PVR_TARGETS = \ + $(BUILD_DIR)/ba_data/textures/achievementBoxer.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementCrossHair.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementDualWielding.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementEmpty.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementFlawlessVictory.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementFootballShutout.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementFootballVictory.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementFreeLoader.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementGotTheMoves.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementInControl.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementMedalLarge.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementMedalMedium.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementMedalSmall.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementMine.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementOffYouGo.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementOnslaught.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementOutline.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementRunaround.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementSharingIsCaring.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementStayinAlive.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementSuperPunch.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementTNT.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementTeamPlayer.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementWall.pvr \ + $(BUILD_DIR)/ba_data/textures/achievementsIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/actionButtons.pvr \ + $(BUILD_DIR)/ba_data/textures/actionHeroColor.pvr \ + $(BUILD_DIR)/ba_data/textures/actionHeroColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/actionHeroIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/actionHeroIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/advancedIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/agentColor.pvr \ + $(BUILD_DIR)/ba_data/textures/agentColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/agentIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/agentIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/aliBSRemoteIOSQR.pvr \ + $(BUILD_DIR)/ba_data/textures/aliColor.pvr \ + $(BUILD_DIR)/ba_data/textures/aliColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/aliControllerQR.pvr \ + $(BUILD_DIR)/ba_data/textures/aliIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/aliIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/aliSplash.pvr \ + $(BUILD_DIR)/ba_data/textures/alienColor.pvr \ + $(BUILD_DIR)/ba_data/textures/alienColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/alienIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/alienIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/alwaysLandBGColor.pvr \ + $(BUILD_DIR)/ba_data/textures/alwaysLandLevelColor.pvr \ + $(BUILD_DIR)/ba_data/textures/alwaysLandPreview.pvr \ + $(BUILD_DIR)/ba_data/textures/analogStick.pvr \ + $(BUILD_DIR)/ba_data/textures/arrow.pvr \ + $(BUILD_DIR)/ba_data/textures/assassinColor.pvr \ + $(BUILD_DIR)/ba_data/textures/assassinColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/assassinIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/assassinIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/audioIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/backIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/bar.pvr \ + $(BUILD_DIR)/ba_data/textures/bearColor.pvr \ + $(BUILD_DIR)/ba_data/textures/bearColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/bearIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/bearIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/bg.pvr \ + $(BUILD_DIR)/ba_data/textures/bigG.pvr \ + $(BUILD_DIR)/ba_data/textures/bigGPreview.pvr \ + $(BUILD_DIR)/ba_data/textures/black.pvr \ + $(BUILD_DIR)/ba_data/textures/bombButton.pvr \ + $(BUILD_DIR)/ba_data/textures/bombColor.pvr \ + $(BUILD_DIR)/ba_data/textures/bombColorIce.pvr \ + $(BUILD_DIR)/ba_data/textures/bombStickyColor.pvr \ + $(BUILD_DIR)/ba_data/textures/bonesColor.pvr \ + $(BUILD_DIR)/ba_data/textures/bonesColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/bonesIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/bonesIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/boxingGlovesColor.pvr \ + $(BUILD_DIR)/ba_data/textures/bridgitLevelColor.pvr \ + $(BUILD_DIR)/ba_data/textures/bridgitPreview.pvr \ + $(BUILD_DIR)/ba_data/textures/bunnyColor.pvr \ + $(BUILD_DIR)/ba_data/textures/bunnyColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/bunnyIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/bunnyIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/buttonBomb.pvr \ + $(BUILD_DIR)/ba_data/textures/buttonJump.pvr \ + $(BUILD_DIR)/ba_data/textures/buttonPickUp.pvr \ + $(BUILD_DIR)/ba_data/textures/buttonPunch.pvr \ + $(BUILD_DIR)/ba_data/textures/buttonSquare.pvr \ + $(BUILD_DIR)/ba_data/textures/chTitleChar1.pvr \ + $(BUILD_DIR)/ba_data/textures/chTitleChar2.pvr \ + $(BUILD_DIR)/ba_data/textures/chTitleChar3.pvr \ + $(BUILD_DIR)/ba_data/textures/chTitleChar4.pvr \ + $(BUILD_DIR)/ba_data/textures/chTitleChar5.pvr \ + $(BUILD_DIR)/ba_data/textures/characterIconMask.pvr \ + $(BUILD_DIR)/ba_data/textures/chestIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/chestIconEmpty.pvr \ + $(BUILD_DIR)/ba_data/textures/chestIconMulti.pvr \ + $(BUILD_DIR)/ba_data/textures/chestOpenIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/circle.pvr \ + $(BUILD_DIR)/ba_data/textures/circleNoAlpha.pvr \ + $(BUILD_DIR)/ba_data/textures/circleOutline.pvr \ + $(BUILD_DIR)/ba_data/textures/circleOutlineNoAlpha.pvr \ + $(BUILD_DIR)/ba_data/textures/circleShadow.pvr \ + $(BUILD_DIR)/ba_data/textures/circleZigZag.pvr \ + $(BUILD_DIR)/ba_data/textures/coin.pvr \ + $(BUILD_DIR)/ba_data/textures/controllerIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/courtyardLevelColor.pvr \ + $(BUILD_DIR)/ba_data/textures/courtyardPreview.pvr \ + $(BUILD_DIR)/ba_data/textures/cowboyColor.pvr \ + $(BUILD_DIR)/ba_data/textures/cowboyColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/cowboyIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/cowboyIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/cragCastleLevelColor.pvr \ + $(BUILD_DIR)/ba_data/textures/cragCastlePreview.pvr \ + $(BUILD_DIR)/ba_data/textures/crossOut.pvr \ + $(BUILD_DIR)/ba_data/textures/crossOutMask.pvr \ + $(BUILD_DIR)/ba_data/textures/cursor.pvr \ + $(BUILD_DIR)/ba_data/textures/cuteSpaz.pvr \ + $(BUILD_DIR)/ba_data/textures/cyborgColor.pvr \ + $(BUILD_DIR)/ba_data/textures/cyborgColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/cyborgIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/cyborgIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/discordLogo.pvr \ + $(BUILD_DIR)/ba_data/textures/doomShroomBGColor.pvr \ + $(BUILD_DIR)/ba_data/textures/doomShroomLevelColor.pvr \ + $(BUILD_DIR)/ba_data/textures/doomShroomPreview.pvr \ + $(BUILD_DIR)/ba_data/textures/downButton.pvr \ + $(BUILD_DIR)/ba_data/textures/egg1.pvr \ + $(BUILD_DIR)/ba_data/textures/egg2.pvr \ + $(BUILD_DIR)/ba_data/textures/egg3.pvr \ + $(BUILD_DIR)/ba_data/textures/egg4.pvr \ + $(BUILD_DIR)/ba_data/textures/eggTex1.pvr \ + $(BUILD_DIR)/ba_data/textures/eggTex2.pvr \ + $(BUILD_DIR)/ba_data/textures/eggTex3.pvr \ + $(BUILD_DIR)/ba_data/textures/empty.pvr \ + $(BUILD_DIR)/ba_data/textures/explosion.pvr \ + $(BUILD_DIR)/ba_data/textures/eyeColor.pvr \ + $(BUILD_DIR)/ba_data/textures/eyeColorTintMask.pvr \ + $(BUILD_DIR)/ba_data/textures/file.pvr \ + $(BUILD_DIR)/ba_data/textures/flagColor.pvr \ + $(BUILD_DIR)/ba_data/textures/flagPoleColor.pvr \ + $(BUILD_DIR)/ba_data/textures/folder.pvr \ + $(BUILD_DIR)/ba_data/textures/fontBig.pvr \ + $(BUILD_DIR)/ba_data/textures/fontExtras.pvr \ + $(BUILD_DIR)/ba_data/textures/fontExtras2.pvr \ + $(BUILD_DIR)/ba_data/textures/fontExtras3.pvr \ + $(BUILD_DIR)/ba_data/textures/fontExtras4.pvr \ + $(BUILD_DIR)/ba_data/textures/fontSmall0.pvr \ + $(BUILD_DIR)/ba_data/textures/fontSmall1.pvr \ + $(BUILD_DIR)/ba_data/textures/fontSmall2.pvr \ + $(BUILD_DIR)/ba_data/textures/fontSmall3.pvr \ + $(BUILD_DIR)/ba_data/textures/fontSmall4.pvr \ + $(BUILD_DIR)/ba_data/textures/fontSmall5.pvr \ + $(BUILD_DIR)/ba_data/textures/fontSmall6.pvr \ + $(BUILD_DIR)/ba_data/textures/fontSmall7.pvr \ + $(BUILD_DIR)/ba_data/textures/footballStadium.pvr \ + $(BUILD_DIR)/ba_data/textures/footballStadiumPreview.pvr \ + $(BUILD_DIR)/ba_data/textures/frameInset.pvr \ + $(BUILD_DIR)/ba_data/textures/frostyColor.pvr \ + $(BUILD_DIR)/ba_data/textures/frostyColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/frostyIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/frostyIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/fuse.pvr \ + $(BUILD_DIR)/ba_data/textures/gameCenterIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/gameCircleIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/githubLogo.pvr \ + $(BUILD_DIR)/ba_data/textures/gladiatorColor.pvr \ + $(BUILD_DIR)/ba_data/textures/gladiatorColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/gladiatorIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/gladiatorIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/glow.pvr \ + $(BUILD_DIR)/ba_data/textures/googlePlayAchievementsIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/googlePlayGamesIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/googlePlayLeaderboardsIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/googlePlusIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/googlePlusSignInButton.pvr \ + $(BUILD_DIR)/ba_data/textures/graphicsIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/heart.pvr \ + $(BUILD_DIR)/ba_data/textures/hockeyStadium.pvr \ + $(BUILD_DIR)/ba_data/textures/hockeyStadiumPreview.pvr \ + $(BUILD_DIR)/ba_data/textures/iconOnslaught.pvr \ + $(BUILD_DIR)/ba_data/textures/iconRunaround.pvr \ + $(BUILD_DIR)/ba_data/textures/iircadeLogo.pvr \ + $(BUILD_DIR)/ba_data/textures/impactBombColor.pvr \ + $(BUILD_DIR)/ba_data/textures/impactBombColorLit.pvr \ + $(BUILD_DIR)/ba_data/textures/inventoryIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/jackColor.pvr \ + $(BUILD_DIR)/ba_data/textures/jackColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/jackIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/jackIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/jumpsuitColor.pvr \ + $(BUILD_DIR)/ba_data/textures/jumpsuitColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/jumpsuitIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/jumpsuitIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/kronk.pvr \ + $(BUILD_DIR)/ba_data/textures/kronkColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/kronkIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/kronkIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/lakeFrigid.pvr \ + $(BUILD_DIR)/ba_data/textures/lakeFrigidPreview.pvr \ + $(BUILD_DIR)/ba_data/textures/lakeFrigidReflections.pvr \ + $(BUILD_DIR)/ba_data/textures/landMine.pvr \ + $(BUILD_DIR)/ba_data/textures/landMineLit.pvr \ + $(BUILD_DIR)/ba_data/textures/leaderboardsIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/leftButton.pvr \ + $(BUILD_DIR)/ba_data/textures/levelIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/light.pvr \ + $(BUILD_DIR)/ba_data/textures/lightSharp.pvr \ + $(BUILD_DIR)/ba_data/textures/lightSoft.pvr \ + $(BUILD_DIR)/ba_data/textures/lock.pvr \ + $(BUILD_DIR)/ba_data/textures/logIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/logo.pvr \ + $(BUILD_DIR)/ba_data/textures/logoEaster.pvr \ + $(BUILD_DIR)/ba_data/textures/mapPreviewMask.pvr \ + $(BUILD_DIR)/ba_data/textures/medalBronze.pvr \ + $(BUILD_DIR)/ba_data/textures/medalComplete.pvr \ + $(BUILD_DIR)/ba_data/textures/medalGold.pvr \ + $(BUILD_DIR)/ba_data/textures/medalSilver.pvr \ + $(BUILD_DIR)/ba_data/textures/melColor.pvr \ + $(BUILD_DIR)/ba_data/textures/melColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/melIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/melIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/menuBG.pvr \ + $(BUILD_DIR)/ba_data/textures/menuButton.pvr \ + $(BUILD_DIR)/ba_data/textures/menuIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/merch.pvr \ + $(BUILD_DIR)/ba_data/textures/meter.pvr \ + $(BUILD_DIR)/ba_data/textures/monkeyFaceLevelColor.pvr \ + $(BUILD_DIR)/ba_data/textures/monkeyFacePreview.pvr \ + $(BUILD_DIR)/ba_data/textures/multiplayerExamples.pvr \ + $(BUILD_DIR)/ba_data/textures/natureBackgroundColor.pvr \ + $(BUILD_DIR)/ba_data/textures/neoSpazColor.pvr \ + $(BUILD_DIR)/ba_data/textures/neoSpazColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/neoSpazIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/neoSpazIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/nextLevelIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/ninjaColor.pvr \ + $(BUILD_DIR)/ba_data/textures/ninjaColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/ninjaIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/ninjaIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/nub.pvr \ + $(BUILD_DIR)/ba_data/textures/null.pvr \ + $(BUILD_DIR)/ba_data/textures/oldLadyColor.pvr \ + $(BUILD_DIR)/ba_data/textures/oldLadyColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/oldLadyIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/oldLadyIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/operaSingerColor.pvr \ + $(BUILD_DIR)/ba_data/textures/operaSingerColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/operaSingerIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/operaSingerIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/ouyaAButton.pvr \ + $(BUILD_DIR)/ba_data/textures/ouyaIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/ouyaOButton.pvr \ + $(BUILD_DIR)/ba_data/textures/ouyaUButton.pvr \ + $(BUILD_DIR)/ba_data/textures/ouyaYButton.pvr \ + $(BUILD_DIR)/ba_data/textures/penguinColor.pvr \ + $(BUILD_DIR)/ba_data/textures/penguinColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/penguinIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/penguinIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/pixieColor.pvr \ + $(BUILD_DIR)/ba_data/textures/pixieColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/pixieIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/pixieIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/playerLineup.pvr \ + $(BUILD_DIR)/ba_data/textures/powerupBomb.pvr \ + $(BUILD_DIR)/ba_data/textures/powerupCurse.pvr \ + $(BUILD_DIR)/ba_data/textures/powerupHealth.pvr \ + $(BUILD_DIR)/ba_data/textures/powerupIceBombs.pvr \ + $(BUILD_DIR)/ba_data/textures/powerupImpactBombs.pvr \ + $(BUILD_DIR)/ba_data/textures/powerupLandMines.pvr \ + $(BUILD_DIR)/ba_data/textures/powerupPunch.pvr \ + $(BUILD_DIR)/ba_data/textures/powerupShield.pvr \ + $(BUILD_DIR)/ba_data/textures/powerupSpeed.pvr \ + $(BUILD_DIR)/ba_data/textures/powerupStickyBombs.pvr \ + $(BUILD_DIR)/ba_data/textures/puckColor.pvr \ + $(BUILD_DIR)/ba_data/textures/rampageBGColor.pvr \ + $(BUILD_DIR)/ba_data/textures/rampageBGColor2.pvr \ + $(BUILD_DIR)/ba_data/textures/rampageLevelColor.pvr \ + $(BUILD_DIR)/ba_data/textures/rampagePreview.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_+x.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_+y.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_+z.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_-x.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_-y.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_-z.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_+x.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_+y.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_+z.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_-x.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_-y.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_-z.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_+x.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_+y.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_+z.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_-x.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_-y.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_-z.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_+x.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_+y.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_+z.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_-x.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_-y.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_-z.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_+x.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_+y.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_+z.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_-x.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_-y.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_-z.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_+x.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_+y.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_+z.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_-x.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_-y.pvr \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_-z.pvr \ + $(BUILD_DIR)/ba_data/textures/replayIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/rgbStripes.pvr \ + $(BUILD_DIR)/ba_data/textures/rightButton.pvr \ + $(BUILD_DIR)/ba_data/textures/robotColor.pvr \ + $(BUILD_DIR)/ba_data/textures/robotColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/robotIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/robotIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/roundaboutLevelColor.pvr \ + $(BUILD_DIR)/ba_data/textures/roundaboutPreview.pvr \ + $(BUILD_DIR)/ba_data/textures/santaColor.pvr \ + $(BUILD_DIR)/ba_data/textures/santaColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/santaIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/santaIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/scorch.pvr \ + $(BUILD_DIR)/ba_data/textures/scorchBig.pvr \ + $(BUILD_DIR)/ba_data/textures/scrollWidget.pvr \ + $(BUILD_DIR)/ba_data/textures/scrollWidgetGlow.pvr \ + $(BUILD_DIR)/ba_data/textures/settingsIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/shadow.pvr \ + $(BUILD_DIR)/ba_data/textures/shadowSharp.pvr \ + $(BUILD_DIR)/ba_data/textures/shadowSoft.pvr \ + $(BUILD_DIR)/ba_data/textures/shield.pvr \ + $(BUILD_DIR)/ba_data/textures/shrapnel1Color.pvr \ + $(BUILD_DIR)/ba_data/textures/slash.pvr \ + $(BUILD_DIR)/ba_data/textures/smoke.pvr \ + $(BUILD_DIR)/ba_data/textures/softRect.pvr \ + $(BUILD_DIR)/ba_data/textures/softRect2.pvr \ + $(BUILD_DIR)/ba_data/textures/softRectVertical.pvr \ + $(BUILD_DIR)/ba_data/textures/sparks.pvr \ + $(BUILD_DIR)/ba_data/textures/star.pvr \ + $(BUILD_DIR)/ba_data/textures/startButton.pvr \ + $(BUILD_DIR)/ba_data/textures/stepRightUpLevelColor.pvr \ + $(BUILD_DIR)/ba_data/textures/stepRightUpPreview.pvr \ + $(BUILD_DIR)/ba_data/textures/storeCharacter.pvr \ + $(BUILD_DIR)/ba_data/textures/storeCharacterEaster.pvr \ + $(BUILD_DIR)/ba_data/textures/storeCharacterXmas.pvr \ + $(BUILD_DIR)/ba_data/textures/storeIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/superheroColor.pvr \ + $(BUILD_DIR)/ba_data/textures/superheroColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/superheroIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/superheroIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/textClearButton.pvr \ + $(BUILD_DIR)/ba_data/textures/thePadLevelColor.pvr \ + $(BUILD_DIR)/ba_data/textures/thePadPreview.pvr \ + $(BUILD_DIR)/ba_data/textures/ticketRoll.pvr \ + $(BUILD_DIR)/ba_data/textures/ticketRollBig.pvr \ + $(BUILD_DIR)/ba_data/textures/ticketRolls.pvr \ + $(BUILD_DIR)/ba_data/textures/tickets.pvr \ + $(BUILD_DIR)/ba_data/textures/ticketsMore.pvr \ + $(BUILD_DIR)/ba_data/textures/tipTopBGColor.pvr \ + $(BUILD_DIR)/ba_data/textures/tipTopLevelColor.pvr \ + $(BUILD_DIR)/ba_data/textures/tipTopPreview.pvr \ + $(BUILD_DIR)/ba_data/textures/tnt.pvr \ + $(BUILD_DIR)/ba_data/textures/touchArrows.pvr \ + $(BUILD_DIR)/ba_data/textures/touchArrowsActions.pvr \ + $(BUILD_DIR)/ba_data/textures/towerDLevelColor.pvr \ + $(BUILD_DIR)/ba_data/textures/towerDPreview.pvr \ + $(BUILD_DIR)/ba_data/textures/treesColor.pvr \ + $(BUILD_DIR)/ba_data/textures/trophy.pvr \ + $(BUILD_DIR)/ba_data/textures/tv.pvr \ + $(BUILD_DIR)/ba_data/textures/uiAtlas.pvr \ + $(BUILD_DIR)/ba_data/textures/uiAtlas2.pvr \ + $(BUILD_DIR)/ba_data/textures/upButton.pvr \ + $(BUILD_DIR)/ba_data/textures/usersButton.pvr \ + $(BUILD_DIR)/ba_data/textures/vrFillMound.pvr \ + $(BUILD_DIR)/ba_data/textures/warriorColor.pvr \ + $(BUILD_DIR)/ba_data/textures/warriorColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/warriorIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/warriorIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/white.pvr \ + $(BUILD_DIR)/ba_data/textures/windowHSmallVMed.pvr \ + $(BUILD_DIR)/ba_data/textures/windowHSmallVSmall.pvr \ + $(BUILD_DIR)/ba_data/textures/wings.pvr \ + $(BUILD_DIR)/ba_data/textures/witchColor.pvr \ + $(BUILD_DIR)/ba_data/textures/witchColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/witchIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/witchIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/wizardColor.pvr \ + $(BUILD_DIR)/ba_data/textures/wizardColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/wizardIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/wizardIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/wrestlerColor.pvr \ + $(BUILD_DIR)/ba_data/textures/wrestlerColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/wrestlerIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/wrestlerIconColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/zigZagLevelColor.pvr \ + $(BUILD_DIR)/ba_data/textures/zigzagPreview.pvr \ + $(BUILD_DIR)/ba_data/textures/zoeColor.pvr \ + $(BUILD_DIR)/ba_data/textures/zoeColorMask.pvr \ + $(BUILD_DIR)/ba_data/textures/zoeIcon.pvr \ + $(BUILD_DIR)/ba_data/textures/zoeIconColorMask.pvr + +TEX2D_KTX_TARGETS = \ + $(BUILD_DIR)/ba_data/textures/achievementBoxer.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementCrossHair.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementDualWielding.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementEmpty.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementFlawlessVictory.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementFootballShutout.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementFootballVictory.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementFreeLoader.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementGotTheMoves.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementInControl.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementMedalLarge.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementMedalMedium.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementMedalSmall.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementMine.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementOffYouGo.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementOnslaught.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementOutline.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementRunaround.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementSharingIsCaring.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementStayinAlive.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementSuperPunch.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementTNT.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementTeamPlayer.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementWall.ktx \ + $(BUILD_DIR)/ba_data/textures/achievementsIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/actionButtons.ktx \ + $(BUILD_DIR)/ba_data/textures/actionHeroColor.ktx \ + $(BUILD_DIR)/ba_data/textures/actionHeroColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/actionHeroIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/actionHeroIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/advancedIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/agentColor.ktx \ + $(BUILD_DIR)/ba_data/textures/agentColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/agentIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/agentIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/aliBSRemoteIOSQR.ktx \ + $(BUILD_DIR)/ba_data/textures/aliColor.ktx \ + $(BUILD_DIR)/ba_data/textures/aliColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/aliControllerQR.ktx \ + $(BUILD_DIR)/ba_data/textures/aliIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/aliIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/aliSplash.ktx \ + $(BUILD_DIR)/ba_data/textures/alienColor.ktx \ + $(BUILD_DIR)/ba_data/textures/alienColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/alienIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/alienIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/alwaysLandBGColor.ktx \ + $(BUILD_DIR)/ba_data/textures/alwaysLandLevelColor.ktx \ + $(BUILD_DIR)/ba_data/textures/alwaysLandPreview.ktx \ + $(BUILD_DIR)/ba_data/textures/analogStick.ktx \ + $(BUILD_DIR)/ba_data/textures/arrow.ktx \ + $(BUILD_DIR)/ba_data/textures/assassinColor.ktx \ + $(BUILD_DIR)/ba_data/textures/assassinColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/assassinIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/assassinIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/audioIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/backIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/bar.ktx \ + $(BUILD_DIR)/ba_data/textures/bearColor.ktx \ + $(BUILD_DIR)/ba_data/textures/bearColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/bearIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/bearIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/bg.ktx \ + $(BUILD_DIR)/ba_data/textures/bigG.ktx \ + $(BUILD_DIR)/ba_data/textures/bigGPreview.ktx \ + $(BUILD_DIR)/ba_data/textures/black.ktx \ + $(BUILD_DIR)/ba_data/textures/bombButton.ktx \ + $(BUILD_DIR)/ba_data/textures/bombColor.ktx \ + $(BUILD_DIR)/ba_data/textures/bombColorIce.ktx \ + $(BUILD_DIR)/ba_data/textures/bombStickyColor.ktx \ + $(BUILD_DIR)/ba_data/textures/bonesColor.ktx \ + $(BUILD_DIR)/ba_data/textures/bonesColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/bonesIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/bonesIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/boxingGlovesColor.ktx \ + $(BUILD_DIR)/ba_data/textures/bridgitLevelColor.ktx \ + $(BUILD_DIR)/ba_data/textures/bridgitPreview.ktx \ + $(BUILD_DIR)/ba_data/textures/bunnyColor.ktx \ + $(BUILD_DIR)/ba_data/textures/bunnyColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/bunnyIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/bunnyIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/buttonBomb.ktx \ + $(BUILD_DIR)/ba_data/textures/buttonJump.ktx \ + $(BUILD_DIR)/ba_data/textures/buttonPickUp.ktx \ + $(BUILD_DIR)/ba_data/textures/buttonPunch.ktx \ + $(BUILD_DIR)/ba_data/textures/buttonSquare.ktx \ + $(BUILD_DIR)/ba_data/textures/chTitleChar1.ktx \ + $(BUILD_DIR)/ba_data/textures/chTitleChar2.ktx \ + $(BUILD_DIR)/ba_data/textures/chTitleChar3.ktx \ + $(BUILD_DIR)/ba_data/textures/chTitleChar4.ktx \ + $(BUILD_DIR)/ba_data/textures/chTitleChar5.ktx \ + $(BUILD_DIR)/ba_data/textures/characterIconMask.ktx \ + $(BUILD_DIR)/ba_data/textures/chestIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/chestIconEmpty.ktx \ + $(BUILD_DIR)/ba_data/textures/chestIconMulti.ktx \ + $(BUILD_DIR)/ba_data/textures/chestOpenIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/circle.ktx \ + $(BUILD_DIR)/ba_data/textures/circleNoAlpha.ktx \ + $(BUILD_DIR)/ba_data/textures/circleOutline.ktx \ + $(BUILD_DIR)/ba_data/textures/circleOutlineNoAlpha.ktx \ + $(BUILD_DIR)/ba_data/textures/circleShadow.ktx \ + $(BUILD_DIR)/ba_data/textures/circleZigZag.ktx \ + $(BUILD_DIR)/ba_data/textures/coin.ktx \ + $(BUILD_DIR)/ba_data/textures/controllerIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/courtyardLevelColor.ktx \ + $(BUILD_DIR)/ba_data/textures/courtyardPreview.ktx \ + $(BUILD_DIR)/ba_data/textures/cowboyColor.ktx \ + $(BUILD_DIR)/ba_data/textures/cowboyColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/cowboyIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/cowboyIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/cragCastleLevelColor.ktx \ + $(BUILD_DIR)/ba_data/textures/cragCastlePreview.ktx \ + $(BUILD_DIR)/ba_data/textures/crossOut.ktx \ + $(BUILD_DIR)/ba_data/textures/crossOutMask.ktx \ + $(BUILD_DIR)/ba_data/textures/cursor.ktx \ + $(BUILD_DIR)/ba_data/textures/cuteSpaz.ktx \ + $(BUILD_DIR)/ba_data/textures/cyborgColor.ktx \ + $(BUILD_DIR)/ba_data/textures/cyborgColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/cyborgIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/cyborgIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/discordLogo.ktx \ + $(BUILD_DIR)/ba_data/textures/doomShroomBGColor.ktx \ + $(BUILD_DIR)/ba_data/textures/doomShroomLevelColor.ktx \ + $(BUILD_DIR)/ba_data/textures/doomShroomPreview.ktx \ + $(BUILD_DIR)/ba_data/textures/downButton.ktx \ + $(BUILD_DIR)/ba_data/textures/egg1.ktx \ + $(BUILD_DIR)/ba_data/textures/egg2.ktx \ + $(BUILD_DIR)/ba_data/textures/egg3.ktx \ + $(BUILD_DIR)/ba_data/textures/egg4.ktx \ + $(BUILD_DIR)/ba_data/textures/eggTex1.ktx \ + $(BUILD_DIR)/ba_data/textures/eggTex2.ktx \ + $(BUILD_DIR)/ba_data/textures/eggTex3.ktx \ + $(BUILD_DIR)/ba_data/textures/empty.ktx \ + $(BUILD_DIR)/ba_data/textures/explosion.ktx \ + $(BUILD_DIR)/ba_data/textures/eyeColor.ktx \ + $(BUILD_DIR)/ba_data/textures/eyeColorTintMask.ktx \ + $(BUILD_DIR)/ba_data/textures/file.ktx \ + $(BUILD_DIR)/ba_data/textures/flagColor.ktx \ + $(BUILD_DIR)/ba_data/textures/flagPoleColor.ktx \ + $(BUILD_DIR)/ba_data/textures/folder.ktx \ + $(BUILD_DIR)/ba_data/textures/fontBig.ktx \ + $(BUILD_DIR)/ba_data/textures/fontExtras.ktx \ + $(BUILD_DIR)/ba_data/textures/fontExtras2.ktx \ + $(BUILD_DIR)/ba_data/textures/fontExtras3.ktx \ + $(BUILD_DIR)/ba_data/textures/fontExtras4.ktx \ + $(BUILD_DIR)/ba_data/textures/fontSmall0.ktx \ + $(BUILD_DIR)/ba_data/textures/fontSmall1.ktx \ + $(BUILD_DIR)/ba_data/textures/fontSmall2.ktx \ + $(BUILD_DIR)/ba_data/textures/fontSmall3.ktx \ + $(BUILD_DIR)/ba_data/textures/fontSmall4.ktx \ + $(BUILD_DIR)/ba_data/textures/fontSmall5.ktx \ + $(BUILD_DIR)/ba_data/textures/fontSmall6.ktx \ + $(BUILD_DIR)/ba_data/textures/fontSmall7.ktx \ + $(BUILD_DIR)/ba_data/textures/footballStadium.ktx \ + $(BUILD_DIR)/ba_data/textures/footballStadiumPreview.ktx \ + $(BUILD_DIR)/ba_data/textures/frameInset.ktx \ + $(BUILD_DIR)/ba_data/textures/frostyColor.ktx \ + $(BUILD_DIR)/ba_data/textures/frostyColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/frostyIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/frostyIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/fuse.ktx \ + $(BUILD_DIR)/ba_data/textures/gameCenterIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/gameCircleIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/githubLogo.ktx \ + $(BUILD_DIR)/ba_data/textures/gladiatorColor.ktx \ + $(BUILD_DIR)/ba_data/textures/gladiatorColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/gladiatorIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/gladiatorIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/glow.ktx \ + $(BUILD_DIR)/ba_data/textures/googlePlayAchievementsIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/googlePlayGamesIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/googlePlayLeaderboardsIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/googlePlusIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/googlePlusSignInButton.ktx \ + $(BUILD_DIR)/ba_data/textures/graphicsIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/heart.ktx \ + $(BUILD_DIR)/ba_data/textures/hockeyStadium.ktx \ + $(BUILD_DIR)/ba_data/textures/hockeyStadiumPreview.ktx \ + $(BUILD_DIR)/ba_data/textures/iconOnslaught.ktx \ + $(BUILD_DIR)/ba_data/textures/iconRunaround.ktx \ + $(BUILD_DIR)/ba_data/textures/iircadeLogo.ktx \ + $(BUILD_DIR)/ba_data/textures/impactBombColor.ktx \ + $(BUILD_DIR)/ba_data/textures/impactBombColorLit.ktx \ + $(BUILD_DIR)/ba_data/textures/inventoryIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/jackColor.ktx \ + $(BUILD_DIR)/ba_data/textures/jackColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/jackIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/jackIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/jumpsuitColor.ktx \ + $(BUILD_DIR)/ba_data/textures/jumpsuitColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/jumpsuitIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/jumpsuitIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/kronk.ktx \ + $(BUILD_DIR)/ba_data/textures/kronkColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/kronkIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/kronkIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/lakeFrigid.ktx \ + $(BUILD_DIR)/ba_data/textures/lakeFrigidPreview.ktx \ + $(BUILD_DIR)/ba_data/textures/lakeFrigidReflections.ktx \ + $(BUILD_DIR)/ba_data/textures/landMine.ktx \ + $(BUILD_DIR)/ba_data/textures/landMineLit.ktx \ + $(BUILD_DIR)/ba_data/textures/leaderboardsIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/leftButton.ktx \ + $(BUILD_DIR)/ba_data/textures/levelIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/light.ktx \ + $(BUILD_DIR)/ba_data/textures/lightSharp.ktx \ + $(BUILD_DIR)/ba_data/textures/lightSoft.ktx \ + $(BUILD_DIR)/ba_data/textures/lock.ktx \ + $(BUILD_DIR)/ba_data/textures/logIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/logo.ktx \ + $(BUILD_DIR)/ba_data/textures/logoEaster.ktx \ + $(BUILD_DIR)/ba_data/textures/mapPreviewMask.ktx \ + $(BUILD_DIR)/ba_data/textures/medalBronze.ktx \ + $(BUILD_DIR)/ba_data/textures/medalComplete.ktx \ + $(BUILD_DIR)/ba_data/textures/medalGold.ktx \ + $(BUILD_DIR)/ba_data/textures/medalSilver.ktx \ + $(BUILD_DIR)/ba_data/textures/melColor.ktx \ + $(BUILD_DIR)/ba_data/textures/melColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/melIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/melIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/menuBG.ktx \ + $(BUILD_DIR)/ba_data/textures/menuButton.ktx \ + $(BUILD_DIR)/ba_data/textures/menuIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/merch.ktx \ + $(BUILD_DIR)/ba_data/textures/meter.ktx \ + $(BUILD_DIR)/ba_data/textures/monkeyFaceLevelColor.ktx \ + $(BUILD_DIR)/ba_data/textures/monkeyFacePreview.ktx \ + $(BUILD_DIR)/ba_data/textures/multiplayerExamples.ktx \ + $(BUILD_DIR)/ba_data/textures/natureBackgroundColor.ktx \ + $(BUILD_DIR)/ba_data/textures/neoSpazColor.ktx \ + $(BUILD_DIR)/ba_data/textures/neoSpazColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/neoSpazIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/neoSpazIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/nextLevelIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/ninjaColor.ktx \ + $(BUILD_DIR)/ba_data/textures/ninjaColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/ninjaIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/ninjaIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/nub.ktx \ + $(BUILD_DIR)/ba_data/textures/null.ktx \ + $(BUILD_DIR)/ba_data/textures/oldLadyColor.ktx \ + $(BUILD_DIR)/ba_data/textures/oldLadyColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/oldLadyIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/oldLadyIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/operaSingerColor.ktx \ + $(BUILD_DIR)/ba_data/textures/operaSingerColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/operaSingerIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/operaSingerIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/ouyaAButton.ktx \ + $(BUILD_DIR)/ba_data/textures/ouyaIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/ouyaOButton.ktx \ + $(BUILD_DIR)/ba_data/textures/ouyaUButton.ktx \ + $(BUILD_DIR)/ba_data/textures/ouyaYButton.ktx \ + $(BUILD_DIR)/ba_data/textures/penguinColor.ktx \ + $(BUILD_DIR)/ba_data/textures/penguinColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/penguinIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/penguinIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/pixieColor.ktx \ + $(BUILD_DIR)/ba_data/textures/pixieColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/pixieIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/pixieIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/playerLineup.ktx \ + $(BUILD_DIR)/ba_data/textures/powerupBomb.ktx \ + $(BUILD_DIR)/ba_data/textures/powerupCurse.ktx \ + $(BUILD_DIR)/ba_data/textures/powerupHealth.ktx \ + $(BUILD_DIR)/ba_data/textures/powerupIceBombs.ktx \ + $(BUILD_DIR)/ba_data/textures/powerupImpactBombs.ktx \ + $(BUILD_DIR)/ba_data/textures/powerupLandMines.ktx \ + $(BUILD_DIR)/ba_data/textures/powerupPunch.ktx \ + $(BUILD_DIR)/ba_data/textures/powerupShield.ktx \ + $(BUILD_DIR)/ba_data/textures/powerupSpeed.ktx \ + $(BUILD_DIR)/ba_data/textures/powerupStickyBombs.ktx \ + $(BUILD_DIR)/ba_data/textures/puckColor.ktx \ + $(BUILD_DIR)/ba_data/textures/rampageBGColor.ktx \ + $(BUILD_DIR)/ba_data/textures/rampageBGColor2.ktx \ + $(BUILD_DIR)/ba_data/textures/rampageLevelColor.ktx \ + $(BUILD_DIR)/ba_data/textures/rampagePreview.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_+x.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_+y.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_+z.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_-x.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_-y.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_-z.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_+x.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_+y.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_+z.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_-x.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_-y.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_-z.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_+x.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_+y.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_+z.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_-x.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_-y.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_-z.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_+x.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_+y.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_+z.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_-x.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_-y.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_-z.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_+x.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_+y.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_+z.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_-x.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_-y.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_-z.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_+x.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_+y.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_+z.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_-x.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_-y.ktx \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_-z.ktx \ + $(BUILD_DIR)/ba_data/textures/replayIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/rgbStripes.ktx \ + $(BUILD_DIR)/ba_data/textures/rightButton.ktx \ + $(BUILD_DIR)/ba_data/textures/robotColor.ktx \ + $(BUILD_DIR)/ba_data/textures/robotColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/robotIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/robotIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/roundaboutLevelColor.ktx \ + $(BUILD_DIR)/ba_data/textures/roundaboutPreview.ktx \ + $(BUILD_DIR)/ba_data/textures/santaColor.ktx \ + $(BUILD_DIR)/ba_data/textures/santaColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/santaIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/santaIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/scorch.ktx \ + $(BUILD_DIR)/ba_data/textures/scorchBig.ktx \ + $(BUILD_DIR)/ba_data/textures/scrollWidget.ktx \ + $(BUILD_DIR)/ba_data/textures/scrollWidgetGlow.ktx \ + $(BUILD_DIR)/ba_data/textures/settingsIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/shadow.ktx \ + $(BUILD_DIR)/ba_data/textures/shadowSharp.ktx \ + $(BUILD_DIR)/ba_data/textures/shadowSoft.ktx \ + $(BUILD_DIR)/ba_data/textures/shield.ktx \ + $(BUILD_DIR)/ba_data/textures/shrapnel1Color.ktx \ + $(BUILD_DIR)/ba_data/textures/slash.ktx \ + $(BUILD_DIR)/ba_data/textures/smoke.ktx \ + $(BUILD_DIR)/ba_data/textures/softRect.ktx \ + $(BUILD_DIR)/ba_data/textures/softRect2.ktx \ + $(BUILD_DIR)/ba_data/textures/softRectVertical.ktx \ + $(BUILD_DIR)/ba_data/textures/sparks.ktx \ + $(BUILD_DIR)/ba_data/textures/star.ktx \ + $(BUILD_DIR)/ba_data/textures/startButton.ktx \ + $(BUILD_DIR)/ba_data/textures/stepRightUpLevelColor.ktx \ + $(BUILD_DIR)/ba_data/textures/stepRightUpPreview.ktx \ + $(BUILD_DIR)/ba_data/textures/storeCharacter.ktx \ + $(BUILD_DIR)/ba_data/textures/storeCharacterEaster.ktx \ + $(BUILD_DIR)/ba_data/textures/storeCharacterXmas.ktx \ + $(BUILD_DIR)/ba_data/textures/storeIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/superheroColor.ktx \ + $(BUILD_DIR)/ba_data/textures/superheroColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/superheroIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/superheroIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/textClearButton.ktx \ + $(BUILD_DIR)/ba_data/textures/thePadLevelColor.ktx \ + $(BUILD_DIR)/ba_data/textures/thePadPreview.ktx \ + $(BUILD_DIR)/ba_data/textures/ticketRoll.ktx \ + $(BUILD_DIR)/ba_data/textures/ticketRollBig.ktx \ + $(BUILD_DIR)/ba_data/textures/ticketRolls.ktx \ + $(BUILD_DIR)/ba_data/textures/tickets.ktx \ + $(BUILD_DIR)/ba_data/textures/ticketsMore.ktx \ + $(BUILD_DIR)/ba_data/textures/tipTopBGColor.ktx \ + $(BUILD_DIR)/ba_data/textures/tipTopLevelColor.ktx \ + $(BUILD_DIR)/ba_data/textures/tipTopPreview.ktx \ + $(BUILD_DIR)/ba_data/textures/tnt.ktx \ + $(BUILD_DIR)/ba_data/textures/touchArrows.ktx \ + $(BUILD_DIR)/ba_data/textures/touchArrowsActions.ktx \ + $(BUILD_DIR)/ba_data/textures/towerDLevelColor.ktx \ + $(BUILD_DIR)/ba_data/textures/towerDPreview.ktx \ + $(BUILD_DIR)/ba_data/textures/treesColor.ktx \ + $(BUILD_DIR)/ba_data/textures/trophy.ktx \ + $(BUILD_DIR)/ba_data/textures/tv.ktx \ + $(BUILD_DIR)/ba_data/textures/uiAtlas.ktx \ + $(BUILD_DIR)/ba_data/textures/uiAtlas2.ktx \ + $(BUILD_DIR)/ba_data/textures/upButton.ktx \ + $(BUILD_DIR)/ba_data/textures/usersButton.ktx \ + $(BUILD_DIR)/ba_data/textures/vrFillMound.ktx \ + $(BUILD_DIR)/ba_data/textures/warriorColor.ktx \ + $(BUILD_DIR)/ba_data/textures/warriorColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/warriorIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/warriorIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/white.ktx \ + $(BUILD_DIR)/ba_data/textures/windowHSmallVMed.ktx \ + $(BUILD_DIR)/ba_data/textures/windowHSmallVSmall.ktx \ + $(BUILD_DIR)/ba_data/textures/wings.ktx \ + $(BUILD_DIR)/ba_data/textures/witchColor.ktx \ + $(BUILD_DIR)/ba_data/textures/witchColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/witchIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/witchIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/wizardColor.ktx \ + $(BUILD_DIR)/ba_data/textures/wizardColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/wizardIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/wizardIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/wrestlerColor.ktx \ + $(BUILD_DIR)/ba_data/textures/wrestlerColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/wrestlerIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/wrestlerIconColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/zigZagLevelColor.ktx \ + $(BUILD_DIR)/ba_data/textures/zigzagPreview.ktx \ + $(BUILD_DIR)/ba_data/textures/zoeColor.ktx \ + $(BUILD_DIR)/ba_data/textures/zoeColorMask.ktx \ + $(BUILD_DIR)/ba_data/textures/zoeIcon.ktx \ + $(BUILD_DIR)/ba_data/textures/zoeIconColorMask.ktx + +TEX2D_PREVIEW_PNG_TARGETS = \ + $(BUILD_DIR)/ba_data/textures/achievementBoxer_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementCrossHair_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementDualWielding_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementEmpty_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementFlawlessVictory_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementFootballShutout_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementFootballVictory_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementFreeLoader_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementGotTheMoves_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementInControl_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementMedalLarge_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementMedalMedium_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementMedalSmall_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementMine_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementOffYouGo_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementOnslaught_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementOutline_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementRunaround_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementSharingIsCaring_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementStayinAlive_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementSuperPunch_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementTNT_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementTeamPlayer_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementWall_preview.png \ + $(BUILD_DIR)/ba_data/textures/achievementsIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/actionButtons_preview.png \ + $(BUILD_DIR)/ba_data/textures/actionHeroColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/actionHeroColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/actionHeroIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/actionHeroIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/advancedIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/agentColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/agentColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/agentIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/agentIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/aliBSRemoteIOSQR_preview.png \ + $(BUILD_DIR)/ba_data/textures/aliColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/aliColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/aliControllerQR_preview.png \ + $(BUILD_DIR)/ba_data/textures/aliIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/aliIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/aliSplash_preview.png \ + $(BUILD_DIR)/ba_data/textures/alienColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/alienColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/alienIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/alienIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/alwaysLandBGColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/alwaysLandLevelColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/alwaysLandPreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/analogStick_preview.png \ + $(BUILD_DIR)/ba_data/textures/arrow_preview.png \ + $(BUILD_DIR)/ba_data/textures/assassinColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/assassinColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/assassinIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/assassinIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/audioIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/backIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/bar_preview.png \ + $(BUILD_DIR)/ba_data/textures/bearColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/bearColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/bearIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/bearIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/bg_preview.png \ + $(BUILD_DIR)/ba_data/textures/bigGPreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/bigG_preview.png \ + $(BUILD_DIR)/ba_data/textures/black_preview.png \ + $(BUILD_DIR)/ba_data/textures/bombButton_preview.png \ + $(BUILD_DIR)/ba_data/textures/bombColorIce_preview.png \ + $(BUILD_DIR)/ba_data/textures/bombColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/bombStickyColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/bonesColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/bonesColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/bonesIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/bonesIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/boxingGlovesColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/bridgitLevelColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/bridgitPreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/bunnyColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/bunnyColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/bunnyIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/bunnyIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/buttonBomb_preview.png \ + $(BUILD_DIR)/ba_data/textures/buttonJump_preview.png \ + $(BUILD_DIR)/ba_data/textures/buttonPickUp_preview.png \ + $(BUILD_DIR)/ba_data/textures/buttonPunch_preview.png \ + $(BUILD_DIR)/ba_data/textures/buttonSquare_preview.png \ + $(BUILD_DIR)/ba_data/textures/chTitleChar1_preview.png \ + $(BUILD_DIR)/ba_data/textures/chTitleChar2_preview.png \ + $(BUILD_DIR)/ba_data/textures/chTitleChar3_preview.png \ + $(BUILD_DIR)/ba_data/textures/chTitleChar4_preview.png \ + $(BUILD_DIR)/ba_data/textures/chTitleChar5_preview.png \ + $(BUILD_DIR)/ba_data/textures/characterIconMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/chestIconEmpty_preview.png \ + $(BUILD_DIR)/ba_data/textures/chestIconMulti_preview.png \ + $(BUILD_DIR)/ba_data/textures/chestIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/chestOpenIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/circleNoAlpha_preview.png \ + $(BUILD_DIR)/ba_data/textures/circleOutlineNoAlpha_preview.png \ + $(BUILD_DIR)/ba_data/textures/circleOutline_preview.png \ + $(BUILD_DIR)/ba_data/textures/circleShadow_preview.png \ + $(BUILD_DIR)/ba_data/textures/circleZigZag_preview.png \ + $(BUILD_DIR)/ba_data/textures/circle_preview.png \ + $(BUILD_DIR)/ba_data/textures/coin_preview.png \ + $(BUILD_DIR)/ba_data/textures/controllerIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/courtyardLevelColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/courtyardPreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/cowboyColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/cowboyColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/cowboyIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/cowboyIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/cragCastleLevelColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/cragCastlePreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/crossOutMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/crossOut_preview.png \ + $(BUILD_DIR)/ba_data/textures/cursor_preview.png \ + $(BUILD_DIR)/ba_data/textures/cuteSpaz_preview.png \ + $(BUILD_DIR)/ba_data/textures/cyborgColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/cyborgColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/cyborgIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/cyborgIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/discordLogo_preview.png \ + $(BUILD_DIR)/ba_data/textures/doomShroomBGColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/doomShroomLevelColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/doomShroomPreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/downButton_preview.png \ + $(BUILD_DIR)/ba_data/textures/egg1_preview.png \ + $(BUILD_DIR)/ba_data/textures/egg2_preview.png \ + $(BUILD_DIR)/ba_data/textures/egg3_preview.png \ + $(BUILD_DIR)/ba_data/textures/egg4_preview.png \ + $(BUILD_DIR)/ba_data/textures/eggTex1_preview.png \ + $(BUILD_DIR)/ba_data/textures/eggTex2_preview.png \ + $(BUILD_DIR)/ba_data/textures/eggTex3_preview.png \ + $(BUILD_DIR)/ba_data/textures/empty_preview.png \ + $(BUILD_DIR)/ba_data/textures/explosion_preview.png \ + $(BUILD_DIR)/ba_data/textures/eyeColorTintMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/eyeColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/file_preview.png \ + $(BUILD_DIR)/ba_data/textures/flagColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/flagPoleColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/folder_preview.png \ + $(BUILD_DIR)/ba_data/textures/fontBig_preview.png \ + $(BUILD_DIR)/ba_data/textures/fontExtras2_preview.png \ + $(BUILD_DIR)/ba_data/textures/fontExtras3_preview.png \ + $(BUILD_DIR)/ba_data/textures/fontExtras4_preview.png \ + $(BUILD_DIR)/ba_data/textures/fontExtras_preview.png \ + $(BUILD_DIR)/ba_data/textures/fontSmall0_preview.png \ + $(BUILD_DIR)/ba_data/textures/fontSmall1_preview.png \ + $(BUILD_DIR)/ba_data/textures/fontSmall2_preview.png \ + $(BUILD_DIR)/ba_data/textures/fontSmall3_preview.png \ + $(BUILD_DIR)/ba_data/textures/fontSmall4_preview.png \ + $(BUILD_DIR)/ba_data/textures/fontSmall5_preview.png \ + $(BUILD_DIR)/ba_data/textures/fontSmall6_preview.png \ + $(BUILD_DIR)/ba_data/textures/fontSmall7_preview.png \ + $(BUILD_DIR)/ba_data/textures/footballStadiumPreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/footballStadium_preview.png \ + $(BUILD_DIR)/ba_data/textures/frameInset_preview.png \ + $(BUILD_DIR)/ba_data/textures/frostyColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/frostyColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/frostyIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/frostyIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/fuse_preview.png \ + $(BUILD_DIR)/ba_data/textures/gameCenterIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/gameCircleIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/githubLogo_preview.png \ + $(BUILD_DIR)/ba_data/textures/gladiatorColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/gladiatorColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/gladiatorIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/gladiatorIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/glow_preview.png \ + $(BUILD_DIR)/ba_data/textures/googlePlayAchievementsIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/googlePlayGamesIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/googlePlayLeaderboardsIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/googlePlusIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/googlePlusSignInButton_preview.png \ + $(BUILD_DIR)/ba_data/textures/graphicsIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/heart_preview.png \ + $(BUILD_DIR)/ba_data/textures/hockeyStadiumPreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/hockeyStadium_preview.png \ + $(BUILD_DIR)/ba_data/textures/iconOnslaught_preview.png \ + $(BUILD_DIR)/ba_data/textures/iconRunaround_preview.png \ + $(BUILD_DIR)/ba_data/textures/iircadeLogo_preview.png \ + $(BUILD_DIR)/ba_data/textures/impactBombColorLit_preview.png \ + $(BUILD_DIR)/ba_data/textures/impactBombColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/inventoryIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/jackColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/jackColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/jackIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/jackIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/jumpsuitColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/jumpsuitColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/jumpsuitIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/jumpsuitIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/kronkColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/kronkIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/kronkIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/kronk_preview.png \ + $(BUILD_DIR)/ba_data/textures/lakeFrigidPreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/lakeFrigidReflections_preview.png \ + $(BUILD_DIR)/ba_data/textures/lakeFrigid_preview.png \ + $(BUILD_DIR)/ba_data/textures/landMineLit_preview.png \ + $(BUILD_DIR)/ba_data/textures/landMine_preview.png \ + $(BUILD_DIR)/ba_data/textures/leaderboardsIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/leftButton_preview.png \ + $(BUILD_DIR)/ba_data/textures/levelIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/lightSharp_preview.png \ + $(BUILD_DIR)/ba_data/textures/lightSoft_preview.png \ + $(BUILD_DIR)/ba_data/textures/light_preview.png \ + $(BUILD_DIR)/ba_data/textures/lock_preview.png \ + $(BUILD_DIR)/ba_data/textures/logIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/logoEaster_preview.png \ + $(BUILD_DIR)/ba_data/textures/logo_preview.png \ + $(BUILD_DIR)/ba_data/textures/mapPreviewMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/medalBronze_preview.png \ + $(BUILD_DIR)/ba_data/textures/medalComplete_preview.png \ + $(BUILD_DIR)/ba_data/textures/medalGold_preview.png \ + $(BUILD_DIR)/ba_data/textures/medalSilver_preview.png \ + $(BUILD_DIR)/ba_data/textures/melColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/melColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/melIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/melIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/menuBG_preview.png \ + $(BUILD_DIR)/ba_data/textures/menuButton_preview.png \ + $(BUILD_DIR)/ba_data/textures/menuIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/merch_preview.png \ + $(BUILD_DIR)/ba_data/textures/meter_preview.png \ + $(BUILD_DIR)/ba_data/textures/monkeyFaceLevelColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/monkeyFacePreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/multiplayerExamples_preview.png \ + $(BUILD_DIR)/ba_data/textures/natureBackgroundColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/neoSpazColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/neoSpazColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/neoSpazIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/neoSpazIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/nextLevelIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/ninjaColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/ninjaColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/ninjaIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/ninjaIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/nub_preview.png \ + $(BUILD_DIR)/ba_data/textures/null_preview.png \ + $(BUILD_DIR)/ba_data/textures/oldLadyColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/oldLadyColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/oldLadyIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/oldLadyIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/operaSingerColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/operaSingerColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/operaSingerIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/operaSingerIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/ouyaAButton_preview.png \ + $(BUILD_DIR)/ba_data/textures/ouyaIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/ouyaOButton_preview.png \ + $(BUILD_DIR)/ba_data/textures/ouyaUButton_preview.png \ + $(BUILD_DIR)/ba_data/textures/ouyaYButton_preview.png \ + $(BUILD_DIR)/ba_data/textures/penguinColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/penguinColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/penguinIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/penguinIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/pixieColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/pixieColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/pixieIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/pixieIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/playerLineup_preview.png \ + $(BUILD_DIR)/ba_data/textures/powerupBomb_preview.png \ + $(BUILD_DIR)/ba_data/textures/powerupCurse_preview.png \ + $(BUILD_DIR)/ba_data/textures/powerupHealth_preview.png \ + $(BUILD_DIR)/ba_data/textures/powerupIceBombs_preview.png \ + $(BUILD_DIR)/ba_data/textures/powerupImpactBombs_preview.png \ + $(BUILD_DIR)/ba_data/textures/powerupLandMines_preview.png \ + $(BUILD_DIR)/ba_data/textures/powerupPunch_preview.png \ + $(BUILD_DIR)/ba_data/textures/powerupShield_preview.png \ + $(BUILD_DIR)/ba_data/textures/powerupSpeed_preview.png \ + $(BUILD_DIR)/ba_data/textures/powerupStickyBombs_preview.png \ + $(BUILD_DIR)/ba_data/textures/puckColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/rampageBGColor2_preview.png \ + $(BUILD_DIR)/ba_data/textures/rampageBGColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/rampageLevelColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/rampagePreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_+x_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_+y_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_+z_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_-x_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_-y_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionChar_-z_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_+x_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_+y_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_+z_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_-x_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_-y_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionPowerup_-z_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_+x_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_+y_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_+z_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_-x_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_-y_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharp_-z_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_+x_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_+y_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_+z_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_-x_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_-y_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharper_-z_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_+x_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_+y_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_+z_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_-x_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_-y_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSharpest_-z_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_+x_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_+y_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_+z_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_-x_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_-y_preview.png \ + $(BUILD_DIR)/ba_data/textures/reflectionSoft_-z_preview.png \ + $(BUILD_DIR)/ba_data/textures/replayIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/rgbStripes_preview.png \ + $(BUILD_DIR)/ba_data/textures/rightButton_preview.png \ + $(BUILD_DIR)/ba_data/textures/robotColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/robotColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/robotIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/robotIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/roundaboutLevelColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/roundaboutPreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/santaColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/santaColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/santaIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/santaIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/scorchBig_preview.png \ + $(BUILD_DIR)/ba_data/textures/scorch_preview.png \ + $(BUILD_DIR)/ba_data/textures/scrollWidgetGlow_preview.png \ + $(BUILD_DIR)/ba_data/textures/scrollWidget_preview.png \ + $(BUILD_DIR)/ba_data/textures/settingsIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/shadowSharp_preview.png \ + $(BUILD_DIR)/ba_data/textures/shadowSoft_preview.png \ + $(BUILD_DIR)/ba_data/textures/shadow_preview.png \ + $(BUILD_DIR)/ba_data/textures/shield_preview.png \ + $(BUILD_DIR)/ba_data/textures/shrapnel1Color_preview.png \ + $(BUILD_DIR)/ba_data/textures/slash_preview.png \ + $(BUILD_DIR)/ba_data/textures/smoke_preview.png \ + $(BUILD_DIR)/ba_data/textures/softRect2_preview.png \ + $(BUILD_DIR)/ba_data/textures/softRectVertical_preview.png \ + $(BUILD_DIR)/ba_data/textures/softRect_preview.png \ + $(BUILD_DIR)/ba_data/textures/sparks_preview.png \ + $(BUILD_DIR)/ba_data/textures/star_preview.png \ + $(BUILD_DIR)/ba_data/textures/startButton_preview.png \ + $(BUILD_DIR)/ba_data/textures/stepRightUpLevelColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/stepRightUpPreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/storeCharacterEaster_preview.png \ + $(BUILD_DIR)/ba_data/textures/storeCharacterXmas_preview.png \ + $(BUILD_DIR)/ba_data/textures/storeCharacter_preview.png \ + $(BUILD_DIR)/ba_data/textures/storeIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/superheroColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/superheroColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/superheroIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/superheroIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/textClearButton_preview.png \ + $(BUILD_DIR)/ba_data/textures/thePadLevelColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/thePadPreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/ticketRollBig_preview.png \ + $(BUILD_DIR)/ba_data/textures/ticketRoll_preview.png \ + $(BUILD_DIR)/ba_data/textures/ticketRolls_preview.png \ + $(BUILD_DIR)/ba_data/textures/ticketsMore_preview.png \ + $(BUILD_DIR)/ba_data/textures/tickets_preview.png \ + $(BUILD_DIR)/ba_data/textures/tipTopBGColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/tipTopLevelColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/tipTopPreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/tnt_preview.png \ + $(BUILD_DIR)/ba_data/textures/touchArrowsActions_preview.png \ + $(BUILD_DIR)/ba_data/textures/touchArrows_preview.png \ + $(BUILD_DIR)/ba_data/textures/towerDLevelColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/towerDPreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/treesColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/trophy_preview.png \ + $(BUILD_DIR)/ba_data/textures/tv_preview.png \ + $(BUILD_DIR)/ba_data/textures/uiAtlas2_preview.png \ + $(BUILD_DIR)/ba_data/textures/uiAtlas_preview.png \ + $(BUILD_DIR)/ba_data/textures/upButton_preview.png \ + $(BUILD_DIR)/ba_data/textures/usersButton_preview.png \ + $(BUILD_DIR)/ba_data/textures/vrFillMound_preview.png \ + $(BUILD_DIR)/ba_data/textures/warriorColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/warriorColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/warriorIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/warriorIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/white_preview.png \ + $(BUILD_DIR)/ba_data/textures/windowHSmallVMed_preview.png \ + $(BUILD_DIR)/ba_data/textures/windowHSmallVSmall_preview.png \ + $(BUILD_DIR)/ba_data/textures/wings_preview.png \ + $(BUILD_DIR)/ba_data/textures/witchColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/witchColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/witchIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/witchIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/wizardColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/wizardColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/wizardIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/wizardIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/wrestlerColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/wrestlerColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/wrestlerIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/wrestlerIcon_preview.png \ + $(BUILD_DIR)/ba_data/textures/zigZagLevelColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/zigzagPreview_preview.png \ + $(BUILD_DIR)/ba_data/textures/zoeColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/zoeColor_preview.png \ + $(BUILD_DIR)/ba_data/textures/zoeIconColorMask_preview.png \ + $(BUILD_DIR)/ba_data/textures/zoeIcon_preview.png + +EXTRAS_TARGETS_WIN_WIN32 = \ + $(BUILD_DIR)/windows/Win32/DLLs/_asyncio.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_asyncio_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_bz2.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_bz2_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_ctypes.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_ctypes_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_ctypes_test.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_ctypes_test_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_decimal.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_decimal_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_elementtree.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_elementtree_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_hashlib.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_hashlib_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_lzma.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_lzma_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_msi.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_msi_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_multiprocessing.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_multiprocessing_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_overlapped.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_overlapped_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_queue.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_queue_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_socket.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_socket_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_sqlite3.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_sqlite3_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_ssl.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_ssl_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_testbuffer.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_testbuffer_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_testcapi.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_testcapi_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_testconsole.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_testconsole_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_testimportmultiple.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_testimportmultiple_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_testinternalcapi.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_testinternalcapi_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_testmultiphase.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_testmultiphase_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_tkinter.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_tkinter_d.lib \ + $(BUILD_DIR)/windows/Win32/DLLs/_tkinter_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_uuid.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_uuid_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_zoneinfo.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/_zoneinfo_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/libcrypto-1_1.dll \ + $(BUILD_DIR)/windows/Win32/DLLs/libffi-8.dll \ + $(BUILD_DIR)/windows/Win32/DLLs/libssl-1_1.dll \ + $(BUILD_DIR)/windows/Win32/DLLs/pyexpat.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/pyexpat_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/python_lib.cat \ + $(BUILD_DIR)/windows/Win32/DLLs/python_tools.cat \ + $(BUILD_DIR)/windows/Win32/DLLs/select.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/select_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/sqlite3.dll \ + $(BUILD_DIR)/windows/Win32/DLLs/sqlite3_d.dll \ + $(BUILD_DIR)/windows/Win32/DLLs/tcl86t.dll \ + $(BUILD_DIR)/windows/Win32/DLLs/tk86t.dll \ + $(BUILD_DIR)/windows/Win32/DLLs/unicodedata.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/unicodedata_d.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/winsound.pyd \ + $(BUILD_DIR)/windows/Win32/DLLs/winsound_d.pyd \ + $(BUILD_DIR)/windows/Win32/Lib/email/architecture.rst \ + $(BUILD_DIR)/windows/Win32/OpenAL32.dll \ + $(BUILD_DIR)/windows/Win32/SDL2.dll \ + $(BUILD_DIR)/windows/Win32/libvorbis.dll \ + $(BUILD_DIR)/windows/Win32/libvorbisfile.dll \ + $(BUILD_DIR)/windows/Win32/msvcp140d.dll \ + $(BUILD_DIR)/windows/Win32/ogg.dll \ + $(BUILD_DIR)/windows/Win32/python.exe \ + $(BUILD_DIR)/windows/Win32/python311.dll \ + $(BUILD_DIR)/windows/Win32/python311_d.dll \ + $(BUILD_DIR)/windows/Win32/python_d.exe \ + $(BUILD_DIR)/windows/Win32/pythonw.exe \ + $(BUILD_DIR)/windows/Win32/pythonw_d.exe \ + $(BUILD_DIR)/windows/Win32/ucrtbased.dll \ + $(BUILD_DIR)/windows/Win32/vc_redist.x86.exe \ + $(BUILD_DIR)/windows/Win32/vcruntime140d.dll + +# Rule to copy src extras to build. +$(EXTRAS_TARGETS_WIN_WIN32) : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + + +EXTRAS_TARGETS_WIN_X64 = \ + $(BUILD_DIR)/windows/x64/DLLs/_asyncio.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_asyncio_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_bz2.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_bz2_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_ctypes.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_ctypes_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_ctypes_test.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_ctypes_test_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_decimal.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_decimal_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_elementtree.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_elementtree_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_hashlib.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_hashlib_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_lzma.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_lzma_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_msi.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_msi_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_multiprocessing.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_multiprocessing_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_overlapped.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_overlapped_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_queue.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_queue_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_socket.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_socket_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_sqlite3.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_sqlite3_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_ssl.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_ssl_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_testbuffer.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_testbuffer_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_testcapi.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_testcapi_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_testconsole.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_testconsole_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_testimportmultiple.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_testimportmultiple_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_testinternalcapi.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_testinternalcapi_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_testmultiphase.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_testmultiphase_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_tkinter.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_tkinter_d.lib \ + $(BUILD_DIR)/windows/x64/DLLs/_tkinter_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_uuid.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_uuid_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_zoneinfo.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/_zoneinfo_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/libcrypto-1_1.dll \ + $(BUILD_DIR)/windows/x64/DLLs/libffi-8.dll \ + $(BUILD_DIR)/windows/x64/DLLs/libssl-1_1.dll \ + $(BUILD_DIR)/windows/x64/DLLs/pyexpat.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/pyexpat_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/python_lib.cat \ + $(BUILD_DIR)/windows/x64/DLLs/python_tools.cat \ + $(BUILD_DIR)/windows/x64/DLLs/select.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/select_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/sqlite3.dll \ + $(BUILD_DIR)/windows/x64/DLLs/sqlite3_d.dll \ + $(BUILD_DIR)/windows/x64/DLLs/tcl86t.dll \ + $(BUILD_DIR)/windows/x64/DLLs/tk86t.dll \ + $(BUILD_DIR)/windows/x64/DLLs/unicodedata.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/unicodedata_d.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/winsound.pyd \ + $(BUILD_DIR)/windows/x64/DLLs/winsound_d.pyd \ + $(BUILD_DIR)/windows/x64/Lib/email/architecture.rst \ + $(BUILD_DIR)/windows/x64/OpenAL32.dll \ + $(BUILD_DIR)/windows/x64/SDL2.dll \ + $(BUILD_DIR)/windows/x64/libvorbis.dll \ + $(BUILD_DIR)/windows/x64/libvorbisfile.dll \ + $(BUILD_DIR)/windows/x64/msvcp140d.dll \ + $(BUILD_DIR)/windows/x64/ogg.dll \ + $(BUILD_DIR)/windows/x64/python.exe \ + $(BUILD_DIR)/windows/x64/python311.dll \ + $(BUILD_DIR)/windows/x64/python311_d.dll \ + $(BUILD_DIR)/windows/x64/python_d.exe \ + $(BUILD_DIR)/windows/x64/pythonw.exe \ + $(BUILD_DIR)/windows/x64/pythonw_d.exe \ + $(BUILD_DIR)/windows/x64/ucrtbased.dll \ + $(BUILD_DIR)/windows/x64/vc_redist.x64.exe \ + $(BUILD_DIR)/windows/x64/vcruntime140_1d.dll \ + $(BUILD_DIR)/windows/x64/vcruntime140d.dll + +# Rule to copy src extras to build. +$(EXTRAS_TARGETS_WIN_X64) : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +# __AUTOGENERATED_PRIVATE_END__ + +ASSET_TARGETS_COMMON += $(MESH_TARGETS) + +$(BUILD_DIR)/%.bob : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +$(BUILD_DIR)/%.cob : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +$(BUILD_DIR)/%.ogg : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +$(BUILD_DIR)/%.fdata : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +$(BUILD_DIR)/%.pem : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +# Langdata one-off json file. +$(BUILD_DIR)/ba_data/data/langdata.json : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +# Languages json files. +$(BUILD_DIR)/ba_data/data/languages/%.json : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +# Map json files. +$(BUILD_DIR)/ba_data/data/maps/%.json : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +ba_data/%.tex2d.png : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +ba_data/%_+x.tex2d.png : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +$(BUILD_DIR)/ba_data/%_preview.png : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +$(BUILD_DIR)/%.dds : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +$(BUILD_DIR)/%.pvr : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +$(BUILD_DIR)/%.ktx : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/assets/$@ + +audio: $(AUDIO_TARGETS) +audio-clean: + rm -rf $(BUILD_DIR)/ba_data/audio + +data: $(DATA_TARGETS) + +data-clean: + rm -rf $(BUILD_DIR)/ba_data/data + +fonts: $(FONT_TARGETS) +fonts-clean: + rm -rf $(BUILD_DIR)/ba_data/fonts + +# Mesh targets needed for all platforms. +MESH_TARGETS = $(BOB_TARGETS) $(COB_TARGETS) + +meshes: $(MESH_TARGETS) +meshes-clean: + rm -rf $(BUILD_DIR)/ba_data/meshes + +# Texture targets needed per-platform (may overlap) +TEXTURE_TARGETS_CMAKE = $(TEX2D_DDS_TARGETS) +TEXTURE_TARGETS_WIN = $(TEX2D_DDS_TARGETS) +TEXTURE_TARGETS_MAC = $(TEX2D_DDS_TARGETS) +TEXTURE_TARGETS_IOS = $(TEX2D_PVR_TARGETS) +TEXTURE_TARGETS_ANDROID = $(TEX2D_KTX_TARGETS) + +# Texture targets needed for all platforms. +TEXTURE_TARGETS_COMMON = $(TEX2D_PREVIEW_PNG_TARGETS) \ + $(TEX2D_PREVIEW_PNG_TARGETS_2) + +textures-clean: + rm -rf $(BUILD_DIR)/ba_data/textures + +# Script targets needed per-platform (may overlap) +SCRIPT_TARGETS_CMAKE = +SCRIPT_TARGETS_WIN_WIN32 = $(SCRIPT_TARGETS_PY_PRIVATE_WIN_WIN32) \ + $(SCRIPT_TARGETS_PYC_PRIVATE_WIN_WIN32) +SCRIPT_TARGETS_WIN_X64 = $(SCRIPT_TARGETS_PY_PRIVATE_WIN_X64) \ + $(SCRIPT_TARGETS_PYC_PRIVATE_WIN_X64) +SCRIPT_TARGETS_MAC = $(SCRIPT_TARGETS_PY_PRIVATE_APPLE) \ + $(SCRIPT_TARGETS_PYC_PRIVATE_APPLE) +SCRIPT_TARGETS_IOS = $(SCRIPT_TARGETS_PY_PRIVATE_APPLE) \ + $(SCRIPT_TARGETS_PYC_PRIVATE_APPLE) +SCRIPT_TARGETS_ANDROID = $(SCRIPT_TARGETS_PY_PRIVATE_ANDROID) \ + $(SCRIPT_TARGETS_PYC_PRIVATE_ANDROID) + +# Script targets needed by all platforms. +SCRIPT_TARGETS_COMMON = $(SCRIPT_TARGETS_PY_PUBLIC) \ + $(SCRIPT_TARGETS_PYC_PUBLIC) $(SCRIPT_TARGETS_PY_PUBLIC_TOOLS) \ + $(SCRIPT_TARGETS_PYC_PUBLIC_TOOLS) $(SCRIPT_TARGETS_PY_PRIVATE_COMMON) \ + $(SCRIPT_TARGETS_PYC_PRIVATE_COMMON) $(PEM_TARGETS) + +# Build scripts for a specific platform. +scripts-cmake: $(SCRIPT_TARGETS_CMAKE) $(SCRIPT_TARGETS_COMMON) +scripts-win-Win32: $(SCRIPT_TARGETS_WIN_WIN32) $(SCRIPT_TARGETS_COMMON) +scripts-win-x64: $(SCRIPT_TARGETS_WIN_X64) $(SCRIPT_TARGETS_COMMON) +scripts-mac: $(SCRIPT_TARGETS_MAC) $(SCRIPT_TARGETS_COMMON) +scripts-ios: $(SCRIPT_TARGETS_IOS) $(SCRIPT_TARGETS_COMMON) +scripts-android: $(SCRIPT_TARGETS_ANDROID) $(SCRIPT_TARGETS_COMMON) + +# Build scripts for all platforms +scripts: scripts-cmake scripts-win-Win32 scripts-win-x64 scripts-mac \ + scripts-ios scripts-android +scripts-clean: + rm -rf $(BUILD_DIR)/ba_data/python $(BUILD_DIR)/ba_data/python-site-packages \ + $(BUILD_DIR)/pylib-android $(BUILD_DIR)/pylib-apple \ + $(BUILD_DIR)/windows/Win32/Lib $(BUILD_DIR)/windows/x64/Lib + +# Build all required assets for a specific platform. +assets-cmake: $(ASSET_TARGETS_CMAKE) $(ASSET_TARGETS_COMMON) +assets-win-Win32: $(ASSET_TARGETS_WIN_WIN32) $(ASSET_TARGETS_COMMON) +assets-win-x64: $(ASSET_TARGETS_WIN_X64) $(ASSET_TARGETS_COMMON) +assets-mac: $(ASSET_TARGETS_MAC) $(ASSET_TARGETS_COMMON) +assets-ios: $(ASSET_TARGETS_IOS) $(ASSET_TARGETS_COMMON) +assets-android: $(ASSET_TARGETS_ANDROID) $(ASSET_TARGETS_COMMON) + +# Build all assets for all platforms. +assets: assets-cmake assets-win-Win32 assets-win-x64 assets-mac assets-ios \ + assets-android +clean: + @rm -rf $(BUILD_DIR) + +# These targets don't correspond to actual files; make sure make knows that. +.PHONY: cmake win mac ios android audio audio-clean fonts fonts-clean \ + data data-clean meshes meshes-clean \ + textures-clean scripts scripts-clean \ + assets assets-cmake assets-win-Win32 assets-win-x64 assets-mac \ + assets-ios assets-android asset_sources clean diff --git a/src/assets/README.md b/src/assets/README.md new file mode 100644 index 00000000..e6216505 --- /dev/null +++ b/src/assets/README.md @@ -0,0 +1,4 @@ +# Ballistica Assets + +This directory contains sources used to build bundled scripts, models, +textures, etc. diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py new file mode 100644 index 00000000..fe5229a0 --- /dev/null +++ b/src/assets/ba_data/python/babase/__init__.py @@ -0,0 +1,184 @@ +# Released under the MIT License. See LICENSE for details. +# +"""The public face of Ballistica. + +This top level module is a collection of most commonly used functionality. +For many modding purposes, the bits exposed here are all you'll need. +In some specific cases you may need to pull in individual submodules instead. +""" +# pylint: disable=redefined-builtin + +import _babase +from _babase import ( + SimpleSound, + ContextRef, + ContextCall, + apptime, + apptimer, + AppTimer, + displaytime, + displaytimer, + DisplayTimer, + Vec3, + do_once, + pushcall, + quit, + safecolor, + set_analytics_screen, + charstr, + clipboard_is_supported, + clipboard_has_text, + clipboard_get_text, + clipboard_set_text, + in_logic_thread, +) + +from babase._accountv2 import AccountV2Handle +from babase._plugin import PotentialPlugin, Plugin, PluginSubsystem +from babase._app import App +from babase._cloud import CloudSubsystem +from babase._mgen.enums import ( + Permission, + SpecialChar, + InputType, + UIScale, +) +from babase._error import ( + print_exception, + print_error, + ContextError, + NotFoundError, + PlayerNotFoundError, + SessionPlayerNotFoundError, + NodeNotFoundError, + ActorNotFoundError, + InputDeviceNotFoundError, + WidgetNotFoundError, + ActivityNotFoundError, + TeamNotFoundError, + MapNotFoundError, + SessionTeamNotFoundError, + SessionNotFoundError, + DelegateNotFoundError, +) + +from babase._language import Lstr, LanguageSubsystem +from babase._appconfig import AppConfig +from babase._apputils import is_browser_likely_available, garbage_collect +from babase._general import ( + DisplayTime, + AppTime, + WeakCall, + Call, + existing, + Existable, + verify_object_death, + storagename, + getclass, +) +from babase._keyboard import Keyboard +from babase._math import normalized_color, is_point_in_box, vec3validate +from babase._meta import MetadataSubsystem +from babase._text import timestring + +_babase.app = app = App() +app.postinit() + +__all__ = [ + 'app', + 'AccountV2Handle', + 'ActivityNotFoundError', + 'ActorNotFoundError', + 'app', + 'App', + 'AppConfig', + 'Call', + 'charstr', + 'clipboard_get_text', + 'clipboard_has_text', + 'clipboard_is_supported', + 'clipboard_set_text', + 'ContextCall', + 'ContextError', + 'CloudSubsystem', + 'DelegateNotFoundError', + 'do_once', + 'Existable', + 'existing', + 'garbage_collect', + 'getclass', + 'in_logic_thread', + 'InputDeviceNotFoundError', + 'InputType', + 'is_browser_likely_available', + 'is_point_in_box', + 'Keyboard', + 'LanguageSubsystem', + 'Lstr', + 'MapNotFoundError', + 'MetadataSubsystem', + 'NodeNotFoundError', + 'normalized_color', + 'NotFoundError', + 'Permission', + 'PlayerNotFoundError', + 'Plugin', + 'PluginSubsystem', + 'PotentialPlugin', + 'print_error', + 'print_exception', + 'pushcall', + 'quit', + 'safecolor', + 'SessionNotFoundError', + 'SessionPlayerNotFoundError', + 'SessionTeamNotFoundError', + 'set_analytics_screen', + 'SpecialChar', + 'storagename', + 'TeamNotFoundError', + 'apptime', + 'timestring', + 'UIScale', + 'Vec3', + 'vec3validate', + 'verify_object_death', + 'WeakCall', + 'WidgetNotFoundError', + 'AppTime', + 'apptime', + 'apptimer', + 'AppTimer', + 'SimpleSound', + 'ContextRef', + 'DisplayTime', + 'displaytimer', + 'displaytime', + 'DisplayTimer', +] + + +# Have these things present themselves cleanly as 'ba.Foo' +# instead of 'ba._submodule.Foo' +def _simplify_module_names() -> None: + import os + + # Though pdoc gets confused when we override __module__, + # so let's make an exception for it. + if os.environ.get('BA_DOCS_GENERATION', '0') != '1': + from efro.util import set_canonical_module + + globs = globals() + set_canonical_module( + module_globals=globs, + names=[n for n in globs.keys() if not n.startswith('_')], + ) + + +_simplify_module_names() +del _simplify_module_names + +# Marker we pop down at the very end so other modules can run sanity +# checks to make sure we aren't importing them reciprocally when they +# import us. +_REACHED_END_OF_MODULE = True diff --git a/assets/src/ba_data/python/ba/_accountv2.py b/src/assets/ba_data/python/babase/_accountv2.py similarity index 85% rename from assets/src/ba_data/python/ba/_accountv2.py rename to src/assets/ba_data/python/babase/_accountv2.py index 5e676230..744831eb 100644 --- a/assets/src/ba_data/python/ba/_accountv2.py +++ b/src/assets/ba_data/python/babase/_accountv2.py @@ -11,12 +11,12 @@ from typing import TYPE_CHECKING from efro.call import tpartial from efro.error import CommunicationError from bacommon.login import LoginType -import _ba +import _babase if TYPE_CHECKING: from typing import Any - from ba._login import LoginAdapter + from babase._login import LoginAdapter DEBUG_LOG = False @@ -27,11 +27,10 @@ class AccountV2Subsystem: Category: **App Classes** - Access the single shared instance of this class at 'ba.app.accounts_v2'. + Access the single shared instance of this class at 'ba.app.accounts'. """ def __init__(self) -> None: - # Whether or not everything related to an initial login # (or lack thereof) has completed. This includes things like # workspace syncing. Completion of this is what flips the app @@ -46,16 +45,22 @@ class AccountV2Subsystem: self._implicit_state_changed = False self._can_do_auto_sign_in = True - if _ba.app.platform == 'android' and _ba.app.subplatform == 'google': - from ba._login import LoginAdapterGPGS + if _babase.app.classic is None: + raise RuntimeError('Needs updating for no-classic case.') + + if ( + _babase.app.classic.platform == 'android' + and _babase.app.classic.subplatform == 'google' + ): + from babase._login import LoginAdapterGPGS self.login_adapters[LoginType.GPGS] = LoginAdapterGPGS() - def on_app_launch(self) -> None: - """Should be called at standard on_app_launch time.""" + def on_app_launching(self) -> None: + """Should be called at standard on_app_launching time.""" for adapter in self.login_adapters.values(): - adapter.on_app_launch() + adapter.on_app_launching() def set_primary_credentials(self, credentials: str | None) -> None: """Set credentials for the primary app account.""" @@ -87,7 +92,7 @@ class AccountV2Subsystem: Will be called with None on log-outs and when new credentials are set but have not yet been verified. """ - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() # Currently don't do anything special on sign-outs. if account is None: @@ -102,7 +107,7 @@ class AccountV2Subsystem: and not self._kicked_off_workspace_load ): self._kicked_off_workspace_load = True - _ba.app.workspaces.set_active_workspace( + _babase.app.workspaces.set_active_workspace( account=account, workspaceid=account.workspaceid, workspacename=account.workspacename, @@ -112,18 +117,18 @@ class AccountV2Subsystem: # Don't activate workspaces if we've already told the game # that initial-log-in is done or if we've already kicked # off a workspace load. - _ba.screenmessage( + _babase.screenmessage( f'\'{account.workspacename}\'' f' will be activated at next app launch.', color=(1, 1, 0), ) - _ba.playsound(_ba.getsound('error')) + _babase.getsimplesound('error').play() return # Ok; no workspace to worry about; carry on. if not self._initial_sign_in_completed: self._initial_sign_in_completed = True - _ba.app.on_initial_sign_in_completed() + _babase.app.on_initial_sign_in_completed() def on_active_logins_changed(self, logins: dict[LoginType, str]) -> None: """Should be called when logins for the active account change.""" @@ -135,9 +140,9 @@ class AccountV2Subsystem: self, login_type: LoginType, login_id: str, display_name: str ) -> None: """An implicit sign-in happened (called by native layer).""" - from ba._login import LoginAdapter + from babase._login import LoginAdapter - with _ba.Context('ui'): + with _babase.ContextRef.empty(): self.login_adapters[login_type].set_implicit_login_state( LoginAdapter.ImplicitLoginState( login_id=login_id, display_name=display_name @@ -146,7 +151,7 @@ class AccountV2Subsystem: def on_implicit_sign_out(self, login_type: LoginType) -> None: """An implicit sign-out happened (called by native layer).""" - with _ba.Context('ui'): + with _babase.ContextRef.empty(): self.login_adapters[login_type].set_implicit_login_state(None) def on_no_initial_primary_account(self) -> None: @@ -158,7 +163,7 @@ class AccountV2Subsystem: """ if not self._initial_sign_in_completed: self._initial_sign_in_completed = True - _ba.app.on_initial_sign_in_completed() + _babase.app.on_initial_sign_in_completed() @staticmethod def _hashstr(val: str) -> str: @@ -179,13 +184,13 @@ class AccountV2Subsystem: types even if the default implicit one can't be explicitly logged out or otherwise controlled. """ - from ba._language import Lstr + from babase._language import Lstr - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() - cfg = _ba.app.config + cfg = _babase.app.config cfgkey = 'ImplicitLoginStates' - cfgdict = _ba.app.config.setdefault(cfgkey, {}) + cfgdict = _babase.app.config.setdefault(cfgkey, {}) # Store which (if any) adapter is currently implicitly signed in. # Making the assumption there will only ever be one implicit @@ -213,10 +218,10 @@ class AccountV2Subsystem: else: service_str = None if service_str is not None: - _ba.timer( + _babase.apptimer( 2.0, tpartial( - _ba.screenmessage, + _babase.screenmessage, Lstr( resource='notUsingAccountText', subs=[ @@ -249,13 +254,14 @@ class AccountV2Subsystem: def on_cloud_connectivity_changed(self, connected: bool) -> None: """Should be called with cloud connectivity changes.""" del connected # Unused. - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() # We may want to auto-sign-in based on this new state. self._update_auto_sign_in() def _update_auto_sign_in(self) -> None: - from ba._internal import get_v1_account_state + plus = _babase.app.plus + assert plus is not None # If implicit state has changed, try to respond. if self._implicit_state_changed: @@ -267,7 +273,7 @@ class AccountV2Subsystem: 'AccountV2: Signing out as result' ' of implicit state change...', ) - _ba.app.accounts_v2.set_primary_credentials(None) + _babase.app.accounts.set_primary_credentials(None) self._implicit_state_changed = False # Once we've made a move here we don't want to @@ -283,7 +289,7 @@ class AccountV2Subsystem: # switching accounts via the back-end). # NOTE: should test case where we don't have # connectivity here. - if _ba.app.cloud.is_connected(): + if _babase.app.cloud.is_connected(): if DEBUG_LOG: logging.debug( 'AccountV2: Signing in as result' @@ -310,9 +316,9 @@ class AccountV2Subsystem: # in as a rule, even if there are corner cases where this might # not be what they want (A user signing out and then restarting # may be auto-signed back in). - connected = _ba.app.cloud.is_connected() - signed_in_v1 = get_v1_account_state() == 'signed_in' - signed_in_v2 = _ba.app.accounts_v2.have_primary_credentials() + connected = _babase.app.cloud.is_connected() + signed_in_v1 = plus.get_v1_account_state() == 'signed_in' + signed_in_v2 = _babase.app.accounts.have_primary_credentials() if ( connected and not signed_in_v1 @@ -334,7 +340,7 @@ class AccountV2Subsystem: result: LoginAdapter.SignInResult | Exception, ) -> None: """A sign-in has completed that the user asked for explicitly.""" - from ba._language import Lstr + from babase._language import Lstr del adapter # Unused. @@ -350,20 +356,19 @@ class AccountV2Subsystem: ) # For now just show 'error'. Should do better than this. - with _ba.Context('ui'): - _ba.screenmessage( - Lstr(resource='internal.signInErrorText'), - color=(1, 0, 0), - ) - _ba.playsound(_ba.getsound('error')) + _babase.screenmessage( + Lstr(resource='internal.signInErrorText'), + color=(1, 0, 0), + ) + _babase.getsimplesound('error').play() # Also I suppose we should sign them out in this case since # it could be misleading to be still signed in with the old # account. - _ba.app.accounts_v2.set_primary_credentials(None) + _babase.app.accounts.set_primary_credentials(None) return - _ba.app.accounts_v2.set_primary_credentials(result.credentials) + _babase.app.accounts.set_primary_credentials(result.credentials) def _on_implicit_sign_in_completed( self, @@ -371,7 +376,8 @@ class AccountV2Subsystem: result: LoginAdapter.SignInResult | Exception, ) -> None: """A sign-in has completed that the user didn't ask for explicitly.""" - from ba._internal import get_v1_account_state + plus = _babase.app.plus + assert plus is not None del adapter # Unused. @@ -391,16 +397,16 @@ class AccountV2Subsystem: # plug in the credentials we got. We want to be extra cautious # in case the user has since explicitly signed in since we # kicked off. - connected = _ba.app.cloud.is_connected() - signed_in_v1 = get_v1_account_state() == 'signed_in' - signed_in_v2 = _ba.app.accounts_v2.have_primary_credentials() + connected = _babase.app.cloud.is_connected() + signed_in_v1 = plus.get_v1_account_state() == 'signed_in' + signed_in_v2 = _babase.app.accounts.have_primary_credentials() if connected and not signed_in_v1 and not signed_in_v2: - _ba.app.accounts_v2.set_primary_credentials(result.credentials) + _babase.app.accounts.set_primary_credentials(result.credentials) def _on_set_active_workspace_completed(self) -> None: if not self._initial_sign_in_completed: self._initial_sign_in_completed = True - _ba.app.on_initial_sign_in_completed() + _babase.app.on_initial_sign_in_completed() class AccountV2Handle: diff --git a/src/assets/ba_data/python/babase/_app.py b/src/assets/ba_data/python/babase/_app.py new file mode 100644 index 00000000..cbd749b1 --- /dev/null +++ b/src/assets/ba_data/python/babase/_app.py @@ -0,0 +1,519 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to the high level state of the app.""" +from __future__ import annotations + +from enum import Enum +import logging +from typing import TYPE_CHECKING +from concurrent.futures import ThreadPoolExecutor +from functools import cached_property + +import _babase +from babase._language import LanguageSubsystem +from babase._plugin import PluginSubsystem +from babase._meta import MetadataSubsystem +from babase._net import NetworkSubsystem +from babase._workspace import WorkspaceSubsystem +from babase._appcomponent import AppComponentSubsystem + +if TYPE_CHECKING: + from typing import Any + import asyncio + + from efro.log import LogHandler + import babase + from babase._cloud import CloudSubsystem + from babase._accountv2 import AccountV2Subsystem + from babase._apputils import AppHealthMonitor + + # Would autogen this begin + from baclassic import ClassicSubsystem + from baplus import PlusSubsystem + + # Would autogen this end + + +class App: + """A class for high level app functionality and state. + + Category: **App Classes** + + Use babase.app to access the single shared instance of this class. + + Note that properties not documented here should be considered internal + and subject to change without warning. + """ + + # pylint: disable=too-many-public-methods + + # Implementations for these will be filled in by internal libs. + accounts: AccountV2Subsystem + cloud: CloudSubsystem + + # log_handler: LogHandler + health_monitor: AppHealthMonitor + + class State(Enum): + """High level state the app can be in.""" + + # The app launch process has not yet begun. + INITIAL = 0 + + # Our app subsystems are being inited but should not yet interact. + LAUNCHING = 1 + + # App subsystems are inited and interacting, but the app has not + # yet embarked on a high level course of action. It is doing initial + # account logins, workspace & asset downloads, etc. in order to + # prepare for this. + LOADING = 2 + + # All pieces are in place and the app is now doing its thing. + RUNNING = 3 + + # The app is backgrounded or otherwise suspended. + PAUSED = 4 + + # The app is shutting down. + SHUTTING_DOWN = 5 + + @property + def aioloop(self) -> asyncio.AbstractEventLoop: + """The logic thread's asyncio event loop. + + This allow async tasks to be run in the logic thread. + Note that, at this time, the asyncio loop is encapsulated + and explicitly stepped by the engine's logic thread loop and + thus things like asyncio.get_running_loop() will not return this + loop from most places in the logic thread; only from within a + task explicitly created in this loop. + """ + assert self._aioloop is not None + return self._aioloop + + @property + def build_number(self) -> int: + """Integer build number. + + This value increases by at least 1 with each release of the game. + It is independent of the human readable babase.App.version string. + """ + assert isinstance(self._env['build_number'], int) + return self._env['build_number'] + + @property + def device_name(self) -> str: + """Name of the device running the game.""" + assert isinstance(self._env['device_name'], str) + return self._env['device_name'] + + @property + def config_file_path(self) -> str: + """Where the game's config file is stored on disk.""" + assert isinstance(self._env['config_file_path'], str) + return self._env['config_file_path'] + + @property + def version(self) -> str: + """Human-readable version string; something like '1.3.24'. + + This should not be interpreted as a number; it may contain + string elements such as 'alpha', 'beta', 'test', etc. + If a numeric version is needed, use 'babase.App.build_number'. + """ + assert isinstance(self._env['version'], str) + return self._env['version'] + + @property + def debug_build(self) -> bool: + """Whether the app was compiled in debug mode. + + Debug builds generally run substantially slower than non-debug + builds due to compiler optimizations being disabled and extra + checks being run. + """ + assert isinstance(self._env['debug_build'], bool) + return self._env['debug_build'] + + @property + def test_build(self) -> bool: + """Whether the game was compiled in test mode. + + Test mode enables extra checks and features that are useful for + release testing but which do not slow the game down significantly. + """ + assert isinstance(self._env['test_build'], bool) + return self._env['test_build'] + + @property + def data_directory(self) -> str: + """Path where static app data lives.""" + assert isinstance(self._env['data_directory'], str) + return self._env['data_directory'] + + @property + def python_directory_user(self) -> str | None: + """Path where ballistica expects its custom user scripts (mods) to live. + + Be aware that this value may be None if ballistica is running in + a non-standard environment, and that python-path modifications may + cause modules to be loaded from other locations. + """ + assert isinstance(self._env['python_directory_user'], (str, type(None))) + return self._env['python_directory_user'] + + @property + def python_directory_app(self) -> str | None: + """Path where ballistica expects its bundled modules to live. + + Be aware that this value may be None if ballistica is running in + a non-standard environment, and that python-path modifications may + cause modules to be loaded from other locations. + """ + assert isinstance(self._env['python_directory_app'], (str, type(None))) + return self._env['python_directory_app'] + + @property + def python_directory_app_site(self) -> str | None: + """Path where ballistica expects its bundled pip modules to live. + + Be aware that this value may be None if ballistica is running in + a non-standard environment, and that python-path modifications may + cause modules to be loaded from other locations. + """ + assert isinstance( + self._env['python_directory_app_site'], (str, type(None)) + ) + return self._env['python_directory_app_site'] + + @property + def config(self) -> babase.AppConfig: + """The babase.AppConfig instance representing the app's config state.""" + assert self._config is not None + return self._config + + @property + def api_version(self) -> int: + """The app's api version. + + Only Python modules and packages associated with the current API + version number will be detected by the game (see the ba_meta tag). + This value will change whenever substantial backward-incompatible + changes are introduced to ballistica APIs. When that happens, + modules/packages should be updated accordingly and set to target + the newer API version number. + """ + from babase._meta import CURRENT_API_VERSION + + return CURRENT_API_VERSION + + @property + def on_tv(self) -> bool: + """Whether the game is currently running on a TV.""" + assert isinstance(self._env['on_tv'], bool) + return self._env['on_tv'] + + @property + def vr_mode(self) -> bool: + """Whether the game is currently running in VR.""" + assert isinstance(self._env['vr_mode'], bool) + return self._env['vr_mode'] + + def __init__(self) -> None: + """(internal) + + Do not instantiate this class; use babase.app to access + the single shared instance. + """ + + self.state = self.State.INITIAL + + self._app_bootstrapping_complete = False + self._called_on_app_launching = False + self._launch_completed = False + self._initial_sign_in_completed = False + self._meta_scan_completed = False + self._called_on_app_loading = False + self._called_on_app_running = False + self._app_paused = False + + # Config. + self.config_file_healthy = False + + # This is incremented any time the app is backgrounded/foregrounded; + # can be a simple way to determine if network data should be + # refreshed/etc. + self.fg_state = 0 + + self._aioloop: asyncio.AbstractEventLoop | None = None + + self._env = _babase.env() + self.protocol_version: int = self._env['protocol_version'] + assert isinstance(self.protocol_version, int) + self.toolbar_test: bool = self._env['toolbar_test'] + assert isinstance(self.toolbar_test, bool) + self.demo_mode: bool = self._env['demo_mode'] + assert isinstance(self.demo_mode, bool) + self.arcade_mode: bool = self._env['arcade_mode'] + assert isinstance(self.arcade_mode, bool) + self.headless_mode: bool = self._env['headless_mode'] + assert isinstance(self.headless_mode, bool) + self.iircade_mode: bool = self._env['iircade_mode'] + assert isinstance(self.iircade_mode, bool) + + # Default executor which can be used for misc background processing. + # It should also be passed to any additional asyncio loops we create + # so that everything shares the same single set of worker threads. + self.threadpool = ThreadPoolExecutor(thread_name_prefix='baworker') + + self._config: babase.AppConfig | None = None + + self.components = AppComponentSubsystem() + self.meta = MetadataSubsystem() + self.plugins = PluginSubsystem() + self.lang = LanguageSubsystem() + self.net = NetworkSubsystem() + self.workspaces = WorkspaceSubsystem() + # self._classic: ClassicSubsystem | None = None + + self._asyncio_timer: babase.AppTimer | None = None + + def postinit(self) -> None: + """Called after we are inited and assigned to babase.app.""" + + # NOTE: the reason we need a postinit here is that + # some of this stuff might try importing babase.app and that doesn't + # exist yet as of our __init__() call. + + # Init classic if present. + # classic_subsystem_type: type[ClassicSubsystem] | None + # try: + # from baclassic import ClassicSubsystem + + # classic_subsystem_type = ClassicSubsystem + # except ImportError: + # classic_subsystem_type = None + + # if classic_subsystem_type is not None: + # self._classic = classic_subsystem_type() + + # Would autogen this begin + + @cached_property + def classic(self) -> ClassicSubsystem | None: + """Our classic subsystem.""" + + try: + from baclassic import ClassicSubsystem + + return ClassicSubsystem() + except ImportError: + return None + except Exception: + logging.exception('Error importing baclassic') + return None + + @cached_property + def plus(self) -> PlusSubsystem | None: + """Our plus subsystem.""" + + try: + from baplus import PlusSubsystem + + return PlusSubsystem() + except ImportError: + return None + except Exception: + logging.exception('Error importing baplus') + return None + + # Would autogen this begin + + def run(self) -> None: + """Run the app to completion. + + Note that this only works on platforms where ballistica + manages its own event loop. + """ + _babase.run_app() + + def on_app_launching(self) -> None: + """Called when the app is first entering the launching state.""" + # pylint: disable=cyclic-import + from babase import _asyncio + from babase import _appconfig + from babase._apputils import log_dumped_app_state, AppHealthMonitor + + assert _babase.in_logic_thread() + + self._aioloop = _asyncio.setup_asyncio() + self.health_monitor = AppHealthMonitor() + + # Only proceed if our config file is healthy so we don't + # overwrite a broken one or whatnot and wipe out data. + if not self.config_file_healthy: + if self.classic is not None: + handled = self.classic.show_config_error_window() + if handled: + return + + # For now on other systems we just overwrite the bum config. + # At this point settings are already set; lets just commit them + # to disk. + _appconfig.commit_app_config(force=True) + + # Get meta-system scanning built-in stuff in the bg. + self.meta.start_scan(scan_complete_cb=self.on_meta_scan_complete) + + self.accounts.on_app_launching() + + # Make sure this runs after we init our accounts stuff, since + # classic accounts key off of our v2 ones. + if self.classic is not None: + self.classic.on_app_launching() + + # If any traceback dumps happened last run, log and clear them. + log_dumped_app_state() + + self._launch_completed = True + self._update_state() + + def on_app_loading(self) -> None: + """Called when initially entering the loading state.""" + + def on_app_running(self) -> None: + """Called when initially entering the running state.""" + + self.plugins.on_app_running() + + def on_app_bootstrapping_complete(self) -> None: + """Called by the C++ layer once its ready to rock.""" + assert _babase.in_logic_thread() + assert not self._app_bootstrapping_complete + self._app_bootstrapping_complete = True + self._update_state() + + def on_meta_scan_complete(self) -> None: + """Called by meta-scan when it is done doing its thing.""" + assert _babase.in_logic_thread() + self.plugins.on_meta_scan_complete() + + assert not self._meta_scan_completed + self._meta_scan_completed = True + self._update_state() + + def _update_state(self) -> None: + # pylint: disable=too-many-branches + assert _babase.in_logic_thread() + + if self._app_paused: + # Entering paused state: + if self.state is not self.State.PAUSED: + self.state = self.State.PAUSED + self.on_app_pause() + else: + # Leaving paused state: + if self.state is self.State.PAUSED: + self.on_app_resume() + + # Handle initially entering or returning to other states. + if self._initial_sign_in_completed and self._meta_scan_completed: + if self.state != self.State.RUNNING: + self.state = self.State.RUNNING + _babase.bootlog('app state running') + if not self._called_on_app_running: + self._called_on_app_running = True + self.on_app_running() + elif self._launch_completed: + if self.state is not self.State.LOADING: + self.state = self.State.LOADING + _babase.bootlog('app state loading') + if not self._called_on_app_loading: + self._called_on_app_loading = True + self.on_app_loading() + else: + # Only thing left is launching. We shouldn't be getting + # called before at least that is complete. + assert self._app_bootstrapping_complete + if self.state is not self.State.LAUNCHING: + self.state = self.State.LAUNCHING + _babase.bootlog('app state launching') + if not self._called_on_app_launching: + self._called_on_app_launching = True + self.on_app_launching() + + def pause(self) -> None: + """Should be called by the native layer when the app pauses.""" + assert not self._app_paused # Should avoid redundant calls. + self._app_paused = True + self._update_state() + + def resume(self) -> None: + """Should be called by the native layer when the app resumes.""" + assert self._app_paused # Should avoid redundant calls. + self._app_paused = False + self._update_state() + + def on_app_pause(self) -> None: + """Called when the app goes to a paused state.""" + self.cloud.on_app_pause() + self.plugins.on_app_pause() + self.health_monitor.on_app_pause() + if self.classic is not None: + self.classic.on_app_pause() + + def on_app_resume(self) -> None: + """Called when resuming.""" + self.fg_state += 1 + self.cloud.on_app_resume() + self.plugins.on_app_resume() + self.health_monitor.on_app_resume() + if self.classic is not None: + self.classic.on_app_resume() + + def on_app_shutdown(self) -> None: + """(internal)""" + self.state = self.State.SHUTTING_DOWN + self.plugins.on_app_shutdown() + if self.classic is not None: + self.classic.on_app_shutdown() + + def read_config(self) -> None: + """(internal)""" + from babase._appconfig import read_config + + self._config, self.config_file_healthy = read_config() + + def handle_deep_link(self, url: str) -> None: + """Handle a deep link URL.""" + from babase._language import Lstr + + appname = _babase.appname() + if url.startswith(f'{appname}://code/'): + code = url.replace(f'{appname}://code/', '') + if self.classic is not None: + self.classic.accounts.add_pending_promo_code(code) + else: + try: + _babase.screenmessage( + Lstr(resource='errorText'), color=(1, 0, 0) + ) + _babase.getsimplesound('error').play() + except ImportError: + pass + + def on_initial_sign_in_completed(self) -> None: + """Callback to be run after initial sign-in (or lack thereof). + + This period includes things such as syncing account workspaces + or other data so it may take a substantial amount of time. + This should also run after a short amount of time if no login + has occurred. + """ + # Tell meta it can start scanning extra stuff that just showed up + # (account workspaces). + self.meta.start_extra_scan() + + self._initial_sign_in_completed = True + self._update_state() diff --git a/assets/src/ba_data/python/ba/_appcomponent.py b/src/assets/ba_data/python/babase/_appcomponent.py similarity index 91% rename from assets/src/ba_data/python/ba/_appcomponent.py rename to src/assets/ba_data/python/babase/_appcomponent.py index 0e1f635d..bbd4cc73 100644 --- a/assets/src/ba_data/python/ba/_appcomponent.py +++ b/src/assets/ba_data/python/babase/_appcomponent.py @@ -5,7 +5,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, TypeVar, cast -import _ba +import _babase if TYPE_CHECKING: from typing import Callable, Any @@ -22,7 +22,7 @@ class AppComponentSubsystem: functionality for the app, and allows plugins or other custom code to easily override said functionality. - Use ba.app.components to get the single shared instance of this class. + Use babase.app.components to get the single shared instance of this class. The general idea with this setup is that a base-class is defined to provide some functionality and then anyone wanting that functionality @@ -47,7 +47,7 @@ class AppComponentSubsystem: """ # Currently limiting this to logic-thread use; can revisit if needed # (would need to guard access to our implementations dict). - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() if not issubclass(implementation, baseclass): raise TypeError( @@ -60,7 +60,7 @@ class AppComponentSubsystem: # If we're the first thing getting dirtied, set up a callback to # clean everything. And add ourself to the dirty list regardless. if not self._dirty_base_classes: - _ba.pushcall(self._run_change_callbacks) + _babase.pushcall(self._run_change_callbacks) self._dirty_base_classes.add(baseclass) def getclass(self, baseclass: T) -> T: @@ -69,7 +69,7 @@ class AppComponentSubsystem: If no custom implementation has been set, the provided base-class is returned. """ - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() del baseclass # Unused. return cast(T, None) @@ -83,7 +83,7 @@ class AppComponentSubsystem: loop. Note that any further setclass calls before the callback runs will not result in additional callbacks. """ - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() self._change_callbacks.setdefault(baseclass, []).append(callback) def _run_change_callbacks(self) -> None: diff --git a/assets/src/ba_data/python/ba/_appconfig.py b/src/assets/ba_data/python/babase/_appconfig.py similarity index 78% rename from assets/src/ba_data/python/ba/_appconfig.py rename to src/assets/ba_data/python/babase/_appconfig.py index b4339718..f5070c3c 100644 --- a/assets/src/ba_data/python/ba/_appconfig.py +++ b/src/assets/ba_data/python/babase/_appconfig.py @@ -5,7 +5,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba +import _babase if TYPE_CHECKING: from typing import Any @@ -20,7 +20,7 @@ class AppConfig(dict): defaults, applying contained values to the game, and committing the config to storage. - Call ba.appconfig() to get the single shared instance of this class. + Call babase.appconfig() to get the single shared instance of this class. 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). @@ -33,32 +33,33 @@ class AppConfig(dict): This will substitute application defaults for values not present in the config dict, filter some invalid values, etc. Note that these values do not represent the state of the app; simply the state of its - config. Use ba.App to access actual live state. + config. Use babase.App to access actual live state. Raises an Exception for unrecognized key names. To get the list of keys - supported by this method, use ba.AppConfig.builtin_keys(). Note that it - is perfectly legal to store other data in the config; it just needs to - be accessed through standard dict methods and missing values handled - manually. + supported by this method, use babase.AppConfig.builtin_keys(). Note + that it is perfectly legal to store other data in the config; it just + needs to be accessed through standard dict methods and missing values + handled manually. """ - return _ba.resolve_appconfig_value(key) + return _babase.resolve_appconfig_value(key) def default_value(self, key: str) -> Any: """Given a string key, return its predefined default value. - This is the value that will be returned by ba.AppConfig.resolve() if - the key is not present in the config dict or of an incompatible type. + This is the value that will be returned by babase.AppConfig.resolve() + if the key is not present in the config dict or of an incompatible + type. Raises an Exception for unrecognized key names. To get the list of keys - supported by this method, use ba.AppConfig.builtin_keys(). Note that it - is perfectly legal to store other data in the config; it just needs to - be accessed through standard dict methods and missing values handled - manually. + supported by this method, use babase.AppConfig.builtin_keys(). Note + that it is perfectly legal to store other data in the config; it just + needs to be accessed through standard dict methods and missing values + handled manually. """ - return _ba.get_appconfig_default_value(key) + return _babase.get_appconfig_default_value(key) def builtin_keys(self) -> list[str]: - """Return the list of valid key names recognized by ba.AppConfig. + """Return the list of valid key names recognized by babase.AppConfig. This set of keys can be used with resolve(), default_value(), etc. It does not vary across platforms and may include keys that are @@ -70,11 +71,11 @@ class AppConfig(dict): config, but in that case it is up to the user to test for the existence of the key in the config dict, fall back to consistent defaults, etc. """ - return _ba.get_appconfig_builtin_keys() + return _babase.get_appconfig_builtin_keys() def apply(self) -> None: """Apply config values to the running app.""" - _ba.apply_config() + _babase.apply_config() def commit(self) -> None: """Commits the config to local storage. @@ -97,13 +98,12 @@ def read_config() -> tuple[AppConfig, bool]: """Read the game config.""" import os import json - from ba._generated.enums import TimeType config_file_healthy = False # NOTE: it is assumed that this only gets called once and the # config object will not change from here on out - config_file_path = _ba.app.config_file_path + config_file_path = _babase.app.config_file_path config_contents = '' try: if os.path.exists(config_file_path): @@ -118,7 +118,7 @@ def read_config() -> tuple[AppConfig, bool]: print( ( 'error reading config file at time ' - + str(_ba.time(TimeType.REAL)) + + str(_babase.apptime()) + ': \'' + config_file_path + '\':\n' @@ -166,12 +166,13 @@ def commit_app_config(force: bool = False) -> None: (internal) """ - from ba._internal import mark_config_dirty + plus = _babase.app.plus + assert plus is not None - if not _ba.app.config_file_healthy and not force: + if not _babase.app.config_file_healthy and not force: print( 'Current config file is broken; ' 'skipping write to avoid losing settings.' ) return - mark_config_dirty() + plus.mark_config_dirty() diff --git a/assets/src/ba_data/python/ba/_apputils.py b/src/assets/ba_data/python/babase/_apputils.py similarity index 69% rename from assets/src/ba_data/python/ba/_apputils.py rename to src/assets/ba_data/python/babase/_apputils.py index af4a7769..a5aaa443 100644 --- a/assets/src/ba_data/python/ba/_apputils.py +++ b/src/assets/ba_data/python/babase/_apputils.py @@ -10,13 +10,15 @@ from threading import Thread from dataclasses import dataclass from typing import TYPE_CHECKING +from efro.call import tpartial from efro.log import LogLevel from efro.dataclassio import ioprepped, dataclass_to_json, dataclass_from_json -import _ba +import _babase if TYPE_CHECKING: from typing import Any, TextIO - import ba + + import babase def is_browser_likely_available() -> bool: @@ -24,36 +26,44 @@ def is_browser_likely_available() -> bool: category: General Utility Functions - If this returns False you may want to avoid calling ba.show_url() + If this returns False you may want to avoid calling babase.show_url() with any lengthy addresses. (ba.show_url() will display an address as a string in a window if unable to bring up a browser, but that is only useful for simple URLs.) """ - app = _ba.app - platform = app.platform - touchscreen = _ba.getinputdevice('TouchScreen', '#1', doraise=False) + app = _babase.app + + if app.classic is None: + logging.warning( + 'is_browser_likely_available() needs to be updated' + ' to work without classic.' + ) + return True + + platform = app.classic.platform + hastouchscreen = _babase.hastouchscreen() # If we're on a vr device or an android device with no touchscreen, # assume no browser. # FIXME: Might not be the case anymore; should make this definable # at the platform level. - if app.vr_mode or (platform == 'android' and touchscreen is None): + if app.vr_mode or (platform == 'android' and hastouchscreen): return False # Anywhere else assume we've got one. return True -def get_remote_app_name() -> ba.Lstr: +def get_remote_app_name() -> babase.Lstr: """(internal)""" - from ba import _language + from babase import _language return _language.Lstr(resource='remote_app.app_name') def should_submit_debug_info() -> bool: """(internal)""" - return _ba.app.config.get('Submit Debug Info', True) + return _babase.app.config.get('Submit Debug Info', True) def handle_v1_cloud_log() -> None: @@ -62,80 +72,88 @@ def handle_v1_cloud_log() -> None: When this happens, we can upload our log to the server after a short bit if desired. """ - from ba._net import master_server_post - from ba._generated.enums import TimeType - from ba._internal import get_news_show - app = _ba.app - app.log_have_new = True - if not app.log_upload_timer_started: + app = _babase.app + + if app.classic is None or app.plus is None: + if _babase.do_once(): + logging.warning( + 'handle_v1_cloud_log should not be used' + ' without classic or plus.' + ) + return + + app.classic.log_have_new = True + if not app.classic.log_upload_timer_started: def _put_log() -> None: + assert app.plus is not None + assert app.classic is not None try: - sessionname = str(_ba.get_foreground_host_session()) + sessionname = str(app.classic.get_foreground_host_session()) except Exception: sessionname = 'unavailable' try: - activityname = str(_ba.get_foreground_host_activity()) + activityname = str(app.classic.get_foreground_host_activity()) except Exception: activityname = 'unavailable' info = { - 'log': _ba.get_v1_cloud_log(), + 'log': _babase.get_v1_cloud_log(), 'version': app.version, 'build': app.build_number, - 'userAgentString': app.user_agent_string, + 'userAgentString': app.classic.user_agent_string, 'session': sessionname, 'activity': activityname, 'fatal': 0, - 'userRanCommands': _ba.has_user_run_commands(), - 'time': _ba.time(TimeType.REAL), - 'userModded': _ba.workspaces_in_use(), - 'newsShow': get_news_show(), + 'userRanCommands': _babase.has_user_run_commands(), + 'time': _babase.apptime(), + 'userModded': _babase.workspaces_in_use(), + 'newsShow': app.plus.get_news_show(), } def response(data: Any) -> None: + assert app.classic is not None # A non-None response means success; lets # take note that we don't need to report further # log info this run if data is not None: - app.log_have_new = False - _ba.mark_log_sent() + app.classic.log_have_new = False + _babase.mark_log_sent() - master_server_post('bsLog', info, response) + app.classic.master_server_v1_post('bsLog', info, response) - app.log_upload_timer_started = True + app.classic.log_upload_timer_started = True # Delay our log upload slightly in case other # pertinent info gets printed between now and then. - with _ba.Context('ui'): - _ba.timer(3.0, _put_log, timetype=TimeType.REAL) + with _babase.ContextRef.empty(): + _babase.apptimer(3.0, _put_log) # After a while, allow another log-put. def _reset() -> None: - app.log_upload_timer_started = False - if app.log_have_new: + assert app.classic is not None + app.classic.log_upload_timer_started = False + if app.classic.log_have_new: handle_v1_cloud_log() - if not _ba.is_log_full(): - with _ba.Context('ui'): - _ba.timer( - 600.0, - _reset, - timetype=TimeType.REAL, - suppress_format_warning=True, - ) + if not _babase.is_log_full(): + with _babase.ContextRef.empty(): + _babase.apptimer(600.0, _reset) def handle_leftover_v1_cloud_log_file() -> None: """Handle an un-uploaded v1-cloud-log from a previous run.""" + + # Only applies with classic present. + if _babase.app.classic is None: + return try: import json - from ba._net import master_server_post - if os.path.exists(_ba.get_v1_cloud_log_file_path()): + if os.path.exists(_babase.get_v1_cloud_log_file_path()): with open( - _ba.get_v1_cloud_log_file_path(), encoding='utf-8' + _babase.get_v1_cloud_log_file_path(), encoding='utf-8' ) as infile: info = json.loads(infile.read()) infile.close() @@ -147,19 +165,21 @@ def handle_leftover_v1_cloud_log_file() -> None: # lets kill it. if data is not None: try: - os.remove(_ba.get_v1_cloud_log_file_path()) + os.remove(_babase.get_v1_cloud_log_file_path()) except FileNotFoundError: # Saw this in the wild. The file just existed # a moment ago but I suppose something could have # killed it since. ¯\_(ツ)_/¯ pass - master_server_post('bsLog', info, response) + _babase.app.classic.master_server_v1_post( + 'bsLog', info, response + ) else: # If they don't want logs uploaded just kill it. - os.remove(_ba.get_v1_cloud_log_file_path()) + os.remove(_babase.get_v1_cloud_log_file_path()) except Exception: - from ba import _error + from babase import _error _error.print_exception('Error handling leftover log file.') @@ -180,8 +200,8 @@ def garbage_collect_session_end() -> None: # running them with an explicit flag passed, but we should never # run them by default because gc.get_objects() can mess up the app. # See notes at top of efro.debug. - if bool(False): - print_live_object_warnings('after session shutdown') + # if bool(False): + # print_live_object_warnings('after session shutdown') def garbage_collect() -> None: @@ -196,75 +216,21 @@ def garbage_collect() -> None: gc.collect() -def print_live_object_warnings( - when: Any, - ignore_session: ba.Session | None = None, - ignore_activity: ba.Activity | None = None, -) -> None: - """Print warnings for remaining objects in the current context. - - IMPORTANT - don't call this in production; usage of gc.get_objects() - can bork Python. See notes at top of efro.debug module. - """ - # pylint: disable=cyclic-import - from ba._session import Session - from ba._actor import Actor - from ba._activity import Activity - - sessions: list[ba.Session] = [] - activities: list[ba.Activity] = [] - actors: list[ba.Actor] = [] - - # Once we come across leaked stuff, printing again is probably - # redundant. - if _ba.app.printed_live_object_warning: - return - for obj in gc.get_objects(): - if isinstance(obj, Actor): - actors.append(obj) - elif isinstance(obj, Session): - sessions.append(obj) - elif isinstance(obj, Activity): - activities.append(obj) - - # Complain about any remaining sessions. - for session in sessions: - if session is ignore_session: - continue - _ba.app.printed_live_object_warning = True - print(f'ERROR: Session found {when}: {session}') - - # Complain about any remaining activities. - for activity in activities: - if activity is ignore_activity: - continue - _ba.app.printed_live_object_warning = True - print(f'ERROR: Activity found {when}: {activity}') - - # Complain about any remaining actors. - for actor in actors: - _ba.app.printed_live_object_warning = True - print(f'ERROR: Actor found {when}: {actor}') - - def print_corrupt_file_error() -> None: """Print an error if a corrupt file is found.""" - from ba._general import Call - from ba._generated.enums import TimeType - _ba.timer( - 2.0, - lambda: _ba.screenmessage( - _ba.app.lang.get_resource('internal.corruptFileText').replace( - '${EMAIL}', 'support@froemling.net' + # FIXME - filter this out for builds without bauiv1. + if not _babase.app.headless_mode: + _babase.apptimer( + 2.0, + lambda: _babase.screenmessage( + _babase.app.lang.get_resource( + 'internal.corruptFileText' + ).replace('${EMAIL}', 'support@froemling.net'), + color=(1, 0, 0), ), - color=(1, 0, 0), - ), - timetype=TimeType.REAL, - ) - _ba.timer( - 2.0, Call(_ba.playsound, _ba.getsound('error')), timetype=TimeType.REAL - ) + ) + _babase.apptimer(2.0, _babase.getsimplesound('error').play) _tbfiles: list[TextIO] = [] @@ -302,7 +268,6 @@ def dump_app_state( """ # pylint: disable=consider-using-with import faulthandler - from ba._generated.enums import TimeType # Dump our metadata immediately. If a delay is passed, it generally # means we expect things to hang momentarily, so we should not delay @@ -311,14 +276,14 @@ def dump_app_state( # the dump in that case. try: mdpath = os.path.join( - os.path.dirname(_ba.app.config_file_path), '_appstate_dump_md' + os.path.dirname(_babase.app.config_file_path), '_appstate_dump_md' ) with open(mdpath, 'w', encoding='utf-8') as outfile: outfile.write( dataclass_to_json( DumpedAppStateMetadata( reason=reason, - app_time=_ba.time(TimeType.REAL), + app_time=_babase.apptime(), log_level=log_level, ) ) @@ -329,7 +294,7 @@ def dump_app_state( return tbpath = os.path.join( - os.path.dirname(_ba.app.config_file_path), '_appstate_dump_tb' + os.path.dirname(_babase.app.config_file_path), '_appstate_dump_tb' ) # faulthandler needs the raw file descriptor to still be valid when @@ -347,10 +312,8 @@ def dump_app_state( # Allow sufficient time since we don't know how long the dump takes. # We want this to work from any thread, so need to kick this part # over to the logic thread so timer works. - _ba.pushcall( - lambda: _ba.timer( - delay + 1.0, log_dumped_app_state, timetype=TimeType.REAL - ), + _babase.pushcall( + tpartial(_babase.apptimer, delay + 1.0, log_dumped_app_state), from_other_thread=True, suppress_other_thread_warning=True, ) @@ -362,7 +325,7 @@ def log_dumped_app_state() -> None: try: out = '' mdpath = os.path.join( - os.path.dirname(_ba.app.config_file_path), '_appstate_dump_md' + os.path.dirname(_babase.app.config_file_path), '_appstate_dump_md' ) if os.path.exists(mdpath): with open(mdpath, 'r', encoding='utf-8') as infile: @@ -375,7 +338,8 @@ def log_dumped_app_state() -> None: f'Time: {metadata.app_time:.2f}' ) tbpath = os.path.join( - os.path.dirname(_ba.app.config_file_path), '_appstate_dump_tb' + os.path.dirname(_babase.app.config_file_path), + '_appstate_dump_tb', ) if os.path.exists(tbpath): with open(tbpath, 'r', encoding='utf-8') as infile: @@ -390,7 +354,7 @@ class AppHealthMonitor: """Logs things like app-not-responding issues.""" def __init__(self) -> None: - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() self._running = True self._thread = Thread(target=self._app_monitor_thread_main, daemon=True) self._thread.start() @@ -398,14 +362,13 @@ class AppHealthMonitor: self._first_check = True def _app_monitor_thread_main(self) -> None: - try: self._monitor_app() except Exception: logging.exception('Error in AppHealthMonitor thread.') def _set_response(self) -> None: - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() self._response = True def _check_running(self) -> bool: @@ -417,7 +380,6 @@ class AppHealthMonitor: import time while bool(True): - # Always sleep a bit between checks. time.sleep(1.234) @@ -428,9 +390,8 @@ class AppHealthMonitor: # Wait for the logic thread to run something we send it. starttime = time.monotonic() self._response = False - _ba.pushcall(self._set_response, raw=True) + _babase.pushcall(self._set_response, raw=True) while not self._response: - # Abort this check if we went into the background. if not self._check_running(): break @@ -460,20 +421,19 @@ class AppHealthMonitor: def on_app_pause(self) -> None: """Should be called when the app pauses.""" - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() self._running = False def on_app_resume(self) -> None: """Should be called when the app resumes.""" - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() self._running = True def on_too_many_file_descriptors() -> None: """Called when too many file descriptors are open; trying to debug.""" - from ba._generated.enums import TimeType - real_time = _ba.time(TimeType.REAL) + real_time = _babase.apptime() def _do_log() -> None: pid = os.getpid() diff --git a/assets/src/ba_data/python/ba/_assetmanager.py b/src/assets/ba_data/python/babase/_assetmanager.py similarity index 98% rename from assets/src/ba_data/python/ba/_assetmanager.py rename to src/assets/ba_data/python/babase/_assetmanager.py index 2e48619e..035c57c2 100644 --- a/assets/src/ba_data/python/ba/_assetmanager.py +++ b/src/assets/ba_data/python/babase/_assetmanager.py @@ -21,7 +21,7 @@ from efro.dataclassio import ( dataclass_from_json, dataclass_to_json, ) -import _ba +import _babase if TYPE_CHECKING: from bacommon.assets import AssetPackageFlavor @@ -184,7 +184,9 @@ def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None: # Pass a very short timeout to urllib so we have opportunities # to cancel even with network blockage. - req = urllib.request.urlopen(url, context=_ba.app.net.sslcontext, timeout=1) + req = urllib.request.urlopen( + url, context=_babase.app.net.sslcontext, timeout=1 + ) file_size = int(req.headers['Content-Length']) print(f'\nDownloading: {filename} Bytes: {file_size:,}') diff --git a/assets/src/ba_data/python/ba/_asyncio.py b/src/assets/ba_data/python/babase/_asyncio.py similarity index 87% rename from assets/src/ba_data/python/ba/_asyncio.py rename to src/assets/ba_data/python/babase/_asyncio.py index 5dbf8edc..2a3e642a 100644 --- a/assets/src/ba_data/python/ba/_asyncio.py +++ b/src/assets/ba_data/python/babase/_asyncio.py @@ -16,10 +16,10 @@ import time import os if TYPE_CHECKING: - import ba + import babase # Our timer and event loop for the ballistica logic thread. -_asyncio_timer: ba.Timer | None = None +_asyncio_timer: babase.AppTimer | None = None _asyncio_event_loop: asyncio.AbstractEventLoop | None = None DEBUG_TIMING = os.environ.get('BA_DEBUG_TIMING') == '1' @@ -29,11 +29,11 @@ def setup_asyncio() -> asyncio.AbstractEventLoop: """Setup asyncio functionality for the logic thread.""" # pylint: disable=global-statement - import _ba - import ba - from ba._generated.enums import TimeType + import _babase + import babase + from babase._mgen.enums import TimeType - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() # Create our event-loop. We don't expect there to be one # running on this thread before we do. @@ -45,7 +45,7 @@ def setup_asyncio() -> asyncio.AbstractEventLoop: global _asyncio_event_loop # pylint: disable=invalid-name _asyncio_event_loop = asyncio.new_event_loop() - _asyncio_event_loop.set_default_executor(ba.app.threadpool) + _asyncio_event_loop.set_default_executor(babase.app.threadpool) # Ideally we should integrate asyncio into our C++ Thread class's # low level event loop so that asyncio timers/sockets/etc. could @@ -74,9 +74,7 @@ def setup_asyncio() -> asyncio.AbstractEventLoop: ) global _asyncio_timer # pylint: disable=invalid-name - _asyncio_timer = _ba.Timer( - 1.0 / 30.0, run_cycle, timetype=TimeType.REAL, repeat=True - ) + _asyncio_timer = _babase.AppTimer(1.0 / 30.0, run_cycle, repeat=True) if bool(False): @@ -87,6 +85,6 @@ def setup_asyncio() -> asyncio.AbstractEventLoop: await asyncio.sleep(2.0) print('TEST AIO TASK ENDING') - _asyncio_event_loop.create_task(aio_test()) + _testtask = _asyncio_event_loop.create_task(aio_test()) return _asyncio_event_loop diff --git a/assets/src/ba_data/python/ba/_cloud.py b/src/assets/ba_data/python/babase/_cloud.py similarity index 95% rename from assets/src/ba_data/python/ba/_cloud.py rename to src/assets/ba_data/python/babase/_cloud.py index e9ead0b4..a925eb79 100644 --- a/assets/src/ba_data/python/ba/_cloud.py +++ b/src/assets/ba_data/python/babase/_cloud.py @@ -7,7 +7,7 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING, overload -import _ba +import _babase if TYPE_CHECKING: from typing import Callable, Any @@ -46,7 +46,7 @@ class CloudSubsystem: # Inform things that use this. # (TODO: should generalize this into some sort of registration system) - _ba.app.accounts_v2.on_cloud_connectivity_changed(connected) + _babase.app.accounts.on_cloud_connectivity_changed(connected) @overload def send_message_cb( @@ -114,11 +114,11 @@ class CloudSubsystem: The provided on_response call will be run in the logic thread and passed either the response or the error that occurred. """ - from ba._general import Call + from babase._general import Call del msg # Unused. - _ba.pushcall( + _babase.pushcall( Call( on_response, RuntimeError('Cloud functionality is not available.'), @@ -155,10 +155,8 @@ def cloud_console_exec(code: str) -> None: """Called by the cloud console to run code in the logic thread.""" import sys import __main__ - from ba._generated.enums import TimeType try: - # First try it as eval. try: evalcode = compile(code, '', 'eval') @@ -187,7 +185,7 @@ def cloud_console_exec(code: str) -> None: except Exception: import traceback - apptime = _ba.time(TimeType.REAL) + apptime = _babase.apptime() print(f'Exec error at time {apptime:.2f}.', file=sys.stderr) traceback.print_exc() diff --git a/assets/src/ba_data/python/ba/_error.py b/src/assets/ba_data/python/babase/_error.py similarity index 73% rename from assets/src/ba_data/python/ba/_error.py rename to src/assets/ba_data/python/babase/_error.py index 960f9e57..65c97bf1 100644 --- a/assets/src/ba_data/python/ba/_error.py +++ b/src/assets/ba_data/python/babase/_error.py @@ -6,29 +6,10 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba +import _babase if TYPE_CHECKING: from typing import Any - import ba - - -class DependencyError(Exception): - """Exception raised when one or more ba.Dependency items are missing. - - Category: **Exception Classes** - - (this will generally be missing assets). - """ - - def __init__(self, deps: list[ba.Dependency]): - super().__init__() - self._deps = deps - - @property - def deps(self) -> list[ba.Dependency]: - """The list of missing dependencies causing this error.""" - return self._deps class ContextError(Exception): @@ -49,28 +30,28 @@ class NotFoundError(Exception): class PlayerNotFoundError(NotFoundError): - """Exception raised when an expected ba.Player does not exist. + """Exception raised when an expected player does not exist. Category: **Exception Classes** """ class SessionPlayerNotFoundError(NotFoundError): - """Exception raised when an expected ba.SessionPlayer does not exist. + """Exception raised when an expected session-player does not exist. Category: **Exception Classes** """ class TeamNotFoundError(NotFoundError): - """Exception raised when an expected ba.Team does not exist. + """Exception raised when an expected bascenev1.Team does not exist. Category: **Exception Classes** """ class MapNotFoundError(NotFoundError): - """Exception raised when an expected ba.Map does not exist. + """Exception raised when an expected bascenev1.Map does not exist. Category: **Exception Classes** """ @@ -84,49 +65,49 @@ class DelegateNotFoundError(NotFoundError): class SessionTeamNotFoundError(NotFoundError): - """Exception raised when an expected ba.SessionTeam does not exist. + """Exception raised when an expected session-team does not exist. Category: **Exception Classes** """ class NodeNotFoundError(NotFoundError): - """Exception raised when an expected ba.Node does not exist. + """Exception raised when an expected Node does not exist. Category: **Exception Classes** """ class ActorNotFoundError(NotFoundError): - """Exception raised when an expected ba.Actor does not exist. + """Exception raised when an expected actor does not exist. Category: **Exception Classes** """ class ActivityNotFoundError(NotFoundError): - """Exception raised when an expected ba.Activity does not exist. + """Exception raised when an expected bascenev1.Activity does not exist. Category: **Exception Classes** """ class SessionNotFoundError(NotFoundError): - """Exception raised when an expected ba.Session does not exist. + """Exception raised when an expected session does not exist. Category: **Exception Classes** """ class InputDeviceNotFoundError(NotFoundError): - """Exception raised when an expected ba.InputDevice does not exist. + """Exception raised when an expected input-device does not exist. Category: **Exception Classes** """ class WidgetNotFoundError(NotFoundError): - """Exception raised when an expected ba.Widget does not exist. + """Exception raised when an expected widget does not exist. Category: **Exception Classes** """ @@ -156,12 +137,12 @@ def print_exception(*args: Any, **keywds: Any) -> None: try: # If we're only printing once and already have, bail. if keywds.get('once', False): - if not _ba.do_once(): + if not _babase.do_once(): return err_str = ' '.join([str(a) for a in args]) print('ERROR:', err_str) - _ba.print_context() + _babase.print_context() print('PRINTED-FROM:') # Basically the output of traceback.print_stack() @@ -174,7 +155,7 @@ def print_exception(*args: Any, **keywds: Any) -> None: print('\n'.join(' ' + l for l in excstr.splitlines())) except Exception: # I suppose using print_exception here would be a bad idea. - print('ERROR: exception in ba.print_exception():') + print('ERROR: exception in babase.print_exception():') traceback.print_exc() @@ -193,15 +174,15 @@ def print_error(err_str: str, once: bool = False) -> None: try: # If we're only printing once and already have, bail. if once: - if not _ba.do_once(): + if not _babase.do_once(): return print('ERROR:', err_str) - _ba.print_context() + _babase.print_context() # Basically the output of traceback.print_stack() stackstr = ''.join(traceback.format_stack()) print(stackstr, end='') except Exception: - print('ERROR: exception in ba.print_error():') + print('ERROR: exception in babase.print_error():') traceback.print_exc() diff --git a/assets/src/ba_data/python/ba/_general.py b/src/assets/ba_data/python/babase/_general.py similarity index 87% rename from assets/src/ba_data/python/ba/_general.py rename to src/assets/ba_data/python/babase/_general.py index bb992315..d98485c5 100644 --- a/assets/src/ba_data/python/ba/_general.py +++ b/src/assets/ba_data/python/babase/_general.py @@ -7,18 +7,30 @@ import types import weakref import random import inspect -from typing import TYPE_CHECKING, TypeVar, Protocol +from typing import TYPE_CHECKING, TypeVar, Protocol, NewType from efro.terminal import Clr -import _ba -from ba._error import print_error, print_exception -from ba._generated.enums import TimeType +import _babase +from babase._error import print_error, print_exception +from babase._mgen.enums import TimeType if TYPE_CHECKING: from typing import Any from efro.call import Call as Call # 'as Call' so we re-export. +# Declare distinct types for different time measurements we use so the +# type-checker can help prevent us from mixing and matching accidentally. + +# Our monotonic time measurement that starts at 0 when the app launches +# and pauses while the app is suspended. +AppTime = NewType('AppTime', float) + +# Like app-time but incremented at frame draw time and in a smooth +# consistent manner; useful to keep animations smooth and jitter-free. +DisplayTime = NewType('DisplayTime', float) + + class Existable(Protocol): """A Protocol for objects supporting an exists() method. @@ -34,7 +46,7 @@ T = TypeVar('T') def existing(obj: ExistableT | None) -> ExistableT | None: - """Convert invalid references to None for any ba.Existable object. + """Convert invalid references to None for any babase.Existable object. Category: **Gameplay Functions** @@ -96,7 +108,7 @@ def json_prep(data: Any) -> Any: try: return data.decode(errors='ignore') except Exception: - from ba import _error + from babase import _error print_error('json_prep encountered utf-8 decode error', once=True) return data.decode(errors='ignore') @@ -147,18 +159,18 @@ class _WeakCall: we overwrite its variable with None because the bound method we pass as a timer callback (foo.bar) strong-references it >>> foo = FooClass() - ... ba.timer(5.0, foo.bar) + ... babase.apptimer(5.0, foo.bar) ... foo = None **EXAMPLE B:** This code will *not* keep our object alive; it will die when we overwrite it with None and the timer will be a no-op when it fires >>> foo = FooClass() - ... ba.timer(5.0, ba.WeakCall(foo.bar)) + ... babase.apptimer(5.0, ba.WeakCall(foo.bar)) ... foo = None **EXAMPLE C:** Wrap a method call with some positional and keyword args: - >>> myweakcall = ba.WeakCall(self.dostuff, argval1, + >>> myweakcall = babase.WeakCall(self.dostuff, argval1, ... namedarg=argval2) ... # Now we have a single callable to run that whole mess. ... # The same as calling myobj.dostuff(argval1, namedarg=argval2) @@ -171,6 +183,8 @@ class _WeakCall: to wrap them in weakrefs manually if desired. """ + _did_invalid_call_warning = False + def __init__(self, *args: Any, **keywds: Any) -> None: """Instantiate a WeakCall. @@ -180,21 +194,21 @@ class _WeakCall: if hasattr(args[0], '__func__'): self._call = WeakMethod(args[0]) else: - app = _ba.app - if not app.did_weak_call_warning: + app = _babase.app + if not self._did_invalid_call_warning: print( ( - 'Warning: callable passed to ba.WeakCall() is not' + 'Warning: callable passed to babase.WeakCall() is not' ' weak-referencable (' + str(args[0]) - + '); use ba.Call() instead to avoid this ' + + '); use babase.Call() instead to avoid this ' 'warning. Stack-trace:' ) ) import traceback traceback.print_stack() - app.did_weak_call_warning = True + self._did_invalid_call_warning = True self._call = args[0] self._args = args[1:] self._keywds = keywds @@ -224,7 +238,7 @@ class _Call: Note that a bound method (ex: ``myobj.dosomething``) contains a reference to ``self`` (``myobj`` in that case), so you will be keeping that object - alive too. Use ba.WeakCall if you want to pass a method to callback + alive too. Use babase.WeakCall if you want to pass a method to callback without keeping its object alive. """ @@ -236,7 +250,7 @@ class _Call: ##### Example Wrap a method call with 1 positional and 1 keyword arg: - >>> mycall = ba.Call(myobj.dostuff, argval, namedarg=argval2) + >>> mycall = babase.Call(myobj.dostuff, argval, namedarg=argval2) ... # Now we have a single callable to run that whole mess. ... # ..the same as calling myobj.dostuff(argval, namedarg=argval2) ... mycall() @@ -303,6 +317,7 @@ def verify_object_death(obj: object) -> None: This can be handy to detect and prevent memory/resource leaks. """ + try: ref = weakref.ref(obj) except Exception: @@ -311,10 +326,11 @@ def verify_object_death(obj: object) -> None: # Use a slight range for our checks so they don't all land at once # if we queue a lot of them. delay = random.uniform(2.0, 5.5) - with _ba.Context('ui'): - _ba.timer( - delay, lambda: _verify_object_death(ref), timetype=TimeType.REAL - ) + + # Make this timer in an empty context; don't want it dying with the + # scene/etc. + with _babase.ContextRef.empty(): + _babase.apptimer(delay, Call(_verify_object_death, ref)) def _verify_object_death(wref: weakref.ref) -> None: @@ -353,7 +369,7 @@ def storagename(suffix: str | None = None) -> str: >>> class MyThingie: ... # This will give something like ... # '_mymodule_submodule_mythingie_data'. - ... _STORENAME = ba.storagename('data') + ... _STORENAME = babase.storagename('data') ... ... # Use that name to store some data in the Activity we were ... # passed. diff --git a/src/assets/ba_data/python/babase/_hooks.py b/src/assets/ba_data/python/babase/_hooks.py new file mode 100644 index 00000000..b97d383e --- /dev/null +++ b/src/assets/ba_data/python/babase/_hooks.py @@ -0,0 +1,415 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Snippets of code for use by the internal layer. + +History: originally the engine would dynamically compile/eval various Python +code from within C++ code, but the major downside there was that none of it +was type-checked so if names or arguments changed it would go unnoticed +until it broke at runtime. By instead defining such snippets here and then +capturing references to them all at launch it is possible to allow linting +and type-checking magic to happen and most issues will be caught immediately. +""" +# (most of these are self-explanatory) +# pylint: disable=missing-function-docstring +from __future__ import annotations + +import time +import logging +from typing import TYPE_CHECKING + +import _babase + +if TYPE_CHECKING: + from typing import Sequence, Any + + import babase + + +def on_app_bootstrapping_complete() -> None: + """Called by C++ layer when bootstrapping finishes.""" + _babase.app.on_app_bootstrapping_complete() + + +def reset_to_main_menu() -> None: + # Some high-level event wants us to return to the main menu. + # an example of this is re-opening the game after we 'soft' quit it + # on Android. + if _babase.app.classic is not None: + _babase.app.classic.return_to_main_menu_session_gracefully() + else: + logging.warning('reset_to_main_menu: no-op due to classic not present.') + + +def set_config_fullscreen_on() -> None: + """The OS has changed our fullscreen state and we should take note.""" + _babase.app.config['Fullscreen'] = True + _babase.app.config.commit() + + +def set_config_fullscreen_off() -> None: + """The OS has changed our fullscreen state and we should take note.""" + _babase.app.config['Fullscreen'] = False + _babase.app.config.commit() + + +def not_signed_in_screen_message() -> None: + from babase._language import Lstr + + _babase.screenmessage(Lstr(resource='notSignedInErrorText')) + + +def open_url_with_webbrowser_module(url: str) -> None: + """Show a URL in the browser or print on-screen error if we can't.""" + import webbrowser + from babase._language import Lstr + + try: + webbrowser.open(url) + except Exception: + logging.exception("Error displaying url '%s'.", url) + _babase.getsimplesound('error').play() + _babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) + + +def connecting_to_party_message() -> None: + from babase._language import Lstr + + _babase.screenmessage( + Lstr(resource='internal.connectingToPartyText'), color=(1, 1, 1) + ) + + +def rejecting_invite_already_in_party_message() -> None: + from babase._language import Lstr + + _babase.screenmessage( + Lstr(resource='internal.rejectingInviteAlreadyInPartyText'), + color=(1, 0.5, 0), + ) + + +def connection_failed_message() -> None: + from babase._language import Lstr + + _babase.screenmessage( + Lstr(resource='internal.connectionFailedText'), color=(1, 0.5, 0) + ) + + +def temporarily_unavailable_message() -> None: + from babase._language import Lstr + + if not _babase.app.headless_mode: + _babase.getsimplesound('error').play() + _babase.screenmessage( + Lstr(resource='getTicketsWindow.unavailableTemporarilyText'), + color=(1, 0, 0), + ) + + +def in_progress_message() -> None: + from babase._language import Lstr + + if not _babase.app.headless_mode: + _babase.getsimplesound('error').play() + _babase.screenmessage( + Lstr(resource='getTicketsWindow.inProgressText'), + color=(1, 0, 0), + ) + + +def error_message() -> None: + from babase._language import Lstr + + if not _babase.app.headless_mode: + _babase.getsimplesound('error').play() + _babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) + + +def purchase_not_valid_error() -> None: + from babase._language import Lstr + + if not _babase.app.headless_mode: + _babase.getsimplesound('error').play() + _babase.screenmessage( + Lstr( + resource='store.purchaseNotValidError', + subs=[('${EMAIL}', 'support@froemling.net')], + ), + color=(1, 0, 0), + ) + + +def purchase_already_in_progress_error() -> None: + from babase._language import Lstr + + if not _babase.app.headless_mode: + _babase.getsimplesound('error').play() + _babase.screenmessage( + Lstr(resource='store.purchaseAlreadyInProgressText'), + color=(1, 0, 0), + ) + + +def gear_vr_controller_warning() -> None: + from babase._language import Lstr + + if not _babase.app.headless_mode: + _babase.getsimplesound('error').play() + _babase.screenmessage( + Lstr(resource='usesExternalControllerText'), color=(1, 0, 0) + ) + + +def uuid_str() -> str: + import uuid + + return str(uuid.uuid4()) + + +def orientation_reset_cb_message() -> None: + from babase._language import Lstr + + _babase.screenmessage( + Lstr(resource='internal.vrOrientationResetCardboardText'), + color=(0, 1, 0), + ) + + +def orientation_reset_message() -> None: + from babase._language import Lstr + + _babase.screenmessage( + Lstr(resource='internal.vrOrientationResetText'), color=(0, 1, 0) + ) + + +def on_app_pause() -> None: + _babase.app.pause() + + +def on_app_resume() -> None: + _babase.app.resume() + + +def show_post_purchase_message() -> None: + assert _babase.app.classic is not None + _babase.app.classic.accounts.show_post_purchase_message() + + +def language_test_toggle() -> None: + _babase.app.lang.setlanguage( + 'Gibberish' if _babase.app.lang.language == 'English' else 'English' + ) + + +def award_in_control_achievement() -> None: + if _babase.app.classic is not None: + _babase.app.classic.ach.award_local_achievement('In Control') + else: + logging.warning('award_in_control_achievement is no-op without classic') + + +def award_dual_wielding_achievement() -> None: + if _babase.app.classic is not None: + _babase.app.classic.ach.award_local_achievement('Dual Wielding') + else: + logging.warning( + 'award_dual_wielding_achievement is no-op without classic' + ) + + +def play_gong_sound() -> None: + if not _babase.app.headless_mode: + _babase.getsimplesound('gong').play() + + +def launch_coop_game(name: str) -> None: + assert _babase.app.classic is not None + _babase.app.classic.launch_coop_game(name) + + +def purchases_restored_message() -> None: + from babase._language import Lstr + + _babase.screenmessage( + Lstr(resource='getTicketsWindow.purchasesRestoredText'), color=(0, 1, 0) + ) + + +def dismiss_wii_remotes_window() -> None: + assert _babase.app.classic is not None + call = _babase.app.classic.ui.dismiss_wii_remotes_window_call + if call is not None: + call() + + +def unavailable_message() -> None: + from babase._language import Lstr + + _babase.screenmessage( + Lstr(resource='getTicketsWindow.unavailableText'), color=(1, 0, 0) + ) + + +def set_last_ad_network(sval: str) -> None: + if _babase.app.classic is not None: + _babase.app.classic.ads.last_ad_network = sval + _babase.app.classic.ads.last_ad_network_set_time = time.time() + + +def no_game_circle_message() -> None: + from babase._language import Lstr + + _babase.screenmessage(Lstr(resource='noGameCircleText'), color=(1, 0, 0)) + + +def google_play_purchases_not_available_message() -> None: + from babase._language import Lstr + + _babase.screenmessage( + Lstr(resource='googlePlayPurchasesNotAvailableText'), color=(1, 0, 0) + ) + + +def google_play_services_not_available_message() -> None: + from babase._language import Lstr + + _babase.screenmessage( + Lstr(resource='googlePlayServicesNotAvailableText'), color=(1, 0, 0) + ) + + +def empty_call() -> None: + pass + + +def print_trace() -> None: + import traceback + + print('Python Traceback (most recent call last):') + traceback.print_stack() + + +def toggle_fullscreen() -> None: + cfg = _babase.app.config + cfg['Fullscreen'] = not cfg.resolve('Fullscreen') + cfg.apply_and_commit() + + +def read_config() -> None: + _babase.app.read_config() + + +def ui_remote_press() -> None: + """Handle a press by a remote device that is only usable for nav.""" + from babase._language import Lstr + + if _babase.app.headless_mode: + return + + # Can be called without a context; need a context for getsound. + _babase.screenmessage( + Lstr(resource='internal.controllerForMenusOnlyText'), + color=(1, 0, 0), + ) + _babase.getsimplesound('error').play() + + +def remove_in_game_ads_message() -> None: + if _babase.app.classic is not None: + _babase.app.classic.ads.do_remove_in_game_ads_message() + + +def do_quit() -> None: + _babase.quit() + + +def shutdown() -> None: + _babase.app.on_app_shutdown() + + +def hash_strings(inputs: list[str]) -> str: + """Hash provided strings into a short output string.""" + import hashlib + + sha = hashlib.sha1() + for inp in inputs: + sha.update(inp.encode()) + + return sha.hexdigest() + + +def have_account_v2_credentials() -> bool: + """Do we have primary account-v2 credentials set?""" + return _babase.app.accounts.have_primary_credentials() + + +def implicit_sign_in( + login_type_str: str, login_id: str, display_name: str +) -> None: + """An implicit login happened.""" + from bacommon.login import LoginType + + _babase.app.accounts.on_implicit_sign_in( + login_type=LoginType(login_type_str), + login_id=login_id, + display_name=display_name, + ) + + +def implicit_sign_out(login_type_str: str) -> None: + """An implicit logout happened.""" + from bacommon.login import LoginType + + _babase.app.accounts.on_implicit_sign_out( + login_type=LoginType(login_type_str) + ) + + +def login_adapter_get_sign_in_token_response( + login_type_str: str, attempt_id_str: str, result_str: str +) -> None: + """Login adapter do-sign-in completed.""" + from bacommon.login import LoginType + from babase._login import LoginAdapterNative + + login_type = LoginType(login_type_str) + attempt_id = int(attempt_id_str) + result = None if result_str == '' else result_str + + adapter = _babase.app.accounts.login_adapters[login_type] + assert isinstance(adapter, LoginAdapterNative) + adapter.on_sign_in_complete(attempt_id=attempt_id, result=result) + + +def show_client_too_old_error() -> None: + """Called at launch if the server tells us we're too old to talk to it.""" + from babase._language import Lstr + + # If you are using an old build of the app and would like to stop + # seeing this error at launch, do: + # ba.app.config['SuppressClientTooOldErrorForBuild'] = ba.app.build_number + # ba.app.config.commit() + # Note that you will have to do that again later if you update to + # a newer build. + if ( + _babase.app.config.get('SuppressClientTooOldErrorForBuild') + == _babase.app.build_number + ): + return + + if not _babase.app.headless_mode: + _babase.getsimplesound('error').play() + + _babase.screenmessage( + Lstr( + translate=( + 'serverResponses', + 'Server functionality is no longer supported' + ' in this version of the game;\n' + 'Please update to a newer version.', + ) + ), + color=(1, 0, 0), + ) diff --git a/assets/src/ba_data/python/ba/_keyboard.py b/src/assets/ba_data/python/babase/_keyboard.py similarity index 91% rename from assets/src/ba_data/python/ba/_keyboard.py rename to src/assets/ba_data/python/babase/_keyboard.py index 36dfc183..6d18ac6b 100644 --- a/assets/src/ba_data/python/ba/_keyboard.py +++ b/src/assets/ba_data/python/babase/_keyboard.py @@ -17,7 +17,7 @@ class Keyboard: Keyboards are discoverable by the meta-tag system and the user can select which one they want to use. - On-screen keyboard uses chars from active ba.Keyboard. + On-screen keyboard uses chars from active babase.Keyboard. """ name: str diff --git a/assets/src/ba_data/python/ba/_language.py b/src/assets/ba_data/python/babase/_language.py similarity index 90% rename from assets/src/ba_data/python/ba/_language.py rename to src/assets/ba_data/python/babase/_language.py index 812f1ec9..a61acbe2 100644 --- a/assets/src/ba_data/python/ba/_language.py +++ b/src/assets/ba_data/python/babase/_language.py @@ -3,16 +3,18 @@ """Language related functionality.""" from __future__ import annotations -import json import os +import json +import logging from typing import TYPE_CHECKING, overload -import _ba +import _babase if TYPE_CHECKING: - import ba from typing import Any, Sequence + import babase + class LanguageSubsystem: """Wraps up language related app functionality. @@ -48,7 +50,7 @@ class LanguageSubsystem: 'Thai', 'Tamil', } - and not _ba.can_display_full_unicode() + and not _babase.can_display_full_unicode() ): return False return True @@ -58,10 +60,10 @@ class LanguageSubsystem: """Raw country/language code detected by the game (such as 'en_US'). Generally for language-specific code you should look at - ba.App.language, which is the language the game is using + babase.App.language, which is the language the game is using (which may differ from locale if the user sets a language, etc.) """ - env = _ba.env() + env = _babase.env() assert isinstance(env['locale'], str) return env['locale'] @@ -117,10 +119,10 @@ class LanguageSubsystem: """The name of the language the game is running in. This can be selected explicitly by the user or may be set - automatically based on ba.App.locale or other factors. + automatically based on babase.App.locale or other factors. """ - assert isinstance(_ba.app.config, dict) - return _ba.app.config.get('Lang', self.default_language) + assert isinstance(_babase.app.config, dict) + return _babase.app.config.get('Lang', self.default_language) @property def available_languages(self) -> list[str]: @@ -132,7 +134,11 @@ class LanguageSubsystem: """ langs = set() try: - names = os.listdir('ba_data/data/languages') + names = os.listdir( + os.path.join( + _babase.app.data_directory, 'ba_data', 'data', 'languages' + ) + ) names = [n.replace('.json', '').capitalize() for n in names] # FIXME: our simple capitalization fails on multi-word names; @@ -141,7 +147,7 @@ class LanguageSubsystem: if name == 'Chinesetraditional': names[i] = 'ChineseTraditional' except Exception: - from ba import _error + from babase import _error _error.print_exception() names = [] @@ -165,7 +171,7 @@ class LanguageSubsystem: # pylint: disable=too-many-locals # pylint: disable=too-many-statements # pylint: disable=too-many-branches - cfg = _ba.app.config + cfg = _babase.app.config cur_language = cfg.get('Lang', None) # Store this in the config if its changing. @@ -181,7 +187,14 @@ class LanguageSubsystem: switched = False with open( - 'ba_data/data/languages/english.json', encoding='utf-8' + os.path.join( + _babase.app.data_directory, + 'ba_data', + 'data', + 'languages', + 'english.json', + ), + encoding='utf-8', ) as infile: lenglishvalues = json.loads(infile.read()) @@ -192,16 +205,20 @@ class LanguageSubsystem: if language == 'English': lmodvalues = None else: - lmodfile = ( - 'ba_data/data/languages/' + language.lower() + '.json' + lmodfile = os.path.join( + _babase.app.data_directory, + 'ba_data', + 'data', + 'languages', + language.lower() + '.json', ) with open(lmodfile, encoding='utf-8') as infile: lmodvalues = json.loads(infile.read()) except Exception: - from ba import _error + from babase import _error _error.print_exception('Exception importing language:', language) - _ba.screenmessage( + _babase.screenmessage( "Error setting language to '" + language + "'; see log for details", @@ -254,9 +271,9 @@ class LanguageSubsystem: n.strip() for n in lmerged['randomPlayerNamesText'].split(',') ] random_names = [n for n in random_names if n != ''] - _ba.set_internal_language_keys(internal_vals, random_names) + _babase.set_internal_language_keys(internal_vals, random_names) if switched and print_change: - _ba.screenmessage( + _babase.screenmessage( Lstr( resource='languageSetText', subs=[ @@ -274,7 +291,7 @@ class LanguageSubsystem: ) -> Any: """Return a translation resource by name. - DEPRECATED; use ba.Lstr functionality for these purposes. + DEPRECATED; use babase.Lstr functionality for these purposes. """ try: # If we have no language set, go ahead and set it. @@ -285,11 +302,9 @@ class LanguageSubsystem: language, print_change=False, store_to_config=False ) except Exception: - from ba import _error + from babase import _error - _error.print_exception( - 'exception setting language to', language - ) + logging.exception('Error setting language to %s.', language) # Try english as a fallback. if language != 'English': @@ -359,7 +374,7 @@ class LanguageSubsystem: # Ok, looks like we couldn't find our main or fallback resource # anywhere. Now if we've been given a fallback value, return it; # otherwise fail. - from ba import _error + from babase import _error if fallback_value is not None: return fallback_value @@ -376,7 +391,7 @@ class LanguageSubsystem: ) -> str: """Translate a value (or return the value if no translation available) - DEPRECATED; use ba.Lstr functionality for these purposes. + DEPRECATED; use babase.Lstr functionality for these purposes. """ try: translated = self.get_resource('translations')[category][strval] @@ -426,26 +441,26 @@ class Lstr: ##### Examples EXAMPLE 1: specify a string from a resource path - >>> mynode.text = ba.Lstr(resource='audioSettingsWindow.titleText') + >>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText') EXAMPLE 2: specify a translated string via a category and english value; if a translated value is available, it will be used; otherwise the english value will be. To see available translation categories, look under the 'translations' resource section. - >>> mynode.text = ba.Lstr(translate=('gameDescriptions', + >>> mynode.text = babase.Lstr(translate=('gameDescriptions', ... 'Defeat all enemies')) EXAMPLE 3: specify a raw value and some substitutions. Substitutions can be used with resource and translate modes as well. - >>> mynode.text = ba.Lstr(value='${A} / ${B}', + >>> mynode.text = babase.Lstr(value='${A} / ${B}', ... subs=[('${A}', str(score)), ('${B}', str(total))]) - EXAMPLE 4: ba.Lstr's can be nested. This example would display the + EXAMPLE 4: babase.Lstr's can be nested. This example would display the resource at res_a but replace ${NAME} with the value of the resource at res_b - >>> mytextnode.text = ba.Lstr( + >>> mytextnode.text = babase.Lstr( ... resource='res_a', - ... subs=[('${NAME}', ba.Lstr(resource='res_b'))]) + ... subs=[('${NAME}', babase.Lstr(resource='res_b'))]) """ # pylint: disable=dangerous-default-value @@ -528,7 +543,7 @@ class Lstr: keywds['v'] = keywds['value'] del keywds['value'] if 'fallback' in keywds: - from ba import _error + from babase import _error _error.print_error( 'deprecated "fallback" arg passed to Lstr(); use ' @@ -553,7 +568,7 @@ class Lstr: You should avoid doing this as much as possible and instead pass and store Lstr values. """ - return _ba.evaluate_lstr(self._get_json()) + return _babase.evaluate_lstr(self._get_json()) def is_flat_value(self) -> bool: """Return whether the Lstr is a 'flat' value. @@ -569,7 +584,7 @@ class Lstr: try: return json.dumps(self.args, separators=(',', ':')) except Exception: - from ba import _error + from babase import _error _error.print_exception('_get_json failed for', self.args) return 'JSON_ERR' @@ -581,8 +596,8 @@ class Lstr: return '' @staticmethod - def from_json(json_string: str) -> ba.Lstr: - """Given a json string, returns a ba.Lstr. Does no data validation.""" + def from_json(json_string: str) -> babase.Lstr: + """Given a json string, returns a babase.Lstr. Does no validation.""" lstr = Lstr(value='') lstr.args = json.loads(json_string) return lstr @@ -625,4 +640,4 @@ class AttrDict(dict): return val def __setattr__(self, attr: str, value: Any) -> None: - raise Exception() + raise AttributeError() diff --git a/assets/src/ba_data/python/ba/_login.py b/src/assets/ba_data/python/babase/_login.py similarity index 90% rename from assets/src/ba_data/python/ba/_login.py rename to src/assets/ba_data/python/babase/_login.py index 49541bd5..995d05cc 100644 --- a/assets/src/ba_data/python/ba/_login.py +++ b/src/assets/ba_data/python/babase/_login.py @@ -10,7 +10,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, final from bacommon.login import LoginType -import _ba +import _babase if TYPE_CHECKING: from typing import Callable @@ -45,12 +45,12 @@ class LoginAdapter: display_name: str def __init__(self, login_type: LoginType): - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() self.login_type = login_type self._implicit_login_state: LoginAdapter.ImplicitLoginState | None = ( None ) - self._on_app_launch_called = False + self._on_app_launching_called = False self._implicit_login_state_dirty = False self._back_end_active = False @@ -61,11 +61,11 @@ class LoginAdapter: self._last_sign_in_time: float | None = None self._last_sign_in_desc: str | None = None - def on_app_launch(self) -> None: - """Should be called for each adapter in on_app_launch.""" + def on_app_launching(self) -> None: + """Should be called for each adapter in on_app_launching.""" - assert not self._on_app_launch_called - self._on_app_launch_called = True + assert not self._on_app_launching_called + self._on_app_launching_called = True # Any implicit state we received up until now needs to be pushed # to the app account subsystem. @@ -79,7 +79,7 @@ class LoginAdapter: This should be called by the adapter back-end when an account of their associated type gets logged in or out. """ - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() # Ignore redundant sets. if state == self._implicit_login_state: @@ -118,7 +118,7 @@ class LoginAdapter: Note that the logins dict passed in should be immutable as only a reference to it is stored, not a copy. """ - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() if DEBUG_LOG: logging.debug( 'LoginAdapter: %s adapter got active logins %s.', @@ -139,7 +139,7 @@ class LoginAdapter: UIs, etc. When not active it should ignore everything and behave as if logged out, even if it technically is still logged in. """ - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() del active # Unused. @final @@ -154,30 +154,29 @@ class LoginAdapter: the adapter will attempt to sign in if possible. An exception will be returned if the sign-in attempt fails. """ - assert _ba.in_logic_thread() - from ba._general import Call - from ba._generated.enums import TimeType + assert _babase.in_logic_thread() + from babase._general import Call # Have been seeing multiple sign-in attempts come through # nearly simultaneously which can be problematic server-side. # Let's error if a sign-in attempt is made within a few seconds # of the last one to address this. now = time.monotonic() - appnow = _ba.time(TimeType.REAL) + appnow = _babase.apptime() if self._last_sign_in_time is not None: since_last = now - self._last_sign_in_time if since_last < 1.0: logging.warning( 'LoginAdapter: %s adapter sign_in() called too soon' ' (%.2fs) after last; this-desc="%s", last-desc="%s",' - ' ba-real-time=%.2f.', + ' ba-app-time=%.2f.', self.login_type.name, since_last, description, self._last_sign_in_desc, appnow, ) - _ba.pushcall( + _babase.pushcall( Call( result_cb, self, @@ -207,7 +206,7 @@ class LoginAdapter: ' aborting sign-in.', self.login_type.name, ) - _ba.pushcall( + _babase.pushcall( Call( result_cb, self, @@ -229,7 +228,6 @@ class LoginAdapter: def _got_sign_in_response( response: bacommon.cloud.SignInResponse | Exception, ) -> None: - if isinstance(response, Exception): if DEBUG_LOG: logging.debug( @@ -238,7 +236,7 @@ class LoginAdapter: self.login_type.name, response, ) - _ba.pushcall(Call(result_cb, self, response)) + _babase.pushcall(Call(result_cb, self, response)) else: if DEBUG_LOG: logging.debug( @@ -257,9 +255,9 @@ class LoginAdapter: result2 = self.SignInResult( credentials=response.credentials ) - _ba.pushcall(Call(result_cb, self, result2)) + _babase.pushcall(Call(result_cb, self, result2)) - _ba.app.cloud.send_message_cb( + _babase.app.cloud.send_message_cb( bacommon.cloud.SignInMessage( self.login_type, result, @@ -288,18 +286,18 @@ class LoginAdapter: The provided completion_cb should then be called with either a token or None if sign in failed or was cancelled. """ - from ba._general import Call + from babase._general import Call # Default implementation simply fails immediately. - _ba.pushcall(Call(completion_cb, None)) + _babase.pushcall(Call(completion_cb, None)) def _update_implicit_login_state(self) -> None: # If we've received an implicit login state, schedule it to be # sent along to the app. We wait until on-app-launch has been # called so that account-client-v2 has had a chance to load # any existing state so it can properly respond to this. - if self._implicit_login_state_dirty and self._on_app_launch_called: - from ba._general import Call + if self._implicit_login_state_dirty and self._on_app_launching_called: + from babase._general import Call if DEBUG_LOG: logging.debug( @@ -308,9 +306,9 @@ class LoginAdapter: self.login_type.name, ) - _ba.pushcall( + _babase.pushcall( Call( - _ba.app.accounts_v2.on_implicit_login_state_changed, + _babase.app.accounts.on_implicit_login_state_changed, self.login_type, self._implicit_login_state, ) @@ -353,14 +351,18 @@ class LoginAdapterNative(LoginAdapter): attempt_id = self._sign_in_attempt_num self._sign_in_attempts[attempt_id] = completion_cb self._sign_in_attempt_num += 1 - _ba.login_adapter_get_sign_in_token(self.login_type.value, attempt_id) + _babase.login_adapter_get_sign_in_token( + self.login_type.value, attempt_id + ) def on_back_end_active_change(self, active: bool) -> None: - _ba.login_adapter_back_end_active_change(self.login_type.value, active) + _babase.login_adapter_back_end_active_change( + self.login_type.value, active + ) def on_sign_in_complete(self, attempt_id: int, result: str | None) -> None: """Called by the native layer on a completed attempt.""" - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() if attempt_id not in self._sign_in_attempts: logging.exception('sign-in attempt_id %d not found', attempt_id) return diff --git a/assets/src/ba_data/python/ba/_math.py b/src/assets/ba_data/python/babase/_math.py similarity index 100% rename from assets/src/ba_data/python/ba/_math.py rename to src/assets/ba_data/python/babase/_math.py diff --git a/assets/src/ba_data/python/ba/_meta.py b/src/assets/ba_data/python/babase/_meta.py similarity index 94% rename from assets/src/ba_data/python/ba/_meta.py rename to src/assets/ba_data/python/babase/_meta.py index 5f05d4c3..85ee7b59 100644 --- a/assets/src/ba_data/python/ba/_meta.py +++ b/src/assets/ba_data/python/babase/_meta.py @@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, TypeVar from dataclasses import dataclass, field from efro.call import tpartial -import _ba +import _babase if TYPE_CHECKING: from typing import Callable @@ -22,7 +22,7 @@ if TYPE_CHECKING: # Only packages and modules requiring this exact api version # will be considered when scanning directories. # See: https://ballistica.net/wiki/Meta-Tag-System -CURRENT_API_VERSION = 7 +CURRENT_API_VERSION = 8 # Meta export lines can use these names to represent these classes. # This is purely a convenience; it is possible to use full class paths @@ -58,7 +58,6 @@ class MetadataSubsystem: """ def __init__(self) -> None: - self._scan: DirectoryScan | None = None # Can be populated before starting the scan. @@ -81,7 +80,14 @@ class MetadataSubsystem: self._scan_complete_cb = scan_complete_cb self._scan = DirectoryScan( - [_ba.app.python_directory_app, _ba.app.python_directory_user] + [ + path + for path in [ + _babase.app.python_directory_app, + _babase.app.python_directory_user, + ] + if path is not None + ] ) Thread(target=self._run_scan_in_bg, daemon=True).start() @@ -128,7 +134,7 @@ class MetadataSubsystem: completion_cb: Callable[[list[type[T]]], None], completion_cb_in_bg_thread: bool, ) -> None: - from ba._general import getclass + from babase._general import getclass classes: list[type[T]] = [] try: @@ -146,12 +152,12 @@ class MetadataSubsystem: if completion_cb_in_bg_thread: completion_call() else: - _ba.pushcall(completion_call, from_other_thread=True) + _babase.pushcall(completion_call, from_other_thread=True) def _wait_for_scan_results(self) -> ScanResults: """Return scan results, blocking if the scan is not yet complete.""" if self.scanresults is None: - if _ba.in_logic_thread(): + if _babase.in_logic_thread(): logging.warning( 'ba.meta._wait_for_scan_results()' ' called in logic thread before scan completed;' @@ -181,13 +187,13 @@ class MetadataSubsystem: # Place results and tell the logic thread they're ready. self.scanresults = results - _ba.pushcall(self._handle_scan_results, from_other_thread=True) + _babase.pushcall(self._handle_scan_results, from_other_thread=True) def _handle_scan_results(self) -> None: """Called in the logic thread with results of a completed scan.""" - from ba._language import Lstr + from babase._language import Lstr - assert _ba.in_logic_thread() + assert _babase.in_logic_thread() results = self.scanresults assert results is not None @@ -199,10 +205,11 @@ class MetadataSubsystem: if results.warnings or results.errors: import textwrap - _ba.screenmessage( + _babase.screenmessage( Lstr(resource='scanScriptsErrorText'), color=(1, 0, 0) ) - _ba.playsound(_ba.getsound('error')) + _babase.getsimplesound('error').play() + if results.warnings: allwarnings = textwrap.indent( '\n'.join(results.warnings), 'Warning (meta-scan): ' @@ -244,7 +251,6 @@ class DirectoryScan: def run(self) -> None: """Do the thing.""" for pathlist in [self.base_paths, self.extra_paths]: - # Spin and wait until extra paths are provided before doing them. if pathlist is self.extra_paths: while not self.extra_paths_set: @@ -272,13 +278,14 @@ class DirectoryScan: ) -> None: """Scan provided path and add module entries to provided list.""" try: - # Special case: let's save some time and skip the whole 'ba' + # Special case: let's save some time and skip the whole 'babase' # package since we know it doesn't contain any meta tags. fullpath = Path(path, subpath) entries = [ (path, Path(subpath, name)) for name in os.listdir(fullpath) - if name != 'ba' + # Actually scratch that for now; trying to avoid special cases. + # if name != 'babase' ] except PermissionError: # Expected sometimes. @@ -326,8 +333,8 @@ class DirectoryScan: # and ignore. if required_api is not None and required_api != CURRENT_API_VERSION: self.results.warnings.append( - f'Warning: {subpath} requires api {required_api} but' - f' we are running {CURRENT_API_VERSION}; ignoring module.' + f'{subpath} requires api {required_api} but' + f' we are running {CURRENT_API_VERSION}. Ignoring module.' ) return diff --git a/src/assets/ba_data/python/babase/_net.py b/src/assets/ba_data/python/babase/_net.py new file mode 100644 index 00000000..64774d52 --- /dev/null +++ b/src/assets/ba_data/python/babase/_net.py @@ -0,0 +1,76 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Networking related functionality.""" +from __future__ import annotations + +import ssl +import threading +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable + import socket + +# Timeout for standard functions talking to the master-server/etc. +DEFAULT_REQUEST_TIMEOUT_SECONDS = 60 + + +class NetworkSubsystem: + """Network related app subsystem.""" + + def __init__(self) -> None: + # Anyone accessing/modifying zone_pings should hold this lock, + # as it is updated by a background thread. + self.zone_pings_lock = threading.Lock() + + # Zone IDs mapped to average pings. This will remain empty + # until enough pings have been run to be reasonably certain + # that a nearby server has been pinged. + self.zone_pings: dict[str, float] = {} + + self._sslcontext: ssl.SSLContext | None = None + + # For debugging. + self.v1_test_log: str = '' + self.v1_ctest_results: dict[int, str] = {} + self.server_time_offset_hours: float | None = None + + @property + def sslcontext(self) -> ssl.SSLContext: + """Create/return our shared SSLContext. + + This can be reused for all standard urllib requests/etc. + """ + # Note: I've run into older Android devices taking upwards of 1 second + # to put together a default SSLContext, so recycling one can definitely + # be a worthwhile optimization. This was suggested to me in this + # thread by one of Python's SSL maintainers: + # https://github.com/python/cpython/issues/94637 + if self._sslcontext is None: + self._sslcontext = ssl.create_default_context() + return self._sslcontext + + +def get_ip_address_type(addr: str) -> socket.AddressFamily: + """Return socket.AF_INET6 or socket.AF_INET4 for the provided address.""" + import socket + + socket_type = None + + # First try it as an ipv4 address. + try: + socket.inet_pton(socket.AF_INET, addr) + socket_type = socket.AF_INET + except OSError: + pass + + # Hmm apparently not ipv4; try ipv6. + if socket_type is None: + try: + socket.inet_pton(socket.AF_INET6, addr) + socket_type = socket.AF_INET6 + except OSError: + pass + if socket_type is None: + raise ValueError(f'addr seems to be neither v4 or v6: {addr}') + return socket_type diff --git a/assets/src/ba_data/python/ba/_plugin.py b/src/assets/ba_data/python/babase/_plugin.py similarity index 78% rename from assets/src/ba_data/python/ba/_plugin.py rename to src/assets/ba_data/python/babase/_plugin.py index 5a89dcf9..121d9922 100644 --- a/assets/src/ba_data/python/ba/_plugin.py +++ b/src/assets/ba_data/python/babase/_plugin.py @@ -8,10 +8,12 @@ import logging from typing import TYPE_CHECKING from dataclasses import dataclass -import _ba +import _babase if TYPE_CHECKING: - import ba + from typing import Any + + import babase class PluginSubsystem: @@ -26,20 +28,22 @@ class PluginSubsystem: AUTO_ENABLE_NEW_PLUGINS_DEFAULT = True def __init__(self) -> None: - self.potential_plugins: list[ba.PotentialPlugin] = [] - self.active_plugins: dict[str, ba.Plugin] = {} + self.potential_plugins: list[babase.PotentialPlugin] = [] + self.active_plugins: dict[str, babase.Plugin] = {} def on_meta_scan_complete(self) -> None: """Should be called when meta-scanning is complete.""" - from ba._language import Lstr + from babase._language import Lstr - plugs = _ba.app.plugins + plugs = _babase.app.plugins config_changed = False found_new = False - plugstates: dict[str, dict] = _ba.app.config.setdefault('Plugins', {}) + plugstates: dict[str, dict] = _babase.app.config.setdefault( + 'Plugins', {} + ) assert isinstance(plugstates, dict) - results = _ba.app.meta.scanresults + results = _babase.app.meta.scanresults assert results is not None # Create a potential-plugin for each class we found in the scan. @@ -52,7 +56,7 @@ class PluginSubsystem: ) ) if ( - _ba.app.config.get( + _babase.app.config.get( self.AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY, self.AUTO_ENABLE_NEW_PLUGINS_DEFAULT, ) @@ -71,14 +75,16 @@ class PluginSubsystem: # Note: these days we complete meta-scan and immediately activate # plugins, so we don't need the message about 'restart to activate' # anymore. + # FIXME: We actually now have an option to NOT activate new plugins + # so we should selectively re-enable this. if found_new and bool(False): - _ba.screenmessage( + _babase.screenmessage( Lstr(resource='pluginsDetectedText'), color=(0, 1, 0) ) - _ba.playsound(_ba.getsound('ding')) + _babase.getsimplesound('ding').play() if config_changed: - _ba.app.config.commit() + _babase.app.config.commit() def on_app_running(self) -> None: """Should be called when the app reaches the running state.""" @@ -88,7 +94,7 @@ class PluginSubsystem: try: plugin.on_app_running() except Exception: - from ba import _error + from babase import _error _error.print_exception('Error in plugin on_app_running()') @@ -98,7 +104,7 @@ class PluginSubsystem: try: plugin.on_app_pause() except Exception: - from ba import _error + from babase import _error _error.print_exception('Error in plugin on_app_pause()') @@ -108,7 +114,7 @@ class PluginSubsystem: try: plugin.on_app_resume() except Exception: - from ba import _error + from babase import _error _error.print_exception('Error in plugin on_app_resume()') @@ -118,18 +124,18 @@ class PluginSubsystem: try: plugin.on_app_shutdown() except Exception: - from ba import _error + from babase import _error _error.print_exception('Error in plugin on_app_shutdown()') def load_plugins(self) -> None: """(internal)""" - from ba._general import getclass - from ba._language import Lstr + from babase._general import getclass + from babase._language import Lstr # Note: the plugins we load is purely based on what's enabled # in the app config. Its not our job to look at meta stuff here. - plugstates: dict[str, dict] = _ba.app.config.get('Plugins', {}) + plugstates: dict[str, dict] = _babase.app.config.get('Plugins', {}) assert isinstance(plugstates, dict) plugkeys: list[str] = sorted( key for key, val in plugstates.items() if val.get('enabled', False) @@ -142,11 +148,14 @@ class PluginSubsystem: disappeared_plugs.add(plugkey) continue except Exception as exc: - _ba.playsound(_ba.getsound('error')) - _ba.screenmessage( + _babase.getsimplesound('error').play() + _babase.screenmessage( Lstr( resource='pluginClassLoadErrorText', - subs=[('${PLUGIN}', plugkey), ('${ERROR}', str(exc))], + subs=[ + ('${PLUGIN}', plugkey), + ('${ERROR}', str(exc)), + ], ), color=(1, 0, 0), ) @@ -157,13 +166,16 @@ class PluginSubsystem: assert plugkey not in self.active_plugins self.active_plugins[plugkey] = plugin except Exception as exc: - from ba import _error + from babase import _error - _ba.playsound(_ba.getsound('error')) - _ba.screenmessage( + _babase.getsimplesound('error').play() + _babase.screenmessage( Lstr( resource='pluginInitErrorText', - subs=[('${PLUGIN}', plugkey), ('${ERROR}', str(exc))], + subs=[ + ('${PLUGIN}', plugkey), + ('${ERROR}', str(exc)), + ], ), color=(1, 0, 0), ) @@ -174,14 +186,15 @@ class PluginSubsystem: # reappear. This makes it much smoother to switch between users # or workspaces. if disappeared_plugs: - _ba.playsound(_ba.getsound('shieldDown')) - _ba.screenmessage( + _babase.getsimplesound('shieldDown').play() + _babase.screenmessage( Lstr( resource='pluginsRemovedText', subs=[('${NUM}', str(len(disappeared_plugs)))], ), color=(1, 1, 0), ) + plugnames = ', '.join(disappeared_plugs) logging.info( '%d plugin(s) no longer found: %s.', @@ -189,13 +202,13 @@ class PluginSubsystem: plugnames, ) for goneplug in disappeared_plugs: - del _ba.app.config['Plugins'][goneplug] - _ba.app.config.commit() + del _babase.app.config['Plugins'][goneplug] + _babase.app.config.commit() @dataclass class PotentialPlugin: - """Represents a ba.Plugin which can potentially be loaded. + """Represents a babase.Plugin which can potentially be loaded. Category: **App Classes** @@ -205,7 +218,7 @@ class PotentialPlugin: for some reason. In that case, 'available' will be set to False. """ - display_name: ba.Lstr + display_name: babase.Lstr class_path: str available: bool @@ -237,5 +250,5 @@ class Plugin: """Called to ask if we have settings UI we can show.""" return False - def show_settings_ui(self, source_widget: ba.Widget | None) -> None: + def show_settings_ui(self, source_widget: Any | None) -> None: """Called to show our settings UI.""" diff --git a/src/assets/ba_data/python/babase/_text.py b/src/assets/ba_data/python/babase/_text.py new file mode 100644 index 00000000..24642c7c --- /dev/null +++ b/src/assets/ba_data/python/babase/_text.py @@ -0,0 +1,90 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Text related functionality.""" + +from __future__ import annotations + + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import babase + + +def timestring( + timeval: float | int, + centi: bool = True, +) -> babase.Lstr: + """Generate a babase.Lstr for displaying a time value. + + Category: **General Utility Functions** + + Given a time value, returns a babase.Lstr with: + (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True). + + WARNING: the underlying Lstr value is somewhat large so don't use this + to rapidly update Node text values for an onscreen timer or you may + consume significant network bandwidth. For that purpose you should + use a 'timedisplay' Node and attribute connections. + + """ + from babase._language import Lstr + + # We take float seconds but operate on int milliseconds internally. + timeval = int(1000 * timeval) + bits = [] + subs = [] + hval = (timeval // 1000) // (60 * 60) + if hval != 0: + bits.append('${H}') + subs.append( + ( + '${H}', + Lstr( + resource='timeSuffixHoursText', + subs=[('${COUNT}', str(hval))], + ), + ) + ) + mval = ((timeval // 1000) // 60) % 60 + if mval != 0: + bits.append('${M}') + subs.append( + ( + '${M}', + Lstr( + resource='timeSuffixMinutesText', + subs=[('${COUNT}', str(mval))], + ), + ) + ) + + # We add seconds if its non-zero *or* we haven't added anything else. + if centi: + # pylint: disable=consider-using-f-string + sval = timeval / 1000.0 % 60.0 + if sval >= 0.005 or not bits: + bits.append('${S}') + subs.append( + ( + '${S}', + Lstr( + resource='timeSuffixSecondsText', + subs=[('${COUNT}', ('%.2f' % sval))], + ), + ) + ) + else: + sval = timeval // 1000 % 60 + if sval != 0 or not bits: + bits.append('${S}') + subs.append( + ( + '${S}', + Lstr( + resource='timeSuffixSecondsText', + subs=[('${COUNT}', str(sval))], + ), + ) + ) + return Lstr(value=' '.join(bits), subs=subs) diff --git a/assets/src/ba_data/python/ba/_workspace.py b/src/assets/ba_data/python/babase/_workspace.py similarity index 87% rename from assets/src/ba_data/python/ba/_workspace.py rename to src/assets/ba_data/python/babase/_workspace.py index c0997396..b6c06d61 100644 --- a/assets/src/ba_data/python/ba/_workspace.py +++ b/src/assets/ba_data/python/babase/_workspace.py @@ -13,14 +13,14 @@ from typing import TYPE_CHECKING from efro.call import tpartial from efro.error import CleanError -import _ba +import _babase import bacommon.cloud from bacommon.transfer import DirectoryManifest if TYPE_CHECKING: from typing import Callable - import ba + import babase class WorkspaceSubsystem: @@ -36,7 +36,7 @@ class WorkspaceSubsystem: def set_active_workspace( self, - account: ba.AccountV2Handle, + account: babase.AccountV2Handle, workspaceid: str, workspacename: str, on_completed: Callable[[], None], @@ -55,36 +55,35 @@ class WorkspaceSubsystem: daemon=True, ).start() - def _errmsg(self, msg: ba.Lstr) -> None: - _ba.screenmessage(msg, color=(1, 0, 0)) - _ba.playsound(_ba.getsound('error')) + def _errmsg(self, msg: babase.Lstr) -> None: + _babase.screenmessage(msg, color=(1, 0, 0)) + _babase.getsimplesound('error').play() - def _successmsg(self, msg: ba.Lstr) -> None: - _ba.screenmessage(msg, color=(0, 1, 0)) - _ba.playsound(_ba.getsound('gunCocking')) + def _successmsg(self, msg: babase.Lstr) -> None: + _babase.screenmessage(msg, color=(0, 1, 0)) + _babase.getsimplesound('gunCocking').play() def _set_active_workspace_bg( self, - account: ba.AccountV2Handle, + account: babase.AccountV2Handle, workspaceid: str, workspacename: str, on_completed: Callable[[], None], ) -> None: - from ba._language import Lstr + from babase._language import Lstr class _SkipSyncError(RuntimeError): pass set_path = True wspath = Path( - _ba.get_volatile_data_directory(), 'workspaces', workspaceid + _babase.get_volatile_data_directory(), 'workspaces', workspaceid ) try: - # If it seems we're offline, don't even attempt a sync, # but allow using the previous synced state. # (is this a good idea?) - if not _ba.app.cloud.is_connected(): + if not _babase.app.cloud.is_connected(): raise _SkipSyncError() manifest = DirectoryManifest.create_from_disk(wspath) @@ -95,7 +94,7 @@ class WorkspaceSubsystem: while True: with account: - response = _ba.app.cloud.send_message( + response = _babase.app.cloud.send_message( bacommon.cloud.WorkspaceFetchMessage( workspaceid=workspaceid, state=state ) @@ -115,7 +114,7 @@ class WorkspaceSubsystem: break state.iteration += 1 - _ba.pushcall( + _babase.pushcall( tpartial( self._successmsg, Lstr( @@ -127,7 +126,7 @@ class WorkspaceSubsystem: ) except _SkipSyncError: - _ba.pushcall( + _babase.pushcall( tpartial( self._errmsg, Lstr( @@ -142,7 +141,7 @@ class WorkspaceSubsystem: # Avoid reusing existing if we fail in the middle; could # be in wonky state. set_path = False - _ba.pushcall( + _babase.pushcall( tpartial(self._errmsg, Lstr(value=str(exc))), from_other_thread=True, ) @@ -150,7 +149,7 @@ class WorkspaceSubsystem: # Ditto. set_path = False logging.exception("Error syncing workspace '%s'.", workspacename) - _ba.pushcall( + _babase.pushcall( tpartial( self._errmsg, Lstr( @@ -165,10 +164,10 @@ class WorkspaceSubsystem: # Add to Python paths and also to list of stuff to be scanned # for meta tags. sys.path.insert(0, str(wspath)) - _ba.app.meta.extra_scan_dirs.append(str(wspath)) + _babase.app.meta.extra_scan_dirs.append(str(wspath)) # Job's done! - _ba.pushcall(on_completed, from_other_thread=True) + _babase.pushcall(on_completed, from_other_thread=True) def _handle_deletes(self, workspace_dir: Path, deletes: list[str]) -> None: """Handle file deletes.""" diff --git a/src/assets/ba_data/python/babase/internal.py b/src/assets/ba_data/python/babase/internal.py new file mode 100644 index 00000000..0a964dc3 --- /dev/null +++ b/src/assets/ba_data/python/babase/internal.py @@ -0,0 +1,80 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Exposed functionality not intended for full public use. + +Classes and functions contained here, while technically 'public', may change +or disappear without warning, so should be avoided (or used sparingly and +defensively). +""" +from __future__ import annotations + +from _babase import ( + add_clean_frame_callback, + increment_analytics_count, + get_string_height, + get_string_width, + appnameupper, + appname, + workspaces_in_use, + charstr, + have_permission, + request_permission, + is_xcode_build, + set_low_level_config_value, + get_low_level_config_value, + has_gamma_control, + get_max_graphics_quality, + get_display_resolution, + is_running_on_fire_tv, + android_get_external_files_dir, + get_replays_dir, +) + +from babase._login import LoginAdapter +from babase._appconfig import commit_app_config +from babase._general import getclass, json_prep, get_type_name +from babase._apputils import ( + is_browser_likely_available, + get_remote_app_name, + should_submit_debug_info, + dump_app_state, + log_dumped_app_state, +) +from babase._net import ( + get_ip_address_type, + DEFAULT_REQUEST_TIMEOUT_SECONDS, +) + +__all__ = [ + 'LoginAdapter', + 'add_clean_frame_callback', + 'increment_analytics_count', + 'get_string_height', + 'get_string_width', + 'appnameupper', + 'appname', + 'workspaces_in_use', + 'charstr', + 'have_permission', + 'request_permission', + 'is_xcode_build', + 'set_low_level_config_value', + 'get_low_level_config_value', + 'has_gamma_control', + 'get_max_graphics_quality', + 'get_display_resolution', + 'is_running_on_fire_tv', + 'android_get_external_files_dir', + 'get_replays_dir', + 'commit_app_config', + 'getclass', + 'json_prep', + 'get_type_name', + 'is_browser_likely_available', + 'get_remote_app_name', + 'should_submit_debug_info', + 'get_ip_address_type', + 'DEFAULT_REQUEST_TIMEOUT_SECONDS', + 'dump_app_state', + 'log_dumped_app_state', +] diff --git a/src/assets/ba_data/python/baclassic/__init__.py b/src/assets/ba_data/python/baclassic/__init__.py new file mode 100644 index 00000000..037c3840 --- /dev/null +++ b/src/assets/ba_data/python/baclassic/__init__.py @@ -0,0 +1,55 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Classic ballistica components. + +This stuff is mostly used in the classic app-mode, old UIs, etc. +The app should be able to function cleanly without this package present +(just lacking classic mode functionality). + +New code should try to avoid using code here if it wants to be usable +with newer more modern app-modes/etc. + +Functionality in this package should be exposed through the ClassicSubsystem +class instance whenever possible. This will allow type-checked code to +go through babase.app.classic which will force it to properly handle the case +where babase.app.classic is None. When code instead imports classic submodules +directly, it will most likely not work without classic present. +""" + +# ba_meta require api 8 + +# Note: Code relying on classic should import things from here *only* +# for type-checking and use the versions in app.classic at runtime; that +# way type-checking will cleanly cover the classic-not-present case +# (app.classic being None). +import logging + +from baclassic._subsystem import ClassicSubsystem +from baclassic._campaign import Campaign +from baclassic._level import Level +from baclassic._lobby import Lobby, Chooser +from baclassic._achievement import Achievement, AchievementSubsystem + +__all__ = [ + 'ClassicSubsystem', + 'Campaign', + 'Level', + 'Lobby', + 'Chooser', + 'Achievement', + 'AchievementSubsystem', +] + +# Sanity check: we want to keep ballistica's dependencies and +# bootstrapping order clearly defined; let's check a few particular +# modules to make sure they never directly or indirectly import us +# before their own execs complete. +if __debug__: + for _mdl in 'babase', '_babase': + if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'): + logging.warning( + '%s was imported before %s finished importing;' + ' should not happen.', + __name__, + _mdl, + ) diff --git a/assets/src/ba_data/python/ba/_accountv1.py b/src/assets/ba_data/python/baclassic/_accountv1.py similarity index 68% rename from assets/src/ba_data/python/ba/_accountv1.py rename to src/assets/ba_data/python/baclassic/_accountv1.py index 7f946097..ae28d30e 100644 --- a/assets/src/ba_data/python/ba/_accountv1.py +++ b/src/assets/ba_data/python/baclassic/_accountv1.py @@ -8,8 +8,8 @@ import copy import time from typing import TYPE_CHECKING -import _ba -from ba import _internal +import _babase +import bauiv1 as bui if TYPE_CHECKING: from typing import Any @@ -20,7 +20,7 @@ class AccountV1Subsystem: Category: **App Classes** - Access the single shared instance of this class at 'ba.app.accounts_v1'. + Access the single instance of this class at 'ba.app.classic.accounts'. """ def __init__(self) -> None: @@ -35,18 +35,20 @@ class AccountV1Subsystem: # not be signed in yet; go ahead and queue them up in that case. self.pending_promo_codes: list[str] = [] - def on_app_launch(self) -> None: + def on_app_launching(self) -> None: """Called when the app is done bootstrapping.""" # Auto-sign-in to a local account in a moment if we're set to. def do_auto_sign_in() -> None: + if _babase.app.plus is None: + return if ( - _ba.app.headless_mode - or _ba.app.config.get('Auto Account State') == 'Local' + _babase.app.headless_mode + or _babase.app.config.get('Auto Account State') == 'Local' ): - _internal.sign_in_v1('Local') + _babase.app.plus.sign_in_v1('Local') - _ba.pushcall(do_auto_sign_in) + _babase.pushcall(do_auto_sign_in) def on_app_pause(self) -> None: """Should be called when app is pausing.""" @@ -64,16 +66,16 @@ class AccountV1Subsystem: (internal) """ - from ba._language import Lstr + from babase._language import Lstr - _ba.screenmessage( + _babase.screenmessage( Lstr( resource='getTicketsWindow.receivedTicketsText', subs=[('${COUNT}', str(count))], ), color=(0, 1, 0), ) - _ba.playsound(_ba.getsound('cashRegister')) + _babase.getsimplesound('cashRegister').play() def cache_league_rank_data(self, data: Any) -> None: """(internal)""" @@ -96,7 +98,8 @@ class AccountV1Subsystem: total_ach_value = data['at'] else: total_ach_value = 0 - for ach in _ba.app.ach.achievements: + assert _babase.app.classic is not None + for ach in _babase.app.classic.ach.achievements: if ach.complete: total_ach_value += ach.power_ranking_value @@ -126,15 +129,18 @@ class AccountV1Subsystem: raise ValueError('invalid subset value: ' + str(subset)) if data['p']: - pro_mult = ( - 1.0 - + float( - _internal.get_v1_account_misc_read_val( - 'proPowerRankingBoost', 0.0 + if _babase.app.plus is None: + pro_mult = 1.0 + else: + pro_mult = ( + 1.0 + + float( + _babase.app.plus.get_v1_account_misc_read_val( + 'proPowerRankingBoost', 0.0 + ) ) + * 0.01 ) - * 0.01 - ) else: pro_mult = 1.0 @@ -147,7 +153,6 @@ class AccountV1Subsystem: def cache_tournament_info(self, info: Any) -> None: """(internal)""" - from ba._generated.enums import TimeType, TimeFormat for entry in info: cache_entry = self.tournament_info[ @@ -156,24 +161,25 @@ class AccountV1Subsystem: # Also store the time we received this, so we can adjust # time-remaining values/etc. - cache_entry['timeReceived'] = _ba.time( - TimeType.REAL, TimeFormat.MILLISECONDS - ) + cache_entry['timeReceived'] = _babase.apptime() cache_entry['valid'] = True def get_purchased_icons(self) -> list[str]: """(internal)""" # pylint: disable=cyclic-import - from ba import _store - - if _internal.get_v1_account_state() != 'signed_in': + plus = _babase.app.plus + if plus is None: + return [] + if plus.get_v1_account_state() != 'signed_in': return [] icons = [] - store_items = _store.get_store_items() + store_items: dict[str, Any] = ( + _babase.app.classic.store.get_store_items() + if _babase.app.classic is not None + else {} + ) for item_name, item in list(store_items.items()): - if item_name.startswith('icons.') and _internal.get_purchased( - item_name - ): + if item_name.startswith('icons.') and plus.get_purchased(item_name): icons.append(item['icon']) return icons @@ -184,25 +190,27 @@ class AccountV1Subsystem: (internal) """ + plus = _babase.app.plus + if plus is None: + return # This only applies when we're signed in. - if _internal.get_v1_account_state() != 'signed_in': + if plus.get_v1_account_state() != 'signed_in': return # If the short version of our account name currently cant be # displayed by the game, cancel. - if not _ba.have_chars( - _internal.get_v1_account_display_string(full=False) + if not _babase.have_chars( + plus.get_v1_account_display_string(full=False) ): return - config = _ba.app.config + config = _babase.app.config if ( 'Player Profiles' not in config or '__account__' not in config['Player Profiles'] ): - # Create a spaz with a nice default purply color. - _internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'ADD_PLAYER_PROFILE', 'name': '__account__', @@ -213,18 +221,22 @@ class AccountV1Subsystem: }, } ) - _internal.run_transactions() + plus.run_v1_account_transactions() def have_pro(self) -> bool: """Return whether pro is currently unlocked.""" + plus = _babase.app.plus + if plus is None: + return False + # Check our tickets-based pro upgrade and our two real-IAP based # upgrades. Also always unlock this stuff in ballistica-core builds. return bool( - _internal.get_purchased('upgrades.pro') - or _internal.get_purchased('static.pro') - or _internal.get_purchased('static.pro_sale') - or 'ballistica' + 'core' == _ba.appname() + plus.get_purchased('upgrades.pro') + or plus.get_purchased('static.pro') + or plus.get_purchased('static.pro_sale') + or 'ballistica' + 'kit' == _babase.appname() ) def have_pro_options(self) -> bool: @@ -234,70 +246,78 @@ class AccountV1Subsystem: before Pro was a requirement for these options. """ + plus = _babase.app.plus + if plus is None: + return False + # We expose pro options if the server tells us to # (which is generally just when we own pro), # or also if we've been grandfathered in # or are using ballistica-core builds. return self.have_pro() or bool( - _internal.get_v1_account_misc_read_val_2( - 'proOptionsUnlocked', False - ) - or _ba.app.config.get('lc14292', 0) > 1 + plus.get_v1_account_misc_read_val_2('proOptionsUnlocked', False) + or _babase.app.config.get('lc14292', 0) > 1 ) def show_post_purchase_message(self) -> None: """(internal)""" - from ba._language import Lstr - from ba._generated.enums import TimeType + from babase._language import Lstr - cur_time = _ba.time(TimeType.REAL) + cur_time = _babase.apptime() if ( self.last_post_purchase_message_time is None or cur_time - self.last_post_purchase_message_time > 3.0 ): self.last_post_purchase_message_time = cur_time - with _ba.Context('ui'): - _ba.screenmessage( - Lstr( - resource='updatingAccountText', - fallback_resource='purchasingText', - ), - color=(0, 1, 0), - ) - _ba.playsound(_ba.getsound('click01')) + _babase.screenmessage( + Lstr( + resource='updatingAccountText', + fallback_resource='purchasingText', + ), + color=(0, 1, 0), + ) + _babase.getsimplesound('click01').play() def on_account_state_changed(self) -> None: """(internal)""" - from ba._language import Lstr + from babase._language import Lstr + plus = _babase.app.plus + if plus is None: + return # Run any pending promo codes we had queued up while not signed in. if ( - _internal.get_v1_account_state() == 'signed_in' + plus.get_v1_account_state() == 'signed_in' and self.pending_promo_codes ): for code in self.pending_promo_codes: - _ba.screenmessage( + _babase.screenmessage( Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0) ) - _internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'PROMO_CODE', 'expire_time': time.time() + 5, 'code': code, } ) - _internal.run_transactions() + plus.run_v1_account_transactions() self.pending_promo_codes = [] def add_pending_promo_code(self, code: str) -> None: """(internal)""" - from ba._language import Lstr - from ba._generated.enums import TimeType + from babase._language import Lstr + + plus = _babase.app.plus + if plus is None: + _babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) + _babase.getsimplesound('error').play() + return # If we're not signed in, queue up the code to run the next time we # are and issue a warning if we haven't signed in within the next # few seconds. - if _internal.get_v1_account_state() != 'signed_in': + if plus.get_v1_account_state() != 'signed_in': def check_pending_codes() -> None: """(internal)""" @@ -305,18 +325,18 @@ class AccountV1Subsystem: # If we're still not signed in and have pending codes, # inform the user that they need to sign in to use them. if self.pending_promo_codes: - _ba.screenmessage( + _babase.screenmessage( Lstr(resource='signInForPromoCodeText'), color=(1, 0, 0) ) - _ba.playsound(_ba.getsound('error')) + _babase.getsimplesound('error').play() self.pending_promo_codes.append(code) - _ba.timer(6.0, check_pending_codes, timetype=TimeType.REAL) + bui.apptimer(6.0, check_pending_codes) return - _ba.screenmessage( + _babase.screenmessage( Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0) ) - _internal.add_transaction( + plus.add_v1_account_transaction( {'type': 'PROMO_CODE', 'expire_time': time.time() + 5, 'code': code} ) - _internal.run_transactions() + plus.run_v1_account_transactions() diff --git a/assets/src/ba_data/python/ba/_achievement.py b/src/assets/ba_data/python/baclassic/_achievement.py similarity index 87% rename from assets/src/ba_data/python/ba/_achievement.py rename to src/assets/ba_data/python/baclassic/_achievement.py index 6d0ebb10..0b5a6988 100644 --- a/assets/src/ba_data/python/ba/_achievement.py +++ b/src/assets/ba_data/python/baclassic/_achievement.py @@ -3,15 +3,21 @@ """Various functionality related to achievements.""" from __future__ import annotations +import logging from typing import TYPE_CHECKING -import _ba -from ba import _internal -from ba._error import print_exception +import _babase +from babase._error import print_exception +import _bascenev1 +import _bauiv1 if TYPE_CHECKING: from typing import Any, Sequence - import ba + + import babase + import bascenev1 + import bauiv1 + import baclassic # This could use some cleanup. # We wear the cone of shame. @@ -73,8 +79,10 @@ class AchievementSubsystem: def __init__(self) -> None: self.achievements: list[Achievement] = [] - self.achievements_to_display: (list[tuple[ba.Achievement, bool]]) = [] - self.achievement_display_timer: _ba.Timer | None = None + self.achievements_to_display: ( + list[tuple[baclassic.Achievement, bool]] + ) = [] + self.achievement_display_timer: _bascenev1.BaseTimer | None = None self.last_achievement_display_time: float = 0.0 self.achievement_completion_banner_slots: set[int] = set() self._init_achievements() @@ -508,15 +516,18 @@ class AchievementSubsystem: def award_local_achievement(self, achname: str) -> None: """For non-game-based achievements such as controller-connection.""" + plus = _babase.app.plus + if plus is None: + logging.warning('achievements require plus feature-set') + return try: ach = self.get_achievement(achname) if not ach.complete: - # Report new achievements to the game-service. - _internal.report_achievement(achname) + plus.report_achievement(achname) # And to our account. - _internal.add_transaction( + plus.add_v1_account_transaction( {'type': 'ACHIEVEMENT', 'name': achname} ) @@ -538,9 +549,9 @@ class AchievementSubsystem: # purely local context somehow instead of trying to inject these # into whatever activity happens to be active # (since that won't work while in client mode). - activity = _ba.get_foreground_host_activity() + activity = _bascenev1.get_foreground_host_activity() if activity is not None: - with _ba.Context(activity): + with activity.context: self.get_achievement(achname).announce_completion() except Exception: print_exception('error showing server ach') @@ -557,7 +568,7 @@ class AchievementSubsystem: # us which achievements we currently have. We always defer to them, # even if that means we have to un-set an achievement we think we have. - cfg = _ba.app.config + cfg = _babase.app.config cfg['Achievements'] = {} for a_name in achs: self.get_achievement(a_name).set_complete(True) @@ -586,7 +597,6 @@ class AchievementSubsystem: def _test(self) -> None: """For testing achievement animations.""" - from ba._generated.enums import TimeType def testcall1() -> None: self.achievements[0].announce_completion() @@ -598,8 +608,8 @@ class AchievementSubsystem: self.achievements[4].announce_completion() self.achievements[5].announce_completion() - _ba.timer(3.0, testcall1, timetype=TimeType.BASE) - _ba.timer(7.0, testcall2, timetype=TimeType.BASE) + _bascenev1.basetimer(3.0, testcall1) + _bascenev1.basetimer(7.0, testcall2) def _get_ach_mult(include_pro_bonus: bool = False) -> int: @@ -607,28 +617,33 @@ def _get_ach_mult(include_pro_bonus: bool = False) -> int: (just for display; changing this here won't affect actual rewards) """ - val: int = _internal.get_v1_account_misc_read_val('achAwardMult', 5) + plus = _babase.app.plus + classic = _babase.app.classic + if plus is None or classic is None: + return 5 + val: int = plus.get_v1_account_misc_read_val('achAwardMult', 5) assert isinstance(val, int) - if include_pro_bonus and _ba.app.accounts_v1.have_pro(): + if include_pro_bonus and classic.accounts.have_pro(): val *= 2 return val def _display_next_achievement() -> None: - # Pull the first achievement off the list and display it, or kill the # display-timer if the list is empty. - app = _ba.app - if app.ach.achievements_to_display: + app = _babase.app + assert app.classic is not None + ach_ss = app.classic.ach + if app.classic.ach.achievements_to_display: try: - ach, sound = app.ach.achievements_to_display.pop(0) + ach, sound = ach_ss.achievements_to_display.pop(0) ach.show_completion_banner(sound) except Exception: print_exception('error showing next achievement') - app.ach.achievements_to_display = [] - app.ach.achievement_display_timer = None + ach_ss.achievements_to_display = [] + ach_ss.achievement_display_timer = None else: - app.ach.achievement_display_timer = None + ach_ss.achievement_display_timer = None class Achievement: @@ -664,9 +679,15 @@ class Achievement: """The name of the level this achievement applies to.""" return self._level_name - def get_icon_texture(self, complete: bool) -> ba.Texture: + def get_icon_ui_texture(self, complete: bool) -> bauiv1.Texture: """Return the icon texture to display for this achievement""" - return _ba.gettexture( + return _bauiv1.gettexture( + self._icon_name if complete else 'achievementEmpty' + ) + + def get_icon_texture(self, complete: bool) -> bascenev1.Texture: + """Return the icon texture to display for this achievement""" + return _bascenev1.gettexture( self._icon_name if complete else 'achievementEmpty' ) @@ -690,20 +711,26 @@ class Achievement: def announce_completion(self, sound: bool = True) -> None: """Kick off an announcement for this achievement's completion.""" - from ba._generated.enums import TimeType - app = _ba.app + app = _babase.app + plus = app.plus + classic = app.classic + if plus is None or classic is None: + logging.warning('ach account_completion not available.') + return + + ach_ss = classic.ach # Even though there are technically achievements when we're not # signed in, lets not show them (otherwise we tend to get # confusing 'controller connected' achievements popping up while # waiting to sign in which can be confusing). - if _internal.get_v1_account_state() != 'signed_in': + if plus.get_v1_account_state() != 'signed_in': return # If we're being freshly complete, display/report it and whatnot. - if (self, sound) not in app.ach.achievements_to_display: - app.ach.achievements_to_display.append((self, sound)) + if (self, sound) not in ach_ss.achievements_to_display: + ach_ss.achievements_to_display.append((self, sound)) # If there's no achievement display timer going, kick one off # (if one's already running it will pick this up before it dies). @@ -711,15 +738,11 @@ class Achievement: # Need to check last time too; its possible our timer wasn't able to # clear itself if an activity died and took it down with it. if ( - app.ach.achievement_display_timer is None - or _ba.time(TimeType.REAL) - app.ach.last_achievement_display_time - > 2.0 - ) and _ba.getactivity(doraise=False) is not None: - app.ach.achievement_display_timer = _ba.Timer( - 1.0, - _display_next_achievement, - repeat=True, - timetype=TimeType.BASE, + ach_ss.achievement_display_timer is None + or _babase.apptime() - ach_ss.last_achievement_display_time > 2.0 + ) and _bascenev1.getactivity(doraise=False) is not None: + ach_ss.achievement_display_timer = _bascenev1.BaseTimer( + 1.0, _display_next_achievement, repeat=True ) # Show the first immediately. @@ -736,18 +759,18 @@ class Achievement: config['Complete'] = complete @property - def display_name(self) -> ba.Lstr: - """Return a ba.Lstr for this Achievement's name.""" - from ba._language import Lstr + def display_name(self) -> babase.Lstr: + """Return a babase.Lstr for this Achievement's name.""" + from babase._language import Lstr - name: ba.Lstr | str + name: babase.Lstr | str try: if self._level_name != '': - from ba._campaign import getcampaign - campaignname, campaign_level = self._level_name.split(':') + classic = _babase.app.classic + assert classic is not None name = ( - getcampaign(campaignname) + classic.getcampaign(campaignname) .getlevel(campaign_level) .displayname ) @@ -762,25 +785,25 @@ class Achievement: ) @property - def description(self) -> ba.Lstr: - """Get a ba.Lstr for the Achievement's brief description.""" - from ba._language import Lstr + def description(self) -> babase.Lstr: + """Get a babase.Lstr for the Achievement's brief description.""" + from babase._language import Lstr if ( 'description' - in _ba.app.lang.get_resource('achievements')[self._name] + in _babase.app.lang.get_resource('achievements')[self._name] ): return Lstr(resource='achievements.' + self._name + '.description') return Lstr(resource='achievements.' + self._name + '.descriptionFull') @property - def description_complete(self) -> ba.Lstr: - """Get a ba.Lstr for the Achievement's description when completed.""" - from ba._language import Lstr + def description_complete(self) -> babase.Lstr: + """Get a babase.Lstr for the Achievement's description when complete.""" + from babase._language import Lstr if ( 'descriptionComplete' - in _ba.app.lang.get_resource('achievements')[self._name] + in _babase.app.lang.get_resource('achievements')[self._name] ): return Lstr( resource='achievements.' + self._name + '.descriptionComplete' @@ -790,9 +813,9 @@ class Achievement: ) @property - def description_full(self) -> ba.Lstr: - """Get a ba.Lstr for the Achievement's full description.""" - from ba._language import Lstr + def description_full(self) -> babase.Lstr: + """Get a babase.Lstr for the Achievement's full description.""" + from babase._language import Lstr return Lstr( resource='achievements.' + self._name + '.descriptionFull', @@ -810,9 +833,9 @@ class Achievement: ) @property - def description_full_complete(self) -> ba.Lstr: - """Get a ba.Lstr for the Achievement's full desc. when completed.""" - from ba._language import Lstr + def description_full_complete(self) -> babase.Lstr: + """Get a babase.Lstr for the Achievement's full desc. when completed.""" + from babase._language import Lstr return Lstr( resource='achievements.' + self._name + '.descriptionFullComplete', @@ -831,7 +854,10 @@ class Achievement: def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int: """Get the ticket award value for this achievement.""" - val: int = _internal.get_v1_account_misc_read_val( + plus = _babase.app.plus + if plus is None: + return 0 + val: int = plus.get_v1_account_misc_read_val( 'achAward.' + self._name, self._award ) * _get_ach_mult(include_pro_bonus) assert isinstance(val, int) @@ -840,7 +866,10 @@ class Achievement: @property def power_ranking_value(self) -> int: """Get the power-ranking award value for this achievement.""" - val: int = _internal.get_v1_account_misc_read_val( + plus = _babase.app.plus + if plus is None: + return 0 + val: int = plus.get_v1_account_misc_read_val( 'achLeaguePoints.' + self._name, self._award ) assert isinstance(val, int) @@ -854,15 +883,15 @@ class Achievement: outdelay: float | None = None, color: Sequence[float] | None = None, style: str = 'post_game', - ) -> list[ba.Actor]: + ) -> list[bascenev1.Actor]: """Create a display for the Achievement. Shows the Achievement icon, name, and description. """ # pylint: disable=cyclic-import - from ba._language import Lstr - from ba._generated.enums import SpecialChar - from ba._coopsession import CoopSession + from babase._language import Lstr + from babase._mgen.enums import SpecialChar + from bascenev1._coopsession import CoopSession from bastd.actor.image import Image from bastd.actor.text import Text @@ -894,7 +923,7 @@ class Achievement: hmo = False else: try: - session = _ba.getsession() + session = _bascenev1.getsession() if isinstance(session, CoopSession): campaign = session.campaign assert campaign is not None @@ -905,7 +934,7 @@ class Achievement: print_exception('Error determining campaign.') hmo = False - objs: list[ba.Actor] + objs: list[bascenev1.Actor] if in_game_colors: objs = [] @@ -1002,7 +1031,7 @@ class Achievement: award_x = -100 objs.append( Text( - _ba.charstr(SpecialChar.TICKET), + _babase.charstr(SpecialChar.TICKET), host_only=True, position=(x + award_x + 33, y + 7), transition=Text.Transition.FADE_IN, @@ -1057,9 +1086,11 @@ class Achievement: if complete: objs.append( Image( - _ba.gettexture('achievementOutline'), + _bascenev1.gettexture('achievementOutline'), host_only=True, - model_transparent=_ba.getmodel('achievementOutline'), + mesh_transparent=_bascenev1.getmesh( + 'achievementOutline' + ), color=(2, 1.4, 0.4, 1), vr_depth=8, position=(x - 25, y + 5), @@ -1075,7 +1106,7 @@ class Achievement: award_x = -100 objs.append( Text( - _ba.charstr(SpecialChar.TICKET), + _babase.charstr(SpecialChar.TICKET), host_only=True, position=(x + award_x + 33, y + 7), transition=Text.Transition.IN_RIGHT, @@ -1084,9 +1115,7 @@ class Achievement: v_attach=v_attach, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, - color=(1, 1, 1, 0.4) - if complete - else (1, 1, 1, (0.1 if hmo else 0.2)), + color=(1, 1, 1, (0.1 if hmo else 0.2)), transition_delay=delay + 0.05, transition_out_delay=None, ).autoretain() @@ -1103,11 +1132,7 @@ class Achievement: v_attach=v_attach, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, - color=( - (0.8, 0.93, 0.8, 1.0) - if complete - else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4)) - ), + color=(0.6, 0.6, 0.6, (0.2 if hmo else 0.4)), transition_delay=delay + 0.05, transition_out_delay=None, ).autoretain() @@ -1186,35 +1211,38 @@ class Achievement: Return the sub-dict in settings where this achievement's state is stored, creating it if need be. """ - val: dict[str, Any] = _ba.app.config.setdefault( + val: dict[str, Any] = _babase.app.config.setdefault( 'Achievements', {} ).setdefault(self._name, {'Complete': False}) assert isinstance(val, dict) return val def _remove_banner_slot(self) -> None: + classic = _babase.app.classic + assert classic is not None assert self._completion_banner_slot is not None - _ba.app.ach.achievement_completion_banner_slots.remove( + classic.ach.achievement_completion_banner_slots.remove( self._completion_banner_slot ) self._completion_banner_slot = None def show_completion_banner(self, sound: bool = True) -> None: """Create the banner/sound for an acquired achievement announcement.""" - from ba import _gameutils + from babase._general import WeakCall + from babase._language import Lstr + from babase._mgen.enums import SpecialChar from bastd.actor.text import Text from bastd.actor.image import Image - from ba._general import WeakCall - from ba._language import Lstr - from ba._messages import DieMessage - from ba._generated.enums import TimeType, SpecialChar + from bascenev1 import _gameutils + from bascenev1._messages import DieMessage - app = _ba.app - app.ach.last_achievement_display_time = _ba.time(TimeType.REAL) + app = _babase.app + assert app.classic is not None + app.classic.ach.last_achievement_display_time = _babase.apptime() # Just piggy-back onto any current activity # (should we use the session instead?..) - activity = _ba.getactivity(doraise=False) + activity = _bascenev1.getactivity(doraise=False) # If this gets called while this achievement is occupying a slot # already, ignore it. (probably should never happen in real @@ -1227,10 +1255,10 @@ class Achievement: return if sound: - _ba.playsound(_ba.getsound('achievement'), host_only=True) + _bascenev1.getsound('achievement').play(host_only=True) else: - _ba.timer( - 0.5, lambda: _ba.playsound(_ba.getsound('ding'), host_only=True) + _bascenev1.timer( + 0.5, lambda: _bascenev1.getsound('ding').play(host_only=True) ) in_time = 0.300 @@ -1241,26 +1269,24 @@ class Achievement: # Find the first free slot. i = 0 while True: - if i not in app.ach.achievement_completion_banner_slots: - app.ach.achievement_completion_banner_slots.add(i) + if i not in app.classic.ach.achievement_completion_banner_slots: + app.classic.ach.achievement_completion_banner_slots.add(i) self._completion_banner_slot = i # Remove us from that slot when we close. - # Use a real-timer in the UI context so the removal runs even - # if our activity/session dies. - with _ba.Context('ui'): - _ba.timer( - in_time + out_time, - self._remove_banner_slot, - timetype=TimeType.REAL, + # Use an app-timer in an empty context so the removal + # runs even if our activity/session dies. + with _babase.ContextRef.empty(): + _babase.apptimer( + in_time + out_time, self._remove_banner_slot ) break i += 1 assert self._completion_banner_slot is not None y_offs = 110 * self._completion_banner_slot - objs: list[ba.Actor] = [] + objs: list[bascenev1.Actor] = [] obj = Image( - _ba.gettexture('shadow'), + _bascenev1.gettexture('shadow'), position=(-30, 30 + y_offs), front=True, attach=Image.Attach.BOTTOM_CENTER, @@ -1275,7 +1301,7 @@ class Achievement: assert obj.node obj.node.host_only = True obj = Image( - _ba.gettexture('light'), + _bascenev1.gettexture('light'), position=(-180, 60 + y_offs), front=True, attach=Image.Attach.BOTTOM_CENTER, @@ -1290,7 +1316,9 @@ class Achievement: assert obj.node obj.node.host_only = True obj.node.premultiplied = True - combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 2}) + combine = _bascenev1.newnode( + 'combine', owner=obj.node, attrs={'size': 2} + ) _gameutils.animate( combine, 'input0', @@ -1332,7 +1360,9 @@ class Achievement: # Flash. color = self.get_icon_color(True) - combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3}) + combine = _bascenev1.newnode( + 'combine', owner=obj.node, attrs={'size': 3} + ) keys = { in_time: 1.0 * color[0], in_time + 0.4: 1.5 * color[0], @@ -1360,8 +1390,8 @@ class Achievement: combine.connectattr('output', obj.node, 'color') obj = Image( - _ba.gettexture('achievementOutline'), - model_transparent=_ba.getmodel('achievementOutline'), + _bascenev1.gettexture('achievementOutline'), + mesh_transparent=_bascenev1.getmesh('achievementOutline'), position=(-180, 60 + y_offs), front=True, attach=Image.Attach.BOTTOM_CENTER, @@ -1376,7 +1406,9 @@ class Achievement: # Flash. color = (2, 1.4, 0.4, 1) - combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3}) + combine = _bascenev1.newnode( + 'combine', owner=obj.node, attrs={'size': 3} + ) keys = { in_time: 1.0 * color[0], in_time + 0.4: 1.5 * color[0], @@ -1442,7 +1474,7 @@ class Achievement: objt.node.host_only = True objt = Text( - _ba.charstr(SpecialChar.TICKET), + _babase.charstr(SpecialChar.TICKET), position=(-120 - 170 + 5, 75 + y_offs - 20), front=True, v_attach=Text.VAttach.BOTTOM, @@ -1482,7 +1514,7 @@ class Achievement: objt.node.host_only = True # Add the 'x 2' if we've got pro. - if app.accounts_v1.have_pro(): + if app.classic.accounts.have_pro(): objt = Text( 'x 2', position=(-120 - 180 + 45, 80 + y_offs - 50), @@ -1522,6 +1554,6 @@ class Achievement: objt.node.host_only = True for actor in objs: - _ba.timer( + _bascenev1.timer( out_time + 1.000, WeakCall(actor.handlemessage, DieMessage()) ) diff --git a/assets/src/ba_data/python/ba/_ads.py b/src/assets/ba_data/python/baclassic/_ads.py similarity index 69% rename from assets/src/ba_data/python/ba/_ads.py rename to src/assets/ba_data/python/baclassic/_ads.py index 243b29d1..90e7ac97 100644 --- a/assets/src/ba_data/python/ba/_ads.py +++ b/src/assets/ba_data/python/baclassic/_ads.py @@ -6,8 +6,9 @@ from __future__ import annotations import time from typing import TYPE_CHECKING -import _ba -from ba import _internal +import _babase +import _bauiv1 +import _bascenev1 if TYPE_CHECKING: from typing import Callable, Any @@ -33,19 +34,18 @@ class AdsSubsystem: def do_remove_in_game_ads_message(self) -> None: """(internal)""" - from ba._language import Lstr - from ba._generated.enums import TimeType + from babase._language import Lstr # Print this message once every 10 minutes at most. - tval = _ba.time(TimeType.REAL) + tval = _babase.apptime() if self.last_in_game_ad_remove_message_show_time is None or ( tval - self.last_in_game_ad_remove_message_show_time > 60 * 10 ): self.last_in_game_ad_remove_message_show_time = tval - with _ba.Context('ui'): - _ba.timer( + with _babase.ContextRef.empty(): + _babase.apptimer( 1.0, - lambda: _ba.screenmessage( + lambda: _babase.screenmessage( Lstr( resource='removeInGameAdsText', subs=[ @@ -58,7 +58,6 @@ class AdsSubsystem: ), color=(1, 1, 0), ), - timetype=TimeType.REAL, ) def show_ad( @@ -66,7 +65,7 @@ class AdsSubsystem: ) -> None: """(internal)""" self.last_ad_purpose = purpose - _ba.show_ad(purpose, on_completion_call) + _bauiv1.show_ad(purpose, on_completion_call) def show_ad_2( self, @@ -75,25 +74,28 @@ class AdsSubsystem: ) -> None: """(internal)""" self.last_ad_purpose = purpose - _ba.show_ad_2(purpose, on_completion_call) + _bauiv1.show_ad_2(purpose, on_completion_call) def call_after_ad(self, call: Callable[[], Any]) -> None: """Run a call after potentially showing an ad.""" # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals - from ba._generated.enums import TimeType - app = _ba.app + app = _babase.app + plus = app.plus + classic = app.classic + assert plus is not None + assert classic is not None show = True # No ads without net-connections, etc. - if not _ba.can_show_ad(): + if not _bauiv1.can_show_ad(): show = False - if app.accounts_v1.have_pro(): + if classic.accounts.have_pro(): show = False # Pro disables interstitials. try: - session = _ba.get_foreground_host_session() + session = _bascenev1.get_foreground_host_session() assert session is not None is_tournament = session.tournament_id is not None except Exception: @@ -107,19 +109,17 @@ class AdsSubsystem: # If we're seeing short ads we may want to space them differently. interval_mult = ( - _internal.get_v1_account_misc_read_val( - 'ads.shortIntervalMult', 1.0 - ) + plus.get_v1_account_misc_read_val('ads.shortIntervalMult', 1.0) if self.last_ad_was_short else 1.0 ) if self.ad_amt is None: if launch_count <= 1: - self.ad_amt = _internal.get_v1_account_misc_read_val( + self.ad_amt = plus.get_v1_account_misc_read_val( 'ads.startVal1', 0.99 ) else: - self.ad_amt = _internal.get_v1_account_misc_read_val( + self.ad_amt = plus.get_v1_account_misc_read_val( 'ads.startVal2', 1.0 ) interval = None @@ -128,23 +128,19 @@ class AdsSubsystem: # ad-show-threshold and see if we should *actually* show # (we reach our threshold faster the longer we've been # playing). - base = 'ads' if _ba.has_video_ads() else 'ads2' - min_lc = _internal.get_v1_account_misc_read_val( - base + '.minLC', 0.0 - ) - max_lc = _internal.get_v1_account_misc_read_val( - base + '.maxLC', 5.0 - ) - min_lc_scale = _internal.get_v1_account_misc_read_val( + base = 'ads' if _bauiv1.has_video_ads() else 'ads2' + min_lc = plus.get_v1_account_misc_read_val(base + '.minLC', 0.0) + max_lc = plus.get_v1_account_misc_read_val(base + '.maxLC', 5.0) + min_lc_scale = plus.get_v1_account_misc_read_val( base + '.minLCScale', 0.25 ) - max_lc_scale = _internal.get_v1_account_misc_read_val( + max_lc_scale = plus.get_v1_account_misc_read_val( base + '.maxLCScale', 0.34 ) - min_lc_interval = _internal.get_v1_account_misc_read_val( + min_lc_interval = plus.get_v1_account_misc_read_val( base + '.minLCInterval', 360 ) - max_lc_interval = _internal.get_v1_account_misc_read_val( + max_lc_interval = plus.get_v1_account_misc_read_val( base + '.maxLCInterval', 300 ) if launch_count < min_lc: @@ -170,7 +166,7 @@ class AdsSubsystem: self.last_ad_completion_time is None or ( interval is not None - and _ba.time(TimeType.REAL) - self.last_ad_completion_time + and _babase.apptime() - self.last_ad_completion_time > (interval * interval_mult) ) ): @@ -192,34 +188,25 @@ class AdsSubsystem: def run(self, fallback: bool = False) -> None: """Run fallback call (and issue a warning about it).""" + assert app.classic is not None if not self._ran: if fallback: + lanst = app.classic.ads.last_ad_network_set_time print( - ( - 'ERROR: relying on fallback ad-callback! ' - 'last network: ' - + app.ads.last_ad_network - + ' (set ' - + str( - int( - time.time() - - app.ads.last_ad_network_set_time - ) - ) - + 's ago); purpose=' - + app.ads.last_ad_purpose - ) + 'ERROR: relying on fallback ad-callback! ' + 'last network: ' + + app.classic.ads.last_ad_network + + ' (set ' + + str(int(time.time() - lanst)) + + 's ago); purpose=' + + app.classic.ads.last_ad_purpose ) - _ba.pushcall(self._call) + _babase.pushcall(self._call) self._ran = True payload = _Payload(call) - with _ba.Context('ui'): - _ba.timer( - 5.0, - lambda: payload.run(fallback=True), - timetype=TimeType.REAL, - ) + with _babase.ContextRef.empty(): + _babase.apptimer(5.0, lambda: payload.run(fallback=True)) self.show_ad('between_game', on_completion_call=payload.run) else: - _ba.pushcall(call) # Just run the callback without the ad. + _babase.pushcall(call) # Just run the callback without the ad. diff --git a/src/assets/ba_data/python/baclassic/_analytics.py b/src/assets/ba_data/python/baclassic/_analytics.py new file mode 100644 index 00000000..8ea4a01e --- /dev/null +++ b/src/assets/ba_data/python/baclassic/_analytics.py @@ -0,0 +1,97 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to analytics.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _babase +import _bascenev1 + +if TYPE_CHECKING: + pass + + +def game_begin_analytics() -> None: + """Update analytics events for the start of a game.""" + # pylint: disable=too-many-branches + # pylint: disable=cyclic-import + from bascenev1._dualteamsession import DualTeamSession + from bascenev1._freeforallsession import FreeForAllSession + from bascenev1._coopsession import CoopSession + from bascenev1._gameactivity import GameActivity + + assert _babase.app.classic is not None + + activity = _bascenev1.getactivity(False) + session = _bascenev1.getsession(False) + + # Fail gracefully if we didn't cleanly get a session and game activity. + if not activity or not session or not isinstance(activity, GameActivity): + return + + if isinstance(session, CoopSession): + campaign = session.campaign + assert campaign is not None + _babase.set_analytics_screen( + 'Coop Game: ' + + campaign.name + + ' ' + + campaign.getlevel( + _babase.app.classic.coop_session_args['level'] + ).name + ) + _babase.increment_analytics_count('Co-op round start') + if len(activity.players) == 1: + _babase.increment_analytics_count( + 'Co-op round start 1 human player' + ) + elif len(activity.players) == 2: + _babase.increment_analytics_count( + 'Co-op round start 2 human players' + ) + elif len(activity.players) == 3: + _babase.increment_analytics_count( + 'Co-op round start 3 human players' + ) + elif len(activity.players) >= 4: + _babase.increment_analytics_count( + 'Co-op round start 4+ human players' + ) + + elif isinstance(session, DualTeamSession): + _babase.set_analytics_screen('Teams Game: ' + activity.getname()) + _babase.increment_analytics_count('Teams round start') + if len(activity.players) == 1: + _babase.increment_analytics_count( + 'Teams round start 1 human player' + ) + elif 1 < len(activity.players) < 8: + _babase.increment_analytics_count( + 'Teams round start ' + + str(len(activity.players)) + + ' human players' + ) + elif len(activity.players) >= 8: + _babase.increment_analytics_count( + 'Teams round start 8+ human players' + ) + + elif isinstance(session, FreeForAllSession): + _babase.set_analytics_screen('FreeForAll Game: ' + activity.getname()) + _babase.increment_analytics_count('Free-for-all round start') + if len(activity.players) == 1: + _babase.increment_analytics_count( + 'Free-for-all round start 1 human player' + ) + elif 1 < len(activity.players) < 8: + _babase.increment_analytics_count( + 'Free-for-all round start ' + + str(len(activity.players)) + + ' human players' + ) + elif len(activity.players) >= 8: + _babase.increment_analytics_count( + 'Free-for-all round start 8+ human players' + ) diff --git a/assets/src/ba_data/python/ba/_appdelegate.py b/src/assets/ba_data/python/baclassic/_appdelegate.py similarity index 52% rename from assets/src/ba_data/python/ba/_appdelegate.py rename to src/assets/ba_data/python/baclassic/_appdelegate.py index b21ec101..e12c872d 100644 --- a/assets/src/ba_data/python/ba/_appdelegate.py +++ b/src/assets/ba_data/python/baclassic/_appdelegate.py @@ -5,9 +5,12 @@ from __future__ import annotations from typing import TYPE_CHECKING +import _babase + if TYPE_CHECKING: from typing import Callable - import ba + import babase + import bascenev1 as bs class AppDelegate: @@ -18,8 +21,8 @@ class AppDelegate: def create_default_game_settings_ui( self, - gameclass: type[ba.GameActivity], - sessiontype: type[ba.Session], + gameclass: type[bs.GameActivity], + sessiontype: type[bs.Session], settings: dict | None, completion_call: Callable[[dict | None], None], ) -> None: @@ -28,9 +31,16 @@ class AppDelegate: It should manipulate the contents of config and call completion_call when done. """ - del gameclass, sessiontype, settings, completion_call # Unused. - from ba import _error + # Replace the main window once we come up successfully. + from bastd.ui.playlist.editgame import PlaylistEditGameWindow - _error.print_error( - "create_default_game_settings_ui needs to be overridden" + assert _babase.app.classic is not None + _babase.app.classic.ui.clear_main_menu_window(transition='out_left') + _babase.app.classic.ui.set_main_menu_window( + PlaylistEditGameWindow( + gameclass, + sessiontype, + settings, + completion_call=completion_call, + ).get_root_widget() ) diff --git a/assets/src/ba_data/python/ba/_benchmark.py b/src/assets/ba_data/python/baclassic/_benchmark.py similarity index 55% rename from assets/src/ba_data/python/ba/_benchmark.py rename to src/assets/ba_data/python/baclassic/_benchmark.py index f8821653..cda5f4da 100644 --- a/assets/src/ba_data/python/ba/_benchmark.py +++ b/src/assets/ba_data/python/baclassic/_benchmark.py @@ -6,48 +6,50 @@ from __future__ import annotations import random from typing import TYPE_CHECKING -import _ba +import _babase +import _bauiv1 +import _bascenev1 if TYPE_CHECKING: from typing import Any, Sequence - import ba + + import babase + import bascenev1 def run_cpu_benchmark() -> None: """Run a cpu benchmark.""" # pylint: disable=cyclic-import from bastd import tutorial - from ba._session import Session + from bascenev1._session import Session class BenchmarkSession(Session): """Session type for cpu benchmark.""" def __init__(self) -> None: - # print('FIXME: BENCHMARK SESSION WOULD CALC DEPS.') - depsets: Sequence[ba.DependencySet] = [] + depsets: Sequence[bascenev1.DependencySet] = [] super().__init__(depsets) # Store old graphics settings. - self._old_quality = _ba.app.config.resolve('Graphics Quality') - cfg = _ba.app.config + self._old_quality = _babase.app.config.resolve('Graphics Quality') + cfg = _babase.app.config cfg['Graphics Quality'] = 'Low' cfg.apply() self.benchmark_type = 'cpu' - self.setactivity(_ba.newactivity(tutorial.TutorialActivity)) + self.setactivity(_bascenev1.newactivity(tutorial.TutorialActivity)) def __del__(self) -> None: - # When we're torn down, restore old graphics settings. - cfg = _ba.app.config + cfg = _babase.app.config cfg['Graphics Quality'] = self._old_quality cfg.apply() - def on_player_request(self, player: ba.SessionPlayer) -> bool: + def on_player_request(self, player: bascenev1.SessionPlayer) -> bool: return False - _ba.new_host_session(BenchmarkSession, benchmark_type='cpu') + _bascenev1.new_host_session(BenchmarkSession, benchmark_type='cpu') def run_stress_test( @@ -57,15 +59,14 @@ def run_stress_test( round_duration: int = 30, ) -> None: """Run a stress test.""" - from ba import modutils - from ba._general import Call - from ba._generated.enums import TimeType + from bauiv1 import modutils + from babase._general import Call - _ba.screenmessage( + _babase.screenmessage( "Beginning stress test.. use 'End Test' to stop testing.", color=(1, 1, 0), ) - with _ba.Context('ui'): + with _babase.ContextRef.empty(): start_stress_test( { 'playlist_type': playlist_type, @@ -74,46 +75,47 @@ def run_stress_test( 'round_duration': round_duration, } ) - _ba.timer( + _babase.apptimer( 7.0, Call( - _ba.screenmessage, + _babase.screenmessage, ( 'stats will be written to ' + modutils.get_human_readable_user_scripts_path() + '/stress_test_stats.csv' ), ), - timetype=TimeType.REAL, ) def stop_stress_test() -> None: """End a running stress test.""" - _ba.set_stress_testing(False, 0) + _babase.set_stress_testing(False, 0) + assert _babase.app.classic is not None try: - if _ba.app.stress_test_reset_timer is not None: - _ba.screenmessage('Ending stress test...', color=(1, 1, 0)) + if _babase.app.classic.stress_test_reset_timer is not None: + _babase.screenmessage('Ending stress test...', color=(1, 1, 0)) except Exception: pass - _ba.app.stress_test_reset_timer = None + _babase.app.classic.stress_test_reset_timer = None def start_stress_test(args: dict[str, Any]) -> None: """(internal)""" - from ba._general import Call - from ba._dualteamsession import DualTeamSession - from ba._freeforallsession import FreeForAllSession - from ba._generated.enums import TimeType, TimeFormat + from babase._general import Call + from bascenev1._dualteamsession import DualTeamSession + from bascenev1._freeforallsession import FreeForAllSession - appconfig = _ba.app.config + assert _babase.app.classic is not None + + appconfig = _babase.app.config playlist_type = args['playlist_type'] if playlist_type == 'Random': if random.random() < 0.5: playlist_type = 'Teams' else: playlist_type = 'Free-For-All' - _ba.screenmessage( + _babase.screenmessage( 'Running Stress Test (listType="' + playlist_type + '", listName="' @@ -123,76 +125,71 @@ def start_stress_test(args: dict[str, Any]) -> None: if playlist_type == 'Teams': appconfig['Team Tournament Playlist Selection'] = args['playlist_name'] appconfig['Team Tournament Playlist Randomize'] = 1 - _ba.timer( + _babase.apptimer( 1.0, - Call(_ba.pushcall, Call(_ba.new_host_session, DualTeamSession)), - timetype=TimeType.REAL, + Call( + _babase.pushcall, + Call(_bascenev1.new_host_session, DualTeamSession), + ), ) else: appconfig['Free-for-All Playlist Selection'] = args['playlist_name'] appconfig['Free-for-All Playlist Randomize'] = 1 - _ba.timer( + _babase.apptimer( 1.0, - Call(_ba.pushcall, Call(_ba.new_host_session, FreeForAllSession)), - timetype=TimeType.REAL, + Call( + _babase.pushcall, + Call(_bascenev1.new_host_session, FreeForAllSession), + ), ) - _ba.set_stress_testing(True, args['player_count']) - _ba.app.stress_test_reset_timer = _ba.Timer( - args['round_duration'] * 1000, - Call(_reset_stress_test, args), - timetype=TimeType.REAL, - timeformat=TimeFormat.MILLISECONDS, + _babase.set_stress_testing(True, args['player_count']) + _babase.app.classic.stress_test_reset_timer = _babase.AppTimer( + args['round_duration'], Call(_reset_stress_test, args) ) def _reset_stress_test(args: dict[str, Any]) -> None: - from ba._general import Call - from ba._generated.enums import TimeType + from babase._general import Call - _ba.set_stress_testing(False, args['player_count']) - _ba.screenmessage('Resetting stress test...') - session = _ba.get_foreground_host_session() + _babase.set_stress_testing(False, args['player_count']) + _babase.screenmessage('Resetting stress test...') + session = _bascenev1.get_foreground_host_session() assert session is not None session.end() - _ba.timer(1.0, Call(start_stress_test, args), timetype=TimeType.REAL) + _babase.apptimer(1.0, Call(start_stress_test, args)) def run_gpu_benchmark() -> None: """Kick off a benchmark to test gpu speeds.""" # FIXME: Not wired up yet. - _ba.screenmessage('Not wired up yet.', color=(1, 0, 0)) + _babase.screenmessage('Not wired up yet.', color=(1, 0, 0)) def run_media_reload_benchmark() -> None: """Kick off a benchmark to test media reloading speeds.""" - from ba._general import Call - from ba._generated.enums import TimeType + from babase._general import Call - _ba.reload_media() - _ba.show_progress_bar() + _babase.reload_media() + _bauiv1.show_progress_bar() def delay_add(start_time: float) -> None: def doit(start_time_2: float) -> None: - _ba.screenmessage( - _ba.app.lang.get_resource( + _babase.screenmessage( + _babase.app.lang.get_resource( 'debugWindow.totalReloadTimeText' - ).replace( - '${TIME}', str(_ba.time(TimeType.REAL) - start_time_2) - ) + ).replace('${TIME}', str(_babase.apptime() - start_time_2)) ) - _ba.print_load_info() - if _ba.app.config.resolve('Texture Quality') != 'High': - _ba.screenmessage( - _ba.app.lang.get_resource( + _babase.print_load_info() + if _babase.app.config.resolve('Texture Quality') != 'High': + _babase.screenmessage( + _babase.app.lang.get_resource( 'debugWindow.reloadBenchmarkBestResultsText' ), color=(1, 1, 0), ) - _ba.add_clean_frame_callback(Call(doit, start_time)) + _babase.add_clean_frame_callback(Call(doit, start_time)) # The reload starts (should add a completion callback to the # reload func to fix this). - _ba.timer( - 0.05, Call(delay_add, _ba.time(TimeType.REAL)), timetype=TimeType.REAL - ) + _babase.apptimer(0.05, Call(delay_add, _babase.apptime())) diff --git a/assets/src/ba_data/python/ba/_campaign.py b/src/assets/ba_data/python/baclassic/_campaign.py similarity index 92% rename from assets/src/ba_data/python/ba/_campaign.py rename to src/assets/ba_data/python/baclassic/_campaign.py index b0242ffa..d18d2c24 100644 --- a/assets/src/ba_data/python/ba/_campaign.py +++ b/src/assets/ba_data/python/baclassic/_campaign.py @@ -5,25 +5,22 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba +import _babase if TYPE_CHECKING: from typing import Any - import ba + import babase + import baclassic -def register_campaign(campaign: ba.Campaign) -> None: +def register_campaign(campaign: baclassic.Campaign) -> None: """Register a new campaign.""" - _ba.app.campaigns[campaign.name] = campaign - - -def getcampaign(name: str) -> ba.Campaign: - """Return a campaign by name.""" - return _ba.app.campaigns[name] + assert _babase.app.classic is not None + _babase.app.classic.campaigns[campaign.name] = campaign class Campaign: - """Represents a unique set or series of ba.Level-s. + """Represents a unique set or series of baclassic.Level-s. Category: **App Classes** """ @@ -32,11 +29,11 @@ class Campaign: self, name: str, sequential: bool = True, - levels: list[ba.Level] | None = None, + levels: list[baclassic.Level] | None = None, ): self._name = name self._sequential = sequential - self._levels: list[ba.Level] = [] + self._levels: list[baclassic.Level] = [] if levels is not None: for level in levels: self.addlevel(level) @@ -51,8 +48,10 @@ class Campaign: """Whether this Campaign's levels must be played in sequence.""" return self._sequential - def addlevel(self, level: ba.Level, index: int | None = None) -> None: - """Adds a ba.Level to the Campaign.""" + def addlevel( + self, level: baclassic.Level, index: int | None = None + ) -> None: + """Adds a baclassic.Level to the Campaign.""" if level.campaign is not None: raise RuntimeError('Level already belongs to a campaign.') level.set_campaign(self, len(self._levels)) @@ -62,13 +61,13 @@ class Campaign: self._levels.insert(index, level) @property - def levels(self) -> list[ba.Level]: - """The list of ba.Level-s in the Campaign.""" + def levels(self) -> list[baclassic.Level]: + """The list of baclassic.Level-s in the Campaign.""" return self._levels - def getlevel(self, name: str) -> ba.Level: - """Return a contained ba.Level by name.""" - from ba import _error + def getlevel(self, name: str) -> baclassic.Level: + """Return a contained baclassic.Level by name.""" + from babase import _error for level in self._levels: if level.name == name: @@ -79,13 +78,14 @@ class Campaign: def reset(self) -> None: """Reset state for the Campaign.""" - _ba.app.config.setdefault('Campaigns', {})[self._name] = {} + _babase.app.config.setdefault('Campaigns', {})[self._name] = {} - # FIXME should these give/take ba.Level instances instead of level names?.. + # FIXME should these give/take baclsssic.Level instances instead + # of level names?.. def set_selected_level(self, levelname: str) -> None: """Set the Level currently selected in the UI (by name).""" self.configdict['Selection'] = levelname - _ba.app.config.commit() + _babase.app.config.commit() def get_selected_level(self) -> str: """Return the name of the Level currently selected in the UI.""" @@ -94,7 +94,7 @@ class Campaign: @property def configdict(self) -> dict[str, Any]: """Return the live config dict for this campaign.""" - val: dict[str, Any] = _ba.app.config.setdefault( + val: dict[str, Any] = _babase.app.config.setdefault( 'Campaigns', {} ).setdefault(self._name, {}) assert isinstance(val, dict) @@ -104,7 +104,7 @@ class Campaign: def init_campaigns() -> None: """Fill out initial default Campaigns.""" # pylint: disable=cyclic-import - from ba._level import Level + from baclassic._level import Level from bastd.game.onslaught import OnslaughtGame from bastd.game.football import FootballCoopGame from bastd.game.runaround import RunaroundGame diff --git a/assets/src/ba_data/python/ba/_input.py b/src/assets/ba_data/python/baclassic/_input.py similarity index 91% rename from assets/src/ba_data/python/ba/_input.py rename to src/assets/ba_data/python/baclassic/_input.py index 682dd046..6c338edf 100644 --- a/assets/src/ba_data/python/ba/_input.py +++ b/src/assets/ba_data/python/baclassic/_input.py @@ -5,15 +5,17 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba -from ba._internal import get_v1_account_display_string +import _babase if TYPE_CHECKING: from typing import Any - import ba + import babase + import bascenev1 -def get_device_value(device: ba.InputDevice, name: str) -> Any: +def get_input_device_mapped_value( + devicename: str, unique_id: str, name: str +) -> Any: """Returns a mapped value for an input device. This checks the user config and falls back to default values @@ -22,16 +24,16 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any: # pylint: disable=too-many-statements # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches - devicename = device.name - unique_id = device.unique_identifier - app = _ba.app - useragentstring = app.user_agent_string - platform = app.platform - subplatform = app.subplatform - appconfig = _ba.app.config + + app = _babase.app + assert app.classic is not None + useragentstring = app.classic.user_agent_string + platform = app.classic.platform + subplatform = app.classic.subplatform + appconfig = _babase.app.config # iiRcade: hard-code for a/b/c/x for now... - if _ba.app.iircade_mode: + if _babase.app.iircade_mode: return { 'triggerRun2': 19, 'unassignedButtonsRun': False, @@ -64,7 +66,6 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any: return mapping.get(name, -1) if platform == 'windows': - # XInput (hopefully this mapping is consistent?...) if devicename.startswith('XInput Controller'): return { @@ -98,7 +99,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any: }.get(name, -1) # Look for some exact types. - if _ba.is_running_on_fire_tv(): + if _babase.is_running_on_fire_tv(): if devicename in ['Thunder', 'Amazon Fire Game Controller']: return { 'triggerRun2': 23, @@ -211,7 +212,6 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any: 'buttonIgnored': 17, }.get(name, -1) if devicename in ['Wireless 360 Controller', 'Controller']: - # Xbox360 gamepads return { 'analogStickDeadZone': 1.2, @@ -325,7 +325,6 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any: # Generic android... if platform == 'android': - # Steelseries stratus xl. if devicename == 'SteelSeries Stratus XL': return { @@ -519,8 +518,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any: # Reasonable defaults. if platform == 'android': - if _ba.is_running_on_fire_tv(): - + if _babase.is_running_on_fire_tv(): # Mostly same as default firetv controller. return { 'triggerRun2': 23, @@ -579,7 +577,7 @@ def _gen_android_input_hash() -> str: except PermissionError: pass except Exception: - from ba import _error + from babase import _error _error.print_exception( 'error in _gen_android_input_hash inner loop' @@ -587,7 +585,7 @@ def _gen_android_input_hash() -> str: return md5.hexdigest() -def get_input_map_hash(inputdevice: ba.InputDevice) -> str: +def get_input_device_map_hash() -> str: """Given an input device, return a hash based on its raw input values. This lets us avoid sharing mappings across devices that may @@ -595,34 +593,36 @@ def get_input_map_hash(inputdevice: ba.InputDevice) -> str: (Different Android versions, for example, may return different key codes for button presses on a given type of controller) """ - del inputdevice # Currently unused. - app = _ba.app - try: - if app.input_map_hash is None: - if app.platform == 'android': - app.input_map_hash = _gen_android_input_hash() - else: - app.input_map_hash = '' - return app.input_map_hash - except Exception: - from ba import _error + app = _babase.app - _error.print_exception('Exception in get_input_map_hash') - return '' + # Currently only using this when classic is present. + # Need to replace with a modern equivalent. + if app.classic is not None: + try: + if app.classic.input_map_hash is None: + if app.classic.platform == 'android': + app.classic.input_map_hash = _gen_android_input_hash() + else: + app.classic.input_map_hash = '' + return app.classic.input_map_hash + except Exception: + from babase import _error + + _error.print_exception('Exception in get_input_map_hash') + return '' + return '' def get_input_device_config( - device: ba.InputDevice, default: bool + name: str, unique_id: str, default: bool ) -> tuple[dict, str]: """Given an input device, return its config dict in the app config. The dict will be created if it does not exist. """ - cfg = _ba.app.config - name = device.name + cfg = _babase.app.config ccfgs: dict[str, Any] = cfg.setdefault('Controllers', {}) ccfgs.setdefault(name, {}) - unique_id = device.unique_identifier if default: if unique_id in ccfgs[name]: del ccfgs[name][unique_id] @@ -632,26 +632,3 @@ def get_input_device_config( if unique_id not in ccfgs[name]: ccfgs[name][unique_id] = {} return ccfgs[name], unique_id - - -def get_last_player_name_from_input_device(device: ba.InputDevice) -> str: - """Return a reasonable player name associated with a device. - - (generally the last one used there) - """ - appconfig = _ba.app.config - - # Look for a default player profile name for them; - # otherwise default to their current random name. - profilename = '_random' - key_name = device.name + ' ' + device.unique_identifier - if ( - 'Default Player Profiles' in appconfig - and key_name in appconfig['Default Player Profiles'] - ): - profilename = appconfig['Default Player Profiles'][key_name] - if profilename == '_random': - profilename = device.get_default_player_name() - if profilename == '__account__': - profilename = get_v1_account_display_string() - return profilename diff --git a/assets/src/ba_data/python/ba/_level.py b/src/assets/ba_data/python/baclassic/_level.py similarity index 85% rename from assets/src/ba_data/python/ba/_level.py rename to src/assets/ba_data/python/baclassic/_level.py index 6c1426da..fc12af56 100644 --- a/assets/src/ba_data/python/ba/_level.py +++ b/src/assets/ba_data/python/baclassic/_level.py @@ -7,15 +7,18 @@ import copy import weakref from typing import TYPE_CHECKING -import _ba +import _bauiv1 if TYPE_CHECKING: from typing import Any - import ba + + import baclassic + import bascenev1 + import bauiv1 class Level: - """An entry in a ba.Campaign consisting of a name, game type, and settings. + """An entry in a baclassic.Campaign consisting of a name, game type, and settings. Category: **Gameplay Classes** """ @@ -23,7 +26,7 @@ class Level: def __init__( self, name: str, - gametype: type[ba.GameActivity], + gametype: type[bascenev1.GameActivity], settings: dict, preview_texture_name: str, displayname: str | None = None, @@ -33,7 +36,7 @@ class Level: self._settings = settings self._preview_texture_name = preview_texture_name self._displayname = displayname - self._campaign: weakref.ref[ba.Campaign] | None = None + self._campaign: weakref.ref[baclassic.Campaign] | None = None self._index: int | None = None self._score_version_string: str | None = None @@ -60,14 +63,14 @@ class Level: """The preview texture name for this Level.""" return self._preview_texture_name - def get_preview_texture(self) -> ba.Texture: + def get_preview_texture(self) -> bauiv1.Texture: """Load/return the preview Texture for this Level.""" - return _ba.gettexture(self._preview_texture_name) + return _bauiv1.gettexture(self._preview_texture_name) @property - def displayname(self) -> ba.Lstr: + def displayname(self) -> bascenev1.Lstr: """The localized name for this Level.""" - from ba import _language + from babase import _language return _language.Lstr( translate=( @@ -82,18 +85,18 @@ class Level: ) @property - def gametype(self) -> type[ba.GameActivity]: + def gametype(self) -> type[bascenev1.GameActivity]: """The type of game used for this Level.""" return self._gametype @property - def campaign(self) -> ba.Campaign | None: - """The ba.Campaign this Level is associated with, or None.""" + def campaign(self) -> baclassic.Campaign | None: + """The baclassic.Campaign this Level is associated with, or None.""" return None if self._campaign is None else self._campaign() @property def index(self) -> int: - """The zero-based index of this Level in its ba.Campaign. + """The zero-based index of this Level in its baclassic.Campaign. Access results in a RuntimeError if the Level is not assigned to a Campaign. @@ -171,8 +174,8 @@ class Level: assert isinstance(val, dict) return val - def set_campaign(self, campaign: ba.Campaign, index: int) -> None: - """For use by ba.Campaign when adding levels to itself. + def set_campaign(self, campaign: baclassic.Campaign, index: int) -> None: + """For use by baclassic.Campaign when adding levels to itself. (internal)""" self._campaign = weakref.ref(campaign) diff --git a/assets/src/ba_data/python/ba/_lobby.py b/src/assets/ba_data/python/baclassic/_lobby.py similarity index 85% rename from assets/src/ba_data/python/ba/_lobby.py rename to src/assets/ba_data/python/baclassic/_lobby.py index 1bfcfeb3..fefad3e7 100644 --- a/assets/src/ba_data/python/ba/_lobby.py +++ b/src/assets/ba_data/python/baclassic/_lobby.py @@ -9,16 +9,19 @@ import weakref from dataclasses import dataclass from typing import TYPE_CHECKING -import _ba -from ba._error import print_exception, print_error, NotFoundError -from ba._gameutils import animate, animate_array -from ba._language import Lstr -from ba._generated.enums import SpecialChar, InputType -from ba._profile import get_player_profile_colors +import _babase +from babase._error import print_exception, print_error, NotFoundError +from babase._language import Lstr +from babase._mgen.enums import SpecialChar, InputType +from bascenev1._gameutils import animate, animate_array +import _bascenev1 +from baclassic._profile import get_player_profile_colors if TYPE_CHECKING: from typing import Any, Sequence - import ba + + import baclassic + import bascenev1 MAX_QUICK_CHANGE_COUNT = 30 QUICK_CHANGE_INTERVAL = 0.05 @@ -29,20 +32,20 @@ QUICK_CHANGE_RESET_INTERVAL = 1.0 class JoinInfo: """Display useful info for joiners.""" - def __init__(self, lobby: ba.Lobby): - from ba._nodeactor import NodeActor - from ba._general import WeakCall + def __init__(self, lobby: baclassic.Lobby): + from bascenev1._nodeactor import NodeActor + from babase._general import WeakCall self._state = 0 - self._press_to_punch: str | ba.Lstr = ( + self._press_to_punch: str | bascenev1.Lstr = ( 'C' - if _ba.app.iircade_mode - else _ba.charstr(SpecialChar.LEFT_BUTTON) + if _babase.app.iircade_mode + else _babase.charstr(SpecialChar.LEFT_BUTTON) ) - self._press_to_bomb: str | ba.Lstr = ( + self._press_to_bomb: str | bascenev1.Lstr = ( 'B' - if _ba.app.iircade_mode - else _ba.charstr(SpecialChar.RIGHT_BUTTON) + if _babase.app.iircade_mode + else _babase.charstr(SpecialChar.RIGHT_BUTTON) ) self._joinmsg = Lstr(resource='pressAnyButtonToJoinText') can_switch_teams = len(lobby.sessionteams) > 1 @@ -50,13 +53,13 @@ class JoinInfo: # If we have a keyboard, grab keys for punch and pickup. # FIXME: This of course is only correct on the local device; # Should change this for net games. - keyboard = _ba.getinputdevice('Keyboard', '#1', doraise=False) + keyboard = _bascenev1.getinputdevice('Keyboard', '#1', doraise=False) if keyboard is not None: self._update_for_keyboard(keyboard) - flatness = 1.0 if _ba.app.vr_mode else 0.0 + flatness = 1.0 if _babase.app.vr_mode else 0.0 self._text = NodeActor( - _ba.newnode( + _bascenev1.newnode( 'text', attrs={ 'position': (0, -40), @@ -70,7 +73,7 @@ class JoinInfo: ) ) - if _ba.app.demo_mode or _ba.app.arcade_mode: + if _babase.app.demo_mode or _babase.app.arcade_mode: self._messages = [self._joinmsg] else: msg1 = Lstr( @@ -78,9 +81,9 @@ class JoinInfo: subs=[ ( '${BUTTONS}', - _ba.charstr(SpecialChar.UP_ARROW) + _babase.charstr(SpecialChar.UP_ARROW) + ' ' - + _ba.charstr(SpecialChar.DOWN_ARROW), + + _babase.charstr(SpecialChar.DOWN_ARROW), ) ], ) @@ -100,9 +103,9 @@ class JoinInfo: subs=[ ( '${BUTTONS}', - _ba.charstr(SpecialChar.LEFT_ARROW) + _babase.charstr(SpecialChar.LEFT_ARROW) + ' ' - + _ba.charstr(SpecialChar.RIGHT_ARROW), + + _babase.charstr(SpecialChar.RIGHT_ARROW), ) ], ) @@ -115,13 +118,16 @@ class JoinInfo: + [self._joinmsg] ) - self._timer = _ba.Timer(4.0, WeakCall(self._update), repeat=True) + self._timer = _bascenev1.Timer(4.0, WeakCall(self._update), repeat=True) - def _update_for_keyboard(self, keyboard: ba.InputDevice) -> None: - from ba import _input + def _update_for_keyboard(self, keyboard: bascenev1.InputDevice) -> None: + from baclassic import _input + + classic = _babase.app.classic + assert classic is not None punch_key = keyboard.get_button_name( - _input.get_device_value(keyboard, 'buttonPunch') + classic.get_input_device_mapped_value(keyboard, 'buttonPunch') ) self._press_to_punch = Lstr( resource='orText', @@ -131,7 +137,7 @@ class JoinInfo: ], ) bomb_key = keyboard.get_button_name( - _input.get_device_value(keyboard, 'buttonBomb') + classic.get_input_device_mapped_value(keyboard, 'buttonBomb') ) self._press_to_bomb = Lstr( resource='orText', @@ -158,7 +164,7 @@ class JoinInfo: class PlayerReadyMessage: """Tells an object a player has been selected from the given chooser.""" - chooser: ba.Chooser + chooser: baclassic.Chooser @dataclass @@ -170,32 +176,34 @@ class ChangeMessage: class Chooser: - """A character/team selector for a ba.Player. + """A character/team selector for a bascenev1.Player. Category: Gameplay Classes """ def __del__(self) -> None: - # Just kill off our base node; the rest should go down with it. if self._text_node: self._text_node.delete() def __init__( - self, vpos: float, sessionplayer: _ba.SessionPlayer, lobby: 'Lobby' + self, + vpos: float, + sessionplayer: bascenev1.SessionPlayer, + lobby: 'Lobby', ) -> None: - self._deek_sound = _ba.getsound('deek') - self._click_sound = _ba.getsound('click01') - self._punchsound = _ba.getsound('punch01') - self._swish_sound = _ba.getsound('punchSwish') - self._errorsound = _ba.getsound('error') - self._mask_texture = _ba.gettexture('characterIconMask') + self._deek_sound = _bascenev1.getsound('deek') + self._click_sound = _bascenev1.getsound('click01') + self._punchsound = _bascenev1.getsound('punch01') + self._swish_sound = _bascenev1.getsound('punchSwish') + self._errorsound = _bascenev1.getsound('error') + self._mask_texture = _bascenev1.gettexture('characterIconMask') self._vpos = vpos self._lobby = weakref.ref(lobby) self._sessionplayer = sessionplayer self._inited = False self._dead = False - self._text_node: ba.Node | None = None + self._text_node: bascenev1.Node | None = None self._profilename = '' self._profilenames: list[str] = [] self._ready: bool = False @@ -203,7 +211,8 @@ class Chooser: self._last_change: Sequence[float | int] = (0, 0) self._profiles: dict[str, dict[str, Any]] = {} - app = _ba.app + app = _babase.app + assert app.classic is not None # Load available player profiles either from the local config or # from the remote device. @@ -224,7 +233,7 @@ class Chooser: # To calc our random character we pick a random one out of our # unlocked list and then locate that character's index in the full # list. - char_index_offset = app.lobby_random_char_index_offset + char_index_offset = app.classic.lobby_random_char_index_offset self._random_character_index = ( sessionplayer.inputdevice.id + char_index_offset ) % len(self._character_names) @@ -234,7 +243,7 @@ class Chooser: self._profileindex = self._select_initial_profile() self._profilename = self._profilenames[self._profileindex] - self._text_node = _ba.newnode( + self._text_node = _bascenev1.newnode( 'text', delegate=self, attrs={ @@ -248,7 +257,7 @@ class Chooser: }, ) animate(self._text_node, 'scale', {0: 0, 0.1: 1.0}) - self.icon = _ba.newnode( + self.icon = _bascenev1.newnode( 'image', owner=self._text_node, attrs={ @@ -279,7 +288,8 @@ class Chooser: self._set_ready(False) def _select_initial_profile(self) -> int: - app = _ba.app + app = _babase.app + assert app.classic is not None profilenames = self._profilenames inputdevice = self._sessionplayer.inputdevice @@ -296,9 +306,9 @@ class Chooser: if ( dprofilename == '__account__' and not inputdevice.is_remote_client - and app.lobby_account_profile_device_id is None + and app.classic.lobby_account_profile_device_id is None ): - app.lobby_account_profile_device_id = inputdevice.id + app.classic.lobby_account_profile_device_id = inputdevice.id return profilenames.index(dprofilename) # We want to mark the first local input-device in the game @@ -308,15 +318,15 @@ class Chooser: and not inputdevice.is_controller_app ): if ( - app.lobby_account_profile_device_id is None + app.classic.lobby_account_profile_device_id is None and '__account__' in profilenames ): - app.lobby_account_profile_device_id = inputdevice.id + app.classic.lobby_account_profile_device_id = inputdevice.id # If this is the designated account-profile-device, try to default # to the account profile. if ( - inputdevice.id == app.lobby_account_profile_device_id + inputdevice.id == app.classic.lobby_account_profile_device_id and '__account__' in profilenames ): return profilenames.index('__account__') @@ -335,24 +345,24 @@ class Chooser: # Cycle through our non-random profiles once; after # that, everyone gets random. - while app.lobby_random_profile_index < len( + while app.classic.lobby_random_profile_index < len( profilenames - ) and profilenames[app.lobby_random_profile_index] in ( + ) and profilenames[app.classic.lobby_random_profile_index] in ( '_random', '__account__', '_edit', ): - app.lobby_random_profile_index += 1 - if app.lobby_random_profile_index < len(profilenames): - profileindex = app.lobby_random_profile_index - app.lobby_random_profile_index += 1 + app.classic.lobby_random_profile_index += 1 + if app.classic.lobby_random_profile_index < len(profilenames): + profileindex = app.classic.lobby_random_profile_index + app.classic.lobby_random_profile_index += 1 return profileindex assert '_random' in profilenames return profilenames.index('_random') @property - def sessionplayer(self) -> ba.SessionPlayer: - """The ba.SessionPlayer associated with this chooser.""" + def sessionplayer(self) -> bascenev1.SessionPlayer: + """The bascenev1.SessionPlayer associated with this chooser.""" return self._sessionplayer @property @@ -369,24 +379,25 @@ class Chooser: self._dead = val @property - def sessionteam(self) -> ba.SessionTeam: - """Return this chooser's currently selected ba.SessionTeam.""" + def sessionteam(self) -> bascenev1.SessionTeam: + """Return this chooser's currently selected bascenev1.SessionTeam.""" return self.lobby.sessionteams[self._selected_team_index] @property - def lobby(self) -> ba.Lobby: - """The chooser's ba.Lobby.""" + def lobby(self) -> baclassic.Lobby: + """The chooser's baclassic.Lobby.""" lobby = self._lobby() if lobby is None: raise NotFoundError('Lobby does not exist.') return lobby - def get_lobby(self) -> ba.Lobby | None: + def get_lobby(self) -> baclassic.Lobby | None: """Return this chooser's lobby if it still exists; otherwise None.""" return self._lobby() def update_from_profile(self) -> None: """Set character/colors based on the current profile.""" + assert _babase.app.classic is not None self._profilename = self._profilenames[self._profileindex] if self._profilename == '_edit': pass @@ -407,7 +418,7 @@ class Chooser: # so no exploit opportunities) if ( character not in self._character_names - and character in _ba.app.spaz_appearances + and character in _babase.app.classic.spaz_appearances ): self._character_names.append(character) self._character_index = self._character_names.index(character) @@ -419,9 +430,10 @@ class Chooser: def reload_profiles(self) -> None: """Reload all player profiles.""" - from ba._general import json_prep + from babase._general import json_prep - app = _ba.app + app = _babase.app + assert app.classic is not None # Re-construct our profile index and other stuff since the profile # list might have changed. @@ -452,7 +464,10 @@ class Chooser: # Filter out any characters we're unaware of. for profile in list(self._profiles.items()): - if profile[1].get('character', '') not in app.spaz_appearances: + if ( + profile[1].get('character', '') + not in app.classic.spaz_appearances + ): profile[1]['character'] = 'Spaz' # Add in a random one so we're ok even if there's no user profiles. @@ -549,7 +564,7 @@ class Chooser: icon = ( self._profiles[name_raw]['icon'] if 'icon' in self._profiles[name_raw] - else _ba.charstr(SpecialChar.LOGO) + else _babase.charstr(SpecialChar.LOGO) ) name = icon + name except Exception: @@ -567,18 +582,18 @@ class Chooser: def _set_ready(self, ready: bool) -> None: # pylint: disable=cyclic-import from bastd.ui.profile import browser as pbrowser - from ba._general import Call + from babase._general import Call profilename = self._profilenames[self._profileindex] # Handle '_edit' as a special case. if profilename == '_edit' and ready: - with _ba.Context('ui'): + with _babase.ContextRef.empty(): pbrowser.ProfileBrowserWindow(in_main_menu=False) # Give their input-device UI ownership too # (prevent someone else from snatching it in crowded games) - _ba.set_ui_input_device(self._sessionplayer.inputdevice) + _babase.set_ui_input_device(self._sessionplayer.inputdevice.id) return if not ready: @@ -640,7 +655,7 @@ class Chooser: input_device = self._sessionplayer.inputdevice name = input_device.name unique_id = input_device.unique_identifier - device_profiles = _ba.app.config.setdefault( + device_profiles = _babase.app.config.setdefault( 'Default Player Profiles', {} ) @@ -656,7 +671,7 @@ class Chooser: del device_profiles[profilekey] else: device_profiles[profilekey] = profilename - _ba.app.config.commit() + _babase.app.config.commit() # Set this player's short and full name. self._sessionplayer.setname( @@ -666,7 +681,7 @@ class Chooser: self._update_text() # Inform the session that this player is ready. - _ba.getsession().handlemessage(PlayerReadyMessage(self)) + _bascenev1.getsession().handlemessage(PlayerReadyMessage(self)) def _handle_ready_msg(self, ready: bool) -> None: force_team_switch = False @@ -674,11 +689,10 @@ class Chooser: # Team auto-balance kicks us to another team if we try to # join the team with the most players. if not self._ready: - if _ba.app.config.get('Auto Balance Teams', False): + if _babase.app.config.get('Auto Balance Teams', False): lobby = self.lobby sessionteams = lobby.sessionteams if len(sessionteams) > 1: - # First, calc how many players are on each team # ..we need to count both active players and # choosers that have been marked as ready. @@ -704,20 +718,22 @@ class Chooser: # Either force switch teams, or actually for realsies do the set-ready. if force_team_switch: - _ba.playsound(self._errorsound) + self._errorsound.play() self.handlemessage(ChangeMessage('team', 1)) else: - _ba.playsound(self._punchsound) + self._punchsound.play() self._set_ready(ready) # TODO: should handle this at the engine layer so this is unnecessary. def _handle_repeat_message_attack(self) -> None: - now = _ba.time() + now = _babase.apptime() count = self._last_change[1] if now - self._last_change[0] < QUICK_CHANGE_INTERVAL: count += 1 if count > MAX_QUICK_CHANGE_COUNT: - _ba.disconnect_client(self._sessionplayer.inputdevice.client_id) + _bascenev1.disconnect_client( + self._sessionplayer.inputdevice.client_id + ) elif now - self._last_change[0] > QUICK_CHANGE_RESET_INTERVAL: count = 0 self._last_change = (now, count) @@ -740,7 +756,7 @@ class Chooser: if msg.what == 'team': sessionteams = self.lobby.sessionteams if len(sessionteams) > 1: - _ba.playsound(self._swish_sound) + self._swish_sound.play() self._selected_team_index = ( self._selected_team_index + msg.value ) % len(sessionteams) @@ -750,22 +766,20 @@ class Chooser: elif msg.what == 'profileindex': if len(self._profilenames) == 1: - # This should be pretty hard to hit now with # automatic local accounts. - _ba.playsound(_ba.getsound('error')) + _bascenev1.getsound('error').play() else: - # Pick the next player profile and assign our name # and character based on that. - _ba.playsound(self._deek_sound) + self._deek_sound.play() self._profileindex = (self._profileindex + msg.value) % len( self._profilenames ) self.update_from_profile() elif msg.what == 'character': - _ba.playsound(self._click_sound) + self._click_sound.play() # update our index in our local list of characters self._character_index = ( self._character_index + msg.value @@ -779,7 +793,6 @@ class Chooser: def _update_text(self) -> None: assert self._text_node is not None if self._ready: - # Once we're ready, we've saved the name, so lets ask the system # for it so we get appended numbers and stuff. text = Lstr(value=self._sessionplayer.getname(full=True)) @@ -793,7 +806,7 @@ class Chooser: can_switch_teams = len(self.lobby.sessionteams) > 1 # Flash as we're coming in. - fin_color = _ba.safecolor(self.get_color()) + (1,) + fin_color = _babase.safecolor(self.get_color()) + (1,) if not self._inited: animate_array( self._text_node, @@ -802,7 +815,6 @@ class Chooser: {0.15: fin_color, 0.25: (2, 2, 2, 1), 0.35: fin_color}, ) else: - # Blend if we're in teams mode; switch instantly otherwise. if can_switch_teams: animate_array( @@ -839,7 +851,6 @@ class Chooser: if self.lobby.use_team_colors: for i, sessionteam in enumerate(self.lobby.sessionteams): if i != self._selected_team_index: - # Find the dominant component of this sessionteam's color # and adjust ours so that the component is # not super-dominant. @@ -861,14 +872,15 @@ class Chooser: highlight[(max_index + 2) % 3] += diff * 0.2 return highlight - def getplayer(self) -> ba.SessionPlayer: + def getplayer(self) -> bascenev1.SessionPlayer: """Return the player associated with this chooser.""" return self._sessionplayer def _update_icon(self) -> None: + assert _babase.app.classic is not None if self._profilenames[self._profileindex] == '_edit': - tex = _ba.gettexture('black') - tint_tex = _ba.gettexture('black') + tex = _bascenev1.gettexture('black') + tint_tex = _bascenev1.gettexture('black') self.icon.color = (1, 1, 1) self.icon.texture = tex self.icon.tint_texture = tint_tex @@ -876,10 +888,10 @@ class Chooser: return try: - tex_name = _ba.app.spaz_appearances[ + tex_name = _babase.app.classic.spaz_appearances[ self._character_names[self._character_index] ].icon_texture - tint_tex_name = _ba.app.spaz_appearances[ + tint_tex_name = _babase.app.classic.spaz_appearances[ self._character_names[self._character_index] ].icon_mask_texture except Exception: @@ -887,8 +899,8 @@ class Chooser: tex_name = 'neoSpazIcon' tint_tex_name = 'neoSpazIconColorMask' - tex = _ba.gettexture(tex_name) - tint_tex = _ba.gettexture(tint_tex_name) + tex = _bascenev1.gettexture(tex_name) + tint_tex = _bascenev1.gettexture(tint_tex_name) self.icon.color = (1, 1, 1) self.icon.texture = tex @@ -921,13 +933,12 @@ class Chooser: class Lobby: - """Container for ba.Choosers. + """Container for baclassic.Choosers. Category: Gameplay Classes """ def __del__(self) -> None: - # Reset any players that still have a chooser in us. # (should allow the choosers to die). sessionplayers = [ @@ -937,10 +948,10 @@ class Lobby: sessionplayer.resetinput() def __init__(self) -> None: - from ba._team import SessionTeam - from ba._coopsession import CoopSession + from bascenev1._team import SessionTeam + from bascenev1._coopsession import CoopSession - session = _ba.getsession() + session = _bascenev1.getsession() self._use_team_colors = session.use_team_colors if session.use_teams: self._sessionteams = [ @@ -976,8 +987,8 @@ class Lobby: return self._use_team_colors @property - def sessionteams(self) -> list[ba.SessionTeam]: - """ba.SessionTeams available in this lobby.""" + def sessionteams(self) -> list[bascenev1.SessionTeam]: + """bascenev1.SessionTeams available in this lobby.""" allteams = [] for tref in self._sessionteams: team = tref() @@ -1002,13 +1013,15 @@ class Lobby: # pylint: disable=cyclic-import from bastd.actor.spazappearance import get_appearances + assert _babase.app.classic is not None + # We may have gained or lost character names if the user # bought something; reload these too. self.character_names_local_unlocked = get_appearances() self.character_names_local_unlocked.sort(key=lambda x: x.lower()) # Do any overall prep we need to such as creating account profile. - _ba.app.accounts_v1.ensure_have_account_player_profile() + _babase.app.classic.accounts.ensure_have_account_player_profile() for chooser in self.choosers: try: chooser.reload_profiles() @@ -1028,7 +1041,7 @@ class Lobby: """Return whether all choosers are marked ready.""" return all(chooser.ready for chooser in self.choosers) - def add_chooser(self, sessionplayer: ba.SessionPlayer) -> None: + def add_chooser(self, sessionplayer: bascenev1.SessionPlayer) -> None: """Add a chooser to the lobby for the provided player.""" self.choosers.append( Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self) @@ -1038,7 +1051,7 @@ class Lobby: ) self._vpos -= 48 - def remove_chooser(self, player: ba.SessionPlayer) -> None: + def remove_chooser(self, player: bascenev1.SessionPlayer) -> None: """Remove a single player's chooser; does not kick them. This is used when a player enters the game and no longer diff --git a/assets/src/ba_data/python/ba/_music.py b/src/assets/ba_data/python/baclassic/_music.py similarity index 81% rename from assets/src/ba_data/python/ba/_music.py rename to src/assets/ba_data/python/baclassic/_music.py index 79693c4e..7059d6d0 100644 --- a/assets/src/ba_data/python/ba/_music.py +++ b/src/assets/ba_data/python/baclassic/_music.py @@ -8,45 +8,15 @@ from typing import TYPE_CHECKING from dataclasses import dataclass from enum import Enum -import _ba +import _babase +import _bascenev1 +from bascenev1._music import MusicType if TYPE_CHECKING: from typing import Callable, Any - import ba - -class MusicType(Enum): - """Types of music available to play in-game. - - Category: **Enums** - - These do not correspond to specific pieces of music, but rather to - 'situations'. The actual music played for each type can be overridden - by the game or by the user. - """ - - MENU = 'Menu' - VICTORY = 'Victory' - CHAR_SELECT = 'CharSelect' - RUN_AWAY = 'RunAway' - ONSLAUGHT = 'Onslaught' - KEEP_AWAY = 'Keep Away' - RACE = 'Race' - EPIC_RACE = 'Epic Race' - SCORES = 'Scores' - GRAND_ROMP = 'GrandRomp' - TO_THE_DEATH = 'ToTheDeath' - CHOSEN_ONE = 'Chosen One' - FORWARD_MARCH = 'ForwardMarch' - FLAG_CATCHER = 'FlagCatcher' - SURVIVAL = 'Survival' - EPIC = 'Epic' - SPORTS = 'Sports' - HOCKEY = 'Hockey' - FOOTBALL = 'Football' - FLYING = 'Flying' - SCARY = 'Scary' - MARCHING = 'Marching' + import babase + import baclassic class MusicPlayMode(Enum): @@ -118,7 +88,8 @@ class MusicSubsystem: def __init__(self) -> None: # pylint: disable=cyclic-import - self._music_node: _ba.Node | None = None + # self._music_node: _bascenev1.Node | None = None + self._playing_internal_music = False self._music_mode: MusicPlayMode = MusicPlayMode.REGULAR self._music_player: MusicPlayer | None = None self._music_player_type: type[MusicPlayer] | None = None @@ -133,29 +104,29 @@ class MusicSubsystem: # Our standard asset playback should probably just be one of them # instead of a special case. if self.supports_soundtrack_entry_type('musicFile'): - from ba.osmusic import OSMusicPlayer + from baclassic.osmusic import OSMusicPlayer self._music_player_type = OSMusicPlayer elif self.supports_soundtrack_entry_type('iTunesPlaylist'): - from ba.macmusicapp import MacMusicAppMusicPlayer + from baclassic.macmusicapp import MacMusicAppMusicPlayer self._music_player_type = MacMusicAppMusicPlayer - def on_app_launch(self) -> None: - """Should be called by app on_app_launch().""" + def on_app_launching(self) -> None: + """Should be called by app on_app_launching().""" # If we're using a non-default playlist, lets go ahead and get our # music-player going since it may hitch (better while we're faded # out than later). try: - cfg = _ba.app.config + cfg = _babase.app.config if 'Soundtrack' in cfg and cfg['Soundtrack'] not in [ '__default__', 'Default Soundtrack', ]: self.get_music_player() except Exception: - from ba import _error + from babase import _error _error.print_exception('error prepping music-player') @@ -188,7 +159,6 @@ class MusicSubsystem: old_mode = self._music_mode self._music_mode = mode if old_mode != self._music_mode or force_restart: - # If we're switching into test mode we don't # actually play anything until its requested. # If we're switching *out* of test mode though @@ -199,7 +169,7 @@ class MusicSubsystem: def supports_soundtrack_entry_type(self, entry_type: str) -> bool: """Return whether provided soundtrack entry type is supported here.""" - uas = _ba.env()['user_agent_string'] + uas = _babase.env()['user_agent_string'] assert isinstance(uas, str) # FIXME: Generalize this. @@ -208,7 +178,7 @@ class MusicSubsystem: if entry_type in ('musicFile', 'musicFolder'): return ( 'android' in uas - and _ba.android_get_external_files_dir() is not None + and _babase.android_get_external_files_dir() is not None ) if entry_type == 'default': return True @@ -246,7 +216,7 @@ class MusicSubsystem: return entry_type raise ValueError('invalid soundtrack entry:' + str(entry)) except Exception: - from ba import _error + from babase import _error _error.print_exception() return 'default' @@ -272,14 +242,14 @@ class MusicSubsystem: return entry['name'] raise ValueError('invalid soundtrack entry:' + str(entry)) except Exception: - from ba import _error + from babase import _error _error.print_exception() return 'default' def on_app_resume(self) -> None: """Should be run when the app resumes from a suspended state.""" - if _ba.is_os_playing_music(): + if _babase.is_os_playing_music(): self.do_play_music(None) def do_play_music( @@ -305,8 +275,7 @@ class MusicSubsystem: print(f"Invalid music type: '{musictype}'") musictype = None - with _ba.Context('ui'): - + with _babase.ContextRef.empty(): # If they don't want to restart music and we're already # playing what's requested, we're done. if continuous and self.music_types[mode] is musictype: @@ -315,7 +284,7 @@ class MusicSubsystem: # If the OS tells us there's currently music playing, # all our operations default to playing nothing. - if _ba.is_os_playing_music(): + if _babase.is_os_playing_music(): musictype = None # If we're not in the mode this music is being set for, @@ -346,7 +315,7 @@ class MusicSubsystem: def _get_user_soundtrack(self) -> dict[str, Any]: """Return current user soundtrack or empty dict otherwise.""" - cfg = _ba.app.config + cfg = _babase.app.config soundtrack: dict[str, Any] = {} soundtrackname = cfg.get('Soundtrack') if soundtrackname is not None and soundtrackname != '__default__': @@ -358,44 +327,53 @@ class MusicSubsystem: return soundtrack def _play_music_player_music(self, entry: Any) -> None: - # Stop any existing internal music. - if self._music_node is not None: - self._music_node.delete() - self._music_node = None + # if self._music_node is not None: + # self._music_node.delete() + # self._music_node = None + if self._playing_internal_music: + _bascenev1.set_internal_music(None) + self._playing_internal_music = False # Do the thing. self.get_music_player().play(entry) def _play_internal_music(self, musictype: MusicType | None) -> None: - # Stop any existing music-player playback. if self._music_player is not None: self._music_player.stop() # Stop any existing internal music. - if self._music_node: - self._music_node.delete() - self._music_node = None + # if self._music_node: + # self._music_node.delete() + # self._music_node = None + if self._playing_internal_music: + _bascenev1.set_internal_music(None) + self._playing_internal_music = False # Start up new internal music. if musictype is not None: - entry = ASSET_SOUNDTRACK_ENTRIES.get(musictype) if entry is None: print(f"Unknown music: '{musictype}'") entry = ASSET_SOUNDTRACK_ENTRIES[MusicType.FLAG_CATCHER] - self._music_node = _ba.newnode( - type='sound', - attrs={ - 'sound': _ba.getsound(entry.assetname), - 'positional': False, - 'music': True, - 'volume': entry.volume * 5.0, - 'loop': entry.loop, - }, + # self._music_node = _bascenev1.newnode( + # type='sound', + # attrs={ + # 'sound': _bascenev1.getsound(entry.assetname), + # 'positional': False, + # 'music': True, + # 'volume': entry.volume * 5.0, + # 'loop': entry.loop, + # }, + # ) + _bascenev1.set_internal_music( + _babase.getsimplesound(entry.assetname), + volume=entry.volume * 5.0, + loop=entry.loop, ) + self._playing_internal_music = True class MusicPlayer: @@ -433,7 +411,7 @@ class MusicPlayer: def play(self, entry: Any) -> None: """Play provided entry.""" if not self._have_set_initial_volume: - self._volume = _ba.app.config.resolve('Music Volume') + self._volume = _babase.app.config.resolve('Music Volume') self.on_set_volume(self._volume) self._have_set_initial_volume = True self._entry_to_play = copy.deepcopy(entry) @@ -482,46 +460,18 @@ class MusicPlayer: """Called on final app shutdown.""" def _update_play_state(self) -> None: - # If we aren't playing, should be, and have positive volume, do so. if not self._actually_playing: if self._entry_to_play is not None and self._volume > 0.0: self.on_play(self._entry_to_play) self._actually_playing = True else: - if self._actually_playing and ( - self._entry_to_play is None or self._volume <= 0.0 - ): + if self._entry_to_play is None or self._volume <= 0.0: self.on_stop() self._actually_playing = False -def setmusic(musictype: ba.MusicType | None, continuous: bool = False) -> None: - """Set the app to play (or stop playing) a certain type of music. - - category: **Gameplay Functions** - - This function will handle loading and playing sound assets as necessary, - and also supports custom user soundtracks on specific platforms so the - user can override particular game music with their own. - - Pass None to stop music. - - if 'continuous' is True and musictype is the same as what is already - playing, the playing track will not be restarted. - """ - - # All we do here now is set a few music attrs on the current globals - # node. The foreground globals' current playing music then gets fed to - # the do_play_music call in our music controller. This way we can - # seamlessly support custom soundtracks in replays/etc since we're being - # driven purely by node data. - gnode = _ba.getactivity().globalsnode - gnode.music_continuous = continuous - gnode.music = '' if musictype is None else musictype.value - gnode.music_count += 1 - - def do_play_music(*args: Any, **keywds: Any) -> None: """A passthrough used by the C++ layer.""" - _ba.app.music.do_play_music(*args, **keywds) + assert _babase.app.classic is not None + _babase.app.classic.music.do_play_music(*args, **keywds) diff --git a/assets/src/ba_data/python/ba/_net.py b/src/assets/ba_data/python/baclassic/_net.py similarity index 53% rename from assets/src/ba_data/python/ba/_net.py rename to src/assets/ba_data/python/baclassic/_net.py index 2a0d4217..2241d967 100644 --- a/assets/src/ba_data/python/ba/_net.py +++ b/src/assets/ba_data/python/baclassic/_net.py @@ -3,14 +3,15 @@ """Networking related functionality.""" from __future__ import annotations -import ssl import copy import threading import weakref from enum import Enum from typing import TYPE_CHECKING -import _ba +import _babase +import _bascenev1 +from babase.internal import DEFAULT_REQUEST_TIMEOUT_SECONDS if TYPE_CHECKING: from typing import Any, Callable @@ -18,80 +19,15 @@ if TYPE_CHECKING: MasterServerCallback = Callable[[None | dict[str, Any]], None] -# Timeout for standard functions talking to the master-server/etc. -DEFAULT_REQUEST_TIMEOUT_SECONDS = 60 - - -class NetworkSubsystem: - """Network related app subsystem.""" - - def __init__(self) -> None: - - # Anyone accessing/modifying zone_pings should hold this lock, - # as it is updated by a background thread. - self.zone_pings_lock = threading.Lock() - - # Zone IDs mapped to average pings. This will remain empty - # until enough pings have been run to be reasonably certain - # that a nearby server has been pinged. - self.zone_pings: dict[str, float] = {} - - self._sslcontext: ssl.SSLContext | None = None - - # For debugging. - self.v1_test_log: str = '' - self.v1_ctest_results: dict[int, str] = {} - self.server_time_offset_hours: float | None = None - - @property - def sslcontext(self) -> ssl.SSLContext: - """Create/return our shared SSLContext. - - This can be reused for all standard urllib requests/etc. - """ - # Note: I've run into older Android devices taking upwards of 1 second - # to put together a default SSLContext, so recycling one can definitely - # be a worthwhile optimization. This was suggested to me in this - # thread by one of Python's SSL maintainers: - # https://github.com/python/cpython/issues/94637 - if self._sslcontext is None: - self._sslcontext = ssl.create_default_context() - return self._sslcontext - - -def get_ip_address_type(addr: str) -> socket.AddressFamily: - """Return socket.AF_INET6 or socket.AF_INET4 for the provided address.""" - import socket - - socket_type = None - - # First try it as an ipv4 address. - try: - socket.inet_pton(socket.AF_INET, addr) - socket_type = socket.AF_INET - except OSError: - pass - - # Hmm apparently not ipv4; try ipv6. - if socket_type is None: - try: - socket.inet_pton(socket.AF_INET6, addr) - socket_type = socket.AF_INET6 - except OSError: - pass - if socket_type is None: - raise ValueError(f'addr seems to be neither v4 or v6: {addr}') - return socket_type - class MasterServerResponseType(Enum): - """How to interpret responses from the master-server.""" + """How to interpret responses from the v1 master-server.""" JSON = 0 -class MasterServerCallThread(threading.Thread): - """Thread to communicate with the master-server.""" +class MasterServerV1CallThread(threading.Thread): + """Thread to communicate with the v1 master-server.""" def __init__( self, @@ -109,10 +45,10 @@ class MasterServerCallThread(threading.Thread): self._response_type = response_type self._data = {} if data is None else copy.deepcopy(data) self._callback: MasterServerCallback | None = callback - self._context = _ba.Context('current') + self._context = _babase.ContextRef() # Save and restore the context we were created from. - activity = _ba.getactivity(doraise=False) + activity = _bascenev1.getactivity(doraise=False) self._activity = weakref.ref(activity) if activity is not None else None def _run_callback(self, arg: None | dict[str, Any]) -> None: @@ -140,17 +76,19 @@ class MasterServerCallThread(threading.Thread): import json from efro.error import is_urllib_communication_error - from ba._general import Call, utf8_all - from ba._internal import get_master_server_address + from babase._general import Call, utf8_all + plus = _babase.app.plus + assert plus is not None response_data: Any = None url: str | None = None try: + assert _babase.app.classic is not None self._data = utf8_all(self._data) - _ba.set_thread_name('BA_ServerCallThread') + _babase.set_thread_name('BA_ServerCallThread') if self._request_type == 'get': url = ( - get_master_server_address() + plus.get_master_server_address() + '/' + self._request + '?' @@ -158,20 +96,22 @@ class MasterServerCallThread(threading.Thread): ) response = urllib.request.urlopen( urllib.request.Request( - url, None, {'User-Agent': _ba.app.user_agent_string} + url, + None, + {'User-Agent': _babase.app.classic.user_agent_string}, ), - context=_ba.app.net.sslcontext, + context=_babase.app.net.sslcontext, timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS, ) elif self._request_type == 'post': - url = get_master_server_address() + '/' + self._request + url = plus.get_master_server_address() + '/' + self._request response = urllib.request.urlopen( urllib.request.Request( url, urllib.parse.urlencode(self._data).encode(), - {'User-Agent': _ba.app.user_agent_string}, + {'User-Agent': _babase.app.classic.user_agent_string}, ), - context=_ba.app.net.sslcontext, + context=_babase.app.net.sslcontext, timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS, ) else: @@ -192,7 +132,6 @@ class MasterServerCallThread(threading.Thread): raise TypeError(f'invalid responsetype: {self._response_type}') except Exception as exc: - # Ignore common network errors; note unexpected ones. if not is_urllib_communication_error(exc, url=url): print( @@ -208,30 +147,6 @@ class MasterServerCallThread(threading.Thread): response_data = None if self._callback is not None: - _ba.pushcall( + _babase.pushcall( Call(self._run_callback, response_data), from_other_thread=True ) - - -def master_server_get( - request: str, - data: dict[str, Any], - callback: MasterServerCallback | None = None, - response_type: MasterServerResponseType = MasterServerResponseType.JSON, -) -> None: - """Make a call to the master server via a http GET.""" - MasterServerCallThread( - request, 'get', data, callback, response_type - ).start() - - -def master_server_post( - request: str, - data: dict[str, Any], - callback: MasterServerCallback | None = None, - response_type: MasterServerResponseType = MasterServerResponseType.JSON, -) -> None: - """Make a call to the master server via a http POST.""" - MasterServerCallThread( - request, 'post', data, callback, response_type - ).start() diff --git a/assets/src/ba_data/python/ba/_profile.py b/src/assets/ba_data/python/baclassic/_profile.py similarity index 91% rename from assets/src/ba_data/python/ba/_profile.py rename to src/assets/ba_data/python/baclassic/_profile.py index 2a24d980..defa9c14 100644 --- a/assets/src/ba_data/python/ba/_profile.py +++ b/src/assets/ba_data/python/baclassic/_profile.py @@ -6,7 +6,7 @@ from __future__ import annotations import random from typing import TYPE_CHECKING -import _ba +import _babase if TYPE_CHECKING: from typing import Any @@ -43,9 +43,9 @@ def get_player_profile_icon(profilename: str) -> str: (non-account profiles only) """ - from ba._generated.enums import SpecialChar + from babase._mgen.enums import SpecialChar - appconfig = _ba.app.config + appconfig = _babase.app.config icon: str try: is_global = appconfig['Player Profiles'][profilename]['global'] @@ -55,7 +55,7 @@ def get_player_profile_icon(profilename: str) -> str: try: icon = appconfig['Player Profiles'][profilename]['icon'] except KeyError: - icon = _ba.charstr(SpecialChar.LOGO) + icon = _babase.charstr(SpecialChar.LOGO) else: icon = '' return icon @@ -65,13 +65,15 @@ def get_player_profile_colors( profilename: str | None, profiles: dict[str, dict[str, Any]] | None = None ) -> tuple[tuple[float, float, float], tuple[float, float, float]]: """Given a profile, return colors for them.""" - appconfig = _ba.app.config + appconfig = _babase.app.config if profiles is None: profiles = appconfig['Player Profiles'] # Special case: when being asked for a random color in kiosk mode, # always return default purple. - if (_ba.app.demo_mode or _ba.app.arcade_mode) and profilename is None: + if ( + _babase.app.demo_mode or _babase.app.arcade_mode + ) and profilename is None: color = (0.5, 0.4, 1.0) highlight = (0.4, 0.4, 0.5) else: diff --git a/assets/src/ba_data/python/ba/_servermode.py b/src/assets/ba_data/python/baclassic/_servermode.py similarity index 78% rename from assets/src/ba_data/python/ba/_servermode.py rename to src/assets/ba_data/python/baclassic/_servermode.py index 52f433fa..c2ae8498 100644 --- a/assets/src/ba_data/python/ba/_servermode.py +++ b/src/assets/ba_data/python/baclassic/_servermode.py @@ -19,17 +19,14 @@ from bacommon.servermanager import ( ClientListCommand, KickCommand, ) -import _ba -from ba._internal import add_transaction, run_transactions, get_v1_account_state -from ba._generated.enums import TimeType -from ba._freeforallsession import FreeForAllSession -from ba._dualteamsession import DualTeamSession -from ba._coopsession import CoopSession +import _babase +import bascenev1 if TYPE_CHECKING: from typing import Any - import ba + import babase + import baclassic from bacommon.servermanager import ServerConfig @@ -37,33 +34,35 @@ def _cmd(command_data: bytes) -> None: """Handle commands coming in from our server manager parent process.""" import pickle + assert _babase.app.classic is not None + command = pickle.loads(command_data) assert isinstance(command, ServerCommand) if isinstance(command, StartServerModeCommand): - assert _ba.app.server is None - _ba.app.server = ServerController(command.config) + assert _babase.app.classic.server is None + _babase.app.classic.server = ServerController(command.config) return if isinstance(command, ShutdownCommand): - assert _ba.app.server is not None - _ba.app.server.shutdown( + assert _babase.app.classic.server is not None + _babase.app.classic.server.shutdown( reason=command.reason, immediate=command.immediate ) return if isinstance(command, ChatMessageCommand): - assert _ba.app.server is not None - _ba.chatmessage(command.message, clients=command.clients) + assert _babase.app.classic.server is not None + bascenev1.chatmessage(command.message, clients=command.clients) return if isinstance(command, ScreenMessageCommand): - assert _ba.app.server is not None + assert _babase.app.classic.server is not None # Note: we have to do transient messages if # clients is specified, so they won't show up # in replays. - _ba.screenmessage( + bascenev1.screenmessage( command.message, color=command.color, clients=command.clients, @@ -72,13 +71,13 @@ def _cmd(command_data: bytes) -> None: return if isinstance(command, ClientListCommand): - assert _ba.app.server is not None - _ba.app.server.print_client_list() + assert _babase.app.classic.server is not None + _babase.app.classic.server.print_client_list() return if isinstance(command, KickCommand): - assert _ba.app.server is not None - _ba.app.server.kick( + assert _babase.app.classic.server is not None + _babase.app.classic.server.kick( client_id=command.client_id, ban_time=command.ban_time ) return @@ -96,11 +95,10 @@ class ServerController: """ def __init__(self, config: ServerConfig) -> None: - self._config = config self._playlist_name = '__default__' self._ran_access_check = False - self._prep_timer: ba.Timer | None = None + self._prep_timer: babase.AppTimer | None = None self._next_stuck_login_warn_time = time.time() + 10.0 self._first_run = True self._shutdown_reason: ShutdownReason | None = None @@ -116,19 +114,16 @@ class ServerController: # Now sit around doing any pre-launch prep such as waiting for # account sign-in or fetching playlists; this will kick off the # session once done. - with _ba.Context('ui'): - self._prep_timer = _ba.Timer( - 0.25, - self._prepare_to_serve, - timetype=TimeType.REAL, - repeat=True, + with _babase.ContextRef.empty(): + self._prep_timer = _babase.AppTimer( + 0.25, self._prepare_to_serve, repeat=True ) def print_client_list(self) -> None: """Print info about all connected clients.""" import json - roster = _ba.get_game_roster() + roster = bascenev1.get_game_roster() title1 = 'Client ID' title2 = 'Account Name' title3 = 'Players' @@ -161,7 +156,7 @@ class ServerController: if ban_time is None: ban_time = 300 - _ba.disconnect_client(client_id=client_id, ban_time=ban_time) + bascenev1.disconnect_client(client_id=client_id, ban_time=ban_time) def shutdown(self, reason: ShutdownReason, immediate: bool) -> None: """Set the app to quit either now or at the next clean opportunity.""" @@ -177,7 +172,7 @@ class ServerController: ) def handle_transition(self) -> bool: - """Handle transitioning to a new ba.Session or quitting the app. + """Handle transitioning to a new bascenev1.Session or quitting the app. Will be called once at the end of an activity that is marked as a good 'end-point' (such as a final score screen). @@ -190,14 +185,14 @@ class ServerController: return False def _execute_shutdown(self) -> None: - from ba._language import Lstr + from babase._language import Lstr if self._executing_shutdown: return self._executing_shutdown = True timestrval = time.strftime('%c') if self._shutdown_reason is ShutdownReason.RESTARTING: - _ba.screenmessage( + _babase.screenmessage( Lstr(resource='internal.serverRestartingText'), color=(1, 0.5, 0.0), ) @@ -206,7 +201,7 @@ class ServerController: f' at {timestrval}.{Clr.RST}' ) else: - _ba.screenmessage( + _babase.screenmessage( Lstr(resource='internal.serverShuttingDownText'), color=(1, 0.5, 0.0), ) @@ -214,16 +209,16 @@ class ServerController: f'{Clr.SBLU}Exiting for server-shutdown' f' at {timestrval}.{Clr.RST}' ) - with _ba.Context('ui'): - _ba.timer(2.0, _ba.quit, timetype=TimeType.REAL) + with _babase.ContextRef.empty(): + _babase.apptimer(2.0, _babase.quit) def _run_access_check(self) -> None: """Check with the master server to see if we're likely joinable.""" - from ba._net import master_server_get + assert _babase.app.classic is not None - master_server_get( + _babase.app.classic.master_server_v1_get( 'bsAccessCheck', - {'port': _ba.get_game_port(), 'b': _ba.app.build_number}, + {'port': bascenev1.get_game_port(), 'b': _babase.app.build_number}, callback=self._access_check_response, ) @@ -262,9 +257,10 @@ class ServerController: def _prepare_to_serve(self) -> None: """Run in a timer to do prep before beginning to serve.""" - signed_in = get_v1_account_state() == 'signed_in' + plus = _babase.app.plus + assert plus is not None + signed_in = plus.get_v1_account_state() == 'signed_in' if not signed_in: - # Signing in to the local server account should not take long; # complain if it does... curtime = time.time() @@ -284,7 +280,7 @@ class ServerController: f'{Clr.SBLU}Requesting shared-playlist' f' {self._config.playlist_code}...{Clr.RST}' ) - add_transaction( + plus.add_v1_account_transaction( { 'type': 'IMPORT_PLAYLIST', 'code': str(self._config.playlist_code), @@ -292,7 +288,7 @@ class ServerController: }, callback=self._on_playlist_fetch_response, ) - run_transactions() + plus.run_v1_account_transactions() self._playlist_fetch_sent_request = True if self._playlist_fetch_got_response: @@ -301,7 +297,7 @@ class ServerController: if can_launch: self._prep_timer = None - _ba.pushcall(self._launch_server_session) + _babase.pushcall(self._launch_server_session) def _on_playlist_fetch_response( self, @@ -325,15 +321,15 @@ class ServerController: self._config.session_type = typename self._playlist_name = result['playlistName'] - def _get_session_type(self) -> type[ba.Session]: + def _get_session_type(self) -> type[bascenev1.Session]: # Convert string session type to the class. # Hmm should we just keep this as a string? if self._config.session_type == 'ffa': - return FreeForAllSession + return bascenev1.FreeForAllSession if self._config.session_type == 'teams': - return DualTeamSession + return bascenev1.DualTeamSession if self._config.session_type == 'coop': - return CoopSession + return bascenev1.CoopSession raise RuntimeError( f'Invalid session_type: "{self._config.session_type}"' ) @@ -341,11 +337,16 @@ class ServerController: def _launch_server_session(self) -> None: """Kick off a host-session based on the current server config.""" # pylint: disable=too-many-branches - app = _ba.app + # pylint: disable=too-many-statements + app = _babase.app + classic = app.classic + plus = app.plus + assert plus is not None + assert classic is not None appcfg = app.config sessiontype = self._get_session_type() - if get_v1_account_state() != 'signed_in': + if plus.get_v1_account_state() != 'signed_in': print( 'WARNING: launch_server_session() expects to run ' 'with a signed in server account' @@ -358,18 +359,18 @@ class ServerController: and self._config.playlist_inline is not None ): self._playlist_name = 'ServerModePlaylist' - if sessiontype is FreeForAllSession: + if sessiontype is bascenev1.FreeForAllSession: ptypename = 'Free-for-All' - elif sessiontype is DualTeamSession: + elif sessiontype is bascenev1.DualTeamSession: ptypename = 'Team Tournament' - elif sessiontype is CoopSession: + elif sessiontype is bascenev1.CoopSession: ptypename = 'Coop' else: raise RuntimeError(f'Unknown session type {sessiontype}') # Need to add this in a transaction instead of just setting # it directly or it will get overwritten by the master-server. - add_transaction( + plus.add_v1_account_transaction( { 'type': 'ADD_PLAYLIST', 'playlistType': ptypename, @@ -377,65 +378,64 @@ class ServerController: 'playlist': self._config.playlist_inline, } ) - run_transactions() + plus.run_v1_account_transactions() if self._first_run: curtimestr = time.strftime('%c') startupmsg = ( - f'{Clr.BLD}{Clr.BLU}{_ba.appnameupper()} {app.version}' + f'{Clr.BLD}{Clr.BLU}{_babase.appnameupper()} {app.version}' f' ({app.build_number})' f' entering server-mode {curtimestr}{Clr.RST}' ) logging.info(startupmsg) - if sessiontype is FreeForAllSession: + if sessiontype is bascenev1.FreeForAllSession: appcfg['Free-for-All Playlist Selection'] = self._playlist_name appcfg[ 'Free-for-All Playlist Randomize' ] = self._config.playlist_shuffle - elif sessiontype is DualTeamSession: + elif sessiontype is bascenev1.DualTeamSession: appcfg['Team Tournament Playlist Selection'] = self._playlist_name appcfg[ 'Team Tournament Playlist Randomize' ] = self._config.playlist_shuffle - elif sessiontype is CoopSession: - app.coop_session_args = { + elif sessiontype is bascenev1.CoopSession: + classic.coop_session_args = { 'campaign': self._config.coop_campaign, 'level': self._config.coop_level, } else: raise RuntimeError(f'Unknown session type {sessiontype}') - app.teams_series_length = self._config.teams_series_length - app.ffa_series_length = self._config.ffa_series_length + classic.teams_series_length = self._config.teams_series_length + classic.ffa_series_length = self._config.ffa_series_length - _ba.set_authenticate_clients(self._config.authenticate_clients) + bascenev1.set_authenticate_clients(self._config.authenticate_clients) - _ba.set_enable_default_kick_voting( + bascenev1.set_enable_default_kick_voting( self._config.enable_default_kick_voting ) - _ba.set_admins(self._config.admins) + bascenev1.set_admins(self._config.admins) # Call set-enabled last (will push state to the cloud). - _ba.set_public_party_max_size(self._config.max_party_size) - _ba.set_public_party_queue_enabled(self._config.enable_queue) - _ba.set_public_party_name(self._config.party_name) - _ba.set_public_party_stats_url(self._config.stats_url) - _ba.set_public_party_enabled(self._config.party_is_public) + bascenev1.set_public_party_max_size(self._config.max_party_size) + bascenev1.set_public_party_queue_enabled(self._config.enable_queue) + bascenev1.set_public_party_name(self._config.party_name) + bascenev1.set_public_party_stats_url(self._config.stats_url) + bascenev1.set_public_party_enabled(self._config.party_is_public) # And here.. we.. go. if self._config.stress_test_players is not None: # Special case: run a stress test. - from ba.internal import run_stress_test - - run_stress_test( + assert _babase.app.classic is not None + _babase.app.classic.run_stress_test( playlist_type='Random', playlist_name='__default__', player_count=self._config.stress_test_players, round_duration=30, ) else: - _ba.new_host_session(sessiontype) + bascenev1.new_host_session(sessiontype) # Run an access check if we're trying to make a public party. if not self._ran_access_check and self._config.party_is_public: diff --git a/src/assets/ba_data/python/baclassic/_store.py b/src/assets/ba_data/python/baclassic/_store.py new file mode 100644 index 00000000..5cbc764b --- /dev/null +++ b/src/assets/ba_data/python/baclassic/_store.py @@ -0,0 +1,558 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Store related functionality for classic mode.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +import _babase + +if TYPE_CHECKING: + from typing import Any + import babase + import baclassic + import bascenev1 + + +class StoreSubsystem: + """Wrangles classic store.""" + + def get_store_item(self, item: str) -> dict[str, Any]: + """(internal)""" + return self.get_store_items()[item] + + def get_store_item_name_translated(self, item_name: str) -> babase.Lstr: + """Return a babase.Lstr for a store item name.""" + # pylint: disable=cyclic-import + from babase import _language + from bascenev1 import _map + + item_info = self.get_store_item(item_name) + if item_name.startswith('characters.'): + return _language.Lstr( + translate=('characterNames', item_info['character']) + ) + if item_name in ['merch']: + return _language.Lstr(resource='merchText') + if item_name in ['upgrades.pro', 'pro']: + return _language.Lstr( + resource='store.bombSquadProNameText', + subs=[('${APP_NAME}', _language.Lstr(resource='titleText'))], + ) + if item_name.startswith('maps.'): + map_type: type[_map.Map] = item_info['map_type'] + return _map.get_map_display_string(map_type.name) + if item_name.startswith('games.'): + gametype: type[bascenev1.GameActivity] = item_info['gametype'] + return gametype.get_display_string() + if item_name.startswith('icons.'): + return _language.Lstr(resource='editProfileWindow.iconText') + raise ValueError('unrecognized item: ' + item_name) + + def get_store_item_display_size( + self, item_name: str + ) -> tuple[float, float]: + """(internal)""" + if item_name.startswith('characters.'): + return 340 * 0.6, 430 * 0.6 + if item_name in ['pro', 'upgrades.pro', 'merch']: + from babase._mgen.enums import UIScale + + assert _babase.app.classic is not None + return 650 * 0.9, 500 * ( + 0.72 + if ( + _babase.app.config.get('Merch Link') + and _babase.app.classic.ui.uiscale is UIScale.SMALL + ) + else 0.85 + ) + if item_name.startswith('maps.'): + return 510 * 0.6, 450 * 0.6 + if item_name.startswith('icons.'): + return 265 * 0.6, 250 * 0.6 + return 450 * 0.6, 450 * 0.6 + + def get_store_items(self) -> dict[str, dict]: + """Returns info about purchasable items. + + (internal) + """ + # pylint: disable=cyclic-import + from babase._mgen.enums import SpecialChar + from bastd import maps + + assert _babase.app.classic is not None + + if _babase.app.classic.store_items is None: + from bastd.game import ninjafight + from bastd.game import meteorshower + from bastd.game import targetpractice + from bastd.game import easteregghunt + + # IMPORTANT - need to keep this synced with the master server. + # (doing so manually for now) + _babase.app.classic.store_items = { + 'characters.kronk': {'character': 'Kronk'}, + 'characters.zoe': {'character': 'Zoe'}, + 'characters.jackmorgan': {'character': 'Jack Morgan'}, + 'characters.mel': {'character': 'Mel'}, + 'characters.snakeshadow': {'character': 'Snake Shadow'}, + 'characters.bones': {'character': 'Bones'}, + 'characters.bernard': { + 'character': 'Bernard', + 'highlight': (0.6, 0.5, 0.8), + }, + 'characters.pixie': {'character': 'Pixel'}, + 'characters.wizard': {'character': 'Grumbledorf'}, + 'characters.frosty': {'character': 'Frosty'}, + 'characters.pascal': {'character': 'Pascal'}, + 'characters.cyborg': {'character': 'B-9000'}, + 'characters.agent': {'character': 'Agent Johnson'}, + 'characters.taobaomascot': {'character': 'Taobao Mascot'}, + 'characters.santa': {'character': 'Santa Claus'}, + 'characters.bunny': {'character': 'Easter Bunny'}, + 'merch': {}, + 'pro': {}, + 'maps.lake_frigid': {'map_type': maps.LakeFrigid}, + 'games.ninja_fight': { + 'gametype': ninjafight.NinjaFightGame, + 'previewTex': 'courtyardPreview', + }, + 'games.meteor_shower': { + 'gametype': meteorshower.MeteorShowerGame, + 'previewTex': 'rampagePreview', + }, + 'games.target_practice': { + 'gametype': targetpractice.TargetPracticeGame, + 'previewTex': 'doomShroomPreview', + }, + 'games.easter_egg_hunt': { + 'gametype': easteregghunt.EasterEggHuntGame, + 'previewTex': 'towerDPreview', + }, + 'icons.flag_us': { + 'icon': _babase.charstr(SpecialChar.FLAG_UNITED_STATES) + }, + 'icons.flag_mexico': { + 'icon': _babase.charstr(SpecialChar.FLAG_MEXICO) + }, + 'icons.flag_germany': { + 'icon': _babase.charstr(SpecialChar.FLAG_GERMANY) + }, + 'icons.flag_brazil': { + 'icon': _babase.charstr(SpecialChar.FLAG_BRAZIL) + }, + 'icons.flag_russia': { + 'icon': _babase.charstr(SpecialChar.FLAG_RUSSIA) + }, + 'icons.flag_china': { + 'icon': _babase.charstr(SpecialChar.FLAG_CHINA) + }, + 'icons.flag_uk': { + 'icon': _babase.charstr(SpecialChar.FLAG_UNITED_KINGDOM) + }, + 'icons.flag_canada': { + 'icon': _babase.charstr(SpecialChar.FLAG_CANADA) + }, + 'icons.flag_india': { + 'icon': _babase.charstr(SpecialChar.FLAG_INDIA) + }, + 'icons.flag_japan': { + 'icon': _babase.charstr(SpecialChar.FLAG_JAPAN) + }, + 'icons.flag_france': { + 'icon': _babase.charstr(SpecialChar.FLAG_FRANCE) + }, + 'icons.flag_indonesia': { + 'icon': _babase.charstr(SpecialChar.FLAG_INDONESIA) + }, + 'icons.flag_italy': { + 'icon': _babase.charstr(SpecialChar.FLAG_ITALY) + }, + 'icons.flag_south_korea': { + 'icon': _babase.charstr(SpecialChar.FLAG_SOUTH_KOREA) + }, + 'icons.flag_netherlands': { + 'icon': _babase.charstr(SpecialChar.FLAG_NETHERLANDS) + }, + 'icons.flag_uae': { + 'icon': _babase.charstr( + SpecialChar.FLAG_UNITED_ARAB_EMIRATES + ) + }, + 'icons.flag_qatar': { + 'icon': _babase.charstr(SpecialChar.FLAG_QATAR) + }, + 'icons.flag_egypt': { + 'icon': _babase.charstr(SpecialChar.FLAG_EGYPT) + }, + 'icons.flag_kuwait': { + 'icon': _babase.charstr(SpecialChar.FLAG_KUWAIT) + }, + 'icons.flag_algeria': { + 'icon': _babase.charstr(SpecialChar.FLAG_ALGERIA) + }, + 'icons.flag_saudi_arabia': { + 'icon': _babase.charstr(SpecialChar.FLAG_SAUDI_ARABIA) + }, + 'icons.flag_malaysia': { + 'icon': _babase.charstr(SpecialChar.FLAG_MALAYSIA) + }, + 'icons.flag_czech_republic': { + 'icon': _babase.charstr(SpecialChar.FLAG_CZECH_REPUBLIC) + }, + 'icons.flag_australia': { + 'icon': _babase.charstr(SpecialChar.FLAG_AUSTRALIA) + }, + 'icons.flag_singapore': { + 'icon': _babase.charstr(SpecialChar.FLAG_SINGAPORE) + }, + 'icons.flag_iran': { + 'icon': _babase.charstr(SpecialChar.FLAG_IRAN) + }, + 'icons.flag_poland': { + 'icon': _babase.charstr(SpecialChar.FLAG_POLAND) + }, + 'icons.flag_argentina': { + 'icon': _babase.charstr(SpecialChar.FLAG_ARGENTINA) + }, + 'icons.flag_philippines': { + 'icon': _babase.charstr(SpecialChar.FLAG_PHILIPPINES) + }, + 'icons.flag_chile': { + 'icon': _babase.charstr(SpecialChar.FLAG_CHILE) + }, + 'icons.fedora': {'icon': _babase.charstr(SpecialChar.FEDORA)}, + 'icons.hal': {'icon': _babase.charstr(SpecialChar.HAL)}, + 'icons.crown': {'icon': _babase.charstr(SpecialChar.CROWN)}, + 'icons.yinyang': { + 'icon': _babase.charstr(SpecialChar.YIN_YANG) + }, + 'icons.eyeball': { + 'icon': _babase.charstr(SpecialChar.EYE_BALL) + }, + 'icons.skull': {'icon': _babase.charstr(SpecialChar.SKULL)}, + 'icons.heart': {'icon': _babase.charstr(SpecialChar.HEART)}, + 'icons.dragon': {'icon': _babase.charstr(SpecialChar.DRAGON)}, + 'icons.helmet': {'icon': _babase.charstr(SpecialChar.HELMET)}, + 'icons.mushroom': { + 'icon': _babase.charstr(SpecialChar.MUSHROOM) + }, + 'icons.ninja_star': { + 'icon': _babase.charstr(SpecialChar.NINJA_STAR) + }, + 'icons.viking_helmet': { + 'icon': _babase.charstr(SpecialChar.VIKING_HELMET) + }, + 'icons.moon': {'icon': _babase.charstr(SpecialChar.MOON)}, + 'icons.spider': {'icon': _babase.charstr(SpecialChar.SPIDER)}, + 'icons.fireball': { + 'icon': _babase.charstr(SpecialChar.FIREBALL) + }, + 'icons.mikirog': {'icon': _babase.charstr(SpecialChar.MIKIROG)}, + } + return _babase.app.classic.store_items + + def get_store_layout(self) -> dict[str, list[dict[str, Any]]]: + """Return what's available in the store at a given time. + + Categorized by tab and by section. + """ + plus = _babase.app.plus + classic = _babase.app.classic + + assert classic is not None + assert plus is not None + + if classic.store_layout is None: + classic.store_layout = { + 'characters': [{'items': []}], + 'extras': [{'items': ['pro']}], + 'maps': [{'items': ['maps.lake_frigid']}], + 'minigames': [], + 'icons': [ + { + 'items': [ + 'icons.mushroom', + 'icons.heart', + 'icons.eyeball', + 'icons.yinyang', + 'icons.hal', + 'icons.flag_us', + 'icons.flag_mexico', + 'icons.flag_germany', + 'icons.flag_brazil', + 'icons.flag_russia', + 'icons.flag_china', + 'icons.flag_uk', + 'icons.flag_canada', + 'icons.flag_india', + 'icons.flag_japan', + 'icons.flag_france', + 'icons.flag_indonesia', + 'icons.flag_italy', + 'icons.flag_south_korea', + 'icons.flag_netherlands', + 'icons.flag_uae', + 'icons.flag_qatar', + 'icons.flag_egypt', + 'icons.flag_kuwait', + 'icons.flag_algeria', + 'icons.flag_saudi_arabia', + 'icons.flag_malaysia', + 'icons.flag_czech_republic', + 'icons.flag_australia', + 'icons.flag_singapore', + 'icons.flag_iran', + 'icons.flag_poland', + 'icons.flag_argentina', + 'icons.flag_philippines', + 'icons.flag_chile', + 'icons.moon', + 'icons.fedora', + 'icons.spider', + 'icons.ninja_star', + 'icons.skull', + 'icons.dragon', + 'icons.viking_helmet', + 'icons.fireball', + 'icons.helmet', + 'icons.crown', + ] + } + ], + } + store_layout = classic.store_layout + store_layout['characters'] = [ + { + 'items': [ + 'characters.kronk', + 'characters.zoe', + 'characters.jackmorgan', + 'characters.mel', + 'characters.snakeshadow', + 'characters.bones', + 'characters.bernard', + 'characters.agent', + 'characters.frosty', + 'characters.pascal', + 'characters.pixie', + ] + } + ] + store_layout['minigames'] = [ + { + 'items': [ + 'games.ninja_fight', + 'games.meteor_shower', + 'games.target_practice', + ] + } + ] + if plus.get_v1_account_misc_read_val('xmas', False): + store_layout['characters'][0]['items'].append('characters.santa') + store_layout['characters'][0]['items'].append('characters.wizard') + store_layout['characters'][0]['items'].append('characters.cyborg') + if plus.get_v1_account_misc_read_val('easter', False): + store_layout['characters'].append( + { + 'title': 'store.holidaySpecialText', + 'items': ['characters.bunny'], + } + ) + store_layout['minigames'].append( + { + 'title': 'store.holidaySpecialText', + 'items': ['games.easter_egg_hunt'], + } + ) + + # This will cause merch to show only if the master-server has + # given us a link (which means merch is available in our region). + store_layout['extras'] = [{'items': ['pro']}] + if _babase.app.config.get('Merch Link'): + store_layout['extras'][0]['items'].append('merch') + return store_layout + + def get_clean_price(self, price_string: str) -> str: + """(internal)""" + + # I'm not brave enough to try and do any numerical + # manipulation on formatted price strings, but lets do a + # few swap-outs to tidy things up a bit. + psubs = { + '$2.99': '$3.00', + '$4.99': '$5.00', + '$9.99': '$10.00', + '$19.99': '$20.00', + '$49.99': '$50.00', + } + return psubs.get(price_string, price_string) + + def get_available_purchase_count(self, tab: str | None = None) -> int: + """(internal)""" + plus = _babase.app.plus + if plus is None: + return 0 + try: + if plus.get_v1_account_state() != 'signed_in': + return 0 + count = 0 + our_tickets = plus.get_v1_account_ticket_count() + store_data = self.get_store_layout() + if tab is not None: + tabs = [(tab, store_data[tab])] + else: + tabs = list(store_data.items()) + for tab_name, tabval in tabs: + if tab_name == 'icons': + continue # too many of these; don't show.. + count = self._calc_count_for_tab(tabval, our_tickets, count) + return count + except Exception: + logging.exception('Error calcing available purchases.') + return 0 + + def _calc_count_for_tab( + self, tabval: list[dict[str, Any]], our_tickets: int, count: int + ) -> int: + plus = _babase.app.plus + assert plus + for section in tabval: + for item in section['items']: + ticket_cost = plus.get_v1_account_misc_read_val( + 'price.' + item, None + ) + if ticket_cost is not None: + if our_tickets >= ticket_cost and not plus.get_purchased( + item + ): + count += 1 + return count + + def get_available_sale_time(self, tab: str) -> int | None: + """(internal)""" + # pylint: disable=too-many-branches + # pylint: disable=too-many-nested-blocks + # pylint: disable=too-many-locals + plus = _babase.app.plus + assert plus is not None + + try: + import datetime + + app = _babase.app + assert app.classic is not None + sale_times: list[int | None] = [] + + # Calc time for our pro sale (old special case). + if tab == 'extras': + config = app.config + if app.classic.accounts.have_pro(): + return None + + # If we haven't calced/loaded start times yet. + if app.classic.pro_sale_start_time is None: + # If we've got a time-remaining in our config, start there. + if 'PSTR' in config: + app.classic.pro_sale_start_time = int( + _babase.apptime() * 1000 + ) + app.classic.pro_sale_start_val = config['PSTR'] + else: + # We start the timer once we get the duration from + # the server. + start_duration = plus.get_v1_account_misc_read_val( + 'proSaleDurationMinutes', None + ) + if start_duration is not None: + app.classic.pro_sale_start_time = int( + _babase.apptime() * 1000 + ) + app.classic.pro_sale_start_val = ( + 60000 * start_duration + ) + + # If we haven't heard from the server yet, no sale.. + else: + return None + + assert app.classic.pro_sale_start_val is not None + val: int | None = max( + 0, + app.classic.pro_sale_start_val + - ( + int(_babase.apptime() * 1000.0) + - app.classic.pro_sale_start_time + ), + ) + + # Keep the value in the config up to date. I suppose we should + # write the config occasionally but it should happen often + # enough for other reasons. + config['PSTR'] = val + if val == 0: + val = None + sale_times.append(val) + + # Now look for sales in this tab. + sales_raw = plus.get_v1_account_misc_read_val('sales', {}) + store_layout = self.get_store_layout() + for section in store_layout[tab]: + for item in section['items']: + if item in sales_raw: + if not plus.get_purchased(item): + to_end = ( + datetime.datetime.utcfromtimestamp( + sales_raw[item]['e'] + ) + - datetime.datetime.utcnow() + ).total_seconds() + if to_end > 0: + sale_times.append(int(to_end * 1000)) + + # Return the smallest time I guess? + sale_times_int = [t for t in sale_times if isinstance(t, int)] + return min(sale_times_int) if sale_times_int else None + + except Exception: + from babase import _error + + _error.print_exception('error calcing sale time') + return None + + def get_unowned_maps(self) -> list[str]: + """Return the list of local maps not owned by the current account. + + Category: **Asset Functions** + """ + plus = _babase.app.plus + unowned_maps: set[str] = set() + if not _babase.app.headless_mode: + for map_section in self.get_store_layout()['maps']: + for mapitem in map_section['items']: + if plus is None or not plus.get_purchased(mapitem): + m_info = self.get_store_item(mapitem) + unowned_maps.add(m_info['map_type'].name) + return sorted(unowned_maps) + + def get_unowned_game_types(self) -> set[type[bascenev1.GameActivity]]: + """Return present game types not owned by the current account.""" + try: + plus = _babase.app.plus + unowned_games: set[type[bascenev1.GameActivity]] = set() + if not _babase.app.headless_mode: + for section in self.get_store_layout()['minigames']: + for mname in section['items']: + if plus is None or not plus.get_purchased(mname): + m_info = self.get_store_item(mname) + unowned_games.add(m_info['gametype']) + return unowned_games + except Exception: + from babase import _error + + _error.print_exception('error calcing un-owned games') + return set() diff --git a/src/assets/ba_data/python/baclassic/_subsystem.py b/src/assets/ba_data/python/baclassic/_subsystem.py new file mode 100644 index 00000000..173efa76 --- /dev/null +++ b/src/assets/ba_data/python/baclassic/_subsystem.py @@ -0,0 +1,639 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides classic app subsystem.""" +from __future__ import annotations + +import random +import logging +from typing import TYPE_CHECKING + +import _babase +import _bauiv1 +import bascenev1 +from babase._general import AppTime +import _baclassic +from baclassic._music import MusicSubsystem +from baclassic._accountv1 import AccountV1Subsystem +from baclassic._ads import AdsSubsystem +from baclassic._net import MasterServerResponseType, MasterServerV1CallThread +from baclassic._achievement import AchievementSubsystem +from baclassic._tips import get_all_tips +from baclassic._store import StoreSubsystem +from baclassic._ui import UISubsystem +from baclassic import _input +from baclassic import _profile + +if TYPE_CHECKING: + from typing import Callable, Any + + import babase + import baclassic + from bastd.actor import spazappearance + from baclassic._appdelegate import AppDelegate + from baclassic._servermode import ServerController + from baclassic._net import MasterServerCallback + + +class ClassicSubsystem: + """Subsystem for classic functionality in the app. + + The single shared instance of this app can be accessed at + babase.app.classic. Note that it is possible for babase.app.classic to + be None if the classic package is not present, and code should handle + that case gracefully. + """ + + # pylint: disable=too-many-public-methods + + # Note: we pull the same things in here that are exposed in + # baclassic/__init__.py. This way this version can be used for + # runtime via babase.app.classic which enforces handling of the + # package-not-present case. + from baclassic._level import Level + from baclassic._campaign import Campaign + from baclassic._lobby import Lobby, Chooser + from baclassic._music import MusicPlayMode # FIXME move 2 subsys + + def __init__(self) -> None: + self._env = _babase.env() + + self.accounts = AccountV1Subsystem() + self.ads = AdsSubsystem() + self.ach = AchievementSubsystem() + self.store = StoreSubsystem() + self.music = MusicSubsystem() + self.ui = UISubsystem() + + # Co-op Campaigns. + self.campaigns: dict[str, baclassic.Campaign] = {} + self.custom_coop_practice_games: list[str] = [] + + # Lobby. + self.lobby_random_profile_index: int = 1 + self.lobby_random_char_index_offset = random.randrange(1000) + self.lobby_account_profile_device_id: int | None = None + + # Misc. + self.tips: list[str] = [] + self.stress_test_reset_timer: babase.AppTimer | None = None + self.value_test_defaults: dict = {} + self.special_offer: dict | None = None + self.ping_thread_count = 0 + self.allow_ticket_purchases: bool = not _babase.app.iircade_mode + + # Main Menu. + self.main_menu_did_initial_transition = False + self.main_menu_last_news_fetch_time: float | None = None + + # Spaz. + self.spaz_appearances: dict[str, spazappearance.Appearance] = {} + self.last_spaz_turbo_warn_time = AppTime(-99999.0) + + # Server Mode. + self.server: ServerController | None = None + + self.log_have_new = False + self.log_upload_timer_started = False + self.printed_live_object_warning = False + + # We include this extra hash with shared input-mapping names so + # that we don't share mappings between differently-configured + # systems. For instance, different android devices may give different + # key values for the same controller type so we keep their mappings + # distinct. + self.input_map_hash: str | None = None + + # Maps. + self.maps: dict[str, type[bascenev1.Map]] = {} + + # Gameplay. + self.teams_series_length = 7 + self.ffa_series_length = 24 + self.coop_session_args: dict = {} + + # UI. + self.first_main_menu = True # FIXME: Move to mainmenu class. + self.did_menu_intro = False # FIXME: Move to mainmenu class. + self.main_menu_window_refresh_check_count = 0 # FIXME: Mv to mainmenu. + self.main_menu_resume_callbacks: list = [] # Can probably go away. + self.invite_confirm_windows: list[Any] = [] # FIXME: Don't use Any. + self.delegate: AppDelegate | None = None + + # Store. + self.store_layout: dict[str, list[dict[str, Any]]] | None = None + self.store_items: dict[str, dict] | None = None + self.pro_sale_start_time: int | None = None + self.pro_sale_start_val: int | None = None + + @property + def platform(self) -> str: + """Name of the current platform. + + Examples are: 'mac', 'windows', android'. + """ + assert isinstance(self._env['platform'], str) + return self._env['platform'] + + @property + def subplatform(self) -> str: + """String for subplatform. + + Can be empty. For the 'android' platform, subplatform may + be 'google', 'amazon', etc. + """ + assert isinstance(self._env['subplatform'], str) + return self._env['subplatform'] + + @property + def user_agent_string(self) -> str: + """String containing various bits of info about OS/device/etc.""" + assert isinstance(self._env['user_agent_string'], str) + return self._env['user_agent_string'] + + def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None: + """(internal)""" + + # If there's no main menu up, just call immediately. + if not self.ui.has_main_menu_window(): + with _babase.ContextRef.empty(): + call() + else: + self.main_menu_resume_callbacks.append(call) + + def on_app_launching(self) -> None: + """Called when the app is first entering the launching state.""" + # pylint: disable=too-many-locals + from baclassic import _campaign + from bascenev1 import _map + from bastd.actor import spazappearance + from bastd import maps as stdmaps + from babase._apputils import handle_leftover_v1_cloud_log_file + from baclassic._appdelegate import AppDelegate + import bauiv1 as bui + + plus = bui.app.plus + assert plus is not None + + cfg = _babase.app.config + + self.ui.on_app_launching() + self.music.on_app_launching() + + self.delegate = AppDelegate() + + # Non-test, non-debug builds should generally be blessed; warn if not. + # (so I don't accidentally release a build that can't play tourneys) + if ( + not _babase.app.debug_build + and not _babase.app.test_build + and not plus.is_blessed() + ): + _babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0)) + + # FIXME: This should not be hard-coded. + for maptype in [ + stdmaps.HockeyStadium, + stdmaps.FootballStadium, + stdmaps.Bridgit, + stdmaps.BigG, + stdmaps.Roundabout, + stdmaps.MonkeyFace, + stdmaps.ZigZag, + stdmaps.ThePad, + stdmaps.DoomShroom, + stdmaps.LakeFrigid, + stdmaps.TipTop, + stdmaps.CragCastle, + stdmaps.TowerD, + stdmaps.HappyThoughts, + stdmaps.StepRightUp, + stdmaps.Courtyard, + stdmaps.Rampage, + ]: + _map.register_map(maptype) + + spazappearance.register_appearances() + _campaign.init_campaigns() + + launch_count = cfg.get('launchCount', 0) + launch_count += 1 + + # So we know how many times we've run the game at various + # version milestones. + for key in ('lc14173', 'lc14292'): + cfg.setdefault(key, launch_count) + + cfg['launchCount'] = launch_count + cfg.commit() + + # Run a test in a few seconds to see if we should pop up an existing + # pending special offer. + def check_special_offer() -> None: + plus = bui.app.plus + assert plus is not None + + from bastd.ui.specialoffer import show_offer + + if ( + 'pendingSpecialOffer' in cfg + and plus.get_v1_account_public_login_id() + == cfg['pendingSpecialOffer']['a'] + ): + self.special_offer = cfg['pendingSpecialOffer']['o'] + show_offer() + + if not _babase.app.headless_mode: + bui.apptimer(3.0, check_special_offer) + + # If there's a leftover log file, attempt to upload it to the + # master-server and/or get rid of it. + handle_leftover_v1_cloud_log_file() + + self.accounts.on_app_launching() + + def on_app_pause(self) -> None: + """Called when the app is getting paused.""" + self.accounts.on_app_pause() + + def on_app_resume(self) -> None: + """Called when the app is getting resumed.""" + self.accounts.on_app_resume() + self.music.on_app_resume() + + def on_app_shutdown(self) -> None: + """Called when the app is shutting down.""" + self.music.on_app_shutdown() + + def pause(self) -> None: + """Pause the game due to a user request or menu popping up. + + If there's a foreground host-activity that says it's pausable, tell it + to pause. Note: we now no longer pause if there are connected clients. + """ + activity: bascenev1.Activity | None = ( + bascenev1.get_foreground_host_activity() + ) + if ( + activity is not None + and activity.allow_pausing + and not bascenev1.have_connected_clients() + ): + from babase._language import Lstr + from bascenev1._nodeactor import NodeActor + + # FIXME: Shouldn't be touching scene stuff here; + # should just pass the request on to the host-session. + with activity.context: + globs = activity.globalsnode + if not globs.paused: + bascenev1.getsound('refWhistle').play() + globs.paused = True + + # FIXME: This should not be an attr on Actor. + activity.paused_text = NodeActor( + bascenev1.newnode( + 'text', + attrs={ + 'text': Lstr(resource='pausedByHostText'), + 'client_only': True, + 'flatness': 1.0, + 'h_align': 'center', + }, + ) + ) + + def resume(self) -> None: + """Resume the game due to a user request or menu closing. + + If there's a foreground host-activity that's currently paused, tell it + to resume. + """ + + # FIXME: Shouldn't be touching scene stuff here; + # should just pass the request on to the host-session. + activity = bascenev1.get_foreground_host_activity() + if activity is not None: + with activity.context: + globs = activity.globalsnode + if globs.paused: + bascenev1.getsound('refWhistle').play() + globs.paused = False + + # FIXME: This should not be an actor attr. + activity.paused_text = None + + def add_coop_practice_level(self, level: baclassic.Level) -> None: + """Adds an individual level to the 'practice' section in Co-op.""" + + # Assign this level to our catch-all campaign. + self.campaigns['Challenges'].addlevel(level) + + # Make note to add it to our challenges UI. + self.custom_coop_practice_games.append(f'Challenges:{level.name}') + + def launch_coop_game( + self, game: str, force: bool = False, args: dict | None = None + ) -> bool: + """High level way to launch a local co-op session.""" + # pylint: disable=cyclic-import + from bastd.ui.coop.level import CoopLevelLockedWindow + + assert _babase.app.classic is not None + + if args is None: + args = {} + if game == '': + raise ValueError('empty game name') + campaignname, levelname = game.split(':') + campaign = _babase.app.classic.getcampaign(campaignname) + + # If this campaign is sequential, make sure we've completed the + # one before this. + if campaign.sequential and not force: + for level in campaign.levels: + if level.name == levelname: + break + if not level.complete: + CoopLevelLockedWindow( + campaign.getlevel(levelname).displayname, + campaign.getlevel(level.name).displayname, + ) + return False + + # Ok, we're good to go. + self.coop_session_args = { + 'campaign': campaignname, + 'level': levelname, + } + for arg_name, arg_val in list(args.items()): + self.coop_session_args[arg_name] = arg_val + + def _fade_end() -> None: + from bascenev1 import _coopsession + + try: + bascenev1.new_host_session(_coopsession.CoopSession) + except Exception: + logging.exception('Error creating coopsession after fade end.') + from bastd.mainmenu import MainMenuSession + + bascenev1.new_host_session(MainMenuSession) + + _babase.fade_screen(False, endcall=_fade_end) + return True + + def return_to_main_menu_session_gracefully( + self, reset_ui: bool = True + ) -> None: + """Attempt to cleanly get back to the main menu.""" + # pylint: disable=cyclic-import + from baclassic import _benchmark + from babase import Call + from bastd.mainmenu import MainMenuSession + + plus = _babase.app.plus + assert plus is not None + + if reset_ui: + self.ui.clear_main_menu_window() + + if isinstance(bascenev1.get_foreground_host_session(), MainMenuSession): + # It may be possible we're on the main menu but the screen is faded + # so fade back in. + _babase.fade_screen(True) + return + + _benchmark.stop_stress_test() # Stop stress-test if in progress. + + # If we're in a host-session, tell them to end. + # This lets them tear themselves down gracefully. + host_session: bascenev1.Session | None = ( + bascenev1.get_foreground_host_session() + ) + if host_session is not None: + # Kick off a little transaction so we'll hopefully have all the + # latest account state when we get back to the menu. + plus.add_v1_account_transaction( + {'type': 'END_SESSION', 'sType': str(type(host_session))} + ) + plus.run_v1_account_transactions() + + host_session.end() + + # Otherwise just force the issue. + else: + _babase.pushcall(Call(bascenev1.new_host_session, MainMenuSession)) + + def getmaps(self, playtype: str) -> list[str]: + """Return a list of bascenev1.Map types supporting a playtype str. + + Category: **Asset Functions** + + Maps supporting a given playtype must provide a particular set of + features and lend themselves to a certain style of play. + + Play Types: + + 'melee' + General fighting map. + Has one or more 'spawn' locations. + + 'team_flag' + For games such as Capture The Flag where each team spawns by a flag. + Has two or more 'spawn' locations, each with a corresponding 'flag' + location (based on index). + + 'single_flag' + For games such as King of the Hill or Keep Away where multiple teams + are fighting over a single flag. + Has two or more 'spawn' locations and 1 'flag_default' location. + + 'conquest' + For games such as Conquest where flags are spread throughout the map + - has 2+ 'flag' locations, 2+ 'spawn_by_flag' locations. + + 'king_of_the_hill' - has 2+ 'spawn' locations, + 1+ 'flag_default' locations, and 1+ 'powerup_spawn' locations + + 'hockey' + For hockey games. + Has two 'goal' locations, corresponding 'spawn' locations, and one + 'flag_default' location (for where puck spawns) + + 'football' + For football games. + Has two 'goal' locations, corresponding 'spawn' locations, and one + 'flag_default' location (for where flag/ball/etc. spawns) + + 'race' + For racing games where players much touch each region in order. + Has two or more 'race_point' locations. + """ + return sorted( + key + for key, val in self.maps.items() + if playtype in val.get_play_types() + ) + + def show_online_score_ui( + self, + show: str = 'general', + game: str | None = None, + game_version: str | None = None, + ) -> None: + """(internal)""" + _bauiv1.show_online_score_ui(show, game, game_version) + + def game_begin_analytics(self) -> None: + """(internal)""" + from baclassic import _analytics + + _analytics.game_begin_analytics() + + def master_server_v1_get( + self, + request: str, + data: dict[str, Any], + callback: MasterServerCallback | None = None, + response_type: MasterServerResponseType = MasterServerResponseType.JSON, + ) -> None: + """Make a call to the master server via a http GET.""" + + MasterServerV1CallThread( + request, 'get', data, callback, response_type + ).start() + + def master_server_v1_post( + self, + request: str, + data: dict[str, Any], + callback: MasterServerCallback | None = None, + response_type: MasterServerResponseType = MasterServerResponseType.JSON, + ) -> None: + """Make a call to the master server via a http POST.""" + MasterServerV1CallThread( + request, 'post', data, callback, response_type + ).start() + + def get_tournament_prize_strings(self, entry: dict[str, Any]) -> list[str]: + """Given a tournament entry, return strings for its prize levels.""" + from baclassic import _tournament + + return _tournament.get_tournament_prize_strings(entry) + + def getcampaign(self, name: str) -> baclassic.Campaign: + """Return a campaign by name.""" + return self.campaigns[name] + + def get_next_tip(self) -> str: + """Returns the next tip to be displayed.""" + if not self.tips: + for tip in get_all_tips(): + self.tips.insert(random.randint(0, len(self.tips)), tip) + tip = self.tips.pop() + return tip + + def run_gpu_benchmark(self) -> None: + """Kick off a benchmark to test gpu speeds.""" + from baclassic._benchmark import run_gpu_benchmark as run + + run() + + def run_cpu_benchmark(self) -> None: + """Kick off a benchmark to test cpu speeds.""" + from baclassic._benchmark import run_cpu_benchmark as run + + run() + + def run_media_reload_benchmark(self) -> None: + """Kick off a benchmark to test media reloading speeds.""" + from baclassic._benchmark import run_media_reload_benchmark as run + + run() + + def run_stress_test( + self, + playlist_type: str = 'Random', + playlist_name: str = '__default__', + player_count: int = 8, + round_duration: int = 30, + ) -> None: + """Run a stress test.""" + from baclassic._benchmark import run_stress_test as run + + run(playlist_type, playlist_name, player_count, round_duration) + + def get_input_device_mapped_value( + self, device: bascenev1.InputDevice, name: str + ) -> Any: + """Returns a mapped value for an input device. + + This checks the user config and falls back to default values + where available. + """ + return _input.get_input_device_mapped_value( + device.name, device.unique_identifier, name + ) + + def get_input_device_map_hash( + self, inputdevice: bascenev1.InputDevice + ) -> str: + """Given an input device, return hash based on its raw input values.""" + del inputdevice # unused currently + return _input.get_input_device_map_hash() + + def get_input_device_config( + self, inputdevice: bascenev1.InputDevice, default: bool + ) -> tuple[dict, str]: + """Given an input device, return its config dict in the app config. + + The dict will be created if it does not exist. + """ + return _input.get_input_device_config( + inputdevice.name, inputdevice.unique_identifier, default + ) + + def get_player_colors(self) -> list[tuple[float, float, float]]: + """Return user-selectable player colors.""" + return _profile.get_player_colors() + + def get_player_profile_icon(self, profilename: str) -> str: + """Given a profile name, returns an icon string for it. + + (non-account profiles only) + """ + return _profile.get_player_profile_icon(profilename) + + def get_player_profile_colors( + self, + profilename: str | None, + profiles: dict[str, dict[str, Any]] | None = None, + ) -> tuple[tuple[float, float, float], tuple[float, float, float]]: + """Given a profile, return colors for them.""" + return _profile.get_player_profile_colors(profilename, profiles) + + def get_foreground_host_session(self) -> bascenev1.Session | None: + """(internal)""" + return bascenev1.get_foreground_host_session() + + def get_foreground_host_activity(self) -> bascenev1.Activity | None: + """(internal)""" + return bascenev1.get_foreground_host_activity() + + def show_config_error_window(self) -> bool: + """(internal)""" + if self.platform in ('mac', 'linux', 'windows'): + from bastd.ui.configerror import ConfigErrorWindow + + _babase.pushcall(ConfigErrorWindow) + return True + return False + + def value_test( + self, + arg: str, + change: float | None = None, + absolute: float | None = None, + ) -> float: + """(internal)""" + return _baclassic.value_test(arg, change, absolute) diff --git a/assets/src/ba_data/python/ba/_tips.py b/src/assets/ba_data/python/baclassic/_tips.py similarity index 90% rename from assets/src/ba_data/python/ba/_tips.py rename to src/assets/ba_data/python/baclassic/_tips.py index 604d825e..9a63acb8 100644 --- a/assets/src/ba_data/python/ba/_tips.py +++ b/src/assets/ba_data/python/baclassic/_tips.py @@ -1,29 +1,18 @@ # Released under the MIT License. See LICENSE for details. # -"""Functionality related to game tips. +"""Functionality related to classic game tips. These can be shown at opportune times such as between rounds.""" from __future__ import annotations from typing import TYPE_CHECKING -import random -import _ba +import _babase if TYPE_CHECKING: pass -def get_next_tip() -> str: - """Returns the next tip to be displayed.""" - app = _ba.app - if not app.tips: - for tip in get_all_tips(): - app.tips.insert(random.randint(0, len(app.tips)), tip) - tip = app.tips.pop() - return tip - - def get_all_tips() -> list[str]: """Return the complete list of tips.""" tips = [ @@ -116,14 +105,15 @@ def get_all_tips() -> list[str]: 'color of sparks from its fuse: yellow..orange..red..BOOM.' ), ] - app = _ba.app + app = _babase.app if not app.iircade_mode: tips += [ 'If your framerate is choppy, try turning down resolution\nor ' 'visuals in the game\'s graphics settings.' ] if ( - app.platform in ('android', 'ios') + app.classic is not None + and app.classic.platform in ('android', 'ios') and not app.on_tv and not app.iircade_mode ): @@ -134,7 +124,11 @@ def get_all_tips() -> list[str]: 'in Settings->Graphics' ), ] - if app.platform in ['mac', 'android'] and not app.iircade_mode: + if ( + app.classic is not None + and app.classic.platform in ['mac', 'android'] + and not app.iircade_mode + ): tips += [ 'Tired of the soundtrack? Replace it with your own!' '\nSee Settings->Audio->Soundtrack' @@ -142,7 +136,11 @@ def get_all_tips() -> list[str]: # Hot-plugging is currently only on some platforms. # FIXME: Should add a platform entry for this so don't forget to update it. - if app.platform in ['mac', 'android', 'windows'] and not app.iircade_mode: + if ( + app.classic is not None + and app.classic.platform in ['mac', 'android', 'windows'] + and not app.iircade_mode + ): tips += [ 'Players can join and leave in the middle of most games,\n' 'and you can also plug and unplug controllers on the fly.', diff --git a/assets/src/ba_data/python/ba/_tournament.py b/src/assets/ba_data/python/baclassic/_tournament.py similarity index 84% rename from assets/src/ba_data/python/ba/_tournament.py rename to src/assets/ba_data/python/baclassic/_tournament.py index 9694e387..bd3160e3 100644 --- a/assets/src/ba_data/python/ba/_tournament.py +++ b/src/assets/ba_data/python/baclassic/_tournament.py @@ -1,12 +1,12 @@ # Released under the MIT License. See LICENSE for details. # -"""Functionality related to tournament play.""" +"""Functionality related to classic tournament play.""" from __future__ import annotations from typing import TYPE_CHECKING -import _ba +import _babase if TYPE_CHECKING: from typing import Any @@ -15,8 +15,8 @@ if TYPE_CHECKING: def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]: """Given a tournament entry, return strings for its prize levels.""" # pylint: disable=too-many-locals - from ba._generated.enums import SpecialChar - from ba._gameutils import get_trophy_string + from babase._mgen.enums import SpecialChar + from bascenev1._gameutils import get_trophy_string range1 = entry.get('prizeRange1') range2 = entry.get('prizeRange2') @@ -47,7 +47,9 @@ def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]: # If we've got trophies but not for this entry, throw some space # in to compensate so the ticket counts line up. if prize is not None: - pvval = _ba.charstr(SpecialChar.TICKET_BACKING) + str(prize) + pvval + pvval = ( + _babase.charstr(SpecialChar.TICKET_BACKING) + str(prize) + pvval + ) out_vals.append(prval) out_vals.append(pvval) return out_vals diff --git a/assets/src/ba_data/python/ba/_ui.py b/src/assets/ba_data/python/baclassic/_ui.py similarity index 85% rename from assets/src/ba_data/python/ba/_ui.py rename to src/assets/ba_data/python/baclassic/_ui.py index c1aba303..7e0b8e88 100644 --- a/assets/src/ba_data/python/ba/_ui.py +++ b/src/assets/ba_data/python/baclassic/_ui.py @@ -6,13 +6,15 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba -from ba._generated.enums import UIScale +import bauiv1 as bui +import _babase +from babase._mgen.enums import UIScale if TYPE_CHECKING: from typing import Any, Callable - from ba.ui import UICleanupCheck - import ba + + from bauiv1.ui import UICleanupCheck, UIController + import babase class UISubsystem: @@ -24,14 +26,14 @@ class UISubsystem: """ def __init__(self) -> None: - env = _ba.env() + env = _babase.env() - self.controller: ba.UIController | None = None + self.controller: UIController | None = None - self._main_menu_window: ba.Widget | None = None + self._main_menu_window: bui.Widget | None = None self._main_menu_location: str | None = None - self._uiscale: ba.UIScale + self._uiscale: babase.UIScale interfacetype = env['ui_scale'] if interfacetype == 'large': @@ -49,7 +51,7 @@ class UISubsystem: self.quit_window: Any = None self.dismiss_wii_remotes_window_call: (Callable[[], Any] | None) = None self.cleanupchecks: list[UICleanupCheck] = [] - self.upkeeptimer: ba.Timer | None = None + self.upkeeptimer: babase.AppTimer | None = None self.use_toolbars = env.get('toolbar_test', True) self.party_window: Any = None # FIXME: Don't use Any. self.title_color = (0.72, 0.7, 0.75) @@ -62,14 +64,13 @@ class UISubsystem: self.selecting_private_party_playlist: bool = False @property - def uiscale(self) -> ba.UIScale: + def uiscale(self) -> babase.UIScale: """Current ui scale for the app.""" return self._uiscale - def on_app_launch(self) -> None: + def on_app_launching(self) -> None: """Should be run on app launch.""" - from ba.ui import UIController, ui_upkeep - from ba._generated.enums import TimeType + from bauiv1.ui import UIController, ui_upkeep # IMPORTANT: If tweaking UI stuff, make sure it behaves for small, # medium, and large UI modes. (doesn't run off screen, etc). @@ -88,9 +89,9 @@ class UISubsystem: if bool(False): # force-test ui scale self._uiscale = UIScale.SMALL - with _ba.Context('ui'): - _ba.pushcall( - lambda: _ba.screenmessage( + with _babase.ContextRef.empty(): + _babase.pushcall( + lambda: _babase.screenmessage( f'FORCING UISCALE {self._uiscale.name} FOR TESTING', color=(1, 0, 1), log=True, @@ -101,14 +102,11 @@ class UISubsystem: # Kick off our periodic UI upkeep. # FIXME: Can probably kill this if we do immediate UI death checks. - self.upkeeptimer = _ba.Timer( - 2.6543, ui_upkeep, timetype=TimeType.REAL, repeat=True - ) + self.upkeeptimer = _babase.AppTimer(2.6543, ui_upkeep, repeat=True) - def set_main_menu_window(self, window: ba.Widget) -> None: + def set_main_menu_window(self, window: bui.Widget) -> None: """Set the current 'main' window, replacing any existing.""" existing = self._main_menu_window - from ba._generated.enums import TimeType from inspect import currentframe, getframeinfo # Let's grab the location where we were called from to report @@ -123,7 +121,7 @@ class UISubsystem: frameinfo = getframeinfo(frame) frameline = f'{frameinfo.filename} {frameinfo.lineno}' except Exception: - from ba._error import print_exception + from babase._error import print_exception print_exception('Error calcing line for set_main_menu_window') @@ -147,14 +145,14 @@ class UISubsystem: ) existing.delete() - _ba.timer(1.0, _delay_kill, timetype=TimeType.REAL) + bui.apptimer(1.0, _delay_kill) self._main_menu_window = window def clear_main_menu_window(self, transition: str | None = None) -> None: """Clear any existing 'main' window with the provided transition.""" if self._main_menu_window: if transition is not None: - _ba.containerwidget( + bui.containerwidget( edit=self._main_menu_window, transition=transition ) else: diff --git a/assets/src/ba_data/python/ba/macmusicapp.py b/src/assets/ba_data/python/baclassic/macmusicapp.py similarity index 82% rename from assets/src/ba_data/python/ba/macmusicapp.py rename to src/assets/ba_data/python/baclassic/macmusicapp.py index e77130c6..3cc8b215 100644 --- a/assets/src/ba_data/python/ba/macmusicapp.py +++ b/src/assets/ba_data/python/baclassic/macmusicapp.py @@ -7,8 +7,9 @@ import threading from collections import deque from typing import TYPE_CHECKING -import _ba -from ba._music import MusicPlayer +import _babase +from baclassic._music import MusicPlayer +import bauiv1 as bui if TYPE_CHECKING: from typing import Callable, Any @@ -46,7 +47,8 @@ class MacMusicAppMusicPlayer(MusicPlayer): self._thread.get_playlists(callback) def on_play(self, entry: Any) -> None: - music = _ba.app.music + assert _babase.app.classic is not None + music = _babase.app.classic.music entry_type = music.get_soundtrack_entry_type(entry) if entry_type == 'iTunesPlaylist': self._thread.play_playlist(music.get_soundtrack_entry_name(entry)) @@ -76,32 +78,30 @@ class _MacMusicAppThread(threading.Thread): def run(self) -> None: """Run the Music.app thread.""" - from ba._general import Call - from ba._language import Lstr - from ba._generated.enums import TimeType + from babase._general import Call + from babase._language import Lstr - _ba.set_thread_name('BA_MacMusicAppThread') - _ba.mac_music_app_init() + _babase.set_thread_name('BA_MacMusicAppThread') + _babase.mac_music_app_init() # Let's mention to the user we're launching Music.app in case # it causes any funny business (this used to background the app # sometimes, though I think that is fixed now) def do_print() -> None: - _ba.timer( + bui.apptimer( 1.0, Call( - _ba.screenmessage, + _babase.screenmessage, Lstr(resource='usingItunesText'), (0, 1, 0), ), - timetype=TimeType.REAL, ) - _ba.pushcall(do_print, from_other_thread=True) + _babase.pushcall(do_print, from_other_thread=True) # Here we grab this to force the actual launch. - _ba.mac_music_app_get_volume() - _ba.mac_music_app_get_library_source() + _babase.mac_music_app_get_volume() + _babase.mac_music_app_get_library_source() done = False while not done: self._commands_available.wait() @@ -137,16 +137,15 @@ class _MacMusicAppThread(threading.Thread): if old_volume > 0.0 and volume == 0.0: try: assert self._orig_volume is not None - _ba.mac_music_app_stop() - _ba.mac_music_app_set_volume(self._orig_volume) + _babase.mac_music_app_stop() + _babase.mac_music_app_set_volume(self._orig_volume) except Exception as exc: print('Error stopping iTunes music:', exc) elif self._volume > 0: - # If volume was zero, store pre-playing volume and start # playing. if old_volume == 0.0: - self._orig_volume = _ba.mac_music_app_get_volume() + self._orig_volume = _babase.mac_music_app_get_volume() self._update_mac_music_app_volume() if old_volume == 0.0: self._play_current_playlist() @@ -170,10 +169,10 @@ class _MacMusicAppThread(threading.Thread): def _handle_get_playlists_command( self, target: Callable[[list[str]], None] ) -> None: - from ba._general import Call + from babase._general import Call try: - playlists = _ba.mac_music_app_get_playlists() + playlists = _babase.mac_music_app_get_playlists() playlists = [ p for p in playlists @@ -197,15 +196,15 @@ class _MacMusicAppThread(threading.Thread): except Exception as exc: print('Error getting iTunes playlists:', exc) playlists = [] - _ba.pushcall(Call(target, playlists), from_other_thread=True) + _babase.pushcall(Call(target, playlists), from_other_thread=True) def _handle_play_command(self, target: str | None) -> None: if target is None: if self._current_playlist is not None and self._volume > 0: try: assert self._orig_volume is not None - _ba.mac_music_app_stop() - _ba.mac_music_app_set_volume(self._orig_volume) + _babase.mac_music_app_stop() + _babase.mac_music_app_set_volume(self._orig_volume) except Exception as exc: print('Error stopping iTunes music:', exc) self._current_playlist = None @@ -215,42 +214,41 @@ class _MacMusicAppThread(threading.Thread): if self._current_playlist is not None and self._volume > 0: try: assert self._orig_volume is not None - _ba.mac_music_app_stop() - _ba.mac_music_app_set_volume(self._orig_volume) + _babase.mac_music_app_stop() + _babase.mac_music_app_set_volume(self._orig_volume) except Exception as exc: print('Error stopping iTunes music:', exc) # Set our playlist and play it if our volume is up. self._current_playlist = target if self._volume > 0: - self._orig_volume = _ba.mac_music_app_get_volume() + self._orig_volume = _babase.mac_music_app_get_volume() self._update_mac_music_app_volume() self._play_current_playlist() def _handle_die_command(self) -> None: - # Only stop if we've actually played something # (we don't want to kill music the user has playing). if self._current_playlist is not None and self._volume > 0: try: assert self._orig_volume is not None - _ba.mac_music_app_stop() - _ba.mac_music_app_set_volume(self._orig_volume) + _babase.mac_music_app_stop() + _babase.mac_music_app_set_volume(self._orig_volume) except Exception as exc: print('Error stopping iTunes music:', exc) def _play_current_playlist(self) -> None: try: - from ba._general import Call + from babase._general import Call assert self._current_playlist is not None - if _ba.mac_music_app_play_playlist(self._current_playlist): + if _babase.mac_music_app_play_playlist(self._current_playlist): pass else: - _ba.pushcall( + _babase.pushcall( Call( - _ba.screenmessage, - _ba.app.lang.get_resource('playlistNotFoundText') + _babase.screenmessage, + _babase.app.lang.get_resource('playlistNotFoundText') + ': \'' + self._current_playlist + '\'', @@ -259,13 +257,13 @@ class _MacMusicAppThread(threading.Thread): from_other_thread=True, ) except Exception: - from ba import _error + from babase import _error _error.print_exception( f'error playing playlist {self._current_playlist}' ) def _update_mac_music_app_volume(self) -> None: - _ba.mac_music_app_set_volume( + _babase.mac_music_app_set_volume( max(0, min(100, int(100.0 * self._volume))) ) diff --git a/assets/src/ba_data/python/ba/osmusic.py b/src/assets/ba_data/python/baclassic/osmusic.py similarity index 87% rename from assets/src/ba_data/python/ba/osmusic.py rename to src/assets/ba_data/python/baclassic/osmusic.py index 9c3f48e5..44ef9746 100644 --- a/assets/src/ba_data/python/ba/osmusic.py +++ b/src/assets/ba_data/python/baclassic/osmusic.py @@ -8,8 +8,8 @@ import random import threading from typing import TYPE_CHECKING -import _ba -from ba._music import MusicPlayer +import _babase +from baclassic._music import MusicPlayer if TYPE_CHECKING: from typing import Callable, Any @@ -47,18 +47,18 @@ class OSMusicPlayer(MusicPlayer): ) def on_set_volume(self, volume: float) -> None: - _ba.music_player_set_volume(volume) + _babase.music_player_set_volume(volume) def on_play(self, entry: Any) -> None: - music = _ba.app.music + assert _babase.app.classic is not None + music = _babase.app.classic.music entry_type = music.get_soundtrack_entry_type(entry) name = music.get_soundtrack_entry_name(entry) assert name is not None if entry_type == 'musicFile': self._want_to_play = self._actually_playing = True - _ba.music_player_play(name) + _babase.music_player_play(name) elif entry_type == 'musicFolder': - # Launch a thread to scan this folder and give us a random # valid file within it. self._want_to_play = True @@ -72,7 +72,7 @@ class OSMusicPlayer(MusicPlayer): def _on_play_folder_cb( self, result: str | list[str], error: str | None = None ) -> None: - from ba import _language + from babase import _language if error is not None: rstr = _language.Lstr( @@ -88,7 +88,7 @@ class OSMusicPlayer(MusicPlayer): err_str = ( rstr.replace('${MUSIC}', '') + '; ' + str(error) ) - _ba.screenmessage(err_str, color=(1, 0, 0)) + _babase.screenmessage(err_str, color=(1, 0, 0)) return # There's a chance a stop could have been issued before our thread @@ -97,15 +97,15 @@ class OSMusicPlayer(MusicPlayer): print('_on_play_folder_cb called with _want_to_play False') else: self._actually_playing = True - _ba.music_player_play(result) + _babase.music_player_play(result) def on_stop(self) -> None: self._want_to_play = False self._actually_playing = False - _ba.music_player_stop() + _babase.music_player_stop() def on_app_shutdown(self) -> None: - _ba.music_player_shutdown() + _babase.music_player_shutdown() class _PickFolderSongThread(threading.Thread): @@ -121,12 +121,12 @@ class _PickFolderSongThread(threading.Thread): self._path = path def run(self) -> None: - from ba import _language - from ba._general import Call + from babase import _language + from babase._general import Call do_print_error = True try: - _ba.set_thread_name('BA_PickFolderSongThread') + _babase.set_thread_name('BA_PickFolderSongThread') all_files: list[str] = [] valid_extensions = ['.' + x for x in self._valid_extensions] for root, _subdirs, filenames in os.walk(self._path): @@ -145,11 +145,11 @@ class _PickFolderSongThread(threading.Thread): resource='internal.noMusicFilesInFolderText' ).evaluate() ) - _ba.pushcall( + _babase.pushcall( Call(self._callback, all_files, None), from_other_thread=True ) except Exception as exc: - from ba import _error + from babase import _error if do_print_error: _error.print_exception() @@ -157,7 +157,7 @@ class _PickFolderSongThread(threading.Thread): err_str = str(exc) except Exception: err_str = '' - _ba.pushcall( + _babase.pushcall( Call(self._callback, self._path, err_str), from_other_thread=True, ) diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py new file mode 100644 index 00000000..365f60c0 --- /dev/null +++ b/src/assets/ba_data/python/baenv.py @@ -0,0 +1,419 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Manage ballistica execution environment. + +This module is used to set up and/or check the global Python environment +before running a ballistica app. This includes things such as paths, +logging, debug-modes, garbage-collection settings, and signal handling. +Because these things are global in nature, this should be done before +any ballistica modules are imported. + +Ballistica can be used without explicitly configuring the environment in +order to integrate it in arbitrary Python environments, but this may +cause some features to be disabled or behave differently than expected. +""" +from __future__ import annotations + +import os +import sys +import signal +import logging +from pathlib import Path +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from efro.log import setup_logging, LogLevel + +if TYPE_CHECKING: + from typing import Any + + from efro.log import LogEntry, LogHandler + +# Build number and version of the ballistica binary we expect to be +# using. +TARGET_BALLISTICA_BUILD = 21015 +TARGET_BALLISTICA_VERSION = '1.7.20' + +_g_env_config: EnvConfig | None = None +_g_babase_imported = False # pylint: disable=invalid-name +_g_babase_app_started = False # pylint: disable=invalid-name +_g_paths_set_failed = False # pylint: disable=invalid-name + + +@dataclass +class EnvConfig: + """Final settings put together by the configure call.""" + + config_dir: str + data_dir: str + user_python_dir: str | None + app_python_dir: str | None + site_python_dir: str | None + log_handler: LogHandler | None + + +def config_exists() -> bool: + """Has a config been created?""" + return _g_env_config is not None + + +def get_config() -> EnvConfig: + """Return the active env-config. Creates default if none exists.""" + if _g_env_config is None: + configure() + assert _g_env_config is not None + return _g_env_config + + +def configure( + config_dir: str | None = None, + data_dir: str | None = None, + user_python_dir: str | None = None, + app_python_dir: str | None = None, + site_python_dir: str | None = None, + contains_python_dist: bool = False, +) -> None: + """Set up the Python environment for running a ballistica app. + + This includes things such as Python paths and log redirection. For + that reason, this should be called before any other ballistica + modules are imported, since it may make changes to sys.path, + affecting where those modules get loaded from. + """ + # pylint: disable=too-many-branches + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + + global _g_env_config # pylint: disable=global-statement, invalid-name + if _g_env_config is not None: + raise RuntimeError('An EnvConfig has already been created.') + + # The very first thing we do is set up our logging system and feed + # Python's stdout/stderr into it. Then we can at least debug problems + # on systems where native stdout/stderr is not easily accessible + # such as Android. + log_handler = setup_logging( + log_path=None, + level=LogLevel.DEBUG, + suppress_non_root_debug=True, + log_stdout_stderr=True, + cache_size_limit=1024 * 1024, + ) + + # Sanity check: we should always be run in UTF-8 mode. + if sys.flags.utf8_mode != 1: + logging.warning( + "Python's UTF-8 mode is not set. Running ballistica without" + ' it may lead to errors.' + ) + + # Now do paths. We want to set stuff up so that engine modules, + # mods, etc. are pulled from predictable places. + cwd_path = Path.cwd() + + # A few paths we can ALWAYS calculate since they don't affect Python + # imports: + + # Default data_dir assumes this module was imported from its + # ba_data/python subdir. + if data_dir is None: + assert Path(__file__).parts[-3:-1] == ('ba_data', 'python') + data_dir_path = Path(__file__).parents[2] + # Prefer tidy relative paths like '.' if possible. + data_dir = str( + data_dir_path.relative_to(cwd_path) + if data_dir_path.is_relative_to(cwd_path) + else data_dir_path + ) + + # Default config-dir is simply ~/.ballisticakit + if config_dir is None: + config_dir = str(Path(Path.home(), '.ballisticakit')) + + # Ok now Python paths. + + # If _babase has already been imported, there's not much we can do + # at this point aside from complain and inform for next time. + if _g_babase_imported: + app_python_dir = user_python_dir = site_python_dir = None + + # We don't actually complain yet here; we simply take note + # that we weren't able to set paths. Then we complain if/when + # the app is started. This way, non-app uses of babase won't be + # filled with unnecessary warnings. + global _g_paths_set_failed # pylint: disable=global-statement + _g_paths_set_failed = True + + else: + # Ok; _babase hasn't been imported yet so we can muck with + # Python paths. + + # By default, app-python-dir is simply ba_data/python under + # data-dir. + if app_python_dir is None: + app_python_dir = str(Path(data_dir, 'ba_data', 'python')) + + # Likewise site-python-dir defaults to ba_data/python-site-packages. + if site_python_dir is None: + site_python_dir = str( + Path(data_dir, 'ba_data', 'python-site-packages') + ) + + # By default, user-python-dir is simply 'mods' under config-dir. + if user_python_dir is None: + user_python_dir = str(Path(config_dir, 'mods')) + + # Ok, now add these to sys.path. + + # First off, strip out any instances of the path containing this + # module. We will probably be re-adding the same path in a + # moment but its technically possible that we won't be (if + # app_python_dir is overridden to somewhere else, etc.) + our_parent_path = Path(__file__).parent.resolve() + paths: list[str] = [ + p for p in sys.path if Path(p).resolve() != our_parent_path + ] + # Let's lookup mods first (so users can do whatever they want). + # and then our bundled scripts last (don't want bundled + # site-package stuff overwriting system versions) + paths.insert(0, user_python_dir) + paths.append(app_python_dir) + paths.append(site_python_dir) + sys.path = paths + + # Attempt to create the dirs that we'll write stuff to. Not the end + # of the world if we fail though. + create_dirs: list[tuple[str, str | None]] = [ + ('config', config_dir), + ('user_python', user_python_dir), + ] + for cdirname, cdir in create_dirs: + if cdir is not None: + try: + os.makedirs(cdir, exist_ok=True) + except Exception: + logging.warning( + "Unable to create %s dir at '%s'.", cdirname, cdir + ) + + _g_env_config = EnvConfig( + config_dir=config_dir, + data_dir=data_dir, + user_python_dir=user_python_dir, + app_python_dir=app_python_dir, + site_python_dir=site_python_dir, + log_handler=log_handler, + ) + + # In embedded situations (when we're providing our own Python) let's + # also provide our own root certs so ssl works. We can consider + # overriding this in particular embedded cases if we can verify that + # system certs are working. (We also allow forcing this via an env + # var if the user desires) + if ( + contains_python_dist + or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1' + ): + import certifi + + # Let both OpenSSL and requests (if present) know to use this. + os.environ['SSL_CERT_FILE'] = os.environ[ + 'REQUESTS_CA_BUNDLE' + ] = certifi.where() + + +def on_babase_import() -> None: + """Should be called just after _babase is imported. + + Sets up logging and issue warnings if anything in the running + _babase environment seems wonky. Many significant environment + modifications such as interrupt handling do not happen until + on_babase_start_app(). This allows bits of _babase to be used under + existing environments without messing things up too badly. + """ + import _babase + + global _g_babase_imported # pylint: disable=global-statement + + assert not _g_babase_imported + _g_babase_imported = True + + # If we have a log_handler set up, wire it up to feed _babase its output. + envconfig = get_config() + if envconfig.log_handler is not None: + _feed_logs_to_babase(envconfig.log_handler) + + env = _babase.pre_env() + + # Give a soft warning if we're being used with a different binary + # version than we were built for. + running_build: int = env['build_number'] + if running_build != TARGET_BALLISTICA_BUILD: + logging.warning( + 'These scripts are meant to be used with' + ' Ballistica build %d, but you are running build %d.' + " This might cause problems. Module path: '%s'.", + TARGET_BALLISTICA_BUILD, + running_build, + __file__, + ) + + debug_build = env['debug_build'] + + # We expect dev_mode on in debug builds and off otherwise; + # make noise if that's not the case. + if debug_build != sys.flags.dev_mode: + logging.warning( + 'Mismatch in ballistica debug_build %s' + ' and sys.flags.dev_mode %s; this may cause problems.', + debug_build, + sys.flags.dev_mode, + ) + + +def on_babase_start_app() -> None: + """Called when ballistica's babase module is spinning up an app.""" + import gc + import _babase + + global _g_babase_app_started # pylint: disable=global-statement + + _g_babase_app_started = True + + assert _g_babase_imported + assert config_exists() + + # If we were unable to set paths earlier, complain now. + if _g_paths_set_failed: + logging.warning( + 'Ballistica Python paths have not been set. This may cause' + ' problems. To ensure paths are set, run baenv.configure()' + ' before importing any ballistica modules.' + ) + + # Set up interrupt-signal handling. + + # Note: I've found we need to set up our C signal handling AFTER + # we've told Python to disable its own; otherwise (on Mac at least) + # it wipes out our existing C handler. + signal.signal(signal.SIGINT, signal.SIG_DFL) # Do default handling. + _babase.setup_sigint() + + # Turn off fancy-pants cyclic garbage-collection. We run it only at + # explicit times to avoid random hitches and keep things more + # deterministic. Non-reference-looped objects will still get cleaned + # up immediately, so we should try to structure things to avoid + # reference loops (just like Swift, ObjC, etc). + + # FIXME - move this to Python bootstrapping code. or perhaps disable + # it completely since we've got more bg stuff happening now?... + # (but put safeguards in place to time/minimize gc pauses). + gc.disable() + + # pylint: disable=c-extension-no-member + if not TYPE_CHECKING: + import __main__ + + # Clear out the standard quit/exit messages since they don't + # work in our embedded situation (should revisit this once we're + # usable from a standard interpreter). Note that these don't + # exist in the first place for our monolithic builds which don't + # use site.py. + for attr in ('quit', 'exit'): + if hasattr(__main__.__builtins__, attr): + delattr(__main__.__builtins__, attr) + + # Also replace standard interactive help with our simplified + # non-blocking one which is more friendly to cloud/in-app console + # situations. + __main__.__builtins__.help = _CustomHelper() + + # On Windows I'm seeing the following error creating asyncio loops + # in background threads with the default proactor setup: + + # ValueError: set_wakeup_fd only works in main thread of the main + # interpreter. + + # So let's explicitly request selector loops. Interestingly this + # error only started showing up once I moved Python init to the main + # thread; previously the various asyncio bg thread loops were + # working fine (maybe something caused them to default to selector + # in that case?.. + if sys.platform == 'win32': + import asyncio + + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + +class _CustomHelper: + """Replacement 'help' that behaves better for our setup.""" + + def __repr__(self) -> str: + return 'Type help(object) for help about object.' + + def __call__(self, *args: Any, **kwds: Any) -> Any: + # We get an ugly error importing pydoc on our embedded platforms + # due to _sysconfigdata_xxx.py not being present (but then + # things mostly work). Let's get the ugly error out of the way + # explicitly. + + # FIXME: we shouldn't be seeing this error anymore. Should + # revisit this. + import sysconfig + + try: + # This errors once but seems to run cleanly after, so let's + # get the error out of the way. + sysconfig.get_path('stdlib') + except ModuleNotFoundError: + pass + + import pydoc + + # Disable pager and interactive help since neither works well + # with our funky multi-threaded setup or in-game/cloud consoles. + # Let's just do simple text dumps. + pydoc.pager = pydoc.plainpager + if not args and not kwds: + print( + 'Interactive help is not available in this environment.\n' + 'Type help(object) for help about object.' + ) + return None + return pydoc.help(*args, **kwds) + + +def _feed_logs_to_babase(log_handler: LogHandler) -> None: + """Route log/print output to internal ballistica console/etc.""" + import _babase + + def _on_log(entry: LogEntry) -> None: + # Forward this along to the engine to display in the in-app + # console, in the Android log, etc. + _babase.display_log( + name=entry.name, + level=entry.level.name, + message=entry.message, + ) + + # We also want to feed some logs to the old v1-cloud-log system. + # Let's go with anything warning or higher as well as the + # stdout/stderr log messages that babase.app.log_handler creates + # for us. We should retire or upgrade this system at some point. + if entry.level.value >= LogLevel.WARNING.value or entry.name in ( + 'stdout', + 'stderr', + ): + _babase.v1_cloud_log(entry.message) + + # Add our callback and also feed it all entries already in the + # cache. This will feed the engine any logs that happened between + # baenv.configure() and now. + + # FIXME: while this works for now, the downside is that these + # callbacks fire in a bg thread so certain things like android + # logging will be delayed compared to code that uses native logging + # calls directly. Perhaps we should add some sort of 'immediate' + # callback option to better handle such cases (similar to the + # immediate echofile stderr print that already occurs). + log_handler.add_callback(_on_log, feed_existing_logs=True) diff --git a/src/assets/ba_data/python/baplus/__init__.py b/src/assets/ba_data/python/baplus/__init__.py new file mode 100644 index 00000000..e2ff4250 --- /dev/null +++ b/src/assets/ba_data/python/baplus/__init__.py @@ -0,0 +1,30 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Closed-source bits of ballistica.""" + +from __future__ import annotations + +# Note: there's not much here. +# All comms with this feature-set should go through app.plus. + +import logging + +from baplus._subsystem import PlusSubsystem + +__all__ = [ + 'PlusSubsystem', +] + +# Sanity check: we want to keep ballistica's dependencies and +# bootstrapping order clearly defined; let's check a few particular +# modules to make sure they never directly or indirectly import us +# before their own execs complete. +if __debug__: + for _mdl in 'babase', '_babase': + if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'): + logging.warning( + '%s was imported before %s finished importing;' + ' should not happen.', + __name__, + _mdl, + ) diff --git a/src/assets/ba_data/python/baplus/_hooks.py b/src/assets/ba_data/python/baplus/_hooks.py new file mode 100644 index 00000000..03489142 --- /dev/null +++ b/src/assets/ba_data/python/baplus/_hooks.py @@ -0,0 +1,15 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Snippets of code for use by the c++ layer.""" +# (most of these are self-explanatory) +# pylint: disable=missing-function-docstring +from __future__ import annotations + +import _baplus + + +def submit_analytics_counts(sval: str) -> None: + _baplus.add_v1_account_transaction( + {'type': 'ANALYTICS_COUNTS', 'values': sval} + ) + _baplus.run_v1_account_transactions() diff --git a/src/assets/ba_data/python/baplus/_subsystem.py b/src/assets/ba_data/python/baplus/_subsystem.py new file mode 100644 index 00000000..47a036e3 --- /dev/null +++ b/src/assets/ba_data/python/baplus/_subsystem.py @@ -0,0 +1,248 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides classic app subsystem.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _baplus + +if TYPE_CHECKING: + from typing import Callable, Any + + +class PlusSubsystem: + """Subsystem for plus functionality in the app. + + The single shared instance of this app can be accessed at + babase.app.plus. Note that it is possible for this to be None if the + plus package is not present, and code should handle that case + gracefully. + """ + + # pylint: disable=too-many-public-methods + + # Note: this is basically just a wrapper around _baplus for + # type-checking purposes. Maybe there's some smart way we could skip + # the overhead of this wrapper at runtime. + + def __init__(self) -> None: + pass + + @staticmethod + def add_v1_account_transaction( + transaction: dict, callback: Callable | None = None + ) -> None: + """(internal)""" + return _baplus.add_v1_account_transaction(transaction, callback) + + @staticmethod + def game_service_has_leaderboard(game: str, config: str) -> bool: + """(internal) + + Given a game and config string, returns whether there is a leaderboard + for it on the game service. + """ + return _baplus.game_service_has_leaderboard(game, config) + + @staticmethod + def get_master_server_address(source: int = -1, version: int = 1) -> str: + """(internal) + + Return the address of the master server. + """ + return _baplus.get_master_server_address(source, version) + + @staticmethod + def get_news_show() -> str: + """(internal)""" + return _baplus.get_news_show() + + @staticmethod + def get_price(item: str) -> str | None: + """(internal)""" + return _baplus.get_price(item) + + @staticmethod + def get_purchased(item: str) -> bool: + """(internal)""" + return _baplus.get_purchased(item) + + @staticmethod + def get_purchases_state() -> int: + """(internal)""" + return _baplus.get_purchases_state() + + @staticmethod + def get_v1_account_display_string(full: bool = True) -> str: + """(internal)""" + return _baplus.get_v1_account_display_string(full) + + @staticmethod + def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any: + """(internal)""" + return _baplus.get_v1_account_misc_read_val(name, default_value) + + @staticmethod + def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any: + """(internal)""" + return _baplus.get_v1_account_misc_read_val_2(name, default_value) + + @staticmethod + def get_v1_account_misc_val(name: str, default_value: Any) -> Any: + """(internal)""" + return _baplus.get_v1_account_misc_val(name, default_value) + + @staticmethod + def get_v1_account_name() -> str: + """(internal)""" + return _baplus.get_v1_account_name() + + @staticmethod + def get_v1_account_public_login_id() -> str | None: + """(internal)""" + return _baplus.get_v1_account_public_login_id() + + @staticmethod + def get_v1_account_state() -> str: + """(internal)""" + return _baplus.get_v1_account_state() + + @staticmethod + def get_v1_account_state_num() -> int: + """(internal)""" + return _baplus.get_v1_account_state_num() + + @staticmethod + def get_v1_account_ticket_count() -> int: + """(internal) + + Returns the number of tickets for the current account. + """ + return _baplus.get_v1_account_ticket_count() + + @staticmethod + def get_v1_account_type() -> str: + """(internal)""" + return _baplus.get_v1_account_type() + + @staticmethod + def get_v2_fleet() -> str: + """(internal)""" + return _baplus.get_v2_fleet() + + @staticmethod + def have_outstanding_v1_account_transactions() -> bool: + """(internal)""" + return _baplus.have_outstanding_v1_account_transactions() + + @staticmethod + def in_game_purchase(item: str, price: int) -> None: + """(internal)""" + return _baplus.in_game_purchase(item, price) + + @staticmethod + def is_blessed() -> bool: + """(internal)""" + return _baplus.is_blessed() + + @staticmethod + def mark_config_dirty() -> None: + """(internal) + + Category: General Utility Functions + """ + return _baplus.mark_config_dirty() + + @staticmethod + def on_app_launching() -> None: + """(internal)""" + return _baplus.on_app_launching() + + @staticmethod + def power_ranking_query(callback: Callable, season: Any = None) -> None: + """(internal)""" + return _baplus.power_ranking_query(callback, season) + + @staticmethod + def purchase(item: str) -> None: + """(internal)""" + return _baplus.purchase(item) + + @staticmethod + def report_achievement( + achievement: str, pass_to_account: bool = True + ) -> None: + """(internal)""" + return _baplus.report_achievement(achievement, pass_to_account) + + @staticmethod + def reset_achievements() -> None: + """(internal)""" + return _baplus.reset_achievements() + + @staticmethod + def restore_purchases() -> None: + """(internal)""" + return _baplus.restore_purchases() + + @staticmethod + def run_v1_account_transactions() -> None: + """(internal)""" + return _baplus.run_v1_account_transactions() + + @staticmethod + def sign_in_v1(account_type: str) -> None: + """(internal) + + Category: General Utility Functions + """ + return _baplus.sign_in_v1(account_type) + + @staticmethod + def sign_out_v1(v2_embedded: bool = False) -> None: + """(internal) + + Category: General Utility Functions + """ + return _baplus.sign_out_v1(v2_embedded) + + @staticmethod + def submit_score( + game: str, + config: str, + name: Any, + score: int | None, + callback: Callable, + order: str = 'increasing', + tournament_id: str | None = None, + score_type: str = 'points', + campaign: str | None = None, + level: str | None = None, + ) -> None: + """(internal) + + Submit a score to the server; callback will be called with the results. + As a courtesy, please don't send fake scores to the server. I'd prefer + to devote my time to improving the game instead of trying to make the + score server more mischief-proof. + """ + return _baplus.submit_score( + game, + config, + name, + score, + callback, + order, + tournament_id, + score_type, + campaign, + level, + ) + + @staticmethod + def tournament_query( + callback: Callable[[dict | None], None], args: dict + ) -> None: + """(internal)""" + return _baplus.tournament_query(callback, args) diff --git a/src/assets/ba_data/python/bascenev1/__init__.py b/src/assets/ba_data/python/bascenev1/__init__.py new file mode 100644 index 00000000..fb01b078 --- /dev/null +++ b/src/assets/ba_data/python/bascenev1/__init__.py @@ -0,0 +1,400 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Ballistica Scene Version 1""" + +# ba_meta require api 8 + +# The stuff we expose here at the top level is our 'public' api. +# It should only be imported by code outside of this package or +# from 'if TYPE_CHECKING' blocks (which will not exec at runtime). +# Code within our package should import things directly from their +# submodules. + +from __future__ import annotations + +import logging + +# Aside from our own stuff, we also bundle a number of things from ba or +# other modules; the goal is to let most simple mods rely solely on this +# module to keep things simple. + +from _babase import ( + app, + ContextRef, + lock_all_input, + unlock_all_input, + fade_screen, + safecolor, + pushcall, + Vec3, + increment_analytics_count, + set_analytics_screen, + apptime, + apptimer, + AppTimer, + displaytime, + displaytimer, + DisplayTimer, +) +from babase._error import NotFoundError, NodeNotFoundError, ContextError +from babase._language import Lstr +from babase._general import ( + WeakCall, + Call, + storagename, + existing, + AppTime, + DisplayTime, +) +from babase._math import is_point_in_box, normalized_color +from babase._text import timestring +from babase._apputils import get_remote_app_name + +from babase._mgen.enums import ( + UIScale, + InputType, +) + + +from _bascenev1 import ( + get_foreground_host_session, + get_foreground_host_activity, + get_game_roster, + set_debug_speed_exponent, + get_replay_speed_exponent, + set_replay_speed_exponent, + reset_random_player_names, + get_random_names, + screenmessage, + set_public_party_stats_url, + set_admins, + set_enable_default_kick_voting, + have_connected_clients, + is_in_replay, + client_info_query_response, + disconnect_from_host, + set_public_party_queue_enabled, + set_public_party_max_size, + set_authenticate_clients, + set_public_party_enabled, + get_game_port, + set_public_party_name, + get_public_party_enabled, + get_public_party_max_size, + connect_to_party, + host_scan_cycle, + end_host_scanning, + set_touchscreen_editing, + get_ui_input_device, + get_local_active_input_devices_count, + have_touchscreen_input, + capture_keyboard_input, + release_keyboard_input, + capture_gamepad_input, + release_gamepad_input, + newactivity, + set_map_bounds, + get_connection_to_host_info, + newnode, + new_replay_session, + new_host_session, + getsession, + InputDevice, + SessionPlayer, + Material, + ActivityData, + camerashake, + emitfx, + ls_objects, + ls_input_devices, + CollisionMesh, + getcollisionmesh, + Data, + getdata, + Mesh, + getmesh, + SessionData, + Sound, + getsound, + getnodes, + printnodes, + getactivity, + time, + timer, + Node, + Texture, + Timer, + gettexture, + getinputdevice, + disconnect_client, + chatmessage, + get_chat_messages, + basetime, + basetimer, + BaseTimer, +) + + +from bascenev1._session import Session +from bascenev1._map import Map +from bascenev1._coopsession import CoopSession +from bascenev1._debug import print_live_object_warnings +from bascenev1._multiteamsession import MultiTeamSession +from bascenev1._coopgame import CoopGameActivity +from bascenev1._freeforallsession import FreeForAllSession +from bascenev1._gameactivity import GameActivity +from bascenev1._score import ScoreType, ScoreConfig +from bascenev1._dualteamsession import DualTeamSession +from bascenev1._messages import ( + UNHANDLED, + OutOfBoundsMessage, + DeathType, + DieMessage, + PlayerDiedMessage, + StandMessage, + PickUpMessage, + DropMessage, + PickedUpMessage, + DroppedMessage, + ShouldShatterMessage, + ImpactDamageMessage, + FreezeMessage, + ThawMessage, + HitMessage, + CelebrateMessage, +) +from bascenev1._player import PlayerInfo, Player, EmptyPlayer, StandLocation +from bascenev1._activity import Activity +from bascenev1._actor import Actor +from bascenev1._gameresults import GameResults +from bascenev1._nodeactor import NodeActor +from bascenev1._collision import Collision, getcollision +from bascenev1._powerup import PowerupMessage, PowerupAcceptMessage +from bascenev1._team import SessionTeam, Team, EmptyTeam +from bascenev1._gameutils import ( + Time, + BaseTime, + GameTip, + animate, + animate_array, + show_damage_count, + cameraflash, +) +from bascenev1._teamgame import TeamGameActivity +from bascenev1._stats import PlayerScoredMessage, PlayerRecord, Stats +from bascenev1._settings import ( + Setting, + IntSetting, + FloatSetting, + ChoiceSetting, + BoolSetting, + IntChoiceSetting, + FloatChoiceSetting, +) +from bascenev1._activitytypes import JoinActivity, ScoreScreenActivity +from bascenev1._music import MusicType, setmusic +from bascenev1._dependency import ( + Dependency, + DependencyComponent, + DependencySet, + AssetPackage, +) + +# if TYPE_CHECKING: +# from babase._app import App + +# app: App + +__all__ = [ + 'app', + 'get_local_active_input_devices_count', + 'lock_all_input', + 'unlock_all_input', + 'getinputdevice', + 'Session', + 'Map', + 'CoopSession', + 'MultiTeamSession', + 'CoopGameActivity', + 'print_live_object_warnings', + 'FreeForAllSession', + 'GameActivity', + 'ScoreType', + 'ScoreConfig', + 'DualTeamSession', + 'UNHANDLED', + 'OutOfBoundsMessage', + 'DeathType', + 'DieMessage', + 'DropMessage', + 'DroppedMessage', + 'FreezeMessage', + 'HitMessage', + 'ImpactDamageMessage', + 'Node', + 'PickedUpMessage', + 'PickUpMessage', + 'PlayerDiedMessage', + 'ShouldShatterMessage', + 'StandMessage', + 'ThawMessage', + 'CelebrateMessage', + 'EmptyPlayer', + 'Player', + 'PlayerInfo', + 'StandLocation', + 'Activity', + 'Actor', + 'GameResults', + 'NodeActor', + 'Collision', + 'getcollision', + 'PowerupMessage', + 'PowerupAcceptMessage', + 'SessionTeam', + 'Team', + 'EmptyTeam', + 'GameTip', + 'animate', + 'animate_array', + 'show_damage_count', + 'cameraflash', + 'TeamGameActivity', + 'PlayerScoredMessage', + 'PlayerRecord', + 'Stats', + 'Setting', + 'IntSetting', + 'FloatSetting', + 'ChoiceSetting', + 'BoolSetting', + 'IntChoiceSetting', + 'FloatChoiceSetting', + 'JoinActivity', + 'ScoreScreenActivity', + 'MusicType', + 'setmusic', + 'newnode', + 'new_host_session', + 'getsession', + 'get_foreground_host_session', + 'get_foreground_host_activity', + 'InputDevice', + 'SessionPlayer', + 'Material', + 'ActivityData', + 'camerashake', + 'emitfx', + 'ls_objects', + 'ls_input_devices', + 'CollisionMesh', + 'getcollisionmesh', + 'Dependency', + 'DependencyComponent', + 'AssetPackage', + 'DependencySet', + 'Data', + 'getdata', + 'Mesh', + 'getmesh', + 'SessionData', + 'Sound', + 'getsound', + 'getnodes', + 'printnodes', + 'getactivity', + 'time', + 'timer', + 'Texture', + 'Vec3', + 'NotFoundError', + 'NodeNotFoundError', + 'Timer', + 'Lstr', + 'gettexture', + 'WeakCall', + 'Call', + 'new_replay_session', + 'increment_analytics_count', + 'set_analytics_screen', + 'set_debug_speed_exponent', + 'screenmessage', + 'InputType', + 'UIScale', + 'pushcall', + 'is_point_in_box', + 'safecolor', + 'storagename', + 'timestring', + 'get_game_roster', + 'disconnect_client', + 'get_connection_to_host_info', + 'chatmessage', + 'get_chat_messages', + 'existing', + 'set_map_bounds', + 'normalized_color', + 'get_remote_app_name', + 'newactivity', + 'ContextError', + 'fade_screen', + 'capture_keyboard_input', + 'release_keyboard_input', + 'capture_gamepad_input', + 'release_gamepad_input', + 'have_touchscreen_input', + 'get_ui_input_device', + 'set_touchscreen_editing', + 'end_host_scanning', + 'host_scan_cycle', + 'connect_to_party', + 'get_public_party_enabled', + 'get_public_party_max_size', + 'set_public_party_name', + 'get_game_port', + 'set_public_party_enabled', + 'set_authenticate_clients', + 'set_public_party_max_size', + 'set_public_party_queue_enabled', + 'disconnect_from_host', + 'client_info_query_response', + 'is_in_replay', + 'have_connected_clients', + 'set_enable_default_kick_voting', + 'set_admins', + 'set_public_party_stats_url', + 'get_random_names', + 'reset_random_player_names', + 'get_replay_speed_exponent', + 'set_replay_speed_exponent', + 'set_debug_speed_exponent', + 'get_game_roster', + 'AppTime', + 'apptime', + 'apptimer', + 'AppTimer', + 'ContextRef', + 'basetime', + 'basetimer', + 'BaseTimer', + 'displaytime', + 'DisplayTime', + 'displaytimer', + 'DisplayTimer', + 'Time', + 'BaseTime', +] + +# Sanity check: we want to keep ballistica's dependencies and +# bootstrapping order clearly defined; let's check a few particular +# modules to make sure they never directly or indirectly import us +# before their own execs complete. +if __debug__: + for _mdl in 'babase', '_babase': + if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'): + logging.warning( + '%s was imported before %s finished importing;' + ' should not happen.', + __name__, + _mdl, + ) diff --git a/assets/src/ba_data/python/ba/_activity.py b/src/assets/ba_data/python/bascenev1/_activity.py similarity index 79% rename from assets/src/ba_data/python/ba/_activity.py rename to src/assets/ba_data/python/bascenev1/_activity.py index ff0acb57..87cf9560 100644 --- a/assets/src/ba_data/python/ba/_activity.py +++ b/src/assets/ba_data/python/bascenev1/_activity.py @@ -6,37 +6,37 @@ from __future__ import annotations import weakref from typing import TYPE_CHECKING, Generic, TypeVar -import _ba -from ba._team import Team -from ba._player import Player -from ba._error import ( +import _babase +from babase._error import ( print_exception, SessionTeamNotFoundError, SessionPlayerNotFoundError, NodeNotFoundError, ) -from ba._dependency import DependencyComponent -from ba._general import Call, verify_object_death -from ba._messages import UNHANDLED +from babase._general import Call, verify_object_death +import _bascenev1 +from bascenev1._dependency import DependencyComponent +from bascenev1._team import Team +from bascenev1._messages import UNHANDLED +from bascenev1._player import Player if TYPE_CHECKING: from typing import Any - import ba + import baclassic + import bascenev1 -# pylint: disable=invalid-name -PlayerType = TypeVar('PlayerType', bound=Player) -TeamType = TypeVar('TeamType', bound=Team) -# pylint: enable=invalid-name +PlayerT = TypeVar('PlayerT', bound=Player) +TeamT = TypeVar('TeamT', bound=Team) -class Activity(DependencyComponent, Generic[PlayerType, TeamType]): - """Units of execution wrangled by a ba.Session. +class Activity(DependencyComponent, Generic[PlayerT, TeamT]): + """Units of execution wrangled by a bascenev1.Session. Category: Gameplay Classes Examples of Activities include games, score-screens, cutscenes, etc. - A ba.Session has one 'current' Activity at any time, though their existence - can overlap during transitions. + A bascenev1.Session has one 'current' Activity at any time, though + their existence can overlap during transitions. """ # pylint: disable=too-many-public-methods @@ -47,17 +47,17 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): activities should pull all values they need from the 'settings' arg passed to the Activity __init__ call.""" - teams: list[TeamType] - """The list of ba.Team-s in the Activity. This gets populated just + teams: list[TeamT] + """The list of bascenev1.Team-s in the Activity. This gets populated just before on_begin() is called and is updated automatically as players join or leave the game. (at least in free-for-all mode where every player gets their own team; in teams mode there are always 2 teams regardless of the player count).""" - players: list[PlayerType] - """The list of ba.Player-s in the Activity. This gets populated just - before on_begin() is called and is updated automatically as players - join or leave the game.""" + players: list[PlayerT] + """The list of bascenev1.Player-s in the Activity. This gets populated + just before on_begin() is called and is updated automatically as + players join or leave the game.""" announce_player_deaths = False """Whether to print every time a player dies. This can be pertinent @@ -125,11 +125,11 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): the next activity?""" def __init__(self, settings: dict): - """Creates an Activity in the current ba.Session. + """Creates an Activity in the current bascenev1.Session. - The activity will not be actually run until ba.Session.setactivity - is called. 'settings' should be a dict of key/value pairs specific - to the activity. + The activity will not be actually run until + bascenev1.Session.setactivity is called. 'settings' should be a + dict of key/value pairs specific to the activity. Activities should preload as much of their media/etc as possible in their constructor, but none of it should actually be used until they @@ -138,23 +138,23 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): super().__init__() # Create our internal engine data. - self._activity_data = _ba.register_activity(self) + self._activity_data = _bascenev1.register_activity(self) assert isinstance(settings, dict) - assert _ba.getactivity() is self + assert _bascenev1.getactivity() is self - self._globalsnode: ba.Node | None = None + self._globalsnode: bascenev1.Node | None = None # Player/Team types should have been specified as type args; # grab those. - self._playertype: type[PlayerType] - self._teamtype: type[TeamType] + self._playertype: type[PlayerT] + self._teamtype: type[TeamT] self._setup_player_and_team_types() # FIXME: Relocate or remove the need for this stuff. - self.paused_text: ba.Actor | None = None + self.paused_text: bascenev1.Actor | None = None - self._session = weakref.ref(_ba.getsession()) + self._session = weakref.ref(_bascenev1.getsession()) # Preloaded data for actors, maps, etc; indexed by type. self.preloads: dict[type, Any] = {} @@ -167,42 +167,41 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): self._has_transitioned_in = False self._has_begun = False self._has_ended = False - self._activity_death_check_timer: ba.Timer | None = None + self._activity_death_check_timer: bascenev1.AppTimer | None = None self._expired = False - self._delay_delete_players: list[PlayerType] = [] - self._delay_delete_teams: list[TeamType] = [] - self._players_that_left: list[weakref.ref[PlayerType]] = [] - self._teams_that_left: list[weakref.ref[TeamType]] = [] + self._delay_delete_players: list[PlayerT] = [] + self._delay_delete_teams: list[TeamT] = [] + self._players_that_left: list[weakref.ref[PlayerT]] = [] + self._teams_that_left: list[weakref.ref[TeamT]] = [] self._transitioning_out = False # A handy place to put most actors; this list is pruned of dead # actors regularly and these actors are insta-killed as the activity # is dying. - self._actor_refs: list[ba.Actor] = [] - self._actor_weak_refs: list[weakref.ref[ba.Actor]] = [] - self._last_prune_dead_actors_time = _ba.time() - self._prune_dead_actors_timer: ba.Timer | None = None + self._actor_refs: list[bascenev1.Actor] = [] + self._actor_weak_refs: list[weakref.ref[bascenev1.Actor]] = [] + self._last_prune_dead_actors_time = _babase.apptime() + self._prune_dead_actors_timer: bascenev1.Timer | None = None self.teams = [] self.players = [] self.lobby = None - self._stats: ba.Stats | None = None + self._stats: bascenev1.Stats | None = None self._customdata: dict | None = {} def __del__(self) -> None: - # If the activity has been run then we should have already cleaned # it up, but we still need to run expire calls for un-run activities. if not self._expired: - with _ba.Context('empty'): + with _babase.ContextRef.empty(): self._expire() # Inform our owner that we officially kicked the bucket. if self._transitioning_out: session = self._session() if session is not None: - _ba.pushcall( + _babase.pushcall( Call( session.transitioning_out_activity_was_freed, self.can_show_ad_on_death, @@ -210,8 +209,13 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): ) @property - def globalsnode(self) -> ba.Node: - """The 'globals' ba.Node for the activity. This contains various + def context(self) -> bascenev1.ContextRef: + """A context-ref pointing at this activity.""" + return self._activity_data.context() + + @property + def globalsnode(self) -> bascenev1.Node: + """The 'globals' bascenev1.Node for the activity. This contains various global controls and values. """ node = self._globalsnode @@ -220,13 +224,14 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): return node @property - def stats(self) -> ba.Stats: + def stats(self) -> bascenev1.Stats: """The stats instance accessible while the activity is running. - If access is attempted before or after, raises a ba.NotFoundError. + If access is attempted before or after, raises a + bascenev1.NotFoundError. """ if self._stats is None: - from ba._error import NotFoundError + from babase._error import NotFoundError raise NotFoundError() return self._stats @@ -263,13 +268,13 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): return self._expired @property - def playertype(self) -> type[PlayerType]: - """The type of ba.Player this Activity is using.""" + def playertype(self) -> type[PlayerT]: + """The type of bascenev1.Player this Activity is using.""" return self._playertype @property - def teamtype(self) -> type[TeamType]: - """The type of ba.Team this Activity is using.""" + def teamtype(self) -> type[TeamT]: + """The type of bascenev1.Team this Activity is using.""" return self._teamtype def set_has_ended(self, val: bool) -> None: @@ -281,19 +286,15 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): (internal) """ - from ba._generated.enums import TimeType - # Create a real-timer that watches a weak-ref of this activity + # Create an app-timer that watches a weak-ref of this activity # and reports any lingering references keeping it alive. # We store the timer on the activity so as soon as the activity dies # it gets cleaned up. - with _ba.Context('ui'): + with _babase.ContextRef.empty(): ref = weakref.ref(self) - self._activity_death_check_timer = _ba.Timer( - 5.0, - Call(self._check_activity_death, ref, [0]), - repeat=True, - timetype=TimeType.REAL, + self._activity_death_check_timer = _babase.AppTimer( + 5.0, Call(self._check_activity_death, ref, [0]), repeat=True ) # Run _expire in an empty context; nothing should be happening in @@ -302,67 +303,67 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): # and we can't properly provide context in that situation anyway; might # as well be consistent). if not self._expired: - with _ba.Context('empty'): + with _babase.ContextRef.empty(): self._expire() else: raise RuntimeError( f'destroy() called when' f' already expired for {self}' ) - def retain_actor(self, actor: ba.Actor) -> None: - """Add a strong-reference to a ba.Actor to this Activity. + def retain_actor(self, actor: bascenev1.Actor) -> None: + """Add a strong-reference to a bascenev1.Actor to this Activity. - The reference will be lazily released once ba.Actor.exists() - returns False for the Actor. The ba.Actor.autoretain() method + The reference will be lazily released once bascenev1.Actor.exists() + returns False for the Actor. The bascenev1.Actor.autoretain() method is a convenient way to access this same functionality. """ if __debug__: - from ba._actor import Actor + from bascenev1._actor import Actor assert isinstance(actor, Actor) self._actor_refs.append(actor) - def add_actor_weak_ref(self, actor: ba.Actor) -> None: - """Add a weak-reference to a ba.Actor to the ba.Activity. + def add_actor_weak_ref(self, actor: bascenev1.Actor) -> None: + """Add a weak-reference to a bascenev1.Actor to the bascenev1.Activity. - (called by the ba.Actor base class) + (called by the bascenev1.Actor base class) """ if __debug__: - from ba._actor import Actor + from bascenev1._actor import Actor assert isinstance(actor, Actor) self._actor_weak_refs.append(weakref.ref(actor)) @property - def session(self) -> ba.Session: - """The ba.Session this ba.Activity belongs go. + def session(self) -> bascenev1.Session: + """The bascenev1.Session this bascenev1.Activity belongs to. - Raises a ba.SessionNotFoundError if the Session no longer exists. + Raises a babase.SessionNotFoundError if the Session no longer exists. """ session = self._session() if session is None: - from ba._error import SessionNotFoundError + from babase._error import SessionNotFoundError raise SessionNotFoundError() return session - def on_player_join(self, player: PlayerType) -> None: - """Called when a new ba.Player has joined the Activity. + def on_player_join(self, player: PlayerT) -> None: + """Called when a new bascenev1.Player has joined the Activity. (including the initial set of Players) """ - def on_player_leave(self, player: PlayerType) -> None: - """Called when a ba.Player is leaving the Activity.""" + def on_player_leave(self, player: PlayerT) -> None: + """Called when a bascenev1.Player is leaving the Activity.""" - def on_team_join(self, team: TeamType) -> None: - """Called when a new ba.Team joins the Activity. + def on_team_join(self, team: TeamT) -> None: + """Called when a new bascenev1.Team joins the Activity. (including the initial set of Teams) """ - def on_team_leave(self, team: TeamType) -> None: - """Called when a ba.Team leaves the Activity.""" + def on_team_leave(self, team: TeamT) -> None: + """Called when a bascenev1.Team leaves the Activity.""" def on_transition_in(self) -> None: """Called when the Activity is first becoming visible. @@ -370,18 +371,18 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity - up until ba.Activity.on_begin() is called. + up until bascenev1.Activity.on_begin() is called. """ def on_transition_out(self) -> None: """Called when your activity begins transitioning out. - Note that this may happen at any time even if ba.Activity.end() has - not been called. + Note that this may happen at any time even if bascenev1.Activity.end() + has not been called. """ def on_begin(self) -> None: - """Called once the previous ba.Activity has finished transitioning out. + """Called once the previous Activity has finished transitioning out. At this point the activity's initial players and teams are filled in and it should begin its actual game logic. @@ -393,12 +394,11 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): return UNHANDLED def has_transitioned_in(self) -> bool: - """Return whether ba.Activity.on_transition_in() - has been called.""" + """Return whether bascenev1.Activity.on_transition_in() has run.""" return self._has_transitioned_in def has_begun(self) -> bool: - """Return whether ba.Activity.on_begin() has been called.""" + """Return whether bascenev1.Activity.on_begin() has run.""" return self._has_begun def has_ended(self) -> bool: @@ -406,10 +406,10 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): return self._has_ended def is_transitioning_out(self) -> bool: - """Return whether ba.Activity.on_transition_out() has been called.""" + """Return whether bascenev1.Activity.on_transition_out() has run.""" return self._transitioning_out - def transition_in(self, prev_globals: ba.Node | None) -> None: + def transition_in(self, prev_globals: bascenev1.Node | None) -> None: """Called by Session to kick off transition-in. (internal) @@ -418,8 +418,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): self._has_transitioned_in = True # Set up the globals node based on our settings. - with _ba.Context(self): - glb = self._globalsnode = _ba.newnode('globals') + with self.context: + glb = self._globalsnode = _bascenev1.newnode('globals') # Now that it's going to be front and center, # set some global values based on what the activity wants. @@ -449,11 +449,11 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): # Start pruning our various things periodically. self._prune_dead_actors() - self._prune_dead_actors_timer = _ba.Timer( + self._prune_dead_actors_timer = _bascenev1.Timer( 5.17, self._prune_dead_actors, repeat=True ) - _ba.timer(13.3, self._prune_delay_deletes, repeat=True) + _bascenev1.timer(13.3, self._prune_delay_deletes, repeat=True) # Also start our low-level scene running. self._activity_data.start() @@ -471,13 +471,13 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): """Called by the Session to start us transitioning out.""" assert not self._transitioning_out self._transitioning_out = True - with _ba.Context(self): + with self.context: try: self.on_transition_out() except Exception: print_exception(f'Error in on_transition_out for {self}.') - def begin(self, session: ba.Session) -> None: + def begin(self, session: bascenev1.Session) -> None: """Begin the activity. (internal) @@ -499,7 +499,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): self._has_begun = True # Let the activity do its thing. - with _ba.Context(self): + with self.context: # Note: do we want to catch errors here? # Currently I believe we wind up canceling the # activity launch; just wanna be sure that is intentional. @@ -508,7 +508,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): def end( self, results: Any = None, delay: float = 0.0, force: bool = False ) -> None: - """Commences Activity shutdown and delivers results to the ba.Session. + """Commences Activity shutdown and delivers results to the Session. 'delay' is the time delay before the Activity actually ends (in seconds). Further calls to end() will be ignored up until @@ -519,20 +519,20 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): # Ask the session to end us. self.session.end_activity(self, results, delay, force) - def create_player(self, sessionplayer: ba.SessionPlayer) -> PlayerType: + def create_player(self, sessionplayer: bascenev1.SessionPlayer) -> PlayerT: """Create the Player instance for this Activity. Subclasses can override this if the activity's player class requires a custom constructor; otherwise it will be called with no args. Note that the player object should not be used at this point as it is not yet fully wired up; wait for - ba.Activity.on_player_join() for that. + bascenev1.Activity.on_player_join() for that. """ del sessionplayer # Unused. player = self._playertype() return player - def create_team(self, sessionteam: ba.SessionTeam) -> TeamType: + def create_team(self, sessionteam: bascenev1.SessionTeam) -> TeamT: """Create the Team instance for this Activity. Subclasses can override this if the activity's team class @@ -545,7 +545,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): team = self._teamtype() return team - def add_player(self, sessionplayer: ba.SessionPlayer) -> None: + def add_player(self, sessionplayer: bascenev1.SessionPlayer) -> None: """(internal)""" assert sessionplayer.sessionteam is not None sessionplayer.resetinput() @@ -554,7 +554,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): team = sessionteam.activityteam assert team is not None sessionplayer.setactivity(self) - with _ba.Context(self): + with self.context: sessionplayer.activityplayer = player = self.create_player( sessionplayer ) @@ -573,7 +573,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): except Exception: print_exception(f'Error in on_player_join for {self}.') - def remove_player(self, sessionplayer: ba.SessionPlayer) -> None: + def remove_player(self, sessionplayer: bascenev1.SessionPlayer) -> None: """Remove a player from the Activity while it is running. (internal) @@ -593,11 +593,11 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): self.players.remove(player) assert player not in self.players - # This should allow our ba.Player instance to die. + # This should allow our bascenev1.Player instance to die. # Complain if that doesn't happen. # verify_object_death(player) - with _ba.Context(self): + with self.context: try: self.on_player_leave(player) except Exception: @@ -616,14 +616,14 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): self._delay_delete_players.append(player) self._players_that_left.append(weakref.ref(player)) - def add_team(self, sessionteam: ba.SessionTeam) -> None: + def add_team(self, sessionteam: bascenev1.SessionTeam) -> None: """Add a team to the Activity (internal) """ assert not self.expired - with _ba.Context(self): + with self.context: sessionteam.activityteam = team = self.create_team(sessionteam) team.postinit(sessionteam) self.teams.append(team) @@ -632,7 +632,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): except Exception: print_exception(f'Error in on_team_join for {self}.') - def remove_team(self, sessionteam: ba.SessionTeam) -> None: + def remove_team(self, sessionteam: bascenev1.SessionTeam) -> None: """Remove a team from a Running Activity (internal) @@ -647,7 +647,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): self.teams.remove(team) assert team not in self.teams - with _ba.Context(self): + with self.context: # Make a decent attempt to persevere if user code breaks. try: self.on_team_leave(team) @@ -668,9 +668,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): self._teams_that_left.append(weakref.ref(team)) def _reset_session_player_for_no_activity( - self, sessionplayer: ba.SessionPlayer + self, sessionplayer: bascenev1.SessionPlayer ) -> None: - # Let's be extra-defensive here: killing a node/input-call/etc # could trigger user-code resulting in errors, but we would still # like to complete the reset if possible. @@ -699,7 +698,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): # TODO: There are proper calls for pulling these in Python 3.8; # should update this code when we adopt that. - # NOTE: If we get Any as PlayerType or TeamType (generally due + # NOTE: If we get Any as PlayerT or TeamT (generally due # to no generic params being passed) we automatically use the # base class types, but also warn the user since this will mean # less type safety for that class. (its better to pass the base @@ -710,7 +709,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): self._playertype = Player print( f'ERROR: {type(self)} was not passed a Player' - f' type argument; please explicitly pass ba.Player' + f' type argument; please explicitly pass bascenev1.Player' f' if you do not want to override it.' ) self._teamtype = type(self).__orig_bases__[-1].__args__[1] @@ -718,7 +717,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): self._teamtype = Team print( f'ERROR: {type(self)} was not passed a Team' - f' type argument; please explicitly pass ba.Team' + f' type argument; please explicitly pass bascenev1.Team' f' if you do not want to override it.' ) assert issubclass(self._playertype, Player) @@ -730,8 +729,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): ) -> None: """Sanity check to make sure an Activity was destroyed properly. - Receives a weakref to a ba.Activity which should have torn itself - down due to no longer being referenced anywhere. Will complain + Receives a weakref to a bascenev1.Activity which should have torn + itself down due to no longer being referenced anywhere. Will complain and/or print debugging info if the Activity still exists. """ try: @@ -751,7 +750,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): counter[0] += 1 if counter[0] == 4: print('Killing app due to stuck activity... :-(') - _ba.quit() + _babase.quit() except Exception: print_exception('Error on _check_activity_death/') @@ -804,7 +803,6 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): ) def _expire_players(self) -> None: - # Issue warnings for any players that left the game but don't # get freed soon. for ex_player in (p() for p in self._players_that_left): @@ -812,7 +810,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): verify_object_death(ex_player) for player in self.players: - # This should allow our ba.Player instance to be freed. + # This should allow our bascenev1.Player instance to be freed. # Complain if that doesn't happen. verify_object_death(player) @@ -833,7 +831,6 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): print_exception(f'Error expiring {player}.') def _expire_teams(self) -> None: - # Issue warnings for any teams that left the game but don't # get freed soon. for ex_team in (p() for p in self._teams_that_left): @@ -841,7 +838,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): verify_object_death(ex_team) for team in self.teams: - # This should allow our ba.Team instance to die. + # This should allow our bascenev1.Team instance to die. # Complain if that doesn't happen. verify_object_death(team) @@ -875,7 +872,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): ] def _prune_dead_actors(self) -> None: - self._last_prune_dead_actors_time = _ba.time() + self._last_prune_dead_actors_time = _babase.apptime() # Prune our strong refs when the Actor's exists() call gives False self._actor_refs = [a for a in self._actor_refs if a.exists()] diff --git a/assets/src/ba_data/python/ba/_activitytypes.py b/src/assets/ba_data/python/bascenev1/_activitytypes.py similarity index 78% rename from assets/src/ba_data/python/ba/_activitytypes.py rename to src/assets/ba_data/python/bascenev1/_activitytypes.py index 5fcf5316..4f290b50 100644 --- a/assets/src/ba_data/python/ba/_activitytypes.py +++ b/src/assets/ba_data/python/bascenev1/_activitytypes.py @@ -5,22 +5,24 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba -from ba._activity import Activity -from ba._music import setmusic, MusicType -from ba._generated.enums import InputType, UIScale +import _babase +import _bascenev1 +from babase._mgen.enums import InputType, UIScale +from bascenev1._activity import Activity # False-positive from pylint due to our class-generics-filter. -from ba._player import EmptyPlayer # pylint: disable=W0611 -from ba._team import EmptyTeam # pylint: disable=W0611 +from bascenev1._player import EmptyPlayer # pylint: disable=W0611 +from bascenev1._team import EmptyTeam # pylint: disable=W0611 +from bascenev1._music import MusicType, setmusic if TYPE_CHECKING: - import ba - from ba._lobby import JoinInfo + import babase + import bascenev1 + from baclassic._lobby import JoinInfo class EndSessionActivity(Activity[EmptyPlayer, EmptyTeam]): - """Special ba.Activity to fade out and end the current ba.Session.""" + """Special Activity to fade out and end the current Session.""" def __init__(self, settings: dict): super().__init__(settings) @@ -34,17 +36,20 @@ class EndSessionActivity(Activity[EmptyPlayer, EmptyTeam]): def on_transition_in(self) -> None: super().on_transition_in() - _ba.fade_screen(False) - _ba.lock_all_input() + _babase.fade_screen(False) + _babase.lock_all_input() def on_begin(self) -> None: # pylint: disable=cyclic-import from bastd.mainmenu import MainMenuSession - from ba._general import Call + from babase._general import Call super().on_begin() - _ba.unlock_all_input() - _ba.app.ads.call_after_ad(Call(_ba.new_host_session, MainMenuSession)) + _babase.unlock_all_input() + assert _babase.app.classic is not None + _babase.app.classic.ads.call_after_ad( + Call(_bascenev1.new_host_session, MainMenuSession) + ) class JoinActivity(Activity[EmptyPlayer, EmptyTeam]): @@ -66,8 +71,8 @@ class JoinActivity(Activity[EmptyPlayer, EmptyTeam]): # In vr mode we don't want stuff moving around. self.use_fixed_vr_overlay = True - self._background: ba.Actor | None = None - self._tips_text: ba.Actor | None = None + self._background: bascenev1.Actor | None = None + self._tips_text: bascenev1.Actor | None = None self._join_info: JoinInfo | None = None def on_transition_in(self) -> None: @@ -82,7 +87,7 @@ class JoinActivity(Activity[EmptyPlayer, EmptyTeam]): self._tips_text = TipsText() setmusic(MusicType.CHAR_SELECT) self._join_info = self.session.lobby.create_join_info() - _ba.set_analytics_screen('Joining Screen') + _babase.set_analytics_screen('Joining Screen') class TransitionActivity(Activity[EmptyPlayer, EmptyTeam]): @@ -101,11 +106,11 @@ class TransitionActivity(Activity[EmptyPlayer, EmptyTeam]): def __init__(self, settings: dict): super().__init__(settings) - self._background: ba.Actor | None = None + self._background: bascenev1.Actor | None = None def on_transition_in(self) -> None: # pylint: disable=cyclic-import - from bastd.actor import background # FIXME: Don't use bastd from ba. + from bastd.actor import background # FIXME: Don't use bastd from here. super().on_transition_in() self._background = background.Background( @@ -116,7 +121,7 @@ class TransitionActivity(Activity[EmptyPlayer, EmptyTeam]): super().on_begin() # Die almost immediately. - _ba.timer(0.1, self.end) + _bascenev1.timer(0.1, self.end) class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]): @@ -134,28 +139,28 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]): def __init__(self, settings: dict): super().__init__(settings) - self._birth_time = _ba.time() + self._birth_time = _babase.apptime() self._min_view_time = 5.0 self._allow_server_transition = False - self._background: ba.Actor | None = None - self._tips_text: ba.Actor | None = None + self._background: bascenev1.Actor | None = None + self._tips_text: bascenev1.Actor | None = None self._kicked_off_server_shutdown = False self._kicked_off_server_restart = False self._default_show_tips = True - self._custom_continue_message: ba.Lstr | None = None + self._custom_continue_message: babase.Lstr | None = None self._server_transitioning: bool | None = None def on_player_join(self, player: EmptyPlayer) -> None: - from ba._general import WeakCall + from babase._general import WeakCall super().on_player_join(player) time_till_assign = max( - 0, self._birth_time + self._min_view_time - _ba.time() + 0, self._birth_time + self._min_view_time - _babase.apptime() ) # If we're still kicking at the end of our assign-delay, assign this # guy's input to trigger us. - _ba.timer(time_till_assign, WeakCall(self._safe_assign, player)) + _bascenev1.timer(time_till_assign, WeakCall(self._safe_assign, player)) def on_transition_in(self) -> None: from bastd.actor.tipstext import TipsText @@ -172,14 +177,15 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]): def on_begin(self) -> None: # pylint: disable=cyclic-import from bastd.actor.text import Text - from ba import _language + from babase import _language super().on_begin() # Pop up a 'press any button to continue' statement after our # min-view-time show a 'press any button to continue..' # thing after a bit. - if _ba.app.ui.uiscale is UIScale.LARGE: + assert _babase.app.classic is not None + if _babase.app.classic.ui.uiscale is UIScale.LARGE: # FIXME: Need a better way to determine whether we've probably # got a keyboard. sval = _language.Lstr(resource='pressAnyKeyButtonText') @@ -202,15 +208,17 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]): ).autoretain() def _player_press(self) -> None: - # If this activity is a good 'end point', ask server-mode just once if # it wants to do anything special like switch sessions or kill the app. if ( self._allow_server_transition - and _ba.app.server is not None + and _babase.app.classic is not None + and _babase.app.classic.server is not None and self._server_transitioning is None ): - self._server_transitioning = _ba.app.server.handle_transition() + self._server_transitioning = ( + _babase.app.classic.server.handle_transition() + ) assert isinstance(self._server_transitioning, bool) # If server-mode is handling this, don't do anything ourself. @@ -221,7 +229,6 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]): self.end() def _safe_assign(self, player: EmptyPlayer) -> None: - # Just to be extra careful, don't assign if we're transitioning out. # (though theoretically that should be ok). if not self.is_transitioning_out() and player: diff --git a/assets/src/ba_data/python/ba/_actor.py b/src/assets/ba_data/python/bascenev1/_actor.py similarity index 62% rename from assets/src/ba_data/python/ba/_actor.py rename to src/assets/ba_data/python/bascenev1/_actor.py index 8df85e01..d75ce5ea 100644 --- a/assets/src/ba_data/python/ba/_actor.py +++ b/src/assets/ba_data/python/bascenev1/_actor.py @@ -7,24 +7,32 @@ from __future__ import annotations import weakref from typing import TYPE_CHECKING, TypeVar, overload -import _ba -from ba._messages import DieMessage, DeathType, OutOfBoundsMessage, UNHANDLED -from ba._error import print_exception, ActivityNotFoundError + +from babase._error import print_exception, ActivityNotFoundError +import _bascenev1 +from bascenev1._messages import ( + DieMessage, + DeathType, + OutOfBoundsMessage, + UNHANDLED, +) if TYPE_CHECKING: from typing import Any, Literal - import ba + + import babase + import bascenev1 ActorT = TypeVar('ActorT', bound='Actor') class Actor: - """High level logical entities in a ba.Activity. + """High level logical entities in a bascenev1.Activity. Category: **Gameplay Classes** - Actors act as controllers, combining some number of ba.Nodes, - ba.Textures, ba.Sounds, etc. into a high-level cohesive unit. + Actors act as controllers, combining some number of Nodes, Textures, + Sounds, etc. into a high-level cohesive unit. Some example actors include the Bomb, Flag, and Spaz classes that live in the bastd.actor.* modules. @@ -44,34 +52,36 @@ class Actor: ... # Either way, the old flag disappears. ... self.flag = None - This is in contrast to the behavior of the more low level ba.Nodes, - which are always explicitly created and destroyed and don't care - how many Python references to them exist. + This is in contrast to the behavior of the more low level + bascenev1.Nodes, which are always explicitly created and destroyed + and don't care how many Python references to them exist. - Note, however, that you can use the ba.Actor.autoretain() method + Note, however, that you can use the bascenev1.Actor.autoretain() method if you want an Actor to stick around until explicitly killed regardless of references. - Another key feature of ba.Actor is its ba.Actor.handlemessage() method, - which takes a single arbitrary object as an argument. This provides a safe - way to communicate between ba.Actor, ba.Activity, ba.Session, and any other + Another key feature of bascenev1.Actor is its + bascenev1.Actor.handlemessage() method, which takes a single arbitrary + object as an argument. This provides a safe way to communicate between + bascenev1.Actor, bascenev1.Activity, bascenev1.Session, and any other class providing a handlemessage() method. The most universally handled - message type for Actors is the ba.DieMessage. + message type for Actors is the bascenev1.DieMessage. Another way to kill the flag from the example above: We can safely call this on any type with a 'handlemessage' method (though its not guaranteed to always have a meaningful effect). In this case the Actor instance will still be around, but its - ba.Actor.exists() and ba.Actor.is_alive() methods will both return False. - >>> self.flag.handlemessage(ba.DieMessage()) + bascenev1.Actor.exists() and bascenev1.Actor.is_alive() methods will + both return False. + >>> self.flag.handlemessage(bascenev1.DieMessage()) """ def __init__(self) -> None: - """Instantiates an Actor in the current ba.Activity.""" + """Instantiates an Actor in the current bascenev1.Activity.""" if __debug__: self._root_actor_init_called = True - activity = _ba.getactivity() + activity = _bascenev1.getactivity() self._activity = weakref.ref(activity) activity.add_actor_weak_ref(self) @@ -83,7 +93,7 @@ class Actor: if not self.expired: self.handlemessage(DieMessage()) except Exception: - print_exception('exception in ba.Actor.__del__() for', self) + print_exception('exception in bascenev1.Actor.__del__() for', self) def handlemessage(self, msg: Any) -> Any: """General message handling; can be passed any message object.""" @@ -98,13 +108,15 @@ class Actor: def autoretain(self: ActorT) -> ActorT: """Keep this Actor alive without needing to hold a reference to it. - This keeps the ba.Actor in existence by storing a reference to it - with the ba.Activity it was created in. The reference is lazily - released once ba.Actor.exists() returns False for it or when the - Activity is set as expired. This can be a convenient alternative - to storing references explicitly just to keep a ba.Actor from dying. - For convenience, this method returns the ba.Actor it is called with, - enabling chained statements such as: myflag = ba.Flag().autoretain() + This keeps the bascenev1.Actor in existence by storing a reference + to it with the bascenev1.Activity it was created in. The reference + is lazily released once bascenev1.Actor.exists() returns False for + it or when the Activity is set as expired. This can be a convenient + alternative to storing references explicitly just to keep a + bascenev1.Actor from dying. + For convenience, this method returns the bascenev1.Actor it is called + with, enabling chained statements such as: + myflag = bascenev1.Flag().autoretain() """ activity = self._activity() if activity is None: @@ -113,15 +125,15 @@ class Actor: return self def on_expire(self) -> None: - """Called for remaining `ba.Actor`s when their ba.Activity shuts down. + """Called for remaining `bascenev1.Actor`s when their activity dies. Actors can use this opportunity to clear callbacks or other - references which have the potential of keeping the ba.Activity + references which have the potential of keeping the bascenev1.Activity alive inadvertently (Activities can not exit cleanly while any Python references to them remain.) - Once an actor is expired (see ba.Actor.is_expired()) it should no - longer perform any game-affecting operations (creating, modifying, + Once an actor is expired (see bascenev1.Actor.is_expired()) it should + no longer perform any game-affecting operations (creating, modifying, or deleting nodes, media, timers, etc.) Attempts to do so will likely result in errors. """ @@ -130,7 +142,7 @@ class Actor: def expired(self) -> bool: """Whether the Actor is expired. - (see ba.Actor.on_expire()) + (see bascenev1.Actor.on_expire()) """ activity = self.getactivity(doraise=False) return True if activity is None else activity.expired @@ -140,11 +152,11 @@ class Actor: Note that a dying character should still return True here as long as their corpse is visible; this is about presence, not being 'alive' - (see ba.Actor.is_alive() for that). + (see bascenev1.Actor.is_alive() for that). If this returns False, it is assumed the Actor can be completely deleted without affecting the game; this call is often used - when pruning lists of Actors, such as with ba.Actor.autoretain() + when pruning lists of Actors, such as with bascenev1.Actor.autoretain() The default implementation of this method always return True. @@ -170,10 +182,11 @@ class Actor: return True @property - def activity(self) -> ba.Activity: + def activity(self) -> bascenev1.Activity: """The Activity this Actor was created in. - Raises a ba.ActivityNotFoundError if the Activity no longer exists. + Raises a bascenev1.ActivityNotFoundError if the Activity no longer + exists. """ activity = self._activity() if activity is None: @@ -183,18 +196,19 @@ class Actor: # Overloads to convey our exact return type depending on 'doraise' value. @overload - def getactivity(self, doraise: Literal[True] = True) -> ba.Activity: + def getactivity(self, doraise: Literal[True] = True) -> bascenev1.Activity: ... @overload - def getactivity(self, doraise: Literal[False]) -> ba.Activity | None: + def getactivity(self, doraise: Literal[False]) -> bascenev1.Activity | None: ... - def getactivity(self, doraise: bool = True) -> ba.Activity | None: - """Return the ba.Activity this Actor is associated with. + def getactivity(self, doraise: bool = True) -> bascenev1.Activity | None: + """Return the bascenev1.Activity this Actor is associated with. - If the Activity no longer exists, raises a ba.ActivityNotFoundError - or returns None depending on whether 'doraise' is True. + If the Activity no longer exists, raises a + bascenev1.ActivityNotFoundError 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/_collision.py b/src/assets/ba_data/python/bascenev1/_collision.py similarity index 58% rename from assets/src/ba_data/python/ba/_collision.py rename to src/assets/ba_data/python/bascenev1/_collision.py index d44c0f4d..0ecb2b36 100644 --- a/assets/src/ba_data/python/ba/_collision.py +++ b/src/assets/ba_data/python/bascenev1/_collision.py @@ -6,11 +6,13 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba -from ba._error import NodeNotFoundError +import _babase +import _bascenev1 +from babase._error import NodeNotFoundError if TYPE_CHECKING: - import ba + import babase + import bascenev1 class Collision: @@ -20,34 +22,34 @@ class Collision: """ @property - def position(self) -> ba.Vec3: + def position(self) -> bascenev1.Vec3: """The position of the current collision.""" - return _ba.Vec3(_ba.get_collision_info('position')) + return _babase.Vec3(_bascenev1.get_collision_info('position')) @property - def sourcenode(self) -> ba.Node: + def sourcenode(self) -> bascenev1.Node: """The node containing the material triggering the current callback. - Throws a ba.NodeNotFoundError if the node does not exist, though - the node should always exist (at least at the start of the collision - callback). + Throws a bascenev1.NodeNotFoundError if the node does not exist, + though the node should always exist (at least at the start of the + collision callback). """ - node = _ba.get_collision_info('sourcenode') - assert isinstance(node, (_ba.Node, type(None))) + node = _bascenev1.get_collision_info('sourcenode') + assert isinstance(node, (_bascenev1.Node, type(None))) if not node: raise NodeNotFoundError() return node @property - def opposingnode(self) -> ba.Node: + def opposingnode(self) -> bascenev1.Node: """The node the current callback material node is hitting. - Throws a ba.NodeNotFoundError if the node does not exist. + Throws a bascenev1.NodeNotFoundError if the node does not exist. This can be expected in some cases such as in 'disconnect' callbacks triggered by deleting a currently-colliding node. """ - node = _ba.get_collision_info('opposingnode') - assert isinstance(node, (_ba.Node, type(None))) + node = _bascenev1.get_collision_info('opposingnode') + assert isinstance(node, (_bascenev1.Node, type(None))) if not node: raise NodeNotFoundError() return node @@ -55,7 +57,7 @@ class Collision: @property def opposingbody(self) -> int: """The body index on the opposing node in the current collision.""" - body = _ba.get_collision_info('opposingbody') + body = _bascenev1.get_collision_info('opposingbody') assert isinstance(body, int) return body diff --git a/assets/src/ba_data/python/ba/_coopgame.py b/src/assets/ba_data/python/bascenev1/_coopgame.py similarity index 76% rename from assets/src/ba_data/python/ba/_coopgame.py rename to src/assets/ba_data/python/bascenev1/_coopgame.py index 6fedc3ca..bf45e9ae 100644 --- a/assets/src/ba_data/python/ba/_coopgame.py +++ b/src/assets/ba_data/python/bascenev1/_coopgame.py @@ -3,36 +3,39 @@ """Functionality related to co-op games.""" from __future__ import annotations +import logging from typing import TYPE_CHECKING, TypeVar -import _ba -from ba import _internal -from ba._gameactivity import GameActivity -from ba._general import WeakCall +import _babase +import _bascenev1 +from babase._general import WeakCall +from bascenev1._gameactivity import GameActivity if TYPE_CHECKING: from typing import Sequence from bastd.actor.playerspaz import PlayerSpaz - import ba + import babase + import baclassic + import bascenev1 -# pylint: disable=invalid-name -PlayerType = TypeVar('PlayerType', bound='ba.Player') -TeamType = TypeVar('TeamType', bound='ba.Team') -# pylint: enable=invalid-name +PlayerT = TypeVar('PlayerT', bound='bascenev1.Player') +TeamT = TypeVar('TeamT', bound='bascenev1.Team') -class CoopGameActivity(GameActivity[PlayerType, TeamType]): +class CoopGameActivity(GameActivity[PlayerT, TeamT]): """Base class for cooperative-mode games. Category: **Gameplay Classes** """ # We can assume our session is a CoopSession. - session: ba.CoopSession + session: bascenev1.CoopSession @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: - from ba._coopsession import CoopSession + def supports_session_type( + cls, sessiontype: type[bascenev1.Session] + ) -> bool: + from bascenev1._coopsession import CoopSession return issubclass(sessiontype, CoopSession) @@ -42,19 +45,19 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): # Cache these for efficiency. self._achievements_awarded: set[str] = set() - self._life_warning_beep: ba.Actor | None = None - self._life_warning_beep_timer: ba.Timer | None = None - self._warn_beeps_sound = _ba.getsound('warnBeeps') + self._life_warning_beep: bascenev1.Actor | None = None + self._life_warning_beep_timer: bascenev1.Timer | None = None + self._warn_beeps_sound = _bascenev1.getsound('warnBeeps') def on_begin(self) -> None: super().on_begin() # Show achievements remaining. - if not (_ba.app.demo_mode or _ba.app.arcade_mode): - _ba.timer(3.8, WeakCall(self._show_remaining_achievements)) + if not (_babase.app.demo_mode or _babase.app.arcade_mode): + _bascenev1.timer(3.8, WeakCall(self._show_remaining_achievements)) # Preload achievement images in case we get some. - _ba.timer(2.0, WeakCall(self._preload_achievements)) + _bascenev1.timer(2.0, WeakCall(self._preload_achievements)) # FIXME: this is now redundant with activityutils.getscoreconfig(); # need to kill this. @@ -75,14 +78,15 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): a wave. duration is given in seconds. """ - from ba._messages import CelebrateMessage + from bascenev1._messages import CelebrateMessage for player in self.players: if player.actor: player.actor.handlemessage(CelebrateMessage(duration)) def _preload_achievements(self) -> None: - achievements = _ba.app.ach.achievements_for_coop_level( + assert _babase.app.classic is not None + achievements = _babase.app.classic.ach.achievements_for_coop_level( self._get_coop_level_name() ) for ach in achievements: @@ -90,19 +94,20 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): def _show_remaining_achievements(self) -> None: # pylint: disable=cyclic-import - from ba._language import Lstr + from babase._language import Lstr from bastd.actor.text import Text + assert _babase.app.classic is not None ts_h_offs = 30 v_offs = -200 achievements = [ a - for a in _ba.app.ach.achievements_for_coop_level( + for a in _babase.app.classic.ach.achievements_for_coop_level( self._get_coop_level_name() ) if not a.complete ] - vrmode = _ba.app.vr_mode + vrmode = _babase.app.vr_mode if achievements: Text( Lstr(resource='achievementsRemainingText'), @@ -134,7 +139,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): def spawn_player_spaz( self, - player: PlayerType, + player: PlayerT, position: Sequence[float] = (0.0, 0.0, 0.0), angle: float | None = None, ) -> PlayerSpaz: @@ -154,10 +159,18 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): False otherwise """ + classic = _babase.app.classic + plus = _babase.app.plus + if classic is None or plus is None: + logging.warning( + '_award_achievement is a no-op without classic and plus.' + ) + return + if achievement_name in self._achievements_awarded: return - ach = _ba.app.ach.get_achievement(achievement_name) + ach = classic.ach.get_achievement(achievement_name) # If we're in the easy campaign and this achievement is hard-mode-only, # ignore it. @@ -167,7 +180,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): if ach.hard_mode_only and campaign.name == 'Easy': return except Exception: - from ba._error import print_exception + from babase._error import print_exception print_exception() @@ -178,10 +191,10 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): self._achievements_awarded.add(achievement_name) # Report new achievements to the game-service. - _internal.report_achievement(achievement_name) + plus.report_achievement(achievement_name) # ...and to our account. - _internal.add_transaction( + plus.add_v1_account_transaction( {'type': 'ACHIEVEMENT', 'name': achievement_name} ) @@ -190,10 +203,10 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): def fade_to_red(self) -> None: """Fade the screen to red; (such as when the good guys have lost).""" - from ba import _gameutils + from bascenev1 import _gameutils c_existing = self.globalsnode.tint - cnode = _ba.newnode( + cnode = _bascenev1.newnode( 'combine', attrs={ 'input0': c_existing[0], @@ -209,7 +222,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): def setup_low_life_warning_sound(self) -> None: """Set up a beeping noise to play when any players are near death.""" self._life_warning_beep = None - self._life_warning_beep_timer = _ba.Timer( + self._life_warning_beep_timer = _bascenev1.Timer( 1.0, WeakCall(self._update_life_warning), repeat=True ) @@ -224,10 +237,10 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): should_beep = True break if should_beep and self._life_warning_beep is None: - from ba._nodeactor import NodeActor + from bascenev1._nodeactor import NodeActor self._life_warning_beep = NodeActor( - _ba.newnode( + _bascenev1.newnode( 'sound', attrs={ 'sound': self._warn_beeps_sound, diff --git a/assets/src/ba_data/python/ba/_coopsession.py b/src/assets/ba_data/python/bascenev1/_coopsession.py similarity index 82% rename from assets/src/ba_data/python/ba/_coopsession.py rename to src/assets/ba_data/python/bascenev1/_coopsession.py index 36f56327..265fe4f2 100644 --- a/assets/src/ba_data/python/ba/_coopsession.py +++ b/src/assets/ba_data/python/bascenev1/_coopsession.py @@ -5,19 +5,22 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba -from ba._session import Session +import _babase +import _bascenev1 +from bascenev1._session import Session if TYPE_CHECKING: from typing import Any, Callable, Sequence - import ba + import babase + import baclassic + import bascenev1 TEAM_COLORS = [(0.2, 0.4, 1.6)] TEAM_NAMES = ['Good Guys'] class CoopSession(Session): - """A ba.Session which runs cooperative-mode games. + """A bascenev1.Session which runs cooperative-mode games. Category: **Gameplay Classes** @@ -33,32 +36,32 @@ class CoopSession(Session): # Note: even though these are instance vars, we annotate them at the # class level so that docs generation can access their types. - campaign: ba.Campaign | None - """The ba.Campaign instance this Session represents, or None if + campaign: baclassic.Campaign | None + """The baclassic.Campaign instance this Session represents, or None if there is no associated Campaign.""" def __init__(self) -> None: """Instantiate a co-op mode session.""" # pylint: disable=cyclic-import - from ba._campaign import getcampaign from bastd.activity.coopjoin import CoopJoinActivity - _ba.increment_analytics_count('Co-op session start') - app = _ba.app + _babase.increment_analytics_count('Co-op session start') + app = _babase.app + assert app.classic is not None # If they passed in explicit min/max, honor that. # Otherwise defer to user overrides or defaults. - if 'min_players' in app.coop_session_args: - min_players = app.coop_session_args['min_players'] + if 'min_players' in app.classic.coop_session_args: + min_players = app.classic.coop_session_args['min_players'] else: min_players = 1 - if 'max_players' in app.coop_session_args: - max_players = app.coop_session_args['max_players'] + if 'max_players' in app.classic.coop_session_args: + max_players = app.classic.coop_session_args['max_players'] else: max_players = app.config.get('Coop Game Max Players', 4) # print('FIXME: COOP SESSION WOULD CALC DEPS.') - depsets: Sequence[ba.DependencySet] = [] + depsets: Sequence[bascenev1.DependencySet] = [] super().__init__( depsets, @@ -69,31 +72,35 @@ class CoopSession(Session): ) # Tournament-ID if we correspond to a co-op tournament (otherwise None) - self.tournament_id: str | None = app.coop_session_args.get( + self.tournament_id: str | None = app.classic.coop_session_args.get( 'tournament_id' ) - self.campaign = getcampaign(app.coop_session_args['campaign']) - self.campaign_level_name: str = app.coop_session_args['level'] + self.campaign = app.classic.getcampaign( + app.classic.coop_session_args['campaign'] + ) + self.campaign_level_name: str = app.classic.coop_session_args['level'] self._ran_tutorial_activity = False - self._tutorial_activity: ba.Activity | None = None + self._tutorial_activity: bascenev1.Activity | None = None self._custom_menu_ui: list[dict[str, Any]] = [] # Start our joining screen. - self.setactivity(_ba.newactivity(CoopJoinActivity)) + self.setactivity(_bascenev1.newactivity(CoopJoinActivity)) - self._next_game_instance: ba.GameActivity | None = None + self._next_game_instance: bascenev1.GameActivity | None = None self._next_game_level_name: str | None = None self._update_on_deck_game_instances() - def get_current_game_instance(self) -> ba.GameActivity: + def get_current_game_instance(self) -> bascenev1.GameActivity: """Get the game instance currently being played.""" return self._current_game_instance - def should_allow_mid_activity_joins(self, activity: ba.Activity) -> bool: + def should_allow_mid_activity_joins( + self, activity: bascenev1.Activity + ) -> bool: # pylint: disable=cyclic-import - from ba._gameactivity import GameActivity + from bascenev1._gameactivity import GameActivity # Disallow any joins in the middle of the game. if isinstance(activity, GameActivity): @@ -103,7 +110,7 @@ class CoopSession(Session): def _update_on_deck_game_instances(self) -> None: # pylint: disable=cyclic-import - from ba._gameactivity import GameActivity + from bascenev1._gameactivity import GameActivity # Instantiate levels we may be running soon to let them load in the bg. @@ -119,7 +126,7 @@ class CoopSession(Session): if setting.name not in settings: settings[setting.name] = setting.default - newactivity = _ba.newactivity(gametype, settings) + newactivity = _bascenev1.newactivity(gametype, settings) assert isinstance(newactivity, GameActivity) self._current_game_instance: GameActivity = newactivity @@ -127,7 +134,7 @@ class CoopSession(Session): levels = self.campaign.levels level = self.campaign.getlevel(self.campaign_level_name) - nextlevel: ba.Level | None + nextlevel: baclassic.Level | None if level.index < len(levels) - 1: nextlevel = levels[level.index + 1] else: @@ -143,7 +150,7 @@ class CoopSession(Session): settings[setting.name] = setting.default # We wanna be in the activity's context while taking it down. - newactivity = _ba.newactivity(gametype, settings) + newactivity = _bascenev1.newactivity(gametype, settings) assert isinstance(newactivity, GameActivity) self._next_game_instance = newactivity self._next_game_level_name = nextlevel.name @@ -161,22 +168,22 @@ class CoopSession(Session): ): from bastd.tutorial import TutorialActivity - self._tutorial_activity = _ba.newactivity(TutorialActivity) + self._tutorial_activity = _bascenev1.newactivity(TutorialActivity) def get_custom_menu_entries(self) -> list[dict[str, Any]]: return self._custom_menu_ui - def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None: - from ba._general import WeakCall + def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None: + from babase._general import WeakCall super().on_player_leave(sessionplayer) - _ba.timer(2.0, WeakCall(self._handle_empty_activity)) + _bascenev1.timer(2.0, WeakCall(self._handle_empty_activity)) def _handle_empty_activity(self) -> None: """Handle cases where all players have left the current activity.""" - from ba._gameactivity import GameActivity + from bascenev1._gameactivity import GameActivity activity = self.getactivity() if activity is None: @@ -189,11 +196,9 @@ class CoopSession(Session): # If there are *not* players in the current activity but there # *are* in the session: if not activity.players and self.sessionplayers: - # If we're in a game, we should restart to pull in players # currently waiting in the session. if isinstance(activity, GameActivity): - # Never restart tourney games however; just end the session # if all players are gone. if self.tournament_id is not None: @@ -204,11 +209,11 @@ class CoopSession(Session): # Hmm; no players anywhere. Let's end the entire session if we're # running a GUI (or just the current game if we're running headless). else: - if not _ba.app.headless_mode: + if not _babase.app.headless_mode: self.end() else: if isinstance(activity, GameActivity): - with _ba.Context(activity): + with activity.context: activity.end_game() def _on_tournament_restart_menu_press( @@ -216,7 +221,7 @@ class CoopSession(Session): ) -> None: # pylint: disable=cyclic-import from bastd.ui.tournamententry import TournamentEntryWindow - from ba._gameactivity import GameActivity + from bascenev1._gameactivity import GameActivity activity = self.getactivity() if activity is not None and not activity.expired: @@ -245,11 +250,13 @@ class CoopSession(Session): activity = self.getactivity() if activity is not None and not activity.expired: activity.can_show_ad_on_death = True - with _ba.Context(activity): + with activity.context: activity.end(results={'outcome': 'restart'}, force=True) # noinspection PyUnresolvedReferences - def on_activity_end(self, activity: ba.Activity, results: Any) -> None: + def on_activity_end( + self, activity: bascenev1.Activity, results: Any + ) -> None: """Method override for co-op sessions. Jumps between co-op games and score screens. @@ -258,17 +265,17 @@ class CoopSession(Session): # pylint: disable=too-many-locals # pylint: disable=too-many-statements # pylint: disable=cyclic-import - from ba._activitytypes import JoinActivity, TransitionActivity - from ba._language import Lstr - from ba._general import WeakCall - from ba._coopgame import CoopGameActivity - from ba._gameresults import GameResults - from ba._score import ScoreType - from ba._player import PlayerInfo + from babase._language import Lstr + from babase._general import WeakCall from bastd.tutorial import TutorialActivity from bastd.activity.coopscore import CoopScoreScreen + from bascenev1._gameresults import GameResults + from bascenev1._player import PlayerInfo + from bascenev1._activitytypes import JoinActivity, TransitionActivity + from bascenev1._coopgame import CoopGameActivity + from bascenev1._score import ScoreType - app = _ba.app + app = _babase.app # If we're running a TeamGameActivity we'll have a GameResults # as results. Otherwise its an old CoopGameActivity so its giving @@ -281,7 +288,7 @@ class CoopSession(Session): # If we're running with a gui and at any point we have no # in-game players, quit out of the session (this can happen if # someone leaves in the tutorial for instance). - if not _ba.app.headless_mode: + if not _babase.app.headless_mode: active_players = [p for p in self.sessionplayers if p.in_game] if not active_players: self.end() @@ -292,7 +299,6 @@ class CoopSession(Session): if isinstance( activity, (JoinActivity, CoopScoreScreen, TransitionActivity) ): - if outcome == 'next_level': if self._next_game_instance is None: raise RuntimeError() @@ -319,11 +325,9 @@ class CoopSession(Session): # Normal case; launch the next round. else: - # Reset stats for the new activity. self.stats.reset() for player in self.sessionplayers: - # Skip players that are still choosing a team. if player.in_game: self.stats.register_sessionplayer(player) @@ -354,10 +358,9 @@ class CoopSession(Session): # If we were in a tutorial, just pop a transition to get to the # actual round. elif isinstance(activity, TutorialActivity): - self.setactivity(_ba.newactivity(TransitionActivity)) + self.setactivity(_bascenev1.newactivity(TransitionActivity)) else: - - playerinfos: list[ba.PlayerInfo] + playerinfos: list[bascenev1.PlayerInfo] # Generic team games. if isinstance(results, GameResults): @@ -411,17 +414,16 @@ class CoopSession(Session): # Validate types. if playerinfos is not None: assert isinstance(playerinfos, list) - assert (isinstance(i, PlayerInfo) for i in playerinfos) + assert all(isinstance(i, PlayerInfo) for i in playerinfos) # Looks like we were in a round - check the outcome and # go from there. if outcome == 'restart': - # This will pop up back in the same round. - self.setactivity(_ba.newactivity(TransitionActivity)) + self.setactivity(_bascenev1.newactivity(TransitionActivity)) else: self.setactivity( - _ba.newactivity( + _bascenev1.newactivity( CoopScoreScreen, { 'playerinfos': playerinfos, diff --git a/src/assets/ba_data/python/bascenev1/_debug.py b/src/assets/ba_data/python/bascenev1/_debug.py new file mode 100644 index 00000000..a3a880d5 --- /dev/null +++ b/src/assets/ba_data/python/bascenev1/_debug.py @@ -0,0 +1,69 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Debugging functionality.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _babase + +if TYPE_CHECKING: + from typing import Any + + import babase + import bascenev1 + + +def print_live_object_warnings( + when: Any, + ignore_session: bascenev1.Session | None = None, + ignore_activity: bascenev1.Activity | None = None, +) -> None: + """Print warnings for remaining objects in the current context. + + IMPORTANT - don't call this in production; usage of gc.get_objects() + can bork Python. See notes at top of efro.debug module. + """ + # pylint: disable=cyclic-import + import gc + + from bascenev1._session import Session + from bascenev1._actor import Actor + from bascenev1._activity import Activity + + assert _babase.app.classic is not None + + sessions: list[bascenev1.Session] = [] + activities: list[bascenev1.Activity] = [] + actors: list[bascenev1.Actor] = [] + + # Once we come across leaked stuff, printing again is probably + # redundant. + if _babase.app.classic.printed_live_object_warning: + return + for obj in gc.get_objects(): + if isinstance(obj, Actor): + actors.append(obj) + elif isinstance(obj, Session): + sessions.append(obj) + elif isinstance(obj, Activity): + activities.append(obj) + + # Complain about any remaining sessions. + for session in sessions: + if session is ignore_session: + continue + _babase.app.classic.printed_live_object_warning = True + print(f'ERROR: Session found {when}: {session}') + + # Complain about any remaining activities. + for activity in activities: + if activity is ignore_activity: + continue + _babase.app.classic.printed_live_object_warning = True + print(f'ERROR: Activity found {when}: {activity}') + + # Complain about any remaining actors. + for actor in actors: + _babase.app.classic.printed_live_object_warning = True + print(f'ERROR: Actor found {when}: {actor}') diff --git a/assets/src/ba_data/python/ba/_dependency.py b/src/assets/ba_data/python/bascenev1/_dependency.py similarity index 83% rename from assets/src/ba_data/python/ba/_dependency.py rename to src/assets/ba_data/python/bascenev1/_dependency.py index a8cb4624..ecbc1610 100644 --- a/assets/src/ba_data/python/ba/_dependency.py +++ b/src/assets/ba_data/python/bascenev1/_dependency.py @@ -7,11 +7,13 @@ from __future__ import annotations import weakref from typing import Generic, TypeVar, TYPE_CHECKING -import _ba +import _babase +import _bascenev1 if TYPE_CHECKING: from typing import Any - import ba + + import bascenev1 T = TypeVar('T', bound='DependencyComponent') @@ -26,13 +28,13 @@ class Dependency(Generic[T]): The class functions as a descriptor, allowing dependencies to be added at a class level much the same as properties or methods and then used with class instances to access those dependencies. - For instance, if you do 'floofcls = ba.Dependency(FloofClass)' you - would then be able to instantiate a FloofClass in your class's + For instance, if you do 'floofcls = bascenev1.Dependency(FloofClass)' + you would then be able to instantiate a FloofClass in your class's methods via self.floofcls(). """ def __init__(self, cls: type[T], config: Any = None): - """Instantiate a Dependency given a ba.DependencyComponent type. + """Instantiate a Dependency given a bascenev1.DependencyComponent type. Optionally, an arbitrary object can be passed as 'config' to influence dependency calculation for the target class. @@ -57,7 +59,7 @@ class Dependency(Generic[T]): ) raise TypeError( f'Dependency cannot be added to class of type {type(obj)}' - ' (class must inherit from ba.DependencyComponent).' + ' (class must inherit from bascenev1.DependencyComponent).' ) # We expect to be instantiated from an already living @@ -125,7 +127,7 @@ class DependencyComponent: class DependencyEntry: - """Data associated with a dependency/config pair in a ba.DependencySet.""" + """Data associated with a dep/config pair in bascenev1.DependencySet.""" # def __del__(self) -> None: # print('~DepEntry()', self.cls) @@ -154,7 +156,6 @@ class DependencyEntry: instance._dep_entry = weakref.ref(self) instance.__init__() # type: ignore - assert self.depset depset = self.depset() assert depset is not None self.component = instance @@ -193,12 +194,12 @@ class DependencySet(Generic[T]): def resolve(self) -> None: """Resolve the complete set of required dependencies for this set. - Raises a ba.DependencyError if dependencies are missing (or other - Exception types on other errors). + Raises a bascenev1.DependencyError if dependencies are missing (or + other Exception types on other errors). """ if self._resolved: - raise Exception('DependencySet has already been resolved.') + raise RuntimeError('DependencySet has already been resolved.') # print('RESOLVING DEP SET') @@ -214,8 +215,6 @@ class DependencySet(Generic[T]): if not entry.cls.dep_is_present(entry.config) ] if missing: - from ba._error import DependencyError - raise DependencyError(missing) self._resolved = True @@ -233,7 +232,7 @@ class DependencySet(Generic[T]): """ ids: set[str] = set() if not self._resolved: - raise Exception('Must be called on a resolved dep-set.') + raise RuntimeError('Must be called on a resolved dep-set.') for entry in self.entries.values(): if issubclass(entry.cls, AssetPackage): assert isinstance(entry.config, str) @@ -268,7 +267,6 @@ class DependencySet(Generic[T]): return rootdata def _resolve(self, dep: Dependency[T], recursion: int) -> None: - # Watch for wacky infinite dep loops. if recursion > 10: raise RecursionError('Max recursion reached') @@ -297,7 +295,7 @@ class DependencySet(Generic[T]): class AssetPackage(DependencyComponent): - """ba.DependencyComponent representing a bundled package of game assets. + """bascenev1.DependencyComponent representing a bundled package of assets. Category: **Asset Classes** """ @@ -306,7 +304,7 @@ class AssetPackage(DependencyComponent): super().__init__() # This is used internally by the get_package_xxx calls. - self.context = _ba.Context('current') + self.context = _babase.ContextRef() entry = self._dep_entry() assert entry is not None @@ -323,40 +321,40 @@ class AssetPackage(DependencyComponent): return True return False - def gettexture(self, name: str) -> ba.Texture: - """Load a named ba.Texture from the AssetPackage. + def gettexture(self, name: str) -> bascenev1.Texture: + """Load a named bascenev1.Texture from the AssetPackage. - Behavior is similar to ba.gettexture() + Behavior is similar to bascenev1.gettexture() """ - return _ba.get_package_texture(self, name) + return _bascenev1.get_package_texture(self, name) - def getmodel(self, name: str) -> ba.Model: - """Load a named ba.Model from the AssetPackage. + def getmesh(self, name: str) -> bascenev1.Mesh: + """Load a named bascenev1.Mesh from the AssetPackage. - Behavior is similar to ba.getmodel() + Behavior is similar to bascenev1.getmesh() """ - return _ba.get_package_model(self, name) + return _bascenev1.get_package_mesh(self, name) - def getcollidemodel(self, name: str) -> ba.CollideModel: - """Load a named ba.CollideModel from the AssetPackage. + def getcollisionmesh(self, name: str) -> bascenev1.CollisionMesh: + """Load a named bascenev1.CollisionMesh from the AssetPackage. - Behavior is similar to ba.getcollideModel() + Behavior is similar to bascenev1.getcollisionmesh() """ - return _ba.get_package_collide_model(self, name) + return _bascenev1.get_package_collision_mesh(self, name) - def getsound(self, name: str) -> ba.Sound: - """Load a named ba.Sound from the AssetPackage. + def getsound(self, name: str) -> bascenev1.Sound: + """Load a named bascenev1.Sound from the AssetPackage. - Behavior is similar to ba.getsound() + Behavior is similar to bascenev1.getsound() """ - return _ba.get_package_sound(self, name) + return _bascenev1.get_package_sound(self, name) - def getdata(self, name: str) -> ba.Data: - """Load a named ba.Data from the AssetPackage. + def getdata(self, name: str) -> bascenev1.Data: + """Load a named bascenev1.Data from the AssetPackage. - Behavior is similar to ba.getdata() + Behavior is similar to bascenev1.getdata() """ - return _ba.get_package_data(self, name) + return _bascenev1.get_package_data(self, name) class TestClassFactory(DependencyComponent): @@ -368,7 +366,7 @@ class TestClassFactory(DependencyComponent): super().__init__() print('Instantiating TestClassFactory') self.tex = self._assets.gettexture('black') - self.model = self._assets.getmodel('landMine') + self.mesh = self._assets.getmesh('landMine') self.sound = self._assets.getsound('error') self.data = self._assets.getdata('langdata') @@ -402,8 +400,6 @@ def test_depset() -> None: print('running test_depset()...') def doit() -> None: - from ba._error import DependencyError - depset = DependencySet(Dependency(TestClass)) try: depset.resolve() @@ -428,4 +424,22 @@ def test_depset() -> None: # To test this, add prints on __del__ for stuff used above; # everything should be dead at this point if we have no cycles. print('everything should be cleaned up...') - _ba.quit() + _babase.quit() + + +class DependencyError(Exception): + """Exception raised when one or more bascenev1.Dependency items are missing. + + Category: **Exception Classes** + + (this will generally be missing assets). + """ + + def __init__(self, deps: list[bascenev1.Dependency]): + super().__init__() + self._deps = deps + + @property + def deps(self) -> list[bascenev1.Dependency]: + """The list of missing dependencies causing this error.""" + return self._deps diff --git a/assets/src/ba_data/python/ba/_dualteamsession.py b/src/assets/ba_data/python/bascenev1/_dualteamsession.py similarity index 76% rename from assets/src/ba_data/python/ba/_dualteamsession.py rename to src/assets/ba_data/python/bascenev1/_dualteamsession.py index 6870fede..6dea02f5 100644 --- a/assets/src/ba_data/python/ba/_dualteamsession.py +++ b/src/assets/ba_data/python/bascenev1/_dualteamsession.py @@ -5,15 +5,17 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba -from ba._multiteamsession import MultiTeamSession +import _babase +import _bascenev1 +from bascenev1._multiteamsession import MultiTeamSession if TYPE_CHECKING: - import ba + import babase + import bascenev1 class DualTeamSession(MultiTeamSession): - """ba.Session type for teams mode games. + """bascenev1.Session type for teams mode games. Category: **Gameplay Classes** """ @@ -27,10 +29,10 @@ class DualTeamSession(MultiTeamSession): _playlists_var = 'Team Tournament Playlists' def __init__(self) -> None: - _ba.increment_analytics_count('Teams session start') + _babase.increment_analytics_count('Teams session start') super().__init__() - def _switch_to_score_screen(self, results: ba.GameResults) -> None: + def _switch_to_score_screen(self, results: bascenev1.GameResults) -> None: # pylint: disable=cyclic-import from bastd.activity.drawscore import DrawScoreScreenActivity from bastd.activity.dualteamscore import TeamVictoryScoreScreenActivity @@ -42,7 +44,7 @@ class DualTeamSession(MultiTeamSession): # If everyone has the same score, call it a draw. if len(winnergroups) < 2: - self.setactivity(_ba.newactivity(DrawScoreScreenActivity)) + self.setactivity(_bascenev1.newactivity(DrawScoreScreenActivity)) else: winner = winnergroups[0].teams[0] winner.customdata['score'] += 1 @@ -50,13 +52,13 @@ class DualTeamSession(MultiTeamSession): # If a team has won, show final victory screen. if winner.customdata['score'] >= (self._series_length - 1) / 2 + 1: self.setactivity( - _ba.newactivity( + _bascenev1.newactivity( TeamSeriesVictoryScoreScreenActivity, {'winner': winner} ) ) else: self.setactivity( - _ba.newactivity( + _bascenev1.newactivity( TeamVictoryScoreScreenActivity, {'winner': winner} ) ) diff --git a/src/assets/ba_data/python/bascenev1/_featureset.py b/src/assets/ba_data/python/bascenev1/_featureset.py new file mode 100644 index 00000000..b1d095f4 --- /dev/null +++ b/src/assets/ba_data/python/bascenev1/_featureset.py @@ -0,0 +1,8 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Gather our feature-set functionality from the _babase binary module.""" + +# This module only exists to imports/rename the parts of our feature-set +# contained in _babase. This allows our internal modules to access this +# functionality through us instead of requiring long-winded +# feature-set-specific names in _babase. diff --git a/assets/src/ba_data/python/ba/_freeforallsession.py b/src/assets/ba_data/python/bascenev1/_freeforallsession.py similarity index 86% rename from assets/src/ba_data/python/ba/_freeforallsession.py rename to src/assets/ba_data/python/bascenev1/_freeforallsession.py index 4be431dc..db37b4a0 100644 --- a/assets/src/ba_data/python/ba/_freeforallsession.py +++ b/src/assets/ba_data/python/bascenev1/_freeforallsession.py @@ -6,15 +6,17 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba -from ba._multiteamsession import MultiTeamSession +import _babase +import _bascenev1 +from bascenev1._multiteamsession import MultiTeamSession if TYPE_CHECKING: - import ba + import babase + import bascenev1 class FreeForAllSession(MultiTeamSession): - """ba.Session type for free-for-all mode games. + """bascenev1.Session type for free-for-all mode games. Category: **Gameplay Classes** """ @@ -48,10 +50,10 @@ class FreeForAllSession(MultiTeamSession): return point_awards def __init__(self) -> None: - _ba.increment_analytics_count('Free-for-all session start') + _babase.increment_analytics_count('Free-for-all session start') super().__init__() - def _switch_to_score_screen(self, results: ba.GameResults) -> None: + def _switch_to_score_screen(self, results: bascenev1.GameResults) -> None: # pylint: disable=cyclic-import from efro.util import asserttype from bastd.activity.drawscore import DrawScoreScreenActivity @@ -68,7 +70,9 @@ class FreeForAllSession(MultiTeamSession): # call it a draw. if len(self.sessionplayers) > 1 and len(winners) < 2: self.setactivity( - _ba.newactivity(DrawScoreScreenActivity, {'results': results}) + _bascenev1.newactivity( + DrawScoreScreenActivity, {'results': results} + ) ) else: # Award different point amounts based on number of players. @@ -95,14 +99,14 @@ class FreeForAllSession(MultiTeamSession): != series_winners[1].customdata['score'] ): self.setactivity( - _ba.newactivity( + _bascenev1.newactivity( TeamSeriesVictoryScoreScreenActivity, {'winner': series_winners[0]}, ) ) else: self.setactivity( - _ba.newactivity( + _bascenev1.newactivity( FreeForAllVictoryScoreScreenActivity, {'results': results}, ) diff --git a/assets/src/ba_data/python/ba/_gameactivity.py b/src/assets/ba_data/python/bascenev1/_gameactivity.py similarity index 81% rename from assets/src/ba_data/python/ba/_gameactivity.py rename to src/assets/ba_data/python/bascenev1/_gameactivity.py index 8dab307d..7512e9cc 100644 --- a/assets/src/ba_data/python/ba/_gameactivity.py +++ b/src/assets/ba_data/python/bascenev1/_gameactivity.py @@ -8,32 +8,33 @@ from __future__ import annotations import random from typing import TYPE_CHECKING, TypeVar -import _ba -from ba import _internal -from ba._activity import Activity -from ba._score import ScoreConfig -from ba._language import Lstr -from ba._messages import PlayerDiedMessage, StandMessage -from ba._error import MapNotFoundError, print_error, print_exception -from ba._general import Call, WeakCall -from ba._player import PlayerInfo -from ba import _map -from ba import _store +import _babase +from babase._language import Lstr +from babase._error import MapNotFoundError, print_error, print_exception +from babase._general import WeakCall +import _bascenev1 +from bascenev1._activity import Activity +from bascenev1._player import PlayerInfo +from bascenev1._messages import PlayerDiedMessage, StandMessage +from bascenev1._score import ScoreConfig +from bascenev1 import _map +from bascenev1 import _music if TYPE_CHECKING: from typing import Any, Callable, Sequence + from bastd.actor.playerspaz import PlayerSpaz from bastd.actor.bomb import TNTSpawner - import ba + import babase + import bascenev1 + import baclassic -# pylint: disable=invalid-name -PlayerType = TypeVar('PlayerType', bound='ba.Player') -TeamType = TypeVar('TeamType', bound='ba.Team') -# pylint: enable=invalid-name +PlayerT = TypeVar('PlayerT', bound='bascenev1.Player') +TeamT = TypeVar('TeamT', bound='bascenev1.Team') -class GameActivity(Activity[PlayerType, TeamType]): - """Common base class for all game ba.Activities. +class GameActivity(Activity[PlayerT, TeamT]): + """Common base class for all game bascenev1.Activities. Category: **Gameplay Classes** """ @@ -41,7 +42,7 @@ class GameActivity(Activity[PlayerType, TeamType]): # pylint: disable=too-many-public-methods # Tips to be presented to the user at the start of the game. - tips: list[str | ba.GameTip] = [] + tips: list[str | bascenev1.GameTip] = [] # Default getname() will return this if not None. name: str | None = None @@ -50,10 +51,10 @@ class GameActivity(Activity[PlayerType, TeamType]): description: str | None = None # Default get_available_settings() will return this if not None. - available_settings: list[ba.Setting] | None = None + available_settings: list[bascenev1.Setting] | None = None # Default getscoreconfig() will return this if not None. - scoreconfig: ba.ScoreConfig | None = None + scoreconfig: bascenev1.ScoreConfig | None = None # Override some defaults. allow_pausing = True @@ -64,18 +65,19 @@ class GameActivity(Activity[PlayerType, TeamType]): # If not None, the music type that should play in on_transition_in() # (unless overridden by the map). - default_music: ba.MusicType | None = None + default_music: bascenev1.MusicType | None = None @classmethod def create_settings_ui( cls, - sessiontype: type[ba.Session], + sessiontype: type[bascenev1.Session], settings: dict | None, completion_call: Callable[[dict | None], None], ) -> None: """Launch an in-game UI to configure settings for a game type. - 'sessiontype' should be the ba.Session class the game will be used in. + 'sessiontype' should be the bascenev1.Session class the game will + be used in. 'settings' should be an existing settings dict (implies 'edit' ui mode) or None (implies 'add' ui mode). @@ -84,18 +86,19 @@ class GameActivity(Activity[PlayerType, TeamType]): success or None on cancel. Generally subclasses don't need to override this; if they override - ba.GameActivity.get_available_settings() and - ba.GameActivity.get_supported_maps() they can just rely on + bascenev1.GameActivity.get_available_settings() and + bascenev1.GameActivity.get_supported_maps() they can just rely on the default implementation here which calls those methods. """ - delegate = _ba.app.delegate + assert _babase.app.classic is not None + delegate = _babase.app.classic.delegate assert delegate is not None delegate.create_default_game_settings_ui( cls, sessiontype, settings, completion_call ) @classmethod - def getscoreconfig(cls) -> ba.ScoreConfig: + def getscoreconfig(cls) -> bascenev1.ScoreConfig: """Return info about game scoring setup; can be overridden by games.""" return cls.scoreconfig if cls.scoreconfig is not None else ScoreConfig() @@ -108,7 +111,7 @@ class GameActivity(Activity[PlayerType, TeamType]): return cls.name if cls.name is not None else 'Untitled Game' @classmethod - def get_display_string(cls, settings: dict | None = None) -> ba.Lstr: + def get_display_string(cls, settings: dict | None = None) -> babase.Lstr: """Return a descriptive name for this game/settings combo. Subclasses should override getname(); not this. @@ -131,12 +134,12 @@ class GameActivity(Activity[PlayerType, TeamType]): return name @classmethod - def get_team_display_string(cls, name: str) -> ba.Lstr: + def get_team_display_string(cls, name: str) -> babase.Lstr: """Given a team name, returns a localized version of it.""" return Lstr(translate=('teamNames', name)) @classmethod - def get_description(cls, sessiontype: type[ba.Session]) -> str: + def get_description(cls, sessiontype: type[bascenev1.Session]) -> str: """Get a str description of this game type. The default implementation simply returns the 'description' class var. @@ -148,8 +151,8 @@ class GameActivity(Activity[PlayerType, TeamType]): @classmethod def get_description_display_string( - cls, sessiontype: type[ba.Session] - ) -> ba.Lstr: + cls, sessiontype: type[bascenev1.Session] + ) -> babase.Lstr: """Return a translated version of get_description(). Sub-classes should override get_description(); not this. @@ -159,8 +162,8 @@ class GameActivity(Activity[PlayerType, TeamType]): @classmethod def get_available_settings( - cls, sessiontype: type[ba.Session] - ) -> list[ba.Setting]: + cls, sessiontype: type[bascenev1.Session] + ) -> list[bascenev1.Setting]: """Return a list of settings relevant to this game type when running under the provided session type. """ @@ -168,17 +171,20 @@ class GameActivity(Activity[PlayerType, TeamType]): return [] if cls.available_settings is None else cls.available_settings @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + def get_supported_maps( + cls, sessiontype: type[bascenev1.Session] + ) -> list[str]: """ - Called by the default ba.GameActivity.create_settings_ui() + Called by the default bascenev1.GameActivity.create_settings_ui() implementation; should return a list of map names valid - for this game-type for the given ba.Session type. + for this game-type for the given bascenev1.Session type. """ del sessiontype # Unused arg. - return _map.getmaps('melee') + assert _babase.app.classic is not None + return _babase.app.classic.getmaps('melee') @classmethod - def get_settings_display_string(cls, config: dict[str, Any]) -> ba.Lstr: + def get_settings_display_string(cls, config: dict[str, Any]) -> babase.Lstr: """Given a game config dict, return a short description for it. This is used when viewing game-lists or showing what game @@ -222,9 +228,11 @@ class GameActivity(Activity[PlayerType, TeamType]): return sval @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + def supports_session_type( + cls, sessiontype: type[bascenev1.Session] + ) -> bool: """Return whether this game supports the provided Session type.""" - from ba._multiteamsession import MultiTeamSession + from bascenev1._multiteamsession import MultiTeamSession # By default, games support any versus mode return issubclass(sessiontype, MultiTeamSession) @@ -233,59 +241,72 @@ class GameActivity(Activity[PlayerType, TeamType]): """Instantiate the Activity.""" super().__init__(settings) + plus = _babase.app.plus + # Holds some flattened info about the player set at the point # when on_begin() is called. - self.initialplayerinfos: list[ba.PlayerInfo] | None = None + self.initialplayerinfos: list[bascenev1.PlayerInfo] | None = None # Go ahead and get our map loading. self._map_type = _map.get_map_class(self._calc_map_name(settings)) - self._spawn_sound = _ba.getsound('spawn') + self._spawn_sound = _bascenev1.getsound('spawn') self._map_type.preload() - self._map: ba.Map | None = None - self._powerup_drop_timer: ba.Timer | None = None + self._map: bascenev1.Map | None = None + self._powerup_drop_timer: bascenev1.Timer | None = None self._tnt_spawners: dict[int, TNTSpawner] | None = None - self._tnt_drop_timer: ba.Timer | None = None - self._game_scoreboard_name_text: ba.Actor | None = None - self._game_scoreboard_description_text: ba.Actor | None = None + self._tnt_drop_timer: bascenev1.Timer | None = None + self._game_scoreboard_name_text: bascenev1.Actor | None = None + self._game_scoreboard_description_text: bascenev1.Actor | None = None self._standard_time_limit_time: int | None = None - self._standard_time_limit_timer: ba.Timer | None = None - self._standard_time_limit_text: ba.NodeActor | None = None - self._standard_time_limit_text_input: ba.NodeActor | None = None + self._standard_time_limit_timer: bascenev1.Timer | None = None + self._standard_time_limit_text: bascenev1.NodeActor | None = None + self._standard_time_limit_text_input: bascenev1.NodeActor | None = None self._tournament_time_limit: int | None = None - self._tournament_time_limit_timer: ba.Timer | None = None - self._tournament_time_limit_title_text: ba.NodeActor | None = None - self._tournament_time_limit_text: ba.NodeActor | None = None - self._tournament_time_limit_text_input: ba.NodeActor | None = None + self._tournament_time_limit_timer: bascenev1.BaseTimer | None = None + self._tournament_time_limit_title_text: bascenev1.NodeActor | None = ( + None + ) + self._tournament_time_limit_text: bascenev1.NodeActor | None = None + self._tournament_time_limit_text_input: bascenev1.NodeActor | None = ( + None + ) self._zoom_message_times: dict[int, float] = {} self._is_waiting_for_continue = False - self._continue_cost = _internal.get_v1_account_misc_read_val( - 'continueStartCost', 25 + self._continue_cost = ( + 25 + if plus is None + else plus.get_v1_account_misc_read_val('continueStartCost', 25) ) - self._continue_cost_mult = _internal.get_v1_account_misc_read_val( - 'continuesMult', 2 + self._continue_cost_mult = ( + 2 + if plus is None + else plus.get_v1_account_misc_read_val('continuesMult', 2) ) - self._continue_cost_offset = _internal.get_v1_account_misc_read_val( - 'continuesOffset', 0 + self._continue_cost_offset = ( + 0 + if plus is None + else plus.get_v1_account_misc_read_val('continuesOffset', 0) ) @property - def map(self) -> ba.Map: + def map(self) -> _map.Map: """The map being used for this game. - Raises a ba.MapNotFoundError if the map does not currently exist. + Raises a bascenev1.MapNotFoundError if the map does not currently + exist. """ if self._map is None: raise MapNotFoundError return self._map - def get_instance_display_string(self) -> ba.Lstr: + def get_instance_display_string(self) -> babase.Lstr: """Return a name for this particular game instance.""" return self.get_display_string(self.settings_raw) # noinspection PyUnresolvedReferences - def get_instance_scoreboard_display_string(self) -> ba.Lstr: + def get_instance_scoreboard_display_string(self) -> babase.Lstr: """Return a name for this particular game instance. This name is used above the game scoreboard in the corner @@ -294,7 +315,7 @@ class GameActivity(Activity[PlayerType, TeamType]): # If we're in a co-op session, use the level name. # FIXME: Should clean this up. try: - from ba._coopsession import CoopSession + from bascenev1._coopsession import CoopSession if isinstance(self.session, CoopSession): campaign = self.session.campaign @@ -371,8 +392,6 @@ class GameActivity(Activity[PlayerType, TeamType]): music = map_music if map_music is not None else self.default_music if music is not None: - from ba import _music - _music.setmusic(music) def on_continue(self) -> None: @@ -383,17 +402,20 @@ class GameActivity(Activity[PlayerType, TeamType]): """ def _continue_choice(self, do_continue: bool) -> None: + plus = _babase.app.plus + assert plus is not None self._is_waiting_for_continue = False if self.has_ended(): return - with _ba.Context(self): + with self.context: if do_continue: - _ba.playsound(_ba.getsound('shieldUp')) - _ba.playsound(_ba.getsound('cashRegister')) - _internal.add_transaction( + _bascenev1.getsound('shieldUp').play() + _bascenev1.getsound('cashRegister').play() + plus.add_v1_account_transaction( {'type': 'CONTINUE', 'cost': self._continue_cost} ) - _internal.run_transactions() + if plus is not None: + plus.run_v1_account_transactions() self._continue_cost = ( self._continue_cost * self._continue_cost_mult + self._continue_cost_offset @@ -409,21 +431,23 @@ class GameActivity(Activity[PlayerType, TeamType]): def continue_or_end_game(self) -> None: """If continues are allowed, prompts the player to purchase a continue - and calls either end_game or continue_game depending on the result""" + and calls either end_game or continue_game depending on the result + """ # pylint: disable=too-many-nested-blocks # pylint: disable=cyclic-import from bastd.ui.continues import ContinuesWindow - from ba._coopsession import CoopSession - from ba._generated.enums import TimeType + from bascenev1._coopsession import CoopSession + plus = _babase.app.plus try: - if _internal.get_v1_account_misc_read_val('enableContinues', False): + if plus is not None and plus.get_v1_account_misc_read_val( + 'enableContinues', False + ): session = self.session # We only support continuing in non-tournament games. tournament_id = session.tournament_id if tournament_id is None: - # We currently only support continuing in sequential # co-op campaigns. if isinstance(session, CoopSession): @@ -433,13 +457,12 @@ class GameActivity(Activity[PlayerType, TeamType]): # Only attempt this if we're not currently paused # and there appears to be no UI. - if ( - not gnode.paused - and not _ba.app.ui.has_main_menu_window() - ): + assert _babase.app.classic is not None + hmmw = _babase.app.classic.ui.has_main_menu_window() + if not gnode.paused and not hmmw: self._is_waiting_for_continue = True - with _ba.Context('ui'): - _ba.timer( + with _babase.ContextRef.empty(): + _babase.apptimer( 0.5, lambda: ContinuesWindow( self, @@ -451,7 +474,6 @@ class GameActivity(Activity[PlayerType, TeamType]): self._continue_choice, False ), ), - timetype=TimeType.REAL, ) return @@ -461,17 +483,16 @@ class GameActivity(Activity[PlayerType, TeamType]): self.end_game() def on_begin(self) -> None: - from ba._analytics import game_begin_analytics - super().on_begin() - game_begin_analytics() + if _babase.app.classic is not None: + _babase.app.classic.game_begin_analytics() # We don't do this in on_transition_in because it may depend on # players/teams which aren't available until now. - _ba.timer(0.001, self._show_scoreboard_info) - _ba.timer(1.0, self._show_info) - _ba.timer(2.5, self._show_tip) + _bascenev1.timer(0.001, self._show_scoreboard_info) + _bascenev1.timer(1.0, self._show_info) + _bascenev1.timer(2.5, self._show_tip) # Store some basic info about players present at start time. self.initialplayerinfos = [ @@ -487,7 +508,8 @@ class GameActivity(Activity[PlayerType, TeamType]): # time is left. tournament_id = self.session.tournament_id if tournament_id is not None: - _internal.tournament_query( + assert _babase.app.plus is not None + _babase.app.plus.tournament_query( args={ 'tournamentIDs': [tournament_id], 'source': 'in-game time remaining query', @@ -502,12 +524,13 @@ class GameActivity(Activity[PlayerType, TeamType]): data_t = data['t'] # This used to be the whole payload. # Keep our cached tourney info up to date - _ba.app.accounts_v1.cache_tournament_info(data_t) + assert _babase.app.classic is not None + _babase.app.classic.accounts.cache_tournament_info(data_t) self._setup_tournament_time_limit( max(5, data_t[0]['timeRemaining']) ) - def on_player_join(self, player: PlayerType) -> None: + def on_player_join(self, player: PlayerT) -> None: super().on_player_join(player) # By default, just spawn a dude. @@ -552,9 +575,9 @@ class GameActivity(Activity[PlayerType, TeamType]): and short description of the game. """ # pylint: disable=too-many-locals - from ba._freeforallsession import FreeForAllSession - from ba._gameutils import animate - from ba._nodeactor import NodeActor + from bascenev1._freeforallsession import FreeForAllSession + from bascenev1._gameutils import animate + from bascenev1._nodeactor import NodeActor sb_name = self.get_instance_scoreboard_display_string() @@ -577,7 +600,7 @@ class GameActivity(Activity[PlayerType, TeamType]): translate=('gameDescriptions', sb_desc_l[0]), subs=subs ) sb_desc = translation - vrmode = _ba.app.vr_mode + vrmode = _babase.app.vr_mode yval = -34 if is_empty else -20 yval -= 16 sbpos = ( @@ -586,7 +609,7 @@ class GameActivity(Activity[PlayerType, TeamType]): else (15, yval) ) self._game_scoreboard_name_text = NodeActor( - _ba.newnode( + _bascenev1.newnode( 'text', attrs={ 'text': sb_name, @@ -615,7 +638,7 @@ class GameActivity(Activity[PlayerType, TeamType]): else (17, -44 + 10) ) self._game_scoreboard_description_text = NodeActor( - _ba.newnode( + _bascenev1.newnode( 'text', attrs={ 'text': sb_desc, @@ -641,7 +664,7 @@ class GameActivity(Activity[PlayerType, TeamType]): def _show_info(self) -> None: """Show the game description.""" - from ba._gameutils import animate + from bascenev1._gameutils import animate from bastd.actor.zoomtext import ZoomText name = self.get_instance_display_string() @@ -655,7 +678,10 @@ class GameActivity(Activity[PlayerType, TeamType]): color=(0.93 * 1.25, 0.9 * 1.25, 1.0 * 1.25), trailcolor=(0.15, 0.05, 1.0, 0.0), ).autoretain() - _ba.timer(0.2, Call(_ba.playsound, _ba.getsound('gong'))) + _bascenev1.timer(0.2, _bascenev1.getsound('gong').play) + # _bascenev1.timer( + # 0.2, Call(_bascenev1.playsound, _bascenev1.getsound('gong')) + # ) # The description can be either a string or a sequence with args # to swap in post-translation. @@ -678,8 +704,8 @@ class GameActivity(Activity[PlayerType, TeamType]): resource='epicDescriptionFilterText', subs=[('${DESCRIPTION}', translation)], ) - vrmode = _ba.app.vr_mode - dnode = _ba.newnode( + vrmode = _babase.app.vr_mode + dnode = _bascenev1.newnode( 'text', attrs={ 'v_attach': 'center', @@ -695,7 +721,7 @@ class GameActivity(Activity[PlayerType, TeamType]): 'text': translation, }, ) - cnode = _ba.newnode( + cnode = _bascenev1.newnode( 'combine', owner=dnode, attrs={'input0': 1.0, 'input1': 1.0, 'input2': 1.0, 'size': 4}, @@ -703,12 +729,12 @@ class GameActivity(Activity[PlayerType, TeamType]): cnode.connectattr('output', dnode, 'color') keys = {0.5: 0, 1.0: 1.0, 2.5: 1.0, 4.0: 0.0} animate(cnode, 'input3', keys) - _ba.timer(4.0, dnode.delete) + _bascenev1.timer(4.0, dnode.delete) def _show_tip(self) -> None: # pylint: disable=too-many-locals - from ba._gameutils import animate, GameTip - from ba._generated.enums import SpecialChar + from babase._mgen.enums import SpecialChar + from bascenev1._gameutils import animate, GameTip # If there's any tips left on the list, display one. if self.tips: @@ -716,8 +742,8 @@ class GameActivity(Activity[PlayerType, TeamType]): tip_title = Lstr( value='${A}:', subs=[('${A}', Lstr(resource='tipText'))] ) - icon: ba.Texture | None = None - sound: ba.Sound | None = None + icon: bascenev1.Texture | None = None + sound: bascenev1.Sound | None = None if isinstance(tip, GameTip): icon = tip.icon sound = tip.sound @@ -727,15 +753,15 @@ class GameActivity(Activity[PlayerType, TeamType]): # Do a few substitutions. tip_lstr = Lstr( translate=('tips', tip), - subs=[('${PICKUP}', _ba.charstr(SpecialChar.TOP_BUTTON))], + subs=[('${PICKUP}', _babase.charstr(SpecialChar.TOP_BUTTON))], ) base_position = (75, 50) tip_scale = 0.8 tip_title_scale = 1.2 - vrmode = _ba.app.vr_mode + vrmode = _babase.app.vr_mode t_offs = -350.0 - tnode = _ba.newnode( + tnode = _bascenev1.newnode( 'text', attrs={ 'text': tip_lstr, @@ -754,7 +780,7 @@ class GameActivity(Activity[PlayerType, TeamType]): base_position[0] + t_offs - (20 if icon is None else 82), base_position[1] + 2, ) - t2node = _ba.newnode( + t2node = _bascenev1.newnode( 'text', owner=tnode, attrs={ @@ -772,7 +798,7 @@ class GameActivity(Activity[PlayerType, TeamType]): ) if icon is not None: ipos = (base_position[0] + t_offs - 40, base_position[1] + 1) - img = _ba.newnode( + img = _bascenev1.newnode( 'image', attrs={ 'texture': icon, @@ -786,11 +812,11 @@ class GameActivity(Activity[PlayerType, TeamType]): }, ) animate(img, 'opacity', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) - _ba.timer(5.0, img.delete) + _bascenev1.timer(5.0, img.delete) if sound is not None: - _ba.playsound(sound) + sound.play() - combine = _ba.newnode( + combine = _bascenev1.newnode( 'combine', owner=tnode, attrs={'input0': 1.0, 'input1': 0.8, 'input2': 1.0, 'size': 4}, @@ -798,12 +824,12 @@ class GameActivity(Activity[PlayerType, TeamType]): combine.connectattr('output', tnode, 'color') combine.connectattr('output', t2node, 'color') animate(combine, 'input3', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) - _ba.timer(5.0, tnode.delete) + _bascenev1.timer(5.0, tnode.delete) def end( self, results: Any = None, delay: float = 0.0, force: bool = False ) -> None: - from ba._gameresults import GameResults + from bascenev1._gameresults import GameResults # If results is a standard team-game-results, associate it with us # so it can grab our score prefs. @@ -831,12 +857,13 @@ class GameActivity(Activity[PlayerType, TeamType]): super().end(results, delay, force) def end_game(self) -> None: - """Tell the game to wrap up and call ba.Activity.end() immediately. + """Tell the game to wrap up and call bascenev1.Activity.end(). This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit - (ba.GameActivity.setup_standard_time_limit()) will work with the game. + (bascenev1.GameActivity.setup_standard_time_limit()) will work with + the game. """ print( 'WARNING: default end_game() implementation called;' @@ -844,10 +871,10 @@ class GameActivity(Activity[PlayerType, TeamType]): ) def respawn_player( - self, player: PlayerType, respawn_time: float | None = None + self, player: PlayerT, respawn_time: float | None = None ) -> None: """ - Given a ba.Player, sets up a standard respawn timer, + Given a bascenev1.Player, sets up a standard respawn timer, along with the standard counter display, etc. At the end of the respawn period spawn_player() will be called if the Player still exists. @@ -879,25 +906,26 @@ class GameActivity(Activity[PlayerType, TeamType]): if player.actor and not self.has_ended(): from bastd.actor.respawnicon import RespawnIcon - player.customdata['respawn_timer'] = _ba.Timer( + player.customdata['respawn_timer'] = _bascenev1.Timer( respawn_time, WeakCall(self.spawn_player_if_exists, player) ) player.customdata['respawn_icon'] = RespawnIcon( player, respawn_time ) - def spawn_player_if_exists(self, player: PlayerType) -> None: + def spawn_player_if_exists(self, player: PlayerT) -> None: """ A utility method which calls self.spawn_player() *only* if the - ba.Player provided still exists; handy for use in timers and whatnot. + bascenev1.Player provided still exists; handy for use in timers + and whatnot. There is no need to override this; just override spawn_player(). """ if player: self.spawn_player(player) - def spawn_player(self, player: PlayerType) -> ba.Actor: - """Spawn *something* for the provided ba.Player. + def spawn_player(self, player: PlayerT) -> bascenev1.Actor: + """Spawn *something* for the provided bascenev1.Player. The default implementation simply calls spawn_player_spaz(). """ @@ -907,16 +935,16 @@ class GameActivity(Activity[PlayerType, TeamType]): def spawn_player_spaz( self, - player: PlayerType, + player: PlayerT, position: Sequence[float] = (0, 0, 0), angle: float | None = None, ) -> PlayerSpaz: - """Create and wire up a ba.PlayerSpaz for the provided ba.Player.""" + """Create and wire up a bascenev1.PlayerSpaz for the provided Player.""" # pylint: disable=too-many-locals # pylint: disable=cyclic-import - from ba import _math - from ba._gameutils import animate - from ba._coopsession import CoopSession + from babase import _math + from bascenev1._gameutils import animate + from bascenev1._coopsession import CoopSession from bastd.actor.playerspaz import PlayerSpaz name = player.getname() @@ -928,7 +956,7 @@ class GameActivity(Activity[PlayerType, TeamType]): playerspaztype = PlayerSpaz light_color = _math.normalized_color(color) - display_color = _ba.safecolor(color, target_intensity=0.75) + display_color = _babase.safecolor(color, target_intensity=0.75) spaz = playerspaztype( color=color, highlight=highlight, @@ -962,11 +990,11 @@ class GameActivity(Activity[PlayerType, TeamType]): position, angle if angle is not None else random.uniform(0, 360) ) ) - _ba.playsound(self._spawn_sound, 1, position=spaz.node.position) - light = _ba.newnode('light', attrs={'color': light_color}) + self._spawn_sound.play(1, position=spaz.node.position) + light = _bascenev1.newnode('light', attrs={'color': light_color}) spaz.node.connectattr('position', light, 'position') animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) - _ba.timer(0.5, light.delete) + _bascenev1.timer(0.5, light.delete) return spaz def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None: @@ -974,7 +1002,7 @@ class GameActivity(Activity[PlayerType, TeamType]): # pylint: disable=cyclic-import from bastd.actor.powerupbox import DEFAULT_POWERUP_INTERVAL - self._powerup_drop_timer = _ba.Timer( + self._powerup_drop_timer = _bascenev1.Timer( DEFAULT_POWERUP_INTERVAL, WeakCall(self._standard_drop_powerups), repeat=True, @@ -1000,7 +1028,7 @@ class GameActivity(Activity[PlayerType, TeamType]): # Drop one powerup per point. points = self.map.powerup_spawn_points for i in range(len(points)): - _ba.timer(i * 0.4, WeakCall(self._standard_drop_powerup, i)) + _bascenev1.timer(i * 0.4, WeakCall(self._standard_drop_powerup, i)) def _setup_standard_tnt_drops(self) -> None: """Standard tnt drop.""" @@ -1019,16 +1047,16 @@ class GameActivity(Activity[PlayerType, TeamType]): This will be displayed at the top of the screen. If the time-limit expires, end_game() will be called. """ - from ba._nodeactor import NodeActor + from bascenev1._nodeactor import NodeActor if duration <= 0.0: return self._standard_time_limit_time = int(duration) - self._standard_time_limit_timer = _ba.Timer( + self._standard_time_limit_timer = _bascenev1.Timer( 1.0, WeakCall(self._standard_time_limit_tick), repeat=True ) self._standard_time_limit_text = NodeActor( - _ba.newnode( + _bascenev1.newnode( 'text', attrs={ 'v_attach': 'top', @@ -1042,7 +1070,7 @@ class GameActivity(Activity[PlayerType, TeamType]): ) ) self._standard_time_limit_text_input = NodeActor( - _ba.newnode( + _bascenev1.newnode( 'timedisplay', attrs={'time2': duration * 1000, 'timemin': 0} ) ) @@ -1056,7 +1084,7 @@ class GameActivity(Activity[PlayerType, TeamType]): ) def _standard_time_limit_tick(self) -> None: - from ba._gameutils import animate + from bascenev1._gameutils import animate assert self._standard_time_limit_time is not None self._standard_time_limit_time -= 1 @@ -1066,7 +1094,7 @@ class GameActivity(Activity[PlayerType, TeamType]): assert self._standard_time_limit_text.node self._standard_time_limit_text.node.scale = 1.3 self._standard_time_limit_text.node.position = (-30, -45) - cnode = _ba.newnode( + cnode = _bascenev1.newnode( 'combine', owner=self._standard_time_limit_text.node, attrs={'size': 4}, @@ -1078,11 +1106,11 @@ class GameActivity(Activity[PlayerType, TeamType]): animate(cnode, 'input1', {0: 1, 0.15: 0.5}, loop=True) animate(cnode, 'input2', {0: 0.1, 0.15: 0.0}, loop=True) cnode.input3 = 1.0 - _ba.playsound(_ba.getsound('tick')) + _bascenev1.getsound('tick').play() if self._standard_time_limit_time <= 0: self._standard_time_limit_timer = None self.end_game() - node = _ba.newnode( + node = _bascenev1.newnode( 'text', attrs={ 'v_attach': 'top', @@ -1094,7 +1122,7 @@ class GameActivity(Activity[PlayerType, TeamType]): 'text': Lstr(resource='timeExpiredText'), }, ) - _ba.playsound(_ba.getsound('refWhistle')) + _bascenev1.getsound('refWhistle').play() animate(node, 'scale', {0.0: 0.0, 0.1: 1.4, 0.15: 1.2}) def _setup_tournament_time_limit(self, duration: float) -> None: @@ -1104,8 +1132,7 @@ class GameActivity(Activity[PlayerType, TeamType]): This will be displayed at the top of the screen. If the time-limit expires, end_game() will be called. """ - from ba._nodeactor import NodeActor - from ba._generated.enums import TimeType + from bascenev1._nodeactor import NodeActor if duration <= 0.0: return @@ -1115,14 +1142,11 @@ class GameActivity(Activity[PlayerType, TeamType]): # so lets go with base-time. Theoretically we should do real-time but # then we have to mess with contexts and whatnot since its currently # not available in activity contexts. :-/ - self._tournament_time_limit_timer = _ba.Timer( - 1.0, - WeakCall(self._tournament_time_limit_tick), - repeat=True, - timetype=TimeType.BASE, + self._tournament_time_limit_timer = _bascenev1.BaseTimer( + 1.0, WeakCall(self._tournament_time_limit_tick), repeat=True ) self._tournament_time_limit_title_text = NodeActor( - _ba.newnode( + _bascenev1.newnode( 'text', attrs={ 'v_attach': 'bottom', @@ -1140,7 +1164,7 @@ class GameActivity(Activity[PlayerType, TeamType]): ) ) self._tournament_time_limit_text = NodeActor( - _ba.newnode( + _bascenev1.newnode( 'text', attrs={ 'v_attach': 'bottom', @@ -1157,7 +1181,7 @@ class GameActivity(Activity[PlayerType, TeamType]): ) ) self._tournament_time_limit_text_input = NodeActor( - _ba.newnode( + _bascenev1.newnode( 'timedisplay', attrs={ 'timemin': 0, @@ -1172,7 +1196,7 @@ class GameActivity(Activity[PlayerType, TeamType]): ) def _tournament_time_limit_tick(self) -> None: - from ba._gameutils import animate + from bascenev1._gameutils import animate assert self._tournament_time_limit is not None self._tournament_time_limit -= 1 @@ -1186,7 +1210,7 @@ class GameActivity(Activity[PlayerType, TeamType]): self._tournament_time_limit_text.node.scale = 1.3 self._tournament_time_limit_title_text.node.position = (80, 85) self._tournament_time_limit_text.node.position = (80, 60) - cnode = _ba.newnode( + cnode = _bascenev1.newnode( 'combine', owner=self._tournament_time_limit_text.node, attrs={'size': 4}, @@ -1203,7 +1227,7 @@ class GameActivity(Activity[PlayerType, TeamType]): animate(cnode, 'input1', {0: 1, 0.15: 0.5}, loop=True) animate(cnode, 'input2', {0: 0.1, 0.15: 0.0}, loop=True) cnode.input3 = 1.0 - _ba.playsound(_ba.getsound('tick')) + _bascenev1.getsound('tick').play() if self._tournament_time_limit <= 0: self._tournament_time_limit_timer = None self.end_game() @@ -1211,7 +1235,7 @@ class GameActivity(Activity[PlayerType, TeamType]): resource='tournamentTimeExpiredText', fallback_resource='timeExpiredText', ) - node = _ba.newnode( + node = _bascenev1.newnode( 'text', attrs={ 'v_attach': 'top', @@ -1223,7 +1247,7 @@ class GameActivity(Activity[PlayerType, TeamType]): 'text': tval, }, ) - _ba.playsound(_ba.getsound('refWhistle')) + _bascenev1.getsound('refWhistle').play() animate(node, 'scale', {0: 0.0, 0.1: 1.4, 0.15: 1.2}) # Normally we just connect this to time, but since this is a bit of a @@ -1236,7 +1260,7 @@ class GameActivity(Activity[PlayerType, TeamType]): def show_zoom_message( self, - message: ba.Lstr, + message: babase.Lstr, color: Sequence[float] = (0.9, 0.4, 0.0), scale: float = 0.8, duration: float = 2.0, @@ -1249,7 +1273,7 @@ class GameActivity(Activity[PlayerType, TeamType]): # Reserve a spot on the screen (in case we get multiple of these so # they don't overlap). i = 0 - cur_time = _ba.time() + cur_time = _babase.apptime() while True: if ( i not in self._zoom_message_times @@ -1276,14 +1300,18 @@ class GameActivity(Activity[PlayerType, TeamType]): else: # If settings doesn't specify a map, pick a random one from the # list of supported ones. - unowned_maps = _store.get_unowned_maps() + unowned_maps: list[str] = ( + _babase.app.classic.store.get_unowned_maps() + if _babase.app.classic is not None + else [] + ) valid_maps: list[str] = [ m for m in self.get_supported_maps(type(self.session)) if m not in unowned_maps ] if not valid_maps: - _ba.screenmessage(Lstr(resource='noValidMapsErrorText')) - raise Exception('No valid maps') + _babase.screenmessage(Lstr(resource='noValidMapsErrorText')) + raise RuntimeError('No valid maps') map_name = valid_maps[random.randrange(len(valid_maps))] return map_name diff --git a/assets/src/ba_data/python/ba/_gameresults.py b/src/assets/ba_data/python/bascenev1/_gameresults.py similarity index 75% rename from assets/src/ba_data/python/ba/_gameresults.py rename to src/assets/ba_data/python/bascenev1/_gameresults.py index 7f223157..a974449c 100644 --- a/assets/src/ba_data/python/ba/_gameresults.py +++ b/src/assets/ba_data/python/bascenev1/_gameresults.py @@ -9,11 +9,13 @@ from dataclasses import dataclass from typing import TYPE_CHECKING from efro.util import asserttype -from ba._team import Team, SessionTeam +from bascenev1._team import Team, SessionTeam if TYPE_CHECKING: from typing import Sequence - import ba + + import babase + import bascenev1 @dataclass @@ -21,7 +23,7 @@ class WinnerGroup: """Entry for a winning team or teams calculated by game-results.""" score: int | None - teams: Sequence[ba.SessionTeam] + teams: Sequence[bascenev1.SessionTeam] class GameResults: @@ -31,22 +33,24 @@ class GameResults: Category: **Gameplay Classes** Upon completion, a game should fill one of these out and pass it to its - ba.Activity.end call. + bascenev1.Activity.end call. """ def __init__(self) -> None: self._game_set = False self._scores: dict[ - int, tuple[weakref.ref[ba.SessionTeam], int | None] + int, tuple[weakref.ref[bascenev1.SessionTeam], int | None] ] = {} - self._sessionteams: list[weakref.ref[ba.SessionTeam]] | None = None - self._playerinfos: list[ba.PlayerInfo] | None = None + self._sessionteams: list[ + weakref.ref[bascenev1.SessionTeam] + ] | None = None + self._playerinfos: list[bascenev1.PlayerInfo] | None = None self._lower_is_better: bool | None = None self._score_label: str | None = None self._none_is_winner: bool | None = None - self._scoretype: ba.ScoreType | None = None + self._scoretype: bascenev1.ScoreType | None = None - def set_game(self, game: ba.GameActivity) -> None: + def set_game(self, game: bascenev1.GameActivity) -> None: """Set the game instance these results are applying to.""" if self._game_set: raise RuntimeError('Game set twice for GameResults.') @@ -61,7 +65,7 @@ class GameResults: self._none_is_winner = scoreconfig.none_is_winner self._scoretype = scoreconfig.scoretype - def set_team_score(self, team: ba.Team, score: int | None) -> None: + def set_team_score(self, team: bascenev1.Team, score: int | None) -> None: """Set the score for a given team. This can be a number or None. @@ -71,8 +75,10 @@ class GameResults: sessionteam = team.sessionteam self._scores[sessionteam.id] = (weakref.ref(sessionteam), score) - def get_sessionteam_score(self, sessionteam: ba.SessionTeam) -> int | None: - """Return the score for a given ba.SessionTeam.""" + def get_sessionteam_score( + self, sessionteam: bascenev1.SessionTeam + ) -> int | None: + """Return the score for a given bascenev1.SessionTeam.""" assert isinstance(sessionteam, SessionTeam) for score in list(self._scores.values()): if score[0]() is sessionteam: @@ -82,8 +88,8 @@ class GameResults: return None @property - def sessionteams(self) -> list[ba.SessionTeam]: - """Return all ba.SessionTeams in the results.""" + def sessionteams(self) -> list[bascenev1.SessionTeam]: + """Return all bascenev1.SessionTeams in the results.""" if not self._game_set: raise RuntimeError("Can't get teams until game is set.") teams = [] @@ -94,19 +100,23 @@ class GameResults: teams.append(team) return teams - def has_score_for_sessionteam(self, sessionteam: ba.SessionTeam) -> bool: + def has_score_for_sessionteam( + self, sessionteam: bascenev1.SessionTeam + ) -> bool: """Return whether there is a score for a given session-team.""" return any(s[0]() is sessionteam for s in self._scores.values()) - def get_sessionteam_score_str(self, sessionteam: ba.SessionTeam) -> ba.Lstr: + def get_sessionteam_score_str( + self, sessionteam: bascenev1.SessionTeam + ) -> babase.Lstr: """Return the score for the given session-team as an Lstr. (properly formatted for the score type.) """ - from ba._gameutils import timestring - from ba._language import Lstr - from ba._generated.enums import TimeFormat - from ba._score import ScoreType + from babase._language import Lstr + from babase._mgen.enums import TimeFormat + from babase._text import timestring + from bascenev1._score import ScoreType if not self._game_set: raise RuntimeError("Can't get team-score-str until game is set.") @@ -115,20 +125,14 @@ class GameResults: if score[1] is None: return Lstr(value='-') if self._scoretype is ScoreType.SECONDS: - return timestring( - score[1] * 1000, - centi=False, - timeformat=TimeFormat.MILLISECONDS, - ) + return timestring(score[1], centi=False) if self._scoretype is ScoreType.MILLISECONDS: - return timestring( - score[1], centi=True, timeformat=TimeFormat.MILLISECONDS - ) + return timestring(score[1] / 1000.0, centi=True) return Lstr(value=str(score[1])) return Lstr(value='-') @property - def playerinfos(self) -> list[ba.PlayerInfo]: + def playerinfos(self) -> list[bascenev1.PlayerInfo]: """Get info about the players represented by the results.""" if not self._game_set: raise RuntimeError("Can't get player-info until game is set.") @@ -136,7 +140,7 @@ class GameResults: return self._playerinfos @property - def scoretype(self) -> ba.ScoreType: + def scoretype(self) -> bascenev1.ScoreType: """The type of score.""" if not self._game_set: raise RuntimeError("Can't get score-type until game is set.") @@ -160,8 +164,8 @@ class GameResults: return self._lower_is_better @property - def winning_sessionteam(self) -> ba.SessionTeam | None: - """The winning ba.SessionTeam if there is exactly one, or else None.""" + def winning_sessionteam(self) -> bascenev1.SessionTeam | None: + """The winning SessionTeam if there is exactly one, or else None.""" if not self._game_set: raise RuntimeError("Can't get winners until game is set.") winners = self.winnergroups @@ -176,7 +180,7 @@ class GameResults: raise RuntimeError("Can't get winners until game is set.") # Group by best scoring teams. - winners: dict[int, list[ba.SessionTeam]] = {} + winners: dict[int, list[bascenev1.SessionTeam]] = {} scores = [ score for score in self._scores.values() @@ -188,7 +192,7 @@ class GameResults: team = score[0]() assert team is not None sval.append(team) - results: list[tuple[int | None, list[ba.SessionTeam]]] = list( + results: list[tuple[int | None, list[bascenev1.SessionTeam]]] = list( winners.items() ) results.sort( @@ -197,7 +201,7 @@ class GameResults: ) # Also group the 'None' scores. - none_sessionteams: list[ba.SessionTeam] = [] + none_sessionteams: list[bascenev1.SessionTeam] = [] for score in self._scores.values(): scoreteam = score[0]() if scoreteam is not None and score[1] is None: @@ -206,7 +210,7 @@ class GameResults: # Add the Nones to the list (either as winners or losers # depending on the rules). if none_sessionteams: - nones: list[tuple[int | None, list[ba.SessionTeam]]] = [ + nones: list[tuple[int | None, list[bascenev1.SessionTeam]]] = [ (None, none_sessionteams) ] if self._none_is_winner: diff --git a/assets/src/ba_data/python/ba/_gameutils.py b/src/assets/ba_data/python/bascenev1/_gameutils.py similarity index 54% rename from assets/src/ba_data/python/ba/_gameutils.py rename to src/assets/ba_data/python/bascenev1/_gameutils.py index bd9c417a..212e83aa 100644 --- a/assets/src/ba_data/python/ba/_gameutils.py +++ b/src/assets/ba_data/python/bascenev1/_gameutils.py @@ -5,15 +5,21 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, NewType -import _ba -from ba._generated.enums import TimeType, TimeFormat, SpecialChar, UIScale -from ba._error import ActivityNotFoundError +import _babase +from babase._mgen.enums import TimeType, TimeFormat, SpecialChar, UIScale +from babase._error import ActivityNotFoundError +import _bascenev1 if TYPE_CHECKING: from typing import Sequence - import ba + + import babase + import bascenev1 + +Time = NewType('Time', float) +BaseTime = NewType('BaseTime', float) TROPHY_CHARS = { '1': SpecialChar.TROPHY1, @@ -33,28 +39,25 @@ class GameTip: """ text: str - icon: ba.Texture | None = None - sound: ba.Sound | None = None + icon: bascenev1.Texture | None = None + sound: bascenev1.Sound | None = None def get_trophy_string(trophy_id: str) -> str: """Given a trophy id, returns a string to visualize it.""" if trophy_id in TROPHY_CHARS: - return _ba.charstr(TROPHY_CHARS[trophy_id]) + return _babase.charstr(TROPHY_CHARS[trophy_id]) return '?' def animate( - node: ba.Node, + node: bascenev1.Node, attr: str, keys: dict[float, float], loop: bool = False, offset: float = 0, - timetype: ba.TimeType = TimeType.SIM, - timeformat: ba.TimeFormat = TimeFormat.SECONDS, - suppress_format_warning: bool = False, -) -> ba.Node: - """Animate values on a target ba.Node. +) -> bascenev1.Node: + """Animate values on a target bascenev1.Node. Category: **Gameplay Functions** @@ -65,37 +68,20 @@ def animate( but timeformat can also be set to MILLISECONDS to recreate the old behavior (prior to ba 1.5) of taking milliseconds. Returns the animcurve node. """ - if timetype is TimeType.SIM: - driver = 'time' - else: - raise Exception('FIXME; only SIM timetype is supported currently.') items = list(keys.items()) items.sort() - # Temp sanity check while we transition from milliseconds to seconds - # based time values. - if __debug__: - if not suppress_format_warning: - for item in items: - _ba.time_format_check(timeformat, item[0]) - - curve = _ba.newnode( + curve = _bascenev1.newnode( 'animcurve', owner=node, name='Driving ' + str(node) + ' \'' + attr + '\'', ) - if timeformat is TimeFormat.SECONDS: - mult = 1000 - elif timeformat is TimeFormat.MILLISECONDS: - mult = 1 - else: - raise ValueError(f'invalid timeformat value: {timeformat}') + # We take seconds but operate on milliseconds internally. + mult = 1000 curve.times = [int(mult * time) for time, val in items] - curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int( - mult * offset - ) + curve.offset = int(_bascenev1.time() * 1000.0) + int(mult * offset) curve.values = [val for time, val in items] curve.loop = loop @@ -105,10 +91,8 @@ def animate( # get disconnected. if not loop: # noinspection PyUnresolvedReferences - _ba.timer( - int(mult * items[-1][0]) + 1000, - curve.delete, - timeformat=TimeFormat.MILLISECONDS, + _bascenev1.timer( + (int(mult * items[-1][0]) + 1000) / 1000.0, curve.delete ) # Do the connects last so all our attrs are in place when we push initial @@ -116,76 +100,55 @@ def animate( # We operate in either activities or sessions.. try: - globalsnode = _ba.getactivity().globalsnode + globalsnode = _bascenev1.getactivity().globalsnode except ActivityNotFoundError: - globalsnode = _ba.getsession().sessionglobalsnode + globalsnode = _bascenev1.getsession().sessionglobalsnode - globalsnode.connectattr(driver, curve, 'in') + globalsnode.connectattr('time', curve, 'in') curve.connectattr('out', node, attr) return curve def animate_array( - node: ba.Node, + node: bascenev1.Node, attr: str, size: int, keys: dict[float, Sequence[float]], loop: bool = False, offset: float = 0, - timetype: ba.TimeType = TimeType.SIM, - timeformat: ba.TimeFormat = TimeFormat.SECONDS, - suppress_format_warning: bool = False, ) -> None: - """Animate an array of values on a target ba.Node. + """Animate an array of values on a target bascenev1.Node. Category: **Gameplay Functions** - Like ba.animate, but operates on array attributes. + Like bs.animate, but operates on array attributes. """ # pylint: disable=too-many-locals - combine = _ba.newnode('combine', owner=node, attrs={'size': size}) - if timetype is TimeType.SIM: - driver = 'time' - else: - raise Exception('FIXME: Only SIM timetype is supported currently.') + combine = _bascenev1.newnode('combine', owner=node, attrs={'size': size}) items = list(keys.items()) items.sort() - # Temp sanity check while we transition from milliseconds to seconds - # based time values. - if __debug__: - if not suppress_format_warning: - for item in items: - # (PyCharm seems to think item is a float, not a tuple) - _ba.time_format_check(timeformat, item[0]) - - if timeformat is TimeFormat.SECONDS: - mult = 1000 - elif timeformat is TimeFormat.MILLISECONDS: - mult = 1 - else: - raise ValueError('invalid timeformat value: "' + str(timeformat) + '"') + # We take seconds but operate on milliseconds internally. + mult = 1000 # We operate in either activities or sessions.. try: - globalsnode = _ba.getactivity().globalsnode + globalsnode = _bascenev1.getactivity().globalsnode except ActivityNotFoundError: - globalsnode = _ba.getsession().sessionglobalsnode + globalsnode = _bascenev1.getsession().sessionglobalsnode for i in range(size): - curve = _ba.newnode( + curve = _bascenev1.newnode( 'animcurve', owner=node, name=( 'Driving ' + str(node) + ' \'' + attr + '\' member ' + str(i) ), ) - globalsnode.connectattr(driver, curve, 'in') + globalsnode.connectattr('time', curve, 'in') curve.times = [int(mult * time) for time, val in items] curve.values = [val[i] for time, val in items] - curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int( - mult * offset - ) + curve.offset = int(_bascenev1.time() * 1000.0) + int(mult * offset) curve.loop = loop curve.connectattr('out', combine, 'input' + str(i)) @@ -194,10 +157,9 @@ def animate_array( if not loop: # (PyCharm seems to think item is a float, not a tuple) # noinspection PyUnresolvedReferences - _ba.timer( - int(mult * items[-1][0]) + 1000, + _bascenev1.timer( + (int(mult * items[-1][0]) + 1000) / 1000.0, curve.delete, - timeformat=TimeFormat.MILLISECONDS, ) combine.connectattr('output', node, attr) @@ -208,10 +170,8 @@ def animate_array( if not loop: # (PyCharm seems to think item is a float, not a tuple) # noinspection PyUnresolvedReferences - _ba.timer( - int(mult * items[-1][0]) + 1000, - combine.delete, - timeformat=TimeFormat.MILLISECONDS, + _bascenev1.timer( + (int(mult * items[-1][0]) + 1000) / 1000.0, combine.delete ) @@ -223,13 +183,14 @@ def show_damage_count( Category: **Gameplay Functions** """ lifespan = 1.0 - app = _ba.app + app = _babase.app # FIXME: Should never vary game elements based on local config. # (connected clients may have differing configs so they won't # get the intended results). - do_big = app.ui.uiscale is UIScale.SMALL or app.vr_mode - txtnode = _ba.newnode( + assert app.classic is not None + do_big = app.classic.ui.uiscale is UIScale.SMALL or app.vr_mode + txtnode = _bascenev1.newnode( 'text', attrs={ 'text': damage, @@ -242,7 +203,7 @@ def show_damage_count( }, ) # Translate upward. - tcombine = _ba.newnode('combine', owner=txtnode, attrs={'size': 3}) + tcombine = _bascenev1.newnode('combine', owner=txtnode, attrs={'size': 3}) tcombine.connectattr('output', txtnode, 'position') v_vals = [] pval = 0.0 @@ -274,104 +235,7 @@ def show_damage_count( {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals}, ) animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0}) - _ba.timer(lifespan, txtnode.delete) - - -def timestring( - timeval: float | int, - centi: bool = True, - timeformat: ba.TimeFormat = TimeFormat.SECONDS, - suppress_format_warning: bool = False, -) -> ba.Lstr: - """Generate a ba.Lstr for displaying a time value. - - Category: **General Utility Functions** - - Given a time value, returns a ba.Lstr with: - (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True). - - Time 'timeval' is specified in seconds by default, or 'timeformat' can - be set to ba.TimeFormat.MILLISECONDS to accept milliseconds instead. - - WARNING: the underlying Lstr value is somewhat large so don't use this - to rapidly update Node text values for an onscreen timer or you may - consume significant network bandwidth. For that purpose you should - use a 'timedisplay' Node and attribute connections. - - """ - from ba._language import Lstr - - # Temp sanity check while we transition from milliseconds to seconds - # based time values. - if __debug__: - if not suppress_format_warning: - _ba.time_format_check(timeformat, timeval) - - # We operate on milliseconds internally. - if timeformat is TimeFormat.SECONDS: - timeval = int(1000 * timeval) - elif timeformat is TimeFormat.MILLISECONDS: - pass - else: - raise ValueError(f'invalid timeformat: {timeformat}') - if not isinstance(timeval, int): - timeval = int(timeval) - bits = [] - subs = [] - hval = (timeval // 1000) // (60 * 60) - if hval != 0: - bits.append('${H}') - subs.append( - ( - '${H}', - Lstr( - resource='timeSuffixHoursText', - subs=[('${COUNT}', str(hval))], - ), - ) - ) - mval = ((timeval // 1000) // 60) % 60 - if mval != 0: - bits.append('${M}') - subs.append( - ( - '${M}', - Lstr( - resource='timeSuffixMinutesText', - subs=[('${COUNT}', str(mval))], - ), - ) - ) - - # We add seconds if its non-zero *or* we haven't added anything else. - if centi: - # pylint: disable=consider-using-f-string - sval = timeval / 1000.0 % 60.0 - if sval >= 0.005 or not bits: - bits.append('${S}') - subs.append( - ( - '${S}', - Lstr( - resource='timeSuffixSecondsText', - subs=[('${COUNT}', ('%.2f' % sval))], - ), - ) - ) - else: - sval = timeval // 1000 % 60 - if sval != 0 or not bits: - bits.append('${S}') - subs.append( - ( - '${S}', - Lstr( - resource='timeSuffixSecondsText', - subs=[('${COUNT}', str(sval))], - ), - ) - ) - return Lstr(value=' '.join(bits), subs=subs) + _bascenev1.timer(lifespan, txtnode.delete) def cameraflash(duration: float = 999.0) -> None: @@ -384,7 +248,7 @@ def cameraflash(duration: float = 999.0) -> None: """ # pylint: disable=too-many-locals import random - from ba._nodeactor import NodeActor + from bascenev1._nodeactor import NodeActor x_spread = 10 y_spread = 5 @@ -400,11 +264,11 @@ def cameraflash(duration: float = 999.0) -> None: # Store this on the current activity so we only have one at a time. # FIXME: Need a type safe way to do this. - activity = _ba.getactivity() + activity = _bascenev1.getactivity() activity.camera_flash_data = [] # type: ignore for i in range(6): light = NodeActor( - _ba.newnode( + _bascenev1.newnode( 'light', attrs={ 'position': (positions[i][0], 0, positions[i][1]), @@ -417,7 +281,7 @@ def cameraflash(duration: float = 999.0) -> None: ) sval = 1.87 iscale = 1.3 - tcombine = _ba.newnode( + tcombine = _bascenev1.newnode( 'combine', owner=light.node, attrs={ @@ -468,9 +332,8 @@ def cameraflash(duration: float = 999.0) -> None: loop=True, offset=times[i], ) - _ba.timer( - (times[i] + random.randint(1, int(duration)) * 40 * sval), + _bascenev1.timer( + (times[i] + random.randint(1, int(duration)) * 40 * sval) / 1000.0, light.node.delete, - timeformat=TimeFormat.MILLISECONDS, ) activity.camera_flash_data.append(light) # type: ignore diff --git a/src/assets/ba_data/python/bascenev1/_hooks.py b/src/assets/ba_data/python/bascenev1/_hooks.py new file mode 100644 index 00000000..93f1e835 --- /dev/null +++ b/src/assets/ba_data/python/bascenev1/_hooks.py @@ -0,0 +1,53 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Snippets of code for use by the c++ layer.""" +# (most of these are self-explanatory) +# pylint: disable=missing-function-docstring +from __future__ import annotations + +from typing import TYPE_CHECKING + +import _babase +import _bascenev1 + +if TYPE_CHECKING: + from typing import Any + + import bascenev1 + + +def launch_main_menu_session() -> None: + from bastd.mainmenu import MainMenuSession + + _bascenev1.new_host_session(MainMenuSession) + + +def get_player_icon(sessionplayer: bascenev1.SessionPlayer) -> dict[str, Any]: + info = sessionplayer.get_icon_info() + return { + 'texture': _bascenev1.gettexture(info['texture']), + 'tint_texture': _bascenev1.gettexture(info['tint_texture']), + 'tint_color': info['tint_color'], + 'tint2_color': info['tint2_color'], + } + + +def filter_chat_message(msg: str, client_id: int) -> str | None: + """Intercept/filter chat messages. + + Called for all chat messages while hosting. + Messages originating from the host will have clientID -1. + Should filter and return the string to be displayed, or return None + to ignore the message. + """ + del client_id # Unused by default. + return msg + + +def local_chat_message(msg: str) -> None: + assert _babase.app.classic is not None + if ( + _babase.app.classic.ui.party_window is not None + and _babase.app.classic.ui.party_window() is not None + ): + _babase.app.classic.ui.party_window().on_chat_message(msg) diff --git a/assets/src/ba_data/python/ba/_map.py b/src/assets/ba_data/python/bascenev1/_map.py similarity index 77% rename from assets/src/ba_data/python/ba/_map.py rename to src/assets/ba_data/python/bascenev1/_map.py index b3a5c675..bd8d75c1 100644 --- a/assets/src/ba_data/python/ba/_map.py +++ b/src/assets/ba_data/python/bascenev1/_map.py @@ -6,13 +6,17 @@ from __future__ import annotations import random from typing import TYPE_CHECKING -import _ba -from ba import _math -from ba._actor import Actor +import _babase +import _bascenev1 +from babase import _math +from bascenev1._actor import Actor if TYPE_CHECKING: from typing import Sequence, Any - import ba + + import babase + import baclassic + import bascenev1 def preload_map_preview_media() -> None: @@ -20,12 +24,13 @@ def preload_map_preview_media() -> None: Category: **Asset Functions** """ - _ba.getmodel('level_select_button_opaque') - _ba.getmodel('level_select_button_transparent') - for maptype in list(_ba.app.maps.values()): + assert _babase.app.classic is not None + _bascenev1.getmesh('level_select_button_opaque') + _bascenev1.getmesh('level_select_button_transparent') + for maptype in list(_babase.app.classic.maps.values()): map_tex_name = maptype.get_preview_texture_name() if map_tex_name is not None: - _ba.gettexture(map_tex_name) + _bascenev1.gettexture(map_tex_name) def get_filtered_map_name(name: str) -> str: @@ -43,78 +48,27 @@ def get_filtered_map_name(name: str) -> str: return name -def get_map_display_string(name: str) -> ba.Lstr: - """Return a ba.Lstr for displaying a given map\'s name. +def get_map_display_string(name: str) -> babase.Lstr: + """Return a babase.Lstr for displaying a given map\'s name. Category: **Asset Functions** """ - from ba import _language + from babase import _language return _language.Lstr(translate=('mapsNames', name)) -def getmaps(playtype: str) -> list[str]: - """Return a list of ba.Map types supporting a playtype str. - - Category: **Asset Functions** - - Maps supporting a given playtype must provide a particular set of - features and lend themselves to a certain style of play. - - Play Types: - - 'melee' - General fighting map. - Has one or more 'spawn' locations. - - 'team_flag' - For games such as Capture The Flag where each team spawns by a flag. - Has two or more 'spawn' locations, each with a corresponding 'flag' - location (based on index). - - 'single_flag' - For games such as King of the Hill or Keep Away where multiple teams - are fighting over a single flag. - Has two or more 'spawn' locations and 1 'flag_default' location. - - 'conquest' - For games such as Conquest where flags are spread throughout the map - - has 2+ 'flag' locations, 2+ 'spawn_by_flag' locations. - - 'king_of_the_hill' - has 2+ 'spawn' locations, 1+ 'flag_default' locations, - and 1+ 'powerup_spawn' locations - - 'hockey' - For hockey games. - Has two 'goal' locations, corresponding 'spawn' locations, and one - 'flag_default' location (for where puck spawns) - - 'football' - For football games. - Has two 'goal' locations, corresponding 'spawn' locations, and one - 'flag_default' location (for where flag/ball/etc. spawns) - - 'race' - For racing games where players much touch each region in order. - Has two or more 'race_point' locations. - """ - return sorted( - key - for key, val in _ba.app.maps.items() - if playtype in val.get_play_types() - ) - - -def get_map_class(name: str) -> type[ba.Map]: +def get_map_class(name: str) -> type[Map]: """Return a map type given a name. Category: **Asset Functions** """ + assert _babase.app.classic is not None name = get_filtered_map_name(name) try: - return _ba.app.maps[name] + return _babase.app.classic.maps[name] except KeyError: - from ba import _error + from babase import _error raise _error.NotFoundError(f"Map not found: '{name}'") from None @@ -137,11 +91,12 @@ class Map(Actor): """Preload map media. This runs the class's on_preload() method as needed to prep it to run. - Preloading should generally be done in a ba.Activity's __init__ method. - Note that this is a classmethod since it is not operate on map - instances but rather on the class itself before instances are made + Preloading should generally be done in a bascenev1.Activity's + __init__ method. Note that this is a classmethod since it is not + operate on map instances but rather on the class itself before + instances are made """ - activity = _ba.getactivity() + activity = _bascenev1.getactivity() if cls not in activity.preloads: activity.preloads[cls] = cls.on_preload() @@ -169,7 +124,7 @@ class Map(Actor): return cls.name @classmethod - def get_music_type(cls) -> ba.MusicType | None: + def get_music_type(cls) -> bascenev1.MusicType | None: """Return a music-type string that should be played on this map. If None is returned, default music will be used. @@ -182,16 +137,17 @@ class Map(Actor): """Instantiate a map.""" super().__init__() - # This is expected to always be a ba.Node object (whether valid or not) - # should be set to something meaningful by child classes. - self.node: _ba.Node | None = None + # This is expected to always be a bascenev1.Node object + # (whether valid or not) should be set to something meaningful + # by child classes. + self.node: _bascenev1.Node | None = None # Make our class' preload-data available to us # (and instruct the user if we weren't preloaded properly). try: - self.preloaddata = _ba.getactivity().preloads[type(self)] + self.preloaddata = _bascenev1.getactivity().preloads[type(self)] except Exception as exc: - from ba import _error + from babase import _error raise _error.NotFoundError( 'Preload data not found for ' @@ -201,7 +157,7 @@ class Map(Actor): ) from exc # Set various globals. - gnode = _ba.getactivity().globalsnode + gnode = _bascenev1.getactivity().globalsnode # Set area-of-interest bounds. aoi_bounds = self.get_def_bound_box('area_of_interest_bounds') @@ -215,7 +171,7 @@ class Map(Actor): if map_bounds is None: print('WARNING: no "map_bounds" found for map:', self.getname()) map_bounds = (-30, -10, -30, 30, 100, 30) - _ba.set_map_bounds(map_bounds) + _bascenev1.set_map_bounds(map_bounds) # Set shadow ranges. try: @@ -284,7 +240,9 @@ class Map(Actor): len(self.ffa_spawn_points) ) - def is_point_near_edge(self, point: ba.Vec3, running: bool = False) -> bool: + def is_point_near_edge( + self, point: babase.Vec3, running: bool = False + ) -> bool: """Return whether the provided point is near an edge of the map. Simple bot logic uses this call to determine if they @@ -356,12 +314,12 @@ class Map(Actor): return pnt def get_ffa_start_position( - self, players: Sequence[ba.Player] + self, players: Sequence[bascenev1.Player] ) -> Sequence[float]: """Return a random starting position in one of the FFA spawn areas. - If a list of ba.Player-s is provided; the returned points will be - as far from these players as possible. + If a list of bascenev1.Player-s is provided; the returned points + will be as far from these players as possible. """ # Get positions for existing players. @@ -392,7 +350,7 @@ class Map(Actor): farthestpt_dist = -1.0 farthestpt = None for _i in range(10): - testpt = _ba.Vec3(_getpt()) + testpt = _babase.Vec3(_getpt()) closest_player_dist = 9999.0 for ppt in player_pts: dist = (ppt - testpt).length() @@ -420,7 +378,7 @@ class Map(Actor): return bool(self.node) def handlemessage(self, msg: Any) -> Any: - from ba import _messages + from bascenev1 import _messages if isinstance(msg, _messages.DieMessage): if self.node: @@ -432,6 +390,7 @@ class Map(Actor): def register_map(maptype: type[Map]) -> None: """Register a map class with the game.""" - if maptype.name in _ba.app.maps: + assert _babase.app.classic is not None + if maptype.name in _babase.app.classic.maps: raise RuntimeError('map "' + maptype.name + '" already registered') - _ba.app.maps[maptype.name] = maptype + _babase.app.classic.maps[maptype.name] = maptype diff --git a/assets/src/ba_data/python/ba/_messages.py b/src/assets/ba_data/python/bascenev1/_messages.py similarity index 84% rename from assets/src/ba_data/python/ba/_messages.py rename to src/assets/ba_data/python/bascenev1/_messages.py index df2701bc..87eb544a 100644 --- a/assets/src/ba_data/python/ba/_messages.py +++ b/src/assets/ba_data/python/bascenev1/_messages.py @@ -8,11 +8,12 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, TypeVar from enum import Enum -import _ba +import _babase if TYPE_CHECKING: from typing import Sequence, Any - import ba + import babase + import bascenev1 class _UnhandledType: @@ -53,7 +54,7 @@ class DieMessage: Category: **Message Classes** - Most ba.Actor-s respond to this. + Most bascenev1.Actor-s respond to this. """ immediate: bool = False @@ -66,13 +67,11 @@ class DieMessage: """The particular reason for death.""" -# pylint: disable=invalid-name -PlayerType = TypeVar('PlayerType', bound='ba.Player') -# pylint: enable=invalid-name +PlayerT = TypeVar('PlayerT', bound='bascenev1.Player') class PlayerDiedMessage: - """A message saying a ba.Player has died. + """A message saying a bascenev1.Player has died. Category: **Message Classes** """ @@ -81,15 +80,15 @@ class PlayerDiedMessage: """If True, the player was killed; If False, they left the game or the round ended.""" - how: ba.DeathType + how: DeathType """The particular type of death.""" def __init__( self, - player: ba.Player, + player: bascenev1.Player, was_killed: bool, - killerplayer: ba.Player | None, - how: ba.DeathType, + killerplayer: bascenev1.Player | None, + how: DeathType, ): """Instantiate a message with the given values.""" @@ -103,18 +102,16 @@ class PlayerDiedMessage: self.killed = was_killed self.how = how - def getkillerplayer( - self, playertype: type[PlayerType] - ) -> PlayerType | None: - """Return the ba.Player responsible for the killing, if any. + def getkillerplayer(self, playertype: type[PlayerT]) -> PlayerT | None: + """Return the bascenev1.Player responsible for the killing, if any. Pass the Player type being used by the current game. """ assert isinstance(self._killerplayer, (playertype, type(None))) return self._killerplayer - def getplayer(self, playertype: type[PlayerType]) -> PlayerType: - """Return the ba.Player that died. + def getplayer(self, playertype: type[PlayerT]) -> PlayerT: + """Return the bascenev1.Player that died. The type of player for the current activity should be passed so that the type-checker properly identifies the returned value as one. @@ -151,8 +148,8 @@ class PickUpMessage: Category: **Message Classes** """ - node: ba.Node - """The ba.Node that is getting picked up.""" + node: bascenev1.Node + """The bascenev1.Node that is getting picked up.""" @dataclass @@ -170,8 +167,8 @@ class PickedUpMessage: Category: **Message Classes** """ - node: ba.Node - """The ba.Node doing the picking up.""" + node: bascenev1.Node + """The bascenev1.Node doing the picking up.""" @dataclass @@ -181,8 +178,8 @@ class DroppedMessage: Category: **Message Classes** """ - node: ba.Node - """The ba.Node doing the dropping.""" + node: bascenev1.Node + """The bascenev1.Node doing the dropping.""" @dataclass @@ -210,7 +207,7 @@ class FreezeMessage: Category: **Message Classes** - As seen in the effects of an ice ba.Bomb. + As seen in the effects of an ice bascenev1.Bomb. """ @@ -244,13 +241,13 @@ class HitMessage: def __init__( self, - srcnode: ba.Node | None = None, + srcnode: bascenev1.Node | None = None, pos: Sequence[float] | None = None, velocity: Sequence[float] | None = None, magnitude: float = 1.0, velocity_magnitude: float = 0.0, radius: float = 1.0, - source_player: ba.Player | None = None, + source_player: bascenev1.Player | None = None, kick_back: float = 1.0, flat_damage: float | None = None, hit_type: str = 'generic', @@ -260,8 +257,8 @@ class HitMessage: """Instantiate a message with given values.""" self.srcnode = srcnode - self.pos = pos if pos is not None else _ba.Vec3() - self.velocity = velocity if velocity is not None else _ba.Vec3() + self.pos = pos if pos is not None else _babase.Vec3() + self.velocity = velocity if velocity is not None else _babase.Vec3() self.magnitude = magnitude self.velocity_magnitude = velocity_magnitude self.radius = radius @@ -277,9 +274,7 @@ class HitMessage: force_direction if force_direction is not None else velocity ) - def get_source_player( - self, playertype: type[PlayerType] - ) -> PlayerType | None: + def get_source_player(self, playertype: type[PlayerT]) -> PlayerT | None: """Return the source-player if one exists and is the provided type.""" player: Any = self._source_player diff --git a/assets/src/ba_data/python/ba/_multiteamsession.py b/src/assets/ba_data/python/bascenev1/_multiteamsession.py similarity index 82% rename from assets/src/ba_data/python/ba/_multiteamsession.py rename to src/assets/ba_data/python/bascenev1/_multiteamsession.py index d653ff95..b27bad93 100644 --- a/assets/src/ba_data/python/ba/_multiteamsession.py +++ b/src/assets/ba_data/python/bascenev1/_multiteamsession.py @@ -7,25 +7,29 @@ import copy import random from typing import TYPE_CHECKING -import _ba -from ba._session import Session -from ba._error import NotFoundError, print_error +import _babase +import _bascenev1 +from babase._error import NotFoundError, print_error +from bascenev1._session import Session if TYPE_CHECKING: from typing import Any, Sequence - import ba + + import babase + import bascenev1 DEFAULT_TEAM_COLORS = ((0.1, 0.25, 1.0), (1.0, 0.25, 0.2)) DEFAULT_TEAM_NAMES = ('Blue', 'Red') class MultiTeamSession(Session): - """Common base class for ba.DualTeamSession and ba.FreeForAllSession. + """Common base for DualTeamSession and FreeForAllSession. Category: **Gameplay Classes** - Free-for-all-mode is essentially just teams-mode with each ba.Player having - their own ba.Team, so there is much overlap in functionality. + Free-for-all-mode is essentially just teams-mode with each + bascenev1.Player having their own bascenev1.Team, so there is much + overlap in functionality. """ # These should be overridden. @@ -34,12 +38,13 @@ class MultiTeamSession(Session): _playlists_var = 'UNSET Playlists' def __init__(self) -> None: - """Set up playlists and launches a ba.Activity to accept joiners.""" + """Set up playlists & launch a bascenev1.Activity to accept joiners.""" # pylint: disable=cyclic-import - from ba import _playlist + from bascenev1 import _playlist from bastd.activity.multiteamjoin import MultiTeamJoinActivity - app = _ba.app + app = _babase.app + assert app.classic is not None cfg = app.config if self.use_teams: @@ -50,7 +55,7 @@ class MultiTeamSession(Session): team_colors = None # print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.') - depsets: Sequence[ba.DependencySet] = [] + depsets: Sequence[bascenev1.DependencySet] = [] super().__init__( depsets, @@ -60,17 +65,19 @@ class MultiTeamSession(Session): max_players=self.get_max_players(), ) - self._series_length = app.teams_series_length - self._ffa_series_length = app.ffa_series_length + self._series_length = app.classic.teams_series_length + self._ffa_series_length = app.classic.ffa_series_length show_tutorial = cfg.get('Show Tutorial', True) - self._tutorial_activity_instance: ba.Activity | None + self._tutorial_activity_instance: bascenev1.Activity | None if show_tutorial: from bastd.tutorial import TutorialActivity # Get this loading. - self._tutorial_activity_instance = _ba.newactivity(TutorialActivity) + self._tutorial_activity_instance = _bascenev1.newactivity( + TutorialActivity + ) else: self._tutorial_activity_instance = None @@ -88,7 +95,6 @@ class MultiTeamSession(Session): self._playlist_name != '__default__' and self._playlist_name in playlists ): - # Make sure to copy this, as we muck with it in place once we've # got it and we don't want that to affect our config. playlist = copy.deepcopy(playlists[self._playlist_name]) @@ -116,7 +122,7 @@ class MultiTeamSession(Session): # Get a game on deck ready to go. self._current_game_spec: dict[str, Any] | None = None self._next_game_spec: dict[str, Any] = self._playlist.pull_next() - self._next_game: type[ba.GameActivity] = self._next_game_spec[ + self._next_game: type[bascenev1.GameActivity] = self._next_game_spec[ 'resolved_type' ] @@ -125,7 +131,7 @@ class MultiTeamSession(Session): self._instantiate_next_game() # Start in our custom join screen. - self.setactivity(_ba.newactivity(MultiTeamJoinActivity)) + self.setactivity(_bascenev1.newactivity(MultiTeamJoinActivity)) def get_ffa_series_length(self) -> int: """Return free-for-all series length.""" @@ -135,10 +141,10 @@ class MultiTeamSession(Session): """Return teams series length.""" return self._series_length - def get_next_game_description(self) -> ba.Lstr: + def get_next_game_description(self) -> babase.Lstr: """Returns a description of the next game on deck.""" # pylint: disable=cyclic-import - from ba._gameactivity import GameActivity + from bascenev1._gameactivity import GameActivity gametype: type[GameActivity] = self._next_game_spec['resolved_type'] assert issubclass(gametype, GameActivity) @@ -148,28 +154,30 @@ class MultiTeamSession(Session): """Returns which game in the series is currently being played.""" return self._game_number - def on_team_join(self, team: ba.SessionTeam) -> None: + def on_team_join(self, team: bascenev1.SessionTeam) -> None: team.customdata['previous_score'] = team.customdata['score'] = 0 def get_max_players(self) -> int: - """Return max number of ba.Player-s allowed to join the game at once""" + """Return max number of Players allowed to join the game at once.""" if self.use_teams: - return _ba.app.config.get('Team Game Max Players', 8) - return _ba.app.config.get('Free-for-All Max Players', 8) + return _babase.app.config.get('Team Game Max Players', 8) + return _babase.app.config.get('Free-for-All Max Players', 8) def _instantiate_next_game(self) -> None: - self._next_game_instance = _ba.newactivity( + self._next_game_instance = _bascenev1.newactivity( self._next_game_spec['resolved_type'], self._next_game_spec['settings'], ) - def on_activity_end(self, activity: ba.Activity, results: Any) -> None: + def on_activity_end( + self, activity: bascenev1.Activity, results: Any + ) -> None: # pylint: disable=cyclic-import from bastd.tutorial import TutorialActivity from bastd.activity.multiteamvictory import ( TeamSeriesVictoryScoreScreenActivity, ) - from ba._activitytypes import ( + from bascenev1._activitytypes import ( TransitionActivity, JoinActivity, ScoreScreenActivity, @@ -185,14 +193,13 @@ class MultiTeamSession(Session): # to transition us into a round gracefully (otherwise we'd snap from # one terrain to another instantly). elif isinstance(activity, TutorialActivity): - self.setactivity(_ba.newactivity(TransitionActivity)) + self.setactivity(_bascenev1.newactivity(TransitionActivity)) # If we're in a between-round activity or a restart-activity, hop # into a round. elif isinstance( activity, (JoinActivity, TransitionActivity, ScoreScreenActivity) ): - # If we're coming from a series-end activity, reset scores. if isinstance(activity, TeamSeriesVictoryScoreScreenActivity): self.stats.reset() @@ -239,8 +246,8 @@ class MultiTeamSession(Session): def announce_game_results( self, - activity: ba.GameActivity, - results: ba.GameResults, + activity: bascenev1.GameActivity, + results: bascenev1.GameResults, delay: float, announce_winning_team: bool = True, ) -> None: @@ -253,14 +260,13 @@ class MultiTeamSession(Session): """ # pylint: disable=cyclic-import # pylint: disable=too-many-locals - from ba._math import normalized_color - from ba._general import Call - from ba._gameutils import cameraflash - from ba._language import Lstr - from ba._freeforallsession import FreeForAllSession - from ba._messages import CelebrateMessage + from babase._math import normalized_color + from babase._language import Lstr + from bascenev1._gameutils import cameraflash + from bascenev1._freeforallsession import FreeForAllSession + from bascenev1._messages import CelebrateMessage - _ba.timer(delay, Call(_ba.playsound, _ba.getsound('boxingBell'))) + _bascenev1.timer(delay, _bascenev1.getsound('boxingBell').play) if announce_winning_team: winning_sessionteam = results.winning_sessionteam diff --git a/src/assets/ba_data/python/bascenev1/_music.py b/src/assets/ba_data/python/bascenev1/_music.py new file mode 100644 index 00000000..52e42165 --- /dev/null +++ b/src/assets/ba_data/python/bascenev1/_music.py @@ -0,0 +1,73 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Music related bits.""" + +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING + +import _bascenev1 + +if TYPE_CHECKING: + pass + + +class MusicType(Enum): + """Types of music available to play in-game. + + Category: **Enums** + + These do not correspond to specific pieces of music, but rather to + 'situations'. The actual music played for each type can be overridden + by the game or by the user. + """ + + MENU = 'Menu' + VICTORY = 'Victory' + CHAR_SELECT = 'CharSelect' + RUN_AWAY = 'RunAway' + ONSLAUGHT = 'Onslaught' + KEEP_AWAY = 'Keep Away' + RACE = 'Race' + EPIC_RACE = 'Epic Race' + SCORES = 'Scores' + GRAND_ROMP = 'GrandRomp' + TO_THE_DEATH = 'ToTheDeath' + CHOSEN_ONE = 'Chosen One' + FORWARD_MARCH = 'ForwardMarch' + FLAG_CATCHER = 'FlagCatcher' + SURVIVAL = 'Survival' + EPIC = 'Epic' + SPORTS = 'Sports' + HOCKEY = 'Hockey' + FOOTBALL = 'Football' + FLYING = 'Flying' + SCARY = 'Scary' + MARCHING = 'Marching' + + +def setmusic(musictype: MusicType | None, continuous: bool = False) -> None: + """Set the app to play (or stop playing) a certain type of music. + + category: **Gameplay Functions** + + This function will handle loading and playing sound assets as necessary, + and also supports custom user soundtracks on specific platforms so the + user can override particular game music with their own. + + Pass None to stop music. + + if 'continuous' is True and musictype is the same as what is already + playing, the playing track will not be restarted. + """ + + # All we do here now is set a few music attrs on the current globals + # node. The foreground globals' current playing music then gets fed to + # the do_play_music call in our music controller. This way we can + # seamlessly support custom soundtracks in replays/etc since we're being + # driven purely by node data. + gnode = _bascenev1.getactivity().globalsnode + gnode.music_continuous = continuous + gnode.music = '' if musictype is None else musictype.value + gnode.music_count += 1 diff --git a/assets/src/ba_data/python/ba/_nodeactor.py b/src/assets/ba_data/python/bascenev1/_nodeactor.py similarity index 77% rename from assets/src/ba_data/python/ba/_nodeactor.py rename to src/assets/ba_data/python/bascenev1/_nodeactor.py index e9e78a23..2e9a1483 100644 --- a/assets/src/ba_data/python/ba/_nodeactor.py +++ b/src/assets/ba_data/python/bascenev1/_nodeactor.py @@ -6,16 +6,17 @@ from __future__ import annotations from typing import TYPE_CHECKING -from ba._messages import DieMessage -from ba._actor import Actor +from bascenev1._messages import DieMessage +from bascenev1._actor import Actor if TYPE_CHECKING: - import ba from typing import Any + import bascenev1 + class NodeActor(Actor): - """A simple ba.Actor type that wraps a single ba.Node. + """A simple bascenev1.Actor type that wraps a single bascenev1.Node. Category: **Gameplay Classes** @@ -23,7 +24,7 @@ class NodeActor(Actor): exists() call will return whether the Node still exists or not. """ - def __init__(self, node: ba.Node): + def __init__(self, node: bascenev1.Node): super().__init__() self.node = node diff --git a/assets/src/ba_data/python/ba/_player.py b/src/assets/ba_data/python/bascenev1/_player.py similarity index 72% rename from assets/src/ba_data/python/ba/_player.py rename to src/assets/ba_data/python/bascenev1/_player.py index 32d17206..c1192743 100644 --- a/assets/src/ba_data/python/ba/_player.py +++ b/src/assets/ba_data/python/bascenev1/_player.py @@ -7,22 +7,23 @@ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, TypeVar, Generic, cast -import _ba -from ba._error import ( +import _babase +from babase._error import ( SessionPlayerNotFoundError, print_exception, ActorNotFoundError, ) -from ba._messages import DeathType, DieMessage +import _bascenev1 +from bascenev1._messages import DeathType, DieMessage if TYPE_CHECKING: from typing import Sequence, Any, Callable - import ba -# pylint: disable=invalid-name -PlayerType = TypeVar('PlayerType', bound='ba.Player') -TeamType = TypeVar('TeamType', bound='ba.Team') -# pylint: enable=invalid-name + import babase + import bascenev1 + +PlayerT = TypeVar('PlayerT', bound='bascenev1.Player') +TeamT = TypeVar('TeamT', bound='bascenev1.Team') @dataclass @@ -43,33 +44,33 @@ class StandLocation: Category: Gameplay Classes """ - position: ba.Vec3 + position: babase.Vec3 angle: float | None = None -class Player(Generic[TeamType]): - """A player in a specific ba.Activity. +class Player(Generic[TeamT]): + """A player in a specific bascenev1.Activity. Category: Gameplay Classes - These correspond to ba.SessionPlayer objects, but are associated with a - single ba.Activity instance. This allows activities to specify their - own custom ba.Player types. + These correspond to bascenev1.SessionPlayer objects, but are associated + with a single bascenev1.Activity instance. This allows activities to + specify their own custom bascenev1.Player types. """ # These are instance attrs but we define them at the type level so # their type annotations are introspectable (for docs generation). character: str - actor: ba.Actor | None - """The ba.Actor associated with the player.""" + actor: bascenev1.Actor | None + """The bascenev1.Actor associated with the player.""" color: Sequence[float] highlight: Sequence[float] - _team: TeamType - _sessionplayer: ba.SessionPlayer - _nodeactor: ba.NodeActor | None + _team: TeamT + _sessionplayer: bascenev1.SessionPlayer + _nodeactor: bascenev1.NodeActor | None _expired: bool _postinited: bool _customdata: dict @@ -79,12 +80,12 @@ class Player(Generic[TeamType]): # This also lets us keep trivial player classes cleaner by skipping # the super().__init__() line. - def postinit(self, sessionplayer: ba.SessionPlayer) -> None: + def postinit(self, sessionplayer: bascenev1.SessionPlayer) -> None: """Wire up a newly created player. (internal) """ - from ba._nodeactor import NodeActor + from bascenev1._nodeactor import NodeActor # Sanity check; if a dataclass is created that inherits from us, # it will define an equality operator by default which will break @@ -100,17 +101,19 @@ class Player(Generic[TeamType]): self.actor = None self.character = '' - self._nodeactor: ba.NodeActor | None = None + self._nodeactor: bascenev1.NodeActor | None = None self._sessionplayer = sessionplayer self.character = sessionplayer.character self.color = sessionplayer.color self.highlight = sessionplayer.highlight - self._team = cast(TeamType, sessionplayer.sessionteam.activityteam) + self._team = cast(TeamT, sessionplayer.sessionteam.activityteam) assert self._team is not None self._customdata = {} self._expired = False self._postinited = True - node = _ba.newnode('player', attrs={'playerID': sessionplayer.id}) + node = _bascenev1.newnode( + 'player', attrs={'playerID': sessionplayer.id} + ) self._nodeactor = NodeActor(node) sessionplayer.setnode(node) @@ -161,8 +164,8 @@ class Player(Generic[TeamType]): """ @property - def team(self) -> TeamType: - """The ba.Team for this player.""" + def team(self) -> TeamT: + """The bascenev1.Team for this player.""" assert self._postinited assert not self._expired return self._team @@ -171,7 +174,7 @@ class Player(Generic[TeamType]): def customdata(self) -> dict: """Arbitrary values associated with the player. Though it is encouraged that most player values be properly defined - on the ba.Player subclass, it may be useful for player-agnostic + on the bascenev1.Player subclass, it may be useful for player-agnostic objects to store values here. This dict is cleared when the player leaves or expires so objects stored here will be disposed of at the expected time, unlike the Player instance itself which may @@ -182,10 +185,10 @@ class Player(Generic[TeamType]): return self._customdata @property - def sessionplayer(self) -> ba.SessionPlayer: - """Return the ba.SessionPlayer corresponding to this Player. + def sessionplayer(self) -> bascenev1.SessionPlayer: + """Return the bascenev1.SessionPlayer corresponding to this Player. - Throws a ba.SessionPlayerNotFoundError if it does not exist. + Throws a bascenev1.SessionPlayerNotFoundError if it does not exist. """ assert self._postinited if bool(self._sessionplayer): @@ -193,8 +196,8 @@ class Player(Generic[TeamType]): raise SessionPlayerNotFoundError() @property - def node(self) -> ba.Node: - """A ba.Node of type 'player' associated with this Player. + def node(self) -> bascenev1.Node: + """A bascenev1.Node of type 'player' associated with this Player. This node can be used to get a generic player position/etc. """ @@ -204,23 +207,24 @@ class Player(Generic[TeamType]): return self._nodeactor.node @property - def position(self) -> ba.Vec3: - """The position of the player, as defined by its current ba.Actor. + def position(self) -> babase.Vec3: + """The position of the player, as defined by its bascenev1.Actor. - If the player currently has no actor, raises a ba.ActorNotFoundError. + If the player currently has no actor, raises a + babase.ActorNotFoundError. """ assert self._postinited assert not self._expired if self.actor is None: raise ActorNotFoundError - return _ba.Vec3(self.node.position) + return _babase.Vec3(self.node.position) def exists(self) -> bool: """Whether the underlying player still exists. - This will return False if the underlying ba.SessionPlayer has - left the game or if the ba.Activity this player was associated - with has ended. + This will return False if the underlying bascenev1.SessionPlayer has + left the game or if the bascenev1.Activity this player was + associated with has ended. Most functionality will fail on a nonexistent player. Note that you can also use the boolean operator for this same functionality, so a statement such as "if player" will do @@ -240,7 +244,7 @@ class Player(Generic[TeamType]): def is_alive(self) -> bool: """ - Returns True if the player has a ba.Actor assigned and its + Returns True if the player has a bascenev1.Actor assigned and its is_alive() method return True. False is returned otherwise. """ assert self._postinited @@ -256,7 +260,9 @@ class Player(Generic[TeamType]): return self._sessionplayer.get_icon() def assigninput( - self, inputtype: ba.InputType | tuple[ba.InputType, ...], call: Callable + self, + inputtype: babase.InputType | tuple[babase.InputType, ...], + call: Callable, ) -> None: """ Set the python callable to be run for one or more types of input. @@ -277,16 +283,17 @@ class Player(Generic[TeamType]): return self.exists() -class EmptyPlayer(Player['ba.EmptyTeam']): +class EmptyPlayer(Player['bascenev1.EmptyTeam']): """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 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. + bascenev1.Player and bascenev1.Team are 'Generic' types, and so passing + those top level classes as type arguments when defining a + bascenev1.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 bascenev1.Activity that does not need custom types of its own. Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, so if you want to define your own class for one of them you should do so @@ -300,13 +307,13 @@ class EmptyPlayer(Player['ba.EmptyTeam']): # instead of requiring extra work by them. -def playercast(totype: type[PlayerType], player: ba.Player) -> PlayerType: - """Cast a ba.Player to a specific ba.Player subclass. +def playercast(totype: type[PlayerT], player: bascenev1.Player) -> PlayerT: + """Cast a bascenev1.Player to a specific bascenev1.Player subclass. Category: Gameplay Functions When writing type-checked code, sometimes code will deal with raw - ba.Player objects which need to be cast back to the game's actual + bascenev1.Player objects which need to be cast back to a game's actual player type so that access can be properly type-checked. This function is a safe way to do so. It ensures that Optional values are not cast into Non-Optional, etc. @@ -319,9 +326,9 @@ def playercast(totype: type[PlayerType], player: ba.Player) -> PlayerType: # for the optional variety, but that currently seems to not be working. # See: https://github.com/python/mypy/issues/8800 def playercast_o( - totype: type[PlayerType], player: ba.Player | None -) -> PlayerType | None: - """A variant of ba.playercast() for use with optional ba.Player values. + totype: type[PlayerT], player: bascenev1.Player | None +) -> PlayerT | None: + """A variant of bascenev1.playercast() for use with optional Player values. Category: Gameplay Functions """ diff --git a/assets/src/ba_data/python/ba/_playlist.py b/src/assets/ba_data/python/bascenev1/_playlist.py similarity index 96% rename from assets/src/ba_data/python/ba/_playlist.py rename to src/assets/ba_data/python/bascenev1/_playlist.py index f489f6f0..cf0b90f8 100644 --- a/assets/src/ba_data/python/ba/_playlist.py +++ b/src/assets/ba_data/python/bascenev1/_playlist.py @@ -8,18 +8,18 @@ import copy import logging from typing import Any, TYPE_CHECKING -import _ba +import _babase if TYPE_CHECKING: from typing import Sequence - from ba import _session + from bascenev1._session import Session PlaylistType = list[dict[str, Any]] def filter_playlist( playlist: PlaylistType, - sessiontype: type[_session.Session], + sessiontype: type[Session], add_resolved_type: bool = False, remove_unowned: bool = True, mark_unowned: bool = False, @@ -34,18 +34,19 @@ def filter_playlist( # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements - from ba._map import get_filtered_map_name - from ba._error import MapNotFoundError - from ba._store import get_unowned_maps, get_unowned_game_types - from ba._general import getclass - from ba._gameactivity import GameActivity + from bascenev1._map import get_filtered_map_name + from babase._error import MapNotFoundError + from babase._general import getclass + from bascenev1._gameactivity import GameActivity + + assert _babase.app.classic is not None goodlist: list[dict] = [] unowned_maps: Sequence[str] - available_maps: list[str] = list(_ba.app.maps.keys()) - if remove_unowned or mark_unowned: - unowned_maps = get_unowned_maps() - unowned_game_types = get_unowned_game_types() + available_maps: list[str] = list(_babase.app.classic.maps.keys()) + if (remove_unowned or mark_unowned) and _babase.app.classic is not None: + unowned_maps = _babase.app.classic.store.get_unowned_maps() + unowned_game_types = _babase.app.classic.store.get_unowned_game_types() else: unowned_maps = [] unowned_game_types = set() diff --git a/assets/src/ba_data/python/ba/_powerup.py b/src/assets/ba_data/python/bascenev1/_powerup.py similarity index 62% rename from assets/src/ba_data/python/ba/_powerup.py rename to src/assets/ba_data/python/bascenev1/_powerup.py index 53d662af..5904bed3 100644 --- a/assets/src/ba_data/python/ba/_powerup.py +++ b/src/assets/ba_data/python/bascenev1/_powerup.py @@ -9,7 +9,8 @@ from dataclasses import dataclass if TYPE_CHECKING: from typing import Sequence - import ba + import babase + import bascenev1 @dataclass @@ -18,27 +19,28 @@ class PowerupMessage: Category: **Message Classes** - This message is normally received by touching a ba.PowerupBox. + This message is normally received by touching a bascenev1.PowerupBox. """ poweruptype: str """The type of powerup to be granted (a string). - See ba.Powerup.poweruptype for available type values.""" + See bascenev1.Powerup.poweruptype for available type values.""" - sourcenode: ba.Node | None = None + sourcenode: bascenev1.Node | None = None """The node the powerup game from, or None otherwise. - If a powerup is accepted, a ba.PowerupAcceptMessage should be sent - back to the sourcenode to inform it of the fact. This will generally - cause the powerup box to make a sound and disappear or whatnot.""" + If a powerup is accepted, a bascenev1.PowerupAcceptMessage should be + sent back to the sourcenode to inform it of the fact. This will + generally cause the powerup box to make a sound and disappear or + whatnot.""" @dataclass class PowerupAcceptMessage: - """A message informing a ba.Powerup that it was accepted. + """A message informing a bascenev1.Powerup that it was accepted. Category: **Message Classes** - This is generally sent in response to a ba.PowerupMessage + This is generally sent in response to a bascenev1.PowerupMessage to inform the box (or whoever granted it) that it can go away. """ diff --git a/assets/src/ba_data/python/ba/_score.py b/src/assets/ba_data/python/bascenev1/_score.py similarity index 93% rename from assets/src/ba_data/python/ba/_score.py rename to src/assets/ba_data/python/bascenev1/_score.py index b371e698..0be57581 100644 --- a/assets/src/ba_data/python/ba/_score.py +++ b/src/assets/ba_data/python/bascenev1/_score.py @@ -9,7 +9,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING if TYPE_CHECKING: - import ba + import bascenev1 @unique @@ -34,7 +34,7 @@ class ScoreConfig: label: str = 'Score' """A label show to the user for scores; 'Score', 'Time Survived', etc.""" - scoretype: ba.ScoreType = ScoreType.POINTS + scoretype: bascenev1.ScoreType = ScoreType.POINTS """How the score value should be displayed.""" lower_is_better: bool = False diff --git a/assets/src/ba_data/python/ba/_session.py b/src/assets/ba_data/python/bascenev1/_session.py similarity index 78% rename from assets/src/ba_data/python/ba/_session.py rename to src/assets/ba_data/python/bascenev1/_session.py index 11f653cb..652ddace 100644 --- a/assets/src/ba_data/python/ba/_session.py +++ b/src/assets/ba_data/python/bascenev1/_session.py @@ -6,26 +6,30 @@ from __future__ import annotations import weakref from typing import TYPE_CHECKING -import _ba -from ba._error import print_error, print_exception, NodeNotFoundError -from ba._language import Lstr -from ba._player import Player +import _babase +from babase._error import print_error, print_exception, NodeNotFoundError +from babase._language import Lstr +import _bascenev1 +from bascenev1._player import Player if TYPE_CHECKING: from typing import Sequence, Any - import ba + + import babase + import baclassic + import bascenev1 class Session: - """Defines a high level series of ba.Activity-es with a common purpose. + """Defines a high level series of bascenev1.Activity-es. Category: **Gameplay Classes** - Examples of sessions are ba.FreeForAllSession, ba.DualTeamSession, and - ba.CoopSession. + Examples of sessions are bascenev1.FreeForAllSession, + bascenv1.DualTeamSession, and bascenev1.CoopSession. A Session is responsible for wrangling and transitioning between various - ba.Activity instances such as mini-games and score-screens, and for + bascenev1.Activity instances such as mini-games and score-screens, and for maintaining state between them (players, teams, score tallies, etc). """ @@ -43,9 +47,9 @@ class Session: # at the class level so that looks better and nobody get lost while # reading large __init__ - lobby: ba.Lobby - """The ba.Lobby instance where new ba.Player-s go to select a - Profile/Team/etc. before being added to games. + lobby: baclassic.Lobby + """The baclassic.Lobby instance where new bascenev1.Player-s go to select + a Profile/Team/etc. before being added to games. Be aware this value may be None if a Session does not allow any such selection.""" @@ -56,23 +60,23 @@ class Session: """The minimum number of players who must be present for the Session to proceed past the initial joining screen""" - sessionplayers: list[ba.SessionPlayer] - """All ba.SessionPlayers in the Session. Most things should use the - list of ba.Player-s in ba.Activity; not this. Some players, such as - those who have not yet selected a character, will only be - found on this list.""" + sessionplayers: list[bascenev1.SessionPlayer] + """All bascenev1.SessionPlayers in the Session. Most things should use + the list of bascenev1.Player-s in bascenev1.Activity; not this. Some + players, such as those who have not yet selected a character, will + only be found on this list.""" customdata: dict """A shared dictionary for objects to use as storage on this session. Ensure that keys here are unique to avoid collisions.""" - sessionteams: list[ba.SessionTeam] - """All the ba.SessionTeams in the Session. Most things should use the - list of ba.Team-s in ba.Activity; not this.""" + sessionteams: list[bascenev1.SessionTeam] + """All the bascenev1.SessionTeams in the Session. Most things should + use the list of bascenev1.Team-s in bascenev1.Activity; not this.""" def __init__( self, - depsets: Sequence[ba.DependencySet], + depsets: Sequence[bascenev1.DependencySet], team_names: Sequence[str] | None = None, team_colors: Sequence[Sequence[float]] | None = None, min_players: int = 1, @@ -80,21 +84,25 @@ class Session: ): """Instantiate a session. - depsets should be a sequence of successfully resolved ba.DependencySet - instances; one for each ba.Activity the session may potentially run. + depsets should be a sequence of successfully resolved + bascenev1.DependencySet instances; one for each bascenev1.Activity + the session may potentially run. """ # pylint: disable=too-many-statements # pylint: disable=too-many-locals # pylint: disable=cyclic-import # pylint: disable=too-many-branches - from ba._lobby import Lobby - from ba._stats import Stats - from ba._gameactivity import GameActivity - from ba._activity import Activity - from ba._team import SessionTeam - from ba._error import DependencyError - from ba._dependency import Dependency, AssetPackage from efro.util import empty_weakref + from baclassic._lobby import Lobby + from bascenev1._dependency import ( + Dependency, + AssetPackage, + DependencyError, + ) + from bascenev1._stats import Stats + from bascenev1._gameactivity import GameActivity + from bascenev1._activity import Activity + from bascenev1._team import SessionTeam # First off, resolve all dependency-sets we were passed. # If things are missing, we'll try to gather them into a single @@ -135,7 +143,7 @@ class Session: # required_asset_packages) # Init our C++ layer data. - self._sessiondata = _ba.register_session(self) + self._sessiondata = _bascenev1.register_session(self) # Should remove this if possible. self.tournament_id: str | None = None @@ -148,16 +156,16 @@ class Session: self.customdata = {} self._in_set_activity = False self._next_team_id = 0 - self._activity_retained: ba.Activity | None = None + self._activity_retained: bascenev1.Activity | None = None self._launch_end_session_activity_time: float | None = None - self._activity_end_timer: ba.Timer | None = None + self._activity_end_timer: bascenev1.BaseTimer | None = None self._activity_weak = empty_weakref(Activity) - self._next_activity: ba.Activity | None = None + self._next_activity: bascenev1.Activity | None = None self._wants_to_end = False self._ending = False self._activity_should_end_immediately = False self._activity_should_end_immediately_results: ( - ba.GameResults | None + bascenev1.GameResults | None ) = None self._activity_should_end_immediately_delay = 0.0 @@ -186,7 +194,7 @@ class Session: self.sessionteams.append(team) self._next_team_id += 1 try: - with _ba.Context(self): + with self.context: self.on_team_join(team) except Exception: print_exception(f'Error in on_team_join for {self}.') @@ -195,17 +203,24 @@ class Session: self.stats = Stats() # Instantiate our session globals node which will apply its settings. - self._sessionglobalsnode = _ba.newnode('sessionglobals') + self._sessionglobalsnode = _bascenev1.newnode('sessionglobals') @property - def sessionglobalsnode(self) -> ba.Node: - """The sessionglobals ba.Node for the session.""" + def context(self) -> bascenev1.ContextRef: + """A context-ref pointing at this activity.""" + return self._sessiondata.context() + + @property + def sessionglobalsnode(self) -> bascenev1.Node: + """The sessionglobals bascenev1.Node for the session.""" node = self._sessionglobalsnode if not node: raise NodeNotFoundError() return node - def should_allow_mid_activity_joins(self, activity: ba.Activity) -> bool: + def should_allow_mid_activity_joins( + self, activity: bascenev1.Activity + ) -> bool: """Ask ourself if we should allow joins during an Activity. Note that for a join to be allowed, both the Session and Activity @@ -215,20 +230,21 @@ class Session: del activity # Unused. return True - def on_player_request(self, player: ba.SessionPlayer) -> bool: - """Called when a new ba.Player wants to join the Session. + def on_player_request(self, player: bascenev1.SessionPlayer) -> bool: + """Called when a new bascenev1.Player wants to join the Session. This should return True or False to accept/reject. """ - # Limit player counts *unless* we're in a stress test. - if _ba.app.stress_test_reset_timer is None: - + if ( + _babase.app.classic is not None + and _babase.app.classic.stress_test_reset_timer is None + ): if len(self.sessionplayers) >= self.max_players: # Print a rejection message *only* to the client trying to # join (prevents spamming everyone else in the game). - _ba.playsound(_ba.getsound('error')) - _ba.screenmessage( + _bascenev1.getsound('error').play() + _bascenev1.screenmessage( Lstr( resource='playerLimitReachedText', subs=[('${COUNT}', str(self.max_players))], @@ -239,11 +255,11 @@ class Session: ) return False - _ba.playsound(_ba.getsound('dripity')) + _bascenev1.getsound('dripity').play() return True - def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None: - """Called when a previously-accepted ba.SessionPlayer leaves.""" + def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None: + """Called when a previously-accepted bascenev1.SessionPlayer leaves.""" if sessionplayer not in self.sessionplayers: print( @@ -252,14 +268,13 @@ class Session: ) return - _ba.playsound(_ba.getsound('playerLeft')) + _bascenev1.getsound('playerLeft').play() activity = self._activity_weak() if not sessionplayer.in_game: - # Ok, the player is still in the lobby; simply remove them. - with _ba.Context(self): + with self.context: try: self.lobby.remove_chooser(sessionplayer) except Exception: @@ -270,7 +285,7 @@ class Session: sessionteam = sessionplayer.sessionteam assert sessionteam is not None - _ba.screenmessage( + _babase.screenmessage( Lstr( resource='playerLeftText', subs=[('${PLAYER}', sessionplayer.getname(full=True))], @@ -305,7 +320,9 @@ class Session: self.sessionplayers.remove(sessionplayer) def _remove_player_team( - self, sessionteam: ba.SessionTeam, activity: ba.Activity | None + self, + sessionteam: bascenev1.SessionTeam, + activity: bascenev1.Activity | None, ) -> None: """Remove the player-specific team in non-teams mode.""" @@ -320,7 +337,7 @@ class Session: print('Team not found in Activity in on_player_leave.') # And then from the Session. - with _ba.Context(self): + with self.context: if sessionteam in self.sessionteams: try: self.sessionteams.remove(sessionteam) @@ -351,11 +368,10 @@ class Session: def _launch_end_session_activity(self) -> None: """(internal)""" - from ba._activitytypes import EndSessionActivity - from ba._generated.enums import TimeType + from bascenev1._activitytypes import EndSessionActivity - with _ba.Context(self): - curtime = _ba.time(TimeType.REAL) + with self.context: + curtime = _babase.apptime() if self._ending: # Ignore repeats unless its been a while. assert self._launch_end_session_activity_time is not None @@ -368,28 +384,32 @@ class Session: + ')' ) self._launch_end_session_activity_time = curtime - self.setactivity(_ba.newactivity(EndSessionActivity)) + self.setactivity(_bascenev1.newactivity(EndSessionActivity)) self._wants_to_end = False self._ending = True # Prevent further actions. - def on_team_join(self, team: ba.SessionTeam) -> None: - """Called when a new ba.Team joins the session.""" + def on_team_join(self, team: bascenev1.SessionTeam) -> None: + """Called when a new bascenev1.Team joins the session.""" - def on_team_leave(self, team: ba.SessionTeam) -> None: - """Called when a ba.Team is leaving the session.""" + def on_team_leave(self, team: bascenev1.SessionTeam) -> None: + """Called when a bascenev1.Team is leaving the session.""" def end_activity( - self, activity: ba.Activity, results: Any, delay: float, force: bool + self, + activity: bascenev1.Activity, + results: Any, + delay: float, + force: bool, ) -> None: - """Commence shutdown of a ba.Activity (if not already occurring). + """Commence shutdown of a bascenev1.Activity (if not already occurring). 'delay' is the time delay before the Activity actually ends (in seconds). Further calls to end() will be ignored up until this time, unless 'force' is True, in which case the new results will replace the old. """ - from ba._general import Call - from ba._generated.enums import TimeType + from babase._general import Call + from babase._mgen.enums import TimeType # Only pay attention if this is coming from our current activity. if activity is not self._activity_retained: @@ -410,16 +430,14 @@ class Session: activity.set_has_ended(True) # Set a timer to set in motion this activity's demise. - self._activity_end_timer = _ba.Timer( - delay, - Call(self._complete_end_activity, activity, results), - timetype=TimeType.BASE, + self._activity_end_timer = _bascenev1.BaseTimer( + delay, Call(self._complete_end_activity, activity, results) ) def handlemessage(self, msg: Any) -> Any: """General message handling; can be passed any message object.""" - from ba._lobby import PlayerReadyMessage - from ba._messages import PlayerProfilesChangedMessage, UNHANDLED + from baclassic._lobby import PlayerReadyMessage + from bascenev1._messages import PlayerProfilesChangedMessage, UNHANDLED if isinstance(msg, PlayerReadyMessage): self._on_player_ready(msg.chooser) @@ -427,7 +445,7 @@ class Session: elif isinstance(msg, PlayerProfilesChangedMessage): # If we have a current activity with a lobby, ask it to reload # profiles. - with _ba.Context(self): + with self.context: self.lobby.reload_profiles() return None @@ -436,7 +454,7 @@ class Session: return None class _SetActivityScopedLock: - def __init__(self, session: ba.Session) -> None: + def __init__(self, session: Session) -> None: self._session = session if session._in_set_activity: raise RuntimeError('Session.setactivity() called recursively.') @@ -445,20 +463,20 @@ class Session: def __del__(self) -> None: self._session._in_set_activity = False - def setactivity(self, activity: ba.Activity) -> None: - """Assign a new current ba.Activity for the session. + def setactivity(self, activity: bascenev1.Activity) -> None: + """Assign a new current bascenev1.Activity for the session. Note that this will not change the current context to the new Activity's. Code must be run in the new activity's methods (on_transition_in, etc) to get it. (so you can't do - session.setactivity(foo) and then ba.newnode() to add a node to foo) + session.setactivity(foo) and then bascenev1.newnode() to add a node + to foo) """ - from ba._generated.enums import TimeType # Make sure we don't get called recursively. _rlock = self._SetActivityScopedLock(self) - if activity.session is not _ba.getsession(): + if activity.session is not _bascenev1.getsession(): raise RuntimeError("Provided Activity's Session is not current.") # Quietly ignore this if the whole session is going down. @@ -507,15 +525,13 @@ class Session: # the activity should have no refs left to it and should die (which # will trigger the next activity to run). if prev_activity is not None: - with _ba.Context('ui'): - _ba.timer( - max(0.0, activity.transition_time), - prev_activity.expire, - timetype=TimeType.REAL, + with _babase.ContextRef.empty(): + _babase.apptimer( + max(0.0, activity.transition_time), prev_activity.expire ) self._in_set_activity = False - def getactivity(self) -> ba.Activity | None: + def getactivity(self) -> bascenev1.Activity | None: """Return the current foreground activity for this session.""" return self._activity_weak() @@ -530,11 +546,11 @@ class Session: return [] def _complete_end_activity( - self, activity: ba.Activity, results: Any + self, activity: bascenev1.Activity, results: Any ) -> None: # Run the subclass callback in the session context. try: - with _ba.Context(self): + with self.context: self.on_activity_end(activity, results) except Exception: print_exception( @@ -542,16 +558,16 @@ class Session: f' activity {activity} with results {results}' ) - def _request_player(self, sessionplayer: ba.SessionPlayer) -> bool: + def _request_player(self, sessionplayer: bascenev1.SessionPlayer) -> bool: """Called by the native layer when a player wants to join.""" # If we're ending, allow no new players. if self._ending: return False - # Ask the ba.Session subclass to approve/deny this request. + # Ask the bascenev1.Session subclass to approve/deny this request. try: - with _ba.Context(self): + with self.context: result = self.on_player_request(sessionplayer) except Exception: print_exception(f'Error in on_player_request for {self}') @@ -560,7 +576,7 @@ class Session: # If they said yes, add the player to the lobby. if result: self.sessionplayers.append(sessionplayer) - with _ba.Context(self): + with self.context: try: self.lobby.add_chooser(sessionplayer) except Exception: @@ -568,11 +584,13 @@ class Session: return result - def on_activity_end(self, activity: ba.Activity, results: Any) -> None: - """Called when the current ba.Activity has ended. + def on_activity_end( + self, activity: bascenev1.Activity, results: Any + ) -> None: + """Called when the current bascenev1.Activity has ended. - The ba.Session should look at the results and start - another ba.Activity. + The bascenev1.Session should look at the results and start + another bascenev1.Activity. """ def begin_next_activity(self) -> None: @@ -611,8 +629,8 @@ class Session: self._activity_should_end_immediately_delay, ) - def _on_player_ready(self, chooser: ba.Chooser) -> None: - """Called when a ba.Player has checked themself ready.""" + def _on_player_ready(self, chooser: baclassic.Chooser) -> None: + """Called when a bascenev1.Player has checked themself ready.""" lobby = chooser.lobby activity = self._activity_weak() @@ -638,14 +656,14 @@ class Session: # Get our next activity going. self._complete_end_activity(activity, {}) else: - _ba.screenmessage( + _babase.screenmessage( Lstr( resource='notEnoughPlayersText', subs=[('${COUNT}', str(min_players))], ), color=(1, 1, 0), ) - _ba.playsound(_ba.getsound('error')) + _bascenev1.getsound('error').play() # Otherwise just add players on the fly. else: @@ -657,7 +675,7 @@ class Session: ) -> None: """(internal)""" # pylint: disable=cyclic-import - from ba._apputils import garbage_collect + from babase._apputils import garbage_collect # Since things should be generally still right now, it's a good time # to run garbage collection to clear out any circular dependency @@ -665,14 +683,17 @@ class Session: # hitches. garbage_collect() - with _ba.Context(self): + assert _babase.app.classic is not None + with self.context: if can_show_ad_on_death: - _ba.app.ads.call_after_ad(self.begin_next_activity) + _babase.app.classic.ads.call_after_ad(self.begin_next_activity) else: - _ba.pushcall(self.begin_next_activity) + _babase.pushcall(self.begin_next_activity) - def _add_chosen_player(self, chooser: ba.Chooser) -> ba.SessionPlayer: - from ba._team import SessionTeam + def _add_chosen_player( + self, chooser: baclassic.Chooser + ) -> bascenev1.SessionPlayer: + from bascenev1._team import SessionTeam sessionplayer = chooser.getplayer() assert sessionplayer in self.sessionplayers, ( @@ -701,8 +722,8 @@ class Session: and self.should_allow_mid_activity_joins(activity) ): pass_to_activity = False - with _ba.Context(self): - _ba.screenmessage( + with self.context: + _babase.screenmessage( Lstr( resource='playerDelayedJoinText', subs=[ @@ -728,7 +749,7 @@ class Session: # Add player's team to the Session. self.sessionteams.append(sessionteam) - with _ba.Context(self): + with self.context: try: self.on_team_join(sessionteam) except Exception: diff --git a/assets/src/ba_data/python/ba/_settings.py b/src/assets/ba_data/python/bascenev1/_settings.py similarity index 100% rename from assets/src/ba_data/python/ba/_settings.py rename to src/assets/ba_data/python/bascenev1/_settings.py diff --git a/assets/src/ba_data/python/ba/_stats.py b/src/assets/ba_data/python/bascenev1/_stats.py similarity index 81% rename from assets/src/ba_data/python/ba/_stats.py rename to src/assets/ba_data/python/bascenev1/_stats.py index 0f68beb7..f48939a4 100644 --- a/assets/src/ba_data/python/ba/_stats.py +++ b/src/assets/ba_data/python/bascenev1/_stats.py @@ -8,8 +8,9 @@ import weakref from typing import TYPE_CHECKING from dataclasses import dataclass -import _ba -from ba._error import ( +import _babase +import _bascenev1 +from babase._error import ( print_exception, print_error, SessionTeamNotFoundError, @@ -17,14 +18,17 @@ from ba._error import ( NotFoundError, ) + if TYPE_CHECKING: - import ba from typing import Any, Sequence + import babase + import bascenev1 + @dataclass class PlayerScoredMessage: - """Informs something that a ba.Player scored. + """Informs something that a bascenev1.Player scored. Category: **Message Classes** """ @@ -34,11 +38,11 @@ class PlayerScoredMessage: class PlayerRecord: - """Stats for an individual player in a ba.Stats object. + """Stats for an individual player in a bascenev1.Stats object. Category: **Gameplay Classes** - This does not necessarily correspond to a ba.Player that is + This does not necessarily correspond to a bascenev1.Player that is still present (stats may be retained for players that leave mid-game) """ @@ -49,8 +53,8 @@ class PlayerRecord: self, name: str, name_full: str, - sessionplayer: ba.SessionPlayer, - stats: ba.Stats, + sessionplayer: bascenev1.SessionPlayer, + stats: bascenev1.Stats, ): self.name = name self.name_full = name_full @@ -60,21 +64,22 @@ class PlayerRecord: self.accum_kill_count = 0 self.killed_count = 0 self.accum_killed_count = 0 - self._multi_kill_timer: ba.Timer | None = None + self._multi_kill_timer: bascenev1.Timer | None = None self._multi_kill_count = 0 self._stats = weakref.ref(stats) - self._last_sessionplayer: ba.SessionPlayer | None = None - self._sessionplayer: ba.SessionPlayer | None = None - self._sessionteam: weakref.ref[ba.SessionTeam] | None = None + self._last_sessionplayer: bascenev1.SessionPlayer | None = None + self._sessionplayer: bascenev1.SessionPlayer | None = None + self._sessionteam: weakref.ref[bascenev1.SessionTeam] | None = None self.streak = 0 self.associate_with_sessionplayer(sessionplayer) @property - def team(self) -> ba.SessionTeam: - """The ba.SessionTeam the last associated player was last on. + def team(self) -> bascenev1.SessionTeam: + """The bascenev1.SessionTeam the last associated player was last on. This can still return a valid result even if the player is gone. - Raises a ba.SessionTeamNotFoundError if the team no longer exists. + Raises a bascenev1.SessionTeamNotFoundError if the team no longer + exists. """ assert self._sessionteam is not None team = self._sessionteam() @@ -83,10 +88,10 @@ class PlayerRecord: return team @property - def player(self) -> ba.SessionPlayer: - """Return the instance's associated ba.SessionPlayer. + def player(self) -> bascenev1.SessionPlayer: + """Return the instance's associated bascenev1.SessionPlayer. - Raises a ba.SessionPlayerNotFoundError if the player + Raises a bascenev1.SessionPlayerNotFoundError if the player no longer exists. """ if not self._sessionplayer: @@ -107,8 +112,8 @@ class PlayerRecord: """Cancel any multi-kill timer for this player entry.""" self._multi_kill_timer = None - def getactivity(self) -> ba.Activity | None: - """Return the ba.Activity this instance is currently associated with. + def getactivity(self) -> bascenev1.Activity | None: + """Return the bascenev1.Activity this instance is associated with. Returns None if the activity no longer exists.""" stats = self._stats() @@ -117,9 +122,9 @@ class PlayerRecord: return None def associate_with_sessionplayer( - self, sessionplayer: ba.SessionPlayer + self, sessionplayer: bascenev1.SessionPlayer ) -> None: - """Associate this entry with a ba.SessionPlayer.""" + """Associate this entry with a bascenev1.SessionPlayer.""" self._sessionteam = weakref.ref(sessionplayer.sessionteam) self.character = sessionplayer.character self._last_sessionplayer = sessionplayer @@ -130,8 +135,8 @@ class PlayerRecord: self._multi_kill_timer = None self._multi_kill_count = 0 - def get_last_sessionplayer(self) -> ba.SessionPlayer: - """Return the last ba.Player we were associated with.""" + def get_last_sessionplayer(self) -> bascenev1.SessionPlayer: + """Return the last bascenev1.Player we were associated with.""" assert self._last_sessionplayer is not None return self._last_sessionplayer @@ -139,8 +144,8 @@ class PlayerRecord: """Submit a kill for this player entry.""" # FIXME Clean this up. # pylint: disable=too-many-statements - from ba._language import Lstr - from ba._general import Call + from babase._language import Lstr + from babase._general import Call self._multi_kill_count += 1 stats = self._stats() @@ -197,13 +202,13 @@ class PlayerRecord: showpoints2: bool, color2: tuple[float, float, float, float], scale2: float, - sound2: ba.Sound | None, + sound2: bascenev1.Sound | None, ) -> None: from bastd.actor.popuptext import PopupText # Only award this if they're still alive and we can get # a current position for them. - our_pos: ba.Vec3 | None = None + our_pos: babase.Vec3 | None = None if self._sessionplayer: if self._sessionplayer.activityplayer is not None: try: @@ -214,7 +219,7 @@ class PlayerRecord: return # Jitter position a bit since these often come in clusters. - our_pos = _ba.Vec3( + our_pos = _babase.Vec3( our_pos[0] + (random.random() - 0.5) * 2.0, our_pos[1] + (random.random() - 0.5) * 2.0, our_pos[2] + (random.random() - 0.5) * 2.0, @@ -232,7 +237,7 @@ class PlayerRecord: position=our_pos, ).autoretain() if sound2: - _ba.playsound(sound2) + sound2.play() self.score += score2 self.accumscore += score2 @@ -242,31 +247,31 @@ class PlayerRecord: activity.handlemessage(PlayerScoredMessage(score=score2)) if name is not None: - _ba.timer( + _bascenev1.timer( 0.3 + delay, Call(_apply, name, score, showpoints, color, scale, sound), ) # Keep the tally rollin'... # set a timer for a bit in the future. - self._multi_kill_timer = _ba.Timer(1.0, self._end_multi_kill) + self._multi_kill_timer = _bascenev1.Timer(1.0, self._end_multi_kill) class Stats: - """Manages scores and statistics for a ba.Session. + """Manages scores and statistics for a bascenev1.Session. Category: **Gameplay Classes** """ def __init__(self) -> None: - self._activity: weakref.ref[ba.Activity] | None = None + self._activity: weakref.ref[bascenev1.Activity] | None = None self._player_records: dict[str, PlayerRecord] = {} - self.orchestrahitsound1: ba.Sound | None = None - self.orchestrahitsound2: ba.Sound | None = None - self.orchestrahitsound3: ba.Sound | None = None - self.orchestrahitsound4: ba.Sound | None = None + self.orchestrahitsound1: bascenev1.Sound | None = None + self.orchestrahitsound2: bascenev1.Sound | None = None + self.orchestrahitsound3: bascenev1.Sound | None = None + self.orchestrahitsound4: bascenev1.Sound | None = None - def setactivity(self, activity: ba.Activity | None) -> None: + def setactivity(self, activity: bascenev1.Activity | None) -> None: """Set the current activity for this instance.""" self._activity = None if activity is None else weakref.ref(activity) @@ -276,10 +281,10 @@ class Stats: if activity.expired: print_error('unexpected finalized activity') else: - with _ba.Context(activity): + with activity.context: self._load_activity_media() - def getactivity(self) -> ba.Activity | None: + def getactivity(self) -> bascenev1.Activity | None: """Get the activity associated with this instance. May return None. @@ -289,10 +294,10 @@ class Stats: return self._activity() def _load_activity_media(self) -> None: - self.orchestrahitsound1 = _ba.getsound('orchestraHit') - self.orchestrahitsound2 = _ba.getsound('orchestraHit2') - self.orchestrahitsound3 = _ba.getsound('orchestraHit3') - self.orchestrahitsound4 = _ba.getsound('orchestraHit4') + self.orchestrahitsound1 = _bascenev1.getsound('orchestraHit') + self.orchestrahitsound2 = _bascenev1.getsound('orchestraHit2') + self.orchestrahitsound3 = _bascenev1.getsound('orchestraHit3') + self.orchestrahitsound4 = _bascenev1.getsound('orchestraHit4') def reset(self) -> None: """Reset the stats instance completely.""" @@ -312,8 +317,8 @@ class Stats: s_player.accum_killed_count = 0 s_player.streak = 0 - def register_sessionplayer(self, player: ba.SessionPlayer) -> None: - """Register a ba.SessionPlayer with this score-set.""" + def register_sessionplayer(self, player: bascenev1.SessionPlayer) -> None: + """Register a bascenev1.SessionPlayer with this score-set.""" assert player.exists() # Invalid refs should never be passed to funcs. name = player.getname() if name in self._player_records: @@ -326,7 +331,7 @@ class Stats: name, name_full, player, self ) - def get_records(self) -> dict[str, ba.PlayerRecord]: + def get_records(self) -> dict[str, bascenev1.PlayerRecord]: """Get PlayerRecord corresponding to still-existing players.""" records = {} @@ -340,14 +345,14 @@ class Stats: def player_scored( self, - player: ba.Player, + player: bascenev1.Player, base_points: int = 1, target: Sequence[float] | None = None, kill: bool = False, - victim_player: ba.Player | None = None, + victim_player: bascenev1.Player | None = None, scale: float = 1.0, color: Sequence[float] | None = None, - title: str | ba.Lstr | None = None, + title: str | babase.Lstr | None = None, screenmessage: bool = True, display: bool = True, importance: int = 1, @@ -364,9 +369,9 @@ class Stats: # pylint: disable=too-many-locals # pylint: disable=too-many-statements from bastd.actor.popuptext import PopupText - from ba import _math - from ba._gameactivity import GameActivity - from ba._language import Lstr + from babase import _math + from babase._language import Lstr + from bascenev1._gameactivity import GameActivity del victim_player # Currently unused. name = player.getname() @@ -438,7 +443,7 @@ class Stats: # Report non-kill scorings. try: if screenmessage and not kill: - _ba.screenmessage( + _bascenev1.screenmessage( Lstr(resource='nameScoresText', subs=[('${NAME}', name)]), top=True, color=player.color, @@ -460,12 +465,12 @@ class Stats: def player_was_killed( self, - player: ba.Player, + player: bascenev1.Player, killed: bool = False, - killer: ba.Player | None = None, + killer: bascenev1.Player | None = None, ) -> None: """Should be called when a player is killed.""" - from ba._language import Lstr + from babase._language import Lstr name = player.getname() prec = self._player_records[name] @@ -474,9 +479,9 @@ class Stats: prec.accum_killed_count += 1 prec.killed_count += 1 try: - if killed and _ba.getactivity().announce_player_deaths: + if killed and _bascenev1.getactivity().announce_player_deaths: if killer is player: - _ba.screenmessage( + _bascenev1.screenmessage( Lstr( resource='nameSuicideText', subs=[('${NAME}', name)] ), @@ -486,7 +491,7 @@ class Stats: ) elif killer is not None: if killer.team is player.team: - _ba.screenmessage( + _bascenev1.screenmessage( Lstr( resource='nameBetrayedText', subs=[ @@ -499,7 +504,7 @@ class Stats: image=killer.get_icon(), ) else: - _ba.screenmessage( + _bascenev1.screenmessage( Lstr( resource='nameKilledText', subs=[ @@ -512,7 +517,7 @@ class Stats: image=killer.get_icon(), ) else: - _ba.screenmessage( + _bascenev1.screenmessage( Lstr(resource='nameDiedText', subs=[('${NAME}', name)]), top=True, color=player.color, diff --git a/assets/src/ba_data/python/ba/_team.py b/src/assets/ba_data/python/bascenev1/_team.py similarity index 74% rename from assets/src/ba_data/python/ba/_team.py rename to src/assets/ba_data/python/bascenev1/_team.py index 9e7390fd..8d89d261 100644 --- a/assets/src/ba_data/python/ba/_team.py +++ b/src/assets/ba_data/python/bascenev1/_team.py @@ -7,36 +7,38 @@ from __future__ import annotations import weakref from typing import TYPE_CHECKING, TypeVar, Generic -from ba._error import print_exception +from babase._error import print_exception if TYPE_CHECKING: from typing import Sequence - import ba + + import babase + import bascenev1 class SessionTeam: - """A team of one or more ba.SessionPlayers. + """A team of one or more bascenev1.SessionPlayers. Category: **Gameplay Classes** Note that a SessionPlayer *always* has a SessionTeam; - in some cases, such as free-for-all ba.Sessions, + in some cases, such as free-for-all bascenev1.Sessions, each SessionTeam consists of just one SessionPlayer. """ # Annotate our attr types at the class level so they're introspectable. - name: ba.Lstr | str + name: babase.Lstr | str """The team's name.""" color: tuple[float, ...] # FIXME: can't we make this fixed len? """The team's color.""" - players: list[ba.SessionPlayer] - """The list of ba.SessionPlayer-s on the team.""" + players: list[bascenev1.SessionPlayer] + """The list of bascenev1.SessionPlayer-s on the team.""" customdata: dict - """A dict for use by the current ba.Session for + """A dict for use by the current bascenev1.Session for storing data associated with this team. Unlike customdata, this persists for the duration of the session.""" @@ -47,13 +49,13 @@ class SessionTeam: def __init__( self, team_id: int = 0, - name: ba.Lstr | str = '', + name: babase.Lstr | str = '', color: Sequence[float] = (1.0, 1.0, 1.0), ): - """Instantiate a ba.SessionTeam. + """Instantiate a bascenev1.SessionTeam. - In most cases, all teams are provided to you by the ba.Session, - ba.Session, so calling this shouldn't be necessary. + In most cases, all teams are provided to you by the bascenev1.Session, + bascenev1.Session, so calling this shouldn't be necessary. """ self.id = team_id @@ -68,25 +70,23 @@ class SessionTeam: self.customdata = {} -# pylint: disable=invalid-name -PlayerType = TypeVar('PlayerType', bound='ba.Player') -# pylint: enable=invalid-name +PlayerT = TypeVar('PlayerT', bound='bascenev1.Player') -class Team(Generic[PlayerType]): - """A team in a specific ba.Activity. +class Team(Generic[PlayerT]): + """A team in a specific bascenev1.Activity. Category: **Gameplay Classes** - These correspond to ba.SessionTeam objects, but are created per activity - so that the activity can use its own custom team subclass. + These correspond to bascenev1.SessionTeam objects, but are created + per activity so that the activity can use its own custom team subclass. """ # Defining these types at the class level instead of in __init__ so # that types are introspectable (these are still instance attrs). - players: list[PlayerType] + players: list[PlayerT] id: int - name: ba.Lstr | str + name: babase.Lstr | str color: tuple[float, ...] # FIXME: can't we make this fixed length? _sessionteam: weakref.ref[SessionTeam] _expired: bool @@ -124,7 +124,7 @@ class Team(Generic[PlayerType]): self._postinited = True def manual_init( - self, team_id: int, name: ba.Lstr | str, color: tuple[float, ...] + self, team_id: int, name: babase.Lstr | str, color: tuple[float, ...] ) -> None: """Manually init a team for uses such as bots.""" self.id = team_id @@ -138,7 +138,7 @@ class Team(Generic[PlayerType]): def customdata(self) -> dict: """Arbitrary values associated with the team. Though it is encouraged that most player values be properly defined - on the ba.Team subclass, it may be useful for player-agnostic + on the bascenev1.Team subclass, it may be useful for player-agnostic objects to store values here. This dict is cleared when the team leaves or expires so objects stored here will be disposed of at the expected time, unlike the Team instance itself which may @@ -180,30 +180,31 @@ class Team(Generic[PlayerType]): @property def sessionteam(self) -> SessionTeam: - """Return the ba.SessionTeam corresponding to this Team. + """Return the bascenev1.SessionTeam corresponding to this Team. - Throws a ba.SessionTeamNotFoundError if there is none. + Throws a babase.SessionTeamNotFoundError if there is none. """ assert self._postinited if self._sessionteam is not None: sessionteam = self._sessionteam() if sessionteam is not None: return sessionteam - from ba import _error + from babase import _error raise _error.SessionTeamNotFoundError() -class EmptyTeam(Team['ba.EmptyPlayer']): +class EmptyTeam(Team['bascenev1.EmptyPlayer']): """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 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. + bascenev1.Player and bascenev1.Team are 'Generic' types, and so passing + those top level classes as type arguments when defining a + bascenev1.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 bascenev1.Activity that does not need custom types of its own. Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, so if you want to define your own class for one of them you should do so diff --git a/assets/src/ba_data/python/ba/_teamgame.py b/src/assets/ba_data/python/bascenev1/_teamgame.py similarity index 75% rename from assets/src/ba_data/python/ba/_teamgame.py rename to src/assets/ba_data/python/bascenev1/_teamgame.py index 5a6aaed9..d8ada009 100644 --- a/assets/src/ba_data/python/ba/_teamgame.py +++ b/src/assets/ba_data/python/bascenev1/_teamgame.py @@ -6,34 +6,36 @@ from __future__ import annotations from typing import TYPE_CHECKING, TypeVar -import _ba -from ba._freeforallsession import FreeForAllSession -from ba._gameactivity import GameActivity -from ba._gameresults import GameResults -from ba._dualteamsession import DualTeamSession +import _babase +import _bascenev1 +from bascenev1._freeforallsession import FreeForAllSession +from bascenev1._gameactivity import GameActivity +from bascenev1._gameresults import GameResults +from bascenev1._dualteamsession import DualTeamSession if TYPE_CHECKING: from typing import Any, Sequence from bastd.actor.playerspaz import PlayerSpaz - import ba + import babase + import bascenev1 -# pylint: disable=invalid-name -PlayerType = TypeVar('PlayerType', bound='ba.Player') -TeamType = TypeVar('TeamType', bound='ba.Team') -# pylint: enable=invalid-name +PlayerT = TypeVar('PlayerT', bound='bascenev1.Player') +TeamT = TypeVar('TeamT', bound='bascenev1.Team') -class TeamGameActivity(GameActivity[PlayerType, TeamType]): +class TeamGameActivity(GameActivity[PlayerT, TeamT]): """Base class for teams and free-for-all mode games. Category: **Gameplay Classes** (Free-for-all is essentially just a special case where every - ba.Player has their own ba.Team) + bascenev1.Player has their own bascenev1.Team) """ @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + def supports_session_type( + cls, sessiontype: type[bascenev1.Session] + ) -> bool: """ Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; @@ -54,7 +56,7 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]): def on_transition_in(self) -> None: # pylint: disable=cyclic-import - from ba._coopsession import CoopSession + from bascenev1._coopsession import CoopSession from bastd.actor.controlsguide import ControlsGuide super().on_transition_in() @@ -81,32 +83,36 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]): def on_begin(self) -> None: super().on_begin() try: - # Award a few achievements. + # Award a few (classic) achievements. if isinstance(self.session, FreeForAllSession): if len(self.players) >= 2: - _ba.app.ach.award_local_achievement('Free Loader') + if _babase.app.classic is not None: + _babase.app.classic.ach.award_local_achievement( + 'Free Loader' + ) elif isinstance(self.session, DualTeamSession): if len(self.players) >= 4: - from ba import _achievement - - _ba.app.ach.award_local_achievement('Team Player') + if _babase.app.classic is not None: + _babase.app.classic.ach.award_local_achievement( + 'Team Player' + ) except Exception: - from ba import _error + from babase import _error _error.print_exception() def spawn_player_spaz( self, - player: PlayerType, + player: PlayerT, position: Sequence[float] | None = None, angle: float | None = None, ) -> PlayerSpaz: """ - Method override; spawns and wires up a standard ba.PlayerSpaz for - a ba.Player. + Method override; spawns and wires up a standard bascenev1.PlayerSpaz + for a bascenev1.Player. If position or angle is not supplied, a default will be chosen based - on the ba.Player and their ba.Team. + on the bascenev1.Player and their bascenev1.Team. """ if position is None: # In teams-mode get our team-start-location. @@ -132,9 +138,9 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]): (for results without a single most-important winner). """ # pylint: disable=arguments-renamed - from ba._coopsession import CoopSession - from ba._multiteamsession import MultiTeamSession - from ba._general import Call + from bascenev1._coopsession import CoopSession + from bascenev1._multiteamsession import MultiTeamSession + from babase._general import Call # Announce win (but only for the first finish() call) # (also don't announce in co-op sessions; we leave that up to them). @@ -166,5 +172,5 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]): delay = 0.0 else: delay = 2.0 - _ba.timer(0.1, Call(_ba.playsound, _ba.getsound('boxingBell'))) + _bascenev1.timer(0.1, _bascenev1.getsound('boxingBell').play) super().end(results, delay=delay, force=force) diff --git a/src/assets/ba_data/python/bascenev1/internal.py b/src/assets/ba_data/python/bascenev1/internal.py new file mode 100644 index 00000000..d0b6673f --- /dev/null +++ b/src/assets/ba_data/python/bascenev1/internal.py @@ -0,0 +1,43 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Exposed functionality not intended for full public use. + +Classes and functions contained here, while technically 'public', may change +or disappear without warning, so should be avoided (or used sparingly and +defensively). +""" +from __future__ import annotations + + +from bascenev1._gameutils import get_trophy_string +from bascenev1._map import ( + get_map_class, + register_map, + preload_map_preview_media, + get_map_display_string, + get_filtered_map_name, +) +from bascenev1._messages import PlayerProfilesChangedMessage +from bascenev1._multiteamsession import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES +from bascenev1._powerup import get_default_powerup_distribution +from bascenev1._playlist import ( + get_default_free_for_all_playlist, + get_default_teams_playlist, + filter_playlist, +) + +__all__ = [ + 'get_trophy_string', + 'get_map_class', + 'register_map', + 'preload_map_preview_media', + 'get_map_display_string', + 'get_filtered_map_name', + 'PlayerProfilesChangedMessage', + 'DEFAULT_TEAM_COLORS', + 'DEFAULT_TEAM_NAMES', + 'get_default_powerup_distribution', + 'get_default_free_for_all_playlist', + 'get_default_teams_playlist', + 'filter_playlist', +] diff --git a/assets/src/ba_data/python/bastd/__init__.py b/src/assets/ba_data/python/bastd/__init__.py similarity index 82% rename from assets/src/ba_data/python/bastd/__init__.py rename to src/assets/ba_data/python/bastd/__init__.py index b77fdd29..17c4007c 100644 --- a/assets/src/ba_data/python/bastd/__init__.py +++ b/src/assets/ba_data/python/bastd/__init__.py @@ -2,4 +2,4 @@ # """Ballistica standard library: games, UI, etc.""" -# ba_meta require api 7 +# ba_meta require api 8 diff --git a/assets/src/ba_data/python/bastd/activity/__init__.py b/src/assets/ba_data/python/bastd/activity/__init__.py similarity index 100% rename from assets/src/ba_data/python/bastd/activity/__init__.py rename to src/assets/ba_data/python/bastd/activity/__init__.py diff --git a/assets/src/ba_data/python/bastd/activity/coopjoin.py b/src/assets/ba_data/python/bastd/activity/coopjoin.py similarity index 77% rename from assets/src/ba_data/python/bastd/activity/coopjoin.py rename to src/assets/ba_data/python/bastd/activity/coopjoin.py index ebe9dea4..a35082d9 100644 --- a/assets/src/ba_data/python/bastd/activity/coopjoin.py +++ b/src/assets/ba_data/python/bastd/activity/coopjoin.py @@ -6,30 +6,30 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba -from ba.internal import JoinActivity +import babase +import bascenev1 as bs if TYPE_CHECKING: pass -class CoopJoinActivity(JoinActivity): +class CoopJoinActivity(bs.JoinActivity): """Join-screen for co-op mode.""" # We can assume our session is a CoopSession. - session: ba.CoopSession + session: bs.CoopSession def __init__(self, settings: dict): super().__init__(settings) session = self.session - assert isinstance(session, ba.CoopSession) + assert isinstance(session, bs.CoopSession) def on_transition_in(self) -> None: from bastd.actor.controlsguide import ControlsGuide from bastd.actor.text import Text super().on_transition_in() - assert isinstance(self.session, ba.CoopSession) + assert isinstance(self.session, bs.CoopSession) assert self.session.campaign Text( self.session.campaign.getlevel( @@ -46,16 +46,16 @@ class CoopJoinActivity(JoinActivity): ).autoretain() ControlsGuide(delay=1.0).autoretain() - ba.pushcall(self._show_remaining_achievements) + babase.pushcall(self._show_remaining_achievements) def _show_remaining_achievements(self) -> None: from bastd.actor.text import Text # We only show achievements and challenges for CoopGameActivities. session = self.session - assert isinstance(session, ba.CoopSession) + assert isinstance(session, bs.CoopSession) gameinstance = session.get_current_game_instance() - if not isinstance(gameinstance, ba.CoopGameActivity): + if not isinstance(gameinstance, bs.CoopGameActivity): return delay = 1.0 @@ -63,24 +63,29 @@ class CoopJoinActivity(JoinActivity): # Now list our remaining achievements for this level. assert self.session.campaign is not None - assert isinstance(self.session, ba.CoopSession) + assert isinstance(self.session, bs.CoopSession) levelname = ( self.session.campaign.name + ':' + self.session.campaign_level_name ) ts_h_offs = 60 - if not (ba.app.demo_mode or ba.app.arcade_mode): + # Show remaining achievements in some cases. + if babase.app.classic is not None and not ( + babase.app.demo_mode or babase.app.arcade_mode + ): achievements = [ a - for a in ba.app.ach.achievements_for_coop_level(levelname) + for a in babase.app.classic.ach.achievements_for_coop_level( + levelname + ) if not a.complete ] have_achievements = bool(achievements) achievements = [a for a in achievements if not a.complete] - vrmode = ba.app.vr_mode + vrmode = babase.app.vr_mode if have_achievements: Text( - ba.Lstr(resource='achievementsRemainingText'), + babase.Lstr(resource='achievementsRemainingText'), host_only=True, position=(ts_h_offs - 10, vpos), transition=Text.Transition.FADE_IN, @@ -100,7 +105,7 @@ class CoopJoinActivity(JoinActivity): vpos -= 55 if not achievements: Text( - ba.Lstr(resource='noAchievementsRemainingText'), + babase.Lstr(resource='noAchievementsRemainingText'), host_only=True, position=(ts_h_offs + 15, vpos + 10), transition=Text.Transition.FADE_IN, diff --git a/assets/src/ba_data/python/bastd/activity/coopscore.py b/src/assets/ba_data/python/bastd/activity/coopscore.py similarity index 79% rename from assets/src/ba_data/python/bastd/activity/coopscore.py rename to src/assets/ba_data/python/bastd/activity/coopscore.py index 63864605..7bcf89a4 100644 --- a/assets/src/ba_data/python/bastd/activity/coopscore.py +++ b/src/assets/ba_data/python/bastd/activity/coopscore.py @@ -6,26 +6,32 @@ from __future__ import annotations import random +import logging from typing import TYPE_CHECKING -import ba -import ba.internal from bastd.actor.text import Text from bastd.actor.zoomtext import ZoomText +import bascenev1 as bs +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Sequence + + import baclassic from bastd.ui.store.button import StoreButton from bastd.ui.league.rankbutton import LeagueRankButton -class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): +class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): """Score screen showing the results of a cooperative game.""" def __init__(self, settings: dict): # pylint: disable=too-many-statements super().__init__(settings) + plus = bs.app.plus + assert plus is not None + # Keep prev activity alive while we fade in self.transition_time = 0.5 self.inherits_tint = True @@ -35,69 +41,67 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): self._do_new_rating: bool = self.session.tournament_id is not None - self._score_display_sound = ba.getsound('scoreHit01') - self._score_display_sound_small = ba.getsound('scoreHit02') - self.drum_roll_sound = ba.getsound('drumRoll') - self.cymbal_sound = ba.getsound('cymbal') + self._score_display_sound = bs.getsound('scoreHit01') + self._score_display_sound_small = bs.getsound('scoreHit02') + self.drum_roll_sound = bs.getsound('drumRoll') + self.cymbal_sound = bs.getsound('cymbal') - # These get used in UI bits so need to load them in the UI context. - with ba.Context('ui'): - self._replay_icon_texture = ba.gettexture('replayIcon') - self._menu_icon_texture = ba.gettexture('menuIcon') - self._next_level_icon_texture = ba.gettexture('nextLevelIcon') + self._replay_icon_texture = bui.gettexture('replayIcon') + self._menu_icon_texture = bui.gettexture('menuIcon') + self._next_level_icon_texture = bui.gettexture('nextLevelIcon') - self._campaign: ba.Campaign = settings['campaign'] + self._campaign: baclassic.Campaign = settings['campaign'] - self._have_achievements = bool( - ba.app.ach.achievements_for_coop_level( + self._have_achievements = ( + bs.app.classic is not None + and bs.app.classic.ach.achievements_for_coop_level( self._campaign.name + ':' + settings['level'] ) ) self._account_type = ( - ba.internal.get_v1_account_type() - if ba.internal.get_v1_account_state() == 'signed_in' + plus.get_v1_account_type() + if plus.get_v1_account_state() == 'signed_in' else None ) self._game_service_icon_color: Sequence[float] | None - self._game_service_achievements_texture: ba.Texture | None - self._game_service_leaderboards_texture: ba.Texture | None + self._game_service_achievements_texture: bui.Texture | None + self._game_service_leaderboards_texture: bui.Texture | None - with ba.Context('ui'): - if self._account_type == 'Game Center': - self._game_service_icon_color = (1.0, 1.0, 1.0) - icon = ba.gettexture('gameCenterIcon') - self._game_service_achievements_texture = icon - self._game_service_leaderboards_texture = icon - self._account_has_achievements = True - elif self._account_type == 'Game Circle': - icon = ba.gettexture('gameCircleIcon') - self._game_service_icon_color = (1, 1, 1) - self._game_service_achievements_texture = icon - self._game_service_leaderboards_texture = icon - self._account_has_achievements = True - elif self._account_type == 'Google Play': - self._game_service_icon_color = (0.8, 1.0, 0.6) - self._game_service_achievements_texture = ba.gettexture( - 'googlePlayAchievementsIcon' - ) - self._game_service_leaderboards_texture = ba.gettexture( - 'googlePlayLeaderboardsIcon' - ) - self._account_has_achievements = True - else: - self._game_service_icon_color = None - self._game_service_achievements_texture = None - self._game_service_leaderboards_texture = None - self._account_has_achievements = False + if self._account_type == 'Game Center': + self._game_service_icon_color = (1.0, 1.0, 1.0) + icon = bui.gettexture('gameCenterIcon') + self._game_service_achievements_texture = icon + self._game_service_leaderboards_texture = icon + self._account_has_achievements = True + elif self._account_type == 'Game Circle': + icon = bui.gettexture('gameCircleIcon') + self._game_service_icon_color = (1, 1, 1) + self._game_service_achievements_texture = icon + self._game_service_leaderboards_texture = icon + self._account_has_achievements = True + elif self._account_type == 'Google Play': + self._game_service_icon_color = (0.8, 1.0, 0.6) + self._game_service_achievements_texture = bui.gettexture( + 'googlePlayAchievementsIcon' + ) + self._game_service_leaderboards_texture = bui.gettexture( + 'googlePlayLeaderboardsIcon' + ) + self._account_has_achievements = True + else: + self._game_service_icon_color = None + self._game_service_achievements_texture = None + self._game_service_leaderboards_texture = None + self._account_has_achievements = False - self._cashregistersound = ba.getsound('cashRegister') - self._gun_cocking_sound = ba.getsound('gunCocking') - self._dingsound = ba.getsound('ding') + self._cashregistersound = bs.getsound('cashRegister') + self._gun_cocking_sound = bs.getsound('gunCocking') + self._dingsound = bs.getsound('ding') self._score_link: str | None = None - self._root_ui: ba.Widget | None = None - self._background: ba.Actor | None = None + self._root_ui: bui.Widget | None = None + self._background: bs.Actor | None = None self._old_best_rank = 0.0 self._game_name_str: str | None = None self._game_config_str: str | None = None @@ -106,9 +110,9 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): self._corner_button_offs: tuple[float, float] | None = None self._league_rank_button: LeagueRankButton | None = None self._store_button_instance: StoreButton | None = None - self._restart_button: ba.Widget | None = None - self._update_corner_button_positions_timer: ba.Timer | None = None - self._next_level_error: ba.Actor | None = None + self._restart_button: bui.Widget | None = None + self._update_corner_button_positions_timer: bui.AppTimer | None = None + self._next_level_error: bs.Actor | None = None # Score/gameplay bits. self._was_complete: bool | None = None @@ -118,27 +122,27 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): self._next_level_name: str | None = None self._show_info: dict[str, Any] | None = None self._name_str: str | None = None - self._friends_loading_status: ba.Actor | None = None - self._score_loading_status: ba.Actor | None = None + self._friends_loading_status: bs.Actor | None = None + self._score_loading_status: bs.Actor | None = None self._tournament_time_remaining: float | None = None self._tournament_time_remaining_text: Text | None = None - self._tournament_time_remaining_text_timer: ba.Timer | None = None + self._tournament_time_remaining_text_timer: bs.BaseTimer | None = None # Stuff for activity skip by pressing button - self._birth_time = ba.time() + self._birth_time = bs.time() self._min_view_time = 5.0 self._allow_server_transition = False self._server_transitioning: bool | None = None - self._playerinfos: list[ba.PlayerInfo] = settings['playerinfos'] + self._playerinfos: list[bs.PlayerInfo] = settings['playerinfos'] assert isinstance(self._playerinfos, list) - assert (isinstance(i, ba.PlayerInfo) for i in self._playerinfos) + assert all(isinstance(i, bs.PlayerInfo) for i in self._playerinfos) self._score: int | None = settings['score'] assert isinstance(self._score, (int, type(None))) - self._fail_message: ba.Lstr | None = settings['fail_message'] - assert isinstance(self._fail_message, (ba.Lstr, type(None))) + self._fail_message: bs.Lstr | None = settings['fail_message'] + assert isinstance(self._fail_message, (bs.Lstr, type(None))) self._begin_time: float | None = None @@ -190,13 +194,13 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # If our UI is still up, kill it. if self._root_ui: - with ba.Context('ui'): - ba.containerwidget(edit=self._root_ui, transition='out_left') + with bui.ContextRef.empty(): + bui.containerwidget(edit=self._root_ui, transition='out_left') def on_transition_in(self) -> None: from bastd.actor import background # FIXME NO BSSTD - ba.set_analytics_screen('Coop Score Screen') + bs.set_analytics_screen('Coop Score Screen') super().on_transition_in() self._background = background.Background( fade_time=0.45, start_faded=False, show_logo=True @@ -207,9 +211,9 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): if specialoffer.show_offer(): return - ba.containerwidget(edit=self._root_ui, transition='out_left') - with ba.Context(self): - ba.timer(0.1, ba.Call(ba.WeakCall(self.session.end))) + bui.containerwidget(edit=self._root_ui, transition='out_left') + with self.context: + bs.timer(0.1, bs.Call(bs.WeakCall(self.session.end))) def _ui_restart(self) -> None: from bastd.ui.tournamententry import TournamentEntryWindow @@ -222,27 +226,27 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # disallow. if self.session.tournament_id is not None: if self._tournament_time_remaining is None: - ba.screenmessage( - ba.Lstr(resource='tournamentCheckingStateText'), + bui.screenmessage( + bui.Lstr(resource='tournamentCheckingStateText'), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return if self._tournament_time_remaining <= 0: - ba.screenmessage( - ba.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) + bui.screenmessage( + bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return # If there are currently fewer players than our session min, # don't allow. if len(self.players) < self.session.min_players: - ba.screenmessage( - ba.Lstr(resource='notEnoughPlayersRemainingText'), + bui.screenmessage( + bui.Lstr(resource='notEnoughPlayersRemainingText'), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return self._campaign.set_selected_level(self._level_name) @@ -258,9 +262,9 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): position=self._restart_button.get_screen_space_center(), ) else: - ba.containerwidget(edit=self._root_ui, transition='out_left') + bui.containerwidget(edit=self._root_ui, transition='out_left') self.can_show_ad_on_death = True - with ba.Context(self): + with self.context: self.end({'outcome': 'restart'}) def _ui_next(self) -> None: @@ -278,33 +282,39 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ): assert self._next_level_name is not None self._campaign.set_selected_level(self._next_level_name) - ba.containerwidget(edit=self._root_ui, transition='out_left') - with ba.Context(self): + bui.containerwidget(edit=self._root_ui, transition='out_left') + with self.context: self.end({'outcome': 'next_level'}) def _ui_gc(self) -> None: - ba.internal.show_online_score_ui( - 'leaderboard', - game=self._game_name_str, - game_version=self._game_config_str, - ) + if bs.app.classic is not None: + bs.app.classic.show_online_score_ui( + 'leaderboard', + game=self._game_name_str, + game_version=self._game_config_str, + ) + else: + logging.warning('show_online_score_ui requires classic') def _ui_show_achievements(self) -> None: - ba.internal.show_online_score_ui('achievements') + if bs.app.classic is not None: + bs.app.classic.show_online_score_ui('achievements') + else: + logging.warning('show_online_score_ui requires classic') def _ui_worlds_best(self) -> None: if self._score_link is None: - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource='scoreListUnavailableText'), color=(1, 0.5, 0) + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='scoreListUnavailableText'), color=(1, 0.5, 0) ) else: - ba.open_url(self._score_link) + bui.open_url(self._score_link) def _ui_error(self) -> None: - with ba.Context(self): + with self.context: self._next_level_error = Text( - ba.Lstr(resource='completeThisLevelToProceedText'), + bs.Lstr(resource='completeThisLevelToProceedText'), flash=True, maxwidth=360, scale=0.54, @@ -312,25 +322,26 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): color=(0.5, 0.7, 0.5, 1), position=(300, -235), ) - ba.playsound(ba.getsound('error')) - ba.timer( + bui.getsound('error').play() + bs.timer( 2.0, - ba.WeakCall( - self._next_level_error.handlemessage, ba.DieMessage() + bs.WeakCall( + self._next_level_error.handlemessage, bs.DieMessage() ), ) def _should_show_worlds_best_button(self) -> bool: # Link is too complicated to display with no browser. - return ba.is_browser_likely_available() + return bui.is_browser_likely_available() def request_ui(self) -> None: """Set up a callback to show our UI at the next opportune time.""" + assert bui.app.classic is not None # We don't want to just show our UI in case the user already has the # main menu up, so instead we add a callback for when the menu # closes; if we're still alive, we'll come up then. # If there's no main menu this gets called immediately. - ba.app.add_main_menu_close_callback(ba.WeakCall(self.show_ui)) + bui.app.classic.add_main_menu_close_callback(bui.WeakCall(self.show_ui)) def show_ui(self) -> None: """Show the UI for restarting, playing the next Level, etc.""" @@ -338,6 +349,8 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): from bastd.ui.store.button import StoreButton from bastd.ui.league.rankbutton import LeagueRankButton + assert bui.app.classic is not None + delay = 0.7 if (self._score is not None) else 0.0 # If there's no players left in the game, lets not show the UI @@ -345,7 +358,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): if not self.players: return - rootc = self._root_ui = ba.containerwidget( + rootc = self._root_ui = bui.containerwidget( size=(0, 0), transition='in_right' ) @@ -355,18 +368,18 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # We wanna prevent controllers users from popping up browsers # or game-center widgets in cases where they can't easily get back # to the game (like on mac). - can_select_extra_buttons = ba.app.platform == 'android' + can_select_extra_buttons = bui.app.classic.platform == 'android' - ba.internal.set_ui_input_device(None) # Menu is up for grabs. + bui.set_ui_input_device(None) # Menu is up for grabs. if self._have_achievements and self._account_has_achievements: - ba.buttonwidget( + bui.buttonwidget( parent=rootc, color=(0.45, 0.4, 0.5), position=(h_offs - 520, v_offs + 450 - 235 + 40), size=(300, 60), - label=ba.Lstr(resource='achievementsText'), - on_activate_call=ba.WeakCall(self._ui_show_achievements), + label=bui.Lstr(resource='achievementsText'), + on_activate_call=bui.WeakCall(self._ui_show_achievements), transition_delay=delay + 1.5, icon=self._game_service_achievements_texture, icon_color=self._game_service_icon_color, @@ -375,18 +388,18 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ) if self._should_show_worlds_best_button(): - ba.buttonwidget( + bui.buttonwidget( parent=rootc, color=(0.45, 0.4, 0.5), position=(160, v_offs + 480), size=(350, 62), - label=ba.Lstr(resource='tournamentStandingsText') + label=bui.Lstr(resource='tournamentStandingsText') if self.session.tournament_id is not None - else ba.Lstr(resource='worldsBestScoresText') + else bui.Lstr(resource='worldsBestScoresText') if self._score_type == 'points' - else ba.Lstr(resource='worldsBestTimesText'), + else bui.Lstr(resource='worldsBestTimesText'), autoselect=True, - on_activate_call=ba.WeakCall(self._ui_worlds_best), + on_activate_call=bui.WeakCall(self._ui_worlds_best), transition_delay=delay + 1.9, selectable=can_select_extra_buttons, ) @@ -394,21 +407,21 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): pass show_next_button = self._is_more_levels and not ( - ba.app.demo_mode or ba.app.arcade_mode + bui.app.demo_mode or bui.app.arcade_mode ) if not show_next_button: h_offs += 70 - menu_button = ba.buttonwidget( + menu_button = bui.buttonwidget( parent=rootc, autoselect=True, position=(h_offs - 130 - 60, v_offs), size=(110, 85), label='', - on_activate_call=ba.WeakCall(self._ui_menu), + on_activate_call=bui.WeakCall(self._ui_menu), ) - ba.imagewidget( + bui.imagewidget( parent=rootc, draw_controller=menu_button, position=(h_offs - 130 - 60 + 22, v_offs + 14), @@ -416,15 +429,15 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): texture=self._menu_icon_texture, opacity=0.8, ) - self._restart_button = restart_button = ba.buttonwidget( + self._restart_button = restart_button = bui.buttonwidget( parent=rootc, autoselect=True, position=(h_offs - 60, v_offs), size=(110, 85), label='', - on_activate_call=ba.WeakCall(self._ui_restart), + on_activate_call=bui.WeakCall(self._ui_restart), ) - ba.imagewidget( + bui.imagewidget( parent=rootc, draw_controller=restart_button, position=(h_offs - 60 + 19, v_offs + 7), @@ -433,22 +446,22 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): opacity=0.8, ) - next_button: ba.Widget | None = None + next_button: bui.Widget | None = None # Our 'next' button is disabled if we haven't unlocked the next # level yet and invisible if there is none. if show_next_button: if self._is_complete: - call = ba.WeakCall(self._ui_next) + call = bui.WeakCall(self._ui_next) button_sound = True image_opacity = 0.8 color = None else: - call = ba.WeakCall(self._ui_error) + call = bui.WeakCall(self._ui_error) button_sound = False image_opacity = 0.2 color = (0.3, 0.3, 0.3) - next_button = ba.buttonwidget( + next_button = bui.buttonwidget( parent=rootc, autoselect=True, position=(h_offs + 130 - 60, v_offs), @@ -458,7 +471,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): color=color, enable_sound=button_sound, ) - ba.imagewidget( + bui.imagewidget( parent=rootc, draw_controller=next_button, position=(h_offs + 130 - 60 + 12, v_offs + 5), @@ -473,7 +486,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): v_offs + 560.0, ) - if ba.app.demo_mode or ba.app.arcade_mode: + if bui.app.demo_mode or bui.app.arcade_mode: self._league_rank_button = None self._store_button_instance = None else: @@ -500,7 +513,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): transition_delay=0.0, ) - ba.containerwidget( + bui.containerwidget( edit=rootc, selected_child=next_button if (self._newly_complete and self._victory and show_next_button) @@ -509,15 +522,12 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ) self._update_corner_button_positions() - self._update_corner_button_positions_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update_corner_button_positions), - repeat=True, - timetype=ba.TimeType.REAL, + self._update_corner_button_positions_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update_corner_button_positions), repeat=True ) def _update_corner_button_positions(self) -> None: - offs = -55 if ba.internal.is_party_icon_visible() else 0 + offs = -55 if bui.is_party_icon_visible() else 0 assert self._corner_button_offs is not None pos_x = self._corner_button_offs[0] + offs pos_y = self._corner_button_offs[1] @@ -533,10 +543,13 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # it wants to do anything special like switch sessions or kill the app. if ( self._allow_server_transition - and ba.app.server is not None + and bs.app.classic is not None + and bs.app.classic.server is not None and self._server_transitioning is None ): - self._server_transitioning = ba.app.server.handle_transition() + self._server_transitioning = ( + bs.app.classic.server.handle_transition() + ) assert isinstance(self._server_transitioning, bool) # If server-mode is handling this, don't do anything ourself. @@ -545,10 +558,10 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # Otherwise restart current level. self._campaign.set_selected_level(self._level_name) - with ba.Context(self): + with self.context: self.end({'outcome': 'restart'}) - def _safe_assign(self, player: ba.Player) -> None: + def _safe_assign(self, player: bs.Player) -> None: # (Only for headless builds). # Just to be extra careful, don't assign if we're transitioning out. @@ -556,24 +569,24 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): if not self.is_transitioning_out() and player: player.assigninput( ( - ba.InputType.JUMP_PRESS, - ba.InputType.PUNCH_PRESS, - ba.InputType.BOMB_PRESS, - ba.InputType.PICK_UP_PRESS, + bs.InputType.JUMP_PRESS, + bs.InputType.PUNCH_PRESS, + bs.InputType.BOMB_PRESS, + bs.InputType.PICK_UP_PRESS, ), self._player_press, ) - def on_player_join(self, player: ba.Player) -> None: + def on_player_join(self, player: bs.Player) -> None: super().on_player_join(player) - if ba.app.server is not None: + if bs.app.classic is not None and bs.app.classic.server is not None: # Host can't press retry button, so anyone can do it instead. time_till_assign = max( - 0, self._birth_time + self._min_view_time - ba.time() + 0, self._birth_time + self._min_view_time - bs.time() ) - ba.timer(time_till_assign, ba.WeakCall(self._safe_assign, player)) + bs.timer(time_till_assign, bs.WeakCall(self._safe_assign, player)) def on_begin(self) -> None: # FIXME: Clean this up. @@ -582,7 +595,10 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # pylint: disable=too-many-locals super().on_begin() - self._begin_time = ba.time() + plus = bs.app.plus + assert plus is not None + + self._begin_time = bs.time() # Calc whether the level is complete and other stuff. levels = self._campaign.levels @@ -596,7 +612,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # Any time we complete a level, set the next one as unlocked. if self._is_complete and self._is_more_levels: - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'COMPLETE_LEVEL', 'campaign': self._campaign.name, @@ -608,30 +624,30 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # If this is the first time we completed it, set the next one # as current. if self._newly_complete: - cfg = ba.app.config + cfg = bs.app.config cfg['Selected Coop Game'] = ( self._campaign.name + ':' + self._next_level_name ) cfg.commit() self._campaign.set_selected_level(self._next_level_name) - ba.timer(1.0, ba.WeakCall(self.request_ui)) + bs.timer(1.0, bs.WeakCall(self.request_ui)) if ( self._is_complete and self._victory and self._is_more_levels - and not (ba.app.demo_mode or ba.app.arcade_mode) + and not (bs.app.demo_mode or bs.app.arcade_mode) ): Text( - ba.Lstr( + bs.Lstr( value='${A}:\n', - subs=[('${A}', ba.Lstr(resource='levelUnlockedText'))], + subs=[('${A}', bs.Lstr(resource='levelUnlockedText'))], ) if self._newly_complete - else ba.Lstr( + else bs.Lstr( value='${A}:\n', - subs=[('${A}', ba.Lstr(resource='nextLevelText'))], + subs=[('${A}', bs.Lstr(resource='nextLevelText'))], ), transition=Text.Transition.IN_RIGHT, transition_delay=5.2, @@ -644,7 +660,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ).autoretain() assert self._next_level_name is not None Text( - ba.Lstr(translate=('coopLevelNames', self._next_level_name)), + bs.Lstr(translate=('coopLevelNames', self._next_level_name)), transition=Text.Transition.IN_RIGHT, transition_delay=5.2, flash=self._newly_complete, @@ -655,17 +671,17 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): position=(270, -255), ).autoretain() if self._newly_complete: - ba.timer(5.2, ba.Call(ba.playsound, self._cashregistersound)) - ba.timer(5.2, ba.Call(ba.playsound, self._dingsound)) + bs.timer(5.2, self._cashregistersound.play) + bs.timer(5.2, self._dingsound.play) offs_x = -195 if len(self._playerinfos) > 1: - pstr = ba.Lstr( + pstr = bs.Lstr( value='- ${A} -', subs=[ ( '${A}', - ba.Lstr( + bs.Lstr( resource='multiPlayerCountText', subs=[('${COUNT}', str(len(self._playerinfos)))], ), @@ -673,9 +689,9 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ], ) else: - pstr = ba.Lstr( + pstr = bs.Lstr( value='- ${A} -', - subs=[('${A}', ba.Lstr(resource='singlePlayerCountText'))], + subs=[('${A}', bs.Lstr(resource='singlePlayerCountText'))], ) ZoomText( self._campaign.getlevel(self._level_name).displayname, @@ -699,12 +715,12 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): position=(0, 230), ).autoretain() - if ba.app.server is None: + if bs.app.classic is not None and bs.app.classic.server is None: # If we're running in normal non-headless build, show this text # because only host can continue the game. - adisp = ba.internal.get_v1_account_display_string() + adisp = plus.get_v1_account_display_string() txt = Text( - ba.Lstr( + bs.Lstr( resource='waitingForHostText', subs=[('${HOST}', adisp)] ), maxwidth=300, @@ -720,7 +736,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): txt.node.client_only = True else: # In headless build, anyone can continue the game. - sval = ba.Lstr(resource='pressAnyButtonPlayAgainText') + sval = bs.Lstr(resource='pressAnyButtonPlayAgainText') Text( sval, v_attach=Text.VAttach.BOTTOM, @@ -735,26 +751,24 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ).autoretain() if self._score is not None: - ba.timer( - 0.35, ba.Call(ba.playsound, self._score_display_sound_small) - ) + bs.timer(0.35, self._score_display_sound_small.play) # Vestigial remain; this stuff should just be instance vars. self._show_info = {} if self._score is not None: - ba.timer(0.8, ba.WeakCall(self._show_score_val, offs_x)) + bs.timer(0.8, bs.WeakCall(self._show_score_val, offs_x)) else: - ba.pushcall(ba.WeakCall(self._show_fail)) + bs.pushcall(bs.WeakCall(self._show_fail)) self._name_str = name_str = ', '.join( [p.name for p in self._playerinfos] ) self._score_loading_status = Text( - ba.Lstr( + bs.Lstr( value='${A}...', - subs=[('${A}', ba.Lstr(resource='loadingText'))], + subs=[('${A}', bs.Lstr(resource='loadingText'))], ), position=(280, 150 + 30), color=(1, 1, 1, 0.4), @@ -764,7 +778,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ) if self._score is not None: - ba.timer(0.4, ba.WeakCall(self._play_drumroll)) + bs.timer(0.4, bs.WeakCall(self._play_drumroll)) # Add us to high scores, filter, and store. our_high_scores_all = self._campaign.getlevel( @@ -794,7 +808,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): reverse=self._score_order == 'increasing', key=lambda x: x[0] ) except Exception: - ba.print_exception('Error sorting scores.') + logging.exception('Error sorting scores.') print(f'our_high_scores: {our_high_scores}') del our_high_scores[10:] @@ -803,7 +817,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): sver = self._campaign.getlevel( self._level_name ).get_score_version_string() - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'SET_LEVEL_LOCAL_HIGH_SCORES', 'campaign': self._campaign.name, @@ -812,20 +826,20 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): 'scores': our_high_scores_all, } ) - if ba.internal.get_v1_account_state() != 'signed_in': + if plus.get_v1_account_state() != 'signed_in': # We expect this only in kiosk mode; complain otherwise. - if not (ba.app.demo_mode or ba.app.arcade_mode): - print('got not-signed-in at score-submit; unexpected') - ba.pushcall(ba.WeakCall(self._got_score_results, None)) + if not (bs.app.demo_mode or bs.app.arcade_mode): + logging.error('got not-signed-in at score-submit; unexpected') + bs.pushcall(bs.WeakCall(self._got_score_results, None)) else: assert self._game_name_str is not None assert self._game_config_str is not None - ba.internal.submit_score( + plus.submit_score( self._game_name_str, self._game_config_str, name_str, self._score, - ba.WeakCall(self._got_score_results), + bs.WeakCall(self._got_score_results), order=self._score_order, tournament_id=self.session.tournament_id, score_type=self._score_type, @@ -834,7 +848,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ) # Apply the transactions we've been adding locally. - ba.internal.run_transactions() + plus.run_v1_account_transactions() # If we're not doing the world's-best button, just show a title # instead. @@ -842,11 +856,11 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ts_h_offs = 210 v_offs = 40 txt = Text( - ba.Lstr(resource='tournamentStandingsText') + bs.Lstr(resource='tournamentStandingsText') if self.session.tournament_id is not None - else ba.Lstr(resource='worldsBestScoresText') + else bs.Lstr(resource='worldsBestScoresText') if self._score_type == 'points' - else ba.Lstr(resource='worldsBestTimesText'), + else bs.Lstr(resource='worldsBestTimesText'), maxwidth=210, position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20), transition=Text.Transition.IN_LEFT, @@ -864,9 +878,9 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ts_h_offs = -480 v_offs = 40 Text( - ba.Lstr(resource='yourBestScoresText') + bs.Lstr(resource='yourBestScoresText') if self._score_type == 'points' - else ba.Lstr(resource='yourBestTimesText'), + else bs.Lstr(resource='yourBestTimesText'), maxwidth=210, position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20), transition=Text.Transition.IN_RIGHT, @@ -912,8 +926,8 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): [p['name'] for p in display_scores[i][1]['players']] ) except Exception: - ba.print_exception( - f'Error calcing name_str for {display_scores}' + logging.exception( + 'Error calcing name_str for %s.', display_scores ) name_str = '-' if display_scores[i] == our_score and not showed_ours: @@ -932,11 +946,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): Text( str(display_scores[i][0]) if self._score_type == 'points' - else ba.timestring( - display_scores[i][0] * 10, - timeformat=ba.TimeFormat.MILLISECONDS, - suppress_format_warning=True, - ), + else bs.timestring((display_scores[i][0] * 10) / 1000.0), position=( ts_h_offs + 20 + h_offs_extra, v_offs_extra @@ -954,7 +964,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ).autoretain() Text( - ba.Lstr(value=name_str), + bs.Lstr(value=name_str), position=( ts_h_offs + 35 + h_offs_extra, v_offs_extra @@ -984,7 +994,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): if self._have_achievements: if not self._account_has_achievements: Text( - ba.Lstr(resource='achievementsText'), + bs.Lstr(resource='achievementsText'), position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 3), maxwidth=210, host_only=True, @@ -995,7 +1005,8 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ).autoretain() assert self._game_name_str is not None - achievements = ba.app.ach.achievements_for_coop_level( + assert bs.app.classic is not None + achievements = bs.app.classic.ach.achievements_for_coop_level( self._game_name_str ) hval = -455 @@ -1006,11 +1017,11 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): vval -= 55 tdelay += 0.250 - ba.timer(5.0, ba.WeakCall(self._show_tips)) + bs.timer(5.0, bs.WeakCall(self._show_tips)) def _play_drumroll(self) -> None: - ba.NodeActor( - ba.newnode( + bs.NodeActor( + bs.newnode( 'sound', attrs={ 'sound': self.drum_roll_sound, @@ -1021,7 +1032,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ).autoretain() def _got_friend_score_results(self, results: list[Any] | None) -> None: - # FIXME: tidy this up # pylint: disable=too-many-locals # pylint: disable=too-many-branches @@ -1030,7 +1040,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # delay a bit if results come in too fast assert self._begin_time is not None - base_delay = max(0, 1.9 - (ba.time() - self._begin_time)) + base_delay = max(0, 1.9 - (bs.time() - self._begin_time)) ts_height = 300 ts_h_offs = -550 v_offs = 30 @@ -1038,7 +1048,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # Report in case of error. if results is None: self._friends_loading_status = Text( - ba.Lstr(resource='friendScoresUnavailableText'), + bs.Lstr(resource='friendScoresUnavailableText'), maxwidth=330, position=(-475, 150 + v_offs), color=(1, 1, 1, 0.4), @@ -1115,9 +1125,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): Text( str(score) if self._score_type == 'points' - else ba.timestring( - score * 10, timeformat=ba.TimeFormat.MILLISECONDS - ), + else bs.timestring((score * 10) / 1000.0), position=( ts_h_offs + 20 + h_offs_extra, v_offs_extra @@ -1138,7 +1146,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): print('Error: got empty name_str on score result:', tval) Text( - ba.Lstr(value=name_str), + bs.Lstr(value=name_str), position=( ts_h_offs + 35 + h_offs_extra, v_offs_extra @@ -1158,25 +1166,27 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ).autoretain() def _got_score_results(self, results: dict[str, Any] | None) -> None: - # FIXME: tidy this up # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements + plus = bs.app.plus + assert plus is not None + # We need to manually run this in the context of our activity # and only if we aren't shutting down. # (really should make the submit_score call handle that stuff itself) if self.expired: return - with ba.Context(self): + with self.context: # Delay a bit if results come in too fast. assert self._begin_time is not None - base_delay = max(0, 2.7 - (ba.time() - self._begin_time)) + base_delay = max(0, 2.7 - (bs.time() - self._begin_time)) v_offs = 20 if results is None: self._score_loading_status = Text( - ba.Lstr(resource='worldScoresUnavailableText'), + bs.Lstr(resource='worldScoresUnavailableText'), position=(230, 150 + v_offs), color=(1, 1, 1, 0.4), transition=Text.Transition.FADE_IN, @@ -1191,7 +1201,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): 'http://' ) and not self._score_link.startswith('https://'): self._score_link = ( - ba.internal.get_master_server_address() + plus.get_master_server_address() + '/' + self._score_link ) @@ -1200,13 +1210,12 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): secs_remaining = results['tournamentSecondsRemaining'] assert isinstance(secs_remaining, int) self._tournament_time_remaining = secs_remaining - self._tournament_time_remaining_text_timer = ba.Timer( + self._tournament_time_remaining_text_timer = bs.BaseTimer( 1.0, - ba.WeakCall( + bs.WeakCall( self._update_tournament_time_remaining_text ), repeat=True, - timetype=ba.TimeType.BASE, ) assert self._show_info is not None @@ -1219,22 +1228,20 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): offs_x = -195 available = self._show_info['results'] is not None if self._score is not None: - ba.timer( + bs.basetimer( (1.5 + base_delay), - ba.WeakCall(self._show_world_rank, offs_x), - timetype=ba.TimeType.BASE, + bs.WeakCall(self._show_world_rank, offs_x), ) ts_h_offs = 200 ts_height = 300 # Show world tops. if available: - # Show the number of games represented by this # list (except for in tournaments). if self.session.tournament_id is None: Text( - ba.Lstr( + bs.Lstr( resource='lastGamesText', subs=[ ( @@ -1304,10 +1311,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): Text( str(score) if self._score_type == 'points' - else ba.timestring( - score * 10, - timeformat=ba.TimeFormat.MILLISECONDS, - ), + else bs.timestring((score * 10) / 1000.0), position=( ts_h_offs + 20 + h_offs_extra, ts_height / 2 @@ -1323,7 +1327,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): transition_delay=tdelay1, ).autoretain() Text( - ba.Lstr(value=name_str), + bs.Lstr(value=name_str), position=( ts_h_offs + 35 + h_offs_extra, ts_height / 2 @@ -1353,9 +1357,8 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): 0, self._tournament_time_remaining - 1 ) if self._tournament_time_remaining_text is not None: - val = ba.timestring( + val = bs.timestring( self._tournament_time_remaining, - suppress_format_warning=True, centi=False, ) self._tournament_time_remaining_text.node.text = val @@ -1365,8 +1368,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements - from ba.internal import get_tournament_prize_strings - + assert bs.app.classic is not None assert self._show_info is not None available = self._show_info['results'] is not None @@ -1394,7 +1396,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # If we've got tournament-seconds-remaining, show it. if self._tournament_time_remaining is not None: Text( - ba.Lstr(resource='coopSelectWindow.timeRemainingText'), + bs.Lstr(resource='coopSelectWindow.timeRemainingText'), position=(-360, -70 - 100), color=(1, 1, 1, 0.7), h_align=Text.HAlign.CENTER, @@ -1418,19 +1420,27 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # If we're a tournament, show prizes. try: + assert bs.app.classic is not None tournament_id = self.session.tournament_id if tournament_id is not None: - if tournament_id in ba.app.accounts_v1.tournament_info: - tourney_info = ba.app.accounts_v1.tournament_info[ + if tournament_id in bs.app.classic.accounts.tournament_info: + tourney_info = bs.app.classic.accounts.tournament_info[ tournament_id ] # pylint: disable=unbalanced-tuple-unpacking - pr1, pv1, pr2, pv2, pr3, pv3 = get_tournament_prize_strings( + ( + pr1, + pv1, + pr2, + pv2, + pr3, + pv3, + ) = bs.app.classic.get_tournament_prize_strings( tourney_info ) # pylint: enable=unbalanced-tuple-unpacking Text( - ba.Lstr(resource='coopSelectWindow.prizesText'), + bs.Lstr(resource='coopSelectWindow.prizesText'), position=(-360, -70 + 77), color=(1, 1, 1, 0.7), h_align=Text.HAlign.CENTER, @@ -1466,12 +1476,12 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ).autoretain() vval -= 35 except Exception: - ba.print_exception('Error showing prize ranges.') + logging.exception('Error showing prize ranges.') if self._do_new_rating: if error: ZoomText( - ba.Lstr(resource='failText'), + bs.Lstr(resource='failText'), flash=True, trail=True, scale=1.0 if available else 0.333, @@ -1482,7 +1492,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): jitter=1.0, ).autoretain() Text( - ba.Lstr(translate=('serverResponses', error)), + bs.Lstr(translate=('serverResponses', error)), position=(0, -140), color=(1, 1, 1, 0.7), h_align=Text.HAlign.CENTER, @@ -1497,7 +1507,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ( ('#' + str(player_rank)) if player_rank is not None - else ba.Lstr(resource='unavailableText') + else bs.Lstr(resource='unavailableText') ), flash=True, trail=True, @@ -1510,9 +1520,9 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ).autoretain() Text( - ba.Lstr( + bs.Lstr( value='${A}:', - subs=[('${A}', ba.Lstr(resource='rankText'))], + subs=[('${A}', bs.Lstr(resource='rankText'))], ), position=(0, 36), maxwidth=300, @@ -1523,7 +1533,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ).autoretain() if best_player_rank is not None: Text( - ba.Lstr( + bs.Lstr( resource='currentStandingText', fallback_resource='bestRankText', subs=[('${RANK}', str(best_player_rank))], @@ -1540,7 +1550,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ( f'{rating:.1f}' if available - else ba.Lstr(resource='unavailableText') + else bs.Lstr(resource='unavailableText') ), flash=True, trail=True, @@ -1561,11 +1571,11 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): stars = 1 else: stars = 0 - star_tex = ba.gettexture('star') + star_tex = bs.gettexture('star') star_x = 135 + offs_x for _i in range(stars): - img = ba.NodeActor( - ba.newnode( + img = bs.NodeActor( + bs.newnode( 'image', attrs={ 'texture': star_tex, @@ -1579,11 +1589,11 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ).autoretain() assert img.node - ba.animate(img.node, 'opacity', {0.15: 0, 0.4: 1}) + bs.animate(img.node, 'opacity', {0.15: 0, 0.4: 1}) star_x += 60 for _i in range(3 - stars): - img = ba.NodeActor( - ba.newnode( + img = bs.NodeActor( + bs.newnode( 'image', attrs={ 'texture': star_tex, @@ -1596,7 +1606,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ) ).autoretain() assert img.node - ba.animate(img.node, 'opacity', {0.15: 0, 0.4: 1}) + bs.animate(img.node, 'opacity', {0.15: 0, 0.4: 1}) star_x += 60 def dostar( @@ -1614,8 +1624,8 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ).autoretain() stx = xval + 20 for _i2 in range(count): - img2 = ba.NodeActor( - ba.newnode( + img2 = bs.NodeActor( + bs.newnode( 'image', attrs={ 'texture': star_tex, @@ -1628,7 +1638,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ) ).autoretain() assert img2.node - ba.animate(img2.node, 'opacity', {1.0: 0.0, 1.5: 0.5}) + bs.animate(img2.node, 'opacity', {1.0: 0.0, 1.5: 0.5}) stx += 13.0 dostar(1, -44 - 30, -112, '0.0') @@ -1641,7 +1651,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): if available: Text( - ba.Lstr( + bs.Lstr( resource='outOfText', subs=[ ( @@ -1663,24 +1673,24 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ).autoretain() new_best = best_rank > self._old_best_rank and best_rank > 0.0 - was_string = ba.Lstr( + was_string = bs.Lstr( value=' ${A}', subs=[ - ('${A}', ba.Lstr(resource='scoreWasText')), + ('${A}', bs.Lstr(resource='scoreWasText')), ('${COUNT}', str(self._old_best_rank)), ], ) if not self._newly_complete: Text( - ba.Lstr( + bs.Lstr( value='${A}${B}', subs=[ - ('${A}', ba.Lstr(resource='newPersonalBestText')), + ('${A}', bs.Lstr(resource='newPersonalBestText')), ('${B}', was_string), ], ) if new_best - else ba.Lstr( + else bs.Lstr( resource='bestRatingText', subs=[('${RATING}', str(best_rank))], ), @@ -1698,9 +1708,9 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ).autoretain() Text( - ba.Lstr( + bs.Lstr( value='${A}:', - subs=[('${A}', ba.Lstr(resource='ratingText'))], + subs=[('${A}', bs.Lstr(resource='ratingText'))], ), position=(0, 36), maxwidth=300, @@ -1710,13 +1720,13 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): transition_delay=0, ).autoretain() - ba.timer(0.35, ba.Call(ba.playsound, self._score_display_sound)) + bs.timer(0.35, self._score_display_sound.play) if not error: - ba.timer(0.35, ba.Call(ba.playsound, self.cymbal_sound)) + bs.timer(0.35, self.cymbal_sound.play) def _show_fail(self) -> None: ZoomText( - ba.Lstr(resource='failText'), + bs.Lstr(resource='failText'), maxwidth=300, flash=False, trail=True, @@ -1735,7 +1745,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): transition=Text.Transition.FADE_IN, transition_delay=1.0, ).autoretain() - ba.timer(0.35, ba.Call(ba.playsound, self._score_display_sound)) + bs.timer(0.35, self._score_display_sound.play) def _show_score_val(self, offs_x: float) -> None: assert self._score_type is not None @@ -1744,9 +1754,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ( str(self._score) if self._score_type == 'points' - else ba.timestring( - self._score * 10, timeformat=ba.TimeFormat.MILLISECONDS - ) + else bs.timestring((self._score * 10) / 1000.0) ), maxwidth=300, flash=True, @@ -1758,14 +1766,14 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): jitter=1.0, ).autoretain() Text( - ba.Lstr( + bs.Lstr( value='${A}:', - subs=[('${A}', ba.Lstr(resource='finalScoreText'))], + subs=[('${A}', bs.Lstr(resource='finalScoreText'))], ) if self._score_type == 'points' - else ba.Lstr( + else bs.Lstr( value='${A}:', - subs=[('${A}', ba.Lstr(resource='finalTimeText'))], + subs=[('${A}', bs.Lstr(resource='finalTimeText'))], ), maxwidth=300, position=(0, 200), @@ -1774,4 +1782,4 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): v_align=Text.VAlign.CENTER, transition_delay=0, ).autoretain() - ba.timer(0.35, ba.Call(ba.playsound, self._score_display_sound)) + bs.timer(0.35, self._score_display_sound.play) diff --git a/assets/src/ba_data/python/bastd/activity/drawscore.py b/src/assets/ba_data/python/bastd/activity/drawscore.py similarity index 81% rename from assets/src/ba_data/python/bastd/activity/drawscore.py rename to src/assets/ba_data/python/bastd/activity/drawscore.py index c9a8c95f..c75a70dc 100644 --- a/assets/src/ba_data/python/bastd/activity/drawscore.py +++ b/src/assets/ba_data/python/bastd/activity/drawscore.py @@ -6,9 +6,10 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba +import babase from bastd.activity.multiteamscore import MultiTeamScoreScreenActivity from bastd.actor.zoomtext import ZoomText +import bascenev1 as bs if TYPE_CHECKING: pass @@ -20,10 +21,10 @@ class DrawScoreScreenActivity(MultiTeamScoreScreenActivity): default_music = None # Awkward silence... def on_begin(self) -> None: - ba.set_analytics_screen('Draw Score Screen') + babase.set_analytics_screen('Draw Score Screen') super().on_begin() ZoomText( - ba.Lstr(resource='drawText'), + babase.Lstr(resource='drawText'), position=(0, 0), maxwidth=400, shiftposition=(-220, 0), @@ -32,5 +33,5 @@ class DrawScoreScreenActivity(MultiTeamScoreScreenActivity): trail=False, jitter=1.0, ).autoretain() - ba.timer(0.35, ba.Call(ba.playsound, self._score_display_sound)) + bs.timer(0.35, self._score_display_sound.play) self.show_player_scores(results=self.settings_raw.get('results', None)) diff --git a/assets/src/ba_data/python/bastd/activity/dualteamscore.py b/src/assets/ba_data/python/bastd/activity/dualteamscore.py similarity index 80% rename from assets/src/ba_data/python/bastd/activity/dualteamscore.py rename to src/assets/ba_data/python/bastd/activity/dualteamscore.py index d00bae88..30ffc916 100644 --- a/assets/src/ba_data/python/bastd/activity/dualteamscore.py +++ b/src/assets/ba_data/python/bastd/activity/dualteamscore.py @@ -6,7 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba +import babase +import bascenev1 as bs from bastd.activity.multiteamscore import MultiTeamScoreScreenActivity from bastd.actor.zoomtext import ZoomText @@ -19,11 +20,11 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): def __init__(self, settings: dict): super().__init__(settings=settings) - self._winner: ba.SessionTeam = settings['winner'] - assert isinstance(self._winner, ba.SessionTeam) + self._winner: bs.SessionTeam = settings['winner'] + assert isinstance(self._winner, bs.SessionTeam) def on_begin(self) -> None: - ba.set_analytics_screen('Teams Score Screen') + babase.set_analytics_screen('Teams Score Screen') super().on_begin() height = 130 @@ -35,14 +36,14 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): # Usually we say 'Best of 7', but if the language prefers we can say # 'First to 4'. session = self.session - assert isinstance(session, ba.MultiTeamSession) - if ba.app.lang.get_resource('bestOfUseFirstToInstead'): - best_txt = ba.Lstr( + assert isinstance(session, bs.MultiTeamSession) + if babase.app.lang.get_resource('bestOfUseFirstToInstead'): + best_txt = babase.Lstr( resource='firstToSeriesText', subs=[('${COUNT}', str(session.get_series_length() / 2 + 1))], ) else: - best_txt = ba.Lstr( + best_txt = babase.Lstr( resource='bestOfSeriesText', subs=[('${COUNT}', str(session.get_series_length()))], ) @@ -60,9 +61,9 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): jitter=3.0, ).autoretain() for team in self.session.sessionteams: - ba.timer( + bs.timer( i * 0.15 + 0.15, - ba.WeakCall( + babase.WeakCall( self._show_team_name, vval - i * height, team, @@ -70,31 +71,25 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): shift_time - (i * 0.150 + 0.150), ), ) - ba.timer( - i * 0.150 + 0.5, - ba.Call(ba.playsound, self._score_display_sound_small), - ) + bs.timer(i * 0.150 + 0.5, self._score_display_sound_small.play) scored = team is self._winner delay = 0.2 if scored: delay = 1.2 - ba.timer( + bs.timer( i * 0.150 + 0.2, - ba.WeakCall( + babase.WeakCall( self._show_team_old_score, vval - i * height, team, shift_time - (i * 0.15 + 0.2), ), ) - ba.timer( - i * 0.15 + 1.5, - ba.Call(ba.playsound, self._score_display_sound), - ) + bs.timer(i * 0.15 + 1.5, self._score_display_sound.play) - ba.timer( + bs.timer( i * 0.150 + delay, - ba.WeakCall( + babase.WeakCall( self._show_team_score, vval - i * height, team, @@ -109,13 +104,13 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): def _show_team_name( self, pos_v: float, - team: ba.SessionTeam, + team: bs.SessionTeam, kill_delay: float, shiftdelay: float, ) -> None: del kill_delay # Unused arg. ZoomText( - ba.Lstr(value='${A}:', subs=[('${A}', team.name)]), + babase.Lstr(value='${A}:', subs=[('${A}', team.name)]), position=(100, pos_v), shiftposition=(-150, pos_v), shiftdelay=shiftdelay, @@ -128,7 +123,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ).autoretain() def _show_team_old_score( - self, pos_v: float, sessionteam: ba.SessionTeam, shiftdelay: float + self, pos_v: float, sessionteam: bs.SessionTeam, shiftdelay: float ) -> None: ZoomText( str(sessionteam.customdata['score'] - 1), @@ -147,7 +142,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): def _show_team_score( self, pos_v: float, - sessionteam: ba.SessionTeam, + sessionteam: bs.SessionTeam, scored: bool, kill_delay: float, shiftdelay: float, diff --git a/assets/src/ba_data/python/bastd/activity/freeforallvictory.py b/src/assets/ba_data/python/bastd/activity/freeforallvictory.py similarity index 85% rename from assets/src/ba_data/python/bastd/activity/freeforallvictory.py rename to src/assets/ba_data/python/bastd/activity/freeforallvictory.py index d4a5c584..7e4c5b46 100644 --- a/assets/src/ba_data/python/bastd/activity/freeforallvictory.py +++ b/src/assets/ba_data/python/bastd/activity/freeforallvictory.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba from bastd.activity.multiteamscore import MultiTeamScoreScreenActivity +import bascenev1 as bs if TYPE_CHECKING: from typing import Any @@ -21,7 +21,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): # Keep prev activity alive while we fade in. self.transition_time = 0.5 - self._cymbal_sound = ba.getsound('cymbal') + self._cymbal_sound = bs.getsound('cymbal') def on_begin(self) -> None: # pylint: disable=too-many-locals @@ -29,7 +29,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): from bastd.actor.text import Text from bastd.actor.image import Image - ba.set_analytics_screen('FreeForAll Score Screen') + bs.set_analytics_screen('FreeForAll Score Screen') super().on_begin() y_base = 100.0 @@ -67,9 +67,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): if order_change: delay3 += 1.5 - ba.timer(0.3, ba.Call(ba.playsound, self._score_display_sound)) + bs.timer(0.3, self._score_display_sound.play) results = self.settings_raw['results'] - assert isinstance(results, ba.GameResults) + assert isinstance(results, bs.GameResults) self.show_player_scores( delay=0.001, results=results, scale=1.2, x_offset=-110.0 ) @@ -107,9 +107,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): transtime2 = 0.250 session = self.session - assert isinstance(session, ba.FreeForAllSession) + assert isinstance(session, bs.FreeForAllSession) title = Text( - ba.Lstr( + bs.Lstr( resource='firstToSeriesText', subs=[('${COUNT}', str(session.get_ffa_series_length()))], ), @@ -127,9 +127,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): v_offs -= 25 v_offs_start = v_offs - ba.timer( + bs.timer( tdelay + delay3, - ba.WeakCall( + bs.WeakCall( self._safe_animate, title.position_combine, 'input0', @@ -142,15 +142,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): for i, player in enumerate(player_order_prev): v_offs_2 = v_offs_start - spacing * (player_order.index(player)) - ba.timer( - tdelay + 0.3, - ba.Call(ba.playsound, self._score_display_sound_small), - ) + bs.timer(tdelay + 0.3, self._score_display_sound_small.play) if order_change: - ba.timer( - tdelay + delay2 + 0.1, - ba.Call(ba.playsound, self._cymbal_sound), - ) + bs.timer(tdelay + delay2 + 0.1, self._cymbal_sound.play) img = Image( player.get_icon(), position=( @@ -161,9 +155,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): transition=Image.Transition.IN_LEFT, transition_delay=tdelay, ).autoretain() - ba.timer( + bs.timer( tdelay + delay2, - ba.WeakCall( + bs.WeakCall( self._safe_animate, img.position_combine, 'input1', @@ -173,9 +167,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): }, ), ) - ba.timer( + bs.timer( tdelay + delay3, - ba.WeakCall( + bs.WeakCall( self._safe_animate, img.position_combine, 'input0', @@ -186,7 +180,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ), ) txt = Text( - ba.Lstr(value=player.getname(full=True)), + bs.Lstr(value=player.getname(full=True)), maxwidth=130.0, scale=0.75 * scale, position=( @@ -195,13 +189,13 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, - color=ba.safecolor(player.team.color + (1,)), + color=bs.safecolor(player.team.color + (1,)), transition=Text.Transition.IN_LEFT, transition_delay=tdelay, ).autoretain() - ba.timer( + bs.timer( tdelay + delay2, - ba.WeakCall( + bs.WeakCall( self._safe_animate, txt.position_combine, 'input1', @@ -211,9 +205,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): }, ), ) - ba.timer( + bs.timer( tdelay + delay3, - ba.WeakCall( + bs.WeakCall( self._safe_animate, txt.position_combine, 'input0', @@ -236,9 +230,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): transition=Text.Transition.IN_LEFT, transition_delay=tdelay, ).autoretain() - ba.timer( + bs.timer( tdelay + delay3, - ba.WeakCall( + bs.WeakCall( self._safe_animate, txt_num.position_combine, 'input0', @@ -257,9 +251,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): 0, 1.0, ) - ba.timer( + bs.timer( tdelay + delay2, - ba.WeakCall( + bs.WeakCall( self._safe_animate, s_txt.position_combine, 'input1', @@ -269,9 +263,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): }, ), ) - ba.timer( + bs.timer( tdelay + delay3, - ba.WeakCall( + bs.WeakCall( self._safe_animate, s_txt.position_combine, 'input0', @@ -298,9 +292,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): 0.7, flash=True, ) - ba.timer( + bs.timer( tdelay + delay2, - ba.WeakCall( + bs.WeakCall( self._safe_animate, s_txt_2.position_combine, 'input1', @@ -310,9 +304,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): }, ), ) - ba.timer( + bs.timer( tdelay + delay3, - ba.WeakCall( + bs.WeakCall( self._safe_animate, s_txt_2.position_combine, 'input0', @@ -324,19 +318,19 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ) def _safesetattr( - node: ba.Node | None, attr: str, value: Any + node: bs.Node | None, attr: str, value: Any ) -> None: if node: setattr(node, attr, value) - ba.timer( + bs.timer( tdelay + delay1, - ba.Call(_safesetattr, s_txt.node, 'color', (1, 1, 1, 1)), + bs.Call(_safesetattr, s_txt.node, 'color', (1, 1, 1, 1)), ) for j in range(score_change): - ba.timer( + bs.timer( (tdelay + delay1 + 0.15 * j), - ba.Call( + bs.Call( _safesetattr, s_txt.node, 'text', @@ -352,17 +346,12 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): tfin = tdelay + delay1 + 0.15 * j if tfin not in sound_times: sound_times.add(tfin) - ba.timer( - tfin, - ba.Call( - ba.playsound, self._score_display_sound_small - ), - ) + bs.timer(tfin, self._score_display_sound_small.play) v_offs -= spacing def _safe_animate( - self, node: ba.Node | None, attr: str, keys: dict[float, float] + self, node: bs.Node | None, attr: str, keys: dict[float, float] ) -> None: """Run an animation on a node if the node still exists.""" if node: - ba.animate(node, attr, keys) + bs.animate(node, attr, keys) diff --git a/assets/src/ba_data/python/bastd/activity/multiteamjoin.py b/src/assets/ba_data/python/bastd/activity/multiteamjoin.py similarity index 80% rename from assets/src/ba_data/python/bastd/activity/multiteamjoin.py rename to src/assets/ba_data/python/bastd/activity/multiteamjoin.py index ae2e7e52..db050da0 100644 --- a/assets/src/ba_data/python/bastd/activity/multiteamjoin.py +++ b/src/assets/ba_data/python/bastd/activity/multiteamjoin.py @@ -6,15 +6,15 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba -from ba.internal import JoinActivity +import babase +import bascenev1 as bs from bastd.actor.text import Text if TYPE_CHECKING: pass -class MultiTeamJoinActivity(JoinActivity): +class MultiTeamJoinActivity(bs.JoinActivity): """Join screen for teams sessions.""" def __init__(self, settings: dict): @@ -23,20 +23,19 @@ class MultiTeamJoinActivity(JoinActivity): def on_transition_in(self) -> None: from bastd.actor.controlsguide import ControlsGuide - from ba import DualTeamSession super().on_transition_in() ControlsGuide(delay=1.0).autoretain() session = self.session - assert isinstance(session, ba.MultiTeamSession) + assert isinstance(session, bs.MultiTeamSession) # Show info about the next up game. self._next_up_text = Text( - ba.Lstr( + babase.Lstr( value='${1} ${2}', subs=[ - ('${1}', ba.Lstr(resource='upFirstText')), + ('${1}', babase.Lstr(resource='upFirstText')), ('${2}', session.get_next_game_description()), ], ), @@ -53,11 +52,11 @@ class MultiTeamJoinActivity(JoinActivity): # In teams mode, show our two team names. # FIXME: Lobby should handle this. - if isinstance(ba.getsession(), DualTeamSession): - team_names = [team.name for team in ba.getsession().sessionteams] + if isinstance(bs.getsession(), bs.DualTeamSession): + team_names = [team.name for team in bs.getsession().sessionteams] team_colors = [ tuple(team.color) + (0.5,) - for team in ba.getsession().sessionteams + for team in bs.getsession().sessionteams ] if len(team_names) == 2: for i in range(2): @@ -73,10 +72,13 @@ class MultiTeamJoinActivity(JoinActivity): ).autoretain() Text( - ba.Lstr( + babase.Lstr( resource='mustInviteFriendsText', subs=[ - ('${GATHER}', ba.Lstr(resource='gatherWindow.titleText')) + ( + '${GATHER}', + babase.Lstr(resource='gatherWindow.titleText'), + ) ], ), h_attach=Text.HAttach.CENTER, diff --git a/assets/src/ba_data/python/bastd/activity/multiteamscore.py b/src/assets/ba_data/python/bastd/activity/multiteamscore.py similarity index 83% rename from assets/src/ba_data/python/bastd/activity/multiteamscore.py rename to src/assets/ba_data/python/bastd/activity/multiteamscore.py index c053660f..d4dbd672 100644 --- a/assets/src/ba_data/python/bastd/activity/multiteamscore.py +++ b/src/assets/ba_data/python/bastd/activity/multiteamscore.py @@ -5,8 +5,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba -from ba.internal import ScoreScreenActivity +import babase +import bascenev1 as bs from bastd.actor.text import Text from bastd.actor.image import Image @@ -14,26 +14,26 @@ if TYPE_CHECKING: pass -class MultiTeamScoreScreenActivity(ScoreScreenActivity): +class MultiTeamScoreScreenActivity(bs.ScoreScreenActivity): """Base class for score screens.""" def __init__(self, settings: dict): super().__init__(settings=settings) - self._score_display_sound = ba.getsound('scoreHit01') - self._score_display_sound_small = ba.getsound('scoreHit02') + self._score_display_sound = bs.getsound('scoreHit01') + self._score_display_sound_small = bs.getsound('scoreHit02') self._show_up_next: bool = True def on_begin(self) -> None: super().on_begin() session = self.session - if self._show_up_next and isinstance(session, ba.MultiTeamSession): - txt = ba.Lstr( + if self._show_up_next and isinstance(session, bs.MultiTeamSession): + txt = babase.Lstr( value='${A} ${B}', subs=[ ( '${A}', - ba.Lstr( + babase.Lstr( resource='upNextText', subs=[ ('${COUNT}', str(session.get_game_number() + 1)) @@ -60,7 +60,7 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity): def show_player_scores( self, delay: float = 2.5, - results: ba.GameResults | None = None, + results: bs.GameResults | None = None, scale: float = 1.0, x_offset: float = 0.0, y_offset: float = 0.0, @@ -74,19 +74,19 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity): tdelay = delay spacing = 40 - is_free_for_all = isinstance(self.session, ba.FreeForAllSession) + is_free_for_all = isinstance(self.session, bs.FreeForAllSession) - def _get_prec_score(p_rec: ba.PlayerRecord) -> int | None: + def _get_prec_score(p_rec: bs.PlayerRecord) -> int | None: if is_free_for_all and results is not None: - assert isinstance(results, ba.GameResults) + assert isinstance(results, bs.GameResults) assert p_rec.team.activityteam is not None val = results.get_sessionteam_score(p_rec.team) return val return p_rec.accumscore - def _get_prec_score_str(p_rec: ba.PlayerRecord) -> str | ba.Lstr: + def _get_prec_score_str(p_rec: bs.PlayerRecord) -> str | babase.Lstr: if is_free_for_all and results is not None: - assert isinstance(results, ba.GameResults) + assert isinstance(results, bs.GameResults) assert p_rec.team.activityteam is not None val = results.get_sessionteam_score_str(p_rec.team) assert val is not None @@ -98,15 +98,14 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity): # (since they're not in results and that's where we pull their # scores from) if results is not None: - assert isinstance(results, ba.GameResults) + assert isinstance(results, bs.GameResults) player_records = [] - assert self.stats valid_players = list(self.stats.get_records().items()) # noinspection PyUnresolvedReferences def _get_player_score_set_entry( - player: ba.SessionPlayer, - ) -> ba.PlayerRecord | None: + player: bs.SessionPlayer, + ) -> bs.PlayerRecord | None: for p_rec in valid_players: if p_rec[1].player is player: return p_rec[1] @@ -129,16 +128,14 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity): for name, p in list(self.stats.get_records().items()) ] player_records_scores.sort(reverse=True) - - # Just want living player entries. - player_records = [p[2] for p in player_records_scores if p[2]] + player_records = [p[2] for p in player_records_scores] voffs = -140.0 + spacing * len(player_records) * 0.5 def _txt( xoffs: float, yoffs: float, - text: ba.Lstr, + text: babase.Lstr, h_align: Text.HAlign = Text.HAlign.RIGHT, extrascale: float = 1.0, maxwidth: float | None = 120.0, @@ -159,8 +156,8 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity): ).autoretain() session = self.session - assert isinstance(session, ba.MultiTeamSession) - tval = ba.Lstr( + assert isinstance(session, bs.MultiTeamSession) + tval = babase.Lstr( resource='gameLeadersText', subs=[('${COUNT}', str(session.get_game_number()))], ) @@ -172,12 +169,14 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity): extrascale=1.4, maxwidth=None, ) - _txt(-15, 4, ba.Lstr(resource='playerText'), h_align=Text.HAlign.LEFT) - _txt(180, 4, ba.Lstr(resource='killsText')) - _txt(280, 4, ba.Lstr(resource='deathsText'), maxwidth=100) + _txt( + -15, 4, babase.Lstr(resource='playerText'), h_align=Text.HAlign.LEFT + ) + _txt(180, 4, babase.Lstr(resource='killsText')) + _txt(280, 4, babase.Lstr(resource='deathsText'), maxwidth=100) score_label = 'Score' if results is None else results.score_label - translated = ba.Lstr(translate=('scoreNames', score_label)) + translated = babase.Lstr(translate=('scoreNames', score_label)) _txt(390, 0, translated) @@ -192,7 +191,7 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity): topkilledcount = min(topkilledcount, prec.accum_killed_count) def _scoretxt( - text: str | ba.Lstr, + text: str | babase.Lstr, x_offs: float, highlight: bool, delay2: float, @@ -229,7 +228,7 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity): transition_delay=tdelay, ).autoretain() Text( - ba.Lstr(value=playerrec.getname(full=True)), + babase.Lstr(value=playerrec.getname(full=True)), maxwidth=160, scale=0.75 * scale, position=( @@ -238,7 +237,7 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity): ), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, - color=ba.safecolor(playerrec.team.color + (1,)), + color=babase.safecolor(playerrec.team.color + (1,)), transition=Text.Transition.IN_LEFT, transition_delay=tdelay, ).autoretain() diff --git a/assets/src/ba_data/python/bastd/activity/multiteamvictory.py b/src/assets/ba_data/python/bastd/activity/multiteamvictory.py similarity index 86% rename from assets/src/ba_data/python/bastd/activity/multiteamvictory.py rename to src/assets/ba_data/python/bastd/activity/multiteamvictory.py index 5ce4d466..ded23027 100644 --- a/assets/src/ba_data/python/bastd/activity/multiteamvictory.py +++ b/src/assets/ba_data/python/bastd/activity/multiteamvictory.py @@ -6,7 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba +import babase +import bascenev1 as bs from bastd.activity.multiteamscore import MultiTeamScoreScreenActivity if TYPE_CHECKING: @@ -22,7 +23,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): def __init__(self, settings: dict): super().__init__(settings=settings) self._min_view_time = 15.0 - self._is_ffa = isinstance(self.session, ba.FreeForAllSession) + self._is_ffa = isinstance(self.session, bs.FreeForAllSession) self._allow_server_transition = True self._tips_text = None self._default_show_tips = False @@ -34,29 +35,30 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): from bastd.actor.text import Text from bastd.actor.image import Image - ba.set_analytics_screen( + babase.set_analytics_screen( 'FreeForAll Series Victory Screen' if self._is_ffa else 'Teams Series Victory Screen' ) - if ba.app.ui.uiscale is ba.UIScale.LARGE: - sval = ba.Lstr(resource='pressAnyKeyButtonPlayAgainText') + assert babase.app.classic is not None + if babase.app.classic.ui.uiscale is babase.UIScale.LARGE: + sval = babase.Lstr(resource='pressAnyKeyButtonPlayAgainText') else: - sval = ba.Lstr(resource='pressAnyButtonPlayAgainText') + sval = babase.Lstr(resource='pressAnyButtonPlayAgainText') self._show_up_next = False self._custom_continue_message = sval super().on_begin() winning_sessionteam = self.settings_raw['winner'] # Pause a moment before playing victory music. - ba.timer(0.6, ba.WeakCall(self._play_victory_music)) - ba.timer( - 4.4, ba.WeakCall(self._show_winner, self.settings_raw['winner']) + bs.timer(0.6, babase.WeakCall(self._play_victory_music)) + bs.timer( + 4.4, babase.WeakCall(self._show_winner, self.settings_raw['winner']) ) - ba.timer(4.6, ba.Call(ba.playsound, self._score_display_sound)) + bs.timer(4.6, self._score_display_sound.play) # Score / Name / Player-record. - player_entries: list[tuple[int, str, ba.PlayerRecord]] = [] + player_entries: list[tuple[int, str, bs.PlayerRecord]] = [] # Note: for ffa, exclude players who haven't entered the game yet. if self._is_ffa: @@ -80,19 +82,19 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): tval = 6.4 t_incr = 0.12 - always_use_first_to = ba.app.lang.get_resource( + always_use_first_to = babase.app.lang.get_resource( 'bestOfUseFirstToInstead' ) session = self.session if self._is_ffa: - assert isinstance(session, ba.FreeForAllSession) - txt = ba.Lstr( + assert isinstance(session, bs.FreeForAllSession) + txt = babase.Lstr( value='${A}:', subs=[ ( '${A}', - ba.Lstr( + babase.Lstr( resource='firstToFinalText', subs=[ ( @@ -105,7 +107,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ], ) else: - assert isinstance(session, ba.MultiTeamSession) + assert isinstance(session, bs.MultiTeamSession) # Some languages may prefer to always show 'first to X' instead of # 'best of X'. @@ -113,12 +115,12 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): # they're not using this language. Should try to come up # with a wording that works everywhere. if always_use_first_to: - txt = ba.Lstr( + txt = babase.Lstr( value='${A}:', subs=[ ( '${A}', - ba.Lstr( + babase.Lstr( resource='firstToFinalText', subs=[ ( @@ -133,12 +135,12 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ], ) else: - txt = ba.Lstr( + txt = babase.Lstr( value='${A}:', subs=[ ( '${A}', - ba.Lstr( + babase.Lstr( resource='bestOfFinalText', subs=[ ( @@ -171,7 +173,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): if not self._is_ffa: Text( - ba.Lstr( + babase.Lstr( resource='gamesToText', subs=[ ('${WINCOUNT}', str(win_score)), @@ -193,7 +195,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): else: v_extra = 0 - mvp: ba.PlayerRecord | None = None + mvp: bs.PlayerRecord | None = None mvp_name: str | None = None # Show game MVP. @@ -206,7 +208,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): break if mvp is not None: Text( - ba.Lstr(resource='mostValuablePlayerText'), + babase.Lstr(resource='mostValuablePlayerText'), color=(0.5, 0.5, 0.5, 1.0), v_align=Text.VAlign.CENTER, maxwidth=300, @@ -226,13 +228,13 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ).autoretain() assert mvp_name is not None Text( - ba.Lstr(value=mvp_name), + babase.Lstr(value=mvp_name), position=(280, ts_height / 2 - 55 + 15 - 5), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, maxwidth=170, scale=1.3, - color=ba.safecolor(mvp.team.color + (1,)), + color=babase.safecolor(mvp.team.color + (1,)), transition=Text.Transition.IN_LEFT, transition_delay=tval, ).autoretain() @@ -247,7 +249,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): most_kills = entry[2].kill_count if mvp is not None: Text( - ba.Lstr(resource='mostViolentPlayerText'), + babase.Lstr(resource='mostViolentPlayerText'), color=(0.5, 0.5, 0.5, 1.0), v_align=Text.VAlign.CENTER, maxwidth=300, @@ -257,12 +259,12 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): transition_delay=tval, ).autoretain() Text( - ba.Lstr( + babase.Lstr( value='(${A})', subs=[ ( '${A}', - ba.Lstr( + babase.Lstr( resource='killsTallyText', subs=[('${COUNT}', str(most_kills))], ), @@ -287,12 +289,12 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ).autoretain() assert mvp_name is not None Text( - ba.Lstr(value=mvp_name), + babase.Lstr(value=mvp_name), position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, maxwidth=180, - color=ba.safecolor(mvp.team.color + (1,)), + color=babase.safecolor(mvp.team.color + (1,)), transition=Text.Transition.IN_LEFT, transition_delay=tval, ).autoretain() @@ -308,7 +310,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): most_killed = entry[2].killed_count if mkp is not None: Text( - ba.Lstr(resource='mostViolatedPlayerText'), + babase.Lstr(resource='mostViolatedPlayerText'), color=(0.5, 0.5, 0.5, 1.0), v_align=Text.VAlign.CENTER, maxwidth=300, @@ -318,12 +320,12 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): transition_delay=tval, ).autoretain() Text( - ba.Lstr( + babase.Lstr( value='(${A})', subs=[ ( '${A}', - ba.Lstr( + babase.Lstr( resource='deathsTallyText', subs=[('${COUNT}', str(most_killed))], ), @@ -347,11 +349,11 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): ).autoretain() assert mkp_name is not None Text( - ba.Lstr(value=mkp_name), + babase.Lstr(value=mkp_name), position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, - color=ba.safecolor(mkp.team.color + (1,)), + color=babase.safecolor(mkp.team.color + (1,)), maxwidth=180, transition=Text.Transition.IN_LEFT, transition_delay=tval, @@ -361,7 +363,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): # Now show individual scores. tdelay = tval Text( - ba.Lstr(resource='finalScoresText'), + babase.Lstr(resource='finalScoresText'), color=(0.5, 0.5, 0.5, 1.0), position=(ts_h_offs, ts_height / 2), transition=Text.Transition.IN_RIGHT, @@ -394,17 +396,17 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): transition_delay=tdelay, ).autoretain() Text( - ba.Lstr(value=name), + babase.Lstr(value=name), position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, maxwidth=180, - color=ba.safecolor(prec.team.color + (1,)), + color=babase.safecolor(prec.team.color + (1,)), transition=Text.Transition.IN_RIGHT, transition_delay=tdelay, ).autoretain() - ba.timer(15.0, ba.WeakCall(self._show_tips)) + bs.timer(15.0, babase.WeakCall(self._show_tips)) def _show_tips(self) -> None: from bastd.actor.tipstext import TipsText @@ -412,12 +414,11 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): self._tips_text = TipsText(offs_y=70) def _play_victory_music(self) -> None: - # Make sure we don't stomp on the next activity's music choice. if not self.is_transitioning_out(): - ba.setmusic(ba.MusicType.VICTORY) + bs.setmusic(bs.MusicType.VICTORY) - def _show_winner(self, team: ba.SessionTeam) -> None: + def _show_winner(self, team: bs.SessionTeam) -> None: from bastd.actor.image import Image from bastd.actor.zoomtext import ZoomText @@ -440,9 +441,9 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): scale=(100, 100), ).autoretain() assert i.node - ba.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0}) + bs.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0}) ZoomText( - ba.Lstr( + babase.Lstr( value=team.players[0].getname(full=True, icon=False) ), position=(0, 97 + offs_v), @@ -455,11 +456,11 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): s_extra = 1.0 if self._is_ffa else 1.0 # Some languages say "FOO WINS" differently for teams vs players. - if isinstance(self.session, ba.FreeForAllSession): + if isinstance(self.session, bs.FreeForAllSession): wins_resource = 'seriesWinLine1PlayerText' else: wins_resource = 'seriesWinLine1TeamText' - wins_text = ba.Lstr(resource=wins_resource) + wins_text = babase.Lstr(resource=wins_resource) # Temp - if these come up as the english default, fall-back to the # unified old form which is more likely to be translated. @@ -472,7 +473,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): maxwidth=250, ).autoretain() ZoomText( - ba.Lstr(resource='seriesWinLine2Text'), + babase.Lstr(resource='seriesWinLine2Text'), position=(0, -110 + offs_v), scale=1.0 * s_extra, color=team.color, diff --git a/assets/src/ba_data/python/bastd/actor/__init__.py b/src/assets/ba_data/python/bastd/actor/__init__.py similarity index 100% rename from assets/src/ba_data/python/bastd/actor/__init__.py rename to src/assets/ba_data/python/bastd/actor/__init__.py diff --git a/assets/src/ba_data/python/bastd/actor/background.py b/src/assets/ba_data/python/bastd/actor/background.py similarity index 80% rename from assets/src/ba_data/python/bastd/actor/background.py rename to src/assets/ba_data/python/bastd/actor/background.py index 4c99717a..49b63dad 100644 --- a/assets/src/ba_data/python/bastd/actor/background.py +++ b/src/assets/ba_data/python/bastd/actor/background.py @@ -6,15 +6,16 @@ from __future__ import annotations import random import weakref +import logging from typing import TYPE_CHECKING -import ba +import bascenev1 as bs if TYPE_CHECKING: from typing import Any -class Background(ba.Actor): +class Background(bs.Actor): """Simple Fading Background Actor.""" def __init__( @@ -30,38 +31,38 @@ class Background(ba.Actor): # scene instead of the activity scene. # This way we can overlap multiple activities for fades # and whatnot. - session = ba.getsession() + session = bs.getsession() self._session = weakref.ref(session) - with ba.Context(session): - self.node = ba.newnode( + with session.context: + self.node = bs.newnode( 'image', delegate=self, attrs={ 'fill_screen': True, - 'texture': ba.gettexture('bg'), + 'texture': bs.gettexture('bg'), 'tilt_translate': -0.3, 'has_alpha_channel': False, 'color': (1, 1, 1), }, ) if not start_faded: - ba.animate( + bs.animate( self.node, 'opacity', {0.0: 0.0, self.fade_time: 1.0}, loop=False, ) if show_logo: - logo_texture = ba.gettexture('logo') - logo_model = ba.getmodel('logo') - logo_model_transparent = ba.getmodel('logoTransparent') - self.logo = ba.newnode( + logo_texture = bs.gettexture('logo') + logo_mesh = bs.getmesh('logo') + logo_mesh_transparent = bs.getmesh('logoTransparent') + self.logo = bs.newnode( 'image', owner=self.node, attrs={ 'texture': logo_texture, - 'model_opaque': logo_model, - 'model_transparent': logo_model_transparent, + 'mesh_opaque': logo_mesh, + 'mesh_transparent': logo_mesh_transparent, 'scale': (0.7, 0.7), 'vr_depth': -250, 'color': (0.15, 0.15, 0.15), @@ -73,19 +74,19 @@ class Background(ba.Actor): self.node.connectattr('opacity', self.logo, 'opacity') # add jitter/pulse for a stop-motion-y look unless we're in VR # in which case stillness is better - if not ba.app.vr_mode: - self.cmb = ba.newnode( + if not bs.app.vr_mode: + self.cmb = bs.newnode( 'combine', owner=self.node, attrs={'size': 2} ) for attr in ['input0', 'input1']: - ba.animate( + bs.animate( self.cmb, attr, {0.0: 0.693, 0.05: 0.7, 0.5: 0.693}, loop=True, ) self.cmb.connectattr('output', self.logo, 'scale') - cmb = ba.newnode( + cmb = bs.newnode( 'combine', owner=self.node, attrs={'size': 2} ) cmb.connectattr('output', self.logo, 'position') @@ -95,13 +96,13 @@ class Background(ba.Actor): for _i in range(10): keys[timeval] = (random.random() - 0.5) * 0.0015 timeval += random.random() * 0.1 - ba.animate(cmb, 'input0', keys, loop=True) + bs.animate(cmb, 'input0', keys, loop=True) keys = {} timeval = 0.0 for _i in range(10): keys[timeval] = (random.random() - 0.5) * 0.0015 + 0.05 timeval += random.random() * 0.1 - ba.animate(cmb, 'input1', keys, loop=True) + bs.animate(cmb, 'input1', keys, loop=True) def __del__(self) -> None: # Normal actors don't get sent DieMessages when their @@ -118,28 +119,28 @@ class Background(ba.Actor): # since it was part of the session's scene. # Let's make sure that's the case. # (since otherwise we have no way to kill it) - ba.print_error( + logging.exception( 'got None session on Background _die' ' (and node still exists!)' ) elif session is not None: - with ba.Context(session): + with session.context: if not self._dying and self.node: self._dying = True if immediate: self.node.delete() else: - ba.animate( + bs.animate( self.node, 'opacity', {0.0: 1.0, self.fade_time: 0.0}, loop=False, ) - ba.timer(self.fade_time + 0.1, self.node.delete) + bs.timer(self.fade_time + 0.1, self.node.delete) def handlemessage(self, msg: Any) -> Any: assert not self.expired - if isinstance(msg, ba.DieMessage): + if isinstance(msg, bs.DieMessage): self._die(msg.immediate) else: super().handlemessage(msg) diff --git a/assets/src/ba_data/python/bastd/actor/bomb.py b/src/assets/ba_data/python/bastd/actor/bomb.py similarity index 75% rename from assets/src/ba_data/python/bastd/actor/bomb.py rename to src/assets/ba_data/python/bastd/actor/bomb.py index 43becf1c..ac99c424 100644 --- a/assets/src/ba_data/python/bastd/actor/bomb.py +++ b/src/assets/ba_data/python/bastd/actor/bomb.py @@ -10,19 +10,17 @@ from __future__ import annotations import random from typing import TYPE_CHECKING, TypeVar -import ba +import bascenev1 as bs from bastd.gameutils import SharedObjects if TYPE_CHECKING: from typing import Any, Sequence, Callable -# pylint: disable=invalid-name -PlayerType = TypeVar('PlayerType', bound='ba.Player') -# pylint: enable=invalid-name +PlayerT = TypeVar('PlayerT', bound='bs.Player') class BombFactory: - """Wraps up media and other resources used by ba.Bombs. + """Wraps up media and other resources used by bs.Bombs. Category: **Gameplay Classes** @@ -30,110 +28,110 @@ class BombFactory: and can be retrieved via bastd.actor.bomb.get_factory(). """ - bomb_model: ba.Model - """The ba.Model of a standard or ice bomb.""" + bomb_mesh: bs.Mesh + """The bs.Mesh of a standard or ice bomb.""" - sticky_bomb_model: ba.Model - """The ba.Model of a sticky-bomb.""" + sticky_bomb_mesh: bs.Mesh + """The bs.Mesh of a sticky-bomb.""" - impact_bomb_model: ba.Model - """The ba.Model of an impact-bomb.""" + impact_bomb_mesh: bs.Mesh + """The bs.Mesh of an impact-bomb.""" - land_mine_model: ba.Model - """The ba.Model of a land-mine.""" + land_mine_mesh: bs.Mesh + """The bs.Mesh of a land-mine.""" - tnt_model: ba.Model - """The ba.Model of a tnt box.""" + tnt_mesh: bs.Mesh + """The bs.Mesh of a tnt box.""" - regular_tex: ba.Texture - """The ba.Texture for regular bombs.""" + regular_tex: bs.Texture + """The bs.Texture for regular bombs.""" - ice_tex: ba.Texture - """The ba.Texture for ice bombs.""" + ice_tex: bs.Texture + """The bs.Texture for ice bombs.""" - sticky_tex: ba.Texture - """The ba.Texture for sticky bombs.""" + sticky_tex: bs.Texture + """The bs.Texture for sticky bombs.""" - impact_tex: ba.Texture - """The ba.Texture for impact bombs.""" + impact_tex: bs.Texture + """The bs.Texture for impact bombs.""" - impact_lit_tex: ba.Texture - """The ba.Texture for impact bombs with lights lit.""" + impact_lit_tex: bs.Texture + """The bs.Texture for impact bombs with lights lit.""" - land_mine_tex: ba.Texture - """The ba.Texture for land-mines.""" + land_mine_tex: bs.Texture + """The bs.Texture for land-mines.""" - land_mine_lit_tex: ba.Texture - """The ba.Texture for land-mines with the light lit.""" + land_mine_lit_tex: bs.Texture + """The bs.Texture for land-mines with the light lit.""" - tnt_tex: ba.Texture - """The ba.Texture for tnt boxes.""" + tnt_tex: bs.Texture + """The bs.Texture for tnt boxes.""" - hiss_sound: ba.Sound - """The ba.Sound for the hiss sound an ice bomb makes.""" + hiss_sound: bs.Sound + """The bs.Sound for the hiss sound an ice bomb makes.""" - debris_fall_sound: ba.Sound - """The ba.Sound for random falling debris after an explosion.""" + debris_fall_sound: bs.Sound + """The bs.Sound for random falling debris after an explosion.""" - wood_debris_fall_sound: ba.Sound - """A ba.Sound for random wood debris falling after an explosion.""" + wood_debris_fall_sound: bs.Sound + """A bs.Sound for random wood debris falling after an explosion.""" - explode_sounds: Sequence[ba.Sound] - """A tuple of ba.Sound-s for explosions.""" + explode_sounds: Sequence[bs.Sound] + """A tuple of bs.Sound-s for explosions.""" - freeze_sound: ba.Sound - """A ba.Sound of an ice bomb freezing something.""" + freeze_sound: bs.Sound + """A bs.Sound of an ice bomb freezing something.""" - fuse_sound: ba.Sound - """A ba.Sound of a burning fuse.""" + fuse_sound: bs.Sound + """A bs.Sound of a burning fuse.""" - activate_sound: ba.Sound - """A ba.Sound for an activating impact bomb.""" + activate_sound: bs.Sound + """A bs.Sound for an activating impact bomb.""" - warn_sound: ba.Sound - """A ba.Sound for an impact bomb about to explode due to time-out.""" + warn_sound: bs.Sound + """A bs.Sound for an impact bomb about to explode due to time-out.""" - bomb_material: ba.Material - """A ba.Material applied to all bombs.""" + bomb_material: bs.Material + """A bs.Material applied to all bombs.""" - normal_sound_material: ba.Material - """A ba.Material that generates standard bomb noises on impacts, etc.""" + normal_sound_material: bs.Material + """A bs.Material that generates standard bomb noises on impacts, etc.""" - sticky_material: ba.Material - """A ba.Material that makes 'splat' sounds and makes collisions softer.""" + sticky_material: bs.Material + """A bs.Material that makes 'splat' sounds and makes collisions softer.""" - land_mine_no_explode_material: ba.Material - """A ba.Material that keeps land-mines from blowing up. + land_mine_no_explode_material: bs.Material + """A bs.Material that keeps land-mines from blowing up. Applied to land-mines when they are created to allow land-mines to touch without exploding.""" - land_mine_blast_material: ba.Material - """A ba.Material applied to activated land-mines that causes them to + land_mine_blast_material: bs.Material + """A bs.Material applied to activated land-mines that causes them to explode on impact.""" - impact_blast_material: ba.Material - """A ba.Material applied to activated impact-bombs that causes them to + impact_blast_material: bs.Material + """A bs.Material applied to activated impact-bombs that causes them to explode on impact.""" - blast_material: ba.Material - """A ba.Material applied to bomb blast geometry which triggers impact + blast_material: bs.Material + """A bs.Material applied to bomb blast geometry which triggers impact events with what it touches.""" - dink_sounds: Sequence[ba.Sound] - """A tuple of ba.Sound-s for when bombs hit the ground.""" + dink_sounds: Sequence[bs.Sound] + """A tuple of bs.Sound-s for when bombs hit the ground.""" - sticky_impact_sound: ba.Sound - """The ba.Sound for a squish made by a sticky bomb hitting something.""" + sticky_impact_sound: bs.Sound + """The bs.Sound for a squish made by a sticky bomb hitting something.""" - roll_sound: ba.Sound - """ba.Sound for a rolling bomb.""" + roll_sound: bs.Sound + """bs.Sound for a rolling bomb.""" - _STORENAME = ba.storagename() + _STORENAME = bs.storagename() @classmethod def get(cls) -> BombFactory: """Get/create a shared bastd.actor.bomb.BombFactory object.""" - activity = ba.getactivity() + activity = bs.getactivity() factory = activity.customdata.get(cls._STORENAME) if factory is None: factory = BombFactory() @@ -141,8 +139,8 @@ class BombFactory: assert isinstance(factory, BombFactory) return factory - def random_explode_sound(self) -> ba.Sound: - """Return a random explosion ba.Sound from the factory.""" + def random_explode_sound(self) -> bs.Sound: + """Return a random explosion bs.Sound from the factory.""" return self.explode_sounds[random.randrange(len(self.explode_sounds))] def __init__(self) -> None: @@ -153,43 +151,43 @@ class BombFactory: """ shared = SharedObjects.get() - self.bomb_model = ba.getmodel('bomb') - self.sticky_bomb_model = ba.getmodel('bombSticky') - self.impact_bomb_model = ba.getmodel('impactBomb') - self.land_mine_model = ba.getmodel('landMine') - self.tnt_model = ba.getmodel('tnt') + self.bomb_mesh = bs.getmesh('bomb') + self.sticky_bomb_mesh = bs.getmesh('bombSticky') + self.impact_bomb_mesh = bs.getmesh('impactBomb') + self.land_mine_mesh = bs.getmesh('landMine') + self.tnt_mesh = bs.getmesh('tnt') - self.regular_tex = ba.gettexture('bombColor') - self.ice_tex = ba.gettexture('bombColorIce') - self.sticky_tex = ba.gettexture('bombStickyColor') - self.impact_tex = ba.gettexture('impactBombColor') - self.impact_lit_tex = ba.gettexture('impactBombColorLit') - self.land_mine_tex = ba.gettexture('landMine') - self.land_mine_lit_tex = ba.gettexture('landMineLit') - self.tnt_tex = ba.gettexture('tnt') + self.regular_tex = bs.gettexture('bombColor') + self.ice_tex = bs.gettexture('bombColorIce') + self.sticky_tex = bs.gettexture('bombStickyColor') + self.impact_tex = bs.gettexture('impactBombColor') + self.impact_lit_tex = bs.gettexture('impactBombColorLit') + self.land_mine_tex = bs.gettexture('landMine') + self.land_mine_lit_tex = bs.gettexture('landMineLit') + self.tnt_tex = bs.gettexture('tnt') - self.hiss_sound = ba.getsound('hiss') - self.debris_fall_sound = ba.getsound('debrisFall') - self.wood_debris_fall_sound = ba.getsound('woodDebrisFall') + self.hiss_sound = bs.getsound('hiss') + self.debris_fall_sound = bs.getsound('debrisFall') + self.wood_debris_fall_sound = bs.getsound('woodDebrisFall') self.explode_sounds = ( - ba.getsound('explosion01'), - ba.getsound('explosion02'), - ba.getsound('explosion03'), - ba.getsound('explosion04'), - ba.getsound('explosion05'), + bs.getsound('explosion01'), + bs.getsound('explosion02'), + bs.getsound('explosion03'), + bs.getsound('explosion04'), + bs.getsound('explosion05'), ) - self.freeze_sound = ba.getsound('freeze') - self.fuse_sound = ba.getsound('fuse01') - self.activate_sound = ba.getsound('activateBeep') - self.warn_sound = ba.getsound('warnBeep') + self.freeze_sound = bs.getsound('freeze') + self.fuse_sound = bs.getsound('fuse01') + self.activate_sound = bs.getsound('activateBeep') + self.warn_sound = bs.getsound('warnBeep') # Set up our material so new bombs don't collide with objects # that they are initially overlapping. - self.bomb_material = ba.Material() - self.normal_sound_material = ba.Material() - self.sticky_material = ba.Material() + self.bomb_material = bs.Material() + self.normal_sound_material = bs.Material() + self.sticky_material = bs.Material() self.bomb_material.add_actions( conditions=( @@ -215,8 +213,8 @@ class BombFactory: actions=('modify_part_collision', 'friction', 0.3) ) - self.land_mine_no_explode_material = ba.Material() - self.land_mine_blast_material = ba.Material() + self.land_mine_no_explode_material = bs.Material() + self.land_mine_blast_material = bs.Material() self.land_mine_blast_material.add_actions( conditions=( ('we_are_older_than', 200), @@ -241,7 +239,7 @@ class BombFactory: actions=('message', 'our_node', 'at_connect', ImpactMessage()), ) - self.impact_blast_material = ba.Material() + self.impact_blast_material = bs.Material() self.impact_blast_material.add_actions( conditions=( ('we_are_older_than', 200), @@ -259,7 +257,7 @@ class BombFactory: actions=('message', 'our_node', 'at_connect', ImpactMessage()), ) - self.blast_material = ba.Material() + self.blast_material = bs.Material() self.blast_material.add_actions( conditions=('they_have_material', shared.object_material), actions=( @@ -270,11 +268,11 @@ class BombFactory: ) self.dink_sounds = ( - ba.getsound('bombDrop01'), - ba.getsound('bombDrop02'), + bs.getsound('bombDrop01'), + bs.getsound('bombDrop02'), ) - self.sticky_impact_sound = ba.getsound('stickyImpact') - self.roll_sound = ba.getsound('bombRoll01') + self.sticky_impact_sound = bs.getsound('stickyImpact') + self.roll_sound = bs.getsound('bombRoll01') # Collision sounds. self.normal_sound_material.add_actions( @@ -326,7 +324,7 @@ class ExplodeHitMessage: """Tell an object it was hit by an explosion.""" -class Blast(ba.Actor): +class Blast(bs.Actor): """An explosion, as generated by a bomb or some other object. category: Gameplay Classes @@ -338,7 +336,7 @@ class Blast(ba.Actor): velocity: Sequence[float] = (0.0, 0.0, 0.0), blast_radius: float = 2.0, blast_type: str = 'normal', - source_player: ba.Player | None = None, + source_player: bs.Player | None = None, hit_type: str = 'explosion', hit_subtype: str = 'normal', ): @@ -361,7 +359,7 @@ class Blast(ba.Actor): # Set our position a bit lower so we throw more things upward. rmats = (factory.blast_material, shared.attack_material) - self.node = ba.newnode( + self.node = bs.newnode( 'region', delegate=self, attrs={ @@ -372,11 +370,11 @@ class Blast(ba.Actor): }, ) - ba.timer(0.05, self.node.delete) + bs.timer(0.05, self.node.delete) # Throw in an explosion and flash. evel = (velocity[0], max(-1.0, velocity[1]), velocity[2]) - explosion = ba.newnode( + explosion = bs.newnode( 'explosion', attrs={ 'position': position, @@ -388,24 +386,24 @@ class Blast(ba.Actor): if self.blast_type == 'ice': explosion.color = (0, 0.05, 0.4) - ba.timer(1.0, explosion.delete) + bs.timer(1.0, explosion.delete) if self.blast_type != 'ice': - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=int(1.0 + random.random() * 4), emit_type='tendrils', tendril_type='thin_smoke', ) - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=int(4.0 + random.random() * 4), emit_type='tendrils', tendril_type='ice' if self.blast_type == 'ice' else 'smoke', ) - ba.emitfx( + bs.emitfx( position=position, emit_type='distortion', spread=1.0 if self.blast_type == 'tnt' else 2.0, @@ -415,7 +413,7 @@ class Blast(ba.Actor): if self.blast_type == 'ice': def emit() -> None: - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=30, @@ -426,19 +424,19 @@ class Blast(ba.Actor): ) # It looks better if we delay a bit. - ba.timer(0.05, emit) + bs.timer(0.05, emit) elif self.blast_type == 'sticky': def emit() -> None: - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=int(4.0 + random.random() * 8), spread=0.7, chunk_type='slime', ) - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=int(4.0 + random.random() * 8), @@ -446,7 +444,7 @@ class Blast(ba.Actor): spread=0.7, chunk_type='slime', ) - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=15, @@ -454,7 +452,7 @@ class Blast(ba.Actor): chunk_type='slime', emit_type='stickers', ) - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=20, @@ -462,7 +460,7 @@ class Blast(ba.Actor): chunk_type='spark', emit_type='stickers', ) - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=int(6.0 + random.random() * 12), @@ -472,26 +470,26 @@ class Blast(ba.Actor): ) # It looks better if we delay a bit. - ba.timer(0.05, emit) + bs.timer(0.05, emit) elif self.blast_type == 'impact': def emit() -> None: - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=int(4.0 + random.random() * 8), scale=0.8, chunk_type='metal', ) - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=int(4.0 + random.random() * 8), scale=0.4, chunk_type='metal', ) - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=20, @@ -499,7 +497,7 @@ class Blast(ba.Actor): chunk_type='spark', emit_type='stickers', ) - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=int(8.0 + random.random() * 15), @@ -509,26 +507,26 @@ class Blast(ba.Actor): ) # It looks better if we delay a bit. - ba.timer(0.05, emit) + bs.timer(0.05, emit) else: # Regular or land mine bomb shrapnel. def emit() -> None: if self.blast_type != 'tnt': - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=int(4.0 + random.random() * 8), chunk_type='rock', ) - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=int(4.0 + random.random() * 8), scale=0.5, chunk_type='rock', ) - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=30, @@ -536,7 +534,7 @@ class Blast(ba.Actor): chunk_type='spark', emit_type='stickers', ) - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=int(18.0 + random.random() * 20), @@ -549,7 +547,7 @@ class Blast(ba.Actor): if self.blast_type == 'tnt': def emit_splinters() -> None: - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=int(20.0 + random.random() * 25), @@ -558,13 +556,13 @@ class Blast(ba.Actor): chunk_type='splinter', ) - ba.timer(0.01, emit_splinters) + bs.timer(0.01, emit_splinters) # Every now and then do a sparky one. if self.blast_type == 'tnt' or random.random() < 0.1: def emit_extra_sparks() -> None: - ba.emitfx( + bs.emitfx( position=position, velocity=velocity, count=int(10.0 + random.random() * 20), @@ -573,13 +571,13 @@ class Blast(ba.Actor): chunk_type='spark', ) - ba.timer(0.02, emit_extra_sparks) + bs.timer(0.02, emit_extra_sparks) # It looks better if we delay a bit. - ba.timer(0.05, emit) + bs.timer(0.05, emit) lcolor = (0.6, 0.6, 1.0) if self.blast_type == 'ice' else (1, 0.3, 0.1) - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': position, @@ -596,7 +594,7 @@ class Blast(ba.Actor): scl *= 3.0 iscale = 1.6 - ba.animate( + bs.animate( light, 'intensity', { @@ -611,7 +609,7 @@ class Blast(ba.Actor): scl * 3.0: 0.0, }, ) - ba.animate( + bs.animate( light, 'radius', { @@ -622,10 +620,10 @@ class Blast(ba.Actor): scl * 1.0: light_radius * 0.05, }, ) - ba.timer(scl * 3.0, light.delete) + bs.timer(scl * 3.0, light.delete) # Make a scorch that fades over time. - scorch = ba.newnode( + scorch = bs.newnode( 'scorch', attrs={ 'position': position, @@ -636,42 +634,42 @@ class Blast(ba.Actor): if self.blast_type == 'ice': scorch.color = (1, 1, 1.5) - ba.animate(scorch, 'presence', {3.000: 1, 13.000: 0}) - ba.timer(13.0, scorch.delete) + bs.animate(scorch, 'presence', {3.000: 1, 13.000: 0}) + bs.timer(13.0, scorch.delete) if self.blast_type == 'ice': - ba.playsound(factory.hiss_sound, position=light.position) + factory.hiss_sound.play(position=light.position) lpos = light.position - ba.playsound(factory.random_explode_sound(), position=lpos) - ba.playsound(factory.debris_fall_sound, position=lpos) + factory.random_explode_sound().play(position=lpos) + factory.debris_fall_sound.play(position=lpos) - ba.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0) + bs.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0) # TNT is more epic. if self.blast_type == 'tnt': - ba.playsound(factory.random_explode_sound(), position=lpos) + factory.random_explode_sound().play(position=lpos) def _extra_boom() -> None: - ba.playsound(factory.random_explode_sound(), position=lpos) + factory.random_explode_sound().play(position=lpos) - ba.timer(0.25, _extra_boom) + bs.timer(0.25, _extra_boom) def _extra_debris_sound() -> None: - ba.playsound(factory.debris_fall_sound, position=lpos) - ba.playsound(factory.wood_debris_fall_sound, position=lpos) + factory.debris_fall_sound.play(position=lpos) + factory.wood_debris_fall_sound.play(position=lpos) - ba.timer(0.4, _extra_debris_sound) + bs.timer(0.4, _extra_debris_sound) def handlemessage(self, msg: Any) -> Any: assert not self.expired - if isinstance(msg, ba.DieMessage): + if isinstance(msg, bs.DieMessage): if self.node: self.node.delete() elif isinstance(msg, ExplodeHitMessage): - node = ba.getcollision().opposingnode + node = bs.getcollision().opposingnode assert self.node nodepos = self.node.position mag = 2000.0 @@ -683,28 +681,26 @@ class Blast(ba.Actor): mag *= 2.0 node.handlemessage( - ba.HitMessage( + bs.HitMessage( pos=nodepos, velocity=(0, 0, 0), magnitude=mag, hit_type=self.hit_type, hit_subtype=self.hit_subtype, radius=self.radius, - source_player=ba.existing(self._source_player), + source_player=bs.existing(self._source_player), ) ) if self.blast_type == 'ice': - ba.playsound( - BombFactory.get().freeze_sound, 10, position=nodepos - ) - node.handlemessage(ba.FreezeMessage()) + BombFactory.get().freeze_sound.play(10, position=nodepos) + node.handlemessage(bs.FreezeMessage()) else: return super().handlemessage(msg) return None -class Bomb(ba.Actor): +class Bomb(bs.Actor): """A standard bomb and its variants such as land-mines and tnt-boxes. category: Gameplay Classes @@ -722,8 +718,8 @@ class Bomb(ba.Actor): bomb_type: str = 'normal', blast_radius: float = 2.0, bomb_scale: float = 1.0, - source_player: ba.Player | None = None, - owner: ba.Node | None = None, + source_player: bs.Player | None = None, + owner: bs.Node | None = None, ): """Create a new Bomb. @@ -750,7 +746,7 @@ class Bomb(ba.Actor): self._exploded = False self.scale = bomb_scale - self.texture_sequence: ba.Node | None = None + self.texture_sequence: bs.Node | None = None if self.bomb_type == 'sticky': self._last_sticky_sound_time = 0.0 @@ -788,7 +784,7 @@ class Bomb(ba.Actor): # since players carrying those things and thus touching footing # objects will think they're on solid ground.. perhaps we don't # wanna add this even in the tnt case? - materials: tuple[ba.Material, ...] + materials: tuple[bs.Material, ...] if self.bomb_type == 'tnt': materials = ( factory.bomb_material, @@ -810,14 +806,14 @@ class Bomb(ba.Actor): if self.bomb_type == 'land_mine': fuse_time = None - self.node = ba.newnode( + self.node = bs.newnode( 'prop', delegate=self, attrs={ 'position': position, 'velocity': velocity, - 'model': factory.land_mine_model, - 'light_model': factory.land_mine_model, + 'mesh': factory.land_mine_mesh, + 'light_mesh': factory.land_mine_mesh, 'body': 'landMine', 'body_scale': self.scale, 'shadow_size': 0.44, @@ -830,14 +826,14 @@ class Bomb(ba.Actor): elif self.bomb_type == 'tnt': fuse_time = None - self.node = ba.newnode( + self.node = bs.newnode( 'prop', delegate=self, attrs={ 'position': position, 'velocity': velocity, - 'model': factory.tnt_model, - 'light_model': factory.tnt_model, + 'mesh': factory.tnt_mesh, + 'light_mesh': factory.tnt_mesh, 'body': 'crate', 'body_scale': self.scale, 'shadow_size': 0.5, @@ -850,7 +846,7 @@ class Bomb(ba.Actor): elif self.bomb_type == 'impact': fuse_time = 20.0 - self.node = ba.newnode( + self.node = bs.newnode( 'prop', delegate=self, attrs={ @@ -858,7 +854,7 @@ class Bomb(ba.Actor): 'velocity': velocity, 'body': 'sphere', 'body_scale': self.scale, - 'model': factory.impact_bomb_model, + 'mesh': factory.impact_bomb_mesh, 'shadow_size': 0.3, 'color_texture': factory.impact_tex, 'reflection': 'powerup', @@ -866,23 +862,23 @@ class Bomb(ba.Actor): 'materials': materials, }, ) - self.arm_timer = ba.Timer( - 0.2, ba.WeakCall(self.handlemessage, ArmMessage()) + self.arm_timer = bs.Timer( + 0.2, bs.WeakCall(self.handlemessage, ArmMessage()) ) - self.warn_timer = ba.Timer( - fuse_time - 1.7, ba.WeakCall(self.handlemessage, WarnMessage()) + self.warn_timer = bs.Timer( + fuse_time - 1.7, bs.WeakCall(self.handlemessage, WarnMessage()) ) else: fuse_time = 3.0 if self.bomb_type == 'sticky': sticky = True - model = factory.sticky_bomb_model + mesh = factory.sticky_bomb_mesh rtype = 'sharper' rscale = 1.8 else: sticky = False - model = factory.bomb_model + mesh = factory.bomb_mesh rtype = 'sharper' rscale = 1.8 if self.bomb_type == 'ice': @@ -891,13 +887,13 @@ class Bomb(ba.Actor): tex = factory.sticky_tex else: tex = factory.regular_tex - self.node = ba.newnode( + self.node = bs.newnode( 'bomb', delegate=self, attrs={ 'position': position, 'velocity': velocity, - 'model': model, + 'mesh': mesh, 'body_scale': self.scale, 'shadow_size': 0.3, 'color_texture': tex, @@ -909,30 +905,28 @@ class Bomb(ba.Actor): }, ) - sound = ba.newnode( + sound = bs.newnode( 'sound', owner=self.node, attrs={'sound': factory.fuse_sound, 'volume': 0.25}, ) self.node.connectattr('position', sound, 'position') - ba.animate(self.node, 'fuse_length', {0.0: 1.0, fuse_time: 0.0}) + bs.animate(self.node, 'fuse_length', {0.0: 1.0, fuse_time: 0.0}) # Light the fuse!!! if self.bomb_type not in ('land_mine', 'tnt'): assert fuse_time is not None - ba.timer( - fuse_time, ba.WeakCall(self.handlemessage, ExplodeMessage()) + bs.timer( + fuse_time, bs.WeakCall(self.handlemessage, ExplodeMessage()) ) - ba.animate( + bs.animate( self.node, - 'model_scale', + 'mesh_scale', {0: 0, 0.2: 1.3 * self.scale, 0.26: self.scale}, ) - def get_source_player( - self, playertype: type[PlayerType] - ) -> PlayerType | None: + def get_source_player(self, playertype: type[PlayerT]) -> PlayerT | None: """Return the source-player if one exists and is the provided type.""" player: Any = self._source_player return ( @@ -952,10 +946,10 @@ class Bomb(ba.Actor): self.node.delete() def _handle_oob(self) -> None: - self.handlemessage(ba.DieMessage()) + self.handlemessage(bs.DieMessage()) def _handle_impact(self) -> None: - node = ba.getcollision().opposingnode + node = bs.getcollision().opposingnode # If we're an impact bomb and we came from this node, don't explode. # (otherwise we blow up on our own head when jumping). @@ -977,29 +971,28 @@ class Bomb(ba.Actor): def _handle_dropped(self) -> None: if self.bomb_type == 'land_mine': - self.arm_timer = ba.Timer( - 1.25, ba.WeakCall(self.handlemessage, ArmMessage()) + self.arm_timer = bs.Timer( + 1.25, bs.WeakCall(self.handlemessage, ArmMessage()) ) # Once we've thrown a sticky bomb we can stick to it. elif self.bomb_type == 'sticky': - def _setsticky(node: ba.Node) -> None: + def _setsticky(node: bs.Node) -> None: if node: node.stick_to_owner = True - ba.timer(0.25, lambda: _setsticky(self.node)) + bs.timer(0.25, lambda: _setsticky(self.node)) def _handle_splat(self) -> None: - node = ba.getcollision().opposingnode + node = bs.getcollision().opposingnode if ( node is not self.owner - and ba.time() - self._last_sticky_sound_time > 1.0 + and bs.time() - self._last_sticky_sound_time > 1.0 ): - self._last_sticky_sound_time = ba.time() + self._last_sticky_sound_time = bs.time() assert self.node - ba.playsound( - BombFactory.get().sticky_impact_sound, + BombFactory.get().sticky_impact_sound.play( 2.0, position=self.node.position, ) @@ -1022,7 +1015,7 @@ class Bomb(ba.Actor): velocity=self.node.velocity, blast_radius=self.blast_radius, blast_type=self.bomb_type, - source_player=ba.existing(self._source_player), + source_player=bs.existing(self._source_player), hit_type=self.hit_type, hit_subtype=self.hit_subtype, ).autoretain() @@ -1031,16 +1024,14 @@ class Bomb(ba.Actor): # We blew up so we need to go away. # NOTE TO SELF: do we actually need this delay? - ba.timer(0.001, ba.WeakCall(self.handlemessage, ba.DieMessage())) + bs.timer(0.001, bs.WeakCall(self.handlemessage, bs.DieMessage())) def _handle_warn(self) -> None: if self.texture_sequence and self.node: self.texture_sequence.rate = 30 - ba.playsound( - BombFactory.get().warn_sound, 0.5, position=self.node.position - ) + BombFactory.get().warn_sound.play(0.5, position=self.node.position) - def _add_material(self, material: ba.Material) -> None: + def _add_material(self, material: bs.Material) -> None: if not self.node: return materials = self.node.materials @@ -1056,20 +1047,20 @@ class Bomb(ba.Actor): if not self.node: return factory = BombFactory.get() - intex: Sequence[ba.Texture] + intex: Sequence[bs.Texture] if self.bomb_type == 'land_mine': intex = (factory.land_mine_lit_tex, factory.land_mine_tex) - self.texture_sequence = ba.newnode( + self.texture_sequence = bs.newnode( 'texture_sequence', owner=self.node, attrs={'rate': 30, 'input_textures': intex}, ) - ba.timer(0.5, self.texture_sequence.delete) + bs.timer(0.5, self.texture_sequence.delete) # We now make it explodable. - ba.timer( + bs.timer( 0.25, - ba.WeakCall( + bs.WeakCall( self._add_material, factory.land_mine_blast_material ), ) @@ -1079,14 +1070,14 @@ class Bomb(ba.Actor): factory.impact_tex, factory.impact_tex, ) - self.texture_sequence = ba.newnode( + self.texture_sequence = bs.newnode( 'texture_sequence', owner=self.node, attrs={'rate': 100, 'input_textures': intex}, ) - ba.timer( + bs.timer( 0.25, - ba.WeakCall( + bs.WeakCall( self._add_material, factory.land_mine_blast_material ), ) @@ -1097,9 +1088,9 @@ class Bomb(ba.Actor): self.texture_sequence.connectattr( 'output_texture', self.node, 'color_texture' ) - ba.playsound(factory.activate_sound, 0.5, position=self.node.position) + factory.activate_sound.play(0.5, position=self.node.position) - def _handle_hit(self, msg: ba.HitMessage) -> None: + def _handle_hit(self, msg: bs.HitMessage) -> None: ispunched = msg.srcnode and msg.srcnode.getnodetype() == 'spaz' # Normal bombs are triggered by non-punch impacts; @@ -1107,11 +1098,10 @@ class Bomb(ba.Actor): if not self._exploded and ( not ispunched or self.bomb_type in ['impact', 'land_mine'] ): - # Also lets change the owner of the bomb to whoever is setting # us off. (this way points for big chain reactions go to the # person causing them). - source_player = msg.get_source_player(ba.Player) + source_player = msg.get_source_player(bs.Player) if source_player is not None: self._source_player = source_player @@ -1125,9 +1115,9 @@ class Bomb(ba.Actor): # self.hit_type = msg.hit_type # self.hit_subtype = msg.hit_subtype - ba.timer( + bs.timer( 0.1 + random.random() * 0.1, - ba.WeakCall(self.handlemessage, ExplodeMessage()), + bs.WeakCall(self.handlemessage, ExplodeMessage()), ) assert self.node self.node.handlemessage( @@ -1157,7 +1147,7 @@ class Bomb(ba.Actor): self._handle_impact() # Ok the logic below looks like it was backwards to me. # Disabling for now; can bring back if need be. - # elif isinstance(msg, ba.PickedUpMessage): + # elif isinstance(msg, bs.PickedUpMessage): # # Change our source to whoever just picked us up *only* if it # # is None. This way we can get points for killing bots with their # # own bombs. Hmm would there be a downside to this? @@ -1165,13 +1155,13 @@ class Bomb(ba.Actor): # self._source_player = msg.node.source_player elif isinstance(msg, SplatMessage): self._handle_splat() - elif isinstance(msg, ba.DroppedMessage): + elif isinstance(msg, bs.DroppedMessage): self._handle_dropped() - elif isinstance(msg, ba.HitMessage): + elif isinstance(msg, bs.HitMessage): self._handle_hit(msg) - elif isinstance(msg, ba.DieMessage): + elif isinstance(msg, bs.DieMessage): self._handle_die() - elif isinstance(msg, ba.OutOfBoundsMessage): + elif isinstance(msg, bs.OutOfBoundsMessage): self._handle_oob() elif isinstance(msg, ArmMessage): self.arm() @@ -1196,8 +1186,8 @@ class TNTSpawner: self._update() # Go with slightly more than 1 second to avoid timer stacking. - self._update_timer = ba.Timer( - 1.1, ba.WeakCall(self._update), repeat=True + self._update_timer = bs.Timer( + 1.1, bs.WeakCall(self._update), repeat=True ) def _update(self) -> None: diff --git a/assets/src/ba_data/python/bastd/actor/controlsguide.py b/src/assets/ba_data/python/bastd/actor/controlsguide.py similarity index 82% rename from assets/src/ba_data/python/bastd/actor/controlsguide.py rename to src/assets/ba_data/python/bastd/actor/controlsguide.py index e7779f97..47af94bd 100644 --- a/assets/src/ba_data/python/bastd/actor/controlsguide.py +++ b/src/assets/ba_data/python/bastd/actor/controlsguide.py @@ -6,14 +6,13 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba -import ba.internal +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence -class ControlsGuide(ba.Actor): +class ControlsGuide(bs.Actor): """A screen overlay of game controls. category: Gameplay Classes @@ -54,14 +53,14 @@ class ControlsGuide(ba.Actor): self._lifespan = lifespan self._dead = False self._bright = bright - self._cancel_timer: ba.Timer | None = None - self._fade_in_timer: ba.Timer | None = None - self._update_timer: ba.Timer | None = None - self._title_text: ba.Node | None + self._cancel_timer: bs.Timer | None = None + self._fade_in_timer: bs.Timer | None = None + self._update_timer: bs.Timer | None = None + self._title_text: bs.Node | None clr: Sequence[float] extra_pos_1: tuple[float, float] | None extra_pos_2: tuple[float, float] | None - if ba.app.iircade_mode: + if bs.app.iircade_mode: xtweak = 0.2 ytweak = 0.2 jump_pos = ( @@ -109,10 +108,10 @@ class ControlsGuide(ba.Actor): position[1] + 139.0 * scale, ) clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) - tval = ba.Lstr( - value='${A}:', subs=[('${A}', ba.Lstr(resource='controlsText'))] + tval = bs.Lstr( + value='${A}:', subs=[('${A}', bs.Lstr(resource='controlsText'))] ) - self._title_text = ba.newnode( + self._title_text = bs.newnode( 'text', attrs={ 'text': tval, @@ -130,10 +129,10 @@ class ControlsGuide(ba.Actor): self._title_text = None pos = jump_pos clr = (0.4, 1, 0.4) - self._jump_image = ba.newnode( + self._jump_image = bs.newnode( 'image', attrs={ - 'texture': ba.gettexture('buttonJump'), + 'texture': bs.gettexture('buttonJump'), 'absolute_scale': True, 'host_only': True, 'vr_depth': 10, @@ -142,7 +141,7 @@ class ControlsGuide(ba.Actor): 'color': clr, }, ) - self._jump_text = ba.newnode( + self._jump_text = bs.newnode( 'text', attrs={ 'v_align': 'top', @@ -158,10 +157,10 @@ class ControlsGuide(ba.Actor): ) clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3) pos = punch_pos - self._punch_image = ba.newnode( + self._punch_image = bs.newnode( 'image', attrs={ - 'texture': ba.gettexture('buttonPunch'), + 'texture': bs.gettexture('buttonPunch'), 'absolute_scale': True, 'host_only': True, 'vr_depth': 10, @@ -170,7 +169,7 @@ class ControlsGuide(ba.Actor): 'color': clr, }, ) - self._punch_text = ba.newnode( + self._punch_text = bs.newnode( 'text', attrs={ 'v_align': 'top', @@ -186,10 +185,10 @@ class ControlsGuide(ba.Actor): ) pos = bomb_pos clr = (1, 0.3, 0.3) - self._bomb_image = ba.newnode( + self._bomb_image = bs.newnode( 'image', attrs={ - 'texture': ba.gettexture('buttonBomb'), + 'texture': bs.gettexture('buttonBomb'), 'absolute_scale': True, 'host_only': True, 'vr_depth': 10, @@ -198,7 +197,7 @@ class ControlsGuide(ba.Actor): 'color': clr, }, ) - self._bomb_text = ba.newnode( + self._bomb_text = bs.newnode( 'text', attrs={ 'h_align': 'center', @@ -214,10 +213,10 @@ class ControlsGuide(ba.Actor): ) pos = pickup_pos clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1) - self._pickup_image = ba.newnode( + self._pickup_image = bs.newnode( 'image', attrs={ - 'texture': ba.gettexture('buttonPickUp'), + 'texture': bs.gettexture('buttonPickUp'), 'absolute_scale': True, 'host_only': True, 'vr_depth': 10, @@ -226,7 +225,7 @@ class ControlsGuide(ba.Actor): 'color': clr, }, ) - self._pick_up_text = ba.newnode( + self._pick_up_text = bs.newnode( 'text', attrs={ 'v_align': 'top', @@ -243,13 +242,13 @@ class ControlsGuide(ba.Actor): clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0) self._run_text_pos_top = (position[0], position[1] - 135.0 * scale) self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale) - sval = 1.0 * scale if ba.app.vr_mode else 0.8 * scale - self._run_text = ba.newnode( + sval = 1.0 * scale if bs.app.vr_mode else 0.8 * scale + self._run_text = bs.newnode( 'text', attrs={ 'scale': sval, 'host_only': True, - 'shadow': 1.0 if ba.app.vr_mode else 0.5, + 'shadow': 1.0 if bs.app.vr_mode else 0.5, 'flatness': 1.0, 'maxwidth': 380, 'v_align': 'top', @@ -258,7 +257,7 @@ class ControlsGuide(ba.Actor): }, ) clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7) - self._extra_text = ba.newnode( + self._extra_text = bs.newnode( 'text', attrs={ 'scale': 0.8 * scale, @@ -273,10 +272,10 @@ class ControlsGuide(ba.Actor): ) if extra_pos_1 is not None: - self._extra_image_1: ba.Node | None = ba.newnode( + self._extra_image_1: bs.Node | None = bs.newnode( 'image', attrs={ - 'texture': ba.gettexture('nub'), + 'texture': bs.gettexture('nub'), 'absolute_scale': True, 'host_only': True, 'vr_depth': 10, @@ -288,10 +287,10 @@ class ControlsGuide(ba.Actor): else: self._extra_image_1 = None if extra_pos_2 is not None: - self._extra_image_2: ba.Node | None = ba.newnode( + self._extra_image_2: bs.Node | None = bs.newnode( 'image', attrs={ - 'texture': ba.gettexture('nub'), + 'texture': bs.gettexture('nub'), 'absolute_scale': True, 'host_only': True, 'vr_depth': 10, @@ -328,40 +327,39 @@ class ControlsGuide(ba.Actor): node.opacity = 0.0 # Don't do anything until our delay has passed. - ba.timer(delay, ba.WeakCall(self._start_updating)) + bs.timer(delay, bs.WeakCall(self._start_updating)) @staticmethod - def _meaningful_button_name(device: ba.InputDevice, button: int) -> str: + def _meaningful_button_name(device: bs.InputDevice, button: int) -> str: """Return a flattened string button name; empty for non-meaningful.""" if not device.has_meaningful_button_names: return '' return device.get_button_name(button).evaluate() def _start_updating(self) -> None: - # Ok, our delay has passed. Now lets periodically see if we can fade # in (if a touch-screen is present we only want to show up if gamepads # are connected, etc). # Also set up a timer so if we haven't faded in by the end of our # duration, abort. if self._lifespan is not None: - self._cancel_timer = ba.Timer( + self._cancel_timer = bs.Timer( self._lifespan, - ba.WeakCall(self.handlemessage, ba.DieMessage(immediate=True)), + bs.WeakCall(self.handlemessage, bs.DieMessage(immediate=True)), ) - self._fade_in_timer = ba.Timer( - 1.0, ba.WeakCall(self._check_fade_in), repeat=True + self._fade_in_timer = bs.Timer( + 1.0, bs.WeakCall(self._check_fade_in), repeat=True ) self._check_fade_in() # Do one check immediately. def _check_fade_in(self) -> None: - from ba.internal import get_device_value + assert bs.app.classic is not None # If we have a touchscreen, we only fade in if we have a player with # an input device that is *not* the touchscreen. # (otherwise it is confusing to see the touchscreen buttons right # next to our display buttons) - touchscreen: ba.InputDevice | None = ba.internal.getinputdevice( + touchscreen: bs.InputDevice | None = bs.getinputdevice( 'TouchScreen', '#1', doraise=False ) @@ -370,7 +368,7 @@ class ControlsGuide(ba.Actor): # We want to get ones who are still in the process of # selecting a character, etc. input_devices = [ - p.inputdevice for p in ba.getsession().sessionplayers + p.inputdevice for p in bs.getsession().sessionplayers ] input_devices = [ i for i in input_devices if i and i is not touchscreen @@ -388,7 +386,10 @@ class ControlsGuide(ba.Actor): ): if ( self._meaningful_button_name( - device, get_device_value(device, name) + device, + bs.app.classic.get_input_device_mapped_value( + device, name + ), ) != '' ): @@ -406,26 +407,29 @@ class ControlsGuide(ba.Actor): def _fade_in(self) -> None: for node in self._nodes: - ba.animate(node, 'opacity', {0: 0.0, 2.0: 1.0}) + bs.animate(node, 'opacity', {0: 0.0, 2.0: 1.0}) # If we were given a lifespan, transition out after it. if self._lifespan is not None: - ba.timer( - self._lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage()) + bs.timer( + self._lifespan, bs.WeakCall(self.handlemessage, bs.DieMessage()) ) self._update() - self._update_timer = ba.Timer( - 1.0, ba.WeakCall(self._update), repeat=True + self._update_timer = bs.Timer( + 1.0, bs.WeakCall(self._update), repeat=True ) def _update(self) -> None: # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals - from ba.internal import get_device_value, get_remote_app_name if self._dead: return + + classic = bs.app.classic + assert classic is not None + punch_button_names = set() jump_button_names = set() pickup_button_names = set() @@ -433,13 +437,13 @@ class ControlsGuide(ba.Actor): # We look at the session's players; not the activity's - we want to # get ones who are still in the process of selecting a character, etc. - input_devices = [p.inputdevice for p in ba.getsession().sessionplayers] + input_devices = [p.inputdevice for p in bs.getsession().sessionplayers] input_devices = [i for i in input_devices if i] # If there's no players with input devices yet, try to default to # showing keyboard controls. if not input_devices: - kbd = ba.internal.getinputdevice('Keyboard', '#1', doraise=False) + kbd = bs.getinputdevice('Keyboard', '#1', doraise=False) if kbd is not None: input_devices.append(kbd) @@ -464,42 +468,56 @@ class ControlsGuide(ba.Actor): if all_keyboards: right_button_names.add( device.get_button_name( - get_device_value(device, 'buttonRight') + classic.get_input_device_mapped_value( + device, 'buttonRight' + ) ) ) left_button_names.add( device.get_button_name( - get_device_value(device, 'buttonLeft') + classic.get_input_device_mapped_value( + device, 'buttonLeft' + ) ) ) down_button_names.add( device.get_button_name( - get_device_value(device, 'buttonDown') + classic.get_input_device_mapped_value( + device, 'buttonDown' + ) ) ) up_button_names.add( - device.get_button_name(get_device_value(device, 'buttonUp')) + device.get_button_name( + classic.get_input_device_mapped_value( + device, 'buttonUp' + ) + ) ) # Ignore empty values; things like the remote app or # wiimotes can return these. bname = self._meaningful_button_name( - device, get_device_value(device, 'buttonPunch') + device, + classic.get_input_device_mapped_value(device, 'buttonPunch'), ) if bname != '': punch_button_names.add(bname) bname = self._meaningful_button_name( - device, get_device_value(device, 'buttonJump') + device, + classic.get_input_device_mapped_value(device, 'buttonJump'), ) if bname != '': jump_button_names.add(bname) bname = self._meaningful_button_name( - device, get_device_value(device, 'buttonBomb') + device, + classic.get_input_device_mapped_value(device, 'buttonBomb'), ) if bname != '': bomb_button_names.add(bname) bname = self._meaningful_button_name( - device, get_device_value(device, 'buttonPickUp') + device, + classic.get_input_device_mapped_value(device, 'buttonPickUp'), ) if bname != '': pickup_button_names.add(bname) @@ -516,19 +534,19 @@ class ControlsGuide(ba.Actor): ) ): # Otherwise on android show standard buttons. - if ba.app.platform == 'android': + if classic.platform == 'android': punch_button_names.add('X') jump_button_names.add('A') bomb_button_names.add('B') pickup_button_names.add('Y') - run_text = ba.Lstr( + run_text = bs.Lstr( value='${R}: ${B}', subs=[ - ('${R}', ba.Lstr(resource='runText')), + ('${R}', bs.Lstr(resource='runText')), ( '${B}', - ba.Lstr( + bs.Lstr( resource='holdAnyKeyText' if all_keyboards else 'holdAnyButtonText' @@ -549,10 +567,10 @@ class ControlsGuide(ba.Actor): down_text = list(down_button_names)[0] left_text = list(left_button_names)[0] right_text = list(right_button_names)[0] - run_text = ba.Lstr( + run_text = bs.Lstr( value='${M}: ${U}, ${L}, ${D}, ${R}\n${RUN}', subs=[ - ('${M}', ba.Lstr(resource='moveText')), + ('${M}', bs.Lstr(resource='moveText')), ('${U}', up_text), ('${L}', left_text), ('${D}', down_text), @@ -568,11 +586,11 @@ class ControlsGuide(ba.Actor): pickup_button_names.clear() self._run_text.text = run_text - w_text: ba.Lstr | str + w_text: bs.Lstr | str if only_remote and self._lifespan is None: - w_text = ba.Lstr( + w_text = bs.Lstr( resource='fireTVRemoteWarningText', - subs=[('${REMOTE_APP_NAME}', get_remote_app_name())], + subs=[('${REMOTE_APP_NAME}', bs.get_remote_app_name())], ) else: w_text = '' @@ -626,14 +644,14 @@ class ControlsGuide(ba.Actor): def handlemessage(self, msg: Any) -> Any: assert not self.expired - if isinstance(msg, ba.DieMessage): + if isinstance(msg, bs.DieMessage): if msg.immediate: self._die() else: # If they don't need immediate, # fade out our nodes and die later. for node in self._nodes: - ba.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0}) - ba.timer(3.1, ba.WeakCall(self._die)) + bs.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0}) + bs.timer(3.1, bs.WeakCall(self._die)) return None return super().handlemessage(msg) diff --git a/assets/src/ba_data/python/bastd/actor/flag.py b/src/assets/ba_data/python/bastd/actor/flag.py similarity index 83% rename from assets/src/ba_data/python/bastd/actor/flag.py rename to src/assets/ba_data/python/bastd/actor/flag.py index b1c5693b..191f8944 100644 --- a/assets/src/ba_data/python/bastd/actor/flag.py +++ b/src/assets/ba_data/python/bastd/actor/flag.py @@ -7,8 +7,8 @@ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING -import ba from bastd.gameutils import SharedObjects +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence @@ -23,23 +23,23 @@ class FlagFactory: and can be retrieved via FlagFactory.get(). """ - flagmaterial: ba.Material - """The ba.Material applied to all `Flag`s.""" + flagmaterial: bs.Material + """The bs.Material applied to all `Flag`s.""" - impact_sound: ba.Sound - """The ba.Sound used when a `Flag` hits the ground.""" + impact_sound: bs.Sound + """The bs.Sound used when a `Flag` hits the ground.""" - skid_sound: ba.Sound - """The ba.Sound used when a `Flag` skids along the ground.""" + skid_sound: bs.Sound + """The bs.Sound used when a `Flag` skids along the ground.""" - no_hit_material: ba.Material - """A ba.Material that prevents contact with most objects; + no_hit_material: bs.Material + """A bs.Material that prevents contact with most objects; applied to 'non-touchable' flags.""" - flag_texture: ba.Texture - """The ba.Texture for flags.""" + flag_texture: bs.Texture + """The bs.Texture for flags.""" - _STORENAME = ba.storagename() + _STORENAME = bs.storagename() def __init__(self) -> None: """Instantiate a `FlagFactory`. @@ -48,7 +48,7 @@ class FlagFactory: get a shared instance. """ shared = SharedObjects.get() - self.flagmaterial = ba.Material() + self.flagmaterial = bs.Material() self.flagmaterial.add_actions( conditions=( ('we_are_younger_than', 100), @@ -69,8 +69,8 @@ class FlagFactory: ), ) - self.impact_sound = ba.getsound('metalHit') - self.skid_sound = ba.getsound('metalSkid') + self.impact_sound = bs.getsound('metalHit') + self.skid_sound = bs.getsound('metalSkid') self.flagmaterial.add_actions( conditions=( 'they_have_material', @@ -82,7 +82,7 @@ class FlagFactory: ), ) - self.no_hit_material = ba.Material() + self.no_hit_material = bs.Material() self.no_hit_material.add_actions( conditions=( ('they_have_material', shared.pickup_material), @@ -105,12 +105,12 @@ class FlagFactory: ), ) - self.flag_texture = ba.gettexture('flagColor') + self.flag_texture = bs.gettexture('flagColor') @classmethod def get(cls) -> FlagFactory: """Get/create a shared `FlagFactory` instance.""" - activity = ba.getactivity() + activity = bs.getactivity() factory = activity.customdata.get(cls._STORENAME) if factory is None: factory = FlagFactory() @@ -129,8 +129,8 @@ class FlagPickedUpMessage: flag: Flag """The `Flag` that has been picked up.""" - node: ba.Node - """The ba.Node doing the picking up.""" + node: bs.Node + """The bs.Node doing the picking up.""" @dataclass @@ -154,11 +154,11 @@ class FlagDroppedMessage: flag: Flag """The `Flag` that was dropped.""" - node: ba.Node - """The ba.Node that was holding it.""" + node: bs.Node + """The bs.Node that was holding it.""" -class Flag(ba.Actor): +class Flag(bs.Actor): """A flag; used in games such as capture-the-flag or king-of-the-hill. Category: **Gameplay Classes** @@ -170,7 +170,7 @@ class Flag(ba.Actor): self, position: Sequence[float] = (0.0, 1.0, 0.0), color: Sequence[float] = (1.0, 1.0, 1.0), - materials: Sequence[ba.Material] | None = None, + materials: Sequence[bs.Material] | None = None, touchable: bool = True, dropped_timeout: int | None = None, ): @@ -180,7 +180,7 @@ class Flag(ba.Actor): useful for things like king-of-the-hill where players should not be moving the flag around. - 'materials can be a list of extra `ba.Material`s to apply to the flag. + 'materials can be a list of extra `bs.Material`s to apply to the flag. If 'dropped_timeout' is provided (in seconds), the flag will die after remaining untouched for that long once it has been moved @@ -206,7 +206,7 @@ class Flag(ba.Actor): shared.object_material, factory.flagmaterial, ] + materials - self.node = ba.newnode( + self.node = bs.newnode( 'flag', attrs={ 'position': (position[0], position[1] + 0.75, position[2]), @@ -220,13 +220,13 @@ class Flag(ba.Actor): if dropped_timeout is not None: dropped_timeout = int(dropped_timeout) self._dropped_timeout = dropped_timeout - self._counter: ba.Node | None + self._counter: bs.Node | None if self._dropped_timeout is not None: self._count = self._dropped_timeout - self._tick_timer = ba.Timer( - 1.0, call=ba.WeakCall(self._tick), repeat=True + self._tick_timer = bs.Timer( + 1.0, call=bs.WeakCall(self._tick), repeat=True ) - self._counter = ba.newnode( + self._counter = bs.newnode( 'text', owner=self.node, attrs={ @@ -242,12 +242,11 @@ class Flag(ba.Actor): self._counter = None self._held_count = 0 - self._score_text: ba.Node | None = None - self._score_text_hide_timer: ba.Timer | None = None + self._score_text: bs.Node | None = None + self._score_text_hide_timer: bs.Timer | None = None def _tick(self) -> None: if self.node: - # Grab our initial position after one tick (in case we fall). if self._initial_position is None: self._initial_position = self.node.position @@ -282,7 +281,7 @@ class Flag(ba.Actor): ) self._counter.text = str(self._count) if self._count < 1: - self.handlemessage(ba.DieMessage()) + self.handlemessage(bs.DieMessage()) else: assert self._counter self._counter.text = '' @@ -290,7 +289,7 @@ class Flag(ba.Actor): def _hide_score_text(self) -> None: assert self._score_text is not None assert isinstance(self._score_text.scale, float) - ba.animate( + bs.animate( self._score_text, 'scale', {0: self._score_text.scale, 0.2: 0} ) @@ -300,13 +299,13 @@ class Flag(ba.Actor): return if not self._score_text: start_scale = 0.0 - math = ba.newnode( + math = bs.newnode( 'math', owner=self.node, attrs={'input1': (0, 1.4, 0), 'operation': 'add'}, ) self.node.connectattr('position', math, 'input2') - self._score_text = ba.newnode( + self._score_text = bs.newnode( 'text', owner=self.node, attrs={ @@ -323,20 +322,20 @@ class Flag(ba.Actor): assert isinstance(self._score_text.scale, float) start_scale = self._score_text.scale self._score_text.text = text - self._score_text.color = ba.safecolor(self.node.color) - ba.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02}) - self._score_text_hide_timer = ba.Timer( - 1.0, ba.WeakCall(self._hide_score_text) + self._score_text.color = bs.safecolor(self.node.color) + bs.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02}) + self._score_text_hide_timer = bs.Timer( + 1.0, bs.WeakCall(self._hide_score_text) ) def handlemessage(self, msg: Any) -> Any: assert not self.expired - if isinstance(msg, ba.DieMessage): + if isinstance(msg, bs.DieMessage): if self.node: self.node.delete() if not msg.immediate: self.activity.handlemessage(FlagDiedMessage(self)) - elif isinstance(msg, ba.HitMessage): + elif isinstance(msg, bs.HitMessage): assert self.node assert msg.force_direction is not None self.node.handlemessage( @@ -355,12 +354,12 @@ class Flag(ba.Actor): msg.force_direction[1], msg.force_direction[2], ) - elif isinstance(msg, ba.PickedUpMessage): + elif isinstance(msg, bs.PickedUpMessage): self._held_count += 1 if self._held_count == 1 and self._counter is not None: self._counter.text = '' self.activity.handlemessage(FlagPickedUpMessage(self, msg.node)) - elif isinstance(msg, ba.DroppedMessage): + elif isinstance(msg, bs.DroppedMessage): self._held_count -= 1 if self._held_count < 0: print('Flag held count < 0.') @@ -377,4 +376,4 @@ class Flag(ba.Actor): movable flag originated from. """ assert len(pos) == 3 - ba.emitfx(position=pos, emit_type='flag_stand') + bs.emitfx(position=pos, emit_type='flag_stand') diff --git a/assets/src/ba_data/python/bastd/actor/image.py b/src/assets/ba_data/python/bastd/actor/image.py similarity index 82% rename from assets/src/ba_data/python/bastd/actor/image.py rename to src/assets/ba_data/python/bastd/actor/image.py index 51774b9d..4fc61bfa 100644 --- a/assets/src/ba_data/python/bastd/actor/image.py +++ b/src/assets/ba_data/python/bastd/actor/image.py @@ -7,13 +7,13 @@ from __future__ import annotations from enum import Enum from typing import TYPE_CHECKING -import ba +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence -class Image(ba.Actor): +class Image(bs.Actor): """Just a wrapped up image node with a few tricks up its sleeve.""" class Transition(Enum): @@ -36,7 +36,7 @@ class Image(ba.Actor): def __init__( self, - texture: ba.Texture | dict[str, Any], + texture: bs.Texture | dict[str, Any], position: tuple[float, float] = (0, 0), transition: Transition | None = None, transition_delay: float = 0.0, @@ -44,8 +44,8 @@ class Image(ba.Actor): color: Sequence[float] = (1.0, 1.0, 1.0, 1.0), scale: tuple[float, float] = (100.0, 100.0), transition_out_delay: float | None = None, - model_opaque: ba.Model | None = None, - model_transparent: ba.Model | None = None, + mesh_opaque: bs.Mesh | None = None, + mesh_transparent: bs.Mesh | None = None, vr_depth: float = 0.0, host_only: bool = False, front: bool = False, @@ -57,20 +57,20 @@ class Image(ba.Actor): # If they provided a dict as texture, assume its an icon. # otherwise its just a texture value itself. - mask_texture: ba.Texture | None + mask_texture: bs.Texture | None if isinstance(texture, dict): tint_color = texture['tint_color'] tint2_color = texture['tint2_color'] tint_texture = texture['tint_texture'] texture = texture['texture'] - mask_texture = ba.gettexture('characterIconMask') + mask_texture = bs.gettexture('characterIconMask') else: tint_color = (1, 1, 1) tint2_color = None tint_texture = None mask_texture = None - self.node = ba.newnode( + self.node = bs.newnode( 'image', attrs={ 'texture': texture, @@ -89,10 +89,10 @@ class Image(ba.Actor): delegate=self, ) - if model_opaque is not None: - self.node.model_opaque = model_opaque - if model_transparent is not None: - self.node.model_transparent = model_transparent + if mesh_opaque is not None: + self.node.mesh_opaque = mesh_opaque + if mesh_transparent is not None: + self.node.mesh_transparent = mesh_transparent if tint2_color is not None: self.node.tint2_color = tint2_color if transition is self.Transition.FADE_IN: @@ -100,8 +100,8 @@ class Image(ba.Actor): if transition_out_delay is not None: keys[transition_delay + transition_out_delay] = color[3] keys[transition_delay + transition_out_delay + 0.5] = 0 - ba.animate(self.node, 'opacity', keys) - cmb = self.position_combine = ba.newnode( + bs.animate(self.node, 'opacity', keys) + cmb = self.position_combine = bs.newnode( 'combine', owner=self.node, attrs={'size': 2} ) if transition is self.Transition.IN_RIGHT: @@ -110,9 +110,9 @@ class Image(ba.Actor): transition_delay + 0.2: position[0], } o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0} - ba.animate(cmb, 'input0', keys) + bs.animate(cmb, 'input0', keys) cmb.input1 = position[1] - ba.animate(self.node, 'opacity', o_keys) + bs.animate(self.node, 'opacity', o_keys) elif transition is self.Transition.IN_LEFT: keys = { transition_delay: position[0] - 1200, @@ -126,15 +126,15 @@ class Image(ba.Actor): ) o_keys[transition_delay + transition_out_delay + 0.15] = 1.0 o_keys[transition_delay + transition_out_delay + 0.2] = 0.0 - ba.animate(cmb, 'input0', keys) + bs.animate(cmb, 'input0', keys) cmb.input1 = position[1] - ba.animate(self.node, 'opacity', o_keys) + bs.animate(self.node, 'opacity', o_keys) elif transition is self.Transition.IN_BOTTOM_SLOW: keys = {transition_delay: -400, transition_delay + 3.5: position[1]} o_keys = {transition_delay: 0.0, transition_delay + 2.0: 1.0} cmb.input0 = position[0] - ba.animate(cmb, 'input1', keys) - ba.animate(self.node, 'opacity', o_keys) + bs.animate(cmb, 'input1', keys) + bs.animate(self.node, 'opacity', o_keys) elif transition is self.Transition.IN_BOTTOM: keys = {transition_delay: -400, transition_delay + 0.2: position[1]} o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0} @@ -144,14 +144,14 @@ class Image(ba.Actor): o_keys[transition_delay + transition_out_delay + 0.15] = 1.0 o_keys[transition_delay + transition_out_delay + 0.2] = 0.0 cmb.input0 = position[0] - ba.animate(cmb, 'input1', keys) - ba.animate(self.node, 'opacity', o_keys) + bs.animate(cmb, 'input1', keys) + bs.animate(self.node, 'opacity', o_keys) elif transition is self.Transition.IN_TOP_SLOW: keys = {transition_delay: 400, transition_delay + 3.5: position[1]} o_keys = {transition_delay: 0.0, transition_delay + 1.0: 1.0} cmb.input0 = position[0] - ba.animate(cmb, 'input1', keys) - ba.animate(self.node, 'opacity', o_keys) + bs.animate(cmb, 'input1', keys) + bs.animate(self.node, 'opacity', o_keys) else: assert transition is self.Transition.FADE_IN or transition is None cmb.input0 = position[0] @@ -160,14 +160,14 @@ class Image(ba.Actor): # If we're transitioning out, die at the end of it. if transition_out_delay is not None: - ba.timer( + bs.timer( transition_delay + transition_out_delay + 1.0, - ba.WeakCall(self.handlemessage, ba.DieMessage()), + bs.WeakCall(self.handlemessage, bs.DieMessage()), ) def handlemessage(self, msg: Any) -> Any: assert not self.expired - if isinstance(msg, ba.DieMessage): + if isinstance(msg, bs.DieMessage): if self.node: self.node.delete() return None diff --git a/assets/src/ba_data/python/bastd/actor/onscreencountdown.py b/src/assets/ba_data/python/bastd/actor/onscreencountdown.py similarity index 69% rename from assets/src/ba_data/python/bastd/actor/onscreencountdown.py rename to src/assets/ba_data/python/bastd/actor/onscreencountdown.py index 2210f51a..c196263a 100644 --- a/assets/src/ba_data/python/bastd/actor/onscreencountdown.py +++ b/src/assets/ba_data/python/bastd/actor/onscreencountdown.py @@ -6,13 +6,14 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba +import babase +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Callable -class OnScreenCountdown(ba.Actor): +class OnScreenCountdown(bs.Actor): """A Handy On-Screen Timer. category: Gameplay Classes @@ -26,7 +27,7 @@ class OnScreenCountdown(ba.Actor): self._timeremaining = duration self._ended = False self._endcall = endcall - self.node = ba.newnode( + self.node = bs.newnode( 'text', attrs={ 'v_attach': 'top', @@ -40,7 +41,7 @@ class OnScreenCountdown(ba.Actor): 'text': '', }, ) - self.inputnode = ba.newnode( + self.inputnode = bs.newnode( 'timedisplay', attrs={ 'time2': duration * 1000, @@ -50,27 +51,27 @@ class OnScreenCountdown(ba.Actor): ) self.inputnode.connectattr('output', self.node, 'text') self._countdownsounds = { - 10: ba.getsound('announceTen'), - 9: ba.getsound('announceNine'), - 8: ba.getsound('announceEight'), - 7: ba.getsound('announceSeven'), - 6: ba.getsound('announceSix'), - 5: ba.getsound('announceFive'), - 4: ba.getsound('announceFour'), - 3: ba.getsound('announceThree'), - 2: ba.getsound('announceTwo'), - 1: ba.getsound('announceOne'), + 10: bs.getsound('announceTen'), + 9: bs.getsound('announceNine'), + 8: bs.getsound('announceEight'), + 7: bs.getsound('announceSeven'), + 6: bs.getsound('announceSix'), + 5: bs.getsound('announceFive'), + 4: bs.getsound('announceFour'), + 3: bs.getsound('announceThree'), + 2: bs.getsound('announceTwo'), + 1: bs.getsound('announceOne'), } - self._timer: ba.Timer | None = None + self._timer: bs.Timer | None = None def start(self) -> None: """Start the timer.""" - globalsnode = ba.getactivity().globalsnode + globalsnode = bs.getactivity().globalsnode globalsnode.connectattr('time', self.inputnode, 'time1') self.inputnode.time2 = ( globalsnode.time + (self._timeremaining + 1) * 1000 ) - self._timer = ba.Timer(1.0, self._update, repeat=True) + self._timer = bs.Timer(1.0, self._update, repeat=True) def on_expire(self) -> None: super().on_expire() @@ -91,16 +92,16 @@ class OnScreenCountdown(ba.Actor): assert self.node assert isinstance(self.node.scale, float) self.node.scale *= 1.2 - cmb = ba.newnode('combine', owner=self.node, attrs={'size': 4}) + cmb = bs.newnode('combine', owner=self.node, attrs={'size': 4}) cmb.connectattr('output', self.node, 'color') - ba.animate(cmb, 'input0', {0: 1.0, 0.15: 1.0}, loop=True) - ba.animate(cmb, 'input1', {0: 1.0, 0.15: 0.5}, loop=True) - ba.animate(cmb, 'input2', {0: 0.1, 0.15: 0.0}, loop=True) + bs.animate(cmb, 'input0', {0: 1.0, 0.15: 1.0}, loop=True) + bs.animate(cmb, 'input1', {0: 1.0, 0.15: 0.5}, loop=True) + bs.animate(cmb, 'input2', {0: 0.1, 0.15: 0.0}, loop=True) cmb.input3 = 1.0 if tval <= 10 and not self._ended: - ba.playsound(ba.getsound('tick')) + bs.getsound('tick').play() if tval in self._countdownsounds: - ba.playsound(self._countdownsounds[tval]) + self._countdownsounds[tval].play() if tval <= 0 and not self._ended: self._ended = True if self._endcall is not None: diff --git a/assets/src/ba_data/python/bastd/actor/onscreentimer.py b/src/assets/ba_data/python/bastd/actor/onscreentimer.py similarity index 57% rename from assets/src/ba_data/python/bastd/actor/onscreentimer.py rename to src/assets/ba_data/python/bastd/actor/onscreentimer.py index 99deae56..9de5509d 100644 --- a/assets/src/ba_data/python/bastd/actor/onscreentimer.py +++ b/src/assets/ba_data/python/bastd/actor/onscreentimer.py @@ -3,15 +3,15 @@ """Defines Actor(s).""" from __future__ import annotations -from typing import TYPE_CHECKING, overload +from typing import TYPE_CHECKING -import ba +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Literal -class OnScreenTimer(ba.Actor): +class OnScreenTimer(bs.Actor): """A handy on-screen timer. category: Gameplay Classes @@ -22,7 +22,7 @@ class OnScreenTimer(ba.Actor): def __init__(self) -> None: super().__init__() self._starttime_ms: int | None = None - self.node = ba.newnode( + self.node = bs.newnode( 'text', attrs={ 'v_attach': 'top', @@ -36,18 +36,18 @@ class OnScreenTimer(ba.Actor): 'text': '', }, ) - self.inputnode = ba.newnode( + self.inputnode = bs.newnode( 'timedisplay', attrs={'timemin': 0, 'showsubseconds': True} ) self.inputnode.connectattr('output', self.node, 'text') def start(self) -> None: """Start the timer.""" - tval = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + tval = int(bs.time() * 1000.0) assert isinstance(tval, int) self._starttime_ms = tval self.inputnode.time1 = self._starttime_ms - ba.getactivity().globalsnode.connectattr( + bs.getactivity().globalsnode.connectattr( 'time', self.inputnode, 'time2' ) @@ -55,52 +55,22 @@ class OnScreenTimer(ba.Actor): """Return whether this timer has started yet.""" return self._starttime_ms is not None - def stop( - self, - endtime: int | float | None = None, - timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS, - ) -> None: + def stop(self, endtime: int | float | None = None) -> None: """End the timer. If 'endtime' is not None, it is used when calculating the final display time; otherwise the current time is used. - - 'timeformat' applies to endtime and can be SECONDS or MILLISECONDS """ if endtime is None: - endtime = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) - timeformat = ba.TimeFormat.MILLISECONDS + endtime = bs.time() if self._starttime_ms is None: print('Warning: OnScreenTimer.stop() called without start() first') else: - endtime_ms: int - if timeformat is ba.TimeFormat.SECONDS: - endtime_ms = int(endtime * 1000) - elif timeformat is ba.TimeFormat.MILLISECONDS: - assert isinstance(endtime, int) - endtime_ms = endtime - else: - raise ValueError(f'invalid timeformat: {timeformat}') - + endtime_ms = int(endtime * 1000) self.inputnode.timemax = endtime_ms - self._starttime_ms - # Overloads so type checker knows our exact return type based in args. - @overload - def getstarttime( - self, timeformat: Literal[ba.TimeFormat.SECONDS] = ba.TimeFormat.SECONDS - ) -> float: - ... - - @overload - def getstarttime( - self, timeformat: Literal[ba.TimeFormat.MILLISECONDS] - ) -> int: - ... - - def getstarttime( - self, timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS - ) -> int | float: + def getstarttime(self) -> float: """Return the sim-time when start() was called. Time will be returned in seconds if timeformat is SECONDS or @@ -109,15 +79,11 @@ class OnScreenTimer(ba.Actor): val_ms: Any if self._starttime_ms is None: print('WARNING: getstarttime() called on un-started timer') - val_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + val_ms = int(bs.time() * 1000.0) else: val_ms = self._starttime_ms assert isinstance(val_ms, int) - if timeformat is ba.TimeFormat.SECONDS: - return 0.001 * val_ms - if timeformat is ba.TimeFormat.MILLISECONDS: - return val_ms - raise ValueError(f'invalid timeformat: {timeformat}') + return 0.001 * val_ms @property def starttime(self) -> float: @@ -126,6 +92,6 @@ class OnScreenTimer(ba.Actor): def handlemessage(self, msg: Any) -> Any: # if we're asked to die, just kill our node/timer - if isinstance(msg, ba.DieMessage): + if isinstance(msg, bs.DieMessage): if self.node: self.node.delete() diff --git a/assets/src/ba_data/python/bastd/actor/playerspaz.py b/src/assets/ba_data/python/bastd/actor/playerspaz.py similarity index 80% rename from assets/src/ba_data/python/bastd/actor/playerspaz.py rename to src/assets/ba_data/python/bastd/actor/playerspaz.py index 269b8053..525173d2 100644 --- a/assets/src/ba_data/python/bastd/actor/playerspaz.py +++ b/src/assets/ba_data/python/bastd/actor/playerspaz.py @@ -6,16 +6,14 @@ from __future__ import annotations from typing import TYPE_CHECKING, TypeVar, overload -import ba +import babase +import bascenev1 as bs from bastd.actor.spaz import Spaz if TYPE_CHECKING: from typing import Any, Sequence, Literal -# pylint: disable=invalid-name -PlayerType = TypeVar('PlayerType', bound=ba.Player) -TeamType = TypeVar('TeamType', bound=ba.Team) -# pylint: enable=invalid-name +PlayerT = TypeVar('PlayerT', bound=bs.Player) class PlayerSpazHurtMessage: @@ -28,32 +26,32 @@ class PlayerSpazHurtMessage: """The PlayerSpaz that was hurt""" def __init__(self, spaz: PlayerSpaz): - """Instantiate with the given ba.Spaz value.""" + """Instantiate with the given bascenev1.Spaz value.""" self.spaz = spaz class PlayerSpaz(Spaz): - """A Spaz subclass meant to be controlled by a ba.Player. + """A Spaz subclass meant to be controlled by a bascenev1.Player. Category: **Gameplay Classes** - When a PlayerSpaz dies, it delivers a ba.PlayerDiedMessage - to the current ba.Activity. (unless the death was the result of the - player leaving the game, in which case no message is sent) + When a PlayerSpaz dies, it delivers a bascenev1.PlayerDiedMessage + to the current bascenev1.Activity. (unless the death was the result + of the player leaving the game, in which case no message is sent) When a PlayerSpaz is hurt, it delivers a PlayerSpazHurtMessage - to the current ba.Activity. + to the current bascenev1.Activity. """ def __init__( self, - player: ba.Player, + player: bs.Player, color: Sequence[float] = (1.0, 1.0, 1.0), highlight: Sequence[float] = (0.5, 0.5, 0.5), character: str = 'Spaz', powerups_expire: bool = True, ): - """Create a spaz for the provided ba.Player. + """Create a spaz for the provided bascenev1.Player. Note: this does not wire up any controls; you must call connect_controls_to_player() to do so. @@ -67,11 +65,11 @@ class PlayerSpaz(Spaz): start_invincible=True, powerups_expire=powerups_expire, ) - self.last_player_attacked_by: ba.Player | None = None + self.last_player_attacked_by: bs.Player | None = None self.last_attacked_time = 0.0 self.last_attacked_type: tuple[str, str] | None = None self.held_count = 0 - self.last_player_held_by: ba.Player | None = None + self.last_player_held_by: bs.Player | None = None self._player = player self._drive_player_position() @@ -79,20 +77,20 @@ class PlayerSpaz(Spaz): @overload def getplayer( - self, playertype: type[PlayerType], doraise: Literal[False] = False - ) -> PlayerType | None: + self, playertype: type[PlayerT], doraise: Literal[False] = False + ) -> PlayerT | None: ... @overload def getplayer( - self, playertype: type[PlayerType], doraise: Literal[True] - ) -> PlayerType: + self, playertype: type[PlayerT], doraise: Literal[True] + ) -> PlayerT: ... def getplayer( - self, playertype: type[PlayerType], doraise: bool = False - ) -> PlayerType | None: - """Get the ba.Player associated with this Spaz. + self, playertype: type[PlayerT], doraise: bool = False + ) -> PlayerT | None: + """Get the bascenev1.Player associated with this Spaz. By default this will return None if the Player no longer exists. If you are logically certain that the Player still exists, pass @@ -101,7 +99,7 @@ class PlayerSpaz(Spaz): player: Any = self._player assert isinstance(player, playertype) if not player.exists() and doraise: - raise ba.PlayerNotFoundError() + raise babase.PlayerNotFoundError() return player if player.exists() else None def connect_controls_to_player( @@ -113,13 +111,13 @@ class PlayerSpaz(Spaz): enable_run: bool = True, enable_fly: bool = True, ) -> None: - """Wire this spaz up to the provided ba.Player. + """Wire this spaz up to the provided bascenev1.Player. Full control of the character is given by default but can be selectively limited by passing False to specific arguments. """ - player = self.getplayer(ba.Player) + player = self.getplayer(bs.Player) assert player # Reset any currently connected player and/or the player we're @@ -131,15 +129,16 @@ class PlayerSpaz(Spaz): else: player.resetinput() - player.assigninput(ba.InputType.UP_DOWN, self.on_move_up_down) - player.assigninput(ba.InputType.LEFT_RIGHT, self.on_move_left_right) + player.assigninput(babase.InputType.UP_DOWN, self.on_move_up_down) + player.assigninput(babase.InputType.LEFT_RIGHT, self.on_move_left_right) player.assigninput( - ba.InputType.HOLD_POSITION_PRESS, self.on_hold_position_press + babase.InputType.HOLD_POSITION_PRESS, self.on_hold_position_press ) player.assigninput( - ba.InputType.HOLD_POSITION_RELEASE, self.on_hold_position_release + babase.InputType.HOLD_POSITION_RELEASE, + self.on_hold_position_release, ) - intp = ba.InputType + intp = babase.InputType if enable_jump: player.assigninput(intp.JUMP_PRESS, self.on_jump_press) player.assigninput(intp.JUMP_RELEASE, self.on_jump_release) @@ -163,7 +162,7 @@ class PlayerSpaz(Spaz): def disconnect_controls_from_player(self) -> None: """ Completely sever any previously connected - ba.Player from control of this spaz. + bascenev1.Player from control of this spaz. """ if self._connected_to_player: self._connected_to_player.resetinput() @@ -193,14 +192,14 @@ class PlayerSpaz(Spaz): assert not self.expired # Keep track of if we're being held and by who most recently. - if isinstance(msg, ba.PickedUpMessage): + if isinstance(msg, bs.PickedUpMessage): # Augment standard behavior. super().handlemessage(msg) self.held_count += 1 picked_up_by = msg.node.source_player if picked_up_by: self.last_player_held_by = picked_up_by - elif isinstance(msg, ba.DroppedMessage): + elif isinstance(msg, bs.DroppedMessage): # Augment standard behavior. super().handlemessage(msg) self.held_count -= 1 @@ -211,9 +210,9 @@ class PlayerSpaz(Spaz): picked_up_by = msg.node.source_player if picked_up_by: self.last_player_attacked_by = picked_up_by - self.last_attacked_time = ba.time() + self.last_attacked_time = babase.apptime() self.last_attacked_type = ('picked_up', 'default') - elif isinstance(msg, ba.StandMessage): + elif isinstance(msg, bs.StandMessage): super().handlemessage(msg) # Augment standard behavior. # Our Spaz was just moved somewhere. Explicitly update @@ -221,19 +220,17 @@ class PlayerSpaz(Spaz): # for logic (otherwise it will be out of date until next step) self._drive_player_position() - elif isinstance(msg, ba.DieMessage): - + elif isinstance(msg, bs.DieMessage): # Report player deaths to the game. if not self._dead: - # Immediate-mode or left-game deaths don't count as 'kills'. killed = ( - not msg.immediate and msg.how is not ba.DeathType.LEFT_GAME + not msg.immediate and msg.how is not bs.DeathType.LEFT_GAME ) activity = self._activity() - player = self.getplayer(ba.Player, False) + player = self.getplayer(bs.Player, False) if not killed: killerplayer = None else: @@ -251,13 +248,13 @@ class PlayerSpaz(Spaz): # something like last_actor_attacked_by to fix that. if ( self.last_player_attacked_by - and ba.time() - self.last_attacked_time < 4.0 + and babase.apptime() - self.last_attacked_time < 4.0 ): killerplayer = self.last_player_attacked_by else: # ok, call it a suicide unless we're in co-op if activity is not None and not isinstance( - activity.session, ba.CoopSession + activity.session, bs.CoopSession ): killerplayer = player else: @@ -270,7 +267,7 @@ class PlayerSpaz(Spaz): # Only report if both the player and the activity still exist. if killed and activity is not None and player: activity.handlemessage( - ba.PlayerDiedMessage( + bs.PlayerDiedMessage( player, killed, killerplayer, msg.how ) ) @@ -278,11 +275,11 @@ class PlayerSpaz(Spaz): super().handlemessage(msg) # Augment standard behavior. # Keep track of the player who last hit us for point rewarding. - elif isinstance(msg, ba.HitMessage): + elif isinstance(msg, bs.HitMessage): source_player = msg.get_source_player(type(self._player)) if source_player: self.last_player_attacked_by = source_player - self.last_attacked_time = ba.time() + self.last_attacked_time = babase.apptime() self.last_attacked_type = (msg.hit_type, msg.hit_subtype) super().handlemessage(msg) # Augment standard behavior. activity = self._activity() @@ -293,7 +290,7 @@ class PlayerSpaz(Spaz): return None def _drive_player_position(self) -> None: - """Drive our ba.Player's official position + """Drive our bascenev1.Player's official position If our position is changed explicitly, this should be called again to instantly update the player position (otherwise it would be out diff --git a/assets/src/ba_data/python/bastd/actor/popuptext.py b/src/assets/ba_data/python/bastd/actor/popuptext.py similarity index 87% rename from assets/src/ba_data/python/bastd/actor/popuptext.py rename to src/assets/ba_data/python/bastd/actor/popuptext.py index 115b3598..e08de510 100644 --- a/assets/src/ba_data/python/bastd/actor/popuptext.py +++ b/src/assets/ba_data/python/bastd/actor/popuptext.py @@ -7,13 +7,14 @@ from __future__ import annotations import random from typing import TYPE_CHECKING -import ba +import babase +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence -class PopupText(ba.Actor): +class PopupText(bs.Actor): """Text that pops up above a position to denote something special. category: Gameplay Classes @@ -21,7 +22,7 @@ class PopupText(ba.Actor): def __init__( self, - text: str | ba.Lstr, + text: str | babase.Lstr, position: Sequence[float] = (0.0, 0.0, 0.0), color: Sequence[float] = (1.0, 1.0, 1.0, 1.0), random_offset: float = 0.5, @@ -43,7 +44,7 @@ class PopupText(ba.Actor): position[2] + offset[2] + random_offset * (0.5 - random.random()), ) - self.node = ba.newnode( + self.node = bs.newnode( 'text', attrs={ 'text': text, @@ -58,7 +59,7 @@ class PopupText(ba.Actor): lifespan = 1.5 # scale up - ba.animate( + bs.animate( self.node, 'scale', { @@ -70,18 +71,18 @@ class PopupText(ba.Actor): ) # translate upward - self._tcombine = ba.newnode( + self._tcombine = bs.newnode( 'combine', owner=self.node, attrs={'input0': pos[0], 'input2': pos[2], 'size': 3}, ) - ba.animate( + bs.animate( self._tcombine, 'input1', {0: pos[1] + 1.5, lifespan: pos[1] + 2.0} ) self._tcombine.connectattr('output', self.node, 'position') # fade our opacity in/out - self._combine = ba.newnode( + self._combine = bs.newnode( 'combine', owner=self.node, attrs={ @@ -92,7 +93,7 @@ class PopupText(ba.Actor): }, ) for i in range(4): - ba.animate( + bs.animate( self._combine, 'input' + str(i), { @@ -101,7 +102,7 @@ class PopupText(ba.Actor): 0.22 * lifespan: color[i], }, ) - ba.animate( + bs.animate( self._combine, 'input3', { @@ -114,13 +115,13 @@ class PopupText(ba.Actor): self._combine.connectattr('output', self.node, 'color') # kill ourself - self._die_timer = ba.Timer( - lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage()) + self._die_timer = bs.Timer( + lifespan, babase.WeakCall(self.handlemessage, bs.DieMessage()) ) def handlemessage(self, msg: Any) -> Any: assert not self.expired - if isinstance(msg, ba.DieMessage): + if isinstance(msg, bs.DieMessage): if self.node: self.node.delete() else: diff --git a/assets/src/ba_data/python/bastd/actor/powerupbox.py b/src/assets/ba_data/python/bastd/actor/powerupbox.py similarity index 60% rename from assets/src/ba_data/python/bastd/actor/powerupbox.py rename to src/assets/ba_data/python/bastd/actor/powerupbox.py index adc31158..165a31b8 100644 --- a/assets/src/ba_data/python/bastd/actor/powerupbox.py +++ b/src/assets/ba_data/python/bastd/actor/powerupbox.py @@ -7,7 +7,7 @@ from __future__ import annotations import random from typing import TYPE_CHECKING -import ba +import bascenev1 as bs from bastd.gameutils import SharedObjects if TYPE_CHECKING: @@ -21,64 +21,64 @@ class _TouchedMessage: class PowerupBoxFactory: - """A collection of media and other resources used by ba.Powerups. + """A collection of media and other resources used by bs.Powerups. Category: **Gameplay Classes** A single instance of this is shared between all powerups - and can be retrieved via ba.Powerup.get_factory(). + and can be retrieved via bs.Powerup.get_factory(). """ - model: ba.Model - """The ba.Model of the powerup box.""" + mesh: bs.Mesh + """The bs.Mesh of the powerup box.""" - model_simple: ba.Model - """A simpler ba.Model of the powerup box, for use in shadows, etc.""" + mesh_simple: bs.Mesh + """A simpler bs.Mesh of the powerup box, for use in shadows, etc.""" - tex_bomb: ba.Texture - """Triple-bomb powerup ba.Texture.""" + tex_bomb: bs.Texture + """Triple-bomb powerup bs.Texture.""" - tex_punch: ba.Texture - """Punch powerup ba.Texture.""" + tex_punch: bs.Texture + """Punch powerup bs.Texture.""" - tex_ice_bombs: ba.Texture - """Ice bomb powerup ba.Texture.""" + tex_ice_bombs: bs.Texture + """Ice bomb powerup bs.Texture.""" - tex_sticky_bombs: ba.Texture - """Sticky bomb powerup ba.Texture.""" + tex_sticky_bombs: bs.Texture + """Sticky bomb powerup bs.Texture.""" - tex_shield: ba.Texture - """Shield powerup ba.Texture.""" + tex_shield: bs.Texture + """Shield powerup bs.Texture.""" - tex_impact_bombs: ba.Texture - """Impact-bomb powerup ba.Texture.""" + tex_impact_bombs: bs.Texture + """Impact-bomb powerup bs.Texture.""" - tex_health: ba.Texture - """Health powerup ba.Texture.""" + tex_health: bs.Texture + """Health powerup bs.Texture.""" - tex_land_mines: ba.Texture - """Land-mine powerup ba.Texture.""" + tex_land_mines: bs.Texture + """Land-mine powerup bs.Texture.""" - tex_curse: ba.Texture - """Curse powerup ba.Texture.""" + tex_curse: bs.Texture + """Curse powerup bs.Texture.""" - health_powerup_sound: ba.Sound - """ba.Sound played when a health powerup is accepted.""" + health_powerup_sound: bs.Sound + """bs.Sound played when a health powerup is accepted.""" - powerup_sound: ba.Sound - """ba.Sound played when a powerup is accepted.""" + powerup_sound: bs.Sound + """bs.Sound played when a powerup is accepted.""" - powerdown_sound: ba.Sound - """ba.Sound that can be used when powerups wear off.""" + powerdown_sound: bs.Sound + """bs.Sound that can be used when powerups wear off.""" - powerup_material: ba.Material - """ba.Material applied to powerup boxes.""" + powerup_material: bs.Material + """bs.Material applied to powerup boxes.""" - powerup_accept_material: ba.Material - """Powerups will send a ba.PowerupMessage to anything they touch - that has this ba.Material applied.""" + powerup_accept_material: bs.Material + """Powerups will send a bs.PowerupMessage to anything they touch + that has this bs.Material applied.""" - _STORENAME = ba.storagename() + _STORENAME = bs.storagename() def __init__(self) -> None: """Instantiate a PowerupBoxFactory. @@ -86,31 +86,31 @@ class PowerupBoxFactory: You shouldn't need to do this; call Powerup.get_factory() to get a shared instance. """ - from ba.internal import get_default_powerup_distribution + from bascenev1.internal import get_default_powerup_distribution shared = SharedObjects.get() self._lastpoweruptype: str | None = None - self.model = ba.getmodel('powerup') - self.model_simple = ba.getmodel('powerupSimple') - self.tex_bomb = ba.gettexture('powerupBomb') - self.tex_punch = ba.gettexture('powerupPunch') - self.tex_ice_bombs = ba.gettexture('powerupIceBombs') - self.tex_sticky_bombs = ba.gettexture('powerupStickyBombs') - self.tex_shield = ba.gettexture('powerupShield') - self.tex_impact_bombs = ba.gettexture('powerupImpactBombs') - self.tex_health = ba.gettexture('powerupHealth') - self.tex_land_mines = ba.gettexture('powerupLandMines') - self.tex_curse = ba.gettexture('powerupCurse') - self.health_powerup_sound = ba.getsound('healthPowerup') - self.powerup_sound = ba.getsound('powerup01') - self.powerdown_sound = ba.getsound('powerdown01') - self.drop_sound = ba.getsound('boxDrop') + self.mesh = bs.getmesh('powerup') + self.mesh_simple = bs.getmesh('powerupSimple') + self.tex_bomb = bs.gettexture('powerupBomb') + self.tex_punch = bs.gettexture('powerupPunch') + self.tex_ice_bombs = bs.gettexture('powerupIceBombs') + self.tex_sticky_bombs = bs.gettexture('powerupStickyBombs') + self.tex_shield = bs.gettexture('powerupShield') + self.tex_impact_bombs = bs.gettexture('powerupImpactBombs') + self.tex_health = bs.gettexture('powerupHealth') + self.tex_land_mines = bs.gettexture('powerupLandMines') + self.tex_curse = bs.gettexture('powerupCurse') + self.health_powerup_sound = bs.getsound('healthPowerup') + self.powerup_sound = bs.getsound('powerup01') + self.powerdown_sound = bs.getsound('powerdown01') + self.drop_sound = bs.getsound('boxDrop') # Material for powerups. - self.powerup_material = ba.Material() + self.powerup_material = bs.Material() # Material for anyone wanting to accept powerups. - self.powerup_accept_material = ba.Material() + self.powerup_accept_material = bs.Material() # Pass a powerup-touched message to applicable stuff. self.powerup_material.add_actions( @@ -145,7 +145,7 @@ class PowerupBoxFactory: ) -> str: """Returns a random powerup type (string). - See ba.Powerup.poweruptype for available type values. + See bs.Powerup.poweruptype for available type values. There are certain non-random aspects to this; a 'curse' powerup, for instance, is always followed by a 'health' powerup (to keep things @@ -175,10 +175,10 @@ class PowerupBoxFactory: @classmethod def get(cls) -> PowerupBoxFactory: - """Return a shared ba.PowerupBoxFactory object, creating if needed.""" - activity = ba.getactivity() + """Return a shared bs.PowerupBoxFactory object, creating if needed.""" + activity = bs.getactivity() if activity is None: - raise ba.ContextError('No current activity.') + raise bs.ContextError('No current activity.') factory = activity.customdata.get(cls._STORENAME) if factory is None: factory = activity.customdata[cls._STORENAME] = PowerupBoxFactory() @@ -186,13 +186,13 @@ class PowerupBoxFactory: return factory -class PowerupBox(ba.Actor): +class PowerupBox(bs.Actor): """A box that grants a powerup. category: Gameplay Classes - This will deliver a ba.PowerupMessage to anything that touches it - which has the ba.PowerupBoxFactory.powerup_accept_material applied. + This will deliver a bs.PowerupMessage to anything that touches it + which has the bs.PowerupBoxFactory.powerup_accept_material applied. """ poweruptype: str @@ -200,8 +200,8 @@ class PowerupBox(ba.Actor): 'ice_bombs', 'impact_bombs', 'land_mines', 'sticky_bombs', 'shield', 'health', or 'curse'.""" - node: ba.Node - """The 'prop' ba.Node representing this box.""" + node: bs.Node + """The 'prop' bs.Node representing this box.""" def __init__( self, @@ -211,7 +211,7 @@ class PowerupBox(ba.Actor): ): """Create a powerup-box of the requested type at the given position. - see ba.Powerup.poweruptype for valid type strings. + see bs.Powerup.poweruptype for valid type strings. """ super().__init__() @@ -244,34 +244,34 @@ class PowerupBox(ba.Actor): if len(position) != 3: raise ValueError('expected 3 floats for position') - self.node = ba.newnode( + self.node = bs.newnode( 'prop', delegate=self, attrs={ 'body': 'box', 'position': position, - 'model': factory.model, - 'light_model': factory.model_simple, + 'mesh': factory.mesh, + 'light_mesh': factory.mesh_simple, 'shadow_size': 0.5, 'color_texture': tex, 'reflection': 'powerup', 'reflection_scale': [1.0], 'materials': (factory.powerup_material, shared.object_material), }, - ) # yapf: disable + ) # Animate in. - curve = ba.animate(self.node, 'model_scale', {0: 0, 0.14: 1.6, 0.2: 1}) - ba.timer(0.2, curve.delete) + curve = bs.animate(self.node, 'mesh_scale', {0: 0, 0.14: 1.6, 0.2: 1}) + bs.timer(0.2, curve.delete) if expire: - ba.timer( + bs.timer( DEFAULT_POWERUP_INTERVAL - 2.5, - ba.WeakCall(self._start_flashing), + bs.WeakCall(self._start_flashing), ) - ba.timer( + bs.timer( DEFAULT_POWERUP_INTERVAL - 1.0, - ba.WeakCall(self.handlemessage, ba.DieMessage()), + bs.WeakCall(self.handlemessage, bs.DieMessage()), ) def _start_flashing(self) -> None: @@ -281,39 +281,40 @@ class PowerupBox(ba.Actor): def handlemessage(self, msg: Any) -> Any: assert not self.expired - if isinstance(msg, ba.PowerupAcceptMessage): + if isinstance(msg, bs.PowerupAcceptMessage): factory = PowerupBoxFactory.get() assert self.node if self.poweruptype == 'health': - ba.playsound( - factory.health_powerup_sound, 3, position=self.node.position + factory.health_powerup_sound.play( + 3, position=self.node.position ) - ba.playsound(factory.powerup_sound, 3, position=self.node.position) + + factory.powerup_sound.play(3, position=self.node.position) self._powersgiven = True - self.handlemessage(ba.DieMessage()) + self.handlemessage(bs.DieMessage()) elif isinstance(msg, _TouchedMessage): if not self._powersgiven: - node = ba.getcollision().opposingnode + node = bs.getcollision().opposingnode node.handlemessage( - ba.PowerupMessage(self.poweruptype, sourcenode=self.node) + bs.PowerupMessage(self.poweruptype, sourcenode=self.node) ) - elif isinstance(msg, ba.DieMessage): + elif isinstance(msg, bs.DieMessage): if self.node: if msg.immediate: self.node.delete() else: - ba.animate(self.node, 'model_scale', {0: 1, 0.1: 0}) - ba.timer(0.1, self.node.delete) + bs.animate(self.node, 'mesh_scale', {0: 1, 0.1: 0}) + bs.timer(0.1, self.node.delete) - elif isinstance(msg, ba.OutOfBoundsMessage): - self.handlemessage(ba.DieMessage()) + elif isinstance(msg, bs.OutOfBoundsMessage): + self.handlemessage(bs.DieMessage()) - elif isinstance(msg, ba.HitMessage): + elif isinstance(msg, bs.HitMessage): # Don't die on punches (that's annoying). if msg.hit_type != 'punch': - self.handlemessage(ba.DieMessage()) + self.handlemessage(bs.DieMessage()) else: return super().handlemessage(msg) return None diff --git a/assets/src/ba_data/python/bastd/actor/respawnicon.py b/src/assets/ba_data/python/bastd/actor/respawnicon.py similarity index 72% rename from assets/src/ba_data/python/bastd/actor/respawnicon.py rename to src/assets/ba_data/python/bastd/actor/respawnicon.py index 0679522c..1b5da40c 100644 --- a/assets/src/ba_data/python/bastd/actor/respawnicon.py +++ b/src/assets/ba_data/python/bastd/actor/respawnicon.py @@ -7,7 +7,8 @@ from __future__ import annotations import weakref from typing import TYPE_CHECKING -import ba +import babase +import bascenev1 as bs if TYPE_CHECKING: pass @@ -18,14 +19,14 @@ class RespawnIcon: category: Gameplay Classes - This is used to indicate that a ba.Player is waiting to respawn. + This is used to indicate that a bascenev1.Player is waiting to respawn. """ - _MASKTEXSTORENAME = ba.storagename('masktex') - _ICONSSTORENAME = ba.storagename('icons') + _MASKTEXSTORENAME = babase.storagename('masktex') + _ICONSSTORENAME = babase.storagename('icons') - def __init__(self, player: ba.Player, respawn_time: float): - """Instantiate with a ba.Player and respawn_time (in seconds).""" + def __init__(self, player: bs.Player, respawn_time: float): + """Instantiate with a bascenev1.Player and respawn_time (in seconds).""" self._visible = True on_right, offs_extra, respawn_icons = self._get_context(player) @@ -33,9 +34,9 @@ class RespawnIcon: # Cache our mask tex on the team for easy access. mask_tex = player.team.customdata.get(self._MASKTEXSTORENAME) if mask_tex is None: - mask_tex = ba.gettexture('characterIconMask') + mask_tex = bs.gettexture('characterIconMask') player.team.customdata[self._MASKTEXSTORENAME] = mask_tex - assert isinstance(mask_tex, ba.Texture) + assert isinstance(mask_tex, bs.Texture) # Now find the first unused slot and use that. index = 0 @@ -52,8 +53,8 @@ class RespawnIcon: texture = icon['texture'] h_offs = -10 ipos = (-40 - h_offs if on_right else 40 + h_offs, -180 + offs) - self._image: ba.NodeActor | None = ba.NodeActor( - ba.newnode( + self._image: bs.NodeActor | None = bs.NodeActor( + bs.newnode( 'image', attrs={ 'texture': texture, @@ -71,22 +72,22 @@ class RespawnIcon: ) assert self._image.node - ba.animate(self._image.node, 'opacity', {0.0: 0, 0.2: 0.7}) + bs.animate(self._image.node, 'opacity', {0.0: 0, 0.2: 0.7}) npos = (-40 - h_offs if on_right else 40 + h_offs, -205 + 49 + offs) - self._name: ba.NodeActor | None = ba.NodeActor( - ba.newnode( + self._name: bs.NodeActor | None = bs.NodeActor( + bs.newnode( 'text', attrs={ 'v_attach': 'top', 'h_attach': 'right' if on_right else 'left', - 'text': ba.Lstr(value=player.getname()), + 'text': babase.Lstr(value=player.getname()), 'maxwidth': 100, 'h_align': 'center', 'v_align': 'center', 'shadow': 1.0, 'flatness': 1.0, - 'color': ba.safecolor(icon['tint_color']), + 'color': babase.safecolor(icon['tint_color']), 'scale': 0.5, 'position': npos, }, @@ -94,11 +95,11 @@ class RespawnIcon: ) assert self._name.node - ba.animate(self._name.node, 'scale', {0: 0, 0.1: 0.5}) + bs.animate(self._name.node, 'scale', {0: 0, 0.1: 0.5}) tpos = (-60 - h_offs if on_right else 60 + h_offs, -192 + offs) - self._text: ba.NodeActor | None = ba.NodeActor( - ba.newnode( + self._text: bs.NodeActor | None = bs.NodeActor( + bs.newnode( 'text', attrs={ 'position': tpos, @@ -108,19 +109,19 @@ class RespawnIcon: 'shadow': 0.5, 'flatness': 0.5, 'v_attach': 'top', - 'color': ba.safecolor(icon['tint_color']), + 'color': babase.safecolor(icon['tint_color']), 'text': '', }, ) ) assert self._text.node - ba.animate(self._text.node, 'scale', {0: 0, 0.1: 0.9}) + bs.animate(self._text.node, 'scale', {0: 0, 0.1: 0.9}) - self._respawn_time = ba.time() + respawn_time + self._respawn_time = babase.apptime() + respawn_time self._update() - self._timer: ba.Timer | None = ba.Timer( - 1.0, ba.WeakCall(self._update), repeat=True + self._timer: bs.Timer | None = bs.Timer( + 1.0, babase.WeakCall(self._update), repeat=True ) @property @@ -128,11 +129,11 @@ class RespawnIcon: """Is this icon still visible?""" return self._visible - def _get_context(self, player: ba.Player) -> tuple[bool, float, dict]: + def _get_context(self, player: bs.Player) -> tuple[bool, float, dict]: """Return info on where we should be shown and stored.""" - activity = ba.getactivity() + activity = bs.getactivity() - if isinstance(ba.getsession(), ba.DualTeamSession): + if isinstance(bs.getsession(), bs.DualTeamSession): on_right = player.team.id % 2 == 1 # Store a list of icons in the team. @@ -151,14 +152,14 @@ class RespawnIcon: activity.customdata[self._ICONSSTORENAME] = icons = {} assert isinstance(icons, dict) - if isinstance(activity.session, ba.FreeForAllSession): + if isinstance(activity.session, bs.FreeForAllSession): offs_extra = -150 else: offs_extra = -20 return on_right, offs_extra, icons def _update(self) -> None: - remaining = int(round(self._respawn_time - ba.time())) + remaining = int(round(self._respawn_time - babase.apptime())) if remaining > 0: assert self._text is not None if self._text.node: diff --git a/assets/src/ba_data/python/bastd/actor/scoreboard.py b/src/assets/ba_data/python/bastd/actor/scoreboard.py similarity index 88% rename from assets/src/ba_data/python/bastd/actor/scoreboard.py rename to src/assets/ba_data/python/bastd/actor/scoreboard.py index de4f8677..0cdc8b5f 100644 --- a/assets/src/ba_data/python/bastd/actor/scoreboard.py +++ b/src/assets/ba_data/python/bastd/actor/scoreboard.py @@ -7,7 +7,7 @@ from __future__ import annotations import weakref from typing import TYPE_CHECKING -import ba +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence @@ -17,10 +17,10 @@ class _Entry: def __init__( self, scoreboard: Scoreboard, - team: ba.Team, + team: bs.Team, do_cover: bool, scale: float, - label: ba.Lstr | None, + label: bs.Lstr | None, flash_length: float, ): # pylint: disable=too-many-statements @@ -32,20 +32,20 @@ class _Entry: self._height = 32.0 * self._scale self._bar_width = 2.0 * self._scale self._bar_height = 32.0 * self._scale - self._bar_tex = self._backing_tex = ba.gettexture('bar') - self._cover_tex = ba.gettexture('uiAtlas') - self._model = ba.getmodel('meterTransparent') + self._bar_tex = self._backing_tex = bs.gettexture('bar') + self._cover_tex = bs.gettexture('uiAtlas') + self._mesh = bs.getmesh('meterTransparent') self._pos: Sequence[float] | None = None - self._flash_timer: ba.Timer | None = None + self._flash_timer: bs.Timer | None = None self._flash_counter: int | None = None self._flash_colors: bool | None = None self._score: float | None = None - safe_team_color = ba.safecolor(team.color, target_intensity=1.0) + safe_team_color = bs.safecolor(team.color, target_intensity=1.0) # FIXME: Should not do things conditionally for vr-mode, as there may # be non-vr clients connected which will also get these value. - vrmode = ba.app.vr_mode + vrmode = bs.app.vr_mode if self._do_cover: if vrmode: @@ -56,8 +56,8 @@ class _Entry: self._backing_color = [0.05 + c * 0.1 for c in safe_team_color] opacity = (0.8 if vrmode else 0.8) if self._do_cover else 0.5 - self._backing = ba.NodeActor( - ba.newnode( + self._backing = bs.NodeActor( + bs.newnode( 'image', attrs={ 'scale': (self._width, self._height), @@ -71,8 +71,8 @@ class _Entry: ) self._barcolor = safe_team_color - self._bar = ba.NodeActor( - ba.newnode( + self._bar = bs.NodeActor( + bs.newnode( 'image', attrs={ 'opacity': 0.7, @@ -83,7 +83,7 @@ class _Entry: ) ) - self._bar_scale = ba.newnode( + self._bar_scale = bs.newnode( 'combine', owner=self._bar.node, attrs={ @@ -94,7 +94,7 @@ class _Entry: ) assert self._bar.node self._bar_scale.connectattr('output', self._bar.node, 'scale') - self._bar_position = ba.newnode( + self._bar_position = bs.newnode( 'combine', owner=self._bar.node, attrs={'size': 2, 'input0': 0, 'input1': 0}, @@ -102,8 +102,8 @@ class _Entry: self._bar_position.connectattr('output', self._bar.node, 'position') self._cover_color = safe_team_color if self._do_cover: - self._cover = ba.NodeActor( - ba.newnode( + self._cover = bs.NodeActor( + bs.newnode( 'image', attrs={ 'scale': (self._width * 1.15, self._height * 1.6), @@ -112,7 +112,7 @@ class _Entry: 'vr_depth': 2, 'attach': 'topLeft', 'texture': self._cover_tex, - 'model_transparent': self._model, + 'mesh_transparent': self._mesh, }, ) ) @@ -120,8 +120,8 @@ class _Entry: clr = safe_team_color maxwidth = 130.0 * (1.0 - scoreboard.score_split) flatness = (1.0 if vrmode else 0.5) if self._do_cover else 1.0 - self._score_text = ba.NodeActor( - ba.newnode( + self._score_text = bs.NodeActor( + bs.newnode( 'text', attrs={ 'h_attach': 'left', @@ -141,7 +141,7 @@ class _Entry: clr = safe_team_color - team_name_label: str | ba.Lstr + team_name_label: str | bs.Lstr if label is not None: team_name_label = label else: @@ -149,23 +149,22 @@ class _Entry: # We do our own clipping here; should probably try to tap into some # existing functionality. - if isinstance(team_name_label, ba.Lstr): - + if isinstance(team_name_label, bs.Lstr): # Hmmm; if the team-name is a non-translatable value lets go # ahead and clip it otherwise we leave it as-is so # translation can occur.. if team_name_label.is_flat_value(): val = team_name_label.evaluate() if len(val) > 10: - team_name_label = ba.Lstr(value=val[:10] + '...') + team_name_label = bs.Lstr(value=val[:10] + '...') else: if len(team_name_label) > 10: team_name_label = team_name_label[:10] + '...' - team_name_label = ba.Lstr(value=team_name_label) + team_name_label = bs.Lstr(value=team_name_label) flatness = (1.0 if vrmode else 0.5) if self._do_cover else 1.0 - self._name_text = ba.NodeActor( - ba.newnode( + self._name_text = bs.NodeActor( + bs.newnode( 'text', attrs={ 'h_attach': 'left', @@ -185,8 +184,8 @@ class _Entry: def flash(self, countdown: bool, extra_flash: bool) -> None: """Flash momentarily.""" - self._flash_timer = ba.Timer( - 0.1, ba.WeakCall(self._do_flash), repeat=True + self._flash_timer = bs.Timer( + 0.1, bs.WeakCall(self._do_flash), repeat=True ) if countdown: self._flash_counter = 10 @@ -230,7 +229,7 @@ class _Entry: def _set_flash_colors(self, flash: bool) -> None: self._flash_colors = flash - def _safesetcolor(node: ba.Node | None, val: Any) -> None: + def _safesetcolor(node: bs.Node | None, val: Any) -> None: if node: node.color = val @@ -315,13 +314,13 @@ class _Entry: ) cur_width = self._bar_scale.input0 - ba.animate( + bs.animate( self._bar_scale, 'input0', {0.0: cur_width, 0.25: self._bar_width} ) self._bar_scale.input1 = self._bar_height cur_x = self._bar_position.input0 assert self._pos is not None - ba.animate( + bs.animate( self._bar_position, 'input0', {0.0: cur_x, 0.25: self._pos[0] + self._bar_width / 2}, @@ -337,7 +336,7 @@ class _Entry: class _EntryProxy: """Encapsulates adding/removing of a scoreboard Entry.""" - def __init__(self, scoreboard: Scoreboard, team: ba.Team): + def __init__(self, scoreboard: Scoreboard, team: bs.Team): self._scoreboard = weakref.ref(scoreboard) # Have to store ID here instead of a weak-ref since the team will be @@ -354,8 +353,8 @@ class _EntryProxy: return try: - ba.pushcall(ba.Call(scoreboard.remove_team, self._team_id)) - except ba.ContextError: + bs.pushcall(bs.Call(scoreboard.remove_team, self._team_id)) + except bs.ContextError: # This happens if we fire after the activity expires. # In that case we don't need to do anything. pass @@ -367,22 +366,22 @@ class Scoreboard: category: Gameplay Classes """ - _ENTRYSTORENAME = ba.storagename('entry') + _ENTRYSTORENAME = bs.storagename('entry') - def __init__(self, label: ba.Lstr | None = None, score_split: float = 0.7): + def __init__(self, label: bs.Lstr | None = None, score_split: float = 0.7): """Instantiate a scoreboard. Label can be something like 'points' and will show up on boards if provided. """ - self._flat_tex = ba.gettexture('null') + self._flat_tex = bs.gettexture('null') self._entries: dict[int, _Entry] = {} self._label = label self.score_split = score_split # For free-for-all we go simpler since we have one per player. self._pos: Sequence[float] - if isinstance(ba.getsession(), ba.FreeForAllSession): + if isinstance(bs.getsession(), bs.FreeForAllSession): self._do_cover = False self._spacing = 35.0 self._pos = (17.0, -65.0) @@ -397,14 +396,14 @@ class Scoreboard: def set_team_value( self, - team: ba.Team, + team: bs.Team, score: float, max_score: float | None = None, countdown: bool = False, flash: bool = True, show_value: bool = True, ) -> None: - """Update the score-board display for the given ba.Team.""" + """Update the score-board display for the given bs.Team.""" if team.id not in self._entries: self._add_team(team) @@ -422,7 +421,7 @@ class Scoreboard: show_value=show_value, ) - def _add_team(self, team: ba.Team) -> None: + def _add_team(self, team: bs.Team) -> None: if team.id in self._entries: raise RuntimeError('Duplicate team add') self._entries[team.id] = _Entry( diff --git a/assets/src/ba_data/python/bastd/actor/spawner.py b/src/assets/ba_data/python/bastd/actor/spawner.py similarity index 89% rename from assets/src/ba_data/python/bastd/actor/spawner.py rename to src/assets/ba_data/python/bastd/actor/spawner.py index 3d006a26..a4a38b90 100644 --- a/assets/src/ba_data/python/bastd/actor/spawner.py +++ b/src/assets/ba_data/python/bastd/actor/spawner.py @@ -6,7 +6,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence, Callable @@ -29,7 +29,7 @@ class Spawner: """ spawner: Spawner - """The ba.Spawner we came from.""" + """The bascenev1.Spawner we came from.""" data: Any """The data object passed by the user.""" @@ -63,11 +63,11 @@ class Spawner: """ self._spawn_callback = spawn_callback self._send_spawn_message = send_spawn_message - self._spawner_sound = ba.getsound('swip2') + self._spawner_sound = bs.getsound('swip2') self._data = data self._pt = pt # create a light where the spawn will happen - self._light = ba.newnode( + self._light = bs.newnode( 'light', attrs={ 'position': tuple(pt), @@ -79,8 +79,8 @@ class Spawner: scl = float(spawn_time) / 3.75 min_val = 0.4 max_val = 0.7 - ba.playsound(self._spawner_sound, position=self._light.position) - ba.animate( + self._spawner_sound.play(position=self._light.position) + bs.animate( self._light, 'intensity', { @@ -103,15 +103,15 @@ class Spawner: 4.000 * scl: 0.0, }, ) - ba.timer(spawn_time, self._spawn) + bs.timer(spawn_time, self._spawn) def _spawn(self) -> None: - ba.timer(1.0, self._light.delete) + bs.timer(1.0, self._light.delete) if self._spawn_callback is not None: self._spawn_callback() if self._send_spawn_message: # only run if our activity still exists - activity = ba.getactivity() + activity = bs.getactivity() if activity is not None: activity.handlemessage( self.SpawnMessage(self, self._data, self._pt) diff --git a/assets/src/ba_data/python/bastd/actor/spaz.py b/src/assets/ba_data/python/bastd/actor/spaz.py similarity index 83% rename from assets/src/ba_data/python/bastd/actor/spaz.py rename to src/assets/ba_data/python/bastd/actor/spaz.py index 67b50b43..b19d0105 100644 --- a/assets/src/ba_data/python/bastd/actor/spaz.py +++ b/src/assets/ba_data/python/bastd/actor/spaz.py @@ -6,13 +6,14 @@ from __future__ import annotations import random +import logging from typing import TYPE_CHECKING -import ba -from bastd.actor import bomb as stdbomb +from bastd.actor.bomb import Bomb, Blast from bastd.actor.powerupbox import PowerupBoxFactory from bastd.actor.spazfactory import SpazFactory from bastd.gameutils import SharedObjects +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence, Callable @@ -37,7 +38,7 @@ class BombDiedMessage: """A bomb has died and thus can be recycled.""" -class Spaz(ba.Actor): +class Spaz(bs.Actor): """ Base class for various Spazzes. @@ -53,8 +54,8 @@ class Spaz(ba.Actor): # pylint: disable=too-many-public-methods # pylint: disable=too-many-locals - node: ba.Node - """The 'spaz' ba.Node.""" + node: bs.Node + """The 'spaz' bs.Node.""" points_mult = 1 curse_time: float | None = 5.0 @@ -68,7 +69,7 @@ class Spaz(ba.Actor): color: Sequence[float] = (1.0, 1.0, 1.0), highlight: Sequence[float] = (0.5, 0.5, 0.5), character: str = 'Spaz', - source_player: ba.Player | None = None, + source_player: bs.Player | None = None, start_invincible: bool = True, can_accept_powerups: bool = True, powerups_expire: bool = False, @@ -97,14 +98,14 @@ class Spaz(ba.Actor): self._punch_power_scale = 1.2 else: self._punch_power_scale = factory.punch_power_scale - self.fly = ba.getactivity().globalsnode.happy_thoughts_mode - if isinstance(activity, ba.GameActivity): + self.fly = bs.getactivity().globalsnode.happy_thoughts_mode + if isinstance(activity, bs.GameActivity): self._hockey = activity.map.is_hockey else: self._hockey = False - self._punched_nodes: set[ba.Node] = set() + self._punched_nodes: set[bs.Node] = set() self._cursed = False - self._connected_to_player: ba.Player | None = None + self._connected_to_player: bs.Player | None = None materials = [ factory.spaz_material, shared.object_material, @@ -122,7 +123,7 @@ class Spaz(ba.Actor): media = factory.get_media(character) punchmats = (factory.punch_material, shared.attack_material) pickupmats = (factory.pickup_material, shared.pickup_material) - self.node: ba.Node = ba.newnode( + self.node: bs.Node = bs.newnode( type='spaz', delegate=self, attrs={ @@ -138,15 +139,15 @@ class Spaz(ba.Actor): 'fall_sounds': media['fall_sounds'], 'color_texture': media['color_texture'], 'color_mask_texture': media['color_mask_texture'], - 'head_model': media['head_model'], - 'torso_model': media['torso_model'], - 'pelvis_model': media['pelvis_model'], - 'upper_arm_model': media['upper_arm_model'], - 'forearm_model': media['forearm_model'], - 'hand_model': media['hand_model'], - 'upper_leg_model': media['upper_leg_model'], - 'lower_leg_model': media['lower_leg_model'], - 'toes_model': media['toes_model'], + 'head_mesh': media['head_mesh'], + 'torso_mesh': media['torso_mesh'], + 'pelvis_mesh': media['pelvis_mesh'], + 'upper_arm_mesh': media['upper_arm_mesh'], + 'forearm_mesh': media['forearm_mesh'], + 'hand_mesh': media['hand_mesh'], + 'upper_leg_mesh': media['upper_leg_mesh'], + 'lower_leg_mesh': media['lower_leg_mesh'], + 'toes_mesh': media['toes_mesh'], 'style': factory.get_style(character), 'fly': self.fly, 'hockey': self._hockey, @@ -159,28 +160,28 @@ class Spaz(ba.Actor): 'source_player': source_player, }, ) - self.shield: ba.Node | None = None + self.shield: bs.Node | None = None if start_invincible: - def _safesetattr(node: ba.Node | None, attr: str, val: Any) -> None: + def _safesetattr(node: bs.Node | None, attr: str, val: Any) -> None: if node: setattr(node, attr, val) - ba.timer(1.0, ba.Call(_safesetattr, self.node, 'invincible', False)) + bs.timer(1.0, bs.Call(_safesetattr, self.node, 'invincible', False)) self.hitpoints = 1000 self.hitpoints_max = 1000 self.shield_hitpoints: int | None = None self.shield_hitpoints_max = 650 self.shield_decay_rate = 0 - self.shield_decay_timer: ba.Timer | None = None - self._boxing_gloves_wear_off_timer: ba.Timer | None = None - self._boxing_gloves_wear_off_flash_timer: ba.Timer | None = None - self._bomb_wear_off_timer: ba.Timer | None = None - self._bomb_wear_off_flash_timer: ba.Timer | None = None - self._multi_bomb_wear_off_timer: ba.Timer | None = None - self._multi_bomb_wear_off_flash_timer: ba.Timer | None = None - self._curse_timer: ba.Timer | None = None + self.shield_decay_timer: bs.Timer | None = None + self._boxing_gloves_wear_off_timer: bs.Timer | None = None + self._boxing_gloves_wear_off_flash_timer: bs.Timer | None = None + self._bomb_wear_off_timer: bs.Timer | None = None + self._bomb_wear_off_flash_timer: bs.Timer | None = None + self._multi_bomb_wear_off_timer: bs.Timer | None = None + self._multi_bomb_wear_off_flash_timer: bs.Timer | None = None + self._curse_timer: bs.Timer | None = None self.bomb_count = self.default_bomb_count self._max_bomb_count = self.default_bomb_count self.bomb_type_default = self.default_bomb_type @@ -214,10 +215,10 @@ class Spaz(ba.Actor): self._bomb_held = False if self.default_shields: self.equip_shields() - self._dropped_bomb_callbacks: list[Callable[[Spaz, ba.Actor], Any]] = [] + self._dropped_bomb_callbacks: list[Callable[[Spaz, bs.Actor], Any]] = [] - self._score_text: ba.Node | None = None - self._score_text_hide_timer: ba.Timer | None = None + self._score_text: bs.Node | None = None + self._score_text_hide_timer: bs.Timer | None = None self._last_stand_pos: Sequence[float] | None = None # Deprecated stuff.. should make these into lists. @@ -236,7 +237,7 @@ class Spaz(ba.Actor): self.pick_up_powerup_callback = None def add_dropped_bomb_callback( - self, call: Callable[[Spaz, ba.Actor], Any] + self, call: Callable[[Spaz, bs.Actor], Any] ) -> None: """ Add a call to be run whenever this Spaz drops a bomb. @@ -254,7 +255,7 @@ class Spaz(ba.Actor): def _hide_score_text(self) -> None: if self._score_text: assert isinstance(self._score_text.scale, float) - ba.animate( + bs.animate( self._score_text, 'scale', {0.0: self._score_text.scale, 0.2: 0.0}, @@ -265,9 +266,7 @@ class Spaz(ba.Actor): Can pass all button presses through here; if we see an obscene number of them in a short time let's shame/pushish this guy for using turbo. """ - t_ms = ba.time( - timetype=ba.TimeType.BASE, timeformat=ba.TimeFormat.MILLISECONDS - ) + t_ms = int(bs.basetime() * 1000.0) assert isinstance(t_ms, int) t_bucket = int(t_ms / 1000) if t_bucket == self._turbo_filter_time_bucket: @@ -279,7 +278,7 @@ class Spaz(ba.Actor): ) self._turbo_filter_times[source] = t_ms # (uncomment to debug; prints what this count is at) - # ba.screenmessage( str(source) + " " + # bs.screenmessage( str(source) + " " # + str(self._turbo_filter_counts[source])) if self._turbo_filter_counts[source] == 15: # Knock 'em out. That'll learn 'em. @@ -287,11 +286,12 @@ class Spaz(ba.Actor): self.node.handlemessage('knockout', 500.0) # Also issue periodic notices about who is turbo-ing. - now = ba.time(ba.TimeType.REAL) - if now > ba.app.last_spaz_turbo_warn_time + 30.0: - ba.app.last_spaz_turbo_warn_time = now - ba.screenmessage( - ba.Lstr( + now = bs.apptime() + assert bs.app.classic is not None + if now > bs.app.classic.last_spaz_turbo_warn_time + 30.0: + bs.app.classic.last_spaz_turbo_warn_time = now + bs.screenmessage( + bs.Lstr( translate=( 'statements', ( @@ -304,7 +304,7 @@ class Spaz(ba.Actor): ), color=(1, 0.5, 0), ) - ba.playsound(ba.getsound('error')) + bs.getsound('error').play() else: self._turbo_filter_times = {} self._turbo_filter_time_bucket = t_bucket @@ -312,7 +312,7 @@ class Spaz(ba.Actor): def set_score_text( self, - text: str | ba.Lstr, + text: str | bs.Lstr, color: Sequence[float] = (1.0, 1.0, 0.4), flash: bool = False, ) -> None: @@ -320,18 +320,18 @@ class Spaz(ba.Actor): Utility func to show a message momentarily over our spaz that follows him around; Handy for score updates and things. """ - color_fin = ba.safecolor(color)[:3] + color_fin = bs.safecolor(color)[:3] if not self.node: return if not self._score_text: start_scale = 0.0 - mnode = ba.newnode( + mnode = bs.newnode( 'math', owner=self.node, attrs={'input1': (0, 1.4, 0), 'operation': 'add'}, ) self.node.connectattr('torso_position', mnode, 'input2') - self._score_text = ba.newnode( + self._score_text = bs.newnode( 'text', owner=self.node, attrs={ @@ -351,7 +351,7 @@ class Spaz(ba.Actor): start_scale = self._score_text.scale self._score_text.text = text if flash: - combine = ba.newnode( + combine = bs.newnode( 'combine', owner=self._score_text, attrs={'size': 3} ) scl = 1.8 @@ -360,16 +360,16 @@ class Spaz(ba.Actor): for i in range(3): cl1 = offs + scl * color_fin[i] cl2 = color_fin[i] - ba.animate( + bs.animate( combine, 'input' + str(i), {0.5 * tval: cl2, 0.75 * tval: cl1, 1.0 * tval: cl2}, ) combine.connectattr('output', self._score_text, 'color') - ba.animate(self._score_text, 'scale', {0.0: start_scale, 0.2: 0.02}) - self._score_text_hide_timer = ba.Timer( - 1.0, ba.WeakCall(self._hide_score_text) + bs.animate(self._score_text, 'scale', {0.0: start_scale, 0.2: 0.02}) + self._score_text_hide_timer = bs.Timer( + 1.0, bs.WeakCall(self._hide_score_text) ) def on_jump_press(self) -> None: @@ -379,7 +379,7 @@ class Spaz(ba.Actor): """ if not self.node: return - t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + t_ms = int(bs.time() * 1000.0) assert isinstance(t_ms, int) if t_ms - self.last_jump_time_ms >= self._jump_cooldown: self.node.jump_pressed = True @@ -402,7 +402,7 @@ class Spaz(ba.Actor): """ if not self.node: return - t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + t_ms = int(bs.time() * 1000.0) assert isinstance(t_ms, int) if t_ms - self.last_pickup_time_ms >= self._pickup_cooldown: self.node.pickup_pressed = True @@ -444,7 +444,7 @@ class Spaz(ba.Actor): """ if not self.node or self.frozen or self.node.knockout > 0.0: return - t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + t_ms = int(bs.time() * 1000.0) assert isinstance(t_ms, int) if t_ms - self.last_punch_time_ms >= self._punch_cooldown: if self.punch_callback is not None: @@ -453,9 +453,9 @@ class Spaz(ba.Actor): self.last_punch_time_ms = t_ms self.node.punch_pressed = True if not self.node.hold_node: - ba.timer( + bs.timer( 0.1, - ba.WeakCall( + bs.WeakCall( self._safe_play_sound, SpazFactory.get().swish_sound, 0.8, @@ -463,10 +463,10 @@ class Spaz(ba.Actor): ) self._turbo_filter_add_press('punch') - def _safe_play_sound(self, sound: ba.Sound, volume: float) -> None: + def _safe_play_sound(self, sound: bs.Sound, volume: float) -> None: """Plays a sound at our position if we exist.""" if self.node: - ba.playsound(sound, volume, self.node.position) + sound.play(volume, self.node.position) def on_punch_release(self) -> None: """ @@ -489,7 +489,7 @@ class Spaz(ba.Actor): return if self.node.knockout > 0.0: return - t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + t_ms = int(bs.time() * 1000.0) assert isinstance(t_ms, int) if t_ms - self.last_bomb_time_ms >= self._bomb_cooldown: self.last_bomb_time_ms = t_ms @@ -515,7 +515,7 @@ class Spaz(ba.Actor): if not self.node: return - t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + t_ms = int(bs.time() * 1000.0) assert isinstance(t_ms, int) self.last_run_time_ms = t_ms self.node.run = value @@ -584,7 +584,7 @@ class Spaz(ba.Actor): def on_punched(self, damage: int) -> None: """Called when this spaz gets punched.""" - def get_death_points(self, how: ba.DeathType) -> tuple[int, int]: + def get_death_points(self, how: bs.DeathType) -> tuple[int, int]: """Get the points awarded for killing this spaz.""" del how # Unused. num_hits = float(max(1, self._num_times_hit)) @@ -616,13 +616,13 @@ class Spaz(ba.Actor): self.node.curse_death_time = -1 else: # Note: curse-death-time takes milliseconds. - tval = ba.time() + tval = bs.time() assert isinstance(tval, (float, int)) self.node.curse_death_time = int( 1000.0 * (tval + self.curse_time) ) - self._curse_timer = ba.Timer( - 5.0, ba.WeakCall(self.curse_explode) + self._curse_timer = bs.Timer( + 5.0, bs.WeakCall(self.curse_explode) ) def equip_boxing_gloves(self) -> None: @@ -646,12 +646,12 @@ class Spaz(ba.Actor): """ if not self.node: - ba.print_error('Can\'t equip shields; no node.') + logging.exception('Can\'t equip shields; no node.') return factory = SpazFactory.get() if self.shield is None: - self.shield = ba.newnode( + self.shield = bs.newnode( 'shield', owner=self.node, attrs={'color': (0.3, 0.2, 2.0), 'radius': 1.3}, @@ -660,11 +660,11 @@ class Spaz(ba.Actor): self.shield_hitpoints = self.shield_hitpoints_max = 650 self.shield_decay_rate = factory.shield_decay_rate if decay else 0 self.shield.hurt = 0 - ba.playsound(factory.shield_up_sound, 1.0, position=self.node.position) + factory.shield_up_sound.play(1.0, position=self.node.position) if self.shield_decay_rate > 0: - self.shield_decay_timer = ba.Timer( - 0.5, ba.WeakCall(self.shield_decay), repeat=True + self.shield_decay_timer = bs.Timer( + 0.5, bs.WeakCall(self.shield_decay), repeat=True ) # So user can see the decay. self.shield.always_show_health_bar = True @@ -685,8 +685,7 @@ class Spaz(ba.Actor): self.shield = None self.shield_decay_timer = None assert self.node - ba.playsound( - SpazFactory.get().shield_down_sound, + SpazFactory.get().shield_down_sound.play( 1.0, position=self.node.position, ) @@ -699,7 +698,7 @@ class Spaz(ba.Actor): # pylint: disable=too-many-branches assert not self.expired - if isinstance(msg, ba.PickedUpMessage): + if isinstance(msg, bs.PickedUpMessage): if self.node: self.node.handlemessage('hurt_sound') self.node.handlemessage('picked_up') @@ -707,18 +706,18 @@ class Spaz(ba.Actor): # This counts as a hit. self._num_times_hit += 1 - elif isinstance(msg, ba.ShouldShatterMessage): + elif isinstance(msg, bs.ShouldShatterMessage): # Eww; seems we have to do this in a timer or it wont work right. # (since we're getting called from within update() perhaps?..) # NOTE: should test to see if that's still the case. - ba.timer(0.001, ba.WeakCall(self.shatter)) + bs.timer(0.001, bs.WeakCall(self.shatter)) - elif isinstance(msg, ba.ImpactDamageMessage): + elif isinstance(msg, bs.ImpactDamageMessage): # Eww; seems we have to do this in a timer or it wont work right. # (since we're getting called from within update() perhaps?..) - ba.timer(0.001, ba.WeakCall(self._hit_self, msg.intensity)) + bs.timer(0.001, bs.WeakCall(self._hit_self, msg.intensity)) - elif isinstance(msg, ba.PowerupMessage): + elif isinstance(msg, bs.PowerupMessage): if self._dead or not self.node: return True if self.pick_up_powerup_callback is not None: @@ -729,21 +728,19 @@ class Spaz(ba.Actor): self.set_bomb_count(3) if self.powerups_expire: self.node.mini_billboard_1_texture = tex - t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + t_ms = int(bs.time() * 1000.0) assert isinstance(t_ms, int) self.node.mini_billboard_1_start_time = t_ms self.node.mini_billboard_1_end_time = ( t_ms + POWERUP_WEAR_OFF_TIME ) - self._multi_bomb_wear_off_flash_timer = ba.Timer( - (POWERUP_WEAR_OFF_TIME - 2000), - ba.WeakCall(self._multi_bomb_wear_off_flash), - timeformat=ba.TimeFormat.MILLISECONDS, + self._multi_bomb_wear_off_flash_timer = bs.Timer( + (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, + bs.WeakCall(self._multi_bomb_wear_off_flash), ) - self._multi_bomb_wear_off_timer = ba.Timer( - POWERUP_WEAR_OFF_TIME, - ba.WeakCall(self._multi_bomb_wear_off), - timeformat=ba.TimeFormat.MILLISECONDS, + self._multi_bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME / 1000.0, + bs.WeakCall(self._multi_bomb_wear_off), ) elif msg.poweruptype == 'land_mines': self.set_land_mine_count(min(self.land_mine_count + 3, 3)) @@ -753,21 +750,19 @@ class Spaz(ba.Actor): self._flash_billboard(tex) if self.powerups_expire: self.node.mini_billboard_2_texture = tex - t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + t_ms = int(bs.time() * 1000.0) assert isinstance(t_ms, int) self.node.mini_billboard_2_start_time = t_ms self.node.mini_billboard_2_end_time = ( t_ms + POWERUP_WEAR_OFF_TIME ) - self._bomb_wear_off_flash_timer = ba.Timer( - POWERUP_WEAR_OFF_TIME - 2000, - ba.WeakCall(self._bomb_wear_off_flash), - timeformat=ba.TimeFormat.MILLISECONDS, + self._bomb_wear_off_flash_timer = bs.Timer( + (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, + bs.WeakCall(self._bomb_wear_off_flash), ) - self._bomb_wear_off_timer = ba.Timer( - POWERUP_WEAR_OFF_TIME, - ba.WeakCall(self._bomb_wear_off), - timeformat=ba.TimeFormat.MILLISECONDS, + self._bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME / 1000.0, + bs.WeakCall(self._bomb_wear_off), ) elif msg.poweruptype == 'sticky_bombs': self.bomb_type = 'sticky' @@ -775,21 +770,19 @@ class Spaz(ba.Actor): self._flash_billboard(tex) if self.powerups_expire: self.node.mini_billboard_2_texture = tex - t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + t_ms = int(bs.time() * 1000.0) assert isinstance(t_ms, int) self.node.mini_billboard_2_start_time = t_ms self.node.mini_billboard_2_end_time = ( t_ms + POWERUP_WEAR_OFF_TIME ) - self._bomb_wear_off_flash_timer = ba.Timer( - POWERUP_WEAR_OFF_TIME - 2000, - ba.WeakCall(self._bomb_wear_off_flash), - timeformat=ba.TimeFormat.MILLISECONDS, + self._bomb_wear_off_flash_timer = bs.Timer( + (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, + bs.WeakCall(self._bomb_wear_off_flash), ) - self._bomb_wear_off_timer = ba.Timer( - POWERUP_WEAR_OFF_TIME, - ba.WeakCall(self._bomb_wear_off), - timeformat=ba.TimeFormat.MILLISECONDS, + self._bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME / 1000.0, + bs.WeakCall(self._bomb_wear_off), ) elif msg.poweruptype == 'punch': tex = PowerupBoxFactory.get().tex_punch @@ -798,21 +791,19 @@ class Spaz(ba.Actor): if self.powerups_expire and not self.default_boxing_gloves: self.node.boxing_gloves_flashing = False self.node.mini_billboard_3_texture = tex - t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + t_ms = int(bs.time() * 1000.0) assert isinstance(t_ms, int) self.node.mini_billboard_3_start_time = t_ms self.node.mini_billboard_3_end_time = ( t_ms + POWERUP_WEAR_OFF_TIME ) - self._boxing_gloves_wear_off_flash_timer = ba.Timer( - POWERUP_WEAR_OFF_TIME - 2000, - ba.WeakCall(self._gloves_wear_off_flash), - timeformat=ba.TimeFormat.MILLISECONDS, + self._boxing_gloves_wear_off_flash_timer = bs.Timer( + (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, + bs.WeakCall(self._gloves_wear_off_flash), ) - self._boxing_gloves_wear_off_timer = ba.Timer( - POWERUP_WEAR_OFF_TIME, - ba.WeakCall(self._gloves_wear_off), - timeformat=ba.TimeFormat.MILLISECONDS, + self._boxing_gloves_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME / 1000.0, + bs.WeakCall(self._gloves_wear_off), ) elif msg.poweruptype == 'shield': factory = SpazFactory.get() @@ -827,21 +818,19 @@ class Spaz(ba.Actor): self._flash_billboard(tex) if self.powerups_expire: self.node.mini_billboard_2_texture = tex - t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + t_ms = int(bs.time() * 1000.0) assert isinstance(t_ms, int) self.node.mini_billboard_2_start_time = t_ms self.node.mini_billboard_2_end_time = ( t_ms + POWERUP_WEAR_OFF_TIME ) - self._bomb_wear_off_flash_timer = ba.Timer( - POWERUP_WEAR_OFF_TIME - 2000, - ba.WeakCall(self._bomb_wear_off_flash), - timeformat=ba.TimeFormat.MILLISECONDS, + self._bomb_wear_off_flash_timer = bs.Timer( + (POWERUP_WEAR_OFF_TIME - 2000) / 1000.0, + bs.WeakCall(self._bomb_wear_off_flash), ) - self._bomb_wear_off_timer = ba.Timer( - POWERUP_WEAR_OFF_TIME, - ba.WeakCall(self._bomb_wear_off), - timeformat=ba.TimeFormat.MILLISECONDS, + self._bomb_wear_off_timer = bs.Timer( + POWERUP_WEAR_OFF_TIME / 1000.0, + bs.WeakCall(self._bomb_wear_off), ) elif msg.poweruptype == 'health': if self._cursed: @@ -870,15 +859,14 @@ class Spaz(ba.Actor): self.node.handlemessage('flash') if msg.sourcenode: - msg.sourcenode.handlemessage(ba.PowerupAcceptMessage()) + msg.sourcenode.handlemessage(bs.PowerupAcceptMessage()) return True - elif isinstance(msg, ba.FreezeMessage): + elif isinstance(msg, bs.FreezeMessage): if not self.node: return None if self.node.invincible: - ba.playsound( - SpazFactory.get().block_sound, + SpazFactory.get().block_sound.play( 1.0, position=self.node.position, ) @@ -888,23 +876,22 @@ class Spaz(ba.Actor): if not self.frozen: self.frozen = True self.node.frozen = True - ba.timer(5.0, ba.WeakCall(self.handlemessage, ba.ThawMessage())) + bs.timer(5.0, bs.WeakCall(self.handlemessage, bs.ThawMessage())) # Instantly shatter if we're already dead. # (otherwise its hard to tell we're dead) if self.hitpoints <= 0: self.shatter() - elif isinstance(msg, ba.ThawMessage): + elif isinstance(msg, bs.ThawMessage): if self.frozen and not self.shattered and self.node: self.frozen = False self.node.frozen = False - elif isinstance(msg, ba.HitMessage): + elif isinstance(msg, bs.HitMessage): if not self.node: return None if self.node.invincible: - ba.playsound( - SpazFactory.get().block_sound, + SpazFactory.get().block_sound.play( 1.0, position=self.node.position, ) @@ -912,7 +899,7 @@ class Spaz(ba.Actor): # If we were recently hit, don't count this as another. # (so punch flurries and bomb pileups essentially count as 1 hit) - local_time = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + local_time = int(bs.time() * 1000.0) assert isinstance(local_time, int) if ( self._last_hit_time is None @@ -964,19 +951,17 @@ class Spaz(ba.Actor): # damage the player. This hopefully gives us a happy medium. max_spillover = SpazFactory.get().max_shield_spillover_damage if self.shield_hitpoints <= 0: - # FIXME: Transition out perhaps? self.shield.delete() self.shield = None - ba.playsound( - SpazFactory.get().shield_down_sound, + SpazFactory.get().shield_down_sound.play( 1.0, position=self.node.position, ) # Emit some cool looking sparks when the shield dies. npos = self.node.position - ba.emitfx( + bs.emitfx( position=(npos[0], npos[1] + 0.9, npos[2]), velocity=self.node.velocity, count=random.randrange(20, 30), @@ -986,15 +971,14 @@ class Spaz(ba.Actor): ) else: - ba.playsound( - SpazFactory.get().shield_hit_sound, + SpazFactory.get().shield_hit_sound.play( 0.5, position=self.node.position, ) # Emit some cool looking sparks on shield hit. assert msg.force_direction is not None - ba.emitfx( + bs.emitfx( position=msg.pos, velocity=( msg.force_direction[0] * 1.0, @@ -1055,7 +1039,7 @@ class Spaz(ba.Actor): # If damage was significant, lets show it. if damage >= 350: assert msg.force_direction is not None - ba.show_damage_count( + bs.show_damage_count( '-' + str(int(damage / 10)) + '%', msg.pos, msg.force_direction, @@ -1064,8 +1048,7 @@ class Spaz(ba.Actor): # Let's always add in a super-punch sound with boxing # gloves just to differentiate them. if msg.hit_subtype == 'super_punch': - ba.playsound( - SpazFactory.get().punch_sound_stronger, + SpazFactory.get().punch_sound_stronger.play( 1.0, position=self.node.position, ) @@ -1076,11 +1059,11 @@ class Spaz(ba.Actor): sound = SpazFactory.get().punch_sound else: sound = SpazFactory.get().punch_sound_weak - ba.playsound(sound, 1.0, position=self.node.position) + sound.play(1.0, position=self.node.position) # Throw up some chunks. assert msg.force_direction is not None - ba.emitfx( + bs.emitfx( position=msg.pos, velocity=( msg.force_direction[0] * 0.5, @@ -1092,7 +1075,7 @@ class Spaz(ba.Actor): spread=0.03, ) - ba.emitfx( + bs.emitfx( position=msg.pos, chunk_type='sweat', velocity=( @@ -1113,7 +1096,7 @@ class Spaz(ba.Actor): msg.pos[2] + msg.force_direction[2] * 0.02, ) flash_color = (1.0, 0.8, 0.4) - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': punchpos, @@ -1123,9 +1106,9 @@ class Spaz(ba.Actor): 'color': flash_color, }, ) - ba.timer(0.06, light.delete) + bs.timer(0.06, light.delete) - flash = ba.newnode( + flash = bs.newnode( 'flash', attrs={ 'position': punchpos, @@ -1133,11 +1116,11 @@ class Spaz(ba.Actor): 'color': flash_color, }, ) - ba.timer(0.06, flash.delete) + bs.timer(0.06, flash.delete) if msg.hit_type == 'impact': assert msg.force_direction is not None - ba.emitfx( + bs.emitfx( position=msg.pos, velocity=( msg.force_direction[0] * 2.0, @@ -1149,7 +1132,6 @@ class Spaz(ba.Actor): spread=0.1, ) if self.hitpoints > 0: - # It's kinda crappy to die from impacts, so lets reduce # impact damage by a reasonable amount *if* it'll keep us alive if msg.hit_type == 'impact' and damage > self.hitpoints: @@ -1170,10 +1152,10 @@ class Spaz(ba.Actor): # If we're cursed, *any* damage blows us up. if self._cursed and damage > 0: - ba.timer( + bs.timer( 0.05, - ba.WeakCall( - self.curse_explode, msg.get_source_player(ba.Player) + bs.WeakCall( + self.curse_explode, msg.get_source_player(bs.Player) ), ) @@ -1182,7 +1164,7 @@ class Spaz(ba.Actor): self.shatter() elif self.hitpoints <= 0: self.node.handlemessage( - ba.DieMessage(how=ba.DeathType.IMPACT) + bs.DieMessage(how=bs.DeathType.IMPACT) ) # If we're dead, take a look at the smoothed damage value @@ -1196,7 +1178,7 @@ class Spaz(ba.Actor): elif isinstance(msg, BombDiedMessage): self.bomb_count += 1 - elif isinstance(msg, ba.DieMessage): + elif isinstance(msg, bs.DieMessage): wasdead = self._dead self._dead = True self.hitpoints = 0 @@ -1206,15 +1188,15 @@ class Spaz(ba.Actor): elif self.node: self.node.hurt = 1.0 if self.play_big_death_sound and not wasdead: - ba.playsound(SpazFactory.get().single_player_death_sound) + SpazFactory.get().single_player_death_sound.play() self.node.dead = True - ba.timer(2.0, self.node.delete) + bs.timer(2.0, self.node.delete) - elif isinstance(msg, ba.OutOfBoundsMessage): + elif isinstance(msg, bs.OutOfBoundsMessage): # By default we just die here. - self.handlemessage(ba.DieMessage(how=ba.DeathType.FALL)) + self.handlemessage(bs.DieMessage(how=bs.DeathType.FALL)) - elif isinstance(msg, ba.StandMessage): + elif isinstance(msg, bs.StandMessage): self._last_stand_pos = ( msg.position[0], msg.position[1], @@ -1235,11 +1217,10 @@ class Spaz(ba.Actor): elif isinstance(msg, PunchHitMessage): if not self.node: return None - node = ba.getcollision().opposingnode + node = bs.getcollision().opposingnode # Only allow one hit per node per punch. if node and (node not in self._punched_nodes): - punch_momentum_angular = ( self.node.punch_momentum_angular * self._punch_power_scale ) @@ -1258,7 +1239,7 @@ class Spaz(ba.Actor): if node.getnodetype() != 'spaz': sounds = SpazFactory.get().impact_sounds_medium sound = sounds[random.randrange(len(sounds))] - ba.playsound(sound, 1.0, position=self.node.position) + sound.play(1.0, position=self.node.position) ppos = self.node.punch_position punchdir = self.node.punch_velocity @@ -1266,7 +1247,7 @@ class Spaz(ba.Actor): self._punched_nodes.add(node) node.handlemessage( - ba.HitMessage( + bs.HitMessage( pos=ppos, velocity=vel, magnitude=punch_power * punch_momentum_angular * 110.0, @@ -1308,10 +1289,10 @@ class Spaz(ba.Actor): return None try: - collision = ba.getcollision() + collision = bs.getcollision() opposingnode = collision.opposingnode opposingbody = collision.opposingbody - except ba.NotFoundError: + except bs.NotFoundError: return True # Don't allow picking up of invincible dudes. @@ -1339,7 +1320,7 @@ class Spaz(ba.Actor): # Note: hold_body needs to be set before hold_node. self.node.hold_body = opposingbody self.node.hold_node = opposingnode - elif isinstance(msg, ba.CelebrateMessage): + elif isinstance(msg, bs.CelebrateMessage): if self.node: self.node.handlemessage('celebrate', int(msg.duration * 1000)) @@ -1347,7 +1328,7 @@ class Spaz(ba.Actor): return super().handlemessage(msg) return None - def drop_bomb(self) -> stdbomb.Bomb | None: + def drop_bomb(self) -> Bomb | None: """ Tell the spaz to drop one of his bombs, and returns the resulting bomb object. @@ -1369,7 +1350,7 @@ class Spaz(ba.Actor): dropping_bomb = True bomb_type = self.bomb_type - bomb = stdbomb.Bomb( + bomb = Bomb( position=(pos[0], pos[1] - 0.0, pos[2]), velocity=(vel[0], vel[1], vel[2]), bomb_type=bomb_type, @@ -1382,7 +1363,7 @@ class Spaz(ba.Actor): if dropping_bomb: self.bomb_count -= 1 bomb.node.add_death_action( - ba.WeakCall(self.handlemessage, BombDiedMessage()) + bs.WeakCall(self.handlemessage, BombDiedMessage()) ) self._pick_up(bomb.node) @@ -1391,7 +1372,7 @@ class Spaz(ba.Actor): return bomb - def _pick_up(self, node: ba.Node) -> None: + def _pick_up(self, node: bs.Node) -> None: if self.node: # Note: hold_body needs to be set before hold_node. self.node.hold_body = 0 @@ -1409,14 +1390,14 @@ class Spaz(ba.Actor): else: self.node.counter_text = '' - def curse_explode(self, source_player: ba.Player | None = None) -> None: + def curse_explode(self, source_player: bs.Player | None = None) -> None: """Explode the poor spaz spectacularly.""" if self._cursed and self.node: self.shatter(extreme=True) - self.handlemessage(ba.DieMessage()) + self.handlemessage(bs.DieMessage()) activity = self._activity() if activity: - stdbomb.Blast( + Blast( position=self.node.position, velocity=self.node.velocity, blast_radius=3.0, @@ -1435,7 +1416,7 @@ class Spaz(ba.Actor): assert self.node if self.frozen: # Momentary flash of light. - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': self.node.position, @@ -1445,13 +1426,13 @@ class Spaz(ba.Actor): }, ) - ba.animate( + bs.animate( light, 'intensity', {0.0: 3.0, 0.04: 0.5, 0.08: 0.07, 0.3: 0} ) - ba.timer(0.3, light.delete) + bs.timer(0.3, light.delete) # Emit ice chunks. - ba.emitfx( + bs.emitfx( position=self.node.position, velocity=self.node.velocity, count=int(random.random() * 10.0 + 10.0), @@ -1459,7 +1440,7 @@ class Spaz(ba.Actor): spread=0.2, chunk_type='ice', ) - ba.emitfx( + bs.emitfx( position=self.node.position, velocity=self.node.velocity, count=int(random.random() * 10.0 + 10.0), @@ -1467,18 +1448,16 @@ class Spaz(ba.Actor): spread=0.2, chunk_type='ice', ) - ba.playsound( - SpazFactory.get().shatter_sound, + SpazFactory.get().shatter_sound.play( 1.0, position=self.node.position, ) else: - ba.playsound( - SpazFactory.get().splatter_sound, + SpazFactory.get().splatter_sound.play( 1.0, position=self.node.position, ) - self.handlemessage(ba.DieMessage()) + self.handlemessage(bs.DieMessage()) self.node.shattered = 2 if extreme else 1 def _hit_self(self, intensity: float) -> None: @@ -1486,7 +1465,7 @@ class Spaz(ba.Actor): return pos = self.node.position self.handlemessage( - ba.HitMessage( + bs.HitMessage( flat_damage=50.0 * intensity, pos=pos, force_direction=self.node.velocity, @@ -1494,7 +1473,7 @@ class Spaz(ba.Actor): ) ) self.node.handlemessage('knockout', max(0.0, 50.0 * intensity)) - sounds: Sequence[ba.Sound] + sounds: Sequence[bs.Sound] if intensity >= 5.0: sounds = SpazFactory.get().impact_sounds_harder elif intensity >= 3.0: @@ -1502,9 +1481,9 @@ class Spaz(ba.Actor): else: sounds = SpazFactory.get().impact_sounds_medium sound = sounds[random.randrange(len(sounds))] - ba.playsound(sound, position=pos, volume=5.0) + sound.play(position=pos, volume=5.0) - def _get_bomb_type_tex(self) -> ba.Texture: + def _get_bomb_type_tex(self) -> bs.Texture: factory = PowerupBoxFactory.get() if self.bomb_type == 'sticky': return factory.tex_sticky_bombs @@ -1514,11 +1493,11 @@ class Spaz(ba.Actor): return factory.tex_impact_bombs raise ValueError('invalid bomb type') - def _flash_billboard(self, tex: ba.Texture) -> None: + def _flash_billboard(self, tex: bs.Texture) -> None: assert self.node self.node.billboard_texture = tex self.node.billboard_cross_out = False - ba.animate( + bs.animate( self.node, 'billboard_opacity', {0.0: 0.0, 0.1: 1.0, 0.4: 1.0, 0.5: 0.0}, @@ -1549,8 +1528,7 @@ class Spaz(ba.Actor): self._punch_cooldown = factory.punch_cooldown self._has_boxing_gloves = False if self.node: - ba.playsound( - PowerupBoxFactory.get().powerdown_sound, + PowerupBoxFactory.get().powerdown_sound.play( position=self.node.position, ) self.node.boxing_gloves = False @@ -1565,8 +1543,7 @@ class Spaz(ba.Actor): def _multi_bomb_wear_off(self) -> None: self.set_bomb_count(self.default_bomb_count) if self.node: - ba.playsound( - PowerupBoxFactory.get().powerdown_sound, + PowerupBoxFactory.get().powerdown_sound.play( position=self.node.position, ) self.node.billboard_opacity = 0.0 @@ -1580,8 +1557,7 @@ class Spaz(ba.Actor): def _bomb_wear_off(self) -> None: self.bomb_type = self.bomb_type_default if self.node: - ba.playsound( - PowerupBoxFactory.get().powerdown_sound, + PowerupBoxFactory.get().powerdown_sound.play( position=self.node.position, ) self.node.billboard_opacity = 0.0 diff --git a/assets/src/ba_data/python/bastd/actor/spazappearance.py b/src/assets/ba_data/python/bastd/actor/spazappearance.py similarity index 70% rename from assets/src/ba_data/python/bastd/actor/spazappearance.py rename to src/assets/ba_data/python/bastd/actor/spazappearance.py index cdc00d7e..5af0df9c 100644 --- a/assets/src/ba_data/python/bastd/actor/spazappearance.py +++ b/src/assets/ba_data/python/bastd/actor/spazappearance.py @@ -3,20 +3,18 @@ """Appearance functionality for spazzes.""" from __future__ import annotations -from typing import TYPE_CHECKING - -import ba -import ba.internal - -if TYPE_CHECKING: - pass +import bascenev1 as bs def get_appearances(include_locked: bool = False) -> list[str]: """Get the list of available spaz appearances.""" # pylint: disable=too-many-statements # pylint: disable=too-many-branches - get_purchased = ba.internal.get_purchased + plus = bs.app.plus + assert plus is not None + + assert bs.app.classic is not None + get_purchased = plus.get_purchased disallowed = [] if not include_locked: # hmm yeah this'll be tough to hack... @@ -79,7 +77,9 @@ def get_appearances(include_locked: bool = False) -> list[str]: if not get_purchased('characters.snakeshadow'): disallowed.append('Snake Shadow') return [ - s for s in list(ba.app.spaz_appearances.keys()) if s not in disallowed + s + for s in list(bs.app.classic.spaz_appearances.keys()) + if s not in disallowed ] @@ -87,25 +87,26 @@ class Appearance: """Create and fill out one of these suckers to define a spaz appearance""" def __init__(self, name: str): + assert bs.app.classic is not None self.name = name - if self.name in ba.app.spaz_appearances: - raise Exception( - 'spaz appearance name "' + self.name + '" already exists.' + if self.name in bs.app.classic.spaz_appearances: + raise RuntimeError( + f'spaz appearance name "{self.name}" already exists.' ) - ba.app.spaz_appearances[self.name] = self + bs.app.classic.spaz_appearances[self.name] = self self.color_texture = '' self.color_mask_texture = '' self.icon_texture = '' self.icon_mask_texture = '' - self.head_model = '' - self.torso_model = '' - self.pelvis_model = '' - self.upper_arm_model = '' - self.forearm_model = '' - self.hand_model = '' - self.upper_leg_model = '' - self.lower_leg_model = '' - self.toes_model = '' + self.head_mesh = '' + self.torso_mesh = '' + self.pelvis_mesh = '' + self.upper_arm_mesh = '' + self.forearm_mesh = '' + self.hand_mesh = '' + self.upper_leg_mesh = '' + self.lower_leg_mesh = '' + self.toes_mesh = '' self.jump_sounds: list[str] = [] self.attack_sounds: list[str] = [] self.impact_sounds: list[str] = [] @@ -131,15 +132,15 @@ def register_appearances() -> None: t.color_mask_texture = 'neoSpazColorMask' t.icon_texture = 'neoSpazIcon' t.icon_mask_texture = 'neoSpazIconColorMask' - t.head_model = 'neoSpazHead' - t.torso_model = 'neoSpazTorso' - t.pelvis_model = 'neoSpazPelvis' - t.upper_arm_model = 'neoSpazUpperArm' - t.forearm_model = 'neoSpazForeArm' - t.hand_model = 'neoSpazHand' - t.upper_leg_model = 'neoSpazUpperLeg' - t.lower_leg_model = 'neoSpazLowerLeg' - t.toes_model = 'neoSpazToes' + t.head_mesh = 'neoSpazHead' + t.torso_mesh = 'neoSpazTorso' + t.pelvis_mesh = 'neoSpazPelvis' + t.upper_arm_mesh = 'neoSpazUpperArm' + t.forearm_mesh = 'neoSpazForeArm' + t.hand_mesh = 'neoSpazHand' + t.upper_leg_mesh = 'neoSpazUpperLeg' + t.lower_leg_mesh = 'neoSpazLowerLeg' + t.toes_mesh = 'neoSpazToes' t.jump_sounds = ['spazJump01', 'spazJump02', 'spazJump03', 'spazJump04'] t.attack_sounds = [ 'spazAttack01', @@ -166,15 +167,15 @@ def register_appearances() -> None: t.default_highlight = (0, 1, 0) t.icon_texture = 'zoeIcon' t.icon_mask_texture = 'zoeIconColorMask' - t.head_model = 'zoeHead' - t.torso_model = 'zoeTorso' - t.pelvis_model = 'zoePelvis' - t.upper_arm_model = 'zoeUpperArm' - t.forearm_model = 'zoeForeArm' - t.hand_model = 'zoeHand' - t.upper_leg_model = 'zoeUpperLeg' - t.lower_leg_model = 'zoeLowerLeg' - t.toes_model = 'zoeToes' + t.head_mesh = 'zoeHead' + t.torso_mesh = 'zoeTorso' + t.pelvis_mesh = 'zoePelvis' + t.upper_arm_mesh = 'zoeUpperArm' + t.forearm_mesh = 'zoeForeArm' + t.hand_mesh = 'zoeHand' + t.upper_leg_mesh = 'zoeUpperLeg' + t.lower_leg_mesh = 'zoeLowerLeg' + t.toes_mesh = 'zoeToes' t.jump_sounds = ['zoeJump01', 'zoeJump02', 'zoeJump03'] t.attack_sounds = [ 'zoeAttack01', @@ -201,15 +202,15 @@ def register_appearances() -> None: t.default_highlight = (0.55, 0.8, 0.55) t.icon_texture = 'ninjaIcon' t.icon_mask_texture = 'ninjaIconColorMask' - t.head_model = 'ninjaHead' - t.torso_model = 'ninjaTorso' - t.pelvis_model = 'ninjaPelvis' - t.upper_arm_model = 'ninjaUpperArm' - t.forearm_model = 'ninjaForeArm' - t.hand_model = 'ninjaHand' - t.upper_leg_model = 'ninjaUpperLeg' - t.lower_leg_model = 'ninjaLowerLeg' - t.toes_model = 'ninjaToes' + t.head_mesh = 'ninjaHead' + t.torso_mesh = 'ninjaTorso' + t.pelvis_mesh = 'ninjaPelvis' + t.upper_arm_mesh = 'ninjaUpperArm' + t.forearm_mesh = 'ninjaForeArm' + t.hand_mesh = 'ninjaHand' + t.upper_leg_mesh = 'ninjaUpperLeg' + t.lower_leg_mesh = 'ninjaLowerLeg' + t.toes_mesh = 'ninjaToes' ninja_attacks = ['ninjaAttack' + str(i + 1) + '' for i in range(7)] ninja_hits = ['ninjaHit' + str(i + 1) + '' for i in range(8)] ninja_jumps = ['ninjaAttack' + str(i + 1) + '' for i in range(7)] @@ -229,15 +230,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0.5, 0.3) t.icon_texture = 'kronkIcon' t.icon_mask_texture = 'kronkIconColorMask' - t.head_model = 'kronkHead' - t.torso_model = 'kronkTorso' - t.pelvis_model = 'kronkPelvis' - t.upper_arm_model = 'kronkUpperArm' - t.forearm_model = 'kronkForeArm' - t.hand_model = 'kronkHand' - t.upper_leg_model = 'kronkUpperLeg' - t.lower_leg_model = 'kronkLowerLeg' - t.toes_model = 'kronkToes' + t.head_mesh = 'kronkHead' + t.torso_mesh = 'kronkTorso' + t.pelvis_mesh = 'kronkPelvis' + t.upper_arm_mesh = 'kronkUpperArm' + t.forearm_mesh = 'kronkForeArm' + t.hand_mesh = 'kronkHand' + t.upper_leg_mesh = 'kronkUpperLeg' + t.lower_leg_mesh = 'kronkLowerLeg' + t.toes_mesh = 'kronkToes' kronk_sounds = [ 'kronk1', 'kronk2', @@ -266,15 +267,15 @@ def register_appearances() -> None: t.default_highlight = (0.1, 0.6, 0.1) t.icon_texture = 'melIcon' t.icon_mask_texture = 'melIconColorMask' - t.head_model = 'melHead' - t.torso_model = 'melTorso' - t.pelvis_model = 'kronkPelvis' - t.upper_arm_model = 'melUpperArm' - t.forearm_model = 'melForeArm' - t.hand_model = 'melHand' - t.upper_leg_model = 'melUpperLeg' - t.lower_leg_model = 'melLowerLeg' - t.toes_model = 'melToes' + t.head_mesh = 'melHead' + t.torso_mesh = 'melTorso' + t.pelvis_mesh = 'kronkPelvis' + t.upper_arm_mesh = 'melUpperArm' + t.forearm_mesh = 'melForeArm' + t.hand_mesh = 'melHand' + t.upper_leg_mesh = 'melUpperLeg' + t.lower_leg_mesh = 'melLowerLeg' + t.toes_mesh = 'melToes' mel_sounds = [ 'mel01', 'mel02', @@ -303,15 +304,15 @@ def register_appearances() -> None: t.default_highlight = (1, 1, 0) t.icon_texture = 'jackIcon' t.icon_mask_texture = 'jackIconColorMask' - t.head_model = 'jackHead' - t.torso_model = 'jackTorso' - t.pelvis_model = 'kronkPelvis' - t.upper_arm_model = 'jackUpperArm' - t.forearm_model = 'jackForeArm' - t.hand_model = 'jackHand' - t.upper_leg_model = 'jackUpperLeg' - t.lower_leg_model = 'jackLowerLeg' - t.toes_model = 'jackToes' + t.head_mesh = 'jackHead' + t.torso_mesh = 'jackTorso' + t.pelvis_mesh = 'kronkPelvis' + t.upper_arm_mesh = 'jackUpperArm' + t.forearm_mesh = 'jackForeArm' + t.hand_mesh = 'jackHand' + t.upper_leg_mesh = 'jackUpperLeg' + t.lower_leg_mesh = 'jackLowerLeg' + t.toes_mesh = 'jackToes' hit_sounds = [ 'jackHit01', 'jackHit02', @@ -338,15 +339,15 @@ def register_appearances() -> None: t.default_highlight = (1, 1, 1) t.icon_texture = 'santaIcon' t.icon_mask_texture = 'santaIconColorMask' - t.head_model = 'santaHead' - t.torso_model = 'santaTorso' - t.pelvis_model = 'kronkPelvis' - t.upper_arm_model = 'santaUpperArm' - t.forearm_model = 'santaForeArm' - t.hand_model = 'santaHand' - t.upper_leg_model = 'santaUpperLeg' - t.lower_leg_model = 'santaLowerLeg' - t.toes_model = 'santaToes' + t.head_mesh = 'santaHead' + t.torso_mesh = 'santaTorso' + t.pelvis_mesh = 'kronkPelvis' + t.upper_arm_mesh = 'santaUpperArm' + t.forearm_mesh = 'santaForeArm' + t.hand_mesh = 'santaHand' + t.upper_leg_mesh = 'santaUpperLeg' + t.lower_leg_mesh = 'santaLowerLeg' + t.toes_mesh = 'santaToes' hit_sounds = ['santaHit01', 'santaHit02', 'santaHit03', 'santaHit04'] sounds = ['santa01', 'santa02', 'santa03', 'santa04', 'santa05'] t.attack_sounds = sounds @@ -365,15 +366,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0.5, 0) t.icon_texture = 'frostyIcon' t.icon_mask_texture = 'frostyIconColorMask' - t.head_model = 'frostyHead' - t.torso_model = 'frostyTorso' - t.pelvis_model = 'frostyPelvis' - t.upper_arm_model = 'frostyUpperArm' - t.forearm_model = 'frostyForeArm' - t.hand_model = 'frostyHand' - t.upper_leg_model = 'frostyUpperLeg' - t.lower_leg_model = 'frostyLowerLeg' - t.toes_model = 'frostyToes' + t.head_mesh = 'frostyHead' + t.torso_mesh = 'frostyTorso' + t.pelvis_mesh = 'frostyPelvis' + t.upper_arm_mesh = 'frostyUpperArm' + t.forearm_mesh = 'frostyForeArm' + t.hand_mesh = 'frostyHand' + t.upper_leg_mesh = 'frostyUpperLeg' + t.lower_leg_mesh = 'frostyLowerLeg' + t.toes_mesh = 'frostyToes' frosty_sounds = ['frosty01', 'frosty02', 'frosty03', 'frosty04', 'frosty05'] frosty_hit_sounds = ['frostyHit01', 'frostyHit02', 'frostyHit03'] t.attack_sounds = frosty_sounds @@ -392,15 +393,15 @@ def register_appearances() -> None: t.default_highlight = (0.6, 0.9, 1) t.icon_texture = 'bonesIcon' t.icon_mask_texture = 'bonesIconColorMask' - t.head_model = 'bonesHead' - t.torso_model = 'bonesTorso' - t.pelvis_model = 'bonesPelvis' - t.upper_arm_model = 'bonesUpperArm' - t.forearm_model = 'bonesForeArm' - t.hand_model = 'bonesHand' - t.upper_leg_model = 'bonesUpperLeg' - t.lower_leg_model = 'bonesLowerLeg' - t.toes_model = 'bonesToes' + t.head_mesh = 'bonesHead' + t.torso_mesh = 'bonesTorso' + t.pelvis_mesh = 'bonesPelvis' + t.upper_arm_mesh = 'bonesUpperArm' + t.forearm_mesh = 'bonesForeArm' + t.hand_mesh = 'bonesHand' + t.upper_leg_mesh = 'bonesUpperLeg' + t.lower_leg_mesh = 'bonesLowerLeg' + t.toes_mesh = 'bonesToes' bones_sounds = ['bones1', 'bones2', 'bones3'] bones_hit_sounds = ['bones1', 'bones2', 'bones3'] t.attack_sounds = bones_sounds @@ -418,15 +419,15 @@ def register_appearances() -> None: t.default_color = (0.7, 0.5, 0.0) t.icon_texture = 'bearIcon' t.icon_mask_texture = 'bearIconColorMask' - t.head_model = 'bearHead' - t.torso_model = 'bearTorso' - t.pelvis_model = 'bearPelvis' - t.upper_arm_model = 'bearUpperArm' - t.forearm_model = 'bearForeArm' - t.hand_model = 'bearHand' - t.upper_leg_model = 'bearUpperLeg' - t.lower_leg_model = 'bearLowerLeg' - t.toes_model = 'bearToes' + t.head_mesh = 'bearHead' + t.torso_mesh = 'bearTorso' + t.pelvis_mesh = 'bearPelvis' + t.upper_arm_mesh = 'bearUpperArm' + t.forearm_mesh = 'bearForeArm' + t.hand_mesh = 'bearHand' + t.upper_leg_mesh = 'bearUpperLeg' + t.lower_leg_mesh = 'bearLowerLeg' + t.toes_mesh = 'bearToes' bear_sounds = ['bear1', 'bear2', 'bear3', 'bear4'] bear_hit_sounds = ['bearHit1', 'bearHit2'] t.attack_sounds = bear_sounds @@ -445,15 +446,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0, 0) t.icon_texture = 'penguinIcon' t.icon_mask_texture = 'penguinIconColorMask' - t.head_model = 'penguinHead' - t.torso_model = 'penguinTorso' - t.pelvis_model = 'penguinPelvis' - t.upper_arm_model = 'penguinUpperArm' - t.forearm_model = 'penguinForeArm' - t.hand_model = 'penguinHand' - t.upper_leg_model = 'penguinUpperLeg' - t.lower_leg_model = 'penguinLowerLeg' - t.toes_model = 'penguinToes' + t.head_mesh = 'penguinHead' + t.torso_mesh = 'penguinTorso' + t.pelvis_mesh = 'penguinPelvis' + t.upper_arm_mesh = 'penguinUpperArm' + t.forearm_mesh = 'penguinForeArm' + t.hand_mesh = 'penguinHand' + t.upper_leg_mesh = 'penguinUpperLeg' + t.lower_leg_mesh = 'penguinLowerLeg' + t.toes_mesh = 'penguinToes' penguin_sounds = ['penguin1', 'penguin2', 'penguin3', 'penguin4'] penguin_hit_sounds = ['penguinHit1', 'penguinHit2'] t.attack_sounds = penguin_sounds @@ -472,15 +473,15 @@ def register_appearances() -> None: t.default_highlight = (1, 1, 1) t.icon_texture = 'aliIcon' t.icon_mask_texture = 'aliIconColorMask' - t.head_model = 'aliHead' - t.torso_model = 'aliTorso' - t.pelvis_model = 'aliPelvis' - t.upper_arm_model = 'aliUpperArm' - t.forearm_model = 'aliForeArm' - t.hand_model = 'aliHand' - t.upper_leg_model = 'aliUpperLeg' - t.lower_leg_model = 'aliLowerLeg' - t.toes_model = 'aliToes' + t.head_mesh = 'aliHead' + t.torso_mesh = 'aliTorso' + t.pelvis_mesh = 'aliPelvis' + t.upper_arm_mesh = 'aliUpperArm' + t.forearm_mesh = 'aliForeArm' + t.hand_mesh = 'aliHand' + t.upper_leg_mesh = 'aliUpperLeg' + t.lower_leg_mesh = 'aliLowerLeg' + t.toes_mesh = 'aliToes' ali_sounds = ['ali1', 'ali2', 'ali3', 'ali4'] ali_hit_sounds = ['aliHit1', 'aliHit2'] t.attack_sounds = ali_sounds @@ -499,15 +500,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0, 0) t.icon_texture = 'cyborgIcon' t.icon_mask_texture = 'cyborgIconColorMask' - t.head_model = 'cyborgHead' - t.torso_model = 'cyborgTorso' - t.pelvis_model = 'cyborgPelvis' - t.upper_arm_model = 'cyborgUpperArm' - t.forearm_model = 'cyborgForeArm' - t.hand_model = 'cyborgHand' - t.upper_leg_model = 'cyborgUpperLeg' - t.lower_leg_model = 'cyborgLowerLeg' - t.toes_model = 'cyborgToes' + t.head_mesh = 'cyborgHead' + t.torso_mesh = 'cyborgTorso' + t.pelvis_mesh = 'cyborgPelvis' + t.upper_arm_mesh = 'cyborgUpperArm' + t.forearm_mesh = 'cyborgForeArm' + t.hand_mesh = 'cyborgHand' + t.upper_leg_mesh = 'cyborgUpperLeg' + t.lower_leg_mesh = 'cyborgLowerLeg' + t.toes_mesh = 'cyborgToes' cyborg_sounds = ['cyborg1', 'cyborg2', 'cyborg3', 'cyborg4'] cyborg_hit_sounds = ['cyborgHit1', 'cyborgHit2'] t.attack_sounds = cyborg_sounds @@ -526,15 +527,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0.5, 0.3) t.icon_texture = 'agentIcon' t.icon_mask_texture = 'agentIconColorMask' - t.head_model = 'agentHead' - t.torso_model = 'agentTorso' - t.pelvis_model = 'agentPelvis' - t.upper_arm_model = 'agentUpperArm' - t.forearm_model = 'agentForeArm' - t.hand_model = 'agentHand' - t.upper_leg_model = 'agentUpperLeg' - t.lower_leg_model = 'agentLowerLeg' - t.toes_model = 'agentToes' + t.head_mesh = 'agentHead' + t.torso_mesh = 'agentTorso' + t.pelvis_mesh = 'agentPelvis' + t.upper_arm_mesh = 'agentUpperArm' + t.forearm_mesh = 'agentForeArm' + t.hand_mesh = 'agentHand' + t.upper_leg_mesh = 'agentUpperLeg' + t.lower_leg_mesh = 'agentLowerLeg' + t.toes_mesh = 'agentToes' agent_sounds = ['agent1', 'agent2', 'agent3', 'agent4'] agent_hit_sounds = ['agentHit1', 'agentHit2'] t.attack_sounds = agent_sounds @@ -553,15 +554,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0, 0) t.icon_texture = 'jumpsuitIcon' t.icon_mask_texture = 'jumpsuitIconColorMask' - t.head_model = 'jumpsuitHead' - t.torso_model = 'jumpsuitTorso' - t.pelvis_model = 'jumpsuitPelvis' - t.upper_arm_model = 'jumpsuitUpperArm' - t.forearm_model = 'jumpsuitForeArm' - t.hand_model = 'jumpsuitHand' - t.upper_leg_model = 'jumpsuitUpperLeg' - t.lower_leg_model = 'jumpsuitLowerLeg' - t.toes_model = 'jumpsuitToes' + t.head_mesh = 'jumpsuitHead' + t.torso_mesh = 'jumpsuitTorso' + t.pelvis_mesh = 'jumpsuitPelvis' + t.upper_arm_mesh = 'jumpsuitUpperArm' + t.forearm_mesh = 'jumpsuitForeArm' + t.hand_mesh = 'jumpsuitHand' + t.upper_leg_mesh = 'jumpsuitUpperLeg' + t.lower_leg_mesh = 'jumpsuitLowerLeg' + t.toes_mesh = 'jumpsuitToes' jumpsuit_sounds = ['jumpsuit1', 'jumpsuit2', 'jumpsuit3', 'jumpsuit4'] jumpsuit_hit_sounds = ['jumpsuitHit1', 'jumpsuitHit2'] t.attack_sounds = jumpsuit_sounds @@ -580,15 +581,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0, 0) t.icon_texture = 'actionHeroIcon' t.icon_mask_texture = 'actionHeroIconColorMask' - t.head_model = 'actionHeroHead' - t.torso_model = 'actionHeroTorso' - t.pelvis_model = 'actionHeroPelvis' - t.upper_arm_model = 'actionHeroUpperArm' - t.forearm_model = 'actionHeroForeArm' - t.hand_model = 'actionHeroHand' - t.upper_leg_model = 'actionHeroUpperLeg' - t.lower_leg_model = 'actionHeroLowerLeg' - t.toes_model = 'actionHeroToes' + t.head_mesh = 'actionHeroHead' + t.torso_mesh = 'actionHeroTorso' + t.pelvis_mesh = 'actionHeroPelvis' + t.upper_arm_mesh = 'actionHeroUpperArm' + t.forearm_mesh = 'actionHeroForeArm' + t.hand_mesh = 'actionHeroHand' + t.upper_leg_mesh = 'actionHeroUpperLeg' + t.lower_leg_mesh = 'actionHeroLowerLeg' + t.toes_mesh = 'actionHeroToes' action_hero_sounds = [ 'actionHero1', 'actionHero2', @@ -612,15 +613,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0, 0) t.icon_texture = 'assassinIcon' t.icon_mask_texture = 'assassinIconColorMask' - t.head_model = 'assassinHead' - t.torso_model = 'assassinTorso' - t.pelvis_model = 'assassinPelvis' - t.upper_arm_model = 'assassinUpperArm' - t.forearm_model = 'assassinForeArm' - t.hand_model = 'assassinHand' - t.upper_leg_model = 'assassinUpperLeg' - t.lower_leg_model = 'assassinLowerLeg' - t.toes_model = 'assassinToes' + t.head_mesh = 'assassinHead' + t.torso_mesh = 'assassinTorso' + t.pelvis_mesh = 'assassinPelvis' + t.upper_arm_mesh = 'assassinUpperArm' + t.forearm_mesh = 'assassinForeArm' + t.hand_mesh = 'assassinHand' + t.upper_leg_mesh = 'assassinUpperLeg' + t.lower_leg_mesh = 'assassinLowerLeg' + t.toes_mesh = 'assassinToes' assassin_sounds = ['assassin1', 'assassin2', 'assassin3', 'assassin4'] assassin_hit_sounds = ['assassinHit1', 'assassinHit2'] t.attack_sounds = assassin_sounds @@ -639,15 +640,15 @@ def register_appearances() -> None: t.default_highlight = (0.06, 0.15, 0.4) t.icon_texture = 'wizardIcon' t.icon_mask_texture = 'wizardIconColorMask' - t.head_model = 'wizardHead' - t.torso_model = 'wizardTorso' - t.pelvis_model = 'wizardPelvis' - t.upper_arm_model = 'wizardUpperArm' - t.forearm_model = 'wizardForeArm' - t.hand_model = 'wizardHand' - t.upper_leg_model = 'wizardUpperLeg' - t.lower_leg_model = 'wizardLowerLeg' - t.toes_model = 'wizardToes' + t.head_mesh = 'wizardHead' + t.torso_mesh = 'wizardTorso' + t.pelvis_mesh = 'wizardPelvis' + t.upper_arm_mesh = 'wizardUpperArm' + t.forearm_mesh = 'wizardForeArm' + t.hand_mesh = 'wizardHand' + t.upper_leg_mesh = 'wizardUpperLeg' + t.lower_leg_mesh = 'wizardLowerLeg' + t.toes_mesh = 'wizardToes' wizard_sounds = ['wizard1', 'wizard2', 'wizard3', 'wizard4'] wizard_hit_sounds = ['wizardHit1', 'wizardHit2'] t.attack_sounds = wizard_sounds @@ -666,15 +667,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0, 0) t.icon_texture = 'cowboyIcon' t.icon_mask_texture = 'cowboyIconColorMask' - t.head_model = 'cowboyHead' - t.torso_model = 'cowboyTorso' - t.pelvis_model = 'cowboyPelvis' - t.upper_arm_model = 'cowboyUpperArm' - t.forearm_model = 'cowboyForeArm' - t.hand_model = 'cowboyHand' - t.upper_leg_model = 'cowboyUpperLeg' - t.lower_leg_model = 'cowboyLowerLeg' - t.toes_model = 'cowboyToes' + t.head_mesh = 'cowboyHead' + t.torso_mesh = 'cowboyTorso' + t.pelvis_mesh = 'cowboyPelvis' + t.upper_arm_mesh = 'cowboyUpperArm' + t.forearm_mesh = 'cowboyForeArm' + t.hand_mesh = 'cowboyHand' + t.upper_leg_mesh = 'cowboyUpperLeg' + t.lower_leg_mesh = 'cowboyLowerLeg' + t.toes_mesh = 'cowboyToes' cowboy_sounds = ['cowboy1', 'cowboy2', 'cowboy3', 'cowboy4'] cowboy_hit_sounds = ['cowboyHit1', 'cowboyHit2'] t.attack_sounds = cowboy_sounds @@ -693,15 +694,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0, 0) t.icon_texture = 'witchIcon' t.icon_mask_texture = 'witchIconColorMask' - t.head_model = 'witchHead' - t.torso_model = 'witchTorso' - t.pelvis_model = 'witchPelvis' - t.upper_arm_model = 'witchUpperArm' - t.forearm_model = 'witchForeArm' - t.hand_model = 'witchHand' - t.upper_leg_model = 'witchUpperLeg' - t.lower_leg_model = 'witchLowerLeg' - t.toes_model = 'witchToes' + t.head_mesh = 'witchHead' + t.torso_mesh = 'witchTorso' + t.pelvis_mesh = 'witchPelvis' + t.upper_arm_mesh = 'witchUpperArm' + t.forearm_mesh = 'witchForeArm' + t.hand_mesh = 'witchHand' + t.upper_leg_mesh = 'witchUpperLeg' + t.lower_leg_mesh = 'witchLowerLeg' + t.toes_mesh = 'witchToes' witch_sounds = ['witch1', 'witch2', 'witch3', 'witch4'] witch_hit_sounds = ['witchHit1', 'witchHit2'] t.attack_sounds = witch_sounds @@ -720,15 +721,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0, 0) t.icon_texture = 'warriorIcon' t.icon_mask_texture = 'warriorIconColorMask' - t.head_model = 'warriorHead' - t.torso_model = 'warriorTorso' - t.pelvis_model = 'warriorPelvis' - t.upper_arm_model = 'warriorUpperArm' - t.forearm_model = 'warriorForeArm' - t.hand_model = 'warriorHand' - t.upper_leg_model = 'warriorUpperLeg' - t.lower_leg_model = 'warriorLowerLeg' - t.toes_model = 'warriorToes' + t.head_mesh = 'warriorHead' + t.torso_mesh = 'warriorTorso' + t.pelvis_mesh = 'warriorPelvis' + t.upper_arm_mesh = 'warriorUpperArm' + t.forearm_mesh = 'warriorForeArm' + t.hand_mesh = 'warriorHand' + t.upper_leg_mesh = 'warriorUpperLeg' + t.lower_leg_mesh = 'warriorLowerLeg' + t.toes_mesh = 'warriorToes' warrior_sounds = ['warrior1', 'warrior2', 'warrior3', 'warrior4'] warrior_hit_sounds = ['warriorHit1', 'warriorHit2'] t.attack_sounds = warrior_sounds @@ -747,15 +748,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0, 0) t.icon_texture = 'superheroIcon' t.icon_mask_texture = 'superheroIconColorMask' - t.head_model = 'superheroHead' - t.torso_model = 'superheroTorso' - t.pelvis_model = 'superheroPelvis' - t.upper_arm_model = 'superheroUpperArm' - t.forearm_model = 'superheroForeArm' - t.hand_model = 'superheroHand' - t.upper_leg_model = 'superheroUpperLeg' - t.lower_leg_model = 'superheroLowerLeg' - t.toes_model = 'superheroToes' + t.head_mesh = 'superheroHead' + t.torso_mesh = 'superheroTorso' + t.pelvis_mesh = 'superheroPelvis' + t.upper_arm_mesh = 'superheroUpperArm' + t.forearm_mesh = 'superheroForeArm' + t.hand_mesh = 'superheroHand' + t.upper_leg_mesh = 'superheroUpperLeg' + t.lower_leg_mesh = 'superheroLowerLeg' + t.toes_mesh = 'superheroToes' superhero_sounds = ['superhero1', 'superhero2', 'superhero3', 'superhero4'] superhero_hit_sounds = ['superheroHit1', 'superheroHit2'] t.attack_sounds = superhero_sounds @@ -774,15 +775,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0, 0) t.icon_texture = 'alienIcon' t.icon_mask_texture = 'alienIconColorMask' - t.head_model = 'alienHead' - t.torso_model = 'alienTorso' - t.pelvis_model = 'alienPelvis' - t.upper_arm_model = 'alienUpperArm' - t.forearm_model = 'alienForeArm' - t.hand_model = 'alienHand' - t.upper_leg_model = 'alienUpperLeg' - t.lower_leg_model = 'alienLowerLeg' - t.toes_model = 'alienToes' + t.head_mesh = 'alienHead' + t.torso_mesh = 'alienTorso' + t.pelvis_mesh = 'alienPelvis' + t.upper_arm_mesh = 'alienUpperArm' + t.forearm_mesh = 'alienForeArm' + t.hand_mesh = 'alienHand' + t.upper_leg_mesh = 'alienUpperLeg' + t.lower_leg_mesh = 'alienLowerLeg' + t.toes_mesh = 'alienToes' alien_sounds = ['alien1', 'alien2', 'alien3', 'alien4'] alien_hit_sounds = ['alienHit1', 'alienHit2'] t.attack_sounds = alien_sounds @@ -801,15 +802,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0, 0) t.icon_texture = 'oldLadyIcon' t.icon_mask_texture = 'oldLadyIconColorMask' - t.head_model = 'oldLadyHead' - t.torso_model = 'oldLadyTorso' - t.pelvis_model = 'oldLadyPelvis' - t.upper_arm_model = 'oldLadyUpperArm' - t.forearm_model = 'oldLadyForeArm' - t.hand_model = 'oldLadyHand' - t.upper_leg_model = 'oldLadyUpperLeg' - t.lower_leg_model = 'oldLadyLowerLeg' - t.toes_model = 'oldLadyToes' + t.head_mesh = 'oldLadyHead' + t.torso_mesh = 'oldLadyTorso' + t.pelvis_mesh = 'oldLadyPelvis' + t.upper_arm_mesh = 'oldLadyUpperArm' + t.forearm_mesh = 'oldLadyForeArm' + t.hand_mesh = 'oldLadyHand' + t.upper_leg_mesh = 'oldLadyUpperLeg' + t.lower_leg_mesh = 'oldLadyLowerLeg' + t.toes_mesh = 'oldLadyToes' old_lady_sounds = ['oldLady1', 'oldLady2', 'oldLady3', 'oldLady4'] old_lady_hit_sounds = ['oldLadyHit1', 'oldLadyHit2'] t.attack_sounds = old_lady_sounds @@ -828,15 +829,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0, 0) t.icon_texture = 'gladiatorIcon' t.icon_mask_texture = 'gladiatorIconColorMask' - t.head_model = 'gladiatorHead' - t.torso_model = 'gladiatorTorso' - t.pelvis_model = 'gladiatorPelvis' - t.upper_arm_model = 'gladiatorUpperArm' - t.forearm_model = 'gladiatorForeArm' - t.hand_model = 'gladiatorHand' - t.upper_leg_model = 'gladiatorUpperLeg' - t.lower_leg_model = 'gladiatorLowerLeg' - t.toes_model = 'gladiatorToes' + t.head_mesh = 'gladiatorHead' + t.torso_mesh = 'gladiatorTorso' + t.pelvis_mesh = 'gladiatorPelvis' + t.upper_arm_mesh = 'gladiatorUpperArm' + t.forearm_mesh = 'gladiatorForeArm' + t.hand_mesh = 'gladiatorHand' + t.upper_leg_mesh = 'gladiatorUpperLeg' + t.lower_leg_mesh = 'gladiatorLowerLeg' + t.toes_mesh = 'gladiatorToes' gladiator_sounds = ['gladiator1', 'gladiator2', 'gladiator3', 'gladiator4'] gladiator_hit_sounds = ['gladiatorHit1', 'gladiatorHit2'] t.attack_sounds = gladiator_sounds @@ -855,15 +856,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0, 0) t.icon_texture = 'wrestlerIcon' t.icon_mask_texture = 'wrestlerIconColorMask' - t.head_model = 'wrestlerHead' - t.torso_model = 'wrestlerTorso' - t.pelvis_model = 'wrestlerPelvis' - t.upper_arm_model = 'wrestlerUpperArm' - t.forearm_model = 'wrestlerForeArm' - t.hand_model = 'wrestlerHand' - t.upper_leg_model = 'wrestlerUpperLeg' - t.lower_leg_model = 'wrestlerLowerLeg' - t.toes_model = 'wrestlerToes' + t.head_mesh = 'wrestlerHead' + t.torso_mesh = 'wrestlerTorso' + t.pelvis_mesh = 'wrestlerPelvis' + t.upper_arm_mesh = 'wrestlerUpperArm' + t.forearm_mesh = 'wrestlerForeArm' + t.hand_mesh = 'wrestlerHand' + t.upper_leg_mesh = 'wrestlerUpperLeg' + t.lower_leg_mesh = 'wrestlerLowerLeg' + t.toes_mesh = 'wrestlerToes' wrestler_sounds = ['wrestler1', 'wrestler2', 'wrestler3', 'wrestler4'] wrestler_hit_sounds = ['wrestlerHit1', 'wrestlerHit2'] t.attack_sounds = wrestler_sounds @@ -882,15 +883,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0, 0) t.icon_texture = 'operaSingerIcon' t.icon_mask_texture = 'operaSingerIconColorMask' - t.head_model = 'operaSingerHead' - t.torso_model = 'operaSingerTorso' - t.pelvis_model = 'operaSingerPelvis' - t.upper_arm_model = 'operaSingerUpperArm' - t.forearm_model = 'operaSingerForeArm' - t.hand_model = 'operaSingerHand' - t.upper_leg_model = 'operaSingerUpperLeg' - t.lower_leg_model = 'operaSingerLowerLeg' - t.toes_model = 'operaSingerToes' + t.head_mesh = 'operaSingerHead' + t.torso_mesh = 'operaSingerTorso' + t.pelvis_mesh = 'operaSingerPelvis' + t.upper_arm_mesh = 'operaSingerUpperArm' + t.forearm_mesh = 'operaSingerForeArm' + t.hand_mesh = 'operaSingerHand' + t.upper_leg_mesh = 'operaSingerUpperLeg' + t.lower_leg_mesh = 'operaSingerLowerLeg' + t.toes_mesh = 'operaSingerToes' opera_singer_sounds = [ 'operaSinger1', 'operaSinger2', @@ -914,15 +915,15 @@ def register_appearances() -> None: t.default_highlight = (0.65, 0.35, 0.75) t.icon_texture = 'pixieIcon' t.icon_mask_texture = 'pixieIconColorMask' - t.head_model = 'pixieHead' - t.torso_model = 'pixieTorso' - t.pelvis_model = 'pixiePelvis' - t.upper_arm_model = 'pixieUpperArm' - t.forearm_model = 'pixieForeArm' - t.hand_model = 'pixieHand' - t.upper_leg_model = 'pixieUpperLeg' - t.lower_leg_model = 'pixieLowerLeg' - t.toes_model = 'pixieToes' + t.head_mesh = 'pixieHead' + t.torso_mesh = 'pixieTorso' + t.pelvis_mesh = 'pixiePelvis' + t.upper_arm_mesh = 'pixieUpperArm' + t.forearm_mesh = 'pixieForeArm' + t.hand_mesh = 'pixieHand' + t.upper_leg_mesh = 'pixieUpperLeg' + t.lower_leg_mesh = 'pixieLowerLeg' + t.toes_mesh = 'pixieToes' pixie_sounds = ['pixie1', 'pixie2', 'pixie3', 'pixie4'] pixie_hit_sounds = ['pixieHit1', 'pixieHit2'] t.attack_sounds = pixie_sounds @@ -941,15 +942,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0, 0) t.icon_texture = 'robotIcon' t.icon_mask_texture = 'robotIconColorMask' - t.head_model = 'robotHead' - t.torso_model = 'robotTorso' - t.pelvis_model = 'robotPelvis' - t.upper_arm_model = 'robotUpperArm' - t.forearm_model = 'robotForeArm' - t.hand_model = 'robotHand' - t.upper_leg_model = 'robotUpperLeg' - t.lower_leg_model = 'robotLowerLeg' - t.toes_model = 'robotToes' + t.head_mesh = 'robotHead' + t.torso_mesh = 'robotTorso' + t.pelvis_mesh = 'robotPelvis' + t.upper_arm_mesh = 'robotUpperArm' + t.forearm_mesh = 'robotForeArm' + t.hand_mesh = 'robotHand' + t.upper_leg_mesh = 'robotUpperLeg' + t.lower_leg_mesh = 'robotLowerLeg' + t.toes_mesh = 'robotToes' robot_sounds = ['robot1', 'robot2', 'robot3', 'robot4'] robot_hit_sounds = ['robotHit1', 'robotHit2'] t.attack_sounds = robot_sounds @@ -968,15 +969,15 @@ def register_appearances() -> None: t.default_highlight = (1, 0.5, 0.5) t.icon_texture = 'bunnyIcon' t.icon_mask_texture = 'bunnyIconColorMask' - t.head_model = 'bunnyHead' - t.torso_model = 'bunnyTorso' - t.pelvis_model = 'bunnyPelvis' - t.upper_arm_model = 'bunnyUpperArm' - t.forearm_model = 'bunnyForeArm' - t.hand_model = 'bunnyHand' - t.upper_leg_model = 'bunnyUpperLeg' - t.lower_leg_model = 'bunnyLowerLeg' - t.toes_model = 'bunnyToes' + t.head_mesh = 'bunnyHead' + t.torso_mesh = 'bunnyTorso' + t.pelvis_mesh = 'bunnyPelvis' + t.upper_arm_mesh = 'bunnyUpperArm' + t.forearm_mesh = 'bunnyForeArm' + t.hand_mesh = 'bunnyHand' + t.upper_leg_mesh = 'bunnyUpperLeg' + t.lower_leg_mesh = 'bunnyLowerLeg' + t.toes_mesh = 'bunnyToes' bunny_sounds = ['bunny1', 'bunny2', 'bunny3', 'bunny4'] bunny_hit_sounds = ['bunnyHit1', 'bunnyHit2'] t.attack_sounds = bunny_sounds diff --git a/assets/src/ba_data/python/bastd/actor/spazbot.py b/src/assets/ba_data/python/bastd/actor/spazbot.py similarity index 85% rename from assets/src/ba_data/python/bastd/actor/spazbot.py rename to src/assets/ba_data/python/bastd/actor/spazbot.py index 9e0c6877..8c0a5237 100644 --- a/assets/src/ba_data/python/bastd/actor/spazbot.py +++ b/src/assets/ba_data/python/bastd/actor/spazbot.py @@ -7,9 +7,10 @@ from __future__ import annotations import random import weakref +import logging from typing import TYPE_CHECKING -import ba +import bascenev1 as bs from bastd.actor.spaz import Spaz if TYPE_CHECKING: @@ -25,13 +26,13 @@ PRO_BOT_HIGHLIGHT = (0.6, 0.1, 0.05) class SpazBotPunchedMessage: - """A message saying a ba.SpazBot got punched. + """A message saying a bs.SpazBot got punched. Category: **Message Classes** """ spazbot: SpazBot - """The ba.SpazBot that got punched.""" + """The bs.SpazBot that got punched.""" damage: int """How much damage was done to the SpazBot.""" @@ -43,7 +44,7 @@ class SpazBotPunchedMessage: class SpazBotDiedMessage: - """A message saying a ba.SpazBot has died. + """A message saying a bs.SpazBot has died. Category: **Message Classes** """ @@ -51,17 +52,17 @@ class SpazBotDiedMessage: spazbot: SpazBot """The SpazBot that was killed.""" - killerplayer: ba.Player | None - """The ba.Player that killed it (or None).""" + killerplayer: bs.Player | None + """The bascenev1.Player that killed it (or None).""" - how: ba.DeathType + how: bs.DeathType """The particular type of death.""" def __init__( self, spazbot: SpazBot, - killerplayer: ba.Player | None, - how: ba.DeathType, + killerplayer: bs.Player | None, + how: bs.DeathType, ): """Instantiate with given values.""" self.spazbot = spazbot @@ -70,20 +71,20 @@ class SpazBotDiedMessage: class SpazBot(Spaz): - """A really dumb AI version of ba.Spaz. + """A really dumb AI version of bs.Spaz. Category: **Bot Classes** - Add these to a ba.BotSet to use them. + Add these to a bs.BotSet to use them. Note: currently the AI has no real ability to navigate obstacles and so should only be used on wide-open maps. - When a SpazBot is killed, it delivers a ba.SpazBotDiedMessage + When a SpazBot is killed, it delivers a bs.SpazBotDiedMessage to the current activity. - When a SpazBot is punched, it delivers a ba.SpazBotPunchedMessage + When a SpazBot is punched, it delivers a bs.SpazBotPunchedMessage to the current activity. """ @@ -123,14 +124,14 @@ class SpazBot(Spaz): # update should be run and True if not. self.update_callback: Callable[[SpazBot], Any] | None = None activity = self.activity - assert isinstance(activity, ba.GameActivity) + assert isinstance(activity, bs.GameActivity) self._map = weakref.ref(activity.map) - self.last_player_attacked_by: ba.Player | None = None + self.last_player_attacked_by: bs.Player | None = None self.last_attacked_time = 0.0 self.last_attacked_type: tuple[str, str] | None = None - self.target_point_default: ba.Vec3 | None = None + self.target_point_default: bs.Vec3 | None = None self.held_count = 0 - self.last_player_held_by: ba.Player | None = None + self.last_player_held_by: bs.Player | None = None self.target_flag: Flag | None = None self._charge_speed = 0.5 * ( self.charge_speed_min + self.charge_speed_max @@ -144,7 +145,7 @@ class SpazBot(Spaz): self._throw_release_time: float | None = None self._have_dropped_throw_bomb: bool | None = None - self._player_pts: list[tuple[ba.Vec3, ba.Vec3]] | None = None + self._player_pts: list[tuple[bs.Vec3, bs.Vec3]] | None = None # These cooldowns didn't exist when these bots were calibrated, # so take them out of the equation. @@ -157,22 +158,22 @@ class SpazBot(Spaz): self.curse() @property - def map(self) -> ba.Map: + def map(self) -> bs.Map: """The map this bot was created on.""" mval = self._map() assert mval is not None return mval - def _get_target_player_pt(self) -> tuple[ba.Vec3 | None, ba.Vec3 | None]: + def _get_target_player_pt(self) -> tuple[bs.Vec3 | None, bs.Vec3 | None]: """Returns the position and velocity of our target. Both values will be None in the case of no target. """ assert self.node - botpt = ba.Vec3(self.node.position) + botpt = bs.Vec3(self.node.position) closest_dist: float | None = None - closest_vel: ba.Vec3 | None = None - closest: ba.Vec3 | None = None + closest_vel: bs.Vec3 | None = None + closest: bs.Vec3 | None = None assert self._player_pts is not None for plpt, plvel in self._player_pts: dist = (plpt - botpt).length() @@ -189,12 +190,12 @@ class SpazBot(Spaz): assert closest_vel is not None assert closest is not None return ( - ba.Vec3(closest[0], closest[1], closest[2]), - ba.Vec3(closest_vel[0], closest_vel[1], closest_vel[2]), + bs.Vec3(closest[0], closest[1], closest[2]), + bs.Vec3(closest_vel[0], closest_vel[1], closest_vel[2]), ) return None, None - def set_player_points(self, pts: list[tuple[ba.Vec3, ba.Vec3]]) -> None: + def set_player_points(self, pts: list[tuple[bs.Vec3, bs.Vec3]]) -> None: """Provide the spaz-bot with the locations of its enemies.""" self._player_pts = pts @@ -212,11 +213,11 @@ class SpazBot(Spaz): return pos = self.node.position - our_pos = ba.Vec3(pos[0], 0, pos[2]) + our_pos = bs.Vec3(pos[0], 0, pos[2]) can_attack = True - target_pt_raw: ba.Vec3 | None - target_vel: ba.Vec3 | None + target_pt_raw: bs.Vec3 | None + target_vel: bs.Vec3 | None # If we're a flag-bearer, we're pretty simple-minded - just walk # towards the flag and try to pick it up. @@ -234,9 +235,9 @@ class SpazBot(Spaz): # Otherwise try to go pick it up. elif self.target_flag.node: - target_pt_raw = ba.Vec3(*self.target_flag.node.position) + target_pt_raw = bs.Vec3(*self.target_flag.node.position) diff = target_pt_raw - our_pos - diff = ba.Vec3(diff[0], 0, diff[2]) # Don't care about y. + diff = bs.Vec3(diff[0], 0, diff[2]) # Don't care about y. dist = diff.length() to_target = diff.normalized() @@ -275,7 +276,7 @@ class SpazBot(Spaz): # Use default target if we've got one. if self.target_point_default is not None: target_pt_raw = self.target_point_default - target_vel = ba.Vec3(0, 0, 0) + target_vel = bs.Vec3(0, 0, 0) can_attack = False # With no target, we stop moving and drop whatever we're holding. @@ -307,9 +308,8 @@ class SpazBot(Spaz): if self._mode == 'throw': # We can only throw if alive and well. if not self._dead and not self.node.knockout: - assert self._throw_release_time is not None - time_till_throw = self._throw_release_time - ba.time() + time_till_throw = self._throw_release_time - bs.time() if not self.node.hold_node: # If we haven't thrown yet, whip out the bomb. @@ -325,7 +325,7 @@ class SpazBot(Spaz): # Oh crap, we're holding a bomb; better throw it. elif time_till_throw <= 0.0: # Jump and throw. - def _safe_pickup(node: ba.Node) -> None: + def _safe_pickup(node: bs.Node) -> None: if node and self.node: self.node.pickup_pressed = True self.node.pickup_pressed = False @@ -335,10 +335,10 @@ class SpazBot(Spaz): self.node.jump_pressed = False # Throws: - ba.timer(0.1, ba.Call(_safe_pickup, self.node)) + bs.timer(0.1, bs.Call(_safe_pickup, self.node)) else: # Throws: - ba.timer(0.1, ba.Call(_safe_pickup, self.node)) + bs.timer(0.1, bs.Call(_safe_pickup, self.node)) if self.static: if time_till_throw < 0.3: @@ -380,7 +380,7 @@ class SpazBot(Spaz): elif self._mode == 'wait': # Every now and then, aim towards our target. # Other than that, just stand there. - if ba.time(timeformat=ba.TimeFormat.MILLISECONDS) % 1234 < 100: + if int(bs.time() * 1000.0) % 1234 < 100: self.node.move_left_right = to_target.x * (400.0 / 33000) self.node.move_up_down = to_target.z * (-400.0 / 33000) else: @@ -402,7 +402,6 @@ class SpazBot(Spaz): # We might wanna switch states unless we're doing a throw # (in which case that's our sole concern). if self._mode != 'throw': - # If we're currently charging, keep track of how far we are # from our target. When this value increases it means our charge # is over (ran by them or something). @@ -427,7 +426,7 @@ class SpazBot(Spaz): else (0.1 + random.random() * 0.4) ) self._have_dropped_throw_bomb = False - self._throw_release_time = ba.time() + ( + self._throw_release_time = bs.time() + ( 1.0 / self.throw_rate ) * (0.8 + 1.3 * random.random()) @@ -474,13 +473,13 @@ class SpazBot(Spaz): if ( self._running and 1.2 < dist < 2.2 - and ba.time() - self._last_jump_time > 1.0 + and bs.time() - self._last_jump_time > 1.0 ) or ( self.bouncy - and ba.time() - self._last_jump_time > 0.4 + and bs.time() - self._last_jump_time > 0.4 and random.random() < 0.5 ): - self._last_jump_time = ba.time() + self._last_jump_time = bs.time() self.node.jump_pressed = True self.node.jump_pressed = False @@ -492,10 +491,10 @@ class SpazBot(Spaz): def on_punched(self, damage: int) -> None: """ - Method override; sends ba.SpazBotPunchedMessage + Method override; sends bs.SpazBotPunchedMessage to the current activity. """ - ba.getactivity().handlemessage(SpazBotPunchedMessage(self, damage)) + bs.getactivity().handlemessage(SpazBotPunchedMessage(self, damage)) def on_expire(self) -> None: super().on_expire() @@ -509,14 +508,14 @@ class SpazBot(Spaz): assert not self.expired # Keep track of if we're being held and by who most recently. - if isinstance(msg, ba.PickedUpMessage): + if isinstance(msg, bs.PickedUpMessage): super().handlemessage(msg) # Augment standard behavior. self.held_count += 1 picked_up_by = msg.node.source_player if picked_up_by: self.last_player_held_by = picked_up_by - elif isinstance(msg, ba.DroppedMessage): + elif isinstance(msg, bs.DroppedMessage): super().handlemessage(msg) # Augment standard behavior. self.held_count -= 1 if self.held_count < 0: @@ -529,20 +528,18 @@ class SpazBot(Spaz): else: picked_up_by = None except Exception: - ba.print_exception('Error on SpazBot DroppedMessage.') + logging.exception('Error on SpazBot DroppedMessage.') picked_up_by = None if picked_up_by: self.last_player_attacked_by = picked_up_by - self.last_attacked_time = ba.time() + self.last_attacked_time = bs.time() self.last_attacked_type = ('picked_up', 'default') - elif isinstance(msg, ba.DieMessage): - + elif isinstance(msg, bs.DieMessage): # Report normal deaths for scoring purposes. if not self._dead and not msg.immediate: - - killerplayer: ba.Player | None + killerplayer: bs.Player | None # If this guy was being held at the time of death, the # holder is the killer. @@ -554,7 +551,7 @@ class SpazBot(Spaz): # Otherwise it was a suicide. if ( self.last_player_attacked_by - and ba.time() - self.last_attacked_time < 4.0 + and bs.time() - self.last_attacked_time < 4.0 ): killerplayer = self.last_player_attacked_by else: @@ -571,11 +568,11 @@ class SpazBot(Spaz): super().handlemessage(msg) # Augment standard behavior. # Keep track of the player who last hit us for point rewarding. - elif isinstance(msg, ba.HitMessage): - source_player = msg.get_source_player(ba.Player) + elif isinstance(msg, bs.HitMessage): + source_player = msg.get_source_player(bs.Player) if source_player: self.last_player_attacked_by = source_player - self.last_attacked_time = ba.time() + self.last_attacked_time = bs.time() self.last_attacked_type = (msg.hit_type, msg.hit_subtype) super().handlemessage(msg) else: @@ -593,7 +590,7 @@ class BomberBot(SpazBot): class BomberBotLite(BomberBot): - """A less aggressive yellow version of ba.BomberBot. + """A less aggressive yellow version of bs.BomberBot. category: Bot Classes """ @@ -608,7 +605,7 @@ class BomberBotLite(BomberBot): class BomberBotStaticLite(BomberBotLite): - """A less aggressive generally immobile weak version of ba.BomberBot. + """A less aggressive generally immobile weak version of bs.BomberBot. category: Bot Classes """ @@ -618,7 +615,7 @@ class BomberBotStaticLite(BomberBotLite): class BomberBotStatic(BomberBot): - """A version of ba.BomberBot who generally stays in one place. + """A version of bs.BomberBot who generally stays in one place. category: Bot Classes """ @@ -628,7 +625,7 @@ class BomberBotStatic(BomberBot): class BomberBotPro(BomberBot): - """A more powerful version of ba.BomberBot. + """A more powerful version of bs.BomberBot. category: Bot Classes """ @@ -645,7 +642,7 @@ class BomberBotPro(BomberBot): class BomberBotProShielded(BomberBotPro): - """A more powerful version of ba.BomberBot who starts with shields. + """A more powerful version of bs.BomberBot who starts with shields. category: Bot Classes """ @@ -655,7 +652,7 @@ class BomberBotProShielded(BomberBotPro): class BomberBotProStatic(BomberBotPro): - """A more powerful ba.BomberBot who generally stays in one place. + """A more powerful bs.BomberBot who generally stays in one place. category: Bot Classes """ @@ -665,7 +662,7 @@ class BomberBotProStatic(BomberBotPro): class BomberBotProStaticShielded(BomberBotProShielded): - """A powerful ba.BomberBot with shields who is generally immobile. + """A powerful bs.BomberBot with shields who is generally immobile. category: Bot Classes """ @@ -690,7 +687,7 @@ class BrawlerBot(SpazBot): class BrawlerBotLite(BrawlerBot): - """A weaker version of ba.BrawlerBot. + """A weaker version of bs.BrawlerBot. category: Bot Classes """ @@ -703,7 +700,7 @@ class BrawlerBotLite(BrawlerBot): class BrawlerBotPro(BrawlerBot): - """A stronger version of ba.BrawlerBot. + """A stronger version of bs.BrawlerBot. category: Bot Classes """ @@ -718,7 +715,7 @@ class BrawlerBotPro(BrawlerBot): class BrawlerBotProShielded(BrawlerBotPro): - """A stronger version of ba.BrawlerBot who starts with shields. + """A stronger version of bs.BrawlerBot who starts with shields. category: Bot Classes """ @@ -768,7 +765,7 @@ class BouncyBot(SpazBot): class ChargerBotPro(ChargerBot): - """A stronger ba.ChargerBot. + """A stronger bs.ChargerBot. category: Bot Classes """ @@ -781,7 +778,7 @@ class ChargerBotPro(ChargerBot): class ChargerBotProShielded(ChargerBotPro): - """A stronger ba.ChargerBot who starts with shields. + """A stronger bs.ChargerBot who starts with shields. category: Bot Classes """ @@ -809,7 +806,7 @@ class TriggerBot(SpazBot): class TriggerBotStatic(TriggerBot): - """A ba.TriggerBot who generally stays in one place. + """A bs.TriggerBot who generally stays in one place. category: Bot Classes """ @@ -819,7 +816,7 @@ class TriggerBotStatic(TriggerBot): class TriggerBotPro(TriggerBot): - """A stronger version of ba.TriggerBot. + """A stronger version of bs.TriggerBot. category: Bot Classes """ @@ -838,7 +835,7 @@ class TriggerBotPro(TriggerBot): class TriggerBotProShielded(TriggerBotPro): - """A stronger version of ba.TriggerBot who starts with shields. + """A stronger version of bs.TriggerBot who starts with shields. category: Bot Classes """ @@ -906,7 +903,7 @@ class ExplodeyBotNoTimeLimit(ExplodeyBot): class ExplodeyBotShielded(ExplodeyBot): - """A ba.ExplodeyBot who starts with shields. + """A bs.ExplodeyBot who starts with shields. category: Bot Classes """ @@ -916,7 +913,7 @@ class ExplodeyBotShielded(ExplodeyBot): class SpazBotSet: - """A container/controller for one or more ba.SpazBots. + """A container/controller for one or more bs.SpazBots. category: Bot Classes """ @@ -932,9 +929,9 @@ class SpazBotSet: self._bot_lists: list[list[SpazBot]] = [ [] for _ in range(self._bot_list_count) ] - self._spawn_sound = ba.getsound('spawn') + self._spawn_sound = bs.getsound('spawn') self._spawning_count = 0 - self._bot_update_timer: ba.Timer | None = None + self._bot_update_timer: bs.Timer | None = None self.start_moving() def __del__(self) -> None: @@ -954,7 +951,7 @@ class SpazBotSet: pt=pos, spawn_time=spawn_time, send_spawn_message=False, - spawn_callback=ba.Call( + spawn_callback=bs.Call( self._spawn_bot, bot_type, pos, on_spawn_call ), ) @@ -967,11 +964,11 @@ class SpazBotSet: on_spawn_call: Callable[[SpazBot], Any] | None, ) -> None: spaz = bot_type() - ba.playsound(self._spawn_sound, position=pos) + self._spawn_sound.play(position=pos) assert spaz.node spaz.node.handlemessage('flash') spaz.node.is_area_of_interest = False - spaz.handlemessage(ba.StandMessage(pos, random.uniform(0, 360))) + spaz.handlemessage(bs.StandMessage(pos, random.uniform(0, 360))) self.add_bot(spaz) self._spawning_count -= 1 if on_spawn_call is not None: @@ -993,7 +990,6 @@ class SpazBotSet: return bots def _update(self) -> None: - # Update one of our bot lists each time through. # First off, remove no-longer-existing bots from the list. try: @@ -1002,9 +998,9 @@ class SpazBotSet: ] except Exception: bot_list = [] - ba.print_exception( - 'Error updating bot list: ' - + str(self._bot_lists[self._bot_update_list]) + logging.exception( + 'Error updating bot list: %s', + self._bot_lists[self._bot_update_list], ) self._bot_update_list = ( self._bot_update_list + 1 @@ -1012,8 +1008,8 @@ class SpazBotSet: # Update our list of player points for the bots to use. player_pts = [] - for player in ba.getactivity().players: - assert isinstance(player, ba.Player) + for player in bs.getactivity().players: + assert isinstance(player, bs.Player) try: # TODO: could use abstracted player.position here so we # don't have to assume their actor type, but we have no @@ -1023,12 +1019,12 @@ class SpazBotSet: assert player.actor.node player_pts.append( ( - ba.Vec3(player.actor.node.position), - ba.Vec3(player.actor.node.velocity), + bs.Vec3(player.actor.node.position), + bs.Vec3(player.actor.node.velocity), ) ) except Exception: - ba.print_exception('Error on bot-set _update.') + logging.exception('Error on bot-set _update.') for bot in bot_list: bot.set_player_points(player_pts) @@ -1038,19 +1034,19 @@ class SpazBotSet: """Immediately clear out any bots in the set.""" # Don't do this if the activity is shutting down or dead. - activity = ba.getactivity(doraise=False) + activity = bs.getactivity(doraise=False) if activity is None or activity.expired: return for i, bot_list in enumerate(self._bot_lists): for bot in bot_list: - bot.handlemessage(ba.DieMessage(immediate=True)) + bot.handlemessage(bs.DieMessage(immediate=True)) self._bot_lists[i] = [] def start_moving(self) -> None: """Start processing bot AI updates so they start doing their thing.""" - self._bot_update_timer = ba.Timer( - 0.05, ba.WeakCall(self._update), repeat=True + self._bot_update_timer = bs.Timer( + 0.05, bs.WeakCall(self._update), repeat=True ) def stop_moving(self) -> None: @@ -1071,7 +1067,7 @@ class SpazBotSet: Duration is given in seconds. """ - msg = ba.CelebrateMessage(duration=duration) + msg = bs.CelebrateMessage(duration=duration) for botlist in self._bot_lists: for bot in botlist: if bot: @@ -1091,9 +1087,9 @@ class SpazBotSet: assert bot.node # (should exist if 'if bot' was True) bot.node.move_left_right = 0 bot.node.move_up_down = 0 - ba.timer( + bs.timer( 0.5 * random.random(), - ba.Call(bot.handlemessage, ba.CelebrateMessage()), + bs.Call(bot.handlemessage, bs.CelebrateMessage()), ) jump_duration = random.randrange(400, 500) j = random.randrange(0, 200) @@ -1101,20 +1097,20 @@ class SpazBotSet: bot.node.jump_pressed = True bot.node.jump_pressed = False j += jump_duration - ba.timer( + bs.timer( random.uniform(0.0, 1.0), - ba.Call(bot.node.handlemessage, 'attack_sound'), + bs.Call(bot.node.handlemessage, 'attack_sound'), ) - ba.timer( + bs.timer( random.uniform(1.0, 2.0), - ba.Call(bot.node.handlemessage, 'attack_sound'), + bs.Call(bot.node.handlemessage, 'attack_sound'), ) - ba.timer( + bs.timer( random.uniform(2.0, 3.0), - ba.Call(bot.node.handlemessage, 'attack_sound'), + bs.Call(bot.node.handlemessage, 'attack_sound'), ) def add_bot(self, bot: SpazBot) -> None: - """Add a ba.SpazBot instance to the set.""" + """Add a bs.SpazBot instance to the set.""" self._bot_lists[self._bot_add_list].append(bot) self._bot_add_list = (self._bot_add_list + 1) % self._bot_list_count diff --git a/assets/src/ba_data/python/bastd/actor/spazfactory.py b/src/assets/ba_data/python/bastd/actor/spazfactory.py similarity index 53% rename from assets/src/ba_data/python/bastd/actor/spazfactory.py rename to src/assets/ba_data/python/bastd/actor/spazfactory.py index 08cdde65..c3da0a6b 100644 --- a/assets/src/ba_data/python/bastd/actor/spazfactory.py +++ b/src/assets/ba_data/python/bastd/actor/spazfactory.py @@ -6,8 +6,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba -import ba.internal +import bascenev1 as bs from bastd.gameutils import SharedObjects if TYPE_CHECKING: @@ -15,70 +14,70 @@ if TYPE_CHECKING: class SpazFactory: - """Wraps up media and other resources used by ba.Spaz instances. + """Wraps up media and other resources used by bs.Spaz instances. Category: **Gameplay Classes** - Generally one of these is created per ba.Activity and shared - between all spaz instances. Use ba.Spaz.get_factory() to return + Generally one of these is created per bascenev1.Activity and shared + between all spaz instances. Use bs.Spaz.get_factory() to return the shared factory for the current activity. """ - impact_sounds_medium: Sequence[ba.Sound] - """A tuple of ba.Sound-s for when a ba.Spaz hits something kinda hard.""" + impact_sounds_medium: Sequence[bs.Sound] + """A tuple of bs.Sound-s for when a bs.Spaz hits something kinda hard.""" - impact_sounds_hard: Sequence[ba.Sound] - """A tuple of ba.Sound-s for when a ba.Spaz hits something really hard.""" + impact_sounds_hard: Sequence[bs.Sound] + """A tuple of bs.Sound-s for when a bs.Spaz hits something really hard.""" - impact_sounds_harder: Sequence[ba.Sound] - """A tuple of ba.Sound-s for when a ba.Spaz hits something really + impact_sounds_harder: Sequence[bs.Sound] + """A tuple of bs.Sound-s for when a bs.Spaz hits something really really hard.""" - single_player_death_sound: ba.Sound + single_player_death_sound: bs.Sound """The sound that plays for an 'important' spaz death such as in co-op games.""" - punch_sound_weak: ba.Sound - """A weak punch ba.Sound.""" + punch_sound_weak: bs.Sound + """A weak punch bs.Sound.""" - punch_sound: ba.Sound - """A standard punch ba.Sound.""" + punch_sound: bs.Sound + """A standard punch bs.Sound.""" - punch_sound_strong: Sequence[ba.Sound] - """A tuple of stronger sounding punch ba.Sounds.""" + punch_sound_strong: Sequence[bs.Sound] + """A tuple of stronger sounding punch bs.Sounds.""" - punch_sound_stronger: ba.Sound - """A really really strong sounding punch ba.Sound.""" + punch_sound_stronger: bs.Sound + """A really really strong sounding punch bs.Sound.""" - swish_sound: ba.Sound - """A punch swish ba.Sound.""" + swish_sound: bs.Sound + """A punch swish bs.Sound.""" - block_sound: ba.Sound - """A ba.Sound for when an attack is blocked by invincibility.""" + block_sound: bs.Sound + """A bs.Sound for when an attack is blocked by invincibility.""" - shatter_sound: ba.Sound - """A ba.Sound for when a frozen ba.Spaz shatters.""" + shatter_sound: bs.Sound + """A bs.Sound for when a frozen bs.Spaz shatters.""" - splatter_sound: ba.Sound - """A ba.Sound for when a ba.Spaz blows up via curse.""" + splatter_sound: bs.Sound + """A bs.Sound for when a bs.Spaz blows up via curse.""" - spaz_material: ba.Material - """A ba.Material applied to all of parts of a ba.Spaz.""" + spaz_material: bs.Material + """A bs.Material applied to all of parts of a bs.Spaz.""" - roller_material: ba.Material - """A ba.Material applied to the invisible roller ball body that - a ba.Spaz uses for locomotion.""" + roller_material: bs.Material + """A bs.Material applied to the invisible roller ball body that + a bs.Spaz uses for locomotion.""" - punch_material: ba.Material - """A ba.Material applied to the 'fist' of a ba.Spaz.""" + punch_material: bs.Material + """A bs.Material applied to the 'fist' of a bs.Spaz.""" - pickup_material: ba.Material - """A ba.Material applied to the 'grabber' body of a ba.Spaz.""" + pickup_material: bs.Material + """A bs.Material applied to the 'grabber' body of a bs.Spaz.""" - curse_material: ba.Material - """A ba.Material applied to a cursed ba.Spaz that triggers an explosion.""" + curse_material: bs.Material + """A bs.Material applied to a cursed bs.Spaz that triggers an explosion.""" - _STORENAME = ba.storagename() + _STORENAME = bs.storagename() def _preload(self, character: str) -> None: """Preload media needed for a given character.""" @@ -87,6 +86,10 @@ class SpazFactory: def __init__(self) -> None: """Instantiate a factory object.""" # pylint: disable=cyclic-import + + plus = bs.app.plus + assert plus is not None + # FIXME: should probably put these somewhere common so we don't # have to import them from a module that imports us. from bastd.actor.spaz import ( @@ -97,35 +100,35 @@ class SpazFactory: shared = SharedObjects.get() self.impact_sounds_medium = ( - ba.getsound('impactMedium'), - ba.getsound('impactMedium2'), + bs.getsound('impactMedium'), + bs.getsound('impactMedium2'), ) self.impact_sounds_hard = ( - ba.getsound('impactHard'), - ba.getsound('impactHard2'), - ba.getsound('impactHard3'), + bs.getsound('impactHard'), + bs.getsound('impactHard2'), + bs.getsound('impactHard3'), ) self.impact_sounds_harder = ( - ba.getsound('bigImpact'), - ba.getsound('bigImpact2'), + bs.getsound('bigImpact'), + bs.getsound('bigImpact2'), ) - self.single_player_death_sound = ba.getsound('playerDeath') - self.punch_sound_weak = ba.getsound('punchWeak01') - self.punch_sound = ba.getsound('punch01') + self.single_player_death_sound = bs.getsound('playerDeath') + self.punch_sound_weak = bs.getsound('punchWeak01') + self.punch_sound = bs.getsound('punch01') self.punch_sound_strong = ( - ba.getsound('punchStrong01'), - ba.getsound('punchStrong02'), + bs.getsound('punchStrong01'), + bs.getsound('punchStrong02'), ) - self.punch_sound_stronger = ba.getsound('superPunch') - self.swish_sound = ba.getsound('punchSwish') - self.block_sound = ba.getsound('block') - self.shatter_sound = ba.getsound('shatter') - self.splatter_sound = ba.getsound('splatter') - self.spaz_material = ba.Material() - self.roller_material = ba.Material() - self.punch_material = ba.Material() - self.pickup_material = ba.Material() - self.curse_material = ba.Material() + self.punch_sound_stronger = bs.getsound('superPunch') + self.swish_sound = bs.getsound('punchSwish') + self.block_sound = bs.getsound('block') + self.shatter_sound = bs.getsound('shatter') + self.splatter_sound = bs.getsound('splatter') + self.spaz_material = bs.Material() + self.roller_material = bs.Material() + self.punch_material = bs.Material() + self.pickup_material = bs.Material() + self.curse_material = bs.Material() footing_material = shared.footing_material object_material = shared.object_material @@ -191,13 +194,13 @@ class SpazFactory: ) self.foot_impact_sounds = ( - ba.getsound('footImpact01'), - ba.getsound('footImpact02'), - ba.getsound('footImpact03'), + bs.getsound('footImpact01'), + bs.getsound('footImpact02'), + bs.getsound('footImpact03'), ) - self.foot_skid_sound = ba.getsound('skid01') - self.foot_roll_sound = ba.getsound('scamper01') + self.foot_skid_sound = bs.getsound('skid01') + self.foot_roll_sound = bs.getsound('scamper01') self.roller_material.add_actions( conditions=('they_have_material', footing_material), @@ -208,7 +211,7 @@ class SpazFactory: ), ) - self.skid_sound = ba.getsound('gravelSkid') + self.skid_sound = bs.getsound('gravelSkid') self.spaz_material.add_actions( conditions=('they_have_material', footing_material), @@ -219,9 +222,9 @@ class SpazFactory: ), ) - self.shield_up_sound = ba.getsound('shieldUp') - self.shield_down_sound = ba.getsound('shieldDown') - self.shield_hit_sound = ba.getsound('shieldHit') + self.shield_up_sound = bs.getsound('shieldUp') + self.shield_down_sound = bs.getsound('shieldDown') + self.shield_hit_sound = bs.getsound('shieldHit') # We don't want to collide with stuff we're initially overlapping # (unless its marked with a special region material). @@ -242,23 +245,17 @@ class SpazFactory: # Lets load some basic rules. # (allows them to be tweaked from the master server) - self.shield_decay_rate = ba.internal.get_v1_account_misc_read_val( - 'rsdr', 10.0 - ) - self.punch_cooldown = ba.internal.get_v1_account_misc_read_val( - 'rpc', 400 - ) - self.punch_cooldown_gloves = ba.internal.get_v1_account_misc_read_val( + self.shield_decay_rate = plus.get_v1_account_misc_read_val('rsdr', 10.0) + self.punch_cooldown = plus.get_v1_account_misc_read_val('rpc', 400) + self.punch_cooldown_gloves = plus.get_v1_account_misc_read_val( 'rpcg', 300 ) - self.punch_power_scale = ba.internal.get_v1_account_misc_read_val( - 'rpp', 1.2 + self.punch_power_scale = plus.get_v1_account_misc_read_val('rpp', 1.2) + self.punch_power_scale_gloves = plus.get_v1_account_misc_read_val( + 'rppg', 1.4 ) - self.punch_power_scale_gloves = ( - ba.internal.get_v1_account_misc_read_val('rppg', 1.4) - ) - self.max_shield_spillover_damage = ( - ba.internal.get_v1_account_misc_read_val('rsms', 500) + self.max_shield_spillover_damage = plus.get_v1_account_misc_read_val( + 'rsms', 500 ) def get_style(self, character: str) -> str: @@ -266,30 +263,32 @@ class SpazFactory: (this influences subtle aspects of their appearance, etc) """ - return ba.app.spaz_appearances[character].style + assert bs.app.classic is not None + return bs.app.classic.spaz_appearances[character].style def get_media(self, character: str) -> dict[str, Any]: """Return the set of media used by this variant of spaz.""" - char = ba.app.spaz_appearances[character] + assert bs.app.classic is not None + char = bs.app.classic.spaz_appearances[character] if character not in self.spaz_media: media = self.spaz_media[character] = { - 'jump_sounds': [ba.getsound(s) for s in char.jump_sounds], - 'attack_sounds': [ba.getsound(s) for s in char.attack_sounds], - 'impact_sounds': [ba.getsound(s) for s in char.impact_sounds], - 'death_sounds': [ba.getsound(s) for s in char.death_sounds], - 'pickup_sounds': [ba.getsound(s) for s in char.pickup_sounds], - 'fall_sounds': [ba.getsound(s) for s in char.fall_sounds], - 'color_texture': ba.gettexture(char.color_texture), - 'color_mask_texture': ba.gettexture(char.color_mask_texture), - 'head_model': ba.getmodel(char.head_model), - 'torso_model': ba.getmodel(char.torso_model), - 'pelvis_model': ba.getmodel(char.pelvis_model), - 'upper_arm_model': ba.getmodel(char.upper_arm_model), - 'forearm_model': ba.getmodel(char.forearm_model), - 'hand_model': ba.getmodel(char.hand_model), - 'upper_leg_model': ba.getmodel(char.upper_leg_model), - 'lower_leg_model': ba.getmodel(char.lower_leg_model), - 'toes_model': ba.getmodel(char.toes_model), + 'jump_sounds': [bs.getsound(s) for s in char.jump_sounds], + 'attack_sounds': [bs.getsound(s) for s in char.attack_sounds], + 'impact_sounds': [bs.getsound(s) for s in char.impact_sounds], + 'death_sounds': [bs.getsound(s) for s in char.death_sounds], + 'pickup_sounds': [bs.getsound(s) for s in char.pickup_sounds], + 'fall_sounds': [bs.getsound(s) for s in char.fall_sounds], + 'color_texture': bs.gettexture(char.color_texture), + 'color_mask_texture': bs.gettexture(char.color_mask_texture), + 'head_mesh': bs.getmesh(char.head_mesh), + 'torso_mesh': bs.getmesh(char.torso_mesh), + 'pelvis_mesh': bs.getmesh(char.pelvis_mesh), + 'upper_arm_mesh': bs.getmesh(char.upper_arm_mesh), + 'forearm_mesh': bs.getmesh(char.forearm_mesh), + 'hand_mesh': bs.getmesh(char.hand_mesh), + 'upper_leg_mesh': bs.getmesh(char.upper_leg_mesh), + 'lower_leg_mesh': bs.getmesh(char.lower_leg_mesh), + 'toes_mesh': bs.getmesh(char.toes_mesh), } else: media = self.spaz_media[character] @@ -297,9 +296,9 @@ class SpazFactory: @classmethod def get(cls) -> SpazFactory: - """Return the shared ba.SpazFactory, creating it if necessary.""" + """Return the shared bs.SpazFactory, creating it if necessary.""" # pylint: disable=cyclic-import - activity = ba.getactivity() + activity = bs.getactivity() factory = activity.customdata.get(cls._STORENAME) if factory is None: factory = activity.customdata[cls._STORENAME] = SpazFactory() diff --git a/assets/src/ba_data/python/bastd/actor/text.py b/src/assets/ba_data/python/bastd/actor/text.py similarity index 87% rename from assets/src/ba_data/python/bastd/actor/text.py rename to src/assets/ba_data/python/bastd/actor/text.py index 123ac3ec..b69edd5f 100644 --- a/assets/src/ba_data/python/bastd/actor/text.py +++ b/src/assets/ba_data/python/bastd/actor/text.py @@ -7,13 +7,14 @@ from __future__ import annotations from enum import Enum from typing import TYPE_CHECKING -import ba +import babase +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence -class Text(ba.Actor): +class Text(bs.Actor): """Text with some tricks.""" class Transition(Enum): @@ -55,7 +56,7 @@ class Text(ba.Actor): def __init__( self, - text: str | ba.Lstr, + text: str | babase.Lstr, position: tuple[float, float] = (0.0, 0.0), h_align: HAlign = HAlign.LEFT, v_align: VAlign = VAlign.NONE, @@ -78,7 +79,7 @@ class Text(ba.Actor): # pylint: disable=too-many-branches # pylint: disable=too-many-locals super().__init__() - self.node = ba.newnode( + self.node = bs.newnode( 'text', delegate=self, attrs={ @@ -104,7 +105,7 @@ class Text(ba.Actor): raise RuntimeError( 'fixme: flash and fade-in currently cant both be on' ) - cmb = ba.newnode( + cmb = bs.newnode( 'combine', owner=self.node, attrs={ @@ -118,27 +119,27 @@ class Text(ba.Actor): if transition_out_delay is not None: keys[transition_delay + transition_out_delay] = color[3] keys[transition_delay + transition_out_delay + 0.5] = 0.0 - ba.animate(cmb, 'input3', keys) + bs.animate(cmb, 'input3', keys) cmb.connectattr('output', self.node, 'color') if flash: mult = 2.0 tm1 = 0.15 tm2 = 0.3 - cmb = ba.newnode('combine', owner=self.node, attrs={'size': 4}) - ba.animate( + cmb = bs.newnode('combine', owner=self.node, attrs={'size': 4}) + bs.animate( cmb, 'input0', {0.0: color[0] * mult, tm1: color[0], tm2: color[0] * mult}, loop=True, ) - ba.animate( + bs.animate( cmb, 'input1', {0.0: color[1] * mult, tm1: color[1], tm2: color[1] * mult}, loop=True, ) - ba.animate( + bs.animate( cmb, 'input2', {0.0: color[2] * mult, tm1: color[2], tm2: color[2] * mult}, @@ -147,7 +148,7 @@ class Text(ba.Actor): cmb.input3 = color[3] cmb.connectattr('output', self.node, 'color') - cmb = self.position_combine = ba.newnode( + cmb = self.position_combine = bs.newnode( 'combine', owner=self.node, attrs={'size': 2} ) @@ -157,9 +158,9 @@ class Text(ba.Actor): transition_delay + 0.2: position[0], } o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0} - ba.animate(cmb, 'input0', keys) + bs.animate(cmb, 'input0', keys) cmb.input1 = position[1] - ba.animate(self.node, 'opacity', o_keys) + bs.animate(self.node, 'opacity', o_keys) elif transition is self.Transition.IN_LEFT: keys = { transition_delay: position[0] - 1300, @@ -173,9 +174,9 @@ class Text(ba.Actor): ) o_keys[transition_delay + transition_out_delay + 0.15] = 1.0 o_keys[transition_delay + transition_out_delay + 0.2] = 0.0 - ba.animate(cmb, 'input0', keys) + bs.animate(cmb, 'input0', keys) cmb.input1 = position[1] - ba.animate(self.node, 'opacity', o_keys) + bs.animate(self.node, 'opacity', o_keys) elif transition is self.Transition.IN_BOTTOM_SLOW: keys = { transition_delay: -100.0, @@ -183,8 +184,8 @@ class Text(ba.Actor): } o_keys = {transition_delay: 0.0, transition_delay + 0.2: 1.0} cmb.input0 = position[0] - ba.animate(cmb, 'input1', keys) - ba.animate(self.node, 'opacity', o_keys) + bs.animate(cmb, 'input1', keys) + bs.animate(self.node, 'opacity', o_keys) elif transition is self.Transition.IN_BOTTOM: keys = { transition_delay: -100.0, @@ -197,8 +198,8 @@ class Text(ba.Actor): o_keys[transition_delay + transition_out_delay + 0.15] = 1.0 o_keys[transition_delay + transition_out_delay + 0.2] = 0.0 cmb.input0 = position[0] - ba.animate(cmb, 'input1', keys) - ba.animate(self.node, 'opacity', o_keys) + bs.animate(cmb, 'input1', keys) + bs.animate(self.node, 'opacity', o_keys) elif transition is self.Transition.IN_TOP_SLOW: keys = { transition_delay: 400.0, @@ -206,8 +207,8 @@ class Text(ba.Actor): } o_keys = {transition_delay: 0, transition_delay + 1.0: 1.0} cmb.input0 = position[0] - ba.animate(cmb, 'input1', keys) - ba.animate(self.node, 'opacity', o_keys) + bs.animate(cmb, 'input1', keys) + bs.animate(self.node, 'opacity', o_keys) else: assert transition is self.Transition.FADE_IN or transition is None cmb.input0 = position[0] @@ -216,14 +217,14 @@ class Text(ba.Actor): # If we're transitioning out, die at the end of it. if transition_out_delay is not None: - ba.timer( + bs.timer( transition_delay + transition_out_delay + 1.0, - ba.WeakCall(self.handlemessage, ba.DieMessage()), + babase.WeakCall(self.handlemessage, bs.DieMessage()), ) def handlemessage(self, msg: Any) -> Any: assert not self.expired - if isinstance(msg, ba.DieMessage): + if isinstance(msg, bs.DieMessage): if self.node: self.node.delete() return None diff --git a/assets/src/ba_data/python/bastd/actor/tipstext.py b/src/assets/ba_data/python/bastd/actor/tipstext.py similarity index 76% rename from assets/src/ba_data/python/bastd/actor/tipstext.py rename to src/assets/ba_data/python/bastd/actor/tipstext.py index f642d959..a462a3ac 100644 --- a/assets/src/ba_data/python/bastd/actor/tipstext.py +++ b/src/assets/ba_data/python/bastd/actor/tipstext.py @@ -6,13 +6,14 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba +import babase +import bascenev1 as bs if TYPE_CHECKING: from typing import Any -class TipsText(ba.Actor): +class TipsText(bs.Actor): """A bit of text showing various helpful game tips.""" def __init__(self, offs_y: float = 100.0): @@ -20,7 +21,7 @@ class TipsText(ba.Actor): self._tip_scale = 0.8 self._tip_title_scale = 1.1 self._offs_y = offs_y - self.node = ba.newnode( + self.node = bs.newnode( 'text', delegate=self, attrs={ @@ -33,10 +34,10 @@ class TipsText(ba.Actor): 'v_attach': 'bottom', }, ) - tval = ba.Lstr( - value='${A}:', subs=[('${A}', ba.Lstr(resource='tipText'))] + tval = babase.Lstr( + value='${A}:', subs=[('${A}', babase.Lstr(resource='tipText'))] ) - self.title_node = ba.newnode( + self.title_node = bs.newnode( 'text', delegate=self, attrs={ @@ -51,12 +52,12 @@ class TipsText(ba.Actor): ) self._message_duration = 10000 self._message_spacing = 3000 - self._change_timer = ba.Timer( + self._change_timer = bs.Timer( 0.001 * (self._message_duration + self._message_spacing), - ba.WeakCall(self.change_phrase), + babase.WeakCall(self.change_phrase), repeat=True, ) - self._combine = ba.newnode( + self._combine = bs.newnode( 'combine', owner=self.node, attrs={'input0': 1.0, 'input1': 0.8, 'input2': 1.0, 'size': 4}, @@ -67,10 +68,15 @@ class TipsText(ba.Actor): def change_phrase(self) -> None: """Switch the visible tip phrase.""" - from ba.internal import get_remote_app_name, get_next_tip + from babase.internal import get_remote_app_name - next_tip = ba.Lstr( - translate=('tips', get_next_tip()), + next_tip = babase.Lstr( + translate=( + 'tips', + babase.app.classic.get_next_tip() + if babase.app.classic is not None + else '', + ), subs=[('${REMOTE_APP_NAME}', get_remote_app_name())], ) spc = self._message_spacing @@ -83,17 +89,16 @@ class TipsText(ba.Actor): spc + self._message_duration - 1000: 1.0, spc + self._message_duration: 0.0, } - ba.animate( + bs.animate( self._combine, 'input3', - {k: v * 0.5 for k, v in list(keys.items())}, - timeformat=ba.TimeFormat.MILLISECONDS, + {k / 1000.0: v * 0.5 for k, v in list(keys.items())}, ) self.node.text = next_tip def handlemessage(self, msg: Any) -> Any: assert not self.expired - if isinstance(msg, ba.DieMessage): + if isinstance(msg, bs.DieMessage): if self.node: self.node.delete() self.title_node.delete() diff --git a/assets/src/ba_data/python/bastd/actor/zoomtext.py b/src/assets/ba_data/python/bastd/actor/zoomtext.py similarity index 82% rename from assets/src/ba_data/python/bastd/actor/zoomtext.py rename to src/assets/ba_data/python/bastd/actor/zoomtext.py index 2cca306f..0a63dae2 100644 --- a/assets/src/ba_data/python/bastd/actor/zoomtext.py +++ b/src/assets/ba_data/python/bastd/actor/zoomtext.py @@ -7,13 +7,14 @@ from __future__ import annotations import random from typing import TYPE_CHECKING -import ba +import babase +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence -class ZoomText(ba.Actor): +class ZoomText(bs.Actor): """Big Zooming Text. Category: Gameplay Classes @@ -23,7 +24,7 @@ class ZoomText(ba.Actor): def __init__( self, - text: str | ba.Lstr, + text: str | babase.Lstr, position: tuple[float, float] = (0.0, 0.0), shiftposition: tuple[float, float] | None = None, shiftdelay: float | None = None, @@ -46,10 +47,10 @@ class ZoomText(ba.Actor): if shiftdelay is None: shiftdelay = 2.500 if shiftdelay < 0.0: - ba.print_error('got shiftdelay < 0') + babase.print_error('got shiftdelay < 0') shiftdelay = 0.0 self._project_scale = project_scale - self.node = ba.newnode( + self.node = bs.newnode( 'text', delegate=self, attrs={ @@ -68,7 +69,7 @@ class ZoomText(ba.Actor): ) # we never jitter in vr mode.. - if ba.app.vr_mode: + if babase.app.vr_mode: jitter = 0.0 # if they want jitter, animate its position slightly... @@ -79,24 +80,26 @@ class ZoomText(ba.Actor): # then resume jittering if shiftposition is not None: positionadjusted2 = (shiftposition[0], shiftposition[1] - 100) - ba.timer( + bs.timer( shiftdelay, - ba.WeakCall(self._shift, positionadjusted, positionadjusted2), + babase.WeakCall( + self._shift, positionadjusted, positionadjusted2 + ), ) if jitter > 0.0: - ba.timer( + bs.timer( shiftdelay + 0.25, - ba.WeakCall( + babase.WeakCall( self._jitter, positionadjusted2, jitter * scale ), ) - color_combine = ba.newnode( + color_combine = bs.newnode( 'combine', owner=self.node, attrs={'input2': color[2], 'input3': 1.0, 'size': 4}, ) if trail: - trailcolor_n = ba.newnode( + trailcolor_n = bs.newnode( 'combine', owner=self.node, attrs={ @@ -108,7 +111,7 @@ class ZoomText(ba.Actor): ) trailcolor_n.connectattr('output', self.node, 'trailcolor') basemult = 0.85 - ba.animate( + bs.animate( self.node, 'trail_project_scale', { @@ -125,19 +128,19 @@ class ZoomText(ba.Actor): mult = 2.0 tm1 = 0.15 tm2 = 0.3 - ba.animate( + bs.animate( color_combine, 'input0', {0: color[0] * mult, tm1: color[0], tm2: color[0] * mult}, loop=True, ) - ba.animate( + bs.animate( color_combine, 'input1', {0: color[1] * mult, tm1: color[1], tm2: color[1] * mult}, loop=True, ) - ba.animate( + bs.animate( color_combine, 'input2', {0: color[2] * mult, tm1: color[2], tm2: color[2] * mult}, @@ -147,7 +150,7 @@ class ZoomText(ba.Actor): color_combine.input0 = color[0] color_combine.input1 = color[1] color_combine.connectattr('output', self.node, 'color') - ba.animate( + bs.animate( self.node, 'project_scale', {0: 0, 0.27: 1.05 * project_scale, 0.3: 1 * project_scale}, @@ -155,17 +158,19 @@ class ZoomText(ba.Actor): # if they give us a lifespan, kill ourself down the line if lifespan is not None: - ba.timer(lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage())) + bs.timer( + lifespan, babase.WeakCall(self.handlemessage, bs.DieMessage()) + ) def handlemessage(self, msg: Any) -> Any: assert not self.expired - if isinstance(msg, ba.DieMessage): + if isinstance(msg, bs.DieMessage): if not self._dying and self.node: self._dying = True if msg.immediate: self.node.delete() else: - ba.animate( + bs.animate( self.node, 'project_scale', { @@ -173,9 +178,9 @@ class ZoomText(ba.Actor): 0.6: 1.2 * self._project_scale, }, ) - ba.animate(self.node, 'opacity', {0.0: 1, 0.3: 0}) - ba.animate(self.node, 'trail_opacity', {0.0: 1, 0.6: 0}) - ba.timer(0.7, self.node.delete) + bs.animate(self.node, 'opacity', {0.0: 1, 0.3: 0}) + bs.animate(self.node, 'trail_opacity', {0.0: 1, 0.6: 0}) + bs.timer(0.7, self.node.delete) return None return super().handlemessage(msg) @@ -184,7 +189,7 @@ class ZoomText(ba.Actor): ) -> None: if not self.node: return - cmb = ba.newnode('combine', owner=self.node, attrs={'size': 2}) + cmb = bs.newnode('combine', owner=self.node, attrs={'size': 2}) for index, attr in enumerate(['input0', 'input1']): keys = {} timeval = 0.0 @@ -195,7 +200,7 @@ class ZoomText(ba.Actor): + (random.random() - 0.5) * jitter_amount * 1.6 ) timeval += random.random() * 0.1 - ba.animate(cmb, attr, keys, loop=True) + bs.animate(cmb, attr, keys, loop=True) cmb.connectattr('output', self.node, 'position') def _shift( @@ -203,7 +208,7 @@ class ZoomText(ba.Actor): ) -> None: if not self.node: return - cmb = ba.newnode('combine', owner=self.node, attrs={'size': 2}) - ba.animate(cmb, 'input0', {0.0: position1[0], 0.25: position2[0]}) - ba.animate(cmb, 'input1', {0.0: position1[1], 0.25: position2[1]}) + cmb = bs.newnode('combine', owner=self.node, attrs={'size': 2}) + bs.animate(cmb, 'input0', {0.0: position1[0], 0.25: position2[0]}) + bs.animate(cmb, 'input1', {0.0: position1[1], 0.25: position2[1]}) cmb.connectattr('output', self.node, 'position') diff --git a/assets/src/ba_data/python/bastd/game/__init__.py b/src/assets/ba_data/python/bastd/game/__init__.py similarity index 100% rename from assets/src/ba_data/python/bastd/game/__init__.py rename to src/assets/ba_data/python/bastd/game/__init__.py diff --git a/assets/src/ba_data/python/bastd/game/assault.py b/src/assets/ba_data/python/bastd/game/assault.py similarity index 78% rename from assets/src/ba_data/python/bastd/game/assault.py rename to src/assets/ba_data/python/bastd/game/assault.py index a2feb11a..77f210d1 100644 --- a/assets/src/ba_data/python/bastd/game/assault.py +++ b/src/assets/ba_data/python/bastd/game/assault.py @@ -2,7 +2,7 @@ # """Defines assault minigame.""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations @@ -10,21 +10,21 @@ from __future__ import annotations import random from typing import TYPE_CHECKING -import ba from bastd.actor.playerspaz import PlayerSpaz from bastd.actor.flag import Flag from bastd.actor.scoreboard import Scoreboard from bastd.gameutils import SharedObjects +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self, base_pos: Sequence[float], flag: Flag) -> None: @@ -34,18 +34,18 @@ class Team(ba.Team[Player]): # ba_meta export game -class AssaultGame(ba.TeamGameActivity[Player, Team]): +class AssaultGame(bs.TeamGameActivity[Player, Team]): """Game where you score by touching the other team's flag.""" name = 'Assault' description = 'Reach the enemy flag to score.' available_settings = [ - ba.IntSetting( + bs.IntSetting( 'Score to Win', min_value=1, default=3, ), - ba.IntChoiceSetting( + bs.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), @@ -57,7 +57,7 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]): ], default=0, ), - ba.FloatChoiceSetting( + bs.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), @@ -68,23 +68,24 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]): ], default=1.0, ), - ba.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Epic Mode', default=False), ] @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: - return issubclass(sessiontype, ba.DualTeamSession) + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: - return ba.getmaps('team_flag') + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + assert bs.app.classic is not None + return bs.app.classic.getmaps('team_flag') def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() self._last_score_time = 0.0 - self._score_sound = ba.getsound('score') - self._base_region_materials: dict[int, ba.Material] = {} + self._score_sound = bs.getsound('score') + self._base_region_materials: dict[int, bs.Material] = {} self._epic_mode = bool(settings['Epic Mode']) self._score_to_win = int(settings['Score to Win']) self._time_limit = float(settings['Time Limit']) @@ -92,7 +93,7 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]): # Base class overrides self.slow_motion = self._epic_mode self.default_music = ( - ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FORWARD_MARCH + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH ) def get_instance_description(self) -> str | Sequence: @@ -105,10 +106,10 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]): return 'touch 1 flag' return 'touch ${ARG1} flags', self._score_to_win - def create_team(self, sessionteam: ba.SessionTeam) -> Team: + def create_team(self, sessionteam: bs.SessionTeam) -> Team: shared = SharedObjects.get() base_pos = self.map.get_flag_position(sessionteam.id) - ba.newnode( + bs.newnode( 'light', attrs={ 'position': base_pos, @@ -123,7 +124,7 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]): flag = Flag(touchable=False, position=base_pos, color=sessionteam.color) team = Team(base_pos=base_pos, flag=flag) - mat = self._base_region_materials[sessionteam.id] = ba.Material() + mat = self._base_region_materials[sessionteam.id] = bs.Material() mat.add_actions( conditions=('they_have_material', shared.player_material), actions=( @@ -132,12 +133,12 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]): ( 'call', 'at_connect', - ba.Call(self._handle_base_collide, team), + bs.Call(self._handle_base_collide, team), ), ), ) - ba.newnode( + bs.newnode( 'region', owner=flag.node, attrs={ @@ -161,14 +162,14 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]): self.setup_standard_powerup_drops() def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.PlayerDiedMessage): + if isinstance(msg, bs.PlayerDiedMessage): super().handlemessage(msg) # Augment standard. self.respawn_player(msg.getplayer(Player)) else: super().handlemessage(msg) def _flash_base(self, team: Team, length: float = 2.0) -> None: - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': team.base_pos, @@ -177,13 +178,13 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]): 'color': team.color, }, ) - ba.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True) - ba.timer(length, light.delete) + bs.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True) + bs.timer(length, light.delete) def _handle_base_collide(self, team: Team) -> None: try: - spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True) - except ba.NotFoundError: + spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True) + except bs.NotFoundError: return if not spaz.is_alive(): @@ -191,18 +192,17 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]): try: player = spaz.getplayer(Player, True) - except ba.NotFoundError: + except bs.NotFoundError: return # If its another team's player, they scored. player_team = player.team if player_team is not team: - # Prevent multiple simultaneous scores. - if ba.time() != self._last_score_time: - self._last_score_time = ba.time() + if bs.apptime() != self._last_score_time: + self._last_score_time = bs.apptime() self.stats.player_scored(player, 50, big_message=True) - ba.playsound(self._score_sound) + self._score_sound.play() self._flash_base(team) # Move all players on the scoring team back to their start @@ -210,7 +210,7 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]): for player in player_team.players: if player.is_alive(): pos = player.node.position - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': pos, @@ -219,11 +219,11 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]): 'radius': 0.4, }, ) - ba.timer(0.5, light.delete) - ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0}) + bs.timer(0.5, light.delete) + bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0}) new_pos = self.map.get_start_position(player_team.id) - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': new_pos, @@ -232,17 +232,17 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]): 'height_attenuated': False, }, ) - ba.timer(0.5, light.delete) - ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0}) + bs.timer(0.5, light.delete) + bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0}) if player.actor: player.actor.handlemessage( - ba.StandMessage(new_pos, random.uniform(0, 360)) + bs.StandMessage(new_pos, random.uniform(0, 360)) ) # Have teammates celebrate. for player in player_team.players: if player.actor: - player.actor.handlemessage(ba.CelebrateMessage(2.0)) + player.actor.handlemessage(bs.CelebrateMessage(2.0)) player_team.score += 1 self._update_scoreboard() @@ -250,7 +250,7 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]): self.end_game() def end_game(self) -> None: - results = ba.GameResults() + results = bs.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results=results) diff --git a/assets/src/ba_data/python/bastd/game/capturetheflag.py b/src/assets/ba_data/python/bastd/game/capturetheflag.py similarity index 84% rename from assets/src/ba_data/python/bastd/game/capturetheflag.py rename to src/assets/ba_data/python/bastd/game/capturetheflag.py index bb8ad9cd..8b6625c6 100644 --- a/assets/src/ba_data/python/bastd/game/capturetheflag.py +++ b/src/assets/ba_data/python/bastd/game/capturetheflag.py @@ -2,14 +2,14 @@ # """Defines a capture-the-flag game.""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations +import logging from typing import TYPE_CHECKING -import ba from bastd.actor.playerspaz import PlayerSpaz from bastd.actor.scoreboard import Scoreboard from bastd.actor.flag import ( @@ -19,6 +19,7 @@ from bastd.actor.flag import ( FlagDroppedMessage, FlagDiedMessage, ) +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence @@ -38,7 +39,7 @@ class CTFFlag(Flag): ) self._team = team self.held_count = 0 - self.counter = ba.newnode( + self.counter = bs.newnode( 'text', owner=self.node, attrs={'in_world': True, 'scale': 0.02, 'h_align': 'center'}, @@ -59,24 +60,24 @@ class CTFFlag(Flag): return self._team -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: self.touching_own_flag = 0 -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" def __init__( self, base_pos: Sequence[float], - base_region_material: ba.Material, - base_region: ba.Node, - spaz_material_no_flag_physical: ba.Material, - spaz_material_no_flag_collide: ba.Material, - flagmaterial: ba.Material, + base_region_material: bs.Material, + base_region: bs.Node, + spaz_material_no_flag_physical: bs.Material, + spaz_material_no_flag_collide: bs.Material, + flagmaterial: bs.Material, ): self.base_pos = base_pos self.base_region_material = base_region_material @@ -87,34 +88,34 @@ class Team(ba.Team[Player]): self.score = 0 self.flag_return_touches = 0 self.home_flag_at_base = True - self.touch_return_timer: ba.Timer | None = None + self.touch_return_timer: bs.Timer | None = None self.enemy_flag_at_base = False self.flag: CTFFlag | None = None self.last_flag_leave_time: float | None = None - self.touch_return_timer_ticking: ba.NodeActor | None = None + self.touch_return_timer_ticking: bs.NodeActor | None = None # ba_meta export game -class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): +class CaptureTheFlagGame(bs.TeamGameActivity[Player, Team]): """Game of stealing other team's flag and returning it to your base.""" name = 'Capture the Flag' description = 'Return the enemy flag to score.' available_settings = [ - ba.IntSetting('Score to Win', min_value=1, default=3), - ba.IntSetting( + bs.IntSetting('Score to Win', min_value=1, default=3), + bs.IntSetting( 'Flag Touch Return Time', min_value=0, default=0, increment=1, ), - ba.IntSetting( + bs.IntSetting( 'Flag Idle Return Time', min_value=5, default=30, increment=5, ), - ba.IntChoiceSetting( + bs.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), @@ -126,7 +127,7 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): ], default=0, ), - ba.FloatChoiceSetting( + bs.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), @@ -137,26 +138,27 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): ], default=1.0, ), - ba.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Epic Mode', default=False), ] @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: - return issubclass(sessiontype, ba.DualTeamSession) + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: - return ba.getmaps('team_flag') + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + assert bs.app.classic is not None + return bs.app.classic.getmaps('team_flag') def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() - self._alarmsound = ba.getsound('alarm') - self._ticking_sound = ba.getsound('ticking') - self._score_sound = ba.getsound('score') - self._swipsound = ba.getsound('swip') + self._alarmsound = bs.getsound('alarm') + self._ticking_sound = bs.getsound('ticking') + self._score_sound = bs.getsound('score') + self._swipsound = bs.getsound('swip') self._last_score_time = 0 - self._all_bases_material = ba.Material() + self._all_bases_material = bs.Material() self._last_home_flag_notice_print_time = 0.0 self._score_to_win = int(settings['Score to Win']) self._epic_mode = bool(settings['Epic Mode']) @@ -168,7 +170,7 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): # Base class overrides. self.slow_motion = self._epic_mode self.default_music = ( - ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FLAG_CATCHER + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FLAG_CATCHER ) def get_instance_description(self) -> str | Sequence: @@ -181,14 +183,13 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): return 'return 1 flag' return 'return ${ARG1} flags', self._score_to_win - def create_team(self, sessionteam: ba.SessionTeam) -> Team: - + def create_team(self, sessionteam: bs.SessionTeam) -> Team: # Create our team instance and its initial values. base_pos = self.map.get_flag_position(sessionteam.id) Flag.project_stand(base_pos) - ba.newnode( + bs.newnode( 'light', attrs={ 'position': base_pos, @@ -200,9 +201,9 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): }, ) - base_region_mat = ba.Material() + base_region_mat = bs.Material() pos = base_pos - base_region = ba.newnode( + base_region = bs.newnode( 'region', attrs={ 'position': (pos[0], pos[1] + 0.75, pos[2]), @@ -212,9 +213,9 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): }, ) - spaz_mat_no_flag_physical = ba.Material() - spaz_mat_no_flag_collide = ba.Material() - flagmat = ba.Material() + spaz_mat_no_flag_physical = bs.Material() + spaz_mat_no_flag_collide = bs.Material() + flagmat = bs.Material() team = Team( base_pos=base_pos, @@ -281,19 +282,19 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): super().on_begin() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() - ba.timer(1.0, call=self._tick, repeat=True) + bs.timer(1.0, call=self._tick, repeat=True) def _spawn_flag_for_team(self, team: Team) -> None: team.flag = CTFFlag(team) team.flag_return_touches = 0 self._flash_base(team, length=1.0) assert team.flag.node - ba.playsound(self._swipsound, position=team.flag.node.position) + self._swipsound.play(position=team.flag.node.position) def _handle_flag_entered_base(self, team: Team) -> None: try: - flag = ba.getcollision().opposingnode.getdelegate(CTFFlag, True) - except ba.NotFoundError: + flag = bs.getcollision().opposingnode.getdelegate(CTFFlag, True) + except bs.NotFoundError: # Don't think this should logically ever happen. print('Error getting CTFFlag in entering-base callback.') return @@ -306,7 +307,7 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): # And show team name which scored (but actually we could # show here player who returned enemy flag). self.show_zoom_message( - ba.Lstr( + bs.Lstr( resource='nameScoresText', subs=[('${NAME}', team.name)] ), color=team.color, @@ -318,7 +319,6 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): # Award points to whoever was carrying the enemy flag. player = flag.last_player_to_hold if player and player.team is team: - assert self.stats self.stats.player_scored(player, 50, big_message=True) # Update score and reset flags. @@ -327,12 +327,12 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): # If the home-team flag isn't here, print a message to that effect. else: # Don't want slo-mo affecting this - curtime = ba.time(ba.TimeType.BASE) + curtime = bs.basetime() if curtime - self._last_home_flag_notice_print_time > 5.0: self._last_home_flag_notice_print_time = curtime bpos = team.base_pos - tval = ba.Lstr(resource='ownFlagAtYourBaseWarning') - tnode = ba.newnode( + tval = bs.Lstr(resource='ownFlagAtYourBaseWarning') + tnode = bs.newnode( 'text', attrs={ 'text': tval, @@ -343,8 +343,8 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): 'position': (bpos[0], bpos[1] + 3.2, bpos[2]), }, ) - ba.timer(5.1, tnode.delete) - ba.animate( + bs.timer(5.1, tnode.delete) + bs.animate( tnode, 'scale', {0.0: 0, 0.2: 0.013, 4.8: 0.013, 5.0: 0} ) @@ -362,7 +362,7 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): assert flag.time_out_respawn_time is not None flag.time_out_respawn_time -= 1 if flag.time_out_respawn_time <= 0: - flag.handlemessage(ba.DieMessage()) + flag.handlemessage(bs.DieMessage()) else: time_out_counting_down = False @@ -388,47 +388,46 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): def _score(self, team: Team) -> None: team.score += 1 - ba.playsound(self._score_sound) + self._score_sound.play() self._flash_base(team) self._update_scoreboard() # Have teammates celebrate. for player in team.players: if player.actor: - player.actor.handlemessage(ba.CelebrateMessage(2.0)) + player.actor.handlemessage(bs.CelebrateMessage(2.0)) # Reset all flags/state. for reset_team in self.teams: if not reset_team.home_flag_at_base: assert reset_team.flag is not None - reset_team.flag.handlemessage(ba.DieMessage()) + reset_team.flag.handlemessage(bs.DieMessage()) reset_team.enemy_flag_at_base = False if team.score >= self._score_to_win: self.end_game() def end_game(self) -> None: - results = ba.GameResults() + results = bs.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results=results, announce_delay=0.8) def _handle_flag_left_base(self, team: Team) -> None: - cur_time = ba.time() + cur_time = bs.time() try: - flag = ba.getcollision().opposingnode.getdelegate(CTFFlag, True) - except ba.NotFoundError: + flag = bs.getcollision().opposingnode.getdelegate(CTFFlag, True) + except bs.NotFoundError: # This can happen if the flag stops touching us due to being # deleted; that's ok. return if flag.team is team: - # Check times here to prevent too much flashing. if ( team.last_flag_leave_time is None or cur_time - team.last_flag_leave_time > 3.0 ): - ba.playsound(self._alarmsound, position=team.base_pos) + self._alarmsound.play(position=team.base_pos) self._flash_base(team) team.last_flag_leave_time = cur_time team.home_flag_at_base = False @@ -442,8 +441,8 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): team.touch_return_timer_ticking = None return # No need to return when its at home. if team.touch_return_timer_ticking is None: - team.touch_return_timer_ticking = ba.NodeActor( - ba.newnode( + team.touch_return_timer_ticking = bs.NodeActor( + bs.newnode( 'sound', attrs={ 'sound': self._ticking_sound, @@ -462,7 +461,7 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): if flag.touch_return_time <= 0.0: self._award_players_touching_own_flag(team) - flag.handlemessage(ba.DieMessage()) + flag.handlemessage(bs.DieMessage()) def _award_players_touching_own_flag(self, team: Team) -> None: for player in team.players: @@ -480,8 +479,8 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): """ player: Player | None try: - spaz = ba.getcollision().sourcenode.getdelegate(PlayerSpaz, True) - except ba.NotFoundError: + spaz = bs.getcollision().sourcenode.getdelegate(PlayerSpaz, True) + except bs.NotFoundError: return if not spaz.is_alive(): @@ -502,16 +501,16 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): and team.flag.held_count == 0 ): self._award_players_touching_own_flag(team) - ba.getcollision().opposingnode.handlemessage(ba.DieMessage()) + bs.getcollision().opposingnode.handlemessage(bs.DieMessage()) # Takes a non-zero amount of time to return. else: if connecting: team.flag_return_touches += 1 if team.flag_return_touches == 1: - team.touch_return_timer = ba.Timer( + team.touch_return_timer = bs.Timer( 0.1, - call=ba.Call(self._touch_return_update, team), + call=bs.Call(self._touch_return_update, team), repeat=True, ) team.touch_return_timer_ticking = None @@ -521,10 +520,10 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): team.touch_return_timer = None team.touch_return_timer_ticking = None if team.flag_return_touches < 0: - ba.print_error('CTF flag_return_touches < 0') + logging.exception('CTF flag_return_touches < 0') def _flash_base(self, team: Team, length: float = 2.0) -> None: - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': team.base_pos, @@ -533,8 +532,8 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): 'color': team.color, }, ) - ba.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True) - ba.timer(length, light.delete) + bs.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True) + bs.timer(length, light.delete) def spawn_player_spaz( self, @@ -547,10 +546,10 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): player = spaz.getplayer(Player, True) team: Team = player.team player.touching_own_flag = 0 - no_physical_mats: list[ba.Material] = [ + no_physical_mats: list[bs.Material] = [ team.spaz_material_no_flag_physical ] - no_collide_mats: list[ba.Material] = [ + no_collide_mats: list[bs.Material] = [ team.spaz_material_no_flag_collide ] @@ -581,24 +580,22 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): ) def handlemessage(self, msg: Any) -> Any: - - if isinstance(msg, ba.PlayerDiedMessage): + if isinstance(msg, bs.PlayerDiedMessage): super().handlemessage(msg) # Augment standard behavior. self.respawn_player(msg.getplayer(Player)) elif isinstance(msg, FlagDiedMessage): assert isinstance(msg.flag, CTFFlag) - ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team)) + bs.timer(0.1, bs.Call(self._spawn_flag_for_team, msg.flag.team)) elif isinstance(msg, FlagPickedUpMessage): - # Store the last player to hold the flag for scoring purposes. assert isinstance(msg.flag, CTFFlag) try: msg.flag.last_player_to_hold = msg.node.getdelegate( PlayerSpaz, True ).getplayer(Player, True) - except ba.NotFoundError: + except bs.NotFoundError: pass msg.flag.held_count += 1 diff --git a/assets/src/ba_data/python/bastd/game/chosenone.py b/src/assets/ba_data/python/bastd/game/chosenone.py similarity index 79% rename from assets/src/ba_data/python/bastd/game/chosenone.py rename to src/assets/ba_data/python/bastd/game/chosenone.py index 8604143c..dbe04d24 100644 --- a/assets/src/ba_data/python/bastd/game/chosenone.py +++ b/src/assets/ba_data/python/bastd/game/chosenone.py @@ -2,31 +2,32 @@ # """Provides the chosen-one mini-game.""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations +import logging from typing import TYPE_CHECKING -import ba from bastd.actor.flag import Flag from bastd.actor.playerspaz import PlayerSpaz from bastd.actor.scoreboard import Scoreboard from bastd.gameutils import SharedObjects +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: - self.chosen_light: ba.NodeActor | None = None + self.chosen_light: bs.NodeActor | None = None -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self, time_remaining: int) -> None: @@ -34,7 +35,7 @@ class Team(ba.Team[Player]): # ba_meta export game -class ChosenOneGame(ba.TeamGameActivity[Player, Team]): +class ChosenOneGame(bs.TeamGameActivity[Player, Team]): """ Game involving trying to remain the one 'chosen one' for a set length of time while everyone else tries to @@ -47,15 +48,15 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): 'Kill the chosen one to become it.' ) available_settings = [ - ba.IntSetting( + bs.IntSetting( 'Chosen One Time', min_value=10, default=30, increment=10, ), - ba.BoolSetting('Chosen One Gets Gloves', default=True), - ba.BoolSetting('Chosen One Gets Shield', default=False), - ba.IntChoiceSetting( + bs.BoolSetting('Chosen One Gets Gloves', default=True), + bs.BoolSetting('Chosen One Gets Shield', default=False), + bs.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), @@ -67,7 +68,7 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): ], default=0, ), - ba.FloatChoiceSetting( + bs.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), @@ -78,35 +79,36 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): ], default=1.0, ), - ba.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Epic Mode', default=False), ] - scoreconfig = ba.ScoreConfig(label='Time Held') + scoreconfig = bs.ScoreConfig(label='Time Held') @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: - return ba.getmaps('keep_away') + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + assert bs.app.classic is not None + return bs.app.classic.getmaps('keep_away') def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() self._chosen_one_player: Player | None = None - self._swipsound = ba.getsound('swip') - self._countdownsounds: dict[int, ba.Sound] = { - 10: ba.getsound('announceTen'), - 9: ba.getsound('announceNine'), - 8: ba.getsound('announceEight'), - 7: ba.getsound('announceSeven'), - 6: ba.getsound('announceSix'), - 5: ba.getsound('announceFive'), - 4: ba.getsound('announceFour'), - 3: ba.getsound('announceThree'), - 2: ba.getsound('announceTwo'), - 1: ba.getsound('announceOne'), + self._swipsound = bs.getsound('swip') + self._countdownsounds: dict[int, bs.Sound] = { + 10: bs.getsound('announceTen'), + 9: bs.getsound('announceNine'), + 8: bs.getsound('announceEight'), + 7: bs.getsound('announceSeven'), + 6: bs.getsound('announceSix'), + 5: bs.getsound('announceFive'), + 4: bs.getsound('announceFour'), + 3: bs.getsound('announceThree'), + 2: bs.getsound('announceTwo'), + 1: bs.getsound('announceOne'), } self._flag_spawn_pos: Sequence[float] | None = None - self._reset_region_material: ba.Material | None = None + self._reset_region_material: bs.Material | None = None self._flag: Flag | None = None - self._reset_region: ba.Node | None = None + self._reset_region: bs.Node | None = None self._epic_mode = bool(settings['Epic Mode']) self._chosen_one_time = int(settings['Chosen One Time']) self._time_limit = float(settings['Time Limit']) @@ -116,13 +118,13 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): # Base class overrides self.slow_motion = self._epic_mode self.default_music = ( - ba.MusicType.EPIC if self._epic_mode else ba.MusicType.CHOSEN_ONE + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.CHOSEN_ONE ) def get_instance_description(self) -> str | Sequence: return 'There can be only one.' - def create_team(self, sessionteam: ba.SessionTeam) -> Team: + def create_team(self, sessionteam: bs.SessionTeam) -> Team: return Team(time_remaining=self._chosen_one_time) def on_team_join(self, team: Team) -> None: @@ -140,9 +142,9 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): self.setup_standard_powerup_drops() self._flag_spawn_pos = self.map.get_flag_position(None) Flag.project_stand(self._flag_spawn_pos) - ba.timer(1.0, call=self._tick, repeat=True) + bs.timer(1.0, call=self._tick, repeat=True) - mat = self._reset_region_material = ba.Material() + mat = self._reset_region_material = bs.Material() mat.add_actions( conditions=( 'they_have_material', @@ -151,7 +153,7 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), - ('call', 'at_connect', ba.WeakCall(self._handle_reset_collide)), + ('call', 'at_connect', bs.WeakCall(self._handle_reset_collide)), ), ) @@ -161,7 +163,7 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): assert self._reset_region_material is not None assert self._flag_spawn_pos is not None pos = self._flag_spawn_pos - self._reset_region = ba.newnode( + self._reset_region = bs.newnode( 'region', attrs={ 'position': (pos[0], pos[1] + 0.75, pos[2]), @@ -184,16 +186,16 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): # Attempt to get a Actor that we hit. try: - spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True) + spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True) player = spaz.getplayer(Player, True) - except ba.NotFoundError: + except bs.NotFoundError: return if spaz.is_alive(): self._set_chosen_one_player(player) def _flash_flag_spawn(self) -> None: - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': self._flag_spawn_pos, @@ -202,22 +204,19 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): 'height_attenuated': False, }, ) - ba.animate(light, 'intensity', {0: 0, 0.25: 0.5, 0.5: 0}, loop=True) - ba.timer(1.0, light.delete) + bs.animate(light, 'intensity', {0: 0, 0.25: 0.5, 0.5: 0}, loop=True) + bs.timer(1.0, light.delete) def _tick(self) -> None: - # Give the chosen one points. player = self._get_chosen_one_player() if player is not None: - # This shouldn't happen, but just in case. if not player.is_alive(): - ba.print_error('got dead player as chosen one in _tick') + logging.error('got dead player as chosen one in _tick') self._set_chosen_one_player(None) else: scoring_team = player.team - assert self.stats self.stats.player_scored( player, 3, screenmessage=False, display=False ) @@ -237,9 +236,7 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): # announce numbers we have sounds for if scoring_team.time_remaining in self._countdownsounds: - ba.playsound( - self._countdownsounds[scoring_team.time_remaining] - ) + self._countdownsounds[scoring_team.time_remaining].play() # Winner! if scoring_team.time_remaining <= 0: @@ -251,11 +248,11 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): # (Chosen-one player ceasing to exist should # trigger on_player_leave which resets chosen-one) if self._chosen_one_player is not None: - ba.print_error('got nonexistent player as chosen one in _tick') + logging.error('got nonexistent player as chosen one in _tick') self._set_chosen_one_player(None) def end_game(self) -> None: - results = ba.GameResults() + results = bs.GameResults() for team in self.teams: results.set_team_score( team, self._chosen_one_time - team.time_remaining @@ -266,7 +263,7 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): existing = self._get_chosen_one_player() if existing: existing.chosen_light = None - ba.playsound(self._swipsound) + self._swipsound.play() if not player: assert self._flag_spawn_pos is not None self._flag = Flag( @@ -278,7 +275,7 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): # Create a light to highlight the flag; # this will go away when the flag dies. - ba.newnode( + bs.newnode( 'light', owner=self._flag.node, attrs={ @@ -303,18 +300,18 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): self._chosen_one_player = player if self._chosen_one_gets_shield: - player.actor.handlemessage(ba.PowerupMessage('shield')) + player.actor.handlemessage(bs.PowerupMessage('shield')) if self._chosen_one_gets_gloves: - player.actor.handlemessage(ba.PowerupMessage('punch')) + player.actor.handlemessage(bs.PowerupMessage('punch')) # Use a color that's partway between their team color # and white. color = [ 0.3 + c * 0.7 - for c in ba.normalized_color(player.team.color) + for c in bs.normalized_color(player.team.color) ] - light = player.chosen_light = ba.NodeActor( - ba.newnode( + light = player.chosen_light = bs.NodeActor( + bs.newnode( 'light', attrs={ 'intensity': 0.6, @@ -327,7 +324,7 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): ) assert light.node - ba.animate( + bs.animate( light.node, 'intensity', {0: 1.0, 0.2: 0.4, 0.4: 1.0}, @@ -339,7 +336,7 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]): ) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.PlayerDiedMessage): + if isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) player = msg.getplayer(Player) diff --git a/assets/src/ba_data/python/bastd/game/conquest.py b/src/assets/ba_data/python/bastd/game/conquest.py similarity index 87% rename from assets/src/ba_data/python/bastd/game/conquest.py rename to src/assets/ba_data/python/bastd/game/conquest.py index 13f6f29e..a7c236de 100644 --- a/assets/src/ba_data/python/bastd/game/conquest.py +++ b/src/assets/ba_data/python/bastd/game/conquest.py @@ -2,7 +2,7 @@ # """Provides the Conquest game.""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations @@ -10,14 +10,15 @@ from __future__ import annotations import random from typing import TYPE_CHECKING -import ba from bastd.actor.flag import Flag from bastd.actor.scoreboard import Scoreboard from bastd.actor.playerspaz import PlayerSpaz from bastd.gameutils import SharedObjects +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence + from bastd.actor.respawnicon import RespawnIcon @@ -27,7 +28,7 @@ class ConquestFlag(Flag): def __init__(self, *args: Any, **keywds: Any): super().__init__(*args, **keywds) self._team: Team | None = None - self.light: ba.Node | None = None + self.light: bs.Node | None = None @property def team(self) -> Team | None: @@ -40,18 +41,18 @@ class ConquestFlag(Flag): self._team = team -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" # FIXME: We shouldn't be using customdata here # (but need to update respawn funcs accordingly first). @property - def respawn_timer(self) -> ba.Timer | None: + def respawn_timer(self) -> bs.Timer | None: """Type safe access to standard respawn timer.""" return self.customdata.get('respawn_timer', None) @respawn_timer.setter - def respawn_timer(self, value: ba.Timer | None) -> None: + def respawn_timer(self, value: bs.Timer | None) -> None: self.customdata['respawn_timer'] = value @property @@ -64,7 +65,7 @@ class Player(ba.Player['Team']): self.customdata['respawn_icon'] = value -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self) -> None: @@ -72,13 +73,13 @@ class Team(ba.Team[Player]): # ba_meta export game -class ConquestGame(ba.TeamGameActivity[Player, Team]): +class ConquestGame(bs.TeamGameActivity[Player, Team]): """A game where teams try to claim all flags on the map.""" name = 'Conquest' description = 'Secure all flags on the map to win.' available_settings = [ - ba.IntChoiceSetting( + bs.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), @@ -90,7 +91,7 @@ class ConquestGame(ba.TeamGameActivity[Player, Team]): ], default=0, ), - ba.FloatChoiceSetting( + bs.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), @@ -101,24 +102,25 @@ class ConquestGame(ba.TeamGameActivity[Player, Team]): ], default=1.0, ), - ba.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Epic Mode', default=False), ] @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: - return issubclass(sessiontype, ba.DualTeamSession) + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: - return ba.getmaps('conquest') + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + assert bs.app.classic is not None + return bs.app.classic.getmaps('conquest') def __init__(self, settings: dict): super().__init__(settings) shared = SharedObjects.get() self._scoreboard = Scoreboard() - self._score_sound = ba.getsound('score') - self._swipsound = ba.getsound('swip') - self._extraflagmat = ba.Material() + self._score_sound = bs.getsound('score') + self._swipsound = bs.getsound('swip') + self._extraflagmat = bs.Material() self._flags: list[ConquestFlag] = [] self._epic_mode = bool(settings['Epic Mode']) self._time_limit = float(settings['Time Limit']) @@ -126,7 +128,7 @@ class ConquestGame(ba.TeamGameActivity[Player, Team]): # Base class overrides. self.slow_motion = self._epic_mode self.default_music = ( - ba.MusicType.EPIC if self._epic_mode else ba.MusicType.GRAND_ROMP + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.GRAND_ROMP ) # We want flags to tell us they've been hit but not react physically. @@ -168,7 +170,7 @@ class ConquestGame(ba.TeamGameActivity[Player, Team]): ) self._flags.append(flag) Flag.project_stand(point) - flag.light = ba.newnode( + flag.light = bs.newnode( 'light', owner=flag.node, attrs={ @@ -204,7 +206,6 @@ class ConquestGame(ba.TeamGameActivity[Player, Team]): if flag.team is not None: flag.team.flags_held += 1 for team in self.teams: - # If a team finds themselves with no flags, cancel all # outstanding spawn-timers. if team.flags_held == 0: @@ -218,7 +219,7 @@ class ConquestGame(ba.TeamGameActivity[Player, Team]): ) def end_game(self) -> None: - results = ba.GameResults() + results = bs.GameResults() for team in self.teams: results.set_team_score(team, team.flags_held) self.end(results=results) @@ -226,7 +227,7 @@ class ConquestGame(ba.TeamGameActivity[Player, Team]): def _flash_flag(self, flag: ConquestFlag, length: float = 1.0) -> None: assert flag.node assert flag.light - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': flag.node.position, @@ -234,17 +235,17 @@ class ConquestGame(ba.TeamGameActivity[Player, Team]): 'color': flag.light.color, }, ) - ba.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}, loop=True) - ba.timer(length, light.delete) + bs.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}, loop=True) + bs.timer(length, light.delete) def _handle_flag_player_collide(self) -> None: - collision = ba.getcollision() + collision = bs.getcollision() try: flag = collision.sourcenode.getdelegate(ConquestFlag, True) player = collision.opposingnode.getdelegate( PlayerSpaz, True ).getplayer(Player, True) - except ba.NotFoundError: + except bs.NotFoundError: return assert flag.light @@ -253,7 +254,7 @@ class ConquestGame(ba.TeamGameActivity[Player, Team]): flag.light.color = player.team.color flag.node.color = player.team.color self.stats.player_scored(player, 10, screenmessage=False) - ba.playsound(self._swipsound) + self._swipsound.play() self._flash_flag(flag) self._update_scores() @@ -269,7 +270,7 @@ class ConquestGame(ba.TeamGameActivity[Player, Team]): self.spawn_player(otherplayer) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.PlayerDiedMessage): + if isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) @@ -283,14 +284,13 @@ class ConquestGame(ba.TeamGameActivity[Player, Team]): else: super().handlemessage(msg) - def spawn_player(self, player: Player) -> ba.Actor: + def spawn_player(self, player: Player) -> bs.Actor: # We spawn players at different places based on what flags are held. return self.spawn_player_spaz( player, self._get_player_spawn_position(player) ) def _get_player_spawn_position(self, player: Player) -> Sequence[float]: - # Iterate until we find a spawn owned by this team. spawn_count = len(self.map.spawn_by_flag_points) @@ -306,14 +306,14 @@ class ConquestGame(ba.TeamGameActivity[Player, Team]): # we'll use that one. for spawn in spawns: spt = self.map.spawn_by_flag_points[spawn] - our_pt = ba.Vec3(spt[0], spt[1], spt[2]) + our_pt = bs.Vec3(spt[0], spt[1], spt[2]) for otherspawn in [ i for i in range(spawn_count) if self._flags[i].team is not player.team ]: spt = self.map.spawn_by_flag_points[otherspawn] - their_pt = ba.Vec3(spt[0], spt[1], spt[2]) + their_pt = bs.Vec3(spt[0], spt[1], spt[2]) dist = (their_pt - our_pt).length() if dist < closest_distance: closest_distance = dist diff --git a/assets/src/ba_data/python/bastd/game/deathmatch.py b/src/assets/ba_data/python/bastd/game/deathmatch.py similarity index 82% rename from assets/src/ba_data/python/bastd/game/deathmatch.py rename to src/assets/ba_data/python/bastd/game/deathmatch.py index b82df09b..51b3a37e 100644 --- a/assets/src/ba_data/python/bastd/game/deathmatch.py +++ b/src/assets/ba_data/python/bastd/game/deathmatch.py @@ -2,26 +2,26 @@ # """DeathMatch game and support classes.""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations from typing import TYPE_CHECKING -import ba from bastd.actor.playerspaz import PlayerSpaz from bastd.actor.scoreboard import Scoreboard +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self) -> None: @@ -29,7 +29,7 @@ class Team(ba.Team[Player]): # ba_meta export game -class DeathMatchGame(ba.TeamGameActivity[Player, Team]): +class DeathMatchGame(bs.TeamGameActivity[Player, Team]): """A game type based on acquiring kills.""" name = 'Death Match' @@ -40,16 +40,16 @@ class DeathMatchGame(ba.TeamGameActivity[Player, Team]): @classmethod def get_available_settings( - cls, sessiontype: type[ba.Session] - ) -> list[ba.Setting]: + cls, sessiontype: type[bs.Session] + ) -> list[bs.Setting]: settings = [ - ba.IntSetting( + bs.IntSetting( 'Kills to Win Per Player', min_value=1, default=5, increment=1, ), - ba.IntChoiceSetting( + bs.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), @@ -61,7 +61,7 @@ class DeathMatchGame(ba.TeamGameActivity[Player, Team]): ], default=0, ), - ba.FloatChoiceSetting( + bs.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), @@ -72,7 +72,7 @@ class DeathMatchGame(ba.TeamGameActivity[Player, Team]): ], default=1.0, ), - ba.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Epic Mode', default=False), ] # In teams mode, a suicide gives a point to the other team, but in @@ -80,28 +80,29 @@ class DeathMatchGame(ba.TeamGameActivity[Player, Team]): # this at zero to benefit new players, but pro players might like to # be able to go negative. (to avoid a strategy of just # suiciding until you get a good drop) - if issubclass(sessiontype, ba.FreeForAllSession): + if issubclass(sessiontype, bs.FreeForAllSession): settings.append( - ba.BoolSetting('Allow Negative Scores', default=False) + bs.BoolSetting('Allow Negative Scores', default=False) ) return settings @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: - return issubclass(sessiontype, ba.DualTeamSession) or issubclass( - sessiontype, ba.FreeForAllSession + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) or issubclass( + sessiontype, bs.FreeForAllSession ) @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: - return ba.getmaps('melee') + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + assert bs.app.classic is not None + return bs.app.classic.getmaps('melee') def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() self._score_to_win: int | None = None - self._dingsound = ba.getsound('dingSmall') + self._dingsound = bs.getsound('dingSmall') self._epic_mode = bool(settings['Epic Mode']) self._kills_to_win_per_player = int(settings['Kills to Win Per Player']) self._time_limit = float(settings['Time Limit']) @@ -112,7 +113,7 @@ class DeathMatchGame(ba.TeamGameActivity[Player, Team]): # Base class overrides. self.slow_motion = self._epic_mode self.default_music = ( - ba.MusicType.EPIC if self._epic_mode else ba.MusicType.TO_THE_DEATH + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.TO_THE_DEATH ) def get_instance_description(self) -> str | Sequence: @@ -137,9 +138,7 @@ class DeathMatchGame(ba.TeamGameActivity[Player, Team]): self._update_scoreboard() def handlemessage(self, msg: Any) -> Any: - - if isinstance(msg, ba.PlayerDiedMessage): - + if isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) @@ -152,9 +151,8 @@ class DeathMatchGame(ba.TeamGameActivity[Player, Team]): # Handle team-kills. if killer.team is player.team: - # In free-for-all, killing yourself loses you a point. - if isinstance(self.session, ba.FreeForAllSession): + if isinstance(self.session, bs.FreeForAllSession): new_score = player.team.score - 1 if not self._allow_negative_scores: new_score = max(0, new_score) @@ -162,7 +160,7 @@ class DeathMatchGame(ba.TeamGameActivity[Player, Team]): # In teams-mode it gives a point to the other team. else: - ba.playsound(self._dingsound) + self._dingsound.play() for team in self.teams: if team is not killer.team: team.score += 1 @@ -170,7 +168,7 @@ class DeathMatchGame(ba.TeamGameActivity[Player, Team]): # Killing someone on another team nets a kill. else: killer.team.score += 1 - ba.playsound(self._dingsound) + self._dingsound.play() # In FFA show scores since its hard to find on the scoreboard. if isinstance(killer.actor, PlayerSpaz) and killer.actor: @@ -187,7 +185,7 @@ class DeathMatchGame(ba.TeamGameActivity[Player, Team]): # close enough) assert self._score_to_win is not None if any(team.score >= self._score_to_win for team in self.teams): - ba.timer(0.5, self.end_game) + bs.timer(0.5, self.end_game) else: return super().handlemessage(msg) @@ -200,7 +198,7 @@ class DeathMatchGame(ba.TeamGameActivity[Player, Team]): ) def end_game(self) -> None: - results = ba.GameResults() + results = bs.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results=results) diff --git a/assets/src/ba_data/python/bastd/game/easteregghunt.py b/src/assets/ba_data/python/bastd/game/easteregghunt.py similarity index 80% rename from assets/src/ba_data/python/bastd/game/easteregghunt.py rename to src/assets/ba_data/python/bastd/game/easteregghunt.py index 2bc0454e..f3e37824 100644 --- a/assets/src/ba_data/python/bastd/game/easteregghunt.py +++ b/src/assets/ba_data/python/bastd/game/easteregghunt.py @@ -2,7 +2,7 @@ # """Provides an easter egg hunt game.""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations @@ -10,7 +10,6 @@ from __future__ import annotations import random from typing import TYPE_CHECKING -import ba from bastd.actor.bomb import Bomb from bastd.actor.playerspaz import PlayerSpaz from bastd.actor.spazbot import SpazBotSet, BouncyBot, SpazBotDiedMessage @@ -18,20 +17,21 @@ from bastd.actor.onscreencountdown import OnScreenCountdown from bastd.actor.scoreboard import Scoreboard from bastd.actor.respawnicon import RespawnIcon from bastd.gameutils import SharedObjects +import bascenev1 as bs if TYPE_CHECKING: from typing import Any -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: - self.respawn_timer: ba.Timer | None = None + self.respawn_timer: bs.Timer | None = None self.respawn_icon: RespawnIcon | None = None -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self) -> None: @@ -39,29 +39,29 @@ class Team(ba.Team[Player]): # ba_meta export game -class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): +class EasterEggHuntGame(bs.TeamGameActivity[Player, Team]): """A game where score is based on collecting eggs.""" name = 'Easter Egg Hunt' description = 'Gather eggs!' available_settings = [ - ba.BoolSetting('Pro Mode', default=False), - ba.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Pro Mode', default=False), + bs.BoolSetting('Epic Mode', default=False), ] - scoreconfig = ba.ScoreConfig(label='Score', scoretype=ba.ScoreType.POINTS) + scoreconfig = bs.ScoreConfig(label='Score', scoretype=bs.ScoreType.POINTS) # We're currently hard-coded for one map. @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: return ['Tower D'] # We support teams, free-for-all, and co-op sessions. @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: return ( - issubclass(sessiontype, ba.CoopSession) - or issubclass(sessiontype, ba.DualTeamSession) - or issubclass(sessiontype, ba.FreeForAllSession) + issubclass(sessiontype, bs.CoopSession) + or issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession) ) def __init__(self, settings: dict): @@ -69,28 +69,28 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): shared = SharedObjects.get() self._last_player_death_time = None self._scoreboard = Scoreboard() - self.egg_model = ba.getmodel('egg') - self.egg_tex_1 = ba.gettexture('eggTex1') - self.egg_tex_2 = ba.gettexture('eggTex2') - self.egg_tex_3 = ba.gettexture('eggTex3') - self._collect_sound = ba.getsound('powerup01') + self.egg_mesh = bs.getmesh('egg') + self.egg_tex_1 = bs.gettexture('eggTex1') + self.egg_tex_2 = bs.gettexture('eggTex2') + self.egg_tex_3 = bs.gettexture('eggTex3') + self._collect_sound = bs.getsound('powerup01') self._pro_mode = settings.get('Pro Mode', False) self._epic_mode = settings.get('Epic Mode', False) self._max_eggs = 1.0 - self.egg_material = ba.Material() + self.egg_material = bs.Material() self.egg_material.add_actions( conditions=('they_have_material', shared.player_material), actions=(('call', 'at_connect', self._on_egg_player_collide),), ) self._eggs: list[Egg] = [] - self._update_timer: ba.Timer | None = None + self._update_timer: bs.Timer | None = None self._countdown: OnScreenCountdown | None = None self._bots: SpazBotSet | None = None # Base class overrides self.slow_motion = self._epic_mode self.default_music = ( - ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FORWARD_MARCH + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH ) def on_team_join(self, team: Team) -> None: @@ -108,17 +108,17 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): gamemap.player_wall.delete() super().on_begin() self._update_scoreboard() - self._update_timer = ba.Timer(0.25, self._update, repeat=True) + self._update_timer = bs.Timer(0.25, self._update, repeat=True) self._countdown = OnScreenCountdown(60, endcall=self.end_game) - ba.timer(4.0, self._countdown.start) + bs.timer(4.0, self._countdown.start) self._bots = SpazBotSet() # Spawn evil bunny in co-op only. - if isinstance(self.session, ba.CoopSession) and self._pro_mode: + if isinstance(self.session, bs.CoopSession) and self._pro_mode: self._spawn_evil_bunny() # Overriding the default character spawning. - def spawn_player(self, player: Player) -> ba.Actor: + def spawn_player(self, player: Player) -> bs.Actor: spaz = self.spawn_player_spaz(player) spaz.connect_controls_to_player() return spaz @@ -130,7 +130,7 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): def _on_egg_player_collide(self) -> None: if self.has_ended(): return - collision = ba.getcollision() + collision = bs.getcollision() # Be defensive here; we could be hitting the corpse of a player # who just left/etc. @@ -139,7 +139,7 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): player = collision.opposingnode.getdelegate( PlayerSpaz, True ).getplayer(Player, True) - except ba.NotFoundError: + except bs.NotFoundError: return player.team.score += 1 @@ -154,10 +154,10 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): elif self._max_eggs < 30: self._max_eggs += 0.3 self._update_scoreboard() - ba.playsound(self._collect_sound, 0.5, position=egg.node.position) + self._collect_sound.play(0.5, position=egg.node.position) # Create a flash. - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': egg.node.position, @@ -166,9 +166,9 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): 'color': (1, 1, 0), }, ) - ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.2: 0}, loop=False) - ba.timer(0.200, light.delete) - egg.handlemessage(ba.DieMessage()) + bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.2: 0}, loop=False) + bs.timer(0.200, light.delete) + egg.handlemessage(bs.DieMessage()) def _update(self) -> None: # Misc. periodic updating. @@ -181,7 +181,6 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): # Spawn more eggs if we've got space. if len(self._eggs) < int(self._max_eggs): - # Occasionally spawn a land-mine in addition. if self._pro_mode and random.random() < 0.25: mine = Bomb( @@ -193,9 +192,8 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): # Various high-level game events come through this method. def handlemessage(self, msg: Any) -> Any: - # Respawn dead players. - if isinstance(msg, ba.PlayerDiedMessage): + if isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) @@ -203,8 +201,8 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): player = msg.getplayer(Player) assert self.initialplayerinfos is not None respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 - player.respawn_timer = ba.Timer( - respawn_time, ba.Call(self.spawn_player_if_exists, player) + player.respawn_timer = bs.Timer( + respawn_time, bs.Call(self.spawn_player_if_exists, player) ) player.respawn_icon = RespawnIcon(player, respawn_time) @@ -234,13 +232,13 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): self._scoreboard.set_team_value(team, team.score) def end_game(self) -> None: - results = ba.GameResults() + results = bs.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results) -class Egg(ba.Actor): +class Egg(bs.Actor): """A lovely egg that can be picked up for points.""" def __init__(self, position: tuple[float, float, float] = (0.0, 1.0, 0.0)): @@ -255,15 +253,15 @@ class Egg(ba.Actor): random.randrange(3) ] mats = [shared.object_material, activity.egg_material] - self.node = ba.newnode( + self.node = bs.newnode( 'prop', delegate=self, attrs={ - 'model': activity.egg_model, + 'mesh': activity.egg_mesh, 'color_texture': ctex, 'body': 'capsule', 'reflection': 'soft', - 'model_scale': 0.5, + 'mesh_scale': 0.5, 'body_scale': 0.6, 'density': 4.0, 'reflection_scale': [0.15], @@ -277,10 +275,10 @@ class Egg(ba.Actor): return bool(self.node) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.DieMessage): + if isinstance(msg, bs.DieMessage): if self.node: self.node.delete() - elif isinstance(msg, ba.HitMessage): + elif isinstance(msg, bs.HitMessage): if self.node: assert msg.force_direction is not None self.node.handlemessage( diff --git a/assets/src/ba_data/python/bastd/game/elimination.py b/src/assets/ba_data/python/bastd/game/elimination.py similarity index 86% rename from assets/src/ba_data/python/bastd/game/elimination.py rename to src/assets/ba_data/python/bastd/game/elimination.py index db31fcb5..397abed2 100644 --- a/assets/src/ba_data/python/bastd/game/elimination.py +++ b/src/assets/ba_data/python/bastd/game/elimination.py @@ -2,22 +2,23 @@ # """Elimination mini-game.""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations +import logging from typing import TYPE_CHECKING -import ba from bastd.actor.spazfactory import SpazFactory from bastd.actor.scoreboard import Scoreboard +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence -class Icon(ba.Actor): +class Icon(bs.Actor): """Creates in in-game icon on screen.""" def __init__( @@ -38,10 +39,10 @@ class Icon(ba.Actor): self._show_lives = show_lives self._show_death = show_death self._name_scale = name_scale - self._outline_tex = ba.gettexture('characterIconMask') + self._outline_tex = bs.gettexture('characterIconMask') icon = player.get_icon() - self.node = ba.newnode( + self.node = bs.newnode( 'image', delegate=self, attrs={ @@ -56,12 +57,12 @@ class Icon(ba.Actor): 'attach': 'bottomCenter', }, ) - self._name_text = ba.newnode( + self._name_text = bs.newnode( 'text', owner=self.node, attrs={ - 'text': ba.Lstr(value=player.getname()), - 'color': ba.safecolor(player.team.color), + 'text': bs.Lstr(value=player.getname()), + 'color': bs.safecolor(player.team.color), 'h_align': 'center', 'v_align': 'center', 'vr_depth': 410, @@ -73,7 +74,7 @@ class Icon(ba.Actor): }, ) if self._show_lives: - self._lives_text = ba.newnode( + self._lives_text = bs.newnode( 'text', owner=self.node, attrs={ @@ -134,7 +135,7 @@ class Icon(ba.Actor): if not self.node: return if self._show_death: - ba.animate( + bs.animate( self.node, 'opacity', { @@ -154,16 +155,16 @@ class Icon(ba.Actor): ) lives = self._player.lives if lives == 0: - ba.timer(0.6, self.update_for_lives) + bs.timer(0.6, self.update_for_lives) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.DieMessage): + if isinstance(msg, bs.DieMessage): self.node.delete() return None return super().handlemessage(msg) -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: @@ -171,7 +172,7 @@ class Player(ba.Player['Team']): self.icons: list[Icon] = [] -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self) -> None: @@ -180,13 +181,13 @@ class Team(ba.Team[Player]): # ba_meta export game -class EliminationGame(ba.TeamGameActivity[Player, Team]): +class EliminationGame(bs.TeamGameActivity[Player, Team]): """Game type where last player(s) left alive win.""" name = 'Elimination' description = 'Last remaining alive wins.' - scoreconfig = ba.ScoreConfig( - label='Survived', scoretype=ba.ScoreType.SECONDS, none_is_winner=True + scoreconfig = bs.ScoreConfig( + label='Survived', scoretype=bs.ScoreType.SECONDS, none_is_winner=True ) # Show messages when players die since it's meaningful here. announce_player_deaths = True @@ -195,17 +196,17 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): @classmethod def get_available_settings( - cls, sessiontype: type[ba.Session] - ) -> list[ba.Setting]: + cls, sessiontype: type[bs.Session] + ) -> list[bs.Setting]: settings = [ - ba.IntSetting( + bs.IntSetting( 'Lives Per Player', default=1, min_value=1, max_value=10, increment=1, ), - ba.IntChoiceSetting( + bs.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), @@ -217,7 +218,7 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): ], default=0, ), - ba.FloatChoiceSetting( + bs.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), @@ -228,31 +229,32 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): ], default=1.0, ), - ba.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Epic Mode', default=False), ] - if issubclass(sessiontype, ba.DualTeamSession): - settings.append(ba.BoolSetting('Solo Mode', default=False)) + if issubclass(sessiontype, bs.DualTeamSession): + settings.append(bs.BoolSetting('Solo Mode', default=False)) settings.append( - ba.BoolSetting('Balance Total Lives', default=False) + bs.BoolSetting('Balance Total Lives', default=False) ) return settings @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: - return issubclass(sessiontype, ba.DualTeamSession) or issubclass( - sessiontype, ba.FreeForAllSession + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) or issubclass( + sessiontype, bs.FreeForAllSession ) @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: - return ba.getmaps('melee') + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + assert bs.app.classic is not None + return bs.app.classic.getmaps('melee') def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() self._start_time: float | None = None - self._vs_text: ba.Actor | None = None - self._round_end_timer: ba.Timer | None = None + self._vs_text: bs.Actor | None = None + self._round_end_timer: bs.Timer | None = None self._epic_mode = bool(settings['Epic Mode']) self._lives_per_player = int(settings['Lives Per Player']) self._time_limit = float(settings['Time Limit']) @@ -264,20 +266,20 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): # Base class overrides: self.slow_motion = self._epic_mode self.default_music = ( - ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SURVIVAL + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL ) def get_instance_description(self) -> str | Sequence: return ( 'Last team standing wins.' - if isinstance(self.session, ba.DualTeamSession) + if isinstance(self.session, bs.DualTeamSession) else 'Last one standing wins.' ) def get_instance_description_short(self) -> str | Sequence: return ( 'last team standing wins' - if isinstance(self.session, ba.DualTeamSession) + if isinstance(self.session, bs.DualTeamSession) else 'last one standing wins' ) @@ -299,12 +301,12 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): def on_begin(self) -> None: super().on_begin() - self._start_time = ba.time() + self._start_time = bs.apptime() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() if self._solo_mode: - self._vs_text = ba.NodeActor( - ba.newnode( + self._vs_text = bs.NodeActor( + bs.newnode( 'text', attrs={ 'position': (0, 105), @@ -316,7 +318,7 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): 'scale': 0.6, 'v_attach': 'bottom', 'color': (0.8, 0.8, 0.3, 1.0), - 'text': ba.Lstr(resource='vsText'), + 'text': bs.Lstr(resource='vsText'), }, ) ) @@ -324,7 +326,7 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): # If balance-team-lives is on, add lives to the smaller team until # total lives match. if ( - isinstance(self.session, ba.DualTeamSession) + isinstance(self.session, bs.DualTeamSession) and self._balance_total_lives and self.teams[0].players and self.teams[1].players @@ -348,7 +350,7 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): # We could check game-over conditions at explicit trigger points, # but lets just do the simple thing and poll it. - ba.timer(1.0, self._update, repeat=True) + bs.timer(1.0, self._update, repeat=True) def _update_solo_mode(self) -> None: # For both teams, find the first player on the spawn order list with @@ -367,7 +369,7 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): # pylint: disable=too-many-branches # In free-for-all mode, everyone is just lined up along the bottom. - if isinstance(self.session, ba.FreeForAllSession): + if isinstance(self.session, bs.FreeForAllSession): count = len(self.teams) x_offs = 85 xval = x_offs * (count - 1) * -0.5 @@ -437,7 +439,7 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): icon.update_for_lives() xval += x_offs - def _get_spawn_point(self, player: Player) -> ba.Vec3 | None: + def _get_spawn_point(self, player: Player) -> bs.Vec3 | None: del player # Unused. # In solo-mode, if there's an existing live player on the map, spawn at @@ -455,10 +457,10 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): break if living_player: assert living_player_pos is not None - player_pos = ba.Vec3(living_player_pos) - points: list[tuple[float, ba.Vec3]] = [] + player_pos = bs.Vec3(living_player_pos) + points: list[tuple[float, bs.Vec3]] = [] for team in self.teams: - start_pos = ba.Vec3(self.map.get_start_position(team.id)) + start_pos = bs.Vec3(self.map.get_start_position(team.id)) points.append( ((start_pos - player_pos).length(), start_pos) ) @@ -467,10 +469,10 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): return points[-1][1] return None - def spawn_player(self, player: Player) -> ba.Actor: + def spawn_player(self, player: Player) -> bs.Actor: actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) if not self._solo_mode: - ba.timer(0.3, ba.Call(self._print_lives, player)) + bs.timer(0.3, bs.Call(self._print_lives, player)) # If we have any icons, update their state. for icon in player.icons: @@ -504,30 +506,29 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): # Update icons in a moment since our team will be gone from the # list then. - ba.timer(0, self._update_icons) + bs.timer(0, self._update_icons) # If the player to leave was the last in spawn order and had # their final turn currently in-progress, mark the survival time # for their team. if self._get_total_team_lives(player.team) == 0: assert self._start_time is not None - player.team.survival_seconds = int(ba.time() - self._start_time) + player.team.survival_seconds = int(bs.apptime() - self._start_time) def _get_total_team_lives(self, team: Team) -> int: return sum(player.lives for player in team.players) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.PlayerDiedMessage): - + if isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) player: Player = msg.getplayer(Player) player.lives -= 1 if player.lives < 0: - ba.print_error( - "Got lives < 0 in Elim; this shouldn't happen. solo:" - + str(self._solo_mode) + logging.exception( + "Got lives < 0 in Elim; this shouldn't happen. solo: %s", + self._solo_mode, ) player.lives = 0 @@ -538,7 +539,7 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): # Play big death sound on our last death # or for every one in solo mode. if self._solo_mode or player.lives == 0: - ba.playsound(SpazFactory.get().single_player_death_sound) + SpazFactory.get().single_player_death_sound.play() # If we hit zero lives, we're dead (and our team might be too). if player.lives == 0: @@ -546,7 +547,7 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): if self._get_total_team_lives(player.team) == 0: assert self._start_time is not None player.team.survival_seconds = int( - ba.time() - self._start_time + bs.apptime() - self._start_time ) else: # Otherwise, in regular mode, respawn. @@ -577,7 +578,7 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): # the game (allows the dust to settle and draws to occur if deaths # are close enough). if len(self._get_living_teams()) < 2: - self._round_end_timer = ba.Timer(0.5, self.end_game) + self._round_end_timer = bs.Timer(0.5, self.end_game) def _get_living_teams(self) -> list[Team]: return [ @@ -590,7 +591,7 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]): def end_game(self) -> None: if self.has_ended(): return - results = ba.GameResults() + results = bs.GameResults() self._vs_text = None # Kill our 'vs' if its there. for team in self.teams: results.set_team_score(team, team.survival_seconds) diff --git a/assets/src/ba_data/python/bastd/game/football.py b/src/assets/ba_data/python/bastd/game/football.py similarity index 83% rename from assets/src/ba_data/python/bastd/game/football.py rename to src/assets/ba_data/python/bastd/game/football.py index a8d87771..b12bae25 100644 --- a/assets/src/ba_data/python/bastd/game/football.py +++ b/src/assets/ba_data/python/bastd/game/football.py @@ -2,16 +2,16 @@ # """Implements football games (both co-op and teams varieties).""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations -import random -from typing import TYPE_CHECKING import math +import random +import logging +from typing import TYPE_CHECKING -import ba from bastd.actor.bomb import TNTSpawner from bastd.actor.playerspaz import PlayerSpaz from bastd.actor.scoreboard import Scoreboard @@ -39,9 +39,11 @@ from bastd.actor.spazbot import ( StickyBot, ExplodeyBot, ) +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence + from bastd.actor.spaz import Spaz from bastd.actor.spazbot import SpazBot @@ -54,12 +56,12 @@ class FootballFlag(Flag): position=position, dropped_timeout=20, color=(1.0, 1.0, 0.3) ) assert self.node - self.last_holding_player: ba.Player | None = None + self.last_holding_player: bs.Player | None = None self.node.is_area_of_interest = True - self.respawn_timer: ba.Timer | None = None + self.respawn_timer: bs.Timer | None = None self.scored = False self.held_count = 0 - self.light = ba.newnode( + self.light = bs.newnode( 'light', owner=self.node, attrs={ @@ -72,15 +74,15 @@ class FootballFlag(Flag): self.node.connectattr('position', self.light, 'position') -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: - self.respawn_timer: ba.Timer | None = None + self.respawn_timer: bs.Timer | None = None self.respawn_icon: RespawnIcon | None = None -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self) -> None: @@ -88,19 +90,19 @@ class Team(ba.Team[Player]): # ba_meta export game -class FootballTeamGame(ba.TeamGameActivity[Player, Team]): +class FootballTeamGame(bs.TeamGameActivity[Player, Team]): """Football game for teams mode.""" name = 'Football' description = 'Get the flag to the enemy end zone.' available_settings = [ - ba.IntSetting( + bs.IntSetting( 'Score to Win', min_value=7, default=21, increment=7, ), - ba.IntChoiceSetting( + bs.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), @@ -112,7 +114,7 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): ], default=0, ), - ba.FloatChoiceSetting( + bs.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), @@ -123,29 +125,30 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): ], default=1.0, ), - ba.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Epic Mode', default=False), ] @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: # We only support two-team play. - return issubclass(sessiontype, ba.DualTeamSession) + return issubclass(sessiontype, bs.DualTeamSession) @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: - return ba.getmaps('football') + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + assert bs.app.classic is not None + return bs.app.classic.getmaps('football') def __init__(self, settings: dict): super().__init__(settings) self._scoreboard: Scoreboard | None = Scoreboard() # Load some media we need. - self._cheer_sound = ba.getsound('cheer') - self._chant_sound = ba.getsound('crowdChant') - self._score_sound = ba.getsound('score') - self._swipsound = ba.getsound('swip') - self._whistle_sound = ba.getsound('refWhistle') - self._score_region_material = ba.Material() + self._cheer_sound = bs.getsound('cheer') + self._chant_sound = bs.getsound('crowdChant') + self._score_sound = bs.getsound('score') + self._swipsound = bs.getsound('swip') + self._whistle_sound = bs.getsound('refWhistle') + self._score_region_material = bs.Material() self._score_region_material.add_actions( conditions=('they_have_material', FlagFactory.get().flagmaterial), actions=( @@ -155,16 +158,16 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): ), ) self._flag_spawn_pos: Sequence[float] | None = None - self._score_regions: list[ba.NodeActor] = [] + self._score_regions: list[bs.NodeActor] = [] self._flag: FootballFlag | None = None - self._flag_respawn_timer: ba.Timer | None = None - self._flag_respawn_light: ba.NodeActor | None = None + self._flag_respawn_timer: bs.Timer | None = None + self._flag_respawn_light: bs.NodeActor | None = None self._score_to_win = int(settings['Score to Win']) self._time_limit = float(settings['Time Limit']) self._epic_mode = bool(settings['Epic Mode']) self.slow_motion = self._epic_mode self.default_music = ( - ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FOOTBALL + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FOOTBALL ) def get_instance_description(self) -> str | Sequence: @@ -193,8 +196,8 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): self._spawn_flag() defs = self.map.defs self._score_regions.append( - ba.NodeActor( - ba.newnode( + bs.NodeActor( + bs.newnode( 'region', attrs={ 'position': defs.boxes['goal1'][0:3], @@ -206,8 +209,8 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): ) ) self._score_regions.append( - ba.NodeActor( - ba.newnode( + bs.NodeActor( + bs.newnode( 'region', attrs={ 'position': defs.boxes['goal2'][0:3], @@ -219,7 +222,7 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): ) ) self._update_scoreboard() - ba.playsound(self._chant_sound) + self._chant_sound.play() def on_team_join(self, team: Team) -> None: self._update_scoreboard() @@ -235,7 +238,7 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): assert self._flag is not None if self._flag.scored: return - region = ba.getcollision().sourcenode + region = bs.getcollision().sourcenode i = None for i, score_region in enumerate(self._score_regions): if region == score_region.node: @@ -247,7 +250,7 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): # Tell all players to celebrate. for player in team.players: if player.actor: - player.actor.handlemessage(ba.CelebrateMessage(2.0)) + player.actor.handlemessage(bs.CelebrateMessage(2.0)) # If someone on this team was last to touch it, # give them points. @@ -262,28 +265,28 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): # End the game if we won. if team.score >= self._score_to_win: self.end_game() - ba.playsound(self._score_sound) - ba.playsound(self._cheer_sound) + self._score_sound.play() + self._cheer_sound.play() assert self._flag self._flag.scored = True # Kill the flag (it'll respawn shortly). - ba.timer(1.0, self._kill_flag) - light = ba.newnode( + bs.timer(1.0, self._kill_flag) + light = bs.newnode( 'light', attrs={ - 'position': ba.getcollision().position, + 'position': bs.getcollision().position, 'height_attenuated': False, 'color': (1, 0, 0), }, ) - ba.animate(light, 'intensity', {0.0: 0, 0.5: 1, 1.0: 0}, loop=True) - ba.timer(1.0, light.delete) - ba.cameraflash(duration=10.0) + bs.animate(light, 'intensity', {0.0: 0, 0.5: 1, 1.0: 0}, loop=True) + bs.timer(1.0, light.delete) + bs.cameraflash(duration=10.0) self._update_scoreboard() def end_game(self) -> None: - results = ba.GameResults() + results = bs.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results=results, announce_delay=0.8) @@ -302,7 +305,7 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): msg.flag.last_holding_player = msg.node.getdelegate( PlayerSpaz, True ).getplayer(Player, True) - except ba.NotFoundError: + except bs.NotFoundError: pass msg.flag.held_count += 1 @@ -311,7 +314,7 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): msg.flag.held_count -= 1 # Respawn dead players if they're still in the game. - elif isinstance(msg, ba.PlayerDiedMessage): + elif isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) self.respawn_player(msg.getplayer(Player)) @@ -319,9 +322,9 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): # Respawn dead flags. elif isinstance(msg, FlagDiedMessage): if not self.has_ended(): - self._flag_respawn_timer = ba.Timer(3.0, self._spawn_flag) - self._flag_respawn_light = ba.NodeActor( - ba.newnode( + self._flag_respawn_timer = bs.Timer(3.0, self._spawn_flag) + self._flag_respawn_light = bs.NodeActor( + bs.newnode( 'light', attrs={ 'position': self._flag_spawn_pos, @@ -332,20 +335,20 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): ) ) assert self._flag_respawn_light.node - ba.animate( + bs.animate( self._flag_respawn_light.node, 'intensity', {0.0: 0, 0.25: 0.15, 0.5: 0}, loop=True, ) - ba.timer(3.0, self._flag_respawn_light.node.delete) + bs.timer(3.0, self._flag_respawn_light.node.delete) else: # Augment standard behavior. super().handlemessage(msg) def _flash_flag_spawn(self) -> None: - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': self._flag_spawn_pos, @@ -353,27 +356,27 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): 'color': (1, 1, 0), }, ) - ba.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) - ba.timer(1.0, light.delete) + bs.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) + bs.timer(1.0, light.delete) def _spawn_flag(self) -> None: - ba.playsound(self._swipsound) - ba.playsound(self._whistle_sound) + self._swipsound.play() + self._whistle_sound.play() self._flash_flag_spawn() assert self._flag_spawn_pos is not None self._flag = FootballFlag(position=self._flag_spawn_pos) -class FootballCoopGame(ba.CoopGameActivity[Player, Team]): +class FootballCoopGame(bs.CoopGameActivity[Player, Team]): """Co-op variant of football.""" name = 'Football' tips = ['Use the pick-up button to grab the flag < ${PICKUP} >'] - scoreconfig = ba.ScoreConfig( - scoretype=ba.ScoreType.MILLISECONDS, version='B' + scoreconfig = bs.ScoreConfig( + scoretype=bs.ScoreType.MILLISECONDS, version='B' ) - default_music = ba.MusicType.FOOTBALL + default_music = bs.MusicType.FOOTBALL # FIXME: Need to update co-op games to use getscoreconfig. def get_score_type(self) -> str: @@ -399,14 +402,14 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): self._preset = settings.get('preset', 'rookie') # Load some media we need. - self._cheer_sound = ba.getsound('cheer') - self._boo_sound = ba.getsound('boo') - self._chant_sound = ba.getsound('crowdChant') - self._score_sound = ba.getsound('score') - self._swipsound = ba.getsound('swip') - self._whistle_sound = ba.getsound('refWhistle') + self._cheer_sound = bs.getsound('cheer') + self._boo_sound = bs.getsound('boo') + self._chant_sound = bs.getsound('crowdChant') + self._score_sound = bs.getsound('score') + self._swipsound = bs.getsound('swip') + self._whistle_sound = bs.getsound('refWhistle') self._score_to_win = 21 - self._score_region_material = ba.Material() + self._score_region_material = bs.Material() self._score_region_material.add_actions( conditions=('they_have_material', FlagFactory.get().flagmaterial), actions=( @@ -421,7 +424,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): self._player_has_punched = False self._scoreboard: Scoreboard | None = None self._flag_spawn_pos: Sequence[float] | None = None - self._score_regions: list[ba.NodeActor] = [] + self._score_regions: list[bs.NodeActor] = [] self._exclude_powerups: list[str] = [] self._have_tnt = False self._bot_types_initial: list[type[SpazBot]] | None = None @@ -429,16 +432,16 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): self._bot_types_14: list[type[SpazBot]] | None = None self._bot_team: Team | None = None self._starttime_ms: int | None = None - self._time_text: ba.NodeActor | None = None - self._time_text_input: ba.NodeActor | None = None + self._time_text: bs.NodeActor | None = None + self._time_text_input: bs.NodeActor | None = None self._tntspawner: TNTSpawner | None = None self._bots = SpazBotSet() - self._bot_spawn_timer: ba.Timer | None = None - self._powerup_drop_timer: ba.Timer | None = None + self._bot_spawn_timer: bs.Timer | None = None + self._powerup_drop_timer: bs.Timer | None = None self._scoring_team: Team | None = None self._final_time_ms: int | None = None - self._time_text_timer: ba.Timer | None = None - self._flag_respawn_light: ba.Actor | None = None + self._time_text_timer: bs.Timer | None = None + self._flag_respawn_light: bs.Actor | None = None self._flag: FootballFlag | None = None def on_transition_in(self) -> None: @@ -450,8 +453,8 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): # Set up the two score regions. defs = self.map.defs self._score_regions.append( - ba.NodeActor( - ba.newnode( + bs.NodeActor( + bs.newnode( 'region', attrs={ 'position': defs.boxes['goal1'][0:3], @@ -463,8 +466,8 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): ) ) self._score_regions.append( - ba.NodeActor( - ba.newnode( + bs.NodeActor( + bs.newnode( 'region', attrs={ 'position': defs.boxes['goal2'][0:3], @@ -475,7 +478,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): ) ) ) - ba.playsound(self._chant_sound) + self._chant_sound.play() def on_begin(self) -> None: # FIXME: Split this up a bit. @@ -485,7 +488,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): super().on_begin() # Show controls help in kiosk mode. - if ba.app.demo_mode or ba.app.arcade_mode: + if bs.app.demo_mode or bs.app.arcade_mode: controlsguide.ControlsGuide( delay=3.0, lifespan=10.0, bright=True ).autoretain() @@ -555,12 +558,12 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): 1 if len(self.initialplayerinfos) < 3 else 2 ) else: - raise Exception() + raise RuntimeError() self.setup_low_life_warning_sound() self._drop_powerups(standard_points=True) - ba.timer(4.0, self._start_powerup_drops) + bs.timer(4.0, self._start_powerup_drops) # Make a bogus team for our bots. bad_team_name = self.get_team_display_string('Bad Guys') @@ -575,11 +578,11 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): self.update_scores() # Time display. - starttime_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + starttime_ms = int(bs.time() * 1000.0) assert isinstance(starttime_ms, int) self._starttime_ms = starttime_ms - self._time_text = ba.NodeActor( - ba.newnode( + self._time_text = bs.NodeActor( + bs.newnode( 'text', attrs={ 'v_attach': 'top', @@ -594,8 +597,8 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): }, ) ) - self._time_text_input = ba.NodeActor( - ba.newnode('timedisplay', attrs={'showsubseconds': True}) + self._time_text_input = bs.NodeActor( + bs.newnode('timedisplay', attrs={'showsubseconds': True}) ) self.globalsnode.connectattr( 'time', self._time_text_input.node, 'time2' @@ -611,14 +614,14 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): self._tntspawner = TNTSpawner(position=(0, 1, -1)) self._bots = SpazBotSet() - self._bot_spawn_timer = ba.Timer(1.0, self._update_bots, repeat=True) + self._bot_spawn_timer = bs.Timer(1.0, self._update_bots, repeat=True) for bottype in self._bot_types_initial: self._spawn_bot(bottype) def _on_bot_spawn(self, spaz: SpazBot) -> None: # We want to move to the left by default. - spaz.target_point_default = ba.Vec3(0, 0, 0) + spaz.target_point_default = bs.Vec3(0, 0, 0) def _spawn_bot( self, spaz_type: type[SpazBot], immediate: bool = False @@ -655,7 +658,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): ): return - flagpos = ba.Vec3(self._flag.node.position) + flagpos = bs.Vec3(self._flag.node.position) closest_bot: SpazBot | None = None closest_dist = 0.0 # Always gets assigned first time through. for bot in bots: @@ -663,7 +666,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): if bot.held_count > 0: continue assert bot.node - botpos = ba.Vec3(bot.node.position) + botpos = bs.Vec3(bot.node.position) botdist = (botpos - flagpos).length() if closest_bot is None or botdist < closest_dist: closest_bot = bot @@ -682,7 +685,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): ).autoretain() def _start_powerup_drops(self) -> None: - self._powerup_drop_timer = ba.Timer( + self._powerup_drop_timer = bs.Timer( 3.0, self._drop_powerups, repeat=True ) @@ -693,8 +696,8 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): if standard_points: spawnpoints = self.map.powerup_spawn_points for i, _point in enumerate(spawnpoints): - ba.timer( - 1.0 + i * 0.5, ba.Call(self._drop_powerup, i, poweruptype) + bs.timer( + 1.0 + i * 0.5, bs.Call(self._drop_powerup, i, poweruptype) ) else: point = ( @@ -721,9 +724,9 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): def _kill_flag(self) -> None: try: assert self._flag is not None - self._flag.handlemessage(ba.DieMessage()) + self._flag.handlemessage(bs.DieMessage()) except Exception: - ba.print_exception('Error in _kill_flag.') + logging.exception('Error in _kill_flag.') def _handle_score(self) -> None: """a point has been scored""" @@ -737,7 +740,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): return # See which score region it was. - region = ba.getcollision().sourcenode + region = bs.getcollision().sourcenode i = None for i, score_region in enumerate(self._score_regions): if region == score_region.node: @@ -752,7 +755,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): if i == 0: for player in team.players: if player.actor: - player.actor.handlemessage(ba.CelebrateMessage(2.0)) + player.actor.handlemessage(bs.CelebrateMessage(2.0)) else: self._bots.celebrate(2.0) @@ -767,35 +770,35 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): for bottype in self._bot_types_14: self._spawn_bot(bottype) - ba.playsound(self._score_sound) + self._score_sound.play() if i == 0: - ba.playsound(self._cheer_sound) + self._cheer_sound.play() else: - ba.playsound(self._boo_sound) + self._boo_sound.play() # Kill the flag (it'll respawn shortly). self._flag.scored = True - ba.timer(0.2, self._kill_flag) + bs.timer(0.2, self._kill_flag) self.update_scores() - light = ba.newnode( + light = bs.newnode( 'light', attrs={ - 'position': ba.getcollision().position, + 'position': bs.getcollision().position, 'height_attenuated': False, 'color': (1, 0, 0), }, ) - ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) - ba.timer(1.0, light.delete) + bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + bs.timer(1.0, light.delete) if i == 0: - ba.cameraflash(duration=10.0) + bs.cameraflash(duration=10.0) def end_game(self) -> None: - ba.setmusic(None) + bs.setmusic(None) self._bots.final_celebrate() - ba.timer(0.001, ba.Call(self.do_end, 'defeat')) + bs.timer(0.001, bs.Call(self.do_end, 'defeat')) def on_continue(self) -> None: # Subtract one touchdown from the bots and get them moving again. @@ -820,7 +823,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): if team is self._bot_team: self.continue_or_end_game() else: - ba.setmusic(ba.MusicType.VICTORY) + bs.setmusic(bs.MusicType.VICTORY) # Completion achievements. assert self._bot_team is not None @@ -857,15 +860,14 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): ) self._bots.stop_moving() self.show_zoom_message( - ba.Lstr(resource='victoryText'), + bs.Lstr(resource='victoryText'), scale=1.0, duration=4.0, ) self.celebrate(10.0) assert self._starttime_ms is not None self._final_time_ms = int( - ba.time(timeformat=ba.TimeFormat.MILLISECONDS) - - self._starttime_ms + int(bs.time() * 1000.0) - self._starttime_ms ) self._time_text_timer = None assert ( @@ -875,7 +877,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): self._time_text_input.node.timemax = self._final_time_ms # FIXME: Does this still need to be deferred? - ba.pushcall(ba.Call(self.do_end, 'victory')) + bs.pushcall(bs.Call(self.do_end, 'victory')) def do_end(self, outcome: str) -> None: """End the game with the specified outcome.""" @@ -897,7 +899,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): def handlemessage(self, msg: Any) -> Any: """handle high-level game messages""" - if isinstance(msg, ba.PlayerDiedMessage): + if isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) @@ -905,15 +907,14 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): player = msg.getplayer(Player) assert self.initialplayerinfos is not None respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 - player.respawn_timer = ba.Timer( - respawn_time, ba.Call(self.spawn_player_if_exists, player) + player.respawn_timer = bs.Timer( + respawn_time, bs.Call(self.spawn_player_if_exists, player) ) player.respawn_icon = RespawnIcon(player, respawn_time) elif isinstance(msg, SpazBotDiedMessage): - # Every time a bad guy dies, spawn a new one. - ba.timer(3.0, ba.Call(self._spawn_bot, (type(msg.spazbot)))) + bs.timer(3.0, bs.Call(self._spawn_bot, (type(msg.spazbot)))) elif isinstance(msg, SpazBotPunchedMessage): if self._preset in ['rookie', 'rookie_easy']: @@ -926,9 +927,9 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): # Respawn dead flags. elif isinstance(msg, FlagDiedMessage): assert isinstance(msg.flag, FootballFlag) - msg.flag.respawn_timer = ba.Timer(3.0, self._spawn_flag) - self._flag_respawn_light = ba.NodeActor( - ba.newnode( + msg.flag.respawn_timer = bs.Timer(3.0, self._spawn_flag) + self._flag_respawn_light = bs.NodeActor( + bs.newnode( 'light', attrs={ 'position': self._flag_spawn_pos, @@ -939,18 +940,18 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): ) ) assert self._flag_respawn_light.node - ba.animate( + bs.animate( self._flag_respawn_light.node, 'intensity', {0: 0, 0.25: 0.15, 0.5: 0}, loop=True, ) - ba.timer(3.0, self._flag_respawn_light.node.delete) + bs.timer(3.0, self._flag_respawn_light.node.delete) else: return super().handlemessage(msg) return None - def _handle_player_dropped_bomb(self, player: Spaz, bomb: ba.Actor) -> None: + def _handle_player_dropped_bomb(self, player: Spaz, bomb: bs.Actor) -> None: del player, bomb # Unused. self._player_has_dropped_bomb = True @@ -958,7 +959,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): del player # Unused. self._player_has_punched = True - def spawn_player(self, player: Player) -> ba.Actor: + def spawn_player(self, player: Player) -> bs.Actor: spaz = self.spawn_player_spaz( player, position=self.map.get_start_position(player.team.id) ) @@ -969,7 +970,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): return spaz def _flash_flag_spawn(self) -> None: - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': self._flag_spawn_pos, @@ -977,12 +978,12 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): 'color': (1, 1, 0), }, ) - ba.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) - ba.timer(1.0, light.delete) + bs.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) + bs.timer(1.0, light.delete) def _spawn_flag(self) -> None: - ba.playsound(self._swipsound) - ba.playsound(self._whistle_sound) + self._swipsound.play() + self._whistle_sound.play() self._flash_flag_spawn() assert self._flag_spawn_pos is not None self._flag = FootballFlag(position=self._flag_spawn_pos) diff --git a/assets/src/ba_data/python/bastd/game/hockey.py b/src/assets/ba_data/python/bastd/game/hockey.py similarity index 81% rename from assets/src/ba_data/python/bastd/game/hockey.py rename to src/assets/ba_data/python/bastd/game/hockey.py index d476aaad..d91d4564 100644 --- a/assets/src/ba_data/python/bastd/game/hockey.py +++ b/src/assets/ba_data/python/bastd/game/hockey.py @@ -2,18 +2,18 @@ # """Hockey game and support classes.""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations from typing import TYPE_CHECKING -import ba from bastd.actor.playerspaz import PlayerSpaz from bastd.actor.scoreboard import Scoreboard from bastd.actor.powerupbox import PowerupBoxFactory from bastd.gameutils import SharedObjects +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence @@ -26,7 +26,7 @@ class PuckDiedMessage: self.puck = puck -class Puck(ba.Actor): +class Puck(bs.Actor): """A lovely giant hockey puck.""" def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)): @@ -41,11 +41,11 @@ class Puck(ba.Actor): assert activity is not None assert isinstance(activity, HockeyGame) pmats = [shared.object_material, activity.puck_material] - self.node = ba.newnode( + self.node = bs.newnode( 'prop', delegate=self, attrs={ - 'model': activity.puck_model, + 'mesh': activity.puck_mesh, 'color_texture': activity.puck_tex, 'body': 'puck', 'reflection': 'soft', @@ -56,10 +56,10 @@ class Puck(ba.Actor): 'materials': pmats, }, ) - ba.animate(self.node, 'model_scale', {0: 0, 0.2: 1.3, 0.26: 1}) + bs.animate(self.node, 'mesh_scale', {0: 0, 0.2: 1.3, 0.26: 1}) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.DieMessage): + if isinstance(msg, bs.DieMessage): assert self.node self.node.delete() activity = self._activity() @@ -67,11 +67,11 @@ class Puck(ba.Actor): activity.handlemessage(PuckDiedMessage(self)) # If we go out of bounds, move back to where we started. - elif isinstance(msg, ba.OutOfBoundsMessage): + elif isinstance(msg, bs.OutOfBoundsMessage): assert self.node self.node.position = self._spawn_pos - elif isinstance(msg, ba.HitMessage): + elif isinstance(msg, bs.HitMessage): assert self.node assert msg.force_direction is not None self.node.handlemessage( @@ -102,11 +102,11 @@ class Puck(ba.Actor): super().handlemessage(msg) -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self) -> None: @@ -114,19 +114,19 @@ class Team(ba.Team[Player]): # ba_meta export game -class HockeyGame(ba.TeamGameActivity[Player, Team]): +class HockeyGame(bs.TeamGameActivity[Player, Team]): """Ice hockey game.""" name = 'Hockey' description = 'Score some goals.' available_settings = [ - ba.IntSetting( + bs.IntSetting( 'Score to Win', min_value=1, default=1, increment=1, ), - ba.IntChoiceSetting( + bs.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), @@ -138,7 +138,7 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): ], default=0, ), - ba.FloatChoiceSetting( + bs.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), @@ -149,30 +149,31 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): ], default=1.0, ), - ba.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Epic Mode', default=False), ] @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: - return issubclass(sessiontype, ba.DualTeamSession) + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: - return ba.getmaps('hockey') + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + assert bs.app.classic is not None + return bs.app.classic.getmaps('hockey') def __init__(self, settings: dict): super().__init__(settings) shared = SharedObjects.get() self._scoreboard = Scoreboard() - self._cheer_sound = ba.getsound('cheer') - self._chant_sound = ba.getsound('crowdChant') - self._foghorn_sound = ba.getsound('foghorn') - self._swipsound = ba.getsound('swip') - self._whistle_sound = ba.getsound('refWhistle') - self.puck_model = ba.getmodel('puck') - self.puck_tex = ba.gettexture('puckColor') - self._puck_sound = ba.getsound('metalHit') - self.puck_material = ba.Material() + self._cheer_sound = bs.getsound('cheer') + self._chant_sound = bs.getsound('crowdChant') + self._foghorn_sound = bs.getsound('foghorn') + self._swipsound = bs.getsound('swip') + self._whistle_sound = bs.getsound('refWhistle') + self.puck_mesh = bs.getmesh('puck') + self.puck_tex = bs.gettexture('puckColor') + self._puck_sound = bs.getsound('metalHit') + self.puck_material = bs.Material() self.puck_material.add_actions( actions=('modify_part_collision', 'friction', 0.5) ) @@ -207,10 +208,10 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): ), actions=( ('modify_part_collision', 'physical', False), - ('message', 'their_node', 'at_connect', ba.DieMessage()), + ('message', 'their_node', 'at_connect', bs.DieMessage()), ), ) - self._score_region_material = ba.Material() + self._score_region_material = bs.Material() self._score_region_material.add_actions( conditions=('they_have_material', self.puck_material), actions=( @@ -220,14 +221,14 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): ), ) self._puck_spawn_pos: Sequence[float] | None = None - self._score_regions: list[ba.NodeActor] | None = None + self._score_regions: list[bs.NodeActor] | None = None self._puck: Puck | None = None self._score_to_win = int(settings['Score to Win']) self._time_limit = float(settings['Time Limit']) self._epic_mode = bool(settings['Epic Mode']) self.slow_motion = self._epic_mode self.default_music = ( - ba.MusicType.EPIC if self._epic_mode else ba.MusicType.HOCKEY + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.HOCKEY ) def get_instance_description(self) -> str | Sequence: @@ -252,8 +253,8 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): defs = self.map.defs self._score_regions = [] self._score_regions.append( - ba.NodeActor( - ba.newnode( + bs.NodeActor( + bs.newnode( 'region', attrs={ 'position': defs.boxes['goal1'][0:3], @@ -265,8 +266,8 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): ) ) self._score_regions.append( - ba.NodeActor( - ba.newnode( + bs.NodeActor( + bs.newnode( 'region', attrs={ 'position': defs.boxes['goal2'][0:3], @@ -278,19 +279,19 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): ) ) self._update_scoreboard() - ba.playsound(self._chant_sound) + self._chant_sound.play() def on_team_join(self, team: Team) -> None: self._update_scoreboard() def _handle_puck_player_collide(self) -> None: - collision = ba.getcollision() + collision = bs.getcollision() try: puck = collision.sourcenode.getdelegate(Puck, True) player = collision.opposingnode.getdelegate( PlayerSpaz, True ).getplayer(Player, True) - except ba.NotFoundError: + except bs.NotFoundError: return puck.last_players_to_touch[player.team.id] = player @@ -309,7 +310,7 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): if self._puck.scored: return - region = ba.getcollision().sourcenode + region = bs.getcollision().sourcenode index = 0 for index, score_region in enumerate(self._score_regions): if region == score_region.node: @@ -323,7 +324,7 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): # Tell all players to celebrate. for player in team.players: if player.actor: - player.actor.handlemessage(ba.CelebrateMessage(2.0)) + player.actor.handlemessage(bs.CelebrateMessage(2.0)) # If we've got the player from the scoring team that last # touched us, give them points. @@ -341,30 +342,30 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): if team.score >= self._score_to_win: self.end_game() - ba.playsound(self._foghorn_sound) - ba.playsound(self._cheer_sound) + self._foghorn_sound.play() + self._cheer_sound.play() self._puck.scored = True # Kill the puck (it'll respawn itself shortly). - ba.timer(1.0, self._kill_puck) + bs.timer(1.0, self._kill_puck) - light = ba.newnode( + light = bs.newnode( 'light', attrs={ - 'position': ba.getcollision().position, + 'position': bs.getcollision().position, 'height_attenuated': False, 'color': (1, 0, 0), }, ) - ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) - ba.timer(1.0, light.delete) + bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) + bs.timer(1.0, light.delete) - ba.cameraflash(duration=10.0) + bs.cameraflash(duration=10.0) self._update_scoreboard() def end_game(self) -> None: - results = ba.GameResults() + results = bs.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results=results) @@ -375,9 +376,8 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): self._scoreboard.set_team_value(team, team.score, winscore) def handlemessage(self, msg: Any) -> Any: - # Respawn dead players if they're still in the game. - if isinstance(msg, ba.PlayerDiedMessage): + if isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior... super().handlemessage(msg) self.respawn_player(msg.getplayer(Player)) @@ -385,12 +385,12 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): # Respawn dead pucks. elif isinstance(msg, PuckDiedMessage): if not self.has_ended(): - ba.timer(3.0, self._spawn_puck) + bs.timer(3.0, self._spawn_puck) else: super().handlemessage(msg) def _flash_puck_spawn(self) -> None: - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': self._puck_spawn_pos, @@ -398,12 +398,12 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): 'color': (1, 0, 0), }, ) - ba.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) - ba.timer(1.0, light.delete) + bs.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) + bs.timer(1.0, light.delete) def _spawn_puck(self) -> None: - ba.playsound(self._swipsound) - ba.playsound(self._whistle_sound) + self._swipsound.play() + self._whistle_sound.play() self._flash_puck_spawn() assert self._puck_spawn_pos is not None self._puck = Puck(position=self._puck_spawn_pos) diff --git a/assets/src/ba_data/python/bastd/game/keepaway.py b/src/assets/ba_data/python/bastd/game/keepaway.py similarity index 78% rename from assets/src/ba_data/python/bastd/game/keepaway.py rename to src/assets/ba_data/python/bastd/game/keepaway.py index af7396e0..f8d33308 100644 --- a/assets/src/ba_data/python/bastd/game/keepaway.py +++ b/src/assets/ba_data/python/bastd/game/keepaway.py @@ -2,15 +2,15 @@ # """Defines a keep-away game type.""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations +import logging from enum import Enum from typing import TYPE_CHECKING -import ba from bastd.actor.playerspaz import PlayerSpaz from bastd.actor.scoreboard import Scoreboard from bastd.actor.flag import ( @@ -19,6 +19,7 @@ from bastd.actor.flag import ( FlagDiedMessage, FlagPickedUpMessage, ) +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence @@ -33,11 +34,11 @@ class FlagState(Enum): HELD = 3 -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self, timeremaining: int) -> None: @@ -46,19 +47,19 @@ class Team(ba.Team[Player]): # ba_meta export game -class KeepAwayGame(ba.TeamGameActivity[Player, Team]): +class KeepAwayGame(bs.TeamGameActivity[Player, Team]): """Game where you try to keep the flag away from your enemies.""" name = 'Keep Away' description = 'Carry the flag for a set length of time.' available_settings = [ - ba.IntSetting( + bs.IntSetting( 'Hold Time', min_value=10, default=30, increment=10, ), - ba.IntChoiceSetting( + bs.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), @@ -70,7 +71,7 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): ], default=0, ), - ba.FloatChoiceSetting( + bs.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), @@ -81,42 +82,43 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): ], default=1.0, ), - ba.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Epic Mode', default=False), ] - scoreconfig = ba.ScoreConfig(label='Time Held') + scoreconfig = bs.ScoreConfig(label='Time Held') @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: - return issubclass(sessiontype, ba.DualTeamSession) or issubclass( - sessiontype, ba.FreeForAllSession + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.DualTeamSession) or issubclass( + sessiontype, bs.FreeForAllSession ) @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: - return ba.getmaps('keep_away') + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + assert bs.app.classic is not None + return bs.app.classic.getmaps('keep_away') def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() - self._swipsound = ba.getsound('swip') - self._tick_sound = ba.getsound('tick') + self._swipsound = bs.getsound('swip') + self._tick_sound = bs.getsound('tick') self._countdownsounds = { - 10: ba.getsound('announceTen'), - 9: ba.getsound('announceNine'), - 8: ba.getsound('announceEight'), - 7: ba.getsound('announceSeven'), - 6: ba.getsound('announceSix'), - 5: ba.getsound('announceFive'), - 4: ba.getsound('announceFour'), - 3: ba.getsound('announceThree'), - 2: ba.getsound('announceTwo'), - 1: ba.getsound('announceOne'), + 10: bs.getsound('announceTen'), + 9: bs.getsound('announceNine'), + 8: bs.getsound('announceEight'), + 7: bs.getsound('announceSeven'), + 6: bs.getsound('announceSix'), + 5: bs.getsound('announceFive'), + 4: bs.getsound('announceFour'), + 3: bs.getsound('announceThree'), + 2: bs.getsound('announceTwo'), + 1: bs.getsound('announceOne'), } self._flag_spawn_pos: Sequence[float] | None = None - self._update_timer: ba.Timer | None = None + self._update_timer: bs.Timer | None = None self._holding_players: list[Player] = [] self._flag_state: FlagState | None = None - self._flag_light: ba.Node | None = None + self._flag_light: bs.Node | None = None self._scoring_team: Team | None = None self._flag: Flag | None = None self._hold_time = int(settings['Hold Time']) @@ -124,7 +126,7 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): self._epic_mode = bool(settings['Epic Mode']) self.slow_motion = self._epic_mode self.default_music = ( - ba.MusicType.EPIC if self._epic_mode else ba.MusicType.KEEP_AWAY + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.KEEP_AWAY ) def get_instance_description(self) -> str | Sequence: @@ -133,7 +135,7 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): def get_instance_description_short(self) -> str | Sequence: return 'carry the flag for ${ARG1} seconds', self._hold_time - def create_team(self, sessionteam: ba.SessionTeam) -> Team: + def create_team(self, sessionteam: bs.SessionTeam) -> Team: return Team(timeremaining=self._hold_time) def on_team_join(self, team: Team) -> None: @@ -145,7 +147,7 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): self.setup_standard_powerup_drops() self._flag_spawn_pos = self.map.get_flag_position(None) self._spawn_flag() - self._update_timer = ba.Timer(1.0, call=self._tick, repeat=True) + self._update_timer = bs.Timer(1.0, call=self._tick, repeat=True) self._update_flag_state() Flag.project_stand(self._flag_spawn_pos) @@ -155,7 +157,6 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): # Award points to all living players holding the flag. for player in self._holding_players: if player: - assert self.stats self.stats.player_scored( player, 3, screenmessage=False, display=False ) @@ -163,9 +164,8 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): scoreteam = self._scoring_team if scoreteam is not None: - if scoreteam.timeremaining > 0: - ba.playsound(self._tick_sound) + self._tick_sound.play() scoreteam.timeremaining = max(0, scoreteam.timeremaining - 1) self._update_scoreboard() @@ -175,14 +175,14 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): # Announce numbers we have sounds for. if scoreteam.timeremaining in self._countdownsounds: - ba.playsound(self._countdownsounds[scoreteam.timeremaining]) + self._countdownsounds[scoreteam.timeremaining].play() # Winner. if scoreteam.timeremaining <= 0: self.end_game() def end_game(self) -> None: - results = ba.GameResults() + results = bs.GameResults() for team in self.teams: results.set_team_score(team, self._hold_time - team.timeremaining) self.end(results=results, announce_delay=0) @@ -204,7 +204,7 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): player.actor.node.hold_node.getnodetype() == 'flag' ) except Exception: - ba.print_exception('Error checking hold flag.') + logging.exception('Error checking hold flag.') if holdingflag: self._holding_players.append(player) player.team.holdingflag = True @@ -223,7 +223,7 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): holdingteam = list(holdingteams)[0] self._flag_state = FlagState.HELD self._scoring_team = holdingteam - self._flag_light.color = ba.normalized_color(holdingteam.color) + self._flag_light.color = bs.normalized_color(holdingteam.color) self._flag.node.color = holdingteam.color else: self._flag_state = FlagState.UNCONTESTED @@ -232,15 +232,15 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): self._flag.node.color = (1, 1, 1) if self._flag_state != prevstate: - ba.playsound(self._swipsound) + self._swipsound.play() def _spawn_flag(self) -> None: - ba.playsound(self._swipsound) + self._swipsound.play() self._flash_flag_spawn() assert self._flag_spawn_pos is not None self._flag = Flag(dropped_timeout=20, position=self._flag_spawn_pos) self._flag_state = FlagState.NEW - self._flag_light = ba.newnode( + self._flag_light = bs.newnode( 'light', owner=self._flag.node, attrs={'intensity': 0.2, 'radius': 0.3, 'color': (0.2, 0.2, 0.2)}, @@ -250,7 +250,7 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): self._update_flag_state() def _flash_flag_spawn(self) -> None: - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': self._flag_spawn_pos, @@ -259,8 +259,8 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): 'height_attenuated': False, }, ) - ba.animate(light, 'intensity', {0.0: 0, 0.25: 0.5, 0.5: 0}, loop=True) - ba.timer(1.0, light.delete) + bs.animate(light, 'intensity', {0.0: 0, 0.25: 0.5, 0.5: 0}, loop=True) + bs.timer(1.0, light.delete) def _update_scoreboard(self) -> None: for team in self.teams: @@ -269,7 +269,7 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): ) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.PlayerDiedMessage): + if isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) self.respawn_player(msg.getplayer(Player)) diff --git a/assets/src/ba_data/python/bastd/game/kingofthehill.py b/src/assets/ba_data/python/bastd/game/kingofthehill.py similarity index 79% rename from assets/src/ba_data/python/bastd/game/kingofthehill.py rename to src/assets/ba_data/python/bastd/game/kingofthehill.py index f70d13fe..dee5431d 100644 --- a/assets/src/ba_data/python/bastd/game/kingofthehill.py +++ b/src/assets/ba_data/python/bastd/game/kingofthehill.py @@ -2,7 +2,7 @@ # """Defines the King of the Hill game.""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations @@ -11,11 +11,11 @@ import weakref from enum import Enum from typing import TYPE_CHECKING -import ba from bastd.actor.flag import Flag from bastd.actor.playerspaz import PlayerSpaz from bastd.actor.scoreboard import Scoreboard from bastd.gameutils import SharedObjects +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence @@ -30,14 +30,14 @@ class FlagState(Enum): HELD = 3 -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: self.time_at_flag = 0 -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self, time_remaining: int) -> None: @@ -45,19 +45,19 @@ class Team(ba.Team[Player]): # ba_meta export game -class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): +class KingOfTheHillGame(bs.TeamGameActivity[Player, Team]): """Game where a team wins by holding a 'hill' for a set amount of time.""" name = 'King of the Hill' description = 'Secure the flag for a set length of time.' available_settings = [ - ba.IntSetting( + bs.IntSetting( 'Hold Time', min_value=10, default=30, increment=10, ), - ba.IntChoiceSetting( + bs.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), @@ -69,7 +69,7 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): ], default=0, ), - ba.FloatChoiceSetting( + bs.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), @@ -80,45 +80,46 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): ], default=1.0, ), - ba.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Epic Mode', default=False), ] - scoreconfig = ba.ScoreConfig(label='Time Held') + scoreconfig = bs.ScoreConfig(label='Time Held') @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: - return issubclass(sessiontype, ba.MultiTeamSession) + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.MultiTeamSession) @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: - return ba.getmaps('king_of_the_hill') + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + assert bs.app.classic is not None + return bs.app.classic.getmaps('king_of_the_hill') def __init__(self, settings: dict): super().__init__(settings) shared = SharedObjects.get() self._scoreboard = Scoreboard() - self._swipsound = ba.getsound('swip') - self._tick_sound = ba.getsound('tick') + self._swipsound = bs.getsound('swip') + self._tick_sound = bs.getsound('tick') self._countdownsounds = { - 10: ba.getsound('announceTen'), - 9: ba.getsound('announceNine'), - 8: ba.getsound('announceEight'), - 7: ba.getsound('announceSeven'), - 6: ba.getsound('announceSix'), - 5: ba.getsound('announceFive'), - 4: ba.getsound('announceFour'), - 3: ba.getsound('announceThree'), - 2: ba.getsound('announceTwo'), - 1: ba.getsound('announceOne'), + 10: bs.getsound('announceTen'), + 9: bs.getsound('announceNine'), + 8: bs.getsound('announceEight'), + 7: bs.getsound('announceSeven'), + 6: bs.getsound('announceSix'), + 5: bs.getsound('announceFive'), + 4: bs.getsound('announceFour'), + 3: bs.getsound('announceThree'), + 2: bs.getsound('announceTwo'), + 1: bs.getsound('announceOne'), } self._flag_pos: Sequence[float] | None = None self._flag_state: FlagState | None = None self._flag: Flag | None = None - self._flag_light: ba.Node | None = None + self._flag_light: bs.Node | None = None self._scoring_team: weakref.ref[Team] | None = None self._hold_time = int(settings['Hold Time']) self._time_limit = float(settings['Time Limit']) self._epic_mode = bool(settings['Epic Mode']) - self._flag_region_material = ba.Material() + self._flag_region_material = bs.Material() self._flag_region_material.add_actions( conditions=('they_have_material', shared.player_material), actions=( @@ -127,12 +128,12 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): ( 'call', 'at_connect', - ba.Call(self._handle_player_flag_region_collide, True), + bs.Call(self._handle_player_flag_region_collide, True), ), ( 'call', 'at_disconnect', - ba.Call(self._handle_player_flag_region_collide, False), + bs.Call(self._handle_player_flag_region_collide, False), ), ), ) @@ -140,7 +141,7 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): # Base class overrides. self.slow_motion = self._epic_mode self.default_music = ( - ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SCARY + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SCARY ) def get_instance_description(self) -> str | Sequence: @@ -149,7 +150,7 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): def get_instance_description_short(self) -> str | Sequence: return 'secure the flag for ${ARG1} seconds', self._hold_time - def create_team(self, sessionteam: ba.SessionTeam) -> Team: + def create_team(self, sessionteam: bs.SessionTeam) -> Team: return Team(time_remaining=self._hold_time) def on_begin(self) -> None: @@ -158,13 +159,13 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() self._flag_pos = self.map.get_flag_position(None) - ba.timer(1.0, self._tick, repeat=True) + bs.timer(1.0, self._tick, repeat=True) self._flag_state = FlagState.NEW Flag.project_stand(self._flag_pos) self._flag = Flag( position=self._flag_pos, touchable=False, color=(1, 1, 1) ) - self._flag_light = ba.newnode( + self._flag_light = bs.newnode( 'light', attrs={ 'position': self._flag_pos, @@ -176,7 +177,7 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): ) # Flag region. flagmats = [self._flag_region_material, shared.region_material] - ba.newnode( + bs.newnode( 'region', attrs={ 'position': self._flag_pos, @@ -201,9 +202,8 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): else: scoring_team = self._scoring_team() if scoring_team: - if scoring_team.time_remaining > 0: - ba.playsound(self._tick_sound) + self._tick_sound.play() scoring_team.time_remaining = max( 0, scoring_team.time_remaining - 1 @@ -216,14 +216,14 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): # Announce numbers we have sounds for. numsound = self._countdownsounds.get(scoring_team.time_remaining) if numsound is not None: - ba.playsound(numsound) + numsound.play() # winner if scoring_team.time_remaining <= 0: self.end_game() def end_game(self) -> None: - results = ba.GameResults() + results = bs.GameResults() for team in self.teams: results.set_team_score(team, self._hold_time - team.time_remaining) self.end(results=results, announce_delay=0) @@ -245,7 +245,7 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): holding_team = list(holding_teams)[0] self._flag_state = FlagState.HELD self._scoring_team = weakref.ref(holding_team) - self._flag_light.color = ba.normalized_color(holding_team.color) + self._flag_light.color = bs.normalized_color(holding_team.color) self._flag.node.color = holding_team.color else: self._flag_state = FlagState.UNCONTESTED @@ -253,12 +253,12 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): self._flag_light.color = (0.2, 0.2, 0.2) self._flag.node.color = (1, 1, 1) if self._flag_state != prev_state: - ba.playsound(self._swipsound) + self._swipsound.play() def _handle_player_flag_region_collide(self, colliding: bool) -> None: try: - spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True) - except ba.NotFoundError: + spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True) + except bs.NotFoundError: return if not spaz.is_alive(): @@ -283,7 +283,7 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): ) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.PlayerDiedMessage): + if isinstance(msg, bs.PlayerDiedMessage): super().handlemessage(msg) # Augment default. # No longer can count as time_at_flag once dead. diff --git a/assets/src/ba_data/python/bastd/game/meteorshower.py b/src/assets/ba_data/python/bastd/game/meteorshower.py similarity index 82% rename from assets/src/ba_data/python/bastd/game/meteorshower.py rename to src/assets/ba_data/python/bastd/game/meteorshower.py index 754fa9a1..fc538e25 100644 --- a/assets/src/ba_data/python/bastd/game/meteorshower.py +++ b/src/assets/ba_data/python/bastd/game/meteorshower.py @@ -2,7 +2,7 @@ # """Defines a bomb-dodging mini-game.""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations @@ -10,15 +10,15 @@ from __future__ import annotations import random from typing import TYPE_CHECKING -import ba from bastd.actor.bomb import Bomb from bastd.actor.onscreentimer import OnScreenTimer +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: @@ -26,19 +26,19 @@ class Player(ba.Player['Team']): self.death_time: float | None = None -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" # ba_meta export game -class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): +class MeteorShowerGame(bs.TeamGameActivity[Player, Team]): """Minigame involving dodging falling bombs.""" name = 'Meteor Shower' description = 'Dodge the falling bombs.' - available_settings = [ba.BoolSetting('Epic Mode', default=False)] - scoreconfig = ba.ScoreConfig( - label='Survived', scoretype=ba.ScoreType.MILLISECONDS, version='B' + available_settings = [bs.BoolSetting('Epic Mode', default=False)] + scoreconfig = bs.ScoreConfig( + label='Survived', scoretype=bs.ScoreType.MILLISECONDS, version='B' ) # Print messages when players die (since its meaningful in this game). @@ -50,16 +50,16 @@ class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): # We're currently hard-coded for one map. @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: return ['Rampage'] # We support teams, free-for-all, and co-op sessions. @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: return ( - issubclass(sessiontype, ba.DualTeamSession) - or issubclass(sessiontype, ba.FreeForAllSession) - or issubclass(sessiontype, ba.CoopSession) + issubclass(sessiontype, bs.DualTeamSession) + or issubclass(sessiontype, bs.FreeForAllSession) + or issubclass(sessiontype, bs.CoopSession) ) def __init__(self, settings: dict): @@ -72,7 +72,7 @@ class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): # Some base class overrides: self.default_music = ( - ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SURVIVAL + bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL ) if self._epic_mode: self.slow_motion = True @@ -86,19 +86,19 @@ class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): delay = 5.0 if len(self.players) > 2 else 2.5 if self._epic_mode: delay *= 0.25 - ba.timer(delay, self._decrement_meteor_time, repeat=True) + bs.timer(delay, self._decrement_meteor_time, repeat=True) # Kick off the first wave in a few seconds. delay = 3.0 if self._epic_mode: delay *= 0.25 - ba.timer(delay, self._set_meteor_timer) + bs.timer(delay, self._set_meteor_timer) self._timer = OnScreenTimer() self._timer.start() # Check for immediate end (if we've only got 1 player, etc). - ba.timer(5.0, self._check_end_game) + bs.timer(5.0, self._check_end_game) def on_player_leave(self, player: Player) -> None: # Augment default behavior. @@ -108,7 +108,7 @@ class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): self._check_end_game() # overriding the default character spawning.. - def spawn_player(self, player: Player) -> ba.Actor: + def spawn_player(self, player: Player) -> bs.Actor: spaz = self.spawn_player_spaz(player) # Let's reconnect this player's controls to this @@ -123,12 +123,11 @@ class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): # Various high-level game events come through this method. def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.PlayerDiedMessage): - + if isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) - curtime = ba.time() + curtime = bs.apptime() # Record the player's moment of death. # assert isinstance(msg.spaz.player @@ -138,15 +137,15 @@ class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): # (more accurate looking). # In teams/ffa, allow a one-second fudge-factor so we can # get more draws if players die basically at the same time. - if isinstance(self.session, ba.CoopSession): + if isinstance(self.session, bs.CoopSession): # Teams will still show up if we check now.. check in # the next cycle. - ba.pushcall(self._check_end_game) + bs.pushcall(self._check_end_game) # Also record this for a final setting of the clock. self._last_player_death_time = curtime else: - ba.timer(1.0, self._check_end_game) + bs.timer(1.0, self._check_end_game) else: # Default handler: @@ -163,7 +162,7 @@ class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): # In co-op, we go till everyone is dead.. otherwise we go # until one team remains. - if isinstance(self.session, ba.CoopSession): + if isinstance(self.session, bs.CoopSession): if living_team_count <= 0: self.end_game() else: @@ -171,21 +170,20 @@ class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): self.end_game() def _set_meteor_timer(self) -> None: - ba.timer( + bs.timer( (1.0 + 0.2 * random.random()) * self._meteor_time, self._drop_bomb_cluster, ) def _drop_bomb_cluster(self) -> None: - # Random note: code like this is a handy way to plot out extents # and debug things. loc_test = False if loc_test: - ba.newnode('locator', attrs={'position': (8, 6, -5.5)}) - ba.newnode('locator', attrs={'position': (8, 6, -2.3)}) - ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) - ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)}) + bs.newnode('locator', attrs={'position': (8, 6, -5.5)}) + bs.newnode('locator', attrs={'position': (8, 6, -2.3)}) + bs.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) + bs.newnode('locator', attrs={'position': (-7.3, 6, -2.3)}) # Drop several bombs in series. delay = 0.0 @@ -203,7 +201,7 @@ class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): random.uniform(-3.066, -4.12), 0, ) - ba.timer(delay, ba.Call(self._drop_bomb, pos, vel)) + bs.timer(delay, bs.Call(self._drop_bomb, pos, vel)) delay += 0.1 self._set_meteor_timer() @@ -216,7 +214,7 @@ class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): self._meteor_time = max(0.01, self._meteor_time * 0.9) def end_game(self) -> None: - cur_time = ba.time() + cur_time = bs.apptime() assert self._timer is not None start_time = self._timer.getstarttime() @@ -247,13 +245,12 @@ class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): # Ok now calc game results: set a score for each team and then tell # the game to end. - results = ba.GameResults() + results = bs.GameResults() # Remember that 'free-for-all' mode is simply a special form # of 'teams' mode where each player gets their own team, so we can # just always deal in teams and have all cases covered. for team in self.teams: - # Set the team score to the max time survived by any player on # that team. longest_life = 0.0 diff --git a/assets/src/ba_data/python/bastd/game/ninjafight.py b/src/assets/ba_data/python/bastd/game/ninjafight.py similarity index 82% rename from assets/src/ba_data/python/bastd/game/ninjafight.py rename to src/assets/ba_data/python/bastd/game/ninjafight.py index 343ac1eb..b0dc3f53 100644 --- a/assets/src/ba_data/python/bastd/game/ninjafight.py +++ b/src/assets/ba_data/python/bastd/game/ninjafight.py @@ -2,7 +2,7 @@ # """Provides Ninja Fight mini-game.""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations @@ -10,24 +10,24 @@ from __future__ import annotations import random from typing import TYPE_CHECKING -import ba from bastd.actor.spazbot import SpazBotSet, ChargerBot, SpazBotDiedMessage from bastd.actor.onscreentimer import OnScreenTimer +import bascenev1 as bs if TYPE_CHECKING: from typing import Any -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" # ba_meta export game -class NinjaFightGame(ba.TeamGameActivity[Player, Team]): +class NinjaFightGame(bs.TeamGameActivity[Player, Team]): """ A co-op game where you try to defeat a group of Ninjas as fast as possible @@ -35,28 +35,28 @@ class NinjaFightGame(ba.TeamGameActivity[Player, Team]): name = 'Ninja Fight' description = 'How fast can you defeat the ninjas?' - scoreconfig = ba.ScoreConfig( - label='Time', scoretype=ba.ScoreType.MILLISECONDS, lower_is_better=True + scoreconfig = bs.ScoreConfig( + label='Time', scoretype=bs.ScoreType.MILLISECONDS, lower_is_better=True ) - default_music = ba.MusicType.TO_THE_DEATH + default_music = bs.MusicType.TO_THE_DEATH @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: # For now we're hard-coding spawn positions and whatnot # so we need to be sure to specify that we only support # a specific map. return ['Courtyard'] @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: # We currently support Co-Op only. - return issubclass(sessiontype, ba.CoopSession) + return issubclass(sessiontype, bs.CoopSession) # In the constructor we should load any media we need/etc. # ...but not actually create anything yet. def __init__(self, settings: dict): super().__init__(settings) - self._winsound = ba.getsound('score') + self._winsound = bs.getsound('score') self._won = False self._timer: OnScreenTimer | None = None self._bots = SpazBotSet() @@ -73,28 +73,28 @@ class NinjaFightGame(ba.TeamGameActivity[Player, Team]): # Make our on-screen timer and start it roughly when our bots appear. self._timer = OnScreenTimer() - ba.timer(4.0, self._timer.start) + bs.timer(4.0, self._timer.start) # Spawn some baddies. - ba.timer( + bs.timer( 1.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(3, 3, -2), spawn_time=3.0 ), ) - ba.timer( + bs.timer( 2.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(-3, 3, -2), spawn_time=3.0 ), ) - ba.timer( + bs.timer( 3.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(5, 3, -2), spawn_time=3.0 ), ) - ba.timer( + bs.timer( 4.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(-5, 3, -2), spawn_time=3.0 @@ -104,14 +104,14 @@ class NinjaFightGame(ba.TeamGameActivity[Player, Team]): # Add some extras for multiplayer or pro mode. assert self.initialplayerinfos is not None if len(self.initialplayerinfos) > 2 or is_pro: - ba.timer( + bs.timer( 5.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(0, 3, -5), spawn_time=3.0 ), ) if len(self.initialplayerinfos) > 3 or is_pro: - ba.timer( + bs.timer( 6.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(0, 3, 1), spawn_time=3.0 @@ -119,8 +119,7 @@ class NinjaFightGame(ba.TeamGameActivity[Player, Team]): ) # Called for each spawning player. - def spawn_player(self, player: Player) -> ba.Actor: - + def spawn_player(self, player: Player) -> bs.Actor: # Let's spawn close to the center. spawn_center = (0, 3, -2) pos = ( @@ -142,9 +141,8 @@ class NinjaFightGame(ba.TeamGameActivity[Player, Team]): # Called for miscellaneous messages. def handlemessage(self, msg: Any) -> Any: - # A player has died. - if isinstance(msg, ba.PlayerDiedMessage): + if isinstance(msg, bs.PlayerDiedMessage): super().handlemessage(msg) # Augment standard behavior. self.respawn_player(msg.getplayer(Player)) @@ -154,7 +152,7 @@ class NinjaFightGame(ba.TeamGameActivity[Player, Team]): # bots if we ask here (the currently-dying bot isn't officially # marked dead yet) ..so lets push a call into the event loop to # check once this guy has finished dying. - ba.pushcall(self._check_if_won) + bs.pushcall(self._check_if_won) # Let the base class handle anything we don't. else: @@ -165,25 +163,26 @@ class NinjaFightGame(ba.TeamGameActivity[Player, Team]): # *regardless* of whether is has been won. (this may be called due # to a tournament ending or other external reason). def end_game(self) -> None: - # Stop our on-screen timer so players can see what they got. assert self._timer is not None self._timer.stop() - results = ba.GameResults() + results = bs.GameResults() # If we won, set our score to the elapsed time in milliseconds. # (there should just be 1 team here since this is co-op). # ..if we didn't win, leave scores as default (None) which means # we lost. if self._won: - elapsed_time_ms = int((ba.time() - self._timer.starttime) * 1000.0) - ba.cameraflash() - ba.playsound(self._winsound) + elapsed_time_ms = int( + (bs.apptime() - self._timer.starttime) * 1000.0 + ) + bs.cameraflash() + self._winsound.play() for team in self.teams: for player in team.players: if player.actor: - player.actor.handlemessage(ba.CelebrateMessage()) + player.actor.handlemessage(bs.CelebrateMessage()) results.set_team_score(team, elapsed_time_ms) # Ends the activity. diff --git a/assets/src/ba_data/python/bastd/game/onslaught.py b/src/assets/ba_data/python/bastd/game/onslaught.py similarity index 91% rename from assets/src/ba_data/python/bastd/game/onslaught.py rename to src/assets/ba_data/python/bastd/game/onslaught.py index bf275137..450d12ca 100644 --- a/assets/src/ba_data/python/bastd/game/onslaught.py +++ b/src/assets/ba_data/python/bastd/game/onslaught.py @@ -5,18 +5,18 @@ # Yes this is a long one.. # pylint: disable=too-many-lines -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations import math import random +import logging from enum import Enum, unique from dataclasses import dataclass from typing import TYPE_CHECKING -import ba from bastd.actor.popuptext import PopupText from bastd.actor.bomb import TNTSpawner from bastd.actor.playerspaz import PlayerSpazHurtMessage @@ -45,6 +45,7 @@ from bastd.actor.spazbot import ( BrawlerBotPro, BomberBotProShielded, ) +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence @@ -130,7 +131,7 @@ class Point(Enum): TOP_HALF_LEFT = 'bot_spawn_top_half_left' -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: @@ -138,17 +139,17 @@ class Player(ba.Player['Team']): self.respawn_wave = 0 -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" -class OnslaughtGame(ba.CoopGameActivity[Player, Team]): +class OnslaughtGame(bs.CoopGameActivity[Player, Team]): """Co-op game where players try to survive attacking waves of enemies.""" name = 'Onslaught' description = 'Defeat all enemies.' - tips: list[str | ba.GameTip] = [ + tips: list[str | bs.GameTip] = [ 'Hold any button to run.' ' (Trigger buttons work well if you have them)', 'Try tricking enemies into killing eachother or running off cliffs.', @@ -163,7 +164,6 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): announce_player_deaths = True def __init__(self, settings: dict): - self._preset = Preset(settings.get('preset', 'training')) if self._preset in { Preset.TRAINING, @@ -179,9 +179,9 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): super().__init__(settings) - self._new_wave_sound = ba.getsound('scoreHit01') - self._winsound = ba.getsound('score') - self._cashregistersound = ba.getsound('cashRegister') + self._new_wave_sound = bs.getsound('scoreHit01') + self._winsound = bs.getsound('score') + self._cashregistersound = bs.getsound('cashRegister') self._a_player_has_been_hurt = False self._player_has_dropped_bomb = False @@ -197,34 +197,34 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): self._powerup_center = (0, 5, -1.6) self._powerup_spread = (4.6, 2.7) else: - raise Exception('Unsupported map: ' + str(settings['map'])) + raise RuntimeError('Unsupported map: ' + str(settings['map'])) self._scoreboard: Scoreboard | None = None self._game_over = False self._wavenum = 0 self._can_end_wave = True self._score = 0 self._time_bonus = 0 - self._spawn_info_text: ba.NodeActor | None = None - self._dingsound = ba.getsound('dingSmall') - self._dingsoundhigh = ba.getsound('dingSmallHigh') + self._spawn_info_text: bs.NodeActor | None = None + self._dingsound = bs.getsound('dingSmall') + self._dingsoundhigh = bs.getsound('dingSmallHigh') self._have_tnt = False self._excluded_powerups: list[str] | None = None self._waves: list[Wave] = [] self._tntspawner: TNTSpawner | None = None self._bots: SpazBotSet | None = None - self._powerup_drop_timer: ba.Timer | None = None - self._time_bonus_timer: ba.Timer | None = None - self._time_bonus_text: ba.NodeActor | None = None + self._powerup_drop_timer: bs.Timer | None = None + self._time_bonus_timer: bs.Timer | None = None + self._time_bonus_text: bs.NodeActor | None = None self._flawless_bonus: int | None = None - self._wave_text: ba.NodeActor | None = None - self._wave_update_timer: ba.Timer | None = None + self._wave_text: bs.NodeActor | None = None + self._wave_update_timer: bs.Timer | None = None self._throw_off_kills = 0 self._land_mine_kills = 0 self._tnt_kills = 0 def on_transition_in(self) -> None: super().on_transition_in() - customdata = ba.getsession().customdata + customdata = bs.getsession().customdata # Show special landmine tip on rookie preset. if self._preset in {Preset.ROOKIE, Preset.ROOKIE_EASY}: @@ -232,10 +232,10 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): if not customdata.get('_showed_onslaught_landmine_tip', False): customdata['_showed_onslaught_landmine_tip'] = True self.tips = [ - ba.GameTip( + bs.GameTip( 'Land-mines are a good way to stop speedy enemies.', - icon=ba.gettexture('powerupLandMines'), - sound=ba.getsound('ding'), + icon=bs.gettexture('powerupLandMines'), + sound=bs.getsound('ding'), ) ] @@ -245,11 +245,11 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): if not customdata.get('_showed_onslaught_tnt_tip', False): customdata['_showed_onslaught_tnt_tip'] = True self.tips = [ - ba.GameTip( + bs.GameTip( 'Take out a group of enemies by\n' 'setting off a bomb near a TNT box.', - icon=ba.gettexture('tnt'), - sound=ba.getsound('ding'), + icon=bs.gettexture('tnt'), + sound=bs.getsound('ding'), ) ] @@ -259,16 +259,16 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): if not customdata.get('_showed_onslaught_curse_tip', False): customdata['_showed_onslaught_curse_tip'] = True self.tips = [ - ba.GameTip( + bs.GameTip( 'Curse boxes turn you into a ticking time bomb.\n' 'The only cure is to quickly grab a health-pack.', - icon=ba.gettexture('powerupCurse'), - sound=ba.getsound('ding'), + icon=bs.gettexture('powerupCurse'), + sound=bs.getsound('ding'), ) ] - self._spawn_info_text = ba.NodeActor( - ba.newnode( + self._spawn_info_text = bs.NodeActor( + bs.newnode( 'text', attrs={ 'position': (15, -130), @@ -280,10 +280,10 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): }, ) ) - ba.setmusic(ba.MusicType.ONSLAUGHT) + bs.setmusic(bs.MusicType.ONSLAUGHT) self._scoreboard = Scoreboard( - label=ba.Lstr(resource='scoreText'), score_split=0.5 + label=bs.Lstr(resource='scoreText'), score_split=0.5 ) def on_begin(self) -> None: @@ -550,9 +550,8 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): ] elif self._preset in {Preset.UBER, Preset.UBER_EASY}: - # Show controls help in demo/arcade modes. - if ba.app.demo_mode or ba.app.arcade_mode: + if bs.app.demo_mode or bs.app.arcade_mode: ControlsGuide( delay=3.0, lifespan=10.0, bright=True ).autoretain() @@ -671,7 +670,7 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): else None ), ) - ba.timer(4.0, self._start_powerup_drops) + bs.timer(4.0, self._start_powerup_drops) # Our TNT spawner (if applicable). if self._have_tnt: @@ -680,7 +679,7 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): self.setup_low_life_warning_sound() self._update_scores() self._bots = SpazBotSet() - ba.timer(4.0, self._start_updating_waves) + bs.timer(4.0, self._start_updating_waves) def _get_dist_grp_totals(self, grps: list[Any]) -> tuple[int, int]: totalpts = 0 @@ -825,8 +824,7 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): break entry_count += 1 - def spawn_player(self, player: Player) -> ba.Actor: - + def spawn_player(self, player: Player) -> bs.Actor: # We keep track of who got hurt each wave for score purposes. player.has_been_hurt = False pos = ( @@ -846,7 +844,7 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): return spaz def _handle_player_dropped_bomb( - self, player: ba.Actor, bomb: ba.Actor + self, player: bs.Actor, bomb: bs.Actor ) -> None: del player, bomb # Unused. self._player_has_dropped_bomb = True @@ -861,8 +859,8 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): ).autoretain() def _start_powerup_drops(self) -> None: - self._powerup_drop_timer = ba.Timer( - 3.0, ba.WeakCall(self._drop_powerups), repeat=True + self._powerup_drop_timer = bs.Timer( + 3.0, bs.WeakCall(self._drop_powerups), repeat=True ) def _drop_powerups( @@ -872,9 +870,9 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): if standard_points: points = self.map.powerup_spawn_points for i in range(len(points)): - ba.timer( + bs.timer( 1.0 + i * 0.5, - ba.WeakCall( + bs.WeakCall( self._drop_powerup, i, poweruptype if i == 0 else None ), ) @@ -910,7 +908,7 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): fail_message = None else: score = None - fail_message = ba.Lstr(resource='reachWave2Text') + fail_message = bs.Lstr(resource='reachWave2Text') self.end( { 'outcome': outcome, @@ -938,7 +936,6 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): self._award_achievement('Uber Onslaught Victory', sound=False) def _update_waves(self) -> None: - # If we have no living bots, go to the next wave. assert self._bots is not None if ( @@ -958,10 +955,10 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): # Reward time bonus. if self._time_bonus > 0: - ba.timer(0, lambda: ba.playsound(self._cashregistersound)) - ba.timer( + bs.timer(0, self._cashregistersound.play) + bs.timer( base_delay, - ba.WeakCall(self._award_time_bonus, self._time_bonus), + bs.WeakCall(self._award_time_bonus, self._time_bonus), ) base_delay += 1.0 @@ -971,9 +968,9 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): for player in self.players: if player.is_alive() and not player.has_been_hurt: have_flawless = True - ba.timer( + bs.timer( base_delay, - ba.WeakCall(self._award_flawless_bonus, player), + bs.WeakCall(self._award_flawless_bonus, player), ) player.has_been_hurt = False # reset if have_flawless: @@ -981,21 +978,21 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): if won: self.show_zoom_message( - ba.Lstr(resource='victoryText'), scale=1.0, duration=4.0 + bs.Lstr(resource='victoryText'), scale=1.0, duration=4.0 ) self.celebrate(20.0) self._award_completion_achievements() - ba.timer(base_delay, ba.WeakCall(self._award_completion_bonus)) + bs.timer(base_delay, bs.WeakCall(self._award_completion_bonus)) base_delay += 0.85 - ba.playsound(self._winsound) - ba.cameraflash() - ba.setmusic(ba.MusicType.VICTORY) + self._winsound.play() + bs.cameraflash() + bs.setmusic(bs.MusicType.VICTORY) self._game_over = True # Can't just pass delay to do_end because our extra bonuses # haven't been added yet (once we call do_end the score # gets locked in). - ba.timer(base_delay, ba.WeakCall(self.do_end, 'victory')) + bs.timer(base_delay, bs.WeakCall(self.do_end, 'victory')) return self._wavenum += 1 @@ -1003,10 +1000,10 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): # Short celebration after waves. if self._wavenum > 1: self.celebrate(0.5) - ba.timer(base_delay, ba.WeakCall(self._start_next_wave)) + bs.timer(base_delay, bs.WeakCall(self._start_next_wave)) def _award_completion_bonus(self) -> None: - ba.playsound(self._cashregistersound) + self._cashregistersound.play() for player in self.players: try: if player.is_alive(): @@ -1016,20 +1013,20 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): int(100 / len(self.initialplayerinfos)), scale=1.4, color=(0.6, 0.6, 1.0, 1.0), - title=ba.Lstr(resource='completionBonusText'), + title=bs.Lstr(resource='completionBonusText'), screenmessage=False, ) except Exception: - ba.print_exception() + logging.exception('error in _award_completion_bonus') def _award_time_bonus(self, bonus: int) -> None: - ba.playsound(self._cashregistersound) + self._cashregistersound.play() PopupText( - ba.Lstr( + bs.Lstr( value='+${A} ${B}', subs=[ ('${A}', str(bonus)), - ('${B}', ba.Lstr(resource='timeBonusText')), + ('${B}', bs.Lstr(resource='timeBonusText')), ], ), color=(1, 1, 0.5, 1), @@ -1040,7 +1037,7 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): self._update_scores() def _award_flawless_bonus(self, player: Player) -> None: - ba.playsound(self._cashregistersound) + self._cashregistersound.play() try: if player.is_alive(): assert self._flawless_bonus is not None @@ -1049,39 +1046,38 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): self._flawless_bonus, scale=1.2, color=(0.6, 1.0, 0.6, 1.0), - title=ba.Lstr(resource='flawlessWaveText'), + title=bs.Lstr(resource='flawlessWaveText'), screenmessage=False, ) except Exception: - ba.print_exception() + logging.exception('error in _award_flawless_bonus') def _start_time_bonus_timer(self) -> None: - self._time_bonus_timer = ba.Timer( - 1.0, ba.WeakCall(self._update_time_bonus), repeat=True + self._time_bonus_timer = bs.Timer( + 1.0, bs.WeakCall(self._update_time_bonus), repeat=True ) def _update_player_spawn_info(self) -> None: - # If we have no living players lets just blank this. assert self._spawn_info_text is not None assert self._spawn_info_text.node if not any(player.is_alive() for player in self.teams[0].players): self._spawn_info_text.node.text = '' else: - text: str | ba.Lstr = '' + text: str | bs.Lstr = '' for player in self.players: if not player.is_alive() and ( self._preset in [Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT] or (player.respawn_wave <= len(self._waves)) ): - rtxt = ba.Lstr( + rtxt = bs.Lstr( resource='onslaughtRespawnText', subs=[ ('${PLAYER}', player.getname()), ('${WAVE}', str(player.respawn_wave)), ], ) - text = ba.Lstr( + text = bs.Lstr( value='${A}${B}\n', subs=[ ('${A}', text), @@ -1132,30 +1128,29 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): point = info.point if point is not None: assert bot_type_2 is not None - spcall = ba.WeakCall( + spcall = bs.WeakCall( self.add_bot_at_point, point, bot_type_2, spawn_time ) - ba.timer(tval, spcall) + bs.timer(tval, spcall) tval += dtime else: spacing = info.spacing bot_angle += spacing * 0.5 if bot_type_2 is not None: - tcall = ba.WeakCall( + tcall = bs.WeakCall( self.add_bot_at_angle, bot_angle, bot_type_2, spawn_time ) - ba.timer(tval, tcall) + bs.timer(tval, tcall) tval += dtime bot_angle += spacing * 0.5 # We can end the wave after all the spawning happens. - ba.timer( + bs.timer( tval + spawn_time - dtime + 0.01, - ba.WeakCall(self._set_can_end_wave), + bs.WeakCall(self._set_can_end_wave), ) def _start_next_wave(self) -> None: - # This can happen if we beat a wave as we die. # We don't wanna respawn players and whatnot if this happens. if self._game_over: @@ -1168,15 +1163,14 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): wave = self._waves[self._wavenum - 1] self._setup_wave_spawns(wave) self._update_wave_ui_and_bonuses() - ba.timer(0.4, ba.Call(ba.playsound, self._new_wave_sound)) + bs.timer(0.4, self._new_wave_sound.play) def _update_wave_ui_and_bonuses(self) -> None: - self.show_zoom_message( - ba.Lstr( + bs.Lstr( value='${A} ${B}', subs=[ - ('${A}', ba.Lstr(resource='waveText')), + ('${A}', bs.Lstr(resource='waveText')), ('${B}', str(self._wavenum)), ], ), @@ -1187,15 +1181,15 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): # Reset our time bonus. tbtcolor = (1, 1, 0, 1) - tbttxt = ba.Lstr( + tbttxt = bs.Lstr( value='${A}: ${B}', subs=[ - ('${A}', ba.Lstr(resource='timeBonusText')), + ('${A}', bs.Lstr(resource='timeBonusText')), ('${B}', str(self._time_bonus)), ], ) - self._time_bonus_text = ba.NodeActor( - ba.newnode( + self._time_bonus_text = bs.NodeActor( + bs.newnode( 'text', attrs={ 'v_attach': 'top', @@ -1212,12 +1206,12 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): ) ) - ba.timer(5.0, ba.WeakCall(self._start_time_bonus_timer)) + bs.timer(5.0, bs.WeakCall(self._start_time_bonus_timer)) wtcolor = (1, 1, 1, 1) - wttxt = ba.Lstr( + wttxt = bs.Lstr( value='${A} ${B}', subs=[ - ('${A}', ba.Lstr(resource='waveText')), + ('${A}', bs.Lstr(resource='waveText')), ( '${B}', str(self._wavenum) @@ -1230,8 +1224,8 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): ), ], ) - self._wave_text = ba.NodeActor( - ba.newnode( + self._wave_text = bs.NodeActor( + bs.newnode( 'text', attrs={ 'v_attach': 'top', @@ -1392,10 +1386,10 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): self._time_bonus = int(self._time_bonus * 0.93) if self._time_bonus > 0 and self._time_bonus_text is not None: assert self._time_bonus_text.node - self._time_bonus_text.node.text = ba.Lstr( + self._time_bonus_text.node.text = bs.Lstr( value='${A}: ${B}', subs=[ - ('${A}', ba.Lstr(resource='timeBonusText')), + ('${A}', bs.Lstr(resource='timeBonusText')), ('${B}', str(self._time_bonus)), ], ) @@ -1403,8 +1397,8 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): self._time_bonus_text = None def _start_updating_waves(self) -> None: - self._wave_update_timer = ba.Timer( - 2.0, ba.WeakCall(self._update_waves), repeat=True + self._wave_update_timer = bs.Timer( + 2.0, bs.WeakCall(self._update_waves), repeat=True ) def _update_scores(self) -> None: @@ -1420,16 +1414,15 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): self._scoreboard.set_team_value(self.teams[0], score, max_score=None) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, PlayerSpazHurtMessage): msg.spaz.getplayer(Player, True).has_been_hurt = True self._a_player_has_been_hurt = True - elif isinstance(msg, ba.PlayerScoredMessage): + elif isinstance(msg, bs.PlayerScoredMessage): self._score += msg.score self._update_scores() - elif isinstance(msg, ba.PlayerDiedMessage): + elif isinstance(msg, bs.PlayerDiedMessage): super().handlemessage(msg) # Augment standard behavior. player = msg.getplayer(Player) self._a_player_has_been_hurt = True @@ -1441,8 +1434,8 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): player.respawn_wave = max(2, self._wavenum + 2) else: player.respawn_wave = max(2, self._wavenum + 3) - ba.timer(0.1, self._update_player_spawn_info) - ba.timer(0.1, self._checkroundover) + bs.timer(0.1, self._update_player_spawn_info) + bs.timer(0.1, self._checkroundover) elif isinstance(msg, SpazBotDiedMessage): pts, importance = msg.spazbot.get_death_points(msg.how) @@ -1463,10 +1456,10 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): screenmessage=False, importance=importance, ) - ba.playsound( - self._dingsound if importance == 1 else self._dingsoundhigh, - volume=0.6, + dingsound = ( + self._dingsound if importance == 1 else self._dingsoundhigh ) + dingsound.play(volume=0.6) # Normally we pull scores from the score-set, but if there's # no player lets be explicit. @@ -1487,7 +1480,6 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): self._handle_uber_kill_achievements(msg) def _handle_uber_kill_achievements(self, msg: SpazBotDiedMessage) -> None: - # Uber mine achievement: if msg.spazbot.last_attacked_type == ('explosion', 'land_mine'): self._land_mine_kills += 1 @@ -1498,19 +1490,18 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): if msg.spazbot.last_attacked_type == ('explosion', 'tnt'): self._tnt_kills += 1 if self._tnt_kills >= 6: - ba.timer( - 0.5, ba.WeakCall(self._award_achievement, 'TNT Terror') + bs.timer( + 0.5, bs.WeakCall(self._award_achievement, 'TNT Terror') ) def _handle_pro_kill_achievements(self, msg: SpazBotDiedMessage) -> None: - # TNT achievement: if msg.spazbot.last_attacked_type == ('explosion', 'tnt'): self._tnt_kills += 1 if self._tnt_kills >= 3: - ba.timer( + bs.timer( 0.5, - ba.WeakCall( + bs.WeakCall( self._award_achievement, 'Boom Goes the Dynamite' ), ) @@ -1540,7 +1531,7 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): self._bots.final_celebrate() self._game_over = True self.do_end('defeat', delay=2.0) - ba.setmusic(None) + bs.setmusic(None) def on_continue(self) -> None: for player in self.players: diff --git a/assets/src/ba_data/python/bastd/game/race.py b/src/assets/ba_data/python/bastd/game/race.py similarity index 82% rename from assets/src/ba_data/python/bastd/game/race.py rename to src/assets/ba_data/python/bastd/game/race.py index cbf320c9..a4791398 100644 --- a/assets/src/ba_data/python/bastd/game/race.py +++ b/src/assets/ba_data/python/bastd/game/race.py @@ -2,23 +2,25 @@ # """Defines Race mini-game.""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations import random +import logging from typing import TYPE_CHECKING from dataclasses import dataclass -import ba from bastd.actor.bomb import Bomb from bastd.actor.playerspaz import PlayerSpaz from bastd.actor.scoreboard import Scoreboard from bastd.gameutils import SharedObjects +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence + from bastd.actor.onscreentimer import OnScreenTimer @@ -30,7 +32,7 @@ class RaceMine: mine: Bomb | None -class RaceRegion(ba.Actor): +class RaceRegion(bs.Actor): """Region used to track progress during a race.""" def __init__(self, pt: Sequence[float], index: int): @@ -39,7 +41,7 @@ class RaceRegion(ba.Actor): assert isinstance(activity, RaceGame) self.pos = pt self.index = index - self.node = ba.newnode( + self.node = bs.newnode( 'region', delegate=self, attrs={ @@ -51,11 +53,11 @@ class RaceRegion(ba.Actor): ) -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: - self.distance_txt: ba.Node | None = None + self.distance_txt: bs.Node | None = None self.last_region = 0 self.lap = 0 self.distance = 0.0 @@ -63,7 +65,7 @@ class Player(ba.Player['Team']): self.rank: int | None = None -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self) -> None: @@ -73,22 +75,22 @@ class Team(ba.Team[Player]): # ba_meta export game -class RaceGame(ba.TeamGameActivity[Player, Team]): +class RaceGame(bs.TeamGameActivity[Player, Team]): """Game of racing around a track.""" name = 'Race' description = 'Run real fast!' - scoreconfig = ba.ScoreConfig( - label='Time', lower_is_better=True, scoretype=ba.ScoreType.MILLISECONDS + scoreconfig = bs.ScoreConfig( + label='Time', lower_is_better=True, scoretype=bs.ScoreType.MILLISECONDS ) @classmethod def get_available_settings( - cls, sessiontype: type[ba.Session] - ) -> list[ba.Setting]: + cls, sessiontype: type[bs.Session] + ) -> list[bs.Setting]: settings = [ - ba.IntSetting('Laps', min_value=1, default=3, increment=1), - ba.IntChoiceSetting( + bs.IntSetting('Laps', min_value=1, default=3, increment=1), + bs.IntChoiceSetting( 'Time Limit', default=0, choices=[ @@ -100,7 +102,7 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): ('20 Minutes', 1200), ], ), - ba.IntChoiceSetting( + bs.IntChoiceSetting( 'Mine Spawning', default=4000, choices=[ @@ -110,7 +112,7 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): ('2 Seconds', 2000), ], ), - ba.IntChoiceSetting( + bs.IntChoiceSetting( 'Bomb Spawning', choices=[ ('None', 0), @@ -121,46 +123,47 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): ], default=2000, ), - ba.BoolSetting('Epic Mode', default=False), + bs.BoolSetting('Epic Mode', default=False), ] # We have some specific settings in teams mode. - if issubclass(sessiontype, ba.DualTeamSession): + if issubclass(sessiontype, bs.DualTeamSession): settings.append( - ba.BoolSetting('Entire Team Must Finish', default=False) + bs.BoolSetting('Entire Team Must Finish', default=False) ) return settings @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: - return issubclass(sessiontype, ba.MultiTeamSession) + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: + return issubclass(sessiontype, bs.MultiTeamSession) @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: - return ba.getmaps('race') + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: + assert bs.app.classic is not None + return bs.app.classic.getmaps('race') def __init__(self, settings: dict): self._race_started = False super().__init__(settings) self._scoreboard = Scoreboard() - self._score_sound = ba.getsound('score') - self._swipsound = ba.getsound('swip') + self._score_sound = bs.getsound('score') + self._swipsound = bs.getsound('swip') self._last_team_time: float | None = None self._front_race_region: int | None = None - self._nub_tex = ba.gettexture('nub') - self._beep_1_sound = ba.getsound('raceBeep1') - self._beep_2_sound = ba.getsound('raceBeep2') - self.race_region_material: ba.Material | None = None + self._nub_tex = bs.gettexture('nub') + self._beep_1_sound = bs.getsound('raceBeep1') + self._beep_2_sound = bs.getsound('raceBeep2') + self.race_region_material: bs.Material | None = None self._regions: list[RaceRegion] = [] self._team_finish_pts: int | None = None - self._time_text: ba.Actor | None = None + self._time_text: bs.Actor | None = None self._timer: OnScreenTimer | None = None self._race_mines: list[RaceMine] | None = None - self._race_mine_timer: ba.Timer | None = None - self._scoreboard_timer: ba.Timer | None = None - self._player_order_update_timer: ba.Timer | None = None - self._start_lights: list[ba.Node] | None = None - self._bomb_spawn_timer: ba.Timer | None = None + self._race_mine_timer: bs.Timer | None = None + self._scoreboard_timer: bs.Timer | None = None + self._player_order_update_timer: bs.Timer | None = None + self._start_lights: list[bs.Node] | None = None + self._bomb_spawn_timer: bs.Timer | None = None self._laps = int(settings['Laps']) self._entire_team_must_finish = bool( settings.get('Entire Team Must Finish', False) @@ -173,12 +176,12 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): # Base class overrides. self.slow_motion = self._epic_mode self.default_music = ( - ba.MusicType.EPIC_RACE if self._epic_mode else ba.MusicType.RACE + bs.MusicType.EPIC_RACE if self._epic_mode else bs.MusicType.RACE ) def get_instance_description(self) -> str | Sequence: if ( - isinstance(self.session, ba.DualTeamSession) + isinstance(self.session, bs.DualTeamSession) and self._entire_team_must_finish ): t_str = ' Your entire team has to finish.' @@ -198,7 +201,7 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): super().on_transition_in() shared = SharedObjects.get() pts = self.map.get_def_points('race_point') - mat = self.race_region_material = ba.Material() + mat = self.race_region_material = bs.Material() mat.add_actions( conditions=('they_have_material', shared.player_material), actions=( @@ -214,7 +217,7 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): assert isinstance(player.actor, PlayerSpaz) assert player.actor.node pos = player.actor.node.position - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': pos, @@ -223,19 +226,19 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): 'radius': 0.4, }, ) - ba.timer(0.5, light.delete) - ba.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0}) + bs.timer(0.5, light.delete) + bs.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0}) def _handle_race_point_collide(self) -> None: # FIXME: Tidy this up. # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-nested-blocks - collision = ba.getcollision() + collision = bs.getcollision() try: region = collision.sourcenode.getdelegate(RaceRegion, True) spaz = collision.opposingnode.getdelegate(PlayerSpaz, True) - except ba.NotFoundError: + except bs.NotFoundError: return if not spaz.is_alive(): @@ -243,23 +246,22 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): try: player = spaz.getplayer(Player, True) - except ba.NotFoundError: + except bs.NotFoundError: return last_region = player.last_region this_region = region.index if last_region != this_region: - # If a player tries to skip regions, smite them. # Allow a one region leeway though (its plausible players can get # blown over a region, etc). if this_region > last_region + 2: if player.is_alive(): assert player.actor - player.actor.handlemessage(ba.DieMessage()) - ba.screenmessage( - ba.Lstr( + player.actor.handlemessage(bs.DieMessage()) + bs.screenmessage( + bs.Lstr( translate=( 'statements', 'Killing ${NAME} for' @@ -284,7 +286,7 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): # value is the min of all team players. # Otherwise its the max. if ( - isinstance(self.session, ba.DualTeamSession) + isinstance(self.session, bs.DualTeamSession) and self._entire_team_must_finish ): team.lap = min(p.lap for p in team.players) @@ -293,10 +295,9 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): # A player is finishing. if player.lap == self._laps: - # In teams mode, hand out points based on the order # players come in. - if isinstance(self.session, ba.DualTeamSession): + if isinstance(self.session, bs.DualTeamSession): assert self._team_finish_pts is not None if self._team_finish_pts > 0: self.stats.player_scored( @@ -311,7 +312,7 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): player.finished = True assert player.actor player.actor.handlemessage( - ba.DieMessage(immediate=True) + bs.DieMessage(immediate=True) ) # Makes sure noone behind them passes them in rank @@ -320,26 +321,26 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): # If the whole team has finished the race. if team.lap == self._laps: - ba.playsound(self._score_sound) + self._score_sound.play() player.team.finished = True assert self._timer is not None - elapsed = ba.time() - self._timer.getstarttime() + elapsed = bs.time() - self._timer.getstarttime() self._last_team_time = player.team.time = elapsed self._check_end_game() # Team has yet to finish. else: - ba.playsound(self._swipsound) + self._swipsound.play() # They've just finished a lap but not the race. else: - ba.playsound(self._swipsound) + self._swipsound.play() self._flash_player(player, 0.3) # Print their lap number over their head. try: assert isinstance(player.actor, PlayerSpaz) - mathnode = ba.newnode( + mathnode = bs.newnode( 'math', owner=player.actor.node, attrs={ @@ -350,14 +351,14 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): player.actor.node.connectattr( 'torso_position', mathnode, 'input2' ) - tstr = ba.Lstr( + tstr = bs.Lstr( resource='lapNumberText', subs=[ ('${CURRENT}', str(player.lap + 1)), ('${TOTAL}', str(self._laps)), ], ) - txtnode = ba.newnode( + txtnode = bs.newnode( 'text', owner=mathnode, attrs={ @@ -369,14 +370,14 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): }, ) mathnode.connectattr('output', txtnode, 'position') - ba.animate( + bs.animate( txtnode, 'scale', {0.0: 0, 0.2: 0.019, 2.0: 0.019, 2.2: 0}, ) - ba.timer(2.3, mathnode.delete) + bs.timer(2.3, mathnode.delete) except Exception: - ba.print_exception('Error printing lap.') + logging.exception('Error printing lap.') def on_team_join(self, team: Team) -> None: self._update_scoreboard() @@ -388,11 +389,11 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): # is on (otherwise in teams mode everyone could just leave except the # leading player to win). if ( - isinstance(self.session, ba.DualTeamSession) + isinstance(self.session, bs.DualTeamSession) and self._entire_team_must_finish ): - ba.screenmessage( - ba.Lstr( + bs.screenmessage( + bs.Lstr( translate=( 'statements', '${TEAM} is disqualified because ${PLAYER} left', @@ -407,18 +408,18 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): player.team.finished = True player.team.time = None player.team.lap = 0 - ba.playsound(ba.getsound('boo')) + bs.getsound('boo').play() for otherplayer in player.team.players: otherplayer.lap = 0 otherplayer.finished = True try: if otherplayer.actor is not None: - otherplayer.actor.handlemessage(ba.DieMessage()) + otherplayer.actor.handlemessage(bs.DieMessage()) except Exception: - ba.print_exception('Error sending DieMessage.') + logging.exception('Error sending DieMessage.') # Defer so team/player lists will be updated. - ba.pushcall(self._check_end_game) + bs.pushcall(self._check_end_game) def _update_scoreboard(self) -> None: for team in self.teams: @@ -427,7 +428,7 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): teams_dist = 0.0 else: if ( - isinstance(self.session, ba.DualTeamSession) + isinstance(self.session, bs.DualTeamSession) and self._entire_team_must_finish ): teams_dist = min(distances) @@ -450,8 +451,8 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): self._team_finish_pts = 100 # Throw a timer up on-screen. - self._time_text = ba.NodeActor( - ba.newnode( + self._time_text = bs.NodeActor( + bs.newnode( 'text', attrs={ 'v_attach': 'top', @@ -474,16 +475,16 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): for p in self.map.get_def_points('race_mine') ] if self._race_mines: - self._race_mine_timer = ba.Timer( + self._race_mine_timer = bs.Timer( 0.001 * self._mine_spawning, self._update_race_mine, repeat=True, ) - self._scoreboard_timer = ba.Timer( + self._scoreboard_timer = bs.Timer( 0.25, self._update_scoreboard, repeat=True ) - self._player_order_update_timer = ba.Timer( + self._player_order_update_timer = bs.Timer( 0.25, self._update_player_order, repeat=True ) @@ -496,17 +497,17 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): lstart = 7.1 * t_scale inc = 1.25 * t_scale - ba.timer(lstart, self._do_light_1) - ba.timer(lstart + inc, self._do_light_2) - ba.timer(lstart + 2 * inc, self._do_light_3) - ba.timer(lstart + 3 * inc, self._start_race) + bs.timer(lstart, self._do_light_1) + bs.timer(lstart + inc, self._do_light_2) + bs.timer(lstart + 2 * inc, self._do_light_3) + bs.timer(lstart + 3 * inc, self._start_race) self._start_lights = [] for i in range(4): - lnub = ba.newnode( + lnub = bs.newnode( 'image', attrs={ - 'texture': ba.gettexture('nub'), + 'texture': bs.gettexture('nub'), 'opacity': 1.0, 'absolute_scale': True, 'position': (-75 + i * 50, light_y), @@ -514,7 +515,7 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): 'attach': 'center', }, ) - ba.animate( + bs.animate( lnub, 'opacity', { @@ -524,7 +525,7 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): 12.5 * t_scale: 0.0, }, ) - ba.timer(13.0 * t_scale, lnub.delete) + bs.timer(13.0 * t_scale, lnub.delete) self._start_lights.append(lnub) self._start_lights[0].color = (0.2, 0, 0) @@ -535,58 +536,57 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): def _do_light_1(self) -> None: assert self._start_lights is not None self._start_lights[0].color = (1.0, 0, 0) - ba.playsound(self._beep_1_sound) + self._beep_1_sound.play() def _do_light_2(self) -> None: assert self._start_lights is not None self._start_lights[1].color = (1.0, 0, 0) - ba.playsound(self._beep_1_sound) + self._beep_1_sound.play() def _do_light_3(self) -> None: assert self._start_lights is not None self._start_lights[2].color = (1.0, 0.3, 0) - ba.playsound(self._beep_1_sound) + self._beep_1_sound.play() def _start_race(self) -> None: assert self._start_lights is not None self._start_lights[3].color = (0.0, 1.0, 0) - ba.playsound(self._beep_2_sound) + self._beep_2_sound.play() for player in self.players: if player.actor is not None: try: assert isinstance(player.actor, PlayerSpaz) player.actor.connect_controls_to_player() except Exception: - ba.print_exception('Error in race player connects.') + logging.exception('Error in race player connects.') assert self._timer is not None self._timer.start() if self._bomb_spawning != 0: - self._bomb_spawn_timer = ba.Timer( + self._bomb_spawn_timer = bs.Timer( 0.001 * self._bomb_spawning, self._spawn_bomb, repeat=True ) self._race_started = True def _update_player_order(self) -> None: - # Calc all player distances. for player in self.players: - pos: ba.Vec3 | None + pos: bs.Vec3 | None try: pos = player.position - except ba.NotFoundError: + except bs.NotFoundError: pos = None if pos is not None: r_index = player.last_region rg1 = self._regions[r_index] - r1pt = ba.Vec3(rg1.pos[:3]) + r1pt = bs.Vec3(rg1.pos[:3]) rg2 = ( self._regions[0] if r_index == len(self._regions) - 1 else self._regions[r_index + 1] ) - r2pt = ba.Vec3(rg2.pos[:3]) + r2pt = bs.Vec3(rg2.pos[:3]) r2dist = (pos - r2pt).length() amt = 1.0 - (r2dist / (r2pt - r1pt).length()) amt = player.lap + (r_index + amt) * (1.0 / len(self._regions)) @@ -626,8 +626,8 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): pos[1] + 1.0, pos[2] + random.uniform(*z_range), ) - ba.timer( - random.uniform(0.0, 2.0), ba.WeakCall(self._spawn_bomb_at_pos, pos) + bs.timer( + random.uniform(0.0, 2.0), bs.WeakCall(self._spawn_bomb_at_pos, pos) ) def _spawn_bomb_at_pos(self, pos: Sequence[float]) -> None: @@ -644,7 +644,7 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): def _flash_mine(self, i: int) -> None: assert self._race_mines is not None rmine = self._race_mines[i] - light = ba.newnode( + light = bs.newnode( 'light', attrs={ 'position': rmine.point[:3], @@ -653,8 +653,8 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): 'height_attenuated': False, }, ) - ba.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True) - ba.timer(1.0, light.delete) + bs.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True) + bs.timer(1.0, light.delete) def _update_race_mine(self) -> None: assert self._race_mines is not None @@ -668,9 +668,9 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): assert rmine is not None if not rmine.mine: self._flash_mine(m_index) - ba.timer(0.95, ba.Call(self._make_mine, m_index)) + bs.timer(0.95, bs.Call(self._make_mine, m_index)) - def spawn_player(self, player: Player) -> ba.Actor: + def spawn_player(self, player: Player) -> bs.Actor: if player.team.finished: # FIXME: This is not type-safe! # This call is expected to always return an Actor! @@ -705,14 +705,14 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): if not self._race_started: spaz.disconnect_controls_from_player() - mathnode = ba.newnode( + mathnode = bs.newnode( 'math', owner=spaz.node, attrs={'input1': (0, 1.4, 0), 'operation': 'add'}, ) spaz.node.connectattr('torso_position', mathnode, 'input2') - distance_txt = ba.newnode( + distance_txt = bs.newnode( 'text', owner=spaz.node, attrs={ @@ -728,7 +728,6 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): return spaz def _check_end_game(self) -> None: - # If there's no teams left racing, finish. teams_still_in = len([t for t in self.teams if not t.finished]) if teams_still_in == 0: @@ -746,21 +745,20 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): # In teams mode its over as soon as any team finishes the race # FIXME: The get_ffa_point_awards code looks dangerous. - if isinstance(session, ba.DualTeamSession): + if isinstance(session, bs.DualTeamSession): self.end_game() else: # In ffa we keep the race going while there's still any points # to be handed out. Find out how many points we have to award # and how many teams have finished, and once that matches # we're done. - assert isinstance(session, ba.FreeForAllSession) + assert isinstance(session, bs.FreeForAllSession) points_to_award = len(session.get_ffa_point_awards()) if teams_completed >= points_to_award - teams_completed: self.end_game() return def end_game(self) -> None: - # Stop updating our time text, and set it to show the exact last # finish time if we have one. (so users don't get upset if their # final time differs from what they see onscreen by a tiny amount) @@ -772,7 +770,7 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): else (self._timer.getstarttime() + self._last_team_time) ) - results = ba.GameResults() + results = bs.GameResults() for team in self.teams: if team.time is not None: @@ -786,11 +784,11 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): # odd to be announcing that now. self.end( results=results, - announce_winning_team=isinstance(self.session, ba.DualTeamSession), + announce_winning_team=isinstance(self.session, bs.DualTeamSession), ) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.PlayerDiedMessage): + if isinstance(msg, bs.PlayerDiedMessage): # Augment default behavior. super().handlemessage(msg) player = msg.getplayer(Player) diff --git a/assets/src/ba_data/python/bastd/game/runaround.py b/src/assets/ba_data/python/bastd/game/runaround.py similarity index 87% rename from assets/src/ba_data/python/bastd/game/runaround.py rename to src/assets/ba_data/python/bastd/game/runaround.py index 379ae8e3..c0738d37 100644 --- a/assets/src/ba_data/python/bastd/game/runaround.py +++ b/src/assets/ba_data/python/bastd/game/runaround.py @@ -5,17 +5,17 @@ # We wear the cone of shame. # pylint: disable=too-many-lines -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations import random -from dataclasses import dataclass +import logging from enum import Enum +from dataclasses import dataclass from typing import TYPE_CHECKING -import ba from bastd.actor.popuptext import PopupText from bastd.actor.bomb import TNTSpawner from bastd.actor.scoreboard import Scoreboard @@ -40,6 +40,7 @@ from bastd.actor.spazbot import ( BomberBotPro, BrawlerBotPro, ) +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence @@ -90,19 +91,19 @@ class Wave: entries: list[Spawn | Spacing | None] -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: - self.respawn_timer: ba.Timer | None = None + self.respawn_timer: bs.Timer | None = None self.respawn_icon: RespawnIcon | None = None -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" -class RunaroundGame(ba.CoopGameActivity[Player, Team]): +class RunaroundGame(bs.CoopGameActivity[Player, Team]): """Game involving trying to bomb bots as they walk through the map.""" name = 'Runaround' @@ -112,7 +113,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): 'No, you can\'t get up on the ledge. You have to throw bombs.', 'Whip back and forth to get more distance on your throws..', ] - default_music = ba.MusicType.MARCHING + default_music = bs.MusicType.MARCHING # How fast our various bot types walk. _bot_speed_map: dict[type[SpazBot], float] = { @@ -137,14 +138,14 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): shared = SharedObjects.get() self._preset = Preset(settings.get('preset', 'pro')) - self._player_death_sound = ba.getsound('playerDeath') - self._new_wave_sound = ba.getsound('scoreHit01') - self._winsound = ba.getsound('score') - self._cashregistersound = ba.getsound('cashRegister') - self._bad_guy_score_sound = ba.getsound('shieldDown') - self._heart_tex = ba.gettexture('heart') - self._heart_model_opaque = ba.getmodel('heartOpaque') - self._heart_model_transparent = ba.getmodel('heartTransparent') + self._player_death_sound = bs.getsound('playerDeath') + self._new_wave_sound = bs.getsound('scoreHit01') + self._winsound = bs.getsound('score') + self._cashregistersound = bs.getsound('cashRegister') + self._bad_guy_score_sound = bs.getsound('shieldDown') + self._heart_tex = bs.gettexture('heart') + self._heart_mesh_opaque = bs.getmesh('heartOpaque') + self._heart_mesh_transparent = bs.getmesh('heartTransparent') self._a_player_has_been_killed = False self._spawn_center = self._map_type.defs.points['spawn1'][0:3] @@ -155,7 +156,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): self._map_type.defs.boxes['powerup_region'][8] * 0.5, ) - self._score_region_material = ba.Material() + self._score_region_material = bs.Material() self._score_region_material.add_actions( conditions=('they_have_material', shared.player_material), actions=( @@ -165,7 +166,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): ), ) - self._last_wave_end_time = ba.time() + self._last_wave_end_time = bs.time() self._player_has_picked_up_powerup = False self._scoreboard: Scoreboard | None = None self._game_over = False @@ -173,33 +174,33 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): self._can_end_wave = True self._score = 0 self._time_bonus = 0 - self._score_region: ba.Actor | None = None - self._dingsound = ba.getsound('dingSmall') - self._dingsoundhigh = ba.getsound('dingSmallHigh') + self._score_region: bs.Actor | None = None + self._dingsound = bs.getsound('dingSmall') + self._dingsoundhigh = bs.getsound('dingSmallHigh') self._exclude_powerups: list[str] | None = None self._have_tnt: bool | None = None self._waves: list[Wave] | None = None self._bots = SpazBotSet() self._tntspawner: TNTSpawner | None = None - self._lives_bg: ba.NodeActor | None = None + self._lives_bg: bs.NodeActor | None = None self._start_lives = 10 self._lives = self._start_lives - self._lives_text: ba.NodeActor | None = None + self._lives_text: bs.NodeActor | None = None self._flawless = True - self._time_bonus_timer: ba.Timer | None = None - self._time_bonus_text: ba.NodeActor | None = None + self._time_bonus_timer: bs.Timer | None = None + self._time_bonus_text: bs.NodeActor | None = None self._time_bonus_mult: float | None = None - self._wave_text: ba.NodeActor | None = None + self._wave_text: bs.NodeActor | None = None self._flawless_bonus: int | None = None - self._wave_update_timer: ba.Timer | None = None + self._wave_update_timer: bs.Timer | None = None def on_transition_in(self) -> None: super().on_transition_in() self._scoreboard = Scoreboard( - label=ba.Lstr(resource='scoreText'), score_split=0.5 + label=bs.Lstr(resource='scoreText'), score_split=0.5 ) - self._score_region = ba.NodeActor( - ba.newnode( + self._score_region = bs.NodeActor( + bs.newnode( 'region', attrs={ 'position': self.map.defs.boxes['score_region'][0:3], @@ -442,7 +443,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): # Spit out a few powerups and start dropping more shortly. self._drop_powerups(standard_points=True) - ba.timer(4.0, self._start_powerup_drops) + bs.timer(4.0, self._start_powerup_drops) self.setup_low_life_warning_sound() self._update_scores() @@ -451,22 +452,23 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): self._tntspawner = TNTSpawner(position=self._tntspawnpos) # Make sure to stay out of the way of menu/party buttons in the corner. - uiscale = ba.app.ui.uiscale + assert bs.app.classic is not None + uiscale = bs.app.classic.ui.uiscale l_offs = ( -80 - if uiscale is ba.UIScale.SMALL + if uiscale is bs.UIScale.SMALL else -40 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bs.UIScale.MEDIUM else 0 ) - self._lives_bg = ba.NodeActor( - ba.newnode( + self._lives_bg = bs.NodeActor( + bs.newnode( 'image', attrs={ 'texture': self._heart_tex, - 'model_opaque': self._heart_model_opaque, - 'model_transparent': self._heart_model_transparent, + 'mesh_opaque': self._heart_mesh_opaque, + 'mesh_transparent': self._heart_mesh_transparent, 'attach': 'topRight', 'scale': (90, 90), 'position': (-110 + l_offs, -50), @@ -476,9 +478,9 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): ) # FIXME; should not set things based on vr mode. # (won't look right to non-vr connected clients, etc) - vrmode = ba.app.vr_mode - self._lives_text = ba.NodeActor( - ba.newnode( + vrmode = bs.app.vr_mode + self._lives_text = bs.NodeActor( + bs.newnode( 'text', attrs={ 'v_attach': 'top', @@ -495,23 +497,23 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): ) ) - ba.timer(2.0, self._start_updating_waves) + bs.timer(2.0, self._start_updating_waves) def _handle_reached_end(self) -> None: - spaz = ba.getcollision().opposingnode.getdelegate(SpazBot, True) + spaz = bs.getcollision().opposingnode.getdelegate(SpazBot, True) if not spaz.is_alive(): return # Ignore bodies flying in. self._flawless = False pos = spaz.node.position - ba.playsound(self._bad_guy_score_sound, position=pos) - light = ba.newnode( + self._bad_guy_score_sound.play(position=pos) + light = bs.newnode( 'light', attrs={'position': pos, 'radius': 0.5, 'color': (1, 0, 0)} ) - ba.animate(light, 'intensity', {0.0: 0, 0.1: 1, 0.5: 0}, loop=False) - ba.timer(1.0, light.delete) + bs.animate(light, 'intensity', {0.0: 0, 0.1: 1, 0.5: 0}, loop=False) + bs.timer(1.0, light.delete) spaz.handlemessage( - ba.DieMessage(immediate=True, how=ba.DeathType.REACHED_GOAL) + bs.DieMessage(immediate=True, how=bs.DeathType.REACHED_GOAL) ) if self._lives > 0: @@ -524,14 +526,14 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): self._lives_text.node.text = str(self._lives) delay = 0.0 - def _safesetattr(node: ba.Node, attr: str, value: Any) -> None: + def _safesetattr(node: bs.Node, attr: str, value: Any) -> None: if node: setattr(node, attr, value) for _i in range(4): - ba.timer( + bs.timer( delay, - ba.Call( + bs.Call( _safesetattr, self._lives_text.node, 'color', @@ -540,28 +542,28 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): ) assert self._lives_bg is not None assert self._lives_bg.node - ba.timer( + bs.timer( delay, - ba.Call(_safesetattr, self._lives_bg.node, 'opacity', 0.5), + bs.Call(_safesetattr, self._lives_bg.node, 'opacity', 0.5), ) delay += 0.125 - ba.timer( + bs.timer( delay, - ba.Call( + bs.Call( _safesetattr, self._lives_text.node, 'color', (1.0, 1.0, 0.0, 1.0), ), ) - ba.timer( + bs.timer( delay, - ba.Call(_safesetattr, self._lives_bg.node, 'opacity', 1.0), + bs.Call(_safesetattr, self._lives_bg.node, 'opacity', 1.0), ) delay += 0.125 - ba.timer( + bs.timer( delay, - ba.Call( + bs.Call( _safesetattr, self._lives_text.node, 'color', @@ -576,7 +578,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): self._lives_text.node.text = str(self._lives) self._bots.start_moving() - def spawn_player(self, player: Player) -> ba.Actor: + def spawn_player(self, player: Player) -> bs.Actor: pos = ( self._spawn_center[0] + random.uniform(-1.5, 1.5), self._spawn_center[1], @@ -590,7 +592,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): spaz.pick_up_powerup_callback = self._on_player_picked_up_powerup return spaz - def _on_player_picked_up_powerup(self, player: ba.Actor) -> None: + def _on_player_picked_up_powerup(self, player: bs.Actor) -> None: del player # Unused. self._player_has_picked_up_powerup = True @@ -605,7 +607,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): ).autoretain() def _start_powerup_drops(self) -> None: - ba.timer(3.0, self._drop_powerups, repeat=True) + bs.timer(3.0, self._drop_powerups, repeat=True) def _drop_powerups( self, standard_points: bool = False, force_first: str | None = None @@ -615,7 +617,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): # If its been a minute since our last wave finished emerging, stop # giving out land-mine powerups. (prevents players from waiting # around for them on purpose and filling the map up) - if ba.time() - self._last_wave_end_time > 60.0: + if bs.time() - self._last_wave_end_time > 60.0: extra_excludes = ['land_mines'] else: extra_excludes = [] @@ -623,9 +625,9 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): if standard_points: points = self.map.powerup_spawn_points for i in range(len(points)): - ba.timer( + bs.timer( 1.0 + i * 0.5, - ba.Call( + bs.Call( self._drop_powerup, i, force_first if i == 0 else None ), ) @@ -653,9 +655,9 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): ).autoretain() def end_game(self) -> None: - ba.pushcall(ba.Call(self.do_end, 'defeat')) - ba.setmusic(None) - ba.playsound(self._player_death_sound) + bs.pushcall(bs.Call(self.do_end, 'defeat')) + bs.setmusic(None) + self._player_death_sound.play() def do_end(self, outcome: str) -> None: """End the game now with the provided outcome.""" @@ -672,7 +674,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): fail_message = None else: score = None - fail_message = ba.Lstr(resource='reachWave2Text') + fail_message = bs.Lstr(resource='reachWave2Text') self.end( delay=delay, @@ -694,7 +696,6 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): and not self._game_over and self._lives > 0 ): - self._can_end_wave = False self._time_bonus_timer = None self._time_bonus_text = None @@ -708,22 +709,21 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): # Reward time bonus. base_delay = 4.0 if won else 0 if self._time_bonus > 0: - ba.timer(0, ba.Call(ba.playsound, self._cashregistersound)) - ba.timer( + bs.timer(0, self._cashregistersound.play) + bs.timer( base_delay, - ba.Call(self._award_time_bonus, self._time_bonus), + bs.Call(self._award_time_bonus, self._time_bonus), ) base_delay += 1.0 # Reward flawless bonus. if self._wavenum > 0 and self._flawless: - ba.timer(base_delay, self._award_flawless_bonus) + bs.timer(base_delay, self._award_flawless_bonus) base_delay += 1.0 self._flawless = True # reset if won: - # Completion achievements: if self._preset in {Preset.PRO, Preset.PRO_EASY}: self._award_achievement( @@ -746,19 +746,19 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): # Give remaining players some points and have them celebrate. self.show_zoom_message( - ba.Lstr(resource='victoryText'), scale=1.0, duration=4.0 + bs.Lstr(resource='victoryText'), scale=1.0, duration=4.0 ) self.celebrate(10.0) - ba.timer(base_delay, self._award_lives_bonus) + bs.timer(base_delay, self._award_lives_bonus) base_delay += 1.0 - ba.timer(base_delay, self._award_completion_bonus) + bs.timer(base_delay, self._award_completion_bonus) base_delay += 0.85 - ba.playsound(self._winsound) - ba.cameraflash() - ba.setmusic(ba.MusicType.VICTORY) + self._winsound.play() + bs.cameraflash() + bs.setmusic(bs.MusicType.VICTORY) self._game_over = True - ba.timer(base_delay, ba.Call(self.do_end, 'victory')) + bs.timer(base_delay, bs.Call(self.do_end, 'victory')) return self._wavenum += 1 @@ -767,17 +767,17 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): if self._wavenum > 1: self.celebrate(0.5) - ba.timer(base_delay, self._start_next_wave) + bs.timer(base_delay, self._start_next_wave) def _award_completion_bonus(self) -> None: bonus = 200 - ba.playsound(self._cashregistersound) + self._cashregistersound.play() PopupText( - ba.Lstr( + bs.Lstr( value='+${A} ${B}', subs=[ ('${A}', str(bonus)), - ('${B}', ba.Lstr(resource='completionBonusText')), + ('${B}', bs.Lstr(resource='completionBonusText')), ], ), color=(0.7, 0.7, 1.0, 1), @@ -789,13 +789,13 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): def _award_lives_bonus(self) -> None: bonus = self._lives * 30 - ba.playsound(self._cashregistersound) + self._cashregistersound.play() PopupText( - ba.Lstr( + bs.Lstr( value='+${A} ${B}', subs=[ ('${A}', str(bonus)), - ('${B}', ba.Lstr(resource='livesBonusText')), + ('${B}', bs.Lstr(resource='livesBonusText')), ], ), color=(0.7, 1.0, 0.3, 1), @@ -806,13 +806,13 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): self._update_scores() def _award_time_bonus(self, bonus: int) -> None: - ba.playsound(self._cashregistersound) + self._cashregistersound.play() PopupText( - ba.Lstr( + bs.Lstr( value='+${A} ${B}', subs=[ ('${A}', str(bonus)), - ('${B}', ba.Lstr(resource='timeBonusText')), + ('${B}', bs.Lstr(resource='timeBonusText')), ], ), color=(1, 1, 0.5, 1), @@ -824,13 +824,13 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): self._update_scores() def _award_flawless_bonus(self) -> None: - ba.playsound(self._cashregistersound) + self._cashregistersound.play() PopupText( - ba.Lstr( + bs.Lstr( value='+${A} ${B}', subs=[ ('${A}', str(self._flawless_bonus)), - ('${B}', ba.Lstr(resource='perfectWaveText')), + ('${B}', bs.Lstr(resource='perfectWaveText')), ], ), color=(1, 1, 0.2, 1), @@ -843,7 +843,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): self._update_scores() def _start_time_bonus_timer(self) -> None: - self._time_bonus_timer = ba.Timer( + self._time_bonus_timer = bs.Timer( 1.0, self._update_time_bonus, repeat=True ) @@ -853,10 +853,10 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): # pylint: disable=too-many-branches # pylint: disable=too-many-statements self.show_zoom_message( - ba.Lstr( + bs.Lstr( value='${A} ${B}', subs=[ - ('${A}', ba.Lstr(resource='waveText')), + ('${A}', bs.Lstr(resource='waveText')), ('${B}', str(self._wavenum)), ], ), @@ -864,7 +864,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): duration=1.0, trail=True, ) - ba.timer(0.4, ba.Call(ba.playsound, self._new_wave_sound)) + bs.timer(0.4, self._new_wave_sound.play) t_sec = 0.0 base_delay = 0.5 delay = 0.0 @@ -1060,18 +1060,18 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): delay = base_delay delay /= self._get_bot_speed(bot_type) t_sec += delay * 0.5 - tcall = ba.Call( + tcall = bs.Call( self.add_bot_at_point, point, bot_type, path, 0.1 if point is Point.START else non_runner_spawn_time, ) - ba.timer(t_sec, tcall) + bs.timer(t_sec, tcall) t_sec += delay * 0.5 # We can end the wave after all the spawning happens. - ba.timer( + bs.timer( t_sec - delay * 0.5 + non_runner_spawn_time + 0.01, self._set_can_end_wave, ) @@ -1084,15 +1084,15 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): self._time_bonus = 150 self._flawless_bonus = this_flawless_bonus assert self._time_bonus_mult is not None - txtval = ba.Lstr( + txtval = bs.Lstr( value='${A}: ${B}', subs=[ - ('${A}', ba.Lstr(resource='timeBonusText')), + ('${A}', bs.Lstr(resource='timeBonusText')), ('${B}', str(int(self._time_bonus * self._time_bonus_mult))), ], ) - self._time_bonus_text = ba.NodeActor( - ba.newnode( + self._time_bonus_text = bs.NodeActor( + bs.newnode( 'text', attrs={ 'v_attach': 'top', @@ -1109,17 +1109,17 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): ) ) - ba.timer(t_sec, self._start_time_bonus_timer) + bs.timer(t_sec, self._start_time_bonus_timer) # Keep track of when this wave finishes emerging. We wanna stop # dropping land-mines powerups at some point (otherwise a crafty # player could fill the whole map with them) - self._last_wave_end_time = ba.time() + t_sec + self._last_wave_end_time = bs.Time(bs.time() + t_sec) totalwaves = str(len(self._waves)) if self._waves is not None else '??' - txtval = ba.Lstr( + txtval = bs.Lstr( value='${A} ${B}', subs=[ - ('${A}', ba.Lstr(resource='waveText')), + ('${A}', bs.Lstr(resource='waveText')), ( '${B}', str(self._wavenum) @@ -1132,8 +1132,8 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): ), ], ) - self._wave_text = ba.NodeActor( - ba.newnode( + self._wave_text = bs.NodeActor( + bs.newnode( 'text', attrs={ 'v_attach': 'top', @@ -1151,7 +1151,6 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): ) def _on_bot_spawn(self, path: int, spaz: SpazBot) -> None: - # Add our custom update callback and set some info for this bot. spaz_type = type(spaz) assert spaz is not None @@ -1178,7 +1177,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): spaztype, pos=pos, spawn_time=spawn_time, - on_spawn_call=ba.Call(self._on_bot_spawn, path), + on_spawn_call=bs.Call(self._on_bot_spawn, path), ) def _update_time_bonus(self) -> None: @@ -1186,10 +1185,10 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): if self._time_bonus > 0 and self._time_bonus_text is not None: assert self._time_bonus_text.node assert self._time_bonus_mult - self._time_bonus_text.node.text = ba.Lstr( + self._time_bonus_text.node.text = bs.Lstr( value='${A}: ${B}', subs=[ - ('${A}', ba.Lstr(resource='timeBonusText')), + ('${A}', bs.Lstr(resource='timeBonusText')), ( '${B}', str(int(self._time_bonus * self._time_bonus_mult)), @@ -1200,7 +1199,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): self._time_bonus_text = None def _start_updating_waves(self) -> None: - self._wave_update_timer = ba.Timer(2.0, self._update_waves, repeat=True) + self._wave_update_timer = bs.Timer(2.0, self._update_waves, repeat=True) def _update_scores(self) -> None: score = self._score @@ -1234,7 +1233,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): # Bots in row 1 attempt the high road.. if r_walk_row == 1: - if ba.is_point_in_box(pos, boxes['b4']): + if bs.is_point_in_box(pos, boxes['b4']): bot.node.move_up_down = speed bot.node.move_left_right = 0 bot.node.run = 0.0 @@ -1242,43 +1241,42 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): # Row 1 and 2 bots attempt the middle road.. if r_walk_row in [1, 2]: - if ba.is_point_in_box(pos, boxes['b1']): + if bs.is_point_in_box(pos, boxes['b1']): bot.node.move_up_down = speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True # All bots settle for the third row. - if ba.is_point_in_box(pos, boxes['b7']): + if bs.is_point_in_box(pos, boxes['b7']): bot.node.move_up_down = speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True - if ba.is_point_in_box(pos, boxes['b2']): + if bs.is_point_in_box(pos, boxes['b2']): bot.node.move_up_down = -speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True - if ba.is_point_in_box(pos, boxes['b3']): + if bs.is_point_in_box(pos, boxes['b3']): bot.node.move_up_down = -speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True - if ba.is_point_in_box(pos, boxes['b5']): + if bs.is_point_in_box(pos, boxes['b5']): bot.node.move_up_down = -speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True - if ba.is_point_in_box(pos, boxes['b6']): + if bs.is_point_in_box(pos, boxes['b6']): bot.node.move_up_down = speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True if ( - ba.is_point_in_box(pos, boxes['b8']) - and not ba.is_point_in_box(pos, boxes['b9']) + bs.is_point_in_box(pos, boxes['b8']) + and not bs.is_point_in_box(pos, boxes['b9']) ) or pos == (0.0, 0.0, 0.0): - # Default to walking right if we're still in the walking area. bot.node.move_left_right = speed bot.node.move_up_down = 0 @@ -1289,11 +1287,11 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): return False def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.PlayerScoredMessage): + if isinstance(msg, bs.PlayerScoredMessage): self._score += msg.score self._update_scores() - elif isinstance(msg, ba.PlayerDiedMessage): + elif isinstance(msg, bs.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) @@ -1303,13 +1301,13 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): player = msg.getplayer(Player) assert self.initialplayerinfos is not None respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 - player.respawn_timer = ba.Timer( - respawn_time, ba.Call(self.spawn_player_if_exists, player) + player.respawn_timer = bs.Timer( + respawn_time, bs.Call(self.spawn_player_if_exists, player) ) player.respawn_icon = RespawnIcon(player, respawn_time) elif isinstance(msg, SpazBotDiedMessage): - if msg.how is ba.DeathType.REACHED_GOAL: + if msg.how is bs.DeathType.REACHED_GOAL: return None pts, importance = msg.spazbot.get_death_points(msg.how) if msg.killerplayer is not None: @@ -1319,7 +1317,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): assert msg.spazbot.node target = msg.spazbot.node.position except Exception: - ba.print_exception() + logging.exception('Error getting SpazBotDied target.') target = None try: if msg.killerplayer: @@ -1331,14 +1329,14 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): screenmessage=False, importance=importance, ) - ba.playsound( + dingsound = ( self._dingsound if importance == 1 - else self._dingsoundhigh, - volume=0.6, + else self._dingsoundhigh ) + dingsound.play(volume=0.6) except Exception: - ba.print_exception('Error on SpazBotDiedMessage.') + logging.exception('Error on SpazBotDiedMessage.') # Normally we pull scores from the score-set, but if there's no # player lets be explicit. diff --git a/assets/src/ba_data/python/bastd/game/targetpractice.py b/src/assets/ba_data/python/bastd/game/targetpractice.py similarity index 79% rename from assets/src/ba_data/python/bastd/game/targetpractice.py rename to src/assets/ba_data/python/bastd/game/targetpractice.py index 3788f01a..9dd11882 100644 --- a/assets/src/ba_data/python/bastd/game/targetpractice.py +++ b/src/assets/ba_data/python/bastd/game/targetpractice.py @@ -2,7 +2,7 @@ # """Implements Target Practice game.""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations @@ -10,25 +10,26 @@ from __future__ import annotations import random from typing import TYPE_CHECKING -import ba from bastd.actor.scoreboard import Scoreboard from bastd.actor.onscreencountdown import OnScreenCountdown from bastd.actor.bomb import Bomb from bastd.actor.popuptext import PopupText +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence + from bastd.actor.bomb import Blast -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: self.streak = 0 -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self) -> None: @@ -36,34 +37,34 @@ class Team(ba.Team[Player]): # ba_meta export game -class TargetPracticeGame(ba.TeamGameActivity[Player, Team]): +class TargetPracticeGame(bs.TeamGameActivity[Player, Team]): """Game where players try to hit targets with bombs.""" name = 'Target Practice' description = 'Bomb as many targets as you can.' available_settings = [ - ba.IntSetting('Target Count', min_value=1, default=3), - ba.BoolSetting('Enable Impact Bombs', default=True), - ba.BoolSetting('Enable Triple Bombs', default=True), + bs.IntSetting('Target Count', min_value=1, default=3), + bs.BoolSetting('Enable Impact Bombs', default=True), + bs.BoolSetting('Enable Triple Bombs', default=True), ] - default_music = ba.MusicType.FORWARD_MARCH + default_music = bs.MusicType.FORWARD_MARCH @classmethod - def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: + def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: return ['Doom Shroom'] @classmethod - def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: + def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: # We support any teams or versus sessions. - return issubclass(sessiontype, ba.CoopSession) or issubclass( - sessiontype, ba.MultiTeamSession + return issubclass(sessiontype, bs.CoopSession) or issubclass( + sessiontype, bs.MultiTeamSession ) def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() self._targets: list[Target] = [] - self._update_timer: ba.Timer | None = None + self._update_timer: bs.Timer | None = None self._countdown: OnScreenCountdown | None = None self._target_count = int(settings['Target Count']) self._enable_impact_bombs = bool(settings['Enable Impact Bombs']) @@ -79,13 +80,13 @@ class TargetPracticeGame(ba.TeamGameActivity[Player, Team]): # Number of targets is based on player count. for i in range(self._target_count): - ba.timer(5.0 + i * 1.0, self._spawn_target) + bs.timer(5.0 + i * 1.0, self._spawn_target) - self._update_timer = ba.Timer(1.0, self._update, repeat=True) + self._update_timer = bs.Timer(1.0, self._update, repeat=True) self._countdown = OnScreenCountdown(60, endcall=self.end_game) - ba.timer(4.0, self._countdown.start) + bs.timer(4.0, self._countdown.start) - def spawn_player(self, player: Player) -> ba.Actor: + def spawn_player(self, player: Player) -> bs.Actor: spawn_center = (0, 3, -5) pos = ( spawn_center[0] + random.uniform(-1.5, 1.5), @@ -107,7 +108,6 @@ class TargetPracticeGame(ba.TeamGameActivity[Player, Team]): return spaz def _spawn_target(self) -> None: - # Generate a few random points; we'll use whichever one is farthest # from our existing targets (don't want overlapping targets). points = [] @@ -119,9 +119,9 @@ class TargetPracticeGame(ba.TeamGameActivity[Player, Team]): ypos = random.uniform(-1.0, 1.0) if xpos * xpos + ypos * ypos < 1.0: break - points.append(ba.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos)) + points.append(bs.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos)) - def get_min_dist_from_target(pnt: ba.Vec3) -> float: + def get_min_dist_from_target(pnt: bs.Vec3) -> float: return min((t.get_dist_from_point(pnt) for t in self._targets)) # If we have existing targets, use the point with the highest @@ -133,7 +133,7 @@ class TargetPracticeGame(ba.TeamGameActivity[Player, Team]): self._targets.append(Target(position=point)) - def _on_spaz_dropped_bomb(self, spaz: ba.Actor, bomb: ba.Actor) -> None: + def _on_spaz_dropped_bomb(self, spaz: bs.Actor, bomb: bs.Actor) -> None: del spaz # Unused. # Wire up this bomb to inform us when it blows up. @@ -145,7 +145,7 @@ class TargetPracticeGame(ba.TeamGameActivity[Player, Team]): pos = blast.node.position # Debugging: throw a locator down where we landed. - # ba.newnode('locator', attrs={'position':blast.node.position}) + # bs.newnode('locator', attrs={'position':blast.node.position}) # Feed the explosion point to all our targets and get points in return. # Note: we operate on a copy of self._targets since the list may change @@ -171,7 +171,7 @@ class TargetPracticeGame(ba.TeamGameActivity[Player, Team]): def handlemessage(self, msg: Any) -> Any: # When players die, respawn them. - if isinstance(msg, ba.PlayerDiedMessage): + if isinstance(msg, bs.PlayerDiedMessage): super().handlemessage(msg) # Do standard stuff. player = msg.getplayer(Player) assert player is not None @@ -189,13 +189,13 @@ class TargetPracticeGame(ba.TeamGameActivity[Player, Team]): self._scoreboard.set_team_value(team, team.score) def end_game(self) -> None: - results = ba.GameResults() + results = bs.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results) -class Target(ba.Actor): +class Target(bs.Actor): """A target practice target.""" class TargetHitMessage: @@ -207,13 +207,13 @@ class Target(ba.Actor): self._r3 = 2.0 self._rfudge = 0.15 super().__init__() - self._position = ba.Vec3(position) + self._position = bs.Vec3(position) self._hit = False # It can be handy to test with this on to make sure the projection # isn't too far off from the actual object. show_in_space = False - loc1 = ba.newnode( + loc1 = bs.newnode( 'locator', attrs={ 'shape': 'circle', @@ -224,7 +224,7 @@ class Target(ba.Actor): 'additive': True, }, ) - loc2 = ba.newnode( + loc2 = bs.newnode( 'locator', attrs={ 'shape': 'circleOutline', @@ -235,7 +235,7 @@ class Target(ba.Actor): 'additive': True, }, ) - loc3 = ba.newnode( + loc3 = bs.newnode( 'locator', attrs={ 'shape': 'circleOutline', @@ -247,23 +247,23 @@ class Target(ba.Actor): }, ) self._nodes = [loc1, loc2, loc3] - ba.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]}) - ba.animate_array(loc2, 'size', 1, {0.05: [0.0], 0.25: [self._r2 * 2.0]}) - ba.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]}) - ba.playsound(ba.getsound('laserReverse')) + bs.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]}) + bs.animate_array(loc2, 'size', 1, {0.05: [0.0], 0.25: [self._r2 * 2.0]}) + bs.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]}) + bs.getsound('laserReverse').play() def exists(self) -> bool: return bool(self._nodes) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.DieMessage): + if isinstance(msg, bs.DieMessage): for node in self._nodes: node.delete() self._nodes = [] else: super().handlemessage(msg) - def get_dist_from_point(self, pos: ba.Vec3) -> float: + def get_dist_from_point(self, pos: bs.Vec3) -> float: """Given a point, returns distance squared from it.""" return (pos - self._position).length() @@ -276,7 +276,7 @@ class Target(ba.Actor): if activity.has_ended() or self._hit or not self._nodes: return False - diff = ba.Vec3(pos) - self._position + diff = bs.Vec3(pos) - self._position # Disregard Y difference. Our target point probably isn't exactly # on the ground anyway. @@ -300,40 +300,38 @@ class Target(ba.Actor): bullseye = True self._nodes[1].color = cdull self._nodes[2].color = cdull - ba.animate_array(self._nodes[0], 'color', 3, keys, loop=True) + bs.animate_array(self._nodes[0], 'color', 3, keys, loop=True) popupscale = 1.8 popupcolor = (1, 1, 0, 1) streak = player.streak points = 10 + min(20, streak * 2) - ba.playsound(ba.getsound('bellHigh')) + bs.getsound('bellHigh').play() if streak > 0: - ba.playsound( - ba.getsound( - 'orchestraHit4' - if streak > 3 - else 'orchestraHit3' - if streak > 2 - else 'orchestraHit2' - if streak > 1 - else 'orchestraHit' - ) - ) + bs.getsound( + 'orchestraHit4' + if streak > 3 + else 'orchestraHit3' + if streak > 2 + else 'orchestraHit2' + if streak > 1 + else 'orchestraHit' + ).play() elif dist <= self._r2 + self._rfudge: self._nodes[0].color = cdull self._nodes[2].color = cdull - ba.animate_array(self._nodes[1], 'color', 3, keys, loop=True) + bs.animate_array(self._nodes[1], 'color', 3, keys, loop=True) popupscale = 1.25 popupcolor = (1, 0.5, 0.2, 1) points = 4 - ba.playsound(ba.getsound('bellMed')) + bs.getsound('bellMed').play() else: self._nodes[0].color = cdull self._nodes[1].color = cdull - ba.animate_array(self._nodes[2], 'color', 3, keys, loop=True) + bs.animate_array(self._nodes[2], 'color', 3, keys, loop=True) popupscale = 1.0 popupcolor = (0.8, 0.3, 0.3, 1) points = 2 - ba.playsound(ba.getsound('bellLow')) + bs.getsound('bellLow').play() # Award points/etc.. (technically should probably leave this up # to the activity). @@ -342,7 +340,7 @@ class Target(ba.Actor): # If there's more than 1 player in the game, include their # names and colors so they know who got the hit. if len(activity.players) > 1: - popupcolor = ba.safecolor(player.color, target_intensity=0.75) + popupcolor = bs.safecolor(player.color, target_intensity=0.75) popupstr += ' ' + player.getname() PopupText( popupstr, @@ -363,24 +361,24 @@ class Target(ba.Actor): player, points, showpoints=False, screenmessage=False ) - ba.animate_array( + bs.animate_array( self._nodes[0], 'size', 1, {0.8: self._nodes[0].size, 1.0: [0.0]}, ) - ba.animate_array( + bs.animate_array( self._nodes[1], 'size', 1, {0.85: self._nodes[1].size, 1.05: [0.0]}, ) - ba.animate_array( + bs.animate_array( self._nodes[2], 'size', 1, {0.9: self._nodes[2].size, 1.1: [0.0]}, ) - ba.timer(1.1, ba.Call(self.handlemessage, ba.DieMessage())) + bs.timer(1.1, bs.Call(self.handlemessage, bs.DieMessage())) return bullseye diff --git a/assets/src/ba_data/python/bastd/game/thelaststand.py b/src/assets/ba_data/python/bastd/game/thelaststand.py similarity index 86% rename from assets/src/ba_data/python/bastd/game/thelaststand.py rename to src/assets/ba_data/python/bastd/game/thelaststand.py index 62e98c91..afe60117 100644 --- a/assets/src/ba_data/python/bastd/game/thelaststand.py +++ b/src/assets/ba_data/python/bastd/game/thelaststand.py @@ -5,10 +5,10 @@ from __future__ import annotations import random +import logging from dataclasses import dataclass from typing import TYPE_CHECKING -import ba from bastd.actor.playerspaz import PlayerSpaz from bastd.actor.bomb import TNTSpawner from bastd.actor.scoreboard import Scoreboard @@ -29,6 +29,7 @@ from bastd.actor.spazbot import ( StickyBot, ExplodeyBot, ) +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence @@ -44,15 +45,15 @@ class SpawnInfo: dincrease: float -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" -class TheLastStandGame(ba.CoopGameActivity[Player, Team]): +class TheLastStandGame(bs.CoopGameActivity[Player, Team]): """Slow motion how-long-can-you-last game.""" name = 'The Last Stand' @@ -68,14 +69,14 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]): # And of course the most important part. slow_motion = True - default_music = ba.MusicType.EPIC + default_music = bs.MusicType.EPIC def __init__(self, settings: dict): settings['map'] = 'Rampage' super().__init__(settings) - self._new_wave_sound = ba.getsound('scoreHit01') - self._winsound = ba.getsound('score') - self._cashregistersound = ba.getsound('cashRegister') + self._new_wave_sound = bs.getsound('scoreHit01') + self._winsound = bs.getsound('score') + self._cashregistersound = bs.getsound('cashRegister') self._spawn_center = (0, 5.5, -4.14) self._tntspawnpos = (0, 5.5, -6) self._powerup_center = (0, 7, -4.14) @@ -85,11 +86,11 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]): self._scoreboard: Scoreboard | None = None self._score = 0 self._bots = SpazBotSet() - self._dingsound = ba.getsound('dingSmall') - self._dingsoundhigh = ba.getsound('dingSmallHigh') + self._dingsound = bs.getsound('dingSmall') + self._dingsoundhigh = bs.getsound('dingSmallHigh') self._tntspawner: TNTSpawner | None = None self._bot_update_interval: float | None = None - self._bot_update_timer: ba.Timer | None = None + self._bot_update_timer: bs.Timer | None = None self._powerup_drop_timer = None # For each bot type: [spawnrate, increase, d_increase] @@ -106,13 +107,13 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]): ChargerBot: SpawnInfo(0.30, 0.05, 0.000), StickyBot: SpawnInfo(0.10, 0.03, 0.001), ExplodeyBot: SpawnInfo(0.05, 0.02, 0.002), - } # yapf: disable + } def on_transition_in(self) -> None: super().on_transition_in() - ba.timer(1.3, ba.Call(ba.playsound, self._new_wave_sound)) + bs.timer(1.3, self._new_wave_sound.play) self._scoreboard = Scoreboard( - label=ba.Lstr(resource='scoreText'), score_split=0.5 + label=bs.Lstr(resource='scoreText'), score_split=0.5 ) def on_begin(self) -> None: @@ -120,15 +121,15 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]): # Spit out a few powerups and start dropping more shortly. self._drop_powerups(standard_points=True) - ba.timer(2.0, ba.WeakCall(self._start_powerup_drops)) - ba.timer(0.001, ba.WeakCall(self._start_bot_updates)) + bs.timer(2.0, bs.WeakCall(self._start_powerup_drops)) + bs.timer(0.001, bs.WeakCall(self._start_bot_updates)) self.setup_low_life_warning_sound() self._update_scores() self._tntspawner = TNTSpawner( position=self._tntspawnpos, respawn_time=10.0 ) - def spawn_player(self, player: Player) -> ba.Actor: + def spawn_player(self, player: Player) -> bs.Actor: pos = ( self._spawn_center[0] + random.uniform(-1.5, 1.5), self._spawn_center[1], @@ -144,8 +145,8 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]): self._update_bots() if len(self.players) > 3: self._update_bots() - self._bot_update_timer = ba.Timer( - self._bot_update_interval, ba.WeakCall(self._update_bots) + self._bot_update_timer = bs.Timer( + self._bot_update_interval, bs.WeakCall(self._update_bots) ) def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None: @@ -159,8 +160,8 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]): ).autoretain() def _start_powerup_drops(self) -> None: - self._powerup_drop_timer = ba.Timer( - 3.0, ba.WeakCall(self._drop_powerups), repeat=True + self._powerup_drop_timer = bs.Timer( + 3.0, bs.WeakCall(self._drop_powerups), repeat=True ) def _drop_powerups( @@ -172,9 +173,9 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]): if standard_points: pts = self.map.powerup_spawn_points for i in range(len(pts)): - ba.timer( + bs.timer( 1.0 + i * 0.5, - ba.WeakCall( + bs.WeakCall( self._drop_powerup, i, force_first if i == 0 else None ), ) @@ -216,8 +217,8 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]): def _update_bots(self) -> None: assert self._bot_update_interval is not None self._bot_update_interval = max(0.5, self._bot_update_interval * 0.98) - self._bot_update_timer = ba.Timer( - self._bot_update_interval, ba.WeakCall(self._update_bots) + self._bot_update_timer = bs.Timer( + self._bot_update_interval, bs.WeakCall(self._update_bots) ) botspawnpts: list[Sequence[float]] = [ [-5.0, 5.5, -4.14], @@ -233,7 +234,7 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]): assert player.actor.node playerpts.append(player.actor.node.position) except Exception: - ba.print_exception('Error updating bots.') + logging.exception('Error updating bots.') for i in range(3): for playerpt in playerpts: dists[i] += abs(playerpt[0] - botspawnpts[i][0]) @@ -290,12 +291,12 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]): self._scoreboard.set_team_value(self.teams[0], score, max_score=None) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, ba.PlayerDiedMessage): + if isinstance(msg, bs.PlayerDiedMessage): player = msg.getplayer(Player) self.stats.player_was_killed(player) - ba.timer(0.1, self._checkroundover) + bs.timer(0.1, self._checkroundover) - elif isinstance(msg, ba.PlayerScoredMessage): + elif isinstance(msg, bs.PlayerScoredMessage): self._score += msg.score self._update_scores() @@ -313,10 +314,10 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]): screenmessage=False, importance=importance, ) - ba.playsound( - self._dingsound if importance == 1 else self._dingsoundhigh, - volume=0.6, + diesound = ( + self._dingsound if importance == 1 else self._dingsoundhigh ) + diesound.play(volume=0.6) # Normally we pull scores from the score-set, but if there's no # player lets be explicit. @@ -329,8 +330,8 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]): def end_game(self) -> None: # Tell our bots to celebrate just to rub it in. self._bots.final_celebrate() - ba.setmusic(None) - ba.pushcall(ba.WeakCall(self.do_end, 'defeat')) + bs.setmusic(None) + bs.pushcall(bs.WeakCall(self.do_end, 'defeat')) def _checkroundover(self) -> None: """End the round if conditions are met.""" diff --git a/assets/src/ba_data/python/bastd/gameutils.py b/src/assets/ba_data/python/bastd/gameutils.py similarity index 62% rename from assets/src/ba_data/python/bastd/gameutils.py rename to src/assets/ba_data/python/bastd/gameutils.py index 7dbef7c5..ca8d7ab1 100644 --- a/assets/src/ba_data/python/bastd/gameutils.py +++ b/src/assets/ba_data/python/bastd/gameutils.py @@ -6,7 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba +import babase +import bascenev1 as bs if TYPE_CHECKING: pass @@ -22,28 +23,28 @@ class SharedObjects: standard materials. """ - _STORENAME = ba.storagename() + _STORENAME = babase.storagename() def __init__(self) -> None: - activity = ba.getactivity() + activity = bs.getactivity() if self._STORENAME in activity.customdata: raise RuntimeError( 'Use SharedObjects.get() to fetch the' ' shared instance for this activity.' ) - self._object_material: ba.Material | None = None - self._player_material: ba.Material | None = None - self._pickup_material: ba.Material | None = None - self._footing_material: ba.Material | None = None - self._attack_material: ba.Material | None = None - self._death_material: ba.Material | None = None - self._region_material: ba.Material | None = None - self._railing_material: ba.Material | None = None + self._object_material: bs.Material | None = None + self._player_material: bs.Material | None = None + self._pickup_material: bs.Material | None = None + self._footing_material: bs.Material | None = None + self._attack_material: bs.Material | None = None + self._death_material: bs.Material | None = None + self._region_material: bs.Material | None = None + self._railing_material: bs.Material | None = None @classmethod def get(cls) -> SharedObjects: """Fetch/create the instance of this class for the current activity.""" - activity = ba.getactivity() + activity = bs.getactivity() shobs = activity.customdata.get(cls._STORENAME) if shobs is None: shobs = SharedObjects() @@ -52,89 +53,89 @@ class SharedObjects: return shobs @property - def player_material(self) -> ba.Material: - """a ba.Material to be applied to player parts. Generally, + def player_material(self) -> bs.Material: + """a bascenev1.Material to be applied to player parts. Generally, materials related to the process of scoring when reaching a goal, etc will look for the presence of this material on things that hit them. """ if self._player_material is None: - self._player_material = ba.Material() + self._player_material = bs.Material() return self._player_material @property - def object_material(self) -> ba.Material: - """A ba.Material that should be applied to any small, + def object_material(self) -> bs.Material: + """A bascenev1.Material that should be applied to any small, normal, physical objects such as bombs, boxes, players, etc. Other materials often check for the presence of this material as a prerequisite for performing certain actions (such as disabling collisions between initially-overlapping objects) """ if self._object_material is None: - self._object_material = ba.Material() + self._object_material = bs.Material() return self._object_material @property - def pickup_material(self) -> ba.Material: - """A ba.Material; collision shapes used for picking things + def pickup_material(self) -> bs.Material: + """A bascenev1.Material; collision shapes used for picking things up will have this material applied. To prevent an object from being picked up, you can add a material that disables collisions against things containing this material. """ if self._pickup_material is None: - self._pickup_material = ba.Material() + self._pickup_material = bs.Material() return self._pickup_material @property - def footing_material(self) -> ba.Material: + def footing_material(self) -> bs.Material: """Anything that can be 'walked on' should have this - ba.Material applied; generally just terrain and whatnot. A character - will snap upright whenever touching something with this material so it - should not be applied to props, etc. + bascenev1.Material applied; generally just terrain and whatnot. + A character will snap upright whenever touching something with this + material so it should not be applied to props, etc. """ if self._footing_material is None: - self._footing_material = ba.Material() + self._footing_material = bs.Material() return self._footing_material @property - def attack_material(self) -> ba.Material: - """A ba.Material applied to explosion shapes, punch + def attack_material(self) -> bs.Material: + """A bascenev1.Material applied to explosion shapes, punch shapes, etc. An object not wanting to receive impulse/etc messages can disable collisions against this material. """ if self._attack_material is None: - self._attack_material = ba.Material() + self._attack_material = bs.Material() return self._attack_material @property - def death_material(self) -> ba.Material: - """A ba.Material that sends a ba.DieMessage() to anything + def death_material(self) -> bs.Material: + """A bascenev1.Material that sends a ba.DieMessage() to anything that touches it; handy for terrain below a cliff, etc. """ if self._death_material is None: - mat = self._death_material = ba.Material() + mat = self._death_material = bs.Material() mat.add_actions( - ('message', 'their_node', 'at_connect', ba.DieMessage()) + ('message', 'their_node', 'at_connect', bs.DieMessage()) ) return self._death_material @property - def region_material(self) -> ba.Material: - """A ba.Material used for non-physical collision shapes + def region_material(self) -> bs.Material: + """A bascenev1.Material used for non-physical collision shapes (regions); collisions can generally be allowed with this material even when initially overlapping since it is not physical. """ if self._region_material is None: - self._region_material = ba.Material() + self._region_material = bs.Material() return self._region_material @property - def railing_material(self) -> ba.Material: - """A ba.Material with a very low friction/stiffness/etc + def railing_material(self) -> bs.Material: + """A bascenev1.Material with a very low friction/stiffness/etc that can be applied to invisible 'railings' useful for gently keeping characters from falling off of cliffs. """ if self._railing_material is None: - mat = self._railing_material = ba.Material() + mat = self._railing_material = bs.Material() mat.add_actions(('modify_part_collision', 'collide', False)) mat.add_actions(('modify_part_collision', 'stiffness', 0.003)) mat.add_actions(('modify_part_collision', 'damping', 0.00001)) diff --git a/assets/src/ba_data/python/bastd/keyboard/__init__.py b/src/assets/ba_data/python/bastd/keyboard/__init__.py similarity index 100% rename from assets/src/ba_data/python/bastd/keyboard/__init__.py rename to src/assets/ba_data/python/bastd/keyboard/__init__.py diff --git a/assets/src/ba_data/python/bastd/keyboard/englishkeyboard.py b/src/assets/ba_data/python/bastd/keyboard/englishkeyboard.py similarity index 92% rename from assets/src/ba_data/python/bastd/keyboard/englishkeyboard.py rename to src/assets/ba_data/python/bastd/keyboard/englishkeyboard.py index fdf37a0c..ae94d938 100644 --- a/assets/src/ba_data/python/bastd/keyboard/englishkeyboard.py +++ b/src/assets/ba_data/python/bastd/keyboard/englishkeyboard.py @@ -2,14 +2,14 @@ # """Defines a default keyboards.""" -# ba_meta require api 7 +# ba_meta require api 8 # (see https://ballistica.net/wiki/meta-tag-system) from __future__ import annotations from typing import TYPE_CHECKING -import ba +import babase if TYPE_CHECKING: from typing import Iterable @@ -33,7 +33,7 @@ def split(chars: Iterable[str], maxlen: int) -> list[list[str]]: def generate_emojis(maxlen: int) -> list[list[str]]: - """Generates a lot of UTF8 emojis prepared for ba.Keyboard pages""" + """Generates a lot of UTF8 emojis prepared for babase.Keyboard pages""" all_emojis = split([chr(i) for i in range(0x1F601, 0x1F650)], maxlen) all_emojis += split([chr(i) for i in range(0x2702, 0x27B1)], maxlen) all_emojis += split([chr(i) for i in range(0x1F680, 0x1F6C1)], maxlen) @@ -41,7 +41,7 @@ def generate_emojis(maxlen: int) -> list[list[str]]: # ba_meta export keyboard -class EnglishKeyboard(ba.Keyboard): +class EnglishKeyboard(babase.Keyboard): """Default English keyboard.""" name = 'English' diff --git a/assets/src/ba_data/python/bastd/mainmenu.py b/src/assets/ba_data/python/bastd/mainmenu.py similarity index 78% rename from assets/src/ba_data/python/bastd/mainmenu.py rename to src/assets/ba_data/python/bastd/mainmenu.py index 8453e629..0f4229a9 100644 --- a/assets/src/ba_data/python/bastd/mainmenu.py +++ b/src/assets/ba_data/python/bastd/mainmenu.py @@ -5,13 +5,13 @@ from __future__ import annotations -import random import time +import random import weakref from typing import TYPE_CHECKING -import ba -import ba.internal +import bascenev1 as bs +import bauiv1 as bui if TYPE_CHECKING: from typing import Any @@ -25,34 +25,40 @@ if TYPE_CHECKING: # noinspection PyAttributeOutsideInit -class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): +class MainMenuActivity(bs.Activity[bs.Player, bs.Team]): """Activity showing the rotating main menu bg stuff.""" - _stdassets = ba.Dependency(ba.AssetPackage, 'stdassets@1') + _stdassets = bs.Dependency(bs.AssetPackage, 'stdassets@1') def on_transition_in(self) -> None: super().on_transition_in() random.seed(123) - self._logo_node: ba.Node | None = None + self._logo_node: bs.Node | None = None self._custom_logo_tex_name: str | None = None - self._word_actors: list[ba.Actor] = [] - app = ba.app + self._word_actors: list[bs.Actor] = [] + app = bs.app + assert app.classic is not None + + plus = bui.app.plus + assert plus is not None # FIXME: We shouldn't be doing things conditionally based on whether # the host is VR mode or not (clients may differ in that regard). # Any differences need to happen at the engine level so everyone # sees things in their own optimal way. - vr_mode = ba.app.vr_mode + vr_mode = bs.app.vr_mode - if not ba.app.toolbar_test: + if not bs.app.toolbar_test: color = (1.0, 1.0, 1.0, 1.0) if vr_mode else (0.5, 0.6, 0.5, 0.6) # FIXME: Need a node attr for vr-specific-scale. scale = ( - 0.9 if (app.ui.uiscale is ba.UIScale.SMALL or vr_mode) else 0.7 + 0.9 + if (app.classic.ui.uiscale is bs.UIScale.SMALL or vr_mode) + else 0.7 ) - self.my_name = ba.NodeActor( - ba.newnode( + self.my_name = bs.NodeActor( + bs.newnode( 'text', attrs={ 'v_attach': 'bottom', @@ -71,12 +77,12 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): # Throw up some text that only clients can see so they know that the # host is navigating menus while they're just staring at an # empty-ish screen. - tval = ba.Lstr( + tval = bs.Lstr( resource='hostIsNavigatingMenusText', - subs=[('${HOST}', ba.internal.get_v1_account_display_string())], + subs=[('${HOST}', plus.get_v1_account_display_string())], ) - self._host_is_navigating_text = ba.NodeActor( - ba.newnode( + self._host_is_navigating_text = bs.NodeActor( + bs.newnode( 'text', attrs={ 'text': tval, @@ -87,36 +93,36 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): }, ) ) - if not ba.app.main_menu_did_initial_transition and hasattr( + if not app.classic.main_menu_did_initial_transition and hasattr( self, 'my_name' ): assert self.my_name.node - ba.animate(self.my_name.node, 'opacity', {2.3: 0, 3.0: 1.0}) + bs.animate(self.my_name.node, 'opacity', {2.3: 0, 3.0: 1.0}) # FIXME: We shouldn't be doing things conditionally based on whether # the host is vr mode or not (clients may not be or vice versa). # Any differences need to happen at the engine level so everyone sees # things in their own optimal way. vr_mode = app.vr_mode - uiscale = app.ui.uiscale + uiscale = app.classic.ui.uiscale # In cases where we're doing lots of dev work lets always show the # build number. force_show_build_number = False - if not ba.app.toolbar_test: + if not bs.app.toolbar_test: if app.debug_build or app.test_build or force_show_build_number: if app.debug_build: - text = ba.Lstr( + text = bs.Lstr( value='${V} (${B}) (${D})', subs=[ ('${V}', app.version), ('${B}', str(app.build_number)), - ('${D}', ba.Lstr(resource='debugText')), + ('${D}', bs.Lstr(resource='debugText')), ], ) else: - text = ba.Lstr( + text = bs.Lstr( value='${V} (${B})', subs=[ ('${V}', app.version), @@ -124,11 +130,11 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): ], ) else: - text = ba.Lstr(value='${V}', subs=[('${V}', app.version)]) - scale = 0.9 if (uiscale is ba.UIScale.SMALL or vr_mode) else 0.7 + text = bs.Lstr(value='${V}', subs=[('${V}', app.version)]) + scale = 0.9 if (uiscale is bs.UIScale.SMALL or vr_mode) else 0.7 color = (1, 1, 1, 1) if vr_mode else (0.5, 0.6, 0.5, 0.7) - self.version = ba.NodeActor( - ba.newnode( + self.version = bs.NodeActor( + bs.newnode( 'text', attrs={ 'v_attach': 'bottom', @@ -144,17 +150,17 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): }, ) ) - if not ba.app.main_menu_did_initial_transition: + if not app.classic.main_menu_did_initial_transition: assert self.version.node - ba.animate(self.version.node, 'opacity', {2.3: 0, 3.0: 1.0}) + bs.animate(self.version.node, 'opacity', {2.3: 0, 3.0: 1.0}) # Show the iircade logo on our iircade build. if app.iircade_mode: - img = ba.NodeActor( - ba.newnode( + img = bs.NodeActor( + bs.newnode( 'image', attrs={ - 'texture': ba.gettexture('iircadeLogo'), + 'texture': bs.gettexture('iircadeLogo'), 'attach': 'center', 'scale': (250, 250), 'position': (0, 0), @@ -163,19 +169,19 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): }, ) ).autoretain() - imgdelay = 0.0 if app.main_menu_did_initial_transition else 1.0 - ba.animate( + imgdelay = ( + 0.0 if app.classic.main_menu_did_initial_transition else 1.0 + ) + bs.animate( img.node, 'opacity', {imgdelay + 1.5: 0.0, imgdelay + 2.5: 1.0} ) # Throw in test build info. self.beta_info = self.beta_info_2 = None if app.test_build and not (app.demo_mode or app.arcade_mode): - pos = ( - (230, 125) if (app.demo_mode or app.arcade_mode) else (230, 35) - ) - self.beta_info = ba.NodeActor( - ba.newnode( + pos = (230, 35) + self.beta_info = bs.NodeActor( + bs.newnode( 'text', attrs={ 'v_attach': 'center', @@ -186,25 +192,25 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): 'scale': 1, 'vr_depth': -60, 'position': pos, - 'text': ba.Lstr(resource='testBuildText'), + 'text': bs.Lstr(resource='testBuildText'), }, ) ) - if not ba.app.main_menu_did_initial_transition: + if not app.classic.main_menu_did_initial_transition: assert self.beta_info.node - ba.animate(self.beta_info.node, 'opacity', {1.3: 0, 1.8: 1.0}) + bs.animate(self.beta_info.node, 'opacity', {1.3: 0, 1.8: 1.0}) - model = ba.getmodel('thePadLevel') - trees_model = ba.getmodel('trees') - bottom_model = ba.getmodel('thePadLevelBottom') - color_texture = ba.gettexture('thePadLevelColor') - trees_texture = ba.gettexture('treesColor') - bgtex = ba.gettexture('menuBG') - bgmodel = ba.getmodel('thePadBG') + mesh = bs.getmesh('thePadLevel') + trees_mesh = bs.getmesh('trees') + bottom_mesh = bs.getmesh('thePadLevelBottom') + color_texture = bs.gettexture('thePadLevelColor') + trees_texture = bs.gettexture('treesColor') + bgtex = bs.gettexture('menuBG') + bgmesh = bs.getmesh('thePadBG') # Load these last since most platforms don't use them. - vr_bottom_fill_model = ba.getmodel('thePadVRFillBottom') - vr_top_fill_model = ba.getmodel('thePadVRFillTop') + vr_bottom_fill_mesh = bs.getmesh('thePadVRFillBottom') + vr_top_fill_mesh = bs.getmesh('thePadVRFillTop') gnode = self.globalsnode gnode.camera_mode = 'rotate' @@ -215,11 +221,11 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): gnode.vignette_outer = (0.45, 0.55, 0.54) gnode.vignette_inner = (0.99, 0.98, 0.98) - self.bottom = ba.NodeActor( - ba.newnode( + self.bottom = bs.NodeActor( + bs.newnode( 'terrain', attrs={ - 'model': bottom_model, + 'mesh': bottom_mesh, 'lighting': False, 'reflection': 'soft', 'reflection_scale': [0.45], @@ -227,44 +233,44 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): }, ) ) - self.vr_bottom_fill = ba.NodeActor( - ba.newnode( + self.vr_bottom_fill = bs.NodeActor( + bs.newnode( 'terrain', attrs={ - 'model': vr_bottom_fill_model, + 'mesh': vr_bottom_fill_mesh, 'lighting': False, 'vr_only': True, 'color_texture': color_texture, }, ) ) - self.vr_top_fill = ba.NodeActor( - ba.newnode( + self.vr_top_fill = bs.NodeActor( + bs.newnode( 'terrain', attrs={ - 'model': vr_top_fill_model, + 'mesh': vr_top_fill_mesh, 'vr_only': True, 'lighting': False, 'color_texture': bgtex, }, ) ) - self.terrain = ba.NodeActor( - ba.newnode( + self.terrain = bs.NodeActor( + bs.newnode( 'terrain', attrs={ - 'model': model, + 'mesh': mesh, 'color_texture': color_texture, 'reflection': 'soft', 'reflection_scale': [0.3], }, ) ) - self.trees = ba.NodeActor( - ba.newnode( + self.trees = bs.NodeActor( + bs.newnode( 'terrain', attrs={ - 'model': trees_model, + 'mesh': trees_mesh, 'lighting': False, 'reflection': 'char', 'reflection_scale': [0.1], @@ -272,11 +278,11 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): }, ) ) - self.bgterrain = ba.NodeActor( - ba.newnode( + self.bgterrain = bs.NodeActor( + bs.newnode( 'terrain', attrs={ - 'model': bgmodel, + 'mesh': bgmesh, 'color': (0.92, 0.91, 0.9), 'lighting': False, 'background': True, @@ -288,11 +294,11 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): self._ts = 0.86 self._language: str | None = None - self._update_timer = ba.Timer(1.0, self._update, repeat=True) + self._update_timer = bs.Timer(1.0, self._update, repeat=True) self._update() # Hopefully this won't hitch but lets space these out anyway. - ba.internal.add_clean_frame_callback(ba.WeakCall(self._start_preloads)) + bui.add_clean_frame_callback(bs.WeakCall(self._start_preloads)) random.seed() @@ -300,41 +306,55 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): class News: """Wrangles news display.""" - def __init__(self, activity: ba.Activity): + def __init__(self, activity: bs.Activity): self._valid = True self._message_duration = 10.0 self._message_spacing = 2.0 - self._text: ba.NodeActor | None = None + self._text: bs.NodeActor | None = None self._activity = weakref.ref(activity) # If we're signed in, fetch news immediately. # Otherwise wait until we are signed in. - self._fetch_timer: ba.Timer | None = ba.Timer( - 1.0, ba.WeakCall(self._try_fetching_news), repeat=True + self._fetch_timer: bs.Timer | None = bs.Timer( + 1.0, bs.WeakCall(self._try_fetching_news), repeat=True ) self._try_fetching_news() + self._used_phrases: list[str] = [] # We now want to wait until we're signed in before fetching news. def _try_fetching_news(self) -> None: - if ba.internal.get_v1_account_state() == 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() == 'signed_in': self._fetch_news() self._fetch_timer = None def _fetch_news(self) -> None: - ba.app.main_menu_last_news_fetch_time = time.time() + plus = bui.app.plus + assert plus is not None + + assert bs.app.classic is not None + bs.app.classic.main_menu_last_news_fetch_time = time.time() # UPDATE - We now just pull news from MRVs. - news = ba.internal.get_v1_account_misc_read_val('n', None) + news = plus.get_v1_account_misc_read_val('n', None) if news is not None: self._got_news(news) def _change_phrase(self) -> None: from bastd.actor.text import Text + app = bs.app + assert app.classic is not None + # If our news is way out of date, lets re-request it; # otherwise, rotate our phrase. - assert ba.app.main_menu_last_news_fetch_time is not None - if time.time() - ba.app.main_menu_last_news_fetch_time > 600.0: + assert app.classic.main_menu_last_news_fetch_time is not None + if ( + time.time() - app.classic.main_menu_last_news_fetch_time + > 600.0 + ): self._fetch_news() self._text = None else: @@ -346,7 +366,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): if val == '__ACH__': vrmode = app.vr_mode Text( - ba.Lstr(resource='nextAchievementsText'), + bs.Lstr(resource='nextAchievementsText'), color=( (1, 1, 1, 1) if vrmode @@ -367,7 +387,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): ).autoretain() achs = [ a - for a in app.ach.achievements + for a in app.classic.ach.achievements if not a.complete ] if achs: @@ -401,7 +421,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): spc + self._message_duration: 0.0, } assert self._text.node - ba.animate(self._text.node, 'opacity', keys) + bs.animate(self._text.node, 'opacity', keys) # {k: v # for k, v in list(keys.items())}) self._text.node.text = val @@ -413,38 +433,38 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): activity = self._activity() if activity is None or activity.expired: return - with ba.Context(activity): - + with activity.context: self._phrases: list[str] = [] # Show upcoming achievements in non-vr versions # (currently too hard to read in vr). self._used_phrases = ( - ['__ACH__'] if not ba.app.vr_mode else [] + ['__ACH__'] if not bs.app.vr_mode else [] ) + [s for s in news.split('
\n') if s != ''] - self._phrase_change_timer = ba.Timer( + self._phrase_change_timer = bs.Timer( (self._message_duration + self._message_spacing), - ba.WeakCall(self._change_phrase), + bs.WeakCall(self._change_phrase), repeat=True, ) + assert bs.app.classic is not None scl = ( 1.2 if ( - ba.app.ui.uiscale is ba.UIScale.SMALL - or ba.app.vr_mode + bs.app.classic.ui.uiscale is bs.UIScale.SMALL + or bs.app.vr_mode ) else 0.8 ) color2 = ( (1, 1, 1, 1) - if ba.app.vr_mode + if bs.app.vr_mode else (0.7, 0.65, 0.75, 1.0) ) - shadow = 1.0 if ba.app.vr_mode else 0.4 - self._text = ba.NodeActor( - ba.newnode( + shadow = 1.0 if bs.app.vr_mode else 0.4 + self._text = bs.NodeActor( + bs.newnode( 'text', attrs={ 'v_attach': 'top', @@ -467,23 +487,24 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): self._news = News(self) # Bring up the last place we were, or start at the main menu otherwise. - with ba.Context('ui'): + with bs.ContextRef.empty(): from bastd.ui import specialoffer + assert bs.app.classic is not None if bool(False): - uicontroller = ba.app.ui.controller + uicontroller = bs.app.classic.ui.controller assert uicontroller is not None uicontroller.show_main_menu() else: - main_menu_location = ba.app.ui.get_main_menu_location() + main_menu_location = bs.app.classic.ui.get_main_menu_location() # When coming back from a kiosk-mode game, jump to # the kiosk start screen. - if ba.app.demo_mode or ba.app.arcade_mode: + if bs.app.demo_mode or bs.app.arcade_mode: # pylint: disable=cyclic-import from bastd.ui.kiosk import KioskWindow - ba.app.ui.set_main_menu_window( + bs.app.classic.ui.set_main_menu_window( KioskWindow().get_root_widget() ) # ..or in normal cases go back to the main menu @@ -492,14 +513,14 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): # pylint: disable=cyclic-import from bastd.ui.gather import GatherWindow - ba.app.ui.set_main_menu_window( + bs.app.classic.ui.set_main_menu_window( GatherWindow(transition=None).get_root_widget() ) elif main_menu_location == 'Watch': # pylint: disable=cyclic-import from bastd.ui.watch import WatchWindow - ba.app.ui.set_main_menu_window( + bs.app.classic.ui.set_main_menu_window( WatchWindow(transition=None).get_root_widget() ) elif main_menu_location == 'Team Game Select': @@ -508,9 +529,9 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): PlaylistBrowserWindow, ) - ba.app.ui.set_main_menu_window( + bs.app.classic.ui.set_main_menu_window( PlaylistBrowserWindow( - sessiontype=ba.DualTeamSession, transition=None + sessiontype=bs.DualTeamSession, transition=None ).get_root_widget() ) elif main_menu_location == 'Free-for-All Game Select': @@ -519,9 +540,9 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): PlaylistBrowserWindow, ) - ba.app.ui.set_main_menu_window( + bs.app.classic.ui.set_main_menu_window( PlaylistBrowserWindow( - sessiontype=ba.FreeForAllSession, + sessiontype=bs.FreeForAllSession, transition=None, ).get_root_widget() ) @@ -529,21 +550,21 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): # pylint: disable=cyclic-import from bastd.ui.coop.browser import CoopBrowserWindow - ba.app.ui.set_main_menu_window( + bs.app.classic.ui.set_main_menu_window( CoopBrowserWindow(transition=None).get_root_widget() ) elif main_menu_location == 'Benchmarks & Stress Tests': # pylint: disable=cyclic-import from bastd.ui.debug import DebugWindow - ba.app.ui.set_main_menu_window( + bs.app.classic.ui.set_main_menu_window( DebugWindow(transition=None).get_root_widget() ) else: # pylint: disable=cyclic-import from bastd.ui.mainmenu import MainMenuWindow - ba.app.ui.set_main_menu_window( + bs.app.classic.ui.set_main_menu_window( MainMenuWindow(transition=None).get_root_widget() ) @@ -557,33 +578,30 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): def try_again() -> None: if not specialoffer.show_offer(): # Try one last time.. - ba.timer( - 2.0, - specialoffer.show_offer, - timetype=ba.TimeType.REAL, - ) + bui.apptimer(2.0, specialoffer.show_offer) - ba.timer(2.0, try_again, timetype=ba.TimeType.REAL) - ba.app.main_menu_did_initial_transition = True + bui.apptimer(2.0, try_again) + app.classic.main_menu_did_initial_transition = True def _update(self) -> None: - app = ba.app + app = bs.app + assert app.classic is not None # Update logo in case it changes. if self._logo_node: custom_texture = self._get_custom_logo_tex_name() if custom_texture != self._custom_logo_tex_name: self._custom_logo_tex_name = custom_texture - self._logo_node.texture = ba.gettexture( + self._logo_node.texture = bs.gettexture( custom_texture if custom_texture is not None else 'logo' ) - self._logo_node.model_opaque = ( - None if custom_texture is not None else ba.getmodel('logo') + self._logo_node.mesh_opaque = ( + None if custom_texture is not None else bs.getmesh('logo') ) - self._logo_node.model_transparent = ( + self._logo_node.mesh_transparent = ( None if custom_texture is not None - else ba.getmodel('logoTransparent') + else bs.getmesh('logoTransparent') ) # If language has changed, recreate our logo text/graphics. @@ -598,7 +616,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): delay_inc = 0.02 # Come on faster after the first time. - if app.main_menu_did_initial_transition: + if app.classic.main_menu_did_initial_transition: base_delay = 0.0 delay = base_delay delay_inc = 0.02 @@ -782,8 +800,8 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): shadow: bool = False, ) -> None: if shadow: - word_obj = ba.NodeActor( - ba.newnode( + word_obj = bs.NodeActor( + bs.newnode( 'text', attrs={ 'position': (x, y), @@ -802,8 +820,8 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): ) self._word_actors.append(word_obj) else: - word_obj = ba.NodeActor( - ba.newnode( + word_obj = bs.NodeActor( + bs.newnode( 'text', attrs={ 'position': (x, y), @@ -824,17 +842,17 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): # Add a bit of stop-motion-y jitter to the logo # (unless we're in VR mode in which case its best to # leave things still). - if not ba.app.vr_mode: - cmb: ba.Node | None - cmb2: ba.Node | None + if not bs.app.vr_mode: + cmb: bs.Node | None + cmb2: bs.Node | None if not shadow: - cmb = ba.newnode( + cmb = bs.newnode( 'combine', owner=word_obj.node, attrs={'size': 2} ) else: cmb = None if shadow: - cmb2 = ba.newnode( + cmb2 = bs.newnode( 'combine', owner=word_obj.node, attrs={'size': 2} ) else: @@ -855,9 +873,9 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): keys2[time_v * self._ts] = val2 + 5 time_v += random.random() * 0.1 if cmb is not None: - ba.animate(cmb, 'input0', keys, loop=True) + bs.animate(cmb, 'input0', keys, loop=True) if cmb2 is not None: - ba.animate(cmb2, 'input0', keys2, loop=True) + bs.animate(cmb2, 'input0', keys2, loop=True) keys = {} keys2 = {} time_v = 0 @@ -868,27 +886,30 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): keys2[time_v * self._ts] = val2 - 9 time_v += random.random() * 0.1 if cmb is not None: - ba.animate(cmb, 'input1', keys, loop=True) + bs.animate(cmb, 'input1', keys, loop=True) if cmb2 is not None: - ba.animate(cmb2, 'input1', keys2, loop=True) + bs.animate(cmb2, 'input1', keys2, loop=True) if not shadow: assert word_obj.node - ba.animate( + bs.animate( word_obj.node, 'project_scale', {delay: 0.0, delay + 0.1: scale * 1.1, delay + 0.2: scale}, ) else: assert word_obj.node - ba.animate( + bs.animate( word_obj.node, 'project_scale', {delay: 0.0, delay + 0.1: scale * 1.1, delay + 0.2: scale}, ) def _get_custom_logo_tex_name(self) -> str | None: - if ba.internal.get_v1_account_misc_read_val('easter', False): + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_misc_read_val('easter', False): return 'logoEaster' return None @@ -904,27 +925,26 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): rotate: float = 0.0, vr_depth_offset: float = 0.0, ) -> None: - # Temp easter goodness. if custom_texture is None: custom_texture = self._get_custom_logo_tex_name() self._custom_logo_tex_name = custom_texture - ltex = ba.gettexture( + ltex = bs.gettexture( custom_texture if custom_texture is not None else 'logo' ) - mopaque = None if custom_texture is not None else ba.getmodel('logo') + mopaque = None if custom_texture is not None else bs.getmesh('logo') mtrans = ( None if custom_texture is not None - else ba.getmodel('logoTransparent') + else bs.getmesh('logoTransparent') ) - logo = ba.NodeActor( - ba.newnode( + logo = bs.NodeActor( + bs.newnode( 'image', attrs={ 'texture': ltex, - 'model_opaque': mopaque, - 'model_transparent': mtrans, + 'mesh_opaque': mopaque, + 'mesh_transparent': mtrans, 'vr_depth': -10 + vr_depth_offset, 'rotate': rotate, 'attach': 'center', @@ -940,8 +960,8 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): # (unless we're in VR mode in which case its best to # leave things still). assert logo.node - if not ba.app.vr_mode: - cmb = ba.newnode('combine', owner=logo.node, attrs={'size': 2}) + if not bs.app.vr_mode: + cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2}) cmb.connectattr('output', logo.node, 'position') keys = {} time_v = 0.0 @@ -950,7 +970,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): for _i in range(10): keys[time_v] = x + (random.random() - 0.5) * 0.7 * jitter_scale time_v += random.random() * 0.1 - ba.animate(cmb, 'input0', keys, loop=True) + bs.animate(cmb, 'input0', keys, loop=True) keys = {} time_v = 0.0 for _i in range(10): @@ -958,19 +978,19 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): y + (random.random() - 0.5) * 0.7 * jitter_scale ) time_v += random.random() * 0.1 - ba.animate(cmb, 'input1', keys, loop=True) + bs.animate(cmb, 'input1', keys, loop=True) else: logo.node.position = (x, y) - cmb = ba.newnode('combine', owner=logo.node, attrs={'size': 2}) + cmb = bs.newnode('combine', owner=logo.node, attrs={'size': 2}) keys = { delay: 0.0, delay + 0.1: 700.0 * scale, delay + 0.2: 600.0 * scale, } - ba.animate(cmb, 'input0', keys) - ba.animate(cmb, 'input1', keys) + bs.animate(cmb, 'input0', keys) + bs.animate(cmb, 'input1', keys) cmb.connectattr('output', logo.node, 'scale') def _start_preloads(self) -> None: @@ -978,10 +998,14 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): # or check for a dead activity so we have to do that ourself. if self.expired: return - with ba.Context(self): + with self.context: _preload1() - ba.timer(0.5, lambda: ba.setmusic(ba.MusicType.MENU)) + def _start_menu_music() -> None: + assert bs.app.classic is not None + bs.setmusic(bs.MusicType.MENU) + + bui.apptimer(0.5, _start_menu_music) def _preload1() -> None: @@ -999,9 +1023,9 @@ def _preload1() -> None: 'scrollWidgetShort', 'windowBGBlotch', ]: - ba.getmodel(mname) + bs.getmesh(mname) for tname in ['playerLineup', 'lock']: - ba.gettexture(tname) + bs.gettexture(tname) for tex in [ 'iconRunaround', 'iconOnslaught', @@ -1011,12 +1035,12 @@ def _preload1() -> None: 'medalGold', 'characterIconMask', ]: - ba.gettexture(tex) - ba.gettexture('bg') + bs.gettexture(tex) + bs.gettexture('bg') from bastd.actor.powerupbox import PowerupBoxFactory PowerupBoxFactory.get() - ba.timer(0.1, _preload2) + bui.apptimer(0.1, _preload2) def _preload2() -> None: @@ -1024,7 +1048,7 @@ def _preload2() -> None: # so they don't have to redundantly call the load # (even if the actual result is cached). for mname in ['powerup', 'powerupSimple']: - ba.getmodel(mname) + bs.getmesh(mname) for tname in [ 'powerupBomb', 'powerupSpeed', @@ -1035,7 +1059,7 @@ def _preload2() -> None: 'powerupImpactBombs', 'powerupHealth', ]: - ba.gettexture(tname) + bs.gettexture(tname) for sname in [ 'powerup01', 'boxDrop', @@ -1046,18 +1070,18 @@ def _preload2() -> None: 'spawn', 'gong', ]: - ba.getsound(sname) + bs.getsound(sname) from bastd.actor.bomb import BombFactory BombFactory.get() - ba.timer(0.1, _preload3) + bui.apptimer(0.1, _preload3) def _preload3() -> None: from bastd.actor.spazfactory import SpazFactory for mname in ['bomb', 'bombSticky', 'impactBomb']: - ba.getmodel(mname) + bs.getmesh(mname) for tname in [ 'bombColor', 'bombColorIce', @@ -1065,44 +1089,43 @@ def _preload3() -> None: 'impactBombColor', 'impactBombColorLit', ]: - ba.gettexture(tname) + bs.gettexture(tname) for sname in ['freeze', 'fuse01', 'activateBeep', 'warnBeep']: - ba.getsound(sname) + bs.getsound(sname) SpazFactory.get() - ba.timer(0.2, _preload4) + bui.apptimer(0.2, _preload4) def _preload4() -> None: for tname in ['bar', 'meter', 'null', 'flagColor', 'achievementOutline']: - ba.gettexture(tname) + bs.gettexture(tname) for mname in ['frameInset', 'meterTransparent', 'achievementOutline']: - ba.getmodel(mname) + bs.getmesh(mname) for sname in ['metalHit', 'metalSkid', 'refWhistle', 'achievement']: - ba.getsound(sname) + bs.getsound(sname) from bastd.actor.flag import FlagFactory FlagFactory.get() -class MainMenuSession(ba.Session): +class MainMenuSession(bs.Session): """Session that runs the main menu environment.""" def __init__(self) -> None: - # Gather dependencies we'll need (just our activity). - self._activity_deps = ba.DependencySet(ba.Dependency(MainMenuActivity)) + self._activity_deps = bs.DependencySet(bs.Dependency(MainMenuActivity)) super().__init__([self._activity_deps]) self._locked = False - self.setactivity(ba.newactivity(MainMenuActivity)) + self.setactivity(bs.newactivity(MainMenuActivity)) - def on_activity_end(self, activity: ba.Activity, results: Any) -> None: + def on_activity_end(self, activity: bs.Activity, results: Any) -> None: if self._locked: - ba.internal.unlock_all_input() + bui.unlock_all_input() # Any ending activity leads us into the main menu one. - self.setactivity(ba.newactivity(MainMenuActivity)) + self.setactivity(bs.newactivity(MainMenuActivity)) - def on_player_request(self, player: ba.SessionPlayer) -> bool: + def on_player_request(self, player: bs.SessionPlayer) -> bool: # Reject all player requests. return False diff --git a/assets/src/ba_data/python/bastd/mapdata/__init__.py b/src/assets/ba_data/python/bastd/mapdata/__init__.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/__init__.py rename to src/assets/ba_data/python/bastd/mapdata/__init__.py diff --git a/assets/src/ba_data/python/bastd/mapdata/big_g.py b/src/assets/ba_data/python/bastd/mapdata/big_g.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/big_g.py rename to src/assets/ba_data/python/bastd/mapdata/big_g.py diff --git a/assets/src/ba_data/python/bastd/mapdata/bridgit.py b/src/assets/ba_data/python/bastd/mapdata/bridgit.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/bridgit.py rename to src/assets/ba_data/python/bastd/mapdata/bridgit.py diff --git a/assets/src/ba_data/python/bastd/mapdata/courtyard.py b/src/assets/ba_data/python/bastd/mapdata/courtyard.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/courtyard.py rename to src/assets/ba_data/python/bastd/mapdata/courtyard.py diff --git a/assets/src/ba_data/python/bastd/mapdata/crag_castle.py b/src/assets/ba_data/python/bastd/mapdata/crag_castle.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/crag_castle.py rename to src/assets/ba_data/python/bastd/mapdata/crag_castle.py diff --git a/assets/src/ba_data/python/bastd/mapdata/doom_shroom.py b/src/assets/ba_data/python/bastd/mapdata/doom_shroom.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/doom_shroom.py rename to src/assets/ba_data/python/bastd/mapdata/doom_shroom.py diff --git a/assets/src/ba_data/python/bastd/mapdata/football_stadium.py b/src/assets/ba_data/python/bastd/mapdata/football_stadium.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/football_stadium.py rename to src/assets/ba_data/python/bastd/mapdata/football_stadium.py diff --git a/assets/src/ba_data/python/bastd/mapdata/happy_thoughts.py b/src/assets/ba_data/python/bastd/mapdata/happy_thoughts.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/happy_thoughts.py rename to src/assets/ba_data/python/bastd/mapdata/happy_thoughts.py diff --git a/assets/src/ba_data/python/bastd/mapdata/hockey_stadium.py b/src/assets/ba_data/python/bastd/mapdata/hockey_stadium.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/hockey_stadium.py rename to src/assets/ba_data/python/bastd/mapdata/hockey_stadium.py diff --git a/assets/src/ba_data/python/bastd/mapdata/lake_frigid.py b/src/assets/ba_data/python/bastd/mapdata/lake_frigid.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/lake_frigid.py rename to src/assets/ba_data/python/bastd/mapdata/lake_frigid.py diff --git a/assets/src/ba_data/python/bastd/mapdata/monkey_face.py b/src/assets/ba_data/python/bastd/mapdata/monkey_face.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/monkey_face.py rename to src/assets/ba_data/python/bastd/mapdata/monkey_face.py diff --git a/assets/src/ba_data/python/bastd/mapdata/rampage.py b/src/assets/ba_data/python/bastd/mapdata/rampage.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/rampage.py rename to src/assets/ba_data/python/bastd/mapdata/rampage.py diff --git a/assets/src/ba_data/python/bastd/mapdata/roundabout.py b/src/assets/ba_data/python/bastd/mapdata/roundabout.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/roundabout.py rename to src/assets/ba_data/python/bastd/mapdata/roundabout.py diff --git a/assets/src/ba_data/python/bastd/mapdata/step_right_up.py b/src/assets/ba_data/python/bastd/mapdata/step_right_up.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/step_right_up.py rename to src/assets/ba_data/python/bastd/mapdata/step_right_up.py diff --git a/assets/src/ba_data/python/bastd/mapdata/the_pad.py b/src/assets/ba_data/python/bastd/mapdata/the_pad.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/the_pad.py rename to src/assets/ba_data/python/bastd/mapdata/the_pad.py diff --git a/assets/src/ba_data/python/bastd/mapdata/tip_top.py b/src/assets/ba_data/python/bastd/mapdata/tip_top.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/tip_top.py rename to src/assets/ba_data/python/bastd/mapdata/tip_top.py diff --git a/assets/src/ba_data/python/bastd/mapdata/tower_d.py b/src/assets/ba_data/python/bastd/mapdata/tower_d.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/tower_d.py rename to src/assets/ba_data/python/bastd/mapdata/tower_d.py diff --git a/assets/src/ba_data/python/bastd/mapdata/zig_zag.py b/src/assets/ba_data/python/bastd/mapdata/zig_zag.py similarity index 100% rename from assets/src/ba_data/python/bastd/mapdata/zig_zag.py rename to src/assets/ba_data/python/bastd/mapdata/zig_zag.py diff --git a/assets/src/ba_data/python/bastd/maps.py b/src/assets/ba_data/python/bastd/maps.py similarity index 64% rename from assets/src/ba_data/python/bastd/maps.py rename to src/assets/ba_data/python/bastd/maps.py index c20c82a1..a31039b4 100644 --- a/assets/src/ba_data/python/bastd/maps.py +++ b/src/assets/ba_data/python/bastd/maps.py @@ -7,14 +7,16 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba +import bascenev1 as bs from bastd.gameutils import SharedObjects if TYPE_CHECKING: from typing import Any + import baclassic -class HockeyStadium(ba.Map): + +class HockeyStadium(bs.Map): """Stadium map used for ice hockey games.""" from bastd.mapdata import hockey_stadium as defs @@ -33,17 +35,17 @@ class HockeyStadium(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'models': ( - ba.getmodel('hockeyStadiumOuter'), - ba.getmodel('hockeyStadiumInner'), - ba.getmodel('hockeyStadiumStands'), + 'meshes': ( + bs.getmesh('hockeyStadiumOuter'), + bs.getmesh('hockeyStadiumInner'), + bs.getmesh('hockeyStadiumStands'), ), - 'vr_fill_model': ba.getmodel('footballStadiumVRFill'), - 'collide_model': ba.getcollidemodel('hockeyStadiumCollide'), - 'tex': ba.gettexture('hockeyStadium'), - 'stands_tex': ba.gettexture('footballStadium'), + 'vr_fill_mesh': bs.getmesh('footballStadiumVRFill'), + 'collision_mesh': bs.getcollisionmesh('hockeyStadiumCollide'), + 'tex': bs.gettexture('hockeyStadium'), + 'stands_tex': bs.gettexture('footballStadium'), } - mat = ba.Material() + mat = bs.Material() mat.add_actions(actions=('modify_part_collision', 'friction', 0.01)) data['ice_material'] = mat return data @@ -51,12 +53,12 @@ class HockeyStadium(ba.Map): def __init__(self) -> None: super().__init__() shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'model': self.preloaddata['models'][0], - 'collide_model': self.preloaddata['collide_model'], + 'mesh': self.preloaddata['meshes'][0], + 'collision_mesh': self.preloaddata['collision_mesh'], 'color_texture': self.preloaddata['tex'], 'materials': [ shared.footing_material, @@ -64,10 +66,10 @@ class HockeyStadium(ba.Map): ], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['vr_fill_model'], + 'mesh': self.preloaddata['vr_fill_mesh'], 'vr_only': True, 'lighting': False, 'background': True, @@ -75,25 +77,25 @@ class HockeyStadium(ba.Map): }, ) mats = [shared.footing_material, self.preloaddata['ice_material']] - self.floor = ba.newnode( + self.floor = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['models'][1], + 'mesh': self.preloaddata['meshes'][1], 'color_texture': self.preloaddata['tex'], 'opacity': 0.92, 'opacity_in_low_or_medium_quality': 1.0, 'materials': mats, }, ) - self.stands = ba.newnode( + self.stands = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['models'][2], + 'mesh': self.preloaddata['meshes'][2], 'visible_in_reflections': False, 'color_texture': self.preloaddata['stands_tex'], }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.floor_reflection = True gnode.debris_friction = 0.3 gnode.debris_kill_height = -0.3 @@ -106,7 +108,7 @@ class HockeyStadium(ba.Map): self.is_hockey = True -class FootballStadium(ba.Map): +class FootballStadium(bs.Map): """Stadium map for football games.""" from bastd.mapdata import football_stadium as defs @@ -125,37 +127,37 @@ class FootballStadium(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model': ba.getmodel('footballStadium'), - 'vr_fill_model': ba.getmodel('footballStadiumVRFill'), - 'collide_model': ba.getcollidemodel('footballStadiumCollide'), - 'tex': ba.gettexture('footballStadium'), + 'mesh': bs.getmesh('footballStadium'), + 'vr_fill_mesh': bs.getmesh('footballStadiumVRFill'), + 'collision_mesh': bs.getcollisionmesh('footballStadiumCollide'), + 'tex': bs.gettexture('footballStadium'), } return data def __init__(self) -> None: super().__init__() shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'model': self.preloaddata['model'], - 'collide_model': self.preloaddata['collide_model'], + 'mesh': self.preloaddata['mesh'], + 'collision_mesh': self.preloaddata['collision_mesh'], 'color_texture': self.preloaddata['tex'], 'materials': [shared.footing_material], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['vr_fill_model'], + 'mesh': self.preloaddata['vr_fill_mesh'], 'lighting': False, 'vr_only': True, 'background': True, 'color_texture': self.preloaddata['tex'], }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.tint = (1.3, 1.2, 1.0) gnode.ambient_color = (1.3, 1.2, 1.0) gnode.vignette_outer = (0.57, 0.57, 0.57) @@ -163,7 +165,7 @@ class FootballStadium(ba.Map): gnode.vr_camera_offset = (0, -0.8, -1.1) gnode.vr_near_clip = 0.5 - def is_point_near_edge(self, point: ba.Vec3, running: bool = False) -> bool: + def is_point_near_edge(self, point: bs.Vec3, running: bool = False) -> bool: box_position = self.defs.boxes['edge_box'][0:3] box_scale = self.defs.boxes['edge_box'][6:9] xpos = (point.x - box_position[0]) / box_scale[0] @@ -171,7 +173,7 @@ class FootballStadium(ba.Map): return xpos < -0.5 or xpos > 0.5 or zpos < -0.5 or zpos > 0.5 -class Bridgit(ba.Map): +class Bridgit(bs.Map): """Map with a narrow bridge in the middle.""" from bastd.mapdata import bridgit as defs @@ -192,18 +194,18 @@ class Bridgit(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model_top': ba.getmodel('bridgitLevelTop'), - 'model_bottom': ba.getmodel('bridgitLevelBottom'), - 'model_bg': ba.getmodel('natureBackground'), - 'bg_vr_fill_model': ba.getmodel('natureBackgroundVRFill'), - 'collide_model': ba.getcollidemodel('bridgitLevelCollide'), - 'tex': ba.gettexture('bridgitLevelColor'), - 'model_bg_tex': ba.gettexture('natureBackgroundColor'), - 'collide_bg': ba.getcollidemodel('natureBackgroundCollide'), - 'railing_collide_model': ( - ba.getcollidemodel('bridgitLevelRailingCollide') + 'mesh_top': bs.getmesh('bridgitLevelTop'), + 'mesh_bottom': bs.getmesh('bridgitLevelBottom'), + 'mesh_bg': bs.getmesh('natureBackground'), + 'bg_vr_fill_mesh': bs.getmesh('natureBackgroundVRFill'), + 'collision_mesh': bs.getcollisionmesh('bridgitLevelCollide'), + 'tex': bs.gettexture('bridgitLevelColor'), + 'mesh_bg_tex': bs.gettexture('natureBackgroundColor'), + 'collide_bg': bs.getcollisionmesh('natureBackgroundCollide'), + 'railing_collision_mesh': ( + bs.getcollisionmesh('bridgitLevelRailingCollide') ), - 'bg_material': ba.Material(), + 'bg_material': bs.Material(), } data['bg_material'].add_actions( actions=('modify_part_collision', 'friction', 10.0) @@ -213,55 +215,55 @@ class Bridgit(ba.Map): def __init__(self) -> None: super().__init__() shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'collide_model': self.preloaddata['collide_model'], - 'model': self.preloaddata['model_top'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh_top'], 'color_texture': self.preloaddata['tex'], 'materials': [shared.footing_material], }, ) - self.bottom = ba.newnode( + self.bottom = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['model_bottom'], + 'mesh': self.preloaddata['mesh_bottom'], 'lighting': False, 'color_texture': self.preloaddata['tex'], }, ) - self.background = ba.newnode( + self.background = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['model_bg'], + 'mesh': self.preloaddata['mesh_bg'], 'lighting': False, 'background': True, - 'color_texture': self.preloaddata['model_bg_tex'], + 'color_texture': self.preloaddata['mesh_bg_tex'], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bg_vr_fill_model'], + 'mesh': self.preloaddata['bg_vr_fill_mesh'], 'lighting': False, 'vr_only': True, 'background': True, - 'color_texture': self.preloaddata['model_bg_tex'], + 'color_texture': self.preloaddata['mesh_bg_tex'], }, ) - self.railing = ba.newnode( + self.railing = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['railing_collide_model'], + 'collision_mesh': self.preloaddata['railing_collision_mesh'], 'materials': [shared.railing_material], 'bumper': True, }, ) - self.bg_collide = ba.newnode( + self.bg_collide = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['collide_bg'], + 'collision_mesh': self.preloaddata['collide_bg'], 'materials': [ shared.footing_material, self.preloaddata['bg_material'], @@ -269,14 +271,14 @@ class Bridgit(ba.Map): ], }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.tint = (1.1, 1.2, 1.3) gnode.ambient_color = (1.1, 1.2, 1.3) gnode.vignette_outer = (0.65, 0.6, 0.55) gnode.vignette_inner = (0.9, 0.9, 0.93) -class BigG(ba.Map): +class BigG(bs.Map): """Large G shaped map for racing""" from bastd.mapdata import big_g as defs @@ -302,16 +304,16 @@ class BigG(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model_top': ba.getmodel('bigG'), - 'model_bottom': ba.getmodel('bigGBottom'), - 'model_bg': ba.getmodel('natureBackground'), - 'bg_vr_fill_model': ba.getmodel('natureBackgroundVRFill'), - 'collide_model': ba.getcollidemodel('bigGCollide'), - 'tex': ba.gettexture('bigG'), - 'model_bg_tex': ba.gettexture('natureBackgroundColor'), - 'collide_bg': ba.getcollidemodel('natureBackgroundCollide'), - 'bumper_collide_model': ba.getcollidemodel('bigGBumper'), - 'bg_material': ba.Material(), + 'mesh_top': bs.getmesh('bigG'), + 'mesh_bottom': bs.getmesh('bigGBottom'), + 'mesh_bg': bs.getmesh('natureBackground'), + 'bg_vr_fill_mesh': bs.getmesh('natureBackgroundVRFill'), + 'collision_mesh': bs.getcollisionmesh('bigGCollide'), + 'tex': bs.gettexture('bigG'), + 'mesh_bg_tex': bs.gettexture('natureBackgroundColor'), + 'collide_bg': bs.getcollisionmesh('natureBackgroundCollide'), + 'bumper_collision_mesh': bs.getcollisionmesh('bigGBumper'), + 'bg_material': bs.Material(), } data['bg_material'].add_actions( actions=('modify_part_collision', 'friction', 10.0) @@ -321,57 +323,57 @@ class BigG(ba.Map): def __init__(self) -> None: super().__init__() shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'collide_model': self.preloaddata['collide_model'], + 'collision_mesh': self.preloaddata['collision_mesh'], 'color': (0.7, 0.7, 0.7), - 'model': self.preloaddata['model_top'], + 'mesh': self.preloaddata['mesh_top'], 'color_texture': self.preloaddata['tex'], 'materials': [shared.footing_material], }, ) - self.bottom = ba.newnode( + self.bottom = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['model_bottom'], + 'mesh': self.preloaddata['mesh_bottom'], 'color': (0.7, 0.7, 0.7), 'lighting': False, 'color_texture': self.preloaddata['tex'], }, ) - self.background = ba.newnode( + self.background = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['model_bg'], + 'mesh': self.preloaddata['mesh_bg'], 'lighting': False, 'background': True, - 'color_texture': self.preloaddata['model_bg_tex'], + 'color_texture': self.preloaddata['mesh_bg_tex'], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bg_vr_fill_model'], + 'mesh': self.preloaddata['bg_vr_fill_mesh'], 'lighting': False, 'vr_only': True, 'background': True, - 'color_texture': self.preloaddata['model_bg_tex'], + 'color_texture': self.preloaddata['mesh_bg_tex'], }, ) - self.railing = ba.newnode( + self.railing = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['bumper_collide_model'], + 'collision_mesh': self.preloaddata['bumper_collision_mesh'], 'materials': [shared.railing_material], 'bumper': True, }, ) - self.bg_collide = ba.newnode( + self.bg_collide = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['collide_bg'], + 'collision_mesh': self.preloaddata['collide_bg'], 'materials': [ shared.footing_material, self.preloaddata['bg_material'], @@ -379,14 +381,14 @@ class BigG(ba.Map): ], }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.tint = (1.1, 1.2, 1.3) gnode.ambient_color = (1.1, 1.2, 1.3) gnode.vignette_outer = (0.65, 0.6, 0.55) gnode.vignette_inner = (0.9, 0.9, 0.93) -class Roundabout(ba.Map): +class Roundabout(bs.Map): """CTF map featuring two platforms and a long way around between them""" from bastd.mapdata import roundabout as defs @@ -405,18 +407,18 @@ class Roundabout(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model': ba.getmodel('roundaboutLevel'), - 'model_bottom': ba.getmodel('roundaboutLevelBottom'), - 'model_bg': ba.getmodel('natureBackground'), - 'bg_vr_fill_model': ba.getmodel('natureBackgroundVRFill'), - 'collide_model': ba.getcollidemodel('roundaboutLevelCollide'), - 'tex': ba.gettexture('roundaboutLevelColor'), - 'model_bg_tex': ba.gettexture('natureBackgroundColor'), - 'collide_bg': ba.getcollidemodel('natureBackgroundCollide'), - 'railing_collide_model': ( - ba.getcollidemodel('roundaboutLevelBumper') + 'mesh': bs.getmesh('roundaboutLevel'), + 'mesh_bottom': bs.getmesh('roundaboutLevelBottom'), + 'mesh_bg': bs.getmesh('natureBackground'), + 'bg_vr_fill_mesh': bs.getmesh('natureBackgroundVRFill'), + 'collision_mesh': bs.getcollisionmesh('roundaboutLevelCollide'), + 'tex': bs.gettexture('roundaboutLevelColor'), + 'mesh_bg_tex': bs.gettexture('natureBackgroundColor'), + 'collide_bg': bs.getcollisionmesh('natureBackgroundCollide'), + 'railing_collision_mesh': ( + bs.getcollisionmesh('roundaboutLevelBumper') ), - 'bg_material': ba.Material(), + 'bg_material': bs.Material(), } data['bg_material'].add_actions( actions=('modify_part_collision', 'friction', 10.0) @@ -426,47 +428,47 @@ class Roundabout(ba.Map): def __init__(self) -> None: super().__init__(vr_overlay_offset=(0, -1, 1)) shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'collide_model': self.preloaddata['collide_model'], - 'model': self.preloaddata['model'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], 'color_texture': self.preloaddata['tex'], 'materials': [shared.footing_material], }, ) - self.bottom = ba.newnode( + self.bottom = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['model_bottom'], + 'mesh': self.preloaddata['mesh_bottom'], 'lighting': False, 'color_texture': self.preloaddata['tex'], }, ) - self.background = ba.newnode( + self.background = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['model_bg'], + 'mesh': self.preloaddata['mesh_bg'], 'lighting': False, 'background': True, - 'color_texture': self.preloaddata['model_bg_tex'], + 'color_texture': self.preloaddata['mesh_bg_tex'], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bg_vr_fill_model'], + 'mesh': self.preloaddata['bg_vr_fill_mesh'], 'lighting': False, 'vr_only': True, 'background': True, - 'color_texture': self.preloaddata['model_bg_tex'], + 'color_texture': self.preloaddata['mesh_bg_tex'], }, ) - self.bg_collide = ba.newnode( + self.bg_collide = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['collide_bg'], + 'collision_mesh': self.preloaddata['collide_bg'], 'materials': [ shared.footing_material, self.preloaddata['bg_material'], @@ -474,15 +476,15 @@ class Roundabout(ba.Map): ], }, ) - self.railing = ba.newnode( + self.railing = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['railing_collide_model'], + 'collision_mesh': self.preloaddata['railing_collision_mesh'], 'materials': [shared.railing_material], 'bumper': True, }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.tint = (1.0, 1.05, 1.1) gnode.ambient_color = (1.0, 1.05, 1.1) gnode.shadow_ortho = True @@ -490,7 +492,7 @@ class Roundabout(ba.Map): gnode.vignette_inner = (0.97, 0.95, 0.93) -class MonkeyFace(ba.Map): +class MonkeyFace(bs.Map): """Map sorta shaped like a monkey face; teehee!""" from bastd.mapdata import monkey_face as defs @@ -509,18 +511,18 @@ class MonkeyFace(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model': ba.getmodel('monkeyFaceLevel'), - 'bottom_model': ba.getmodel('monkeyFaceLevelBottom'), - 'model_bg': ba.getmodel('natureBackground'), - 'bg_vr_fill_model': ba.getmodel('natureBackgroundVRFill'), - 'collide_model': ba.getcollidemodel('monkeyFaceLevelCollide'), - 'tex': ba.gettexture('monkeyFaceLevelColor'), - 'model_bg_tex': ba.gettexture('natureBackgroundColor'), - 'collide_bg': ba.getcollidemodel('natureBackgroundCollide'), - 'railing_collide_model': ( - ba.getcollidemodel('monkeyFaceLevelBumper') + 'mesh': bs.getmesh('monkeyFaceLevel'), + 'bottom_mesh': bs.getmesh('monkeyFaceLevelBottom'), + 'mesh_bg': bs.getmesh('natureBackground'), + 'bg_vr_fill_mesh': bs.getmesh('natureBackgroundVRFill'), + 'collision_mesh': bs.getcollisionmesh('monkeyFaceLevelCollide'), + 'tex': bs.gettexture('monkeyFaceLevelColor'), + 'mesh_bg_tex': bs.gettexture('natureBackgroundColor'), + 'collide_bg': bs.getcollisionmesh('natureBackgroundCollide'), + 'railing_collision_mesh': ( + bs.getcollisionmesh('monkeyFaceLevelBumper') ), - 'bg_material': ba.Material(), + 'bg_material': bs.Material(), } data['bg_material'].add_actions( actions=('modify_part_collision', 'friction', 10.0) @@ -530,47 +532,47 @@ class MonkeyFace(ba.Map): def __init__(self) -> None: super().__init__() shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'collide_model': self.preloaddata['collide_model'], - 'model': self.preloaddata['model'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], 'color_texture': self.preloaddata['tex'], 'materials': [shared.footing_material], }, ) - self.bottom = ba.newnode( + self.bottom = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bottom_model'], + 'mesh': self.preloaddata['bottom_mesh'], 'lighting': False, 'color_texture': self.preloaddata['tex'], }, ) - self.background = ba.newnode( + self.background = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['model_bg'], + 'mesh': self.preloaddata['mesh_bg'], 'lighting': False, 'background': True, - 'color_texture': self.preloaddata['model_bg_tex'], + 'color_texture': self.preloaddata['mesh_bg_tex'], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bg_vr_fill_model'], + 'mesh': self.preloaddata['bg_vr_fill_mesh'], 'lighting': False, 'vr_only': True, 'background': True, - 'color_texture': self.preloaddata['model_bg_tex'], + 'color_texture': self.preloaddata['mesh_bg_tex'], }, ) - self.bg_collide = ba.newnode( + self.bg_collide = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['collide_bg'], + 'collision_mesh': self.preloaddata['collide_bg'], 'materials': [ shared.footing_material, self.preloaddata['bg_material'], @@ -578,15 +580,15 @@ class MonkeyFace(ba.Map): ], }, ) - self.railing = ba.newnode( + self.railing = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['railing_collide_model'], + 'collision_mesh': self.preloaddata['railing_collision_mesh'], 'materials': [shared.railing_material], 'bumper': True, }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.tint = (1.1, 1.2, 1.2) gnode.ambient_color = (1.2, 1.3, 1.3) gnode.vignette_outer = (0.60, 0.62, 0.66) @@ -594,7 +596,7 @@ class MonkeyFace(ba.Map): gnode.vr_camera_offset = (-1.4, 0, 0) -class ZigZag(ba.Map): +class ZigZag(bs.Map): """A very long zig-zaggy map""" from bastd.mapdata import zig_zag as defs @@ -619,16 +621,16 @@ class ZigZag(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model': ba.getmodel('zigZagLevel'), - 'model_bottom': ba.getmodel('zigZagLevelBottom'), - 'model_bg': ba.getmodel('natureBackground'), - 'bg_vr_fill_model': ba.getmodel('natureBackgroundVRFill'), - 'collide_model': ba.getcollidemodel('zigZagLevelCollide'), - 'tex': ba.gettexture('zigZagLevelColor'), - 'model_bg_tex': ba.gettexture('natureBackgroundColor'), - 'collide_bg': ba.getcollidemodel('natureBackgroundCollide'), - 'railing_collide_model': ba.getcollidemodel('zigZagLevelBumper'), - 'bg_material': ba.Material(), + 'mesh': bs.getmesh('zigZagLevel'), + 'mesh_bottom': bs.getmesh('zigZagLevelBottom'), + 'mesh_bg': bs.getmesh('natureBackground'), + 'bg_vr_fill_mesh': bs.getmesh('natureBackgroundVRFill'), + 'collision_mesh': bs.getcollisionmesh('zigZagLevelCollide'), + 'tex': bs.gettexture('zigZagLevelColor'), + 'mesh_bg_tex': bs.gettexture('natureBackgroundColor'), + 'collide_bg': bs.getcollisionmesh('natureBackgroundCollide'), + 'railing_collision_mesh': bs.getcollisionmesh('zigZagLevelBumper'), + 'bg_material': bs.Material(), } data['bg_material'].add_actions( actions=('modify_part_collision', 'friction', 10.0) @@ -638,46 +640,46 @@ class ZigZag(ba.Map): def __init__(self) -> None: super().__init__() shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'collide_model': self.preloaddata['collide_model'], - 'model': self.preloaddata['model'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], 'color_texture': self.preloaddata['tex'], 'materials': [shared.footing_material], }, ) - self.background = ba.newnode( + self.background = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['model_bg'], + 'mesh': self.preloaddata['mesh_bg'], 'lighting': False, - 'color_texture': self.preloaddata['model_bg_tex'], + 'color_texture': self.preloaddata['mesh_bg_tex'], }, ) - self.bottom = ba.newnode( + self.bottom = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['model_bottom'], + 'mesh': self.preloaddata['mesh_bottom'], 'lighting': False, 'color_texture': self.preloaddata['tex'], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bg_vr_fill_model'], + 'mesh': self.preloaddata['bg_vr_fill_mesh'], 'lighting': False, 'vr_only': True, 'background': True, - 'color_texture': self.preloaddata['model_bg_tex'], + 'color_texture': self.preloaddata['mesh_bg_tex'], }, ) - self.bg_collide = ba.newnode( + self.bg_collide = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['collide_bg'], + 'collision_mesh': self.preloaddata['collide_bg'], 'materials': [ shared.footing_material, self.preloaddata['bg_material'], @@ -685,15 +687,15 @@ class ZigZag(ba.Map): ], }, ) - self.railing = ba.newnode( + self.railing = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['railing_collide_model'], + 'collision_mesh': self.preloaddata['railing_collision_mesh'], 'materials': [shared.railing_material], 'bumper': True, }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.tint = (1.0, 1.15, 1.15) gnode.ambient_color = (1.0, 1.15, 1.15) gnode.vignette_outer = (0.57, 0.59, 0.63) @@ -701,7 +703,7 @@ class ZigZag(ba.Map): gnode.vr_camera_offset = (-1.5, 0, 0) -class ThePad(ba.Map): +class ThePad(bs.Map): """A simple square shaped map with a raised edge.""" from bastd.mapdata import the_pad as defs @@ -720,15 +722,15 @@ class ThePad(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model': ba.getmodel('thePadLevel'), - 'bottom_model': ba.getmodel('thePadLevelBottom'), - 'collide_model': ba.getcollidemodel('thePadLevelCollide'), - 'tex': ba.gettexture('thePadLevelColor'), - 'bgtex': ba.gettexture('menuBG'), - 'bgmodel': ba.getmodel('thePadBG'), - 'railing_collide_model': ba.getcollidemodel('thePadLevelBumper'), - 'vr_fill_mound_model': ba.getmodel('thePadVRFillMound'), - 'vr_fill_mound_tex': ba.gettexture('vrFillMound'), + 'mesh': bs.getmesh('thePadLevel'), + 'bottom_mesh': bs.getmesh('thePadLevelBottom'), + 'collision_mesh': bs.getcollisionmesh('thePadLevelCollide'), + 'tex': bs.gettexture('thePadLevelColor'), + 'bgtex': bs.gettexture('menuBG'), + 'bgmesh': bs.getmesh('thePadBG'), + 'railing_collision_mesh': bs.getcollisionmesh('thePadLevelBumper'), + 'vr_fill_mound_mesh': bs.getmesh('thePadVRFillMound'), + 'vr_fill_mound_tex': bs.gettexture('vrFillMound'), } # fixme should chop this into vr/non-vr sections for efficiency return data @@ -736,45 +738,45 @@ class ThePad(ba.Map): def __init__(self) -> None: super().__init__() shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'collide_model': self.preloaddata['collide_model'], - 'model': self.preloaddata['model'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], 'color_texture': self.preloaddata['tex'], 'materials': [shared.footing_material], }, ) - self.bottom = ba.newnode( + self.bottom = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bottom_model'], + 'mesh': self.preloaddata['bottom_mesh'], 'lighting': False, 'color_texture': self.preloaddata['tex'], }, ) - self.background = ba.newnode( + self.background = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bgmodel'], + 'mesh': self.preloaddata['bgmesh'], 'lighting': False, 'background': True, 'color_texture': self.preloaddata['bgtex'], }, ) - self.railing = ba.newnode( + self.railing = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['railing_collide_model'], + 'collision_mesh': self.preloaddata['railing_collision_mesh'], 'materials': [shared.railing_material], 'bumper': True, }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['vr_fill_mound_model'], + 'mesh': self.preloaddata['vr_fill_mound_mesh'], 'lighting': False, 'vr_only': True, 'color': (0.56, 0.55, 0.47), @@ -782,14 +784,14 @@ class ThePad(ba.Map): 'color_texture': self.preloaddata['vr_fill_mound_tex'], }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.tint = (1.1, 1.1, 1.0) gnode.ambient_color = (1.1, 1.1, 1.0) gnode.vignette_outer = (0.7, 0.65, 0.75) gnode.vignette_inner = (0.95, 0.95, 0.93) -class DoomShroom(ba.Map): +class DoomShroom(bs.Map): """A giant mushroom. Of doom!""" from bastd.mapdata import doom_shroom as defs @@ -808,72 +810,72 @@ class DoomShroom(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model': ba.getmodel('doomShroomLevel'), - 'collide_model': ba.getcollidemodel('doomShroomLevelCollide'), - 'tex': ba.gettexture('doomShroomLevelColor'), - 'bgtex': ba.gettexture('doomShroomBGColor'), - 'bgmodel': ba.getmodel('doomShroomBG'), - 'vr_fill_model': ba.getmodel('doomShroomVRFill'), - 'stem_model': ba.getmodel('doomShroomStem'), - 'collide_bg': ba.getcollidemodel('doomShroomStemCollide'), + 'mesh': bs.getmesh('doomShroomLevel'), + 'collision_mesh': bs.getcollisionmesh('doomShroomLevelCollide'), + 'tex': bs.gettexture('doomShroomLevelColor'), + 'bgtex': bs.gettexture('doomShroomBGColor'), + 'bgmesh': bs.getmesh('doomShroomBG'), + 'vr_fill_mesh': bs.getmesh('doomShroomVRFill'), + 'stem_mesh': bs.getmesh('doomShroomStem'), + 'collide_bg': bs.getcollisionmesh('doomShroomStemCollide'), } return data def __init__(self) -> None: super().__init__() shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'collide_model': self.preloaddata['collide_model'], - 'model': self.preloaddata['model'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], 'color_texture': self.preloaddata['tex'], 'materials': [shared.footing_material], }, ) - self.background = ba.newnode( + self.background = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bgmodel'], + 'mesh': self.preloaddata['bgmesh'], 'lighting': False, 'background': True, 'color_texture': self.preloaddata['bgtex'], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['vr_fill_model'], + 'mesh': self.preloaddata['vr_fill_mesh'], 'lighting': False, 'vr_only': True, 'background': True, 'color_texture': self.preloaddata['bgtex'], }, ) - self.stem = ba.newnode( + self.stem = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['stem_model'], + 'mesh': self.preloaddata['stem_mesh'], 'lighting': False, 'color_texture': self.preloaddata['tex'], }, ) - self.bg_collide = ba.newnode( + self.bg_collide = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['collide_bg'], + 'collision_mesh': self.preloaddata['collide_bg'], 'materials': [shared.footing_material, shared.death_material], }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.tint = (0.82, 1.10, 1.15) gnode.ambient_color = (0.9, 1.3, 1.1) gnode.shadow_ortho = False gnode.vignette_outer = (0.76, 0.76, 0.76) gnode.vignette_inner = (0.95, 0.95, 0.99) - def is_point_near_edge(self, point: ba.Vec3, running: bool = False) -> bool: + def is_point_near_edge(self, point: bs.Vec3, running: bool = False) -> bool: xpos = point.x zpos = point.z x_adj = xpos * 0.125 @@ -884,7 +886,7 @@ class DoomShroom(ba.Map): return x_adj * x_adj + z_adj * z_adj > 1.0 -class LakeFrigid(ba.Map): +class LakeFrigid(bs.Map): """An icy lake fit for racing.""" from bastd.mapdata import lake_frigid as defs @@ -903,15 +905,15 @@ class LakeFrigid(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model': ba.getmodel('lakeFrigid'), - 'model_top': ba.getmodel('lakeFrigidTop'), - 'model_reflections': ba.getmodel('lakeFrigidReflections'), - 'collide_model': ba.getcollidemodel('lakeFrigidCollide'), - 'tex': ba.gettexture('lakeFrigid'), - 'tex_reflections': ba.gettexture('lakeFrigidReflections'), - 'vr_fill_model': ba.getmodel('lakeFrigidVRFill'), + 'mesh': bs.getmesh('lakeFrigid'), + 'mesh_top': bs.getmesh('lakeFrigidTop'), + 'mesh_reflections': bs.getmesh('lakeFrigidReflections'), + 'collision_mesh': bs.getcollisionmesh('lakeFrigidCollide'), + 'tex': bs.gettexture('lakeFrigid'), + 'tex_reflections': bs.gettexture('lakeFrigidReflections'), + 'vr_fill_mesh': bs.getmesh('lakeFrigidVRFill'), } - mat = ba.Material() + mat = bs.Material() mat.add_actions(actions=('modify_part_collision', 'friction', 0.01)) data['ice_material'] = mat return data @@ -919,12 +921,12 @@ class LakeFrigid(ba.Map): def __init__(self) -> None: super().__init__() shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'collide_model': self.preloaddata['collide_model'], - 'model': self.preloaddata['model'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], 'color_texture': self.preloaddata['tex'], 'materials': [ shared.footing_material, @@ -932,35 +934,35 @@ class LakeFrigid(ba.Map): ], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['model_top'], + 'mesh': self.preloaddata['mesh_top'], 'lighting': False, 'color_texture': self.preloaddata['tex'], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['model_reflections'], + 'mesh': self.preloaddata['mesh_reflections'], 'lighting': False, 'overlay': True, 'opacity': 0.15, 'color_texture': self.preloaddata['tex_reflections'], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['vr_fill_model'], + 'mesh': self.preloaddata['vr_fill_mesh'], 'lighting': False, 'vr_only': True, 'background': True, 'color_texture': self.preloaddata['tex'], }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.tint = (1, 1, 1) gnode.ambient_color = (1, 1, 1) gnode.shadow_ortho = True @@ -970,7 +972,7 @@ class LakeFrigid(ba.Map): self.is_hockey = True -class TipTop(ba.Map): +class TipTop(bs.Map): """A pointy map good for king-of-the-hill-ish games.""" from bastd.mapdata import tip_top as defs @@ -989,65 +991,65 @@ class TipTop(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model': ba.getmodel('tipTopLevel'), - 'bottom_model': ba.getmodel('tipTopLevelBottom'), - 'collide_model': ba.getcollidemodel('tipTopLevelCollide'), - 'tex': ba.gettexture('tipTopLevelColor'), - 'bgtex': ba.gettexture('tipTopBGColor'), - 'bgmodel': ba.getmodel('tipTopBG'), - 'railing_collide_model': ba.getcollidemodel('tipTopLevelBumper'), + 'mesh': bs.getmesh('tipTopLevel'), + 'bottom_mesh': bs.getmesh('tipTopLevelBottom'), + 'collision_mesh': bs.getcollisionmesh('tipTopLevelCollide'), + 'tex': bs.gettexture('tipTopLevelColor'), + 'bgtex': bs.gettexture('tipTopBGColor'), + 'bgmesh': bs.getmesh('tipTopBG'), + 'railing_collision_mesh': bs.getcollisionmesh('tipTopLevelBumper'), } return data def __init__(self) -> None: super().__init__(vr_overlay_offset=(0, -0.2, 2.5)) shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'collide_model': self.preloaddata['collide_model'], - 'model': self.preloaddata['model'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], 'color_texture': self.preloaddata['tex'], 'color': (0.7, 0.7, 0.7), 'materials': [shared.footing_material], }, ) - self.bottom = ba.newnode( + self.bottom = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bottom_model'], + 'mesh': self.preloaddata['bottom_mesh'], 'lighting': False, 'color': (0.7, 0.7, 0.7), 'color_texture': self.preloaddata['tex'], }, ) - self.background = ba.newnode( + self.background = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bgmodel'], + 'mesh': self.preloaddata['bgmesh'], 'lighting': False, 'color': (0.4, 0.4, 0.4), 'background': True, 'color_texture': self.preloaddata['bgtex'], }, ) - self.railing = ba.newnode( + self.railing = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['railing_collide_model'], + 'collision_mesh': self.preloaddata['railing_collision_mesh'], 'materials': [shared.railing_material], 'bumper': True, }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.tint = (0.8, 0.9, 1.3) gnode.ambient_color = (0.8, 0.9, 1.3) gnode.vignette_outer = (0.79, 0.79, 0.69) gnode.vignette_inner = (0.97, 0.97, 0.99) -class CragCastle(ba.Map): +class CragCastle(bs.Map): """A lovely castle map.""" from bastd.mapdata import crag_castle as defs @@ -1066,17 +1068,17 @@ class CragCastle(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model': ba.getmodel('cragCastleLevel'), - 'bottom_model': ba.getmodel('cragCastleLevelBottom'), - 'collide_model': ba.getcollidemodel('cragCastleLevelCollide'), - 'tex': ba.gettexture('cragCastleLevelColor'), - 'bgtex': ba.gettexture('menuBG'), - 'bgmodel': ba.getmodel('thePadBG'), - 'railing_collide_model': ( - ba.getcollidemodel('cragCastleLevelBumper') + 'mesh': bs.getmesh('cragCastleLevel'), + 'bottom_mesh': bs.getmesh('cragCastleLevelBottom'), + 'collision_mesh': bs.getcollisionmesh('cragCastleLevelCollide'), + 'tex': bs.gettexture('cragCastleLevelColor'), + 'bgtex': bs.gettexture('menuBG'), + 'bgmesh': bs.getmesh('thePadBG'), + 'railing_collision_mesh': ( + bs.getcollisionmesh('cragCastleLevelBumper') ), - 'vr_fill_mound_model': ba.getmodel('cragCastleVRFillMound'), - 'vr_fill_mound_tex': ba.gettexture('vrFillMound'), + 'vr_fill_mound_mesh': bs.getmesh('cragCastleVRFillMound'), + 'vr_fill_mound_tex': bs.gettexture('vrFillMound'), } # fixme should chop this into vr/non-vr sections return data @@ -1084,45 +1086,45 @@ class CragCastle(ba.Map): def __init__(self) -> None: super().__init__() shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'collide_model': self.preloaddata['collide_model'], - 'model': self.preloaddata['model'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], 'color_texture': self.preloaddata['tex'], 'materials': [shared.footing_material], }, ) - self.bottom = ba.newnode( + self.bottom = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bottom_model'], + 'mesh': self.preloaddata['bottom_mesh'], 'lighting': False, 'color_texture': self.preloaddata['tex'], }, ) - self.background = ba.newnode( + self.background = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bgmodel'], + 'mesh': self.preloaddata['bgmesh'], 'lighting': False, 'background': True, 'color_texture': self.preloaddata['bgtex'], }, ) - self.railing = ba.newnode( + self.railing = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['railing_collide_model'], + 'collision_mesh': self.preloaddata['railing_collision_mesh'], 'materials': [shared.railing_material], 'bumper': True, }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['vr_fill_mound_model'], + 'mesh': self.preloaddata['vr_fill_mound_mesh'], 'lighting': False, 'vr_only': True, 'color': (0.2, 0.25, 0.2), @@ -1130,7 +1132,7 @@ class CragCastle(ba.Map): 'color_texture': self.preloaddata['vr_fill_mound_tex'], }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.shadow_ortho = True gnode.shadow_offset = (0, 0, -5.0) gnode.tint = (1.15, 1.05, 0.75) @@ -1140,7 +1142,7 @@ class CragCastle(ba.Map): gnode.vr_near_clip = 1.0 -class TowerD(ba.Map): +class TowerD(bs.Map): """Map used for runaround mini-game.""" from bastd.mapdata import tower_d as defs @@ -1159,21 +1161,23 @@ class TowerD(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model': ba.getmodel('towerDLevel'), - 'model_bottom': ba.getmodel('towerDLevelBottom'), - 'collide_model': ba.getcollidemodel('towerDLevelCollide'), - 'tex': ba.gettexture('towerDLevelColor'), - 'bgtex': ba.gettexture('menuBG'), - 'bgmodel': ba.getmodel('thePadBG'), - 'player_wall_collide_model': ba.getcollidemodel('towerDPlayerWall'), - 'player_wall_material': ba.Material(), + 'mesh': bs.getmesh('towerDLevel'), + 'mesh_bottom': bs.getmesh('towerDLevelBottom'), + 'collision_mesh': bs.getcollisionmesh('towerDLevelCollide'), + 'tex': bs.gettexture('towerDLevelColor'), + 'bgtex': bs.gettexture('menuBG'), + 'bgmesh': bs.getmesh('thePadBG'), + 'player_wall_collision_mesh': bs.getcollisionmesh( + 'towerDPlayerWall' + ), + 'player_wall_material': bs.Material(), } # fixme should chop this into vr/non-vr sections data['player_wall_material'].add_actions( actions=('modify_part_collision', 'friction', 0.0) ) # anything that needs to hit the wall can apply this material - data['collide_with_wall_material'] = ba.Material() + data['collide_with_wall_material'] = bs.Material() data['player_wall_material'].add_actions( conditions=( 'they_dont_have_material', @@ -1181,36 +1185,36 @@ class TowerD(ba.Map): ), actions=('modify_part_collision', 'collide', False), ) - data['vr_fill_mound_model'] = ba.getmodel('stepRightUpVRFillMound') - data['vr_fill_mound_tex'] = ba.gettexture('vrFillMound') + data['vr_fill_mound_mesh'] = bs.getmesh('stepRightUpVRFillMound') + data['vr_fill_mound_tex'] = bs.gettexture('vrFillMound') return data def __init__(self) -> None: super().__init__(vr_overlay_offset=(0, 1, 1)) shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'collide_model': self.preloaddata['collide_model'], - 'model': self.preloaddata['model'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], 'color_texture': self.preloaddata['tex'], 'materials': [shared.footing_material], }, ) - self.node_bottom = ba.newnode( + self.node_bottom = bs.newnode( 'terrain', delegate=self, attrs={ - 'model': self.preloaddata['model_bottom'], + 'mesh': self.preloaddata['mesh_bottom'], 'lighting': False, 'color_texture': self.preloaddata['tex'], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['vr_fill_mound_model'], + 'mesh': self.preloaddata['vr_fill_mound_mesh'], 'lighting': False, 'vr_only': True, 'color': (0.53, 0.57, 0.5), @@ -1218,30 +1222,32 @@ class TowerD(ba.Map): 'color_texture': self.preloaddata['vr_fill_mound_tex'], }, ) - self.background = ba.newnode( + self.background = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bgmodel'], + 'mesh': self.preloaddata['bgmesh'], 'lighting': False, 'background': True, 'color_texture': self.preloaddata['bgtex'], }, ) - self.player_wall = ba.newnode( + self.player_wall = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['player_wall_collide_model'], + 'collision_mesh': self.preloaddata[ + 'player_wall_collision_mesh' + ], 'affect_bg_dynamics': False, 'materials': [self.preloaddata['player_wall_material']], }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.tint = (1.15, 1.11, 1.03) gnode.ambient_color = (1.2, 1.1, 1.0) gnode.vignette_outer = (0.7, 0.73, 0.7) gnode.vignette_inner = (0.95, 0.95, 0.95) - def is_point_near_edge(self, point: ba.Vec3, running: bool = False) -> bool: + def is_point_near_edge(self, point: bs.Vec3, running: bool = False) -> bool: # see if we're within edge_box boxes = self.defs.boxes box_position = boxes['edge_box'][0:3] @@ -1258,7 +1264,7 @@ class TowerD(ba.Map): ) -class HappyThoughts(ba.Map): +class HappyThoughts(bs.Map): """Flying map.""" from bastd.mapdata import happy_thoughts as defs @@ -1283,55 +1289,55 @@ class HappyThoughts(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model': ba.getmodel('alwaysLandLevel'), - 'bottom_model': ba.getmodel('alwaysLandLevelBottom'), - 'bgmodel': ba.getmodel('alwaysLandBG'), - 'collide_model': ba.getcollidemodel('alwaysLandLevelCollide'), - 'tex': ba.gettexture('alwaysLandLevelColor'), - 'bgtex': ba.gettexture('alwaysLandBGColor'), - 'vr_fill_mound_model': ba.getmodel('alwaysLandVRFillMound'), - 'vr_fill_mound_tex': ba.gettexture('vrFillMound'), + 'mesh': bs.getmesh('alwaysLandLevel'), + 'bottom_mesh': bs.getmesh('alwaysLandLevelBottom'), + 'bgmesh': bs.getmesh('alwaysLandBG'), + 'collision_mesh': bs.getcollisionmesh('alwaysLandLevelCollide'), + 'tex': bs.gettexture('alwaysLandLevelColor'), + 'bgtex': bs.gettexture('alwaysLandBGColor'), + 'vr_fill_mound_mesh': bs.getmesh('alwaysLandVRFillMound'), + 'vr_fill_mound_tex': bs.gettexture('vrFillMound'), } return data @classmethod - def get_music_type(cls) -> ba.MusicType: - return ba.MusicType.FLYING + def get_music_type(cls) -> bs.MusicType: + return bs.MusicType.FLYING def __init__(self) -> None: super().__init__(vr_overlay_offset=(0, -3.7, 2.5)) shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'collide_model': self.preloaddata['collide_model'], - 'model': self.preloaddata['model'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], 'color_texture': self.preloaddata['tex'], 'materials': [shared.footing_material], }, ) - self.bottom = ba.newnode( + self.bottom = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bottom_model'], + 'mesh': self.preloaddata['bottom_mesh'], 'lighting': False, 'color_texture': self.preloaddata['tex'], }, ) - self.background = ba.newnode( + self.background = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bgmodel'], + 'mesh': self.preloaddata['bgmesh'], 'lighting': False, 'background': True, 'color_texture': self.preloaddata['bgtex'], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['vr_fill_mound_model'], + 'mesh': self.preloaddata['vr_fill_mound_mesh'], 'lighting': False, 'vr_only': True, 'color': (0.2, 0.25, 0.2), @@ -1339,7 +1345,7 @@ class HappyThoughts(ba.Map): 'color_texture': self.preloaddata['vr_fill_mound_tex'], }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.happy_thoughts_mode = True gnode.shadow_offset = (0.0, 8.0, 5.0) gnode.tint = (1.3, 1.23, 1.0) @@ -1350,10 +1356,10 @@ class HappyThoughts(ba.Map): self.is_flying = True # throw out some tips on flying - txt = ba.newnode( + txt = bs.newnode( 'text', attrs={ - 'text': ba.Lstr(resource='pressJumpToFlyText'), + 'text': bs.Lstr(resource='pressJumpToFlyText'), 'scale': 1.2, 'maxwidth': 800, 'position': (0, 200), @@ -1363,17 +1369,17 @@ class HappyThoughts(ba.Map): 'v_attach': 'bottom', }, ) - cmb = ba.newnode( + cmb = bs.newnode( 'combine', owner=txt, attrs={'size': 4, 'input0': 0.3, 'input1': 0.9, 'input2': 0.0}, ) - ba.animate(cmb, 'input3', {3.0: 0, 4.0: 1, 9.0: 1, 10.0: 0}) + bs.animate(cmb, 'input3', {3.0: 0, 4.0: 1, 9.0: 1, 10.0: 0}) cmb.connectattr('output', txt, 'color') - ba.timer(10.0, txt.delete) + bs.timer(10.0, txt.delete) -class StepRightUp(ba.Map): +class StepRightUp(bs.Map): """Wide stepped map good for CTF or Assault.""" from bastd.mapdata import step_right_up as defs @@ -1392,14 +1398,14 @@ class StepRightUp(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model': ba.getmodel('stepRightUpLevel'), - 'model_bottom': ba.getmodel('stepRightUpLevelBottom'), - 'collide_model': ba.getcollidemodel('stepRightUpLevelCollide'), - 'tex': ba.gettexture('stepRightUpLevelColor'), - 'bgtex': ba.gettexture('menuBG'), - 'bgmodel': ba.getmodel('thePadBG'), - 'vr_fill_mound_model': ba.getmodel('stepRightUpVRFillMound'), - 'vr_fill_mound_tex': ba.gettexture('vrFillMound'), + 'mesh': bs.getmesh('stepRightUpLevel'), + 'mesh_bottom': bs.getmesh('stepRightUpLevelBottom'), + 'collision_mesh': bs.getcollisionmesh('stepRightUpLevelCollide'), + 'tex': bs.gettexture('stepRightUpLevelColor'), + 'bgtex': bs.gettexture('menuBG'), + 'bgmesh': bs.getmesh('thePadBG'), + 'vr_fill_mound_mesh': bs.getmesh('stepRightUpVRFillMound'), + 'vr_fill_mound_tex': bs.gettexture('vrFillMound'), } # fixme should chop this into vr/non-vr chunks return data @@ -1407,29 +1413,29 @@ class StepRightUp(ba.Map): def __init__(self) -> None: super().__init__(vr_overlay_offset=(0, -1, 2)) shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'collide_model': self.preloaddata['collide_model'], - 'model': self.preloaddata['model'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], 'color_texture': self.preloaddata['tex'], 'materials': [shared.footing_material], }, ) - self.node_bottom = ba.newnode( + self.node_bottom = bs.newnode( 'terrain', delegate=self, attrs={ - 'model': self.preloaddata['model_bottom'], + 'mesh': self.preloaddata['mesh_bottom'], 'lighting': False, 'color_texture': self.preloaddata['tex'], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['vr_fill_mound_model'], + 'mesh': self.preloaddata['vr_fill_mound_mesh'], 'lighting': False, 'vr_only': True, 'color': (0.53, 0.57, 0.5), @@ -1437,23 +1443,23 @@ class StepRightUp(ba.Map): 'color_texture': self.preloaddata['vr_fill_mound_tex'], }, ) - self.background = ba.newnode( + self.background = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bgmodel'], + 'mesh': self.preloaddata['bgmesh'], 'lighting': False, 'background': True, 'color_texture': self.preloaddata['bgtex'], }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.tint = (1.2, 1.1, 1.0) gnode.ambient_color = (1.2, 1.1, 1.0) gnode.vignette_outer = (0.7, 0.65, 0.75) gnode.vignette_inner = (0.95, 0.95, 0.93) -class Courtyard(ba.Map): +class Courtyard(bs.Map): """A courtyard-ish looking map for co-op levels.""" from bastd.mapdata import courtyard as defs @@ -1472,23 +1478,23 @@ class Courtyard(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model': ba.getmodel('courtyardLevel'), - 'model_bottom': ba.getmodel('courtyardLevelBottom'), - 'collide_model': ba.getcollidemodel('courtyardLevelCollide'), - 'tex': ba.gettexture('courtyardLevelColor'), - 'bgtex': ba.gettexture('menuBG'), - 'bgmodel': ba.getmodel('thePadBG'), - 'player_wall_collide_model': ( - ba.getcollidemodel('courtyardPlayerWall') + 'mesh': bs.getmesh('courtyardLevel'), + 'mesh_bottom': bs.getmesh('courtyardLevelBottom'), + 'collision_mesh': bs.getcollisionmesh('courtyardLevelCollide'), + 'tex': bs.gettexture('courtyardLevelColor'), + 'bgtex': bs.gettexture('menuBG'), + 'bgmesh': bs.getmesh('thePadBG'), + 'player_wall_collision_mesh': ( + bs.getcollisionmesh('courtyardPlayerWall') ), - 'player_wall_material': ba.Material(), + 'player_wall_material': bs.Material(), } # FIXME: Chop this into vr and non-vr chunks. data['player_wall_material'].add_actions( actions=('modify_part_collision', 'friction', 0.0) ) # anything that needs to hit the wall should apply this. - data['collide_with_wall_material'] = ba.Material() + data['collide_with_wall_material'] = bs.Material() data['player_wall_material'].add_actions( conditions=( 'they_dont_have_material', @@ -1496,44 +1502,44 @@ class Courtyard(ba.Map): ), actions=('modify_part_collision', 'collide', False), ) - data['vr_fill_mound_model'] = ba.getmodel('stepRightUpVRFillMound') - data['vr_fill_mound_tex'] = ba.gettexture('vrFillMound') + data['vr_fill_mound_mesh'] = bs.getmesh('stepRightUpVRFillMound') + data['vr_fill_mound_tex'] = bs.gettexture('vrFillMound') return data def __init__(self) -> None: super().__init__() shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'collide_model': self.preloaddata['collide_model'], - 'model': self.preloaddata['model'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], 'color_texture': self.preloaddata['tex'], 'materials': [shared.footing_material], }, ) - self.background = ba.newnode( + self.background = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bgmodel'], + 'mesh': self.preloaddata['bgmesh'], 'lighting': False, 'background': True, 'color_texture': self.preloaddata['bgtex'], }, ) - self.bottom = ba.newnode( + self.bottom = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['model_bottom'], + 'mesh': self.preloaddata['mesh_bottom'], 'lighting': False, 'color_texture': self.preloaddata['tex'], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['vr_fill_mound_model'], + 'mesh': self.preloaddata['vr_fill_mound_mesh'], 'lighting': False, 'vr_only': True, 'color': (0.53, 0.57, 0.5), @@ -1543,23 +1549,23 @@ class Courtyard(ba.Map): ) # in co-op mode games, put up a wall to prevent players # from getting in the turrets (that would foil our brilliant AI) - if isinstance(ba.getsession(), ba.CoopSession): - cmodel = self.preloaddata['player_wall_collide_model'] - self.player_wall = ba.newnode( + if isinstance(bs.getsession(), bs.CoopSession): + cmesh = self.preloaddata['player_wall_collision_mesh'] + self.player_wall = bs.newnode( 'terrain', attrs={ - 'collide_model': cmodel, + 'collision_mesh': cmesh, 'affect_bg_dynamics': False, 'materials': [self.preloaddata['player_wall_material']], }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.tint = (1.2, 1.17, 1.1) gnode.ambient_color = (1.2, 1.17, 1.1) gnode.vignette_outer = (0.6, 0.6, 0.64) gnode.vignette_inner = (0.95, 0.95, 0.93) - def is_point_near_edge(self, point: ba.Vec3, running: bool = False) -> bool: + def is_point_near_edge(self, point: bs.Vec3, running: bool = False) -> bool: # count anything off our ground level as safe (for our platforms) # see if we're within edge_box box_position = self.defs.boxes['edge_box'][0:3] @@ -1569,7 +1575,7 @@ class Courtyard(ba.Map): return xpos < -0.5 or xpos > 0.5 or zpos < -0.5 or zpos > 0.5 -class Rampage(ba.Map): +class Rampage(bs.Map): """Wee little map with ramps on the sides.""" from bastd.mapdata import rampage as defs @@ -1588,83 +1594,83 @@ class Rampage(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - 'model': ba.getmodel('rampageLevel'), - 'bottom_model': ba.getmodel('rampageLevelBottom'), - 'collide_model': ba.getcollidemodel('rampageLevelCollide'), - 'tex': ba.gettexture('rampageLevelColor'), - 'bgtex': ba.gettexture('rampageBGColor'), - 'bgtex2': ba.gettexture('rampageBGColor2'), - 'bgmodel': ba.getmodel('rampageBG'), - 'bgmodel2': ba.getmodel('rampageBG2'), - 'vr_fill_model': ba.getmodel('rampageVRFill'), - 'railing_collide_model': ba.getcollidemodel('rampageBumper'), + 'mesh': bs.getmesh('rampageLevel'), + 'bottom_mesh': bs.getmesh('rampageLevelBottom'), + 'collision_mesh': bs.getcollisionmesh('rampageLevelCollide'), + 'tex': bs.gettexture('rampageLevelColor'), + 'bgtex': bs.gettexture('rampageBGColor'), + 'bgtex2': bs.gettexture('rampageBGColor2'), + 'bgmesh': bs.getmesh('rampageBG'), + 'bgmesh2': bs.getmesh('rampageBG2'), + 'vr_fill_mesh': bs.getmesh('rampageVRFill'), + 'railing_collision_mesh': bs.getcollisionmesh('rampageBumper'), } return data def __init__(self) -> None: super().__init__(vr_overlay_offset=(0, 0, 2)) shared = SharedObjects.get() - self.node = ba.newnode( + self.node = bs.newnode( 'terrain', delegate=self, attrs={ - 'collide_model': self.preloaddata['collide_model'], - 'model': self.preloaddata['model'], + 'collision_mesh': self.preloaddata['collision_mesh'], + 'mesh': self.preloaddata['mesh'], 'color_texture': self.preloaddata['tex'], 'materials': [shared.footing_material], }, ) - self.background = ba.newnode( + self.background = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bgmodel'], + 'mesh': self.preloaddata['bgmesh'], 'lighting': False, 'background': True, 'color_texture': self.preloaddata['bgtex'], }, ) - self.bottom = ba.newnode( + self.bottom = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bottom_model'], + 'mesh': self.preloaddata['bottom_mesh'], 'lighting': False, 'color_texture': self.preloaddata['tex'], }, ) - self.bg2 = ba.newnode( + self.bg2 = bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['bgmodel2'], + 'mesh': self.preloaddata['bgmesh2'], 'lighting': False, 'background': True, 'color_texture': self.preloaddata['bgtex2'], }, ) - ba.newnode( + bs.newnode( 'terrain', attrs={ - 'model': self.preloaddata['vr_fill_model'], + 'mesh': self.preloaddata['vr_fill_mesh'], 'lighting': False, 'vr_only': True, 'background': True, 'color_texture': self.preloaddata['bgtex2'], }, ) - self.railing = ba.newnode( + self.railing = bs.newnode( 'terrain', attrs={ - 'collide_model': self.preloaddata['railing_collide_model'], + 'collision_mesh': self.preloaddata['railing_collision_mesh'], 'materials': [shared.railing_material], 'bumper': True, }, ) - gnode = ba.getactivity().globalsnode + gnode = bs.getactivity().globalsnode gnode.tint = (1.2, 1.1, 0.97) gnode.ambient_color = (1.3, 1.2, 1.03) gnode.vignette_outer = (0.62, 0.64, 0.69) gnode.vignette_inner = (0.97, 0.95, 0.93) - def is_point_near_edge(self, point: ba.Vec3, running: bool = False) -> bool: + def is_point_near_edge(self, point: bs.Vec3, running: bool = False) -> bool: box_position = self.defs.boxes['edge_box'][0:3] box_scale = self.defs.boxes['edge_box'][6:9] xpos = (point.x - box_position[0]) / box_scale[0] diff --git a/assets/src/ba_data/python/bastd/session/__init__.py b/src/assets/ba_data/python/bastd/session/__init__.py similarity index 100% rename from assets/src/ba_data/python/bastd/session/__init__.py rename to src/assets/ba_data/python/bastd/session/__init__.py diff --git a/assets/src/ba_data/python/bastd/tutorial.py b/src/assets/ba_data/python/bastd/tutorial.py similarity index 90% rename from assets/src/ba_data/python/bastd/tutorial.py rename to src/assets/ba_data/python/bastd/tutorial.py index 3d09d3e8..868a364b 100644 --- a/assets/src/ba_data/python/bastd/tutorial.py +++ b/src/assets/ba_data/python/bastd/tutorial.py @@ -16,18 +16,20 @@ from __future__ import annotations import math +import logging from collections import deque from typing import TYPE_CHECKING -import ba -import ba.internal -from bastd.actor import spaz as basespaz +import bascenev1 as bs +from bastd.actor.spaz import Spaz if TYPE_CHECKING: from typing import Any, Callable, Sequence + import baclassic -def _safesetattr(node: ba.Node | None, attr: str, value: Any) -> None: + +def _safesetattr(node: bs.Node | None, attr: str, value: Any) -> None: if node: setattr(node, attr, value) @@ -48,7 +50,7 @@ class ButtonPress: def run(self, a: TutorialActivity) -> None: s = a.current_spaz assert s is not None - img: ba.Node | None + img: bs.Node | None release_call: Callable[[], None] | None color: Sequence[float] | None if self._button == 'punch': @@ -72,12 +74,12 @@ class ButtonPress: img = a.pickup_image color = a.pickup_image_color elif self._button == 'run': - call = ba.Call(s.on_run, 1.0) - release_call = ba.Call(s.on_run, 0.0) + call = bs.Call(s.on_run, 1.0) + release_call = bs.Call(s.on_run, 0.0) img = None color = None else: - raise Exception(f'invalid button: {self._button}') + raise RuntimeError(f'invalid button: {self._button}') brightness = 4.0 if color is not None: @@ -94,35 +96,31 @@ class ButtonPress: img.color = c_bright img.vr_depth = -40 else: - ba.timer(self._delay, call, timeformat=ba.TimeFormat.MILLISECONDS) + bs.timer(self._delay / 1000.0, call) if img is not None: - ba.timer( - self._delay, - ba.Call(_safesetattr, img, 'color', c_bright), - timeformat=ba.TimeFormat.MILLISECONDS, + bs.timer( + self._delay / 1000.0, + bs.Call(_safesetattr, img, 'color', c_bright), ) - ba.timer( - self._delay, - ba.Call(_safesetattr, img, 'vr_depth', -30), - timeformat=ba.TimeFormat.MILLISECONDS, + bs.timer( + self._delay / 1000.0, + bs.Call(_safesetattr, img, 'vr_depth', -30), ) if self._release: if self._delay == 0 and self._release_delay == 0: release_call() else: - ba.timer( + bs.timer( 0.001 * (self._delay + self._release_delay), release_call ) if img is not None: - ba.timer( - self._delay + self._release_delay + 100, - ba.Call(_safesetattr, img, 'color', color), - timeformat=ba.TimeFormat.MILLISECONDS, + bs.timer( + (self._delay + self._release_delay + 100) / 1000.0, + bs.Call(_safesetattr, img, 'color', color), ) - ba.timer( - self._delay + self._release_delay + 100, - ba.Call(_safesetattr, img, 'vr_depth', -20), - timeformat=ba.TimeFormat.MILLISECONDS, + bs.timer( + (self._delay + self._release_delay + 100) / 1000.0, + bs.Call(_safesetattr, img, 'vr_depth', -20), ) @@ -135,7 +133,7 @@ class ButtonRelease: s = a.current_spaz assert s is not None call: Callable[[], None] | None - img: ba.Node | None + img: bs.Node | None color: Sequence[float] | None if self._button == 'punch': call = s.on_punch_release @@ -154,51 +152,49 @@ class ButtonRelease: img = a.pickup_image color = a.pickup_image_color elif self._button == 'run': - call = ba.Call(s.on_run, 0.0) + call = bs.Call(s.on_run, 0.0) img = None color = None else: - raise Exception('invalid button: ' + self._button) + raise RuntimeError('invalid button: ' + self._button) if self._delay == 0: call() else: - ba.timer(self._delay, call, timeformat=ba.TimeFormat.MILLISECONDS) + bs.timer(self._delay / 1000.0, call) if img is not None: - ba.timer( - self._delay + 100, - ba.Call(_safesetattr, img, 'color', color), - timeformat=ba.TimeFormat.MILLISECONDS, + bs.timer( + (self._delay + 100) / 1000.0, + bs.Call(_safesetattr, img, 'color', color), ) - ba.timer( - self._delay + 100, - ba.Call(_safesetattr, img, 'vr_depth', -20), - timeformat=ba.TimeFormat.MILLISECONDS, + bs.timer( + (self._delay + 100 / 1000.0), + bs.Call(_safesetattr, img, 'vr_depth', -20), ) -class Player(ba.Player['Team']): +class Player(bs.Player['Team']): """Our player type for this game.""" def __init__(self) -> None: self.pressed = False -class Team(ba.Team[Player]): +class Team(bs.Team[Player]): """Our team type for this game.""" def __init__(self) -> None: pass -class TutorialActivity(ba.Activity[Player, Team]): +class TutorialActivity(bs.Activity[Player, Team]): def __init__(self, settings: dict | None = None): from bastd.maps import Rampage if settings is None: settings = {} super().__init__(settings) - self.current_spaz: basespaz.Spaz | None = None - self._benchmark_type = getattr(ba.getsession(), 'benchmark_type', None) + self.current_spaz: Spaz | None = None + self._benchmark_type = getattr(bs.getsession(), 'benchmark_type', None) self.last_start_time: int | None = None self.cycle_times: list[int] = [] self.allow_pausing = True @@ -206,18 +202,18 @@ class TutorialActivity(ba.Activity[Player, Team]): self._issued_warning = False self._map_type = Rampage self._map_type.preload() - self._jump_button_tex = ba.gettexture('buttonJump') - self._pick_up_button_tex = ba.gettexture('buttonPickUp') - self._bomb_button_tex = ba.gettexture('buttonBomb') - self._punch_button_tex = ba.gettexture('buttonPunch') + self._jump_button_tex = bs.gettexture('buttonJump') + self._pick_up_button_tex = bs.gettexture('buttonPickUp') + self._bomb_button_tex = bs.gettexture('buttonBomb') + self._punch_button_tex = bs.gettexture('buttonPunch') self._r = 'tutorial' self._have_skipped = False self.stick_image_position_x = self.stick_image_position_y = 0.0 - self.spawn_sound = ba.getsound('spawn') - self.map: ba.Map | None = None - self.text: ba.Node | None = None - self._skip_text: ba.Node | None = None - self._skip_count_text: ba.Node | None = None + self.spawn_sound = bs.getsound('spawn') + self.map: bs.Map | None = None + self.text: bs.Node | None = None + self._skip_text: bs.Node | None = None + self._skip_count_text: bs.Node | None = None self._scale: float | None = None self._stick_base_position: tuple[float, float] = (0.0, 0.0) self._stick_nub_position: tuple[float, float] = (0.0, 0.0) @@ -225,31 +221,31 @@ class TutorialActivity(ba.Activity[Player, Team]): self._stick_nub_image_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0) self._time: int = -1 self.punch_image_color = (1.0, 1.0, 1.0) - self.punch_image: ba.Node | None = None - self.bomb_image: ba.Node | None = None - self.jump_image: ba.Node | None = None - self.pickup_image: ba.Node | None = None - self._stick_base_image: ba.Node | None = None - self._stick_nub_image: ba.Node | None = None + self.punch_image: bs.Node | None = None + self.bomb_image: bs.Node | None = None + self.jump_image: bs.Node | None = None + self.pickup_image: bs.Node | None = None + self._stick_base_image: bs.Node | None = None + self._stick_nub_image: bs.Node | None = None self.bomb_image_color = (1.0, 1.0, 1.0) self.pickup_image_color = (1.0, 1.0, 1.0) - self.control_ui_nodes: list[ba.Node] = [] - self.spazzes: dict[int, basespaz.Spaz] = {} + self.control_ui_nodes: list[bs.Node] = [] + self.spazzes: dict[int, Spaz] = {} self.jump_image_color = (1.0, 1.0, 1.0) self._entries: deque[Any] = deque() - self._read_entries_timer: ba.Timer | None = None - self._entry_timer: ba.Timer | None = None + self._read_entries_timer: bs.Timer | None = None + self._entry_timer: bs.Timer | None = None def on_transition_in(self) -> None: super().on_transition_in() - ba.setmusic(ba.MusicType.CHAR_SELECT, continuous=True) + bs.setmusic(bs.MusicType.CHAR_SELECT, continuous=True) self.map = self._map_type() def on_begin(self) -> None: super().on_begin() - ba.set_analytics_screen('Tutorial Start') - ba.internal.increment_analytics_count('Tutorial start') + bs.set_analytics_screen('Tutorial Start') + bs.increment_analytics_count('Tutorial start') if bool(False): # Buttons on top. @@ -261,7 +257,7 @@ class TutorialActivity(ba.Activity[Player, Team]): buttons_y = 160 # Need different versions of this: taps/buttons/keys. - self.text = ba.newnode( + self.text = bs.newnode( 'text', attrs={ 'text': '', @@ -278,11 +274,11 @@ class TutorialActivity(ba.Activity[Player, Team]): # Need different versions of this: taps/buttons/keys. txt = ( - ba.Lstr(resource=self._r + '.cpuBenchmarkText') + bs.Lstr(resource=self._r + '.cpuBenchmarkText') if self._benchmark_type == 'cpu' - else ba.Lstr(resource=self._r + '.toSkipPressAnythingText') + else bs.Lstr(resource=self._r + '.toSkipPressAnythingText') ) - t = self._skip_text = ba.newnode( + t = self._skip_text = bs.newnode( 'text', attrs={ 'text': txt, @@ -295,8 +291,8 @@ class TutorialActivity(ba.Activity[Player, Team]): 'v_attach': 'bottom', }, ) - ba.animate(t, 'opacity', {1.0: 0.0, 2.0: 0.7}) - self._skip_count_text = ba.newnode( + bs.animate(t, 'opacity', {1.0: 0.0, 2.0: 0.7}) + self._skip_count_text = bs.newnode( 'text', attrs={ 'text': '', @@ -324,7 +320,7 @@ class TutorialActivity(ba.Activity[Player, Team]): return 0.6 * r, 0.6 * g, 0.6 * b self.jump_image_color = c = _sc(0.4, 1, 0.4) - self.jump_image = ba.newnode( + self.jump_image = bs.newnode( 'image', attrs={ 'texture': self._jump_button_tex, @@ -339,10 +335,10 @@ class TutorialActivity(ba.Activity[Player, Team]): self.punch_image_color = c = ( _sc(0.2, 0.6, 1) if ouya else _sc(1, 0.7, 0.3) ) - self.punch_image = ba.newnode( + self.punch_image = bs.newnode( 'image', attrs={ - 'texture': ba.gettexture('buttonPunch'), + 'texture': bs.gettexture('buttonPunch'), 'absolute_scale': True, 'vr_depth': -20, 'position': p, @@ -352,10 +348,10 @@ class TutorialActivity(ba.Activity[Player, Team]): ) p = (position[0] + center_offs + offs, position[1]) self.bomb_image_color = c = _sc(1, 0.3, 0.3) - self.bomb_image = ba.newnode( + self.bomb_image = bs.newnode( 'image', attrs={ - 'texture': ba.gettexture('buttonBomb'), + 'texture': bs.gettexture('buttonBomb'), 'absolute_scale': True, 'vr_depth': -20, 'position': p, @@ -367,10 +363,10 @@ class TutorialActivity(ba.Activity[Player, Team]): self.pickup_image_color = c = ( _sc(1, 0.8, 0.3) if ouya else _sc(0.5, 0.5, 1) ) - self.pickup_image = ba.newnode( + self.pickup_image = bs.newnode( 'image', attrs={ - 'texture': ba.gettexture('buttonPickUp'), + 'texture': bs.gettexture('buttonPickUp'), 'absolute_scale': True, 'vr_depth': -20, 'position': p, @@ -381,10 +377,10 @@ class TutorialActivity(ba.Activity[Player, Team]): self._stick_base_position = p = (position[0] - center_offs, position[1]) self._stick_base_image_color = c2 = (0.25, 0.25, 0.25, 1.0) - self._stick_base_image = ba.newnode( + self._stick_base_image = bs.newnode( 'image', attrs={ - 'texture': ba.gettexture('nub'), + 'texture': bs.gettexture('nub'), 'absolute_scale': True, 'vr_depth': -40, 'position': p, @@ -394,10 +390,10 @@ class TutorialActivity(ba.Activity[Player, Team]): ) self._stick_nub_position = p = (position[0] - center_offs, position[1]) self._stick_nub_image_color = c3 = (0.4, 0.4, 0.4, 1.0) - self._stick_nub_image = ba.newnode( + self._stick_nub_image = bs.newnode( 'image', attrs={ - 'texture': ba.gettexture('nub'), + 'texture': bs.gettexture('nub'), 'absolute_scale': True, 'position': p, 'scale': (nub_size, nub_size), @@ -417,7 +413,6 @@ class TutorialActivity(ba.Activity[Player, Team]): self._read_entries() def set_stick_image_position(self, x: float, y: float) -> None: - # Clamp this to a circle. len_squared = x * x + y * y if len_squared > 1.0: @@ -458,34 +453,28 @@ class TutorialActivity(ba.Activity[Player, Team]): pass def run(self, a: TutorialActivity) -> None: - # if we're looping, print out how long each cycle took # print out how long each cycle took.. if a.last_start_time is not None: - tval = ( - ba.time( - ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS - ) - - a.last_start_time - ) + tval = int(bs.apptime() * 1000.0) - a.last_start_time assert isinstance(tval, int) diff = tval a.cycle_times.append(diff) - ba.screenmessage( + bs.screenmessage( 'cycle time: ' + str(diff) + ' (average: ' + str(sum(a.cycle_times) / len(a.cycle_times)) + ')' ) - tval = ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) + tval = int(bs.apptime() * 1000.0) assert isinstance(tval, int) a.last_start_time = tval assert a.text a.text.text = '' for spaz in list(a.spazzes.values()): - spaz.handlemessage(ba.DieMessage(immediate=True)) + spaz.handlemessage(bs.DieMessage(immediate=True)) a.spazzes = {} a.current_spaz = None for n in a.control_ui_nodes: @@ -499,7 +488,7 @@ class TutorialActivity(ba.Activity[Player, Team]): def run(self, a: TutorialActivity) -> None: print('setting to', self._speed) - ba.internal.set_debug_speed_exponent(self._speed) + bs.set_debug_speed_exponent(self._speed) class RemoveGloves: def __init__(self) -> None: @@ -529,7 +518,7 @@ class TutorialActivity(ba.Activity[Player, Team]): color: Sequence[float] = (1.0, 1.0, 1.0), make_current: bool = False, relative_to: int | None = None, - name: str | ba.Lstr = '', + name: str | bs.Lstr = '', flash: bool = True, angle: float = 0.0, ): @@ -543,7 +532,6 @@ class TutorialActivity(ba.Activity[Player, Team]): self._angle = angle def run(self, a: TutorialActivity) -> None: - # if they gave a 'relative to' spaz, position is relative # to them pos: Sequence[float] @@ -562,10 +550,10 @@ class TutorialActivity(ba.Activity[Player, Team]): # if there's already a spaz at this spot, insta-kill it if self._num in a.spazzes: a.spazzes[self._num].handlemessage( - ba.DieMessage(immediate=True) + bs.DieMessage(immediate=True) ) - s = a.spazzes[self._num] = basespaz.Spaz( + s = a.spazzes[self._num] = Spaz( color=self._color, start_invincible=self._flash, demo_mode=True, @@ -573,16 +561,16 @@ class TutorialActivity(ba.Activity[Player, Team]): # FIXME: Should extend spaz to support Lstr names. assert s.node - if isinstance(self._name, ba.Lstr): + if isinstance(self._name, bs.Lstr): s.node.name = self._name.evaluate() else: s.node.name = self._name s.node.name_color = self._color - s.handlemessage(ba.StandMessage(pos, self._angle)) + s.handlemessage(bs.StandMessage(pos, self._angle)) if self._make_current: a.current_spaz = s if self._flash: - ba.playsound(a.spawn_sound, position=pos) + a.spawn_sound.play(position=pos) class Powerup: def __init__( @@ -629,7 +617,7 @@ class TutorialActivity(ba.Activity[Player, Team]): self._screen = screen def run(self, a: TutorialActivity) -> None: - ba.set_analytics_screen(self._screen) + bs.set_analytics_screen(self._screen) class DelayOld: def __init__(self, time: int) -> None: @@ -650,7 +638,7 @@ class TutorialActivity(ba.Activity[Player, Team]): pass def run(self, a: TutorialActivity) -> None: - ba.internal.increment_analytics_count('Tutorial finish') + bs.increment_analytics_count('Tutorial finish') a.end() class Move: @@ -797,10 +785,10 @@ class TutorialActivity(ba.Activity[Player, Team]): def run(self, a: TutorialActivity) -> None: for n in a.control_ui_nodes: - ba.animate(n, 'opacity', {0.0: 0.0, 1.0: 1.0}) + bs.animate(n, 'opacity', {0.0: 0.0, 1.0: 1.0}) class Text: - def __init__(self, text: str | ba.Lstr): + def __init__(self, text: str | bs.Lstr): self.text = text def run(self, a: TutorialActivity) -> None: @@ -827,7 +815,7 @@ class TutorialActivity(ba.Activity[Player, Team]): def run(self, a: TutorialActivity) -> None: s = a.current_spaz assert s - s.handlemessage(ba.StandMessage(self._pos, 0)) + s.handlemessage(bs.StandMessage(self._pos, 0)) class Celebrate: def __init__( @@ -853,7 +841,7 @@ class TutorialActivity(ba.Activity[Player, Team]): elif self._celebrate_type == 'both': s.node.handlemessage('celebrate', self._duration) else: - raise Exception( + raise RuntimeError( 'invalid celebrate type ' + self._celebrate_type ) @@ -864,15 +852,15 @@ class TutorialActivity(ba.Activity[Player, Team]): DelayOld(1000), AnalyticsScreen('Tutorial Section 1'), Text( - ba.Lstr(resource=self._r + '.phrase01Text') + bs.Lstr(resource=self._r + '.phrase01Text') ), # hi there Celebrate('left'), DelayOld(2000), Text( - ba.Lstr( + bs.Lstr( resource=self._r + '.phrase02Text', subs=[ - ('${APP_NAME}', ba.Lstr(resource='titleText')) + ('${APP_NAME}', bs.Lstr(resource='titleText')) ], ) ), # welcome to @@ -900,7 +888,7 @@ class TutorialActivity(ba.Activity[Player, Team]): MoveUD(0), DelayOld(1500), Text( - ba.Lstr(resource=self._r + '.phrase03Text') + bs.Lstr(resource=self._r + '.phrase03Text') ), # here's a few tips DelayOld(1000), ShowControls(), @@ -911,10 +899,10 @@ class TutorialActivity(ba.Activity[Player, Team]): DelayOld(1000), AnalyticsScreen('Tutorial Section 2'), Text( - ba.Lstr( + bs.Lstr( resource=self._r + '.phrase04Text', subs=[ - ('${APP_NAME}', ba.Lstr(resource='titleText')) + ('${APP_NAME}', bs.Lstr(resource='titleText')) ], ) ), # many things are based on physics @@ -1273,7 +1261,7 @@ class TutorialActivity(ba.Activity[Player, Team]): Move(0, 0), DelayOld(1000), Text( - ba.Lstr(resource=self._r + '.phrase05Text') + bs.Lstr(resource=self._r + '.phrase05Text') ), # for example when you punch.. DelayOld(510), Move(0, -0.01), @@ -1291,7 +1279,7 @@ class TutorialActivity(ba.Activity[Player, Team]): (-3.1, 4.3, -2.0), make_current=False, color=(1, 1, 0.4), - name=ba.Lstr(resource=self._r + '.randomName1Text'), + name=bs.Lstr(resource=self._r + '.randomName1Text'), ), Move(-1.0, 0), DelayOld(1050), @@ -1300,7 +1288,7 @@ class TutorialActivity(ba.Activity[Player, Team]): Move(0, 0), DelayOld(1000), Text( - ba.Lstr(resource=self._r + '.phrase06Text') + bs.Lstr(resource=self._r + '.phrase06Text') ), # your damage is based DelayOld(1200), Move(-0.05, 0), @@ -1315,12 +1303,12 @@ class TutorialActivity(ba.Activity[Player, Team]): DelayOld(100), Move(0, 0), Text( - ba.Lstr( + bs.Lstr( resource=self._r + '.phrase07Text', subs=[ ( '${NAME}', - ba.Lstr( + bs.Lstr( resource=self._r + '.randomName1Text' ), ) @@ -1331,7 +1319,7 @@ class TutorialActivity(ba.Activity[Player, Team]): Celebrate('right', spaz_num=1), DelayOld(1400), Text( - ba.Lstr(resource=self._r + '.phrase08Text') + bs.Lstr(resource=self._r + '.phrase08Text') ), # lets jump and spin to get more speed DelayOld(30), MoveLR(0), @@ -1532,12 +1520,12 @@ class TutorialActivity(ba.Activity[Player, Team]): Move(0, 0), DelayOld(1000), Text( - ba.Lstr(resource=self._r + '.phrase09Text') + bs.Lstr(resource=self._r + '.phrase09Text') ), # ah that's better DelayOld(1900), AnalyticsScreen('Tutorial Section 3'), Text( - ba.Lstr(resource=self._r + '.phrase10Text') + bs.Lstr(resource=self._r + '.phrase10Text') ), # running also helps DelayOld(100), SpawnSpaz( @@ -1548,11 +1536,11 @@ class TutorialActivity(ba.Activity[Player, Team]): (3.3, 4.2, -5.8), make_current=False, color=(0.9, 0.5, 1.0), - name=ba.Lstr(resource=self._r + '.randomName2Text'), + name=bs.Lstr(resource=self._r + '.randomName2Text'), ), DelayOld(1800), Text( - ba.Lstr(resource=self._r + '.phrase11Text') + bs.Lstr(resource=self._r + '.phrase11Text') ), # hold ANY button to run DelayOld(300), MoveUD(0), @@ -1809,7 +1797,7 @@ class TutorialActivity(ba.Activity[Player, Team]): MoveUD(0), AnalyticsScreen('Tutorial Section 4'), Text( - ba.Lstr(resource=self._r + '.phrase12Text') + bs.Lstr(resource=self._r + '.phrase12Text') ), # for extra-awesome punches,... DelayOld(200), SpawnSpaz( @@ -1828,7 +1816,7 @@ class TutorialActivity(ba.Activity[Player, Team]): make_current=False, color=(1.0, 0.7, 0.3), # name=R.randomName3Text), - name=ba.Lstr(resource=self._r + '.randomName3Text'), + name=bs.Lstr(resource=self._r + '.randomName3Text'), ), DelayOld(100), Powerup(1, (2.5, 0.0, 0), relative_to=0), @@ -2026,12 +2014,12 @@ class TutorialActivity(ba.Activity[Player, Team]): DelayOld(750), MoveLR(0), Text( - ba.Lstr( + bs.Lstr( resource=self._r + '.phrase13Text', subs=[ ( '${NAME}', - ba.Lstr( + bs.Lstr( resource=self._r + '.randomName3Text' ), ) @@ -2042,12 +2030,12 @@ class TutorialActivity(ba.Activity[Player, Team]): DelayOld(2000), AnalyticsScreen('Tutorial Section 5'), Text( - ba.Lstr( + bs.Lstr( resource=self._r + '.phrase14Text', subs=[ ( '${NAME}', - ba.Lstr( + bs.Lstr( resource=self._r + '.randomName4Text' ), ) @@ -2067,7 +2055,7 @@ class TutorialActivity(ba.Activity[Player, Team]): relative_to=0, make_current=False, color=(0.4, 1.0, 0.7), - name=ba.Lstr(resource=self._r + '.randomName4Text'), + name=bs.Lstr(resource=self._r + '.randomName4Text'), ), DelayOld(1000), Celebrate('left', 1, duration=1000), @@ -2095,11 +2083,11 @@ class TutorialActivity(ba.Activity[Player, Team]): ), AnalyticsScreen('Tutorial Section 6'), Text( - ba.Lstr(resource=self._r + '.phrase15Text') + bs.Lstr(resource=self._r + '.phrase15Text') ), # lastly there's bombs DelayOld(1900), Text( - ba.Lstr(resource=self._r + '.phrase16Text') + bs.Lstr(resource=self._r + '.phrase16Text') ), # throwing bombs takes practice DelayOld(2000), Bomb(), @@ -2111,11 +2099,11 @@ class TutorialActivity(ba.Activity[Player, Team]): Bomb(), DelayOld(2000), Text( - ba.Lstr(resource=self._r + '.phrase17Text') + bs.Lstr(resource=self._r + '.phrase17Text') ), # not a very good throw DelayOld(3000), Text( - ba.Lstr(resource=self._r + '.phrase18Text') + bs.Lstr(resource=self._r + '.phrase18Text') ), # moving helps you get distance DelayOld(1000), Bomb(), @@ -2133,7 +2121,7 @@ class TutorialActivity(ba.Activity[Player, Team]): Move(0, 0), DelayOld(2500), Text( - ba.Lstr(resource=self._r + '.phrase19Text') + bs.Lstr(resource=self._r + '.phrase19Text') ), # jumping helps you get height DelayOld(2000), Bomb(), @@ -2153,7 +2141,7 @@ class TutorialActivity(ba.Activity[Player, Team]): Move(0, 0), DelayOld(2000), Text( - ba.Lstr(resource=self._r + '.phrase20Text') + bs.Lstr(resource=self._r + '.phrase20Text') ), # whiplash your bombs DelayOld(1000), Bomb(release=False), @@ -2315,7 +2303,7 @@ class TutorialActivity(ba.Activity[Player, Team]): DelayOld(2000), AnalyticsScreen('Tutorial Section 7'), Text( - ba.Lstr(resource=self._r + '.phrase21Text') + bs.Lstr(resource=self._r + '.phrase21Text') ), # timing your bombs can be tricky Move(-1, 0), DelayOld(1000), @@ -2335,7 +2323,7 @@ class TutorialActivity(ba.Activity[Player, Team]): relative_to=0, make_current=False, color=(0.3, 0.8, 1.0), - name=ba.Lstr(resource=self._r + '.randomName5Text'), + name=bs.Lstr(resource=self._r + '.randomName5Text'), ), DelayOld2(1000), Move(-1, 0), @@ -2353,12 +2341,12 @@ class TutorialActivity(ba.Activity[Player, Team]): DelayOld2(1000), Move(0, 0), DelayOld2(1500), - Text(ba.Lstr(resource=self._r + '.phrase22Text')), # dang + Text(bs.Lstr(resource=self._r + '.phrase22Text')), # dang Delay(1500), Text(''), Delay(200), Text( - ba.Lstr(resource=self._r + '.phrase23Text') + bs.Lstr(resource=self._r + '.phrase23Text') ), # try cooking off Delay(1500), Bomb(), @@ -2374,7 +2362,7 @@ class TutorialActivity(ba.Activity[Player, Team]): Move(0, 0), Delay(2000), Text( - ba.Lstr(resource=self._r + '.phrase24Text') + bs.Lstr(resource=self._r + '.phrase24Text') ), # hooray nicely cooked Celebrate(), DelayOld(2000), @@ -2388,23 +2376,23 @@ class TutorialActivity(ba.Activity[Player, Team]): DelayOld(1000), AnalyticsScreen('Tutorial Section 8'), Text( - ba.Lstr(resource=self._r + '.phrase25Text') + bs.Lstr(resource=self._r + '.phrase25Text') ), # well that's just about it DelayOld(2000), Text( - ba.Lstr(resource=self._r + '.phrase26Text') + bs.Lstr(resource=self._r + '.phrase26Text') ), # go get em tiger DelayOld(2000), Text( - ba.Lstr(resource=self._r + '.phrase27Text') + bs.Lstr(resource=self._r + '.phrase27Text') ), # remember you training DelayOld(3000), Text( - ba.Lstr(resource=self._r + '.phrase28Text') + bs.Lstr(resource=self._r + '.phrase28Text') ), # well maybe DelayOld(1600), Text( - ba.Lstr(resource=self._r + '.phrase29Text') + bs.Lstr(resource=self._r + '.phrase29Text') ), # good luck Celebrate('right', duration=10000), DelayOld(1000), @@ -2414,47 +2402,44 @@ class TutorialActivity(ba.Activity[Player, Team]): ) except Exception: - ba.print_exception() + logging.exception('Error running tutorial.') # If we read some, exec them. if self._entries: self._run_next_entry() # Otherwise try again in a few seconds. else: - self._read_entries_timer = ba.Timer( - 3.0, ba.WeakCall(self._read_entries) + self._read_entries_timer = bs.Timer( + 3.0, bs.WeakCall(self._read_entries) ) def _run_next_entry(self) -> None: - while self._entries: entry = self._entries.popleft() try: result = entry.run(self) except Exception: result = None - ba.print_exception() + logging.exception('Error in tutorial _run_next_entry.') # If the entry returns an int value, set a timer; # otherwise just keep going. if result is not None: - self._entry_timer = ba.Timer( - result, - ba.WeakCall(self._run_next_entry), - timeformat=ba.TimeFormat.MILLISECONDS, + self._entry_timer = bs.Timer( + result / 1000.0, bs.WeakCall(self._run_next_entry) ) return # Done with these entries.. start over soon. - self._read_entries_timer = ba.Timer( - 1.0, ba.WeakCall(self._read_entries) + self._read_entries_timer = bs.Timer( + 1.0, bs.WeakCall(self._read_entries) ) def _update_skip_votes(self) -> None: count = sum(1 for player in self.players if player.pressed) assert self._skip_count_text self._skip_count_text.text = ( - ba.Lstr( + bs.Lstr( resource=self._r + '.skipVoteCountText', subs=[ ('${COUNT}', str(count)), @@ -2469,12 +2454,12 @@ class TutorialActivity(ba.Activity[Player, Team]): and self.players and not self._have_skipped ): - ba.internal.increment_analytics_count('Tutorial skip') - ba.set_analytics_screen('Tutorial Skip') + bs.increment_analytics_count('Tutorial skip') + bs.set_analytics_screen('Tutorial Skip') self._have_skipped = True - ba.playsound(ba.getsound('swish')) + bs.getsound('swish').play() # self._skip_count_text.text = self._r.skippingText - self._skip_count_text.text = ba.Lstr( + self._skip_count_text.text = bs.Lstr( resource=self._r + '.skippingText' ) assert self._skip_text @@ -2482,14 +2467,13 @@ class TutorialActivity(ba.Activity[Player, Team]): self.end() def _player_pressed_button(self, player: Player) -> None: - # Special case: if there's only one player, we give them a # warning on their first press (some players were thinking the # on-screen guide meant they were supposed to press something). if len(self.players) == 1 and not self._issued_warning: self._issued_warning = True assert self._skip_text - self._skip_text.text = ba.Lstr( + self._skip_text.text = bs.Lstr( resource=self._r + '.skipConfirmText' ) self._skip_text.color = (1, 1, 1) @@ -2497,37 +2481,35 @@ class TutorialActivity(ba.Activity[Player, Team]): incr = 50 t = incr for _i in range(6): - ba.timer( - t, - ba.Call(setattr, self._skip_text, 'color', (1, 0.5, 0.1)), - timeformat=ba.TimeFormat.MILLISECONDS, + bs.timer( + t / 1000.0, + bs.Call(setattr, self._skip_text, 'color', (1, 0.5, 0.1)), ) t += incr - ba.timer( - t, - ba.Call(setattr, self._skip_text, 'color', (1, 1, 0)), - timeformat=ba.TimeFormat.MILLISECONDS, + bs.timer( + t / 1000.0, + bs.Call(setattr, self._skip_text, 'color', (1, 1, 0)), ) t += incr - ba.timer(6.0, ba.WeakCall(self._revert_confirm)) + bs.timer(6.0, bs.WeakCall(self._revert_confirm)) return player.pressed = True # test... if not all(self.players): - ba.print_error( - 'Nonexistent player in _player_pressed_button: ' - + str([str(p) for p in self.players]) - + ': we are ' - + str(player) + logging.error( + 'Nonexistent player in _player_pressed_button:' + ' %s: we are %s', + [str(p) for p in self.players], + player, ) self._update_skip_votes() def _revert_confirm(self) -> None: assert self._skip_text - self._skip_text.text = ba.Lstr( + self._skip_text.text = bs.Lstr( resource=self._r + '.toSkipPressAnythingText' ) self._skip_text.color = (1, 1, 1) @@ -2539,21 +2521,20 @@ class TutorialActivity(ba.Activity[Player, Team]): # We just wanna know if this player presses anything. player.assigninput( ( - ba.InputType.JUMP_PRESS, - ba.InputType.PUNCH_PRESS, - ba.InputType.BOMB_PRESS, - ba.InputType.PICK_UP_PRESS, + bs.InputType.JUMP_PRESS, + bs.InputType.PUNCH_PRESS, + bs.InputType.BOMB_PRESS, + bs.InputType.PICK_UP_PRESS, ), - ba.Call(self._player_pressed_button, player), + bs.Call(self._player_pressed_button, player), ) def on_player_leave(self, player: Player) -> None: if not all(self.players): - ba.print_error( - 'Nonexistent player in on_player_leave: ' - + str([str(p) for p in self.players]) - + ': we are ' - + str(player) + logging.error( + 'Nonexistent player in on_player_leave: %s: we are %s', + [str(p) for p in self.players], + player, ) super().on_player_leave(player) # our leaving may influence the vote total needed/etc diff --git a/assets/src/ba_data/python/bastd/ui/__init__.py b/src/assets/ba_data/python/bastd/ui/__init__.py similarity index 100% rename from assets/src/ba_data/python/bastd/ui/__init__.py rename to src/assets/ba_data/python/bastd/ui/__init__.py diff --git a/assets/src/ba_data/python/bastd/ui/account/__init__.py b/src/assets/ba_data/python/bastd/ui/account/__init__.py similarity index 59% rename from assets/src/ba_data/python/bastd/ui/account/__init__.py rename to src/assets/ba_data/python/bastd/ui/account/__init__.py index b4ea7e2b..6e4b99dc 100644 --- a/assets/src/ba_data/python/bastd/ui/account/__init__.py +++ b/src/assets/ba_data/python/bastd/ui/account/__init__.py @@ -4,30 +4,35 @@ from __future__ import annotations -import ba +import bauiv1 as bui def show_sign_in_prompt(account_type: str | None = None) -> None: """Bring up a prompt telling the user they must sign in.""" from bastd.ui.confirm import ConfirmWindow from bastd.ui.account import settings - from ba.internal import sign_in_v1 if account_type == 'Google Play': + + def _do_sign_in() -> None: + plus = bui.app.plus + assert plus is not None + plus.sign_in_v1('Google Play') + ConfirmWindow( - ba.Lstr(resource='notSignedInGooglePlayErrorText'), - lambda: sign_in_v1('Google Play'), - ok_text=ba.Lstr(resource='accountSettingsWindow.signInText'), + bui.Lstr(resource='notSignedInGooglePlayErrorText'), + _do_sign_in, + ok_text=bui.Lstr(resource='accountSettingsWindow.signInText'), width=460, height=130, ) else: ConfirmWindow( - ba.Lstr(resource='notSignedInErrorText'), + bui.Lstr(resource='notSignedInErrorText'), lambda: settings.AccountSettingsWindow( modal=True, close_once_signed_in=True ), - ok_text=ba.Lstr(resource='accountSettingsWindow.signInText'), + ok_text=bui.Lstr(resource='accountSettingsWindow.signInText'), width=460, height=130, ) diff --git a/assets/src/ba_data/python/bastd/ui/account/link.py b/src/assets/ba_data/python/bastd/ui/account/link.py similarity index 71% rename from assets/src/ba_data/python/bastd/ui/account/link.py rename to src/assets/ba_data/python/bastd/ui/account/link.py index 7309a797..47c17f31 100644 --- a/assets/src/ba_data/python/bastd/ui/account/link.py +++ b/src/assets/ba_data/python/bastd/ui/account/link.py @@ -8,17 +8,19 @@ import copy import time from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any -class AccountLinkWindow(ba.Window): +class AccountLinkWindow(bui.Window): """Window for linking accounts.""" - def __init__(self, origin_widget: ba.Widget | None = None): + def __init__(self, origin_widget: bui.Widget | None = None): + plus = bui.app.plus + assert plus is not None + scale_origin: tuple[float, float] | None if origin_widget is not None: self._transition_out = 'out_scale' @@ -31,26 +33,27 @@ class AccountLinkWindow(ba.Window): bg_color = (0.4, 0.4, 0.5) self._width = 560 self._height = 420 - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale base_scale = ( 1.65 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.5 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.1 ) super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), transition=transition, scale=base_scale, scale_origin_stack_offset=scale_origin, stack_offset=(0, -10) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self._root_widget, position=(40, self._height - 45), size=(50, 50), @@ -59,46 +62,44 @@ class AccountLinkWindow(ba.Window): color=bg_color, on_activate_call=self._cancel, autoselect=True, - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) - maxlinks = ba.internal.get_v1_account_misc_read_val( - 'maxLinkAccounts', 5 - ) - ba.textwidget( + maxlinks = plus.get_v1_account_misc_read_val('maxLinkAccounts', 5) + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.56), size=(0, 0), - text=ba.Lstr( + text=bui.Lstr( resource=( 'accountSettingsWindow.linkAccountsInstructionsNewText' ), subs=[('${COUNT}', str(maxlinks))], ), maxwidth=self._width * 0.9, - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, max_height=self._height * 0.6, h_align='center', v_align='center', ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._cancel_button ) - ba.buttonwidget( + bui.buttonwidget( parent=self._root_widget, position=(40, 30), size=(200, 60), - label=ba.Lstr( + label=bui.Lstr( resource='accountSettingsWindow.linkAccountsGenerateCodeText' ), autoselect=True, on_activate_call=self._generate_press, ) - self._enter_code_button = ba.buttonwidget( + self._enter_code_button = bui.buttonwidget( parent=self._root_widget, position=(self._width - 240, 30), size=(200, 60), - label=ba.Lstr( + label=bui.Lstr( resource='accountSettingsWindow.linkAccountsEnterCodeText' ), autoselect=True, @@ -108,20 +109,23 @@ class AccountLinkWindow(ba.Window): def _generate_press(self) -> None: from bastd.ui import account - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return - ba.screenmessage( - ba.Lstr(resource='gatherWindow.requestingAPromoCodeText'), + bui.screenmessage( + bui.Lstr(resource='gatherWindow.requestingAPromoCodeText'), color=(0, 1, 0), ) - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'ACCOUNT_LINK_CODE_REQUEST', 'expire_time': time.time() + 5, } ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() def _enter_code_press(self) -> None: from bastd.ui import promocode @@ -129,41 +133,42 @@ class AccountLinkWindow(ba.Window): promocode.PromoCodeWindow( modal=True, origin_widget=self._enter_code_button ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) def _cancel(self) -> None: - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) -class AccountLinkCodeWindow(ba.Window): +class AccountLinkCodeWindow(bui.Window): """Window showing code for account-linking.""" def __init__(self, data: dict[str, Any]): self._width = 350 self._height = 200 - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), color=(0.45, 0.63, 0.15), transition='in_scale', scale=( 1.8 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.35 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), ) ) self._data = copy.deepcopy(data) - ba.playsound(ba.getsound('cashRegister')) - ba.playsound(ba.getsound('swish')) - self._cancel_button = ba.buttonwidget( + bui.getsound('cashRegister').play() + bui.getsound('swish').play() + self._cancel_button = bui.buttonwidget( parent=self._root_widget, scale=0.5, position=(40, self._height - 40), @@ -172,13 +177,13 @@ class AccountLinkCodeWindow(ba.Window): on_activate_call=self.close, autoselect=True, color=(0.45, 0.63, 0.15), - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._cancel_button ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.5), size=(0, 0), @@ -192,4 +197,4 @@ class AccountLinkCodeWindow(ba.Window): def close(self) -> None: """close the window""" - ba.containerwidget(edit=self._root_widget, transition='out_scale') + bui.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/assets/src/ba_data/python/bastd/ui/account/settings.py b/src/assets/ba_data/python/bastd/ui/account/settings.py similarity index 70% rename from assets/src/ba_data/python/bastd/ui/account/settings.py rename to src/assets/ba_data/python/bastd/ui/account/settings.py index 214f8c92..64f7f281 100644 --- a/assets/src/ba_data/python/bastd/ui/account/settings.py +++ b/src/assets/ba_data/python/bastd/ui/account/settings.py @@ -7,15 +7,10 @@ from __future__ import annotations import time import logging -from typing import TYPE_CHECKING -import bacommon.cloud from bacommon.login import LoginType -import ba -import ba.internal - -if TYPE_CHECKING: - from ba.internal import LoginAdapter +import bacommon.cloud +import bauiv1 as bui # These days we're directing people to the web based account settings # for V2 account linking and trying to get them to disconnect remaining @@ -23,26 +18,29 @@ if TYPE_CHECKING: FORCE_ENABLE_V1_LINKING = False -class AccountSettingsWindow(ba.Window): +class AccountSettingsWindow(bui.Window): """Window for account related functionality.""" def __init__( self, transition: str = 'in_right', modal: bool = False, - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, close_once_signed_in: bool = False, ): # pylint: disable=too-many-statements - self._sign_in_v2_proxy_button: ba.Widget | None = None - self._sign_in_device_button: ba.Widget | None = None + plus = bui.app.plus + assert plus is not None + + self._sign_in_v2_proxy_button: bui.Widget | None = None + self._sign_in_device_button: bui.Widget | None = None self._show_legacy_unlink_button = False - self._signing_in_adapter: LoginAdapter | None = None + self._signing_in_adapter: bui.LoginAdapter | None = None self._close_once_signed_in = close_once_signed_in - ba.set_analytics_screen('Account Window') + bui.set_analytics_screen('Account Window') self._explicitly_signed_out_of_gpgs = False @@ -59,33 +57,31 @@ class AccountSettingsWindow(ba.Window): self._r = 'accountSettingsWindow' self._modal = modal self._needs_refresh = False - self._v1_signed_in = ba.internal.get_v1_account_state() == 'signed_in' - self._v1_account_state_num = ba.internal.get_v1_account_state_num() - self._check_sign_in_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - timetype=ba.TimeType.REAL, - repeat=True, + self._v1_signed_in = plus.get_v1_account_state() == 'signed_in' + self._v1_account_state_num = plus.get_v1_account_state_num() + self._check_sign_in_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) # Currently we can only reset achievements on game-center. v1_account_type: str | None if self._v1_signed_in: - v1_account_type = ba.internal.get_v1_account_type() + v1_account_type = plus.get_v1_account_type() else: v1_account_type = None self._can_reset_achievements = v1_account_type == 'Game Center' - app = ba.app - uiscale = app.ui.uiscale + app = bui.app + assert app.classic is not None + uiscale = app.classic.ui.uiscale - self._width = 760 if uiscale is ba.UIScale.SMALL else 660 - x_offs = 50 if uiscale is ba.UIScale.SMALL else 0 + self._width = 760 if uiscale is bui.UIScale.SMALL else 660 + x_offs = 50 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 390 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 430 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 490 ) @@ -99,7 +95,7 @@ class AccountSettingsWindow(ba.Window): # Determine which sign-in/sign-out buttons we should show. self._show_sign_in_buttons: list[str] = [] - if LoginType.GPGS in ba.app.accounts_v2.login_adapters: + if LoginType.GPGS in bui.app.accounts.login_adapters: self._show_sign_in_buttons.append('Google Play') # Always want to show our web-based v2 login option. @@ -109,65 +105,65 @@ class AccountSettingsWindow(ba.Window): # (though we need to start phasing them out at some point). self._show_sign_in_buttons.append('Device') - top_extra = 15 if uiscale is ba.UIScale.SMALL else 0 + top_extra = 15 if uiscale is bui.UIScale.SMALL else 0 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), transition=transition, toolbar_visibility='menu_minimal', scale_origin_stack_offset=scale_origin, scale=( 2.09 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.4 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -19) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: + if uiscale is bui.UIScale.SMALL and app.classic.ui.use_toolbars: self._back_button = None - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, on_cancel_call=self._back ) else: - self._back_button = btn = ba.buttonwidget( + self._back_button = btn = bui.buttonwidget( parent=self._root_widget, position=(51 + x_offs, self._height - 62), size=(120, 60), scale=0.8, text_scale=1.2, autoselect=True, - label=ba.Lstr( + label=bui.Lstr( resource='doneText' if self._modal else 'backText' ), button_type='regular' if self._modal else 'back', on_activate_call=self._back, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) if not self._modal: - ba.buttonwidget( + bui.buttonwidget( edit=btn, button_type='backSmall', size=(60, 56), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 41), size=(0, 0), - text=ba.Lstr(resource=self._r + '.titleText'), - color=ba.app.ui.title_color, + text=bui.Lstr(resource=self._r + '.titleText'), + color=app.classic.ui.title_color, maxwidth=self._width - 340, h_align='center', v_align='center', ) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, highlight=False, position=( @@ -179,11 +175,13 @@ class AccountSettingsWindow(ba.Window): claims_tab=True, selection_loops_to_parent=True, ) - self._subcontainer: ba.Widget | None = None + self._subcontainer: bui.Widget | None = None self._refresh() self._restore_state() def _update(self) -> None: + plus = bui.app.plus + assert plus is not None # If they want us to close once we're signed in, do so. if self._close_once_signed_in and self._v1_signed_in: @@ -193,8 +191,8 @@ class AccountSettingsWindow(ba.Window): # Hmm should update this to use get_account_state_num. # Theoretically if we switch from one signed-in account to another # in the background this would break. - v1_account_state_num = ba.internal.get_v1_account_state_num() - v1_account_state = ba.internal.get_v1_account_state() + v1_account_state_num = plus.get_v1_account_state_num() + v1_account_state = plus.get_v1_account_state() show_legacy_unlink_button = self._should_show_legacy_unlink_button() if ( @@ -216,8 +214,8 @@ class AccountSettingsWindow(ba.Window): self._refresh_tickets_text() self._refresh_account_name_text() - def _get_sign_in_text(self) -> ba.Lstr: - return ba.Lstr(resource=self._r + '.signInText') + def _get_sign_in_text(self) -> bui.Lstr: + return bui.Lstr(resource=self._r + '.signInText') def _refresh(self) -> None: # pylint: disable=too-many-statements @@ -226,19 +224,20 @@ class AccountSettingsWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui import confirm - primary_v2_account = ba.app.accounts_v2.primary + plus = bui.app.plus + assert plus is not None - v1_state = ba.internal.get_v1_account_state() + primary_v2_account = bui.app.accounts.primary + + v1_state = plus.get_v1_account_state() v1_account_type = ( - ba.internal.get_v1_account_type() - if v1_state == 'signed_in' - else 'unknown' + plus.get_v1_account_type() if v1_state == 'signed_in' else 'unknown' ) # We expose GPGS-specific functionality only if it is 'active' # (meaning the current GPGS player matches one of our account's # logins). - gpgs_adapter = ba.app.accounts_v2.login_adapters.get(LoginType.GPGS) + gpgs_adapter = bui.app.accounts.login_adapters.get(LoginType.GPGS) is_gpgs = ( False if gpgs_adapter is None else gpgs_adapter.is_back_end_active() ) @@ -340,7 +339,7 @@ class AccountSettingsWindow(ba.Window): # provide us with v2 credentials or waiting for those credentials # to be verified. show_cancel_sign_in_button = self._signing_in_adapter is not None or ( - ba.app.accounts_v2.have_primary_credentials() + bui.app.accounts.have_primary_credentials() and primary_v2_account is None ) cancel_sign_in_button_space = 70.0 @@ -392,7 +391,7 @@ class AccountSettingsWindow(ba.Window): self._sub_height += sign_out_button_space if show_cancel_sign_in_button: self._sub_height += cancel_sign_in_button_space - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._sub_width, self._sub_height), background=False, @@ -404,28 +403,29 @@ class AccountSettingsWindow(ba.Window): first_selectable = None v = self._sub_height - 10.0 - self._account_name_what_is_text: ba.Widget | None + assert bui.app.classic is not None + self._account_name_what_is_text: bui.Widget | None self._account_name_what_is_y = 0.0 - self._account_name_text: ba.Widget | None + self._account_name_text: bui.Widget | None if show_signed_in_as: v -= signed_in_as_space * 0.2 - txt = ba.Lstr( + txt = bui.Lstr( resource='accountSettingsWindow.youAreSignedInAsText', fallback_resource='accountSettingsWindow.youAreLoggedInAsText', ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v), size=(0, 0), text=txt, scale=0.9, - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, maxwidth=self._sub_width * 0.9, h_align='center', v_align='center', ) v -= signed_in_as_space * 0.5 - self._account_name_text = ba.textwidget( + self._account_name_text = bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v), size=(0, 0), @@ -439,13 +439,13 @@ class AccountSettingsWindow(ba.Window): if show_what_is_v2: self._account_name_what_is_y = v - 23.0 - self._account_name_what_is_text = ba.textwidget( + self._account_name_what_is_text = bui.textwidget( parent=self._subcontainer, position=(0.0, self._account_name_what_is_y), size=(200.0, 60), - text=ba.Lstr( + text=bui.Lstr( value='${WHAT} -->', - subs=[('${WHAT}', ba.Lstr(resource='whatIsThisText'))], + subs=[('${WHAT}', bui.Lstr(resource='whatIsThisText'))], ), scale=0.6, color=(0.3, 0.7, 0.05), @@ -471,35 +471,42 @@ class AccountSettingsWindow(ba.Window): self._account_name_what_is_text = None if self._back_button is None: - bbtn = ba.internal.get_special_widget('back_button') + bbtn = bui.get_special_widget('back_button') else: bbtn = self._back_button if show_sign_in_benefits: v -= sign_in_benefits_space - app = ba.app - extra: str | ba.Lstr | None - if app.platform in ['mac', 'ios'] and app.subplatform == 'appstore': - extra = ba.Lstr( + app = bui.app + assert app.classic is not None + extra: str | bui.Lstr | None + if ( + app.classic.platform in ['mac', 'ios'] + and app.classic.subplatform == 'appstore' + ): + extra = bui.Lstr( value='\n${S}', subs=[ - ('${S}', ba.Lstr(resource='signInWithGameCenterText')) + ('${S}', bui.Lstr(resource='signInWithGameCenterText')) ], ) else: extra = '' - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=( self._sub_width * 0.5, v + sign_in_benefits_space * 0.4, ), size=(0, 0), - text=ba.Lstr( + text=bui.Lstr( value='${A}${B}', subs=[ - ('${A}', ba.Lstr(resource=self._r + '.signInInfoText')), + ( + '${A}', + bui.Lstr(resource=self._r + '.signInInfoText'), + ), ('${B}', extra), ], ), @@ -514,14 +521,14 @@ class AccountSettingsWindow(ba.Window): if show_signing_in_text: v -= signing_in_text_space - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=( self._sub_width * 0.5, v + signing_in_text_space * 0.5, ), size=(0, 0), - text=ba.Lstr(resource='accountSettingsWindow.signingInText'), + text=bui.Lstr(resource='accountSettingsWindow.signingInText'), scale=0.9, color=(0, 1, 0), maxwidth=self._sub_width * 0.8, @@ -532,21 +539,21 @@ class AccountSettingsWindow(ba.Window): if show_google_play_sign_in_button: button_width = 350 v -= sign_in_button_space - self._sign_in_google_play_button = btn = ba.buttonwidget( + self._sign_in_google_play_button = btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v - 20), autoselect=True, size=(button_width, 60), - label=ba.Lstr( + label=bui.Lstr( value='${A}${B}', subs=[ ( '${A}', - ba.charstr(ba.SpecialChar.GOOGLE_PLAY_GAMES_LOGO), + bui.charstr(bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO), ), ( '${B}', - ba.Lstr( + bui.Lstr( resource=self._r + '.signInWithGooglePlayText' ), ), @@ -556,19 +563,19 @@ class AccountSettingsWindow(ba.Window): ) if first_selectable is None: first_selectable = btn - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.widget(edit=btn, left_widget=bbtn) - ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) + bui.widget(edit=btn, left_widget=bbtn) + bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) self._sign_in_text = None if show_v2_proxy_sign_in_button: button_width = 350 v -= sign_in_button_space - self._sign_in_v2_proxy_button = btn = ba.buttonwidget( + self._sign_in_v2_proxy_button = btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v - 20), autoselect=True, @@ -576,34 +583,34 @@ class AccountSettingsWindow(ba.Window): label='', on_activate_call=self._v2_proxy_sign_in_press, ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, draw_controller=btn, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v + 17), - text=ba.Lstr( + text=bui.Lstr( value='${A}${B}', subs=[ - ('${A}', ba.charstr(ba.SpecialChar.V2_LOGO)), + ('${A}', bui.charstr(bui.SpecialChar.V2_LOGO)), ( '${B}', - ba.Lstr(resource=self._r + '.signInWithV2Text'), + bui.Lstr(resource=self._r + '.signInWithV2Text'), ), ], ), maxwidth=button_width * 0.8, color=(0.75, 1.0, 0.7), ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, draw_controller=btn, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v - 4), - text=ba.Lstr(resource=self._r + '.signInWithV2InfoText'), + text=bui.Lstr(resource=self._r + '.signInWithV2InfoText'), flatness=1.0, scale=0.57, maxwidth=button_width * 0.9, @@ -611,19 +618,19 @@ class AccountSettingsWindow(ba.Window): ) if first_selectable is None: first_selectable = btn - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.widget(edit=btn, left_widget=bbtn) - ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) + bui.widget(edit=btn, left_widget=bbtn) + bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) self._sign_in_text = None if show_device_sign_in_button: button_width = 350 v -= sign_in_button_space + deprecated_space - self._sign_in_device_button = btn = ba.buttonwidget( + self._sign_in_device_button = btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v - 20), autoselect=True, @@ -631,46 +638,48 @@ class AccountSettingsWindow(ba.Window): label='', on_activate_call=lambda: self._sign_in_press('Local'), ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v + 60), - text=ba.Lstr(resource='deprecatedText'), + text=bui.Lstr(resource='deprecatedText'), scale=0.8, maxwidth=300, color=(0.6, 0.55, 0.45), ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, draw_controller=btn, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v + 17), - text=ba.Lstr( + text=bui.Lstr( value='${A}${B}', subs=[ - ('${A}', ba.charstr(ba.SpecialChar.LOCAL_ACCOUNT)), + ('${A}', bui.charstr(bui.SpecialChar.LOCAL_ACCOUNT)), ( '${B}', - ba.Lstr(resource=self._r + '.signInWithDeviceText'), + bui.Lstr( + resource=self._r + '.signInWithDeviceText' + ), ), ], ), maxwidth=button_width * 0.8, color=(0.75, 1.0, 0.7), ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, draw_controller=btn, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v - 4), - text=ba.Lstr(resource=self._r + '.signInWithDeviceInfoText'), + text=bui.Lstr(resource=self._r + '.signInWithDeviceInfoText'), flatness=1.0, scale=0.57, maxwidth=button_width * 0.9, @@ -678,98 +687,98 @@ class AccountSettingsWindow(ba.Window): ) if first_selectable is None: first_selectable = btn - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.widget(edit=btn, left_widget=bbtn) - ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) + bui.widget(edit=btn, left_widget=bbtn) + bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) self._sign_in_text = None if show_manage_v2_account_button: button_width = 300 v -= manage_v2_account_button_space - self._manage_v2_button = btn = ba.buttonwidget( + self._manage_v2_button = btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v + 30), autoselect=True, size=(button_width, 60), - label=ba.Lstr(resource=self._r + '.manageAccountText'), + label=bui.Lstr(resource=self._r + '.manageAccountText'), color=(0.55, 0.5, 0.6), - icon=ba.gettexture('settingsIcon'), + icon=bui.gettexture('settingsIcon'), textcolor=(0.75, 0.7, 0.8), - on_activate_call=ba.WeakCall(self._on_manage_account_press), + on_activate_call=bui.WeakCall(self._on_manage_account_press), ) if first_selectable is None: first_selectable = btn - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.widget(edit=btn, left_widget=bbtn) + bui.widget(edit=btn, left_widget=bbtn) if show_player_profiles_button: button_width = 300 v -= player_profiles_button_space - self._player_profiles_button = btn = ba.buttonwidget( + self._player_profiles_button = btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v + 30), autoselect=True, size=(button_width, 60), - label=ba.Lstr(resource='playerProfilesWindow.titleText'), + label=bui.Lstr(resource='playerProfilesWindow.titleText'), color=(0.55, 0.5, 0.6), - icon=ba.gettexture('cuteSpaz'), + icon=bui.gettexture('cuteSpaz'), textcolor=(0.75, 0.7, 0.8), on_activate_call=self._player_profiles_press, ) if first_selectable is None: first_selectable = btn - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=0) + bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=0) # the button to go to OS-Specific leaderboards/high-score-lists/etc. if show_game_service_button: button_width = 300 v -= game_service_button_space * 0.85 - v1_account_type = ba.internal.get_v1_account_type() + v1_account_type = plus.get_v1_account_type() if v1_account_type == 'Game Center': - v1_account_type_name = ba.Lstr(resource='gameCenterText') + v1_account_type_name = bui.Lstr(resource='gameCenterText') else: raise ValueError( "unknown account type: '" + str(v1_account_type) + "'" ) - self._game_service_button = btn = ba.buttonwidget( + self._game_service_button = btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), autoselect=True, - on_activate_call=ba.internal.show_online_score_ui, + on_activate_call=self._on_game_service_button_press, size=(button_width, 50), label=v1_account_type_name, ) if first_selectable is None: first_selectable = btn - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.widget(edit=btn, left_widget=bbtn) + bui.widget(edit=btn, left_widget=bbtn) v -= game_service_button_space * 0.15 else: self.game_service_button = None - self._achievements_text: ba.Widget | None + self._achievements_text: bui.Widget | None if show_achievements_text: v -= achievements_text_space * 0.5 - self._achievements_text = ba.textwidget( + self._achievements_text = bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v), size=(0, 0), @@ -783,17 +792,17 @@ class AccountSettingsWindow(ba.Window): else: self._achievements_text = None - self._achievements_button: ba.Widget | None + self._achievements_button: bui.Widget | None if show_achievements_button: button_width = 300 v -= achievements_button_space * 0.85 - self._achievements_button = btn = ba.buttonwidget( + self._achievements_button = btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), autoselect=True, - icon=ba.gettexture( + icon=bui.gettexture( 'googlePlayAchievementsIcon' if is_gpgs else 'achievementsIcon' @@ -809,12 +818,12 @@ class AccountSettingsWindow(ba.Window): ) if first_selectable is None: first_selectable = btn - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.widget(edit=btn, left_widget=bbtn) + bui.widget(edit=btn, left_widget=bbtn) v -= achievements_button_space * 0.15 else: self._achievements_button = None @@ -822,38 +831,38 @@ class AccountSettingsWindow(ba.Window): if show_achievements_text or show_achievements_button: self._refresh_achievements() - self._leaderboards_button: ba.Widget | None + self._leaderboards_button: bui.Widget | None if show_leaderboards_button: button_width = 300 v -= leaderboards_button_space * 0.85 - self._leaderboards_button = btn = ba.buttonwidget( + self._leaderboards_button = btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), autoselect=True, - icon=ba.gettexture('googlePlayLeaderboardsIcon'), + icon=bui.gettexture('googlePlayLeaderboardsIcon'), icon_color=(0.8, 0.95, 0.7), on_activate_call=self._on_leaderboards_press, size=(button_width, 50), - label=ba.Lstr(resource='leaderboardsText'), + label=bui.Lstr(resource='leaderboardsText'), ) if first_selectable is None: first_selectable = btn - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.widget(edit=btn, left_widget=bbtn) + bui.widget(edit=btn, left_widget=bbtn) v -= leaderboards_button_space * 0.15 else: self._leaderboards_button = None - self._campaign_progress_text: ba.Widget | None + self._campaign_progress_text: bui.Widget | None if show_campaign_progress: v -= campaign_progress_space * 0.5 - self._campaign_progress_text = ba.textwidget( + self._campaign_progress_text = bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v), size=(0, 0), @@ -868,10 +877,10 @@ class AccountSettingsWindow(ba.Window): else: self._campaign_progress_text = None - self._tickets_text: ba.Widget | None + self._tickets_text: bui.Widget | None if show_tickets: v -= tickets_space * 0.5 - self._tickets_text = ba.textwidget( + self._tickets_text = bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v), size=(0, 0), @@ -894,21 +903,21 @@ class AccountSettingsWindow(ba.Window): button_width = 250 if show_reset_progress_button: confirm_text = ( - ba.Lstr(resource=self._r + '.resetProgressConfirmText') + bui.Lstr(resource=self._r + '.resetProgressConfirmText') if self._can_reset_achievements - else ba.Lstr( + else bui.Lstr( resource=self._r + '.resetProgressConfirmNoAchievementsText' ) ) v -= reset_progress_button_space - self._reset_progress_button = btn = ba.buttonwidget( + self._reset_progress_button = btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), autoselect=True, size=(button_width, 60), - label=ba.Lstr(resource=self._r + '.resetProgressText'), + label=bui.Lstr(resource=self._r + '.resetProgressText'), on_activate_call=lambda: confirm.ConfirmWindow( text=confirm_text, width=500, @@ -918,24 +927,24 @@ class AccountSettingsWindow(ba.Window): ) if first_selectable is None: first_selectable = btn - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.widget(edit=btn, left_widget=bbtn) + bui.widget(edit=btn, left_widget=bbtn) - self._linked_accounts_text: ba.Widget | None + self._linked_accounts_text: bui.Widget | None if show_linked_accounts_text: v -= linked_accounts_text_space * 0.8 - self._linked_accounts_text = ba.textwidget( + self._linked_accounts_text = bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v), size=(0, 0), scale=0.9, color=(0.75, 0.7, 0.8), maxwidth=self._sub_width * 0.95, - text=ba.Lstr(resource=self._r + '.linkedAccountsText'), + text=bui.Lstr(resource=self._r + '.linkedAccountsText'), h_align='center', v_align='center', ) @@ -948,7 +957,7 @@ class AccountSettingsWindow(ba.Window): if show_link_accounts_button: v -= link_accounts_button_space - self._link_accounts_button = btn = ba.buttonwidget( + self._link_accounts_button = btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), autoselect=True, @@ -957,25 +966,25 @@ class AccountSettingsWindow(ba.Window): color=(0.55, 0.5, 0.6), on_activate_call=self._link_accounts_press, ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, draw_controller=btn, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v + 17 + 20), - text=ba.Lstr(resource=self._r + '.linkAccountsText'), + text=bui.Lstr(resource=self._r + '.linkAccountsText'), maxwidth=button_width * 0.8, color=(0.75, 0.7, 0.8), ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, draw_controller=btn, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v - 4 + 20), - text=ba.Lstr(resource=self._r + '.linkAccountsInfoText'), + text=bui.Lstr(resource=self._r + '.linkAccountsInfoText'), flatness=1.0, scale=0.5, maxwidth=button_width * 0.8, @@ -983,17 +992,17 @@ class AccountSettingsWindow(ba.Window): ) if first_selectable is None: first_selectable = btn - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) + bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) - self._unlink_accounts_button: ba.Widget | None + self._unlink_accounts_button: bui.Widget | None if show_unlink_accounts_button: v -= unlink_accounts_button_space - self._unlink_accounts_button = btn = ba.buttonwidget( + self._unlink_accounts_button = btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v + 25), autoselect=True, @@ -1002,38 +1011,38 @@ class AccountSettingsWindow(ba.Window): color=(0.55, 0.5, 0.6), on_activate_call=self._unlink_accounts_press, ) - self._unlink_accounts_button_label = ba.textwidget( + self._unlink_accounts_button_label = bui.textwidget( parent=self._subcontainer, draw_controller=btn, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v + 55), - text=ba.Lstr(resource=self._r + '.unlinkAccountsText'), + text=bui.Lstr(resource=self._r + '.unlinkAccountsText'), maxwidth=button_width * 0.8, color=(0.75, 0.7, 0.8), ) if first_selectable is None: first_selectable = btn - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) + bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) self._update_unlink_accounts_button() else: self._unlink_accounts_button = None if show_v2_link_info: v -= v2_link_info_space - ba.textwidget( + bui.textwidget( parent=self._subcontainer, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v + v2_link_info_space - 20), - text=ba.Lstr(resource='v2AccountLinkingInfoText'), + text=bui.Lstr(resource='v2AccountLinkingInfoText'), flatness=1.0, scale=0.8, maxwidth=450, @@ -1043,11 +1052,11 @@ class AccountSettingsWindow(ba.Window): if self._show_legacy_unlink_button: v -= legacy_unlink_button_space button_width_w = button_width * 1.5 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5 - 150.0, v + 75), size=(300.0, 60), - text=ba.Lstr(resource='whatIsThisText'), + text=bui.Lstr(resource='whatIsThisText'), scale=0.8, color=(0.3, 0.7, 0.05), maxwidth=200.0, @@ -1058,12 +1067,14 @@ class AccountSettingsWindow(ba.Window): on_activate_call=show_what_is_legacy_unlinking_page, click_activate=True, ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width_w) * 0.5, v + 25), autoselect=True, size=(button_width_w, 60), - label=ba.Lstr(resource=self._r + '.unlinkLegacyV1AccountsText'), + label=bui.Lstr( + resource=self._r + '.unlinkLegacyV1AccountsText' + ), textcolor=(0.8, 0.4, 0), color=(0.55, 0.5, 0.6), on_activate_call=self._unlink_accounts_press, @@ -1071,11 +1082,11 @@ class AccountSettingsWindow(ba.Window): if show_sign_out_button: v -= sign_out_button_space - self._sign_out_button = btn = ba.buttonwidget( + self._sign_out_button = btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), size=(button_width, 60), - label=ba.Lstr(resource=self._r + '.signOutText'), + label=bui.Lstr(resource=self._r + '.signOutText'), color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), autoselect=True, @@ -1083,20 +1094,20 @@ class AccountSettingsWindow(ba.Window): ) if first_selectable is None: first_selectable = btn - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) + bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) if show_cancel_sign_in_button: v -= cancel_sign_in_button_space - self._cancel_sign_in_button = btn = ba.buttonwidget( + self._cancel_sign_in_button = btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), size=(button_width, 60), - label=ba.Lstr(resource='cancelText'), + label=bui.Lstr(resource='cancelText'), color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), autoselect=True, @@ -1104,31 +1115,39 @@ class AccountSettingsWindow(ba.Window): ) if first_selectable is None: first_selectable = btn - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) + bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) # Whatever the topmost selectable thing is, we want it to scroll all # the way up when we select it. if first_selectable is not None: - ba.widget( + bui.widget( edit=first_selectable, up_widget=bbtn, show_buffer_top=400 ) # (this should re-scroll us to the top..) - ba.containerwidget( + bui.containerwidget( edit=self._subcontainer, visible_child=first_selectable ) self._needs_refresh = False + def _on_game_service_button_press(self) -> None: + if bui.app.classic is not None: + bui.app.classic.show_online_score_ui() + else: + logging.warning('game service ui not available without classic.') + def _on_custom_achievements_press(self) -> None: - ba.timer( - 0.15, - ba.Call(ba.internal.show_online_score_ui, 'achievements'), - timetype=ba.TimeType.REAL, - ) + if bui.app.classic is not None: + bui.apptimer( + 0.15, + bui.Call(bui.app.classic.show_online_score_ui, 'achievements'), + ) + else: + logging.warning('show_online_score_ui requires classic') def _on_achievements_press(self) -> None: # pylint: disable=cyclic-import @@ -1143,47 +1162,49 @@ class AccountSettingsWindow(ba.Window): show_what_is_v2_page() def _on_manage_account_press(self) -> None: - ba.screenmessage(ba.Lstr(resource='oneMomentText')) + bui.screenmessage(bui.Lstr(resource='oneMomentText')) # We expect to have a v2 account signed in if we get here. - if ba.app.accounts_v2.primary is None: + if bui.app.accounts.primary is None: logging.exception( 'got manage-account press without v2 account present' ) return - with ba.app.accounts_v2.primary: - ba.app.cloud.send_message_cb( + with bui.app.accounts.primary: + bui.app.cloud.send_message_cb( bacommon.cloud.ManageAccountMessage(), - on_response=ba.WeakCall(self._on_manage_account_response), + on_response=bui.WeakCall(self._on_manage_account_response), ) def _on_manage_account_response( self, response: bacommon.cloud.ManageAccountResponse | Exception ) -> None: - if isinstance(response, Exception) or response.url is None: - ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) - ba.playsound(ba.getsound('error')) + bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0)) + bui.getsound('error').play() return - ba.open_url(response.url) + bui.open_url(response.url) def _on_leaderboards_press(self) -> None: - ba.timer( - 0.15, - ba.Call(ba.internal.show_online_score_ui, 'leaderboards'), - timetype=ba.TimeType.REAL, - ) + if bui.app.classic is not None: + bui.apptimer( + 0.15, + bui.Call(bui.app.classic.show_online_score_ui, 'leaderboards'), + ) + else: + logging.warning('show_online_score_ui requires classic') def _have_unlinkable_v1_accounts(self) -> bool: + plus = bui.app.plus + assert plus is not None + # if this is not present, we haven't had contact from the server so # let's not proceed.. - if ba.internal.get_public_login_id() is None: + if plus.get_v1_account_public_login_id() is None: return False - accounts = ba.internal.get_v1_account_misc_read_val_2( - 'linkedAccounts', [] - ) + accounts = plus.get_v1_account_misc_read_val_2('linkedAccounts', []) return len(accounts) > 1 def _update_unlink_accounts_button(self) -> None: @@ -1193,229 +1214,236 @@ class AccountSettingsWindow(ba.Window): clr = (0.75, 0.7, 0.8, 1.0) else: clr = (1.0, 1.0, 1.0, 0.25) - ba.textwidget(edit=self._unlink_accounts_button_label, color=clr) + bui.textwidget(edit=self._unlink_accounts_button_label, color=clr) def _should_show_legacy_unlink_button(self) -> bool: - # Only show this when fully signed in to a v2 account. - if not self._v1_signed_in or ba.app.accounts_v2.primary is None: + if not self._v1_signed_in or bui.app.accounts.primary is None: return False out = self._have_unlinkable_v1_accounts() return out def _update_linked_accounts_text(self) -> None: + plus = bui.app.plus + assert plus is not None + if self._linked_accounts_text is None: return # Disable this by default when signed in to a V2 account # (since this shows V1 links which we should no longer care about). - if ( - ba.app.accounts_v2.primary is not None - and not FORCE_ENABLE_V1_LINKING - ): + if bui.app.accounts.primary is not None and not FORCE_ENABLE_V1_LINKING: return # if this is not present, we haven't had contact from the server so # let's not proceed.. - if ba.internal.get_public_login_id() is None: + if plus.get_v1_account_public_login_id() is None: num = int(time.time()) % 4 accounts_str = num * '.' + (4 - num) * ' ' else: - accounts = ba.internal.get_v1_account_misc_read_val_2( - 'linkedAccounts', [] - ) + accounts = plus.get_v1_account_misc_read_val_2('linkedAccounts', []) # UPDATE - we now just print the number here; not the actual # accounts (they can see that in the unlink section if they're # curious) accounts_str = str(max(0, len(accounts) - 1)) - ba.textwidget( + bui.textwidget( edit=self._linked_accounts_text, - text=ba.Lstr( + text=bui.Lstr( value='${L} ${A}', subs=[ - ('${L}', ba.Lstr(resource=self._r + '.linkedAccountsText')), + ( + '${L}', + bui.Lstr(resource=self._r + '.linkedAccountsText'), + ), ('${A}', accounts_str), ], ), ) def _refresh_campaign_progress_text(self) -> None: - from ba.internal import getcampaign - if self._campaign_progress_text is None: return - p_str: str | ba.Lstr + p_str: str | bui.Lstr try: - campaign = getcampaign('Default') + assert bui.app.classic is not None + campaign = bui.app.classic.getcampaign('Default') levels = campaign.levels levels_complete = sum((1 if l.complete else 0) for l in levels) # Last level cant be completed; hence the -1; progress = min(1.0, float(levels_complete) / (len(levels) - 1)) - p_str = ba.Lstr( + p_str = bui.Lstr( resource=self._r + '.campaignProgressText', subs=[('${PROGRESS}', str(int(progress * 100.0)) + '%')], ) except Exception: p_str = '?' - ba.print_exception('Error calculating co-op campaign progress.') - ba.textwidget(edit=self._campaign_progress_text, text=p_str) + logging.exception('Error calculating co-op campaign progress.') + bui.textwidget(edit=self._campaign_progress_text, text=p_str) def _refresh_tickets_text(self) -> None: + plus = bui.app.plus + assert plus is not None + if self._tickets_text is None: return try: - tc_str = str(ba.internal.get_v1_account_ticket_count()) + tc_str = str(plus.get_v1_account_ticket_count()) except Exception: - ba.print_exception() + logging.exception('error refreshing tickets text') tc_str = '-' - ba.textwidget( + bui.textwidget( edit=self._tickets_text, - text=ba.Lstr( + text=bui.Lstr( resource=self._r + '.ticketsText', subs=[('${COUNT}', tc_str)] ), ) def _refresh_account_name_text(self) -> None: + plus = bui.app.plus + assert plus is not None if self._account_name_text is None: return try: - name_str = ba.internal.get_v1_account_display_string() + name_str = plus.get_v1_account_display_string() except Exception: - ba.print_exception() + logging.exception('error refreshing tickets text') name_str = '??' - ba.textwidget(edit=self._account_name_text, text=name_str) + bui.textwidget(edit=self._account_name_text, text=name_str) if self._account_name_what_is_text is not None: - swidth = ba.internal.get_string_width( - name_str, suppress_warning=True - ) + swidth = bui.get_string_width(name_str, suppress_warning=True) # Eww; number-fudging. Need to recalibrate this if # account name scaling changes. x = self._sub_width * 0.5 - swidth * 0.75 - 170 - ba.textwidget( + bui.textwidget( edit=self._account_name_what_is_text, position=(x, self._account_name_what_is_y), ) def _refresh_achievements(self) -> None: + assert bui.app.classic is not None if ( self._achievements_text is None and self._achievements_button is None ): return - complete = sum(1 if a.complete else 0 for a in ba.app.ach.achievements) - total = len(ba.app.ach.achievements) - txt_final = ba.Lstr( + complete = sum( + 1 if a.complete else 0 for a in bui.app.classic.ach.achievements + ) + total = len(bui.app.classic.ach.achievements) + txt_final = bui.Lstr( resource=self._r + '.achievementProgressText', subs=[('${COUNT}', str(complete)), ('${TOTAL}', str(total))], ) if self._achievements_text is not None: - ba.textwidget(edit=self._achievements_text, text=txt_final) + bui.textwidget(edit=self._achievements_text, text=txt_final) if self._achievements_button is not None: - ba.buttonwidget(edit=self._achievements_button, label=txt_final) + bui.buttonwidget(edit=self._achievements_button, label=txt_final) def _link_accounts_press(self) -> None: # pylint: disable=cyclic-import - from bastd.ui.account import link + from bastd.ui.account.link import AccountLinkWindow - link.AccountLinkWindow(origin_widget=self._link_accounts_button) + AccountLinkWindow(origin_widget=self._link_accounts_button) def _unlink_accounts_press(self) -> None: # pylint: disable=cyclic-import - from bastd.ui.account import unlink + from bastd.ui.account.unlink import AccountUnlinkWindow if not self._have_unlinkable_v1_accounts(): - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return - unlink.AccountUnlinkWindow(origin_widget=self._unlink_accounts_button) + + AccountUnlinkWindow(origin_widget=self._unlink_accounts_button) def _player_profiles_press(self) -> None: # pylint: disable=cyclic-import - from bastd.ui.profile import browser as pbrowser + from bastd.ui.profile.browser import ProfileBrowserWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - pbrowser.ProfileBrowserWindow( - origin_widget=self._player_profiles_button - ) + bui.containerwidget(edit=self._root_widget, transition='out_left') + ProfileBrowserWindow(origin_widget=self._player_profiles_button) def _cancel_sign_in_press(self) -> None: - # If we're waiting on an adapter to give us credentials, abort. self._signing_in_adapter = None # Say we don't wanna be signed in anymore if we are. - ba.app.accounts_v2.set_primary_credentials(None) + bui.app.accounts.set_primary_credentials(None) self._needs_refresh = True # Speed UI updates along. - ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) + bui.apptimer(0.1, bui.WeakCall(self._update)) def _sign_out_press(self) -> None: + plus = bui.app.plus + assert plus is not None - if ba.app.accounts_v2.have_primary_credentials(): + if bui.app.accounts.have_primary_credentials(): if ( - ba.app.accounts_v2.primary is not None - and LoginType.GPGS in ba.app.accounts_v2.primary.logins + bui.app.accounts.primary is not None + and LoginType.GPGS in bui.app.accounts.primary.logins ): self._explicitly_signed_out_of_gpgs = True - ba.app.accounts_v2.set_primary_credentials(None) + bui.app.accounts.set_primary_credentials(None) else: - ba.internal.sign_out_v1() + plus.sign_out_v1() - cfg = ba.app.config + cfg = bui.app.config # Also take note that its our *explicit* intention to not be # signed in at this point (affects v1 accounts). cfg['Auto Account State'] = 'signed_out' cfg.commit() - ba.buttonwidget( + bui.buttonwidget( edit=self._sign_out_button, - label=ba.Lstr(resource=self._r + '.signingOutText'), + label=bui.Lstr(resource=self._r + '.signingOutText'), ) # Speed UI updates along. - ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) + bui.apptimer(0.1, bui.WeakCall(self._update)) def _sign_in_press(self, login_type: str | LoginType) -> None: + plus = bui.app.plus + assert plus is not None # V1 login types are strings. if isinstance(login_type, str): - ba.internal.sign_in_v1(login_type) + plus.sign_in_v1(login_type) # Make note of the type account we're *wanting* # to be signed in with. - cfg = ba.app.config + cfg = bui.app.config cfg['Auto Account State'] = login_type cfg.commit() self._needs_refresh = True - ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) + bui.apptimer(0.1, bui.WeakCall(self._update)) return # V2 login sign-in buttons generally go through adapters. - adapter = ba.app.accounts_v2.login_adapters.get(login_type) + adapter = bui.app.accounts.login_adapters.get(login_type) if adapter is not None: self._signing_in_adapter = adapter adapter.sign_in( - result_cb=ba.WeakCall(self._on_adapter_sign_in_result), + result_cb=bui.WeakCall(self._on_adapter_sign_in_result), description='account settings button', ) # Will get 'Signing in...' to show. self._needs_refresh = True - ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) + bui.apptimer(0.1, bui.WeakCall(self._update)) else: - ba.screenmessage(f'Unsupported login_type: {login_type.name}') + bui.screenmessage(f'Unsupported login_type: {login_type.name}') def _on_adapter_sign_in_result( self, - adapter: LoginAdapter, - result: LoginAdapter.SignInResult | Exception, + adapter: bui.LoginAdapter, + result: bui.LoginAdapter.SignInResult | Exception, ) -> None: is_us = self._signing_in_adapter is adapter @@ -1429,13 +1457,13 @@ class AccountSettingsWindow(ba.Window): if isinstance(result, Exception): # For now just make a bit of noise if anything went wrong; # can get more specific as needed later. - ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) - ba.playsound(ba.getsound('error')) + bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0)) + bui.getsound('error').play() else: # Success! Plug in these credentials which will begin # verifying them and set our primary account-handle # when finished. - ba.app.accounts_v2.set_primary_credentials(result.credentials) + bui.app.accounts.set_primary_credentials(result.credentials) # Special case - if the user has explicitly logged out and # logged in again with GPGS via this button, warn them that @@ -1447,11 +1475,11 @@ class AccountSettingsWindow(ba.Window): ): # Delay this slightly so it hopefully pops up after # credentials go through and the account name shows up. - ba.timer( + bui.apptimer( 1.5, - ba.Call( - ba.screenmessage, - ba.Lstr( + bui.Call( + bui.screenmessage, + bui.Lstr( resource=self._r + '.googlePlayGamesAccountSwitchText' ), @@ -1460,7 +1488,7 @@ class AccountSettingsWindow(ba.Window): # Speed any UI updates along. self._needs_refresh = True - ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) + bui.apptimer(0.1, bui.WeakCall(self._update)) def _v2_proxy_sign_in_press(self) -> None: # pylint: disable=cyclic-import @@ -1471,20 +1499,20 @@ class AccountSettingsWindow(ba.Window): def _reset_progress(self) -> None: try: - from ba.internal import getcampaign - + assert bui.app.classic is not None # FIXME: This would need to happen server-side these days. if self._can_reset_achievements: - ba.app.config['Achievements'] = {} - ba.internal.reset_achievements() - campaign = getcampaign('Default') + logging.warning('ach resets not wired up.') + # bui.app.config['Achievements'] = {} + # bui.reset_achievements() + campaign = bui.app.classic.getcampaign('Default') campaign.reset() # also writes the config.. - campaign = getcampaign('Challenges') + campaign = bui.app.classic.getcampaign('Challenges') campaign.reset() # also writes the config.. except Exception: - ba.print_exception('Error resetting co-op campaign progress.') + logging.exception('Error resetting co-op campaign progress.') - ba.playsound(ba.getsound('shieldDown')) + bui.getsound('shieldDown').play() self._refresh() def _back(self) -> None: @@ -1492,12 +1520,13 @@ class AccountSettingsWindow(ba.Window): from bastd.ui.mainmenu import MainMenuWindow self._save_state() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) if not self._modal: - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( MainMenuWindow(transition='in_left').get_root_widget() ) @@ -1510,31 +1539,39 @@ class AccountSettingsWindow(ba.Window): sel_name = 'Scroll' else: raise ValueError('unrecognized selection') - ba.app.ui.window_states[type(self)] = sel_name + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = sel_name except Exception: - ba.print_exception(f'Error saving state for {self}.') + logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: try: - sel_name = ba.app.ui.window_states.get(type(self)) + assert bui.app.classic is not None + sel_name = bui.app.classic.ui.window_states.get(type(self)) if sel_name == 'Back': sel = self._back_button elif sel_name == 'Scroll': sel = self._scrollwidget else: sel = self._back_button - ba.containerwidget(edit=self._root_widget, selected_child=sel) + bui.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: - ba.print_exception(f'Error restoring state for {self}.') + logging.exception('Error restoring state for %s.', self) def show_what_is_v2_page() -> None: """Show the webpage describing V2 accounts.""" - bamasteraddr = ba.internal.get_master_server_address(version=2) - ba.open_url(f'{bamasteraddr}/whatisv2') + plus = bui.app.plus + assert plus is not None + + bamasteraddr = plus.get_master_server_address(version=2) + bui.open_url(f'{bamasteraddr}/whatisv2') def show_what_is_legacy_unlinking_page() -> None: """Show the webpage describing legacy unlinking.""" - bamasteraddr = ba.internal.get_master_server_address(version=2) - ba.open_url(f'{bamasteraddr}/whatarev1links') + plus = bui.app.plus + assert plus is not None + + bamasteraddr = plus.get_master_server_address(version=2) + bui.open_url(f'{bamasteraddr}/whatarev1links') diff --git a/assets/src/ba_data/python/bastd/ui/account/unlink.py b/src/assets/ba_data/python/bastd/ui/account/unlink.py similarity index 69% rename from assets/src/ba_data/python/bastd/ui/account/unlink.py rename to src/assets/ba_data/python/bastd/ui/account/unlink.py index ffbce7db..8a7dd418 100644 --- a/assets/src/ba_data/python/bastd/ui/account/unlink.py +++ b/src/assets/ba_data/python/bastd/ui/account/unlink.py @@ -7,17 +7,19 @@ from __future__ import annotations import time from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any -class AccountUnlinkWindow(ba.Window): +class AccountUnlinkWindow(bui.Window): """A window to kick off account unlinks.""" - def __init__(self, origin_widget: ba.Widget | None = None): + def __init__(self, origin_widget: bui.Widget | None = None): + plus = bui.app.plus + assert plus is not None + scale_origin: tuple[float, float] | None if origin_widget is not None: self._transition_out = 'out_scale' @@ -32,26 +34,27 @@ class AccountUnlinkWindow(ba.Window): self._height = 350 self._scroll_width = 400 self._scroll_height = 200 - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale base_scale = ( 2.0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.6 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.1 ) super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), transition=transition, scale=base_scale, scale_origin_stack_offset=scale_origin, stack_offset=(0, -10) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self._root_widget, position=(30, self._height - 50), size=(50, 50), @@ -60,26 +63,26 @@ class AccountUnlinkWindow(ba.Window): color=bg_color, on_activate_call=self._cancel, autoselect=True, - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.88), size=(0, 0), - text=ba.Lstr( + text=bui.Lstr( resource='accountSettingsWindow.unlinkAccountsInstructionsText' ), maxwidth=self._width * 0.7, - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, h_align='center', v_align='center', ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._cancel_button ) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, highlight=False, position=( @@ -88,16 +91,16 @@ class AccountUnlinkWindow(ba.Window): ), size=(self._scroll_width, self._scroll_height), ) - ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) - self._columnwidget = ba.columnwidget( + bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) + self._columnwidget = bui.columnwidget( parent=self._scrollwidget, border=2, margin=0, left_border=10 ) - our_login_id = ba.internal.get_public_login_id() + our_login_id = plus.get_v1_account_public_login_id() if our_login_id is None: entries = [] else: - account_infos = ba.internal.get_v1_account_misc_read_val_2( + account_infos = plus.get_v1_account_misc_read_val_2( 'linkedAccounts2', [] ) entries = [ @@ -108,41 +111,44 @@ class AccountUnlinkWindow(ba.Window): # (avoid getting our selection stuck on an empty column widget) if not entries: - ba.containerwidget(edit=self._scrollwidget, selectable=False) + bui.containerwidget(edit=self._scrollwidget, selectable=False) for i, entry in enumerate(entries): - txt = ba.textwidget( + txt = bui.textwidget( parent=self._columnwidget, selectable=True, text=entry['name'], size=(self._scroll_width - 30, 30), autoselect=True, click_activate=True, - on_activate_call=ba.Call(self._on_entry_selected, entry), + on_activate_call=bui.Call(self._on_entry_selected, entry), ) - ba.widget(edit=txt, left_widget=self._cancel_button) + bui.widget(edit=txt, left_widget=self._cancel_button) if i == 0: - ba.widget(edit=txt, up_widget=self._cancel_button) + bui.widget(edit=txt, up_widget=self._cancel_button) def _on_entry_selected(self, entry: dict[str, Any]) -> None: - ba.screenmessage( - ba.Lstr( + plus = bui.app.plus + assert plus is not None + + bui.screenmessage( + bui.Lstr( resource='pleaseWaitText', fallback_resource='requestingText' ), color=(0, 1, 0), ) - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'ACCOUNT_UNLINK_REQUEST', 'accountID': entry['id'], 'expire_time': time.time() + 5, } ) - ba.internal.run_transactions() - ba.containerwidget( + plus.run_v1_account_transactions() + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) def _cancel(self) -> None: - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) diff --git a/assets/src/ba_data/python/bastd/ui/account/v2proxy.py b/src/assets/ba_data/python/bastd/ui/account/v2proxy.py similarity index 71% rename from assets/src/ba_data/python/bastd/ui/account/v2proxy.py rename to src/assets/ba_data/python/bastd/ui/account/v2proxy.py index d782fcf7..384e52d3 100644 --- a/assets/src/ba_data/python/bastd/ui/account/v2proxy.py +++ b/src/assets/ba_data/python/bastd/ui/account/v2proxy.py @@ -5,32 +5,27 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING - -import ba -import ba.internal from efro.error import CommunicationError import bacommon.cloud - -if TYPE_CHECKING: - pass +import bauiv1 as bui STATUS_CHECK_INTERVAL_SECONDS = 2.0 -class V2ProxySignInWindow(ba.Window): +class V2ProxySignInWindow(bui.Window): """A window allowing signing in to a v2 account.""" - def __init__(self, origin_widget: ba.Widget): + def __init__(self, origin_widget: bui.Widget): self._width = 600 self._height = 550 self._proxyid: str | None = None self._proxykey: str | None = None - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), transition='in_scale', scale_origin_stack_offset=( @@ -38,33 +33,33 @@ class V2ProxySignInWindow(ba.Window): ), scale=( 1.25 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.05 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 0.9 ), ) ) - self._loading_text = ba.textwidget( + self._loading_text = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.5), h_align='center', v_align='center', size=(0, 0), maxwidth=0.9 * self._width, - text=ba.Lstr( + text=bui.Lstr( value='${A}...', - subs=[('${A}', ba.Lstr(resource='loadingText'))], + subs=[('${A}', bui.Lstr(resource='loadingText'))], ), ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self._root_widget, position=(30, self._height - 65), size=(130, 50), scale=0.8, - label=ba.Lstr(resource='cancelText'), + label=bui.Lstr(resource='cancelText'), on_activate_call=self._done, autoselect=True, color=(0.55, 0.5, 0.6), @@ -72,53 +67,53 @@ class V2ProxySignInWindow(ba.Window): ) if bool(False): - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._cancel_button ) - self._update_timer: ba.Timer | None = None + self._update_timer: bui.AppTimer | None = None # Ask the cloud for a proxy login id. - ba.app.cloud.send_message_cb( + bui.app.cloud.send_message_cb( bacommon.cloud.LoginProxyRequestMessage(), - on_response=ba.WeakCall(self._on_proxy_request_response), + on_response=bui.WeakCall(self._on_proxy_request_response), ) def _on_proxy_request_response( self, response: bacommon.cloud.LoginProxyRequestResponse | Exception ) -> None: - from ba.internal import is_browser_likely_available + plus = bui.app.plus + assert plus is not None # Something went wrong. Show an error message and that's it. if isinstance(response, Exception): - ba.textwidget( + bui.textwidget( edit=self._loading_text, - text=ba.Lstr(resource='internal.unavailableNoConnectionText'), + text=bui.Lstr(resource='internal.unavailableNoConnectionText'), color=(1, 0, 0), ) return # Show link(s) the user can use to sign in. - address = ( - ba.internal.get_master_server_address(version=2) + response.url - ) + address = plus.get_master_server_address(version=2) + response.url address_pretty = address.removeprefix('https://') - ba.textwidget( + assert bui.app.classic is not None + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 95), size=(0, 0), - text=ba.Lstr( + text=bui.Lstr( resource='accountSettingsWindow.v2LinkInstructionsText' ), - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, maxwidth=self._width * 0.9, h_align='center', v_align='center', ) button_width = 450 - if is_browser_likely_available(): - ba.buttonwidget( + if bui.is_browser_likely_available(): + bui.buttonwidget( parent=self._root_widget, position=( (self._width * 0.5 - button_width * 0.5), @@ -126,18 +121,18 @@ class V2ProxySignInWindow(ba.Window): ), autoselect=True, size=(button_width, 60), - label=ba.Lstr(value=address_pretty), + label=bui.Lstr(value=address_pretty), color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), - on_activate_call=lambda: ba.open_url(address), + on_activate_call=lambda: bui.open_url(address), ) qroffs = 0.0 else: - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 145), size=(0, 0), - text=ba.Lstr(value=address_pretty), + text=bui.Lstr(value=address_pretty), flatness=1.0, maxwidth=self._width, scale=0.75, @@ -147,45 +142,44 @@ class V2ProxySignInWindow(ba.Window): qroffs = 20.0 qr_size = 270 - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, position=( self._width * 0.5 - qr_size * 0.5, self._height * 0.36 + qroffs - qr_size * 0.5, ), size=(qr_size, qr_size), - texture=ba.internal.get_qrcode_texture(address), + texture=bui.get_qrcode_texture(address), ) # Start querying for results. self._proxyid = response.proxyid self._proxykey = response.proxykey - ba.timer( - STATUS_CHECK_INTERVAL_SECONDS, ba.WeakCall(self._ask_for_status) + bui.apptimer( + STATUS_CHECK_INTERVAL_SECONDS, bui.WeakCall(self._ask_for_status) ) def _ask_for_status(self) -> None: assert self._proxyid is not None assert self._proxykey is not None - ba.app.cloud.send_message_cb( + bui.app.cloud.send_message_cb( bacommon.cloud.LoginProxyStateQueryMessage( proxyid=self._proxyid, proxykey=self._proxykey ), - on_response=ba.WeakCall(self._got_status), + on_response=bui.WeakCall(self._got_status), ) def _got_status( self, response: bacommon.cloud.LoginProxyStateQueryResponse | Exception ) -> None: - # For now, if anything goes wrong on the server-side, just abort # with a vague error message. Can be more verbose later if need be. if ( isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse) and response.state is response.State.FAIL ): - ba.playsound(ba.getsound('error')) - ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) + bui.getsound('error').play() + bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0)) self._done() return @@ -195,17 +189,17 @@ class V2ProxySignInWindow(ba.Window): and response.state is response.State.SUCCESS ): assert response.credentials is not None - ba.app.accounts_v2.set_primary_credentials(response.credentials) + bui.app.accounts.set_primary_credentials(response.credentials) # As a courtesy, tell the server we're done with this proxy # so it can clean up (not a huge deal if this fails) assert self._proxyid is not None try: - ba.app.cloud.send_message_cb( + bui.app.cloud.send_message_cb( bacommon.cloud.LoginProxyCompleteMessage( proxyid=self._proxyid ), - on_response=ba.WeakCall(self._proxy_complete_response), + on_response=bui.WeakCall(self._proxy_complete_response), ) except CommunicationError: pass @@ -223,8 +217,9 @@ class V2ProxySignInWindow(ba.Window): isinstance(response, Exception) or response.state is response.State.WAITING ): - ba.timer( - STATUS_CHECK_INTERVAL_SECONDS, ba.WeakCall(self._ask_for_status) + bui.apptimer( + STATUS_CHECK_INTERVAL_SECONDS, + bui.WeakCall(self._ask_for_status), ) def _proxy_complete_response(self, response: None | Exception) -> None: @@ -233,4 +228,4 @@ class V2ProxySignInWindow(ba.Window): # this isn't critical so we'll just let anything slide. def _done(self) -> None: - ba.containerwidget(edit=self._root_widget, transition='out_scale') + bui.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/assets/src/ba_data/python/bastd/ui/account/viewer.py b/src/assets/ba_data/python/bastd/ui/account/viewer.py similarity index 78% rename from assets/src/ba_data/python/bastd/ui/account/viewer.py rename to src/assets/ba_data/python/bastd/ui/account/viewer.py index d8cdfbc9..bfce9387 100644 --- a/assets/src/ba_data/python/bastd/ui/account/viewer.py +++ b/src/assets/ba_data/python/bastd/ui/account/viewer.py @@ -5,16 +5,18 @@ from __future__ import annotations from typing import TYPE_CHECKING +import logging -import ba -import ba.internal -from bastd.ui import popup +from bastd.ui.popup import PopupWindow, PopupMenuWindow +import bauiv1 as bui if TYPE_CHECKING: from typing import Any + from bastd.ui.popup import PopupMenu -class AccountViewerWindow(popup.PopupWindow): + +class AccountViewerWindow(PopupWindow): """Popup window that displays info for an account.""" def __init__( @@ -25,18 +27,22 @@ class AccountViewerWindow(popup.PopupWindow): scale: float | None = None, offset: tuple[float, float] = (0.0, 0.0), ): - from ba.internal import is_browser_likely_available, master_server_get + if bui.app.classic is None: + raise RuntimeError('This requires classic support.') + + plus = bui.app.plus + assert plus is not None self._account_id = account_id self._profile_id = profile_id - uiscale = ba.app.ui.uiscale + uiscale = bui.app.classic.ui.uiscale if scale is None: scale = ( 2.6 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.8 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.4 ) self._transitioning_out = False @@ -44,18 +50,17 @@ class AccountViewerWindow(popup.PopupWindow): self._width = 400 self._height = ( 300 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 400 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 450 ) - self._subcontainer: ba.Widget | None = None + self._subcontainer: bui.Widget | None = None bg_color = (0.5, 0.4, 0.6) # Creates our _root_widget. - popup.PopupWindow.__init__( - self, + super().__init__( position=position, size=(self._width, self._height), scale=scale, @@ -63,7 +68,7 @@ class AccountViewerWindow(popup.PopupWindow): offset=offset, ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self.root_widget, position=(50, self._height - 30), size=(50, 50), @@ -72,37 +77,37 @@ class AccountViewerWindow(popup.PopupWindow): color=bg_color, on_activate_call=self._on_cancel_press, autoselect=True, - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=self.root_widget, position=(self._width * 0.5, self._height - 20), size=(0, 0), h_align='center', v_align='center', scale=0.6, - text=ba.Lstr(resource='playerInfoText'), + text=bui.Lstr(resource='playerInfoText'), maxwidth=200, color=(0.7, 0.7, 0.7, 0.7), ) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self.root_widget, size=(self._width - 60, self._height - 70), position=(30, 30), capture_arrows=True, simple_culling_v=10, ) - ba.widget(edit=self._scrollwidget, autoselect=True) + bui.widget(edit=self._scrollwidget, autoselect=True) - self._loading_text = ba.textwidget( + self._loading_text = bui.textwidget( parent=self._scrollwidget, scale=0.5, - text=ba.Lstr( + text=bui.Lstr( value='${A}...', - subs=[('${A}', ba.Lstr(resource='loadingText'))], + subs=[('${A}', bui.Lstr(resource='loadingText'))], ), size=(self._width - 60, 100), h_align='center', @@ -112,13 +117,12 @@ class AccountViewerWindow(popup.PopupWindow): # In cases where the user most likely has a browser/email, lets # offer a 'report this user' button. if ( - is_browser_likely_available() - and ba.internal.get_v1_account_misc_read_val( + bui.is_browser_likely_available() + and plus.get_v1_account_misc_read_val( 'showAccountExtrasMenu', False ) ): - - self._extras_menu_button = ba.buttonwidget( + self._extras_menu_button = bui.buttonwidget( parent=self.root_widget, size=(20, 20), position=(self._width - 60, self._height - 30), @@ -130,22 +134,22 @@ class AccountViewerWindow(popup.PopupWindow): on_activate_call=self._on_extras_menu_press, ) - ba.containerwidget( + bui.containerwidget( edit=self.root_widget, cancel_button=self._cancel_button ) - master_server_get( + bui.app.classic.master_server_v1_get( 'bsAccountInfo', { - 'buildNumber': ba.app.build_number, + 'buildNumber': bui.app.build_number, 'accountID': self._account_id, 'profileID': self._profile_id, }, - callback=ba.WeakCall(self._on_query_response), + callback=bui.WeakCall(self._on_query_response), ) def popup_menu_selected_choice( - self, window: popup.PopupMenu, choice: str + self, window: PopupMenu, choice: str ) -> None: """Called when a menu entry is selected.""" del window # Unused arg. @@ -158,29 +162,30 @@ class AccountViewerWindow(popup.PopupWindow): else: print('ERROR: unknown account info extras menu item:', choice) - def popup_menu_closing(self, window: popup.PopupMenu) -> None: + def popup_menu_closing(self, window: PopupMenu) -> None: """Called when the popup menu is closing.""" def _on_extras_menu_press(self) -> None: choices = ['more', 'report'] choices_display = [ - ba.Lstr(resource='coopSelectWindow.seeMoreText'), - ba.Lstr(resource='reportThisPlayerText'), + bui.Lstr(resource='coopSelectWindow.seeMoreText'), + bui.Lstr(resource='reportThisPlayerText'), ] is_admin = False if is_admin: - ba.screenmessage('TEMP FORCING ADMIN ON') + bui.screenmessage('TEMP FORCING ADMIN ON') choices.append('ban') - choices_display.append(ba.Lstr(resource='banThisPlayerText')) + choices_display.append(bui.Lstr(resource='banThisPlayerText')) - uiscale = ba.app.ui.uiscale - popup.PopupMenuWindow( + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + PopupMenuWindow( position=self._extras_menu_button.get_screen_space_center(), scale=( 2.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.65 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.23 ), choices=choices, @@ -190,10 +195,13 @@ class AccountViewerWindow(popup.PopupWindow): ) def _on_ban_press(self) -> None: - ba.internal.add_transaction( + plus = bui.app.plus + assert plus is not None + + plus.add_v1_account_transaction( {'type': 'BAN_ACCOUNT', 'account': self._account_id} ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() def _on_report_press(self) -> None: from bastd.ui import report @@ -203,8 +211,10 @@ class AccountViewerWindow(popup.PopupWindow): ) def _on_more_press(self) -> None: - ba.open_url( - ba.internal.get_master_server_address() + plus = bui.app.plus + assert plus is not None + bui.open_url( + plus.get_master_server_address() + '/highscores?profile=' + self._account_id ) @@ -215,10 +225,11 @@ class AccountViewerWindow(popup.PopupWindow): # pylint: disable=too-many-branches # pylint: disable=too-many-statements # pylint: disable=too-many-nested-blocks + assert bui.app.classic is not None if data is None: - ba.textwidget( + bui.textwidget( edit=self._loading_text, - text=ba.Lstr(resource='internal.unavailableNoConnectionText'), + text=bui.Lstr(resource='internal.unavailableNoConnectionText'), ) else: try: @@ -235,10 +246,10 @@ class AccountViewerWindow(popup.PopupWindow): if trophystr == '': trophystr = '-' except Exception: - ba.print_exception('Error displaying trophies.') + logging.exception('Error displaying trophies.') account_name_spacing = 15 tscale = 0.65 - ts_height = ba.internal.get_string_height( + ts_height = bui.get_string_height( trophystr, suppress_warning=True ) sub_width = self._width - 80 @@ -247,7 +258,7 @@ class AccountViewerWindow(popup.PopupWindow): + ts_height * tscale + account_name_spacing * len(data['accountDisplayStrings']) ) - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(sub_width, sub_height), background=False, @@ -263,7 +274,8 @@ class AccountViewerWindow(popup.PopupWindow): try: if data['profile'] is not None: profile = data['profile'] - character = ba.app.spaz_appearances.get( + assert bui.app.classic is not None + character = bui.app.classic.spaz_appearances.get( profile['character'], None ) if character is not None: @@ -279,33 +291,33 @@ class AccountViewerWindow(popup.PopupWindow): ) icon_tex = character.icon_texture tint_tex = character.icon_mask_texture - mask_texture = ba.gettexture( + mask_texture = bui.gettexture( 'characterIconMask' ) - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, position=(sub_width * center - 40, v - 80), size=(80, 80), color=(1, 1, 1), mask_texture=mask_texture, - texture=ba.gettexture(icon_tex), - tint_texture=ba.gettexture(tint_tex), + texture=bui.gettexture(icon_tex), + tint_texture=bui.gettexture(tint_tex), tint_color=tint_color, tint2_color=tint2_color, ) v -= 95 except Exception: - ba.print_exception('Error displaying character.') - ba.textwidget( + logging.exception('Error displaying character.') + bui.textwidget( parent=self._subcontainer, size=(0, 0), position=(sub_width * center, v), h_align='center', v_align='center', scale=0.9, - color=ba.safecolor(tint_color, 0.7), + color=bui.safecolor(tint_color, 0.7), shadow=1.0, - text=ba.Lstr(value=data['profileDisplayString']), + text=bui.Lstr(value=data['profileDisplayString']), maxwidth=sub_width * maxwidth_scale * 0.75, ) showing_character = True @@ -316,15 +328,15 @@ class AccountViewerWindow(popup.PopupWindow): v = sub_height - 20 if len(data['accountDisplayStrings']) <= 1: - account_title = ba.Lstr( + account_title = bui.Lstr( resource='settingsWindow.accountText' ) else: - account_title = ba.Lstr( + account_title = bui.Lstr( resource='accountSettingsWindow.accountsText', fallback_resource='settingsWindow.accountText', ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, size=(0, 0), position=(sub_width * center, v), @@ -332,7 +344,7 @@ class AccountViewerWindow(popup.PopupWindow): h_align='center', v_align='center', scale=title_scale, - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, text=account_title, maxwidth=sub_width * maxwidth_scale, ) @@ -341,7 +353,7 @@ class AccountViewerWindow(popup.PopupWindow): ) v -= 14 if draw_small else 20 for account_string in data['accountDisplayStrings']: - ba.textwidget( + bui.textwidget( parent=self._subcontainer, size=(0, 0), position=(sub_width * center, v), @@ -356,7 +368,7 @@ class AccountViewerWindow(popup.PopupWindow): v += account_name_spacing v -= 25 if showing_character else 29 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, size=(0, 0), position=(sub_width * center, v), @@ -364,8 +376,8 @@ class AccountViewerWindow(popup.PopupWindow): h_align='center', v_align='center', scale=title_scale, - color=ba.app.ui.infotextcolor, - text=ba.Lstr(resource='rankText'), + color=bui.app.classic.ui.infotextcolor, + text=bui.Lstr(resource='rankText'), maxwidth=sub_width * maxwidth_scale, ) v -= 14 @@ -373,17 +385,17 @@ class AccountViewerWindow(popup.PopupWindow): rank_str = '-' suffix_offset = None else: - str_raw = ba.Lstr( + str_raw = bui.Lstr( resource='league.rankInLeagueText' ).evaluate() # FIXME: Would be nice to not have to eval this. - rank_str = ba.Lstr( + rank_str = bui.Lstr( resource='league.rankInLeagueText', subs=[ ('${RANK}', str(data['rank'][2])), ( '${NAME}', - ba.Lstr( + bui.Lstr( translate=('leagueNames', data['rank'][0]) ), ), @@ -392,9 +404,7 @@ class AccountViewerWindow(popup.PopupWindow): ).evaluate() rank_str_width = min( sub_width * maxwidth_scale, - ba.internal.get_string_width( - rank_str, suppress_warning=True - ) + bui.get_string_width(rank_str, suppress_warning=True) * 0.55, ) @@ -408,7 +418,7 @@ class AccountViewerWindow(popup.PopupWindow): else: suffix_offset = None - ba.textwidget( + bui.textwidget( parent=self._subcontainer, size=(0, 0), position=(sub_width * center, v), @@ -420,7 +430,7 @@ class AccountViewerWindow(popup.PopupWindow): ) if suffix_offset is not None: assert data['rank'] is not None - ba.textwidget( + bui.textwidget( parent=self._subcontainer, size=(0, 0), position=(sub_width * center + suffix_offset, v + 3), @@ -432,29 +442,31 @@ class AccountViewerWindow(popup.PopupWindow): ) v -= 14 - str_raw = ba.Lstr(resource='league.rankInLeagueText').evaluate() + str_raw = bui.Lstr( + resource='league.rankInLeagueText' + ).evaluate() old_offs = -50 prev_ranks_shown = 0 for prev_rank in data['prevRanks']: - rank_str = ba.Lstr( + rank_str = bui.Lstr( value='${S}: ${I}', subs=[ ( '${S}', - ba.Lstr( + bui.Lstr( resource='league.seasonText', subs=[('${NUMBER}', str(prev_rank[0]))], ), ), ( '${I}', - ba.Lstr( + bui.Lstr( resource='league.rankInLeagueText', subs=[ ('${RANK}', str(prev_rank[3])), ( '${NAME}', - ba.Lstr( + bui.Lstr( translate=( 'leagueNames', prev_rank[1], @@ -469,9 +481,7 @@ class AccountViewerWindow(popup.PopupWindow): ).evaluate() rank_str_width = min( sub_width * maxwidth_scale, - ba.internal.get_string_width( - rank_str, suppress_warning=True - ) + bui.get_string_width(rank_str, suppress_warning=True) * 0.3, ) @@ -484,7 +494,7 @@ class AccountViewerWindow(popup.PopupWindow): suffix_offset = rank_str_width + 2 else: suffix_offset = None - ba.textwidget( + bui.textwidget( parent=self._subcontainer, size=(0, 0), position=(sub_width * center + old_offs, v), @@ -496,7 +506,7 @@ class AccountViewerWindow(popup.PopupWindow): maxwidth=sub_width * maxwidth_scale, ) if suffix_offset is not None: - ba.textwidget( + bui.textwidget( parent=self._subcontainer, size=(0, 0), position=( @@ -514,7 +524,7 @@ class AccountViewerWindow(popup.PopupWindow): v -= 13 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, size=(0, 0), position=(sub_width * center, v), @@ -522,12 +532,12 @@ class AccountViewerWindow(popup.PopupWindow): h_align='center', v_align='center', scale=title_scale, - color=ba.app.ui.infotextcolor, - text=ba.Lstr(resource='achievementsText'), + color=bui.app.classic.ui.infotextcolor, + text=bui.Lstr(resource='achievementsText'), maxwidth=sub_width * maxwidth_scale, ) v -= 14 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, size=(0, 0), position=(sub_width * center, v), @@ -536,7 +546,7 @@ class AccountViewerWindow(popup.PopupWindow): scale=0.55, text=str(data['achievementsCompleted']) + ' / ' - + str(len(ba.app.ach.achievements)), + + str(len(bui.app.classic.ach.achievements)), maxwidth=sub_width * maxwidth_scale, ) v -= 25 @@ -549,23 +559,23 @@ class AccountViewerWindow(popup.PopupWindow): center = 0.5 maxwidth_scale = 0.9 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, size=(0, 0), position=(sub_width * center, v), h_align='center', v_align='center', scale=title_scale, - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, flatness=1.0, - text=ba.Lstr( + text=bui.Lstr( resource='trophiesThisSeasonText', fallback_resource='trophiesText', ), maxwidth=sub_width * maxwidth_scale, ) v -= 19 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, size=(0, ts_height), position=(sub_width * 0.5, v - ts_height * tscale), @@ -576,7 +586,7 @@ class AccountViewerWindow(popup.PopupWindow): ) except Exception: - ba.print_exception('Error displaying account info.') + logging.exception('Error displaying account info.') def _on_cancel_press(self) -> None: self._transition_out() @@ -584,8 +594,8 @@ class AccountViewerWindow(popup.PopupWindow): def _transition_out(self) -> None: if not self._transitioning_out: self._transitioning_out = True - ba.containerwidget(edit=self.root_widget, transition='out_scale') + bui.containerwidget(edit=self.root_widget, transition='out_scale') def on_popup_cancel(self) -> None: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transition_out() diff --git a/assets/src/ba_data/python/bastd/ui/achievements.py b/src/assets/ba_data/python/bastd/ui/achievements.py similarity index 80% rename from assets/src/ba_data/python/bastd/ui/achievements.py rename to src/assets/ba_data/python/bastd/ui/achievements.py index cc862a95..c97d89c6 100644 --- a/assets/src/ba_data/python/bastd/ui/achievements.py +++ b/src/assets/ba_data/python/bastd/ui/achievements.py @@ -4,52 +4,47 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -import ba -from bastd.ui import popup - -if TYPE_CHECKING: - pass +from bastd.ui.popup import PopupWindow +import bauiv1 as bui -class AchievementsWindow(popup.PopupWindow): +class AchievementsWindow(PopupWindow): """Popup window to view achievements.""" def __init__( self, position: tuple[float, float], scale: float | None = None ): # pylint: disable=too-many-locals - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale if scale is None: scale = ( 2.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.65 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.23 ) self._transitioning_out = False self._width = 450 self._height = ( 300 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 370 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 450 ) bg_color = (0.5, 0.4, 0.6) # creates our _root_widget - popup.PopupWindow.__init__( - self, + super().__init__( position=position, size=(self._width, self._height), scale=scale, bg_color=bg_color, ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self.root_widget, position=(50, self._height - 30), size=(50, 50), @@ -58,21 +53,21 @@ class AchievementsWindow(popup.PopupWindow): color=bg_color, on_activate_call=self._on_cancel_press, autoselect=True, - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) - achievements = ba.app.ach.achievements + achievements = bui.app.classic.ach.achievements num_complete = len([a for a in achievements if a.complete]) - txt_final = ba.Lstr( + txt_final = bui.Lstr( resource='accountSettingsWindow.achievementProgressText', subs=[ ('${COUNT}', str(num_complete)), ('${TOTAL}', str(len(achievements))), ], ) - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=self.root_widget, position=(self._width * 0.5, self._height - 20), size=(0, 0), @@ -84,16 +79,16 @@ class AchievementsWindow(popup.PopupWindow): color=(1, 1, 1, 0.4), ) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self.root_widget, size=(self._width - 60, self._height - 70), position=(30, 30), capture_arrows=True, simple_culling_v=10, ) - ba.widget(edit=self._scrollwidget, autoselect=True) + bui.widget(edit=self._scrollwidget, autoselect=True) - ba.containerwidget( + bui.containerwidget( edit=self.root_widget, cancel_button=self._cancel_button ) @@ -104,7 +99,7 @@ class AchievementsWindow(popup.PopupWindow): eq_rsrc = 'coopSelectWindow.powerRankingPointsEqualsText' pts_rsrc = 'coopSelectWindow.powerRankingPointsText' - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(sub_width, sub_height), background=False, @@ -113,7 +108,7 @@ class AchievementsWindow(popup.PopupWindow): total_pts = 0 for i, ach in enumerate(achievements): complete = ach.complete - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(sub_width * 0.08 - 5, sub_height - 20 - incr * i), maxwidth=20, @@ -127,7 +122,7 @@ class AchievementsWindow(popup.PopupWindow): v_align='center', ) - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, position=(sub_width * 0.10 + 1, sub_height - 20 - incr * i - 9) if complete @@ -135,10 +130,10 @@ class AchievementsWindow(popup.PopupWindow): size=(18, 18) if complete else (27, 27), opacity=1.0 if complete else 0.3, color=ach.get_icon_color(complete)[:3], - texture=ach.get_icon_texture(complete), + texture=ach.get_icon_ui_texture(complete), ) if complete: - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, position=( sub_width * 0.10 - 4, @@ -146,9 +141,9 @@ class AchievementsWindow(popup.PopupWindow): ), size=(28, 28), color=(2, 1.4, 0), - texture=ba.gettexture('achievementOutline'), + texture=bui.gettexture('achievementOutline'), ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(sub_width * 0.19, sub_height - 19 - incr * i + 3), maxwidth=sub_width * 0.62, @@ -162,7 +157,7 @@ class AchievementsWindow(popup.PopupWindow): v_align='center', ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(sub_width * 0.19, sub_height - 19 - incr * i - 10), maxwidth=sub_width * 0.62, @@ -179,7 +174,7 @@ class AchievementsWindow(popup.PopupWindow): ) pts = ach.power_ranking_value - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(sub_width * 0.92, sub_height - 20 - incr * i), maxwidth=sub_width * 0.15, @@ -187,7 +182,9 @@ class AchievementsWindow(popup.PopupWindow): flatness=1.0, shadow=0.0, scale=0.6, - text=ba.Lstr(resource=pts_rsrc, subs=[('${NUMBER}', str(pts))]), + text=bui.Lstr( + resource=pts_rsrc, subs=[('${NUMBER}', str(pts))] + ), size=(0, 0), h_align='center', v_align='center', @@ -195,7 +192,7 @@ class AchievementsWindow(popup.PopupWindow): if complete: total_pts += pts - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=( sub_width * 1.0, @@ -206,13 +203,13 @@ class AchievementsWindow(popup.PopupWindow): color=(0.7, 0.8, 1.0), flatness=1.0, shadow=0.0, - text=ba.Lstr( + text=bui.Lstr( value='${A} ${B}', subs=[ - ('${A}', ba.Lstr(resource='coopSelectWindow.totalText')), + ('${A}', bui.Lstr(resource='coopSelectWindow.totalText')), ( '${B}', - ba.Lstr( + bui.Lstr( resource=eq_rsrc, subs=[('${NUMBER}', str(total_pts))], ), @@ -230,8 +227,8 @@ class AchievementsWindow(popup.PopupWindow): def _transition_out(self) -> None: if not self._transitioning_out: self._transitioning_out = True - ba.containerwidget(edit=self.root_widget, transition='out_scale') + bui.containerwidget(edit=self.root_widget, transition='out_scale') def on_popup_cancel(self) -> None: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transition_out() diff --git a/src/assets/ba_data/python/bastd/ui/appinvite.py b/src/assets/ba_data/python/bastd/ui/appinvite.py new file mode 100644 index 00000000..bce7c79d --- /dev/null +++ b/src/assets/ba_data/python/bastd/ui/appinvite.py @@ -0,0 +1,243 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI functionality related to inviting people to try the game.""" + +from __future__ import annotations + +import copy +import time +from typing import TYPE_CHECKING + +import bauiv1 as bui + +if TYPE_CHECKING: + from typing import Any + + +class ShowFriendCodeWindow(bui.Window): + """Window showing a code for sharing with friends.""" + + def __init__(self, data: dict[str, Any]): + bui.set_analytics_screen('Friend Promo Code') + self._width = 650 + self._height = 400 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + super().__init__( + root_widget=bui.containerwidget( + size=(self._width, self._height), + color=(0.45, 0.63, 0.15), + transition='in_scale', + scale=( + 1.7 + if uiscale is bui.UIScale.SMALL + else 1.35 + if uiscale is bui.UIScale.MEDIUM + else 1.0 + ), + ) + ) + self._data = copy.deepcopy(data) + bui.getsound('cashRegister').play() + bui.getsound('swish').play() + + self._cancel_button = bui.buttonwidget( + parent=self._root_widget, + scale=0.7, + position=(50, self._height - 50), + size=(60, 60), + label='', + on_activate_call=self.close, + autoselect=True, + color=(0.45, 0.63, 0.15), + icon=bui.gettexture('crossOut'), + iconscale=1.2, + ) + bui.containerwidget( + edit=self._root_widget, cancel_button=self._cancel_button + ) + + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.8), + size=(0, 0), + color=bui.app.classic.ui.infotextcolor, + scale=1.0, + flatness=1.0, + h_align='center', + v_align='center', + text=bui.Lstr(resource='gatherWindow.shareThisCodeWithFriendsText'), + maxwidth=self._width * 0.85, + ) + + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.645), + size=(0, 0), + color=(1.0, 3.0, 1.0), + scale=2.0, + h_align='center', + v_align='center', + text=data['code'], + maxwidth=self._width * 0.85, + ) + + award_str: str | bui.Lstr | None + if self._data['awardTickets'] != 0: + award_str = bui.Lstr( + resource='gatherWindow.friendPromoCodeAwardText', + subs=[('${COUNT}', str(self._data['awardTickets']))], + ) + else: + award_str = '' + bui.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height * 0.37), + size=(0, 0), + color=bui.app.classic.ui.infotextcolor, + scale=1.0, + flatness=1.0, + h_align='center', + v_align='center', + text=bui.Lstr( + value='${A}\n${B}\n${C}\n${D}', + subs=[ + ( + '${A}', + bui.Lstr( + resource=( + 'gatherWindow.friendPromoCodeRedeemLongText' + ), + subs=[ + ('${COUNT}', str(self._data['tickets'])), + ( + '${MAX_USES}', + str(self._data['usesRemaining']), + ), + ], + ), + ), + ( + '${B}', + bui.Lstr( + resource=( + 'gatherWindow.friendPromoCodeWhereToEnterText' + ) + ), + ), + ('${C}', award_str), + ( + '${D}', + bui.Lstr( + resource='gatherWindow.friendPromoCodeExpireText', + subs=[ + ( + '${EXPIRE_HOURS}', + str(self._data['expireHours']), + ) + ], + ), + ), + ], + ), + maxwidth=self._width * 0.9, + max_height=self._height * 0.35, + ) + + if bui.is_browser_likely_available(): + xoffs = 0 + bui.buttonwidget( + parent=self._root_widget, + size=(200, 40), + position=(self._width * 0.5 - 100 + xoffs, 39), + autoselect=True, + label=bui.Lstr(resource='gatherWindow.emailItText'), + on_activate_call=bui.WeakCall(self._email), + ) + + def _email(self) -> None: + import urllib.parse + + plus = bui.app.plus + assert plus is not None + + # If somehow we got signed out. + if plus.get_v1_account_state() != 'signed_in': + bui.screenmessage( + bui.Lstr(resource='notSignedInText'), color=(1, 0, 0) + ) + bui.getsound('error').play() + return + + bui.set_analytics_screen('Email Friend Code') + subject = ( + bui.Lstr(resource='gatherWindow.friendHasSentPromoCodeText') + .evaluate() + .replace('${NAME}', plus.get_v1_account_name()) + .replace('${APP_NAME}', bui.Lstr(resource='titleText').evaluate()) + .replace('${COUNT}', str(self._data['tickets'])) + ) + body = ( + bui.Lstr(resource='gatherWindow.youHaveBeenSentAPromoCodeText') + .evaluate() + .replace('${APP_NAME}', bui.Lstr(resource='titleText').evaluate()) + + '\n\n' + + str(self._data['code']) + + '\n\n' + ) + body += ( + ( + bui.Lstr(resource='gatherWindow.friendPromoCodeRedeemShortText') + .evaluate() + .replace('${COUNT}', str(self._data['tickets'])) + ) + + '\n\n' + + bui.Lstr(resource='gatherWindow.friendPromoCodeInstructionsText') + .evaluate() + .replace('${APP_NAME}', bui.Lstr(resource='titleText').evaluate()) + + '\n' + + bui.Lstr(resource='gatherWindow.friendPromoCodeExpireText') + .evaluate() + .replace('${EXPIRE_HOURS}', str(self._data['expireHours'])) + + '\n' + + bui.Lstr(resource='enjoyText').evaluate() + ) + bui.open_url( + 'mailto:?subject=' + + urllib.parse.quote(subject) + + '&body=' + + urllib.parse.quote(body) + ) + + def close(self) -> None: + """Close the window.""" + bui.containerwidget(edit=self._root_widget, transition='out_scale') + + +def handle_app_invites_press() -> None: + """(internal)""" + app = bui.app + plus = app.plus + assert plus is not None + + bui.screenmessage( + bui.Lstr(resource='gatherWindow.requestingAPromoCodeText'), + color=(0, 1, 0), + ) + + def handle_result(result: dict[str, Any] | None) -> None: + if result is None: + bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0)) + bui.getsound('error').play() + else: + ShowFriendCodeWindow(result) + + plus.add_v1_account_transaction( + { + 'type': 'FRIEND_PROMO_CODE_REQUEST', + 'ali': False, + 'expire_time': time.time() + 10, + }, + callback=handle_result, + ) + plus.run_v1_account_transactions() diff --git a/assets/src/ba_data/python/bastd/ui/characterpicker.py b/src/assets/ba_data/python/bastd/ui/characterpicker.py similarity index 78% rename from assets/src/ba_data/python/bastd/ui/characterpicker.py rename to src/assets/ba_data/python/bastd/ui/characterpicker.py index 490c0626..0ca01f6f 100644 --- a/assets/src/ba_data/python/bastd/ui/characterpicker.py +++ b/src/assets/ba_data/python/bastd/ui/characterpicker.py @@ -7,20 +7,19 @@ from __future__ import annotations import math from typing import TYPE_CHECKING -import ba -import ba.internal -from bastd.ui import popup +from bastd.ui.popup import PopupWindow +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Sequence -class CharacterPicker(popup.PopupWindow): +class CharacterPicker(PopupWindow): """Popup window for selecting characters.""" def __init__( self, - parent: ba.Widget, + parent: bui.Widget, position: tuple[float, float] = (0.0, 0.0), delegate: Any = None, scale: float | None = None, @@ -32,14 +31,16 @@ class CharacterPicker(popup.PopupWindow): # pylint: disable=too-many-locals from bastd.actor import spazappearance + assert bui.app.classic is not None + del parent # unused here - uiscale = ba.app.ui.uiscale + uiscale = bui.app.classic.ui.uiscale if scale is None: scale = ( 1.85 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.65 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.23 ) @@ -50,11 +51,13 @@ class CharacterPicker(popup.PopupWindow): self._spazzes = spazappearance.get_appearances() self._spazzes.sort() self._icon_textures = [ - ba.gettexture(ba.app.spaz_appearances[s].icon_texture) + bui.gettexture(bui.app.classic.spaz_appearances[s].icon_texture) for s in self._spazzes ] self._icon_tint_textures = [ - ba.gettexture(ba.app.spaz_appearances[s].icon_mask_texture) + bui.gettexture( + bui.app.classic.spaz_appearances[s].icon_mask_texture + ) for s in self._spazzes ] @@ -72,7 +75,7 @@ class CharacterPicker(popup.PopupWindow): 1.0 / 0.95 ) * (1.0 / 0.8) self._height = self._width * ( - 0.8 if uiscale is ba.UIScale.SMALL else 1.06 + 0.8 if uiscale is bui.UIScale.SMALL else 1.06 ) self._scroll_width = self._width * 0.8 @@ -82,9 +85,8 @@ class CharacterPicker(popup.PopupWindow): (self._height - self._scroll_height) * 0.5, ) - # creates our _root_widget - popup.PopupWindow.__init__( - self, + # Creates our _root_widget. + super().__init__( position=position, size=(self._width, self._height), scale=scale, @@ -94,26 +96,26 @@ class CharacterPicker(popup.PopupWindow): focus_size=(self._scroll_width, self._scroll_height), ) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self.root_widget, size=(self._scroll_width, self._scroll_height), color=(0.55, 0.55, 0.55), highlight=False, position=self._scroll_position, ) - ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) + bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) self._sub_width = self._scroll_width * 0.95 self._sub_height = ( 5 + rows * (button_height + 2 * button_buffer_v) + 100 ) - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._sub_width, self._sub_height), background=False, ) index = 0 - mask_texture = ba.gettexture('characterIconMask') + mask_texture = bui.gettexture('characterIconMask') for y in range(rows): for x in range(columns): pos = ( @@ -122,7 +124,7 @@ class CharacterPicker(popup.PopupWindow): - (y + 1) * (button_height + 2 * button_buffer_v) + 12, ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._subcontainer, button_type='square', size=(button_width, button_height), @@ -134,22 +136,22 @@ class CharacterPicker(popup.PopupWindow): color=(1, 1, 1), tint_color=tint_color, tint2_color=tint2_color, - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._select_character, self._spazzes[index] ), position=pos, ) - ba.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) + bui.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) if self._spazzes[index] == selected_character: - ba.containerwidget( + bui.containerwidget( edit=self._subcontainer, selected_child=btn, visible_child=btn, ) - name = ba.Lstr( + name = bui.Lstr( translate=('characterNames', self._spazzes[index]) ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, text=name, position=(pos[0] + button_width * 0.5, pos[1] - 12), @@ -167,23 +169,26 @@ class CharacterPicker(popup.PopupWindow): break if index >= count: break - self._get_more_characters_button = btn = ba.buttonwidget( + self._get_more_characters_button = btn = bui.buttonwidget( parent=self._subcontainer, size=(self._sub_width * 0.8, 60), position=(self._sub_width * 0.1, 30), - label=ba.Lstr(resource='editProfileWindow.getMoreCharactersText'), + label=bui.Lstr(resource='editProfileWindow.getMoreCharactersText'), on_activate_call=self._on_store_press, color=(0.6, 0.6, 0.6), textcolor=(0.8, 0.8, 0.8), autoselect=True, ) - ba.widget(edit=btn, show_buffer_top=30, show_buffer_bottom=30) + bui.widget(edit=btn, show_buffer_top=30, show_buffer_bottom=30) def _on_store_press(self) -> None: from bastd.ui.account import show_sign_in_prompt from bastd.ui.store.browser import StoreBrowserWindow - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._transition_out() @@ -201,8 +206,8 @@ class CharacterPicker(popup.PopupWindow): def _transition_out(self) -> None: if not self._transitioning_out: self._transitioning_out = True - ba.containerwidget(edit=self.root_widget, transition='out_scale') + bui.containerwidget(edit=self.root_widget, transition='out_scale') def on_popup_cancel(self) -> None: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transition_out() diff --git a/assets/src/ba_data/python/bastd/ui/colorpicker.py b/src/assets/ba_data/python/bastd/ui/colorpicker.py similarity index 78% rename from assets/src/ba_data/python/bastd/ui/colorpicker.py rename to src/assets/ba_data/python/bastd/ui/colorpicker.py index 378f56f1..dc4110e2 100644 --- a/assets/src/ba_data/python/bastd/ui/colorpicker.py +++ b/src/assets/ba_data/python/bastd/ui/colorpicker.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba from bastd.ui.popup import PopupWindow +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Sequence @@ -21,7 +21,7 @@ class ColorPicker(PopupWindow): def __init__( self, - parent: ba.Widget, + parent: bui.Widget, position: tuple[float, float], initial_color: Sequence[float] = (1.0, 1.0, 1.0), delegate: Any = None, @@ -30,19 +30,19 @@ class ColorPicker(PopupWindow): tag: Any = '', ): # pylint: disable=too-many-locals - from ba.internal import get_player_colors + assert bui.app.classic is not None - c_raw = get_player_colors() + c_raw = bui.app.classic.get_player_colors() assert len(c_raw) == 16 self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]] - uiscale = ba.app.ui.uiscale + uiscale = bui.app.classic.ui.uiscale if scale is None: scale = ( 2.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.65 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.23 ) self._parent = parent @@ -55,8 +55,7 @@ class ColorPicker(PopupWindow): self._initial_color = initial_color # Create our _root_widget. - PopupWindow.__init__( - self, + super().__init__( position=position, size=(210, 240), scale=scale, @@ -65,11 +64,11 @@ class ColorPicker(PopupWindow): bg_color=(0.5, 0.5, 0.5), offset=offset, ) - rows: list[list[ba.Widget]] = [] + rows: list[list[bui.Widget]] = [] closest_dist = 9999.0 closest = (0, 0) for y in range(4): - row: list[ba.Widget] = [] + row: list[bui.Widget] = [] rows.append(row) for x in range(4): color = self.colors[y][x] @@ -81,52 +80,53 @@ class ColorPicker(PopupWindow): if dist < closest_dist: closest = (x, y) closest_dist = dist - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self.root_widget, position=(22 + 45 * x, 185 - 45 * y), size=(35, 40), label='', button_type='square', - on_activate_call=ba.WeakCall(self._select, x, y), + on_activate_call=bui.WeakCall(self._select, x, y), autoselect=True, color=color, extra_touch_border_scale=0.0, ) row.append(btn) - other_button = ba.buttonwidget( + other_button = bui.buttonwidget( parent=self.root_widget, position=(105 - 60, 13), color=(0.7, 0.7, 0.7), text_scale=0.5, textcolor=(0.8, 0.8, 0.8), size=(120, 30), - label=ba.Lstr( + label=bui.Lstr( resource='otherText', fallback_resource='coopSelectWindow.customText', ), autoselect=True, - on_activate_call=ba.WeakCall(self._select_other), + on_activate_call=bui.WeakCall(self._select_other), ) # Custom colors are limited to pro currently. - if not ba.app.accounts_v1.have_pro(): - ba.imagewidget( + assert bui.app.classic is not None + if not bui.app.classic.accounts.have_pro(): + bui.imagewidget( parent=self.root_widget, position=(50, 12), size=(30, 30), - texture=ba.gettexture('lock'), + texture=bui.gettexture('lock'), draw_controller=other_button, ) # If their color is close to one of our swatches, select it. # Otherwise select 'other'. if closest_dist < 0.03: - ba.containerwidget( + bui.containerwidget( edit=self.root_widget, selected_child=rows[closest[1]][closest[0]], ) else: - ba.containerwidget( + bui.containerwidget( edit=self.root_widget, selected_child=other_button ) @@ -138,7 +138,8 @@ class ColorPicker(PopupWindow): from bastd.ui import purchase # Requires pro. - if not ba.app.accounts_v1.have_pro(): + assert bui.app.classic is not None + if not bui.app.classic.accounts.have_pro(): purchase.PurchaseWindow(items=['pro']) self._transition_out() return @@ -160,18 +161,18 @@ class ColorPicker(PopupWindow): def _select(self, x: int, y: int) -> None: if self._delegate: self._delegate.color_picker_selected_color(self, self.colors[y][x]) - ba.timer(0.05, self._transition_out, timetype=ba.TimeType.REAL) + bui.apptimer(0.05, self._transition_out) def _transition_out(self) -> None: if not self._transitioning_out: self._transitioning_out = True if self._delegate is not None: self._delegate.color_picker_closing(self) - ba.containerwidget(edit=self.root_widget, transition='out_scale') + bui.containerwidget(edit=self.root_widget, transition='out_scale') def on_popup_cancel(self) -> None: if not self._transitioning_out: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transition_out() @@ -181,7 +182,7 @@ class ColorPickerExact(PopupWindow): def __init__( self, - parent: ba.Widget, + parent: bui.Widget, position: tuple[float, float], initial_color: Sequence[float] = (1.0, 1.0, 1.0), delegate: Any = None, @@ -191,28 +192,26 @@ class ColorPickerExact(PopupWindow): ): # pylint: disable=too-many-locals del parent # Unused var. - from ba.internal import get_player_colors + assert bui.app.classic is not None - c_raw = get_player_colors() + c_raw = bui.app.classic.get_player_colors() assert len(c_raw) == 16 self.colors = [c_raw[0:4], c_raw[4:8], c_raw[8:12], c_raw[12:16]] - uiscale = ba.app.ui.uiscale + uiscale = bui.app.classic.ui.uiscale if scale is None: scale = ( 2.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.65 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.23 ) self._delegate = delegate self._transitioning_out = False self._tag = tag self._color = list(initial_color) - self._last_press_time = ba.time( - ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS - ) + self._last_press_time = bui.apptime() self._last_press_color_name: str | None = None self._last_press_increasing: bool | None = None self._change_speed = 1.0 @@ -220,8 +219,7 @@ class ColorPickerExact(PopupWindow): height = 240.0 # Creates our _root_widget. - PopupWindow.__init__( - self, + super().__init__( position=position, size=(width, height), scale=scale, @@ -230,24 +228,24 @@ class ColorPickerExact(PopupWindow): bg_color=(0.5, 0.5, 0.5), offset=offset, ) - self._swatch = ba.imagewidget( + self._swatch = bui.imagewidget( parent=self.root_widget, position=(width * 0.5 - 50, height - 70), size=(100, 70), - texture=ba.gettexture('buttonSquare'), + texture=bui.gettexture('buttonSquare'), color=(1, 0, 0), ) x = 50 y = height - 90 - self._label_r: ba.Widget - self._label_g: ba.Widget - self._label_b: ba.Widget + self._label_r: bui.Widget + self._label_g: bui.Widget + self._label_b: bui.Widget for color_name, color_val in [ ('r', (1, 0.15, 0.15)), ('g', (0.15, 1, 0.15)), ('b', (0.15, 0.15, 1)), ]: - txt = ba.textwidget( + txt = bui.textwidget( parent=self.root_widget, position=(x - 10, y), size=(0, 0), @@ -258,7 +256,7 @@ class ColorPickerExact(PopupWindow): ) setattr(self, '_label_' + color_name, txt) for b_label, bhval, binc in [('-', 30, False), ('+', 75, True)]: - ba.buttonwidget( + bui.buttonwidget( parent=self.root_widget, position=(x + bhval, y - 15), scale=0.8, @@ -268,24 +266,24 @@ class ColorPickerExact(PopupWindow): label=b_label, autoselect=True, enable_sound=False, - on_activate_call=ba.WeakCall( + on_activate_call=bui.WeakCall( self._color_change_press, color_name, binc ), ) y -= 42 - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self.root_widget, position=(width * 0.5 - 40, 10), size=(80, 30), text_scale=0.6, color=(0.6, 0.6, 0.6), textcolor=(0.7, 0.7, 0.7), - label=ba.Lstr(resource='doneText'), - on_activate_call=ba.WeakCall(self._transition_out), + label=bui.Lstr(resource='doneText'), + on_activate_call=bui.WeakCall(self._transition_out), autoselect=True, ) - ba.containerwidget(edit=self.root_widget, start_button=btn) + bui.containerwidget(edit=self.root_widget, start_button=btn) # Unlike the swatch picker, we stay open and constantly push our # color to the delegate, so start doing that. @@ -295,23 +293,23 @@ class ColorPickerExact(PopupWindow): def _update_for_color(self) -> None: if not self.root_widget: return - ba.imagewidget(edit=self._swatch, color=self._color) + bui.imagewidget(edit=self._swatch, color=self._color) # We generate these procedurally, so pylint misses them. # FIXME: create static attrs instead. # pylint: disable=consider-using-f-string - ba.textwidget(edit=self._label_r, text='%.2f' % self._color[0]) - ba.textwidget(edit=self._label_g, text='%.2f' % self._color[1]) - ba.textwidget(edit=self._label_b, text='%.2f' % self._color[2]) + bui.textwidget(edit=self._label_r, text='%.2f' % self._color[0]) + bui.textwidget(edit=self._label_g, text='%.2f' % self._color[1]) + bui.textwidget(edit=self._label_b, text='%.2f' % self._color[2]) if self._delegate is not None: self._delegate.color_picker_selected_color(self, self._color) def _color_change_press(self, color_name: str, increasing: bool) -> None: # If we get rapid-fire presses, eventually start moving faster. - current_time = ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) + current_time = bui.apptime() since_last = current_time - self._last_press_time if ( - since_last < 200 + since_last < 0.2 and self._last_press_color_name == color_name and self._last_press_increasing == increasing ): @@ -338,9 +336,9 @@ class ColorPickerExact(PopupWindow): self._transitioning_out = True if self._delegate is not None: self._delegate.color_picker_closing(self) - ba.containerwidget(edit=self.root_widget, transition='out_scale') + bui.containerwidget(edit=self.root_widget, transition='out_scale') def on_popup_cancel(self) -> None: if not self._transitioning_out: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transition_out() diff --git a/assets/src/ba_data/python/bastd/ui/config.py b/src/assets/ba_data/python/bastd/ui/config.py similarity index 78% rename from assets/src/ba_data/python/bastd/ui/config.py rename to src/assets/ba_data/python/bastd/ui/config.py index b2008665..15032cfa 100644 --- a/assets/src/ba_data/python/bastd/ui/config.py +++ b/src/assets/ba_data/python/bastd/ui/config.py @@ -6,7 +6,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable @@ -19,16 +19,16 @@ class ConfigCheckBox: value changes. """ - widget: ba.Widget - """The underlying ba.Widget instance.""" + widget: bui.Widget + """The underlying bui.Widget instance.""" def __init__( self, - parent: ba.Widget, + parent: bui.Widget, configkey: str, position: tuple[float, float], size: tuple[float, float], - displayname: str | ba.Lstr | None = None, + displayname: str | bui.Lstr | None = None, scale: float | None = None, maxwidth: float | None = None, autoselect: bool = True, @@ -38,23 +38,23 @@ class ConfigCheckBox: displayname = configkey self._value_change_call = value_change_call self._configkey = configkey - self.widget = ba.checkboxwidget( + self.widget = bui.checkboxwidget( parent=parent, autoselect=autoselect, position=position, size=size, text=displayname, textcolor=(0.8, 0.8, 0.8), - value=ba.app.config.resolve(configkey), + value=bui.app.config.resolve(configkey), on_value_change_call=self._value_changed, scale=scale, maxwidth=maxwidth, ) # complain if we outlive our checkbox - ba.uicleanupcheck(self, self.widget) + bui.uicleanupcheck(self, self.widget) def _value_changed(self, val: bool) -> None: - cfg = ba.app.config + cfg = bui.app.config cfg[self._configkey] = val if self._value_change_call is not None: self._value_change_call(val) @@ -68,21 +68,21 @@ class ConfigNumberEdit: value changes. """ - nametext: ba.Widget + nametext: bui.Widget """The text widget displaying the name.""" - valuetext: ba.Widget + valuetext: bui.Widget """The text widget displaying the current value.""" - minusbutton: ba.Widget + minusbutton: bui.Widget """The button widget used to reduce the value.""" - plusbutton: ba.Widget + plusbutton: bui.Widget """The button widget used to increase the value.""" def __init__( self, - parent: ba.Widget, + parent: bui.Widget, configkey: str, position: tuple[float, float], minval: float = 0.0, @@ -90,7 +90,7 @@ class ConfigNumberEdit: increment: float = 1.0, callback: Callable[[float], Any] | None = None, xoffset: float = 0.0, - displayname: str | ba.Lstr | None = None, + displayname: str | bui.Lstr | None = None, changesound: bool = True, textscale: float = 1.0, ): @@ -102,9 +102,9 @@ class ConfigNumberEdit: self._maxval = maxval self._increment = increment self._callback = callback - self._value = ba.app.config.resolve(configkey) + self._value = bui.app.config.resolve(configkey) - self.nametext = ba.textwidget( + self.nametext = bui.textwidget( parent=parent, position=position, size=(100, 30), @@ -115,7 +115,7 @@ class ConfigNumberEdit: v_align='center', scale=textscale, ) - self.valuetext = ba.textwidget( + self.valuetext = bui.textwidget( parent=parent, position=(246 + xoffset, position[1]), size=(60, 28), @@ -126,28 +126,28 @@ class ConfigNumberEdit: text=str(self._value), padding=2, ) - self.minusbutton = ba.buttonwidget( + self.minusbutton = bui.buttonwidget( parent=parent, position=(330 + xoffset, position[1]), size=(28, 28), label='-', autoselect=True, - on_activate_call=ba.Call(self._down), + on_activate_call=bui.Call(self._down), repeat=True, enable_sound=changesound, ) - self.plusbutton = ba.buttonwidget( + self.plusbutton = bui.buttonwidget( parent=parent, position=(380 + xoffset, position[1]), size=(28, 28), label='+', autoselect=True, - on_activate_call=ba.Call(self._up), + on_activate_call=bui.Call(self._up), repeat=True, enable_sound=changesound, ) # Complain if we outlive our widgets. - ba.uicleanupcheck(self, self.nametext) + bui.uicleanupcheck(self, self.nametext) self._update_display() def _up(self) -> None: @@ -162,8 +162,8 @@ class ConfigNumberEdit: self._update_display() if self._callback: self._callback(self._value) - ba.app.config[self._configkey] = self._value - ba.app.config.apply_and_commit() + bui.app.config[self._configkey] = self._value + bui.app.config.apply_and_commit() def _update_display(self) -> None: - ba.textwidget(edit=self.valuetext, text=f'{self._value:.1f}') + bui.textwidget(edit=self.valuetext, text=f'{self._value:.1f}') diff --git a/assets/src/ba_data/python/bastd/ui/configerror.py b/src/assets/ba_data/python/bastd/ui/configerror.py similarity index 65% rename from assets/src/ba_data/python/bastd/ui/configerror.py rename to src/assets/ba_data/python/bastd/ui/configerror.py index 3165bfc3..da87065d 100644 --- a/assets/src/ba_data/python/bastd/ui/configerror.py +++ b/src/assets/ba_data/python/bastd/ui/configerror.py @@ -4,26 +4,20 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -import ba -import ba.internal - -if TYPE_CHECKING: - pass +import bauiv1 as bui -class ConfigErrorWindow(ba.Window): +class ConfigErrorWindow(bui.Window): """Window for dealing with a broken config.""" def __init__(self) -> None: - self._config_file_path = ba.app.config_file_path + self._config_file_path = bui.app.config_file_path width = 800 super().__init__( - ba.containerwidget(size=(width, 400), transition='in_right') + bui.containerwidget(size=(width, 400), transition='in_right') ) padding = 20 - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(padding, 220 + 60), size=(width - 2 * padding, 100 - 2 * padding), @@ -31,7 +25,7 @@ class ConfigErrorWindow(ba.Window): v_align='top', scale=0.73, text=( - f'Error reading {ba.internal.appnameupper()} config file' + f'Error reading {bui.appnameupper()} config file' ':\n\n\nCheck the console' ' (press ~ twice) for details.\n\nWould you like to quit and' ' try to fix it by hand\nor overwrite it with defaults?\n\n' @@ -39,7 +33,7 @@ class ConfigErrorWindow(ba.Window): ' overwrite)' ), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(padding, 198 + 60), size=(width - 2 * padding, 100 - 2 * padding), @@ -48,41 +42,39 @@ class ConfigErrorWindow(ba.Window): scale=0.5, text=self._config_file_path, ) - quit_button = ba.buttonwidget( + quit_button = bui.buttonwidget( parent=self._root_widget, position=(35, 30), size=(240, 54), label='Quit and Edit', on_activate_call=self._quit, ) - ba.buttonwidget( + bui.buttonwidget( parent=self._root_widget, position=(width - 370, 30), size=(330, 54), label='Overwrite with Defaults', on_activate_call=self._defaults, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=quit_button, selected_child=quit_button, ) def _quit(self) -> None: - ba.timer(0.001, self._edit_and_quit, timetype=ba.TimeType.REAL) - ba.internal.lock_all_input() + bui.apptimer(0.001, self._edit_and_quit) + bui.lock_all_input() def _edit_and_quit(self) -> None: - ba.internal.open_file_externally(self._config_file_path) - ba.timer(0.1, ba.quit, timetype=ba.TimeType.REAL) + bui.open_file_externally(self._config_file_path) + bui.apptimer(0.1, bui.quit) def _defaults(self) -> None: - from ba.internal import commit_app_config - - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.playsound(ba.getsound('gunCocking')) - ba.screenmessage('settings reset.', color=(1, 1, 0)) + bui.containerwidget(edit=self._root_widget, transition='out_left') + bui.getsound('gunCocking').play() + bui.screenmessage('settings reset.', color=(1, 1, 0)) # At this point settings are already set; lets just commit them # to disk. - commit_app_config(force=True) + bui.commit_app_config(force=True) diff --git a/assets/src/ba_data/python/bastd/ui/confirm.py b/src/assets/ba_data/python/bastd/ui/confirm.py similarity index 70% rename from assets/src/ba_data/python/bastd/ui/confirm.py rename to src/assets/ba_data/python/bastd/ui/confirm.py index 74e32dca..82bccef0 100644 --- a/assets/src/ba_data/python/bastd/ui/confirm.py +++ b/src/assets/ba_data/python/bastd/ui/confirm.py @@ -5,9 +5,9 @@ from __future__ import annotations from typing import TYPE_CHECKING +import logging -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable @@ -18,7 +18,7 @@ class ConfirmWindow: def __init__( self, - text: str | ba.Lstr = 'Are you sure?', + text: str | bui.Lstr = 'Are you sure?', action: Callable[[], Any] | None = None, width: float = 360.0, height: float = 100.0, @@ -26,15 +26,15 @@ class ConfirmWindow: cancel_is_selected: bool = False, color: tuple[float, float, float] = (1, 1, 1), text_scale: float = 1.0, - ok_text: str | ba.Lstr | None = None, - cancel_text: str | ba.Lstr | None = None, - origin_widget: ba.Widget | None = None, + ok_text: str | bui.Lstr | None = None, + cancel_text: str | bui.Lstr | None = None, + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-locals if ok_text is None: - ok_text = ba.Lstr(resource='okText') + ok_text = bui.Lstr(resource='okText') if cancel_text is None: - cancel_text = ba.Lstr(resource='cancelText') + cancel_text = bui.Lstr(resource='cancelText') height += 40 width = max(width, 360) self._action = action @@ -51,23 +51,24 @@ class ConfirmWindow: scale_origin = None transition = 'in_right' - uiscale = ba.app.ui.uiscale - self.root_widget = ba.containerwidget( + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self.root_widget = bui.containerwidget( size=(width, height), transition=transition, toolbar_visibility='menu_minimal_no_back', - parent=ba.internal.get_special_widget('overlay_stack'), + parent=bui.get_special_widget('overlay_stack'), scale=( 2.1 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.5 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), scale_origin_stack_offset=scale_origin, ) - ba.textwidget( + bui.textwidget( parent=self.root_widget, position=(width * 0.5, height - 5 - (height - 75) * 0.5), size=(0, 0), @@ -80,9 +81,9 @@ class ConfirmWindow: max_height=height - 75, ) - cbtn: ba.Widget | None + cbtn: bui.Widget | None if cancel_button: - cbtn = btn = ba.buttonwidget( + cbtn = btn = bui.buttonwidget( parent=self.root_widget, autoselect=True, position=(20, 20), @@ -90,7 +91,7 @@ class ConfirmWindow: label=cancel_text, on_activate_call=self._cancel, ) - ba.containerwidget(edit=self.root_widget, cancel_button=btn) + bui.containerwidget(edit=self.root_widget, cancel_button=btn) ok_button_h = width - 175 else: # if they don't want a cancel button, we still want back presses to @@ -98,7 +99,7 @@ class ConfirmWindow: # button ok_button_h = width * 0.5 - 75 cbtn = None - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self.root_widget, autoselect=True, position=(ok_button_h, 20), @@ -110,11 +111,11 @@ class ConfirmWindow: # if they didn't want a cancel button, we still want to be able to hit # cancel/back/etc to dismiss the window if not cancel_button: - ba.containerwidget( + bui.containerwidget( edit=self.root_widget, on_cancel_call=btn.activate ) - ba.containerwidget( + bui.containerwidget( edit=self.root_widget, selected_child=( cbtn if cbtn is not None and cancel_is_selected else btn @@ -123,7 +124,7 @@ class ConfirmWindow: ) def _cancel(self) -> None: - ba.containerwidget( + bui.containerwidget( edit=self.root_widget, transition=( 'out_right' @@ -135,7 +136,7 @@ class ConfirmWindow: def _ok(self) -> None: if not self.root_widget: return - ba.containerwidget( + bui.containerwidget( edit=self.root_widget, transition=( 'out_left' @@ -154,10 +155,11 @@ class QuitWindow: self, swish: bool = False, back: bool = False, - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): - ui = ba.app.ui - app = ba.app + assert bui.app.classic is not None + ui = bui.app.classic.ui + app = bui.app self._back = back # If there's already one of us up somewhere, kill it. @@ -165,26 +167,38 @@ class QuitWindow: ui.quit_window.delete() ui.quit_window = None if swish: - ba.playsound(ba.getsound('swish')) - quit_resource = ( - 'quitGameText' if app.platform == 'mac' else 'exitGameText' - ) + bui.getsound('swish').play() + + if app.classic is None: + if bui.do_once(): + logging.warning( + 'QuitWindow needs to be updated to work without classic.' + ) + quit_resource = 'exitGameText' + else: + quit_resource = ( + 'quitGameText' + if app.classic.platform == 'mac' + else 'exitGameText' + ) self._root_widget = ui.quit_window = ConfirmWindow( - ba.Lstr( + bui.Lstr( resource=quit_resource, - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], + subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ), self._fade_and_quit, origin_widget=origin_widget, ).root_widget def _fade_and_quit(self) -> None: - ba.internal.fade_screen( - False, time=0.2, endcall=lambda: ba.quit(soft=True, back=self._back) + bui.fade_screen( + False, + time=0.2, + endcall=lambda: bui.quit(soft=True, back=self._back), ) - ba.internal.lock_all_input() + bui.lock_all_input() # Unlock and fade back in shortly.. just in case something goes wrong # (or on android where quit just backs out of our activity and # we may come back) - ba.timer(0.3, ba.internal.unlock_all_input, timetype=ba.TimeType.REAL) + bui.apptimer(0.3, bui.unlock_all_input) diff --git a/assets/src/ba_data/python/bastd/ui/continues.py b/src/assets/ba_data/python/bastd/ui/continues.py similarity index 71% rename from assets/src/ba_data/python/bastd/ui/continues.py rename to src/assets/ba_data/python/bastd/ui/continues.py index 11bdeedb..3b500422 100644 --- a/assets/src/ba_data/python/bastd/ui/continues.py +++ b/src/assets/ba_data/python/bastd/ui/continues.py @@ -7,23 +7,25 @@ from __future__ import annotations import weakref from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable + import bascenev1 as bs -class ContinuesWindow(ba.Window): + +class ContinuesWindow(bui.Window): """A window to continue a game.""" def __init__( self, - activity: ba.Activity, + activity: bs.Activity, cost: int, continue_call: Callable[[], Any], cancel_call: Callable[[], Any], ): + assert bui.app.classic is not None self._activity = weakref.ref(activity) self._cost = cost self._continue_call = continue_call @@ -33,7 +35,7 @@ class ContinuesWindow(ba.Window): self._height = 200 self._transitioning_out = False super().__init__( - ba.containerwidget( + bui.containerwidget( size=(self._width, self._height), background=False, toolbar_visibility='menu_currency', @@ -42,25 +44,19 @@ class ContinuesWindow(ba.Window): ) ) txt = ( - ba.Lstr(resource='continuePurchaseText') + bui.Lstr(resource='continuePurchaseText') .evaluate() .split('${PRICE}') ) t_left = txt[0] - t_left_width = ba.internal.get_string_width( - t_left, suppress_warning=True - ) - t_price = ba.charstr(ba.SpecialChar.TICKET) + str(self._cost) - t_price_width = ba.internal.get_string_width( - t_price, suppress_warning=True - ) + t_left_width = bui.get_string_width(t_left, suppress_warning=True) + t_price = bui.charstr(bui.SpecialChar.TICKET) + str(self._cost) + t_price_width = bui.get_string_width(t_price, suppress_warning=True) t_right = txt[-1] - t_right_width = ba.internal.get_string_width( - t_right, suppress_warning=True - ) + t_right_width = bui.get_string_width(t_right, suppress_warning=True) width_total_half = (t_left_width + t_price_width + t_right_width) * 0.5 - ba.textwidget( + bui.textwidget( parent=self._root_widget, text=t_left, flatness=1.0, @@ -70,7 +66,7 @@ class ContinuesWindow(ba.Window): v_align='center', position=(self._width * 0.5 - width_total_half, self._height - 30), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, text=t_price, flatness=1.0, @@ -84,7 +80,7 @@ class ContinuesWindow(ba.Window): h_align='left', v_align='center', ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, text=t_right, flatness=1.0, @@ -103,13 +99,13 @@ class ContinuesWindow(ba.Window): ) self._tickets_text_base: str | None - self._tickets_text: ba.Widget | None - if not ba.app.ui.use_toolbars: - self._tickets_text_base = ba.Lstr( + self._tickets_text: bui.Widget | None + if not bui.app.classic.ui.use_toolbars: + self._tickets_text_base = bui.Lstr( resource='getTicketsWindow.youHaveShortText', fallback_resource='getTicketsWindow.youHaveText', ).evaluate() - self._tickets_text = ba.textwidget( + self._tickets_text = bui.textwidget( parent=self._root_widget, text='', flatness=1.0, @@ -128,7 +124,7 @@ class ContinuesWindow(ba.Window): self._tickets_text_base = None self._tickets_text = None - self._counter_text = ba.textwidget( + self._counter_text = bui.textwidget( parent=self._root_widget, text=str(self._count), color=(0.7, 0.7, 0.7), @@ -141,24 +137,24 @@ class ContinuesWindow(ba.Window): h_align='center', v_align='center', ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self._root_widget, position=(30, 30), size=(120, 50), - label=ba.Lstr(resource='endText', fallback_resource='cancelText'), + label=bui.Lstr(resource='endText', fallback_resource='cancelText'), autoselect=True, enable_sound=False, on_activate_call=self._on_cancel_press, ) - self._continue_button = ba.buttonwidget( + self._continue_button = bui.buttonwidget( parent=self._root_widget, - label=ba.Lstr(resource='continueText'), + label=bui.Lstr(resource='continueText'), autoselect=True, position=(self._width - 130, 30), size=(120, 50), on_activate_call=self._on_continue_press, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._cancel_button, start_button=self._continue_button, @@ -166,88 +162,95 @@ class ContinuesWindow(ba.Window): ) self._counting_down = True - self._countdown_timer = ba.Timer( - 1.0, ba.WeakCall(self._tick), repeat=True, timetype=ba.TimeType.REAL + self._countdown_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._tick), repeat=True ) # If there is foreground activity, suspend it. - ba.app.pause() + bui.app.classic.pause() self._tick() def __del__(self) -> None: # If there is suspended foreground activity, resume it. - ba.app.resume() + assert bui.app.classic is not None + bui.app.classic.resume() def _tick(self) -> None: + plus = bui.app.plus + assert plus is not None + # if our target activity is gone or has ended, go away activity = self._activity() if activity is None or activity.has_ended(): self._on_cancel() return - if ba.internal.get_v1_account_state() == 'signed_in': - sval = ba.charstr(ba.SpecialChar.TICKET) + str( - ba.internal.get_v1_account_ticket_count() + if plus.get_v1_account_state() == 'signed_in': + sval = bui.charstr(bui.SpecialChar.TICKET) + str( + plus.get_v1_account_ticket_count() ) else: sval = '?' if self._tickets_text is not None: assert self._tickets_text_base is not None - ba.textwidget( + bui.textwidget( edit=self._tickets_text, text=self._tickets_text_base.replace('${COUNT}', sval), ) if self._counting_down: self._count -= 1 - ba.playsound(ba.getsound('tick')) + bui.getsound('tick').play() if self._count <= 0: self._on_cancel() else: - ba.textwidget(edit=self._counter_text, text=str(self._count)) + bui.textwidget(edit=self._counter_text, text=str(self._count)) def _on_cancel_press(self) -> None: # disallow for first second if self._start_count - self._count < 2: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() else: self._on_cancel() def _on_continue_press(self) -> None: from bastd.ui import getcurrency + plus = bui.app.plus + assert plus is not None + # Disallow for first second. if self._start_count - self._count < 2: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() else: # If somehow we got signed out... - if ba.internal.get_v1_account_state() != 'signed_in': - ba.screenmessage( - ba.Lstr(resource='notSignedInText'), color=(1, 0, 0) + if plus.get_v1_account_state() != 'signed_in': + bui.screenmessage( + bui.Lstr(resource='notSignedInText'), color=(1, 0, 0) ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return # If it appears we don't have enough tickets, offer to buy more. - tickets = ba.internal.get_v1_account_ticket_count() + tickets = plus.get_v1_account_ticket_count() if tickets < self._cost: # FIXME: Should we start the timer back up again after? self._counting_down = False - ba.textwidget(edit=self._counter_text, text='') - ba.playsound(ba.getsound('error')) + bui.textwidget(edit=self._counter_text, text='') + bui.getsound('error').play() getcurrency.show_get_tickets_prompt() return if not self._transitioning_out: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transitioning_out = True - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition='out_scale' ) self._continue_call() def _on_cancel(self) -> None: if not self._transitioning_out: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transitioning_out = True - ba.containerwidget(edit=self._root_widget, transition='out_scale') + bui.containerwidget(edit=self._root_widget, transition='out_scale') self._cancel_call() diff --git a/assets/src/ba_data/python/bastd/ui/coop/__init__.py b/src/assets/ba_data/python/bastd/ui/coop/__init__.py similarity index 100% rename from assets/src/ba_data/python/bastd/ui/coop/__init__.py rename to src/assets/ba_data/python/bastd/ui/coop/__init__.py diff --git a/assets/src/ba_data/python/bastd/ui/coop/browser.py b/src/assets/ba_data/python/bastd/ui/coop/browser.py similarity index 72% rename from assets/src/ba_data/python/bastd/ui/coop/browser.py rename to src/assets/ba_data/python/bastd/ui/coop/browser.py index f96e0a55..2390a63d 100644 --- a/assets/src/ba_data/python/bastd/ui/coop/browser.py +++ b/src/assets/ba_data/python/bastd/ui/coop/browser.py @@ -6,13 +6,14 @@ from __future__ import annotations +import logging +from threading import Thread from typing import TYPE_CHECKING -import ba -import ba.internal from bastd.ui.store.button import StoreButton from bastd.ui.league.rankbutton import LeagueRankButton from bastd.ui.store.browser import StoreBrowserWindow +import bauiv1 as bui if TYPE_CHECKING: from typing import Any @@ -20,64 +21,39 @@ if TYPE_CHECKING: from bastd.ui.coop.tournamentbutton import TournamentButton -class CoopBrowserWindow(ba.Window): +class CoopBrowserWindow(bui.Window): """Window for browsing co-op levels/games/etc.""" - def _update_corner_button_positions(self) -> None: - uiscale = ba.app.ui.uiscale - offs = ( - -55 - if uiscale is ba.UIScale.SMALL - and ba.internal.is_party_icon_visible() - else 0 - ) - if self._league_rank_button is not None: - self._league_rank_button.set_position( - ( - self._width - 282 + offs - self._x_inset, - self._height - - 85 - - (4 if uiscale is ba.UIScale.SMALL else 0), - ) - ) - if self._store_button is not None: - self._store_button.set_position( - ( - self._width - 170 + offs - self._x_inset, - self._height - - 85 - - (4 if uiscale is ba.UIScale.SMALL else 0), - ) - ) - def __init__( self, transition: str | None = 'in_right', - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements # pylint: disable=cyclic-import - import threading + + plus = bui.app.plus + assert plus is not None # Preload some modules we use in a background thread so we won't # have a visual hitch when the user taps them. - threading.Thread(target=self._preload_modules).start() + Thread(target=self._preload_modules).start() - ba.set_analytics_screen('Coop Window') + bui.set_analytics_screen('Coop Window') - app = ba.app + app = bui.app + assert app.classic is not None cfg = app.config # Quick note to players that tourneys won't work in ballistica # core builds. (need to split the word so it won't get subbed out) - if 'ballistica' + 'core' == ba.internal.appname(): - ba.timer( + if 'ballistica' + 'kit' == bui.appname(): + bui.apptimer( 1.0, - lambda: ba.screenmessage( - ba.Lstr(resource='noTournamentsInTestBuildText'), + lambda: bui.screenmessage( + bui.Lstr(resource='noTournamentsInTestBuildText'), color=(1, 1, 0), ), - timetype=ba.TimeType.REAL, ) # If they provided an origin-widget, scale up from that. @@ -95,91 +71,99 @@ class CoopBrowserWindow(ba.Window): self._tournament_button_count = app.config.get('Tournament Rows', 0) assert isinstance(self._tournament_button_count, int) - self._easy_button: ba.Widget | None = None - self._hard_button: ba.Widget | None = None - self._hard_button_lock_image: ba.Widget | None = None - self._campaign_percent_text: ba.Widget | None = None + self.star_tex = bui.gettexture('star') + self.lsbt = bui.getmesh('level_select_button_transparent') + self.lsbo = bui.getmesh('level_select_button_opaque') + self.a_outline_tex = bui.gettexture('achievementOutline') + self.a_outline_mesh = bui.getmesh('achievementOutline') + self._campaign_sub_container: bui.Widget | None = None + self._tournament_info_button: bui.Widget | None = None + self._easy_button: bui.Widget | None = None + self._hard_button: bui.Widget | None = None + self._hard_button_lock_image: bui.Widget | None = None + self._campaign_percent_text: bui.Widget | None = None - uiscale = ba.app.ui.uiscale - self._width = 1320 if uiscale is ba.UIScale.SMALL else 1120 - self._x_inset = x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self._width = 1320 if uiscale is bui.UIScale.SMALL else 1120 + self._x_inset = x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 657 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 730 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 800 ) - app.ui.set_main_menu_location('Coop Select') + app.classic.ui.set_main_menu_location('Coop Select') self._r = 'coopSelectWindow' - top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 self._tourney_data_up_to_date = False - self._campaign_difficulty = ba.internal.get_v1_account_misc_val( + self._campaign_difficulty = plus.get_v1_account_misc_val( 'campaignDifficulty', 'easy' ) super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), toolbar_visibility='menu_full', scale_origin_stack_offset=scale_origin, stack_offset=( (0, -15) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0) - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else (0, 0) ), transition=transition, scale=( 1.2 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 0.8 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 0.75 ), ) ) - if app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: + if app.classic.ui.use_toolbars and uiscale is bui.UIScale.SMALL: self._back_button = None else: - self._back_button = ba.buttonwidget( + self._back_button = bui.buttonwidget( parent=self._root_widget, position=( 75 + x_inset, self._height - 87 - - (4 if uiscale is ba.UIScale.SMALL else 0), + - (4 if uiscale is bui.UIScale.SMALL else 0), ), size=(120, 60), scale=1.2, autoselect=True, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', ) self._league_rank_button: LeagueRankButton | None self._store_button: StoreButton | None - self._store_button_widget: ba.Widget | None - self._league_rank_button_widget: ba.Widget | None + self._store_button_widget: bui.Widget | None + self._league_rank_button_widget: bui.Widget | None - if not app.ui.use_toolbars: + if not app.classic.ui.use_toolbars: prb = self._league_rank_button = LeagueRankButton( parent=self._root_widget, position=( self._width - (282 + x_inset), self._height - 85 - - (4 if uiscale is ba.UIScale.SMALL else 0), + - (4 if uiscale is bui.UIScale.SMALL else 0), ), size=(100, 60), color=(0.4, 0.4, 0.9), textcolor=(0.9, 0.9, 2.0), scale=1.05, - on_activate_call=ba.WeakCall(self._switch_to_league_rankings), + on_activate_call=bui.WeakCall(self._switch_to_league_rankings), ) self._league_rank_button_widget = prb.get_button() @@ -189,7 +173,7 @@ class CoopBrowserWindow(ba.Window): self._width - (170 + x_inset), self._height - 85 - - (4 if uiscale is ba.UIScale.SMALL else 0), + - (4 if uiscale is bui.UIScale.SMALL else 0), ), size=(100, 60), color=(0.6, 0.4, 0.7), @@ -198,14 +182,14 @@ class CoopBrowserWindow(ba.Window): sale_scale=0.85, textcolor=(0.9, 0.7, 1.0), scale=1.05, - on_activate_call=ba.WeakCall(self._switch_to_score, None), + on_activate_call=bui.WeakCall(self._switch_to_score, None), ) self._store_button_widget = sbtn.get_button() - ba.widget( + bui.widget( edit=self._back_button, right_widget=self._league_rank_button_widget, ) - ba.widget( + bui.widget( edit=self._league_rank_button_widget, left_widget=self._back_button, ) @@ -218,11 +202,8 @@ class CoopBrowserWindow(ba.Window): # Move our corner buttons dynamically to keep them out of the way of # the party icon :-( self._update_corner_button_positions() - self._update_corner_button_positions_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update_corner_button_positions), - repeat=True, - timetype=ba.TimeType.REAL, + self._update_corner_button_positions_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update_corner_button_positions), repeat=True ) self._last_tournament_query_time: float | None = None @@ -239,29 +220,29 @@ class CoopBrowserWindow(ba.Window): # Don't want initial construction affecting our last-selected. self._do_selection_callbacks = False v = self._height - 95 - txt = ba.textwidget( + txt = bui.textwidget( parent=self._root_widget, position=( self._width * 0.5, - v + 40 - (0 if uiscale is ba.UIScale.SMALL else 0), + v + 40 - (0 if uiscale is bui.UIScale.SMALL else 0), ), size=(0, 0), - text=ba.Lstr( + text=bui.Lstr( resource='playModes.singlePlayerCoopText', fallback_resource='playModes.coopText', ), h_align='center', - color=app.ui.title_color, + color=app.classic.ui.title_color, scale=1.5, maxwidth=500, v_align='center', ) - if app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: - ba.textwidget(edit=txt, text='') + if app.classic.ui.use_toolbars and uiscale is bui.UIScale.SMALL: + bui.textwidget(edit=txt, text='') if self._back_button is not None: - ba.buttonwidget( + bui.buttonwidget( edit=self._back_button, button_type='backSmall', size=(60, 50), @@ -269,33 +250,29 @@ class CoopBrowserWindow(ba.Window): 75 + x_inset, self._height - 87 - - (4 if uiscale is ba.UIScale.SMALL else 0) + - (4 if uiscale is bui.UIScale.SMALL else 0) + 6, ), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) self._selected_row = cfg.get('Selected Coop Row', None) - self.star_tex = ba.gettexture('star') - self.lsbt = ba.getmodel('level_select_button_transparent') - self.lsbo = ba.getmodel('level_select_button_opaque') - self.a_outline_tex = ba.gettexture('achievementOutline') - self.a_outline_model = ba.getmodel('achievementOutline') - self._scroll_width = self._width - (130 + 2 * x_inset) self._scroll_height = self._height - ( - 190 if uiscale is ba.UIScale.SMALL and app.ui.use_toolbars else 160 + 190 + if uiscale is bui.UIScale.SMALL and app.classic.ui.use_toolbars + else 160 ) self._subcontainerwidth = 800.0 self._subcontainerheight = 1400.0 - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, highlight=False, position=(65 + x_inset, 120) - if uiscale is ba.UIScale.SMALL and app.ui.use_toolbars + if uiscale is bui.UIScale.SMALL and app.classic.ui.use_toolbars else (65 + x_inset, 70), size=(self._scroll_width, self._scroll_height), simple_culling_v=10.0, @@ -303,10 +280,10 @@ class CoopBrowserWindow(ba.Window): claims_tab=True, selection_loops_to_parent=True, ) - self._subcontainer: ba.Widget | None = None + self._subcontainer: bui.Widget | None = None # Take note of our account state; we'll refresh later if this changes. - self._account_state_num = ba.internal.get_v1_account_state_num() + self._account_state_num = plus.get_v1_account_state_num() # Same for fg/bg state. self._fg_state = app.fg_state @@ -323,29 +300,53 @@ class CoopBrowserWindow(ba.Window): # each one of those tournaments, go ahead and display it as a # starting point. if ( - app.accounts_v1.account_tournament_list is not None - and app.accounts_v1.account_tournament_list[0] - == ba.internal.get_v1_account_state_num() + app.classic.accounts.account_tournament_list is not None + and app.classic.accounts.account_tournament_list[0] + == plus.get_v1_account_state_num() and all( - t_id in app.accounts_v1.tournament_info - for t_id in app.accounts_v1.account_tournament_list[1] + t_id in app.classic.accounts.tournament_info + for t_id in app.classic.accounts.account_tournament_list[1] ) ): tourney_data = [ - app.accounts_v1.tournament_info[t_id] - for t_id in app.accounts_v1.account_tournament_list[1] + app.classic.accounts.tournament_info[t_id] + for t_id in app.classic.accounts.account_tournament_list[1] ] self._update_for_data(tourney_data) # This will pull new data periodically, update timers, etc. - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - timetype=ba.TimeType.REAL, - repeat=True, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) self._update() + def _update_corner_button_positions(self) -> None: + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + offs = ( + -55 + if uiscale is bui.UIScale.SMALL and bui.is_party_icon_visible() + else 0 + ) + if self._league_rank_button is not None: + self._league_rank_button.set_position( + ( + self._width - 282 + offs - self._x_inset, + self._height + - 85 + - (4 if uiscale is bui.UIScale.SMALL else 0), + ) + ) + if self._store_button is not None: + self._store_button.set_position( + ( + self._width - 170 + offs - self._x_inset, + self._height + - 85 + - (4 if uiscale is bui.UIScale.SMALL else 0), + ) + ) + # noinspection PyUnresolvedReferences @staticmethod def _preload_modules() -> None: @@ -363,25 +364,27 @@ class CoopBrowserWindow(ba.Window): import bastd.ui.coop.tournamentbutton as _unused11 def _update(self) -> None: + plus = bui.app.plus + assert plus is not None + # Do nothing if we've somehow outlived our actual UI. if not self._root_widget: return - cur_time = ba.time(ba.TimeType.REAL) + cur_time = bui.apptime() # If its been a while since we got a tournament update, consider the # data invalid (prevents us from joining tournaments if our internet # connection goes down for a while). if ( self._last_tournament_query_response_time is None - or ba.time(ba.TimeType.REAL) - - self._last_tournament_query_response_time + or bui.apptime() - self._last_tournament_query_response_time > 60.0 * 2 ): self._tourney_data_up_to_date = False # If our account state has changed, do a full request. - account_state_num = ba.internal.get_v1_account_state_num() + account_state_num = plus.get_v1_account_state_num() if account_state_num != self._account_state_num: self._account_state_num = account_state_num self._save_state() @@ -394,35 +397,31 @@ class CoopBrowserWindow(ba.Window): # If we've been backgrounded/foregrounded, invalidate our # tournament entries (they will be refreshed below asap). - if self._fg_state != ba.app.fg_state: + if self._fg_state != bui.app.fg_state: self._tourney_data_up_to_date = False # Send off a new tournament query if its been long enough or whatnot. if not self._doing_tournament_query and ( self._last_tournament_query_time is None or cur_time - self._last_tournament_query_time > 30.0 - or self._fg_state != ba.app.fg_state + or self._fg_state != bui.app.fg_state ): - self._fg_state = ba.app.fg_state + self._fg_state = bui.app.fg_state self._last_tournament_query_time = cur_time self._doing_tournament_query = True - ba.internal.tournament_query( + plus.tournament_query( args={'source': 'coop window refresh', 'numScores': 1}, - callback=ba.WeakCall(self._on_tournament_query_response), + callback=bui.WeakCall(self._on_tournament_query_response), ) # Decrement time on our tournament buttons. - ads_enabled = ba.internal.have_incentivized_ad() + ads_enabled = bui.have_incentivized_ad() for tbtn in self._tournament_buttons: tbtn.time_remaining = max(0, tbtn.time_remaining - 1) if tbtn.time_remaining_value_text is not None: - ba.textwidget( + bui.textwidget( edit=tbtn.time_remaining_value_text, - text=ba.timestring( - tbtn.time_remaining, - centi=False, - suppress_format_warning=True, - ) + text=bui.timestring(tbtn.time_remaining, centi=False) if ( tbtn.has_time_remaining and self._tourney_data_up_to_date @@ -431,12 +430,12 @@ class CoopBrowserWindow(ba.Window): ) # Also adjust the ad icon visibility. - if tbtn.allow_ads and ba.internal.has_video_ads(): - ba.imagewidget( + if tbtn.allow_ads and bui.has_video_ads(): + bui.imagewidget( edit=tbtn.entry_fee_ad_image, opacity=1.0 if ads_enabled else 0.25, ) - ba.textwidget( + bui.textwidget( edit=tbtn.entry_fee_text_remaining, color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2), ) @@ -444,23 +443,25 @@ class CoopBrowserWindow(ba.Window): self._update_hard_mode_lock_image() def _update_hard_mode_lock_image(self) -> None: + assert bui.app.classic is not None try: - ba.imagewidget( + bui.imagewidget( edit=self._hard_button_lock_image, - opacity=0.0 if ba.app.accounts_v1.have_pro_options() else 1.0, + opacity=0.0 + if bui.app.classic.accounts.have_pro_options() + else 1.0, ) except Exception: - ba.print_exception('Error updating campaign lock.') + logging.exception('Error updating campaign lock.') def _update_for_data(self, data: list[dict[str, Any]] | None) -> None: - # If the number of tournaments or challenges in the data differs from # our current arrangement, refresh with the new number. if (data is None and self._tournament_button_count != 0) or ( data is not None and (len(data) != self._tournament_button_count) ): self._tournament_button_count = len(data) if data is not None else 0 - ba.app.config['Tournament Rows'] = self._tournament_button_count + bui.app.config['Tournament Rows'] = self._tournament_button_count self._refresh() # Update all of our tourney buttons based on whats in data. @@ -471,12 +472,14 @@ class CoopBrowserWindow(ba.Window): def _on_tournament_query_response( self, data: dict[str, Any] | None ) -> None: - accounts = ba.app.accounts_v1 + plus = bui.app.plus + assert plus is not None + + assert bui.app.classic is not None + accounts = bui.app.classic.accounts if data is not None: tournament_data = data['t'] # This used to be the whole payload. - self._last_tournament_query_response_time = ba.time( - ba.TimeType.REAL - ) + self._last_tournament_query_response_time = bui.apptime() else: tournament_data = None @@ -487,7 +490,7 @@ class CoopBrowserWindow(ba.Window): # Also cache the current tourney list/order for this account. accounts.account_tournament_list = ( - ba.internal.get_v1_account_state_num(), + plus.get_v1_account_state_num(), [e['tournamentID'] for e in tournament_data], ) @@ -498,19 +501,23 @@ class CoopBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.purchase import PurchaseWindow + plus = bui.app.plus + assert plus is not None + + assert bui.app.classic is not None if difficulty != self._campaign_difficulty: if ( difficulty == 'hard' - and not ba.app.accounts_v1.have_pro_options() + and not bui.app.classic.accounts.have_pro_options() ): PurchaseWindow(items=['pro']) return - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() if difficulty not in ('easy', 'hard'): print('ERROR: invalid campaign difficulty:', difficulty) difficulty = 'easy' self._campaign_difficulty = difficulty - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'SET_MISC_VAL', 'name': 'campaignDifficulty', @@ -519,17 +526,17 @@ class CoopBrowserWindow(ba.Window): ) self._refresh_campaign_row() else: - ba.playsound(ba.getsound('click01')) + bui.getsound('click01').play() def _refresh_campaign_row(self) -> None: # pylint: disable=too-many-locals # pylint: disable=cyclic-import - from ba.internal import getcampaign from bastd.ui.coop.gamebutton import GameButton parent_widget = self._campaign_sub_container # Clear out anything in the parent widget already. + assert parent_widget is not None for child in parent_widget.get_children(): child.delete() @@ -542,16 +549,16 @@ class CoopBrowserWindow(ba.Window): un_sel_color = (0.5, 0.5, 0.5) sel_textcolor = (2, 2, 0.8) un_sel_textcolor = (0.6, 0.6, 0.6) - self._easy_button = ba.buttonwidget( + self._easy_button = bui.buttonwidget( parent=parent_widget, position=(h + 30, v2 + 105), size=(120, 70), - label=ba.Lstr(resource='difficultyEasyText'), + label=bui.Lstr(resource='difficultyEasyText'), button_type='square', autoselect=True, enable_sound=False, - on_activate_call=ba.Call(self._set_campaign_difficulty, 'easy'), - on_select_call=ba.Call(self.sel_change, 'campaign', 'easyButton'), + on_activate_call=bui.Call(self._set_campaign_difficulty, 'easy'), + on_select_call=bui.Call(self.sel_change, 'campaign', 'easyButton'), color=sel_color if self._campaign_difficulty == 'easy' else un_sel_color, @@ -559,25 +566,25 @@ class CoopBrowserWindow(ba.Window): if self._campaign_difficulty == 'easy' else un_sel_textcolor, ) - ba.widget(edit=self._easy_button, show_buffer_left=100) + bui.widget(edit=self._easy_button, show_buffer_left=100) if self._selected_campaign_level == 'easyButton': - ba.containerwidget( + bui.containerwidget( edit=parent_widget, selected_child=self._easy_button, visible_child=self._easy_button, ) - lock_tex = ba.gettexture('lock') + lock_tex = bui.gettexture('lock') - self._hard_button = ba.buttonwidget( + self._hard_button = bui.buttonwidget( parent=parent_widget, position=(h + 30, v2 + 32), size=(120, 70), - label=ba.Lstr(resource='difficultyHardText'), + label=bui.Lstr(resource='difficultyHardText'), button_type='square', autoselect=True, enable_sound=False, - on_activate_call=ba.Call(self._set_campaign_difficulty, 'hard'), - on_select_call=ba.Call(self.sel_change, 'campaign', 'hardButton'), + on_activate_call=bui.Call(self._set_campaign_difficulty, 'hard'), + on_select_call=bui.Call(self.sel_change, 'campaign', 'hardButton'), color=sel_color_hard if self._campaign_difficulty == 'hard' else un_sel_color, @@ -585,7 +592,7 @@ class CoopBrowserWindow(ba.Window): if self._campaign_difficulty == 'hard' else un_sel_textcolor, ) - self._hard_button_lock_image = ba.imagewidget( + self._hard_button_lock_image = bui.imagewidget( parent=parent_widget, size=(30, 30), draw_controller=self._hard_button, @@ -593,15 +600,15 @@ class CoopBrowserWindow(ba.Window): texture=lock_tex, ) self._update_hard_mode_lock_image() - ba.widget(edit=self._hard_button, show_buffer_left=100) + bui.widget(edit=self._hard_button, show_buffer_left=100) if self._selected_campaign_level == 'hardButton': - ba.containerwidget( + bui.containerwidget( edit=parent_widget, selected_child=self._hard_button, visible_child=self._hard_button, ) - ba.widget(edit=self._hard_button, down_widget=next_widget_down) + bui.widget(edit=self._hard_button, down_widget=next_widget_down) h_spacing = 200 campaign_buttons = [] if self._campaign_difficulty == 'easy': @@ -632,19 +639,20 @@ class CoopBrowserWindow(ba.Window): ) h += h_spacing - ba.widget(edit=campaign_buttons[0], left_widget=self._easy_button) + bui.widget(edit=campaign_buttons[0], left_widget=self._easy_button) if self._back_button is not None: - ba.widget(edit=self._easy_button, up_widget=self._back_button) + bui.widget(edit=self._easy_button, up_widget=self._back_button) for btn in campaign_buttons: - ba.widget( + bui.widget( edit=btn, up_widget=self._back_button, down_widget=next_widget_down, ) # Update our existing percent-complete text. - campaign = getcampaign(campaignname) + assert bui.app.classic is not None + campaign = bui.app.classic.getcampaign(campaignname) levels = campaign.levels levels_complete = sum((1 if l.complete else 0) for l in levels) @@ -652,12 +660,12 @@ class CoopBrowserWindow(ba.Window): progress = min(1.0, float(levels_complete) / (len(levels) - 1)) p_str = str(int(progress * 100.0)) + '%' - self._campaign_percent_text = ba.textwidget( + self._campaign_percent_text = bui.textwidget( edit=self._campaign_percent_text, - text=ba.Lstr( + text=bui.Lstr( value='${C} (${P})', subs=[ - ('${C}', ba.Lstr(resource=self._r + '.campaignText')), + ('${C}', bui.Lstr(resource=self._r + '.campaignText')), ('${P}', p_str), ], ), @@ -667,7 +675,7 @@ class CoopBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.confirm import ConfirmWindow - txt = ba.Lstr(resource=self._r + '.tournamentInfoText') + txt = bui.Lstr(resource=self._r + '.tournamentInfoText') ConfirmWindow( txt, cancel_button=False, @@ -684,6 +692,10 @@ class CoopBrowserWindow(ba.Window): from bastd.ui.coop.gamebutton import GameButton from bastd.ui.coop.tournamentbutton import TournamentButton + plus = bui.app.plus + assert plus is not None + assert bui.app.classic is not None + # (Re)create the sub-container if need be. if self._subcontainer is not None: self._subcontainer.delete() @@ -693,7 +705,7 @@ class CoopBrowserWindow(ba.Window): 620 + self._tournament_button_count * tourney_row_height ) - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._subcontainerwidth, self._subcontainerheight), background=False, @@ -702,11 +714,11 @@ class CoopBrowserWindow(ba.Window): selection_loops_to_parent=True, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) if self._back_button is not None: - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button ) @@ -715,21 +727,21 @@ class CoopBrowserWindow(ba.Window): v = self._subcontainerheight - 73 - self._campaign_percent_text = ba.textwidget( + self._campaign_percent_text = bui.textwidget( parent=w_parent, position=(h_base + 27, v + 30), size=(0, 0), text='', h_align='left', v_align='center', - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, scale=1.1, ) row_v_show_buffer = 100 v -= 198 - h_scroll = ba.hscrollwidget( + h_scroll = bui.hscrollwidget( parent=w_parent, size=(self._scroll_width - 10, 205), position=(-5, v), @@ -740,18 +752,18 @@ class CoopBrowserWindow(ba.Window): on_select_call=lambda: self._on_row_selected('campaign'), ) self._campaign_h_scroll = h_scroll - ba.widget( + bui.widget( edit=h_scroll, show_buffer_top=row_v_show_buffer, show_buffer_bottom=row_v_show_buffer, autoselect=True, ) if self._selected_row == 'campaign': - ba.containerwidget( + bui.containerwidget( edit=w_parent, selected_child=h_scroll, visible_child=h_scroll ) - ba.containerwidget(edit=h_scroll, claims_left_right=True) - self._campaign_sub_container = ba.containerwidget( + bui.containerwidget(edit=h_scroll, claims_left_right=True) + self._campaign_sub_container = bui.containerwidget( parent=h_scroll, size=(180 + 200 * 10, 200), background=False ) @@ -761,21 +773,21 @@ class CoopBrowserWindow(ba.Window): v -= 53 # FIXME shouldn't use hard-coded strings here. - txt = ba.Lstr( + txt = bui.Lstr( resource='tournamentsText', fallback_resource='tournamentText' ).evaluate() - t_width = ba.internal.get_string_width(txt, suppress_warning=True) - ba.textwidget( + t_width = bui.get_string_width(txt, suppress_warning=True) + bui.textwidget( parent=w_parent, position=(h_base + 27, v + 30), size=(0, 0), text=txt, h_align='left', v_align='center', - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, scale=1.1, ) - self._tournament_info_button = ba.buttonwidget( + self._tournament_info_button = bui.buttonwidget( parent=w_parent, label='?', size=(20, 20), @@ -788,7 +800,7 @@ class CoopBrowserWindow(ba.Window): up_widget=self._campaign_h_scroll, on_activate_call=self._on_tournament_info_press, ) - ba.widget( + bui.widget( edit=self._tournament_info_button, left_widget=self._tournament_info_button, right_widget=self._tournament_info_button, @@ -798,23 +810,23 @@ class CoopBrowserWindow(ba.Window): # signed in add that as well (that's probably why we see # no tournaments). if self._tournament_button_count == 0: - unavailable_text = ba.Lstr(resource='unavailableText') - if ba.internal.get_v1_account_state() != 'signed_in': - unavailable_text = ba.Lstr( + unavailable_text = bui.Lstr(resource='unavailableText') + if plus.get_v1_account_state() != 'signed_in': + unavailable_text = bui.Lstr( value='${A} (${B})', subs=[ ('${A}', unavailable_text), - ('${B}', ba.Lstr(resource='notSignedInText')), + ('${B}', bui.Lstr(resource='notSignedInText')), ], ) - ba.textwidget( + bui.textwidget( parent=w_parent, position=(h_base + 47, v), size=(0, 0), text=unavailable_text, h_align='left', v_align='center', - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, scale=0.9, ) v -= 40 @@ -823,31 +835,31 @@ class CoopBrowserWindow(ba.Window): tournament_h_scroll = None if self._tournament_button_count > 0: for i in range(self._tournament_button_count): - tournament_h_scroll = h_scroll = ba.hscrollwidget( + tournament_h_scroll = h_scroll = bui.hscrollwidget( parent=w_parent, size=(self._scroll_width - 10, 205), position=(-5, v), highlight=False, border_opacity=0.0, color=(0.45, 0.4, 0.5), - on_select_call=ba.Call( + on_select_call=bui.Call( self._on_row_selected, 'tournament' + str(i + 1) ), ) - ba.widget( + bui.widget( edit=h_scroll, show_buffer_top=row_v_show_buffer, show_buffer_bottom=row_v_show_buffer, autoselect=True, ) if self._selected_row == 'tournament' + str(i + 1): - ba.containerwidget( + bui.containerwidget( edit=w_parent, selected_child=h_scroll, visible_child=h_scroll, ) - ba.containerwidget(edit=h_scroll, claims_left_right=True) - sc2 = ba.containerwidget( + bui.containerwidget(edit=h_scroll, claims_left_right=True) + sc2 = bui.containerwidget( parent=h_scroll, size=(self._scroll_width - 24, 200), background=False, @@ -861,24 +873,24 @@ class CoopBrowserWindow(ba.Window): h, v2, is_last_sel, - on_pressed=ba.WeakCall(self.run_tournament), + on_pressed=bui.WeakCall(self.run_tournament), ) ) v -= 200 # Custom Games. (called 'Practice' in UI these days). v -= 50 - ba.textwidget( + bui.textwidget( parent=w_parent, position=(h_base + 27, v + 30 + 198), size=(0, 0), - text=ba.Lstr( + text=bui.Lstr( resource='practiceText', fallback_resource='coopSelectWindow.customText', ), h_align='left', v_align='center', - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, scale=1.1, ) @@ -893,39 +905,39 @@ class CoopBrowserWindow(ba.Window): ] # Show easter-egg-hunt either if its easter or we own it. - if ba.internal.get_v1_account_misc_read_val( + if plus.get_v1_account_misc_read_val( 'easter', False - ) or ba.internal.get_purchased('games.easter_egg_hunt'): + ) or plus.get_purchased('games.easter_egg_hunt'): items = [ 'Challenges:Easter Egg Hunt', 'Challenges:Pro Easter Egg Hunt', ] + items # If we've defined custom games, put them at the beginning. - if ba.app.custom_coop_practice_games: - items = ba.app.custom_coop_practice_games + items + if bui.app.classic.custom_coop_practice_games: + items = bui.app.classic.custom_coop_practice_games + items - self._custom_h_scroll = custom_h_scroll = h_scroll = ba.hscrollwidget( + self._custom_h_scroll = custom_h_scroll = h_scroll = bui.hscrollwidget( parent=w_parent, size=(self._scroll_width - 10, 205), position=(-5, v), highlight=False, border_opacity=0.0, color=(0.45, 0.4, 0.5), - on_select_call=ba.Call(self._on_row_selected, 'custom'), + on_select_call=bui.Call(self._on_row_selected, 'custom'), ) - ba.widget( + bui.widget( edit=h_scroll, show_buffer_top=row_v_show_buffer, show_buffer_bottom=1.5 * row_v_show_buffer, autoselect=True, ) if self._selected_row == 'custom': - ba.containerwidget( + bui.containerwidget( edit=w_parent, selected_child=h_scroll, visible_child=h_scroll ) - ba.containerwidget(edit=h_scroll, claims_left_right=True) - sc2 = ba.containerwidget( + bui.containerwidget(edit=h_scroll, claims_left_right=True) + sc2 = bui.containerwidget( parent=h_scroll, size=(max(self._scroll_width - 24, 30 + 200 * len(items)), 200), background=False, @@ -946,7 +958,7 @@ class CoopBrowserWindow(ba.Window): self._refresh_campaign_row() for i, tbutton in enumerate(self._tournament_buttons): - ba.widget( + bui.widget( edit=tbutton.button, up_widget=self._tournament_info_button if i == 0 @@ -955,7 +967,7 @@ class CoopBrowserWindow(ba.Window): if i + 1 < len(self._tournament_buttons) else custom_h_scroll, ) - ba.widget( + bui.widget( edit=tbutton.more_scores_button, down_widget=self._tournament_buttons[ (i + 1) @@ -963,7 +975,7 @@ class CoopBrowserWindow(ba.Window): if i + 1 < len(self._tournament_buttons) else custom_h_scroll, ) - ba.widget( + bui.widget( edit=tbutton.current_leader_name_text, up_widget=self._tournament_info_button if i == 0 @@ -972,25 +984,27 @@ class CoopBrowserWindow(ba.Window): for btn in self._custom_buttons: try: - ba.widget( + bui.widget( edit=btn.get_button(), up_widget=tournament_h_scroll if self._tournament_buttons else self._tournament_info_button, ) except Exception: - ba.print_exception('Error wiring up custom buttons.') + logging.exception('Error wiring up custom buttons.') if self._back_button is not None: - ba.buttonwidget(edit=self._back_button, on_activate_call=self._back) + bui.buttonwidget( + edit=self._back_button, on_activate_call=self._back + ) else: - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, on_cancel_call=self._back ) # There's probably several 'onSelected' callbacks pushed onto the # event queue.. we need to push ours too so we're enabled *after* them. - ba.pushcall(self._enable_selectable_callback) + bui.pushcall(self._enable_selectable_callback) def _on_row_selected(self, row: str) -> None: if self._do_selection_callbacks: @@ -1005,13 +1019,17 @@ class CoopBrowserWindow(ba.Window): from bastd.ui.account import show_sign_in_prompt from bastd.ui.league.rankwindow import LeagueRankWindow - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.containerwidget(edit=self._root_widget, transition='out_left') assert self._league_rank_button is not None - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( LeagueRankWindow( origin_widget=self._league_rank_button.get_button() ).get_root_widget() @@ -1025,13 +1043,17 @@ class CoopBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.account import show_sign_in_prompt - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.containerwidget(edit=self._root_widget, transition='out_left') assert self._store_button is not None - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( StoreBrowserWindow( origin_widget=self._store_button.get_button(), show_tab=show_tab, @@ -1051,11 +1073,16 @@ class CoopBrowserWindow(ba.Window): from bastd.ui.purchase import PurchaseWindow from bastd.ui.account import show_sign_in_prompt + plus = bui.app.plus + assert plus is not None + + assert bui.app.classic is not None + args: dict[str, Any] = {} if game == 'Easy:The Last Stand': ConfirmWindow( - ba.Lstr( + bui.Lstr( resource='difficultyHardUnlockOnlyText', fallback_resource='difficultyHardOnlyText', ), @@ -1073,9 +1100,9 @@ class CoopBrowserWindow(ba.Window): 'Challenges:Infinite Runaround', 'Challenges:Infinite Onslaught', ) - and not ba.app.accounts_v1.have_pro() + and not bui.app.classic.accounts.have_pro() ): - if ba.internal.get_v1_account_state() != 'signed_in': + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() else: PurchaseWindow(items=['pro']) @@ -1101,10 +1128,10 @@ class CoopBrowserWindow(ba.Window): else: required_purchase = None - if required_purchase is not None and not ba.internal.get_purchased( + if required_purchase is not None and not plus.get_purchased( required_purchase ): - if ba.internal.get_v1_account_state() != 'signed_in': + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() else: PurchaseWindow(items=[required_purchase]) @@ -1112,49 +1139,53 @@ class CoopBrowserWindow(ba.Window): self._save_state() - if ba.app.launch_coop_game(game, args=args): - ba.containerwidget(edit=self._root_widget, transition='out_left') + if bui.app.classic.launch_coop_game(game, args=args): + bui.containerwidget(edit=self._root_widget, transition='out_left') def run_tournament(self, tournament_button: TournamentButton) -> None: """Run the provided tournament game.""" from bastd.ui.account import show_sign_in_prompt from bastd.ui.tournamententry import TournamentEntryWindow - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return - if ba.internal.workspaces_in_use(): - ba.screenmessage( - ba.Lstr(resource='tournamentsDisabledWorkspaceText'), + if bui.workspaces_in_use(): + bui.screenmessage( + bui.Lstr(resource='tournamentsDisabledWorkspaceText'), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return if not self._tourney_data_up_to_date: - ba.screenmessage( - ba.Lstr(resource='tournamentCheckingStateText'), color=(1, 1, 0) + bui.screenmessage( + bui.Lstr(resource='tournamentCheckingStateText'), + color=(1, 1, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return if tournament_button.tournament_id is None: - ba.screenmessage( - ba.Lstr(resource='internal.unavailableNoConnectionText'), + bui.screenmessage( + bui.Lstr(resource='internal.unavailableNoConnectionText'), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return if tournament_button.required_league is not None: - ba.screenmessage( - ba.Lstr( + bui.screenmessage( + bui.Lstr( resource='league.tournamentLeagueText', subs=[ ( '${NAME}', - ba.Lstr( + bui.Lstr( translate=( 'leagueNames', tournament_button.required_league, @@ -1165,14 +1196,14 @@ class CoopBrowserWindow(ba.Window): ), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return if tournament_button.time_remaining <= 0: - ba.screenmessage( - ba.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) + bui.screenmessage( + bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return self._save_state() @@ -1189,15 +1220,16 @@ class CoopBrowserWindow(ba.Window): # If something is selected, store it. self._save_state() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( PlayWindow(transition='in_left').get_root_widget() ) def _save_state(self) -> None: - cfg = ba.app.config + cfg = bui.app.config try: sel = self._root_widget.get_selected_child() if sel == self._back_button: @@ -1210,9 +1242,12 @@ class CoopBrowserWindow(ba.Window): sel_name = 'Scroll' else: raise ValueError('unrecognized selection') - ba.app.ui.window_states[type(self)] = {'sel_name': sel_name} + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = { + 'sel_name': sel_name + } except Exception: - ba.print_exception(f'Error saving state for {self}.') + logging.exception('Error saving state for %s.', self) cfg['Selected Coop Row'] = self._selected_row cfg['Selected Coop Custom Level'] = self._selected_custom_level @@ -1221,7 +1256,8 @@ class CoopBrowserWindow(ba.Window): def _restore_state(self) -> None: try: - sel_name = ba.app.ui.window_states.get(type(self), {}).get( + assert bui.app.classic is not None + sel_name = bui.app.classic.ui.window_states.get(type(self), {}).get( 'sel_name' ) if sel_name == 'Back': @@ -1234,9 +1270,9 @@ class CoopBrowserWindow(ba.Window): sel = self._store_button_widget else: sel = self._scrollwidget - ba.containerwidget(edit=self._root_widget, selected_child=sel) + bui.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: - ba.print_exception(f'Error restoring state for {self}.') + logging.exception('Error restoring state for %s.', self) def sel_change(self, row: str, game: str) -> None: """(internal)""" diff --git a/assets/src/ba_data/python/bastd/ui/coop/gamebutton.py b/src/assets/ba_data/python/bastd/ui/coop/gamebutton.py similarity index 77% rename from assets/src/ba_data/python/bastd/ui/coop/gamebutton.py rename to src/assets/ba_data/python/bastd/ui/coop/gamebutton.py index 4182416f..ea22a7ba 100644 --- a/assets/src/ba_data/python/bastd/ui/coop/gamebutton.py +++ b/src/assets/ba_data/python/bastd/ui/coop/gamebutton.py @@ -7,7 +7,7 @@ from __future__ import annotations import random from typing import TYPE_CHECKING -import ba +import bauiv1 as bui if TYPE_CHECKING: from bastd.ui.coop.browser import CoopBrowserWindow @@ -19,7 +19,7 @@ class GameButton: def __init__( self, window: CoopBrowserWindow, - parent: ba.Widget, + parent: bui.Widget, game: str, x: float, y: float, @@ -28,8 +28,8 @@ class GameButton: ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals - from ba.internal import getcampaign + assert bui.app.classic is not None self._game = game sclx = 195.0 scly = 195.0 @@ -43,7 +43,7 @@ class GameButton: campaignname = 'Default' rating: float | None - campaign = getcampaign(campaignname) + campaign = bui.app.classic.getcampaign(campaignname) rating = campaign.getlevel(levelname).rating if game == 'Easy:The Last Stand': @@ -58,17 +58,17 @@ class GameButton: else: stars = 1 - self._button = btn = ba.buttonwidget( + self._button = btn = bui.buttonwidget( parent=parent, position=(x + 23, y + 4), size=(sclx, scly), label='', - on_activate_call=ba.Call(window.run_game, game), + on_activate_call=bui.Call(window.run_game, game), button_type='square', autoselect=True, - on_select_call=ba.Call(window.sel_change, row, game), + on_select_call=bui.Call(window.sel_change, row, game), ) - ba.widget( + bui.widget( edit=btn, show_buffer_bottom=50, show_buffer_top=50, @@ -76,25 +76,27 @@ class GameButton: show_buffer_right=200, ) if select: - ba.containerwidget( + bui.containerwidget( edit=parent, selected_child=btn, visible_child=btn ) image_width = sclx * 0.85 * 0.75 - self._preview_widget = ba.imagewidget( + self._preview_widget = bui.imagewidget( parent=parent, draw_controller=btn, position=(x + 21 + sclx * 0.5 - image_width * 0.5, y + scly - 104), size=(image_width, image_width * 0.5), - model_transparent=window.lsbt, - model_opaque=window.lsbo, + mesh_transparent=window.lsbt, + mesh_opaque=window.lsbo, texture=campaign.getlevel(levelname).get_preview_texture(), - mask_texture=ba.gettexture('mapPreviewMask'), + mask_texture=bui.gettexture('mapPreviewMask'), ) translated = campaign.getlevel(levelname).displayname - self._achievements = ba.app.ach.achievements_for_coop_level(game) + self._achievements = bui.app.classic.ach.achievements_for_coop_level( + game + ) - self._name_widget = ba.textwidget( + self._name_widget = bui.textwidget( parent=parent, draw_controller=btn, position=(x + 20 + sclx * 0.5, y + scly - 27), @@ -110,9 +112,9 @@ class GameButton: starscale = 35.0 if self._achievements else 45.0 - self._star_widgets: list[ba.Widget] = [] + self._star_widgets: list[bui.Widget] = [] for _i in range(stars): - imw = ba.imagewidget( + imw = bui.imagewidget( parent=parent, draw_controller=btn, position=(xscl, yscl), @@ -122,7 +124,7 @@ class GameButton: self._star_widgets.append(imw) xscl += starscale for _i in range(3 - stars): - ba.imagewidget( + bui.imagewidget( parent=parent, draw_controller=btn, position=(xscl, yscl), @@ -136,10 +138,10 @@ class GameButton: xach = x + 69 yach = y + scly - 168 a_scale = 30.0 - self._achievement_widgets: list[tuple[ba.Widget, ba.Widget]] = [] + self._achievement_widgets: list[tuple[bui.Widget, bui.Widget]] = [] for ach in self._achievements: a_complete = ach.complete - imw = ba.imagewidget( + imw = bui.imagewidget( parent=parent, draw_controller=btn, position=(xach, yach), @@ -147,47 +149,51 @@ class GameButton: color=tuple(ach.get_icon_color(a_complete)[:3]) if a_complete else (1.2, 1.2, 1.2), - texture=ach.get_icon_texture(a_complete), + texture=ach.get_icon_ui_texture(a_complete), ) - imw2 = ba.imagewidget( + imw2 = bui.imagewidget( parent=parent, draw_controller=btn, position=(xach, yach), size=(a_scale, a_scale), color=(2, 1.4, 0.4), texture=window.a_outline_tex, - model_transparent=window.a_outline_model, + mesh_transparent=window.a_outline_mesh, ) self._achievement_widgets.append((imw, imw2)) # if a_complete: xach += a_scale * 1.2 # if not unlocked: - self._lock_widget = ba.imagewidget( + self._lock_widget = bui.imagewidget( parent=parent, draw_controller=btn, position=(x - 8 + sclx * 0.5, y + scly * 0.5 - 20), size=(60, 60), opacity=0.0, - texture=ba.gettexture('lock'), + texture=bui.gettexture('lock'), ) # give a quasi-random update increment to spread the load.. - self._update_timer = ba.Timer( + self._update_timer = bui.AppTimer( 0.001 * (900 + random.randrange(200)), - ba.WeakCall(self._update), + bui.WeakCall(self._update), repeat=True, - timetype=ba.TimeType.REAL, ) self._update() - def get_button(self) -> ba.Widget: - """Return the underlying button ba.Widget.""" + def get_button(self) -> bui.Widget: + """Return the underlying button bui.Widget.""" return self._button def _update(self) -> None: # pylint: disable=too-many-boolean-expressions - from ba.internal import getcampaign + + plus = bui.app.plus + assert plus is not None + + classic = bui.app.classic + assert classic is not None # In case we stick around after our UI... if not self._button: @@ -202,7 +208,7 @@ class GameButton: if game == 'Easy:The Last Stand': campaignname = 'Default' - campaign = getcampaign(campaignname) + campaign = classic.getcampaign(campaignname) # If this campaign is sequential, make sure we've unlocked # everything up to here. @@ -220,6 +226,7 @@ class GameButton: unlocked = False # Hard-code games we haven't unlocked. + assert bui.app.classic is not None if ( ( game @@ -227,11 +234,11 @@ class GameButton: 'Challenges:Infinite Runaround', 'Challenges:Infinite Onslaught', ) - and not ba.app.accounts_v1.have_pro() + and not bui.app.classic.accounts.have_pro() ) or ( game in ('Challenges:Meteor Shower',) - and not ba.internal.get_purchased('games.meteor_shower') + and not plus.get_purchased('games.meteor_shower') ) or ( game @@ -239,15 +246,15 @@ class GameButton: 'Challenges:Target Practice', 'Challenges:Target Practice B', ) - and not ba.internal.get_purchased('games.target_practice') + and not plus.get_purchased('games.target_practice') ) or ( game in ('Challenges:Ninja Fight',) - and not ba.internal.get_purchased('games.ninja_fight') + and not plus.get_purchased('games.ninja_fight') ) or ( game in ('Challenges:Pro Ninja Fight',) - and not ba.internal.get_purchased('games.ninja_fight') + and not plus.get_purchased('games.ninja_fight') ) or ( game @@ -255,7 +262,7 @@ class GameButton: 'Challenges:Easter Egg Hunt', 'Challenges:Pro Easter Egg Hunt', ) - and not ba.internal.get_purchased('games.easter_egg_hunt') + and not plus.get_purchased('games.easter_egg_hunt') ) ): unlocked = False @@ -266,32 +273,34 @@ class GameButton: (0.85, 0.95, 0.5) if game.startswith('Easy:') else (0.5, 0.7, 0.2) ) - ba.buttonwidget( + bui.buttonwidget( edit=self._button, color=unlocked_color if unlocked else (0.5, 0.5, 0.5), ) - ba.imagewidget(edit=self._lock_widget, opacity=0.0 if unlocked else 1.0) - ba.imagewidget( + bui.imagewidget( + edit=self._lock_widget, opacity=0.0 if unlocked else 1.0 + ) + bui.imagewidget( edit=self._preview_widget, opacity=1.0 if unlocked else 0.3 ) - ba.textwidget( + bui.textwidget( edit=self._name_widget, color=(0.8, 1.0, 0.8, 1.0) if unlocked else (0.7, 0.7, 0.7, 0.7), ) for widget in self._star_widgets: - ba.imagewidget( + bui.imagewidget( edit=widget, opacity=1.0 if unlocked else 0.3, color=(2.2, 1.2, 0.3) if unlocked else (1, 1, 1), ) for i, ach in enumerate(self._achievements): a_complete = ach.complete - ba.imagewidget( + bui.imagewidget( edit=self._achievement_widgets[i][0], opacity=1.0 if (a_complete and unlocked) else 0.3, ) - ba.imagewidget( + bui.imagewidget( edit=self._achievement_widgets[i][1], opacity=( 1.0 diff --git a/assets/src/ba_data/python/bastd/ui/coop/level.py b/src/assets/ba_data/python/bastd/ui/coop/level.py similarity index 67% rename from assets/src/ba_data/python/bastd/ui/coop/level.py rename to src/assets/ba_data/python/bastd/ui/coop/level.py index fd19541a..3c6148b0 100644 --- a/assets/src/ba_data/python/bastd/ui/coop/level.py +++ b/src/assets/ba_data/python/bastd/ui/coop/level.py @@ -4,75 +4,76 @@ from __future__ import annotations -import ba +import bauiv1 as bui -class CoopLevelLockedWindow(ba.Window): +class CoopLevelLockedWindow(bui.Window): """Window showing that a level is locked.""" - def __init__(self, name: ba.Lstr, dep_name: ba.Lstr): + def __init__(self, name: bui.Lstr, dep_name: bui.Lstr): width = 550.0 height = 250.0 - lock_tex = ba.gettexture('lock') - uiscale = ba.app.ui.uiscale + lock_tex = bui.gettexture('lock') + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height), transition='in_right', scale=( 1.7 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.3 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), ) ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(150 - 20, height * 0.63), size=(0, 0), h_align='left', v_align='center', - text=ba.Lstr( + text=bui.Lstr( resource='levelIsLockedText', subs=[('${LEVEL}', name)] ), maxwidth=400, color=(1, 0.8, 0.3, 1), scale=1.1, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(150 - 20, height * 0.48), size=(0, 0), h_align='left', v_align='center', - text=ba.Lstr( + text=bui.Lstr( resource='levelMustBeCompletedFirstText', subs=[('${LEVEL}', dep_name)], ), maxwidth=400, - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, scale=0.8, ) - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, position=(56 - 20, height * 0.39), size=(80, 80), texture=lock_tex, opacity=1.0, ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=((width - 140) / 2, 30), size=(140, 50), - label=ba.Lstr(resource='okText'), + label=bui.Lstr(resource='okText'), on_activate_call=self._ok, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=btn, start_button=btn ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() def _ok(self) -> None: - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.containerwidget(edit=self._root_widget, transition='out_left') diff --git a/assets/src/ba_data/python/bastd/ui/coop/tournamentbutton.py b/src/assets/ba_data/python/bastd/ui/coop/tournamentbutton.py similarity index 77% rename from assets/src/ba_data/python/bastd/ui/coop/tournamentbutton.py rename to src/assets/ba_data/python/bastd/ui/coop/tournamentbutton.py index 9f439789..66a6e777 100644 --- a/assets/src/ba_data/python/bastd/ui/coop/tournamentbutton.py +++ b/src/assets/ba_data/python/bastd/ui/coop/tournamentbutton.py @@ -7,8 +7,7 @@ from __future__ import annotations from typing import TYPE_CHECKING import copy -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable @@ -19,7 +18,7 @@ class TournamentButton: def __init__( self, - parent: ba.Widget, + parent: bui.Widget, x: float, y: float, select: bool, @@ -29,25 +28,24 @@ class TournamentButton: sclx = 300 scly = 195.0 self.on_pressed = on_pressed - self.lsbt = ba.getmodel('level_select_button_transparent') - self.lsbo = ba.getmodel('level_select_button_opaque') + self.lsbt = bui.getmesh('level_select_button_transparent') + self.lsbo = bui.getmesh('level_select_button_opaque') self.allow_ads = False self.tournament_id: str | None = None self.time_remaining: int = 0 self.has_time_remaining: bool = False self.leader: Any = None self.required_league: str | None = None - self.button = btn = ba.buttonwidget( + self.button = btn = bui.buttonwidget( parent=parent, position=(x + 23, y + 4), size=(sclx, scly), label='', button_type='square', autoselect=True, - # on_activate_call=lambda: self.run(None, tournament_button=data) - on_activate_call=ba.WeakCall(self._pressed), + on_activate_call=bui.WeakCall(self._pressed), ) - ba.widget( + bui.widget( edit=btn, show_buffer_bottom=50, show_buffer_top=50, @@ -55,33 +53,33 @@ class TournamentButton: show_buffer_right=200, ) if select: - ba.containerwidget( + bui.containerwidget( edit=parent, selected_child=btn, visible_child=btn ) image_width = sclx * 0.85 * 0.75 - self.image = ba.imagewidget( + self.image = bui.imagewidget( parent=parent, draw_controller=btn, position=(x + 21 + sclx * 0.5 - image_width * 0.5, y + scly - 150), size=(image_width, image_width * 0.5), - model_transparent=self.lsbt, - model_opaque=self.lsbo, - texture=ba.gettexture('black'), + mesh_transparent=self.lsbt, + mesh_opaque=self.lsbo, + texture=bui.gettexture('black'), opacity=0.2, - mask_texture=ba.gettexture('mapPreviewMask'), + mask_texture=bui.gettexture('mapPreviewMask'), ) - self.lock_image = ba.imagewidget( + self.lock_image = bui.imagewidget( parent=parent, draw_controller=btn, position=(x + 21 + sclx * 0.5 - image_width * 0.25, y + scly - 150), size=(image_width * 0.5, image_width * 0.5), - texture=ba.gettexture('lock'), + texture=bui.gettexture('lock'), opacity=0.0, ) - self.button_text = ba.textwidget( + self.button_text = bui.textwidget( parent=parent, draw_controller=btn, position=(x + 20 + sclx * 0.5, y + scly - 35), @@ -98,13 +96,13 @@ class TournamentButton: value_color = (0.6, 0.6, 0.6, 1) x_offs = 0 - ba.textwidget( + bui.textwidget( parent=parent, draw_controller=btn, position=(x + 360, y + scly - 20), size=(0, 0), h_align='center', - text=ba.Lstr(resource=self._r + '.entryFeeText'), + text=bui.Lstr(resource=self._r + '.entryFeeText'), v_align='center', maxwidth=100, scale=0.9, @@ -112,7 +110,7 @@ class TournamentButton: flatness=1.0, ) - self.entry_fee_text_top = ba.textwidget( + self.entry_fee_text_top = bui.textwidget( parent=parent, draw_controller=btn, position=(x + 360, y + scly - 60), @@ -125,7 +123,7 @@ class TournamentButton: color=value_color, flatness=1.0, ) - self.entry_fee_text_or = ba.textwidget( + self.entry_fee_text_or = bui.textwidget( parent=parent, draw_controller=btn, position=(x + 360, y + scly - 90), @@ -138,7 +136,7 @@ class TournamentButton: color=value_color, flatness=1.0, ) - self.entry_fee_text_remaining = ba.textwidget( + self.entry_fee_text_remaining = bui.textwidget( parent=parent, draw_controller=btn, position=(x + 360, y + scly - 90), @@ -152,24 +150,24 @@ class TournamentButton: flatness=1.0, ) - self.entry_fee_ad_image = ba.imagewidget( + self.entry_fee_ad_image = bui.imagewidget( parent=parent, size=(40, 40), draw_controller=btn, position=(x + 360 - 20, y + scly - 140), opacity=0.0, - texture=ba.gettexture('tv'), + texture=bui.gettexture('tv'), ) x_offs += 50 - ba.textwidget( + bui.textwidget( parent=parent, draw_controller=btn, position=(x + 447 + x_offs, y + scly - 20), size=(0, 0), h_align='center', - text=ba.Lstr(resource=self._r + '.prizesText'), + text=bui.Lstr(resource=self._r + '.prizesText'), v_align='center', maxwidth=130, scale=0.9, @@ -184,7 +182,7 @@ class TournamentButton: xo2 = 0 prize_value_scale = 1.5 - self.prize_range_1_text = ba.textwidget( + self.prize_range_1_text = bui.textwidget( parent=parent, draw_controller=btn, position=(x + 355 + xo2 + x_offs, y + scly - 93), @@ -197,7 +195,7 @@ class TournamentButton: color=header_color, flatness=1.0, ) - self.prize_value_1_text = ba.textwidget( + self.prize_value_1_text = bui.textwidget( parent=parent, draw_controller=btn, position=(x + 380 + xo2 + x_offs, y + scly - 93), @@ -211,7 +209,7 @@ class TournamentButton: flatness=1.0, ) - self.prize_range_2_text = ba.textwidget( + self.prize_range_2_text = bui.textwidget( parent=parent, draw_controller=btn, position=(x + 355 + xo2 + x_offs, y + scly - 93), @@ -223,7 +221,7 @@ class TournamentButton: color=header_color, flatness=1.0, ) - self.prize_value_2_text = ba.textwidget( + self.prize_value_2_text = bui.textwidget( parent=parent, draw_controller=btn, position=(x + 380 + xo2 + x_offs, y + scly - 93), @@ -237,7 +235,7 @@ class TournamentButton: flatness=1.0, ) - self.prize_range_3_text = ba.textwidget( + self.prize_range_3_text = bui.textwidget( parent=parent, draw_controller=btn, position=(x + 355 + xo2 + x_offs, y + scly - 93), @@ -249,7 +247,7 @@ class TournamentButton: color=header_color, flatness=1.0, ) - self.prize_value_3_text = ba.textwidget( + self.prize_value_3_text = bui.textwidget( parent=parent, draw_controller=btn, position=(x + 380 + xo2 + x_offs, y + scly - 93), @@ -263,20 +261,20 @@ class TournamentButton: flatness=1.0, ) - ba.textwidget( + bui.textwidget( parent=parent, draw_controller=btn, position=(x + 620 + x_offs, y + scly - 20), size=(0, 0), h_align='center', - text=ba.Lstr(resource=self._r + '.currentBestText'), + text=bui.Lstr(resource=self._r + '.currentBestText'), v_align='center', maxwidth=180, scale=0.9, color=header_color, flatness=1.0, ) - self.current_leader_name_text = ba.textwidget( + self.current_leader_name_text = bui.textwidget( parent=parent, draw_controller=btn, position=( @@ -286,7 +284,7 @@ class TournamentButton: selectable=True, click_activate=True, autoselect=True, - on_activate_call=ba.WeakCall(self._show_leader), + on_activate_call=bui.WeakCall(self._show_leader), size=(170 / 1.4, 40), h_align='center', text='-', @@ -296,7 +294,7 @@ class TournamentButton: color=value_color, flatness=1.0, ) - self.current_leader_score_text = ba.textwidget( + self.current_leader_score_text = bui.textwidget( parent=parent, draw_controller=btn, position=(x + 620 + x_offs, y + scly - 113 + 10), @@ -310,7 +308,7 @@ class TournamentButton: flatness=1.0, ) - self.more_scores_button = ba.buttonwidget( + self.more_scores_button = bui.buttonwidget( parent=parent, position=(x + 620 + x_offs - 60, y + scly - 50 - 125), color=(0.5, 0.5, 0.6), @@ -320,27 +318,27 @@ class TournamentButton: autoselect=True, up_widget=self.current_leader_name_text, text_scale=0.6, - on_activate_call=ba.WeakCall(self._show_scores), + on_activate_call=bui.WeakCall(self._show_scores), ) - ba.widget( + bui.widget( edit=self.current_leader_name_text, down_widget=self.more_scores_button, ) - ba.textwidget( + bui.textwidget( parent=parent, draw_controller=btn, position=(x + 820 + x_offs, y + scly - 20), size=(0, 0), h_align='center', - text=ba.Lstr(resource=self._r + '.timeRemainingText'), + text=bui.Lstr(resource=self._r + '.timeRemainingText'), v_align='center', maxwidth=180, scale=0.9, color=header_color, flatness=1.0, ) - self.time_remaining_value_text = ba.textwidget( + self.time_remaining_value_text = bui.textwidget( parent=parent, draw_controller=btn, position=(x + 820 + x_offs, y + scly - 68), @@ -353,7 +351,7 @@ class TournamentButton: color=value_color, flatness=1.0, ) - self.time_remaining_out_of_text = ba.textwidget( + self.time_remaining_out_of_text = bui.textwidget( parent=parent, draw_controller=btn, position=(x + 820 + x_offs, y + scly - 110), @@ -383,9 +381,9 @@ class TournamentButton: or self.leader is None or len(self.leader[2]) != 1 ): - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() AccountViewerWindow( account_id=self.leader[2][0].get('a', None), profile_id=self.leader[2][0].get('p', None), @@ -398,7 +396,7 @@ class TournamentButton: tournament_id = self.tournament_id if tournament_id is None: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return TournamentScoresWindow( @@ -411,8 +409,11 @@ class TournamentButton: # pylint: disable=too-many-statements # pylint: disable=too-many-locals # pylint: disable=too-many-branches - from ba.internal import getcampaign, get_tournament_prize_strings + plus = bui.app.plus + assert plus is not None + + assert bui.app.classic is not None prize_y_offs = ( 34 if 'prizeRange3' in entry @@ -424,15 +425,22 @@ class TournamentButton: # This seems to be a false alarm. # pylint: disable=unbalanced-tuple-unpacking - pr1, pv1, pr2, pv2, pr3, pv3 = get_tournament_prize_strings(entry) + ( + pr1, + pv1, + pr2, + pv2, + pr3, + pv3, + ) = bui.app.classic.get_tournament_prize_strings(entry) # pylint: enable=unbalanced-tuple-unpacking enabled = 'requiredLeague' not in entry - ba.buttonwidget( + bui.buttonwidget( edit=self.button, color=(0.5, 0.7, 0.2) if enabled else (0.5, 0.5, 0.5), ) - ba.imagewidget(edit=self.lock_image, opacity=0.0 if enabled else 1.0) - ba.textwidget( + bui.imagewidget(edit=self.lock_image, opacity=0.0 if enabled else 1.0) + bui.textwidget( edit=self.prize_range_1_text, text='-' if pr1 == '' else pr1, position=( @@ -443,11 +451,11 @@ class TournamentButton: # We want to draw values containing tickets a bit smaller # (scratch that; we now draw medals a bit bigger). - ticket_char = ba.charstr(ba.SpecialChar.TICKET_BACKING) + ticket_char = bui.charstr(bui.SpecialChar.TICKET_BACKING) prize_value_scale_large = 1.0 prize_value_scale_small = 1.0 - ba.textwidget( + bui.textwidget( edit=self.prize_value_1_text, text='-' if pv1 == '' else pv1, scale=prize_value_scale_large @@ -459,7 +467,7 @@ class TournamentButton: ), ) - ba.textwidget( + bui.textwidget( edit=self.prize_range_2_text, text=pr2, position=( @@ -467,7 +475,7 @@ class TournamentButton: self.button_y + self.button_scale_y - 93 - 45 + prize_y_offs, ), ) - ba.textwidget( + bui.textwidget( edit=self.prize_value_2_text, text=pv2, scale=prize_value_scale_large @@ -479,7 +487,7 @@ class TournamentButton: ), ) - ba.textwidget( + bui.textwidget( edit=self.prize_range_3_text, text=pr3, position=( @@ -487,7 +495,7 @@ class TournamentButton: self.button_y + self.button_scale_y - 93 - 90 + prize_y_offs, ), ) - ba.textwidget( + bui.textwidget( edit=self.prize_value_3_text, text=pv3, scale=prize_value_scale_large @@ -500,49 +508,40 @@ class TournamentButton: ) leader_name = '-' - leader_score: str | ba.Lstr = '-' + leader_score: str | bui.Lstr = '-' if entry['scores']: score = self.leader = copy.deepcopy(entry['scores'][0]) leader_name = score[1] leader_score = ( - ba.timestring( - score[0] * 10, - centi=True, - timeformat=ba.TimeFormat.MILLISECONDS, - suppress_format_warning=True, - ) + bui.timestring((score[0] * 10) / 1000.0, centi=True) if entry['scoreType'] == 'time' else str(score[0]) ) else: self.leader = None - ba.textwidget( - edit=self.current_leader_name_text, text=ba.Lstr(value=leader_name) + bui.textwidget( + edit=self.current_leader_name_text, text=bui.Lstr(value=leader_name) ) - ba.textwidget(edit=self.current_leader_score_text, text=leader_score) - ba.buttonwidget( + bui.textwidget(edit=self.current_leader_score_text, text=leader_score) + bui.buttonwidget( edit=self.more_scores_button, - label=ba.Lstr(resource=self._r + '.seeMoreText'), + label=bui.Lstr(resource=self._r + '.seeMoreText'), ) - out_of_time_text: str | ba.Lstr = ( + out_of_time_text: str | bui.Lstr = ( '-' if 'totalTime' not in entry - else ba.Lstr( + else bui.Lstr( resource=self._r + '.ofTotalTimeText', subs=[ ( '${TOTAL}', - ba.timestring( - entry['totalTime'], - centi=False, - suppress_format_warning=True, - ), + bui.timestring(entry['totalTime'], centi=False), ) ], ) ) - ba.textwidget( + bui.textwidget( edit=self.time_remaining_out_of_text, text=out_of_time_text ) @@ -553,34 +552,37 @@ class TournamentButton: None if 'requiredLeague' not in entry else entry['requiredLeague'] ) - game = ba.app.accounts_v1.tournament_info[self.tournament_id]['game'] + assert bui.app.classic is not None + game = bui.app.classic.accounts.tournament_info[self.tournament_id][ + 'game' + ] if game is None: - ba.textwidget(edit=self.button_text, text='-') - ba.imagewidget( - edit=self.image, texture=ba.gettexture('black'), opacity=0.2 + bui.textwidget(edit=self.button_text, text='-') + bui.imagewidget( + edit=self.image, texture=bui.gettexture('black'), opacity=0.2 ) else: campaignname, levelname = game.split(':') - campaign = getcampaign(campaignname) - max_players = ba.app.accounts_v1.tournament_info[ + campaign = bui.app.classic.getcampaign(campaignname) + max_players = bui.app.classic.accounts.tournament_info[ self.tournament_id ]['maxPlayers'] - txt = ba.Lstr( + txt = bui.Lstr( value='${A} ${B}', subs=[ ('${A}', campaign.getlevel(levelname).displayname), ( '${B}', - ba.Lstr( + bui.Lstr( resource='playerCountAbbreviatedText', subs=[('${COUNT}', str(max_players))], ), ), ], ) - ba.textwidget(edit=self.button_text, text=txt) - ba.imagewidget( + bui.textwidget(edit=self.button_text, text=txt) + bui.imagewidget( edit=self.image, texture=campaign.getlevel(levelname).get_preview_texture(), opacity=1.0 if enabled else 0.5, @@ -608,42 +610,43 @@ class TournamentButton: final_fee: int | None = ( None if fee_var is None - else ba.internal.get_v1_account_misc_read_val(fee_var, '?') + else plus.get_v1_account_misc_read_val(fee_var, '?') ) - final_fee_str: str | ba.Lstr + final_fee_str: str | bui.Lstr if fee_var is None: final_fee_str = '' else: if final_fee == 0: - final_fee_str = ba.Lstr(resource='getTicketsWindow.freeText') + final_fee_str = bui.Lstr(resource='getTicketsWindow.freeText') else: - final_fee_str = ba.charstr(ba.SpecialChar.TICKET_BACKING) + str( - final_fee - ) + final_fee_str = bui.charstr( + bui.SpecialChar.TICKET_BACKING + ) + str(final_fee) - ad_tries_remaining = ba.app.accounts_v1.tournament_info[ + assert bui.app.classic is not None + ad_tries_remaining = bui.app.classic.accounts.tournament_info[ self.tournament_id ]['adTriesRemaining'] - free_tries_remaining = ba.app.accounts_v1.tournament_info[ + free_tries_remaining = bui.app.classic.accounts.tournament_info[ self.tournament_id ]['freeTriesRemaining'] # Now, if this fee allows ads and we support video ads, show # the 'or ad' version. - if allow_ads and ba.internal.has_video_ads(): - ads_enabled = ba.internal.have_incentivized_ad() - ba.imagewidget( + if allow_ads and bui.has_video_ads(): + ads_enabled = bui.have_incentivized_ad() + bui.imagewidget( edit=self.entry_fee_ad_image, opacity=1.0 if ads_enabled else 0.25, ) or_text = ( - ba.Lstr(resource='orText', subs=[('${A}', ''), ('${B}', '')]) + bui.Lstr(resource='orText', subs=[('${A}', ''), ('${B}', '')]) .evaluate() .strip() ) - ba.textwidget(edit=self.entry_fee_text_or, text=or_text) - ba.textwidget( + bui.textwidget(edit=self.entry_fee_text_or, text=or_text) + bui.textwidget( edit=self.entry_fee_text_top, position=( self.button_x + 360, @@ -654,7 +657,7 @@ class TournamentButton: ) # Possibly show number of ad-plays remaining. - ba.textwidget( + bui.textwidget( edit=self.entry_fee_text_remaining, position=( self.button_x + 360, @@ -666,9 +669,9 @@ class TournamentButton: color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2), ) else: - ba.imagewidget(edit=self.entry_fee_ad_image, opacity=0.0) - ba.textwidget(edit=self.entry_fee_text_or, text='') - ba.textwidget( + bui.imagewidget(edit=self.entry_fee_ad_image, opacity=0.0) + bui.textwidget(edit=self.entry_fee_text_or, text='') + bui.textwidget( edit=self.entry_fee_text_top, position=( self.button_x + 360, @@ -679,7 +682,7 @@ class TournamentButton: ) # Possibly show number of free-plays remaining. - ba.textwidget( + bui.textwidget( edit=self.entry_fee_text_remaining, position=( self.button_x + 360, diff --git a/assets/src/ba_data/python/bastd/ui/creditslist.py b/src/assets/ba_data/python/bastd/ui/creditslist.py similarity index 72% rename from assets/src/ba_data/python/bastd/ui/creditslist.py rename to src/assets/ba_data/python/bastd/ui/creditslist.py index 6a79582c..f1ac8e40 100644 --- a/assets/src/ba_data/python/bastd/ui/creditslist.py +++ b/src/assets/ba_data/python/bastd/ui/creditslist.py @@ -4,24 +4,25 @@ from __future__ import annotations +import os +import logging from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Sequence -class CreditsListWindow(ba.Window): +class CreditsListWindow(bui.Window): """Window for displaying game credits.""" - def __init__(self, origin_widget: ba.Widget | None = None): + def __init__(self, origin_widget: bui.Widget | None = None): # pylint: disable=too-many-locals # pylint: disable=too-many-statements import json - ba.set_analytics_screen('Credits Window') + bui.set_analytics_screen('Credits Window') # if they provided an origin-widget, scale up from that scale_origin: tuple[float, float] | None @@ -34,98 +35,100 @@ class CreditsListWindow(ba.Window): scale_origin = None transition = 'in_right' - uiscale = ba.app.ui.uiscale - width = 870 if uiscale is ba.UIScale.SMALL else 670 - x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 - height = 398 if uiscale is ba.UIScale.SMALL else 500 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + width = 870 if uiscale is bui.UIScale.SMALL else 670 + x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 + height = 398 if uiscale is bui.UIScale.SMALL else 500 self._r = 'creditsWindow' super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height), transition=transition, toolbar_visibility='menu_minimal', scale_origin_stack_offset=scale_origin, scale=( 2.0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.3 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), - stack_offset=(0, -8) if uiscale is ba.UIScale.SMALL else (0, 0), + stack_offset=(0, -8) + if uiscale is bui.UIScale.SMALL + else (0, 0), ) ) - if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: - ba.containerwidget( + if bui.app.classic.ui.use_toolbars and uiscale is bui.UIScale.SMALL: + bui.containerwidget( edit=self._root_widget, on_cancel_call=self._back ) else: - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=( 40 + x_inset, - height - (68 if uiscale is ba.UIScale.SMALL else 62), + height - (68 if uiscale is bui.UIScale.SMALL else 62), ), size=(140, 60), scale=0.8, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', on_activate_call=self._back, autoselect=True, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) - ba.buttonwidget( + bui.buttonwidget( edit=btn, button_type='backSmall', position=( 40 + x_inset, - height - (68 if uiscale is ba.UIScale.SMALL else 62) + 5, + height - (68 if uiscale is bui.UIScale.SMALL else 62) + 5, ), size=(60, 48), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, - position=(0, height - (59 if uiscale is ba.UIScale.SMALL else 54)), + position=(0, height - (59 if uiscale is bui.UIScale.SMALL else 54)), size=(width, 30), - text=ba.Lstr( + text=bui.Lstr( resource=self._r + '.titleText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], + subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ), h_align='center', - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, maxwidth=330, v_align='center', ) - scroll = ba.scrollwidget( + scroll = bui.scrollwidget( parent=self._root_widget, position=(40 + x_inset, 35), size=(width - (80 + 2 * x_inset), height - 100), capture_arrows=True, ) - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=scroll, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - if uiscale is ba.UIScale.SMALL: - ba.widget( + if uiscale is bui.UIScale.SMALL: + bui.widget( edit=scroll, - left_widget=ba.internal.get_special_widget('back_button'), + left_widget=bui.get_special_widget('back_button'), ) def _format_names(names2: Sequence[str], inset: float) -> str: sval = '' # measure a series since there's overlaps and stuff.. space_width = ( - ba.internal.get_string_width(' ' * 10, suppress_warning=True) - / 10.0 + bui.get_string_width(' ' * 10, suppress_warning=True) / 10.0 ) spacing = 330.0 col1 = inset @@ -149,14 +152,12 @@ class CreditsListWindow(ba.Window): spacingstr = ' ' * int((target - line_width) / space_width) nline += spacingstr nline += name - line_width = ba.internal.get_string_width( - nline, suppress_warning=True - ) + line_width = bui.get_string_width(nline, suppress_warning=True) if nline != '': sval += nline + '\n' return sval - sound_and_music = ba.Lstr( + sound_and_music = bui.Lstr( resource=self._r + '.songCreditText' ).evaluate() sound_and_music = sound_and_music.replace( @@ -209,12 +210,17 @@ class CreditsListWindow(ba.Window): freesound_names = _format_names(names, 90) try: - with open('ba_data/data/langdata.json', encoding='utf-8') as infile: + with open( + os.path.join( + bui.app.data_directory, 'ba_data', 'data', 'langdata.json' + ), + encoding='utf-8', + ) as infile: translation_contributors = json.loads(infile.read())[ 'translation_contributors' ] except Exception: - ba.print_exception('Error reading translation contributors.') + logging.exception('Error reading translation contributors.') translation_contributors = [] translation_names = _format_names(translation_contributors, 60) @@ -225,48 +231,50 @@ class CreditsListWindow(ba.Window): # (or add mesh splitting under the hood) credits_text = ( ' ' - + ba.Lstr(resource=self._r + '.codingGraphicsAudioText') + + bui.Lstr(resource=self._r + '.codingGraphicsAudioText') .evaluate() .replace('${NAME}', 'Eric Froemling') + '\n' '\n' ' ' - + ba.Lstr(resource=self._r + '.additionalAudioArtIdeasText') + + bui.Lstr(resource=self._r + '.additionalAudioArtIdeasText') .evaluate() .replace('${NAME}', 'Raphael Suter') + '\n' '\n' ' ' - + ba.Lstr(resource=self._r + '.soundAndMusicText').evaluate() + + bui.Lstr(resource=self._r + '.soundAndMusicText').evaluate() + '\n' '\n' + sound_and_music + '\n' '\n' ' ' - + ba.Lstr(resource=self._r + '.publicDomainMusicViaText') + + bui.Lstr(resource=self._r + '.publicDomainMusicViaText') .evaluate() .replace('${NAME}', 'Musopen.com') + '\n' ' ' - + ba.Lstr(resource=self._r + '.thanksEspeciallyToText') + + bui.Lstr(resource=self._r + '.thanksEspeciallyToText') .evaluate() .replace('${NAME}', 'the US Army, Navy, and Marine Bands') + '\n' '\n' ' ' - + ba.Lstr(resource=self._r + '.additionalMusicFromText') + + bui.Lstr(resource=self._r + '.additionalMusicFromText') .evaluate() .replace('${NAME}', 'The YouTube Audio Library') + '\n' '\n' ' ' - + ba.Lstr(resource=self._r + '.soundsText') + + bui.Lstr(resource=self._r + '.soundsText') .evaluate() .replace('${SOURCE}', 'Freesound.org') + '\n' '\n' + freesound_names + '\n' '\n' ' ' - + ba.Lstr(resource=self._r + '.languageTranslationsText').evaluate() + + bui.Lstr( + resource=self._r + '.languageTranslationsText' + ).evaluate() + '\n' '\n' + '\n'.join(translation_names.splitlines()[:146]) @@ -286,25 +294,25 @@ class CreditsListWindow(ba.Window): ' Holiday theme vector art designed by Freepik\n' '\n' ' ' - + ba.Lstr(resource=self._r + '.specialThanksText').evaluate() + + bui.Lstr(resource=self._r + '.specialThanksText').evaluate() + '\n' '\n' ' Todd, Laura, and Robert Froemling\n' ' ' - + ba.Lstr(resource=self._r + '.allMyFamilyText') + + bui.Lstr(resource=self._r + '.allMyFamilyText') .evaluate() .replace('\n', '\n ') + '\n' ' ' - + ba.Lstr( + + bui.Lstr( resource=self._r + '.whoeverInventedCoffeeText' ).evaluate() + '\n' '\n' - ' ' + ba.Lstr(resource=self._r + '.legalText').evaluate() + '\n' + ' ' + bui.Lstr(resource=self._r + '.legalText').evaluate() + '\n' '\n' ' ' - + ba.Lstr(resource=self._r + '.softwareBasedOnText') + + bui.Lstr(resource=self._r + '.softwareBasedOnText') .evaluate() .replace('${NAME}', 'the Khronos Group') + '\n' @@ -321,7 +329,7 @@ class CreditsListWindow(ba.Window): self._sub_width = width - 80 self._sub_height = line_height * len(lines) + 40 - container = self._subcontainer = ba.containerwidget( + container = self._subcontainer = bui.containerwidget( parent=scroll, size=(self._sub_width, self._sub_height), background=False, @@ -331,7 +339,7 @@ class CreditsListWindow(ba.Window): voffs = 0 for line in lines: - ba.textwidget( + bui.textwidget( parent=container, padding=4, color=(0.7, 0.9, 0.7, 1.0), @@ -341,16 +349,17 @@ class CreditsListWindow(ba.Window): position=(0, self._sub_height - 20 + voffs), h_align='left', v_align='top', - text=ba.Lstr(value=line), + text=bui.Lstr(value=line), ) voffs -= line_height def _back(self) -> None: from bastd.ui.mainmenu import MainMenuWindow - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( MainMenuWindow(transition='in_left').get_root_widget() ) diff --git a/assets/src/ba_data/python/bastd/ui/debug.py b/src/assets/ba_data/python/bastd/ui/debug.py similarity index 70% rename from assets/src/ba_data/python/bastd/ui/debug.py rename to src/assets/ba_data/python/bastd/ui/debug.py index 888fd15f..e71583ce 100644 --- a/assets/src/ba_data/python/bastd/ui/debug.py +++ b/src/assets/ba_data/python/bastd/ui/debug.py @@ -4,15 +4,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING, cast +import logging +from typing import cast -import ba - -if TYPE_CHECKING: - pass +import bauiv1 as bui -class DebugWindow(ba.Window): +class DebugWindow(bui.Window): """Window for debugging internal values.""" def __init__(self, transition: str | None = 'in_right'): @@ -20,14 +18,15 @@ class DebugWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui import popup - ba.app.ui.set_main_menu_location('Benchmarks & Stress Tests') - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_location('Benchmarks & Stress Tests') + uiscale = bui.app.classic.ui.uiscale self._width = width = 580 self._height = height = ( 350 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 420 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 520 ) @@ -43,54 +42,54 @@ class DebugWindow(ba.Window): self._stress_test_round_duration = 30 self._r = 'debugWindow' - uiscale = ba.app.ui.uiscale + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height), transition=transition, scale=( 2.35 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.55 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -30) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - self._done_button = btn = ba.buttonwidget( + self._done_button = btn = bui.buttonwidget( parent=self._root_widget, position=(40, height - 67), size=(120, 60), scale=0.8, autoselect=True, - label=ba.Lstr(resource='doneText'), + label=bui.Lstr(resource='doneText'), on_activate_call=self._done, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) - ba.textwidget( + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.textwidget( parent=self._root_widget, position=(0, height - 60), size=(width, 30), - text=ba.Lstr(resource=self._r + '.titleText'), + text=bui.Lstr(resource=self._r + '.titleText'), h_align='center', - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, v_align='center', maxwidth=260, ) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, highlight=False, size=(self._scroll_width, self._scroll_height), position=((self._width - self._scroll_width) * 0.5, 50), ) - ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) + bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._sub_width, self._sub_height), background=False, @@ -98,46 +97,46 @@ class DebugWindow(ba.Window): v = self._sub_height - 70 button_width = 300 - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), size=(button_width, 60), autoselect=True, - label=ba.Lstr(resource=self._r + '.runCPUBenchmarkText'), + label=bui.Lstr(resource=self._r + '.runCPUBenchmarkText'), on_activate_call=self._run_cpu_benchmark_pressed, ) - ba.widget( + bui.widget( edit=btn, up_widget=self._done_button, left_widget=self._done_button ) v -= 60 - ba.buttonwidget( + bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), size=(button_width, 60), autoselect=True, - label=ba.Lstr(resource=self._r + '.runGPUBenchmarkText'), + label=bui.Lstr(resource=self._r + '.runGPUBenchmarkText'), on_activate_call=self._run_gpu_benchmark_pressed, ) v -= 60 - ba.buttonwidget( + bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), size=(button_width, 60), autoselect=True, - label=ba.Lstr(resource=self._r + '.runMediaReloadBenchmarkText'), + label=bui.Lstr(resource=self._r + '.runMediaReloadBenchmarkText'), on_activate_call=self._run_media_reload_benchmark_pressed, ) v -= 60 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v + 22), size=(0, 0), - text=ba.Lstr(resource=self._r + '.stressTestTitleText'), + text=bui.Lstr(resource=self._r + '.stressTestTitleText'), maxwidth=200, - color=ba.app.ui.heading_color, + color=bui.app.classic.ui.heading_color, scale=0.85, h_align='center', v_align='center', @@ -145,13 +144,13 @@ class DebugWindow(ba.Window): v -= 45 x_offs = 165 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(x_offs - 10, v + 22), size=(0, 0), - text=ba.Lstr(resource=self._r + '.stressTestPlaylistTypeText'), + text=bui.Lstr(resource=self._r + '.stressTestPlaylistTypeText'), maxwidth=130, - color=ba.app.ui.heading_color, + color=bui.app.classic.ui.heading_color, scale=0.65, h_align='right', v_align='center', @@ -163,7 +162,7 @@ class DebugWindow(ba.Window): width=150, choices=['Random', 'Teams', 'Free-For-All'], choices_display=[ - ba.Lstr(resource=a) + bui.Lstr(resource=a) for a in [ 'randomText', 'playModes.teamsText', @@ -175,19 +174,19 @@ class DebugWindow(ba.Window): ) v -= 46 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(x_offs - 10, v + 22), size=(0, 0), - text=ba.Lstr(resource=self._r + '.stressTestPlaylistNameText'), + text=bui.Lstr(resource=self._r + '.stressTestPlaylistNameText'), maxwidth=130, - color=ba.app.ui.heading_color, + color=bui.app.classic.ui.heading_color, scale=0.65, h_align='right', v_align='center', ) - self._stress_test_playlist_name_field = ba.textwidget( + self._stress_test_playlist_name_field = bui.textwidget( parent=self._subcontainer, position=(x_offs + 5, v - 5), size=(250, 46), @@ -196,7 +195,7 @@ class DebugWindow(ba.Window): v_align='center', autoselect=True, color=(0.9, 0.9, 0.9, 1.0), - description=ba.Lstr( + description=bui.Lstr( resource=self._r + '.stressTestPlaylistDescriptionText' ), editable=True, @@ -206,18 +205,18 @@ class DebugWindow(ba.Window): x_sub = 60 # Player count. - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(x_offs - 10, v), size=(0, 0), - text=ba.Lstr(resource=self._r + '.stressTestPlayerCountText'), + text=bui.Lstr(resource=self._r + '.stressTestPlayerCountText'), color=(0.8, 0.8, 0.8, 1.0), h_align='right', v_align='center', scale=0.65, maxwidth=130, ) - self._stress_test_player_count_text = ba.textwidget( + self._stress_test_player_count_text = bui.textwidget( parent=self._subcontainer, position=(246 - x_sub, v - 14), size=(60, 28), @@ -228,41 +227,41 @@ class DebugWindow(ba.Window): text=str(self._stress_test_player_count), padding=2, ) - ba.buttonwidget( + bui.buttonwidget( parent=self._subcontainer, position=(330 - x_sub, v - 11), size=(28, 28), label='-', autoselect=True, - on_activate_call=ba.Call(self._stress_test_player_count_decrement), + on_activate_call=bui.Call(self._stress_test_player_count_decrement), repeat=True, enable_sound=True, ) - ba.buttonwidget( + bui.buttonwidget( parent=self._subcontainer, position=(380 - x_sub, v - 11), size=(28, 28), label='+', autoselect=True, - on_activate_call=ba.Call(self._stress_test_player_count_increment), + on_activate_call=bui.Call(self._stress_test_player_count_increment), repeat=True, enable_sound=True, ) v -= 42 # Round duration. - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(x_offs - 10, v), size=(0, 0), - text=ba.Lstr(resource=self._r + '.stressTestRoundDurationText'), + text=bui.Lstr(resource=self._r + '.stressTestRoundDurationText'), color=(0.8, 0.8, 0.8, 1.0), h_align='right', v_align='center', scale=0.65, maxwidth=130, ) - self._stress_test_round_duration_text = ba.textwidget( + self._stress_test_round_duration_text = bui.textwidget( parent=self._subcontainer, position=(246 - x_sub, v - 14), size=(60, 28), @@ -273,53 +272,53 @@ class DebugWindow(ba.Window): text=str(self._stress_test_round_duration), padding=2, ) - ba.buttonwidget( + bui.buttonwidget( parent=self._subcontainer, position=(330 - x_sub, v - 11), size=(28, 28), label='-', autoselect=True, - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._stress_test_round_duration_decrement ), repeat=True, enable_sound=True, ) - ba.buttonwidget( + bui.buttonwidget( parent=self._subcontainer, position=(380 - x_sub, v - 11), size=(28, 28), label='+', autoselect=True, - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._stress_test_round_duration_increment ), repeat=True, enable_sound=True, ) v -= 82 - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), size=(button_width, 60), autoselect=True, - label=ba.Lstr(resource=self._r + '.runStressTestText'), + label=bui.Lstr(resource=self._r + '.runStressTestText'), on_activate_call=self._stress_test_pressed, ) - ba.widget(btn, show_buffer_bottom=50) + bui.widget(btn, show_buffer_bottom=50) def _stress_test_player_count_decrement(self) -> None: self._stress_test_player_count = max( 1, self._stress_test_player_count - 1 ) - ba.textwidget( + bui.textwidget( edit=self._stress_test_player_count_text, text=str(self._stress_test_player_count), ) def _stress_test_player_count_increment(self) -> None: self._stress_test_player_count = self._stress_test_player_count + 1 - ba.textwidget( + bui.textwidget( edit=self._stress_test_player_count_text, text=str(self._stress_test_player_count), ) @@ -328,14 +327,14 @@ class DebugWindow(ba.Window): self._stress_test_round_duration = max( 10, self._stress_test_round_duration - 10 ) - ba.textwidget( + bui.textwidget( edit=self._stress_test_round_duration_text, text=str(self._stress_test_round_duration), ) def _stress_test_round_duration_increment(self) -> None: self._stress_test_round_duration = self._stress_test_round_duration + 10 - ba.textwidget( + bui.textwidget( edit=self._stress_test_round_duration_text, text=str(self._stress_test_round_duration), ) @@ -344,38 +343,44 @@ class DebugWindow(ba.Window): self._stress_test_game_type = game_type def _run_cpu_benchmark_pressed(self) -> None: - from ba.internal import run_cpu_benchmark - - run_cpu_benchmark() + if bui.app.classic is None: + logging.warning('run-cpu-benchmark requires classic') + return + bui.app.classic.run_cpu_benchmark() def _run_gpu_benchmark_pressed(self) -> None: - from ba.internal import run_gpu_benchmark - - run_gpu_benchmark() + if bui.app.classic is None: + logging.warning('run-gpu-benchmark requires classic') + return + bui.app.classic.run_gpu_benchmark() def _run_media_reload_benchmark_pressed(self) -> None: - from ba.internal import run_media_reload_benchmark - - run_media_reload_benchmark() + if bui.app.classic is None: + logging.warning('run-media-reload-benchmark requires classic') + return + bui.app.classic.run_media_reload_benchmark() def _stress_test_pressed(self) -> None: - from ba.internal import run_stress_test + if bui.app.classic is None: + logging.warning('stress-test requires classic') + return - run_stress_test( + bui.app.classic.run_stress_test( playlist_type=self._stress_test_game_type, playlist_name=cast( - str, ba.textwidget(query=self._stress_test_playlist_name_field) + str, bui.textwidget(query=self._stress_test_playlist_name_field) ), player_count=self._stress_test_player_count, round_duration=self._stress_test_round_duration, ) - ba.containerwidget(edit=self._root_widget, transition='out_right') + bui.containerwidget(edit=self._root_widget, transition='out_right') def _done(self) -> None: # pylint: disable=cyclic-import from bastd.ui.settings.advanced import AdvancedSettingsWindow - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_right') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( AdvancedSettingsWindow(transition='in_left').get_root_widget() ) diff --git a/assets/src/ba_data/python/bastd/ui/feedback.py b/src/assets/ba_data/python/bastd/ui/feedback.py similarity index 61% rename from assets/src/ba_data/python/bastd/ui/feedback.py rename to src/assets/ba_data/python/bastd/ui/feedback.py index b640d978..e8ca493a 100644 --- a/assets/src/ba_data/python/bastd/ui/feedback.py +++ b/src/assets/ba_data/python/bastd/ui/feedback.py @@ -4,23 +4,18 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -import ba -import ba.internal - -if TYPE_CHECKING: - pass +import bauiv1 as bui -def ask_for_rating() -> ba.Widget | None: +def ask_for_rating() -> bui.Widget | None: """(internal)""" - app = ba.app - platform = app.platform - subplatform = app.subplatform + app = bui.app + assert app.classic is not None + platform = app.classic.platform + subplatform = app.classic.subplatform # FIXME: should whitelist platforms we *do* want this for. - if ba.app.test_build: + if bui.app.test_build: return None if not ( platform == 'mac' @@ -30,35 +25,36 @@ def ask_for_rating() -> ba.Widget | None: width = 700 height = 400 spacing = 40 - uiscale = ba.app.ui.uiscale - dlg = ba.containerwidget( + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + dlg = bui.containerwidget( size=(width, height), transition='in_right', scale=( 1.6 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.35 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), ) v = height - 50 v -= spacing v -= 140 - ba.imagewidget( + bui.imagewidget( parent=dlg, position=(width / 2 - 100, v + 10), size=(200, 200), - texture=ba.gettexture('cuteSpaz'), + texture=bui.gettexture('cuteSpaz'), ) - ba.textwidget( + bui.textwidget( parent=dlg, position=(15, v - 55), size=(width - 30, 30), - color=ba.app.ui.infotextcolor, - text=ba.Lstr( + color=bui.app.classic.ui.infotextcolor, + text=bui.Lstr( resource='pleaseRateText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], + subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ), maxwidth=width * 0.95, max_height=130, @@ -69,7 +65,7 @@ def ask_for_rating() -> ba.Widget | None: def do_rating() -> None: if platform == 'android': - appname = ba.internal.appname() + appname = bui.appname() if subplatform == 'google': url = f'market://details?id=net.froemling.{appname}' else: @@ -77,28 +73,28 @@ def ask_for_rating() -> ba.Widget | None: else: url = 'macappstore://itunes.apple.com/app/id416482767?ls=1&mt=12' - ba.open_url(url) - ba.containerwidget(edit=dlg, transition='out_left') + bui.open_url(url) + bui.containerwidget(edit=dlg, transition='out_left') - ba.buttonwidget( + bui.buttonwidget( parent=dlg, position=(60, 20), size=(200, 60), - label=ba.Lstr(resource='wellSureText'), + label=bui.Lstr(resource='wellSureText'), autoselect=True, on_activate_call=do_rating, ) def close() -> None: - ba.containerwidget(edit=dlg, transition='out_left') + bui.containerwidget(edit=dlg, transition='out_left') - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=dlg, position=(width - 270, 20), size=(200, 60), - label=ba.Lstr(resource='noThanksText'), + label=bui.Lstr(resource='noThanksText'), autoselect=True, on_activate_call=close, ) - ba.containerwidget(edit=dlg, cancel_button=btn, selected_child=btn) + bui.containerwidget(edit=dlg, cancel_button=btn, selected_child=btn) return dlg diff --git a/assets/src/ba_data/python/bastd/ui/fileselector.py b/src/assets/ba_data/python/bastd/ui/fileselector.py similarity index 78% rename from assets/src/ba_data/python/bastd/ui/fileselector.py rename to src/assets/ba_data/python/bastd/ui/fileselector.py index a3876d5c..5492b577 100644 --- a/assets/src/ba_data/python/bastd/ui/fileselector.py +++ b/src/assets/ba_data/python/bastd/ui/fileselector.py @@ -5,18 +5,18 @@ from __future__ import annotations import os -import threading import time +import logging +from threading import Thread from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable, Sequence -class FileSelectorWindow(ba.Window): +class FileSelectorWindow(bui.Window): """Window for selecting files.""" def __init__( @@ -29,10 +29,11 @@ class FileSelectorWindow(ba.Window): ): if valid_file_extensions is None: valid_file_extensions = [] - uiscale = ba.app.ui.uiscale - self._width = 700 if uiscale is ba.UIScale.SMALL else 600 - self._x_inset = x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 - self._height = 365 if uiscale is ba.UIScale.SMALL else 418 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self._width = 700 if uiscale is bui.UIScale.SMALL else 600 + self._x_inset = x_inset = 50 if uiscale is bui.UIScale.SMALL else 0 + self._height = 365 if uiscale is bui.UIScale.SMALL else 418 self._callback = callback self._base_path = path self._path: str | None = None @@ -42,56 +43,56 @@ class FileSelectorWindow(ba.Window): '.' + ext for ext in valid_file_extensions ] self._allow_folders = allow_folders - self._subcontainer: ba.Widget | None = None + self._subcontainer: bui.Widget | None = None self._subcontainerheight: float | None = None self._scroll_width = self._width - (80 + 2 * x_inset) self._scroll_height = self._height - 170 self._r = 'fileSelectorWindow' super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), transition='in_right', scale=( 2.23 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.4 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -35) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 42), size=(0, 0), - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, h_align='center', v_align='center', - text=ba.Lstr(resource=self._r + '.titleFolderText') + text=bui.Lstr(resource=self._r + '.titleFolderText') if (allow_folders and not valid_file_extensions) - else ba.Lstr(resource=self._r + '.titleFileText') + else bui.Lstr(resource=self._r + '.titleFileText') if not allow_folders - else ba.Lstr(resource=self._r + '.titleFileFolderText'), + else bui.Lstr(resource=self._r + '.titleFileFolderText'), maxwidth=210, ) self._button_width = 146 - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self._root_widget, position=(35 + x_inset, self._height - 67), autoselect=True, size=(self._button_width, 50), - label=ba.Lstr(resource='cancelText'), + label=bui.Lstr(resource='cancelText'), on_activate_call=self._cancel, ) - ba.widget(edit=self._cancel_button, left_widget=self._cancel_button) + bui.widget(edit=self._cancel_button, left_widget=self._cancel_button) b_color = (0.6, 0.53, 0.63) - self._back_button = ba.buttonwidget( + self._back_button = bui.buttonwidget( parent=self._root_widget, button_type='square', position=(43 + x_inset, self._height - 113), @@ -99,35 +100,35 @@ class FileSelectorWindow(ba.Window): textcolor=(0.75, 0.7, 0.8), enable_sound=False, size=(55, 35), - label=ba.charstr(ba.SpecialChar.LEFT_ARROW), + label=bui.charstr(bui.SpecialChar.LEFT_ARROW), on_activate_call=self._on_back_press, ) - self._folder_tex = ba.gettexture('folder') + self._folder_tex = bui.gettexture('folder') self._folder_color = (1.1, 0.8, 0.2) - self._file_tex = ba.gettexture('file') + self._file_tex = bui.gettexture('file') self._file_color = (1, 1, 1) - self._use_folder_button: ba.Widget | None = None + self._use_folder_button: bui.Widget | None = None self._folder_center = self._width * 0.5 + 15 - self._folder_icon = ba.imagewidget( + self._folder_icon = bui.imagewidget( parent=self._root_widget, size=(40, 40), position=(40, self._height - 117), texture=self._folder_tex, color=self._folder_color, ) - self._path_text = ba.textwidget( + self._path_text = bui.textwidget( parent=self._root_widget, position=(self._folder_center, self._height - 98), size=(0, 0), - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, h_align='center', v_align='center', text=self._path, maxwidth=self._width * 0.9, ) - self._scrollwidget: ba.Widget | None = None - ba.containerwidget( + self._scrollwidget: bui.Widget | None = None + bui.containerwidget( edit=self._root_widget, cancel_button=self._cancel_button ) self._set_path(path) @@ -137,14 +138,14 @@ class FileSelectorWindow(ba.Window): def _on_back_press(self) -> None: if len(self._recent_paths) > 1: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._recent_paths.pop() self._set_path(self._recent_paths.pop()) else: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() def _on_folder_entry_activated(self) -> None: - ba.containerwidget(edit=self._root_widget, transition='out_right') + bui.containerwidget(edit=self._root_widget, transition='out_right') if self._callback is not None: assert self._path is not None self._callback(self._path) @@ -161,25 +162,25 @@ class FileSelectorWindow(ba.Window): if new_path == '': new_path = '/' else: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() else: if self._path == '/': test_path = self._path + entry else: test_path = self._path + '/' + entry if os.path.isdir(test_path): - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() new_path = test_path elif os.path.isfile(test_path): if self._is_valid_file_path(test_path): - ba.playsound(ba.getsound('swish')) - ba.containerwidget( + bui.getsound('swish').play() + bui.containerwidget( edit=self._root_widget, transition='out_right' ) if self._callback is not None: self._callback(test_path) else: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() else: print( ( @@ -188,14 +189,14 @@ class FileSelectorWindow(ba.Window): ) ) except Exception: - ba.print_exception( + logging.exception( 'Error in FileSelectorWindow._on_entry_activated().' ) if new_path is not None: self._set_path(new_path) - class _RefreshThread(threading.Thread): + class _RefreshThread(Thread): def __init__( self, path: str, callback: Callable[[list[str], str | None], Any] ): @@ -214,16 +215,17 @@ class FileSelectorWindow(ba.Window): # has time to see the selection highlight. if duration < min_time: time.sleep(min_time - duration) - ba.pushcall( - ba.Call(self._callback, files, None), from_other_thread=True + bui.pushcall( + bui.Call(self._callback, files, None), + from_other_thread=True, ) except Exception as exc: # Ignore permission-denied. if 'Errno 13' not in str(exc): - ba.print_exception() + logging.exception('Error in fileselector refresh thread.') nofiles: list[str] = [] - ba.pushcall( - ba.Call(self._callback, nofiles, str(exc)), + bui.pushcall( + bui.Call(self._callback, nofiles, str(exc)), from_other_thread=True, ) @@ -260,13 +262,13 @@ class FileSelectorWindow(ba.Window): b_color_disabled = (0.65, 0.65, 0.65) if len(self._recent_paths) < 2: - ba.buttonwidget( + bui.buttonwidget( edit=self._back_button, color=b_color_disabled, textcolor=(0.5, 0.5, 0.5), ) else: - ba.buttonwidget( + bui.buttonwidget( edit=self._back_button, color=b_color, textcolor=(0.75, 0.7, 0.8), @@ -275,12 +277,12 @@ class FileSelectorWindow(ba.Window): max_str_width = 300.0 str_width = min( max_str_width, - ba.internal.get_string_width(folder_name, suppress_warning=True), + bui.get_string_width(folder_name, suppress_warning=True), ) - ba.textwidget( + bui.textwidget( edit=self._path_text, text=folder_name, maxwidth=max_str_width ) - ba.imagewidget( + bui.imagewidget( edit=self._folder_icon, position=( self._folder_center - str_width * 0.5 - 40, @@ -294,9 +296,9 @@ class FileSelectorWindow(ba.Window): if self._use_folder_button is not None: self._use_folder_button.delete() - ba.widget(edit=self._cancel_button, right_widget=self._back_button) + bui.widget(edit=self._cancel_button, right_widget=self._back_button) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, position=( (self._width - self._scroll_width) * 0.5, @@ -306,18 +308,18 @@ class FileSelectorWindow(ba.Window): ) if scrollwidget_selected: - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) # show error case.. if error is not None: - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._scroll_width, self._scroll_height), background=False, ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, color=(1, 1, 0, 1), text=error, @@ -348,65 +350,65 @@ class FileSelectorWindow(ba.Window): folder_entry_height if show_folder_entry else 0 ) - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._scroll_width, self._subcontainerheight), background=False, ) - ba.containerwidget( + bui.containerwidget( edit=self._scrollwidget, claims_left_right=False, claims_tab=False, ) - ba.containerwidget( + bui.containerwidget( edit=self._subcontainer, claims_left_right=False, claims_tab=False, selection_loops=False, print_list_exit_instructions=False, ) - ba.widget(edit=self._subcontainer, up_widget=self._back_button) + bui.widget(edit=self._subcontainer, up_widget=self._back_button) if show_use_folder_button: - self._use_folder_button = btn = ba.buttonwidget( + self._use_folder_button = btn = bui.buttonwidget( parent=self._root_widget, position=( self._width - self._button_width - 35 - self._x_inset, self._height - 67, ), size=(self._button_width, 50), - label=ba.Lstr( + label=bui.Lstr( resource=self._r + '.useThisFolderButtonText' ), on_activate_call=self._on_folder_entry_activated, ) - ba.widget( + bui.widget( edit=btn, left_widget=self._cancel_button, down_widget=self._scrollwidget, ) - ba.widget(edit=self._cancel_button, right_widget=btn) - ba.containerwidget(edit=self._root_widget, start_button=btn) + bui.widget(edit=self._cancel_button, right_widget=btn) + bui.containerwidget(edit=self._root_widget, start_button=btn) folder_icon_size = 35 for num, entry in enumerate(entries): - cnt = ba.containerwidget( + cnt = bui.containerwidget( parent=self._subcontainer, position=(0, v - entry_height), size=(self._scroll_width, entry_height), root_selectable=True, background=False, click_activate=True, - on_activate_call=ba.Call(self._on_entry_activated, entry), + on_activate_call=bui.Call(self._on_entry_activated, entry), ) if num == 0: - ba.widget(edit=cnt, up_widget=self._back_button) + bui.widget(edit=cnt, up_widget=self._back_button) is_valid_file_path = self._is_valid_file_path(entry) assert self._path is not None is_dir = os.path.isdir(self._path + '/' + entry) if is_dir: - ba.imagewidget( + bui.imagewidget( parent=cnt, size=(folder_icon_size, folder_icon_size), position=( @@ -418,7 +420,7 @@ class FileSelectorWindow(ba.Window): color=self._folder_color, ) else: - ba.imagewidget( + bui.imagewidget( parent=cnt, size=(folder_icon_size, folder_icon_size), position=( @@ -430,7 +432,7 @@ class FileSelectorWindow(ba.Window): texture=self._file_tex, color=self._file_color, ) - ba.textwidget( + bui.textwidget( parent=cnt, draw_controller=cnt, text=entry, @@ -451,6 +453,6 @@ class FileSelectorWindow(ba.Window): ) def _cancel(self) -> None: - ba.containerwidget(edit=self._root_widget, transition='out_right') + bui.containerwidget(edit=self._root_widget, transition='out_right') if self._callback is not None: self._callback(None) diff --git a/assets/src/ba_data/python/bastd/ui/gather/__init__.py b/src/assets/ba_data/python/bastd/ui/gather/__init__.py similarity index 71% rename from assets/src/ba_data/python/bastd/ui/gather/__init__.py rename to src/assets/ba_data/python/bastd/ui/gather/__init__.py index 42688276..a9ebc56a 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/__init__.py +++ b/src/assets/ba_data/python/bastd/ui/gather/__init__.py @@ -5,15 +5,11 @@ from __future__ import annotations import weakref +import logging from enum import Enum -from typing import TYPE_CHECKING -import ba -import ba.internal from bastd.ui.tabs import TabRow - -if TYPE_CHECKING: - pass +import bauiv1 as bui class GatherTab: @@ -27,18 +23,18 @@ class GatherTab: """The GatherWindow that this tab belongs to.""" window = self._window() if window is None: - raise ba.NotFoundError("GatherTab's window no longer exists.") + raise bui.NotFoundError("GatherTab's window no longer exists.") return window def on_activate( self, - parent_widget: ba.Widget, - tab_button: ba.Widget, + parent_widget: bui.Widget, + tab_button: bui.Widget, region_width: float, region_height: float, region_left: float, region_bottom: float, - ) -> ba.Widget: + ) -> bui.Widget: """Called when the tab becomes the active one. The tab should create and return a container widget covering the @@ -56,7 +52,7 @@ class GatherTab: """Called when the parent window is restoring state.""" -class GatherWindow(ba.Window): +class GatherWindow(bui.Window): """Window for joining/inviting friends.""" class TabID(Enum): @@ -71,7 +67,7 @@ class GatherWindow(ba.Window): def __init__( self, transition: str | None = 'in_right', - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals @@ -82,7 +78,10 @@ class GatherWindow(ba.Window): from bastd.ui.gather.publictab import PublicGatherTab from bastd.ui.gather.nearbytab import NearbyGatherTab - ba.set_analytics_screen('Gather Window') + plus = bui.app.plus + assert plus is not None + + bui.set_analytics_screen('Gather Window') scale_origin: tuple[float, float] | None if origin_widget is not None: self._transition_out = 'out_scale' @@ -91,87 +90,88 @@ class GatherWindow(ba.Window): else: self._transition_out = 'out_right' scale_origin = None - ba.app.ui.set_main_menu_location('Gather') - ba.internal.set_party_icon_always_visible(True) - uiscale = ba.app.ui.uiscale - self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 - x_offs = 100 if uiscale is ba.UIScale.SMALL else 0 + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_location('Gather') + bui.set_party_icon_always_visible(True) + uiscale = bui.app.classic.ui.uiscale + self._width = 1240 if uiscale is bui.UIScale.SMALL else 1040 + x_offs = 100 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 582 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 680 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 800 ) self._current_tab: GatherWindow.TabID | None = None - extra_top = 20 if uiscale is ba.UIScale.SMALL else 0 + extra_top = 20 if uiscale is bui.UIScale.SMALL else 0 self._r = 'gatherWindow' super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + extra_top), transition=transition, toolbar_visibility='menu_minimal', scale_origin_stack_offset=scale_origin, scale=( 1.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 0.97 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 0.8 ), stack_offset=(0, -11) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0) - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else (0, 0), ) ) - if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: - ba.containerwidget( + if uiscale is bui.UIScale.SMALL and bui.app.classic.ui.use_toolbars: + bui.containerwidget( edit=self._root_widget, on_cancel_call=self._back ) self._back_button = None else: - self._back_button = btn = ba.buttonwidget( + self._back_button = btn = bui.buttonwidget( parent=self._root_widget, position=(70 + x_offs, self._height - 74), size=(140, 60), scale=1.1, autoselect=True, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', on_activate_call=self._back, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) - ba.buttonwidget( + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.buttonwidget( edit=btn, button_type='backSmall', position=(70 + x_offs, self._height - 78), size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) - condensed = uiscale is not ba.UIScale.LARGE + condensed = uiscale is not bui.UIScale.LARGE t_offs_y = ( - 0 if not condensed else 25 if uiscale is ba.UIScale.MEDIUM else 17 + 0 if not condensed else 25 if uiscale is bui.UIScale.MEDIUM else 17 ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 42 + t_offs_y), size=(0, 0), - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, scale=( 1.5 if not condensed else 1.0 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 0.6 ), h_align='center', v_align='center', - text=ba.Lstr(resource=self._r + '.titleText'), + text=bui.Lstr(resource=self._r + '.titleText'), maxwidth=550, ) @@ -179,23 +179,24 @@ class GatherWindow(ba.Window): tab_buffer_h = (320 if condensed else 250) + 2 * x_offs # Build up the set of tabs we want. - tabdefs: list[tuple[GatherWindow.TabID, ba.Lstr]] = [ - (self.TabID.ABOUT, ba.Lstr(resource=self._r + '.aboutText')) + tabdefs: list[tuple[GatherWindow.TabID, bui.Lstr]] = [ + (self.TabID.ABOUT, bui.Lstr(resource=self._r + '.aboutText')) ] - if ba.internal.get_v1_account_misc_read_val( - 'enablePublicParties', True - ): + if plus.get_v1_account_misc_read_val('enablePublicParties', True): tabdefs.append( - (self.TabID.INTERNET, ba.Lstr(resource=self._r + '.publicText')) + ( + self.TabID.INTERNET, + bui.Lstr(resource=self._r + '.publicText'), + ) ) tabdefs.append( - (self.TabID.PRIVATE, ba.Lstr(resource=self._r + '.privateText')) + (self.TabID.PRIVATE, bui.Lstr(resource=self._r + '.privateText')) ) tabdefs.append( - (self.TabID.NEARBY, ba.Lstr(resource=self._r + '.nearbyText')) + (self.TabID.NEARBY, bui.Lstr(resource=self._r + '.nearbyText')) ) tabdefs.append( - (self.TabID.MANUAL, ba.Lstr(resource=self._r + '.manualText')) + (self.TabID.MANUAL, bui.Lstr(resource=self._r + '.manualText')) ) # On small UI, push our tabs up closer to the top of the screen to @@ -206,7 +207,7 @@ class GatherWindow(ba.Window): tabdefs, pos=(tab_buffer_h * 0.5, self._height - 130 + tabs_top_extra), size=(self._width - tab_buffer_h, 50), - on_select_call=ba.WeakCall(self._set_tab), + on_select_call=bui.WeakCall(self._set_tab), ) # Now instantiate handlers for these tabs. @@ -223,15 +224,15 @@ class GatherWindow(ba.Window): if tabtype is not None: self._tabs[tab_id] = tabtype(self) - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=self._tab_row.tabs[tabdefs[-1][0]].button, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - if uiscale is ba.UIScale.SMALL: - ba.widget( + if uiscale is bui.UIScale.SMALL: + bui.widget( edit=self._tab_row.tabs[tabdefs[0][0]].button, - left_widget=ba.internal.get_special_widget('back_button'), + left_widget=bui.get_special_widget('back_button'), ) self._scroll_width = self._width - scroll_buffer_h @@ -245,7 +246,7 @@ class GatherWindow(ba.Window): buffer_v = 4 # Not actually using a scroll widget anymore; just an image. - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, position=( self._scroll_left - buffer_h, @@ -255,24 +256,25 @@ class GatherWindow(ba.Window): self._scroll_width + 2 * buffer_h, self._scroll_height + 2 * buffer_v, ), - texture=ba.gettexture('scrollWidget'), - model_transparent=ba.getmodel('softEdgeOutside'), + texture=bui.gettexture('scrollWidget'), + mesh_transparent=bui.getmesh('softEdgeOutside'), ) - self._tab_container: ba.Widget | None = None + self._tab_container: bui.Widget | None = None self._restore_state() def __del__(self) -> None: - ba.internal.set_party_icon_always_visible(False) + bui.set_party_icon_always_visible(False) - def playlist_select(self, origin_widget: ba.Widget) -> None: + def playlist_select(self, origin_widget: bui.Widget) -> None: """Called by the private-hosting tab to select a playlist.""" from bastd.ui.play import PlayWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.selecting_private_party_playlist = True - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.selecting_private_party_playlist = True + bui.app.classic.ui.set_main_menu_window( PlayWindow(origin_widget=origin_widget).get_root_widget() ) @@ -283,7 +285,7 @@ class GatherWindow(ba.Window): self._current_tab = tab_id # We wanna preserve our current tab between runs. - cfg = ba.app.config + cfg = bui.app.config cfg['Gather Tab'] = tab_id.value cfg.commit() @@ -331,11 +333,12 @@ class GatherWindow(ba.Window): sel_name = 'TabContainer' else: raise ValueError(f'unrecognized selection: \'{sel}\'') - ba.app.ui.window_states[type(self)] = { + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = { 'sel_name': sel_name, } except Exception: - ba.print_exception(f'Error saving state for {self}.') + logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: from efro.util import enum_by_value @@ -344,12 +347,13 @@ class GatherWindow(ba.Window): for tab in self._tabs.values(): tab.restore_state() - sel: ba.Widget | None - winstate = ba.app.ui.window_states.get(type(self), {}) + sel: bui.Widget | None + assert bui.app.classic is not None + winstate = bui.app.classic.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 - gather_tab_val = ba.app.config.get('Gather Tab') + gather_tab_val = bui.app.config.get('Gather Tab') try: stored_tab = enum_by_value(self.TabID, gather_tab_val) if stored_tab in self._tab_row.tabs: @@ -371,17 +375,19 @@ class GatherWindow(ba.Window): sel = self._tab_row.tabs[sel_tab_id].button else: sel = self._tab_row.tabs[current_tab].button - ba.containerwidget(edit=self._root_widget, selected_child=sel) + bui.containerwidget(edit=self._root_widget, selected_child=sel) + except Exception: - ba.print_exception('Error restoring gather-win state.') + logging.exception('Error restoring state for %s.', self) def _back(self) -> None: from bastd.ui.mainmenu import MainMenuWindow self._save_state() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( MainMenuWindow(transition='in_left').get_root_widget() ) diff --git a/assets/src/ba_data/python/bastd/ui/gather/abouttab.py b/src/assets/ba_data/python/bastd/ui/gather/abouttab.py similarity index 77% rename from assets/src/ba_data/python/bastd/ui/gather/abouttab.py rename to src/assets/ba_data/python/bastd/ui/gather/abouttab.py index 848032aa..4104b100 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/abouttab.py +++ b/src/assets/ba_data/python/bastd/ui/gather/abouttab.py @@ -6,9 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba -import ba.internal from bastd.ui.gather import GatherTab +import bauiv1 as bui if TYPE_CHECKING: from bastd.ui.gather import GatherWindow @@ -19,40 +18,43 @@ class AboutGatherTab(GatherTab): def __init__(self, window: GatherWindow) -> None: super().__init__(window) - self._container: ba.Widget | None = None + self._container: bui.Widget | None = None def on_activate( self, - parent_widget: ba.Widget, - tab_button: ba.Widget, + parent_widget: bui.Widget, + tab_button: bui.Widget, region_width: float, region_height: float, region_left: float, region_bottom: float, - ) -> ba.Widget: + ) -> bui.Widget: + plus = bui.app.plus + assert plus is not None + party_button_label = ( 'X' - if ba.app.iircade_mode - else ba.charstr(ba.SpecialChar.TOP_BUTTON) + if bui.app.iircade_mode + else bui.charstr(bui.SpecialChar.TOP_BUTTON) ) - message = ba.Lstr( + message = bui.Lstr( resource='gatherWindow.aboutDescriptionText', subs=[ - ('${PARTY}', ba.charstr(ba.SpecialChar.PARTY_ICON)), + ('${PARTY}', bui.charstr(bui.SpecialChar.PARTY_ICON)), ('${BUTTON}', party_button_label), ], ) # Let's not talk about sharing in vr-mode; its tricky to fit more # than one head in a VR-headset ;-) - if not ba.app.vr_mode: - message = ba.Lstr( + if not bui.app.vr_mode: + message = bui.Lstr( value='${A}\n\n${B}', subs=[ ('${A}', message), ( '${B}', - ba.Lstr( + bui.Lstr( resource='gatherWindow.' 'aboutDescriptionLocalMultiplayerExtraText' ), @@ -63,12 +65,12 @@ class AboutGatherTab(GatherTab): include_invite = True msc_scale = 1.1 c_height_2 = min(region_height, string_height * msc_scale + 100) - try_tickets = ba.internal.get_v1_account_misc_read_val( + try_tickets = plus.get_v1_account_misc_read_val( 'friendTryTickets', None ) if try_tickets is None: include_invite = False - self._container = ba.containerwidget( + self._container = bui.containerwidget( parent=parent_widget, position=( region_left, @@ -78,9 +80,9 @@ class AboutGatherTab(GatherTab): background=False, selectable=include_invite, ) - ba.widget(edit=self._container, up_widget=tab_button) + bui.widget(edit=self._container, up_widget=tab_button) - ba.textwidget( + bui.textwidget( parent=self._container, position=( region_width * 0.5, @@ -97,7 +99,7 @@ class AboutGatherTab(GatherTab): ) if include_invite: - ba.textwidget( + bui.textwidget( parent=self._container, position=(region_width * 0.57, 35), color=(0, 1, 0), @@ -107,23 +109,23 @@ class AboutGatherTab(GatherTab): h_align='right', v_align='center', flatness=1.0, - text=ba.Lstr( + text=bui.Lstr( resource='gatherWindow.inviteAFriendText', subs=[('${COUNT}', str(try_tickets))], ), ) - ba.buttonwidget( + bui.buttonwidget( parent=self._container, position=(region_width * 0.59, 10), size=(230, 50), color=(0.54, 0.42, 0.56), textcolor=(0, 1, 0), - label=ba.Lstr( + label=bui.Lstr( resource='gatherWindow.inviteFriendsText', fallback_resource='gatherWindow.getFriendInviteCodeText', ), autoselect=True, - on_activate_call=ba.WeakCall(self._invite_to_try_press), + on_activate_call=bui.WeakCall(self._invite_to_try_press), up_widget=tab_button, ) return self._container @@ -132,7 +134,10 @@ class AboutGatherTab(GatherTab): from bastd.ui.account import show_sign_in_prompt from bastd.ui.appinvite import handle_app_invites_press - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return handle_app_invites_press() diff --git a/assets/src/ba_data/python/bastd/ui/gather/manualtab.py b/src/assets/ba_data/python/bastd/ui/gather/manualtab.py similarity index 66% rename from assets/src/ba_data/python/bastd/ui/gather/manualtab.py rename to src/assets/ba_data/python/bastd/ui/gather/manualtab.py index 77576c06..a61fc267 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/manualtab.py +++ b/src/assets/ba_data/python/bastd/ui/gather/manualtab.py @@ -5,31 +5,33 @@ from __future__ import annotations -import threading +import logging +from threading import Thread from typing import TYPE_CHECKING, cast from enum import Enum from dataclasses import dataclass from bastd.ui.gather import GatherTab -import ba -import ba.internal +import bauiv1 as bui +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Callable + from bastd.ui.gather import GatherWindow def _safe_set_text( - txt: ba.Widget | None, val: str | ba.Lstr, success: bool = True + txt: bui.Widget | None, val: str | bui.Lstr, success: bool = True ) -> None: if txt: - ba.textwidget( + bui.textwidget( edit=txt, text=val, color=(0, 1, 0) if success else (1, 1, 0) ) -class _HostLookupThread(threading.Thread): +class _HostLookupThread(Thread): """Thread to fetch an addr.""" def __init__( @@ -48,7 +50,7 @@ class _HostLookupThread(threading.Thread): result = socket.gethostbyname(self._name) except Exception: result = None - ba.pushcall( + bui.pushcall( lambda: self._call(result, self._port), from_other_thread=True ) @@ -72,46 +74,45 @@ class ManualGatherTab(GatherTab): def __init__(self, window: GatherWindow) -> None: super().__init__(window) - self._check_button: ba.Widget | None = None + self._check_button: bui.Widget | None = None self._doing_access_check: bool | None = None self._access_check_count: int | None = None self._sub_tab: SubTabType = SubTabType.JOIN_BY_ADDRESS - self._t_addr: ba.Widget | None = None - self._t_accessible: ba.Widget | None = None - self._t_accessible_extra: ba.Widget | None = None - self._access_check_timer: ba.Timer | None = None - self._checking_state_text: ba.Widget | None = None - self._container: ba.Widget | None = None - self._join_by_address_text: ba.Widget | None = None - self._favorites_text: ba.Widget | None = None + self._t_addr: bui.Widget | None = None + self._t_accessible: bui.Widget | None = None + self._t_accessible_extra: bui.Widget | None = None + self._access_check_timer: bui.AppTimer | None = None + self._checking_state_text: bui.Widget | None = None + self._container: bui.Widget | None = None + self._join_by_address_text: bui.Widget | None = None + self._favorites_text: bui.Widget | None = None self._width: int | None = None self._height: int | None = None self._scroll_width: int | None = None self._scroll_height: int | None = None self._favorites_scroll_width: int | None = None - self._favorites_connect_button: ba.Widget | None = None - self._scrollwidget: ba.Widget | None = None - self._columnwidget: ba.Widget | None = None + self._favorites_connect_button: bui.Widget | None = None + self._scrollwidget: bui.Widget | None = None + self._columnwidget: bui.Widget | None = None self._favorite_selected: str | None = None - self._favorite_edit_window: ba.Widget | None = None - self._party_edit_name_text: ba.Widget | None = None - self._party_edit_addr_text: ba.Widget | None = None - self._party_edit_port_text: ba.Widget | None = None + self._favorite_edit_window: bui.Widget | None = None + self._party_edit_name_text: bui.Widget | None = None + self._party_edit_addr_text: bui.Widget | None = None + self._party_edit_port_text: bui.Widget | None = None def on_activate( self, - parent_widget: ba.Widget, - tab_button: ba.Widget, + parent_widget: bui.Widget, + tab_button: bui.Widget, region_width: float, region_height: float, region_left: float, region_bottom: float, - ) -> ba.Widget: - + ) -> bui.Widget: c_width = region_width c_height = region_height - 20 - self._container = ba.containerwidget( + self._container = bui.containerwidget( parent=parent_widget, position=( region_left, @@ -122,7 +123,7 @@ class ManualGatherTab(GatherTab): selection_loops_to_parent=True, ) v = c_height - 30 - self._join_by_address_text = ba.textwidget( + self._join_by_address_text = bui.textwidget( parent=self._container, position=(c_width * 0.5 - 245, v - 13), color=(0.6, 1.0, 0.6), @@ -140,9 +141,9 @@ class ManualGatherTab(GatherTab): region_height, playsound=True, ), - text=ba.Lstr(resource='gatherWindow.manualJoinSectionText'), + text=bui.Lstr(resource='gatherWindow.manualJoinSectionText'), ) - self._favorites_text = ba.textwidget( + self._favorites_text = bui.textwidget( parent=self._container, position=(c_width * 0.5 + 45, v - 13), color=(0.6, 1.0, 0.6), @@ -160,16 +161,16 @@ class ManualGatherTab(GatherTab): region_height, playsound=True, ), - text=ba.Lstr(resource='gatherWindow.favoritesText'), + text=bui.Lstr(resource='gatherWindow.favoritesText'), ) - ba.widget(edit=self._join_by_address_text, up_widget=tab_button) - ba.widget( + bui.widget(edit=self._join_by_address_text, up_widget=tab_button) + bui.widget( edit=self._favorites_text, left_widget=self._join_by_address_text, up_widget=tab_button, ) - ba.widget(edit=tab_button, down_widget=self._favorites_text) - ba.widget( + bui.widget(edit=tab_button, down_widget=self._favorites_text) + bui.widget( edit=self._join_by_address_text, right_widget=self._favorites_text ) self._set_sub_tab(self._sub_tab, region_width, region_height) @@ -177,10 +178,14 @@ class ManualGatherTab(GatherTab): return self._container def save_state(self) -> None: - ba.app.ui.window_states[type(self)] = State(sub_tab=self._sub_tab) + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = State( + sub_tab=self._sub_tab + ) def restore_state(self) -> None: - state = ba.app.ui.window_states.get(type(self)) + assert bui.app.classic is not None + state = bui.app.classic.ui.window_states.get(type(self)) if state is None: state = State() assert isinstance(state, State) @@ -195,18 +200,18 @@ class ManualGatherTab(GatherTab): ) -> None: assert self._container if playsound: - ba.playsound(ba.getsound('click01')) + bui.getsound('click01').play() self._sub_tab = value active_color = (0.6, 1.0, 0.6) inactive_color = (0.5, 0.4, 0.5) - ba.textwidget( + bui.textwidget( edit=self._join_by_address_text, color=active_color if value is SubTabType.JOIN_BY_ADDRESS else inactive_color, ) - ba.textwidget( + bui.textwidget( edit=self._favorites_text, color=active_color if value is SubTabType.FAVORITES @@ -233,11 +238,11 @@ class ManualGatherTab(GatherTab): ) -> None: c_width = region_width c_height = region_height - 20 - last_addr = ba.app.config.get('Last Manual Party Connect Address', '') - last_port = ba.app.config.get('Last Manual Party Connect Port', 43210) + last_addr = bui.app.config.get('Last Manual Party Connect Address', '') + last_port = bui.app.config.get('Last Manual Party Connect Port', 43210) v = c_height - 70 v -= 70 - ba.textwidget( + bui.textwidget( parent=self._container, position=(c_width * 0.5 - 260 - 50, v), color=(0.6, 1.0, 0.6), @@ -246,12 +251,12 @@ class ManualGatherTab(GatherTab): maxwidth=130, h_align='right', v_align='center', - text=ba.Lstr(resource='gatherWindow.' 'manualAddressText'), + text=bui.Lstr(resource='gatherWindow.' 'manualAddressText'), ) - txt = ba.textwidget( + txt = bui.textwidget( parent=self._container, editable=True, - description=ba.Lstr(resource='gatherWindow.' 'manualAddressText'), + description=bui.Lstr(resource='gatherWindow.' 'manualAddressText'), position=(c_width * 0.5 - 240 - 50, v - 30), text=last_addr, autoselect=True, @@ -260,9 +265,9 @@ class ManualGatherTab(GatherTab): maxwidth=380, size=(420, 60), ) - ba.widget(edit=self._join_by_address_text, down_widget=txt) - ba.widget(edit=self._favorites_text, down_widget=txt) - ba.textwidget( + bui.widget(edit=self._join_by_address_text, down_widget=txt) + bui.widget(edit=self._favorites_text, down_widget=txt) + bui.textwidget( parent=self._container, position=(c_width * 0.5 - 260 + 490, v), color=(0.6, 1.0, 0.6), @@ -271,12 +276,12 @@ class ManualGatherTab(GatherTab): maxwidth=80, h_align='right', v_align='center', - text=ba.Lstr(resource='gatherWindow.' 'portText'), + text=bui.Lstr(resource='gatherWindow.' 'portText'), ) - txt2 = ba.textwidget( + txt2 = bui.textwidget( parent=self._container, editable=True, - description=ba.Lstr(resource='gatherWindow.' 'portText'), + description=bui.Lstr(resource='gatherWindow.' 'portText'), text=str(last_port), autoselect=True, max_chars=5, @@ -288,32 +293,32 @@ class ManualGatherTab(GatherTab): v -= 110 - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._container, size=(300, 70), - label=ba.Lstr(resource='gatherWindow.' 'manualConnectText'), + label=bui.Lstr(resource='gatherWindow.' 'manualConnectText'), position=(c_width * 0.5 - 300, v), autoselect=True, - on_activate_call=ba.Call(self._connect, txt, txt2), + on_activate_call=bui.Call(self._connect, txt, txt2), ) - savebutton = ba.buttonwidget( + savebutton = bui.buttonwidget( parent=self._container, size=(300, 70), - label=ba.Lstr(resource='gatherWindow.favoritesSaveText'), + label=bui.Lstr(resource='gatherWindow.favoritesSaveText'), position=(c_width * 0.5 - 240 + 490 - 200, v), autoselect=True, - on_activate_call=ba.Call(self._save_server, txt, txt2), + on_activate_call=bui.Call(self._save_server, txt, txt2), ) - ba.widget(edit=btn, right_widget=savebutton) - ba.widget(edit=savebutton, left_widget=btn, up_widget=txt2) - ba.textwidget(edit=txt, on_return_press_call=btn.activate) - ba.textwidget(edit=txt2, on_return_press_call=btn.activate) + bui.widget(edit=btn, right_widget=savebutton) + bui.widget(edit=savebutton, left_widget=btn, up_widget=txt2) + bui.textwidget(edit=txt, on_return_press_call=btn.activate) + bui.textwidget(edit=txt2, on_return_press_call=btn.activate) v -= 45 - self._check_button = ba.textwidget( + self._check_button = bui.textwidget( parent=self._container, size=(250, 60), - text=ba.Lstr(resource='gatherWindow.' 'showMyAddressText'), + text=bui.Lstr(resource='gatherWindow.' 'showMyAddressText'), v_align='center', h_align='center', click_activate=True, @@ -322,57 +327,57 @@ class ManualGatherTab(GatherTab): color=(0.5, 0.9, 0.5), scale=0.8, selectable=True, - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._on_show_my_address_button_press, v, self._container, c_width, ), ) - ba.widget(edit=self._check_button, up_widget=btn) + bui.widget(edit=self._check_button, up_widget=btn) # Tab containing saved favorite addresses def _build_favorites_tab(self, region_height: float) -> None: - c_height = region_height - 20 v = c_height - 35 - 25 - 30 - uiscale = ba.app.ui.uiscale - self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 - x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self._width = 1240 if uiscale is bui.UIScale.SMALL else 1040 + x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 578 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 670 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 800 ) self._scroll_width = self._width - 130 + 2 * x_inset self._scroll_height = self._height - 180 - x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 c_height = self._scroll_height - 20 sub_scroll_height = c_height - 63 self._favorites_scroll_width = sub_scroll_width = ( - 680 if uiscale is ba.UIScale.SMALL else 640 + 680 if uiscale is bui.UIScale.SMALL else 640 ) v = c_height - 30 - b_width = 140 if uiscale is ba.UIScale.SMALL else 178 + b_width = 140 if uiscale is bui.UIScale.SMALL else 178 b_height = ( 107 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 142 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 190 ) b_space_extra = ( 0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else -2 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else -5 ) @@ -380,69 +385,69 @@ class ManualGatherTab(GatherTab): c_height - ( 48 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 45 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 40 ) - b_height ) - self._favorites_connect_button = btn1 = ba.buttonwidget( + self._favorites_connect_button = btn1 = bui.buttonwidget( parent=self._container, size=(b_width, b_height), - position=(40 if uiscale is ba.UIScale.SMALL else 40, btnv), + position=(40 if uiscale is bui.UIScale.SMALL else 40, btnv), button_type='square', color=(0.6, 0.53, 0.63), textcolor=(0.75, 0.7, 0.8), on_activate_call=self._on_favorites_connect_press, - text_scale=1.0 if uiscale is ba.UIScale.SMALL else 1.2, - label=ba.Lstr(resource='gatherWindow.manualConnectText'), + text_scale=1.0 if uiscale is bui.UIScale.SMALL else 1.2, + label=bui.Lstr(resource='gatherWindow.manualConnectText'), autoselect=True, ) - if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: - ba.widget( + if uiscale is bui.UIScale.SMALL and bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn1, - left_widget=ba.internal.get_special_widget('back_button'), + left_widget=bui.get_special_widget('back_button'), ) btnv -= b_height + b_space_extra - ba.buttonwidget( + bui.buttonwidget( parent=self._container, size=(b_width, b_height), - position=(40 if uiscale is ba.UIScale.SMALL else 40, btnv), + position=(40 if uiscale is bui.UIScale.SMALL else 40, btnv), button_type='square', color=(0.6, 0.53, 0.63), textcolor=(0.75, 0.7, 0.8), on_activate_call=self._on_favorites_edit_press, - text_scale=1.0 if uiscale is ba.UIScale.SMALL else 1.2, - label=ba.Lstr(resource='editText'), + text_scale=1.0 if uiscale is bui.UIScale.SMALL else 1.2, + label=bui.Lstr(resource='editText'), autoselect=True, ) btnv -= b_height + b_space_extra - ba.buttonwidget( + bui.buttonwidget( parent=self._container, size=(b_width, b_height), - position=(40 if uiscale is ba.UIScale.SMALL else 40, btnv), + position=(40 if uiscale is bui.UIScale.SMALL else 40, btnv), button_type='square', color=(0.6, 0.53, 0.63), textcolor=(0.75, 0.7, 0.8), on_activate_call=self._on_favorite_delete_press, - text_scale=1.0 if uiscale is ba.UIScale.SMALL else 1.2, - label=ba.Lstr(resource='deleteText'), + text_scale=1.0 if uiscale is bui.UIScale.SMALL else 1.2, + label=bui.Lstr(resource='deleteText'), autoselect=True, ) v -= sub_scroll_height + 23 - self._scrollwidget = scrlw = ba.scrollwidget( + self._scrollwidget = scrlw = bui.scrollwidget( parent=self._container, - position=(190 if uiscale is ba.UIScale.SMALL else 225, v), + position=(190 if uiscale is bui.UIScale.SMALL else 225, v), size=(sub_scroll_width, sub_scroll_height), claims_left_right=True, ) - ba.widget( + bui.widget( edit=self._favorites_connect_button, right_widget=self._scrollwidget ) - self._columnwidget = ba.columnwidget( + self._columnwidget = bui.columnwidget( parent=scrlw, left_border=10, border=2, @@ -454,21 +459,21 @@ class ManualGatherTab(GatherTab): self._refresh_favorites() def _no_favorite_selected_error(self) -> None: - ba.screenmessage( - ba.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) + bui.screenmessage( + bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() def _on_favorites_connect_press(self) -> None: if self._favorite_selected is None: self._no_favorite_selected_error() else: - config = ba.app.config['Saved Servers'][self._favorite_selected] + config = bui.app.config['Saved Servers'][self._favorite_selected] _HostLookupThread( name=config['addr'], port=config['port'], - call=ba.WeakCall(self._host_lookup_result), + call=bui.WeakCall(self._host_lookup_result), ).start() def _on_favorites_edit_press(self) -> None: @@ -478,31 +483,32 @@ class ManualGatherTab(GatherTab): c_width = 600 c_height = 310 - uiscale = ba.app.ui.uiscale - self._favorite_edit_window = cnt = ba.containerwidget( + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self._favorite_edit_window = cnt = bui.containerwidget( scale=( 1.8 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.55 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), size=(c_width, c_height), transition='in_scale', ) - ba.textwidget( + bui.textwidget( parent=cnt, size=(0, 0), h_align='center', v_align='center', - text=ba.Lstr(resource='editText'), + text=bui.Lstr(resource='editText'), color=(0.6, 1.0, 0.6), maxwidth=c_width * 0.8, position=(c_width * 0.5, c_height - 60), ) - ba.textwidget( + bui.textwidget( parent=cnt, position=(c_width * 0.2 - 15, c_height - 120), color=(0.6, 1.0, 0.6), @@ -511,26 +517,26 @@ class ManualGatherTab(GatherTab): maxwidth=60, h_align='right', v_align='center', - text=ba.Lstr(resource='nameText'), + text=bui.Lstr(resource='nameText'), ) - self._party_edit_name_text = ba.textwidget( + self._party_edit_name_text = bui.textwidget( parent=cnt, size=(c_width * 0.7, 40), h_align='left', v_align='center', - text=ba.app.config['Saved Servers'][self._favorite_selected][ + text=bui.app.config['Saved Servers'][self._favorite_selected][ 'name' ], editable=True, - description=ba.Lstr(resource='nameText'), + description=bui.Lstr(resource='nameText'), position=(c_width * 0.2, c_height - 140), autoselect=True, maxwidth=c_width * 0.6, max_chars=200, ) - ba.textwidget( + bui.textwidget( parent=cnt, position=(c_width * 0.2 - 15, c_height - 180), color=(0.6, 1.0, 0.6), @@ -539,26 +545,26 @@ class ManualGatherTab(GatherTab): maxwidth=60, h_align='right', v_align='center', - text=ba.Lstr(resource='gatherWindow.' 'manualAddressText'), + text=bui.Lstr(resource='gatherWindow.' 'manualAddressText'), ) - self._party_edit_addr_text = ba.textwidget( + self._party_edit_addr_text = bui.textwidget( parent=cnt, size=(c_width * 0.4, 40), h_align='left', v_align='center', - text=ba.app.config['Saved Servers'][self._favorite_selected][ + text=bui.app.config['Saved Servers'][self._favorite_selected][ 'addr' ], editable=True, - description=ba.Lstr(resource='gatherWindow.manualAddressText'), + description=bui.Lstr(resource='gatherWindow.manualAddressText'), position=(c_width * 0.2, c_height - 200), autoselect=True, maxwidth=c_width * 0.35, max_chars=200, ) - ba.textwidget( + bui.textwidget( parent=cnt, position=(c_width * 0.7 - 10, c_height - 180), color=(0.6, 1.0, 0.6), @@ -567,46 +573,46 @@ class ManualGatherTab(GatherTab): maxwidth=45, h_align='right', v_align='center', - text=ba.Lstr(resource='gatherWindow.' 'portText'), + text=bui.Lstr(resource='gatherWindow.' 'portText'), ) - self._party_edit_port_text = ba.textwidget( + self._party_edit_port_text = bui.textwidget( parent=cnt, size=(c_width * 0.2, 40), h_align='left', v_align='center', text=str( - ba.app.config['Saved Servers'][self._favorite_selected]['port'] + bui.app.config['Saved Servers'][self._favorite_selected]['port'] ), editable=True, - description=ba.Lstr(resource='gatherWindow.portText'), + description=bui.Lstr(resource='gatherWindow.portText'), position=(c_width * 0.7, c_height - 200), autoselect=True, maxwidth=c_width * 0.2, max_chars=6, ) - cbtn = ba.buttonwidget( + cbtn = bui.buttonwidget( parent=cnt, - label=ba.Lstr(resource='cancelText'), - on_activate_call=ba.Call( - lambda c: ba.containerwidget(edit=c, transition='out_scale'), + label=bui.Lstr(resource='cancelText'), + on_activate_call=bui.Call( + lambda c: bui.containerwidget(edit=c, transition='out_scale'), cnt, ), size=(180, 60), position=(30, 30), autoselect=True, ) - okb = ba.buttonwidget( + okb = bui.buttonwidget( parent=cnt, - label=ba.Lstr(resource='saveText'), + label=bui.Lstr(resource='saveText'), size=(180, 60), position=(c_width - 230, 30), - on_activate_call=ba.Call(self._edit_saved_party), + on_activate_call=bui.Call(self._edit_saved_party), autoselect=True, ) - ba.widget(edit=cbtn, right_widget=okb) - ba.widget(edit=okb, left_widget=cbtn) - ba.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) + bui.widget(edit=cbtn, right_widget=okb) + bui.widget(edit=okb, left_widget=cbtn) + bui.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) def _edit_saved_party(self) -> None: server = self._favorite_selected @@ -616,26 +622,26 @@ class ManualGatherTab(GatherTab): if not self._party_edit_name_text or not self._party_edit_addr_text: return new_name_raw = cast( - str, ba.textwidget(query=self._party_edit_name_text) + str, bui.textwidget(query=self._party_edit_name_text) ) new_addr_raw = cast( - str, ba.textwidget(query=self._party_edit_addr_text) + str, bui.textwidget(query=self._party_edit_addr_text) ) new_port_raw = cast( - str, ba.textwidget(query=self._party_edit_port_text) + str, bui.textwidget(query=self._party_edit_port_text) ) - ba.app.config['Saved Servers'][server]['name'] = new_name_raw - ba.app.config['Saved Servers'][server]['addr'] = new_addr_raw + bui.app.config['Saved Servers'][server]['name'] = new_name_raw + bui.app.config['Saved Servers'][server]['addr'] = new_addr_raw try: - ba.app.config['Saved Servers'][server]['port'] = int(new_port_raw) + bui.app.config['Saved Servers'][server]['port'] = int(new_port_raw) except ValueError: # Notify about incorrect port? I'm lazy; simply leave old value. pass - ba.app.config.commit() - ba.playsound(ba.getsound('gunCocking')) + bui.app.config.commit() + bui.getsound('gunCocking').play() self._refresh_favorites() - ba.containerwidget( + bui.containerwidget( edit=self._favorite_edit_window, transition='out_scale' ) @@ -646,14 +652,14 @@ class ManualGatherTab(GatherTab): self._no_favorite_selected_error() return confirm.ConfirmWindow( - ba.Lstr( + bui.Lstr( resource='gameListWindow.deleteConfirmText', subs=[ ( '${LIST}', - ba.app.config['Saved Servers'][self._favorite_selected][ - 'name' - ], + bui.app.config['Saved Servers'][ + self._favorite_selected + ]['name'], ) ], ), @@ -666,11 +672,11 @@ class ManualGatherTab(GatherTab): if self._favorite_selected is None: self._no_favorite_selected_error() return - config = ba.app.config['Saved Servers'] + config = bui.app.config['Saved Servers'] del config[self._favorite_selected] self._favorite_selected = None - ba.app.config.commit() - ba.playsound(ba.getsound('shieldDown')) + bui.app.config.commit() + bui.getsound('shieldDown').play() self._refresh_favorites() def _on_favorite_select(self, server: str) -> None: @@ -682,7 +688,7 @@ class ManualGatherTab(GatherTab): child.delete() t_scale = 1.6 - config = ba.app.config + config = bui.app.config if 'Saved Servers' in config: servers = config['Saved Servers'] @@ -692,13 +698,13 @@ class ManualGatherTab(GatherTab): assert self._favorites_scroll_width is not None assert self._favorites_connect_button is not None for i, server in enumerate(servers): - txt = ba.textwidget( + txt = bui.textwidget( parent=self._columnwidget, size=(self._favorites_scroll_width / t_scale, 30), selectable=True, color=(1.0, 1, 0.4), always_highlight=True, - on_select_call=ba.Call(self._on_favorite_select, server), + on_select_call=bui.Call(self._on_favorite_select, server), on_activate_call=self._favorites_connect_button.activate, text=( config['Saved Servers'][server]['name'] @@ -713,20 +719,20 @@ class ManualGatherTab(GatherTab): maxwidth=(self._favorites_scroll_width / t_scale) * 0.93, ) if i == 0: - ba.widget(edit=txt, up_widget=self._favorites_text) - ba.widget( + bui.widget(edit=txt, up_widget=self._favorites_text) + bui.widget( edit=txt, left_widget=self._favorites_connect_button, right_widget=txt, ) # If there's no servers, allow selecting out of the scroll area - ba.containerwidget( + bui.containerwidget( edit=self._scrollwidget, claims_left_right=bool(servers), claims_up_down=bool(servers), ) - ba.widget( + bui.widget( edit=self._scrollwidget, up_widget=self._favorites_text, left_widget=self._favorites_connect_button, @@ -736,55 +742,55 @@ class ManualGatherTab(GatherTab): self._access_check_timer = None def _connect( - self, textwidget: ba.Widget, port_textwidget: ba.Widget + self, textwidget: bui.Widget, port_textwidget: bui.Widget ) -> None: - addr = cast(str, ba.textwidget(query=textwidget)) + addr = cast(str, bui.textwidget(query=textwidget)) if addr == '': - ba.screenmessage( - ba.Lstr(resource='internal.invalidAddressErrorText'), + bui.screenmessage( + bui.Lstr(resource='internal.invalidAddressErrorText'), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return try: - port = int(cast(str, ba.textwidget(query=port_textwidget))) + port = int(cast(str, bui.textwidget(query=port_textwidget))) except ValueError: port = -1 if port > 65535 or port < 0: - ba.screenmessage( - ba.Lstr(resource='internal.invalidPortErrorText'), + bui.screenmessage( + bui.Lstr(resource='internal.invalidPortErrorText'), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return _HostLookupThread( - name=addr, port=port, call=ba.WeakCall(self._host_lookup_result) + name=addr, port=port, call=bui.WeakCall(self._host_lookup_result) ).start() def _save_server( - self, textwidget: ba.Widget, port_textwidget: ba.Widget + self, textwidget: bui.Widget, port_textwidget: bui.Widget ) -> None: - addr = cast(str, ba.textwidget(query=textwidget)) + addr = cast(str, bui.textwidget(query=textwidget)) if addr == '': - ba.screenmessage( - ba.Lstr(resource='internal.invalidAddressErrorText'), + bui.screenmessage( + bui.Lstr(resource='internal.invalidAddressErrorText'), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return try: - port = int(cast(str, ba.textwidget(query=port_textwidget))) + port = int(cast(str, bui.textwidget(query=port_textwidget))) except ValueError: port = -1 if port > 65535 or port < 0: - ba.screenmessage( - ba.Lstr(resource='internal.invalidPortErrorText'), + bui.screenmessage( + bui.Lstr(resource='internal.invalidPortErrorText'), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return - config = ba.app.config + config = bui.app.config if addr: if not isinstance(config.get('Saved Servers'), dict): @@ -795,27 +801,27 @@ class ManualGatherTab(GatherTab): 'name': addr, } config.commit() - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() else: - ba.screenmessage('Invalid Address', color=(1, 0, 0)) - ba.playsound(ba.getsound('error')) + bui.screenmessage('Invalid Address', color=(1, 0, 0)) + bui.getsound('error').play() def _host_lookup_result( self, resolved_address: str | None, port: int ) -> None: if resolved_address is None: - ba.screenmessage( - ba.Lstr(resource='internal.unableToResolveHostText'), + bui.screenmessage( + bui.Lstr(resource='internal.unableToResolveHostText'), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() else: # Store for later. - config = ba.app.config + config = bui.app.config config['Last Manual Party Connect Address'] = resolved_address config['Last Manual Party Connect Port'] = port config.commit() - ba.internal.connect_to_party(resolved_address, port=port) + bs.connect_to_party(resolved_address, port=port) def _run_addr_fetch(self) -> None: try: @@ -826,8 +832,8 @@ class ManualGatherTab(GatherTab): sock.connect(('8.8.8.8', 80)) val = sock.getsockname()[0] sock.close() - ba.pushcall( - ba.Call( + bui.pushcall( + bui.Call( _safe_set_text, self._checking_state_text, val, @@ -838,36 +844,31 @@ class ManualGatherTab(GatherTab): from efro.error import is_udp_communication_error if is_udp_communication_error(exc): - ba.pushcall( - ba.Call( + bui.pushcall( + bui.Call( _safe_set_text, self._checking_state_text, - ba.Lstr(resource='gatherWindow.' 'noConnectionText'), + bui.Lstr(resource='gatherWindow.' 'noConnectionText'), False, ), from_other_thread=True, ) else: - ba.pushcall( - ba.Call( + bui.pushcall( + bui.Call( _safe_set_text, self._checking_state_text, - ba.Lstr( + bui.Lstr( resource='gatherWindow.' 'addressFetchErrorText' ), False, ), from_other_thread=True, ) - ba.pushcall( - ba.Call( - ba.print_error, 'error in AddrFetchThread: ' + str(exc) - ), - from_other_thread=True, - ) + logging.exception('Error in AddrFetchThread.') def _on_show_my_address_button_press( - self, v2: float, container: ba.Widget | None, c_width: float + self, v2: float, container: bui.Widget | None, c_width: float ) -> None: if not container: return @@ -875,8 +876,8 @@ class ManualGatherTab(GatherTab): tscl = 0.85 tspc = 25 - ba.playsound(ba.getsound('swish')) - ba.textwidget( + bui.getsound('swish').play() + bui.textwidget( parent=container, position=(c_width * 0.5 - 10, v2), color=(0.6, 1.0, 0.6), @@ -886,9 +887,11 @@ class ManualGatherTab(GatherTab): flatness=1.0, h_align='right', v_align='center', - text=ba.Lstr(resource='gatherWindow.' 'manualYourLocalAddressText'), + text=bui.Lstr( + resource='gatherWindow.' 'manualYourLocalAddressText' + ), ) - self._checking_state_text = ba.textwidget( + self._checking_state_text = bui.textwidget( parent=container, position=(c_width * 0.5, v2), color=(0.5, 0.5, 0.5), @@ -898,13 +901,13 @@ class ManualGatherTab(GatherTab): flatness=1.0, h_align='left', v_align='center', - text=ba.Lstr(resource='gatherWindow.' 'checkingText'), + text=bui.Lstr(resource='gatherWindow.' 'checkingText'), ) - threading.Thread(target=self._run_addr_fetch).start() + Thread(target=self._run_addr_fetch).start() v2 -= tspc - ba.textwidget( + bui.textwidget( parent=container, position=(c_width * 0.5 - 10, v2), color=(0.6, 1.0, 0.6), @@ -914,12 +917,12 @@ class ManualGatherTab(GatherTab): flatness=1.0, h_align='right', v_align='center', - text=ba.Lstr( + text=bui.Lstr( resource='gatherWindow.' 'manualYourAddressFromInternetText' ), ) - t_addr = ba.textwidget( + t_addr = bui.textwidget( parent=container, position=(c_width * 0.5, v2), color=(0.5, 0.5, 0.5), @@ -929,10 +932,10 @@ class ManualGatherTab(GatherTab): h_align='left', v_align='center', flatness=1.0, - text=ba.Lstr(resource='gatherWindow.' 'checkingText'), + text=bui.Lstr(resource='gatherWindow.' 'checkingText'), ) v2 -= tspc - ba.textwidget( + bui.textwidget( parent=container, position=(c_width * 0.5 - 10, v2), color=(0.6, 1.0, 0.6), @@ -942,12 +945,12 @@ class ManualGatherTab(GatherTab): flatness=1.0, h_align='right', v_align='center', - text=ba.Lstr( + text=bui.Lstr( resource='gatherWindow.' 'manualJoinableFromInternetText' ), ) - t_accessible = ba.textwidget( + t_accessible = bui.textwidget( parent=container, position=(c_width * 0.5, v2), color=(0.5, 0.5, 0.5), @@ -957,10 +960,10 @@ class ManualGatherTab(GatherTab): flatness=1.0, h_align='left', v_align='center', - text=ba.Lstr(resource='gatherWindow.' 'checkingText'), + text=bui.Lstr(resource='gatherWindow.' 'checkingText'), ) v2 -= 28 - t_accessible_extra = ba.textwidget( + t_accessible_extra = bui.textwidget( parent=container, position=(c_width * 0.5, v2), color=(1, 0.5, 0.2), @@ -975,16 +978,15 @@ class ManualGatherTab(GatherTab): self._doing_access_check = False self._access_check_count = 0 # Cap our refreshes eventually. - self._access_check_timer = ba.Timer( + self._access_check_timer = bui.AppTimer( 10.0, - ba.WeakCall( + bui.WeakCall( self._access_check_update, t_addr, t_accessible, t_accessible_extra, ), repeat=True, - timetype=ba.TimeType.REAL, ) # Kick initial off. @@ -994,12 +996,11 @@ class ManualGatherTab(GatherTab): def _access_check_update( self, - t_addr: ba.Widget, - t_accessible: ba.Widget, - t_accessible_extra: ba.Widget, + t_addr: bui.Widget, + t_accessible: bui.Widget, + t_accessible_extra: bui.Widget, ) -> None: - from ba.internal import master_server_get - + assert bui.app.classic is not None # If we don't have an outstanding query, start one.. assert self._doing_access_check is not None assert self._access_check_count is not None @@ -1009,10 +1010,10 @@ class ManualGatherTab(GatherTab): self._t_addr = t_addr self._t_accessible = t_accessible self._t_accessible_extra = t_accessible_extra - master_server_get( + bui.app.classic.master_server_v1_get( 'bsAccessCheck', - {'b': ba.app.build_number}, - callback=ba.WeakCall(self._on_accessible_response), + {'b': bui.app.build_number}, + callback=bui.WeakCall(self._on_accessible_response), ) def _on_accessible_response(self, data: dict[str, Any] | None) -> None: @@ -1024,52 +1025,54 @@ class ManualGatherTab(GatherTab): color_good = (0, 1, 0) if data is None or 'address' not in data or 'accessible' not in data: if t_addr: - ba.textwidget( + bui.textwidget( edit=t_addr, - text=ba.Lstr(resource='gatherWindow.' 'noConnectionText'), + text=bui.Lstr(resource='gatherWindow.' 'noConnectionText'), color=color_bad, ) if t_accessible: - ba.textwidget( + bui.textwidget( edit=t_accessible, - text=ba.Lstr(resource='gatherWindow.' 'noConnectionText'), + text=bui.Lstr(resource='gatherWindow.' 'noConnectionText'), color=color_bad, ) if t_accessible_extra: - ba.textwidget(edit=t_accessible_extra, text='', color=color_bad) + bui.textwidget( + edit=t_accessible_extra, text='', color=color_bad + ) return if t_addr: - ba.textwidget(edit=t_addr, text=data['address'], color=color_good) + bui.textwidget(edit=t_addr, text=data['address'], color=color_good) if t_accessible: if data['accessible']: - ba.textwidget( + bui.textwidget( edit=t_accessible, - text=ba.Lstr( + text=bui.Lstr( resource='gatherWindow.' 'manualJoinableYesText' ), color=color_good, ) if t_accessible_extra: - ba.textwidget( + bui.textwidget( edit=t_accessible_extra, text='', color=color_good ) else: - ba.textwidget( + bui.textwidget( edit=t_accessible, - text=ba.Lstr( + text=bui.Lstr( resource='gatherWindow.' 'manualJoinableNoWithAsteriskText' ), color=color_bad, ) if t_accessible_extra: - ba.textwidget( + bui.textwidget( edit=t_accessible_extra, - text=ba.Lstr( + text=bui.Lstr( resource='gatherWindow.' 'manualRouterForwardingText', subs=[ - ('${PORT}', str(ba.internal.get_game_port())), + ('${PORT}', str(bs.get_game_port())), ], ), color=color_bad, diff --git a/assets/src/ba_data/python/bastd/ui/gather/nearbytab.py b/src/assets/ba_data/python/bastd/ui/gather/nearbytab.py similarity index 73% rename from assets/src/ba_data/python/bastd/ui/gather/nearbytab.py rename to src/assets/ba_data/python/bastd/ui/gather/nearbytab.py index 16f68b39..c9e3f87b 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/nearbytab.py +++ b/src/assets/ba_data/python/bastd/ui/gather/nearbytab.py @@ -7,54 +7,52 @@ from __future__ import annotations import weakref from typing import TYPE_CHECKING -import ba -import ba.internal from bastd.ui.gather import GatherTab +import bauiv1 as bui +import bascenev1 as bs if TYPE_CHECKING: from typing import Any + from bastd.ui.gather import GatherWindow class NetScanner: - """Class for scanning for games on the lan.""" + """Class for scanning for nearby games (lan, bluetooth, etc).""" def __init__( self, tab: GatherTab, - scrollwidget: ba.Widget, - tab_button: ba.Widget, + scrollwidget: bui.Widget, + tab_button: bui.Widget, width: float, ): self._tab = weakref.ref(tab) self._scrollwidget = scrollwidget self._tab_button = tab_button - self._columnwidget = ba.columnwidget( + self._columnwidget = bui.columnwidget( parent=self._scrollwidget, border=2, margin=0, left_border=10 ) - ba.widget(edit=self._columnwidget, up_widget=tab_button) + bui.widget(edit=self._columnwidget, up_widget=tab_button) self._width = width self._last_selected_host: dict[str, Any] | None = None - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self.update), - timetype=ba.TimeType.REAL, - repeat=True, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self.update), repeat=True ) # Go ahead and run a few *almost* immediately so we don't # have to wait a second. self.update() - ba.timer(0.25, ba.WeakCall(self.update), timetype=ba.TimeType.REAL) + bui.apptimer(0.25, bui.WeakCall(self.update)) def __del__(self) -> None: - ba.internal.end_host_scanning() + bs.end_host_scanning() def _on_select(self, host: dict[str, Any]) -> None: self._last_selected_host = host def _on_activate(self, host: dict[str, Any]) -> None: - ba.internal.connect_to_party(host['address']) + bs.connect_to_party(host['address']) def update(self) -> None: """(internal)""" @@ -62,8 +60,7 @@ class NetScanner: # In case our UI was killed from under us. if not self._columnwidget: print( - f'ERROR: NetScanner running without UI at time' - f' {ba.time(timetype=ba.TimeType.REAL)}.' + f'ERROR: NetScanner running without UI at time {bui.apptime()}.' ) return @@ -73,15 +70,15 @@ class NetScanner: # Grab this now this since adding widgets will change it. last_selected_host = self._last_selected_host - hosts = ba.internal.host_scan_cycle() + hosts = bs.host_scan_cycle() for i, host in enumerate(hosts): - txt3 = ba.textwidget( + txt3 = bui.textwidget( parent=self._columnwidget, size=(self._width / t_scale, 30), selectable=True, color=(1, 1, 1), - on_select_call=ba.Call(self._on_select, host), - on_activate_call=ba.Call(self._on_activate, host), + on_select_call=bui.Call(self._on_select, host), + on_activate_call=bui.Call(self._on_activate, host), click_activate=True, text=host['display_string'], h_align='left', @@ -90,13 +87,13 @@ class NetScanner: maxwidth=(self._width / t_scale) * 0.93, ) if host == last_selected_host: - ba.containerwidget( + bui.containerwidget( edit=self._columnwidget, selected_child=txt3, visible_child=txt3, ) if i == 0: - ba.widget(edit=txt3, up_widget=self._tab_button) + bui.widget(edit=txt3, up_widget=self._tab_button) class NearbyGatherTab(GatherTab): @@ -105,22 +102,22 @@ class NearbyGatherTab(GatherTab): def __init__(self, window: GatherWindow) -> None: super().__init__(window) self._net_scanner: NetScanner | None = None - self._container: ba.Widget | None = None + self._container: bui.Widget | None = None def on_activate( self, - parent_widget: ba.Widget, - tab_button: ba.Widget, + parent_widget: bui.Widget, + tab_button: bui.Widget, region_width: float, region_height: float, region_left: float, region_bottom: float, - ) -> ba.Widget: + ) -> bui.Widget: c_width = region_width c_height = region_height - 20 sub_scroll_height = c_height - 85 sub_scroll_width = 650 - self._container = ba.containerwidget( + self._container = bui.containerwidget( parent=parent_widget, position=( region_left, @@ -131,7 +128,7 @@ class NearbyGatherTab(GatherTab): selection_loops_to_parent=True, ) v = c_height - 30 - ba.textwidget( + bui.textwidget( parent=self._container, position=(c_width * 0.5, v - 3), color=(0.6, 1.0, 0.6), @@ -140,13 +137,13 @@ class NearbyGatherTab(GatherTab): maxwidth=c_width * 0.9, h_align='center', v_align='center', - text=ba.Lstr( + text=bui.Lstr( resource='gatherWindow.' 'localNetworkDescriptionText' ), ) v -= 15 v -= sub_scroll_height + 23 - scrollw = ba.scrollwidget( + scrollw = bui.scrollwidget( parent=self._container, position=((region_width - sub_scroll_width) * 0.5, v), size=(sub_scroll_width, sub_scroll_height), @@ -156,7 +153,7 @@ class NearbyGatherTab(GatherTab): self, scrollw, tab_button, width=sub_scroll_width ) - ba.widget(edit=scrollw, autoselect=True, up_widget=tab_button) + bui.widget(edit=scrollw, autoselect=True, up_widget=tab_button) return self._container def on_deactivate(self) -> None: diff --git a/assets/src/ba_data/python/bastd/ui/gather/privatetab.py b/src/assets/ba_data/python/bastd/ui/gather/privatetab.py similarity index 79% rename from assets/src/ba_data/python/bastd/ui/gather/privatetab.py rename to src/assets/ba_data/python/bastd/ui/gather/privatetab.py index 88ed22b7..227c11f2 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/privatetab.py +++ b/src/assets/ba_data/python/bastd/ui/gather/privatetab.py @@ -7,6 +7,7 @@ from __future__ import annotations import os import copy import time +import logging from enum import Enum from dataclasses import dataclass from typing import TYPE_CHECKING, cast @@ -17,15 +18,17 @@ from bacommon.net import ( PrivateHostingConfig, PrivatePartyConnectResult, ) -import ba -import ba.internal from bastd.ui.gather import GatherTab -from bastd.ui import getcurrency +from bastd.ui.getcurrency import GetCurrencyWindow, show_get_tickets_prompt +import bascenev1 as bs +import bauiv1 as bui if TYPE_CHECKING: from typing import Any + from bastd.ui.gather import GatherWindow + # Print a bit of info about queries, etc. DEBUG_SERVER_COMMUNICATION = os.environ.get('BA_DEBUG_PPTABCOM') == '1' @@ -49,24 +52,24 @@ class PrivateGatherTab(GatherTab): def __init__(self, window: GatherWindow) -> None: super().__init__(window) - self._container: ba.Widget | None = None + self._container: bui.Widget | None = None self._state: State = State() self._hostingstate = PrivateHostingState() - self._join_sub_tab_text: ba.Widget | None = None - self._host_sub_tab_text: ba.Widget | None = None - self._update_timer: ba.Timer | None = None - self._join_party_code_text: ba.Widget | None = None + self._join_sub_tab_text: bui.Widget | None = None + self._host_sub_tab_text: bui.Widget | None = None + self._update_timer: bui.AppTimer | None = None + self._join_party_code_text: bui.Widget | None = None self._c_width: float = 0.0 self._c_height: float = 0.0 self._last_hosting_state_query_time: float | None = None self._waiting_for_initial_state = True self._waiting_for_start_stop_response = True - self._host_playlist_button: ba.Widget | None = None - self._host_copy_button: ba.Widget | None = None - self._host_connect_button: ba.Widget | None = None - self._host_start_stop_button: ba.Widget | None = None - self._get_tickets_button: ba.Widget | None = None - self._ticket_count_text: ba.Widget | None = None + self._host_playlist_button: bui.Widget | None = None + self._host_copy_button: bui.Widget | None = None + self._host_connect_button: bui.Widget | None = None + self._host_start_stop_button: bui.Widget | None = None + self._get_tickets_button: bui.Widget | None = None + self._ticket_count_text: bui.Widget | None = None self._showing_not_signed_in_screen = False self._create_time = time.time() self._last_action_send_time: float | None = None @@ -74,21 +77,21 @@ class PrivateGatherTab(GatherTab): try: self._hostingconfig = self._build_hosting_config() except Exception: - ba.print_exception('Error building hosting config') + logging.exception('Error building hosting config.') self._hostingconfig = PrivateHostingConfig() def on_activate( self, - parent_widget: ba.Widget, - tab_button: ba.Widget, + parent_widget: bui.Widget, + tab_button: bui.Widget, region_width: float, region_height: float, region_left: float, region_bottom: float, - ) -> ba.Widget: + ) -> bui.Widget: self._c_width = region_width self._c_height = region_height - 20 - self._container = ba.containerwidget( + self._container = bui.containerwidget( parent=parent_widget, position=( region_left, @@ -99,7 +102,7 @@ class PrivateGatherTab(GatherTab): selection_loops_to_parent=True, ) v = self._c_height - 30.0 - self._join_sub_tab_text = ba.textwidget( + self._join_sub_tab_text = bui.textwidget( parent=self._container, position=(self._c_width * 0.5 - 245, v - 13), color=(0.6, 1.0, 0.6), @@ -115,9 +118,9 @@ class PrivateGatherTab(GatherTab): SubTabType.JOIN, playsound=True, ), - text=ba.Lstr(resource='gatherWindow.privatePartyJoinText'), + text=bui.Lstr(resource='gatherWindow.privatePartyJoinText'), ) - self._host_sub_tab_text = ba.textwidget( + self._host_sub_tab_text = bui.textwidget( parent=self._container, position=(self._c_width * 0.5 + 45, v - 13), color=(0.6, 1.0, 0.6), @@ -133,23 +136,20 @@ class PrivateGatherTab(GatherTab): SubTabType.HOST, playsound=True, ), - text=ba.Lstr(resource='gatherWindow.privatePartyHostText'), + text=bui.Lstr(resource='gatherWindow.privatePartyHostText'), ) - ba.widget(edit=self._join_sub_tab_text, up_widget=tab_button) - ba.widget( + bui.widget(edit=self._join_sub_tab_text, up_widget=tab_button) + bui.widget( edit=self._host_sub_tab_text, left_widget=self._join_sub_tab_text, up_widget=tab_button, ) - ba.widget( + bui.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, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) # Prevent taking any action until we've updated our state. @@ -166,26 +166,27 @@ class PrivateGatherTab(GatherTab): def _build_hosting_config(self) -> PrivateHostingConfig: # pylint: disable=too-many-branches + # pylint: disable=too-many-locals from bastd.ui.playlist import PlaylistTypeVars - from ba.internal import filter_playlist + from bascenev1.internal import filter_playlist hcfg = PrivateHostingConfig() - cfg = ba.app.config + cfg = bui.app.config sessiontypestr = cfg.get('Private Party Host Session Type', 'ffa') if not isinstance(sessiontypestr, str): raise RuntimeError(f'Invalid sessiontype {sessiontypestr}') hcfg.session_type = sessiontypestr - sessiontype: type[ba.Session] + sessiontype: type[bs.Session] if hcfg.session_type == 'ffa': - sessiontype = ba.FreeForAllSession + sessiontype = bs.FreeForAllSession elif hcfg.session_type == 'teams': - sessiontype = ba.DualTeamSession + sessiontype = bs.DualTeamSession else: raise RuntimeError(f'Invalid sessiontype: {hcfg.session_type}') pvars = PlaylistTypeVars(sessiontype) - playlist_name = ba.app.config.get( + playlist_name = bui.app.config.get( f'{pvars.config_name} Playlist Selection' ) if not isinstance(playlist_name, str): @@ -221,8 +222,9 @@ class PrivateGatherTab(GatherTab): if hcfg.session_type == 'teams': ctn: list[str] | None = cfg.get('Custom Team Names') if ctn is not None: + ctn_any: Any = ctn # Actual value may not match type checker. if ( - isinstance(ctn, (list, tuple)) + isinstance(ctn_any, (list, tuple)) and len(ctn) == 2 and all(isinstance(x, str) for x in ctn) ): @@ -232,8 +234,9 @@ class PrivateGatherTab(GatherTab): ctc: list[list[float]] | None = cfg.get('Custom Team Colors') if ctc is not None: + ctc_any: Any = ctc # Actual value may not match type checker. if ( - isinstance(ctc, (list, tuple)) + isinstance(ctc_any, (list, tuple)) and len(ctc) == 2 and all(isinstance(x, (list, tuple)) for x in ctc) and all(len(x) == 3 for x in ctc) @@ -252,55 +255,59 @@ class PrivateGatherTab(GatherTab): def _update_currency_ui(self) -> None: # Keep currency count up to date if applicable. + plus = bui.app.plus + assert plus is not None + try: - t_str = str(ba.internal.get_v1_account_ticket_count()) + t_str = str(plus.get_v1_account_ticket_count()) except Exception: t_str = '?' if self._get_tickets_button: - ba.buttonwidget( + bui.buttonwidget( edit=self._get_tickets_button, - label=ba.charstr(ba.SpecialChar.TICKET) + t_str, + label=bui.charstr(bui.SpecialChar.TICKET) + t_str, ) if self._ticket_count_text: - ba.textwidget( + bui.textwidget( edit=self._ticket_count_text, - text=ba.charstr(ba.SpecialChar.TICKET) + t_str, + text=bui.charstr(bui.SpecialChar.TICKET) + t_str, ) def _update(self) -> None: """Periodic updating.""" - now = ba.time(ba.TimeType.REAL) + plus = bui.app.plus + assert plus is not None + + now = bui.apptime() 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.internal.get_v1_account_state() != 'signed_in' + plus.get_v1_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.internal.get_v1_account_state() == 'signed_in': - ba.internal.add_transaction( + if plus.get_v1_account_state() == 'signed_in': + plus.add_v1_account_transaction( { 'type': 'PRIVATE_PARTY_QUERY', 'expire_time': time.time() + 20, }, - callback=ba.WeakCall( + callback=bui.WeakCall( self._hosting_state_idle_response ), ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() else: self._hosting_state_idle_response(None) self._last_hosting_state_query_time = now @@ -308,7 +315,6 @@ class PrivateGatherTab(GatherTab): def _hosting_state_idle_response( self, result: dict[str, Any] | None ) -> 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 @@ -326,7 +332,6 @@ class PrivateGatherTab(GatherTab): self._hosting_state_response(result) def _hosting_state_response(self, result: dict[str, Any] | None) -> None: - # Its possible for this to come back to us after our UI is dead; # ignore in that case. if not self._container: @@ -340,7 +345,7 @@ class PrivateGatherTab(GatherTab): PrivateHostingState, result, discard_unknown_attrs=True ) except Exception: - ba.print_exception('Got invalid PrivateHostingState data') + logging.exception('Got invalid PrivateHostingState data') else: self._debug_server_comm('private party state response errored') @@ -357,7 +362,7 @@ class PrivateGatherTab(GatherTab): def _set_sub_tab(self, value: SubTabType, playsound: bool = False) -> None: assert self._container if playsound: - ba.playsound(ba.getsound('click01')) + bui.getsound('click01').play() # If switching from join to host, do a fresh state query. if self._state.sub_tab is SubTabType.JOIN and value is SubTabType.HOST: @@ -372,11 +377,11 @@ class PrivateGatherTab(GatherTab): self._state.sub_tab = value active_color = (0.6, 1.0, 0.6) inactive_color = (0.5, 0.4, 0.5) - ba.textwidget( + bui.textwidget( edit=self._join_sub_tab_text, color=active_color if value is SubTabType.JOIN else inactive_color, ) - ba.textwidget( + bui.textwidget( edit=self._host_sub_tab_text, color=active_color if value is SubTabType.HOST else inactive_color, ) @@ -384,9 +389,9 @@ class PrivateGatherTab(GatherTab): self._refresh_sub_tab() # Kick off an update to get any needed messages sent/etc. - ba.pushcall(self._update) + bui.pushcall(self._update) - def _selwidgets(self) -> list[ba.Widget | None]: + def _selwidgets(self) -> list[bui.Widget | None]: """An indexed list of widgets we can use for saving/restoring sel.""" return [ self._host_playlist_button, @@ -428,13 +433,12 @@ class PrivateGatherTab(GatherTab): if selindex is not None: selwidget = self._selwidgets()[selindex] if selwidget: - ba.containerwidget( + bui.containerwidget( edit=self._container, selected_child=selwidget ) def _build_join_tab(self) -> None: - - ba.textwidget( + bui.textwidget( parent=self._container, position=(self._c_width * 0.5, self._c_height - 140), color=(0.5, 0.46, 0.5), @@ -443,53 +447,54 @@ class PrivateGatherTab(GatherTab): maxwidth=250, h_align='center', v_align='center', - text=ba.Lstr(resource='gatherWindow.partyCodeText'), + text=bui.Lstr(resource='gatherWindow.partyCodeText'), ) - self._join_party_code_text = ba.textwidget( + self._join_party_code_text = bui.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'), + description=bui.Lstr(resource='gatherWindow.partyCodeText'), autoselect=True, maxwidth=250, h_align='left', v_align='center', text='', ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._container, size=(300, 70), - label=ba.Lstr(resource='gatherWindow.' 'manualConnectText'), + label=bui.Lstr(resource='gatherWindow.' 'manualConnectText'), position=(self._c_width * 0.5 - 150, self._c_height - 350), on_activate_call=self._join_connect_press, autoselect=True, ) - ba.textwidget( + bui.textwidget( edit=self._join_party_code_text, on_return_press_call=btn.activate ) def _on_get_tickets_press(self) -> None: - if self._waiting_for_start_stop_response: 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 - ) + GetCurrencyWindow(modal=True, origin_widget=self._get_tickets_button) def _build_host_tab(self) -> None: # pylint: disable=too-many-branches # pylint: disable=too-many-statements + assert bui.app.classic is not None + + plus = bui.app.plus + assert plus is not None hostingstate = self._hostingstate - if ba.internal.get_v1_account_state() != 'signed_in': - ba.textwidget( + if plus.get_v1_account_state() != 'signed_in': + bui.textwidget( parent=self._container, size=(0, 0), h_align='center', @@ -498,7 +503,7 @@ class PrivateGatherTab(GatherTab): 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'), + text=bui.Lstr(resource='notSignedInErrorText'), ) self._showing_not_signed_in_screen = True return @@ -511,7 +516,7 @@ class PrivateGatherTab(GatherTab): # playlists/etc without having to wait for the server each time # back to the ui. if self._waiting_for_initial_state and bool(False): - ba.textwidget( + bui.textwidget( parent=self._container, size=(0, 0), h_align='center', @@ -520,9 +525,9 @@ class PrivateGatherTab(GatherTab): scale=0.8, color=(0.6, 0.56, 0.6), position=(self._c_width * 0.5, self._c_height * 0.5), - text=ba.Lstr( + text=bui.Lstr( value='${A}...', - subs=[('${A}', ba.Lstr(resource='store.loadingText'))], + subs=[('${A}', bui.Lstr(resource='store.loadingText'))], ), ) return @@ -534,9 +539,9 @@ class PrivateGatherTab(GatherTab): and hostingstate.party_code is None and 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( + if not bui.app.classic.ui.use_toolbars: + if bui.app.classic.allow_ticket_purchases: + self._get_tickets_button = bui.buttonwidget( parent=self._container, position=( self._c_width - 210 + 125, @@ -546,12 +551,12 @@ class PrivateGatherTab(GatherTab): scale=0.6, size=(120, 60), textcolor=(0.2, 1, 0.2), - label=ba.charstr(ba.SpecialChar.TICKET), + label=bui.charstr(bui.SpecialChar.TICKET), color=(0.65, 0.5, 0.8), on_activate_call=self._on_get_tickets_press, ) else: - self._ticket_count_text = ba.textwidget( + self._ticket_count_text = bui.textwidget( parent=self._container, scale=0.6, position=( @@ -567,7 +572,7 @@ class PrivateGatherTab(GatherTab): v = self._c_height - 90 if hostingstate.party_code is None: - ba.textwidget( + bui.textwidget( parent=self._container, size=(0, 0), h_align='center', @@ -577,7 +582,7 @@ class PrivateGatherTab(GatherTab): flatness=1.0, color=(0.5, 0.46, 0.5), position=(self._c_width * 0.5, v), - text=ba.Lstr( + text=bui.Lstr( resource='gatherWindow.privatePartyCloudDescriptionText' ), ) @@ -585,7 +590,7 @@ class PrivateGatherTab(GatherTab): v -= 100 if hostingstate.party_code is None: # We've got no current party running; show options to set one up. - ba.textwidget( + bui.textwidget( parent=self._container, size=(0, 0), h_align='right', @@ -594,9 +599,9 @@ class PrivateGatherTab(GatherTab): scale=0.8, color=(0.6, 0.56, 0.6), position=(self._c_width * 0.5 - 210, v), - text=ba.Lstr(resource='playlistText'), + text=bui.Lstr(resource='playlistText'), ) - self._host_playlist_button = ba.buttonwidget( + self._host_playlist_button = bui.buttonwidget( parent=self._container, size=(400, 70), color=(0.6, 0.5, 0.6), @@ -610,15 +615,15 @@ class PrivateGatherTab(GatherTab): # If it appears we're coming back from playlist selection, # re-select our playlist button. - if ba.app.ui.selecting_private_party_playlist: - ba.containerwidget( + if bui.app.classic.ui.selecting_private_party_playlist: + bui.containerwidget( edit=self._container, selected_child=self._host_playlist_button, ) - ba.app.ui.selecting_private_party_playlist = False + bui.app.classic.ui.selecting_private_party_playlist = False else: # We've got a current party; show its info. - ba.textwidget( + bui.textwidget( parent=self._container, size=(0, 0), h_align='center', @@ -627,9 +632,9 @@ class PrivateGatherTab(GatherTab): scale=0.9, color=(0.7, 0.64, 0.7), position=(self._c_width * 0.5, v + 90), - text=ba.Lstr(resource='gatherWindow.partyServerRunningText'), + text=bui.Lstr(resource='gatherWindow.partyServerRunningText'), ) - ba.textwidget( + bui.textwidget( parent=self._container, size=(0, 0), h_align='center', @@ -638,9 +643,9 @@ class PrivateGatherTab(GatherTab): scale=0.7, color=(0.7, 0.64, 0.7), position=(self._c_width * 0.5, v + 50), - text=ba.Lstr(resource='gatherWindow.partyCodeText'), + text=bui.Lstr(resource='gatherWindow.partyCodeText'), ) - ba.textwidget( + bui.textwidget( parent=self._container, size=(0, 0), h_align='center', @@ -652,26 +657,26 @@ class PrivateGatherTab(GatherTab): ) # Also action buttons to copy it and connect to it. - if ba.clipboard_is_supported(): + if bui.clipboard_is_supported(): cbtnoffs = 10 - self._host_copy_button = ba.buttonwidget( + self._host_copy_button = bui.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'), + label=bui.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( + self._host_connect_button = bui.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'), + label=bui.Lstr(resource='gatherWindow.manualConnectText'), on_activate_call=self._host_connect_press, position=(self._c_width * 0.5 + cbtnoffs, v - 70), autoselect=True, @@ -686,7 +691,7 @@ class PrivateGatherTab(GatherTab): pass elif hostingstate.unavailable_error is not None: # If hosting is unavailable, show the associated reason. - ba.textwidget( + bui.textwidget( parent=self._container, size=(0, 0), h_align='center', @@ -696,7 +701,7 @@ class PrivateGatherTab(GatherTab): flatness=1.0, color=(1.0, 0.0, 0.0), position=(self._c_width * 0.5, v), - text=ba.Lstr( + text=bui.Lstr( translate=( 'serverResponses', hostingstate.unavailable_error, @@ -705,7 +710,7 @@ class PrivateGatherTab(GatherTab): ) elif hostingstate.free_host_minutes_remaining is not None: # If we've been pre-approved to start/stop for free, show that. - ba.textwidget( + bui.textwidget( parent=self._container, size=(0, 0), h_align='center', @@ -719,7 +724,7 @@ class PrivateGatherTab(GatherTab): else (0.0, 1.0, 0.0) ), position=(self._c_width * 0.5, v), - text=ba.Lstr( + text=bui.Lstr( resource='gatherWindow.startStopHostingMinutesText', subs=[ ( @@ -734,7 +739,7 @@ class PrivateGatherTab(GatherTab): # or will be at some point. if hostingstate.party_code is None: if hostingstate.tickets_to_host_now == 0: - ba.textwidget( + bui.textwidget( parent=self._container, size=(0, 0), h_align='center', @@ -744,7 +749,7 @@ class PrivateGatherTab(GatherTab): flatness=1.0, color=(0.0, 1.0, 0.0), position=(self._c_width * 0.5, v), - text=ba.Lstr( + text=bui.Lstr( resource=( 'gatherWindow.freeCloudServerAvailableNowText' ) @@ -752,7 +757,7 @@ class PrivateGatherTab(GatherTab): ) else: if hostingstate.minutes_until_free_host is None: - ba.textwidget( + bui.textwidget( parent=self._container, size=(0, 0), h_align='center', @@ -762,7 +767,7 @@ class PrivateGatherTab(GatherTab): flatness=1.0, color=(1.0, 0.6, 0.0), position=(self._c_width * 0.5, v), - text=ba.Lstr( + text=bui.Lstr( resource=( 'gatherWindow' '.freeCloudServerNotAvailableText' @@ -771,7 +776,7 @@ class PrivateGatherTab(GatherTab): ) else: availmins = hostingstate.minutes_until_free_host - ba.textwidget( + bui.textwidget( parent=self._container, size=(0, 0), h_align='center', @@ -781,7 +786,7 @@ class PrivateGatherTab(GatherTab): flatness=1.0, color=(1.0, 0.6, 0.0), position=(self._c_width * 0.5, v), - text=ba.Lstr( + text=bui.Lstr( resource='gatherWindow.' 'freeCloudServerAvailableMinutesText', subs=[('${MINUTES}', f'{availmins:.0f}')], @@ -794,31 +799,33 @@ class PrivateGatherTab(GatherTab): self._waiting_for_start_stop_response or self._waiting_for_initial_state ): - btnlabel = ba.Lstr(resource='oneMomentText') + btnlabel = bui.Lstr(resource='oneMomentText') else: if hostingstate.unavailable_error is not None: - btnlabel = ba.Lstr( + btnlabel = bui.Lstr( resource='gatherWindow.hostingUnavailableText' ) elif hostingstate.party_code is None: - ticon = ba.internal.charstr(ba.SpecialChar.TICKET) + ticon = bui.charstr(bui.SpecialChar.TICKET) nowtickets = hostingstate.tickets_to_host_now if nowtickets > 0: - btnlabel = ba.Lstr( + btnlabel = bui.Lstr( resource='gatherWindow.startHostingPaidText', subs=[('${COST}', f'{ticon}{nowtickets}')], ) else: - btnlabel = ba.Lstr(resource='gatherWindow.startHostingText') + btnlabel = bui.Lstr( + resource='gatherWindow.startHostingText' + ) else: - btnlabel = ba.Lstr(resource='gatherWindow.stopHostingText') + btnlabel = bui.Lstr(resource='gatherWindow.stopHostingText') disabled = ( hostingstate.unavailable_error is not None or self._waiting_for_initial_state ) waiting = self._waiting_for_start_stop_response - self._host_start_stop_button = ba.buttonwidget( + self._host_start_stop_button = bui.buttonwidget( parent=self._container, size=(400, 80), color=( @@ -842,8 +849,8 @@ class PrivateGatherTab(GatherTab): 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')) + bui.clipboard_set_text(self._hostingstate.party_code) + bui.screenmessage(bui.Lstr(resource='gatherWindow.copyCodeConfirmText')) def _host_connect_press(self) -> None: assert self._hostingstate.party_code is not None @@ -857,9 +864,11 @@ class PrivateGatherTab(GatherTab): ) def _connect_to_party_code(self, code: str) -> None: - # Ignore attempted followup sends for a few seconds. # (this will reset if we get a response) + plus = bui.app.plus + assert plus is not None + now = time.time() if ( self._connect_press_time is not None @@ -872,90 +881,90 @@ class PrivateGatherTab(GatherTab): self._connect_press_time = now self._debug_server_comm('sending private party connect') - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'PRIVATE_PARTY_CONNECT', 'expire_time': time.time() + 20, 'code': code, }, - callback=ba.WeakCall(self._connect_response), + callback=bui.WeakCall(self._connect_response), ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() def _start_stop_button_press(self) -> None: + plus = bui.app.plus + assert plus is not None if ( self._waiting_for_start_stop_response or self._waiting_for_initial_state ): return - if ba.internal.get_v1_account_state() != 'signed_in': - ba.screenmessage(ba.Lstr(resource='notSignedInErrorText')) - ba.playsound(ba.getsound('error')) + if plus.get_v1_account_state() != 'signed_in': + bui.screenmessage(bui.Lstr(resource='notSignedInErrorText')) + bui.getsound('error').play() self._refresh_sub_tab() return if self._hostingstate.unavailable_error is not None: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return - ba.playsound(ba.getsound('click01')) + bui.getsound('click01').play() # 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: int | None try: - ticket_count = ba.internal.get_v1_account_ticket_count() + ticket_count = plus.get_v1_account_ticket_count() except Exception: - # FIXME: should add a ba.NotSignedInError we can use here. + # FIXME: should add a bui.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')) + show_get_tickets_prompt() + bui.getsound('error').play() return self._last_action_send_time = time.time() - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'PRIVATE_PARTY_START', 'config': dataclass_to_dict(self._hostingconfig), - 'region_pings': ba.app.net.zone_pings, + 'region_pings': bui.app.net.zone_pings, 'expire_time': time.time() + 20, }, - callback=ba.WeakCall(self._hosting_state_response), + callback=bui.WeakCall(self._hosting_state_response), ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() else: self._last_action_send_time = time.time() - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'PRIVATE_PARTY_STOP', 'expire_time': time.time() + 20, }, - callback=ba.WeakCall(self._hosting_state_response), + callback=bui.WeakCall(self._hosting_state_response), ) - ba.internal.run_transactions() - ba.playsound(ba.getsound('click01')) + plus.run_v1_account_transactions() + bui.getsound('click01').play() self._waiting_for_start_stop_response = True self._refresh_sub_tab() def _join_connect_press(self) -> None: - # Error immediately if its an empty code. code: str | None = None if self._join_party_code_text: - code = cast(str, ba.textwidget(query=self._join_party_code_text)) + code = cast(str, bui.textwidget(query=self._join_party_code_text)) if not code: - ba.screenmessage( - ba.Lstr(resource='internal.invalidAddressErrorText'), + bui.screenmessage( + bui.Lstr(resource='internal.invalidAddressErrorText'), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return self._connect_to_party_code(code) @@ -970,24 +979,28 @@ class PrivateGatherTab(GatherTab): ) if cresult.error is not None: self._debug_server_comm('got error connect response') - ba.screenmessage( - ba.Lstr(translate=('serverResponses', cresult.error)), + bui.screenmessage( + bui.Lstr(translate=('serverResponses', cresult.error)), (1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return self._debug_server_comm('got valid connect response') assert cresult.addr is not None and cresult.port is not None - ba.internal.connect_to_party(cresult.addr, port=cresult.port) + bs.connect_to_party(cresult.addr, port=cresult.port) except Exception: self._debug_server_comm('got connect response error') - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() def save_state(self) -> None: - ba.app.ui.window_states[type(self)] = copy.deepcopy(self._state) + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = copy.deepcopy( + self._state + ) def restore_state(self) -> None: - state = ba.app.ui.window_states.get(type(self)) + assert bui.app.classic is not None + state = bui.app.classic.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/publictab.py b/src/assets/ba_data/python/bastd/ui/gather/publictab.py similarity index 80% rename from assets/src/ba_data/python/bastd/ui/gather/publictab.py rename to src/assets/ba_data/python/bastd/ui/gather/publictab.py index a1e083c3..89eee6b5 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/publictab.py +++ b/src/assets/ba_data/python/bastd/ui/gather/publictab.py @@ -7,17 +7,19 @@ from __future__ import annotations import copy import time -import threading +import logging +from threading import Thread from enum import Enum from dataclasses import dataclass from typing import TYPE_CHECKING, cast -import ba -import ba.internal from bastd.ui.gather import GatherTab +import bauiv1 as bui +import bascenev1 as bs if TYPE_CHECKING: from typing import Callable, Any + from bastd.ui.gather import GatherWindow # Print a bit of info about pings, queries, etc. @@ -61,10 +63,10 @@ class UIRow: """Wrangles UI for a row in the party list.""" def __init__(self) -> None: - self._name_widget: ba.Widget | None = None - self._size_widget: ba.Widget | None = None - self._ping_widget: ba.Widget | None = None - self._stats_button: ba.Widget | None = None + self._name_widget: bui.Widget | None = None + self._size_widget: bui.Widget | None = None + self._ping_widget: bui.Widget | None = None + self._stats_button: bui.Widget | None = None def __del__(self) -> None: self._clear() @@ -86,37 +88,40 @@ class UIRow: sub_scroll_width: float, sub_scroll_height: float, lineheight: float, - columnwidget: ba.Widget, - join_text: ba.Widget, - filter_text: ba.Widget, + columnwidget: bui.Widget, + join_text: bui.Widget, + filter_text: bui.Widget, existing_selection: Selection | None, tab: PublicGatherTab, ) -> None: """Update for the given data.""" # pylint: disable=too-many-locals + plus = bui.app.plus + assert plus is not None + # Quick-out: if we've been marked clean for a certain index and # we're still at that index, we're done. if party.clean_display_index == index: return - ping_good = ba.internal.get_v1_account_misc_read_val('pingGood', 100) - ping_med = ba.internal.get_v1_account_misc_read_val('pingMed', 500) + ping_good = plus.get_v1_account_misc_read_val('pingGood', 100) + ping_med = plus.get_v1_account_misc_read_val('pingMed', 500) self._clear() hpos = 20 vpos = sub_scroll_height - lineheight * index - 50 - self._name_widget = ba.textwidget( - text=ba.Lstr(value=party.name), + self._name_widget = bui.textwidget( + text=bui.Lstr(value=party.name), parent=columnwidget, size=(sub_scroll_width * 0.63, 20), position=(0 + hpos, 4 + vpos), selectable=True, - on_select_call=ba.WeakCall( + on_select_call=bui.WeakCall( tab.set_public_party_selection, Selection(party.get_key(), SelectionComponent.NAME), ), - on_activate_call=ba.WeakCall(tab.on_public_party_activate, party), + on_activate_call=bui.WeakCall(tab.on_public_party_activate, party), click_activate=True, maxwidth=sub_scroll_width * 0.45, corner_scale=1.4, @@ -125,7 +130,7 @@ class UIRow: h_align='left', v_align='center', ) - ba.widget( + bui.widget( edit=self._name_widget, left_widget=join_text, show_buffer_top=64.0, @@ -134,24 +139,24 @@ class UIRow: if existing_selection == Selection( party.get_key(), SelectionComponent.NAME ): - ba.containerwidget( + bui.containerwidget( edit=columnwidget, selected_child=self._name_widget ) if party.stats_addr: url = party.stats_addr.replace( '${ACCOUNT}', - ba.internal.get_v1_account_misc_read_val_2( + plus.get_v1_account_misc_read_val_2( 'resolvedAccountID', 'UNKNOWN' ), ) - self._stats_button = ba.buttonwidget( + self._stats_button = bui.buttonwidget( color=(0.3, 0.6, 0.94), textcolor=(1.0, 1.0, 1.0), - label=ba.Lstr(resource='statsText'), + label=bui.Lstr(resource='statsText'), parent=columnwidget, autoselect=True, - on_activate_call=ba.Call(ba.open_url, url), - on_select_call=ba.WeakCall( + on_activate_call=bui.Call(bui.open_url, url), + on_select_call=bui.WeakCall( tab.set_public_party_selection, Selection(party.get_key(), SelectionComponent.STATS_BUTTON), ), @@ -162,11 +167,11 @@ class UIRow: if existing_selection == Selection( party.get_key(), SelectionComponent.STATS_BUTTON ): - ba.containerwidget( + bui.containerwidget( edit=columnwidget, selected_child=self._stats_button ) - self._size_widget = ba.textwidget( + self._size_widget = bui.textwidget( text=str(party.size) + '/' + str(party.size_max), parent=columnwidget, size=(0, 0), @@ -178,11 +183,11 @@ class UIRow: ) if index == 0: - ba.widget(edit=self._name_widget, up_widget=filter_text) + bui.widget(edit=self._name_widget, up_widget=filter_text) if self._stats_button: - ba.widget(edit=self._stats_button, up_widget=filter_text) + bui.widget(edit=self._stats_button, up_widget=filter_text) - self._ping_widget = ba.textwidget( + self._ping_widget = bui.textwidget( parent=columnwidget, size=(0, 0), position=(sub_scroll_width * 0.94 + hpos, 20 + vpos), @@ -191,11 +196,11 @@ class UIRow: v_align='center', ) if party.ping is None: - ba.textwidget( + bui.textwidget( edit=self._ping_widget, text='-', color=(0.5, 0.5, 0.5) ) else: - ba.textwidget( + bui.textwidget( edit=self._ping_widget, text=str(int(party.ping)), color=(0, 1, 0) @@ -235,7 +240,7 @@ class Selection: component: SelectionComponent -class AddrFetchThread(threading.Thread): +class AddrFetchThread(Thread): """Thread for fetching an address in the bg.""" def __init__(self, call: Callable[[Any], Any]): @@ -251,7 +256,7 @@ class AddrFetchThread(threading.Thread): sock.connect(('8.8.8.8', 80)) val = sock.getsockname()[0] sock.close() - ba.pushcall(ba.Call(self._call, val), from_other_thread=True) + bui.pushcall(bui.Call(self._call, val), from_other_thread=True) except Exception as exc: from efro.error import is_udp_communication_error @@ -259,10 +264,10 @@ class AddrFetchThread(threading.Thread): if is_udp_communication_error(exc): pass else: - ba.print_exception() + logging.exception('Error in addr-fetch-thread') -class PingThread(threading.Thread): +class PingThread(Thread): """Thread for sending out game pings.""" def __init__( @@ -277,13 +282,13 @@ class PingThread(threading.Thread): self._call = call def run(self) -> None: - ba.app.ping_thread_count += 1 + assert bui.app.classic is not None + bui.app.classic.ping_thread_count += 1 sock: socket.socket | None = None try: import socket - from ba.internal import get_ip_address_type - socket_type = get_ip_address_type(self._address) + socket_type = bui.get_ip_address_type(self._address) sock = socket.socket(socket_type, socket.SOCK_DGRAM) sock.connect((self._address, self._port)) @@ -307,8 +312,8 @@ class PingThread(threading.Thread): break time.sleep(1) ping = (time.time() - starttime) * 1000.0 - ba.pushcall( - ba.Call( + bui.pushcall( + bui.Call( self._call, self._address, self._port, @@ -322,15 +327,17 @@ class PingThread(threading.Thread): if is_udp_communication_error(exc): pass else: - ba.print_exception('Error on gather ping', once=True) + if bui.do_once(): + logging.exception('Error on gather ping.') finally: try: if sock is not None: sock.close() except Exception: - ba.print_exception('Error on gather ping cleanup', once=True) + if bui.do_once(): + logging.exception('Error on gather ping cleanup') - ba.app.ping_thread_count -= 1 + bui.app.classic.ping_thread_count -= 1 class PublicGatherTab(GatherTab): @@ -338,26 +345,26 @@ class PublicGatherTab(GatherTab): def __init__(self, window: GatherWindow) -> None: super().__init__(window) - self._container: ba.Widget | None = None - self._join_text: ba.Widget | None = None - self._host_text: ba.Widget | None = None - self._filter_text: ba.Widget | None = None + self._container: bui.Widget | None = None + self._join_text: bui.Widget | None = None + self._host_text: bui.Widget | None = None + self._filter_text: bui.Widget | None = None self._local_address: str | None = None self._last_connect_attempt_time: float | None = None self._sub_tab: SubTabType = SubTabType.JOIN self._selection: Selection | None = None self._refreshing_list = False - self._update_timer: ba.Timer | None = None - self._host_scrollwidget: ba.Widget | None = None - self._host_name_text: ba.Widget | None = None - self._host_toggle_button: ba.Widget | None = None + self._update_timer: bui.AppTimer | None = None + self._host_scrollwidget: bui.Widget | None = None + self._host_name_text: bui.Widget | None = None + self._host_toggle_button: bui.Widget | None = None self._last_server_list_query_time: float | None = None - self._join_list_column: ba.Widget | None = None - self._join_status_text: ba.Widget | None = None - self._host_max_party_size_value: ba.Widget | None = None - self._host_max_party_size_minus_button: (ba.Widget | None) = None - self._host_max_party_size_plus_button: (ba.Widget | None) = None - self._host_status_text: ba.Widget | None = None + self._join_list_column: bui.Widget | None = None + self._join_status_text: bui.Widget | None = None + self._host_max_party_size_value: bui.Widget | None = None + self._host_max_party_size_minus_button: (bui.Widget | None) = None + self._host_max_party_size_plus_button: (bui.Widget | None) = None + self._host_status_text: bui.Widget | None = None self._signed_in = False self._ui_rows: list[UIRow] = [] self._refresh_ui_row = 0 @@ -383,16 +390,16 @@ class PublicGatherTab(GatherTab): def on_activate( self, - parent_widget: ba.Widget, - tab_button: ba.Widget, + parent_widget: bui.Widget, + tab_button: bui.Widget, region_width: float, region_height: float, region_left: float, region_bottom: float, - ) -> ba.Widget: + ) -> bui.Widget: c_width = region_width c_height = region_height - 20 - self._container = ba.containerwidget( + self._container = bui.containerwidget( parent=parent_widget, position=( region_left, @@ -403,7 +410,7 @@ class PublicGatherTab(GatherTab): selection_loops_to_parent=True, ) v = c_height - 30 - self._join_text = ba.textwidget( + self._join_text = bui.textwidget( parent=self._container, position=(c_width * 0.5 - 245, v - 13), color=(0.6, 1.0, 0.6), @@ -421,11 +428,11 @@ class PublicGatherTab(GatherTab): region_height, playsound=True, ), - text=ba.Lstr( + text=bui.Lstr( resource='gatherWindow.' 'joinPublicPartyDescriptionText' ), ) - self._host_text = ba.textwidget( + self._host_text = bui.textwidget( parent=self._container, position=(c_width * 0.5 + 45, v - 13), color=(0.6, 1.0, 0.6), @@ -443,28 +450,25 @@ class PublicGatherTab(GatherTab): region_height, playsound=True, ), - text=ba.Lstr( + text=bui.Lstr( resource='gatherWindow.' 'hostPublicPartyDescriptionText' ), ) - ba.widget(edit=self._join_text, up_widget=tab_button) - ba.widget( + bui.widget(edit=self._join_text, up_widget=tab_button) + bui.widget( edit=self._host_text, left_widget=self._join_text, up_widget=tab_button, ) - ba.widget(edit=self._join_text, right_widget=self._host_text) + bui.widget(edit=self._join_text, right_widget=self._host_text) # Attempt to fetch our local address so we have it for error messages. if self._local_address is None: - AddrFetchThread(ba.WeakCall(self._fetch_local_addr_cb)).start() + AddrFetchThread(bui.WeakCall(self._fetch_local_addr_cb)).start() self._set_sub_tab(self._sub_tab, region_width, region_height) - self._update_timer = ba.Timer( - 0.1, - ba.WeakCall(self._update), - repeat=True, - timetype=ba.TimeType.REAL, + self._update_timer = bui.AppTimer( + 0.1, bui.WeakCall(self._update), repeat=True ) return self._container @@ -472,12 +476,12 @@ class PublicGatherTab(GatherTab): self._update_timer = None def save_state(self) -> None: - # Save off a small number of parties with the lowest ping; we'll # 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[type(self)] = State( + assert bui.app.classic is not None + bui.app.classic.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, @@ -487,7 +491,8 @@ class PublicGatherTab(GatherTab): ) def restore_state(self) -> None: - state = ba.app.ui.window_states.get(type(self)) + assert bui.app.classic is not None + state = bui.app.classic.ui.window_states.get(type(self)) if state is None: state = State() assert isinstance(state, State) @@ -517,7 +522,7 @@ class PublicGatherTab(GatherTab): ) -> None: assert self._container if playsound: - ba.playsound(ba.getsound('click01')) + bui.getsound('click01').play() # Reset our selection. # (prevents selecting something way down the list if we switched away @@ -533,11 +538,11 @@ class PublicGatherTab(GatherTab): self._sub_tab = value active_color = (0.6, 1.0, 0.6) inactive_color = (0.5, 0.4, 0.5) - ba.textwidget( + bui.textwidget( edit=self._join_text, color=active_color if value is SubTabType.JOIN else inactive_color, ) - ba.textwidget( + bui.textwidget( edit=self._host_text, color=active_color if value is SubTabType.HOST else inactive_color, ) @@ -562,8 +567,8 @@ class PublicGatherTab(GatherTab): sub_scroll_width = 830 v = c_height - 35 v -= 60 - filter_txt = ba.Lstr(resource='filterText') - self._filter_text = ba.textwidget( + filter_txt = bui.Lstr(resource='filterText') + self._filter_text = bui.textwidget( parent=self._container, text=self._filter_value, size=(350, 45), @@ -574,8 +579,8 @@ class PublicGatherTab(GatherTab): maxwidth=310, description=filter_txt, ) - ba.widget(edit=self._filter_text, up_widget=self._join_text) - ba.textwidget( + bui.widget(edit=self._filter_text, up_widget=self._join_text) + bui.textwidget( text=filter_txt, parent=self._container, size=(0, 0), @@ -588,8 +593,8 @@ class PublicGatherTab(GatherTab): v_align='center', ) - ba.textwidget( - text=ba.Lstr(resource='nameText'), + bui.textwidget( + text=bui.Lstr(resource='nameText'), parent=self._container, size=(0, 0), position=(90, v - 8), @@ -600,8 +605,8 @@ class PublicGatherTab(GatherTab): h_align='center', v_align='center', ) - ba.textwidget( - text=ba.Lstr(resource='gatherWindow.partySizeText'), + bui.textwidget( + text=bui.Lstr(resource='gatherWindow.partySizeText'), parent=self._container, size=(0, 0), position=(755, v - 8), @@ -612,8 +617,8 @@ class PublicGatherTab(GatherTab): h_align='center', v_align='center', ) - ba.textwidget( - text=ba.Lstr(resource='gatherWindow.pingText'), + bui.textwidget( + text=bui.Lstr(resource='gatherWindow.pingText'), parent=self._container, size=(0, 0), position=(825, v - 8), @@ -625,7 +630,7 @@ class PublicGatherTab(GatherTab): v_align='center', ) v -= sub_scroll_height + 23 - self._host_scrollwidget = scrollw = ba.scrollwidget( + self._host_scrollwidget = scrollw = bui.scrollwidget( parent=self._container, simple_culling_v=10, position=((c_width - sub_scroll_width) * 0.5, v), @@ -634,13 +639,13 @@ class PublicGatherTab(GatherTab): claims_left_right=True, autoselect=True, ) - self._join_list_column = ba.containerwidget( + self._join_list_column = bui.containerwidget( parent=scrollw, background=False, size=(400, 400), claims_left_right=True, ) - self._join_status_text = ba.textwidget( + self._join_status_text = bui.textwidget( parent=self._container, text='', size=(0, 0), @@ -661,10 +666,10 @@ class PublicGatherTab(GatherTab): c_height = region_height - 20 v = c_height - 35 v -= 25 - is_public_enabled = ba.internal.get_public_party_enabled() + is_public_enabled = bs.get_public_party_enabled() v -= 30 - ba.textwidget( + bui.textwidget( parent=self._container, size=(0, 0), h_align='center', @@ -674,31 +679,32 @@ class PublicGatherTab(GatherTab): flatness=1.0, color=(0.5, 0.46, 0.5), position=(region_width * 0.5, v + 10), - text=ba.Lstr(resource='gatherWindow.publicHostRouterConfigText'), + text=bui.Lstr(resource='gatherWindow.publicHostRouterConfigText'), ) v -= 30 - party_name_text = ba.Lstr( + party_name_text = bui.Lstr( resource='gatherWindow.partyNameText', fallback_resource='editGameListWindow.nameText', ) - ba.textwidget( + assert bui.app.classic is not None + bui.textwidget( parent=self._container, size=(0, 0), h_align='right', v_align='center', maxwidth=200, scale=0.8, - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, position=(210, v - 9), text=party_name_text, ) - self._host_name_text = ba.textwidget( + self._host_name_text = bui.textwidget( parent=self._container, editable=True, size=(535, 40), position=(230, v - 30), - text=ba.app.config.get('Public Party Name', ''), + text=bui.app.config.get('Public Party Name', ''), maxwidth=494, shadow=0.3, flatness=1.0, @@ -709,21 +715,21 @@ class PublicGatherTab(GatherTab): ) v -= 60 - ba.textwidget( + bui.textwidget( parent=self._container, size=(0, 0), h_align='right', v_align='center', maxwidth=200, scale=0.8, - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, position=(210, v - 9), - text=ba.Lstr( + text=bui.Lstr( resource='maxPartySizeText', fallback_resource='maxConnectionsText', ), ) - self._host_max_party_size_value = ba.textwidget( + self._host_max_party_size_value = bui.textwidget( parent=self._container, size=(0, 0), h_align='center', @@ -731,22 +737,22 @@ class PublicGatherTab(GatherTab): scale=1.2, color=(1, 1, 1), position=(240, v - 9), - text=str(ba.internal.get_public_party_max_size()), + text=str(bs.get_public_party_max_size()), ) - btn1 = self._host_max_party_size_minus_button = ba.buttonwidget( + btn1 = self._host_max_party_size_minus_button = bui.buttonwidget( parent=self._container, size=(40, 40), - on_activate_call=ba.WeakCall( + on_activate_call=bui.WeakCall( self._on_max_public_party_size_minus_press ), position=(280, v - 26), label='-', autoselect=True, ) - btn2 = self._host_max_party_size_plus_button = ba.buttonwidget( + btn2 = self._host_max_party_size_plus_button = bui.buttonwidget( parent=self._container, size=(40, 40), - on_activate_call=ba.WeakCall( + on_activate_call=bui.WeakCall( self._on_max_public_party_size_plus_press ), position=(350, v - 26), @@ -756,16 +762,16 @@ class PublicGatherTab(GatherTab): v -= 50 v -= 70 if is_public_enabled: - label = ba.Lstr( + label = bui.Lstr( resource='gatherWindow.makePartyPrivateText', fallback_resource='gatherWindow.stopAdvertisingText', ) else: - label = ba.Lstr( + label = bui.Lstr( resource='gatherWindow.makePartyPublicText', fallback_resource='gatherWindow.startAdvertisingText', ) - self._host_toggle_button = ba.buttonwidget( + self._host_toggle_button = bui.buttonwidget( parent=self._container, label=label, size=(400, 80), @@ -776,14 +782,14 @@ class PublicGatherTab(GatherTab): autoselect=True, up_widget=btn2, ) - ba.widget(edit=self._host_name_text, down_widget=btn2) - ba.widget(edit=btn2, up_widget=self._host_name_text) - ba.widget(edit=btn1, up_widget=self._host_name_text) - ba.widget(edit=self._join_text, down_widget=self._host_name_text) + bui.widget(edit=self._host_name_text, down_widget=btn2) + bui.widget(edit=btn2, up_widget=self._host_name_text) + bui.widget(edit=btn1, up_widget=self._host_name_text) + bui.widget(edit=self._join_text, down_widget=self._host_name_text) v -= 10 - self._host_status_text = ba.textwidget( + self._host_status_text = bui.textwidget( parent=self._container, - text=ba.Lstr(resource='gatherWindow.' 'partyStatusNotPublicText'), + text=bui.Lstr(resource='gatherWindow.' 'partyStatusNotPublicText'), size=(0, 0), scale=0.7, flatness=1.0, @@ -794,9 +800,9 @@ class PublicGatherTab(GatherTab): position=(c_width * 0.5, v), ) v -= 90 - ba.textwidget( + bui.textwidget( parent=self._container, - text=ba.Lstr(resource='gatherWindow.dedicatedServerInfoText'), + text=bui.Lstr(resource='gatherWindow.dedicatedServerInfoText'), size=(0, 0), scale=0.7, flatness=1.0, @@ -809,7 +815,7 @@ class PublicGatherTab(GatherTab): # If public sharing is already on, # launch a status-check immediately. - if ba.internal.get_public_party_enabled(): + if bs.get_public_party_enabled(): self._do_status_check() def _on_public_party_query_result( @@ -862,17 +868,19 @@ class PublicGatherTab(GatherTab): def _update(self) -> None: """Periodic updating.""" + plus = bui.app.plus + assert plus is not None + # Special case: if a party-queue window is up, don't do any of this # (keeps things smoother). - # if ba.app.ui.have_party_queue_window: + # if bui.app.ui.have_party_queue_window: # return if self._sub_tab is SubTabType.JOIN: - # Keep our filter-text up to date from the UI. text = self._filter_text if text: - filter_value = cast(str, ba.textwidget(query=text)) + filter_value = cast(str, bui.textwidget(query=text)) if filter_value != self._filter_value: self._filter_value = filter_value self._party_lists_dirty = True @@ -891,7 +899,7 @@ class PublicGatherTab(GatherTab): self._process_pending_party_infos() # Anytime we sign in/out, make sure we refresh our list. - signed_in = ba.internal.get_v1_account_state() == 'signed_in' + signed_in = plus.get_v1_account_state() == 'signed_in' if self._signed_in != signed_in: self._signed_in = signed_in self._party_lists_dirty = True @@ -903,36 +911,37 @@ class PublicGatherTab(GatherTab): # into our public host name. text = self._host_name_text if text: - name = cast(str, ba.textwidget(query=self._host_name_text)) - ba.internal.set_public_party_name(name) + name = cast(str, bui.textwidget(query=self._host_name_text)) + bs.set_public_party_name(name) # Update status text. status_text = self._join_status_text if status_text: if not signed_in: - ba.textwidget( - edit=status_text, text=ba.Lstr(resource='notSignedInText') + bui.textwidget( + edit=status_text, text=bui.Lstr(resource='notSignedInText') ) else: # If we have a valid list, show no status; just the list. # Otherwise show either 'loading...' or 'error' depending # on whether this is our first go-round. if self._have_valid_server_list: - ba.textwidget(edit=status_text, text='') + bui.textwidget(edit=status_text, text='') else: if self._have_server_list_response: - ba.textwidget( - edit=status_text, text=ba.Lstr(resource='errorText') + bui.textwidget( + edit=status_text, + text=bui.Lstr(resource='errorText'), ) else: - ba.textwidget( + bui.textwidget( edit=status_text, - text=ba.Lstr( + text=bui.Lstr( value='${A}...', subs=[ ( '${A}', - ba.Lstr(resource='store.loadingText'), + bui.Lstr(resource='store.loadingText'), ) ], ), @@ -950,7 +959,7 @@ class PublicGatherTab(GatherTab): # Janky - allow escaping when there's nothing in our list. assert self._host_scrollwidget - ba.containerwidget( + bui.containerwidget( edit=self._host_scrollwidget, claims_up_down=(len(self._parties_displayed) > 0), ) @@ -968,7 +977,7 @@ class PublicGatherTab(GatherTab): sub_scroll_width = 830 lineheight = 42 sub_scroll_height = lineheight * len(self._parties_displayed) + 50 - ba.containerwidget( + bui.containerwidget( edit=columnwidget, size=(sub_scroll_width, sub_scroll_height) ) @@ -989,7 +998,7 @@ class PublicGatherTab(GatherTab): def refresh_on() -> None: self._refreshing_list = True - ba.pushcall(refresh_on) + bui.pushcall(refresh_on) # Ok, now here's the deal: we want to avoid creating/updating this # entire list at one time because it will lead to hitches. So we @@ -1030,7 +1039,7 @@ class PublicGatherTab(GatherTab): def refresh_off() -> None: self._refreshing_list = False - ba.pushcall(refresh_off) + bui.pushcall(refresh_off) def _process_pending_party_infos(self) -> None: starttime = time.time() @@ -1050,8 +1059,7 @@ class PublicGatherTab(GatherTab): # If this party is new to us, init it. party = PartyEntry( address=addr, - next_ping_time=ba.time(ba.TimeType.REAL) - + 0.001 * party_in['pd'], + next_ping_time=bui.apptime() + 0.001 * party_in['pd'], index=self._next_entry_index, ) self._parties[party_key] = party @@ -1088,6 +1096,9 @@ class PublicGatherTab(GatherTab): ) def _update_party_lists(self) -> None: + plus = bui.app.plus + assert plus is not None + if not self._party_lists_dirty: return starttime = time.time() @@ -1102,7 +1113,7 @@ class PublicGatherTab(GatherTab): # If signed out or errored, show no parties. if ( - ba.internal.get_v1_account_state() != 'signed_in' + plus.get_v1_account_state() != 'signed_in' or not self._have_valid_server_list ): self._parties_displayed = {} @@ -1139,41 +1150,45 @@ class PublicGatherTab(GatherTab): ) def _query_party_list_periodically(self) -> None: - now = ba.time(ba.TimeType.REAL) + now = bui.apptime() + + plus = bui.app.plus + assert plus is not None # Fire off a new public-party query periodically. if ( self._last_server_list_query_time is None or now - self._last_server_list_query_time > 0.001 - * ba.internal.get_v1_account_misc_read_val( - 'pubPartyRefreshMS', 10000 - ) + * plus.get_v1_account_misc_read_val('pubPartyRefreshMS', 10000) ): self._last_server_list_query_time = now if DEBUG_SERVER_COMMUNICATION: print('REQUESTING SERVER LIST') - if ba.internal.get_v1_account_state() == 'signed_in': - ba.internal.add_transaction( + if plus.get_v1_account_state() == 'signed_in': + plus.add_v1_account_transaction( { 'type': 'PUBLIC_PARTY_QUERY', - 'proto': ba.app.protocol_version, - 'lang': ba.app.lang.language, + 'proto': bui.app.protocol_version, + 'lang': bui.app.lang.language, }, - callback=ba.WeakCall(self._on_public_party_query_result), + callback=bui.WeakCall(self._on_public_party_query_result), ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() else: self._on_public_party_query_result(None) def _ping_parties_periodically(self) -> None: - now = ba.time(ba.TimeType.REAL) + assert bui.app.classic is not None + now = bui.apptime() # Go through our existing public party entries firing off pings # for any that have timed out. for party in list(self._parties.values()): - if party.next_ping_time <= now and ba.app.ping_thread_count < 15: - + if ( + party.next_ping_time <= now + and bui.app.classic.ping_thread_count < 15 + ): # Crank the interval up for high-latency or non-responding # parties to save us some useless work. mult = 1 @@ -1199,7 +1214,7 @@ class PublicGatherTab(GatherTab): party.ping_attempts += 1 PingThread( - party.address, party.port, ba.WeakCall(self._ping_callback) + party.address, party.port, bui.WeakCall(self._ping_callback) ).start() def _ping_callback( @@ -1234,28 +1249,27 @@ class PublicGatherTab(GatherTab): def _on_public_party_accessible_response( self, data: dict[str, Any] | None ) -> None: - # If we've got status text widgets, update them. text = self._host_status_text if text: if data is None: - ba.textwidget( + bui.textwidget( edit=text, - text=ba.Lstr( + text=bui.Lstr( resource='gatherWindow.' 'partyStatusNoConnectionText' ), color=(1, 0, 0), ) else: if not data.get('accessible', False): - ex_line: str | ba.Lstr + ex_line: str | bui.Lstr if self._local_address is not None: - ex_line = ba.Lstr( + ex_line = bui.Lstr( value='\n${A} ${B}', subs=[ ( '${A}', - ba.Lstr( + bui.Lstr( resource='gatherWindow.' 'manualYourLocalAddressText' ), @@ -1265,29 +1279,27 @@ class PublicGatherTab(GatherTab): ) else: ex_line = '' - ba.textwidget( + bui.textwidget( edit=text, - text=ba.Lstr( + text=bui.Lstr( value='${A}\n${B}${C}', subs=[ ( '${A}', - ba.Lstr( + bui.Lstr( resource='gatherWindow.' 'partyStatusNotJoinableText' ), ), ( '${B}', - ba.Lstr( + bui.Lstr( resource='gatherWindow.' 'manualRouterForwardingText', subs=[ ( '${PORT}', - str( - ba.internal.get_game_port() - ), + str(bs.get_game_port()), ) ], ), @@ -1298,58 +1310,60 @@ class PublicGatherTab(GatherTab): color=(1, 0, 0), ) else: - ba.textwidget( + bui.textwidget( edit=text, - text=ba.Lstr( + text=bui.Lstr( resource='gatherWindow.' 'partyStatusJoinableText' ), color=(0, 1, 0), ) def _do_status_check(self) -> None: - from ba.internal import master_server_get - - ba.textwidget( + assert bui.app.classic is not None + bui.textwidget( edit=self._host_status_text, color=(1, 1, 0), - text=ba.Lstr(resource='gatherWindow.' 'partyStatusCheckingText'), + text=bui.Lstr(resource='gatherWindow.' 'partyStatusCheckingText'), ) - master_server_get( + bui.app.classic.master_server_v1_get( 'bsAccessCheck', - {'b': ba.app.build_number}, - callback=ba.WeakCall(self._on_public_party_accessible_response), + {'b': bui.app.build_number}, + callback=bui.WeakCall(self._on_public_party_accessible_response), ) def _on_start_advertizing_press(self) -> None: from bastd.ui.account import show_sign_in_prompt - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return - name = cast(str, ba.textwidget(query=self._host_name_text)) + name = cast(str, bui.textwidget(query=self._host_name_text)) if name == '': - ba.screenmessage( - ba.Lstr(resource='internal.invalidNameErrorText'), + bui.screenmessage( + bui.Lstr(resource='internal.invalidNameErrorText'), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return - ba.internal.set_public_party_name(name) - cfg = ba.app.config + bs.set_public_party_name(name) + cfg = bui.app.config cfg['Public Party Name'] = name cfg.commit() - ba.playsound(ba.getsound('shieldUp')) - ba.internal.set_public_party_enabled(True) + bui.getsound('shieldUp').play() + bs.set_public_party_enabled(True) # In GUI builds we want to authenticate clients only when hosting # public parties. - ba.internal.set_authenticate_clients(True) + bs.set_authenticate_clients(True) self._do_status_check() - ba.buttonwidget( + bui.buttonwidget( edit=self._host_toggle_button, - label=ba.Lstr( + label=bui.Lstr( resource='gatherWindow.makePartyPrivateText', fallback_resource='gatherWindow.stopAdvertisingText', ), @@ -1357,24 +1371,24 @@ class PublicGatherTab(GatherTab): ) def _on_stop_advertising_press(self) -> None: - ba.internal.set_public_party_enabled(False) + bs.set_public_party_enabled(False) # In GUI builds we want to authenticate clients only when hosting # public parties. - ba.internal.set_authenticate_clients(False) - ba.playsound(ba.getsound('shieldDown')) + bs.set_authenticate_clients(False) + bui.getsound('shieldDown').play() text = self._host_status_text if text: - ba.textwidget( + bui.textwidget( edit=text, - text=ba.Lstr( + text=bui.Lstr( resource='gatherWindow.' 'partyStatusNotPublicText' ), color=(0.6, 0.6, 0.6), ) - ba.buttonwidget( + bui.buttonwidget( edit=self._host_toggle_button, - label=ba.Lstr( + label=bui.Lstr( resource='gatherWindow.makePartyPublicText', fallback_resource='gatherWindow.startAdvertisingText', ), @@ -1387,7 +1401,7 @@ class PublicGatherTab(GatherTab): if party.queue is not None: from bastd.ui.partyqueue import PartyQueueWindow - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() PartyQueueWindow(party.queue, party.address, party.port) else: address = party.address @@ -1397,7 +1411,7 @@ class PublicGatherTab(GatherTab): now = time.time() last_connect_time = self._last_connect_attempt_time if last_connect_time is None or now - last_connect_time > 2.0: - ba.internal.connect_to_party(address, port=port) + bs.connect_to_party(address, port=port) self._last_connect_attempt_time = now def set_public_party_selection(self, sel: Selection) -> None: @@ -1408,12 +1422,12 @@ class PublicGatherTab(GatherTab): self._have_user_selected_row = True def _on_max_public_party_size_minus_press(self) -> None: - val = max(1, ba.internal.get_public_party_max_size() - 1) - ba.internal.set_public_party_max_size(val) - ba.textwidget(edit=self._host_max_party_size_value, text=str(val)) + val = max(1, bs.get_public_party_max_size() - 1) + bs.set_public_party_max_size(val) + bui.textwidget(edit=self._host_max_party_size_value, text=str(val)) def _on_max_public_party_size_plus_press(self) -> None: - val = ba.internal.get_public_party_max_size() + val = bs.get_public_party_max_size() val += 1 - ba.internal.set_public_party_max_size(val) - ba.textwidget(edit=self._host_max_party_size_value, text=str(val)) + bs.set_public_party_max_size(val) + bui.textwidget(edit=self._host_max_party_size_value, text=str(val)) diff --git a/assets/src/ba_data/python/bastd/ui/getcurrency.py b/src/assets/ba_data/python/bastd/ui/getcurrency.py similarity index 71% rename from assets/src/ba_data/python/bastd/ui/getcurrency.py rename to src/assets/ba_data/python/bastd/ui/getcurrency.py index 6d4cd90b..54b13434 100644 --- a/assets/src/ba_data/python/bastd/ui/getcurrency.py +++ b/src/assets/ba_data/python/bastd/ui/getcurrency.py @@ -6,14 +6,13 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any -class GetCurrencyWindow(ba.Window): +class GetCurrencyWindow(bui.Window): """Window for purchasing/acquiring currency.""" def __init__( @@ -21,19 +20,22 @@ class GetCurrencyWindow(ba.Window): transition: str = 'in_right', from_modal_store: bool = False, modal: bool = False, - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, store_back_location: str | None = None, ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals - ba.set_analytics_screen('Get Tickets Window') + plus = bui.app.plus + assert plus is not None + + bui.set_analytics_screen('Get Tickets Window') self._transitioning_out = False self._store_back_location = store_back_location # ew. self._ad_button_greyed = False - self._smooth_update_timer: ba.Timer | None = None + self._smooth_update_timer: bui.AppTimer | None = None self._ad_button = None self._ad_label = None self._ad_image = None @@ -49,65 +51,68 @@ class GetCurrencyWindow(ba.Window): self._transition_out = 'out_right' scale_origin = None - uiscale = ba.app.ui.uiscale - self._width = 1000.0 if uiscale is ba.UIScale.SMALL else 800.0 - x_inset = 100.0 if uiscale is ba.UIScale.SMALL else 0.0 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self._width = 1000.0 if uiscale is bui.UIScale.SMALL else 800.0 + x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 self._height = 480.0 self._modal = modal self._from_modal_store = from_modal_store self._r = 'getTicketsWindow' - top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), transition=transition, scale_origin_stack_offset=scale_origin, color=(0.4, 0.37, 0.55), scale=( 1.63 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.2 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), - stack_offset=(0, -3) if uiscale is ba.UIScale.SMALL else (0, 0), + stack_offset=(0, -3) + if uiscale is bui.UIScale.SMALL + else (0, 0), ) ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=(55 + x_inset, self._height - 79), size=(140, 60), scale=1.0, autoselect=True, - label=ba.Lstr(resource='doneText' if modal else 'backText'), + label=bui.Lstr(resource='doneText' if modal else 'backText'), button_type='regular' if modal else 'back', on_activate_call=self._back, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 55), size=(0, 0), - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, scale=1.2, h_align='center', v_align='center', - text=ba.Lstr(resource=self._r + '.titleText'), + text=bui.Lstr(resource=self._r + '.titleText'), maxwidth=290, ) if not modal: - ba.buttonwidget( + bui.buttonwidget( edit=btn, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) b_size = (220.0, 180.0) @@ -120,15 +125,15 @@ class GetCurrencyWindow(ba.Window): item: str, position: tuple[float, float], size: tuple[float, float], - label: ba.Lstr, + label: bui.Lstr, price: str | None = None, tex_name: str | None = None, tex_opacity: float = 1.0, tex_scale: float = 1.0, enabled: bool = True, text_scale: float = 1.0, - ) -> ba.Widget: - btn2 = ba.buttonwidget( + ) -> bui.Widget: + btn2 = bui.buttonwidget( parent=self._root_widget, position=position, button_type='square', @@ -137,12 +142,12 @@ class GetCurrencyWindow(ba.Window): autoselect=True, color=None if enabled else (0.5, 0.5, 0.5), on_activate_call=( - ba.Call(self._purchase, item) + bui.Call(self._purchase, item) if enabled else self._disabled_press ), ) - txt = ba.textwidget( + txt = bui.textwidget( parent=self._root_widget, text=label, position=( @@ -158,7 +163,7 @@ class GetCurrencyWindow(ba.Window): color=(0.7, 0.9, 0.7, 1.0 if enabled else 0.2), ) if price is not None and enabled: - ba.textwidget( + bui.textwidget( parent=self._root_widget, text=price, position=( @@ -176,9 +181,9 @@ class GetCurrencyWindow(ba.Window): i = None if tex_name is not None: tex_size = 90.0 * tex_scale - i = ba.imagewidget( + i = bui.imagewidget( parent=self._root_widget, - texture=ba.gettexture(tex_name), + texture=bui.gettexture(tex_name), position=( position[0] + size[0] * 0.5 - tex_size * 0.5, position[1] + size[1] * 0.66 - tex_size * 0.5, @@ -192,7 +197,7 @@ class GetCurrencyWindow(ba.Window): self._ad_label = txt assert i is not None self._ad_image = i - self._ad_time_text = ba.textwidget( + self._ad_time_text = bui.textwidget( parent=self._root_widget, text='1m 10s', position=( @@ -211,52 +216,50 @@ class GetCurrencyWindow(ba.Window): rsrc = self._r + '.ticketsText' - c2txt = ba.Lstr( + c2txt = bui.Lstr( resource=rsrc, subs=[ ( '${COUNT}', str( - ba.internal.get_v1_account_misc_read_val( - 'tickets2Amount', 500 - ) + plus.get_v1_account_misc_read_val('tickets2Amount', 500) ), ) ], ) - c3txt = ba.Lstr( + c3txt = bui.Lstr( resource=rsrc, subs=[ ( '${COUNT}', str( - ba.internal.get_v1_account_misc_read_val( + plus.get_v1_account_misc_read_val( 'tickets3Amount', 1500 ) ), ) ], ) - c4txt = ba.Lstr( + c4txt = bui.Lstr( resource=rsrc, subs=[ ( '${COUNT}', str( - ba.internal.get_v1_account_misc_read_val( + plus.get_v1_account_misc_read_val( 'tickets4Amount', 5000 ) ), ) ], ) - c5txt = ba.Lstr( + c5txt = bui.Lstr( resource=rsrc, subs=[ ( '${COUNT}', str( - ba.internal.get_v1_account_misc_read_val( + plus.get_v1_account_misc_read_val( 'tickets5Amount', 15000 ) ), @@ -266,11 +269,11 @@ class GetCurrencyWindow(ba.Window): h = 110.0 - # enable buttons if we have prices.. - tickets2_price = ba.internal.get_price('tickets2') - tickets3_price = ba.internal.get_price('tickets3') - tickets4_price = ba.internal.get_price('tickets4') - tickets5_price = ba.internal.get_price('tickets5') + # Enable buttons if we have prices. + tickets2_price = plus.get_price('tickets2') + tickets3_price = plus.get_price('tickets3') + tickets4_price = plus.get_price('tickets4') + tickets5_price = plus.get_price('tickets5') # TEMP # tickets1_price = '$0.99' @@ -331,7 +334,7 @@ class GetCurrencyWindow(ba.Window): tex_scale=1.2, ) # 19.99-ish - self._enable_ad_button = ba.internal.has_video_ads() + self._enable_ad_button = bui.has_video_ads() h = self._width * 0.5 + 110.0 v = self._height - b_size[1] - 115.0 @@ -342,13 +345,13 @@ class GetCurrencyWindow(ba.Window): 'ad', position=(h + h_offs, v), size=b_size_3, - label=ba.Lstr( + label=bui.Lstr( resource=self._r + '.ticketsFromASponsorText', subs=[ ( '${COUNT}', str( - ba.internal.get_v1_account_misc_read_val( + plus.get_v1_account_misc_read_val( 'sponsorTickets', 5 ) ), @@ -361,24 +364,17 @@ class GetCurrencyWindow(ba.Window): tex_scale=0.7, text_scale=0.7, ) - ba.buttonwidget( - edit=cdb, - color=(0.65, 0.5, 0.7) - if self._enable_ad_button - else (0.5, 0.5, 0.5), - ) + bui.buttonwidget(edit=cdb, color=(0.65, 0.5, 0.7)) - self._ad_free_text = ba.textwidget( + self._ad_free_text = bui.textwidget( parent=self._root_widget, - text=ba.Lstr(resource=self._r + '.freeText'), + text=bui.Lstr(resource=self._r + '.freeText'), position=( h + h_offs + b_size_3[0] * 0.5, v + b_size_3[1] * 0.5 + 25, ), size=(0, 0), - color=(1, 1, 0, 1.0) - if self._enable_ad_button - else (1, 1, 1, 0.2), + color=(1, 1, 0, 1.0), draw_controller=cdb, rotate=15, shadow=1.0, @@ -398,13 +394,13 @@ class GetCurrencyWindow(ba.Window): 'app_invite', position=(h + h_offs, v), size=b_size_3, - label=ba.Lstr( + label=bui.Lstr( resource='gatherWindow.earnTicketsForRecommendingText', subs=[ ( '${COUNT}', str( - ba.internal.get_v1_account_misc_read_val( + plus.get_v1_account_misc_read_val( 'sponsorTickets', 5 ) ), @@ -417,11 +413,11 @@ class GetCurrencyWindow(ba.Window): tex_scale=0.7, text_scale=0.7, ) - ba.buttonwidget(edit=cdb, color=(0.65, 0.5, 0.7)) + bui.buttonwidget(edit=cdb, color=(0.65, 0.5, 0.7)) - ba.textwidget( + bui.textwidget( parent=self._root_widget, - text=ba.Lstr(resource=self._r + '.freeText'), + text=bui.Lstr(resource=self._r + '.freeText'), position=( h + h_offs + b_size_3[0] * 0.5, v + b_size_3[1] * 0.5 + 25, @@ -442,19 +438,19 @@ class GetCurrencyWindow(ba.Window): v = self._height - 95 + tc_y_offs txt1 = ( - ba.Lstr(resource=self._r + '.youHaveText') + bui.Lstr(resource=self._r + '.youHaveText') .evaluate() .partition('${COUNT}')[0] .strip() ) txt2 = ( - ba.Lstr(resource=self._r + '.youHaveText') + bui.Lstr(resource=self._r + '.youHaveText') .evaluate() .rpartition('${COUNT}')[-1] .strip() ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, text=txt1, position=(h, v), @@ -466,7 +462,7 @@ class GetCurrencyWindow(ba.Window): scale=0.8, ) v -= 30 - self._ticket_count_text = ba.textwidget( + self._ticket_count_text = bui.textwidget( parent=self._root_widget, position=(h, v), size=(0, 0), @@ -477,7 +473,7 @@ class GetCurrencyWindow(ba.Window): scale=1.6, ) v -= 30 - ba.textwidget( + bui.textwidget( parent=self._root_widget, text=txt2, position=(h, v), @@ -489,23 +485,19 @@ class GetCurrencyWindow(ba.Window): scale=0.8, ) - # update count now and once per second going forward.. - self._ticking_node: ba.Node | None = None + self._ticking_sound: bui.Sound | None = None self._smooth_ticket_count: float | None = None self._ticket_count = 0 self._update() - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - timetype=ba.TimeType.REAL, - repeat=True, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) self._smooth_increase_speed = 1.0 def __del__(self) -> None: - if self._ticking_node is not None: - self._ticking_node.delete() - self._ticking_node = None + if self._ticking_sound is not None: + self._ticking_sound.stop() + self._ticking_sound = None def _smooth_update(self) -> None: if not self._ticket_count_text: @@ -514,13 +506,13 @@ class GetCurrencyWindow(ba.Window): finished = False - # if we're going down, do it immediately + # If we're going down, do it immediately. assert self._smooth_ticket_count is not None if int(self._smooth_ticket_count) >= self._ticket_count: self._smooth_ticket_count = float(self._ticket_count) finished = True else: - # we're going up; start a sound if need be + # We're going up; start a sound if need be. self._smooth_ticket_count = min( self._smooth_ticket_count + 1.0 * self._smooth_increase_speed, self._ticket_count, @@ -528,43 +520,40 @@ class GetCurrencyWindow(ba.Window): if int(self._smooth_ticket_count) >= self._ticket_count: finished = True self._smooth_ticket_count = float(self._ticket_count) - elif self._ticking_node is None: - with ba.Context('ui'): - self._ticking_node = ba.newnode( - 'sound', - attrs={ - 'sound': ba.getsound('scoreIncrease'), - 'positional': False, - }, - ) + elif self._ticking_sound is None: + self._ticking_sound = bui.getsound('scoreIncrease') + self._ticking_sound.play() - ba.textwidget( + bui.textwidget( edit=self._ticket_count_text, text=str(int(self._smooth_ticket_count)), ) - # if we've reached the target, kill the timer/sound/etc + # If we've reached the target, kill the timer/sound/etc. if finished: self._smooth_update_timer = None - if self._ticking_node is not None: - self._ticking_node.delete() - self._ticking_node = None - ba.playsound(ba.getsound('cashRegister2')) + if self._ticking_sound is not None: + self._ticking_sound.stop() + self._ticking_sound = None + bui.getsound('cashRegister2').play() def _update(self) -> None: import datetime + plus = bui.app.plus + assert plus is not None + # if we somehow get signed out, just die.. - if ba.internal.get_v1_account_state() != 'signed_in': + if plus.get_v1_account_state() != 'signed_in': self._back() return - self._ticket_count = ba.internal.get_v1_account_ticket_count() + self._ticket_count = plus.get_v1_account_ticket_count() # update our incentivized ad button depending on whether ads are # available if self._ad_button is not None: - next_reward_ad_time = ba.internal.get_v1_account_misc_read_val_2( + next_reward_ad_time = plus.get_v1_account_misc_read_val_2( 'nextRewardAdTime', None ) if next_reward_ad_time is not None: @@ -572,34 +561,32 @@ class GetCurrencyWindow(ba.Window): next_reward_ad_time ) now = datetime.datetime.utcnow() - if ba.internal.have_incentivized_ad() and ( + if bui.have_incentivized_ad() and ( next_reward_ad_time is None or next_reward_ad_time <= now ): self._ad_button_greyed = False - ba.buttonwidget(edit=self._ad_button, color=(0.65, 0.5, 0.7)) - ba.textwidget(edit=self._ad_label, color=(0.7, 0.9, 0.7, 1.0)) - ba.textwidget(edit=self._ad_free_text, color=(1, 1, 0, 1)) - ba.imagewidget(edit=self._ad_image, opacity=0.6) - ba.textwidget(edit=self._ad_time_text, text='') + bui.buttonwidget(edit=self._ad_button, color=(0.65, 0.5, 0.7)) + bui.textwidget(edit=self._ad_label, color=(0.7, 0.9, 0.7, 1.0)) + bui.textwidget(edit=self._ad_free_text, color=(1, 1, 0, 1)) + bui.imagewidget(edit=self._ad_image, opacity=0.6) + bui.textwidget(edit=self._ad_time_text, text='') else: self._ad_button_greyed = True - ba.buttonwidget(edit=self._ad_button, color=(0.5, 0.5, 0.5)) - ba.textwidget(edit=self._ad_label, color=(0.7, 0.9, 0.7, 0.2)) - ba.textwidget(edit=self._ad_free_text, color=(1, 1, 0, 0.2)) - ba.imagewidget(edit=self._ad_image, opacity=0.6 * 0.25) - sval: str | ba.Lstr + bui.buttonwidget(edit=self._ad_button, color=(0.5, 0.5, 0.5)) + bui.textwidget(edit=self._ad_label, color=(0.7, 0.9, 0.7, 0.2)) + bui.textwidget(edit=self._ad_free_text, color=(1, 1, 0, 0.2)) + bui.imagewidget(edit=self._ad_image, opacity=0.6 * 0.25) + sval: str | bui.Lstr if ( next_reward_ad_time is not None and next_reward_ad_time > now ): - sval = ba.timestring( - (next_reward_ad_time - now).total_seconds() * 1000.0, - centi=False, - timeformat=ba.TimeFormat.MILLISECONDS, + sval = bui.timestring( + (next_reward_ad_time - now).total_seconds(), centi=False ) else: sval = '' - ba.textwidget(edit=self._ad_time_text, text=sval) + bui.textwidget(edit=self._ad_time_text, text=sval) # if this is our first update, assign immediately; otherwise kick # off a smooth transition if the value has changed @@ -611,11 +598,8 @@ class GetCurrencyWindow(ba.Window): self._ticket_count != int(self._smooth_ticket_count) and self._smooth_update_timer is None ): - self._smooth_update_timer = ba.Timer( - 0.05, - ba.WeakCall(self._smooth_update), - repeat=True, - timetype=ba.TimeType.REAL, + self._smooth_update_timer = bui.AppTimer( + 0.05, bui.WeakCall(self._smooth_update), repeat=True ) diff = abs(float(self._ticket_count) - self._smooth_ticket_count) self._smooth_increase_speed = ( @@ -629,63 +613,70 @@ class GetCurrencyWindow(ba.Window): ) def _disabled_press(self) -> None: + plus = bui.app.plus + assert plus is not None # if we're on a platform without purchases, inform the user they # can link their accounts and buy stuff elsewhere - app = ba.app + app = bui.app + assert app.classic is not None if ( app.test_build or ( - app.platform == 'android' - and app.subplatform in ['oculus', 'cardboard'] + app.classic.platform == 'android' + and app.classic.subplatform in ['oculus', 'cardboard'] ) - ) and ba.internal.get_v1_account_misc_read_val( - 'allowAccountLinking2', False - ): - ba.screenmessage( - ba.Lstr(resource=self._r + '.unavailableLinkAccountText'), + ) and plus.get_v1_account_misc_read_val('allowAccountLinking2', False): + bui.screenmessage( + bui.Lstr(resource=self._r + '.unavailableLinkAccountText'), color=(1, 0.5, 0), ) else: - ba.screenmessage( - ba.Lstr(resource=self._r + '.unavailableText'), + bui.screenmessage( + bui.Lstr(resource=self._r + '.unavailableText'), color=(1, 0.5, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() def _purchase(self, item: str) -> None: from bastd.ui import account from bastd.ui import appinvite - from ba.internal import master_server_get + + plus = bui.app.plus + assert plus is not None + + if bui.app.classic is None: + raise RuntimeError('This requires classic support.') if item == 'app_invite': - if ba.internal.get_v1_account_state() != 'signed_in': + if plus.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return appinvite.handle_app_invites_press() return # here we ping the server to ask if it's valid for us to # purchase this.. (better to fail now than after we've paid locally) - app = ba.app - master_server_get( + app = bui.app + assert app.classic is not None + bui.app.classic.master_server_v1_get( 'bsAccountPurchaseCheck', { 'item': item, - 'platform': app.platform, - 'subplatform': app.subplatform, + 'platform': app.classic.platform, + 'subplatform': app.classic.subplatform, 'version': app.version, 'buildNumber': app.build_number, }, - callback=ba.WeakCall(self._purchase_check_result, item), + callback=bui.WeakCall(self._purchase_check_result, item), ) def _purchase_check_result( self, item: str, result: dict[str, Any] | None ) -> None: if result is None: - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource='internal.unavailableNoConnectionText'), + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='internal.unavailableNoConnectionText'), color=(1, 0, 0), ) else: @@ -693,25 +684,28 @@ class GetCurrencyWindow(ba.Window): self._do_purchase(item) else: if result['reason'] == 'versionTooOld': - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource='getTicketsWindow.versionTooOldText'), + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='getTicketsWindow.versionTooOldText'), color=(1, 0, 0), ) else: - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource='getTicketsWindow.unavailableText'), + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='getTicketsWindow.unavailableText'), color=(1, 0, 0), ) # actually start the purchase locally.. def _do_purchase(self, item: str) -> None: + plus = bui.app.plus + assert plus is not None + if item == 'ad': import datetime # if ads are disabled until some time, error.. - next_reward_ad_time = ba.internal.get_v1_account_misc_read_val_2( + next_reward_ad_time = plus.get_v1_account_misc_read_val_2( 'nextRewardAdTime', None ) if next_reward_ad_time is not None: @@ -722,24 +716,25 @@ class GetCurrencyWindow(ba.Window): if ( next_reward_ad_time is not None and next_reward_ad_time > now ) or self._ad_button_greyed: - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr( + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr( resource='getTicketsWindow.unavailableTemporarilyText' ), color=(1, 0, 0), ) elif self._enable_ad_button: - ba.app.ads.show_ad('tickets') + assert bui.app.classic is not None + bui.app.classic.ads.show_ad('tickets') else: - ba.internal.purchase(item) + plus.purchase(item) def _back(self) -> None: from bastd.ui.store import browser if self._transitioning_out: return - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) if not self._modal: @@ -749,7 +744,8 @@ class GetCurrencyWindow(ba.Window): back_location=self._store_back_location, ).get_root_widget() if not self._from_modal_store: - ba.app.ui.set_main_menu_window(window) + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window(window) self._transitioning_out = True @@ -761,22 +757,23 @@ def show_get_tickets_prompt() -> None: """ from bastd.ui.confirm import ConfirmWindow - if ba.app.allow_ticket_purchases: + assert bui.app.classic is not None + if bui.app.classic.allow_ticket_purchases: ConfirmWindow( - ba.Lstr( + bui.Lstr( translate=( 'serverResponses', 'You don\'t have enough tickets for this!', ) ), lambda: GetCurrencyWindow(modal=True), - ok_text=ba.Lstr(resource='getTicketsWindow.titleText'), + ok_text=bui.Lstr(resource='getTicketsWindow.titleText'), width=460, height=130, ) else: ConfirmWindow( - ba.Lstr( + bui.Lstr( translate=( 'serverResponses', 'You don\'t have enough tickets for this!', diff --git a/assets/src/ba_data/python/bastd/ui/getremote.py b/src/assets/ba_data/python/bastd/ui/getremote.py similarity index 69% rename from assets/src/ba_data/python/bastd/ui/getremote.py rename to src/assets/ba_data/python/bastd/ui/getremote.py index c25d5ff8..d1821b93 100644 --- a/assets/src/ba_data/python/bastd/ui/getremote.py +++ b/src/assets/ba_data/python/bastd/ui/getremote.py @@ -4,40 +4,35 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -import ba -from bastd.ui import popup - -if TYPE_CHECKING: - pass +from bastd.ui.popup import PopupWindow +import bauiv1 as bui -class GetBSRemoteWindow(popup.PopupWindow): +class GetBSRemoteWindow(PopupWindow): """Popup telling the user about BSRemote app.""" def __init__(self) -> None: position = (0.0, 0.0) - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale scale = ( 2.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.65 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.23 ) self._transitioning_out = False self._width = 570 self._height = 350 bg_color = (0.5, 0.4, 0.6) - popup.PopupWindow.__init__( - self, + super().__init__( position=position, size=(self._width, self._height), scale=scale, bg_color=bg_color, ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self.root_widget, position=(50, self._height - 30), size=(50, 50), @@ -46,29 +41,29 @@ class GetBSRemoteWindow(popup.PopupWindow): color=bg_color, on_activate_call=self._on_cancel_press, autoselect=True, - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) - ba.imagewidget( + bui.imagewidget( parent=self.root_widget, position=(self._width * 0.5 - 110, self._height * 0.67 - 110), size=(220, 220), - texture=ba.gettexture('multiplayerExamples'), + texture=bui.gettexture('multiplayerExamples'), ) - ba.textwidget( + bui.textwidget( parent=self.root_widget, size=(0, 0), h_align='center', v_align='center', maxwidth=self._width * 0.9, position=(self._width * 0.5, 60), - text=ba.Lstr( + text=bui.Lstr( resource='remoteAppInfoShortText', subs=[ - ('${APP_NAME}', ba.Lstr(resource='titleText')), + ('${APP_NAME}', bui.Lstr(resource='titleText')), ( '${REMOTE_APP_NAME}', - ba.Lstr(resource='remote_app.app_name'), + bui.Lstr(resource='remote_app.app_name'), ), ], ), @@ -80,8 +75,8 @@ class GetBSRemoteWindow(popup.PopupWindow): def _transition_out(self) -> None: if not self._transitioning_out: self._transitioning_out = True - ba.containerwidget(edit=self.root_widget, transition='out_scale') + bui.containerwidget(edit=self.root_widget, transition='out_scale') def on_popup_cancel(self) -> None: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transition_out() diff --git a/assets/src/ba_data/python/bastd/ui/helpui.py b/src/assets/ba_data/python/bastd/ui/helpui.py similarity index 75% rename from assets/src/ba_data/python/bastd/ui/helpui.py rename to src/assets/ba_data/python/bastd/ui/helpui.py index 17baee0a..3725795c 100644 --- a/assets/src/ba_data/python/bastd/ui/helpui.py +++ b/src/assets/ba_data/python/bastd/ui/helpui.py @@ -4,26 +4,19 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -import ba -import ba.internal - -if TYPE_CHECKING: - pass +import bauiv1 as bui -class HelpWindow(ba.Window): +class HelpWindow(bui.Window): """A window providing help on how to play.""" def __init__( - self, main_menu: bool = False, origin_widget: ba.Widget | None = None + self, main_menu: bool = False, origin_widget: bui.Widget | None = None ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals - from ba.internal import get_remote_app_name - ba.set_analytics_screen('Help Window') + bui.set_analytics_screen('Help Window') # If they provided an origin-widget, scale up from that. scale_origin: tuple[float, float] | None @@ -38,94 +31,95 @@ class HelpWindow(ba.Window): self._r = 'helpWindow' - getres = ba.app.lang.get_resource + getres = bui.app.lang.get_resource self._main_menu = main_menu - uiscale = ba.app.ui.uiscale - width = 950 if uiscale is ba.UIScale.SMALL else 750 - x_offs = 100 if uiscale is ba.UIScale.SMALL else 0 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + width = 950 if uiscale is bui.UIScale.SMALL else 750 + x_offs = 100 if uiscale is bui.UIScale.SMALL else 0 height = ( 460 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 530 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 600 ) super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height), transition=transition, toolbar_visibility='menu_minimal', scale_origin_stack_offset=scale_origin, scale=( 1.77 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.25 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -30) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 15) - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else (0, 0), ) ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, - position=(0, height - (50 if uiscale is ba.UIScale.SMALL else 45)), + position=(0, height - (50 if uiscale is bui.UIScale.SMALL else 45)), size=(width, 25), - text=ba.Lstr( + text=bui.Lstr( resource=self._r + '.titleText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], + subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ), - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, h_align='center', v_align='top', ) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, - position=(44 + x_offs, 55 if uiscale is ba.UIScale.SMALL else 55), + position=(44 + x_offs, 55 if uiscale is bui.UIScale.SMALL else 55), simple_culling_v=100.0, size=( width - (88 + 2 * x_offs), - height - 120 + (5 if uiscale is ba.UIScale.SMALL else 0), + height - 120 + (5 if uiscale is bui.UIScale.SMALL else 0), ), capture_arrows=True, ) - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=self._scrollwidget, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) # ugly: create this last so it gets first dibs at touch events (since # we have it close to the scroll widget) - if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: - ba.containerwidget( + if uiscale is bui.UIScale.SMALL and bui.app.classic.ui.use_toolbars: + bui.containerwidget( edit=self._root_widget, on_cancel_call=self._close ) - ba.widget( + bui.widget( edit=self._scrollwidget, - left_widget=ba.internal.get_special_widget('back_button'), + left_widget=bui.get_special_widget('back_button'), ) else: - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=( - x_offs + (40 + 0 if uiscale is ba.UIScale.SMALL else 70), - height - (59 if uiscale is ba.UIScale.SMALL else 50), + x_offs + (40 + 0 if uiscale is bui.UIScale.SMALL else 70), + height - (59 if uiscale is bui.UIScale.SMALL else 50), ), size=(140, 60), - scale=0.7 if uiscale is ba.UIScale.SMALL else 0.8, - label=ba.Lstr(resource='backText') + scale=0.7 if uiscale is bui.UIScale.SMALL else 0.8, + label=bui.Lstr(resource='backText') if self._main_menu else 'Close', button_type='back' if self._main_menu else None, @@ -133,26 +127,26 @@ class HelpWindow(ba.Window): autoselect=True, on_activate_call=self._close, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) if self._main_menu: - ba.buttonwidget( + bui.buttonwidget( edit=btn, button_type='backSmall', size=(60, 55), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) self._sub_width = 660 self._sub_height = ( 1590 - + ba.app.lang.get_resource(self._r + '.someDaysExtraSpace') - + ba.app.lang.get_resource( + + bui.app.lang.get_resource(self._r + '.someDaysExtraSpace') + + bui.app.lang.get_resource( self._r + '.orPunchingSomethingExtraSpace' ) ) - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._sub_width, self._sub_height), background=False, @@ -163,19 +157,19 @@ class HelpWindow(ba.Window): spacing = 1.0 h = self._sub_width * 0.5 v = self._sub_height - 55 - logo_tex = ba.gettexture('logo') + logo_tex = bui.gettexture('logo') icon_buffer = 1.1 header = (0.7, 1.0, 0.7, 1.0) header2 = (0.8, 0.8, 1.0, 1.0) paragraph = (0.8, 0.8, 1.0, 1.0) - txt = ba.Lstr( + txt = bui.Lstr( resource=self._r + '.welcomeText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], + subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ).evaluate() txt_scale = 1.4 txt_maxwidth = 480 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h, v), size=(0, 0), @@ -190,13 +184,12 @@ class HelpWindow(ba.Window): ) txt_width = min( txt_maxwidth, - ba.internal.get_string_width(txt, suppress_warning=True) - * txt_scale, + bui.get_string_width(txt, suppress_warning=True) * txt_scale, ) icon_size = 70 hval2 = h - (txt_width * 0.5 + icon_size * 0.5 * icon_buffer) - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, size=(icon_size, icon_size), position=(hval2 - 0.5 * icon_size, v - 0.45 * icon_size), @@ -204,9 +197,11 @@ class HelpWindow(ba.Window): ) force_test = False - app = ba.app + app = bui.app + assert app.classic is not None if ( - app.platform == 'android' and app.subplatform == 'alibaba' + app.classic.platform == 'android' + and app.classic.subplatform == 'alibaba' ) or force_test: v -= 120.0 txtv = ( @@ -228,7 +223,7 @@ class HelpWindow(ba.Window): '\xe5\x85\xa5\xc2\xa08\xc2\xa0\xe4\xb8\xaa\xe5\xa4\x96\xe8' '\xae\xbe' ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, size=(0, 0), h_align='center', @@ -237,24 +232,24 @@ class HelpWindow(ba.Window): position=(self._sub_width * 0.5, v - 180), text=txtv, ) - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, position=(self._sub_width - 320, v - 120), size=(200, 200), - texture=ba.gettexture('aliControllerQR'), + texture=bui.gettexture('aliControllerQR'), ) - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, position=(90, v - 130), size=(210, 210), - texture=ba.gettexture('multiplayerExamples'), + texture=bui.gettexture('multiplayerExamples'), ) v -= 120.0 else: v -= spacing * 50.0 - txt = ba.Lstr(resource=self._r + '.someDaysText').evaluate() - ba.textwidget( + txt = bui.Lstr(resource=self._r + '.someDaysText').evaluate() + bui.textwidget( parent=self._subcontainer, position=(h, v), size=(0, 0), @@ -268,10 +263,10 @@ class HelpWindow(ba.Window): ) v -= spacing * 25.0 + getres(self._r + '.someDaysExtraSpace') txt_scale = 0.66 - txt = ba.Lstr( + txt = bui.Lstr( resource=self._r + '.orPunchingSomethingText' ).evaluate() - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h, v), size=(0, 0), @@ -287,11 +282,11 @@ class HelpWindow(ba.Window): self._r + '.orPunchingSomethingExtraSpace' ) txt_scale = 1.0 - txt = ba.Lstr( + txt = bui.Lstr( resource=self._r + '.canHelpText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], + subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ).evaluate() - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h, v), size=(0, 0), @@ -305,8 +300,8 @@ class HelpWindow(ba.Window): v -= spacing * 70.0 txt_scale = 1.0 - txt = ba.Lstr(resource=self._r + '.toGetTheMostText').evaluate() - ba.textwidget( + txt = bui.Lstr(resource=self._r + '.toGetTheMostText').evaluate() + bui.textwidget( parent=self._subcontainer, position=(h, v), size=(0, 0), @@ -321,9 +316,9 @@ class HelpWindow(ba.Window): v -= spacing * 40.0 txt_scale = 0.74 - txt = ba.Lstr(resource=self._r + '.friendsText').evaluate() + txt = bui.Lstr(resource=self._r + '.friendsText').evaluate() hval2 = h - 220 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(hval2, v), size=(0, 0), @@ -336,12 +331,12 @@ class HelpWindow(ba.Window): flatness=1.0, ) - txt = ba.Lstr( + txt = bui.Lstr( resource=self._r + '.friendsGoodText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], + subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ).evaluate() txt_scale = 0.7 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(hval2 + 10, v + 8), size=(0, 0), @@ -353,17 +348,17 @@ class HelpWindow(ba.Window): flatness=1.0, ) - app = ba.app + app = bui.app v -= spacing * 45.0 txt = ( - ba.Lstr(resource=self._r + '.devicesText').evaluate() + bui.Lstr(resource=self._r + '.devicesText').evaluate() if app.vr_mode - else ba.Lstr(resource=self._r + '.controllersText').evaluate() + else bui.Lstr(resource=self._r + '.controllersText').evaluate() ) txt_scale = 0.74 hval2 = h - 220 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(hval2, v), size=(0, 0), @@ -383,21 +378,21 @@ class HelpWindow(ba.Window): if app.iircade_mode else '.controllersInfoText' ) - txt = ba.Lstr( + txt = bui.Lstr( resource=self._r + infotxt, fallback_resource=self._r + '.controllersInfoText', subs=[ - ('${APP_NAME}', ba.Lstr(resource='titleText')), - ('${REMOTE_APP_NAME}', get_remote_app_name()), + ('${APP_NAME}', bui.Lstr(resource='titleText')), + ('${REMOTE_APP_NAME}', bui.get_remote_app_name()), ], ).evaluate() else: - txt = ba.Lstr( + txt = bui.Lstr( resource=self._r + '.devicesInfoText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], + subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ).evaluate() - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(hval2 + 10, v + 8), size=(0, 0), @@ -412,10 +407,10 @@ class HelpWindow(ba.Window): v -= spacing * 150.0 - txt = ba.Lstr(resource=self._r + '.controlsText').evaluate() + txt = bui.Lstr(resource=self._r + '.controlsText').evaluate() txt_scale = 1.4 txt_maxwidth = 480 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h, v), size=(0, 0), @@ -430,13 +425,12 @@ class HelpWindow(ba.Window): ) txt_width = min( txt_maxwidth, - ba.internal.get_string_width(txt, suppress_warning=True) - * txt_scale, + bui.get_string_width(txt, suppress_warning=True) * txt_scale, ) icon_size = 70 hval2 = h - (txt_width * 0.5 + icon_size * 0.5 * icon_buffer) - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, size=(icon_size, icon_size), position=(hval2 - 0.5 * icon_size, v - 0.45 * icon_size), @@ -446,11 +440,11 @@ class HelpWindow(ba.Window): v -= spacing * 45.0 txt_scale = 0.7 - txt = ba.Lstr( + txt = bui.Lstr( resource=self._r + '.controlsSubtitleText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], + subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ).evaluate() - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h, v), size=(0, 0), @@ -469,17 +463,17 @@ class HelpWindow(ba.Window): # icon_size_2 = 30 hval2 = h - sep vval2 = v - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, size=(icon_size, icon_size), position=(hval2 - 0.5 * icon_size, vval2 - 0.5 * icon_size), - texture=ba.gettexture('buttonPunch'), + texture=bui.gettexture('buttonPunch'), color=(1, 0.7, 0.3), ) txt_scale = getres(self._r + '.punchInfoTextScale') - txt = ba.Lstr(resource=self._r + '.punchInfoText').evaluate() - ba.textwidget( + txt = bui.Lstr(resource=self._r + '.punchInfoText').evaluate() + bui.textwidget( parent=self._subcontainer, position=(h - sep - 185 + 70, v + 120), size=(0, 0), @@ -493,17 +487,17 @@ class HelpWindow(ba.Window): hval2 = h + sep vval2 = v - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, size=(icon_size, icon_size), position=(hval2 - 0.5 * icon_size, vval2 - 0.5 * icon_size), - texture=ba.gettexture('buttonBomb'), + texture=bui.gettexture('buttonBomb'), color=(1, 0.3, 0.3), ) - txt = ba.Lstr(resource=self._r + '.bombInfoText').evaluate() + txt = bui.Lstr(resource=self._r + '.bombInfoText').evaluate() txt_scale = getres(self._r + '.bombInfoTextScale') - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h + sep + 50 + 60, v - 35), size=(0, 0), @@ -518,17 +512,17 @@ class HelpWindow(ba.Window): hval2 = h vval2 = v + sep - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, size=(icon_size, icon_size), position=(hval2 - 0.5 * icon_size, vval2 - 0.5 * icon_size), - texture=ba.gettexture('buttonPickUp'), + texture=bui.gettexture('buttonPickUp'), color=(0.5, 0.5, 1), ) - txtl = ba.Lstr(resource=self._r + '.pickUpInfoText') + txtl = bui.Lstr(resource=self._r + '.pickUpInfoText') txt_scale = getres(self._r + '.pickUpInfoTextScale') - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h + 60 + 120, v + sep + 50), size=(0, 0), @@ -542,17 +536,17 @@ class HelpWindow(ba.Window): hval2 = h vval2 = v - sep - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, size=(icon_size, icon_size), position=(hval2 - 0.5 * icon_size, vval2 - 0.5 * icon_size), - texture=ba.gettexture('buttonJump'), + texture=bui.gettexture('buttonJump'), color=(0.4, 1, 0.4), ) - txt = ba.Lstr(resource=self._r + '.jumpInfoText').evaluate() + txt = bui.Lstr(resource=self._r + '.jumpInfoText').evaluate() txt_scale = getres(self._r + '.jumpInfoTextScale') - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h - 250 + 75, v - sep - 15 + 30), size=(0, 0), @@ -564,9 +558,9 @@ class HelpWindow(ba.Window): v_align='top', ) - txt = ba.Lstr(resource=self._r + '.runInfoText').evaluate() + txt = bui.Lstr(resource=self._r + '.runInfoText').evaluate() txt_scale = getres(self._r + '.runInfoTextScale') - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h, v - sep - 100), size=(0, 0), @@ -581,10 +575,10 @@ class HelpWindow(ba.Window): v -= spacing * 280.0 - txt = ba.Lstr(resource=self._r + '.powerupsText').evaluate() + txt = bui.Lstr(resource=self._r + '.powerupsText').evaluate() txt_scale = 1.4 txt_maxwidth = 480 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h, v), size=(0, 0), @@ -598,12 +592,11 @@ class HelpWindow(ba.Window): ) txt_width = min( txt_maxwidth, - ba.internal.get_string_width(txt, suppress_warning=True) - * txt_scale, + bui.get_string_width(txt, suppress_warning=True) * txt_scale, ) icon_size = 70 hval2 = h - (txt_width * 0.5 + icon_size * 0.5 * icon_buffer) - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, size=(icon_size, icon_size), position=(hval2 - 0.5 * icon_size, v - 0.45 * icon_size), @@ -612,8 +605,8 @@ class HelpWindow(ba.Window): v -= spacing * 50.0 txt_scale = getres(self._r + '.powerupsSubtitleTextScale') - txt = ba.Lstr(resource=self._r + '.powerupsSubtitleText').evaluate() - ba.textwidget( + txt = bui.Lstr(resource=self._r + '.powerupsSubtitleText').evaluate() + bui.textwidget( parent=self._subcontainer, position=(h, v), size=(0, 0), @@ -638,7 +631,7 @@ class HelpWindow(ba.Window): t_big = 1.1 t_small = 0.65 - shadow_tex = ba.gettexture('shadowSharp') + shadow_tex = bui.gettexture('shadowSharp') for tex in [ 'powerupPunch', @@ -651,12 +644,12 @@ class HelpWindow(ba.Window): 'powerupLandMines', 'powerupCurse', ]: - name = ba.Lstr(resource=self._r + '.' + tex + 'NameText') - desc = ba.Lstr(resource=self._r + '.' + tex + 'DescriptionText') + name = bui.Lstr(resource=self._r + '.' + tex + 'NameText') + desc = bui.Lstr(resource=self._r + '.' + tex + 'DescriptionText') v -= spacing * 60.0 - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, size=(shadow_size, shadow_size), position=( @@ -667,16 +660,16 @@ class HelpWindow(ba.Window): color=(0, 0, 0), opacity=0.5, ) - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, size=(icon_size, icon_size), position=(h + mm1 - 0.5 * icon_size, v - 0.5 * icon_size), - texture=ba.gettexture(tex), + texture=bui.gettexture(tex), ) txt_scale = t_big txtl = name - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h + mm2, v + 3), size=(0, 0), @@ -690,7 +683,7 @@ class HelpWindow(ba.Window): ) txt_scale = t_small txtl = desc - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h + mm3, v), size=(0, 0), @@ -708,10 +701,11 @@ class HelpWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.mainmenu import MainMenuWindow - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) if self._main_menu: - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( MainMenuWindow(transition='in_left').get_root_widget() ) diff --git a/assets/src/ba_data/python/bastd/ui/iconpicker.py b/src/assets/ba_data/python/bastd/ui/iconpicker.py similarity index 78% rename from assets/src/ba_data/python/bastd/ui/iconpicker.py rename to src/assets/ba_data/python/bastd/ui/iconpicker.py index bcbac259..c33ea689 100644 --- a/assets/src/ba_data/python/bastd/ui/iconpicker.py +++ b/src/assets/ba_data/python/bastd/ui/iconpicker.py @@ -7,20 +7,19 @@ from __future__ import annotations import math from typing import TYPE_CHECKING -import ba -import ba.internal -from bastd.ui import popup +from bastd.ui.popup import PopupWindow +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Sequence -class IconPicker(popup.PopupWindow): +class IconPicker(PopupWindow): """Picker for icons.""" def __init__( self, - parent: ba.Widget, + parent: bui.Widget, position: tuple[float, float] = (0.0, 0.0), delegate: Any = None, scale: float | None = None, @@ -33,22 +32,24 @@ class IconPicker(popup.PopupWindow): del parent # unused here del tint_color # unused_here del tint2_color # unused here - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale if scale is None: scale = ( 1.85 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.65 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.23 ) self._delegate = delegate self._transitioning_out = False + assert bui.app.classic is not None self._icons = [ - ba.charstr(ba.SpecialChar.LOGO) - ] + ba.app.accounts_v1.get_purchased_icons() + bui.charstr(bui.SpecialChar.LOGO) + ] + bui.app.classic.accounts.get_purchased_icons() count = len(self._icons) columns = 4 rows = int(math.ceil(float(count) / columns)) @@ -62,7 +63,7 @@ class IconPicker(popup.PopupWindow): 1.0 / 0.95 ) * (1.0 / 0.8) self._height = self._width * ( - 0.8 if uiscale is ba.UIScale.SMALL else 1.06 + 0.8 if uiscale is bui.UIScale.SMALL else 1.06 ) self._scroll_width = self._width * 0.8 @@ -73,8 +74,7 @@ class IconPicker(popup.PopupWindow): ) # creates our _root_widget - popup.PopupWindow.__init__( - self, + super().__init__( position=position, size=(self._width, self._height), scale=scale, @@ -84,20 +84,20 @@ class IconPicker(popup.PopupWindow): focus_size=(self._scroll_width, self._scroll_height), ) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self.root_widget, size=(self._scroll_width, self._scroll_height), color=(0.55, 0.55, 0.55), highlight=False, position=self._scroll_position, ) - ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) + bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) self._sub_width = self._scroll_width * 0.95 self._sub_height = ( 5 + rows * (button_height + 2 * button_buffer_v) + 100 ) - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._sub_width, self._sub_height), background=False, @@ -111,7 +111,7 @@ class IconPicker(popup.PopupWindow): - (y + 1) * (button_height + 2 * button_buffer_v) + 0, ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._subcontainer, button_type='square', size=(button_width, button_height), @@ -119,12 +119,12 @@ class IconPicker(popup.PopupWindow): text_scale=1.2, label='', color=(0.65, 0.65, 0.65), - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._select_icon, self._icons[index] ), position=pos, ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, h_align='center', v_align='center', @@ -134,9 +134,9 @@ class IconPicker(popup.PopupWindow): text=self._icons[index], scale=1.8, ) - ba.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) + bui.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) if self._icons[index] == selected_icon: - ba.containerwidget( + bui.containerwidget( edit=self._subcontainer, selected_child=btn, visible_child=btn, @@ -147,23 +147,26 @@ class IconPicker(popup.PopupWindow): break if index >= count: break - self._get_more_icons_button = btn = ba.buttonwidget( + self._get_more_icons_button = btn = bui.buttonwidget( parent=self._subcontainer, size=(self._sub_width * 0.8, 60), position=(self._sub_width * 0.1, 30), - label=ba.Lstr(resource='editProfileWindow.getMoreIconsText'), + label=bui.Lstr(resource='editProfileWindow.getMoreIconsText'), on_activate_call=self._on_store_press, color=(0.6, 0.6, 0.6), textcolor=(0.8, 0.8, 0.8), autoselect=True, ) - ba.widget(edit=btn, show_buffer_top=30, show_buffer_bottom=30) + bui.widget(edit=btn, show_buffer_top=30, show_buffer_bottom=30) def _on_store_press(self) -> None: from bastd.ui.account import show_sign_in_prompt from bastd.ui.store.browser import StoreBrowserWindow - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._transition_out() @@ -181,8 +184,8 @@ class IconPicker(popup.PopupWindow): def _transition_out(self) -> None: if not self._transitioning_out: self._transitioning_out = True - ba.containerwidget(edit=self.root_widget, transition='out_scale') + bui.containerwidget(edit=self.root_widget, transition='out_scale') def on_popup_cancel(self) -> None: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transition_out() diff --git a/assets/src/ba_data/python/bastd/ui/kiosk.py b/src/assets/ba_data/python/bastd/ui/kiosk.py similarity index 73% rename from assets/src/ba_data/python/bastd/ui/kiosk.py rename to src/assets/ba_data/python/bastd/ui/kiosk.py index ae041f4f..ccaf388c 100644 --- a/assets/src/ba_data/python/bastd/ui/kiosk.py +++ b/src/assets/ba_data/python/bastd/ui/kiosk.py @@ -4,22 +4,19 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -import ba -import ba.internal - -if TYPE_CHECKING: - pass +import bascenev1 as bs +import bauiv1 as bui -class KioskWindow(ba.Window): +class KioskWindow(bui.Window): """Kiosk mode window.""" def __init__(self, transition: str = 'in_right'): # pylint: disable=too-many-locals, too-many-statements from bastd.ui.confirm import QuitWindow + assert bui.app.classic is not None + self._width = 720.0 self._height = 340.0 @@ -27,7 +24,7 @@ class KioskWindow(ba.Window): QuitWindow(swish=True, back=True) super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), transition=transition, on_cancel_call=_do_cancel, @@ -41,20 +38,20 @@ class KioskWindow(ba.Window): self._show_multiplayer = False # Let's reset all random player names every time we hit the main menu. - ba.internal.reset_random_player_names() + bs.reset_random_player_names() # Reset achievements too (at least locally). - ba.app.config['Achievements'] = {} + bui.app.config['Achievements'] = {} t_delay_base = 0.0 t_delay_scale = 0.0 - if not ba.app.did_menu_intro: + if not bui.app.classic.did_menu_intro: t_delay_base = 1.0 t_delay_scale = 1.0 - model_opaque = ba.getmodel('level_select_button_opaque') - model_transparent = ba.getmodel('level_select_button_transparent') - mask_tex = ba.gettexture('mapPreviewMask') + mesh_opaque = bui.getmesh('level_select_button_opaque') + mesh_transparent = bui.getmesh('level_select_button_transparent') + mask_tex = bui.gettexture('mapPreviewMask') y_extra = 130.0 + (0.0 if self._show_multiplayer else -130.0) b_width = 250.0 @@ -67,12 +64,12 @@ class KioskWindow(ba.Window): if self._show_multiplayer: tdelay = t_delay_base + t_delay_scale * 1.3 - ba.textwidget( + bui.textwidget( parent=self._root_widget, size=(0, 0), position=(self._width * 0.5, self._height + y_extra - 44), transition_delay=tdelay, - text=ba.Lstr(resource=self._r + '.singlePlayerExamplesText'), + text=bui.Lstr(resource=self._r + '.singlePlayerExamplesText'), flatness=1.0, scale=1.2, h_align='center', @@ -81,17 +78,17 @@ class KioskWindow(ba.Window): ) else: tdelay = t_delay_base + t_delay_scale * 0.7 - ba.textwidget( + bui.textwidget( parent=self._root_widget, size=(0, 0), position=(self._width * 0.5, self._height + y_extra - 34), transition_delay=tdelay, text=( - ba.Lstr( + bui.Lstr( resource='demoText', fallback_resource='mainMenu.demoMenuText', ) - if ba.app.demo_mode + if bui.app.demo_mode else 'ARCADE' ), flatness=1.0, @@ -102,87 +99,87 @@ class KioskWindow(ba.Window): ) h = self._width * 0.5 - b_space tdelay = t_delay_base + t_delay_scale * 0.7 - self._b1 = btn = ba.buttonwidget( + self._b1 = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, size=(b_width, b_height), - on_activate_call=ba.Call(self._do_game, 'easy'), + on_activate_call=bui.Call(self._do_game, 'easy'), transition_delay=tdelay, position=(h - b_width * 0.5, b_v), label='', button_type='square', ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, draw_controller=btn, transition_delay=tdelay, size=(0, 0), position=(h, label_height), maxwidth=b_width * 0.7, - text=ba.Lstr(resource=self._r + '.easyText'), + text=bui.Lstr(resource=self._r + '.easyText'), scale=1.3, h_align='center', v_align='center', ) - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, draw_controller=btn, size=(img_width, 0.5 * img_width), transition_delay=tdelay, position=(h - img_width * 0.5, img_v), - texture=ba.gettexture('doomShroomPreview'), - model_opaque=model_opaque, - model_transparent=model_transparent, + texture=bui.gettexture('doomShroomPreview'), + mesh_opaque=mesh_opaque, + mesh_transparent=mesh_transparent, mask_texture=mask_tex, ) h = self._width * 0.5 tdelay = t_delay_base + t_delay_scale * 0.65 - self._b2 = btn = ba.buttonwidget( + self._b2 = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, size=(b_width, b_height), - on_activate_call=ba.Call(self._do_game, 'medium'), + on_activate_call=bui.Call(self._do_game, 'medium'), position=(h - b_width * 0.5, b_v), label='', button_type='square', transition_delay=tdelay, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, draw_controller=btn, transition_delay=tdelay, size=(0, 0), position=(h, label_height), maxwidth=b_width * 0.7, - text=ba.Lstr(resource=self._r + '.mediumText'), + text=bui.Lstr(resource=self._r + '.mediumText'), scale=1.3, h_align='center', v_align='center', ) - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, draw_controller=btn, size=(img_width, 0.5 * img_width), transition_delay=tdelay, position=(h - img_width * 0.5, img_v), - texture=ba.gettexture('footballStadiumPreview'), - model_opaque=model_opaque, - model_transparent=model_transparent, + texture=bui.gettexture('footballStadiumPreview'), + mesh_opaque=mesh_opaque, + mesh_transparent=mesh_transparent, mask_texture=mask_tex, ) h = self._width * 0.5 + b_space tdelay = t_delay_base + t_delay_scale * 0.6 - self._b3 = btn = ba.buttonwidget( + self._b3 = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, size=(b_width, b_height), - on_activate_call=ba.Call(self._do_game, 'hard'), + on_activate_call=bui.Call(self._do_game, 'hard'), transition_delay=tdelay, position=(h - b_width * 0.5, b_v), label='', button_type='square', ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, draw_controller=btn, transition_delay=tdelay, @@ -194,31 +191,31 @@ class KioskWindow(ba.Window): h_align='center', v_align='center', ) - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, draw_controller=btn, transition_delay=tdelay, size=(img_width, 0.5 * img_width), position=(h - img_width * 0.5, img_v), - texture=ba.gettexture('courtyardPreview'), - model_opaque=model_opaque, - model_transparent=model_transparent, + texture=bui.gettexture('courtyardPreview'), + mesh_opaque=mesh_opaque, + mesh_transparent=mesh_transparent, mask_texture=mask_tex, ) - if not ba.app.did_menu_intro: - ba.app.did_menu_intro = True + if not bui.app.classic.did_menu_intro: + bui.app.classic.did_menu_intro = True - self._b4: ba.Widget | None - self._b5: ba.Widget | None - self._b6: ba.Widget | None + self._b4: bui.Widget | None + self._b5: bui.Widget | None + self._b6: bui.Widget | None if bool(False): - ba.textwidget( + bui.textwidget( parent=self._root_widget, size=(0, 0), position=(self._width * 0.5, self._height + y_extra - 44), transition_delay=tdelay, - text=ba.Lstr(resource=self._r + '.versusExamplesText'), + text=bui.Lstr(resource=self._r + '.versusExamplesText'), flatness=1.0, scale=1.2, h_align='center', @@ -227,116 +224,116 @@ class KioskWindow(ba.Window): ) h = self._width * 0.5 - b_space tdelay = t_delay_base + t_delay_scale * 0.7 - self._b4 = btn = ba.buttonwidget( + self._b4 = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, size=(b_width, b_height), - on_activate_call=ba.Call(self._do_game, 'ctf'), + on_activate_call=bui.Call(self._do_game, 'ctf'), transition_delay=tdelay, position=(h - b_width * 0.5, b_v), label='', button_type='square', ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, draw_controller=btn, transition_delay=tdelay, size=(0, 0), position=(h, label_height), maxwidth=b_width * 0.7, - text=ba.Lstr(translate=('gameNames', 'Capture the Flag')), + text=bui.Lstr(translate=('gameNames', 'Capture the Flag')), scale=1.3, h_align='center', v_align='center', ) - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, draw_controller=btn, size=(img_width, 0.5 * img_width), transition_delay=tdelay, position=(h - img_width * 0.5, img_v), - texture=ba.gettexture('bridgitPreview'), - model_opaque=model_opaque, - model_transparent=model_transparent, + texture=bui.gettexture('bridgitPreview'), + mesh_opaque=mesh_opaque, + mesh_transparent=mesh_transparent, mask_texture=mask_tex, ) h = self._width * 0.5 tdelay = t_delay_base + t_delay_scale * 0.65 - self._b5 = btn = ba.buttonwidget( + self._b5 = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, size=(b_width, b_height), - on_activate_call=ba.Call(self._do_game, 'hockey'), + on_activate_call=bui.Call(self._do_game, 'hockey'), position=(h - b_width * 0.5, b_v), label='', button_type='square', transition_delay=tdelay, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, draw_controller=btn, transition_delay=tdelay, size=(0, 0), position=(h, label_height), maxwidth=b_width * 0.7, - text=ba.Lstr(translate=('gameNames', 'Hockey')), + text=bui.Lstr(translate=('gameNames', 'Hockey')), scale=1.3, h_align='center', v_align='center', ) - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, draw_controller=btn, size=(img_width, 0.5 * img_width), transition_delay=tdelay, position=(h - img_width * 0.5, img_v), - texture=ba.gettexture('hockeyStadiumPreview'), - model_opaque=model_opaque, - model_transparent=model_transparent, + texture=bui.gettexture('hockeyStadiumPreview'), + mesh_opaque=mesh_opaque, + mesh_transparent=mesh_transparent, mask_texture=mask_tex, ) h = self._width * 0.5 + b_space tdelay = t_delay_base + t_delay_scale * 0.6 - self._b6 = btn = ba.buttonwidget( + self._b6 = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, size=(b_width, b_height), - on_activate_call=ba.Call(self._do_game, 'epic'), + on_activate_call=bui.Call(self._do_game, 'epic'), transition_delay=tdelay, position=(h - b_width * 0.5, b_v), label='', button_type='square', ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, draw_controller=btn, transition_delay=tdelay, size=(0, 0), position=(h, label_height), maxwidth=b_width * 0.7, - text=ba.Lstr(resource=self._r + '.epicModeText'), + text=bui.Lstr(resource=self._r + '.epicModeText'), scale=1.3, h_align='center', v_align='center', ) - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, draw_controller=btn, transition_delay=tdelay, size=(img_width, 0.5 * img_width), position=(h - img_width * 0.5, img_v), - texture=ba.gettexture('tipTopPreview'), - model_opaque=model_opaque, - model_transparent=model_transparent, + texture=bui.gettexture('tipTopPreview'), + mesh_opaque=mesh_opaque, + mesh_transparent=mesh_transparent, mask_texture=mask_tex, ) else: self._b4 = self._b5 = self._b6 = None - self._b7: ba.Widget | None - if ba.app.arcade_mode: - self._b7 = ba.buttonwidget( + self._b7: bui.Widget | None + if bui.app.arcade_mode: + self._b7 = bui.buttonwidget( parent=self._root_widget, autoselect=True, size=(b_width, 50), @@ -345,23 +342,21 @@ class KioskWindow(ba.Window): scale=0.5, position=(self._width * 0.5 - 60.0, b_v - 70.0), transition_delay=tdelay, - label=ba.Lstr(resource=self._r + '.fullMenuText'), + label=bui.Lstr(resource=self._r + '.fullMenuText'), on_activate_call=self._do_full_menu, ) else: self._b7 = None self._restore_state() self._update() - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - timetype=ba.TimeType.REAL, - repeat=True, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) def _restore_state(self) -> None: - sel_name = ba.app.ui.window_states.get(type(self)) - sel: ba.Widget | None + assert bui.app.classic is not None + sel_name = bui.app.classic.ui.window_states.get(type(self)) + sel: bui.Widget | None if sel_name == 'b1': sel = self._b1 elif sel_name == 'b2': @@ -379,7 +374,7 @@ class KioskWindow(ba.Window): else: sel = self._b1 if sel: - ba.containerwidget(edit=self._root_widget, selected_child=sel) + bui.containerwidget(edit=self._root_widget, selected_child=sel) def _save_state(self) -> None: sel = self._root_widget.get_selected_child() @@ -399,24 +394,29 @@ class KioskWindow(ba.Window): sel_name = 'b7' else: sel_name = 'b1' - ba.app.ui.window_states[type(self)] = sel_name + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = sel_name def _update(self) -> None: + plus = bui.app.plus + assert plus is not None + # Kiosk-mode is designed to be used signed-out... try for force # the issue. - if ba.internal.get_v1_account_state() == 'signed_in': + if plus.get_v1_account_state() == 'signed_in': # _bs.sign_out() # FIXME: Try to delete player profiles here too. pass else: # Also make sure there's no player profiles. - appconfig = ba.app.config + appconfig = bui.app.config appconfig['Player Profiles'] = {} def _do_game(self, mode: str) -> None: + assert bui.app.classic is not None self._save_state() if mode in ['epic', 'ctf', 'hockey']: - appconfig = ba.app.config + appconfig = bui.app.config if 'Team Tournament Playlists' not in appconfig: appconfig['Team Tournament Playlists'] = {} if 'Free-for-All Playlists' not in appconfig: @@ -436,13 +436,11 @@ class KioskWindow(ba.Window): } ] appconfig['Free-for-All Playlist Selection'] = 'Just Epic Elim' - ba.internal.fade_screen( + bui.fade_screen( False, - endcall=ba.Call( - ba.pushcall, - ba.Call( - ba.internal.new_host_session, ba.FreeForAllSession - ), + endcall=bui.Call( + bui.pushcall, + bui.Call(bs.new_host_session, bs.FreeForAllSession), ), ) else: @@ -477,16 +475,14 @@ class KioskWindow(ba.Window): appconfig[ 'Team Tournament Playlist Selection' ] = 'Just Hockey' - ba.internal.fade_screen( + bui.fade_screen( False, - endcall=ba.Call( - ba.pushcall, - ba.Call( - ba.internal.new_host_session, ba.DualTeamSession - ), + endcall=bui.Call( + bui.pushcall, + bui.Call(bs.new_host_session, bs.DualTeamSession), ), ) - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.containerwidget(edit=self._root_widget, transition='out_left') return game = ( @@ -496,16 +492,20 @@ class KioskWindow(ba.Window): if mode == 'medium' else 'Easy:Uber Onslaught' ) - cfg = ba.app.config + cfg = bui.app.config cfg['Selected Coop Game'] = game cfg.commit() - if ba.app.launch_coop_game(game, force=True): - ba.containerwidget(edit=self._root_widget, transition='out_left') + if bui.app.classic.launch_coop_game(game, force=True): + bui.containerwidget(edit=self._root_widget, transition='out_left') def _do_full_menu(self) -> None: from bastd.ui.mainmenu import MainMenuWindow + assert bui.app.classic is not None + self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.did_menu_intro = True # prevent delayed transition-in - ba.app.ui.set_main_menu_window(MainMenuWindow().get_root_widget()) + bui.containerwidget(edit=self._root_widget, transition='out_left') + bui.app.classic.did_menu_intro = True # prevent delayed transition-in + bui.app.classic.ui.set_main_menu_window( + MainMenuWindow().get_root_widget() + ) diff --git a/assets/src/ba_data/python/bastd/ui/league/__init__.py b/src/assets/ba_data/python/bastd/ui/league/__init__.py similarity index 100% rename from assets/src/ba_data/python/bastd/ui/league/__init__.py rename to src/assets/ba_data/python/bastd/ui/league/__init__.py diff --git a/assets/src/ba_data/python/bastd/ui/league/rankbutton.py b/src/assets/ba_data/python/bastd/ui/league/rankbutton.py similarity index 76% rename from assets/src/ba_data/python/bastd/ui/league/rankbutton.py rename to src/assets/ba_data/python/bastd/ui/league/rankbutton.py index ce8166ac..9f31ad00 100644 --- a/assets/src/ba_data/python/bastd/ui/league/rankbutton.py +++ b/src/assets/ba_data/python/bastd/ui/league/rankbutton.py @@ -4,10 +4,10 @@ from __future__ import annotations +import logging from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable @@ -18,7 +18,7 @@ class LeagueRankButton: def __init__( self, - parent: ba.Widget, + parent: bui.Widget, position: tuple[float, float], size: tuple[float, float], scale: float, @@ -29,10 +29,10 @@ class LeagueRankButton: smooth_update_delay: float | None = None, ): if on_activate_call is None: - on_activate_call = ba.WeakCall(self._default_on_activate_call) + on_activate_call = bui.WeakCall(self._default_on_activate_call) self._on_activate_call = on_activate_call if smooth_update_delay is None: - smooth_update_delay = 1000 + smooth_update_delay = 1.0 self._smooth_update_delay = smooth_update_delay self._size = size self._scale = scale @@ -46,7 +46,7 @@ class LeagueRankButton: self._parent = parent self._position: tuple[float, float] = (0.0, 0.0) - self._button = ba.buttonwidget( + self._button = bui.buttonwidget( parent=parent, size=size, label='', @@ -58,14 +58,14 @@ class LeagueRankButton: color=color, ) - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=parent, size=(0, 0), draw_controller=self._button, h_align='center', v_align='center', maxwidth=size[0] * scale * 0.85, - text=ba.Lstr( + text=bui.Lstr( resource='league.leagueRankText', fallback_resource='coopSelectWindow.powerRankingText', ), @@ -76,7 +76,7 @@ class LeagueRankButton: transition_delay=transition_delay, ) - self._value_text = ba.textwidget( + self._value_text = bui.textwidget( parent=parent, size=(0, 0), h_align='center', @@ -90,50 +90,49 @@ class LeagueRankButton: color=textcolor, ) + plus = bui.app.plus + assert plus is not None + self._smooth_percent: float | None = None self._percent: int | None = None self._smooth_rank: float | None = None self._rank: int | None = None - self._ticking_node: ba.Node | None = None + self._ticking_sound: bui.Sound | None = None self._smooth_increase_speed = 1.0 self._league: str | None = None self._improvement_text: str | None = None - self._smooth_update_timer: ba.Timer | None = None + self._smooth_update_timer: bui.AppTimer | None = None # Take note of our account state; we'll refresh later if this changes. - self._account_state_num = ba.internal.get_v1_account_state_num() + self._account_state_num = plus.get_v1_account_state_num() self._last_power_ranking_query_time: float | None = None self._doing_power_ranking_query = False self.set_position(position) self._bg_flash = False - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - timetype=ba.TimeType.REAL, - repeat=True, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) self._update() # If we've got cached power-ranking data already, apply it. - data = ba.app.accounts_v1.get_cached_league_rank_data() + assert bui.app.classic is not None + data = bui.app.classic.accounts.get_cached_league_rank_data() if data is not None: self._update_for_league_rank_data(data) def _on_activate(self) -> None: - ba.internal.increment_analytics_count('League rank button press') + bui.increment_analytics_count('League rank button press') self._on_activate_call() def __del__(self) -> None: - if self._ticking_node is not None: - self._ticking_node.delete() + if self._ticking_sound is not None: + self._ticking_sound.stop() + self._ticking_sound = None def _start_smooth_update(self) -> None: - self._smooth_update_timer = ba.Timer( - 0.05, - ba.WeakCall(self._smooth_update), - repeat=True, - timetype=ba.TimeType.REAL, + self._smooth_update_timer = bui.AppTimer( + 0.05, bui.WeakCall(self._smooth_update), repeat=True ) def _smooth_update(self) -> None: @@ -142,15 +141,9 @@ class LeagueRankButton: try: if not self._button: return - if self._ticking_node is None: - with ba.Context('ui'): - self._ticking_node = ba.newnode( - 'sound', - attrs={ - 'sound': ba.getsound('scoreIncrease'), - 'positional': False, - }, - ) + if self._ticking_sound is None: + self._ticking_sound = bui.getsound('scoreIncrease') + self._ticking_sound.play() self._bg_flash = not self._bg_flash color_used = ( (self._color[0] * 2, self._color[1] * 2, self._color[2] * 2) @@ -180,12 +173,12 @@ class LeagueRankButton: color_used = self._color textcolor_used = self._textcolor self._smooth_update_timer = None - if self._ticking_node is not None: - self._ticking_node.delete() - self._ticking_node = None - ba.playsound(ba.getsound('cashRegister2')) + if self._ticking_sound is not None: + self._ticking_sound.stop() + self._ticking_sound = None + bui.getsound('cashRegister2').play() assert self._improvement_text is not None - diff_text = ba.textwidget( + diff_text = bui.textwidget( parent=self._parent, size=(0, 0), h_align='center', @@ -201,19 +194,15 @@ class LeagueRankButton: scale=self._scale * 0.7, ) - def safe_delete(widget: ba.Widget) -> None: + def safe_delete(widget: bui.Widget) -> None: if widget: widget.delete() - ba.timer( - 2.0, - ba.Call(safe_delete, diff_text), - timetype=ba.TimeType.REAL, - ) - status_text: str | ba.Lstr + bui.apptimer(2.0, bui.Call(safe_delete, diff_text)) + status_text: str | bui.Lstr if self._rank is not None: assert self._smooth_rank is not None - status_text = ba.Lstr( + status_text = bui.Lstr( resource='numberText', subs=[('${NUMBER}', str(int(self._smooth_rank)))], ) @@ -221,29 +210,32 @@ class LeagueRankButton: status_text = str(int(self._smooth_percent)) + '%' else: status_text = '-' - ba.textwidget( + bui.textwidget( edit=self._value_text, text=status_text, color=textcolor_used ) - ba.textwidget(edit=self._title_text, color=header_color_used) - ba.buttonwidget(edit=self._button, color=color_used) + bui.textwidget(edit=self._title_text, color=header_color_used) + bui.buttonwidget(edit=self._button, color=color_used) except Exception: - ba.print_exception('Error doing smooth update.') + logging.exception('Error doing smooth update.') self._smooth_update_timer = None def _update_for_league_rank_data(self, data: dict[str, Any] | None) -> None: # pylint: disable=too-many-branches # pylint: disable=too-many-statements + plus = bui.app.plus + assert plus is not None + # If our button has died, ignore. if not self._button: return - status_text: str | ba.Lstr + status_text: str | bui.Lstr in_top = data is not None and data['rank'] is not None do_percent = False - if data is None or ba.internal.get_v1_account_state() != 'signed_in': + if data is None or plus.get_v1_account_state() != 'signed_in': self._percent = self._rank = None status_text = '-' elif in_top: @@ -261,7 +253,7 @@ class LeagueRankButton: or self._rank > int(self._smooth_rank) ): self._smooth_rank = float(self._rank) - status_text = ba.Lstr( + status_text = bui.Lstr( resource='numberText', subs=[('${NUMBER}', str(int(self._smooth_rank)))], ) @@ -271,7 +263,10 @@ class LeagueRankButton: self._percent = self._rank = None status_text = '-' else: - our_points = ba.app.accounts_v1.get_league_rank_points(data) + assert bui.app.classic is not None + our_points = ( + bui.app.classic.accounts.get_league_rank_points(data) + ) progress = float(our_points) / data['scores'][-1][1] self._percent = int(progress * 100.0) self._rank = None @@ -290,7 +285,7 @@ class LeagueRankButton: status_text = str(int(self._smooth_percent)) + '%' except Exception: - ba.print_exception('Error updating power ranking.') + logging.exception('Error updating power ranking.') self._percent = self._rank = None status_text = '-' @@ -313,11 +308,9 @@ class LeagueRankButton: else: self._smooth_increase_speed = diff / 40.0 self._smooth_increase_speed = max(0.4, self._smooth_increase_speed) - ba.timer( + bui.apptimer( self._smooth_update_delay, - ba.WeakCall(self._start_smooth_update), - timetype=ba.TimeType.REAL, - timeformat=ba.TimeFormat.MILLISECONDS, + bui.WeakCall(self._start_smooth_update), ) if ( @@ -329,52 +322,55 @@ class LeagueRankButton: (int(self._percent) - int(self._smooth_percent)) ) self._smooth_increase_speed = 0.3 - ba.timer( + bui.apptimer( self._smooth_update_delay, - ba.WeakCall(self._start_smooth_update), - timetype=ba.TimeType.REAL, - timeformat=ba.TimeFormat.MILLISECONDS, + bui.WeakCall(self._start_smooth_update), ) if do_percent: - ba.textwidget( + bui.textwidget( edit=self._title_text, - text=ba.Lstr(resource='coopSelectWindow.toRankedText'), + text=bui.Lstr(resource='coopSelectWindow.toRankedText'), ) else: try: assert data is not None - txt = ba.Lstr( + txt = bui.Lstr( resource='league.leagueFullText', subs=[ ( '${NAME}', - ba.Lstr(translate=('leagueNames', data['l']['n'])), + bui.Lstr(translate=('leagueNames', data['l']['n'])), ), ], ) t_color = data['l']['c'] except Exception: - txt = ba.Lstr( + txt = bui.Lstr( resource='league.leagueRankText', fallback_resource='coopSelectWindow.powerRankingText', ) - t_color = ba.app.ui.title_color - ba.textwidget(edit=self._title_text, text=txt, color=t_color) - ba.textwidget(edit=self._value_text, text=status_text) + assert bui.app.classic is not None + t_color = bui.app.classic.ui.title_color + bui.textwidget(edit=self._title_text, text=txt, color=t_color) + bui.textwidget(edit=self._value_text, text=status_text) def _on_power_ranking_query_response( self, data: dict[str, Any] | None ) -> None: self._doing_power_ranking_query = False - ba.app.accounts_v1.cache_league_rank_data(data) + assert bui.app.classic is not None + bui.app.classic.accounts.cache_league_rank_data(data) self._update_for_league_rank_data(data) def _update(self) -> None: - cur_time = ba.time(ba.TimeType.REAL) + cur_time = bui.apptime() + + plus = bui.app.plus + assert plus is not None # If our account state has changed, refresh our UI. - account_state_num = ba.internal.get_v1_account_state_num() + account_state_num = plus.get_v1_account_state_num() if account_state_num != self._account_state_num: self._account_state_num = account_state_num @@ -390,8 +386,8 @@ class LeagueRankButton: ): self._last_power_ranking_query_time = cur_time self._doing_power_ranking_query = True - ba.internal.power_ranking_query( - callback=ba.WeakCall(self._on_power_ranking_query_response) + plus.power_ranking_query( + callback=bui.WeakCall(self._on_power_ranking_query_response) ) def _default_on_activate_call(self) -> None: @@ -405,15 +401,15 @@ class LeagueRankButton: self._position = position if not self._button: return - ba.buttonwidget(edit=self._button, position=self._position) - ba.textwidget( + bui.buttonwidget(edit=self._button, position=self._position) + bui.textwidget( edit=self._title_text, position=( self._position[0] + self._size[0] * 0.5 * self._scale, self._position[1] + self._size[1] * 0.82 * self._scale, ), ) - ba.textwidget( + bui.textwidget( edit=self._value_text, position=( self._position[0] + self._size[0] * 0.5 * self._scale, @@ -421,6 +417,6 @@ class LeagueRankButton: ), ) - def get_button(self) -> ba.Widget: - """Return the underlying button ba.Widget>""" + def get_button(self) -> bui.Widget: + """Return the underlying button bui.Widget>""" return self._button diff --git a/assets/src/ba_data/python/bastd/ui/league/rankwindow.py b/src/assets/ba_data/python/bastd/ui/league/rankwindow.py similarity index 73% rename from assets/src/ba_data/python/bastd/ui/league/rankwindow.py rename to src/assets/ba_data/python/bastd/ui/league/rankwindow.py index 3bf2589b..53c6ceb8 100644 --- a/assets/src/ba_data/python/bastd/ui/league/rankwindow.py +++ b/src/assets/ba_data/python/bastd/ui/league/rankwindow.py @@ -6,30 +6,46 @@ from __future__ import annotations import copy +import logging from typing import TYPE_CHECKING -import ba -import ba.internal -from bastd.ui import popup as popup_ui +from bastd.ui.popup import PopupMenu +import bauiv1 as bui if TYPE_CHECKING: from typing import Any -class LeagueRankWindow(ba.Window): +class LeagueRankWindow(bui.Window): """Window for showing league rank.""" def __init__( self, transition: str = 'in_right', modal: bool = False, - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): - ba.set_analytics_screen('League Rank Window') + # pylint: disable=too-many-statements + plus = bui.app.plus + assert plus is not None + + bui.set_analytics_screen('League Rank Window') self._league_rank_data: dict[str, Any] | None = None self._modal = modal + self._power_ranking_achievements_button: bui.Widget | None = None + self._pro_mult_button: bui.Widget | None = None + self._power_ranking_trophies_button: bui.Widget | None = None + self._league_title_text: bui.Widget | None = None + self._league_text: bui.Widget | None = None + self._league_number_text: bui.Widget | None = None + self._your_power_ranking_text: bui.Widget | None = None + self._season_ends_text: bui.Widget | None = None + self._power_ranking_rank_text: bui.Widget | None = None + self._to_ranked_text: bui.Widget | None = None + self._trophy_counts_reset_text: bui.Widget | None = None + # If they provided an origin-widget, scale up from that. scale_origin: tuple[float, float] | None if origin_widget is not None: @@ -40,19 +56,20 @@ class LeagueRankWindow(ba.Window): self._transition_out = 'out_right' scale_origin = None - uiscale = ba.app.ui.uiscale - self._width = 1320 if uiscale is ba.UIScale.SMALL else 1120 - x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self._width = 1320 if uiscale is bui.UIScale.SMALL else 1120 + x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 657 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 710 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 800 ) self._r = 'coopSelectWindow' - self._rdict = ba.app.lang.get_resource(self._r) - top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + self._rdict = bui.app.lang.get_resource(self._r) + top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 self._league_url_arg = '' @@ -60,77 +77,77 @@ class LeagueRankWindow(ba.Window): self._can_do_more_button = True super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), stack_offset=(0, -15) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 10) - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else (0, 0), transition=transition, scale_origin_stack_offset=scale_origin, scale=( 1.2 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 0.93 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 0.8 ), ) ) - self._back_button = btn = ba.buttonwidget( + self._back_button = btn = bui.buttonwidget( parent=self._root_widget, position=( 75 + x_inset, - self._height - 87 - (4 if uiscale is ba.UIScale.SMALL else 0), + self._height - 87 - (4 if uiscale is bui.UIScale.SMALL else 0), ), size=(120, 60), scale=1.2, autoselect=True, - label=ba.Lstr(resource='doneText' if self._modal else 'backText'), + label=bui.Lstr(resource='doneText' if self._modal else 'backText'), button_type=None if self._modal else 'back', on_activate_call=self._back, ) - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 56), size=(0, 0), - text=ba.Lstr( + text=bui.Lstr( resource='league.leagueRankText', fallback_resource='coopSelectWindow.powerRankingText', ), h_align='center', - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, scale=1.4, maxwidth=600, v_align='center', ) - ba.buttonwidget( + bui.buttonwidget( edit=btn, button_type='backSmall', position=( 75 + x_inset, - self._height - 87 - (2 if uiscale is ba.UIScale.SMALL else 0), + self._height - 87 - (2 if uiscale is bui.UIScale.SMALL else 0), ), size=(60, 55), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) self._scroll_width = self._width - (130 + 2 * x_inset) self._scroll_height = self._height - 160 - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, highlight=False, position=(65 + x_inset, 70), size=(self._scroll_width, self._scroll_height), center_small_content=True, ) - ba.widget(edit=self._scrollwidget, autoselect=True) - ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) - ba.containerwidget( + bui.widget(edit=self._scrollwidget, autoselect=True) + bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) + bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button, selected_child=self._back_button, @@ -139,33 +156,31 @@ class LeagueRankWindow(ba.Window): self._last_power_ranking_query_time: float | None = None self._doing_power_ranking_query = False - self._subcontainer: ba.Widget | None = None + self._subcontainer: bui.Widget | None = None self._subcontainerwidth = 800 self._subcontainerheight = 483 - self._power_ranking_score_widgets: list[ba.Widget] = [] + self._power_ranking_score_widgets: list[bui.Widget] = [] - self._season_popup_menu: popup_ui.PopupMenu | None = None + self._season_popup_menu: PopupMenu | None = None self._requested_season: str | None = None self._season: str | None = None # take note of our account state; we'll refresh later if this changes - self._account_state = ba.internal.get_v1_account_state() + self._account_state = plus.get_v1_account_state() self._refresh() self._restore_state() # if we've got cached power-ranking data already, display it - info = ba.app.accounts_v1.get_cached_league_rank_data() + assert bui.app.classic is not None + info = bui.app.classic.accounts.get_cached_league_rank_data() if info is not None: self._update_for_league_rank_data(info) - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - timetype=ba.TimeType.REAL, - repeat=True, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) - self._update(show=(info is None)) + self._update(show=info is None) def _on_achievements_press(self) -> None: from bastd.ui import achievements @@ -174,23 +189,27 @@ class LeagueRankWindow(ba.Window): # (we currently don't keep specific achievement data for old seasons) if self._season == 'a' or self._is_current_season: prab = self._power_ranking_achievements_button + assert prab is not None achievements.AchievementsWindow( position=prab.get_screen_space_center() ) else: - ba.screenmessage( - ba.Lstr( + bui.screenmessage( + bui.Lstr( resource='achievementsUnavailableForOldSeasonsText', fallback_resource='unavailableText', ), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() def _on_activity_mult_press(self) -> None: from bastd.ui import confirm - txt = ba.Lstr( + plus = bui.app.plus + assert plus is not None + + txt = bui.Lstr( resource='coopSelectWindow.activenessAllTimeInfoText' if self._season == 'a' else 'coopSelectWindow.activenessInfoText', @@ -198,9 +217,7 @@ class LeagueRankWindow(ba.Window): ( '${MAX}', str( - ba.internal.get_v1_account_misc_read_val( - 'activenessMax', 1.0 - ) + plus.get_v1_account_misc_read_val('activenessMax', 1.0) ), ) ], @@ -216,22 +233,25 @@ class LeagueRankWindow(ba.Window): def _on_pro_mult_press(self) -> None: from bastd.ui import confirm - txt = ba.Lstr( + plus = bui.app.plus + assert plus is not None + + txt = bui.Lstr( resource='coopSelectWindow.proMultInfoText', subs=[ ( '${PERCENT}', str( - ba.internal.get_v1_account_misc_read_val( + plus.get_v1_account_misc_read_val( 'proPowerRankingBoost', 10 ) ), ), ( '${PRO}', - ba.Lstr( + bui.Lstr( resource='store.bombSquadProNameText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], + subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ), ), ], @@ -250,21 +270,25 @@ class LeagueRankWindow(ba.Window): info = self._league_rank_data if info is not None: prtb = self._power_ranking_trophies_button + assert prtb is not None TrophiesWindow( position=prtb.get_screen_space_center(), data=info, ) else: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() def _on_power_ranking_query_response( self, data: dict[str, Any] | None ) -> None: self._doing_power_ranking_query = False - # important: *only* cache this if we requested the current season.. + + # Important: *only* cache this if we requested the current season. if data is not None and data.get('s', None) is None: - ba.app.accounts_v1.cache_league_rank_data(data) - # always store a copy locally though (even for other seasons) + assert bui.app.classic is not None + bui.app.classic.accounts.cache_league_rank_data(data) + + # Always store a copy locally though (even for other seasons). self._league_rank_data = copy.deepcopy(data) self._update_for_league_rank_data(data) @@ -272,59 +296,64 @@ class LeagueRankWindow(ba.Window): pass def _update(self, show: bool = False) -> None: + plus = bui.app.plus + assert plus is not None - cur_time = ba.time(ba.TimeType.REAL) + cur_time = bui.apptime() - # if our account state has changed, refresh our UI - account_state = ba.internal.get_v1_account_state() + # If our account state has changed, refresh our UI. + account_state = plus.get_v1_account_state() if account_state != self._account_state: self._account_state = account_state self._save_state() self._refresh() - # and power ranking too... + # And power ranking too. if not self._doing_power_ranking_query: self._last_power_ranking_query_time = None - # send off a new power-ranking query if its been long enough or our - # requested season has changed or whatnot.. + # Send off a new power-ranking query if its been long enough or our + # requested season has changed or whatnot. if not self._doing_power_ranking_query and ( self._last_power_ranking_query_time is None or cur_time - self._last_power_ranking_query_time > 30.0 ): try: if show: - ba.textwidget(edit=self._league_title_text, text='') - ba.textwidget(edit=self._league_text, text='') - ba.textwidget(edit=self._league_number_text, text='') - ba.textwidget( + bui.textwidget(edit=self._league_title_text, text='') + bui.textwidget(edit=self._league_text, text='') + bui.textwidget(edit=self._league_number_text, text='') + bui.textwidget( edit=self._your_power_ranking_text, - text=ba.Lstr( + text=bui.Lstr( value='${A}...', - subs=[('${A}', ba.Lstr(resource='loadingText'))], + subs=[('${A}', bui.Lstr(resource='loadingText'))], ), ) - ba.textwidget(edit=self._to_ranked_text, text='') - ba.textwidget(edit=self._power_ranking_rank_text, text='') - ba.textwidget(edit=self._season_ends_text, text='') - ba.textwidget(edit=self._trophy_counts_reset_text, text='') + bui.textwidget(edit=self._to_ranked_text, text='') + bui.textwidget(edit=self._power_ranking_rank_text, text='') + bui.textwidget(edit=self._season_ends_text, text='') + bui.textwidget(edit=self._trophy_counts_reset_text, text='') except Exception: - ba.print_exception('Error showing updated rank info.') + logging.exception('Error showing updated rank info.') self._last_power_ranking_query_time = cur_time self._doing_power_ranking_query = True - ba.internal.power_ranking_query( + plus.power_ranking_query( season=self._requested_season, - callback=ba.WeakCall(self._on_power_ranking_query_response), + callback=bui.WeakCall(self._on_power_ranking_query_response), ) def _refresh(self) -> None: # pylint: disable=too-many-statements + plus = bui.app.plus + assert plus is not None + # (re)create the sub-container if need be.. if self._subcontainer is not None: self._subcontainer.delete() - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._subcontainerwidth, self._subcontainerheight), background=False, @@ -345,13 +374,13 @@ class LeagueRankWindow(ba.Window): tally_maxwidth = 120 v2 -= 70 - ba.textwidget( + bui.textwidget( parent=w_parent, position=(h2 - 60, v2 + 106), size=(0, 0), flatness=1.0, shadow=0.0, - text=ba.Lstr(resource='coopSelectWindow.pointsText'), + text=bui.Lstr(resource='coopSelectWindow.pointsText'), h_align='left', v_align='center', scale=0.8, @@ -359,13 +388,13 @@ class LeagueRankWindow(ba.Window): maxwidth=200, ) - self._power_ranking_achievements_button = ba.buttonwidget( + self._power_ranking_achievements_button = bui.buttonwidget( parent=w_parent, position=(h2 - 60, v2 + 10), size=(200, 80), - icon=ba.gettexture('achievementsIcon'), + icon=bui.gettexture('achievementsIcon'), autoselect=True, - on_activate_call=ba.WeakCall(self._on_achievements_press), + on_activate_call=bui.WeakCall(self._on_achievements_press), up_widget=self._back_button, left_widget=self._back_button, color=(0.5, 0.5, 0.6), @@ -373,7 +402,7 @@ class LeagueRankWindow(ba.Window): label='', ) - self._power_ranking_achievement_total_text = ba.textwidget( + self._power_ranking_achievement_total_text = bui.textwidget( parent=w_parent, position=(h2 + h_offs_tally, v2 + 45), size=(0, 0), @@ -389,19 +418,19 @@ class LeagueRankWindow(ba.Window): v2 -= 80 - self._power_ranking_trophies_button = ba.buttonwidget( + self._power_ranking_trophies_button = bui.buttonwidget( parent=w_parent, position=(h2 - 60, v2 + 10), size=(200, 80), - icon=ba.gettexture('medalSilver'), + icon=bui.gettexture('medalSilver'), autoselect=True, - on_activate_call=ba.WeakCall(self._on_trophies_press), + on_activate_call=bui.WeakCall(self._on_trophies_press), left_widget=self._back_button, color=(0.5, 0.5, 0.6), textcolor=(0.7, 0.7, 0.8), label='', ) - self._power_ranking_trophies_total_text = ba.textwidget( + self._power_ranking_trophies_total_text = bui.textwidget( parent=w_parent, position=(h2 + h_offs_tally, v2 + 45), size=(0, 0), @@ -417,13 +446,13 @@ class LeagueRankWindow(ba.Window): v2 -= 100 - ba.textwidget( + bui.textwidget( parent=w_parent, position=(h2 - 60, v2 + 86), size=(0, 0), flatness=1.0, shadow=0.0, - text=ba.Lstr(resource='coopSelectWindow.multipliersText'), + text=bui.Lstr(resource='coopSelectWindow.multipliersText'), h_align='left', v_align='center', scale=0.8, @@ -431,23 +460,23 @@ class LeagueRankWindow(ba.Window): maxwidth=200, ) - self._activity_mult_button: ba.Widget | None - if ba.internal.get_v1_account_misc_read_val('act', False): - self._activity_mult_button = ba.buttonwidget( + self._activity_mult_button: bui.Widget | None + if plus.get_v1_account_misc_read_val('act', False): + self._activity_mult_button = bui.buttonwidget( parent=w_parent, position=(h2 - 60, v2 + 10), size=(200, 60), - icon=ba.gettexture('heart'), + icon=bui.gettexture('heart'), icon_color=(0.5, 0, 0.5), - label=ba.Lstr(resource='coopSelectWindow.activityText'), + label=bui.Lstr(resource='coopSelectWindow.activityText'), autoselect=True, - on_activate_call=ba.WeakCall(self._on_activity_mult_press), + on_activate_call=bui.WeakCall(self._on_activity_mult_press), left_widget=self._back_button, color=(0.5, 0.5, 0.6), textcolor=(0.7, 0.7, 0.8), ) - self._activity_mult_text = ba.textwidget( + self._activity_mult_text = bui.textwidget( parent=w_parent, position=(h2 + h_offs_tally, v2 + 40), size=(0, 0), @@ -464,24 +493,24 @@ class LeagueRankWindow(ba.Window): else: self._activity_mult_button = None - self._pro_mult_button = ba.buttonwidget( + self._pro_mult_button = bui.buttonwidget( parent=w_parent, position=(h2 - 60, v2 + 10), size=(200, 60), - icon=ba.gettexture('logo'), + icon=bui.gettexture('logo'), icon_color=(0.3, 0, 0.3), - label=ba.Lstr( + label=bui.Lstr( resource='store.bombSquadProNameText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], + subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ), autoselect=True, - on_activate_call=ba.WeakCall(self._on_pro_mult_press), + on_activate_call=bui.WeakCall(self._on_pro_mult_press), left_widget=self._back_button, color=(0.5, 0.5, 0.6), textcolor=(0.7, 0.7, 0.8), ) - self._pro_mult_text = ba.textwidget( + self._pro_mult_text = bui.textwidget( parent=w_parent, position=(h2 + h_offs_tally, v2 + 40), size=(0, 0), @@ -497,20 +526,20 @@ class LeagueRankWindow(ba.Window): v2 -= 30 v2 -= spc - ba.textwidget( + bui.textwidget( parent=w_parent, position=(h2 + h_offs_tally - 10 - 40, v2 + 35), size=(0, 0), flatness=1.0, shadow=0.0, - text=ba.Lstr(resource='finalScoreText'), + text=bui.Lstr(resource='finalScoreText'), h_align='right', v_align='center', scale=0.9, color=worth_color, maxwidth=150, ) - self._power_ranking_total_text = ba.textwidget( + self._power_ranking_total_text = bui.textwidget( parent=w_parent, position=(h2 + h_offs_tally - 40, v2 + 35), size=(0, 0), @@ -524,13 +553,13 @@ class LeagueRankWindow(ba.Window): maxwidth=tally_maxwidth, ) - self._season_show_text = ba.textwidget( + self._season_show_text = bui.textwidget( parent=w_parent, position=(390 - 15, v - 20), size=(0, 0), color=(0.6, 0.6, 0.7), maxwidth=200, - text=ba.Lstr(resource='showText'), + text=bui.Lstr(resource='showText'), h_align='right', v_align='center', scale=0.8, @@ -538,7 +567,7 @@ class LeagueRankWindow(ba.Window): flatness=1.0, ) - self._league_title_text = ba.textwidget( + self._league_title_text = bui.textwidget( parent=w_parent, position=(470, v - 97), size=(0, 0), @@ -554,7 +583,7 @@ class LeagueRankWindow(ba.Window): self._league_text_scale = 1.8 self._league_text_maxwidth = 210 - self._league_text = ba.textwidget( + self._league_text = bui.textwidget( parent=w_parent, position=(470, v - 140), size=(0, 0), @@ -568,7 +597,7 @@ class LeagueRankWindow(ba.Window): flatness=1.0, ) self._league_number_base_pos = (470, v - 140) - self._league_number_text = ba.textwidget( + self._league_number_text = bui.textwidget( parent=w_parent, position=(470, v - 140), size=(0, 0), @@ -582,7 +611,7 @@ class LeagueRankWindow(ba.Window): flatness=1.0, ) - self._your_power_ranking_text = ba.textwidget( + self._your_power_ranking_text = bui.textwidget( parent=w_parent, position=(470, v - 142 - 70), size=(0, 0), @@ -596,7 +625,7 @@ class LeagueRankWindow(ba.Window): flatness=1.0, ) - self._to_ranked_text = ba.textwidget( + self._to_ranked_text = bui.textwidget( parent=w_parent, position=(470, v - 250 - 70), size=(0, 0), @@ -610,7 +639,7 @@ class LeagueRankWindow(ba.Window): flatness=1.0, ) - self._power_ranking_rank_text = ba.textwidget( + self._power_ranking_rank_text = bui.textwidget( parent=w_parent, position=(473, v - 210 - 70), size=(0, 0), @@ -621,7 +650,7 @@ class LeagueRankWindow(ba.Window): scale=1.0, ) - self._season_ends_text = ba.textwidget( + self._season_ends_text = bui.textwidget( parent=w_parent, position=(470, v - 380), size=(0, 0), @@ -634,7 +663,7 @@ class LeagueRankWindow(ba.Window): shadow=0, flatness=1.0, ) - self._trophy_counts_reset_text = ba.textwidget( + self._trophy_counts_reset_text = bui.textwidget( parent=w_parent, position=(470, v - 410), size=(0, 0), @@ -655,7 +684,7 @@ class LeagueRankWindow(ba.Window): h = 707 v -= 451 - self._see_more_button = ba.buttonwidget( + self._see_more_button = bui.buttonwidget( parent=w_parent, label=self._rdict.seeMoreText, position=(h, v), @@ -663,17 +692,20 @@ class LeagueRankWindow(ba.Window): textcolor=(0.7, 0.7, 0.8), size=(230, 60), autoselect=True, - on_activate_call=ba.WeakCall(self._on_more_press), + on_activate_call=bui.WeakCall(self._on_more_press), ) def _on_more_press(self) -> None: - our_login_id = ba.internal.get_public_login_id() + plus = bui.app.plus + assert plus is not None + + our_login_id = plus.get_v1_account_public_login_id() # our_login_id = _bs.get_account_misc_read_val_2( # 'resolvedAccountID', None) if not self._can_do_more_button or our_login_id is None: - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource='unavailableText'), color=(1, 0, 0) + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='unavailableText'), color=(1, 0, 0) ) return if self._season is None: @@ -686,8 +718,8 @@ class LeagueRankWindow(ba.Window): league_str = '&league=' + self._league_url_arg else: league_str = '' - ba.open_url( - ba.internal.get_master_server_address() + bui.open_url( + plus.get_master_server_address() + '/highscores?list=powerRankings&v=2' + league_str + season_str @@ -701,18 +733,21 @@ class LeagueRankWindow(ba.Window): # pylint: disable=too-many-locals if not self._root_widget: return - accounts = ba.app.accounts_v1 + plus = bui.app.plus + assert plus is not None + assert bui.app.classic is not None + accounts = bui.app.classic.accounts in_top = data is not None and data['rank'] is not None eq_text = self._rdict.powerRankingPointsEqualsText pts_txt = self._rdict.powerRankingPointsText - num_text = ba.Lstr(resource='numberText').evaluate() + num_text = bui.Lstr(resource='numberText').evaluate() do_percent = False finished_season_unranked = False self._can_do_more_button = True extra_text = '' - if ba.internal.get_v1_account_state() != 'signed_in': + if plus.get_v1_account_state() != 'signed_in': status_text = ( - '(' + ba.Lstr(resource='notSignedInText').evaluate() + ')' + '(' + bui.Lstr(resource='notSignedInText').evaluate() + ')' ) elif in_top: assert data is not None @@ -740,7 +775,7 @@ class LeagueRankWindow(ba.Window): ) do_percent = True except Exception: - ba.print_exception('Error updating power ranking.') + logging.exception('Error updating power ranking.') status_text = self._rdict.powerRankingNotInTopText.replace( '${NUMBER}', str(data['listSize']) ) @@ -768,7 +803,7 @@ class LeagueRankWindow(ba.Window): season_choices.append(ssn) if ssn != 'a' and not did_first: season_choices_display.append( - ba.Lstr( + bui.Lstr( resource='league.currentSeasonText', subs=[('${NUMBER}', ssn)], ) @@ -780,47 +815,47 @@ class LeagueRankWindow(ba.Window): self._is_current_season = True elif ssn == 'a': season_choices_display.append( - ba.Lstr(resource='league.allTimeText') + bui.Lstr(resource='league.allTimeText') ) else: season_choices_display.append( - ba.Lstr( + bui.Lstr( resource='league.seasonText', subs=[('${NUMBER}', ssn)], ) ) assert self._subcontainer - self._season_popup_menu = popup_ui.PopupMenu( + self._season_popup_menu = PopupMenu( parent=self._subcontainer, position=(390, v - 45), width=150, button_size=(200, 50), choices=season_choices, - on_value_change_call=ba.WeakCall(self._on_season_change), + on_value_change_call=bui.WeakCall(self._on_season_change), choices_display=season_choices_display, current_choice=self._season, ) if popup_was_selected: - ba.containerwidget( + bui.containerwidget( edit=self._subcontainer, selected_child=self._season_popup_menu.get_button(), ) - ba.widget(edit=self._see_more_button, show_buffer_bottom=100) - ba.widget( + bui.widget(edit=self._see_more_button, show_buffer_bottom=100) + bui.widget( edit=self._season_popup_menu.get_button(), up_widget=self._back_button, ) - ba.widget( + bui.widget( edit=self._back_button, down_widget=self._power_ranking_achievements_button, right_widget=self._season_popup_menu.get_button(), ) - ba.textwidget( + bui.textwidget( edit=self._league_title_text, text='' if self._season == 'a' - else ba.Lstr(resource='league.leagueText'), + else bui.Lstr(resource='league.leagueText'), ) if data is None: @@ -829,13 +864,13 @@ class LeagueRankWindow(ba.Window): lcolor = (1, 1, 1) self._league_url_arg = '' elif self._season == 'a': - lname = ba.Lstr(resource='league.allTimeText').evaluate() + lname = bui.Lstr(resource='league.allTimeText').evaluate() lnum = '' lcolor = (1, 1, 1) self._league_url_arg = '' else: lnum = ('[' + str(data['l']['i']) + ']') if data['l']['i2'] else '' - lname = ba.Lstr( + lname = bui.Lstr( translate=('leagueNames', data['l']['n']) ).evaluate() lcolor = data['l']['c'] @@ -843,7 +878,7 @@ class LeagueRankWindow(ba.Window): data['l']['n'] + '_' + str(data['l']['i']) ).lower() - to_end_string: ba.Lstr | str + to_end_string: bui.Lstr | str if data is None or self._season == 'a' or data['se'] is None: to_end_string = '' show_season_end = False @@ -852,41 +887,41 @@ class LeagueRankWindow(ba.Window): days_to_end = data['se'][0] minutes_to_end = data['se'][1] if days_to_end > 0: - to_end_string = ba.Lstr( + to_end_string = bui.Lstr( resource='league.seasonEndsDaysText', subs=[('${NUMBER}', str(days_to_end))], ) elif days_to_end == 0 and minutes_to_end >= 60: - to_end_string = ba.Lstr( + to_end_string = bui.Lstr( resource='league.seasonEndsHoursText', subs=[('${NUMBER}', str(minutes_to_end // 60))], ) elif days_to_end == 0 and minutes_to_end >= 0: - to_end_string = ba.Lstr( + to_end_string = bui.Lstr( resource='league.seasonEndsMinutesText', subs=[('${NUMBER}', str(minutes_to_end))], ) else: - to_end_string = ba.Lstr( + to_end_string = bui.Lstr( resource='league.seasonEndedDaysAgoText', subs=[('${NUMBER}', str(-(days_to_end + 1)))], ) - ba.textwidget(edit=self._season_ends_text, text=to_end_string) - ba.textwidget( + bui.textwidget(edit=self._season_ends_text, text=to_end_string) + bui.textwidget( edit=self._trophy_counts_reset_text, - text=ba.Lstr(resource='league.trophyCountsResetText') + text=bui.Lstr(resource='league.trophyCountsResetText') if self._is_current_season and show_season_end else '', ) - ba.textwidget(edit=self._league_text, text=lname, color=lcolor) + bui.textwidget(edit=self._league_text, text=lname, color=lcolor) l_text_width = min( self._league_text_maxwidth, - ba.internal.get_string_width(lname, suppress_warning=True) + bui.get_string_width(lname, suppress_warning=True) * self._league_text_scale, ) - ba.textwidget( + bui.textwidget( edit=self._league_number_text, text=lnum, color=lcolor, @@ -895,18 +930,18 @@ class LeagueRankWindow(ba.Window): self._league_number_base_pos[1] + 10, ), ) - ba.textwidget( + bui.textwidget( edit=self._to_ranked_text, - text=ba.Lstr(resource='coopSelectWindow.toRankedText').evaluate() + text=bui.Lstr(resource='coopSelectWindow.toRankedText').evaluate() + '' + extra_text if do_percent else '', ) - ba.textwidget( + bui.textwidget( edit=self._your_power_ranking_text, - text=ba.Lstr( + text=bui.Lstr( resource='rankText', fallback_resource='coopSelectWindow.yourPowerRankingText', ) @@ -914,7 +949,7 @@ class LeagueRankWindow(ba.Window): else '', ) - ba.textwidget( + bui.textwidget( edit=self._power_ranking_rank_text, position=(473, v - 70 - (170 if do_percent else 220)), text=status_text, @@ -928,20 +963,20 @@ class LeagueRankWindow(ba.Window): if self._activity_mult_button is not None: if data is None or data['act'] is None: - ba.buttonwidget( + bui.buttonwidget( edit=self._activity_mult_button, textcolor=(0.7, 0.7, 0.8, 0.5), icon_color=(0.5, 0, 0.5, 0.3), ) - ba.textwidget(edit=self._activity_mult_text, text=' -') + bui.textwidget(edit=self._activity_mult_text, text=' -') else: - ba.buttonwidget( + bui.buttonwidget( edit=self._activity_mult_button, textcolor=(0.7, 0.7, 0.8, 1.0), icon_color=(0.5, 0, 0.5, 1.0), ) # pylint: disable=consider-using-f-string - ba.textwidget( + bui.textwidget( edit=self._activity_mult_text, text='x ' + ('%.2f' % data['act']), ) @@ -950,41 +985,39 @@ class LeagueRankWindow(ba.Window): pro_mult = ( 1.0 + float( - ba.internal.get_v1_account_misc_read_val( - 'proPowerRankingBoost', 0.0 - ) + plus.get_v1_account_misc_read_val('proPowerRankingBoost', 0.0) ) * 0.01 ) # pylint: disable=consider-using-f-string - ba.textwidget( + bui.textwidget( edit=self._pro_mult_text, text=' -' if (data is None or not have_pro) else 'x ' + ('%.2f' % pro_mult), ) - ba.buttonwidget( + bui.buttonwidget( edit=self._pro_mult_button, textcolor=(0.7, 0.7, 0.8, (1.0 if have_pro else 0.5)), icon_color=(0.5, 0, 0.5) if have_pro else (0.5, 0, 0.5, 0.2), ) - ba.buttonwidget( + bui.buttonwidget( edit=self._power_ranking_achievements_button, - label=('' if data is None else (str(data['a']) + ' ')) - + ba.Lstr(resource='achievementsText').evaluate(), + label=('' if data is None else str(data['a']) + ' ') + + bui.Lstr(resource='achievementsText').evaluate(), ) # for the achievement value, use the number they gave us for # non-current seasons; otherwise calc our own total_ach_value = 0 - for ach in ba.app.ach.achievements: + for ach in bui.app.classic.ach.achievements: if ach.complete: total_ach_value += ach.power_ranking_value if self._season != 'a' and not self._is_current_season: if data is not None and 'at' in data: total_ach_value = data['at'] - ba.textwidget( + bui.textwidget( edit=self._power_ranking_achievement_total_text, text='-' if data is None @@ -995,12 +1028,12 @@ class LeagueRankWindow(ba.Window): data, 'trophyCount' ) total_trophies_value = accounts.get_league_rank_points(data, 'trophies') - ba.buttonwidget( + bui.buttonwidget( edit=self._power_ranking_trophies_button, - label=('' if data is None else (str(total_trophies_count) + ' ')) - + ba.Lstr(resource='trophiesText').evaluate(), + label=('' if data is None else str(total_trophies_count) + ' ') + + bui.Lstr(resource='trophiesText').evaluate(), ) - ba.textwidget( + bui.textwidget( edit=self._power_ranking_trophies_total_text, text='-' if data is None @@ -1009,7 +1042,7 @@ class LeagueRankWindow(ba.Window): ), ) - ba.textwidget( + bui.textwidget( edit=self._power_ranking_total_text, text='-' if data is None @@ -1030,7 +1063,7 @@ class LeagueRankWindow(ba.Window): h2 = 680 is_us = score[3] self._power_ranking_score_widgets.append( - ba.textwidget( + bui.textwidget( parent=w_parent, position=(h2 - 20, v2), size=(0, 0), @@ -1045,7 +1078,7 @@ class LeagueRankWindow(ba.Window): ) ) self._power_ranking_score_widgets.append( - ba.textwidget( + bui.textwidget( parent=w_parent, position=(h2 + 20, v2), size=(0, 0), @@ -1059,7 +1092,7 @@ class LeagueRankWindow(ba.Window): scale=0.7, ) ) - txt = ba.textwidget( + txt = bui.textwidget( parent=w_parent, position=(h2 + 60, v2 - (28 * 0.5) / 0.9), size=(210 / 0.9, 28), @@ -1076,24 +1109,24 @@ class LeagueRankWindow(ba.Window): scale=0.9, ) self._power_ranking_score_widgets.append(txt) - ba.textwidget( + bui.textwidget( edit=txt, - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._show_account_info, score[4], txt ), ) assert self._season_popup_menu is not None - ba.widget( + bui.widget( edit=txt, left_widget=self._season_popup_menu.get_button() ) v2 -= 28 def _show_account_info( - self, account_id: str, textwidget: ba.Widget + self, account_id: str, textwidget: bui.Widget ) -> None: from bastd.ui.account import viewer - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() viewer.AccountViewerWindow( account_id=account_id, position=textwidget.get_screen_space_center() ) @@ -1110,10 +1143,11 @@ class LeagueRankWindow(ba.Window): from bastd.ui.coop.browser import CoopBrowserWindow self._save_state() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) if not self._modal: - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( CoopBrowserWindow(transition='in_left').get_root_widget() ) diff --git a/assets/src/ba_data/python/bastd/ui/mainmenu.py b/src/assets/ba_data/python/bastd/ui/mainmenu.py similarity index 74% rename from assets/src/ba_data/python/bastd/ui/mainmenu.py rename to src/assets/ba_data/python/bastd/ui/mainmenu.py index b4369f0a..c3e74df1 100644 --- a/assets/src/ba_data/python/bastd/ui/mainmenu.py +++ b/src/assets/ba_data/python/bastd/ui/mainmenu.py @@ -6,15 +6,16 @@ from __future__ import annotations from typing import TYPE_CHECKING +import logging -import ba -import ba.internal +import bauiv1 as bui +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Callable -class MainMenuWindow(ba.Window): +class MainMenuWindow(bui.Window): """The main menu window, both in-game and in the main menu session.""" def __init__(self, transition: str | None = 'in_right'): @@ -22,8 +23,12 @@ class MainMenuWindow(ba.Window): import threading from bastd.mainmenu import MainMenuSession + plus = bui.app.plus + assert plus is not None + self._in_game = not isinstance( - ba.internal.get_foreground_host_session(), MainMenuSession + bs.get_foreground_host_session(), + MainMenuSession, ) # Preload some modules we use in a background thread so we won't @@ -31,12 +36,12 @@ class MainMenuWindow(ba.Window): threading.Thread(target=self._preload_modules).start() if not self._in_game: - ba.set_analytics_screen('Main Menu') + bui.set_analytics_screen('Main Menu') self._show_remote_app_info_on_first_launch() # Make a vanilla container; we'll modify it to our needs in refresh. super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( transition=transition, toolbar_visibility='menu_minimal_no_back' if self._in_game @@ -45,9 +50,9 @@ class MainMenuWindow(ba.Window): ) # Grab this stuff in case it changes. - self._is_demo = ba.app.demo_mode - self._is_arcade = ba.app.arcade_mode - self._is_iircade = ba.app.iircade_mode + self._is_demo = bui.app.demo_mode + self._is_arcade = bui.app.arcade_mode + self._is_iircade = bui.app.iircade_mode self._tdelay = 0.0 self._t_delay_inc = 0.02 @@ -58,14 +63,14 @@ class MainMenuWindow(ba.Window): self._button_height = 45.0 self._width = 100.0 self._height = 100.0 - self._demo_menu_button: ba.Widget | None = None - self._gather_button: ba.Widget | None = None - self._start_button: ba.Widget | None = None - self._watch_button: ba.Widget | None = None - self._account_button: ba.Widget | None = None - self._how_to_play_button: ba.Widget | None = None - self._credits_button: ba.Widget | None = None - self._settings_button: ba.Widget | None = None + self._demo_menu_button: bui.Widget | None = None + self._gather_button: bui.Widget | None = None + self._start_button: bui.Widget | None = None + self._watch_button: bui.Widget | None = None + self._account_button: bui.Widget | None = None + self._how_to_play_button: bui.Widget | None = None + self._credits_button: bui.Widget | None = None + self._settings_button: bui.Widget | None = None self._next_refresh_allow_time = 0.0 self._store_char_tex = self._get_store_char_tex() @@ -74,18 +79,15 @@ class MainMenuWindow(ba.Window): self._restore_state() # Keep an eye on a few things and refresh if they change. - self._account_state = ba.internal.get_v1_account_state() - self._account_state_num = ba.internal.get_v1_account_state_num() + self._account_state = plus.get_v1_account_state() + self._account_state_num = plus.get_v1_account_state_num() self._account_type = ( - ba.internal.get_v1_account_type() + plus.get_v1_account_type() if self._account_state == 'signed_in' else None ) - self._refresh_timer = ba.Timer( - 0.27, - ba.WeakCall(self._check_refresh), - repeat=True, - timetype=ba.TimeType.REAL, + self._refresh_timer = bui.AppTimer( + 0.27, bui.WeakCall(self._check_refresh), repeat=True ) # noinspection PyUnresolvedReferences @@ -106,74 +108,74 @@ class MainMenuWindow(ba.Window): import bastd.ui.play as _unused12 def _show_remote_app_info_on_first_launch(self) -> None: + app = bui.app + assert app.classic is not None # The first time the non-in-game menu pops up, we might wanna show # a 'get-remote-app' dialog in front of it. - if ba.app.first_main_menu: - ba.app.first_main_menu = False + if app.classic.first_main_menu: + app.classic.first_main_menu = False try: - app = ba.app force_test = False - ba.internal.get_local_active_input_devices_count() + bs.get_local_active_input_devices_count() if ( - (app.on_tv or app.platform == 'mac') - and ba.app.config.get('launchCount', 0) <= 1 + (app.on_tv or app.classic.platform == 'mac') + and bui.app.config.get('launchCount', 0) <= 1 ) or force_test: def _check_show_bs_remote_window() -> None: try: from bastd.ui.getremote import GetBSRemoteWindow - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() GetBSRemoteWindow() except Exception: - ba.print_exception( + logging.exception( 'Error showing get-remote window.' ) - ba.timer( - 2.5, - _check_show_bs_remote_window, - timetype=ba.TimeType.REAL, - ) + bui.apptimer(2.5, _check_show_bs_remote_window) except Exception: - ba.print_exception('Error showing get-remote-app info') + logging.exception('Error showing get-remote-app info.') def _get_store_char_tex(self) -> str: + plus = bui.app.plus + assert plus is not None return ( 'storeCharacterXmas' - if ba.internal.get_v1_account_misc_read_val('xmas', False) + if plus.get_v1_account_misc_read_val('xmas', False) else 'storeCharacterEaster' - if ba.internal.get_v1_account_misc_read_val('easter', False) + if plus.get_v1_account_misc_read_val('easter', False) else 'storeCharacter' ) def _check_refresh(self) -> None: + plus = bui.app.plus + assert plus is not None + if not self._root_widget: return - now = ba.time(ba.TimeType.REAL) + now = bui.apptime() if now < self._next_refresh_allow_time: return # Don't refresh for the first few seconds the game is up so we don't # interrupt the transition in. - # ba.app.main_menu_window_refresh_check_count += 1 - # if ba.app.main_menu_window_refresh_check_count < 4: + # bui.app.main_menu_window_refresh_check_count += 1 + # if bui.app.main_menu_window_refresh_check_count < 4: # return store_char_tex = self._get_store_char_tex() - account_state_num = ba.internal.get_v1_account_state_num() + account_state_num = plus.get_v1_account_state_num() if ( account_state_num != self._account_state_num or store_char_tex != self._store_char_tex ): self._store_char_tex = store_char_tex self._account_state_num = account_state_num - account_state = ( - self._account_state - ) = ba.internal.get_v1_account_state() + account_state = self._account_state = plus.get_v1_account_state() self._account_type = ( - ba.internal.get_v1_account_type() + plus.get_v1_account_type() if account_state == 'signed_in' else None ) @@ -181,7 +183,7 @@ class MainMenuWindow(ba.Window): self._refresh() self._restore_state() - def get_play_button(self) -> ba.Widget | None: + def get_play_button(self) -> bui.Widget | None: """Return the play button.""" return self._start_button @@ -192,6 +194,9 @@ class MainMenuWindow(ba.Window): from bastd.ui.confirm import QuitWindow from bastd.ui.store.button import StoreButton + plus = bui.app.plus + assert plus is not None + # Clear everything that was there. children = self._root_widget.get_children() for child in children: @@ -205,9 +210,14 @@ class MainMenuWindow(ba.Window): self._r = 'mainMenu' - app = ba.app - self._have_quit_button = app.ui.uiscale is ba.UIScale.LARGE or ( - app.platform == 'windows' and app.subplatform == 'oculus' + app = bui.app + assert app.classic is not None + self._have_quit_button = ( + app.classic.ui.uiscale is bui.UIScale.LARGE + or ( + app.classic.platform == 'windows' + and app.classic.subplatform == 'oculus' + ) ) self._have_store_button = not self._in_game @@ -216,11 +226,15 @@ class MainMenuWindow(ba.Window): not self._in_game or not app.toolbar_test ) and not (self._is_demo or self._is_arcade or self._is_iircade) - self._input_device = input_device = ba.internal.get_ui_input_device() + self._input_device = input_device = bs.get_ui_input_device() + + # Are we connected to a local player? self._input_player = input_device.player if input_device else None + + # Are we connected to a remote player?. self._connected_to_remote_player = ( - input_device.is_connected_to_remote_player() - if input_device + input_device.is_attached_to_player() + if (input_device and self._input_player is None) else False ) @@ -235,24 +249,24 @@ class MainMenuWindow(ba.Window): if self._have_settings_button: h, v, scale = positions[self._p_index] self._p_index += 1 - self._settings_button = ba.buttonwidget( + self._settings_button = bui.buttonwidget( parent=self._root_widget, position=(h - self._button_width * 0.5 * scale, v), size=(self._button_width, self._button_height), scale=scale, autoselect=self._use_autoselect, - label=ba.Lstr(resource=self._r + '.settingsText'), + label=bui.Lstr(resource=self._r + '.settingsText'), transition_delay=self._tdelay, on_activate_call=self._settings, ) # Scattered eggs on easter. if ( - ba.internal.get_v1_account_misc_read_val('easter', False) + plus.get_v1_account_misc_read_val('easter', False) and not self._in_game ): icon_size = 34 - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, position=( h - icon_size * 0.5 - 15, @@ -260,7 +274,7 @@ class MainMenuWindow(ba.Window): ), transition_delay=self._tdelay, size=(icon_size, icon_size), - texture=ba.gettexture('egg3'), + texture=bui.gettexture('egg3'), tilt_scale=0.0, ) @@ -271,24 +285,24 @@ class MainMenuWindow(ba.Window): self._p_index += 1 # If we're in a replay, we have a 'Leave Replay' button. - if ba.internal.is_in_replay(): - ba.buttonwidget( + if bs.is_in_replay(): + bui.buttonwidget( parent=self._root_widget, position=(h - self._button_width * 0.5 * scale, v), scale=scale, size=(self._button_width, self._button_height), autoselect=self._use_autoselect, - label=ba.Lstr(resource='replayEndText'), + label=bui.Lstr(resource='replayEndText'), on_activate_call=self._confirm_end_replay, ) - elif ba.internal.get_foreground_host_session() is not None: - ba.buttonwidget( + elif bs.get_foreground_host_session() is not None: + bui.buttonwidget( parent=self._root_widget, position=(h - self._button_width * 0.5 * scale, v), scale=scale, size=(self._button_width, self._button_height), autoselect=self._use_autoselect, - label=ba.Lstr( + label=bui.Lstr( resource=self._r + ( '.endTestText' @@ -304,17 +318,17 @@ class MainMenuWindow(ba.Window): ) # Assume we're in a client-session. else: - ba.buttonwidget( + bui.buttonwidget( parent=self._root_widget, position=(h - self._button_width * 0.5 * scale, v), scale=scale, size=(self._button_width, self._button_height), autoselect=self._use_autoselect, - label=ba.Lstr(resource=self._r + '.leavePartyText'), + label=bui.Lstr(resource=self._r + '.leavePartyText'), on_activate_call=self._confirm_leave_party, ) - self._store_button: ba.Widget | None + self._store_button: bui.Widget | None if self._have_store_button: this_b_width = self._button_width h, v, scale = positions[self._p_index] @@ -325,20 +339,21 @@ class MainMenuWindow(ba.Window): position=(h - this_b_width * 0.5 * scale, v), size=(this_b_width, self._button_height), scale=scale, - on_activate_call=ba.WeakCall(self._on_store_pressed), + on_activate_call=bui.WeakCall(self._on_store_pressed), sale_scale=1.3, transition_delay=self._tdelay, ) self._store_button = store_button = sbtn.get_button() - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale icon_size = ( 55 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 55 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 70 ) - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, position=( h - icon_size * 0.5, @@ -346,7 +361,7 @@ class MainMenuWindow(ba.Window): ), transition_delay=self._tdelay, size=(icon_size, icon_size), - texture=ba.gettexture(self._store_char_tex), + texture=bui.gettexture(self._store_char_tex), tilt_scale=0.0, draw_controller=store_button, ) @@ -355,21 +370,21 @@ class MainMenuWindow(ba.Window): else: self._store_button = None - self._quit_button: ba.Widget | None + self._quit_button: bui.Widget | None if not self._in_game and self._have_quit_button: h, v, scale = positions[self._p_index] self._p_index += 1 - self._quit_button = quit_button = ba.buttonwidget( + self._quit_button = quit_button = bui.buttonwidget( parent=self._root_widget, autoselect=self._use_autoselect, position=(h - self._button_width * 0.5 * scale, v), size=(self._button_width, self._button_height), scale=scale, - label=ba.Lstr( + label=bui.Lstr( resource=self._r + ( '.quitText' - if 'Mac' in ba.app.user_agent_string + if 'Mac' in app.classic.user_agent_string else '.exitGameText' ) ), @@ -378,9 +393,9 @@ class MainMenuWindow(ba.Window): ) # Scattered eggs on easter. - if ba.internal.get_v1_account_misc_read_val('easter', False): + if plus.get_v1_account_misc_read_val('easter', False): icon_size = 30 - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, position=( h - icon_size * 0.5 + 25, @@ -391,11 +406,11 @@ class MainMenuWindow(ba.Window): ), transition_delay=self._tdelay, size=(icon_size, icon_size), - texture=ba.gettexture('egg1'), + texture=bui.gettexture('egg1'), tilt_scale=0.0, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=quit_button ) self._tdelay += self._t_delay_inc @@ -407,36 +422,37 @@ class MainMenuWindow(ba.Window): if ( not self._in_game and not self._have_quit_button - and ba.app.platform == 'android' + and app.classic.platform == 'android' ): def _do_quit() -> None: QuitWindow(swish=True, back=True) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, on_cancel_call=_do_quit ) # Add speed-up/slow-down buttons for replays. # (ideally this should be part of a fading-out playback bar like most # media players but this works for now). - if ba.internal.is_in_replay(): + if bs.is_in_replay(): b_size = 50.0 b_buffer = 10.0 t_scale = 0.75 - uiscale = ba.app.ui.uiscale - if uiscale is ba.UIScale.SMALL: + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + if uiscale is bui.UIScale.SMALL: b_size *= 0.6 b_buffer *= 1.0 v_offs = -40 t_scale = 0.5 - elif uiscale is ba.UIScale.MEDIUM: + elif uiscale is bui.UIScale.MEDIUM: v_offs = -70 else: v_offs = -100 - self._replay_speed_text = ba.textwidget( + self._replay_speed_text = bui.textwidget( parent=self._root_widget, - text=ba.Lstr( + text=bui.Lstr( resource='watchWindow.playbackSpeedText', subs=[('${SPEED}', str(1.23))], ), @@ -451,13 +467,10 @@ class MainMenuWindow(ba.Window): self._change_replay_speed(0) # Keep updating in a timer in case it gets changed elsewhere. - self._change_replay_speed_timer = ba.Timer( - 0.25, - ba.WeakCall(self._change_replay_speed, 0), - timetype=ba.TimeType.REAL, - repeat=True, + self._change_replay_speed_timer = bui.AppTimer( + 0.25, bui.WeakCall(self._change_replay_speed, 0), repeat=True ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=( h - b_size - b_buffer, @@ -467,9 +480,9 @@ class MainMenuWindow(ba.Window): size=(b_size, b_size), label='', autoselect=True, - on_activate_call=ba.Call(self._change_replay_speed, -1), + on_activate_call=bui.Call(self._change_replay_speed, -1), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, draw_controller=btn, text='-', @@ -482,16 +495,16 @@ class MainMenuWindow(ba.Window): size=(0, 0), scale=3.0 * t_scale, ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=(h + b_buffer, v - b_size - b_buffer + v_offs), button_type='square', size=(b_size, b_size), label='', autoselect=True, - on_activate_call=ba.Call(self._change_replay_speed, 1), + on_activate_call=bui.Call(self._change_replay_speed, 1), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, draw_controller=btn, text='+', @@ -511,13 +524,17 @@ class MainMenuWindow(ba.Window): # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements - if not ba.app.did_menu_intro: + plus = bui.app.plus + assert plus is not None + + assert bui.app.classic is not None + if not bui.app.classic.did_menu_intro: self._tdelay = 2.0 self._t_delay_inc = 0.02 self._t_delay_play = 1.7 def _set_allow_time() -> None: - self._next_refresh_allow_time = ba.time(ba.TimeType.REAL) + 2.5 + self._next_refresh_allow_time = bui.apptime() + 2.5 # Slight hack: widget transitions currently only progress when # frames are being drawn, but this tends to get called before @@ -527,19 +544,19 @@ class MainMenuWindow(ba.Window): # redundant set of the time in a deferred call which hopefully # happens closer to actual frame draw times. _set_allow_time() - ba.pushcall(_set_allow_time) + bui.pushcall(_set_allow_time) - ba.app.did_menu_intro = True + bui.app.classic.did_menu_intro = True self._width = 400.0 self._height = 200.0 enable_account_button = True - account_type_name: str | ba.Lstr - if ba.internal.get_v1_account_state() == 'signed_in': - account_type_name = ba.internal.get_v1_account_display_string() + account_type_name: str | bui.Lstr + if plus.get_v1_account_state() == 'signed_in': + account_type_name = plus.get_v1_account_display_string() account_type_icon = None account_textcolor = (1.0, 1.0, 1.0) else: - account_type_name = ba.Lstr( + account_type_name = bui.Lstr( resource='notSignedInText', fallback_resource='accountSettingsWindow.titleText', ) @@ -557,8 +574,8 @@ class MainMenuWindow(ba.Window): b_count += 1 if self._have_store_button: b_count += 1 - uiscale = ba.app.ui.uiscale - if uiscale is ba.UIScale.SMALL: + uiscale = bui.app.classic.ui.uiscale + if uiscale is bui.UIScale.SMALL: root_widget_scale = 1.6 play_button_width = self._button_width * 0.65 play_button_height = self._button_height * 1.1 @@ -567,7 +584,7 @@ class MainMenuWindow(ba.Window): button_y_offs2 = -60.0 self._button_height *= 1.3 button_spacing = 1.04 - elif uiscale is ba.UIScale.MEDIUM: + elif uiscale is bui.UIScale.MEDIUM: root_widget_scale = 1.3 play_button_width = self._button_width * 0.65 play_button_height = self._button_height * 1.1 @@ -586,7 +603,7 @@ class MainMenuWindow(ba.Window): self._button_height *= 1.2 button_spacing = 1.1 spc = self._button_width * small_button_scale * button_spacing - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, size=(self._width, self._height), background=False, @@ -604,7 +621,7 @@ class MainMenuWindow(ba.Window): ) ) # In kiosk mode, provide a button to get back to the kiosk menu. - if ba.app.demo_mode or ba.app.arcade_mode: + if bui.app.demo_mode or bui.app.arcade_mode: h, v, scale = positions[self._p_index] this_b_width = self._button_width * 0.4 * scale demo_menu_delay = ( @@ -612,16 +629,16 @@ class MainMenuWindow(ba.Window): if self._t_delay_play == 0.0 else max(0, self._t_delay_play + 0.1) ) - self._demo_menu_button = ba.buttonwidget( + self._demo_menu_button = bui.buttonwidget( parent=self._root_widget, position=(self._width * 0.5 - this_b_width * 0.5, v + 90), size=(this_b_width, 45), autoselect=True, color=(0.45, 0.55, 0.45), textcolor=(0.7, 0.8, 0.7), - label=ba.Lstr( + label=bui.Lstr( resource='modeArcadeText' - if ba.app.arcade_mode + if bui.app.arcade_mode else 'modeDemoText' ), transition_delay=demo_menu_delay, @@ -629,12 +646,12 @@ class MainMenuWindow(ba.Window): ) else: self._demo_menu_button = None - uiscale = ba.app.ui.uiscale + uiscale = bui.app.classic.ui.uiscale foof = ( -1 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 3 ) h, v, scale = positions[self._p_index] @@ -649,7 +666,7 @@ class MainMenuWindow(ba.Window): this_h = h - play_button_width * 0.5 * scale - 40 * scale this_b_width = self._button_width * 0.25 * scale this_b_height = self._button_height * 0.82 * scale - self._gather_button = btn = ba.buttonwidget( + self._gather_button = btn = bui.buttonwidget( parent=self._root_widget, position=(this_h - this_b_width * 0.5, v), size=(this_b_width, this_b_height), @@ -659,7 +676,7 @@ class MainMenuWindow(ba.Window): transition_delay=gather_delay, on_activate_call=self._gather_press, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(this_h, v + self._button_height * 0.33), size=(0, 0), @@ -668,35 +685,35 @@ class MainMenuWindow(ba.Window): draw_controller=btn, color=(0.75, 1.0, 0.7), maxwidth=self._button_width * 0.33, - text=ba.Lstr(resource='gatherWindow.titleText'), + text=bui.Lstr(resource='gatherWindow.titleText'), h_align='center', v_align='center', ) icon_size = this_b_width * 0.6 - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, size=(icon_size, icon_size), draw_controller=btn, transition_delay=gather_delay, position=(this_h - 0.5 * icon_size, v + 0.31 * this_b_height), - texture=ba.gettexture('usersButton'), + texture=bui.gettexture('usersButton'), ) # Play button. h, v, scale = positions[self._p_index] self._p_index += 1 - self._start_button = start_button = ba.buttonwidget( + self._start_button = start_button = bui.buttonwidget( parent=self._root_widget, position=(h - play_button_width * 0.5 * scale, v), size=(play_button_width, play_button_height), autoselect=self._use_autoselect, scale=scale, text_res_scale=2.0, - label=ba.Lstr(resource='playText'), + label=bui.Lstr(resource='playText'), transition_delay=self._t_delay_play, on_activate_call=self._play_press, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, start_button=start_button, selected_child=start_button, @@ -710,7 +727,7 @@ class MainMenuWindow(ba.Window): this_h = h + play_button_width * 0.5 * scale + 40 * scale this_b_width = self._button_width * 0.25 * scale this_b_height = self._button_height * 0.82 * scale - self._watch_button = btn = ba.buttonwidget( + self._watch_button = btn = bui.buttonwidget( parent=self._root_widget, position=(this_h - this_b_width * 0.5, v), size=(this_b_width, this_b_height), @@ -720,7 +737,7 @@ class MainMenuWindow(ba.Window): transition_delay=watch_delay, on_activate_call=self._watch_press, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(this_h, v + self._button_height * 0.33), size=(0, 0), @@ -729,24 +746,24 @@ class MainMenuWindow(ba.Window): color=(0.75, 1.0, 0.7), draw_controller=btn, maxwidth=self._button_width * 0.33, - text=ba.Lstr(resource='watchWindow.titleText'), + text=bui.Lstr(resource='watchWindow.titleText'), h_align='center', v_align='center', ) icon_size = this_b_width * 0.55 - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, size=(icon_size, icon_size), draw_controller=btn, transition_delay=watch_delay, position=(this_h - 0.5 * icon_size, v + 0.33 * this_b_height), - texture=ba.gettexture('tv'), + texture=bui.gettexture('tv'), ) if not self._in_game and enable_account_button: this_b_width = self._button_width h, v, scale = positions[self._p_index] self._p_index += 1 - self._account_button = ba.buttonwidget( + self._account_button = bui.buttonwidget( parent=self._root_widget, position=(h - this_b_width * 0.5 * scale, v), size=(this_b_width, self._button_height), @@ -763,11 +780,11 @@ class MainMenuWindow(ba.Window): # Scattered eggs on easter. if ( - ba.internal.get_v1_account_misc_read_val('easter', False) + plus.get_v1_account_misc_read_val('easter', False) and not self._in_game ): icon_size = 32 - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, position=( h - icon_size * 0.5 + 35, @@ -778,7 +795,7 @@ class MainMenuWindow(ba.Window): ), transition_delay=self._tdelay, size=(icon_size, icon_size), - texture=ba.gettexture('egg2'), + texture=bui.gettexture('egg2'), tilt_scale=0.0, ) self._tdelay += self._t_delay_inc @@ -788,13 +805,13 @@ class MainMenuWindow(ba.Window): # How-to-play button. h, v, scale = positions[self._p_index] self._p_index += 1 - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=(h - self._button_width * 0.5 * scale, v), scale=scale, autoselect=self._use_autoselect, size=(self._button_width, self._button_height), - label=ba.Lstr(resource=self._r + '.howToPlayText'), + label=bui.Lstr(resource=self._r + '.howToPlayText'), transition_delay=self._tdelay, on_activate_call=self._howtoplay, ) @@ -802,11 +819,11 @@ class MainMenuWindow(ba.Window): # Scattered eggs on easter. if ( - ba.internal.get_v1_account_misc_read_val('easter', False) + plus.get_v1_account_misc_read_val('easter', False) and not self._in_game ): icon_size = 28 - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, position=( h - icon_size * 0.5 + 30, @@ -814,19 +831,19 @@ class MainMenuWindow(ba.Window): ), transition_delay=self._tdelay, size=(icon_size, icon_size), - texture=ba.gettexture('egg4'), + texture=bui.gettexture('egg4'), tilt_scale=0.0, ) # Credits button. self._tdelay += self._t_delay_inc h, v, scale = positions[self._p_index] self._p_index += 1 - self._credits_button = ba.buttonwidget( + self._credits_button = bui.buttonwidget( parent=self._root_widget, position=(h - self._button_width * 0.5 * scale, v), size=(self._button_width, self._button_height), autoselect=self._use_autoselect, - label=ba.Lstr(resource=self._r + '.creditsText'), + label=bui.Lstr(resource=self._r + '.creditsText'), scale=scale, transition_delay=self._tdelay, on_activate_call=self._credits, @@ -840,16 +857,18 @@ class MainMenuWindow(ba.Window): # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements + assert bui.app.classic is not None custom_menu_entries: list[dict[str, Any]] = [] - session = ba.internal.get_foreground_host_session() + session = bs.get_foreground_host_session() if session is not None: try: custom_menu_entries = session.get_custom_menu_entries() for cme in custom_menu_entries: + cme_any: Any = cme # Type check may not hold true. if ( - not isinstance(cme, dict) + not isinstance(cme_any, dict) or 'label' not in cme - or not isinstance(cme['label'], (str, ba.Lstr)) + or not isinstance(cme['label'], (str, bui.Lstr)) or 'call' not in cme or not callable(cme['call']) ): @@ -858,8 +877,8 @@ class MainMenuWindow(ba.Window): ) except Exception: custom_menu_entries = [] - ba.print_exception( - f'Error getting custom menu entries for {session}' + logging.exception( + 'Error getting custom menu entries for %s.', session ) self._width = 250.0 self._height = 250.0 if self._input_player else 180.0 @@ -871,15 +890,15 @@ class MainMenuWindow(ba.Window): # In this case we have a leave *and* a disconnect button. self._height += 50 self._height += 50 * (len(custom_menu_entries)) - uiscale = ba.app.ui.uiscale - ba.containerwidget( + uiscale = bui.app.classic.ui.uiscale + bui.containerwidget( edit=self._root_widget, size=(self._width, self._height), scale=( 2.15 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.6 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), ) @@ -894,36 +913,36 @@ class MainMenuWindow(ba.Window): h += h_offset h_offset += d_h_offset self._start_button = None - ba.app.pause() + bui.app.classic.pause() # Player name if applicable. if self._input_player: player_name = self._input_player.getname() h, v, scale = positions[self._p_index] v += 35 - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(h - self._button_width / 2, v), size=(self._button_width, self._button_height), color=(1, 1, 1, 0.5), scale=0.7, h_align='center', - text=ba.Lstr(value=player_name), + text=bui.Lstr(value=player_name), ) else: player_name = '' h, v, scale = positions[self._p_index] self._p_index += 1 - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=(h - self._button_width / 2, v), size=(self._button_width, self._button_height), scale=scale, - label=ba.Lstr(resource=self._r + '.resumeText'), + label=bui.Lstr(resource=self._r + '.resumeText'), autoselect=self._use_autoselect, on_activate_call=self._resume, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) # Add any custom options defined by the current game. for entry in custom_menu_entries: @@ -935,11 +954,11 @@ class MainMenuWindow(ba.Window): resume = bool(entry.get('resume_on_call', True)) if resume: - call = ba.Call(self._resume_and_call, entry['call']) + call = bui.Call(self._resume_and_call, entry['call']) else: - call = ba.Call(entry['call'], ba.WeakCall(self._resume)) + call = bui.Call(entry['call'], bui.WeakCall(self._resume)) - ba.buttonwidget( + bui.buttonwidget( parent=self._root_widget, position=(h - self._button_width / 2, v), size=(self._button_width, self._button_height), @@ -954,7 +973,7 @@ class MainMenuWindow(ba.Window): ): h, v, scale = positions[self._p_index] self._p_index += 1 - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=(h - self._button_width / 2, v), size=(self._button_width, self._button_height), @@ -969,13 +988,13 @@ class MainMenuWindow(ba.Window): and player_name[0] != '<' and player_name[-1] != '>' ): - txt = ba.Lstr( + txt = bui.Lstr( resource=self._r + '.justPlayerText', subs=[('${NAME}', player_name)], ) else: - txt = ba.Lstr(value=player_name) - ba.textwidget( + txt = bui.Lstr(value=player_name) + bui.textwidget( parent=self._root_widget, position=( h, @@ -984,7 +1003,7 @@ class MainMenuWindow(ba.Window): * (0.64 if player_name != '' else 0.5), ), size=(0, 0), - text=ba.Lstr(resource=self._r + '.leaveGameText'), + text=bui.Lstr(resource=self._r + '.leaveGameText'), scale=(0.83 if player_name != '' else 1.0), color=(0.75, 1.0, 0.7), h_align='center', @@ -992,7 +1011,7 @@ class MainMenuWindow(ba.Window): draw_controller=btn, maxwidth=self._button_width * 0.9, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(h, v + self._button_height * 0.27), size=(0, 0), @@ -1008,16 +1027,14 @@ class MainMenuWindow(ba.Window): def _change_replay_speed(self, offs: int) -> None: if not self._replay_speed_text: - if ba.do_once(): + if bui.do_once(): print('_change_replay_speed called without widget') return - ba.internal.set_replay_speed_exponent( - ba.internal.get_replay_speed_exponent() + offs - ) - actual_speed = pow(2.0, ba.internal.get_replay_speed_exponent()) - ba.textwidget( + bs.set_replay_speed_exponent(bs.get_replay_speed_exponent() + offs) + actual_speed = pow(2.0, bs.get_replay_speed_exponent()) + bui.textwidget( edit=self._replay_speed_text, - text=ba.Lstr( + text=bui.Lstr( resource='watchWindow.playbackSpeedText', subs=[('${SPEED}', str(actual_speed))], ), @@ -1034,8 +1051,9 @@ class MainMenuWindow(ba.Window): from bastd.ui.kiosk import KioskWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_right') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( KioskWindow(transition='in_left').get_root_widget() ) @@ -1044,8 +1062,9 @@ class MainMenuWindow(ba.Window): from bastd.ui.account.settings import AccountSettingsWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( AccountSettingsWindow( origin_widget=self._account_button ).get_root_widget() @@ -1056,22 +1075,26 @@ class MainMenuWindow(ba.Window): from bastd.ui.store.browser import StoreBrowserWindow from bastd.ui.account import show_sign_in_prompt - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( StoreBrowserWindow( origin_widget=self._store_button ).get_root_widget() ) def _is_benchmark(self) -> bool: - session = ba.internal.get_foreground_host_session() - return ( - getattr(session, 'benchmark_type', None) == 'cpu' - or ba.app.stress_test_reset_timer is not None + session = bs.get_foreground_host_session() + return getattr(session, 'benchmark_type', None) == 'cpu' or ( + bui.app.classic is not None + and bui.app.classic.stress_test_reset_timer is not None ) def _confirm_end_game(self) -> None: @@ -1083,7 +1106,7 @@ class MainMenuWindow(ba.Window): # Select cancel by default; this occasionally gets called by accident # in a fit of button mashing and this will help reduce damage. ConfirmWindow( - ba.Lstr(resource=self._r + '.exitToMenuText'), + bui.Lstr(resource=self._r + '.exitToMenuText'), self._end_game, cancel_is_selected=True, ) @@ -1095,7 +1118,7 @@ class MainMenuWindow(ba.Window): # Select cancel by default; this occasionally gets called by accident # in a fit of button mashing and this will help reduce damage. ConfirmWindow( - ba.Lstr(resource=self._r + '.exitToMenuText'), + bui.Lstr(resource=self._r + '.exitToMenuText'), self._end_game, cancel_is_selected=True, ) @@ -1107,7 +1130,7 @@ class MainMenuWindow(ba.Window): # Select cancel by default; this occasionally gets called by accident # in a fit of button mashing and this will help reduce damage. ConfirmWindow( - ba.Lstr(resource=self._r + '.exitToMenuText'), + bui.Lstr(resource=self._r + '.exitToMenuText'), self._end_game, cancel_is_selected=True, ) @@ -1119,26 +1142,27 @@ class MainMenuWindow(ba.Window): # Select cancel by default; this occasionally gets called by accident # in a fit of button mashing and this will help reduce damage. ConfirmWindow( - ba.Lstr(resource=self._r + '.leavePartyConfirmText'), + bui.Lstr(resource=self._r + '.leavePartyConfirmText'), self._leave_party, cancel_is_selected=True, ) def _leave_party(self) -> None: - ba.internal.disconnect_from_host() + bs.disconnect_from_host() def _end_game(self) -> None: + assert bui.app.classic is not None if not self._root_widget: return - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.return_to_main_menu_session_gracefully(reset_ui=False) + bui.containerwidget(edit=self._root_widget, transition='out_left') + bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False) def _leave(self) -> None: if self._input_player: self._input_player.remove_from_game() elif self._connected_to_remote_player: if self._input_device: - self._input_device.remove_remote_player_from_game() + self._input_device.detach_from_player() self._resume() def _credits(self) -> None: @@ -1146,8 +1170,9 @@ class MainMenuWindow(ba.Window): from bastd.ui.creditslist import CreditsListWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( CreditsListWindow( origin_widget=self._credits_button ).get_root_widget() @@ -1158,8 +1183,9 @@ class MainMenuWindow(ba.Window): from bastd.ui.helpui import HelpWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( HelpWindow( main_menu=True, origin_widget=self._how_to_play_button ).get_root_widget() @@ -1170,8 +1196,9 @@ class MainMenuWindow(ba.Window): from bastd.ui.settings.allsettings import AllSettingsWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( AllSettingsWindow( origin_widget=self._settings_button ).get_root_widget() @@ -1183,37 +1210,41 @@ class MainMenuWindow(ba.Window): def _do_game_service_press(self) -> None: self._save_state() - ba.internal.show_online_score_ui() + if bui.app.classic is not None: + bui.app.classic.show_online_score_ui() + else: + logging.warning('classic is required to show game service ui') def _save_state(self) -> None: - # Don't do this for the in-game menu. if self._in_game: return + assert bui.app.classic is not None + ui = bui.app.classic.ui sel = self._root_widget.get_selected_child() if sel == self._start_button: - ba.app.ui.main_menu_selection = 'Start' + ui.main_menu_selection = 'Start' elif sel == self._gather_button: - ba.app.ui.main_menu_selection = 'Gather' + ui.main_menu_selection = 'Gather' elif sel == self._watch_button: - ba.app.ui.main_menu_selection = 'Watch' + ui.main_menu_selection = 'Watch' elif sel == self._how_to_play_button: - ba.app.ui.main_menu_selection = 'HowToPlay' + ui.main_menu_selection = 'HowToPlay' elif sel == self._credits_button: - ba.app.ui.main_menu_selection = 'Credits' + ui.main_menu_selection = 'Credits' elif sel == self._settings_button: - ba.app.ui.main_menu_selection = 'Settings' + ui.main_menu_selection = 'Settings' elif sel == self._account_button: - ba.app.ui.main_menu_selection = 'Account' + ui.main_menu_selection = 'Account' elif sel == self._store_button: - ba.app.ui.main_menu_selection = 'Store' + ui.main_menu_selection = 'Store' elif sel == self._quit_button: - ba.app.ui.main_menu_selection = 'Quit' + ui.main_menu_selection = 'Quit' elif sel == self._demo_menu_button: - ba.app.ui.main_menu_selection = 'DemoMenu' + ui.main_menu_selection = 'DemoMenu' else: print('unknown widget in main menu store selection:', sel) - ba.app.ui.main_menu_selection = 'Start' + ui.main_menu_selection = 'Start' def _restore_state(self) -> None: # pylint: disable=too-many-branches @@ -1221,8 +1252,9 @@ class MainMenuWindow(ba.Window): # Don't do this for the in-game menu. if self._in_game: return - sel_name = ba.app.ui.main_menu_selection - sel: ba.Widget | None + assert bui.app.classic is not None + sel_name = bui.app.classic.ui.main_menu_selection + sel: bui.Widget | None if sel_name is None: sel_name = 'Start' if sel_name == 'HowToPlay': @@ -1246,15 +1278,16 @@ class MainMenuWindow(ba.Window): else: sel = self._start_button if sel is not None: - ba.containerwidget(edit=self._root_widget, selected_child=sel) + bui.containerwidget(edit=self._root_widget, selected_child=sel) def _gather_press(self) -> None: # pylint: disable=cyclic-import from bastd.ui.gather import GatherWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( GatherWindow(origin_widget=self._gather_button).get_root_widget() ) @@ -1263,8 +1296,9 @@ class MainMenuWindow(ba.Window): from bastd.ui.watch import WatchWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( WatchWindow(origin_widget=self._watch_button).get_root_widget() ) @@ -1273,20 +1307,22 @@ class MainMenuWindow(ba.Window): from bastd.ui.play import PlayWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.selecting_private_party_playlist = False - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.selecting_private_party_playlist = False + bui.app.classic.ui.set_main_menu_window( PlayWindow(origin_widget=self._start_button).get_root_widget() ) def _resume(self) -> None: - ba.app.resume() + assert bui.app.classic is not None + bui.app.classic.resume() if self._root_widget: - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.app.ui.clear_main_menu_window() + bui.containerwidget(edit=self._root_widget, transition='out_right') + bui.app.classic.ui.clear_main_menu_window() # If there's callbacks waiting for this window to go away, call them. - for call in ba.app.main_menu_resume_callbacks: + for call in bui.app.classic.main_menu_resume_callbacks: call() - del ba.app.main_menu_resume_callbacks[:] + del bui.app.classic.main_menu_resume_callbacks[:] diff --git a/assets/src/ba_data/python/bastd/ui/party.py b/src/assets/ba_data/python/bastd/ui/party.py similarity index 75% rename from assets/src/ba_data/python/bastd/ui/party.py rename to src/assets/ba_data/python/bastd/ui/party.py index 2d8ca5cd..cb65efff 100644 --- a/assets/src/ba_data/python/bastd/ui/party.py +++ b/src/assets/ba_data/python/bastd/ui/party.py @@ -5,61 +5,65 @@ from __future__ import annotations import math +import logging from typing import TYPE_CHECKING, cast -import ba -import ba.internal -from bastd.ui import popup +from bastd.ui.popup import PopupMenuWindow +import bauiv1 as bui +import bascenev1 as bs if TYPE_CHECKING: from typing import Sequence, Any + from bastd.ui.popup import PopupWindow -class PartyWindow(ba.Window): + +class PartyWindow(bui.Window): """Party list/chat window.""" def __del__(self) -> None: - ba.internal.set_party_window_open(False) + bui.set_party_window_open(False) def __init__(self, origin: Sequence[float] = (0, 0)): - ba.internal.set_party_window_open(True) + bui.set_party_window_open(True) self._r = 'partyWindow' self._popup_type: str | None = None self._popup_party_member_client_id: int | None = None self._popup_party_member_is_host: bool | None = None self._width = 500 - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale self._height = ( 365 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 480 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 600 ) super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), transition='in_scale', color=(0.40, 0.55, 0.20), - parent=ba.internal.get_special_widget('overlay_stack'), + parent=bui.get_special_widget('overlay_stack'), on_outside_click_call=self.close_with_sound, scale_origin_stack_offset=origin, scale=( 2.0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.35 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -10) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (240, 0) - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else (330, 20), ) ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self._root_widget, scale=0.7, position=(30, self._height - 47), @@ -68,14 +72,14 @@ class PartyWindow(ba.Window): on_activate_call=self.close, autoselect=True, color=(0.45, 0.63, 0.15), - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._cancel_button ) - self._menu_button = ba.buttonwidget( + self._menu_button = bui.buttonwidget( parent=self._root_widget, scale=0.7, position=(self._width - 60, self._height - 47), @@ -83,18 +87,18 @@ class PartyWindow(ba.Window): label='...', autoselect=True, button_type='square', - on_activate_call=ba.WeakCall(self._on_menu_button_press), + on_activate_call=bui.WeakCall(self._on_menu_button_press), color=(0.55, 0.73, 0.25), iconscale=1.2, ) - info = ba.internal.get_connection_to_host_info() + info = bs.get_connection_to_host_info() if info.get('name', '') != '': - title = ba.Lstr(value=info['name']) + title = bui.Lstr(value=info['name']) else: - title = ba.Lstr(resource=self._r + '.titleText') + title = bui.Lstr(resource=self._r + '.titleText') - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=self._root_widget, scale=0.9, color=(0.5, 0.7, 0.5), @@ -106,7 +110,7 @@ class PartyWindow(ba.Window): v_align='center', ) - self._empty_str = ba.textwidget( + self._empty_str = bui.textwidget( parent=self._root_widget, scale=0.75, size=(0, 0), @@ -117,34 +121,34 @@ class PartyWindow(ba.Window): ) self._scroll_width = self._width - 50 - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, size=(self._scroll_width, self._height - 200), position=(30, 80), color=(0.4, 0.6, 0.3), ) - self._columnwidget = ba.columnwidget( + self._columnwidget = bui.columnwidget( parent=self._scrollwidget, border=2, margin=0 ) - ba.widget(edit=self._menu_button, down_widget=self._columnwidget) + bui.widget(edit=self._menu_button, down_widget=self._columnwidget) - self._muted_text = ba.textwidget( + self._muted_text = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.5), size=(0, 0), h_align='center', v_align='center', - text=ba.Lstr(resource='chatMutedText'), + text=bui.Lstr(resource='chatMutedText'), ) - self._chat_texts: list[ba.Widget] = [] + self._chat_texts: list[bui.Widget] = [] # add all existing messages if chat is not muted - if not ba.app.config.resolve('Chat Muted'): - msgs = ba.internal.get_chat_messages() + if not bui.app.config.resolve('Chat Muted'): + msgs = bs.get_chat_messages() for msg in msgs: self._add_msg(msg) - self._text_field = txt = ba.textwidget( + self._text_field = txt = bui.textwidget( parent=self._root_widget, editable=True, size=(530, 40), @@ -153,53 +157,50 @@ class PartyWindow(ba.Window): maxwidth=494, shadow=0.3, flatness=1.0, - description=ba.Lstr(resource=self._r + '.chatMessageText'), + description=bui.Lstr(resource=self._r + '.chatMessageText'), autoselect=True, v_align='center', corner_scale=0.7, ) - ba.widget( + bui.widget( edit=self._scrollwidget, autoselect=True, left_widget=self._cancel_button, up_widget=self._cancel_button, down_widget=self._text_field, ) - ba.widget( + bui.widget( edit=self._columnwidget, autoselect=True, up_widget=self._cancel_button, down_widget=self._text_field, ) - ba.containerwidget(edit=self._root_widget, selected_child=txt) - btn = ba.buttonwidget( + bui.containerwidget(edit=self._root_widget, selected_child=txt) + btn = bui.buttonwidget( parent=self._root_widget, size=(50, 35), - label=ba.Lstr(resource=self._r + '.sendText'), + label=bui.Lstr(resource=self._r + '.sendText'), button_type='square', autoselect=True, position=(self._width - 70, 35), on_activate_call=self._send_chat_message, ) - ba.textwidget(edit=txt, on_return_press_call=btn.activate) - self._name_widgets: list[ba.Widget] = [] + bui.textwidget(edit=txt, on_return_press_call=btn.activate) + self._name_widgets: list[bui.Widget] = [] self._roster: list[dict[str, Any]] | None = None - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - repeat=True, - timetype=ba.TimeType.REAL, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) self._update() def on_chat_message(self, msg: str) -> None: """Called when a new chat message comes through.""" - if not ba.app.config.resolve('Chat Muted'): + if not bui.app.config.resolve('Chat Muted'): self._add_msg(msg) def _add_msg(self, msg: str) -> None: - txt = ba.textwidget( + txt = bui.textwidget( parent=self._columnwidget, text=msg, h_align='left', @@ -213,23 +214,24 @@ class PartyWindow(ba.Window): self._chat_texts.append(txt) while len(self._chat_texts) > 40: self._chat_texts.pop(0).delete() - ba.containerwidget(edit=self._columnwidget, visible_child=txt) + bui.containerwidget(edit=self._columnwidget, visible_child=txt) def _on_menu_button_press(self) -> None: - is_muted = ba.app.config.resolve('Chat Muted') - uiscale = ba.app.ui.uiscale - popup.PopupMenuWindow( + is_muted = bui.app.config.resolve('Chat Muted') + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + PopupMenuWindow( position=self._menu_button.get_screen_space_center(), scale=( 2.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.65 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.23 ), choices=['unmute' if is_muted else 'mute'], choices_display=[ - ba.Lstr( + bui.Lstr( resource='chatUnMuteText' if is_muted else 'chatMuteText' ) ], @@ -245,18 +247,18 @@ class PartyWindow(ba.Window): # pylint: disable=too-many-nested-blocks # update muted state - if ba.app.config.resolve('Chat Muted'): - ba.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.3)) + if bui.app.config.resolve('Chat Muted'): + bui.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.3)) # clear any chat texts we're showing if self._chat_texts: while self._chat_texts: first = self._chat_texts.pop() first.delete() else: - ba.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.0)) + bui.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.0)) # update roster section - roster = ba.internal.get_game_roster() + roster = bs.get_game_roster() if roster != self._roster: self._roster = roster @@ -266,11 +268,11 @@ class PartyWindow(ba.Window): self._name_widgets = [] if not self._roster: top_section_height = 60 - ba.textwidget( + bui.textwidget( edit=self._empty_str, - text=ba.Lstr(resource=self._r + '.emptyText'), + text=bui.Lstr(resource=self._r + '.emptyText'), ) - ba.scrollwidget( + bui.scrollwidget( edit=self._scrollwidget, size=( self._width - 50, @@ -331,12 +333,12 @@ class PartyWindow(ba.Window): 'display_string' ] except Exception: - ba.print_exception( + logging.exception( 'Error calcing client name str.' ) p_str = '???' - widget = ba.textwidget( + widget = bui.textwidget( parent=self._root_widget, position=(pos[0], pos[1]), scale=t_scale, @@ -346,7 +348,7 @@ class PartyWindow(ba.Window): selectable=True, autoselect=True, click_activate=True, - text=ba.Lstr(value=p_str), + text=bui.Lstr(value=p_str), h_align='left', v_align='center', ) @@ -364,9 +366,9 @@ class PartyWindow(ba.Window): # FIXME: Should pass client_id to these sort of # calls; not spec-string (perhaps should wait till # client_id is more readily available though). - ba.textwidget( + bui.textwidget( edit=widget, - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._on_party_member_press, self._roster[index]['client_id'], is_host, @@ -386,13 +388,13 @@ class PartyWindow(ba.Window): if is_host: twd = min( c_width * 0.85, - ba.internal.get_string_width( + bui.get_string_width( p_str, suppress_warning=True ) * t_scale, ) self._name_widgets.append( - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=( pos[0] + twd + 1, @@ -403,7 +405,7 @@ class PartyWindow(ba.Window): v_align='center', maxwidth=c_width * 0.96 - twd, color=(0.1, 1, 0.1, 0.5), - text=ba.Lstr( + text=bui.Lstr( resource=self._r + '.hostText' ), scale=0.4, @@ -411,8 +413,8 @@ class PartyWindow(ba.Window): flatness=1.0, ) ) - ba.textwidget(edit=self._empty_str, text='') - ba.scrollwidget( + bui.textwidget(edit=self._empty_str, text='') + bui.scrollwidget( edit=self._scrollwidget, size=( self._width - 50, @@ -422,64 +424,62 @@ class PartyWindow(ba.Window): ) def popup_menu_selected_choice( - self, popup_window: popup.PopupMenuWindow, choice: str + self, popup_window: PopupMenuWindow, choice: str ) -> None: """Called when a choice is selected in the popup.""" del popup_window # unused if self._popup_type == 'partyMemberPress': if self._popup_party_member_is_host: - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource='internal.cantKickHostError'), + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='internal.cantKickHostError'), color=(1, 0, 0), ) else: assert self._popup_party_member_client_id is not None # Ban for 5 minutes. - result = ba.internal.disconnect_client( + result = bs.disconnect_client( self._popup_party_member_client_id, ban_time=5 * 60 ) if not result: - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource='getTicketsWindow.unavailableText'), + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='getTicketsWindow.unavailableText'), color=(1, 0, 0), ) elif self._popup_type == 'menu': if choice in ('mute', 'unmute'): - cfg = ba.app.config + cfg = bui.app.config cfg['Chat Muted'] = choice == 'mute' cfg.apply_and_commit() self._update() else: print(f'unhandled popup type: {self._popup_type}') - def popup_menu_closing(self, popup_window: popup.PopupWindow) -> None: + def popup_menu_closing(self, popup_window: PopupWindow) -> None: """Called when the popup is closing.""" def _on_party_member_press( - self, client_id: int, is_host: bool, widget: ba.Widget + self, client_id: int, is_host: bool, widget: bui.Widget ) -> None: # if we're the host, pop up 'kick' options for all non-host members - if ba.internal.get_foreground_host_session() is not None: - kick_str = ba.Lstr(resource='kickText') + if bs.get_foreground_host_session() is not None: + kick_str = bui.Lstr(resource='kickText') else: # kick-votes appeared in build 14248 - if ( - ba.internal.get_connection_to_host_info().get('build_number', 0) - < 14248 - ): + if bs.get_connection_to_host_info().get('build_number', 0) < 14248: return - kick_str = ba.Lstr(resource='kickVoteText') - uiscale = ba.app.ui.uiscale - popup.PopupMenuWindow( + kick_str = bui.Lstr(resource='kickVoteText') + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + PopupMenuWindow( position=widget.get_screen_space_center(), scale=( 2.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.65 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.23 ), choices=['kick'], @@ -492,16 +492,14 @@ class PartyWindow(ba.Window): self._popup_party_member_is_host = is_host def _send_chat_message(self) -> None: - ba.internal.chatmessage( - cast(str, ba.textwidget(query=self._text_field)) - ) - ba.textwidget(edit=self._text_field, text='') + bs.chatmessage(cast(str, bui.textwidget(query=self._text_field))) + bui.textwidget(edit=self._text_field, text='') def close(self) -> None: """Close the window.""" - ba.containerwidget(edit=self._root_widget, transition='out_scale') + bui.containerwidget(edit=self._root_widget, transition='out_scale') def close_with_sound(self) -> None: """Close the window and make a lovely sound.""" - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self.close() diff --git a/assets/src/ba_data/python/bastd/ui/partyqueue.py b/src/assets/ba_data/python/bastd/ui/partyqueue.py similarity index 77% rename from assets/src/ba_data/python/bastd/ui/partyqueue.py rename to src/assets/ba_data/python/bastd/ui/partyqueue.py index f5355da2..9854a165 100644 --- a/assets/src/ba_data/python/bastd/ui/partyqueue.py +++ b/src/assets/ba_data/python/bastd/ui/partyqueue.py @@ -4,21 +4,22 @@ from __future__ import annotations -import random import time +import random +import logging from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui +import bascenev1 as bs if TYPE_CHECKING: from typing import Any, Sequence -class PartyQueueWindow(ba.Window): +class PartyQueueWindow(bui.Window): """Window showing players waiting to join a server.""" - # ewww this needs quite a bit of de-linting if/when i revisit it.. + # Ewww this needs quite a bit of de-linting if/when i revisit it.. # pylint: disable=invalid-name # pylint: disable=consider-using-dict-comprehension class Dude: @@ -58,30 +59,30 @@ class PartyQueueWindow(ba.Window): 0.7 * 1.0 + 0.3 * self._color[1], 0.7 * 1.0 + 0.3 * self._color[2], ) - self._body_image = ba.buttonwidget( + self._body_image = bui.buttonwidget( parent=parent.get_root_widget(), selectable=True, label='', size=(sc * 60, sc * 80), color=self._color, texture=parent.lineup_tex, - model_transparent=parent.lineup_1_transparent_model, + mesh_transparent=parent.lineup_1_transparent_mesh, ) - ba.buttonwidget( + bui.buttonwidget( edit=self._body_image, - on_activate_call=ba.WeakCall( + on_activate_call=bui.WeakCall( parent.on_account_press, account_id, self._body_image ), ) - ba.widget(edit=self._body_image, autoselect=True) - self._eyes_image = ba.imagewidget( + bui.widget(edit=self._body_image, autoselect=True) + self._eyes_image = bui.imagewidget( parent=parent.get_root_widget(), size=(sc * 36, sc * 18), texture=parent.lineup_tex, color=self._eye_color, - model_transparent=parent.eyes_model, + mesh_transparent=parent.eyes_mesh, ) - self._name_text = ba.textwidget( + self._name_text = bui.textwidget( parent=parent.get_root_widget(), size=(0, 0), shadow=0, @@ -96,22 +97,22 @@ class PartyQueueWindow(ba.Window): self._update_image() # DEBUG: vis target pos.. - self._body_image_target: ba.Widget | None - self._eyes_image_target: ba.Widget | None + self._body_image_target: bui.Widget | None + self._eyes_image_target: bui.Widget | None if self._debug: - self._body_image_target = ba.imagewidget( + self._body_image_target = bui.imagewidget( parent=parent.get_root_widget(), size=(sc * 60, sc * 80), color=self._color, texture=parent.lineup_tex, - model_transparent=parent.lineup_1_transparent_model, + mesh_transparent=parent.lineup_1_transparent_mesh, ) - self._eyes_image_target = ba.imagewidget( + self._eyes_image_target = bui.imagewidget( parent=parent.get_root_widget(), size=(sc * 36, sc * 18), texture=parent.lineup_tex, color=self._eye_color, - model_transparent=parent.eyes_model, + mesh_transparent=parent.eyes_mesh, ) # (updates our image positions) self.set_target_distance(self._target_distance) @@ -119,20 +120,19 @@ class PartyQueueWindow(ba.Window): self._body_image_target = self._eyes_image_target = None def __del__(self) -> None: - # ew. our destructor here may get called as part of an internal # widget tear-down. # running further widget calls here can quietly break stuff, so we # need to push a deferred call to kill these as necessary instead. # (should bulletproof internal widget code to give a clean error # in this case) - def kill_widgets(widgets: Sequence[ba.Widget | None]) -> None: + def kill_widgets(widgets: Sequence[bui.Widget | None]) -> None: for widget in widgets: if widget: widget.delete() - ba.pushcall( - ba.Call( + bui.pushcall( + bui.Call( kill_widgets, [ self._body_image, @@ -154,14 +154,14 @@ class PartyQueueWindow(ba.Window): + self._line_width * (1.0 - self._target_distance), self._line_bottom - 30, ) - ba.imagewidget( + bui.imagewidget( edit=self._body_image_target, position=( position[0] - sc * 30, position[1] - sc * 25 - 70, ), ) - ba.imagewidget( + bui.imagewidget( edit=self._eyes_image_target, position=( position[0] - sc * 18, @@ -185,7 +185,7 @@ class PartyQueueWindow(ba.Window): self._line_bottom + 40, ) brightness = 1.0 + self._boost_brightness - ba.buttonwidget( + bui.buttonwidget( edit=self._body_image, position=( position[0] - sc * 30, @@ -197,7 +197,7 @@ class PartyQueueWindow(ba.Window): self._color[2] * brightness, ), ) - ba.imagewidget( + bui.imagewidget( edit=self._eyes_image, position=( position[0] - sc * 18, @@ -209,7 +209,7 @@ class PartyQueueWindow(ba.Window): self._eye_color[2] * brightness, ), ) - ba.textwidget( + bui.textwidget( edit=self._name_text, position=(position[0] - sc * 0, position[1] + sc * 40.0), ) @@ -223,7 +223,8 @@ class PartyQueueWindow(ba.Window): self._boost_brightness += 0.6 def __init__(self, queue_id: str, address: str, port: int): - ba.app.ui.have_party_queue_window = True + assert bui.app.classic is not None + bui.app.classic.ui.have_party_queue_window = True self._address = address self._port = port self._queue_id = queue_id @@ -231,56 +232,57 @@ class PartyQueueWindow(ba.Window): self._height = 400 self._last_connect_attempt_time: float | None = None self._last_transaction_time: float | None = None - self._boost_button: ba.Widget | None = None - self._boost_price: ba.Widget | None = None - self._boost_label: ba.Widget | None = None + self._boost_button: bui.Widget | None = None + self._boost_price: bui.Widget | None = None + self._boost_label: bui.Widget | None = None self._field_shown = False self._dudes: list[PartyQueueWindow.Dude] = [] self._dudes_by_id: dict[int, PartyQueueWindow.Dude] = {} self._line_left = 40.0 self._line_width = self._width - 190 self._line_bottom = self._height * 0.4 - self.lineup_tex = ba.gettexture('playerLineup') + self.lineup_tex: bui.Texture = bui.gettexture('playerLineup') self._smoothing = 0.0 self._initial_offset = 0.0 self._boost_tickets = 0 self._boost_strength = 0.0 - self._angry_computer_transparent_model = ba.getmodel( + self._angry_computer_transparent_mesh = bui.getmesh( 'angryComputerTransparent' ) - self._angry_computer_image: ba.Widget | None = None - self.lineup_1_transparent_model = ba.getmodel( + self._angry_computer_image: bui.Widget | None = None + self.lineup_1_transparent_mesh: bui.Mesh = bui.getmesh( 'playerLineup1Transparent' ) - self._lineup_2_transparent_model = ba.getmodel( + self._lineup_2_transparent_mesh: bui.Mesh = bui.getmesh( 'playerLineup2Transparent' ) - self._lineup_3_transparent_model = ba.getmodel( + + self._lineup_3_transparent_mesh = bui.getmesh( 'playerLineup3Transparent' ) - self._lineup_4_transparent_model = ba.getmodel( + self._lineup_4_transparent_mesh = bui.getmesh( 'playerLineup4Transparent' ) - self._line_image: ba.Widget | None = None - self.eyes_model = ba.getmodel('plasticEyesTransparent') - self._white_tex = ba.gettexture('white') - uiscale = ba.app.ui.uiscale + self._line_image: bui.Widget | None = None + self.eyes_mesh: bui.Mesh = bui.getmesh('plasticEyesTransparent') + self._white_tex = bui.gettexture('white') + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), color=(0.45, 0.63, 0.15), transition='in_scale', scale=( 1.4 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.2 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), ) ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self._root_widget, scale=1.0, position=(60, self._height - 80), @@ -289,14 +291,14 @@ class PartyQueueWindow(ba.Window): on_activate_call=self.close, autoselect=True, color=(0.45, 0.63, 0.15), - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._cancel_button ) - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.55), size=(0, 0), @@ -304,11 +306,11 @@ class PartyQueueWindow(ba.Window): scale=1.3, h_align='center', v_align='center', - text=ba.Lstr(resource='internal.connectingToPartyText'), + text=bui.Lstr(resource='internal.connectingToPartyText'), maxwidth=self._width * 0.65, ) - self._tickets_text = ba.textwidget( + self._tickets_text = bui.textwidget( parent=self._root_widget, position=(self._width - 180, self._height - 20), size=(0, 0), @@ -320,23 +322,24 @@ class PartyQueueWindow(ba.Window): ) # update at roughly 30fps - self._update_timer = ba.Timer( - 0.033, - ba.WeakCall(self.update), - repeat=True, - timetype=ba.TimeType.REAL, + self._update_timer = bui.AppTimer( + 0.033, bui.WeakCall(self.update), repeat=True ) self.update() def __del__(self) -> None: try: - ba.app.ui.have_party_queue_window = False - ba.internal.add_transaction( + plus = bui.app.plus + assert plus is not None + + assert bui.app.classic is not None + bui.app.classic.ui.have_party_queue_window = False + plus.add_v1_account_transaction( {'type': 'PARTY_QUEUE_REMOVE', 'q': self._queue_id} ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() except Exception: - ba.print_exception('Error removing self from party queue.') + logging.exception('Error removing self from party queue.') def get_line_left(self) -> float: """(internal)""" @@ -351,13 +354,13 @@ class PartyQueueWindow(ba.Window): return self._line_bottom def on_account_press( - self, account_id: str | None, origin_widget: ba.Widget + self, account_id: str | None, origin_widget: bui.Widget ) -> None: """A dude was clicked so we should show his account info.""" from bastd.ui.account import viewer if account_id is None: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return viewer.AccountViewerWindow( account_id=account_id, @@ -366,19 +369,22 @@ class PartyQueueWindow(ba.Window): def close(self) -> None: """Close the ui.""" - ba.containerwidget(edit=self._root_widget, transition='out_scale') + bui.containerwidget(edit=self._root_widget, transition='out_scale') def _update_field(self, response: dict[str, Any]) -> None: + plus = bui.app.plus + assert plus is not None + if self._angry_computer_image is None: - self._angry_computer_image = ba.imagewidget( + self._angry_computer_image = bui.imagewidget( parent=self._root_widget, position=(self._width - 180, self._height * 0.5 - 65), size=(150, 150), texture=self.lineup_tex, - model_transparent=self._angry_computer_transparent_model, + mesh_transparent=self._angry_computer_transparent_mesh, ) if self._line_image is None: - self._line_image = ba.imagewidget( + self._line_image = bui.imagewidget( parent=self._root_widget, color=(0.0, 0.0, 0.0), opacity=0.2, @@ -401,10 +407,8 @@ class PartyQueueWindow(ba.Window): response['d'], self._initial_offset, True, - ba.internal.get_v1_account_misc_read_val_2( - 'resolvedAccountID', None - ), - ba.internal.get_v1_account_display_string(), + plus.get_v1_account_misc_read_val_2('resolvedAccountID', None), + plus.get_v1_account_display_string(), ) self._dudes_by_id[-1] = dude self._dudes.append(dude) @@ -473,17 +477,17 @@ class PartyQueueWindow(ba.Window): # If they gave us a position, show the field. if should_show_field: - ba.textwidget( + bui.textwidget( edit=self._title_text, - text=ba.Lstr(resource='waitingInLineText'), + text=bui.Lstr(resource='waitingInLineText'), position=(self._width * 0.5, self._height * 0.85), ) self._update_field(response) self._field_shown = True if not should_show_field and self._field_shown: - ba.textwidget( + bui.textwidget( edit=self._title_text, - text=ba.Lstr(resource='internal.connectingToPartyText'), + text=bui.Lstr(resource='internal.connectingToPartyText'), position=(self._width * 0.5, self._height * 0.55), ) self._hide_field() @@ -494,7 +498,7 @@ class PartyQueueWindow(ba.Window): self._boost_tickets = response['bt'] self._boost_strength = response['ba'] if self._boost_button is None: - self._boost_button = ba.buttonwidget( + self._boost_button = bui.buttonwidget( parent=self._root_widget, scale=1.0, position=(self._width * 0.5 - 75, 20), @@ -506,7 +510,7 @@ class PartyQueueWindow(ba.Window): color=(0, 1, 0), autoselect=True, ) - self._boost_label = ba.textwidget( + self._boost_label = bui.textwidget( parent=self._root_widget, draw_controller=self._boost_button, position=(self._width * 0.5, 88), @@ -515,10 +519,10 @@ class PartyQueueWindow(ba.Window): scale=1.5, h_align='center', v_align='center', - text=ba.Lstr(resource='boostText'), + text=bui.Lstr(resource='boostText'), maxwidth=150, ) - self._boost_price = ba.textwidget( + self._boost_price = bui.textwidget( parent=self._root_widget, draw_controller=self._boost_button, position=(self._width * 0.5, 50), @@ -527,7 +531,7 @@ class PartyQueueWindow(ba.Window): scale=0.9, h_align='center', v_align='center', - text=ba.charstr(ba.SpecialChar.TICKET) + text=bui.charstr(bui.SpecialChar.TICKET) + str(self._boost_tickets), maxwidth=150, ) @@ -553,7 +557,7 @@ class PartyQueueWindow(ba.Window): self._last_connect_attempt_time is None or now - self._last_connect_attempt_time > 10.0 ): - ba.internal.connect_to_party( + bs.connect_to_party( address=self._address, port=self._port, print_progress=False, @@ -565,23 +569,26 @@ class PartyQueueWindow(ba.Window): from bastd.ui import account from bastd.ui import getcurrency - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return - if ba.internal.get_v1_account_ticket_count() < self._boost_tickets: - ba.playsound(ba.getsound('error')) + if plus.get_v1_account_ticket_count() < self._boost_tickets: + bui.getsound('error').play() getcurrency.show_get_tickets_prompt() return - ba.playsound(ba.getsound('laserReverse')) - ba.internal.add_transaction( + bui.getsound('laserReverse').play() + plus.add_v1_account_transaction( { 'type': 'PARTY_QUEUE_BOOST', 't': self._boost_tickets, 'q': self._queue_id, }, - callback=ba.WeakCall(self.on_update_response), + callback=bui.WeakCall(self.on_update_response), ) # lets not run these immediately (since they may be rapid-fire, # just bucket them until the next tick) @@ -595,25 +602,27 @@ class PartyQueueWindow(ba.Window): def update(self) -> None: """Update!""" + plus = bui.app.plus + assert plus is not None + if not self._root_widget: return # Update boost-price. if self._boost_price is not None: - ba.textwidget( + bui.textwidget( edit=self._boost_price, - text=ba.charstr(ba.SpecialChar.TICKET) + text=bui.charstr(bui.SpecialChar.TICKET) + str(self._boost_tickets), ) # Update boost button color based on if we have enough moola. if self._boost_button is not None: can_boost = ( - ba.internal.get_v1_account_state() == 'signed_in' - and ba.internal.get_v1_account_ticket_count() - >= self._boost_tickets + plus.get_v1_account_state() == 'signed_in' + and plus.get_v1_account_ticket_count() >= self._boost_tickets ) - ba.buttonwidget( + bui.buttonwidget( edit=self._boost_button, color=(0, 1, 0) if can_boost else (0.7, 0.7, 0.7), ) @@ -621,28 +630,28 @@ class PartyQueueWindow(ba.Window): # Update ticket-count. if self._tickets_text is not None: if self._boost_button is not None: - if ba.internal.get_v1_account_state() == 'signed_in': - val = ba.charstr(ba.SpecialChar.TICKET) + str( - ba.internal.get_v1_account_ticket_count() + if plus.get_v1_account_state() == 'signed_in': + val = bui.charstr(bui.SpecialChar.TICKET) + str( + plus.get_v1_account_ticket_count() ) else: - val = ba.charstr(ba.SpecialChar.TICKET) + '???' - ba.textwidget(edit=self._tickets_text, text=val) + val = bui.charstr(bui.SpecialChar.TICKET) + '???' + bui.textwidget(edit=self._tickets_text, text=val) else: - ba.textwidget(edit=self._tickets_text, text='') + bui.textwidget(edit=self._tickets_text, text='') - current_time = ba.time(ba.TimeType.REAL) + current_time = bui.apptime() if ( self._last_transaction_time is None or current_time - self._last_transaction_time - > 0.001 * ba.internal.get_v1_account_misc_read_val('pqInt', 5000) + > 0.001 * plus.get_v1_account_misc_read_val('pqInt', 5000) ): self._last_transaction_time = current_time - ba.internal.add_transaction( + plus.add_v1_account_transaction( {'type': 'PARTY_QUEUE_QUERY', 'q': self._queue_id}, - callback=ba.WeakCall(self.on_update_response), + callback=bui.WeakCall(self.on_update_response), ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() # step our dudes for dude in self._dudes: diff --git a/assets/src/ba_data/python/bastd/ui/play.py b/src/assets/ba_data/python/bastd/ui/play.py similarity index 77% rename from assets/src/ba_data/python/bastd/ui/play.py rename to src/assets/ba_data/python/bastd/ui/play.py index 976109f8..7729e1d9 100644 --- a/assets/src/ba_data/python/bastd/ui/play.py +++ b/src/assets/ba_data/python/bastd/ui/play.py @@ -4,22 +4,19 @@ from __future__ import annotations -from typing import TYPE_CHECKING +import logging -import ba -import ba.internal - -if TYPE_CHECKING: - pass +import bascenev1 as bs +import bauiv1 as bui -class PlayWindow(ba.Window): +class PlayWindow(bui.Window): """Window for selecting overall play type.""" def __init__( self, transition: str = 'in_right', - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals @@ -31,11 +28,14 @@ class PlayWindow(ba.Window): # We can currently be used either for main menu duty or for selecting # playlists (should make this more elegant/general). - self._is_main_menu = not ba.app.ui.selecting_private_party_playlist + assert bui.app.classic is not None + self._is_main_menu = ( + not bui.app.classic.ui.selecting_private_party_playlist + ) - uiscale = ba.app.ui.uiscale - width = 1000 if uiscale is ba.UIScale.SMALL else 800 - x_offs = 100 if uiscale is ba.UIScale.SMALL else 0 + uiscale = bui.app.classic.ui.uiscale + width = 1000 if uiscale is bui.UIScale.SMALL else 800 + x_offs = 100 if uiscale is bui.UIScale.SMALL else 0 height = 550 button_width = 400 @@ -51,22 +51,22 @@ class PlayWindow(ba.Window): self._r = 'playWindow' super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height), transition=transition, toolbar_visibility='menu_full', scale_origin_stack_offset=scale_origin, scale=( 1.6 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 0.9 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 0.8 ), - stack_offset=(0, 0) if uiscale is ba.UIScale.SMALL else (0, 0), + stack_offset=(0, 0) if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - self._back_button = back_button = btn = ba.buttonwidget( + self._back_button = back_button = btn = bui.buttonwidget( parent=self._root_widget, position=(55 + x_offs, height - 132), size=(120, 60), @@ -74,17 +74,17 @@ class PlayWindow(ba.Window): text_res_scale=1.5, text_scale=1.2, autoselect=True, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', ) - txt = ba.textwidget( + txt = bui.textwidget( parent=self._root_widget, position=(width * 0.5, height - 101), # position=(width * 0.5, height - # (101 if main_menu else 61)), size=(0, 0), - text=ba.Lstr( + text=bui.Lstr( resource=(self._r + '.titleText') if self._is_main_menu else 'playlistsText' @@ -92,56 +92,60 @@ class PlayWindow(ba.Window): scale=1.7, res_scale=2.0, maxwidth=400, - color=ba.app.ui.heading_color, + color=bui.app.classic.ui.heading_color, h_align='center', v_align='center', ) - ba.buttonwidget( + bui.buttonwidget( edit=btn, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) - if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: - ba.textwidget(edit=txt, text='') + if bui.app.classic.ui.use_toolbars and uiscale is bui.UIScale.SMALL: + bui.textwidget(edit=txt, text='') v = height - (110 if self._is_main_menu else 90) v -= 100 clr = (0.6, 0.7, 0.6, 1.0) v -= 280 if self._is_main_menu else 180 - v += 30 if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL else 0 + v += ( + 30 + if bui.app.classic.ui.use_toolbars and uiscale is bui.UIScale.SMALL + else 0 + ) hoffs = x_offs + 80 if self._is_main_menu else x_offs - 100 scl = 1.13 if self._is_main_menu else 0.68 - self._lineup_tex = ba.gettexture('playerLineup') - angry_computer_transparent_model = ba.getmodel( + self._lineup_tex = bui.gettexture('playerLineup') + angry_computer_transparent_mesh = bui.getmesh( 'angryComputerTransparent' ) - self._lineup_1_transparent_model = ba.getmodel( + self._lineup_1_transparent_mesh = bui.getmesh( 'playerLineup1Transparent' ) - self._lineup_2_transparent_model = ba.getmodel( + self._lineup_2_transparent_mesh = bui.getmesh( 'playerLineup2Transparent' ) - self._lineup_3_transparent_model = ba.getmodel( + self._lineup_3_transparent_mesh = bui.getmesh( 'playerLineup3Transparent' ) - self._lineup_4_transparent_model = ba.getmodel( + self._lineup_4_transparent_mesh = bui.getmesh( 'playerLineup4Transparent' ) - self._eyes_model = ba.getmodel('plasticEyesTransparent') + self._eyes_mesh = bui.getmesh('plasticEyesTransparent') - self._coop_button: ba.Widget | None = None + self._coop_button: bui.Widget | None = None # Only show coop button in main-menu variant. if self._is_main_menu: - self._coop_button = btn = ba.buttonwidget( + self._coop_button = btn = bui.buttonwidget( parent=self._root_widget, - position=(hoffs, v + (scl * 15 if self._is_main_menu else 0)), + position=(hoffs, v + (scl * 15)), size=( scl * button_width, - scl * (300 if self._is_main_menu else 360), + scl * 300, ), extra_touch_border_scale=0.1, autoselect=True, @@ -151,20 +155,18 @@ class PlayWindow(ba.Window): on_activate_call=self._coop, ) - if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: - ba.widget( + if bui.app.classic.ui.use_toolbars and uiscale is bui.UIScale.SMALL: + bui.widget( edit=btn, - left_widget=ba.internal.get_special_widget('back_button'), + left_widget=bui.get_special_widget('back_button'), ) - ba.widget( + bui.widget( edit=btn, - up_widget=ba.internal.get_special_widget('account_button'), + up_widget=bui.get_special_widget('account_button'), ) - ba.widget( + bui.widget( edit=btn, - down_widget=ba.internal.get_special_widget( - 'settings_button' - ), + down_widget=bui.get_special_widget('settings_button'), ) self._draw_dude( @@ -197,21 +199,21 @@ class PlayWindow(ba.Window): self._draw_dude( 3, btn, hoffs, v, scl, position=(255, 57), color=(0.7, 0.3, 1.0) ) - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, draw_controller=btn, position=(hoffs + scl * 230, v + scl * 153), size=(scl * 115, scl * 115), texture=self._lineup_tex, - model_transparent=angry_computer_transparent_model, + mesh_transparent=angry_computer_transparent_mesh, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, draw_controller=btn, position=(hoffs + scl * (-10), v + scl * 95), size=(scl * button_width, scl * 50), - text=ba.Lstr( + text=bui.Lstr( resource='playModes.singlePlayerCoopText', fallback_resource='playModes.coopText', ), @@ -223,12 +225,12 @@ class PlayWindow(ba.Window): scale=scl * 2.3, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, draw_controller=btn, position=(hoffs + scl * (-10), v + (scl * 54)), size=(scl * button_width, scl * 30), - text=ba.Lstr(resource=self._r + '.oneToFourPlayersText'), + text=bui.Lstr(resource=self._r + '.oneToFourPlayersText'), h_align='center', v_align='center', scale=0.83 * scl, @@ -241,7 +243,7 @@ class PlayWindow(ba.Window): hoffs += 440 if self._is_main_menu else 216 v += 180 if self._is_main_menu else -68 - self._teams_button = btn = ba.buttonwidget( + self._teams_button = btn = bui.buttonwidget( parent=self._root_widget, position=(hoffs, v + (scl * 15 if self._is_main_menu else 0)), size=( @@ -256,11 +258,11 @@ class PlayWindow(ba.Window): on_activate_call=self._team_tourney, ) - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - up_widget=ba.internal.get_special_widget('tickets_plus_button'), - right_widget=ba.internal.get_special_widget('party_button'), + up_widget=bui.get_special_widget('tickets_plus_button'), + right_widget=bui.get_special_widget('party_button'), ) xxx = -14 @@ -339,12 +341,12 @@ class PlayWindow(ba.Window): color=(1.0, 0.5, 0.5), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, draw_controller=btn, position=(hoffs + scl * (-10), v + scl * 95), size=(scl * button_width, scl * 50), - text=ba.Lstr( + text=bui.Lstr( resource='playModes.teamsText', fallback_resource='teamsText' ), res_scale=1.5, @@ -354,12 +356,12 @@ class PlayWindow(ba.Window): color=(0.7, 0.9, 0.7, 1.0), scale=scl * 2.3, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, draw_controller=btn, position=(hoffs + scl * (-10), v + (scl * 54)), size=(scl * button_width, scl * 30), - text=ba.Lstr(resource=self._r + '.twoToEightPlayersText'), + text=bui.Lstr(resource=self._r + '.twoToEightPlayersText'), h_align='center', v_align='center', res_scale=1.5, @@ -371,7 +373,7 @@ class PlayWindow(ba.Window): hoffs += 0 if self._is_main_menu else 300 v -= 155 if self._is_main_menu else 0 - self._free_for_all_button = btn = ba.buttonwidget( + self._free_for_all_button = btn = bui.buttonwidget( parent=self._root_widget, position=(hoffs, v + (scl * 15 if self._is_main_menu else 0)), size=( @@ -460,12 +462,12 @@ class PlayWindow(ba.Window): position=(xxx + 266, 53), color=(0.4, 0.5, 0.8), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, draw_controller=btn, position=(hoffs + scl * (-10), v + scl * 95), size=(scl * button_width, scl * 50), - text=ba.Lstr( + text=bui.Lstr( resource='playModes.freeForAllText', fallback_resource='freeForAllText', ), @@ -475,12 +477,12 @@ class PlayWindow(ba.Window): color=(0.7, 0.9, 0.7, 1.0), scale=scl * 1.9, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, draw_controller=btn, position=(hoffs + scl * (-10), v + (scl * 54)), size=(scl * button_width, scl * 30), - text=ba.Lstr(resource=self._r + '.twoToEightPlayersText'), + text=bui.Lstr(resource=self._r + '.twoToEightPlayersText'), h_align='center', v_align='center', scale=0.9 * scl, @@ -489,9 +491,9 @@ class PlayWindow(ba.Window): color=clr, ) - if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: + if bui.app.classic.ui.use_toolbars and uiscale is bui.UIScale.SMALL: back_button.delete() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, on_cancel_call=self._back, selected_child=self._coop_button @@ -499,8 +501,8 @@ class PlayWindow(ba.Window): else self._teams_button, ) else: - ba.buttonwidget(edit=back_button, on_activate_call=self._back) - ba.containerwidget( + bui.buttonwidget(edit=back_button, on_activate_call=self._back) + bui.containerwidget( edit=self._root_widget, cancel_button=back_button, selected_child=self._coop_button @@ -525,20 +527,22 @@ class PlayWindow(ba.Window): from bastd.ui.mainmenu import MainMenuWindow self._save_state() - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( MainMenuWindow(transition='in_left').get_root_widget() ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) else: from bastd.ui.gather import GatherWindow self._save_state() - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( GatherWindow(transition='in_left').get_root_widget() ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) @@ -547,12 +551,16 @@ class PlayWindow(ba.Window): from bastd.ui.account import show_sign_in_prompt from bastd.ui.coop.browser import CoopBrowserWindow - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( CoopBrowserWindow(origin_widget=self._coop_button).get_root_widget() ) @@ -561,10 +569,11 @@ class PlayWindow(ba.Window): from bastd.ui.playlist.browser import PlaylistBrowserWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( PlaylistBrowserWindow( - origin_widget=self._teams_button, sessiontype=ba.DualTeamSession + origin_widget=self._teams_button, sessiontype=bs.DualTeamSession ).get_root_widget() ) @@ -573,18 +582,19 @@ class PlayWindow(ba.Window): from bastd.ui.playlist.browser import PlaylistBrowserWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( PlaylistBrowserWindow( origin_widget=self._free_for_all_button, - sessiontype=ba.FreeForAllSession, + sessiontype=bs.FreeForAllSession, ).get_root_widget() ) def _draw_dude( self, i: int, - btn: ba.Widget, + btn: bui.Widget, hoffs: float, v: float, scl: float, @@ -599,7 +609,7 @@ class PlayWindow(ba.Window): 0.7 * 1.0 + 0.3 * color[2], ) if i == 0: - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, draw_controller=btn, position=( @@ -609,9 +619,9 @@ class PlayWindow(ba.Window): size=(scl * 60, scl * 80), color=color, texture=self._lineup_tex, - model_transparent=self._lineup_1_transparent_model, + mesh_transparent=self._lineup_1_transparent_mesh, ) - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, draw_controller=btn, position=( @@ -621,10 +631,10 @@ class PlayWindow(ba.Window): size=(scl * 36, scl * 18), texture=self._lineup_tex, color=eye_color, - model_transparent=self._eyes_model, + mesh_transparent=self._eyes_mesh, ) elif i == 1: - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, draw_controller=btn, position=( @@ -634,9 +644,9 @@ class PlayWindow(ba.Window): size=(scl * 45, scl * 90), color=color, texture=self._lineup_tex, - model_transparent=self._lineup_2_transparent_model, + mesh_transparent=self._lineup_2_transparent_mesh, ) - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, draw_controller=btn, position=( @@ -646,10 +656,10 @@ class PlayWindow(ba.Window): size=(scl * 32, scl * 16), texture=self._lineup_tex, color=eye_color, - model_transparent=self._eyes_model, + mesh_transparent=self._eyes_mesh, ) elif i == 2: - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, draw_controller=btn, position=( @@ -659,9 +669,9 @@ class PlayWindow(ba.Window): size=(scl * 45, scl * 90), color=color, texture=self._lineup_tex, - model_transparent=self._lineup_3_transparent_model, + mesh_transparent=self._lineup_3_transparent_mesh, ) - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, draw_controller=btn, position=( @@ -671,10 +681,10 @@ class PlayWindow(ba.Window): size=(scl * 34, scl * 17), texture=self._lineup_tex, color=eye_color, - model_transparent=self._eyes_model, + mesh_transparent=self._eyes_mesh, ) elif i == 3: - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, draw_controller=btn, position=( @@ -684,9 +694,9 @@ class PlayWindow(ba.Window): size=(scl * 48, scl * 96), color=color, texture=self._lineup_tex, - model_transparent=self._lineup_4_transparent_model, + mesh_transparent=self._lineup_4_transparent_mesh, ) - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, draw_controller=btn, position=( @@ -696,7 +706,7 @@ class PlayWindow(ba.Window): size=(scl * 38, scl * 19), texture=self._lineup_tex, color=eye_color, - model_transparent=self._eyes_model, + mesh_transparent=self._eyes_mesh, ) def _save_state(self) -> None: @@ -712,13 +722,15 @@ class PlayWindow(ba.Window): sel_name = 'Back' else: raise ValueError(f'unrecognized selection {sel}') - ba.app.ui.window_states[type(self)] = sel_name + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = sel_name except Exception: - ba.print_exception(f'Error saving state for {self}.') + logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: try: - sel_name = ba.app.ui.window_states.get(type(self)) + assert bui.app.classic is not None + sel_name = bui.app.classic.ui.window_states.get(type(self)) if sel_name == 'Team Games': sel = self._teams_button elif sel_name == 'Co-op Games' and self._coop_button is not None: @@ -733,6 +745,6 @@ class PlayWindow(ba.Window): if self._coop_button is not None else self._teams_button ) - ba.containerwidget(edit=self._root_widget, selected_child=sel) + bui.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: - ba.print_exception(f'Error restoring state for {self}.') + logging.exception('Error restoring state for %s.', self) diff --git a/assets/src/ba_data/python/bastd/ui/playlist/__init__.py b/src/assets/ba_data/python/bastd/ui/playlist/__init__.py similarity index 68% rename from assets/src/ba_data/python/bastd/ui/playlist/__init__.py rename to src/assets/ba_data/python/bastd/ui/playlist/__init__.py index 163487a2..718b952c 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/__init__.py +++ b/src/assets/ba_data/python/bastd/ui/playlist/__init__.py @@ -6,59 +6,61 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba +import babase +import bascenev1 as bs if TYPE_CHECKING: - pass + import baclassic + import bascenev1 as bs # FIXME: Could change this to be a classmethod of session types? class PlaylistTypeVars: """Defines values for a playlist type (config names to use, etc).""" - def __init__(self, sessiontype: type[ba.Session]): - from ba.internal import ( + def __init__(self, sessiontype: type[bs.Session]): + from bascenev1.internal import ( get_default_teams_playlist, get_default_free_for_all_playlist, ) - self.sessiontype: type[ba.Session] + self.sessiontype: type[bs.Session] - if issubclass(sessiontype, ba.DualTeamSession): - play_mode_name = ba.Lstr( + if issubclass(sessiontype, bs.DualTeamSession): + play_mode_name = babase.Lstr( resource='playModes.teamsText', fallback_resource='teamsText' ) self.get_default_list_call = get_default_teams_playlist self.session_type_name = 'ba.DualTeamSession' self.config_name = 'Team Tournament' - self.window_title_name = ba.Lstr( + self.window_title_name = babase.Lstr( resource='playModes.teamsText', fallback_resource='teamsText' ) - self.sessiontype = ba.DualTeamSession + self.sessiontype = bs.DualTeamSession - elif issubclass(sessiontype, ba.FreeForAllSession): - play_mode_name = ba.Lstr( + elif issubclass(sessiontype, bs.FreeForAllSession): + play_mode_name = babase.Lstr( resource='playModes.freeForAllText', fallback_resource='freeForAllText', ) self.get_default_list_call = get_default_free_for_all_playlist self.session_type_name = 'ba.FreeForAllSession' self.config_name = 'Free-for-All' - self.window_title_name = ba.Lstr( + self.window_title_name = babase.Lstr( resource='playModes.freeForAllText', fallback_resource='freeForAllText', ) - self.sessiontype = ba.FreeForAllSession + self.sessiontype = bs.FreeForAllSession else: raise RuntimeError( f'Playlist type vars undefined for sessiontype: {sessiontype}' ) - self.default_list_name = ba.Lstr( + self.default_list_name = babase.Lstr( resource='defaultGameListNameText', subs=[('${PLAYMODE}', play_mode_name)], ) - self.default_new_list_name = ba.Lstr( + self.default_new_list_name = babase.Lstr( resource='defaultNewGameListNameText', subs=[('${PLAYMODE}', play_mode_name)], ) diff --git a/assets/src/ba_data/python/bastd/ui/playlist/addgame.py b/src/assets/ba_data/python/bastd/ui/playlist/addgame.py similarity index 70% rename from assets/src/ba_data/python/bastd/ui/playlist/addgame.py rename to src/assets/ba_data/python/bastd/ui/playlist/addgame.py index 5d1aad26..a984d4a7 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/addgame.py +++ b/src/assets/ba_data/python/bastd/ui/playlist/addgame.py @@ -6,14 +6,14 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba -import ba.internal +import bascenev1 as bs +import bauiv1 as bui if TYPE_CHECKING: from bastd.ui.playlist.editcontroller import PlaylistEditController -class PlaylistAddGameWindow(ba.Window): +class PlaylistAddGameWindow(bui.Window): """Window for selecting a game type to add to a playlist.""" def __init__( @@ -23,76 +23,77 @@ class PlaylistAddGameWindow(ba.Window): ): self._editcontroller = editcontroller self._r = 'addGameWindow' - uiscale = ba.app.ui.uiscale - self._width = 750 if uiscale is ba.UIScale.SMALL else 650 - x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self._width = 750 if uiscale is bui.UIScale.SMALL else 650 + x_inset = 50 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 346 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 380 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 440 ) - top_extra = 30 if uiscale is ba.UIScale.SMALL else 20 + top_extra = 30 if uiscale is bui.UIScale.SMALL else 20 self._scroll_width = 210 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), transition=transition, scale=( 2.17 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.5 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), - stack_offset=(0, 1) if uiscale is ba.UIScale.SMALL else (0, 0), + stack_offset=(0, 1) if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - self._back_button = ba.buttonwidget( + self._back_button = bui.buttonwidget( parent=self._root_widget, position=(58 + x_inset, self._height - 53), size=(165, 70), scale=0.75, text_scale=1.2, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), autoselect=True, button_type='back', on_activate_call=self._back, ) - self._select_button = select_button = ba.buttonwidget( + self._select_button = select_button = bui.buttonwidget( parent=self._root_widget, position=(self._width - (172 + x_inset), self._height - 50), autoselect=True, size=(160, 60), scale=0.75, text_scale=1.2, - label=ba.Lstr(resource='selectText'), + label=bui.Lstr(resource='selectText'), on_activate_call=self._add, ) - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=select_button, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 28), size=(0, 0), scale=1.0, - text=ba.Lstr(resource=self._r + '.titleText'), + text=bui.Lstr(resource=self._r + '.titleText'), h_align='center', - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, maxwidth=250, v_align='center', ) v = self._height - 64 - self._selected_title_text = ba.textwidget( + self._selected_title_text = bui.textwidget( parent=self._root_widget, position=(x_inset + self._scroll_width + 50 + 30, v - 15), size=(0, 0), @@ -104,7 +105,7 @@ class PlaylistAddGameWindow(ba.Window): ) v -= 30 - self._selected_description_text = ba.textwidget( + self._selected_description_text = bui.textwidget( parent=self._root_widget, position=(x_inset + self._scroll_width + 50 + 30, v), size=(0, 0), @@ -118,37 +119,37 @@ class PlaylistAddGameWindow(ba.Window): v = self._height - 60 - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, position=(x_inset + 61, v - scroll_height), size=(self._scroll_width, scroll_height), highlight=False, ) - ba.widget( + bui.widget( edit=self._scrollwidget, up_widget=self._back_button, left_widget=self._back_button, right_widget=select_button, ) - self._column: ba.Widget | None = None + self._column: bui.Widget | None = None v -= 35 - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button, start_button=select_button, ) - self._selected_game_type: type[ba.GameActivity] | None = None + self._selected_game_type: type[bs.GameActivity] | None = None - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) - self._game_types: list[type[ba.GameActivity]] = [] + self._game_types: list[type[bs.GameActivity]] = [] # Get actual games loading in the bg. - ba.app.meta.load_exported_classes( - ba.GameActivity, + bui.app.meta.load_exported_classes( + bs.GameActivity, self._on_game_types_loaded, completion_cb_in_bg_thread=True, ) @@ -158,15 +159,16 @@ class PlaylistAddGameWindow(ba.Window): self._refresh() def _on_game_types_loaded( - self, gametypes: list[type[ba.GameActivity]] + self, gametypes: list[type[bs.GameActivity]] ) -> None: - from ba.internal import get_unowned_game_types + assert bui.app.classic is not None + store = bui.app.classic.store # We asked for a bg thread completion cb so we can do some # filtering here in the bg thread where its not gonna cause hitches. - assert not ba.in_logic_thread() + assert not bui.in_logic_thread() sessiontype = self._editcontroller.get_session_type() - unowned = get_unowned_game_types() + unowned = store.get_unowned_game_types() self._game_types = [ gt for gt in gametypes @@ -177,15 +179,15 @@ class PlaylistAddGameWindow(ba.Window): self._game_types.sort(key=lambda g: g.get_display_string().evaluate()) # Tell ourself to refresh back in the logic thread. - ba.pushcall(self._refresh, from_other_thread=True) + bui.pushcall(self._refresh, from_other_thread=True) def _refresh(self, select_get_more_games_button: bool = False) -> None: - # from ba.internal import get_game_types + # from bui import get_game_types if self._column is not None: self._column.delete() - self._column = ba.columnwidget( + self._column = bui.columnwidget( parent=self._scrollwidget, border=2, margin=0 ) @@ -193,13 +195,9 @@ class PlaylistAddGameWindow(ba.Window): def _doit() -> None: if self._select_button: - ba.timer( - 0.1, - self._select_button.activate, - timetype=ba.TimeType.REAL, - ) + bui.apptimer(0.1, self._select_button.activate) - txt = ba.textwidget( + txt = bui.textwidget( parent=self._column, position=(0, 0), size=(self._width - 88, 24), @@ -208,25 +206,25 @@ class PlaylistAddGameWindow(ba.Window): v_align='center', color=(0.8, 0.8, 0.8, 1.0), maxwidth=self._scroll_width * 0.8, - on_select_call=ba.Call(self._set_selected_game_type, gametype), + on_select_call=bui.Call(self._set_selected_game_type, gametype), always_highlight=True, selectable=True, on_activate_call=_doit, ) if i == 0: - ba.widget(edit=txt, up_widget=self._back_button) + bui.widget(edit=txt, up_widget=self._back_button) - self._get_more_games_button = ba.buttonwidget( + self._get_more_games_button = bui.buttonwidget( parent=self._column, autoselect=True, - label=ba.Lstr(resource=self._r + '.getMoreGamesText'), + label=bui.Lstr(resource=self._r + '.getMoreGamesText'), color=(0.54, 0.52, 0.67), textcolor=(0.7, 0.65, 0.7), on_activate_call=self._on_get_more_games_press, size=(178, 50), ) if select_get_more_games_button: - ba.containerwidget( + bui.containerwidget( edit=self._column, selected_child=self._get_more_games_button, visible_child=self._get_more_games_button, @@ -236,7 +234,10 @@ class PlaylistAddGameWindow(ba.Window): from bastd.ui.account import show_sign_in_prompt from bastd.ui.store.browser import StoreBrowserWindow - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return StoreBrowserWindow( @@ -250,17 +251,17 @@ class PlaylistAddGameWindow(ba.Window): self._refresh(select_get_more_games_button=True) def _add(self) -> None: - ba.internal.lock_all_input() # Make sure no more commands happen. - ba.timer(0.1, ba.internal.unlock_all_input, timetype=ba.TimeType.REAL) + bui.lock_all_input() # Make sure no more commands happen. + bui.apptimer(0.1, bui.unlock_all_input) assert self._selected_game_type is not None self._editcontroller.add_game_type_selected(self._selected_game_type) - def _set_selected_game_type(self, gametype: type[ba.GameActivity]) -> None: + def _set_selected_game_type(self, gametype: type[bs.GameActivity]) -> None: self._selected_game_type = gametype - ba.textwidget( + bui.textwidget( edit=self._selected_title_text, text=gametype.get_display_string() ) - ba.textwidget( + bui.textwidget( edit=self._selected_description_text, text=gametype.get_description_display_string( self._editcontroller.get_session_type() diff --git a/assets/src/ba_data/python/bastd/ui/playlist/browser.py b/src/assets/ba_data/python/bastd/ui/playlist/browser.py similarity index 77% rename from assets/src/ba_data/python/bastd/ui/playlist/browser.py rename to src/assets/ba_data/python/bastd/ui/playlist/browser.py index d5644525..5b575234 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/browser.py +++ b/src/assets/ba_data/python/bastd/ui/playlist/browser.py @@ -4,25 +4,22 @@ from __future__ import annotations +import logging import copy import math -from typing import TYPE_CHECKING -import ba -import ba.internal - -if TYPE_CHECKING: - pass +import bascenev1 as bs +import bauiv1 as bui -class PlaylistBrowserWindow(ba.Window): +class PlaylistBrowserWindow(bui.Window): """Window for starting teams games.""" def __init__( self, - sessiontype: type[ba.Session], + sessiontype: type[bs.Session], transition: str | None = 'in_right', - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements # pylint: disable=cyclic-import @@ -38,101 +35,105 @@ class PlaylistBrowserWindow(ba.Window): self._transition_out = 'out_right' scale_origin = None + assert bui.app.classic is not None + # Store state for when we exit the next game. - if issubclass(sessiontype, ba.DualTeamSession): - ba.app.ui.set_main_menu_location('Team Game Select') - ba.set_analytics_screen('Teams Window') - elif issubclass(sessiontype, ba.FreeForAllSession): - ba.app.ui.set_main_menu_location('Free-for-All Game Select') - ba.set_analytics_screen('FreeForAll Window') + if issubclass(sessiontype, bs.DualTeamSession): + bui.app.classic.ui.set_main_menu_location('Team Game Select') + bui.set_analytics_screen('Teams Window') + elif issubclass(sessiontype, bs.FreeForAllSession): + bui.app.classic.ui.set_main_menu_location( + 'Free-for-All Game Select' + ) + bui.set_analytics_screen('FreeForAll Window') else: raise TypeError(f'Invalid sessiontype: {sessiontype}.') self._pvars = PlaylistTypeVars(sessiontype) self._sessiontype = sessiontype - self._customize_button: ba.Widget | None = None + self._customize_button: bui.Widget | None = None self._sub_width: float | None = None self._sub_height: float | None = None self._ensure_standard_playlists_exist() # Get the current selection (if any). - self._selected_playlist = ba.app.config.get( + self._selected_playlist = bui.app.config.get( self._pvars.config_name + ' Playlist Selection' ) - uiscale = ba.app.ui.uiscale - self._width = 900.0 if uiscale is ba.UIScale.SMALL else 800.0 - x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 + uiscale = bui.app.classic.ui.uiscale + self._width = 900.0 if uiscale is bui.UIScale.SMALL else 800.0 + x_inset = 50 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 480 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 510 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 580 ) - top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), transition=transition, toolbar_visibility='menu_full', scale_origin_stack_offset=scale_origin, scale=( 1.69 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.05 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 0.9 ), stack_offset=(0, -26) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - self._back_button: ba.Widget | None = ba.buttonwidget( + self._back_button: bui.Widget | None = bui.buttonwidget( parent=self._root_widget, position=(59 + x_inset, self._height - 70), size=(120, 60), scale=1.0, on_activate_call=self._on_back_press, autoselect=True, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button ) - txt = self._title_text = ba.textwidget( + txt = self._title_text = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 41), size=(0, 0), text=self._pvars.window_title_name, scale=1.3, res_scale=1.5, - color=ba.app.ui.heading_color, + color=bui.app.classic.ui.heading_color, h_align='center', v_align='center', ) - if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: - ba.textwidget(edit=txt, text='') + if uiscale is bui.UIScale.SMALL and bui.app.classic.ui.use_toolbars: + bui.textwidget(edit=txt, text='') - ba.buttonwidget( + bui.buttonwidget( edit=self._back_button, button_type='backSmall', size=(60, 54), position=(59 + x_inset, self._height - 67), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) - if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: + if uiscale is bui.UIScale.SMALL and bui.app.classic.ui.use_toolbars: self._back_button.delete() self._back_button = None - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, on_cancel_call=self._on_back_press ) scroll_offs = 33 @@ -141,10 +142,10 @@ class PlaylistBrowserWindow(ba.Window): self._scroll_width = self._width - (100 + 2 * x_inset) self._scroll_height = self._height - ( 146 - if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars + if uiscale is bui.UIScale.SMALL and bui.app.classic.ui.use_toolbars else 136 ) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, highlight=False, size=(self._scroll_width, self._scroll_height), @@ -153,38 +154,36 @@ class PlaylistBrowserWindow(ba.Window): 65 + scroll_offs, ), ) - ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) - self._subcontainer: ba.Widget | None = None + bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) + self._subcontainer: bui.Widget | None = None self._config_name_full = self._pvars.config_name + ' Playlists' self._last_config = None # Update now and once per second. # (this should do our initial refresh) self._update() - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - timetype=ba.TimeType.REAL, - repeat=True, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) def _ensure_standard_playlists_exist(self) -> None: + plus = bui.app.plus + assert plus is not None + # On new installations, go ahead and create a few playlists # besides the hard-coded default one: - if not ba.internal.get_v1_account_misc_val( - 'madeStandardPlaylists', False - ): - ba.internal.add_transaction( + if not plus.get_v1_account_misc_val('madeStandardPlaylists', False): + plus.add_v1_account_transaction( { 'type': 'ADD_PLAYLIST', 'playlistType': 'Free-for-All', - 'playlistName': ba.Lstr( + 'playlistName': bui.Lstr( resource='singleGamePlaylistNameText' ) .evaluate() .replace( '${GAME}', - ba.Lstr( + bui.Lstr( translate=('gameNames', 'Death Match') ).evaluate(), ), @@ -212,17 +211,17 @@ class PlaylistBrowserWindow(ba.Window): ], } ) - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'ADD_PLAYLIST', 'playlistType': 'Team Tournament', - 'playlistName': ba.Lstr( + 'playlistName': bui.Lstr( resource='singleGamePlaylistNameText' ) .evaluate() .replace( '${GAME}', - ba.Lstr( + bui.Lstr( translate=('gameNames', 'Capture the Flag') ).evaluate(), ), @@ -266,11 +265,11 @@ class PlaylistBrowserWindow(ba.Window): ], } ) - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'ADD_PLAYLIST', 'playlistType': 'Team Tournament', - 'playlistName': ba.Lstr( + 'playlistName': bui.Lstr( translate=('playlistNames', 'Just Sports') ).evaluate(), 'playlist': [ @@ -295,11 +294,11 @@ class PlaylistBrowserWindow(ba.Window): ], } ) - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'ADD_PLAYLIST', 'playlistType': 'Free-for-All', - 'playlistName': ba.Lstr( + 'playlistName': bui.Lstr( translate=('playlistNames', 'Just Epic') ).evaluate(), 'playlist': [ @@ -316,14 +315,14 @@ class PlaylistBrowserWindow(ba.Window): ], } ) - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'SET_MISC_VAL', 'name': 'madeStandardPlaylists', 'value': True, } ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() def _refresh(self) -> None: # FIXME: Should tidy this up. @@ -332,7 +331,8 @@ class PlaylistBrowserWindow(ba.Window): # pylint: disable=too-many-locals # pylint: disable=too-many-nested-blocks from efro.util import asserttype - from ba.internal import get_map_class, filter_playlist + from bascenev1.internal import get_map_class + from bascenev1.internal import filter_playlist if not self._root_widget: return @@ -341,10 +341,10 @@ class PlaylistBrowserWindow(ba.Window): self._subcontainer.delete() # Make sure config exists. - if self._config_name_full not in ba.app.config: - ba.app.config[self._config_name_full] = {} + if self._config_name_full not in bui.app.config: + bui.app.config[self._config_name_full] = {} - items = list(ba.app.config[self._config_name_full].items()) + items = list(bui.app.config[self._config_name_full].items()) # Make sure everything is unicode. items = [ @@ -369,7 +369,7 @@ class PlaylistBrowserWindow(ba.Window): ) assert self._sub_width is not None assert self._sub_height is not None - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._sub_width, self._sub_height), background=False, @@ -379,29 +379,30 @@ class PlaylistBrowserWindow(ba.Window): for child in children: child.delete() - ba.textwidget( + assert bui.app.classic is not None + bui.textwidget( parent=self._subcontainer, - text=ba.Lstr(resource='playlistsText'), + text=bui.Lstr(resource='playlistsText'), position=(40, self._sub_height - 26), size=(0, 0), scale=1.0, maxwidth=400, - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, h_align='left', v_align='center', ) index = 0 - appconfig = ba.app.config + appconfig = bui.app.config - model_opaque = ba.getmodel('level_select_button_opaque') - model_transparent = ba.getmodel('level_select_button_transparent') - mask_tex = ba.gettexture('mapPreviewMask') + mesh_opaque = bui.getmesh('level_select_button_opaque') + mesh_transparent = bui.getmesh('level_select_button_transparent') + mask_tex = bui.gettexture('mapPreviewMask') h_offs = 225 if count == 1 else 115 if count == 2 else 0 h_offs_bottom = 0 - uiscale = ba.app.ui.uiscale + uiscale = bui.app.classic.ui.uiscale for y in range(rows): for x in range(columns): name = items[index][0] @@ -415,7 +416,7 @@ class PlaylistBrowserWindow(ba.Window): - 47 - (y + 1) * (button_height + 2 * button_buffer_v), ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._subcontainer, button_type='square', size=(button_width, button_height), @@ -426,37 +427,33 @@ class PlaylistBrowserWindow(ba.Window): if ( x == 0 - and ba.app.ui.use_toolbars - and uiscale is ba.UIScale.SMALL + and bui.app.classic.ui.use_toolbars + and uiscale is bui.UIScale.SMALL ): - ba.widget( + bui.widget( edit=btn, - left_widget=ba.internal.get_special_widget( - 'back_button' - ), + left_widget=bui.get_special_widget('back_button'), ) if ( x == columns - 1 - and ba.app.ui.use_toolbars - and uiscale is ba.UIScale.SMALL + and bui.app.classic.ui.use_toolbars + and uiscale is bui.UIScale.SMALL ): - ba.widget( + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget( - 'party_button' - ), + right_widget=bui.get_special_widget('party_button'), ) - ba.buttonwidget( + bui.buttonwidget( edit=btn, - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._on_playlist_press, btn, name ), - on_select_call=ba.Call(self._on_playlist_select, name), + on_select_call=bui.Call(self._on_playlist_select, name), ) - ba.widget(edit=btn, show_buffer_top=50, show_buffer_bottom=50) + bui.widget(edit=btn, show_buffer_top=50, show_buffer_bottom=50) if self._selected_playlist == name: - ba.containerwidget( + bui.containerwidget( edit=self._subcontainer, selected_child=btn, visible_child=btn, @@ -464,16 +461,16 @@ class PlaylistBrowserWindow(ba.Window): if self._back_button is not None: if y == 0: - ba.widget(edit=btn, up_widget=self._back_button) + bui.widget(edit=btn, up_widget=self._back_button) if x == 0: - ba.widget(edit=btn, left_widget=self._back_button) + bui.widget(edit=btn, left_widget=self._back_button) - print_name: str | ba.Lstr | None + print_name: str | bui.Lstr | None if name == '__default__': print_name = self._pvars.default_list_name else: print_name = name - ba.textwidget( + bui.textwidget( parent=self._subcontainer, text=print_name, position=( @@ -521,10 +518,10 @@ class PlaylistBrowserWindow(ba.Window): ) for entry in playlist: mapname = entry['settings']['map'] - maptype: type[ba.Map] | None + maptype: type[bs.Map] | None try: maptype = get_map_class(mapname) - except ba.NotFoundError: + except bui.NotFoundError: maptype = None if maptype is not None: tex_name = maptype.get_preview_texture_name() @@ -581,34 +578,34 @@ class PlaylistBrowserWindow(ba.Window): h = pos[0] + h_offs_img + scl * 250 * col v = pos[1] + v_offs_img - scl * 130 * row map_images.append( - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, size=(scl * 250.0, scl * 125.0), position=(h, v), - texture=ba.gettexture(tex_name), + texture=bui.gettexture(tex_name), opacity=1.0 if owned else 0.25, draw_controller=btn, - model_opaque=model_opaque, - model_transparent=model_transparent, + mesh_opaque=mesh_opaque, + mesh_transparent=mesh_transparent, mask_texture=mask_tex, ) ) if not owned: - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, size=(scl * 100.0, scl * 100.0), position=(h + scl * 75, v + scl * 10), - texture=ba.gettexture('lock'), + texture=bui.gettexture('lock'), draw_controller=btn, ) if v is not None: v -= scl * 130.0 except Exception: - ba.print_exception('Error listing playlist maps.') + logging.exception('Error listing playlist maps.') if not map_images: - ba.textwidget( + bui.textwidget( parent=self._subcontainer, text='???', scale=1.5, @@ -629,48 +626,49 @@ class PlaylistBrowserWindow(ba.Window): break if index >= count: break - self._customize_button = btn = ba.buttonwidget( + self._customize_button = btn = bui.buttonwidget( parent=self._subcontainer, size=(100, 30), position=(34 + h_offs_bottom, 50), text_scale=0.6, - label=ba.Lstr(resource='customizeText'), + label=bui.Lstr(resource='customizeText'), on_activate_call=self._on_customize_press, color=(0.54, 0.52, 0.67), textcolor=(0.7, 0.65, 0.7), autoselect=True, ) - ba.widget(edit=btn, show_buffer_top=22, show_buffer_bottom=28) + bui.widget(edit=btn, show_buffer_top=22, show_buffer_bottom=28) self._restore_state() def on_play_options_window_run_game(self) -> None: """(internal)""" if not self._root_widget: return - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.containerwidget(edit=self._root_widget, transition='out_left') def _on_playlist_select(self, playlist_name: str) -> None: self._selected_playlist = playlist_name def _update(self) -> None: - # make sure config exists - if self._config_name_full not in ba.app.config: - ba.app.config[self._config_name_full] = {} + if self._config_name_full not in bui.app.config: + bui.app.config[self._config_name_full] = {} - cfg = ba.app.config[self._config_name_full] + cfg = bui.app.config[self._config_name_full] if cfg != self._last_config: self._last_config = copy.deepcopy(cfg) self._refresh() - def _on_playlist_press(self, button: ba.Widget, playlist_name: str) -> None: + def _on_playlist_press( + self, button: bui.Widget, playlist_name: str + ) -> None: # pylint: disable=cyclic-import from bastd.ui.playoptions import PlayOptionsWindow # Make sure the target playlist still exists. exists = ( playlist_name == '__default__' - or playlist_name in ba.app.config.get(self._config_name_full, {}) + or playlist_name in bui.app.config.get(self._config_name_full, {}) ) if not exists: return @@ -690,8 +688,9 @@ class PlaylistBrowserWindow(ba.Window): ) self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( PlaylistCustomizeBrowserWindow( origin_widget=self._customize_button, sessiontype=self._sessiontype, @@ -704,21 +703,22 @@ class PlaylistBrowserWindow(ba.Window): # Store our selected playlist if that's changed. if self._selected_playlist is not None: - prev_sel = ba.app.config.get( + prev_sel = bui.app.config.get( self._pvars.config_name + ' Playlist Selection' ) if self._selected_playlist != prev_sel: - cfg = ba.app.config + cfg = bui.app.config cfg[ self._pvars.config_name + ' Playlist Selection' ] = self._selected_playlist cfg.commit() self._save_state() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( PlayWindow(transition='in_left').get_root_widget() ) @@ -735,27 +735,29 @@ class PlaylistBrowserWindow(ba.Window): else: sel_name = 'Scroll' else: - raise Exception('unrecognized selected widget') - ba.app.ui.window_states[type(self)] = sel_name + raise RuntimeError('Unrecognized selected widget.') + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = sel_name except Exception: - ba.print_exception(f'Error saving state for {self}.') + logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: try: - sel_name = ba.app.ui.window_states.get(type(self)) + assert bui.app.classic is not None + sel_name = bui.app.classic.ui.window_states.get(type(self)) if sel_name == 'Back': sel = self._back_button elif sel_name == 'Scroll': sel = self._scrollwidget elif sel_name == 'Customize': sel = self._scrollwidget - ba.containerwidget( + bui.containerwidget( edit=self._subcontainer, selected_child=self._customize_button, visible_child=self._customize_button, ) else: sel = self._scrollwidget - ba.containerwidget(edit=self._root_widget, selected_child=sel) + bui.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: - ba.print_exception(f'Error restoring state for {self}.') + logging.exception('Error restoring state for %s.', self) diff --git a/assets/src/ba_data/python/bastd/ui/playlist/customizebrowser.py b/src/assets/ba_data/python/bastd/ui/playlist/customizebrowser.py similarity index 70% rename from assets/src/ba_data/python/bastd/ui/playlist/customizebrowser.py rename to src/assets/ba_data/python/bastd/ui/playlist/customizebrowser.py index c8b78a53..3c8135a0 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/customizebrowser.py +++ b/src/assets/ba_data/python/bastd/ui/playlist/customizebrowser.py @@ -6,24 +6,25 @@ from __future__ import annotations import copy import time +import logging from typing import TYPE_CHECKING -import ba -import ba.internal +import bascenev1 as bs +import bauiv1 as bui if TYPE_CHECKING: from typing import Any -class PlaylistCustomizeBrowserWindow(ba.Window): +class PlaylistCustomizeBrowserWindow(bui.Window): """Window for viewing a playlist.""" def __init__( self, - sessiontype: type[ba.Session], + sessiontype: type[bs.Session], transition: str = 'in_right', select_playlist: str | None = None, - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): # Yes this needs tidying. # pylint: disable=too-many-locals @@ -44,85 +45,86 @@ class PlaylistCustomizeBrowserWindow(ba.Window): self._pvars = playlist.PlaylistTypeVars(sessiontype) self._max_playlists = 30 self._r = 'gameListWindow' - uiscale = ba.app.ui.uiscale - self._width = 750.0 if uiscale is ba.UIScale.SMALL else 650.0 - x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self._width = 750.0 if uiscale is bui.UIScale.SMALL else 650.0 + x_inset = 50.0 if uiscale is bui.UIScale.SMALL else 0.0 self._height = ( 380.0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 420.0 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 500.0 ) - top_extra = 20.0 if uiscale is ba.UIScale.SMALL else 0.0 + top_extra = 20.0 if uiscale is bui.UIScale.SMALL else 0.0 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), transition=transition, scale_origin_stack_offset=scale_origin, scale=( 2.05 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.5 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -10) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - self._back_button = back_button = btn = ba.buttonwidget( + self._back_button = back_button = btn = bui.buttonwidget( parent=self._root_widget, position=(43 + x_inset, self._height - 60), size=(160, 68), scale=0.77, autoselect=True, text_scale=1.3, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(0, self._height - 47), size=(self._width, 25), - text=ba.Lstr( + text=bui.Lstr( resource=self._r + '.titleText', subs=[('${TYPE}', self._pvars.window_title_name)], ), - color=ba.app.ui.heading_color, + color=bui.app.classic.ui.heading_color, maxwidth=290, h_align='center', v_align='center', ) - ba.buttonwidget( + bui.buttonwidget( edit=btn, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) v = self._height - 59.0 h = 41 + x_inset b_color = (0.6, 0.53, 0.63) b_textcolor = (0.75, 0.7, 0.8) - self._lock_images: list[ba.Widget] = [] - lock_tex = ba.gettexture('lock') + self._lock_images: list[bui.Widget] = [] + lock_tex = bui.gettexture('lock') scl = ( 1.1 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.27 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.57 ) scl *= 0.63 v -= 65.0 * scl - new_button = btn = ba.buttonwidget( + new_button = btn = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(90, 58.0 * scl), @@ -132,12 +134,12 @@ class PlaylistCustomizeBrowserWindow(ba.Window): button_type='square', textcolor=b_textcolor, text_scale=0.7, - label=ba.Lstr( + label=bui.Lstr( resource='newText', fallback_resource=self._r + '.newText' ), ) self._lock_images.append( - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, size=(30, 30), draw_controller=btn, @@ -147,7 +149,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): ) v -= 65.0 * scl - self._edit_button = edit_button = btn = ba.buttonwidget( + self._edit_button = edit_button = btn = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(90, 58.0 * scl), @@ -157,12 +159,12 @@ class PlaylistCustomizeBrowserWindow(ba.Window): textcolor=b_textcolor, button_type='square', text_scale=0.7, - label=ba.Lstr( + label=bui.Lstr( resource='editText', fallback_resource=self._r + '.editText' ), ) self._lock_images.append( - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, size=(30, 30), draw_controller=btn, @@ -172,7 +174,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): ) v -= 65.0 * scl - duplicate_button = btn = ba.buttonwidget( + duplicate_button = btn = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(90, 58.0 * scl), @@ -182,13 +184,13 @@ class PlaylistCustomizeBrowserWindow(ba.Window): textcolor=b_textcolor, button_type='square', text_scale=0.7, - label=ba.Lstr( + label=bui.Lstr( resource='duplicateText', fallback_resource=self._r + '.duplicateText', ), ) self._lock_images.append( - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, size=(30, 30), draw_controller=btn, @@ -198,7 +200,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): ) v -= 65.0 * scl - delete_button = btn = ba.buttonwidget( + delete_button = btn = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(90, 58.0 * scl), @@ -208,12 +210,12 @@ class PlaylistCustomizeBrowserWindow(ba.Window): textcolor=b_textcolor, button_type='square', text_scale=0.7, - label=ba.Lstr( + label=bui.Lstr( resource='deleteText', fallback_resource=self._r + '.deleteText' ), ) self._lock_images.append( - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, size=(30, 30), draw_controller=btn, @@ -222,7 +224,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): ) ) v -= 65.0 * scl - self._import_button = ba.buttonwidget( + self._import_button = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(90, 58.0 * scl), @@ -232,10 +234,10 @@ class PlaylistCustomizeBrowserWindow(ba.Window): textcolor=b_textcolor, button_type='square', text_scale=0.7, - label=ba.Lstr(resource='importText'), + label=bui.Lstr(resource='importText'), ) v -= 65.0 * scl - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(90, 58.0 * scl), @@ -245,10 +247,10 @@ class PlaylistCustomizeBrowserWindow(ba.Window): textcolor=b_textcolor, button_type='square', text_scale=0.7, - label=ba.Lstr(resource='shareText'), + label=bui.Lstr(resource='shareText'), ) self._lock_images.append( - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, size=(30, 30), draw_controller=btn, @@ -259,81 +261,80 @@ class PlaylistCustomizeBrowserWindow(ba.Window): v = self._height - 75 self._scroll_height = self._height - 119 - scrollwidget = ba.scrollwidget( + scrollwidget = bui.scrollwidget( parent=self._root_widget, position=(140 + x_inset, v - self._scroll_height), size=(self._width - (180 + 2 * x_inset), self._scroll_height + 10), highlight=False, ) - ba.widget(edit=back_button, right_widget=scrollwidget) - self._columnwidget = ba.columnwidget( + bui.widget(edit=back_button, right_widget=scrollwidget) + self._columnwidget = bui.columnwidget( parent=scrollwidget, border=2, margin=0 ) h = 145 - self._do_randomize_val = ba.app.config.get( + self._do_randomize_val = bui.app.config.get( self._pvars.config_name + ' Playlist Randomize', 0 ) h += 210 for btn in [new_button, delete_button, edit_button, duplicate_button]: - ba.widget(edit=btn, right_widget=scrollwidget) - ba.widget( + bui.widget(edit=btn, right_widget=scrollwidget) + bui.widget( edit=scrollwidget, left_widget=new_button, - right_widget=ba.internal.get_special_widget('party_button') - if ba.app.ui.use_toolbars + right_widget=bui.get_special_widget('party_button') + if bui.app.classic.ui.use_toolbars else None, ) # make sure config exists self._config_name_full = self._pvars.config_name + ' Playlists' - if self._config_name_full not in ba.app.config: - ba.app.config[self._config_name_full] = {} + if self._config_name_full not in bui.app.config: + bui.app.config[self._config_name_full] = {} self._selected_playlist_name: str | None = None self._selected_playlist_index: int | None = None - self._playlist_widgets: list[ba.Widget] = [] + self._playlist_widgets: list[bui.Widget] = [] self._refresh(select_playlist=select_playlist) - ba.buttonwidget(edit=back_button, on_activate_call=self._back) - ba.containerwidget(edit=self._root_widget, cancel_button=back_button) + bui.buttonwidget(edit=back_button, on_activate_call=self._back) + bui.containerwidget(edit=self._root_widget, cancel_button=back_button) - ba.containerwidget(edit=self._root_widget, selected_child=scrollwidget) + bui.containerwidget(edit=self._root_widget, selected_child=scrollwidget) # Keep our lock images up to date/etc. - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - timetype=ba.TimeType.REAL, - repeat=True, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) self._update() def _update(self) -> None: - have = ba.app.accounts_v1.have_pro_options() + assert bui.app.classic is not None + have = bui.app.classic.accounts.have_pro_options() for lock in self._lock_images: - ba.imagewidget(edit=lock, opacity=0.0 if have else 1.0) + bui.imagewidget(edit=lock, opacity=0.0 if have else 1.0) def _back(self) -> None: # pylint: disable=cyclic-import from bastd.ui.playlist import browser if self._selected_playlist_name is not None: - cfg = ba.app.config + cfg = bui.app.config cfg[ self._pvars.config_name + ' Playlist Selection' ] = self._selected_playlist_name cfg.commit() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( browser.PlaylistBrowserWindow( transition='in_left', sessiontype=self._sessiontype ).get_root_widget() @@ -345,24 +346,24 @@ class PlaylistCustomizeBrowserWindow(ba.Window): def _run_selected_playlist(self) -> None: # pylint: disable=cyclic-import - ba.internal.unlock_all_input() + bui.unlock_all_input() try: - ba.internal.new_host_session(self._sessiontype) + bs.new_host_session(self._sessiontype) except Exception: from bastd import mainmenu - ba.print_exception(f'Error running session {self._sessiontype}.') + logging.exception('Error running session %s.', self._sessiontype) # Drop back into a main menu session. - ba.internal.new_host_session(mainmenu.MainMenuSession) + bs.new_host_session(mainmenu.MainMenuSession) def _choose_playlist(self) -> None: if self._selected_playlist_name is None: return self._save_playlist_selection() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.internal.fade_screen(False, endcall=self._run_selected_playlist) - ba.internal.lock_all_input() + bui.containerwidget(edit=self._root_widget, transition='out_left') + bui.fade_screen(False, endcall=self._run_selected_playlist) + bui.lock_all_input() def _refresh(self, select_playlist: str | None = None) -> None: from efro.util import asserttype @@ -371,7 +372,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): # If there was no prev selection, look in prefs. if old_selection is None: - old_selection = ba.app.config.get( + old_selection = bui.app.config.get( self._pvars.config_name + ' Playlist Selection' ) @@ -381,7 +382,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): while self._playlist_widgets: self._playlist_widgets.pop().delete() - items = list(ba.app.config[self._config_name_full].items()) + items = list(bui.app.config[self._config_name_full].items()) # Make sure everything is unicode now. items = [ @@ -395,7 +396,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): index = 0 for pname, _ in items: assert pname is not None - txtw = ba.textwidget( + txtw = bui.textwidget( parent=self._columnwidget, size=(self._width - 40, 30), maxwidth=self._width - 110, @@ -406,22 +407,22 @@ class PlaylistCustomizeBrowserWindow(ba.Window): if pname == '__default__' else (0.85, 0.85, 0.85, 1), always_highlight=True, - on_select_call=ba.Call(self._select, pname, index), - on_activate_call=ba.Call(self._edit_button.activate), + on_select_call=bui.Call(self._select, pname, index), + on_activate_call=bui.Call(self._edit_button.activate), selectable=True, ) - ba.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50) + bui.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50) # Hitting up from top widget should jump to 'back' if index == 0: - ba.widget(edit=txtw, up_widget=self._back_button) + bui.widget(edit=txtw, up_widget=self._back_button) self._playlist_widgets.append(txtw) # Select this one if the user requested it. if select_playlist is not None: if pname == select_playlist: - ba.columnwidget( + bui.columnwidget( edit=self._columnwidget, selected_child=txtw, visible_child=txtw, @@ -431,14 +432,14 @@ class PlaylistCustomizeBrowserWindow(ba.Window): # Go by index if there's one. if old_selection_index is not None: if index == old_selection_index: - ba.columnwidget( + bui.columnwidget( edit=self._columnwidget, selected_child=txtw, visible_child=txtw, ) else: # Otherwise look by name. if pname == old_selection: - ba.columnwidget( + bui.columnwidget( edit=self._columnwidget, selected_child=txtw, visible_child=txtw, @@ -451,7 +452,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): # This serves dual purposes of letting us re-select it next time # if we want and also lets us pass it to the game (since we reset # the whole python environment that's not actually easy). - cfg = ba.app.config + cfg = bui.app.config cfg[ self._pvars.config_name + ' Playlist Selection' ] = self._selected_playlist_name @@ -465,14 +466,15 @@ class PlaylistCustomizeBrowserWindow(ba.Window): from bastd.ui.playlist.editcontroller import PlaylistEditController from bastd.ui.purchase import PurchaseWindow - if not ba.app.accounts_v1.have_pro_options(): + assert bui.app.classic is not None + if not bui.app.classic.accounts.have_pro_options(): PurchaseWindow(items=['pro']) return # Clamp at our max playlist number. - if len(ba.app.config[self._config_name_full]) > self._max_playlists: - ba.screenmessage( - ba.Lstr( + if len(bui.app.config[self._config_name_full]) > self._max_playlists: + bui.screenmessage( + bui.Lstr( translate=( 'serverResponses', 'Max number of playlists reached.', @@ -480,7 +482,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): ), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return # In case they cancel so we can return to this state. @@ -488,47 +490,52 @@ class PlaylistCustomizeBrowserWindow(ba.Window): # Kick off the edit UI. PlaylistEditController(sessiontype=self._sessiontype) - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.containerwidget(edit=self._root_widget, transition='out_left') def _edit_playlist(self) -> None: # pylint: disable=cyclic-import from bastd.ui.playlist.editcontroller import PlaylistEditController from bastd.ui.purchase import PurchaseWindow - if not ba.app.accounts_v1.have_pro_options(): + assert bui.app.classic is not None + if not bui.app.classic.accounts.have_pro_options(): PurchaseWindow(items=['pro']) return if self._selected_playlist_name is None: return if self._selected_playlist_name == '__default__': - ba.playsound(ba.getsound('error')) - ba.screenmessage(ba.Lstr(resource=self._r + '.cantEditDefaultText')) + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource=self._r + '.cantEditDefaultText') + ) return self._save_playlist_selection() PlaylistEditController( existing_playlist_name=self._selected_playlist_name, sessiontype=self._sessiontype, ) - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.containerwidget(edit=self._root_widget, transition='out_left') def _do_delete_playlist(self) -> None: - ba.internal.add_transaction( + plus = bui.app.plus + assert plus is not None + plus.add_v1_account_transaction( { 'type': 'REMOVE_PLAYLIST', 'playlistType': self._pvars.config_name, 'playlistName': self._selected_playlist_name, } ) - ba.internal.run_transactions() - ba.playsound(ba.getsound('shieldDown')) + plus.run_v1_account_transactions() + bui.getsound('shieldDown').play() # (we don't use len()-1 here because the default list adds one) assert self._selected_playlist_index is not None if self._selected_playlist_index > len( - ba.app.config[self._pvars.config_name + ' Playlists'] + bui.app.config[self._pvars.config_name + ' Playlists'] ): self._selected_playlist_index = len( - ba.app.config[self._pvars.config_name + ' Playlists'] + bui.app.config[self._pvars.config_name + ' Playlists'] ) self._refresh() @@ -536,17 +543,20 @@ class PlaylistCustomizeBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.playlist import share + plus = bui.app.plus + assert plus is not None + # Gotta be signed in for this to work. - if ba.internal.get_v1_account_state() != 'signed_in': - ba.screenmessage( - ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) + if plus.get_v1_account_state() != 'signed_in': + bui.screenmessage( + bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return share.SharePlaylistImportWindow( origin_widget=self._import_button, - on_success_callback=ba.WeakCall(self._on_playlist_import_success), + on_success_callback=bui.WeakCall(self._on_playlist_import_success), ) def _on_playlist_import_success(self) -> None: @@ -557,11 +567,11 @@ class PlaylistCustomizeBrowserWindow(ba.Window): from bastd.ui.playlist import share if response is None: - ba.screenmessage( - ba.Lstr(resource='internal.unavailableNoConnectionText'), + bui.screenmessage( + bui.Lstr(resource='internal.unavailableNoConnectionText'), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return share.SharePlaylistResultsWindow(name, response) @@ -569,21 +579,25 @@ class PlaylistCustomizeBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.purchase import PurchaseWindow - if not ba.app.accounts_v1.have_pro_options(): + plus = bui.app.plus + assert plus is not None + + assert bui.app.classic is not None + if not bui.app.classic.accounts.have_pro_options(): PurchaseWindow(items=['pro']) return # Gotta be signed in for this to work. - if ba.internal.get_v1_account_state() != 'signed_in': - ba.screenmessage( - ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) + if plus.get_v1_account_state() != 'signed_in': + bui.screenmessage( + bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return if self._selected_playlist_name == '__default__': - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource=self._r + '.cantShareDefaultText'), + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource=self._r + '.cantShareDefaultText'), color=(1, 0, 0), ) return @@ -591,39 +605,40 @@ class PlaylistCustomizeBrowserWindow(ba.Window): if self._selected_playlist_name is None: return - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'SHARE_PLAYLIST', 'expire_time': time.time() + 5, 'playlistType': self._pvars.config_name, 'playlistName': self._selected_playlist_name, }, - callback=ba.WeakCall( + callback=bui.WeakCall( self._on_share_playlist_response, self._selected_playlist_name ), ) - ba.internal.run_transactions() - ba.screenmessage(ba.Lstr(resource='sharingText')) + plus.run_v1_account_transactions() + bui.screenmessage(bui.Lstr(resource='sharingText')) def _delete_playlist(self) -> None: # pylint: disable=cyclic-import from bastd.ui.purchase import PurchaseWindow from bastd.ui.confirm import ConfirmWindow - if not ba.app.accounts_v1.have_pro_options(): + assert bui.app.classic is not None + if not bui.app.classic.accounts.have_pro_options(): PurchaseWindow(items=['pro']) return if self._selected_playlist_name is None: return if self._selected_playlist_name == '__default__': - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource=self._r + '.cantDeleteDefaultText') + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource=self._r + '.cantDeleteDefaultText') ) else: ConfirmWindow( - ba.Lstr( + bui.Lstr( resource=self._r + '.deleteConfirmText', subs=[('${LIST}', self._selected_playlist_name)], ), @@ -632,13 +647,13 @@ class PlaylistCustomizeBrowserWindow(ba.Window): 150, ) - def _get_playlist_display_name(self, playlist: str) -> ba.Lstr: + def _get_playlist_display_name(self, playlist: str) -> bui.Lstr: if playlist == '__default__': return self._pvars.default_list_name return ( playlist - if isinstance(playlist, ba.Lstr) - else ba.Lstr(value=playlist) + if isinstance(playlist, bui.Lstr) + else bui.Lstr(value=playlist) ) def _duplicate_playlist(self) -> None: @@ -646,7 +661,11 @@ class PlaylistCustomizeBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.purchase import PurchaseWindow - if not ba.app.accounts_v1.have_pro_options(): + plus = bui.app.plus + assert plus is not None + + assert bui.app.classic is not None + if not bui.app.classic.accounts.have_pro_options(): PurchaseWindow(items=['pro']) return if self._selected_playlist_name is None: @@ -655,17 +674,17 @@ class PlaylistCustomizeBrowserWindow(ba.Window): if self._selected_playlist_name == '__default__': plst = self._pvars.get_default_list_call() else: - plst = ba.app.config[self._config_name_full].get( + plst = bui.app.config[self._config_name_full].get( self._selected_playlist_name ) if plst is None: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return # clamp at our max playlist number - if len(ba.app.config[self._config_name_full]) > self._max_playlists: - ba.screenmessage( - ba.Lstr( + if len(bui.app.config[self._config_name_full]) > self._max_playlists: + bui.screenmessage( + bui.Lstr( translate=( 'serverResponses', 'Max number of playlists reached.', @@ -673,10 +692,10 @@ class PlaylistCustomizeBrowserWindow(ba.Window): ), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return - copy_text = ba.Lstr(resource='copyOfText').evaluate() + copy_text = bui.Lstr(resource='copyOfText').evaluate() # get just 'Copy' or whatnot copy_word = copy_text.replace('${NAME}', '').strip() # find a valid dup name that doesn't exist @@ -697,11 +716,11 @@ class PlaylistCustomizeBrowserWindow(ba.Window): test_name = copy_text.replace('${NAME}', base_name) if test_index > 1: test_name += ' ' + str(test_index) - if test_name not in ba.app.config[self._config_name_full]: + if test_name not in bui.app.config[self._config_name_full]: break test_index += 1 - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'ADD_PLAYLIST', 'playlistType': self._pvars.config_name, @@ -709,7 +728,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): 'playlist': copy.deepcopy(plst), } ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() self._refresh(select_playlist=test_name) diff --git a/assets/src/ba_data/python/bastd/ui/playlist/edit.py b/src/assets/ba_data/python/bastd/ui/playlist/edit.py similarity index 70% rename from assets/src/ba_data/python/bastd/ui/playlist/edit.py rename to src/assets/ba_data/python/bastd/ui/playlist/edit.py index ed9b319f..6d6e887f 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/edit.py +++ b/src/assets/ba_data/python/bastd/ui/playlist/edit.py @@ -4,16 +4,17 @@ from __future__ import annotations +import logging from typing import TYPE_CHECKING, cast -import ba -import ba.internal +import bascenev1 as bs +import bauiv1 as bui if TYPE_CHECKING: from bastd.ui.playlist.editcontroller import PlaylistEditController -class PlaylistEditWindow(ba.Window): +class PlaylistEditWindow(bui.Window): """Window for editing an individual game playlist.""" def __init__( @@ -28,72 +29,73 @@ class PlaylistEditWindow(ba.Window): self._r = 'editGameListWindow' prev_selection = self._editcontroller.get_edit_ui_selection() - uiscale = ba.app.ui.uiscale - self._width = 770 if uiscale is ba.UIScale.SMALL else 670 - x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self._width = 770 if uiscale is bui.UIScale.SMALL else 670 + x_inset = 50 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 400 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 470 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 540 ) - top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), transition=transition, scale=( 2.0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.3 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -16) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - cancel_button = ba.buttonwidget( + cancel_button = bui.buttonwidget( parent=self._root_widget, position=(35 + x_inset, self._height - 60), scale=0.8, size=(175, 60), autoselect=True, - label=ba.Lstr(resource='cancelText'), + label=bui.Lstr(resource='cancelText'), text_scale=1.2, ) - save_button = btn = ba.buttonwidget( + save_button = btn = bui.buttonwidget( parent=self._root_widget, position=(self._width - (195 + x_inset), self._height - 60), scale=0.8, size=(190, 60), autoselect=True, left_widget=cancel_button, - label=ba.Lstr(resource='saveText'), + label=bui.Lstr(resource='saveText'), text_scale=1.2, ) - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - ba.widget( + bui.widget( edit=cancel_button, left_widget=cancel_button, right_widget=save_button, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(-10, self._height - 50), size=(self._width, 25), - text=ba.Lstr(resource=self._r + '.titleText'), - color=ba.app.ui.title_color, + text=bui.Lstr(resource=self._r + '.titleText'), + color=bui.app.classic.ui.title_color, scale=1.05, h_align='center', v_align='center', @@ -104,9 +106,9 @@ class PlaylistEditWindow(ba.Window): self._scroll_width = self._width - (205 + 2 * x_inset) - ba.textwidget( + bui.textwidget( parent=self._root_widget, - text=ba.Lstr(resource=self._r + '.listNameText'), + text=bui.Lstr(resource=self._r + '.listNameText'), position=(196 + x_inset, v + 31), maxwidth=150, color=(0.8, 0.8, 0.8, 0.5), @@ -116,7 +118,7 @@ class PlaylistEditWindow(ba.Window): v_align='center', ) - self._text_field = ba.textwidget( + self._text_field = bui.textwidget( parent=self._root_widget, position=(210 + x_inset, v + 7), size=(self._scroll_width - 53, 43), @@ -126,14 +128,14 @@ class PlaylistEditWindow(ba.Window): max_chars=40, autoselect=True, color=(0.9, 0.9, 0.9, 1.0), - description=ba.Lstr(resource=self._r + '.listNameText'), + description=bui.Lstr(resource=self._r + '.listNameText'), editable=True, padding=4, on_return_press_call=self._save_press_with_sound, ) - ba.widget(edit=cancel_button, down_widget=self._text_field) + bui.widget(edit=cancel_button, down_widget=self._text_field) - self._list_widgets: list[ba.Widget] = [] + self._list_widgets: list[bui.Widget] = [] h = 40 + x_inset v = self._height - 172.0 @@ -146,45 +148,45 @@ class PlaylistEditWindow(ba.Window): scl = ( 1.03 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.36 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.74 ) v -= 63.0 * scl - add_game_button = ba.buttonwidget( + add_game_button = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(110, 61.0 * scl), on_activate_call=self._add, - on_select_call=ba.Call(self._set_ui_selection, 'add_button'), + on_select_call=bui.Call(self._set_ui_selection, 'add_button'), autoselect=True, button_type='square', color=b_color, textcolor=b_textcolor, text_scale=0.8, - label=ba.Lstr(resource=self._r + '.addGameText'), + label=bui.Lstr(resource=self._r + '.addGameText'), ) - ba.widget(edit=add_game_button, up_widget=self._text_field) + bui.widget(edit=add_game_button, up_widget=self._text_field) v -= 63.0 * scl - self._edit_button = edit_game_button = ba.buttonwidget( + self._edit_button = edit_game_button = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(110, 61.0 * scl), on_activate_call=self._edit, - on_select_call=ba.Call(self._set_ui_selection, 'editButton'), + on_select_call=bui.Call(self._set_ui_selection, 'editButton'), autoselect=True, button_type='square', color=b_color, textcolor=b_textcolor, text_scale=0.8, - label=ba.Lstr(resource=self._r + '.editGameText'), + label=bui.Lstr(resource=self._r + '.editGameText'), ) v -= 63.0 * scl - remove_game_button = ba.buttonwidget( + remove_game_button = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(110, 61.0 * scl), @@ -194,16 +196,16 @@ class PlaylistEditWindow(ba.Window): button_type='square', color=b_color, textcolor=b_textcolor, - label=ba.Lstr(resource=self._r + '.removeGameText'), + label=bui.Lstr(resource=self._r + '.removeGameText'), ) v -= 40 h += 9 - ba.buttonwidget( + bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(42, 35), on_activate_call=self._move_up, - label=ba.charstr(ba.SpecialChar.UP_ARROW), + label=bui.charstr(bui.SpecialChar.UP_ARROW), button_type='square', color=b_color, textcolor=b_textcolor, @@ -211,7 +213,7 @@ class PlaylistEditWindow(ba.Window): repeat=True, ) h += 52 - ba.buttonwidget( + bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(42, 35), @@ -220,56 +222,56 @@ class PlaylistEditWindow(ba.Window): button_type='square', color=b_color, textcolor=b_textcolor, - label=ba.charstr(ba.SpecialChar.DOWN_ARROW), + label=bui.charstr(bui.SpecialChar.DOWN_ARROW), repeat=True, ) v = self._height - 100 scroll_height = self._height - 155 - scrollwidget = ba.scrollwidget( + scrollwidget = bui.scrollwidget( parent=self._root_widget, position=(160 + x_inset, v - scroll_height), highlight=False, - on_select_call=ba.Call(self._set_ui_selection, 'gameList'), + on_select_call=bui.Call(self._set_ui_selection, 'gameList'), size=(self._scroll_width, (scroll_height - 15)), ) - ba.widget( + bui.widget( edit=scrollwidget, left_widget=add_game_button, right_widget=scrollwidget, ) - self._columnwidget = ba.columnwidget( + self._columnwidget = bui.columnwidget( parent=scrollwidget, border=2, margin=0 ) - ba.widget(edit=self._columnwidget, up_widget=self._text_field) + bui.widget(edit=self._columnwidget, up_widget=self._text_field) for button in [add_game_button, edit_game_button, remove_game_button]: - ba.widget( + bui.widget( edit=button, left_widget=button, right_widget=scrollwidget ) self._refresh() - ba.buttonwidget(edit=cancel_button, on_activate_call=self._cancel) - ba.containerwidget( + bui.buttonwidget(edit=cancel_button, on_activate_call=self._cancel) + bui.containerwidget( edit=self._root_widget, cancel_button=cancel_button, selected_child=scrollwidget, ) - ba.buttonwidget(edit=save_button, on_activate_call=self._save_press) - ba.containerwidget(edit=self._root_widget, start_button=save_button) + bui.buttonwidget(edit=save_button, on_activate_call=self._save_press) + bui.containerwidget(edit=self._root_widget, start_button=save_button) if prev_selection == 'add_button': - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=add_game_button ) elif prev_selection == 'editButton': - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=edit_game_button ) elif prev_selection == 'gameList': - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=scrollwidget ) @@ -281,9 +283,10 @@ class PlaylistEditWindow(ba.Window): PlaylistCustomizeBrowserWindow, ) - ba.playsound(ba.getsound('powerdown01')) - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.app.ui.set_main_menu_window( + bui.getsound('powerdown01').play() + bui.containerwidget(edit=self._root_widget, transition='out_right') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( PlaylistCustomizeBrowserWindow( transition='in_left', sessiontype=self._editcontroller.get_session_type(), @@ -296,14 +299,14 @@ class PlaylistEditWindow(ba.Window): def _add(self) -> None: # Store list name then tell the session to perform an add. self._editcontroller.setname( - cast(str, ba.textwidget(query=self._text_field)) + cast(str, bui.textwidget(query=self._text_field)) ) self._editcontroller.add_game_pressed() def _edit(self) -> None: # Store list name then tell the session to perform an add. self._editcontroller.setname( - cast(str, ba.textwidget(query=self._text_field)) + cast(str, bui.textwidget(query=self._text_field)) ) self._editcontroller.edit_game_pressed() @@ -312,41 +315,44 @@ class PlaylistEditWindow(ba.Window): PlaylistCustomizeBrowserWindow, ) - new_name = cast(str, ba.textwidget(query=self._text_field)) + plus = bui.app.plus + assert plus is not None + + new_name = cast(str, bui.textwidget(query=self._text_field)) if ( new_name != self._editcontroller.get_existing_playlist_name() and new_name - in ba.app.config[ + in bui.app.config[ self._editcontroller.get_config_name() + ' Playlists' ] ): - ba.screenmessage( - ba.Lstr(resource=self._r + '.cantSaveAlreadyExistsText') + bui.screenmessage( + bui.Lstr(resource=self._r + '.cantSaveAlreadyExistsText') ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return if not new_name: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return if not self._editcontroller.get_playlist(): - ba.screenmessage( - ba.Lstr(resource=self._r + '.cantSaveEmptyListText') + bui.screenmessage( + bui.Lstr(resource=self._r + '.cantSaveEmptyListText') ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return # We couldn't actually replace the default list anyway, but disallow # using its exact name to avoid confusion. if new_name == self._editcontroller.get_default_list_name().evaluate(): - ba.screenmessage( - ba.Lstr(resource=self._r + '.cantOverwriteDefaultText') + bui.screenmessage( + bui.Lstr(resource=self._r + '.cantOverwriteDefaultText') ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return # If we had an old one, delete it. if self._editcontroller.get_existing_playlist_name() is not None: - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'REMOVE_PLAYLIST', 'playlistType': self._editcontroller.get_config_name(), @@ -356,7 +362,7 @@ class PlaylistEditWindow(ba.Window): } ) - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'ADD_PLAYLIST', 'playlistType': self._editcontroller.get_config_name(), @@ -364,11 +370,12 @@ class PlaylistEditWindow(ba.Window): 'playlist': self._editcontroller.get_playlist(), } ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.playsound(ba.getsound('gunCocking')) - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_right') + bui.getsound('gunCocking').play() + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( PlaylistCustomizeBrowserWindow( transition='in_left', sessiontype=self._editcontroller.get_session_type(), @@ -377,15 +384,13 @@ class PlaylistEditWindow(ba.Window): ) def _save_press_with_sound(self) -> None: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._save_press() def _select(self, index: int) -> None: self._editcontroller.set_selected_index(index) def _refresh(self) -> None: - from ba.internal import getclass - # Need to grab this here as rebuilding the list will # change it otherwise. old_selection_index = self._editcontroller.get_selected_index() @@ -393,18 +398,17 @@ class PlaylistEditWindow(ba.Window): while self._list_widgets: self._list_widgets.pop().delete() for index, pentry in enumerate(self._editcontroller.get_playlist()): - try: - cls = getclass(pentry['type'], subclassof=ba.GameActivity) + cls = bui.getclass(pentry['type'], subclassof=bs.GameActivity) desc = cls.get_settings_display_string(pentry) except Exception: - ba.print_exception() + logging.exception('Error in playlist refresh.') desc = "(invalid: '" + pentry['type'] + "')" - txtw = ba.textwidget( + txtw = bui.textwidget( parent=self._columnwidget, size=(self._width - 80, 30), - on_select_call=ba.Call(self._select, index), + on_select_call=bui.Call(self._select, index), always_highlight=True, color=(0.8, 0.8, 0.8, 1.0), padding=0, @@ -414,14 +418,14 @@ class PlaylistEditWindow(ba.Window): v_align='center', selectable=True, ) - ba.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50) + bui.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50) # Wanna be able to jump up to the text field from the top one. if index == 0: - ba.widget(edit=txtw, up_widget=self._text_field) + bui.widget(edit=txtw, up_widget=self._text_field) self._list_widgets.append(txtw) if old_selection_index == index: - ba.columnwidget( + bui.columnwidget( edit=self._columnwidget, selected_child=txtw, visible_child=txtw, @@ -463,5 +467,5 @@ class PlaylistEditWindow(ba.Window): index = len(playlist) - 1 self._editcontroller.set_playlist(playlist) self._editcontroller.set_selected_index(index) - ba.playsound(ba.getsound('shieldDown')) + bui.getsound('shieldDown').play() self._refresh() diff --git a/assets/src/ba_data/python/bastd/ui/playlist/editcontroller.py b/src/assets/ba_data/python/bastd/ui/playlist/editcontroller.py similarity index 79% rename from assets/src/ba_data/python/bastd/ui/playlist/editcontroller.py rename to src/assets/ba_data/python/bastd/ui/playlist/editcontroller.py index 9f978a36..f4a3e98a 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/editcontroller.py +++ b/src/assets/ba_data/python/bastd/ui/playlist/editcontroller.py @@ -7,7 +7,8 @@ from __future__ import annotations import copy from typing import TYPE_CHECKING -import ba +import bascenev1 as bs +import bauiv1 as bui if TYPE_CHECKING: from typing import Any @@ -18,17 +19,18 @@ class PlaylistEditController: def __init__( self, - sessiontype: type[ba.Session], + sessiontype: type[bs.Session], existing_playlist_name: str | None = None, transition: str = 'in_right', playlist: list[dict[str, Any]] | None = None, playlist_name: str | None = None, ): - from ba.internal import preload_map_preview_media, filter_playlist + from bascenev1.internal import filter_playlist + from bascenev1.internal import preload_map_preview_media from bastd.ui.playlist import PlaylistTypeVars from bastd.ui.playlist.edit import PlaylistEditWindow - appconfig = ba.app.config + appconfig = bui.app.config # Since we may be showing our map list momentarily, # lets go ahead and preload all map preview textures. @@ -36,7 +38,7 @@ class PlaylistEditController: self._sessiontype = sessiontype self._editing_game = False - self._editing_game_type: type[ba.GameActivity] | None = None + self._editing_game_type: type[bs.GameActivity] | None = None self._pvars = PlaylistTypeVars(sessiontype) self._existing_playlist_name = existing_playlist_name self._config_name_full = self._pvars.config_name + ' Playlists' @@ -67,7 +69,6 @@ class PlaylistEditController: if playlist_name is not None: self._name = playlist_name else: - # Find a good unused name. i = 1 while True: @@ -86,7 +87,8 @@ class PlaylistEditController: # and that's all they can do. self._edit_ui_selection = 'add_button' - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( PlaylistEditWindow( editcontroller=self, transition=transition ).get_root_widget() @@ -124,15 +126,15 @@ class PlaylistEditController: """Set the playlist contents.""" self._playlist = copy.deepcopy(playlist) - def get_session_type(self) -> type[ba.Session]: - """Return the ba.Session type for this edit-session.""" + def get_session_type(self) -> type[bs.Session]: + """Return the bascenev1.Session type for this edit-session.""" return self._sessiontype def get_selected_index(self) -> int: """Return the index of the selected playlist.""" return self._selected_index - def get_default_list_name(self) -> ba.Lstr: + def get_default_list_name(self) -> bui.Lstr: """(internal)""" return self._pvars.default_list_name @@ -144,21 +146,21 @@ class PlaylistEditController: """(internal)""" from bastd.ui.playlist.addgame import PlaylistAddGameWindow - ba.app.ui.clear_main_menu_window(transition='out_left') - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.clear_main_menu_window(transition='out_left') + bui.app.classic.ui.set_main_menu_window( PlaylistAddGameWindow(editcontroller=self).get_root_widget() ) def edit_game_pressed(self) -> None: """Should be called by supplemental UIs when a game is to be edited.""" - from ba.internal import getclass if not self._playlist: return self._show_edit_ui( - gametype=getclass( + gametype=bui.getclass( self._playlist[self._selected_index]['type'], - subclassof=ba.GameActivity, + subclassof=bs.GameActivity, ), settings=self._playlist[self._selected_index], ) @@ -167,15 +169,16 @@ class PlaylistEditController: """(internal)""" from bastd.ui.playlist.edit import PlaylistEditWindow - ba.app.ui.clear_main_menu_window(transition='out_right') - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.clear_main_menu_window(transition='out_right') + bui.app.classic.ui.set_main_menu_window( PlaylistEditWindow( editcontroller=self, transition='in_left' ).get_root_widget() ) def _show_edit_ui( - self, gametype: type[ba.GameActivity], settings: dict[str, Any] | None + self, gametype: type[bs.GameActivity], settings: dict[str, Any] | None ) -> None: self._editing_game = settings is not None self._editing_game_type = gametype @@ -184,21 +187,23 @@ class PlaylistEditController: self._sessiontype, copy.deepcopy(settings), self._edit_game_done ) - def add_game_type_selected(self, gametype: type[ba.GameActivity]) -> None: + def add_game_type_selected(self, gametype: type[bs.GameActivity]) -> None: """(internal)""" self._show_edit_ui(gametype=gametype, settings=None) def _edit_game_done(self, config: dict[str, Any] | None) -> None: from bastd.ui.playlist.edit import PlaylistEditWindow from bastd.ui.playlist.addgame import PlaylistAddGameWindow - from ba.internal import get_type_name + assert bui.app.classic is not None if config is None: # If we were editing, go back to our list. if self._editing_game: - ba.playsound(ba.getsound('powerdown01')) - ba.app.ui.clear_main_menu_window(transition='out_right') - ba.app.ui.set_main_menu_window( + bui.getsound('powerdown01').play() + bui.app.classic.ui.clear_main_menu_window( + transition='out_right' + ) + bui.app.classic.ui.set_main_menu_window( PlaylistEditWindow( editcontroller=self, transition='in_left' ).get_root_widget() @@ -206,8 +211,10 @@ class PlaylistEditController: # Otherwise we were adding; go back to the add type choice list. else: - ba.app.ui.clear_main_menu_window(transition='out_right') - ba.app.ui.set_main_menu_window( + bui.app.classic.ui.clear_main_menu_window( + transition='out_right' + ) + bui.app.classic.ui.set_main_menu_window( PlaylistAddGameWindow( editcontroller=self, transition='in_left' ).get_root_widget() @@ -215,7 +222,7 @@ class PlaylistEditController: else: # Make sure type is in there. assert self._editing_game_type is not None - config['type'] = get_type_name(self._editing_game_type) + config['type'] = bui.get_type_name(self._editing_game_type) if self._editing_game: self._playlist[self._selected_index] = copy.deepcopy(config) @@ -227,9 +234,9 @@ class PlaylistEditController: self._playlist.insert(insert_index, copy.deepcopy(config)) self._selected_index = insert_index - ba.playsound(ba.getsound('gunCocking')) - ba.app.ui.clear_main_menu_window(transition='out_right') - ba.app.ui.set_main_menu_window( + bui.getsound('gunCocking').play() + bui.app.classic.ui.clear_main_menu_window(transition='out_right') + bui.app.classic.ui.set_main_menu_window( PlaylistEditWindow( editcontroller=self, transition='in_left' ).get_root_widget() diff --git a/assets/src/ba_data/python/bastd/ui/playlist/editgame.py b/src/assets/ba_data/python/bastd/ui/playlist/editgame.py similarity index 77% rename from assets/src/ba_data/python/bastd/ui/playlist/editgame.py rename to src/assets/ba_data/python/bastd/ui/playlist/editgame.py index a2483364..2389d025 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/editgame.py +++ b/src/assets/ba_data/python/bastd/ui/playlist/editgame.py @@ -6,22 +6,23 @@ from __future__ import annotations import copy import random +import logging from typing import TYPE_CHECKING, cast -import ba -import ba.internal +import bascenev1 as bs +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable -class PlaylistEditGameWindow(ba.Window): +class PlaylistEditGameWindow(bui.Window): """Window for editing a game config.""" def __init__( self, - gametype: type[ba.GameActivity], - sessiontype: type[ba.Session], + gametype: type[bs.GameActivity], + sessiontype: type[bs.Session], config: dict[str, Any] | None, completion_call: Callable[[dict[str, Any] | None], Any], default_selection: str | None = None, @@ -31,13 +32,15 @@ class PlaylistEditGameWindow(ba.Window): # pylint: disable=too-many-branches # pylint: disable=too-many-statements # pylint: disable=too-many-locals - from ba.internal import ( - get_unowned_maps, + from bascenev1.internal import ( get_filtered_map_name, get_map_class, get_map_display_string, ) + assert bui.app.classic is not None + store = bui.app.classic.store + self._gametype = gametype self._sessiontype = sessiontype @@ -58,14 +61,14 @@ class PlaylistEditGameWindow(ba.Window): valid_maps = gametype.get_supported_maps(sessiontype) if not valid_maps: - ba.screenmessage(ba.Lstr(resource='noValidMapsErrorText')) - raise Exception('No valid maps') + bui.screenmessage(bui.Lstr(resource='noValidMapsErrorText')) + raise RuntimeError('No valid maps found.') self._settings_defs = gametype.get_available_settings(sessiontype) self._completion_call = completion_call # To start with, pick a random map out of the ones we own. - unowned_maps = get_unowned_maps() + unowned_maps = store.get_unowned_maps() valid_maps_owned = [m for m in valid_maps if m not in unowned_maps] if valid_maps_owned: self._map = valid_maps[random.randrange(len(valid_maps_owned))] @@ -90,7 +93,7 @@ class PlaylistEditGameWindow(ba.Window): if filtered_map_name in valid_maps: self._map = filtered_map_name except Exception: - ba.print_exception('Error getting map for editor.') + logging.exception('Error getting map for editor.') if config is not None and 'settings' in config: self._settings = config['settings'] @@ -99,14 +102,14 @@ class PlaylistEditGameWindow(ba.Window): self._choice_selections: dict[str, int] = {} - uiscale = ba.app.ui.uiscale - width = 720 if uiscale is ba.UIScale.SMALL else 620 - x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 + uiscale = bui.app.classic.ui.uiscale + width = 720 if uiscale is bui.UIScale.SMALL else 620 + x_inset = 50 if uiscale is bui.UIScale.SMALL else 0 height = ( 365 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 460 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 550 ) spacing = 52 @@ -115,63 +118,63 @@ class PlaylistEditGameWindow(ba.Window): map_tex_name = get_map_class(self._map).get_preview_texture_name() if map_tex_name is None: - raise Exception('no map preview tex found for' + self._map) - map_tex = ba.gettexture(map_tex_name) + raise RuntimeError(f'No map preview tex found for {self._map}.') + map_tex = bui.gettexture(map_tex_name) - top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height + top_extra), transition=transition, scale=( 2.19 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.35 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -17) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=(45 + x_inset, height - 82 + y_extra2), size=(180, 70) if is_add else (180, 65), - label=ba.Lstr(resource='backText') + label=bui.Lstr(resource='backText') if is_add - else ba.Lstr(resource='cancelText'), + else bui.Lstr(resource='cancelText'), button_type='back' if is_add else None, autoselect=True, scale=0.75, text_scale=1.3, - on_activate_call=ba.Call(self._cancel), + on_activate_call=bui.Call(self._cancel), ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) - add_button = ba.buttonwidget( + add_button = bui.buttonwidget( parent=self._root_widget, position=(width - (193 + x_inset), height - 82 + y_extra2), size=(200, 65), scale=0.75, text_scale=1.3, - label=ba.Lstr(resource=self._r + '.addGameText') + label=bui.Lstr(resource=self._r + '.addGameText') if is_add - else ba.Lstr(resource='doneText'), + else bui.Lstr(resource='doneText'), ) - if ba.app.ui.use_toolbars: - pbtn = ba.internal.get_special_widget('party_button') - ba.widget(edit=add_button, right_widget=pbtn, up_widget=pbtn) + if bui.app.classic.ui.use_toolbars: + pbtn = bui.get_special_widget('party_button') + bui.widget(edit=add_button, right_widget=pbtn, up_widget=pbtn) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(-8, height - 70 + y_extra2), size=(width, 25), text=gametype.get_display_string(), - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, maxwidth=235, scale=1.1, h_align='center', @@ -186,7 +189,7 @@ class PlaylistEditGameWindow(ba.Window): scroll_height += spacing * len(self._settings_defs) scroll_width = width - (86 + 2 * x_inset) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, position=(44 + x_inset, 35 + y_extra), size=(scroll_width, height - 116), @@ -195,7 +198,7 @@ class PlaylistEditGameWindow(ba.Window): claims_tab=True, selection_loops_to_parent=True, ) - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(scroll_width, scroll_height), background=False, @@ -209,40 +212,40 @@ class PlaylistEditGameWindow(ba.Window): # Keep track of all the selectable widgets we make so we can wire # them up conveniently. - widget_column: list[list[ba.Widget]] = [] + widget_column: list[list[bui.Widget]] = [] # Map select button. - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h + 49, v - 63), size=(100, 30), maxwidth=110, - text=ba.Lstr(resource='mapText'), + text=bui.Lstr(resource='mapText'), h_align='left', color=(0.8, 0.8, 0.8, 1.0), v_align='center', ) - ba.imagewidget( + bui.imagewidget( parent=self._subcontainer, size=(256 * 0.7, 125 * 0.7), position=(h + 261 - 128 + 128.0 * 0.56, v - 90), texture=map_tex, - model_opaque=ba.getmodel('level_select_button_opaque'), - model_transparent=ba.getmodel('level_select_button_transparent'), - mask_texture=ba.gettexture('mapPreviewMask'), + mesh_opaque=bui.getmesh('level_select_button_opaque'), + mesh_transparent=bui.getmesh('level_select_button_transparent'), + mask_texture=bui.gettexture('mapPreviewMask'), ) - map_button = btn = ba.buttonwidget( + map_button = btn = bui.buttonwidget( parent=self._subcontainer, size=(140, 60), position=(h + 448, v - 72), - on_activate_call=ba.Call(self._select_map), + on_activate_call=bui.Call(self._select_map), scale=0.7, - label=ba.Lstr(resource='mapSelectText'), + label=bui.Lstr(resource='mapSelectText'), ) widget_column.append([btn]) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h + 363 - 123, v - 114), size=(100, 30), @@ -271,7 +274,7 @@ class PlaylistEditGameWindow(ba.Window): ): value = value_type(config['settings'][setting.name]) except Exception: - ba.print_exception() + logging.exception('Error getting game setting.') # Shove the starting value in there to start. self._settings[setting.name] = value @@ -282,7 +285,7 @@ class PlaylistEditGameWindow(ba.Window): mw2 = 70 # Handle types with choices specially: - if isinstance(setting, ba.ChoiceSetting): + if isinstance(setting, bs.ChoiceSetting): for choice in setting.choices: if len(choice) != 2: raise ValueError( @@ -315,7 +318,7 @@ class PlaylistEditGameWindow(ba.Window): break v -= spacing - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h + 50, v), size=(100, 30), @@ -325,7 +328,7 @@ class PlaylistEditGameWindow(ba.Window): color=(0.8, 0.8, 0.8, 1.0), v_align='center', ) - txt = ba.textwidget( + txt = bui.textwidget( parent=self._subcontainer, position=(h + 509 - 95, v), size=(0, 28), @@ -341,36 +344,36 @@ class PlaylistEditGameWindow(ba.Window): v_align='center', padding=2, ) - btn1 = ba.buttonwidget( + btn1 = bui.buttonwidget( parent=self._subcontainer, position=(h + 509 - 50 - 1, v), size=(28, 28), label='<', autoselect=True, - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._choice_inc, setting.name, txt, setting, -1 ), repeat=True, ) - btn2 = ba.buttonwidget( + btn2 = bui.buttonwidget( parent=self._subcontainer, position=(h + 509 + 5, v), size=(28, 28), label='>', autoselect=True, - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._choice_inc, setting.name, txt, setting, 1 ), repeat=True, ) widget_column.append([btn1, btn2]) - elif isinstance(setting, (ba.IntSetting, ba.FloatSetting)): + elif isinstance(setting, (bs.IntSetting, bs.FloatSetting)): v -= spacing min_value = setting.min_value max_value = setting.max_value increment = setting.increment - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h + 50, v), size=(100, 30), @@ -380,7 +383,7 @@ class PlaylistEditGameWindow(ba.Window): v_align='center', maxwidth=mw1, ) - txt = ba.textwidget( + txt = bui.textwidget( parent=self._subcontainer, position=(h + 509 - 95, v), size=(0, 28), @@ -392,13 +395,13 @@ class PlaylistEditGameWindow(ba.Window): v_align='center', padding=2, ) - btn1 = ba.buttonwidget( + btn1 = bui.buttonwidget( parent=self._subcontainer, position=(h + 509 - 50 - 1, v), size=(28, 28), label='-', autoselect=True, - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._inc, txt, min_value, @@ -409,13 +412,13 @@ class PlaylistEditGameWindow(ba.Window): ), repeat=True, ) - btn2 = ba.buttonwidget( + btn2 = bui.buttonwidget( parent=self._subcontainer, position=(h + 509 + 5, v), size=(28, 28), label='+', autoselect=True, - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._inc, txt, min_value, @@ -430,7 +433,7 @@ class PlaylistEditGameWindow(ba.Window): elif value_type == bool: v -= spacing - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h + 50, v), size=(100, 30), @@ -440,13 +443,13 @@ class PlaylistEditGameWindow(ba.Window): v_align='center', maxwidth=mw1, ) - txt = ba.textwidget( + txt = bui.textwidget( parent=self._subcontainer, position=(h + 509 - 95, v), size=(0, 28), - text=ba.Lstr(resource='onText') + text=bui.Lstr(resource='onText') if value - else ba.Lstr(resource='offText'), + else bui.Lstr(resource='offText'), editable=False, color=(0.6, 1.0, 0.6, 1.0), maxwidth=mw2, @@ -454,7 +457,7 @@ class PlaylistEditGameWindow(ba.Window): v_align='center', padding=2, ) - cbw = ba.checkboxwidget( + cbw = bui.checkboxwidget( parent=self._subcontainer, text='', position=(h + 505 - 50 - 5, v - 2), @@ -462,58 +465,59 @@ class PlaylistEditGameWindow(ba.Window): autoselect=True, textcolor=(0.8, 0.8, 0.8), value=value, - on_value_change_call=ba.Call( + on_value_change_call=bui.Call( self._check_value_change, setting.name, txt ), ) widget_column.append([cbw]) else: - raise Exception() + raise TypeError(f'Invalid value type: {value_type}.') # Ok now wire up the column. try: - prev_widgets: list[ba.Widget] | None = None + prev_widgets: list[bui.Widget] | None = None for cwdg in widget_column: if prev_widgets is not None: # Wire our rightmost to their rightmost. - ba.widget(edit=prev_widgets[-1], down_widget=cwdg[-1]) - ba.widget(cwdg[-1], up_widget=prev_widgets[-1]) + bui.widget(edit=prev_widgets[-1], down_widget=cwdg[-1]) + bui.widget(cwdg[-1], up_widget=prev_widgets[-1]) # Wire our leftmost to their leftmost. - ba.widget(edit=prev_widgets[0], down_widget=cwdg[0]) - ba.widget(cwdg[0], up_widget=prev_widgets[0]) + bui.widget(edit=prev_widgets[0], down_widget=cwdg[0]) + bui.widget(cwdg[0], up_widget=prev_widgets[0]) prev_widgets = cwdg except Exception: - ba.print_exception( + logging.exception( 'Error wiring up game-settings-select widget column.' ) - ba.buttonwidget(edit=add_button, on_activate_call=ba.Call(self._add)) - ba.containerwidget( + bui.buttonwidget(edit=add_button, on_activate_call=bui.Call(self._add)) + bui.containerwidget( edit=self._root_widget, selected_child=add_button, start_button=add_button, ) if default_selection == 'map': - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) - ba.containerwidget( + bui.containerwidget( edit=self._subcontainer, selected_child=map_button ) - def _get_localized_setting_name(self, name: str) -> ba.Lstr: - return ba.Lstr(translate=('settingNames', name)) + def _get_localized_setting_name(self, name: str) -> bui.Lstr: + return bui.Lstr(translate=('settingNames', name)) def _select_map(self) -> None: # pylint: disable=cyclic-import from bastd.ui.playlist.mapselect import PlaylistMapSelectWindow # Replace ourself with the map-select UI. - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( PlaylistMapSelectWindow( self._gametype, self._sessiontype, @@ -526,8 +530,8 @@ class PlaylistEditGameWindow(ba.Window): def _choice_inc( self, setting_name: str, - widget: ba.Widget, - setting: ba.ChoiceSetting, + widget: bui.Widget, + setting: bs.ChoiceSetting, increment: int, ) -> None: choices = setting.choices @@ -539,7 +543,7 @@ class PlaylistEditGameWindow(ba.Window): self._choice_selections[setting_name] = max( 0, self._choice_selections[setting_name] - 1 ) - ba.textwidget( + bui.textwidget( edit=widget, text=self._get_localized_setting_name( choices[self._choice_selections[setting_name]][0] @@ -553,13 +557,13 @@ class PlaylistEditGameWindow(ba.Window): self._completion_call(None) def _check_value_change( - self, setting_name: str, widget: ba.Widget, value: int + self, setting_name: str, widget: bui.Widget, value: int ) -> None: - ba.textwidget( + bui.textwidget( edit=widget, - text=ba.Lstr(resource='onText') + text=bui.Lstr(resource='onText') if value - else ba.Lstr(resource='offText'), + else bui.Lstr(resource='offText'), ) self._settings[setting_name] = value @@ -573,7 +577,7 @@ class PlaylistEditGameWindow(ba.Window): def _inc( self, - ctrl: ba.Widget, + ctrl: bui.Widget, min_val: int | float, max_val: int | float, increment: int | float, @@ -581,15 +585,15 @@ class PlaylistEditGameWindow(ba.Window): setting_name: str, ) -> None: if setting_type == float: - val = float(cast(str, ba.textwidget(query=ctrl))) + val = float(cast(str, bui.textwidget(query=ctrl))) else: - val = int(cast(str, ba.textwidget(query=ctrl))) + val = int(cast(str, bui.textwidget(query=ctrl))) val += increment val = max(min_val, min(val, max_val)) if setting_type == float: - ba.textwidget(edit=ctrl, text=str(round(val, 2))) + bui.textwidget(edit=ctrl, text=str(round(val, 2))) elif setting_type == int: - ba.textwidget(edit=ctrl, text=str(int(val))) + bui.textwidget(edit=ctrl, text=str(int(val))) else: raise TypeError('invalid vartype: ' + str(setting_type)) self._settings[setting_name] = val diff --git a/assets/src/ba_data/python/bastd/ui/playlist/mapselect.py b/src/assets/ba_data/python/bastd/ui/playlist/mapselect.py similarity index 70% rename from assets/src/ba_data/python/bastd/ui/playlist/mapselect.py rename to src/assets/ba_data/python/bastd/ui/playlist/mapselect.py index 5ced52af..73140811 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/mapselect.py +++ b/src/assets/ba_data/python/bastd/ui/playlist/mapselect.py @@ -7,33 +7,34 @@ from __future__ import annotations import math from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable + import bascenev1 as bs -class PlaylistMapSelectWindow(ba.Window): + +class PlaylistMapSelectWindow(bui.Window): """Window to select a map.""" def __init__( self, - gametype: type[ba.GameActivity], - sessiontype: type[ba.Session], + gametype: type[bs.GameActivity], + sessiontype: type[bs.Session], config: dict[str, Any], edit_info: dict[str, Any], completion_call: Callable[[dict[str, Any] | None], Any], transition: str = 'in_right', ): - from ba.internal import get_filtered_map_name + from bascenev1.internal import get_filtered_map_name self._gametype = gametype self._sessiontype = sessiontype self._config = config self._completion_call = completion_call self._edit_info = edit_info - self._maps: list[tuple[str, ba.Texture]] = [] + self._maps: list[tuple[str, bui.Texture]] = [] try: self._previous_map = get_filtered_map_name( config['settings']['map'] @@ -41,58 +42,59 @@ class PlaylistMapSelectWindow(ba.Window): except Exception: self._previous_map = '' - uiscale = ba.app.ui.uiscale - width = 715 if uiscale is ba.UIScale.SMALL else 615 - x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + width = 715 if uiscale is bui.UIScale.SMALL else 615 + x_inset = 50 if uiscale is bui.UIScale.SMALL else 0 height = ( 400 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 480 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 600 ) - top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height + top_extra), transition=transition, scale=( 2.17 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.3 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -27) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - self._cancel_button = btn = ba.buttonwidget( + self._cancel_button = btn = bui.buttonwidget( parent=self._root_widget, position=(38 + x_inset, height - 67), size=(140, 50), scale=0.9, text_scale=1.0, autoselect=True, - label=ba.Lstr(resource='cancelText'), + label=bui.Lstr(resource='cancelText'), on_activate_call=self._cancel, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) - ba.textwidget( + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.textwidget( parent=self._root_widget, position=(width * 0.5, height - 46), size=(0, 0), maxwidth=260, scale=1.1, - text=ba.Lstr( + text=bui.Lstr( resource='mapSelectTitleText', subs=[('${GAME}', self._gametype.get_display_string())], ), - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, h_align='center', v_align='center', ) @@ -100,51 +102,51 @@ class PlaylistMapSelectWindow(ba.Window): self._scroll_width = width - (80 + 2 * x_inset) self._scroll_height = height - 140 - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, position=(40 + x_inset, v - self._scroll_height), size=(self._scroll_width, self._scroll_height), ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) - ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) + bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) - self._subcontainer: ba.Widget | None = None + self._subcontainer: bui.Widget | None = None self._refresh() def _refresh(self, select_get_more_maps_button: bool = False) -> None: # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals - from ba.internal import ( - get_unowned_maps, + from bascenev1.internal import ( get_map_class, get_map_display_string, ) + assert bui.app.classic is not None + store = bui.app.classic.store # Kill old. if self._subcontainer is not None: self._subcontainer.delete() - model_opaque = ba.getmodel('level_select_button_opaque') - model_transparent = ba.getmodel('level_select_button_transparent') + mesh_opaque = bui.getmesh('level_select_button_opaque') + mesh_transparent = bui.getmesh('level_select_button_transparent') self._maps = [] map_list = self._gametype.get_supported_maps(self._sessiontype) map_list_sorted = list(map_list) map_list_sorted.sort() - unowned_maps = get_unowned_maps() + unowned_maps = store.get_unowned_maps() for mapname in map_list_sorted: - # Disallow ones we don't own. if mapname in unowned_maps: continue map_tex_name = get_map_class(mapname).get_preview_texture_name() if map_tex_name is not None: try: - map_tex = ba.gettexture(map_tex_name) + map_tex = bui.gettexture(map_tex_name) self._maps.append((mapname, map_tex)) except Exception: print(f'Invalid map preview texture: "{map_tex_name}".') @@ -162,13 +164,13 @@ class PlaylistMapSelectWindow(ba.Window): self._sub_height = ( 5 + rows * (button_height + 2 * button_buffer_v) + 100 ) - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._sub_width, self._sub_height), background=False, ) index = 0 - mask_texture = ba.gettexture('mapPreviewMask') + mask_texture = bui.gettexture('mapPreviewMask') h_offs = 130 if len(self._maps) == 1 else 0 for y in range(rows): for x in range(columns): @@ -180,43 +182,41 @@ class PlaylistMapSelectWindow(ba.Window): - (y + 1) * (button_height + 2 * button_buffer_v) + 12, ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._subcontainer, button_type='square', size=(button_width, button_height), autoselect=True, texture=self._maps[index][1], mask_texture=mask_texture, - model_opaque=model_opaque, - model_transparent=model_transparent, + mesh_opaque=mesh_opaque, + mesh_transparent=mesh_transparent, label='', color=(1, 1, 1), - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._select_with_delay, self._maps[index][0] ), position=pos, ) if x == 0: - ba.widget(edit=btn, left_widget=self._cancel_button) + bui.widget(edit=btn, left_widget=self._cancel_button) if y == 0: - ba.widget(edit=btn, up_widget=self._cancel_button) - if x == columns - 1 and ba.app.ui.use_toolbars: - ba.widget( + bui.widget(edit=btn, up_widget=self._cancel_button) + if x == columns - 1 and bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget( - 'party_button' - ), + right_widget=bui.get_special_widget('party_button'), ) - ba.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) + bui.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) if self._maps[index][0] == self._previous_map: - ba.containerwidget( + bui.containerwidget( edit=self._subcontainer, selected_child=btn, visible_child=btn, ) name = get_map_display_string(self._maps[index][0]) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, text=name, position=(pos[0] + button_width * 0.5, pos[1] - 12), @@ -234,19 +234,19 @@ class PlaylistMapSelectWindow(ba.Window): break if index >= count: break - self._get_more_maps_button = btn = ba.buttonwidget( + self._get_more_maps_button = btn = bui.buttonwidget( parent=self._subcontainer, size=(self._sub_width * 0.8, 60), position=(self._sub_width * 0.1, 30), - label=ba.Lstr(resource='mapSelectGetMoreMapsText'), + label=bui.Lstr(resource='mapSelectGetMoreMapsText'), on_activate_call=self._on_store_press, color=(0.6, 0.53, 0.63), textcolor=(0.75, 0.7, 0.8), autoselect=True, ) - ba.widget(edit=btn, show_buffer_top=30, show_buffer_bottom=30) + bui.widget(edit=btn, show_buffer_top=30, show_buffer_bottom=30) if select_get_more_maps_button: - ba.containerwidget( + bui.containerwidget( edit=self._subcontainer, selected_child=btn, visible_child=btn ) @@ -254,7 +254,10 @@ class PlaylistMapSelectWindow(ba.Window): from bastd.ui import account from bastd.ui.store.browser import StoreBrowserWindow - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return StoreBrowserWindow( @@ -271,8 +274,9 @@ class PlaylistMapSelectWindow(ba.Window): from bastd.ui.playlist.editgame import PlaylistEditGameWindow self._config['settings']['map'] = map_name - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_right') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( PlaylistEditGameWindow( self._gametype, self._sessiontype, @@ -285,17 +289,16 @@ class PlaylistMapSelectWindow(ba.Window): ) def _select_with_delay(self, map_name: str) -> None: - ba.internal.lock_all_input() - ba.timer(0.1, ba.internal.unlock_all_input, timetype=ba.TimeType.REAL) - ba.timer( - 0.1, ba.WeakCall(self._select, map_name), timetype=ba.TimeType.REAL - ) + bui.lock_all_input() + bui.apptimer(0.1, bui.unlock_all_input) + bui.apptimer(0.1, bui.WeakCall(self._select, map_name)) def _cancel(self) -> None: from bastd.ui.playlist.editgame import PlaylistEditGameWindow - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_right') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( PlaylistEditGameWindow( self._gametype, self._sessiontype, diff --git a/assets/src/ba_data/python/bastd/ui/playlist/share.py b/src/assets/ba_data/python/bastd/ui/playlist/share.py similarity index 63% rename from assets/src/ba_data/python/bastd/ui/playlist/share.py rename to src/assets/ba_data/python/bastd/ui/playlist/share.py index 2118c536..5feace43 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/share.py +++ b/src/assets/ba_data/python/bastd/ui/playlist/share.py @@ -7,42 +7,39 @@ from __future__ import annotations import time from typing import TYPE_CHECKING -import ba -import ba.internal -from bastd.ui import promocode +from bastd.ui.promocode import PromoCodeWindow +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable -class SharePlaylistImportWindow(promocode.PromoCodeWindow): +class SharePlaylistImportWindow(PromoCodeWindow): """Window for importing a shared playlist.""" def __init__( self, - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, on_success_callback: Callable[[], Any] | None = None, ): - promocode.PromoCodeWindow.__init__( - self, modal=True, origin_widget=origin_widget - ) + PromoCodeWindow.__init__(self, modal=True, origin_widget=origin_widget) self._on_success_callback = on_success_callback def _on_import_response(self, response: dict[str, Any] | None) -> None: if response is None: - ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) - ba.playsound(ba.getsound('error')) + bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0)) + bui.getsound('error').play() return if response['playlistType'] == 'Team Tournament': - playlist_type_name = ba.Lstr(resource='playModes.teamsText') + playlist_type_name = bui.Lstr(resource='playModes.teamsText') elif response['playlistType'] == 'Free-for-All': - playlist_type_name = ba.Lstr(resource='playModes.freeForAllText') + playlist_type_name = bui.Lstr(resource='playModes.freeForAllText') else: - playlist_type_name = ba.Lstr(value=response['playlistType']) + playlist_type_name = bui.Lstr(value=response['playlistType']) - ba.screenmessage( - ba.Lstr( + bui.screenmessage( + bui.Lstr( resource='importPlaylistSuccessText', subs=[ ('${TYPE}', playlist_type_name), @@ -51,27 +48,30 @@ class SharePlaylistImportWindow(promocode.PromoCodeWindow): ), color=(0, 1, 0), ) - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() if self._on_success_callback is not None: self._on_success_callback() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) def _do_enter(self) -> None: - ba.internal.add_transaction( + plus = bui.app.plus + assert plus is not None + + plus.add_v1_account_transaction( { 'type': 'IMPORT_PLAYLIST', 'expire_time': time.time() + 5, - 'code': ba.textwidget(query=self._text_field), + 'code': bui.textwidget(query=self._text_field), }, - callback=ba.WeakCall(self._on_import_response), + callback=bui.WeakCall(self._on_import_response), ) - ba.internal.run_transactions() - ba.screenmessage(ba.Lstr(resource='importingText')) + plus.run_v1_account_transactions() + bui.screenmessage(bui.Lstr(resource='importingText')) -class SharePlaylistResultsWindow(ba.Window): +class SharePlaylistResultsWindow(bui.Window): """Window for sharing playlists.""" def __init__( @@ -80,25 +80,26 @@ class SharePlaylistResultsWindow(ba.Window): del origin # unused arg self._width = 450 self._height = 300 - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), color=(0.45, 0.63, 0.15), transition='in_scale', scale=( 1.8 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.35 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), ) ) - ba.playsound(ba.getsound('cashRegister')) - ba.playsound(ba.getsound('swish')) + bui.getsound('cashRegister').play() + bui.getsound('swish').play() - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self._root_widget, scale=0.7, position=(40, self._height - 40), @@ -107,42 +108,42 @@ class SharePlaylistResultsWindow(ba.Window): on_activate_call=self.close, autoselect=True, color=(0.45, 0.63, 0.15), - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._cancel_button ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.745), size=(0, 0), - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, scale=1.0, flatness=1.0, h_align='center', v_align='center', - text=ba.Lstr( + text=bui.Lstr( resource='exportSuccessText', subs=[('${NAME}', name)] ), maxwidth=self._width * 0.85, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.645), size=(0, 0), - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, scale=0.6, flatness=1.0, h_align='center', v_align='center', - text=ba.Lstr(resource='importPlaylistCodeInstructionsText'), + text=bui.Lstr(resource='importPlaylistCodeInstructionsText'), maxwidth=self._width * 0.85, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.4), size=(0, 0), @@ -156,4 +157,4 @@ class SharePlaylistResultsWindow(ba.Window): def close(self) -> None: """Close the window.""" - ba.containerwidget(edit=self._root_widget, transition='out_scale') + bui.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/assets/src/ba_data/python/bastd/ui/playoptions.py b/src/assets/ba_data/python/bastd/ui/playoptions.py similarity index 74% rename from assets/src/ba_data/python/bastd/ui/playoptions.py rename to src/assets/ba_data/python/bastd/ui/playoptions.py index 507550bc..51c8c8b6 100644 --- a/assets/src/ba_data/python/bastd/ui/playoptions.py +++ b/src/assets/ba_data/python/bastd/ui/playoptions.py @@ -4,22 +4,23 @@ from __future__ import annotations +import logging from typing import TYPE_CHECKING -import ba -import ba.internal -from bastd.ui import popup +from bastd.ui.popup import PopupWindow +import bascenev1 as bs +import bauiv1 as bui if TYPE_CHECKING: from typing import Any -class PlayOptionsWindow(popup.PopupWindow): +class PlayOptionsWindow(PopupWindow): """A popup window for configuring play options.""" def __init__( self, - sessiontype: type[ba.Session], + sessiontype: type[bs.Session], playlist: str, scale_origin: tuple[float, float], delegate: Any = None, @@ -28,7 +29,7 @@ class PlayOptionsWindow(popup.PopupWindow): # pylint: disable=too-many-branches # pylint: disable=too-many-statements # pylint: disable=too-many-locals - from ba.internal import get_map_class, getclass, filter_playlist + from bascenev1.internal import filter_playlist, get_map_class from bastd.ui.playlist import PlaylistTypeVars self._r = 'gameListWindow' @@ -38,9 +39,12 @@ class PlayOptionsWindow(popup.PopupWindow): # We behave differently if we're being used for playlist selection # vs starting a game directly (should make this more elegant). - self._selecting_mode = ba.app.ui.selecting_private_party_playlist + assert bui.app.classic is not None + self._selecting_mode = ( + bui.app.classic.ui.selecting_private_party_playlist + ) - self._do_randomize_val = ba.app.config.get( + self._do_randomize_val = bui.app.config.get( self._pvars.config_name + ' Playlist Randomize', 0 ) @@ -51,15 +55,15 @@ class PlayOptionsWindow(popup.PopupWindow): self._height = 330.0 - 50.0 # In teams games, show the custom names/colors button. - if self._sessiontype is ba.DualTeamSession: + if self._sessiontype is bs.DualTeamSession: self._height += 50.0 self._row_height = 45.0 # Grab our maps to display. - model_opaque = ba.getmodel('level_select_button_opaque') - model_transparent = ba.getmodel('level_select_button_transparent') - mask_tex = ba.gettexture('mapPreviewMask') + mesh_opaque = bui.getmesh('level_select_button_opaque') + mesh_transparent = bui.getmesh('level_select_button_transparent') + mask_tex = bui.gettexture('mapPreviewMask') # Poke into this playlist and see if we can display some of its maps. map_textures = [] @@ -76,7 +80,7 @@ class PlayOptionsWindow(popup.PopupWindow): plst = self._pvars.get_default_list_call() else: try: - plst = ba.app.config[ + plst = bui.app.config[ self._pvars.config_name + ' Playlists' ][name] except Exception: @@ -87,7 +91,7 @@ class PlayOptionsWindow(popup.PopupWindow): print( 'ERROR INFO: playlist names are:', list( - ba.app.config[ + bui.app.config[ self._pvars.config_name + ' Playlists' ].keys() ), @@ -103,10 +107,10 @@ class PlayOptionsWindow(popup.PopupWindow): game_count = len(plst) for entry in plst: mapname = entry['settings']['map'] - maptype: type[ba.Map] | None + maptype: type[bs.Map] | None try: maptype = get_map_class(mapname) - except ba.NotFoundError: + except bui.NotFoundError: maptype = None if maptype is not None: tex_name = maptype.get_preview_texture_name() @@ -130,7 +134,7 @@ class PlayOptionsWindow(popup.PopupWindow): self._height += self._row_height * rows except Exception: - ba.print_exception('Error listing playlist maps.') + logging.exception('Error listing playlist maps.') show_shuffle_check_box = game_count > 1 @@ -138,24 +142,24 @@ class PlayOptionsWindow(popup.PopupWindow): self._height += 40 # Creates our _root_widget. - uiscale = ba.app.ui.uiscale + uiscale = bui.app.classic.ui.uiscale scale = ( 1.69 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.1 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 0.85 ) super().__init__( position=scale_origin, size=(self._width, self._height), scale=scale ) - playlist_name: str | ba.Lstr = ( + playlist_name: str | bui.Lstr = ( self._pvars.default_list_name if playlist == '__default__' else playlist ) - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=self.root_widget, position=(self._width * 0.5, self._height - 89 + 51), size=(0, 0), @@ -167,7 +171,7 @@ class PlayOptionsWindow(popup.PopupWindow): v_align='center', ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self.root_widget, position=(25, self._height - 53), size=(50, 50), @@ -176,7 +180,7 @@ class PlayOptionsWindow(popup.PopupWindow): color=(0.42, 0.73, 0.2), on_activate_call=self._on_cancel_press, autoselect=True, - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) @@ -205,17 +209,17 @@ class PlayOptionsWindow(popup.PopupWindow): self._have_at_least_one_owned = True try: - desc = getclass( - entry['type'], subclassof=ba.GameActivity + desc = bui.getclass( + entry['type'], subclassof=bs.GameActivity ).get_settings_display_string(entry) if not owned: - desc = ba.Lstr( + desc = bui.Lstr( value='${DESC}\n${UNLOCK}', subs=[ ('${DESC}', desc), ( '${UNLOCK}', - ba.Lstr( + bui.Lstr( resource='unlockThisInTheStoreText' ), ), @@ -223,74 +227,74 @@ class PlayOptionsWindow(popup.PopupWindow): ) desc_color = (0, 1, 0) if owned else (1, 0, 0) except Exception: - desc = ba.Lstr(value='(invalid)') + desc = bui.Lstr(value='(invalid)') desc_color = (1, 0, 0) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self.root_widget, size=(scl * 240.0, scl * 120.0), position=(h, v), - texture=ba.gettexture(tex_name if owned else 'empty'), - model_opaque=model_opaque if owned else None, - on_activate_call=ba.Call( - ba.screenmessage, desc, desc_color + texture=bui.gettexture(tex_name if owned else 'empty'), + mesh_opaque=mesh_opaque if owned else None, + on_activate_call=bui.Call( + bui.screenmessage, desc, desc_color ), label='', color=(1, 1, 1), autoselect=True, extra_touch_border_scale=0.0, - model_transparent=model_transparent if owned else None, + mesh_transparent=mesh_transparent if owned else None, mask_texture=mask_tex if owned else None, ) if row == 0 and col == 0: - ba.widget(edit=self._cancel_button, down_widget=btn) + bui.widget(edit=self._cancel_button, down_widget=btn) if row == rows - 1: bottom_row_buttons.append(btn) if not owned: - # Ewww; buttons don't currently have alpha so in this # case we draw an image over our button with an empty # texture on it. - ba.imagewidget( + bui.imagewidget( parent=self.root_widget, size=(scl * 260.0, scl * 130.0), position=(h - 10.0 * scl, v - 4.0 * scl), draw_controller=btn, color=(1, 1, 1), - texture=ba.gettexture(tex_name), - model_opaque=model_opaque, + texture=bui.gettexture(tex_name), + mesh_opaque=mesh_opaque, opacity=0.25, - model_transparent=model_transparent, + mesh_transparent=mesh_transparent, mask_texture=mask_tex, ) - ba.imagewidget( + bui.imagewidget( parent=self.root_widget, size=(scl * 100, scl * 100), draw_controller=btn, position=(h + scl * 70, v + scl * 10), - texture=ba.gettexture('lock'), + texture=bui.gettexture('lock'), ) # Team names/colors. - self._custom_colors_names_button: ba.Widget | None - if self._sessiontype is ba.DualTeamSession: + self._custom_colors_names_button: bui.Widget | None + if self._sessiontype is bs.DualTeamSession: y_offs = 50 if show_shuffle_check_box else 0 - self._custom_colors_names_button = ba.buttonwidget( + self._custom_colors_names_button = bui.buttonwidget( parent=self.root_widget, position=(100, 200 + y_offs), size=(290, 35), - on_activate_call=ba.WeakCall(self._custom_colors_names_press), + on_activate_call=bui.WeakCall(self._custom_colors_names_press), autoselect=True, textcolor=(0.8, 0.8, 0.8), - label=ba.Lstr(resource='teamNamesColorText'), + label=bui.Lstr(resource='teamNamesColorText'), ) - if not ba.app.accounts_v1.have_pro(): - ba.imagewidget( + assert bui.app.classic is not None + if not bui.app.classic.accounts.have_pro(): + bui.imagewidget( parent=self.root_widget, size=(30, 30), position=(95, 202 + y_offs), - texture=ba.gettexture('lock'), + texture=bui.gettexture('lock'), draw_controller=self._custom_colors_names_button, ) else: @@ -299,20 +303,20 @@ class PlayOptionsWindow(popup.PopupWindow): # Shuffle. def _cb_callback(val: bool) -> None: self._do_randomize_val = val - cfg = ba.app.config + cfg = bui.app.config cfg[ self._pvars.config_name + ' Playlist Randomize' ] = self._do_randomize_val cfg.commit() if show_shuffle_check_box: - self._shuffle_check_box = ba.checkboxwidget( + self._shuffle_check_box = bui.checkboxwidget( parent=self.root_widget, position=(110, 200), scale=1.0, size=(250, 30), autoselect=True, - text=ba.Lstr(resource=self._r + '.shuffleGameOrderText'), + text=bui.Lstr(resource=self._r + '.shuffleGameOrderText'), maxwidth=300, textcolor=(0.8, 0.8, 0.8), value=self._do_randomize_val, @@ -320,20 +324,20 @@ class PlayOptionsWindow(popup.PopupWindow): ) # Show tutorial. - show_tutorial = bool(ba.app.config.get('Show Tutorial', True)) + show_tutorial = bool(bui.app.config.get('Show Tutorial', True)) def _cb_callback_2(val: bool) -> None: - cfg = ba.app.config + cfg = bui.app.config cfg['Show Tutorial'] = val cfg.commit() - self._show_tutorial_check_box = ba.checkboxwidget( + self._show_tutorial_check_box = bui.checkboxwidget( parent=self.root_widget, position=(110, 151), scale=1.0, size=(250, 30), autoselect=True, - text=ba.Lstr(resource=self._r + '.showTutorialText'), + text=bui.Lstr(resource=self._r + '.showTutorialText'), maxwidth=300, textcolor=(0.8, 0.8, 0.8), value=show_tutorial, @@ -344,29 +348,29 @@ class PlayOptionsWindow(popup.PopupWindow): # with checkboxes. if self._custom_colors_names_button is not None: for btn in bottom_row_buttons: - ba.widget( + bui.widget( edit=btn, down_widget=self._custom_colors_names_button ) if show_shuffle_check_box: - ba.widget( + bui.widget( edit=self._custom_colors_names_button, down_widget=self._shuffle_check_box, ) - ba.widget( + bui.widget( edit=self._shuffle_check_box, up_widget=self._custom_colors_names_button, ) else: - ba.widget( + bui.widget( edit=self._custom_colors_names_button, down_widget=self._show_tutorial_check_box, ) - ba.widget( + bui.widget( edit=self._show_tutorial_check_box, up_widget=self._custom_colors_names_button, ) - self._ok_button = ba.buttonwidget( + self._ok_button = bui.buttonwidget( parent=self.root_widget, position=(70, 44), size=(200, 45), @@ -374,14 +378,16 @@ class PlayOptionsWindow(popup.PopupWindow): text_res_scale=1.5, on_activate_call=self._on_ok_press, autoselect=True, - label=ba.Lstr( + label=bui.Lstr( resource='okText' if self._selecting_mode else 'playText' ), ) - ba.widget(edit=self._ok_button, up_widget=self._show_tutorial_check_box) + bui.widget( + edit=self._ok_button, up_widget=self._show_tutorial_check_box + ) - ba.containerwidget( + bui.containerwidget( edit=self.root_widget, start_button=self._ok_button, cancel_button=self._cancel_button, @@ -389,11 +395,8 @@ class PlayOptionsWindow(popup.PopupWindow): ) # Update now and once per second. - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - timetype=ba.TimeType.REAL, - repeat=True, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) self._update() @@ -402,8 +405,12 @@ class PlayOptionsWindow(popup.PopupWindow): from bastd.ui.teamnamescolors import TeamNamesColorsWindow from bastd.ui.purchase import PurchaseWindow - if not ba.app.accounts_v1.have_pro(): - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + assert bui.app.classic is not None + if not bui.app.classic.accounts.have_pro(): + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() else: PurchaseWindow(items=['pro']) @@ -419,7 +426,7 @@ class PlayOptionsWindow(popup.PopupWindow): def _does_target_playlist_exist(self) -> bool: if self._playlist == '__default__': return True - return self._playlist in ba.app.config.get( + return self._playlist in bui.app.config.get( self._pvars.config_name + ' Playlists', {} ) @@ -432,31 +439,30 @@ class PlayOptionsWindow(popup.PopupWindow): def _transition_out(self, transition: str = 'out_scale') -> None: if not self._transitioning_out: self._transitioning_out = True - ba.containerwidget(edit=self.root_widget, transition=transition) + bui.containerwidget(edit=self.root_widget, transition=transition) def on_popup_cancel(self) -> None: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transition_out() def _on_cancel_press(self) -> None: self._transition_out() def _on_ok_press(self) -> None: - # Disallow if our playlist has disappeared. if not self._does_target_playlist_exist(): return # Disallow if we have no unlocked games. if not self._have_at_least_one_owned: - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource='playlistNoValidGamesErrorText'), + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='playlistNoValidGamesErrorText'), color=(1, 0, 0), ) return - cfg = ba.app.config + cfg = bui.app.config cfg[self._pvars.config_name + ' Playlist Selection'] = self._playlist # Head back to the gather window in playlist-select mode @@ -464,23 +470,24 @@ class PlayOptionsWindow(popup.PopupWindow): if self._selecting_mode: from bastd.ui.gather import GatherWindow - if self._sessiontype is ba.FreeForAllSession: + if self._sessiontype is bs.FreeForAllSession: typename = 'ffa' - elif self._sessiontype is ba.DualTeamSession: + elif self._sessiontype is bs.DualTeamSession: typename = 'teams' else: raise RuntimeError('Only teams and ffa currently supported') cfg['Private Party Host Session Type'] = typename - ba.playsound(ba.getsound('gunCocking')) - ba.app.ui.set_main_menu_window( + bui.getsound('gunCocking').play() + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( GatherWindow(transition='in_right').get_root_widget() ) self._transition_out(transition='out_left') if self._delegate is not None: self._delegate.on_play_options_window_run_game() else: - ba.internal.fade_screen(False, endcall=self._run_selected_playlist) - ba.internal.lock_all_input() + bui.fade_screen(False, endcall=self._run_selected_playlist) + bui.lock_all_input() self._transition_out(transition='out_left') if self._delegate is not None: self._delegate.on_play_options_window_run_game() @@ -488,13 +495,13 @@ class PlayOptionsWindow(popup.PopupWindow): cfg.commit() def _run_selected_playlist(self) -> None: - ba.internal.unlock_all_input() + bui.unlock_all_input() try: - ba.internal.new_host_session(self._sessiontype) + bs.new_host_session(self._sessiontype) except Exception: from bastd import mainmenu - ba.print_exception('exception running session', self._sessiontype) + logging.exception('Error running session %s.', self._sessiontype) # Drop back into a main menu session. - ba.internal.new_host_session(mainmenu.MainMenuSession) + bs.new_host_session(mainmenu.MainMenuSession) diff --git a/assets/src/ba_data/python/bastd/ui/popup.py b/src/assets/ba_data/python/bastd/ui/popup.py similarity index 87% rename from assets/src/ba_data/python/bastd/ui/popup.py rename to src/assets/ba_data/python/bastd/ui/popup.py index 79d3de5d..6e6f1595 100644 --- a/assets/src/ba_data/python/bastd/ui/popup.py +++ b/src/assets/ba_data/python/bastd/ui/popup.py @@ -7,8 +7,7 @@ from __future__ import annotations import weakref from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Sequence, Callable @@ -35,7 +34,7 @@ class PopupWindow: focus_size = size # In vr mode we can't have windows going outside the screen. - if ba.app.vr_mode: + if bui.app.vr_mode: focus_size = size focus_position = (0, 0) @@ -45,7 +44,7 @@ class PopupWindow: # Ok, we've been given a desired width, height, and scale; # we now need to ensure that we're all onscreen by scaling down if # need be and clamping it to the UI bounds. - bounds = ba.app.ui_bounds + bounds = bui.uibounds() edge_buffer = 15 bounds_width = bounds[1] - bounds[0] - edge_buffer * 2 bounds_height = bounds[3] - bounds[2] - edge_buffer * 2 @@ -80,12 +79,12 @@ class PopupWindow: (focus_position[1] + focus_size[1] * 0.5) - (size[1] * 0.5) ) * scale - self.root_widget = ba.containerwidget( + self.root_widget = bui.containerwidget( transition='in_scale', scale=scale, toolbar_visibility=toolbar_visibility, size=size, - parent=ba.internal.get_special_widget('overlay_stack'), + parent=bui.get_special_widget('overlay_stack'), stack_offset=(x_fin - x_offs, y_fin - y_offs), scale_origin_stack_offset=(position[0], position[1]), on_outside_click_call=self.on_popup_cancel, @@ -94,7 +93,7 @@ class PopupWindow: on_cancel_call=self.on_popup_cancel, ) # complain if we outlive our root widget - ba.uicleanupcheck(self, self.root_widget) + bui.uicleanupcheck(self, self.root_widget) def on_popup_cancel(self) -> None: """Called when the popup is canceled. @@ -117,7 +116,7 @@ class PopupMenuWindow(PopupWindow): maxwidth: float | None = None, scale: float = 1.0, choices_disabled: Sequence[str] | None = None, - choices_display: Sequence[ba.Lstr] | None = None, + choices_display: Sequence[bui.Lstr] | None = None, ): # FIXME: Clean up a bit. # pylint: disable=too-many-branches @@ -166,7 +165,7 @@ class PopupMenuWindow(PopupWindow): self._width, min( maxwidth, - ba.internal.get_string_width( + bui.get_string_width( choice_display_name, suppress_warning=True ), ) @@ -177,7 +176,7 @@ class PopupMenuWindow(PopupWindow): self._width, min( maxwidth, - ba.internal.get_string_width( + bui.get_string_width( choice_display_name, suppress_warning=True ), ) @@ -186,29 +185,29 @@ class PopupMenuWindow(PopupWindow): # init parent class - this will rescale and reposition things as # needed and create our root widget - PopupWindow.__init__( - self, position, size=(self._width, self._height), scale=self._scale + super().__init__( + position, size=(self._width, self._height), scale=self._scale ) if self._use_scroll: - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self.root_widget, position=(20, 20), highlight=False, color=(0.35, 0.55, 0.15), size=(self._width - 40, self._height - 40), ) - self._columnwidget = ba.columnwidget( + self._columnwidget = bui.columnwidget( parent=self._scrollwidget, border=2, margin=0 ) else: - self._offset_widget = ba.containerwidget( + self._offset_widget = bui.containerwidget( parent=self.root_widget, position=(30, 15), size=(self._width - 40, self._height), background=False, ) - self._columnwidget = ba.columnwidget( + self._columnwidget = bui.columnwidget( parent=self._offset_widget, border=2, margin=0 ) for index, choice in enumerate(choices): @@ -217,10 +216,10 @@ class PopupMenuWindow(PopupWindow): else: choice_display_name = choice inactive = choice in self._choices_disabled - wdg = ba.textwidget( + wdg = bui.textwidget( parent=self._columnwidget, size=(self._width - 40, 28), - on_select_call=ba.Call(self._select, index), + on_select_call=bui.Call(self._select, index), click_activate=True, color=(0.5, 0.5, 0.5, 0.5) if inactive @@ -237,7 +236,7 @@ class PopupMenuWindow(PopupWindow): selectable=(not inactive), ) if choice == self._current_choice: - ba.containerwidget( + bui.containerwidget( edit=self._columnwidget, selected_child=wdg, visible_child=wdg, @@ -252,16 +251,16 @@ class PopupMenuWindow(PopupWindow): self._current_choice = self._choices[index] def _activate(self) -> None: - ba.playsound(ba.getsound('swish')) - ba.timer(0.05, self._transition_out, timetype=ba.TimeType.REAL) + bui.getsound('swish').play() + bui.apptimer(0.05, self._transition_out) delegate = self._getdelegate() if delegate is not None: # Call this in a timer so it doesn't interfere with us killing # our widgets and whatnot. - call = ba.Call( + call = bui.Call( delegate.popup_menu_selected_choice, self, self._current_choice ) - ba.timer(0, call, timetype=ba.TimeType.REAL) + bui.apptimer(0, call) def _getdelegate(self) -> Any: return None if self._delegate is None else self._delegate() @@ -274,11 +273,11 @@ class PopupMenuWindow(PopupWindow): delegate = self._getdelegate() if delegate is not None: delegate.popup_menu_closing(self) - ba.containerwidget(edit=self.root_widget, transition='out_scale') + bui.containerwidget(edit=self.root_widget, transition='out_scale') def on_popup_cancel(self) -> None: if not self._transitioning_out: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transition_out() @@ -290,7 +289,7 @@ class PopupMenu: def __init__( self, - parent: ba.Widget, + parent: bui.Widget, position: tuple[float, float], choices: Sequence[str], current_choice: str | None = None, @@ -301,7 +300,7 @@ class PopupMenu: maxwidth: float | None = None, scale: float | None = None, choices_disabled: Sequence[str] | None = None, - choices_display: Sequence[ba.Lstr] | None = None, + choices_display: Sequence[bui.Lstr] | None = None, button_size: tuple[float, float] = (160.0, 50.0), autoselect: bool = True, ): @@ -310,13 +309,14 @@ class PopupMenu: choices_disabled = [] if choices_display is None: choices_display = [] - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale if scale is None: scale = ( 2.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.65 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.23 ) if current_choice not in choices: @@ -339,16 +339,14 @@ class PopupMenu: self._parent = parent self._button_size = button_size - self._button = ba.buttonwidget( + self._button = bui.buttonwidget( parent=self._parent, position=(self._position[0], self._position[1]), autoselect=autoselect, size=self._button_size, scale=1.0, label='', - on_activate_call=lambda: ba.timer( - 0, self._make_popup, timetype=ba.TimeType.REAL - ), + on_activate_call=lambda: bui.apptimer(0, self._make_popup), ) self._on_value_change_call = None # Don't wanna call for initial set. self._opening_call = opening_call @@ -356,10 +354,10 @@ class PopupMenu: self._closing_call = closing_call self.set_choice(self._current_choice) self._on_value_change_call = on_value_change_call - self._window_widget: ba.Widget | None = None + self._window_widget: bui.Widget | None = None # Complain if we outlive our button. - ba.uicleanupcheck(self, self._button) + bui.uicleanupcheck(self, self._button) def _make_popup(self) -> None: if not self._button: @@ -378,11 +376,11 @@ class PopupMenu: choices_display=self._choices_display, ).root_widget - def get_button(self) -> ba.Widget: + def get_button(self) -> bui.Widget: """Return the menu's button widget.""" return self._button - def get_window_widget(self) -> ba.Widget | None: + def get_window_widget(self) -> bui.Widget | None: """Return the menu's window widget (or None if nonexistent).""" return self._window_widget @@ -399,7 +397,7 @@ class PopupMenu: """Called when the menu is closing.""" del popup_window # Unused here. if self._button: - ba.containerwidget(edit=self._parent, selected_child=self._button) + bui.containerwidget(edit=self._parent, selected_child=self._button) self._window_widget = None if self._closing_call: self._closing_call() @@ -407,10 +405,10 @@ class PopupMenu: def set_choice(self, choice: str) -> None: """Set the selected choice.""" self._current_choice = choice - displayname: str | ba.Lstr + displayname: str | bui.Lstr if len(self._choices_display) == len(self._choices): displayname = self._choices_display[self._choices.index(choice)] else: displayname = choice if self._button: - ba.buttonwidget(edit=self._button, label=displayname) + bui.buttonwidget(edit=self._button, label=displayname) diff --git a/assets/src/ba_data/python/bastd/ui/profile/__init__.py b/src/assets/ba_data/python/bastd/ui/profile/__init__.py similarity index 100% rename from assets/src/ba_data/python/bastd/ui/profile/__init__.py rename to src/assets/ba_data/python/bastd/ui/profile/__init__.py diff --git a/assets/src/ba_data/python/bastd/ui/profile/browser.py b/src/assets/ba_data/python/bastd/ui/profile/browser.py similarity index 67% rename from assets/src/ba_data/python/bastd/ui/profile/browser.py rename to src/assets/ba_data/python/bastd/ui/profile/browser.py index 350e303d..99005ce1 100644 --- a/assets/src/ba_data/python/bastd/ui/profile/browser.py +++ b/src/assets/ba_data/python/bastd/ui/profile/browser.py @@ -4,16 +4,17 @@ from __future__ import annotations +import logging from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui +import bascenev1 as bs if TYPE_CHECKING: from typing import Any -class ProfileBrowserWindow(ba.Window): +class ProfileBrowserWindow(bui.Window): """Window for browsing player profiles.""" def __init__( @@ -21,29 +22,31 @@ class ProfileBrowserWindow(ba.Window): transition: str = 'in_right', in_main_menu: bool = True, selected_profile: str | None = None, - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals self._in_main_menu = in_main_menu if self._in_main_menu: - back_label = ba.Lstr(resource='backText') + back_label = bui.Lstr(resource='backText') else: - back_label = ba.Lstr(resource='doneText') - uiscale = ba.app.ui.uiscale - self._width = 700.0 if uiscale is ba.UIScale.SMALL else 600.0 - x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0 + back_label = bui.Lstr(resource='doneText') + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self._width = 700.0 if uiscale is bui.UIScale.SMALL else 600.0 + x_inset = 50.0 if uiscale is bui.UIScale.SMALL else 0.0 self._height = ( 360.0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 385.0 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 410.0 ) # If we're being called up standalone, handle pause/resume ourself. if not self._in_main_menu: - ba.app.pause() + assert bui.app.classic is not None + bui.app.classic.pause() # If they provided an origin-widget, scale up from that. scale_origin: tuple[float, float] | None @@ -58,29 +61,30 @@ class ProfileBrowserWindow(ba.Window): self._r = 'playerProfilesWindow' # Ensure we've got an account-profile in cases where we're signed in. - ba.app.accounts_v1.ensure_have_account_player_profile() + assert bui.app.classic is not None + bui.app.classic.accounts.ensure_have_account_player_profile() - top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), transition=transition, scale_origin_stack_offset=scale_origin, scale=( 2.2 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.6 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -14) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - self._back_button = btn = ba.buttonwidget( + self._back_button = btn = bui.buttonwidget( parent=self._root_widget, position=(40 + x_inset, self._height - 59), size=(120, 60), @@ -90,26 +94,26 @@ class ProfileBrowserWindow(ba.Window): autoselect=True, on_activate_call=self._back, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 36), size=(0, 0), - text=ba.Lstr(resource=self._r + '.titleText'), + text=bui.Lstr(resource=self._r + '.titleText'), maxwidth=300, - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, scale=0.9, h_align='center', v_align='center', ) if self._in_main_menu: - ba.buttonwidget( + bui.buttonwidget( edit=btn, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) scroll_height = self._height - 140.0 @@ -120,13 +124,13 @@ class ProfileBrowserWindow(ba.Window): scl = ( 1.055 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.18 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.3 ) v -= 70.0 * scl - self._new_button = ba.buttonwidget( + self._new_button = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(80, 66.0 * scl), @@ -136,10 +140,10 @@ class ProfileBrowserWindow(ba.Window): autoselect=True, textcolor=(0.75, 0.7, 0.8), text_scale=0.7, - label=ba.Lstr(resource=self._r + '.newButtonText'), + label=bui.Lstr(resource=self._r + '.newButtonText'), ) v -= 70.0 * scl - self._edit_button = ba.buttonwidget( + self._edit_button = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(80, 66.0 * scl), @@ -149,10 +153,10 @@ class ProfileBrowserWindow(ba.Window): autoselect=True, textcolor=(0.75, 0.7, 0.8), text_scale=0.7, - label=ba.Lstr(resource=self._r + '.editButtonText'), + label=bui.Lstr(resource=self._r + '.editButtonText'), ) v -= 70.0 * scl - self._delete_button = ba.buttonwidget( + self._delete_button = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(80, 66.0 * scl), @@ -162,44 +166,44 @@ class ProfileBrowserWindow(ba.Window): autoselect=True, textcolor=(0.75, 0.7, 0.8), text_scale=0.7, - label=ba.Lstr(resource=self._r + '.deleteButtonText'), + label=bui.Lstr(resource=self._r + '.deleteButtonText'), ) v = self._height - 87 - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 71), size=(0, 0), - text=ba.Lstr(resource=self._r + '.explanationText'), - color=ba.app.ui.infotextcolor, + text=bui.Lstr(resource=self._r + '.explanationText'), + color=bui.app.classic.ui.infotextcolor, maxwidth=self._width * 0.83, scale=0.6, h_align='center', v_align='center', ) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, highlight=False, position=(140 + x_inset, v - scroll_height), size=(self._scroll_width, scroll_height), ) - ba.widget( + bui.widget( edit=self._scrollwidget, autoselect=True, left_widget=self._new_button, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) - self._columnwidget = ba.columnwidget( + self._columnwidget = bui.columnwidget( parent=self._scrollwidget, border=2, margin=0 ) v -= 255 self._profiles: dict[str, dict[str, Any]] | None = None self._selected_profile = selected_profile - self._profile_widgets: list[ba.Widget] = [] + self._profile_widgets: list[bui.Widget] = [] self._refresh() self._restore_state() @@ -208,18 +212,20 @@ class ProfileBrowserWindow(ba.Window): from bastd.ui.profile.edit import EditProfileWindow from bastd.ui.purchase import PurchaseWindow + plus = bui.app.plus + assert plus is not None + # Limit to a handful profiles if they don't have pro-options. - max_non_pro_profiles = ba.internal.get_v1_account_misc_read_val( - 'mnpp', 5 - ) + max_non_pro_profiles = plus.get_v1_account_misc_read_val('mnpp', 5) assert self._profiles is not None + assert bui.app.classic is not None if ( - not ba.app.accounts_v1.have_pro_options() + not bui.app.classic.accounts.have_pro_options() and len(self._profiles) >= max_non_pro_profiles ): PurchaseWindow( items=['pro'], - header_text=ba.Lstr( + header_text=bui.Lstr( resource='unlockThisProfilesText', subs=[('${NUM}', str(max_non_pro_profiles))], ), @@ -229,8 +235,8 @@ class ProfileBrowserWindow(ba.Window): # Clamp at 100 profiles (otherwise the server will and that's less # elegant looking). if len(self._profiles) > 100: - ba.screenmessage( - ba.Lstr( + bui.screenmessage( + bui.Lstr( translate=( 'serverResponses', 'Max number of profiles reached.', @@ -238,12 +244,12 @@ class ProfileBrowserWindow(ba.Window): ), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + bui.app.classic.ui.set_main_menu_window( EditProfileWindow( existing_profile=None, in_main_menu=self._in_main_menu ).get_root_widget() @@ -254,20 +260,20 @@ class ProfileBrowserWindow(ba.Window): from bastd.ui import confirm if self._selected_profile is None: - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) ) return if self._selected_profile == '__account__': - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource=self._r + '.cantDeleteAccountProfileText'), + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource=self._r + '.cantDeleteAccountProfileText'), color=(1, 0, 0), ) return confirm.ConfirmWindow( - ba.Lstr( + bui.Lstr( resource=self._r + '.deleteConfirmText', subs=[('${PROFILE}', self._selected_profile)], ), @@ -276,15 +282,18 @@ class ProfileBrowserWindow(ba.Window): ) def _do_delete_profile(self) -> None: - ba.internal.add_transaction( + plus = bui.app.plus + assert plus is not None + + plus.add_v1_account_transaction( {'type': 'REMOVE_PLAYER_PROFILE', 'name': self._selected_profile} ) - ba.internal.run_transactions() - ba.playsound(ba.getsound('shieldDown')) + plus.run_v1_account_transactions() + bui.getsound('shieldDown').play() self._refresh() # Select profile list. - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) @@ -293,14 +302,15 @@ class ProfileBrowserWindow(ba.Window): from bastd.ui.profile.edit import EditProfileWindow if self._selected_profile is None: - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0) ) return self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( EditProfileWindow( self._selected_profile, in_main_menu=self._in_main_menu ).get_root_widget() @@ -314,73 +324,79 @@ class ProfileBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.account.settings import AccountSettingsWindow + assert bui.app.classic is not None + self._save_state() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) if self._in_main_menu: - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( AccountSettingsWindow(transition='in_left').get_root_widget() ) # If we're being called up standalone, handle pause/resume ourself. else: - ba.app.resume() + bui.app.classic.resume() def _refresh(self) -> None: # pylint: disable=too-many-locals from efro.util import asserttype - from ba.internal import ( - PlayerProfilesChangedMessage, - get_player_profile_colors, - get_player_profile_icon, - ) + from bascenev1.internal import PlayerProfilesChangedMessage + + assert bui.app.classic is not None + + plus = bui.app.plus + assert plus is not None old_selection = self._selected_profile # Delete old. while self._profile_widgets: self._profile_widgets.pop().delete() - self._profiles = ba.app.config.get('Player Profiles', {}) + self._profiles = bui.app.config.get('Player Profiles', {}) assert self._profiles is not None items = list(self._profiles.items()) items.sort(key=lambda x: asserttype(x[0], str).lower()) index = 0 account_name: str | None - if ba.internal.get_v1_account_state() == 'signed_in': - account_name = ba.internal.get_v1_account_display_string() + if plus.get_v1_account_state() == 'signed_in': + account_name = plus.get_v1_account_display_string() else: account_name = None widget_to_select = None for p_name, _ in items: if p_name == '__account__' and account_name is None: continue - color, _highlight = get_player_profile_colors(p_name) + color, _highlight = bui.app.classic.get_player_profile_colors( + p_name + ) scl = 1.1 tval = ( account_name if p_name == '__account__' - else get_player_profile_icon(p_name) + p_name + else bui.app.classic.get_player_profile_icon(p_name) + p_name ) assert isinstance(tval, str) - txtw = ba.textwidget( + txtw = bui.textwidget( parent=self._columnwidget, position=(0, 32), size=((self._width - 40) / scl, 28), - text=ba.Lstr(value=tval), + text=bui.Lstr(value=tval), h_align='left', v_align='center', - on_select_call=ba.WeakCall(self._select, p_name, index), + on_select_call=bui.WeakCall(self._select, p_name, index), maxwidth=self._scroll_width * 0.92, corner_scale=scl, - color=ba.safecolor(color, 0.4), + color=bui.safecolor(color, 0.4), always_highlight=True, - on_activate_call=ba.Call(self._edit_button.activate), + on_activate_call=bui.Call(self._edit_button.activate), selectable=True, ) if index == 0: - ba.widget(edit=txtw, up_widget=self._back_button) - ba.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40) + bui.widget(edit=txtw, up_widget=self._back_button) + bui.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40) self._profile_widgets.append(txtw) # Select/show this one if it was previously selected @@ -392,7 +408,7 @@ class ProfileBrowserWindow(ba.Window): index += 1 if widget_to_select is not None: - ba.columnwidget( + bui.columnwidget( edit=self._columnwidget, selected_child=widget_to_select, visible_child=widget_to_select, @@ -400,7 +416,7 @@ class ProfileBrowserWindow(ba.Window): # If there's a team-chooser in existence, tell it the profile-list # has probably changed. - session = ba.internal.get_foreground_host_session() + session = bs.get_foreground_host_session() if session is not None: session.handlemessage(PlayerProfilesChangedMessage()) @@ -417,13 +433,15 @@ class ProfileBrowserWindow(ba.Window): sel_name = 'Scroll' else: sel_name = 'Back' - ba.app.ui.window_states[type(self)] = sel_name + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = sel_name except Exception: - ba.print_exception(f'Error saving state for {self}.') + logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: try: - sel_name = ba.app.ui.window_states.get(type(self)) + assert bui.app.classic is not None + sel_name = bui.app.classic.ui.window_states.get(type(self)) if sel_name == 'Scroll': sel = self._scrollwidget elif sel_name == 'New': @@ -441,6 +459,6 @@ class ProfileBrowserWindow(ba.Window): sel = self._new_button else: sel = self._scrollwidget - ba.containerwidget(edit=self._root_widget, selected_child=sel) + bui.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: - ba.print_exception(f'Error restoring state for {self}.') + logging.exception('Error restoring state for %s.', self) diff --git a/assets/src/ba_data/python/bastd/ui/profile/edit.py b/src/assets/ba_data/python/bastd/ui/profile/edit.py similarity index 73% rename from assets/src/ba_data/python/bastd/ui/profile/edit.py rename to src/assets/ba_data/python/bastd/ui/profile/edit.py index 2ebb1620..9b0fbd79 100644 --- a/assets/src/ba_data/python/bastd/ui/profile/edit.py +++ b/src/assets/ba_data/python/bastd/ui/profile/edit.py @@ -5,23 +5,22 @@ from __future__ import annotations import random -from typing import TYPE_CHECKING, cast +from typing import cast -import ba -import ba.internal - -if TYPE_CHECKING: - from bastd.ui.colorpicker import ColorPicker +from bastd.ui.colorpicker import ColorPicker +import bauiv1 as bui +import bascenev1 as bs -class EditProfileWindow(ba.Window): +class EditProfileWindow(bui.Window): """Window for editing a player profile.""" # FIXME: WILL NEED TO CHANGE THIS FOR UILOCATION. def reload_window(self) -> None: """Transitions out and recreates ourself.""" - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( EditProfileWindow( self.getname(), self._in_main_menu ).get_root_widget() @@ -37,77 +36,83 @@ class EditProfileWindow(ba.Window): # pylint: disable=too-many-branches # pylint: disable=too-many-statements # pylint: disable=too-many-locals - from ba.internal import get_player_profile_colors + assert bui.app.classic is not None + + plus = bui.app.plus + assert plus is not None self._in_main_menu = in_main_menu self._existing_profile = existing_profile self._r = 'editProfileWindow' self._spazzes: list[str] = [] - self._icon_textures: list[ba.Texture] = [] - self._icon_tint_textures: list[ba.Texture] = [] + self._icon_textures: list[bui.Texture] = [] + self._icon_tint_textures: list[bui.Texture] = [] # Grab profile colors or pick random ones. - self._color, self._highlight = get_player_profile_colors( - existing_profile - ) - uiscale = ba.app.ui.uiscale - self._width = width = 780.0 if uiscale is ba.UIScale.SMALL else 680.0 - self._x_inset = x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0 + ( + self._color, + self._highlight, + ) = bui.app.classic.get_player_profile_colors(existing_profile) + uiscale = bui.app.classic.ui.uiscale + self._width = width = 780.0 if uiscale is bui.UIScale.SMALL else 680.0 + self._x_inset = x_inset = 50.0 if uiscale is bui.UIScale.SMALL else 0.0 self._height = height = ( 350.0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 400.0 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 450.0 ) spacing = 40 self._base_scale = ( 2.05 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.5 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ) - top_extra = 15 if uiscale is ba.UIScale.SMALL else 15 + top_extra = 15 if uiscale is bui.UIScale.SMALL else 15 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height + top_extra), transition=transition, scale=self._base_scale, - stack_offset=(0, 15) if uiscale is ba.UIScale.SMALL else (0, 0), + stack_offset=(0, 15) + if uiscale is bui.UIScale.SMALL + else (0, 0), ) ) - cancel_button = btn = ba.buttonwidget( + cancel_button = btn = bui.buttonwidget( parent=self._root_widget, position=(52 + x_inset, height - 60), size=(155, 60), scale=0.8, autoselect=True, - label=ba.Lstr(resource='cancelText'), + label=bui.Lstr(resource='cancelText'), on_activate_call=self._cancel, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) - save_button = btn = ba.buttonwidget( + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + save_button = btn = bui.buttonwidget( parent=self._root_widget, position=(width - (177 + x_inset), height - 60), size=(155, 60), autoselect=True, scale=0.8, - label=ba.Lstr(resource='saveText'), + label=bui.Lstr(resource='saveText'), ) - ba.widget(edit=save_button, left_widget=cancel_button) - ba.widget(edit=cancel_button, right_widget=save_button) - ba.containerwidget(edit=self._root_widget, start_button=btn) - ba.textwidget( + bui.widget(edit=save_button, left_widget=cancel_button) + bui.widget(edit=cancel_button, right_widget=save_button) + bui.containerwidget(edit=self._root_widget, start_button=btn) + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, height - 38), size=(0, 0), text=( - ba.Lstr(resource=self._r + '.titleNewText') + bui.Lstr(resource=self._r + '.titleNewText') if existing_profile is None - else ba.Lstr(resource=self._r + '.titleEditText') + else bui.Lstr(resource=self._r + '.titleEditText') ), - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, maxwidth=290, scale=1.0, h_align='center', @@ -116,7 +121,7 @@ class EditProfileWindow(ba.Window): # Make a list of spaz icons. self.refresh_characters() - profile = ba.app.config.get('Player Profiles', {}).get( + profile = bui.app.config.get('Player Profiles', {}).get( self._existing_profile, {} ) @@ -128,7 +133,7 @@ class EditProfileWindow(ba.Window): if 'icon' in profile: self._icon = profile['icon'] else: - self._icon = ba.charstr(ba.SpecialChar.LOGO) + self._icon = bui.charstr(bui.SpecialChar.LOGO) assigned_random_char = False @@ -140,7 +145,7 @@ class EditProfileWindow(ba.Window): # that we go random. # (SCRATCH THAT.. we now hard-code account-profiles to start with # spaz which has a similar effect) - # try: p_len = len(ba.app.config['Player Profiles']) + # try: p_len = len(bui.app.config['Player Profiles']) # except Exception: p_len = 0 # if p_len == 0: icon_index = self._spazzes.index('Spaz') # else: @@ -148,7 +153,7 @@ class EditProfileWindow(ba.Window): icon_index = random.randrange(len(self._spazzes)) assigned_random_char = True self._icon_index = icon_index - ba.buttonwidget(edit=save_button, on_activate_call=self.save) + bui.buttonwidget(edit=save_button, on_activate_call=self.save) v = height - 115.0 self._name = ( @@ -159,12 +164,13 @@ class EditProfileWindow(ba.Window): # If we just picked a random character, see if it has specific # colors/highlights associated with it and assign them if so. if assigned_random_char: - clr = ba.app.spaz_appearances[ + assert bui.app.classic is not None + clr = bui.app.classic.spaz_appearances[ self._spazzes[icon_index] ].default_color if clr is not None: self._color = clr - highlight = ba.app.spaz_appearances[ + highlight = bui.app.classic.spaz_appearances[ self._spazzes[icon_index] ].default_highlight if highlight is not None: @@ -172,10 +178,10 @@ class EditProfileWindow(ba.Window): # Assign a random name if they had none. if self._name == '': - names = ba.internal.get_random_names() + names = bs.get_random_names() self._name = names[random.randrange(len(names))] - self._clipped_name_text = ba.textwidget( + self._clipped_name_text = bui.textwidget( parent=self._root_widget, text='', position=(540 + x_inset, v - 8), @@ -190,9 +196,9 @@ class EditProfileWindow(ba.Window): ) if not self._is_account_profile and not self._global: - ba.textwidget( + bui.textwidget( parent=self._root_widget, - text=ba.Lstr(resource=self._r + '.nameText'), + text=bui.Lstr(resource=self._r + '.nameText'), position=(200 + x_inset, v - 6), size=(0, 0), h_align='right', @@ -203,11 +209,11 @@ class EditProfileWindow(ba.Window): self._upgrade_button = None if self._is_account_profile: - if ba.internal.get_v1_account_state() == 'signed_in': - sval = ba.internal.get_v1_account_display_string() + if plus.get_v1_account_state() == 'signed_in': + sval = plus.get_v1_account_display_string() else: sval = '??' - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, v - 7), size=(0, 0), @@ -217,25 +223,25 @@ class EditProfileWindow(ba.Window): h_align='center', v_align='center', ) - txtl = ba.Lstr( + txtl = bui.Lstr( resource='editProfileWindow.accountProfileText' ).evaluate() b_width = min( 270.0, - ba.internal.get_string_width(txtl, suppress_warning=True) * 0.6, + bui.get_string_width(txtl, suppress_warning=True) * 0.6, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, v - 39), size=(0, 0), scale=0.6, - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, text=txtl, maxwidth=270, h_align='center', v_align='center', ) - self._account_type_info_button = ba.buttonwidget( + self._account_type_info_button = bui.buttonwidget( parent=self._root_widget, label='?', size=(15, 15), @@ -247,9 +253,8 @@ class EditProfileWindow(ba.Window): on_activate_call=self.show_account_profile_info, ) elif self._global: - b_size = 60 - self._icon_button = btn = ba.buttonwidget( + self._icon_button = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(self._width * 0.5 - 160 - b_size * 0.5, v - 38 - 15), @@ -260,7 +265,7 @@ class EditProfileWindow(ba.Window): text_scale=1.2, on_activate_call=self._on_icon_press, ) - self._icon_button_label = ba.textwidget( + self._icon_button_label = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5 - 160, v - 35), draw_controller=btn, @@ -272,22 +277,22 @@ class EditProfileWindow(ba.Window): scale=2.0, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, h_align='center', v_align='center', position=(self._width * 0.5 - 160, v - 55 - 15), size=(0, 0), draw_controller=btn, - text=ba.Lstr(resource=self._r + '.iconText'), + text=bui.Lstr(resource=self._r + '.iconText'), scale=0.7, - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, maxwidth=120, ) self._update_icon() - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, v - 7), size=(0, 0), @@ -298,25 +303,25 @@ class EditProfileWindow(ba.Window): v_align='center', ) # FIXME hard coded strings are bad - txtl = ba.Lstr( + txtl = bui.Lstr( resource='editProfileWindow.globalProfileText' ).evaluate() b_width = min( 240.0, - ba.internal.get_string_width(txtl, suppress_warning=True) * 0.6, + bui.get_string_width(txtl, suppress_warning=True) * 0.6, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, v - 39), size=(0, 0), scale=0.6, - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, text=txtl, maxwidth=240, h_align='center', v_align='center', ) - self._account_type_info_button = ba.buttonwidget( + self._account_type_info_button = bui.buttonwidget( parent=self._root_widget, label='?', size=(15, 15), @@ -328,7 +333,7 @@ class EditProfileWindow(ba.Window): on_activate_call=self.show_global_profile_info, ) else: - self._text_field = ba.textwidget( + self._text_field = bui.textwidget( parent=self._root_widget, position=(220 + x_inset, v - 30), size=(265, 40), @@ -336,34 +341,34 @@ class EditProfileWindow(ba.Window): h_align='left', v_align='center', max_chars=16, - description=ba.Lstr(resource=self._r + '.nameDescriptionText'), + description=bui.Lstr(resource=self._r + '.nameDescriptionText'), autoselect=True, editable=True, padding=4, color=(0.9, 0.9, 0.9, 1.0), - on_return_press_call=ba.Call(save_button.activate), + on_return_press_call=bui.Call(save_button.activate), ) # FIXME hard coded strings are bad - txtl = ba.Lstr( + txtl = bui.Lstr( resource='editProfileWindow.localProfileText' ).evaluate() b_width = min( 270.0, - ba.internal.get_string_width(txtl, suppress_warning=True) * 0.6, + bui.get_string_width(txtl, suppress_warning=True) * 0.6, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, v - 43), size=(0, 0), scale=0.6, - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, text=txtl, maxwidth=270, h_align='center', v_align='center', ) - self._account_type_info_button = ba.buttonwidget( + self._account_type_info_button = bui.buttonwidget( parent=self._root_widget, label='?', size=(15, 15), @@ -374,9 +379,9 @@ class EditProfileWindow(ba.Window): autoselect=True, on_activate_call=self.show_local_profile_info, ) - self._upgrade_button = ba.buttonwidget( + self._upgrade_button = bui.buttonwidget( parent=self._root_widget, - label=ba.Lstr(resource='upgradeText'), + label=bui.Lstr(resource='upgradeText'), size=(40, 17), text_scale=1.0, button_type='square', @@ -387,18 +392,15 @@ class EditProfileWindow(ba.Window): ) self._update_clipped_name() - self._clipped_name_timer = ba.Timer( - 0.333, - ba.WeakCall(self._update_clipped_name), - timetype=ba.TimeType.REAL, - repeat=True, + self._clipped_name_timer = bui.AppTimer( + 0.333, bui.WeakCall(self._update_clipped_name), repeat=True ) v -= spacing * 3.0 b_size = 80 b_size_2 = 100 b_offs = 150 - self._color_button = btn = ba.buttonwidget( + self._color_button = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(self._width * 0.5 - b_offs - b_size * 0.5, v - 50), @@ -408,24 +410,24 @@ class EditProfileWindow(ba.Window): button_type='square', ) origin = self._color_button.get_screen_space_center() - ba.buttonwidget( + bui.buttonwidget( edit=self._color_button, - on_activate_call=ba.WeakCall(self._make_picker, 'color', origin), + on_activate_call=bui.WeakCall(self._make_picker, 'color', origin), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, h_align='center', v_align='center', position=(self._width * 0.5 - b_offs, v - 65), size=(0, 0), draw_controller=btn, - text=ba.Lstr(resource=self._r + '.colorText'), + text=bui.Lstr(resource=self._r + '.colorText'), scale=0.7, - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, maxwidth=120, ) - self._character_button = btn = ba.buttonwidget( + self._character_button = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(self._width * 0.5 - b_size_2 * 0.5, v - 60), @@ -434,26 +436,26 @@ class EditProfileWindow(ba.Window): size=(b_size_2, b_size_2), label='', color=(1, 1, 1), - mask_texture=ba.gettexture('characterIconMask'), + mask_texture=bui.gettexture('characterIconMask'), ) if not self._is_account_profile and not self._global: - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._text_field ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, h_align='center', v_align='center', position=(self._width * 0.5, v - 80), size=(0, 0), draw_controller=btn, - text=ba.Lstr(resource=self._r + '.characterText'), + text=bui.Lstr(resource=self._r + '.characterText'), scale=0.7, - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, maxwidth=130, ) - self._highlight_button = btn = ba.buttonwidget( + self._highlight_button = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(self._width * 0.5 + b_offs - b_size * 0.5, v - 50), @@ -467,31 +469,31 @@ class EditProfileWindow(ba.Window): ) if not self._is_account_profile and not self._global: - ba.widget(edit=cancel_button, down_widget=self._text_field) - ba.widget(edit=save_button, down_widget=self._text_field) - ba.widget(edit=self._color_button, up_widget=self._text_field) - ba.widget( + bui.widget(edit=cancel_button, down_widget=self._text_field) + bui.widget(edit=save_button, down_widget=self._text_field) + bui.widget(edit=self._color_button, up_widget=self._text_field) + bui.widget( edit=self._account_type_info_button, down_widget=self._character_button, ) origin = self._highlight_button.get_screen_space_center() - ba.buttonwidget( + bui.buttonwidget( edit=self._highlight_button, - on_activate_call=ba.WeakCall( + on_activate_call=bui.WeakCall( self._make_picker, 'highlight', origin ), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, h_align='center', v_align='center', position=(self._width * 0.5 + b_offs, v - 65), size=(0, 0), draw_controller=btn, - text=ba.Lstr(resource=self._r + '.highlightText'), + text=bui.Lstr(resource=self._r + '.highlightText'), scale=0.7, - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, maxwidth=120, ) self._update_character() @@ -501,7 +503,10 @@ class EditProfileWindow(ba.Window): from bastd.ui import account from bastd.ui.profile import upgrade as pupgrade - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return @@ -513,20 +518,20 @@ class EditProfileWindow(ba.Window): icons_str = ' '.join( [ - ba.charstr(n) + bui.charstr(n) for n in [ - ba.SpecialChar.GOOGLE_PLAY_GAMES_LOGO, - ba.SpecialChar.GAME_CENTER_LOGO, - ba.SpecialChar.GAME_CIRCLE_LOGO, - ba.SpecialChar.OUYA_LOGO, - ba.SpecialChar.LOCAL_ACCOUNT, - ba.SpecialChar.ALIBABA_LOGO, - ba.SpecialChar.OCULUS_LOGO, - ba.SpecialChar.NVIDIA_LOGO, + bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO, + bui.SpecialChar.GAME_CENTER_LOGO, + bui.SpecialChar.GAME_CIRCLE_LOGO, + bui.SpecialChar.OUYA_LOGO, + bui.SpecialChar.LOCAL_ACCOUNT, + bui.SpecialChar.ALIBABA_LOGO, + bui.SpecialChar.OCULUS_LOGO, + bui.SpecialChar.NVIDIA_LOGO, ] ] ) - txtl = ba.Lstr( + txtl = bui.Lstr( resource='editProfileWindow.accountProfileInfoText', subs=[('${ICONS}', icons_str)], ) @@ -542,7 +547,7 @@ class EditProfileWindow(ba.Window): """Show an explanation of local profiles.""" from bastd.ui.confirm import ConfirmWindow - txtl = ba.Lstr(resource='editProfileWindow.localProfileInfoText') + txtl = bui.Lstr(resource='editProfileWindow.localProfileInfoText') ConfirmWindow( txtl, cancel_button=False, @@ -555,7 +560,7 @@ class EditProfileWindow(ba.Window): """Show an explanation of global profiles.""" from bastd.ui.confirm import ConfirmWindow - txtl = ba.Lstr(resource='editProfileWindow.globalProfileInfoText') + txtl = bui.Lstr(resource='editProfileWindow.globalProfileInfoText') ConfirmWindow( txtl, cancel_button=False, @@ -568,14 +573,18 @@ class EditProfileWindow(ba.Window): """Refresh available characters/icons.""" from bastd.actor import spazappearance + assert bui.app.classic is not None + self._spazzes = spazappearance.get_appearances() self._spazzes.sort() self._icon_textures = [ - ba.gettexture(ba.app.spaz_appearances[s].icon_texture) + bui.gettexture(bui.app.classic.spaz_appearances[s].icon_texture) for s in self._spazzes ] self._icon_tint_textures = [ - ba.gettexture(ba.app.spaz_appearances[s].icon_mask_texture) + bui.gettexture( + bui.app.classic.spaz_appearances[s].icon_mask_texture + ) for s in self._spazzes ] @@ -623,15 +632,13 @@ class EditProfileWindow(ba.Window): def _make_picker( self, picker_type: str, origin: tuple[float, float] ) -> None: - from bastd.ui import colorpicker - if picker_type == 'color': initial_color = self._color elif picker_type == 'highlight': initial_color = self._highlight else: raise ValueError('invalid picker_type: ' + picker_type) - colorpicker.ColorPicker( + ColorPicker( parent=self._root_widget, position=origin, offset=( @@ -646,8 +653,9 @@ class EditProfileWindow(ba.Window): def _cancel(self) -> None: from bastd.ui.profile.browser import ProfileBrowserWindow - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_right') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( ProfileBrowserWindow( 'in_left', selected_profile=self._existing_profile, @@ -658,12 +666,12 @@ class EditProfileWindow(ba.Window): def _set_color(self, color: tuple[float, float, float]) -> None: self._color = color if self._color_button: - ba.buttonwidget(edit=self._color_button, color=color) + bui.buttonwidget(edit=self._color_button, color=color) def _set_highlight(self, color: tuple[float, float, float]) -> None: self._highlight = color if self._highlight_button: - ba.buttonwidget(edit=self._highlight_button, color=color) + bui.buttonwidget(edit=self._highlight_button, color=color) def color_picker_closing(self, picker: ColorPicker) -> None: """Called when a color picker is closing.""" @@ -671,11 +679,11 @@ class EditProfileWindow(ba.Window): return tag = picker.get_tag() if tag == 'color': - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._color_button ) elif tag == 'highlight': - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._highlight_button ) else: @@ -697,30 +705,33 @@ class EditProfileWindow(ba.Window): self._update_character() def _update_clipped_name(self) -> None: + plus = bui.app.plus + assert plus is not None + if not self._clipped_name_text: return name = self.getname() if name == '__account__': name = ( - ba.internal.get_v1_account_name() - if ba.internal.get_v1_account_state() == 'signed_in' + plus.get_v1_account_name() + if plus.get_v1_account_state() == 'signed_in' else '???' ) if len(name) > 10 and not (self._global or self._is_account_profile): - ba.textwidget( + bui.textwidget( edit=self._clipped_name_text, - text=ba.Lstr( + text=bui.Lstr( resource='inGameClippedNameText', subs=[('${NAME}', name[:10] + '...')], ), ) else: - ba.textwidget(edit=self._clipped_name_text, text='') + bui.textwidget(edit=self._clipped_name_text, text='') def _update_character(self, change: int = 0) -> None: self._icon_index = (self._icon_index + change) % len(self._spazzes) if self._character_button: - ba.buttonwidget( + bui.buttonwidget( edit=self._character_button, texture=self._icon_textures[self._icon_index], tint_texture=self._icon_tint_textures[self._icon_index], @@ -730,7 +741,7 @@ class EditProfileWindow(ba.Window): def _update_icon(self) -> None: if self._icon_button_label: - ba.textwidget(edit=self._icon_button_label, text=self._icon) + bui.textwidget(edit=self._icon_button_label, text=self._icon) def getname(self) -> str: """Return the current profile name value.""" @@ -739,26 +750,29 @@ class EditProfileWindow(ba.Window): elif self._global: new_name = self._name else: - new_name = cast(str, ba.textwidget(query=self._text_field)) + new_name = cast(str, bui.textwidget(query=self._text_field)) return new_name def save(self, transition_out: bool = True) -> bool: """Save has been selected.""" from bastd.ui.profile.browser import ProfileBrowserWindow + plus = bui.app.plus + assert plus is not None + new_name = self.getname().strip() if not new_name: - ba.screenmessage(ba.Lstr(resource='nameNotEmptyText')) - ba.playsound(ba.getsound('error')) + bui.screenmessage(bui.Lstr(resource='nameNotEmptyText')) + bui.getsound('error').play() return False if transition_out: - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() # Delete old in case we're renaming. if self._existing_profile and self._existing_profile != new_name: - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'REMOVE_PLAYER_PROFILE', 'name': self._existing_profile, @@ -769,7 +783,7 @@ class EditProfileWindow(ba.Window): # new name (will need to re-request it). self._global = False - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'ADD_PLAYER_PROFILE', 'name': new_name, @@ -784,9 +798,10 @@ class EditProfileWindow(ba.Window): ) if transition_out: - ba.internal.run_transactions() - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.app.ui.set_main_menu_window( + plus.run_v1_account_transactions() + bui.containerwidget(edit=self._root_widget, transition='out_right') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( ProfileBrowserWindow( 'in_left', selected_profile=new_name, diff --git a/assets/src/ba_data/python/bastd/ui/profile/upgrade.py b/src/assets/ba_data/python/bastd/ui/profile/upgrade.py similarity index 66% rename from assets/src/ba_data/python/bastd/ui/profile/upgrade.py rename to src/assets/ba_data/python/bastd/ui/profile/upgrade.py index e4a30f62..4d3b3ca5 100644 --- a/assets/src/ba_data/python/bastd/ui/profile/upgrade.py +++ b/src/assets/ba_data/python/bastd/ui/profile/upgrade.py @@ -8,15 +8,15 @@ import time import weakref from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any + from bastd.ui.profile.edit import EditProfileWindow -class ProfileUpgradeWindow(ba.Window): +class ProfileUpgradeWindow(bui.Window): """Window for player profile upgrades to global.""" def __init__( @@ -24,88 +24,97 @@ class ProfileUpgradeWindow(ba.Window): edit_profile_window: EditProfileWindow, transition: str = 'in_right', ): - from ba.internal import master_server_get + if bui.app.classic is None: + raise RuntimeError('This requires classic.') + + plus = bui.app.plus + assert plus is not None self._r = 'editProfileWindow' self._width = 680 self._height = 350 - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale self._base_scale = ( 2.05 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.5 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.2 ) self._upgrade_start_time: float | None = None self._name = edit_profile_window.getname() self._edit_profile_window = weakref.ref(edit_profile_window) - top_extra = 15 if uiscale is ba.UIScale.SMALL else 15 + top_extra = 15 if uiscale is bui.UIScale.SMALL else 15 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), toolbar_visibility='menu_currency', transition=transition, scale=self._base_scale, - stack_offset=(0, 15) if uiscale is ba.UIScale.SMALL else (0, 0), + stack_offset=(0, 15) + if uiscale is bui.UIScale.SMALL + else (0, 0), ) ) - cancel_button = ba.buttonwidget( + cancel_button = bui.buttonwidget( parent=self._root_widget, position=(52, 30), size=(155, 60), scale=0.8, autoselect=True, - label=ba.Lstr(resource='cancelText'), + label=bui.Lstr(resource='cancelText'), on_activate_call=self._cancel, ) - self._upgrade_button = ba.buttonwidget( + self._upgrade_button = bui.buttonwidget( parent=self._root_widget, position=(self._width - 190, 30), size=(155, 60), scale=0.8, autoselect=True, - label=ba.Lstr(resource='upgradeText'), + label=bui.Lstr(resource='upgradeText'), on_activate_call=self._on_upgrade_press, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=cancel_button, start_button=self._upgrade_button, selected_child=self._upgrade_button, ) - ba.textwidget( + assert bui.app.classic is not None + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 38), size=(0, 0), - text=ba.Lstr(resource=self._r + '.upgradeToGlobalProfileText'), - color=ba.app.ui.title_color, + text=bui.Lstr(resource=self._r + '.upgradeToGlobalProfileText'), + color=bui.app.classic.ui.title_color, maxwidth=self._width * 0.45, scale=1.0, h_align='center', v_align='center', ) - ba.textwidget( + assert bui.app.classic is not None + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 100), size=(0, 0), - text=ba.Lstr(resource=self._r + '.upgradeProfileInfoText'), - color=ba.app.ui.infotextcolor, + text=bui.Lstr(resource=self._r + '.upgradeProfileInfoText'), + color=bui.app.classic.ui.infotextcolor, maxwidth=self._width * 0.8, scale=0.7, h_align='center', v_align='center', ) - self._status_text = ba.textwidget( + self._status_text = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 160), size=(0, 0), - text=ba.Lstr( + text=bui.Lstr( resource=self._r + '.checkingAvailabilityText', subs=[('${NAME}', self._name)], ), @@ -116,7 +125,7 @@ class ProfileUpgradeWindow(ba.Window): v_align='center', ) - self._price_text = ba.textwidget( + self._price_text = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 230), size=(0, 0), @@ -128,13 +137,13 @@ class ProfileUpgradeWindow(ba.Window): v_align='center', ) - self._tickets_text: ba.Widget | None - if not ba.app.ui.use_toolbars: - self._tickets_text = ba.textwidget( + self._tickets_text: bui.Widget | None + if not bui.app.classic.ui.use_toolbars: + self._tickets_text = bui.textwidget( parent=self._root_widget, position=(self._width * 0.9 - 5, self._height - 30), size=(0, 0), - text=ba.charstr(ba.SpecialChar.TICKET) + '123', + text=bui.charstr(bui.SpecialChar.TICKET) + '123', color=(0.2, 1, 0.2), maxwidth=100, scale=0.5, @@ -144,62 +153,59 @@ class ProfileUpgradeWindow(ba.Window): else: self._tickets_text = None - master_server_get( + bui.app.classic.master_server_v1_get( 'bsGlobalProfileCheck', - {'name': self._name, 'b': ba.app.build_number}, - callback=ba.WeakCall(self._profile_check_result), + {'name': self._name, 'b': bui.app.build_number}, + callback=bui.WeakCall(self._profile_check_result), ) - self._cost = ba.internal.get_v1_account_misc_read_val( + self._cost = plus.get_v1_account_misc_read_val( 'price.global_profile', 500 ) self._status: str | None = 'waiting' - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - timetype=ba.TimeType.REAL, - repeat=True, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) self._update() def _profile_check_result(self, result: dict[str, Any] | None) -> None: if result is None: - ba.textwidget( + bui.textwidget( edit=self._status_text, - text=ba.Lstr(resource='internal.unavailableNoConnectionText'), + text=bui.Lstr(resource='internal.unavailableNoConnectionText'), color=(1, 0, 0), ) self._status = 'error' - ba.buttonwidget( + bui.buttonwidget( edit=self._upgrade_button, color=(0.4, 0.4, 0.4), textcolor=(0.5, 0.5, 0.5), ) else: if result['available']: - ba.textwidget( + bui.textwidget( edit=self._status_text, - text=ba.Lstr( + text=bui.Lstr( resource=self._r + '.availableText', subs=[('${NAME}', self._name)], ), color=(0, 1, 0), ) - ba.textwidget( + bui.textwidget( edit=self._price_text, - text=ba.charstr(ba.SpecialChar.TICKET) + str(self._cost), + text=bui.charstr(bui.SpecialChar.TICKET) + str(self._cost), ) self._status = None else: - ba.textwidget( + bui.textwidget( edit=self._status_text, - text=ba.Lstr( + text=bui.Lstr( resource=self._r + '.unavailableText', subs=[('${NAME}', self._name)], ), color=(1, 0, 0), ) self._status = 'unavailable' - ba.buttonwidget( + bui.buttonwidget( edit=self._upgrade_button, color=(0.4, 0.4, 0.4), textcolor=(0.5, 0.5, 0.5), @@ -209,14 +215,17 @@ class ProfileUpgradeWindow(ba.Window): from bastd.ui import getcurrency if self._status is None: + plus = bui.app.plus + assert plus is not None + # If it appears we don't have enough tickets, offer to buy more. - tickets = ba.internal.get_v1_account_ticket_count() + tickets = plus.get_v1_account_ticket_count() if tickets < self._cost: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() getcurrency.show_get_tickets_prompt() return - ba.screenmessage( - ba.Lstr(resource='purchasingText'), color=(0, 1, 0) + bui.screenmessage( + bui.Lstr(resource='purchasingText'), color=(0, 1, 0) ) self._status = 'pre_upgrading' @@ -230,30 +239,38 @@ class ProfileUpgradeWindow(ba.Window): success = edit_profile_window.save(transition_out=False) if not success: print('profile upgrade: error occurred saving profile') - ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) - ba.playsound(ba.getsound('error')) + bui.screenmessage( + bui.Lstr(resource='errorText'), color=(1, 0, 0) + ) + bui.getsound('error').play() return - ba.internal.add_transaction( + plus.add_v1_account_transaction( {'type': 'UPGRADE_PROFILE', 'name': self._name} ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() self._status = 'upgrading' self._upgrade_start_time = time.time() else: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() def _update(self) -> None: + plus = bui.app.plus + assert plus is not None + try: - t_str = str(ba.internal.get_v1_account_ticket_count()) + t_str = str(plus.get_v1_account_ticket_count()) except Exception: t_str = '?' if self._tickets_text is not None: - ba.textwidget( + bui.textwidget( edit=self._tickets_text, - text=ba.Lstr( + text=bui.Lstr( resource='getTicketsWindow.youHaveShortText', subs=[ - ('${COUNT}', ba.charstr(ba.SpecialChar.TICKET) + t_str) + ( + '${COUNT}', + bui.charstr(bui.SpecialChar.TICKET) + t_str, + ) ], ), ) @@ -262,10 +279,10 @@ class ProfileUpgradeWindow(ba.Window): # through, we're done. if ( self._status == 'upgrading' - and not ba.internal.have_outstanding_transactions() + and not plus.have_outstanding_v1_account_transactions() ): self._status = 'exiting' - ba.containerwidget(edit=self._root_widget, transition='out_right') + bui.containerwidget(edit=self._root_widget, transition='out_right') edit_profile_window = self._edit_profile_window() if edit_profile_window is None: print( @@ -273,7 +290,7 @@ class ProfileUpgradeWindow(ba.Window): ' original edit window gone' ) return - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() edit_profile_window.reload_window() def _cancel(self) -> None: @@ -283,6 +300,6 @@ class ProfileUpgradeWindow(ba.Window): self._upgrade_start_time is not None and time.time() - self._upgrade_start_time < 10.0 ): - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return - ba.containerwidget(edit=self._root_widget, transition='out_right') + bui.containerwidget(edit=self._root_widget, transition='out_right') diff --git a/assets/src/ba_data/python/bastd/ui/promocode.py b/src/assets/ba_data/python/bastd/ui/promocode.py similarity index 72% rename from assets/src/ba_data/python/bastd/ui/promocode.py rename to src/assets/ba_data/python/bastd/ui/promocode.py index 1096ec12..4a9f9424 100644 --- a/assets/src/ba_data/python/bastd/ui/promocode.py +++ b/src/assets/ba_data/python/bastd/ui/promocode.py @@ -5,22 +5,16 @@ from __future__ import annotations import time -from typing import TYPE_CHECKING -import ba -import ba.internal - -if TYPE_CHECKING: - pass +import bauiv1 as bui -class PromoCodeWindow(ba.Window): +class PromoCodeWindow(bui.Window): """Window for entering promo codes.""" def __init__( - self, modal: bool = False, origin_widget: ba.Widget | None = None + self, modal: bool = False, origin_widget: bui.Widget | None = None ): - scale_origin: tuple[float, float] | None if origin_widget is not None: self._transition_out = 'out_scale' @@ -37,24 +31,25 @@ class PromoCodeWindow(ba.Window): self._modal = modal self._r = 'promoCodeWindow' - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height), transition=transition, toolbar_visibility='menu_minimal_no_back', scale_origin_stack_offset=scale_origin, scale=( 2.0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.5 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), ) ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, scale=0.5, position=(40, height - 40), @@ -63,19 +58,19 @@ class PromoCodeWindow(ba.Window): on_activate_call=self._do_back, autoselect=True, color=(0.55, 0.5, 0.6), - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, - text=ba.Lstr(resource=self._r + '.codeText'), + text=bui.Lstr(resource=self._r + '.codeText'), position=(22, height - 113), color=(0.8, 0.8, 0.8, 1.0), size=(90, 30), h_align='right', ) - self._text_field = ba.textwidget( + self._text_field = bui.textwidget( parent=self._root_widget, position=(125, height - 121), size=(280, 46), @@ -84,25 +79,25 @@ class PromoCodeWindow(ba.Window): v_align='center', max_chars=64, color=(0.9, 0.9, 0.9, 1.0), - description=ba.Lstr(resource=self._r + '.codeText'), + description=bui.Lstr(resource=self._r + '.codeText'), editable=True, padding=4, on_return_press_call=self._activate_enter_button, ) - ba.widget(edit=btn, down_widget=self._text_field) + bui.widget(edit=btn, down_widget=self._text_field) b_width = 200 - self._enter_button = btn2 = ba.buttonwidget( + self._enter_button = btn2 = bui.buttonwidget( parent=self._root_widget, position=(width * 0.5 - b_width * 0.5, height - 200), size=(b_width, 60), scale=1.0, - label=ba.Lstr( + label=bui.Lstr( resource='submitText', fallback_resource=self._r + '.enterText' ), on_activate_call=self._do_enter, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=btn, start_button=btn2, @@ -113,11 +108,12 @@ class PromoCodeWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.settings.advanced import AdvancedSettingsWindow - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) if not self._modal: - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( AdvancedSettingsWindow(transition='in_left').get_root_widget() ) @@ -128,18 +124,22 @@ class PromoCodeWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.settings.advanced import AdvancedSettingsWindow - ba.containerwidget( + plus = bui.app.plus + assert plus is not None + + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) if not self._modal: - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( AdvancedSettingsWindow(transition='in_left').get_root_widget() ) - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'PROMO_CODE', 'expire_time': time.time() + 5, - 'code': ba.textwidget(query=self._text_field), + 'code': bui.textwidget(query=self._text_field), } ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() diff --git a/assets/src/ba_data/python/bastd/ui/purchase.py b/src/assets/ba_data/python/bastd/ui/purchase.py similarity index 65% rename from assets/src/ba_data/python/bastd/ui/purchase.py rename to src/assets/ba_data/python/bastd/ui/purchase.py index 8bcdd097..ba0819ea 100644 --- a/assets/src/ba_data/python/bastd/ui/purchase.py +++ b/src/assets/ba_data/python/bastd/ui/purchase.py @@ -6,27 +6,31 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any -class PurchaseWindow(ba.Window): +class PurchaseWindow(bui.Window): """Window for purchasing one or more items.""" def __init__( self, items: list[str], transition: str = 'in_right', - header_text: ba.Lstr | None = None, + header_text: bui.Lstr | None = None, ): - from ba.internal import get_store_item_display_size - from bastd.ui.store import item as storeitemui + from bastd.ui.store.item import instantiate_store_item_display + + plus = bui.app.plus + assert plus is not None + + assert bui.app.classic is not None + store = bui.app.classic.store if header_text is None: - header_text = ba.Lstr( + header_text = bui.Lstr( resource='unlockThisText', fallback_resource='unlockThisInTheStoreText', ) @@ -35,26 +39,26 @@ class PurchaseWindow(ba.Window): self._items = list(items) self._width = 580 self._height = 520 - uiscale = ba.app.ui.uiscale + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), transition=transition, toolbar_visibility='menu_currency', scale=( 1.2 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.1 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -15) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) self._is_double = False - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 30), size=(0, 0), @@ -65,9 +69,9 @@ class PurchaseWindow(ba.Window): scale=1.2, color=(1, 0.8, 0.3, 1), ) - size = get_store_item_display_size(items[0]) + size = store.get_store_item_display_size(items[0]) display: dict[str, Any] = {} - storeitemui.instantiate_store_item_display( + instantiate_store_item_display( items[0], display, parent_widget=self._root_widget, @@ -91,15 +95,15 @@ class PurchaseWindow(ba.Window): pass # not working else: if self._items == ['pro']: - price_str = ba.internal.get_price(self._items[0]) + price_str = plus.get_price(self._items[0]) pyoffs = -15 else: pyoffs = 0 - price = self._price = ba.internal.get_v1_account_misc_read_val( + price = self._price = plus.get_v1_account_misc_read_val( 'price.' + str(items[0]), -1 ) - price_str = ba.charstr(ba.SpecialChar.TICKET) + str(price) - self._price_text = ba.textwidget( + price_str = bui.charstr(bui.SpecialChar.TICKET) + str(price) + self._price_text = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, 150 + pyoffs), size=(0, 0), @@ -111,33 +115,30 @@ class PurchaseWindow(ba.Window): color=(0.2, 1, 0.2), ) - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - timetype=ba.TimeType.REAL, - repeat=True, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self._root_widget, position=(50, 40), size=(150, 60), scale=1.0, on_activate_call=self._cancel, autoselect=True, - label=ba.Lstr(resource='cancelText'), + label=bui.Lstr(resource='cancelText'), ) - self._purchase_button = ba.buttonwidget( + self._purchase_button = bui.buttonwidget( parent=self._root_widget, position=(self._width - 200, 40), size=(150, 60), scale=1.0, on_activate_call=self._purchase, autoselect=True, - label=ba.Lstr(resource='store.purchaseText'), + label=bui.Lstr(resource='store.purchaseText'), ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._cancel_button, start_button=self._purchase_button, @@ -147,38 +148,48 @@ class PurchaseWindow(ba.Window): def _update(self) -> None: can_die = False + plus = bui.app.plus + assert plus is not None + # We go away if we see that our target item is owned. if self._items == ['pro']: - if ba.app.accounts_v1.have_pro(): + assert bui.app.classic is not None + if bui.app.classic.accounts.have_pro(): can_die = True else: - if ba.internal.get_purchased(self._items[0]): + if plus.get_purchased(self._items[0]): can_die = True if can_die: - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.containerwidget(edit=self._root_widget, transition='out_left') def _purchase(self) -> None: from bastd.ui import getcurrency + plus = bui.app.plus + assert plus is not None + if self._items == ['pro']: - ba.internal.purchase('pro') + plus.purchase('pro') else: ticket_count: int | None try: - ticket_count = ba.internal.get_v1_account_ticket_count() + ticket_count = plus.get_v1_account_ticket_count() except Exception: ticket_count = None if ticket_count is not None and ticket_count < self._price: getcurrency.show_get_tickets_prompt() - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return def do_it() -> None: - ba.internal.in_game_purchase(self._items[0], self._price) + plus = bui.app.plus + assert plus is not None - ba.playsound(ba.getsound('swish')) + plus.in_game_purchase(self._items[0], self._price) + + bui.getsound('swish').play() do_it() def _cancel(self) -> None: - ba.containerwidget(edit=self._root_widget, transition='out_right') + bui.containerwidget(edit=self._root_widget, transition='out_right') diff --git a/assets/src/ba_data/python/bastd/ui/qrcode.py b/src/assets/ba_data/python/bastd/ui/qrcode.py similarity index 69% rename from assets/src/ba_data/python/bastd/ui/qrcode.py rename to src/assets/ba_data/python/bastd/ui/qrcode.py index 76e5de26..953e0edc 100644 --- a/assets/src/ba_data/python/bastd/ui/qrcode.py +++ b/src/assets/ba_data/python/bastd/ui/qrcode.py @@ -3,36 +3,35 @@ """Provides functionality for displaying QR codes.""" from __future__ import annotations -import ba -from bastd.ui import popup +from bastd.ui.popup import PopupWindow +import bauiv1 as bui -class QRCodeWindow(popup.PopupWindow): +class QRCodeWindow(PopupWindow): """Popup window that shows a QR code.""" - def __init__(self, origin_widget: ba.Widget, qr_tex: ba.Texture): - + def __init__(self, origin_widget: bui.Widget, qr_tex: bui.Texture): position = origin_widget.get_screen_space_center() - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale scale = ( 2.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.65 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.23 ) self._transitioning_out = False self._width = 450 self._height = 400 bg_color = (0.5, 0.4, 0.6) - popup.PopupWindow.__init__( - self, + super().__init__( position=position, size=(self._width, self._height), scale=scale, bg_color=bg_color, ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self.root_widget, position=(50, self._height - 30), size=(50, 50), @@ -41,10 +40,10 @@ class QRCodeWindow(popup.PopupWindow): color=bg_color, on_activate_call=self._on_cancel_press, autoselect=True, - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) - ba.imagewidget( + bui.imagewidget( parent=self.root_widget, position=(self._width * 0.5 - 150, self._height * 0.5 - 150), size=(300, 300), @@ -57,8 +56,8 @@ class QRCodeWindow(popup.PopupWindow): def _transition_out(self) -> None: if not self._transitioning_out: self._transitioning_out = True - ba.containerwidget(edit=self.root_widget, transition='out_scale') + bui.containerwidget(edit=self.root_widget, transition='out_scale') def on_popup_cancel(self) -> None: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transition_out() diff --git a/assets/src/ba_data/python/bastd/ui/radiogroup.py b/src/assets/ba_data/python/bastd/ui/radiogroup.py similarity index 77% rename from assets/src/ba_data/python/bastd/ui/radiogroup.py rename to src/assets/ba_data/python/bastd/ui/radiogroup.py index 1cc8301f..64927864 100644 --- a/assets/src/ba_data/python/bastd/ui/radiogroup.py +++ b/src/assets/ba_data/python/bastd/ui/radiogroup.py @@ -6,14 +6,14 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable, Sequence def make_radio_group( - check_boxes: Sequence[ba.Widget], + check_boxes: Sequence[bui.Widget], value_names: Sequence[str], value: str, value_change_call: Callable[[str], Any], @@ -21,19 +21,19 @@ def make_radio_group( """Link the provided check_boxes together into a radio group.""" def _radio_press( - check_string: str, other_check_boxes: list[ba.Widget], val: int + check_string: str, other_check_boxes: list[bui.Widget], val: int ) -> None: if val == 1: value_change_call(check_string) for cbx in other_check_boxes: - ba.checkboxwidget(edit=cbx, value=False) + bui.checkboxwidget(edit=cbx, value=False) for i, check_box in enumerate(check_boxes): - ba.checkboxwidget( + bui.checkboxwidget( edit=check_box, value=(value == value_names[i]), is_radio_button=True, - on_value_change_call=ba.Call( + on_value_change_call=bui.Call( _radio_press, value_names[i], [c for c in check_boxes if c != check_box], diff --git a/assets/src/ba_data/python/bastd/ui/report.py b/src/assets/ba_data/python/bastd/ui/report.py similarity index 65% rename from assets/src/ba_data/python/bastd/ui/report.py rename to src/assets/ba_data/python/bastd/ui/report.py index 1a760434..a56dca19 100644 --- a/assets/src/ba_data/python/bastd/ui/report.py +++ b/src/assets/ba_data/python/bastd/ui/report.py @@ -4,38 +4,38 @@ from __future__ import annotations -import ba -import ba.internal +import bauiv1 as bui -class ReportPlayerWindow(ba.Window): +class ReportPlayerWindow(bui.Window): """Player for reporting naughty players.""" - def __init__(self, account_id: str, origin_widget: ba.Widget): + def __init__(self, account_id: str, origin_widget: bui.Widget): self._width = 550 self._height = 220 self._account_id = account_id self._transition_out = 'out_scale' scale_origin = origin_widget.get_screen_space_center() - overlay_stack = ba.internal.get_special_widget('overlay_stack') - uiscale = ba.app.ui.uiscale + overlay_stack = bui.get_special_widget('overlay_stack') + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), parent=overlay_stack, transition='in_scale', scale_origin_stack_offset=scale_origin, scale=( 1.8 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.35 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), ) ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self._root_widget, scale=0.7, position=(40, self._height - 50), @@ -44,13 +44,13 @@ class ReportPlayerWindow(ba.Window): on_activate_call=self.close, autoselect=True, color=(0.4, 0.4, 0.5), - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._cancel_button ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.64), size=(0, 0), @@ -58,22 +58,22 @@ class ReportPlayerWindow(ba.Window): scale=1.2, h_align='center', v_align='center', - text=ba.Lstr(resource='reportThisPlayerReasonText'), + text=bui.Lstr(resource='reportThisPlayerReasonText'), maxwidth=self._width * 0.85, ) - ba.buttonwidget( + bui.buttonwidget( parent=self._root_widget, size=(235, 60), position=(20, 30), - label=ba.Lstr(resource='reportThisPlayerLanguageText'), + label=bui.Lstr(resource='reportThisPlayerLanguageText'), on_activate_call=self._on_language_press, autoselect=True, ) - ba.buttonwidget( + bui.buttonwidget( parent=self._root_widget, size=(235, 60), position=(self._width - 255, 30), - label=ba.Lstr(resource='reportThisPlayerCheatingText'), + label=bui.Lstr(resource='reportThisPlayerCheatingText'), on_activate_call=self._on_cheating_press, autoselect=True, ) @@ -81,17 +81,20 @@ class ReportPlayerWindow(ba.Window): def _on_language_press(self) -> None: from urllib import parse - ba.internal.add_transaction( + plus = bui.app.plus + assert plus is not None + + plus.add_v1_account_transaction( { 'type': 'REPORT_ACCOUNT', 'reason': 'language', 'account': self._account_id, } ) - body = ba.Lstr(resource='reportPlayerExplanationText').evaluate() - ba.open_url( + body = bui.Lstr(resource='reportPlayerExplanationText').evaluate() + bui.open_url( 'mailto:support@froemling.net' - f'?subject={ba.internal.appnameupper()} Player Report: ' + f'?subject={bui.appnameupper()} Player Report: ' + self._account_id + '&body=' + parse.quote(body) @@ -101,17 +104,20 @@ class ReportPlayerWindow(ba.Window): def _on_cheating_press(self) -> None: from urllib import parse - ba.internal.add_transaction( + plus = bui.app.plus + assert plus is not None + + plus.add_v1_account_transaction( { 'type': 'REPORT_ACCOUNT', 'reason': 'cheating', 'account': self._account_id, } ) - body = ba.Lstr(resource='reportPlayerExplanationText').evaluate() - ba.open_url( + body = bui.Lstr(resource='reportPlayerExplanationText').evaluate() + bui.open_url( 'mailto:support@froemling.net' - f'?subject={ba.internal.appnameupper()} Player Report: ' + f'?subject={bui.appnameupper()} Player Report: ' + self._account_id + '&body=' + parse.quote(body) @@ -120,4 +126,4 @@ class ReportPlayerWindow(ba.Window): def close(self) -> None: """Close the window.""" - ba.containerwidget(edit=self._root_widget, transition='out_scale') + bui.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/assets/src/ba_data/python/bastd/ui/resourcetypeinfo.py b/src/assets/ba_data/python/bastd/ui/resourcetypeinfo.py similarity index 68% rename from assets/src/ba_data/python/bastd/ui/resourcetypeinfo.py rename to src/assets/ba_data/python/bastd/ui/resourcetypeinfo.py index 2bd829bd..45988e23 100644 --- a/assets/src/ba_data/python/bastd/ui/resourcetypeinfo.py +++ b/src/assets/ba_data/python/bastd/ui/resourcetypeinfo.py @@ -4,35 +4,35 @@ from __future__ import annotations -import ba -from bastd.ui import popup +from bastd.ui.popup import PopupWindow +import bauiv1 as bui -class ResourceTypeInfoWindow(popup.PopupWindow): +class ResourceTypeInfoWindow(PopupWindow): """Popup window providing info about resource types.""" - def __init__(self, origin_widget: ba.Widget): - uiscale = ba.app.ui.uiscale + def __init__(self, origin_widget: bui.Widget): + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale scale = ( 2.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.65 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.23 ) self._transitioning_out = False self._width = 570 self._height = 350 bg_color = (0.5, 0.4, 0.6) - popup.PopupWindow.__init__( - self, + super().__init__( size=(self._width, self._height), toolbar_visibility='inherit', scale=scale, bg_color=bg_color, position=origin_widget.get_screen_space_center(), ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self.root_widget, position=(50, self._height - 30), size=(50, 50), @@ -41,7 +41,7 @@ class ResourceTypeInfoWindow(popup.PopupWindow): color=bg_color, on_activate_call=self._on_cancel_press, autoselect=True, - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) @@ -51,8 +51,8 @@ class ResourceTypeInfoWindow(popup.PopupWindow): def _transition_out(self) -> None: if not self._transitioning_out: self._transitioning_out = True - ba.containerwidget(edit=self.root_widget, transition='out_scale') + bui.containerwidget(edit=self.root_widget, transition='out_scale') def on_popup_cancel(self) -> None: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transition_out() diff --git a/assets/src/ba_data/python/bastd/ui/serverdialog.py b/src/assets/ba_data/python/bastd/ui/serverdialog.py similarity index 70% rename from assets/src/ba_data/python/bastd/ui/serverdialog.py rename to src/assets/ba_data/python/bastd/ui/serverdialog.py index 6f3ec3c8..4c0c49dd 100644 --- a/assets/src/ba_data/python/bastd/ui/serverdialog.py +++ b/src/assets/ba_data/python/bastd/ui/serverdialog.py @@ -6,15 +6,11 @@ from __future__ import annotations import logging from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Annotated +from typing import Annotated from efro.dataclassio import ioprepped, IOAttrs -import ba -import ba.internal - -if TYPE_CHECKING: - pass +import bauiv1 as bui @ioprepped @@ -31,40 +27,40 @@ class ServerDialogData: copy_text: Annotated[str | None, IOAttrs('copyText')] = None -class ServerDialogWindow(ba.Window): +class ServerDialogWindow(bui.Window): """A dialog window driven by the master-server.""" def __init__(self, data: ServerDialogData): self._data = data - txt = ba.Lstr( + txt = bui.Lstr( translate=('serverResponses', data.text), subs=data.subs ).evaluate() txt = txt.strip() txt_scale = 1.5 txt_height = ( - ba.internal.get_string_height(txt, suppress_warning=True) - * txt_scale + bui.get_string_height(txt, suppress_warning=True) * txt_scale ) self._width = 500 self._height = 160 + min(200, txt_height) - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), transition='in_scale', scale=( 1.8 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.35 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), ) ) - self._starttime = ba.time(ba.TimeType.REAL) + self._starttime = bui.apptime() - ba.playsound(ba.getsound('swish')) - ba.textwidget( + bui.getsound('swish').play() + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, 70 + (self._height - 70) * 0.5), size=(0, 0), @@ -77,7 +73,7 @@ class ServerDialogWindow(ba.Window): max_height=(self._height - 110), ) - show_copy = data.copy_text is not None and ba.clipboard_is_supported() + show_copy = data.copy_text is not None and bui.clipboard_is_supported() # Currently can't do both copy and cancel since they go in the same # spot. Cancel wins in this case since it is important functionality @@ -92,12 +88,12 @@ class ServerDialogWindow(ba.Window): self._cancel_button = ( None if not data.show_cancel - else ba.buttonwidget( + else bui.buttonwidget( parent=self._root_widget, position=(30, 30), size=(160, 60), autoselect=True, - label=ba.Lstr(resource='cancelText'), + label=bui.Lstr(resource='cancelText'), on_activate_call=self._cancel_press, ) ) @@ -105,17 +101,17 @@ class ServerDialogWindow(ba.Window): self._copy_button = ( None if not show_copy - else ba.buttonwidget( + else bui.buttonwidget( parent=self._root_widget, position=(30, 30), size=(160, 60), autoselect=True, - label=ba.Lstr(resource='copyText'), + label=bui.Lstr(resource='copyText'), on_activate_call=self._copy_press, ) ) - self._ok_button = ba.buttonwidget( + self._ok_button = bui.buttonwidget( parent=self._root_widget, position=( (self._width - 182) @@ -125,11 +121,11 @@ class ServerDialogWindow(ba.Window): ), size=(160, 60), autoselect=True, - label=ba.Lstr(resource='okText'), + label=bui.Lstr(resource='okText'), on_activate_call=self._ok_press, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._cancel_button, start_button=self._ok_button, @@ -138,31 +134,35 @@ class ServerDialogWindow(ba.Window): def _copy_press(self) -> None: assert self._data.copy_text is not None - ba.clipboard_set_text(self._data.copy_text) - ba.screenmessage(ba.Lstr(resource='copyConfirmText'), color=(0, 1, 0)) + bui.clipboard_set_text(self._data.copy_text) + bui.screenmessage(bui.Lstr(resource='copyConfirmText'), color=(0, 1, 0)) def _ok_press(self) -> None: - if ba.time(ba.TimeType.REAL) - self._starttime < 1.0: - ba.playsound(ba.getsound('error')) + plus = bui.app.plus + assert plus is not None + if bui.apptime() - self._starttime < 1.0: + bui.getsound('error').play() return - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'DIALOG_RESPONSE', 'dialogID': self._data.dialog_id, 'response': 1, } ) - ba.containerwidget(edit=self._root_widget, transition='out_scale') + bui.containerwidget(edit=self._root_widget, transition='out_scale') def _cancel_press(self) -> None: - if ba.time(ba.TimeType.REAL) - self._starttime < 1.0: - ba.playsound(ba.getsound('error')) + plus = bui.app.plus + assert plus is not None + if bui.apptime() - self._starttime < 1.0: + bui.getsound('error').play() return - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'DIALOG_RESPONSE', 'dialogID': self._data.dialog_id, 'response': 0, } ) - ba.containerwidget(edit=self._root_widget, transition='out_scale') + bui.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/assets/src/ba_data/python/bastd/ui/settings/__init__.py b/src/assets/ba_data/python/bastd/ui/settings/__init__.py similarity index 100% rename from assets/src/ba_data/python/bastd/ui/settings/__init__.py rename to src/assets/ba_data/python/bastd/ui/settings/__init__.py diff --git a/assets/src/ba_data/python/bastd/ui/settings/advanced.py b/src/assets/ba_data/python/bastd/ui/settings/advanced.py similarity index 72% rename from assets/src/ba_data/python/bastd/ui/settings/advanced.py rename to src/assets/ba_data/python/bastd/ui/settings/advanced.py index 65ea7085..acce6f2a 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/advanced.py +++ b/src/assets/ba_data/python/bastd/ui/settings/advanced.py @@ -4,33 +4,37 @@ from __future__ import annotations +import os +import logging from typing import TYPE_CHECKING -import ba -import ba.internal -from bastd.ui import popup as popup_ui +from bastd.ui.popup import PopupMenu +import bauiv1 as bui if TYPE_CHECKING: from typing import Any -class AdvancedSettingsWindow(ba.Window): +class AdvancedSettingsWindow(bui.Window): """Window for editing advanced game settings.""" def __init__( self, transition: str = 'in_right', - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements - from ba.internal import master_server_get import threading + if bui.app.classic is None: + raise RuntimeError('This requires classic support.') + # Preload some modules we use in a background thread so we won't # have a visual hitch when the user taps them. threading.Thread(target=self._preload_modules).start() - app = ba.app + app = bui.app + assert app.classic is not None # If they provided an origin-widget, scale up from that. scale_origin: tuple[float, float] | None @@ -42,34 +46,36 @@ class AdvancedSettingsWindow(ba.Window): self._transition_out = 'out_right' scale_origin = None - uiscale = ba.app.ui.uiscale - self._width = 870.0 if uiscale is ba.UIScale.SMALL else 670.0 - x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + uiscale = bui.app.classic.ui.uiscale + self._width = 870.0 if uiscale is bui.UIScale.SMALL else 670.0 + x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 390.0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 450.0 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 520.0 ) + self._lang_status_text: bui.Widget | None = None + self._spacing = 32 self._menu_open = False - top_extra = 10 if uiscale is ba.UIScale.SMALL else 0 + top_extra = 10 if uiscale is bui.UIScale.SMALL else 0 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), transition=transition, toolbar_visibility='menu_minimal', scale_origin_stack_offset=scale_origin, scale=( 2.06 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.4 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -25) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) @@ -78,7 +84,7 @@ class AdvancedSettingsWindow(ba.Window): self._prev_lang_list: list[str] = [] self._complete_langs_list: list | None = None self._complete_langs_error = False - self._language_popup: popup_ui.PopupMenu | None = None + self._language_popup: PopupMenu | None = None # In vr-mode, the internal keyboard is currently the *only* option, # so no need to show this. @@ -94,7 +100,7 @@ class AdvancedSettingsWindow(ba.Window): if self._show_always_use_internal_keyboard: self._sub_height += 62 - self._show_disable_gyro = app.platform in {'ios', 'android'} + self._show_disable_gyro = app.classic.platform in {'ios', 'android'} if self._show_disable_gyro: self._sub_height += 42 @@ -110,45 +116,45 @@ class AdvancedSettingsWindow(ba.Window): self._r = 'settingsWindowAdvanced' - if app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: - ba.containerwidget( + if app.classic.ui.use_toolbars and uiscale is bui.UIScale.SMALL: + bui.containerwidget( edit=self._root_widget, on_cancel_call=self._do_back ) self._back_button = None else: - self._back_button = ba.buttonwidget( + self._back_button = bui.buttonwidget( parent=self._root_widget, position=(53 + x_inset, self._height - 60), size=(140, 60), scale=0.8, autoselect=True, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', on_activate_call=self._do_back, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button ) - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=self._root_widget, position=(0, self._height - 52), size=(self._width, 25), - text=ba.Lstr(resource=f'{self._r}.titleText'), - color=app.ui.title_color, + text=bui.Lstr(resource=f'{self._r}.titleText'), + color=app.classic.ui.title_color, h_align='center', v_align='top', ) if self._back_button is not None: - ba.buttonwidget( + bui.buttonwidget( edit=self._back_button, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, position=(50 + x_inset, 50), simple_culling_v=20.0, @@ -156,8 +162,8 @@ class AdvancedSettingsWindow(ba.Window): size=(self._scroll_width, self._scroll_height), selection_loops_to_parent=True, ) - ba.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) - self._subcontainer = ba.containerwidget( + bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._sub_width, self._sub_height), background=False, @@ -167,26 +173,23 @@ class AdvancedSettingsWindow(ba.Window): self._rebuild() # Rebuild periodically to pick up language changes/additions/etc. - self._rebuild_timer = ba.Timer( - 1.0, - ba.WeakCall(self._rebuild), - repeat=True, - timetype=ba.TimeType.REAL, + self._rebuild_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._rebuild), repeat=True ) # Fetch the list of completed languages. - master_server_get( + bui.app.classic.master_server_v1_get( 'bsLangGetCompleted', {'b': app.build_number}, - callback=ba.WeakCall(self._completed_langs_cb), + callback=bui.WeakCall(self._completed_langs_cb), ) # noinspection PyUnresolvedReferences @staticmethod def _preload_modules() -> None: """Preload modules we use (called in bg thread).""" + from bauiv1 import modutils as _unused2 from bastd.ui import config as _unused1 - from ba import modutils as _unused2 from bastd.ui.settings import vrtesting as _unused3 from bastd.ui.settings import nettesting as _unused4 from bastd.ui import appinvite as _unused5 @@ -197,26 +200,28 @@ class AdvancedSettingsWindow(ba.Window): def _update_lang_status(self) -> None: if self._complete_langs_list is not None: - up_to_date = ba.app.lang.language in self._complete_langs_list - ba.textwidget( + up_to_date = bui.app.lang.language in self._complete_langs_list + bui.textwidget( edit=self._lang_status_text, text='' - if ba.app.lang.language == 'Test' - else ba.Lstr( + if bui.app.lang.language == 'Test' + else bui.Lstr( resource=f'{self._r}.translationNoUpdateNeededText' ) if up_to_date - else ba.Lstr(resource=f'{self._r}.translationUpdateNeededText'), + else bui.Lstr( + resource=f'{self._r}.translationUpdateNeededText' + ), color=(0.2, 1.0, 0.2, 0.8) if up_to_date else (1.0, 0.2, 0.2, 0.8), ) else: - ba.textwidget( + bui.textwidget( edit=self._lang_status_text, - text=ba.Lstr(resource=f'{self._r}.translationFetchErrorText') + text=bui.Lstr(resource=f'{self._r}.translationFetchErrorText') if self._complete_langs_error - else ba.Lstr( + else bui.Lstr( resource=f'{self._r}.translationFetchingStatusText' ), color=(1.0, 0.5, 0.2) @@ -228,10 +233,14 @@ class AdvancedSettingsWindow(ba.Window): # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals - from bastd.ui.config import ConfigCheckBox - from ba.modutils import show_user_scripts - available_languages = ba.app.lang.available_languages + from bastd.ui.config import ConfigCheckBox + from bauiv1.modutils import show_user_scripts + + plus = bui.app.plus + assert plus is not None + + available_languages = bui.app.lang.available_languages # Don't rebuild if the menu is open or if our language and # language-list hasn't changed. @@ -241,11 +250,11 @@ class AdvancedSettingsWindow(ba.Window): # make this more limited to it only rebuilds that one menu instead # of everything. if self._menu_open or ( - self._prev_lang == ba.app.config.get('Lang', None) + self._prev_lang == bui.app.config.get('Lang', None) and self._prev_lang_list == available_languages ): return - self._prev_lang = ba.app.config.get('Lang', None) + self._prev_lang = bui.app.config.get('Lang', None) self._prev_lang_list = available_languages # Clear out our sub-container. @@ -259,50 +268,52 @@ class AdvancedSettingsWindow(ba.Window): # Update our existing back button and title. if self._back_button is not None: - ba.buttonwidget( - edit=self._back_button, label=ba.Lstr(resource='backText') + bui.buttonwidget( + edit=self._back_button, label=bui.Lstr(resource='backText') ) - ba.buttonwidget( - edit=self._back_button, label=ba.charstr(ba.SpecialChar.BACK) + bui.buttonwidget( + edit=self._back_button, label=bui.charstr(bui.SpecialChar.BACK) ) - ba.textwidget( - edit=self._title_text, text=ba.Lstr(resource=f'{self._r}.titleText') + bui.textwidget( + edit=self._title_text, + text=bui.Lstr(resource=f'{self._r}.titleText'), ) this_button_width = 410 - self._promo_code_button = ba.buttonwidget( + self._promo_code_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 14), size=(this_button_width, 60), autoselect=True, - label=ba.Lstr(resource=f'{self._r}.enterPromoCodeText'), + label=bui.Lstr(resource=f'{self._r}.enterPromoCodeText'), text_scale=1.0, on_activate_call=self._on_promo_code_press, ) if self._back_button is not None: - ba.widget( + bui.widget( edit=self._promo_code_button, up_widget=self._back_button, left_widget=self._back_button, ) v -= self._extra_button_spacing * 0.8 - ba.textwidget( + assert bui.app.classic is not None + bui.textwidget( parent=self._subcontainer, position=(200, v + 10), size=(0, 0), - text=ba.Lstr(resource=f'{self._r}.languageText'), + text=bui.Lstr(resource=f'{self._r}.languageText'), maxwidth=150, scale=0.95, - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, h_align='right', v_align='center', ) - languages = ba.app.lang.available_languages - cur_lang = ba.app.config.get('Lang', None) + languages = bui.app.lang.available_languages + cur_lang = bui.app.config.get('Lang', None) if cur_lang is None: cur_lang = 'Auto' @@ -311,12 +322,17 @@ class AdvancedSettingsWindow(ba.Window): try: import json - with open('ba_data/data/langdata.json', encoding='utf-8') as infile: + with open( + os.path.join( + bui.app.data_directory, 'ba_data', 'data', 'langdata.json' + ), + encoding='utf-8', + ) as infile: lang_names_translated = json.loads(infile.read())[ 'lang_names_translated' ] except Exception: - ba.print_exception('Error reading lang data.') + logging.exception('Error reading lang data.') lang_names_translated = {} langs_translated = {} @@ -325,7 +341,7 @@ class AdvancedSettingsWindow(ba.Window): langs_full = {} for lang in languages: - lang_translated = ba.Lstr(translate=('languages', lang)).evaluate() + lang_translated = bui.Lstr(translate=('languages', lang)).evaluate() if langs_translated[lang] == lang_translated: langs_full[lang] = lang_translated else: @@ -333,46 +349,46 @@ class AdvancedSettingsWindow(ba.Window): langs_translated[lang] + ' (' + lang_translated + ')' ) - self._language_popup = popup_ui.PopupMenu( + self._language_popup = PopupMenu( parent=self._subcontainer, position=(210, v - 19), width=150, - opening_call=ba.WeakCall(self._on_menu_open), - closing_call=ba.WeakCall(self._on_menu_close), + opening_call=bui.WeakCall(self._on_menu_open), + closing_call=bui.WeakCall(self._on_menu_close), autoselect=False, - on_value_change_call=ba.WeakCall(self._on_menu_choice), + on_value_change_call=bui.WeakCall(self._on_menu_choice), choices=['Auto'] + languages, button_size=(250, 60), choices_display=( [ - ba.Lstr( + bui.Lstr( value=( - ba.Lstr(resource='autoText').evaluate() + bui.Lstr(resource='autoText').evaluate() + ' (' - + ba.Lstr( + + bui.Lstr( translate=( 'languages', - ba.app.lang.default_language, + bui.app.lang.default_language, ) ).evaluate() + ')' ) ) ] - + [ba.Lstr(value=langs_full[l]) for l in languages] + + [bui.Lstr(value=langs_full[l]) for l in languages] ), current_choice=cur_lang, ) v -= self._spacing * 1.8 - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v + 10), size=(0, 0), - text=ba.Lstr( + text=bui.Lstr( resource=f'{self._r}.helpTranslateText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], + subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ), maxwidth=self._sub_width * 0.9, max_height=55, @@ -384,21 +400,21 @@ class AdvancedSettingsWindow(ba.Window): ) v -= self._spacing * 1.9 this_button_width = 410 - self._translation_editor_button = ba.buttonwidget( + self._translation_editor_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 24), size=(this_button_width, 60), - label=ba.Lstr( + label=bui.Lstr( resource=f'{self._r}.translationEditorButtonText', - subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))], + subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))], ), autoselect=True, - on_activate_call=ba.Call( - ba.open_url, 'https://legacy.ballistica.net/translate' + on_activate_call=bui.Call( + bui.open_url, 'https://legacy.ballistica.net/translate' ), ) - self._lang_status_text = ba.textwidget( + self._lang_status_text = bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v - 40), size=(0, 0), @@ -412,9 +428,9 @@ class AdvancedSettingsWindow(ba.Window): self._update_lang_status() v -= 40 - lang_inform = ba.internal.get_v1_account_misc_val('langInform', False) + lang_inform = plus.get_v1_account_misc_val('langInform', False) - self._language_inform_checkbox = cbw = ba.checkboxwidget( + self._language_inform_checkbox = cbw = bui.checkboxwidget( parent=self._subcontainer, position=(50, v - 50), size=(self._sub_width - 100, 30), @@ -422,11 +438,13 @@ class AdvancedSettingsWindow(ba.Window): maxwidth=430, textcolor=(0.8, 0.8, 0.8), value=lang_inform, - text=ba.Lstr(resource=f'{self._r}.translationInformMe'), - on_value_change_call=ba.WeakCall(self._on_lang_inform_value_change), + text=bui.Lstr(resource=f'{self._r}.translationInformMe'), + on_value_change_call=bui.WeakCall( + self._on_lang_inform_value_change + ), ) - ba.widget( + bui.widget( edit=self._translation_editor_button, down_widget=cbw, up_widget=self._language_popup.get_button(), @@ -439,7 +457,7 @@ class AdvancedSettingsWindow(ba.Window): position=(50, v), size=(self._sub_width - 100, 30), configkey='Kick Idle Players', - displayname=ba.Lstr(resource=f'{self._r}.kickIdlePlayersText'), + displayname=bui.Lstr(resource=f'{self._r}.kickIdlePlayersText'), scale=1.0, maxwidth=430, ) @@ -450,7 +468,7 @@ class AdvancedSettingsWindow(ba.Window): position=(50, v), size=(self._sub_width - 100, 30), configkey='Show Ping', - displayname=ba.Lstr(resource=f'{self._r}.showInGamePingText'), + displayname=bui.Lstr(resource=f'{self._r}.showInGamePingText'), scale=1.0, maxwidth=430, ) @@ -461,7 +479,7 @@ class AdvancedSettingsWindow(ba.Window): position=(50, v), size=(self._sub_width - 100, 30), configkey='Disable Camera Shake', - displayname=ba.Lstr(resource=f'{self._r}.disableCameraShakeText'), + displayname=bui.Lstr(resource=f'{self._r}.disableCameraShakeText'), scale=1.0, maxwidth=430, ) @@ -474,7 +492,7 @@ class AdvancedSettingsWindow(ba.Window): position=(50, v), size=(self._sub_width - 100, 30), configkey='Disable Camera Gyro', - displayname=ba.Lstr( + displayname=bui.Lstr( resource=f'{self._r}.disableCameraGyroscopeMotionText' ), scale=1.0, @@ -490,17 +508,17 @@ class AdvancedSettingsWindow(ba.Window): size=(self._sub_width - 100, 30), configkey='Always Use Internal Keyboard', autoselect=True, - displayname=ba.Lstr( + displayname=bui.Lstr( resource=f'{self._r}.alwaysUseInternalKeyboardText' ), scale=1.0, maxwidth=430, ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(90, v - 10), size=(0, 0), - text=ba.Lstr( + text=bui.Lstr( resource=( f'{self._r}.alwaysUseInternalKeyboardDescriptionText' ) @@ -519,87 +537,87 @@ class AdvancedSettingsWindow(ba.Window): v -= self._spacing * 2.1 this_button_width = 410 - self._modding_guide_button = ba.buttonwidget( + self._modding_guide_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 10), size=(this_button_width, 60), autoselect=True, - label=ba.Lstr(resource=f'{self._r}.moddingGuideText'), + label=bui.Lstr(resource=f'{self._r}.moddingGuideText'), text_scale=1.0, - on_activate_call=ba.Call( - ba.open_url, 'https://ballistica.net/wiki/modding-guide' + on_activate_call=bui.Call( + bui.open_url, 'https://ballistica.net/wiki/modding-guide' ), ) if self._show_always_use_internal_keyboard: assert self._always_use_internal_keyboard_check_box is not None - ba.widget( + bui.widget( edit=self._always_use_internal_keyboard_check_box.widget, down_widget=self._modding_guide_button, ) - ba.widget( + bui.widget( edit=self._modding_guide_button, up_widget=self._always_use_internal_keyboard_check_box.widget, ) else: - ba.widget( + bui.widget( edit=self._modding_guide_button, up_widget=self._kick_idle_players_check_box.widget, ) - ba.widget( + bui.widget( edit=self._kick_idle_players_check_box.widget, down_widget=self._modding_guide_button, ) v -= self._spacing * 2.0 - self._show_user_mods_button = ba.buttonwidget( + self._show_user_mods_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 10), size=(this_button_width, 60), autoselect=True, - label=ba.Lstr(resource=f'{self._r}.showUserModsText'), + label=bui.Lstr(resource=f'{self._r}.showUserModsText'), text_scale=1.0, on_activate_call=show_user_scripts, ) v -= self._spacing * 2.0 - self._plugins_button = ba.buttonwidget( + self._plugins_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 10), size=(this_button_width, 60), autoselect=True, - label=ba.Lstr(resource='pluginsText'), + label=bui.Lstr(resource='pluginsText'), text_scale=1.0, on_activate_call=self._on_plugins_button_press, ) v -= self._spacing * 0.6 - self._vr_test_button: ba.Widget | None + self._vr_test_button: bui.Widget | None if self._do_vr_test_button: v -= self._extra_button_spacing - self._vr_test_button = ba.buttonwidget( + self._vr_test_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 14), size=(this_button_width, 60), autoselect=True, - label=ba.Lstr(resource=f'{self._r}.vrTestingText'), + label=bui.Lstr(resource=f'{self._r}.vrTestingText'), text_scale=1.0, on_activate_call=self._on_vr_test_press, ) else: self._vr_test_button = None - self._net_test_button: ba.Widget | None + self._net_test_button: bui.Widget | None if self._do_net_test_button: v -= self._extra_button_spacing - self._net_test_button = ba.buttonwidget( + self._net_test_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 14), size=(this_button_width, 60), autoselect=True, - label=ba.Lstr(resource=f'{self._r}.netTestingText'), + label=bui.Lstr(resource=f'{self._r}.netTestingText'), text_scale=1.0, on_activate_call=self._on_net_test_press, ) @@ -607,65 +625,71 @@ class AdvancedSettingsWindow(ba.Window): self._net_test_button = None v -= 70 - self._benchmarks_button = ba.buttonwidget( + self._benchmarks_button = bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width / 2 - this_button_width / 2, v - 14), size=(this_button_width, 60), autoselect=True, - label=ba.Lstr(resource=f'{self._r}.benchmarksText'), + label=bui.Lstr(resource=f'{self._r}.benchmarksText'), text_scale=1.0, on_activate_call=self._on_benchmark_press, ) for child in self._subcontainer.get_children(): - ba.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20) + bui.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20) - if ba.app.ui.use_toolbars: - pbtn = ba.internal.get_special_widget('party_button') - ba.widget(edit=self._scrollwidget, right_widget=pbtn) + if bui.app.classic.ui.use_toolbars: + pbtn = bui.get_special_widget('party_button') + bui.widget(edit=self._scrollwidget, right_widget=pbtn) if self._back_button is None: - ba.widget( + bui.widget( edit=self._scrollwidget, - left_widget=ba.internal.get_special_widget('back_button'), + left_widget=bui.get_special_widget('back_button'), ) self._restore_state() def _show_restart_needed(self, value: Any) -> None: del value # Unused. - ba.screenmessage( - ba.Lstr(resource=f'{self._r}.mustRestartText'), color=(1, 1, 0) + bui.screenmessage( + bui.Lstr(resource=f'{self._r}.mustRestartText'), color=(1, 1, 0) ) def _on_lang_inform_value_change(self, val: bool) -> None: - ba.internal.add_transaction( + plus = bui.app.plus + assert plus is not None + plus.add_v1_account_transaction( {'type': 'SET_MISC_VAL', 'name': 'langInform', 'value': val} ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() def _on_vr_test_press(self) -> None: from bastd.ui.settings.vrtesting import VRTestingWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( VRTestingWindow(transition='in_right').get_root_widget() ) def _on_net_test_press(self) -> None: + plus = bui.app.plus + assert plus is not None from bastd.ui.settings.nettesting import NetTestingWindow # Net-testing requires a signed in v1 account. - if ba.internal.get_v1_account_state() != 'signed_in': - ba.screenmessage( - ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) + if plus.get_v1_account_state() != 'signed_in': + bui.screenmessage( + bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( NetTestingWindow(transition='in_right').get_root_widget() ) @@ -673,7 +697,10 @@ class AdvancedSettingsWindow(ba.Window): from bastd.ui import appinvite from bastd.ui import account - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return appinvite.handle_app_invites_press() @@ -682,8 +709,9 @@ class AdvancedSettingsWindow(ba.Window): from bastd.ui.settings.plugins import PluginWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( PluginWindow(origin_widget=self._plugins_button).get_root_widget() ) @@ -691,13 +719,17 @@ class AdvancedSettingsWindow(ba.Window): from bastd.ui.promocode import PromoCodeWindow from bastd.ui.account import show_sign_in_prompt + plus = bui.app.plus + assert plus is not None + # We have to be logged in for promo-codes to work. - if ba.internal.get_v1_account_state() != 'signed_in': + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( PromoCodeWindow( origin_widget=self._promo_code_button ).get_root_widget() @@ -707,8 +739,9 @@ class AdvancedSettingsWindow(ba.Window): from bastd.ui.debug import DebugWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( DebugWindow(transition='in_right').get_root_widget() ) @@ -764,20 +797,25 @@ class AdvancedSettingsWindow(ba.Window): sel_name = 'Back' else: raise ValueError(f'unrecognized selection \'{sel}\'') - ba.app.ui.window_states[type(self)] = {'sel_name': sel_name} + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = { + 'sel_name': sel_name + } + except Exception: - ba.print_exception(f'Error saving state for {self.__class__}') + logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: # pylint: disable=too-many-branches try: - sel_name = ba.app.ui.window_states.get(type(self), {}).get( + assert bui.app.classic is not None + sel_name = bui.app.classic.ui.window_states.get(type(self), {}).get( 'sel_name' ) if sel_name == 'Back': sel = self._back_button else: - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) if sel_name == 'VRTest': @@ -821,13 +859,13 @@ class AdvancedSettingsWindow(ba.Window): else: sel = None if sel is not None: - ba.containerwidget( + bui.containerwidget( edit=self._subcontainer, selected_child=sel, visible_child=sel, ) except Exception: - ba.print_exception(f'Error restoring state for {self.__class__}') + logging.exception('Error restoring state for %s.', self) def _on_menu_open(self) -> None: self._menu_open = True @@ -836,9 +874,9 @@ class AdvancedSettingsWindow(ba.Window): self._menu_open = False def _on_menu_choice(self, choice: str) -> None: - ba.app.lang.setlanguage(None if choice == 'Auto' else choice) + bui.app.lang.setlanguage(None if choice == 'Auto' else choice) self._save_state() - ba.timer(0.1, ba.WeakCall(self._rebuild), timetype=ba.TimeType.REAL) + bui.apptimer(0.1, bui.WeakCall(self._rebuild)) def _completed_langs_cb(self, results: dict[str, Any] | None) -> None: if results is not None and results['langs'] is not None: @@ -847,19 +885,16 @@ class AdvancedSettingsWindow(ba.Window): else: self._complete_langs_list = None self._complete_langs_error = True - ba.timer( - 0.001, - ba.WeakCall(self._update_lang_status), - timetype=ba.TimeType.REAL, - ) + bui.apptimer(0.001, bui.WeakCall(self._update_lang_status)) def _do_back(self) -> None: from bastd.ui.settings.allsettings import AllSettingsWindow self._save_state() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( AllSettingsWindow(transition='in_left').get_root_widget() ) diff --git a/assets/src/ba_data/python/bastd/ui/settings/allsettings.py b/src/assets/ba_data/python/bastd/ui/settings/allsettings.py similarity index 65% rename from assets/src/ba_data/python/bastd/ui/settings/allsettings.py rename to src/assets/ba_data/python/bastd/ui/settings/allsettings.py index 9882a93b..a4b5d20d 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/allsettings.py +++ b/src/assets/ba_data/python/bastd/ui/settings/allsettings.py @@ -5,31 +5,31 @@ from __future__ import annotations from typing import TYPE_CHECKING +from threading import Thread +import logging -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: pass -class AllSettingsWindow(ba.Window): +class AllSettingsWindow(bui.Window): """Window for selecting a settings category.""" def __init__( self, transition: str = 'in_right', - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals - import threading # Preload some modules we use in a background thread so we won't # have a visual hitch when the user taps them. - threading.Thread(target=self._preload_modules).start() + Thread(target=self._preload_modules).start() - ba.set_analytics_screen('Settings Window') + bui.set_analytics_screen('Settings Window') scale_origin: tuple[float, float] | None if origin_widget is not None: self._transition_out = 'out_scale' @@ -38,76 +38,79 @@ class AllSettingsWindow(ba.Window): else: self._transition_out = 'out_right' scale_origin = None - uiscale = ba.app.ui.uiscale - width = 900 if uiscale is ba.UIScale.SMALL else 580 - x_inset = 75 if uiscale is ba.UIScale.SMALL else 0 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + width = 900 if uiscale is bui.UIScale.SMALL else 580 + x_inset = 75 if uiscale is bui.UIScale.SMALL else 0 height = 435 self._r = 'settingsWindow' - top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 - uiscale = ba.app.ui.uiscale + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height + top_extra), transition=transition, toolbar_visibility='menu_minimal', scale_origin_stack_offset=scale_origin, scale=( 1.75 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.35 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), - stack_offset=(0, -8) if uiscale is ba.UIScale.SMALL else (0, 0), + stack_offset=(0, -8) + if uiscale is bui.UIScale.SMALL + else (0, 0), ) ) - if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: + if bui.app.classic.ui.use_toolbars and uiscale is bui.UIScale.SMALL: self._back_button = None - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, on_cancel_call=self._do_back ) else: - self._back_button = btn = ba.buttonwidget( + self._back_button = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(40 + x_inset, height - 55), size=(130, 60), scale=0.8, text_scale=1.2, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', on_activate_call=self._do_back, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(0, height - 44), size=(width, 25), - text=ba.Lstr(resource=self._r + '.titleText'), - color=ba.app.ui.title_color, + text=bui.Lstr(resource=self._r + '.titleText'), + color=bui.app.classic.ui.title_color, h_align='center', v_align='center', maxwidth=130, ) if self._back_button is not None: - ba.buttonwidget( + bui.buttonwidget( edit=self._back_button, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) v = height - 80 v -= 145 - basew = 280 if uiscale is ba.UIScale.SMALL else 230 + basew = 280 if uiscale is bui.UIScale.SMALL else 230 baseh = 170 x_offs = ( - x_inset + (105 if uiscale is ba.UIScale.SMALL else 72) - basew + x_inset + (105 if uiscale is bui.UIScale.SMALL else 72) - basew ) # now unused x_offs2 = x_offs + basew - 7 x_offs3 = x_offs + 2 * (basew - 7) @@ -115,9 +118,9 @@ class AllSettingsWindow(ba.Window): x_offs5 = x_offs3 def _b_title( - x: float, y: float, button: ba.Widget, text: str | ba.Lstr + x: float, y: float, button: bui.Widget, text: str | bui.Lstr ) -> None: - ba.textwidget( + bui.textwidget( parent=self._root_widget, text=text, position=(x + basew * 0.47, y + baseh * 0.22), @@ -129,7 +132,7 @@ class AllSettingsWindow(ba.Window): color=(0.7, 0.9, 0.7, 1.0), ) - ctb = self._controllers_button = ba.buttonwidget( + ctb = self._controllers_button = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(x_offs2, v), @@ -138,22 +141,22 @@ class AllSettingsWindow(ba.Window): label='', on_activate_call=self._do_controllers, ) - if ba.app.ui.use_toolbars and self._back_button is None: - bbtn = ba.internal.get_special_widget('back_button') - ba.widget(edit=ctb, left_widget=bbtn) + if bui.app.classic.ui.use_toolbars and self._back_button is None: + bbtn = bui.get_special_widget('back_button') + bui.widget(edit=ctb, left_widget=bbtn) _b_title( - x_offs2, v, ctb, ba.Lstr(resource=self._r + '.controllersText') + x_offs2, v, ctb, bui.Lstr(resource=self._r + '.controllersText') ) imgw = imgh = 130 - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, position=(x_offs2 + basew * 0.49 - imgw * 0.5, v + 35), size=(imgw, imgh), - texture=ba.gettexture('controllerIcon'), + texture=bui.gettexture('controllerIcon'), draw_controller=ctb, ) - gfxb = self._graphics_button = ba.buttonwidget( + gfxb = self._graphics_button = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(x_offs3, v), @@ -162,22 +165,22 @@ class AllSettingsWindow(ba.Window): label='', on_activate_call=self._do_graphics, ) - if ba.app.ui.use_toolbars: - pbtn = ba.internal.get_special_widget('party_button') - ba.widget(edit=gfxb, up_widget=pbtn, right_widget=pbtn) - _b_title(x_offs3, v, gfxb, ba.Lstr(resource=self._r + '.graphicsText')) + if bui.app.classic.ui.use_toolbars: + pbtn = bui.get_special_widget('party_button') + bui.widget(edit=gfxb, up_widget=pbtn, right_widget=pbtn) + _b_title(x_offs3, v, gfxb, bui.Lstr(resource=self._r + '.graphicsText')) imgw = imgh = 110 - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, position=(x_offs3 + basew * 0.49 - imgw * 0.5, v + 42), size=(imgw, imgh), - texture=ba.gettexture('graphicsIcon'), + texture=bui.gettexture('graphicsIcon'), draw_controller=gfxb, ) v -= baseh - 5 - abtn = self._audio_button = ba.buttonwidget( + abtn = self._audio_button = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(x_offs4, v), @@ -186,18 +189,18 @@ class AllSettingsWindow(ba.Window): label='', on_activate_call=self._do_audio, ) - _b_title(x_offs4, v, abtn, ba.Lstr(resource=self._r + '.audioText')) + _b_title(x_offs4, v, abtn, bui.Lstr(resource=self._r + '.audioText')) imgw = imgh = 120 - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, position=(x_offs4 + basew * 0.49 - imgw * 0.5 + 5, v + 35), size=(imgw, imgh), color=(1, 1, 0), - texture=ba.gettexture('audioIcon'), + texture=bui.gettexture('audioIcon'), draw_controller=abtn, ) - avb = self._advanced_button = ba.buttonwidget( + avb = self._advanced_button = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(x_offs5, v), @@ -206,14 +209,14 @@ class AllSettingsWindow(ba.Window): label='', on_activate_call=self._do_advanced, ) - _b_title(x_offs5, v, avb, ba.Lstr(resource=self._r + '.advancedText')) + _b_title(x_offs5, v, avb, bui.Lstr(resource=self._r + '.advancedText')) imgw = imgh = 120 - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, position=(x_offs5 + basew * 0.49 - imgw * 0.5 + 5, v + 35), size=(imgw, imgh), color=(0.8, 0.95, 1), - texture=ba.gettexture('advancedIcon'), + texture=bui.gettexture('advancedIcon'), draw_controller=avb, ) self._restore_state() @@ -233,10 +236,11 @@ class AllSettingsWindow(ba.Window): from bastd.ui.mainmenu import MainMenuWindow self._save_state() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( MainMenuWindow(transition='in_left').get_root_widget() ) @@ -245,8 +249,9 @@ class AllSettingsWindow(ba.Window): from bastd.ui.settings.controls import ControlsSettingsWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( ControlsSettingsWindow( origin_widget=self._controllers_button ).get_root_widget() @@ -257,8 +262,9 @@ class AllSettingsWindow(ba.Window): from bastd.ui.settings.graphics import GraphicsSettingsWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( GraphicsSettingsWindow( origin_widget=self._graphics_button ).get_root_widget() @@ -269,8 +275,9 @@ class AllSettingsWindow(ba.Window): from bastd.ui.settings.audio import AudioSettingsWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( AudioSettingsWindow( origin_widget=self._audio_button ).get_root_widget() @@ -281,8 +288,9 @@ class AllSettingsWindow(ba.Window): from bastd.ui.settings.advanced import AdvancedSettingsWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( AdvancedSettingsWindow( origin_widget=self._advanced_button ).get_root_widget() @@ -303,16 +311,20 @@ class AllSettingsWindow(ba.Window): sel_name = 'Back' else: raise ValueError(f'unrecognized selection \'{sel}\'') - ba.app.ui.window_states[type(self)] = {'sel_name': sel_name} + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = { + 'sel_name': sel_name + } except Exception: - ba.print_exception(f'Error saving state for {self}.') + logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: try: - sel_name = ba.app.ui.window_states.get(type(self), {}).get( + assert bui.app.classic is not None + sel_name = bui.app.classic.ui.window_states.get(type(self), {}).get( 'sel_name' ) - sel: ba.Widget | None + sel: bui.Widget | None if sel_name == 'Controllers': sel = self._controllers_button elif sel_name == 'Graphics': @@ -326,6 +338,6 @@ class AllSettingsWindow(ba.Window): else: sel = self._controllers_button if sel is not None: - ba.containerwidget(edit=self._root_widget, selected_child=sel) + bui.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: - ba.print_exception(f'Error restoring state for {self}.') + logging.exception('Error restoring state for %s.', self) diff --git a/assets/src/ba_data/python/bastd/ui/settings/audio.py b/src/assets/ba_data/python/bastd/ui/settings/audio.py similarity index 70% rename from assets/src/ba_data/python/bastd/ui/settings/audio.py rename to src/assets/ba_data/python/bastd/ui/settings/audio.py index 296cc0b0..9a9f67de 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/audio.py +++ b/src/assets/ba_data/python/bastd/ui/settings/audio.py @@ -5,21 +5,21 @@ from __future__ import annotations from typing import TYPE_CHECKING +import logging -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: pass -class AudioSettingsWindow(ba.Window): +class AudioSettingsWindow(bui.Window): """Window for editing audio settings.""" def __init__( self, transition: str = 'in_right', - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals @@ -27,7 +27,8 @@ class AudioSettingsWindow(ba.Window): from bastd.ui.popup import PopupMenu from bastd.ui.config import ConfigNumberEdit - music = ba.app.music + assert bui.app.classic is not None + music = bui.app.classic.music # If they provided an origin-widget, scale up from that. scale_origin: tuple[float, float] | None @@ -47,7 +48,7 @@ class AudioSettingsWindow(ba.Window): # Update: hard-coding head-relative audio to true now, # so not showing options. - # show_vr_head_relative_audio = True if ba.app.vr_mode else False + # show_vr_head_relative_audio = True if bui.app.vr_mode else False show_vr_head_relative_audio = False if show_vr_head_relative_audio: @@ -58,58 +59,58 @@ class AudioSettingsWindow(ba.Window): show_soundtracks = True height += spacing * 2.0 - uiscale = ba.app.ui.uiscale + uiscale = bui.app.classic.ui.uiscale base_scale = ( 2.05 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.6 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ) popup_menu_scale = base_scale * 1.2 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height), transition=transition, scale=base_scale, scale_origin_stack_offset=scale_origin, stack_offset=(0, -20) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - self._back_button = back_button = btn = ba.buttonwidget( + self._back_button = back_button = btn = bui.buttonwidget( parent=self._root_widget, position=(35, height - 55), size=(120, 60), scale=0.8, text_scale=1.2, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', on_activate_call=self._back, autoselect=True, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) v = height - 60 v -= spacing * 1.0 - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(width * 0.5, height - 32), size=(0, 0), - text=ba.Lstr(resource=self._r + '.titleText'), - color=ba.app.ui.title_color, + text=bui.Lstr(resource=self._r + '.titleText'), + color=bui.app.classic.ui.title_color, maxwidth=180, h_align='center', v_align='center', ) - ba.buttonwidget( + bui.buttonwidget( edit=self._back_button, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) self._sound_volume_numedit = svne = ConfigNumberEdit( @@ -117,15 +118,15 @@ class AudioSettingsWindow(ba.Window): position=(40, v), xoffset=10, configkey='Sound Volume', - displayname=ba.Lstr(resource=self._r + '.soundVolumeText'), + displayname=bui.Lstr(resource=self._r + '.soundVolumeText'), minval=0.0, maxval=1.0, increment=0.1, ) - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=svne.plusbutton, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) v -= spacing self._music_volume_numedit = ConfigNumberEdit( @@ -133,7 +134,7 @@ class AudioSettingsWindow(ba.Window): position=(40, v), xoffset=10, configkey='Music Volume', - displayname=ba.Lstr(resource=self._r + '.musicVolumeText'), + displayname=bui.Lstr(resource=self._r + '.musicVolumeText'), minval=0.0, maxval=1.0, increment=0.1, @@ -143,14 +144,14 @@ class AudioSettingsWindow(ba.Window): v -= 0.5 * spacing - self._vr_head_relative_audio_button: ba.Widget | None + self._vr_head_relative_audio_button: bui.Widget | None if show_vr_head_relative_audio: v -= 40 - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(40, v + 24), size=(0, 0), - text=ba.Lstr(resource=self._r + '.headRelativeVRAudioText'), + text=bui.Lstr(resource=self._r + '.headRelativeVRAudioText'), color=(0.8, 0.8, 0.8), maxwidth=230, h_align='left', @@ -165,19 +166,21 @@ class AudioSettingsWindow(ba.Window): scale=popup_menu_scale, choices=['Auto', 'On', 'Off'], choices_display=[ - ba.Lstr(resource='autoText'), - ba.Lstr(resource='onText'), - ba.Lstr(resource='offText'), + bui.Lstr(resource='autoText'), + bui.Lstr(resource='onText'), + bui.Lstr(resource='offText'), ], - current_choice=ba.app.config.resolve('VR Head Relative Audio'), + current_choice=bui.app.config.resolve('VR Head Relative Audio'), on_value_change_call=self._set_vr_head_relative_audio, ) self._vr_head_relative_audio_button = popup.get_button() - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(width * 0.5, v - 11), size=(0, 0), - text=ba.Lstr(resource=self._r + '.headRelativeVRAudioInfoText'), + text=bui.Lstr( + resource=self._r + '.headRelativeVRAudioInfoText' + ), scale=0.5, color=(0.7, 0.8, 0.7), maxwidth=400, @@ -189,23 +192,23 @@ class AudioSettingsWindow(ba.Window): else: self._vr_head_relative_audio_button = None - self._soundtrack_button: ba.Widget | None + self._soundtrack_button: bui.Widget | None if show_soundtracks: v -= 1.2 * spacing - self._soundtrack_button = ba.buttonwidget( + self._soundtrack_button = bui.buttonwidget( parent=self._root_widget, position=((width - 310) / 2, v), size=(310, 50), autoselect=True, - label=ba.Lstr(resource=self._r + '.soundtrackButtonText'), + label=bui.Lstr(resource=self._r + '.soundtrackButtonText'), on_activate_call=self._do_soundtracks, ) v -= spacing * 0.5 - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(0, v), size=(width, 20), - text=ba.Lstr(resource=self._r + '.soundtrackDescriptionText'), + text=bui.Lstr(resource=self._r + '.soundtrackDescriptionText'), flatness=1.0, h_align='center', scale=0.5, @@ -217,14 +220,14 @@ class AudioSettingsWindow(ba.Window): # Tweak a few navigation bits. try: - ba.widget(edit=back_button, down_widget=svne.minusbutton) + bui.widget(edit=back_button, down_widget=svne.minusbutton) except Exception: - ba.print_exception('Error wiring AudioSettingsWindow.') + logging.exception('Error wiring AudioSettingsWindow.') self._restore_state() def _set_vr_head_relative_audio(self, val: str) -> None: - cfg = ba.app.config + cfg = bui.app.config cfg['VR Head Relative Audio'] = val cfg.apply_and_commit() @@ -234,22 +237,21 @@ class AudioSettingsWindow(ba.Window): # We require disk access for soundtracks; # if we don't have it, request it. - if not ba.internal.have_permission(ba.Permission.STORAGE): - ba.playsound(ba.getsound('ding')) - ba.screenmessage( - ba.Lstr(resource='storagePermissionAccessText'), + if not bui.have_permission(bui.Permission.STORAGE): + bui.getsound('ding').play() + bui.screenmessage( + bui.Lstr(resource='storagePermissionAccessText'), color=(0.5, 1, 0.5), ) - ba.timer( - 1.0, - ba.Call(ba.internal.request_permission, ba.Permission.STORAGE), - timetype=ba.TimeType.REAL, + bui.apptimer( + 1.0, bui.Call(bui.request_permission, bui.Permission.STORAGE) ) return self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( stb.SoundtrackBrowserWindow( origin_widget=self._soundtrack_button ).get_root_widget() @@ -260,10 +262,11 @@ class AudioSettingsWindow(ba.Window): from bastd.ui.settings import allsettings self._save_state() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( allsettings.AllSettingsWindow( transition='in_left' ).get_root_widget() @@ -288,14 +291,16 @@ class AudioSettingsWindow(ba.Window): sel_name = 'VRHeadRelative' else: raise ValueError(f'unrecognized selection \'{sel}\'') - ba.app.ui.window_states[type(self)] = sel_name + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = sel_name except Exception: - ba.print_exception(f'Error saving state for {self.__class__}.') + logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: try: - sel_name = ba.app.ui.window_states.get(type(self)) - sel: ba.Widget | None + assert bui.app.classic is not None + sel_name = bui.app.classic.ui.window_states.get(type(self)) + sel: bui.Widget | None if sel_name == 'SoundMinus': sel = self._sound_volume_numedit.minusbutton elif sel_name == 'SoundPlus': @@ -313,6 +318,6 @@ class AudioSettingsWindow(ba.Window): else: sel = self._back_button if sel: - ba.containerwidget(edit=self._root_widget, selected_child=sel) + bui.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: - ba.print_exception(f'Error restoring state for {self.__class__}.') + logging.exception('Error restoring state for %s.', self) diff --git a/assets/src/ba_data/python/bastd/ui/settings/controls.py b/src/assets/ba_data/python/bastd/ui/settings/controls.py similarity index 67% rename from assets/src/ba_data/python/bastd/ui/settings/controls.py rename to src/assets/ba_data/python/bastd/ui/settings/controls.py index 73a2fe39..c45e9a03 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/controls.py +++ b/src/assets/ba_data/python/bastd/ui/settings/controls.py @@ -4,29 +4,24 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -import ba -import ba.internal - -if TYPE_CHECKING: - pass +from bastd.ui.popup import PopupMenu +import bascenev1 as bs +import bauiv1 as bui -class ControlsSettingsWindow(ba.Window): +class ControlsSettingsWindow(bui.Window): """Top level control settings window.""" def __init__( self, transition: str = 'in_right', - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): # FIXME: should tidy up here. # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=cyclic-import - from bastd.ui import popup as popup_ui self._have_selected_child = False @@ -42,9 +37,8 @@ class ControlsSettingsWindow(ba.Window): scale_origin = None self._r = 'configControllersWindow' - app = ba.app - - # is_fire_tv = ba.internal.is_running_on_fire_tv() + app = bui.app + assert app.classic is not None spacing = 50.0 button_width = 350.0 @@ -57,8 +51,8 @@ class ControlsSettingsWindow(ba.Window): # not hard code them here. show_gamepads = False - platform = app.platform - subplatform = app.subplatform + platform = app.classic.platform + subplatform = app.classic.subplatform non_vr_windows = platform == 'windows' and ( subplatform != 'oculus' or not app.vr_mode ) @@ -67,7 +61,7 @@ class ControlsSettingsWindow(ba.Window): height += spacing show_touch = False - if ba.internal.have_touchscreen_input(): + if bs.have_touchscreen_input(): show_touch = True height += spacing @@ -77,10 +71,7 @@ class ControlsSettingsWindow(ba.Window): height += space_height show_keyboard = False - if ( - ba.internal.getinputdevice('Keyboard', '#1', doraise=False) - is not None - ): + if bs.getinputdevice('Keyboard', '#1', doraise=False) is not None: show_keyboard = True height += spacing show_keyboard_p2 = False if app.vr_mode else show_keyboard @@ -108,7 +99,7 @@ class ControlsSettingsWindow(ba.Window): # (we can run into problems where devices register as one of each # type otherwise).. show_mac_controller_subsystem = False - if platform == 'mac' and ba.internal.is_xcode_build(): + if platform == 'mac' and bui.is_xcode_build(): show_mac_controller_subsystem = True if show_mac_controller_subsystem: @@ -117,107 +108,108 @@ class ControlsSettingsWindow(ba.Window): if show_xinput_toggle: height += spacing - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale smallscale = 1.7 if show_keyboard else 2.2 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height), transition=transition, scale_origin_stack_offset=scale_origin, stack_offset=( - (0, -10) if uiscale is ba.UIScale.SMALL else (0, 0) + (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0) ), scale=( smallscale - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.5 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), ) ) - self._back_button = btn = ba.buttonwidget( + self._back_button = btn = bui.buttonwidget( parent=self._root_widget, position=(35, height - 60), size=(140, 65), scale=0.8, text_scale=1.2, autoselect=True, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', on_activate_call=self._back, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) # We need these vars to exist even if the buttons don't. - self._gamepads_button: ba.Widget | None = None - self._touch_button: ba.Widget | None = None - self._keyboard_button: ba.Widget | None = None - self._keyboard_2_button: ba.Widget | None = None - self._idevices_button: ba.Widget | None = None + self._gamepads_button: bui.Widget | None = None + self._touch_button: bui.Widget | None = None + self._keyboard_button: bui.Widget | None = None + self._keyboard_2_button: bui.Widget | None = None + self._idevices_button: bui.Widget | None = None - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(0, height - 49), size=(width, 25), - text=ba.Lstr(resource=self._r + '.titleText'), - color=ba.app.ui.title_color, + text=bui.Lstr(resource=self._r + '.titleText'), + color=bui.app.classic.ui.title_color, h_align='center', v_align='top', ) - ba.buttonwidget( + bui.buttonwidget( edit=btn, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) v = height - 75 v -= spacing if show_touch: - self._touch_button = btn = ba.buttonwidget( + self._touch_button = btn = bui.buttonwidget( parent=self._root_widget, position=((width - button_width) / 2, v), size=(button_width, 43), autoselect=True, - label=ba.Lstr(resource=self._r + '.configureTouchText'), + label=bui.Lstr(resource=self._r + '.configureTouchText'), on_activate_call=self._do_touchscreen, ) - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) if not self._have_selected_child: - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._touch_button ) - ba.widget( + bui.widget( edit=self._back_button, down_widget=self._touch_button ) self._have_selected_child = True v -= spacing if show_gamepads: - self._gamepads_button = btn = ba.buttonwidget( + self._gamepads_button = btn = bui.buttonwidget( parent=self._root_widget, position=((width - button_width) / 2 - 7, v), size=(button_width, 43), autoselect=True, - label=ba.Lstr(resource=self._r + '.configureControllersText'), + label=bui.Lstr(resource=self._r + '.configureControllersText'), on_activate_call=self._do_gamepads, ) - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) if not self._have_selected_child: - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._gamepads_button ) - ba.widget( + bui.widget( edit=self._back_button, down_widget=self._gamepads_button ) self._have_selected_child = True @@ -229,59 +221,59 @@ class ControlsSettingsWindow(ba.Window): v -= space_height if show_keyboard: - self._keyboard_button = btn = ba.buttonwidget( + self._keyboard_button = btn = bui.buttonwidget( parent=self._root_widget, position=((width - button_width) / 2 + 5, v), size=(button_width, 43), autoselect=True, - label=ba.Lstr(resource=self._r + '.configureKeyboardText'), + label=bui.Lstr(resource=self._r + '.configureKeyboardText'), on_activate_call=self._config_keyboard, ) - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) if not self._have_selected_child: - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._keyboard_button ) - ba.widget( + bui.widget( edit=self._back_button, down_widget=self._keyboard_button ) self._have_selected_child = True v -= spacing if show_keyboard_p2: - self._keyboard_2_button = ba.buttonwidget( + self._keyboard_2_button = bui.buttonwidget( parent=self._root_widget, position=((width - button_width) / 2 - 3, v), size=(button_width, 43), autoselect=True, - label=ba.Lstr(resource=self._r + '.configureKeyboard2Text'), + label=bui.Lstr(resource=self._r + '.configureKeyboard2Text'), on_activate_call=self._config_keyboard2, ) v -= spacing if show_space_2: v -= space_height if show_remote: - self._idevices_button = btn = ba.buttonwidget( + self._idevices_button = btn = bui.buttonwidget( parent=self._root_widget, position=((width - button_width) / 2 - 5, v), size=(button_width, 43), autoselect=True, - label=ba.Lstr(resource=self._r + '.configureMobileText'), + label=bui.Lstr(resource=self._r + '.configureMobileText'), on_activate_call=self._do_mobile_devices, ) - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) if not self._have_selected_child: - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._idevices_button ) - ba.widget( + bui.widget( edit=self._back_button, down_widget=self._idevices_button ) self._have_selected_child = True @@ -290,43 +282,37 @@ class ControlsSettingsWindow(ba.Window): if show_xinput_toggle: def do_toggle(value: bool) -> None: - ba.screenmessage( - ba.Lstr(resource='settingsWindowAdvanced.mustRestartText'), + bui.screenmessage( + bui.Lstr(resource='settingsWindowAdvanced.mustRestartText'), color=(1, 1, 0), ) - ba.playsound(ba.getsound('gunCocking')) - ba.internal.set_low_level_config_value( - 'enablexinput', not value - ) + bui.getsound('gunCocking').play() + bui.set_low_level_config_value('enablexinput', not value) - ba.checkboxwidget( + bui.checkboxwidget( parent=self._root_widget, position=(100, v + 3), size=(120, 30), - value=( - not ba.internal.get_low_level_config_value( - 'enablexinput', 1 - ) - ), + value=(not bui.get_low_level_config_value('enablexinput', 1)), maxwidth=200, on_value_change_call=do_toggle, - text=ba.Lstr(resource='disableXInputText'), + text=bui.Lstr(resource='disableXInputText'), autoselect=True, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(width * 0.5, v - 5), size=(0, 0), - text=ba.Lstr(resource='disableXInputDescriptionText'), + text=bui.Lstr(resource='disableXInputDescriptionText'), scale=0.5, h_align='center', v_align='center', - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, maxwidth=width * 0.8, ) v -= spacing if show_mac_controller_subsystem: - popup_ui.PopupMenu( + PopupMenu( parent=self._root_widget, position=(260, v - 10), width=160, @@ -334,42 +320,42 @@ class ControlsSettingsWindow(ba.Window): scale=1.5, choices=['Classic', 'MFi', 'Both'], choices_display=[ - ba.Lstr(resource='macControllerSubsystemClassicText'), - ba.Lstr(resource='macControllerSubsystemMFiText'), - ba.Lstr(resource='macControllerSubsystemBothText'), + bui.Lstr(resource='macControllerSubsystemClassicText'), + bui.Lstr(resource='macControllerSubsystemMFiText'), + bui.Lstr(resource='macControllerSubsystemBothText'), ], - current_choice=ba.app.config.resolve( + current_choice=bui.app.config.resolve( 'Mac Controller Subsystem' ), on_value_change_call=self._set_mac_controller_subsystem, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(245, v + 13), size=(0, 0), - text=ba.Lstr(resource='macControllerSubsystemTitleText'), + text=bui.Lstr(resource='macControllerSubsystemTitleText'), scale=1.0, h_align='right', v_align='center', - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, maxwidth=180, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(width * 0.5, v - 20), size=(0, 0), - text=ba.Lstr(resource='macControllerSubsystemDescriptionText'), + text=bui.Lstr(resource='macControllerSubsystemDescriptionText'), scale=0.5, h_align='center', v_align='center', - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, maxwidth=width * 0.8, ) v -= spacing * 1.5 self._restore_state() def _set_mac_controller_subsystem(self, val: str) -> None: - cfg = ba.app.config + cfg = bui.app.config cfg['Mac Controller Subsystem'] = val cfg.apply_and_commit() @@ -378,10 +364,11 @@ class ControlsSettingsWindow(ba.Window): from bastd.ui.settings.keyboard import ConfigKeyboardWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( ConfigKeyboardWindow( - ba.internal.getinputdevice('Keyboard', '#1') + bs.getinputdevice('Keyboard', '#1') ).get_root_widget() ) @@ -390,10 +377,11 @@ class ControlsSettingsWindow(ba.Window): from bastd.ui.settings.keyboard import ConfigKeyboardWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( ConfigKeyboardWindow( - ba.internal.getinputdevice('Keyboard', '#2') + bs.getinputdevice('Keyboard', '#2') ).get_root_widget() ) @@ -402,8 +390,9 @@ class ControlsSettingsWindow(ba.Window): from bastd.ui.settings.remoteapp import RemoteAppSettingsWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( RemoteAppSettingsWindow().get_root_widget() ) @@ -412,16 +401,20 @@ class ControlsSettingsWindow(ba.Window): from bastd.ui.settings.gamepadselect import GamepadSelectWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window(GamepadSelectWindow().get_root_widget()) + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( + GamepadSelectWindow().get_root_widget() + ) def _do_touchscreen(self) -> None: # pylint: disable=cyclic-import from bastd.ui.settings.touchscreen import TouchscreenSettingsWindow self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( TouchscreenSettingsWindow().get_root_widget() ) @@ -439,10 +432,12 @@ class ControlsSettingsWindow(ba.Window): sel_name = 'iDevices' else: sel_name = 'Back' - ba.app.ui.window_states[type(self)] = sel_name + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = sel_name def _restore_state(self) -> None: - sel_name = ba.app.ui.window_states.get(type(self)) + assert bui.app.classic is not None + sel_name = bui.app.classic.ui.window_states.get(type(self)) if sel_name == 'GamePads': sel = self._gamepads_button elif sel_name == 'Touch': @@ -461,16 +456,17 @@ class ControlsSettingsWindow(ba.Window): if self._gamepads_button is not None else self._back_button ) - ba.containerwidget(edit=self._root_widget, selected_child=sel) + bui.containerwidget(edit=self._root_widget, selected_child=sel) def _back(self) -> None: # pylint: disable=cyclic-import from bastd.ui.settings.allsettings import AllSettingsWindow self._save_state() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( AllSettingsWindow(transition='in_left').get_root_widget() ) diff --git a/assets/src/ba_data/python/bastd/ui/settings/gamepad.py b/src/assets/ba_data/python/bastd/ui/settings/gamepad.py similarity index 80% rename from assets/src/ba_data/python/bastd/ui/settings/gamepad.py rename to src/assets/ba_data/python/bastd/ui/settings/gamepad.py index cbb58a73..b4c23a5b 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/gamepad.py +++ b/src/assets/ba_data/python/bastd/ui/settings/gamepad.py @@ -4,21 +4,22 @@ from __future__ import annotations +import logging from typing import TYPE_CHECKING -import ba -import ba.internal +import bascenev1 as bs +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable -class GamepadSettingsWindow(ba.Window): +class GamepadSettingsWindow(bui.Window): """Window for configuring a gamepad.""" def __init__( self, - gamepad: ba.InputDevice, + gamepad: bs.InputDevice, is_main_menu: bool = True, transition: str = 'in_right', transition_out: str = 'out_right', @@ -44,19 +45,20 @@ class GamepadSettingsWindow(ba.Window): self._width = 700 if self._is_secondary else 730 self._height = 440 if self._is_secondary else 450 self._spacing = 40 - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), scale=( 1.63 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.35 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(-20, -16) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), transition=transition, ) @@ -68,18 +70,18 @@ class GamepadSettingsWindow(ba.Window): def _rebuild_ui(self) -> None: # pylint: disable=too-many-statements # pylint: disable=too-many-locals - from ba.internal import get_device_value + + assert bui.app.classic is not None # Clear existing UI. for widget in self._root_widget.get_children(): widget.delete() - self._textwidgets: dict[str, ba.Widget] = {} + self._textwidgets: dict[str, bui.Widget] = {} # If we were supplied with settings, we're a secondary joystick and # just operate on that. in the other (normal) case we make our own. if not self._is_secondary: - # Fill our temp config with present values (for our primary and # secondary controls). self._settings = {} @@ -142,93 +144,92 @@ class GamepadSettingsWindow(ba.Window): 'analogStickUD_B', 'enableSecondary', ]: - val = get_device_value(self._input, skey) + val = bui.app.classic.get_input_device_mapped_value( + self._input, skey + ) if val != -1: self._settings[skey] = val - back_button: ba.Widget | None + back_button: bui.Widget | None if self._is_secondary: - back_button = ba.buttonwidget( + back_button = bui.buttonwidget( parent=self._root_widget, position=(self._width - 180, self._height - 65), autoselect=True, size=(160, 60), - label=ba.Lstr(resource='doneText'), + label=bui.Lstr(resource='doneText'), scale=0.9, on_activate_call=self._save, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, start_button=back_button, on_cancel_call=back_button.activate, ) cancel_button = None else: - cancel_button = ba.buttonwidget( + cancel_button = bui.buttonwidget( parent=self._root_widget, position=(51, self._height - 65), autoselect=True, size=(160, 60), - label=ba.Lstr(resource='cancelText'), + label=bui.Lstr(resource='cancelText'), scale=0.9, on_activate_call=self._cancel, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=cancel_button ) - save_button: ba.Widget | None + save_button: bui.Widget | None if not self._is_secondary: - save_button = ba.buttonwidget( + save_button = bui.buttonwidget( parent=self._root_widget, - position=( - self._width - (165 if self._is_secondary else 195), - self._height - 65, - ), - size=((160 if self._is_secondary else 180), 60), + position=(self._width - 195, self._height - 65), + size=(180, 60), autoselect=True, - label=ba.Lstr(resource='doneText') - if self._is_secondary - else ba.Lstr(resource='saveText'), + label=bui.Lstr(resource='saveText'), scale=0.9, on_activate_call=self._save, ) - ba.containerwidget(edit=self._root_widget, start_button=save_button) + bui.containerwidget( + edit=self._root_widget, start_button=save_button + ) else: save_button = None if not self._is_secondary: v = self._height - 59 - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(0, v + 5), size=(self._width, 25), - text=ba.Lstr(resource=self._r + '.titleText'), - color=ba.app.ui.title_color, + text=bui.Lstr(resource=self._r + '.titleText'), + color=bui.app.classic.ui.title_color, maxwidth=310, h_align='center', v_align='center', ) v -= 48 - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(0, v + 3), size=(self._width, 25), text=self._name, - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, maxwidth=self._width * 0.9, h_align='center', v_align='center', ) v -= self._spacing * 1 - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(50, v + 10), size=(self._width - 100, 30), - text=ba.Lstr(resource=self._r + '.appliesToAllText'), + text=bui.Lstr(resource=self._r + '.appliesToAllText'), maxwidth=330, scale=0.65, color=(0.5, 0.6, 0.5, 1.0), @@ -239,36 +240,36 @@ class GamepadSettingsWindow(ba.Window): self._enable_check_box = None else: v = self._height - 49 - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(0, v + 5), size=(self._width, 25), - text=ba.Lstr(resource=self._r + '.secondaryText'), - color=ba.app.ui.title_color, + text=bui.Lstr(resource=self._r + '.secondaryText'), + color=bui.app.classic.ui.title_color, maxwidth=300, h_align='center', v_align='center', ) v -= self._spacing * 1 - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(50, v + 10), size=(self._width - 100, 30), - text=ba.Lstr(resource=self._r + '.secondHalfText'), + text=bui.Lstr(resource=self._r + '.secondHalfText'), maxwidth=300, scale=0.65, color=(0.6, 0.8, 0.6, 1.0), h_align='center', ) - self._enable_check_box = ba.checkboxwidget( + self._enable_check_box = bui.checkboxwidget( parent=self._root_widget, position=(self._width * 0.5 - 80, v - 73), value=self.get_enable_secondary_value(), autoselect=True, on_value_change_call=self._enable_check_box_changed, size=(200, 30), - text=ba.Lstr(resource=self._r + '.secondaryEnableText'), + text=bui.Lstr(resource=self._r + '.secondaryEnableText'), scale=1.2, ) v = self._height - 205 @@ -278,13 +279,13 @@ class GamepadSettingsWindow(ba.Window): d_color = (0.4, 0.4, 0.8) sclx = 1.2 scly = 0.98 - dpm = ba.Lstr(resource=self._r + '.pressAnyButtonOrDpadText') - dpm2 = ba.Lstr(resource=self._r + '.ifNothingHappensTryAnalogText') + dpm = bui.Lstr(resource=self._r + '.pressAnyButtonOrDpadText') + dpm2 = bui.Lstr(resource=self._r + '.ifNothingHappensTryAnalogText') self._capture_button( pos=(h_offs, v + scly * dist), color=d_color, button='buttonUp' + self._ext, - texture=ba.gettexture('upButton'), + texture=bui.gettexture('upButton'), scale=1.0, message=dpm, message2=dpm2, @@ -293,7 +294,7 @@ class GamepadSettingsWindow(ba.Window): pos=(h_offs - sclx * dist, v), color=d_color, button='buttonLeft' + self._ext, - texture=ba.gettexture('leftButton'), + texture=bui.gettexture('leftButton'), scale=1.0, message=dpm, message2=dpm2, @@ -302,7 +303,7 @@ class GamepadSettingsWindow(ba.Window): pos=(h_offs + sclx * dist, v), color=d_color, button='buttonRight' + self._ext, - texture=ba.gettexture('rightButton'), + texture=bui.gettexture('rightButton'), scale=1.0, message=dpm, message2=dpm2, @@ -311,21 +312,21 @@ class GamepadSettingsWindow(ba.Window): pos=(h_offs, v - scly * dist), color=d_color, button='buttonDown' + self._ext, - texture=ba.gettexture('downButton'), + texture=bui.gettexture('downButton'), scale=1.0, message=dpm, message2=dpm2, ) - dpm3 = ba.Lstr(resource=self._r + '.ifNothingHappensTryDpadText') + dpm3 = bui.Lstr(resource=self._r + '.ifNothingHappensTryDpadText') self._capture_button( pos=(h_offs + 130, v - 125), color=(0.4, 0.4, 0.6), button='analogStickLR' + self._ext, maxwidth=140, - texture=ba.gettexture('analogStick'), + texture=bui.gettexture('analogStick'), scale=1.2, - message=ba.Lstr(resource=self._r + '.pressLeftRightText'), + message=bui.Lstr(resource=self._r + '.pressLeftRightText'), message2=dpm3, ) @@ -333,7 +334,7 @@ class GamepadSettingsWindow(ba.Window): pos=(self._width * 0.5, v), color=(0.4, 0.4, 0.6), button='buttonStart' + self._ext, - texture=ba.gettexture('startButton'), + texture=bui.gettexture('startButton'), scale=0.7, ) @@ -343,35 +344,35 @@ class GamepadSettingsWindow(ba.Window): pos=(h_offs, v + scly * dist), color=(0.6, 0.4, 0.8), button='buttonPickUp' + self._ext, - texture=ba.gettexture('buttonPickUp'), + texture=bui.gettexture('buttonPickUp'), scale=1.0, ) self._capture_button( pos=(h_offs - sclx * dist, v), color=(0.7, 0.5, 0.1), button='buttonPunch' + self._ext, - texture=ba.gettexture('buttonPunch'), + texture=bui.gettexture('buttonPunch'), scale=1.0, ) self._capture_button( pos=(h_offs + sclx * dist, v), color=(0.5, 0.2, 0.1), button='buttonBomb' + self._ext, - texture=ba.gettexture('buttonBomb'), + texture=bui.gettexture('buttonBomb'), scale=1.0, ) self._capture_button( pos=(h_offs, v - scly * dist), color=(0.2, 0.5, 0.2), button='buttonJump' + self._ext, - texture=ba.gettexture('buttonJump'), + texture=bui.gettexture('buttonJump'), scale=1.0, ) - self._advanced_button = ba.buttonwidget( + self._advanced_button = bui.buttonwidget( parent=self._root_widget, autoselect=True, - label=ba.Lstr(resource=self._r + '.advancedText'), + label=bui.Lstr(resource=self._r + '.advancedText'), text_scale=0.9, color=(0.45, 0.4, 0.5), textcolor=(0.65, 0.6, 0.7), @@ -382,16 +383,16 @@ class GamepadSettingsWindow(ba.Window): try: if cancel_button is not None and save_button is not None: - ba.widget(edit=cancel_button, right_widget=save_button) - ba.widget(edit=save_button, left_widget=cancel_button) + bui.widget(edit=cancel_button, right_widget=save_button) + bui.widget(edit=save_button, left_widget=cancel_button) except Exception: - ba.print_exception('Error wiring up gamepad config window.') + logging.exception('Error wiring up gamepad config window.') def get_r(self) -> str: """(internal)""" return self._r - def get_advanced_button(self) -> ba.Widget: + def get_advanced_button(self) -> bui.Widget: """(internal)""" return self._advanced_button @@ -408,7 +409,7 @@ class GamepadSettingsWindow(ba.Window): """(internal)""" return self._ext - def get_input(self) -> ba.InputDevice: + def get_input(self) -> bs.InputDevice: """(internal)""" return self._input @@ -437,7 +438,6 @@ class GamepadSettingsWindow(ba.Window): assert self._settings is not None if value: if 'unassignedButtonsRun' in self._settings: - # Clear since this is default. del self._settings['unassignedButtonsRun'] return @@ -455,7 +455,6 @@ class GamepadSettingsWindow(ba.Window): assert self._settings is not None if value: if 'startButtonActivatesDefaultWidget' in self._settings: - # Clear since this is default. del self._settings['startButtonActivatesDefaultWidget'] return @@ -471,7 +470,6 @@ class GamepadSettingsWindow(ba.Window): assert self._settings is not None if not value: if 'uiOnly' in self._settings: - # Clear since this is default. del self._settings['uiOnly'] return @@ -487,7 +485,6 @@ class GamepadSettingsWindow(ba.Window): assert self._settings is not None if not value: if 'ignoreCompletely' in self._settings: - # Clear since this is default. del self._settings['ignoreCompletely'] return @@ -503,7 +500,6 @@ class GamepadSettingsWindow(ba.Window): assert self._settings is not None if not value: if 'autoRecalibrateAnalogStick' in self._settings: - # Clear since this is default. del self._settings['autoRecalibrateAnalogStick'] else: @@ -513,7 +509,7 @@ class GamepadSettingsWindow(ba.Window): """(internal)""" assert self._settings is not None if not self._is_secondary: - raise Exception('enable value only applies to secondary editor') + raise RuntimeError('Enable value only applies to secondary editor.') return self._settings.get('enableSecondary', False) def show_secondary_editor(self) -> None: @@ -526,12 +522,11 @@ class GamepadSettingsWindow(ba.Window): transition_out='out_scale', ) - def get_control_value_name(self, control: str) -> str | ba.Lstr: + def get_control_value_name(self, control: str) -> str | bui.Lstr: """(internal)""" # pylint: disable=too-many-return-statements assert self._settings is not None if control == 'analogStickLR' + self._ext: - # This actually shows both LR and UD. sval1 = ( self._settings['analogStickLR' + self._ext] @@ -557,7 +552,7 @@ class GamepadSettingsWindow(ba.Window): if control in ['triggerRun1' + self._ext, 'triggerRun2' + self._ext]: if control in self._settings: return self._input.get_axis_name(self._settings[control]) - return ba.Lstr(resource=self._r + '.unsetText') + return bui.Lstr(resource=self._r + '.unsetText') # Dead-zone. if control == 'analogStickDeadZone' + self._ext: @@ -574,18 +569,17 @@ class GamepadSettingsWindow(ba.Window): 'buttonDown' + self._ext, ] if control in dpad_buttons: - # If *any* dpad buttons are assigned, show only button assignments. if any(b in self._settings for b in dpad_buttons): if control in self._settings: return self._input.get_button_name(self._settings[control]) - return ba.Lstr(resource=self._r + '.unsetText') + return bui.Lstr(resource=self._r + '.unsetText') # No dpad buttons - show the dpad number for all 4. - return ba.Lstr( + return bui.Lstr( value='${A} ${B}', subs=[ - ('${A}', ba.Lstr(resource=self._r + '.dpadText')), + ('${A}', bui.Lstr(resource=self._r + '.dpadText')), ( '${B}', str( @@ -602,7 +596,7 @@ class GamepadSettingsWindow(ba.Window): # other buttons.. if control in self._settings: return self._input.get_button_name(self._settings[control]) - return ba.Lstr(resource=self._r + '.unsetText') + return bui.Lstr(resource=self._r + '.unsetText') def _gamepad_event( self, @@ -625,7 +619,6 @@ class GamepadSettingsWindow(ba.Window): 'buttonRight' + ext, ]: if event['type'] in ['BUTTONDOWN', 'HATMOTION']: - # If its a button-down. if event['type'] == 'BUTTONDOWN': value = event['button'] @@ -643,7 +636,6 @@ class GamepadSettingsWindow(ba.Window): if btn in self._settings: del self._settings[btn] if event['hat'] == (2 if self._is_secondary else 1): - # Exclude value in default case. if 'dpad' + ext in self._settings: del self._settings['dpad' + ext] @@ -651,43 +643,41 @@ class GamepadSettingsWindow(ba.Window): self._settings['dpad' + ext] = event['hat'] # Update the 4 dpad button txt widgets. - ba.textwidget( + bui.textwidget( edit=self._textwidgets['buttonUp' + ext], text=self.get_control_value_name('buttonUp' + ext), ) - ba.textwidget( + bui.textwidget( edit=self._textwidgets['buttonLeft' + ext], text=self.get_control_value_name('buttonLeft' + ext), ) - ba.textwidget( + bui.textwidget( edit=self._textwidgets['buttonRight' + ext], text=self.get_control_value_name('buttonRight' + ext), ) - ba.textwidget( + bui.textwidget( edit=self._textwidgets['buttonDown' + ext], text=self.get_control_value_name('buttonDown' + ext), ) - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() dialog.die() elif control == 'analogStickLR' + ext: if event['type'] == 'AXISMOTION': - # Ignore small values or else we might get triggered by noise. if abs(event['value']) > 0.5: axis = event['axis'] if axis == (5 if self._is_secondary else 1): - # Exclude value in default case. if 'analogStickLR' + ext in self._settings: del self._settings['analogStickLR' + ext] else: self._settings['analogStickLR' + ext] = axis - ba.textwidget( + bui.textwidget( edit=self._textwidgets['analogStickLR' + ext], text=self.get_control_value_name('analogStickLR' + ext), ) - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() dialog.die() # Now launch the up/down listener. @@ -695,12 +685,11 @@ class GamepadSettingsWindow(ba.Window): self._input, 'analogStickUD' + ext, self._gamepad_event, - ba.Lstr(resource=self._r + '.pressUpDownText'), + bui.Lstr(resource=self._r + '.pressUpDownText'), ) elif control == 'analogStickUD' + ext: if event['type'] == 'AXISMOTION': - # Ignore small values or else we might get triggered by noise. if abs(event['value']) > 0.5: axis = event['axis'] @@ -712,19 +701,18 @@ class GamepadSettingsWindow(ba.Window): lr_axis = 5 if self._is_secondary else 1 if axis != lr_axis: if axis == (6 if self._is_secondary else 2): - # Exclude value in default case. if 'analogStickUD' + ext in self._settings: del self._settings['analogStickUD' + ext] else: self._settings['analogStickUD' + ext] = axis - ba.textwidget( + bui.textwidget( edit=self._textwidgets['analogStickLR' + ext], text=self.get_control_value_name( 'analogStickLR' + ext ), ) - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() dialog.die() else: # For other buttons we just want a button-press. @@ -733,28 +721,28 @@ class GamepadSettingsWindow(ba.Window): self._settings[control] = value # Update the button's text widget. - ba.textwidget( + bui.textwidget( edit=self._textwidgets[control], text=self.get_control_value_name(control), ) - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() dialog.die() def _capture_button( self, pos: tuple[float, float], color: tuple[float, float, float], - texture: ba.Texture, + texture: bui.Texture, button: str, scale: float = 1.0, - message: ba.Lstr | None = None, - message2: ba.Lstr | None = None, + message: bui.Lstr | None = None, + message2: bui.Lstr | None = None, maxwidth: float = 80.0, - ) -> ba.Widget: + ) -> bui.Widget: if message is None: - message = ba.Lstr(resource=self._r + '.pressAnyButtonText') + message = bui.Lstr(resource=self._r + '.pressAnyButtonText') base_size = 79 - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=( pos[0] - base_size * 0.5 * scale, @@ -771,7 +759,7 @@ class GamepadSettingsWindow(ba.Window): def doit() -> None: uiscale = 0.9 * scale - txt = ba.textwidget( + txt = bui.textwidget( parent=self._root_widget, position=(pos[0] + 0.0 * scale, pos[1] - 58.0 * scale), color=(1, 1, 1, 0.3), @@ -783,9 +771,9 @@ class GamepadSettingsWindow(ba.Window): maxwidth=maxwidth, ) self._textwidgets[button] = txt - ba.buttonwidget( + bui.buttonwidget( edit=btn, - on_activate_call=ba.Call( + on_activate_call=bui.Call( AwaitGamepadInputWindow, self._input, button, @@ -795,29 +783,26 @@ class GamepadSettingsWindow(ba.Window): ), ) - ba.timer(0, doit, timetype=ba.TimeType.REAL) + bui.apptimer(0, doit) return btn def _cancel(self) -> None: from bastd.ui.settings.controls import ControlsSettingsWindow - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) if self._is_main_menu: - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( ControlsSettingsWindow(transition='in_left').get_root_widget() ) def _save(self) -> None: - from ba.internal import ( - master_server_post, - get_input_device_config, - get_input_map_hash, - should_submit_debug_info, - ) + classic = bui.app.classic + assert classic is not None - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) @@ -828,7 +813,7 @@ class GamepadSettingsWindow(ba.Window): assert self._settings is not None if self._input: - dst = get_input_device_config(self._input, default=True) + dst = classic.get_input_device_config(self._input, default=True) dst2: dict[str, Any] = dst[0][dst[1]] dst2.clear() @@ -839,67 +824,68 @@ class GamepadSettingsWindow(ba.Window): # If we're allowed to phone home, send this config so we can # generate more defaults in the future. - inputhash = get_input_map_hash(self._input) - if should_submit_debug_info(): - master_server_post( - 'controllerConfig', - { - 'ua': ba.app.user_agent_string, - 'b': ba.app.build_number, - 'name': self._name, - 'inputMapHash': inputhash, - 'config': dst2, - 'v': 2, - }, - ) - ba.app.config.apply_and_commit() - ba.playsound(ba.getsound('gunCocking')) + inputhash = classic.get_input_device_map_hash(self._input) + classic.master_server_v1_post( + 'controllerConfig', + { + 'ua': classic.user_agent_string, + 'b': bui.app.build_number, + 'name': self._name, + 'inputMapHash': inputhash, + 'config': dst2, + 'v': 2, + }, + ) + bui.app.config.apply_and_commit() + bui.getsound('gunCocking').play() else: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() if self._is_main_menu: from bastd.ui.settings.controls import ControlsSettingsWindow - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( ControlsSettingsWindow(transition='in_left').get_root_widget() ) -class AwaitGamepadInputWindow(ba.Window): +class AwaitGamepadInputWindow(bui.Window): """Window for capturing a gamepad button press.""" def __init__( self, - gamepad: ba.InputDevice, + gamepad: bs.InputDevice, button: str, callback: Callable[[str, dict[str, Any], AwaitGamepadInputWindow], Any], - message: ba.Lstr | None = None, - message2: ba.Lstr | None = None, + message: bui.Lstr | None = None, + message2: bui.Lstr | None = None, ): if message is None: print('AwaitGamepadInputWindow message is None!') # Shouldn't get here. - message = ba.Lstr(value='Press any button...') + message = bui.Lstr(value='Press any button...') self._callback = callback self._input = gamepad self._capture_button = button width = 400 height = 150 - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( scale=( 2.0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.9 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), size=(width, height), transition='in_scale', ), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(0, (height - 60) if message2 is None else (height - 50)), size=(width, 25), @@ -909,7 +895,7 @@ class AwaitGamepadInputWindow(ba.Window): v_align='center', ) if message2 is not None: - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(width * 0.5, height - 60), size=(0, 0), @@ -921,7 +907,7 @@ class AwaitGamepadInputWindow(ba.Window): v_align='center', ) self._counter = 5 - self._count_down_text = ba.textwidget( + self._count_down_text = bui.textwidget( parent=self._root_widget, h_align='center', position=(0, height - 110), @@ -929,13 +915,10 @@ class AwaitGamepadInputWindow(ba.Window): color=(1, 1, 1, 0.3), text=str(self._counter), ) - self._decrement_timer: ba.Timer | None = ba.Timer( - 1.0, - ba.Call(self._decrement), - repeat=True, - timetype=ba.TimeType.REAL, + self._decrement_timer: bui.AppTimer | None = bui.AppTimer( + 1.0, bui.Call(self._decrement), repeat=True ) - ba.internal.capture_gamepad_input(ba.WeakCall(self._event_callback)) + bs.capture_gamepad_input(bui.WeakCall(self._event_callback)) def __del__(self) -> None: pass @@ -945,13 +928,13 @@ class AwaitGamepadInputWindow(ba.Window): # This strong-refs us; killing it allow us to die now. self._decrement_timer = None - ba.internal.release_gamepad_input() + bs.release_gamepad_input() if self._root_widget: - ba.containerwidget(edit=self._root_widget, transition='out_scale') + bui.containerwidget(edit=self._root_widget, transition='out_scale') def _event_callback(self, event: dict[str, Any]) -> None: input_device = event['input_device'] - assert isinstance(input_device, ba.InputDevice) + assert isinstance(input_device, bs.InputDevice) # Update - we now allow *any* input device of this type. if ( @@ -965,9 +948,9 @@ class AwaitGamepadInputWindow(ba.Window): self._counter -= 1 if self._counter >= 1: if self._count_down_text: - ba.textwidget( + bui.textwidget( edit=self._count_down_text, text=str(self._counter) ) else: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() self.die() diff --git a/assets/src/ba_data/python/bastd/ui/settings/gamepadadvanced.py b/src/assets/ba_data/python/bastd/ui/settings/gamepadadvanced.py similarity index 74% rename from assets/src/ba_data/python/bastd/ui/settings/gamepadadvanced.py rename to src/assets/ba_data/python/bastd/ui/settings/gamepadadvanced.py index 73265887..7042fa2c 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/gamepadadvanced.py +++ b/src/assets/ba_data/python/bastd/ui/settings/gamepadadvanced.py @@ -6,76 +6,80 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba +import bauiv1 as bui if TYPE_CHECKING: from typing import Any - from bastd.ui.settings import gamepad as gpsui + from bastd.ui.settings.gamepad import ( + GamepadSettingsWindow, + AwaitGamepadInputWindow, + ) -class GamepadAdvancedSettingsWindow(ba.Window): +class GamepadAdvancedSettingsWindow(bui.Window): """Window for advanced gamepad configuration.""" - def __init__(self, parent_window: gpsui.GamepadSettingsWindow): + def __init__(self, parent_window: GamepadSettingsWindow): # pylint: disable=too-many-statements # pylint: disable=too-many-locals self._parent_window = parent_window - app = ba.app + app = bui.app self._r = parent_window.get_r() - uiscale = ba.app.ui.uiscale - self._width = 900 if uiscale is ba.UIScale.SMALL else 700 - self._x_inset = x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 - self._height = 402 if uiscale is ba.UIScale.SMALL else 512 - self._textwidgets: dict[str, ba.Widget] = {} + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self._width = 900 if uiscale is bui.UIScale.SMALL else 700 + self._x_inset = x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 + self._height = 402 if uiscale is bui.UIScale.SMALL else 512 + self._textwidgets: dict[str, bui.Widget] = {} advb = parent_window.get_advanced_button() super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( transition='in_scale', size=(self._width, self._height), scale=1.06 * ( 1.85 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.35 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -25) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), scale_origin_stack_offset=(advb.get_screen_space_center()), ) ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=( self._width * 0.5, - self._height - (40 if uiscale is ba.UIScale.SMALL else 34), + self._height - (40 if uiscale is bui.UIScale.SMALL else 34), ), size=(0, 0), - text=ba.Lstr(resource=self._r + '.advancedTitleText'), + text=bui.Lstr(resource=self._r + '.advancedTitleText'), maxwidth=320, - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, h_align='center', v_align='center', ) - back_button = btn = ba.buttonwidget( + back_button = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=( self._width - (176 + x_inset), - self._height - (60 if uiscale is ba.UIScale.SMALL else 55), + self._height - (60 if uiscale is bui.UIScale.SMALL else 55), ), size=(120, 48), text_scale=0.8, - label=ba.Lstr(resource='doneText'), + label=bui.Lstr(resource='doneText'), on_activate_call=self._done, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, start_button=btn, on_cancel_call=btn.activate, @@ -89,7 +93,7 @@ class GamepadAdvancedSettingsWindow(ba.Window): ) if app.vr_mode: self._sub_height += 50 - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, position=( (self._width - self._scroll_width) * 0.5, @@ -100,7 +104,7 @@ class GamepadAdvancedSettingsWindow(ba.Window): claims_tab=True, selection_loops_to_parent=True, ) - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._sub_width, self._sub_height), background=False, @@ -108,7 +112,7 @@ class GamepadAdvancedSettingsWindow(ba.Window): claims_tab=True, selection_loops_to_parent=True, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) @@ -120,11 +124,11 @@ class GamepadAdvancedSettingsWindow(ba.Window): # don't allow secondary joysticks to handle unassigned buttons if not self._parent_window.get_is_secondary(): v -= 40 - cb1 = ba.checkboxwidget( + cb1 = bui.checkboxwidget( parent=self._subcontainer, position=(h + 70, v), size=(500, 30), - text=ba.Lstr(resource=self._r + '.unassignedButtonsRunText'), + text=bui.Lstr(resource=self._r + '.unassignedButtonsRunText'), textcolor=(0.8, 0.8, 0.8), maxwidth=330, scale=1.0, @@ -134,27 +138,27 @@ class GamepadAdvancedSettingsWindow(ba.Window): autoselect=True, value=self._parent_window.get_unassigned_buttons_run_value(), ) - ba.widget(edit=cb1, up_widget=back_button) + bui.widget(edit=cb1, up_widget=back_button) v -= 60 capb = self._capture_button( pos=(h2, v), - name=ba.Lstr(resource=self._r + '.runButton1Text'), + name=bui.Lstr(resource=self._r + '.runButton1Text'), control='buttonRun1' + self._parent_window.get_ext(), ) if self._parent_window.get_is_secondary(): for widget in capb: - ba.widget(edit=widget, up_widget=back_button) + bui.widget(edit=widget, up_widget=back_button) v -= 42 self._capture_button( pos=(h2, v), - name=ba.Lstr(resource=self._r + '.runButton2Text'), + name=bui.Lstr(resource=self._r + '.runButton2Text'), control='buttonRun2' + self._parent_window.get_ext(), ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v - 24), size=(0, 0), - text=ba.Lstr(resource=self._r + '.runTriggerDescriptionText'), + text=bui.Lstr(resource=self._r + '.runTriggerDescriptionText'), color=(0.7, 1, 0.7, 0.6), maxwidth=self._sub_width * 0.8, scale=0.7, @@ -166,16 +170,16 @@ class GamepadAdvancedSettingsWindow(ba.Window): self._capture_button( pos=(h2, v), - name=ba.Lstr(resource=self._r + '.runTrigger1Text'), + name=bui.Lstr(resource=self._r + '.runTrigger1Text'), control='triggerRun1' + self._parent_window.get_ext(), - message=ba.Lstr(resource=self._r + '.pressAnyAnalogTriggerText'), + message=bui.Lstr(resource=self._r + '.pressAnyAnalogTriggerText'), ) v -= 42 self._capture_button( pos=(h2, v), - name=ba.Lstr(resource=self._r + '.runTrigger2Text'), + name=bui.Lstr(resource=self._r + '.runTrigger2Text'), control='triggerRun2' + self._parent_window.get_ext(), - message=ba.Lstr(resource=self._r + '.pressAnyAnalogTriggerText'), + message=bui.Lstr(resource=self._r + '.pressAnyAnalogTriggerText'), ) # in vr mode, allow assigning a reset-view button @@ -183,45 +187,45 @@ class GamepadAdvancedSettingsWindow(ba.Window): v -= 50 self._capture_button( pos=(h2, v), - name=ba.Lstr(resource=self._r + '.vrReorientButtonText'), + name=bui.Lstr(resource=self._r + '.vrReorientButtonText'), control='buttonVRReorient' + self._parent_window.get_ext(), ) v -= 60 self._capture_button( pos=(h2, v), - name=ba.Lstr(resource=self._r + '.extraStartButtonText'), + name=bui.Lstr(resource=self._r + '.extraStartButtonText'), control='buttonStart2' + self._parent_window.get_ext(), ) v -= 60 self._capture_button( pos=(h2, v), - name=ba.Lstr(resource=self._r + '.ignoredButton1Text'), + name=bui.Lstr(resource=self._r + '.ignoredButton1Text'), control='buttonIgnored' + self._parent_window.get_ext(), ) v -= 42 self._capture_button( pos=(h2, v), - name=ba.Lstr(resource=self._r + '.ignoredButton2Text'), + name=bui.Lstr(resource=self._r + '.ignoredButton2Text'), control='buttonIgnored2' + self._parent_window.get_ext(), ) v -= 42 self._capture_button( pos=(h2, v), - name=ba.Lstr(resource=self._r + '.ignoredButton3Text'), + name=bui.Lstr(resource=self._r + '.ignoredButton3Text'), control='buttonIgnored3' + self._parent_window.get_ext(), ) v -= 42 self._capture_button( pos=(h2, v), - name=ba.Lstr(resource=self._r + '.ignoredButton4Text'), + name=bui.Lstr(resource=self._r + '.ignoredButton4Text'), control='buttonIgnored4' + self._parent_window.get_ext(), ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v - 14), size=(0, 0), - text=ba.Lstr(resource=self._r + '.ignoredButtonDescriptionText'), + text=bui.Lstr(resource=self._r + '.ignoredButtonDescriptionText'), color=(0.7, 1, 0.7, 0.6), scale=0.8, maxwidth=self._sub_width * 0.8, @@ -231,12 +235,14 @@ class GamepadAdvancedSettingsWindow(ba.Window): v -= 80 pwin = self._parent_window - ba.checkboxwidget( + bui.checkboxwidget( parent=self._subcontainer, autoselect=True, position=(h + 50, v), size=(400, 30), - text=ba.Lstr(resource=self._r + '.startButtonActivatesDefaultText'), + text=bui.Lstr( + resource=self._r + '.startButtonActivatesDefaultText' + ), textcolor=(0.8, 0.8, 0.8), maxwidth=450, scale=0.9, @@ -245,11 +251,11 @@ class GamepadAdvancedSettingsWindow(ba.Window): ), value=pwin.get_start_button_activates_default_widget_value(), ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v - 12), size=(0, 0), - text=ba.Lstr( + text=bui.Lstr( resource=self._r + '.startButtonActivatesDefaultDescriptionText' ), color=(0.7, 1, 0.7, 0.6), @@ -260,23 +266,23 @@ class GamepadAdvancedSettingsWindow(ba.Window): ) v -= 80 - ba.checkboxwidget( + bui.checkboxwidget( parent=self._subcontainer, autoselect=True, position=(h + 50, v), size=(400, 30), - text=ba.Lstr(resource=self._r + '.uiOnlyText'), + text=bui.Lstr(resource=self._r + '.uiOnlyText'), textcolor=(0.8, 0.8, 0.8), maxwidth=450, scale=0.9, on_value_change_call=self._parent_window.set_ui_only_value, value=self._parent_window.get_ui_only_value(), ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v - 12), size=(0, 0), - text=ba.Lstr(resource=self._r + '.uiOnlyDescriptionText'), + text=bui.Lstr(resource=self._r + '.uiOnlyDescriptionText'), color=(0.7, 1, 0.7, 0.6), maxwidth=self._sub_width * 0.8, scale=0.7, @@ -285,23 +291,25 @@ class GamepadAdvancedSettingsWindow(ba.Window): ) v -= 80 - ba.checkboxwidget( + bui.checkboxwidget( parent=self._subcontainer, autoselect=True, position=(h + 50, v), size=(400, 30), - text=ba.Lstr(resource=self._r + '.ignoreCompletelyText'), + text=bui.Lstr(resource=self._r + '.ignoreCompletelyText'), textcolor=(0.8, 0.8, 0.8), maxwidth=450, scale=0.9, on_value_change_call=pwin.set_ignore_completely_value, value=self._parent_window.get_ignore_completely_value(), ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v - 12), size=(0, 0), - text=ba.Lstr(resource=self._r + '.ignoreCompletelyDescriptionText'), + text=bui.Lstr( + resource=self._r + '.ignoreCompletelyDescriptionText' + ), color=(0.7, 1, 0.7, 0.6), maxwidth=self._sub_width * 0.8, scale=0.7, @@ -311,23 +319,23 @@ class GamepadAdvancedSettingsWindow(ba.Window): v -= 80 - cb1 = ba.checkboxwidget( + cb1 = bui.checkboxwidget( parent=self._subcontainer, autoselect=True, position=(h + 50, v), size=(400, 30), - text=ba.Lstr(resource=self._r + '.autoRecalibrateText'), + text=bui.Lstr(resource=self._r + '.autoRecalibrateText'), textcolor=(0.8, 0.8, 0.8), maxwidth=450, scale=0.9, on_value_change_call=pwin.set_auto_recalibrate_analog_stick_value, value=self._parent_window.get_auto_recalibrate_analog_stick_value(), ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v - 12), size=(0, 0), - text=ba.Lstr(resource=self._r + '.autoRecalibrateDescriptionText'), + text=bui.Lstr(resource=self._r + '.autoRecalibrateDescriptionText'), color=(0.7, 1, 0.7, 0.6), maxwidth=self._sub_width * 0.8, scale=0.7, @@ -337,7 +345,7 @@ class GamepadAdvancedSettingsWindow(ba.Window): v -= 80 buttons = self._config_value_editor( - ba.Lstr(resource=self._r + '.analogStickDeadZoneText'), + bui.Lstr(resource=self._r + '.analogStickDeadZoneText'), control=('analogStickDeadZone' + self._parent_window.get_ext()), position=(h + 40, v), min_val=0, @@ -345,14 +353,14 @@ class GamepadAdvancedSettingsWindow(ba.Window): increment=0.1, x_offset=100, ) - ba.widget(edit=buttons[0], left_widget=cb1, up_widget=cb1) - ba.widget(edit=cb1, right_widget=buttons[0], down_widget=buttons[0]) + bui.widget(edit=buttons[0], left_widget=cb1, up_widget=cb1) + bui.widget(edit=cb1, right_widget=buttons[0], down_widget=buttons[0]) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v - 12), size=(0, 0), - text=ba.Lstr( + text=bui.Lstr( resource=self._r + '.analogStickDeadZoneDescriptionText' ), color=(0.7, 1, 0.7, 0.6), @@ -366,10 +374,10 @@ class GamepadAdvancedSettingsWindow(ba.Window): # child joysticks cant have child joysticks.. that's just # crazy talk if not self._parent_window.get_is_secondary(): - ba.buttonwidget( + bui.buttonwidget( parent=self._subcontainer, autoselect=True, - label=ba.Lstr(resource=self._r + '.twoInOneSetupText'), + label=bui.Lstr(resource=self._r + '.twoInOneSetupText'), position=(40, v), size=(self._sub_width - 80, 50), on_activate_call=self._parent_window.show_secondary_editor, @@ -380,20 +388,20 @@ class GamepadAdvancedSettingsWindow(ba.Window): # so we can see the text below them when navigating with # a gamepad for child in self._subcontainer.get_children(): - ba.widget(edit=child, show_buffer_bottom=30, show_buffer_top=30) + bui.widget(edit=child, show_buffer_bottom=30, show_buffer_top=30) def _capture_button( self, pos: tuple[float, float], - name: ba.Lstr, + name: bui.Lstr, control: str, - message: ba.Lstr | None = None, - ) -> tuple[ba.Widget, ba.Widget]: + message: bui.Lstr | None = None, + ) -> tuple[bui.Widget, bui.Widget]: if message is None: - message = ba.Lstr( + message = bui.Lstr( resource=self._parent_window.get_r() + '.pressAnyButtonText' ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._subcontainer, autoselect=True, position=(pos[0], pos[1]), @@ -401,27 +409,27 @@ class GamepadAdvancedSettingsWindow(ba.Window): size=(250, 60), scale=0.7, ) - btn2 = ba.buttonwidget( + btn2 = bui.buttonwidget( parent=self._subcontainer, autoselect=True, position=(pos[0] + 400, pos[1] + 2), left_widget=btn, color=(0.45, 0.4, 0.5), textcolor=(0.65, 0.6, 0.7), - label=ba.Lstr(resource=self._r + '.clearText'), + label=bui.Lstr(resource=self._r + '.clearText'), size=(110, 50), scale=0.7, - on_activate_call=ba.Call(self._clear_control, control), + on_activate_call=bui.Call(self._clear_control, control), ) - ba.widget(edit=btn, right_widget=btn2) + bui.widget(edit=btn, right_widget=btn2) # make this in a timer so that it shows up on top of all # other buttons def doit() -> None: - from bastd.ui.settings import gamepad + from bastd.ui.settings.gamepad import AwaitGamepadInputWindow - txt = ba.textwidget( + txt = bui.textwidget( parent=self._subcontainer, position=(pos[0] + 285, pos[1] + 20), color=(1, 1, 1, 0.3), @@ -433,10 +441,10 @@ class GamepadAdvancedSettingsWindow(ba.Window): maxwidth=200, ) self._textwidgets[control] = txt - ba.buttonwidget( + bui.buttonwidget( edit=btn, - on_activate_call=ba.Call( - gamepad.AwaitGamepadInputWindow, + on_activate_call=bui.Call( + AwaitGamepadInputWindow, self._parent_window.get_input(), control, self._gamepad_event, @@ -444,7 +452,7 @@ class GamepadAdvancedSettingsWindow(ba.Window): ), ) - ba.timer(0, doit, timetype=ba.TimeType.REAL) + bui.apptimer(0, doit) return btn, btn2 def _inc( @@ -457,14 +465,14 @@ class GamepadAdvancedSettingsWindow(ba.Window): del self._parent_window.get_settings()[control] else: self._parent_window.get_settings()[control] = round(val, 1) - ba.textwidget( + bui.textwidget( edit=self._textwidgets[control], text=self._parent_window.get_control_value_name(control), ) def _config_value_editor( self, - name: ba.Lstr, + name: bui.Lstr, control: str, position: tuple[float, float], min_val: float = 0.0, @@ -472,12 +480,11 @@ class GamepadAdvancedSettingsWindow(ba.Window): increment: float = 1.0, change_sound: bool = True, x_offset: float = 0.0, - displayname: ba.Lstr | None = None, - ) -> tuple[ba.Widget, ba.Widget]: - + displayname: bui.Lstr | None = None, + ) -> tuple[bui.Widget, bui.Widget]: if displayname is None: displayname = name - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=position, size=(100, 30), @@ -488,7 +495,7 @@ class GamepadAdvancedSettingsWindow(ba.Window): scale=1.0, maxwidth=280, ) - self._textwidgets[control] = ba.textwidget( + self._textwidgets[control] = bui.textwidget( parent=self._subcontainer, position=(246.0 + x_offset, position[1]), size=(60, 28), @@ -499,25 +506,25 @@ class GamepadAdvancedSettingsWindow(ba.Window): text=self._parent_window.get_control_value_name(control), padding=2, ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._subcontainer, autoselect=True, position=(330 + x_offset, position[1] + 4), size=(28, 28), label='-', - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._inc, control, min_val, max_val, -increment ), repeat=True, enable_sound=(change_sound is True), ) - btn2 = ba.buttonwidget( + btn2 = bui.buttonwidget( parent=self._subcontainer, autoselect=True, position=(380 + x_offset, position[1] + 4), size=(28, 28), label='+', - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._inc, control, min_val, max_val, increment ), repeat=True, @@ -528,7 +535,7 @@ class GamepadAdvancedSettingsWindow(ba.Window): def _clear_control(self, control: str) -> None: if control in self._parent_window.get_settings(): del self._parent_window.get_settings()[control] - ba.textwidget( + bui.textwidget( edit=self._textwidgets[control], text=self._parent_window.get_control_value_name(control), ) @@ -537,7 +544,7 @@ class GamepadAdvancedSettingsWindow(ba.Window): self, control: str, event: dict[str, Any], - dialog: gpsui.AwaitGamepadInputWindow, + dialog: AwaitGamepadInputWindow, ) -> None: ext = self._parent_window.get_ext() if control in ['triggerRun1' + ext, 'triggerRun2' + ext]: @@ -548,13 +555,13 @@ class GamepadAdvancedSettingsWindow(ba.Window): self._parent_window.get_settings()[control] = event['axis'] # update the button's text widget if self._textwidgets[control]: - ba.textwidget( + bui.textwidget( edit=self._textwidgets[control], text=self._parent_window.get_control_value_name( control ), ) - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() dialog.die() else: if event['type'] == 'BUTTONDOWN': @@ -562,14 +569,14 @@ class GamepadAdvancedSettingsWindow(ba.Window): self._parent_window.get_settings()[control] = value # update the button's text widget if self._textwidgets[control]: - ba.textwidget( + bui.textwidget( edit=self._textwidgets[control], text=self._parent_window.get_control_value_name( control ), ) - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() dialog.die() def _done(self) -> None: - ba.containerwidget(edit=self._root_widget, transition='out_scale') + bui.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/assets/src/ba_data/python/bastd/ui/settings/gamepadselect.py b/src/assets/ba_data/python/bastd/ui/settings/gamepadselect.py similarity index 62% rename from assets/src/ba_data/python/bastd/ui/settings/gamepadselect.py rename to src/assets/ba_data/python/bastd/ui/settings/gamepadselect.py index c719985b..c1afe8f8 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/gamepadselect.py +++ b/src/assets/ba_data/python/bastd/ui/settings/gamepadselect.py @@ -4,10 +4,11 @@ from __future__ import annotations +import logging from typing import TYPE_CHECKING -import ba -import ba.internal +import bascenev1 as bs +import bauiv1 as bui if TYPE_CHECKING: from typing import Any @@ -15,54 +16,54 @@ if TYPE_CHECKING: def gamepad_configure_callback(event: dict[str, Any]) -> None: """Respond to a gamepad button press during config selection.""" - from ba.internal import get_remote_app_name from bastd.ui.settings import gamepad # Ignore all but button-presses. if event['type'] not in ['BUTTONDOWN', 'HATMOTION']: return - ba.internal.release_gamepad_input() + bs.release_gamepad_input() + assert bui.app.classic is not None try: - ba.app.ui.clear_main_menu_window(transition='out_left') + bui.app.classic.ui.clear_main_menu_window(transition='out_left') except Exception: - ba.print_exception('Error transitioning out main_menu_window.') - ba.playsound(ba.getsound('activateBeep')) - ba.playsound(ba.getsound('swish')) + logging.exception('Error transitioning out main_menu_window.') + bui.getsound('activateBeep').play() + bui.getsound('swish').play() inputdevice = event['input_device'] - assert isinstance(inputdevice, ba.InputDevice) + assert isinstance(inputdevice, bs.InputDevice) if inputdevice.allows_configuring: - ba.app.ui.set_main_menu_window( + bui.app.classic.ui.set_main_menu_window( gamepad.GamepadSettingsWindow(inputdevice).get_root_widget() ) else: width = 700 height = 200 button_width = 100 - uiscale = ba.app.ui.uiscale - dlg = ba.containerwidget( + uiscale = bui.app.classic.ui.uiscale + dlg = bui.containerwidget( scale=( 1.7 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.4 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), size=(width, height), transition='in_right', ) - ba.app.ui.set_main_menu_window(dlg) + bui.app.classic.ui.set_main_menu_window(dlg) device_name = inputdevice.name if device_name == 'iDevice': - msg = ba.Lstr( + msg = bui.Lstr( resource='bsRemoteConfigureInAppText', - subs=[('${REMOTE_APP_NAME}', get_remote_app_name())], + subs=[('${REMOTE_APP_NAME}', bui.get_remote_app_name())], ) else: - msg = ba.Lstr( + msg = bui.Lstr( resource='cantConfigureDeviceText', subs=[('${DEVICE}', device_name)], ) - ba.textwidget( + bui.textwidget( parent=dlg, position=(0, height - 80), size=(width, 25), @@ -75,23 +76,24 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None: def _ok() -> None: from bastd.ui.settings import controls - ba.containerwidget(edit=dlg, transition='out_right') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=dlg, transition='out_right') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( controls.ControlsSettingsWindow( transition='in_left' ).get_root_widget() ) - ba.buttonwidget( + bui.buttonwidget( parent=dlg, position=((width - button_width) / 2, 20), size=(button_width, 60), - label=ba.Lstr(resource='okText'), + label=bui.Lstr(resource='okText'), on_activate_call=_ok, ) -class GamepadSelectWindow(ba.Window): +class GamepadSelectWindow(bui.Window): """Window for selecting a gamepad to configure.""" def __init__(self) -> None: @@ -102,14 +104,15 @@ class GamepadSelectWindow(ba.Window): spacing = 40 self._r = 'configGamepadSelectWindow' - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( scale=( 2.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.5 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), size=(width, height), @@ -117,75 +120,76 @@ class GamepadSelectWindow(ba.Window): ) ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=(20, height - 60), size=(130, 60), - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', scale=0.8, on_activate_call=self._back, ) # Let's not have anything selected by default; its misleading looking # for the controller getting configured. - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=btn, - selected_child=cast(ba.Widget, 0), + selected_child=cast(bui.Widget, 0), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(20, height - 50), size=(width, 25), - text=ba.Lstr(resource=self._r + '.titleText'), + text=bui.Lstr(resource=self._r + '.titleText'), maxwidth=250, - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, h_align='center', v_align='center', ) - ba.buttonwidget( + bui.buttonwidget( edit=btn, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) v: float = height - 60 v -= spacing - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(15, v), size=(width - 30, 30), scale=0.8, - text=ba.Lstr(resource=self._r + '.pressAnyButtonText'), + text=bui.Lstr(resource=self._r + '.pressAnyButtonText'), maxwidth=width * 0.95, - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, h_align='center', v_align='top', ) v -= spacing * 1.24 - if ba.app.platform == 'android': - ba.textwidget( + if bui.app.classic.platform == 'android': + bui.textwidget( parent=self._root_widget, position=(15, v), size=(width - 30, 30), scale=0.46, - text=ba.Lstr(resource=self._r + '.androidNoteText'), + text=bui.Lstr(resource=self._r + '.androidNoteText'), maxwidth=width * 0.95, color=(0.7, 0.9, 0.7, 0.5), h_align='center', v_align='top', ) - ba.internal.capture_gamepad_input(gamepad_configure_callback) + bs.capture_gamepad_input(gamepad_configure_callback) def _back(self) -> None: from bastd.ui.settings import controls - ba.internal.release_gamepad_input() - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.app.ui.set_main_menu_window( + bs.release_gamepad_input() + bui.containerwidget(edit=self._root_widget, transition='out_right') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( controls.ControlsSettingsWindow( transition='in_left' ).get_root_widget() diff --git a/assets/src/ba_data/python/bastd/ui/settings/graphics.py b/src/assets/ba_data/python/bastd/ui/settings/graphics.py similarity index 68% rename from assets/src/ba_data/python/bastd/ui/settings/graphics.py rename to src/assets/ba_data/python/bastd/ui/settings/graphics.py index a771f6e0..e58f647d 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/graphics.py +++ b/src/assets/ba_data/python/bastd/ui/settings/graphics.py @@ -4,28 +4,24 @@ from __future__ import annotations -from typing import TYPE_CHECKING +import logging -import ba -import ba.internal - -if TYPE_CHECKING: - pass +from bastd.ui.popup import PopupMenu +from bastd.ui.config import ConfigCheckBox, ConfigNumberEdit +import bauiv1 as bui -class GraphicsSettingsWindow(ba.Window): +class GraphicsSettingsWindow(bui.Window): """Window for graphics settings.""" def __init__( self, transition: str = 'in_right', - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements - from bastd.ui import popup - from bastd.ui.config import ConfigCheckBox, ConfigNumberEdit # if they provided an origin-widget, scale up from that scale_origin: tuple[float, float] | None @@ -38,92 +34,96 @@ class GraphicsSettingsWindow(ba.Window): scale_origin = None self._r = 'graphicsSettingsWindow' - app = ba.app + app = bui.app + assert app.classic is not None spacing = 32 self._have_selected_child = False - uiscale = app.ui.uiscale + uiscale = app.classic.ui.uiscale width = 450.0 height = 302.0 self._show_fullscreen = False fullscreen_spacing_top = spacing * 0.2 fullscreen_spacing = spacing * 1.2 - if uiscale == ba.UIScale.LARGE and app.platform != 'android': + if uiscale == bui.UIScale.LARGE and app.classic.platform != 'android': self._show_fullscreen = True height += fullscreen_spacing + fullscreen_spacing_top show_gamma = False gamma_spacing = spacing * 1.3 - if ba.internal.has_gamma_control(): + if bui.has_gamma_control(): show_gamma = True height += gamma_spacing show_vsync = False - if app.platform == 'mac': + if app.classic.platform == 'mac': show_vsync = True show_resolution = True if app.vr_mode: show_resolution = ( - app.platform == 'android' and app.subplatform == 'cardboard' + app.classic.platform == 'android' + and app.classic.subplatform == 'cardboard' ) - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale base_scale = ( 2.4 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.5 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ) popup_menu_scale = base_scale * 1.2 v = height - 50 v -= spacing * 1.15 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height), transition=transition, scale_origin_stack_offset=scale_origin, scale=base_scale, stack_offset=(0, -30) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=(35, height - 50), size=(120, 60), scale=0.8, text_scale=1.2, autoselect=True, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', on_activate_call=self._back, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(0, height - 44), size=(width, 25), - text=ba.Lstr(resource=self._r + '.titleText'), - color=ba.app.ui.title_color, + text=bui.Lstr(resource=self._r + '.titleText'), + color=bui.app.classic.ui.title_color, h_align='center', v_align='top', ) - ba.buttonwidget( + bui.buttonwidget( edit=btn, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) - self._fullscreen_checkbox: ba.Widget | None + self._fullscreen_checkbox: bui.Widget | None = None + self._gamma_controls: ConfigNumberEdit | None = None if self._show_fullscreen: v -= fullscreen_spacing_top self._fullscreen_checkbox = ConfigCheckBox( @@ -132,118 +132,113 @@ class GraphicsSettingsWindow(ba.Window): maxwidth=200, size=(300, 30), configkey='Fullscreen', - displayname=ba.Lstr( + displayname=bui.Lstr( resource=self._r + ( '.fullScreenCmdText' - if app.platform == 'mac' + if app.classic.platform == 'mac' else '.fullScreenCtrlText' ) ), ).widget if not self._have_selected_child: - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._fullscreen_checkbox, ) self._have_selected_child = True v -= fullscreen_spacing - else: - self._fullscreen_checkbox = None - self._gamma_controls: ConfigNumberEdit | None if show_gamma: self._gamma_controls = gmc = ConfigNumberEdit( parent=self._root_widget, position=(90, v), configkey='Screen Gamma', - displayname=ba.Lstr(resource=self._r + '.gammaText'), + displayname=bui.Lstr(resource=self._r + '.gammaText'), minval=0.1, maxval=2.0, increment=0.1, xoffset=-70, textscale=0.85, ) - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=gmc.plusbutton, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) if not self._have_selected_child: - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=gmc.minusbutton ) self._have_selected_child = True v -= gamma_spacing - else: - self._gamma_controls = None self._selected_color = (0.5, 1, 0.5, 1) self._unselected_color = (0.7, 0.7, 0.7, 1) # quality - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(60, v), size=(160, 25), - text=ba.Lstr(resource=self._r + '.visualsText'), - color=ba.app.ui.heading_color, + text=bui.Lstr(resource=self._r + '.visualsText'), + color=bui.app.classic.ui.heading_color, scale=0.65, maxwidth=150, h_align='center', v_align='center', ) - popup.PopupMenu( + PopupMenu( parent=self._root_widget, position=(60, v - 50), width=150, scale=popup_menu_scale, choices=['Auto', 'Higher', 'High', 'Medium', 'Low'], choices_disabled=['Higher', 'High'] - if ba.internal.get_max_graphics_quality() == 'Medium' + if bui.get_max_graphics_quality() == 'Medium' else [], choices_display=[ - ba.Lstr(resource='autoText'), - ba.Lstr(resource=self._r + '.higherText'), - ba.Lstr(resource=self._r + '.highText'), - ba.Lstr(resource=self._r + '.mediumText'), - ba.Lstr(resource=self._r + '.lowText'), + bui.Lstr(resource='autoText'), + bui.Lstr(resource=self._r + '.higherText'), + bui.Lstr(resource=self._r + '.highText'), + bui.Lstr(resource=self._r + '.mediumText'), + bui.Lstr(resource=self._r + '.lowText'), ], - current_choice=ba.app.config.resolve('Graphics Quality'), + current_choice=bui.app.config.resolve('Graphics Quality'), on_value_change_call=self._set_quality, ) # texture controls - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(230, v), size=(160, 25), - text=ba.Lstr(resource=self._r + '.texturesText'), - color=ba.app.ui.heading_color, + text=bui.Lstr(resource=self._r + '.texturesText'), + color=bui.app.classic.ui.heading_color, scale=0.65, maxwidth=150, h_align='center', v_align='center', ) - textures_popup = popup.PopupMenu( + textures_popup = PopupMenu( parent=self._root_widget, position=(230, v - 50), width=150, scale=popup_menu_scale, choices=['Auto', 'High', 'Medium', 'Low'], choices_display=[ - ba.Lstr(resource='autoText'), - ba.Lstr(resource=self._r + '.highText'), - ba.Lstr(resource=self._r + '.mediumText'), - ba.Lstr(resource=self._r + '.lowText'), + bui.Lstr(resource='autoText'), + bui.Lstr(resource=self._r + '.highText'), + bui.Lstr(resource=self._r + '.mediumText'), + bui.Lstr(resource=self._r + '.lowText'), ], - current_choice=ba.app.config.resolve('Texture Quality'), + current_choice=bui.app.config.resolve('Texture Quality'), on_value_change_call=self._set_textures, ) - if ba.app.ui.use_toolbars: - ba.widget( + if bui.app.classic.ui.use_toolbars: + bui.widget( edit=textures_popup.get_button(), - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) v -= 80 @@ -251,12 +246,12 @@ class GraphicsSettingsWindow(ba.Window): if show_resolution: # resolution - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(h_offs + 60, v), size=(160, 25), - text=ba.Lstr(resource=self._r + '.resolutionText'), - color=ba.app.ui.heading_color, + text=bui.Lstr(resource=self._r + '.resolutionText'), + color=bui.app.classic.ui.heading_color, scale=0.65, maxwidth=150, h_align='center', @@ -265,10 +260,10 @@ class GraphicsSettingsWindow(ba.Window): # on standard android we have 'Auto', 'Native', and a few # HD standards - if app.platform == 'android': + if app.classic.platform == 'android': # on cardboard/daydream android we have a few # render-target-scale options - if app.subplatform == 'cardboard': + if app.classic.subplatform == 'cardboard': current_res_cardboard = ( str( min( @@ -277,7 +272,7 @@ class GraphicsSettingsWindow(ba.Window): 10, int( round( - ba.app.config.resolve( + bui.app.config.resolve( 'GVR Render Target Scale' ) * 100.0 @@ -287,8 +282,8 @@ class GraphicsSettingsWindow(ba.Window): ) ) + '%' - ) # yapf: disable - popup.PopupMenu( + ) + PopupMenu( parent=self._root_widget, position=(h_offs + 60, v - 50), width=120, @@ -298,12 +293,12 @@ class GraphicsSettingsWindow(ba.Window): on_value_change_call=self._set_gvr_render_target_scale, ) else: - native_res = ba.internal.get_display_resolution() + native_res = bui.get_display_resolution() assert native_res is not None choices = ['Auto', 'Native'] choices_display = [ - ba.Lstr(resource='autoText'), - ba.Lstr(resource='nativeText'), + bui.Lstr(resource='autoText'), + bui.Lstr(resource='nativeText'), ] for res in [1440, 1080, 960, 720, 480]: # nav bar is 72px so lets allow for that in what @@ -311,11 +306,11 @@ class GraphicsSettingsWindow(ba.Window): if native_res[1] >= res - 72: res_str = str(res) + 'p' choices.append(res_str) - choices_display.append(ba.Lstr(value=res_str)) - current_res_android = ba.app.config.resolve( + choices_display.append(bui.Lstr(value=res_str)) + current_res_android = bui.app.config.resolve( 'Resolution (Android)' ) - popup.PopupMenu( + PopupMenu( parent=self._root_widget, position=(h_offs + 60, v - 50), width=120, @@ -326,9 +321,9 @@ class GraphicsSettingsWindow(ba.Window): on_value_change_call=self._set_android_res, ) else: - # if we're on a system that doesn't allow setting resolution, - # set pixel-scale instead - current_res = ba.internal.get_display_resolution() + # If we're on a system that doesn't allow setting resolution, + # set pixel-scale instead. + current_res = bui.get_display_resolution() if current_res is None: current_res2 = ( str( @@ -338,7 +333,7 @@ class GraphicsSettingsWindow(ba.Window): 10, int( round( - ba.app.config.resolve( + bui.app.config.resolve( 'Screen Pixel Scale' ) * 100.0 @@ -348,8 +343,8 @@ class GraphicsSettingsWindow(ba.Window): ) ) + '%' - ) # yapf: disable - popup.PopupMenu( + ) + PopupMenu( parent=self._root_widget, position=(h_offs + 60, v - 50), width=120, @@ -359,37 +354,37 @@ class GraphicsSettingsWindow(ba.Window): on_value_change_call=self._set_pixel_scale, ) else: - raise Exception( + raise RuntimeError( 'obsolete path; discrete resolutions' ' no longer supported' ) # vsync if show_vsync: - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(230, v), size=(160, 25), - text=ba.Lstr(resource=self._r + '.verticalSyncText'), - color=ba.app.ui.heading_color, + text=bui.Lstr(resource=self._r + '.verticalSyncText'), + color=bui.app.classic.ui.heading_color, scale=0.65, maxwidth=150, h_align='center', v_align='center', ) - popup.PopupMenu( + PopupMenu( parent=self._root_widget, position=(230, v - 50), width=150, scale=popup_menu_scale, choices=['Auto', 'Always', 'Never'], choices_display=[ - ba.Lstr(resource='autoText'), - ba.Lstr(resource=self._r + '.alwaysText'), - ba.Lstr(resource=self._r + '.neverText'), + bui.Lstr(resource='autoText'), + bui.Lstr(resource=self._r + '.alwaysText'), + bui.Lstr(resource=self._r + '.neverText'), ], - current_choice=ba.app.config.resolve('Vertical Sync'), + current_choice=bui.app.config.resolve('Vertical Sync'), on_value_change_call=self._set_vsync, ) @@ -400,85 +395,83 @@ class GraphicsSettingsWindow(ba.Window): size=(210, 30), scale=0.86, configkey='Show FPS', - displayname=ba.Lstr(resource=self._r + '.showFPSText'), + displayname=bui.Lstr(resource=self._r + '.showFPSText'), maxwidth=130, ) # (tv mode doesnt apply to vr) - if not ba.app.vr_mode: + if not bui.app.vr_mode: tvc = ConfigCheckBox( parent=self._root_widget, position=(240, v - 6), size=(210, 30), scale=0.86, configkey='TV Border', - displayname=ba.Lstr(resource=self._r + '.tvBorderText'), + displayname=bui.Lstr(resource=self._r + '.tvBorderText'), maxwidth=130, ) # grumble.. - ba.widget(edit=fpsc.widget, right_widget=tvc.widget) + bui.widget(edit=fpsc.widget, right_widget=tvc.widget) try: pass except Exception: - ba.print_exception('Exception wiring up graphics settings UI:') + logging.exception('Exception wiring up graphics settings UI.') v -= spacing - # make a timer to update our controls in case the config changes - # under us - self._update_timer = ba.Timer( - 0.25, - ba.WeakCall(self._update_controls), - repeat=True, - timetype=ba.TimeType.REAL, + # Make a timer to update our controls in case the config changes + # under us. + self._update_timer = bui.AppTimer( + 0.25, bui.WeakCall(self._update_controls), repeat=True ) def _back(self) -> None: from bastd.ui.settings import allsettings - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( allsettings.AllSettingsWindow( transition='in_left' ).get_root_widget() ) def _set_quality(self, quality: str) -> None: - cfg = ba.app.config + cfg = bui.app.config cfg['Graphics Quality'] = quality cfg.apply_and_commit() def _set_textures(self, val: str) -> None: - cfg = ba.app.config + cfg = bui.app.config cfg['Texture Quality'] = val cfg.apply_and_commit() def _set_android_res(self, val: str) -> None: - cfg = ba.app.config + cfg = bui.app.config cfg['Resolution (Android)'] = val cfg.apply_and_commit() def _set_pixel_scale(self, res: str) -> None: - cfg = ba.app.config + cfg = bui.app.config cfg['Screen Pixel Scale'] = float(res[:-1]) / 100.0 cfg.apply_and_commit() def _set_gvr_render_target_scale(self, res: str) -> None: - cfg = ba.app.config + cfg = bui.app.config cfg['GVR Render Target Scale'] = float(res[:-1]) / 100.0 cfg.apply_and_commit() def _set_vsync(self, val: str) -> None: - cfg = ba.app.config + cfg = bui.app.config cfg['Vertical Sync'] = val cfg.apply_and_commit() def _update_controls(self) -> None: if self._show_fullscreen: - ba.checkboxwidget( + bui.checkboxwidget( edit=self._fullscreen_checkbox, - value=ba.app.config.resolve('Fullscreen'), + value=bui.app.config.resolve('Fullscreen'), ) diff --git a/assets/src/ba_data/python/bastd/ui/settings/keyboard.py b/src/assets/ba_data/python/bastd/ui/settings/keyboard.py similarity index 69% rename from assets/src/ba_data/python/bastd/ui/settings/keyboard.py rename to src/assets/ba_data/python/bastd/ui/settings/keyboard.py index a40b0d0a..730a53ae 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/keyboard.py +++ b/src/assets/ba_data/python/bastd/ui/settings/keyboard.py @@ -6,17 +6,17 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui +import bascenev1 as bs if TYPE_CHECKING: from typing import Any -class ConfigKeyboardWindow(ba.Window): +class ConfigKeyboardWindow(bui.Window): """Window for configuring keyboards.""" - def __init__(self, c: ba.InputDevice, transition: str = 'in_right'): + def __init__(self, c: bs.InputDevice, transition: str = 'in_right'): self._r = 'configKeyboardWindow' self._input = c self._name = self._input.name @@ -24,25 +24,26 @@ class ConfigKeyboardWindow(ba.Window): dname_raw = self._name if self._unique_id != '#1': dname_raw += ' ' + self._unique_id.replace('#', 'P') - self._displayname = ba.Lstr(translate=('inputDeviceNames', dname_raw)) + self._displayname = bui.Lstr(translate=('inputDeviceNames', dname_raw)) self._width = 700 if self._unique_id != '#1': self._height = 480 else: self._height = 375 self._spacing = 40 - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), scale=( 1.6 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.3 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), - stack_offset=(0, 5) if uiscale is ba.UIScale.SMALL else (0, 0), + stack_offset=(0, 5) if uiscale is bui.UIScale.SMALL else (0, 0), transition=transition, ) ) @@ -50,7 +51,7 @@ class ConfigKeyboardWindow(ba.Window): self._rebuild_ui() def _rebuild_ui(self) -> None: - from ba.internal import get_device_value + assert bui.app.classic is not None for widget in self._root_widget.get_children(): widget.delete() @@ -69,46 +70,50 @@ class ConfigKeyboardWindow(ba.Window): 'buttonLeft', 'buttonRight', ]: - self._settings[button] = get_device_value(self._input, button) + self._settings[ + button + ] = bui.app.classic.get_input_device_mapped_value( + self._input, button + ) - cancel_button = ba.buttonwidget( + cancel_button = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(38, self._height - 85), size=(170, 60), - label=ba.Lstr(resource='cancelText'), + label=bui.Lstr(resource='cancelText'), scale=0.9, on_activate_call=self._cancel, ) - save_button = ba.buttonwidget( + save_button = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(self._width - 190, self._height - 85), size=(180, 60), - label=ba.Lstr(resource='saveText'), + label=bui.Lstr(resource='saveText'), scale=0.9, text_scale=0.9, on_activate_call=self._save, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=cancel_button, start_button=save_button, ) - ba.widget(edit=cancel_button, right_widget=save_button) - ba.widget(edit=save_button, left_widget=cancel_button) + bui.widget(edit=cancel_button, right_widget=save_button) + bui.widget(edit=save_button, left_widget=cancel_button) v = self._height - 74.0 - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, v + 15), size=(0, 0), - text=ba.Lstr( + text=bui.Lstr( resource=self._r + '.configuringText', subs=[('${DEVICE}', self._displayname)], ), - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, h_align='center', v_align='center', maxwidth=270, @@ -119,15 +124,15 @@ class ConfigKeyboardWindow(ba.Window): if self._unique_id != '#1': v -= 20 v -= self._spacing - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(0, v + 19), size=(self._width, 50), - text=ba.Lstr(resource=self._r + '.keyboard2NoteText'), + text=bui.Lstr(resource=self._r + '.keyboard2NoteText'), scale=0.7, maxwidth=self._width * 0.75, max_height=110, - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, h_align='center', v_align='top', ) @@ -143,28 +148,28 @@ class ConfigKeyboardWindow(ba.Window): pos=(h_offs, v + 0.95 * dist), color=d_color, button='buttonUp', - texture=ba.gettexture('upButton'), + texture=bui.gettexture('upButton'), scale=1.0, ) self._capture_button( pos=(h_offs - 1.2 * dist, v), color=d_color, button='buttonLeft', - texture=ba.gettexture('leftButton'), + texture=bui.gettexture('leftButton'), scale=1.0, ) self._capture_button( pos=(h_offs + 1.2 * dist, v), color=d_color, button='buttonRight', - texture=ba.gettexture('rightButton'), + texture=bui.gettexture('rightButton'), scale=1.0, ) self._capture_button( pos=(h_offs, v - 0.95 * dist), color=d_color, button='buttonDown', - texture=ba.gettexture('downButton'), + texture=bui.gettexture('downButton'), scale=1.0, ) @@ -173,7 +178,7 @@ class ConfigKeyboardWindow(ba.Window): pos=(self._width * 0.5, v + 0.1 * dist), color=(0.4, 0.4, 0.6), button='buttonStart', - texture=ba.gettexture('startButton'), + texture=bui.gettexture('startButton'), scale=0.8, ) @@ -183,28 +188,28 @@ class ConfigKeyboardWindow(ba.Window): pos=(h_offs, v + 0.95 * dist), color=(0.6, 0.4, 0.8), button='buttonPickUp', - texture=ba.gettexture('buttonPickUp'), + texture=bui.gettexture('buttonPickUp'), scale=1.0, ) self._capture_button( pos=(h_offs - 1.2 * dist, v), color=(0.7, 0.5, 0.1), button='buttonPunch', - texture=ba.gettexture('buttonPunch'), + texture=bui.gettexture('buttonPunch'), scale=1.0, ) self._capture_button( pos=(h_offs + 1.2 * dist, v), color=(0.5, 0.2, 0.1), button='buttonBomb', - texture=ba.gettexture('buttonBomb'), + texture=bui.gettexture('buttonBomb'), scale=1.0, ) self._capture_button( pos=(h_offs, v - 0.95 * dist), color=(0.2, 0.5, 0.2), button='buttonJump', - texture=ba.gettexture('buttonJump'), + texture=bui.gettexture('buttonJump'), scale=1.0, ) @@ -212,12 +217,12 @@ class ConfigKeyboardWindow(ba.Window): self, pos: tuple[float, float], color: tuple[float, float, float], - texture: ba.Texture, + texture: bui.Texture, button: str, scale: float = 1.0, ) -> None: base_size = 79 - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=( @@ -236,7 +241,7 @@ class ConfigKeyboardWindow(ba.Window): return uiscale = 0.66 * scale * 2.0 maxwidth = 76.0 * scale - txt = ba.textwidget( + txt = bui.textwidget( parent=self._root_widget, position=(pos[0] + 0.0 * scale, pos[1] - (57.0 - 18.0) * scale), color=(1, 1, 1, 0.3), @@ -247,40 +252,39 @@ class ConfigKeyboardWindow(ba.Window): maxwidth=maxwidth, text=self._input.get_button_name(self._settings[button]), ) - ba.buttonwidget( + bui.buttonwidget( edit=btn, autoselect=True, - on_activate_call=ba.Call( + on_activate_call=bui.Call( AwaitKeyboardInputWindow, button, txt, self._settings ), ) - ba.pushcall(doit) + bui.pushcall(doit) def _cancel(self) -> None: from bastd.ui.settings.controls import ControlsSettingsWindow - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_right') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( ControlsSettingsWindow(transition='in_left').get_root_widget() ) def _save(self) -> None: from bastd.ui.settings.controls import ControlsSettingsWindow - from ba.internal import ( - get_input_device_config, - should_submit_debug_info, - master_server_post, - ) - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.playsound(ba.getsound('gunCocking')) + assert bui.app.classic is not None + bui.containerwidget(edit=self._root_widget, transition='out_right') + bui.getsound('gunCocking').play() # There's a chance the device disappeared; handle that gracefully. if not self._input: return - dst = get_input_device_config(self._input, default=False) + dst = bui.app.classic.get_input_device_config( + self._input, default=False + ) dst2: dict[str, Any] = dst[0][dst[1]] dst2.clear() @@ -289,61 +293,61 @@ class ConfigKeyboardWindow(ba.Window): if val != -1: dst2[key] = val - # If we're allowed to phone home, send this config so we can generate + # Send this config to the master-server so we can generate # more defaults in the future. - if should_submit_debug_info(): - master_server_post( + if bui.app.classic is not None: + bui.app.classic.master_server_v1_post( 'controllerConfig', { - 'ua': ba.app.user_agent_string, + 'ua': bui.app.classic.user_agent_string, 'name': self._name, - 'b': ba.app.build_number, + 'b': bui.app.build_number, 'config': dst2, 'v': 2, }, ) - ba.app.config.apply_and_commit() - ba.app.ui.set_main_menu_window( + bui.app.config.apply_and_commit() + bui.app.classic.ui.set_main_menu_window( ControlsSettingsWindow(transition='in_left').get_root_widget() ) -class AwaitKeyboardInputWindow(ba.Window): +class AwaitKeyboardInputWindow(bui.Window): """Window for capturing a keypress.""" - def __init__(self, button: str, ui: ba.Widget, settings: dict): - + def __init__(self, button: str, ui: bui.Widget, settings: dict): self._capture_button = button self._capture_key_ui = ui self._settings = settings width = 400 height = 150 - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height), transition='in_right', scale=( 2.0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.5 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), ) ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(0, height - 60), size=(width, 25), - text=ba.Lstr(resource='pressAnyKeyText'), + text=bui.Lstr(resource='pressAnyKeyText'), h_align='center', v_align='top', ) self._counter = 5 - self._count_down_text = ba.textwidget( + self._count_down_text = bui.textwidget( parent=self._root_widget, h_align='center', position=(0, height - 110), @@ -351,31 +355,31 @@ class AwaitKeyboardInputWindow(ba.Window): color=(1, 1, 1, 0.3), text=str(self._counter), ) - self._decrement_timer: ba.Timer | None = ba.Timer( - 1.0, self._decrement, repeat=True, timetype=ba.TimeType.REAL + self._decrement_timer: bui.AppTimer | None = bui.AppTimer( + 1.0, self._decrement, repeat=True ) - ba.internal.capture_keyboard_input(ba.WeakCall(self._button_callback)) + bs.capture_keyboard_input(bui.WeakCall(self._button_callback)) def __del__(self) -> None: - ba.internal.release_keyboard_input() + bs.release_keyboard_input() def _die(self) -> None: # This strong-refs us; killing it allows us to die now. self._decrement_timer = None if self._root_widget: - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.containerwidget(edit=self._root_widget, transition='out_left') def _button_callback(self, event: dict[str, Any]) -> None: self._settings[self._capture_button] = event['button'] if event['type'] == 'BUTTONDOWN': bname = event['input_device'].get_button_name(event['button']) - ba.textwidget(edit=self._capture_key_ui, text=bname) - ba.playsound(ba.getsound('gunCocking')) + bui.textwidget(edit=self._capture_key_ui, text=bname) + bui.getsound('gunCocking').play() self._die() def _decrement(self) -> None: self._counter -= 1 if self._counter >= 1: - ba.textwidget(edit=self._count_down_text, text=str(self._counter)) + bui.textwidget(edit=self._count_down_text, text=str(self._counter)) else: self._die() diff --git a/assets/src/ba_data/python/bastd/ui/settings/nettesting.py b/src/assets/ba_data/python/bastd/ui/settings/nettesting.py similarity index 78% rename from assets/src/ba_data/python/bastd/ui/settings/nettesting.py rename to src/assets/ba_data/python/bastd/ui/settings/nettesting.py index c50e4158..41203ce8 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/nettesting.py +++ b/src/assets/ba_data/python/bastd/ui/settings/nettesting.py @@ -11,9 +11,8 @@ from threading import Thread from typing import TYPE_CHECKING from efro.error import CleanError -import ba -import ba.internal from bastd.ui.settings.testing import TestingWindow +import bauiv1 as bui if TYPE_CHECKING: from typing import Callable, Any @@ -23,80 +22,81 @@ if TYPE_CHECKING: MAX_TEST_SECONDS = 60 * 2 -class NetTestingWindow(ba.Window): +class NetTestingWindow(bui.Window): """Window that runs a networking test suite to help diagnose issues.""" def __init__(self, transition: str = 'in_right'): self._width = 820 self._height = 500 self._printed_lines: list[str] = [] - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), scale=( 1.56 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.2 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 0.8 ), - stack_offset=(0.0, -7 if uiscale is ba.UIScale.SMALL else 0.0), + stack_offset=(0.0, -7 if uiscale is bui.UIScale.SMALL else 0.0), transition=transition, ) ) - self._done_button = ba.buttonwidget( + self._done_button = bui.buttonwidget( parent=self._root_widget, position=(40, self._height - 77), size=(120, 60), scale=0.8, autoselect=True, - label=ba.Lstr(resource='doneText'), + label=bui.Lstr(resource='doneText'), on_activate_call=self._done, ) - self._copy_button = ba.buttonwidget( + self._copy_button = bui.buttonwidget( parent=self._root_widget, position=(self._width - 200, self._height - 77), size=(100, 60), scale=0.8, autoselect=True, - label=ba.Lstr(resource='copyText'), + label=bui.Lstr(resource='copyText'), on_activate_call=self._copy, ) - self._settings_button = ba.buttonwidget( + self._settings_button = bui.buttonwidget( parent=self._root_widget, position=(self._width - 100, self._height - 77), size=(60, 60), scale=0.8, autoselect=True, - label=ba.Lstr(value='...'), + label=bui.Lstr(value='...'), on_activate_call=self._show_val_testing, ) twidth = self._width - 450 - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 55), size=(0, 0), - text=ba.Lstr(resource='settingsWindowAdvanced.netTestingText'), + text=bui.Lstr(resource='settingsWindowAdvanced.netTestingText'), color=(0.8, 0.8, 0.8, 1.0), h_align='center', v_align='center', maxwidth=twidth, ) - self._scroll = ba.scrollwidget( + self._scroll = bui.scrollwidget( parent=self._root_widget, position=(50, 50), size=(self._width - 100, self._height - 140), capture_arrows=True, autoselect=True, ) - self._rows = ba.columnwidget(parent=self._scroll) + self._rows = bui.columnwidget(parent=self._scroll) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._done_button ) @@ -106,13 +106,13 @@ class NetTestingWindow(ba.Window): # so it doesn't keep the app running if the user is trying to quit. Thread( daemon=True, - target=ba.Call(_run_diagnostics, weakref.ref(self)), + target=bui.Call(_run_diagnostics, weakref.ref(self)), ).start() def print(self, text: str, color: tuple[float, float, float]) -> None: """Print text to our console thingie.""" for line in text.splitlines(): - txt = ba.textwidget( + txt = bui.textwidget( parent=self._rows, color=color, text=line, @@ -121,30 +121,34 @@ class NetTestingWindow(ba.Window): shadow=0.0, size=(0, 20), ) - ba.containerwidget(edit=self._rows, visible_child=txt) + bui.containerwidget(edit=self._rows, visible_child=txt) self._printed_lines.append(line) def _copy(self) -> None: - if not ba.clipboard_is_supported(): - ba.screenmessage( + if not bui.clipboard_is_supported(): + bui.screenmessage( 'Clipboard not supported on this platform.', color=(1, 0, 0) ) return - ba.clipboard_set_text('\n'.join(self._printed_lines)) - ba.screenmessage(f'{len(self._printed_lines)} lines copied.') + bui.clipboard_set_text('\n'.join(self._printed_lines)) + bui.screenmessage(f'{len(self._printed_lines)} lines copied.') def _show_val_testing(self) -> None: - ba.app.ui.set_main_menu_window(NetValTestingWindow().get_root_widget()) - ba.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( + NetValTestingWindow().get_root_widget() + ) + bui.containerwidget(edit=self._root_widget, transition='out_left') def _done(self) -> None: # pylint: disable=cyclic-import from bastd.ui.settings.advanced import AdvancedSettingsWindow - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( AdvancedSettingsWindow(transition='in_left').get_root_widget() ) - ba.containerwidget(edit=self._root_widget, transition='out_right') + bui.containerwidget(edit=self._root_widget, transition='out_right') def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: @@ -164,7 +168,7 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: if win is not None: win.print(text, (1.0, 1.0, 1.0) if color is None else color) - ba.pushcall(_print_in_logic_thread, from_other_thread=True) + bui.pushcall(_print_in_logic_thread, from_other_thread=True) def _print_test_results(call: Callable[[], Any]) -> bool: """Run the provided call, print result, & return success.""" @@ -189,9 +193,14 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: return False try: + plus = bui.app.plus + assert plus is not None + + assert bui.app.classic is not None + _print( f'Running network diagnostics...\n' - f'ua: {ba.app.user_agent_string}\n' + f'ua: {bui.app.classic.user_agent_string}\n' f'time: {utc_now()}.' ) @@ -203,7 +212,7 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: _print_test_results(_dummy_fail) # V1 ping - baseaddr = ba.internal.get_master_server_address(source=0, version=1) + baseaddr = plus.get_master_server_address(source=0, version=1) _print(f'\nContacting V1 master-server src0 ({baseaddr})...') v1worked = _print_test_results(lambda: _test_fetch(baseaddr)) @@ -211,32 +220,30 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: if v1worked: _print('\nSkipping V1 master-server src1 test since src0 worked.') else: - baseaddr = ba.internal.get_master_server_address( - source=1, version=1 - ) + baseaddr = plus.get_master_server_address(source=1, version=1) _print(f'\nContacting V1 master-server src1 ({baseaddr})...') _print_test_results(lambda: _test_fetch(baseaddr)) - if 'none succeeded' in ba.app.net.v1_test_log: + if 'none succeeded' in bui.app.net.v1_test_log: _print( - f'\nV1-test-log failed: {ba.app.net.v1_test_log}', + f'\nV1-test-log failed: {bui.app.net.v1_test_log}', color=(1, 0, 0), ) have_error[0] = True else: - _print(f'\nV1-test-log ok: {ba.app.net.v1_test_log}') + _print(f'\nV1-test-log ok: {bui.app.net.v1_test_log}') - for srcid, result in sorted(ba.app.net.v1_ctest_results.items()): + for srcid, result in sorted(bui.app.net.v1_ctest_results.items()): _print(f'\nV1 src{srcid} result: {result}') - curv1addr = ba.internal.get_master_server_address(version=1) + curv1addr = plus.get_master_server_address(version=1) _print(f'\nUsing V1 address: {curv1addr}') _print('\nRunning V1 transaction...') _print_test_results(_test_v1_transaction) # V2 ping - baseaddr = ba.internal.get_master_server_address(version=2) + baseaddr = plus.get_master_server_address(version=2) _print(f'\nContacting V2 master-server ({baseaddr})...') _print_test_results(lambda: _test_fetch(baseaddr)) @@ -244,8 +251,8 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: _print_test_results(_test_v2_time) # Get V2 nearby zone - with ba.app.net.zone_pings_lock: - zone_pings = copy.deepcopy(ba.app.net.zone_pings) + with bui.app.net.zone_pings_lock: + zone_pings = copy.deepcopy(bui.app.net.zone_pings) nearest_zone = ( None if not zone_pings @@ -295,7 +302,10 @@ def _dummy_fail() -> None: def _test_v1_transaction() -> None: """Dummy fail test case.""" - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': raise RuntimeError('Not signed in.') starttime = time.monotonic() @@ -311,17 +321,19 @@ def _test_v1_transaction() -> None: results[0] = True # Success! def _do_it() -> None: + plus = bui.app.plus + assert plus is not None # Fire off a transaction with a callback. - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'PRIVATE_PARTY_QUERY', 'expire_time': time.time() + 20, }, callback=_cb, ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() - ba.pushcall(_do_it, from_other_thread=True) + bui.pushcall(_do_it, from_other_thread=True) while results[0] is False: time.sleep(0.01) @@ -358,10 +370,10 @@ def _test_v2_cloud_message() -> None: def _send() -> None: # Note: this runs in another thread so need to avoid exceptions. results.send_time = time.monotonic() - ba.app.cloud.send_message_cb(bacommon.cloud.PingMessage(), _cb) + bui.app.cloud.send_message_cb(bacommon.cloud.PingMessage(), _cb) # This stuff expects to be run from the logic thread. - ba.pushcall(_send, from_other_thread=True) + bui.pushcall(_send, from_other_thread=True) wait_start_time = time.monotonic() while True: @@ -378,7 +390,7 @@ def _test_v2_cloud_message() -> None: def _test_v2_time() -> None: - offset = ba.app.net.server_time_offset_hours + offset = bui.app.net.server_time_offset_hours if offset is None: raise RuntimeError( 'no time offset found;' @@ -397,11 +409,14 @@ def _test_fetch(baseaddr: str) -> None: # pylint: disable=consider-using-with import urllib.request + assert bui.app.classic is not None response = urllib.request.urlopen( urllib.request.Request( - f'{baseaddr}/ping', None, {'User-Agent': ba.app.user_agent_string} + f'{baseaddr}/ping', + None, + {'User-Agent': bui.app.classic.user_agent_string}, ), - context=ba.app.net.sslcontext, + context=bui.app.net.sslcontext, timeout=MAX_TEST_SECONDS, ) if response.getcode() != 200: @@ -425,7 +440,6 @@ class NetValTestingWindow(TestingWindow): """Window to test network related settings.""" def __init__(self, transition: str = 'in_right'): - entries = [ {'name': 'bufferTime', 'label': 'Buffer Time', 'increment': 1.0}, { @@ -441,7 +455,7 @@ class NetValTestingWindow(TestingWindow): {'name': 'showNetInfo', 'label': 'Show Net Info', 'increment': 1}, ] super().__init__( - title=ba.Lstr(resource='settingsWindowAdvanced.netTestingText'), + title=bui.Lstr(resource='settingsWindowAdvanced.netTestingText'), entries=entries, transition=transition, back_call=lambda: NetTestingWindow(transition='in_left'), diff --git a/assets/src/ba_data/python/bastd/ui/settings/plugins.py b/src/assets/ba_data/python/bastd/ui/settings/plugins.py similarity index 67% rename from assets/src/ba_data/python/bastd/ui/settings/plugins.py rename to src/assets/ba_data/python/bastd/ui/settings/plugins.py index d782eaf5..80db4a62 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/plugins.py +++ b/src/assets/ba_data/python/bastd/ui/settings/plugins.py @@ -6,23 +6,23 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba +import bauiv1 as bui if TYPE_CHECKING: pass -class PluginWindow(ba.Window): +class PluginWindow(bui.Window): """Window for configuring plugins.""" def __init__( self, transition: str = 'in_right', - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-locals # pylint: disable=too-many-statements - app = ba.app + app = bui.app # If they provided an origin-widget, scale up from that. scale_origin: tuple[float, float] | None @@ -34,32 +34,33 @@ class PluginWindow(ba.Window): self._transition_out = 'out_right' scale_origin = None - uiscale = ba.app.ui.uiscale - self._width = 870.0 if uiscale is ba.UIScale.SMALL else 670.0 - x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self._width = 870.0 if uiscale is bui.UIScale.SMALL else 670.0 + x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 390.0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 450.0 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 520.0 ) - top_extra = 10 if uiscale is ba.UIScale.SMALL else 0 + top_extra = 10 if uiscale is bui.UIScale.SMALL else 0 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + top_extra), transition=transition, toolbar_visibility='menu_minimal', scale_origin_stack_offset=scale_origin, scale=( 2.06 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.4 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -25) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) @@ -69,45 +70,46 @@ class PluginWindow(ba.Window): self._sub_width = self._scroll_width * 0.95 self._sub_height = 724.0 - if app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: - ba.containerwidget( + assert app.classic is not None + if app.classic.ui.use_toolbars and uiscale is bui.UIScale.SMALL: + bui.containerwidget( edit=self._root_widget, on_cancel_call=self._do_back ) self._back_button = None else: - self._back_button = ba.buttonwidget( + self._back_button = bui.buttonwidget( parent=self._root_widget, position=(53 + x_inset, self._height - 60), size=(140, 60), scale=0.8, autoselect=True, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', on_activate_call=self._do_back, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button ) - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=self._root_widget, position=(0, self._height - 52), size=(self._width, 25), - text=ba.Lstr(resource='pluginsText'), - color=app.ui.title_color, + text=bui.Lstr(resource='pluginsText'), + color=app.classic.ui.title_color, h_align='center', v_align='top', ) if self._back_button is not None: - ba.buttonwidget( + bui.buttonwidget( edit=self._back_button, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) - settings_button_x = 670 if uiscale is ba.UIScale.SMALL else 570 - self._settings_button = ba.buttonwidget( + settings_button_x = 670 if uiscale is bui.UIScale.SMALL else 570 + self._settings_button = bui.buttonwidget( parent=self._root_widget, position=(settings_button_x, self._height - 60), size=(40, 40), @@ -115,20 +117,20 @@ class PluginWindow(ba.Window): on_activate_call=self._open_settings, ) - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, position=(settings_button_x + 3, self._height - 60), size=(35, 35), - texture=ba.gettexture('settingsIcon'), + texture=bui.gettexture('settingsIcon'), ) - ba.widget( + bui.widget( edit=self._settings_button, up_widget=self._settings_button, right_widget=self._settings_button, ) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, position=(50 + x_inset, 50), simple_culling_v=20.0, @@ -137,28 +139,28 @@ class PluginWindow(ba.Window): selection_loops_to_parent=True, claims_left_right=True, ) - ba.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) + bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) - if ba.app.meta.scanresults is None: - ba.screenmessage( + if bui.app.meta.scanresults is None: + bui.screenmessage( 'Still scanning plugins; please try again.', color=(1, 0, 0) ) - ba.playsound(ba.getsound('error')) - pluglist = ba.app.plugins.potential_plugins - plugstates: dict[str, dict] = ba.app.config.setdefault('Plugins', {}) + bui.getsound('error').play() + pluglist = bui.app.plugins.potential_plugins + plugstates: dict[str, dict] = bui.app.config.setdefault('Plugins', {}) assert isinstance(plugstates, dict) plug_line_height = 50 sub_width = self._scroll_width sub_height = len(pluglist) * plug_line_height - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(sub_width, sub_height), background=False, ) for i, availplug in enumerate(pluglist): - plugin = ba.app.plugins.active_plugins.get(availplug.class_path) + plugin = bui.app.plugins.active_plugins.get(availplug.class_path) active = plugin is not None plugstate = plugstates.setdefault(availplug.class_path, {}) @@ -166,7 +168,7 @@ class PluginWindow(ba.Window): assert isinstance(checked, bool) item_y = sub_height - (i + 1) * plug_line_height - check = ba.checkboxwidget( + check = bui.checkboxwidget( parent=self._subcontainer, text=availplug.display_name, autoselect=True, @@ -174,7 +176,7 @@ class PluginWindow(ba.Window): maxwidth=self._scroll_width - 200, position=(10, item_y), size=(self._scroll_width - 40, 50), - on_value_change_call=ba.Call( + on_value_change_call=bui.Call( self._check_value_changed, availplug ), textcolor=( @@ -186,62 +188,63 @@ class PluginWindow(ba.Window): ), ) if plugin is not None and plugin.has_settings_ui(): - button = ba.buttonwidget( + button = bui.buttonwidget( parent=self._subcontainer, - label=ba.Lstr(resource='mainMenu.settingsText'), + label=bui.Lstr(resource='mainMenu.settingsText'), autoselect=True, size=(100, 40), position=(sub_width - 130, item_y + 6), ) - ba.buttonwidget( + bui.buttonwidget( edit=button, - on_activate_call=ba.Call(plugin.show_settings_ui, button), + on_activate_call=bui.Call(plugin.show_settings_ui, button), ) else: button = None # Allow getting back to back button. if i == 0: - ba.widget( + bui.widget( edit=check, up_widget=self._back_button, left_widget=self._back_button, right_widget=self._settings_button, ) if button is not None: - ba.widget(edit=button, up_widget=self._back_button) + bui.widget(edit=button, up_widget=self._back_button) # Make sure we scroll all the way to the end when using # keyboard/button nav. - ba.widget(edit=check, show_buffer_top=40, show_buffer_bottom=40) + bui.widget(edit=check, show_buffer_top=40, show_buffer_bottom=40) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) self._restore_state() def _check_value_changed( - self, plug: ba.PotentialPlugin, value: bool + self, plug: bui.PotentialPlugin, value: bool ) -> None: - ba.screenmessage( - ba.Lstr(resource='settingsWindowAdvanced.mustRestartText'), + bui.screenmessage( + bui.Lstr(resource='settingsWindowAdvanced.mustRestartText'), color=(1.0, 0.5, 0.0), ) - plugstates: dict[str, dict] = ba.app.config.setdefault('Plugins', {}) + plugstates: dict[str, dict] = bui.app.config.setdefault('Plugins', {}) assert isinstance(plugstates, dict) plugstate = plugstates.setdefault(plug.class_path, {}) plugstate['enabled'] = value - ba.app.config.commit() + bui.app.config.commit() def _open_settings(self) -> None: # pylint: disable=cyclic-import from bastd.ui.settings.pluginsettings import PluginSettingsWindow - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( PluginSettingsWindow(transition='in_right').get_root_widget() ) @@ -256,9 +259,10 @@ class PluginWindow(ba.Window): from bastd.ui.settings.advanced import AdvancedSettingsWindow self._save_state() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( AdvancedSettingsWindow(transition='in_left').get_root_widget() ) diff --git a/assets/src/ba_data/python/bastd/ui/settings/pluginsettings.py b/src/assets/ba_data/python/bastd/ui/settings/pluginsettings.py similarity index 64% rename from assets/src/ba_data/python/bastd/ui/settings/pluginsettings.py rename to src/assets/ba_data/python/bastd/ui/settings/pluginsettings.py index 37e9248b..5decd22a 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/pluginsettings.py +++ b/src/assets/ba_data/python/bastd/ui/settings/pluginsettings.py @@ -4,85 +4,80 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -import ba +import bauiv1 as bui from bastd.ui.confirm import ConfirmWindow -if TYPE_CHECKING: - pass - -class PluginSettingsWindow(ba.Window): +class PluginSettingsWindow(bui.Window): """Plugin Settings Window""" def __init__(self, transition: str = 'in_right'): - scale_origin: tuple[float, float] | None self._transition_out = 'out_right' scale_origin = None - uiscale = ba.app.ui.uiscale - width = 470.0 if uiscale is ba.UIScale.SMALL else 470.0 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + width = 470.0 if uiscale is bui.UIScale.SMALL else 470.0 height = ( 365.0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 300.0 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 370.0 ) - top_extra = 10 if uiscale is ba.UIScale.SMALL else 0 + top_extra = 10 if uiscale is bui.UIScale.SMALL else 0 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height + top_extra), transition=transition, toolbar_visibility='menu_minimal', scale_origin_stack_offset=scale_origin, scale=( 2.06 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.4 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -25) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - self._back_button = ba.buttonwidget( + self._back_button = bui.buttonwidget( parent=self._root_widget, position=(53, height - 60), size=(60, 60), scale=0.8, autoselect=True, - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), button_type='backSmall', on_activate_call=self._do_back, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button ) - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=self._root_widget, position=(0, height - 52), size=(width, 25), - text=ba.Lstr(resource='pluginSettingsText'), - color=ba.app.ui.title_color, + text=bui.Lstr(resource='pluginSettingsText'), + color=bui.app.classic.ui.title_color, h_align='center', v_align='top', ) - self._y_position = 170 if uiscale is ba.UIScale.MEDIUM else 205 - self._enable_plugins_button = ba.buttonwidget( + self._y_position = 170 if uiscale is bui.UIScale.MEDIUM else 205 + self._enable_plugins_button = bui.buttonwidget( parent=self._root_widget, position=(65, self._y_position), size=(350, 60), autoselect=True, - label=ba.Lstr(resource='pluginsEnableAllText'), + label=bui.Lstr(resource='pluginsEnableAllText'), text_scale=1.0, on_activate_call=lambda: ConfirmWindow( action=self._enable_all_plugins, @@ -90,12 +85,12 @@ class PluginSettingsWindow(ba.Window): ) self._y_position -= 70 - self._disable_plugins_button = ba.buttonwidget( + self._disable_plugins_button = bui.buttonwidget( parent=self._root_widget, position=(65, self._y_position), size=(350, 60), autoselect=True, - label=ba.Lstr(resource='pluginsDisableAllText'), + label=bui.Lstr(resource='pluginsDisableAllText'), text_scale=1.0, on_activate_call=lambda: ConfirmWindow( action=self._disable_all_plugins, @@ -103,30 +98,30 @@ class PluginSettingsWindow(ba.Window): ) self._y_position -= 70 - self._enable_new_plugins_check_box = ba.checkboxwidget( + self._enable_new_plugins_check_box = bui.checkboxwidget( parent=self._root_widget, position=(65, self._y_position), size=(350, 60), - value=ba.app.config.get( - ba.app.plugins.AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY, - ba.app.plugins.AUTO_ENABLE_NEW_PLUGINS_DEFAULT, + value=bui.app.config.get( + bui.app.plugins.AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY, + bui.app.plugins.AUTO_ENABLE_NEW_PLUGINS_DEFAULT, ), - text=ba.Lstr(resource='pluginsAutoEnableNewText'), + text=bui.Lstr(resource='pluginsAutoEnableNewText'), scale=1.0, maxwidth=308, on_value_change_call=self._update_value, ) - ba.widget( + bui.widget( edit=self._back_button, down_widget=self._enable_plugins_button ) - ba.widget( + bui.widget( edit=self._disable_plugins_button, left_widget=self._disable_plugins_button, ) - ba.widget( + bui.widget( edit=self._enable_new_plugins_check_box, left_widget=self._enable_new_plugins_check_box, right_widget=self._enable_new_plugins_check_box, @@ -134,41 +129,42 @@ class PluginSettingsWindow(ba.Window): ) def _enable_all_plugins(self) -> None: - cfg = ba.app.config + cfg = bui.app.config plugs: dict[str, dict] = cfg.setdefault('Plugins', {}) for plug in plugs.values(): plug['enabled'] = True cfg.apply_and_commit() - ba.screenmessage( - ba.Lstr(resource='settingsWindowAdvanced.mustRestartText'), + bui.screenmessage( + bui.Lstr(resource='settingsWindowAdvanced.mustRestartText'), color=(1.0, 0.5, 0.0), ) def _disable_all_plugins(self) -> None: - cfg = ba.app.config + cfg = bui.app.config plugs: dict[str, dict] = cfg.setdefault('Plugins', {}) for plug in plugs.values(): plug['enabled'] = False cfg.apply_and_commit() - ba.screenmessage( - ba.Lstr(resource='settingsWindowAdvanced.mustRestartText'), + bui.screenmessage( + bui.Lstr(resource='settingsWindowAdvanced.mustRestartText'), color=(1.0, 0.5, 0.0), ) def _update_value(self, val: bool) -> None: - cfg = ba.app.config - cfg[ba.app.plugins.AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY] = val + cfg = bui.app.config + cfg[bui.app.plugins.AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY] = val cfg.apply_and_commit() def _do_back(self) -> None: # pylint: disable=cyclic-import from bastd.ui.settings.plugins import PluginWindow - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( PluginWindow(transition='in_left').get_root_widget() ) diff --git a/assets/src/ba_data/python/bastd/ui/settings/remoteapp.py b/src/assets/ba_data/python/bastd/ui/settings/remoteapp.py similarity index 70% rename from assets/src/ba_data/python/bastd/ui/settings/remoteapp.py rename to src/assets/ba_data/python/bastd/ui/settings/remoteapp.py index c0e8b2e2..350da5ed 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/remoteapp.py +++ b/src/assets/ba_data/python/bastd/ui/settings/remoteapp.py @@ -4,82 +4,81 @@ from __future__ import annotations -import ba +import bauiv1 as bui -class RemoteAppSettingsWindow(ba.Window): +class RemoteAppSettingsWindow(bui.Window): """Window showing info/settings related to the remote app.""" def __init__(self) -> None: - from ba.internal import get_remote_app_name - self._r = 'connectMobileDevicesWindow' width = 700 height = 390 spacing = 40 - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(width, height), transition='in_right', scale=( 1.85 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.3 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(-10, 0) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=(40, height - 67), size=(140, 65), scale=0.8, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', text_scale=1.1, autoselect=True, on_activate_call=self._back, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(width * 0.5, height - 42), size=(0, 0), - text=ba.Lstr(resource=self._r + '.titleText'), + text=bui.Lstr(resource=self._r + '.titleText'), maxwidth=370, - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, scale=0.8, h_align='center', v_align='center', ) - ba.buttonwidget( + bui.buttonwidget( edit=btn, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) v = height - 70.0 v -= spacing * 1.2 - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(15, v - 26), size=(width - 30, 30), maxwidth=width * 0.95, color=(0.7, 0.9, 0.7, 1.0), scale=0.8, - text=ba.Lstr( + text=bui.Lstr( resource=self._r + '.explanationText', subs=[ - ('${APP_NAME}', ba.Lstr(resource='titleText')), - ('${REMOTE_APP_NAME}', get_remote_app_name()), + ('${APP_NAME}', bui.Lstr(resource='titleText')), + ('${REMOTE_APP_NAME}', bui.get_remote_app_name()), ], ), max_height=100, @@ -92,7 +91,7 @@ class RemoteAppSettingsWindow(ba.Window): # apple-specific-ish # Update: now we just show link to the remote webpage. - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(width * 0.5, v + 5), size=(0, 0), @@ -106,41 +105,42 @@ class RemoteAppSettingsWindow(ba.Window): ) v -= 30 - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(width * 0.5, v - 35), size=(0, 0), color=(0.7, 0.9, 0.7, 0.8), scale=0.65, - text=ba.Lstr(resource=self._r + '.bestResultsText'), + text=bui.Lstr(resource=self._r + '.bestResultsText'), maxwidth=width * 0.95, max_height=height * 0.19, h_align='center', v_align='center', ) - ba.checkboxwidget( + bui.checkboxwidget( parent=self._root_widget, position=(width * 0.5 - 150, v - 116), size=(300, 30), maxwidth=300, scale=0.8, - value=not ba.app.config.resolve('Enable Remote App'), + value=not bui.app.config.resolve('Enable Remote App'), autoselect=True, - text=ba.Lstr(resource='disableRemoteAppConnectionsText'), + text=bui.Lstr(resource='disableRemoteAppConnectionsText'), on_value_change_call=self._on_check_changed, ) def _on_check_changed(self, value: bool) -> None: - cfg = ba.app.config + cfg = bui.app.config cfg['Enable Remote App'] = not value cfg.apply_and_commit() def _back(self) -> None: from bastd.ui.settings import controls - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_right') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( controls.ControlsSettingsWindow( transition='in_left' ).get_root_widget() diff --git a/assets/src/ba_data/python/bastd/ui/settings/testing.py b/src/assets/ba_data/python/bastd/ui/settings/testing.py similarity index 64% rename from assets/src/ba_data/python/bastd/ui/settings/testing.py rename to src/assets/ba_data/python/bastd/ui/settings/testing.py index 739ba417..3a4d861e 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/testing.py +++ b/src/assets/ba_data/python/bastd/ui/settings/testing.py @@ -7,99 +7,99 @@ from __future__ import annotations import copy from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable -class TestingWindow(ba.Window): +class TestingWindow(bui.Window): """Window for conveniently testing various settings.""" def __init__( self, - title: ba.Lstr, + title: bui.Lstr, entries: list[dict[str, Any]], transition: str = 'in_right', - back_call: Callable[[], ba.Window] | None = None, + back_call: Callable[[], bui.Window] | None = None, ): - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale self._width = 600 - self._height = 324 if uiscale is ba.UIScale.SMALL else 400 + self._height = 324 if uiscale is bui.UIScale.SMALL else 400 self._entries = copy.deepcopy(entries) self._back_call = back_call super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), transition=transition, scale=( 2.5 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.2 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -28) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - self._back_button = btn = ba.buttonwidget( + self._back_button = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(65, self._height - 59), size=(130, 60), scale=0.8, text_scale=1.2, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', on_activate_call=self._do_back, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 35), size=(0, 0), - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, h_align='center', v_align='center', maxwidth=245, text=title, ) - ba.buttonwidget( + bui.buttonwidget( edit=self._back_button, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 75), size=(0, 0), - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, h_align='center', v_align='center', maxwidth=self._width * 0.75, - text=ba.Lstr(resource='settingsWindowAdvanced.forTestingText'), + text=bui.Lstr(resource='settingsWindowAdvanced.forTestingText'), ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) self._scroll_width = self._width - 130 self._scroll_height = self._height - 140 - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, size=(self._scroll_width, self._scroll_height), highlight=False, position=((self._width - self._scroll_width) * 0.5, 40), ) - ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) + bui.containerwidget(edit=self._scrollwidget, claims_left_right=True) self._spacing = 50 self._sub_width = self._scroll_width * 0.95 self._sub_height = 50 + len(self._entries) * self._spacing + 60 - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._sub_width, self._sub_height), background=False, @@ -109,17 +109,16 @@ class TestingWindow(ba.Window): v = self._sub_height - 48 for i, entry in enumerate(self._entries): - entry_name = entry['name'] # If we haven't yet, record the default value for this name so # we can reset if we want.. - if entry_name not in ba.app.value_test_defaults: - ba.app.value_test_defaults[entry_name] = ba.internal.value_test( + if entry_name not in bui.app.classic.value_test_defaults: + bui.app.classic.value_test_defaults[ entry_name - ) + ] = bui.app.classic.value_test(entry_name) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(h, v), size=(0, 0), @@ -128,7 +127,7 @@ class TestingWindow(ba.Window): maxwidth=200, text=entry['label'], ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._subcontainer, position=(h + 20, v - 19), size=(40, 40), @@ -137,21 +136,21 @@ class TestingWindow(ba.Window): left_widget=self._back_button, button_type='square', label='-', - on_activate_call=ba.Call(self._on_minus_press, entry['name']), + on_activate_call=bui.Call(self._on_minus_press, entry['name']), ) if i == 0: - ba.widget(edit=btn, up_widget=self._back_button) + bui.widget(edit=btn, up_widget=self._back_button) # pylint: disable=consider-using-f-string - entry['widget'] = ba.textwidget( + entry['widget'] = bui.textwidget( parent=self._subcontainer, position=(h + 100, v), size=(0, 0), h_align='center', v_align='center', maxwidth=60, - text='%.4g' % ba.internal.value_test(entry_name), + text='%.4g' % bui.app.classic.value_test(entry_name), ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._subcontainer, position=(h + 140, v - 19), size=(40, 40), @@ -159,18 +158,18 @@ class TestingWindow(ba.Window): repeat=True, button_type='square', label='+', - on_activate_call=ba.Call(self._on_plus_press, entry['name']), + on_activate_call=bui.Call(self._on_plus_press, entry['name']), ) if i == 0: - ba.widget(edit=btn, up_widget=self._back_button) + bui.widget(edit=btn, up_widget=self._back_button) v -= self._spacing v -= 35 - ba.buttonwidget( + bui.buttonwidget( parent=self._subcontainer, autoselect=True, size=(200, 50), position=(self._sub_width * 0.5 - 100, v), - label=ba.Lstr(resource='settingsWindowAdvanced.resetText'), + label=bui.Lstr(resource='settingsWindowAdvanced.resetText'), right_widget=btn, on_activate_call=self._on_reset_press, ) @@ -179,46 +178,50 @@ class TestingWindow(ba.Window): for entry in self._entries: if entry['name'] == name: return entry - raise ba.NotFoundError(f'Entry not found: {name}') + raise bui.NotFoundError(f'Entry not found: {name}') def _on_reset_press(self) -> None: + assert bui.app.classic is not None for entry in self._entries: - ba.internal.value_test( + bui.app.classic.value_test( entry['name'], - absolute=ba.app.value_test_defaults[entry['name']], + absolute=bui.app.classic.value_test_defaults[entry['name']], ) # pylint: disable=consider-using-f-string - ba.textwidget( + bui.textwidget( edit=entry['widget'], - text='%.4g' % ba.internal.value_test(entry['name']), + text='%.4g' % bui.app.classic.value_test(entry['name']), ) def _on_minus_press(self, entry_name: str) -> None: + assert bui.app.classic is not None entry = self._get_entry(entry_name) - ba.internal.value_test(entry['name'], change=-entry['increment']) + bui.app.classic.value_test(entry['name'], change=-entry['increment']) # pylint: disable=consider-using-f-string - ba.textwidget( + bui.textwidget( edit=entry['widget'], - text='%.4g' % ba.internal.value_test(entry['name']), + text='%.4g' % bui.app.classic.value_test(entry['name']), ) def _on_plus_press(self, entry_name: str) -> None: + assert bui.app.classic is not None entry = self._get_entry(entry_name) - ba.internal.value_test(entry['name'], change=entry['increment']) + bui.app.classic.value_test(entry['name'], change=entry['increment']) # pylint: disable=consider-using-f-string - ba.textwidget( + bui.textwidget( edit=entry['widget'], - text='%.4g' % ba.internal.value_test(entry['name']), + text='%.4g' % bui.app.classic.value_test(entry['name']), ) def _do_back(self) -> None: # pylint: disable=cyclic-import from bastd.ui.settings.advanced import AdvancedSettingsWindow - ba.containerwidget(edit=self._root_widget, transition='out_right') + bui.containerwidget(edit=self._root_widget, transition='out_right') backwin = ( self._back_call() if self._back_call is not None else AdvancedSettingsWindow(transition='in_left') ) - ba.app.ui.set_main_menu_window(backwin.get_root_widget()) + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window(backwin.get_root_widget()) diff --git a/assets/src/ba_data/python/bastd/ui/settings/touchscreen.py b/src/assets/ba_data/python/bastd/ui/settings/touchscreen.py similarity index 73% rename from assets/src/ba_data/python/bastd/ui/settings/touchscreen.py rename to src/assets/ba_data/python/bastd/ui/settings/touchscreen.py index c1140ccb..48c851f9 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/touchscreen.py +++ b/src/assets/ba_data/python/bastd/ui/settings/touchscreen.py @@ -3,11 +3,11 @@ """UI settings functionality related to touchscreens.""" from __future__ import annotations -import ba -import ba.internal +import bauiv1 as bui +import bascenev1 as bs -class TouchscreenSettingsWindow(ba.Window): +class TouchscreenSettingsWindow(bui.Window): """Settings window for touchscreens.""" def __del__(self) -> None: @@ -16,59 +16,59 @@ class TouchscreenSettingsWindow(ba.Window): # FIXME: Could switch to a UI destroy callback now that those are a # thing that exists. - ba.internal.set_touchscreen_editing(False) + bs.set_touchscreen_editing(False) def __init__(self) -> None: - self._width = 650 self._height = 380 self._spacing = 40 self._r = 'configTouchscreenWindow' - ba.internal.set_touchscreen_editing(True) + bs.set_touchscreen_editing(True) - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), transition='in_right', scale=( 1.9 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.55 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.2 ), ) ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=(55, self._height - 60), size=(120, 60), - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', scale=0.8, on_activate_call=self._back, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(25, self._height - 50), size=(self._width, 25), - text=ba.Lstr(resource=self._r + '.titleText'), - color=ba.app.ui.title_color, + text=bui.Lstr(resource=self._r + '.titleText'), + color=bui.app.classic.ui.title_color, maxwidth=280, h_align='center', v_align='center', ) - ba.buttonwidget( + bui.buttonwidget( edit=btn, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) self._scroll_width = self._width - 100 @@ -76,7 +76,7 @@ class TouchscreenSettingsWindow(ba.Window): self._sub_width = self._scroll_width - 20 self._sub_height = 360 - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, position=( (self._width - self._scroll_width) * 0.5, @@ -87,7 +87,7 @@ class TouchscreenSettingsWindow(ba.Window): claims_tab=True, selection_loops_to_parent=True, ) - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(self._sub_width, self._sub_height), background=False, @@ -109,11 +109,11 @@ class TouchscreenSettingsWindow(ba.Window): v = self._sub_height - 85 clr = (0.8, 0.8, 0.8, 1.0) clr2 = (0.8, 0.8, 0.8) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(-10, v + 43), size=(self._sub_width, 25), - text=ba.Lstr(resource=self._r + '.swipeInfoText'), + text=bui.Lstr(resource=self._r + '.swipeInfoText'), flatness=1.0, color=(0, 0.9, 0.1, 0.7), maxwidth=self._sub_width * 0.9, @@ -121,30 +121,30 @@ class TouchscreenSettingsWindow(ba.Window): h_align='center', v_align='center', ) - cur_val = ba.app.config.get('Touch Movement Control Type', 'swipe') - ba.textwidget( + cur_val = bui.app.config.get('Touch Movement Control Type', 'swipe') + bui.textwidget( parent=self._subcontainer, position=(h, v - 2), size=(0, 30), - text=ba.Lstr(resource=self._r + '.movementText'), + text=bui.Lstr(resource=self._r + '.movementText'), maxwidth=190, color=clr, v_align='center', ) - cb1 = ba.checkboxwidget( + cb1 = bui.checkboxwidget( parent=self._subcontainer, position=(h + 220, v), size=(170, 30), - text=ba.Lstr(resource=self._r + '.joystickText'), + text=bui.Lstr(resource=self._r + '.joystickText'), maxwidth=100, textcolor=clr2, scale=0.9, ) - cb2 = ba.checkboxwidget( + cb2 = bui.checkboxwidget( parent=self._subcontainer, position=(h + 357, v), size=(170, 30), - text=ba.Lstr(resource=self._r + '.swipeText'), + text=bui.Lstr(resource=self._r + '.swipeText'), maxwidth=100, textcolor=clr2, value=False, @@ -159,37 +159,39 @@ class TouchscreenSettingsWindow(ba.Window): position=(h, v), xoffset=65, configkey='Touch Controls Scale Movement', - displayname=ba.Lstr(resource=self._r + '.movementControlScaleText'), + displayname=bui.Lstr( + resource=self._r + '.movementControlScaleText' + ), changesound=False, minval=0.1, maxval=4.0, increment=0.1, ) v -= 50 - cur_val = ba.app.config.get('Touch Action Control Type', 'buttons') - ba.textwidget( + cur_val = bui.app.config.get('Touch Action Control Type', 'buttons') + bui.textwidget( parent=self._subcontainer, position=(h, v - 2), size=(0, 30), - text=ba.Lstr(resource=self._r + '.actionsText'), + text=bui.Lstr(resource=self._r + '.actionsText'), maxwidth=190, color=clr, v_align='center', ) - cb1 = ba.checkboxwidget( + cb1 = bui.checkboxwidget( parent=self._subcontainer, position=(h + 220, v), size=(170, 30), - text=ba.Lstr(resource=self._r + '.buttonsText'), + text=bui.Lstr(resource=self._r + '.buttonsText'), maxwidth=100, textcolor=clr2, scale=0.9, ) - cb2 = ba.checkboxwidget( + cb2 = bui.checkboxwidget( parent=self._subcontainer, position=(h + 357, v), size=(170, 30), - text=ba.Lstr(resource=self._r + '.swipeText'), + text=bui.Lstr(resource=self._r + '.swipeText'), maxwidth=100, textcolor=clr2, scale=0.9, @@ -203,7 +205,7 @@ class TouchscreenSettingsWindow(ba.Window): position=(h, v), xoffset=65, configkey='Touch Controls Scale Actions', - displayname=ba.Lstr(resource=self._r + '.actionControlScaleText'), + displayname=bui.Lstr(resource=self._r + '.actionControlScaleText'), changesound=False, minval=0.1, maxval=4.0, @@ -217,42 +219,42 @@ class TouchscreenSettingsWindow(ba.Window): size=(400, 30), maxwidth=400, configkey='Touch Controls Swipe Hidden', - displayname=ba.Lstr(resource=self._r + '.swipeControlsHiddenText'), + displayname=bui.Lstr(resource=self._r + '.swipeControlsHiddenText'), ) v -= 65 - ba.buttonwidget( + bui.buttonwidget( parent=self._subcontainer, position=(self._sub_width * 0.5 - 70, v), size=(170, 60), - label=ba.Lstr(resource=self._r + '.resetText'), + label=bui.Lstr(resource=self._r + '.resetText'), scale=0.75, on_activate_call=self._reset, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, 38), size=(0, 0), h_align='center', - text=ba.Lstr(resource=self._r + '.dragControlsText'), + text=bui.Lstr(resource=self._r + '.dragControlsText'), maxwidth=self._width * 0.8, scale=0.65, color=(1, 1, 1, 0.4), ) def _actions_changed(self, v: str) -> None: - cfg = ba.app.config + cfg = bui.app.config cfg['Touch Action Control Type'] = v cfg.apply_and_commit() def _movement_changed(self, v: str) -> None: - cfg = ba.app.config + cfg = bui.app.config cfg['Touch Movement Control Type'] = v cfg.apply_and_commit() def _reset(self) -> None: - cfg = ba.app.config + cfg = bui.app.config cfgkeys = [ 'Touch Movement Control Type', 'Touch Action Control Type', @@ -269,15 +271,16 @@ class TouchscreenSettingsWindow(ba.Window): if cfgkey in cfg: del cfg[cfgkey] cfg.apply_and_commit() - ba.timer(0, self._build_gui, timetype=ba.TimeType.REAL) + bui.apptimer(0, self._build_gui) def _back(self) -> None: from bastd.ui.settings import controls - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_right') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( controls.ControlsSettingsWindow( transition='in_left' ).get_root_widget() ) - ba.internal.set_touchscreen_editing(False) + bs.set_touchscreen_editing(False) diff --git a/assets/src/ba_data/python/bastd/ui/settings/vrtesting.py b/src/assets/ba_data/python/bastd/ui/settings/vrtesting.py similarity index 90% rename from assets/src/ba_data/python/bastd/ui/settings/vrtesting.py rename to src/assets/ba_data/python/bastd/ui/settings/vrtesting.py index 43f18155..668ccf70 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/vrtesting.py +++ b/src/assets/ba_data/python/bastd/ui/settings/vrtesting.py @@ -6,7 +6,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba +import babase from bastd.ui.settings.testing import TestingWindow if TYPE_CHECKING: @@ -17,11 +17,15 @@ class VRTestingWindow(TestingWindow): """Window for testing vr settings.""" def __init__(self, transition: str = 'in_right'): - entries: list[dict[str, Any]] = [] - app = ba.app + app = babase.app + assert app.classic is not None + # these are gear-vr only - if app.platform == 'android' and app.subplatform == 'oculus': + if ( + app.classic.platform == 'android' + and app.classic.subplatform == 'oculus' + ): entries += [ { 'name': 'timeWarpDebug', @@ -84,7 +88,7 @@ class VRTestingWindow(TestingWindow): ] super().__init__( - ba.Lstr(resource='settingsWindowAdvanced.vrTestingText'), + babase.Lstr(resource='settingsWindowAdvanced.vrTestingText'), entries, transition, ) diff --git a/assets/src/ba_data/python/bastd/ui/soundtrack/__init__.py b/src/assets/ba_data/python/bastd/ui/soundtrack/__init__.py similarity index 100% rename from assets/src/ba_data/python/bastd/ui/soundtrack/__init__.py rename to src/assets/ba_data/python/bastd/ui/soundtrack/__init__.py diff --git a/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py b/src/assets/ba_data/python/bastd/ui/soundtrack/browser.py similarity index 72% rename from assets/src/ba_data/python/bastd/ui/soundtrack/browser.py rename to src/assets/ba_data/python/bastd/ui/soundtrack/browser.py index 457b441f..d709a5dc 100644 --- a/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py +++ b/src/assets/ba_data/python/bastd/ui/soundtrack/browser.py @@ -5,22 +5,22 @@ from __future__ import annotations import copy +import logging from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any -class SoundtrackBrowserWindow(ba.Window): +class SoundtrackBrowserWindow(bui.Window): """Window for browsing soundtracks.""" def __init__( self, transition: str = 'in_right', - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-locals # pylint: disable=too-many-statements @@ -36,14 +36,15 @@ class SoundtrackBrowserWindow(ba.Window): scale_origin = None self._r = 'editSoundtrackWindow' - uiscale = ba.app.ui.uiscale - self._width = 800 if uiscale is ba.UIScale.SMALL else 600 - x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self._width = 800 if uiscale is bui.UIScale.SMALL else 600 + x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 340 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 370 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 440 ) spacing = 40.0 @@ -51,49 +52,50 @@ class SoundtrackBrowserWindow(ba.Window): v -= spacing * 1.0 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), transition=transition, toolbar_visibility='menu_minimal', scale_origin_stack_offset=scale_origin, scale=( 2.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.6 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -18) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) - if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: + assert bui.app.classic is not None + if bui.app.classic.ui.use_toolbars and uiscale is bui.UIScale.SMALL: self._back_button = None else: - self._back_button = ba.buttonwidget( + self._back_button = bui.buttonwidget( parent=self._root_widget, position=(45 + x_inset, self._height - 60), size=(120, 60), scale=0.8, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', autoselect=True, ) - ba.buttonwidget( + bui.buttonwidget( edit=self._back_button, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 35), size=(0, 0), maxwidth=300, - text=ba.Lstr(resource=self._r + '.titleText'), - color=ba.app.ui.title_color, + text=bui.Lstr(resource=self._r + '.titleText'), + color=bui.app.classic.ui.title_color, h_align='center', v_align='center', ) @@ -102,18 +104,18 @@ class SoundtrackBrowserWindow(ba.Window): v = self._height - 60 b_color = (0.6, 0.53, 0.63) b_textcolor = (0.75, 0.7, 0.8) - lock_tex = ba.gettexture('lock') - self._lock_images: list[ba.Widget] = [] + lock_tex = bui.gettexture('lock') + self._lock_images: list[bui.Widget] = [] scl = ( 1.0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.13 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.4 ) v -= 60.0 * scl - self._new_button = btn = ba.buttonwidget( + self._new_button = btn = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(100, 55.0 * scl), @@ -123,10 +125,10 @@ class SoundtrackBrowserWindow(ba.Window): autoselect=True, textcolor=b_textcolor, text_scale=0.7, - label=ba.Lstr(resource=self._r + '.newText'), + label=bui.Lstr(resource=self._r + '.newText'), ) self._lock_images.append( - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, size=(30, 30), draw_controller=btn, @@ -136,13 +138,13 @@ class SoundtrackBrowserWindow(ba.Window): ) if self._back_button is None: - ba.widget( + bui.widget( edit=btn, - left_widget=ba.internal.get_special_widget('back_button'), + left_widget=bui.get_special_widget('back_button'), ) v -= 60.0 * scl - self._edit_button = btn = ba.buttonwidget( + self._edit_button = btn = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(100, 55.0 * scl), @@ -152,10 +154,10 @@ class SoundtrackBrowserWindow(ba.Window): autoselect=True, textcolor=b_textcolor, text_scale=0.7, - label=ba.Lstr(resource=self._r + '.editText'), + label=bui.Lstr(resource=self._r + '.editText'), ) self._lock_images.append( - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, size=(30, 30), draw_controller=btn, @@ -164,13 +166,13 @@ class SoundtrackBrowserWindow(ba.Window): ) ) if self._back_button is None: - ba.widget( + bui.widget( edit=btn, - left_widget=ba.internal.get_special_widget('back_button'), + left_widget=bui.get_special_widget('back_button'), ) v -= 60.0 * scl - self._duplicate_button = btn = ba.buttonwidget( + self._duplicate_button = btn = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(100, 55.0 * scl), @@ -180,10 +182,10 @@ class SoundtrackBrowserWindow(ba.Window): color=b_color, textcolor=b_textcolor, text_scale=0.7, - label=ba.Lstr(resource=self._r + '.duplicateText'), + label=bui.Lstr(resource=self._r + '.duplicateText'), ) self._lock_images.append( - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, size=(30, 30), draw_controller=btn, @@ -192,13 +194,13 @@ class SoundtrackBrowserWindow(ba.Window): ) ) if self._back_button is None: - ba.widget( + bui.widget( edit=btn, - left_widget=ba.internal.get_special_widget('back_button'), + left_widget=bui.get_special_widget('back_button'), ) v -= 60.0 * scl - self._delete_button = btn = ba.buttonwidget( + self._delete_button = btn = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(100, 55.0 * scl), @@ -208,10 +210,10 @@ class SoundtrackBrowserWindow(ba.Window): autoselect=True, textcolor=b_textcolor, text_scale=0.7, - label=ba.Lstr(resource=self._r + '.deleteText'), + label=bui.Lstr(resource=self._r + '.deleteText'), ) self._lock_images.append( - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, size=(30, 30), draw_controller=btn, @@ -220,66 +222,68 @@ class SoundtrackBrowserWindow(ba.Window): ) ) if self._back_button is None: - ba.widget( + bui.widget( edit=btn, - left_widget=ba.internal.get_special_widget('back_button'), + left_widget=bui.get_special_widget('back_button'), ) # Keep our lock images up to date/etc. - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - timetype=ba.TimeType.REAL, - repeat=True, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) self._update() v = self._height - 65 scroll_height = self._height - 105 v -= scroll_height - self._scrollwidget = scrollwidget = ba.scrollwidget( + self._scrollwidget = scrollwidget = bui.scrollwidget( parent=self._root_widget, position=(152 + x_inset, v), highlight=False, size=(self._width - (205 + 2 * x_inset), scroll_height), ) - ba.widget( + bui.widget( edit=self._scrollwidget, left_widget=self._new_button, - right_widget=ba.internal.get_special_widget('party_button') - if ba.app.ui.use_toolbars + right_widget=bui.get_special_widget('party_button') + if bui.app.classic.ui.use_toolbars else self._scrollwidget, ) - self._col = ba.columnwidget(parent=scrollwidget, border=2, margin=0) + self._col = bui.columnwidget(parent=scrollwidget, border=2, margin=0) self._soundtracks: dict[str, Any] | None = None self._selected_soundtrack: str | None = None self._selected_soundtrack_index: int | None = None - self._soundtrack_widgets: list[ba.Widget] = [] + self._soundtrack_widgets: list[bui.Widget] = [] self._allow_changing_soundtracks = False self._refresh() if self._back_button is not None: - ba.buttonwidget(edit=self._back_button, on_activate_call=self._back) - ba.containerwidget( + bui.buttonwidget( + edit=self._back_button, on_activate_call=self._back + ) + bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button ) else: - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, on_cancel_call=self._back ) def _update(self) -> None: - have = ba.app.accounts_v1.have_pro_options() + have = ( + bui.app.classic is None + or bui.app.classic.accounts.have_pro_options() + ) for lock in self._lock_images: - ba.imagewidget(edit=lock, opacity=0.0 if have else 1.0) + bui.imagewidget(edit=lock, opacity=0.0 if have else 1.0) def _do_delete_soundtrack(self) -> None: - cfg = ba.app.config + cfg = bui.app.config soundtracks = cfg.setdefault('Soundtracks', {}) if self._selected_soundtrack in soundtracks: del soundtracks[self._selected_soundtrack] cfg.commit() - ba.playsound(ba.getsound('shieldDown')) + bui.getsound('shieldDown').play() assert self._selected_soundtrack_index is not None assert self._soundtracks is not None if self._selected_soundtrack_index >= len(self._soundtracks): @@ -291,20 +295,23 @@ class SoundtrackBrowserWindow(ba.Window): from bastd.ui.purchase import PurchaseWindow from bastd.ui.confirm import ConfirmWindow - if not ba.app.accounts_v1.have_pro_options(): + if ( + bui.app.classic is not None + and not bui.app.classic.accounts.have_pro_options() + ): PurchaseWindow(items=['pro']) return if self._selected_soundtrack is None: return if self._selected_soundtrack == '__default__': - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource=self._r + '.cantDeleteDefaultText'), + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource=self._r + '.cantDeleteDefaultText'), color=(1, 0, 0), ) else: ConfirmWindow( - ba.Lstr( + bui.Lstr( resource=self._r + '.deleteConfirmText', subs=[('${NAME}', self._selected_soundtrack)], ), @@ -317,10 +324,13 @@ class SoundtrackBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.purchase import PurchaseWindow - if not ba.app.accounts_v1.have_pro_options(): + if ( + bui.app.classic is not None + and not bui.app.classic.accounts.have_pro_options() + ): PurchaseWindow(items=['pro']) return - cfg = ba.app.config + cfg = bui.app.config cfg.setdefault('Soundtracks', {}) if self._selected_soundtrack is None: @@ -333,7 +343,7 @@ class SoundtrackBrowserWindow(ba.Window): # Find a valid dup name that doesn't exist. test_index = 1 - copy_text = ba.Lstr(resource='copyOfText').evaluate() + copy_text = bui.Lstr(resource='copyOfText').evaluate() # Get just 'Copy' or whatnot. copy_word = copy_text.replace('${NAME}', '').strip() base_name = self._get_soundtrack_display_name( @@ -361,31 +371,35 @@ class SoundtrackBrowserWindow(ba.Window): self._refresh(select_soundtrack=test_name) def _select(self, name: str, index: int) -> None: - music = ba.app.music + assert bui.app.classic is not None + music = bui.app.classic.music self._selected_soundtrack_index = index self._selected_soundtrack = name - cfg = ba.app.config + cfg = bui.app.config current_soundtrack = cfg.setdefault('Soundtrack', '__default__') # If it varies from current, commit and play. if current_soundtrack != name and self._allow_changing_soundtracks: - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() cfg['Soundtrack'] = self._selected_soundtrack cfg.commit() # Just play whats already playing.. this'll grab it from the # new soundtrack. - music.do_play_music(music.music_types[ba.MusicPlayMode.REGULAR]) + music.do_play_music( + music.music_types[bui.app.classic.MusicPlayMode.REGULAR] + ) def _back(self) -> None: # pylint: disable=cyclic-import from bastd.ui.settings import audio self._save_state() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( audio.AudioSettingsWindow(transition='in_left').get_root_widget() ) @@ -393,10 +407,13 @@ class SoundtrackBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.purchase import PurchaseWindow - if not ba.app.accounts_v1.have_pro_options(): + if ( + bui.app.classic is not None + and not bui.app.classic.accounts.have_pro_options() + ): PurchaseWindow(items=['pro']) return - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._edit_soundtrack() def _edit_soundtrack(self) -> None: @@ -404,31 +421,35 @@ class SoundtrackBrowserWindow(ba.Window): from bastd.ui.purchase import PurchaseWindow from bastd.ui.soundtrack.edit import SoundtrackEditWindow - if not ba.app.accounts_v1.have_pro_options(): + if ( + bui.app.classic is not None + and not bui.app.classic.accounts.have_pro_options() + ): PurchaseWindow(items=['pro']) return if self._selected_soundtrack is None: return if self._selected_soundtrack == '__default__': - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource=self._r + '.cantEditDefaultText'), + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource=self._r + '.cantEditDefaultText'), color=(1, 0, 0), ) return self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( SoundtrackEditWindow( existing_soundtrack=self._selected_soundtrack ).get_root_widget() ) - def _get_soundtrack_display_name(self, soundtrack: str) -> ba.Lstr: + def _get_soundtrack_display_name(self, soundtrack: str) -> bui.Lstr: if soundtrack == '__default__': - return ba.Lstr(resource=self._r + '.defaultSoundtrackNameText') - return ba.Lstr(value=soundtrack) + return bui.Lstr(resource=self._r + '.defaultSoundtrackNameText') + return bui.Lstr(value=soundtrack) def _refresh(self, select_soundtrack: str | None = None) -> None: from efro.util import asserttype @@ -438,14 +459,14 @@ class SoundtrackBrowserWindow(ba.Window): # If there was no prev selection, look in prefs. if old_selection is None: - old_selection = ba.app.config.get('Soundtrack') + old_selection = bui.app.config.get('Soundtrack') old_selection_index = self._selected_soundtrack_index # Delete old. while self._soundtrack_widgets: self._soundtrack_widgets.pop().delete() - self._soundtracks = ba.app.config.get('Soundtracks', {}) + self._soundtracks = bui.app.config.get('Soundtracks', {}) assert self._soundtracks is not None items = list(self._soundtracks.items()) items.sort(key=lambda x: asserttype(x[0], str).lower()) @@ -453,7 +474,7 @@ class SoundtrackBrowserWindow(ba.Window): index = 0 for pname, _pval in items: assert pname is not None - txtw = ba.textwidget( + txtw = bui.textwidget( parent=self._col, size=(self._width - 40, 24), text=self._get_soundtrack_display_name(pname), @@ -461,18 +482,18 @@ class SoundtrackBrowserWindow(ba.Window): v_align='center', maxwidth=self._width - 110, always_highlight=True, - on_select_call=ba.WeakCall(self._select, pname, index), + on_select_call=bui.WeakCall(self._select, pname, index), on_activate_call=self._edit_soundtrack_with_sound, selectable=True, ) if index == 0: - ba.widget(edit=txtw, up_widget=self._back_button) + bui.widget(edit=txtw, up_widget=self._back_button) self._soundtrack_widgets.append(txtw) # Select this one if the user requested it if select_soundtrack is not None: if pname == select_soundtrack: - ba.columnwidget( + bui.columnwidget( edit=self._col, selected_child=txtw, visible_child=txtw ) else: @@ -480,14 +501,14 @@ class SoundtrackBrowserWindow(ba.Window): # Go by index if there's one. if old_selection_index is not None: if index == old_selection_index: - ba.columnwidget( + bui.columnwidget( edit=self._col, selected_child=txtw, visible_child=txtw, ) else: # Otherwise look by name. if pname == old_selection: - ba.columnwidget( + bui.columnwidget( edit=self._col, selected_child=txtw, visible_child=txtw, @@ -500,11 +521,7 @@ class SoundtrackBrowserWindow(ba.Window): # Eww need to run this in a timer so it happens after our select # callbacks. With a small-enough time sometimes it happens before # anyway. Ew. need a way to just schedule a callable i guess. - ba.timer( - 0.1, - ba.WeakCall(self._set_allow_changing), - timetype=ba.TimeType.REAL, - ) + bui.apptimer(0.1, bui.WeakCall(self._set_allow_changing)) def _set_allow_changing(self) -> None: self._allow_changing_soundtracks = True @@ -517,16 +534,19 @@ class SoundtrackBrowserWindow(ba.Window): from bastd.ui.purchase import PurchaseWindow from bastd.ui.soundtrack.edit import SoundtrackEditWindow - if not ba.app.accounts_v1.have_pro_options(): + if ( + bui.app.classic is not None + and not bui.app.classic.accounts.have_pro_options() + ): PurchaseWindow(items=['pro']) return self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.containerwidget(edit=self._root_widget, transition='out_left') SoundtrackEditWindow(existing_soundtrack=None) def _create_done(self, new_soundtrack: str) -> None: if new_soundtrack is not None: - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() self._refresh(select_soundtrack=new_soundtrack) def _save_state(self) -> None: @@ -546,13 +566,15 @@ class SoundtrackBrowserWindow(ba.Window): sel_name = 'Back' else: raise ValueError(f'unrecognized selection \'{sel}\'') - ba.app.ui.window_states[type(self)] = sel_name + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = sel_name except Exception: - ba.print_exception(f'Error saving state for {self}.') + logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: try: - sel_name = ba.app.ui.window_states.get(type(self)) + assert bui.app.classic is not None + sel_name = bui.app.classic.ui.window_states.get(type(self)) if sel_name == 'Scroll': sel = self._scrollwidget elif sel_name == 'New': @@ -565,6 +587,6 @@ class SoundtrackBrowserWindow(ba.Window): sel = self._delete_button else: sel = self._scrollwidget - ba.containerwidget(edit=self._root_widget, selected_child=sel) + bui.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: - ba.print_exception(f'Error restoring state for {self}.') + logging.exception('Error restoring state for %s.', self) diff --git a/assets/src/ba_data/python/bastd/ui/soundtrack/edit.py b/src/assets/ba_data/python/bastd/ui/soundtrack/edit.py similarity index 68% rename from assets/src/ba_data/python/bastd/ui/soundtrack/edit.py rename to src/assets/ba_data/python/bastd/ui/soundtrack/edit.py index cb8d27c4..14b0f747 100644 --- a/assets/src/ba_data/python/bastd/ui/soundtrack/edit.py +++ b/src/assets/ba_data/python/bastd/ui/soundtrack/edit.py @@ -8,13 +8,14 @@ import copy import os from typing import TYPE_CHECKING, cast -import ba +import bascenev1 as bs +import bauiv1 as bui if TYPE_CHECKING: from typing import Any -class SoundtrackEditWindow(ba.Window): +class SoundtrackEditWindow(bui.Window): """Window for editing a soundtrack.""" def __init__( @@ -23,61 +24,62 @@ class SoundtrackEditWindow(ba.Window): transition: str = 'in_right', ): # pylint: disable=too-many-statements - appconfig = ba.app.config + appconfig = bui.app.config self._r = 'editSoundtrackWindow' - self._folder_tex = ba.gettexture('folder') - self._file_tex = ba.gettexture('file') - uiscale = ba.app.ui.uiscale - self._width = 848 if uiscale is ba.UIScale.SMALL else 648 - x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + self._folder_tex = bui.gettexture('folder') + self._file_tex = bui.gettexture('file') + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self._width = 848 if uiscale is bui.UIScale.SMALL else 648 + x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 395 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 450 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 560 ) super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), transition=transition, scale=( 2.08 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.5 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -48) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 15) - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else (0, 0), ) ) - cancel_button = ba.buttonwidget( + cancel_button = bui.buttonwidget( parent=self._root_widget, position=(38 + x_inset, self._height - 60), size=(160, 60), autoselect=True, - label=ba.Lstr(resource='cancelText'), + label=bui.Lstr(resource='cancelText'), scale=0.8, ) - save_button = ba.buttonwidget( + save_button = bui.buttonwidget( parent=self._root_widget, position=(self._width - (168 + x_inset), self._height - 60), autoselect=True, size=(160, 60), - label=ba.Lstr(resource='saveText'), + label=bui.Lstr(resource='saveText'), scale=0.8, ) - ba.widget(edit=save_button, left_widget=cancel_button) - ba.widget(edit=cancel_button, right_widget=save_button) - ba.textwidget( + bui.widget(edit=save_button, left_widget=cancel_button) + bui.widget(edit=cancel_button, right_widget=save_button) + bui.textwidget( parent=self._root_widget, position=(0, self._height - 50), size=(self._width, 25), - text=ba.Lstr( + text=bui.Lstr( resource=self._r + ( '.editSoundtrackText' @@ -85,7 +87,7 @@ class SoundtrackEditWindow(ba.Window): else '.newSoundtrackText' ) ), - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, h_align='center', v_align='center', maxwidth=280, @@ -121,9 +123,9 @@ class SoundtrackEditWindow(ba.Window): self._soundtrack = {} self._last_edited_song_type = None - ba.textwidget( + bui.textwidget( parent=self._root_widget, - text=ba.Lstr(resource=self._r + '.nameText'), + text=bui.Lstr(resource=self._r + '.nameText'), maxwidth=80, scale=0.8, position=(105 + x_inset, v + 19), @@ -136,7 +138,7 @@ class SoundtrackEditWindow(ba.Window): # if there's no initial value, find a good initial unused name if existing_soundtrack is None: i = 1 - st_name_text = ba.Lstr( + st_name_text = bui.Lstr( resource=self._r + '.newSoundtrackNameText' ).evaluate() if '${COUNT}' not in st_name_text: @@ -148,7 +150,7 @@ class SoundtrackEditWindow(ba.Window): break i += 1 - self._text_field = ba.textwidget( + self._text_field = bui.textwidget( parent=self._root_widget, position=(120 + x_inset, v - 5), size=(self._width - (160 + 2 * x_inset), 43), @@ -157,14 +159,14 @@ class SoundtrackEditWindow(ba.Window): v_align='center', max_chars=32, autoselect=True, - description=ba.Lstr(resource=self._r + '.nameText'), + description=bui.Lstr(resource=self._r + '.nameText'), editable=True, padding=4, on_return_press_call=self._do_it_with_sound, ) scroll_height = self._height - 180 - self._scrollwidget = scrollwidget = ba.scrollwidget( + self._scrollwidget = scrollwidget = bui.scrollwidget( parent=self._root_widget, highlight=False, position=(40 + x_inset, v - (scroll_height + 10)), @@ -174,22 +176,22 @@ class SoundtrackEditWindow(ba.Window): claims_tab=True, selection_loops_to_parent=True, ) - ba.widget(edit=self._text_field, down_widget=self._scrollwidget) - self._col = ba.columnwidget( + bui.widget(edit=self._text_field, down_widget=self._scrollwidget) + self._col = bui.columnwidget( parent=scrollwidget, claims_left_right=True, claims_tab=True, selection_loops_to_parent=True, ) - self._song_type_buttons: dict[str, ba.Widget] = {} + self._song_type_buttons: dict[str, bui.Widget] = {} self._refresh() - ba.buttonwidget(edit=cancel_button, on_activate_call=self._cancel) - ba.containerwidget(edit=self._root_widget, cancel_button=cancel_button) - ba.buttonwidget(edit=save_button, on_activate_call=self._do_it) - ba.containerwidget(edit=self._root_widget, start_button=save_button) - ba.widget(edit=self._text_field, up_widget=cancel_button) - ba.widget(edit=cancel_button, down_widget=self._text_field) + bui.buttonwidget(edit=cancel_button, on_activate_call=self._cancel) + bui.containerwidget(edit=self._root_widget, cancel_button=cancel_button) + bui.buttonwidget(edit=save_button, on_activate_call=self._do_it) + bui.containerwidget(edit=self._root_widget, start_button=save_button) + bui.widget(edit=self._text_field, up_widget=cancel_button) + bui.widget(edit=cancel_button, down_widget=self._text_field) def _refresh(self) -> None: for widget in self._col.get_children(): @@ -219,12 +221,12 @@ class SoundtrackEditWindow(ba.Window): ] # FIXME: We should probably convert this to use translations. - type_names_translated = ba.app.lang.get_resource('soundtrackTypeNames') - prev_type_button: ba.Widget | None = None - prev_test_button: ba.Widget | None = None + type_names_translated = bui.app.lang.get_resource('soundtrackTypeNames') + prev_type_button: bui.Widget | None = None + prev_test_button: bui.Widget | None = None for index, song_type in enumerate(types): - row = ba.rowwidget( + row = bui.rowwidget( parent=self._col, size=(self._width - 40, 40), claims_left_right=True, @@ -232,7 +234,7 @@ class SoundtrackEditWindow(ba.Window): selection_loops_to_parent=True, ) type_name = type_names_translated.get(song_type, song_type) - ba.textwidget( + bui.textwidget( parent=row, size=(230, 25), always_highlight=True, @@ -253,12 +255,12 @@ class SoundtrackEditWindow(ba.Window): entry = copy.deepcopy(entry) icon_type = self._get_entry_button_display_icon_type(entry) - self._song_type_buttons[song_type] = btn = ba.buttonwidget( + self._song_type_buttons[song_type] = btn = bui.buttonwidget( parent=row, size=(230, 32), label=self._get_entry_button_display_name(entry), text_scale=0.6, - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._get_entry, song_type, entry, type_name ), icon=( @@ -277,54 +279,56 @@ class SoundtrackEditWindow(ba.Window): up_widget=prev_type_button, ) if index == 0: - ba.widget(edit=btn, up_widget=self._text_field) - ba.widget(edit=btn, down_widget=btn) + bui.widget(edit=btn, up_widget=self._text_field) + bui.widget(edit=btn, down_widget=btn) if ( self._last_edited_song_type is not None and song_type == self._last_edited_song_type ): - ba.containerwidget( + bui.containerwidget( edit=row, selected_child=btn, visible_child=btn ) - ba.containerwidget( + bui.containerwidget( edit=self._col, selected_child=row, visible_child=row ) - ba.containerwidget( + bui.containerwidget( edit=self._scrollwidget, selected_child=self._col, visible_child=self._col, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget, visible_child=self._scrollwidget, ) if prev_type_button is not None: - ba.widget(edit=prev_type_button, down_widget=btn) + bui.widget(edit=prev_type_button, down_widget=btn) prev_type_button = btn - ba.textwidget(parent=row, size=(10, 32), text='') # spacing - btn = ba.buttonwidget( + bui.textwidget(parent=row, size=(10, 32), text='') # spacing + assert bui.app.classic is not None + btn = bui.buttonwidget( parent=row, size=(50, 32), - label=ba.Lstr(resource=self._r + '.testText'), + label=bui.Lstr(resource=self._r + '.testText'), text_scale=0.6, - on_activate_call=ba.Call(self._test, ba.MusicType(song_type)), + on_activate_call=bui.Call(self._test, bs.MusicType(song_type)), up_widget=prev_test_button if prev_test_button is not None else self._text_field, ) if prev_test_button is not None: - ba.widget(edit=prev_test_button, down_widget=btn) - ba.widget(edit=btn, down_widget=btn, right_widget=btn) + bui.widget(edit=prev_test_button, down_widget=btn) + bui.widget(edit=btn, down_widget=btn, right_widget=btn) prev_test_button = btn @classmethod def _restore_editor( cls, state: dict[str, Any], musictype: str, entry: Any ) -> None: - music = ba.app.music + assert bui.app.classic is not None + music = bui.app.classic.music # Apply the change and recreate the window. soundtrack = state['soundtrack'] @@ -332,7 +336,7 @@ class SoundtrackEditWindow(ba.Window): None if musictype not in soundtrack else soundtrack[musictype] ) if existing_entry != entry: - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() # Make sure this doesn't get mucked with after we get it. if entry is not None: @@ -346,14 +350,15 @@ class SoundtrackEditWindow(ba.Window): else: soundtrack[musictype] = entry - ba.app.ui.set_main_menu_window( + bui.app.classic.ui.set_main_menu_window( cls(state, transition='in_left').get_root_widget() ) def _get_entry( self, song_type: str, entry: Any, selection_target_name: str ) -> None: - music = ba.app.music + assert bui.app.classic is not None + music = bui.app.classic.music if selection_target_name != '': selection_target_name = "'" + selection_target_name + "'" state = { @@ -362,40 +367,42 @@ class SoundtrackEditWindow(ba.Window): 'soundtrack': self._soundtrack, 'last_edited_song_type': song_type, } - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + bui.app.classic.ui.set_main_menu_window( music.get_music_player() .select_entry( - ba.Call(self._restore_editor, state, song_type), + bui.Call(self._restore_editor, state, song_type), entry, selection_target_name, ) .get_root_widget() ) - def _test(self, song_type: ba.MusicType) -> None: - music = ba.app.music + def _test(self, song_type: bs.MusicType) -> None: + assert bui.app.classic is not None + music = bui.app.classic.music # Warn if volume is zero. - if ba.app.config.resolve('Music Volume') < 0.01: - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource=self._r + '.musicVolumeZeroWarning'), + if bui.app.config.resolve('Music Volume') < 0.01: + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource=self._r + '.musicVolumeZeroWarning'), color=(1, 0.5, 0), ) - music.set_music_play_mode(ba.MusicPlayMode.TEST) + music.set_music_play_mode(bui.app.classic.MusicPlayMode.TEST) music.do_play_music( song_type, - mode=ba.MusicPlayMode.TEST, + mode=bui.app.classic.MusicPlayMode.TEST, testsoundtrack=self._soundtrack, ) - def _get_entry_button_display_name(self, entry: Any) -> str | ba.Lstr: - music = ba.app.music + def _get_entry_button_display_name(self, entry: Any) -> str | bui.Lstr: + assert bui.app.classic is not None + music = bui.app.classic.music etype = music.get_soundtrack_entry_type(entry) - ename: str | ba.Lstr + ename: str | bui.Lstr if etype == 'default': - ename = ba.Lstr(resource=self._r + '.defaultGameMusicText') + ename = bui.Lstr(resource=self._r + '.defaultGameMusicText') elif etype in ('musicFile', 'musicFolder'): ename = os.path.basename(music.get_soundtrack_entry_name(entry)) else: @@ -403,7 +410,8 @@ class SoundtrackEditWindow(ba.Window): return ename def _get_entry_button_display_icon_type(self, entry: Any) -> str | None: - music = ba.app.music + assert bui.app.classic is not None + music = bui.app.classic.music etype = music.get_soundtrack_entry_type(entry) if etype == 'musicFile': return 'file' @@ -414,40 +422,42 @@ class SoundtrackEditWindow(ba.Window): def _cancel(self) -> None: from bastd.ui.soundtrack import browser as stb - music = ba.app.music + assert bui.app.classic is not None + music = bui.app.classic.music # Resets music back to normal. - music.set_music_play_mode(ba.MusicPlayMode.REGULAR) - ba.containerwidget(edit=self._root_widget, transition='out_right') - ba.app.ui.set_main_menu_window( + music.set_music_play_mode(bui.app.classic.MusicPlayMode.REGULAR) + bui.containerwidget(edit=self._root_widget, transition='out_right') + bui.app.classic.ui.set_main_menu_window( stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget() ) def _do_it(self) -> None: from bastd.ui.soundtrack import browser as stb - music = ba.app.music - cfg = ba.app.config - new_name = cast(str, ba.textwidget(query=self._text_field)) + assert bui.app.classic is not None + music = bui.app.classic.music + cfg = bui.app.config + new_name = cast(str, bui.textwidget(query=self._text_field)) if new_name != self._soundtrack_name and new_name in cfg['Soundtracks']: - ba.screenmessage( - ba.Lstr(resource=self._r + '.cantSaveAlreadyExistsText') + bui.screenmessage( + bui.Lstr(resource=self._r + '.cantSaveAlreadyExistsText') ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return if not new_name: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return if ( new_name - == ba.Lstr( + == bui.Lstr( resource=self._r + '.defaultSoundtrackNameText' ).evaluate() ): - ba.screenmessage( - ba.Lstr(resource=self._r + '.cantOverwriteDefaultText') + bui.screenmessage( + bui.Lstr(resource=self._r + '.cantOverwriteDefaultText') ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return # Make sure config exists. @@ -464,16 +474,18 @@ class SoundtrackEditWindow(ba.Window): cfg['Soundtrack'] = new_name cfg.commit() - ba.playsound(ba.getsound('gunCocking')) - ba.containerwidget(edit=self._root_widget, transition='out_right') + bui.getsound('gunCocking').play() + bui.containerwidget(edit=self._root_widget, transition='out_right') # Resets music back to normal. - music.set_music_play_mode(ba.MusicPlayMode.REGULAR, force_restart=True) + music.set_music_play_mode( + bui.app.classic.MusicPlayMode.REGULAR, force_restart=True + ) - ba.app.ui.set_main_menu_window( + bui.app.classic.ui.set_main_menu_window( stb.SoundtrackBrowserWindow(transition='in_left').get_root_widget() ) def _do_it_with_sound(self) -> None: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._do_it() diff --git a/assets/src/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py b/src/assets/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py similarity index 70% rename from assets/src/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py rename to src/assets/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py index 21e6137f..e7f9d84a 100644 --- a/assets/src/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py +++ b/src/assets/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py @@ -6,14 +6,13 @@ from __future__ import annotations import copy from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable -class SoundtrackEntryTypeSelectWindow(ba.Window): +class SoundtrackEntryTypeSelectWindow(bui.Window): """Window for selecting a soundtrack entry type.""" def __init__( @@ -23,7 +22,8 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): selection_target_name: str, transition: str = 'in_right', ): - music = ba.app.music + assert bui.app.classic is not None + music = bui.app.classic.music self._r = 'editSoundtrackWindow' self._callback = callback @@ -49,7 +49,7 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): if do_music_folder: self._height += spacing - uiscale = ba.app.ui.uiscale + uiscale = bui.app.classic.ui.uiscale # NOTE: When something is selected, we close our UI and kick off # another window which then calls us back when its done, so the @@ -57,46 +57,46 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): # to our instance after its ui is gone. Should restructure in a # cleaner way, but just disabling that check for now. super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), transition=transition, scale=( 1.7 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.4 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), ), cleanupcheck=False, ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=(35, self._height - 65), size=(160, 60), scale=0.8, text_scale=1.2, - label=ba.Lstr(resource='cancelText'), + label=bui.Lstr(resource='cancelText'), on_activate_call=self._on_cancel_press, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) - ba.textwidget( + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 32), size=(0, 0), - text=ba.Lstr(resource=self._r + '.selectASourceText'), - color=ba.app.ui.title_color, + text=bui.Lstr(resource=self._r + '.selectASourceText'), + color=bui.app.classic.ui.title_color, maxwidth=230, h_align='center', v_align='center', ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 56), size=(0, 0), text=selection_target_name, - color=ba.app.ui.infotextcolor, + color=bui.app.classic.ui.infotextcolor, scale=0.7, maxwidth=230, h_align='center', @@ -108,64 +108,65 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): current_entry_type = music.get_soundtrack_entry_type(current_entry) if do_default: - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, size=(self._width - 100, 60), position=(50, v), - label=ba.Lstr(resource=self._r + '.useDefaultGameMusicText'), + label=bui.Lstr(resource=self._r + '.useDefaultGameMusicText'), on_activate_call=self._on_default_press, ) if current_entry_type == 'default': - ba.containerwidget(edit=self._root_widget, selected_child=btn) + bui.containerwidget(edit=self._root_widget, selected_child=btn) v -= spacing if do_mac_music_app_playlist: - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, size=(self._width - 100, 60), position=(50, v), - label=ba.Lstr(resource=self._r + '.useITunesPlaylistText'), + label=bui.Lstr(resource=self._r + '.useITunesPlaylistText'), on_activate_call=self._on_mac_music_app_playlist_press, icon=None, ) if current_entry_type == 'iTunesPlaylist': - ba.containerwidget(edit=self._root_widget, selected_child=btn) + bui.containerwidget(edit=self._root_widget, selected_child=btn) v -= spacing if do_music_file: - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, size=(self._width - 100, 60), position=(50, v), - label=ba.Lstr(resource=self._r + '.useMusicFileText'), + label=bui.Lstr(resource=self._r + '.useMusicFileText'), on_activate_call=self._on_music_file_press, - icon=ba.gettexture('file'), + icon=bui.gettexture('file'), ) if current_entry_type == 'musicFile': - ba.containerwidget(edit=self._root_widget, selected_child=btn) + bui.containerwidget(edit=self._root_widget, selected_child=btn) v -= spacing if do_music_folder: - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, size=(self._width - 100, 60), position=(50, v), - label=ba.Lstr(resource=self._r + '.useMusicFolderText'), + label=bui.Lstr(resource=self._r + '.useMusicFolderText'), on_activate_call=self._on_music_folder_press, - icon=ba.gettexture('folder'), + icon=bui.gettexture('folder'), icon_color=(1.1, 0.8, 0.2), ) if current_entry_type == 'musicFolder': - ba.containerwidget(edit=self._root_widget, selected_child=btn) + bui.containerwidget(edit=self._root_widget, selected_child=btn) v -= spacing def _on_mac_music_app_playlist_press(self) -> None: - music = ba.app.music + assert bui.app.classic is not None + music = bui.app.classic.music from bastd.ui.soundtrack.macmusicapp import ( MacMusicAppPlaylistSelectWindow, ) - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.containerwidget(edit=self._root_widget, transition='out_left') current_playlist_entry: str | None if ( @@ -177,19 +178,21 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): ) else: current_playlist_entry = None - ba.app.ui.set_main_menu_window( + bui.app.classic.ui.set_main_menu_window( MacMusicAppPlaylistSelectWindow( self._callback, current_playlist_entry, self._current_entry ).get_root_widget() ) def _on_music_file_press(self) -> None: - from ba.osmusic import OSMusicPlayer + from babase.internal import android_get_external_files_dir + from baclassic.osmusic import OSMusicPlayer from bastd.ui.fileselector import FileSelectorWindow - ba.containerwidget(edit=self._root_widget, transition='out_left') - base_path = ba.internal.android_get_external_files_dir() - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + base_path = android_get_external_files_dir() + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( FileSelectorWindow( base_path, callback=self._music_file_selector_cb, @@ -203,10 +206,12 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): def _on_music_folder_press(self) -> None: from bastd.ui.fileselector import FileSelectorWindow + from babase.internal import android_get_external_files_dir - ba.containerwidget(edit=self._root_widget, transition='out_left') - base_path = ba.internal.android_get_external_files_dir() - ba.app.ui.set_main_menu_window( + bui.containerwidget(edit=self._root_widget, transition='out_left') + base_path = android_get_external_files_dir() + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( FileSelectorWindow( base_path, callback=self._music_folder_selector_cb, @@ -229,9 +234,9 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): self._callback({'type': 'musicFolder', 'name': result}) def _on_default_press(self) -> None: - ba.containerwidget(edit=self._root_widget, transition='out_right') + bui.containerwidget(edit=self._root_widget, transition='out_right') self._callback(None) def _on_cancel_press(self) -> None: - ba.containerwidget(edit=self._root_widget, transition='out_right') + bui.containerwidget(edit=self._root_widget, transition='out_right') self._callback(self._current_entry) diff --git a/assets/src/ba_data/python/bastd/ui/soundtrack/macmusicapp.py b/src/assets/ba_data/python/bastd/ui/soundtrack/macmusicapp.py similarity index 68% rename from assets/src/ba_data/python/bastd/ui/soundtrack/macmusicapp.py rename to src/assets/ba_data/python/bastd/ui/soundtrack/macmusicapp.py index c564018b..c2e07403 100644 --- a/assets/src/ba_data/python/bastd/ui/soundtrack/macmusicapp.py +++ b/src/assets/ba_data/python/bastd/ui/soundtrack/macmusicapp.py @@ -7,13 +7,13 @@ from __future__ import annotations import copy from typing import TYPE_CHECKING -import ba +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable -class MacMusicAppPlaylistSelectWindow(ba.Window): +class MacMusicAppPlaylistSelectWindow(bui.Window): """Window for selecting an iTunes playlist.""" def __init__( @@ -22,7 +22,7 @@ class MacMusicAppPlaylistSelectWindow(ba.Window): existing_playlist: str | None, existing_entry: Any, ): - from ba.macmusicapp import MacMusicAppMusicPlayer + from baclassic.macmusicapp import MacMusicAppMusicPlayer self._r = 'editSoundtrackWindow' self._callback = callback @@ -34,54 +34,56 @@ class MacMusicAppPlaylistSelectWindow(ba.Window): v = self._height - 90.0 v -= self._spacing * 1.0 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), transition='in_right' ) ) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=(35, self._height - 65), size=(130, 50), - label=ba.Lstr(resource='cancelText'), + label=bui.Lstr(resource='cancelText'), on_activate_call=self._back, autoselect=True, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) - ba.textwidget( + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + assert bui.app.classic is not None + bui.textwidget( parent=self._root_widget, position=(20, self._height - 54), size=(self._width, 25), - text=ba.Lstr(resource=self._r + '.selectAPlaylistText'), - color=ba.app.ui.title_color, + text=bui.Lstr(resource=self._r + '.selectAPlaylistText'), + color=bui.app.classic.ui.title_color, h_align='center', v_align='center', maxwidth=200, ) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, position=(40, v - 340), size=(self._width - 80, 400), claims_tab=True, selection_loops_to_parent=True, ) - ba.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) - self._column = ba.columnwidget( + bui.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) + self._column = bui.columnwidget( parent=self._scrollwidget, claims_tab=True, selection_loops_to_parent=True, ) - ba.textwidget( + bui.textwidget( parent=self._column, size=(self._width - 80, 22), - text=ba.Lstr(resource=self._r + '.fetchingITunesText'), + text=bui.Lstr(resource=self._r + '.fetchingITunesText'), color=(0.6, 0.9, 0.6, 1.0), scale=0.8, ) - musicplayer = ba.app.music.get_music_player() + assert bui.app.classic is not None + musicplayer = bui.app.classic.music.get_music_player() assert isinstance(musicplayer, MacMusicAppMusicPlayer) musicplayer.get_playlists(self._playlists_cb) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) @@ -90,29 +92,29 @@ class MacMusicAppPlaylistSelectWindow(ba.Window): for widget in self._column.get_children(): widget.delete() for i, playlist in enumerate(playlists): - txt = ba.textwidget( + txt = bui.textwidget( parent=self._column, size=(self._width - 80, 30), text=playlist, v_align='center', maxwidth=self._width - 110, selectable=True, - on_activate_call=ba.Call(self._sel, playlist), + on_activate_call=bui.Call(self._sel, playlist), click_activate=True, ) - ba.widget(edit=txt, show_buffer_top=40, show_buffer_bottom=40) + bui.widget(edit=txt, show_buffer_top=40, show_buffer_bottom=40) if playlist == self._existing_playlist: - ba.columnwidget( + bui.columnwidget( edit=self._column, selected_child=txt, visible_child=txt ) if i == len(playlists) - 1: - ba.widget(edit=txt, down_widget=txt) + bui.widget(edit=txt, down_widget=txt) def _sel(self, selection: str) -> None: if self._root_widget: - ba.containerwidget(edit=self._root_widget, transition='out_right') + bui.containerwidget(edit=self._root_widget, transition='out_right') self._callback({'type': 'iTunesPlaylist', 'name': selection}) def _back(self) -> None: - ba.containerwidget(edit=self._root_widget, transition='out_right') + bui.containerwidget(edit=self._root_widget, transition='out_right') self._callback(self._existing_entry) diff --git a/assets/src/ba_data/python/bastd/ui/specialoffer.py b/src/assets/ba_data/python/bastd/ui/specialoffer.py similarity index 71% rename from assets/src/ba_data/python/bastd/ui/specialoffer.py rename to src/assets/ba_data/python/bastd/ui/specialoffer.py index b2f864ad..a7fa6777 100644 --- a/assets/src/ba_data/python/bastd/ui/specialoffer.py +++ b/src/assets/ba_data/python/bastd/ui/specialoffer.py @@ -5,26 +5,31 @@ from __future__ import annotations import copy +import logging from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any -class SpecialOfferWindow(ba.Window): +class SpecialOfferWindow(bui.Window): """Window for presenting sales/etc.""" def __init__(self, offer: dict[str, Any], transition: str = 'in_right'): # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals - from ba.internal import get_store_item_display_size, get_clean_price - from ba import SpecialChar + from babase import SpecialChar from bastd.ui.store import item as storeitemui + plus = bui.app.plus + assert plus is not None + + assert bui.app.classic is not None + store = bui.app.classic.store + self._cancel_delay = offer.get('cancelDelay', 0) # First thing: if we're offering pro or an IAP, see if we have a @@ -36,17 +41,17 @@ class SpecialOfferWindow(ba.Window): # Misnomer: 'pro' actually means offer 'pro_sale'. if offer['item'] in ['pro', 'pro_fullprice']: - real_price = ba.internal.get_price( + real_price = plus.get_price( 'pro' if offer['item'] == 'pro_fullprice' else 'pro_sale' ) - if real_price is None and ba.app.debug_build: + if real_price is None and bui.app.debug_build: print('NOTE: Faking prices for debug build.') real_price = '$1.23' zombie = real_price is None elif isinstance(offer['price'], str): # (a string price implies IAP id) - real_price = ba.internal.get_price(offer['price']) - if real_price is None and ba.app.debug_build: + real_price = plus.get_price(offer['price']) + if real_price is None and bui.app.debug_build: print('NOTE: Faking price for debug build.') real_price = '$1.23' zombie = real_price is None @@ -66,41 +71,37 @@ class SpecialOfferWindow(ba.Window): return # This can pop up suddenly, so lets block input for 1 second. - ba.internal.lock_all_input() - ba.timer(1.0, ba.internal.unlock_all_input, timetype=ba.TimeType.REAL) - ba.playsound(ba.getsound('ding')) - ba.timer( - 0.3, - lambda: ba.playsound(ba.getsound('ooh')), - timetype=ba.TimeType.REAL, - ) + bui.lock_all_input() + bui.apptimer(1.0, bui.unlock_all_input) + bui.getsound('ding').play() + bui.apptimer(0.3, bui.getsound('ooh').play) self._offer = copy.deepcopy(offer) self._width = 580 self._height = 590 - uiscale = ba.app.ui.uiscale + uiscale = bui.app.classic.ui.uiscale super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height), transition=transition, scale=( 1.2 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.15 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -15) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0), ) ) self._is_bundle_sale = False try: if offer['item'] in ['pro', 'pro_fullprice']: - original_price_str = ba.internal.get_price('pro') + original_price_str = plus.get_price('pro') if original_price_str is None: original_price_str = '?' - new_price_str = ba.internal.get_price('pro_sale') + new_price_str = plus.get_price('pro_sale') if new_price_str is None: new_price_str = '?' percent_off_text = '' @@ -111,14 +112,14 @@ class SpecialOfferWindow(ba.Window): and offer['bonusTickets'] is not None ): self._is_bundle_sale = True - original_price = ba.internal.get_v1_account_misc_read_val( + original_price = plus.get_v1_account_misc_read_val( 'price.' + self._offer_item, 9999 ) # For pure ticket prices we can show a percent-off. if isinstance(offer['price'], int): new_price = offer['price'] - tchar = ba.charstr(SpecialChar.TICKET) + tchar = bui.charstr(SpecialChar.TICKET) original_price_str = tchar + str(original_price) new_price_str = tchar + str(new_price) percent_off = int( @@ -126,7 +127,7 @@ class SpecialOfferWindow(ba.Window): 100.0 - (float(new_price) / original_price) * 100.0 ) ) - percent_off_text = ' ' + ba.Lstr( + percent_off_text = ' ' + bui.Lstr( resource='store.salePercentText' ).evaluate().replace('${PERCENT}', str(percent_off)) else: @@ -134,37 +135,36 @@ class SpecialOfferWindow(ba.Window): percent_off_text = '' except Exception: - print(f'Offer: {offer}') - ba.print_exception('Error setting up special-offer') + logging.exception('Error setting up special-offer: %s.', offer) original_price_str = new_price_str = '?' percent_off_text = '' # If its a bundle sale, change the title. if self._is_bundle_sale: - sale_text = ba.Lstr( + sale_text = bui.Lstr( resource='store.saleBundleText', fallback_resource='store.saleText', ).evaluate() else: # For full pro we say 'Upgrade?' since its not really a sale. if offer['item'] == 'pro_fullprice': - sale_text = ba.Lstr( + sale_text = bui.Lstr( resource='store.upgradeQuestionText', fallback_resource='store.saleExclaimText', ).evaluate() else: - sale_text = ba.Lstr( + sale_text = bui.Lstr( resource='store.saleExclaimText', fallback_resource='store.saleText', ).evaluate() - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 40), size=(0, 0), text=sale_text + ( - (' ' + ba.Lstr(resource='store.oneTimeOnlyText').evaluate()) + (' ' + bui.Lstr(resource='store.oneTimeOnlyText').evaluate()) if self._offer['oneTimeOnly'] else '' ) @@ -177,17 +177,12 @@ class SpecialOfferWindow(ba.Window): ) self._flash_on = False - self._flashing_timer: ba.Timer | None = ba.Timer( - 0.05, - ba.WeakCall(self._flash_cycle), - repeat=True, - timetype=ba.TimeType.REAL, - ) - ba.timer( - 0.6, ba.WeakCall(self._stop_flashing), timetype=ba.TimeType.REAL + self._flashing_timer: bui.AppTimer | None = bui.AppTimer( + 0.05, bui.WeakCall(self._flash_cycle), repeat=True ) + bui.apptimer(0.6, bui.WeakCall(self._stop_flashing)) - size = get_store_item_display_size(self._offer_item) + size = store.get_store_item_display_size(self._offer_item) display: dict[str, Any] = {} storeitemui.instantiate_store_item_display( self._offer_item, @@ -210,7 +205,7 @@ class SpecialOfferWindow(ba.Window): # Wire up the parts we need. if self._is_bundle_sale: - self._plus_text = ba.textwidget( + self._plus_text = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.5 + 50), size=(0, 0), @@ -221,11 +216,11 @@ class SpecialOfferWindow(ba.Window): scale=1.4, color=(0.5, 0.5, 0.5), ) - self._plus_tickets = ba.textwidget( + self._plus_tickets = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5 + 120, self._height * 0.5 + 50), size=(0, 0), - text=ba.charstr(SpecialChar.TICKET_BACKING) + text=bui.charstr(SpecialChar.TICKET_BACKING) + str(offer['bonusTickets']), h_align='center', v_align='center', @@ -233,7 +228,7 @@ class SpecialOfferWindow(ba.Window): scale=2.5, color=(0.2, 1, 0.2), ) - self._price_text = ba.textwidget( + self._price_text = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, 150), size=(0, 0), @@ -247,16 +242,16 @@ class SpecialOfferWindow(ba.Window): # Total-value if they supplied it. total_worth_item = offer.get('valueItem', None) if total_worth_item is not None: - price = ba.internal.get_price(total_worth_item) + price = plus.get_price(total_worth_item) total_worth_price = ( - get_clean_price(price) if price is not None else None + store.get_clean_price(price) if price is not None else None ) if total_worth_price is not None: - total_worth_text = ba.Lstr( + total_worth_text = bui.Lstr( resource='store.totalWorthText', subs=[('${TOTAL_WORTH}', total_worth_price)], ) - self._total_worth_text = ba.textwidget( + self._total_worth_text = bui.textwidget( parent=self._root_widget, text=total_worth_text, position=(self._width * 0.5, 210), @@ -272,26 +267,26 @@ class SpecialOfferWindow(ba.Window): elif offer['item'] == 'pro_fullprice': # for full-price pro we simply show full price - ba.textwidget(edit=display['price_widget'], text=real_price) - ba.buttonwidget( + bui.textwidget(edit=display['price_widget'], text=real_price) + bui.buttonwidget( edit=display['button'], on_activate_call=self._purchase ) else: # Show old/new prices otherwise (for pro sale). - ba.buttonwidget( + bui.buttonwidget( edit=display['button'], on_activate_call=self._purchase ) - ba.imagewidget(edit=display['price_slash_widget'], opacity=1.0) - ba.textwidget( + bui.imagewidget(edit=display['price_slash_widget'], opacity=1.0) + bui.textwidget( edit=display['price_widget_left'], text=original_price_str ) - ba.textwidget( + bui.textwidget( edit=display['price_widget_right'], text=new_price_str ) # Add ticket button only if this is ticket-purchasable. if isinstance(offer.get('price'), int): - self._get_tickets_button = ba.buttonwidget( + self._get_tickets_button = bui.buttonwidget( parent=self._root_widget, position=(self._width - 125, self._height - 68), size=(90, 55), @@ -300,26 +295,20 @@ class SpecialOfferWindow(ba.Window): color=(0.7, 0.5, 0.85), textcolor=(0.2, 1, 0.2), autoselect=True, - label=ba.Lstr(resource='getTicketsWindow.titleText'), + label=bui.Lstr(resource='getTicketsWindow.titleText'), on_activate_call=self._on_get_more_tickets_press, ) - self._ticket_text_update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update_tickets_text), - timetype=ba.TimeType.REAL, - repeat=True, + self._ticket_text_update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update_tickets_text), repeat=True ) self._update_tickets_text() - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - timetype=ba.TimeType.REAL, - repeat=True, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self._root_widget, position=(50, 40) if self._is_bundle_sale @@ -328,9 +317,9 @@ class SpecialOfferWindow(ba.Window): scale=1.0, on_activate_call=self._cancel, autoselect=True, - label=ba.Lstr(resource='noThanksText'), + label=bui.Lstr(resource='noThanksText'), ) - self._cancel_countdown_text = ba.textwidget( + self._cancel_countdown_text = bui.textwidget( parent=self._root_widget, text='', position=(50 + 150 + 20, 40 + 27) @@ -347,17 +336,17 @@ class SpecialOfferWindow(ba.Window): self._update_cancel_button_graphics() if self._is_bundle_sale: - self._purchase_button = ba.buttonwidget( + self._purchase_button = bui.buttonwidget( parent=self._root_widget, position=(self._width - 200, 40), size=(150, 60), scale=1.0, on_activate_call=self._purchase, autoselect=True, - label=ba.Lstr(resource='store.purchaseText'), + label=bui.Lstr(resource='store.purchaseText'), ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, cancel_button=self._cancel_button, start_button=self._purchase_button @@ -370,19 +359,19 @@ class SpecialOfferWindow(ba.Window): def _stop_flashing(self) -> None: self._flashing_timer = None - ba.textwidget(edit=self._title_text, color=(0.3, 1, 0.3)) + bui.textwidget(edit=self._title_text, color=(0.3, 1, 0.3)) def _flash_cycle(self) -> None: if not self._root_widget: return self._flash_on = not self._flash_on - ba.textwidget( + bui.textwidget( edit=self._title_text, color=(0.3, 1, 0.3) if self._flash_on else (1, 0.5, 0), ) def _update_cancel_button_graphics(self) -> None: - ba.buttonwidget( + bui.buttonwidget( edit=self._cancel_button, color=(0.5, 0.5, 0.5) if self._cancel_delay > 0 @@ -391,12 +380,14 @@ class SpecialOfferWindow(ba.Window): if self._cancel_delay > 0 else (0.9, 0.9, 1.0), ) - ba.textwidget( + bui.textwidget( edit=self._cancel_countdown_text, text=str(self._cancel_delay) if self._cancel_delay > 0 else '', ) def _update(self) -> None: + plus = bui.app.plus + assert plus is not None # If we've got seconds left on our countdown, update it. if self._cancel_delay > 0: @@ -407,10 +398,11 @@ class SpecialOfferWindow(ba.Window): # We go away if we see that our target item is owned. if self._offer_item == 'pro': - if ba.app.accounts_v1.have_pro(): + assert bui.app.classic is not None + if bui.app.classic.accounts.have_pro(): can_die = True else: - if ba.internal.get_purchased(self._offer_item): + if plus.get_purchased(self._offer_item): can_die = True if can_die: @@ -418,86 +410,102 @@ class SpecialOfferWindow(ba.Window): def _transition_out(self, transition: str = 'out_left') -> None: # Also clear any pending-special-offer we've stored at this point. - cfg = ba.app.config + cfg = bui.app.config if 'pendingSpecialOffer' in cfg: del cfg['pendingSpecialOffer'] cfg.commit() - ba.containerwidget(edit=self._root_widget, transition=transition) + bui.containerwidget(edit=self._root_widget, transition=transition) def _update_tickets_text(self) -> None: - from ba import SpecialChar + from babase import SpecialChar + + plus = bui.app.plus + assert plus is not None if not self._root_widget: return - sval: str | ba.Lstr - if ba.internal.get_v1_account_state() == 'signed_in': - sval = ba.charstr(SpecialChar.TICKET) + str( - ba.internal.get_v1_account_ticket_count() + sval: str | bui.Lstr + if plus.get_v1_account_state() == 'signed_in': + sval = bui.charstr(SpecialChar.TICKET) + str( + plus.get_v1_account_ticket_count() ) else: - sval = ba.Lstr(resource='getTicketsWindow.titleText') - ba.buttonwidget(edit=self._get_tickets_button, label=sval) + sval = bui.Lstr(resource='getTicketsWindow.titleText') + bui.buttonwidget(edit=self._get_tickets_button, label=sval) def _on_get_more_tickets_press(self) -> None: from bastd.ui import account from bastd.ui import getcurrency - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return getcurrency.GetCurrencyWindow(modal=True).get_root_widget() def _purchase(self) -> None: - from ba.internal import get_store_item_name_translated from bastd.ui import getcurrency from bastd.ui import confirm + plus = bui.app.plus + assert plus is not None + + assert bui.app.classic is not None + store = bui.app.classic.store + if self._offer['item'] == 'pro': - ba.internal.purchase('pro_sale') + plus.purchase('pro_sale') elif self._offer['item'] == 'pro_fullprice': - ba.internal.purchase('pro') + plus.purchase('pro') elif self._is_bundle_sale: # With bundle sales, the price is the name of the IAP. - ba.internal.purchase(self._offer['price']) + plus.purchase(self._offer['price']) else: ticket_count: int | None try: - ticket_count = ba.internal.get_v1_account_ticket_count() + ticket_count = plus.get_v1_account_ticket_count() except Exception: ticket_count = None if ticket_count is not None and ticket_count < self._offer['price']: getcurrency.show_get_tickets_prompt() - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return def do_it() -> None: - ba.internal.in_game_purchase( + plus = bui.app.plus + assert plus is not None + + plus.in_game_purchase( 'offer:' + str(self._offer['id']), self._offer['price'] ) - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() confirm.ConfirmWindow( - ba.Lstr( + bui.Lstr( resource='store.purchaseConfirmText', subs=[ ( '${ITEM}', - get_store_item_name_translated(self._offer['item']), + store.get_store_item_name_translated( + self._offer['item'] + ), ) ], ), width=400, height=120, action=do_it, - ok_text=ba.Lstr( + ok_text=bui.Lstr( resource='store.purchaseText', fallback_resource='okText' ), ) def _cancel(self) -> None: if self._cancel_delay > 0: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return self._transition_out('out_right') @@ -507,37 +515,42 @@ def show_offer() -> bool: try: from bastd.ui import feedback - app = ba.app + plus = bui.app.plus + assert plus is not None + + app = bui.app + assert app.classic is not None # Space things out a bit so we don't hit the poor user with an ad and # then an in-game offer. has_been_long_enough_since_ad = True - if app.ads.last_ad_completion_time is not None and ( - ba.time(ba.TimeType.REAL) - app.ads.last_ad_completion_time < 30.0 + if app.classic.ads.last_ad_completion_time is not None and ( + bui.apptime() - app.classic.ads.last_ad_completion_time < 30.0 ): has_been_long_enough_since_ad = False - if app.special_offer is not None and has_been_long_enough_since_ad: - + if ( + app.classic.special_offer is not None + and has_been_long_enough_since_ad + ): # Special case: for pro offers, store this in our prefs so we # can re-show it if the user kills us (set phasers to 'NAG'!!!). - if app.special_offer.get('item') == 'pro_fullprice': + if app.classic.special_offer.get('item') == 'pro_fullprice': cfg = app.config cfg['pendingSpecialOffer'] = { - 'a': ba.internal.get_public_login_id(), - 'o': app.special_offer, + 'a': plus.get_v1_account_public_login_id(), + 'o': app.classic.special_offer, } cfg.commit() - with ba.Context('ui'): - if app.special_offer['item'] == 'rating': - feedback.ask_for_rating() - else: - SpecialOfferWindow(app.special_offer) + if app.classic.special_offer['item'] == 'rating': + feedback.ask_for_rating() + else: + SpecialOfferWindow(app.classic.special_offer) - app.special_offer = None + app.classic.special_offer = None return True except Exception: - ba.print_exception('Error showing offer.') + logging.exception('Error showing offer.') return False diff --git a/assets/src/ba_data/python/bastd/ui/store/__init__.py b/src/assets/ba_data/python/bastd/ui/store/__init__.py similarity index 100% rename from assets/src/ba_data/python/bastd/ui/store/__init__.py rename to src/assets/ba_data/python/bastd/ui/store/__init__.py diff --git a/assets/src/ba_data/python/bastd/ui/store/browser.py b/src/assets/ba_data/python/bastd/ui/store/browser.py similarity index 75% rename from assets/src/ba_data/python/bastd/ui/store/browser.py rename to src/assets/ba_data/python/bastd/ui/store/browser.py index 1bc8da43..7771058d 100644 --- a/assets/src/ba_data/python/bastd/ui/store/browser.py +++ b/src/assets/ba_data/python/bastd/ui/store/browser.py @@ -9,14 +9,14 @@ import copy import math import logging import weakref +import datetime from enum import Enum from threading import Thread from typing import TYPE_CHECKING from efro.error import CommunicationError import bacommon.cloud -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable, Sequence @@ -24,7 +24,7 @@ if TYPE_CHECKING: MERCH_LINK_KEY = 'Merch Link' -class StoreBrowserWindow(ba.Window): +class StoreBrowserWindow(bui.Window): """Window for browsing the store.""" class TabID(Enum): @@ -43,17 +43,18 @@ class StoreBrowserWindow(ba.Window): show_tab: StoreBrowserWindow.TabID | None = None, on_close_call: Callable[[], Any] | None = None, back_location: str | None = None, - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals from bastd.ui.tabs import TabRow - from ba import SpecialChar + from bauiv1 import SpecialChar - app = ba.app - uiscale = app.ui.uiscale + app = bui.app + assert app.classic is not None + uiscale = app.classic.ui.uiscale - ba.set_analytics_screen('Store Window') + bui.set_analytics_screen('Store Window') scale_origin: tuple[float, float] | None @@ -67,69 +68,69 @@ class StoreBrowserWindow(ba.Window): scale_origin = None self.button_infos: dict[str, dict[str, Any]] | None = None - self.update_buttons_timer: ba.Timer | None = None + self.update_buttons_timer: bui.AppTimer | None = None self._status_textwidget_update_timer = None self._back_location = back_location self._on_close_call = on_close_call self._show_tab = show_tab self._modal = modal - self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 - self._x_inset = x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + self._width = 1240 if uiscale is bui.UIScale.SMALL else 1040 + self._x_inset = x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 578 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 645 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 800 ) self._current_tab: StoreBrowserWindow.TabID | None = None - extra_top = 30 if uiscale is ba.UIScale.SMALL else 0 + extra_top = 30 if uiscale is bui.UIScale.SMALL else 0 self._request: Any = None self._r = 'store' self._last_buy_time: float | None = None super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + extra_top), transition=transition, toolbar_visibility='menu_full', scale=( 1.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 0.9 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 0.8 ), scale_origin_stack_offset=scale_origin, stack_offset=( (0, -5) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0) - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else (0, 0) ), ) ) - self._back_button = btn = ba.buttonwidget( + self._back_button = btn = bui.buttonwidget( parent=self._root_widget, position=(70 + x_inset, self._height - 74), size=(140, 60), scale=1.1, autoselect=True, - label=ba.Lstr(resource='doneText' if self._modal else 'backText'), + label=bui.Lstr(resource='doneText' if self._modal else 'backText'), button_type=None if self._modal else 'back', on_activate_call=self._back, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.containerwidget(edit=self._root_widget, cancel_button=btn) - self._ticket_count_text: ba.Widget | None = None - self._get_tickets_button: ba.Widget | None = None + self._ticket_count_text: bui.Widget | None = None + self._get_tickets_button: bui.Widget | None = None - if ba.app.allow_ticket_purchases: - self._get_tickets_button = ba.buttonwidget( + if app.classic.allow_ticket_purchases: + self._get_tickets_button = bui.buttonwidget( parent=self._root_widget, size=(210, 65), on_activate_call=self._on_get_more_tickets_press, @@ -139,10 +140,10 @@ class StoreBrowserWindow(ba.Window): left_widget=self._back_button, color=(0.7, 0.5, 0.85), textcolor=(0.2, 1.0, 0.2), - label=ba.Lstr(resource='getTicketsWindow.titleText'), + label=bui.Lstr(resource='getTicketsWindow.titleText'), ) else: - self._ticket_count_text = ba.textwidget( + self._ticket_count_text = bui.textwidget( parent=self._root_widget, size=(210, 64), color=(0.2, 1.0, 0.2), @@ -152,73 +153,73 @@ class StoreBrowserWindow(ba.Window): # Move this dynamically to keep it out of the way of the party icon. self._update_get_tickets_button_pos() - self._get_ticket_pos_update_timer = ba.Timer( + self._get_ticket_pos_update_timer = bui.AppTimer( 1.0, - ba.WeakCall(self._update_get_tickets_button_pos), + bui.WeakCall(self._update_get_tickets_button_pos), repeat=True, - timetype=ba.TimeType.REAL, ) if self._get_tickets_button: - ba.widget( + bui.widget( edit=self._back_button, right_widget=self._get_tickets_button ) - self._ticket_text_update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update_tickets_text), - timetype=ba.TimeType.REAL, - repeat=True, + self._ticket_text_update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update_tickets_text), repeat=True ) self._update_tickets_text() - app = ba.app - if app.platform in ['mac', 'ios'] and app.subplatform == 'appstore': - ba.buttonwidget( + if ( + app.classic.platform in ['mac', 'ios'] + and app.classic.subplatform == 'appstore' + ): + bui.buttonwidget( parent=self._root_widget, position=(self._width * 0.5 - 70, 16), size=(230, 50), scale=0.65, - on_activate_call=ba.WeakCall(self._restore_purchases), + on_activate_call=bui.WeakCall(self._restore_purchases), color=(0.35, 0.3, 0.4), selectable=False, textcolor=(0.55, 0.5, 0.6), - label=ba.Lstr(resource='getTicketsWindow.restorePurchasesText'), + label=bui.Lstr( + resource='getTicketsWindow.restorePurchasesText' + ), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 44), size=(0, 0), - color=app.ui.title_color, + color=app.classic.ui.title_color, scale=1.5, h_align='center', v_align='center', - text=ba.Lstr(resource='storeText'), + text=bui.Lstr(resource='storeText'), maxwidth=420, ) if not self._modal: - ba.buttonwidget( + bui.buttonwidget( edit=self._back_button, button_type='backSmall', size=(60, 60), - label=ba.charstr(SpecialChar.BACK), + label=bui.charstr(SpecialChar.BACK), ) scroll_buffer_h = 130 + 2 * x_inset tab_buffer_h = 250 + 2 * x_inset tabs_def = [ - (self.TabID.EXTRAS, ba.Lstr(resource=self._r + '.extrasText')), - (self.TabID.MAPS, ba.Lstr(resource=self._r + '.mapsText')), + (self.TabID.EXTRAS, bui.Lstr(resource=self._r + '.extrasText')), + (self.TabID.MAPS, bui.Lstr(resource=self._r + '.mapsText')), ( self.TabID.MINIGAMES, - ba.Lstr(resource=self._r + '.miniGamesText'), + bui.Lstr(resource=self._r + '.miniGamesText'), ), ( self.TabID.CHARACTERS, - ba.Lstr(resource=self._r + '.charactersText'), + bui.Lstr(resource=self._r + '.charactersText'), ), - (self.TabID.ICONS, ba.Lstr(resource=self._r + '.iconsText')), + (self.TabID.ICONS, bui.Lstr(resource=self._r + '.iconsText')), ] self._tab_row = TabRow( @@ -240,14 +241,14 @@ class StoreBrowserWindow(ba.Window): button = tab.button rad = 10 center = (pos[0] + 0.1 * size[0], pos[1] + 0.9 * size[1]) - img = ba.imagewidget( + img = bui.imagewidget( parent=self._root_widget, position=(center[0] - rad * 1.04, center[1] - rad * 1.15), size=(rad * 2.2, rad * 2.2), - texture=ba.gettexture('circleShadow'), + texture=bui.gettexture('circleShadow'), color=(1, 0, 0), ) - txt = ba.textwidget( + txt = bui.textwidget( parent=self._root_widget, position=center, size=(0, 0), @@ -259,15 +260,15 @@ class StoreBrowserWindow(ba.Window): flatness=1.0, ) rad = 20 - sale_img = ba.imagewidget( + sale_img = bui.imagewidget( parent=self._root_widget, position=(center[0] - rad, center[1] - rad), size=(rad * 2, rad * 2), draw_controller=button, - texture=ba.gettexture('circleZigZag'), + texture=bui.gettexture('circleZigZag'), color=(0.5, 0, 1.0), ) - sale_title_text = ba.textwidget( + sale_title_text = bui.textwidget( parent=self._root_widget, position=(center[0], center[1] + 0.24 * rad), size=(0, 0), @@ -280,7 +281,7 @@ class StoreBrowserWindow(ba.Window): flatness=1.0, color=(0, 1, 0), ) - sale_time_text = ba.textwidget( + sale_time_text = bui.textwidget( parent=self._root_widget, position=(center[0], center[1] - 0.29 * rad), size=(0, 0), @@ -300,20 +301,17 @@ class StoreBrowserWindow(ba.Window): 'sale_title_text': sale_title_text, 'sale_time_text': sale_time_text, } - self._tab_update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update_tabs), - timetype=ba.TimeType.REAL, - repeat=True, + self._tab_update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update_tabs), repeat=True ) self._update_tabs() if self._get_tickets_button: last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button - ba.widget( + bui.widget( edit=self._get_tickets_button, down_widget=last_tab_button ) - ba.widget( + bui.widget( edit=last_tab_button, up_widget=self._get_tickets_button, right_widget=self._get_tickets_button, @@ -322,12 +320,13 @@ class StoreBrowserWindow(ba.Window): self._scroll_width = self._width - scroll_buffer_h self._scroll_height = self._height - 180 - self._scrollwidget: ba.Widget | None = None - self._status_textwidget: ba.Widget | None = None + self._scrollwidget: bui.Widget | None = None + self._status_textwidget: bui.Widget | None = None self._restore_state() def _update_get_tickets_button_pos(self) -> None: - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale pos = ( self._width - 252 @@ -335,81 +334,79 @@ class StoreBrowserWindow(ba.Window): self._x_inset + ( 47 - if uiscale is ba.UIScale.SMALL - and ba.internal.is_party_icon_visible() + if uiscale is bui.UIScale.SMALL + and bui.is_party_icon_visible() else 0 ) ), self._height - 70, ) if self._get_tickets_button: - ba.buttonwidget(edit=self._get_tickets_button, position=pos) + bui.buttonwidget(edit=self._get_tickets_button, position=pos) if self._ticket_count_text: - ba.textwidget(edit=self._ticket_count_text, position=pos) + bui.textwidget(edit=self._ticket_count_text, position=pos) def _restore_purchases(self) -> None: from bastd.ui import account - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + if plus.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() else: - ba.internal.restore_purchases() + plus.restore_purchases() def _update_tabs(self) -> None: - from ba.internal import ( - get_available_sale_time, - get_available_purchase_count, - ) + assert bui.app.classic is not None + store = bui.app.classic.store if not self._root_widget: return for tab_id, tab_data in list(self._purchasable_count_widgets.items()): - sale_time = get_available_sale_time(tab_id.value) + sale_time = store.get_available_sale_time(tab_id.value) if sale_time is not None: - ba.textwidget( + bui.textwidget( edit=tab_data['sale_title_text'], - text=ba.Lstr(resource='store.saleText'), + text=bui.Lstr(resource='store.saleText'), ) - ba.textwidget( + bui.textwidget( edit=tab_data['sale_time_text'], - text=ba.timestring( - sale_time, - centi=False, - timeformat=ba.TimeFormat.MILLISECONDS, - ), + text=bui.timestring(sale_time / 1000.0, centi=False), ) - ba.imagewidget(edit=tab_data['sale_img'], opacity=1.0) + bui.imagewidget(edit=tab_data['sale_img'], opacity=1.0) count = 0 else: - ba.textwidget(edit=tab_data['sale_title_text'], text='') - ba.textwidget(edit=tab_data['sale_time_text'], text='') - ba.imagewidget(edit=tab_data['sale_img'], opacity=0.0) - count = get_available_purchase_count(tab_id.value) + bui.textwidget(edit=tab_data['sale_title_text'], text='') + bui.textwidget(edit=tab_data['sale_time_text'], text='') + bui.imagewidget(edit=tab_data['sale_img'], opacity=0.0) + count = store.get_available_purchase_count(tab_id.value) if count > 0: - ba.textwidget(edit=tab_data['text'], text=str(count)) - ba.imagewidget(edit=tab_data['img'], opacity=1.0) + bui.textwidget(edit=tab_data['text'], text=str(count)) + bui.imagewidget(edit=tab_data['img'], opacity=1.0) else: - ba.textwidget(edit=tab_data['text'], text='') - ba.imagewidget(edit=tab_data['img'], opacity=0.0) + bui.textwidget(edit=tab_data['text'], text='') + bui.imagewidget(edit=tab_data['img'], opacity=0.0) def _update_tickets_text(self) -> None: - from ba import SpecialChar + from bauiv1 import SpecialChar if not self._root_widget: return - sval: str | ba.Lstr - if ba.internal.get_v1_account_state() == 'signed_in': - sval = ba.charstr(SpecialChar.TICKET) + str( - ba.internal.get_v1_account_ticket_count() + plus = bui.app.plus + assert plus is not None + sval: str | bui.Lstr + if plus.get_v1_account_state() == 'signed_in': + sval = bui.charstr(SpecialChar.TICKET) + str( + plus.get_v1_account_ticket_count() ) else: - sval = ba.Lstr(resource='getTicketsWindow.titleText') + sval = bui.Lstr(resource='getTicketsWindow.titleText') if self._get_tickets_button: - ba.buttonwidget(edit=self._get_tickets_button, label=sval) + bui.buttonwidget(edit=self._get_tickets_button, label=sval) if self._ticket_count_text: - ba.textwidget(edit=self._ticket_count_text, text=sval) + bui.textwidget(edit=self._ticket_count_text, text=sval) def _set_tab(self, tab_id: TabID) -> None: if self._current_tab is tab_id: @@ -417,7 +414,7 @@ class StoreBrowserWindow(ba.Window): self._current_tab = tab_id # We wanna preserve our current tab between runs. - cfg = ba.app.config + cfg = bui.app.config cfg['Store Tab'] = tab_id.value cfg.commit() @@ -428,7 +425,7 @@ class StoreBrowserWindow(ba.Window): if self._scrollwidget: self._scrollwidget.delete() - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self._root_widget, highlight=False, position=( @@ -449,14 +446,14 @@ class StoreBrowserWindow(ba.Window): # Show status over top. if self._status_textwidget: self._status_textwidget.delete() - self._status_textwidget = ba.textwidget( + self._status_textwidget = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.5), size=(0, 0), color=(1, 0.7, 1, 0.5), h_align='center', v_align='center', - text=ba.Lstr(resource=self._r + '.loadingText'), + text=bui.Lstr(resource=self._r + '.loadingText'), maxwidth=self._scroll_width * 0.9, ) @@ -464,11 +461,7 @@ class StoreBrowserWindow(ba.Window): def __init__(self, window: StoreBrowserWindow): self._window = weakref.ref(window) data = {'tab': tab_id.value} - ba.timer( - 0.1, - ba.WeakCall(self._on_response, data), - timetype=ba.TimeType.REAL, - ) + bui.apptimer(0.1, bui.WeakCall(self._on_response, data)) def _on_response(self, data: dict[str, Any] | None) -> None: # FIXME: clean this up. @@ -486,16 +479,18 @@ class StoreBrowserWindow(ba.Window): def _purchase_check_result( self, item: str, is_ticket_purchase: bool, result: dict[str, Any] | None ) -> None: + plus = bui.app.plus + assert plus is not None if result is None: - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource='internal.unavailableNoConnectionText'), + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='internal.unavailableNoConnectionText'), color=(1, 0, 0), ) else: if is_ticket_purchase: if result['allow']: - price = ba.internal.get_v1_account_misc_read_val( + price = plus.get_v1_account_misc_read_val( 'price.' + item, None ) if ( @@ -509,23 +504,23 @@ class StoreBrowserWindow(ba.Window): 'for item', item, ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() else: - ba.playsound(ba.getsound('click01')) - ba.internal.in_game_purchase(item, price) + bui.getsound('click01').play() + plus.in_game_purchase(item, price) else: if result['reason'] == 'versionTooOld': - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr( + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr( resource='getTicketsWindow.versionTooOldText' ), color=(1, 0, 0), ) else: - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr( + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr( resource='getTicketsWindow.unavailableText' ), color=(1, 0, 0), @@ -533,20 +528,20 @@ class StoreBrowserWindow(ba.Window): # Real in-app purchase. else: if result['allow']: - ba.internal.purchase(item) + plus.purchase(item) else: if result['reason'] == 'versionTooOld': - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr( + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr( resource='getTicketsWindow.versionTooOldText' ), color=(1, 0, 0), ) else: - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr( + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr( resource='getTicketsWindow.unavailableText' ), color=(1, 0, 0), @@ -555,75 +550,80 @@ class StoreBrowserWindow(ba.Window): def _do_purchase_check( self, item: str, is_ticket_purchase: bool = False ) -> None: - from ba.internal import master_server_get + app = bui.app + if app.classic is None: + logging.warning('_do_purchase_check() requires classic.') + return # Here we ping the server to ask if it's valid for us to # purchase this. Better to fail now than after we've # paid locally. - app = ba.app - master_server_get( + + app.classic.master_server_v1_get( 'bsAccountPurchaseCheck', { 'item': item, - 'platform': app.platform, - 'subplatform': app.subplatform, + 'platform': app.classic.platform, + 'subplatform': app.classic.subplatform, 'version': app.version, 'buildNumber': app.build_number, 'purchaseType': 'ticket' if is_ticket_purchase else 'real', }, - callback=ba.WeakCall( + callback=bui.WeakCall( self._purchase_check_result, item, is_ticket_purchase ), ) def buy(self, item: str) -> None: """Attempt to purchase the provided item.""" - from ba.internal import ( - get_available_sale_time, - get_store_item_name_translated, - ) from bastd.ui import account from bastd.ui.confirm import ConfirmWindow from bastd.ui import getcurrency + assert bui.app.classic is not None + store = bui.app.classic.store + + plus = bui.app.plus + assert plus is not None + # Prevent pressing buy within a few seconds of the last press # (gives the buttons time to disable themselves and whatnot). - curtime = ba.time(ba.TimeType.REAL) + curtime = bui.apptime() if ( self._last_buy_time is not None and (curtime - self._last_buy_time) < 2.0 ): - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() else: - if ba.internal.get_v1_account_state() != 'signed_in': + if plus.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() else: self._last_buy_time = curtime # Merch is a special case - just a link. if item == 'merch': - url = ba.app.config.get('Merch Link') + url = bui.app.config.get('Merch Link') if isinstance(url, str): - ba.open_url(url) + bui.open_url(url) # Pro is an actual IAP, and the rest are ticket purchases. elif item == 'pro': - ba.playsound(ba.getsound('click01')) + bui.getsound('click01').play() # Purchase either pro or pro_sale depending on whether # there is a sale going on. self._do_purchase_check( 'pro' - if get_available_sale_time('extras') is None + if store.get_available_sale_time('extras') is None else 'pro_sale' ) else: - price = ba.internal.get_v1_account_misc_read_val( + price = plus.get_v1_account_misc_read_val( 'price.' + item, None ) - our_tickets = ba.internal.get_v1_account_ticket_count() + our_tickets = plus.get_v1_account_ticket_count() if price is not None and our_tickets < price: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() getcurrency.show_get_tickets_prompt() else: @@ -632,49 +632,55 @@ class StoreBrowserWindow(ba.Window): item, is_ticket_purchase=True ) - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() ConfirmWindow( - ba.Lstr( + bui.Lstr( resource='store.purchaseConfirmText', subs=[ ( '${ITEM}', - get_store_item_name_translated(item), + store.get_store_item_name_translated( + item + ), ) ], ), width=400, height=120, action=do_it, - ok_text=ba.Lstr( + ok_text=bui.Lstr( resource='store.purchaseText', fallback_resource='okText', ), ) def _print_already_own(self, charname: str) -> None: - ba.screenmessage( - ba.Lstr( + bui.screenmessage( + bui.Lstr( resource=self._r + '.alreadyOwnText', subs=[('${NAME}', charname)], ), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() def update_buttons(self) -> None: """Update our buttons.""" # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals - from ba.internal import get_available_sale_time - from ba import SpecialChar + from bauiv1 import SpecialChar + + assert bui.app.classic is not None + store = bui.app.classic.store + + plus = bui.app.plus + assert plus is not None if not self._root_widget: return - import datetime - sales_raw = ba.internal.get_v1_account_misc_read_val('sales', {}) + sales_raw = plus.get_v1_account_misc_read_val('sales', {}) sales = {} try: # Look at the current set of sales; filter any with time remaining. @@ -689,27 +695,27 @@ class StoreBrowserWindow(ba.Window): 'original_price': sale_info['op'], } except Exception: - ba.print_exception('Error parsing sales.') + logging.exception('Error parsing sales.') assert self.button_infos is not None for b_type, b_info in self.button_infos.items(): - if b_type == 'merch': purchased = False elif b_type in ['upgrades.pro', 'pro']: - purchased = ba.app.accounts_v1.have_pro() + assert bui.app.classic is not None + purchased = bui.app.classic.accounts.have_pro() else: - purchased = ba.internal.get_purchased(b_type) + purchased = plus.get_purchased(b_type) sale_opacity = 0.0 - sale_title_text: str | ba.Lstr = '' - sale_time_text: str | ba.Lstr = '' + sale_title_text: str | bui.Lstr = '' + sale_time_text: str | bui.Lstr = '' if purchased: title_color = (0.8, 0.7, 0.9, 1.0) color = (0.63, 0.55, 0.78) extra_image_opacity = 0.5 - call = ba.WeakCall(self._print_already_own, b_info['name']) + call = bui.WeakCall(self._print_already_own, b_info['name']) price_text = '' price_text_left = '' price_text_right = '' @@ -727,40 +733,38 @@ class StoreBrowserWindow(ba.Window): price_text_left = '' price_text_right = '' elif b_type in ['upgrades.pro', 'pro']: - sale_time = get_available_sale_time('extras') + sale_time = store.get_available_sale_time('extras') if sale_time is not None: - priceraw = ba.internal.get_price('pro') + priceraw = plus.get_price('pro') price_text_left = ( priceraw if priceraw is not None else '?' ) - priceraw = ba.internal.get_price('pro_sale') + priceraw = plus.get_price('pro_sale') price_text_right = ( priceraw if priceraw is not None else '?' ) sale_opacity = 1.0 price_text = '' - sale_title_text = ba.Lstr(resource='store.saleText') - sale_time_text = ba.timestring( - sale_time, - centi=False, - timeformat=ba.TimeFormat.MILLISECONDS, + sale_title_text = bui.Lstr(resource='store.saleText') + sale_time_text = bui.timestring( + sale_time / 1000.0, centi=False ) else: - priceraw = ba.internal.get_price('pro') + priceraw = plus.get_price('pro') price_text = priceraw if priceraw is not None else '?' price_text_left = '' price_text_right = '' else: - price = ba.internal.get_v1_account_misc_read_val( + price = plus.get_v1_account_misc_read_val( 'price.' + b_type, 0 ) # Color the button differently if we cant afford this. - if ba.internal.get_v1_account_state() == 'signed_in': - if ba.internal.get_v1_account_ticket_count() < price: + if plus.get_v1_account_state() == 'signed_in': + if plus.get_v1_account_ticket_count() < price: color = (0.6, 0.61, 0.6) - price_text = ba.charstr(ba.SpecialChar.TICKET) + str( - ba.internal.get_v1_account_misc_read_val( + price_text = bui.charstr(bui.SpecialChar.TICKET) + str( + plus.get_v1_account_misc_read_val( 'price.' + b_type, '?' ) ) @@ -770,16 +774,14 @@ class StoreBrowserWindow(ba.Window): # TESTING: if b_type in sales: sale_opacity = 1.0 - price_text_left = ba.charstr(SpecialChar.TICKET) + str( + price_text_left = bui.charstr(SpecialChar.TICKET) + str( sales[b_type]['original_price'] ) price_text_right = price_text price_text = '' - sale_title_text = ba.Lstr(resource='store.saleText') - sale_time_text = ba.timestring( - int(sales[b_type]['to_end'] * 1000), - centi=False, - timeformat=ba.TimeFormat.MILLISECONDS, + sale_title_text = bui.Lstr(resource='store.saleText') + sale_time_text = bui.timestring( + sales[b_type]['to_end'], centi=False ) description_color = (0.5, 1.0, 0.5) @@ -788,75 +790,78 @@ class StoreBrowserWindow(ba.Window): show_purchase_check = False if 'title_text' in b_info: - ba.textwidget(edit=b_info['title_text'], color=title_color) + bui.textwidget(edit=b_info['title_text'], color=title_color) if 'purchase_check' in b_info: - ba.imagewidget( + bui.imagewidget( edit=b_info['purchase_check'], opacity=1.0 if show_purchase_check else 0.0, ) if 'price_widget' in b_info: - ba.textwidget( + bui.textwidget( edit=b_info['price_widget'], text=price_text, color=price_color, ) if 'price_widget_left' in b_info: - ba.textwidget( + bui.textwidget( edit=b_info['price_widget_left'], text=price_text_left ) if 'price_widget_right' in b_info: - ba.textwidget( + bui.textwidget( edit=b_info['price_widget_right'], text=price_text_right ) if 'price_slash_widget' in b_info: - ba.imagewidget( + bui.imagewidget( edit=b_info['price_slash_widget'], opacity=sale_opacity ) if 'sale_bg_widget' in b_info: - ba.imagewidget( + bui.imagewidget( edit=b_info['sale_bg_widget'], opacity=sale_opacity ) if 'sale_title_widget' in b_info: - ba.textwidget( + bui.textwidget( edit=b_info['sale_title_widget'], text=sale_title_text ) if 'sale_time_widget' in b_info: - ba.textwidget( + bui.textwidget( edit=b_info['sale_time_widget'], text=sale_time_text ) if 'button' in b_info: - ba.buttonwidget( + bui.buttonwidget( edit=b_info['button'], color=color, on_activate_call=call ) if 'extra_backings' in b_info: for bck in b_info['extra_backings']: - ba.imagewidget( + bui.imagewidget( edit=bck, color=color, opacity=extra_image_opacity ) if 'extra_images' in b_info: for img in b_info['extra_images']: - ba.imagewidget(edit=img, opacity=extra_image_opacity) + bui.imagewidget(edit=img, opacity=extra_image_opacity) if 'extra_texts' in b_info: for etxt in b_info['extra_texts']: - ba.textwidget(edit=etxt, color=description_color) + bui.textwidget(edit=etxt, color=description_color) if 'extra_texts_2' in b_info: for etxt in b_info['extra_texts_2']: - ba.textwidget(edit=etxt, color=description_color2) + bui.textwidget(edit=etxt, color=description_color2) if 'descriptionText' in b_info: - ba.textwidget( + bui.textwidget( edit=b_info['descriptionText'], color=description_color ) def _on_response(self, data: dict[str, Any] | None) -> None: # pylint: disable=too-many-statements + assert bui.app.classic is not None + cstore = bui.app.classic.store + # clear status text.. if self._status_textwidget: self._status_textwidget.delete() self._status_textwidget_update_timer = None if data is None: - self._status_textwidget = ba.textwidget( + self._status_textwidget = bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.5), size=(0, 0), @@ -865,7 +870,7 @@ class StoreBrowserWindow(ba.Window): color=(1, 0.3, 0.3, 1.0), h_align='center', v_align='center', - text=ba.Lstr(resource=self._r + '.loadErrorText'), + text=bui.Lstr(resource=self._r + '.loadErrorText'), maxwidth=self._scroll_width * 0.9, ) else: @@ -877,19 +882,15 @@ class StoreBrowserWindow(ba.Window): sdata: dict[str, Any], width: float, ): - from ba.internal import ( - get_store_item_display_size, - get_store_layout, - ) - self._store_window = store_window self._width = width - store_data = get_store_layout() + store_data = cstore.get_store_layout() self._tab = sdata['tab'] self._sections = copy.deepcopy(store_data[sdata['tab']]) self._height: float | None = None - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale # Pre-calc a few things and add them to store-data. for section in self._sections: @@ -903,14 +904,14 @@ class StoreBrowserWindow(ba.Window): dummy_name = 'icons.foo' else: dummy_name = '' - section['button_size'] = get_store_item_display_size( - dummy_name - ) + section[ + 'button_size' + ] = cstore.get_store_item_display_size(dummy_name) section['v_spacing'] = ( -25 if ( self._tab == 'extras' - and uiscale is ba.UIScale.SMALL + and uiscale is bui.UIScale.SMALL ) else -17 if self._tab == 'characters' @@ -929,13 +930,13 @@ class StoreBrowserWindow(ba.Window): 20 if ( self._tab == 'extras' - and uiscale is ba.UIScale.SMALL - and ba.app.config.get('Merch Link') + and uiscale is bui.UIScale.SMALL + and bui.app.config.get('Merch Link') ) else 55 if ( self._tab == 'extras' - and uiscale is ba.UIScale.SMALL + and uiscale is bui.UIScale.SMALL ) else -20 if self._tab == 'icons' @@ -943,7 +944,7 @@ class StoreBrowserWindow(ba.Window): ) def instantiate( - self, scrollwidget: ba.Widget, tab_button: ba.Widget + self, scrollwidget: bui.Widget, tab_button: bui.Widget ) -> None: """Create the store.""" # pylint: disable=too-many-locals @@ -984,7 +985,7 @@ class StoreBrowserWindow(ba.Window): self._height += b_height_total assert self._height is not None - cnt2 = ba.containerwidget( + cnt2 = bui.containerwidget( parent=scrollwidget, scale=1.0, size=(self._width, self._height), @@ -996,12 +997,12 @@ class StoreBrowserWindow(ba.Window): v = self._height - 20 if self._tab == 'characters': - txt = ba.Lstr( + txt = bui.Lstr( resource='store.howToSwitchCharactersText', subs=[ ( '${SETTINGS}', - ba.Lstr( + bui.Lstr( resource=( 'accountSettingsWindow.titleText' ) @@ -1009,7 +1010,7 @@ class StoreBrowserWindow(ba.Window): ), ( '${PLAYER_PROFILES}', - ba.Lstr( + bui.Lstr( resource=( 'playerProfilesWindow.titleText' ) @@ -1017,7 +1018,7 @@ class StoreBrowserWindow(ba.Window): ), ], ) - ba.textwidget( + bui.textwidget( parent=cnt2, text=txt, size=(0, 0), @@ -1032,16 +1033,16 @@ class StoreBrowserWindow(ba.Window): transition_delay=0.4, ) elif self._tab == 'icons': - txt = ba.Lstr( + txt = bui.Lstr( resource='store.howToUseIconsText', subs=[ ( '${SETTINGS}', - ba.Lstr(resource='mainMenu.settingsText'), + bui.Lstr(resource='mainMenu.settingsText'), ), ( '${PLAYER_PROFILES}', - ba.Lstr( + bui.Lstr( resource=( 'playerProfilesWindow.titleText' ) @@ -1049,7 +1050,7 @@ class StoreBrowserWindow(ba.Window): ), ], ) - ba.textwidget( + bui.textwidget( parent=cnt2, text=txt, size=(0, 0), @@ -1066,8 +1067,8 @@ class StoreBrowserWindow(ba.Window): elif self._tab == 'maps': assert self._width is not None assert self._height is not None - txt = ba.Lstr(resource='store.howToUseMapsText') - ba.textwidget( + txt = bui.Lstr(resource='store.howToUseMapsText') + bui.textwidget( parent=cnt2, text=txt, size=(0, 0), @@ -1088,7 +1089,7 @@ class StoreBrowserWindow(ba.Window): delay = 0.3 for section in self._sections: if section['title'] != '': - ba.textwidget( + bui.textwidget( parent=cnt2, position=(60, v - title_spacing * 0.8), size=(0, 0), @@ -1097,7 +1098,7 @@ class StoreBrowserWindow(ba.Window): color=(0.7, 0.9, 0.7, 1), h_align='left', v_align='center', - text=ba.Lstr(resource=section['title']), + text=bui.Lstr(resource=section['title']), maxwidth=self._width * 0.7, ) v -= title_spacing @@ -1118,7 +1119,7 @@ class StoreBrowserWindow(ba.Window): item = self._store_window.button_infos[ item_name ] = {} - item['call'] = ba.WeakCall( + item['call'] = bui.WeakCall( self._store_window.buy, item_name ) if 'x_offs' in section: @@ -1156,11 +1157,11 @@ class StoreBrowserWindow(ba.Window): # previous row. if prev_row_buttons is not None: if len(prev_row_buttons) > col: - ba.widget( + bui.widget( edit=btn, up_widget=prev_row_buttons[col], ) - ba.widget( + bui.widget( edit=prev_row_buttons[col], down_widget=btn, ) @@ -1176,15 +1177,15 @@ class StoreBrowserWindow(ba.Window): for b_prev in prev_row_buttons[ col + 1 : ]: - ba.widget( + bui.widget( edit=b_prev, down_widget=btn ) else: - ba.widget( + bui.widget( edit=btn, up_widget=prev_row_buttons[-1] ) else: - ba.widget(edit=btn, up_widget=tab_button) + bui.widget(edit=btn, up_widget=tab_button) col += 1 if col == b_column_count or i == b_count - 1: @@ -1199,11 +1200,10 @@ class StoreBrowserWindow(ba.Window): # Set a timer to update these buttons periodically as long # as we're alive (so if we buy one it will grey out, etc). - self._store_window.update_buttons_timer = ba.Timer( + self._store_window.update_buttons_timer = bui.AppTimer( 0.5, - ba.WeakCall(self._store_window.update_buttons), + bui.WeakCall(self._store_window.update_buttons), repeat=True, - timetype=ba.TimeType.REAL, ) # Also update them immediately. @@ -1223,7 +1223,7 @@ class StoreBrowserWindow(ba.Window): tab_button=self._tab_row.tabs[self._current_tab].button, ) else: - cnt = ba.containerwidget( + cnt = bui.containerwidget( parent=self._scrollwidget, scale=1.0, size=(self._scroll_width, self._scroll_height * 0.95), @@ -1232,7 +1232,7 @@ class StoreBrowserWindow(ba.Window): claims_tab=True, selection_loops_to_parent=True, ) - self._status_textwidget = ba.textwidget( + self._status_textwidget = bui.textwidget( parent=cnt, position=( self._scroll_width * 0.5, @@ -1244,7 +1244,7 @@ class StoreBrowserWindow(ba.Window): color=(1, 1, 0.3, 1.0), h_align='center', v_align='center', - text=ba.Lstr(resource=self._r + '.comingSoonText'), + text=bui.Lstr(resource=self._r + '.comingSoonText'), maxwidth=self._scroll_width * 0.9, ) @@ -1267,25 +1267,27 @@ 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[type(self)] = { + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = { 'sel_name': sel_name, } except Exception: - ba.print_exception(f'Error saving state for {self}.') + logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: from efro.util import enum_by_value try: - sel: ba.Widget | None - sel_name = ba.app.ui.window_states.get(type(self), {}).get( + sel: bui.Widget | None + assert bui.app.classic is not None + sel_name = bui.app.classic.ui.window_states.get(type(self), {}).get( 'sel_name' ) assert isinstance(sel_name, (str, type(None))) try: current_tab = enum_by_value( - self.TabID, ba.app.config.get('Store Tab') + self.TabID, bui.app.config.get('Store Tab') ) except ValueError: current_tab = self.TabID.CHARACTERS @@ -1317,26 +1319,30 @@ class StoreBrowserWindow(ba.Window): sel = self._tab_row.tabs[self._show_tab].button self._set_tab(current_tab) if sel is not None: - ba.containerwidget(edit=self._root_widget, selected_child=sel) + bui.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: - ba.print_exception(f'Error restoring state for {self}.') + logging.exception('Error restoring state for %s.', self) def _on_get_more_tickets_press(self) -> None: # pylint: disable=cyclic-import from bastd.ui.account import show_sign_in_prompt from bastd.ui.getcurrency import GetCurrencyWindow - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.containerwidget(edit=self._root_widget, transition='out_left') window = GetCurrencyWindow( from_modal_store=self._modal, store_back_location=self._back_location, ).get_root_widget() if not self._modal: - ba.app.ui.set_main_menu_window(window) + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window(window) def _back(self) -> None: # pylint: disable=cyclic-import @@ -1344,16 +1350,17 @@ class StoreBrowserWindow(ba.Window): from bastd.ui.mainmenu import MainMenuWindow self._save_state() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) if not self._modal: + assert bui.app.classic is not None if self._back_location == 'CoopBrowserWindow': - ba.app.ui.set_main_menu_window( + bui.app.classic.ui.set_main_menu_window( CoopBrowserWindow(transition='in_left').get_root_widget() ) else: - ba.app.ui.set_main_menu_window( + bui.app.classic.ui.set_main_menu_window( MainMenuWindow(transition='in_left').get_root_widget() ) if self._on_close_call is not None: @@ -1368,14 +1375,15 @@ def _check_merch_availability_in_bg_thread() -> None: # launch and store the results. for _i in range(15): try: - if ba.app.cloud.is_connected(): - response = ba.app.cloud.send_message( + if bui.app.cloud.is_connected(): + response = bui.app.cloud.send_message( bacommon.cloud.MerchAvailabilityMessage() ) def _store_in_logic_thread() -> None: - cfg = ba.app.config + cfg = bui.app.config current: str | None = cfg.get(MERCH_LINK_KEY) + # pylint: disable=isinstance-second-argument-not-valid-type if not isinstance(current, str | None): current = None if current != response.url: @@ -1384,7 +1392,7 @@ def _check_merch_availability_in_bg_thread() -> None: # If we successfully get a response, kick it over to the # logic thread to store and we're done. - ba.pushcall(_store_in_logic_thread, from_other_thread=True) + bui.pushcall(_store_in_logic_thread, from_other_thread=True) return except CommunicationError: pass diff --git a/assets/src/ba_data/python/bastd/ui/store/button.py b/src/assets/ba_data/python/bastd/ui/store/button.py similarity index 73% rename from assets/src/ba_data/python/bastd/ui/store/button.py rename to src/assets/ba_data/python/bastd/ui/store/button.py index 93ca4de3..04cc98b1 100644 --- a/assets/src/ba_data/python/bastd/ui/store/button.py +++ b/src/assets/ba_data/python/bastd/ui/store/button.py @@ -3,10 +3,10 @@ """UI functionality for a button leading to the store.""" from __future__ import annotations +import logging from typing import TYPE_CHECKING -import ba -import ba.internal +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Sequence, Callable @@ -17,7 +17,7 @@ class StoreButton: def __init__( self, - parent: ba.Widget, + parent: bui.Widget, position: Sequence[float], size: Sequence[float], scale: float, @@ -34,13 +34,13 @@ class StoreButton: self._scale = scale if on_activate_call is None: - on_activate_call = ba.WeakCall(self._default_on_activate_call) + on_activate_call = bui.WeakCall(self._default_on_activate_call) self._on_activate_call = on_activate_call - self._button = ba.buttonwidget( + self._button = bui.buttonwidget( parent=parent, size=size, - label='' if show_tickets else ba.Lstr(resource='storeText'), + label='' if show_tickets else bui.Lstr(resource='storeText'), scale=scale, autoselect=True, on_activate_call=self._on_activate, @@ -49,11 +49,11 @@ class StoreButton: button_type=button_type, ) - self._title_text: ba.Widget | None - self._ticket_text: ba.Widget | None + self._title_text: bui.Widget | None + self._ticket_text: bui.Widget | None if show_tickets: - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=parent, position=( position[0] + size[0] * 0.5 * scale, @@ -63,13 +63,13 @@ class StoreButton: h_align='center', v_align='center', maxwidth=size[0] * scale * 0.65, - text=ba.Lstr(resource='storeText'), + text=bui.Lstr(resource='storeText'), draw_controller=self._button, scale=scale, transition_delay=transition_delay, color=textcolor, ) - self._ticket_text = ba.textwidget( + self._ticket_text = bui.textwidget( parent=parent, size=(0, 0), h_align='center', @@ -90,15 +90,15 @@ class StoreButton: self._circle_center = (0.0, 0.0) self._sale_circle_center = (0.0, 0.0) - self._available_purchase_backing = ba.imagewidget( + self._available_purchase_backing = bui.imagewidget( parent=parent, color=(1, 0, 0), draw_controller=self._button, size=(2.2 * self._circle_rad, 2.2 * self._circle_rad), - texture=ba.gettexture('circleShadow'), + texture=bui.gettexture('circleShadow'), transition_delay=transition_delay, ) - self._available_purchase_text = ba.textwidget( + self._available_purchase_text = bui.textwidget( parent=parent, size=(0, 0), h_align='center', @@ -114,15 +114,15 @@ class StoreButton: ) self._sale_circle_rad = 18 * scale * sale_scale - self._sale_backing = ba.imagewidget( + self._sale_backing = bui.imagewidget( parent=parent, color=(0.5, 0, 1.0), draw_controller=self._button, size=(2 * self._sale_circle_rad, 2 * self._sale_circle_rad), - texture=ba.gettexture('circleZigZag'), + texture=bui.gettexture('circleZigZag'), transition_delay=transition_delay, ) - self._sale_title_text = ba.textwidget( + self._sale_title_text = bui.textwidget( parent=parent, size=(0, 0), h_align='center', @@ -135,7 +135,7 @@ class StoreButton: maxwidth=self._sale_circle_rad * 1.5, transition_delay=transition_delay, ) - self._sale_time_text = ba.textwidget( + self._sale_time_text = bui.textwidget( parent=parent, size=(0, 0), h_align='center', @@ -150,16 +150,13 @@ class StoreButton: ) self.set_position(position) - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - repeat=True, - timetype=ba.TimeType.REAL, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) self._update() def _on_activate(self) -> None: - ba.internal.increment_analytics_count('Store button press') + bui.increment_analytics_count('Store button press') self._on_activate_call() def set_position(self, position: Sequence[float]) -> None: @@ -176,9 +173,9 @@ class StoreButton: if not self._button: return - ba.buttonwidget(edit=self._button, position=self._position) + bui.buttonwidget(edit=self._button, position=self._position) if self._title_text is not None: - ba.textwidget( + bui.textwidget( edit=self._title_text, position=( self._position[0] + self._size[0] * 0.5 * self._scale, @@ -186,7 +183,7 @@ class StoreButton: ), ) if self._ticket_text is not None: - ba.textwidget( + bui.textwidget( edit=self._ticket_text, position=( position[0] + self._size[0] * 0.5 * self._scale, @@ -194,32 +191,32 @@ class StoreButton: ), size=(0, 0), ) - ba.imagewidget( + bui.imagewidget( edit=self._available_purchase_backing, position=( self._circle_center[0] - self._circle_rad * 1.02, self._circle_center[1] - self._circle_rad * 1.13, ), ) - ba.textwidget( + bui.textwidget( edit=self._available_purchase_text, position=self._circle_center ) - ba.imagewidget( + bui.imagewidget( edit=self._sale_backing, position=( self._sale_circle_center[0] - self._sale_circle_rad, self._sale_circle_center[1] - self._sale_circle_rad, ), ) - ba.textwidget( + bui.textwidget( edit=self._sale_title_text, position=( self._sale_circle_center[0], self._sale_circle_center[1] + self._sale_circle_rad * 0.3, ), ) - ba.textwidget( + bui.textwidget( edit=self._sale_time_text, position=( self._sale_circle_center[0], @@ -232,51 +229,54 @@ class StoreButton: from bastd.ui.account import show_sign_in_prompt from bastd.ui.store.browser import StoreBrowserWindow - if ba.internal.get_v1_account_state() != 'signed_in': + plus = bui.app.plus + assert plus is not None + if plus.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return StoreBrowserWindow(modal=True, origin_widget=self._button) - def get_button(self) -> ba.Widget: + def get_button(self) -> bui.Widget: """Return the underlying button widget.""" return self._button def _update(self) -> None: # pylint: disable=too-many-branches # pylint: disable=cyclic-import - from ba import SpecialChar, TimeFormat - from ba.internal import ( - get_available_sale_time, - get_available_purchase_count, - ) + from babase import SpecialChar + + plus = bui.app.plus + assert plus is not None + assert bui.app.classic is not None + store = bui.app.classic.store if not self._button: return # Our instance may outlive our UI objects. if self._ticket_text is not None: - if ba.internal.get_v1_account_state() == 'signed_in': - sval = ba.charstr(SpecialChar.TICKET) + str( - ba.internal.get_v1_account_ticket_count() + if plus.get_v1_account_state() == 'signed_in': + sval = bui.charstr(SpecialChar.TICKET) + str( + plus.get_v1_account_ticket_count() ) else: sval = '-' - ba.textwidget(edit=self._ticket_text, text=sval) - available_purchases = get_available_purchase_count() + bui.textwidget(edit=self._ticket_text, text=sval) + available_purchases = store.get_available_purchase_count() # Old pro sale stuff.. - sale_time = get_available_sale_time('extras') + sale_time = store.get_available_sale_time('extras') # ..also look for new style sales. if sale_time is None: import datetime - sales_raw = ba.internal.get_v1_account_misc_read_val('sales', {}) + sales_raw = plus.get_v1_account_misc_read_val('sales', {}) sale_times = [] try: # Look at the current set of sales; filter any with time # remaining that we don't own. for sale_item, sale_info in list(sales_raw.items()): - if not ba.internal.get_purchased(sale_item): + if not plus.get_purchased(sale_item): to_end = ( datetime.datetime.utcfromtimestamp(sale_info['e']) - datetime.datetime.utcnow() @@ -284,39 +284,37 @@ class StoreButton: if to_end > 0: sale_times.append(to_end) except Exception: - ba.print_exception('Error parsing sales.') + logging.exception('Error parsing sales.') if sale_times: sale_time = int(min(sale_times) * 1000) if sale_time is not None: - ba.textwidget( + bui.textwidget( edit=self._sale_title_text, - text=ba.Lstr(resource='store.saleText'), + text=bui.Lstr(resource='store.saleText'), ) - ba.textwidget( + bui.textwidget( edit=self._sale_time_text, - text=ba.timestring( - sale_time, centi=False, timeformat=TimeFormat.MILLISECONDS - ), + text=bui.timestring(sale_time / 1000.0, centi=False), ) - ba.imagewidget(edit=self._sale_backing, opacity=1.0) - ba.imagewidget(edit=self._available_purchase_backing, opacity=1.0) - ba.textwidget(edit=self._available_purchase_text, text='') - ba.imagewidget(edit=self._available_purchase_backing, opacity=0.0) + bui.imagewidget(edit=self._sale_backing, opacity=1.0) + bui.imagewidget(edit=self._available_purchase_backing, opacity=1.0) + bui.textwidget(edit=self._available_purchase_text, text='') + bui.imagewidget(edit=self._available_purchase_backing, opacity=0.0) else: - ba.imagewidget(edit=self._sale_backing, opacity=0.0) - ba.textwidget(edit=self._sale_time_text, text='') - ba.textwidget(edit=self._sale_title_text, text='') + bui.imagewidget(edit=self._sale_backing, opacity=0.0) + bui.textwidget(edit=self._sale_time_text, text='') + bui.textwidget(edit=self._sale_title_text, text='') if available_purchases > 0: - ba.textwidget( + bui.textwidget( edit=self._available_purchase_text, text=str(available_purchases), ) - ba.imagewidget( + bui.imagewidget( edit=self._available_purchase_backing, opacity=1.0 ) else: - ba.textwidget(edit=self._available_purchase_text, text='') - ba.imagewidget( + bui.textwidget(edit=self._available_purchase_text, text='') + bui.imagewidget( edit=self._available_purchase_backing, opacity=0.0 ) diff --git a/assets/src/ba_data/python/bastd/ui/store/item.py b/src/assets/ba_data/python/bastd/ui/store/item.py similarity index 80% rename from assets/src/ba_data/python/bastd/ui/store/item.py rename to src/assets/ba_data/python/bastd/ui/store/item.py index 6c8b827e..3a801466 100644 --- a/assets/src/ba_data/python/bastd/ui/store/item.py +++ b/src/assets/ba_data/python/bastd/ui/store/item.py @@ -5,7 +5,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba +import bascenev1 as bs +import bauiv1 as bui if TYPE_CHECKING: from typing import Any @@ -14,7 +15,7 @@ if TYPE_CHECKING: def instantiate_store_item_display( item_name: str, item: dict[str, Any], - parent_widget: ba.Widget, + parent_widget: bui.Widget, b_pos: tuple[float, float], b_width: float, b_height: float, @@ -28,29 +29,29 @@ def instantiate_store_item_display( # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals - from ba.internal import ( - get_store_item, - get_store_item_name_translated, - get_clean_price, - ) + assert bui.app.classic is not None + store = bui.app.classic.store + + plus = bui.app.plus + assert plus is not None del boffs_h # unused arg del boffs_h2 # unused arg del boffs_v2 # unused arg - item_info = get_store_item(item_name) + item_info = store.get_store_item(item_name) title_v = 0.24 price_v = 0.145 base_text_scale = 1.0 - item['name'] = title = get_store_item_name_translated(item_name) + item['name'] = title = store.get_store_item_name_translated(item_name) - btn: ba.Widget | None + btn: bui.Widget | None # Hack; showbuffer stuff isn't working well when we're showing merch. showbuffer = 10 if item_name in {'merch', 'pro', 'pro_sale'} else 76.0 if button: - item['button'] = btn = ba.buttonwidget( + item['button'] = btn = bui.buttonwidget( parent=parent_widget, position=b_pos, transition_delay=delay, @@ -61,7 +62,7 @@ def instantiate_store_item_display( autoselect=True, label='', ) - ba.widget(edit=btn, show_buffer_bottom=showbuffer) + bui.widget(edit=btn, show_buffer_bottom=showbuffer) else: btn = None @@ -74,10 +75,11 @@ def instantiate_store_item_display( tint2_color = None tex_name: str | None = None desc: str | None = None - modes: ba.Lstr | None = None + modes: bui.Lstr | None = None if item_name.startswith('characters.'): - character = ba.app.spaz_appearances[item_info['character']] + assert bui.app.classic is not None + character = bui.app.classic.spaz_appearances[item_info['character']] tint_color = ( item_info['color'] if 'color' in item_info @@ -113,15 +115,15 @@ def instantiate_store_item_display( elif item_name.startswith('games.'): gametype = item_info['gametype'] modes_l = [] - if gametype.supports_session_type(ba.CoopSession): - modes_l.append(ba.Lstr(resource='playModes.coopText')) - if gametype.supports_session_type(ba.DualTeamSession): - modes_l.append(ba.Lstr(resource='playModes.teamsText')) - if gametype.supports_session_type(ba.FreeForAllSession): - modes_l.append(ba.Lstr(resource='playModes.freeForAllText')) + if gametype.supports_session_type(bs.CoopSession): + modes_l.append(bui.Lstr(resource='playModes.coopText')) + if gametype.supports_session_type(bs.DualTeamSession): + modes_l.append(bui.Lstr(resource='playModes.teamsText')) + if gametype.supports_session_type(bs.FreeForAllSession): + modes_l.append(bui.Lstr(resource='playModes.freeForAllText')) if len(modes_l) == 3: - modes = ba.Lstr( + modes = bui.Lstr( value='${A}, ${B}, ${C}', subs=[ ('${A}', modes_l[0]), @@ -130,15 +132,15 @@ def instantiate_store_item_display( ], ) elif len(modes_l) == 2: - modes = ba.Lstr( + modes = bui.Lstr( value='${A}, ${B}', subs=[('${A}', modes_l[0]), ('${B}', modes_l[1])], ) elif len(modes_l) == 1: modes = modes_l[0] else: - raise Exception() - desc = gametype.get_description_display_string(ba.CoopSession) + raise RuntimeError() + desc = gametype.get_description_display_string(bs.CoopSession) tex_name = item_info['previewTex'] base_text_scale = 0.8 title_v = 0.48 @@ -156,10 +158,10 @@ def instantiate_store_item_display( b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x, b_pos[1] + b_height * 0.57 - im_dim * 0.5, ) - mask_texture = ba.gettexture('characterIconMask') + mask_texture = bui.gettexture('characterIconMask') assert icon_tex is not None assert tint_tex is not None - ba.imagewidget( + bui.imagewidget( parent=parent_widget, position=im_pos, size=(im_dim, im_dim), @@ -167,8 +169,8 @@ def instantiate_store_item_display( transition_delay=delay, mask_texture=mask_texture, draw_controller=btn, - texture=ba.gettexture(icon_tex), - tint_texture=ba.gettexture(tint_tex), + texture=bui.gettexture(icon_tex), + tint_texture=bui.gettexture(tint_tex), tint_color=tint_color, tint2_color=tint2_color, ) @@ -180,14 +182,14 @@ def instantiate_store_item_display( b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x, b_pos[1] + b_height * 0.47 - im_dim * 0.5, ) - ba.imagewidget( + bui.imagewidget( parent=parent_widget, position=im_pos, size=(im_dim, im_dim), transition_delay=delay, draw_controller=btn, opacity=1.0, - texture=ba.gettexture('merch'), + texture=bui.gettexture('merch'), ) if item_name in ['pro', 'upgrades.pro']: @@ -197,7 +199,7 @@ def instantiate_store_item_display( b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x, b_pos[1] + b_height * 0.5 - im_dim * 0.5, ) - ba.imagewidget( + bui.imagewidget( parent=parent_widget, position=im_pos, size=(im_dim, im_dim), @@ -205,11 +207,11 @@ def instantiate_store_item_display( draw_controller=btn, color=(0.3, 0.0, 0.3), opacity=0.3, - texture=ba.gettexture('logo'), + texture=bui.gettexture('logo'), ) - txt = ba.Lstr(resource='store.bombSquadProNewDescriptionText') + txt = bui.Lstr(resource='store.bombSquadProNewDescriptionText') - item['descriptionText'] = ba.textwidget( + item['descriptionText'] = bui.textwidget( parent=parent_widget, text=txt, position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.69), @@ -230,14 +232,14 @@ def instantiate_store_item_display( extra_texts_2 = item['extra_texts_2'] = [] backing_color = (0.5, 0.8, 0.3) if button else (0.6, 0.5, 0.65) - b_square_texture = ba.gettexture('buttonSquare') - char_mask_texture = ba.gettexture('characterIconMask') + b_square_texture = bui.gettexture('buttonSquare') + char_mask_texture = bui.gettexture('characterIconMask') pos = (0.17, 0.43) tile_size = (b_width * 0.16 * 1.2, b_width * 0.2 * 1.2) tile_pos = (b_pos[0] + b_width * pos[0], b_pos[1] + b_height * pos[1]) extra_backings.append( - ba.imagewidget( + bui.imagewidget( parent=parent_widget, position=( tile_pos[0] - tile_size[0] * 0.5, @@ -252,7 +254,7 @@ def instantiate_store_item_display( ) im_size = tile_size[0] * 0.8 extra_images.append( - ba.imagewidget( + bui.imagewidget( parent=parent_widget, position=( tile_pos[0] - im_size * 0.5, @@ -262,14 +264,14 @@ def instantiate_store_item_display( transition_delay=delay, draw_controller=btn, color=(1, 1, 1), - texture=ba.gettexture('ticketsMore'), + texture=bui.gettexture('ticketsMore'), ) ) bonus_tickets = str( - ba.internal.get_v1_account_misc_read_val('proBonusTickets', 100) + plus.get_v1_account_misc_read_val('proBonusTickets', 100) ) extra_texts.append( - ba.textwidget( + bui.textwidget( parent=parent_widget, draw_controller=btn, position=( @@ -283,7 +285,7 @@ def instantiate_store_item_display( v_align='center', maxwidth=tile_size[0] * 0.7, scale=0.55, - text=ba.Lstr( + text=bui.Lstr( resource='getTicketsWindow.ticketsText', subs=[('${COUNT}', bonus_tickets)], ), @@ -303,9 +305,10 @@ def instantiate_store_item_display( b_pos[0] + b_width * pos[0], b_pos[1] + b_height * pos[1], ) - character = ba.app.spaz_appearances[charname] + assert bui.app.classic is not None + character = bui.app.classic.spaz_appearances[charname] extra_backings.append( - ba.imagewidget( + bui.imagewidget( parent=parent_widget, position=( tile_pos[0] - tile_size[0] * 0.5, @@ -320,7 +323,7 @@ def instantiate_store_item_display( ) im_size = tile_size[0] * 0.7 extra_images.append( - ba.imagewidget( + bui.imagewidget( parent=parent_widget, position=( tile_pos[0] - im_size * 0.53, @@ -330,15 +333,15 @@ def instantiate_store_item_display( transition_delay=delay, draw_controller=btn, color=(1, 1, 1), - texture=ba.gettexture(character.icon_texture), - tint_texture=ba.gettexture(character.icon_mask_texture), + texture=bui.gettexture(character.icon_texture), + tint_texture=bui.gettexture(character.icon_mask_texture), tint_color=character.default_color, tint2_color=character.default_highlight, mask_texture=char_mask_texture, ) ) extra_texts.append( - ba.textwidget( + bui.textwidget( parent=parent_widget, draw_controller=btn, position=( @@ -352,7 +355,7 @@ def instantiate_store_item_display( v_align='center', maxwidth=tile_size[0] * 0.7, scale=0.55, - text=ba.Lstr(translate=('characterNames', charname)), + text=bui.Lstr(translate=('characterNames', charname)), flatness=1.0, shadow=0.0, ) @@ -360,25 +363,25 @@ def instantiate_store_item_display( # If we have a 'total-worth' item-id for this id, show that price so # the user knows how much this is worth. - total_worth_item = ba.internal.get_v1_account_misc_read_val( - 'twrths', {} - ).get(item_name) + total_worth_item = plus.get_v1_account_misc_read_val('twrths', {}).get( + item_name + ) total_worth_price: str | None if total_worth_item is not None: - price = ba.internal.get_price(total_worth_item) + price = plus.get_price(total_worth_item) total_worth_price = ( - get_clean_price(price) if price is not None else '??' + store.get_clean_price(price) if price is not None else '??' ) else: total_worth_price = None if total_worth_price is not None: - total_worth_text = ba.Lstr( + total_worth_text = bui.Lstr( resource='store.totalWorthText', subs=[('${TOTAL_WORTH}', total_worth_price)], ) extra_texts_2.append( - ba.textwidget( + bui.textwidget( parent=parent_widget, text=total_worth_text, position=( @@ -398,9 +401,9 @@ def instantiate_store_item_display( ) ) - model_opaque = ba.getmodel('level_select_button_opaque') - model_transparent = ba.getmodel('level_select_button_transparent') - mask_tex = ba.gettexture('mapPreviewMask') + mesh_opaque = bui.getmesh('level_select_button_opaque') + mesh_transparent = bui.getmesh('level_select_button_transparent') + mask_tex = bui.gettexture('mapPreviewMask') for levelname, preview_tex_name, pos in [ ('Infinite Onslaught', 'doomShroomPreview', (0.80, 0.48)), ('Infinite Runaround', 'towerDPreview', (0.80, 0.32)), @@ -412,7 +415,7 @@ def instantiate_store_item_display( ) im_size = tile_size[0] * 0.8 extra_backings.append( - ba.imagewidget( + bui.imagewidget( parent=parent_widget, position=( tile_pos[0] - tile_size[0] * 0.5, @@ -427,9 +430,9 @@ def instantiate_store_item_display( ) # Hack - gotta draw two transparent versions to avoid z issues. - for mod in model_opaque, model_transparent: + for mod in mesh_opaque, mesh_transparent: extra_images.append( - ba.imagewidget( + bui.imagewidget( parent=parent_widget, position=( tile_pos[0] - im_size * 0.52, @@ -437,15 +440,15 @@ def instantiate_store_item_display( ), size=(im_size, im_size * 0.5), transition_delay=delay, - model_transparent=mod, + mesh_transparent=mod, mask_texture=mask_tex, draw_controller=btn, - texture=ba.gettexture(preview_tex_name), + texture=bui.gettexture(preview_tex_name), ) ) extra_texts.append( - ba.textwidget( + bui.textwidget( parent=parent_widget, draw_controller=btn, position=( @@ -459,14 +462,14 @@ def instantiate_store_item_display( v_align='center', maxwidth=tile_size[0] * 0.7, scale=0.55, - text=ba.Lstr(translate=('coopLevelNames', levelname)), + text=bui.Lstr(translate=('coopLevelNames', levelname)), flatness=1.0, shadow=0.0, ) ) if item_name.startswith('icons.'): - item['icon_text'] = ba.textwidget( + item['icon_text'] = bui.textwidget( parent=parent_widget, text=item_info['icon'], position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.5), @@ -487,20 +490,20 @@ def instantiate_store_item_display( b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x, b_pos[1] + b_height * 0.62 - im_dim * 0.25, ) - model_opaque = ba.getmodel('level_select_button_opaque') - model_transparent = ba.getmodel('level_select_button_transparent') - mask_tex = ba.gettexture('mapPreviewMask') + mesh_opaque = bui.getmesh('level_select_button_opaque') + mesh_transparent = bui.getmesh('level_select_button_transparent') + mask_tex = bui.gettexture('mapPreviewMask') assert tex_name is not None - ba.imagewidget( + bui.imagewidget( parent=parent_widget, position=im_pos, size=(im_dim, im_dim * 0.5), transition_delay=delay, - model_opaque=model_opaque, - model_transparent=model_transparent, + mesh_opaque=mesh_opaque, + mesh_transparent=mesh_transparent, mask_texture=mask_tex, draw_controller=btn, - texture=ba.gettexture(tex_name), + texture=bui.gettexture(tex_name), ) if item_name.startswith('games.'): @@ -510,22 +513,22 @@ def instantiate_store_item_display( b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x, b_pos[1] + b_height * 0.72 - im_dim * 0.25, ) - model_opaque = ba.getmodel('level_select_button_opaque') - model_transparent = ba.getmodel('level_select_button_transparent') - mask_tex = ba.gettexture('mapPreviewMask') + mesh_opaque = bui.getmesh('level_select_button_opaque') + mesh_transparent = bui.getmesh('level_select_button_transparent') + mask_tex = bui.gettexture('mapPreviewMask') assert tex_name is not None - ba.imagewidget( + bui.imagewidget( parent=parent_widget, position=im_pos, size=(im_dim, im_dim * 0.5), transition_delay=delay, - model_opaque=model_opaque, - model_transparent=model_transparent, + mesh_opaque=mesh_opaque, + mesh_transparent=mesh_transparent, mask_texture=mask_tex, draw_controller=btn, - texture=ba.gettexture(tex_name), + texture=bui.gettexture(tex_name), ) - item['descriptionText'] = ba.textwidget( + item['descriptionText'] = bui.textwidget( parent=parent_widget, text=desc, position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.36), @@ -541,7 +544,7 @@ def instantiate_store_item_display( shadow=0.0, color=(0.6, 1, 0.6), ) - item['gameModesText'] = ba.textwidget( + item['gameModesText'] = bui.textwidget( parent=parent_widget, text=modes, position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.26), @@ -558,7 +561,7 @@ def instantiate_store_item_display( ) if not item_name.startswith('icons.'): - item['title_text'] = ba.textwidget( + item['title_text'] = bui.textwidget( parent=parent_widget, text=title, position=( @@ -575,18 +578,18 @@ def instantiate_store_item_display( color=(0.7, 0.9, 0.7, 1.0), ) - item['purchase_check'] = ba.imagewidget( + item['purchase_check'] = bui.imagewidget( parent=parent_widget, position=(b_pos[0] + b_width * check_pos, b_pos[1] + b_height * 0.05), transition_delay=delay, - model_transparent=ba.getmodel('checkTransparent'), + mesh_transparent=bui.getmesh('checkTransparent'), opacity=0.0, size=(60, 60), color=(0.6, 0.5, 0.8), draw_controller=btn, - texture=ba.gettexture('uiAtlas'), + texture=bui.gettexture('uiAtlas'), ) - item['price_widget'] = ba.textwidget( + item['price_widget'] = bui.textwidget( parent=parent_widget, text='', position=( @@ -602,7 +605,7 @@ def instantiate_store_item_display( draw_controller=btn, color=(0.2, 1, 0.2, 1.0), ) - item['price_widget_left'] = ba.textwidget( + item['price_widget_left'] = bui.textwidget( parent=parent_widget, text='', position=( @@ -618,7 +621,7 @@ def instantiate_store_item_display( draw_controller=btn, color=(0.2, 1, 0.2, 0.5), ) - item['price_widget_right'] = ba.textwidget( + item['price_widget_right'] = bui.textwidget( parent=parent_widget, text='', position=( @@ -634,14 +637,14 @@ def instantiate_store_item_display( draw_controller=btn, color=(0.2, 1, 0.2, 1.0), ) - item['price_slash_widget'] = ba.imagewidget( + item['price_slash_widget'] = bui.imagewidget( parent=parent_widget, position=( b_pos[0] + b_width * 0.33 + b_offs_x - 36, b_pos[1] + b_height * price_v - 35, ), transition_delay=delay, - texture=ba.gettexture('slash'), + texture=bui.gettexture('slash'), opacity=0.0, size=(70, 70), draw_controller=btn, @@ -652,17 +655,17 @@ def instantiate_store_item_display( b_pos[0] + b_width * 0.1 + b_offs_x, b_pos[1] + b_height * 0.87, ) - item['sale_bg_widget'] = ba.imagewidget( + item['sale_bg_widget'] = bui.imagewidget( parent=parent_widget, position=(badge_center[0] - badge_rad, badge_center[1] - badge_rad), opacity=0.0, transition_delay=delay, - texture=ba.gettexture('circleZigZag'), + texture=bui.gettexture('circleZigZag'), draw_controller=btn, size=(badge_rad * 2, badge_rad * 2), color=(0.5, 0, 1), ) - item['sale_title_widget'] = ba.textwidget( + item['sale_title_widget'] = bui.textwidget( parent=parent_widget, position=(badge_center[0], badge_center[1] + 12), transition_delay=delay, @@ -676,7 +679,7 @@ def instantiate_store_item_display( flatness=1.0, color=(0, 1, 0), ) - item['sale_time_widget'] = ba.textwidget( + item['sale_time_widget'] = bui.textwidget( parent=parent_widget, position=(badge_center[0], badge_center[1] - 12), transition_delay=delay, diff --git a/assets/src/ba_data/python/bastd/ui/tabs.py b/src/assets/ba_data/python/bastd/ui/tabs.py similarity index 88% rename from assets/src/ba_data/python/bastd/ui/tabs.py rename to src/assets/ba_data/python/bastd/ui/tabs.py index 6ded23ee..2803e634 100644 --- a/assets/src/ba_data/python/bastd/ui/tabs.py +++ b/src/assets/ba_data/python/bastd/ui/tabs.py @@ -7,7 +7,8 @@ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, TypeVar, Generic -import ba +import babase +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable @@ -17,7 +18,7 @@ if TYPE_CHECKING: class Tab: """Info for an individual tab in a TabRow""" - button: ba.Widget + button: bui.Widget position: tuple[float, float] size: tuple[float, float] @@ -33,8 +34,8 @@ class TabRow(Generic[T]): def __init__( self, - parent: ba.Widget, - tabdefs: list[tuple[T, ba.Lstr]], + parent: bui.Widget, + tabdefs: list[tuple[T, babase.Lstr]], pos: tuple[float, float], size: tuple[float, float], on_select_call: Callable[[T], None] | None = None, @@ -49,7 +50,7 @@ class TabRow(Generic[T]): for tab_id, tab_label in tabdefs: pos = (h + tab_spacing * 0.5, tab_pos_v) size = (tab_button_width - tab_spacing, 50.0) - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=parent, position=pos, autoselect=True, @@ -57,7 +58,7 @@ class TabRow(Generic[T]): size=size, label=tab_label, enable_sound=False, - on_activate_call=ba.Call( + on_activate_call=babase.Call( self._tick_and_call, on_select_call, tab_id ), ) @@ -68,13 +69,13 @@ class TabRow(Generic[T]): """Update appearances to make the provided tab appear selected.""" for tab_id, tab in self.tabs.items(): if tab_id == selected_tab_id: - ba.buttonwidget( + bui.buttonwidget( edit=tab.button, color=(0.5, 0.4, 0.93), textcolor=(0.85, 0.75, 0.95), ) # lit else: - ba.buttonwidget( + bui.buttonwidget( edit=tab.button, color=(0.52, 0.48, 0.63), textcolor=(0.65, 0.6, 0.7), @@ -83,6 +84,6 @@ class TabRow(Generic[T]): def _tick_and_call( self, call: Callable[[Any], None] | None, arg: Any ) -> None: - ba.playsound(ba.getsound('click01')) + bui.getsound('click01').play() if call is not None: call(arg) diff --git a/assets/src/ba_data/python/bastd/ui/teamnamescolors.py b/src/assets/ba_data/python/bastd/ui/teamnamescolors.py similarity index 73% rename from assets/src/ba_data/python/bastd/ui/teamnamescolors.py rename to src/assets/ba_data/python/bastd/ui/teamnamescolors.py index 959f444d..8028ed79 100644 --- a/assets/src/ba_data/python/bastd/ui/teamnamescolors.py +++ b/src/assets/ba_data/python/bastd/ui/teamnamescolors.py @@ -6,19 +6,19 @@ from __future__ import annotations from typing import TYPE_CHECKING, cast -import ba -from bastd.ui import popup +from bastd.ui.popup import PopupWindow +from bastd.ui.colorpicker import ColorPicker +import bauiv1 as bui if TYPE_CHECKING: from typing import Sequence - from bastd.ui.colorpicker import ColorPicker -class TeamNamesColorsWindow(popup.PopupWindow): +class TeamNamesColorsWindow(PopupWindow): """A popup window for customizing team names and colors.""" def __init__(self, scale_origin: tuple[float, float]): - from ba.internal import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES + from bascenev1.internal import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES self._width = 500 self._height = 330 @@ -26,19 +26,20 @@ class TeamNamesColorsWindow(popup.PopupWindow): self._max_name_length = 16 # Creates our _root_widget. - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale scale = ( 1.69 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.1 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 0.85 ) super().__init__( position=scale_origin, size=(self._width, self._height), scale=scale ) - appconfig = ba.app.config + appconfig = bui.app.config self._names = list( appconfig.get('Custom Team Names', DEFAULT_TEAM_NAMES) ) @@ -46,18 +47,18 @@ class TeamNamesColorsWindow(popup.PopupWindow): # We need to flatten the translation since it will be an # editable string. self._names = [ - ba.Lstr(translate=('teamNames', n)).evaluate() for n in self._names + bui.Lstr(translate=('teamNames', n)).evaluate() for n in self._names ] self._colors = list( appconfig.get('Custom Team Colors', DEFAULT_TEAM_COLORS) ) - self._color_buttons: list[ba.Widget] = [] - self._color_text_fields: list[ba.Widget] = [] + self._color_buttons: list[bui.Widget] = [] + self._color_text_fields: list[bui.Widget] = [] - resetbtn = ba.buttonwidget( + resetbtn = bui.buttonwidget( parent=self.root_widget, - label=ba.Lstr(resource='settingsWindowAdvanced.resetText'), + label=bui.Lstr(resource='settingsWindowAdvanced.resetText'), autoselect=True, scale=0.7, on_activate_call=self._reset, @@ -67,11 +68,11 @@ class TeamNamesColorsWindow(popup.PopupWindow): for i in range(2): self._color_buttons.append( - ba.buttonwidget( + bui.buttonwidget( parent=self.root_widget, autoselect=True, position=(50, 0 + 195 - 90 * i), - on_activate_call=ba.Call(self._color_click, i), + on_activate_call=bui.Call(self._color_click, i), size=(70, 70), color=self._colors[i], label='', @@ -79,7 +80,7 @@ class TeamNamesColorsWindow(popup.PopupWindow): ) ) self._color_text_fields.append( - ba.textwidget( + bui.textwidget( parent=self.root_widget, position=(135, 0 + 201 - 90 * i), size=(280, 46), @@ -88,46 +89,44 @@ class TeamNamesColorsWindow(popup.PopupWindow): v_align='center', max_chars=self._max_name_length, color=self._colors[i], - description=ba.Lstr(resource='nameText'), + description=bui.Lstr(resource='nameText'), editable=True, padding=4, ) ) - ba.widget( + bui.widget( edit=self._color_text_fields[0], down_widget=self._color_text_fields[1], ) - ba.widget( + bui.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) + bui.widget(edit=self._color_text_fields[0], up_widget=resetbtn) - cancelbtn = ba.buttonwidget( + cancelbtn = bui.buttonwidget( parent=self.root_widget, - label=ba.Lstr(resource='cancelText'), + label=bui.Lstr(resource='cancelText'), autoselect=True, on_activate_call=self._on_cancel_press, size=(150, 50), position=(self._width * 0.5 - 200, 20), ) - okbtn = ba.buttonwidget( + okbtn = bui.buttonwidget( parent=self.root_widget, - label=ba.Lstr(resource='okText'), + label=bui.Lstr(resource='okText'), autoselect=True, on_activate_call=self._ok, size=(150, 50), position=(self._width * 0.5 + 50, 20), ) - ba.containerwidget( + bui.containerwidget( edit=self.root_widget, selected_child=self._color_buttons[0] ) - ba.widget(edit=okbtn, left_widget=cancelbtn) + bui.widget(edit=okbtn, left_widget=cancelbtn) self._update() def _color_click(self, i: int) -> None: - from bastd.ui.colorpicker import ColorPicker - ColorPicker( parent=self.root_widget, position=self._color_buttons[i].get_screen_space_center(), @@ -148,29 +147,29 @@ class TeamNamesColorsWindow(popup.PopupWindow): self._update() def _reset(self) -> None: - from ba.internal import DEFAULT_TEAM_NAMES, DEFAULT_TEAM_COLORS + from bascenev1.internal import DEFAULT_TEAM_NAMES, DEFAULT_TEAM_COLORS for i in range(2): self._colors[i] = DEFAULT_TEAM_COLORS[i] - name = ba.Lstr( + name = bui.Lstr( translate=('teamNames', DEFAULT_TEAM_NAMES[i]) ).evaluate() if len(name) > self._max_name_length: print('GOT DEFAULT TEAM NAME LONGER THAN MAX LENGTH') - ba.textwidget(edit=self._color_text_fields[i], text=name) + bui.textwidget(edit=self._color_text_fields[i], text=name) self._update() def _update(self) -> None: for i in range(2): - ba.buttonwidget(edit=self._color_buttons[i], color=self._colors[i]) - ba.textwidget( + bui.buttonwidget(edit=self._color_buttons[i], color=self._colors[i]) + bui.textwidget( edit=self._color_text_fields[i], color=self._colors[i] ) def _ok(self) -> None: - from ba.internal import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES + from bascenev1.internal import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES - cfg = ba.app.config + cfg = bui.app.config # First, determine whether the values here are defaults, in which case # we can clear any values from prefs. Currently if the string matches @@ -180,12 +179,12 @@ class TeamNamesColorsWindow(popup.PopupWindow): new_names: list[str] = [] is_default = True for i in range(2): - name = cast(str, ba.textwidget(query=self._color_text_fields[i])) + name = cast(str, bui.textwidget(query=self._color_text_fields[i])) if not name: - ba.screenmessage( - ba.Lstr(resource='nameNotEmptyText'), color=(1, 0, 0) + bui.screenmessage( + bui.Lstr(resource='nameNotEmptyText'), color=(1, 0, 0) ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return new_names.append(name) @@ -193,7 +192,7 @@ class TeamNamesColorsWindow(popup.PopupWindow): if self._colors[i] != DEFAULT_TEAM_COLORS[i]: is_default = False default_team_name = DEFAULT_TEAM_NAMES[i] - default_team_name_translated = ba.Lstr( + default_team_name_translated = bui.Lstr( translate=('teamNames', default_team_name) ).evaluate() if ( @@ -216,10 +215,10 @@ class TeamNamesColorsWindow(popup.PopupWindow): def _transition_out(self, transition: str = 'out_scale') -> None: if not self._transitioning_out: self._transitioning_out = True - ba.containerwidget(edit=self.root_widget, transition=transition) + bui.containerwidget(edit=self.root_widget, transition=transition) def on_popup_cancel(self) -> None: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transition_out() def _on_cancel_press(self) -> None: diff --git a/assets/src/ba_data/python/bastd/ui/tournamententry.py b/src/assets/ba_data/python/bastd/ui/tournamententry.py similarity index 71% rename from assets/src/ba_data/python/bastd/ui/tournamententry.py rename to src/assets/ba_data/python/bastd/ui/tournamententry.py index cf461912..81e89657 100644 --- a/assets/src/ba_data/python/bastd/ui/tournamententry.py +++ b/src/assets/ba_data/python/bastd/ui/tournamententry.py @@ -4,23 +4,25 @@ from __future__ import annotations +import logging from typing import TYPE_CHECKING -import ba -import ba.internal -from bastd.ui import popup +from bastd.ui.popup import PopupWindow +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Callable + import bascenev1 as bs -class TournamentEntryWindow(popup.PopupWindow): + +class TournamentEntryWindow(PopupWindow): """Popup window for entering tournaments.""" def __init__( self, tournament_id: str, - tournament_activity: ba.Activity | None = None, + tournament_activity: bs.Activity | None = None, position: tuple[float, float] = (0.0, 0.0), delegate: Any = None, scale: float | None = None, @@ -31,10 +33,11 @@ class TournamentEntryWindow(popup.PopupWindow): # pylint: disable=too-many-branches # pylint: disable=too-many-statements - ba.set_analytics_screen('Tournament Entry Window') + assert bui.app.classic is not None + bui.set_analytics_screen('Tournament Entry Window') self._tournament_id = tournament_id - self._tournament_info = ba.app.accounts_v1.tournament_info[ + self._tournament_info = bui.app.classic.accounts.tournament_info[ self._tournament_id ] @@ -63,12 +66,12 @@ class TournamentEntryWindow(popup.PopupWindow): self._on_close_call = on_close_call if scale is None: - uiscale = ba.app.ui.uiscale + uiscale = bui.app.classic.ui.uiscale scale = ( 2.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.65 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.23 ) self._delegate = delegate @@ -82,8 +85,7 @@ class TournamentEntryWindow(popup.PopupWindow): bg_color = (0.5, 0.4, 0.6) # Creates our root_widget. - popup.PopupWindow.__init__( - self, + super().__init__( position=position, size=(self._width, self._height), scale=scale, @@ -98,11 +100,11 @@ class TournamentEntryWindow(popup.PopupWindow): self._launched = False # Show the ad button only if we support ads *and* it has a level 1 fee. - self._do_ad_btn = ba.internal.has_video_ads() and self._allow_ads + self._do_ad_btn = bui.has_video_ads() and self._allow_ads x_offs = 0 if self._do_ad_btn else 85 - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self.root_widget, position=(20, self._height - 34), size=(60, 60), @@ -111,23 +113,23 @@ class TournamentEntryWindow(popup.PopupWindow): color=bg_color, on_activate_call=self._on_cancel, autoselect=True, - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=self.root_widget, position=(self._width * 0.5, self._height - 20), size=(0, 0), h_align='center', v_align='center', scale=0.6, - text=ba.Lstr(resource='tournamentEntryText'), + text=bui.Lstr(resource='tournamentEntryText'), maxwidth=180, color=(1, 1, 1, 0.4), ) - btn = self._pay_with_tickets_button = ba.buttonwidget( + btn = self._pay_with_tickets_button = bui.buttonwidget( parent=self.root_widget, position=(30 + x_offs, 60), autoselect=True, @@ -138,16 +140,16 @@ class TournamentEntryWindow(popup.PopupWindow): ) self._ticket_img_pos = (50 + x_offs, 94) self._ticket_img_pos_free = (50 + x_offs, 80) - self._ticket_img = ba.imagewidget( + self._ticket_img = bui.imagewidget( parent=self.root_widget, draw_controller=btn, size=(80, 80), position=self._ticket_img_pos, - texture=ba.gettexture('tickets'), + texture=bui.gettexture('tickets'), ) self._ticket_cost_text_position = (87 + x_offs, 88) self._ticket_cost_text_position_free = (87 + x_offs, 120) - self._ticket_cost_text = ba.textwidget( + self._ticket_cost_text = bui.textwidget( parent=self.root_widget, draw_controller=btn, position=self._ticket_cost_text_position, @@ -159,7 +161,7 @@ class TournamentEntryWindow(popup.PopupWindow): maxwidth=95, color=(0, 1, 0), ) - self._free_plays_remaining_text = ba.textwidget( + self._free_plays_remaining_text = bui.textwidget( parent=self.root_widget, draw_controller=btn, position=(87 + x_offs, 78), @@ -171,9 +173,9 @@ class TournamentEntryWindow(popup.PopupWindow): maxwidth=95, color=(0, 0.8, 0), ) - self._pay_with_ad_btn: ba.Widget | None + self._pay_with_ad_btn: bui.Widget | None if self._do_ad_btn: - btn = self._pay_with_ad_btn = ba.buttonwidget( + btn = self._pay_with_ad_btn = bui.buttonwidget( parent=self.root_widget, position=(190, 60), autoselect=True, @@ -182,12 +184,12 @@ class TournamentEntryWindow(popup.PopupWindow): label='', on_activate_call=self._on_pay_with_ad_press, ) - self._pay_with_ad_img = ba.imagewidget( + self._pay_with_ad_img = bui.imagewidget( parent=self.root_widget, draw_controller=btn, size=(80, 80), position=(210, 94), - texture=ba.gettexture('tv'), + texture=bui.gettexture('tv'), ) self._ad_text_position = (251, 88) @@ -195,7 +197,7 @@ class TournamentEntryWindow(popup.PopupWindow): have_ad_tries_remaining = ( self._tournament_info['adTriesRemaining'] is not None ) - self._ad_text = ba.textwidget( + self._ad_text = bui.textwidget( parent=self.root_widget, draw_controller=btn, position=self._ad_text_position_remaining @@ -207,7 +209,7 @@ class TournamentEntryWindow(popup.PopupWindow): scale=0.6, # Note: AdMob now requires rewarded ad usage # specifically says 'Ad' in it. - text=ba.Lstr(resource='watchAnAdText'), + text=bui.Lstr(resource='watchAnAdText'), maxwidth=95, color=(0, 1, 0), ) @@ -216,7 +218,7 @@ class TournamentEntryWindow(popup.PopupWindow): if not have_ad_tries_remaining else '' + str(self._tournament_info['adTriesRemaining']) ) - self._ad_plays_remaining_text = ba.textwidget( + self._ad_plays_remaining_text = bui.textwidget( parent=self.root_widget, draw_controller=btn, position=(251, 78), @@ -229,14 +231,14 @@ class TournamentEntryWindow(popup.PopupWindow): color=(0, 0.8, 0), ) - ba.textwidget( + bui.textwidget( parent=self.root_widget, position=(self._width * 0.5, 120), size=(0, 0), h_align='center', v_align='center', scale=0.6, - text=ba.Lstr( + text=bui.Lstr( resource='orText', subs=[('${A}', ''), ('${B}', '')] ), maxwidth=35, @@ -245,23 +247,23 @@ class TournamentEntryWindow(popup.PopupWindow): else: self._pay_with_ad_btn = None - self._get_tickets_button: ba.Widget | None = None - self._ticket_count_text: ba.Widget | None = None - if not ba.app.ui.use_toolbars: - if ba.app.allow_ticket_purchases: - self._get_tickets_button = ba.buttonwidget( + self._get_tickets_button: bui.Widget | None = None + self._ticket_count_text: bui.Widget | None = None + if not bui.app.classic.ui.use_toolbars: + if bui.app.classic.allow_ticket_purchases: + self._get_tickets_button = bui.buttonwidget( parent=self.root_widget, position=(self._width - 190 + 125, self._height - 34), autoselect=True, scale=0.5, size=(120, 60), textcolor=(0.2, 1, 0.2), - label=ba.charstr(ba.SpecialChar.TICKET), + label=bui.charstr(bui.SpecialChar.TICKET), color=(0.65, 0.5, 0.8), on_activate_call=self._on_get_tickets_press, ) else: - self._ticket_count_text = ba.textwidget( + self._ticket_count_text = bui.textwidget( parent=self.root_widget, scale=0.5, position=(self._width - 190 + 125, self._height - 34), @@ -272,15 +274,15 @@ class TournamentEntryWindow(popup.PopupWindow): self._seconds_remaining = None - ba.containerwidget( + bui.containerwidget( edit=self.root_widget, cancel_button=self._cancel_button ) # Let's also ask the server for info about this tournament # (time remaining, etc) so we can show the user time remaining, # disallow entry if time has run out, etc. - # xoffs = 104 if ba.app.ui.use_toolbars else 0 - self._time_remaining_text = ba.textwidget( + # xoffs = 104 if bui.app.ui.use_toolbars else 0 + self._time_remaining_text = bui.textwidget( parent=self.root_widget, position=(self._width / 2, 28), size=(0, 0), @@ -292,13 +294,13 @@ class TournamentEntryWindow(popup.PopupWindow): flatness=1.0, color=(0.7, 0.7, 0.7), ) - self._time_remaining_label_text = ba.textwidget( + self._time_remaining_label_text = bui.textwidget( parent=self.root_widget, position=(self._width / 2, 45), size=(0, 0), h_align='center', v_align='center', - text=ba.Lstr(resource='coopSelectWindow.timeRemainingText'), + text=bui.Lstr(resource='coopSelectWindow.timeRemainingText'), scale=0.45, flatness=1.0, maxwidth=100, @@ -310,46 +312,39 @@ class TournamentEntryWindow(popup.PopupWindow): # If there seems to be a relatively-recent valid cached info for this # tournament, use it. Otherwise we'll kick off a query ourselves. if ( - self._tournament_id in ba.app.accounts_v1.tournament_info - and ba.app.accounts_v1.tournament_info[self._tournament_id]['valid'] + self._tournament_id in bui.app.classic.accounts.tournament_info + and bui.app.classic.accounts.tournament_info[self._tournament_id][ + 'valid' + ] and ( - ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) - - ba.app.accounts_v1.tournament_info[self._tournament_id][ + bui.apptime() + - bui.app.classic.accounts.tournament_info[self._tournament_id][ 'timeReceived' ] - < 1000 * 60 * 5 + < 60 * 5 ) ): try: - info = ba.app.accounts_v1.tournament_info[self._tournament_id] + info = bui.app.classic.accounts.tournament_info[ + self._tournament_id + ] self._seconds_remaining = max( 0, info['timeRemaining'] - - int( - ( - ba.time( - ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS - ) - - info['timeReceived'] - ) - / 1000 - ), + - int((bui.apptime() - info['timeReceived'])), ) self._have_valid_data = True - self._last_query_time = ba.time(ba.TimeType.REAL) + self._last_query_time = bui.apptime() except Exception: - ba.print_exception('error using valid tourney data') + logging.exception('Error using valid tourney data.') self._have_valid_data = False else: self._have_valid_data = False - self._fg_state = ba.app.fg_state + self._fg_state = bui.app.fg_state self._running_query = False - self._update_timer = ba.Timer( - 1.0, - ba.WeakCall(self._update), - repeat=True, - timetype=ba.TimeType.REAL, + self._update_timer = bui.AppTimer( + 1.0, bui.WeakCall(self._update), repeat=True ) self._update() self._restore_state() @@ -357,7 +352,8 @@ class TournamentEntryWindow(popup.PopupWindow): def _on_tournament_query_response( self, data: dict[str, Any] | None ) -> None: - accounts = ba.app.accounts_v1 + assert bui.app.classic is not None + accounts = bui.app.classic.accounts self._running_query = False if data is not None: data = data['t'] # This used to be the whole payload. @@ -375,76 +371,76 @@ class TournamentEntryWindow(popup.PopupWindow): sel_name = 'Ad' else: sel_name = 'Tickets' - cfg = ba.app.config + cfg = bui.app.config cfg['Tournament Pay Selection'] = sel_name cfg.commit() def _restore_state(self) -> None: - sel_name = ba.app.config.get('Tournament Pay Selection', 'Tickets') + sel_name = bui.app.config.get('Tournament Pay Selection', 'Tickets') if sel_name == 'Ad' and self._pay_with_ad_btn is not None: sel = self._pay_with_ad_btn else: sel = self._pay_with_tickets_button - ba.containerwidget(edit=self.root_widget, selected_child=sel) + bui.containerwidget(edit=self.root_widget, selected_child=sel) def _update(self) -> None: + plus = bui.app.plus + assert plus is not None + # We may outlive our widgets. if not self.root_widget: return # If we've been foregrounded/backgrounded we need to re-grab data. - if self._fg_state != ba.app.fg_state: - self._fg_state = ba.app.fg_state + if self._fg_state != bui.app.fg_state: + self._fg_state = bui.app.fg_state self._have_valid_data = False # If we need to run another tournament query, do so. if not self._running_query and ( (self._last_query_time is None) or (not self._have_valid_data) - or (ba.time(ba.TimeType.REAL) - self._last_query_time > 30.0) + or (bui.apptime() - self._last_query_time > 30.0) ): - ba.internal.tournament_query( + plus.tournament_query( args={ 'source': 'entry window' if self._tournament_activity is None else 'retry entry window' }, - callback=ba.WeakCall(self._on_tournament_query_response), + callback=bui.WeakCall(self._on_tournament_query_response), ) - self._last_query_time = ba.time(ba.TimeType.REAL) + self._last_query_time = bui.apptime() self._running_query = True # Grab the latest info on our tourney. - self._tournament_info = ba.app.accounts_v1.tournament_info[ + assert bui.app.classic is not None + self._tournament_info = bui.app.classic.accounts.tournament_info[ self._tournament_id ] # If we don't have valid data always show a '-' for time. if not self._have_valid_data: - ba.textwidget(edit=self._time_remaining_text, text='-') + bui.textwidget(edit=self._time_remaining_text, text='-') else: if self._seconds_remaining is not None: self._seconds_remaining = max(0, self._seconds_remaining - 1) - ba.textwidget( + bui.textwidget( edit=self._time_remaining_text, - text=ba.timestring( - self._seconds_remaining * 1000, - centi=False, - timeformat=ba.TimeFormat.MILLISECONDS, - ), + text=bui.timestring(self._seconds_remaining, centi=False), ) # Keep price up-to-date and update the button with it. - self._purchase_price = ba.internal.get_v1_account_misc_read_val( + self._purchase_price = plus.get_v1_account_misc_read_val( self._purchase_price_name, None ) - ba.textwidget( + bui.textwidget( edit=self._ticket_cost_text, text=( - ba.Lstr(resource='getTicketsWindow.freeText') + bui.Lstr(resource='getTicketsWindow.freeText') if self._purchase_price == 0 - else ba.Lstr( + else bui.Lstr( resource='getTicketsWindow.ticketsText', subs=[ ( @@ -462,7 +458,7 @@ class TournamentEntryWindow(popup.PopupWindow): scale=1.0 if self._purchase_price == 0 else 0.6, ) - ba.textwidget( + bui.textwidget( edit=self._free_plays_remaining_text, text='' if ( @@ -472,7 +468,7 @@ class TournamentEntryWindow(popup.PopupWindow): else '' + str(self._tournament_info['freeTriesRemaining']), ) - ba.imagewidget( + bui.imagewidget( edit=self._ticket_img, opacity=0.2 if self._purchase_price == 0 else 1.0, position=self._ticket_img_pos_free @@ -481,22 +477,22 @@ class TournamentEntryWindow(popup.PopupWindow): ) if self._do_ad_btn: - enabled = ba.internal.have_incentivized_ad() + enabled = bui.have_incentivized_ad() have_ad_tries_remaining = ( self._tournament_info['adTriesRemaining'] is not None and self._tournament_info['adTriesRemaining'] > 0 ) - ba.textwidget( + bui.textwidget( edit=self._ad_text, position=self._ad_text_position_remaining if have_ad_tries_remaining else self._ad_text_position, color=(0, 1, 0) if enabled else (0.5, 0.5, 0.5), ) - ba.imagewidget( + bui.imagewidget( edit=self._pay_with_ad_img, opacity=1.0 if enabled else 0.2 ) - ba.buttonwidget( + bui.buttonwidget( edit=self._pay_with_ad_btn, color=(0.5, 0.7, 0.2) if enabled else (0.5, 0.5, 0.5), ) @@ -505,28 +501,29 @@ class TournamentEntryWindow(popup.PopupWindow): if not have_ad_tries_remaining else '' + str(self._tournament_info['adTriesRemaining']) ) - ba.textwidget( + bui.textwidget( edit=self._ad_plays_remaining_text, text=ad_plays_remaining_text, color=(0, 0.8, 0) if enabled else (0.4, 0.4, 0.4), ) try: - t_str = str(ba.internal.get_v1_account_ticket_count()) + t_str = str(plus.get_v1_account_ticket_count()) except Exception: t_str = '?' if self._get_tickets_button: - ba.buttonwidget( + bui.buttonwidget( edit=self._get_tickets_button, - label=ba.charstr(ba.SpecialChar.TICKET) + t_str, + label=bui.charstr(bui.SpecialChar.TICKET) + t_str, ) if self._ticket_count_text: - ba.textwidget( + bui.textwidget( edit=self._ticket_count_text, - text=ba.charstr(ba.SpecialChar.TICKET) + t_str, + text=bui.charstr(bui.SpecialChar.TICKET) + t_str, ) def _launch(self) -> None: + assert bui.app.classic is not None if self._launched: return self._launched = True @@ -535,19 +532,15 @@ class TournamentEntryWindow(popup.PopupWindow): # If they gave us an existing activity, just restart it. if self._tournament_activity is not None: try: - ba.timer( - 0.1, - lambda: ba.playsound(ba.getsound('cashRegister')), - timetype=ba.TimeType.REAL, - ) - with ba.Context(self._tournament_activity): + bui.apptimer(0.1, bui.getsound('cashRegister').play) + with self._tournament_activity.context: self._tournament_activity.end( {'outcome': 'restart'}, force=True ) - ba.timer(0.3, self._transition_out, timetype=ba.TimeType.REAL) + bui.apptimer(0.3, self._transition_out) launched = True - ba.screenmessage( - ba.Lstr( + bui.screenmessage( + bui.Lstr( translate=('serverResponses', 'Entering tournament...') ), color=(0, 1, 0), @@ -559,31 +552,28 @@ class TournamentEntryWindow(popup.PopupWindow): # This is not ideal since players will have to rejoin, etc., # but it works for now. except Exception: - ba.print_exception('Error restarting tournament activity.') + logging.exception('Error restarting tournament activity.') # If we had no existing activity (or were unable to restart it) # launch a new session. if not launched: - ba.timer( - 0.1, - lambda: ba.playsound(ba.getsound('cashRegister')), - timetype=ba.TimeType.REAL, - ) - ba.timer( + bui.apptimer(0.1, bui.getsound('cashRegister').play) + bui.apptimer( 1.0, - lambda: ba.app.launch_coop_game( + lambda: bui.app.classic.launch_coop_game( self._tournament_info['game'], args={ 'min_players': self._tournament_info['minPlayers'], 'max_players': self._tournament_info['maxPlayers'], 'tournament_id': self._tournament_id, }, - ), - timetype=ba.TimeType.REAL, + ) + if bui.app.classic is not None + else None, ) - ba.timer(0.7, self._transition_out, timetype=ba.TimeType.REAL) - ba.screenmessage( - ba.Lstr( + bui.apptimer(0.7, self._transition_out) + bui.screenmessage( + bui.Lstr( translate=('serverResponses', 'Entering tournament...') ), color=(0, 1, 0), @@ -592,97 +582,105 @@ class TournamentEntryWindow(popup.PopupWindow): def _on_pay_with_tickets_press(self) -> None: from bastd.ui import getcurrency + plus = bui.app.plus + assert plus is not None + # If we're already entering, ignore. if self._entering: return if not self._have_valid_data: - ba.screenmessage( - ba.Lstr(resource='tournamentCheckingStateText'), color=(1, 0, 0) + bui.screenmessage( + bui.Lstr(resource='tournamentCheckingStateText'), + color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return # If we don't have a price. if self._purchase_price is None: - ba.screenmessage( - ba.Lstr(resource='tournamentCheckingStateText'), color=(1, 0, 0) + bui.screenmessage( + bui.Lstr(resource='tournamentCheckingStateText'), + color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return # Deny if it looks like the tourney has ended. if self._seconds_remaining == 0: - ba.screenmessage( - ba.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) + bui.screenmessage( + bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return # Deny if we don't have enough tickets. ticket_count: int | None try: - ticket_count = ba.internal.get_v1_account_ticket_count() + ticket_count = plus.get_v1_account_ticket_count() except Exception: - # FIXME: should add a ba.NotSignedInError we can use here. + # FIXME: should add a bui.NotSignedInError we can use here. ticket_count = None ticket_cost = self._purchase_price if ticket_count is not None and ticket_count < ticket_cost: getcurrency.show_get_tickets_prompt() - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() self._transition_out() return - cur_time = ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) + cur_time = bui.apptime() self._last_ticket_press_time = cur_time assert isinstance(ticket_cost, int) - ba.internal.in_game_purchase(self._purchase_name, ticket_cost) + plus.in_game_purchase(self._purchase_name, ticket_cost) self._entering = True - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'ENTER_TOURNAMENT', 'fee': self._fee, 'tournamentID': self._tournament_id, } ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() self._launch() def _on_pay_with_ad_press(self) -> None: - # If we're already entering, ignore. if self._entering: return if not self._have_valid_data: - ba.screenmessage( - ba.Lstr(resource='tournamentCheckingStateText'), color=(1, 0, 0) + bui.screenmessage( + bui.Lstr(resource='tournamentCheckingStateText'), + color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return # Deny if it looks like the tourney has ended. if self._seconds_remaining == 0: - ba.screenmessage( - ba.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) + bui.screenmessage( + bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return - cur_time = ba.time(ba.TimeType.REAL) + cur_time = bui.apptime() if cur_time - self._last_ad_press_time > 5.0: self._last_ad_press_time = cur_time - ba.app.ads.show_ad_2( + assert bui.app.classic is not None + bui.app.classic.ads.show_ad_2( 'tournament_entry', - on_completion_call=ba.WeakCall(self._on_ad_complete), + on_completion_call=bui.WeakCall(self._on_ad_complete), ) def _on_ad_complete(self, actually_showed: bool) -> None: + plus = bui.app.plus + assert plus is not None # Make sure any transactions the ad added got locally applied # (rewards added, etc.). - ba.internal.run_transactions() + plus.run_v1_account_transactions() # If we're already entering the tourney, ignore. if self._entering: @@ -694,21 +692,21 @@ class TournamentEntryWindow(popup.PopupWindow): # This should have awarded us the tournament_entry_ad purchase; # make sure that's present. # (otherwise the server will ignore our tournament entry anyway) - if not ba.internal.get_purchased('tournament_entry_ad'): + if not plus.get_purchased('tournament_entry_ad'): print('no tournament_entry_ad purchase present in _on_ad_complete') - ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) - ba.playsound(ba.getsound('error')) + bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0)) + bui.getsound('error').play() return self._entering = True - ba.internal.add_transaction( + plus.add_v1_account_transaction( { 'type': 'ENTER_TOURNAMENT', 'fee': 'ad', 'tournamentID': self._tournament_id, } ) - ba.internal.run_transactions() + plus.run_v1_account_transactions() self._launch() def _on_get_tickets_press(self) -> None: @@ -726,20 +724,17 @@ class TournamentEntryWindow(popup.PopupWindow): self._transition_out() def _on_cancel(self) -> None: - + plus = bui.app.plus + assert plus is not None # Don't allow canceling for several seconds after poking an enter # button if it looks like we're waiting on a purchase or entering # the tournament. - if ( - ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) - - self._last_ticket_press_time - < 6000 - ) and ( - ba.internal.have_outstanding_transactions() - or ba.internal.get_purchased(self._purchase_name) + if (bui.apptime() - self._last_ticket_press_time < 6.0) and ( + plus.have_outstanding_v1_account_transactions() + or plus.get_purchased(self._purchase_name) or self._entering ): - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return self._transition_out() @@ -749,10 +744,10 @@ class TournamentEntryWindow(popup.PopupWindow): if not self._transitioning_out: self._transitioning_out = True self._save_state() - ba.containerwidget(edit=self.root_widget, transition='out_scale') + bui.containerwidget(edit=self.root_widget, transition='out_scale') if self._on_close_call is not None: self._on_close_call() def on_popup_cancel(self) -> None: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._on_cancel() diff --git a/assets/src/ba_data/python/bastd/ui/tournamentscores.py b/src/assets/ba_data/python/bastd/ui/tournamentscores.py similarity index 76% rename from assets/src/ba_data/python/bastd/ui/tournamentscores.py rename to src/assets/ba_data/python/bastd/ui/tournamentscores.py index 418f5788..00f513e7 100644 --- a/assets/src/ba_data/python/bastd/ui/tournamentscores.py +++ b/src/assets/ba_data/python/bastd/ui/tournamentscores.py @@ -6,21 +6,22 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba -import ba.internal -from bastd.ui import popup as popup_ui +from bastd.ui.popup import PopupWindow +import bauiv1 as bui if TYPE_CHECKING: from typing import Any, Sequence, Callable + import bascenev1 as bs -class TournamentScoresWindow(popup_ui.PopupWindow): + +class TournamentScoresWindow(PopupWindow): """Window for viewing tournament scores.""" def __init__( self, tournament_id: str, - tournament_activity: ba.GameActivity | None = None, + tournament_activity: bs.GameActivity | None = None, position: tuple[float, float] = (0.0, 0.0), scale: float | None = None, offset: tuple[float, float] = (0.0, 0.0), @@ -29,21 +30,24 @@ class TournamentScoresWindow(popup_ui.PopupWindow): selected_character: str | None = None, on_close_call: Callable[[], Any] | None = None, ): + plus = bui.app.plus + assert plus is not None del tournament_activity # unused arg del tint_color # unused arg del tint2_color # unused arg del selected_character # unused arg self._tournament_id = tournament_id - self._subcontainer: ba.Widget | None = None + self._subcontainer: bui.Widget | None = None self._on_close_call = on_close_call - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale if scale is None: scale = ( 2.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.65 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.23 ) self._transitioning_out = False @@ -51,9 +55,9 @@ class TournamentScoresWindow(popup_ui.PopupWindow): self._width = 400 self._height = ( 300 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 370 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 450 ) @@ -68,9 +72,7 @@ class TournamentScoresWindow(popup_ui.PopupWindow): offset=offset, ) - # app = ba.app - - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self.root_widget, position=(50, self._height - 30), size=(50, 50), @@ -79,54 +81,54 @@ class TournamentScoresWindow(popup_ui.PopupWindow): color=bg_color, on_activate_call=self._on_cancel_press, autoselect=True, - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=self.root_widget, position=(self._width * 0.5, self._height - 20), size=(0, 0), h_align='center', v_align='center', scale=0.6, - text=ba.Lstr(resource='tournamentStandingsText'), + text=bui.Lstr(resource='tournamentStandingsText'), maxwidth=200, color=(1, 1, 1, 0.4), ) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self.root_widget, size=(self._width - 60, self._height - 70), position=(30, 30), highlight=False, simple_culling_v=10, ) - ba.widget(edit=self._scrollwidget, autoselect=True) + bui.widget(edit=self._scrollwidget, autoselect=True) - self._loading_text = ba.textwidget( + self._loading_text = bui.textwidget( parent=self._scrollwidget, scale=0.5, - text=ba.Lstr( + text=bui.Lstr( value='${A}...', - subs=[('${A}', ba.Lstr(resource='loadingText'))], + subs=[('${A}', bui.Lstr(resource='loadingText'))], ), size=(self._width - 60, 100), h_align='center', v_align='center', ) - ba.containerwidget( + bui.containerwidget( edit=self.root_widget, cancel_button=self._cancel_button ) - ba.internal.tournament_query( + plus.tournament_query( args={ 'tournamentIDs': [tournament_id], 'numScores': 50, 'source': 'scores window', }, - callback=ba.WeakCall(self._on_tournament_query_response), + callback=bui.WeakCall(self._on_tournament_query_response), ) def _on_tournament_query_response( @@ -140,21 +142,20 @@ class TournamentScoresWindow(popup_ui.PopupWindow): if data_t[0]['scores']: self._loading_text.delete() else: - ba.textwidget( + bui.textwidget( edit=self._loading_text, - text=ba.Lstr(resource='noScoresYetText'), + text=bui.Lstr(resource='noScoresYetText'), ) incr = 30 sub_width = self._width - 90 sub_height = 30 + len(data_t[0]['scores']) * incr - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(sub_width, sub_height), background=False, ) for i, entry in enumerate(data_t[0]['scores']): - - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(sub_width * 0.1 - 5, sub_height - 20 - incr * i), maxwidth=20, @@ -168,7 +169,7 @@ class TournamentScoresWindow(popup_ui.PopupWindow): v_align='center', ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(sub_width * 0.25 - 2, sub_height - 20 - incr * i), maxwidth=sub_width * 0.24, @@ -177,10 +178,9 @@ class TournamentScoresWindow(popup_ui.PopupWindow): shadow=0.0, scale=0.6, text=( - ba.timestring( - entry[0] * 10, + bui.timestring( + (entry[0] * 10) / 1000.0, centi=True, - timeformat=ba.TimeFormat.MILLISECONDS, ) if data_t[0]['scoreType'] == 'time' else str(entry[0]) @@ -190,7 +190,7 @@ class TournamentScoresWindow(popup_ui.PopupWindow): v_align='center', ) - txt = ba.textwidget( + txt = bui.textwidget( parent=self._subcontainer, position=( sub_width * 0.25, @@ -200,7 +200,7 @@ class TournamentScoresWindow(popup_ui.PopupWindow): scale=0.7, flatness=1.0, shadow=0.0, - text=ba.Lstr(value=entry[1]), + text=bui.Lstr(value=entry[1]), selectable=True, click_activate=True, autoselect=True, @@ -210,23 +210,23 @@ class TournamentScoresWindow(popup_ui.PopupWindow): v_align='center', ) - ba.textwidget( + bui.textwidget( edit=txt, - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._show_player_info, entry, txt ), ) if i == 0: - ba.widget(edit=txt, up_widget=self._cancel_button) + bui.widget(edit=txt, up_widget=self._cancel_button) - def _show_player_info(self, entry: Any, textwidget: ba.Widget) -> None: + def _show_player_info(self, entry: Any, textwidget: bui.Widget) -> None: from bastd.ui.account.viewer import AccountViewerWindow # for the moment we only work if a single player-info is present.. if len(entry[2]) != 1: - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() return - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() AccountViewerWindow( account_id=entry[2][0].get('a', None), profile_id=entry[2][0].get('p', None), @@ -240,10 +240,10 @@ class TournamentScoresWindow(popup_ui.PopupWindow): def _transition_out(self) -> None: if not self._transitioning_out: self._transitioning_out = True - ba.containerwidget(edit=self.root_widget, transition='out_scale') + bui.containerwidget(edit=self.root_widget, transition='out_scale') if self._on_close_call is not None: self._on_close_call() def on_popup_cancel(self) -> None: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transition_out() diff --git a/assets/src/ba_data/python/bastd/ui/trophies.py b/src/assets/ba_data/python/bastd/ui/trophies.py similarity index 84% rename from assets/src/ba_data/python/bastd/ui/trophies.py rename to src/assets/ba_data/python/bastd/ui/trophies.py index f263cc23..cb45be6e 100644 --- a/assets/src/ba_data/python/bastd/ui/trophies.py +++ b/src/assets/ba_data/python/bastd/ui/trophies.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import ba from bastd.ui import popup +import bauiv1 as bui if TYPE_CHECKING: from typing import Any @@ -23,13 +23,14 @@ class TrophiesWindow(popup.PopupWindow): scale: float | None = None, ): self._data = data - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale if scale is None: scale = ( 2.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.65 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.23 ) self._transitioning_out = False @@ -37,15 +38,14 @@ class TrophiesWindow(popup.PopupWindow): self._height = 300 bg_color = (0.5, 0.4, 0.6) - popup.PopupWindow.__init__( - self, + super().__init__( position=position, size=(self._width, self._height), scale=scale, bg_color=bg_color, ) - self._cancel_button = ba.buttonwidget( + self._cancel_button = bui.buttonwidget( parent=self.root_widget, position=(50, self._height - 30), size=(50, 50), @@ -54,31 +54,31 @@ class TrophiesWindow(popup.PopupWindow): color=bg_color, on_activate_call=self._on_cancel_press, autoselect=True, - icon=ba.gettexture('crossOut'), + icon=bui.gettexture('crossOut'), iconscale=1.2, ) - self._title_text = ba.textwidget( + self._title_text = bui.textwidget( parent=self.root_widget, position=(self._width * 0.5, self._height - 20), size=(0, 0), h_align='center', v_align='center', scale=0.6, - text=ba.Lstr(resource='trophiesText'), + text=bui.Lstr(resource='trophiesText'), maxwidth=200, color=(1, 1, 1, 0.4), ) - self._scrollwidget = ba.scrollwidget( + self._scrollwidget = bui.scrollwidget( parent=self.root_widget, size=(self._width - 60, self._height - 70), position=(30, 30), capture_arrows=True, ) - ba.widget(edit=self._scrollwidget, autoselect=True) + bui.widget(edit=self._scrollwidget, autoselect=True) - ba.containerwidget( + bui.containerwidget( edit=self.root_widget, cancel_button=self._cancel_button ) @@ -88,11 +88,11 @@ class TrophiesWindow(popup.PopupWindow): trophy_types = [['0a'], ['0b'], ['1'], ['2'], ['3'], ['4']] sub_height = 40 + len(trophy_types) * incr - eq_text = ba.Lstr( + eq_text = bui.Lstr( resource='coopSelectWindow.powerRankingPointsEqualsText' ).evaluate() - self._subcontainer = ba.containerwidget( + self._subcontainer = bui.containerwidget( parent=self._scrollwidget, size=(sub_width, sub_height), background=False, @@ -100,7 +100,7 @@ class TrophiesWindow(popup.PopupWindow): total_pts = 0 - multi_txt = ba.Lstr( + multi_txt = bui.Lstr( resource='coopSelectWindow.powerRankingPointsMultText' ).evaluate() @@ -108,7 +108,7 @@ class TrophiesWindow(popup.PopupWindow): eq_text, incr, multi_txt, sub_height, sub_width, trophy_types ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=( sub_width * 1.0, @@ -119,7 +119,7 @@ class TrophiesWindow(popup.PopupWindow): color=(0.7, 0.8, 1.0), flatness=1.0, shadow=0.0, - text=ba.Lstr(resource='coopSelectWindow.totalText').evaluate() + text=bui.Lstr(resource='coopSelectWindow.totalText').evaluate() + ' ' + eq_text.replace('${NUMBER}', str(total_pts)), size=(0, 0), @@ -136,13 +136,13 @@ class TrophiesWindow(popup.PopupWindow): sub_width: int, trophy_types: list[list[str]], ) -> int: - from ba.internal import get_trophy_string + from bascenev1.internal import get_trophy_string total_pts = 0 for i, trophy_type in enumerate(trophy_types): t_count = self._data['t' + trophy_type[0]] t_mult = self._data['t' + trophy_type[0] + 'm'] - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(sub_width * 0.15, sub_height - 20 - incr * i), scale=0.7, @@ -155,7 +155,7 @@ class TrophiesWindow(popup.PopupWindow): v_align='center', ) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(sub_width * 0.31, sub_height - 20 - incr * i), maxwidth=sub_width * 0.2, @@ -170,7 +170,7 @@ class TrophiesWindow(popup.PopupWindow): ) txt = multi_txt.replace('${NUMBER}', str(t_mult)) - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(sub_width * 0.57, sub_height - 20 - incr * i), maxwidth=sub_width * 0.3, @@ -187,7 +187,7 @@ class TrophiesWindow(popup.PopupWindow): ) this_pts = t_count * t_mult - ba.textwidget( + bui.textwidget( parent=self._subcontainer, position=(sub_width * 0.88, sub_height - 20 - incr * i), maxwidth=sub_width * 0.3, @@ -211,8 +211,8 @@ class TrophiesWindow(popup.PopupWindow): def _transition_out(self) -> None: if not self._transitioning_out: self._transitioning_out = True - ba.containerwidget(edit=self.root_widget, transition='out_scale') + bui.containerwidget(edit=self.root_widget, transition='out_scale') def on_popup_cancel(self) -> None: - ba.playsound(ba.getsound('swish')) + bui.getsound('swish').play() self._transition_out() diff --git a/assets/src/ba_data/python/bastd/ui/url.py b/src/assets/ba_data/python/bastd/ui/url.py similarity index 70% rename from assets/src/ba_data/python/bastd/ui/url.py rename to src/assets/ba_data/python/bastd/ui/url.py index bcecbb95..a9355a00 100644 --- a/assets/src/ba_data/python/bastd/ui/url.py +++ b/src/assets/ba_data/python/bastd/ui/url.py @@ -4,52 +4,51 @@ from __future__ import annotations -import ba -import ba.internal +import bauiv1 as bui -class ShowURLWindow(ba.Window): +class ShowURLWindow(bui.Window): """A window presenting a URL to the user visually.""" def __init__(self, address: str): - # in some cases we might want to show it as a qr code # (for long URLs especially) - app = ba.app - uiscale = app.ui.uiscale + app = bui.app + assert app.classic is not None + uiscale = app.classic.ui.uiscale self._address = address self._width = 800 self._height = 450 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + 40), transition='in_right', scale=( 1.25 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.25 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.25 ), ) ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 10), size=(0, 0), - color=ba.app.ui.title_color, + color=app.classic.ui.title_color, h_align='center', v_align='center', - text=ba.Lstr(resource='directBrowserToURLText'), + text=bui.Lstr(resource='directBrowserToURLText'), maxwidth=self._width * 0.95, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 60), size=(0, 0), scale=1.3, - color=ba.app.ui.infotextcolor, + color=app.classic.ui.infotextcolor, h_align='center', v_align='center', text=address, @@ -58,20 +57,20 @@ class ShowURLWindow(ba.Window): button_width = 200 qr_size = 220 - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, position=( self._width * 0.5 - qr_size * 0.5, self._height * 0.5 - qr_size * 0.5 + 10, ), size=(qr_size, qr_size), - texture=ba.internal.get_qrcode_texture(address), + texture=bui.get_qrcode_texture(address), ) xoffs = 0 - if ba.clipboard_is_supported(): + if bui.clipboard_is_supported(): xoffs = -150 - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=( self._width * 0.5 - button_width * 0.5 + xoffs, @@ -79,22 +78,22 @@ class ShowURLWindow(ba.Window): ), size=(button_width, 65), autoselect=True, - label=ba.Lstr(resource='copyText'), + label=bui.Lstr(resource='copyText'), on_activate_call=self._copy, ) xoffs = 150 - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=(self._width * 0.5 - button_width * 0.5 + xoffs, 20), size=(button_width, 65), autoselect=True, - label=ba.Lstr(resource='doneText'), + label=bui.Lstr(resource='doneText'), on_activate_call=self._done, ) # we have no 'cancel' button but still want to be able to # hit back/escape/etc to leave.. - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=btn, start_button=btn, @@ -102,8 +101,8 @@ class ShowURLWindow(ba.Window): ) def _copy(self) -> None: - ba.clipboard_set_text(self._address) - ba.screenmessage(ba.Lstr(resource='copyConfirmText'), color=(0, 1, 0)) + bui.clipboard_set_text(self._address) + bui.screenmessage(bui.Lstr(resource='copyConfirmText'), color=(0, 1, 0)) def _done(self) -> None: - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.containerwidget(edit=self._root_widget, transition='out_left') diff --git a/assets/src/ba_data/python/bastd/ui/v2upgrade.py b/src/assets/ba_data/python/bastd/ui/v2upgrade.py similarity index 67% rename from assets/src/ba_data/python/bastd/ui/v2upgrade.py rename to src/assets/ba_data/python/bastd/ui/v2upgrade.py index 7210c95e..7550f01c 100644 --- a/assets/src/ba_data/python/bastd/ui/v2upgrade.py +++ b/src/assets/ba_data/python/bastd/ui/v2upgrade.py @@ -4,52 +4,52 @@ from __future__ import annotations -import ba -import ba.internal +import bauiv1 as bui -class V2UpgradeWindow(ba.Window): +class V2UpgradeWindow(bui.Window): """A window presenting a URL to the user visually.""" def __init__(self, login_name: str, code: str): from bastd.ui.account.settings import show_what_is_v2_page - app = ba.app - uiscale = app.ui.uiscale + app = bui.app + assert app.classic is not None + uiscale = app.classic.ui.uiscale self._code = code self._width = 700 self._height = 270 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + 40), transition='in_right', scale=( 1.25 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.25 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.25 ), ) ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 46), size=(0, 0), - color=ba.app.ui.title_color, + color=app.classic.ui.title_color, h_align='center', v_align='center', - text=ba.Lstr( + text=bui.Lstr( resource='deviceAccountUpgradeText', subs=[('${NAME}', login_name)], ), maxwidth=self._width * 0.95, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, 125), size=(0, 0), @@ -58,60 +58,63 @@ class V2UpgradeWindow(ba.Window): h_align='center', v_align='center', text=( - ba.charstr(ba.SpecialChar.LOCAL_ACCOUNT) + bui.charstr(bui.SpecialChar.LOCAL_ACCOUNT) + login_name + ' ----> ' - + ba.charstr(ba.SpecialChar.V2_LOGO) + + bui.charstr(bui.SpecialChar.V2_LOGO) + login_name ), maxwidth=self._width * 0.95, ) button_width = 200 - cancel_button = ba.buttonwidget( + cancel_button = bui.buttonwidget( parent=self._root_widget, position=(20, 25), size=(button_width, 65), autoselect=True, - label=ba.Lstr(resource='notNowText'), + label=bui.Lstr(resource='notNowText'), on_activate_call=self._done, ) - _what_is_this_button = ba.buttonwidget( + _what_is_this_button = bui.buttonwidget( parent=self._root_widget, position=(self._width * 0.5 - button_width * 0.5, 25), size=(button_width, 65), autoselect=True, - label=ba.Lstr(resource='whatIsThisText'), + label=bui.Lstr(resource='whatIsThisText'), color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), on_activate_call=show_what_is_v2_page, ) - upgrade_button = ba.buttonwidget( + upgrade_button = bui.buttonwidget( parent=self._root_widget, position=(self._width - button_width - 20, 25), size=(button_width, 65), autoselect=True, - label=ba.Lstr(resource='upgradeText'), + label=bui.Lstr(resource='upgradeText'), on_activate_call=self._upgrade_press, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=upgrade_button, cancel_button=cancel_button, ) def _upgrade_press(self) -> None: + plus = bui.app.plus + assert plus is not None + # Get rid of the window and sign out before kicking the # user over to a browser to do the upgrade. This hopefully # makes it more clear when they come back that they need to # sign in with the 'BombSquad account' option. - ba.containerwidget(edit=self._root_widget, transition='out_left') - ba.internal.sign_out_v1() - bamasteraddr = ba.internal.get_master_server_address(version=2) - ba.open_url(f'{bamasteraddr}/v2uda/{self._code}') + bui.containerwidget(edit=self._root_widget, transition='out_left') + plus.sign_out_v1() + bamasteraddr = plus.get_master_server_address(version=2) + bui.open_url(f'{bamasteraddr}/v2uda/{self._code}') def _done(self) -> None: - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.containerwidget(edit=self._root_widget, transition='out_left') diff --git a/assets/src/ba_data/python/bastd/ui/watch.py b/src/assets/ba_data/python/bastd/ui/watch.py similarity index 65% rename from assets/src/ba_data/python/bastd/ui/watch.py rename to src/assets/ba_data/python/bastd/ui/watch.py index 85d484e0..c08c5e85 100644 --- a/assets/src/ba_data/python/bastd/ui/watch.py +++ b/src/assets/ba_data/python/bastd/ui/watch.py @@ -5,17 +5,18 @@ from __future__ import annotations import os +import logging from enum import Enum from typing import TYPE_CHECKING, cast -import ba -import ba.internal +import bascenev1 as bs +import bauiv1 as bui if TYPE_CHECKING: from typing import Any -class WatchWindow(ba.Window): +class WatchWindow(bui.Window): """Window for watching replays.""" class TabID(Enum): @@ -27,13 +28,13 @@ class WatchWindow(ba.Window): def __init__( self, transition: str | None = 'in_right', - origin_widget: ba.Widget | None = None, + origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-locals # pylint: disable=too-many-statements from bastd.ui.tabs import TabRow - ba.set_analytics_screen('Watch Window') + bui.set_analytics_screen('Watch Window') scale_origin: tuple[float, float] | None if origin_widget is not None: self._transition_out = 'out_scale' @@ -42,92 +43,93 @@ class WatchWindow(ba.Window): else: self._transition_out = 'out_right' scale_origin = None - ba.app.ui.set_main_menu_location('Watch') + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_location('Watch') self._tab_data: dict[str, Any] = {} self._my_replays_scroll_width: float | None = None - self._my_replays_watch_replay_button: ba.Widget | None = None - self._scrollwidget: ba.Widget | None = None - self._columnwidget: ba.Widget | None = None + self._my_replays_watch_replay_button: bui.Widget | None = None + self._scrollwidget: bui.Widget | None = None + self._columnwidget: bui.Widget | None = None self._my_replay_selected: str | None = None - self._my_replays_rename_window: ba.Widget | None = None - self._my_replay_rename_text: ba.Widget | None = None + self._my_replays_rename_window: bui.Widget | None = None + self._my_replay_rename_text: bui.Widget | None = None self._r = 'watchWindow' - uiscale = ba.app.ui.uiscale - self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 - x_inset = 100 if uiscale is ba.UIScale.SMALL else 0 + uiscale = bui.app.classic.ui.uiscale + self._width = 1240 if uiscale is bui.UIScale.SMALL else 1040 + x_inset = 100 if uiscale is bui.UIScale.SMALL else 0 self._height = ( 578 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 670 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 800 ) self._current_tab: WatchWindow.TabID | None = None - extra_top = 20 if uiscale is ba.UIScale.SMALL else 0 + extra_top = 20 if uiscale is bui.UIScale.SMALL else 0 super().__init__( - root_widget=ba.containerwidget( + root_widget=bui.containerwidget( size=(self._width, self._height + extra_top), transition=transition, toolbar_visibility='menu_minimal', scale_origin_stack_offset=scale_origin, scale=( 1.3 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 0.97 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 0.8 ), stack_offset=(0, -10) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 15) - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else (0, 0), ) ) - if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: - ba.containerwidget( + if uiscale is bui.UIScale.SMALL and bui.app.classic.ui.use_toolbars: + bui.containerwidget( edit=self._root_widget, on_cancel_call=self._back ) self._back_button = None else: - self._back_button = btn = ba.buttonwidget( + self._back_button = btn = bui.buttonwidget( parent=self._root_widget, autoselect=True, position=(70 + x_inset, self._height - 74), size=(140, 60), scale=1.1, - label=ba.Lstr(resource='backText'), + label=bui.Lstr(resource='backText'), button_type='back', on_activate_call=self._back, ) - ba.containerwidget(edit=self._root_widget, cancel_button=btn) - ba.buttonwidget( + bui.containerwidget(edit=self._root_widget, cancel_button=btn) + bui.buttonwidget( edit=btn, button_type='backSmall', size=(60, 60), - label=ba.charstr(ba.SpecialChar.BACK), + label=bui.charstr(bui.SpecialChar.BACK), ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 38), size=(0, 0), - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, scale=1.5, h_align='center', v_align='center', - text=ba.Lstr(resource=self._r + '.titleText'), + text=bui.Lstr(resource=self._r + '.titleText'), maxwidth=400, ) tabdefs = [ ( self.TabID.MY_REPLAYS, - ba.Lstr(resource=self._r + '.myReplaysText'), + bui.Lstr(resource=self._r + '.myReplaysText'), ), - # (self.TabID.TEST_TAB, ba.Lstr(value='Testing')), + # (self.TabID.TEST_TAB, bui.Lstr(value='Testing')), ] scroll_buffer_h = 130 + 2 * x_inset @@ -141,16 +143,16 @@ class WatchWindow(ba.Window): on_select_call=self._set_tab, ) - if ba.app.ui.use_toolbars: + if bui.app.classic.ui.use_toolbars: first_tab = self._tab_row.tabs[tabdefs[0][0]] last_tab = self._tab_row.tabs[tabdefs[-1][0]] - ba.widget( + bui.widget( edit=last_tab.button, - right_widget=ba.internal.get_special_widget('party_button'), + right_widget=bui.get_special_widget('party_button'), ) - if uiscale is ba.UIScale.SMALL: - bbtn = ba.internal.get_special_widget('back_button') - ba.widget( + if uiscale is bui.UIScale.SMALL: + bbtn = bui.get_special_widget('back_button') + bui.widget( edit=first_tab.button, up_widget=bbtn, left_widget=bbtn ) @@ -162,17 +164,17 @@ class WatchWindow(ba.Window): scroll_bottom = self._height - self._scroll_height - 79 - 48 buffer_h = 10 buffer_v = 4 - ba.imagewidget( + bui.imagewidget( parent=self._root_widget, position=(scroll_left - buffer_h, scroll_bottom - buffer_v), size=( self._scroll_width + 2 * buffer_h, self._scroll_height + 2 * buffer_v, ), - texture=ba.gettexture('scrollWidget'), - model_transparent=ba.getmodel('softEdgeOutside'), + texture=bui.gettexture('scrollWidget'), + mesh_transparent=bui.getmesh('softEdgeOutside'), ) - self._tab_container: ba.Widget | None = None + self._tab_container: bui.Widget | None = None self._restore_state() @@ -184,7 +186,7 @@ class WatchWindow(ba.Window): self._current_tab = tab_id # Preserve our current tab between runs. - cfg = ba.app.config + cfg = bui.app.config cfg['Watch Tab'] = tab_id.value cfg.commit() @@ -201,16 +203,17 @@ class WatchWindow(ba.Window): # switching to a different tab self._tab_data = {} - uiscale = ba.app.ui.uiscale + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale if tab_id is self.TabID.MY_REPLAYS: c_width = self._scroll_width c_height = self._scroll_height - 20 sub_scroll_height = c_height - 63 self._my_replays_scroll_width = sub_scroll_width = ( - 680 if uiscale is ba.UIScale.SMALL else 640 + 680 if uiscale is bui.UIScale.SMALL else 640 ) - self._tab_container = cnt = ba.containerwidget( + self._tab_container = cnt = bui.containerwidget( parent=self._root_widget, position=( scroll_left, @@ -222,7 +225,7 @@ class WatchWindow(ba.Window): ) v = c_height - 30 - ba.textwidget( + bui.textwidget( parent=cnt, position=(c_width * 0.5, v), color=(0.6, 1.0, 0.6), @@ -231,27 +234,30 @@ class WatchWindow(ba.Window): maxwidth=c_width * 0.9, h_align='center', v_align='center', - text=ba.Lstr( + text=bui.Lstr( resource='replayRenameWarningText', subs=[ - ('${REPLAY}', ba.Lstr(resource='replayNameDefaultText')) + ( + '${REPLAY}', + bui.Lstr(resource='replayNameDefaultText'), + ) ], ), ) - b_width = 140 if uiscale is ba.UIScale.SMALL else 178 + b_width = 140 if uiscale is bui.UIScale.SMALL else 178 b_height = ( 107 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 142 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 190 ) b_space_extra = ( 0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else -2 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else -5 ) @@ -261,17 +267,17 @@ class WatchWindow(ba.Window): c_height - ( 48 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 45 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 40 ) - b_height ) - btnh = 40 if uiscale is ba.UIScale.SMALL else 40 - smlh = 190 if uiscale is ba.UIScale.SMALL else 225 - tscl = 1.0 if uiscale is ba.UIScale.SMALL else 1.2 - self._my_replays_watch_replay_button = btn1 = ba.buttonwidget( + btnh = 40 if uiscale is bui.UIScale.SMALL else 40 + smlh = 190 if uiscale is bui.UIScale.SMALL else 225 + tscl = 1.0 if uiscale is bui.UIScale.SMALL else 1.2 + self._my_replays_watch_replay_button = btn1 = bui.buttonwidget( parent=cnt, size=(b_width, b_height), position=(btnh, btnv), @@ -280,17 +286,18 @@ class WatchWindow(ba.Window): textcolor=b_textcolor, on_activate_call=self._on_my_replay_play_press, text_scale=tscl, - label=ba.Lstr(resource=self._r + '.watchReplayButtonText'), + label=bui.Lstr(resource=self._r + '.watchReplayButtonText'), autoselect=True, ) - ba.widget(edit=btn1, up_widget=self._tab_row.tabs[tab_id].button) - if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: - ba.widget( + bui.widget(edit=btn1, up_widget=self._tab_row.tabs[tab_id].button) + assert bui.app.classic is not None + if uiscale is bui.UIScale.SMALL and bui.app.classic.ui.use_toolbars: + bui.widget( edit=btn1, - left_widget=ba.internal.get_special_widget('back_button'), + left_widget=bui.get_special_widget('back_button'), ) btnv -= b_height + b_space_extra - ba.buttonwidget( + bui.buttonwidget( parent=cnt, size=(b_width, b_height), position=(btnh, btnv), @@ -299,11 +306,11 @@ class WatchWindow(ba.Window): textcolor=b_textcolor, on_activate_call=self._on_my_replay_rename_press, text_scale=tscl, - label=ba.Lstr(resource=self._r + '.renameReplayButtonText'), + label=bui.Lstr(resource=self._r + '.renameReplayButtonText'), autoselect=True, ) btnv -= b_height + b_space_extra - ba.buttonwidget( + bui.buttonwidget( parent=cnt, size=(b_width, b_height), position=(btnh, btnv), @@ -312,67 +319,67 @@ class WatchWindow(ba.Window): textcolor=b_textcolor, on_activate_call=self._on_my_replay_delete_press, text_scale=tscl, - label=ba.Lstr(resource=self._r + '.deleteReplayButtonText'), + label=bui.Lstr(resource=self._r + '.deleteReplayButtonText'), autoselect=True, ) v -= sub_scroll_height + 23 - self._scrollwidget = scrlw = ba.scrollwidget( + self._scrollwidget = scrlw = bui.scrollwidget( parent=cnt, position=(smlh, v), size=(sub_scroll_width, sub_scroll_height), ) - ba.containerwidget(edit=cnt, selected_child=scrlw) - self._columnwidget = ba.columnwidget( + bui.containerwidget(edit=cnt, selected_child=scrlw) + self._columnwidget = bui.columnwidget( parent=scrlw, left_border=10, border=2, margin=0 ) - ba.widget( + bui.widget( edit=scrlw, autoselect=True, left_widget=btn1, up_widget=self._tab_row.tabs[tab_id].button, ) - ba.widget(edit=self._tab_row.tabs[tab_id].button, down_widget=scrlw) + bui.widget( + edit=self._tab_row.tabs[tab_id].button, down_widget=scrlw + ) self._my_replay_selected = None self._refresh_my_replays() def _no_replay_selected_error(self) -> None: - ba.screenmessage( - ba.Lstr(resource=self._r + '.noReplaySelectedErrorText'), + bui.screenmessage( + bui.Lstr(resource=self._r + '.noReplaySelectedErrorText'), color=(1, 0, 0), ) - ba.playsound(ba.getsound('error')) + bui.getsound('error').play() def _on_my_replay_play_press(self) -> None: if self._my_replay_selected is None: self._no_replay_selected_error() return - ba.internal.increment_analytics_count('Replay watch') + bui.increment_analytics_count('Replay watch') def do_it() -> None: try: # Reset to normal speed. - ba.internal.set_replay_speed_exponent(0) - ba.internal.fade_screen(True) + bs.set_replay_speed_exponent(0) + bui.fade_screen(True) assert self._my_replay_selected is not None - ba.internal.new_replay_session( - ba.internal.get_replays_dir() - + '/' - + self._my_replay_selected + bs.new_replay_session( + bui.get_replays_dir() + '/' + self._my_replay_selected ) except Exception: - ba.print_exception('Error running replay session.') + logging.exception('Error running replay session.') # Drop back into a fresh main menu session # in case we half-launched or something. from bastd import mainmenu - ba.internal.new_host_session(mainmenu.MainMenuSession) + bs.new_host_session(mainmenu.MainMenuSession) - ba.internal.fade_screen(False, endcall=ba.Call(ba.pushcall, do_it)) - ba.containerwidget(edit=self._root_widget, transition='out_left') + bui.fade_screen(False, endcall=bui.Call(bui.pushcall, do_it)) + bui.containerwidget(edit=self._root_widget, transition='out_left') def _on_my_replay_rename_press(self) -> None: if self._my_replay_selected is None: @@ -380,69 +387,70 @@ class WatchWindow(ba.Window): return c_width = 600 c_height = 250 - uiscale = ba.app.ui.uiscale - self._my_replays_rename_window = cnt = ba.containerwidget( + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + self._my_replays_rename_window = cnt = bui.containerwidget( scale=( 1.8 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.55 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), size=(c_width, c_height), transition='in_scale', ) dname = self._get_replay_display_name(self._my_replay_selected) - ba.textwidget( + bui.textwidget( parent=cnt, size=(0, 0), h_align='center', v_align='center', - text=ba.Lstr( + text=bui.Lstr( resource=self._r + '.renameReplayText', subs=[('${REPLAY}', dname)], ), maxwidth=c_width * 0.8, position=(c_width * 0.5, c_height - 60), ) - self._my_replay_rename_text = txt = ba.textwidget( + self._my_replay_rename_text = txt = bui.textwidget( parent=cnt, size=(c_width * 0.8, 40), h_align='left', v_align='center', text=dname, editable=True, - description=ba.Lstr(resource=self._r + '.replayNameText'), + description=bui.Lstr(resource=self._r + '.replayNameText'), position=(c_width * 0.1, c_height - 140), autoselect=True, maxwidth=c_width * 0.7, max_chars=200, ) - cbtn = ba.buttonwidget( + cbtn = bui.buttonwidget( parent=cnt, - label=ba.Lstr(resource='cancelText'), - on_activate_call=ba.Call( - lambda c: ba.containerwidget(edit=c, transition='out_scale'), + label=bui.Lstr(resource='cancelText'), + on_activate_call=bui.Call( + lambda c: bui.containerwidget(edit=c, transition='out_scale'), cnt, ), size=(180, 60), position=(30, 30), autoselect=True, ) - okb = ba.buttonwidget( + okb = bui.buttonwidget( parent=cnt, - label=ba.Lstr(resource=self._r + '.renameText'), + label=bui.Lstr(resource=self._r + '.renameText'), size=(180, 60), position=(c_width - 230, 30), - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._rename_my_replay, self._my_replay_selected ), autoselect=True, ) - ba.widget(edit=cbtn, right_widget=okb) - ba.widget(edit=okb, left_widget=cbtn) - ba.textwidget(edit=txt, on_return_press_call=okb.activate) - ba.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) + bui.widget(edit=cbtn, right_widget=okb) + bui.widget(edit=okb, left_widget=cbtn) + bui.textwidget(edit=txt, on_return_press_call=okb.activate) + bui.containerwidget(edit=cnt, cancel_button=cbtn, start_button=okb) def _rename_my_replay(self, replay: str) -> None: new_name = None @@ -450,7 +458,7 @@ class WatchWindow(ba.Window): if not self._my_replay_rename_text: return new_name_raw = cast( - str, ba.textwidget(query=self._my_replay_rename_text) + str, bui.textwidget(query=self._my_replay_rename_text) ) new_name = new_name_raw + '.brp' @@ -460,47 +468,47 @@ class WatchWindow(ba.Window): replay != new_name and self._get_replay_display_name(replay) != new_name_raw ): - old_name_full = ( - ba.internal.get_replays_dir() + '/' + replay - ).encode('utf-8') - new_name_full = ( - ba.internal.get_replays_dir() + '/' + new_name - ).encode('utf-8') - # False alarm; ba.textwidget can return non-None val. + old_name_full = (bui.get_replays_dir() + '/' + replay).encode( + 'utf-8' + ) + new_name_full = (bui.get_replays_dir() + '/' + new_name).encode( + 'utf-8' + ) + # False alarm; bui.textwidget can return non-None val. # pylint: disable=unsupported-membership-test if os.path.exists(new_name_full): - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr( + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr( resource=self._r + '.replayRenameErrorAlreadyExistsText' ), color=(1, 0, 0), ) elif any(char in new_name_raw for char in ['/', '\\', ':']): - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr( + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr( resource=self._r + '.replayRenameErrorInvalidName' ), color=(1, 0, 0), ) else: - ba.internal.increment_analytics_count('Replay rename') + bui.increment_analytics_count('Replay rename') os.rename(old_name_full, new_name_full) self._refresh_my_replays() - ba.playsound(ba.getsound('gunCocking')) + bui.getsound('gunCocking').play() except Exception: - ba.print_exception( - f"Error renaming replay '{replay}' to '{new_name}'." + logging.exception( + "Error renaming replay '%s' to '%s'.", replay, new_name ) - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource=self._r + '.replayRenameErrorText'), + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource=self._r + '.replayRenameErrorText'), color=(1, 0, 0), ) - ba.containerwidget( + bui.containerwidget( edit=self._my_replays_rename_window, transition='out_scale' ) @@ -511,7 +519,7 @@ class WatchWindow(ba.Window): self._no_replay_selected_error() return confirm.ConfirmWindow( - ba.Lstr( + bui.Lstr( resource=self._r + '.deleteConfirmText', subs=[ ( @@ -520,7 +528,7 @@ class WatchWindow(ba.Window): ) ], ), - ba.Call(self._delete_replay, self._my_replay_selected), + bui.Call(self._delete_replay, self._my_replay_selected), 450, 150, ) @@ -529,24 +537,22 @@ class WatchWindow(ba.Window): if replay.endswith('.brp'): replay = replay[:-4] if replay == '__lastReplay': - return ba.Lstr(resource='replayNameDefaultText').evaluate() + return bui.Lstr(resource='replayNameDefaultText').evaluate() return replay def _delete_replay(self, replay: str) -> None: try: - ba.internal.increment_analytics_count('Replay delete') - os.remove( - (ba.internal.get_replays_dir() + '/' + replay).encode('utf-8') - ) + bui.increment_analytics_count('Replay delete') + os.remove((bui.get_replays_dir() + '/' + replay).encode('utf-8')) self._refresh_my_replays() - ba.playsound(ba.getsound('shieldDown')) + bui.getsound('shieldDown').play() if replay == self._my_replay_selected: self._my_replay_selected = None except Exception: - ba.print_exception(f"Error deleting replay '{replay}'.") - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource=self._r + '.replayDeleteErrorText'), + logging.exception("Error deleting replay '%s'.", replay) + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource=self._r + '.replayDeleteErrorText'), color=(1, 0, 0), ) @@ -559,19 +565,19 @@ class WatchWindow(ba.Window): child.delete() t_scale = 1.6 try: - names = os.listdir(ba.internal.get_replays_dir()) + names = os.listdir(bui.get_replays_dir()) # Ignore random other files in there. names = [n for n in names if n.endswith('.brp')] names.sort(key=lambda x: x.lower()) except Exception: - ba.print_exception('Error listing replays dir.') + logging.exception('Error listing replays dir.') names = [] assert self._my_replays_scroll_width is not None assert self._my_replays_watch_replay_button is not None for i, name in enumerate(names): - txt = ba.textwidget( + txt = bui.textwidget( parent=self._columnwidget, size=(self._my_replays_scroll_width / t_scale, 30), selectable=True, @@ -579,7 +585,7 @@ class WatchWindow(ba.Window): if name == '__lastReplay.brp' else (1, 1, 1), always_highlight=True, - on_select_call=ba.Call(self._on_my_replay_select, name), + on_select_call=bui.Call(self._on_my_replay_select, name), on_activate_call=self._my_replays_watch_replay_button.activate, text=self._get_replay_display_name(name), h_align='left', @@ -588,7 +594,7 @@ class WatchWindow(ba.Window): maxwidth=(self._my_replays_scroll_width / t_scale) * 0.93, ) if i == 0: - ba.widget( + bui.widget( edit=txt, up_widget=self._tab_row.tabs[self.TabID.MY_REPLAYS].button, ) @@ -610,22 +616,26 @@ class WatchWindow(ba.Window): sel_name = 'TabContainer' else: raise ValueError(f'unrecognized selection {sel}') - ba.app.ui.window_states[type(self)] = {'sel_name': sel_name} + assert bui.app.classic is not None + bui.app.classic.ui.window_states[type(self)] = { + 'sel_name': sel_name + } except Exception: - ba.print_exception(f'Error saving state for {self}.') + logging.exception('Error saving state for %s.', self) def _restore_state(self) -> None: from efro.util import enum_by_value try: - sel: ba.Widget | None - sel_name = ba.app.ui.window_states.get(type(self), {}).get( + sel: bui.Widget | None + assert bui.app.classic is not None + sel_name = bui.app.classic.ui.window_states.get(type(self), {}).get( 'sel_name' ) assert isinstance(sel_name, (str, type(None))) try: current_tab = enum_by_value( - self.TabID, ba.app.config.get('Watch Tab') + self.TabID, bui.app.config.get('Watch Tab') ) except ValueError: current_tab = self.TabID.MY_REPLAYS @@ -648,17 +658,18 @@ class WatchWindow(ba.Window): sel = self._tab_container else: sel = self._tab_row.tabs[current_tab].button - ba.containerwidget(edit=self._root_widget, selected_child=sel) + bui.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: - ba.print_exception(f'Error restoring state for {self}.') + logging.exception('Error restoring state for %s.', self) def _back(self) -> None: from bastd.ui.mainmenu import MainMenuWindow self._save_state() - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) - ba.app.ui.set_main_menu_window( + assert bui.app.classic is not None + bui.app.classic.ui.set_main_menu_window( MainMenuWindow(transition='in_left').get_root_widget() ) diff --git a/src/assets/ba_data/python/batemplatefs/__init__.py b/src/assets/ba_data/python/batemplatefs/__init__.py new file mode 100644 index 00000000..6c4ddda3 --- /dev/null +++ b/src/assets/ba_data/python/batemplatefs/__init__.py @@ -0,0 +1,11 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Ballistica Template Feature Set""" + +# ba_meta require api 8 + +from batemplatefs._subsystem import TemplateFsSubsystem + +__all__ = [ + 'TemplateFsSubsystem', +] diff --git a/src/assets/ba_data/python/batemplatefs/_hooks.py b/src/assets/ba_data/python/batemplatefs/_hooks.py new file mode 100644 index 00000000..81e9a49a --- /dev/null +++ b/src/assets/ba_data/python/batemplatefs/_hooks.py @@ -0,0 +1,12 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Snippets of code for use by the c++ layer.""" + +# (most of these are self-explanatory) +# pylint: disable=missing-function-docstring + +from __future__ import annotations + + +def hello_world() -> None: + print('HELLO WORLD FROM TemplateFs!') diff --git a/src/assets/ba_data/python/batemplatefs/_subsystem.py b/src/assets/ba_data/python/batemplatefs/_subsystem.py new file mode 100644 index 00000000..2645fbf8 --- /dev/null +++ b/src/assets/ba_data/python/batemplatefs/_subsystem.py @@ -0,0 +1,19 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Provides the TemplateFs subsystem.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + pass + + +class TemplateFsSubsystem: + """Subsystem for TemplateFs functionality in the app. + + The single shared instance of this app can be accessed at + babase.app.templatefs. Note that it is possible for babase.app.templatefs + to be None if the TemplateFs feature-set is not enabled, and code + should handle that case gracefully. + """ diff --git a/src/assets/ba_data/python/bauiv1/__init__.py b/src/assets/ba_data/python/bauiv1/__init__.py new file mode 100644 index 00000000..c92590ac --- /dev/null +++ b/src/assets/ba_data/python/bauiv1/__init__.py @@ -0,0 +1,218 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Ballistica UI Version 1""" + +# ba_meta require api 8 + +# The stuff we expose here at the top level is our 'public' api. +# It should only be imported by code outside of this package or +# from 'if TYPE_CHECKING' blocks (which will not exec at runtime). +# Code within our package should import things directly from their +# submodules. + +from __future__ import annotations + +# pylint: disable=redefined-builtin + +import logging + +from _babase import ( + app, + ContextRef, + fade_screen, + set_ui_input_device, + is_running_on_fire_tv, + get_display_resolution, + get_max_graphics_quality, + apptime, + apptimer, + AppTimer, + displaytime, + displaytimer, + DisplayTimer, + quit, + in_logic_thread, + add_clean_frame_callback, + charstr, + pushcall, + has_gamma_control, + get_string_width, + get_string_height, + set_analytics_screen, + is_xcode_build, + set_low_level_config_value, + get_low_level_config_value, + have_permission, + request_permission, + appnameupper, + safecolor, + appname, + workspaces_in_use, + increment_analytics_count, + get_replays_dir, + lock_all_input, + unlock_all_input, + do_once, + clipboard_is_supported, + clipboard_set_text, +) +from _babase import screenmessage + +from babase._general import Call, WeakCall, AppTime, DisplayTime +from babase._language import Lstr +from babase._plugin import PotentialPlugin, Plugin +from babase._apputils import get_remote_app_name, is_browser_likely_available +from babase._login import LoginAdapter +from babase._general import getclass, get_type_name +from babase._net import get_ip_address_type +from babase._keyboard import Keyboard +from babase._appconfig import commit_app_config + + +from babase._error import NotFoundError + +from babase._mgen.enums import ( + Permission, + UIScale, + SpecialChar, +) +from babase._text import timestring + + +from _bauiv1 import ( + uibounds, + set_party_window_open, + get_qrcode_texture, + is_party_icon_visible, + set_party_icon_always_visible, + open_url, + have_incentivized_ad, + has_video_ads, + get_special_widget, + open_file_externally, + Sound, + getsound, + Texture, + gettexture, + Mesh, + getmesh, + checkboxwidget, + columnwidget, + imagewidget, + buttonwidget, + containerwidget, + rowwidget, + scrollwidget, + textwidget, + hscrollwidget, + Widget, + widget, +) +from bauiv1.ui import Window, uicleanupcheck + + +__all__ = [ + 'lock_all_input', + 'unlock_all_input', + 'get_qrcode_texture', + 'get_replays_dir', + 'fade_screen', + 'increment_analytics_count', + 'workspaces_in_use', + 'appname', + 'is_party_icon_visible', + 'LoginAdapter', + 'safecolor', + 'is_browser_likely_available', + 'NotFoundError', + 'set_party_icon_always_visible', + 'get_remote_app_name', + 'appnameupper', + 'open_url', + 'Permission', + 'request_permission', + 'have_permission', + 'get_low_level_config_value', + 'set_low_level_config_value', + 'is_xcode_build', + 'apptime', + 'set_analytics_screen', + 'have_incentivized_ad', + 'has_video_ads', + 'timestring', + 'get_string_width', + 'get_string_height', + 'get_special_widget', + 'has_gamma_control', + 'WeakCall', + 'apptimer', + 'pushcall', + 'PotentialPlugin', + 'Plugin', + 'screenmessage', + 'SpecialChar', + 'charstr', + 'UIScale', + 'uicleanupcheck', + 'Lstr', + 'app', + 'Call', + 'widget', + 'Window', + 'Sound', + 'getsound', + 'Texture', + 'gettexture', + 'Mesh', + 'getmesh', + 'checkboxwidget', + 'columnwidget', + 'imagewidget', + 'buttonwidget', + 'containerwidget', + 'rowwidget', + 'scrollwidget', + 'textwidget', + 'hscrollwidget', + 'Widget', + 'getclass', + 'get_ip_address_type', + 'do_once', + 'Keyboard', + 'clipboard_is_supported', + 'clipboard_set_text', + 'set_ui_input_device', + 'set_party_window_open', + 'add_clean_frame_callback', + 'in_logic_thread', + 'get_type_name', + 'open_file_externally', + 'appnameupper', + 'commit_app_config', + 'quit', + 'get_display_resolution', + 'get_max_graphics_quality', + 'is_running_on_fire_tv', + 'AppTime', + 'AppTimer', + 'ContextRef', + 'displaytime', + 'DisplayTime', + 'displaytimer', + 'DisplayTimer', + 'uibounds', +] + +# Sanity check: we want to keep ballistica's dependencies and +# bootstrapping order clearly defined; let's check a few particular +# modules to make sure they never directly or indirectly import us +# before their own execs complete. +if __debug__: + for _mdl in 'babase', '_babase': + if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'): + logging.warning( + '%s was imported before %s finished importing;' + ' should not happen.', + __name__, + _mdl, + ) diff --git a/src/assets/ba_data/python/bauiv1/_hooks.py b/src/assets/ba_data/python/bauiv1/_hooks.py new file mode 100644 index 00000000..85912b20 --- /dev/null +++ b/src/assets/ba_data/python/bauiv1/_hooks.py @@ -0,0 +1,93 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Snippets of code for use by the c++ layer.""" +# (most of these are self-explanatory) +# pylint: disable=missing-function-docstring +from __future__ import annotations + +import weakref +from typing import TYPE_CHECKING + +import _bauiv1 + +if TYPE_CHECKING: + from typing import Sequence + + +def ticket_icon_press() -> None: + # FIXME: move this into our package. + from bastd.ui.resourcetypeinfo import ResourceTypeInfoWindow + + ResourceTypeInfoWindow( + origin_widget=_bauiv1.get_special_widget('tickets_info_button') + ) + + +def trophy_icon_press() -> None: + print('TROPHY ICON PRESSED') + + +def level_icon_press() -> None: + print('LEVEL ICON PRESSED') + + +def coin_icon_press() -> None: + print('COIN ICON PRESSED') + + +def empty_call() -> None: + pass + + +def back_button_press() -> None: + _bauiv1.back_press() + + +def friends_button_press() -> None: + print('FRIEND BUTTON PRESSED!') + + +def party_icon_activate(origin: Sequence[float]) -> None: + from bastd.ui.party import PartyWindow + from babase import app + + assert not app.headless_mode + + assert app.classic is not None + ui = app.classic.ui + + _bauiv1.getsound('swish').play() + + # If it exists, dismiss it; otherwise make a new one. + if ui.party_window is not None and ui.party_window() is not None: + ui.party_window().close() + else: + ui.party_window = weakref.ref(PartyWindow(origin=origin)) + + +def quit_window() -> None: + from bastd.ui.confirm import QuitWindow + + QuitWindow() + + +def device_menu_press(device_id: int | None) -> None: + from bastd.ui.mainmenu import MainMenuWindow + from babase import app + from bauiv1 import set_ui_input_device + + assert app.classic is not None + in_main_menu = app.classic.ui.has_main_menu_window() + if not in_main_menu: + set_ui_input_device(device_id) + + if not app.headless_mode: + _bauiv1.getsound('swish').play() + + app.classic.ui.set_main_menu_window(MainMenuWindow().get_root_widget()) + + +def show_url_window(address: str) -> None: + from bastd.ui.url import ShowURLWindow + + ShowURLWindow(address) diff --git a/assets/src/ba_data/python/ba/modutils.py b/src/assets/ba_data/python/bauiv1/modutils.py similarity index 75% rename from assets/src/ba_data/python/ba/modutils.py rename to src/assets/ba_data/python/bauiv1/modutils.py index 6fef1e8c..b89725b3 100644 --- a/assets/src/ba_data/python/ba/modutils.py +++ b/src/assets/ba_data/python/bauiv1/modutils.py @@ -6,7 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING import os -import _ba +import _babase +import _bauiv1 if TYPE_CHECKING: from typing import Sequence @@ -17,7 +18,7 @@ def get_human_readable_user_scripts_path() -> str: This is NOT a valid filesystem path; may be something like "(SD Card)". """ - app = _ba.app + app = _babase.app path: str | None = app.python_directory_user if path is None: return '' @@ -35,7 +36,7 @@ def get_human_readable_user_scripts_path() -> str: # and show the whole ugly path as a fallback. # Note that we used to use externalStorageText resource but gonna try # without it for now. (simply 'foo' instead of /foo). - if app.platform == 'android': + if app.classic is not None and app.classic.platform == 'android': for pre in ['/storage/emulated/0/']: if path.startswith(pre): path = path.removeprefix(pre) @@ -45,27 +46,34 @@ def get_human_readable_user_scripts_path() -> str: def _request_storage_permission() -> bool: """If needed, requests storage permission from the user (& return true).""" - from ba._language import Lstr - from ba._generated.enums import Permission + from babase._language import Lstr + from babase._mgen.enums import Permission - if not _ba.have_permission(Permission.STORAGE): - _ba.playsound(_ba.getsound('error')) - _ba.screenmessage( + if not _babase.have_permission(Permission.STORAGE): + _bauiv1.getsound('error').play() + _babase.screenmessage( Lstr(resource='storagePermissionAccessText'), color=(1, 0, 0) ) - _ba.timer(1.0, lambda: _ba.request_permission(Permission.STORAGE)) + _babase.apptimer( + 1.0, lambda: _babase.request_permission(Permission.STORAGE) + ) return True return False def show_user_scripts() -> None: """Open or nicely print the location of the user-scripts directory.""" - app = _ba.app + app = _babase.app # First off, if we need permission for this, ask for it. if _request_storage_permission(): return + # If we're running in a nonstandard environment its possible this is unset. + if app.python_directory_user is None: + _babase.screenmessage('') + return + # Secondly, if the dir doesn't exist, attempt to make it. if not os.path.exists(app.python_directory_user): os.makedirs(app.python_directory_user) @@ -76,7 +84,7 @@ def show_user_scripts() -> None: # doesn't seem like there's a way to inform the media scanner of an empty # directory, which means they would have to reboot their device before # they can see it. - if app.platform == 'android': + if app.classic is not None and app.classic.platform == 'android': try: usd: str | None = app.python_directory_user if usd is not None and os.path.isdir(usd): @@ -89,17 +97,17 @@ def show_user_scripts() -> None: ) except Exception: - from ba import _error + from babase import _error _error.print_exception('error writing about_this_folder stuff') # On a few platforms we try to open the dir in the UI. - if app.platform in ['mac', 'windows']: - _ba.open_dir_externally(app.python_directory_user) + if app.classic is not None and app.classic.platform in ['mac', 'windows']: + _bauiv1.open_dir_externally(app.python_directory_user) # Otherwise we just print a pretty version of it. else: - _ba.screenmessage(get_human_readable_user_scripts_path()) + _babase.screenmessage(get_human_readable_user_scripts_path()) def create_user_system_scripts() -> None: @@ -109,12 +117,18 @@ def create_user_system_scripts() -> None: """ import shutil - app = _ba.app + app = _babase.app # First off, if we need permission for this, ask for it. if _request_storage_permission(): return + # Its possible these are unset in non-standard environments. + if app.python_directory_user is None: + raise RuntimeError('user python dir unset') + if app.python_directory_app is None: + raise RuntimeError('app python dir unset') + path = app.python_directory_user + '/sys/' + app.version pathtmp = path + '_tmp' if os.path.exists(path): @@ -138,10 +152,10 @@ def create_user_system_scripts() -> None: shutil.move(pathtmp, path) print( f"Created system scripts at :'{path}" - f"'\nRestart {_ba.appname()} to use them." - f' (use ba.quit() to exit the game)' + f"'\nRestart {_babase.appname()} to use them." + f' (use babase.quit() to exit the game)' ) - if app.platform == 'android': + if app.classic is not None and app.classic.platform == 'android': print( 'Note: the new files may not be visible via ' 'android-file-transfer until you restart your device.' @@ -152,14 +166,18 @@ def delete_user_system_scripts() -> None: """Clean out the scripts created by create_user_system_scripts().""" import shutil - app = _ba.app + app = _babase.app + + if app.python_directory_user is None: + raise RuntimeError('user python dir unset') + path = app.python_directory_user + '/sys/' + app.version if os.path.exists(path): shutil.rmtree(path) print( f'User system scripts deleted.\n' - f'Restart {_ba.appname()} to use internal' - f' scripts. (use ba.quit() to exit the game)' + f'Restart {_babase.appname()} to use internal' + f' scripts. (use babase.quit() to exit the game)' ) else: print('User system scripts not found.') diff --git a/assets/src/ba_data/python/bastd/ui/onscreenkeyboard.py b/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py similarity index 70% rename from assets/src/ba_data/python/bastd/ui/onscreenkeyboard.py rename to src/assets/ba_data/python/bauiv1/onscreenkeyboard.py index 19d6c95c..d1371fb8 100644 --- a/assets/src/ba_data/python/bastd/ui/onscreenkeyboard.py +++ b/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py @@ -4,27 +4,25 @@ from __future__ import annotations -from typing import TYPE_CHECKING, cast +import logging +from typing import cast -import ba -import ba.internal - -if TYPE_CHECKING: - pass +import bauiv1 as bui -class OnScreenKeyboardWindow(ba.Window): +class OnScreenKeyboardWindow(bui.Window): """Simple built-in on-screen keyboard.""" - def __init__(self, textwidget: ba.Widget, label: str, max_chars: int): + def __init__(self, textwidget: bui.Widget, label: str, max_chars: int): self._target_text = textwidget self._width = 700 self._height = 400 - uiscale = ba.app.ui.uiscale - top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 + assert bui.app.classic is not None + uiscale = bui.app.classic.ui.uiscale + top_extra = 20 if uiscale is bui.UIScale.SMALL else 0 super().__init__( - root_widget=ba.containerwidget( - parent=ba.internal.get_special_widget('overlay_stack'), + root_widget=bui.containerwidget( + parent=bui.get_special_widget('overlay_stack'), size=(self._width, self._height + top_extra), transition='in_scale', scale_origin_stack_offset=( @@ -32,49 +30,49 @@ class OnScreenKeyboardWindow(ba.Window): ), scale=( 2.0 - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else 1.5 - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else 1.0 ), stack_offset=(0, 0) - if uiscale is ba.UIScale.SMALL + if uiscale is bui.UIScale.SMALL else (0, 0) - if uiscale is ba.UIScale.MEDIUM + if uiscale is bui.UIScale.MEDIUM else (0, 0), ) ) - self._done_button = ba.buttonwidget( + self._done_button = bui.buttonwidget( parent=self._root_widget, position=(self._width - 200, 44), size=(140, 60), autoselect=True, - label=ba.Lstr(resource='doneText'), + label=bui.Lstr(resource='doneText'), on_activate_call=self._done, ) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, on_cancel_call=self._cancel, start_button=self._done_button, ) - ba.textwidget( + bui.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 41), size=(0, 0), scale=0.95, text=label, maxwidth=self._width - 140, - color=ba.app.ui.title_color, + color=bui.app.classic.ui.title_color, h_align='center', v_align='center', ) - self._text_field = ba.textwidget( + self._text_field = bui.textwidget( parent=self._root_widget, position=(70, self._height - 116), max_chars=max_chars, - text=cast(str, ba.textwidget(query=self._target_text)), + text=cast(str, bui.textwidget(query=self._target_text)), on_return_press_call=self._done, autoselect=True, size=(self._width - 140, 55), @@ -89,18 +87,18 @@ class OnScreenKeyboardWindow(ba.Window): self._key_color = (0.69, 0.6, 0.74) self._key_color_dark = (0.55, 0.55, 0.71) - self._shift_button: ba.Widget | None = None - self._backspace_button: ba.Widget | None = None - self._space_button: ba.Widget | None = None + self._shift_button: bui.Widget | None = None + self._backspace_button: bui.Widget | None = None + self._space_button: bui.Widget | None = None self._double_press_shift = False - self._num_mode_button: ba.Widget | None = None - self._emoji_button: ba.Widget | None = None - self._char_keys: list[ba.Widget] = [] + self._num_mode_button: bui.Widget | None = None + self._emoji_button: bui.Widget | None = None + self._char_keys: list[bui.Widget] = [] self._keyboard_index = 0 self._last_space_press = 0.0 self._double_space_interval = 0.3 - self._keyboard: ba.Keyboard + self._keyboard: bui.Keyboard self._chars: list[str] self._modes: list[str] self._mode: str @@ -124,7 +122,7 @@ class OnScreenKeyboardWindow(ba.Window): key_color = self._key_color key_color_dark = self._key_color_dark - self._click_sound = ba.getsound('click01') + self._click_sound = bui.getsound('click01') # kill prev char keys for key in self._char_keys: @@ -139,21 +137,21 @@ class OnScreenKeyboardWindow(ba.Window): h = row_starts[row_num] # shift key before row 3 if row_num == 2 and self._shift_button is None: - self._shift_button = ba.buttonwidget( + self._shift_button = bui.buttonwidget( parent=self._root_widget, position=(h - key_width * 2.0, v), size=(key_width * 1.7, key_height), autoselect=True, textcolor=key_textcolor, color=key_color_dark, - label=ba.charstr(ba.SpecialChar.SHIFT), + label=bui.charstr(bui.SpecialChar.SHIFT), enable_sound=False, extra_touch_border_scale=0.3, button_type='square', ) for _ in row: - btn = ba.buttonwidget( + btn = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(key_width, key_height), @@ -173,7 +171,7 @@ class OnScreenKeyboardWindow(ba.Window): if self._backspace_button is not None: self._backspace_button.delete() - self._backspace_button = ba.buttonwidget( + self._backspace_button = bui.buttonwidget( parent=self._root_widget, position=(h + 4, v), size=(key_width * 1.8, key_height), @@ -182,7 +180,7 @@ class OnScreenKeyboardWindow(ba.Window): repeat=True, textcolor=key_textcolor, color=key_color_dark, - label=ba.charstr(ba.SpecialChar.DELETE), + label=bui.charstr(bui.SpecialChar.DELETE), button_type='square', on_activate_call=self._del, ) @@ -190,7 +188,7 @@ class OnScreenKeyboardWindow(ba.Window): # Do space bar and stuff. if row_num == 2: if self._num_mode_button is None: - self._num_mode_button = ba.buttonwidget( + self._num_mode_button = bui.buttonwidget( parent=self._root_widget, position=(112, v - 8), size=(key_width * 2, key_height + 5), @@ -203,7 +201,7 @@ class OnScreenKeyboardWindow(ba.Window): label='', ) if self._emoji_button is None: - self._emoji_button = ba.buttonwidget( + self._emoji_button = bui.buttonwidget( parent=self._root_widget, position=(56, v - 8), size=(key_width, key_height + 5), @@ -211,13 +209,13 @@ class OnScreenKeyboardWindow(ba.Window): enable_sound=False, textcolor=key_textcolor, color=key_color_dark, - label=ba.charstr(ba.SpecialChar.LOGO_FLAT), + label=bui.charstr(bui.SpecialChar.LOGO_FLAT), extra_touch_border_scale=0.3, button_type='square', ) btn1 = self._num_mode_button if self._space_button is None: - self._space_button = ba.buttonwidget( + self._space_button = bui.buttonwidget( parent=self._root_widget, position=(210, v - 12), size=(key_width * 6.1, key_height + 15), @@ -226,49 +224,49 @@ class OnScreenKeyboardWindow(ba.Window): autoselect=True, textcolor=key_textcolor, color=key_color_dark, - label=ba.Lstr(resource='spaceKeyText'), - on_activate_call=ba.Call(self._type_char, ' '), + label=bui.Lstr(resource='spaceKeyText'), + on_activate_call=bui.Call(self._type_char, ' '), ) # Show change instructions only if we have more than one # keyboard option. keyboards = ( - ba.app.meta.scanresults.exports_of_class(ba.Keyboard) - if ba.app.meta.scanresults is not None + bui.app.meta.scanresults.exports_of_class(bui.Keyboard) + if bui.app.meta.scanresults is not None else [] ) if len(keyboards) > 1: - ba.textwidget( + bui.textwidget( parent=self._root_widget, h_align='center', position=(210, v - 70), size=(key_width * 6.1, key_height + 15), - text=ba.Lstr( + text=bui.Lstr( resource='keyboardChangeInstructionsText' ), scale=0.75, ) btn2 = self._space_button btn3 = self._emoji_button - ba.widget(edit=btn1, right_widget=btn2, left_widget=btn3) - ba.widget( + bui.widget(edit=btn1, right_widget=btn2, left_widget=btn3) + bui.widget( edit=btn2, left_widget=btn1, right_widget=self._done_button ) - ba.widget(edit=btn3, left_widget=btn1) - ba.widget(edit=self._done_button, left_widget=btn2) + bui.widget(edit=btn3, left_widget=btn1) + bui.widget(edit=self._done_button, left_widget=btn2) - ba.containerwidget( + bui.containerwidget( edit=self._root_widget, selected_child=self._char_keys[14] ) self._refresh() - def _get_keyboard(self) -> ba.Keyboard: - assert ba.app.meta.scanresults is not None - classname = ba.app.meta.scanresults.exports_of_class(ba.Keyboard)[ + def _get_keyboard(self) -> bui.Keyboard: + assert bui.app.meta.scanresults is not None + classname = bui.app.meta.scanresults.exports_of_class(bui.Keyboard)[ self._keyboard_index ] - kbclass = ba.getclass(classname, ba.Keyboard) + kbclass = bui.getclass(classname, bui.Keyboard) return kbclass() def _refresh(self) -> None: @@ -277,23 +275,23 @@ class OnScreenKeyboardWindow(ba.Window): chars = list(self._chars) if self._mode == 'caps': chars = [c.upper() for c in chars] - ba.buttonwidget( + bui.buttonwidget( edit=self._shift_button, color=self._key_color_lit if self._mode == 'caps' else self._key_color_dark, - label=ba.charstr(ba.SpecialChar.SHIFT), + label=bui.charstr(bui.SpecialChar.SHIFT), on_activate_call=self._shift, ) - ba.buttonwidget( + bui.buttonwidget( edit=self._num_mode_button, label='123#&*', on_activate_call=self._num_mode, ) - ba.buttonwidget( + bui.buttonwidget( edit=self._emoji_button, color=self._key_color_dark, - label=ba.charstr(ba.SpecialChar.LOGO_FLAT), + label=bui.charstr(bui.SpecialChar.LOGO_FLAT), on_activate_call=self._next_mode, ) else: @@ -301,21 +299,21 @@ class OnScreenKeyboardWindow(ba.Window): chars = list(self._keyboard.nums) else: chars = list(self._keyboard.pages[self._mode]) - ba.buttonwidget( + bui.buttonwidget( edit=self._shift_button, color=self._key_color_dark, label='', on_activate_call=self._null_press, ) - ba.buttonwidget( + bui.buttonwidget( edit=self._num_mode_button, label='abc', on_activate_call=self._abc_mode, ) - ba.buttonwidget( + bui.buttonwidget( edit=self._emoji_button, color=self._key_color_dark, - label=ba.charstr(ba.SpecialChar.LOGO_FLAT), + label=bui.charstr(bui.SpecialChar.LOGO_FLAT), on_activate_call=self._next_mode, ) @@ -326,55 +324,56 @@ class OnScreenKeyboardWindow(ba.Window): # No such char. have_char = False pagename = self._mode - ba.print_error( - f'Size of page "{pagename}" of keyboard' - f' "{self._keyboard.name}" is incorrect:' - f' {len(chars)} != {len(self._chars)}' - f' (size of default "normal" page)', - once=True, - ) - ba.buttonwidget( + if bui.do_once(): + errstr = ( + f'Size of page "{pagename}" of keyboard' + f' "{self._keyboard.name}" is incorrect:' + f' {len(chars)} != {len(self._chars)}' + f' (size of default "normal" page)' + ) + logging.error(errstr) + bui.buttonwidget( edit=btn, label=chars[i] if have_char else ' ', - on_activate_call=ba.Call( + on_activate_call=bui.Call( self._type_char, chars[i] if have_char else ' ' ), ) def _null_press(self) -> None: - ba.playsound(self._click_sound) + self._click_sound.play() def _abc_mode(self) -> None: - ba.playsound(self._click_sound) + self._click_sound.play() self._mode = 'normal' self._refresh() def _num_mode(self) -> None: - ba.playsound(self._click_sound) + self._click_sound.play() self._mode = 'num' self._refresh() def _next_mode(self) -> None: - ba.playsound(self._click_sound) + self._click_sound.play() self._mode_index = (self._mode_index + 1) % len(self._modes) self._mode = self._modes[self._mode_index] self._refresh() def _next_keyboard(self) -> None: - assert ba.app.meta.scanresults is not None - kbexports = ba.app.meta.scanresults.exports_of_class(ba.Keyboard) + assert bui.app.meta.scanresults is not None + kbexports = bui.app.meta.scanresults.exports_of_class(bui.Keyboard) self._keyboard_index = (self._keyboard_index + 1) % len(kbexports) self._load_keyboard() if len(kbexports) < 2: - ba.playsound(ba.getsound('error')) - ba.screenmessage( - ba.Lstr(resource='keyboardNoOthersAvailableText'), + bui.getsound('error').play() + bui.screenmessage( + bui.Lstr(resource='keyboardNoOthersAvailableText'), color=(1, 0, 0), ) else: - ba.screenmessage( - ba.Lstr( + bui.screenmessage( + bui.Lstr( resource='keyboardSwitchText', subs=[('${NAME}', self._keyboard.name)], ), @@ -382,7 +381,7 @@ class OnScreenKeyboardWindow(ba.Window): ) def _shift(self) -> None: - ba.playsound(self._click_sound) + self._click_sound.play() if self._mode == 'normal': self._mode = 'caps' self._double_press_shift = False @@ -394,30 +393,30 @@ class OnScreenKeyboardWindow(ba.Window): self._refresh() def _del(self) -> None: - ba.playsound(self._click_sound) - txt = cast(str, ba.textwidget(query=self._text_field)) + self._click_sound.play() + txt = cast(str, bui.textwidget(query=self._text_field)) # pylint: disable=unsubscriptable-object txt = txt[:-1] - ba.textwidget(edit=self._text_field, text=txt) + bui.textwidget(edit=self._text_field, text=txt) def _type_char(self, char: str) -> None: - ba.playsound(self._click_sound) + self._click_sound.play() if char.isspace(): if ( - ba.time(ba.TimeType.REAL) - self._last_space_press + bui.apptime() - self._last_space_press < self._double_space_interval ): self._last_space_press = 0 self._next_keyboard() self._del() # We typed unneeded space around 1s ago. return - self._last_space_press = ba.time(ba.TimeType.REAL) + self._last_space_press = bui.apptime() # Operate in unicode so we don't do anything funky like chop utf-8 # chars in half. - txt = cast(str, ba.textwidget(query=self._text_field)) + txt = cast(str, bui.textwidget(query=self._text_field)) txt += char - ba.textwidget(edit=self._text_field, text=txt) + bui.textwidget(edit=self._text_field, text=txt) # If we were caps, go back only if not Shift is pressed twice. if self._mode == 'caps' and not self._double_press_shift: @@ -425,13 +424,13 @@ class OnScreenKeyboardWindow(ba.Window): self._refresh() def _cancel(self) -> None: - ba.playsound(ba.getsound('swish')) - ba.containerwidget(edit=self._root_widget, transition='out_scale') + bui.getsound('swish').play() + bui.containerwidget(edit=self._root_widget, transition='out_scale') def _done(self) -> None: - ba.containerwidget(edit=self._root_widget, transition='out_scale') + bui.containerwidget(edit=self._root_widget, transition='out_scale') if self._target_text: - ba.textwidget( + bui.textwidget( edit=self._target_text, - text=cast(str, ba.textwidget(query=self._text_field)), + text=cast(str, bui.textwidget(query=self._text_field)), ) diff --git a/assets/src/ba_data/python/ba/ui/__init__.py b/src/assets/ba_data/python/bauiv1/ui/__init__.py similarity index 87% rename from assets/src/ba_data/python/ba/ui/__init__.py rename to src/assets/ba_data/python/bauiv1/ui/__init__.py index 3d9740b4..eb49b8b2 100644 --- a/assets/src/ba_data/python/ba/ui/__init__.py +++ b/src/assets/ba_data/python/bauiv1/ui/__init__.py @@ -9,13 +9,14 @@ import weakref from dataclasses import dataclass from typing import TYPE_CHECKING, cast, Type -import _ba -from ba._generated.enums import TimeType +import _babase +import _bauiv1 if TYPE_CHECKING: from typing import Any - import ba + import babase + import bauiv1 # Set environment variable BA_DEBUG_UI_CLEANUP_CHECKS to 1 # to print detailed info about what is getting cleaned up when. @@ -28,14 +29,14 @@ class Window: Category: User Interface Classes """ - def __init__(self, root_widget: ba.Widget, cleanupcheck: bool = True): + def __init__(self, root_widget: bauiv1.Widget, cleanupcheck: bool = True): self._root_widget = root_widget # Complain if we outlive our root widget. if cleanupcheck: uicleanupcheck(self, root_widget) - def get_root_widget(self) -> ba.Widget: + def get_root_widget(self) -> bauiv1.Widget: """Return the root widget.""" return self._root_widget @@ -45,7 +46,7 @@ class UICleanupCheck: """Holds info about a uicleanupcheck target.""" obj: weakref.ref - widget: ba.Widget + widget: bauiv1.Widget widget_death_time: float | None @@ -76,9 +77,9 @@ class UILocationWindow(UILocation): def __init__(self) -> None: super().__init__() - self._root_widget: ba.Widget | None = None + self._root_widget: bauiv1.Widget | None = None - def get_root_widget(self) -> ba.Widget: + def get_root_widget(self) -> bauiv1.Widget: """Return the root widget for this window.""" assert self._root_widget is not None return self._root_widget @@ -118,13 +119,12 @@ class UIEntry: class UIController: - """Wrangles ba.UILocations. + """Wrangles bauiv1.UILocations. Category: User Interface Classes """ def __init__(self) -> None: - # FIXME: document why we have separate stacks for game and menu... self._main_stack_game: list[UIEntry] = [] self._main_stack_menu: list[UIEntry] = [] @@ -167,25 +167,25 @@ class UIController: entrynew.create() -def uicleanupcheck(obj: Any, widget: ba.Widget) -> None: +def uicleanupcheck(obj: Any, widget: bauiv1.Widget) -> None: """Add a check to ensure a widget-owning object gets cleaned up properly. Category: User Interface Functions This adds a check which will print an error message if the provided - object still exists ~5 seconds after the provided ba.Widget dies. + object still exists ~5 seconds after the provided bauiv1.Widget dies. This is a good sanity check for any sort of object that wraps or - controls a ba.Widget. For instance, a 'Window' class instance has - no reason to still exist once its root container ba.Widget has fully + controls a bauiv1.Widget. For instance, a 'Window' class instance has + no reason to still exist once its root container bauiv1.Widget has fully transitioned out and been destroyed. Circular references or careless strong referencing can lead to such objects never getting destroyed, however, and this helps detect such cases to avoid memory leaks. """ if DEBUG_UI_CLEANUP_CHECKS: print(f'adding uicleanup to {obj}') - if not isinstance(widget, _ba.Widget): - raise TypeError('widget arg is not a ba.Widget') + if not isinstance(widget, _bauiv1.Widget): + raise TypeError('widget arg is not a bauiv1.Widget') if bool(False): @@ -196,7 +196,8 @@ def uicleanupcheck(obj: Any, widget: ba.Widget) -> None: widget.add_delete_callback(foobar) - _ba.app.ui.cleanupchecks.append( + assert _babase.app.classic is not None + _babase.app.classic.ui.cleanupchecks.append( UICleanupCheck( obj=weakref.ref(obj), widget=widget, widget_death_time=None ) @@ -205,9 +206,10 @@ def uicleanupcheck(obj: Any, widget: ba.Widget) -> None: def ui_upkeep() -> None: """Run UI cleanup checks, etc. should be called periodically.""" - ui = _ba.app.ui + assert _babase.app.classic is not None + ui = _babase.app.classic.ui remainingchecks = [] - now = _ba.time(TimeType.REAL) + now = _babase.apptime() for check in ui.cleanupchecks: obj = check.obj() diff --git a/assets/src/pdoc/templates/custom.css b/src/assets/pdoc/templates/custom.css similarity index 100% rename from assets/src/pdoc/templates/custom.css rename to src/assets/pdoc/templates/custom.css diff --git a/assets/src/pdoc/templates/index.html.jinja2 b/src/assets/pdoc/templates/index.html.jinja2 similarity index 100% rename from assets/src/pdoc/templates/index.html.jinja2 rename to src/assets/pdoc/templates/index.html.jinja2 diff --git a/assets/src/pdoc/templates/module.html.jinja2 b/src/assets/pdoc/templates/module.html.jinja2 similarity index 100% rename from assets/src/pdoc/templates/module.html.jinja2 rename to src/assets/pdoc/templates/module.html.jinja2 diff --git a/assets/src/server/README.txt b/src/assets/server/README.txt similarity index 86% rename from assets/src/server/README.txt rename to src/assets/server/README.txt index 78a1237a..407c003c 100644 --- a/assets/src/server/README.txt +++ b/src/assets/server/README.txt @@ -1,5 +1,5 @@ -To run this, simply cd into this directory and run ./ballisticacore_server -(on mac or linux) or launch_ballisticacore_server.bat (on windows). +To run this, simply cd into this directory and run ./ballisticakit_server +(on mac or linux) or launch_ballisticakit_server.bat (on windows). You'll need to open a UDP port (43210 by default) so that the world can communicate with your server. You can configure your server by editing the config.yaml file. diff --git a/assets/src/server/ballisticacore_server.py b/src/assets/server/ballisticakit_server.py similarity index 98% rename from assets/src/server/ballisticacore_server.py rename to src/assets/server/ballisticakit_server.py index 448e0e2d..840079dd 100755 --- a/assets/src/server/ballisticacore_server.py +++ b/src/assets/server/ballisticakit_server.py @@ -1,7 +1,7 @@ -#!/usr/bin/env python3.10 +#!/usr/bin/env python3.11 # Released under the MIT License. See LICENSE for details. # -"""BallisticaCore server manager.""" +"""BallisticaKit server manager.""" from __future__ import annotations import json @@ -35,7 +35,7 @@ VERSION_STR = '1.3.1' # Version history: # 1.3.1 -# Windows binary is now named BallisticaCoreHeadless.exe +# Windows binary is now named BallisticaKitHeadless.exe # 1.3: # Added show_tutorial config option # Added team_names config option @@ -62,10 +62,10 @@ VERSION_STR = '1.3.1' class ServerManagerApp: - """An app which manages BallisticaCore server execution. + """An app which manages BallisticaKit server execution. Handles configuring, launching, re-launching, and otherwise - managing BallisticaCore operating in server mode. + managing BallisticaKit operating in server mode. """ # How many seconds we wait after asking our subprocess to do an immediate @@ -125,7 +125,7 @@ class ServerManagerApp: dbgstr = 'debug' if __debug__ else 'opt' print( - f'{Clr.CYN}{Clr.BLD}BallisticaCore server manager {VERSION_STR}' + f'{Clr.CYN}{Clr.BLD}BallisticaKit server manager {VERSION_STR}' f' starting up ({dbgstr} mode)...{Clr.RST}', flush=True, ) @@ -427,7 +427,7 @@ class ServerManagerApp: f'{Clr.BLD}{filename} usage:{Clr.RST}\n' + cls._par( 'This script handles configuring, launching, re-launching,' - ' and otherwise managing BallisticaCore operating' + ' and otherwise managing BallisticaKit operating' ' in server mode. It can be run with no arguments, but' ' accepts the following optional ones:' ) @@ -534,11 +534,9 @@ class ServerManagerApp: time.sleep(1) def _load_config_from_file(self, print_confirmation: bool) -> ServerConfig: - out: ServerConfig | None = None if not os.path.exists(self._config_path): - # Special case: # If the user didn't specify a particular config file, allow # gracefully falling back to defaults if the default one is @@ -631,9 +629,9 @@ class ServerManagerApp: print(f'{Clr.CYN}Launching server subprocess...{Clr.RST}', flush=True) binary_name = ( - 'BallisticaCoreHeadless.exe' + 'BallisticaKitHeadless.exe' if os.name == 'nt' - else './ballisticacore_headless' + else './ballisticakit_headless' ) assert self._ba_root_path is not None self._subprocess = None @@ -641,7 +639,7 @@ class ServerManagerApp: # Launch! try: self._subprocess = subprocess.Popen( - [binary_name, '-cfgdir', self._ba_root_path], + [binary_name, '--config-dir', self._ba_root_path], stdin=subprocess.PIPE, cwd='dist', ) @@ -695,7 +693,6 @@ class ServerManagerApp: # If we want to die completely after this subprocess has ended, # tell the main thread to die. if self._wrapper_shutdown_desired: - # Only do this if the main thread is not already waiting for # us to die; otherwise it can lead to deadlock. # (we hang in os.kill while main thread is blocked in Thread.join) @@ -719,7 +716,7 @@ class ServerManagerApp: bincfg = {} # Some of our config values translate directly into the - # ballisticacore config file; the rest we pass at runtime. + # ballisticakit config file; the rest we pass at runtime. bincfg['Port'] = self._config.port bincfg['Auto Balance Teams'] = self._config.auto_balance_teams bincfg['Show Tutorial'] = self._config.show_tutorial @@ -759,7 +756,8 @@ class ServerManagerApp: val = repr(pickle.dumps(command)) assert '\n' not in val execcode = ( - f'import ba._servermode;' f' ba._servermode._cmd({val})\n' + f'import baclassic._servermode;' + f' baclassic._servermode._cmd({val})\n' ).encode() self._subprocess.stdin.write(execcode) self._subprocess.stdin.flush() @@ -777,7 +775,6 @@ class ServerManagerApp: self._send_server_command(StartServerModeCommand(self._config)) while True: - # If the app is trying to shut down, nope out immediately. if self._done: break @@ -815,7 +812,6 @@ class ServerManagerApp: # Watch for the server process exiting.. code: int | None = self._subprocess.poll() if code is not None: - clr = Clr.CYN if code == 0 else Clr.RED print( f'{clr}Server subprocess exited' @@ -941,7 +937,7 @@ class ServerManagerApp: def main() -> None: - """Run the BallisticaCore server manager.""" + """Run the BallisticaKit server manager.""" try: ServerManagerApp().run() except CleanError as exc: diff --git a/assets/src/server/config_template.yaml b/src/assets/server/config_template.yaml similarity index 73% rename from assets/src/server/config_template.yaml rename to src/assets/server/config_template.yaml index a2c89c9f..f2b38507 100644 --- a/assets/src/server/config_template.yaml +++ b/src/assets/server/config_template.yaml @@ -1,5 +1,5 @@ # To configure your server, create a config.yaml file in the same directory -# as the ballisticacore_server script. The config_template.yaml file can be +# as the ballisticakit_server script. The config_template.yaml file can be # copied or renamed as a convenient starting point. # Uncomment any of these values to override defaults. diff --git a/src/assets/server/launch_ballisticakit_server.bat b/src/assets/server/launch_ballisticakit_server.bat new file mode 100644 index 00000000..642162e3 --- /dev/null +++ b/src/assets/server/launch_ballisticakit_server.bat @@ -0,0 +1,3 @@ +:: Simply run the ballisticakit_server.py script with the bundled +:: Python interpreter. +dist\\python.exe ballisticakit_server.py diff --git a/src/ballistica/README.md b/src/ballistica/README.md new file mode 100644 index 00000000..dad97377 --- /dev/null +++ b/src/ballistica/README.md @@ -0,0 +1,86 @@ +# Ballistica Native Layer Source + +This directory is where most of Ballistica's 'native' layer lives. This code is +mostly C++ but with a smattering of other languages depending on the platform. +It gets compiled into several Python binary modules such as `_babase` or +`_bascenev1` which are then used by user-facing Python packages such as +[babase](../assets/ba_data/python/babase) or +[bascenev1](../assets/ba_data/python/bascenev1). Be aware that this separation +into distinct binary modules is largely for logic/organization purposes and does +not imply separate binaries; it is common for a Ballistica app to be +compiled into a single monolithic binary containing all of these modules +and often the Python library itself. + +## Feature Sets and C++ + +Similar to other places in the engine layout, code here is organized based on +[feature-sets](../../config/featuresets). Feature-sets make it easy to isolate, +add, and remove functionality from the engine at a high level. The only +subdirectory here *not* associated with a feature-set is 'shared'. + +On the Python side, a feature-set generally (but not always) has a corresponding +Python package. To access the `scene_v1` feature-set from Python, for instance, +one does `import bascenev1`. In turn, `bascenev1` itself may import +functionality it needs from other feature-set packages such as `babase` or +`baclassic`. And so on and so on. In this way, Python's module system provides +an elegant way to split the Python parts of the engine into logical pieces and +init each one only when it is needed. + +Taking things further, in order to keep things as consistent as possible between +the Python and native layers, our native layer has recently been redesigned to +sit on top of Python's module system. So even though our feature-sets still +often talk to each other directly through native C++ interfaces, they go through +Python's import mechanism to init each other and acquire those interfaces. + +The benefits of this setup is safety and consistency. It does not matter if we +do `import bascenev1` in Python or `scene_v1::SceneV1FeatureSet::Import()` from +C++; in either case we can be sure that both Python and C++ parts of the +`scene_v1` feature-set have been inited and are ready for use. In earlier +iterations of the feature-set system the Python and C++ worlds were more +independent and one had to take care to avoid using certain parts of a +feature-set from C++ if its Python side had not yet been imported, which was +both more confusing and more error prone. + +## C++ 'Module' Mechanism Details + +At the code level, we use a combination of C++ namespaces and global variables +to 'emulate' Python's module mechanism. + +Python consists of modules that import other modules (or stuff from within those +modules) into their own global namespaces for their own code to use. So when you +write `import babase` at the top of your Python module, this is what you are +doing - you are creating a `babase` global for yourself that you can then use +from anywhere in your module. + +Our analog to Python modules in Ballistica C++ is the `FeatureSetFrontEnd` +class. Feature-sets can define a subclass of `FeatureSetFrontEnd` which exposes +some C++ functionality, and other feature-sets can call that class's static +`Import()` method to get access to a shared singleton instance of that +front-end. So far this sounds pretty similar to Python modules. + +Where it breaks down, however, is the concept of module globals - the `babase` +we imported at the top of our Python script and can then use throughout it. Yes, +we could create a global `g_base` pointer in C++ for the `BaseFeatureSet` we +just imported, but then *all* our C++ code can access that global and there's no +elegant way to ensure it has been imported before being used. Alternately we +could have *no* globals and just have each `FeatureSetFrontEnd` store pointers +to any other `FeatureSetFrontEnd` it uses, but then we'd have to be passing +around, storing, and jumping through tons of feature-set pointers constantly to +do anything in the engine. + +In the end, the happy-medium solution employed by Ballistica is a combination of +globals and namespaces. Each feature-set has its own C++ namespace that is +basically thought of as its module namespace in Python. When a feature-set gets +imported, the first thing that it does is import any other feature-sets that it +uses into its own private namespace globals. So the `scene_v1` feature-set, when +imported, might import the `base` feature-set as a `g_base` global. But because +`scene_v1` has its own namespace, this global will actually be +`ballistica::scene_v1::g_base` which will be distinct from any `g_base` global +held by any other feature-set. So as long as each feature-set correctly lives in +its own namespace and uses only its own set of globals, things should behave +pretty much as they do for Python modules; feature-sets simply import what they +use when they themselves are imported and all code throughout the feature-set +can safely use those globals from that point on. + +Check out the [Template Feature Set](template_fs) for examples of wrangling +globals and namespaces to implement a feature-set-front-end in C++. diff --git a/src/ballistica/app/app.cc b/src/ballistica/app/app.cc deleted file mode 100644 index 55ef7da0..00000000 --- a/src/ballistica/app/app.cc +++ /dev/null @@ -1,18 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/app/app.h" - -#include - -namespace ballistica { - -App::App(int argc_in, char** argv_in) - : argc{argc_in}, argv{argv_in}, main_thread_id{std::this_thread::get_id()} { - // Enable extra timing logs via env var. - const char* debug_timing_env = getenv("BA_DEBUG_TIMING"); - if (debug_timing_env != nullptr && !strcmp(debug_timing_env, "1")) { - debug_timing = true; - } -} - -} // namespace ballistica diff --git a/src/ballistica/app/app.h b/src/ballistica/app/app.h deleted file mode 100644 index 4cfaacaf..00000000 --- a/src/ballistica/app/app.h +++ /dev/null @@ -1,85 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_APP_APP_H_ -#define BALLISTICA_APP_APP_H_ - -#include -#include -#include -#include -#include - -#include "ballistica/ballistica.h" - -namespace ballistica { - -// The first thing the engine does is allocate an instance of this as g_app. -class App { - public: - App(int argc, char** argv); - - // The following are misc values that should be migrated to applicable - // subsystem classes. - int argc{}; - char** argv{}; - bool threads_paused{}; - std::unordered_map node_types; - std::unordered_map node_types_by_id; - std::unordered_map node_message_types; - std::vector node_message_formats; - bool workspaces_in_use{}; - bool replay_open{}; - std::vector pausable_threads; - TouchInput* touch_input{}; - std::string console_startup_messages; - std::mutex v1_cloud_log_mutex; - std::string v1_cloud_log; - bool did_put_v1_cloud_log{}; - bool v1_cloud_log_full{}; - int master_server_source{0}; - int session_count{}; - bool shutting_down{}; - bool have_incentivized_ad{false}; - bool should_pause{}; - TelnetServer* telnet_server{}; - Console* console{}; - bool reset_vr_orientation{}; - bool user_ran_commands{}; - V1AccountType account_type{V1AccountType::kInvalid}; - bool remote_server_accepting_connections{true}; - std::string exec_command; - std::string user_agent_string{"BA_USER_AGENT_UNSET (" BA_PLATFORM_STRING ")"}; - int return_value{}; - bool debug_timing{}; - std::thread::id main_thread_id{}; - bool is_bootstrapped{}; - bool args_handled{}; - std::string user_config_dir; - bool started_suicide{}; - - // Maximum time in milliseconds to buffer game input/output before sending - // it over the network. - int buffer_time{0}; - - // How often we send dynamics resync messages. - int dynamics_sync_time{500}; - - // How many steps we sample for each bucket. - int delay_bucket_samples{60}; - - bool vr_mode{g_buildconfig.vr_build()}; - millisecs_t real_time{}; - millisecs_t last_real_time_ticks{}; - std::mutex real_time_mutex; - std::mutex thread_name_map_mutex; - std::unordered_map thread_name_map; -#if BA_DEBUG_BUILD - std::mutex object_list_mutex; - Object* object_list_first{}; - int object_count{0}; -#endif -}; - -} // namespace ballistica - -#endif // BALLISTICA_APP_APP_H_ diff --git a/src/ballistica/app/app_flavor.cc b/src/ballistica/app/app_flavor.cc deleted file mode 100644 index 0d237d87..00000000 --- a/src/ballistica/app/app_flavor.cc +++ /dev/null @@ -1,450 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/app/app_flavor.h" - -#include "ballistica/app/stress_test.h" -#include "ballistica/core/thread.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/renderer.h" -#include "ballistica/input/device/touch_input.h" -#include "ballistica/input/input.h" -#include "ballistica/logic/logic.h" -#include "ballistica/networking/network_reader.h" -#include "ballistica/networking/networking.h" -#include "ballistica/networking/telnet_server.h" -#include "ballistica/python/python.h" -#include "ballistica/ui/ui.h" - -namespace ballistica { - -AppFlavor::AppFlavor(Thread* thread) - : thread_(thread), stress_test_(std::make_unique()) { - // We modify some app behavior when run under the server manager. - auto* envval = getenv("BA_SERVER_WRAPPER_MANAGED"); - server_wrapper_managed_ = (envval && strcmp(envval, "1") == 0); -} - -void AppFlavor::PostInit() { - // Sanity check: make sure asserts are stripped out of release builds - // (NDEBUG should do this). -#if !BA_DEBUG_BUILD -#ifndef NDEBUG -#error Expected NDEBUG to be defined for release builds. -#endif // NDEBUG - assert(true); -#endif // !BA_DEBUG_BUILD - - g_app->user_agent_string = g_platform->GetUserAgentString(); - - // Figure out where our data is and chdir there. - g_platform->SetupDataDirectory(); - - // Run these just to make sure these dirs exist. - // (otherwise they might not get made if nothing writes to them). - g_platform->GetConfigDirectory(); - g_platform->GetUserPythonDirectory(); -} - -auto AppFlavor::ManagesEventLoop() const -> bool { - // We have 2 redundant values for essentially the same thing; - // should get rid of IsEventPushMode() once we've created - // AppFlavor subclasses for our various platforms. - return !g_platform->IsEventPushMode(); -} - -void AppFlavor::RunRenderUpkeepCycle() { - // This should only be used in cases where the OS is handling the event loop. - assert(!ManagesEventLoop()); - if (ManagesEventLoop()) { - return; - } - - // Pump thread messages (we're being driven by frame-draw callbacks - // so this is the only place that it gets done at). - thread()->RunEventLoop(true); // Single pass only. - - // Now do the general app event cycle for whoever needs to process things. - RunEvents(); -} - -void AppFlavor::RebuildLostGLContext() { - assert(InMainThread()); - assert(g_graphics_server); - if (g_graphics_server) { - g_graphics_server->RebuildLostContext(); - } -} - -void AppFlavor::DrawFrame(bool during_resize) { - assert(InMainThread()); - - // It's possible to receive frames before we're ready to draw. - if (!g_graphics_server || !g_graphics_server->renderer()) { - return; - } - - millisecs_t starttime = GetRealTime(); - - // A resize-draw event means that we're drawing due to a window resize. - // In this case we ignore regular draw events for a short while - // afterwards which makes resizing smoother. - // FIXME: should figure out the *correct* way to handle this; - // I believe the underlying cause here is some sort of context contention - // across threads. - if (during_resize) { - last_resize_draw_event_time_ = starttime; - } else { - if (starttime - last_resize_draw_event_time_ < (1000 / 30)) { - return; - } - } - g_graphics_server->TryRender(); - RunRenderUpkeepCycle(); -} - -void AppFlavor::SetScreenResolution(float width, float height) { - assert(InMainThread()); - if (!HeadlessMode()) { - g_graphics_server->VideoResize(width, height); - } -} - -void AppFlavor::PushShutdownCompleteCall() { - thread()->PushCall([this] { ShutdownComplete(); }); -} - -void AppFlavor::ShutdownComplete() { - assert(InMainThread()); - assert(g_platform); - - done_ = true; - - // Kill our own event loop (or tell the OS to kill its). - if (ManagesEventLoop()) { - thread()->Quit(); - } else { - g_platform->QuitApp(); - } -} - -void AppFlavor::RunEvents() { - // there's probably a better place for this... - stress_test_->Update(); - - // Give platforms a chance to pump/handle their own events. - // FIXME: now that we have app class overrides, platform should really - // not be doing event handling. (need to fix rift build). - g_platform->RunEvents(); -} - -void AppFlavor::UpdatePauseResume() { - if (actually_paused_) { - // Unpause if no one wants pause. - if (!sys_paused_app_) { - OnResume(); - actually_paused_ = false; - } - } else { - // Pause if anyone wants. - if (sys_paused_app_) { - OnPause(); - actually_paused_ = true; - } - } -} - -void AppFlavor::OnPause() { - assert(InMainThread()); - - g_graphics->SetGyroEnabled(false); - - // IMPORTANT: Any on-pause related stuff that threads need to do must - // be done from registered pause-callbacks. If we instead push runnables - // to them from here they may or may not be called before the thread - // is actually paused. - - Thread::SetThreadsPaused(true); - - assert(g_networking); - g_networking->Pause(); - - assert(g_network_reader); - if (g_network_reader) { - g_network_reader->Pause(); - } - - if (g_app->telnet_server) { - g_app->telnet_server->Pause(); - } - - g_platform->OnAppPause(); -} - -void AppFlavor::OnResume() { - assert(InMainThread()); - last_app_resume_time_ = GetRealTime(); - Thread::SetThreadsPaused(false); - - g_platform->OnAppResume(); - g_networking->Resume(); - g_network_reader->Resume(); - - if (g_app->telnet_server) { - g_app->telnet_server->Resume(); - } - - // Also let the Python layer do what it needs to - // (starting/stopping music, etc.). - g_python->PushObjCall(Python::ObjID::kHandleAppResumeCall); - g_logic->PushOnAppResumeCall(); - - g_graphics->SetGyroEnabled(true); - - // When resuming from a paused state, we may want to - // pause whatever game was running when we last were active. - // TODO(efro): we should make this smarter so it doesn't happen if - // we're in a network game or something that we can't pause; - // bringing up the menu doesn't really accomplish anything there. - if (g_app->should_pause) { - g_app->should_pause = false; - - // If we've been completely backgrounded, - // send a menu-press command to the game; this will - // bring up a pause menu if we're in the game/etc. - g_ui->PushMainMenuPressCall(nullptr); - } -} - -auto AppFlavor::GetProductPrice(const std::string& product) -> std::string { - std::scoped_lock lock(product_prices_mutex_); - auto i = product_prices_.find(product); - if (i == product_prices_.end()) { - return ""; - } else { - return i->second; - } -} - -void AppFlavor::SetProductPrice(const std::string& product, - const std::string& price) { - std::scoped_lock lock(product_prices_mutex_); - product_prices_[product] = price; -} - -void AppFlavor::PauseApp() { - assert(InMainThread()); - millisecs_t start_time{Platform::GetCurrentMilliseconds()}; - - // Apple mentioned 5 seconds to run stuff once backgrounded or - // they bring down the hammer. Let's aim to stay under 2. - millisecs_t max_duration{2000}; - - Platform::DebugLog("PauseApp@" - + std::to_string(Platform::GetCurrentMilliseconds())); - assert(!sys_paused_app_); - sys_paused_app_ = true; - UpdatePauseResume(); - - // We assume that the OS will completely suspend our process the moment - // we return from this call (though this is not technically true on all - // platforms). So we want to spin and wait for threads to actually - // process the pause message. - size_t running_thread_count{}; - while (std::abs(Platform::GetCurrentMilliseconds() - start_time) - < max_duration) { - // If/when we get to a point with no threads waiting to be paused, - // we're good to go. - auto threads{Thread::GetStillPausingThreads()}; - running_thread_count = threads.size(); - if (running_thread_count == 0) { - if (g_buildconfig.debug_build()) { - Log(LogLevel::kDebug, - "PauseApp() completed in " - + std::to_string(Platform::GetCurrentMilliseconds() - - start_time) - + "ms."); - } - return; - } - } - - // If we made it here, we timed out. Complain. - Log(LogLevel::kError, - std::string("PauseApp() took too long; ") - + std::to_string(running_thread_count) - + " threads not yet paused after " - + std::to_string(Platform::GetCurrentMilliseconds() - start_time) - + " ms."); -} - -void AppFlavor::ResumeApp() { - assert(InMainThread()); - millisecs_t start_time{Platform::GetCurrentMilliseconds()}; - Platform::DebugLog("ResumeApp@" - + std::to_string(Platform::GetCurrentMilliseconds())); - assert(sys_paused_app_); - sys_paused_app_ = false; - UpdatePauseResume(); - if (g_buildconfig.debug_build()) { - Log(LogLevel::kDebug, - "ResumeApp() completed in " - + std::to_string(Platform::GetCurrentMilliseconds() - start_time) - + "ms."); - } -} - -void AppFlavor::DidFinishRenderingFrame(FrameDef* frame) {} - -void AppFlavor::PrimeEventPump() { - assert(!ManagesEventLoop()); - - // Pump events manually until a screen gets created. - // At that point we use frame-draws to drive our event loop. - while (!g_graphics_server->initial_screen_created()) { - thread()->RunEventLoop(true); - Platform::SleepMS(1); - } -} - -#pragma mark Push-Calls - -// FIXME - move this call to Platform. -void AppFlavor::PushShowOnlineScoreUICall(const std::string& show, - const std::string& game, - const std::string& game_version) { - thread()->PushCall([show, game, game_version] { - assert(InMainThread()); - g_platform->ShowOnlineScoreUI(show, game, game_version); - }); -} - -void AppFlavor::PushNetworkSetupCall(int port, int telnet_port, - bool enable_telnet, - const std::string& telnet_password) { - thread()->PushCall([port, telnet_port, enable_telnet, telnet_password] { - assert(InMainThread()); - g_network_reader->SetPort(port); - if (g_app->telnet_server == nullptr && enable_telnet) { - new TelnetServer(telnet_port); - assert(g_app->telnet_server); - if (telnet_password.empty()) { - g_app->telnet_server->SetPassword(nullptr); - } else { - g_app->telnet_server->SetPassword(telnet_password.c_str()); - } - } - }); -} - -void AppFlavor::PushPurchaseAckCall(const std::string& purchase, - const std::string& order_id) { - thread()->PushCall( - [purchase, order_id] { g_platform->PurchaseAck(purchase, order_id); }); -} - -auto AppFlavor::PushPurchaseCall(const std::string& item) -> void { - thread()->PushCall([item] { - assert(InMainThread()); - g_platform->Purchase(item); - }); -} - -void AppFlavor::PushRestorePurchasesCall() { - thread()->PushCall([] { - assert(InMainThread()); - g_platform->RestorePurchases(); - }); -} - -void AppFlavor::PushOpenURLCall(const std::string& url) { - thread()->PushCall([url] { g_platform->OpenURL(url); }); -} - -void AppFlavor::PushSubmitScoreCall(const std::string& game, - const std::string& game_version, - int64_t score) { - thread()->PushCall([game, game_version, score] { - g_platform->SubmitScore(game, game_version, score); - }); -} - -void AppFlavor::PushAchievementReportCall(const std::string& achievement) { - thread()->PushCall( - [achievement] { g_platform->ReportAchievement(achievement); }); -} - -void AppFlavor::PushStringEditCall(const std::string& name, - const std::string& value, int max_chars) { - thread()->PushCall([name, value, max_chars] { - static millisecs_t last_edit_time = 0; - millisecs_t t = GetRealTime(); - - // Ignore if too close together. - // (in case second request comes in before first takes effect). - if (t - last_edit_time < 1000) { - return; - } - last_edit_time = t; - assert(InMainThread()); - g_platform->EditText(name, value, max_chars); - }); -} - -void AppFlavor::PushSetStressTestingCall(bool enable, int player_count) { - thread()->PushCall([this, enable, player_count] { - stress_test_->SetStressTesting(enable, player_count); - }); -} - -void AppFlavor::PushResetAchievementsCall() { - thread()->PushCall([] { g_platform->ResetAchievements(); }); -} - -void AppFlavor::OnAppStart() { - assert(InMainThread()); - assert(g_input); - - // If we're running in a terminal, print some info. - // if (g_platform->is_stdin_a_terminal()) { - { - char buffer[256]; - if (g_buildconfig.headless_build()) { - snprintf(buffer, sizeof(buffer), "BallisticaCore Headless %s build %d.", - kAppVersion, kAppBuildNumber); - } else { - snprintf(buffer, sizeof(buffer), "BallisticaCore %s build %d.", - kAppVersion, kAppBuildNumber); - } - Log(LogLevel::kInfo, buffer); - } - // } - - // If we've got a nice themed hardware cursor, show it. - // Otherwise, hide the hardware cursor; we'll draw it in software. - // (need to run this in postinit because SDL/etc. may not be inited yet - // as of AppFlavor::AppFlavor()). - g_platform->SetHardwareCursorVisible(g_buildconfig.hardware_cursor()); - - if (!HeadlessMode()) { - // On desktop systems we just assume keyboard input exists and add it - // immediately. - if (g_platform->IsRunningOnDesktop()) { - g_input->PushCreateKeyboardInputDevices(); - } - - // On non-tv, non-desktop, non-vr systems, create a touchscreen input. - if (!g_platform->IsRunningOnTV() && !IsVRMode() - && !g_platform->IsRunningOnDesktop()) { - g_input->CreateTouchInput(); - } - } -} - -void AppFlavor::PushCursorUpdate(bool vis) { - thread()->PushCall([vis] { - assert(InMainThread()); - g_platform->SetHardwareCursorVisible(vis); - }); -} - -} // namespace ballistica diff --git a/src/ballistica/app/app_flavor_headless.cc b/src/ballistica/app/app_flavor_headless.cc deleted file mode 100644 index 2e83bc05..00000000 --- a/src/ballistica/app/app_flavor_headless.cc +++ /dev/null @@ -1,24 +0,0 @@ -// Released under the MIT License. See LICENSE for details. -#if BA_HEADLESS_BUILD - -#include "ballistica/app/app_flavor_headless.h" - -#include "ballistica/ballistica.h" - -namespace ballistica { - -// We could technically use the vanilla App class here since we're not -// changing anything. -AppFlavorHeadless::AppFlavorHeadless(Thread* thread) : AppFlavor(thread) { - // Handle a few misc things like stress-test updates. - // (SDL builds set up a similar timer so we need to also). - // This can probably go away at some point. - this->thread()->NewTimer(10, true, NewLambdaRunnable([this] { - assert(g_app_flavor); - g_app_flavor->RunEvents(); - })); -} - -} // namespace ballistica - -#endif // BA_HEADLESS_BUILD diff --git a/src/ballistica/app/app_flavor_headless.h b/src/ballistica/app/app_flavor_headless.h deleted file mode 100644 index 2dead6c3..00000000 --- a/src/ballistica/app/app_flavor_headless.h +++ /dev/null @@ -1,20 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_APP_APP_FLAVOR_HEADLESS_H_ -#define BALLISTICA_APP_APP_FLAVOR_HEADLESS_H_ -#if BA_HEADLESS_BUILD - -#include "ballistica/app/app_flavor.h" -#include "ballistica/core/thread.h" - -namespace ballistica { - -class AppFlavorHeadless : public AppFlavor { - public: - explicit AppFlavorHeadless(Thread* thread); -}; - -} // namespace ballistica - -#endif // BA_HEADLESS_BUILD -#endif // BALLISTICA_APP_APP_FLAVOR_HEADLESS_H_ diff --git a/src/ballistica/app/app_flavor_vr.cc b/src/ballistica/app/app_flavor_vr.cc deleted file mode 100644 index 4fa5f7a6..00000000 --- a/src/ballistica/app/app_flavor_vr.cc +++ /dev/null @@ -1,112 +0,0 @@ -// Released under the MIT License. See LICENSE for details. -#if BA_VR_BUILD - -#include "ballistica/app/app_flavor_vr.h" - -#include "ballistica/core/thread.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/renderer.h" -#include "ballistica/logic/logic.h" - -namespace ballistica { - -AppFlavorVR::AppFlavorVR(Thread* thread) : AppFlavor(thread) {} - -auto AppFlavorVR::PushVRSimpleRemoteStateCall(const VRSimpleRemoteState& state) - -> void { - thread()->PushCall([this, state] { - // Convert this to a full hands state, adding in some simple elbow - // positioning of our own and left/right. - VRHandsState s; - s.l.tx = -0.2f; - s.l.ty = -0.2f; - s.l.tz = -0.3f; - - // Hmm; for now lets always assign this as right hand even when its in - // left-handed mode to keep things simple on the back-end. Can change later - // if there's a downside to that. - s.r.type = VRHandType::kDaydreamRemote; - s.r.tx = 0.2f; - s.r.ty = -0.2f; - s.r.tz = -0.3f; - s.r.yaw = state.r0; - s.r.pitch = state.r1; - s.r.roll = state.r2; - VRSetHands(s); - }); -} - -auto AppFlavorVR::VRSetDrawDimensions(int w, int h) -> void { - g_graphics_server->VideoResize(w, h); -} - -void AppFlavorVR::VRPreDraw() { - if (!g_graphics_server || !g_graphics_server->renderer()) { - return; - } - assert(InMainThread()); - if (FrameDef* frame_def = g_graphics_server->GetRenderFrameDef()) { - // Note: this could be part of PreprocessRenderFrameDef but - // the non-vr path needs it to be separate since preprocess doesn't - // happen sometimes. Should probably clean that up. - g_graphics_server->RunFrameDefMeshUpdates(frame_def); - - // store this for the duration of this frame - vr_render_frame_def_ = frame_def; - g_graphics_server->PreprocessRenderFrameDef(frame_def); - } -} - -auto AppFlavorVR::VRPostDraw() -> void { - assert(InMainThread()); - if (!g_graphics_server || !g_graphics_server->renderer()) { - return; - } - if (vr_render_frame_def_) { - g_graphics_server->FinishRenderFrameDef(vr_render_frame_def_); - vr_render_frame_def_ = nullptr; - } - RunRenderUpkeepCycle(); -} - -auto AppFlavorVR::VRSetHead(float tx, float ty, float tz, float yaw, - float pitch, float roll) -> void { - assert(InMainThread()); - Renderer* renderer = g_graphics_server->renderer(); - if (renderer == nullptr) return; - renderer->VRSetHead(tx, ty, tz, yaw, pitch, roll); -} - -auto AppFlavorVR::VRSetHands(const VRHandsState& state) -> void { - assert(InMainThread()); - - // Pass this along to the renderer (in this same thread) for drawing - // (so hands can be drawn at their absolute most up-to-date positions, etc). - Renderer* renderer = g_graphics_server->renderer(); - if (renderer == nullptr) return; - renderer->VRSetHands(state); - - // ALSO ship it off to the game/ui thread to actually handle input from it. - g_logic->PushVRHandsState(state); -} - -auto AppFlavorVR::VRDrawEye(int eye, float yaw, float pitch, float roll, - float tan_l, float tan_r, float tan_b, float tan_t, - float eye_x, float eye_y, float eye_z, - int viewport_x, int viewport_y) -> void { - if (!g_graphics_server || !g_graphics_server->renderer()) { - return; - } - assert(InMainThread()); - if (vr_render_frame_def_) { - // set up VR eye stuff... - Renderer* renderer = g_graphics_server->renderer(); - renderer->VRSetEye(eye, yaw, pitch, roll, tan_l, tan_r, tan_b, tan_t, eye_x, - eye_y, eye_z, viewport_x, viewport_y); - g_graphics_server->DrawRenderFrameDef(vr_render_frame_def_); - } -} - -} // namespace ballistica - -#endif // BA_VR_BUILD diff --git a/src/ballistica/app/app_flavor_vr.h b/src/ballistica/app/app_flavor_vr.h deleted file mode 100644 index e0620275..00000000 --- a/src/ballistica/app/app_flavor_vr.h +++ /dev/null @@ -1,50 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_APP_APP_FLAVOR_VR_H_ -#define BALLISTICA_APP_APP_FLAVOR_VR_H_ - -#if BA_VR_BUILD - -#include "ballistica/app/app_flavor.h" - -namespace ballistica { - -class AppFlavorVR : public AppFlavor { - public: - /// For passing in state of Daydream remote (and maybe gear vr?..). - struct VRSimpleRemoteState { - bool right_handed = true; - float r0 = 0.0f; - float r1 = 0.0f; - float r2 = 0.0f; - }; - - /// Return g_app_flavor as a AppFlavorVR. (assumes it actually is one). - static auto get() -> AppFlavorVR* { - assert(g_app_flavor != nullptr); - assert(dynamic_cast(g_app_flavor) - == static_cast(g_app_flavor)); - return static_cast(g_app_flavor); - } - - explicit AppFlavorVR(Thread* thread); - auto PushVRSimpleRemoteStateCall(const VRSimpleRemoteState& state) -> void; - auto VRSetDrawDimensions(int w, int h) -> void; - auto VRPreDraw() -> void; - auto VRPostDraw() -> void; - auto VRSetHead(float tx, float ty, float tz, float yaw, float pitch, - float roll) -> void; - auto VRSetHands(const VRHandsState& state) -> void; - auto VRDrawEye(int eye, float yaw, float pitch, float roll, float tan_l, - float tan_r, float tan_b, float tan_t, float eye_x, - float eye_y, float eye_z, int viewport_x, int viewport_y) - -> void; - - private: - FrameDef* vr_render_frame_def_{}; -}; - -} // namespace ballistica - -#endif // BA_VR_BUILD -#endif // BALLISTICA_APP_APP_FLAVOR_VR_H_ diff --git a/src/ballistica/app/stress_test.cc b/src/ballistica/app/stress_test.cc deleted file mode 100644 index cbc6524c..00000000 --- a/src/ballistica/app/stress_test.cc +++ /dev/null @@ -1,114 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/app/stress_test.h" - -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/renderer.h" -#include "ballistica/input/input.h" - -namespace ballistica { - -void StressTest::SetStressTesting(bool enable, int player_count) { - bool was_stress_testing = stress_testing_; - stress_testing_ = enable; - stress_test_player_count_ = player_count; - - // If we're turning on, reset our intervals and things. - if (!was_stress_testing && stress_testing_) { - // So our first sample is 1 interval from now... - last_stress_test_update_time_ = GetRealTime(); - // Reset our frames-rendered tally. - if (g_graphics && g_graphics_server->renderer()) { - last_total_frames_rendered_ = - g_graphics_server->renderer()->total_frames_rendered(); - } else { - // Assume zero if there's no graphics yet. - last_total_frames_rendered_ = 0; - } - } -} - -void StressTest::Update() { - assert(InMainThread()); - - // Handle a little misc stuff here. - // If we're currently running stress-tests, update that stuff. - if (stress_testing_ && g_input) { - // Update our fake inputs to make our dudes run around. - g_input->ProcessStressTesting(stress_test_player_count_); - - // Every 10 seconds update our stress-test stats. - millisecs_t t = GetRealTime(); - if (t - last_stress_test_update_time_ >= 10000) { - if (stress_test_stats_file_ == nullptr) { - assert(g_platform); - std::string f_name = - g_platform->GetUserPythonDirectory() + "/stress_test_stats.csv"; - stress_test_stats_file_ = g_platform->FOpen(f_name.c_str(), "wb"); - if (stress_test_stats_file_ != nullptr) { - fprintf(stress_test_stats_file_, - "time,averageFps,nodes,models,collide_models,textures,sounds," - "pssMem,sharedDirtyMem,privateDirtyMem\n"); - fflush(stress_test_stats_file_); - } - } - if (stress_test_stats_file_ != nullptr) { - // See how many frames we've rendered this past interval. - int total_frames_rendered; - if (g_graphics_server && g_graphics_server->renderer()) { - total_frames_rendered = - g_graphics_server->renderer()->total_frames_rendered(); - } else { - total_frames_rendered = last_total_frames_rendered_; - } - float avg = - static_cast(total_frames_rendered - - last_total_frames_rendered_) - / (static_cast(t - last_stress_test_update_time_) / 1000.0f); - last_total_frames_rendered_ = total_frames_rendered; - uint32_t model_count = 0; - uint32_t collide_model_count = 0; - uint32_t texture_count = 0; - uint32_t sound_count = 0; - uint32_t node_count = 0; - if (g_assets) { - model_count = g_assets->total_model_count(); - collide_model_count = g_assets->total_collide_model_count(); - texture_count = g_assets->total_texture_count(); - sound_count = g_assets->total_sound_count(); - } - assert(g_logic); - std::string mem_usage = g_platform->GetMemUsageInfo(); - fprintf(stress_test_stats_file_, "%d,%.1f,%d,%d,%d,%d,%d,%s\n", - static_cast_check_fit(GetRealTime()), avg, node_count, - model_count, collide_model_count, texture_count, sound_count, - mem_usage.c_str()); - fflush(stress_test_stats_file_); - } - last_stress_test_update_time_ = t; - } - } -} - -void StressTest::Set(bool enable, int player_count) { - assert(InMainThread()); - bool was_stress_testing = stress_testing_; - stress_testing_ = enable; - stress_test_player_count_ = player_count; - - // If we're turning on, reset our intervals and things. - if (!was_stress_testing && stress_testing_) { - // So our first sample is 1 interval from now. - last_stress_test_update_time_ = GetRealTime(); - - // Reset our frames-rendered tally. - if (g_graphics_server && g_graphics_server->renderer()) { - last_total_frames_rendered_ = - g_graphics_server->renderer()->total_frames_rendered(); - } else { - // Assume zero if there's no graphics yet. - last_total_frames_rendered_ = 0; - } - } -} -} // namespace ballistica diff --git a/src/ballistica/app/stress_test.h b/src/ballistica/app/stress_test.h deleted file mode 100644 index ae346649..00000000 --- a/src/ballistica/app/stress_test.h +++ /dev/null @@ -1,31 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_APP_STRESS_TEST_H_ -#define BALLISTICA_APP_STRESS_TEST_H_ - -#include "ballistica/ballistica.h" - -namespace ballistica { - -// FIXME: This is not wired up; I just moved things here from App. -class StressTest { - public: - // This used to be a SetStressTesting() call in App. - void Set(bool enable, int player_count); - - // This used to get run from RunEvents() in App. - void Update(); - - void SetStressTesting(bool enable, int player_count); - - private: - FILE* stress_test_stats_file_{}; - millisecs_t last_stress_test_update_time_{}; - bool stress_testing_{}; - int stress_test_player_count_{8}; - int last_total_frames_rendered_{}; -}; - -} // namespace ballistica - -#endif // BALLISTICA_APP_STRESS_TEST_H_ diff --git a/src/ballistica/assets/assets.cc b/src/ballistica/assets/assets.cc deleted file mode 100644 index 8bcc5ee1..00000000 --- a/src/ballistica/assets/assets.cc +++ /dev/null @@ -1,1238 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/assets/assets.h" - -#if !BA_OSTYPE_WINDOWS -#include -#endif - -#include "ballistica/assets/assets_server.h" -#include "ballistica/assets/component/collide_model.h" -#include "ballistica/assets/component/data.h" -#include "ballistica/assets/component/model.h" -#include "ballistica/assets/component/texture.h" -#include "ballistica/assets/data/sound_data.h" -#include "ballistica/audio/audio_server.h" -#include "ballistica/core/thread.h" -#include "ballistica/generic/timer.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/text/text_packer.h" -#include "ballistica/logic/logic.h" -#include "ballistica/python/python_sys.h" - -namespace ballistica { - -// Debug printing: -#define BA_SHOW_LOADS_UNLOADS 0 -#define SHOW_PRUNING_INFO 0 - -// Standard prune time for unused assets: 10 minutes (1000ms * 60 * 10). -#define STANDARD_ASSET_PRUNE_TIME 600000 - -// More aggressive prune time for dynamically-generated text-textures: 10 -// seconds. -#define TEXT_TEXTURE_PRUNE_TIME 10000 - -#define QR_TEXTURE_PRUNE_TIME 10000 - -// How long we should spend loading assets in each runPendingLoads() call. -#define PENDING_LOAD_PROCESS_TIME 5 - -Assets::Assets() { - asset_paths_.emplace_back("ba_data"); - for (bool& have_pending_load : have_pending_loads_) { - have_pending_load = false; - } -} - -void Assets::LoadSystemTexture(SystemTextureID id, const char* name) { - assert(asset_lists_locked_); - system_textures_.push_back(GetTextureData(name)); - assert(system_textures_.size() == static_cast(id) + 1); -} - -void Assets::LoadSystemCubeMapTexture(SystemCubeMapTextureID id, - const char* name) { - assert(asset_lists_locked_); - system_cube_map_textures_.push_back(GetCubeMapTextureData(name)); - assert(system_cube_map_textures_.size() == static_cast(id) + 1); -} - -void Assets::LoadSystemSound(SystemSoundID id, const char* name) { - system_sounds_.push_back(GetSoundData(name)); - assert(system_sounds_.size() == static_cast(id) + 1); -} - -void Assets::LoadSystemData(SystemDataID id, const char* name) { - system_datas_.push_back(GetDataData(name)); - assert(system_datas_.size() == static_cast(id) + 1); -} - -void Assets::LoadSystemModel(SystemModelID id, const char* name) { - system_models_.push_back(GetModelData(name)); - assert(system_models_.size() == static_cast(id) + 1); -} - -void Assets::LoadSystemAssets() { - assert(InLogicThread()); - assert(g_audio_server && g_assets_server && g_graphics_server); - assert(g_graphics_server - && g_graphics_server->texture_compression_types_are_set()); - assert(g_graphics && g_graphics_server->texture_quality_set()); - - // Just grab the lock once for all this stuff for efficiency. - AssetListLock lock; - - // System textures: - LoadSystemTexture(SystemTextureID::kUIAtlas, "uiAtlas"); - LoadSystemTexture(SystemTextureID::kButtonSquare, "buttonSquare"); - LoadSystemTexture(SystemTextureID::kWhite, "white"); - LoadSystemTexture(SystemTextureID::kFontSmall0, "fontSmall0"); - LoadSystemTexture(SystemTextureID::kFontBig, "fontBig"); - LoadSystemTexture(SystemTextureID::kCursor, "cursor"); - LoadSystemTexture(SystemTextureID::kBoxingGlove, "boxingGlovesColor"); - LoadSystemTexture(SystemTextureID::kShield, "shield"); - LoadSystemTexture(SystemTextureID::kExplosion, "explosion"); - LoadSystemTexture(SystemTextureID::kTextClearButton, "textClearButton"); - LoadSystemTexture(SystemTextureID::kWindowHSmallVMed, "windowHSmallVMed"); - LoadSystemTexture(SystemTextureID::kWindowHSmallVSmall, "windowHSmallVSmall"); - LoadSystemTexture(SystemTextureID::kGlow, "glow"); - LoadSystemTexture(SystemTextureID::kScrollWidget, "scrollWidget"); - LoadSystemTexture(SystemTextureID::kScrollWidgetGlow, "scrollWidgetGlow"); - LoadSystemTexture(SystemTextureID::kFlagPole, "flagPoleColor"); - LoadSystemTexture(SystemTextureID::kScorch, "scorch"); - LoadSystemTexture(SystemTextureID::kScorchBig, "scorchBig"); - LoadSystemTexture(SystemTextureID::kShadow, "shadow"); - LoadSystemTexture(SystemTextureID::kLight, "light"); - LoadSystemTexture(SystemTextureID::kShadowSharp, "shadowSharp"); - LoadSystemTexture(SystemTextureID::kLightSharp, "lightSharp"); - LoadSystemTexture(SystemTextureID::kShadowSoft, "shadowSoft"); - LoadSystemTexture(SystemTextureID::kLightSoft, "lightSoft"); - LoadSystemTexture(SystemTextureID::kSparks, "sparks"); - LoadSystemTexture(SystemTextureID::kEye, "eyeColor"); - LoadSystemTexture(SystemTextureID::kEyeTint, "eyeColorTintMask"); - LoadSystemTexture(SystemTextureID::kFuse, "fuse"); - LoadSystemTexture(SystemTextureID::kShrapnel1, "shrapnel1Color"); - LoadSystemTexture(SystemTextureID::kSmoke, "smoke"); - LoadSystemTexture(SystemTextureID::kCircle, "circle"); - LoadSystemTexture(SystemTextureID::kCircleOutline, "circleOutline"); - LoadSystemTexture(SystemTextureID::kCircleNoAlpha, "circleNoAlpha"); - LoadSystemTexture(SystemTextureID::kCircleOutlineNoAlpha, - "circleOutlineNoAlpha"); - LoadSystemTexture(SystemTextureID::kCircleShadow, "circleShadow"); - LoadSystemTexture(SystemTextureID::kSoftRect, "softRect"); - LoadSystemTexture(SystemTextureID::kSoftRect2, "softRect2"); - LoadSystemTexture(SystemTextureID::kSoftRectVertical, "softRectVertical"); - LoadSystemTexture(SystemTextureID::kStartButton, "startButton"); - LoadSystemTexture(SystemTextureID::kBombButton, "bombButton"); - LoadSystemTexture(SystemTextureID::kOuyaAButton, "ouyaAButton"); - LoadSystemTexture(SystemTextureID::kBackIcon, "backIcon"); - LoadSystemTexture(SystemTextureID::kNub, "nub"); - LoadSystemTexture(SystemTextureID::kArrow, "arrow"); - LoadSystemTexture(SystemTextureID::kMenuButton, "menuButton"); - LoadSystemTexture(SystemTextureID::kUsersButton, "usersButton"); - LoadSystemTexture(SystemTextureID::kActionButtons, "actionButtons"); - LoadSystemTexture(SystemTextureID::kTouchArrows, "touchArrows"); - LoadSystemTexture(SystemTextureID::kTouchArrowsActions, "touchArrowsActions"); - LoadSystemTexture(SystemTextureID::kRGBStripes, "rgbStripes"); - LoadSystemTexture(SystemTextureID::kUIAtlas2, "uiAtlas2"); - LoadSystemTexture(SystemTextureID::kFontSmall1, "fontSmall1"); - LoadSystemTexture(SystemTextureID::kFontSmall2, "fontSmall2"); - LoadSystemTexture(SystemTextureID::kFontSmall3, "fontSmall3"); - LoadSystemTexture(SystemTextureID::kFontSmall4, "fontSmall4"); - LoadSystemTexture(SystemTextureID::kFontSmall5, "fontSmall5"); - LoadSystemTexture(SystemTextureID::kFontSmall6, "fontSmall6"); - LoadSystemTexture(SystemTextureID::kFontSmall7, "fontSmall7"); - LoadSystemTexture(SystemTextureID::kFontExtras, "fontExtras"); - LoadSystemTexture(SystemTextureID::kFontExtras2, "fontExtras2"); - LoadSystemTexture(SystemTextureID::kFontExtras3, "fontExtras3"); - LoadSystemTexture(SystemTextureID::kFontExtras4, "fontExtras4"); - LoadSystemTexture(SystemTextureID::kCharacterIconMask, "characterIconMask"); - LoadSystemTexture(SystemTextureID::kBlack, "black"); - LoadSystemTexture(SystemTextureID::kWings, "wings"); - - // System cube map textures: - LoadSystemCubeMapTexture(SystemCubeMapTextureID::kReflectionChar, - "reflectionChar#"); - LoadSystemCubeMapTexture(SystemCubeMapTextureID::kReflectionPowerup, - "reflectionPowerup#"); - LoadSystemCubeMapTexture(SystemCubeMapTextureID::kReflectionSoft, - "reflectionSoft#"); - LoadSystemCubeMapTexture(SystemCubeMapTextureID::kReflectionSharp, - "reflectionSharp#"); - LoadSystemCubeMapTexture(SystemCubeMapTextureID::kReflectionSharper, - "reflectionSharper#"); - LoadSystemCubeMapTexture(SystemCubeMapTextureID::kReflectionSharpest, - "reflectionSharpest#"); - - // System sounds: - LoadSystemSound(SystemSoundID::kDeek, "deek"); - LoadSystemSound(SystemSoundID::kBlip, "blip"); - LoadSystemSound(SystemSoundID::kBlank, "blank"); - LoadSystemSound(SystemSoundID::kPunch, "punch01"); - LoadSystemSound(SystemSoundID::kClick, "click01"); - LoadSystemSound(SystemSoundID::kErrorBeep, "error"); - LoadSystemSound(SystemSoundID::kSwish, "swish"); - LoadSystemSound(SystemSoundID::kSwish2, "swish2"); - LoadSystemSound(SystemSoundID::kSwish3, "swish3"); - LoadSystemSound(SystemSoundID::kTap, "tap"); - LoadSystemSound(SystemSoundID::kCorkPop, "corkPop"); - LoadSystemSound(SystemSoundID::kGunCock, "gunCocking"); - LoadSystemSound(SystemSoundID::kTickingCrazy, "tickingCrazy"); - LoadSystemSound(SystemSoundID::kSparkle, "sparkle01"); - LoadSystemSound(SystemSoundID::kSparkle2, "sparkle02"); - LoadSystemSound(SystemSoundID::kSparkle3, "sparkle03"); - - // System datas: - // (crickets) - - // System models: - LoadSystemModel(SystemModelID::kButtonSmallTransparent, - "buttonSmallTransparent"); - LoadSystemModel(SystemModelID::kButtonSmallOpaque, "buttonSmallOpaque"); - LoadSystemModel(SystemModelID::kButtonMediumTransparent, - "buttonMediumTransparent"); - LoadSystemModel(SystemModelID::kButtonMediumOpaque, "buttonMediumOpaque"); - LoadSystemModel(SystemModelID::kButtonBackTransparent, - "buttonBackTransparent"); - LoadSystemModel(SystemModelID::kButtonBackOpaque, "buttonBackOpaque"); - LoadSystemModel(SystemModelID::kButtonBackSmallTransparent, - "buttonBackSmallTransparent"); - LoadSystemModel(SystemModelID::kButtonBackSmallOpaque, - "buttonBackSmallOpaque"); - LoadSystemModel(SystemModelID::kButtonTabTransparent, "buttonTabTransparent"); - LoadSystemModel(SystemModelID::kButtonTabOpaque, "buttonTabOpaque"); - LoadSystemModel(SystemModelID::kButtonLargeTransparent, - "buttonLargeTransparent"); - LoadSystemModel(SystemModelID::kButtonLargeOpaque, "buttonLargeOpaque"); - LoadSystemModel(SystemModelID::kButtonLargerTransparent, - "buttonLargerTransparent"); - LoadSystemModel(SystemModelID::kButtonLargerOpaque, "buttonLargerOpaque"); - LoadSystemModel(SystemModelID::kButtonSquareTransparent, - "buttonSquareTransparent"); - LoadSystemModel(SystemModelID::kButtonSquareOpaque, "buttonSquareOpaque"); - LoadSystemModel(SystemModelID::kCheckTransparent, "checkTransparent"); - LoadSystemModel(SystemModelID::kScrollBarThumbTransparent, - "scrollBarThumbTransparent"); - LoadSystemModel(SystemModelID::kScrollBarThumbOpaque, "scrollBarThumbOpaque"); - LoadSystemModel(SystemModelID::kScrollBarThumbSimple, "scrollBarThumbSimple"); - LoadSystemModel(SystemModelID::kScrollBarThumbShortTransparent, - "scrollBarThumbShortTransparent"); - LoadSystemModel(SystemModelID::kScrollBarThumbShortOpaque, - "scrollBarThumbShortOpaque"); - LoadSystemModel(SystemModelID::kScrollBarThumbShortSimple, - "scrollBarThumbShortSimple"); - LoadSystemModel(SystemModelID::kScrollBarTroughTransparent, - "scrollBarTroughTransparent"); - LoadSystemModel(SystemModelID::kTextBoxTransparent, "textBoxTransparent"); - LoadSystemModel(SystemModelID::kImage1x1, "image1x1"); - LoadSystemModel(SystemModelID::kImage1x1FullScreen, "image1x1FullScreen"); - LoadSystemModel(SystemModelID::kImage2x1, "image2x1"); - LoadSystemModel(SystemModelID::kImage4x1, "image4x1"); - LoadSystemModel(SystemModelID::kImage16x1, "image16x1"); -#if BA_VR_BUILD - LoadSystemModel(SystemModelID::kImage1x1VRFullScreen, "image1x1VRFullScreen"); - LoadSystemModel(SystemModelID::kVROverlay, "vrOverlay"); - LoadSystemModel(SystemModelID::kVRFade, "vrFade"); -#endif // BA_VR_BUILD - LoadSystemModel(SystemModelID::kOverlayGuide, "overlayGuide"); - LoadSystemModel(SystemModelID::kWindowHSmallVMedTransparent, - "windowHSmallVMedTransparent"); - LoadSystemModel(SystemModelID::kWindowHSmallVMedOpaque, - "windowHSmallVMedOpaque"); - LoadSystemModel(SystemModelID::kWindowHSmallVSmallTransparent, - "windowHSmallVSmallTransparent"); - LoadSystemModel(SystemModelID::kWindowHSmallVSmallOpaque, - "windowHSmallVSmallOpaque"); - LoadSystemModel(SystemModelID::kSoftEdgeOutside, "softEdgeOutside"); - LoadSystemModel(SystemModelID::kSoftEdgeInside, "softEdgeInside"); - LoadSystemModel(SystemModelID::kBoxingGlove, "boxingGlove"); - LoadSystemModel(SystemModelID::kShield, "shield"); - LoadSystemModel(SystemModelID::kFlagPole, "flagPole"); - LoadSystemModel(SystemModelID::kFlagStand, "flagStand"); - LoadSystemModel(SystemModelID::kScorch, "scorch"); - LoadSystemModel(SystemModelID::kEyeBall, "eyeBall"); - LoadSystemModel(SystemModelID::kEyeBallIris, "eyeBallIris"); - LoadSystemModel(SystemModelID::kEyeLid, "eyeLid"); - LoadSystemModel(SystemModelID::kHairTuft1, "hairTuft1"); - LoadSystemModel(SystemModelID::kHairTuft1b, "hairTuft1b"); - LoadSystemModel(SystemModelID::kHairTuft2, "hairTuft2"); - LoadSystemModel(SystemModelID::kHairTuft3, "hairTuft3"); - LoadSystemModel(SystemModelID::kHairTuft4, "hairTuft4"); - LoadSystemModel(SystemModelID::kShrapnel1, "shrapnel1"); - LoadSystemModel(SystemModelID::kShrapnelSlime, "shrapnelSlime"); - LoadSystemModel(SystemModelID::kShrapnelBoard, "shrapnelBoard"); - LoadSystemModel(SystemModelID::kShockWave, "shockWave"); - LoadSystemModel(SystemModelID::kFlash, "flash"); - LoadSystemModel(SystemModelID::kCylinder, "cylinder"); - LoadSystemModel(SystemModelID::kArrowFront, "arrowFront"); - LoadSystemModel(SystemModelID::kArrowBack, "arrowBack"); - LoadSystemModel(SystemModelID::kActionButtonLeft, "actionButtonLeft"); - LoadSystemModel(SystemModelID::kActionButtonTop, "actionButtonTop"); - LoadSystemModel(SystemModelID::kActionButtonRight, "actionButtonRight"); - LoadSystemModel(SystemModelID::kActionButtonBottom, "actionButtonBottom"); - LoadSystemModel(SystemModelID::kBox, "box"); - LoadSystemModel(SystemModelID::kLocator, "locator"); - LoadSystemModel(SystemModelID::kLocatorBox, "locatorBox"); - LoadSystemModel(SystemModelID::kLocatorCircle, "locatorCircle"); - LoadSystemModel(SystemModelID::kLocatorCircleOutline, "locatorCircleOutline"); - LoadSystemModel(SystemModelID::kCrossOut, "crossOut"); - LoadSystemModel(SystemModelID::kWing, "wing"); - - // Hooray! - system_assets_loaded_ = true; -} - -void Assets::PrintLoadInfo() { - std::string s; - char buffer[256]; - int num = 1; - - // Need to lock lists while iterating over them. - AssetListLock lock; - s = "Assets load results: (all times in milliseconds):\n"; - snprintf(buffer, sizeof(buffer), " %-50s %10s %10s", "FILE", - "PRELOAD_TIME", "LOAD_TIME"); - s += buffer; - Log(LogLevel::kInfo, s); - millisecs_t total_preload_time = 0; - millisecs_t total_load_time = 0; - assert(asset_lists_locked_); - for (auto&& i : models_) { - millisecs_t preload_time = i.second->preload_time(); - millisecs_t load_time = i.second->load_time(); - total_preload_time += preload_time; - total_load_time += load_time; - snprintf(buffer, sizeof(buffer), "%-3d %-50s %10d %10d", num, - i.second->GetName().c_str(), - static_cast_check_fit(preload_time), - static_cast_check_fit(load_time)); - Log(LogLevel::kInfo, buffer); - num++; - } - assert(asset_lists_locked_); - for (auto&& i : collide_models_) { - millisecs_t preload_time = i.second->preload_time(); - millisecs_t load_time = i.second->load_time(); - total_preload_time += preload_time; - total_load_time += load_time; - snprintf(buffer, sizeof(buffer), "%-3d %-50s %10d %10d", num, - i.second->GetName().c_str(), - static_cast_check_fit(preload_time), - static_cast_check_fit(load_time)); - Log(LogLevel::kInfo, buffer); - num++; - } - assert(asset_lists_locked_); - for (auto&& i : sounds_) { - millisecs_t preload_time = i.second->preload_time(); - millisecs_t load_time = i.second->load_time(); - total_preload_time += preload_time; - total_load_time += load_time; - snprintf(buffer, sizeof(buffer), "%-3d %-50s %10d %10d", num, - i.second->GetName().c_str(), - static_cast_check_fit(preload_time), - static_cast_check_fit(load_time)); - Log(LogLevel::kInfo, buffer); - num++; - } - assert(asset_lists_locked_); - for (auto&& i : datas_) { - millisecs_t preload_time = i.second->preload_time(); - millisecs_t load_time = i.second->load_time(); - total_preload_time += preload_time; - total_load_time += load_time; - snprintf(buffer, sizeof(buffer), "%-3d %-50s %10d %10d", num, - i.second->GetName().c_str(), - static_cast_check_fit(preload_time), - static_cast_check_fit(load_time)); - Log(LogLevel::kInfo, buffer); - num++; - } - assert(asset_lists_locked_); - for (auto&& i : textures_) { - millisecs_t preload_time = i.second->preload_time(); - millisecs_t load_time = i.second->load_time(); - total_preload_time += preload_time; - total_load_time += load_time; - snprintf(buffer, sizeof(buffer), "%-3d %-50s %10d %10d", num, - i.second->file_name_full().c_str(), - static_cast_check_fit(preload_time), - static_cast_check_fit(load_time)); - Log(LogLevel::kInfo, buffer); - num++; - } - snprintf(buffer, sizeof(buffer), - "Total preload time (loading data from disk): %i\nTotal load time " - "(feeding data to OpenGL, etc): %i", - static_cast(total_preload_time), - static_cast(total_load_time)); - Log(LogLevel::kInfo, buffer); -} - -void Assets::MarkAllAssetsForLoad() { - assert(InLogicThread()); - - // Need to keep lists locked while iterating over them. - AssetListLock m_lock; - for (auto&& i : textures_) { - if (!i.second->preloaded()) { - AssetComponentData::LockGuard lock(i.second.get()); - have_pending_loads_[static_cast(AssetType::kTexture)] = true; - MarkComponentForLoad(i.second.get()); - } - } - for (auto&& i : text_textures_) { - if (!i.second->preloaded()) { - AssetComponentData::LockGuard lock(i.second.get()); - have_pending_loads_[static_cast(AssetType::kTexture)] = true; - MarkComponentForLoad(i.second.get()); - } - } - for (auto&& i : qr_textures_) { - if (!i.second->preloaded()) { - AssetComponentData::LockGuard lock(i.second.get()); - have_pending_loads_[static_cast(AssetType::kTexture)] = true; - MarkComponentForLoad(i.second.get()); - } - } - for (auto&& i : models_) { - if (!i.second->preloaded()) { - AssetComponentData::LockGuard lock(i.second.get()); - have_pending_loads_[static_cast(AssetType::kModel)] = true; - MarkComponentForLoad(i.second.get()); - } - } -} - -// Call this from the graphics thread to immediately unload all -// assets used by it. (for when GL context gets lost, etc). -void Assets::UnloadRendererBits(bool do_textures, bool do_models) { - assert(InGraphicsThread()); - // need to keep lists locked while iterating over them.. - AssetListLock m_lock; - if (do_textures) { - assert(asset_lists_locked_); - for (auto&& i : textures_) { - AssetComponentData::LockGuard lock(i.second.get()); - i.second->Unload(true); - } - for (auto&& i : text_textures_) { - AssetComponentData::LockGuard lock(i.second.get()); - i.second->Unload(true); - } - for (auto&& i : qr_textures_) { - AssetComponentData::LockGuard lock(i.second.get()); - i.second->Unload(true); - } - } - if (do_models) { - for (auto&& i : models_) { - AssetComponentData::LockGuard lock(i.second.get()); - i.second->Unload(true); - } - } -} - -auto Assets::GetModelData(const std::string& file_name) - -> Object::Ref { - return GetComponentData(file_name, &models_); -} - -auto Assets::GetSoundData(const std::string& file_name) - -> Object::Ref { - return GetComponentData(file_name, &sounds_); -} - -auto Assets::GetDataData(const std::string& file_name) - -> Object::Ref { - return GetComponentData(file_name, &datas_); -} - -auto Assets::GetCollideModelData(const std::string& file_name) - -> Object::Ref { - return GetComponentData(file_name, &collide_models_); -} - -template -auto Assets::GetComponentData( - const std::string& file_name, - std::unordered_map >* c_list) - -> Object::Ref { - assert(InLogicThread()); - assert(asset_lists_locked_); - auto i = c_list->find(file_name); - if (i != c_list->end()) { - return Object::Ref(i->second.get()); - } else { - auto d(Object::New(file_name)); - (*c_list)[file_name] = d; - { - AssetComponentData::LockGuard lock(d.get()); - have_pending_loads_[static_cast(d->GetAssetType())] = true; - MarkComponentForLoad(d.get()); - } - d->set_last_used_time(GetRealTime()); - return d; - } -} - -auto Assets::GetTextureData(TextPacker* packer) -> Object::Ref { - assert(InLogicThread()); - assert(asset_lists_locked_); - const std::string& hash(packer->hash()); - auto i = text_textures_.find(hash); - if (i != text_textures_.end()) { - return Object::Ref(i->second.get()); - } else { - auto d(Object::New(packer)); - text_textures_[hash] = d; - { - AssetComponentData::LockGuard lock(d.get()); - have_pending_loads_[static_cast(d->GetAssetType())] = true; - MarkComponentForLoad(d.get()); - } - d->set_last_used_time(GetRealTime()); - return d; - } -} - -auto Assets::GetTextureDataQRCode(const std::string& url) - -> Object::Ref { - assert(InLogicThread()); - assert(asset_lists_locked_); - auto i = qr_textures_.find(url); - if (i != qr_textures_.end()) { - return Object::Ref(i->second.get()); - } else { - auto d(Object::New(url)); - qr_textures_[url] = d; - { - AssetComponentData::LockGuard lock(d.get()); - have_pending_loads_[static_cast(d->GetAssetType())] = true; - MarkComponentForLoad(d.get()); - } - d->set_last_used_time(GetRealTime()); - return d; - } -} - -// Eww can't recycle GetComponent here since we need extra stuff (tex-type arg) -// ..should fix. -auto Assets::GetCubeMapTextureData(const std::string& file_name) - -> Object::Ref { - assert(InLogicThread()); - assert(asset_lists_locked_); - auto i = textures_.find(file_name); - if (i != textures_.end()) { - return Object::Ref(i->second.get()); - } else { - auto d(Object::New(file_name, TextureType::kCubeMap, - TextureMinQuality::kLow)); - textures_[file_name] = d; - { - AssetComponentData::LockGuard lock(d.get()); - have_pending_loads_[static_cast(d->GetAssetType())] = true; - MarkComponentForLoad(d.get()); - } - d->set_last_used_time(GetRealTime()); - return d; - } -} - -// Eww; can't recycle GetComponent here since we need extra stuff (quality -// settings, etc). Should fix. -auto Assets::GetTextureData(const std::string& file_name) - -> Object::Ref { - assert(InLogicThread()); - assert(asset_lists_locked_); - auto i = textures_.find(file_name); - if (i != textures_.end()) { - return Object::Ref(i->second.get()); - } else { - static std::set* quality_map_medium = nullptr; - static std::set* quality_map_high = nullptr; - static bool quality_maps_inited = false; - - // TEMP - we currently set min quality based on filename; - // in the future this will be stored with the texture package or whatnot - if (!quality_maps_inited) { - quality_maps_inited = true; - quality_map_medium = new std::set(); - quality_map_high = new std::set(); - const char* vals_med[] = { - "fontSmall0", "fontSmall1", "fontSmall2", "fontSmall3", "fontSmall4", - "fontSmall5", "fontSmall6", "fontSmall7", "fontExtras", nullptr}; - - const char* vals_high[] = {"frostyIcon", "jackIcon", "melIcon", - "santaIcon", "ninjaIcon", "neoSpazIcon", - "zoeIcon", "kronkIcon", "scrollWidgetGlow", - "glow", nullptr}; - - for (const char** val3 = vals_med; *val3 != nullptr; val3++) { - quality_map_medium->insert(*val3); - } - for (const char** val2 = vals_high; *val2 != nullptr; val2++) { - quality_map_high->insert(*val2); - } - } - - TextureMinQuality min_quality = TextureMinQuality::kLow; - if (quality_map_medium->find(file_name) != quality_map_medium->end()) { - min_quality = TextureMinQuality::kMedium; - } else if (quality_map_high->find(file_name) != quality_map_high->end()) { - min_quality = TextureMinQuality::kHigh; - } - - auto d(Object::New(file_name, TextureType::k2D, min_quality)); - textures_[file_name] = d; - { - AssetComponentData::LockGuard lock(d.get()); - have_pending_loads_[static_cast(d->GetAssetType())] = true; - MarkComponentForLoad(d.get()); - } - d->set_last_used_time(GetRealTime()); - return d; - } -} - -void Assets::MarkComponentForLoad(AssetComponentData* c) { - assert(InLogicThread()); - - assert(c->locked()); - - // *allocate* a reference as a standalone pointer so we can be - // sure this guy sticks around until it's been sent all the way - // through the preload/load cycle. (since other threads will be touching it) - // once it makes it back to us we can delete the ref (in - // ClearPendingLoadsDoneList) - - auto asset_ref_ptr = new Object::Ref(c); - g_assets_server->PushPendingPreload(asset_ref_ptr); -} - -#pragma clang diagnostic push -#pragma ide diagnostic ignored "UnreachableCode" -#pragma ide diagnostic ignored "ConstantFunctionResult" - -auto Assets::GetModelPendingLoadCount() -> int { - if (!have_pending_loads_[static_cast(AssetType::kModel)]) { - return 0; - } - AssetListLock lock; - int total = GetComponentPendingLoadCount(&models_, AssetType::kModel); - if (total == 0) { - // When fully loaded, stop counting. - have_pending_loads_[static_cast(AssetType::kModel)] = false; - } - return total; -} - -auto Assets::GetTexturePendingLoadCount() -> int { - if (!have_pending_loads_[static_cast(AssetType::kTexture)]) { - return 0; - } - AssetListLock lock; - int total = - (GetComponentPendingLoadCount(&textures_, AssetType::kTexture) - + GetComponentPendingLoadCount(&text_textures_, AssetType::kTexture) - + GetComponentPendingLoadCount(&qr_textures_, AssetType::kTexture)); - if (total == 0) { - // When fully loaded, stop counting. - have_pending_loads_[static_cast(AssetType::kTexture)] = false; - } - return total; -} - -auto Assets::GetSoundPendingLoadCount() -> int { - if (!have_pending_loads_[static_cast(AssetType::kSound)]) { - return 0; - } - AssetListLock lock; - int total = GetComponentPendingLoadCount(&sounds_, AssetType::kSound); - if (total == 0) { - // When fully loaded, stop counting. - have_pending_loads_[static_cast(AssetType::kSound)] = false; - } - return total; -} - -auto Assets::GetDataPendingLoadCount() -> int { - if (!have_pending_loads_[static_cast(AssetType::kData)]) { - return 0; - } - AssetListLock lock; - int total = GetComponentPendingLoadCount(&datas_, AssetType::kData); - if (total == 0) { - // When fully loaded, stop counting. - have_pending_loads_[static_cast(AssetType::kData)] = false; - } - return total; -} - -auto Assets::GetCollideModelPendingLoadCount() -> int { - if (!have_pending_loads_[static_cast(AssetType::kCollideModel)]) { - return 0; - } - AssetListLock lock; - int total = - GetComponentPendingLoadCount(&collide_models_, AssetType::kCollideModel); - if (total == 0) { - // When fully loaded, stop counting. - have_pending_loads_[static_cast(AssetType::kCollideModel)] = false; - } - return total; -} - -#pragma clang diagnostic pop - -auto Assets::GetGraphicalPendingLoadCount() -> int { - // Each of these calls lock the asset-lists so we don't. - return GetModelPendingLoadCount() + GetTexturePendingLoadCount(); -} - -auto Assets::GetPendingLoadCount() -> int { - // Each of these calls lock the asset-lists so we don't. - return GetModelPendingLoadCount() + GetTexturePendingLoadCount() - + GetDataPendingLoadCount() + GetSoundPendingLoadCount() - + GetCollideModelPendingLoadCount(); -} - -template -auto Assets::GetComponentPendingLoadCount( - std::unordered_map >* t_list, AssetType type) - -> int { - assert(InLogicThread()); - assert(asset_lists_locked_); - - int c = 0; - for (auto&& i : (*t_list)) { - if (i.second.exists()) { - if (i.second->TryLock()) { - AssetComponentData::LockGuard lock( - i.second.get(), AssetComponentData::LockGuard::Type::kInheritLock); - if (!i.second->loaded()) { - c++; - } - } else { - c++; - } - } - } - return c; -} - -// Runs the pending loads that need to run from the audio thread. -auto Assets::RunPendingAudioLoads() -> bool { - assert(InAudioThread()); - return RunPendingLoadList(&pending_loads_sounds_); -} - -// Runs the pending loads that need to run from the graphics thread. -auto Assets::RunPendingGraphicsLoads() -> bool { - assert(InGraphicsThread()); - return RunPendingLoadList(&pending_loads_graphics_); -} - -// Runs the pending loads that run in the main thread. Also clears the list of -// done loads. -auto Assets::RunPendingLoadsLogicThread() -> bool { - assert(InLogicThread()); - return RunPendingLoadList(&pending_loads_other_); -} - -template -auto Assets::RunPendingLoadList(std::vector*>* c_list) -> bool { - bool flush = false; - millisecs_t starttime = GetRealTime(); - - std::vector*> l; - std::vector*> l_unfinished; - std::vector*> l_finished; - { - std::scoped_lock lock(pending_load_list_mutex_); - - // If we're already out of time. - if (!flush && GetRealTime() - starttime > PENDING_LOAD_PROCESS_TIME) { - bool return_val = (!c_list->empty()); - return return_val; - } - - // Save time if there's nothing to load. - if (c_list->empty()) { - return false; - } - - // Pull the contents of c_list and set it to empty. - l.swap(*c_list); - } - - // Run loads on our list until either the list is empty or we're out of time - // (don't want to block here for very long...) - // We should also think about the fact that even if a load is quick here it - // may add work on the graphics thread/etc so maybe we should add other - // restrictions. - bool out_of_time = false; - if (!l.empty()) { - while (true) { - for (auto i = l.begin(); i != l.end(); i++) { - if (!out_of_time) { - (***i).Load(false); - - // If the load finished, pop it on our "done-loading" list.. otherwise - // keep it around. - l_finished.push_back(*i); // else l_unfinished.push_back(*i); - if (GetRealTime() - starttime > PENDING_LOAD_PROCESS_TIME && !flush) { - out_of_time = true; - } - } else { - // Already out of time - just save this one for later. - l_unfinished.push_back(*i); - } - } - l = l_unfinished; - l_unfinished.clear(); - if (l.empty() || out_of_time) { - break; - } - } - } - - // Now add unfinished ones back onto the original list and finished ones into - // the done list. - { - std::scoped_lock lock(pending_load_list_mutex_); - for (auto&& i : l) { - c_list->push_back(i); - } - for (auto&& i : l_finished) { - pending_loads_done_.push_back(i); - } - } - - // if we dumped anything on the pending loads done list, shake the logic - // thread to tell it to kill the reference.. - if (!l_finished.empty()) { - assert(g_logic); - g_logic->PushHavePendingLoadsDoneCall(); - } - return (!l.empty()); -} - -void Assets::Prune(int level) { - assert(InLogicThread()); - millisecs_t current_time = GetRealTime(); - - // need lists locked while accessing/modifying them - AssetListLock lock; - - // we can specify level for more aggressive pruning (during memory warnings - // and whatnot) - millisecs_t standard_asset_prune_time = STANDARD_ASSET_PRUNE_TIME; - millisecs_t text_texture_prune_time = TEXT_TEXTURE_PRUNE_TIME; - millisecs_t qr_texture_prune_time = QR_TEXTURE_PRUNE_TIME; - switch (level) { - case 1: - standard_asset_prune_time = 120000; // 2 min - text_texture_prune_time = 1000; // 1 sec - qr_texture_prune_time = 1000; // 1 sec - break; - case 2: - standard_asset_prune_time = 30000; // 30 sec - text_texture_prune_time = 1000; // 1 sec - qr_texture_prune_time = 1000; // 1 sec - break; - case 3: - standard_asset_prune_time = 5000; // 5 sec - text_texture_prune_time = 1000; // 1 sec - qr_texture_prune_time = 1000; // 1 sec - break; - default: - break; - } - - std::vector*> graphics_thread_unloads; - std::vector*> audio_thread_unloads; - -#if SHOW_PRUNING_INFO - assert(asset_lists_locked_); - int old_texture_count = textures_.size(); - int old_text_texture_count = text_textures_.size(); - int old_qr_texture_count = qr_textures_.size(); - int old_model_count = models_.size(); - int old_collide_model_count = collide_models_.size(); - int old_sound_count = sounds_.size(); -#endif // SHOW_PRUNING_INFO - - // prune textures.. - assert(asset_lists_locked_); - for (auto i = textures_.begin(); i != textures_.end();) { - TextureData* texture_data = i->second.get(); - // attempt to prune if there are no references remaining except our own and - // its been a while since it was used - if (current_time - texture_data->last_used_time() - > standard_asset_prune_time - && (texture_data->object_strong_ref_count() <= 1)) { - // if its preloaded/loaded we need to ask the graphics thread to unload it - // first - if (texture_data->preloaded()) { - // allocate a reference to keep this texture_data alive while the unload - // is happening - graphics_thread_unloads.push_back( - new Object::Ref(texture_data)); - auto i_next = i; - i_next++; - textures_.erase(i); - i = i_next; - } - } else { - i++; - } - } - - // prune text-textures more aggressively since we may generate lots of them - // FIXME - we may want to prune based on total number of these instead of - // time.. - assert(asset_lists_locked_); - for (auto i = text_textures_.begin(); i != text_textures_.end();) { - TextureData* texture_data = i->second.get(); - // attempt to prune if there are no references remaining except our own and - // its been a while since it was used - if (current_time - texture_data->last_used_time() > text_texture_prune_time - && (texture_data->object_strong_ref_count() <= 1)) { - // if its preloaded/loaded we need to ask the graphics thread to unload it - // first - if (texture_data->preloaded()) { - // allocate a reference to keep this texture_data alive while the unload - // is happening - graphics_thread_unloads.push_back( - new Object::Ref(texture_data)); - auto i_next = i; - i_next++; - text_textures_.erase(i); - i = i_next; - } - } else { - i++; - } - } - - // prune textures - assert(asset_lists_locked_); - for (auto i = qr_textures_.begin(); i != qr_textures_.end();) { - TextureData* texture_data = i->second.get(); - // attempt to prune if there are no references remaining except our own and - // its been a while since it was used - if (current_time - texture_data->last_used_time() > qr_texture_prune_time - && (texture_data->object_strong_ref_count() <= 1)) { - // if its preloaded/loaded we need to ask the graphics thread to unload it - // first - if (texture_data->preloaded()) { - // allocate a reference to keep this texture_data alive while the unload - // is happening - graphics_thread_unloads.push_back( - new Object::Ref(texture_data)); - auto i_next = i; - i_next++; - qr_textures_.erase(i); - i = i_next; - } - } else { - i++; - } - } - - // prune models.. - assert(asset_lists_locked_); - for (auto i = models_.begin(); i != models_.end();) { - ModelData* model_data = i->second.get(); - // attempt to prune if there are no references remaining except our own and - // its been a while since it was used - if (current_time - model_data->last_used_time() > standard_asset_prune_time - && (model_data->object_strong_ref_count() <= 1)) { - // if its preloaded/loaded we need to ask the graphics thread to unload it - // first - if (model_data->preloaded()) { - // allocate a reference to keep this model_data alive while the unload - // is happening - graphics_thread_unloads.push_back( - new Object::Ref(model_data)); - auto i_next = i; - i_next++; - models_.erase(i); - i = i_next; - } - } else { - i++; - } - } - - // Prune collide-models. - assert(asset_lists_locked_); - for (auto i = collide_models_.begin(); i != collide_models_.end();) { - CollideModelData* collide_model_data = i->second.get(); - // attempt to prune if there are no references remaining except our own and - // its been a while since it was used (unlike other assets we never prune - // these if there's still references to them - if (current_time - collide_model_data->last_used_time() - > standard_asset_prune_time - && (collide_model_data->object_strong_ref_count() <= 1)) { - // we can unload it immediately since that happens in the logic thread... - collide_model_data->Unload(); - auto i_next = i; - ++i_next; - collide_models_.erase(i); - i = i_next; - } else { - i++; - } - } - - // Prune sounds. - // (DISABLED FOR NOW - getting AL errors; need to better determine which - // sounds are still in active use by OpenAL and ensure references exist for - // them somewhere while that is the case - if (explicit_bool(false)) { - assert(asset_lists_locked_); - for (auto i = sounds_.begin(); i != sounds_.end();) { - SoundData* sound_data = i->second.get(); - // Attempt to prune if there are no references remaining except our own - // and its been a while since it was used. - if (current_time - sound_data->last_used_time() - > standard_asset_prune_time - && (sound_data->object_strong_ref_count() <= 1)) { - // If its preloaded/loaded we need to ask the graphics thread to unload - // it first. - if (sound_data->preloaded()) { - // Allocate a reference to keep this sound_data alive while the unload - // is happening. - audio_thread_unloads.push_back( - new Object::Ref(sound_data)); - auto i_next = i; - i_next++; - sounds_.erase(i); - i = i_next; - } - } else { - i++; - } - } - } - - if (!graphics_thread_unloads.empty()) { - g_graphics_server->PushComponentUnloadCall(graphics_thread_unloads); - } - if (!audio_thread_unloads.empty()) { - g_audio_server->PushComponentUnloadCall(audio_thread_unloads); - } - -#if SHOW_PRUNING_INFO - assert(asset_lists_locked_); - if (textures_.size() != old_texture_count) { - Log("Textures pruned from " + std::to_string(old_texture_count) + " to " - + std::to_string(textures_.size())); - } - if (text_textures_.size() != old_text_texture_count) { - Log("TextTextures pruned from " + std::to_string(old_text_texture_count) - + " to " + std::to_string(text_textures_.size())); - } - if (qr_textures_.size() != old_qr_texture_count) { - Log("QrTextures pruned from " + std::to_string(old_qr_texture_count) - + " to " + std::to_string(qr_textures_.size())); - } - if (models_.size() != old_model_count) { - Log("Models pruned from " + std::to_string(old_model_count) + " to " - + std::to_string(models_.size())); - } - if (collide_models_.size() != old_collide_model_count) { - Log("CollideModels pruned from " + std::to_string(old_collide_model_count) - + " to " + std::to_string(collide_models_.size())); - } - if (sounds_.size() != old_sound_count) { - Log("Sounds pruned from " + std::to_string(old_sound_count) + " to " - + std::to_string(sounds_.size())); - } -#endif // SHOW_PRUNING_INFO -} - -auto Assets::FindAssetFile(FileType type, const std::string& name) - -> std::string { - std::string file_out; - - // We don't protect package-path access so make sure its always from here. - assert(InLogicThread()); - - const char* ext = ""; - const char* prefix = ""; - - switch (type) { - case FileType::kSound: -#if BA_HEADLESS_BUILD - return "headless_dummy_path.sound"; -#else // BA_HEADLESS_BUILD - prefix = "audio/"; - ext = ".ogg"; - break; -#endif // BA_HEADLESS_BUILD - - case FileType::kModel: -#if BA_HEADLESS_BUILD - return "headless_dummy_path.model"; -#else // BA_HEADLESS_BUILD - prefix = "models/"; - ext = ".bob"; - break; -#endif // BA_HEADLESS_BUILD - - case FileType::kCollisionModel: - prefix = "models/"; - ext = ".cob"; - break; - - case FileType::kData: - prefix = "data/"; - ext = ".json"; - break; - - case FileType::kTexture: { -#if BA_HEADLESS_BUILD - if (strchr(name.c_str(), '#')) { - return "headless_dummy_path#.nop"; - } else { - return "headless_dummy_path.nop"; - } -#else // BA_HEADLESS_BUILD - - assert(g_graphics_server - && g_graphics_server->texture_compression_types_are_set()); - assert(g_graphics_server && g_graphics_server->texture_quality_set()); - prefix = "textures/"; - -#if BA_OSTYPE_ANDROID && !BA_ANDROID_DDS_BUILD - // On most android builds we go for .kvm, which contains etc2 and etc1. - ext = ".ktx"; -#elif BA_OSTYPE_IOS_TVOS - // On iOS we use pvr. - ext = ".pvr"; -#else - // all else defaults to dds - ext = ".dds"; -#endif -#endif // BA_HEADLESS_BUILD - break; - } - default: - break; - } - - const std::vector& asset_paths_used = asset_paths_; - - for (auto&& i : asset_paths_used) { - struct BA_STAT stats {}; - file_out = i + "/" + prefix + name + ext; // NOLINT - int result; - - // '#' denotes a cube map texture, which is actually 6 files. - if (strchr(file_out.c_str(), '#')) { - std::string tmp_name = file_out; - tmp_name.replace(tmp_name.find('#'), 1, "_+x"); - - // Just look for one of them i guess. - result = g_platform->Stat(tmp_name.c_str(), &stats); - } else { - result = g_platform->Stat(file_out.c_str(), &stats); - } - if (result == 0) { - if (S_ISREG(stats.st_mode)) { // NOLINT - return file_out; - } - } - } - - // We wanna fail gracefully for some types. - if (type == FileType::kSound && name != "blank") { - Log(LogLevel::kError, - "Unable to load audio: '" + name + "'; trying fallback..."); - return FindAssetFile(type, "blank"); - } else if (type == FileType::kTexture && name != "white") { - Log(LogLevel::kError, - "Unable to load texture: '" + name + "'; trying fallback..."); - return FindAssetFile(type, "white"); - } - - throw Exception("Can't find asset: \"" + name + "\""); - // return file_out; -} - -void Assets::AddPendingLoad(Object::Ref* c) { - switch ((**c).GetAssetType()) { - case AssetType::kTexture: - case AssetType::kModel: { - // Tell the graphics thread there's pending loads... - std::scoped_lock lock(pending_load_list_mutex_); - pending_loads_graphics_.push_back(c); - break; - } - case AssetType::kSound: { - // Tell the audio thread there's pending loads. - { - std::scoped_lock lock(pending_load_list_mutex_); - pending_loads_sounds_.push_back(c); - } - g_audio_server->PushHavePendingLoadsCall(); - break; - } - default: { - // Tell the logic thread there's pending loads. - { - std::scoped_lock lock(pending_load_list_mutex_); - pending_loads_other_.push_back(c); - } - g_logic->PushHavePendingLoadsCall(); - break; - } - } -} - -void Assets::ClearPendingLoadsDoneList() { - assert(InLogicThread()); - - std::scoped_lock lock(pending_load_list_mutex_); - - // Our explicitly-allocated reference pointer has made it back to us here in - // the logic thread. - // We can now kill the reference knowing that it's safe for this component - // to die at any time (anyone needing it to be alive now should be holding a - // reference themselves). - for (Object::Ref* i : pending_loads_done_) { - delete i; - } - pending_loads_done_.clear(); -} - -void Assets::AddPackage(const std::string& name, const std::string& path) { - // we don't protect package-path access so make sure its always from here.. - assert(InLogicThread()); -#if BA_DEBUG_BUILD - if (packages_.find(name) != packages_.end()) { - Log(LogLevel::kWarning, "adding duplicate package: '" + name + "'"); - } -#endif // BA_DEBUG_BUILD - packages_[name] = path; -} - -Assets::AssetListLock::AssetListLock() { - BA_DEBUG_FUNCTION_TIMER_BEGIN(); - g_assets->asset_lists_mutex_.lock(); - assert(!g_assets->asset_lists_locked_); - g_assets->asset_lists_locked_ = true; - BA_DEBUG_FUNCTION_TIMER_END_THREAD(20); -} - -Assets::AssetListLock::~AssetListLock() { - assert(g_assets->asset_lists_locked_); - g_assets->asset_lists_locked_ = false; - g_assets->asset_lists_mutex_.unlock(); -} - -} // namespace ballistica diff --git a/src/ballistica/assets/assets.h b/src/ballistica/assets/assets.h deleted file mode 100644 index a23bcefe..00000000 --- a/src/ballistica/assets/assets.h +++ /dev/null @@ -1,210 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_ASSETS_ASSETS_H_ -#define BALLISTICA_ASSETS_ASSETS_H_ - -#include -#include -#include -#include - -#include "ballistica/core/object.h" - -namespace ballistica { - -/// Global assets wrangling class. -class Assets { - public: - Assets(); - - /// Handy function to try to return an asset from a std::unordered_map - /// of weak-refs, loading/adding it if need be. - template - static auto GetAsset( - std::unordered_map >* list, - const std::string& name, Scene* scene) -> Object::Ref { - assert(InLogicThread()); - assert(list); - auto i = list->find(name); - - // If we have an entry pointing to a live component, just return a new ref - // to it. - if (i != list->end() && i->second.exists()) { - return Object::Ref(i->second.get()); - } else { - // Otherwise make a new one, pop a weak-ref on our list, and return a - // strong-ref to it. - auto t(Object::New(name, scene)); - (*list)[name] = t; - return t; - } - } - - auto AddPackage(const std::string& name, const std::string& path) -> void; - auto Prune(int level = 0) -> void; - - /// Finish loading any assets that have been preloaded but still need to be - /// loaded by the proper thread. - auto RunPendingLoadsLogicThread() -> bool; - - /// Return true if audio loads remain to be done. - auto RunPendingAudioLoads() -> bool; - - /// Return true if graphics loads remain to be done. - auto RunPendingGraphicsLoads() -> bool; - auto ClearPendingLoadsDoneList() -> void; - template - auto RunPendingLoadList(std::vector*>* cList) -> bool; - - /// This function takes a newly allocated pointer which - /// is deleted once the load is completed. - auto AddPendingLoad(Object::Ref* c) -> void; - enum class FileType { kModel, kCollisionModel, kTexture, kSound, kData }; - auto FindAssetFile(FileType fileType, const std::string& file_in) - -> std::string; - - /// Unload renderer-specific bits only (gl display lists, etc) - used when - /// recreating/adjusting the renderer. - auto UnloadRendererBits(bool textures, bool models) -> void; - - /// Should be called from the logic thread after UnloadRendererBits(); - /// kicks off bg loads for all existing unloaded assets. - auto MarkAllAssetsForLoad() -> void; - auto PrintLoadInfo() -> void; - - auto GetModelPendingLoadCount() -> int; - auto GetTexturePendingLoadCount() -> int; - auto GetSoundPendingLoadCount() -> int; - auto GetDataPendingLoadCount() -> int; - auto GetCollideModelPendingLoadCount() -> int; - - /// Return the total number of graphics related pending loads. - auto GetGraphicalPendingLoadCount() -> int; - - /// Return the total number of pending loads. - auto GetPendingLoadCount() -> int; - - /// You must hold one of these locks while calling Get*Data() below. - class AssetListLock { - public: - AssetListLock(); - ~AssetListLock(); - }; - - /// Load/cache assets (make sure you hold a AssetListLock). - auto GetTextureData(const std::string& file_name) -> Object::Ref; - auto GetTextureData(TextPacker* packer) -> Object::Ref; - auto GetTextureDataQRCode(const std::string& file_name) - -> Object::Ref; - auto GetCubeMapTextureData(const std::string& file_name) - -> Object::Ref; - auto GetModelData(const std::string& file_name) -> Object::Ref; - auto GetSoundData(const std::string& file_name) -> Object::Ref; - auto GetDataData(const std::string& file_name) -> Object::Ref; - auto GetCollideModelData(const std::string& file_name) - -> Object::Ref; - - // Get system assets. - auto GetTexture(SystemTextureID id) -> TextureData* { - BA_PRECONDITION_FATAL(system_assets_loaded_); // Revert to assert later. - assert(InLogicThread()); - assert(static_cast(id) < system_textures_.size()); - return system_textures_[static_cast(id)].get(); - } - auto GetCubeMapTexture(SystemCubeMapTextureID id) -> TextureData* { - BA_PRECONDITION_FATAL(system_assets_loaded_); // Revert to assert later. - assert(InLogicThread()); - assert(static_cast(id) < system_cube_map_textures_.size()); - return system_cube_map_textures_[static_cast(id)].get(); - } - auto GetSound(SystemSoundID id) -> SoundData* { - BA_PRECONDITION_FATAL(system_assets_loaded_); // Revert to assert later. - assert(InLogicThread()); - assert(static_cast(id) < system_sounds_.size()); - return system_sounds_[static_cast(id)].get(); - } - auto GetModel(SystemModelID id) -> ModelData* { - BA_PRECONDITION_FATAL(system_assets_loaded_); // Revert to assert later. - assert(InLogicThread()); - assert(static_cast(id) < system_models_.size()); - return system_models_[static_cast(id)].get(); - } - - /// Load up hard-coded assets for interface, etc. - auto LoadSystemAssets() -> void; - - auto total_model_count() const -> uint32_t { - return static_cast(models_.size()); - } - auto total_texture_count() const -> uint32_t { - return static_cast(textures_.size() + text_textures_.size() - + qr_textures_.size()); - } - auto total_sound_count() const -> uint32_t { - return static_cast(sounds_.size()); - } - auto total_collide_model_count() const -> uint32_t { - return static_cast(collide_models_.size()); - } - - private: - static auto MarkComponentForLoad(AssetComponentData* c) -> void; - auto LoadSystemTexture(SystemTextureID id, const char* name) -> void; - auto LoadSystemCubeMapTexture(SystemCubeMapTextureID id, const char* name) - -> void; - auto LoadSystemSound(SystemSoundID id, const char* name) -> void; - auto LoadSystemData(SystemDataID id, const char* name) -> void; - auto LoadSystemModel(SystemModelID id, const char* name) -> void; - - template - auto GetComponentPendingLoadCount( - std::unordered_map >* t_list, AssetType type) - -> int; - - template - auto GetComponentData( - const std::string& file_name, - std::unordered_map >* c_list) - -> Object::Ref; - - std::vector asset_paths_; - bool have_pending_loads_[static_cast(AssetType::kLast)]{}; - std::unordered_map packages_; - - // For use by AssetListLock; don't manually acquire - std::mutex asset_lists_mutex_; - - // Will be true while a AssetListLock exists. Good to debug-verify this - // during any asset list access. - bool asset_lists_locked_{}; - - // 'hard-wired' internal assets - bool system_assets_loaded_{}; - std::vector > system_textures_; - std::vector > system_cube_map_textures_; - std::vector > system_sounds_; - std::vector > system_datas_; - std::vector > system_models_; - - // All existing assets by filename (including internal). - std::unordered_map > textures_; - std::unordered_map > text_textures_; - std::unordered_map > qr_textures_; - std::unordered_map > models_; - std::unordered_map > sounds_; - std::unordered_map > datas_; - std::unordered_map > - collide_models_; - - // Components that have been preloaded but need to be loaded. - std::mutex pending_load_list_mutex_; - std::vector*> pending_loads_graphics_; - std::vector*> pending_loads_sounds_; - std::vector*> pending_loads_datas_; - std::vector*> pending_loads_other_; - std::vector*> pending_loads_done_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_ASSETS_ASSETS_H_ diff --git a/src/ballistica/assets/assets_server.h b/src/ballistica/assets/assets_server.h deleted file mode 100644 index e9be2e6a..00000000 --- a/src/ballistica/assets/assets_server.h +++ /dev/null @@ -1,42 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_ASSETS_ASSETS_SERVER_H_ -#define BALLISTICA_ASSETS_ASSETS_SERVER_H_ - -#include -#include - -#include "ballistica/core/object.h" - -namespace ballistica { - -class AssetsServer { - public: - AssetsServer(); - auto OnAppStart() -> void; - auto PushBeginWriteReplayCall() -> void; - auto PushEndWriteReplayCall() -> void; - auto PushAddMessageToReplayCall(const std::vector& data) -> void; - auto PushPendingPreload(Object::Ref* asset_ref_ptr) - -> void; - auto thread() const -> Thread* { return thread_; } - - private: - auto OnAppStartInThread() -> void; - void Process(); - void WriteReplayMessages(); - Thread* thread_{}; - FILE* replay_out_file_{}; - size_t replay_bytes_written_{}; - bool writing_replay_{}; - bool replays_broken_{}; - std::list > replay_messages_; - size_t replay_message_bytes_{}; - Timer* process_timer_{}; - std::vector*> pending_preloads_; - std::vector*> pending_preloads_audio_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_ASSETS_ASSETS_SERVER_H_ diff --git a/src/ballistica/assets/component/asset_component.cc b/src/ballistica/assets/component/asset_component.cc deleted file mode 100644 index 9e7ace5e..00000000 --- a/src/ballistica/assets/component/asset_component.cc +++ /dev/null @@ -1,34 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/assets/component/asset_component.h" - -#include "ballistica/python/python_sys.h" -#include "ballistica/scene/scene.h" - -namespace ballistica { - -AssetComponent::AssetComponent(std::string name, Scene* scene) - : name_(std::move(name)), scene_(scene) {} - -auto AssetComponent::GetPyRef(bool new_ref) -> PyObject* { - if (!py_object_) { - // if we have no python object, create it - py_object_ = CreatePyObject(); - assert(py_object_ != nullptr); - } - if (new_ref) { - Py_INCREF(py_object_); - } - return py_object_; -} - -auto AssetComponent::GetObjectDescription() const -> std::string { - return ""; -} - -void AssetComponent::ClearPyObject() { - assert(py_object_ != nullptr); - py_object_ = nullptr; -} - -} // namespace ballistica diff --git a/src/ballistica/assets/component/asset_component.h b/src/ballistica/assets/component/asset_component.h deleted file mode 100644 index 335f39c0..00000000 --- a/src/ballistica/assets/component/asset_component.h +++ /dev/null @@ -1,63 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_ASSETS_COMPONENT_ASSET_COMPONENT_H_ -#define BALLISTICA_ASSETS_COMPONENT_ASSET_COMPONENT_H_ - -#include - -#include "ballistica/core/context.h" -#include "ballistica/core/object.h" - -namespace ballistica { - -class AssetComponent : public Object { - public: - AssetComponent(std::string name, Scene* scene); - auto name() const -> std::string { return name_; } - - // Returns true if this texture was created in the UI context. - // UI stuff should check this before accepting a texture. - auto IsFromUIContext() const -> bool { - return (context_.GetUIContext() != nullptr); - } - auto has_py_object() const -> bool { return (py_object_ != nullptr); } - auto NewPyRef() -> PyObject* { return GetPyRef(true); } - auto BorrowPyRef() -> PyObject* { return GetPyRef(false); } - auto GetObjectDescription() const -> std::string override; - auto scene() const -> Scene* { return scene_.get(); } - - // Called by python wrapper objs when they are dying. - void ClearPyObject(); - - auto stream_id() const -> int64_t { return stream_id_; } - void set_stream_id(int64_t val) { - assert(stream_id_ == -1); - stream_id_ = val; - } - - void clear_stream_id() { - assert(stream_id_ != -1); - stream_id_ = -1; - } - - protected: - virtual auto GetAssetComponentTypeName() const -> std::string = 0; - - // Create a python representation of this object. - virtual auto CreatePyObject() -> PyObject* = 0; - - private: - int64_t stream_id_{-1}; - Object::WeakRef scene_; - PyObject* py_object_{}; - - // Return a python reference to the object, (creating python obj if needed). - auto GetPyRef(bool new_ref = true) -> PyObject*; - std::string name_; - Context context_; - friend class ClientSession; -}; - -} // namespace ballistica - -#endif // BALLISTICA_ASSETS_COMPONENT_ASSET_COMPONENT_H_ diff --git a/src/ballistica/assets/component/collide_model.cc b/src/ballistica/assets/component/collide_model.cc deleted file mode 100644 index 99435d4f..00000000 --- a/src/ballistica/assets/component/collide_model.cc +++ /dev/null @@ -1,44 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/assets/component/collide_model.h" - -#include "ballistica/python/class/python_class_collide_model.h" -#include "ballistica/scene/scene.h" -#include "ballistica/scene/scene_stream.h" - -namespace ballistica { - -CollideModel::CollideModel(const std::string& name, Scene* scene) - : AssetComponent(name, scene), dead_(false) { - assert(InLogicThread()); - if (scene) { - if (SceneStream* os = scene->GetSceneStream()) { - os->AddCollideModel(this); - } - } - { - Assets::AssetListLock lock; - collide_model_data_ = g_assets->GetCollideModelData(name); - } - assert(collide_model_data_.exists()); -} - -CollideModel::~CollideModel() { MarkDead(); } - -void CollideModel::MarkDead() { - if (dead_) { - return; - } - if (Scene* s = scene()) { - if (SceneStream* os = s->GetSceneStream()) { - os->RemoveCollideModel(this); - } - } - dead_ = true; -} - -auto CollideModel::CreatePyObject() -> PyObject* { - return PythonClassCollideModel::Create(this); -} - -} // namespace ballistica diff --git a/src/ballistica/assets/component/collide_model.h b/src/ballistica/assets/component/collide_model.h deleted file mode 100644 index 09671fa3..00000000 --- a/src/ballistica/assets/component/collide_model.h +++ /dev/null @@ -1,41 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_ASSETS_COMPONENT_COLLIDE_MODEL_H_ -#define BALLISTICA_ASSETS_COMPONENT_COLLIDE_MODEL_H_ - -#include - -#include "ballistica/assets/assets.h" -#include "ballistica/assets/component/asset_component.h" -#include "ballistica/assets/data/collide_model_data.h" - -namespace ballistica { - -// user-facing collide_model class -class CollideModel : public AssetComponent { - public: - CollideModel(const std::string& name, Scene* scene); - ~CollideModel() override; - - // return the CollideModelData currently associated with this collide_model - // note that a collide_model's data can change over time as different - // versions are spooled in/out/etc - auto collide_model_data() const -> CollideModelData* { - return collide_model_data_.get(); - } - auto GetAssetComponentTypeName() const -> std::string override { - return "CollideModel"; - } - void MarkDead(); - - protected: - auto CreatePyObject() -> PyObject* override; - - private: - bool dead_; - Object::Ref collide_model_data_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_ASSETS_COMPONENT_COLLIDE_MODEL_H_ diff --git a/src/ballistica/assets/component/cube_map_texture.cc b/src/ballistica/assets/component/cube_map_texture.cc deleted file mode 100644 index 6af4d80b..00000000 --- a/src/ballistica/assets/component/cube_map_texture.cc +++ /dev/null @@ -1,21 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/assets/component/cube_map_texture.h" - -#include "ballistica/assets/assets.h" - -namespace ballistica { - -CubeMapTexture::CubeMapTexture(const std::string& name, Scene* scene) - : AssetComponent(name, scene) { - assert(InLogicThread()); - - // cant currently add these to scenes so nothing to do here.. - { - Assets::AssetListLock lock; - texture_data_ = g_assets->GetCubeMapTextureData(name); - } - assert(texture_data_.exists()); -} - -} // namespace ballistica diff --git a/src/ballistica/assets/component/cube_map_texture.h b/src/ballistica/assets/component/cube_map_texture.h deleted file mode 100644 index db5ed159..00000000 --- a/src/ballistica/assets/component/cube_map_texture.h +++ /dev/null @@ -1,32 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_ASSETS_COMPONENT_CUBE_MAP_TEXTURE_H_ -#define BALLISTICA_ASSETS_COMPONENT_CUBE_MAP_TEXTURE_H_ - -#include - -#include "ballistica/assets/component/asset_component.h" -#include "ballistica/assets/data/texture_data.h" - -namespace ballistica { - -// user-facing texture class -class CubeMapTexture : public AssetComponent { - public: - CubeMapTexture(const std::string& name, Scene* s); - - // return the TextureData currently associated with this texture - // note that a texture's data can change over time as different - // versions are spooled in/out/etc - auto GetTextureData() const -> TextureData* { return texture_data_.get(); } - auto GetAssetComponentTypeName() const -> std::string override { - return "CubeMapTexture"; - } - - private: - Object::Ref texture_data_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_ASSETS_COMPONENT_CUBE_MAP_TEXTURE_H_ diff --git a/src/ballistica/assets/component/data.cc b/src/ballistica/assets/component/data.cc deleted file mode 100644 index 17b23702..00000000 --- a/src/ballistica/assets/component/data.cc +++ /dev/null @@ -1,45 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/assets/component/data.h" - -#include "ballistica/python/class/python_class_data.h" -#include "ballistica/scene/scene.h" -#include "ballistica/scene/scene_stream.h" - -namespace ballistica { - -Data::Data(const std::string& name, Scene* scene) - : AssetComponent(name, scene), dead_(false) { - assert(InLogicThread()); - - if (scene) { - if (SceneStream* os = scene->GetSceneStream()) { - os->AddData(this); - } - } - { - Assets::AssetListLock lock; - data_data_ = g_assets->GetDataData(name); - } - assert(data_data_.exists()); -} - -Data::~Data() { MarkDead(); } - -void Data::MarkDead() { - if (dead_) { - return; - } - if (Scene* s = scene()) { - if (SceneStream* os = s->GetSceneStream()) { - os->RemoveData(this); - } - } - dead_ = true; -} - -auto Data::CreatePyObject() -> PyObject* { - return PythonClassData::Create(this); -} - -} // namespace ballistica diff --git a/src/ballistica/assets/component/data.h b/src/ballistica/assets/component/data.h deleted file mode 100644 index fe3e917d..00000000 --- a/src/ballistica/assets/component/data.h +++ /dev/null @@ -1,43 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_ASSETS_COMPONENT_DATA_H_ -#define BALLISTICA_ASSETS_COMPONENT_DATA_H_ - -#include -#include - -#include "ballistica/assets/assets.h" -#include "ballistica/assets/component/asset_component.h" -#include "ballistica/assets/data/asset_component_data.h" -#include "ballistica/assets/data/data_data.h" -#include "ballistica/ballistica.h" -#include "ballistica/core/object.h" - -namespace ballistica { - -// user-facing data class -class Data : public AssetComponent { - public: - Data(const std::string& name, Scene* scene); - ~Data() override; - - // return the DataData currently associated with this data - // note that a data's data can change over time as different - // versions are spooled in/out/etc. - auto data_data() const -> DataData* { return data_data_.get(); } - auto GetAssetComponentTypeName() const -> std::string override { - return "Data"; - } - void MarkDead(); - - protected: - auto CreatePyObject() -> PyObject* override; - - private: - bool dead_; - Object::Ref data_data_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_ASSETS_COMPONENT_DATA_H_ diff --git a/src/ballistica/assets/component/model.cc b/src/ballistica/assets/component/model.cc deleted file mode 100644 index 196ebbf6..00000000 --- a/src/ballistica/assets/component/model.cc +++ /dev/null @@ -1,45 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/assets/component/model.h" - -#include "ballistica/python/class/python_class_model.h" -#include "ballistica/scene/scene.h" -#include "ballistica/scene/scene_stream.h" - -namespace ballistica { - -Model::Model(const std::string& name, Scene* scene) - : AssetComponent(name, scene), dead_(false) { - assert(InLogicThread()); - - if (scene) { - if (SceneStream* os = scene->GetSceneStream()) { - os->AddModel(this); - } - } - { - Assets::AssetListLock lock; - model_data_ = g_assets->GetModelData(name); - } - assert(model_data_.exists()); -} - -Model::~Model() { MarkDead(); } - -void Model::MarkDead() { - if (dead_) { - return; - } - if (Scene* s = scene()) { - if (SceneStream* os = s->GetSceneStream()) { - os->RemoveModel(this); - } - } - dead_ = true; -} - -auto Model::CreatePyObject() -> PyObject* { - return PythonClassModel::Create(this); -} - -} // namespace ballistica diff --git a/src/ballistica/assets/component/model.h b/src/ballistica/assets/component/model.h deleted file mode 100644 index 239813a0..00000000 --- a/src/ballistica/assets/component/model.h +++ /dev/null @@ -1,44 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_ASSETS_COMPONENT_MODEL_H_ -#define BALLISTICA_ASSETS_COMPONENT_MODEL_H_ - -#include -#include - -#include "ballistica/assets/assets.h" -#include "ballistica/assets/component/asset_component.h" -#include "ballistica/assets/data/asset_component_data.h" -#include "ballistica/assets/data/model_data.h" -#include "ballistica/assets/data/model_renderer_data.h" -#include "ballistica/ballistica.h" -#include "ballistica/core/object.h" - -namespace ballistica { - -// user-facing model class -class Model : public AssetComponent { - public: - Model(const std::string& name, Scene* scene); - ~Model() override; - - // return the ModelData currently associated with this model - // note that a model's data can change over time as different - // versions are spooled in/out/etc - auto model_data() const -> ModelData* { return model_data_.get(); } - auto GetAssetComponentTypeName() const -> std::string override { - return "Model"; - } - void MarkDead(); - - protected: - auto CreatePyObject() -> PyObject* override; - - private: - bool dead_; - Object::Ref model_data_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_ASSETS_COMPONENT_MODEL_H_ diff --git a/src/ballistica/assets/component/sound.cc b/src/ballistica/assets/component/sound.cc deleted file mode 100644 index 9c76aba2..00000000 --- a/src/ballistica/assets/component/sound.cc +++ /dev/null @@ -1,44 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/assets/component/sound.h" - -#include "ballistica/assets/assets.h" -#include "ballistica/assets/data/sound_data.h" -#include "ballistica/python/class/python_class_sound.h" -#include "ballistica/scene/scene.h" -#include "ballistica/scene/scene_stream.h" - -namespace ballistica { - -Sound::Sound(const std::string& name, Scene* scene) - : AssetComponent(name, scene) { - assert(InLogicThread()); - if (scene) { - if (SceneStream* os = scene->GetSceneStream()) { - os->AddSound(this); - } - } - { - Assets::AssetListLock lock; - sound_data_ = g_assets->GetSoundData(name); - } - assert(sound_data_.exists()); -} - -Sound::~Sound() { MarkDead(); } - -void Sound::MarkDead() { - if (dead_) return; - if (Scene* s = scene()) { - if (SceneStream* os = s->GetSceneStream()) { - os->RemoveSound(this); - } - } - dead_ = true; -} - -auto Sound::CreatePyObject() -> PyObject* { - return PythonClassSound::Create(this); -} - -} // namespace ballistica diff --git a/src/ballistica/assets/component/sound.h b/src/ballistica/assets/component/sound.h deleted file mode 100644 index ff0295b7..00000000 --- a/src/ballistica/assets/component/sound.h +++ /dev/null @@ -1,37 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_ASSETS_COMPONENT_SOUND_H_ -#define BALLISTICA_ASSETS_COMPONENT_SOUND_H_ - -#include -#include - -#include "ballistica/assets/component/asset_component.h" - -namespace ballistica { - -class Sound : public AssetComponent { - public: - Sound(const std::string& name, Scene* scene); - ~Sound() override; - - // Return the SoundData currently associated with this sound. - // Note that a sound's data can change over time as different - // versions are spooled in/out/etc. - auto GetSoundData() const -> SoundData* { return sound_data_.get(); } - auto GetAssetComponentTypeName() const -> std::string override { - return "Sound"; - } - void MarkDead(); - - protected: - auto CreatePyObject() -> PyObject* override; - - private: - bool dead_{}; - Object::Ref sound_data_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_ASSETS_COMPONENT_SOUND_H_ diff --git a/src/ballistica/assets/component/texture.cc b/src/ballistica/assets/component/texture.cc deleted file mode 100644 index 52994fdb..00000000 --- a/src/ballistica/assets/component/texture.cc +++ /dev/null @@ -1,57 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/assets/component/texture.h" - -#include "ballistica/graphics/renderer.h" -#include "ballistica/python/class/python_class_texture.h" -#include "ballistica/scene/scene.h" -#include "ballistica/scene/scene_stream.h" - -namespace ballistica { - -Texture::Texture(const std::string& name, Scene* scene) - : AssetComponent(name, scene), dead_(false) { - assert(InLogicThread()); - - // Add to the provided scene to get a numeric ID. - if (scene) { - if (SceneStream* os = scene->GetSceneStream()) { - os->AddTexture(this); - } - } - { - Assets::AssetListLock lock; - texture_data_ = g_assets->GetTextureData(name); - } - assert(texture_data_.exists()); -} - -// qrcode version -Texture::Texture(const std::string& qr_url) : AssetComponent(qr_url, nullptr) { - assert(InLogicThread()); - { - Assets::AssetListLock lock; - texture_data_ = g_assets->GetTextureDataQRCode(qr_url); - } - assert(texture_data_.exists()); -} - -Texture::~Texture() { MarkDead(); } - -void Texture::MarkDead() { - if (dead_) { - return; - } - if (Scene* s = scene()) { - if (SceneStream* os = s->GetSceneStream()) { - os->RemoveTexture(this); - } - } - dead_ = true; -} - -auto Texture::CreatePyObject() -> PyObject* { - return PythonClassTexture::Create(this); -} - -} // namespace ballistica diff --git a/src/ballistica/assets/component/texture.h b/src/ballistica/assets/component/texture.h deleted file mode 100644 index 892767ee..00000000 --- a/src/ballistica/assets/component/texture.h +++ /dev/null @@ -1,39 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_ASSETS_COMPONENT_TEXTURE_H_ -#define BALLISTICA_ASSETS_COMPONENT_TEXTURE_H_ - -#include - -#include "ballistica/assets/component/asset_component.h" -#include "ballistica/assets/data/texture_data.h" - -namespace ballistica { - -// User-facing texture class. -class Texture : public AssetComponent { - public: - Texture(const std::string& name, Scene* scene); - explicit Texture(const std::string& qr_url); - ~Texture() override; - - // Return the TextureData currently associated with this texture. - // Note that a texture's data can change over time as different - // versions are spooled in/out/etc. - auto texture_data() const -> TextureData* { return texture_data_.get(); } - auto GetAssetComponentTypeName() const -> std::string override { - return "Texture"; - } - void MarkDead(); - - protected: - auto CreatePyObject() -> PyObject* override; - - private: - bool dead_ = false; - Object::Ref texture_data_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_ASSETS_COMPONENT_TEXTURE_H_ diff --git a/src/ballistica/assets/data/collide_model_data.h b/src/ballistica/assets/data/collide_model_data.h deleted file mode 100644 index 0dba9bfd..00000000 --- a/src/ballistica/assets/data/collide_model_data.h +++ /dev/null @@ -1,47 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_ASSETS_DATA_COLLIDE_MODEL_DATA_H_ -#define BALLISTICA_ASSETS_DATA_COLLIDE_MODEL_DATA_H_ - -#include -#include - -#include "ballistica/assets/data/asset_component_data.h" -#include "ode/ode.h" - -namespace ballistica { - -// Loadable model for collision detection. -class CollideModelData : public AssetComponentData { - public: - CollideModelData() = default; - explicit CollideModelData(const std::string& file_name_in); - void DoPreload() override; - void DoLoad() override; - void DoUnload() override; - auto GetAssetType() const -> AssetType override { - return AssetType::kCollideModel; - } - auto GetName() const -> std::string override { - if (!file_name_full_.empty()) { - return file_name_full_; - } else { - return "invalid CollideModel"; - } - } - auto GetMeshData() -> dTriMeshDataID; - auto GetBGMeshData() -> dTriMeshDataID; - - private: - std::string file_name_; - std::string file_name_full_; - std::vector vertices_; - std::vector indices_; - std::vector normals_; - dTriMeshDataID tri_mesh_data_{}; - dTriMeshDataID tri_mesh_data_bg_{}; -}; - -} // namespace ballistica - -#endif // BALLISTICA_ASSETS_DATA_COLLIDE_MODEL_DATA_H_ diff --git a/src/ballistica/assets/data/data_data.h b/src/ballistica/assets/data/data_data.h deleted file mode 100644 index 57baa42b..00000000 --- a/src/ballistica/assets/data/data_data.h +++ /dev/null @@ -1,47 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_ASSETS_DATA_DATA_DATA_H_ -#define BALLISTICA_ASSETS_DATA_DATA_DATA_H_ - -#include - -#include "ballistica/assets/data/asset_component_data.h" -#include "ballistica/python/python_ref.h" - -namespace ballistica { - -class DataData : public AssetComponentData { - public: - DataData() = default; - explicit DataData(const std::string& file_name_in); - - void DoPreload() override; - void DoLoad() override; - void DoUnload() override; - - auto GetAssetType() const -> AssetType override { return AssetType::kData; } - auto GetName() const -> std::string override { - if (!file_name_full_.empty()) { - return file_name_full_; - } else { - return "invalid data"; - } - } - auto object() -> const PythonRef& { - assert(InLogicThread()); - assert(loaded()); - return object_; - } - auto file_name() const -> const std::string& { return file_name_; } - auto file_name_full() const -> const std::string& { return file_name_full_; } - - private: - PythonRef object_; - std::string file_name_; - std::string file_name_full_; - std::string raw_input_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_ASSETS_DATA_DATA_DATA_H_ diff --git a/src/ballistica/assets/data/model_renderer_data.h b/src/ballistica/assets/data/model_renderer_data.h deleted file mode 100644 index 4128e465..00000000 --- a/src/ballistica/assets/data/model_renderer_data.h +++ /dev/null @@ -1,21 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_ASSETS_DATA_MODEL_RENDERER_DATA_H_ -#define BALLISTICA_ASSETS_DATA_MODEL_RENDERER_DATA_H_ - -#include "ballistica/core/object.h" - -namespace ballistica { - -// Renderer-specific data (gl display list, etc) -// this is provided by the renderer -class ModelRendererData : public Object { - public: - auto GetDefaultOwnerThread() const -> ThreadTag override { - return ThreadTag::kMain; - } -}; - -} // namespace ballistica - -#endif // BALLISTICA_ASSETS_DATA_MODEL_RENDERER_DATA_H_ diff --git a/src/ballistica/assets/data/sound_data.h b/src/ballistica/assets/data/sound_data.h deleted file mode 100644 index d24172de..00000000 --- a/src/ballistica/assets/data/sound_data.h +++ /dev/null @@ -1,58 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_ASSETS_DATA_SOUND_DATA_H_ -#define BALLISTICA_ASSETS_DATA_SOUND_DATA_H_ - -#include -#include - -#include "ballistica/assets/data/asset_component_data.h" -#include "ballistica/audio/al_sys.h" - -namespace ballistica { - -class SoundData : public AssetComponentData { - public: - SoundData() = default; - explicit SoundData(const std::string& file_name_in); - void DoPreload() override; - void DoLoad() override; - - // FIXME: Should make sure the sound_data isn't in use before unloading it. - void DoUnload() override; - auto GetAssetType() const -> AssetType override { return AssetType::kSound; } - auto GetName() const -> std::string override { - if (!file_name_full_.empty()) - return file_name_full_; - else - return "invalid sound"; - } -#if BA_ENABLE_AUDIO - auto format() const -> ALenum { return format_; } - auto buffer() const -> ALuint { - assert(!is_streamed_); - return buffer_; - } -#endif // BA_ENABLE_AUDIO - auto is_streamed() const -> bool { return is_streamed_; } - auto file_name() const -> const std::string& { return file_name_; } - auto file_name_full() const -> const std::string& { return file_name_full_; } - void UpdatePlayTime() { last_play_time_ = GetRealTime(); } - auto last_play_time() const -> millisecs_t { return last_play_time_; } - - private: - std::string file_name_; - std::string file_name_full_; - bool is_streamed_{}; -#if BA_ENABLE_AUDIO - ALuint buffer_{}; - ALenum format_{}; - ALsizei freq_{}; -#endif // BA_ENABLE_AUDIO - std::vector load_buffer_; - millisecs_t last_play_time_{}; -}; - -} // namespace ballistica - -#endif // BALLISTICA_ASSETS_DATA_SOUND_DATA_H_ diff --git a/src/ballistica/assets/data/texture_data.h b/src/ballistica/assets/data/texture_data.h deleted file mode 100644 index e1ce22db..00000000 --- a/src/ballistica/assets/data/texture_data.h +++ /dev/null @@ -1,62 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_ASSETS_DATA_TEXTURE_DATA_H_ -#define BALLISTICA_ASSETS_DATA_TEXTURE_DATA_H_ - -#include -#include - -#include "ballistica/assets/data/asset_component_data.h" - -namespace ballistica { - -// Loadable texture media component. -class TextureData : public AssetComponentData { - public: - TextureData(); - ~TextureData() override; - - // pass a newly allocated TextPacker pointer here; TextureData takes ownership - // and handles cleaning it up - explicit TextureData(TextPacker* packer); - explicit TextureData(const std::string& file_in, TextureType type_in, - TextureMinQuality min_quality_in); - explicit TextureData(const std::string& qr_url); - auto GetName() const -> std::string override { - return (!file_name_.empty()) ? file_name_ : "invalid texture"; - } - auto GetNameFull() const -> std::string override { return file_name_full(); } - auto file_name() const -> const std::string& { return file_name_; } - auto file_name_full() const -> const std::string& { return file_name_full_; } - auto GetAssetType() const -> AssetType override { - return AssetType::kTexture; - } - void DoPreload() override; - void DoLoad() override; - void DoUnload() override; - auto texture_type() const -> TextureType { return type_; } - auto is_qr_code() const -> bool { return is_qr_code_; } - auto preload_datas() const -> const std::vector& { - return preload_datas_; - } - auto renderer_data() const -> TextureRendererData* { - assert(renderer_data_.exists()); - return renderer_data_.get(); - } - auto base_level() const -> int { return base_level_; } - - private: - Object::Ref packer_; - bool is_qr_code_ = false; - std::string file_name_; - std::string file_name_full_; - std::vector preload_datas_; - TextureType type_ = TextureType::k2D; - TextureMinQuality min_quality_ = TextureMinQuality::kLow; - Object::Ref renderer_data_; - int base_level_ = 0; -}; - -} // namespace ballistica - -#endif // BALLISTICA_ASSETS_DATA_TEXTURE_DATA_H_ diff --git a/src/ballistica/assets/data/texture_renderer_data.h b/src/ballistica/assets/data/texture_renderer_data.h deleted file mode 100644 index f3467940..00000000 --- a/src/ballistica/assets/data/texture_renderer_data.h +++ /dev/null @@ -1,27 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_ASSETS_DATA_TEXTURE_RENDERER_DATA_H_ -#define BALLISTICA_ASSETS_DATA_TEXTURE_RENDERER_DATA_H_ - -namespace ballistica { - -// Renderer-specific data (gl tex, etc) -// this is extended by the renderer -class TextureRendererData : public Object { - public: - auto GetDefaultOwnerThread() const -> ThreadTag override { - return ThreadTag::kMain; - } - - // Create the renderer data but don't load it in yet. - TextureRendererData() = default; - - // load the data. - // if incremental is true, return whether the load was completed - // (non-incremental loads should always complete) - virtual void Load() = 0; -}; - -} // namespace ballistica - -#endif // BALLISTICA_ASSETS_DATA_TEXTURE_RENDERER_DATA_H_ diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc deleted file mode 100644 index dcfc21b3..00000000 --- a/src/ballistica/ballistica.cc +++ /dev/null @@ -1,353 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/ballistica.h" - -#include - -#include "ballistica/app/app_config.h" -#include "ballistica/app/app_flavor.h" -#include "ballistica/assets/assets.h" -#include "ballistica/assets/assets_server.h" -#include "ballistica/audio/audio.h" -#include "ballistica/audio/audio_server.h" -#include "ballistica/core/fatal_error.h" -#include "ballistica/core/logging.h" -#include "ballistica/core/thread.h" -#include "ballistica/dynamics/bg/bg_dynamics_server.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/text/text_graphics.h" -#include "ballistica/input/input.h" -#include "ballistica/internal/app_internal.h" -#include "ballistica/logic/v1_account.h" -#include "ballistica/networking/network_reader.h" -#include "ballistica/networking/network_writer.h" -#include "ballistica/networking/networking.h" -#include "ballistica/platform/platform.h" -#include "ballistica/platform/stdio_console.h" -#include "ballistica/python/python.h" -#include "ballistica/scene/scene.h" -#include "ballistica/scene/v1/scene_v1.h" -#include "ballistica/ui/ui.h" - -namespace ballistica { - -// These are set automatically via script; don't modify them here. -const int kAppBuildNumber = 21005; -const char* kAppVersion = "1.7.20"; - -// Our standalone globals. -// These are separated out for easy access. -// Everything else should go into App (or more ideally into a class). -int g_early_v1_cloud_log_writes{10}; - -App* g_app{}; -AppConfig* g_app_config{}; -AppInternal* g_app_internal{}; -AppFlavor* g_app_flavor{}; -Assets* g_assets{}; -AssetsServer* g_assets_server{}; -Audio* g_audio{}; -AudioServer* g_audio_server{}; -BGDynamics* g_bg_dynamics{}; -BGDynamicsServer* g_bg_dynamics_server{}; -Context* g_context{}; -Graphics* g_graphics{}; -GraphicsServer* g_graphics_server{}; -Input* g_input{}; -Logic* g_logic{}; -Thread* g_main_thread{}; -Networking* g_networking{}; -NetworkReader* g_network_reader{}; -NetworkWriter* g_network_writer{}; -Platform* g_platform{}; -Python* g_python{}; -SceneV1* g_scene_v1{}; -StdioConsole* g_stdio_console{}; -TextGraphics* g_text_graphics{}; -UI* g_ui{}; -Utils* g_utils{}; -V1Account* g_v1_account{}; - -auto BallisticaMain(int argc, char** argv) -> int { - try { - // Even at the absolute start of execution we should be able to - // phone home on errors. Set env var BA_CRASH_TEST=1 to test this. - if (const char* crashenv = getenv("BA_CRASH_TEST")) { - if (!strcmp(crashenv, "1")) { - FatalError("Fatal-Error-Test"); - } - } - - // ------------------------------------------------------------------------- - // Phase 1: "The board is set." - // ------------------------------------------------------------------------- - - // Here we instantiate all of our globals. Code here should - // avoid any logic that accesses other globals since they may - // not yet exist. - - // Minimal globals we must assign immediately as they ARE needed - // for construction of the others (would be great to eliminate this need). - g_platform = Platform::Create(); - g_app = new App(argc, argv); - g_app_internal = CreateAppInternal(); - g_main_thread = new Thread(ThreadTag::kMain, ThreadSource::kWrapMain); - - // For everything else, we hold off until the end to actually assign - // them to their globals. This keeps us honest and catches any stray - // inter-global access that we might accidentally include in a - // constructor. - auto* app_flavor = g_platform->CreateAppFlavor(); - auto* python = Python::Create(); - auto* graphics = g_platform->CreateGraphics(); - auto* graphics_server = new GraphicsServer(); - auto* audio = new Audio(); - auto* audio_server = new AudioServer(); - auto* context = new Context(nullptr); - auto* text_graphics = new TextGraphics(); - auto* app_config = new AppConfig(); - auto* v1_account = new V1Account(); - auto* utils = new Utils(); - auto* assets = new Assets(); - auto* assets_server = new AssetsServer(); - auto* ui = Object::NewUnmanaged(); - auto* networking = new Networking(); - auto* network_reader = new NetworkReader(); - auto* network_writer = new NetworkWriter(); - auto* input = new Input(); - auto* logic = new Logic(); - auto* scene_v1 = new SceneV1(); - auto* bg_dynamics = HeadlessMode() ? nullptr : new BGDynamics; - auto* bg_dynamics_server = HeadlessMode() ? nullptr : new BGDynamicsServer; - auto* stdio_console = - g_buildconfig.enable_stdio_console() ? new StdioConsole() : nullptr; - - g_app_flavor = app_flavor; - g_python = python; - g_graphics = graphics; - g_graphics_server = graphics_server; - g_audio = audio; - g_audio_server = audio_server; - g_context = context; - g_text_graphics = text_graphics; - g_app_config = app_config; - g_v1_account = v1_account; - g_utils = utils; - g_assets = assets; - g_assets_server = assets_server; - g_ui = ui; - g_networking = networking; - g_network_reader = network_reader; - g_network_writer = network_writer; - g_input = input; - g_logic = logic; - g_scene_v1 = scene_v1; - g_bg_dynamics = bg_dynamics; - g_bg_dynamics_server = bg_dynamics_server; - g_stdio_console = stdio_console; - - g_app->is_bootstrapped = true; - - // ------------------------------------------------------------------------- - // Phase 2: "The pieces are moving." - // ------------------------------------------------------------------------- - - // Allow our subsystems to start doing work in their own threads - // and communicating with other subsystems. Note that we may still - // want to run some things serially here and ordering may be important - // (for instance we want to give our main thread a chance to register - // all initial input devices with the logic thread before the logic - // thread applies the current config to them). - - g_logic->OnAppStart(); - g_audio_server->OnAppStart(); - g_assets_server->OnAppStart(); - g_platform->OnAppStart(); - g_app_flavor->OnAppStart(); - if (g_stdio_console) { - g_stdio_console->OnAppStart(); - } - - // As the last step of this phase, tell the logic thread to apply - // the app config which will kick off screen creation and otherwise - // get the ball rolling. - g_logic->PushApplyConfigCall(); - - // ------------------------------------------------------------------------- - // Phase 3: "We come to it at last; the great battle of our time." - // ------------------------------------------------------------------------- - - // At this point all threads are off and running and we simply - // feed events until things end (or return and let the OS do that). - - if (g_app_flavor->ManagesEventLoop()) { - // On our event-loop-managing platforms we now simply sit in our event - // loop until the app is quit. - g_main_thread->RunEventLoop(false); - } else { - // In this case we'll now simply return and let the OS feed us events - // until the app quits. - // However, we may need to 'prime the pump' first. For instance, - // if the main thread event loop is driven by frame draws, it may need - // to manually pump events until drawing begins (otherwise it will never - // process the 'create-screen' event and wind up deadlocked). - g_app_flavor->PrimeEventPump(); - } - } catch (const std::exception& exc) { - std::string error_msg = - std::string("Unhandled exception in BallisticaMain(): ") + exc.what(); - - // Exiting the app via an exception tends to trigger crash reports - // on various platforms. If it seems we're not on an official live - // build then we'd rather just exit cleanly with an error code and avoid - // polluting crash report logs from dev builds. - FatalError::ReportFatalError(error_msg, true); - bool exit_cleanly = !IsUnmodifiedBlessedBuild(); - bool handled = FatalError::HandleFatalError(exit_cleanly, true); - - // Do the default thing if it's not been handled. - if (!handled) { - if (exit_cleanly) { - exit(1); - } else { - throw; - } - } - } - - g_platform->WillExitMain(false); - return g_app->return_value; -} - -auto GetRealTime() -> millisecs_t { - millisecs_t t = g_platform->GetTicks(); - - // If we're at a different time than our last query, do our funky math. - if (t != g_app->last_real_time_ticks) { - std::scoped_lock lock(g_app->real_time_mutex); - millisecs_t passed = t - g_app->last_real_time_ticks; - - // GetTicks() is supposed to be monotonic, but I've seen 'passed' - // equal -1 even when it is using std::chrono::steady_clock. Let's do - // our own filtering here to make 100% sure we don't go backwards. - if (passed < 0) { - passed = 0; - } else { - // Very large times-passed probably means we went to sleep or something; - // clamp to a reasonable value. - if (passed > 250) { - passed = 250; - } - } - g_app->real_time += passed; - g_app->last_real_time_ticks = t; - } - return g_app->real_time; -} - -auto FatalError(const std::string& message) -> void { - FatalError::ReportFatalError(message, false); - bool exit_cleanly = !IsUnmodifiedBlessedBuild(); - bool handled = FatalError::HandleFatalError(exit_cleanly, false); - BA_PRECONDITION(handled); -} - -// FIXME: move this to g_app or whatnot. -auto GetAppInstanceUUID() -> const std::string& { - static std::string app_instance_uuid; - static bool have_app_instance_uuid = false; - - if (!have_app_instance_uuid) { - if (g_python) { - Python::ScopedInterpreterLock gil; - auto uuid = g_python->obj(Python::ObjID::kUUIDStrCall).Call(); - if (uuid.exists()) { - app_instance_uuid = uuid.ValueAsString().c_str(); - have_app_instance_uuid = true; - } - } - if (!have_app_instance_uuid) { - // As an emergency fallback simply use a single random number. - // We should probably simply disallow this before Python is up. - Log(LogLevel::kWarning, "GetSessionUUID() using rand fallback."); - srand(static_cast( - Platform::GetCurrentMilliseconds())); // NOLINT - app_instance_uuid = - std::to_string(static_cast(rand())); // NOLINT - have_app_instance_uuid = true; - } - if (app_instance_uuid.size() >= 100) { - Log(LogLevel::kWarning, "session id longer than it should be."); - } - } - return app_instance_uuid; -} - -auto InMainThread() -> bool { - assert(g_main_thread); // Root out early use of this. - return (g_main_thread->IsCurrent()); -} - -auto InLogicThread() -> bool { - assert(g_app && g_app->is_bootstrapped); // Root out early use of this. - return (g_logic && g_logic->thread()->IsCurrent()); -} - -auto InGraphicsThread() -> bool { - assert(g_app && g_app->is_bootstrapped); // Root out early use of this. - return (g_graphics_server && g_graphics_server->thread()->IsCurrent()); -} - -auto InAudioThread() -> bool { - assert(g_app && g_app->is_bootstrapped); // Root out early use of this. - return (g_audio_server && g_audio_server->thread()->IsCurrent()); -} - -auto InBGDynamicsThread() -> bool { - assert(g_app && g_app->is_bootstrapped); // Root out early use of this. - return (g_bg_dynamics_server && g_bg_dynamics_server->thread()->IsCurrent()); -} - -auto InAssetsThread() -> bool { - assert(g_app && g_app->is_bootstrapped); // Root out early use of this. - return (g_assets_server && g_assets_server->thread()->IsCurrent()); -} - -auto InNetworkWriteThread() -> bool { - assert(g_app && g_app->is_bootstrapped); // Root out early use of this. - return (g_network_writer && g_network_writer->thread()->IsCurrent()); -} - -auto Log(LogLevel level, const std::string& msg) -> void { - Logging::Log(level, msg); -} - -auto IsVRMode() -> bool { return g_app->vr_mode; } - -void ScreenMessage(const std::string& s, const Vector3f& color) { - if (g_logic) { - g_logic->PushScreenMessage(s, color); - } else { - Log(LogLevel::kError, - "ScreenMessage before g_logic init (will be lost): '" + s + "'"); - } -} - -auto ScreenMessage(const std::string& msg) -> void { - ScreenMessage(msg, {1.0f, 1.0f, 1.0f}); -} - -auto GetCurrentThreadName() -> std::string { - return Thread::GetCurrentThreadName(); -} - -auto IsBootstrapped() -> bool { return g_app->is_bootstrapped; } - -} // namespace ballistica - -// If desired, define main() in the global namespace. -#if BA_DEFINE_MAIN -auto main(int argc, char** argv) -> int { - return ballistica::BallisticaMain(argc, argv); -} -#endif diff --git a/src/ballistica/base/README.md b/src/ballistica/base/README.md new file mode 100644 index 00000000..35439b54 --- /dev/null +++ b/src/ballistica/base/README.md @@ -0,0 +1,3 @@ +# Base Feature Set + +Most of the engine lives in this feature set. diff --git a/src/ballistica/base/app/app.cc b/src/ballistica/base/app/app.cc new file mode 100644 index 00000000..dfa5214b --- /dev/null +++ b/src/ballistica/base/app/app.cc @@ -0,0 +1,413 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/app/app.h" + +#include "ballistica/base/app/stress_test.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/base/input/device/touch_input.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/networking/network_reader.h" +#include "ballistica/base/networking/networking.h" +#include "ballistica/base/platform/base_platform.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" + +namespace ballistica::base { + +App::App(EventLoop* event_loop) + : event_loop_(event_loop), stress_test_(std::make_unique()) { + // We modify some app behavior when run under the server manager. + auto* envval = getenv("BA_SERVER_WRAPPER_MANAGED"); + server_wrapper_managed_ = (envval && strcmp(envval, "1") == 0); +} + +void App::PostInit() { + // Sanity check: make sure asserts are stripped out of release builds + // (NDEBUG should do this). +#if !BA_DEBUG_BUILD +#ifndef NDEBUG +#error Expected NDEBUG to be defined for release builds. +#endif // NDEBUG + assert(true); +#endif // !BA_DEBUG_BUILD + + g_core->user_agent_string = g_core->platform->GetUserAgentString(); +} + +void App::LogicThreadApplyAppConfig() { + // Note: this gets called in the logic thread since that's where + // config reading happens. We should grab whatever values we need + // and then forward them to ourself in the main thread. + // We also can give our other main-thread-based subsystems a chance + // to do the same. + assert(g_base->InLogicThread()); + + g_base->networking->ApplyAppConfig(); +} + +void App::LogicThreadStepDisplayTime() { assert(g_base->InLogicThread()); } + +auto App::ManagesEventLoop() const -> bool { + // We have 2 redundant values for essentially the same thing; + // should get rid of IsEventPushMode() once we've created + // App subclasses for our various platforms. + return !g_core->platform->IsEventPushMode(); +} + +void App::RunRenderUpkeepCycle() { + // This should only be used in cases where the OS is handling the event loop. + assert(!ManagesEventLoop()); + if (ManagesEventLoop()) { + return; + } + + // Pump thread messages (we're being driven by frame-draw callbacks + // so this is the only place that it gets done at). + event_loop()->RunEventLoop(true); // Single pass only. + + // Now do the general app event cycle for whoever needs to process things. + RunEvents(); +} + +void App::RebuildLostGLContext() { + assert(g_base->InGraphicsThread()); + assert(g_base->graphics_server); + if (g_base->graphics_server) { + g_base->graphics_server->RebuildLostContext(); + } +} + +void App::DrawFrame(bool during_resize) { + assert(g_base->InGraphicsThread()); + + // It's possible to receive frames before we're ready to draw. + if (!g_base->graphics_server || !g_base->graphics_server->renderer()) { + return; + } + + millisecs_t starttime = g_core->GetAppTimeMillisecs(); + + // A resize-draw event means that we're drawing due to a window resize. + // In this case we ignore regular draw events for a short while + // afterwards which makes resizing smoother. + // FIXME: should figure out the *correct* way to handle this; + // I believe the underlying cause here is some sort of context contention + // across threads. + if (during_resize) { + last_resize_draw_event_time_ = starttime; + } else { + if (starttime - last_resize_draw_event_time_ < (1000 / 30)) { + return; + } + } + g_base->graphics_server->TryRender(); + RunRenderUpkeepCycle(); +} + +void App::LogicThreadShutdownComplete() { + assert(g_core->InMainThread()); + assert(g_core); + + done_ = true; + + // Kill our own event loop (or tell the OS to kill its). + if (ManagesEventLoop()) { + event_loop()->Quit(); + } else { + g_core->platform->QuitApp(); + } +} + +void App::RunEvents() { + // there's probably a better place for this... + stress_test_->Update(); + + // Give platforms a chance to pump/handle their own events. + // FIXME: now that we have app class overrides, platform should really + // not be doing event handling. (need to fix rift build). + g_core->platform->RunEvents(); +} + +void App::UpdatePauseResume() { + if (actually_paused_) { + // Unpause if no one wants pause. + if (!sys_paused_app_) { + OnAppResume(); + actually_paused_ = false; + } + } else { + // OnAppPause if anyone wants. + if (sys_paused_app_) { + OnAppPause(); + actually_paused_ = true; + } + } +} + +void App::OnAppPause() { + assert(g_core->InMainThread()); + + // IMPORTANT: Any pause related stuff that event-loop-threads need to do + // should be done from their registered pause-callbacks. If we instead push + // runnables to them from here they may or may not be called before their + // event-loop is actually paused. + + // Pause all event loops. + EventLoop::SetThreadsPaused(true); + + if (g_base->network_reader) { + g_base->network_reader->OnAppPause(); + } + g_base->networking->OnAppPause(); + g_core->platform->OnAppPause(); +} + +void App::OnAppResume() { + assert(g_core->InMainThread()); + last_app_resume_time_ = g_core->GetAppTimeMillisecs(); + + // Spin all event-loops back up. + EventLoop::SetThreadsPaused(false); + + // Run resumes that expect to happen in the main thread. + g_core->platform->OnAppResume(); + g_base->network_reader->OnAppResume(); + g_base->networking->OnAppResume(); + + // When resuming from a paused state, we may want to + // pause whatever game was running when we last were active. + // TODO(efro): we should make this smarter so it doesn't happen if + // we're in a network game or something that we can't pause; + // bringing up the menu doesn't really accomplish anything there. + if (g_core->should_pause) { + g_core->should_pause = false; + + // If we've been completely backgrounded, + // send a menu-press command to the game; this will + // bring up a pause menu if we're in the game/etc. + g_base->ui->PushMainMenuPressCall(nullptr); + } +} + +auto App::GetProductPrice(const std::string& product) -> std::string { + std::scoped_lock lock(product_prices_mutex_); + auto i = product_prices_.find(product); + if (i == product_prices_.end()) { + return ""; + } else { + return i->second; + } +} + +void App::SetProductPrice(const std::string& product, + const std::string& price) { + std::scoped_lock lock(product_prices_mutex_); + product_prices_[product] = price; +} + +void App::PauseApp() { + assert(g_core); + assert(g_core->InMainThread()); + millisecs_t start_time{core::CorePlatform::GetCurrentMillisecs()}; + + // Apple mentioned 5 seconds to run stuff once backgrounded or + // they bring down the hammer. Let's aim to stay under 2. + millisecs_t max_duration{2000}; + + g_core->platform->DebugLog( + "PauseApp@" + std::to_string(core::CorePlatform::GetCurrentMillisecs())); + assert(!sys_paused_app_); + sys_paused_app_ = true; + UpdatePauseResume(); + + // We assume that the OS will completely suspend our process the moment + // we return from this call (though this is not technically true on all + // platforms). So we want to spin and wait for threads to actually + // process the pause message. + size_t running_thread_count{}; + while (std::abs(core::CorePlatform::GetCurrentMillisecs() - start_time) + < max_duration) { + // If/when we get to a point with no threads waiting to be paused, + // we're good to go. + auto threads{EventLoop::GetStillPausingThreads()}; + running_thread_count = threads.size(); + if (running_thread_count == 0) { + if (g_buildconfig.debug_build()) { + Log(LogLevel::kDebug, + "PauseApp() completed in " + + std::to_string(core::CorePlatform::GetCurrentMillisecs() + - start_time) + + "ms."); + } + return; + } + } + + // If we made it here, we timed out. Complain. + Log(LogLevel::kError, + std::string("PauseApp() took too long; ") + + std::to_string(running_thread_count) + + " threads not yet paused after " + + std::to_string(core::CorePlatform::GetCurrentMillisecs() + - start_time) + + " ms."); +} + +void App::ResumeApp() { + assert(g_core && g_core->InMainThread()); + millisecs_t start_time{core::CorePlatform::GetCurrentMillisecs()}; + g_core->platform->DebugLog( + "ResumeApp@" + std::to_string(core::CorePlatform::GetCurrentMillisecs())); + assert(sys_paused_app_); + sys_paused_app_ = false; + UpdatePauseResume(); + if (g_buildconfig.debug_build()) { + Log(LogLevel::kDebug, + "ResumeApp() completed in " + + std::to_string(core::CorePlatform::GetCurrentMillisecs() + - start_time) + + "ms."); + } +} + +void App::DidFinishRenderingFrame(FrameDef* frame) {} + +void App::PrimeMainThreadEventPump() { + assert(!ManagesEventLoop()); + + // Pump events manually until a screen gets created. + // At that point we use frame-draws to drive our event loop. + while (!g_base->graphics_server->initial_screen_created()) { + event_loop()->RunEventLoop(true); + core::CorePlatform::SleepMillisecs(1); + } +} + +#pragma mark Push-Calls + +// FIXME - move this call to Platform. +void App::PushShowOnlineScoreUICall(const std::string& show, + const std::string& game, + const std::string& game_version) { + event_loop()->PushCall([show, game, game_version] { + assert(g_core->InMainThread()); + g_core->platform->ShowOnlineScoreUI(show, game, game_version); + }); +} + +void App::PushPurchaseAckCall(const std::string& purchase, + const std::string& order_id) { + event_loop()->PushCall([purchase, order_id] { + g_base->platform->PurchaseAck(purchase, order_id); + }); +} + +void App::PushPurchaseCall(const std::string& item) { + event_loop()->PushCall([item] { + assert(g_core->InMainThread()); + g_base->platform->Purchase(item); + }); +} + +void App::PushRestorePurchasesCall() { + event_loop()->PushCall([] { + assert(g_core->InMainThread()); + g_base->platform->RestorePurchases(); + }); +} + +void App::PushOpenURLCall(const std::string& url) { + event_loop()->PushCall([url] { g_base->platform->OpenURL(url); }); +} + +void App::PushSubmitScoreCall(const std::string& game, + const std::string& game_version, int64_t score) { + event_loop()->PushCall([game, game_version, score] { + g_core->platform->SubmitScore(game, game_version, score); + }); +} + +void App::PushAchievementReportCall(const std::string& achievement) { + event_loop()->PushCall( + [achievement] { g_core->platform->ReportAchievement(achievement); }); +} + +void App::PushStringEditCall(const std::string& name, const std::string& value, + int max_chars) { + event_loop()->PushCall([name, value, max_chars] { + static millisecs_t last_edit_time = 0; + millisecs_t t = g_core->GetAppTimeMillisecs(); + + // Ignore if too close together. + // (in case second request comes in before first takes effect). + if (t - last_edit_time < 1000) { + return; + } + last_edit_time = t; + assert(g_core->InMainThread()); + g_core->platform->EditText(name, value, max_chars); + }); +} + +void App::PushSetStressTestingCall(bool enable, int player_count) { + event_loop()->PushCall([this, enable, player_count] { + stress_test_->Set(enable, player_count); + }); +} + +void App::PushResetAchievementsCall() { + event_loop()->PushCall([] { g_core->platform->ResetAchievements(); }); +} + +void App::OnMainThreadStartApp() { + assert(g_core->InMainThread()); + assert(g_base->input); + assert(g_core); + + // If we're running in a terminal, print some info. + // if (g_core->platform->is_stdin_a_terminal()) { + { + char buffer[256]; + if (g_buildconfig.headless_build()) { + snprintf(buffer, sizeof(buffer), "BallisticaKit Headless %s build %d.", + kEngineVersion, kEngineBuildNumber); + } else { + snprintf(buffer, sizeof(buffer), "BallisticaKit %s build %d.", + kEngineVersion, kEngineBuildNumber); + } + Log(LogLevel::kInfo, buffer); + } + // } + + // If we've got a nice themed hardware cursor, show it. + // Otherwise, hide the hardware cursor; we'll draw it in software. + // (need to run this in postinit because SDL/etc. may not be inited yet + // as of App::App()). + g_core->platform->SetHardwareCursorVisible(g_buildconfig.hardware_cursor()); + + if (!g_core->HeadlessMode()) { + // On desktop systems we just assume keyboard input exists and add it + // immediately. + if (g_core->platform->IsRunningOnDesktop()) { + g_base->input->PushCreateKeyboardInputDevices(); + } + + // On non-tv, non-desktop, non-vr systems, create a touchscreen input. + if (!g_core->platform->IsRunningOnTV() && !g_core->IsVRMode() + && !g_core->platform->IsRunningOnDesktop()) { + g_base->input->CreateTouchInput(); + } + } +} + +void App::PushCursorUpdate(bool vis) { + event_loop()->PushCall([vis] { + assert(g_core->InMainThread()); + g_core->platform->SetHardwareCursorVisible(vis); + }); +} + +} // namespace ballistica::base diff --git a/src/ballistica/app/app_flavor.h b/src/ballistica/base/app/app.h similarity index 50% rename from src/ballistica/app/app_flavor.h rename to src/ballistica/base/app/app.h index c0693b48..cea2ca1d 100644 --- a/src/ballistica/app/app_flavor.h +++ b/src/ballistica/base/app/app.h @@ -1,32 +1,41 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_APP_APP_FLAVOR_H_ -#define BALLISTICA_APP_APP_FLAVOR_H_ +#ifndef BALLISTICA_BASE_APP_APP_H_ +#define BALLISTICA_BASE_APP_APP_H_ #include #include #include #include -#include "ballistica/app/stress_test.h" -#include "ballistica/ballistica.h" +#include "ballistica/base/app/stress_test.h" +#include "ballistica/base/base.h" -namespace ballistica { +namespace ballistica::base { -/// Defines our high level app behavior. -class AppFlavor { +/// Encapsulates high level app behavior based on platform, build type, or +/// other factors determined at launch. A single binary can support multiple +/// app-flavors (standard, vr, headless, etc.), but the app will always have +/// a single constant flavor for a given run. +class App { public: - explicit AppFlavor(Thread* thread); + explicit App(EventLoop* event_loop); /// Should be run after the instance is created and assigned. /// Any setup that may trigger virtual methods or lookups via global /// should go here. - auto PostInit() -> void; + void PostInit(); + + /// Gets called when the app config is being read. + /// Note that this call happens in the logic thread, so we should + /// do any reading that needs to happen in the logic thread and then + /// forward the values to ourself back in our main thread. + void LogicThreadApplyAppConfig(); /// Return whether this class runs its own event loop. - /// If true, BallisticaMain() will continuously ask the app for events - /// until the app is quit, at which point BallisticaMain() returns. - /// If false, BallisticaMain returns immediately and it is assumed + /// If true, MonolithicMain() will continuously ask the app for events + /// until the app is quit, at which point MonolithicMain() returns. + /// If false, MonolithicMain returns immediately and it is assumed /// that the OS handles the app lifecycle and pushes events to the app /// via callbacks/etc. auto ManagesEventLoop() const -> bool; @@ -35,13 +44,13 @@ class AppFlavor { /// ensure they are self-sustaining. For instance, an app relying on /// frame-draws for its main thread event processing may need to /// manually pump events until frame rendering begins. - virtual auto PrimeEventPump() -> void; + virtual void PrimeMainThreadEventPump(); /// Handle any pending OS events. /// On normal graphical builds this is triggered by RunRenderUpkeepCycle(); /// timer intervals for headless builds, etc. /// Should process any pending OS events, etc. - virtual auto RunEvents() -> void; + virtual void RunEvents(); /// Put the app into a paused state. Should be called from the main /// thread. Pauses work, closes network sockets, etc. @@ -49,83 +58,79 @@ class AppFlavor { /// It is assumed that, as soon as this call returns, all work is /// finished and all threads can be suspended by the OS without any /// negative side effects. - auto PauseApp() -> void; + void PauseApp(); auto paused() const -> bool { return actually_paused_; } - /// Resume the app; corresponds to returning to foreground on mobile/etc. + /// OnAppResume the app; corresponds to returning to foreground on mobile/etc. /// Spins threads back up, re-opens network sockets, etc. - auto ResumeApp() -> void; + void ResumeApp(); - /// The last time the app was resumed (uses GetRealTime() value). + /// The last time the app was resumed (uses GetAppTimeMillisecs() value). auto last_app_resume_time() const -> millisecs_t { return last_app_resume_time_; } - /// Should be called when the window/screen resolution changes. - auto SetScreenResolution(float width, float height) -> void; - - /// Should be called if the platform detects the GL context was lost. - auto RebuildLostGLContext() -> void; + /// Should be called if the platform detects the GL context_ref was lost. + void RebuildLostGLContext(); /// Attempt to draw a frame. - auto DrawFrame(bool during_resize = false) -> void; + void DrawFrame(bool during_resize = false); + + /// Run updates in the logic thread. Generally called once per frame rendered + /// or at some fixed rate for headless builds. + void LogicThreadStepDisplayTime(); /// Used on platforms where our main thread event processing is driven by /// frame-draw commands given to us. This should be called after drawing /// a frame in order to bring game state up to date and process OS events. - auto RunRenderUpkeepCycle() -> void; + void RunRenderUpkeepCycle(); /// Called by the graphics-server when drawing completes for a frame. - virtual auto DidFinishRenderingFrame(FrameDef* frame) -> void; + virtual void DidFinishRenderingFrame(FrameDef* frame); /// Return the price of an IAP product as a human-readable string, /// or an empty string if not found. /// FIXME: move this to platform. auto GetProductPrice(const std::string& product) -> std::string; - auto SetProductPrice(const std::string& product, const std::string& price) - -> void; + void SetProductPrice(const std::string& product, const std::string& price); auto done() const -> bool { return done_; } - /// Whether we're running under ballisticacore_server.py + /// Whether we're running under ballisticakit_server.py /// (affects some app behavior). auto server_wrapper_managed() const -> bool { return server_wrapper_managed_; } - virtual auto OnAppStart() -> void; + virtual void OnMainThreadStartApp(); // Deferred calls that can be made from other threads. - auto PushCursorUpdate(bool vis) -> void; - auto PushShowOnlineScoreUICall(const std::string& show, + void PushCursorUpdate(bool vis); + void PushShowOnlineScoreUICall(const std::string& show, const std::string& game, - const std::string& game_version) -> void; - auto PushSubmitScoreCall(const std::string& game, - const std::string& game_version, int64_t score) - -> void; - auto PushAchievementReportCall(const std::string& achievement) -> void; - auto PushOpenURLCall(const std::string& url) -> void; - auto PushStringEditCall(const std::string& name, const std::string& value, - int max_chars) -> void; - auto PushSetStressTestingCall(bool enable, int player_count) -> void; - auto PushPurchaseCall(const std::string& item) -> void; - auto PushRestorePurchasesCall() -> void; - auto PushResetAchievementsCall() -> void; - auto PushPurchaseAckCall(const std::string& purchase, - const std::string& order_id) -> void; - auto PushNetworkSetupCall(int port, int telnet_port, bool enable_telnet, - const std::string& telnet_password) -> void; - auto PushShutdownCompleteCall() -> void; - auto thread() const -> Thread* { return thread_; } + const std::string& game_version); + void PushSubmitScoreCall(const std::string& game, + const std::string& game_version, int64_t score); + void PushAchievementReportCall(const std::string& achievement); + void PushOpenURLCall(const std::string& url); + void PushStringEditCall(const std::string& name, const std::string& value, + int max_chars); + void PushSetStressTestingCall(bool enable, int player_count); + void PushPurchaseCall(const std::string& item); + void PushRestorePurchasesCall(); + void PushResetAchievementsCall(); + void PushPurchaseAckCall(const std::string& purchase, + const std::string& order_id); + auto event_loop() const -> EventLoop* { return event_loop_; } + void LogicThreadShutdownComplete(); private: - auto UpdatePauseResume() -> void; - auto OnPause() -> void; - auto OnResume() -> void; - auto ShutdownComplete() -> void; - Thread* thread_{}; + void UpdatePauseResume(); + void OnAppPause(); + void OnAppResume(); + EventLoop* event_loop_{}; bool done_{}; bool server_wrapper_managed_{}; bool sys_paused_app_{}; @@ -137,6 +142,6 @@ class AppFlavor { std::mutex product_prices_mutex_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_APP_APP_FLAVOR_H_ +#endif // BALLISTICA_BASE_APP_APP_H_ diff --git a/src/ballistica/app/app_config.cc b/src/ballistica/base/app/app_config.cc similarity index 88% rename from src/ballistica/app/app_config.cc rename to src/ballistica/base/app/app_config.cc index 6446ca71..fcac66f0 100644 --- a/src/ballistica/app/app_config.cc +++ b/src/ballistica/base/app/app_config.cc @@ -1,12 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/app/app_config.h" +#include "ballistica/base/app/app_config.h" -#include "ballistica/ballistica.h" -#include "ballistica/platform/platform.h" -#include "ballistica/python/python.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/core/core.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/python/python.h" -namespace ballistica { +namespace ballistica::base { auto AppConfig::Entry::FloatValue() const -> float { throw Exception("not a float entry"); @@ -56,7 +58,8 @@ class AppConfig::StringEntry : public AppConfig::Entry { : Entry(name), default_value_(std::move(default_value)) {} auto GetType() const -> Type override { return Type::kString; } auto Resolve() const -> std::string { - return g_python->GetRawConfigValue(name().c_str(), default_value_.c_str()); + return g_base->python->GetRawConfigValue(name().c_str(), + default_value_.c_str()); } auto StringValue() const -> std::string override { return Resolve(); } auto DefaultStringValue() const -> std::string override { @@ -74,7 +77,7 @@ class AppConfig::FloatEntry : public AppConfig::Entry { : Entry(name), default_value_(default_value) {} auto GetType() const -> Type override { return Type::kFloat; } auto Resolve() const -> float { - return g_python->GetRawConfigValue(name().c_str(), default_value_); + return g_base->python->GetRawConfigValue(name().c_str(), default_value_); } auto FloatValue() const -> float override { return Resolve(); } auto DefaultFloatValue() const -> float override { return default_value_; } @@ -90,7 +93,7 @@ class AppConfig::OptionalFloatEntry : public AppConfig::Entry { : Entry(name), default_value_(default_value) {} auto GetType() const -> Type override { return Type::kOptionalFloat; } auto Resolve() const -> std::optional { - return g_python->GetRawConfigValue(name().c_str(), default_value_); + return g_base->python->GetRawConfigValue(name().c_str(), default_value_); } auto OptionalFloatValue() const -> std::optional override { return Resolve(); @@ -110,7 +113,7 @@ class AppConfig::IntEntry : public AppConfig::Entry { : Entry(name), default_value_(default_value) {} auto GetType() const -> Type override { return Type::kInt; } auto Resolve() const -> int { - return g_python->GetRawConfigValue(name().c_str(), default_value_); + return g_base->python->GetRawConfigValue(name().c_str(), default_value_); } auto IntValue() const -> int override { return Resolve(); } auto DefaultIntValue() const -> int override { return default_value_; } @@ -126,7 +129,7 @@ class AppConfig::BoolEntry : public AppConfig::Entry { : Entry(name), default_value_(default_value) {} auto GetType() const -> Type override { return Type::kBool; } auto Resolve() const -> bool { - return g_python->GetRawConfigValue(name().c_str(), default_value_); + return g_base->python->GetRawConfigValue(name().c_str(), default_value_); } auto BoolValue() const -> bool override { return Resolve(); } auto DefaultBoolValue() const -> bool override { return default_value_; } @@ -179,7 +182,7 @@ void AppConfig::SetupEntries() { float_entries_[FloatID::kMusicVolume] = FloatEntry("Music Volume", 1.0F); // Note: keep this synced with the defaults in MainActivity.java. - float gvrrts_default = g_platform->IsRunningOnDaydream() ? 1.0F : 0.5F; + float gvrrts_default = g_core->platform->IsRunningOnDaydream() ? 1.0F : 0.5F; float_entries_[FloatID::kGoogleVRRenderTargetScale] = FloatEntry("GVR Render Target Scale", gvrrts_default); @@ -203,14 +206,7 @@ void AppConfig::SetupEntries() { string_entries_[StringID::kMacControllerSubsystem] = StringEntry("Mac Controller Subsystem", "Classic"); - // We don't want passwords by default for GUI builds since we must - // approve access there. - string_entries_[StringID::kTelnetPassword] = StringEntry( - "Telnet Password", g_buildconfig.headless_build() ? "changeme" : ""); - int_entries_[IntID::kPort] = IntEntry("Port", kDefaultPort); - int_entries_[IntID::kTelnetPort] = - IntEntry("Telnet Port", kDefaultTelnetPort); bool_entries_[BoolID::kTouchControlsSwipeHidden] = BoolEntry("Touch Controls Swipe Hidden", false); @@ -223,8 +219,8 @@ void AppConfig::SetupEntries() { BoolEntry("Always Use Internal Keyboard", g_buildconfig.iircade_build()); bool_entries_[BoolID::kShowFPS] = BoolEntry("Show FPS", false); bool_entries_[BoolID::kShowPing] = BoolEntry("Show Ping", false); - bool_entries_[BoolID::kTVBorder] = - BoolEntry("TV Border", g_platform->IsRunningOnTV()); + bool_entries_[BoolID::kEnableTVBorder] = + BoolEntry("TV Border", g_core->platform->IsRunningOnTV()); bool_entries_[BoolID::kKeyboardP2Enabled] = BoolEntry("Keyboard P2 Enabled", false); bool_entries_[BoolID::kEnablePackageMods] = @@ -232,7 +228,6 @@ void AppConfig::SetupEntries() { bool_entries_[BoolID::kChatMuted] = BoolEntry("Chat Muted", false); bool_entries_[BoolID::kEnableRemoteApp] = BoolEntry("Enable Remote App", true); - bool_entries_[BoolID::kEnableTelnet] = BoolEntry("Enable Telnet", true); bool_entries_[BoolID::kDisableCameraShake] = BoolEntry("Disable Camera Shake", false); bool_entries_[BoolID::kDisableCameraGyro] = @@ -246,6 +241,7 @@ void AppConfig::SetupEntries() { } auto AppConfig::Resolve(FloatID id) -> float { + assert(g_base->InLogicThread()); auto i = float_entries_.find(id); if (i == float_entries_.end()) { throw Exception("Invalid config entry"); @@ -254,6 +250,7 @@ auto AppConfig::Resolve(FloatID id) -> float { } auto AppConfig::Resolve(OptionalFloatID id) -> std::optional { + assert(g_base->InLogicThread()); auto i = optional_float_entries_.find(id); if (i == optional_float_entries_.end()) { throw Exception("Invalid config entry"); @@ -262,6 +259,7 @@ auto AppConfig::Resolve(OptionalFloatID id) -> std::optional { } auto AppConfig::Resolve(StringID id) -> std::string { + assert(g_base->InLogicThread()); auto i = string_entries_.find(id); if (i == string_entries_.end()) { throw Exception("Invalid config entry"); @@ -270,6 +268,7 @@ auto AppConfig::Resolve(StringID id) -> std::string { } auto AppConfig::Resolve(BoolID id) -> bool { + assert(g_base->InLogicThread()); auto i = bool_entries_.find(id); if (i == bool_entries_.end()) { throw Exception("Invalid config entry"); @@ -278,6 +277,7 @@ auto AppConfig::Resolve(BoolID id) -> bool { } auto AppConfig::Resolve(IntID id) -> int { + assert(g_base->InLogicThread()); auto i = int_entries_.find(id); if (i == int_entries_.end()) { throw Exception("Invalid config entry"); @@ -285,4 +285,4 @@ auto AppConfig::Resolve(IntID id) -> int { return i->second.Resolve(); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/app/app_config.h b/src/ballistica/base/app/app_config.h similarity index 85% rename from src/ballistica/app/app_config.h rename to src/ballistica/base/app/app_config.h index bfd3e3ee..d27e7786 100644 --- a/src/ballistica/app/app_config.h +++ b/src/ballistica/base/app/app_config.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_APP_APP_CONFIG_H_ -#define BALLISTICA_APP_APP_CONFIG_H_ +#ifndef BALLISTICA_BASE_APP_APP_CONFIG_H_ +#define BALLISTICA_BASE_APP_APP_CONFIG_H_ #include #include @@ -9,12 +9,16 @@ #include #include -namespace ballistica { +// FIXME: this system is old and dumb. +// Should come up with a better one using the meta system +// based on Python dataclassio types or whatnot. -// This class wrangles user config values for the app. -// The underlying config data currently lives in the Python layer, -// so at the moment these calls are only usable from the logic thread, -// but that may change in the future. +namespace ballistica::base { + +/// Wrangles user config values for the app. +/// The underlying config data currently lives in the Python layer, +/// so at the moment these calls are only usable from the logic thread, +/// but that may change in the future. class AppConfig { public: // Our official config values: @@ -45,13 +49,11 @@ class AppConfig { kVerticalSync, kVRHeadRelativeAudio, kMacControllerSubsystem, - kTelnetPassword, kLast // Sentinel. }; enum class IntID { kPort, - kTelnetPort, kLast // Sentinel. }; @@ -62,12 +64,11 @@ class AppConfig { kAlwaysUseInternalKeyboard, kShowFPS, kShowPing, - kTVBorder, + kEnableTVBorder, kKeyboardP2Enabled, kEnablePackageMods, kChatMuted, kEnableRemoteApp, - kEnableTelnet, kDisableCameraShake, kDisableCameraGyro, kLast // Sentinel. @@ -136,6 +137,6 @@ class AppConfig { std::map bool_entries_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_APP_APP_CONFIG_H_ +#endif // BALLISTICA_BASE_APP_APP_CONFIG_H_ diff --git a/src/ballistica/base/app/app_headless.cc b/src/ballistica/base/app/app_headless.cc new file mode 100644 index 00000000..6399f2d4 --- /dev/null +++ b/src/ballistica/base/app/app_headless.cc @@ -0,0 +1,24 @@ +// Released under the MIT License. See LICENSE for details. +#if BA_HEADLESS_BUILD + +#include "ballistica/base/app/app_headless.h" + +#include "ballistica/shared/ballistica.h" + +namespace ballistica::base { + +// We could technically use the vanilla App class here since we're not +// changing anything. +AppHeadless::AppHeadless(EventLoop* event_loop) : App(event_loop) { + // Handle a few misc things like stress-test updates. + // (SDL builds set up a similar timer so we need to also). + // This can probably go away at some point. + this->event_loop()->NewTimer(10, true, NewLambdaRunnable([this] { + assert(g_base->app); + g_base->app->RunEvents(); + })); +} + +} // namespace ballistica::base + +#endif // BA_HEADLESS_BUILD diff --git a/src/ballistica/base/app/app_headless.h b/src/ballistica/base/app/app_headless.h new file mode 100644 index 00000000..050d8279 --- /dev/null +++ b/src/ballistica/base/app/app_headless.h @@ -0,0 +1,20 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_APP_APP_HEADLESS_H_ +#define BALLISTICA_BASE_APP_APP_HEADLESS_H_ +#if BA_HEADLESS_BUILD + +#include "ballistica/base/app/app.h" +#include "ballistica/shared/foundation/event_loop.h" + +namespace ballistica::base { + +class AppHeadless : public App { + public: + explicit AppHeadless(EventLoop* event_loop); +}; + +} // namespace ballistica::base + +#endif // BA_HEADLESS_BUILD +#endif // BALLISTICA_BASE_APP_APP_HEADLESS_H_ diff --git a/src/ballistica/base/app/app_mode.cc b/src/ballistica/base/app/app_mode.cc new file mode 100644 index 00000000..472064f4 --- /dev/null +++ b/src/ballistica/base/app/app_mode.cc @@ -0,0 +1,67 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/app/app_mode.h" + +#include "ballistica/base/input/device/input_device_delegate.h" +#include "ballistica/base/support/context.h" + +namespace ballistica::base { + +AppMode::AppMode() = default; + +void AppMode::OnActivate() {} +void AppMode::OnDeactivate() {} + +void AppMode::OnAppStart() {} +void AppMode::OnAppPause() {} +void AppMode::OnAppResume() {} +void AppMode::OnAppShutdown() {} + +auto AppMode::CreateInputDeviceDelegate(InputDevice* device) + -> InputDeviceDelegate* { + return Object::NewDeferred(); +} + +auto AppMode::HandleJSONPing(const std::string& data_str) -> std::string { + return ""; +} + +void AppMode::HandleIncomingUDPPacket(const std::vector& data_in, + const SockAddr& addr) {} + +void AppMode::HandleGameQuery(const char* buffer, size_t size, + sockaddr_storage* from) {} + +auto AppMode::DoesWorldFillScreen() -> bool { return false; } + +void AppMode::GraphicsQualityChanged(GraphicsQuality quality) {} + +void AppMode::DrawWorld(FrameDef* frame_def) {} + +void AppMode::ChangeGameSpeed(int offs) {} + +void AppMode::StepDisplayTime() {} + +auto AppMode::GetPartySize() const -> int { return 0; } + +auto AppMode::GetNetworkDebugString() -> std::string { return ""; } + +auto AppMode::GetPingString() -> std::string { return ""; } + +auto AppMode::HasConnectionToHost() const -> bool { return false; } + +auto AppMode::HasConnectionToClients() const -> bool { return false; } + +void AppMode::ApplyAppConfig() {} + +auto AppMode::GetForegroundContext() -> ContextRef { return {}; } + +void AppMode::OnScreenSizeChange() {} + +void AppMode::LanguageChanged() {} + +auto AppMode::LastClientJoinTime() const -> millisecs_t { return -1; } + +auto AppMode::InMainMenu() const -> bool { return false; } + +} // namespace ballistica::base diff --git a/src/ballistica/base/app/app_mode.h b/src/ballistica/base/app/app_mode.h new file mode 100644 index 00000000..46563ea9 --- /dev/null +++ b/src/ballistica/base/app/app_mode.h @@ -0,0 +1,102 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_APP_APP_MODE_H_ +#define BALLISTICA_BASE_APP_APP_MODE_H_ + +#include + +#include "ballistica/base/base.h" + +namespace ballistica::base { + +/// Represents 'what the app is doing'. The global app-mode can be switched +/// as the app is running. Be aware that, unlike the App/App classes +/// which operate in the main thread, most functionality here is based in the +/// logic thread. +class AppMode { + public: + AppMode(); + virtual ~AppMode() = default; + + /// Called when the app-mode is becoming the active one. + virtual void OnActivate(); + + /// Called just before the app-mode ceases being the active one. + virtual void OnDeactivate(); + + virtual void OnAppStart(); + virtual void OnAppPause(); + virtual void OnAppResume(); + virtual void OnAppShutdown(); + + /// Apply the app config. + virtual void ApplyAppConfig(); + + /// Update the logic thread. Can be called at any frequency; generally + /// corresponds to frame draws or a fixed timer. + virtual void StepDisplayTime(); + + /// Create a delegate for an input-device. + /// Return a raw pointer allocated using Object::NewDeferred. + virtual auto CreateInputDeviceDelegate(InputDevice* device) + -> InputDeviceDelegate*; + + /// Speed/slow stuff (generally debug builds only). + virtual void ChangeGameSpeed(int offs); + + /// Used for things like running Python code interactively. + virtual auto GetForegroundContext() -> ContextRef; + + /// If this returns true, renderers may opt to skip filling with a bg color. + virtual auto DoesWorldFillScreen() -> bool; + + virtual void DrawWorld(FrameDef* frame_def); + + virtual void GraphicsQualityChanged(GraphicsQuality quality); + + /// Called whenever screen size changes. + virtual void OnScreenSizeChange(); + + /// Called when language changes. + virtual void LanguageChanged(); + + /// Are we currently in a 'main menu'? + virtual auto InMainMenu() const -> bool; + + /// Get current party size (for legacy parties). + virtual auto GetPartySize() const -> int; + + /// Return whether we are connected to a host (for legacy parties). + virtual auto HasConnectionToHost() const -> bool; + + /// Return whether we are connected to one or more clients + /// (for legacy parties). + virtual auto HasConnectionToClients() const -> bool; + + /// Return real-time when last client joined (for legacy parties). + /// Returns -1 if nobody has joined yet. + virtual auto LastClientJoinTime() const -> millisecs_t; + + /// Handle raw network traffic. + virtual void HandleIncomingUDPPacket(const std::vector& data_in, + const SockAddr& addr); + + /// Handle a ping packet coming in (legacy). This is called from the + /// network-reader thread. + virtual auto HandleJSONPing(const std::string& data_str) -> std::string; + + /// Handle an incoming game query packet (devices on the local network + /// searching for games). + virtual void HandleGameQuery(const char* buffer, size_t size, + sockaddr_storage* from); + + /// Get a string for debugging current net i/o. + virtual auto GetNetworkDebugString() -> std::string; + + /// Get a string for current ping display. + virtual auto GetPingString() -> std::string; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_APP_APP_MODE_H_ diff --git a/src/ballistica/base/app/app_mode_empty.cc b/src/ballistica/base/app/app_mode_empty.cc new file mode 100644 index 00000000..abc45e20 --- /dev/null +++ b/src/ballistica/base/app/app_mode_empty.cc @@ -0,0 +1,21 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/app/app_mode_empty.h" + +namespace ballistica::base { + +static AppModeEmpty* g_app_mode_empty{}; + +AppModeEmpty::AppModeEmpty() = default; + +auto AppModeEmpty::GetSingleton() -> AppModeEmpty* { + // TODO(ericf): Turn this back on once we're creating in logic thread. + + // assert(g_base->InLogicThread()); // Can relax this if need be. + if (g_app_mode_empty == nullptr) { + g_app_mode_empty = new AppModeEmpty(); + } + return g_app_mode_empty; +} + +} // namespace ballistica::base diff --git a/src/ballistica/base/app/app_mode_empty.h b/src/ballistica/base/app/app_mode_empty.h new file mode 100644 index 00000000..b501b236 --- /dev/null +++ b/src/ballistica/base/app/app_mode_empty.h @@ -0,0 +1,21 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_APP_APP_MODE_EMPTY_H_ +#define BALLISTICA_BASE_APP_APP_MODE_EMPTY_H_ + +#include + +#include "ballistica/base/app/app_mode.h" + +namespace ballistica::base { + +class AppModeEmpty : public AppMode { + public: + AppModeEmpty(); + + static auto GetSingleton() -> AppModeEmpty*; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_APP_APP_MODE_EMPTY_H_ diff --git a/src/ballistica/base/app/app_vr.cc b/src/ballistica/base/app/app_vr.cc new file mode 100644 index 00000000..042a1de1 --- /dev/null +++ b/src/ballistica/base/app/app_vr.cc @@ -0,0 +1,122 @@ +// Released under the MIT License. See LICENSE for details. +#if BA_VR_BUILD + +#include "ballistica/base/app/app_vr.h" + +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/graphics/graphics_vr.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/shared/foundation/event_loop.h" + +namespace ballistica::base { + +AppVR::AppVR(EventLoop* event_loop) : App(event_loop) {} + +void AppVR::PushVRSimpleRemoteStateCall(const VRSimpleRemoteState& state) { + event_loop()->PushCall([this, state] { + // Convert this to a full hands state, adding in some simple elbow + // positioning of our own and left/right. + VRHandsState s; + s.l.tx = -0.2f; + s.l.ty = -0.2f; + s.l.tz = -0.3f; + + // Hmm; for now lets always assign this as right hand even when its in + // left-handed mode to keep things simple on the back-end. Can change later + // if there's a downside to that. + s.r.type = VRHandType::kDaydreamRemote; + s.r.tx = 0.2f; + s.r.ty = -0.2f; + s.r.tz = -0.3f; + s.r.yaw = state.r0; + s.r.pitch = state.r1; + s.r.roll = state.r2; + VRSetHands(s); + }); +} + +void AppVR::VRSetDrawDimensions(int w, int h) { + g_base->graphics_server->SetScreenResolution(w, h); +} + +void AppVR::VRPreDraw() { + if (!g_base || !g_base->graphics_server + || !g_base->graphics_server->renderer()) { + return; + } + assert(g_base->InGraphicsThread()); + if (FrameDef* frame_def = g_base->graphics_server->GetRenderFrameDef()) { + // Note: this could be part of PreprocessRenderFrameDef but + // the non-vr path needs it to be separate since preprocess doesn't + // happen sometimes. Should probably clean that up. + g_base->graphics_server->RunFrameDefMeshUpdates(frame_def); + + // store this for the duration of this frame + vr_render_frame_def_ = frame_def; + g_base->graphics_server->PreprocessRenderFrameDef(frame_def); + } +} + +void AppVR::VRPostDraw() { + assert(g_base->InGraphicsThread()); + if (!g_base || !g_base->graphics_server + || !g_base->graphics_server->renderer()) { + return; + } + if (vr_render_frame_def_) { + g_base->graphics_server->FinishRenderFrameDef(vr_render_frame_def_); + vr_render_frame_def_ = nullptr; + } + RunRenderUpkeepCycle(); +} + +void AppVR::VRSetHead(float tx, float ty, float tz, float yaw, float pitch, + float roll) { + assert(g_base->InGraphicsThread()); + Renderer* renderer = g_base->graphics_server->renderer(); + if (renderer == nullptr) return; + renderer->VRSetHead(tx, ty, tz, yaw, pitch, roll); +} + +void AppVR::VRSetHands(const VRHandsState& state) { + assert(g_base->InGraphicsThread()); + + // Pass this along to the renderer (in this same thread) for drawing + // (so hands can be drawn at their absolute most up-to-date positions, etc). + Renderer* renderer = g_base->graphics_server->renderer(); + if (renderer == nullptr) { + return; + } + renderer->VRSetHands(state); + + // ALSO ship it off to the logic thread to actually handle input from it. + // FIXME: This should get shipped to a logic or input variant once we have + // that for vr; not the graphics variant. + // Shipping it to the renderer above covers graphics needs in a lower + // latency way. + g_base->logic->event_loop()->PushCall( + [state] { GraphicsVR::get()->set_vr_hands_state(state); }); +} + +void AppVR::VRDrawEye(int eye, float yaw, float pitch, float roll, float tan_l, + float tan_r, float tan_b, float tan_t, float eye_x, + float eye_y, float eye_z, int viewport_x, + int viewport_y) { + if (!g_base || !g_base->graphics_server + || !g_base->graphics_server->renderer()) { + return; + } + assert(g_base->InGraphicsThread()); + if (vr_render_frame_def_) { + // Set up VR eye stuff. + Renderer* renderer = g_base->graphics_server->renderer(); + renderer->VRSetEye(eye, yaw, pitch, roll, tan_l, tan_r, tan_b, tan_t, eye_x, + eye_y, eye_z, viewport_x, viewport_y); + g_base->graphics_server->DrawRenderFrameDef(vr_render_frame_def_); + } +} + +} // namespace ballistica::base + +#endif // BA_VR_BUILD diff --git a/src/ballistica/base/app/app_vr.h b/src/ballistica/base/app/app_vr.h new file mode 100644 index 00000000..834a4b89 --- /dev/null +++ b/src/ballistica/base/app/app_vr.h @@ -0,0 +1,49 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_APP_APP_VR_H_ +#define BALLISTICA_BASE_APP_APP_VR_H_ + +#if BA_VR_BUILD + +#include "ballistica/base/app/app.h" + +namespace ballistica::base { + +class AppVR : public App { + public: + /// For passing in state of Daydream remote (and maybe gear vr?..). + struct VRSimpleRemoteState { + bool right_handed = true; + float r0 = 0.0f; + float r1 = 0.0f; + float r2 = 0.0f; + }; + + /// Return g_app as a AppVR. (assumes it actually is one). + static auto get() -> AppVR* { + assert(g_base != nullptr && g_base->app != nullptr); + assert(dynamic_cast(g_base->app) + == static_cast(g_base->app)); + return static_cast(g_base->app); + } + + explicit AppVR(EventLoop* event_loop); + void PushVRSimpleRemoteStateCall(const VRSimpleRemoteState& state); + void VRSetDrawDimensions(int w, int h); + void VRPreDraw(); + void VRPostDraw(); + void VRSetHead(float tx, float ty, float tz, float yaw, float pitch, + float roll); + void VRSetHands(const VRHandsState& state); + void VRDrawEye(int eye, float yaw, float pitch, float roll, float tan_l, + float tan_r, float tan_b, float tan_t, float eye_x, + float eye_y, float eye_z, int viewport_x, int viewport_y); + + private: + FrameDef* vr_render_frame_def_{}; +}; + +} // namespace ballistica::base + +#endif // BA_VR_BUILD +#endif // BALLISTICA_BASE_APP_APP_VR_H_ diff --git a/src/ballistica/platform/sdl/sdl_app.cc b/src/ballistica/base/app/sdl_app.cc similarity index 69% rename from src/ballistica/platform/sdl/sdl_app.cc rename to src/ballistica/base/app/sdl_app.cc index 3f28f087..066921a1 100644 --- a/src/ballistica/platform/sdl/sdl_app.cc +++ b/src/ballistica/base/app/sdl_app.cc @@ -2,25 +2,26 @@ #if BA_SDL_BUILD -#include "ballistica/platform/sdl/sdl_app.h" +#include "ballistica/base/app/sdl_app.h" -#include "ballistica/app/stress_test.h" -#include "ballistica/core/thread.h" -#include "ballistica/dynamics/bg/bg_dynamics.h" -#include "ballistica/graphics/gl/gl_sys.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/input/device/joystick.h" -#include "ballistica/input/input.h" -#include "ballistica/logic/logic.h" -#include "ballistica/platform/platform.h" -#include "ballistica/python/python.h" +#include "ballistica/base/app/stress_test.h" +#include "ballistica/base/dynamics/bg/bg_dynamics.h" +#include "ballistica/base/graphics/gl/gl_sys.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/input/device/joystick_input.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" -namespace ballistica { +namespace ballistica::base { // NOTE TO SELF: slowly try to phase everything out from here and into // non-sdl event/call pushes. void SDLApp::HandleSDLEvent(const SDL_Event& event) { - assert(InMainThread()); + assert(g_core->InMainThread()); switch (event.type) { case SDL_JOYAXISMOTION: @@ -37,10 +38,10 @@ void SDLApp::HandleSDLEvent(const SDL_Event& event) { || sdl_joysticks_[event.jbutton.which] == nullptr) { return; } - Joystick* js = GetSDLJoyStickInput(&event); + JoystickInput* js = GetSDLJoyStickInput(&event); if (js) { - if (g_input) { - g_input->PushJoystickEvent(event, js); + if (g_base) { + g_base->input->PushJoystickEvent(event, js); } } else { Log(LogLevel::kError, "Unable to get SDL Joystick for event type " @@ -55,8 +56,8 @@ void SDLApp::HandleSDLEvent(const SDL_Event& event) { // Convert sdl's coords to normalized view coords. float x = static_cast(e->x) / screen_dimensions_.x; float y = 1.0f - static_cast(e->y) / screen_dimensions_.y; - if (g_input) { - g_input->PushMouseDownEvent(e->button, Vector2f(x, y)); + if (g_base) { + g_base->input->PushMouseDownEvent(e->button, Vector2f(x, y)); } break; } @@ -66,8 +67,8 @@ void SDLApp::HandleSDLEvent(const SDL_Event& event) { // Convert sdl's coords to normalized view coords. float x = static_cast(e->x) / screen_dimensions_.x; float y = 1.0f - static_cast(e->y) / screen_dimensions_.y; - if (g_input) { - g_input->PushMouseUpEvent(e->button, Vector2f(x, y)); + if (g_base) { + g_base->input->PushMouseUpEvent(e->button, Vector2f(x, y)); } break; } @@ -77,20 +78,20 @@ void SDLApp::HandleSDLEvent(const SDL_Event& event) { // Convert sdl's coords to normalized view coords. float x = static_cast(e->x) / screen_dimensions_.x; float y = 1.0f - static_cast(e->y) / screen_dimensions_.y; - if (g_input) { - g_input->PushMouseMotionEvent(Vector2f(x, y)); + if (g_base) { + g_base->input->PushMouseMotionEvent(Vector2f(x, y)); } break; } case SDL_KEYDOWN: { - if (g_input) { - g_input->PushKeyPressEvent(event.key.keysym); + if (g_base) { + g_base->input->PushKeyPressEvent(event.key.keysym); } break; } case SDL_KEYUP: { - if (g_input) { - g_input->PushKeyReleaseEvent(event.key.keysym); + if (g_base) { + g_base->input->PushKeyReleaseEvent(event.key.keysym); } break; } @@ -111,8 +112,8 @@ void SDLApp::HandleSDLEvent(const SDL_Event& event) { } else { scroll_speed = 500; } - if (g_input) { - g_input->PushMouseScrollEvent( + if (g_base) { + g_base->input->PushMouseScrollEvent( Vector2f(static_cast(e->x * scroll_speed), static_cast(e->y * scroll_speed))); } @@ -123,8 +124,8 @@ void SDLApp::HandleSDLEvent(const SDL_Event& event) { #if BA_OSTYPE_MACOS && BA_XCODE_BUILD case SDL_SMOOTHSCROLLEVENT: { const SDL_SmoothScrollEvent* e = &event.scroll; - if (g_input) { - g_input->PushSmoothMouseScrollEvent( + if (g_base) { + g_base->input->PushSmoothMouseScrollEvent( Vector2f(0.2f * e->deltaX, -0.2f * e->deltaY), e->momentum); } break; @@ -155,25 +156,25 @@ void SDLApp::HandleSDLEvent(const SDL_Event& event) { #endif case SDL_QUIT: - g_logic->PushShutdownCall(false); + g_base->logic->event_loop()->PushCall([] { g_base->logic->Shutdown(); }); break; #if BA_OSTYPE_MACOS && BA_XCODE_BUILD && !BA_HEADLESS_BUILD case SDL_FULLSCREENSWITCH: // Our custom hacked-up SDL informs *us* when our window enters or exits // fullscreen. Let's commit this to our config so that we're in sync.. - g_python->PushObjCall(event.user.code - ? Python::ObjID::kSetConfigFullscreenOnCall - : Python::ObjID::kSetConfigFullscreenOffCall); - g_graphics_server->set_fullscreen_enabled(event.user.code); + g_base->python->objs().PushCall( + event.user.code ? BasePython::ObjID::kSetConfigFullscreenOnCall + : BasePython::ObjID::kSetConfigFullscreenOffCall); + g_base->graphics_server->set_fullscreen_enabled(event.user.code); break; #endif #if BA_SDL2_BUILD case SDL_TEXTINPUT: { - if (g_input) { - g_input->PushTextInputEvent(event.text.text); + if (g_base) { + g_base->input->PushTextInputEvent(event.text.text); } break; } @@ -201,13 +202,14 @@ void SDLApp::HandleSDLEvent(const SDL_Event& event) { // Do nothing here currently. #else // Generic SDL: int pixels_x, pixels_y; - SDL_GL_GetDrawableSize(g_graphics_server->gl_context()->sdl_window(), - &pixels_x, &pixels_y); + SDL_GL_GetDrawableSize( + g_base->graphics_server->gl_context()->sdl_window(), &pixels_x, + &pixels_y); // Pixel density is number of pixels divided by window dimension. screen_dimensions_ = Vector2f(event.window.data1, event.window.data2); - SetScreenResolution(static_cast(pixels_x), - static_cast(pixels_y)); + g_base->graphics_server->SetScreenResolution( + static_cast(pixels_x), static_cast(pixels_y)); #endif // BA_OSTYPE_IOS_TVOS break; @@ -222,7 +224,8 @@ void SDLApp::HandleSDLEvent(const SDL_Event& event) { #else // BA_SDL2_BUILD case SDL_VIDEORESIZE: { screen_dimensions_ = Vector2f(event.resize.w, event.resize.h); - SetScreenResolution(event.resize.w, event.resize.h); + g_base->graphics_server->SetScreenResolution(event.resize.w, + event.resize.h); break; } #endif // BA_SDL2_BUILD @@ -232,8 +235,8 @@ void SDLApp::HandleSDLEvent(const SDL_Event& event) { auto FilterSDLEvent(const SDL_Event* event) -> int { try { // If this event is coming from this thread, handle it immediately. - if (std::this_thread::get_id() == g_app->main_thread_id) { - auto app = static_cast_check_type(g_app_flavor); + if (std::this_thread::get_id() == g_core->main_thread_id) { + auto app = static_cast_check_type(g_base->app); assert(app); if (app) { app->HandleSDLEvent(*event); @@ -260,17 +263,17 @@ inline auto FilterSDL2Event(void* user_data, SDL_Event* event) -> int { // Note: can move this to SDLApp::SDLApp() once it is no longer needed by // the legacy mac build. void SDLApp::InitSDL() { - assert(g_platform != nullptr); + assert(g_core); if (g_buildconfig.ostype_macos()) { // We don't want sdl to translate command/option clicks to different mouse // buttons dernit. - g_platform->SetEnv("SDL_HAS3BUTTONMOUSE", "1"); + g_core->platform->SetEnv("SDL_HAS3BUTTONMOUSE", "1"); } // Let's turn on extra GL debugging on linux debug builds. if (g_buildconfig.ostype_linux() && g_buildconfig.debug_build()) { - g_platform->SetEnv("MESA_DEBUG", "true"); + g_core->platform->SetEnv("MESA_DEBUG", "true"); } uint32_t sdl_flags{}; @@ -296,7 +299,7 @@ void SDLApp::InitSDL() { // We want xinput on windows. if (g_buildconfig.ostype_windows()) { - if (!g_platform->GetLowLevelConfigValue("enablexinput", 1)) { + if (!g_core->platform->GetLowLevelConfigValue("enablexinput", 1)) { SDL_SetHint(SDL_HINT_XINPUT_ENABLED, "0"); } } @@ -319,7 +322,7 @@ void SDLApp::InitSDL() { #endif } -SDLApp::SDLApp(Thread* thread) : AppFlavor(thread) { +SDLApp::SDLApp(EventLoop* event_loop) : App(event_loop) { InitSDL(); // If we're not running our own even loop, we set up a filter to intercept @@ -341,15 +344,15 @@ SDLApp::SDLApp(Thread* thread) : AppFlavor(thread) { // something is returned; In spirit, we're pretty much doing that same // thing, except that we're free to handle other matters concurrently // instead of being locked in a delay call. - this->thread()->NewTimer(10, true, NewLambdaRunnable([this] { - assert(g_app_flavor); - g_app_flavor->RunEvents(); - })); + this->event_loop()->NewTimer(10, true, NewLambdaRunnable([this] { + assert(g_base->app); + g_base->app->RunEvents(); + })); } } void SDLApp::RunEvents() { - AppFlavor::RunEvents(); + App::RunEvents(); // Now run all pending SDL events until we run out or we're told to quit. SDL_Event event; @@ -359,15 +362,15 @@ void SDLApp::RunEvents() { } void SDLApp::DidFinishRenderingFrame(FrameDef* frame) { - AppFlavor::DidFinishRenderingFrame(frame); + App::DidFinishRenderingFrame(frame); SwapBuffers(); } void SDLApp::DoSwap() { - assert(InMainThread()); + assert(g_base->InGraphicsThread()); if (g_buildconfig.debug_build()) { - millisecs_t diff = GetRealTime() - swap_start_time_; + millisecs_t diff = g_core->GetAppTimeMillisecs() - swap_start_time_; if (diff > 5) { Log(LogLevel::kWarning, "Swap handling delay of " + std::to_string(diff)); } @@ -375,13 +378,13 @@ void SDLApp::DoSwap() { #if BA_ENABLE_OPENGL #if BA_SDL2_BUILD - SDL_GL_SwapWindow(g_graphics_server->gl_context()->sdl_window()); + SDL_GL_SwapWindow(g_base->graphics_server->gl_context()->sdl_window()); #else SDL_GL_SwapBuffers(); #endif // BA_SDL2_BUILD #endif // BA_ENABLE_OPENGL - millisecs_t cur_time = GetRealTime(); + millisecs_t cur_time = g_core->GetAppTimeMillisecs(); // Do some post-render analysis/updates. if (last_swap_time_ != 0) { @@ -404,15 +407,15 @@ void SDLApp::DoSwap() { // A common cause of slowness is excessive smoke and bg stuff; // lets tell the bg dynamics thread to tone it down. - g_bg_dynamics->TooSlow(); + g_base->bg_dynamics->TooSlow(); } } last_swap_time_ = cur_time; } void SDLApp::SwapBuffers() { - swap_start_time_ = GetRealTime(); - assert(thread()->IsCurrent()); + swap_start_time_ = g_core->GetAppTimeMillisecs(); + assert(event_loop()->ThreadIsCurrent()); DoSwap(); // FIXME: Move this somewhere reasonable. Not here. @@ -423,7 +426,7 @@ void SDLApp::SwapBuffers() { static int f_count = 0; f_count++; if (f_count == 5) { - g_platform->GameCenterLogin(); + g_core->platform->GameCenterLogin(); } } } @@ -433,44 +436,63 @@ void SDLApp::UpdateAutoVSync(int diff) { // If we're currently vsyncing, watch for slow frames. if (vsync_enabled_) { - // Keep a smoothed average of the FPS we get with VSync on. - { - float this_fps = 1000.0f / static_cast(diff); - float smoothing = 0.95f; - average_vsync_fps_ = - smoothing * average_vsync_fps_ + (1.0f - smoothing) * this_fps; - } + bool should_disable{}; - // If framerate drops significantly below 60, flip vsync off to get a - // better framerate (but *only* if we're pretty sure we can hit 60 with - // it on; otherwise if we're on a 30hz monitor we'll get into a cycle of - // flipping it off and on repeatedly since we slow down a lot with it on - // and then speed up a lot with it off). - if (diff >= 1000 / 40 && average_vsync_fps_ > 55.0f) { - vsync_bad_frame_count_++; + // Note (March 2023): Currently mac opengl vsync seems broken on recent OSs. + // See discussions: https://github.com/libsdl-org/SDL/issues/4918 + // Since Mac compositor generally avoids tearing anyway, just gonna + // have 'auto' mode disable vsync for now. Explicit enable is still + // available for odd cases where it still may be beneficial. + if (g_buildconfig.ostype_macos()) { + should_disable = true; } else { - vsync_bad_frame_count_ = 0; - } + // Keep a smoothed average of the FPS we get with VSync on. + { + float this_fps = 1000.0f / static_cast(diff); + float smoothing = 0.95f; + average_vsync_fps_ = + smoothing * average_vsync_fps_ + (1.0f - smoothing) * this_fps; + } - if (vsync_bad_frame_count_ >= 10) { + // FIXME: should not be assuming a 60fps framerate these days. + // If framerate drops significantly below 60, flip vsync off to get a + // better framerate (but *only* if we're pretty sure we can hit 60 with + // it on; otherwise if we're on a 30hz monitor we'll get into a cycle of + // flipping it off and on repeatedly since we slow down a lot with it on + // and then speed up a lot with it off). + if (diff >= 1000 / 40 && average_vsync_fps_ > 55.0f) { + vsync_bad_frame_count_++; + } else { + vsync_bad_frame_count_ = 0; + } + should_disable = vsync_bad_frame_count_ >= 10; + } + if (should_disable) { vsync_enabled_ = false; #if BA_ENABLE_OPENGL - g_graphics_server->gl_context()->SetVSync(vsync_enabled_); + g_base->graphics_server->gl_context()->SetVSync(vsync_enabled_); #endif vsync_good_frame_count_ = 0; } } else { - // Vsync is currently off.. watch for framerate staying consistently high - // and then turn it on again. - if (diff <= 1000 / 50) { - vsync_good_frame_count_++; + bool should_enable{}; + if (g_buildconfig.ostype_macos()) { + should_enable = false; } else { - vsync_good_frame_count_ = 0; + // Vsync is currently off.. watch for framerate staying consistently high + // and then turn it on again. + if (diff <= 1000 / 50) { + vsync_good_frame_count_++; + } else { + vsync_good_frame_count_ = 0; + } + // FIXME - should not be assuming a 60fps framerate these days. + should_enable = vsync_good_frame_count_ >= 60; } - if (vsync_good_frame_count_ >= 60) { + if (should_enable) { vsync_enabled_ = true; #if BA_ENABLE_OPENGL - g_graphics_server->gl_context()->SetVSync(vsync_enabled_); + g_base->graphics_server->gl_context()->SetVSync(vsync_enabled_); #endif vsync_bad_frame_count_ = 0; } @@ -483,15 +505,15 @@ void SDLApp::SetAutoVSync(bool enable) { if (enable) { vsync_enabled_ = true; #if BA_ENABLE_OPENGL - g_graphics_server->gl_context()->SetVSync(vsync_enabled_); + g_base->graphics_server->gl_context()->SetVSync(vsync_enabled_); #endif } } -void SDLApp::OnAppStart() { - AppFlavor::OnAppStart(); +void SDLApp::OnMainThreadStartApp() { + App::OnMainThreadStartApp(); - if (!HeadlessMode() && g_buildconfig.enable_sdl_joysticks()) { + if (!g_core->HeadlessMode() && g_buildconfig.enable_sdl_joysticks()) { // Add initial sdl joysticks. any added/removed after this will be handled // via events. (it seems (on mac at least) even the initial ones are handled // via events, so lets make sure we handle redundant joystick connections @@ -509,11 +531,11 @@ void SDLApp::OnAppStart() { } void SDLApp::SDLJoystickConnected(int device_index) { - assert(InMainThread()); + assert(g_core && g_core->InMainThread()); // We add all existing inputs when bootstrapping is complete; we should // never be getting these before that happens. - if (g_input == nullptr || g_app_flavor == nullptr || !IsBootstrapped()) { + if (!g_base) { Log(LogLevel::kError, "Unexpected SDLJoystickConnected early in boot sequence."); return; @@ -524,7 +546,7 @@ void SDLApp::SDLJoystickConnected(int device_index) { if (g_buildconfig.ostype_ios_tvos()) { BA_LOG_ONCE(LogLevel::kError, "WTF GOT SDL-JOY-CONNECTED ON IOS"); } else { - auto* j = Object::NewDeferred(device_index); + auto* j = Object::NewDeferred(device_index); if (g_buildconfig.sdl2_build() && g_buildconfig.enable_sdl_joysticks()) { int instance_id = SDL_JoystickInstanceID(j->sdl_joystick()); get()->AddSDLInputDevice(j, instance_id); @@ -535,19 +557,19 @@ void SDLApp::SDLJoystickConnected(int device_index) { } void SDLApp::SDLJoystickDisconnected(int index) { - assert(InMainThread()); + assert(g_core->InMainThread()); assert(index >= 0); get()->RemoveSDLInputDevice(index); } -auto SDLApp::SetInitialScreenDimensions(const Vector2f& dimensions) -> void { +void SDLApp::SetInitialScreenDimensions(const Vector2f& dimensions) { screen_dimensions_ = dimensions; } -void SDLApp::AddSDLInputDevice(Joystick* input, int index) { - assert(g_input != nullptr); +void SDLApp::AddSDLInputDevice(JoystickInput* input, int index) { + assert(g_base && g_base->input != nullptr); assert(input != nullptr); - assert(InMainThread()); + assert(g_core->InMainThread()); assert(index >= 0); // Keep a mapping of SDL input-device indices to Joysticks. @@ -556,13 +578,13 @@ void SDLApp::AddSDLInputDevice(Joystick* input, int index) { } sdl_joysticks_[index] = input; - g_input->PushAddInputDeviceCall(input, true); + g_base->input->PushAddInputDeviceCall(input, true); } void SDLApp::RemoveSDLInputDevice(int index) { - assert(InMainThread()); + assert(g_core->InMainThread()); assert(index >= 0); - Joystick* j = GetSDLJoyStickInput(index); + JoystickInput* j = GetSDLJoyStickInput(index); assert(j); if (static_cast_check_fit(sdl_joysticks_.size()) > index) { sdl_joysticks_[index] = nullptr; @@ -571,11 +593,11 @@ void SDLApp::RemoveSDLInputDevice(int index) { + std::to_string(sdl_joysticks_.size()) + "; index is " + std::to_string(index)); } - g_input->PushRemoveInputDeviceCall(j, true); + g_base->input->PushRemoveInputDeviceCall(j, true); } -auto SDLApp::GetSDLJoyStickInput(const SDL_Event* e) const -> Joystick* { - assert(InMainThread()); +auto SDLApp::GetSDLJoyStickInput(const SDL_Event* e) const -> JoystickInput* { + assert(g_core->InMainThread()); int joy_id; // Attempt to pull the joystick id from the event. @@ -599,8 +621,8 @@ auto SDLApp::GetSDLJoyStickInput(const SDL_Event* e) const -> Joystick* { return GetSDLJoyStickInput(joy_id); } -auto SDLApp::GetSDLJoyStickInput(int sdl_joystick_id) const -> Joystick* { - assert(InMainThread()); +auto SDLApp::GetSDLJoyStickInput(int sdl_joystick_id) const -> JoystickInput* { + assert(g_core->InMainThread()); for (auto sdl_joystick : sdl_joysticks_) { if ((sdl_joystick != nullptr) && (*sdl_joystick).sdl_joystick_id() >= 0 && (*sdl_joystick).sdl_joystick_id() == sdl_joystick_id) @@ -609,6 +631,6 @@ auto SDLApp::GetSDLJoyStickInput(int sdl_joystick_id) const -> Joystick* { return nullptr; // Epic fail. } -} // namespace ballistica +} // namespace ballistica::base #endif // BA_SDL_BUILD diff --git a/src/ballistica/base/app/sdl_app.h b/src/ballistica/base/app/sdl_app.h new file mode 100644 index 00000000..7584c4d4 --- /dev/null +++ b/src/ballistica/base/app/sdl_app.h @@ -0,0 +1,66 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_APP_SDL_APP_H_ +#define BALLISTICA_BASE_APP_SDL_APP_H_ + +#if BA_SDL_BUILD + +#include + +#include "ballistica/base/app/app.h" +#include "ballistica/shared/math/vector2f.h" + +namespace ballistica::base { + +class SDLApp : public App { + public: + static void InitSDL(); + explicit SDLApp(EventLoop* event_loop); + void HandleSDLEvent(const SDL_Event& event); + void RunEvents() override; + void DidFinishRenderingFrame(FrameDef* frame) override; + void SetAutoVSync(bool enable); + static void SDLJoystickConnected(int index); + static void SDLJoystickDisconnected(int index); + void OnMainThreadStartApp() override; + + /// Return g_base->app as a SDLApp. (assumes it actually is one). + static SDLApp* get() { + assert(g_base && g_base->app != nullptr); + assert(dynamic_cast(g_base->app) + == static_cast(g_base->app)); + return static_cast(g_base->app); + } + void SetInitialScreenDimensions(const Vector2f& dimensions); + + private: + // Given an sdl joystick ID, returns our ballistica input for it. + auto GetSDLJoyStickInput(int sdl_joystick_id) const -> JoystickInput*; + + // The same but using sdl events. + auto GetSDLJoyStickInput(const SDL_Event* e) const -> JoystickInput*; + + void DoSwap(); + void SwapBuffers(); + void UpdateAutoVSync(int diff); + void AddSDLInputDevice(JoystickInput* input, int index); + void RemoveSDLInputDevice(int index); + millisecs_t last_swap_time_{}; + millisecs_t swap_start_time_{}; + int too_slow_frame_count_{}; + bool auto_vsync_{}; + bool vsync_enabled_{true}; + float average_vsync_fps_{60.0f}; + int vsync_good_frame_count_{}; + int vsync_bad_frame_count_{}; + std::vector sdl_joysticks_; + + /// This is in points; not pixels. + Vector2f screen_dimensions_{1.0f, 1.0f}; +}; + +} // namespace ballistica::base + +#endif // BA_SDL_BUILD + +#endif // BALLISTICA_BASE_APP_SDL_APP_H_ diff --git a/src/ballistica/base/app/stress_test.cc b/src/ballistica/base/app/stress_test.cc new file mode 100644 index 00000000..88a21428 --- /dev/null +++ b/src/ballistica/base/app/stress_test.cc @@ -0,0 +1,101 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/app/stress_test.h" + +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/base/input/input.h" + +namespace ballistica::base { + +void StressTest::Set(bool enable, int player_count) { + assert(g_core->InMainThread()); + bool was_stress_testing = stress_testing_; + stress_testing_ = enable; + stress_test_player_count_ = player_count; + + // If we're turning on, reset our intervals and things. + if (!was_stress_testing && stress_testing_) { + // So our first sample is 1 interval from now. + last_stress_test_update_time_ = g_core->GetAppTimeMillisecs(); + + // Reset our frames-rendered tally. + if (g_base && g_base->graphics_server + && g_base->graphics_server->renderer()) { + last_total_frames_rendered_ = + g_base->graphics_server->renderer()->total_frames_rendered(); + } else { + // Assume zero if there's no graphics yet. + last_total_frames_rendered_ = 0; + } + } +} + +void StressTest::Update() { + assert(g_core->InMainThread()); + + // Handle a little misc stuff here. + // If we're currently running stress-tests, update that stuff. + if (stress_testing_ && g_base->input) { + // Update our fake inputs to make our dudes run around. + g_base->input->ProcessStressTesting(stress_test_player_count_); + + // Every 10 seconds update our stress-test stats. + millisecs_t t = g_core->GetAppTimeMillisecs(); + if (t - last_stress_test_update_time_ >= 10000) { + if (stress_test_stats_file_ == nullptr) { + assert(g_core); + auto user_python_dir = g_core->platform->GetUserPythonDirectory(); + if (user_python_dir) { + std::string f_name = *user_python_dir + "/stress_test_stats.csv"; + stress_test_stats_file_ = + g_core->platform->FOpen(f_name.c_str(), "wb"); + if (stress_test_stats_file_ != nullptr) { + fprintf( + stress_test_stats_file_, + "time,averageFps,nodes,meshes,collision_meshes,textures,sounds," + "pssMem,sharedDirtyMem,privateDirtyMem\n"); + fflush(stress_test_stats_file_); + } + } + } + if (stress_test_stats_file_ != nullptr) { + // See how many frames we've rendered this past interval. + int total_frames_rendered; + if (g_base && g_base->graphics_server + && g_base->graphics_server->renderer()) { + total_frames_rendered = + g_base->graphics_server->renderer()->total_frames_rendered(); + } else { + total_frames_rendered = last_total_frames_rendered_; + } + float avg = + static_cast(total_frames_rendered + - last_total_frames_rendered_) + / (static_cast(t - last_stress_test_update_time_) / 1000.0f); + last_total_frames_rendered_ = total_frames_rendered; + uint32_t mesh_count = 0; + uint32_t collision_mesh_count = 0; + uint32_t texture_count = 0; + uint32_t sound_count = 0; + uint32_t node_count = 0; + if (g_base) { + mesh_count = g_base->assets->total_mesh_count(); + collision_mesh_count = g_base->assets->total_collision_mesh_count(); + texture_count = g_base->assets->total_texture_count(); + sound_count = g_base->assets->total_sound_count(); + } + assert(g_base->logic); + std::string mem_usage = g_core->platform->GetMemUsageInfo(); + fprintf(stress_test_stats_file_, "%d,%.1f,%d,%d,%d,%d,%d,%s\n", + static_cast_check_fit(g_core->GetAppTimeMillisecs()), avg, + node_count, mesh_count, collision_mesh_count, texture_count, + sound_count, mem_usage.c_str()); + fflush(stress_test_stats_file_); + } + last_stress_test_update_time_ = t; + } + } +} + +} // namespace ballistica::base diff --git a/src/ballistica/base/app/stress_test.h b/src/ballistica/base/app/stress_test.h new file mode 100644 index 00000000..7ad13129 --- /dev/null +++ b/src/ballistica/base/app/stress_test.h @@ -0,0 +1,25 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_APP_STRESS_TEST_H_ +#define BALLISTICA_BASE_APP_STRESS_TEST_H_ + +#include "ballistica/shared/ballistica.h" + +namespace ballistica::base { + +class StressTest { + public: + void Set(bool enable, int player_count); + void Update(); + + private: + FILE* stress_test_stats_file_{}; + millisecs_t last_stress_test_update_time_{}; + bool stress_testing_{}; + int stress_test_player_count_{8}; + int last_total_frames_rendered_{}; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_APP_STRESS_TEST_H_ diff --git a/src/ballistica/assets/data/asset_component_data.cc b/src/ballistica/base/assets/asset.cc similarity index 64% rename from src/ballistica/assets/data/asset_component_data.cc rename to src/ballistica/base/assets/asset.cc index bb7e96ed..e554ab60 100644 --- a/src/ballistica/assets/data/asset_component_data.cc +++ b/src/ballistica/base/assets/asset.cc @@ -1,41 +1,38 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/assets/data/asset_component_data.h" +#include "ballistica/base/assets/asset.h" -namespace ballistica { +namespace ballistica::base { -AssetComponentData::AssetComponentData() { - assert(InLogicThread()); - assert(g_assets); - last_used_time_ = GetRealTime(); +Asset::Asset() { + assert(g_base); + assert(g_base->InLogicThread()); + last_used_time_ = g_core->GetAppTimeMillisecs(); } -AssetComponentData::~AssetComponentData() { +Asset::~Asset() { // at the moment whoever owns the last reference to us // needs to make sure to unload us before we die.. // I feel like there should be a more elegant solution to that. - assert(g_assets); + assert(g_base && g_base->assets); assert(!locked()); assert(!loaded()); } -void AssetComponentData::Preload(bool already_locked) { +void Asset::Preload(bool already_locked) { LockGuard lock(this, already_locked ? LockGuard::Type::kDontLock : LockGuard::Type::kLock); if (!preloaded_) { assert(!loaded_); -#if BA_SHOW_LOADS_UNLOADS - printf("pre-loading %s\n", GetName().c_str()); -#endif BA_PRECONDITION(locked()); - preload_start_time_ = GetRealTime(); + preload_start_time_ = g_core->GetAppTimeMillisecs(); DoPreload(); - preload_end_time_ = GetRealTime(); + preload_end_time_ = g_core->GetAppTimeMillisecs(); preloaded_ = true; } } -void AssetComponentData::Load(bool already_locked) { +void Asset::Load(bool already_locked) { LockGuard lock(this, already_locked ? LockGuard::Type::kDontLock : LockGuard::Type::kLock); if (!preloaded_) { @@ -43,21 +40,18 @@ void AssetComponentData::Load(bool already_locked) { } if (!loaded_) { -#if BA_SHOW_LOADS_UNLOADS - printf("loading %s\n", GetName().c_str()); -#endif assert(preloaded_ && !loaded_); BA_DEBUG_FUNCTION_TIMER_BEGIN(); BA_PRECONDITION(locked()); - load_start_time_ = GetRealTime(); + load_start_time_ = g_core->GetAppTimeMillisecs(); DoLoad(); - load_end_time_ = GetRealTime(); + load_end_time_ = g_core->GetAppTimeMillisecs(); BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(50, GetName()); loaded_ = true; } } -void AssetComponentData::Unload(bool already_locked) { +void Asset::Unload(bool already_locked) { LockGuard lock(this, already_locked ? LockGuard::Type::kDontLock : LockGuard::Type::kLock); @@ -70,9 +64,6 @@ void AssetComponentData::Unload(bool already_locked) { Load(true); } if (loaded_ && preloaded_) { -#if BA_SHOW_LOADS_UNLOADS - printf("unloading %s\n", GetName().c_str()); -#endif BA_PRECONDITION(locked()); DoUnload(); preloaded_ = false; @@ -80,8 +71,23 @@ void AssetComponentData::Unload(bool already_locked) { } } -AssetComponentData::LockGuard::LockGuard(AssetComponentData* data, Type type) - : data_(data) { +void Asset::Lock() { + BA_DEBUG_FUNCTION_TIMER_BEGIN(); + mutex_.lock(); + assert(!locked_); + locked_ = true; + BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(20, GetName()); +} + +void Asset::Unlock() { + BA_DEBUG_FUNCTION_TIMER_BEGIN(); + assert(locked_); + locked_ = false; + mutex_.unlock(); + BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(20, GetName()); +} + +Asset::LockGuard::LockGuard(Asset* data, Type type) : data_(data) { switch (type) { case kLock: { BA_DEBUG_FUNCTION_TIMER_BEGIN(); @@ -100,9 +106,9 @@ AssetComponentData::LockGuard::LockGuard(AssetComponentData* data, Type type) } } -AssetComponentData::LockGuard::~LockGuard() { +Asset::LockGuard::~LockGuard() { if (holds_lock_) { data_->Unlock(); } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/assets/data/asset_component_data.h b/src/ballistica/base/assets/asset.h similarity index 78% rename from src/ballistica/assets/data/asset_component_data.h rename to src/ballistica/base/assets/asset.h index 4a2f91e8..0284ce18 100644 --- a/src/ballistica/assets/data/asset_component_data.h +++ b/src/ballistica/base/assets/asset.h @@ -1,27 +1,33 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_ASSETS_DATA_ASSET_COMPONENT_DATA_H_ -#define BALLISTICA_ASSETS_DATA_ASSET_COMPONENT_DATA_H_ +#ifndef BALLISTICA_BASE_ASSETS_ASSET_H_ +#define BALLISTICA_BASE_ASSETS_ASSET_H_ #include #include -#include "ballistica/core/object.h" -#include "ballistica/platform/platform.h" +#include "ballistica/base/base.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::base { -/// Base class for loadable asset components. -class AssetComponentData : public Object { +/// Base class for loadable assets. +/// This represents the actual underlying data for the assets. +/// Representations of assets in scenes/ui-systems/etc. +/// will generally be other classes containing one of these. +class Asset : public Object { public: - AssetComponentData(); - ~AssetComponentData() override; + Asset(); + ~Asset() override; + + virtual auto GetAssetType() const -> AssetType = 0; + void Preload(bool already_locked = false); void Load(bool already_locked = false); void Unload(bool already_locked = false); auto preloaded() const -> bool { return preloaded_; } auto loaded() const -> bool { return preloaded_ && loaded_; } - virtual auto GetAssetType() const -> AssetType = 0; // Return name or another identifier. For debugging purposes. virtual auto GetName() const -> std::string { return "invalid"; } @@ -35,14 +41,14 @@ class AssetComponentData : public Object { class LockGuard { public: enum Type { kLock, kInheritLock, kDontLock }; - explicit LockGuard(AssetComponentData* data, Type type = kLock); + explicit LockGuard(Asset* data, Type type = kLock); ~LockGuard(); // Does this guard hold a lock? auto holds_lock() const -> bool { return holds_lock_; } private: - AssetComponentData* data_ = nullptr; + Asset* data_ = nullptr; bool holds_lock_ = false; }; @@ -97,23 +103,11 @@ class AssetComponentData : public Object { private: // Lock the component - components must be locked whenever using them. - void Lock() { - BA_DEBUG_FUNCTION_TIMER_BEGIN(); - mutex_.lock(); - assert(!locked_); - locked_ = true; - BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(20, GetName()); - } + void Lock(); // Unlock the component. each call to lock must be accompanied by one of // these. - void Unlock() { - BA_DEBUG_FUNCTION_TIMER_BEGIN(); - assert(locked_); - locked_ = false; - mutex_.unlock(); - BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(20, GetName()); - } + void Unlock(); bool locked_ = false; millisecs_t preload_start_time_ = 0; @@ -128,9 +122,9 @@ class AssetComponentData : public Object { bool preloaded_ = false; bool loaded_ = false; std::mutex mutex_; - BA_DISALLOW_CLASS_COPIES(AssetComponentData); + BA_DISALLOW_CLASS_COPIES(Asset); }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_ASSETS_DATA_ASSET_COMPONENT_DATA_H_ +#endif // BALLISTICA_BASE_ASSETS_ASSET_H_ diff --git a/src/ballistica/base/assets/assets.cc b/src/ballistica/base/assets/assets.cc new file mode 100644 index 00000000..3d47201e --- /dev/null +++ b/src/ballistica/base/assets/assets.cc @@ -0,0 +1,1627 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/assets/assets.h" + +#include "ballistica/base/app/app_mode.h" +#include "ballistica/base/assets/assets_server.h" +#include "ballistica/base/assets/collision_mesh_asset.h" +#include "ballistica/base/assets/data_asset.h" +#include "ballistica/base/assets/mesh_asset.h" +#include "ballistica/base/assets/sound_asset.h" +#include "ballistica/base/assets/texture_asset.h" +#include "ballistica/base/audio/audio_server.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/graphics/text/text_packer.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/json.h" +#include "ballistica/shared/python/python.h" + +namespace ballistica::base { + +static const bool kShowPruningInfo = false; + +#define SHOW_PRUNING_INFO 0 + +// Standard prune time for unused assets: 10 minutes (1000ms * 60 * 10). +#define STANDARD_ASSET_PRUNE_TIME 600000 + +// More aggressive prune time for dynamically-generated text-textures: 10 +// seconds. +#define TEXT_TEXTURE_PRUNE_TIME 10000 + +#define QR_TEXTURE_PRUNE_TIME 10000 + +// How long we should spend loading assets in each runPendingLoads() call. +#define PENDING_LOAD_PROCESS_TIME 5 + +Assets::Assets() { + asset_paths_.emplace_back(g_core->platform->GetDataDirectory() + BA_DIRSLASH + + "ba_data"); + for (bool& have_pending_load : have_pending_loads_) { + have_pending_load = false; + } + + InitSpecialChars(); +} + +void Assets::LoadSystemTexture(SysTextureID id, const char* name) { + assert(asset_lists_locked_); + system_textures_.push_back(GetTexture(name)); + assert(system_textures_.size() == static_cast(id) + 1); +} + +void Assets::LoadSystemCubeMapTexture(SysCubeMapTextureID id, + const char* name) { + assert(asset_lists_locked_); + system_cube_map_textures_.push_back(GetCubeMapTexture(name)); + assert(system_cube_map_textures_.size() == static_cast(id) + 1); +} + +void Assets::LoadSystemSound(SysSoundID id, const char* name) { + system_sounds_.push_back(GetSound(name)); + assert(system_sounds_.size() == static_cast(id) + 1); +} + +void Assets::LoadSystemData(SystemDataID id, const char* name) { + system_datas_.push_back(GetDataAsset(name)); + assert(system_datas_.size() == static_cast(id) + 1); +} + +void Assets::LoadSystemMesh(SysMeshID id, const char* name) { + system_meshes_.push_back(GetMesh(name)); + assert(system_meshes_.size() == static_cast(id) + 1); +} + +void Assets::StartLoading() { + assert(g_base->InLogicThread()); + assert(g_base); + assert(g_base->audio_server && g_base->assets_server + && g_base->graphics_server); + assert(g_base->graphics_server->texture_compression_types_are_set()); + assert(g_base->graphics_server->texture_quality_set()); + + assert(!asset_loads_allowed_); // We should only be called once. + asset_loads_allowed_ = true; + + // Just grab the lock once for all this stuff for efficiency. + AssetListLock lock; + + // System textures: + LoadSystemTexture(SysTextureID::kUIAtlas, "uiAtlas"); + LoadSystemTexture(SysTextureID::kButtonSquare, "buttonSquare"); + LoadSystemTexture(SysTextureID::kWhite, "white"); + LoadSystemTexture(SysTextureID::kFontSmall0, "fontSmall0"); + LoadSystemTexture(SysTextureID::kFontBig, "fontBig"); + LoadSystemTexture(SysTextureID::kCursor, "cursor"); + LoadSystemTexture(SysTextureID::kBoxingGlove, "boxingGlovesColor"); + LoadSystemTexture(SysTextureID::kShield, "shield"); + LoadSystemTexture(SysTextureID::kExplosion, "explosion"); + LoadSystemTexture(SysTextureID::kTextClearButton, "textClearButton"); + LoadSystemTexture(SysTextureID::kWindowHSmallVMed, "windowHSmallVMed"); + LoadSystemTexture(SysTextureID::kWindowHSmallVSmall, "windowHSmallVSmall"); + LoadSystemTexture(SysTextureID::kGlow, "glow"); + LoadSystemTexture(SysTextureID::kScrollWidget, "scrollWidget"); + LoadSystemTexture(SysTextureID::kScrollWidgetGlow, "scrollWidgetGlow"); + LoadSystemTexture(SysTextureID::kFlagPole, "flagPoleColor"); + LoadSystemTexture(SysTextureID::kScorch, "scorch"); + LoadSystemTexture(SysTextureID::kScorchBig, "scorchBig"); + LoadSystemTexture(SysTextureID::kShadow, "shadow"); + LoadSystemTexture(SysTextureID::kLight, "light"); + LoadSystemTexture(SysTextureID::kShadowSharp, "shadowSharp"); + LoadSystemTexture(SysTextureID::kLightSharp, "lightSharp"); + LoadSystemTexture(SysTextureID::kShadowSoft, "shadowSoft"); + LoadSystemTexture(SysTextureID::kLightSoft, "lightSoft"); + LoadSystemTexture(SysTextureID::kSparks, "sparks"); + LoadSystemTexture(SysTextureID::kEye, "eyeColor"); + LoadSystemTexture(SysTextureID::kEyeTint, "eyeColorTintMask"); + LoadSystemTexture(SysTextureID::kFuse, "fuse"); + LoadSystemTexture(SysTextureID::kShrapnel1, "shrapnel1Color"); + LoadSystemTexture(SysTextureID::kSmoke, "smoke"); + LoadSystemTexture(SysTextureID::kCircle, "circle"); + LoadSystemTexture(SysTextureID::kCircleOutline, "circleOutline"); + LoadSystemTexture(SysTextureID::kCircleNoAlpha, "circleNoAlpha"); + LoadSystemTexture(SysTextureID::kCircleOutlineNoAlpha, + "circleOutlineNoAlpha"); + LoadSystemTexture(SysTextureID::kCircleShadow, "circleShadow"); + LoadSystemTexture(SysTextureID::kSoftRect, "softRect"); + LoadSystemTexture(SysTextureID::kSoftRect2, "softRect2"); + LoadSystemTexture(SysTextureID::kSoftRectVertical, "softRectVertical"); + LoadSystemTexture(SysTextureID::kStartButton, "startButton"); + LoadSystemTexture(SysTextureID::kBombButton, "bombButton"); + LoadSystemTexture(SysTextureID::kOuyaAButton, "ouyaAButton"); + LoadSystemTexture(SysTextureID::kBackIcon, "backIcon"); + LoadSystemTexture(SysTextureID::kNub, "nub"); + LoadSystemTexture(SysTextureID::kArrow, "arrow"); + LoadSystemTexture(SysTextureID::kMenuButton, "menuButton"); + LoadSystemTexture(SysTextureID::kUsersButton, "usersButton"); + LoadSystemTexture(SysTextureID::kActionButtons, "actionButtons"); + LoadSystemTexture(SysTextureID::kTouchArrows, "touchArrows"); + LoadSystemTexture(SysTextureID::kTouchArrowsActions, "touchArrowsActions"); + LoadSystemTexture(SysTextureID::kRGBStripes, "rgbStripes"); + LoadSystemTexture(SysTextureID::kUIAtlas2, "uiAtlas2"); + LoadSystemTexture(SysTextureID::kFontSmall1, "fontSmall1"); + LoadSystemTexture(SysTextureID::kFontSmall2, "fontSmall2"); + LoadSystemTexture(SysTextureID::kFontSmall3, "fontSmall3"); + LoadSystemTexture(SysTextureID::kFontSmall4, "fontSmall4"); + LoadSystemTexture(SysTextureID::kFontSmall5, "fontSmall5"); + LoadSystemTexture(SysTextureID::kFontSmall6, "fontSmall6"); + LoadSystemTexture(SysTextureID::kFontSmall7, "fontSmall7"); + LoadSystemTexture(SysTextureID::kFontExtras, "fontExtras"); + LoadSystemTexture(SysTextureID::kFontExtras2, "fontExtras2"); + LoadSystemTexture(SysTextureID::kFontExtras3, "fontExtras3"); + LoadSystemTexture(SysTextureID::kFontExtras4, "fontExtras4"); + LoadSystemTexture(SysTextureID::kCharacterIconMask, "characterIconMask"); + LoadSystemTexture(SysTextureID::kBlack, "black"); + LoadSystemTexture(SysTextureID::kWings, "wings"); + + // System cube map textures: + LoadSystemCubeMapTexture(SysCubeMapTextureID::kReflectionChar, + "reflectionChar#"); + LoadSystemCubeMapTexture(SysCubeMapTextureID::kReflectionPowerup, + "reflectionPowerup#"); + LoadSystemCubeMapTexture(SysCubeMapTextureID::kReflectionSoft, + "reflectionSoft#"); + LoadSystemCubeMapTexture(SysCubeMapTextureID::kReflectionSharp, + "reflectionSharp#"); + LoadSystemCubeMapTexture(SysCubeMapTextureID::kReflectionSharper, + "reflectionSharper#"); + LoadSystemCubeMapTexture(SysCubeMapTextureID::kReflectionSharpest, + "reflectionSharpest#"); + + // System sounds: + LoadSystemSound(SysSoundID::kDeek, "deek"); + LoadSystemSound(SysSoundID::kBlip, "blip"); + LoadSystemSound(SysSoundID::kBlank, "blank"); + LoadSystemSound(SysSoundID::kPunch, "punch01"); + LoadSystemSound(SysSoundID::kClick, "click01"); + LoadSystemSound(SysSoundID::kErrorBeep, "error"); + LoadSystemSound(SysSoundID::kSwish, "swish"); + LoadSystemSound(SysSoundID::kSwish2, "swish2"); + LoadSystemSound(SysSoundID::kSwish3, "swish3"); + LoadSystemSound(SysSoundID::kTap, "tap"); + LoadSystemSound(SysSoundID::kCorkPop, "corkPop"); + LoadSystemSound(SysSoundID::kGunCock, "gunCocking"); + LoadSystemSound(SysSoundID::kTickingCrazy, "tickingCrazy"); + LoadSystemSound(SysSoundID::kSparkle, "sparkle01"); + LoadSystemSound(SysSoundID::kSparkle2, "sparkle02"); + LoadSystemSound(SysSoundID::kSparkle3, "sparkle03"); + + // System datas: + // (crickets) + + // System meshes: + LoadSystemMesh(SysMeshID::kButtonSmallTransparent, "buttonSmallTransparent"); + LoadSystemMesh(SysMeshID::kButtonSmallOpaque, "buttonSmallOpaque"); + LoadSystemMesh(SysMeshID::kButtonMediumTransparent, + "buttonMediumTransparent"); + LoadSystemMesh(SysMeshID::kButtonMediumOpaque, "buttonMediumOpaque"); + LoadSystemMesh(SysMeshID::kButtonBackTransparent, "buttonBackTransparent"); + LoadSystemMesh(SysMeshID::kButtonBackOpaque, "buttonBackOpaque"); + LoadSystemMesh(SysMeshID::kButtonBackSmallTransparent, + "buttonBackSmallTransparent"); + LoadSystemMesh(SysMeshID::kButtonBackSmallOpaque, "buttonBackSmallOpaque"); + LoadSystemMesh(SysMeshID::kButtonTabTransparent, "buttonTabTransparent"); + LoadSystemMesh(SysMeshID::kButtonTabOpaque, "buttonTabOpaque"); + LoadSystemMesh(SysMeshID::kButtonLargeTransparent, "buttonLargeTransparent"); + LoadSystemMesh(SysMeshID::kButtonLargeOpaque, "buttonLargeOpaque"); + LoadSystemMesh(SysMeshID::kButtonLargerTransparent, + "buttonLargerTransparent"); + LoadSystemMesh(SysMeshID::kButtonLargerOpaque, "buttonLargerOpaque"); + LoadSystemMesh(SysMeshID::kButtonSquareTransparent, + "buttonSquareTransparent"); + LoadSystemMesh(SysMeshID::kButtonSquareOpaque, "buttonSquareOpaque"); + LoadSystemMesh(SysMeshID::kCheckTransparent, "checkTransparent"); + LoadSystemMesh(SysMeshID::kScrollBarThumbTransparent, + "scrollBarThumbTransparent"); + LoadSystemMesh(SysMeshID::kScrollBarThumbOpaque, "scrollBarThumbOpaque"); + LoadSystemMesh(SysMeshID::kScrollBarThumbSimple, "scrollBarThumbSimple"); + LoadSystemMesh(SysMeshID::kScrollBarThumbShortTransparent, + "scrollBarThumbShortTransparent"); + LoadSystemMesh(SysMeshID::kScrollBarThumbShortOpaque, + "scrollBarThumbShortOpaque"); + LoadSystemMesh(SysMeshID::kScrollBarThumbShortSimple, + "scrollBarThumbShortSimple"); + LoadSystemMesh(SysMeshID::kScrollBarTroughTransparent, + "scrollBarTroughTransparent"); + LoadSystemMesh(SysMeshID::kTextBoxTransparent, "textBoxTransparent"); + LoadSystemMesh(SysMeshID::kImage1x1, "image1x1"); + LoadSystemMesh(SysMeshID::kImage1x1FullScreen, "image1x1FullScreen"); + LoadSystemMesh(SysMeshID::kImage2x1, "image2x1"); + LoadSystemMesh(SysMeshID::kImage4x1, "image4x1"); + LoadSystemMesh(SysMeshID::kImage16x1, "image16x1"); +#if BA_VR_BUILD + LoadSystemMesh(SysMeshID::kImage1x1VRFullScreen, "image1x1VRFullScreen"); + LoadSystemMesh(SysMeshID::kVROverlay, "vrOverlay"); + LoadSystemMesh(SysMeshID::kVRFade, "vrFade"); +#endif // BA_VR_BUILD + LoadSystemMesh(SysMeshID::kOverlayGuide, "overlayGuide"); + LoadSystemMesh(SysMeshID::kWindowHSmallVMedTransparent, + "windowHSmallVMedTransparent"); + LoadSystemMesh(SysMeshID::kWindowHSmallVMedOpaque, "windowHSmallVMedOpaque"); + LoadSystemMesh(SysMeshID::kWindowHSmallVSmallTransparent, + "windowHSmallVSmallTransparent"); + LoadSystemMesh(SysMeshID::kWindowHSmallVSmallOpaque, + "windowHSmallVSmallOpaque"); + LoadSystemMesh(SysMeshID::kSoftEdgeOutside, "softEdgeOutside"); + LoadSystemMesh(SysMeshID::kSoftEdgeInside, "softEdgeInside"); + LoadSystemMesh(SysMeshID::kBoxingGlove, "boxingGlove"); + LoadSystemMesh(SysMeshID::kShield, "shield"); + LoadSystemMesh(SysMeshID::kFlagPole, "flagPole"); + LoadSystemMesh(SysMeshID::kFlagStand, "flagStand"); + LoadSystemMesh(SysMeshID::kScorch, "scorch"); + LoadSystemMesh(SysMeshID::kEyeBall, "eyeBall"); + LoadSystemMesh(SysMeshID::kEyeBallIris, "eyeBallIris"); + LoadSystemMesh(SysMeshID::kEyeLid, "eyeLid"); + LoadSystemMesh(SysMeshID::kHairTuft1, "hairTuft1"); + LoadSystemMesh(SysMeshID::kHairTuft1b, "hairTuft1b"); + LoadSystemMesh(SysMeshID::kHairTuft2, "hairTuft2"); + LoadSystemMesh(SysMeshID::kHairTuft3, "hairTuft3"); + LoadSystemMesh(SysMeshID::kHairTuft4, "hairTuft4"); + LoadSystemMesh(SysMeshID::kShrapnel1, "shrapnel1"); + LoadSystemMesh(SysMeshID::kShrapnelSlime, "shrapnelSlime"); + LoadSystemMesh(SysMeshID::kShrapnelBoard, "shrapnelBoard"); + LoadSystemMesh(SysMeshID::kShockWave, "shockWave"); + LoadSystemMesh(SysMeshID::kFlash, "flash"); + LoadSystemMesh(SysMeshID::kCylinder, "cylinder"); + LoadSystemMesh(SysMeshID::kArrowFront, "arrowFront"); + LoadSystemMesh(SysMeshID::kArrowBack, "arrowBack"); + LoadSystemMesh(SysMeshID::kActionButtonLeft, "actionButtonLeft"); + LoadSystemMesh(SysMeshID::kActionButtonTop, "actionButtonTop"); + LoadSystemMesh(SysMeshID::kActionButtonRight, "actionButtonRight"); + LoadSystemMesh(SysMeshID::kActionButtonBottom, "actionButtonBottom"); + LoadSystemMesh(SysMeshID::kBox, "box"); + LoadSystemMesh(SysMeshID::kLocator, "locator"); + LoadSystemMesh(SysMeshID::kLocatorBox, "locatorBox"); + LoadSystemMesh(SysMeshID::kLocatorCircle, "locatorCircle"); + LoadSystemMesh(SysMeshID::kLocatorCircleOutline, "locatorCircleOutline"); + LoadSystemMesh(SysMeshID::kCrossOut, "crossOut"); + LoadSystemMesh(SysMeshID::kWing, "wing"); +} + +void Assets::PrintLoadInfo() { + std::string s; + char buffer[256]; + int num = 1; + + // Need to lock lists while iterating over them. + AssetListLock lock; + s = "Assets load results: (all times in milliseconds):\n"; + snprintf(buffer, sizeof(buffer), " %-50s %10s %10s", "FILE", + "PRELOAD_TIME", "LOAD_TIME"); + s += buffer; + Log(LogLevel::kInfo, s); + millisecs_t total_preload_time = 0; + millisecs_t total_load_time = 0; + assert(asset_lists_locked_); + for (auto&& i : meshes_) { + millisecs_t preload_time = i.second->preload_time(); + millisecs_t load_time = i.second->load_time(); + total_preload_time += preload_time; + total_load_time += load_time; + snprintf(buffer, sizeof(buffer), "%-3d %-50s %10d %10d", num, + i.second->GetName().c_str(), + static_cast_check_fit(preload_time), + static_cast_check_fit(load_time)); + Log(LogLevel::kInfo, buffer); + num++; + } + assert(asset_lists_locked_); + for (auto&& i : collision_meshes_) { + millisecs_t preload_time = i.second->preload_time(); + millisecs_t load_time = i.second->load_time(); + total_preload_time += preload_time; + total_load_time += load_time; + snprintf(buffer, sizeof(buffer), "%-3d %-50s %10d %10d", num, + i.second->GetName().c_str(), + static_cast_check_fit(preload_time), + static_cast_check_fit(load_time)); + Log(LogLevel::kInfo, buffer); + num++; + } + assert(asset_lists_locked_); + for (auto&& i : sounds_) { + millisecs_t preload_time = i.second->preload_time(); + millisecs_t load_time = i.second->load_time(); + total_preload_time += preload_time; + total_load_time += load_time; + snprintf(buffer, sizeof(buffer), "%-3d %-50s %10d %10d", num, + i.second->GetName().c_str(), + static_cast_check_fit(preload_time), + static_cast_check_fit(load_time)); + Log(LogLevel::kInfo, buffer); + num++; + } + assert(asset_lists_locked_); + for (auto&& i : datas_) { + millisecs_t preload_time = i.second->preload_time(); + millisecs_t load_time = i.second->load_time(); + total_preload_time += preload_time; + total_load_time += load_time; + snprintf(buffer, sizeof(buffer), "%-3d %-50s %10d %10d", num, + i.second->GetName().c_str(), + static_cast_check_fit(preload_time), + static_cast_check_fit(load_time)); + Log(LogLevel::kInfo, buffer); + num++; + } + assert(asset_lists_locked_); + for (auto&& i : textures_) { + millisecs_t preload_time = i.second->preload_time(); + millisecs_t load_time = i.second->load_time(); + total_preload_time += preload_time; + total_load_time += load_time; + snprintf(buffer, sizeof(buffer), "%-3d %-50s %10d %10d", num, + i.second->file_name_full().c_str(), + static_cast_check_fit(preload_time), + static_cast_check_fit(load_time)); + Log(LogLevel::kInfo, buffer); + num++; + } + snprintf(buffer, sizeof(buffer), + "Total preload time (loading data from disk): %i\nTotal load time " + "(feeding data to OpenGL, etc): %i", + static_cast(total_preload_time), + static_cast(total_load_time)); + Log(LogLevel::kInfo, buffer); +} + +void Assets::MarkAllAssetsForLoad() { + assert(g_base->InLogicThread()); + + // Need to keep lists locked while iterating over them. + AssetListLock m_lock; + for (auto&& i : textures_) { + if (!i.second->preloaded()) { + Asset::LockGuard lock(i.second.Get()); + have_pending_loads_[static_cast(AssetType::kTexture)] = true; + MarkAssetForLoad(i.second.Get()); + } + } + for (auto&& i : text_textures_) { + if (!i.second->preloaded()) { + Asset::LockGuard lock(i.second.Get()); + have_pending_loads_[static_cast(AssetType::kTexture)] = true; + MarkAssetForLoad(i.second.Get()); + } + } + for (auto&& i : qr_textures_) { + if (!i.second->preloaded()) { + Asset::LockGuard lock(i.second.Get()); + have_pending_loads_[static_cast(AssetType::kTexture)] = true; + MarkAssetForLoad(i.second.Get()); + } + } + for (auto&& i : meshes_) { + if (!i.second->preloaded()) { + Asset::LockGuard lock(i.second.Get()); + have_pending_loads_[static_cast(AssetType::kMesh)] = true; + MarkAssetForLoad(i.second.Get()); + } + } +} + +// Call this from the graphics thread to immediately unload all +// assets used by it. (for when GL context gets lost, etc). +void Assets::UnloadRendererBits(bool do_textures, bool do_meshes) { + assert(g_base->InGraphicsThread()); + // need to keep lists locked while iterating over them.. + AssetListLock m_lock; + if (do_textures) { + assert(asset_lists_locked_); + for (auto&& i : textures_) { + Asset::LockGuard lock(i.second.Get()); + i.second->Unload(true); + } + for (auto&& i : text_textures_) { + Asset::LockGuard lock(i.second.Get()); + i.second->Unload(true); + } + for (auto&& i : qr_textures_) { + Asset::LockGuard lock(i.second.Get()); + i.second->Unload(true); + } + } + if (do_meshes) { + for (auto&& i : meshes_) { + Asset::LockGuard lock(i.second.Get()); + i.second->Unload(true); + } + } +} + +auto Assets::GetMesh(const std::string& file_name) -> Object::Ref { + return GetAsset(file_name, &meshes_); +} + +auto Assets::GetSound(const std::string& file_name) -> Object::Ref { + return GetAsset(file_name, &sounds_); +} + +auto Assets::GetDataAsset(const std::string& file_name) + -> Object::Ref { + return GetAsset(file_name, &datas_); +} + +auto Assets::GetCollisionMesh(const std::string& file_name) + -> Object::Ref { + return GetAsset(file_name, &collision_meshes_); +} + +template +auto Assets::GetAsset(const std::string& file_name, + std::unordered_map >* c_list) + -> Object::Ref { + assert(g_base->InLogicThread()); + assert(asset_lists_locked_); + assert(asset_loads_allowed_); + auto i = c_list->find(file_name); + if (i != c_list->end()) { + return Object::Ref(i->second.Get()); + } else { + auto d(Object::New(file_name)); + (*c_list)[file_name] = d; + { + Asset::LockGuard lock(d.Get()); + have_pending_loads_[static_cast(d->GetAssetType())] = true; + MarkAssetForLoad(d.Get()); + } + d->set_last_used_time(g_core->GetAppTimeMillisecs()); + return d; + } +} + +auto Assets::GetTexture(TextPacker* packer) -> Object::Ref { + assert(g_base->InLogicThread()); + assert(asset_lists_locked_); + const std::string& hash(packer->hash()); + auto i = text_textures_.find(hash); + if (i != text_textures_.end()) { + return Object::Ref(i->second.Get()); + } else { + auto d(Object::New(packer)); + text_textures_[hash] = d; + { + Asset::LockGuard lock(d.Get()); + have_pending_loads_[static_cast(d->GetAssetType())] = true; + MarkAssetForLoad(d.Get()); + } + d->set_last_used_time(g_core->GetAppTimeMillisecs()); + return d; + } +} + +auto Assets::GetQRCodeTexture(const std::string& url) + -> Object::Ref { + assert(g_base->InLogicThread()); + assert(asset_lists_locked_); + auto i = qr_textures_.find(url); + if (i != qr_textures_.end()) { + return Object::Ref(i->second.Get()); + } else { + auto d(Object::New(url)); + qr_textures_[url] = d; + { + Asset::LockGuard lock(d.Get()); + have_pending_loads_[static_cast(d->GetAssetType())] = true; + MarkAssetForLoad(d.Get()); + } + d->set_last_used_time(g_core->GetAppTimeMillisecs()); + return d; + } +} + +// Eww can't recycle GetComponent here since we need extra stuff (tex-type arg) +// ..should fix. +auto Assets::GetCubeMapTexture(const std::string& file_name) + -> Object::Ref { + assert(g_base->InLogicThread()); + assert(asset_lists_locked_); + auto i = textures_.find(file_name); + if (i != textures_.end()) { + return Object::Ref(i->second.Get()); + } else { + auto d(Object::New(file_name, TextureType::kCubeMap, + TextureMinQuality::kLow)); + textures_[file_name] = d; + { + Asset::LockGuard lock(d.Get()); + have_pending_loads_[static_cast(d->GetAssetType())] = true; + MarkAssetForLoad(d.Get()); + } + d->set_last_used_time(g_core->GetAppTimeMillisecs()); + return d; + } +} + +// Eww; can't recycle GetComponent here since we need extra stuff (quality +// settings, etc). Should fix. +auto Assets::GetTexture(const std::string& file_name) + -> Object::Ref { + assert(g_base->InLogicThread()); + assert(asset_lists_locked_); + auto i = textures_.find(file_name); + if (i != textures_.end()) { + return Object::Ref(i->second.Get()); + } else { + static std::set* quality_map_medium = nullptr; + static std::set* quality_map_high = nullptr; + static bool quality_maps_inited = false; + + // TEMP - we currently set min quality based on filename; + // in the future this will be stored with the texture package or whatnot + if (!quality_maps_inited) { + quality_maps_inited = true; + quality_map_medium = new std::set(); + quality_map_high = new std::set(); + const char* vals_med[] = { + "fontSmall0", "fontSmall1", "fontSmall2", "fontSmall3", "fontSmall4", + "fontSmall5", "fontSmall6", "fontSmall7", "fontExtras", nullptr}; + + const char* vals_high[] = {"frostyIcon", "jackIcon", "melIcon", + "santaIcon", "ninjaIcon", "neoSpazIcon", + "zoeIcon", "kronkIcon", "scrollWidgetGlow", + "glow", nullptr}; + + for (const char** val3 = vals_med; *val3 != nullptr; val3++) { + quality_map_medium->insert(*val3); + } + for (const char** val2 = vals_high; *val2 != nullptr; val2++) { + quality_map_high->insert(*val2); + } + } + + TextureMinQuality min_quality = TextureMinQuality::kLow; + if (quality_map_medium->find(file_name) != quality_map_medium->end()) { + min_quality = TextureMinQuality::kMedium; + } else if (quality_map_high->find(file_name) != quality_map_high->end()) { + min_quality = TextureMinQuality::kHigh; + } + + auto d(Object::New(file_name, TextureType::k2D, min_quality)); + textures_[file_name] = d; + { + Asset::LockGuard lock(d.Get()); + have_pending_loads_[static_cast(d->GetAssetType())] = true; + MarkAssetForLoad(d.Get()); + } + d->set_last_used_time(g_core->GetAppTimeMillisecs()); + return d; + } +} + +void Assets::MarkAssetForLoad(Asset* c) { + assert(g_base->InLogicThread()); + + assert(c->locked()); + + // *allocate* a reference as a standalone pointer so we can be + // sure this guy sticks around until it's been sent all the way + // through the preload/load cycle. (since other threads will be touching it) + // once it makes it back to us we can delete the ref (in + // ClearPendingLoadsDoneList) + + auto asset_ref_ptr = new Object::Ref(c); + g_base->assets_server->PushPendingPreload(asset_ref_ptr); +} + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "UnreachableCode" +#pragma ide diagnostic ignored "ConstantFunctionResult" + +auto Assets::GetMeshPendingLoadCount() -> int { + if (!have_pending_loads_[static_cast(AssetType::kMesh)]) { + return 0; + } + AssetListLock lock; + int total = GetAssetPendingLoadCount(&meshes_, AssetType::kMesh); + if (total == 0) { + // When fully loaded, stop counting. + have_pending_loads_[static_cast(AssetType::kMesh)] = false; + } + return total; +} + +auto Assets::GetTexturePendingLoadCount() -> int { + if (!have_pending_loads_[static_cast(AssetType::kTexture)]) { + return 0; + } + AssetListLock lock; + int total = (GetAssetPendingLoadCount(&textures_, AssetType::kTexture) + + GetAssetPendingLoadCount(&text_textures_, AssetType::kTexture) + + GetAssetPendingLoadCount(&qr_textures_, AssetType::kTexture)); + if (total == 0) { + // When fully loaded, stop counting. + have_pending_loads_[static_cast(AssetType::kTexture)] = false; + } + return total; +} + +auto Assets::GetSoundPendingLoadCount() -> int { + if (!have_pending_loads_[static_cast(AssetType::kSound)]) { + return 0; + } + AssetListLock lock; + int total = GetAssetPendingLoadCount(&sounds_, AssetType::kSound); + if (total == 0) { + // When fully loaded, stop counting. + have_pending_loads_[static_cast(AssetType::kSound)] = false; + } + return total; +} + +auto Assets::GetDataPendingLoadCount() -> int { + if (!have_pending_loads_[static_cast(AssetType::kData)]) { + return 0; + } + AssetListLock lock; + int total = GetAssetPendingLoadCount(&datas_, AssetType::kData); + if (total == 0) { + // When fully loaded, stop counting. + have_pending_loads_[static_cast(AssetType::kData)] = false; + } + return total; +} + +auto Assets::GetCollisionMeshPendingLoadCount() -> int { + if (!have_pending_loads_[static_cast(AssetType::kCollisionMesh)]) { + return 0; + } + AssetListLock lock; + int total = + GetAssetPendingLoadCount(&collision_meshes_, AssetType::kCollisionMesh); + if (total == 0) { + // When fully loaded, stop counting. + have_pending_loads_[static_cast(AssetType::kCollisionMesh)] = false; + } + return total; +} + +#pragma clang diagnostic pop + +auto Assets::GetGraphicalPendingLoadCount() -> int { + // Each of these calls lock the asset-lists so we don't. + return GetMeshPendingLoadCount() + GetTexturePendingLoadCount(); +} + +auto Assets::GetPendingLoadCount() -> int { + // Each of these calls lock the asset-lists so we don't. + return GetMeshPendingLoadCount() + GetTexturePendingLoadCount() + + GetDataPendingLoadCount() + GetSoundPendingLoadCount() + + GetCollisionMeshPendingLoadCount(); +} + +template +auto Assets::GetAssetPendingLoadCount( + std::unordered_map >* t_list, AssetType type) + -> int { + assert(g_base->InLogicThread()); + assert(asset_lists_locked_); + + int c = 0; + for (auto&& i : (*t_list)) { + if (i.second.Exists()) { + if (i.second->TryLock()) { + Asset::LockGuard lock(i.second.Get(), + Asset::LockGuard::Type::kInheritLock); + if (!i.second->loaded()) { + c++; + } + } else { + c++; + } + } + } + return c; +} + +// Runs the pending loads that need to run from the audio thread. +auto Assets::RunPendingAudioLoads() -> bool { + assert(g_base->InAudioThread()); + return RunPendingLoadList(&pending_loads_sounds_); +} + +// Runs the pending loads that need to run from the graphics thread. +auto Assets::RunPendingGraphicsLoads() -> bool { + assert(g_base->InGraphicsThread()); + return RunPendingLoadList(&pending_loads_graphics_); +} + +// Runs the pending loads that run in the main thread. Also clears the list of +// done loads. +auto Assets::RunPendingLoadsLogicThread() -> bool { + assert(g_base->InLogicThread()); + return RunPendingLoadList(&pending_loads_other_); +} + +template +auto Assets::RunPendingLoadList(std::vector*>* c_list) -> bool { + bool flush = false; + millisecs_t starttime = g_core->GetAppTimeMillisecs(); + + std::vector*> l; + std::vector*> l_unfinished; + std::vector*> l_finished; + { + std::scoped_lock lock(pending_load_list_mutex_); + + // If we're already out of time. + if (!flush + && g_core->GetAppTimeMillisecs() - starttime + > PENDING_LOAD_PROCESS_TIME) { + bool return_val = (!c_list->empty()); + return return_val; + } + + // Save time if there's nothing to load. + if (c_list->empty()) { + return false; + } + + // Pull the contents of c_list and set it to empty. + l.swap(*c_list); + } + + // Run loads on our list until either the list is empty or we're out of time + // (don't want to block here for very long...) + // We should also think about the fact that even if a load is quick here it + // may add work on the graphics thread/etc so maybe we should add other + // restrictions. + bool out_of_time = false; + if (!l.empty()) { + while (true) { + for (auto i = l.begin(); i != l.end(); i++) { + if (!out_of_time) { + (***i).Load(false); + + // If the load finished, pop it on our "done-loading" list.. otherwise + // keep it around. + l_finished.push_back(*i); // else l_unfinished.push_back(*i); + if (g_core->GetAppTimeMillisecs() - starttime + > PENDING_LOAD_PROCESS_TIME + && !flush) { + out_of_time = true; + } + } else { + // Already out of time - just save this one for later. + l_unfinished.push_back(*i); + } + } + l = l_unfinished; + l_unfinished.clear(); + if (l.empty() || out_of_time) { + break; + } + } + } + + // Now add unfinished ones back onto the original list and finished ones into + // the done list. + { + std::scoped_lock lock(pending_load_list_mutex_); + for (auto&& i : l) { + c_list->push_back(i); + } + for (auto&& i : l_finished) { + pending_loads_done_.push_back(i); + } + } + + // If we dumped anything on the pending loads done list, shake the logic + // thread to tell it to kill the reference. + if (!l_finished.empty()) { + assert(g_base->logic); + g_base->logic->event_loop()->PushCall( + [] { g_base->assets->ClearPendingLoadsDoneList(); }); + } + return (!l.empty()); +} + +void Assets::Prune(int level) { + assert(g_base->InLogicThread()); + millisecs_t current_time = g_core->GetAppTimeMillisecs(); + + // Need lists locked while accessing/modifying them. + AssetListLock lock; + + // We can specify level for more aggressive pruning (during memory warnings + // and whatnot). + millisecs_t standard_asset_prune_time = STANDARD_ASSET_PRUNE_TIME; + millisecs_t text_texture_prune_time = TEXT_TEXTURE_PRUNE_TIME; + millisecs_t qr_texture_prune_time = QR_TEXTURE_PRUNE_TIME; + switch (level) { + case 1: + standard_asset_prune_time = 120000; // 2 min + text_texture_prune_time = 1000; // 1 sec + qr_texture_prune_time = 1000; // 1 sec + break; + case 2: + standard_asset_prune_time = 30000; // 30 sec + text_texture_prune_time = 1000; // 1 sec + qr_texture_prune_time = 1000; // 1 sec + break; + case 3: + standard_asset_prune_time = 5000; // 5 sec + text_texture_prune_time = 1000; // 1 sec + qr_texture_prune_time = 1000; // 1 sec + break; + default: + break; + } + + std::vector*> graphics_thread_unloads; + std::vector*> audio_thread_unloads; + + assert(asset_lists_locked_); + auto old_texture_count = textures_.size(); + auto old_text_texture_count = text_textures_.size(); + auto old_qr_texture_count = qr_textures_.size(); + auto old_mesh_count = meshes_.size(); + auto old_collision_mesh_count = collision_meshes_.size(); + auto old_sound_count = sounds_.size(); + + // Prune textures. + assert(asset_lists_locked_); + for (auto i = textures_.begin(); i != textures_.end();) { + TextureAsset* texture = i->second.Get(); + // Attempt to prune if there are no references remaining except our own and + // its been a while since it was used. + if (current_time - texture->last_used_time() > standard_asset_prune_time + && (texture->object_strong_ref_count() <= 1)) { + // If its preloaded/loaded we need to ask the graphics thread to unload it + // first. + if (texture->preloaded()) { + // Allocate a reference to keep this texture_data alive while the unload + // is happening. + graphics_thread_unloads.push_back(new Object::Ref(texture)); + auto i_next = i; + i_next++; + textures_.erase(i); + i = i_next; + } + } else { + i++; + } + } + + // Prune text-textures more aggressively since we may generate lots of them + // FIXME - we may want to prune based on total number of these instead of + // time. + assert(asset_lists_locked_); + for (auto i = text_textures_.begin(); i != text_textures_.end();) { + TextureAsset* texture = i->second.Get(); + // Attempt to prune if there are no references remaining except our own and + // its been a while since it was used. + if (current_time - texture->last_used_time() > text_texture_prune_time + && (texture->object_strong_ref_count() <= 1)) { + // If its preloaded/loaded we need to ask the graphics thread to unload it + // first. + if (texture->preloaded()) { + // Allocate a reference to keep this texture_data alive while the unload + // is happening. + graphics_thread_unloads.push_back(new Object::Ref(texture)); + auto i_next = i; + i_next++; + text_textures_.erase(i); + i = i_next; + } + } else { + i++; + } + } + + // Prune qr-textures. + assert(asset_lists_locked_); + for (auto i = qr_textures_.begin(); i != qr_textures_.end();) { + TextureAsset* texture = i->second.Get(); + // Attempt to prune if there are no references remaining except our own and + // its been a while since it was used. + if (current_time - texture->last_used_time() > qr_texture_prune_time + && (texture->object_strong_ref_count() <= 1)) { + // If its preloaded/loaded we need to ask the graphics thread to unload it + // first. + if (texture->preloaded()) { + // Allocate a reference to keep this texture_data alive while the unload + // is happening. + graphics_thread_unloads.push_back(new Object::Ref(texture)); + auto i_next = i; + i_next++; + qr_textures_.erase(i); + i = i_next; + } + } else { + i++; + } + } + + // Prune meshes. + assert(asset_lists_locked_); + for (auto i = meshes_.begin(); i != meshes_.end();) { + MeshAsset* mesh = i->second.Get(); + // Attempt to prune if there are no references remaining except our own and + // its been a while since it was used. + if (current_time - mesh->last_used_time() > standard_asset_prune_time + && (mesh->object_strong_ref_count() <= 1)) { + // If its preloaded/loaded we need to ask the graphics thread to unload it + // first. + if (mesh->preloaded()) { + // Allocate a reference to keep this mesh_data alive while the unload + // is happening. + graphics_thread_unloads.push_back(new Object::Ref(mesh)); + auto i_next = i; + i_next++; + meshes_.erase(i); + i = i_next; + } + } else { + i++; + } + } + + // Prune collision-meshes. + assert(asset_lists_locked_); + for (auto i = collision_meshes_.begin(); i != collision_meshes_.end();) { + CollisionMeshAsset* mesh = i->second.Get(); + // Attempt to prune if there are no references remaining except our own and + // its been a while since it was used. + if (current_time - mesh->last_used_time() > standard_asset_prune_time + && (mesh->object_strong_ref_count() <= 1)) { + // We can unload it immediately since that happens here in the logic + // thread. + mesh->Unload(); + auto i_next = i; + ++i_next; + collision_meshes_.erase(i); + i = i_next; + } else { + i++; + } + } + + // Prune sounds. + // (DISABLED FOR NOW - getting AL errors; need to better determine which + // sounds are still in active use by OpenAL and ensure references exist for + // them somewhere while that is the case + if (explicit_bool(false)) { + assert(asset_lists_locked_); + for (auto i = sounds_.begin(); i != sounds_.end();) { + SoundAsset* sound = i->second.Get(); + // Attempt to prune if there are no references remaining except our own + // and its been a while since it was used. + if (current_time - sound->last_used_time() > standard_asset_prune_time + && (sound->object_strong_ref_count() <= 1)) { + // If its preloaded/loaded we need to ask the audio thread to unload + // it first. + if (sound->preloaded()) { + // Allocate a reference to keep this sound_data alive while the unload + // is happening. + audio_thread_unloads.push_back(new Object::Ref(sound)); + auto i_next = i; + i_next++; + sounds_.erase(i); + i = i_next; + } + } else { + i++; + } + } + } + + if (!graphics_thread_unloads.empty()) { + g_base->graphics_server->PushComponentUnloadCall(graphics_thread_unloads); + } + if (!audio_thread_unloads.empty()) { + g_base->audio_server->PushComponentUnloadCall(audio_thread_unloads); + } + + if (kShowPruningInfo) { + assert(asset_lists_locked_); + if (textures_.size() != old_texture_count) { + Log(LogLevel::kInfo, "Textures pruned from " + + std::to_string(old_texture_count) + " to " + + std::to_string(textures_.size())); + } + if (text_textures_.size() != old_text_texture_count) { + Log(LogLevel::kInfo, "TextTextures pruned from " + + std::to_string(old_text_texture_count) + " to " + + std::to_string(text_textures_.size())); + } + if (qr_textures_.size() != old_qr_texture_count) { + Log(LogLevel::kInfo, "QrTextures pruned from " + + std::to_string(old_qr_texture_count) + " to " + + std::to_string(qr_textures_.size())); + } + if (meshes_.size() != old_mesh_count) { + Log(LogLevel::kInfo, "Meshes pruned from " + + std::to_string(old_mesh_count) + " to " + + std::to_string(meshes_.size())); + } + if (collision_meshes_.size() != old_collision_mesh_count) { + Log(LogLevel::kInfo, "CollisionMeshes pruned from " + + std::to_string(old_collision_mesh_count) + + " to " + + std::to_string(collision_meshes_.size())); + } + if (sounds_.size() != old_sound_count) { + Log(LogLevel::kInfo, "Sounds pruned from " + + std::to_string(old_sound_count) + " to " + + std::to_string(sounds_.size())); + } + } +} + +auto Assets::FindAssetFile(FileType type, const std::string& name) + -> std::string { + std::string file_out; + + // We don't protect package-path access so make sure its always from here. + assert(g_base->InLogicThread()); + + const char* ext = ""; + const char* prefix = ""; + + switch (type) { + case FileType::kSound: + if (g_core->HeadlessMode()) { + return "headless_dummy_path.sound"; + } + prefix = "audio/"; + ext = ".ogg"; + break; + + case FileType::kMesh: + if (g_core->HeadlessMode()) { + return "headless_dummy_path.mesh"; + } + prefix = "meshes/"; + ext = ".bob"; + break; + + case FileType::kCollisionMesh: + prefix = "meshes/"; + ext = ".cob"; + break; + + case FileType::kData: + prefix = "data/"; + ext = ".json"; + break; + + case FileType::kTexture: { + if (g_core->HeadlessMode()) { + if (strchr(name.c_str(), '#')) { + return "headless_dummy_path#.nop"; + } else { + return "headless_dummy_path.nop"; + } + } + + assert(g_base->graphics_server + && g_base->graphics_server->texture_compression_types_are_set()); + assert(g_base->graphics_server + && g_base->graphics_server->texture_quality_set()); + prefix = "textures/"; + +#if BA_OSTYPE_ANDROID && !BA_ANDROID_DDS_BUILD + // On most android builds we go for .ktx, which contains etc2 and etc1. + ext = ".ktx"; +#elif BA_OSTYPE_IOS_TVOS + // On iOS we use pvr. + ext = ".pvr"; +#else + // all else defaults to dds + ext = ".dds"; +#endif + break; + } + default: + break; + } + + const std::vector& asset_paths_used = asset_paths_; + + for (auto&& i : asset_paths_used) { + file_out = i + "/" + prefix + name + ext; // NOLINT + bool exists; + + // '#' denotes a cube map texture, which is actually 6 files. + if (strchr(file_out.c_str(), '#')) { + // Just look for one of them i guess. + std::string tmp_name = file_out; + tmp_name.replace(tmp_name.find('#'), 1, "_+x"); + exists = g_core->platform->FilePathExists(tmp_name); + } else { + exists = g_core->platform->FilePathExists(file_out); + } + if (exists) { + return file_out; + } + } + + // We wanna fail gracefully for some types. + if (type == FileType::kSound && name != "blank") { + Log(LogLevel::kError, + "Unable to load audio: '" + name + "'; trying fallback..."); + return FindAssetFile(type, "blank"); + } else if (type == FileType::kTexture && name != "white") { + Log(LogLevel::kError, + "Unable to load texture: '" + name + "'; trying fallback..."); + return FindAssetFile(type, "white"); + } + + throw Exception("Can't find asset: \"" + name + "\""); +} + +void Assets::AddPendingLoad(Object::Ref* c) { + switch ((**c).GetAssetType()) { + case AssetType::kTexture: + case AssetType::kMesh: { + // Tell the graphics thread there's pending loads... + std::scoped_lock lock(pending_load_list_mutex_); + pending_loads_graphics_.push_back(c); + break; + } + case AssetType::kSound: { + // Tell the audio thread there's pending loads. + { + std::scoped_lock lock(pending_load_list_mutex_); + pending_loads_sounds_.push_back(c); + } + g_base->audio_server->PushHavePendingLoadsCall(); + break; + } + default: { + // Tell the logic thread there's pending loads. + { + std::scoped_lock lock(pending_load_list_mutex_); + pending_loads_other_.push_back(c); + } + g_base->logic->event_loop()->PushCall( + [] { g_base->logic->NotifyOfPendingAssetLoads(); }); + break; + } + } +} + +void Assets::ClearPendingLoadsDoneList() { + assert(g_base->InLogicThread()); + + std::scoped_lock lock(pending_load_list_mutex_); + + // Our explicitly-allocated reference pointer has made it back to us here in + // the logic thread. + // We can now kill the reference knowing that it's safe for this component + // to die at any time (anyone needing it to be alive now should be holding a + // reference themselves). + for (Object::Ref* i : pending_loads_done_) { + delete i; + } + pending_loads_done_.clear(); +} + +void Assets::AddPackage(const std::string& name, const std::string& path) { + // We don't protect package-path access so make sure its always from here. + assert(g_base->InLogicThread()); + if (g_buildconfig.debug_build()) { + if (packages_.find(name) != packages_.end()) { + Log(LogLevel::kWarning, "adding duplicate package: '" + name + "'"); + } + } + packages_[name] = path; +} + +void Assets::InitSpecialChars() { + std::scoped_lock lock(special_char_mutex_); + + special_char_strings_[SpecialChar::kDownArrow] = "\xee\x80\x84"; + special_char_strings_[SpecialChar::kUpArrow] = "\xee\x80\x83"; + special_char_strings_[SpecialChar::kLeftArrow] = "\xee\x80\x81"; + special_char_strings_[SpecialChar::kRightArrow] = "\xee\x80\x82"; + special_char_strings_[SpecialChar::kTopButton] = "\xee\x80\x86"; + special_char_strings_[SpecialChar::kLeftButton] = "\xee\x80\x85"; + special_char_strings_[SpecialChar::kRightButton] = "\xee\x80\x87"; + special_char_strings_[SpecialChar::kBottomButton] = "\xee\x80\x88"; + special_char_strings_[SpecialChar::kDelete] = "\xee\x80\x89"; + special_char_strings_[SpecialChar::kShift] = "\xee\x80\x8A"; + special_char_strings_[SpecialChar::kBack] = "\xee\x80\x8B"; + special_char_strings_[SpecialChar::kLogoFlat] = "\xee\x80\x8C"; + special_char_strings_[SpecialChar::kRewindButton] = "\xee\x80\x8D"; + special_char_strings_[SpecialChar::kPlayPauseButton] = "\xee\x80\x8E"; + special_char_strings_[SpecialChar::kFastForwardButton] = "\xee\x80\x8F"; + special_char_strings_[SpecialChar::kDpadCenterButton] = "\xee\x80\x90"; + + special_char_strings_[SpecialChar::kOuyaButtonO] = "\xee\x80\x99"; + special_char_strings_[SpecialChar::kOuyaButtonU] = "\xee\x80\x9A"; + special_char_strings_[SpecialChar::kOuyaButtonY] = "\xee\x80\x9B"; + special_char_strings_[SpecialChar::kOuyaButtonA] = "\xee\x80\x9C"; + special_char_strings_[SpecialChar::kOuyaLogo] = "\xee\x80\x9D"; + special_char_strings_[SpecialChar::kLogo] = "\xee\x80\x9E"; + special_char_strings_[SpecialChar::kTicket] = "\xee\x80\x9F"; + special_char_strings_[SpecialChar::kGooglePlayGamesLogo] = "\xee\x80\xA0"; + special_char_strings_[SpecialChar::kGameCenterLogo] = "\xee\x80\xA1"; + special_char_strings_[SpecialChar::kDiceButton1] = "\xee\x80\xA2"; + special_char_strings_[SpecialChar::kDiceButton2] = "\xee\x80\xA3"; + special_char_strings_[SpecialChar::kDiceButton3] = "\xee\x80\xA4"; + special_char_strings_[SpecialChar::kDiceButton4] = "\xee\x80\xA5"; + special_char_strings_[SpecialChar::kGameCircleLogo] = "\xee\x80\xA6"; + special_char_strings_[SpecialChar::kPartyIcon] = "\xee\x80\xA7"; + special_char_strings_[SpecialChar::kTestAccount] = "\xee\x80\xA8"; + special_char_strings_[SpecialChar::kTicketBacking] = "\xee\x80\xA9"; + special_char_strings_[SpecialChar::kTrophy1] = "\xee\x80\xAA"; + special_char_strings_[SpecialChar::kTrophy2] = "\xee\x80\xAB"; + special_char_strings_[SpecialChar::kTrophy3] = "\xee\x80\xAC"; + special_char_strings_[SpecialChar::kTrophy0a] = "\xee\x80\xAD"; + special_char_strings_[SpecialChar::kTrophy0b] = "\xee\x80\xAE"; + special_char_strings_[SpecialChar::kTrophy4] = "\xee\x80\xAF"; + special_char_strings_[SpecialChar::kLocalAccount] = "\xee\x80\xB0"; + special_char_strings_[SpecialChar::kAlibabaLogo] = "\xee\x80\xB1"; + + special_char_strings_[SpecialChar::kFlagUnitedStates] = "\xee\x80\xB2"; + special_char_strings_[SpecialChar::kFlagMexico] = "\xee\x80\xB3"; + special_char_strings_[SpecialChar::kFlagGermany] = "\xee\x80\xB4"; + special_char_strings_[SpecialChar::kFlagBrazil] = "\xee\x80\xB5"; + special_char_strings_[SpecialChar::kFlagRussia] = "\xee\x80\xB6"; + special_char_strings_[SpecialChar::kFlagChina] = "\xee\x80\xB7"; + special_char_strings_[SpecialChar::kFlagUnitedKingdom] = "\xee\x80\xB8"; + special_char_strings_[SpecialChar::kFlagCanada] = "\xee\x80\xB9"; + special_char_strings_[SpecialChar::kFlagIndia] = "\xee\x80\xBA"; + special_char_strings_[SpecialChar::kFlagJapan] = "\xee\x80\xBB"; + special_char_strings_[SpecialChar::kFlagFrance] = "\xee\x80\xBC"; + special_char_strings_[SpecialChar::kFlagIndonesia] = "\xee\x80\xBD"; + special_char_strings_[SpecialChar::kFlagItaly] = "\xee\x80\xBE"; + special_char_strings_[SpecialChar::kFlagSouthKorea] = "\xee\x80\xBF"; + special_char_strings_[SpecialChar::kFlagNetherlands] = "\xee\x81\x80"; + + special_char_strings_[SpecialChar::kFedora] = "\xee\x81\x81"; + special_char_strings_[SpecialChar::kHal] = "\xee\x81\x82"; + special_char_strings_[SpecialChar::kCrown] = "\xee\x81\x83"; + special_char_strings_[SpecialChar::kYinYang] = "\xee\x81\x84"; + special_char_strings_[SpecialChar::kEyeBall] = "\xee\x81\x85"; + special_char_strings_[SpecialChar::kSkull] = "\xee\x81\x86"; + special_char_strings_[SpecialChar::kHeart] = "\xee\x81\x87"; + special_char_strings_[SpecialChar::kDragon] = "\xee\x81\x88"; + special_char_strings_[SpecialChar::kHelmet] = "\xee\x81\x89"; + special_char_strings_[SpecialChar::kMushroom] = "\xee\x81\x8A"; + + special_char_strings_[SpecialChar::kNinjaStar] = "\xee\x81\x8B"; + special_char_strings_[SpecialChar::kVikingHelmet] = "\xee\x81\x8C"; + special_char_strings_[SpecialChar::kMoon] = "\xee\x81\x8D"; + special_char_strings_[SpecialChar::kSpider] = "\xee\x81\x8E"; + special_char_strings_[SpecialChar::kFireball] = "\xee\x81\x8F"; + + special_char_strings_[SpecialChar::kFlagUnitedArabEmirates] = "\xee\x81\x90"; + special_char_strings_[SpecialChar::kFlagQatar] = "\xee\x81\x91"; + special_char_strings_[SpecialChar::kFlagEgypt] = "\xee\x81\x92"; + special_char_strings_[SpecialChar::kFlagKuwait] = "\xee\x81\x93"; + special_char_strings_[SpecialChar::kFlagAlgeria] = "\xee\x81\x94"; + special_char_strings_[SpecialChar::kFlagSaudiArabia] = "\xee\x81\x95"; + special_char_strings_[SpecialChar::kFlagMalaysia] = "\xee\x81\x96"; + special_char_strings_[SpecialChar::kFlagCzechRepublic] = "\xee\x81\x97"; + special_char_strings_[SpecialChar::kFlagAustralia] = "\xee\x81\x98"; + special_char_strings_[SpecialChar::kFlagSingapore] = "\xee\x81\x99"; + + special_char_strings_[SpecialChar::kOculusLogo] = "\xee\x81\x9A"; + special_char_strings_[SpecialChar::kSteamLogo] = "\xee\x81\x9B"; + special_char_strings_[SpecialChar::kNvidiaLogo] = "\xee\x81\x9C"; + + special_char_strings_[SpecialChar::kFlagIran] = "\xee\x81\x9D"; + special_char_strings_[SpecialChar::kFlagPoland] = "\xee\x81\x9E"; + special_char_strings_[SpecialChar::kFlagArgentina] = "\xee\x81\x9F"; + special_char_strings_[SpecialChar::kFlagPhilippines] = "\xee\x81\xA0"; + special_char_strings_[SpecialChar::kFlagChile] = "\xee\x81\xA1"; + + special_char_strings_[SpecialChar::kMikirog] = "\xee\x81\xA2"; + special_char_strings_[SpecialChar::kV2Logo] = "\xee\x81\xA3"; +} + +void Assets::SetLanguageKeys( + const std::unordered_map& language) { + assert(g_base->InLogicThread()); + { + std::scoped_lock lock(language_mutex_); + language_ = language; + } + + // Let some subsystems know that language has changed. + g_base->app_mode->LanguageChanged(); + g_base->ui->LanguageChanged(); + g_base->graphics->LanguageChanged(); +} + +auto DoCompileResourceString(cJSON* obj) -> std::string { + // NOTE: We currently talk to Python here so need to be sure + // we're holding the GIL. Perhaps in the future we could handle this + // stuff completely in C++ and be free of this limitation. + assert(Python::HaveGIL()); + assert(obj != nullptr); + + std::string result; + + // If its got a "r" key, look it up as a resource.. (with optional fallback). + cJSON* resource = cJSON_GetObjectItem(obj, "r"); + if (resource == nullptr) { + resource = cJSON_GetObjectItem(obj, "resource"); + // As of build 14318, complain if we find long key names; hope to remove + // them soon. + if (resource != nullptr) { + static bool printed = false; + if (!printed) { + printed = true; + char* c = cJSON_Print(obj); + BA_LOG_ONCE( + LogLevel::kError, + "found long key 'resource' in raw lstr json: " + std::string(c)); + free(c); + } + } + } + if (resource != nullptr) { + // Look for fallback-resource. + cJSON* fallback_resource = cJSON_GetObjectItem(obj, "f"); + if (fallback_resource == nullptr) { + fallback_resource = cJSON_GetObjectItem(obj, "fallback"); + + // As of build 14318, complain if we find old long key names; hope to + // remove them soon. + if (fallback_resource != nullptr) { + static bool printed = false; + if (!printed) { + printed = true; + char* c = cJSON_Print(obj); + BA_LOG_ONCE( + LogLevel::kError, + "found long key 'fallback' in raw lstr json: " + std::string(c)); + free(c); + } + } + } + cJSON* fallback_value = cJSON_GetObjectItem(obj, "fv"); + result = g_base->python->GetResource( + resource->valuestring, + fallback_resource ? fallback_resource->valuestring : nullptr, + fallback_value ? fallback_value->valuestring : nullptr); + } else { + // Apparently not a resource; lets try as a translation ("t" keys). + cJSON* translate = cJSON_GetObjectItem(obj, "t"); + if (translate == nullptr) { + translate = cJSON_GetObjectItem(obj, "translate"); + + // As of build 14318, complain if we find long key names; hope to remove + // them soon. + if (translate != nullptr) { + static bool printed = false; + if (!printed) { + printed = true; + char* c = cJSON_Print(obj); + BA_LOG_ONCE( + LogLevel::kError, + "found long key 'translate' in raw lstr json: " + std::string(c)); + free(c); + } + } + } + if (translate != nullptr) { + if (translate->type != cJSON_Array + || cJSON_GetArraySize(translate) != 2) { + throw Exception("Expected a 2 member array for translate"); + } + cJSON* category = cJSON_GetArrayItem(translate, 0); + if (category->type != cJSON_String) { + throw Exception( + "First member of translate array (category) must be a string"); + } + cJSON* value = cJSON_GetArrayItem(translate, 1); + if (value->type != cJSON_String) { + throw Exception( + "Second member of translate array (value) must be a string"); + } + result = g_base->python->GetTranslation(category->valuestring, + value->valuestring); + } else { + // Lastly try it as a value ("value" or "v"). + // (can be useful for feeding explicit strings while still allowing + // translated subs + cJSON* value = cJSON_GetObjectItem(obj, "v"); + if (value == nullptr) { + value = cJSON_GetObjectItem(obj, "value"); + + // As of build 14318, complain if we find long key names; hope to remove + // them soon. + if (value != nullptr) { + static bool printed = false; + if (!printed) { + printed = true; + char* c = cJSON_Print(obj); + BA_LOG_ONCE( + LogLevel::kError, + "found long key 'value' in raw lstr json: " + std::string(c)); + free(c); + } + } + } + if (value != nullptr) { + if (value->type != cJSON_String) { + throw Exception("Expected a string for value"); + } + result = value->valuestring; + } else { + throw Exception("no 'resource', 'translate', or 'value' keys found"); + } + } + } + + // Ok; now no matter what it was, see if it contains any subs and replace + // them. + // ("subs" or "s") + cJSON* subs = cJSON_GetObjectItem(obj, "s"); + if (subs == nullptr) { + subs = cJSON_GetObjectItem(obj, "subs"); + + // As of build 14318, complain if we find long key names; hope to remove + // them soon. + if (subs != nullptr) { + static bool printed = false; + if (!printed) { + printed = true; + char* c = cJSON_Print(obj); + BA_LOG_ONCE(LogLevel::kError, "found long key 'subs' in raw lstr json: " + + std::string(c)); + free(c); + } + } + } + if (subs != nullptr) { + if (subs->type != cJSON_Array) { + throw Exception("expected an array for 'subs'"); + } + int subs_count = cJSON_GetArraySize(subs); + for (int i = 0; i < subs_count; i++) { + cJSON* sub = cJSON_GetArrayItem(subs, i); + if (sub->type != cJSON_Array || cJSON_GetArraySize(sub) != 2) { + throw Exception( + "Invalid subs entry; expected length 2 list of sub/replacement."); + } + + // First item should be a string. + cJSON* key = cJSON_GetArrayItem(sub, 0); + if (key->type != cJSON_String) { + throw Exception("Sub keys must be strings."); + } + std::string s_key = key->valuestring; + + // Second item can be a string or a dict; if its a dict, we go recursive. + cJSON* value = cJSON_GetArrayItem(sub, 1); + std::string s_val; + if (value->type == cJSON_String) { + s_val = value->valuestring; + } else if (value->type == cJSON_Object) { + s_val = DoCompileResourceString(value); + } else { + throw Exception("Sub values must be strings or dicts."); + } + + // Replace *ALL* occurrences. + // FIXME: Using this simple logic, If our replace value contains our + // search value we get an infinite loop. For now, just error in that case. + if (s_val.find(s_key) != std::string::npos) { + throw Exception("Subs replace string cannot contain search string."); + } + while (true) { + size_t pos = result.find(s_key); + if (pos == std::string::npos) { + break; + } + result.replace(pos, s_key.size(), s_val); + } + } + } + return result; +} + +auto Assets::CompileResourceString(const std::string& s, const std::string& loc, + bool* valid) -> std::string { + bool dummyvalid; + if (valid == nullptr) { + valid = &dummyvalid; + } + + // Quick out: if it doesn't start with a { and end with a }, treat it as a + // literal and just return it as-is. + if (s.size() < 2 || s[0] != '{' || s[s.size() - 1] != '}') { + *valid = true; + return s; + } + + cJSON* root = cJSON_Parse(s.c_str()); + if (root == nullptr) { + Log(LogLevel::kError, "CompileResourceString failed (loc " + loc + + "); invalid json: '" + s + "'"); + *valid = false; + return ""; + } + std::string result; + try { + result = DoCompileResourceString(root); + *valid = true; + } catch (const std::exception& e) { + Log(LogLevel::kError, "CompileResourceString failed (loc " + loc + "): " + + std::string(e.what()) + "; str='" + s + "'"); + result = ""; + *valid = false; + } + cJSON_Delete(root); + return result; +} + +auto Assets::GetResourceString(const std::string& key) -> std::string { + std::string val; + { + std::scoped_lock lock(language_mutex_); + auto i = language_.find(key); + if (i != language_.end()) { + val = i->second; + } + } + return val; +} + +auto Assets::CharStr(SpecialChar id) -> std::string { + std::scoped_lock lock(special_char_mutex_); + std::string val; + auto i = special_char_strings_.find(id); + if (i != special_char_strings_.end()) { + val = i->second; + } else { + BA_LOG_PYTHON_TRACE_ONCE("invalid key in CharStr(): '" + + std::to_string(static_cast(id)) + "'"); + val = "?"; + } + return val; +} + +Assets::AssetListLock::AssetListLock() { + BA_DEBUG_FUNCTION_TIMER_BEGIN(); + g_base->assets->asset_lists_mutex_.lock(); + assert(!g_base->assets->asset_lists_locked_); + g_base->assets->asset_lists_locked_ = true; + BA_DEBUG_FUNCTION_TIMER_END_THREAD(20); +} + +Assets::AssetListLock::~AssetListLock() { + assert(g_base->assets->asset_lists_locked_); + g_base->assets->asset_lists_locked_ = false; + g_base->assets->asset_lists_mutex_.unlock(); +} + +auto Assets::SysTexture(SysTextureID id) -> TextureAsset* { + assert(asset_loads_allowed_); + assert(g_base->InLogicThread()); + assert(static_cast(id) < system_textures_.size()); + return system_textures_[static_cast(id)].Get(); +} + +auto Assets::SysCubeMapTexture(SysCubeMapTextureID id) -> TextureAsset* { + assert(asset_loads_allowed_); + assert(g_base->InLogicThread()); + assert(static_cast(id) < system_cube_map_textures_.size()); + return system_cube_map_textures_[static_cast(id)].Get(); +} + +auto Assets::SysSound(SysSoundID id) -> SoundAsset* { + assert(asset_loads_allowed_); + assert(g_base->InLogicThread()); + assert(static_cast(id) < system_sounds_.size()); + return system_sounds_[static_cast(id)].Get(); +} + +auto Assets::SysMesh(SysMeshID id) -> MeshAsset* { + assert(asset_loads_allowed_); + assert(g_base->InLogicThread()); + assert(static_cast(id) < system_meshes_.size()); + return system_meshes_[static_cast(id)].Get(); +} + +} // namespace ballistica::base diff --git a/src/ballistica/base/assets/assets.h b/src/ballistica/base/assets/assets.h new file mode 100644 index 00000000..99419e59 --- /dev/null +++ b/src/ballistica/base/assets/assets.h @@ -0,0 +1,181 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_ASSETS_ASSETS_H_ +#define BALLISTICA_BASE_ASSETS_ASSETS_H_ + +#include +#include +#include +#include + +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::base { + +/// Global assets wrangling class. +class Assets { + public: + Assets(); + + void AddPackage(const std::string& name, const std::string& path); + void Prune(int level = 0); + + /// Finish loading any assets that have been preloaded but still need to be + /// loaded by the proper thread. + auto RunPendingLoadsLogicThread() -> bool; + + /// Return true if audio loads remain to be done. + auto RunPendingAudioLoads() -> bool; + + /// Return true if graphics loads remain to be done. + auto RunPendingGraphicsLoads() -> bool; + void ClearPendingLoadsDoneList(); + template + auto RunPendingLoadList(std::vector*>* assets) -> bool; + + /// This function takes a newly allocated pointer which + /// is deleted once the load is completed. + void AddPendingLoad(Object::Ref* c); + enum class FileType { kMesh, kCollisionMesh, kTexture, kSound, kData }; + auto FindAssetFile(FileType fileType, const std::string& file_in) + -> std::string; + + /// Unload renderer-specific bits only (gl display lists, etc) - used when + /// recreating/adjusting the renderer. + void UnloadRendererBits(bool textures, bool meshes); + + /// Should be called from the logic thread after UnloadRendererBits(); + /// kicks off bg loads for all existing unloaded assets. + void MarkAllAssetsForLoad(); + void PrintLoadInfo(); + + auto GetMeshPendingLoadCount() -> int; + auto GetTexturePendingLoadCount() -> int; + auto GetSoundPendingLoadCount() -> int; + auto GetDataPendingLoadCount() -> int; + auto GetCollisionMeshPendingLoadCount() -> int; + + /// Return the total number of graphics related pending loads. + auto GetGraphicalPendingLoadCount() -> int; + + /// Return the total number of pending loads. + auto GetPendingLoadCount() -> int; + + /// You must hold one of these locks while calling GetXXX() below. + class AssetListLock { + public: + AssetListLock(); + ~AssetListLock(); + }; + + /// Enable asset-loads and start loading sys-assets. + void StartLoading(); + + // Get system assets. These are loaded at startup so are always instantly + // available. + auto SysTexture(SysTextureID id) -> TextureAsset*; + auto SysCubeMapTexture(SysCubeMapTextureID id) -> TextureAsset*; + auto SysSound(SysSoundID id) -> SoundAsset*; + auto SysMesh(SysMeshID id) -> MeshAsset*; + + /// Load/cache custom assets. Make sure you hold a AssetListLock. + auto GetTexture(const std::string& file_name) -> Object::Ref; + auto GetTexture(TextPacker* packer) -> Object::Ref; + auto GetQRCodeTexture(const std::string& url) -> Object::Ref; + auto GetCubeMapTexture(const std::string& file_name) + -> Object::Ref; + auto GetMesh(const std::string& file_name) -> Object::Ref; + auto GetSound(const std::string& file_name) -> Object::Ref; + auto GetDataAsset(const std::string& file_name) -> Object::Ref; + auto GetCollisionMesh(const std::string& file_name) + -> Object::Ref; + + auto total_mesh_count() const -> uint32_t { + return static_cast(meshes_.size()); + } + auto total_texture_count() const -> uint32_t { + return static_cast(textures_.size() + text_textures_.size() + + qr_textures_.size()); + } + auto total_sound_count() const -> uint32_t { + return static_cast(sounds_.size()); + } + auto total_collision_mesh_count() const -> uint32_t { + return static_cast(collision_meshes_.size()); + } + + // Text & Language (need to mold this into more asset-like concepts). + void SetLanguageKeys( + const std::unordered_map& language); + auto GetResourceString(const std::string& key) -> std::string; + auto CharStr(SpecialChar id) -> std::string; + auto CompileResourceString(const std::string& s, const std::string& loc, + bool* valid = nullptr) -> std::string; + + private: + static void MarkAssetForLoad(Asset* c); + void LoadSystemTexture(SysTextureID id, const char* name); + void LoadSystemCubeMapTexture(SysCubeMapTextureID id, const char* name); + void LoadSystemSound(SysSoundID id, const char* name); + void LoadSystemData(SystemDataID id, const char* name); + void LoadSystemMesh(SysMeshID id, const char* name); + void InitSpecialChars(); + + template + auto GetAssetPendingLoadCount( + std::unordered_map >* t_list, AssetType type) + -> int; + + template + auto GetAsset(const std::string& file_name, + std::unordered_map >* c_list) + -> Object::Ref; + + std::vector asset_paths_; + bool have_pending_loads_[static_cast(AssetType::kLast)]{}; + std::unordered_map packages_; + + // For use by AssetListLock; don't manually acquire. + std::mutex asset_lists_mutex_; + + // Will be true while a AssetListLock exists. Good to debug-verify this + // during any asset list access. + bool asset_lists_locked_{}; + + // 'hard-wired' internal assets + bool asset_loads_allowed_{}; + std::vector > system_textures_; + std::vector > system_cube_map_textures_; + std::vector > system_sounds_; + std::vector > system_datas_; + std::vector > system_meshes_; + + // All existing assets by filename (including internal). + std::unordered_map > textures_; + std::unordered_map > text_textures_; + std::unordered_map > qr_textures_; + std::unordered_map > meshes_; + std::unordered_map > sounds_; + std::unordered_map > datas_; + std::unordered_map > + collision_meshes_; + + // Components that have been preloaded but need to be loaded. + std::mutex pending_load_list_mutex_; + std::vector*> pending_loads_graphics_; + std::vector*> pending_loads_sounds_; + std::vector*> pending_loads_datas_; + std::vector*> pending_loads_other_; + std::vector*> pending_loads_done_; + + // Text & Language (need to mold this into more asset-like concepts). + std::mutex language_mutex_; + std::unordered_map language_; + std::mutex special_char_mutex_; + std::unordered_map special_char_strings_; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_ASSETS_ASSETS_H_ diff --git a/src/ballistica/assets/assets_server.cc b/src/ballistica/base/assets/assets_server.cc similarity index 59% rename from src/ballistica/assets/assets_server.cc rename to src/ballistica/base/assets/assets_server.cc index 81168ff9..6efb78c9 100644 --- a/src/ballistica/assets/assets_server.cc +++ b/src/ballistica/base/assets/assets_server.cc @@ -1,41 +1,37 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/assets/assets_server.h" +#include "ballistica/base/assets/assets_server.h" -#include "ballistica/assets/assets.h" -#include "ballistica/assets/data/asset_component_data.h" -#include "ballistica/core/thread.h" -#include "ballistica/generic/huffman.h" -#include "ballistica/generic/timer.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/graphics_server.h" +#include "ballistica/base/assets/asset.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/support/huffman.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::base { -AssetsServer::AssetsServer() { - // We're a singleton; make sure we don't already exist. - assert(g_assets_server == nullptr); +AssetsServer::AssetsServer() = default; +void AssetsServer::OnMainThreadStartApp() { // Spin up our thread. - thread_ = new Thread(ThreadTag::kAssets); - g_app->pausable_threads.push_back(thread_); + event_loop_ = new EventLoop(EventLoopID::kAssets); + g_core->pausable_event_loops.push_back(event_loop_); + + event_loop_->PushCallSynchronous([this] { OnAppStartInThread(); }); } -auto AssetsServer::OnAppStart() -> void { - thread_->PushCallSynchronous([this] { OnAppStartInThread(); }); +void AssetsServer::OnAppStartInThread() { + assert(g_base->InAssetsThread()); + // Ask our thread to give us periodic processing time (close to but + // not *exactly* one second; try to avoid aliasing with similar updates). + process_timer_ = event_loop()->NewTimer( + 987, true, NewLambdaRunnable([this] { Process(); })); } -auto AssetsServer::OnAppStartInThread() -> void { - assert(InAssetsThread()); - // get our thread to give us periodic processing time... - process_timer_ = - thread()->NewTimer(1000, true, NewLambdaRunnable([this] { Process(); })); -} - -auto AssetsServer::PushPendingPreload( - Object::Ref* asset_ref_ptr) -> void { - thread()->PushCall([this, asset_ref_ptr] { - assert(InAssetsThread()); +void AssetsServer::PushPendingPreload(Object::Ref* asset_ref_ptr) { + event_loop()->PushCall([this, asset_ref_ptr] { + assert(g_base->InAssetsThread()); // Add our pointer to one of the preload lists and shake our preload thread // to wake it up @@ -49,13 +45,13 @@ auto AssetsServer::PushPendingPreload( } void AssetsServer::PushBeginWriteReplayCall() { - thread()->PushCall([this] { + event_loop()->PushCall([this] { if (replays_broken_) { return; } - // we only allow writing one replay at once; make sure that's actually the - // case + // We only allow writing one replay at once; make sure that's actually + // the case. if (writing_replay_) { Log(LogLevel::kError, "AssetsServer got BeginWriteReplayCall while already writing"); @@ -70,19 +66,19 @@ void AssetsServer::PushBeginWriteReplayCall() { writing_replay_ = true; std::string f_name = "__lastReplay"; - assert(g_platform); + assert(g_core); std::string file_path = - g_platform->GetReplaysDir() + BA_DIRSLASH + f_name + ".brp"; - replay_out_file_ = g_platform->FOpen(file_path.c_str(), "wb"); + g_core->platform->GetReplaysDir() + BA_DIRSLASH + f_name + ".brp"; + replay_out_file_ = g_core->platform->FOpen(file_path.c_str(), "wb"); replay_bytes_written_ = 0; if (!replay_out_file_) { Log(LogLevel::kError, "unable to open output-stream file: '" + file_path + "'"); } else { - // write file id and protocol-version - // NOTE - we always write replays in our host protocol version - // no matter what the client stream is + // Write file id and protocol-version. + // NOTE: We always write replays in our host protocol version + // no matter what the client stream is. uint32_t file_id = kBrpFileID; uint16_t version = kProtocolVersion; if ((fwrite(&file_id, sizeof(file_id), 1, replay_out_file_) != 1) @@ -90,25 +86,25 @@ void AssetsServer::PushBeginWriteReplayCall() { fclose(replay_out_file_); replay_out_file_ = nullptr; Log(LogLevel::kError, "error writing replay file header: " - + g_platform->GetErrnoString()); + + g_core->platform->GetErrnoString()); } replay_bytes_written_ = 5; } - // trigger our process timer to go off immediately - // (we may need to wake it up) - g_assets_server->process_timer_->SetLength(0); + // Trigger our process timer to go off immediately + // (we may need to wake it up). + g_base->assets_server->process_timer_->SetLength(0); }); } void AssetsServer::PushAddMessageToReplayCall( const std::vector& data) { - thread()->PushCall([this, data] { + event_loop()->PushCall([this, data] { if (replays_broken_) { return; } - // sanity check.. + // Sanity check. if (!writing_replay_) { Log(LogLevel::kError, "AssetsServer got AddMessageToReplayCall while not writing replay"); @@ -116,10 +112,10 @@ void AssetsServer::PushAddMessageToReplayCall( return; } - // just add it to our list + // Just add it to our list. if (replay_out_file_) { - // if we've got too much data built up (lets go with 10 megs for now), - // abort + // If we've got too much data built up (lets go with 10 megs for now), + // abort. if (replay_message_bytes_ > 10000000) { Log(LogLevel::kError, "replay output buffer exceeded 10 megs; aborting replay"); @@ -136,12 +132,12 @@ void AssetsServer::PushAddMessageToReplayCall( } void AssetsServer::PushEndWriteReplayCall() { - thread()->PushCall([this] { + event_loop()->PushCall([this] { if (replays_broken_) { return; } - // sanity check.. + // Sanity check. if (!writing_replay_) { Log(LogLevel::kError, "_finishWritingReplay called while not writing"); replays_broken_ = true; @@ -149,8 +145,8 @@ void AssetsServer::PushEndWriteReplayCall() { } WriteReplayMessages(); - // whether or not we actually have a file has no impact on our - // writing_replay_ status.. + // Whether or not we actually have a file has no impact on our + // writing_replay_ status. if (replay_out_file_) { fclose(replay_out_file_); replay_out_file_ = nullptr; @@ -162,7 +158,7 @@ void AssetsServer::PushEndWriteReplayCall() { void AssetsServer::WriteReplayMessages() { if (replay_out_file_) { for (auto&& i : replay_messages_) { - std::vector data_compressed = g_utils->huffman()->compress(i); + std::vector data_compressed = g_base->huffman->compress(i); // If message length is < 254, write length as one byte. // If its between 254 and 65535, write 254 and then 2 length bytes @@ -180,8 +176,8 @@ void AssetsServer::WriteReplayMessages() { if (fwrite(&len8, 1, 1, replay_out_file_) != 1) { fclose(replay_out_file_); replay_out_file_ = nullptr; - Log(LogLevel::kError, - "error writing replay file: " + g_platform->GetErrnoString()); + Log(LogLevel::kError, "error writing replay file: " + + g_core->platform->GetErrnoString()); return; } } @@ -192,28 +188,28 @@ void AssetsServer::WriteReplayMessages() { if (fwrite(&len16, 2, 1, replay_out_file_) != 1) { fclose(replay_out_file_); replay_out_file_ = nullptr; - Log(LogLevel::kError, - "error writing replay file: " + g_platform->GetErrnoString()); + Log(LogLevel::kError, "error writing replay file: " + + g_core->platform->GetErrnoString()); return; } } else { if (fwrite(&len32, 4, 1, replay_out_file_) != 1) { fclose(replay_out_file_); replay_out_file_ = nullptr; - Log(LogLevel::kError, - "error writing replay file: " + g_platform->GetErrnoString()); + Log(LogLevel::kError, "error writing replay file: " + + g_core->platform->GetErrnoString()); return; } } } - // write buffer + // Write buffer. size_t result = fwrite(&(data_compressed[0]), data_compressed.size(), 1, replay_out_file_); if (result != 1) { fclose(replay_out_file_); replay_out_file_ = nullptr; Log(LogLevel::kError, - "error writing replay file: " + g_platform->GetErrnoString()); + "error writing replay file: " + g_core->platform->GetErrnoString()); return; } replay_bytes_written_ += data_compressed.size() + 2; @@ -224,35 +220,36 @@ void AssetsServer::WriteReplayMessages() { } void AssetsServer::Process() { - // make sure we don't do any loading until we know what kind/quality of - // textures we'll be loading - if (!g_assets || !g_graphics_server - || !g_graphics_server->texture_compression_types_are_set() // NOLINT - || !g_graphics_server->texture_quality_set()) { + // Make sure we don't do any loading until we know what kind/quality of + // textures we'll be loading. + if (!g_base->assets || !g_base->graphics_server + || !g_base->graphics_server + ->texture_compression_types_are_set() // NOLINT + || !g_base->graphics_server->texture_quality_set()) { return; } - // process exactly 1 preload item.. empty out our non-audio list first + // Process exactly 1 preload item. Empty out our non-audio list first // (audio is less likely to cause noticeable hitches if it needs to be loaded - // on-demand, so that's a lower priority for us) + // on-demand, so that's a lower priority for us). if (!pending_preloads_.empty()) { (**pending_preloads_.back()).Preload(); - // pass the ref-pointer along to the load queue - g_assets->AddPendingLoad(pending_preloads_.back()); + // Pass the ref-pointer along to the load queue. + g_base->assets->AddPendingLoad(pending_preloads_.back()); pending_preloads_.pop_back(); } else if (!pending_preloads_audio_.empty()) { (**pending_preloads_audio_.back()).Preload(); - // pass the ref-pointer along to the load queue - g_assets->AddPendingLoad(pending_preloads_audio_.back()); + // Pass the ref-pointer along to the load queue. + g_base->assets->AddPendingLoad(pending_preloads_audio_.back()); pending_preloads_audio_.pop_back(); } - // if we're writing a replay, dump anything we've got built up.. + // If we're writing a replay, dump anything we've got built up. if (writing_replay_) { WriteReplayMessages(); } - // if we've got nothing left, set our timer to go off every now and then if + // If we've got nothing left, set our timer to go off every now and then if // we're writing a replay.. otherwise just sleep indefinitely. if (pending_preloads_.empty() && pending_preloads_audio_.empty()) { if (writing_replay_) { @@ -263,4 +260,4 @@ void AssetsServer::Process() { } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/base/assets/assets_server.h b/src/ballistica/base/assets/assets_server.h new file mode 100644 index 00000000..4a8772ff --- /dev/null +++ b/src/ballistica/base/assets/assets_server.h @@ -0,0 +1,42 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_ASSETS_ASSETS_SERVER_H_ +#define BALLISTICA_BASE_ASSETS_ASSETS_SERVER_H_ + +#include +#include + +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::base { + +class AssetsServer { + public: + AssetsServer(); + void OnMainThreadStartApp(); + void PushBeginWriteReplayCall(); + void PushEndWriteReplayCall(); + void PushAddMessageToReplayCall(const std::vector& data); + void PushPendingPreload(Object::Ref* asset_ref_ptr); + auto event_loop() const -> EventLoop* { return event_loop_; } + + private: + void OnAppStartInThread(); + void Process(); + void WriteReplayMessages(); + EventLoop* event_loop_{}; + FILE* replay_out_file_{}; + size_t replay_bytes_written_{}; + bool writing_replay_{}; + bool replays_broken_{}; + std::list > replay_messages_; + size_t replay_message_bytes_{}; + Timer* process_timer_{}; + std::vector*> pending_preloads_; + std::vector*> pending_preloads_audio_; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_ASSETS_ASSETS_SERVER_H_ diff --git a/src/ballistica/assets/data/collide_model_data.cc b/src/ballistica/base/assets/collision_mesh_asset.cc similarity index 70% rename from src/ballistica/assets/data/collide_model_data.cc rename to src/ballistica/base/assets/collision_mesh_asset.cc index 763a6d23..e078306f 100644 --- a/src/ballistica/assets/data/collide_model_data.cc +++ b/src/ballistica/base/assets/collision_mesh_asset.cc @@ -1,25 +1,40 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/assets/data/collide_model_data.h" +#include "ballistica/base/assets/collision_mesh_asset.h" -#include "ballistica/assets/assets.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/core/core.h" -namespace ballistica { +namespace ballistica::base { -CollideModelData::CollideModelData(const std::string& file_name_in) +CollisionMeshAsset::CollisionMeshAsset(const std::string& file_name_in) : file_name_(file_name_in) { - file_name_full_ = - g_assets->FindAssetFile(Assets::FileType::kCollisionModel, file_name_in); + assert(g_base && g_base->assets); + file_name_full_ = g_base->assets->FindAssetFile( + Assets::FileType::kCollisionMesh, file_name_in); valid_ = true; } -void CollideModelData::DoPreload() { +auto CollisionMeshAsset::GetAssetType() const -> AssetType { + return AssetType::kCollisionMesh; +} + +auto CollisionMeshAsset::GetName() const -> std::string { + if (!file_name_full_.empty()) { + return file_name_full_; + } else { + return "invalid CollisionMesh"; + } +} + +void CollisionMeshAsset::DoPreload() { assert(!file_name_.empty()); - FILE* f = g_platform->FOpen(file_name_full_.c_str(), "rb"); + FILE* f = g_core->platform->FOpen(file_name_full_.c_str(), "rb"); uint32_t i_vals[2]; if (!f) { - throw Exception("Can't open collide model: '" + file_name_full_ + "'"); + throw Exception("Can't open collision mesh file: '" + file_name_full_ + + "'"); } uint32_t version; @@ -66,7 +81,7 @@ void CollideModelData::DoPreload() { tri_mesh_data_ = dGeomTriMeshDataCreate(); BA_PRECONDITION(tri_mesh_data_); - if (!HeadlessMode()) { + if (!g_core->HeadlessMode()) { tri_mesh_data_bg_ = dGeomTriMeshDataCreate(); BA_PRECONDITION(tri_mesh_data_bg_); } @@ -76,7 +91,7 @@ void CollideModelData::DoPreload() { tri_mesh_data_, &(vertices_[0]), 3 * sizeof(dReal), static_cast_check_fit(vertex_count), &(indices_[0]), static_cast(indices_.size()), 3 * sizeof(uint32_t), &(normals_[0])); - if (!HeadlessMode()) { + if (!g_core->HeadlessMode()) { dGeomTriMeshDataBuildSingle1(tri_mesh_data_bg_, &(vertices_[0]), 3 * sizeof(dReal), i_vals[0], &(indices_[0]), static_cast(indices_.size()), @@ -97,12 +112,12 @@ void CollideModelData::DoPreload() { #endif // dSINGLE } // namespace ballistica -void CollideModelData::DoLoad() { assert(InLogicThread()); } +void CollisionMeshAsset::DoLoad() { assert(g_base->InLogicThread()); } -void CollideModelData::DoUnload() { +void CollisionMeshAsset::DoUnload() { // TODO(ericf): if we want to support in-game reloading we need - // to keep track of what ODE trimeshes are using our data and update - // them all accordingly on unload/loads... + // to keep track of what ODE trimeshes are using our data and update + // them all accordingly on unload/loads... // we should still be fine for regular pruning unloads though; // if there are no references remaining to us then nothing in the @@ -118,15 +133,15 @@ void CollideModelData::DoUnload() { } } -auto CollideModelData::GetMeshData() -> dTriMeshDataID { +auto CollisionMeshAsset::GetMeshData() -> dTriMeshDataID { assert(tri_mesh_data_); return tri_mesh_data_; } -auto CollideModelData::GetBGMeshData() -> dTriMeshDataID { +auto CollisionMeshAsset::GetBGMeshData() -> dTriMeshDataID { assert(loaded()); - assert(!HeadlessMode()); + assert(!g_core->HeadlessMode()); return tri_mesh_data_bg_; } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/base/assets/collision_mesh_asset.h b/src/ballistica/base/assets/collision_mesh_asset.h new file mode 100644 index 00000000..9af25d50 --- /dev/null +++ b/src/ballistica/base/assets/collision_mesh_asset.h @@ -0,0 +1,41 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_ASSETS_COLLISION_MESH_ASSET_H_ +#define BALLISTICA_BASE_ASSETS_COLLISION_MESH_ASSET_H_ + +#include +#include + +#include "ballistica/base/assets/asset.h" +#include "ode/ode.h" + +namespace ballistica::base { + +// Loadable mesh for collision detection. +class CollisionMeshAsset : public Asset { + public: + CollisionMeshAsset() = default; + explicit CollisionMeshAsset(const std::string& file_name_in); + + void DoPreload() override; + void DoLoad() override; + void DoUnload() override; + auto GetAssetType() const -> AssetType override; + auto GetName() const -> std::string override; + + auto GetMeshData() -> dTriMeshDataID; + auto GetBGMeshData() -> dTriMeshDataID; + + private: + std::string file_name_; + std::string file_name_full_; + std::vector vertices_; + std::vector indices_; + std::vector normals_; + dTriMeshDataID tri_mesh_data_{}; + dTriMeshDataID tri_mesh_data_bg_{}; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_ASSETS_COLLISION_MESH_ASSET_H_ diff --git a/src/ballistica/assets/data/data_data.cc b/src/ballistica/base/assets/data_asset.cc similarity index 50% rename from src/ballistica/assets/data/data_data.cc rename to src/ballistica/base/assets/data_asset.cc index 0a509fc4..6933a3d8 100644 --- a/src/ballistica/assets/data/data_data.cc +++ b/src/ballistica/base/assets/data_asset.cc @@ -1,20 +1,32 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/assets/data/data_data.h" +#include "ballistica/base/assets/data_asset.h" -#include "ballistica/assets/assets.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_sys.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/core/python/core_python.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python_sys.h" -namespace ballistica { +namespace ballistica::base { -DataData::DataData(const std::string& file_name_in) : file_name_(file_name_in) { +DataAsset::DataAsset(const std::string& file_name_in) + : file_name_(file_name_in) { file_name_full_ = - g_assets->FindAssetFile(Assets::FileType::kData, file_name_in); + g_base->assets->FindAssetFile(Assets::FileType::kData, file_name_in); valid_ = true; } -void DataData::DoPreload() { +auto DataAsset::GetAssetType() const -> AssetType { return AssetType::kData; } + +auto DataAsset::GetName() const -> std::string { + if (!file_name_full_.empty()) { + return file_name_full_; + } else { + return "invalid data"; + } +} + +void DataAsset::DoPreload() { // NOTE TO SELF: originally I tried to grab the GIL here and do our actual // Python loading in Preload(). However this resulted in deadlock // in the following case: @@ -31,20 +43,22 @@ void DataData::DoPreload() { raw_input_ = Utils::FileToString(file_name_full_); } -void DataData::DoLoad() { - assert(InLogicThread()); +void DataAsset::DoLoad() { + assert(g_base->InLogicThread()); assert(valid_); PythonRef args(Py_BuildValue("(s)", raw_input_.c_str()), PythonRef::kSteal); - object_ = g_python->obj(Python::ObjID::kJsonLoadsCall).Call(args); - if (!object_.exists()) { + object_ = g_core->python->objs() + .Get(core::CorePython::ObjID::kJsonLoadsCall) + .Call(args); + if (!object_.Exists()) { throw Exception("Unable to load data: '" + file_name_ + "'."); } } -void DataData::DoUnload() { - assert(InLogicThread()); +void DataAsset::DoUnload() { + assert(g_base->InLogicThread()); assert(valid_); object_.Release(); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/base/assets/data_asset.h b/src/ballistica/base/assets/data_asset.h new file mode 100644 index 00000000..06d3d81d --- /dev/null +++ b/src/ballistica/base/assets/data_asset.h @@ -0,0 +1,41 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_ASSETS_DATA_ASSET_H_ +#define BALLISTICA_BASE_ASSETS_DATA_ASSET_H_ + +#include + +#include "ballistica/base/assets/asset.h" +#include "ballistica/shared/python/python_ref.h" + +namespace ballistica::base { + +class DataAsset : public Asset { + public: + DataAsset() = default; + explicit DataAsset(const std::string& file_name_in); + + void DoPreload() override; + void DoLoad() override; + void DoUnload() override; + auto GetAssetType() const -> AssetType override; + auto GetName() const -> std::string override; + + auto object() -> const PythonRef& { + assert(g_base->InLogicThread()); + assert(loaded()); + return object_; + } + auto file_name() const -> const std::string& { return file_name_; } + auto file_name_full() const -> const std::string& { return file_name_full_; } + + private: + PythonRef object_; + std::string file_name_; + std::string file_name_full_; + std::string raw_input_; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_ASSETS_DATA_ASSET_H_ diff --git a/src/ballistica/assets/data/model_data.cc b/src/ballistica/base/assets/mesh_asset.cc similarity index 75% rename from src/ballistica/assets/data/model_data.cc rename to src/ballistica/base/assets/mesh_asset.cc index 48e2cf99..bbc14e3c 100644 --- a/src/ballistica/assets/data/model_data.cc +++ b/src/ballistica/base/assets/mesh_asset.cc @@ -1,27 +1,38 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/assets/data/model_data.h" +#include "ballistica/base/assets/mesh_asset.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/renderer.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/core/core.h" -namespace ballistica { +namespace ballistica::base { -ModelData::ModelData(const std::string& file_name_in) +MeshAsset::MeshAsset(const std::string& file_name_in) : file_name_(file_name_in) { file_name_full_ = - g_assets->FindAssetFile(Assets::FileType::kModel, file_name_in); + g_base->assets->FindAssetFile(Assets::FileType::kMesh, file_name_in); valid_ = true; } -void ModelData::DoPreload() { +auto MeshAsset::GetAssetType() const -> AssetType { return AssetType::kMesh; } + +auto MeshAsset::GetName() const -> std::string { + if (!file_name_full_.empty()) { + return file_name_full_; + } else { + return "invalid mesh"; + } +} + +void MeshAsset::DoPreload() { // In headless, don't load anything. #if !BA_HEADLESS_BUILD assert(!file_name_.empty()); - FILE* f = g_platform->FOpen(file_name_full_.c_str(), "rb"); + FILE* f = g_core->platform->FOpen(file_name_full_.c_str(), "rb"); if (!f) { - throw Exception("Can't open model: '" + file_name_full_ + "'"); + throw Exception("Can't open mesh file: '" + file_name_full_ + "'"); } // We currently read/write in little-endian since that's all we run on at the @@ -99,10 +110,9 @@ void ModelData::DoPreload() { #endif // BA_HEADLESS_BUILD } -void ModelData::DoLoad() { - assert(!renderer_data_.exists()); - renderer_data_ = Object::MakeRefCounted( - g_graphics_server->renderer()->NewModelData(*this)); +void MeshAsset::DoLoad() { + assert(!renderer_data_.Exists()); + renderer_data_ = g_base->graphics_server->renderer()->NewMeshAssetData(*this); // once we're loaded lets free up our vert data memory std::vector().swap(vertices_); @@ -111,9 +121,9 @@ void ModelData::DoLoad() { std::vector().swap(indices32_); } -void ModelData::DoUnload() { +void MeshAsset::DoUnload() { assert(valid_); - assert(renderer_data_.exists()); + assert(renderer_data_.Exists()); std::vector().swap(vertices_); std::vector().swap(indices8_); std::vector().swap(indices16_); @@ -121,4 +131,4 @@ void ModelData::DoUnload() { renderer_data_.Clear(); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/assets/data/model_data.h b/src/ballistica/base/assets/mesh_asset.h similarity index 53% rename from src/ballistica/assets/data/model_data.h rename to src/ballistica/base/assets/mesh_asset.h index e5388922..d5e91836 100644 --- a/src/ballistica/assets/data/model_data.h +++ b/src/ballistica/base/assets/mesh_asset.h @@ -1,34 +1,29 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_ASSETS_DATA_MODEL_DATA_H_ -#define BALLISTICA_ASSETS_DATA_MODEL_DATA_H_ +#ifndef BALLISTICA_BASE_ASSETS_MESH_ASSET_H_ +#define BALLISTICA_BASE_ASSETS_MESH_ASSET_H_ #include #include -#include "ballistica/assets/data/asset_component_data.h" -#include "ballistica/assets/data/model_renderer_data.h" +#include "ballistica/base/assets/asset.h" +#include "ballistica/base/assets/mesh_asset_renderer_data.h" -namespace ballistica { +namespace ballistica::base { -class ModelData : public AssetComponentData { +class MeshAsset : public Asset { public: - ModelData() = default; - explicit ModelData(const std::string& file_name_in); + MeshAsset() = default; + explicit MeshAsset(const std::string& file_name_in); void DoPreload() override; void DoLoad() override; void DoUnload() override; - auto GetAssetType() const -> AssetType override { return AssetType::kModel; } - auto GetName() const -> std::string override { - if (!file_name_full_.empty()) { - return file_name_full_; - } else { - return "invalid Model"; - } - } - auto renderer_data() const -> ModelRendererData* { - assert(renderer_data_.exists()); - return renderer_data_.get(); + auto GetAssetType() const -> AssetType override; + auto GetName() const -> std::string override; + + auto renderer_data() const -> MeshAssetRendererData* { + assert(renderer_data_.Exists()); + return renderer_data_.Get(); } auto vertices() const -> const std::vector& { return vertices_; @@ -50,7 +45,7 @@ class ModelData : public AssetComponentData { } private: - Object::Ref renderer_data_; + Object::Ref renderer_data_; std::string file_name_; std::string file_name_full_; MeshFormat format_{}; @@ -58,10 +53,10 @@ class ModelData : public AssetComponentData { std::vector indices8_; std::vector indices16_; std::vector indices32_; - friend class ModelRendererData; - BA_DISALLOW_CLASS_COPIES(ModelData); + friend class MeshAssetRendererData; + BA_DISALLOW_CLASS_COPIES(MeshAsset); }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_ASSETS_DATA_MODEL_DATA_H_ +#endif // BALLISTICA_BASE_ASSETS_MESH_ASSET_H_ diff --git a/src/ballistica/base/assets/mesh_asset_renderer_data.h b/src/ballistica/base/assets/mesh_asset_renderer_data.h new file mode 100644 index 00000000..4b249042 --- /dev/null +++ b/src/ballistica/base/assets/mesh_asset_renderer_data.h @@ -0,0 +1,21 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_ASSETS_MESH_ASSET_RENDERER_DATA_H_ +#define BALLISTICA_BASE_ASSETS_MESH_ASSET_RENDERER_DATA_H_ + +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::base { + +// Renderer-specific data (gl display list, etc) +// this is provided by the renderer +class MeshAssetRendererData : public Object { + public: + auto GetDefaultOwnerThread() const -> EventLoopID override { + return EventLoopID::kMain; + } +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_ASSETS_MESH_ASSET_RENDERER_DATA_H_ diff --git a/src/ballistica/assets/data/sound_data.cc b/src/ballistica/base/assets/sound_asset.cc similarity index 83% rename from src/ballistica/assets/data/sound_data.cc rename to src/ballistica/base/assets/sound_asset.cc index 5b257c0d..69571ecd 100644 --- a/src/ballistica/assets/data/sound_data.cc +++ b/src/ballistica/base/assets/sound_asset.cc @@ -1,6 +1,8 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/assets/data/sound_data.h" +#include "ballistica/base/assets/sound_asset.h" + +#include #if BA_ENABLE_AUDIO #if BA_USE_TREMOR_VORBIS @@ -10,16 +12,18 @@ #endif #endif // BA_ENABLE_AUDIO -#include "ballistica/assets/assets.h" -#include "ballistica/audio/audio_server.h" -#include "ballistica/python/python.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/audio/audio_server.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/core/core.h" +#include "ballistica/shared/python/python.h" // Need to move away from OpenAL on Apple stuff. #if __clang__ #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #endif -namespace ballistica { +namespace ballistica::base { #if BA_ENABLE_AUDIO @@ -53,7 +57,7 @@ static auto LoadOgg(const char* file_name, std::vector* buffer, bool fallback = false; // Open for binary reading. - f = g_platform->FOpen(file_name, "rb"); + f = g_core->platform->FOpen(file_name, "rb"); if (f == nullptr) { fallback = true; Log(LogLevel::kError, std::string("Can't open sound file '") + file_name @@ -61,7 +65,7 @@ static auto LoadOgg(const char* file_name, std::vector* buffer, // Attempt a fallback standin; if that doesn't work, throw in the towel. file_name = "data/global/audio/blank.ogg"; - f = g_platform->FOpen(file_name, "rb"); + f = g_core->platform->FOpen(file_name, "rb"); if (f == nullptr) throw Exception(std::string("Can't open fallback sound file '") + file_name + "' for reading..."); @@ -84,7 +88,7 @@ static auto LoadOgg(const char* file_name, std::vector* buffer, // Attempt fallback. file_name = "data/global/audio/blank.ogg"; - f = g_platform->FOpen(file_name, "rb"); + f = g_core->platform->FOpen(file_name, "rb"); // If fallback doesn't work, throw in the towel. if (f == nullptr) @@ -140,7 +144,8 @@ static auto LoadOgg(const char* file_name, std::vector* buffer, static bool reported_corrupt = false; if (!reported_corrupt) { reported_corrupt = true; - g_python->PushObjCall(Python::ObjID::kPrintCorruptFileErrorCall); + g_base->python->objs().PushCall( + BasePython::ObjID::kPrintCorruptFileErrorCall); } (*buffer) = std::vector(32 * 100, 0); } @@ -155,10 +160,10 @@ static auto LoadOgg(const char* file_name, std::vector* buffer, static void LoadCachedOgg(const char* file_name, std::vector* buffer, ALenum* format, ALsizei* freq) { std::string sound_cache_dir = - g_platform->GetVolatileDataDirectory() + BA_DIRSLASH + "audiocache"; + g_core->platform->GetVolatileDataDirectory() + BA_DIRSLASH + "audiocache"; static bool made_sound_cache_dir = false; if (!made_sound_cache_dir) { - g_platform->MakeDir(sound_cache_dir); + g_core->platform->MakeDir(sound_cache_dir); made_sound_cache_dir = true; } std::vector b(strlen(file_name) + 1); @@ -172,10 +177,10 @@ static void LoadCachedOgg(const char* file_name, std::vector* buffer, // load it. struct BA_STAT stat_ogg {}; time_t ogg_mod_time = 0; - if (g_platform->Stat(file_name, &stat_ogg) == 0) { + if (g_core->platform->Stat(file_name, &stat_ogg) == 0) { ogg_mod_time = stat_ogg.st_mtime; } - FILE* f_cache = g_platform->FOpen(cache_file_name.c_str(), "rb"); + FILE* f_cache = g_core->platform->FOpen(cache_file_name.c_str(), "rb"); if (f_cache && ogg_mod_time != 0) { bool got_cache = false; time_t cache_mod_time; @@ -215,7 +220,7 @@ static void LoadCachedOgg(const char* file_name, std::vector* buffer, // If the load went cleanly, attempt to write a cache file. if (success) { - FILE* f = g_platform->FOpen(cache_file_name.c_str(), "wb"); + FILE* f = g_core->platform->FOpen(cache_file_name.c_str(), "wb"); bool success2 = false; if (f) { if (fwrite(&ogg_mod_time, sizeof(ogg_mod_time), 1, f) == 1) { @@ -234,7 +239,7 @@ static void LoadCachedOgg(const char* file_name, std::vector* buffer, // Attempt to clean up if it looks like something went wrong. if (!success2) { - g_platform->Unlink(cache_file_name.c_str()); + g_core->platform->Unlink(cache_file_name.c_str()); } } } @@ -242,19 +247,24 @@ static void LoadCachedOgg(const char* file_name, std::vector* buffer, #endif // BA_ENABLE_AUDIO -SoundData::SoundData(const std::string& file_name_in) - : file_name_(file_name_in), - is_streamed_(false), -#if BA_ENABLE_AUDIO - buffer_(0), -#endif // BA_ENABLE_AUDIO - last_play_time_(0) { +SoundAsset::SoundAsset(const std::string& file_name_in) + : file_name_(file_name_in) { file_name_full_ = - g_assets->FindAssetFile(Assets::FileType::kSound, file_name_in); + g_base->assets->FindAssetFile(Assets::FileType::kSound, file_name_in); valid_ = true; } -void SoundData::DoPreload() { +auto SoundAsset::GetAssetType() const -> AssetType { return AssetType::kSound; } + +auto SoundAsset::GetName() const -> std::string { + if (!file_name_full_.empty()) { + return file_name_full_; + } else { + return "invalid sound"; + } +} + +void SoundAsset::DoPreload() { #if BA_ENABLE_AUDIO // Its an ogg sound file. @@ -272,12 +282,12 @@ void SoundData::DoPreload() { #endif // BA_ENABLE_AUDIO } -void SoundData::DoLoad() { - assert(InAudioThread()); +void SoundAsset::DoLoad() { + assert(g_base->InAudioThread()); assert(valid_); #if BA_ENABLE_AUDIO - assert(!g_audio_server->paused()); + assert(!g_base->audio_server->paused()); // Note: streamed sources create buffers as they're used; not here. if (!is_streamed_) { @@ -300,9 +310,9 @@ void SoundData::DoLoad() { #endif // BA_ENABLE_AUDIO } -void SoundData::DoUnload() { +void SoundAsset::DoUnload() { + assert(g_base->InAudioThread()); assert(valid_); - assert(InAudioThread()); #if BA_ENABLE_AUDIO if (!is_streamed_) { assert(buffer_); @@ -313,4 +323,8 @@ void SoundData::DoUnload() { #endif // BA_ENABLE_AUDIO } -} // namespace ballistica +void SoundAsset::UpdatePlayTime() { + last_play_time_ = g_core->GetAppTimeMillisecs(); +} + +} // namespace ballistica::base diff --git a/src/ballistica/base/assets/sound_asset.h b/src/ballistica/base/assets/sound_asset.h new file mode 100644 index 00000000..43b9333a --- /dev/null +++ b/src/ballistica/base/assets/sound_asset.h @@ -0,0 +1,53 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_ASSETS_SOUND_ASSET_H_ +#define BALLISTICA_BASE_ASSETS_SOUND_ASSET_H_ + +#include +#include + +#include "ballistica/base/assets/asset.h" +#include "ballistica/base/audio/al_sys.h" + +namespace ballistica::base { + +class SoundAsset : public Asset { + public: + SoundAsset() = default; + explicit SoundAsset(const std::string& file_name_in); + + void DoPreload() override; + void DoLoad() override; + // FIXME: Should make sure the sound_data isn't in use before unloading it. + void DoUnload() override; + auto GetAssetType() const -> AssetType override; + auto GetName() const -> std::string override; +#if BA_ENABLE_AUDIO + auto format() const -> ALenum { return format_; } + auto buffer() const -> ALuint { + assert(!is_streamed_); + return buffer_; + } +#endif // BA_ENABLE_AUDIO + auto is_streamed() const { return is_streamed_; } + const auto& file_name() const { return file_name_; } + const auto& file_name_full() const { return file_name_full_; } + void UpdatePlayTime(); + auto last_play_time() const { return last_play_time_; } + + private: + std::string file_name_; + std::string file_name_full_; + bool is_streamed_{}; +#if BA_ENABLE_AUDIO + ALuint buffer_{}; + ALenum format_{}; + ALsizei freq_{}; +#endif // BA_ENABLE_AUDIO + std::vector load_buffer_; + millisecs_t last_play_time_{}; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_ASSETS_SOUND_ASSET_H_ diff --git a/src/ballistica/assets/data/texture_data.cc b/src/ballistica/base/assets/texture_asset.cc similarity index 82% rename from src/ballistica/assets/data/texture_data.cc rename to src/ballistica/base/assets/texture_asset.cc index 11aa82f7..8122e958 100644 --- a/src/ballistica/assets/data/texture_data.cc +++ b/src/ballistica/base/assets/texture_asset.cc @@ -1,25 +1,26 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/assets/data/texture_data.h" +#include "ballistica/base/assets/texture_asset.h" -#include "ballistica/assets/data/texture_preload_data.h" -#include "ballistica/assets/data/texture_renderer_data.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/renderer.h" -#include "ballistica/graphics/text/text_packer.h" -#include "ballistica/graphics/texture/dds.h" -#include "ballistica/graphics/texture/ktx.h" -#include "ballistica/graphics/texture/pvr.h" +#include "ballistica/base/assets/texture_asset_preload_data.h" +#include "ballistica/base/assets/texture_asset_renderer_data.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/base/graphics/text/text_packer.h" +#include "ballistica/base/graphics/texture/dds.h" +#include "ballistica/base/graphics/texture/ktx.h" +#include "ballistica/base/graphics/texture/pvr.h" +#include "ballistica/core/core.h" #include "external/qr_code_generator/QrCode.hpp" -namespace ballistica { +namespace ballistica::base { -static void rgba8888_unpremultiply_in_place(void* src, size_t cb) { +static void rgba8888_unpremultiply_in_place(uint8_t* src, size_t cb) { // Compute the actual number of pixel elements in the buffer. size_t cpel = cb / 4; - auto* psrc = static_cast(src); - auto* pdst = static_cast(src); + auto* psrc = src; + auto* pdst = src; for (size_t i = 0; i < cpel; i++) { int r = *psrc++; int g = *psrc++; @@ -39,38 +40,50 @@ static void rgba8888_unpremultiply_in_place(void* src, size_t cb) { } } -TextureData::TextureData() = default; -TextureData::TextureData(const std::string& file_in, TextureType type_in, - TextureMinQuality min_quality_in) +TextureAsset::TextureAsset() = default; +TextureAsset::TextureAsset(const std::string& file_in, TextureType type_in, + TextureMinQuality min_quality_in) : file_name_(file_in), type_(type_in), min_quality_(min_quality_in) { file_name_full_ = - g_assets->FindAssetFile(Assets::FileType::kTexture, file_in); + g_base->assets->FindAssetFile(Assets::FileType::kTexture, file_in); valid_ = true; } -TextureData::TextureData(TextPacker* packer) : packer_(packer) { +auto TextureAsset::GetAssetType() const -> AssetType { + return AssetType::kTexture; +} + +TextureAsset::TextureAsset(TextPacker* packer) : packer_(packer) { file_name_ = packer->hash(); valid_ = true; } -TextureData::TextureData(const std::string& qr_url) : is_qr_code_(true) { +TextureAsset::TextureAsset(const std::string& qr_url) : is_qr_code_(true) { file_name_ = qr_url; valid_ = true; } -TextureData::~TextureData() = default; +TextureAsset::~TextureAsset() = default; -void TextureData::DoPreload() { +auto TextureAsset::GetName() const -> std::string { + return (!file_name_.empty()) ? file_name_ : "invalid texture"; +} + +auto TextureAsset::GetNameFull() const -> std::string { + return file_name_full(); +} + +void TextureAsset::DoPreload() { assert(valid_); - assert(g_graphics_server - && g_graphics_server->texture_compression_types_are_set()); + assert(g_base->graphics_server + && g_base->graphics_server->texture_compression_types_are_set()); // We figure out which LOD should be our base level based on quality. - TextureQuality texture_quality = g_graphics_server->texture_quality(); + TextureQuality texture_quality = g_base->graphics_server->texture_quality(); // If we're a text-texture. - if (packer_.exists()) { + if (packer_.Exists()) { assert(type_ == TextureType::k2D); int width = packer_->texture_width(); @@ -105,9 +118,9 @@ void TextureData::DoPreload() { assert(!strings.empty()); assert(strings.size() * 2 == positions.size()); - void* tex_ref{g_platform->CreateTextTexture( + void* tex_ref{g_core->platform->CreateTextTexture( width, height, strings, positions, visible_widths, scale)}; - uint8_t* pixels{g_platform->GetTextTextureData(tex_ref)}; + uint8_t* pixels{g_core->platform->GetTextTextureData(tex_ref)}; assert(pixels); assert(tex_ref); @@ -130,10 +143,10 @@ void TextureData::DoPreload() { preload_datas_[0].formats[0] = TextureFormat::kRGBA_8888; preload_datas_[0].base_level = 0; - g_platform->FreeTextTexture(tex_ref); + g_core->platform->FreeTextTexture(tex_ref); // Downsample this down to rgba4444 in-place. - TexturePreloadData::rgba8888_to_rgba4444_in_place(buffer, buffer_size); + TextureAssetPreloadData::rgba8888_to_rgba4444_in_place(buffer, buffer_size); preload_datas_[0].formats[0] = TextureFormat::kRGBA_4444; } else if (is_qr_code_) { @@ -192,11 +205,11 @@ void TextureData::DoPreload() { #endif // We should only be loading this if we support etc1 in hardware. - assert(g_graphics_server->SupportsTextureCompressionType( + assert(g_base->graphics_server->SupportsTextureCompressionType( TextureCompressionType::kETC1)); // Decompress dxt1/dxt5 ones if we don't natively support S3TC. - if (!g_graphics_server->SupportsTextureCompressionType( + if (!g_base->graphics_server->SupportsTextureCompressionType( TextureCompressionType::kS3TC)) { if ((preload_datas_[0].formats[preload_datas_[0].base_level] == TextureFormat::kDXT5) @@ -219,7 +232,7 @@ void TextureData::DoPreload() { #endif // Decompress dxt1/dxt5 if we don't natively support it. - if (!g_graphics_server->SupportsTextureCompressionType( + if (!g_base->graphics_server->SupportsTextureCompressionType( TextureCompressionType::kS3TC)) { preload_datas_[0].ConvertToUncompressed(this); } @@ -246,7 +259,7 @@ void TextureData::DoPreload() { == TextureFormat::kETC2_RGB) || (preload_datas_[0].formats[preload_datas_[0].base_level] == TextureFormat::kETC2_RGBA)) - && (!g_graphics_server->SupportsTextureCompressionType( + && (!g_base->graphics_server->SupportsTextureCompressionType( TextureCompressionType::kETC2))) { preload_datas_[0].ConvertToUncompressed(this); } @@ -254,7 +267,7 @@ void TextureData::DoPreload() { // Decompress etc1 if we don't natively support it. if ((preload_datas_[0].formats[preload_datas_[0].base_level] == TextureFormat::kETC1) - && (!g_graphics_server->SupportsTextureCompressionType( + && (!g_base->graphics_server->SupportsTextureCompressionType( TextureCompressionType::kETC1))) { preload_datas_[0].ConvertToUncompressed(this); } @@ -269,7 +282,7 @@ void TextureData::DoPreload() { &preload_datas_[0].base_level); // We should only be loading this if we support pvr in hardware. - assert(g_graphics_server->SupportsTextureCompressionType( + assert(g_base->graphics_server->SupportsTextureCompressionType( TextureCompressionType::kPVR)); } else if (!strcmp(file_name_full_.c_str() + file_name_size - 4, ".nop")) { @@ -328,11 +341,11 @@ void TextureData::DoPreload() { } // We should only be loading this if we support etc1 in hardware. - assert(g_graphics_server->SupportsTextureCompressionType( + assert(g_base->graphics_server->SupportsTextureCompressionType( TextureCompressionType::kETC1)); // Decompress dxt1/dxt5 ones if we don't natively support S3TC. - if (!g_graphics_server->SupportsTextureCompressionType( + if (!g_base->graphics_server->SupportsTextureCompressionType( TextureCompressionType::kS3TC)) { if ((preload_datas_[d].formats[preload_datas_[d].base_level] == TextureFormat::kDXT5) @@ -355,7 +368,7 @@ void TextureData::DoPreload() { #endif // Decompress dxt1/dxt5 if we don't natively support it. - if (!g_graphics_server->SupportsTextureCompressionType( + if (!g_base->graphics_server->SupportsTextureCompressionType( TextureCompressionType::kS3TC)) { preload_datas_[d].ConvertToUncompressed(this); } @@ -377,7 +390,7 @@ void TextureData::DoPreload() { == TextureFormat::kETC2_RGB) || (preload_datas_[d].formats[preload_datas_[d].base_level] == TextureFormat::kETC2_RGBA)) - && (!g_graphics_server->SupportsTextureCompressionType( + && (!g_base->graphics_server->SupportsTextureCompressionType( TextureCompressionType::kETC2))) { preload_datas_[d].ConvertToUncompressed(this); } @@ -385,7 +398,7 @@ void TextureData::DoPreload() { // Decompress etc1 if we don't natively support it. if ((preload_datas_[d].formats[preload_datas_[d].base_level] == TextureFormat::kETC1) - && (!g_graphics_server->SupportsTextureCompressionType( + && (!g_base->graphics_server->SupportsTextureCompressionType( TextureCompressionType::kETC1))) { preload_datas_[d].ConvertToUncompressed(this); } @@ -417,12 +430,11 @@ void TextureData::DoPreload() { } } -void TextureData::DoLoad() { - assert(InGraphicsThread()); - assert(!renderer_data_.exists()); - renderer_data_ = Object::MakeRefCounted( - g_graphics_server->renderer()->NewTextureData(*this)); - assert(renderer_data_.exists()); +void TextureAsset::DoLoad() { + assert(g_base->InGraphicsThread()); + assert(!renderer_data_.Exists()); + renderer_data_ = g_base->graphics_server->renderer()->NewTextureData(*this); + assert(renderer_data_.Exists()); renderer_data_->Load(); // Store our base-level from the preload-data so we know if we're lower than @@ -434,12 +446,12 @@ void TextureData::DoLoad() { preload_datas_.clear(); } -void TextureData::DoUnload() { - assert(InGraphicsThread()); +void TextureAsset::DoUnload() { + assert(g_base->InGraphicsThread()); assert(valid_); - assert(renderer_data_.exists()); + assert(renderer_data_.Exists()); renderer_data_.Clear(); base_level_ = 0; } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/base/assets/texture_asset.h b/src/ballistica/base/assets/texture_asset.h new file mode 100644 index 00000000..b12f32f2 --- /dev/null +++ b/src/ballistica/base/assets/texture_asset.h @@ -0,0 +1,59 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_ASSETS_TEXTURE_ASSET_H_ +#define BALLISTICA_BASE_ASSETS_TEXTURE_ASSET_H_ + +#include +#include + +#include "ballistica/base/assets/asset.h" + +namespace ballistica::base { + +// A lovely texture asset. +class TextureAsset : public Asset { + public: + TextureAsset(); + ~TextureAsset() override; + // Pass a newly allocated TextPacker pointer here; TextureData takes ownership + // and handles cleaning it up. + explicit TextureAsset(TextPacker* packer); + explicit TextureAsset(const std::string& file_in, TextureType type_in, + TextureMinQuality min_quality_in); + explicit TextureAsset(const std::string& qr_url); + + auto GetName() const -> std::string override; + auto GetNameFull() const -> std::string override; + auto GetAssetType() const -> AssetType override; + void DoPreload() override; + void DoLoad() override; + void DoUnload() override; + + auto file_name() const -> const std::string& { return file_name_; } + auto file_name_full() const -> const std::string& { return file_name_full_; } + auto texture_type() const -> TextureType { return type_; } + auto is_qr_code() const -> bool { return is_qr_code_; } + auto preload_datas() const -> const std::vector& { + return preload_datas_; + } + auto renderer_data() const -> TextureAssetRendererData* { + assert(renderer_data_.Exists()); + return renderer_data_.Get(); + } + auto base_level() const -> int { return base_level_; } + + private: + Object::Ref packer_; + bool is_qr_code_{}; + std::string file_name_; + std::string file_name_full_; + std::vector preload_datas_; + TextureType type_{TextureType::k2D}; + TextureMinQuality min_quality_{TextureMinQuality::kLow}; + Object::Ref renderer_data_; + int base_level_{}; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_ASSETS_TEXTURE_ASSET_H_ diff --git a/src/ballistica/assets/data/texture_preload_data.cc b/src/ballistica/base/assets/texture_asset_preload_data.cc similarity index 97% rename from src/ballistica/assets/data/texture_preload_data.cc rename to src/ballistica/base/assets/texture_asset_preload_data.cc index eda797fa..85dcea00 100644 --- a/src/ballistica/assets/data/texture_preload_data.cc +++ b/src/ballistica/base/assets/texture_asset_preload_data.cc @@ -1,15 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/assets/data/texture_preload_data.h" +#include "ballistica/base/assets/texture_asset_preload_data.h" #if BA_OSTYPE_LINUX #include #endif -#include "ballistica/assets/component/texture.h" -#include "ballistica/graphics/texture/ktx.h" +#include "ballistica/base/graphics/texture/ktx.h" +#include "ballistica/scene_v1/assets/scene_texture.h" -namespace ballistica { +namespace ballistica::base { #pragma clang diagnostic push #pragma ide diagnostic ignored "hicpp-signed-bitwise" @@ -25,7 +25,8 @@ namespace ballistica { #define GL_ETC1_RGB8_OES 0x8D64 #endif -void TexturePreloadData::rgba8888_to_rgba4444_in_place(void* src, size_t cb) { +void TextureAssetPreloadData::rgba8888_to_rgba4444_in_place(void* src, + size_t cb) { // Compute the actual number of pixel elements in the buffer. size_t cpel = cb / 4; auto* psrc = static_cast(src); @@ -404,7 +405,7 @@ static void BlockDecompressImageDXT5(uint32_t width, uint32_t height, } } -void TexturePreloadData::ConvertToUncompressed(TextureData* texture) { +void TextureAssetPreloadData::ConvertToUncompressed(TextureAsset* texture) { // FIXME; we could technically get better quality on our // lower mip levels by dynamically generating them in this // case instead of decompressing each level. @@ -565,7 +566,7 @@ void TexturePreloadData::ConvertToUncompressed(TextureData* texture) { } } -TexturePreloadData::~TexturePreloadData() { +TextureAssetPreloadData::~TextureAssetPreloadData() { for (auto& buffer : buffers) { if (buffer) { free(buffer); @@ -575,4 +576,4 @@ TexturePreloadData::~TexturePreloadData() { #pragma clang diagnostic pop -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/assets/data/texture_preload_data.h b/src/ballistica/base/assets/texture_asset_preload_data.h similarity index 67% rename from src/ballistica/assets/data/texture_preload_data.h rename to src/ballistica/base/assets/texture_asset_preload_data.h index 03c9d5c4..db251a52 100644 --- a/src/ballistica/assets/data/texture_preload_data.h +++ b/src/ballistica/base/assets/texture_asset_preload_data.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_ASSETS_DATA_TEXTURE_PRELOAD_DATA_H_ -#define BALLISTICA_ASSETS_DATA_TEXTURE_PRELOAD_DATA_H_ +#ifndef BALLISTICA_BASE_ASSETS_TEXTURE_ASSET_PRELOAD_DATA_H_ +#define BALLISTICA_BASE_ASSETS_TEXTURE_ASSET_PRELOAD_DATA_H_ -#include "ballistica/ballistica.h" +#include "ballistica/base/base.h" -namespace ballistica { +namespace ballistica::base { // Determined by the biggest tex dimension we support (current 4096). // FIXME: Should define that dimension as a constant somewhere. @@ -13,19 +13,19 @@ const int kMaxTextureLevels = 14; // Temporary data that is passed along to the renderer when creating // rendererdata. This may include sdl surfaces and/or compressed buffers. -class TexturePreloadData { +class TextureAssetPreloadData { public: static void rgba8888_to_rgba4444_in_place(void* src, size_t cb); - TexturePreloadData() { + TextureAssetPreloadData() { // There isn't a way to do this in bracket-init, is there? // (aside from writing out all values manually I mean). for (auto& format : formats) { format = TextureFormat::kNone; } } - ~TexturePreloadData(); - void ConvertToUncompressed(TextureData* texture); + ~TextureAssetPreloadData(); + void ConvertToUncompressed(TextureAsset* texture); uint8_t* buffers[kMaxTextureLevels]{}; size_t sizes[kMaxTextureLevels]{}; @@ -35,6 +35,6 @@ class TexturePreloadData { int base_level{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_ASSETS_DATA_TEXTURE_PRELOAD_DATA_H_ +#endif // BALLISTICA_BASE_ASSETS_TEXTURE_ASSET_PRELOAD_DATA_H_ diff --git a/src/ballistica/base/assets/texture_asset_renderer_data.h b/src/ballistica/base/assets/texture_asset_renderer_data.h new file mode 100644 index 00000000..575bc14c --- /dev/null +++ b/src/ballistica/base/assets/texture_asset_renderer_data.h @@ -0,0 +1,29 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_ASSETS_TEXTURE_ASSET_RENDERER_DATA_H_ +#define BALLISTICA_BASE_ASSETS_TEXTURE_ASSET_RENDERER_DATA_H_ + +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::base { + +// Renderer-specific data (gl tex, etc) +// this is extended by the renderer +class TextureAssetRendererData : public Object { + public: + auto GetDefaultOwnerThread() const -> EventLoopID override { + return EventLoopID::kMain; + } + + // Create the renderer data but don't load it in yet. + TextureAssetRendererData() = default; + + // load the data. + // if incremental is true, return whether the load was completed + // (non-incremental loads should always complete) + virtual void Load() = 0; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_ASSETS_TEXTURE_ASSET_RENDERER_DATA_H_ diff --git a/src/ballistica/audio/al_sys.cc b/src/ballistica/base/audio/al_sys.cc similarity index 84% rename from src/ballistica/audio/al_sys.cc rename to src/ballistica/base/audio/al_sys.cc index c39a14d2..39b6fada 100644 --- a/src/ballistica/audio/al_sys.cc +++ b/src/ballistica/base/audio/al_sys.cc @@ -1,9 +1,9 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/audio/al_sys.h" +#include "ballistica/base/audio/al_sys.h" -#include "ballistica/audio/audio_server.h" -#include "ballistica/generic/utils.h" +#include "ballistica/base/audio/audio_server.h" +#include "ballistica/shared/generic/utils.h" // Need to move away from OpenAL on Apple stuff. #if __clang__ @@ -12,10 +12,10 @@ #if BA_ENABLE_AUDIO -namespace ballistica { +namespace ballistica::base { void _check_al_error(const char* file, int line) { - if (g_audio_server->paused()) { + if (g_base->audio_server->paused()) { Log(LogLevel::kError, Utils::BaseName(file) + ":" + std::to_string(line) + ": Checking OpenAL error while paused."); } @@ -47,6 +47,6 @@ auto GetALErrorString(ALenum err) -> const char* { #undef DO_AL_ERR_CASE } -} // namespace ballistica +} // namespace ballistica::base #endif // BA_ENABLE_AUDIO diff --git a/src/ballistica/audio/al_sys.h b/src/ballistica/base/audio/al_sys.h similarity index 74% rename from src/ballistica/audio/al_sys.h rename to src/ballistica/base/audio/al_sys.h index 44b1c740..316d3f3c 100644 --- a/src/ballistica/audio/al_sys.h +++ b/src/ballistica/base/audio/al_sys.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_AUDIO_AL_SYS_H_ -#define BALLISTICA_AUDIO_AL_SYS_H_ +#ifndef BALLISTICA_BASE_AUDIO_AL_SYS_H_ +#define BALLISTICA_BASE_AUDIO_AL_SYS_H_ #include -#include "ballistica/ballistica.h" +#include "ballistica/shared/ballistica.h" #if BA_ENABLE_AUDIO @@ -24,7 +24,7 @@ #define DEBUG_CHECK_AL_ERROR ((void)0) #endif -namespace ballistica { +namespace ballistica::base { const int kAudioStreamBufferSize = 4096 * 8; const int kAudioStreamBufferCount = 7; @@ -34,8 +34,8 @@ auto GetALErrorString(ALenum err) -> const char*; void _check_al_error(const char* file, int line); -} // namespace ballistica +} // namespace ballistica::base #endif // BA_ENABLE_AUDIO -#endif // BALLISTICA_AUDIO_AL_SYS_H_ +#endif // BALLISTICA_BASE_AUDIO_AL_SYS_H_ diff --git a/src/ballistica/audio/audio.cc b/src/ballistica/base/audio/audio.cc similarity index 63% rename from src/ballistica/audio/audio.cc rename to src/ballistica/base/audio/audio.cc index 8f4fef9a..e73e2c9b 100644 --- a/src/ballistica/audio/audio.cc +++ b/src/ballistica/base/audio/audio.cc @@ -1,47 +1,66 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/audio/audio.h" +#include "ballistica/base/audio/audio.h" -#include "ballistica/assets/data/sound_data.h" -#include "ballistica/audio/audio_server.h" -#include "ballistica/audio/audio_source.h" -#include "ballistica/core/thread.h" +#include "ballistica/base/app/app_config.h" +#include "ballistica/base/assets/sound_asset.h" +#include "ballistica/base/audio/audio_server.h" +#include "ballistica/base/audio/audio_source.h" +#include "ballistica/shared/foundation/event_loop.h" -namespace ballistica { +namespace ballistica::base { -Audio::Audio() {} +Audio::Audio() = default; void Audio::Reset() { - assert(InLogicThread()); - g_audio_server->PushResetCall(); + assert(g_base->InLogicThread()); + g_base->audio_server->PushResetCall(); } +void Audio::OnAppStart() { assert(g_base->InLogicThread()); } + +void Audio::OnAppPause() { assert(g_base->InLogicThread()); } + +void Audio::OnAppResume() { assert(g_base->InLogicThread()); } + +void Audio::OnAppShutdown() { assert(g_base->InLogicThread()); } + +void Audio::StepDisplayTime() { assert(g_base->InLogicThread()); } + +void Audio::ApplyAppConfig() { + assert(g_base->InLogicThread()); + SetVolumes(g_base->app_config->Resolve(AppConfig::FloatID::kMusicVolume), + g_base->app_config->Resolve(AppConfig::FloatID::kSoundVolume)); +} + +void Audio::OnScreenSizeChange() { assert(g_base->InLogicThread()); } + void Audio::SetVolumes(float music_volume, float sound_volume) { - g_audio_server->PushSetVolumesCall(music_volume, sound_volume); + g_base->audio_server->PushSetVolumesCall(music_volume, sound_volume); } void Audio::SetSoundPitch(float pitch) { - g_audio_server->PushSetSoundPitchCall(pitch); + g_base->audio_server->PushSetSoundPitchCall(pitch); } void Audio::SetListenerPosition(const Vector3f& p) { - g_audio_server->PushSetListenerPositionCall(p); + g_base->audio_server->PushSetListenerPositionCall(p); } void Audio::SetListenerOrientation(const Vector3f& forward, const Vector3f& up) { - g_audio_server->PushSetListenerOrientationCall(forward, up); + g_base->audio_server->PushSetListenerOrientationCall(forward, up); } // This stops a particular sound play ID only. void Audio::PushSourceStopSoundCall(uint32_t play_id) { - g_audio_server->thread()->PushCall( - [play_id] { g_audio_server->StopSound(play_id); }); + g_base->audio_server->event_loop()->PushCall( + [play_id] { g_base->audio_server->StopSound(play_id); }); } void Audio::PushSourceFadeOutCall(uint32_t play_id, uint32_t time) { - g_audio_server->thread()->PushCall( - [play_id, time] { g_audio_server->FadeSoundOut(play_id, time); }); + g_base->audio_server->event_loop()->PushCall( + [play_id, time] { g_base->audio_server->FadeSoundOut(play_id, time); }); } // Seems we get a false alarm here. @@ -98,7 +117,7 @@ auto Audio::SourceBeginExisting(uint32_t play_id, int debug_id) // has set it up. However, no one should be trying to get a playing // sound unless they've already started playing one which implies // everything was set up already. I think we're good. - assert(g_audio->client_sources_.size() > source_id); + assert(g_base->audio->client_sources_.size() > source_id); // If this guy's still got the play id they're asking about, lock/return it. client_sources_[source_id]->Lock(debug_id); @@ -118,56 +137,63 @@ auto Audio::SourceBeginExisting(uint32_t play_id, int debug_id) return nullptr; } -auto Audio::ShouldPlay(SoundData* sound) -> bool { - millisecs_t time = GetRealTime(); +auto Audio::ShouldPlay(SoundAsset* sound) -> bool { + millisecs_t time = g_core->GetAppTimeMillisecs(); assert(sound); return (time - sound->last_play_time() > 50); } -void Audio::PlaySound(SoundData* sound, float volume) { - assert(InLogicThread()); +auto Audio::PlaySound(SoundAsset* sound, float volume) + -> std::optional { + assert(g_core); + assert(g_base->InLogicThread()); BA_DEBUG_FUNCTION_TIMER_BEGIN(); assert(sound); + std::optional play_id{}; if (!ShouldPlay(sound)) { - return; + return play_id; } AudioSource* s = SourceBeginNew(); if (s) { // In vr mode, play non-positional sounds positionally in space roughly // where the menu is. - if (IsVRMode()) { + if (g_core->IsVRMode()) { s->SetGain(volume); s->SetPositional(true); float x = 0.0f; float y = 4.5f; float z = -3.0f; s->SetPosition(x, y, z); - s->Play(sound); + play_id = s->Play(sound); s->End(); } else { s->SetGain(volume); s->SetPositional(false); - s->Play(sound); + play_id = s->Play(sound); s->End(); } } BA_DEBUG_FUNCTION_TIMER_END(20); + return play_id; } -void Audio::PlaySoundAtPosition(SoundData* sound, float volume, float x, - float y, float z) { +auto Audio::PlaySoundAtPosition(SoundAsset* sound, float volume, float x, + float y, float z) -> std::optional { + assert(g_base->InLogicThread()); assert(sound); + std::optional play_id{}; if (!ShouldPlay(sound)) { - return; + return play_id; } // Run locally. if (AudioSource* source = SourceBeginNew()) { source->SetGain(volume); source->SetPositional(true); source->SetPosition(x, y, z); - source->Play(sound); + play_id = source->Play(sound); source->End(); } + return play_id; } void Audio::AddClientSource(AudioSource* source) { @@ -178,4 +204,4 @@ void Audio::MakeSourceAvailable(AudioSource* source) { available_sources_.push_back(source); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/audio/audio.h b/src/ballistica/base/audio/audio.h similarity index 60% rename from src/ballistica/audio/audio.h rename to src/ballistica/base/audio/audio.h index 1cc1f000..6136296e 100644 --- a/src/ballistica/audio/audio.h +++ b/src/ballistica/base/audio/audio.h @@ -1,29 +1,38 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_AUDIO_AUDIO_H_ -#define BALLISTICA_AUDIO_AUDIO_H_ +#ifndef BALLISTICA_BASE_AUDIO_AUDIO_H_ +#define BALLISTICA_BASE_AUDIO_AUDIO_H_ #include #include +#include #include -#include "ballistica/core/object.h" +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::base { /// Client class for audio operations; /// used by the game and/or other threads. class Audio { public: Audio(); - auto Reset() -> void; + void Reset(); - auto SetVolumes(float music_volume, float sound_volume) -> void; + virtual void OnAppStart(); + virtual void OnAppPause(); + virtual void OnAppResume(); + virtual void OnAppShutdown(); + virtual void ApplyAppConfig(); + virtual void OnScreenSizeChange(); + virtual void StepDisplayTime(); - auto SetListenerPosition(const Vector3f& p) -> void; - auto SetListenerOrientation(const Vector3f& forward, const Vector3f& up) - -> void; - auto SetSoundPitch(float pitch) -> void; + void SetVolumes(float music_volume, float sound_volume); + + void SetListenerPosition(const Vector3f& p); + void SetListenerOrientation(const Vector3f& forward, const Vector3f& up); + void SetSoundPitch(float pitch); // Return a pointer to a locked sound source, or nullptr if they're all busy. // The sound source will be reset to standard settings (no loop, fade 1, pos @@ -42,22 +51,22 @@ class Audio { auto IsSoundPlaying(uint32_t play_id) -> bool; // Simple one-shot play functions. - auto PlaySound(SoundData* s, float volume = 1.0f) -> void; - auto PlaySoundAtPosition(SoundData* sound, float volume, float x, float y, - float z) -> void; + auto PlaySound(SoundAsset* s, float volume = 1.0f) -> std::optional; + auto PlaySoundAtPosition(SoundAsset* sound, float volume, float x, float y, + float z) -> std::optional; // Call this if you want to prevent repeated plays of the same sound. It'll // tell you if the sound has been played recently. The one-shot sound-play // functions use this under the hood. (PlaySound, PlaySoundAtPosition). - auto ShouldPlay(SoundData* s) -> bool; + auto ShouldPlay(SoundAsset* s) -> bool; // Hmm; shouldn't these be accessed through the Source class? - auto PushSourceFadeOutCall(uint32_t play_id, uint32_t time) -> void; - auto PushSourceStopSoundCall(uint32_t play_id) -> void; + void PushSourceFadeOutCall(uint32_t play_id, uint32_t time); + void PushSourceStopSoundCall(uint32_t play_id); - auto AddClientSource(AudioSource* source) -> void; + void AddClientSource(AudioSource* source); - auto MakeSourceAvailable(AudioSource* source) -> void; + void MakeSourceAvailable(AudioSource* source); auto available_sources_mutex() -> std::mutex& { return available_sources_mutex_; } @@ -75,6 +84,6 @@ class Audio { std::mutex available_sources_mutex_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_AUDIO_AUDIO_H_ +#endif // BALLISTICA_BASE_AUDIO_AUDIO_H_ diff --git a/src/ballistica/audio/audio_server.cc b/src/ballistica/base/audio/audio_server.cc similarity index 81% rename from src/ballistica/audio/audio_server.cc rename to src/ballistica/base/audio/audio_server.cc index 511d8a97..b1de41dd 100644 --- a/src/ballistica/audio/audio_server.cc +++ b/src/ballistica/base/audio/audio_server.cc @@ -1,35 +1,34 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/audio/audio_server.h" +#include "ballistica/base/audio/audio_server.h" -#include "ballistica/app/app.h" -#include "ballistica/assets/assets.h" -#include "ballistica/assets/data/sound_data.h" -#include "ballistica/audio/al_sys.h" -#include "ballistica/audio/audio.h" -#include "ballistica/audio/audio_source.h" -#include "ballistica/audio/audio_streamer.h" -#include "ballistica/audio/ogg_stream.h" -#include "ballistica/core/thread.h" -#include "ballistica/generic/timer.h" -#include "ballistica/logic/logic.h" -#include "ballistica/math/vector3f.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/assets/sound_asset.h" +#include "ballistica/base/audio/al_sys.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/audio/audio_source.h" +#include "ballistica/base/audio/audio_streamer.h" +#include "ballistica/base/audio/ogg_stream.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/core/core.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/math/vector3f.h" // Need to move away from OpenAL on Apple stuff. #if __clang__ #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #endif -namespace ballistica { - -// FIXME: move these to platform. -extern "C" void opensl_pause_playback(); -extern "C" void opensl_resume_playback(); +namespace ballistica::base { #if BA_RIFT_BUILD extern std::string g_rift_audio_device_name; #endif +// FIXME: move these to platform. +extern "C" void opensl_pause_playback(); +extern "C" void opensl_resume_playback(); + const int kAudioProcessIntervalNormal{500}; const int kAudioProcessIntervalFade{50}; const int kAudioProcessIntervalPendingLoad{1}; @@ -55,7 +54,7 @@ class AudioServer::ThreadSource : public Object { // not be used. ThreadSource(AudioServer* audio_thread, int id, bool* valid); ~ThreadSource() override; - auto Reset() -> void { + void Reset() { SetIsMusic(false); SetPositional(true); SetPosition(0, 0, 0); @@ -66,18 +65,18 @@ class AudioServer::ThreadSource : public Object { /// Set whether a sound is "music". /// This influences which volume controls affect it. - auto SetIsMusic(bool m) -> void; + void SetIsMusic(bool m); /// Set whether a source is positional. /// A non-positional source's position coords are always relative to the /// listener - ie: 0, 0, 0 will always be centered. - auto SetPositional(bool p) -> void; - auto SetPosition(float x, float y, float z) -> void; - auto SetGain(float g) -> void; - auto SetFade(float f) -> void; - auto SetLooping(bool loop) -> void; - auto Play(const Object::Ref* s) -> uint32_t; - auto Stop() -> void; + void SetPositional(bool p); + void SetPosition(float x, float y, float z); + void SetGain(float g); + void SetFade(float f); + void SetLooping(bool loop); + auto Play(const Object::Ref* s) -> uint32_t; + void Stop(); auto play_count() -> uint32_t { return play_count_; } auto is_streamed() const -> bool { return is_streamed_; } auto current_is_music() const -> bool { return current_is_music_; } @@ -86,18 +85,18 @@ class AudioServer::ThreadSource : public Object { auto play_id() const -> uint32_t { return (play_count_ << 16u) | (static_cast(id_) & 0xFFFFu); } - auto UpdateAvailability() -> void; - auto GetDefaultOwnerThread() const -> ThreadTag override; + void UpdateAvailability(); + auto GetDefaultOwnerThread() const -> EventLoopID override; auto client_source() const -> AudioSource* { return client_source_.get(); } - auto source_sound() const -> SoundData* { - return source_sound_ ? source_sound_->get() : nullptr; + auto source_sound() const -> SoundAsset* { + return source_sound_ ? source_sound_->Get() : nullptr; } - auto UpdatePitch() -> void; - auto UpdateVolume() -> void; - auto ExecStop() -> void; - auto ExecPlay() -> void; - auto Update() -> void; + void UpdatePitch(); + void UpdateVolume(); + void ExecStop(); + void ExecPlay(); + void Update(); private: bool looping_{}; @@ -106,7 +105,7 @@ class AudioServer::ThreadSource : public Object { float gain_{1.0f}; AudioServer* audio_thread_{}; bool valid_{}; - const Object::Ref* source_sound_{}; + const Object::Ref* source_sound_{}; int id_{}; uint32_t play_count_{}; bool is_actually_playing_{}; @@ -129,6 +128,140 @@ class AudioServer::ThreadSource : public Object { friend class AudioServer; }; // ThreadSource +AudioServer::AudioServer() : impl_{new AudioServer::Impl()} {} + +void AudioServer::OnMainThreadStartApp() { + // Spin up our thread. + event_loop_ = new EventLoop(EventLoopID::kAudio); + g_core->pausable_event_loops.push_back(event_loop_); + + // Run some setup stuff from our shiny new thread. + event_loop_->PushCall([this] { + // We want to be informed when our event-loop is pausing and unpausing. + event_loop()->AddPauseCallback( + NewLambdaRunnableUnmanaged([this] { OnThreadPause(); })); + event_loop()->AddResumeCallback( + NewLambdaRunnableUnmanaged([this] { OnThreadResume(); })); + }); + + event_loop_->PushCallSynchronous([this] { OnAppStartInThread(); }); +} + +void AudioServer::OnAppStartInThread() { + assert(g_base->InAudioThread()); + + // Get our thread to give us periodic processing time. + process_timer_ = + event_loop()->NewTimer(kAudioProcessIntervalNormal, true, + NewLambdaRunnable([this] { Process(); })); + +#if BA_ENABLE_AUDIO + + // Bring up OpenAL stuff. + { + const char* al_device_name = nullptr; + +// On the rift build in vr mode we need to make sure we open the rift audio +// device. +#if BA_RIFT_BUILD + if (g_core->IsVRMode()) { + ALboolean enumeration = + alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT"); + if (enumeration == AL_FALSE) { + Log(LogLevel::kError, "OpenAL enumeration extensions missing."); + } else { + const ALCchar* devices = + alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); + const ALCchar *device = devices, *next = devices + 1; + size_t len = 0; + + // If the string is blank, we weren't able to find the oculus + // audio device. In that case we'll just go with default. + if (g_rift_audio_device_name != "") { + // Log(LogLevel::kInfo, "AL Devices list:"); + // Log(LogLevel::kInfo, "----------"); + while (device && *device != '\0' && next && *next != '\0') { + // These names seem to be things like "OpenAL Soft on FOO" + // ..we should be able to search for FOO. + if (strstr(device, g_rift_audio_device_name.c_str())) { + al_device_name = device; + } + len = strlen(device); + device += (len + 1); + next += (len + 2); + } + // Log(LogLevel::kInfo, "----------"); + } + } + } +#endif // BA_RIFT_BUILD + + ALCdevice* device; + device = alcOpenDevice(al_device_name); + if (!device) { + FatalError( + "No audio devices found. Do you have speakers/headphones/etc. " + "connected?"); + } + impl_->alc_context_ = alcCreateContext(device, nullptr); + BA_PRECONDITION(impl_->alc_context_); + BA_PRECONDITION(alcMakeContextCurrent(impl_->alc_context_)); + CHECK_AL_ERROR; + } + + ALfloat listener_pos[] = {0.0f, 0.0f, 0.0f}; + ALfloat listener_vel[] = {0.0f, 0.0f, 0.0f}; + ALfloat listener_ori[] = {0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f}; + + alListenerfv(AL_POSITION, listener_pos); + alListenerfv(AL_VELOCITY, listener_vel); + alListenerfv(AL_ORIENTATION, listener_ori); + CHECK_AL_ERROR; + + // Create our sources. + int target_source_count = 30; + for (int i = 0; i < target_source_count; i++) { + bool valid = false; + auto s(Object::New(this, i, &valid)); + if (valid) { + s->client_source_ = std::make_unique(i); + g_base->audio->AddClientSource(&(*s->client_source_)); + sound_source_refs_.push_back(s); + sources_.push_back(&(*s)); + } else { + Log(LogLevel::kError, "Made " + std::to_string(i) + " sources; (wanted " + + std::to_string(target_source_count) + ")."); + break; + } + } + CHECK_AL_ERROR; + + // Now make available any stopped sources (should be all of them). + UpdateAvailableSources(); + +#endif // BA_ENABLE_AUDIO +} + +AudioServer::~AudioServer() { +#if BA_ENABLE_AUDIO + sound_source_refs_.clear(); + + // Take down AL stuff. + { + ALCdevice* device; + BA_PRECONDITION_LOG(alcMakeContextCurrent(nullptr)); + device = alcGetContextsDevice(impl_->alc_context_); + alcDestroyContext(impl_->alc_context_); + assert(alcGetError(device) == ALC_NO_ERROR); + alcCloseDevice(device); + } + assert(streaming_sources_.empty()); + assert(al_source_count_ == 0); + +#endif // BA_ENABLE_AUDIO + delete impl_; +} + struct AudioServer::SoundFadeNode { uint32_t play_id; millisecs_t starttime; @@ -136,8 +269,8 @@ struct AudioServer::SoundFadeNode { bool out; SoundFadeNode(uint32_t play_id_in, millisecs_t duration_in, bool out_in) : play_id(play_id_in), - starttime(GetRealTime()), - endtime(GetRealTime() + duration_in), + starttime(g_core->GetAppTimeMillisecs()), + endtime(g_core->GetAppTimeMillisecs() + duration_in), out(out_in) {} }; @@ -196,7 +329,7 @@ void AudioServer::SetPaused(bool pause) { } void AudioServer::PushSourceSetIsMusicCall(uint32_t play_id, bool val) { - thread()->PushCall([this, play_id, val] { + event_loop()->PushCall([this, play_id, val] { ThreadSource* s = GetPlayingSound(play_id); if (s) { s->SetIsMusic(val); @@ -205,7 +338,7 @@ void AudioServer::PushSourceSetIsMusicCall(uint32_t play_id, bool val) { } void AudioServer::PushSourceSetPositionalCall(uint32_t play_id, bool val) { - thread()->PushCall([this, play_id, val] { + event_loop()->PushCall([this, play_id, val] { ThreadSource* s = GetPlayingSound(play_id); if (s) { s->SetPositional(val); @@ -215,7 +348,7 @@ void AudioServer::PushSourceSetPositionalCall(uint32_t play_id, bool val) { void AudioServer::PushSourceSetPositionCall(uint32_t play_id, const Vector3f& p) { - thread()->PushCall([this, play_id, p] { + event_loop()->PushCall([this, play_id, p] { ThreadSource* s = GetPlayingSound(play_id); if (s) { s->SetPosition(p.x, p.y, p.z); @@ -224,7 +357,7 @@ void AudioServer::PushSourceSetPositionCall(uint32_t play_id, } void AudioServer::PushSourceSetGainCall(uint32_t play_id, float val) { - thread()->PushCall([this, play_id, val] { + event_loop()->PushCall([this, play_id, val] { ThreadSource* s = GetPlayingSound(play_id); if (s) { s->SetGain(val); @@ -233,7 +366,7 @@ void AudioServer::PushSourceSetGainCall(uint32_t play_id, float val) { } void AudioServer::PushSourceSetFadeCall(uint32_t play_id, float val) { - thread()->PushCall([this, play_id, val] { + event_loop()->PushCall([this, play_id, val] { ThreadSource* s = GetPlayingSound(play_id); if (s) { s->SetFade(val); @@ -242,7 +375,7 @@ void AudioServer::PushSourceSetFadeCall(uint32_t play_id, float val) { } void AudioServer::PushSourceSetLoopingCall(uint32_t play_id, bool val) { - thread()->PushCall([this, play_id, val] { + event_loop()->PushCall([this, play_id, val] { ThreadSource* s = GetPlayingSound(play_id); if (s) { s->SetLooping(val); @@ -251,8 +384,8 @@ void AudioServer::PushSourceSetLoopingCall(uint32_t play_id, bool val) { } void AudioServer::PushSourcePlayCall(uint32_t play_id, - Object::Ref* sound) { - thread()->PushCall([this, play_id, sound] { + Object::Ref* sound) { + event_loop()->PushCall([this, play_id, sound] { ThreadSource* s = GetPlayingSound(play_id); // If this play command is valid, pass it along. @@ -272,7 +405,7 @@ void AudioServer::PushSourcePlayCall(uint32_t play_id, } void AudioServer::PushSourceStopCall(uint32_t play_id) { - thread()->PushCall([this, play_id] { + event_loop()->PushCall([this, play_id] { ThreadSource* s = GetPlayingSound(play_id); if (s) { s->Stop(); @@ -281,7 +414,7 @@ void AudioServer::PushSourceStopCall(uint32_t play_id) { } void AudioServer::PushSourceEndCall(uint32_t play_id) { - thread()->PushCall([this, play_id] { + event_loop()->PushCall([this, play_id] { ThreadSource* s = GetPlayingSound(play_id); assert(s); s->client_source()->Lock(5); @@ -293,11 +426,11 @@ void AudioServer::PushSourceEndCall(uint32_t play_id) { } void AudioServer::PushResetCall() { - thread()->PushCall([this] { Reset(); }); + event_loop()->PushCall([this] { Reset(); }); } void AudioServer::PushSetListenerPositionCall(const Vector3f& p) { - thread()->PushCall([this, p] { + event_loop()->PushCall([this, p] { #if BA_ENABLE_AUDIO if (!paused_) { ALfloat lpos[3] = {p.x, p.y, p.z}; @@ -310,7 +443,7 @@ void AudioServer::PushSetListenerPositionCall(const Vector3f& p) { void AudioServer::PushSetListenerOrientationCall(const Vector3f& forward, const Vector3f& up) { - thread()->PushCall([this, forward, up] { + event_loop()->PushCall([this, forward, up] { #if BA_ENABLE_AUDIO if (!paused_) { ALfloat lorient[6] = {forward.x, forward.y, forward.z, up.x, up.y, up.z}; @@ -321,132 +454,6 @@ void AudioServer::PushSetListenerOrientationCall(const Vector3f& forward, }); } -AudioServer::AudioServer() : impl_{new AudioServer::Impl()} { - // We're a singleton; make sure we don't already exist. - assert(g_audio_server == nullptr); - - // Spin up our thread. - thread_ = new Thread(ThreadTag::kAudio); - g_app->pausable_threads.push_back(thread_); -} - -auto AudioServer::OnAppStart() -> void { - thread_->PushCallSynchronous([this] { OnAppStartInThread(); }); -} - -auto AudioServer::OnAppStartInThread() -> void { - assert(InAudioThread()); - thread()->AddPauseCallback(NewLambdaRunnableRaw([this] { OnThreadPause(); })); - thread()->AddResumeCallback( - NewLambdaRunnableRaw([this] { OnThreadResume(); })); - - // Get our thread to give us periodic processing time. - process_timer_ = thread()->NewTimer(kAudioProcessIntervalNormal, true, - NewLambdaRunnable([this] { Process(); })); - -#if BA_ENABLE_AUDIO - - // Bring up OpenAL stuff. - { - const char* al_device_name = nullptr; - -// On the rift build in vr mode we need to make sure we open the rift audio -// device. -#if BA_RIFT_BUILD - if (IsVRMode()) { - ALboolean enumeration = - alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT"); - if (enumeration == AL_FALSE) { - Log(LogLevel::kError, "OpenAL enumeration extensions missing."); - } else { - const ALCchar* devices = - alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); - const ALCchar *device = devices, *next = devices + 1; - size_t len = 0; - - // If the string is blank, we weren't able to find the oculus - // audio device. In that case we'll just go with default. - if (g_rift_audio_device_name != "") { - // Log(LogLevel::kInfo, "AL Devices list:"); - // Log(LogLevel::kInfo, "----------"); - while (device && *device != '\0' && next && *next != '\0') { - // These names seem to be things like "OpenAL Soft on FOO" - // ..we should be able to search for FOO. - if (strstr(device, g_rift_audio_device_name.c_str())) { - al_device_name = device; - } - len = strlen(device); - device += (len + 1); - next += (len + 2); - } - // Log(LogLevel::kInfo, "----------"); - } - } - } -#endif // BA_RIFT_BUILD - - ALCdevice* device; - device = alcOpenDevice(al_device_name); - BA_PRECONDITION(device); - impl_->alc_context_ = alcCreateContext(device, nullptr); - BA_PRECONDITION(impl_->alc_context_); - BA_PRECONDITION(alcMakeContextCurrent(impl_->alc_context_)); - CHECK_AL_ERROR; - } - - ALfloat listener_pos[] = {0.0f, 0.0f, 0.0f}; - ALfloat listener_vel[] = {0.0f, 0.0f, 0.0f}; - ALfloat listener_ori[] = {0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f}; - - alListenerfv(AL_POSITION, listener_pos); - alListenerfv(AL_VELOCITY, listener_vel); - alListenerfv(AL_ORIENTATION, listener_ori); - CHECK_AL_ERROR; - - // Create our sources. - int target_source_count = 30; - for (int i = 0; i < target_source_count; i++) { - bool valid = false; - auto s(Object::New(this, i, &valid)); - if (valid) { - s->client_source_ = std::make_unique(i); - g_audio->AddClientSource(&(*s->client_source_)); - sound_source_refs_.push_back(s); - sources_.push_back(&(*s)); - } else { - Log(LogLevel::kError, "Made " + std::to_string(i) + " sources; (wanted " - + std::to_string(target_source_count) + ")."); - break; - } - } - CHECK_AL_ERROR; - - // Now make available any stopped sources (should be all of them). - UpdateAvailableSources(); - -#endif // BA_ENABLE_AUDIO -} - -AudioServer::~AudioServer() { -#if BA_ENABLE_AUDIO - sound_source_refs_.clear(); - - // Take down AL stuff. - { - ALCdevice* device; - BA_PRECONDITION_LOG(alcMakeContextCurrent(nullptr)); - device = alcGetContextsDevice(impl_->alc_context_); - alcDestroyContext(impl_->alc_context_); - assert(alcGetError(device) == ALC_NO_ERROR); - alcCloseDevice(device); - } - assert(streaming_sources_.empty()); - assert(al_source_count_ == 0); - -#endif // BA_ENABLE_AUDIO - delete impl_; -} - void AudioServer::UpdateAvailableSources() { for (auto&& i : sources_) { i->UpdateAvailability(); @@ -456,7 +463,7 @@ void AudioServer::UpdateAvailableSources() { // and see how many are in use, how many are currently locked by the client, // etc. #if (BA_DEBUG_BUILD || BA_TEST_BUILD) - millisecs_t t = GetRealTime(); + millisecs_t t = g_core->GetAppTimeMillisecs(); if (t - last_sanity_check_time_ > 5000) { last_sanity_check_time_ = t; @@ -605,14 +612,13 @@ void AudioServer::UpdateMusicPlayState() { } void AudioServer::Process() { - millisecs_t real_time = GetRealTime(); - - assert(InAudioThread()); + assert(g_base->InAudioThread()); + millisecs_t real_time = g_core->GetAppTimeMillisecs(); // If we're paused we don't do nothin'. if (!paused_) { // Do some loading... - have_pending_loads_ = g_assets->RunPendingAudioLoads(); + have_pending_loads_ = g_base->assets->RunPendingAudioLoads(); // Keep that available-sources list filled. UpdateAvailableSources(); @@ -654,13 +660,14 @@ void AudioServer::ProcessSoundFades() { AudioServer::ThreadSource* s = GetPlayingSound(i->second.play_id); if (s) { - if (GetRealTime() > i->second.endtime) { + if (g_core->GetAppTimeMillisecs() > i->second.endtime) { StopSound(i->second.play_id); sound_fade_nodes_.erase(i); } else { float fade_val = 1 - - (static_cast(GetRealTime() - i->second.starttime) + - (static_cast(g_core->GetAppTimeMillisecs() + - i->second.starttime) / static_cast(i->second.endtime - i->second.starttime)); s->SetFade(fade_val); } @@ -677,8 +684,8 @@ void AudioServer::FadeSoundOut(uint32_t play_id, uint32_t time) { std::make_pair(play_id, SoundFadeNode(play_id, time, true))); } -void AudioServer::DeleteAssetComponent(AssetComponentData* c) { - assert(InAudioThread()); +void AudioServer::DeleteAssetComponent(Asset* c) { + assert(g_base->InAudioThread()); c->Unload(); delete c; } @@ -687,6 +694,7 @@ AudioServer::ThreadSource::ThreadSource(AudioServer* audio_thread_in, int id_in, bool* valid_out) : id_(id_in), audio_thread_(audio_thread_in) { #if BA_ENABLE_AUDIO + assert(g_core); assert(valid_out != nullptr); CHECK_AL_ERROR; @@ -700,7 +708,7 @@ AudioServer::ThreadSource::ThreadSource(AudioServer* audio_thread_in, int id_in, } else { // In vr mode we keep the microphone a bit closer to the camera // for realism purposes, so we need stuff louder in general. - if (IsVRMode()) { + if (g_core->IsVRMode()) { alSourcef(source_, AL_MAX_DISTANCE, 100); alSourcef(source_, AL_REFERENCE_DISTANCE, 7.5f); } else { @@ -747,14 +755,14 @@ AudioServer::ThreadSource::~ThreadSource() { #endif // BA_ENABLE_AUDIO } -auto AudioServer::ThreadSource::GetDefaultOwnerThread() const -> ThreadTag { - return ThreadTag::kAudio; +auto AudioServer::ThreadSource::GetDefaultOwnerThread() const -> EventLoopID { + return EventLoopID::kAudio; } void AudioServer::ThreadSource::UpdateAvailability() { #if BA_ENABLE_AUDIO - assert(InAudioThread()); + assert(g_base->InAudioThread()); // If it's waiting to be picked up by a client or has pending client commands, // skip. @@ -774,12 +782,12 @@ void AudioServer::ThreadSource::UpdateAvailability() { // If it's non-looping, we check its play state and snatch it if it's not // playing. bool busy; - if (looping_ || (is_streamed_ && streamer_.exists() && streamer_->loops())) { + if (looping_ || (is_streamed_ && streamer_.Exists() && streamer_->loops())) { busy = want_to_play_; } else { // If our context is paused, we know nothing is playing // (and we can't ask AL cuz we have no context). - if (g_audio_server->paused()) { + if (g_base->audio_server->paused()) { busy = false; } else { ALint state; @@ -794,8 +802,9 @@ void AudioServer::ThreadSource::UpdateAvailability() { // If we can't get a lock it's no biggie... we'll come back to this guy later. if (!busy) { - if (g_audio->available_sources_mutex().try_lock()) { - std::lock_guard lock(g_audio->available_sources_mutex(), std::adopt_lock); + if (g_base->audio->available_sources_mutex().try_lock()) { + std::lock_guard lock(g_base->audio->available_sources_mutex(), + std::adopt_lock); Stop(); Reset(); #if BA_DEBUG_BUILD @@ -833,7 +842,7 @@ void AudioServer::ThreadSource::SetFade(float f) { void AudioServer::ThreadSource::SetLooping(bool loop) { looping_ = loop; - if (!g_audio_server->paused()) { + if (!g_base->audio_server->paused()) { #if BA_ENABLE_AUDIO alSourcei(source_, AL_LOOPING, loop); CHECK_AL_ERROR; @@ -843,7 +852,7 @@ void AudioServer::ThreadSource::SetLooping(bool loop) { void AudioServer::ThreadSource::SetPositional(bool p) { #if BA_ENABLE_AUDIO - if (!g_audio_server->paused()) { + if (!g_base->audio_server->paused()) { // TODO(ericf): Don't allow setting of positional // on stereo sounds - we check this at initial play() // but should do it here too. @@ -855,7 +864,7 @@ void AudioServer::ThreadSource::SetPositional(bool p) { void AudioServer::ThreadSource::SetPosition(float x, float y, float z) { #if BA_ENABLE_AUDIO - if (!g_audio_server->paused()) { + if (!g_base->audio_server->paused()) { bool oob = false; if (x < -500) { oob = true; @@ -894,7 +903,8 @@ void AudioServer::ThreadSource::SetPosition(float x, float y, float z) { void AudioServer::ThreadSource::ExecPlay() { #if BA_ENABLE_AUDIO - assert(source_sound_->exists()); + assert(g_core); + assert(source_sound_->Exists()); assert((**source_sound_).valid()); assert((**source_sound_).loaded()); assert(!is_actually_playing_); @@ -921,7 +931,7 @@ void AudioServer::ThreadSource::ExecPlay() { bool do_normal = true; // In vr mode, play non-positional sounds positionally in space roughly // where the menu is. - if (IsVRMode()) { + if (g_core->IsVRMode()) { do_normal = false; SetPositional(true); SetPosition(0.0f, 4.5f, -3.0f); @@ -953,14 +963,14 @@ void AudioServer::ThreadSource::ExecPlay() { #endif // BA_ENABLE_AUDIO } -auto AudioServer::ThreadSource::Play(const Object::Ref* sound) +auto AudioServer::ThreadSource::Play(const Object::Ref* sound) -> uint32_t { #if BA_ENABLE_AUDIO // FatalError("Testing other thread."); - assert(InAudioThread()); - assert(sound->exists()); + assert(g_base->InAudioThread()); + assert(sound->Exists()); // Stop whatever we were doing. Stop(); @@ -968,7 +978,7 @@ auto AudioServer::ThreadSource::Play(const Object::Ref* sound) assert(source_sound_ == nullptr); source_sound_ = sound; - if (!g_audio_server->paused()) { + if (!g_base->audio_server->paused()) { // Ok, here's where we might start needing to access our media... can't hold // off any longer... (**source_sound_).Load(); @@ -980,7 +990,8 @@ auto AudioServer::ThreadSource::Play(const Object::Ref* sound) streamer_ = Object::New( (**source_sound_).file_name_full().c_str(), source_, looping_); } else { - alSourcei(source_, AL_BUFFER, (**source_sound_).buffer()); + alSourcei(source_, AL_BUFFER, + static_cast((**source_sound_).buffer())); } CHECK_AL_ERROR; @@ -989,8 +1000,8 @@ auto AudioServer::ThreadSource::Play(const Object::Ref* sound) UpdateVolume(); UpdatePitch(); - bool music_should_play = ((g_audio_server->music_volume_ > 0.000001f) - && !g_audio_server->paused()); + bool music_should_play = ((g_base->audio_server->music_volume_ > 0.000001f) + && !g_base->audio_server->paused()); if ((!current_is_music_) || music_should_play) { ExecPlay(); } @@ -1004,10 +1015,10 @@ auto AudioServer::ThreadSource::Play(const Object::Ref* sound) void AudioServer::ThreadSource::ExecStop() { #if BA_ENABLE_AUDIO - assert(InAudioThread()); - assert(!g_audio_server->paused()); + assert(g_base->InAudioThread()); + assert(!g_base->audio_server->paused()); assert(is_actually_playing_); - if (streamer_.exists()) { + if (streamer_.Exists()) { assert(is_streamed_); streamer_->Stop(); for (auto i = audio_thread_->streaming_sources_.begin(); @@ -1030,23 +1041,23 @@ void AudioServer::ThreadSource::ExecStop() { // Do a complete stop... take us off the music list, detach our source, etc. void AudioServer::ThreadSource::Stop() { #if BA_ENABLE_AUDIO - assert(g_audio_server); + assert(g_base->audio_server); // If our context is paused we can't actually stop now; just record our // intent. - if (g_audio_server->paused()) { + if (g_base->audio_server->paused()) { want_to_play_ = false; } else { if (is_actually_playing_) ExecStop(); - if (streamer_.exists()) { + if (streamer_.Exists()) { streamer_.Clear(); } // If we've got an attached sound, toss it back to the main thread // to free up... // (we can't kill media-refs outside the main thread) if (source_sound_) { - assert(g_assets); - g_audio_server->AddSoundRefDelete(source_sound_); + assert(g_base->assets); + g_base->audio_server->AddSoundRefDelete(source_sound_); source_sound_ = nullptr; } want_to_play_ = false; @@ -1056,8 +1067,8 @@ void AudioServer::ThreadSource::Stop() { void AudioServer::ThreadSource::UpdateVolume() { #if BA_ENABLE_AUDIO - assert(InAudioThread()); - if (!g_audio_server->paused()) { + assert(g_base->InAudioThread()); + if (!g_base->audio_server->paused()) { float val = gain_ * fade_; if (current_is_music()) { val *= audio_thread_->music_volume() / 7.0f; @@ -1072,8 +1083,8 @@ void AudioServer::ThreadSource::UpdateVolume() { void AudioServer::ThreadSource::UpdatePitch() { #if BA_ENABLE_AUDIO - assert(InAudioThread()); - if (!g_audio_server->paused()) { + assert(g_base->InAudioThread()); + if (!g_base->audio_server->paused()) { float val = 1.0f; if (current_is_music()) { } else { @@ -1086,18 +1097,18 @@ void AudioServer::ThreadSource::UpdatePitch() { } void AudioServer::PushSetVolumesCall(float music_volume, float sound_volume) { - thread()->PushCall([this, music_volume, sound_volume] { + event_loop()->PushCall([this, music_volume, sound_volume] { SetSoundVolume(sound_volume); SetMusicVolume(music_volume); }); } void AudioServer::PushSetSoundPitchCall(float val) { - thread()->PushCall([this, val] { SetSoundPitch(val); }); + event_loop()->PushCall([this, val] { SetSoundPitch(val); }); } void AudioServer::PushSetPausedCall(bool pause) { - thread()->PushCall([this, pause] { + event_loop()->PushCall([this, pause] { if (g_buildconfig.ostype_android()) { Log(LogLevel::kError, "Shouldn't be getting SetPausedCall on android."); } @@ -1106,82 +1117,85 @@ void AudioServer::PushSetPausedCall(bool pause) { } void AudioServer::PushComponentUnloadCall( - const std::vector*>& components) { - thread()->PushCall([this, components] { - // Unload all components we were passed... + const std::vector*>& components) { + event_loop()->PushCall([components] { + // Unload the components. for (auto&& i : components) { (**i).Unload(); } - // ...and then ship these pointers back to the logic thread, so it can free - // the references. - g_logic->PushFreeAssetComponentRefsCall(components); + // Then kick them over to the logic thread for deletion. + g_base->logic->event_loop()->PushCall([components] { + for (auto&& i : components) { + delete i; + } + }); }); } void AudioServer::PushHavePendingLoadsCall() { - thread()->PushCall([this] { + event_loop()->PushCall([this] { have_pending_loads_ = true; UpdateTimerInterval(); }); } -void AudioServer::AddSoundRefDelete(const Object::Ref* c) { +void AudioServer::AddSoundRefDelete(const Object::Ref* c) { { std::scoped_lock lock(sound_ref_delete_list_mutex_); sound_ref_delete_list_.push_back(c); } // Now push a call to the logic thread to do the deletes. - g_logic->thread()->PushCall( - [] { g_audio_server->ClearSoundRefDeleteList(); }); + g_base->logic->event_loop()->PushCall( + [] { g_base->audio_server->ClearSoundRefDeleteList(); }); } void AudioServer::ClearSoundRefDeleteList() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); std::scoped_lock lock(sound_ref_delete_list_mutex_); - for (const Object::Ref* i : sound_ref_delete_list_) { + for (const Object::Ref* i : sound_ref_delete_list_) { delete i; } sound_ref_delete_list_.clear(); } void AudioServer::BeginInterruption() { - assert(!InAudioThread()); - g_audio_server->PushSetPausedCall(true); + assert(!g_base->InAudioThread()); + g_base->audio_server->PushSetPausedCall(true); // Wait a reasonable amount of time for the thread to act on it. - millisecs_t t = GetRealTime(); + millisecs_t t = g_core->GetAppTimeMillisecs(); while (true) { - if (g_audio_server->paused()) { + if (g_base->audio_server->paused()) { break; } - if (GetRealTime() - t > 1000) { + if (g_core->GetAppTimeMillisecs() - t > 1000) { Log(LogLevel::kError, "Timed out waiting for audio pause."); break; } - Platform::SleepMS(2); + core::CorePlatform::SleepMillisecs(2); } } -auto AudioServer::OnThreadPause() -> void { SetPaused(true); } +void AudioServer::OnThreadPause() { SetPaused(true); } -auto AudioServer::OnThreadResume() -> void { SetPaused(false); } +void AudioServer::OnThreadResume() { SetPaused(false); } void AudioServer::EndInterruption() { - assert(!InAudioThread()); - g_audio_server->PushSetPausedCall(false); + assert(!g_base->InAudioThread()); + g_base->audio_server->PushSetPausedCall(false); // Wait a reasonable amount of time for the thread to act on it. - millisecs_t t = GetRealTime(); + millisecs_t t = g_core->GetAppTimeMillisecs(); while (true) { - if (!g_audio_server->paused()) { + if (!g_base->audio_server->paused()) { break; } - if (GetRealTime() - t > 1000) { + if (g_core->GetAppTimeMillisecs() - t > 1000) { Log(LogLevel::kError, "Timed out waiting for audio unpause."); break; } - Platform::SleepMS(2); + core::CorePlatform::SleepMillisecs(2); } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/audio/audio_server.h b/src/ballistica/base/audio/audio_server.h similarity index 52% rename from src/ballistica/audio/audio_server.h rename to src/ballistica/base/audio/audio_server.h index e8d6720e..1d7dbd55 100644 --- a/src/ballistica/audio/audio_server.h +++ b/src/ballistica/base/audio/audio_server.h @@ -1,15 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_AUDIO_AUDIO_SERVER_H_ -#define BALLISTICA_AUDIO_AUDIO_SERVER_H_ +#ifndef BALLISTICA_BASE_AUDIO_AUDIO_SERVER_H_ +#define BALLISTICA_BASE_AUDIO_AUDIO_SERVER_H_ #include #include #include -#include "ballistica/core/object.h" +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::base { /// A module that handles audio processing. class AudioServer { @@ -23,64 +24,63 @@ class AudioServer { } AudioServer(); - auto OnAppStart() -> void; + void OnMainThreadStartApp(); - auto PushSetVolumesCall(float music_volume, float sound_volume) -> void; - auto PushSetSoundPitchCall(float val) -> void; - auto PushSetPausedCall(bool pause) -> void; + void PushSetVolumesCall(float music_volume, float sound_volume); + void PushSetSoundPitchCall(float val); + void PushSetPausedCall(bool pause); - static auto BeginInterruption() -> void; - static auto EndInterruption() -> void; + static void BeginInterruption(); + static void EndInterruption(); - auto PushSetListenerPositionCall(const Vector3f& p) -> void; - auto PushSetListenerOrientationCall(const Vector3f& forward, - const Vector3f& up) -> void; - auto PushResetCall() -> void; - auto PushHavePendingLoadsCall() -> void; - auto PushComponentUnloadCall( - const std::vector*>& components) -> void; + void PushSetListenerPositionCall(const Vector3f& p); + void PushSetListenerOrientationCall(const Vector3f& forward, + const Vector3f& up); + void PushResetCall(); + void PushHavePendingLoadsCall(); + void PushComponentUnloadCall( + const std::vector*>& components); /// For use by g_logic_module(). - auto ClearSoundRefDeleteList() -> void; + void ClearSoundRefDeleteList(); auto paused() const -> bool { return paused_; } // Client sources use these to pass settings to the server. - auto PushSourceSetIsMusicCall(uint32_t play_id, bool val) -> void; - auto PushSourceSetPositionalCall(uint32_t play_id, bool val) -> void; - auto PushSourceSetPositionCall(uint32_t play_id, const Vector3f& p) -> void; - auto PushSourceSetGainCall(uint32_t play_id, float val) -> void; - auto PushSourceSetFadeCall(uint32_t play_id, float val) -> void; - auto PushSourceSetLoopingCall(uint32_t play_id, bool val) -> void; - auto PushSourcePlayCall(uint32_t play_id, Object::Ref* sound) - -> void; - auto PushSourceStopCall(uint32_t play_id) -> void; - auto PushSourceEndCall(uint32_t play_id) -> void; + void PushSourceSetIsMusicCall(uint32_t play_id, bool val); + void PushSourceSetPositionalCall(uint32_t play_id, bool val); + void PushSourceSetPositionCall(uint32_t play_id, const Vector3f& p); + void PushSourceSetGainCall(uint32_t play_id, float val); + void PushSourceSetFadeCall(uint32_t play_id, float val); + void PushSourceSetLoopingCall(uint32_t play_id, bool val); + void PushSourcePlayCall(uint32_t play_id, Object::Ref* sound); + void PushSourceStopCall(uint32_t play_id); + void PushSourceEndCall(uint32_t play_id); // Fade a playing sound out over the given time. If it is already // fading or does not exist, does nothing. - auto FadeSoundOut(uint32_t play_id, uint32_t time) -> void; + void FadeSoundOut(uint32_t play_id, uint32_t time); // Stop a sound from playing if it exists. - auto StopSound(uint32_t play_id) -> void; + void StopSound(uint32_t play_id); - auto thread() const -> Thread* { return thread_; } + auto event_loop() const -> EventLoop* { return event_loop_; } private: class ThreadSource; struct Impl; - auto OnAppStartInThread() -> void; + void OnAppStartInThread(); ~AudioServer(); - auto OnThreadPause() -> void; - auto OnThreadResume() -> void; + void OnThreadPause(); + void OnThreadResume(); - auto SetPaused(bool paused) -> void; + void SetPaused(bool paused); - auto SetMusicVolume(float volume) -> void; - auto SetSoundVolume(float volume) -> void; - auto SetSoundPitch(float pitch) -> void; + void SetMusicVolume(float volume); + void SetSoundVolume(float volume); + void SetSoundPitch(float pitch); auto music_volume() -> float { return music_volume_; } auto sound_volume() -> float { return sound_volume_; } auto sound_pitch() -> float { return sound_pitch_; } @@ -88,29 +88,29 @@ class AudioServer { /// If a sound play id is currently playing, return the sound. auto GetPlayingSound(uint32_t play_id) -> ThreadSource*; - auto Reset() -> void; - auto Process() -> void; + void Reset(); + void Process(); /// Send a component to the audio thread to delete. - auto DeleteAssetComponent(AssetComponentData* c) -> void; + void DeleteAssetComponent(Asset* c); - auto UpdateTimerInterval() -> void; - auto UpdateAvailableSources() -> void; - auto UpdateMusicPlayState() -> void; - auto ProcessSoundFades() -> void; + void UpdateTimerInterval(); + void UpdateAvailableSources(); + void UpdateMusicPlayState(); + void ProcessSoundFades(); // Some threads such as audio hold onto allocated Media-Component-Refs to keep // media components alive that they need. Media-Component-Refs, however, must // be disposed of in the logic thread, so they are passed back to it through // this function. - auto AddSoundRefDelete(const Object::Ref* c) -> void; + void AddSoundRefDelete(const Object::Ref* c); // Note: should use unique_ptr for this, but build fails on raspberry pi // (gcc 8.3.0). Works on Ubuntu 9.3 so should try again later. // std::unique_ptr impl_{}; Impl* impl_{}; - Thread* thread_{}; + EventLoop* event_loop_{}; Timer* process_timer_{}; bool have_pending_loads_{}; bool paused_{}; @@ -139,13 +139,13 @@ class AudioServer { std::mutex sound_ref_delete_list_mutex_; // Our list of sound media components to delete via the main thread. - std::vector*> sound_ref_delete_list_; + std::vector*> sound_ref_delete_list_; millisecs_t last_sanity_check_time_{}; static int al_source_count_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_AUDIO_AUDIO_SERVER_H_ +#endif // BALLISTICA_BASE_AUDIO_AUDIO_SERVER_H_ diff --git a/src/ballistica/audio/audio_source.cc b/src/ballistica/base/audio/audio_source.cc similarity index 61% rename from src/ballistica/audio/audio_source.cc rename to src/ballistica/base/audio/audio_source.cc index 4e391f34..541d6d89 100644 --- a/src/ballistica/audio/audio_source.cc +++ b/src/ballistica/base/audio/audio_source.cc @@ -1,13 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/audio/audio_source.h" +#include "ballistica/base/audio/audio_source.h" -#include "ballistica/assets/data/sound_data.h" -#include "ballistica/audio/audio.h" -#include "ballistica/audio/audio_server.h" -#include "ballistica/math/vector3f.h" +#include "ballistica/base/assets/sound_asset.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/audio/audio_server.h" +#include "ballistica/core/core.h" +#include "ballistica/shared/math/vector3f.h" -namespace ballistica { +namespace ballistica::base { AudioSource::AudioSource(int id_in) : id_(id_in) {} @@ -19,55 +20,55 @@ void AudioSource::MakeAvailable(uint32_t play_id_new) { assert(locked()); play_id_ = play_id_new; assert(!available_); - assert(g_audio); - g_audio->MakeSourceAvailable(this); + assert(g_base->audio); + g_base->audio->MakeSourceAvailable(this); available_ = true; } void AudioSource::SetIsMusic(bool val) { - assert(g_audio_server); + assert(g_base->audio_server); assert(client_queue_size_ > 0); - g_audio_server->PushSourceSetIsMusicCall(play_id_, val); + g_base->audio_server->PushSourceSetIsMusicCall(play_id_, val); } void AudioSource::SetPositional(bool val) { - assert(g_audio_server); + assert(g_base->audio_server); assert(client_queue_size_ > 0); - g_audio_server->PushSourceSetPositionalCall(play_id_, val); + g_base->audio_server->PushSourceSetPositionalCall(play_id_, val); } void AudioSource::SetPosition(float x, float y, float z) { - assert(g_audio_server); + assert(g_base->audio_server); assert(client_queue_size_ > 0); #if BA_DEBUG_BUILD if (std::isnan(x) || std::isnan(y) || std::isnan(z)) { Log(LogLevel::kError, "Got nan value in AudioSource::SetPosition."); } #endif - g_audio_server->PushSourceSetPositionCall(play_id_, Vector3f(x, y, z)); + g_base->audio_server->PushSourceSetPositionCall(play_id_, Vector3f(x, y, z)); } void AudioSource::SetGain(float val) { - assert(g_audio_server); + assert(g_base->audio_server); assert(client_queue_size_ > 0); - g_audio_server->PushSourceSetGainCall(play_id_, val); + g_base->audio_server->PushSourceSetGainCall(play_id_, val); } void AudioSource::SetFade(float val) { - assert(g_audio_server); + assert(g_base->audio_server); assert(client_queue_size_ > 0); - g_audio_server->PushSourceSetFadeCall(play_id_, val); + g_base->audio_server->PushSourceSetFadeCall(play_id_, val); } void AudioSource::SetLooping(bool val) { - assert(g_audio_server); + assert(g_base->audio_server); assert(client_queue_size_ > 0); - g_audio_server->PushSourceSetLoopingCall(play_id_, val); + g_base->audio_server->PushSourceSetLoopingCall(play_id_, val); } -auto AudioSource::Play(SoundData* ptr_in) -> uint32_t { +auto AudioSource::Play(SoundAsset* ptr_in) -> uint32_t { assert(ptr_in); - assert(g_audio_server); + assert(g_base->audio_server); assert(client_queue_size_ > 0); // allocate a new reference to this guy and pass it along @@ -77,22 +78,22 @@ auto AudioSource::Play(SoundData* ptr_in) -> uint32_t { // with it for the main thread to destroy. ptr_in->UpdatePlayTime(); - auto ptr = new Object::Ref(ptr_in); - g_audio_server->PushSourcePlayCall(play_id_, ptr); + auto ptr = new Object::Ref(ptr_in); + g_base->audio_server->PushSourcePlayCall(play_id_, ptr); return play_id_; } void AudioSource::Stop() { - assert(g_audio_server); + assert(g_base->audio_server); assert(client_queue_size_ > 0); - g_audio_server->PushSourceStopCall(play_id_); + g_base->audio_server->PushSourceStopCall(play_id_); } void AudioSource::End() { assert(client_queue_size_ > 0); // send the thread a "this source is potentially free now" message - assert(g_audio_server); - g_audio_server->PushSourceEndCall(play_id_); + assert(g_base->audio_server); + g_base->audio_server->PushSourceEndCall(play_id_); Unlock(); } @@ -100,7 +101,7 @@ void AudioSource::Lock(int debug_id) { BA_DEBUG_FUNCTION_TIMER_BEGIN(); mutex_.lock(); #if BA_DEBUG_BUILD - last_lock_time_ = GetRealTime(); + last_lock_time_ = g_core->GetAppTimeMillisecs(); lock_debug_id_ = debug_id; locked_ = true; #endif @@ -112,7 +113,7 @@ auto AudioSource::TryLock(int debug_id) -> bool { #if (BA_DEBUG_BUILD || BA_TEST_BUILD) if (locked) { locked_ = true; - last_lock_time_ = GetRealTime(); + last_lock_time_ = g_core->GetAppTimeMillisecs(); lock_debug_id_ = debug_id; } #endif @@ -128,4 +129,4 @@ void AudioSource::Unlock() { #endif } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/audio/audio_source.h b/src/ballistica/base/audio/audio_source.h similarity index 77% rename from src/ballistica/audio/audio_source.h rename to src/ballistica/base/audio/audio_source.h index 3663d77c..675d073c 100644 --- a/src/ballistica/audio/audio_source.h +++ b/src/ballistica/base/audio/audio_source.h @@ -1,13 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_AUDIO_AUDIO_SOURCE_H_ -#define BALLISTICA_AUDIO_AUDIO_SOURCE_H_ +#ifndef BALLISTICA_BASE_AUDIO_AUDIO_SOURCE_H_ +#define BALLISTICA_BASE_AUDIO_AUDIO_SOURCE_H_ #include -#include "ballistica/core/object.h" +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::base { // Location for sound emission (client version) class AudioSource { @@ -25,7 +26,7 @@ class AudioSource { void SetGain(float g); void SetFade(float f); void SetLooping(bool loop); - auto Play(SoundData* ptr) -> uint32_t; + auto Play(SoundAsset* ptr) -> uint32_t; void Stop(); // Always call this when done sending commands to the source. @@ -56,16 +57,16 @@ class AudioSource { private: std::mutex mutex_; #if BA_DEBUG_BUILD || BA_TEST_BUILD - millisecs_t last_lock_time_ = 0; - int lock_debug_id_ = 0; - bool locked_ = false; + millisecs_t last_lock_time_{}; + int lock_debug_id_{}; + bool locked_{}; #endif - int client_queue_size_ = 0; - bool available_ = false; - int id_ = 0; - uint32_t play_id_ = 0; + int client_queue_size_{}; + bool available_{}; + int id_{}; + uint32_t play_id_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_AUDIO_AUDIO_SOURCE_H_ +#endif // BALLISTICA_BASE_AUDIO_AUDIO_SOURCE_H_ diff --git a/src/ballistica/audio/audio_streamer.cc b/src/ballistica/base/audio/audio_streamer.cc similarity index 92% rename from src/ballistica/audio/audio_streamer.cc rename to src/ballistica/base/audio/audio_streamer.cc index 9cdee4a1..60e03fa5 100644 --- a/src/ballistica/audio/audio_streamer.cc +++ b/src/ballistica/base/audio/audio_streamer.cc @@ -1,28 +1,28 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/audio/audio_streamer.h" +#include "ballistica/base/audio/audio_streamer.h" -#include "ballistica/audio/audio.h" -#include "ballistica/audio/audio_server.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/audio/audio_server.h" // Need to move away from OpenAL on Apple stuff. #if __clang__ #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #endif -namespace ballistica { +namespace ballistica::base { #if BA_ENABLE_AUDIO AudioStreamer::AudioStreamer(const char* file_name, ALuint source_in, bool loop) : source_(source_in), file_name_(file_name), loops_(loop) { - assert(InAudioThread()); + assert(g_base->InAudioThread()); alGenBuffers(kAudioStreamBufferCount, buffers_); CHECK_AL_ERROR; } AudioStreamer::~AudioStreamer() { assert(!playing_); - assert(g_audio_server); + assert(g_base->audio_server); alDeleteBuffers(kAudioStreamBufferCount, buffers_); CHECK_AL_ERROR; @@ -154,4 +154,4 @@ auto AudioStreamer::Stream(ALuint buffer) -> bool { #endif // BA_ENABLE_AUDIO -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/audio/audio_streamer.h b/src/ballistica/base/audio/audio_streamer.h similarity index 74% rename from src/ballistica/audio/audio_streamer.h rename to src/ballistica/base/audio/audio_streamer.h index 15eb8012..e4104fa6 100644 --- a/src/ballistica/audio/audio_streamer.h +++ b/src/ballistica/base/audio/audio_streamer.h @@ -1,22 +1,22 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_AUDIO_AUDIO_STREAMER_H_ -#define BALLISTICA_AUDIO_AUDIO_STREAMER_H_ +#ifndef BALLISTICA_BASE_AUDIO_AUDIO_STREAMER_H_ +#define BALLISTICA_BASE_AUDIO_AUDIO_STREAMER_H_ #include #include -#include "ballistica/audio/al_sys.h" // FIXME: shouldn't need this here. -#include "ballistica/core/object.h" +#include "ballistica/base/audio/al_sys.h" // FIXME: shouldn't need this here. +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::base { #if BA_ENABLE_AUDIO // Provider for streamed audio data. class AudioStreamer : public Object { public: - auto GetDefaultOwnerThread() const -> ThreadTag override { - return ThreadTag::kAudio; + auto GetDefaultOwnerThread() const -> EventLoopID override { + return EventLoopID::kAudio; } AudioStreamer(const char* file_name, ALuint source, bool loop); ~AudioStreamer() override; @@ -57,6 +57,6 @@ class AudioStreamer : public Object { #endif // BA_ENABLE_AUDIO -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_AUDIO_AUDIO_STREAMER_H_ +#endif // BALLISTICA_BASE_AUDIO_AUDIO_STREAMER_H_ diff --git a/src/ballistica/audio/ogg_stream.cc b/src/ballistica/base/audio/ogg_stream.cc similarity index 93% rename from src/ballistica/audio/ogg_stream.cc rename to src/ballistica/base/audio/ogg_stream.cc index 68b40ef6..abe45317 100644 --- a/src/ballistica/audio/ogg_stream.cc +++ b/src/ballistica/base/audio/ogg_stream.cc @@ -1,10 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/audio/ogg_stream.h" +#include "ballistica/base/audio/ogg_stream.h" -#include "ballistica/platform/platform.h" +#include "ballistica/base/base.h" +#include "ballistica/core/core.h" +#include "ballistica/core/platform/core_platform.h" -namespace ballistica { +namespace ballistica::base { #if BA_ENABLE_AUDIO @@ -29,7 +31,7 @@ OggStream::OggStream(const char* file_name, ALuint source, bool loop) : AudioStreamer(file_name, source, loop), have_ogg_file_(false) { int result; FILE* f; - if (!(f = g_platform->FOpen(file_name, "rb"))) { + if (!(f = g_core->platform->FOpen(file_name, "rb"))) { throw Exception("can't open ogg file: '" + std::string(file_name) + "'"); } ov_callbacks callbacks; @@ -132,4 +134,4 @@ auto OggStream::GetErrorString(int code) -> std::string { #endif // BA_ENABLE_AUDIO -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/audio/ogg_stream.h b/src/ballistica/base/audio/ogg_stream.h similarity index 75% rename from src/ballistica/audio/ogg_stream.h rename to src/ballistica/base/audio/ogg_stream.h index caec2b39..143c0a6e 100644 --- a/src/ballistica/audio/ogg_stream.h +++ b/src/ballistica/base/audio/ogg_stream.h @@ -1,9 +1,9 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_AUDIO_OGG_STREAM_H_ -#define BALLISTICA_AUDIO_OGG_STREAM_H_ +#ifndef BALLISTICA_BASE_AUDIO_OGG_STREAM_H_ +#define BALLISTICA_BASE_AUDIO_OGG_STREAM_H_ -#include "ballistica/audio/audio_streamer.h" +#include "ballistica/base/audio/audio_streamer.h" #if BA_ENABLE_AUDIO #if BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID @@ -15,7 +15,7 @@ #include -namespace ballistica { +namespace ballistica::base { #if BA_ENABLE_AUDIO @@ -38,6 +38,6 @@ class OggStream : public AudioStreamer { #endif // BA_ENABLE_AUDIO -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_AUDIO_OGG_STREAM_H_ +#endif // BALLISTICA_BASE_AUDIO_OGG_STREAM_H_ diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc new file mode 100644 index 00000000..65a509a8 --- /dev/null +++ b/src/ballistica/base/base.cc @@ -0,0 +1,516 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/base.h" + +#include "ballistica/base/app/app.h" +#include "ballistica/base/app/app_config.h" +#include "ballistica/base/app/app_mode_empty.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/assets/assets_server.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/audio/audio_server.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_server.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/graphics/text/text_graphics.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/networking/network_reader.h" +#include "ballistica/base/networking/network_writer.h" +#include "ballistica/base/networking/networking.h" +#include "ballistica/base/platform/base_platform.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/python/class/python_class_feature_set_data.h" +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/base/support/huffman.h" +#include "ballistica/base/support/plus_soft.h" +#include "ballistica/base/support/stdio_console.h" +#include "ballistica/base/ui/console.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/classic/classic.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/core/python/core_python.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/foundation/logging.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_command.h" + +// TEMP. +extern auto TempSV1CreateAppMode() -> ballistica::base::AppMode*; + +namespace ballistica::base { + +core::CoreFeatureSet* g_core{}; +BaseFeatureSet* g_base{}; +PlusSoftInterface* g_plus_soft{}; +classic::ClassicFeatureSet* g_classic{}; +ui_v1::UIV1FeatureSet* g_ui_v1{}; + +BaseFeatureSet::BaseFeatureSet() + : python{new BasePython()}, + platform{BasePlatform::CreatePlatform()}, + audio{new Audio()}, + utils{new Utils()}, + logic{new Logic()}, + huffman{new Huffman()}, + ui{new UI()}, + networking{new Networking()}, + app{BasePlatform::CreateApp()}, + context_ref{new ContextRef(nullptr)}, + network_reader{new NetworkReader()}, + network_writer{new NetworkWriter()}, + assets_server{new AssetsServer()}, + bg_dynamics{g_core->HeadlessMode() ? nullptr : new BGDynamics}, + bg_dynamics_server{g_core->HeadlessMode() ? nullptr + : new BGDynamicsServer}, + app_config{new AppConfig()}, + graphics{BasePlatform::CreateGraphics()}, + graphics_server{new GraphicsServer()}, + input{new Input()}, + text_graphics{new TextGraphics()}, + audio_server{new AudioServer()}, + assets{new Assets()}, + app_mode{TempSV1CreateAppMode()}, + // app_mode{AppModeEmpty::GetSingleton()}, + stdio_console{g_buildconfig.enable_stdio_console() ? new StdioConsole() + : nullptr} { + // We're a singleton. If there's already one of us, something's wrong. + assert(g_base == nullptr); +} + +void BaseFeatureSet::OnModuleExec(PyObject* module) { + // Ok, our feature-set's Python module is getting imported. + // Like any normal Python module, we take this opportunity to + // import/create the stuff we use. + + // Importing core should always be the first thing we do. + // Various ballistica functionality will fail if this has not been done. + assert(g_core == nullptr); + g_core = core::CoreFeatureSet::Import(); + + // Want to run this at the last possible moment before spinning up + // our BaseFeatureSet. This locks in baenv customizations. + g_core->python->ApplyBaEnvConfig(); + + g_core->BootLog("_babase exec begin"); + + // Create our feature-set's C++ front-end. + assert(g_base == nullptr); + g_base = new BaseFeatureSet(); + + // Core uses some of our functionality when we're present. Let them + // know we're now present. + core::g_base_soft = g_base; + + g_base->python->AddPythonClasses(module); + + // Store our C++ front-end with our Python module. + // This lets anyone get at us by going through the Python + // import system (keeping things nice and consistent between + // Python and C++ worlds). + g_base->StoreOnPythonModule(module); + + // baenv can now feed us logs and run some checks. + g_core->python->RunBaEnvOnBaBaseImport(); + + // ..and because baenv is now feeding us logs, we can push any logs through + // that we've been holding on to. + g_core->python->EnablePythonLoggingCalls(); + + g_base->python->ImportPythonObjs(); + + // Read the app config. Should this perhaps go in StartApp or something? + g_base->python->ReadConfig(); + + // Import any other C++ feature-set-front-ends we use. + // FIXME: neither of these should be here. + assert(g_classic == nullptr); + g_classic = classic::ClassicFeatureSet::Import(); + assert(g_ui_v1 == nullptr); + g_ui_v1 = ui_v1::UIV1FeatureSet::Import(); + + // Marker we pop down at the very end so other modules can run sanity + // checks to make sure we aren't importing them reciprocally when they + // import us. + Python::MarkReachedEndOfModule(module); + + g_core->BootLog("_babase exec end"); +} + +auto BaseFeatureSet::Import() -> BaseFeatureSet* { + return ImportThroughPythonModule("_babase"); +} + +void BaseFeatureSet::OnScreenAndAssetsReady() { + assert(InLogicThread()); + assert(console_ == nullptr); + console_ = new Console(); + + // Print any messages that have built up. + if (!console_startup_messages_.empty()) { + console_->Print(console_startup_messages_); + console_startup_messages_.clear(); + } +} + +void BaseFeatureSet::StartApp() { + BA_PRECONDITION(g_core->InMainThread()); + BA_PRECONDITION(g_base); + + // Currently limiting this to once per process. + BA_PRECONDITION(!called_start_app_); + called_start_app_ = true; + assert(!app_running_); // Shouldn't be possible. + + g_core->BootLog("start-app begin"); + // Allow our subsystems to start doing work in their own threads + // and communicating with other subsystems. Note that we may still + // want to run some things serially here and ordering may be important + // (for instance we want to give our main thread a chance to register + // all initial input devices with the logic thread before the logic + // thread applies the current config to them). + + python->OnMainThreadStartApp(); + logic->OnMainThreadStartApp(); + graphics_server->OnMainThreadStartApp(); + bg_dynamics_server->OnMainThreadStartApp(); + network_writer->OnMainThreadStartApp(); + audio_server->OnMainThreadStartApp(); + assets_server->OnMainThreadStartApp(); + g_core->platform->OnMainThreadStartApp(); // FIXME SHOULD NOT NEED THIS + app->OnMainThreadStartApp(); + if (stdio_console) { + stdio_console->OnMainThreadStartApp(); + } + + // Take note that we're now 'running'. Various code such as anything that + // pushes messages to threads can watch for this state to avoid crashing + // if called early. + app_running_ = true; + + // As the last step of this phase, tell the logic thread to apply + // the app config which will kick off screen creation and otherwise + // get the ball rolling. + logic->event_loop()->PushCall([this] { logic->ApplyAppConfig(); }); + g_core->BootLog("start-app end"); +} + +auto BaseFeatureSet::AppManagesEventLoop() -> bool { + return app->ManagesEventLoop(); +} + +void BaseFeatureSet::RunAppToCompletion() { + BA_PRECONDITION(g_core->InMainThread()); + BA_PRECONDITION(g_base); + BA_PRECONDITION(g_base->app->ManagesEventLoop()); + BA_PRECONDITION(!called_run_app_to_completion_); + called_run_app_to_completion_ = true; + + // Start things moving if not done yet. + if (!called_start_app_) { + StartApp(); + } + + // On our event-loop-managing platforms we now simply sit in our event + // loop until the app is quit. + g_core->main_event_loop()->RunEventLoop(false); +} + +void BaseFeatureSet::PrimeAppMainThreadEventPump() { + app->PrimeMainThreadEventPump(); +} + +auto BaseFeatureSet::HavePlus() -> bool { + if (!tried_importing_plus_) { + python->SoftImportPlus(); + // Important to set this *after* import attempt, or a second import attempt + // while first is ongoing can insta-fail. Multiple import attempts shouldn't + // hurt anything. + tried_importing_plus_ = true; + } + return g_plus_soft != nullptr; +} + +/// Access the plus feature-set. Will throw an exception if not present. +auto BaseFeatureSet::Plus() -> PlusSoftInterface* { + if (!tried_importing_plus_) { + python->SoftImportPlus(); + // Important to set this *after* import attempt, or a second import attempt + // while first is ongoing can insta-fail. Multiple import attempts shouldn't + // hurt anything. + tried_importing_plus_ = true; + } + return g_plus_soft; +} + +auto BaseFeatureSet::GetAppInstanceUUID() -> const std::string& { + static std::string app_instance_uuid; + static bool have_app_instance_uuid = false; + + if (!have_app_instance_uuid) { + if (g_base) { + Python::ScopedInterpreterLock gil; + auto uuid = + g_base->python->objs().Get(BasePython::ObjID::kUUIDStrCall).Call(); + if (uuid.Exists()) { + app_instance_uuid = uuid.ValueAsString(); + have_app_instance_uuid = true; + } + } + if (!have_app_instance_uuid) { + // As an emergency fallback simply use a single random number. + // We should probably simply disallow this before Python is up. + Log(LogLevel::kWarning, "GetSessionUUID() using rand fallback."); + srand(static_cast( + core::CorePlatform::GetCurrentMillisecs())); // NOLINT + app_instance_uuid = + std::to_string(static_cast(rand())); // NOLINT + have_app_instance_uuid = true; + } + if (app_instance_uuid.size() >= 100) { + Log(LogLevel::kWarning, "session id longer than it should be."); + } + } + return app_instance_uuid; +} + +void BaseFeatureSet::PlusDirectSendV1CloudLogs(const std::string& prefix, + const std::string& suffix, + bool instant, int* result) { + if (g_plus_soft != nullptr) { + g_plus_soft->DirectSendV1CloudLogs(prefix, suffix, instant, result); + } +} + +auto BaseFeatureSet::CreateFeatureSetData(FeatureSetFrontEnd* featureset) + -> PyObject* { + return PythonClassFeatureSetData::Create(featureset); +} + +auto BaseFeatureSet::FeatureSetFromData(PyObject* obj) -> FeatureSetFrontEnd* { + if (!PythonClassFeatureSetData::Check(obj)) { + FatalError("Module FeatureSetData attr is an incorrect type."); + } + return PythonClassFeatureSetData::FromPyObj(obj).feature_set(); +} + +auto BaseFeatureSet::IsUnmodifiedBlessedBuild() -> bool { + // If we've got plus present, ask them. Otherwise assume no. + if (HavePlus()) { + return Plus()->IsUnmodifiedBlessedBuild(); + } + return false; +} + +auto BaseFeatureSet::InAssetsThread() const -> bool { + if (auto* loop = assets_server->event_loop()) { + return loop->ThreadIsCurrent(); + } + return false; +} + +auto BaseFeatureSet::InLogicThread() const -> bool { + if (auto* loop = logic->event_loop()) { + return loop->ThreadIsCurrent(); + } + return false; +} + +auto BaseFeatureSet::InGraphicsThread() const -> bool { + if (auto* loop = graphics_server->event_loop()) { + return loop->ThreadIsCurrent(); + } + return false; +} + +auto BaseFeatureSet::InAudioThread() const -> bool { + if (auto* loop = audio_server->event_loop()) { + return loop->ThreadIsCurrent(); + } + return false; +} + +auto BaseFeatureSet::InBGDynamicsThread() const -> bool { + if (auto* loop = bg_dynamics_server->event_loop()) { + return loop->ThreadIsCurrent(); + } + return false; +} + +auto BaseFeatureSet::InNetworkWriteThread() const -> bool { + if (auto* loop = network_writer->event_loop()) { + return loop->ThreadIsCurrent(); + } + return false; +} + +void BaseFeatureSet::ScreenMessage(const std::string& s, + const Vector3f& color) { + logic->event_loop()->PushCall( + [this, s, color] { graphics->AddScreenMessage(s, color); }); +} + +void BaseFeatureSet::V1CloudLog(const std::string& msg) { + // If we've got a fully running app environment, let the Python layer + // handle logs. It will group log messages intelligently and ship them + // to the master server with various other context info included. + if (app_running_) { + python->objs().PushCall(BasePython::ObjID::kHandleV1CloudLogCall); + } else { + if (HavePlus()) { + // For log messages before that time we ship them immediately since + // we don't know if the Python layer is (or will be) able to. + if (g_early_v1_cloud_log_writes > 0) { + g_early_v1_cloud_log_writes -= 1; + std::string logprefix = "EARLY-LOG:"; + std::string logsuffix; + + // If we're an early enough error, our global log isn't even available, + // so include this specific message as a suffix instead. + if (g_core == nullptr) { + logsuffix = msg; + } + Plus()->DirectSendV1CloudLogs(logprefix, logsuffix, false, nullptr); + } + } + } +} + +void BaseFeatureSet::PushConsolePrintCall(const std::string& msg) { + // Completely ignore this stuff in headless mode. + if (g_core->HeadlessMode()) { + return; + } + // If our event loop AND console are up and running, ship it off to + // be printed. Otherwise store it for the console to grab when it's ready. + if (auto* event_loop = logic->event_loop()) { + if (console_ != nullptr) { + event_loop->PushCall([this, msg] { console_->Print(msg); }); + return; + } + } + // Didn't send a print; store for later. + console_startup_messages_ += msg; +} + +PyObject* BaseFeatureSet::GetPyExceptionType(PyExcType exctype) { + switch (exctype) { + case PyExcType::kContext: + return python->objs().Get(BasePython::ObjID::kContextError).Get(); + case PyExcType::kNotFound: + return python->objs().Get(BasePython::ObjID::kNotFoundError).Get(); + case PyExcType::kNodeNotFound: + return python->objs().Get(BasePython::ObjID::kNodeNotFoundError).Get(); + case PyExcType::kSessionPlayerNotFound: + return python->objs() + .Get(BasePython::ObjID::kSessionPlayerNotFoundError) + .Get(); + case PyExcType::kInputDeviceNotFound: + return python->objs() + .Get(BasePython::ObjID::kInputDeviceNotFoundError) + .Get(); + case PyExcType::kDelegateNotFound: + return python->objs() + .Get(BasePython::ObjID::kDelegateNotFoundError) + .Get(); + case PyExcType::kWidgetNotFound: + return python->objs().Get(BasePython::ObjID::kWidgetNotFoundError).Get(); + case PyExcType::kActivityNotFound: + return python->objs() + .Get(BasePython::ObjID::kActivityNotFoundError) + .Get(); + case PyExcType::kSessionNotFound: + return python->objs().Get(BasePython::ObjID::kSessionNotFoundError).Get(); + default: + return nullptr; + } +} + +void BaseFeatureSet::SetCurrentContext(const ContextRef& context) { + assert(InLogicThread()); // Up to caller to ensure this. + context_ref->SetTarget(context.Get()); +} + +auto BaseFeatureSet::PrintPythonStackTrace() -> bool { + Python::ScopedInterpreterLock lock; + auto objid{BasePython::ObjID::kPrintTraceCall}; + if (python->objs().Exists(objid)) { + python->objs().Get(objid).Call(); + return true; // available! + } + return false; // not available. +} + +auto BaseFeatureSet::GetPyLString(PyObject* obj) -> std::string { + return python->GetPyLString(obj); +} + +std::string BaseFeatureSet::DoGetContextBaseString() { + if (!InLogicThread()) { + return " context_ref: "; + } + return " context_ref: " + g_base->CurrentContext().GetDescription(); +} +void BaseFeatureSet::DoPrintContextAuto() { + if (!InLogicThread()) { + PrintContextNonLogicThread(); + } else if (const char* label = Python::ScopedCallLabel::current_label()) { + PrintContextForCallableLabel(label); + } else if (PythonCommand* cmd = PythonCommand::current_command()) { + cmd->PrintContext(); + } else if (PythonContextCall* call = PythonContextCall::current_call()) { + call->PrintContext(); + } else { + PrintContextUnavailable(); + } +} +void BaseFeatureSet::PrintContextNonLogicThread() { + std::string s = std::string( + " root call: "); + PySys_WriteStderr("%s\n", s.c_str()); +} + +void BaseFeatureSet::PrintContextForCallableLabel(const char* label) { + assert(InLogicThread()); + assert(label); + std::string s = std::string(" root call: ") + label + "\n"; + s += Python::GetContextBaseString(); + PySys_WriteStderr("%s\n", s.c_str()); +} + +void BaseFeatureSet::PrintContextUnavailable() { + // (no logic-thread-check here; can be called early or from other threads) + std::string s = std::string(" root call: \n"); + s += Python::GetContextBaseString(); + PySys_WriteStderr("%s\n", s.c_str()); +} + +void BaseFeatureSet::DoPushObjCall(const PythonObjectSetBase* objset, int id) { + // Watch for uses before we've created our event loop; + // should fix them at the source. + assert(IsAppRunning()); + + logic->event_loop()->PushCall([objset, id] { + ScopedSetContext ssc(nullptr); + objset->Obj(id).Call(); + }); +} + +void BaseFeatureSet::DoPushObjCall(const PythonObjectSetBase* objset, int id, + const std::string& arg) { + // Watch for uses before we've created our event loop; + // should fix them at the source. + assert(IsAppRunning()); + + logic->event_loop()->PushCall([objset, id, arg] { + ScopedSetContext ssc(nullptr); + PythonRef args(Py_BuildValue("(s)", arg.c_str()), + ballistica::PythonRef::kSteal); + objset->Obj(id).Call(args); + }); +} + +auto BaseFeatureSet::IsAppRunning() const -> bool { return app_running_; } + +} // namespace ballistica::base diff --git a/src/ballistica/base/base.h b/src/ballistica/base/base.h new file mode 100644 index 00000000..b2503ff8 --- /dev/null +++ b/src/ballistica/base/base.h @@ -0,0 +1,726 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_BASE_H_ +#define BALLISTICA_BASE_BASE_H_ + +#include +#include + +#include "ballistica/core/support/base_soft.h" +#include "ballistica/shared/foundation/feature_set_front_end.h" + +// Common header that most everything using our feature-set should include. +// It predeclares our feature-set's various types and globals and other +// bits. + +// Predeclared types from other feature sets that we use. +namespace ballistica::core { +class CoreConfig; +class CoreFeatureSet; +} // namespace ballistica::core +namespace ballistica::plus { +class PlusFeatureSet; +} +namespace ballistica::classic { +class ClassicFeatureSet; +} +namespace ballistica::ui_v1 { +class UIV1FeatureSet; +} + +namespace ballistica::base { + +// Predeclare types we use throughout our FeatureSet so most +// headers can get away with just including this header. +class App; +class AppConfig; +class AppTimer; +class AppMode; +class PlusSoftInterface; +class AreaOfInterest; +class Assets; +class Audio; +class AudioServer; +class AudioStreamer; +class AudioSource; +class BaseFeatureSet; +class BasePlatform; +class BasePython; +class BGDynamics; +class BGDynamicsServer; +class BGDynamicsDrawSnapshot; +class BGDynamicsEmission; +class BGDynamicsFuse; +struct BGDynamicsFuseData; +class BGDynamicsHeightCache; +class BGDynamicsShadow; +struct BGDynamicsShadowData; +class BGDynamicsVolumeLight; +struct BGDynamicsVolumeLightData; +class Camera; +class CollisionMeshAsset; +class CollisionCache; +class Console; +class Context; +class ContextRef; +class DataAsset; +class FrameDef; +class GLContext; +class Graphics; +class GraphicsServer; +class Huffman; +class ImageMesh; +class Input; +class InputDevice; +class InputDeviceDelegate; +class JoystickInput; +class KeyboardInput; +class Logic; +class Asset; +class AssetsServer; +class MeshBufferBase; +class MeshBufferVertexSprite; +class MeshBufferVertexSimpleFull; +class MeshBufferVertexSmokeFull; +class Mesh; +class MeshData; +class MeshDataClientHandle; +class MeshIndexBuffer16; +class MeshIndexedSimpleFull; +class MeshIndexedSmokeFull; +class MeshRendererData; +class MeshAsset; +class MeshAssetRendererData; +class NetClientThread; +class NetGraph; +class Networking; +class NetworkReader; +class NetworkWriter; +class ObjectComponent; +class PythonClassUISound; +class PythonContextCall; +class Renderer; +class RenderComponent; +class RenderCommandBuffer; +class RenderPass; +class RenderTarget; +class RemoteAppServer; +class RemoteControlInput; +class ScoreToBeat; +class SDLApp; +class SDLContext; +class SoundAsset; +class SpriteMesh; +class StdioConsole; +class StressTest; +class Module; +class TestInput; +class TextGroup; +class TextGraphics; +class TextMesh; +class TextPacker; +class TextureAsset; +class TextureAssetPreloadData; +class TextureAssetRendererData; +class TouchInput; +class UI; +class AppVR; +class GraphicsVR; + +enum class AssetType { + kTexture, + kCollisionMesh, + kMesh, + kSound, + kData, + kLast, +}; + +enum class DrawType { + kTriangles, + kPoints, +}; + +/// Hints to the renderer - stuff that is changed rarely should be static, +/// and stuff changed often should be dynamic. +enum class MeshDrawType { + kStatic, + kDynamic, +}; + +enum class ReflectionType { + kNone, + kChar, + kPowerup, + kSoft, + kSharp, + kSharper, + kSharpest, +}; + +enum class GraphicsQuality { + /// Quality has not yet been set. + kUnset, + /// Bare minimum graphics. + kLow, + /// Basic graphics; no post-processing. + kMedium, + /// Graphics with bare minimum post-processing. + kHigh, + /// Graphics with full post-processing. + kHigher, +}; + +/// Requests for exact or auto graphics quality values. +enum class GraphicsQualityRequest { + kUnset, + kLow, + kMedium, + kHigh, + kHigher, + kAuto, +}; + +// Standard vertex structs used in rendering/fileIO/etc. +// Remember to make sure components are on 4 byte boundaries. +// (need to find out how strict we need to be on Metal, Vulkan, etc). + +struct VertexSimpleSplitStatic { + uint16_t uv[2]; +}; + +struct VertexSimpleSplitDynamic { + float position[3]; +}; + +struct VertexSimpleFull { + float position[3]; + uint16_t uv[2]; +}; + +struct VertexDualTextureFull { + float position[3]; + uint16_t uv[2]; + uint16_t uv2[2]; +}; + +struct VertexObjectSplitStatic { + uint16_t uv[2]; +}; + +struct VertexObjectSplitDynamic { + float position[3]; + int16_t normal[3]; + int8_t padding[2]; +}; + +struct VertexObjectFull { + float position[3]; + uint16_t uv[2]; + int16_t normal[3]; + uint8_t padding[2]; +}; + +struct VertexSmokeFull { + float position[3]; + float uv[2]; + uint8_t color[4]; + uint8_t diffuse; + uint8_t padding1[3]; + uint8_t erode; + uint8_t padding2[3]; +}; + +struct VertexSprite { + float position[3]; + uint16_t uv[2]; + float size; + float color[4]; +}; + +enum class MeshFormat { + /// 16bit UV, 8bit normal, 8bit pt-index. + kUV16N8Index8, + /// 16bit UV, 8bit normal, 16bit pt-index. + kUV16N8Index16, + /// 16bit UV, 8bit normal, 32bit pt-index. + kUV16N8Index32, +}; + +enum class TextureType { + k2D, + kCubeMap, +}; + +enum class TextureFormat { + kNone, + kRGBA_8888, + kRGB_888, + kRGBA_4444, + kRGB_565, + kDXT1, + kDXT5, + kETC1, + kPVR2, + kPVR4, + kETC2_RGB, + kETC2_RGBA, +}; + +enum class TextureCompressionType { + kS3TC, + kPVR, + kETC1, + kETC2, +}; + +enum class TextureMinQuality { + kLow, + kMedium, + kHigh, +}; + +enum class CameraMode { + kFollow, + kOrbit, +}; + +enum class MeshDataType { + kIndexedSimpleSplit, + kIndexedObjectSplit, + kIndexedSimpleFull, + kIndexedDualTextureFull, + kIndexedSmokeFull, + kSprite +}; + +struct TouchEvent { + enum class Type { kDown, kUp, kMoved, kCanceled }; + Type type{}; + void* touch{}; + bool overall{}; // For sanity-checks. + float x{}; + float y{}; +}; + +enum class TextMeshEntryType { + kRegular, + kExtras, + kOSRendered, +}; + +enum MeshDrawFlags { + kMeshDrawFlagNoReflection = 1, +}; + +enum class LightShadowType { + kNone, + kTerrain, + kObject, +}; + +enum class TextureQualityRequest { + kUnset, + kAuto, + kHigh, + kMedium, + kLow, +}; +enum class TextureQuality { + kUnset, + kHigh, + kMedium, + kLow, +}; + +enum class BenchmarkType { + kNone, + kCPU, + kGPU, +}; + +#if BA_VR_BUILD +enum class VRHandType { + kNone, + kDaydreamRemote, + kOculusTouchL, + kOculusTouchR, +}; +struct VRHandState { + VRHandType type = VRHandType::kNone; + float tx = 0.0f; + float ty = 0.0f; + float tz = 0.0f; + float yaw = 0.0f; + float pitch = 0.0f; + float roll = 0.0f; +}; +struct VRHandsState { + VRHandState l; + VRHandState r; +}; +#endif // BA_VR_BUILD + +/// Types of shading. +/// These do not necessarily correspond to actual shader objects in the renderer +/// (a single shader may handle more than one of these, etc). +/// These are simply categories of looks. +enum class ShadingType { + kSimpleColor, + kSimpleColorTransparent, + kSimpleColorTransparentDoubleSided, + kSimpleTexture, + kSimpleTextureModulated, + kSimpleTextureModulatedColorized, + kSimpleTextureModulatedColorized2, + kSimpleTextureModulatedColorized2Masked, + kSimpleTextureModulatedTransparent, + kSimpleTextureModulatedTransFlatness, + kSimpleTextureModulatedTransparentDoubleSided, + kSimpleTextureModulatedTransparentColorized, + kSimpleTextureModulatedTransparentColorized2, + kSimpleTextureModulatedTransparentColorized2Masked, + kSimpleTextureModulatedTransparentShadow, + kSimpleTexModulatedTransShadowFlatness, + kSimpleTextureModulatedTransparentGlow, + kSimpleTextureModulatedTransparentGlowMaskUV2, + kObject, + kObjectTransparent, + kObjectLightShadowTransparent, + kSpecial, + kShield, + kObjectReflect, + kObjectReflectTransparent, + kObjectReflectAddTransparent, + kObjectLightShadow, + kObjectReflectLightShadow, + kObjectReflectLightShadowDoubleSided, + kObjectReflectLightShadowColorized, + kObjectReflectLightShadowColorized2, + kObjectReflectLightShadowAdd, + kObjectReflectLightShadowAddColorized, + kObjectReflectLightShadowAddColorized2, + kSmoke, + kSmokeOverlay, + kPostProcess, + kPostProcessEyes, + kPostProcessNormalDistort, + kSprite, + kCount +}; + +enum class SysTextureID { + kUIAtlas, + kButtonSquare, + kWhite, + kFontSmall0, + kFontBig, + kCursor, + kBoxingGlove, + kShield, + kExplosion, + kTextClearButton, + kWindowHSmallVMed, + kWindowHSmallVSmall, + kGlow, + kScrollWidget, + kScrollWidgetGlow, + kFlagPole, + kScorch, + kScorchBig, + kShadow, + kLight, + kShadowSharp, + kLightSharp, + kShadowSoft, + kLightSoft, + kSparks, + kEye, + kEyeTint, + kFuse, + kShrapnel1, + kSmoke, + kCircle, + kCircleOutline, + kCircleNoAlpha, + kCircleOutlineNoAlpha, + kCircleShadow, + kSoftRect, + kSoftRect2, + kSoftRectVertical, + kStartButton, + kBombButton, + kOuyaAButton, + kBackIcon, + kNub, + kArrow, + kMenuButton, + kUsersButton, + kActionButtons, + kTouchArrows, + kTouchArrowsActions, + kRGBStripes, + kUIAtlas2, + kFontSmall1, + kFontSmall2, + kFontSmall3, + kFontSmall4, + kFontSmall5, + kFontSmall6, + kFontSmall7, + kFontExtras, + kFontExtras2, + kFontExtras3, + kFontExtras4, + kCharacterIconMask, + kBlack, + kWings +}; + +enum class SysCubeMapTextureID { + kReflectionChar, + kReflectionPowerup, + kReflectionSoft, + kReflectionSharp, + kReflectionSharper, + kReflectionSharpest +}; + +enum class SysSoundID { + kDeek = 0, + kBlip, + kBlank, + kPunch, + kClick, + kErrorBeep, + kSwish, + kSwish2, + kSwish3, + kTap, + kCorkPop, + kGunCock, + kTickingCrazy, + kSparkle, + kSparkle2, + kSparkle3 +}; + +enum class SystemDataID {}; + +enum class SysMeshID { + kButtonSmallTransparent, + kButtonSmallOpaque, + kButtonMediumTransparent, + kButtonMediumOpaque, + kButtonBackTransparent, + kButtonBackOpaque, + kButtonBackSmallTransparent, + kButtonBackSmallOpaque, + kButtonTabTransparent, + kButtonTabOpaque, + kButtonLargeTransparent, + kButtonLargeOpaque, + kButtonLargerTransparent, + kButtonLargerOpaque, + kButtonSquareTransparent, + kButtonSquareOpaque, + kCheckTransparent, + kScrollBarThumbTransparent, + kScrollBarThumbOpaque, + kScrollBarThumbSimple, + kScrollBarThumbShortTransparent, + kScrollBarThumbShortOpaque, + kScrollBarThumbShortSimple, + kScrollBarTroughTransparent, + kTextBoxTransparent, + kImage1x1, + kImage1x1FullScreen, + kImage2x1, + kImage4x1, + kImage16x1, +#if BA_VR_BUILD + kImage1x1VRFullScreen, + kVROverlay, + kVRFade, +#endif + kOverlayGuide, + kWindowHSmallVMedTransparent, + kWindowHSmallVMedOpaque, + kWindowHSmallVSmallTransparent, + kWindowHSmallVSmallOpaque, + kSoftEdgeOutside, + kSoftEdgeInside, + kBoxingGlove, + kShield, + kFlagPole, + kFlagStand, + kScorch, + kEyeBall, + kEyeBallIris, + kEyeLid, + kHairTuft1, + kHairTuft1b, + kHairTuft2, + kHairTuft3, + kHairTuft4, + kShrapnel1, + kShrapnelSlime, + kShrapnelBoard, + kShockWave, + kFlash, + kCylinder, + kArrowFront, + kArrowBack, + kActionButtonLeft, + kActionButtonTop, + kActionButtonRight, + kActionButtonBottom, + kBox, + kLocator, + kLocatorBox, + kLocatorCircle, + kLocatorCircleOutline, + kCrossOut, + kWing +}; + +// Our feature-set's globals. +// Feature-sets should NEVER directly access globals in another feature-set's +// namespace. All functionality we need from other feature-sets should be +// imported into globals in our own namespace. Generally we do this when we +// are initially imported (just as regular Python modules do). +extern core::CoreFeatureSet* g_core; +extern base::BaseFeatureSet* g_base; +extern PlusSoftInterface* g_plus_soft; +extern classic::ClassicFeatureSet* g_classic; +extern ui_v1::UIV1FeatureSet* g_ui_v1; + +/// Our C++ front-end to our feature set. This is what other C++ +/// feature-sets can 'Import' from us. +class BaseFeatureSet : public FeatureSetFrontEnd, + public core::BaseSoftInterface { + public: + /// Instantiates our FeatureSet if needed and returns the single + /// instance of it. Basically C++ analog to Python import. + static auto Import() -> BaseFeatureSet*; + + /// Called when our associated Python module is instantiated. + static void OnModuleExec(PyObject* module); + + /// Start app systems in motion. + void StartApp() override; + + auto AppManagesEventLoop() -> bool override; + + /// Run app event loop to completion (only applies to flavors which manage + /// their own event loop). + void RunAppToCompletion() override; + + void PrimeAppMainThreadEventPump() override; + + auto CurrentContext() -> const ContextRef& { + assert(InLogicThread()); // Up to caller to ensure this. + return *context_ref; + } + + void SetCurrentContext(const ContextRef& context); + + /// Whether the plus feature-set is available. + auto HavePlus() -> bool; + + /// Access the plus feature-set. Will throw an exception if not present. + auto Plus() -> PlusSoftInterface*; + + /// Return a string that should be universally unique to this particular + /// running instance of the app. + auto GetAppInstanceUUID() -> const std::string&; + + /// Does it appear that we are a blessed build with no known + /// user-modifications? + /// Note that some corner cases (such as being called too early in the launch + /// process) may result in false negatives (saying we're *not* unmodified when + /// in reality we are unmodified). + auto IsUnmodifiedBlessedBuild() -> bool override; + + auto InAssetsThread() const -> bool override; + auto InLogicThread() const -> bool override; + auto InGraphicsThread() const -> bool override; + auto InAudioThread() const -> bool override; + auto InBGDynamicsThread() const -> bool override; + auto InNetworkWriteThread() const -> bool override; + + /// High level screen-message call usable from any thread. + void ScreenMessage(const std::string& s, const Vector3f& color) override; + + /// Have we bootstrapped and started running an app? + /// Code that sends calls/messages to other threads or otherwise uses + /// app functionality may want to check this to avoid crashes. + auto IsAppRunning() const -> bool override; + + void PlusDirectSendV1CloudLogs(const std::string& prefix, + const std::string& suffix, bool instant, + int* result) override; + auto CreateFeatureSetData(FeatureSetFrontEnd* featureset) + -> PyObject* override; + auto FeatureSetFromData(PyObject* obj) -> FeatureSetFrontEnd* override; + void V1CloudLog(const std::string& msg) override; + void PushConsolePrintCall(const std::string& msg) override; + auto GetPyExceptionType(PyExcType exctype) -> PyObject* override; + auto PrintPythonStackTrace() -> bool override; + auto GetPyLString(PyObject* obj) -> std::string override; + auto DoGetContextBaseString() -> std::string override; + void DoPrintContextAuto() override; + void DoPushObjCall(const PythonObjectSetBase* objset, int id) override; + void DoPushObjCall(const PythonObjectSetBase* objset, int id, + const std::string& arg) override; + + /// Called in the logic thread once our screen is up and assets are loading. + void OnScreenAndAssetsReady(); + + // Const subsystems. + App* const app; + AppConfig* const app_config; + Assets* const assets; + AssetsServer* const assets_server; + Audio* const audio; + AudioServer* const audio_server; + BasePlatform* const platform; + BasePython* const python; + BGDynamics* const bg_dynamics; + BGDynamicsServer* const bg_dynamics_server; + ContextRef* const context_ref; + Graphics* const graphics; + GraphicsServer* const graphics_server; + Huffman* const huffman; + Input* const input; + Logic* const logic; + Networking* const networking; + NetworkReader* const network_reader; + NetworkWriter* const network_writer; + StdioConsole* const stdio_console; + TextGraphics* const text_graphics; + UI* const ui; + Utils* const utils; + + // Non-const bits (fixme: clean up access to these). + AppMode* app_mode; + auto* console() { return console_; } + TouchInput* touch_input{}; + + private: + BaseFeatureSet(); + void PrintContextNonLogicThread(); + void PrintContextForCallableLabel(const char* label); + void PrintContextUnavailable(); + + Console* console_{}; + std::string console_startup_messages_; + bool called_start_app_{}; + bool app_running_{}; + bool called_run_app_to_completion_{}; + bool tried_importing_plus_{}; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_BASE_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics.cc b/src/ballistica/base/dynamics/bg/bg_dynamics.cc similarity index 65% rename from src/ballistica/dynamics/bg/bg_dynamics.cc rename to src/ballistica/base/dynamics/bg/bg_dynamics.cc index 6a3565f3..86c6697f 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics.cc +++ b/src/ballistica/base/dynamics/bg/bg_dynamics.cc @@ -1,51 +1,48 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/bg/bg_dynamics.h" +#include "ballistica/base/dynamics/bg/bg_dynamics.h" -#include "ballistica/assets/component/collide_model.h" -#include "ballistica/core/thread.h" -#include "ballistica/dynamics/bg/bg_dynamics_draw_snapshot.h" -#include "ballistica/dynamics/bg/bg_dynamics_fuse_data.h" -#include "ballistica/dynamics/bg/bg_dynamics_shadow_data.h" -#include "ballistica/dynamics/bg/bg_dynamics_volume_light_data.h" -#include "ballistica/graphics/component/object_component.h" -#include "ballistica/graphics/component/smoke_component.h" -#include "ballistica/graphics/component/sprite_component.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_draw_snapshot.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_fuse_data.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_shadow_data.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_volume_light_data.h" +#include "ballistica/base/graphics/component/object_component.h" +#include "ballistica/base/graphics/component/smoke_component.h" +#include "ballistica/base/graphics/component/sprite_component.h" +#include "ballistica/scene_v1/assets/scene_collision_mesh.h" +#include "ballistica/shared/foundation/event_loop.h" -namespace ballistica { +namespace ballistica::base { -BGDynamics::BGDynamics() { - // We're a singleton; make sure we don't already exist. - assert(g_bg_dynamics == nullptr); -} +BGDynamics::BGDynamics() = default; -void BGDynamics::AddTerrain(CollideModelData* o) { - assert(InLogicThread()); +void BGDynamics::AddTerrain(CollisionMeshAsset* o) { + assert(g_base->InLogicThread()); - // Allocate a fresh reference to keep this collide-model alive as long as + // Allocate a fresh reference to keep this collision-mesh alive as long as // we're using it. Once we're done, we'll pass the pointer back to the // main thread to free. - auto* model_ref = new Object::Ref(o); - g_bg_dynamics_server->PushAddTerrainCall(model_ref); + auto* mesh_ref = new Object::Ref(o); + g_base->bg_dynamics_server->PushAddTerrainCall(mesh_ref); } -void BGDynamics::RemoveTerrain(CollideModelData* o) { - assert(InLogicThread()); - g_bg_dynamics_server->PushRemoveTerrainCall(o); +void BGDynamics::RemoveTerrain(CollisionMeshAsset* o) { + assert(g_base->InLogicThread()); + g_base->bg_dynamics_server->PushRemoveTerrainCall(o); } void BGDynamics::Emit(const BGDynamicsEmission& e) { - assert(InLogicThread()); - g_bg_dynamics_server->PushEmitCall(e); + assert(g_base->InLogicThread()); + g_base->bg_dynamics_server->PushEmitCall(e); } // Call friend client to step our sim. -void BGDynamics::Step(const Vector3f& cam_pos) { - assert(InLogicThread()); +void BGDynamics::Step(const Vector3f& cam_pos, int step_millisecs) { + assert(g_base->InLogicThread()); // The BG dynamics thread just processes steps as fast as it can; // we need to throttle what we send or tell it to cut back if its behind - int step_count = g_bg_dynamics_server->step_count(); + int step_count = g_base->bg_dynamics_server->step_count(); // If we're really getting behind, start pruning stuff. if (step_count > 3) { @@ -59,16 +56,17 @@ void BGDynamics::Step(const Vector3f& cam_pos) { // Pass a newly allocated raw pointer to the bg-dynamics thread; it takes care // of disposing it when done. auto d = Object::NewDeferred(); + d->step_millisecs = step_millisecs; d->cam_pos = cam_pos; { // Shadows. BA_DEBUG_TIME_CHECK_BEGIN(bg_dynamic_shadow_list_lock); { - std::scoped_lock lock(g_bg_dynamics_server->shadow_list_mutex_); - auto size = g_bg_dynamics_server->shadows_.size(); + std::scoped_lock lock(g_base->bg_dynamics_server->shadow_list_mutex()); + auto size = g_base->bg_dynamics_server->shadows().size(); d->shadow_step_data_.resize(size); if (size > 0) { - BGDynamicsShadowData** sd_client = &(g_bg_dynamics_server->shadows_[0]); + auto sd_client = &(g_base->bg_dynamics_server->shadows()[0]); std::pair* sd = &(d->shadow_step_data_[0]); for (size_t i = 0; i < size; i++) { @@ -83,12 +81,12 @@ void BGDynamics::Step(const Vector3f& cam_pos) { { // Volume lights. BA_DEBUG_TIME_CHECK_BEGIN(bg_dynamic_volumelights_list_lock); { - std::scoped_lock lock(g_bg_dynamics_server->volume_light_list_mutex_); - auto size = g_bg_dynamics_server->volume_lights_.size(); + std::scoped_lock lock( + g_base->bg_dynamics_server->volume_light_list_mutex()); + auto size = g_base->bg_dynamics_server->volume_lights().size(); d->volume_light_step_data_.resize(size); if (size > 0) { - BGDynamicsVolumeLightData** vd_client = - &(g_bg_dynamics_server->volume_lights_[0]); + auto vd_client = &(g_base->bg_dynamics_server->volume_lights()[0]); std::pair* vd = &(d->volume_light_step_data_[0]); @@ -108,11 +106,11 @@ void BGDynamics::Step(const Vector3f& cam_pos) { { // Fuses. BA_DEBUG_TIME_CHECK_BEGIN(bg_dynamic_fuse_list_lock); { - std::scoped_lock lock(g_bg_dynamics_server->fuse_list_mutex_); - auto size = g_bg_dynamics_server->fuses_.size(); + std::scoped_lock lock(g_base->bg_dynamics_server->fuse_list_mutex()); + auto size = g_base->bg_dynamics_server->fuses().size(); d->fuse_step_data_.resize(size); if (size > 0) { - BGDynamicsFuseData** fd_client = &(g_bg_dynamics_server->fuses_[0]); + auto fd_client = &(g_base->bg_dynamics_server->fuses()[0]); std::pair* fd = &(d->fuse_step_data_[0]); for (size_t i = 0; i < size; i++) { @@ -127,14 +125,8 @@ void BGDynamics::Step(const Vector3f& cam_pos) { BA_DEBUG_TIME_CHECK_END(bg_dynamic_fuse_list_lock, 10); } - // Increase our step count and ship it. - { - std::scoped_lock lock(g_bg_dynamics_server->step_count_mutex_); - g_bg_dynamics_server->step_count_++; - } - // Ok send the thread on its way. - g_bg_dynamics_server->PushStepCall(d); + g_base->bg_dynamics_server->PushStep(d); } void BGDynamics::SetDrawSnapshot(BGDynamicsDrawSnapshot* s) { @@ -144,23 +136,23 @@ void BGDynamics::SetDrawSnapshot(BGDynamicsDrawSnapshot* s) { } void BGDynamics::TooSlow() { - if (!Thread::AreThreadsPaused()) { - g_bg_dynamics_server->PushTooSlowCall(); + if (!EventLoop::AreThreadsPaused()) { + g_base->bg_dynamics_server->PushTooSlowCall(); } } void BGDynamics::SetDebrisFriction(float val) { - assert(InLogicThread()); - g_bg_dynamics_server->PushSetDebrisFrictionCall(val); + assert(g_base->InLogicThread()); + g_base->bg_dynamics_server->PushSetDebrisFrictionCall(val); } void BGDynamics::SetDebrisKillHeight(float val) { - assert(InLogicThread()); - g_bg_dynamics_server->PushSetDebrisKillHeightCall(val); + assert(g_base->InLogicThread()); + g_base->bg_dynamics_server->PushSetDebrisKillHeightCall(val); } void BGDynamics::Draw(FrameDef* frame_def) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); BGDynamicsDrawSnapshot* ds{draw_snapshot_.get()}; if (!ds) { @@ -168,8 +160,8 @@ void BGDynamics::Draw(FrameDef* frame_def) { } // Draw sparks. - if (ds->spark_vertices.exists()) { - if (!sparks_mesh_.exists()) sparks_mesh_ = Object::New(); + if (ds->spark_vertices.Exists()) { + if (!sparks_mesh_.Exists()) sparks_mesh_ = Object::New(); sparks_mesh_->SetIndexData(ds->spark_indices); sparks_mesh_->SetData( Object::Ref>(ds->spark_vertices)); @@ -182,36 +174,36 @@ void BGDynamics::Draw(FrameDef* frame_def) { c.SetCameraAligned(true); c.SetColor(2.0f, 2.0f, 2.0f, 1.0f); c.SetOverlay(draw_in_overlay); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kSparks)); - c.DrawMesh(sparks_mesh_.get(), kModelDrawFlagNoReflection); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kSparks)); + c.DrawMesh(sparks_mesh_.Get(), kMeshDrawFlagNoReflection); c.Submit(); } // Draw lights. - if (ds->light_vertices.exists()) { - assert(ds->light_indices.exists()); + if (ds->light_vertices.Exists()) { + assert(ds->light_indices.Exists()); assert(!ds->light_indices->elements.empty()); assert(!ds->light_vertices->elements.empty()); - if (!lights_mesh_.exists()) lights_mesh_ = Object::New(); + if (!lights_mesh_.Exists()) lights_mesh_ = Object::New(); lights_mesh_->SetIndexData(ds->light_indices); lights_mesh_->SetData( Object::Ref>(ds->light_vertices)); SpriteComponent c(frame_def->light_shadow_pass()); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kLightSoft)); - c.DrawMesh(lights_mesh_.get()); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kLightSoft)); + c.DrawMesh(lights_mesh_.Get()); c.Submit(); } // Draw shadows. - if (ds->shadow_vertices.exists()) { - assert(ds->shadow_indices.exists()); - if (!shadows_mesh_.exists()) shadows_mesh_ = Object::New(); + if (ds->shadow_vertices.Exists()) { + assert(ds->shadow_indices.Exists()); + if (!shadows_mesh_.Exists()) shadows_mesh_ = Object::New(); shadows_mesh_->SetIndexData(ds->shadow_indices); shadows_mesh_->SetData( Object::Ref>(ds->shadow_vertices)); SpriteComponent c(frame_def->light_shadow_pass()); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kLight)); - c.DrawMesh(shadows_mesh_.get()); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kLight)); + c.DrawMesh(shadows_mesh_.Get()); c.Submit(); } @@ -226,8 +218,8 @@ void BGDynamics::Draw(FrameDef* frame_def) { DrawChunks(frame_def, &ds->flag_stands, BGDynamicsChunkType::kFlagStand); // Draw tendrils. - if (ds->tendril_vertices.exists()) { - if (!tendrils_mesh_.exists()) + if (ds->tendril_vertices.Exists()) { + if (!tendrils_mesh_.Exists()) tendrils_mesh_ = Object::New(); tendrils_mesh_->SetIndexData(ds->tendril_indices); tendrils_mesh_->SetData( @@ -237,7 +229,7 @@ void BGDynamics::Draw(FrameDef* frame_def) { : frame_def->beauty_pass()); c.SetOverlay(draw_in_overlay); c.SetColor(1.0f, 1.0f, 1.0f, 1.0f); - c.DrawMesh(tendrils_mesh_.get(), kModelDrawFlagNoReflection); + c.DrawMesh(tendrils_mesh_.Get(), kMeshDrawFlagNoReflection); c.Submit(); // Shadows. @@ -245,25 +237,25 @@ void BGDynamics::Draw(FrameDef* frame_def) { for (auto&& i : ds->tendril_shadows) { if (i.density > 0.0001f) { Vector3f& p(i.p); - g_graphics->DrawBlotch(p, 2.0f * i.density, 0.02f * i.density, - 0.01f * i.density, 0, 0.15f * i.density); + g_base->graphics->DrawBlotch(p, 2.0f * i.density, 0.02f * i.density, + 0.01f * i.density, 0, 0.15f * i.density); } } } } // Draw fuses. - if (ds->fuse_vertices.exists()) { + if (ds->fuse_vertices.Exists()) { // Update our mesh with this data. - if (!fuses_mesh_.exists()) + if (!fuses_mesh_.Exists()) fuses_mesh_ = Object::New(); fuses_mesh_->SetIndexData(ds->fuse_indices); fuses_mesh_->SetData( Object::Ref>(ds->fuse_vertices)); { // Draw! ObjectComponent c(frame_def->beauty_pass()); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kFuse)); - c.DrawMesh(fuses_mesh_.get(), kModelDrawFlagNoReflection); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kFuse)); + c.DrawMesh(fuses_mesh_.Get(), kMeshDrawFlagNoReflection); c.Submit(); } } @@ -277,19 +269,19 @@ void BGDynamics::DrawChunks(FrameDef* frame_def, } // Draw ourselves into the beauty pass. - ModelData* model; + MeshAsset* mesh; switch (chunk_type) { case BGDynamicsChunkType::kFlagStand: - model = g_assets->GetModel(SystemModelID::kFlagStand); + mesh = g_base->assets->SysMesh(SysMeshID::kFlagStand); break; case BGDynamicsChunkType::kSplinter: - model = g_assets->GetModel(SystemModelID::kShrapnelBoard); + mesh = g_base->assets->SysMesh(SysMeshID::kShrapnelBoard); break; case BGDynamicsChunkType::kSlime: - model = g_assets->GetModel(SystemModelID::kShrapnelSlime); + mesh = g_base->assets->SysMesh(SysMeshID::kShrapnelSlime); break; default: - model = g_assets->GetModel(SystemModelID::kShrapnel1); + mesh = g_base->assets->SysMesh(SysMeshID::kShrapnel1); break; } ObjectComponent c(frame_def->beauty_pass()); @@ -297,20 +289,20 @@ void BGDynamics::DrawChunks(FrameDef* frame_def, // Set up shading. switch (chunk_type) { case BGDynamicsChunkType::kRock: { - c.SetTexture(g_assets->GetTexture(SystemTextureID::kShrapnel1)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kShrapnel1)); c.SetReflection(ReflectionType::kSoft); c.SetReflectionScale(0.2f, 0.2f, 0.2f); c.SetColor(0.6f, 0.6f, 0.5f); break; } case BGDynamicsChunkType::kIce: { - c.SetTexture(g_assets->GetTexture(SystemTextureID::kShrapnel1)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kShrapnel1)); c.SetReflection(ReflectionType::kSharp); c.SetAddColor(0.5f, 0.5f, 0.9f); break; } case BGDynamicsChunkType::kSlime: { - c.SetTexture(g_assets->GetTexture(SystemTextureID::kShrapnel1)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kShrapnel1)); c.SetReflection(ReflectionType::kSharper); c.SetReflectionScale(3.0f, 3.0f, 3.0f); c.SetColor(0.0f, 0.0f, 0.0f); @@ -318,13 +310,13 @@ void BGDynamics::DrawChunks(FrameDef* frame_def, break; } case BGDynamicsChunkType::kMetal: { - c.SetTexture(g_assets->GetTexture(SystemTextureID::kShrapnel1)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kShrapnel1)); c.SetReflection(ReflectionType::kPowerup); c.SetColor(0.5f, 0.5f, 0.55f); break; } case BGDynamicsChunkType::kSpark: { - c.SetTexture(g_assets->GetTexture(SystemTextureID::kShrapnel1)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kShrapnel1)); c.SetReflection(ReflectionType::kSharp); c.SetColor(0.0f, 0.0f, 0.0f, 1.0f); c.SetReflectionScale(4.0f, 3.0f, 2.0f); @@ -332,7 +324,7 @@ void BGDynamics::DrawChunks(FrameDef* frame_def, break; } case BGDynamicsChunkType::kSplinter: { - c.SetTexture(g_assets->GetTexture(SystemTextureID::kShrapnel1)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kShrapnel1)); c.SetReflection(ReflectionType::kSoft); c.SetColor(1.0f, 0.8f, 0.5f); break; @@ -341,7 +333,7 @@ void BGDynamics::DrawChunks(FrameDef* frame_def, c.SetTransparent(true); c.SetPremultiplied(true); c.SetLightShadow(LightShadowType::kNone); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kShrapnel1)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kShrapnel1)); c.SetReflection(ReflectionType::kSharp); c.SetReflectionScale(0.5f, 0.4f, 0.3f); c.SetColor(0.2f, 0.15f, 0.15f, 0.07f); @@ -349,14 +341,14 @@ void BGDynamics::DrawChunks(FrameDef* frame_def, break; } case BGDynamicsChunkType::kFlagStand: { - c.SetTexture(g_assets->GetTexture(SystemTextureID::kFlagPole)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kFlagPole)); c.SetReflection(ReflectionType::kSharp); c.SetColor(0.9f, 0.6f, 0.3f, 1.0f); break; } } - c.DrawModelInstanced(model, *draw_snapshot, kModelDrawFlagNoReflection); + c.DrawMeshAssetInstanced(mesh, *draw_snapshot, kMeshDrawFlagNoReflection); c.Submit(); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/dynamics/bg/bg_dynamics.h b/src/ballistica/base/dynamics/bg/bg_dynamics.h similarity index 78% rename from src/ballistica/dynamics/bg/bg_dynamics.h rename to src/ballistica/base/dynamics/bg/bg_dynamics.h index 264b0b26..20c94555 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics.h +++ b/src/ballistica/base/dynamics/bg/bg_dynamics.h @@ -1,15 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_H_ -#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_H_ +#ifndef BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_H_ +#define BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_H_ #include #include -#include "ballistica/core/object.h" -#include "ballistica/math/vector3f.h" +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/math/vector3f.h" -namespace ballistica { +namespace ballistica::base { enum class BGDynamicsEmitType { kChunks, @@ -51,7 +52,7 @@ class BGDynamics { BGDynamics(); void Emit(const BGDynamicsEmission& def); - void Step(const Vector3f& cam_pos); + void Step(const Vector3f& cam_pos, int step_millisecs); // can be called to inform the bg dynamics thread to kill off some // smoke/chunks/etc. if rendering is chugging or whatnot. @@ -61,8 +62,8 @@ class BGDynamics { void Draw(FrameDef* frame_def); void SetDebrisFriction(float val); void SetDebrisKillHeight(float val); - void AddTerrain(CollideModelData* o); - void RemoveTerrain(CollideModelData* o); + void AddTerrain(CollisionMeshAsset* o); + void RemoveTerrain(CollisionMeshAsset* o); // (sent to us by the bg dynamics server) void SetDrawSnapshot(BGDynamicsDrawSnapshot* s); @@ -78,6 +79,6 @@ class BGDynamics { std::unique_ptr draw_snapshot_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_H_ +#endif // BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_draw_snapshot.h b/src/ballistica/base/dynamics/bg/bg_dynamics_draw_snapshot.h similarity index 61% rename from src/ballistica/dynamics/bg/bg_dynamics_draw_snapshot.h rename to src/ballistica/base/dynamics/bg/bg_dynamics_draw_snapshot.h index 777843ae..4e2db886 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics_draw_snapshot.h +++ b/src/ballistica/base/dynamics/bg/bg_dynamics_draw_snapshot.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_DRAW_SNAPSHOT_H_ -#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_DRAW_SNAPSHOT_H_ +#ifndef BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_DRAW_SNAPSHOT_H_ +#define BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_DRAW_SNAPSHOT_H_ #include -#include "ballistica/graphics/renderer.h" +#include "ballistica/base/graphics/renderer/renderer.h" -namespace ballistica { +namespace ballistica::base { // Big chunk of data sent back from the bg-dynamics server thread // to the logic thread for drawing. @@ -21,20 +21,20 @@ class BGDynamicsDrawSnapshot { }; // These are created in the bg-dynamics thread, and object ownership - // needs to be switched back to the game-thread default when it is passed + // needs to be switched back to the logic-thread default when it is passed // over or else the debug thread-access-checks will error. void SetLogicThreadOwnership() { if (g_buildconfig.debug_build()) { - for (Object* o : {static_cast(tendril_indices.get()), - static_cast(tendril_vertices.get()), - static_cast(fuse_indices.get()), - static_cast(fuse_vertices.get()), - static_cast(shadow_indices.get()), - static_cast(shadow_vertices.get()), - static_cast(light_indices.get()), - static_cast(light_vertices.get()), - static_cast(spark_indices.get()), - static_cast(spark_vertices.get())}) { + for (Object* o : {static_cast(tendril_indices.Get()), + static_cast(tendril_vertices.Get()), + static_cast(fuse_indices.Get()), + static_cast(fuse_vertices.Get()), + static_cast(shadow_indices.Get()), + static_cast(shadow_vertices.Get()), + static_cast(light_indices.Get()), + static_cast(light_vertices.Get()), + static_cast(spark_indices.Get()), + static_cast(spark_vertices.Get())}) { if (o) { o->SetThreadOwnership(Object::ThreadOwnership::kClassDefault); } @@ -74,6 +74,6 @@ class BGDynamicsDrawSnapshot { Object::Ref spark_vertices; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_DRAW_SNAPSHOT_H_ +#endif // BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_DRAW_SNAPSHOT_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_fuse.cc b/src/ballistica/base/dynamics/bg/bg_dynamics_fuse.cc similarity index 57% rename from src/ballistica/dynamics/bg/bg_dynamics_fuse.cc rename to src/ballistica/base/dynamics/bg/bg_dynamics_fuse.cc index 6669754f..543b5f2e 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics_fuse.cc +++ b/src/ballistica/base/dynamics/bg/bg_dynamics_fuse.cc @@ -1,41 +1,41 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/bg/bg_dynamics_fuse.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_fuse.h" -#include "ballistica/dynamics/bg/bg_dynamics_fuse_data.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_fuse_data.h" -namespace ballistica { +namespace ballistica::base { BGDynamicsFuse::BGDynamicsFuse() { - assert(g_bg_dynamics_server); - assert(InLogicThread()); + assert(g_base->bg_dynamics_server); + assert(g_base->InLogicThread()); // Allocate our data. We'll pass this to the BGDynamics thread, and // it'll then own it. data_ = new BGDynamicsFuseData(); - g_bg_dynamics_server->PushAddFuseCall(data_); + g_base->bg_dynamics_server->PushAddFuseCall(data_); } BGDynamicsFuse::~BGDynamicsFuse() { - assert(g_bg_dynamics_server); - assert(InLogicThread()); + assert(g_base->bg_dynamics_server); + assert(g_base->InLogicThread()); // Let the data know the client side is dead // so that we're no longer included in step messages. // (since by the time the worker gets it the data will be gone). data_->client_dead_ = true; - g_bg_dynamics_server->PushRemoveFuseCall(data_); + g_base->bg_dynamics_server->PushRemoveFuseCall(data_); } void BGDynamicsFuse::SetTransform(const Matrix44f& t) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); data_->transform_client_ = t; data_->have_transform_client_ = true; } void BGDynamicsFuse::SetLength(float length) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); data_->length_client_ = length; } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/dynamics/bg/bg_dynamics_fuse.h b/src/ballistica/base/dynamics/bg/bg_dynamics_fuse.h similarity index 50% rename from src/ballistica/dynamics/bg/bg_dynamics_fuse.h rename to src/ballistica/base/dynamics/bg/bg_dynamics_fuse.h index b0e8c9e6..f56c1883 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics_fuse.h +++ b/src/ballistica/base/dynamics/bg/bg_dynamics_fuse.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_FUSE_H_ -#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_FUSE_H_ +#ifndef BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_FUSE_H_ +#define BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_FUSE_H_ -#include "ballistica/ballistica.h" +#include "ballistica/base/base.h" -namespace ballistica { +namespace ballistica::base { // Client controlled fuse. class BGDynamicsFuse { @@ -19,6 +19,6 @@ class BGDynamicsFuse { BGDynamicsFuseData* data_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_FUSE_H_ +#endif // BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_FUSE_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_fuse_data.h b/src/ballistica/base/dynamics/bg/bg_dynamics_fuse_data.h similarity index 90% rename from src/ballistica/dynamics/bg/bg_dynamics_fuse_data.h rename to src/ballistica/base/dynamics/bg/bg_dynamics_fuse_data.h index dfc75ca0..eb100921 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics_fuse_data.h +++ b/src/ballistica/base/dynamics/bg/bg_dynamics_fuse_data.h @@ -1,13 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_FUSE_DATA_H_ -#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_FUSE_DATA_H_ +#ifndef BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_FUSE_DATA_H_ +#define BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_FUSE_DATA_H_ #include -#include "ballistica/dynamics/bg/bg_dynamics_server.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_server.h" +#include "ballistica/shared/math/random.h" -namespace ballistica { +namespace ballistica::base { const int kFusePointCount = 4; @@ -108,6 +109,6 @@ struct BGDynamicsFuseData { bool initial_position_set_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_FUSE_DATA_H_ +#endif // BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_FUSE_DATA_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_height_cache.cc b/src/ballistica/base/dynamics/bg/bg_dynamics_height_cache.cc similarity index 97% rename from src/ballistica/dynamics/bg/bg_dynamics_height_cache.cc rename to src/ballistica/base/dynamics/bg/bg_dynamics_height_cache.cc index 397b2a19..7fd00919 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics_height_cache.cc +++ b/src/ballistica/base/dynamics/bg/bg_dynamics_height_cache.cc @@ -1,8 +1,8 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/bg/bg_dynamics_height_cache.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_height_cache.h" -namespace ballistica { +namespace ballistica::base { const int kBGDynamicsHeightCacheMaxContacts = 20; @@ -167,4 +167,4 @@ void BGDynamicsHeightCache::Update() { dirty_ = false; } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/dynamics/bg/bg_dynamics_height_cache.h b/src/ballistica/base/dynamics/bg/bg_dynamics_height_cache.h similarity index 71% rename from src/ballistica/dynamics/bg/bg_dynamics_height_cache.h rename to src/ballistica/base/dynamics/bg/bg_dynamics_height_cache.h index 71502ff5..be86ee8e 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics_height_cache.h +++ b/src/ballistica/base/dynamics/bg/bg_dynamics_height_cache.h @@ -1,14 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_HEIGHT_CACHE_H_ -#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_HEIGHT_CACHE_H_ +#ifndef BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_HEIGHT_CACHE_H_ +#define BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_HEIGHT_CACHE_H_ #include -#include "ballistica/math/vector3f.h" +#include "ballistica/shared/math/vector3f.h" #include "ode/ode.h" -namespace ballistica { +namespace ballistica::base { // given geoms, creates/samples a height map on the fly // for fast but not-perfectly-accurate height values @@ -37,6 +37,6 @@ class BGDynamicsHeightCache { float z_max_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_HEIGHT_CACHE_H_ +#endif // BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_HEIGHT_CACHE_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_server.cc b/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc similarity index 91% rename from src/ballistica/dynamics/bg/bg_dynamics_server.cc rename to src/ballistica/base/dynamics/bg/bg_dynamics_server.cc index cbcd8821..dccdc139 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics_server.cc +++ b/src/ballistica/base/dynamics/bg/bg_dynamics_server.cc @@ -1,20 +1,20 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/bg/bg_dynamics_server.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_server.h" -#include "ballistica/assets/component/collide_model.h" -#include "ballistica/core/thread.h" -#include "ballistica/dynamics/bg/bg_dynamics_draw_snapshot.h" -#include "ballistica/dynamics/bg/bg_dynamics_fuse_data.h" -#include "ballistica/dynamics/bg/bg_dynamics_height_cache.h" -#include "ballistica/dynamics/bg/bg_dynamics_shadow_data.h" -#include "ballistica/dynamics/bg/bg_dynamics_volume_light_data.h" -#include "ballistica/dynamics/collision_cache.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/logic/logic.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_draw_snapshot.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_fuse_data.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_height_cache.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_shadow_data.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_volume_light_data.h" +#include "ballistica/base/dynamics/collision_cache.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/scene_v1/assets/scene_collision_mesh.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::base { // Some triangle-on-box cases generate tons of contacts; lets try limiting it // this way... If that doesn't work we'll crank this up and add collision @@ -55,74 +55,80 @@ inline auto Reflect(const Vector3f& v, const Vector3f& normal) -> Vector3f { return -(n_projected - (v - n_projected)); } -static void CalcERPCFM(dReal stiffness, dReal damping, dReal* erp, dReal* cfm) { +void BGDynamicsServer::CalcERPCFM(dReal stiffness, dReal damping, dReal* erp, + dReal* cfm) { if (stiffness <= 0.0f && damping <= 0.0f) { *erp = 0.0f; // cfm = dInfinity; // doesn't seem to be happy... *cfm = 9999999999.0f; } else { - *erp = (kGameStepSeconds * stiffness) - / ((kGameStepSeconds * stiffness) + damping); - *cfm = 1.0f / ((kGameStepSeconds * stiffness) + damping); + *erp = + (step_seconds_ * stiffness) / ((step_seconds_ * stiffness) + damping); + *cfm = 1.0f / ((step_seconds_ * stiffness) + damping); } } class BGDynamicsServer::Terrain { public: - Terrain(BGDynamicsServer* t, Object::Ref* collide_model_in) - : collide_model_(collide_model_in) { - assert((**collide_model_).loaded()); - geom_ = dCreateTriMesh(nullptr, (**collide_model_).GetBGMeshData(), nullptr, - nullptr, nullptr); + Terrain(BGDynamicsServer* t, + Object::Ref* collision_mesh_in) + : collision_mesh_(collision_mesh_in) { + assert((**collision_mesh_).loaded()); + geom_ = dCreateTriMesh(nullptr, (**collision_mesh_).GetBGMeshData(), + nullptr, nullptr, nullptr); } - auto GetCollideModel() const -> CollideModelData* { - return collide_model_->get(); + auto GetCollisionMesh() const -> CollisionMeshAsset* { + return collision_mesh_->Get(); } ~Terrain() { dGeomDestroy(geom_); - // We were passed an allocated pointer to a CollideModelData strong-ref + // We were passed an allocated pointer to a CollisionMeshData strong-ref // object to keep it alive while we're using it. We need to pass that // back to the main thread to get freed. - if (collide_model_) { - Object::Ref* ref = collide_model_; - g_logic->thread()->PushCall([ref] { - (**ref).set_last_used_time(GetRealTime()); + if (collision_mesh_) { + Object::Ref* ref = collision_mesh_; + g_base->logic->event_loop()->PushCall([ref] { + (**ref).set_last_used_time(g_core->GetAppTimeMillisecs()); delete ref; }); - collide_model_ = nullptr; + collision_mesh_ = nullptr; } } auto geom() const -> dGeomID { return geom_; } private: - Object::Ref* collide_model_; + Object::Ref* collision_mesh_; dGeomID geom_; }; class BGDynamicsServer::Field { public: Field(BGDynamicsServer* t, const Vector3f& pos, float mag) - : pos_(pos), rad_(5), mag_(mag), birth_time_(t->time()), lifespan_(500) {} + : pos_(pos), + rad_(5), + mag_(mag), + birth_time_ms_(t->time_ms()), + lifespan_ms_(500) {} ~Field() = default; auto rad() const -> dReal { return rad_; } auto pos() const -> Vector3f { return pos_; } auto amt() const -> dReal { return amt_; } void set_amt(dReal val) { amt_ = val; } - auto birth_time() const -> uint32_t { return birth_time_; } - auto lifespan() const -> dReal { return lifespan_; } + auto birth_time_ms() const { return birth_time_ms_; } + auto lifespan_ms() const -> dReal { return lifespan_ms_; } auto mag() const -> dReal { return mag_; } private: Vector3f pos_; - dReal rad_; - dReal mag_; - uint32_t birth_time_; - dReal lifespan_; - dReal amt_{}; + float rad_; + float mag_; + float birth_time_ms_; + float lifespan_ms_; + float amt_{}; }; class BGDynamicsServer::Tendril { @@ -138,28 +144,28 @@ class BGDynamicsServer::Tendril { float brightness{}; float fade{}; float fade_rate{}; - float age{}; + float age_ms{}; float glow_r{}; float glow_g{}; float glow_b{}; - void Update(const Tendril& t) { - p += v * kGameStepSeconds; - age += kGameStepMilliseconds; + void Update(BGDynamicsServer* dynamics, const Tendril& t) { + p += v * dynamics->step_seconds(); + age_ms += dynamics->step_milliseconds(); v *= 0.992f; v.y -= 0.003f * bouyancy; // Bouyancy. v.x += 0.005f * t.wind_amt_; // Slight side drift. erode *= (1.0f - 0.06f * erode_rate); - if (age > 750 * fade_rate) fade *= 1.0f - 0.0085f * fade_rate; + if (age_ms > 750 * fade_rate) fade *= 1.0f - 0.0085f * fade_rate; } void UpdateDistortion(const BGDynamicsServer& d) { p_distorted = p; for (auto&& fi : d.fields_) { const Field& f(*fi); - float fRad = f.rad(); - float fRadSquared = fRad * fRad; + float f_rad = f.rad(); + float f_rad_squared = f_rad * f_rad; Vector3f diff = p_distorted - f.pos(); float dist_squared = diff.LengthSquared(); - if (dist_squared <= fRadSquared) { + if (dist_squared <= f_rad_squared) { float dist = sqrtf(dist_squared); // Shift our point towards or away from the field by its calced mag. @@ -169,7 +175,7 @@ class BGDynamicsServer::Tendril { // ratio of dist to mag. if (dist < -mag) mag *= (dist / -mag); float falloff = - (1.0f - (dist / fRad)); // falloff with dist from field + (1.0f - (dist / f_rad)); // falloff with dist from field mag *= falloff; Vector3f diff_norm = diff.Normalized(); p_distorted += diff_norm * mag; @@ -177,7 +183,7 @@ class BGDynamicsServer::Tendril { // Also apply a very slight amount of actual outward force to // ourselves (only if we're kinda old though - otherwise it screws // with our initial shape too much). - if (age > 400) { + if (age_ms > 400) { v += Vector3f(diff_norm.x * 0.03f, diff_norm.y * 0.01f, diff_norm.z * 0.03f) * falloff; @@ -226,7 +232,7 @@ class BGDynamicsServer::Tendril { controller_{nullptr}, emitting_{true}, emit_rate_{0.8f + 0.4f * RandomFloat()}, - birth_time_{t->time()}, + birth_time_{t->time_ms()}, radius_{0.1f + RandomFloat() * 0.1f}, tex_coord_{RandomFloat()}, start_erode_{0.1f}, @@ -247,8 +253,8 @@ class BGDynamicsServer::Tendril { } void UpdateSlices(BGDynamicsServer* t) { for (auto&& i : slices_) { - i.p1.Update(*this); - i.p2.Update(*this); + i.p1.Update(t, *this); + i.p2.Update(t, *this); // Push them together slightly if they're getting too far apart. Vector3f diff = i.p1.p - i.p2.p; @@ -343,7 +349,7 @@ class BGDynamicsServer::Tendril { Vector3f prev_pos_{0.0f, 0.0f, 0.0f}; Vector3f velocity_{0.0f, 0.0f, 0.0f}; Vector3f medium_velocity_{0.0f, 0.0f, 0.0f}; - uint32_t birth_time_{}; + float birth_time_{}; float tex_coord_{}; float radius_{}; BGDynamicsTendrilType type_{}; @@ -384,7 +390,7 @@ class BGDynamicsServer::Chunk { dynamic_(dynamic), can_die_(can_die), tendril_controller_(nullptr), - birth_time_{t->time()}, + birth_time_{t->time_ms()}, flicker_{1.0f}, flicker_scale_{1.0f} { flicker_scale_ = RandomFloat(); // NOLINT @@ -481,12 +487,12 @@ class BGDynamicsServer::Chunk { TendrilController* tendril_controller_; bool dynamic_; bool can_die_; - dReal lifespan_; - dReal flicker_; - dReal flicker_scale_; + float lifespan_; + float flicker_; + float flicker_scale_; float static_transform_[16]{}; BGDynamicsChunkType type_{}; - uint32_t birth_time_{}; + float birth_time_{}; dBodyID body_{}; dGeomID geom_{}; float size_[3]{}; @@ -524,6 +530,8 @@ void BGDynamicsServer::ParticleSet::Emit(const Vector3f& pos, void BGDynamicsServer::ParticleSet::UpdateAndCreateSnapshot( Object::Ref* index_buffer, Object::Ref* buffer) { + assert(g_base->InBGDynamicsThread()); + auto p_count = static_cast(particles[current_set].size()); // Quick-out: return empty. @@ -538,15 +546,16 @@ void BGDynamicsServer::ParticleSet::UpdateAndCreateSnapshot( Particle* p_dst = &particles[!current_set][0]; auto* ibuf = Object::NewDeferred(p_count * 6); - - // Logic thread is default owner; needs to be us until we hand it over. + // Logic thread is default owner for this type. It needs to be us until + // we hand it over, so set that up before creating the first ref. ibuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); - *index_buffer = Object::MakeRefCounted(ibuf); - auto* vbuf = Object::NewDeferred(p_count * 4); + *index_buffer = Object::CompleteDeferred(ibuf); - // Logic thread is default owner; needs to be us until we hand it over. + auto* vbuf = Object::NewDeferred(p_count * 4); + // Logic thread is default owner for this type. It needs to be us until + // we hand it over, so set that up before creating the first ref. vbuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); - *buffer = Object::MakeRefCounted(vbuf); + *buffer = Object::CompleteDeferred(vbuf); uint16_t* i_render = &(*index_buffer)->elements[0]; VertexSprite* p_render = &(*buffer)->elements[0]; @@ -663,13 +672,6 @@ void BGDynamicsServer::ParticleSet::UpdateAndCreateSnapshot( BGDynamicsServer::BGDynamicsServer() : height_cache_(new BGDynamicsHeightCache()), collision_cache_(new CollisionCache) { - // We're a singleton; make sure we don't already exist. - BA_PRECONDITION(g_bg_dynamics_server == nullptr); - - // Spin up our thread. - thread_ = new Thread(ThreadTag::kBGDynamics); - g_app->pausable_threads.push_back(thread_); - // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer) ode_world_ = dWorldCreate(); assert(ode_world_); @@ -686,6 +688,12 @@ BGDynamicsServer::BGDynamicsServer() assert(ode_contact_group_); } +void BGDynamicsServer::OnMainThreadStartApp() { + // Spin up our thread. + event_loop_ = new EventLoop(EventLoopID::kBGDynamics); + g_core->pausable_event_loops.push_back(event_loop_); +} + BGDynamicsServer::Tendril::~Tendril() { // If we have a controller, tell them not to call us anymore. if (controller_) { @@ -731,7 +739,7 @@ void BGDynamicsServer::UpdateTendrils() { if (t.controller_ == nullptr) { t.prev_pos_ = t.position_; t.velocity_ += Vector3f(0, -0.1f, 0); // Gravity. - t.position_ += t.velocity_ * kGameStepSeconds; + t.position_ += t.velocity_ * step_seconds_; } // If we're still emitting, potentially drop in some new slices. @@ -804,7 +812,7 @@ void BGDynamicsServer::UpdateTendrils() { slice.p1.erode = t.start_erode_; slice.p1.erode_rate = std::max( 0.0f, density + erode_rate_randomness * (RandomFloat() - 0.5f)); - slice.p1.age = 0.0f; + slice.p1.age_ms = 0; slice.p1.bouyancy = 0.3f + 0.2f * RandomFloat(); slice.p1.brightness = std::max( 0.0f, start_brightness @@ -823,7 +831,7 @@ void BGDynamicsServer::UpdateTendrils() { slice.p2.erode = t.start_erode_; slice.p2.erode_rate = std::max( 0.0f, density + erode_rate_randomness * (RandomFloat() - 0.5f)); - slice.p2.age = 0.0f; + slice.p2.age_ms = 0; slice.p2.bouyancy = 0.3f + 0.2f * RandomFloat(); slice.p2.brightness = std::max( 0.0f, start_brightness_2 @@ -868,7 +876,7 @@ void BGDynamicsServer::UpdateTendrils() { slice.p1.erode = start_erode; slice.p1.erode_rate = std::max( 0.0f, density + erode_rate_randomness * (RandomFloat() - 0.5f)); - slice.p1.age = 0.0f; + slice.p1.age_ms = 0; slice.p1.bouyancy = 0.3f + 0.2f * RandomFloat(); slice.p1.brightness = std::max( 0.0f, start_brightness @@ -887,7 +895,7 @@ void BGDynamicsServer::UpdateTendrils() { slice.p2.erode = start_erode; slice.p2.erode_rate = std::max( 0.0f, density + erode_rate_randomness * (RandomFloat() - 0.5f)); - slice.p2.age = 0.0f; + slice.p2.age_ms = 0; slice.p2.bouyancy = 0.3f + 0.2f * RandomFloat(); slice.p2.brightness = std::max( 0.0f, start_brightness_2 @@ -916,7 +924,7 @@ void BGDynamicsServer::UpdateTendrils() { t.cur_slice_.p1.erode = t.start_erode_; t.cur_slice_.p1.erode_rate = std::max( 0.0f, density + erode_rate_randomness * (RandomFloat() - 0.5f)); - t.cur_slice_.p1.age = 0.0f; + t.cur_slice_.p1.age_ms = 0; t.cur_slice_.p1.brightness = start_brightness; t.cur_slice_.p1.fade = density * t.start_fade_scale_; t.cur_slice_.p1.glow_r = t.cur_slice_.p1.glow_g = @@ -931,7 +939,7 @@ void BGDynamicsServer::UpdateTendrils() { t.cur_slice_.p2.erode = t.start_erode_; t.cur_slice_.p2.erode_rate = std::max( 0.0f, density + erode_rate_randomness * (RandomFloat() - 0.5f)); - t.cur_slice_.p2.age = 0.0f; + t.cur_slice_.p2.age_ms = 0; t.cur_slice_.p2.brightness = start_brightness_2; t.cur_slice_.p2.fade = density * t.start_fade_scale_; t.cur_slice_.p2.glow_r = t.cur_slice_.p2.glow_g = @@ -994,11 +1002,11 @@ void BGDynamicsServer::Clear() { } void BGDynamicsServer::PushEmitCall(const BGDynamicsEmission& def) { - thread()->PushCall([this, def] { Emit(def); }); + event_loop()->PushCall([this, def] { Emit(def); }); } void BGDynamicsServer::Emit(const BGDynamicsEmission& def) { - assert(InBGDynamicsThread()); + assert(g_base->InBGDynamicsThread()); if (def.emit_type == BGDynamicsEmitType::kDistortion) { fields_.push_back(new Field(this, def.position, def.spread)); @@ -1037,7 +1045,7 @@ void BGDynamicsServer::Emit(const BGDynamicsEmission& def) { // On k1 android let's ramp things up even more. #if BA_OSTYPE_ANDROID - if (g_platform->is_tegra_k1()) { + if (g_core->platform->is_tegra_k1()) { chunk_max = static_cast(static_cast(chunk_max) * 1.5f); emit_count = static_cast(static_cast(emit_count) * 1.5f); tendril_thin_max = @@ -1366,12 +1374,13 @@ void BGDynamicsServer::Emit(const BGDynamicsEmission& def) { } } -void BGDynamicsServer::PushRemoveTerrainCall(CollideModelData* collide_model) { - thread()->PushCall([this, collide_model] { - assert(collide_model != nullptr); +void BGDynamicsServer::PushRemoveTerrainCall( + CollisionMeshAsset* collision_mesh) { + event_loop()->PushCall([this, collision_mesh] { + assert(collision_mesh != nullptr); bool found = false; for (auto i = terrains_.begin(); i != terrains_.end(); ++i) { - if ((**i).GetCollideModel() == collide_model) { + if ((**i).GetCollisionMesh() == collision_mesh) { found = true; delete *i; terrains_.erase(i); @@ -1397,16 +1406,16 @@ void BGDynamicsServer::PushRemoveTerrainCall(CollideModelData* collide_model) { } void BGDynamicsServer::PushAddShadowCall(BGDynamicsShadowData* shadow_data) { - thread()->PushCall([this, shadow_data] { - assert(InBGDynamicsThread()); + event_loop()->PushCall([this, shadow_data] { + assert(g_base->InBGDynamicsThread()); std::scoped_lock lock(shadow_list_mutex_); shadows_.push_back(shadow_data); }); } void BGDynamicsServer::PushRemoveShadowCall(BGDynamicsShadowData* shadow_data) { - thread()->PushCall([this, shadow_data] { - assert(InBGDynamicsThread()); + event_loop()->PushCall([this, shadow_data] { + assert(g_base->InBGDynamicsThread()); bool found = false; { std::scoped_lock lock(shadow_list_mutex_); @@ -1425,7 +1434,7 @@ void BGDynamicsServer::PushRemoveShadowCall(BGDynamicsShadowData* shadow_data) { void BGDynamicsServer::PushAddVolumeLightCall( BGDynamicsVolumeLightData* volume_light_data) { - thread()->PushCall([this, volume_light_data] { + event_loop()->PushCall([this, volume_light_data] { // Add to our internal list. std::scoped_lock lock(volume_light_list_mutex_); volume_lights_.push_back(volume_light_data); @@ -1434,7 +1443,7 @@ void BGDynamicsServer::PushAddVolumeLightCall( void BGDynamicsServer::PushRemoveVolumeLightCall( BGDynamicsVolumeLightData* volume_light_data) { - thread()->PushCall([this, volume_light_data] { + event_loop()->PushCall([this, volume_light_data] { // Remove from our list and kill. bool found = false; { @@ -1453,14 +1462,14 @@ void BGDynamicsServer::PushRemoveVolumeLightCall( } void BGDynamicsServer::PushAddFuseCall(BGDynamicsFuseData* fuse_data) { - thread()->PushCall([this, fuse_data] { + event_loop()->PushCall([this, fuse_data] { std::scoped_lock lock(fuse_list_mutex_); fuses_.push_back(fuse_data); }); } void BGDynamicsServer::PushRemoveFuseCall(BGDynamicsFuseData* fuse_data) { - thread()->PushCall([this, fuse_data] { + event_loop()->PushCall([this, fuse_data] { bool found = false; { std::scoped_lock lock(fuse_list_mutex_); @@ -1478,15 +1487,15 @@ void BGDynamicsServer::PushRemoveFuseCall(BGDynamicsFuseData* fuse_data) { } void BGDynamicsServer::PushSetDebrisFrictionCall(float friction) { - thread()->PushCall([this, friction] { debris_friction_ = friction; }); + event_loop()->PushCall([this, friction] { debris_friction_ = friction; }); } void BGDynamicsServer::PushSetDebrisKillHeightCall(float height) { - thread()->PushCall([this, height] { debris_kill_height_ = height; }); + event_loop()->PushCall([this, height] { debris_kill_height_ = height; }); } auto BGDynamicsServer::CreateDrawSnapshot() -> BGDynamicsDrawSnapshot* { - assert(InBGDynamicsThread()); + assert(g_base->InBGDynamicsThread()); auto* ss = new BGDynamicsDrawSnapshot(); @@ -1594,35 +1603,39 @@ auto BGDynamicsServer::CreateDrawSnapshot() -> BGDynamicsDrawSnapshot* { uint16_t *s_index = nullptr, *l_index = nullptr; VertexSprite *s_vertex = nullptr, *l_vertex = nullptr; uint32_t s_vertex_index = 0, l_vertex_index = 0; + if (shadow_max_count > 0) { auto* ibuf = Object::NewDeferred(shadow_max_count * 6); - - // Logic thread is default owner; needs to be us until we hand it over. + // Logic thread is default owner for this type. It needs to be us until + // we hand it over, so set that up before creating the first ref. ibuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); - ss->shadow_indices = Object::MakeRefCounted(ibuf); + ss->shadow_indices = Object::CompleteDeferred(ibuf); s_index = &ss->shadow_indices->elements[0]; + auto* vbuf = Object::NewDeferred(shadow_max_count * 4); - - // Logic thread is default owner; needs to be us until we hand it over. + // Logic thread is default owner for this type. It needs to be us until + // we hand it over, so set that up before creating the first ref. vbuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); - ss->shadow_vertices = Object::MakeRefCounted(vbuf); + ss->shadow_vertices = Object::CompleteDeferred(vbuf); s_vertex = &ss->shadow_vertices->elements[0]; s_vertex_index = 0; } + if (light_max_count > 0) { auto* ibuf = Object::NewDeferred(light_max_count * 6); - - // Logic thread is default owner; needs to be us until we hand it over. + // Logic thread is default owner for this type. It needs to be us until + // we hand it over, so set that up before creating the first ref. ibuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); - ss->light_indices = Object::MakeRefCounted(ibuf); + ss->light_indices = Object::CompleteDeferred(ibuf); l_index = &ss->light_indices->elements[0]; + auto* vbuf = Object::NewDeferred(light_max_count * 4); - - // Logic thread is default owner; needs to be us until we hand it over. + // Logic thread is default owner for this type. It needs to be us until + // we hand it over, so set that up before creating the first ref. vbuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); - ss->light_vertices = Object::MakeRefCounted(vbuf); + ss->light_vertices = Object::CompleteDeferred(vbuf); l_vertex = &ss->light_vertices->elements[0]; l_vertex_index = 0; } @@ -1691,9 +1704,9 @@ auto BGDynamicsServer::CreateDrawSnapshot() -> BGDynamicsDrawSnapshot* { if (type == BGDynamicsChunkType::kSplinter) shadow_size *= 0.65f; float flicker = i->flicker_; float shadow_dist = i->shadow_dist_; - float life = std::min( - 1.0f, (static_cast(time_) - static_cast(i->birth_time_)) - / i->lifespan_); + float life = std::min(1.0f, (static_cast(time_ms_) + - static_cast(i->birth_time_)) + / i->lifespan_); // Shrink our matrix down over time. switch (type) { @@ -1795,7 +1808,8 @@ auto BGDynamicsServer::CreateDrawSnapshot() -> BGDynamicsDrawSnapshot* { 1.0f + std::max(0.0f, std::min(1.0f, (sd / max_shadow_grow_dist)) * (max_shadow_scale - 1.0f)); - density = 0.5f * g_graphics->GetShadowDensity(m[12], m[13], m[14]) + density = 0.5f + * g_base->graphics->GetShadowDensity(m[12], m[13], m[14]) * (1.0f - (sd / max_shadow_dist)); } @@ -1968,17 +1982,18 @@ auto BGDynamicsServer::CreateDrawSnapshot() -> BGDynamicsDrawSnapshot* { } if (smoke_slice_count > 0) { auto* ibuf = Object::NewDeferred(smoke_index_count); - - // Logic thread is default owner; needs to be us until we hand it over. + // Logic thread is default owner for this type. It needs to be us until + // we hand it over, so set that up before creating the first ref. ibuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); - ss->tendril_indices = Object::MakeRefCounted(ibuf); + ss->tendril_indices = Object::CompleteDeferred(ibuf); uint16_t* index = &ss->tendril_indices->elements[0]; + auto* vbuf = Object::NewDeferred(smoke_slice_count * 2); - - // Logic thread is default owner; needs to be us until we hand it over. + // Logic thread is default owner for this type. It needs to be us until + // we hand it over, so set that up before creating the first ref. vbuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); - ss->tendril_vertices = Object::MakeRefCounted(vbuf); + ss->tendril_vertices = Object::CompleteDeferred(vbuf); VertexSmokeFull* v = &ss->tendril_vertices->elements[0]; ss->tendril_shadows.reserve(static_cast(shadow_count)); int v_num = 0; @@ -2133,16 +2148,17 @@ auto BGDynamicsServer::CreateDrawSnapshot() -> BGDynamicsDrawSnapshot* { static_cast(2 * kFusePointCount * fuse_count); auto* ibuf = Object::NewDeferred(index_count); - - // Logic thread is default owner; needs to be us until we hand it over. + // Logic thread is default owner for this type. It needs to be us until + // we hand it over, so set that up before creating the first ref. ibuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); - ss->fuse_indices = Object::MakeRefCounted(ibuf); + ss->fuse_indices = Object::CompleteDeferred(ibuf); + auto* vbuf = Object::NewDeferred(vertex_count); - - // Logic thread is default owner; needs to be us until we hand it over. + // Logic thread is default owner for this type. It needs to be us until + // we hand it over, so set that up before creating the first ref. vbuf->SetThreadOwnership(Object::ThreadOwnership::kNextReferencing); - ss->fuse_vertices = Object::MakeRefCounted(vbuf); + ss->fuse_vertices = Object::CompleteDeferred(vbuf); uint16_t* index = &ss->fuse_indices->elements[0]; VertexSimpleFull* v = &ss->fuse_vertices->elements[0]; @@ -2218,7 +2234,7 @@ auto BGDynamicsServer::CreateDrawSnapshot() -> BGDynamicsDrawSnapshot* { } // NOLINT (yes this should be shorter) void BGDynamicsServer::PushTooSlowCall() { - thread()->PushCall([this] { + event_loop()->PushCall([this] { if (chunk_count_ > 0 || tendril_count_thick_ > 0 || tendril_count_thin_ > 0) { // Ok lets kill a small percentage of our oldest chunks. @@ -2258,15 +2274,15 @@ void BGDynamicsServer::PushTooSlowCall() { } void BGDynamicsServer::Step(StepData* step_data) { - assert(InBGDynamicsThread()); + assert(g_base->InBGDynamicsThread()); assert(step_data); // Grab a ref to the raw StepData pointer we were passed... we now own the // data. - auto ref(Object::MakeRefCounted(step_data)); + auto ref(Object::CompleteDeferred(step_data)); // Keep this in sync with the game thread's. - graphics_quality_ = g_graphics_server->graphics_quality_requested(); + graphics_quality_ = g_base->graphics_server->graphics_quality(); cam_pos_ = step_data->cam_pos; @@ -2310,18 +2326,21 @@ void BGDynamicsServer::Step(StepData* step_data) { UpdateTendrils(); UpdateFuses(); + step_milliseconds_ = static_cast(step_data->step_millisecs); + step_seconds_ = step_milliseconds_ / 1000.0f; + // Step the world. - dWorldQuickStep(ode_world_, kGameStepSeconds); + dWorldQuickStep(ode_world_, step_seconds_); // Now generate a snapshot of our state and send it to the game thread, // so they can draw us. BGDynamicsDrawSnapshot* snapshot = CreateDrawSnapshot(); - g_logic->thread()->PushCall([snapshot] { + g_base->logic->event_loop()->PushCall([snapshot] { snapshot->SetLogicThreadOwnership(); - g_bg_dynamics->SetDrawSnapshot(snapshot); + g_base->bg_dynamics->SetDrawSnapshot(snapshot); }); - time_ += kGameStepMilliseconds; // milliseconds per step + time_ms_ += step_milliseconds_; // milliseconds per step // Give our collision cache a bit of processing time here and // there to fill itself in slowly. @@ -2335,21 +2354,27 @@ void BGDynamicsServer::Step(StepData* step_data) { assert(step_count_ >= 0); } -void BGDynamicsServer::PushStepCall(StepData* data) { - thread()->PushCall([this, data] { Step(data); }); +void BGDynamicsServer::PushStep(StepData* data) { + // Increase our step count and ship it. + { + std::scoped_lock lock(step_count_mutex_); + step_count_++; + } + + event_loop()->PushCall([this, data] { Step(data); }); } void BGDynamicsServer::PushAddTerrainCall( - Object::Ref* collide_model) { - thread()->PushCall([this, collide_model] { - assert(InBGDynamicsThread()); - assert(collide_model != nullptr); + Object::Ref* collision_mesh) { + event_loop()->PushCall([this, collision_mesh] { + assert(g_base->InBGDynamicsThread()); + assert(collision_mesh != nullptr); // Make sure its loaded (might not be when we get it). - (**collide_model).Load(); + (**collision_mesh).Load(); // (the terrain now owns the ref pointer passed in) - terrains_.push_back(new Terrain(this, collide_model)); + terrains_.push_back(new Terrain(this, collision_mesh)); // Rebuild geom list from our present terrains. std::vector geoms; @@ -2373,7 +2398,7 @@ void BGDynamicsServer::UpdateFields() { // First off, kill this field if its time has come. { bool kill = false; - if (static_cast(time_ - f.birth_time()) > f.lifespan()) { + if (static_cast(time_ms_ - f.birth_time_ms()) > f.lifespan_ms()) { kill = true; } if (kill) { @@ -2387,7 +2412,7 @@ void BGDynamicsServer::UpdateFields() { } // Update its distortion amount based on age (get an age in 0-1). - float age = static_cast(time() - f.birth_time()) / f.lifespan(); + float age = (time_ms() - f.birth_time_ms()) / f.lifespan_ms(); float time_scale = 1.3f; float start_mag = 0.0f; @@ -2492,7 +2517,7 @@ void BGDynamicsServer::UpdateChunks() { // first off, kill this chunk if its time has come { bool kill = false; - if (static_cast(time_ - c.birth_time_) > c.lifespan_) { + if (time_ms_ - c.birth_time_ > c.lifespan_) { kill = true; } @@ -2651,4 +2676,4 @@ void BGDynamicsServer::UpdateShadows() { } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/dynamics/bg/bg_dynamics_server.h b/src/ballistica/base/dynamics/bg/bg_dynamics_server.h similarity index 65% rename from src/ballistica/dynamics/bg/bg_dynamics_server.h rename to src/ballistica/base/dynamics/bg/bg_dynamics_server.h index 8e6a2e61..46918f45 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics_server.h +++ b/src/ballistica/base/dynamics/bg/bg_dynamics_server.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SERVER_H_ -#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SERVER_H_ +#ifndef BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_SERVER_H_ +#define BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_SERVER_H_ #include #include @@ -9,12 +9,12 @@ #include #include -#include "ballistica/dynamics/bg/bg_dynamics.h" -#include "ballistica/math/matrix44f.h" -#include "ballistica/math/vector3f.h" +#include "ballistica/base/dynamics/bg/bg_dynamics.h" +#include "ballistica/shared/math/matrix44f.h" +#include "ballistica/shared/math/vector3f.h" #include "ode/ode.h" -namespace ballistica { +namespace ballistica::base { class BGDynamicsServer { public: @@ -49,9 +49,11 @@ class BGDynamicsServer { void UpdateAndCreateSnapshot(Object::Ref* index_buffer, Object::Ref* buffer); }; + struct ShadowStepData { Vector3f position; }; + struct VolumeLightStepData { Vector3f pos{}; float radius{}; @@ -59,16 +61,19 @@ class BGDynamicsServer { float g{}; float b{}; }; + struct FuseStepData { Matrix44f transform{}; bool have_transform{}; float length{}; }; + class StepData : public Object { public: - auto GetDefaultOwnerThread() const -> ThreadTag override { - return ThreadTag::kBGDynamics; + auto GetDefaultOwnerThread() const -> EventLoopID override { + return EventLoopID::kBGDynamics; } + int step_millisecs{}; Vector3f cam_pos{0.0f, 0.0f, 0.0f}; // Basically a bit list of pointers to the current set of @@ -81,8 +86,9 @@ class BGDynamicsServer { }; BGDynamicsServer(); + void OnMainThreadStartApp(); - auto time() const -> uint32_t { return time_; } + auto time_ms() const { return time_ms_; } auto graphics_quality() const -> GraphicsQuality { return graphics_quality_; } void PushAddVolumeLightCall(BGDynamicsVolumeLightData* volume_light_data); @@ -91,14 +97,31 @@ class BGDynamicsServer { void PushRemoveFuseCall(BGDynamicsFuseData* fuse_data); void PushAddShadowCall(BGDynamicsShadowData* shadow_data); void PushRemoveShadowCall(BGDynamicsShadowData* shadow_data); - void PushAddTerrainCall(Object::Ref* collide_model); - void PushRemoveTerrainCall(CollideModelData* collide_model); + void PushAddTerrainCall(Object::Ref* collision_mesh); + void PushRemoveTerrainCall(CollisionMeshAsset* collision_mesh); void PushEmitCall(const BGDynamicsEmission& def); auto spark_particles() const -> ParticleSet* { return spark_particles_.get(); } auto step_count() const -> int { return step_count_; } - auto thread() const -> Thread* { return thread_; } + auto event_loop() const -> EventLoop* { return event_loop_; } + + auto& shadow_list_mutex() { return shadow_list_mutex_; } + auto& volume_light_list_mutex() { return volume_light_list_mutex_; } + auto& fuse_list_mutex() { return fuse_list_mutex_; } + auto& step_count_mutex() { return step_count_mutex_; } + + const auto& terrains() const { return terrains_; } + const auto& shadows() const { return shadows_; } + const auto& volume_lights() const { return volume_lights_; } + const auto& fuses() const { return fuses_; } + void PushStep(StepData* data); + void PushTooSlowCall(); + void PushSetDebrisFrictionCall(float friction); + void PushSetDebrisKillHeightCall(float height); + + auto step_seconds() const { return step_seconds_; } + auto step_milliseconds() const { return step_milliseconds_; } private: class Terrain; @@ -110,11 +133,7 @@ class BGDynamicsServer { static void TerrainCollideCallback(void* data, dGeomID o1, dGeomID o2); void Emit(const BGDynamicsEmission& def); - void PushStepCall(StepData* data); void Step(StepData* data); - void PushTooSlowCall(); - void PushSetDebrisFrictionCall(float friction); - void PushSetDebrisKillHeightCall(float height); void Clear(); void UpdateFields(); void UpdateChunks(); @@ -122,52 +141,54 @@ class BGDynamicsServer { void UpdateFuses(); void UpdateShadows(); auto CreateDrawSnapshot() -> BGDynamicsDrawSnapshot*; + void CalcERPCFM(dReal stiffness, dReal damping, dReal* erp, dReal* cfm); - Thread* thread_{}; + EventLoop* event_loop_{}; BGDynamicsChunkType cb_type_ = BGDynamicsChunkType::kRock; dBodyID cb_body_{}; - float cb_cfm_{0.0f}; - float cb_erp_{0.0f}; + float cb_cfm_{}; + float cb_erp_{}; // FIXME: We're assuming at the moment - // that collide-models passed to this thread never get deallocated. ew. - MeshIndexedSmokeFull* tendrils_smoke_mesh_{nullptr}; - MeshIndexedSimpleFull* fuses_mesh_{nullptr}; - SpriteMesh* shadows_mesh_{nullptr}; - SpriteMesh* lights_mesh_{nullptr}; - SpriteMesh* sparks_mesh_{nullptr}; - int miss_count_{0}; + // that collision-meshes passed to this thread never get deallocated. ew. + MeshIndexedSmokeFull* tendrils_smoke_mesh_{}; + MeshIndexedSimpleFull* fuses_mesh_{}; + SpriteMesh* shadows_mesh_{}; + SpriteMesh* lights_mesh_{}; + SpriteMesh* sparks_mesh_{}; + int miss_count_{}; Vector3f cam_pos_{0.0f, 0.0f, 0.0f}; std::vector terrains_; std::vector shadows_; std::vector volume_lights_; std::vector fuses_; - dWorldID ode_world_{nullptr}; - dJointGroupID ode_contact_group_{nullptr}; + dWorldID ode_world_{}; + dJointGroupID ode_contact_group_{}; // Held by the dynamics module when changing any of these lists. // Should be grabbed by a client if they need to access the list safely. std::mutex shadow_list_mutex_; std::mutex volume_light_list_mutex_; std::mutex fuse_list_mutex_; - int step_count_{0}; + int step_count_{}; std::mutex step_count_mutex_; - std::unique_ptr spark_particles_{nullptr}; + std::unique_ptr spark_particles_{}; std::list chunks_; std::list fields_; std::list tendrils_; - int tendril_count_thick_{0}; - int tendril_count_thin_{0}; - int chunk_count_{0}; + int tendril_count_thick_{}; + int tendril_count_thin_{}; + int chunk_count_{}; std::unique_ptr height_cache_; std::unique_ptr collision_cache_; - uint32_t time_{0}; // Internal time step. + float time_ms_{}; // Internal time step. float debris_friction_{1.0f}; float debris_kill_height_{-50.0f}; + float step_seconds_{}; + float step_milliseconds_{}; GraphicsQuality graphics_quality_{GraphicsQuality::kLow}; - friend class BGDynamics; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SERVER_H_ +#endif // BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_SERVER_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_shadow.cc b/src/ballistica/base/dynamics/bg/bg_dynamics_shadow.cc similarity index 57% rename from src/ballistica/dynamics/bg/bg_dynamics_shadow.cc rename to src/ballistica/base/dynamics/bg/bg_dynamics_shadow.cc index 4d74075b..649ffbf8 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics_shadow.cc +++ b/src/ballistica/base/dynamics/bg/bg_dynamics_shadow.cc @@ -1,53 +1,53 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/bg/bg_dynamics_shadow.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_shadow.h" -#include "ballistica/dynamics/bg/bg_dynamics_server.h" -#include "ballistica/dynamics/bg/bg_dynamics_shadow_data.h" -#include "ballistica/graphics/graphics.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_server.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_shadow_data.h" +#include "ballistica/base/graphics/graphics.h" -namespace ballistica { +namespace ballistica::base { BGDynamicsShadow::BGDynamicsShadow(float height_scaling) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // allocate our shadow data... we'll pass this to the BGDynamics thread, // which will then own it. data_ = new BGDynamicsShadowData(height_scaling); - assert(g_bg_dynamics_server); - g_bg_dynamics_server->PushAddShadowCall(data_); + assert(g_base->bg_dynamics_server); + g_base->bg_dynamics_server->PushAddShadowCall(data_); } BGDynamicsShadow::~BGDynamicsShadow() { - assert(InLogicThread()); - assert(g_bg_dynamics_server); + assert(g_base->InLogicThread()); + assert(g_base->bg_dynamics_server); // let the data know the client side is dead, // so we're no longer included in step messages // (since by the time the worker gets it the data will be gone) data_->client_dead = true; - g_bg_dynamics_server->PushRemoveShadowCall(data_); + g_base->bg_dynamics_server->PushRemoveShadowCall(data_); } void BGDynamicsShadow::SetPosition(const Vector3f& pos) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); data_->pos_client = pos; } auto BGDynamicsShadow::GetPosition() const -> const Vector3f& { - assert(InLogicThread()); + assert(g_base->InLogicThread()); return data_->pos_client; } void BGDynamicsShadow::GetValues(float* scale, float* density) const { - assert(InLogicThread()); + assert(g_base->InLogicThread()); assert(scale); assert(density); *scale = data_->shadow_scale_client; *density = data_->shadow_density_client - * g_graphics->GetShadowDensity( + * g_base->graphics->GetShadowDensity( data_->pos_client.x, data_->pos_client.y, data_->pos_client.z); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/dynamics/bg/bg_dynamics_shadow.h b/src/ballistica/base/dynamics/bg/bg_dynamics_shadow.h similarity index 73% rename from src/ballistica/dynamics/bg/bg_dynamics_shadow.h rename to src/ballistica/base/dynamics/bg/bg_dynamics_shadow.h index e50ffb9f..30cc0af8 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics_shadow.h +++ b/src/ballistica/base/dynamics/bg/bg_dynamics_shadow.h @@ -1,12 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SHADOW_H_ -#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SHADOW_H_ +#ifndef BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_SHADOW_H_ +#define BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_SHADOW_H_ -#include "ballistica/ballistica.h" -#include "ballistica/dynamics/bg/bg_dynamics.h" +#include "ballistica/base/dynamics/bg/bg_dynamics.h" +#include "ballistica/shared/ballistica.h" -namespace ballistica { +namespace ballistica::base { // A utility class for client use which uses ray-testing and // BG collision terrains to create a variably dense/soft shadow @@ -31,6 +31,6 @@ class BGDynamicsShadow { BA_DISALLOW_CLASS_COPIES(BGDynamicsShadow); }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SHADOW_H_ +#endif // BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_SHADOW_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_shadow_data.h b/src/ballistica/base/dynamics/bg/bg_dynamics_shadow_data.h similarity index 82% rename from src/ballistica/dynamics/bg/bg_dynamics_shadow_data.h rename to src/ballistica/base/dynamics/bg/bg_dynamics_shadow_data.h index 1ec313ea..420b668b 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics_shadow_data.h +++ b/src/ballistica/base/dynamics/bg/bg_dynamics_shadow_data.h @@ -1,9 +1,9 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SHADOW_DATA_H_ -#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SHADOW_DATA_H_ +#ifndef BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_SHADOW_DATA_H_ +#define BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_SHADOW_DATA_H_ -namespace ballistica { +namespace ballistica::base { struct BGDynamicsShadowData { explicit BGDynamicsShadowData(float height_scaling) @@ -41,6 +41,6 @@ struct BGDynamicsShadowData { float shadow_density_client{0.0f}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_SHADOW_DATA_H_ +#endif // BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_SHADOW_DATA_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_volume_light.cc b/src/ballistica/base/dynamics/bg/bg_dynamics_volume_light.cc similarity index 59% rename from src/ballistica/dynamics/bg/bg_dynamics_volume_light.cc rename to src/ballistica/base/dynamics/bg/bg_dynamics_volume_light.cc index 2d228f98..c9654ef6 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics_volume_light.cc +++ b/src/ballistica/base/dynamics/bg/bg_dynamics_volume_light.cc @@ -1,47 +1,47 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/bg/bg_dynamics_volume_light.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_volume_light.h" -#include "ballistica/dynamics/bg/bg_dynamics_volume_light_data.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_volume_light_data.h" -namespace ballistica { +namespace ballistica::base { BGDynamicsVolumeLight::BGDynamicsVolumeLight() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // allocate our light data... we'll pass this to the BGDynamics thread, // which will then own it data_ = new BGDynamicsVolumeLightData(); - assert(g_bg_dynamics_server); - g_bg_dynamics_server->PushAddVolumeLightCall(data_); + assert(g_base->bg_dynamics_server); + g_base->bg_dynamics_server->PushAddVolumeLightCall(data_); } BGDynamicsVolumeLight::~BGDynamicsVolumeLight() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // let the data know the client side is dead, // so we're no longer included in step messages // (since by the time the worker gets it the data will be gone) data_->client_dead = true; - assert(g_bg_dynamics_server); - g_bg_dynamics_server->PushRemoveVolumeLightCall(data_); + assert(g_base->bg_dynamics_server); + g_base->bg_dynamics_server->PushRemoveVolumeLightCall(data_); } void BGDynamicsVolumeLight::SetPosition(const Vector3f& pos) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); data_->pos_client = pos; } void BGDynamicsVolumeLight::SetRadius(float radius) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); data_->radius_client = radius; } void BGDynamicsVolumeLight::SetColor(float r, float g, float b) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); data_->r_client = r; data_->g_client = g; data_->b_client = b; } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/dynamics/bg/bg_dynamics_volume_light.h b/src/ballistica/base/dynamics/bg/bg_dynamics_volume_light.h similarity index 53% rename from src/ballistica/dynamics/bg/bg_dynamics_volume_light.h rename to src/ballistica/base/dynamics/bg/bg_dynamics_volume_light.h index 20ebc2db..d871e2b1 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics_volume_light.h +++ b/src/ballistica/base/dynamics/bg/bg_dynamics_volume_light.h @@ -1,11 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_H_ -#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_H_ +#ifndef BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_H_ +#define BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_H_ -#include "ballistica/core/object.h" +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::base { // Client-controlled lights for bg smoke. class BGDynamicsVolumeLight : public Object { @@ -20,6 +21,6 @@ class BGDynamicsVolumeLight : public Object { BGDynamicsVolumeLightData* data_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_H_ +#endif // BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_H_ diff --git a/src/ballistica/dynamics/bg/bg_dynamics_volume_light_data.h b/src/ballistica/base/dynamics/bg/bg_dynamics_volume_light_data.h similarity index 58% rename from src/ballistica/dynamics/bg/bg_dynamics_volume_light_data.h rename to src/ballistica/base/dynamics/bg/bg_dynamics_volume_light_data.h index 61e31941..e718ef69 100644 --- a/src/ballistica/dynamics/bg/bg_dynamics_volume_light_data.h +++ b/src/ballistica/base/dynamics/bg/bg_dynamics_volume_light_data.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_DATA_H_ -#define BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_DATA_H_ +#ifndef BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_DATA_H_ +#define BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_DATA_H_ -#include "ballistica/dynamics/bg/bg_dynamics_server.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_server.h" -namespace ballistica { +namespace ballistica::base { struct BGDynamicsVolumeLightData { bool client_dead{}; @@ -25,6 +25,6 @@ struct BGDynamicsVolumeLightData { float b_worker{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_DATA_H_ +#endif // BALLISTICA_BASE_DYNAMICS_BG_BG_DYNAMICS_VOLUME_LIGHT_DATA_H_ diff --git a/src/ballistica/dynamics/collision_cache.cc b/src/ballistica/base/dynamics/collision_cache.cc similarity index 91% rename from src/ballistica/dynamics/collision_cache.cc rename to src/ballistica/base/dynamics/collision_cache.cc index 8568d0f6..2f005dc0 100644 --- a/src/ballistica/dynamics/collision_cache.cc +++ b/src/ballistica/base/dynamics/collision_cache.cc @@ -1,12 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/collision_cache.h" +#include "ballistica/base/dynamics/collision_cache.h" -#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/base/graphics/component/simple_component.h" #include "ode/ode_collision_kernel.h" #include "ode/ode_collision_space_internal.h" -namespace ballistica { +namespace ballistica::base { CollisionCache::CollisionCache() : test_box_{dCreateBox(nullptr, 1, 1, 1)} {} @@ -17,12 +17,12 @@ CollisionCache::~CollisionCache() { dGeomDestroy(test_box_); } -auto CollisionCache::SetGeoms(const std::vector& geoms) -> void { +void CollisionCache::SetGeoms(const std::vector& geoms) { dirty_ = true; geoms_ = geoms; } -auto CollisionCache::Draw(FrameDef* frame_def) -> void { +void CollisionCache::Draw(FrameDef* frame_def) { if (cells_.empty()) { return; } @@ -36,7 +36,7 @@ auto CollisionCache::Draw(FrameDef* frame_def) -> void { c.Scale(x_max_ - x_min_, 1, z_max_ - z_min_); c.PushTransform(); c.Scale(1, 0.01f, 1); - c.DrawModel(g_assets->GetModel(SystemModelID::kBox)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kBox)); c.PopTransform(); c.Translate(-0.5f + 0.5f * cell_width, 0, -0.5f + 0.5f * cell_height); for (int x = 0; x < grid_width_; x++) { @@ -53,7 +53,7 @@ auto CollisionCache::Draw(FrameDef* frame_def) -> void { cells_[cell_index].height_confirmed_collide_, static_cast(z) / static_cast(grid_height_)); c.Scale(0.95f * cell_width, 0.01f, 0.95f * cell_height); - c.DrawModel(g_assets->GetModel(SystemModelID::kBox)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kBox)); c.PopTransform(); if (glow_[cell_index]) { c.SetColor(1, 1, 1, 0.2f); @@ -65,7 +65,7 @@ auto CollisionCache::Draw(FrameDef* frame_def) -> void { cells_[cell_index].height_confirmed_empty_, static_cast(z) / static_cast(grid_height_)); c.Scale(0.95f * cell_width, 0.01f, 0.95f * cell_height); - c.DrawModel(g_assets->GetModel(SystemModelID::kBox)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kBox)); c.PopTransform(); glow_[cell_index] = 0; } @@ -84,7 +84,7 @@ auto CollisionCache::Draw(FrameDef* frame_def) -> void { c2.Scale(x_max_ - x_min_, 1, z_max_ - z_min_); c2.PushTransform(); c2.Scale(1, 0.01f, 1); - c2.DrawModel(g_assets->GetModel(SystemModelID::kBox)); + c2.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kBox)); c2.PopTransform(); c2.Translate(-0.5f + 0.5f * cell_width2, 0, -0.5f + 0.5f * cell_height2); for (int x = 0; x < grid_width_; x++) { @@ -101,7 +101,7 @@ auto CollisionCache::Draw(FrameDef* frame_def) -> void { cells_[cell_index].height_confirmed_empty_, static_cast(z) / static_cast(grid_height_)); c2.Scale(0.95f * cell_width2, 0.01f, 0.95f * cell_height2); - c2.DrawModel(g_assets->GetModel(SystemModelID::kBox)); + c2.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kBox)); c2.PopTransform(); if (glow_[cell_index]) { c2.SetColor(1, 1, 1, 0.2f); @@ -113,7 +113,7 @@ auto CollisionCache::Draw(FrameDef* frame_def) -> void { cells_[cell_index].height_confirmed_collide_, static_cast(z) / static_cast(grid_height_)); c2.Scale(0.95f * cell_width2, 0.01f, 0.95f * cell_height2); - c2.DrawModel(g_assets->GetModel(SystemModelID::kBox)); + c2.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kBox)); c2.PopTransform(); glow_[cell_index] = 0; @@ -124,7 +124,7 @@ auto CollisionCache::Draw(FrameDef* frame_def) -> void { } } -auto CollisionCache::Precalc() -> void { +void CollisionCache::Precalc() { Update(); if (precalc_index_ >= cells_.size()) { @@ -138,8 +138,8 @@ auto CollisionCache::Precalc() -> void { TestCell(precalc_index_++, x, z); } -auto CollisionCache::CollideAgainstGeom(dGeomID g1, void* data, - dNearCallback* callback) -> void { +void CollisionCache::CollideAgainstGeom(dGeomID g1, void* data, + dNearCallback* callback) { // Update bounds, test for quick out against our height map, // and proceed to a full test on a positive result. g1->recomputeAABB(); @@ -204,7 +204,7 @@ auto CollisionCache::CollideAgainstGeom(dGeomID g1, void* data, } } -auto CollisionCache::TestCell(size_t cell_index, int x, int z) -> void { +void CollisionCache::TestCell(size_t cell_index, int x, int z) { int t_count = static_cast(geoms_.size()); float top = cells_[cell_index].height_confirmed_empty_; @@ -252,8 +252,8 @@ auto CollisionCache::TestCell(size_t cell_index, int x, int z) -> void { } } -auto CollisionCache::CollideAgainstSpace(dSpaceID space, void* data, - dNearCallback* callback) -> void { +void CollisionCache::CollideAgainstSpace(dSpaceID space, void* data, + dNearCallback* callback) { // We handle our own testing against trimeshes, so we can bring our fancy // caching into play. if (!geoms_.empty()) { @@ -264,7 +264,7 @@ auto CollisionCache::CollideAgainstSpace(dSpaceID space, void* data, } } -auto CollisionCache::Update() -> void { +void CollisionCache::Update() { if (!dirty_) { return; } @@ -336,4 +336,4 @@ auto CollisionCache::Update() -> void { dirty_ = false; } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/dynamics/collision_cache.h b/src/ballistica/base/dynamics/collision_cache.h similarity index 61% rename from src/ballistica/dynamics/collision_cache.h rename to src/ballistica/base/dynamics/collision_cache.h index ea8e02c7..62eaa654 100644 --- a/src/ballistica/dynamics/collision_cache.h +++ b/src/ballistica/base/dynamics/collision_cache.h @@ -1,14 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_COLLISION_CACHE_H_ -#define BALLISTICA_DYNAMICS_COLLISION_CACHE_H_ +#ifndef BALLISTICA_BASE_DYNAMICS_COLLISION_CACHE_H_ +#define BALLISTICA_BASE_DYNAMICS_COLLISION_CACHE_H_ #include -#include "ballistica/ballistica.h" +#include "ballistica/base/base.h" #include "ode/ode.h" -namespace ballistica { +namespace ballistica::base { // Given geoms, creates/samples a height map on the fly // which can be used for very fast AABB tests against the geometry. @@ -18,20 +18,18 @@ class CollisionCache { ~CollisionCache(); // If returns true, the provided AABB *may* intersect the geoms. - auto SetGeoms(const std::vector& geoms) -> void; - auto Draw(FrameDef* f) -> void; // For debugging. - auto CollideAgainstSpace(dSpaceID space, void* data, dNearCallback* callback) - -> void; - auto CollideAgainstGeom(dGeomID geom, void* data, dNearCallback* callback) - -> void; + void SetGeoms(const std::vector& geoms); + void Draw(FrameDef* f); // For debugging. + void CollideAgainstSpace(dSpaceID space, void* data, dNearCallback* callback); + void CollideAgainstGeom(dGeomID geom, void* data, dNearCallback* callback); // Call this periodically (once per cycle or so) to slowly fill in // the cache so there's less to do during spurts of activity; - auto Precalc() -> void; + void Precalc(); private: - auto TestCell(size_t cell_index, int x, int z) -> void; - auto Update() -> void; + void TestCell(size_t cell_index, int x, int z); + void Update(); uint32_t precalc_index_{}; std::vector geoms_; struct Cell { @@ -55,6 +53,6 @@ class CollisionCache { float z_max_{1.0f}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_DYNAMICS_COLLISION_CACHE_H_ +#endif // BALLISTICA_BASE_DYNAMICS_COLLISION_CACHE_H_ diff --git a/src/ballistica/graphics/component/empty_component.h b/src/ballistica/base/graphics/component/empty_component.h similarity index 62% rename from src/ballistica/graphics/component/empty_component.h rename to src/ballistica/base/graphics/component/empty_component.h index 3b70970a..1a809936 100644 --- a/src/ballistica/graphics/component/empty_component.h +++ b/src/ballistica/base/graphics/component/empty_component.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_COMPONENT_EMPTY_COMPONENT_H_ -#define BALLISTICA_GRAPHICS_COMPONENT_EMPTY_COMPONENT_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_COMPONENT_EMPTY_COMPONENT_H_ +#define BALLISTICA_BASE_GRAPHICS_COMPONENT_EMPTY_COMPONENT_H_ -#include "ballistica/graphics/component/render_component.h" +#include "ballistica/base/graphics/component/render_component.h" -namespace ballistica { +namespace ballistica::base { // Empty component - has no shader but can be useful for spitting out // transform/scissor/etc state changes. @@ -25,6 +25,6 @@ class EmptyComponent : public RenderComponent { bool transparent_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_COMPONENT_EMPTY_COMPONENT_H_ +#endif // BALLISTICA_BASE_GRAPHICS_COMPONENT_EMPTY_COMPONENT_H_ diff --git a/src/ballistica/graphics/component/object_component.cc b/src/ballistica/base/graphics/component/object_component.cc similarity index 82% rename from src/ballistica/graphics/component/object_component.cc rename to src/ballistica/base/graphics/component/object_component.cc index e8d1a6bd..89d3fda7 100644 --- a/src/ballistica/graphics/component/object_component.cc +++ b/src/ballistica/base/graphics/component/object_component.cc @@ -1,19 +1,19 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/component/object_component.h" +#include "ballistica/base/graphics/component/object_component.h" -namespace ballistica { +namespace ballistica::base { void ObjectComponent::WriteConfig() { // If they didn't give us a texture, just use a blank white texture. // This is not a common case and easier than forking all our shaders to // create non-textured versions. - if (!texture_.exists()) { - texture_ = g_assets->GetTexture(SystemTextureID::kWhite); + if (!texture_.Exists()) { + texture_ = g_base->assets->SysTexture(SysTextureID::kWhite); } if (reflection_ == ReflectionType::kNone) { assert(!double_sided_); // Unsupported combo. - assert(!colorize_texture_.exists()); // Unsupported combo. + assert(!colorize_texture_.Exists()); // Unsupported combo. assert(!have_color_add_); // Unsupported combo. if (light_shadow_ == LightShadowType::kNone) { if (transparent_) { @@ -45,7 +45,7 @@ void ObjectComponent::WriteConfig() { } else { if (light_shadow_ == LightShadowType::kNone) { assert(!double_sided_); // Unsupported combo. - assert(!colorize_texture_.exists()); // Unsupported combo. + assert(!colorize_texture_.Exists()); // Unsupported combo. if (transparent_) { assert(!world_space_); // Unsupported combo. if (have_color_add_) { @@ -56,9 +56,9 @@ void ObjectComponent::WriteConfig() { reflection_scale_r_, reflection_scale_g_, reflection_scale_b_); cmd_buffer_->PutTexture(texture_); - SystemCubeMapTextureID r = + SysCubeMapTextureID r = Graphics::CubeMapFromReflectionType(reflection_); - cmd_buffer_->PutCubeMapTexture(g_assets->GetCubeMapTexture(r)); + cmd_buffer_->PutCubeMapTexture(g_base->assets->SysCubeMapTexture(r)); } else { ConfigForShading(ShadingType::kObjectReflectTransparent); cmd_buffer_->PutInt(premultiplied_); @@ -66,9 +66,9 @@ void ObjectComponent::WriteConfig() { reflection_scale_r_, reflection_scale_g_, reflection_scale_b_); cmd_buffer_->PutTexture(texture_); - SystemCubeMapTextureID r = + SysCubeMapTextureID r = Graphics::CubeMapFromReflectionType(reflection_); - cmd_buffer_->PutCubeMapTexture(g_assets->GetCubeMapTexture(r)); + cmd_buffer_->PutCubeMapTexture(g_base->assets->SysCubeMapTexture(r)); } } else { ConfigForShading(ShadingType::kObjectReflect); @@ -77,15 +77,15 @@ void ObjectComponent::WriteConfig() { reflection_scale_r_, reflection_scale_g_, reflection_scale_b_); cmd_buffer_->PutTexture(texture_); - SystemCubeMapTextureID r = + SysCubeMapTextureID r = Graphics::CubeMapFromReflectionType(reflection_); - cmd_buffer_->PutCubeMapTexture(g_assets->GetCubeMapTexture(r)); + cmd_buffer_->PutCubeMapTexture(g_base->assets->SysCubeMapTexture(r)); } } else { // With add. assert(!transparent_); // Unsupported combo. if (!have_color_add_) { - if (colorize_texture_.exists()) { + if (colorize_texture_.Exists()) { assert(!double_sided_); // Unsupported combo. assert(!world_space_); // Unsupported combo. if (do_colorize_2_) { @@ -98,9 +98,10 @@ void ObjectComponent::WriteConfig() { colorize_color2_g_, colorize_color2_b_); cmd_buffer_->PutTexture(texture_); cmd_buffer_->PutTexture(colorize_texture_); - SystemCubeMapTextureID r = + SysCubeMapTextureID r = Graphics::CubeMapFromReflectionType(reflection_); - cmd_buffer_->PutCubeMapTexture(g_assets->GetCubeMapTexture(r)); + cmd_buffer_->PutCubeMapTexture( + g_base->assets->SysCubeMapTexture(r)); } else { ConfigForShading(ShadingType::kObjectReflectLightShadowColorized); cmd_buffer_->PutInt(static_cast(light_shadow_)); @@ -110,9 +111,10 @@ void ObjectComponent::WriteConfig() { colorize_color_g_, colorize_color_b_); cmd_buffer_->PutTexture(texture_); cmd_buffer_->PutTexture(colorize_texture_); - SystemCubeMapTextureID r = + SysCubeMapTextureID r = Graphics::CubeMapFromReflectionType(reflection_); - cmd_buffer_->PutCubeMapTexture(g_assets->GetCubeMapTexture(r)); + cmd_buffer_->PutCubeMapTexture( + g_base->assets->SysCubeMapTexture(r)); } } else { if (double_sided_) { @@ -123,9 +125,10 @@ void ObjectComponent::WriteConfig() { reflection_scale_r_, reflection_scale_g_, reflection_scale_b_); cmd_buffer_->PutTexture(texture_); - SystemCubeMapTextureID r = + SysCubeMapTextureID r = Graphics::CubeMapFromReflectionType(reflection_); - cmd_buffer_->PutCubeMapTexture(g_assets->GetCubeMapTexture(r)); + cmd_buffer_->PutCubeMapTexture( + g_base->assets->SysCubeMapTexture(r)); } else { ConfigForShading(ShadingType::kObjectReflectLightShadow); cmd_buffer_->PutInt(static_cast(light_shadow_)); @@ -134,15 +137,16 @@ void ObjectComponent::WriteConfig() { reflection_scale_r_, reflection_scale_g_, reflection_scale_b_); cmd_buffer_->PutTexture(texture_); - SystemCubeMapTextureID r = + SysCubeMapTextureID r = Graphics::CubeMapFromReflectionType(reflection_); - cmd_buffer_->PutCubeMapTexture(g_assets->GetCubeMapTexture(r)); + cmd_buffer_->PutCubeMapTexture( + g_base->assets->SysCubeMapTexture(r)); } } } else { assert(!double_sided_); // Unsupported combo. assert(!world_space_); // Unsupported config. - if (colorize_texture_.exists()) { + if (colorize_texture_.Exists()) { if (do_colorize_2_) { ConfigForShading( ShadingType::kObjectReflectLightShadowAddColorized2); @@ -155,9 +159,10 @@ void ObjectComponent::WriteConfig() { colorize_color2_b_); cmd_buffer_->PutTexture(texture_); cmd_buffer_->PutTexture(colorize_texture_); - SystemCubeMapTextureID r = + SysCubeMapTextureID r = Graphics::CubeMapFromReflectionType(reflection_); - cmd_buffer_->PutCubeMapTexture(g_assets->GetCubeMapTexture(r)); + cmd_buffer_->PutCubeMapTexture( + g_base->assets->SysCubeMapTexture(r)); } else { ConfigForShading( ShadingType::kObjectReflectLightShadowAddColorized); @@ -169,9 +174,10 @@ void ObjectComponent::WriteConfig() { colorize_color_g_, colorize_color_b_); cmd_buffer_->PutTexture(texture_); cmd_buffer_->PutTexture(colorize_texture_); - SystemCubeMapTextureID r = + SysCubeMapTextureID r = Graphics::CubeMapFromReflectionType(reflection_); - cmd_buffer_->PutCubeMapTexture(g_assets->GetCubeMapTexture(r)); + cmd_buffer_->PutCubeMapTexture( + g_base->assets->SysCubeMapTexture(r)); } } else { ConfigForShading(ShadingType::kObjectReflectLightShadowAdd); @@ -181,13 +187,13 @@ void ObjectComponent::WriteConfig() { reflection_scale_r_, reflection_scale_g_, reflection_scale_b_); cmd_buffer_->PutTexture(texture_); - SystemCubeMapTextureID r = + SysCubeMapTextureID r = Graphics::CubeMapFromReflectionType(reflection_); - cmd_buffer_->PutCubeMapTexture(g_assets->GetCubeMapTexture(r)); + cmd_buffer_->PutCubeMapTexture(g_base->assets->SysCubeMapTexture(r)); } } } } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/component/object_component.h b/src/ballistica/base/graphics/component/object_component.h similarity index 80% rename from src/ballistica/graphics/component/object_component.h rename to src/ballistica/base/graphics/component/object_component.h index bd0a2165..5c1bf11b 100644 --- a/src/ballistica/graphics/component/object_component.h +++ b/src/ballistica/base/graphics/component/object_component.h @@ -1,40 +1,22 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_COMPONENT_OBJECT_COMPONENT_H_ -#define BALLISTICA_GRAPHICS_COMPONENT_OBJECT_COMPONENT_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_COMPONENT_OBJECT_COMPONENT_H_ +#define BALLISTICA_BASE_GRAPHICS_COMPONENT_OBJECT_COMPONENT_H_ -#include "ballistica/graphics/component/render_component.h" +#include "ballistica/base/graphics/component/render_component.h" -namespace ballistica { +namespace ballistica::base { class ObjectComponent : public RenderComponent { public: explicit ObjectComponent(RenderPass* pass) : RenderComponent(pass) {} - void SetTexture(const Object::Ref& t_in) { - EnsureConfiguring(); - if (t_in.exists()) { - texture_ = t_in->texture_data(); - } else { - texture_.Clear(); - } - } - - void SetTexture(TextureData* t) { + void SetTexture(TextureAsset* t) { EnsureConfiguring(); texture_ = t; } - void SetColorizeTexture(const Object::Ref& t_in) { - EnsureConfiguring(); - if (t_in.exists()) { - colorize_texture_ = t_in->texture_data(); - } else { - colorize_texture_.Clear(); - } - } - - void SetColorizeTexture(TextureData* t) { + void SetColorizeTexture(TextureAsset* t) { EnsureConfiguring(); colorize_texture_ = t; } @@ -149,8 +131,8 @@ class ObjectComponent : public RenderComponent { float reflection_scale_r_{1.0f}; float reflection_scale_g_{1.0f}; float reflection_scale_b_{1.0f}; - Object::Ref texture_; - Object::Ref colorize_texture_; + Object::Ref texture_; + Object::Ref colorize_texture_; ReflectionType reflection_{ReflectionType::kNone}; LightShadowType light_shadow_{LightShadowType::kObject}; bool world_space_{}; @@ -161,6 +143,6 @@ class ObjectComponent : public RenderComponent { bool do_colorize_2_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_COMPONENT_OBJECT_COMPONENT_H_ +#endif // BALLISTICA_BASE_GRAPHICS_COMPONENT_OBJECT_COMPONENT_H_ diff --git a/src/ballistica/graphics/component/post_process_component.cc b/src/ballistica/base/graphics/component/post_process_component.cc similarity index 77% rename from src/ballistica/graphics/component/post_process_component.cc rename to src/ballistica/base/graphics/component/post_process_component.cc index c410d9ba..bb92a317 100644 --- a/src/ballistica/graphics/component/post_process_component.cc +++ b/src/ballistica/base/graphics/component/post_process_component.cc @@ -1,8 +1,8 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/component/post_process_component.h" +#include "ballistica/base/graphics/component/post_process_component.h" -namespace ballistica { +namespace ballistica::base { void PostProcessComponent::WriteConfig() { if (eyes_) { @@ -18,4 +18,4 @@ void PostProcessComponent::WriteConfig() { } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/component/post_process_component.h b/src/ballistica/base/graphics/component/post_process_component.h similarity index 59% rename from src/ballistica/graphics/component/post_process_component.h rename to src/ballistica/base/graphics/component/post_process_component.h index 6f47e9b9..39435599 100644 --- a/src/ballistica/graphics/component/post_process_component.h +++ b/src/ballistica/base/graphics/component/post_process_component.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_COMPONENT_POST_PROCESS_COMPONENT_H_ -#define BALLISTICA_GRAPHICS_COMPONENT_POST_PROCESS_COMPONENT_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_COMPONENT_POST_PROCESS_COMPONENT_H_ +#define BALLISTICA_BASE_GRAPHICS_COMPONENT_POST_PROCESS_COMPONENT_H_ -#include "ballistica/graphics/component/render_component.h" +#include "ballistica/base/graphics/component/render_component.h" -namespace ballistica { +namespace ballistica::base { class PostProcessComponent : public RenderComponent { public: @@ -26,6 +26,6 @@ class PostProcessComponent : public RenderComponent { float normal_distort_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_COMPONENT_POST_PROCESS_COMPONENT_H_ +#endif // BALLISTICA_BASE_GRAPHICS_COMPONENT_POST_PROCESS_COMPONENT_H_ diff --git a/src/ballistica/base/graphics/component/render_component.cc b/src/ballistica/base/graphics/component/render_component.cc new file mode 100644 index 00000000..f79a1811 --- /dev/null +++ b/src/ballistica/base/graphics/component/render_component.cc @@ -0,0 +1,39 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/graphics/component/render_component.h" + +#include "ballistica/scene_v1/dynamics/rigid_body.h" + +namespace ballistica::base { + +void RenderComponent::ScissorPush(const Rect& rIn) { + EnsureDrawing(); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kScissorPush); + cmd_buffer_->PutFloats(rIn.l, rIn.b, rIn.r, rIn.t); +} + +#if BA_DEBUG_BUILD +void RenderComponent::ConfigForEmptyDebugChecks(bool transparent) { + assert(g_base->InLogicThread()); + if (g_base->graphics->drawing_opaque_only() && transparent) { + throw Exception("Transparent component submitted in opaque-only section"); + } + if (g_base->graphics->drawing_transparent_only() && !transparent) { + throw Exception("Opaque component submitted in transparent-only section"); + } +} + +void RenderComponent::ConfigForShadingDebugChecks(ShadingType shading_type) { + assert(g_base->InLogicThread()); + if (g_base->graphics->drawing_opaque_only() + && Graphics::IsShaderTransparent(shading_type)) { + throw Exception("Transparent component submitted in opaque-only section"); + } + if (g_base->graphics->drawing_transparent_only() + && !Graphics::IsShaderTransparent(shading_type)) { + throw Exception("Opaque component submitted in transparent-only section"); + } +} +#endif // BA_DEBUG_BUILD + +} // namespace ballistica::base diff --git a/src/ballistica/graphics/component/render_component.h b/src/ballistica/base/graphics/component/render_component.h similarity index 91% rename from src/ballistica/graphics/component/render_component.h rename to src/ballistica/base/graphics/component/render_component.h index a85f9bff..d2a829b5 100644 --- a/src/ballistica/graphics/component/render_component.h +++ b/src/ballistica/base/graphics/component/render_component.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_COMPONENT_RENDER_COMPONENT_H_ -#define BALLISTICA_GRAPHICS_COMPONENT_RENDER_COMPONENT_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_COMPONENT_RENDER_COMPONENT_H_ +#define BALLISTICA_BASE_GRAPHICS_COMPONENT_RENDER_COMPONENT_H_ #include -#include "ballistica/graphics/renderer.h" +#include "ballistica/base/graphics/renderer/renderer.h" -namespace ballistica { +namespace ballistica::base { class RenderComponent { public: @@ -19,20 +19,21 @@ class RenderComponent { "RenderComponent dying without submit() having been called."); } } - void DrawModel(ModelData* model, uint32_t flags = 0) { + void DrawMeshAsset(MeshAsset* mesh, uint32_t flags = 0) { EnsureDrawing(); - cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kDrawModel); + cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kDrawMeshAsset); cmd_buffer_->PutInt(flags); - cmd_buffer_->PutModel(model); + cmd_buffer_->PutMeshAsset(mesh); } - void DrawModelInstanced(ModelData* model, - const std::vector& matrices, - int flags = 0) { + void DrawMeshAssetInstanced(MeshAsset* mesh, + const std::vector& matrices, + int flags = 0) { assert(!matrices.empty()); EnsureDrawing(); - cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kDrawModelInstanced); + cmd_buffer_->PutCommand( + RenderCommandBuffer::Command::kDrawMeshAssetInstanced); cmd_buffer_->PutInt(flags); - cmd_buffer_->PutModel(model); + cmd_buffer_->PutMeshAsset(mesh); cmd_buffer_->PutMatrices(matrices); } void DrawMesh(Mesh* m, int flags = 0) { @@ -120,7 +121,6 @@ class RenderComponent { cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kMultMatrix); cmd_buffer_->PutFloatArray16(t); } - void TransformToBody(const RigidBody& b); #if BA_VR_BUILD void VRTransformToRightHand() { EnsureDrawing(); @@ -258,6 +258,6 @@ class RenderComponent { RenderPass* pass_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_COMPONENT_RENDER_COMPONENT_H_ +#endif // BALLISTICA_BASE_GRAPHICS_COMPONENT_RENDER_COMPONENT_H_ diff --git a/src/ballistica/graphics/component/shield_component.cc b/src/ballistica/base/graphics/component/shield_component.cc similarity index 53% rename from src/ballistica/graphics/component/shield_component.cc rename to src/ballistica/base/graphics/component/shield_component.cc index cbd9e948..2e5bb24e 100644 --- a/src/ballistica/graphics/component/shield_component.cc +++ b/src/ballistica/base/graphics/component/shield_component.cc @@ -1,9 +1,9 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/component/shield_component.h" +#include "ballistica/base/graphics/component/shield_component.h" -namespace ballistica { +namespace ballistica::base { void ShieldComponent::WriteConfig() { ConfigForShading(ShadingType::kShield); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/base/graphics/component/shield_component.h b/src/ballistica/base/graphics/component/shield_component.h new file mode 100644 index 00000000..baaf0c8e --- /dev/null +++ b/src/ballistica/base/graphics/component/shield_component.h @@ -0,0 +1,21 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_GRAPHICS_COMPONENT_SHIELD_COMPONENT_H_ +#define BALLISTICA_BASE_GRAPHICS_COMPONENT_SHIELD_COMPONENT_H_ + +#include "ballistica/base/graphics/component/render_component.h" + +namespace ballistica::base { + +// handles special cases such as drawing light/shadow/back buffers. +class ShieldComponent : public RenderComponent { + public: + explicit ShieldComponent(RenderPass* pass) : RenderComponent(pass) {} + + protected: + void WriteConfig() override; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_GRAPHICS_COMPONENT_SHIELD_COMPONENT_H_ diff --git a/src/ballistica/graphics/component/simple_component.cc b/src/ballistica/base/graphics/component/simple_component.cc similarity index 86% rename from src/ballistica/graphics/component/simple_component.cc rename to src/ballistica/base/graphics/component/simple_component.cc index b6816201..797b4e22 100644 --- a/src/ballistica/graphics/component/simple_component.cc +++ b/src/ballistica/base/graphics/component/simple_component.cc @@ -1,23 +1,23 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/base/graphics/component/simple_component.h" -namespace ballistica { +namespace ballistica::base { void SimpleComponent::WriteConfig() { // if we're transparent we don't want to do optimization-based // shader swapping (ie: when color is 1). This is because it can // affect draw order, which is important unlike with opaque stuff. if (transparent_) { - if (texture_.exists()) { - if (colorize_texture_.exists()) { + if (texture_.Exists()) { + if (colorize_texture_.Exists()) { assert(flatness_ == 0.0f); // unimplemented combo assert(glow_amount_ == 0.0f); // unimplemented combo assert(shadow_opacity_ == 0.0f); // unimplemented combo assert(!double_sided_); // unimplemented combo - assert(!mask_uv2_texture_.exists()); // unimplemented combo + assert(!mask_uv2_texture_.Exists()); // unimplemented combo if (do_colorize_2_) { - if (mask_texture_.exists()) { + if (mask_texture_.Exists()) { ConfigForShading( ShadingType:: kSimpleTextureModulatedTransparentColorized2Masked); @@ -41,7 +41,7 @@ void SimpleComponent::WriteConfig() { cmd_buffer_->PutTexture(colorize_texture_); } } else { - assert(!mask_texture_.exists()); // unimplemented combo + assert(!mask_texture_.Exists()); // unimplemented combo ConfigForShading( ShadingType::kSimpleTextureModulatedTransparentColorized); cmd_buffer_->PutInt(premultiplied_); @@ -54,12 +54,12 @@ void SimpleComponent::WriteConfig() { } else { // non-colorized with texture if (double_sided_) { - assert(!mask_texture_.exists()); // unimplemented combo + assert(!mask_texture_.Exists()); // unimplemented combo assert(flatness_ == 0.0f); // unimplemented combo assert(glow_amount_ == 0.0f); // unimplemented combo assert(shadow_opacity_ == 0.0f); // unimplemented combo - assert(!mask_texture_.exists()); // unimplemented combo - assert(!mask_uv2_texture_.exists()); // unimplemented combo + assert(!mask_texture_.Exists()); // unimplemented combo + assert(!mask_uv2_texture_.Exists()); // unimplemented combo ConfigForShading( ShadingType::kSimpleTextureModulatedTransparentDoubleSided); cmd_buffer_->PutInt(premultiplied_); @@ -67,9 +67,9 @@ void SimpleComponent::WriteConfig() { cmd_buffer_->PutTexture(texture_); } else { if (shadow_opacity_ > 0.0f) { - assert(!mask_texture_.exists()); // unimplemented combo + assert(!mask_texture_.Exists()); // unimplemented combo assert(glow_amount_ == 0.0f); // unimplemented combo - assert(mask_uv2_texture_.exists()); + assert(mask_uv2_texture_.Exists()); if (flatness_ != 0.0f) { ConfigForShading( ShadingType::kSimpleTexModulatedTransShadowFlatness); @@ -91,9 +91,9 @@ void SimpleComponent::WriteConfig() { } } else { if (glow_amount_ > 0.0f) { - assert(!mask_texture_.exists()); // unimplemented combo + assert(!mask_texture_.Exists()); // unimplemented combo assert(flatness_ == 0.0f); // unimplemented combo - if (mask_uv2_texture_.exists()) { + if (mask_uv2_texture_.Exists()) { ConfigForShading( ShadingType::kSimpleTextureModulatedTransparentGlowMaskUV2); cmd_buffer_->PutInt(premultiplied_); @@ -111,7 +111,7 @@ void SimpleComponent::WriteConfig() { } } else { if (flatness_ != 0.0f) { - assert(!mask_texture_.exists()); // unimplemented + assert(!mask_texture_.Exists()); // unimplemented ConfigForShading( ShadingType::kSimpleTextureModulatedTransFlatness); cmd_buffer_->PutInt(premultiplied_); @@ -119,7 +119,7 @@ void SimpleComponent::WriteConfig() { flatness_); cmd_buffer_->PutTexture(texture_); } else { - if (mask_texture_.exists()) { + if (mask_texture_.Exists()) { // currently mask functionality requires colorize too, so // just send a black texture for that.. ConfigForShading( @@ -132,7 +132,7 @@ void SimpleComponent::WriteConfig() { colorize_color2_g_, colorize_color2_b_); cmd_buffer_->PutTexture(texture_); cmd_buffer_->PutTexture( - g_assets->GetTexture(SystemTextureID::kBlack)); + g_base->assets->SysTexture(SysTextureID::kBlack)); cmd_buffer_->PutTexture(mask_texture_); } else { ConfigForShading( @@ -151,9 +151,9 @@ void SimpleComponent::WriteConfig() { assert(flatness_ == 0.0f); // unimplemented combo assert(glow_amount_ == 0.0f); // unimplemented combo assert(shadow_opacity_ == 0.0f); // unimplemented combo - assert(!colorize_texture_.exists()); // unimplemented combo - assert(!mask_texture_.exists()); // unimplemented combo - assert(!mask_uv2_texture_.exists()); // unimplemented combo + assert(!colorize_texture_.Exists()); // unimplemented combo + assert(!mask_texture_.Exists()); // unimplemented combo + assert(!mask_uv2_texture_.Exists()); // unimplemented combo if (double_sided_) { ConfigForShading(ShadingType::kSimpleColorTransparentDoubleSided); cmd_buffer_->PutInt(premultiplied_); @@ -171,10 +171,10 @@ void SimpleComponent::WriteConfig() { assert(glow_amount_ == 0.0f); // unimplemented combo assert(shadow_opacity_ == 0.0f); // unimplemented combo assert(!double_sided_); // not implemented - assert(!mask_uv2_texture_.exists()); // unimplemented combo - if (texture_.exists()) { - if (colorize_texture_.exists()) { - assert(!mask_texture_.exists()); // unimplemented combo + assert(!mask_uv2_texture_.Exists()); // unimplemented combo + if (texture_.Exists()) { + if (colorize_texture_.Exists()) { + assert(!mask_texture_.Exists()); // unimplemented combo if (do_colorize_2_) { ConfigForShading(ShadingType::kSimpleTextureModulatedColorized2); cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, @@ -193,7 +193,7 @@ void SimpleComponent::WriteConfig() { } } else { assert(!do_colorize_2_); // unsupported combo - if (mask_texture_.exists()) { + if (mask_texture_.Exists()) { // currently mask functionality requires colorize too, so // we have to send a black texture along for that.. ConfigForShading( @@ -204,7 +204,7 @@ void SimpleComponent::WriteConfig() { colorize_color2_g_, colorize_color2_b_); cmd_buffer_->PutTexture(texture_); cmd_buffer_->PutTexture( - g_assets->GetTexture(SystemTextureID::kBlack)); + g_base->assets->SysTexture(SysTextureID::kBlack)); cmd_buffer_->PutTexture(mask_texture_); } else { // if no color was provided we can do a super-cheap version @@ -219,11 +219,11 @@ void SimpleComponent::WriteConfig() { } } } else { - assert(!mask_texture_.exists()); // unimplemented combo - assert(!colorize_texture_.exists()); // unsupported here + assert(!mask_texture_.Exists()); // unimplemented combo + assert(!colorize_texture_.Exists()); // unsupported here ConfigForShading(ShadingType::kSimpleColor); cmd_buffer_->PutFloats(color_r_, color_g_, color_b_); } } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/component/simple_component.h b/src/ballistica/base/graphics/component/simple_component.h similarity index 73% rename from src/ballistica/graphics/component/simple_component.h rename to src/ballistica/base/graphics/component/simple_component.h index f6e23fd1..d3c76118 100644 --- a/src/ballistica/graphics/component/simple_component.h +++ b/src/ballistica/base/graphics/component/simple_component.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_COMPONENT_SIMPLE_COMPONENT_H_ -#define BALLISTICA_GRAPHICS_COMPONENT_SIMPLE_COMPONENT_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_COMPONENT_SIMPLE_COMPONENT_H_ +#define BALLISTICA_BASE_GRAPHICS_COMPONENT_SIMPLE_COMPONENT_H_ -#include "ballistica/graphics/component/render_component.h" +#include "ballistica/base/graphics/component/render_component.h" -namespace ballistica { +namespace ballistica::base { // used for UI and overlays and things - no world tinting/etc is applied class SimpleComponent : public RenderComponent { @@ -44,57 +44,29 @@ class SimpleComponent : public RenderComponent { EnsureConfiguring(); transparent_ = val; } - void SetTexture(TextureData* t) { + void SetTexture(TextureAsset* t) { EnsureConfiguring(); texture_ = t; } - void SetTexture(const Object::Ref& t_in) { + void SetTexture(const Object::Ref& t) { EnsureConfiguring(); - if (t_in.exists()) { - texture_ = t_in->texture_data(); - } else { - texture_.Clear(); - } + texture_ = t; } // used with colorize color 1 and 2 // red areas of the texture will get multiplied by colorize-color1 // and green areas by colorize-color2 - void SetColorizeTexture(const Object::Ref& t_in) { - EnsureConfiguring(); - if (t_in.exists()) { - colorize_texture_ = t_in->texture_data(); - } else { - colorize_texture_.Clear(); - } - } - void SetColorizeTexture(TextureData* t) { + void SetColorizeTexture(TextureAsset* t) { EnsureConfiguring(); colorize_texture_ = t; } // red multiplies source color, green adds colorize1-color, // and blue adds white // (currently requires colorize1 and colorize 2 to be set) - void SetMaskTexture(const Object::Ref& t_in) { - EnsureConfiguring(); - if (t_in.exists()) { - mask_texture_ = t_in->texture_data(); - } else { - mask_texture_.Clear(); - } - } - void SetMaskTexture(TextureData* t) { + void SetMaskTexture(TextureAsset* t) { EnsureConfiguring(); mask_texture_ = t; } - void SetMaskUV2Texture(const Object::Ref& t_in) { - EnsureConfiguring(); - if (t_in.exists()) { - mask_uv2_texture_ = t_in->texture_data(); - } else { - mask_uv2_texture_.Clear(); - } - } - void SetMaskUV2Texture(TextureData* t) { + void SetMaskUV2Texture(TextureAsset* t) { EnsureConfiguring(); mask_uv2_texture_ = t; } @@ -169,10 +141,10 @@ class SimpleComponent : public RenderComponent { float shadow_offset_x_, shadow_offset_y_, shadow_blur_, shadow_opacity_; float glow_amount_, glow_blur_; float flatness_; - Object::Ref texture_; - Object::Ref colorize_texture_; - Object::Ref mask_texture_; - Object::Ref mask_uv2_texture_; + Object::Ref texture_; + Object::Ref colorize_texture_; + Object::Ref mask_texture_; + Object::Ref mask_uv2_texture_; bool do_colorize_2_; bool transparent_; bool premultiplied_; @@ -180,6 +152,6 @@ class SimpleComponent : public RenderComponent { bool double_sided_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_COMPONENT_SIMPLE_COMPONENT_H_ +#endif // BALLISTICA_BASE_GRAPHICS_COMPONENT_SIMPLE_COMPONENT_H_ diff --git a/src/ballistica/graphics/component/smoke_component.cc b/src/ballistica/base/graphics/component/smoke_component.cc similarity index 56% rename from src/ballistica/graphics/component/smoke_component.cc rename to src/ballistica/base/graphics/component/smoke_component.cc index 617c9053..eaebe8d1 100644 --- a/src/ballistica/graphics/component/smoke_component.cc +++ b/src/ballistica/base/graphics/component/smoke_component.cc @@ -1,19 +1,19 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/component/smoke_component.h" +#include "ballistica/base/graphics/component/smoke_component.h" -namespace ballistica { +namespace ballistica::base { void SmokeComponent::WriteConfig() { if (overlay_) { ConfigForShading(ShadingType::kSmokeOverlay); cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_); - cmd_buffer_->PutTexture(g_assets->GetTexture(SystemTextureID::kSmoke)); + cmd_buffer_->PutTexture(g_base->assets->SysTexture(SysTextureID::kSmoke)); } else { ConfigForShading(ShadingType::kSmoke); cmd_buffer_->PutFloats(color_r_, color_g_, color_b_, color_a_); - cmd_buffer_->PutTexture(g_assets->GetTexture(SystemTextureID::kSmoke)); + cmd_buffer_->PutTexture(g_base->assets->SysTexture(SysTextureID::kSmoke)); } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/component/smoke_component.h b/src/ballistica/base/graphics/component/smoke_component.h similarity index 68% rename from src/ballistica/graphics/component/smoke_component.h rename to src/ballistica/base/graphics/component/smoke_component.h index 698044b9..45fa1e92 100644 --- a/src/ballistica/graphics/component/smoke_component.h +++ b/src/ballistica/base/graphics/component/smoke_component.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_COMPONENT_SMOKE_COMPONENT_H_ -#define BALLISTICA_GRAPHICS_COMPONENT_SMOKE_COMPONENT_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_COMPONENT_SMOKE_COMPONENT_H_ +#define BALLISTICA_BASE_GRAPHICS_COMPONENT_SMOKE_COMPONENT_H_ -#include "ballistica/graphics/component/render_component.h" +#include "ballistica/base/graphics/component/render_component.h" -namespace ballistica { +namespace ballistica::base { class SmokeComponent : public RenderComponent { public: @@ -34,6 +34,6 @@ class SmokeComponent : public RenderComponent { bool overlay_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_COMPONENT_SMOKE_COMPONENT_H_ +#endif // BALLISTICA_BASE_GRAPHICS_COMPONENT_SMOKE_COMPONENT_H_ diff --git a/src/ballistica/graphics/component/special_component.cc b/src/ballistica/base/graphics/component/special_component.cc similarity index 60% rename from src/ballistica/graphics/component/special_component.cc rename to src/ballistica/base/graphics/component/special_component.cc index c3f79e5d..bf96b456 100644 --- a/src/ballistica/graphics/component/special_component.cc +++ b/src/ballistica/base/graphics/component/special_component.cc @@ -1,12 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/component/special_component.h" +#include "ballistica/base/graphics/component/special_component.h" -namespace ballistica { +namespace ballistica::base { void SpecialComponent::WriteConfig() { ConfigForShading(ShadingType::kSpecial); cmd_buffer_->PutInt(static_cast(source_)); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/component/special_component.h b/src/ballistica/base/graphics/component/special_component.h similarity index 57% rename from src/ballistica/graphics/component/special_component.h rename to src/ballistica/base/graphics/component/special_component.h index ba6ee66b..49875ef8 100644 --- a/src/ballistica/graphics/component/special_component.h +++ b/src/ballistica/base/graphics/component/special_component.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_COMPONENT_SPECIAL_COMPONENT_H_ -#define BALLISTICA_GRAPHICS_COMPONENT_SPECIAL_COMPONENT_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_COMPONENT_SPECIAL_COMPONENT_H_ +#define BALLISTICA_BASE_GRAPHICS_COMPONENT_SPECIAL_COMPONENT_H_ -#include "ballistica/graphics/component/render_component.h" +#include "ballistica/base/graphics/component/render_component.h" -namespace ballistica { +namespace ballistica::base { // handles special cases such as drawing light/shadow/back buffers. class SpecialComponent : public RenderComponent { @@ -21,6 +21,6 @@ class SpecialComponent : public RenderComponent { Source source_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_COMPONENT_SPECIAL_COMPONENT_H_ +#endif // BALLISTICA_BASE_GRAPHICS_COMPONENT_SPECIAL_COMPONENT_H_ diff --git a/src/ballistica/graphics/component/sprite_component.cc b/src/ballistica/base/graphics/component/sprite_component.cc similarity index 72% rename from src/ballistica/graphics/component/sprite_component.cc rename to src/ballistica/base/graphics/component/sprite_component.cc index 32a2ade4..2c9d01fc 100644 --- a/src/ballistica/graphics/component/sprite_component.cc +++ b/src/ballistica/base/graphics/component/sprite_component.cc @@ -1,15 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/component/sprite_component.h" +#include "ballistica/base/graphics/component/sprite_component.h" -namespace ballistica { +namespace ballistica::base { void SpriteComponent::WriteConfig() { // if they didn't give us a texture, just use a blank white texture; // this is not a common case and easier than forking all our shaders // to create non-textured versions. - if (!texture_.exists()) { - texture_ = g_assets->GetTexture(SystemTextureID::kWhite); + if (!texture_.Exists()) { + texture_ = g_base->assets->SysTexture(SysTextureID::kWhite); } if (exponent_ == 1) { ConfigForShading(ShadingType::kSprite); @@ -22,4 +22,4 @@ void SpriteComponent::WriteConfig() { } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/component/sprite_component.h b/src/ballistica/base/graphics/component/sprite_component.h similarity index 63% rename from src/ballistica/graphics/component/sprite_component.h rename to src/ballistica/base/graphics/component/sprite_component.h index 2836034d..01f67e10 100644 --- a/src/ballistica/graphics/component/sprite_component.h +++ b/src/ballistica/base/graphics/component/sprite_component.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_COMPONENT_SPRITE_COMPONENT_H_ -#define BALLISTICA_GRAPHICS_COMPONENT_SPRITE_COMPONENT_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_COMPONENT_SPRITE_COMPONENT_H_ +#define BALLISTICA_BASE_GRAPHICS_COMPONENT_SPRITE_COMPONENT_H_ -#include "ballistica/graphics/component/render_component.h" +#include "ballistica/base/graphics/component/render_component.h" -namespace ballistica { +namespace ballistica::base { class SpriteComponent : public RenderComponent { public: @@ -30,15 +30,7 @@ class SpriteComponent : public RenderComponent { EnsureConfiguring(); exponent_ = static_cast_check_fit(i); } - void SetTexture(const Object::Ref& t_in) { - EnsureConfiguring(); - if (t_in.exists()) { - texture_ = t_in->texture_data(); - } else { - texture_.Clear(); - } - } - void SetTexture(TextureData* t) { + void SetTexture(TextureAsset* t) { EnsureConfiguring(); texture_ = t; } @@ -53,9 +45,9 @@ class SpriteComponent : public RenderComponent { float color_g_{1.0f}; float color_b_{1.0f}; float color_a_{1.0f}; - Object::Ref texture_; + Object::Ref texture_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_COMPONENT_SPRITE_COMPONENT_H_ +#endif // BALLISTICA_BASE_GRAPHICS_COMPONENT_SPRITE_COMPONENT_H_ diff --git a/src/ballistica/graphics/gl/gl_sys.cc b/src/ballistica/base/graphics/gl/gl_sys.cc similarity index 96% rename from src/ballistica/graphics/gl/gl_sys.cc rename to src/ballistica/base/graphics/gl/gl_sys.cc index f804fc39..ce1aed42 100644 --- a/src/ballistica/graphics/gl/gl_sys.cc +++ b/src/ballistica/base/graphics/gl/gl_sys.cc @@ -1,14 +1,16 @@ // Released under the MIT License. See LICENSE for details. #if BA_ENABLE_OPENGL -#include "ballistica/graphics/gl/gl_sys.h" +#include "ballistica/base/graphics/gl/gl_sys.h" -#include "ballistica/platform/sdl/sdl_app.h" +#include "ballistica/base/app/sdl_app.h" +#include "ballistica/base/base.h" +#include "ballistica/core/core.h" #if BA_OSTYPE_ANDROID #include #if !BA_USE_ES3_INCLUDES -#include "ballistica/platform/android/android_gl3.h" +#include "ballistica/core/platform/android/android_gl3.h" #endif #endif @@ -100,7 +102,7 @@ PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog = nullptr; PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog = nullptr; #endif // BA_OSTYPE_WINDOWS -namespace ballistica { +namespace ballistica::base { #pragma clang diagnostic push #pragma ide diagnostic ignored "hicpp-signed-bitwise" @@ -108,12 +110,12 @@ namespace ballistica { GLContext::GLContext(int target_res_x, int target_res_y, bool fullscreen) : fullscreen_(fullscreen) { - assert(InMainThread()); + assert(g_base->InGraphicsThread()); bool need_window = true; #if BA_RIFT_BUILD // on the rift build we don't need a window when running in vr mode; we just // use the context we're created into... - if (IsVRMode()) { + if (g_core->IsVRMode()) { need_window = false; } #endif // BA_RIFT_BUILD @@ -141,7 +143,7 @@ GLContext::GLContext(int target_res_x, int target_res_y, bool fullscreen) if (!sdl_gl_context_) { throw Exception("Unable to create SDL GL Context"); } - SDL_SetWindowTitle(sdl_window_, "BallisticaCore"); + SDL_SetWindowTitle(sdl_window_, "BallisticaKit"); // Our actual drawable size could differ from the window size on retina // devices. @@ -187,7 +189,7 @@ GLContext::GLContext(int target_res_x, int target_res_y, bool fullscreen) res_x_ = surface_->w; res_y_ = surface_->h; SDLApp::get()->SetInitialScreenDimensions(Vector2f(res_x_, res_y_)); - SDL_WM_SetCaption("BallisticaCore", "BallisticaCore"); + SDL_WM_SetCaption("BallisticaKit", "BallisticaKit"); #elif BA_OSTYPE_ANDROID // On Android the Java layer creates a GL setup before even calling us. // So we have nothing to do here. Hooray! @@ -301,7 +303,7 @@ GLContext::GLContext(int target_res_x, int target_res_y, bool fullscreen) #pragma clang diagnostic pop void GLContext::SetVSync(bool enable) { - assert(InMainThread()); + assert(g_base->InGraphicsThread()); #if BA_OSTYPE_MACOS CGLContextObj context = CGLGetCurrentContext(); @@ -314,14 +316,14 @@ void GLContext::SetVSync(bool enable) { } GLContext::~GLContext() { - if (!InMainThread()) { + if (!g_base->InGraphicsThread()) { Log(LogLevel::kError, "GLContext dying in non-graphics thread"); } #if BA_SDL2_BUILD #if BA_RIFT_BUILD // (in rift we only have a window in 2d mode) - if (!IsVRMode()) { + if (!g_core->IsVRMode()) { BA_PRECONDITION_LOG(sdl_window_); } #else // BA_RIFT_MODE @@ -360,6 +362,6 @@ auto GLErrorToString(GLenum err) -> std::string { } } -} // namespace ballistica +} // namespace ballistica::base #endif // BA_ENABLE_OPENGL diff --git a/src/ballistica/graphics/gl/gl_sys.h b/src/ballistica/base/graphics/gl/gl_sys.h similarity index 95% rename from src/ballistica/graphics/gl/gl_sys.h rename to src/ballistica/base/graphics/gl/gl_sys.h index 154e0ce7..968896ab 100644 --- a/src/ballistica/graphics/gl/gl_sys.h +++ b/src/ballistica/base/graphics/gl/gl_sys.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_GL_GL_SYS_H_ -#define BALLISTICA_GRAPHICS_GL_GL_SYS_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_GL_GL_SYS_H_ +#define BALLISTICA_BASE_GRAPHICS_GL_GL_SYS_H_ #if BA_ENABLE_OPENGL @@ -68,8 +68,8 @@ #endif // BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID -#include "ballistica/core/object.h" -#include "ballistica/platform/min_sdl.h" +#include "ballistica/core/platform/support/min_sdl.h" +#include "ballistica/shared/foundation/object.h" #if BA_OSTYPE_ANDROID extern PFNGLDISCARDFRAMEBUFFEREXTPROC _glDiscardFramebufferEXT; @@ -161,7 +161,7 @@ extern PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog; #define GL_POP_GROUP_MARKER() ((void)0) #endif -namespace ballistica { +namespace ballistica::base { auto GLErrorToString(GLenum err) -> std::string; @@ -202,8 +202,8 @@ class GLContext { #endif }; // GLContext -} // namespace ballistica +} // namespace ballistica::base #endif // BA_ENABLE_OPENGL -#endif // BALLISTICA_GRAPHICS_GL_GL_SYS_H_ +#endif // BALLISTICA_BASE_GRAPHICS_GL_GL_SYS_H_ diff --git a/src/ballistica/graphics/gl/renderer_gl.cc b/src/ballistica/base/graphics/gl/renderer_gl.cc similarity index 95% rename from src/ballistica/graphics/gl/renderer_gl.cc rename to src/ballistica/base/graphics/gl/renderer_gl.cc index 1b6f0988..d819efc6 100644 --- a/src/ballistica/graphics/gl/renderer_gl.cc +++ b/src/ballistica/base/graphics/gl/renderer_gl.cc @@ -1,16 +1,18 @@ // Released under the MIT License. See LICENSE for details. #if BA_ENABLE_OPENGL -#include "ballistica/graphics/gl/renderer_gl.h" +#include "ballistica/base/graphics/gl/renderer_gl.h" -#include "ballistica/assets/data/texture_preload_data.h" -#include "ballistica/assets/data/texture_renderer_data.h" -#include "ballistica/graphics/component/special_component.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/mesh/mesh_renderer_data.h" +#include "ballistica/base/assets/texture_asset_preload_data.h" +#include "ballistica/base/assets/texture_asset_renderer_data.h" +#include "ballistica/base/graphics/component/special_component.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/graphics/mesh/mesh_renderer_data.h" +#include "ballistica/base/platform/base_platform.h" +#include "ballistica/core/core.h" #if BA_OSTYPE_IOS_TVOS -#include "ballistica/platform/apple/apple_utils.h" +#include "ballistica/core/platform/apple/apple_utils.h" #endif #define MSAA_ERROR_TEST 0 @@ -19,9 +21,9 @@ #include #include #if !BA_USE_ES3_INCLUDES -#include "ballistica/platform/android/android_gl3.h" +#include "ballistica/core/platform/android/android_gl3.h" #endif -#include "ballistica/ui/ui.h" +#include "ballistica/base/ui/ui.h" #define glDepthRange glDepthRangef #define glDiscardFramebufferEXT _glDiscardFramebufferEXT #ifndef GL_RGB565_OES @@ -108,7 +110,7 @@ void (*glInvalidateFramebuffer)(GLenum target, GLsizei num_attachments, #define BLURSCALE #endif -namespace ballistica { +namespace ballistica::base { // Lots of signed bitwise stuff happening in there; should tidy it up. #pragma clang diagnostic push @@ -144,10 +146,11 @@ static void _check_gl_error(int line) { const char* version = (const char*)glGetString(GL_VERSION); const char* vendor = (const char*)glGetString(GL_VENDOR); const char* renderer = (const char*)glGetString(GL_RENDERER); - Log(LogLevel::kError, "OpenGL Error at line " + std::to_string(line) + ": " - + GLErrorToString(err) + "\nrenderer: " + renderer - + "\nvendor: " + vendor + "\nversion: " + version - + "\ntime: " + std::to_string(GetRealTime())); + Log(LogLevel::kError, + "OpenGL Error at line " + std::to_string(line) + ": " + + GLErrorToString(err) + "\nrenderer: " + renderer + + "\nvendor: " + vendor + "\nversion: " + version + + "\ntime: " + std::to_string(g_core->GetAppTimeMillisecs())); } } @@ -220,7 +223,7 @@ static auto CheckGLExtension(const char* exts, const char* ext) -> bool { void RendererGL::CheckGLExtensions() { DEBUG_CHECK_GL_ERROR; - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); // const char *version_str = (const char*)glGetString(GL_VERSION); const char* ex = (const char*)glGetString(GL_EXTENSIONS); @@ -356,7 +359,7 @@ void RendererGL::CheckGLExtensions() { } // Also store this globally for a few other bits of the app to use.. - g_platform->set_is_tegra_k1(is_tegra_k1_); + g_core->platform->set_is_tegra_k1(is_tegra_k1_); // Extra-speedy implies speedy too.. if (is_extra_speedy_android_device_) { @@ -366,7 +369,7 @@ void RendererGL::CheckGLExtensions() { #endif // BA_OSTYPE_ANDROID std::list c_types; - assert(g_graphics); + assert(g_base->graphics); if (CheckGLExtension(ex, "texture_compression_s3tc")) c_types.push_back(TextureCompressionType::kS3TC); @@ -389,7 +392,7 @@ void RendererGL::CheckGLExtensions() { // eventually get there) if (g_running_es3) c_types.push_back(TextureCompressionType::kETC2); - g_graphics_server->SetTextureCompressionTypes(c_types); + g_base->graphics_server->SetTextureCompressionTypes(c_types); // Check whether we support high-quality mode (requires a few things like // depth textures) For now lets also disallow high-quality in some VR @@ -398,13 +401,13 @@ void RendererGL::CheckGLExtensions() { if (CheckGLExtension(ex, "depth_texture")) { supports_depth_textures_ = true; #if BA_CARDBOARD_BUILD - g_graphics->SetSupportsHighQualityGraphics(false); + g_base->graphics->SetSupportsHighQualityGraphics(false); #else // BA_CARDBOARD_BUILD - g_graphics->SetSupportsHighQualityGraphics(true); + g_base->graphics->SetSupportsHighQualityGraphics(true); #endif // BA_CARDBOARD_BUILD } else { supports_depth_textures_ = false; - g_graphics->SetSupportsHighQualityGraphics(false); + g_base->graphics->SetSupportsHighQualityGraphics(false); } // Store the tex-compression type we support. @@ -722,7 +725,7 @@ class RendererGL::FramebufferObjectGL : public Framebuffer { void Load(bool force_low_quality = false) { if (loaded_) return; - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); DEBUG_CHECK_GL_ERROR; GLenum status; DEBUG_CHECK_GL_ERROR; @@ -919,7 +922,7 @@ class RendererGL::FramebufferObjectGL : public Framebuffer { } void Unload() { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); if (!loaded_) return; // If our textures are currently bound as anything, clear that out. @@ -932,7 +935,7 @@ class RendererGL::FramebufferObjectGL : public Framebuffer { } } - if (!g_graphics_server->renderer_context_lost()) { + if (!g_base->graphics_server->renderer_context_lost()) { // Tear down the FBO and texture attachment if (is_texture_) { glDeleteTextures(1, &texture_); @@ -961,7 +964,7 @@ class RendererGL::FramebufferObjectGL : public Framebuffer { } void Bind() { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); renderer_->BindFramebuffer(framebuffer_); // if (time(nullptr)%2 == 0) { // glDisable(GL_FRAMEBUFFER_SRGB); @@ -1000,12 +1003,12 @@ class RendererGL::FramebufferObjectGL : public Framebuffer { // Base class for fragment/vertex shaders. class RendererGL::ShaderGL : public Object { public: - auto GetDefaultOwnerThread() const -> ThreadTag override { - return ThreadTag::kMain; + auto GetDefaultOwnerThread() const -> EventLoopID override { + return EventLoopID::kMain; } ShaderGL(GLenum type_in, const std::string& src_in) : type_(type_in) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); DEBUG_CHECK_GL_ERROR; assert(type_ == GL_FRAGMENT_SHADER || type_ == GL_VERTEX_SHADER); shader_ = glCreateShader(type_); @@ -1049,8 +1052,8 @@ class RendererGL::ShaderGL : public Object { DEBUG_CHECK_GL_ERROR; } ~ShaderGL() override { - assert(InGraphicsThread()); - if (!g_graphics_server->renderer_context_lost()) { + assert(g_base->InGraphicsThread()); + if (!g_base->graphics_server->renderer_context_lost()) { glDeleteShader(shader_); DEBUG_CHECK_GL_ERROR; } @@ -1106,7 +1109,7 @@ class RendererGL::ProgramGL { renderer_(renderer), pflags_(pflags), name_(std::move(name)) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); DEBUG_CHECK_GL_ERROR; program_ = glCreateProgram(); BA_PRECONDITION(program_); @@ -1180,8 +1183,8 @@ class RendererGL::ProgramGL { } virtual ~ProgramGL() { - assert(InGraphicsThread()); - if (!g_graphics_server->renderer_context_lost()) { + assert(g_base->InGraphicsThread()); + if (!g_base->graphics_server->renderer_context_lost()) { glDetachShader(program_, fragment_shader_->shader()); glDetachShader(program_, vertex_shader_->shader()); glDeleteProgram(program_); @@ -1207,11 +1210,13 @@ class RendererGL::ProgramGL { // update matrices as necessary... - uint32_t mvpState = g_graphics_server->GetModelViewProjectionMatrixState(); + uint32_t mvpState = + g_base->graphics_server->GetModelViewProjectionMatrixState(); if (mvpState != mvp_state_) { mvp_state_ = mvpState; - glUniformMatrix4fv(mvp_uniform_, 1, 0, - g_graphics_server->GetModelViewProjectionMatrix().m); + glUniformMatrix4fv( + mvp_uniform_, 1, 0, + g_base->graphics_server->GetModelViewProjectionMatrix().m); } DEBUG_CHECK_GL_ERROR; @@ -1219,11 +1224,11 @@ class RendererGL::ProgramGL { assert(!(pflags_ & PFLAG_WORLD_SPACE_PTS)); // with world space points this would // be identity; don't waste time. - uint32_t state = g_graphics_server->GetModelWorldMatrixState(); + uint32_t state = g_base->graphics_server->GetModelWorldMatrixState(); if (state != model_world_matrix_state_) { model_world_matrix_state_ = state; glUniformMatrix4fv(model_world_matrix_uniform_, 1, 0, - g_graphics_server->GetModelWorldMatrix().m); + g_base->graphics_server->GetModelWorldMatrix().m); } } DEBUG_CHECK_GL_ERROR; @@ -1233,43 +1238,44 @@ class RendererGL::ProgramGL { & PFLAG_WORLD_SPACE_PTS)); // with world space points this would // be identity; don't waste time. // there's no state for just modelview but this works - uint32_t state = g_graphics_server->GetModelViewProjectionMatrixState(); + uint32_t state = + g_base->graphics_server->GetModelViewProjectionMatrixState(); if (state != model_view_matrix_state_) { model_view_matrix_state_ = state; glUniformMatrix4fv(model_view_matrix_uniform_, 1, 0, - g_graphics_server->model_view_matrix().m); + g_base->graphics_server->model_view_matrix().m); } } DEBUG_CHECK_GL_ERROR; if (pflags_ & PFLAG_USES_CAM_POS) { - uint32_t state = g_graphics_server->cam_pos_state(); + uint32_t state = g_base->graphics_server->cam_pos_state(); if (state != cam_pos_state_) { cam_pos_state_ = state; - const Vector3f& p(g_graphics_server->cam_pos()); + const Vector3f& p(g_base->graphics_server->cam_pos()); glUniform4f(cam_pos_uniform_, p.x, p.y, p.z, 1.0f); } } DEBUG_CHECK_GL_ERROR; if (pflags_ & PFLAG_USES_CAM_ORIENT_MATRIX) { - uint32_t state = g_graphics_server->GetCamOrientMatrixState(); + uint32_t state = g_base->graphics_server->GetCamOrientMatrixState(); if (state != cam_orient_matrix_state_) { cam_orient_matrix_state_ = state; glUniformMatrix4fv(cam_orient_matrix_uniform_, 1, 0, - g_graphics_server->GetCamOrientMatrix().m); + g_base->graphics_server->GetCamOrientMatrix().m); } } DEBUG_CHECK_GL_ERROR; if (pflags_ & PFLAG_USES_SHADOW_PROJECTION_MATRIX) { uint32_t state = - g_graphics_server->light_shadow_projection_matrix_state(); + g_base->graphics_server->light_shadow_projection_matrix_state(); if (state != light_shadow_projection_matrix_state_) { light_shadow_projection_matrix_state_ = state; glUniformMatrix4fv( light_shadow_projection_matrix_uniform_, 1, 0, - g_graphics_server->light_shadow_projection_matrix().m); + g_base->graphics_server->light_shadow_projection_matrix().m); } } DEBUG_CHECK_GL_ERROR; @@ -1375,7 +1381,7 @@ class RendererGL::SimpleProgramGL : public RendererGL::ProgramGL { SetTextureUnit("maskUV2Tex", kMaskUV2TexUnit); } } - void SetColorTexture(const TextureData* t) { + void SetColorTexture(const TextureAsset* t) { assert(flags_ & SHD_TEXTURE); assert(IsBound()); renderer()->BindTexture(GL_TEXTURE_2D, t, kColorTexUnit); @@ -1452,15 +1458,15 @@ class RendererGL::SimpleProgramGL : public RendererGL::ProgramGL { colorize2_b_, colorize2_a_); } } - void SetColorizeTexture(const TextureData* t) { + void SetColorizeTexture(const TextureAsset* t) { assert(flags_ & SHD_COLORIZE); renderer()->BindTexture(GL_TEXTURE_2D, t, kColorizeTexUnit); } - void SetMaskTexture(const TextureData* t) { + void SetMaskTexture(const TextureAsset* t) { assert(flags_ & SHD_MASKED); renderer()->BindTexture(GL_TEXTURE_2D, t, kMaskTexUnit); } - void SetMaskUV2Texture(const TextureData* t) { + void SetMaskUV2Texture(const TextureAsset* t) { assert(flags_ & SHD_MASK_UV2); renderer()->BindTexture(GL_TEXTURE_2D, t, kMaskUV2TexUnit); } @@ -1701,10 +1707,10 @@ class RendererGL::ObjectProgramGL : public RendererGL::ProgramGL { assert(colorize2_color_location_ != -1); } } - void SetColorTexture(const TextureData* t) { + void SetColorTexture(const TextureAsset* t) { renderer()->BindTexture(GL_TEXTURE_2D, t, kColorTexUnit); } - void SetReflectionTexture(const TextureData* t) { + void SetReflectionTexture(const TextureAsset* t) { assert(flags_ & SHD_REFLECTION); renderer()->BindTexture(GL_TEXTURE_CUBE_MAP, t, kReflectionTexUnit); } @@ -1780,7 +1786,7 @@ class RendererGL::ObjectProgramGL : public RendererGL::ProgramGL { colorize2_b_, colorize2_a_); } } - void SetColorizeTexture(const TextureData* t) { + void SetColorizeTexture(const TextureAsset* t) { assert(flags_ & SHD_COLORIZE); renderer()->BindTexture(GL_TEXTURE_2D, t, kColorizeTexUnit); } @@ -1964,7 +1970,7 @@ class RendererGL::SmokeProgramGL : public RendererGL::ProgramGL { color_location_ = glGetUniformLocation(program(), "colorMult"); assert(color_location_ != -1); } - void SetColorTexture(const TextureData* t) { + void SetColorTexture(const TextureAsset* t) { renderer()->BindTexture(GL_TEXTURE_2D, t, kColorTexUnit); } void SetDepthTexture(GLuint t) { @@ -2122,7 +2128,7 @@ class RendererGL::BlurProgramGL : public RendererGL::ProgramGL { } } - void SetColorTexture(const TextureData* t) { + void SetColorTexture(const TextureAsset* t) { renderer()->BindTexture(GL_TEXTURE_2D, t, kColorTexUnit); } void SetColorTexture(GLuint t) { @@ -2695,7 +2701,7 @@ class RendererGL::SpriteProgramGL : public RendererGL::ProgramGL { } DEBUG_CHECK_GL_ERROR; } - void SetColorTexture(const TextureData* t) { + void SetColorTexture(const TextureAsset* t) { renderer()->BindTexture(GL_TEXTURE_2D, t, kColorTexUnit); } void SetDepthTexture(GLuint t) { @@ -2816,18 +2822,18 @@ class RendererGL::SpriteProgramGL : public RendererGL::ProgramGL { int flags_; }; -class RendererGL::TextureDataGL : public TextureRendererData { +class RendererGL::TextureDataGL : public TextureAssetRendererData { public: - TextureDataGL(const TextureData& texture_in, RendererGL* renderer_in) + TextureDataGL(const TextureAsset& texture_in, RendererGL* renderer_in) : tex_media_(&texture_in), texture_(0), renderer_(renderer_in) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); DEBUG_CHECK_GL_ERROR; glGenTextures(1, &texture_); DEBUG_CHECK_GL_ERROR; } ~TextureDataGL() override { - if (!InGraphicsThread()) { + if (!g_base->InGraphicsThread()) { Log(LogLevel::kError, "TextureDataGL dying outside of graphics thread."); } else { // if we're currently bound as anything, clear that out @@ -2840,7 +2846,7 @@ class RendererGL::TextureDataGL : public TextureRendererData { renderer_->bound_textures_cube_map_[i] = -1; } } - if (!g_graphics_server->renderer_context_lost()) { + if (!g_base->graphics_server->renderer_context_lost()) { glDeleteTextures(1, &texture_); DEBUG_CHECK_GL_ERROR; } @@ -2850,15 +2856,16 @@ class RendererGL::TextureDataGL : public TextureRendererData { auto GetTexture() const -> GLuint { return texture_; } void Load() override { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); DEBUG_CHECK_GL_ERROR; if (tex_media_->texture_type() == TextureType::k2D) { renderer_->BindTexture(GL_TEXTURE_2D, texture_); - const TexturePreloadData* preload_data = &tex_media_->preload_datas()[0]; + const TextureAssetPreloadData* preload_data = + &tex_media_->preload_datas()[0]; int base_src_level = preload_data->base_level; assert(preload_data->buffers[base_src_level]); - GraphicsQuality q = g_graphics_server->quality(); + GraphicsQuality q = g_base->graphics_server->quality(); // Determine whether to use anisotropic sampling on this texture: // basically all the UI stuff that is only ever seen from straight on @@ -3031,12 +3038,12 @@ class RendererGL::TextureDataGL : public TextureRendererData { bool do_generate_mips = false; for (uint32_t i = 0; i < 6; i++) { - const TexturePreloadData* preload_data = + const TextureAssetPreloadData* preload_data = &tex_media_->preload_datas()[i]; int base_src_level = preload_data->base_level; assert(preload_data->buffers[base_src_level]); - GraphicsQuality q = g_graphics_server->quality(); + GraphicsQuality q = g_base->graphics_server->quality(); // do trilinear in higher quality; otherwise bilinear is good enough.. if (q >= GraphicsQuality::kHigher) { @@ -3119,7 +3126,7 @@ class RendererGL::TextureDataGL : public TextureRendererData { } private: - const TextureData* tex_media_; + const TextureAsset* tex_media_; RendererGL* renderer_; GLuint texture_; }; // TextureDataGL @@ -3170,7 +3177,7 @@ void RendererGL::BindArrayBuffer(GLuint b) { } } -void RendererGL::BindTexture(GLuint type, const TextureData* t, +void RendererGL::BindTexture(GLuint type, const TextureAsset* t, GLuint tex_unit) { if (t) { auto data = static_cast_check_type(t->renderer_data()); @@ -3204,17 +3211,17 @@ void RendererGL::BindTexture(GLuint type, GLuint tex, GLuint tex_unit) { } } -class RendererGL::ModelDataGL : public ModelRendererData { +class RendererGL::MeshAssetDataGL : public MeshAssetRendererData { public: enum BufferType { kVertices, kIndices, kBufferCount }; - ModelDataGL(const ModelData& model, RendererGL* renderer) + MeshAssetDataGL(const MeshAsset& model, RendererGL* renderer) : renderer_(renderer), fake_vao_(nullptr) { #if BA_DEBUG_BUILD name_ = model.GetName(); #endif // BA_DEBUG_BUILD - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); DEBUG_CHECK_GL_ERROR; // Create our vertex array to hold all this state (if supported). @@ -3308,10 +3315,10 @@ class RendererGL::ModelDataGL : public ModelRendererData { index_data, GL_STATIC_DRAW); DEBUG_CHECK_GL_ERROR; - } // ModelDataGL + } // MeshAssetDataGL - ~ModelDataGL() override { - assert(InGraphicsThread()); + ~MeshAssetDataGL() override { + assert(g_base->InGraphicsThread()); DEBUG_CHECK_GL_ERROR; // Unbind if we're bound; otherwise if a new vao pops up with our same ID @@ -3320,7 +3327,7 @@ class RendererGL::ModelDataGL : public ModelRendererData { if (vao_ == renderer_->current_vertex_array_) { renderer_->BindVertexArray(0); } - if (!g_graphics_server->renderer_context_lost()) { + if (!g_base->graphics_server->renderer_context_lost()) { glDeleteVertexArrays(1, &vao_); } } else { @@ -3335,7 +3342,7 @@ class RendererGL::ModelDataGL : public ModelRendererData { renderer_->active_array_buffer_ = -1; } } - if (!g_graphics_server->renderer_context_lost()) { + if (!g_base->graphics_server->renderer_context_lost()) { glDeleteBuffers(kBufferCount, vbos_); DEBUG_CHECK_GL_ERROR; } @@ -3374,7 +3381,7 @@ class RendererGL::ModelDataGL : public ModelRendererData { GLuint vao_{}; GLuint vbos_[kBufferCount]{}; FakeVertexArrayObject* fake_vao_{}; -}; // ModelDataGL +}; // MeshAssetDataGL class RendererGL::MeshDataGL : public MeshRendererData { public: @@ -3392,7 +3399,7 @@ class RendererGL::MeshDataGL : public MeshRendererData { : renderer_(renderer), uses_secondary_data_(static_cast(flags & kUsesSecondaryBuffer)), uses_index_data_(static_cast(flags & kUsesIndexBuffer)) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); // Create our vertex array to hold all this state. if (g_vao_support) { @@ -3447,14 +3454,14 @@ class RendererGL::MeshDataGL : public MeshRendererData { } ~MeshDataGL() override { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); // unbind if we're bound .. otherwise we might prevent a new with our ID // from binding if (g_vao_support) { if (vao_ == renderer_->current_vertex_array_) { renderer_->BindVertexArray(0); } - if (!g_graphics_server->renderer_context_lost()) { + if (!g_base->graphics_server->renderer_context_lost()) { glDeleteVertexArrays(1, &vao_); } } else { @@ -3469,7 +3476,7 @@ class RendererGL::MeshDataGL : public MeshRendererData { renderer_->active_array_buffer_ = -1; } } - if (!g_graphics_server->renderer_context_lost()) { + if (!g_base->graphics_server->renderer_context_lost()) { glDeleteBuffers(GetBufferCount(), vbos_); DEBUG_CHECK_GL_ERROR; } @@ -3864,7 +3871,7 @@ class RendererGL::RenderTargetGL : public RenderTarget { public: void Bind() { if (type_ == Type::kFramebuffer) { - assert(framebuffer_.exists()); + assert(framebuffer_.Exists()); framebuffer_->Bind(); } else { assert(type_ == Type::kScreen); @@ -3874,7 +3881,7 @@ class RendererGL::RenderTargetGL : public RenderTarget { void DrawBegin(bool must_clear_color, float clear_r, float clear_g, float clear_b, float clear_a) override { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); DEBUG_CHECK_GL_ERROR; Bind(); @@ -3930,7 +3937,7 @@ class RendererGL::RenderTargetGL : public RenderTarget { auto GetFramebufferID() -> GLuint { if (type_ == Type::kFramebuffer) { - assert(framebuffer_.exists()); + assert(framebuffer_.Exists()); return framebuffer_->id(); } else { return 0; // screen @@ -3938,12 +3945,12 @@ class RendererGL::RenderTargetGL : public RenderTarget { } auto framebuffer() -> FramebufferObjectGL* { assert(type_ == Type::kFramebuffer); - return framebuffer_.get(); + return framebuffer_.Get(); } // Screen. explicit RenderTargetGL(RendererGL* renderer) : RenderTarget(Type::kScreen), renderer_(renderer) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); depth_ = true; // This will update our width/height values. @@ -3955,7 +3962,7 @@ class RendererGL::RenderTargetGL : public RenderTarget { bool linear_interp, bool depth, bool texture, bool depth_texture, bool high_quality, bool msaa, bool alpha) : RenderTarget(Type::kFramebuffer), renderer_(renderer) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); DEBUG_CHECK_GL_ERROR; framebuffer_ = Object::New( renderer, width, height, linear_interp, depth, texture, depth_texture, @@ -3985,7 +3992,7 @@ RendererGL::RendererGL() { glGetError(); #endif // BA_OSTYPE_MACOS - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); DEBUG_CHECK_GL_ERROR; SyncGLState(); @@ -4035,8 +4042,8 @@ void RendererGL::CheckFunkyDepthIssue() { SimpleProgramGL* p = simple_color_prog_; p->Bind(); p->SetColor(1, 0, 1); - g_graphics_server->ModelViewReset(); - g_graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1); + g_base->graphics_server->ModelViewReset(); + g_base->graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1); GetActiveProgram()->PrepareToDraw(); screen_mesh_->Bind(); screen_mesh_->Draw(DrawType::kTriangles); @@ -4051,8 +4058,8 @@ void RendererGL::CheckFunkyDepthIssue() { test_rt2->DrawBegin(false, 1.0f, 1.0f, 1.0f, 1.0f); p = simple_tex_dtest_prog_; p->Bind(); - g_graphics_server->ModelViewReset(); - g_graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1); + g_base->graphics_server->ModelViewReset(); + g_base->graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1); p->SetColorTexture(test_rt1->framebuffer()->depth_texture()); GetActiveProgram()->PrepareToDraw(); screen_mesh_->Bind(); @@ -4125,7 +4132,7 @@ void RendererGL::InvalidateFramebuffer(bool color, bool depth, } RendererGL::~RendererGL() { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); printf("FIXME: need to unload renderer on destroy.\n"); // Unload(); DEBUG_CHECK_GL_ERROR; @@ -4140,7 +4147,7 @@ void RendererGL::UseProgram(ProgramGL* p) { void RendererGL::SyncGLState() { #if BA_RIFT_BUILD - if (IsVRMode()) { + if (g_core->IsVRMode()) { glFrontFace(GL_CCW); } @@ -4184,7 +4191,7 @@ void RendererGL::SyncGLState() { // texture, and in that case we need alpha to accumulate; not get overwritten. // could probably enable this everywhere but I don't know if it's supported on // all hardware or slower.. - if (IsVRMode()) { + if (g_core->IsVRMode()) { #if BA_OSTYPE_WINDOWS if (glBlendFuncSeparate == nullptr) { throw Exception( @@ -4225,21 +4232,21 @@ void RendererGL::SyncGLState() { assert(*index_size == 4 || *index_size == 2); \ bool use_indices32 = (*index_size == 4); \ if (use_indices32) { \ - indices32 = static_cast(buffer->get()); \ + indices32 = static_cast(buffer->Get()); \ assert(indices32&& indices32 \ - == dynamic_cast(buffer->get())); \ + == dynamic_cast(buffer->Get())); \ } else { \ - indices16 = static_cast(buffer->get()); \ + indices16 = static_cast(buffer->Get()); \ assert(indices16&& indices16 \ - == dynamic_cast(buffer->get())); \ + == dynamic_cast(buffer->Get())); \ } \ index_size++; \ buffer++ #define GET_BUFFER(TYPE, VAR) \ assert(buffer != buffers.end()); \ - auto* VAR = static_cast(buffer->get()); \ - assert(VAR&& VAR == dynamic_cast(buffer->get())); \ + auto* VAR = static_cast(buffer->Get()); \ + assert(VAR&& VAR == dynamic_cast(buffer->Get())); \ buffer++ // Takes all latest mesh data from the client side and applies it @@ -4482,8 +4489,8 @@ void RendererGL::ProcessRenderCommandBuffer(RenderCommandBuffer* buffer, SimpleProgramGL* p = simple_tex_mod_shadow_prog_; p->Bind(); p->SetColor(r, g, b, a); - const TextureData* t = buffer->GetTexture(); - const TextureData* t_mask = buffer->GetTexture(); + const TextureAsset* t = buffer->GetTexture(); + const TextureAsset* t_mask = buffer->GetTexture(); p->SetColorTexture(t); // If this isn't a full-res texture, ramp down the blurring we do. p->SetShadow(shadow_offset_x, shadow_offset_y, @@ -4504,8 +4511,8 @@ void RendererGL::ProcessRenderCommandBuffer(RenderCommandBuffer* buffer, SimpleProgramGL* p = simple_tex_mod_shadow_flatness_prog_; p->Bind(); p->SetColor(r, g, b, a); - const TextureData* t = buffer->GetTexture(); - const TextureData* t_mask = buffer->GetTexture(); + const TextureAsset* t = buffer->GetTexture(); + const TextureAsset* t_mask = buffer->GetTexture(); p->SetColorTexture(t); // If this isn't a full-res texture, ramp down the blurring we do. p->SetShadow(shadow_offset_x, shadow_offset_y, @@ -4524,7 +4531,7 @@ void RendererGL::ProcessRenderCommandBuffer(RenderCommandBuffer* buffer, SimpleProgramGL* p = simple_tex_mod_glow_prog_; p->Bind(); p->SetColor(r, g, b, a); - const TextureData* t = buffer->GetTexture(); + const TextureAsset* t = buffer->GetTexture(); p->SetColorTexture(t); // Glow. @@ -4541,9 +4548,9 @@ void RendererGL::ProcessRenderCommandBuffer(RenderCommandBuffer* buffer, SimpleProgramGL* p = simple_tex_mod_glow_maskuv2_prog_; p->Bind(); p->SetColor(r, g, b, a); - const TextureData* t = buffer->GetTexture(); + const TextureAsset* t = buffer->GetTexture(); p->SetColorTexture(t); - const TextureData* t_mask = buffer->GetTexture(); + const TextureAsset* t_mask = buffer->GetTexture(); p->SetMaskUV2Texture(t_mask); // Glow. p->setGlow(glow_amount, std::max(0.0f, glow_blur)); @@ -5277,42 +5284,44 @@ void RendererGL::ProcessRenderCommandBuffer(RenderCommandBuffer* buffer, p->SetAddColor(r, g, b); break; } - case RenderCommandBuffer::Command::kDrawModel: { + case RenderCommandBuffer::Command::kDrawMeshAsset: { int flags = buffer->GetInt(); - const ModelData* m = buffer->GetModel(); + const MeshAsset* m = buffer->GetMesh(); assert(m); - auto model = static_cast_check_type(m->renderer_data()); - assert(model); + auto mesh = + static_cast_check_type(m->renderer_data()); + assert(mesh); // if they don't wanna draw in reflections... - if ((flags & kModelDrawFlagNoReflection) && drawing_reflection()) { + if ((flags & kMeshDrawFlagNoReflection) && drawing_reflection()) { break; } GetActiveProgram()->PrepareToDraw(); - model->Bind(); - model->Draw(); + mesh->Bind(); + mesh->Draw(); break; } - case RenderCommandBuffer::Command::kDrawModelInstanced: { + case RenderCommandBuffer::Command::kDrawMeshAssetInstanced: { int flags = buffer->GetInt(); - const ModelData* m = buffer->GetModel(); + const MeshAsset* m = buffer->GetMesh(); assert(m); - auto model = static_cast_check_type(m->renderer_data()); - assert(model); + auto mesh = + static_cast_check_type(m->renderer_data()); + assert(mesh); Matrix44f* mats; int count; mats = buffer->GetMatrices(&count); // if they don't wanna draw in reflections... - if ((flags & kModelDrawFlagNoReflection) && drawing_reflection()) { + if ((flags & kMeshDrawFlagNoReflection) && drawing_reflection()) { break; } - model->Bind(); + mesh->Bind(); for (int i = 0; i < count; i++) { - g_graphics_server->PushTransform(); - g_graphics_server->MultMatrix(mats[i]); + g_base->graphics_server->PushTransform(); + g_base->graphics_server->MultMatrix(mats[i]); GetActiveProgram()->PrepareToDraw(); - model->Draw(); - g_graphics_server->PopTransform(); + mesh->Draw(); + g_base->graphics_server->PopTransform(); } break; } @@ -5349,7 +5358,7 @@ void RendererGL::ProcessRenderCommandBuffer(RenderCommandBuffer* buffer, int flags = buffer->GetInt(); auto* mesh = buffer->GetMeshRendererData(); assert(mesh); - if ((flags & kModelDrawFlagNoReflection) && drawing_reflection()) { + if ((flags & kMeshDrawFlagNoReflection) && drawing_reflection()) { break; } GetActiveProgram()->PrepareToDraw(); @@ -5361,16 +5370,16 @@ void RendererGL::ProcessRenderCommandBuffer(RenderCommandBuffer* buffer, // Save proj/mv matrices, set up to draw a simple screen quad at the // back of our depth range, draw, and restore Matrix44f old_model_view_matrix = - g_graphics_server->model_view_matrix(); + g_base->graphics_server->model_view_matrix(); Matrix44f old_projection_matrix = - g_graphics_server->projection_matrix(); - g_graphics_server->SetModelViewMatrix(kMatrix44fIdentity); - g_graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 0.01f); + g_base->graphics_server->projection_matrix(); + g_base->graphics_server->SetModelViewMatrix(kMatrix44fIdentity); + g_base->graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 0.01f); GetActiveProgram()->PrepareToDraw(); screen_mesh_->Bind(); screen_mesh_->Draw(DrawType::kTriangles); - g_graphics_server->SetModelViewMatrix(old_model_view_matrix); - g_graphics_server->SetProjectionMatrix(old_projection_matrix); + g_base->graphics_server->SetModelViewMatrix(old_model_view_matrix); + g_base->graphics_server->SetProjectionMatrix(old_projection_matrix); break; } case RenderCommandBuffer::Command::kScissorPush: { @@ -5379,10 +5388,10 @@ void RendererGL::ProcessRenderCommandBuffer(RenderCommandBuffer* buffer, // Convert scissor-values from model space to view space. // this of course assumes there's no rotations and whatnot.. - Vector3f bot_left_pt = - g_graphics_server->model_view_matrix() * Vector3f(r.l, r.b, 0); - Vector3f top_right_pt = - g_graphics_server->model_view_matrix() * Vector3f(r.r, r.t, 0); + Vector3f bot_left_pt = g_base->graphics_server->model_view_matrix() + * Vector3f(r.l, r.b, 0); + Vector3f top_right_pt = g_base->graphics_server->model_view_matrix() + * Vector3f(r.r, r.t, 0); r.l = bot_left_pt.x; r.b = bot_left_pt.y; r.r = top_right_pt.x; @@ -5395,42 +5404,42 @@ void RendererGL::ProcessRenderCommandBuffer(RenderCommandBuffer* buffer, break; } case RenderCommandBuffer::Command::kPushTransform: { - g_graphics_server->PushTransform(); + g_base->graphics_server->PushTransform(); break; } case RenderCommandBuffer::Command::kTranslate2: { float x, y; buffer->GetFloats(&x, &y); - g_graphics_server->Translate(Vector3f(x, y, 0)); + g_base->graphics_server->Translate(Vector3f(x, y, 0)); break; } case RenderCommandBuffer::Command::kTranslate3: { float x, y, z; buffer->GetFloats(&x, &y, &z); - g_graphics_server->Translate(Vector3f(x, y, z)); + g_base->graphics_server->Translate(Vector3f(x, y, z)); break; } case RenderCommandBuffer::Command::kCursorTranslate: { float x, y; - g_platform->GetCursorPosition(&x, &y); - g_graphics_server->Translate(Vector3f(x, y, 0)); + g_base->platform->GetCursorPosition(&x, &y); + g_base->graphics_server->Translate(Vector3f(x, y, 0)); break; } case RenderCommandBuffer::Command::kScale2: { float x, y; buffer->GetFloats(&x, &y); - g_graphics_server->scale(Vector3f(x, y, 1.0f)); + g_base->graphics_server->scale(Vector3f(x, y, 1.0f)); break; } case RenderCommandBuffer::Command::kScale3: { float x, y, z; buffer->GetFloats(&x, &y, &z); - g_graphics_server->scale(Vector3f(x, y, z)); + g_base->graphics_server->scale(Vector3f(x, y, z)); break; } case RenderCommandBuffer::Command::kScaleUniform: { float s = buffer->GetFloat(); - g_graphics_server->scale(Vector3f(s, s, s)); + g_base->graphics_server->scale(Vector3f(s, s, s)); break; } #if BA_VR_BUILD @@ -5452,23 +5461,23 @@ void RendererGL::ProcessRenderCommandBuffer(RenderCommandBuffer* buffer, buffer->GetFloats(&x, &y, &z); Vector3f t = pass.frame_def()->beauty_pass()->tex_project_matrix() * Vector3f(x, y, z); - g_graphics_server->Translate( - Vector3f(t.x * g_graphics_server->screen_virtual_width(), - t.y * g_graphics_server->screen_virtual_height(), 0)); + g_base->graphics_server->Translate(Vector3f( + t.x * g_base->graphics_server->screen_virtual_width(), + t.y * g_base->graphics_server->screen_virtual_height(), 0)); break; } case RenderCommandBuffer::Command::kRotate: { float angle, x, y, z; buffer->GetFloats(&angle, &x, &y, &z); - g_graphics_server->Rotate(angle, Vector3f(x, y, z)); + g_base->graphics_server->Rotate(angle, Vector3f(x, y, z)); break; } case RenderCommandBuffer::Command::kMultMatrix: { - g_graphics_server->MultMatrix(*(buffer->GetMatrix())); + g_base->graphics_server->MultMatrix(*(buffer->GetMatrix())); break; } case RenderCommandBuffer::Command::kPopTransform: { - g_graphics_server->PopTransform(); + g_base->graphics_server->PopTransform(); break; } case RenderCommandBuffer::Command::kFlipCullFace: { @@ -5530,8 +5539,8 @@ void RendererGL::BlitBuffer(RenderTarget* src_in, RenderTarget* dst_in, SetDepthWriting(false); SetDepthTesting(false); dst_in->DrawBegin(false); - g_graphics_server->ModelViewReset(); - g_graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1); + g_base->graphics_server->ModelViewReset(); + g_base->graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1); // Copied from ShadingType::kSimpleColor SetDoubleSided(false); @@ -5673,7 +5682,7 @@ void RendererGL::SetBlendPremult(bool b) { // texture, and in that case we need alpha to accumulate; not get // overwritten. could probably enable this everywhere but I don't know if // it's supported on all hardware or is slower or whatnot.. - if (IsVRMode()) { + if (g_core->IsVRMode()) { glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); } else { @@ -5702,7 +5711,7 @@ void RendererGL::SetDoubleSided(bool d) { } void RendererGL::UpdateVignetteTex(bool force) { - if (force || vignette_quality_ != g_graphics_server->quality() + if (force || vignette_quality_ != g_base->graphics_server->quality() || vignette_tex_outer_r_ != vignette_outer().x || vignette_tex_outer_g_ != vignette_outer().y || vignette_tex_outer_b_ != vignette_outer().z @@ -5715,7 +5724,7 @@ void RendererGL::UpdateVignetteTex(bool force) { vignette_tex_inner_r_ = vignette_inner().x; vignette_tex_inner_g_ = vignette_inner().y; vignette_tex_inner_b_ = vignette_inner().z; - vignette_quality_ = g_graphics_server->quality(); + vignette_quality_ = g_base->graphics_server->quality(); const int width = 64; const int height = 64; @@ -5841,7 +5850,7 @@ void RendererGL::CheckCapabilities() { CheckGLExtensions(); } #if BA_OSTYPE_ANDROID std::string RendererGL::GetAutoAndroidRes() { - assert(InMainThread()); + assert(g_base->InGraphicsThread()); const char* renderer = (const char*)glGetString(GL_RENDERER); @@ -5849,7 +5858,7 @@ std::string RendererGL::GetAutoAndroidRes() { if (strstr(renderer, "Adreno (TM) 4") || strstr(renderer, "Adreno (TM) 5")) { // for phones lets go with 1080p (phones most likely have 1920x1080-ish // aspect ratios) - if (g_ui->scale() == UIScale::kSmall) { + if (g_base->ui->scale() == UIScale::kSmall) { return "1080p"; } else { // tablets are more likely to have 1920x1200 so lets inch a bit higher @@ -5861,7 +5870,7 @@ std::string RendererGL::GetAutoAndroidRes() { if (is_extra_speedy_android_device_) { // for phones lets go with 1080p (phones most likely have 1920x1080-ish // aspect ratios) - if (g_ui->scale() == UIScale::kSmall) { + if (g_base->ui->scale() == UIScale::kSmall) { return "1080p"; } else { // tablets are more likely to have 1920x1200 so lets inch a bit higher @@ -5870,7 +5879,7 @@ std::string RendererGL::GetAutoAndroidRes() { } // Amazon Fire tablet (as of jan '18) needs REAL low res to feel smooth. - if (g_platform->GetDeviceName() == "Amazon KFAUWI") { + if (g_core->platform->GetDeviceName() == "Amazon KFAUWI") { return "480p"; } @@ -5881,14 +5890,14 @@ std::string RendererGL::GetAutoAndroidRes() { #endif // BA_OSTYPE_ANDROID auto RendererGL::GetAutoTextureQuality() -> TextureQuality { - assert(InMainThread()); + assert(g_base->InGraphicsThread()); TextureQuality qual{TextureQuality::kHigh}; #if BA_OSTYPE_ANDROID { // lets be cheaper in VR mode since we have to draw twice.. - if (IsVRMode()) { + if (g_core->IsVRMode()) { qual = TextureQuality::kMedium; } else { // ouya is a special case since we have dds textures there; default to @@ -5898,7 +5907,7 @@ auto RendererGL::GetAutoTextureQuality() -> TextureQuality { #else // BA_OUYA_BUILD // on android we default to high quality mode if we support ETC2; // otherwise go with medium - if (g_graphics_server->SupportsTextureCompressionType( + if (g_base->graphics_server->SupportsTextureCompressionType( TextureCompressionType::kETC2) || is_speedy_android_device_) { qual = TextureQuality::kHigh; @@ -5910,7 +5919,7 @@ auto RendererGL::GetAutoTextureQuality() -> TextureQuality { } #elif BA_OSTYPE_IOS_TVOS { - if (AppleUtils::IsSlowIOSDevice()) { + if (core::AppleUtils::IsSlowIOSDevice()) { qual = TextureQuality::kMedium; } else { qual = TextureQuality::kHigh; @@ -5927,11 +5936,11 @@ auto RendererGL::GetAutoTextureQuality() -> TextureQuality { } auto RendererGL::GetAutoGraphicsQuality() -> GraphicsQuality { - assert(InMainThread()); + assert(g_base->InGraphicsThread()); GraphicsQuality q{GraphicsQuality::kMedium}; #if BA_OSTYPE_ANDROID // lets be cheaper in VR mode since we draw twice.. - if (IsVRMode()) { + if (g_core->IsVRMode()) { q = GraphicsQuality::kMedium; } else { if (is_extra_speedy_android_device_) { @@ -5946,11 +5955,11 @@ auto RendererGL::GetAutoGraphicsQuality() -> GraphicsQuality { // on IOS we default to low-quality for slow devices (iphone-4, etc) // medium for recent-ish ones (ipad2, iphone4s, etc), high for newer-ish // (iPhone5, iPad4), and higher for anything beyond that - if (AppleUtils::IsSlowIOSDevice()) { + if (core::AppleUtils::IsSlowIOSDevice()) { q = GraphicsQuality::kLow; - } else if (AppleUtils::IsMediumIOSDevice()) { + } else if (core::AppleUtils::IsMediumIOSDevice()) { q = GraphicsQuality::kMedium; - } else if (AppleUtils::IsHighIOSDevice()) { + } else if (core::AppleUtils::IsHighIOSDevice()) { q = GraphicsQuality::kHigh; } else { q = GraphicsQuality::kHigher; @@ -5965,9 +5974,9 @@ auto RendererGL::GetAutoGraphicsQuality() -> GraphicsQuality { void RendererGL::RetainShader(ProgramGL* p) { shaders_.emplace_back(p); } void RendererGL::Load() { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); assert(!data_loaded_); - assert(g_graphics_server->graphics_quality_set()); + assert(g_base->graphics_server->graphics_quality_set()); if (!got_screen_framebuffer_) { got_screen_framebuffer_ = true; @@ -5979,7 +5988,7 @@ void RendererGL::Load() { } Renderer::Load(); int high_qual_pp_flag = - g_graphics_server->quality() >= GraphicsQuality::kHigher + g_base->graphics_server->quality() >= GraphicsQuality::kHigher ? SHD_HIGHER_QUALITY : 0; screen_mesh_ = std::make_unique(this); @@ -6104,7 +6113,7 @@ void RendererGL::Load() { // Gonna wait before a clean win before turning it on. p = postprocess_prog_ = new PostProcessProgramGL(this, high_qual_pp_flag); RetainShader(p); - if (g_graphics_server->quality() >= GraphicsQuality::kHigher) { + if (g_base->graphics_server->quality() >= GraphicsQuality::kHigher) { p = postprocess_eyes_prog_ = new PostProcessProgramGL(this, SHD_EYES); RetainShader(p); } else { @@ -6190,7 +6199,7 @@ void RendererGL::PostLoad() { } void RendererGL::Unload() { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); DEBUG_CHECK_GL_ERROR; assert(data_loaded_); Renderer::Unload(); @@ -6220,7 +6229,7 @@ void RendererGL::Unload() { } recycle_mesh_datas_sprite_.clear(); screen_mesh_.reset(); - if (!g_graphics_server->renderer_context_lost()) { + if (!g_base->graphics_server->renderer_context_lost()) { glDeleteTextures(1, &random_tex_); glDeleteTextures(1, &vignette_tex_); } @@ -6268,12 +6277,13 @@ void RendererGL::Unload() { DEBUG_CHECK_GL_ERROR; } -auto RendererGL::NewModelData(const ModelData& model) -> ModelRendererData* { - return Object::NewDeferred(model, this); +auto RendererGL::NewMeshAssetData(const MeshAsset& model) + -> Object::Ref { + return Object::New(model, this); } -auto RendererGL::NewTextureData(const TextureData& texture) - -> TextureRendererData* { - return Object::NewDeferred(texture, this); +auto RendererGL::NewTextureData(const TextureAsset& texture) + -> Object::Ref { + return Object::New(texture, this); } auto RendererGL::NewScreenRenderTarget() -> RenderTarget* { return Object::NewDeferred(this); @@ -6282,10 +6292,11 @@ auto RendererGL::NewFramebufferRenderTarget(int width, int height, bool linear_interp, bool depth, bool texture, bool depth_texture, bool high_quality, bool msaa, - bool alpha) -> RenderTarget* { - return Object::NewDeferred(this, width, height, linear_interp, - depth, texture, depth_texture, - high_quality, msaa, alpha); + bool alpha) + -> Object::Ref { + return Object::New( + this, width, height, linear_interp, depth, texture, depth_texture, + high_quality, msaa, alpha); } auto RendererGL::NewMeshData(MeshDataType mesh_type, MeshDrawType draw_type) @@ -6380,10 +6391,10 @@ auto RendererGL::NewMeshData(MeshDataType mesh_type, MeshDrawType draw_type) } void RendererGL::DeleteMeshData(MeshRendererData* source_in, MeshDataType mesh_type) { - // when we're done with mesh-data we keep it around for recycling... - // it seems that killing off VAO/VBOs can be hitchy (on mac at least) - // hmmm should we have some sort of threshold at which point we kill off - // some?.. + // When we're done with mesh-data we keep it around for recycling. + // It seems that killing off VAO/VBOs can be hitchy (on mac at least). + // Hmmm should we have some sort of threshold at which point we kill off + // some? switch (mesh_type) { case MeshDataType::kIndexedSimpleSplit: { @@ -6459,46 +6470,46 @@ void RendererGL::DrawDebug() { SimpleProgramGL* p = simple_tex_prog_; p->Bind(); - g_graphics_server->ModelViewReset(); - g_graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1); + g_base->graphics_server->ModelViewReset(); + g_base->graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1); float tx = -0.6f; float ty = 0.6f; - g_graphics_server->PushTransform(); - g_graphics_server->scale(Vector3f(0.4f, 0.4f, 0.4f)); - g_graphics_server->Translate(Vector3f(-1.3f, -0.7f, 0)); + g_base->graphics_server->PushTransform(); + g_base->graphics_server->scale(Vector3f(0.4f, 0.4f, 0.4f)); + g_base->graphics_server->Translate(Vector3f(-1.3f, -0.7f, 0)); // Draw cam buffer. - g_graphics_server->PushTransform(); - g_graphics_server->Translate(Vector3f(tx, ty, 0)); + g_base->graphics_server->PushTransform(); + g_base->graphics_server->Translate(Vector3f(tx, ty, 0)); tx += 0.2f; ty -= 0.25f; - g_graphics_server->scale(Vector3f(0.5f, 0.5f, 1.0f)); + g_base->graphics_server->scale(Vector3f(0.5f, 0.5f, 1.0f)); p->SetColorTexture(static_cast(camera_render_target()) ->framebuffer() ->texture()); GetActiveProgram()->PrepareToDraw(); screen_mesh_->Bind(); screen_mesh_->Draw(DrawType::kTriangles); - g_graphics_server->PopTransform(); + g_base->graphics_server->PopTransform(); // Draw blur buffers. if (explicit_bool(false)) { for (auto&& i : blur_buffers_) { - g_graphics_server->PushTransform(); - g_graphics_server->Translate(Vector3f(tx, ty, 0)); + g_base->graphics_server->PushTransform(); + g_base->graphics_server->Translate(Vector3f(tx, ty, 0)); tx += 0.2f; ty -= 0.25f; - g_graphics_server->scale(Vector3f(0.5f, 0.5f, 1.0f)); + g_base->graphics_server->scale(Vector3f(0.5f, 0.5f, 1.0f)); p->SetColorTexture(i->texture()); GetActiveProgram()->PrepareToDraw(); screen_mesh_->Bind(); screen_mesh_->Draw(DrawType::kTriangles); - g_graphics_server->PopTransform(); + g_base->graphics_server->PopTransform(); } } - g_graphics_server->PopTransform(); + g_base->graphics_server->PopTransform(); } } } @@ -6524,7 +6535,7 @@ void RendererGL::GenerateCameraBufferBlurPasses() { // In higher-quality we do multiple levels and 16-bit dithering is kinda // noticeable and ugly then. bool high_quality_fbos = - (g_graphics_server->quality() >= GraphicsQuality::kHigher); + (g_base->graphics_server->quality() >= GraphicsQuality::kHigher); for (int i = 0; i < blur_res_count(); i++) { assert(w % 2 == 0); assert(h % 2 == 0); @@ -6543,7 +6554,7 @@ void RendererGL::GenerateCameraBufferBlurPasses() { } // Final redundant one (we run an extra blur without down-rezing). - if (g_graphics_server->quality() >= GraphicsQuality::kHigher) + if (g_base->graphics_server->quality() >= GraphicsQuality::kHigher) blur_buffers_.push_back(Object::New( this, w, h, true, // linear_interp @@ -6559,8 +6570,8 @@ void RendererGL::GenerateCameraBufferBlurPasses() { // Ok now go through and do the blurring. SetDepthWriting(false); SetDepthTesting(false); - g_graphics_server->ModelViewReset(); - g_graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1); + g_base->graphics_server->ModelViewReset(); + g_base->graphics_server->SetOrthoProjection(-1, 1, -1, 1, -1, 1); SetDoubleSided(false); SetBlend(false); @@ -6570,7 +6581,7 @@ void RendererGL::GenerateCameraBufferBlurPasses() { FramebufferObjectGL* src_fb = static_cast(camera_render_target())->framebuffer(); for (auto&& i : blur_buffers_) { - FramebufferObjectGL* fb = i.get(); + FramebufferObjectGL* fb = i.Get(); assert(fb); fb->Bind(); SetViewport(0, 0, fb->width(), fb->height()); @@ -6595,7 +6606,7 @@ void RendererGL::CardboardDisableScissor() { glDisable(GL_SCISSOR_TEST); } void RendererGL::CardboardEnableScissor() { glEnable(GL_SCISSOR_TEST); } void RendererGL::VREyeRenderBegin() { - assert(IsVRMode()); + assert(g_core->IsVRMode()); // On rift we need to turn off srgb conversion for each eye render // so we can dump our linear data into oculus' srgb buffer as-is. @@ -6619,7 +6630,7 @@ void RendererGL::VRSyncRenderStates() { void RendererGL::RenderFrameDefEnd() { // Need to set some states to keep cardboard happy. #if BA_CARDBOARD_BUILD - if (IsVRMode()) { + if (g_core->IsVRMode()) { SyncGLState(); glEnable(GL_SCISSOR_TEST); } @@ -6628,6 +6639,6 @@ void RendererGL::RenderFrameDefEnd() { #pragma clang diagnostic pop -} // namespace ballistica +} // namespace ballistica::base #endif // BA_ENABLE_OPENGL diff --git a/src/ballistica/graphics/gl/renderer_gl.h b/src/ballistica/base/graphics/gl/renderer_gl.h similarity index 92% rename from src/ballistica/graphics/gl/renderer_gl.h rename to src/ballistica/base/graphics/gl/renderer_gl.h index 864cdce7..1fab1534 100644 --- a/src/ballistica/graphics/gl/renderer_gl.h +++ b/src/ballistica/base/graphics/gl/renderer_gl.h @@ -1,21 +1,21 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_GL_RENDERER_GL_H_ -#define BALLISTICA_GRAPHICS_GL_RENDERER_GL_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_GL_RENDERER_GL_H_ +#define BALLISTICA_BASE_GRAPHICS_GL_RENDERER_GL_H_ #include #include #include -#include "ballistica/ballistica.h" +#include "ballistica/shared/ballistica.h" #if BA_ENABLE_OPENGL -#include "ballistica/core/object.h" -#include "ballistica/graphics/gl/gl_sys.h" -#include "ballistica/graphics/renderer.h" +#include "ballistica/base/graphics/gl/gl_sys.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::base { // for now lets not go above 8 since that's what the iPhone 3gs has.. // ...haha perhaps should revisit this @@ -24,7 +24,7 @@ constexpr int kMaxGLTexUnitsUsed = 5; class RendererGL : public Renderer { class FakeVertexArrayObject; class TextureDataGL; - class ModelDataGL; + class MeshAssetDataGL; class MeshDataGL; class MeshDataSimpleSplitGL; class MeshDataObjectSplitGL; @@ -87,13 +87,16 @@ class RendererGL : public Renderer { bool depth, bool texture, bool depth_is_texture, bool high_quality, bool msaa, bool alpha) - -> RenderTarget* override; - auto NewModelData(const ModelData& model) -> ModelRendererData* override; - auto NewTextureData(const TextureData& texture) - -> TextureRendererData* override; + -> Object::Ref override; + auto NewMeshAssetData(const MeshAsset& mesh) + -> Object::Ref override; + auto NewTextureData(const TextureAsset& texture) + -> Object::Ref override; + auto NewMeshData(MeshDataType type, MeshDrawType drawType) -> MeshRendererData* override; void DeleteMeshData(MeshRendererData* data, MeshDataType type) override; + void ProcessRenderCommandBuffer(RenderCommandBuffer* buffer, const RenderPass& pass, RenderTarget* render_target) override; @@ -144,7 +147,7 @@ class RendererGL : public Renderer { // Note: This is only for use when VAOs aren't supported. void SetVertexAttribArrayEnabled(GLuint i, bool enabled); - void BindTexture(GLuint type, const TextureData* t, GLuint tex_unit = 0); + void BindTexture(GLuint type, const TextureAsset* t, GLuint tex_unit = 0); void BindTexture(GLuint type, GLuint tex, GLuint tex_unit = 0); void BindTextureUnit(uint32_t tex_unit); void BindFramebuffer(GLuint fb); @@ -256,8 +259,8 @@ class RendererGL : public Renderer { int error_check_counter_{}; }; -} // namespace ballistica +} // namespace ballistica::base #endif // BA_ENABLE_OPENGL -#endif // BALLISTICA_GRAPHICS_GL_RENDERER_GL_H_ +#endif // BALLISTICA_BASE_GRAPHICS_GL_RENDERER_GL_H_ diff --git a/src/ballistica/graphics/graphics.cc b/src/ballistica/base/graphics/graphics.cc similarity index 72% rename from src/ballistica/graphics/graphics.cc rename to src/ballistica/base/graphics/graphics.cc index e5a397ed..d00e2a91 100644 --- a/src/ballistica/graphics/graphics.cc +++ b/src/ballistica/base/graphics/graphics.cc @@ -1,37 +1,35 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/graphics.h" +#include "ballistica/base/graphics/graphics.h" -#include "ballistica/app/app.h" -#include "ballistica/app/app_flavor.h" -#include "ballistica/dynamics/bg/bg_dynamics.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/camera.h" -#include "ballistica/graphics/component/empty_component.h" -#include "ballistica/graphics/component/object_component.h" -#include "ballistica/graphics/component/post_process_component.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/graphics/component/special_component.h" -#include "ballistica/graphics/component/sprite_component.h" -#include "ballistica/graphics/gl/renderer_gl.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/net_graph.h" -#include "ballistica/graphics/text/text_graphics.h" -#include "ballistica/input/input.h" -#include "ballistica/logic/connection/connection_set.h" -#include "ballistica/logic/connection/connection_to_client.h" -#include "ballistica/logic/connection/connection_to_host.h" -#include "ballistica/logic/session/session.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_context_call.h" -#include "ballistica/scene/node/globals_node.h" -#include "ballistica/scene/scene.h" -#include "ballistica/ui/console.h" -#include "ballistica/ui/root_ui.h" -#include "ballistica/ui/ui.h" -#include "ballistica/ui/widget/root_widget.h" +#include "ballistica/base/app/app.h" +#include "ballistica/base/app/app_config.h" +#include "ballistica/base/app/app_mode.h" +#include "ballistica/base/dynamics/bg/bg_dynamics.h" +#include "ballistica/base/graphics/component/empty_component.h" +#include "ballistica/base/graphics/component/object_component.h" +#include "ballistica/base/graphics/component/post_process_component.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/graphics/component/special_component.h" +#include "ballistica/base/graphics/component/sprite_component.h" +#include "ballistica/base/graphics/gl/renderer_gl.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/graphics/support/camera.h" +#include "ballistica/base/graphics/support/net_graph.h" +#include "ballistica/base/graphics/text/text_graphics.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/base/ui/console.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/core/core.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/ui_v1/support/root_ui.h" +#include "ballistica/ui_v1/widget/root_widget.h" -namespace ballistica { +namespace ballistica::base { const float kScreenMessageZDepth{-0.06f}; const float kScreenMeshZDepth{-0.05f}; @@ -92,16 +90,153 @@ auto Graphics::IsShaderTransparent(ShadingType c) -> bool { Graphics::Graphics() = default; Graphics::~Graphics() = default; +void Graphics::OnAppStart() { assert(g_base->InLogicThread()); } + +void Graphics::OnAppPause() { + assert(g_base->InLogicThread()); + SetGyroEnabled(false); +} + +void Graphics::OnAppResume() { + assert(g_base->InLogicThread()); + g_base->graphics->SetGyroEnabled(true); +} + +void Graphics::OnAppShutdown() { assert(g_base->InLogicThread()); } + +void Graphics::ApplyAppConfig() { + assert(g_base->InLogicThread()); + + // Not relevant for fullscreen anymore + // since we're fullscreen windows everywhere. + int width = 800; + int height = 600; + + // Texture quality. + TextureQualityRequest texture_quality_requested; + std::string texqualstr = + g_base->app_config->Resolve(AppConfig::StringID::kTextureQuality); + + if (texqualstr == "Auto") { + texture_quality_requested = TextureQualityRequest::kAuto; + } else if (texqualstr == "High") { + texture_quality_requested = TextureQualityRequest::kHigh; + } else if (texqualstr == "Medium") { + texture_quality_requested = TextureQualityRequest::kMedium; + } else if (texqualstr == "Low") { + texture_quality_requested = TextureQualityRequest::kLow; + } else { + Log(LogLevel::kError, + "Invalid texture quality: '" + texqualstr + "'; defaulting to low."); + texture_quality_requested = TextureQualityRequest::kLow; + } + + // Graphics quality. + std::string gqualstr = + g_base->app_config->Resolve(AppConfig::StringID::kGraphicsQuality); + GraphicsQualityRequest graphics_quality_requested; + + if (gqualstr == "Auto") { + graphics_quality_requested = GraphicsQualityRequest::kAuto; + } else if (gqualstr == "Higher") { + graphics_quality_requested = GraphicsQualityRequest::kHigher; + } else if (gqualstr == "High") { + graphics_quality_requested = GraphicsQualityRequest::kHigh; + } else if (gqualstr == "Medium") { + graphics_quality_requested = GraphicsQualityRequest::kMedium; + } else if (gqualstr == "Low") { + graphics_quality_requested = GraphicsQualityRequest::kLow; + } else { + Log(LogLevel::kError, + "Invalid graphics quality: '" + gqualstr + "'; defaulting to auto."); + graphics_quality_requested = GraphicsQualityRequest::kAuto; + } + + // Android res string. + std::string android_res = + g_base->app_config->Resolve(AppConfig::StringID::kResolutionAndroid); + + bool fullscreen = g_base->app_config->Resolve(AppConfig::BoolID::kFullscreen); + + // Note: when the graphics-thread applies the first set-screen event it will + // trigger the remainder of startup such as media-loading; make sure nothing + // below this will affect that. + g_base->graphics_server->PushSetScreenCall( + fullscreen, width, height, texture_quality_requested, + graphics_quality_requested, android_res); + + set_show_fps(g_base->app_config->Resolve(AppConfig::BoolID::kShowFPS)); + set_show_ping(g_base->app_config->Resolve(AppConfig::BoolID::kShowPing)); + + g_base->graphics_server->PushSetScreenGammaCall( + g_base->app_config->Resolve(AppConfig::FloatID::kScreenGamma)); + g_base->graphics_server->PushSetScreenPixelScaleCall( + g_base->app_config->Resolve(AppConfig::FloatID::kScreenPixelScale)); + + // Set tv border (for both client and server). + // FIXME: this should exist either on the client or the server; not both. + // (and should be communicated via frameldefs/etc.) + bool tv_border = + g_base->app_config->Resolve(AppConfig::BoolID::kEnableTVBorder); + g_base->graphics_server->event_loop()->PushCall( + [tv_border] { g_base->graphics_server->set_tv_border(tv_border); }); + set_tv_border(tv_border); + + // V-sync setting. + std::string v_sync = + g_base->app_config->Resolve(AppConfig::StringID::kVerticalSync); + bool do_v_sync{}; + bool auto_v_sync{}; + if (v_sync == "Auto") { + do_v_sync = true; + auto_v_sync = true; + } else if (v_sync == "Always") { + do_v_sync = true; + auto_v_sync = false; + } else if (v_sync == "Never") { + do_v_sync = false; + auto_v_sync = false; + } else { + do_v_sync = false; + auto_v_sync = false; + Log(LogLevel::kError, "Invalid 'Vertical Sync' value: '" + v_sync + "'"); + } + g_base->graphics_server->PushSetVSyncCall(do_v_sync, auto_v_sync); + + bool disable_camera_shake = + g_base->app_config->Resolve(AppConfig::BoolID::kDisableCameraShake); + set_camera_shake_disabled(disable_camera_shake); + + bool disable_camera_gyro = + g_base->app_config->Resolve(AppConfig::BoolID::kDisableCameraGyro); + set_camera_gyro_explicitly_disabled(disable_camera_gyro); +} + +void Graphics::StepDisplayTime() { assert(g_base->InLogicThread()); } + +void Graphics::AddCleanFrameCommand(const Object::Ref& c) { + BA_PRECONDITION(g_base->InLogicThread()); + clean_frame_commands_.push_back(c); +} + +void Graphics::RunCleanFrameCommands() { + assert(g_base->InLogicThread()); + for (auto&& i : clean_frame_commands_) { + i->Run(); + } + clean_frame_commands_.clear(); +} + void Graphics::SetGyroEnabled(bool enable) { // If we're turning back on, suppress gyro updates for a bit. if (enable && !gyro_enabled_) { - last_suppress_gyro_time_ = GetRealTime(); + last_suppress_gyro_time_ = g_core->GetAppTimeMillisecs(); } gyro_enabled_ = enable; } void Graphics::UpdateProgressBarProgress(float target) { - millisecs_t real_time = GetRealTime(); + millisecs_t real_time = g_core->GetAppTimeMillisecs(); float p = target; if (p < 0) { p = 0; @@ -116,21 +251,24 @@ void Graphics::UpdateProgressBarProgress(float target) { } void Graphics::DrawProgressBar(RenderPass* pass, float opacity) { - millisecs_t real_time = GetRealTime(); + millisecs_t real_time = g_core->GetAppTimeMillisecs(); float amount = progress_bar_progress_; - if (amount < 0) amount = 0; + if (amount < 0) { + amount = 0; + } SimpleComponent c(pass); c.SetTransparent(true); - float o = opacity; - float delay = 0; + float o{opacity}; + float delay{}; // Fade in for the first 2 seconds if desired. if (progress_bar_fade_in_) { - millisecs_t since_start = real_time - last_progress_bar_start_time_; + auto since_start = + static_cast(real_time - last_progress_bar_start_time_); if (since_start < delay) { o = 0.0f; - } else if (since_start < 2000 + delay) { + } else if (since_start < 2000.0f + delay) { o *= (since_start - delay) / 2000.0f; } } @@ -158,11 +296,11 @@ void Graphics::DrawProgressBar(RenderPass* pass, float opacity) { (t - b)); c.SetColor(0.0f, 0.07f, 0.0f, 1 * o); - c.DrawMesh(progress_bar_bottom_mesh_.get()); + c.DrawMesh(progress_bar_bottom_mesh_.Get()); c.Submit(); c.SetColor(0.23f, 0.17f, 0.35f, 1 * o); - c.DrawMesh(progress_bar_top_mesh_.get()); + c.DrawMesh(progress_bar_top_mesh_.Get()); c.Submit(); } @@ -197,8 +335,8 @@ auto Graphics::GetShadowDensity(float x, float y, float z) -> float { class Graphics::ScreenMessageEntry { public: ScreenMessageEntry(std::string s_in, bool align_left_in, uint32_t c, - const Vector3f& color_in, Texture* texture_in, - Texture* tint_texture_in, const Vector3f& tint_in, + const Vector3f& color_in, TextureAsset* texture_in, + TextureAsset* tint_texture_in, const Vector3f& tint_in, const Vector3f& tint2_in) : align_left(align_left_in), creation_time(c), @@ -220,8 +358,8 @@ class Graphics::ScreenMessageEntry { Vector3f tint2; std::string s_raw; std::string s_translated; - Object::Ref texture; - Object::Ref tint_texture; + Object::Ref texture; + Object::Ref tint_texture; float v_smoothed; bool translation_dirty; bool mesh_dirty; @@ -233,14 +371,14 @@ class Graphics::ScreenMessageEntry { // Draw controls and things that lie on top of the action. void Graphics::DrawMiscOverlays(RenderPass* pass) { // Every now and then, update our stats. - while (GetRealTime() >= next_stat_update_time_) { - if (GetRealTime() - next_stat_update_time_ > 1000) { - next_stat_update_time_ = GetRealTime() + 1000; + while (g_core->GetAppTimeMillisecs() >= next_stat_update_time_) { + if (g_core->GetAppTimeMillisecs() - next_stat_update_time_ > 1000) { + next_stat_update_time_ = g_core->GetAppTimeMillisecs() + 1000; } else { next_stat_update_time_ += 1000; } int total_frames_rendered = - g_graphics_server->renderer()->total_frames_rendered(); + g_base->graphics_server->renderer()->total_frames_rendered(); last_fps_ = total_frames_rendered - last_total_frames_rendered_; last_total_frames_rendered_ = total_frames_rendered; } @@ -251,14 +389,14 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { snprintf(fps_str, sizeof(fps_str), "%d", last_fps_); if (fps_str != fps_string_) { fps_string_ = fps_str; - if (!fps_text_group_.exists()) { + if (!fps_text_group_.Exists()) { fps_text_group_ = Object::New(); } fps_text_group_->SetText(fps_string_); } SimpleComponent c(pass); c.SetTransparent(true); - if (IsVRMode()) { + if (g_core->IsVRMode()) { c.SetColor(1, 1, 1, 1); } else { c.SetColor(0.8f, 0.8f, 0.8f, 1.0f); @@ -266,7 +404,7 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { int text_elem_count = fps_text_group_->GetElementCount(); for (int e = 0; e < text_elem_count; e++) { c.SetTexture(fps_text_group_->GetElementTexture(e)); - if (IsVRMode()) { + if (g_core->IsVRMode()) { c.SetShadow(-0.003f * fps_text_group_->GetElementUScale(e), -0.003f * fps_text_group_->GetElementVScale(e), 0.0f, 1.0f); c.SetMaskUV2Texture(fps_text_group_->GetElementMaskUV2Texture(e)); @@ -278,106 +416,47 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { } if (show_ping_) { + std::string ping_str = g_base->app_mode->GetPingString(); float ping{}; - char ping_str[32]; - if (ConnectionToHost* connection_to_host = - g_logic->connections()->connection_to_host()) { - if (connection_to_host->can_communicate()) { - ping = connection_to_host->current_ping(); - snprintf(ping_str, sizeof(ping_str), "%.0f ms", ping); - if (ping_str != ping_string_) { - ping_string_ = ping_str; - if (!ping_text_group_.exists()) { - ping_text_group_ = Object::New(); - } - ping_text_group_->SetText(ping_string_); + if (!ping_str.empty()) { + if (ping_str != ping_string_) { + ping_string_ = ping_str; + if (!ping_text_group_.Exists()) { + ping_text_group_ = Object::New(); } - SimpleComponent c(pass); - c.SetTransparent(true); - c.SetColor(0.5f, 0.9f, 0.5f, 1.0f); - if (ping > 100.0f) { - c.SetColor(0.8f, 0.8f, 0.0f, 1.0f); - } - if (ping > 500.0f) { - c.SetColor(0.9f, 0.2f, 0.2f, 1.0f); - } - - int text_elem_count = ping_text_group_->GetElementCount(); - for (int e = 0; e < text_elem_count; e++) { - c.SetTexture(ping_text_group_->GetElementTexture(e)); - c.SetFlatness(1.0f); - c.PushTransform(); - c.Translate(14.0f + (show_fps_ ? 30.0f : 0.0f), 0.1f, - kScreenMessageZDepth); - c.Scale(0.7f, 0.7f); - c.DrawMesh(ping_text_group_->GetElementMesh(e)); - c.PopTransform(); - } - c.Submit(); + ping_text_group_->SetText(ping_string_); } + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetColor(0.5f, 0.9f, 0.5f, 1.0f); + if (ping > 100.0f) { + c.SetColor(0.8f, 0.8f, 0.0f, 1.0f); + } + if (ping > 500.0f) { + c.SetColor(0.9f, 0.2f, 0.2f, 1.0f); + } + + int text_elem_count = ping_text_group_->GetElementCount(); + for (int e = 0; e < text_elem_count; e++) { + c.SetTexture(ping_text_group_->GetElementTexture(e)); + c.SetFlatness(1.0f); + c.PushTransform(); + c.Translate(14.0f + (show_fps_ ? 30.0f : 0.0f), 0.1f, + kScreenMessageZDepth); + c.Scale(0.7f, 0.7f); + c.DrawMesh(ping_text_group_->GetElementMesh(e)); + c.PopTransform(); + } + c.Submit(); } } if (show_net_info_) { - char net_info_str[128]; - int64_t in_count = 0; - int64_t in_size = 0; - int64_t in_size_compressed = 0; - int64_t outCount = 0; - int64_t out_size = 0; - int64_t out_size_compressed = 0; - int64_t resends = 0; - int64_t resends_size = 0; - bool show = false; - - // Add in/out data for any host connection. - if (ConnectionToHost* connection_to_host = - g_logic->connections()->connection_to_host()) { - if (connection_to_host->can_communicate()) show = true; - in_size += connection_to_host->GetBytesInPerSecond(); - in_size_compressed += connection_to_host->GetBytesInPerSecondCompressed(); - in_count += connection_to_host->GetMessagesInPerSecond(); - out_size += connection_to_host->GetBytesOutPerSecond(); - out_size_compressed += - connection_to_host->GetBytesOutPerSecondCompressed(); - outCount += connection_to_host->GetMessagesOutPerSecond(); - resends += connection_to_host->GetMessageResendsPerSecond(); - resends_size += connection_to_host->GetBytesResentPerSecond(); - } else { - int connected_count = 0; - for (auto&& i : g_logic->connections()->connections_to_clients()) { - ConnectionToClient* client = i.second.get(); - if (client->can_communicate()) { - show = true; - connected_count += 1; - } - in_size += client->GetBytesInPerSecond(); - in_size_compressed += client->GetBytesInPerSecondCompressed(); - in_count += client->GetMessagesInPerSecond(); - out_size += client->GetBytesOutPerSecond(); - out_size_compressed += client->GetBytesOutPerSecondCompressed(); - outCount += client->GetMessagesOutPerSecond(); - resends += client->GetMessageResendsPerSecond(); - resends_size += client->GetBytesResentPerSecond(); - } - } - - if (show) { - snprintf(net_info_str, sizeof(net_info_str), - "in: %d/%d/%d\nout: %d/%d/%d\nrpt: %d/%d", - static_cast_check_fit(in_size), - static_cast_check_fit(in_size_compressed), - static_cast_check_fit(in_count), - static_cast_check_fit(out_size), - static_cast_check_fit(out_size_compressed), - static_cast_check_fit(outCount), - static_cast_check_fit(resends_size), - static_cast_check_fit(resends)); - - net_info_str[sizeof(net_info_str) - 1] = 0; // in case we overran.. + auto net_info_str{g_base->app_mode->GetNetworkDebugString()}; + if (!net_info_str.empty()) { if (net_info_str != net_info_string_) { net_info_string_ = net_info_str; - if (!net_info_text_group_.exists()) { + if (!net_info_text_group_.Exists()) { net_info_text_group_ = Object::New(); } net_info_text_group_->SetText(net_info_string_); @@ -402,14 +481,15 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { // Draw any debug graphs. { float debug_graph_y = 50.0; - auto now = GetRealTime(); + auto now = g_core->GetAppTimeMillisecs(); for (auto it = debug_graphs_.begin(); it != debug_graphs_.end();) { - assert(it->second.exists()); + assert(it->second.Exists()); if (now - it->second->LastUsedTime() > 1000) { it = debug_graphs_.erase(it); } else { - it->second->Draw(pass, GetRealTime(), 50.0f, debug_graph_y, 500.0f, - 100.0f); + it->second->Draw(pass, + static_cast(g_core->GetAppTimeMillisecs()), + 50.0f, debug_graph_y, 500.0f, 100.0f); debug_graph_y += 110.0f; ++it; @@ -422,8 +502,8 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { // Delete old ones. if (!screen_messages_.empty()) { millisecs_t cutoff; - if (GetRealTime() > 5000) { - cutoff = GetRealTime() - 5000; + if (g_core->GetAppTimeMillisecs() > 5000) { + cutoff = g_core->GetAppTimeMillisecs() - 5000; for (auto i = screen_messages_.begin(); i != screen_messages_.end();) { if (i->creation_time < cutoff) { auto next = i; @@ -444,13 +524,13 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { // Draw all existing. if (!screen_messages_.empty()) { - bool vr = IsVRMode(); + bool vr = g_core->IsVRMode(); // These are less disruptive in the middle for menus but at the bottom // during gameplay. - float start_v = g_graphics->screen_virtual_height() * 0.05f; + float start_v = g_base->graphics->screen_virtual_height() * 0.05f; float scale; - switch (g_ui->scale()) { + switch (g_base->ui->scale()) { case UIScale::kSmall: scale = 1.5f; break; @@ -466,9 +546,10 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { { SimpleComponent c(pass); c.SetTransparent(true); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kSoftRectVertical)); + c.SetTexture( + g_base->assets->SysTexture(SysTextureID::kSoftRectVertical)); - float screen_width = g_graphics->screen_virtual_width(); + float screen_width = g_base->graphics->screen_virtual_width(); v = start_v; @@ -479,13 +560,14 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { // Update the translation if need be. i->UpdateTranslation(); - millisecs_t age = GetRealTime() - i->creation_time; + millisecs_t age = g_core->GetAppTimeMillisecs() - i->creation_time; youngest_age = std::min(youngest_age, age); float s_extra = 1.0f; if (age < 100) { - s_extra = std::min(1.2f, 1.2f * (age / 100.0f)); + s_extra = std::min(1.2f, 1.2f * (static_cast(age) / 100.0f)); } else if (age < 150) { - s_extra = 1.2f - 0.2f * ((150.0f - age) / 50.0f); + s_extra = + 1.2f - 0.2f * ((150.0f - static_cast(age)) / 50.0f); } float a; @@ -502,9 +584,9 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { assert(!i->translation_dirty); float str_height = - g_text_graphics->GetStringHeight(i->s_translated.c_str()); + g_base->text_graphics->GetStringHeight(i->s_translated.c_str()); float str_width = - g_text_graphics->GetStringWidth(i->s_translated.c_str()); + g_base->text_graphics->GetStringWidth(i->s_translated.c_str()); if ((str_width * scale) > (screen_width - 40)) { s_extra *= ((screen_width - 40) / (str_width * scale)); @@ -515,13 +597,13 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { float b = i->color.z; GetSafeColor(&r, &g, &b); - float v_extra = scale * (youngest_age * 0.01f); + float v_extra = scale * (static_cast(youngest_age) * 0.01f); float fade; if (age < 100) { fade = 1.0f; } else { - fade = std::max(0.0f, (200.0f - age) / 100.0f); + fade = std::max(0.0f, (200.0f - static_cast(age)) / 100.0f); } c.SetColor(r * fade, g * fade, b * fade, a); @@ -550,11 +632,11 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { // Align our bottom with where we just scaled from. c.Translate(0, 0.5f, 0); } - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1)); c.PopTransform(); v += scale * (36 + str_height); - if (v > g_graphics->screen_virtual_height() + 30) { + if (v > g_base->graphics->screen_virtual_height() + 30) { break; } } @@ -566,19 +648,20 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { SimpleComponent c(pass); c.SetTransparent(true); - float screen_width = g_graphics->screen_virtual_width(); + float screen_width = g_base->graphics->screen_virtual_width(); v = start_v; millisecs_t youngest_age = 9999; for (auto i = screen_messages_.rbegin(); i != screen_messages_.rend(); i++) { - millisecs_t age = GetRealTime() - i->creation_time; + millisecs_t age = g_core->GetAppTimeMillisecs() - i->creation_time; youngest_age = std::min(youngest_age, age); float s_extra = 1.0f; if (age < 100) { - s_extra = std::min(1.2f, 1.2f * (age / 100.0f)); + s_extra = std::min(1.2f, 1.2f * (static_cast(age) / 100.0f)); } else if (age < 150) { - s_extra = 1.2f - 0.2f * ((150.0f - age) / 50.0f); + s_extra = + 1.2f - 0.2f * ((150.0f - static_cast(age)) / 50.0f); } float a; if (age > 3000) { @@ -588,9 +671,9 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { } assert(!i->translation_dirty); float str_height = - g_text_graphics->GetStringHeight(i->s_translated.c_str()); + g_base->text_graphics->GetStringHeight(i->s_translated.c_str()); float str_width = - g_text_graphics->GetStringWidth(i->s_translated.c_str()); + g_base->text_graphics->GetStringWidth(i->s_translated.c_str()); if ((str_width * scale) > (screen_width - 40)) { s_extra *= ((screen_width - 40) / (str_width * scale)); @@ -603,7 +686,7 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { int elem_count = i->GetText().GetElementCount(); for (int e = 0; e < elem_count; e++) { // Gracefully skip unloaded textures. - TextureData* t = i->GetText().GetElementTexture(e); + TextureAsset* t = i->GetText().GetElementTexture(e); if (!t->preloaded()) continue; c.SetTexture(t); if (i->GetText().GetElementCanColor(e)) { @@ -622,7 +705,7 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { } v += scale * (36 + str_height); - if (v > g_graphics->screen_virtual_height() + 30) break; + if (v > g_base->graphics->screen_virtual_height() + 30) break; } c.Submit(); } @@ -634,8 +717,8 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { // Delete old ones. if (!screen_messages_top_.empty()) { millisecs_t cutoff; - if (GetRealTime() > 5000) { - cutoff = GetRealTime() - 5000; + if (g_core->GetAppTimeMillisecs() > 5000) { + cutoff = g_core->GetAppTimeMillisecs() - 5000; for (auto i = screen_messages_top_.begin(); i != screen_messages_top_.end();) { if (i->creation_time < cutoff) { @@ -661,9 +744,9 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { // Draw all existing. float h = pass->virtual_width() - 300.0f; - v = g_graphics->screen_virtual_height() - 50.0f; + v = g_base->graphics->screen_virtual_height() - 50.0f; - float v_base = g_graphics->screen_virtual_height(); + float v_base = g_base->graphics->screen_virtual_height(); float last_v = -999.0f; float min_spacing = 25.0f; @@ -673,12 +756,12 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { // Update the translation if need be. i->UpdateTranslation(); - millisecs_t age = GetRealTime() - i->creation_time; + millisecs_t age = g_core->GetAppTimeMillisecs() - i->creation_time; float s_extra = 1.0f; if (age < 100) { - s_extra = std::min(1.1f, 1.1f * (age / 100.0f)); + s_extra = std::min(1.1f, 1.1f * (static_cast(age) / 100.0f)); } else if (age < 150) { - s_extra = 1.1f - 0.1f * ((150.0f - age) / 50.0f); + s_extra = 1.1f - 0.1f * ((150.0f - static_cast(age)) / 50.0f); } float a; @@ -696,25 +779,25 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { last_v = i->v_smoothed; // Draw the image if they provided one. - if (i->texture.exists()) { + if (i->texture.Exists()) { c.Submit(); SimpleComponent c2(pass); c2.SetTransparent(true); c2.SetTexture(i->texture); - if (i->tint_texture.exists()) { - c2.SetColorizeTexture(i->tint_texture); + if (i->tint_texture.Exists()) { + c2.SetColorizeTexture(i->tint_texture.Get()); c2.SetColorizeColor(i->tint.x, i->tint.y, i->tint.z); c2.SetColorizeColor2(i->tint2.x, i->tint2.y, i->tint2.z); c2.SetMaskTexture( - g_assets->GetTexture(SystemTextureID::kCharacterIconMask)); + g_base->assets->SysTexture(SysTextureID::kCharacterIconMask)); } c2.SetColor(1, 1, 1, a); c2.PushTransform(); c2.Translate(h - 14, v_base + 10 + i->v_smoothed, kScreenMessageZDepth); c2.Scale(22.0f * s_extra, 22.0f * s_extra); - c2.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c2.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1)); c2.PopTransform(); c2.Submit(); } @@ -727,7 +810,7 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { int elem_count = i->GetText().GetElementCount(); for (int e = 0; e < elem_count; e++) { // Gracefully skip unloaded textures. - TextureData* t = i->GetText().GetElementTexture(e); + TextureAsset* t = i->GetText().GetElementTexture(e); if (!t->preloaded()) continue; c.SetTexture(t); if (i->GetText().GetElementCanColor(e)) { @@ -747,7 +830,8 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { c.PopTransform(); } assert(!i->translation_dirty); - v -= g_text_graphics->GetStringHeight(i->s_translated.c_str()) * 0.6f + v -= g_base->text_graphics->GetStringHeight(i->s_translated.c_str()) + * 0.6f + 8.0f; } c.Submit(); @@ -763,8 +847,8 @@ auto Graphics::GetDebugGraph(const std::string& name, bool smoothed) debug_graphs_[name]->SetLabel(name); debug_graphs_[name]->SetSmoothed(smoothed); } - debug_graphs_[name]->SetLastUsedTime(GetRealTime()); - return debug_graphs_[name].get(); + debug_graphs_[name]->SetLastUsedTime(g_core->GetAppTimeMillisecs()); + return debug_graphs_[name].Get(); } void Graphics::GetSafeColor(float* red, float* green, float* blue, @@ -799,13 +883,13 @@ void Graphics::GetSafeColor(float* red, float* green, float* blue, } void Graphics::AddScreenMessage(const std::string& msg, const Vector3f& color, - bool top, Texture* texture, - Texture* tint_texture, const Vector3f& tint, - const Vector3f& tint2) { + bool top, TextureAsset* texture, + TextureAsset* tint_texture, + const Vector3f& tint, const Vector3f& tint2) { // So we know we're always dealing with valid utf8. std::string m = Utils::GetValidUTF8(msg.c_str(), "ga9msg"); - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (top) { float start_v = -40.0f; if (!screen_messages_top_.empty()) { @@ -813,12 +897,13 @@ void Graphics::AddScreenMessage(const std::string& msg, const Vector3f& color, start_v, std::max(-100.0f, screen_messages_top_.back().v_smoothed - 25.0f)); } - screen_messages_top_.emplace_back(m, true, GetRealTime(), color, texture, - tint_texture, tint, tint2); + screen_messages_top_.emplace_back(m, true, g_core->GetAppTimeMillisecs(), + color, texture, tint_texture, tint, + tint2); screen_messages_top_.back().v_smoothed = start_v; } else { - screen_messages_.emplace_back(m, false, GetRealTime(), color, texture, - tint_texture, tint, tint2); + screen_messages_.emplace_back(m, false, g_core->GetAppTimeMillisecs(), + color, texture, tint_texture, tint, tint2); } } @@ -826,7 +911,7 @@ void Graphics::Reset() { fade_ = 0; fade_start_ = 0; - if (!camera_.exists()) { + if (!camera_.Exists()) { camera_ = Object::New(); } @@ -843,7 +928,7 @@ void Graphics::InitInternalComponents(FrameDef* frame_def) { // Let's draw a bit bigger than screen to account for tv-border-mode. float w = pass->virtual_width(); float h = pass->virtual_height(); - if (IsVRMode()) { + if (g_core->IsVRMode()) { screen_mesh_->SetPositionAndSize( -(0.5f * kVRBorder) * w, (-0.5f * kVRBorder) * h, kScreenMeshZDepth, (1.0f + kVRBorder) * w, (1.0f + kVRBorder) * h); @@ -859,7 +944,7 @@ void Graphics::InitInternalComponents(FrameDef* frame_def) { } auto Graphics::GetEmptyFrameDef() -> FrameDef* { - assert(InLogicThread()); + assert(g_base->InLogicThread()); FrameDef* frame_def; // Grab a ready-to-use recycled one if available. @@ -874,7 +959,7 @@ auto Graphics::GetEmptyFrameDef() -> FrameDef* { } void Graphics::ClearFrameDefDeleteList() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); std::scoped_lock lock(frame_def_delete_list_mutex_); for (auto& i : frame_def_delete_list_) { @@ -892,12 +977,12 @@ void Graphics::ClearFrameDefDeleteList() { void Graphics::FadeScreen(bool to, millisecs_t time, PyObject* endcall) { // If there's an ourstanding fade-end command, go ahead and run it. // (otherwise, overlapping fades can cause things to get lost) - if (fade_end_call_.exists()) { + if (fade_end_call_.Exists()) { if (g_buildconfig.debug_build()) { Log(LogLevel::kWarning, "2 fades overlapping; running first fade-end-call early"); } - g_logic->PushPythonCall(fade_end_call_); + fade_end_call_->Schedule(); fade_end_call_.Clear(); } set_fade_start_on_next_draw_ = true; @@ -916,12 +1001,12 @@ void Graphics::DrawLoadDot(RenderPass* pass) { // Draw red if we've got graphics stuff loading. Green if only other stuff // left. - if (g_assets->GetGraphicalPendingLoadCount() > 0) { + if (g_base->assets->GetGraphicalPendingLoadCount() > 0) { c.SetColor(0.2f, 0, 0, 1); } else { c.SetColor(0, 0.2f, 0, 1); } - c.DrawMesh(load_dot_mesh_.get()); + c.DrawMesh(load_dot_mesh_.Get()); c.Submit(); } @@ -962,7 +1047,7 @@ void Graphics::UpdateGyro(millisecs_t real_time, millisecs_t elapsed) { // Technically this will behave slightly differently at different time scales, // but it should be close to correct.. // tilt_pos_ *= 0.991f; - tilt_pos_ *= std::max(0.0, 1.0f - 0.01 * timescale); + tilt_pos_ *= std::max(0.0f, 1.0f - 0.01f * timescale); // Some gyros seem wonky and either give us crazy big values or consistently // offset ones. Let's keep a running tally of magnitude that slowly drops over @@ -985,16 +1070,13 @@ void Graphics::ApplyCamera(FrameDef* frame_def) { camera_->ApplyToFrameDef(frame_def); } -void Graphics::DrawWorld(Session* session, FrameDef* frame_def) { +void Graphics::DrawWorld(FrameDef* frame_def) { + assert(!g_core->HeadlessMode()); + // Draw all session contents (nodes, etc.) overlay_node_z_depth_ = -0.95f; - if (session) { - session->Draw(frame_def); - frame_def->set_benchmark_type(session->benchmark_type()); - } - if (!HeadlessMode()) { - g_bg_dynamics->Draw(frame_def); - } + g_base->app_mode->DrawWorld(frame_def); + g_base->bg_dynamics->Draw(frame_def); // Lastly draw any blotches that have been building up. DrawBlotches(frame_def); @@ -1003,32 +1085,40 @@ void Graphics::DrawWorld(Session* session, FrameDef* frame_def) { DrawBoxingGlovesTest(frame_def); } +void Graphics::DrawUI(FrameDef* frame_def) { + // Just do generic thing in our default implementation. + // Special variants like GraphicsVR may do fancier stuff here. + g_base->ui->Draw(frame_def); +} + void Graphics::BuildAndPushFrameDef() { - assert(InLogicThread()); - assert(camera_.exists()); + assert(g_base->InLogicThread()); + assert(camera_.Exists()); // We should not be building/pushing any frames until after - // app-launch-commands have been run.. - BA_PRECONDITION_FATAL(g_logic->ran_app_launch_commands()); + // app-launch-commands have been run. + BA_PRECONDITION_FATAL(g_base->logic->app_bootstrapping_complete()); // This should no longer be necessary.. WaitForRendererToExist(); - Session* session = g_logic->GetForegroundSession(); - bool session_fills_screen = session ? session->DoesFillScreen() : false; - millisecs_t real_time = GetRealTime(); + millisecs_t app_time_millisecs = g_core->GetAppTimeMillisecs(); // Store how much time this frame_def represents. - millisecs_t net_time = g_logic->master_time(); - millisecs_t elapsed = - std::min(millisecs_t{50}, net_time - last_create_frame_def_time_); - last_create_frame_def_time_ = net_time; + auto display_time_millisecs = + static_cast(g_base->logic->display_time() * 1000.0); + millisecs_t elapsed = std::min( + millisecs_t{50}, display_time_millisecs - last_create_frame_def_time_); + last_create_frame_def_time_ = display_time_millisecs; - UpdateGyro(real_time, elapsed); + // This probably should not be here. Though I guess we get the most up-to-date + // values possible this way. But it should probably live in g_input. + UpdateGyro(app_time_millisecs, elapsed); FrameDef* frame_def = GetEmptyFrameDef(); - frame_def->set_real_time(real_time); - frame_def->set_base_time(g_logic->master_time()); + frame_def->set_app_time_millisecs(app_time_millisecs); + frame_def->set_base_time( + static_cast(g_base->logic->display_time() * 1000.0)); frame_def->set_base_time_elapsed(elapsed); frame_def->set_frame_number(frame_def_count_++); @@ -1037,35 +1127,40 @@ void Graphics::BuildAndPushFrameDef() { internal_components_inited_ = true; } + // If graphics quality has changed since our last draw, inform anyone who + // wants to know. + if (last_frame_def_graphics_quality_ != frame_def->quality()) { + last_frame_def_graphics_quality_ = frame_def->quality(); + g_base->app_mode->GraphicsQualityChanged(frame_def->quality()); + } + ApplyCamera(frame_def); - // Clear to black for either progress bar or when we've got no meaningful - // session to draw. - frame_def->set_needs_clear(progress_bar_ || !session_fills_screen); - if (progress_bar_) { - UpdateAndDrawProgressBar(frame_def, real_time); + frame_def->set_needs_clear(true); + UpdateAndDrawProgressBar(frame_def, app_time_millisecs); } else { // Ok, we're drawing a real frame. - DrawWorld(session, frame_def); + bool session_fills_screen = g_base->app_mode->DoesWorldFillScreen(); - // Now some overlay stuff. - RenderPass* overlay_pass = frame_def->overlay_pass(); + frame_def->set_needs_clear(!session_fills_screen); + DrawWorld(frame_def); DrawUI(frame_def); // Let input draw anything it needs to. (touch input graphics, etc) - g_input->Draw(frame_def); + g_base->input->Draw(frame_def); + RenderPass* overlay_pass = frame_def->overlay_pass(); DrawMiscOverlays(overlay_pass); // Draw console. - if (!HeadlessMode() && g_app->console) { - g_app->console->Draw(overlay_pass); + if (!g_core->HeadlessMode() && g_base->console()) { + g_base->console()->Draw(overlay_pass); } - DrawCursor(overlay_pass, real_time); + DrawCursor(overlay_pass, app_time_millisecs); // Draw our light/shadow images to the screen if desired. DrawDebugBuffers(overlay_pass); @@ -1079,29 +1174,28 @@ void Graphics::BuildAndPushFrameDef() { c.Submit(); } - DrawFades(frame_def, real_time); + DrawFades(frame_def, app_time_millisecs); // Sanity test: If we're in VR, the only reason we should have stuff in the // flat overlay pass is if there's windows present (we want to avoid // drawing/blitting the 2d UI buffer during gameplay for efficiency). - if (IsVRMode()) { + if (g_core->IsVRMode()) { if (frame_def->GetOverlayFlatPass()->HasDrawCommands()) { - if (!g_ui->IsWindowPresent()) { - BA_LOG_ONCE( - LogLevel::kError, - "Drawing in overlay pass in VR mode without UI; shouldn't " - "happen!"); + if (!g_base->ui->IsWindowPresent()) { + BA_LOG_ONCE(LogLevel::kError, + "Drawing in overlay pass in VR mode with no UI present; " + "shouldn't happen!"); } } } - if (g_assets->GetPendingLoadCount() > 0) { + if (g_base->assets->GetPendingLoadCount() > 0) { DrawLoadDot(overlay_pass); } // Lastly, if we had anything waiting to run until the progress bar was // gone, run it. - g_python->RunCleanFrameCommands(); + RunCleanFrameCommands(); } frame_def->Finalize(); @@ -1114,7 +1208,7 @@ void Graphics::BuildAndPushFrameDef() { frame_def->set_mesh_data_destroys(mesh_data_destroys_); mesh_data_destroys_.clear(); - g_graphics_server->SetFrameDef(frame_def); + g_base->graphics_server->SetFrameDef(frame_def); // Clean up frame_defs awaiting deletion. ClearFrameDefDeleteList(); @@ -1128,8 +1222,6 @@ void Graphics::BuildAndPushFrameDef() { blotch_soft_obj_verts_.clear(); } -auto Graphics::DrawUI(FrameDef* frame_def) -> void { g_ui->Draw(frame_def); } - void Graphics::DrawBoxingGlovesTest(FrameDef* frame_def) { // Test: boxing glove. if (explicit_bool(false)) { @@ -1143,7 +1235,7 @@ void Graphics::DrawBoxingGlovesTest(FrameDef* frame_def) { c.Translate(0, 7, -3.3f); c.Scale(10, 10, 10); c.Rotate(a, 0, 0, 1); - c.DrawModel(g_assets->GetModel(SystemModelID::kBoxingGlove)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kBoxingGlove)); c.PopTransform(); c.Submit(); } @@ -1151,14 +1243,14 @@ void Graphics::DrawBoxingGlovesTest(FrameDef* frame_def) { // Beauty. if (explicit_bool(false)) { ObjectComponent c(frame_def->beauty_pass()); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kBoxingGlove)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kBoxingGlove)); c.SetReflection(ReflectionType::kSoft); c.SetReflectionScale(0.4f, 0.4f, 0.4f); c.PushTransform(); c.Translate(0.0f, 3.7f, -3.3f); c.Scale(10.0f, 10.0f, 10.0f); c.Rotate(a, 0.0f, 0.0f, 1.0f); - c.DrawModel(g_assets->GetModel(SystemModelID::kBoxingGlove)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kBoxingGlove)); c.PopTransform(); c.Submit(); } @@ -1172,7 +1264,7 @@ void Graphics::DrawBoxingGlovesTest(FrameDef* frame_def) { c.Translate(0.0f, 3.7f, -3.3f); c.Scale(10.0f, 10.0f, 10.0f); c.Rotate(a, 0.0f, 0.0f, 1.0f); - c.DrawModel(g_assets->GetModel(SystemModelID::kBoxingGlove)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kBoxingGlove)); c.PopTransform(); c.Submit(); } @@ -1187,7 +1279,7 @@ void Graphics::DrawDebugBuffers(RenderPass* pass) { c.PushTransform(); c.Translate(70, 400, kDebugImgZDepth); c.Scale(csize, csize); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1)); c.PopTransform(); c.Submit(); } @@ -1197,7 +1289,7 @@ void Graphics::DrawDebugBuffers(RenderPass* pass) { c.PushTransform(); c.Translate(70, 250, kDebugImgZDepth); c.Scale(csize, csize); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1)); c.PopTransform(); c.Submit(); } @@ -1209,18 +1301,18 @@ void Graphics::UpdateAndDrawProgressBar(FrameDef* frame_def, RenderPass* pass = frame_def->overlay_pass(); UpdateProgressBarProgress( 1.0f - - static_cast(g_assets->GetGraphicalPendingLoadCount()) + - static_cast(g_base->assets->GetGraphicalPendingLoadCount()) / static_cast(progress_bar_loads_)); DrawProgressBar(pass, 1.0f); // If we were drawing a progress bar, see if everything is now loaded.. if // so, start rendering normally next frame. - int count = g_assets->GetGraphicalPendingLoadCount(); + int count = g_base->assets->GetGraphicalPendingLoadCount(); if (count <= 0) { progress_bar_ = false; progress_bar_end_time_ = real_time; } - if (g_assets->GetPendingLoadCount() > 0) { + if (g_base->assets->GetPendingLoadCount() > 0) { DrawLoadDot(pass); } } @@ -1256,8 +1348,8 @@ void Graphics::DrawFades(FrameDef* frame_def, millisecs_t real_time) { if (fade_ <= 0) fade_ = 0.00001f; } else { fade_ = 0; - if (!was_done && fade_end_call_.exists()) { - g_logic->PushPythonCall(fade_end_call_); + if (!was_done && fade_end_call_.Exists()) { + fade_end_call_->Schedule(); fade_end_call_.Clear(); } } @@ -1275,7 +1367,8 @@ void Graphics::DrawFades(FrameDef* frame_def, millisecs_t real_time) { / static_cast(kProgressBarFadeTime)) * (1.0f - a); } - if (IsVRMode()) { + // TODO(ericf): move this to GraphicsVR. + if (g_core->IsVRMode()) { #if BA_VR_BUILD SimpleComponent c(frame_def->vr_cover_pass()); c.SetTransparent(false); @@ -1313,7 +1406,7 @@ void Graphics::DrawFades(FrameDef* frame_def, millisecs_t real_time) { float inv_a = 1.0f - a; float s = 100.0f * inv_a + 5.0f * a; c.Scale(s, s, s); - c.DrawModel(g_assets->GetModel(SystemModelID::kVRFade)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kVRFade)); c.PopTransform(); c.Submit(); #else // BA_VR_BUILD @@ -1323,7 +1416,7 @@ void Graphics::DrawFades(FrameDef* frame_def, millisecs_t real_time) { SimpleComponent c(overlay_pass); c.SetTransparent(a < 1.0f); c.SetColor(0, 0, 0, a); - c.DrawMesh(screen_mesh_.get()); + c.DrawMesh(screen_mesh_.Get()); c.Submit(); } @@ -1339,10 +1432,11 @@ void Graphics::DrawFades(FrameDef* frame_def, millisecs_t real_time) { } void Graphics::DrawCursor(RenderPass* pass, millisecs_t real_time) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); - bool can_show_cursor = g_platform->IsRunningOnDesktop(); - bool should_show_cursor = camera_->manual() || g_input->IsCursorVisible(); + bool can_show_cursor = g_core->platform->IsRunningOnDesktop(); + bool should_show_cursor = + camera_->manual() || g_base->input->IsCursorVisible(); if (g_buildconfig.hardware_cursor()) { // If we're using a hardware cursor, ship hardware cursor visibility @@ -1358,7 +1452,7 @@ void Graphics::DrawCursor(RenderPass* pass, millisecs_t real_time) { || real_time - last_cursor_visibility_event_time_ > 2000) { hardware_cursor_visible_ = new_cursor_visibility; last_cursor_visibility_event_time_ = real_time; - g_app_flavor->PushCursorUpdate(hardware_cursor_visible_); + g_base->app->PushCursorUpdate(hardware_cursor_visible_); } } else { // Draw software cursor. @@ -1366,7 +1460,7 @@ void Graphics::DrawCursor(RenderPass* pass, millisecs_t real_time) { SimpleComponent c(pass); c.SetTransparent(true); float csize = 50.0f; - c.SetTexture(g_assets->GetTexture(SystemTextureID::kCursor)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kCursor)); c.PushTransform(); // Note: we don't plug in known cursor position values here; we tell the @@ -1375,7 +1469,7 @@ void Graphics::DrawCursor(RenderPass* pass, millisecs_t real_time) { c.CursorTranslate(); c.Translate(csize * 0.44f, csize * -0.44f, kCursorZDepth); c.Scale(csize, csize); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1)); c.PopTransform(); c.Submit(); } @@ -1384,19 +1478,19 @@ void Graphics::DrawCursor(RenderPass* pass, millisecs_t real_time) { void Graphics::DrawBlotches(FrameDef* frame_def) { if (!this->blotch_verts_.empty()) { - if (!this->shadow_blotch_mesh_.exists()) + if (!this->shadow_blotch_mesh_.Exists()) this->shadow_blotch_mesh_ = Object::New(); this->shadow_blotch_mesh_->SetIndexData(Object::New( this->blotch_indices_.size(), &this->blotch_indices_[0])); this->shadow_blotch_mesh_->SetData(Object::New>( this->blotch_verts_.size(), &this->blotch_verts_[0])); SpriteComponent c(frame_def->light_shadow_pass()); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kLight)); - c.DrawMesh(this->shadow_blotch_mesh_.get()); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kLight)); + c.DrawMesh(this->shadow_blotch_mesh_.Get()); c.Submit(); } if (!this->blotch_soft_verts_.empty()) { - if (!this->shadow_blotch_soft_mesh_.exists()) + if (!this->shadow_blotch_soft_mesh_.Exists()) this->shadow_blotch_soft_mesh_ = Object::New(); this->shadow_blotch_soft_mesh_->SetIndexData(Object::New( this->blotch_soft_indices_.size(), &this->blotch_soft_indices_[0])); @@ -1404,12 +1498,12 @@ void Graphics::DrawBlotches(FrameDef* frame_def) { Object::New>(this->blotch_soft_verts_.size(), &this->blotch_soft_verts_[0])); SpriteComponent c(frame_def->light_shadow_pass()); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kLightSoft)); - c.DrawMesh(this->shadow_blotch_soft_mesh_.get()); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kLightSoft)); + c.DrawMesh(this->shadow_blotch_soft_mesh_.Get()); c.Submit(); } if (!this->blotch_soft_obj_verts_.empty()) { - if (!this->shadow_blotch_soft_obj_mesh_.exists()) { + if (!this->shadow_blotch_soft_obj_mesh_.Exists()) { this->shadow_blotch_soft_obj_mesh_ = Object::New(); } this->shadow_blotch_soft_obj_mesh_->SetIndexData( @@ -1420,8 +1514,8 @@ void Graphics::DrawBlotches(FrameDef* frame_def) { this->blotch_soft_obj_verts_.size(), &this->blotch_soft_obj_verts_[0])); SpriteComponent c(frame_def->light_pass()); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kLightSoft)); - c.DrawMesh(this->shadow_blotch_soft_obj_mesh_.get()); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kLightSoft)); + c.DrawMesh(this->shadow_blotch_soft_obj_mesh_.Get()); c.Submit(); } } @@ -1442,12 +1536,12 @@ void Graphics::ClearScreenMessageTranslations() { void Graphics::ReturnCompletedFrameDef(FrameDef* frame_def) { std::scoped_lock lock(frame_def_delete_list_mutex_); - g_graphics->frame_def_delete_list_.push_back(frame_def); + g_base->graphics->frame_def_delete_list_.push_back(frame_def); } void Graphics::AddMeshDataCreate(MeshData* d) { - assert(InLogicThread()); - assert(g_graphics); + assert(g_base->InLogicThread()); + assert(g_base->graphics); // Add this to our list of new-mesh-datas. We'll include this with our // next frame_def to have the graphics thread load before it processes @@ -1456,8 +1550,8 @@ void Graphics::AddMeshDataCreate(MeshData* d) { } void Graphics::AddMeshDataDestroy(MeshData* d) { - assert(InLogicThread()); - assert(g_graphics); + assert(g_base->InLogicThread()); + assert(g_base->graphics); // Add this to our list of delete-mesh-datas; we'll include this with our // next frame_def to have the graphics thread kill before it processes @@ -1466,20 +1560,20 @@ void Graphics::AddMeshDataDestroy(MeshData* d) { } void Graphics::EnableProgressBar(bool fade_in) { - assert(InLogicThread()); - progress_bar_loads_ = g_assets->GetGraphicalPendingLoadCount(); + assert(g_base->InLogicThread()); + progress_bar_loads_ = g_base->assets->GetGraphicalPendingLoadCount(); assert(progress_bar_loads_ >= 0); if (progress_bar_loads_ > 0) { progress_bar_ = true; progress_bar_fade_in_ = fade_in; - last_progress_bar_draw_time_ = GetRealTime(); + last_progress_bar_draw_time_ = g_core->GetAppTimeMillisecs(); last_progress_bar_start_time_ = last_progress_bar_draw_time_; progress_bar_progress_ = 0.0f; } } void Graphics::ToggleManualCamera() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); camera_->SetManual(!camera_->manual()); if (camera_->manual()) { ScreenMessage("Manual Camera On"); @@ -1489,14 +1583,14 @@ void Graphics::ToggleManualCamera() { } void Graphics::LocalCameraShake(float mag) { - assert(InLogicThread()); - if (camera_.exists()) { + assert(g_base->InLogicThread()); + if (camera_.Exists()) { camera_->Shake(mag); } } void Graphics::ToggleNetworkDebugDisplay() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); network_debug_display_enabled_ = !network_debug_display_enabled_; if (network_debug_display_enabled_) { ScreenMessage("Network Debug Display Enabled"); @@ -1506,10 +1600,10 @@ void Graphics::ToggleNetworkDebugDisplay() { } void Graphics::ToggleDebugDraw() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); debug_draw_ = !debug_draw_; - if (g_graphics_server->renderer()) { - g_graphics_server->renderer()->set_debug_draw_mode(debug_draw_); + if (g_base->graphics_server->renderer()) { + g_base->graphics_server->renderer()->set_debug_draw_mode(debug_draw_); } } @@ -1519,12 +1613,12 @@ void Graphics::WaitForRendererToExist() { // Conceivably we could hit this point before our graphics thread has created // the renderer. In that case lets wait a moment. int sleep_count = 0; - while (g_graphics_server == nullptr - || g_graphics_server->renderer() == nullptr) { + while (g_base->graphics_server == nullptr + || g_base->graphics_server->renderer() == nullptr) { BA_LOG_ONCE( LogLevel::kWarning, "BuildAndPushFrameDef() called before renderer is up; spinning..."); - Platform::SleepMS(100); + core::CorePlatform::SleepMillisecs(100); sleep_count++; if (sleep_count > 100) { throw Exception( @@ -1542,7 +1636,7 @@ void Graphics::DoDrawBlotch(std::vector* indices, std::vector* verts, const Vector3f& pos, float size, float r, float g, float b, float a) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); assert(indices && verts); // Add verts. @@ -1793,7 +1887,7 @@ void Graphics::DrawRadialMeter(MeshIndexedSimpleFull* m, float amt) { auto Graphics::ScreenMessageEntry::GetText() -> TextGroup& { assert(!translation_dirty); - if (!s_mesh_.exists()) { + if (!s_mesh_.Exists()) { s_mesh_ = Object::New(); mesh_dirty = true; } @@ -1807,9 +1901,9 @@ auto Graphics::ScreenMessageEntry::GetText() -> TextGroup& { return *s_mesh_; } -void Graphics::ScreenResize(float virtual_width, float virtual_height, - float pixel_width, float pixel_height) { - assert(InLogicThread()); +void Graphics::OnScreenSizeChange(float virtual_width, float virtual_height, + float pixel_width, float pixel_height) { + assert(g_base->InLogicThread()); res_x_virtual_ = virtual_width; res_y_virtual_ = virtual_height; res_x_ = pixel_width; @@ -1821,7 +1915,7 @@ void Graphics::ScreenResize(float virtual_width, float virtual_height, void Graphics::ScreenMessageEntry::UpdateTranslation() { if (translation_dirty) { - s_translated = g_logic->CompileResourceString( + s_translated = g_base->assets->CompileResourceString( s_raw, "Graphics::ScreenMessageEntry::UpdateTranslation"); translation_dirty = false; mesh_dirty = true; @@ -1829,20 +1923,20 @@ void Graphics::ScreenMessageEntry::UpdateTranslation() { } auto Graphics::CubeMapFromReflectionType(ReflectionType reflection_type) - -> SystemCubeMapTextureID { + -> SysCubeMapTextureID { switch (reflection_type) { case ReflectionType::kChar: - return SystemCubeMapTextureID::kReflectionChar; + return SysCubeMapTextureID::kReflectionChar; case ReflectionType::kPowerup: - return SystemCubeMapTextureID::kReflectionPowerup; + return SysCubeMapTextureID::kReflectionPowerup; case ReflectionType::kSoft: - return SystemCubeMapTextureID::kReflectionSoft; + return SysCubeMapTextureID::kReflectionSoft; case ReflectionType::kSharp: - return SystemCubeMapTextureID::kReflectionSharp; + return SysCubeMapTextureID::kReflectionSharp; case ReflectionType::kSharper: - return SystemCubeMapTextureID::kReflectionSharper; + return SysCubeMapTextureID::kReflectionSharper; case ReflectionType::kSharpest: - return SystemCubeMapTextureID::kReflectionSharpest; + return SysCubeMapTextureID::kReflectionSharpest; default: throw Exception(); } @@ -1901,27 +1995,9 @@ auto Graphics::ReflectionTypeFromString(const std::string& s) return r; } -auto Graphics::ApplyGlobals(GlobalsNode* globals) -> void { - set_floor_reflection(globals->floor_reflection()); - camera()->SetMode(globals->camera_mode()); - camera()->set_vr_offset(Vector3f(globals->vr_camera_offset())); - camera()->set_happy_thoughts_mode(globals->happy_thoughts_mode()); - set_shadow_scale(globals->shadow_scale()[0], globals->shadow_scale()[1]); - auto&& area_of_interest_bounds{globals->area_of_interest_bounds()}; - camera()->set_area_of_interest_bounds( - area_of_interest_bounds[0], area_of_interest_bounds[1], - area_of_interest_bounds[2], area_of_interest_bounds[3], - area_of_interest_bounds[4], area_of_interest_bounds[5]); - auto&& shadow_range{globals->shadow_range()}; - SetShadowRange(shadow_range[0], shadow_range[1], shadow_range[2], - shadow_range[3]); - set_shadow_offset(Vector3f(globals->shadow_offset())); - set_shadow_ortho(globals->shadow_ortho()); - set_tint(Vector3f(globals->tint())); - - set_ambient_color(Vector3f(globals->ambient_color())); - set_vignette_outer(Vector3f(globals->vignette_outer())); - set_vignette_inner(Vector3f(globals->vignette_inner())); +void Graphics::LanguageChanged() { + // Also clear translations on all screen-messages. + ClearScreenMessageTranslations(); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/graphics.h b/src/ballistica/base/graphics/graphics.h similarity index 66% rename from src/ballistica/graphics/graphics.h rename to src/ballistica/base/graphics/graphics.h index 5c04ce3c..31367ed2 100644 --- a/src/ballistica/graphics/graphics.h +++ b/src/ballistica/base/graphics/graphics.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_GRAPHICS_H_ -#define BALLISTICA_GRAPHICS_GRAPHICS_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_GRAPHICS_H_ +#define BALLISTICA_BASE_GRAPHICS_GRAPHICS_H_ #include #include @@ -10,12 +10,14 @@ #include #include -#include "ballistica/core/object.h" -#include "ballistica/math/matrix44f.h" -#include "ballistica/math/rect.h" -#include "ballistica/math/vector2f.h" +#include "ballistica/base/base.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/math/matrix44f.h" +#include "ballistica/shared/math/rect.h" +#include "ballistica/shared/math/vector2f.h" -namespace ballistica { +namespace ballistica::base { // Light/shadow res is divided by this to get pure light res. const int kLightResDiv = 4; @@ -49,9 +51,18 @@ class Graphics { Graphics(); virtual ~Graphics(); + void OnAppStart(); + void OnAppPause(); + void OnAppResume(); + void OnAppShutdown(); + void ApplyAppConfig(); + void OnScreenSizeChange(float virtual_width, float virtual_height, + float physical_width, float physical_height); + void StepDisplayTime(); + static auto IsShaderTransparent(ShadingType c) -> bool; static auto CubeMapFromReflectionType(ReflectionType reflection_type) - -> SystemCubeMapTextureID; + -> SysCubeMapTextureID; // Given a string, return a reflection type. static auto ReflectionTypeFromString(const std::string& s) -> ReflectionType; @@ -60,18 +71,19 @@ class Graphics { static auto StringFromReflectionType(ReflectionType reflectionType) -> std::string; - auto Reset() -> void; - auto BuildAndPushFrameDef() -> void; + void Reset(); + void BuildAndPushFrameDef(); - virtual auto ApplyCamera(FrameDef* frame_def) -> void; - virtual auto ApplyGlobals(GlobalsNode* globals) -> void; + virtual void ApplyCamera(FrameDef* frame_def); - // Called when the GraphicsServer's screen configuration changes. - auto ScreenResize(float virtual_width, float virtual_height, - float physical_width, float physical_height) -> void; + /// Called when the language changes. + void LanguageChanged(); + + void AddCleanFrameCommand(const Object::Ref& c); + void RunCleanFrameCommands(); // Called when the GraphicsServer has sent us a frame-def for deletion. - auto ReturnCompletedFrameDef(FrameDef* frame_def) -> void; + void ReturnCompletedFrameDef(FrameDef* frame_def); auto screen_pixel_width() const -> float { return res_x_; } auto screen_pixel_height() const -> float { return res_y_; } @@ -81,139 +93,139 @@ class Graphics { auto screen_virtual_width() const -> float { return res_x_virtual_; } auto screen_virtual_height() const -> float { return res_y_virtual_; } - auto ClearScreenMessageTranslations() -> void; + void ClearScreenMessageTranslations(); // Given a point in space, returns the shadow density that should be drawn // into the shadow pass. Does this belong somewhere else? auto GetShadowDensity(float x, float y, float z) -> float; - static auto GetSafeColor(float* r, float* g, float* b, - float target_intensity = 0.6f) -> void; + static void GetSafeColor(float* r, float* g, float* b, + float target_intensity = 0.6f); // Print a message to the on-screen list. - auto AddScreenMessage(const std::string& msg, + void AddScreenMessage(const std::string& msg, const Vector3f& color = Vector3f{1, 1, 1}, - bool top = false, Texture* texture = nullptr, - Texture* tint_texture = nullptr, + bool top = false, TextureAsset* texture = nullptr, + TextureAsset* tint_texture = nullptr, const Vector3f& tint = Vector3f{1, 1, 1}, - const Vector3f& tint2 = Vector3f{1, 1, 1}) -> void; + const Vector3f& tint2 = Vector3f{1, 1, 1}); // Fade the local screen in or out over the given time period. - auto FadeScreen(bool to, millisecs_t time, PyObject* endcall) -> void; + void FadeScreen(bool to, millisecs_t time, PyObject* endcall); - static auto DrawRadialMeter(MeshIndexedSimpleFull* m, float amt) -> void; + static void DrawRadialMeter(MeshIndexedSimpleFull* m, float amt); // Ways to add a few simple component types quickly. // (uses particle rendering for efficient batches). - auto DrawBlotch(const Vector3f& pos, float size, float r, float g, float b, - float a) -> void { + void DrawBlotch(const Vector3f& pos, float size, float r, float g, float b, + float a) { DoDrawBlotch(&blotch_indices_, &blotch_verts_, pos, size, r, g, b, a); } - auto DrawBlotchSoft(const Vector3f& pos, float size, float r, float g, - float b, float a) -> void { + void DrawBlotchSoft(const Vector3f& pos, float size, float r, float g, + float b, float a) { DoDrawBlotch(&blotch_soft_indices_, &blotch_soft_verts_, pos, size, r, g, b, a); } // Draw a soft blotch on objects; not terrain. - auto DrawBlotchSoftObj(const Vector3f& pos, float size, float r, float g, - float b, float a) -> void { + void DrawBlotchSoftObj(const Vector3f& pos, float size, float r, float g, + float b, float a) { DoDrawBlotch(&blotch_soft_obj_indices_, &blotch_soft_obj_verts_, pos, size, r, g, b, a); } // Enable progress bar drawing locally. - auto EnableProgressBar(bool fade_in) -> void; + void EnableProgressBar(bool fade_in); - auto camera() -> Camera* { return camera_.get(); } - auto ToggleManualCamera() -> void; - auto LocalCameraShake(float intensity) -> void; - auto ToggleDebugDraw() -> void; + auto camera() -> Camera* { return camera_.Get(); } + void ToggleManualCamera(); + void LocalCameraShake(float intensity); + void ToggleDebugDraw(); auto network_debug_info_display_enabled() const -> bool { return network_debug_display_enabled_; } - auto ToggleNetworkDebugDisplay() -> void; - auto SetGyroEnabled(bool enable) -> void; + void ToggleNetworkDebugDisplay(); + void SetGyroEnabled(bool enable); auto floor_reflection() const -> bool { - assert(InLogicThread()); + assert(g_base->InLogicThread()); return floor_reflection_; } - auto set_floor_reflection(bool val) -> void { - assert(InLogicThread()); + void set_floor_reflection(bool val) { + assert(g_base->InLogicThread()); floor_reflection_ = val; } - auto set_shadow_offset(const Vector3f& val) -> void { - assert(InLogicThread()); + void set_shadow_offset(const Vector3f& val) { + assert(g_base->InLogicThread()); shadow_offset_ = val; } - auto set_shadow_scale(float x, float y) -> void { - assert(InLogicThread()); + void set_shadow_scale(float x, float y) { + assert(g_base->InLogicThread()); shadow_scale_.x = x; shadow_scale_.y = y; } - auto set_shadow_ortho(bool o) -> void { - assert(InLogicThread()); + void set_shadow_ortho(bool o) { + assert(g_base->InLogicThread()); shadow_ortho_ = o; } auto tint() -> const Vector3f& { return tint_; } - auto set_tint(const Vector3f& val) -> void { - assert(InLogicThread()); + void set_tint(const Vector3f& val) { + assert(g_base->InLogicThread()); tint_ = val; } - auto set_ambient_color(const Vector3f& val) -> void { - assert(InLogicThread()); + void set_ambient_color(const Vector3f& val) { + assert(g_base->InLogicThread()); ambient_color_ = val; } - auto set_vignette_outer(const Vector3f& val) -> void { - assert(InLogicThread()); + void set_vignette_outer(const Vector3f& val) { + assert(g_base->InLogicThread()); vignette_outer_ = val; } - auto set_vignette_inner(const Vector3f& val) -> void { - assert(InLogicThread()); + void set_vignette_inner(const Vector3f& val) { + assert(g_base->InLogicThread()); vignette_inner_ = val; } auto shadow_offset() const -> const Vector3f& { - assert(InLogicThread()); + assert(g_base->InLogicThread()); return shadow_offset_; } auto shadow_scale() const -> const Vector2f& { - assert(InLogicThread()); + assert(g_base->InLogicThread()); return shadow_scale_; } auto tint() const -> const Vector3f& { - assert(InLogicThread()); + assert(g_base->InLogicThread()); return tint_; } auto ambient_color() const -> const Vector3f& { - assert(InLogicThread()); + assert(g_base->InLogicThread()); return ambient_color_; } auto vignette_outer() const -> const Vector3f& { - assert(InLogicThread()); + assert(g_base->InLogicThread()); return vignette_outer_; } auto vignette_inner() const -> const Vector3f& { - assert(InLogicThread()); + assert(g_base->InLogicThread()); return vignette_inner_; } auto shadow_ortho() const -> bool { - assert(InLogicThread()); + assert(g_base->InLogicThread()); return shadow_ortho_; } - auto SetShadowRange(float lower_bottom, float lower_top, float upper_bottom, - float upper_top) -> void; - auto ReleaseFadeEndCommand() -> void; - auto set_show_fps(bool val) -> void { show_fps_ = val; } - auto set_show_ping(bool val) -> void { show_ping_ = val; } + void SetShadowRange(float lower_bottom, float lower_top, float upper_bottom, + float upper_top); + void ReleaseFadeEndCommand(); + void set_show_fps(bool val) { show_fps_ = val; } + void set_show_ping(bool val) { show_ping_ = val; } // FIXME - move to graphics_server - auto set_tv_border(bool val) -> void { - assert(InLogicThread()); + void set_tv_border(bool val) { + assert(g_base->InLogicThread()); tv_border_ = val; } auto tv_border() const -> bool { - assert(InLogicThread()); + assert(g_base->InLogicThread()); return tv_border_; } @@ -226,8 +238,8 @@ class Graphics { // This should be called before/after drawing each node to keep the value // incrementing. - auto PreNodeDraw() -> void { fetched_overlay_node_z_depth_ = false; } - auto PostNodeDraw() -> void { + void PreNodeDraw() { fetched_overlay_node_z_depth_ = false; } + void PostNodeDraw() { if (fetched_overlay_node_z_depth_) { overlay_node_z_depth_ *= 0.99f; } @@ -258,73 +270,76 @@ class Graphics { assert(has_supports_high_quality_graphics_value_); return supports_high_quality_graphics_; } - auto SetSupportsHighQualityGraphics(bool s) -> void; + + void SetSupportsHighQualityGraphics(bool s); auto has_supports_high_quality_graphics_value() const -> bool { return has_supports_high_quality_graphics_value_; } - auto set_internal_components_inited(bool val) -> void { + + void set_internal_components_inited(bool val) { internal_components_inited_ = val; } - auto set_gyro_vals(const Vector3f& vals) -> void { gyro_vals_ = vals; } + void set_gyro_vals(const Vector3f& vals) { gyro_vals_ = vals; } auto show_net_info() const -> bool { return show_net_info_; } - auto set_show_net_info(bool val) -> void { show_net_info_ = val; } + void set_show_net_info(bool val) { show_net_info_ = val; } auto GetDebugGraph(const std::string& name, bool smoothed) -> NetGraph*; // Used by meshes. - auto AddMeshDataCreate(MeshData* d) -> void; - auto AddMeshDataDestroy(MeshData* d) -> void; + void AddMeshDataCreate(MeshData* d); + void AddMeshDataDestroy(MeshData* d); // For debugging: ensures that only transparent or opaque components // are submitted while enabled. auto drawing_transparent_only() const -> bool { return drawing_transparent_only_; } - auto set_drawing_transparent_only(bool val) -> void { + void set_drawing_transparent_only(bool val) { drawing_transparent_only_ = val; } + virtual void DrawUI(FrameDef* frame_def); auto drawing_opaque_only() const -> bool { return drawing_opaque_only_; } - auto set_drawing_opaque_only(bool val) -> void { drawing_opaque_only_ = val; } + void set_drawing_opaque_only(bool val) { drawing_opaque_only_ = val; } - // Handle testing values from _ba.value_test() + // Handle testing values from _baclassic.value_test() virtual auto ValueTest(const std::string& arg, double* absval, double* deltaval, double* outval) -> bool; - virtual auto DrawUI(FrameDef* frame_def) -> void; - virtual auto DrawWorld(Session* session, FrameDef* frame_def) -> void; + virtual void DrawWorld(FrameDef* frame_def); - auto set_camera_shake_disabled(bool disabled) -> void { + void set_camera_shake_disabled(bool disabled) { camera_shake_disabled_ = disabled; } auto camera_shake_disabled() const { return camera_shake_disabled_; } - auto set_camera_gyro_explicitly_disabled(bool disabled) -> void { + void set_camera_gyro_explicitly_disabled(bool disabled) { camera_gyro_explicitly_disabled_ = disabled; } private: class ScreenMessageEntry; - auto DrawBoxingGlovesTest(FrameDef* frame_def) -> void; - auto DrawBlotches(FrameDef* frame_def) -> void; - auto DrawCursor(RenderPass* pass, millisecs_t real_time) -> void; - auto DrawFades(FrameDef* frame_def, millisecs_t real_time) -> void; - auto DrawDebugBuffers(RenderPass* pass) -> void; - auto WaitForRendererToExist() -> void; + void DrawBoxingGlovesTest(FrameDef* frame_def); + void DrawBlotches(FrameDef* frame_def); + void DrawCursor(RenderPass* pass, millisecs_t real_time); + void DrawFades(FrameDef* frame_def, millisecs_t real_time); + void DrawDebugBuffers(RenderPass* pass); + void WaitForRendererToExist(); - auto UpdateAndDrawProgressBar(FrameDef* frame_def, millisecs_t real_time) - -> void; - auto DoDrawBlotch(std::vector* indices, + void UpdateAndDrawProgressBar(FrameDef* frame_def, millisecs_t real_time); + void DoDrawBlotch(std::vector* indices, std::vector* verts, const Vector3f& pos, - float size, float r, float g, float b, float a) -> void; + float size, float r, float g, float b, float a); auto GetEmptyFrameDef() -> FrameDef*; - auto InitInternalComponents(FrameDef* frame_def) -> void; - auto DrawMiscOverlays(RenderPass* pass) -> void; - auto DrawLoadDot(RenderPass* pass) -> void; - auto ClearFrameDefDeleteList() -> void; - auto DrawProgressBar(RenderPass* pass, float opacity) -> void; - auto UpdateProgressBarProgress(float target) -> void; - auto UpdateGyro(millisecs_t real_time, millisecs_t elapsed) -> void; + void InitInternalComponents(FrameDef* frame_def); + void DrawMiscOverlays(RenderPass* pass); + void DrawLoadDot(RenderPass* pass); + void ClearFrameDefDeleteList(); + void DrawProgressBar(RenderPass* pass, float opacity); + void UpdateProgressBarProgress(float target); + void UpdateGyro(millisecs_t real_time, millisecs_t elapsed); + GraphicsQuality last_frame_def_graphics_quality_{GraphicsQuality::kUnset}; bool drawing_transparent_only_{}; bool drawing_opaque_only_{}; + std::list> clean_frame_commands_; std::vector mesh_data_creates_; std::vector mesh_data_destroys_; bool has_supports_high_quality_graphics_value_{}; @@ -377,7 +392,7 @@ class Graphics { bool show_net_info_{}; bool tv_border_{}; bool floor_reflection_{}; - std::map > debug_graphs_; + std::map> debug_graphs_; std::mutex frame_def_delete_list_mutex_; std::vector frame_def_delete_list_; bool debug_draw_{}; @@ -420,6 +435,6 @@ class Graphics { millisecs_t last_suppress_gyro_time_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_GRAPHICS_H_ +#endif // BALLISTICA_BASE_GRAPHICS_GRAPHICS_H_ diff --git a/src/ballistica/graphics/graphics_server.cc b/src/ballistica/base/graphics/graphics_server.cc similarity index 69% rename from src/ballistica/graphics/graphics_server.cc rename to src/ballistica/base/graphics/graphics_server.cc index f0abe83f..8c21647c 100644 --- a/src/ballistica/graphics/graphics_server.cc +++ b/src/ballistica/base/graphics/graphics_server.cc @@ -1,24 +1,24 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/graphics_server.h" +#include "ballistica/base/graphics/graphics_server.h" -#include "ballistica/core/thread.h" -#include "ballistica/graphics/gl/renderer_gl.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/graphics/gl/renderer_gl.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/shared/foundation/event_loop.h" // FIXME: clear out this conditional stuff. #if BA_SDL_BUILD -#include "ballistica/platform/sdl/sdl_app.h" +#include "ballistica/base/app/sdl_app.h" #else -#include "ballistica/app/app_flavor.h" -#include "ballistica/assets/assets.h" -#include "ballistica/graphics/frame_def.h" -#include "ballistica/graphics/mesh/mesh_data.h" -#include "ballistica/graphics/renderer.h" -#include "ballistica/platform/platform.h" +#include "ballistica/base/app/app.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/graphics/mesh/mesh_data.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/base/graphics/support/frame_def.h" +#include "ballistica/core/platform/core_platform.h" #endif -namespace ballistica { +namespace ballistica::base { #if BA_OSTYPE_MACOS && BA_XCODE_BUILD && !BA_XCODE_NEW_PROJECT void GraphicsServer::FullscreenCheck() { @@ -30,24 +30,23 @@ void GraphicsServer::FullscreenCheck() { } #endif -GraphicsServer::GraphicsServer() : thread_(g_main_thread) { - // We're a singleton; make sure we don't already exist. - assert(g_graphics_server == nullptr); +GraphicsServer::GraphicsServer() : event_loop_(g_core->main_event_loop()) {} - // For janky old non-event-push mode, just fall back on a timer for rendering. - if (!g_platform->IsEventPushMode()) { - render_timer_ = this->thread()->NewTimer( - 1000 / 60, true, NewLambdaRunnable([this] { TryRender(); })); - } -} - -GraphicsServer::~GraphicsServer() { assert(g_graphics); } +GraphicsServer::~GraphicsServer() { assert(g_base->graphics); } void GraphicsServer::SetRenderHold() { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); render_hold_++; } +void GraphicsServer::OnMainThreadStartApp() { + // For janky old non-event-push mode, just fall back on a timer for rendering. + if (!g_core->platform->IsEventPushMode()) { + render_timer_ = this->event_loop()->NewTimer( + 1000 / 60, true, NewLambdaRunnable([this] { TryRender(); })); + } +} + void GraphicsServer::SetFrameDef(FrameDef* framedef) { // Note: we're just setting the framedef directly here // even though this gets called from the logic thread. @@ -55,58 +54,72 @@ void GraphicsServer::SetFrameDef(FrameDef* framedef) { // event list, but currently we spin-lock waiting for new // frames to appear which would prevent that from working; // we would need to change that code. - assert(frame_def_ == nullptr); - frame_def_ = framedef; + { + std::scoped_lock llock(frame_def_mutex_); + assert(frame_def_ == nullptr); + frame_def_ = framedef; + } } auto GraphicsServer::GetRenderFrameDef() -> FrameDef* { - assert(InGraphicsThread()); - millisecs_t real_time = GetRealTime(); + assert(g_base->InGraphicsThread()); + millisecs_t app_time = g_core->GetAppTimeMillisecs(); if (!renderer_) { return nullptr; } - // If the app says it's minimized minimized, don't do anything. + // If the app says it's minimized, don't do anything. // (on iOS we'll get shut down if we make GL calls in this state, etc) - if (g_app_flavor->paused()) { + if (g_base->app->paused()) { return nullptr; } // Do some incremental loading every time we try to render. - g_assets->RunPendingGraphicsLoads(); + g_base->assets->RunPendingGraphicsLoads(); // Spin and wait for a short bit for a frame_def to appear. If it does, we // grab it, render it, and also message the logic thread to start generating // another one. while (true) { - if (frame_def_) { - FrameDef* frame_def = frame_def_; - frame_def_ = nullptr; - - // Tell the logic thread we're ready for the next frame_def so it can - // start building it while we render this one. - g_logic->PushFrameDefRequest(); + FrameDef* frame_def{}; + { + std::scoped_lock llock(frame_def_mutex_); + if (frame_def_) { + frame_def = frame_def_; + frame_def_ = nullptr; + } + } + if (frame_def) { + // As soon as we start working on rendering a frame, ask the logic + // thread to start working on the next one for us. Keeps things nice and + // pipelined. + g_base->logic->event_loop()->PushCall([] { g_base->logic->Draw(); }); return frame_def; } // If there's no frame_def for us, sleep for a bit and wait for it. // But if we've been waiting for too long, give up. - // On some platforms such as android, this frame will still get flipped - // whether we draw in it or not, so we really dont want to not draw if we - // can help it. - millisecs_t t = GetRealTime() - real_time; + // On some platforms such as Android, this frame will still get flipped + // whether we draw in it or not, so we *really* want to not skip drawing + // if we can help it. + millisecs_t t = g_core->GetAppTimeMillisecs() - app_time; if (t >= 1000) { + if (g_buildconfig.debug_build()) { + Log(LogLevel::kWarning, + "GraphicsServer: aborting GetRenderFrameDef after " + + std::to_string(t) + "ms."); + } break; // Fail. } - Platform::SleepMS(2); + core::CorePlatform::SleepMillisecs(1); } return nullptr; } // Runs any mesh updates contained in the frame-def. void GraphicsServer::RunFrameDefMeshUpdates(FrameDef* frame_def) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); // Run any mesh-data creates/destroys included with this frame_def. for (auto&& i : frame_def->mesh_data_creates()) { @@ -123,7 +136,7 @@ void GraphicsServer::RunFrameDefMeshUpdates(FrameDef* frame_def) { // Renders shadow passes and other common parts of a frame_def. void GraphicsServer::PreprocessRenderFrameDef(FrameDef* frame_def) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); // Now let the renderer do any preprocess passes (shadows, etc). assert(renderer_); @@ -149,12 +162,12 @@ void GraphicsServer::FinishRenderFrameDef(FrameDef* frame_def) { // Let the app know a frame render is complete (it may need to do a // swap/etc). - g_app_flavor->DidFinishRenderingFrame(frame_def); + g_base->app->DidFinishRenderingFrame(frame_def); } } void GraphicsServer::TryRender() { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); if (FrameDef* frame_def = GetRenderFrameDef()) { // Note: we always run mesh updates contained in the framedef @@ -173,40 +186,40 @@ void GraphicsServer::TryRender() { FinishRenderFrameDef(frame_def); } - // Send this frame_def back to the logic thread for deletion. - g_graphics->ReturnCompletedFrameDef(frame_def); + // Send this frame_def back to the logic thread for deletion or recycling. + g_base->graphics->ReturnCompletedFrameDef(frame_def); } } // Reload all media (for debugging/benchmarking purposes). void GraphicsServer::ReloadMedia() { - assert(InMainThread()); + assert(g_base->InGraphicsThread()); // Immediately unload all renderer data here in this thread. if (renderer_) { - g_assets->UnloadRendererBits(true, true); + g_base->assets->UnloadRendererBits(true, true); } // Set a render-hold so we ignore all frame_defs up until the point at which // we receive the corresponding remove-hold. // (At which point subsequent frame-defs will be be progress-bar frame_defs so // we won't hitch if we actually render them.) - assert(g_graphics_server); + assert(g_base->graphics_server); SetRenderHold(); // Now tell the logic thread to kick off loads for everything, flip on // progress bar drawing, and then tell the graphics thread to stop ignoring // frame-defs. - g_logic->thread()->PushCall([this] { - g_assets->MarkAllAssetsForLoad(); - g_graphics->EnableProgressBar(false); + g_base->logic->event_loop()->PushCall([this] { + g_base->assets->MarkAllAssetsForLoad(); + g_base->graphics->EnableProgressBar(false); PushRemoveRenderHoldCall(); }); } // Call when renderer context has been lost. void GraphicsServer::RebuildLostContext() { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); if (!renderer_) { Log(LogLevel::kError, "No renderer on GraphicsServer::_rebuildContext."); @@ -217,8 +230,8 @@ void GraphicsServer::RebuildLostContext() { // down itself. set_renderer_context_lost(true); - // Unload all texture and model data here in the render thread. - g_assets->UnloadRendererBits(true, true); + // Unload all texture and mesh data here in the render thread. + g_base->assets->UnloadRendererBits(true, true); // Also unload dynamic meshes. for (auto&& i : mesh_datas_) { @@ -249,27 +262,27 @@ void GraphicsServer::RebuildLostContext() { // Now tell the logic thread to kick off loads for everything, flip on // progress bar drawing, and then tell the graphics thread to stop ignoring // frame-defs. - g_logic->thread()->PushCall([this] { - g_assets->MarkAllAssetsForLoad(); - g_graphics->EnableProgressBar(false); + g_base->logic->event_loop()->PushCall([this] { + g_base->assets->MarkAllAssetsForLoad(); + g_base->graphics->EnableProgressBar(false); PushRemoveRenderHoldCall(); }); } -void GraphicsServer::SetScreen(bool fullscreen, int width, int height, - TextureQuality texture_quality_requested, - GraphicsQuality graphics_quality_requested, - const std::string& android_res) { - assert(InGraphicsThread()); +void GraphicsServer::SetScreen( + bool fullscreen, int width, int height, + TextureQualityRequest texture_quality_requested, + GraphicsQualityRequest graphics_quality_requested, + const std::string& android_res) { + assert(g_base->InGraphicsThread()); - // If we know what we support, filter out requests we don't support - // (will keep us from rebuilding contexts due to our requested and actual - // values not lining up). - if (g_graphics->has_supports_high_quality_graphics_value()) { - if (!g_graphics->supports_high_quality_graphics() - && (graphics_quality_requested == GraphicsQuality::kHigh - || graphics_quality_requested == GraphicsQuality::kHigher)) { - graphics_quality_requested = GraphicsQuality::kMedium; + // If we know what we support, filter request types to those we support. + // (will keep us from rebuilding contexts if request type is flipping between + // different types we don't support). + if (g_base->graphics->has_supports_high_quality_graphics_value()) { + if (!g_base->graphics->supports_high_quality_graphics() + && graphics_quality_requested > GraphicsQualityRequest::kMedium) { + graphics_quality_requested = GraphicsQualityRequest::kMedium; } } @@ -280,18 +293,20 @@ void GraphicsServer::SetScreen(bool fullscreen, int width, int height, bool do_toggle_fs = false; bool do_set_existing_fs = false; - if (HeadlessMode()) { + if (g_core->HeadlessMode()) { // We don't actually make or update a renderer in headless, but we // still need to set our list of supported textures types/etc. to avoid // complaints. std::list c_types; SetTextureCompressionTypes(c_types); - quality_requested_ = quality_actual_ = GraphicsQuality::kLow; + graphics_quality_requested_ = GraphicsQualityRequest::kLow; + graphics_quality_ = GraphicsQuality::kLow; graphics_quality_set_ = true; - texture_quality_requested_ = texture_quality_actual_ = TextureQuality::kLow; + texture_quality_requested_ = TextureQualityRequest::kLow; + texture_quality_ = TextureQuality::kLow; texture_quality_set_ = true; } else { - // OK - starting in SDL2 we never pass in specific resolution requests.. + // OK - starting in SDL2 we never pass in specific resolution requests. // 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) @@ -306,7 +321,7 @@ void GraphicsServer::SetScreen(bool fullscreen, int width, int height, // We need a full renderer reload if quality values have changed. need_renderer_reload = ((texture_quality_requested_ != texture_quality_requested) - || (quality_requested_ != graphics_quality_requested) + || (graphics_quality_requested_ != graphics_quality_requested) || !texture_quality_set() || !graphics_quality_set()); // This stuff requires a full context rebuild. @@ -343,8 +358,8 @@ void GraphicsServer::SetScreen(bool fullscreen, int width, int height, #if BA_OSTYPE_MACOS && BA_XCODE_BUILD && BA_SDL_BUILD if (create_fullscreen_check_timer) { - thread()->NewTimer(1000, false, - NewLambdaRunnable([this] { FullscreenCheck(); })); + event_loop()->NewTimer(1000, false, + NewLambdaRunnable([this] { FullscreenCheck(); })); } #endif // BA_OSTYPE_MACOS @@ -352,12 +367,13 @@ void GraphicsServer::SetScreen(bool fullscreen, int width, int height, } // The first time we complete setting up our screen, we send a message - // back to the logic thread to complete the init process.. (they can't start + // back to the logic thread to complete the init process. (they can't start // loading graphics and things until we have our context set up so we know // what types of textures to load, etc) if (!initial_screen_created_) { initial_screen_created_ = true; - g_logic->PushInitialScreenCreatedCall(); + g_base->logic->event_loop()->PushCall( + [] { g_base->logic->OnInitialScreenCreated(); }); } } @@ -367,13 +383,13 @@ void GraphicsServer::SetScreen(bool fullscreen, int width, int height, void GraphicsServer::HandleFullContextScreenRebuild( bool need_full_context_rebuild, bool fullscreen, int width, int height, - GraphicsQuality graphics_quality_requested, - TextureQuality texture_quality_requested) { + GraphicsQualityRequest graphics_quality_requested, + TextureQualityRequest texture_quality_requested) { // Unload renderer-specific data (display-lists, internal textures, etc) if (renderer_) { - // Unload all textures and models.. these will be reloaded as-needed - // automatically for the new context.. - g_assets->UnloadRendererBits(true, true); + // Unload all textures and meshes. These will be reloaded as-needed + // automatically for the new context. + g_base->assets->UnloadRendererBits(true, true); // Also unload all dynamic meshes. for (auto&& i : mesh_datas_) { @@ -404,8 +420,10 @@ void GraphicsServer::HandleFullContextScreenRebuild( UpdateVirtualScreenRes(); // Inform the logic thread of the latest values. - g_logic->PushScreenResizeCall(res_x_virtual_, res_y_virtual_, res_x_, - res_y_); + g_base->logic->event_loop()->PushCall( + [vx = res_x_virtual_, vy = res_y_virtual_, x = res_x_, y = res_y_] { + g_base->logic->OnScreenSizeChange(vx, vy, x, y); + }); } if (!renderer_) { @@ -418,29 +436,60 @@ void GraphicsServer::HandleFullContextScreenRebuild( // whatnot. renderer_->CheckCapabilities(); - // Update graphics quality. - quality_requested_ = graphics_quality_requested; - if (quality_requested_ == GraphicsQuality::kAuto) { - quality_actual_ = renderer_->GetAutoGraphicsQuality(); - } else { - quality_actual_ = quality_requested_; + // Update graphics quality based on request. + graphics_quality_requested_ = graphics_quality_requested; + switch (graphics_quality_requested_) { + case GraphicsQualityRequest::kLow: + graphics_quality_ = GraphicsQuality::kLow; + break; + case GraphicsQualityRequest::kMedium: + graphics_quality_ = GraphicsQuality::kMedium; + break; + case GraphicsQualityRequest::kHigh: + graphics_quality_ = GraphicsQuality::kHigh; + break; + case GraphicsQualityRequest::kHigher: + graphics_quality_ = GraphicsQuality::kHigher; + break; + case GraphicsQualityRequest::kAuto: + graphics_quality_ = renderer_->GetAutoGraphicsQuality(); + break; + default: + Log(LogLevel::kError, + "Unhandled GraphicsQualityRequest value: " + + std::to_string(static_cast(graphics_quality_requested_))); + graphics_quality_ = GraphicsQuality::kLow; } // If we don't support high quality graphics, make sure we're no higher than // medium. - BA_PRECONDITION(g_graphics->has_supports_high_quality_graphics_value()); - if (!g_graphics->supports_high_quality_graphics() - && quality_actual_ >= GraphicsQuality::kHigh) { - quality_actual_ = GraphicsQuality::kMedium; + BA_PRECONDITION(g_base->graphics->has_supports_high_quality_graphics_value()); + if (!g_base->graphics->supports_high_quality_graphics() + && graphics_quality_ > GraphicsQuality::kMedium) { + graphics_quality_ = GraphicsQuality::kMedium; } graphics_quality_set_ = true; - // Update texture quality. + // Update texture quality based on request. texture_quality_requested_ = texture_quality_requested; - if (texture_quality_requested_ == TextureQuality::kAuto) { - texture_quality_actual_ = renderer_->GetAutoTextureQuality(); - } else { - texture_quality_actual_ = texture_quality_requested_; + switch (texture_quality_requested_) { + case TextureQualityRequest::kLow: + texture_quality_ = TextureQuality::kLow; + break; + case TextureQualityRequest::kMedium: + texture_quality_ = TextureQuality::kMedium; + break; + case TextureQualityRequest::kHigh: + texture_quality_ = TextureQuality::kHigh; + break; + case TextureQualityRequest::kAuto: + texture_quality_ = renderer_->GetAutoTextureQuality(); + break; + default: + Log(LogLevel::kError, + "Unhandled TextureQualityRequest value: " + + std::to_string(static_cast(texture_quality_requested_))); + texture_quality_ = TextureQuality::kLow; } texture_quality_set_ = true; @@ -463,10 +512,10 @@ void GraphicsServer::HandleFullContextScreenRebuild( // Now tell the logic thread to kick off loads for everything, flip on // progress bar drawing, and then tell the graphics thread to stop ignoring // frame-defs. - g_logic->thread()->PushCall([this] { - g_assets->MarkAllAssetsForLoad(); - g_graphics->set_internal_components_inited(false); - g_graphics->EnableProgressBar(false); + g_base->logic->event_loop()->PushCall([this] { + g_base->assets->MarkAllAssetsForLoad(); + g_base->graphics->set_internal_components_inited(false); + g_base->graphics->EnableProgressBar(false); PushRemoveRenderHoldCall(); }); } @@ -488,10 +537,10 @@ void GraphicsServer::CalcVirtualRes(float* x, float* y) { } void GraphicsServer::UpdateVirtualScreenRes() { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); // In vr mode our virtual res is independent of our screen size. // (since it gets drawn to an overlay) - if (IsVRMode()) { + if (g_core->IsVRMode()) { res_x_virtual_ = kBaseVirtualResX; res_y_virtual_ = kBaseVirtualResY; } else { @@ -501,8 +550,8 @@ void GraphicsServer::UpdateVirtualScreenRes() { } } -void GraphicsServer::VideoResize(float h, float v) { - assert(InGraphicsThread()); +void GraphicsServer::SetScreenResolution(float h, float v) { + assert(g_base->InGraphicsThread()); if (target_res_x_ == h && target_res_y_ == v) { return; @@ -514,11 +563,16 @@ void GraphicsServer::VideoResize(float h, float v) { res_y_ = v; UpdateVirtualScreenRes(); - // Inform the logic thread of the latest values. - g_logic->PushScreenResizeCall(res_x_virtual_, res_y_virtual_, res_x_, res_y_); + // Inform renderer of the change. if (renderer_) { renderer_->ScreenSizeChanged(); } + + // Inform logic thread of the change. + g_base->logic->event_loop()->PushCall( + [vx = res_x_virtual_, vy = res_y_virtual_, x = res_x_, y = res_y_] { + g_base->logic->OnScreenSizeChange(vx, vy, x, y); + }); } // FIXME: Shouldn't have android-specific code in here. @@ -539,7 +593,7 @@ void GraphicsServer::HandlePushAndroidRes(const std::string& android_res) { } else { fin_res = android_res; } - g_platform->AndroidSetResString(fin_res); + g_core->platform->AndroidSetResString(fin_res); } } @@ -554,7 +608,7 @@ void GraphicsServer::HandleFullscreenToggling(bool do_set_existing_fs, #if BA_SDL2_BUILD bool rift_vr_mode = false; #if BA_RIFT_BUILD - if (IsVRMode()) { + if (g_core->IsVRMode()) { rift_vr_mode = true; } #endif // BA_RIFT_BUILD @@ -628,7 +682,7 @@ void GraphicsServer::SetOrthoProjection(float left, float right, float bottom, void GraphicsServer::SetCamera(const Vector3f& eye, const Vector3f& target, const Vector3f& up_vector) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); // Reset the modelview stack. model_view_stack_.clear(); @@ -674,7 +728,7 @@ void GraphicsServer::SetCamera(const Vector3f& eye, const Vector3f& target, #pragma ide diagnostic ignored "ConstantConditionsOC" void GraphicsServer::UpdateCamOrientMatrix() { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); if (cam_orient_matrix_dirty_) { cam_orient_matrix_ = kMatrix44fIdentity; Vector3f to_cam = cam_pos_ - cam_target_; @@ -704,23 +758,24 @@ void GraphicsServer::UpdateCamOrientMatrix() { #pragma mark PushCalls -void GraphicsServer::PushSetScreenCall(bool fullscreen, int width, int height, - TextureQuality texture_quality, - GraphicsQuality graphics_quality, - const std::string& android_res) { - thread()->PushCall([=] { - SetScreen(fullscreen, width, height, texture_quality, graphics_quality, - android_res); +void GraphicsServer::PushSetScreenCall( + bool fullscreen, int width, int height, + TextureQualityRequest texture_quality_request, + GraphicsQualityRequest graphics_quality_request, + const std::string& android_res) { + event_loop()->PushCall([=] { + SetScreen(fullscreen, width, height, texture_quality_request, + graphics_quality_request, android_res); }); } void GraphicsServer::PushReloadMediaCall() { - thread()->PushCall([this] { ReloadMedia(); }); + event_loop()->PushCall([this] { ReloadMedia(); }); } void GraphicsServer::PushSetScreenGammaCall(float gamma) { - thread()->PushCall([this, gamma] { - assert(InGraphicsThread()); + event_loop()->PushCall([this, gamma] { + assert(g_base->InGraphicsThread()); if (!renderer_) { return; } @@ -729,8 +784,8 @@ void GraphicsServer::PushSetScreenGammaCall(float gamma) { } void GraphicsServer::PushSetScreenPixelScaleCall(float pixel_scale) { - thread()->PushCall([this, pixel_scale] { - assert(InGraphicsThread()); + event_loop()->PushCall([this, pixel_scale] { + assert(g_base->InGraphicsThread()); if (!renderer_) { return; } @@ -739,8 +794,8 @@ void GraphicsServer::PushSetScreenPixelScaleCall(float pixel_scale) { } void GraphicsServer::PushSetVSyncCall(bool sync, bool auto_sync) { - thread()->PushCall([this, sync, auto_sync] { - assert(InGraphicsThread()); + event_loop()->PushCall([this, sync, auto_sync] { + assert(g_base->InGraphicsThread()); #if BA_SDL_BUILD @@ -749,7 +804,7 @@ void GraphicsServer::PushSetVSyncCall(bool sync, bool auto_sync) { if (g_buildconfig.sdl_build()) { // Even if we were built with SDL, we may not be running in sdl-app-mode // (for instance, Rift in VR mode). Only do this if we're an sdl app. - if (auto app = dynamic_cast(g_app_flavor)) { + if (auto app = dynamic_cast(g_base->app)) { v_sync_ = sync; auto_vsync_ = auto_sync; if (gl_context_) { @@ -759,7 +814,7 @@ void GraphicsServer::PushSetVSyncCall(bool sync, bool auto_sync) { gl_context_->SetVSync(v_sync_); } } else { - Log(LogLevel::kError, "Got SetVSyncCall with no gl context."); + Log(LogLevel::kError, "Got SetVSyncCall with no gl context_ref."); } } } @@ -768,20 +823,23 @@ void GraphicsServer::PushSetVSyncCall(bool sync, bool auto_sync) { } void GraphicsServer::PushComponentUnloadCall( - const std::vector*>& components) { - thread()->PushCall([this, components] { - // Unload all components we were passed. + const std::vector*>& components) { + event_loop()->PushCall([components] { + // Unload the components. for (auto&& i : components) { (**i).Unload(); } - // ..and then ship these pointers back to the logic thread so it can free - // the references. - g_logic->PushFreeAssetComponentRefsCall(components); + // Then kick them over to the logic thread for deletion. + g_base->logic->event_loop()->PushCall([components] { + for (auto&& i : components) { + delete i; + } + }); }); } void GraphicsServer::PushRemoveRenderHoldCall() { - thread()->PushCall([this] { + event_loop()->PushCall([this] { assert(render_hold_); render_hold_--; if (render_hold_ < 0) { @@ -791,4 +849,4 @@ void GraphicsServer::PushRemoveRenderHoldCall() { }); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/graphics_server.h b/src/ballistica/base/graphics/graphics_server.h similarity index 67% rename from src/ballistica/graphics/graphics_server.h rename to src/ballistica/base/graphics/graphics_server.h index 061da29c..91ae7a8e 100644 --- a/src/ballistica/graphics/graphics_server.h +++ b/src/ballistica/base/graphics/graphics_server.h @@ -1,40 +1,50 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_GRAPHICS_SERVER_H_ -#define BALLISTICA_GRAPHICS_GRAPHICS_SERVER_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_GRAPHICS_SERVER_H_ +#define BALLISTICA_BASE_GRAPHICS_GRAPHICS_SERVER_H_ #include #include +#include #include #include -#include "ballistica/ballistica.h" -#include "ballistica/core/object.h" -#include "ballistica/math/matrix44f.h" +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/math/matrix44f.h" -namespace ballistica { +namespace ballistica::base { // Runs in the main thread and renders frame_defs shipped to it by the // Graphics class GraphicsServer { public: GraphicsServer(); - auto PushSetScreenGammaCall(float gamma) -> void; - auto PushSetScreenPixelScaleCall(float pixel_scale) -> void; - auto PushSetVSyncCall(bool sync, bool auto_sync) -> void; - auto PushSetScreenCall(bool fullscreen, int width, int height, - TextureQuality texture_quality, - GraphicsQuality graphics_quality, - const std::string& android_res) -> void; - auto PushReloadMediaCall() -> void; - auto PushRemoveRenderHoldCall() -> void; - auto PushComponentUnloadCall( - const std::vector*>& components) -> void; - auto SetRenderHold() -> void; + + void OnMainThreadStartApp(); + + /// Should be called to inform ballistica of screen size changes; this will be + /// applied to the server and then sent to the logic thread to apply to + /// various + // app systems (ui, etc.). + void SetScreenResolution(float h, float v); + + void PushSetScreenGammaCall(float gamma); + void PushSetScreenPixelScaleCall(float pixel_scale); + void PushSetVSyncCall(bool sync, bool auto_sync); + void PushSetScreenCall(bool fullscreen, int width, int height, + TextureQualityRequest texture_quality_request, + GraphicsQualityRequest graphics_quality_request, + const std::string& android_res); + void PushReloadMediaCall(); + void PushRemoveRenderHoldCall(); + void PushComponentUnloadCall( + const std::vector*>& components); + void SetRenderHold(); // Used by the logic thread to pass frame-defs to the graphics server // for rendering. - auto SetFrameDef(FrameDef* framedef) -> void; + void SetFrameDef(FrameDef* framedef); // returns the next frame_def needing to be rendered, waiting for it to arrive // if necessary. this can return nullptr if no frame_defs come in within a @@ -42,36 +52,36 @@ class GraphicsServer { // of using the RenderFrameDef* calls auto GetRenderFrameDef() -> FrameDef*; - auto RunFrameDefMeshUpdates(FrameDef* frame_def) -> void; + void RunFrameDefMeshUpdates(FrameDef* frame_def); // renders shadow passes and other common parts of a frame_def - auto PreprocessRenderFrameDef(FrameDef* frame_def) -> void; + void PreprocessRenderFrameDef(FrameDef* frame_def); // Does the default drawing to the screen, either from the left or right // stereo eye or in mono. - auto DrawRenderFrameDef(FrameDef* frame_def, int eye = -1) -> void; + void DrawRenderFrameDef(FrameDef* frame_def, int eye = -1); // Clean up the frame_def once done drawing it. - auto FinishRenderFrameDef(FrameDef* frame_def) -> void; + void FinishRenderFrameDef(FrameDef* frame_def); // Equivalent to calling GetRenderFrameDef() and then preprocess, draw (in // mono), and finish. - auto TryRender() -> void; + void TryRender(); // init the modelview matrix to look here - auto SetCamera(const Vector3f& eye, const Vector3f& target, - const Vector3f& up) -> void; + void SetCamera(const Vector3f& eye, const Vector3f& target, + const Vector3f& up); - auto SetOrthoProjection(float left, float right, float bottom, float top, - float near, float far) -> void; + void SetOrthoProjection(float left, float right, float bottom, float top, + float near, float far); - auto ModelViewReset() -> void { + void ModelViewReset() { model_view_matrix_ = kMatrix44fIdentity; model_view_projection_matrix_dirty_ = model_world_matrix_dirty_ = true; model_view_stack_.clear(); } - auto SetProjectionMatrix(const Matrix44f& p) -> void { + void SetProjectionMatrix(const Matrix44f& p) { projection_matrix_ = p; model_view_projection_matrix_dirty_ = true; projection_matrix_state_++; @@ -81,7 +91,7 @@ class GraphicsServer { return projection_matrix_state_; } - auto SetLightShadowProjectionMatrix(const Matrix44f& p) -> void { + void SetLightShadowProjectionMatrix(const Matrix44f& p) { // This will generally get repeatedly set to the same value // so we can do nothing most of the time. if (p != light_shadow_projection_matrix_) { @@ -134,7 +144,7 @@ class GraphicsServer { auto model_view_matrix() const -> const Matrix44f& { return model_view_matrix_; } - auto SetModelViewMatrix(const Matrix44f& m) -> void { + void SetModelViewMatrix(const Matrix44f& m) { model_view_matrix_ = m; model_view_projection_matrix_dirty_ = model_world_matrix_dirty_ = true; } @@ -142,75 +152,75 @@ class GraphicsServer { auto projection_matrix() const -> const Matrix44f& { return projection_matrix_; } - auto PushTransform() -> void { + void PushTransform() { model_view_stack_.push_back(model_view_matrix_); assert(model_view_stack_.size() < 20); } - auto PopTransform() -> void { + void PopTransform() { assert(!model_view_stack_.empty()); model_view_matrix_ = model_view_stack_.back(); model_view_stack_.pop_back(); model_view_projection_matrix_dirty_ = model_world_matrix_dirty_ = true; } - auto Translate(const Vector3f& t) -> void { + void Translate(const Vector3f& t) { model_view_matrix_ = Matrix44fTranslate(t) * model_view_matrix_; model_view_projection_matrix_dirty_ = model_world_matrix_dirty_ = true; } - auto Rotate(float angle, const Vector3f& axis) -> void { + void Rotate(float angle, const Vector3f& axis) { model_view_matrix_ = Matrix44fRotate(axis, angle) * model_view_matrix_; model_view_projection_matrix_dirty_ = model_world_matrix_dirty_ = true; } - auto MultMatrix(const Matrix44f& m) -> void { + void MultMatrix(const Matrix44f& m) { model_view_matrix_ = m * model_view_matrix_; model_view_projection_matrix_dirty_ = model_world_matrix_dirty_ = true; } - auto scale(const Vector3f& s) -> void { + void scale(const Vector3f& s) { model_view_matrix_ = Matrix44fScale(s) * model_view_matrix_; model_view_projection_matrix_dirty_ = model_world_matrix_dirty_ = true; } - auto RebuildLostContext() -> void; + void RebuildLostContext(); ~GraphicsServer(); auto renderer() { return renderer_; } auto quality() const -> GraphicsQuality { assert(graphics_quality_set_); - return quality_actual_; + return graphics_quality_; } auto texture_quality() const -> TextureQuality { assert(texture_quality_set_); - return texture_quality_actual_; + return texture_quality_; } auto screen_pixel_width() const -> float { - assert(InMainThread()); + assert(g_base->InGraphicsThread()); return res_x_; } auto screen_pixel_height() const -> float { - assert(InMainThread()); + assert(g_base->InGraphicsThread()); return res_y_; } auto screen_virtual_width() const -> float { - assert(InMainThread()); + assert(g_base->InGraphicsThread()); return res_x_virtual_; } auto screen_virtual_height() const -> float { - assert(InMainThread()); + assert(g_base->InGraphicsThread()); return res_y_virtual_; } - auto set_tv_border(bool val) -> void { - assert(InMainThread()); + void set_tv_border(bool val) { + assert(g_base->InGraphicsThread()); tv_border_ = val; } auto tv_border() const { - assert(InMainThread()); + assert(g_base->InGraphicsThread()); return tv_border_; } @@ -222,8 +232,8 @@ class GraphicsServer { return ((texture_compression_types_ & (0x01u << static_cast(t))) != 0u); } - auto SetTextureCompressionTypes( - const std::list& types) -> void; + void SetTextureCompressionTypes( + const std::list& types); auto texture_compression_types_are_set() const { return texture_compression_types_set_; @@ -235,58 +245,61 @@ class GraphicsServer { // This doesn't actually toggle fullscreen. It is used to inform the game // when fullscreen changes under it. auto set_fullscreen_enabled(bool fs) { fullscreen_enabled_ = fs; } - auto VideoResize(float h, float v) -> void; #if BA_ENABLE_OPENGL auto gl_context() const -> GLContext* { return gl_context_.get(); } #endif - auto graphics_quality_requested() const { return quality_requested_; } + auto graphics_quality_requested() const { + return graphics_quality_requested_; + } + auto graphics_quality() const { return graphics_quality_; } auto texture_quality_requested() const { return texture_quality_requested_; } auto renderer() const { return renderer_; } auto initial_screen_created() const { return initial_screen_created_; } - auto thread() const -> Thread* { return thread_; } + auto event_loop() const -> EventLoop* { return event_loop_; } private: - auto HandleFullscreenToggling(bool do_set_existing_fs, bool do_toggle_fs, - bool fullscreen) -> void; - auto HandlePushAndroidRes(const std::string& android_res) -> void; - auto HandleFullContextScreenRebuild( + void HandleFullscreenToggling(bool do_set_existing_fs, bool do_toggle_fs, + bool fullscreen); + void HandlePushAndroidRes(const std::string& android_res); + void HandleFullContextScreenRebuild( bool need_full_context_rebuild, bool fullscreen, int width, int height, - GraphicsQuality graphics_quality_requested, - TextureQuality texture_quality_requested) -> void; + GraphicsQualityRequest graphics_quality_requested, + TextureQualityRequest texture_quality_requested); // Update virtual screen dimensions based on the current physical ones. - static auto CalcVirtualRes(float* x, float* y) -> void; + static void CalcVirtualRes(float* x, float* y); - auto UpdateVirtualScreenRes() -> void; - auto UpdateCamOrientMatrix() -> void; - auto ReloadMedia() -> void; - auto UpdateModelViewProjectionMatrix() -> void { + void UpdateVirtualScreenRes(); + void UpdateCamOrientMatrix(); + void ReloadMedia(); + void UpdateModelViewProjectionMatrix() { if (model_view_projection_matrix_dirty_) { model_view_projection_matrix_ = model_view_matrix_ * projection_matrix_; model_view_projection_matrix_state_++; model_view_projection_matrix_dirty_ = false; } } - auto UpdateModelWorldMatrix() -> void { + void UpdateModelWorldMatrix() { if (model_world_matrix_dirty_) { model_world_matrix_ = model_view_matrix_ * view_world_matrix_; model_world_matrix_state_++; model_world_matrix_dirty_ = false; } } - auto SetScreen(bool fullscreen, int width, int height, - TextureQuality texture_quality, - GraphicsQuality graphics_quality, - const std::string& android_res) -> void; + void SetScreen(bool fullscreen, int width, int height, + TextureQualityRequest texture_quality_request, + GraphicsQualityRequest graphics_quality_request, + const std::string& android_res); + #if BA_OSTYPE_MACOS && BA_XCODE_BUILD void FullscreenCheck(); #endif #if BA_ENABLE_OPENGL std::unique_ptr gl_context_; #endif - Thread* thread_{}; + EventLoop* event_loop_{}; float res_x_{}; float res_y_{}; float res_x_virtual_{0.0f}; @@ -295,10 +308,12 @@ class GraphicsServer { bool renderer_context_lost_{}; uint32_t texture_compression_types_{}; bool texture_compression_types_set_{}; - TextureQuality texture_quality_requested_{TextureQuality::kLow}; - TextureQuality texture_quality_actual_{TextureQuality::kLow}; - GraphicsQuality quality_requested_{GraphicsQuality::kLow}; - GraphicsQuality quality_actual_{GraphicsQuality::kLow}; + TextureQualityRequest texture_quality_requested_{ + TextureQualityRequest::kUnset}; + TextureQuality texture_quality_{TextureQuality::kLow}; + GraphicsQualityRequest graphics_quality_requested_{ + GraphicsQualityRequest::kUnset}; + GraphicsQuality graphics_quality_{GraphicsQuality::kUnset}; bool graphics_quality_set_{}; bool texture_quality_set_{}; bool fullscreen_enabled_{}; @@ -331,8 +346,9 @@ class GraphicsServer { FrameDef* frame_def_{}; bool initial_screen_created_{}; int render_hold_{}; + std::mutex frame_def_mutex_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_GRAPHICS_SERVER_H_ +#endif // BALLISTICA_BASE_GRAPHICS_GRAPHICS_SERVER_H_ diff --git a/src/ballistica/graphics/vr_graphics.cc b/src/ballistica/base/graphics/graphics_vr.cc similarity index 81% rename from src/ballistica/graphics/vr_graphics.cc rename to src/ballistica/base/graphics/graphics_vr.cc index 30aa4514..d53f92d8 100644 --- a/src/ballistica/graphics/vr_graphics.cc +++ b/src/ballistica/base/graphics/graphics_vr.cc @@ -1,19 +1,19 @@ // Released under the MIT License. See LICENSE for details. #if BA_VR_BUILD -#include "ballistica/graphics/vr_graphics.h" +#include "ballistica/base/graphics/graphics_vr.h" -#include "ballistica/app/app.h" -#include "ballistica/graphics/camera.h" -#include "ballistica/graphics/component/object_component.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/graphics/component/special_component.h" -#include "ballistica/graphics/frame_def.h" -#include "ballistica/graphics/render_pass.h" -#include "ballistica/logic/logic.h" -#include "ballistica/scene/node/globals_node.h" +#include "ballistica/base/graphics/component/object_component.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/graphics/component/special_component.h" +#include "ballistica/base/graphics/renderer/render_pass.h" +#include "ballistica/base/graphics/support/camera.h" +#include "ballistica/base/graphics/support/frame_def.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/core/core.h" +#include "ballistica/scene_v1/node/globals_node.h" -namespace ballistica { +namespace ballistica::base { static auto ValueTestFloat(float* storage, double* absval, double* deltaval) -> double { @@ -37,7 +37,7 @@ static auto ValueTestBool(bool* storage, double* absval, double* deltaval) return static_cast(*storage); } -auto VRGraphics::ValueTest(const std::string& arg, double* absval, +auto GraphicsVR::ValueTest(const std::string& arg, double* absval, double* deltaval, double* outval) -> bool { if (arg == "vrOverlayScale") { *outval = ValueTestFloat(&vr_overlay_scale_, absval, deltaval); @@ -48,7 +48,7 @@ auto VRGraphics::ValueTest(const std::string& arg, double* absval, } else if (arg == "headScale") { *outval = ValueTestFloat(&vr_test_head_scale_, absval, deltaval); } else if (arg == "vrCamOffsetY") { - Camera* camera = g_graphics->camera(); + Camera* camera = g_base->graphics->camera(); if (camera) { Vector3f val = camera->vr_extra_offset(); if (deltaval) { @@ -60,7 +60,7 @@ auto VRGraphics::ValueTest(const std::string& arg, double* absval, *outval = camera->vr_extra_offset().y; } } else if (arg == "vrCamOffsetZ") { - Camera* camera = g_graphics->camera(); + Camera* camera = g_base->graphics->camera(); if (camera) { Vector3f val = camera->vr_extra_offset(); if (deltaval) { @@ -78,21 +78,21 @@ auto VRGraphics::ValueTest(const std::string& arg, double* absval, return true; } -void VRGraphics::ApplyCamera(FrameDef* frame_def) { +void GraphicsVR::ApplyCamera(FrameDef* frame_def) { Graphics::ApplyCamera(frame_def); CalcVROverlayMatrices(frame_def); } -void VRGraphics::DrawWorld(Session* session, FrameDef* frame_def) { +void GraphicsVR::DrawWorld(FrameDef* frame_def) { // Draw the standard world. - Graphics::DrawWorld(session, frame_def); + Graphics::DrawWorld(frame_def); // Draw extra VR-Only bits. DrawVRControllers(frame_def); } -void VRGraphics::DrawUI(FrameDef* frame_def) { +void GraphicsVR::DrawUI(FrameDef* frame_def) { // Draw the UI normally, but then blit its texture into 3d space. Graphics::DrawUI(frame_def); @@ -104,10 +104,10 @@ void VRGraphics::DrawUI(FrameDef* frame_def) { DrawOverlayBounds(frame_def->overlay_pass()); } -void VRGraphics::CalcVROverlayMatrices(FrameDef* frame_def) { +void GraphicsVR::CalcVROverlayMatrices(FrameDef* frame_def) { // For VR mode, calc our overlay matrix for use in positioning overlay // elements. - if (IsVRMode()) { + if (g_core->IsVRMode()) { Vector3f cam_target_pt(frame_def->cam_target_original()); Matrix44f vr_overlay_matrix{kMatrix44fIdentity}; Matrix44f vr_overlay_matrix_fixed{kMatrix44fIdentity}; @@ -201,7 +201,7 @@ void VRGraphics::CalcVROverlayMatrices(FrameDef* frame_def) { } } -auto VRGraphics::CalcVROverlayMatrix(const Vector3f& cam_pt, +auto GraphicsVR::CalcVROverlayMatrix(const Vector3f& cam_pt, const Vector3f& cam_target_pt) const -> Matrix44f { Matrix44f m = Matrix44fTranslate(cam_target_pt); @@ -225,14 +225,14 @@ auto VRGraphics::CalcVROverlayMatrix(const Vector3f& cam_pt, base_scale)) * m; } -void VRGraphics::DrawVROverlay(FrameDef* frame_def) { +void GraphicsVR::DrawVROverlay(FrameDef* frame_def) { // In vr mode we have draw our overlay-flat texture in to space // as part of our regular overlay pass. // NOTE: this assumes nothing after this point gets drawn into // the overlay-flat pass (otherwise it may get skipped). // This should be a safe assumption since this is pretty much just for // widgets. - if (IsVRMode() && frame_def->overlay_flat_pass()->HasDrawCommands()) { + if (g_core->IsVRMode() && frame_def->overlay_flat_pass()->HasDrawCommands()) { // Draw our overlay-flat stuff into our overlay pass. SpecialComponent c(frame_def->overlay_pass(), SpecialComponent::Source::kVROverlayBuffer); @@ -241,12 +241,12 @@ void VRGraphics::DrawVROverlay(FrameDef* frame_def) { c.Scale(kBaseVirtualResX * (1.0f + kVRBorder), kBaseVirtualResY * (1.0f + kVRBorder), kBaseVirtualResX * (1.0f + kVRBorder)); - c.DrawModel(g_assets->GetModel(SystemModelID::kVROverlay)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kVROverlay)); c.PopTransform(); c.Submit(); } } -void VRGraphics::DrawOverlayBounds(RenderPass* pass) { +void GraphicsVR::DrawOverlayBounds(RenderPass* pass) { // We can optionally draw a guide to show the edges of the overlay pass if (draw_overlay_bounds_) { SimpleComponent c(pass); @@ -258,14 +258,14 @@ void VRGraphics::DrawOverlayBounds(RenderPass* pass) { // Slight offset in z to reduce z fighting. c.Translate(0.5f * width, 0.5f * height, 1.0f); c.Scale(width, height, 100.0f); - c.DrawModel(g_assets->GetModel(SystemModelID::kOverlayGuide)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kOverlayGuide)); c.PopTransform(); c.Submit(); } } -void VRGraphics::DrawVRControllers(FrameDef* frame_def) { - if (!IsVRMode()) { +void GraphicsVR::DrawVRControllers(FrameDef* frame_def) { + if (!g_core->IsVRMode()) { return; } @@ -277,33 +277,33 @@ void VRGraphics::DrawVRControllers(FrameDef* frame_def) { if (false) { ObjectComponent c(frame_def->beauty_pass()); c.SetColor(1, 0, 0); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kBoxingGlove)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kBoxingGlove)); c.SetReflection(ReflectionType::kSoft); c.SetReflectionScale(0.4f, 0.4f, 0.4f); c.PushTransform(); c.VRTransformToHead(); c.Translate(0, 0, 5); c.Scale(2, 2, 2); - c.DrawModel(g_assets->GetModel(SystemModelID::kBoxingGlove)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kBoxingGlove)); c.PopTransform(); c.Submit(); } // test right hand - const VRHandsState& s(g_logic->vr_hands_state()); + const VRHandsState& s(vr_hands_state()); switch (s.r.type) { case VRHandType::kOculusTouchR: case VRHandType::kDaydreamRemote: { ObjectComponent c(frame_def->beauty_pass()); c.SetColor(0, 1, 0); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kBoxingGlove)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kBoxingGlove)); c.SetReflection(ReflectionType::kSoft); c.SetReflectionScale(0.4f, 0.4f, 0.4f); c.PushTransform(); c.VRTransformToRightHand(); c.Scale(10, 10, 10); - c.DrawModel(g_assets->GetModel(SystemModelID::kBoxingGlove)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kBoxingGlove)); c.PopTransform(); c.Submit(); break; @@ -316,13 +316,13 @@ void VRGraphics::DrawVRControllers(FrameDef* frame_def) { case VRHandType::kOculusTouchL: { ObjectComponent c(frame_def->beauty_pass()); c.SetColor(0, 0, 1); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kBoxingGlove)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kBoxingGlove)); c.SetReflection(ReflectionType::kSoft); c.SetReflectionScale(0.4f, 0.4f, 0.4f); c.PushTransform(); c.VRTransformToLeftHand(); c.Scale(10, 10, 10); - c.DrawModel(g_assets->GetModel(SystemModelID::kBoxingGlove)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kBoxingGlove)); c.PopTransform(); c.Submit(); break; @@ -332,16 +332,6 @@ void VRGraphics::DrawVRControllers(FrameDef* frame_def) { } } -auto VRGraphics::ApplyGlobals(GlobalsNode* globals) -> void { - Graphics::ApplyGlobals(globals); - - if (IsVRMode()) { - set_vr_near_clip(globals->vr_near_clip()); - set_vr_overlay_center(Vector3f(globals->vr_overlay_center())); - set_vr_overlay_center_enabled(globals->vr_overlay_center_enabled()); - } -} - -} // namespace ballistica +} // namespace ballistica::base #endif // BA_VR_BUILD diff --git a/src/ballistica/graphics/vr_graphics.h b/src/ballistica/base/graphics/graphics_vr.h similarity index 69% rename from src/ballistica/graphics/vr_graphics.h rename to src/ballistica/base/graphics/graphics_vr.h index 479002d3..62ccd218 100644 --- a/src/ballistica/graphics/vr_graphics.h +++ b/src/ballistica/base/graphics/graphics_vr.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_VR_GRAPHICS_H_ -#define BALLISTICA_GRAPHICS_VR_GRAPHICS_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_GRAPHICS_VR_H_ +#define BALLISTICA_BASE_GRAPHICS_GRAPHICS_VR_H_ #include -#include "ballistica/graphics/graphics.h" +#include "ballistica/base/graphics/graphics.h" -namespace ballistica { +namespace ballistica::base { const float kDefaultVRHeadScale = 18.0f; const float kVRFixedOverlayOffsetY = -7.0f; @@ -15,19 +15,18 @@ const float kVRFixedOverlayOffsetZ = -22.0f; #if BA_VR_BUILD -class VRGraphics : public Graphics { +class GraphicsVR : public Graphics { public: - /// Return g_graphics as a VRGraphics. (assumes it actually is one). - static VRGraphics* get() { - assert(g_graphics != nullptr); - assert(dynamic_cast(g_graphics) - == static_cast(g_graphics)); - return static_cast(g_graphics); + /// Return g_graphics as a GraphicsVR. (assumes it actually is one). + static GraphicsVR* get() { + assert(g_base && g_base->graphics != nullptr); + assert(dynamic_cast(g_base->graphics) + == static_cast(g_base->graphics)); + return static_cast(g_base->graphics); } - auto ApplyCamera(FrameDef* frame_def) -> void override; - auto ApplyGlobals(GlobalsNode* globals) -> void override; + void ApplyCamera(FrameDef* frame_def) override; - void DrawWorld(Session* session, FrameDef* frame_def) override; + void DrawWorld(FrameDef* frame_def) override; void DrawUI(FrameDef* frame_def) override; auto vr_head_forward() const -> const Vector3f& { return vr_head_forward_; } @@ -39,14 +38,14 @@ class VRGraphics : public Graphics { void set_vr_head_up(const Vector3f& v) { vr_head_up_ = v; } void set_vr_head_translate(const Vector3f& v) { vr_head_translate_ = v; } void set_vr_overlay_center(const Vector3f& val) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); vr_overlay_center_ = val; } auto vr_overlay_center() const -> const Vector3f& { return vr_overlay_center_; } void set_vr_overlay_center_enabled(bool val) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); vr_overlay_center_enabled_ = val; } auto vr_overlay_center_enabled() const -> bool { @@ -59,6 +58,11 @@ class VRGraphics : public Graphics { float vr_test_head_scale() const { return vr_test_head_scale_; } + auto vr_hands_state() const -> VRHandsState { return vr_hands_state_; } + void set_vr_hands_state(const VRHandsState& state) { + vr_hands_state_ = state; + } + private: void CalcVROverlayMatrices(FrameDef* frame_def); auto CalcVROverlayMatrix(const Vector3f& cam_pt, @@ -79,9 +83,10 @@ class VRGraphics : public Graphics { bool lock_vr_overlay_{}; bool draw_overlay_bounds_{}; float vr_test_head_scale_{kDefaultVRHeadScale}; + VRHandsState vr_hands_state_; }; #endif // BA_VR_BUILD -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_VR_GRAPHICS_H_ +#endif // BALLISTICA_BASE_GRAPHICS_GRAPHICS_VR_H_ diff --git a/src/ballistica/graphics/mesh/image_mesh.cc b/src/ballistica/base/graphics/mesh/image_mesh.cc similarity index 79% rename from src/ballistica/graphics/mesh/image_mesh.cc rename to src/ballistica/base/graphics/mesh/image_mesh.cc index 6045e1d8..c999c956 100644 --- a/src/ballistica/graphics/mesh/image_mesh.cc +++ b/src/ballistica/base/graphics/mesh/image_mesh.cc @@ -1,8 +1,8 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/mesh/image_mesh.h" +#include "ballistica/base/graphics/mesh/image_mesh.h" -namespace ballistica { +namespace ballistica::base { const uint16_t kImageMeshIndices[] = {0, 1, 2, 1, 3, 2}; const VertexSimpleSplitStatic kImageMeshVerticesStatic[] = { @@ -14,4 +14,4 @@ ImageMesh::ImageMesh() { 4, kImageMeshVerticesStatic)); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/mesh/image_mesh.h b/src/ballistica/base/graphics/mesh/image_mesh.h similarity index 68% rename from src/ballistica/graphics/mesh/image_mesh.h rename to src/ballistica/base/graphics/mesh/image_mesh.h index 2044ddc6..aa267d48 100644 --- a/src/ballistica/graphics/mesh/image_mesh.h +++ b/src/ballistica/base/graphics/mesh/image_mesh.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_MESH_IMAGE_MESH_H_ -#define BALLISTICA_GRAPHICS_MESH_IMAGE_MESH_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_IMAGE_MESH_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_IMAGE_MESH_H_ -#include "ballistica/graphics/mesh/mesh_indexed_simple_split.h" +#include "ballistica/base/graphics/mesh/mesh_indexed_simple_split.h" -namespace ballistica { +namespace ballistica::base { // a mesh set up to draw images class ImageMesh : public MeshIndexedSimpleSplit { @@ -22,6 +22,6 @@ class ImageMesh : public MeshIndexedSimpleSplit { } }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_MESH_IMAGE_MESH_H_ +#endif // BALLISTICA_BASE_GRAPHICS_MESH_IMAGE_MESH_H_ diff --git a/src/ballistica/graphics/mesh/mesh.h b/src/ballistica/base/graphics/mesh/mesh.h similarity index 71% rename from src/ballistica/graphics/mesh/mesh.h rename to src/ballistica/base/graphics/mesh/mesh.h index 185bcc82..c556032f 100644 --- a/src/ballistica/graphics/mesh/mesh.h +++ b/src/ballistica/base/graphics/mesh/mesh.h @@ -1,15 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_H_ -#include "ballistica/core/object.h" -#include "ballistica/graphics/mesh/mesh_data.h" -#include "ballistica/graphics/mesh/mesh_data_client_handle.h" +#include "ballistica/base/graphics/mesh/mesh_data.h" +#include "ballistica/base/graphics/mesh/mesh_data_client_handle.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::base { -// A user-defined dynamic mesh (unlike a model which is completely static) +// A dynamically defined mesh (unlike a mesh asset which is completely static). class Mesh : public Object { public: auto type() const -> MeshDataType { return type_; } @@ -41,6 +41,6 @@ class Mesh : public Object { bool valid_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_MESH_MESH_H_ +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_H_ diff --git a/src/ballistica/graphics/mesh/mesh_buffer.h b/src/ballistica/base/graphics/mesh/mesh_buffer.h similarity index 63% rename from src/ballistica/graphics/mesh/mesh_buffer.h rename to src/ballistica/base/graphics/mesh/mesh_buffer.h index 009b5ae2..b4a9ba6e 100644 --- a/src/ballistica/graphics/mesh/mesh_buffer.h +++ b/src/ballistica/base/graphics/mesh/mesh_buffer.h @@ -1,14 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_BUFFER_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_BUFFER_H_ #include #include -#include "ballistica/graphics/mesh/mesh_buffer_base.h" +#include "ballistica/base/graphics/mesh/mesh_buffer_base.h" -namespace ballistica { +namespace ballistica::base { // Buffer for arbitrary mesh data. template @@ -23,6 +23,6 @@ class MeshBuffer : public MeshBufferBase { std::vector elements; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_H_ +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_BUFFER_H_ diff --git a/src/ballistica/graphics/mesh/mesh_buffer_base.h b/src/ballistica/base/graphics/mesh/mesh_buffer_base.h similarity index 63% rename from src/ballistica/graphics/mesh/mesh_buffer_base.h rename to src/ballistica/base/graphics/mesh/mesh_buffer_base.h index 04f264a6..a3637655 100644 --- a/src/ballistica/graphics/mesh/mesh_buffer_base.h +++ b/src/ballistica/base/graphics/mesh/mesh_buffer_base.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_BASE_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_BASE_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_BUFFER_BASE_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_BUFFER_BASE_H_ -#include "ballistica/core/object.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::base { // Buffers used by the logic thread to pass indices/vertices/etc. to meshes in // the graphics thread. Note that it is safe to create these in other threads; @@ -16,6 +16,6 @@ class MeshBufferBase : public Object { uint32_t state; // which dynamicState value on the mesh this corresponds to }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_BASE_H_ +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_BUFFER_BASE_H_ diff --git a/src/ballistica/base/graphics/mesh/mesh_buffer_vertex_simple_full.h b/src/ballistica/base/graphics/mesh/mesh_buffer_vertex_simple_full.h new file mode 100644 index 00000000..db70d9ca --- /dev/null +++ b/src/ballistica/base/graphics/mesh/mesh_buffer_vertex_simple_full.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SIMPLE_FULL_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SIMPLE_FULL_H_ + +#include "ballistica/base/graphics/mesh/mesh_buffer.h" + +namespace ballistica::base { + +// just make this a vanilla child class of our template +// (simply so we could predeclare this) +class MeshBufferVertexSimpleFull : public MeshBuffer { + using MeshBuffer::MeshBuffer; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SIMPLE_FULL_H_ diff --git a/src/ballistica/base/graphics/mesh/mesh_buffer_vertex_smoke_full.h b/src/ballistica/base/graphics/mesh/mesh_buffer_vertex_smoke_full.h new file mode 100644 index 00000000..87cf5cb5 --- /dev/null +++ b/src/ballistica/base/graphics/mesh/mesh_buffer_vertex_smoke_full.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SMOKE_FULL_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SMOKE_FULL_H_ + +#include "ballistica/base/graphics/mesh/mesh_buffer.h" + +namespace ballistica::base { + +// just make this a vanilla child class of our template +// (simply so we could predeclare this) +class MeshBufferVertexSmokeFull : public MeshBuffer { + using MeshBuffer::MeshBuffer; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SMOKE_FULL_H_ diff --git a/src/ballistica/base/graphics/mesh/mesh_buffer_vertex_sprite.h b/src/ballistica/base/graphics/mesh/mesh_buffer_vertex_sprite.h new file mode 100644 index 00000000..9c5db5f7 --- /dev/null +++ b/src/ballistica/base/graphics/mesh/mesh_buffer_vertex_sprite.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SPRITE_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SPRITE_H_ + +#include "ballistica/base/graphics/mesh/mesh_buffer.h" + +namespace ballistica::base { + +// just make this a vanilla child class of our template +// (simply so we could predeclare this) +class MeshBufferVertexSprite : public MeshBuffer { + using MeshBuffer::MeshBuffer; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SPRITE_H_ diff --git a/src/ballistica/graphics/mesh/mesh_data.cc b/src/ballistica/base/graphics/mesh/mesh_data.cc similarity index 59% rename from src/ballistica/graphics/mesh/mesh_data.cc rename to src/ballistica/base/graphics/mesh/mesh_data.cc index e2de4570..1d3f9c23 100644 --- a/src/ballistica/graphics/mesh/mesh_data.cc +++ b/src/ballistica/base/graphics/mesh/mesh_data.cc @@ -1,24 +1,24 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/mesh/mesh_data.h" +#include "ballistica/base/graphics/mesh/mesh_data.h" -#include "ballistica/graphics/renderer.h" +#include "ballistica/base/graphics/renderer/renderer.h" -namespace ballistica { +namespace ballistica::base { void MeshData::Load(Renderer* renderer) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); if (!renderer_data_) { renderer_data_ = renderer->NewMeshData(type(), draw_type()); } } void MeshData::Unload(Renderer* renderer) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); if (renderer_data_) { renderer->DeleteMeshData(renderer_data_, type()); renderer_data_ = nullptr; } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/mesh/mesh_data.h b/src/ballistica/base/graphics/mesh/mesh_data.h similarity index 78% rename from src/ballistica/graphics/mesh/mesh_data.h rename to src/ballistica/base/graphics/mesh/mesh_data.h index 016b98c1..9520465b 100644 --- a/src/ballistica/graphics/mesh/mesh_data.h +++ b/src/ballistica/base/graphics/mesh/mesh_data.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_DATA_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_DATA_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_DATA_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_DATA_H_ #include -#include "ballistica/ballistica.h" +#include "ballistica/base/base.h" -namespace ballistica { +namespace ballistica::base { // The portion of a mesh that is owned by the graphics thread. // This contains the renderer-specific data (GL buffers, etc) @@ -37,6 +37,6 @@ class MeshData { BA_DISALLOW_CLASS_COPIES(MeshData); }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_MESH_MESH_DATA_H_ +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_DATA_H_ diff --git a/src/ballistica/base/graphics/mesh/mesh_data_client_handle.cc b/src/ballistica/base/graphics/mesh/mesh_data_client_handle.cc new file mode 100644 index 00000000..940120ef --- /dev/null +++ b/src/ballistica/base/graphics/mesh/mesh_data_client_handle.cc @@ -0,0 +1,17 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/graphics/mesh/mesh_data_client_handle.h" + +#include "ballistica/base/graphics/graphics.h" + +namespace ballistica::base { + +MeshDataClientHandle::MeshDataClientHandle(MeshData* d) : mesh_data(d) { + g_base->graphics->AddMeshDataCreate(mesh_data); +} + +MeshDataClientHandle::~MeshDataClientHandle() { + g_base->graphics->AddMeshDataDestroy(mesh_data); +} + +} // namespace ballistica::base diff --git a/src/ballistica/graphics/mesh/mesh_data_client_handle.h b/src/ballistica/base/graphics/mesh/mesh_data_client_handle.h similarity index 55% rename from src/ballistica/graphics/mesh/mesh_data_client_handle.h rename to src/ballistica/base/graphics/mesh/mesh_data_client_handle.h index f0eae583..af6a1c88 100644 --- a/src/ballistica/graphics/mesh/mesh_data_client_handle.h +++ b/src/ballistica/base/graphics/mesh/mesh_data_client_handle.h @@ -1,11 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_DATA_CLIENT_HANDLE_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_DATA_CLIENT_HANDLE_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_DATA_CLIENT_HANDLE_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_DATA_CLIENT_HANDLE_H_ -#include "ballistica/core/object.h" +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::base { // Client-side (game-thread) handle to server-side (graphics-thread) mesh data. // Server-side data will be created when this object is instantiated and @@ -17,6 +18,6 @@ class MeshDataClientHandle : public Object { MeshData* mesh_data; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_MESH_MESH_DATA_CLIENT_HANDLE_H_ +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_DATA_CLIENT_HANDLE_H_ diff --git a/src/ballistica/base/graphics/mesh/mesh_index_buffer_16.h b/src/ballistica/base/graphics/mesh/mesh_index_buffer_16.h new file mode 100644 index 00000000..1b8adab8 --- /dev/null +++ b/src/ballistica/base/graphics/mesh/mesh_index_buffer_16.h @@ -0,0 +1,17 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEX_BUFFER_16_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEX_BUFFER_16_H_ + +#include "ballistica/base/graphics/mesh/mesh_buffer.h" + +namespace ballistica::base { + +// standard buffer for indices +class MeshIndexBuffer16 : public MeshBuffer { + using MeshBuffer::MeshBuffer; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEX_BUFFER_16_H_ diff --git a/src/ballistica/base/graphics/mesh/mesh_index_buffer_32.h b/src/ballistica/base/graphics/mesh/mesh_index_buffer_32.h new file mode 100644 index 00000000..d1bfdbd8 --- /dev/null +++ b/src/ballistica/base/graphics/mesh/mesh_index_buffer_32.h @@ -0,0 +1,17 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEX_BUFFER_32_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEX_BUFFER_32_H_ + +#include "ballistica/base/graphics/mesh/mesh_buffer.h" + +namespace ballistica::base { + +// standard buffer for indices +class MeshIndexBuffer32 : public MeshBuffer { + using MeshBuffer::MeshBuffer; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEX_BUFFER_32_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed.h b/src/ballistica/base/graphics/mesh/mesh_indexed.h similarity index 73% rename from src/ballistica/graphics/mesh/mesh_indexed.h rename to src/ballistica/base/graphics/mesh/mesh_indexed.h index 2841ea11..84c0f8a6 100644 --- a/src/ballistica/graphics/mesh/mesh_indexed.h +++ b/src/ballistica/base/graphics/mesh/mesh_indexed.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_H_ -#include "ballistica/graphics/mesh/mesh_indexed_base.h" +#include "ballistica/base/graphics/mesh/mesh_indexed_base.h" -namespace ballistica { +namespace ballistica::base { // Mesh using indices and vertex data (all either static or dynamic). // Supports both 16 and 32 bit indices. @@ -22,7 +22,7 @@ class MeshIndexed : public MeshIndexedBase { auto data() const -> const Object::Ref>& { return data_; } auto IsValid() const -> bool override { - if (!data_.exists() || data_->elements.empty() + if (!data_.Exists() || data_->elements.empty() || !MeshIndexedBase::IsValid()) { return false; } @@ -36,6 +36,6 @@ class MeshIndexed : public MeshIndexedBase { uint32_t data_state_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_H_ +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed_base.h b/src/ballistica/base/graphics/mesh/mesh_indexed_base.h similarity index 74% rename from src/ballistica/graphics/mesh/mesh_indexed_base.h rename to src/ballistica/base/graphics/mesh/mesh_indexed_base.h index 5b372252..3323b3e0 100644 --- a/src/ballistica/graphics/mesh/mesh_indexed_base.h +++ b/src/ballistica/base/graphics/mesh/mesh_indexed_base.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_BASE_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_BASE_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_BASE_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_BASE_H_ -#include "ballistica/graphics/mesh/mesh.h" -#include "ballistica/graphics/mesh/mesh_index_buffer_16.h" -#include "ballistica/graphics/mesh/mesh_index_buffer_32.h" +#include "ballistica/base/graphics/mesh/mesh.h" +#include "ballistica/base/graphics/mesh/mesh_index_buffer_16.h" +#include "ballistica/base/graphics/mesh/mesh_index_buffer_32.h" -namespace ballistica { +namespace ballistica::base { // Mesh supporting index data. class MeshIndexedBase : public Mesh { @@ -22,11 +22,11 @@ class MeshIndexedBase : public Mesh { } void SetIndexData(const Object::Ref& data) { - assert(data.exists() && !data->elements.empty()); + assert(data.Exists() && !data->elements.empty()); // unlike vertex data, index data might often remain the same, so lets test // for that and avoid some gl updates.. - if (index_data_32_.exists()) { - assert(data.exists() && index_data_32_.get()); + if (index_data_32_.Exists()) { + assert(data.Exists() && index_data_32_.Get()); if (data->elements == index_data_32_->elements) { return; // just keep our existing one } @@ -39,11 +39,11 @@ class MeshIndexedBase : public Mesh { } void SetIndexData(const Object::Ref& data) { - assert(data.exists() && !data->elements.empty()); + assert(data.Exists() && !data->elements.empty()); // unlike vertex data, index data might often remain the same, so lets test // for that and avoid some gl updates.. - if (index_data_16_.exists()) { - assert(index_data_16_.get()); + if (index_data_16_.Exists()) { + assert(index_data_16_.Get()); if (data->elements == index_data_16_->elements) { return; // just keep our existing one } @@ -64,9 +64,9 @@ class MeshIndexedBase : public Mesh { auto IsValid() const -> bool override { switch (index_data_size()) { case 4: - return (index_data_32_.exists() && !index_data_32_->elements.empty()); + return (index_data_32_.Exists() && !index_data_32_->elements.empty()); case 2: - return (index_data_16_.exists() && !index_data_16_->elements.empty()); + return (index_data_16_.Exists() && !index_data_16_->elements.empty()); default: return false; } @@ -87,9 +87,9 @@ class MeshIndexedBase : public Mesh { auto GetIndexData() const -> MeshBufferBase* { switch (index_data_size()) { case 4: - return index_data_32_.get(); + return index_data_32_.Get(); case 2: - return index_data_16_.get(); + return index_data_16_.Get(); default: throw Exception(); } @@ -102,6 +102,6 @@ class MeshIndexedBase : public Mesh { uint32_t index_state_ = 0; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_BASE_H_ +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_BASE_H_ diff --git a/src/ballistica/base/graphics/mesh/mesh_indexed_dual_texture_full.h b/src/ballistica/base/graphics/mesh/mesh_indexed_dual_texture_full.h new file mode 100644 index 00000000..e2906070 --- /dev/null +++ b/src/ballistica/base/graphics/mesh/mesh_indexed_dual_texture_full.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_DUAL_TEXTURE_FULL_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_DUAL_TEXTURE_FULL_H_ + +#include "ballistica/base/graphics/mesh/mesh_indexed.h" + +namespace ballistica::base { + +class MeshIndexedDualTextureFull + : public MeshIndexed { + using MeshIndexed::MeshIndexed; // wheee c++11 magic +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_DUAL_TEXTURE_FULL_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed_object_split.h b/src/ballistica/base/graphics/mesh/mesh_indexed_object_split.h similarity index 57% rename from src/ballistica/graphics/mesh/mesh_indexed_object_split.h rename to src/ballistica/base/graphics/mesh/mesh_indexed_object_split.h index c1f57a3b..5a3eaeb4 100644 --- a/src/ballistica/graphics/mesh/mesh_indexed_object_split.h +++ b/src/ballistica/base/graphics/mesh/mesh_indexed_object_split.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_OBJECT_SPLIT_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_OBJECT_SPLIT_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_OBJECT_SPLIT_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_OBJECT_SPLIT_H_ -#include "ballistica/graphics/mesh/mesh_indexed_static_dynamic.h" +#include "ballistica/base/graphics/mesh/mesh_indexed_static_dynamic.h" -namespace ballistica { +namespace ballistica::base { // a mesh with static indices and UVs and dynamic positions and normals class MeshIndexedObjectSplit @@ -15,6 +15,6 @@ class MeshIndexedObjectSplit using MeshIndexedStaticDynamic::MeshIndexedStaticDynamic; // c++11 magic! }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_OBJECT_SPLIT_H_ +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_OBJECT_SPLIT_H_ diff --git a/src/ballistica/base/graphics/mesh/mesh_indexed_simple_full.h b/src/ballistica/base/graphics/mesh/mesh_indexed_simple_full.h new file mode 100644 index 00000000..8d82e3d0 --- /dev/null +++ b/src/ballistica/base/graphics/mesh/mesh_indexed_simple_full.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_FULL_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_FULL_H_ + +#include "ballistica/base/graphics/mesh/mesh_indexed.h" + +namespace ballistica::base { + +// a simple mesh with all data provided together (either static or dynamic) +class MeshIndexedSimpleFull + : public MeshIndexed { + using MeshIndexed::MeshIndexed; // wheee c++11 magic +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_FULL_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed_simple_split.h b/src/ballistica/base/graphics/mesh/mesh_indexed_simple_split.h similarity index 56% rename from src/ballistica/graphics/mesh/mesh_indexed_simple_split.h rename to src/ballistica/base/graphics/mesh/mesh_indexed_simple_split.h index d74f3095..8f06746b 100644 --- a/src/ballistica/graphics/mesh/mesh_indexed_simple_split.h +++ b/src/ballistica/base/graphics/mesh/mesh_indexed_simple_split.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_SPLIT_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_SPLIT_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_SPLIT_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_SPLIT_H_ -#include "ballistica/graphics/mesh/mesh_indexed_static_dynamic.h" +#include "ballistica/base/graphics/mesh/mesh_indexed_static_dynamic.h" -namespace ballistica { +namespace ballistica::base { // a mesh with static indices and UVs and dynamic positions class MeshIndexedSimpleSplit @@ -15,6 +15,6 @@ class MeshIndexedSimpleSplit using MeshIndexedStaticDynamic::MeshIndexedStaticDynamic; // c++11 magic! }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_SPLIT_H_ +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_SPLIT_H_ diff --git a/src/ballistica/base/graphics/mesh/mesh_indexed_smoke_full.h b/src/ballistica/base/graphics/mesh/mesh_indexed_smoke_full.h new file mode 100644 index 00000000..57c53813 --- /dev/null +++ b/src/ballistica/base/graphics/mesh/mesh_indexed_smoke_full.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_SMOKE_FULL_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_SMOKE_FULL_H_ + +#include "ballistica/base/graphics/mesh/mesh_indexed.h" + +namespace ballistica::base { + +// a mesh with all data provided together (either static or dynamic) +class MeshIndexedSmokeFull + : public MeshIndexed { + using MeshIndexed::MeshIndexed; // wheee c++11 magic +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_SMOKE_FULL_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed_static_dynamic.h b/src/ballistica/base/graphics/mesh/mesh_indexed_static_dynamic.h similarity index 77% rename from src/ballistica/graphics/mesh/mesh_indexed_static_dynamic.h rename to src/ballistica/base/graphics/mesh/mesh_indexed_static_dynamic.h index eb792a64..50911081 100644 --- a/src/ballistica/graphics/mesh/mesh_indexed_static_dynamic.h +++ b/src/ballistica/base/graphics/mesh/mesh_indexed_static_dynamic.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_STATIC_DYNAMIC_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_STATIC_DYNAMIC_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_STATIC_DYNAMIC_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_STATIC_DYNAMIC_H_ -#include "ballistica/graphics/mesh/mesh_indexed_base.h" +#include "ballistica/base/graphics/mesh/mesh_indexed_base.h" -namespace ballistica { +namespace ballistica::base { // mesh with static indices, some static vertex data, // and some dynamic vertex data @@ -24,8 +24,8 @@ class MeshIndexedStaticDynamic : public MeshIndexedBase { dynamic_data_->state = ++dynamic_state_; } auto IsValid() const -> bool override { - if (!static_data_.exists() || static_data_->elements.empty() - || !dynamic_data_.exists() || dynamic_data_->elements.empty() + if (!static_data_.Exists() || static_data_->elements.empty() + || !dynamic_data_.Exists() || dynamic_data_->elements.empty() || !MeshIndexedBase::IsValid()) { return false; } @@ -54,6 +54,6 @@ class MeshIndexedStaticDynamic : public MeshIndexedBase { uint32_t dynamic_state_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_STATIC_DYNAMIC_H_ +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_INDEXED_STATIC_DYNAMIC_H_ diff --git a/src/ballistica/graphics/mesh/mesh_non_indexed.h b/src/ballistica/base/graphics/mesh/mesh_non_indexed.h similarity index 76% rename from src/ballistica/graphics/mesh/mesh_non_indexed.h rename to src/ballistica/base/graphics/mesh/mesh_non_indexed.h index e0509379..e9ee5a48 100644 --- a/src/ballistica/graphics/mesh/mesh_non_indexed.h +++ b/src/ballistica/base/graphics/mesh/mesh_non_indexed.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_NON_INDEXED_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_NON_INDEXED_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_NON_INDEXED_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_NON_INDEXED_H_ -#include "ballistica/graphics/mesh/mesh.h" +#include "ballistica/base/graphics/mesh/mesh.h" -namespace ballistica { +namespace ballistica::base { // Mesh using non-indexed vertex data. Good for situations where vertices // are never shared between primitives (such as drawing points/sprites/etc). @@ -37,6 +37,6 @@ class MeshNonIndexed : public Mesh { uint32_t data_state_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_MESH_MESH_NON_INDEXED_H_ +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_NON_INDEXED_H_ diff --git a/src/ballistica/base/graphics/mesh/mesh_renderer_data.h b/src/ballistica/base/graphics/mesh/mesh_renderer_data.h new file mode 100644 index 00000000..7261b3db --- /dev/null +++ b/src/ballistica/base/graphics/mesh/mesh_renderer_data.h @@ -0,0 +1,15 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_MESH_RENDERER_DATA_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_MESH_RENDERER_DATA_H_ + +namespace ballistica::base { + +class MeshRendererData { + public: + virtual ~MeshRendererData() = default; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_GRAPHICS_MESH_MESH_RENDERER_DATA_H_ diff --git a/src/ballistica/base/graphics/mesh/sprite_mesh.h b/src/ballistica/base/graphics/mesh/sprite_mesh.h new file mode 100644 index 00000000..333f45ac --- /dev/null +++ b/src/ballistica/base/graphics/mesh/sprite_mesh.h @@ -0,0 +1,17 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_SPRITE_MESH_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_SPRITE_MESH_H_ + +#include "ballistica/base/graphics/mesh/mesh_indexed.h" + +namespace ballistica::base { + +// an indexed sprite-mesh +class SpriteMesh : public MeshIndexed { + using MeshIndexed::MeshIndexed; // wheeee c++11 magic +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_GRAPHICS_MESH_SPRITE_MESH_H_ diff --git a/src/ballistica/graphics/mesh/text_mesh.cc b/src/ballistica/base/graphics/mesh/text_mesh.cc similarity index 95% rename from src/ballistica/graphics/mesh/text_mesh.cc rename to src/ballistica/base/graphics/mesh/text_mesh.cc index 25b0d19b..f2c7e378 100644 --- a/src/ballistica/graphics/mesh/text_mesh.cc +++ b/src/ballistica/base/graphics/mesh/text_mesh.cc @@ -1,13 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/mesh/text_mesh.h" +#include "ballistica/base/graphics/mesh/text_mesh.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/graphics/text/text_graphics.h" -#include "ballistica/graphics/text/text_packer.h" +#include "ballistica/base/graphics/text/text_graphics.h" +#include "ballistica/base/graphics/text/text_packer.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::base { TextMesh::TextMesh() : MeshIndexedDualTextureFull(MeshDrawType::kStatic) {} @@ -58,8 +57,8 @@ void TextMesh::SetText(const std::string& text_in, HAlign alignment_h, } auto vertices(Object::New>(4 * text_size)); - uint16_t* index16 = indices16.exists() ? indices16->elements.data() : nullptr; - uint32_t* index32 = indices32.exists() ? indices32->elements.data() : nullptr; + uint16_t* index16 = indices16.Exists() ? indices16->elements.data() : nullptr; + uint32_t* index32 = indices32.Exists() ? indices32->elements.data() : nullptr; VertexDualTextureFull* v = &vertices->elements[0]; uint32_t index_offset = 0; @@ -143,7 +142,7 @@ void TextMesh::SetText(const std::string& text_in, HAlign alignment_h, Rect r2; float width; std::string s = Utils::UTF8FromUnicode(os_span); - g_text_graphics->GetOSTextSpanBoundsAndWidth(s, &r2, &width); + g_base->text_graphics->GetOSTextSpanBoundsAndWidth(s, &r2, &width); if (packer) { packer->AddSpan(s, x_offset, y_offset, r2); } @@ -187,12 +186,12 @@ void TextMesh::SetText(const std::string& text_in, HAlign alignment_h, if (TextGraphics::IsOSDrawableAscii(val) && !os_span_l.empty()) { os_span_l.push_back(val); } else if (TextGraphics::Glyph* g = - g_text_graphics->GetGlyph(val, big)) { + g_base->text_graphics->GetGlyph(val, big)) { // Flipping back to glyphs; if we had been building an os_span, // tally it. if (!os_span_l.empty()) { std::string s = Utils::UTF8FromUnicode(os_span_l); - line_length += g_text_graphics->GetOSTextSpanWidth(s); + line_length += g_base->text_graphics->GetOSTextSpanWidth(s); os_span_l.clear(); } line_length += char_width * g->advance; @@ -210,7 +209,7 @@ void TextMesh::SetText(const std::string& text_in, HAlign alignment_h, // Add final os_span if there is one. if (!os_span_l.empty()) { std::string s = Utils::UTF8FromUnicode(os_span_l); - line_length += g_text_graphics->GetOSTextSpanWidth(s); + line_length += g_base->text_graphics->GetOSTextSpanWidth(s); os_span_l.clear(); } if (alignment_h == HAlign::kCenter) { @@ -248,14 +247,14 @@ void TextMesh::SetText(const std::string& text_in, HAlign alignment_h, if (TextGraphics::IsOSDrawableAscii(char_val) && !os_span.empty()) { os_span.push_back(char_val); } else if (TextGraphics::Glyph* glyph = - g_text_graphics->GetGlyph(char_val, big)) { + g_base->text_graphics->GetGlyph(char_val, big)) { // If we had been building up an OS-text span, // commit it since we're flipping to glyphs now. if (!os_span.empty()) { Rect r2; float width; std::string s = Utils::UTF8FromUnicode(os_span); - g_text_graphics->GetOSTextSpanBoundsAndWidth(s, &r2, &width); + g_base->text_graphics->GetOSTextSpanBoundsAndWidth(s, &r2, &width); if (packer) packer->AddSpan(s, x_offset, y_offset, r2); x_offset += width; os_span.clear(); @@ -352,7 +351,7 @@ void TextMesh::SetText(const std::string& text_in, HAlign alignment_h, Rect r2; float width; std::string s = Utils::UTF8FromUnicode(os_span); - g_text_graphics->GetOSTextSpanBoundsAndWidth(s, &r2, &width); + g_base->text_graphics->GetOSTextSpanBoundsAndWidth(s, &r2, &width); packer->AddSpan(s, x_offset, y_offset, r2); os_span.clear(); } @@ -567,4 +566,4 @@ void TextMesh::SetText(const std::string& text_in, HAlign alignment_h, } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/mesh/text_mesh.h b/src/ballistica/base/graphics/mesh/text_mesh.h similarity index 72% rename from src/ballistica/graphics/mesh/text_mesh.h rename to src/ballistica/base/graphics/mesh/text_mesh.h index ca73c141..27a854f6 100644 --- a/src/ballistica/graphics/mesh/text_mesh.h +++ b/src/ballistica/base/graphics/mesh/text_mesh.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_MESH_TEXT_MESH_H_ -#define BALLISTICA_GRAPHICS_MESH_TEXT_MESH_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_MESH_TEXT_MESH_H_ +#define BALLISTICA_BASE_GRAPHICS_MESH_TEXT_MESH_H_ #include -#include "ballistica/graphics/mesh/mesh_indexed_dual_texture_full.h" +#include "ballistica/base/graphics/mesh/mesh_indexed_dual_texture_full.h" -namespace ballistica { +namespace ballistica::base { // a mesh set up to draw text // in general you should not use this directly; use TextGroup below, which will @@ -27,6 +27,6 @@ class TextMesh : public MeshIndexedDualTextureFull { std::string text_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_MESH_TEXT_MESH_H_ +#endif // BALLISTICA_BASE_GRAPHICS_MESH_TEXT_MESH_H_ diff --git a/src/ballistica/base/graphics/renderer/framebuffer.h b/src/ballistica/base/graphics/renderer/framebuffer.h new file mode 100644 index 00000000..0dc84928 --- /dev/null +++ b/src/ballistica/base/graphics/renderer/framebuffer.h @@ -0,0 +1,19 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_GRAPHICS_RENDERER_FRAMEBUFFER_H_ +#define BALLISTICA_BASE_GRAPHICS_RENDERER_FRAMEBUFFER_H_ + +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::base { + +class Framebuffer : public Object { + public: + auto GetDefaultOwnerThread() const -> EventLoopID override { + return EventLoopID::kMain; + } +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_GRAPHICS_RENDERER_FRAMEBUFFER_H_ diff --git a/src/ballistica/graphics/render_pass.cc b/src/ballistica/base/graphics/renderer/render_pass.cc similarity index 82% rename from src/ballistica/graphics/render_pass.cc rename to src/ballistica/base/graphics/renderer/render_pass.cc index 22e577ae..f1642c40 100644 --- a/src/ballistica/graphics/render_pass.cc +++ b/src/ballistica/base/graphics/renderer/render_pass.cc @@ -1,14 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/render_pass.h" +#include "ballistica/base/graphics/renderer/render_pass.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/renderer.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/graphics/renderer/renderer.h" // Turn this off to not draw any transparent stuff. #define DRAW_TRANSPARENT 1 -namespace ballistica { +namespace ballistica::base { const float kCamNearClip = 4.0f; const float kCamFarClip = 1000.0f; @@ -36,19 +36,19 @@ RenderPass::RenderPass(RenderPass::Type type_in, FrameDef* frame_def_in) RenderPass::~RenderPass() = default; void RenderPass::Render(RenderTarget* render_target, bool transparent) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); if (explicit_bool(!DRAW_TRANSPARENT) && transparent) { return; } #undef DRAW_TRANSPRENT - Renderer* renderer = g_graphics_server->renderer(); + Renderer* renderer = g_base->graphics_server->renderer(); // Set up camera & depth. switch (type()) { case Type::kBeautyPass: { - g_graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); + g_base->graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); // If this changes, make sure to change // it before _drawCameraBuffer() too. @@ -60,10 +60,11 @@ void RenderPass::Render(RenderTarget* render_target, bool transparent) { renderer->SetDepthRange(kBackingDepth3, kBackingDepth4); SetFrustum(cam_near_clip_, cam_far_clip_); - tex_project_matrix_ = g_graphics_server->GetModelViewProjectionMatrix(); - model_view_matrix_ = g_graphics_server->model_view_matrix(); + tex_project_matrix_ = + g_base->graphics_server->GetModelViewProjectionMatrix(); + model_view_matrix_ = g_base->graphics_server->model_view_matrix(); model_view_projection_matrix_ = - g_graphics_server->GetModelViewProjectionMatrix(); + g_base->graphics_server->GetModelViewProjectionMatrix(); // Store our matrix to get things in screen space. tex_project_matrix_ *= Matrix44fScale(0.5f); @@ -71,7 +72,7 @@ void RenderPass::Render(RenderTarget* render_target, bool transparent) { break; } case Type::kOverlay3DPass: { - g_graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); + g_base->graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); // If we drew the world directly to the screen we need to use a depth // range that lies fully in front of that range so we don't get obscured @@ -94,7 +95,7 @@ void RenderPass::Render(RenderTarget* render_target, bool transparent) { break; } case Type::kVRCoverPass: { - g_graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); + g_base->graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); // We use the front depth range where the overlays would // live in the non-vr path. @@ -103,7 +104,7 @@ void RenderPass::Render(RenderTarget* render_target, bool transparent) { break; } case Type::kBlitPass: { - g_graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); + g_base->graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); // We render into a little sliver of the depth buffer in the // back just in front of the backing blit. @@ -113,7 +114,7 @@ void RenderPass::Render(RenderTarget* render_target, bool transparent) { break; } case Type::kBeautyPassBG: { - g_graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); + g_base->graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); renderer->SetDepthRange(kBackingDepth3, kBackingDepth4); SetFrustum(cam_near_clip_, cam_far_clip_); break; @@ -124,20 +125,20 @@ void RenderPass::Render(RenderTarget* render_target, bool transparent) { case Type::kOverlayFlatPass: { // In vr mode we draw the flat-overlay into its own buffer so can use // the full depth range (shouldn't matter but why not?...) shouldn't. - if (IsVRMode()) { + if (g_core->IsVRMode()) { // In vr mode, our overlay-flat pass is ortho-projected // while our regular overlay is just rendered in world space using // the vr-overlay matrix. if (type() == Type::kOverlayFlatPass) { - g_graphics_server->ModelViewReset(); + g_base->graphics_server->ModelViewReset(); renderer->SetDepthRange(0, 1); // we can use full depth range!! float amt = 0.5f * kVRBorder; float w = virtual_width(); float h = virtual_height(); - g_graphics_server->SetOrthoProjection( + g_base->graphics_server->SetOrthoProjection( -amt * w, (1.0f + amt) * w, -amt * h, (1.0f + amt) * h, -1, 1); } else { - g_graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); + g_base->graphics_server->SetCamera(cam_pos_, cam_target_, cam_up_); // We set the same depth ranges as the overlay-3d pass since // we're essentially doing the same thing. See explanation in the // overlay-3d case above the one difference is that we split the @@ -165,31 +166,31 @@ void RenderPass::Render(RenderTarget* render_target, bool transparent) { // Now move to wherever our 2d plane in space is to start with. if (type() == Type::kOverlayPass || type() == Type::kOverlayFrontPass) { - g_graphics_server->MultMatrix( + g_base->graphics_server->MultMatrix( frame_def()->vr_overlay_screen_matrix()); } else { assert(type() == Type::kOverlayFixedPass); - g_graphics_server->MultMatrix( + g_base->graphics_server->MultMatrix( frame_def()->vr_overlay_screen_matrix_fixed()); } } } else { // Nn non-vr mode both our overlays are just ortho projected. - g_graphics_server->ModelViewReset(); + g_base->graphics_server->ModelViewReset(); if (type() == Type::kOverlayFrontPass) { renderer->SetDepthRange(kBackingDepth1, kBackingDepth1B); } else { renderer->SetDepthRange(kBackingDepth1B, kBackingDepth2); } - if (g_graphics_server->tv_border()) { + if (g_base->graphics_server->tv_border()) { float amt = 0.5f * kTVBorder; float w = virtual_width(); float h = virtual_height(); - g_graphics_server->SetOrthoProjection( + g_base->graphics_server->SetOrthoProjection( -amt * w, (1.0f + amt) * w, -amt * h, (1.0f + amt) * h, -1, 1); } else { - g_graphics_server->SetOrthoProjection(0, virtual_width(), 0, - virtual_height(), -1, 1); + g_base->graphics_server->SetOrthoProjection(0, virtual_width(), 0, + virtual_height(), -1, 1); } } break; @@ -198,15 +199,17 @@ void RenderPass::Render(RenderTarget* render_target, bool transparent) { case Type::kLightShadowPass: { // Ortho shadows. if (renderer->shadow_ortho()) { - g_graphics_server->ModelViewReset(); - g_graphics_server->SetOrthoProjection(-12, 12, -12, 12, 10, 100); - g_graphics_server->Translate(Vector3f(0, 0, renderer->light_tz())); - g_graphics_server->Rotate(80, Vector3f(1.0f, 0, 0)); + g_base->graphics_server->ModelViewReset(); + g_base->graphics_server->SetOrthoProjection(-12, 12, -12, 12, 10, 100); + g_base->graphics_server->Translate( + Vector3f(0, 0, renderer->light_tz())); + g_base->graphics_server->Rotate(80, Vector3f(1.0f, 0, 0)); const Vector3f& soffs = renderer->shadow_offset(); - g_graphics_server->Translate(Vector3f(-soffs.x, -soffs.y, -soffs.z)); - g_graphics_server->scale(Vector3f(1.0f / renderer->shadow_scale_x(), - 1.0f, - 1.0f / renderer->shadow_scale_z())); + g_base->graphics_server->Translate( + Vector3f(-soffs.x, -soffs.y, -soffs.z)); + g_base->graphics_server->scale( + Vector3f(1.0f / renderer->shadow_scale_x(), 1.0f, + 1.0f / renderer->shadow_scale_z())); } else { float fovy = 45.0f * kPi / 180.0f; float fovx = fovy; @@ -215,34 +218,36 @@ void RenderPass::Render(RenderTarget* render_target, bool transparent) { float x = near_val * tanf(fovx); float y = near_val * tanf(fovy); - g_graphics_server->SetProjectionMatrix( + g_base->graphics_server->SetProjectionMatrix( Matrix44fFrustum(-x, x, -y, y, near_val, far_val)); - g_graphics_server->ModelViewReset(); - g_graphics_server->Translate( + g_base->graphics_server->ModelViewReset(); + g_base->graphics_server->Translate( Vector3f(0.0f, 0.0f, renderer->light_tz())); - g_graphics_server->Rotate(renderer->light_pitch(), - Vector3f(1.0f, 0.0f, 0.0f)); - g_graphics_server->Rotate(renderer->light_heading(), - Vector3f(0.0f, 1.0f, 0.0f)); + g_base->graphics_server->Rotate(renderer->light_pitch(), + Vector3f(1.0f, 0.0f, 0.0f)); + g_base->graphics_server->Rotate(renderer->light_heading(), + Vector3f(0.0f, 1.0f, 0.0f)); const Vector3f& soffs = renderer->shadow_offset(); // Well, this is slightly terrifying; '-soffs' is causing crashes // here but multing by -1.000001f works. // (generally just on android 4.3 on atom processors) - g_graphics_server->Translate(Vector3f( + g_base->graphics_server->Translate(Vector3f( -1.000001f * soffs.x, -1.000001f * soffs.y, -1.000001f * soffs.z)); } // ...now store the matrix we'll use to project this as a texture // FIXME: most of these calculations could be cached instead of // redoing them every pass - tex_project_matrix_ = g_graphics_server->GetModelViewProjectionMatrix(); - model_view_matrix_ = g_graphics_server->model_view_matrix(); + tex_project_matrix_ = + g_base->graphics_server->GetModelViewProjectionMatrix(); + model_view_matrix_ = g_base->graphics_server->model_view_matrix(); model_view_projection_matrix_ = - g_graphics_server->GetModelViewProjectionMatrix(); + g_base->graphics_server->GetModelViewProjectionMatrix(); tex_project_matrix_ *= Matrix44fScale(0.5f); tex_project_matrix_ *= Matrix44fTranslate(0.5f, 0.5f, 0); - g_graphics_server->SetLightShadowProjectionMatrix(tex_project_matrix_); + g_base->graphics_server->SetLightShadowProjectionMatrix( + tex_project_matrix_); break; } @@ -273,9 +278,9 @@ void RenderPass::Render(RenderTarget* render_target, bool transparent) { && frame_def()->quality() >= GraphicsQuality::kHigher) { doing_reflection = true; renderer->set_drawing_reflection(true); - g_graphics_server->PushTransform(); + g_base->graphics_server->PushTransform(); Matrix44f m = Matrix44fScale(Vector3f(1, -1, 1)); - g_graphics_server->MultMatrix(m); + g_base->graphics_server->MultMatrix(m); renderer->FlipCullFace(); // Flip into reflection drawing. } else { continue; @@ -346,7 +351,7 @@ void RenderPass::Render(RenderTarget* render_target, bool transparent) { if (doing_reflection) { renderer->FlipCullFace(); // Flip out of reflection drawing. - g_graphics_server->PopTransform(); + g_base->graphics_server->PopTransform(); } } renderer->set_drawing_reflection(false); @@ -397,7 +402,7 @@ void RenderPass::Reset() { cam_fov_y_ = 40.0f; tex_project_matrix_ = kMatrix44fIdentity; - Renderer* renderer = g_graphics_server->renderer(); + Renderer* renderer = g_base->graphics_server->renderer(); // Figure our our width/height for drawing commands to reference // (we cant wait until the drawing is actually occurring because @@ -412,8 +417,8 @@ void RenderPass::Reset() { case Type::kVRCoverPass: case Type::kOverlayFixedPass: case Type::kBlitPass: - physical_width_ = g_graphics->screen_pixel_width(); - physical_height_ = g_graphics->screen_pixel_height(); + physical_width_ = g_base->graphics->screen_pixel_width(); + physical_height_ = g_base->graphics->screen_pixel_height(); break; case Type::kLightPass: physical_width_ = physical_height_ = @@ -434,8 +439,8 @@ void RenderPass::Reset() { case Type::kOverlayFrontPass: case Type::kOverlayFixedPass: case Type::kOverlayFlatPass: - virtual_width_ = g_graphics->screen_virtual_width(); - virtual_height_ = g_graphics->screen_virtual_height(); + virtual_width_ = g_base->graphics->screen_virtual_width(); + virtual_height_ = g_base->graphics->screen_virtual_height(); break; default: virtual_width_ = physical_width_; @@ -455,7 +460,7 @@ void RenderPass::Reset() { } void RenderPass::SetFrustum(float near_val, float far_val) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); // If we're using fov-tangents: if (cam_use_fov_tangents_) { float l = near_val * cam_fov_l_tan_; @@ -478,7 +483,7 @@ void RenderPass::SetFrustum(float near_val, float far_val) { } projection_matrix_ = Matrix44fFrustum(-x, x, -y, y, near_val, far_val); } - g_graphics_server->SetProjectionMatrix(projection_matrix_); + g_base->graphics_server->SetProjectionMatrix(projection_matrix_); } void RenderPass::Finalize() { @@ -501,4 +506,4 @@ auto RenderPass::HasDrawCommands() const -> bool { } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/render_pass.h b/src/ballistica/base/graphics/renderer/render_pass.h similarity index 94% rename from src/ballistica/graphics/render_pass.h rename to src/ballistica/base/graphics/renderer/render_pass.h index 6c794519..3c7fa7f0 100644 --- a/src/ballistica/graphics/render_pass.h +++ b/src/ballistica/base/graphics/renderer/render_pass.h @@ -1,14 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_RENDER_PASS_H_ -#define BALLISTICA_GRAPHICS_RENDER_PASS_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_RENDERER_RENDER_PASS_H_ +#define BALLISTICA_BASE_GRAPHICS_RENDERER_RENDER_PASS_H_ #include #include -#include "ballistica/math/matrix44f.h" +#include "ballistica/base/base.h" +#include "ballistica/shared/math/matrix44f.h" -namespace ballistica { +namespace ballistica::base { // A drawing context for one pass. This can be a render to the screen, a shadow // pass, a window, etc. @@ -162,6 +163,6 @@ class RenderPass { float virtual_height_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_RENDER_PASS_H_ +#endif // BALLISTICA_BASE_GRAPHICS_RENDERER_RENDER_PASS_H_ diff --git a/src/ballistica/graphics/render_target.cc b/src/ballistica/base/graphics/renderer/render_target.cc similarity index 50% rename from src/ballistica/graphics/render_target.cc rename to src/ballistica/base/graphics/renderer/render_target.cc index 4f31918d..34ebd6b6 100644 --- a/src/ballistica/graphics/render_target.cc +++ b/src/ballistica/base/graphics/renderer/render_target.cc @@ -1,67 +1,72 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/render_target.h" +#include "ballistica/base/graphics/renderer/render_target.h" -#include "ballistica/graphics/graphics_server.h" +#include "ballistica/base/graphics/graphics_server.h" -namespace ballistica { +namespace ballistica::base { RenderTarget::RenderTarget(Type type) : type_(type) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); } RenderTarget::~RenderTarget() = default; void RenderTarget::ScreenSizeChanged() { assert(type_ == Type::kScreen); - physical_width_ = g_graphics_server->screen_pixel_width(); - physical_height_ = g_graphics_server->screen_pixel_height(); + physical_width_ = g_base->graphics_server->screen_pixel_width(); + physical_height_ = g_base->graphics_server->screen_pixel_height(); } auto RenderTarget::GetScissorX(float x) const -> float { - if (IsVRMode()) { + assert(g_core); + if (g_core->IsVRMode()) { // map -0.05f to 1.1f in logical coordinates to 0 to 1 physical ones - float res_x_virtual = g_graphics_server->screen_virtual_width(); + float res_x_virtual = g_base->graphics_server->screen_virtual_width(); return physical_width_ * (((x / res_x_virtual) + (kVRBorder * 0.5f)) / (1.0f + kVRBorder)); } else { - if (g_graphics_server->tv_border()) { + if (g_base->graphics_server->tv_border()) { // map -0.05f to 1.1f in logical coordinates to 0 to 1 physical ones - float res_x_virtual = g_graphics_server->screen_virtual_width(); + float res_x_virtual = g_base->graphics_server->screen_virtual_width(); return physical_width_ * (((x / res_x_virtual) + (kTVBorder * 0.5f)) / (1.0f + kTVBorder)); } else { - return (physical_width_ / g_graphics_server->screen_virtual_width()) * x; + return (physical_width_ / g_base->graphics_server->screen_virtual_width()) + * x; } } } auto RenderTarget::GetScissorY(float y) const -> float { - if (IsVRMode()) { + assert(g_core); + if (g_core->IsVRMode()) { // map -0.05f to 1.1f in logical coordinates to 0 to 1 physical ones - float res_y_virtual = g_graphics_server->screen_virtual_height(); + float res_y_virtual = g_base->graphics_server->screen_virtual_height(); return physical_height_ * (((y / res_y_virtual) + (kVRBorder * 0.5f)) / (1.0f + kVRBorder)); } else { - if (g_graphics_server->tv_border()) { + if (g_base->graphics_server->tv_border()) { // map -0.05f to 1.1f in logical coordinates to 0 to 1 physical ones - float res_y_virtual = g_graphics_server->screen_virtual_height(); + float res_y_virtual = g_base->graphics_server->screen_virtual_height(); return physical_height_ * (((y / res_y_virtual) + (kTVBorder * 0.5f)) / (1.0f + kTVBorder)); } else { - return (physical_height_ / g_graphics_server->screen_virtual_height()) + return (physical_height_ + / g_base->graphics_server->screen_virtual_height()) * y; } } } auto RenderTarget::GetScissorScaleX() const -> float { - if (IsVRMode()) { - float f = physical_width_ / g_graphics_server->screen_virtual_width(); + assert(g_core); + if (g_core->IsVRMode()) { + float f = physical_width_ / g_base->graphics_server->screen_virtual_width(); return f / (1.0f + kVRBorder); } else { - float f = physical_width_ / g_graphics_server->screen_virtual_width(); - if (g_graphics_server->tv_border()) { + float f = physical_width_ / g_base->graphics_server->screen_virtual_width(); + if (g_base->graphics_server->tv_border()) { return f / (1.0f + kTVBorder); } return f; @@ -69,16 +74,19 @@ auto RenderTarget::GetScissorScaleX() const -> float { } auto RenderTarget::GetScissorScaleY() const -> float { - if (IsVRMode()) { - float f = physical_height_ / g_graphics_server->screen_virtual_height(); + assert(g_core); + if (g_core->IsVRMode()) { + float f = + physical_height_ / g_base->graphics_server->screen_virtual_height(); return f / (1.0f + kVRBorder); } else { - float f = physical_height_ / g_graphics_server->screen_virtual_height(); - if (g_graphics_server->tv_border()) { + float f = + physical_height_ / g_base->graphics_server->screen_virtual_height(); + if (g_base->graphics_server->tv_border()) { return f / (1.0f + kTVBorder); } return f; } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/render_target.h b/src/ballistica/base/graphics/renderer/render_target.h similarity index 71% rename from src/ballistica/graphics/render_target.h rename to src/ballistica/base/graphics/renderer/render_target.h index 721a446e..26324205 100644 --- a/src/ballistica/graphics/render_target.h +++ b/src/ballistica/base/graphics/renderer/render_target.h @@ -1,18 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_RENDER_TARGET_H_ -#define BALLISTICA_GRAPHICS_RENDER_TARGET_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_RENDERER_RENDER_TARGET_H_ +#define BALLISTICA_BASE_GRAPHICS_RENDERER_RENDER_TARGET_H_ -#include "ballistica/core/object.h" -#include "ballistica/math/vector4f.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/math/vector4f.h" -namespace ballistica { +namespace ballistica::base { // Encapsulates framebuffers, main windows, etc. class RenderTarget : public Object { public: - auto GetDefaultOwnerThread() const -> ThreadTag override { - return ThreadTag::kMain; + auto GetDefaultOwnerThread() const -> EventLoopID override { + return EventLoopID::kMain; } enum class Type { kScreen, kFramebuffer }; explicit RenderTarget(Type type); @@ -42,6 +42,6 @@ class RenderTarget : public Object { Type type_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_RENDER_TARGET_H_ +#endif // BALLISTICA_BASE_GRAPHICS_RENDERER_RENDER_TARGET_H_ diff --git a/src/ballistica/graphics/renderer.cc b/src/ballistica/base/graphics/renderer/renderer.cc similarity index 91% rename from src/ballistica/graphics/renderer.cc rename to src/ballistica/base/graphics/renderer/renderer.cc index 43c9ddb1..7ae07e57 100644 --- a/src/ballistica/graphics/renderer.cc +++ b/src/ballistica/base/graphics/renderer/renderer.cc @@ -1,20 +1,20 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/renderer.h" +#include "ballistica/base/graphics/renderer/renderer.h" -#include "ballistica/graphics/graphics_server.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/core/core.h" // FIXME: Clear out conditional stuff. #if BA_OSTYPE_MACOS && BA_SDL_BUILD && !BA_SDL2_BUILD -#include "ballistica/platform/min_sdl.h" +#include "ballistica/core/platform/support/min_sdl.h" #endif #if BA_VR_BUILD -#include "ballistica/app/app.h" -#include "ballistica/graphics/vr_graphics.h" +#include "ballistica/base/graphics/graphics_vr.h" #endif -namespace ballistica { +namespace ballistica::base { #if BA_VR_BUILD const float kBaseVRWorldScale = 1.38f; @@ -35,11 +35,11 @@ Renderer::~Renderer() { } void Renderer::PreprocessFrameDef(FrameDef* frame_def) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); // If this frame_def was made in a different quality mode than we're - // currently in, don't try to render it. - if (frame_def->quality() != g_graphics_server->quality()) { + // currently in, don't attempt to render it. + if (frame_def->quality() != g_base->graphics_server->quality()) { frame_def->set_rendering(false); return; } @@ -48,7 +48,7 @@ void Renderer::PreprocessFrameDef(FrameDef* frame_def) { // Some VR environments muck with render states before/after // they call us; resync as needed.... #if BA_VR_BUILD - if (IsVRMode()) { + if (g_core->IsVRMode()) { VRSyncRenderStates(); } #endif // BA_VR_BUILD @@ -96,7 +96,7 @@ void Renderer::PreprocessFrameDef(FrameDef* frame_def) { // actually render one of these frame_def suckers... // (called within the graphics thread) void Renderer::RenderFrameDef(FrameDef* frame_def) { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); // If preprocess decided not to render this. if (!frame_def->rendering()) return; @@ -111,7 +111,7 @@ void Renderer::RenderFrameDef(FrameDef* frame_def) { // In higher-quality modes we draw the world into the camera buffer // which we'll later render into the backing buffer with depth-of-field // and other stuff added. - if (camera_render_target_.exists()) { + if (camera_render_target_.Exists()) { DrawWorldToCameraBuffer(frame_def); } @@ -121,7 +121,7 @@ void Renderer::RenderFrameDef(FrameDef* frame_def) { SetDepthWriting(true); SetDepthTesting(true); RenderTarget* backing; - if (backing_render_target_.exists()) { + if (backing_render_target_.Exists()) { backing = backing_render_target(); } else { backing = screen_render_target(); @@ -140,7 +140,7 @@ void Renderer::RenderFrameDef(FrameDef* frame_def) { #endif backing->DrawBegin(backing_needs_clear); - bool overlays_in_3d = IsVRMode(); + bool overlays_in_3d = g_core->IsVRMode(); bool overlays_in_2d = !overlays_in_3d; // Draw opaque stuff front-to-back. @@ -160,7 +160,7 @@ void Renderer::RenderFrameDef(FrameDef* frame_def) { frame_def->overlay_pass()->Render(backing, false); frame_def->overlay_fixed_pass()->Render(backing, false); } - if (camera_render_target_.exists()) { + if (camera_render_target_.Exists()) { UpdateDOFParams(frame_def); // We've already drawn the world. // Now just draw our blit shapes (opaque shapes which blit portions of the @@ -184,7 +184,7 @@ void Renderer::RenderFrameDef(FrameDef* frame_def) { SetDrawAtEqualDepth(true); // Now draw transparent stuff back to front. - if (camera_render_target_.exists()) { + if (camera_render_target_.Exists()) { // When copying camera buffer to the backing there's nothing transparent // to draw. } else { @@ -216,7 +216,7 @@ void Renderer::RenderFrameDef(FrameDef* frame_def) { PopGroupMarker(); // If we've been drawing to a backing buffer, blit it to the screen. - if (backing_render_target_.exists()) { + if (backing_render_target_.Exists()) { // FIXME - should we just be discarding both depth and color // after the blit?.. (of course, this code path shouldn't be used on // mobile/slow-stuff so maybe it doesn't matter) @@ -247,12 +247,12 @@ void Renderer::FinishFrameDef(FrameDef* frame_def) { #if BA_VR_BUILD void Renderer::VRPreprocess(FrameDef* frame_def) { - if (!IsVRMode()) { + if (!g_core->IsVRMode()) { return; } // if we're in VR mode, make sure we've got our VR overlay target - if (!vr_overlay_flat_render_target_.exists()) { + if (!vr_overlay_flat_render_target_.Exists()) { // find this res to be ideal on current gen equipment // (2017-ish, 1st gen rift/gear-vr/etc) // ..can revisit once higher-res stuff is commonplace @@ -271,7 +271,7 @@ void Renderer::VRPreprocess(FrameDef* frame_def) { true // alpha ); // NOLINT(whitespace/parens) } - auto* vrgraphics = VRGraphics::get(); + auto* vrgraphics = GraphicsVR::get(); // Also store our custom near clip plane dist. frame_def->set_vr_near_clip(vrgraphics->vr_near_clip()); @@ -280,7 +280,7 @@ void Renderer::VRPreprocess(FrameDef* frame_def) { frame_def->cam_original().z); float world_scale = - kBaseVRWorldScale * VRGraphics::get()->vr_test_head_scale(); + kBaseVRWorldScale * GraphicsVR::get()->vr_test_head_scale(); float extra_yaw = (frame_def->camera_mode() == CameraMode::kOrbit) ? -0.3f : 0.0f; @@ -317,8 +317,8 @@ void Renderer::VRPreprocess(FrameDef* frame_def) { * Matrix44fTranslate(vr_raw_head_tx_, vr_raw_head_ty_, vr_raw_head_tz_) * vr_base_transform_; - if (g_app->reset_vr_orientation) { - g_app->reset_vr_orientation = false; + if (g_core->reset_vr_orientation) { + g_core->reset_vr_orientation = false; } Vector3f translate = vr_transform_head_.GetTranslate(); @@ -332,12 +332,12 @@ void Renderer::VRPreprocess(FrameDef* frame_def) { } void Renderer::VRUpdateForEyeRender(FrameDef* frame_def) { - if (!IsVRMode()) { + if (!g_core->IsVRMode()) { return; } VREyeRenderBegin(); float world_scale = - kBaseVRWorldScale * VRGraphics::get()->vr_test_head_scale(); + kBaseVRWorldScale * GraphicsVR::get()->vr_test_head_scale(); Matrix44f eye_transform = Matrix44fRotate(Vector3f(0, 0, 1), -vr_eye_roll_ * kDegPi) * Matrix44fRotate(Vector3f(1, 0, 0), -vr_eye_pitch_ * kDegPi) @@ -383,7 +383,7 @@ void Renderer::VRUpdateForEyeRender(FrameDef* frame_def) { } void Renderer::VRDrawOverlayFlatPass(FrameDef* frame_def) { - if (IsVRMode()) { + if (g_core->IsVRMode()) { // The overlay-flat pass should generally only have commands in it // when UI is visible; skip rendering it if not. if (frame_def->overlay_flat_pass()->HasDrawCommands()) { @@ -409,14 +409,14 @@ void Renderer::VRDrawOverlayFlatPass(FrameDef* frame_def) { } void Renderer::VRTransformToRightHand() { - g_graphics_server->MultMatrix(vr_transform_right_hand_); + g_base->graphics_server->MultMatrix(vr_transform_right_hand_); } void Renderer::VRTransformToLeftHand() { - g_graphics_server->MultMatrix(vr_transform_left_hand_); + g_base->graphics_server->MultMatrix(vr_transform_left_hand_); } void Renderer::VRTransformToHead() { - g_graphics_server->MultMatrix(vr_transform_head_); + g_base->graphics_server->MultMatrix(vr_transform_head_); } #endif // BA_VR_BUILD @@ -439,7 +439,7 @@ void Renderer::UpdateSizesQualitiesAndColors(FrameDef* frame_def) { if (last_render_quality_ != frame_def->quality()) { light_render_target_.Clear(); light_shadow_render_target_.Clear(); - if (IsVRMode()) { + if (g_core->IsVRMode()) { vr_overlay_flat_render_target_.Clear(); } } @@ -452,7 +452,7 @@ void Renderer::UpdateSizesQualitiesAndColors(FrameDef* frame_def) { set_tint(1.5f * frame_def->tint()); // FIXME; why the 1.5? set_ambient_color(frame_def->ambient_color()); set_vignette_inner(frame_def->vignette_inner()); - if (IsVRMode()) { + if (g_core->IsVRMode()) { // In VR mode we dont want vignetting; // just use the inner color for both in and out. set_vignette_outer(frame_def->vignette_inner()); @@ -463,8 +463,8 @@ void Renderer::UpdateSizesQualitiesAndColors(FrameDef* frame_def) { } void Renderer::UpdateLightAndShadowBuffers(FrameDef* frame_def) { - if (!light_render_target_.exists() || !light_shadow_render_target_.exists()) { - assert(screen_render_target_.exists()); + if (!light_render_target_.Exists() || !light_shadow_render_target_.Exists()) { + assert(screen_render_target_.Exists()); // Base shadow res on quality. if (frame_def->quality() >= GraphicsQuality::kHigher) { @@ -480,7 +480,7 @@ void Renderer::UpdateLightAndShadowBuffers(FrameDef* frame_def) { // 16 bit dithering is a bit noticeable here.. bool high_qual = true; - light_render_target_ = Object::MakeRefCounted(NewFramebufferRenderTarget( + light_render_target_ = NewFramebufferRenderTarget( shadow_res_ / kLightResDiv, shadow_res_ / kLightResDiv, true, // linear_interp false, // depth @@ -489,8 +489,8 @@ void Renderer::UpdateLightAndShadowBuffers(FrameDef* frame_def) { high_qual, // high-quality false, // msaa false // alpha - )); // NOLINT(whitespace/parens) - light_shadow_render_target_ = Object::MakeRefCounted( + ); // NOLINT(whitespace/parens) + light_shadow_render_target_ = NewFramebufferRenderTarget(shadow_res_, shadow_res_, true, // linear_interp false, // depth @@ -499,7 +499,7 @@ void Renderer::UpdateLightAndShadowBuffers(FrameDef* frame_def) { high_qual, // high-quality false, // msaa false // alpha - )); // NOLINT(whitespace/parens) + ); // NOLINT(whitespace/parens) } } @@ -532,7 +532,7 @@ void Renderer::UpdateCameraRenderTargets(FrameDef* frame_def) { // In higher-quality modes we render the world into a buffer // so we can do depth-of-field filtering and whatnot. if (frame_def->quality() >= GraphicsQuality::kHigh) { - if (!camera_render_target_.exists()) { + if (!camera_render_target_.Exists()) { float pixel_scale_fin = std::min(1.0f, std::max(0.1f, pixel_scale_)); int w = static_cast(screen_render_target_->physical_width() * pixel_scale_fin); @@ -568,16 +568,15 @@ void Renderer::UpdateCameraRenderTargets(FrameDef* frame_def) { } w = ((w % foo == 0) ? w : (w + (foo - (w % foo)))); h = ((h % foo == 0) ? h : (h + (foo - (h % foo)))); - camera_render_target_ = Object::MakeRefCounted(NewFramebufferRenderTarget( - w, h, - true, // linear-interp - true, // depth - true, // tex - true, // depth-tex - false, // high-qual - false, // msaa - false // alpha - )); // NOLINT(whitespace/parens) + camera_render_target_ = NewFramebufferRenderTarget(w, h, + true, // linear-interp + true, // depth + true, // tex + true, // depth-tex + false, // high-qual + false, // msaa + false // alpha + ); // NOLINT(whitespace/parens) // If screen size just changed or whatnot, // update whether we should do msaa. @@ -620,7 +619,7 @@ void Renderer::UpdatePixelScaleAndBackingBuffer(FrameDef* frame_def) { // We need our backing buffer for non-1.0 pixel-scales. if (pixel_scale_requested_ != 1.0f) { if (pixel_scale_requested_ != pixel_scale_ - || !backing_render_target_.exists()) { + || !backing_render_target_.Exists()) { float pixel_scale_fin = std::min(1.0f, std::max(0.1f, pixel_scale_requested_)); int w = static_cast(screen_render_target_->physical_width() @@ -640,7 +639,7 @@ void Renderer::UpdatePixelScaleAndBackingBuffer(FrameDef* frame_def) { } } else { // Otherwise we don't need backing buffer. Kill it if it exists. - if (backing_render_target_.exists()) { + if (backing_render_target_.Exists()) { backing_render_target_.Clear(); } } @@ -648,9 +647,9 @@ void Renderer::UpdatePixelScaleAndBackingBuffer(FrameDef* frame_def) { } void Renderer::LoadMedia(FrameDef* frame_def) { - millisecs_t t = GetRealTime(); + millisecs_t t = g_core->GetAppTimeMillisecs(); for (auto&& i : frame_def->media_components()) { - AssetComponentData* mc = i.get(); + Asset* mc = i.Get(); assert(mc); mc->Load(); @@ -665,7 +664,7 @@ void Renderer::HandleFunkyMacGammaIssue(FrameDef* frame_def) { // default about 1 second after a res change, etc... // so if we're using a non-1.0 gamma, lets keep setting it periodically // to force the issue - millisecs_t t = GetRealTime(); + millisecs_t t = g_core->GetAppTimeMillisecs(); if (screen_gamma_requested_ != screen_gamma_ || (t - last_screen_gamma_update_time_ > 300 && screen_gamma_ != 1.0f)) { screen_gamma_ = screen_gamma_requested_; @@ -756,7 +755,7 @@ void Renderer::UpdateDOFParams(FrameDef* frame_def) { } void Renderer::ScreenSizeChanged() { - assert(InGraphicsThread()); + assert(g_base->InGraphicsThread()); // We can actually get these events at times when we don't have a valid // gl context, so instead of doing any GL work here let's just make a note to @@ -775,7 +774,7 @@ void Renderer::Unload() { } void Renderer::Load() { - screen_render_target_ = Object::MakeRefCounted(NewScreenRenderTarget()); + screen_render_target_ = Object::CompleteDeferred(NewScreenRenderTarget()); // Restore current gamma value. if (screen_gamma_ != 1.0f) { @@ -843,4 +842,4 @@ auto Renderer::GetAutoAndroidRes() -> std::string { throw Exception("This should be overridden."); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/renderer.h b/src/ballistica/base/graphics/renderer/renderer.h similarity index 74% rename from src/ballistica/graphics/renderer.h rename to src/ballistica/base/graphics/renderer/renderer.h index ccf7a257..85dce947 100644 --- a/src/ballistica/graphics/renderer.h +++ b/src/ballistica/base/graphics/renderer/renderer.h @@ -1,48 +1,48 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_RENDERER_H_ -#define BALLISTICA_GRAPHICS_RENDERER_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_RENDERER_RENDERER_H_ +#define BALLISTICA_BASE_GRAPHICS_RENDERER_RENDERER_H_ #include #include #include -#include "ballistica/assets/assets.h" -#include "ballistica/assets/component/texture.h" -#include "ballistica/assets/data/model_data.h" -#include "ballistica/core/object.h" -#include "ballistica/graphics/frame_def.h" -#include "ballistica/graphics/framebuffer.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/graphics/mesh/image_mesh.h" -#include "ballistica/graphics/mesh/mesh.h" -#include "ballistica/graphics/mesh/mesh_buffer.h" -#include "ballistica/graphics/mesh/mesh_buffer_base.h" -#include "ballistica/graphics/mesh/mesh_buffer_vertex_simple_full.h" -#include "ballistica/graphics/mesh/mesh_buffer_vertex_smoke_full.h" -#include "ballistica/graphics/mesh/mesh_buffer_vertex_sprite.h" -#include "ballistica/graphics/mesh/mesh_data.h" -#include "ballistica/graphics/mesh/mesh_data_client_handle.h" -#include "ballistica/graphics/mesh/mesh_index_buffer_16.h" -#include "ballistica/graphics/mesh/mesh_index_buffer_32.h" -#include "ballistica/graphics/mesh/mesh_indexed.h" -#include "ballistica/graphics/mesh/mesh_indexed_dual_texture_full.h" -#include "ballistica/graphics/mesh/mesh_indexed_object_split.h" -#include "ballistica/graphics/mesh/mesh_indexed_simple_full.h" -#include "ballistica/graphics/mesh/mesh_indexed_simple_split.h" -#include "ballistica/graphics/mesh/mesh_indexed_smoke_full.h" -#include "ballistica/graphics/mesh/mesh_indexed_static_dynamic.h" -#include "ballistica/graphics/mesh/mesh_non_indexed.h" -#include "ballistica/graphics/mesh/sprite_mesh.h" -#include "ballistica/graphics/mesh/text_mesh.h" -#include "ballistica/graphics/render_command_buffer.h" -#include "ballistica/graphics/render_pass.h" -#include "ballistica/graphics/render_target.h" -#include "ballistica/graphics/text/text_group.h" -#include "ballistica/math/matrix44f.h" -#include "ballistica/math/vector3f.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/assets/mesh_asset.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/graphics/mesh/image_mesh.h" +#include "ballistica/base/graphics/mesh/mesh.h" +#include "ballistica/base/graphics/mesh/mesh_buffer.h" +#include "ballistica/base/graphics/mesh/mesh_buffer_base.h" +#include "ballistica/base/graphics/mesh/mesh_buffer_vertex_simple_full.h" +#include "ballistica/base/graphics/mesh/mesh_buffer_vertex_smoke_full.h" +#include "ballistica/base/graphics/mesh/mesh_buffer_vertex_sprite.h" +#include "ballistica/base/graphics/mesh/mesh_data.h" +#include "ballistica/base/graphics/mesh/mesh_data_client_handle.h" +#include "ballistica/base/graphics/mesh/mesh_index_buffer_16.h" +#include "ballistica/base/graphics/mesh/mesh_index_buffer_32.h" +#include "ballistica/base/graphics/mesh/mesh_indexed.h" +#include "ballistica/base/graphics/mesh/mesh_indexed_dual_texture_full.h" +#include "ballistica/base/graphics/mesh/mesh_indexed_object_split.h" +#include "ballistica/base/graphics/mesh/mesh_indexed_simple_full.h" +#include "ballistica/base/graphics/mesh/mesh_indexed_simple_split.h" +#include "ballistica/base/graphics/mesh/mesh_indexed_smoke_full.h" +#include "ballistica/base/graphics/mesh/mesh_indexed_static_dynamic.h" +#include "ballistica/base/graphics/mesh/mesh_non_indexed.h" +#include "ballistica/base/graphics/mesh/sprite_mesh.h" +#include "ballistica/base/graphics/mesh/text_mesh.h" +#include "ballistica/base/graphics/renderer/framebuffer.h" +#include "ballistica/base/graphics/renderer/render_pass.h" +#include "ballistica/base/graphics/renderer/render_target.h" +#include "ballistica/base/graphics/support/frame_def.h" +#include "ballistica/base/graphics/support/render_command_buffer.h" +#include "ballistica/base/graphics/text/text_group.h" +#include "ballistica/scene_v1/assets/scene_texture.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/math/matrix44f.h" +#include "ballistica/shared/math/vector3f.h" -namespace ballistica { +namespace ballistica::base { // The renderer is responsible for converting a frame_def to onscreen pixels class Renderer { @@ -99,38 +99,38 @@ class Renderer { void ScreenSizeChanged(); auto has_camera_render_target() const -> bool { - return camera_render_target_.exists(); + return camera_render_target_.Exists(); } auto has_camera_msaa_render_target() const -> bool { - return camera_msaa_render_target_.exists(); + return camera_msaa_render_target_.Exists(); } auto camera_render_target() -> RenderTarget* { - assert(camera_render_target_.exists()); - return camera_render_target_.get(); + assert(camera_render_target_.Exists()); + return camera_render_target_.Get(); } auto camera_msaa_render_target() -> RenderTarget* { - assert(camera_msaa_render_target_.exists()); - return camera_msaa_render_target_.get(); + assert(camera_msaa_render_target_.Exists()); + return camera_msaa_render_target_.Get(); } auto backing_render_target() -> RenderTarget* { - assert(backing_render_target_.exists()); - return backing_render_target_.get(); + assert(backing_render_target_.Exists()); + return backing_render_target_.Get(); } auto screen_render_target() -> RenderTarget* { - assert(screen_render_target_.exists()); - return screen_render_target_.get(); + assert(screen_render_target_.Exists()); + return screen_render_target_.Get(); } auto light_render_target() -> RenderTarget* { - assert(light_render_target_.exists()); - return light_render_target_.get(); + assert(light_render_target_.Exists()); + return light_render_target_.Get(); } auto light_shadow_render_target() -> RenderTarget* { - assert(light_shadow_render_target_.exists()); - return light_shadow_render_target_.get(); + assert(light_shadow_render_target_.Exists()); + return light_shadow_render_target_.Get(); } auto vr_overlay_flat_render_target() -> RenderTarget* { - assert(vr_overlay_flat_render_target_.exists()); - return vr_overlay_flat_render_target_.get(); + assert(vr_overlay_flat_render_target_.Exists()); + return vr_overlay_flat_render_target_.Get(); } auto shadow_res() const -> int { return shadow_res_; } auto blur_res_count() const -> int { return blur_res_count_; } @@ -151,9 +151,10 @@ class Renderer { int VRGetViewportY() const { return vr_viewport_y_; } #endif // BA_VR_BUILD - virtual auto NewModelData(const ModelData& model) -> ModelRendererData* = 0; - virtual auto NewTextureData(const TextureData& texture) - -> TextureRendererData* = 0; + virtual auto NewMeshAssetData(const MeshAsset& mesh) + -> Object::Ref = 0; + virtual auto NewTextureData(const TextureAsset& texture) + -> Object::Ref = 0; virtual auto NewMeshData(MeshDataType t, MeshDrawType drawType) -> MeshRendererData* = 0; virtual void DeleteMeshData(MeshRendererData* data, MeshDataType t) = 0; @@ -182,7 +183,8 @@ class Renderer { bool linear_interp, bool depth, bool texture, bool depth_texture, bool high_quality, bool msaa, - bool alpha) -> RenderTarget* = 0; + bool alpha) + -> Object::Ref = 0; virtual void PushGroupMarker(const char* label) = 0; virtual void PopGroupMarker() = 0; virtual void BlitBuffer(RenderTarget* src, RenderTarget* dst, bool depth, @@ -283,7 +285,7 @@ class Renderer { int last_commands_buffer_size_{}; int last_f_vals_buffer_size_{}; int last_i_vals_buffer_size_{}; - int last_models_buffer_size_{}; + int last_meshes_buffer_size_{}; int last_textures_buffer_size_{}; bool debug_draw_mode_{}; int frames_rendered_count_{}; @@ -293,6 +295,6 @@ class Renderer { GraphicsQuality last_render_quality_{GraphicsQuality::kLow}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_RENDERER_H_ +#endif // BALLISTICA_BASE_GRAPHICS_RENDERER_RENDERER_H_ diff --git a/src/ballistica/graphics/area_of_interest.cc b/src/ballistica/base/graphics/support/area_of_interest.cc similarity index 56% rename from src/ballistica/graphics/area_of_interest.cc rename to src/ballistica/base/graphics/support/area_of_interest.cc index a4f0bc5e..eaa5ecd2 100644 --- a/src/ballistica/graphics/area_of_interest.cc +++ b/src/ballistica/base/graphics/support/area_of_interest.cc @@ -1,10 +1,10 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/area_of_interest.h" +#include "ballistica/base/graphics/support/area_of_interest.h" -#include "ballistica/ui/ui.h" +#include "ballistica/base/ui/ui.h" -namespace ballistica { +namespace ballistica::base { AreaOfInterest::AreaOfInterest(bool in_focus) : in_focus_(in_focus) {} @@ -12,8 +12,8 @@ AreaOfInterest::~AreaOfInterest() = default; void AreaOfInterest::SetRadius(float r_in) { // We slightly scale this for phone situations. - float extrascale = (g_ui->scale() == UIScale::kSmall) ? 0.85f : 1.0f; + float extrascale = (g_base->ui->scale() == UIScale::kSmall) ? 0.85f : 1.0f; radius_ = r_in * extrascale; } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/area_of_interest.h b/src/ballistica/base/graphics/support/area_of_interest.h similarity index 71% rename from src/ballistica/graphics/area_of_interest.h rename to src/ballistica/base/graphics/support/area_of_interest.h index 3fd61feb..d881efb4 100644 --- a/src/ballistica/graphics/area_of_interest.h +++ b/src/ballistica/base/graphics/support/area_of_interest.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_AREA_OF_INTEREST_H_ -#define BALLISTICA_GRAPHICS_AREA_OF_INTEREST_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_SUPPORT_AREA_OF_INTEREST_H_ +#define BALLISTICA_BASE_GRAPHICS_SUPPORT_AREA_OF_INTEREST_H_ #include -#include "ballistica/math/vector3f.h" +#include "ballistica/shared/math/vector3f.h" -namespace ballistica { +namespace ballistica::base { class AreaOfInterest { public: @@ -28,6 +28,6 @@ class AreaOfInterest { bool in_focus_ = false; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_AREA_OF_INTEREST_H_ +#endif // BALLISTICA_BASE_GRAPHICS_SUPPORT_AREA_OF_INTEREST_H_ diff --git a/src/ballistica/graphics/camera.cc b/src/ballistica/base/graphics/support/camera.cc similarity index 95% rename from src/ballistica/graphics/camera.cc rename to src/ballistica/base/graphics/support/camera.cc index c22d1e06..aa0554df 100644 --- a/src/ballistica/graphics/camera.cc +++ b/src/ballistica/base/graphics/support/camera.cc @@ -1,17 +1,19 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/camera.h" +#include "ballistica/base/graphics/support/camera.h" -#include "ballistica/audio/audio.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/area_of_interest.h" -#include "ballistica/graphics/frame_def.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/graphics/render_pass.h" -#include "ballistica/graphics/vr_graphics.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/graphics/graphics_vr.h" +#include "ballistica/base/graphics/renderer/render_pass.h" +#include "ballistica/base/graphics/support/area_of_interest.h" +#include "ballistica/base/graphics/support/frame_def.h" +#include "ballistica/core/core.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/math/random.h" #include "ode/ode_collision_util.h" -namespace ballistica { +namespace ballistica::base { const float kCameraOffsetX = 0.0f; const float kCameraOffsetY = -8.3f; @@ -21,9 +23,9 @@ const float kPanMax = 9.0f; const float kPanMin = -9.0f; Camera::Camera() - : last_mode_set_time_(GetRealTime()), - lock_panning_(IsVRMode()), - pan_speed_scale_(IsVRMode() ? 0.3f : 1.0f) {} + : last_mode_set_time_(g_core->GetAppTimeMillisecs()), + lock_panning_(g_core->IsVRMode()), + pan_speed_scale_(g_core->IsVRMode() ? 0.3f : 1.0f) {} Camera::~Camera() = default; @@ -200,12 +202,15 @@ void Camera::UpdatePosition() { if (explicit_bool(true)) { float lr_jitter; { - if (IsVRMode()) { + if (g_core->IsVRMode()) { lr_jitter = 0.0f; } else { lr_jitter = - sinf(static_cast(GetRealTime()) / 108.0f) * 0.4f - + sinf(static_cast(GetRealTime()) / 268.0f) * 1.0f; + sinf(static_cast(g_core->GetAppTimeMillisecs()) / 108.0f) + * 0.4f + + sinf(static_cast(g_core->GetAppTimeMillisecs()) + / 268.0f) + * 1.0f; lr_jitter *= 0.05f; } } @@ -488,7 +493,7 @@ void Camera::UpdatePosition() { // our fixed-overlay matrix and our regular overlay matrix come out // the same. if (g_buildconfig.vr_build()) { - if (IsVRMode()) { + if (g_core->IsVRMode()) { // Only apply map's X offset if camera is locked. x_min = x_max = position_.x @@ -571,7 +576,7 @@ void Camera::UpdatePosition() { to_cam.Normalize(); Vector3f cam_space_lr = Vector3f::Cross(to_cam, Vector3f(0, 1, 0)); Vector3f cam_space_ud = Vector3f::Cross(cam_space_lr, to_cam); - Vector3f tilt = 0.1f * g_graphics->tilt(); + Vector3f tilt = 0.1f * g_base->graphics->tilt(); if (manual_) { tilt.x = 0.0f; tilt.y = 0.0f; @@ -602,14 +607,14 @@ void Camera::Update(millisecs_t elapsed) { float damping = 0.006f; float damping2 = 0.006f; float xy_blend_speed = 0.0002f; - millisecs_t real_time = GetRealTime(); + millisecs_t real_time = g_core->GetAppTimeMillisecs(); // Prevent camera "explosions" if we've been unable to update for a while. elapsed = std::min(millisecs_t{100}, elapsed); auto elapsedf{static_cast(elapsed)}; // In normal mode we orbit; in vr mode we don't. - if (IsVRMode()) { + if (g_core->IsVRMode()) { heading_ = -0.3f; } else { heading_ += static_cast(elapsed) / 10000.0f; @@ -647,7 +652,7 @@ void Camera::Update(millisecs_t elapsed) { xy_constrain_blend_ = std::max(0.0f, xy_constrain_blend_); } - if (!IsVRMode()) { + if (!g_core->IsVRMode()) { smooth_speed_.x += elapsedf * rand_component * (-0.5f + Utils::precalc_rand_1((real_time / rand_incr_1) @@ -662,7 +667,7 @@ void Camera::Update(millisecs_t elapsed) { % kPrecalcRandsCount)); } - if (RandomFloat() < 0.1f && !IsVRMode()) { + if (RandomFloat() < 0.1f && !g_core->IsVRMode()) { smooth_speed_2_.x += elapsedf * rand_component * 4.0f * (-0.5f + RandomFloat()); smooth_speed_2_.y += @@ -726,21 +731,21 @@ void Camera::Update(millisecs_t elapsed) { shake_vel_ *= 0.99f; } - if (g_graphics->camera_shake_disabled()) { + if (g_base->graphics->camera_shake_disabled()) { shake_pos_ = {0, 0, 0}; } } // Update audio position more often in vr since we can whip our head around. - uint32_t interval = IsVRMode() ? 50 : 100; + uint32_t interval = g_core->IsVRMode() ? 50 : 100; // Occasionally, update microphone position for audio. if (real_time - last_listener_update_time_ > interval) { last_listener_update_time_ = real_time; bool do_regular_update = true; - if (IsVRMode()) { + if (g_core->IsVRMode()) { #if BA_VR_MODE - VRGraphics* vrgraphics = VRGraphics::get(); + GraphicsVR* vrgraphics = GraphicsVR::get(); do_regular_update = false; Vector3f listener_pos = vrgraphics->vr_head_translate() + vrgraphics->vr_head_forward() * 5.0f; @@ -756,8 +761,8 @@ void Camera::Update(millisecs_t elapsed) { position_.x + to_target * (target_smoothed_.x - position_.x), position_.y + to_target * (target_smoothed_.y - position_.y), position_.z + to_target * (target_smoothed_.z - position_.z)); - assert(g_audio_server); - g_audio->SetListenerPosition(listener_pos); + assert(g_base->audio_server); + g_base->audio->SetListenerPosition(listener_pos); } } } @@ -852,13 +857,13 @@ void Camera::ManualHandleMouseMove(float move_h, float move_v) { } auto Camera::NewAreaOfInterest(bool in_focus) -> AreaOfInterest* { - assert(InLogicThread()); + assert(g_base->InLogicThread()); areas_of_interest_.emplace_back(in_focus); return &areas_of_interest_.back(); } void Camera::DeleteAreaOfInterest(AreaOfInterest* a) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); for (auto i = areas_of_interest_.begin(); i != areas_of_interest_.end(); ++i) { if (&(*i) == a) { @@ -886,7 +891,7 @@ void Camera::SetMode(CameraMode m) { if (mode_ != m) { mode_ = m; smooth_next_frame_ = false; - last_mode_set_time_ = GetRealTime(); + last_mode_set_time_ = g_core->GetAppTimeMillisecs(); heading_ = kInitialHeading; } } @@ -979,7 +984,7 @@ void Camera::ApplyToFrameDef(FrameDef* frame_def) { // If we're vr, apply current vr offsets. // FIXME: should create a VRCamera subclass or whatnot. - if (IsVRMode()) { + if (g_core->IsVRMode()) { if (mode_ == CameraMode::kFollow) { Vector3f cam_original = frame_def->cam_original(); @@ -1007,11 +1012,12 @@ void Camera::ApplyToFrameDef(FrameDef* frame_def) { position_ + extra_pos_2_, target_smoothed_ + shake_pos_ + extra_pos_, up_, 4, 1000.0f, -1.0f, // Auto x fov. - final_fov_y * (g_graphics->tv_border() ? (1.0f + kTVBorder) : 1.0f), + final_fov_y + * (g_base->graphics->tv_border() ? (1.0f + kTVBorder) : 1.0f), false, 0, 0, 0, 0, // Not using tangent fovs. area_of_interest_points_); } smooth_next_frame_ = true; } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/camera.h b/src/ballistica/base/graphics/support/camera.h similarity index 93% rename from src/ballistica/graphics/camera.h rename to src/ballistica/base/graphics/support/camera.h index e9ae3704..7b13bb4c 100644 --- a/src/ballistica/graphics/camera.h +++ b/src/ballistica/base/graphics/support/camera.h @@ -1,15 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_CAMERA_H_ -#define BALLISTICA_GRAPHICS_CAMERA_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_SUPPORT_CAMERA_H_ +#define BALLISTICA_BASE_GRAPHICS_SUPPORT_CAMERA_H_ #include #include -#include "ballistica/core/object.h" -#include "ballistica/math/vector3f.h" +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/math/vector3f.h" -namespace ballistica { +namespace ballistica::base { // Hmm; this shouldn't be here. const float kHappyThoughtsZPlane = -5.52f; @@ -147,6 +148,6 @@ class Camera : public Object { std::vector area_of_interest_points_{{0.0f, 0.0f, 0.0f}}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_CAMERA_H_ +#endif // BALLISTICA_BASE_GRAPHICS_SUPPORT_CAMERA_H_ diff --git a/src/ballistica/graphics/frame_def.cc b/src/ballistica/base/graphics/support/frame_def.cc similarity index 76% rename from src/ballistica/graphics/frame_def.cc rename to src/ballistica/base/graphics/support/frame_def.cc index 747cfa74..de38d0bb 100644 --- a/src/ballistica/graphics/frame_def.cc +++ b/src/ballistica/base/graphics/support/frame_def.cc @@ -1,13 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/frame_def.h" +#include "ballistica/base/graphics/support/frame_def.h" -#include "ballistica/graphics/camera.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/render_pass.h" -#include "ballistica/graphics/renderer.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/graphics/renderer/render_pass.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/base/graphics/support/camera.h" +#include "ballistica/core/core.h" -namespace ballistica { +namespace ballistica::base { FrameDef::FrameDef() : light_pass_(new RenderPass(RenderPass::Type::kLightPass, this)), @@ -26,11 +27,29 @@ FrameDef::FrameDef() new RenderPass(RenderPass::Type::kOverlayFlatPass, this)), blit_pass_(new RenderPass(RenderPass::Type::kBlitPass, this)) {} -FrameDef::~FrameDef() { assert(InLogicThread()); } +FrameDef::~FrameDef() { assert(g_base->InLogicThread()); } + +auto FrameDef::GetOverlayFixedPass() -> RenderPass* { + assert(g_core); + if (g_core->IsVRMode()) { + return overlay_fixed_pass_.get(); + } else { + return overlay_pass_.get(); + } +} + +auto FrameDef::GetOverlayFlatPass() -> RenderPass* { + assert(g_core); + if (g_core->IsVRMode()) { + return overlay_flat_pass_.get(); + } else { + return overlay_pass_.get(); + } +} void FrameDef::Reset() { - assert(InLogicThread()); - real_time_ = 0; + assert(g_base->InLogicThread()); + app_time_millisecs_ = 0; base_time_ = 0; base_time_elapsed_ = 0; frame_number_ = 0; @@ -49,19 +68,19 @@ void FrameDef::Reset() { mesh_index_sizes_.clear(); mesh_buffers_.clear(); - quality_ = g_graphics_server->quality(); + quality_ = g_base->graphics_server->quality(); - assert(g_graphics->has_supports_high_quality_graphics_value()); - orbiting_ = (g_graphics->camera()->mode() == CameraMode::kOrbit); + assert(g_base->graphics->has_supports_high_quality_graphics_value()); + orbiting_ = (g_base->graphics->camera()->mode() == CameraMode::kOrbit); - shadow_offset_ = g_graphics->shadow_offset(); - shadow_scale_ = g_graphics->shadow_scale(); - shadow_ortho_ = g_graphics->shadow_ortho(); - tint_ = g_graphics->tint(); - ambient_color_ = g_graphics->ambient_color(); + shadow_offset_ = g_base->graphics->shadow_offset(); + shadow_scale_ = g_base->graphics->shadow_scale(); + shadow_ortho_ = g_base->graphics->shadow_ortho(); + tint_ = g_base->graphics->tint(); + ambient_color_ = g_base->graphics->ambient_color(); - vignette_outer_ = g_graphics->vignette_outer(); - vignette_inner_ = g_graphics->vignette_inner(); + vignette_outer_ = g_base->graphics->vignette_outer(); + vignette_inner_ = g_base->graphics->vignette_inner(); light_pass_->Reset(); light_shadow_pass_->Reset(); @@ -69,14 +88,14 @@ void FrameDef::Reset() { beauty_pass_bg_->Reset(); overlay_pass_->Reset(); overlay_front_pass_->Reset(); - if (IsVRMode()) { + if (g_core->IsVRMode()) { overlay_flat_pass_->Reset(); overlay_fixed_pass_->Reset(); vr_cover_pass_->Reset(); } overlay_3d_pass_->Reset(); blit_pass_->Reset(); - beauty_pass_->set_floor_reflection(g_graphics->floor_reflection()); + beauty_pass_->set_floor_reflection(g_base->graphics->floor_reflection()); } void FrameDef::Finalize() { @@ -87,7 +106,7 @@ void FrameDef::Finalize() { beauty_pass_bg_->Finalize(); overlay_pass_->Finalize(); overlay_front_pass_->Finalize(); - if (IsVRMode()) { + if (g_core->IsVRMode()) { overlay_fixed_pass_->Finalize(); overlay_flat_pass_->Finalize(); vr_cover_pass_->Finalize(); @@ -170,4 +189,4 @@ void FrameDef::AddMesh(Mesh* mesh) { } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/frame_def.h b/src/ballistica/base/graphics/support/frame_def.h similarity index 88% rename from src/ballistica/graphics/frame_def.h rename to src/ballistica/base/graphics/support/frame_def.h index 90db34ae..b9a8c790 100644 --- a/src/ballistica/graphics/frame_def.h +++ b/src/ballistica/base/graphics/support/frame_def.h @@ -1,16 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_FRAME_DEF_H_ -#define BALLISTICA_GRAPHICS_FRAME_DEF_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_SUPPORT_FRAME_DEF_H_ +#define BALLISTICA_BASE_GRAPHICS_SUPPORT_FRAME_DEF_H_ #include #include -#include "ballistica/assets/data/asset_component_data.h" -#include "ballistica/math/matrix44f.h" -#include "ballistica/math/vector2f.h" +#include "ballistica/base/assets/asset.h" +#include "ballistica/shared/math/matrix44f.h" +#include "ballistica/shared/math/vector2f.h" -namespace ballistica { +namespace ballistica::base { /// A flattened representation of a frame; generated by the logic thread and /// sent to the graphics thread to render. @@ -28,23 +28,11 @@ class FrameDef { void set_benchmark_type(BenchmarkType val) { benchmark_type_ = val; } // Returns the fixed overlay pass if there is one; otherwise the regular. - auto GetOverlayFixedPass() -> RenderPass* { - if (IsVRMode()) { - return overlay_fixed_pass_.get(); - } else { - return overlay_pass_.get(); - } - } + auto GetOverlayFixedPass() -> RenderPass*; // Return either the overlay-flat pass (in vr) or regular overlay pass (for // non-vr). - auto GetOverlayFlatPass() -> RenderPass* { - if (IsVRMode()) { - return overlay_flat_pass_.get(); - } else { - return overlay_pass_.get(); - } - } + auto GetOverlayFlatPass() -> RenderPass*; auto overlay_3d_pass() -> RenderPass* { return overlay_3d_pass_.get(); } auto blit_pass() -> RenderPass* { return blit_pass_.get(); } auto vr_cover_pass() -> RenderPass* { return vr_cover_pass_.get(); } @@ -52,7 +40,7 @@ class FrameDef { // Returns the real-time this frame_def originated at. // For a more smoothly-incrementing value, // use getbasetime() - auto real_time() const -> millisecs_t { return real_time_; } + auto real_time() const -> millisecs_t { return app_time_millisecs_; } auto frame_number() const -> int64_t { return frame_number_; } // Returns the bsGame master-net-time when this was made @@ -101,7 +89,7 @@ class FrameDef { auto has_depth_texture() const -> bool { return (quality_ >= GraphicsQuality::kHigh); } - void AddComponent(const Object::Ref& component) { + void AddComponent(const Object::Ref& component) { // Add a reference to this component only if we havn't yet. if (component->last_frame_def_num() != frame_number_) { component->set_last_frame_def_num(frame_number_); @@ -118,7 +106,7 @@ class FrameDef { void Finalize(); void set_base_time_elapsed(millisecs_t val) { base_time_elapsed_ = val; } - void set_real_time(millisecs_t val) { real_time_ = val; } + void set_app_time_millisecs(millisecs_t val) { app_time_millisecs_ = val; } void set_base_time(millisecs_t val) { base_time_ = val; } void set_frame_number(int64_t val) { frame_number_ = val; } @@ -155,8 +143,7 @@ class FrameDef { auto mesh_index_sizes() const -> const std::vector& { return mesh_index_sizes_; } - auto media_components() const - -> const std::vector>& { + auto media_components() const -> const std::vector>& { return media_components_; } @@ -189,7 +176,7 @@ class FrameDef { std::vector> meshes_; std::vector> mesh_buffers_; std::vector mesh_index_sizes_; - std::vector> media_components_; + std::vector> media_components_; #if BA_DEBUG_BUILD // Sanity checking: make sure components are completely submitted @@ -210,7 +197,7 @@ class FrameDef { std::unique_ptr blit_pass_; GraphicsQuality quality_{GraphicsQuality::kLow}; bool orbiting_{}; - millisecs_t real_time_{}; + millisecs_t app_time_millisecs_{}; millisecs_t base_time_{}; millisecs_t base_time_elapsed_{}; int64_t frame_number_{}; @@ -223,6 +210,6 @@ class FrameDef { Vector3f vignette_inner_{1.0f, 1.0f, 1.0f}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_FRAME_DEF_H_ +#endif // BALLISTICA_BASE_GRAPHICS_SUPPORT_FRAME_DEF_H_ diff --git a/src/ballistica/graphics/net_graph.cc b/src/ballistica/base/graphics/support/net_graph.cc similarity index 92% rename from src/ballistica/graphics/net_graph.cc rename to src/ballistica/base/graphics/support/net_graph.cc index 7ad9997b..6c44fefc 100644 --- a/src/ballistica/graphics/net_graph.cc +++ b/src/ballistica/base/graphics/support/net_graph.cc @@ -1,12 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/net_graph.h" +#include "ballistica/base/graphics/support/net_graph.h" #include -#include "ballistica/graphics/component/simple_component.h" +#include "ballistica/base/graphics/component/simple_component.h" -namespace ballistica { +namespace ballistica::base { class NetGraph::Impl { public: @@ -26,17 +26,15 @@ NetGraph::NetGraph() : impl_(new NetGraph::Impl()) {} NetGraph::~NetGraph() = default; -auto NetGraph::SetLabel(const std::string& label) -> void { - impl_->label = label; -} -auto NetGraph::SetSmoothed(bool val) -> void { impl_->smoothed = val; } +void NetGraph::SetLabel(const std::string& label) { impl_->label = label; } +void NetGraph::SetSmoothed(bool val) { impl_->smoothed = val; } -auto NetGraph::SetLastUsedTime(millisecs_t real_time) -> void { +void NetGraph::SetLastUsedTime(millisecs_t real_time) { impl_->last_used_time = real_time; } auto NetGraph::LastUsedTime() -> millisecs_t { return impl_->last_used_time; } -auto NetGraph::AddSample(double time, double value) -> void { +void NetGraph::AddSample(double time, double value) { impl_->samples.emplace_back(time, value); double cutoffTime = time - impl_->duration; @@ -174,4 +172,4 @@ void NetGraph::Draw(RenderPass* pass, double time, double x, double y, double w, c2.Submit(); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/base/graphics/support/net_graph.h b/src/ballistica/base/graphics/support/net_graph.h new file mode 100644 index 00000000..89cae62e --- /dev/null +++ b/src/ballistica/base/graphics/support/net_graph.h @@ -0,0 +1,33 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_GRAPHICS_SUPPORT_NET_GRAPH_H_ +#define BALLISTICA_BASE_GRAPHICS_SUPPORT_NET_GRAPH_H_ + +#include +#include + +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::base { + +class NetGraph : public Object { + public: + NetGraph(); + ~NetGraph() override; + void AddSample(double time, double value); + void SetLabel(const std::string& label); + void SetLastUsedTime(millisecs_t real_time); + auto LastUsedTime() -> millisecs_t; + void SetSmoothed(bool smoothed); + void Draw(RenderPass* pass, double time, double x, double y, double w, + double h); + + private: + class Impl; + std::unique_ptr impl_; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_GRAPHICS_SUPPORT_NET_GRAPH_H_ diff --git a/src/ballistica/graphics/render_command_buffer.h b/src/ballistica/base/graphics/support/render_command_buffer.h similarity index 90% rename from src/ballistica/graphics/render_command_buffer.h rename to src/ballistica/base/graphics/support/render_command_buffer.h index 691100f7..ab4854a3 100644 --- a/src/ballistica/graphics/render_command_buffer.h +++ b/src/ballistica/base/graphics/support/render_command_buffer.h @@ -1,18 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_RENDER_COMMAND_BUFFER_H_ -#define BALLISTICA_GRAPHICS_RENDER_COMMAND_BUFFER_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_SUPPORT_RENDER_COMMAND_BUFFER_H_ +#define BALLISTICA_BASE_GRAPHICS_SUPPORT_RENDER_COMMAND_BUFFER_H_ #include -#include "ballistica/assets/component/model.h" -#include "ballistica/assets/component/texture.h" -#include "ballistica/ballistica.h" -#include "ballistica/graphics/frame_def.h" -#include "ballistica/graphics/mesh/mesh.h" -#include "ballistica/math/matrix44f.h" +#include "ballistica/base/graphics/mesh/mesh.h" +#include "ballistica/base/graphics/support/frame_def.h" +#include "ballistica/scene_v1/assets/scene_mesh.h" +#include "ballistica/scene_v1/assets/scene_texture.h" +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/math/matrix44f.h" -namespace ballistica { +namespace ballistica::base { class RenderCommandBuffer { public: @@ -21,8 +21,8 @@ class RenderCommandBuffer { enum class Command { kEnd, kShader, - kDrawModel, - kDrawModelInstanced, + kDrawMeshAsset, + kDrawMeshAssetInstanced, kDrawMesh, kDrawScreenQuad, kScissorPush, @@ -255,29 +255,29 @@ class RenderCommandBuffer { ivals_.push_back(val); } - void PutModel(ModelData* model) { + void PutMeshAsset(MeshAsset* mesh) { assert(frame_def_); assert(!finalized_); - frame_def_->AddComponent(Object::Ref(model)); - models_.push_back(model); + frame_def_->AddComponent(Object::Ref(mesh)); + meshes_.push_back(mesh); } - void PutTexture(TextureData* texture) { + void PutTexture(TextureAsset* texture) { assert(frame_def_); assert(!finalized_); - frame_def_->AddComponent(Object::Ref(texture)); + frame_def_->AddComponent(Object::Ref(texture)); textures_.push_back(texture); } - void PutTexture(const Object::Ref& texture) { - assert(texture.exists()); - PutTexture(texture.get()); + void PutTexture(const Object::Ref& texture) { + assert(texture.Exists()); + PutTexture(texture.Get()); } - void PutCubeMapTexture(TextureData* texture) { + void PutCubeMapTexture(TextureAsset* texture) { assert(frame_def_); assert(!finalized_); - frame_def_->AddComponent(Object::Ref(texture)); + frame_def_->AddComponent(Object::Ref(texture)); textures_.push_back(texture); } @@ -472,10 +472,10 @@ class RenderCommandBuffer { return m; } - auto GetModel() -> const ModelData* { + auto GetMesh() -> const MeshAsset* { assert(finalized_); - assert(models_index_ < models_.size()); - return models_[models_index_++]; + assert(meshes_index_ < meshes_.size()); + return meshes_[meshes_index_++]; } template @@ -491,7 +491,7 @@ class RenderCommandBuffer { return m; } - auto GetTexture() -> const TextureData* { + auto GetTexture() -> const TextureAsset* { assert(finalized_); assert(textures_index_ < textures_.size()); return textures_[textures_index_++]; @@ -501,7 +501,7 @@ class RenderCommandBuffer { commands_.resize(0); fvals_.resize(0); ivals_.resize(0); - models_.resize(0); + meshes_.resize(0); textures_.resize(0); mesh_datas_.resize(0); finalized_ = false; @@ -519,15 +519,15 @@ class RenderCommandBuffer { commands_index_ = 0; fvals_index_ = 0; ivals_index_ = 0; - models_index_ = 0; + meshes_index_ = 0; textures_index_ = 0; mesh_datas_index_ = 0; } auto has_draw_commands() const -> bool { for (auto& command : commands_) { switch (command) { - case Command::kDrawModel: - case Command::kDrawModelInstanced: + case Command::kDrawMeshAsset: + case Command::kDrawMeshAssetInstanced: case Command::kDrawMesh: case Command::kDrawScreenQuad: return true; @@ -542,7 +542,7 @@ class RenderCommandBuffer { auto IsEmpty() -> bool { return ( (commands_index_ == commands_.size()) && (fvals_index_ == fvals_.size()) - && (ivals_index_ == ivals_.size()) && (models_index_ == models_.size()) + && (ivals_index_ == ivals_.size()) && (meshes_index_ == meshes_.size()) && (textures_index_ == textures_.size()) && (mesh_datas_index_ == mesh_datas_.size())); } @@ -558,19 +558,19 @@ class RenderCommandBuffer { std::vector commands_; std::vector fvals_; std::vector ivals_; - std::vector models_{}; - std::vector textures_{}; + std::vector meshes_{}; + std::vector textures_{}; std::vector mesh_datas_{}; unsigned int commands_index_{}; unsigned int fvals_index_{}; unsigned int ivals_index_{}; - unsigned int models_index_{}; + unsigned int meshes_index_{}; unsigned int textures_index_{}; unsigned int mesh_datas_index_{}; bool finalized_{}; FrameDef* frame_def_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_RENDER_COMMAND_BUFFER_H_ +#endif // BALLISTICA_BASE_GRAPHICS_SUPPORT_RENDER_COMMAND_BUFFER_H_ diff --git a/src/ballistica/graphics/text/font_page_map_data.h b/src/ballistica/base/graphics/text/font_page_map_data.h similarity index 93% rename from src/ballistica/graphics/text/font_page_map_data.h rename to src/ballistica/base/graphics/text/font_page_map_data.h index d1a20d0a..10ff01c3 100644 --- a/src/ballistica/graphics/text/font_page_map_data.h +++ b/src/ballistica/base/graphics/text/font_page_map_data.h @@ -1,17 +1,17 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_TEXT_FONT_PAGE_MAP_DATA_H_ -#define BALLISTICA_GRAPHICS_TEXT_FONT_PAGE_MAP_DATA_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_TEXT_FONT_PAGE_MAP_DATA_H_ +#define BALLISTICA_BASE_GRAPHICS_TEXT_FONT_PAGE_MAP_DATA_H_ // this file was generated automatically from the font construction tool on // 2015-06-27 // NOTE: IT HAS BEEN MODIFIED BY HAND SINCE!!! IF WE RECREATE IT VIA TOOL // AT SOME POINT WE NEED TO UPDATE THE TOOL!!!!!!!! -#include "ballistica/graphics/graphics.h" -#include "ballistica/graphics/text/text_graphics.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/graphics/text/text_graphics.h" -namespace ballistica { +namespace ballistica::base { // the total number of glyph pages we have #define BA_GLYPH_PAGE_COUNT 8 @@ -84,6 +84,6 @@ uint16_t g_glyph_map[kGlyphCount] = { 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7}; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_TEXT_FONT_PAGE_MAP_DATA_H_ +#endif // BALLISTICA_BASE_GRAPHICS_TEXT_FONT_PAGE_MAP_DATA_H_ diff --git a/src/ballistica/graphics/text/text_graphics.cc b/src/ballistica/base/graphics/text/text_graphics.cc similarity index 97% rename from src/ballistica/graphics/text/text_graphics.cc rename to src/ballistica/base/graphics/text/text_graphics.cc index 5e045591..09aaba4f 100644 --- a/src/ballistica/graphics/text/text_graphics.cc +++ b/src/ballistica/base/graphics/text/text_graphics.cc @@ -1,12 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/text/text_graphics.h" +#include "ballistica/base/graphics/text/text_graphics.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/text/font_page_map_data.h" -#include "ballistica/platform/platform.h" +#include "ballistica/base/graphics/text/font_page_map_data.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::base { class TextGraphics::TextSpanBoundsCacheEntry : public Object { public: @@ -825,8 +825,10 @@ void TextGraphics::LoadGlyphPage(uint32_t index) { // Its possible someone else coulda loaded it since we last checked. if (g_glyph_pages[index] == nullptr) { char buffer[256]; - snprintf(buffer, sizeof(buffer), "ba_data/fonts/fontSmall%d.fdata", index); - FILE* f = g_platform->FOpen(buffer, "rb"); + snprintf(buffer, sizeof(buffer), "%s%sba_data%sfonts%sfontSmall%d.fdata", + g_core->platform->GetDataDirectory().c_str(), BA_DIRSLASH, + BA_DIRSLASH, BA_DIRSLASH, index); + FILE* f = g_core->platform->FOpen(buffer, "rb"); BA_PRECONDITION(f); BA_PRECONDITION(sizeof(TextGraphics::Glyph[2]) == sizeof(float[18])); uint32_t total_size = sizeof(Glyph) * g_glyph_page_glyph_counts[index]; @@ -994,7 +996,7 @@ auto TextGraphics::GetGlyph(uint32_t val, bool big) -> TextGraphics::Glyph* { void TextGraphics::GetOSTextSpanBoundsAndWidth(const std::string& s, Rect* r, float* width) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // Asking the OS to calculate text bounds sounds expensive, // so let's use a cache of recent results. @@ -1018,7 +1020,7 @@ void TextGraphics::GetOSTextSpanBoundsAndWidth(const std::string& s, Rect* r, auto entry(Object::New()); entry->string = s; if (g_buildconfig.enable_os_font_rendering()) { - g_platform->GetTextBoundsAndWidth(s, &entry->r, &entry->width); + g_core->platform->GetTextBoundsAndWidth(s, &entry->r, &entry->width); } else { BA_LOG_ONCE( LogLevel::kError, @@ -1174,4 +1176,4 @@ void TextGraphics::BreakUpString(const char* text, float width, } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/text/text_graphics.h b/src/ballistica/base/graphics/text/text_graphics.h similarity index 90% rename from src/ballistica/graphics/text/text_graphics.h rename to src/ballistica/base/graphics/text/text_graphics.h index c293da30..a3337502 100644 --- a/src/ballistica/graphics/text/text_graphics.h +++ b/src/ballistica/base/graphics/text/text_graphics.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_TEXT_TEXT_GRAPHICS_H_ -#define BALLISTICA_GRAPHICS_TEXT_TEXT_GRAPHICS_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_TEXT_TEXT_GRAPHICS_H_ +#define BALLISTICA_BASE_GRAPHICS_TEXT_TEXT_GRAPHICS_H_ #include #include @@ -10,10 +10,10 @@ #include #include -#include "ballistica/core/object.h" -#include "ballistica/math/rect.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/math/rect.h" -namespace ballistica { +namespace ballistica::base { // Largest unicode value we ask the OS to draw for us. const int kTextMaxUnicodeVal = 999999; @@ -104,6 +104,6 @@ class TextGraphics { Glyph glyphs_big_[64]{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_TEXT_TEXT_GRAPHICS_H_ +#endif // BALLISTICA_BASE_GRAPHICS_TEXT_TEXT_GRAPHICS_H_ diff --git a/src/ballistica/graphics/text/text_group.cc b/src/ballistica/base/graphics/text/text_group.cc similarity index 84% rename from src/ballistica/graphics/text/text_group.cc rename to src/ballistica/base/graphics/text/text_group.cc index 63e4e619..2ceaeb5e 100644 --- a/src/ballistica/graphics/text/text_group.cc +++ b/src/ballistica/base/graphics/text/text_group.cc @@ -1,13 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/text/text_group.h" +#include "ballistica/base/graphics/text/text_group.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/graphics/text/text_graphics.h" -#include "ballistica/graphics/text/text_packer.h" +#include "ballistica/base/graphics/text/text_graphics.h" +#include "ballistica/base/graphics/text/text_packer.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::base { void TextGroup::SetText(const std::string& text, TextMesh::HAlign alignment_h, TextMesh::VAlign alignment_v, bool big, @@ -33,7 +32,7 @@ void TextGroup::SetText(const std::string& text, TextMesh::HAlign alignment_h, entry->max_flatness = 1.0f; entry->mesh.SetText(text, alignment_h, alignment_v, true, 0, 65535, TextMeshEntryType::kRegular, nullptr); - entry->tex = g_assets->GetTexture(SystemTextureID::kFontBig); + entry->tex = g_base->assets->SysTexture(SysTextureID::kFontBig); entries_.push_back(std::move(entry)); } else { @@ -41,7 +40,7 @@ void TextGroup::SetText(const std::string& text, TextMesh::HAlign alignment_h, // First, calc which font pages we'll need to draw this text. std::set font_pages; - g_text_graphics->GetFontPagesForText(text, &font_pages); + g_base->text_graphics->GetFontPagesForText(text, &font_pages); // Now create entries for each page we use. // (we iterate this in reverse so that our custom pages draw first; @@ -50,7 +49,7 @@ void TextGroup::SetText(const std::string& text, TextMesh::HAlign alignment_h, entries_.clear(); for (auto i = font_pages.rbegin(); i != font_pages.rend(); i++) { uint32_t min, max; - g_text_graphics->GetFontPageCharRange(*i, &min, &max); + g_base->text_graphics->GetFontPageCharRange(*i, &min, &max); std::unique_ptr entry(new TextMeshEntry()); // Our custom font page IDs start at value 9990 (kExtras1); @@ -101,16 +100,16 @@ void TextGroup::SetText(const std::string& text, TextMesh::HAlign alignment_h, } entry->mesh.SetText(text, alignment_h, alignment_v, false, min, max, - entry->type, packer.get()); + entry->type, packer.Get()); - if (packer.exists()) { + if (packer.Exists()) { // If we made a text-packer, we need to fetch/generate a texture // that matches it. // There should only ever be one of these. - assert(!os_texture_.exists()); + assert(!os_texture_.Exists()); { Assets::AssetListLock lock; - os_texture_ = g_assets->GetTextureData(packer.get()); + os_texture_ = g_base->assets->GetTexture(packer.Get()); } // We also need to know what uv-scales to use for shadows/etc. @@ -122,43 +121,43 @@ void TextGroup::SetText(const std::string& text, TextMesh::HAlign alignment_h, } switch (*i) { case 0: - entry->tex = g_assets->GetTexture(SystemTextureID::kFontSmall0); + entry->tex = g_base->assets->SysTexture(SysTextureID::kFontSmall0); break; case 1: - entry->tex = g_assets->GetTexture(SystemTextureID::kFontSmall1); + entry->tex = g_base->assets->SysTexture(SysTextureID::kFontSmall1); break; case 2: - entry->tex = g_assets->GetTexture(SystemTextureID::kFontSmall2); + entry->tex = g_base->assets->SysTexture(SysTextureID::kFontSmall2); break; case 3: - entry->tex = g_assets->GetTexture(SystemTextureID::kFontSmall3); + entry->tex = g_base->assets->SysTexture(SysTextureID::kFontSmall3); break; case 4: - entry->tex = g_assets->GetTexture(SystemTextureID::kFontSmall4); + entry->tex = g_base->assets->SysTexture(SysTextureID::kFontSmall4); break; case 5: - entry->tex = g_assets->GetTexture(SystemTextureID::kFontSmall5); + entry->tex = g_base->assets->SysTexture(SysTextureID::kFontSmall5); break; case 6: - entry->tex = g_assets->GetTexture(SystemTextureID::kFontSmall6); + entry->tex = g_base->assets->SysTexture(SysTextureID::kFontSmall6); break; case 7: - entry->tex = g_assets->GetTexture(SystemTextureID::kFontSmall7); + entry->tex = g_base->assets->SysTexture(SysTextureID::kFontSmall7); break; case static_cast(TextGraphics::FontPage::kOSRendered): entry->tex = os_texture_; break; case static_cast(TextGraphics::FontPage::kExtras1): - entry->tex = g_assets->GetTexture(SystemTextureID::kFontExtras); + entry->tex = g_base->assets->SysTexture(SysTextureID::kFontExtras); break; case static_cast(TextGraphics::FontPage::kExtras2): - entry->tex = g_assets->GetTexture(SystemTextureID::kFontExtras2); + entry->tex = g_base->assets->SysTexture(SysTextureID::kFontExtras2); break; case static_cast(TextGraphics::FontPage::kExtras3): - entry->tex = g_assets->GetTexture(SystemTextureID::kFontExtras3); + entry->tex = g_base->assets->SysTexture(SysTextureID::kFontExtras3); break; case static_cast(TextGraphics::FontPage::kExtras4): - entry->tex = g_assets->GetTexture(SystemTextureID::kFontExtras4); + entry->tex = g_base->assets->SysTexture(SysTextureID::kFontExtras4); break; default: throw Exception(); @@ -271,7 +270,7 @@ void TextGroup::GetCaratPts(const std::string& text_in, // chars onto it instead of switching back to glyph mode. // (to reduce the number of times we switch back and forth) if (TextGraphics::Glyph* g = - g_text_graphics->GetGlyph(val, big_)) { + g_base->text_graphics->GetGlyph(val, big_)) { line_length += char_width * g->advance; } else { // TODO(ericf): add non-glyph chars into spans and ask @@ -311,10 +310,10 @@ void TextGroup::GetCaratPts(const std::string& text_in, } char_num++; } - *carat_x = - x_offset - + g_text_graphics->GetStringWidth(Utils::UTF8FromUnicode(line).c_str()); + *carat_x = x_offset + + g_base->text_graphics->GetStringWidth( + Utils::UTF8FromUnicode(line).c_str()); *carat_y = y_offset; } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/text/text_group.h b/src/ballistica/base/graphics/text/text_group.h similarity index 71% rename from src/ballistica/graphics/text/text_group.h rename to src/ballistica/base/graphics/text/text_group.h index 36081d75..127762fd 100644 --- a/src/ballistica/graphics/text/text_group.h +++ b/src/ballistica/base/graphics/text/text_group.h @@ -1,18 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_TEXT_TEXT_GROUP_H_ -#define BALLISTICA_GRAPHICS_TEXT_TEXT_GROUP_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_TEXT_TEXT_GROUP_H_ +#define BALLISTICA_BASE_GRAPHICS_TEXT_TEXT_GROUP_H_ #include #include #include -#include "ballistica/assets/assets.h" -#include "ballistica/assets/data/texture_data.h" -#include "ballistica/core/object.h" -#include "ballistica/graphics/mesh/text_mesh.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/assets/texture_asset.h" +#include "ballistica/base/graphics/mesh/text_mesh.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::base { // encapsulates the multiple meshes and textures necessary to // draw arbitrary text. To actually draw the text, iterate over the meshes @@ -25,9 +25,9 @@ class TextGroup : public Object { assert(index < static_cast(entries_.size())); return &(entries_[index]->mesh); } - auto GetElementTexture(int index) const -> TextureData* { + auto GetElementTexture(int index) const -> TextureAsset* { assert(index < static_cast(entries_.size())); - return entries_[index]->tex.get(); + return entries_[index]->tex.Get(); } // if you are doing any shader effects in UV-space (such as drop-shadows), // scale them by this ..this will account for different character sheets @@ -48,12 +48,12 @@ class TextGroup : public Object { assert(index < static_cast(entries_.size())); return entries_[index]->can_color; } - auto GetElementMaskUV2Texture(int index) const -> TextureData* { + auto GetElementMaskUV2Texture(int index) const -> TextureAsset* { assert(index < static_cast(entries_.size())); - return g_assets->GetTexture(entries_[index]->type - == TextMeshEntryType::kOSRendered - ? SystemTextureID::kSoftRect2 - : SystemTextureID::kSoftRect); + return g_base->assets->SysTexture(entries_[index]->type + == TextMeshEntryType::kOSRendered + ? SysTextureID::kSoftRect2 + : SysTextureID::kSoftRect); } void SetText(const std::string& text, TextMesh::HAlign alignment_h = TextMesh::HAlign::kLeft, @@ -67,19 +67,19 @@ class TextGroup : public Object { private: struct TextMeshEntry { TextMeshEntryType type; - Object::Ref tex; + Object::Ref tex; TextMesh mesh; float u_scale; float v_scale; bool can_color; float max_flatness; }; - Object::Ref os_texture_; + Object::Ref os_texture_; std::vector> entries_; std::string text_; bool big_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_TEXT_TEXT_GROUP_H_ +#endif // BALLISTICA_BASE_GRAPHICS_TEXT_TEXT_GROUP_H_ diff --git a/src/ballistica/graphics/text/text_packer.cc b/src/ballistica/base/graphics/text/text_packer.cc similarity index 98% rename from src/ballistica/graphics/text/text_packer.cc rename to src/ballistica/base/graphics/text/text_packer.cc index 1afcbb1f..2f9ea250 100644 --- a/src/ballistica/graphics/text/text_packer.cc +++ b/src/ballistica/base/graphics/text/text_packer.cc @@ -1,8 +1,8 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/text/text_packer.h" +#include "ballistica/base/graphics/text/text_packer.h" -namespace ballistica { +namespace ballistica::base { TextPacker::TextPacker(float resolution_scale) : resolution_scale_{resolution_scale} {} @@ -200,4 +200,4 @@ void TextPacker::compile() { compiled_ = true; } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/text/text_packer.h b/src/ballistica/base/graphics/text/text_packer.h similarity index 83% rename from src/ballistica/graphics/text/text_packer.h rename to src/ballistica/base/graphics/text/text_packer.h index 0a8d0509..d1889ba8 100644 --- a/src/ballistica/graphics/text/text_packer.h +++ b/src/ballistica/base/graphics/text/text_packer.h @@ -1,16 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_TEXT_TEXT_PACKER_H_ -#define BALLISTICA_GRAPHICS_TEXT_TEXT_PACKER_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_TEXT_TEXT_PACKER_H_ +#define BALLISTICA_BASE_GRAPHICS_TEXT_TEXT_PACKER_H_ #include #include #include -#include "ballistica/core/object.h" -#include "ballistica/math/rect.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/math/rect.h" -namespace ballistica { +namespace ballistica::base { class TextPacker : public Object { public: @@ -80,6 +80,6 @@ class TextPacker : public Object { std::list spans_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_TEXT_TEXT_PACKER_H_ +#endif // BALLISTICA_BASE_GRAPHICS_TEXT_TEXT_PACKER_H_ diff --git a/src/ballistica/graphics/texture/dds.cc b/src/ballistica/base/graphics/texture/dds.cc similarity index 94% rename from src/ballistica/graphics/texture/dds.cc rename to src/ballistica/base/graphics/texture/dds.cc index 56ec6245..3fe3bc1a 100644 --- a/src/ballistica/graphics/texture/dds.cc +++ b/src/ballistica/base/graphics/texture/dds.cc @@ -1,8 +1,9 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/texture/dds.h" +#include "ballistica/base/graphics/texture/dds.h" -#include "ballistica/platform/platform.h" +#include "ballistica/core/core.h" +#include "ballistica/core/platform/core_platform.h" #if BA_ENABLE_OPENGL @@ -13,7 +14,7 @@ /* No warranty is expressed or implied. Use at your own risk, */ /* or not at all. */ -namespace ballistica { +namespace ballistica::base { // Should tidy this up to use unsigned vals but don't want to touch for now. #pragma clang diagnostic push @@ -39,7 +40,7 @@ void LoadDDS(const std::string& file_name, unsigned char** buffers, int* widths, TextureQuality texture_quality, int min_quality, int* base_level) { (*base_level) = 0; - FILE* f = g_platform->FOpen(file_name.c_str(), "rb"); + FILE* f = g_core->platform->FOpen(file_name.c_str(), "rb"); if (!f) throw Exception("can't open file: \"" + file_name + "\""); DDS_header hdr{}; @@ -140,6 +141,6 @@ void LoadDDS(const std::string& file_name, unsigned char** buffers, int* widths, #pragma clang diagnostic pop -} // namespace ballistica +} // namespace ballistica::base #endif // BA_ENABLE_OPENGL diff --git a/src/ballistica/graphics/texture/dds.h b/src/ballistica/base/graphics/texture/dds.h similarity index 95% rename from src/ballistica/graphics/texture/dds.h rename to src/ballistica/base/graphics/texture/dds.h index 4d7a050d..f4dddc02 100644 --- a/src/ballistica/graphics/texture/dds.h +++ b/src/ballistica/base/graphics/texture/dds.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_TEXTURE_DDS_H_ -#define BALLISTICA_GRAPHICS_TEXTURE_DDS_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_TEXTURE_DDS_H_ +#define BALLISTICA_BASE_GRAPHICS_TEXTURE_DDS_H_ #pragma clang diagnostic push #pragma ide diagnostic ignored "OCUnusedMacroInspection" @@ -15,7 +15,7 @@ #include -#include "ballistica/ballistica.h" +#include "ballistica/base/base.h" #if BA_ENABLE_OPENGL @@ -100,7 +100,7 @@ #pragma clang diagnostic pop -namespace ballistica { +namespace ballistica::base { union DDS_header { struct { @@ -150,8 +150,8 @@ void LoadDDS(const std::string& file_name, unsigned char** buffers, int* widths, int* heights, TextureFormat* formats, size_t* sizes, TextureQuality texture_quality, int min_quality, int* base_level); -} // namespace ballistica +} // namespace ballistica::base #endif // BA_ENABLE_OPENGL -#endif // BALLISTICA_GRAPHICS_TEXTURE_DDS_H_ +#endif // BALLISTICA_BASE_GRAPHICS_TEXTURE_DDS_H_ diff --git a/src/ballistica/graphics/texture/ktx.cc b/src/ballistica/base/graphics/texture/ktx.cc similarity index 99% rename from src/ballistica/graphics/texture/ktx.cc rename to src/ballistica/base/graphics/texture/ktx.cc index 5b5c0be4..33781f3d 100644 --- a/src/ballistica/graphics/texture/ktx.cc +++ b/src/ballistica/base/graphics/texture/ktx.cc @@ -1,12 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/texture/ktx.h" +#include "ballistica/base/graphics/texture/ktx.h" -#include "ballistica/platform/platform.h" +#include "ballistica/core/core.h" +#include "ballistica/core/platform/core_platform.h" #if !BA_HEADLESS_BUILD -namespace ballistica { +namespace ballistica::base { // Inspection is not terribly happy about this file but it works so not // gonna touch it for now. @@ -71,7 +72,7 @@ typedef struct KTX_header_t { void LoadKTX(const std::string& file_name, unsigned char** buffers, int* widths, int* heights, TextureFormat* formats, size_t* sizes, TextureQuality texture_quality, int min_quality, int* base_level) { - FILE* f = g_platform->FOpen(file_name.c_str(), "rb"); + FILE* f = g_core->platform->FOpen(file_name.c_str(), "rb"); if (!f) throw Exception("can't open file: \"" + file_name + "\""); KTX_header_t header{}; @@ -2292,6 +2293,6 @@ void KTXUnpackETC(const GLubyte* srcETC, const GLenum srcFormat, #pragma clang diagnostic pop -} // namespace ballistica +} // namespace ballistica::base #endif // !BA_HEADLESS_BUILD diff --git a/src/ballistica/graphics/texture/ktx.h b/src/ballistica/base/graphics/texture/ktx.h similarity index 75% rename from src/ballistica/graphics/texture/ktx.h rename to src/ballistica/base/graphics/texture/ktx.h index 0f9463b8..8c13e1c0 100644 --- a/src/ballistica/graphics/texture/ktx.h +++ b/src/ballistica/base/graphics/texture/ktx.h @@ -1,16 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_TEXTURE_KTX_H_ -#define BALLISTICA_GRAPHICS_TEXTURE_KTX_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_TEXTURE_KTX_H_ +#define BALLISTICA_BASE_GRAPHICS_TEXTURE_KTX_H_ #include -#include "ballistica/ballistica.h" +#include "ballistica/base/base.h" // currently need gl for this stuff.. probably not necessary. #if BA_ENABLE_OPENGL -namespace ballistica { +namespace ballistica::base { void LoadKTX(const std::string& file_name, unsigned char** buffers, int* widths, int* heights, TextureFormat* formats, size_t* sizes, @@ -22,8 +22,8 @@ void KTXUnpackETC(const uint8_t* src_etc, unsigned int src_format, unsigned int* internal_format, unsigned int* type, int r16_formats, bool supports_srgb); -} // namespace ballistica +} // namespace ballistica::base #endif // BA_ENABLE_OPENGL -#endif // BALLISTICA_GRAPHICS_TEXTURE_KTX_H_ +#endif // BALLISTICA_BASE_GRAPHICS_TEXTURE_KTX_H_ diff --git a/src/ballistica/graphics/texture/pvr.cc b/src/ballistica/base/graphics/texture/pvr.cc similarity index 97% rename from src/ballistica/graphics/texture/pvr.cc rename to src/ballistica/base/graphics/texture/pvr.cc index bc52a418..c2cc9125 100644 --- a/src/ballistica/graphics/texture/pvr.cc +++ b/src/ballistica/base/graphics/texture/pvr.cc @@ -1,10 +1,10 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/graphics/texture/pvr.h" +#include "ballistica/base/graphics/texture/pvr.h" -#include "ballistica/platform/platform.h" +#include "ballistica/core/platform/core_platform.h" -namespace ballistica { +namespace ballistica::base { #define PVR_TEXTURE_FLAG_TYPE_MASK 0xffu @@ -46,7 +46,7 @@ void LoadPVR(const std::string& file_name, unsigned char** buffers, int* widths, TextureQuality texture_quality, int min_quality, int* base_level) { (*base_level) = 0; - FILE* f = g_platform->FOpen(file_name.c_str(), "rb"); + FILE* f = g_core->platform->FOpen(file_name.c_str(), "rb"); if (!f) throw Exception("can't open file: \"" + file_name + "\""); TextureFormat internal_format; @@ -240,4 +240,4 @@ void LoadPVR(const std::string& file_name, unsigned char** buffers, int* widths, fclose(f); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/graphics/texture/pvr.h b/src/ballistica/base/graphics/texture/pvr.h similarity index 59% rename from src/ballistica/graphics/texture/pvr.h rename to src/ballistica/base/graphics/texture/pvr.h index 27a50f08..a755287c 100644 --- a/src/ballistica/graphics/texture/pvr.h +++ b/src/ballistica/base/graphics/texture/pvr.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GRAPHICS_TEXTURE_PVR_H_ -#define BALLISTICA_GRAPHICS_TEXTURE_PVR_H_ +#ifndef BALLISTICA_BASE_GRAPHICS_TEXTURE_PVR_H_ +#define BALLISTICA_BASE_GRAPHICS_TEXTURE_PVR_H_ #include -#include "ballistica/ballistica.h" +#include "ballistica/base/base.h" -namespace ballistica { +namespace ballistica::base { static char gPVRTexIdentifier[5] = "PVR!"; @@ -15,6 +15,6 @@ void LoadPVR(const std::string& file_name, unsigned char** buffers, int* widths, int* heights, TextureFormat* formats, size_t* sizes, TextureQuality texture_quality, int min_quality, int* base_level); -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GRAPHICS_TEXTURE_PVR_H_ +#endif // BALLISTICA_BASE_GRAPHICS_TEXTURE_PVR_H_ diff --git a/src/ballistica/base/input/device/input_device.cc b/src/ballistica/base/input/device/input_device.cc new file mode 100644 index 00000000..51ae3b86 --- /dev/null +++ b/src/ballistica/base/input/device/input_device.cc @@ -0,0 +1,93 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/input/device/input_device.h" + +#include +#include + +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/logic/logic.h" + +namespace ballistica::base { + +InputDevice::InputDevice() = default; + +auto InputDevice::ShouldBeHiddenFromUser() -> bool { + // Ask the input system whether they want to ignore us.. + return g_base->input->ShouldCompletelyIgnoreInputDevice(this); +} + +auto InputDevice::GetDeviceName() -> std::string { + assert(g_base->InLogicThread()); + return GetRawDeviceName(); +} + +auto InputDevice::GetButtonName(int id) -> std::string { + // By default just say 'button 1' or whatnot. + // FIXME: should return this in Lstr json form. + return g_base->assets->GetResourceString("buttonText") + " " + + std::to_string(id); +} + +auto InputDevice::GetAxisName(int id) -> std::string { + // By default just return 'axis 5' or whatnot. + // FIXME: should return this in Lstr json form. + return g_base->assets->GetResourceString("axisText") + " " + + std::to_string(id); +} + +auto InputDevice::HasMeaningfulButtonNames() -> bool { return false; } + +auto InputDevice::GetPersistentIdentifier() const -> std::string { + assert(g_base->InLogicThread()); + char buffer[128]; + snprintf(buffer, sizeof(buffer), "#%d", number_); + return buffer; +} + +InputDevice::~InputDevice() { assert(g_base->InLogicThread()); } + +// Called to let the current host/client-session know that we'd like to control +// something please. +void InputDevice::RequestPlayer() { + assert(g_base->InLogicThread()); + // Make note that we're being used in some way. + last_input_time_millisecs_ = + static_cast(g_base->logic->display_time() * 1000.0); + + delegate_->RequestPlayer(); +} + +// If we're attached to a remote player, ship completed packets every now and +// then. +void InputDevice::Update() { delegate_->Update(); } + +auto InputDevice::AttachedToPlayer() const -> bool { + return delegate_->AttachedToPlayer(); +} + +void InputDevice::DetachFromPlayer() { delegate_->DetachFromPlayer(); } + +void InputDevice::UpdateLastInputTime() { + // Keep our own individual time, and also let + // the overall input system know something happened. + last_input_time_millisecs_ = + static_cast(g_base->logic->display_time() * 1000.0); + g_base->input->mark_input_active(); +} + +void InputDevice::InputCommand(InputType type, float value) { + assert(g_base->InLogicThread()); + + // Make note that we're being used in some way. + UpdateLastInputTime(); + + delegate_->InputCommand(type, value); +} + +void InputDevice::ResetHeldStates() {} + +auto InputDevice::GetPartyButtonName() const -> std::string { return ""; } + +} // namespace ballistica::base diff --git a/src/ballistica/base/input/device/input_device.h b/src/ballistica/base/input/device/input_device.h new file mode 100644 index 00000000..c511e6d4 --- /dev/null +++ b/src/ballistica/base/input/device/input_device.h @@ -0,0 +1,152 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_INPUT_DEVICE_INPUT_DEVICE_H_ +#define BALLISTICA_BASE_INPUT_DEVICE_INPUT_DEVICE_H_ + +#include +#include + +#include "ballistica/base/input/device/input_device_delegate.h" +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::base { + +/// Base class for game input devices (keyboard, gamepads, etc). +/// InputDevices can be allocated in any thread (generally on the main +/// thread in response to some system event). An AddInputDevice() call +/// should then be pushed to the logic thread to inform it of the new +/// device. Deletion of the input-device is then handled by the logic +/// thread and can be triggered by pushing a RemoveInputDevice() call +/// to it. +class InputDevice : public Object { + public: + InputDevice(); + ~InputDevice() override; + + /// Request a player in the local game for this device. + void RequestPlayer(); + + auto AttachedToPlayer() const -> bool; + void DetachFromPlayer(); + + /// Pass some input command on to whatever we're controlling + /// (player or remote-player). + void InputCommand(InputType type, float value = 0.0f); + + /// Return the (not necessarily unique) name of the input device. + auto GetDeviceName() -> std::string; + + /// Called during the game loop - for manual button repeats, etc. + virtual void Update(); + + virtual void ResetHeldStates(); + + /// Return the name of the button used to evoke the party menu from UIs. + virtual auto GetPartyButtonName() const -> std::string; + + /// Returns a number specific to this device type (saying this is the Nth + /// device of this type). + auto device_number() const -> int { return number_; } + auto GetPersistentIdentifier() const -> std::string; + + /// Return the overall device index; unique among all devices. + auto index() const -> int { return index_; } + void set_index(int index_in) { index_ = index_in; } + + /// Our number specific to our type. + auto number() const { return number_; } + void set_number(int n) { number_ = n; } + + /// Read and apply new control values from config. + virtual void UpdateMapping() {} + +#if BA_SDL_BUILD || BA_MINSDL_BUILD + virtual void HandleSDLEvent(const SDL_Event* e) {} +#endif + virtual auto GetAllowsConfiguring() -> bool { return true; } + + virtual auto IsController() -> bool { return false; } + virtual auto IsSDLController() -> bool { return false; } + virtual auto IsTouchScreen() -> bool { return false; } + virtual auto IsRemoteControl() -> bool { return false; } + virtual auto IsTestInput() -> bool { return false; } + virtual auto IsKeyboard() -> bool { return false; } + virtual auto IsMFiController() -> bool { return false; } + virtual auto IsLocal() -> bool { return true; } + virtual auto IsUIOnly() -> bool { return false; } + virtual auto IsRemoteApp() -> bool { return false; } + + /// Return a human-readable name for a button/key. + virtual auto GetButtonName(int index) -> std::string; + + /// Return a human-readable name for an axis. + virtual auto GetAxisName(int index) -> std::string; + + /// Return whether button-names returned by GetButtonName() for this + /// device are identifiable to the user on the input-device itself. + /// For example, if a gamepad returns 'A', 'B', 'X', 'Y', etc. as names, + /// this should return true, but if it returns 'button 123', 'button 124', + /// etc. then it should return false. + virtual auto HasMeaningfulButtonNames() -> bool; + + /// Should return true if the input device has a start button and + /// that button activates default widgets (will cause a start icon to show up + /// on them). + virtual auto start_button_activates_default_widget() -> bool { return false; } + auto last_input_time_millisecs() const -> millisecs_t { + return last_input_time_millisecs_; + } + virtual auto ShouldBeHiddenFromUser() -> bool; + + /// Return a human-readable name for the device's type. + /// This is used for display and also for storing configs/etc. + virtual auto GetRawDeviceName() -> std::string { return "Input Device"; } + + /// Return any extra description for the device. + /// This portion is only used for display and not for storing configs. + /// An example is Mac PS3 controllers; they return "(bluetooth)" or "(usb)" + /// here depending on how they are connected. + virtual auto GetDeviceExtraDescription() -> std::string { return ""; } + + /// Devices that have a way of identifying uniquely against other devices of + /// the same type (a serial number, usb-port, etc) should return that here as + /// a string. + virtual auto GetDeviceIdentifier() -> std::string { return ""; } + + /// Called for all devices when they've successfully been added + /// to the input-device list, have a valid ID, name, etc. + virtual void ConnectionComplete() {} + + void UpdateLastInputTime(); + + auto delegate() -> InputDeviceDelegate& { return *delegate_; } + auto set_delegate(const Object::Ref& delegate) { + delegate_ = delegate; + } + + /// Provide a custom player-name that the game can choose to honor. + /// This is used by the remote app. + auto custom_default_player_name() const -> std::string { + return custom_default_player_name_; + } + void set_custom_default_player_name(const std::string& val) { + custom_default_player_name_ = val; + } + + private: + Object::Ref delegate_; + + // note: this is in base-net-time + millisecs_t last_input_time_millisecs_{}; + + int index_{-1}; // Our overall device index. + int number_{-1}; // Our type-specific number. + + std::string custom_default_player_name_; + + BA_DISALLOW_CLASS_COPIES(InputDevice); +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_INPUT_DEVICE_INPUT_DEVICE_H_ diff --git a/src/ballistica/base/input/device/input_device_delegate.cc b/src/ballistica/base/input/device/input_device_delegate.cc new file mode 100644 index 00000000..8d43d6b4 --- /dev/null +++ b/src/ballistica/base/input/device/input_device_delegate.cc @@ -0,0 +1,35 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/input/device/input_device_delegate.h" + +#include "ballistica/base/input/device/input_device.h" +#include "ballistica/shared/math/vector3f.h" + +namespace ballistica::base { + +InputDeviceDelegate::InputDeviceDelegate() = default; +InputDeviceDelegate::~InputDeviceDelegate() = default; + +auto InputDeviceDelegate::AttachedToPlayer() const -> bool { return false; } + +auto InputDeviceDelegate::DescribeAttachedTo() const -> std::string { + return AttachedToPlayer() ? "something" : "nothing"; +} + +auto InputDeviceDelegate::GetPlayerPosition() -> std::optional { + return {}; +} + +void InputDeviceDelegate::RequestPlayer() {} + +void InputDeviceDelegate::InputCommand(InputType type, float value) {} + +void InputDeviceDelegate::set_input_device(InputDevice* device) { + input_device_ = device; +} + +void InputDeviceDelegate::DetachFromPlayer() {} + +void InputDeviceDelegate::Update() {} + +} // namespace ballistica::base diff --git a/src/ballistica/base/input/device/input_device_delegate.h b/src/ballistica/base/input/device/input_device_delegate.h new file mode 100644 index 00000000..a908e20a --- /dev/null +++ b/src/ballistica/base/input/device/input_device_delegate.h @@ -0,0 +1,57 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_INPUT_DEVICE_INPUT_DEVICE_DELEGATE_H_ +#define BALLISTICA_BASE_INPUT_DEVICE_INPUT_DEVICE_DELEGATE_H_ + +#include + +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::base { + +class InputDeviceDelegate : public Object { + public: + InputDeviceDelegate(); + ~InputDeviceDelegate() override; + + /// Called when the device is pressing a button/etc. which should + /// 'join the game' in some way. + virtual void RequestPlayer(); + + /// Does the device currently have something in the game it is controlling? + virtual auto AttachedToPlayer() const -> bool; + + /// For debugging; should return something like 'remote-player' + /// 'local-player'. + virtual auto DescribeAttachedTo() const -> std::string; + + /// Does the device have a position for something in the game that it is + /// controlling? (for drawing guides such as touch-screen direction + /// arrows/etc.) + virtual auto GetPlayerPosition() -> std::optional; + + /// Called when the device is passing input to its player. + virtual void InputCommand(InputType type, float value); + + /// Called when the device wants to stop controlling any player in the + /// game it is controlling. + virtual void DetachFromPlayer(); + + /// Called once per update cycle (generally corresponds with frame draws). + virtual void Update(); + + /// An input-device-delegate should never outlive its input_device; + /// our accessor returns a reference to show this does not need + /// to be checked. + auto input_device() const -> InputDevice& { return *input_device_; } + void set_input_device(InputDevice* device); + auto InputDeviceExists() const -> bool { return input_device_.Exists(); } + + private: + Object::WeakRef input_device_; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_INPUT_DEVICE_INPUT_DEVICE_DELEGATE_H_ diff --git a/src/ballistica/input/device/joystick.cc b/src/ballistica/base/input/device/joystick_input.cc similarity index 82% rename from src/ballistica/input/device/joystick.cc rename to src/ballistica/base/input/device/joystick_input.cc index 8841157a..1b7c9781 100644 --- a/src/ballistica/input/device/joystick.cc +++ b/src/ballistica/base/input/device/joystick_input.cc @@ -1,21 +1,23 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/input/device/joystick.h" +#include "ballistica/base/input/device/joystick_input.h" -#include "ballistica/app/app.h" -#include "ballistica/app/app_flavor.h" -#include "ballistica/audio/audio.h" -#include "ballistica/core/thread.h" -#include "ballistica/graphics/renderer.h" -#include "ballistica/logic/connection/connection_set.h" -#include "ballistica/logic/player.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_command.h" -#include "ballistica/ui/root_ui.h" -#include "ballistica/ui/ui.h" -#include "ballistica/ui/widget/container_widget.h" +#include "ballistica/base/app/app.h" +#include "ballistica/base/app/app_mode.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/classic/python/classic_python.h" +#include "ballistica/core/core.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_command.h" +#include "ballistica/ui_v1/support/root_ui.h" +#include "ballistica/ui_v1/widget/container_widget.h" -namespace ballistica { +namespace ballistica::base { const char* kMFiControllerName = "iOS/Mac Controller"; @@ -33,13 +35,14 @@ const int kJoystickCalibrationTimeThreshold{1000}; // How fast calibration occurs. const float kJoystickCalibrationSpeed = 0.7f; -Joystick::Joystick(int sdl_joystick_id, const std::string& custom_device_name, - bool can_configure, bool calibrate) +JoystickInput::JoystickInput(int sdl_joystick_id, + const std::string& custom_device_name, + bool can_configure, bool calibrate) : calibration_threshold_(kJoystickCalibrationThreshold), calibration_break_threshold_(kJoystickCalibrationBreakThreshold), custom_device_name_(custom_device_name), can_configure_(can_configure), - creation_time_(GetRealTime()), + creation_time_(g_core->GetAppTimeMillisecs()), calibrate_(calibrate) { // This is the default calibration for 'non-full' analog calibration. for (float& analog_calibration_val : analog_calibration_vals_) { @@ -57,7 +60,7 @@ Joystick::Joystick(int sdl_joystick_id, const std::string& custom_device_name, #if BA_ENABLE_SDL_JOYSTICKS // Standard SDL joysticks should be getting created in the main thread. // Custom joysticks can come from anywhere. - assert(InMainThread()); + assert(g_core->InMainThread()); sdl_joystick_ = SDL_JoystickOpen(sdl_joystick_id); assert(sdl_joystick_); @@ -110,7 +113,7 @@ Joystick::Joystick(int sdl_joystick_id, const std::string& custom_device_name, } } -auto Joystick::GetAxisName(int index) -> std::string { +auto JoystickInput::GetAxisName(int index) -> std::string { // On android, lets return some popular axis names. if (g_buildconfig.ostype_android()) { @@ -151,7 +154,7 @@ auto Joystick::GetAxisName(int index) -> std::string { return InputDevice::GetAxisName(index); } -auto Joystick::HasMeaningfulButtonNames() -> bool { +auto JoystickInput::HasMeaningfulButtonNames() -> bool { // Only return true in cases where we know we have proper names // for stuff. if (is_mfi_controller_) { @@ -160,7 +163,7 @@ auto Joystick::HasMeaningfulButtonNames() -> bool { return g_buildconfig.ostype_android(); } -auto Joystick::GetButtonName(int index) -> std::string { +auto JoystickInput::GetButtonName(int index) -> std::string { // FIXME: Should get fancier here now that PS4 and XBone // controllers are supported through this. if (is_mfi_controller_) { @@ -183,13 +186,13 @@ auto Joystick::GetButtonName(int index) -> std::string { if (strstr(GetDeviceName().c_str(), "Samsung Game Pad EI")) { switch (index) { case 101: - return g_logic->CharStr(SpecialChar::kDiceButton4); // Y + return g_base->assets->CharStr(SpecialChar::kDiceButton4); // Y case 100: - return g_logic->CharStr(SpecialChar::kDiceButton3); // X + return g_base->assets->CharStr(SpecialChar::kDiceButton3); // X case 98: - return g_logic->CharStr(SpecialChar::kDiceButton2); // B + return g_base->assets->CharStr(SpecialChar::kDiceButton2); // B case 97: - return g_logic->CharStr(SpecialChar::kDiceButton1); // A + return g_base->assets->CharStr(SpecialChar::kDiceButton1); // A default: break; } @@ -272,13 +275,13 @@ auto Joystick::GetButtonName(int index) -> std::string { case 204: return "B16"; case 90: - return g_logic->CharStr(SpecialChar::kRewindButton); + return g_base->assets->CharStr(SpecialChar::kRewindButton); case 91: - return g_logic->CharStr(SpecialChar::kFastForwardButton); + return g_base->assets->CharStr(SpecialChar::kFastForwardButton); case 24: - return g_logic->CharStr(SpecialChar::kDpadCenterButton); + return g_base->assets->CharStr(SpecialChar::kDpadCenterButton); case 86: - return g_logic->CharStr(SpecialChar::kPlayPauseButton); + return g_base->assets->CharStr(SpecialChar::kPlayPauseButton); default: break; } @@ -286,14 +289,14 @@ auto Joystick::GetButtonName(int index) -> std::string { return InputDevice::GetButtonName(index); } -Joystick::~Joystick() { - if (!InLogicThread()) { +JoystickInput::~JoystickInput() { + if (!g_base->InLogicThread()) { Log(LogLevel::kError, "Joystick dying in wrong thread."); } // Kill our child if need be. if (child_joy_stick_) { - g_input->RemoveInputDevice(child_joy_stick_, true); + g_base->input->RemoveInputDevice(child_joy_stick_, true); child_joy_stick_ = nullptr; } @@ -304,9 +307,9 @@ Joystick::~Joystick() { // here in the logic thread.. if (sdl_joystick_) { #if BA_ENABLE_SDL_JOYSTICKS - assert(g_app_flavor); + assert(g_base->app); auto joystick = sdl_joystick_; - g_app_flavor->thread()->PushCall( + g_base->app->event_loop()->PushCall( [joystick] { SDL_JoystickClose(joystick); }); sdl_joystick_ = nullptr; #else @@ -316,16 +319,9 @@ Joystick::~Joystick() { } } -auto Joystick::GetDefaultPlayerName() -> std::string { - if (!custom_default_player_name_.empty()) { - return custom_default_player_name_; - } - return InputDevice::GetDefaultPlayerName(); -} +void JoystickInput::ConnectionComplete() { assert(g_base->InLogicThread()); } -void Joystick::ConnectionComplete() { assert(InLogicThread()); } - -auto Joystick::ShouldBeHiddenFromUser() -> bool { +auto JoystickInput::ShouldBeHiddenFromUser() -> bool { std::string d_name = GetDeviceName(); // To lowercase. @@ -343,7 +339,8 @@ auto Joystick::ShouldBeHiddenFromUser() -> bool { } } -auto Joystick::GetCalibratedValue(float raw, float neutral) const -> int32_t { +auto JoystickInput::GetCalibratedValue(float raw, float neutral) const + -> int32_t { int32_t val; float dead_zone = 0.5f; float mag, target; @@ -362,10 +359,10 @@ auto Joystick::GetCalibratedValue(float raw, float neutral) const -> int32_t { return val; } -void Joystick::Update() { +void JoystickInput::Update() { InputDevice::Update(); - assert(InLogicThread()); + assert(g_base->InLogicThread()); // We seem to get a fair amount of bogus direction-pressed events from newly // plugged in joysticks.. this leads to continuous scrolling in menus and such @@ -378,7 +375,7 @@ void Joystick::Update() { // Let's take this opportunity to update our calibration // (should probably have a specific place to do that but this works) if (calibrate_) { - millisecs_t time = GetRealTime(); + millisecs_t time = g_core->GetAppTimeMillisecs(); // If we're doing 'aggressive' auto-recalibration we expand extents outward // but suck them inward a tiny bit too to account for jitter or random fluke @@ -390,8 +387,8 @@ void Joystick::Update() { * ((kJoystickAnalogCalibrationDivisions) / (2.0f * kPi))); cell = std::min(kJoystickAnalogCalibrationDivisions - 1, std::max(0, cell)); - float x = jaxis_x_ / 32767.0f; - float y = jaxis_y_ / 32767.0f; + auto x{static_cast(jaxis_x_) / 32767.0f}; + float y{static_cast(jaxis_y_) / 32767.0f}; float mag = sqrtf(x * x + y * y); if (mag > analog_calibration_vals_[cell]) { analog_calibration_vals_[cell] = std::min(1.0f, mag); @@ -414,13 +411,14 @@ void Joystick::Update() { && (static_cast(std::abs(jaxis_raw_x_)) < calibration_threshold_)) { calibrated_neutral_x_ = - kJoystickCalibrationSpeed * jaxis_raw_x_ + kJoystickCalibrationSpeed * static_cast(jaxis_raw_x_) + (1.0f - kJoystickCalibrationSpeed) * calibrated_neutral_x_; // Grab our new calibrated x value.. if it differs from the current, ship // an event. if (static_cast(std::abs(jaxis_raw_x_)) < calibration_threshold_) { - int32_t x = GetCalibratedValue(jaxis_raw_x_, calibrated_neutral_x_); + int32_t x = GetCalibratedValue(static_cast(jaxis_raw_x_), + calibrated_neutral_x_); if (x != jaxis_x_) { jaxis_x_ = x; InputCommand(InputType::kLeftRight, @@ -433,13 +431,14 @@ void Joystick::Update() { && (static_cast(std::abs(jaxis_raw_y_)) < calibration_threshold_)) { calibrated_neutral_y_ = - kJoystickCalibrationSpeed * jaxis_raw_y_ + kJoystickCalibrationSpeed * static_cast(jaxis_raw_y_) + (1.0f - kJoystickCalibrationSpeed) * calibrated_neutral_y_; // Grab our new calibrated x value.. if it differs from the current, ship // an event. if (fabs(static_cast(jaxis_raw_y_)) < calibration_threshold_) { - int32_t y = GetCalibratedValue(jaxis_raw_y_, calibrated_neutral_y_); + int32_t y = GetCalibratedValue(static_cast(jaxis_raw_y_), + calibrated_neutral_y_); if (y != jaxis_y_) { jaxis_y_ = y; InputCommand(InputType::kUpDown, @@ -453,11 +452,11 @@ void Joystick::Update() { if (up_held_ || down_held_ || left_held_ || right_held_) { // Don't ask for the widget unless we have something held. // (otherwise we prevent other inputs from getting at it) - if (g_ui->GetWidgetForInput(this)) { + if (g_base->ui->GetWidgetForInput(this)) { millisecs_t repeat_delay = kJoystickRepeatDelay; - millisecs_t t = GetRealTime(); - WidgetMessage::Type c = WidgetMessage::Type::kEmptyMessage; + millisecs_t t = g_core->GetAppTimeMillisecs(); + auto c = WidgetMessage::Type::kEmptyMessage; if (t - last_hold_time_ < repeat_delay) { return; } @@ -478,17 +477,19 @@ void Joystick::Update() { c = WidgetMessage::Type::kMoveRight; } if (pass) { - g_ui->SendWidgetMessage(WidgetMessage(c)); + g_base->ui->SendWidgetMessage(WidgetMessage(c)); } // Set another repeat to happen sooner. - last_hold_time_ = t - static_cast(repeat_delay * 0.8f); + last_hold_time_ = + t + - static_cast(static_cast(repeat_delay) * 0.8f); } } } } -void Joystick::SetStandardExtendedButtons() { +void JoystickInput::SetStandardExtendedButtons() { // Assign some non-zero dpad values so we can drive them in custom joysticks. up_button_ = 20; down_button_ = 21; @@ -500,7 +501,7 @@ void Joystick::SetStandardExtendedButtons() { remote_enter_button_ = 13; } -void Joystick::ResetHeldStates() { +void JoystickInput::ResetHeldStates() { // So we push events through even if there's a dialog in the way. resetting_ = true; @@ -532,12 +533,12 @@ void Joystick::ResetHeldStates() { resetting_ = false; } -void Joystick::HandleSDLEvent(const SDL_Event* e) { - assert(InLogicThread()); +void JoystickInput::HandleSDLEvent(const SDL_Event* e) { + assert(g_base->InLogicThread()); // If we've got a child joystick, send them any events they're set to handle. if (child_joy_stick_) { - assert(g_logic); + assert(g_base->logic); bool send = false; switch (e->type) { @@ -573,7 +574,7 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { break; } if (send) { - g_input->PushJoystickEvent(*e, child_joy_stick_); + g_base->input->PushJoystickEvent(*e, child_joy_stick_); return; } } @@ -583,7 +584,7 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { return; } - millisecs_t time = GetRealTime(); + millisecs_t time = g_core->GetAppTimeMillisecs(); SDL_Event e2; // Ignore analog-stick input while we're holding a hat switch or d-pad @@ -758,22 +759,22 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { || e->jbutton.button == start_button_2_) { // If there's some UI up already, we just pass this along to it. // otherwise we request a main menu. - if (g_ui && g_ui->screen_root_widget() - && g_ui->screen_root_widget()->HasChildren()) { + if (g_base && g_base->ui->screen_root_widget() + && g_base->ui->screen_root_widget()->HasChildren()) { // Do nothing in this case. } else { // If there's no menu up, // tell the game to pop it up and snag menu ownership for ourself. - g_ui->PushMainMenuPressCall(this); + g_base->ui->PushMainMenuPressCall(this); return; } } // On our oculus build, select presses reset the orientation. - if (e->jbutton.button == vr_reorient_button_ && IsVRMode()) { - ScreenMessage(g_logic->GetResourceString("vrOrientationResetText"), + if (e->jbutton.button == vr_reorient_button_ && g_core->IsVRMode()) { + ScreenMessage(g_base->assets->GetResourceString("vrOrientationResetText"), {0, 1, 0}); - g_app->reset_vr_orientation = true; + g_core->reset_vr_orientation = true; return; } } @@ -839,7 +840,7 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { // below our 'pressed' threshold.. Otherwise fuzzy analog joystick // readings would cause rampant UI stealing even if no events are being sent. bool would_go_to_dialog = false; - WidgetMessage::Type wm = WidgetMessage::Type::kEmptyMessage; + auto wm = WidgetMessage::Type::kEmptyMessage; if (isAnalogStickJAxisEvent || isHoldPositionEvent) { // Even when we're not sending, clear out some 'held' states. @@ -879,7 +880,7 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { UpdateLastInputTime(); } - if (would_go_to_dialog && g_ui->GetWidgetForInput(this)) { + if (would_go_to_dialog && g_base->ui->GetWidgetForInput(this)) { bool pass = false; // Special case.. either joy-axis-motion or hold-position events trigger @@ -888,14 +889,14 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { if (dialogJaxisX > kJoystickDiscreteThreshold) { // To the right. if (!right_held_ && !up_held_ && !down_held_) { - last_hold_time_ = GetRealTime(); + last_hold_time_ = g_core->GetAppTimeMillisecs(); right_held_ = true; wm = WidgetMessage::Type::kMoveRight; pass = true; } } else if (dialogJaxisX < -kJoystickDiscreteThreshold) { if (!left_held_ && !up_held_ && !down_held_) { - last_hold_time_ = GetRealTime(); + last_hold_time_ = g_core->GetAppTimeMillisecs(); wm = WidgetMessage::Type::kMoveLeft; pass = true; left_held_ = true; @@ -903,14 +904,14 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { } if (dialogJaxisY > kJoystickDiscreteThreshold) { if (!down_held_ && !left_held_ && !right_held_) { - last_hold_time_ = GetRealTime(); + last_hold_time_ = g_core->GetAppTimeMillisecs(); wm = WidgetMessage::Type::kMoveDown; pass = true; down_held_ = true; } } else if (dialogJaxisY < -kJoystickDiscreteThreshold) { if (!up_held_ && !left_held_ && !right_held_) { - last_hold_time_ = GetRealTime(); + last_hold_time_ = g_core->GetAppTimeMillisecs(); wm = WidgetMessage::Type::kMoveUp; pass = true; up_held_ = true; @@ -927,7 +928,7 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { switch (e->jhat.value) { case SDL_HAT_LEFT: { if (!left_held_) { - last_hold_time_ = GetRealTime(); + last_hold_time_ = g_core->GetAppTimeMillisecs(); wm = WidgetMessage::Type::kMoveLeft; pass = true; left_held_ = true; @@ -938,7 +939,7 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { case SDL_HAT_RIGHT: { if (!right_held_) { - last_hold_time_ = GetRealTime(); + last_hold_time_ = g_core->GetAppTimeMillisecs(); wm = WidgetMessage::Type::kMoveRight; pass = true; right_held_ = true; @@ -948,7 +949,7 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { } case SDL_HAT_UP: { if (!up_held_) { - last_hold_time_ = GetRealTime(); + last_hold_time_ = g_core->GetAppTimeMillisecs(); wm = WidgetMessage::Type::kMoveUp; pass = true; up_held_ = true; @@ -958,7 +959,7 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { } case SDL_HAT_DOWN: { if (!down_held_) { - last_hold_time_ = GetRealTime(); + last_hold_time_ = g_core->GetAppTimeMillisecs(); wm = WidgetMessage::Type::kMoveDown; pass = true; down_held_ = true; @@ -983,19 +984,20 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { pass = true; if (e->jbutton.button == start_button_ || e->jbutton.button == start_button_2_) { - if (start_button_activates_default_widget_) + if (start_button_activates_default_widget_) { wm = WidgetMessage::Type::kStart; - else + } else { pass = false; + } } else if (e->jbutton.button == bomb_button_ || e->jbutton.button == back_button_) { wm = WidgetMessage::Type::kCancel; } else { // FIXME: Need a call we can make for this. bool do_party_button = false; - int party_size = g_logic->GetPartySize(); - if (party_size > 1 || g_logic->connections()->connection_to_host() - || g_ui->root_ui()->always_draw_party_icon()) { + int party_size = g_base->app_mode->GetPartySize(); + if (party_size > 1 || g_base->app_mode->HasConnectionToHost() + || g_base->ui->root_ui()->always_draw_party_icon()) { do_party_button = true; } @@ -1005,7 +1007,7 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { if (do_party_button && e->jbutton.button == pickup_button_ && (!IsRemoteControl())) { pass = false; - g_ui->root_ui()->ActivatePartyIcon(); + g_base->ui->root_ui()->ActivatePartyIcon(); break; } wm = WidgetMessage::Type::kActivate; @@ -1016,29 +1018,31 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { break; } if (pass) { - g_ui->SendWidgetMessage(WidgetMessage(wm)); + g_base->ui->SendWidgetMessage(WidgetMessage(wm)); } return; } // If there's a UI up (even if we didn't get it) lets not pass events along. // The only exception is if we're doing a reset. - Widget* root{}; - if (g_ui) { - root = g_ui->screen_root_widget(); + ui_v1::Widget* root{}; + if (g_base) { + root = g_base->ui->screen_root_widget(); } if (root && root->HasChildren() && !resetting_) { return; } - if (!attached_to_player()) { + if (!AttachedToPlayer()) { if (e->type == SDL_JOYBUTTONDOWN && (e->jbutton.button != hold_position_button_) && (e->jbutton.button != back_button_)) { if (ui_only_ || e->jbutton.button == remote_enter_button_) { - millisecs_t current_time = GetRealTime(); + millisecs_t current_time = g_core->GetAppTimeMillisecs(); if (current_time - last_ui_only_print_time_ > 5000) { - g_python->obj(Python::ObjID::kUIRemotePressCall).Call(); + g_base->python->objs() + .Get(BasePython::ObjID::kUIRemotePressCall) + .Call(); last_ui_only_print_time_ = current_time; } } else { @@ -1113,8 +1117,8 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { if (static_cast(abs(jaxis_raw_x_)) < calibration_threshold_ && static_cast(abs(jaxis_raw_y_)) < calibration_threshold_) { - input_value = - GetCalibratedValue(input_value, calibrated_neutral_x_); + input_value = GetCalibratedValue(static_cast(input_value), + calibrated_neutral_x_); } } if (input_value > 32767) { @@ -1130,8 +1134,8 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { if (static_cast(abs(jaxis_raw_x_)) < calibration_threshold_ && static_cast(abs(jaxis_raw_y_)) < calibration_threshold_) { - input_value = - GetCalibratedValue(input_value, calibrated_neutral_y_); + input_value = GetCalibratedValue(static_cast(input_value), + calibrated_neutral_y_); } } input_value = -input_value; @@ -1150,12 +1154,15 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { // Handle analog stick calibration.. 'full' auto-recalibration. if (auto_recalibrate_analog_stick_) { int cell = static_cast( - (atan2(static_cast(jaxis_y_), static_cast(jaxis_x_)) + (atan2f(static_cast(jaxis_y_), + static_cast(jaxis_x_)) + kPi) * ((kJoystickAnalogCalibrationDivisions) / (2.0f * kPi))); cell = std::min(kJoystickAnalogCalibrationDivisions - 1, std::max(0, cell)); - input_value *= (1.0f / analog_calibration_vals_[cell]); + input_value = + static_cast(static_cast(input_value) + * (1.0f / analog_calibration_vals_[cell])); if (input_value > 32767) { input_value = 32767; } else if (input_value < -32767) { @@ -1269,8 +1276,8 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { } } // NOLINT(readability/fn_size) Yes I know this is too long. -void Joystick::UpdateRunningState() { - if (!attached_to_player()) { +void JoystickInput::UpdateRunningState() { + if (!AttachedToPlayer()) { return; } float value; @@ -1293,55 +1300,54 @@ void Joystick::UpdateRunningState() { } } -void Joystick::UpdateMapping() { - assert(InLogicThread()); +void JoystickInput::UpdateMapping() { + assert(g_base->InLogicThread()); // This doesn't apply to manual ones (except children which are). if (!can_configure_ && !parent_joy_stick_) { return; } + auto* cpy{g_classic->python}; + // If we're a child, use our parent's id to search for config values and just // tack on a '2'. - Joystick* js = parent_joy_stick_ ? parent_joy_stick_ : this; + JoystickInput* js = parent_joy_stick_ ? parent_joy_stick_ : this; std::string ext = parent_joy_stick_ ? "_B" : ""; // Grab all button values from Python. Traditionally we stored these // with the first index 1 so we need to subtract 1 to get the zero-indexed // value. (grumble). - jump_button_ = g_python->GetControllerValue(js, "buttonJump" + ext) - 1; - punch_button_ = g_python->GetControllerValue(js, "buttonPunch" + ext) - 1; - bomb_button_ = g_python->GetControllerValue(js, "buttonBomb" + ext) - 1; - pickup_button_ = g_python->GetControllerValue(js, "buttonPickUp" + ext) - 1; - start_button_ = g_python->GetControllerValue(js, "buttonStart" + ext) - 1; - start_button_2_ = g_python->GetControllerValue(js, "buttonStart2" + ext) - 1; + jump_button_ = cpy->GetControllerValue(js, "buttonJump" + ext) - 1; + punch_button_ = cpy->GetControllerValue(js, "buttonPunch" + ext) - 1; + bomb_button_ = cpy->GetControllerValue(js, "buttonBomb" + ext) - 1; + pickup_button_ = cpy->GetControllerValue(js, "buttonPickUp" + ext) - 1; + start_button_ = cpy->GetControllerValue(js, "buttonStart" + ext) - 1; + start_button_2_ = cpy->GetControllerValue(js, "buttonStart2" + ext) - 1; hold_position_button_ = - g_python->GetControllerValue(js, "buttonHoldPosition" + ext) - 1; - run_button1_ = g_python->GetControllerValue(js, "buttonRun1" + ext) - 1; - run_button2_ = g_python->GetControllerValue(js, "buttonRun2" + ext) - 1; + cpy->GetControllerValue(js, "buttonHoldPosition" + ext) - 1; + run_button1_ = cpy->GetControllerValue(js, "buttonRun1" + ext) - 1; + run_button2_ = cpy->GetControllerValue(js, "buttonRun2" + ext) - 1; vr_reorient_button_ = - g_python->GetControllerValue(js, "buttonVRReorient" + ext) - 1; - ignored_button_ = g_python->GetControllerValue(js, "buttonIgnored" + ext) - 1; - ignored_button2_ = - g_python->GetControllerValue(js, "buttonIgnored2" + ext) - 1; - ignored_button3_ = - g_python->GetControllerValue(js, "buttonIgnored3" + ext) - 1; - ignored_button4_ = - g_python->GetControllerValue(js, "buttonIgnored4" + ext) - 1; + cpy->GetControllerValue(js, "buttonVRReorient" + ext) - 1; + ignored_button_ = cpy->GetControllerValue(js, "buttonIgnored" + ext) - 1; + ignored_button2_ = cpy->GetControllerValue(js, "buttonIgnored2" + ext) - 1; + ignored_button3_ = cpy->GetControllerValue(js, "buttonIgnored3" + ext) - 1; + ignored_button4_ = cpy->GetControllerValue(js, "buttonIgnored4" + ext) - 1; int old_run_trigger_1 = run_trigger1_; - run_trigger1_ = g_python->GetControllerValue(js, "triggerRun1" + ext) - 1; + run_trigger1_ = cpy->GetControllerValue(js, "triggerRun1" + ext) - 1; int old_run_trigger_2 = run_trigger2_; - run_trigger2_ = g_python->GetControllerValue(js, "triggerRun2" + ext) - 1; - up_button_ = g_python->GetControllerValue(js, "buttonUp" + ext) - 1; - left_button_ = g_python->GetControllerValue(js, "buttonLeft" + ext) - 1; - right_button_ = g_python->GetControllerValue(js, "buttonRight" + ext) - 1; - down_button_ = g_python->GetControllerValue(js, "buttonDown" + ext) - 1; - up_button2_ = g_python->GetControllerValue(js, "buttonUp2" + ext) - 1; - left_button2_ = g_python->GetControllerValue(js, "buttonLeft2" + ext) - 1; - right_button2_ = g_python->GetControllerValue(js, "buttonRight2" + ext) - 1; - down_button2_ = g_python->GetControllerValue(js, "buttonDown2" + ext) - 1; + run_trigger2_ = cpy->GetControllerValue(js, "triggerRun2" + ext) - 1; + up_button_ = cpy->GetControllerValue(js, "buttonUp" + ext) - 1; + left_button_ = cpy->GetControllerValue(js, "buttonLeft" + ext) - 1; + right_button_ = cpy->GetControllerValue(js, "buttonRight" + ext) - 1; + down_button_ = cpy->GetControllerValue(js, "buttonDown" + ext) - 1; + up_button2_ = cpy->GetControllerValue(js, "buttonUp2" + ext) - 1; + left_button2_ = cpy->GetControllerValue(js, "buttonLeft2" + ext) - 1; + right_button2_ = cpy->GetControllerValue(js, "buttonRight2" + ext) - 1; + down_button2_ = cpy->GetControllerValue(js, "buttonDown2" + ext) - 1; unassigned_buttons_run_ = static_cast( - g_python->GetControllerValue(js, "unassignedButtonsRun" + ext)); + cpy->GetControllerValue(js, "unassignedButtonsRun" + ext)); // If our run trigger has changed, reset its calibration. // NOTE: It looks like on Mac we're getting analog trigger values from -1 to 1 @@ -1356,21 +1362,21 @@ void Joystick::UpdateMapping() { run_trigger2_max_ = 0.8f; } - int ival = g_python->GetControllerValue(js, "uiOnly" + ext); + int ival = cpy->GetControllerValue(js, "uiOnly" + ext); if (ival == -1) { ui_only_ = false; } else { ui_only_ = static_cast(ival); } - ival = g_python->GetControllerValue(js, "ignoreCompletely" + ext); + ival = cpy->GetControllerValue(js, "ignoreCompletely" + ext); if (ival == -1) { ignore_completely_ = false; } else { ignore_completely_ = static_cast(ival); } - ival = g_python->GetControllerValue(js, "autoRecalibrateAnalogSticks" + ext); + ival = cpy->GetControllerValue(js, "autoRecalibrateAnalogSticks" + ext); { bool was_on = auto_recalibrate_analog_stick_; @@ -1396,8 +1402,7 @@ void Joystick::UpdateMapping() { } } - ival = g_python->GetControllerValue( - js, "startButtonActivatesDefaultWidget" + ext); + ival = cpy->GetControllerValue(js, "startButtonActivatesDefaultWidget" + ext); if (ival == -1) { start_button_activates_default_widget_ = true; @@ -1406,7 +1411,7 @@ void Joystick::UpdateMapping() { } // Update calibration stuff. - float as = g_python->GetControllerFloatValue(js, "analogStickDeadZone" + ext); + float as = cpy->GetControllerFloatValue(js, "analogStickDeadZone" + ext); if (as < 0) { as = 1.0f; @@ -1420,7 +1425,7 @@ void Joystick::UpdateMapping() { calibration_threshold_ = kJoystickCalibrationThreshold * as; calibration_break_threshold_ = kJoystickCalibrationBreakThreshold * as; - hat_ = g_python->GetControllerValue(js, "dpad" + ext) - 1; + hat_ = cpy->GetControllerValue(js, "dpad" + ext) - 1; // If unset, use our default. if (hat_ == -2) { @@ -1432,7 +1437,7 @@ void Joystick::UpdateMapping() { } // Grab our analog stick. - analog_lr_ = g_python->GetControllerValue(js, "analogStickLR" + ext) - 1; + analog_lr_ = cpy->GetControllerValue(js, "analogStickLR" + ext) - 1; // If we got unset, set to our default. if (analog_lr_ == -2) { @@ -1443,7 +1448,7 @@ void Joystick::UpdateMapping() { } } - analog_ud_ = g_python->GetControllerValue(js, "analogStickUD" + ext) - 1; + analog_ud_ = cpy->GetControllerValue(js, "analogStickUD" + ext) - 1; // If we got unset, set to our default. if (analog_ud_ == -2) { @@ -1456,7 +1461,7 @@ void Joystick::UpdateMapping() { // See whether we have a child-joystick and create it if need be. if (!parent_joy_stick_) { - int enable = g_python->GetControllerValue(js, "enableSecondary"); + int enable = cpy->GetControllerValue(js, "enableSecondary"); if (enable == -1) { enable = 0; } @@ -1467,25 +1472,25 @@ void Joystick::UpdateMapping() { snprintf(m, sizeof(m), "%s B", GetDeviceName().c_str()); if (!child_joy_stick_) { child_joy_stick_ = - Object::NewDeferred(-1, // Not an sdl joystick. - m, // Device name. - false, // Allow configuring. - true); // Do calibrate. + Object::NewDeferred(-1, // Not an sdl joystick. + m, // Device name. + false, // Allow configuring. + true); // Do calibrate. child_joy_stick_->parent_joy_stick_ = this; - assert(g_input); - g_input->AddInputDevice(child_joy_stick_, true); + assert(g_base->input); + g_base->input->AddInputDevice(child_joy_stick_, true); } } else { // Kill if need be. if (child_joy_stick_) { - g_input->RemoveInputDevice(child_joy_stick_, true); + g_base->input->RemoveInputDevice(child_joy_stick_, true); child_joy_stick_ = nullptr; } } } } -auto Joystick::GetRawDeviceName() -> std::string { +auto JoystickInput::GetRawDeviceName() -> std::string { if (!custom_device_name_.empty()) { return custom_device_name_; } @@ -1504,7 +1509,7 @@ auto Joystick::GetRawDeviceName() -> std::string { } } -auto Joystick::GetDeviceExtraDescription() -> std::string { +auto JoystickInput::GetDeviceExtraDescription() -> std::string { std::string s; // On mac, PS3 controllers can connect via USB or bluetooth, @@ -1527,15 +1532,15 @@ auto Joystick::GetDeviceExtraDescription() -> std::string { return s; } -auto Joystick::GetDeviceIdentifier() -> std::string { +auto JoystickInput::GetDeviceIdentifier() -> std::string { return raw_sdl_joystick_identifier_; } -auto Joystick::GetPartyButtonName() const -> std::string { +auto JoystickInput::GetPartyButtonName() const -> std::string { if (g_buildconfig.iircade_build()) { return "X"; } - return g_logic->CharStr(SpecialChar::kTopButton); + return g_base->assets->CharStr(SpecialChar::kTopButton); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/input/device/joystick.h b/src/ballistica/base/input/device/joystick_input.h similarity index 88% rename from src/ballistica/input/device/joystick.h rename to src/ballistica/base/input/device/joystick_input.h index 0b5a587a..dcbb1ccf 100644 --- a/src/ballistica/input/device/joystick.h +++ b/src/ballistica/base/input/device/joystick_input.h @@ -1,14 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_INPUT_DEVICE_JOYSTICK_H_ -#define BALLISTICA_INPUT_DEVICE_JOYSTICK_H_ +#ifndef BALLISTICA_BASE_INPUT_DEVICE_JOYSTICK_INPUT_H_ +#define BALLISTICA_BASE_INPUT_DEVICE_JOYSTICK_INPUT_H_ #include #include -#include "ballistica/input/device/input_device.h" +#include "ballistica/base/input/device/input_device.h" -namespace ballistica { +namespace ballistica::base { // iOS controllers feel more natural with a lower threshold here, // but it throws off cheap controllers elsewhere. @@ -19,15 +19,15 @@ const int kJoystickAnalogCalibrationDivisions{20}; extern const char* kMFiControllerName; /// A physical game controller. -class Joystick : public InputDevice { +class JoystickInput : public InputDevice { public: // Create from an SDL joystick id. // Pass -1 to create a manual joystick from a non-sdl-source. // (in which case you are in charge of feeding it SDL events to make it go) - explicit Joystick(int index, const std::string& custom_device_name = "", - bool can_configure = true, bool calibrate = true); + explicit JoystickInput(int index, const std::string& custom_device_name = "", + bool can_configure = true, bool calibrate = true); - ~Joystick() override; + ~JoystickInput() override; void HandleSDLEvent(const SDL_Event* e) override; @@ -48,7 +48,6 @@ class Joystick : public InputDevice { } auto GetPartyButtonName() const -> std::string override; - auto GetDefaultPlayerName() -> std::string override; auto GetButtonName(int index) -> std::string override; auto GetAxisName(int index) -> std::string override; @@ -72,9 +71,6 @@ class Joystick : public InputDevice { start_button_activates_default_widget_ = value; } - void set_custom_default_player_name(const std::string& val) { - custom_default_player_name_ = val; - } auto HasMeaningfulButtonNames() -> bool override; protected: @@ -91,12 +87,11 @@ class Joystick : public InputDevice { void UpdateRunningState(); auto GetCalibratedValue(float raw, float neutral) const -> int32_t; - std::string custom_default_player_name_; std::string raw_sdl_joystick_name_; std::string raw_sdl_joystick_identifier_; float run_value_{}; - Joystick* child_joy_stick_{}; - Joystick* parent_joy_stick_{}; + JoystickInput* child_joy_stick_{}; + JoystickInput* parent_joy_stick_{}; millisecs_t last_ui_only_print_time_{}; bool ui_only_{}; bool unassigned_buttons_run_{true}; @@ -193,8 +188,10 @@ class Joystick : public InputDevice { float calibrated_neutral_y_{}; bool resetting_{}; bool calibrate_{}; + + BA_DISALLOW_CLASS_COPIES(JoystickInput); }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_INPUT_DEVICE_JOYSTICK_H_ +#endif // BALLISTICA_BASE_INPUT_DEVICE_JOYSTICK_INPUT_H_ diff --git a/src/ballistica/input/device/keyboard_input.cc b/src/ballistica/base/input/device/keyboard_input.cc similarity index 90% rename from src/ballistica/input/device/keyboard_input.cc rename to src/ballistica/base/input/device/keyboard_input.cc index c4e26bae..b668d808 100644 --- a/src/ballistica/input/device/keyboard_input.cc +++ b/src/ballistica/base/input/device/keyboard_input.cc @@ -1,14 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/input/device/keyboard_input.h" +#include "ballistica/base/input/device/keyboard_input.h" -#include "ballistica/logic/player.h" -#include "ballistica/platform/platform.h" -#include "ballistica/python/python.h" -#include "ballistica/ui/ui.h" -#include "ballistica/ui/widget/container_widget.h" +#include "ballistica/base/platform/base_platform.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/classic/python/classic_python.h" +#include "ballistica/ui_v1/widget/container_widget.h" -namespace ballistica { +namespace ballistica::base { KeyboardInput::KeyboardInput(KeyboardInput* parent_keyboard_input_in) { if (parent_keyboard_input_in) { @@ -48,9 +47,9 @@ auto KeyboardInput::HandleKey(const SDL_Keysym* keysym, bool repeat, bool down) -> bool { // Only allow the *main* keyboard to talk to the UI if (parent_keyboard_input_ == nullptr) { - if (g_ui->GetWidgetForInput(this)) { + if (g_base->ui->GetWidgetForInput(this)) { bool pass = false; - WidgetMessage::Type c = WidgetMessage::Type::kEmptyMessage; + auto c = WidgetMessage::Type::kEmptyMessage; if (down) { switch (keysym->sym) { case SDLK_TAB: @@ -130,16 +129,17 @@ auto KeyboardInput::HandleKey(const SDL_Keysym* keysym, bool repeat, bool down) } } if (pass) { - g_ui->SendWidgetMessage(WidgetMessage(c, keysym)); + g_base->ui->SendWidgetMessage(WidgetMessage(c, keysym)); } return (pass); } } // Bring up menu if start is pressed. - if (keysym->sym == start_key_ && !repeat && g_ui && g_ui->screen_root_widget() - && g_ui->screen_root_widget()->GetChildCount() == 0) { - g_ui->PushMainMenuPressCall(this); + if (keysym->sym == start_key_ && !repeat && g_base->ui + && g_base->ui->screen_root_widget() + && g_base->ui->screen_root_widget()->GetChildCount() == 0) { + g_base->ui->PushMainMenuPressCall(this); return true; } @@ -157,7 +157,7 @@ auto KeyboardInput::HandleKey(const SDL_Keysym* keysym, bool repeat, bool down) #pragma clang diagnostic pop - if (!attached_to_player()) { + if (!AttachedToPlayer()) { if (down && ((keysym->sym == jump_key_) || (keysym->sym == punch_key_) || (keysym->sym == bomb_key_) @@ -373,7 +373,9 @@ void KeyboardInput::UpdateRun(SDL_Keycode key, bool down) { } void KeyboardInput::UpdateMapping() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); + + auto* cpy = g_classic->python; SDL_Keycode up_key_default, down_key_default, left_key_default, right_key_default, jump_key_default, punch_key_default, bomb_key_default, @@ -411,44 +413,44 @@ void KeyboardInput::UpdateMapping() { left_key_assigned_ = right_key_assigned_ = up_key_assigned_ = down_key_assigned_ = false; - int val = g_python->GetControllerValue(this, "buttonJump"); + int val = cpy->GetControllerValue(this, "buttonJump"); jump_key_ = (val == -1) ? jump_key_default : (SDL_Keycode)val; UpdateArrowKeys(jump_key_); - val = g_python->GetControllerValue(this, "buttonPunch"); + val = cpy->GetControllerValue(this, "buttonPunch"); punch_key_ = (val == -1) ? punch_key_default : (SDL_Keycode)val; UpdateArrowKeys(punch_key_); - val = g_python->GetControllerValue(this, "buttonBomb"); + val = cpy->GetControllerValue(this, "buttonBomb"); bomb_key_ = (val == -1) ? bomb_key_default : (SDL_Keycode)val; UpdateArrowKeys(bomb_key_); - val = g_python->GetControllerValue(this, "buttonPickUp"); + val = cpy->GetControllerValue(this, "buttonPickUp"); pick_up_key_ = (val == -1) ? pick_up_key_default : (SDL_Keycode)val; UpdateArrowKeys(pick_up_key_); - val = g_python->GetControllerValue(this, "buttonHoldPosition"); + val = cpy->GetControllerValue(this, "buttonHoldPosition"); hold_position_key_ = (val == -1) ? hold_position_key_default : (SDL_Keycode)val; UpdateArrowKeys(hold_position_key_); - val = g_python->GetControllerValue(this, "buttonStart"); + val = cpy->GetControllerValue(this, "buttonStart"); start_key_ = (val == -1) ? start_key_default : (SDL_Keycode)val; UpdateArrowKeys(start_key_); - val = g_python->GetControllerValue(this, "buttonUp"); + val = cpy->GetControllerValue(this, "buttonUp"); up_key_ = (val == -1) ? up_key_default : (SDL_Keycode)val; UpdateArrowKeys(up_key_); - val = g_python->GetControllerValue(this, "buttonDown"); + val = cpy->GetControllerValue(this, "buttonDown"); down_key_ = (val == -1) ? down_key_default : (SDL_Keycode)val; UpdateArrowKeys(down_key_); - val = g_python->GetControllerValue(this, "buttonLeft"); + val = cpy->GetControllerValue(this, "buttonLeft"); left_key_ = (val == -1) ? left_key_default : (SDL_Keycode)val; UpdateArrowKeys(left_key_); - val = g_python->GetControllerValue(this, "buttonRight"); + val = cpy->GetControllerValue(this, "buttonRight"); right_key_ = (val == -1) ? right_key_default : (SDL_Keycode)val; UpdateArrowKeys(right_key_); @@ -470,7 +472,7 @@ void KeyboardInput::UpdateArrowKeys(SDL_Keycode key) { } auto KeyboardInput::GetButtonName(int index) -> std::string { - return g_platform->GetKeyName(index); + return g_base->platform->GetKeyName(index); // return InputDevice::GetButtonName(index); } @@ -478,4 +480,4 @@ auto KeyboardInput::GetRawDeviceName() -> std::string { return "Keyboard"; } auto KeyboardInput::GetPartyButtonName() const -> std::string { return "F5"; } auto KeyboardInput::HasMeaningfulButtonNames() -> bool { return true; } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/input/device/keyboard_input.h b/src/ballistica/base/input/device/keyboard_input.h similarity index 74% rename from src/ballistica/input/device/keyboard_input.h rename to src/ballistica/base/input/device/keyboard_input.h index 23e597c1..6d790e9c 100644 --- a/src/ballistica/input/device/keyboard_input.h +++ b/src/ballistica/base/input/device/keyboard_input.h @@ -1,24 +1,24 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_INPUT_DEVICE_KEYBOARD_INPUT_H_ -#define BALLISTICA_INPUT_DEVICE_KEYBOARD_INPUT_H_ +#ifndef BALLISTICA_BASE_INPUT_DEVICE_KEYBOARD_INPUT_H_ +#define BALLISTICA_BASE_INPUT_DEVICE_KEYBOARD_INPUT_H_ #include #include -#include "ballistica/input/device/input_device.h" -#include "ballistica/platform/min_sdl.h" +#include "ballistica/base/input/device/input_device.h" +#include "ballistica/core/platform/support/min_sdl.h" -namespace ballistica { +namespace ballistica::base { class KeyboardInput : public InputDevice { public: explicit KeyboardInput(KeyboardInput* parent); ~KeyboardInput() override; auto HandleKey(const SDL_Keysym* keysym, bool repeat, bool down) -> bool; - auto UpdateMapping() -> void override; + void UpdateMapping() override; auto GetRawDeviceName() -> std::string override; - auto ResetHeldStates() -> void override; + void ResetHeldStates() override; auto left_key_assigned() const { return left_key_assigned_; } auto right_key_assigned() const { return right_key_assigned_; } auto up_key_assigned() const { return up_key_assigned_; } @@ -29,8 +29,8 @@ class KeyboardInput : public InputDevice { auto GetButtonName(int index) -> std::string override; private: - auto UpdateArrowKeys(SDL_Keycode key) -> void; - auto UpdateRun(SDL_Keycode key, bool down) -> void; + void UpdateArrowKeys(SDL_Keycode key); + void UpdateRun(SDL_Keycode key, bool down); SDL_Keycode up_key_{}; SDL_Keycode down_key_{}; SDL_Keycode left_key_{}; @@ -55,6 +55,6 @@ class KeyboardInput : public InputDevice { std::set keys_held_; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_INPUT_DEVICE_KEYBOARD_INPUT_H_ +#endif // BALLISTICA_BASE_INPUT_DEVICE_KEYBOARD_INPUT_H_ diff --git a/src/ballistica/input/device/test_input.cc b/src/ballistica/base/input/device/test_input.cc similarity index 76% rename from src/ballistica/input/device/test_input.cc rename to src/ballistica/base/input/device/test_input.cc index 40272ee4..7e20a291 100644 --- a/src/ballistica/input/device/test_input.cc +++ b/src/ballistica/base/input/device/test_input.cc @@ -1,26 +1,28 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/input/device/test_input.h" +#include "ballistica/base/input/device/test_input.h" -#include "ballistica/input/device/joystick.h" -#include "ballistica/input/input.h" -#include "ballistica/logic/logic.h" -#include "ballistica/platform/min_sdl.h" +#include "ballistica/base/input/device/joystick_input.h" +#include "ballistica/base/input/input.h" +#include "ballistica/core/platform/support/min_sdl.h" +#include "ballistica/shared/math/random.h" -namespace ballistica { +namespace ballistica::base { TestInput::TestInput() { - joystick_ = Object::NewDeferred(-1, // not an sdl joystick - "TestInput", // device name - false, // allow configuring? - false); // calibrate?; - g_input->PushAddInputDeviceCall(joystick_, true); + joystick_ = Object::NewDeferred(-1, // not an sdl joystick + "TestInput", // device name + false, // allow configuring? + false); // calibrate?; + g_base->input->PushAddInputDeviceCall(joystick_, true); } -TestInput::~TestInput() { g_input->PushRemoveInputDeviceCall(joystick_, true); } +TestInput::~TestInput() { + g_base->input->PushRemoveInputDeviceCall(joystick_, true); +} void TestInput::Reset() { - assert(InMainThread()); + assert(g_core->InMainThread()); reset_ = true; } @@ -31,7 +33,7 @@ void TestInput::HandleAlreadyPressedTwice() { } void TestInput::Process(millisecs_t time) { - assert(InMainThread()); + assert(g_core->InMainThread()); if (reset_) { reset_ = false; @@ -75,10 +77,10 @@ void TestInput::Process(millisecs_t time) { e.type = SDL_JOYAXISMOTION; e.jaxis.axis = 0; e.jaxis.value = static_cast_check_fit(ud_); - g_input->PushJoystickEvent(e, joystick_); + g_base->input->PushJoystickEvent(e, joystick_); e.jaxis.axis = 1; e.jaxis.value = static_cast_check_fit(lr_); - g_input->PushJoystickEvent(e, joystick_); + g_base->input->PushJoystickEvent(e, joystick_); } else { // Button change. r = RandomFloat(); @@ -94,7 +96,7 @@ void TestInput::Process(millisecs_t time) { } e.type = jump_pressed_ ? SDL_JOYBUTTONDOWN : SDL_JOYBUTTONUP; e.jbutton.button = 0; - g_input->PushJoystickEvent(e, joystick_); + g_base->input->PushJoystickEvent(e, joystick_); } } else if (r > 0.5f) { // Bomb: @@ -112,7 +114,7 @@ void TestInput::Process(millisecs_t time) { } e.type = bomb_pressed_ ? SDL_JOYBUTTONDOWN : SDL_JOYBUTTONUP; e.jbutton.button = 2; - g_input->PushJoystickEvent(e, joystick_); + g_base->input->PushJoystickEvent(e, joystick_); } } else if (r > 0.25f) { @@ -128,7 +130,7 @@ void TestInput::Process(millisecs_t time) { } e.type = pickup_pressed_ ? SDL_JOYBUTTONDOWN : SDL_JOYBUTTONUP; e.jbutton.button = 3; - g_input->PushJoystickEvent(e, joystick_); + g_base->input->PushJoystickEvent(e, joystick_); } } else { // Punch: @@ -142,11 +144,11 @@ void TestInput::Process(millisecs_t time) { } e.type = punch_pressed_ ? SDL_JOYBUTTONDOWN : SDL_JOYBUTTONUP; e.jbutton.button = 1; - g_input->PushJoystickEvent(e, joystick_); + g_base->input->PushJoystickEvent(e, joystick_); } } } } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/input/device/test_input.h b/src/ballistica/base/input/device/test_input.h similarity index 66% rename from src/ballistica/input/device/test_input.h rename to src/ballistica/base/input/device/test_input.h index 5f349144..34e2e4ce 100644 --- a/src/ballistica/input/device/test_input.h +++ b/src/ballistica/base/input/device/test_input.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_INPUT_DEVICE_TEST_INPUT_H_ -#define BALLISTICA_INPUT_DEVICE_TEST_INPUT_H_ +#ifndef BALLISTICA_BASE_INPUT_DEVICE_TEST_INPUT_H_ +#define BALLISTICA_BASE_INPUT_DEVICE_TEST_INPUT_H_ -#include "ballistica/ballistica.h" +#include "ballistica/base/base.h" -namespace ballistica { +namespace ballistica::base { class TestInput { public: @@ -27,11 +27,11 @@ class TestInput { millisecs_t join_end_time_{9999}; int join_press_count_{}; bool reset_{true}; - Joystick* joystick_{}; + JoystickInput* joystick_{}; bool print_non_join_{}; bool print_already_did2_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_INPUT_DEVICE_TEST_INPUT_H_ +#endif // BALLISTICA_BASE_INPUT_DEVICE_TEST_INPUT_H_ diff --git a/src/ballistica/input/device/touch_input.cc b/src/ballistica/base/input/device/touch_input.cc similarity index 85% rename from src/ballistica/input/device/touch_input.cc rename to src/ballistica/base/input/device/touch_input.cc index a76bfa0c..341749ed 100644 --- a/src/ballistica/input/device/touch_input.cc +++ b/src/ballistica/base/input/device/touch_input.cc @@ -1,18 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/input/device/touch_input.h" +#include "ballistica/base/input/device/touch_input.h" -#include "ballistica/app/app.h" -#include "ballistica/app/app_config.h" -#include "ballistica/graphics/camera.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/logic/host_activity.h" -#include "ballistica/logic/player.h" -#include "ballistica/python/python.h" -#include "ballistica/scene/node/player_node.h" -#include "ballistica/ui/ui.h" +#include "ballistica/base/app/app_config.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/graphics/support/camera.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/ui/ui.h" -namespace ballistica { +namespace ballistica::base { const float kButtonSpread = 10.0f; const float kDrawDepth = -0.07f; @@ -67,7 +64,7 @@ void TouchInput::HandleTouchEvent(TouchEvent::Type type, void* touch, float x, } TouchInput::TouchInput() { - switch (g_ui->scale()) { + switch (g_base->ui->scale()) { case UIScale::kSmall: base_controls_scale_ = 2.0f; world_draw_scale_ = 1.2f; @@ -82,18 +79,19 @@ TouchInput::TouchInput() { break; } - assert(g_app->touch_input == nullptr); - g_app->touch_input = this; + assert(g_base); + assert(g_base->touch_input == nullptr); + g_base->touch_input = this; } TouchInput::~TouchInput() = default; void TouchInput::UpdateButtons(bool new_touch) { - millisecs_t real_time = GetRealTime(); + millisecs_t real_time = g_core->GetAppTimeMillisecs(); float spread_scaled_actions = kButtonSpread * base_controls_scale_ * controls_scale_actions_; - float width = g_graphics->screen_virtual_width(); - float height = g_graphics->screen_virtual_height(); + float width = g_base->graphics->screen_virtual_width(); + float height = g_base->graphics->screen_virtual_height(); float edge_buffer = spread_scaled_actions; if (new_touch && action_control_type_ == ActionControlType::kSwipe) { @@ -124,7 +122,7 @@ void TouchInput::UpdateButtons(bool new_touch) { throw Exception(); } if (buttons_touch_) { - last_buttons_touch_time_ = GetRealTime(); + last_buttons_touch_time_ = g_core->GetAppTimeMillisecs(); } // Handle swipe mode. @@ -351,21 +349,21 @@ void TouchInput::UpdateDPad() { } void TouchInput::Draw(FrameDef* frame_def) { - assert(InLogicThread()); - bool active = (!g_ui->IsWindowPresent()); + assert(g_base->InLogicThread()); + bool active = (!g_base->ui->IsWindowPresent()); millisecs_t real_time = frame_def->real_time(); // Update our action center whenever possible in case screen is resized. if (!buttons_touch_) { - float width = g_graphics->screen_virtual_width(); - float height = g_graphics->screen_virtual_height(); + float width = g_base->graphics->screen_virtual_width(); + float height = g_base->graphics->screen_virtual_height(); buttons_x_ = width * buttons_default_frac_x_; buttons_y_ = height * buttons_default_frac_y_; } // Same for dpad. if (!d_pad_touch_) { - float width = g_graphics->screen_virtual_width(); - float height = g_graphics->screen_virtual_height(); + float width = g_base->graphics->screen_virtual_width(); + float height = g_base->graphics->screen_virtual_height(); d_pad_x_ = d_pad_base_x_ = width * d_pad_default_frac_x_; d_pad_y_ = d_pad_base_y_ = height * d_pad_default_frac_y_; } @@ -378,7 +376,7 @@ void TouchInput::Draw(FrameDef* frame_def) { update_time_ += 10; // Update presence based on whether or not we're active. - if ((attached_to_player() && active) || editing_) { + if ((AttachedToPlayer() && active) || editing_) { presence_ = std::min(1.0f, presence_ + 0.06f); } else { presence_ = std::max(0.0f, presence_ - 0.06f); @@ -407,8 +405,8 @@ void TouchInput::Draw(FrameDef* frame_def) { } if (presence_ > 0.0f) { - float width = g_graphics->screen_virtual_width(); - float height = g_graphics->screen_virtual_height(); + float width = g_base->graphics->screen_virtual_width(); + float height = g_base->graphics->screen_virtual_height(); SimpleComponent c(frame_def->GetOverlayFlatPass()); c.SetTransparent(true); @@ -429,7 +427,7 @@ void TouchInput::Draw(FrameDef* frame_def) { if (movement_control_type_ == MovementControlType::kSwipe) sc2 *= 0.6f; if (movement_control_type_ == MovementControlType::kSwipe) { - c.SetTexture(g_assets->GetTexture(SystemTextureID::kTouchArrows)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kTouchArrows)); if (editing_) { float val = 1.5f + sinf(static_cast(real_time) * 0.02f); c.SetColor(val, val, 1.0f, 1.0f); @@ -442,7 +440,7 @@ void TouchInput::Draw(FrameDef* frame_def) { val = 0.35f; } c.SetColor(0.5f, 0.3f, 0.8f, val); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kCircle)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kCircle)); } float x_offs = @@ -453,7 +451,7 @@ void TouchInput::Draw(FrameDef* frame_def) { c.PushTransform(); c.Translate(d_pad_base_x_ + x_offs, d_pad_base_y_ + y_offs, kDrawDepth); c.Scale(sc2, sc2); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1)); c.PopTransform(); if (movement_control_type_ == MovementControlType::kJoystick) { @@ -467,7 +465,7 @@ void TouchInput::Draw(FrameDef* frame_def) { c.PushTransform(); c.Translate(d_pad_x_ + x_offs, d_pad_y_ + y_offs, kDrawDepth); c.Scale(sc_move * 0.5f, sc_move * 0.5f); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1)); c.PopTransform(); } } @@ -475,7 +473,8 @@ void TouchInput::Draw(FrameDef* frame_def) { if (!buttons_touch_ && action_control_type_ == ActionControlType::kSwipe && !swipe_controls_hidden_) { float sc2{sc_actions * 0.6f}; - c.SetTexture(g_assets->GetTexture(SystemTextureID::kTouchArrowsActions)); + c.SetTexture( + g_base->assets->SysTexture(SysTextureID::kTouchArrowsActions)); if (editing_) { float val = 1.5f + sinf(static_cast(real_time) * 0.02f); c.SetColor(val, val, 1.0f, 1.0f); @@ -489,7 +488,7 @@ void TouchInput::Draw(FrameDef* frame_def) { height * (-0.1f - buttons_default_frac_y_) * (1.0f - presence_); c.Translate(buttons_x_ + x_offs, buttons_y_ + y_offs, kDrawDepth); c.Scale(sc2, sc2); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1)); c.PopTransform(); } c.Submit(); @@ -497,25 +496,10 @@ void TouchInput::Draw(FrameDef* frame_def) { bool have_player_position{false}; std::vector player_position(3); - if (attached_to_player()) { - PlayerNode* player_node{}; - - // Try to come up with whichever scene is in the foreground, and try - // to pull a node for the player we're attached to. - - if (HostActivity* host_activity = - g_logic->GetForegroundContext().GetHostActivity()) { - if (Player* player = GetPlayer()) { - player_node = host_activity->scene()->GetPlayerNode(player->id()); - } - } else { - if (Scene* scene = g_logic->GetForegroundScene()) { - player_node = scene->GetPlayerNode(remote_player_id()); - } - } - if (player_node) { - have_player_position = true; - player_position = player_node->position(); + if (AttachedToPlayer()) { + auto pos = delegate().GetPlayerPosition(); + if (pos) { + player_position = pos->AsStdVector(); } } @@ -539,7 +523,7 @@ void TouchInput::Draw(FrameDef* frame_def) { base_fade = 0.25f; } else { base_fade = 0.8f; - c.SetTexture(g_assets->GetTexture(SystemTextureID::kActionButtons)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kActionButtons)); } float x_offs; @@ -552,8 +536,8 @@ void TouchInput::Draw(FrameDef* frame_def) { // Do transition in button mode. if (presence_ < 1.0f) { - float width = g_graphics->screen_virtual_width(); - float height = g_graphics->screen_virtual_height(); + float width = g_base->graphics->screen_virtual_width(); + float height = g_base->graphics->screen_virtual_height(); x_offs = width * (1.1f - buttons_default_frac_x_) * (1.0f - presence_); y_offs = height * (-0.1f - buttons_default_frac_y_) * (1.0f - presence_); @@ -632,7 +616,7 @@ void TouchInput::Draw(FrameDef* frame_def) { } else { c.Scale(b_width, b_width); } - c.DrawModel(g_assets->GetModel(SystemModelID::kActionButtonRight)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kActionButtonRight)); c.PopTransform(); } @@ -659,7 +643,7 @@ void TouchInput::Draw(FrameDef* frame_def) { } else { c.Scale(b_width, b_width); } - c.DrawModel(g_assets->GetModel(SystemModelID::kActionButtonLeft)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kActionButtonLeft)); c.PopTransform(); } @@ -685,7 +669,7 @@ void TouchInput::Draw(FrameDef* frame_def) { } else { c.Scale(b_width, b_width); } - c.DrawModel(g_assets->GetModel(SystemModelID::kActionButtonBottom)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kActionButtonBottom)); c.PopTransform(); } @@ -713,13 +697,13 @@ void TouchInput::Draw(FrameDef* frame_def) { } else { c.Scale(b_width, b_width); } - c.DrawModel(g_assets->GetModel(SystemModelID::kActionButtonTop)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kActionButtonTop)); c.PopTransform(); } // Center point. if (buttons_touch_ && action_control_type_ == ActionControlType::kSwipe) { - c.SetTexture(g_assets->GetTexture(SystemTextureID::kCircle)); + c.SetTexture(g_base->assets->SysTexture(SysTextureID::kCircle)); c.SetColor(1.0f, 1.0f, 0.0f, 0.8f); c.PushTransform(); @@ -744,7 +728,7 @@ void TouchInput::Draw(FrameDef* frame_def) { kDrawDepth); } c.Scale(b_width * 0.3f, b_width * 0.3f); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1)); c.PopTransform(); } c.PopTransform(); @@ -784,7 +768,7 @@ void TouchInput::Draw(FrameDef* frame_def) { dist = 0.05f; } - c2.SetTexture(g_assets->GetTexture(SystemTextureID::kArrow)); + c2.SetTexture(g_base->assets->SysTexture(SysTextureID::kArrow)); Matrix44f orient = Matrix44fOrient(d_pad_draw_dir_, Vector3f(0.0f, 1.0f, 0.0f)); c2.PushTransform(); @@ -795,7 +779,7 @@ void TouchInput::Draw(FrameDef* frame_def) { player_position[2]); // In happy thoughts mode show the arrow on the xy plane instead of xz. - if (g_graphics->camera()->happy_thoughts_mode()) { + if (g_base->graphics->camera()->happy_thoughts_mode()) { c2.Translate(0.0f, 0.5f, 0.0f); c2.Rotate(90.0f, 1.0f, 0.0f, 0.0f); } @@ -816,13 +800,13 @@ void TouchInput::Draw(FrameDef* frame_def) { c2.PushTransform(); c2.Translate(0.0f, dist * -0.5f, 0.0f); c2.Scale(0.15f, dist, 0.2f); - c2.DrawModel(g_assets->GetModel(SystemModelID::kArrowBack)); + c2.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kArrowBack)); c2.PopTransform(); c2.PushTransform(); c2.Translate(0.0f, dist * -1.0f - 0.15f, 0.0f); c2.Scale(0.45f, 0.3f, 0.3f); - c2.DrawModel(g_assets->GetModel(SystemModelID::kArrowFront)); + c2.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kArrowFront)); c2.PopTransform(); c2.PopTransform(); @@ -831,10 +815,10 @@ void TouchInput::Draw(FrameDef* frame_def) { } void TouchInput::UpdateMapping() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); - std::string touch_movement_type = - g_app_config->Resolve(AppConfig::StringID::kTouchMovementControlType); + std::string touch_movement_type = g_base->app_config->Resolve( + AppConfig::StringID::kTouchMovementControlType); if (touch_movement_type == "swipe") { movement_control_type_ = TouchInput::MovementControlType::kSwipe; } else if (touch_movement_type == "joystick") { @@ -845,7 +829,7 @@ void TouchInput::UpdateMapping() { movement_control_type_ = TouchInput::MovementControlType::kSwipe; } std::string touch_action_type = - g_app_config->Resolve(AppConfig::StringID::kTouchActionControlType); + g_base->app_config->Resolve(AppConfig::StringID::kTouchActionControlType); if (touch_action_type == "swipe") { action_control_type_ = TouchInput::ActionControlType::kSwipe; } else if (touch_action_type == "buttons") { @@ -855,15 +839,15 @@ void TouchInput::UpdateMapping() { action_control_type_ = TouchInput::ActionControlType::kSwipe; } - controls_scale_move_ = - g_app_config->Resolve(AppConfig::FloatID::kTouchControlsScaleMovement); - controls_scale_actions_ = - g_app_config->Resolve(AppConfig::FloatID::kTouchControlsScaleActions); + controls_scale_move_ = g_base->app_config->Resolve( + AppConfig::FloatID::kTouchControlsScaleMovement); + controls_scale_actions_ = g_base->app_config->Resolve( + AppConfig::FloatID::kTouchControlsScaleActions); swipe_controls_hidden_ = - g_app_config->Resolve(AppConfig::BoolID::kTouchControlsSwipeHidden); + g_base->app_config->Resolve(AppConfig::BoolID::kTouchControlsSwipeHidden); // Start with defaults. - switch (g_ui->scale()) { + switch (g_base->ui->scale()) { case UIScale::kSmall: buttons_default_frac_x_ = 0.88f; buttons_default_frac_y_ = 0.2f; @@ -886,20 +870,20 @@ void TouchInput::UpdateMapping() { // Now override with config. d_pad_default_frac_x_ = - g_python->GetRawConfigValue("Touch DPad X", d_pad_default_frac_x_); + g_base->python->GetRawConfigValue("Touch DPad X", d_pad_default_frac_x_); d_pad_default_frac_y_ = - g_python->GetRawConfigValue("Touch DPad Y", d_pad_default_frac_y_); - buttons_default_frac_x_ = - g_python->GetRawConfigValue("Touch Buttons X", buttons_default_frac_x_); - buttons_default_frac_y_ = - g_python->GetRawConfigValue("Touch Buttons Y", buttons_default_frac_y_); + g_base->python->GetRawConfigValue("Touch DPad Y", d_pad_default_frac_y_); + buttons_default_frac_x_ = g_base->python->GetRawConfigValue( + "Touch Buttons X", buttons_default_frac_x_); + buttons_default_frac_y_ = g_base->python->GetRawConfigValue( + "Touch Buttons Y", buttons_default_frac_y_); } auto TouchInput::HandleTouchDown(void* touch, float x, float y) -> bool { - assert(InLogicThread()); + assert(g_base->InLogicThread()); - float width = g_graphics->screen_virtual_width(); - float height = g_graphics->screen_virtual_height(); + float width = g_base->graphics->screen_virtual_width(); + float height = g_base->graphics->screen_virtual_height(); // If we're in edit mode, see if the touch should become an edit-dpad touch or // an edit-button touch. @@ -931,11 +915,11 @@ auto TouchInput::HandleTouchDown(void* touch, float x, float y) -> bool { // Normal in-game operation: // Normal operation is disabled while a UI is up. - if (g_ui->IsWindowPresent()) { + if (g_base->ui->IsWindowPresent()) { return false; } - if (!attached_to_player()) { + if (!AttachedToPlayer()) { // Ignore touches at the very top (so we don't interfere with the menu). if (y < height * 0.8f) { RequestPlayer(); @@ -944,9 +928,9 @@ auto TouchInput::HandleTouchDown(void* touch, float x, float y) -> bool { // be accidental if there's a trackpad on the controller. // ..so lets issue a warning to that effect if there's already // controllers active.. (only if we got a player though). - if (attached_to_player() && g_input->HaveControllerWithPlayer()) { + if (AttachedToPlayer() && g_base->input->HaveControllerWithPlayer()) { ScreenMessage( - g_logic->GetResourceString("touchScreenJoinWarningText"), + g_base->assets->GetResourceString("touchScreenJoinWarningText"), {1.0f, 1.0f, 0.0f}); } } @@ -987,23 +971,25 @@ auto TouchInput::HandleTouchDown(void* touch, float x, float y) -> bool { } auto TouchInput::HandleTouchUp(void* touch, float x, float y) -> bool { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // Release dpad drag touch. if (touch == d_pad_drag_touch_) { d_pad_drag_touch_ = nullptr; // Write the current frac to our config. - g_python->SetRawConfigValue("Touch DPad X", d_pad_default_frac_x_); - g_python->SetRawConfigValue("Touch DPad Y", d_pad_default_frac_y_); + g_base->python->SetRawConfigValue("Touch DPad X", d_pad_default_frac_x_); + g_base->python->SetRawConfigValue("Touch DPad Y", d_pad_default_frac_y_); } if (touch == buttons_drag_touch_) { buttons_drag_touch_ = nullptr; // Write the current frac to our config. - g_python->SetRawConfigValue("Touch Buttons X", buttons_default_frac_x_); - g_python->SetRawConfigValue("Touch Buttons Y", buttons_default_frac_y_); + g_base->python->SetRawConfigValue("Touch Buttons X", + buttons_default_frac_x_); + g_base->python->SetRawConfigValue("Touch Buttons Y", + buttons_default_frac_y_); } // Release on button touch. @@ -1029,10 +1015,10 @@ auto TouchInput::HandleTouchUp(void* touch, float x, float y) -> bool { } auto TouchInput::HandleTouchMoved(void* touch, float x, float y) -> bool { - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (touch == d_pad_drag_touch_) { - float width = g_graphics->screen_virtual_width(); - float height = g_graphics->screen_virtual_height(); + float width = g_base->graphics->screen_virtual_width(); + float height = g_base->graphics->screen_virtual_height(); float ratio_x = std::min(0.45f, std::max(0.0f, (x - d_pad_drag_x_offs_) / width)); float ratio_y = @@ -1041,8 +1027,8 @@ auto TouchInput::HandleTouchMoved(void* touch, float x, float y) -> bool { d_pad_default_frac_y_ = ratio_y; } if (touch == buttons_drag_touch_) { - float width = g_graphics->screen_virtual_width(); - float height = g_graphics->screen_virtual_height(); + float width = g_base->graphics->screen_virtual_width(); + float height = g_base->graphics->screen_virtual_height(); float ratio_x = std::min(1.0f, std::max(0.55f, (x - buttons_drag_x_offs_) / width)); float ratio_y = @@ -1052,7 +1038,7 @@ auto TouchInput::HandleTouchMoved(void* touch, float x, float y) -> bool { } // Ignore button/pad touches while gui is up. - if (g_ui->IsWindowPresent()) { + if (g_base->ui->IsWindowPresent()) { return false; } if (touch == buttons_touch_) { @@ -1072,4 +1058,4 @@ auto TouchInput::HandleTouchMoved(void* touch, float x, float y) -> bool { auto TouchInput::GetRawDeviceName() -> std::string { return "TouchScreen"; } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/input/device/touch_input.h b/src/ballistica/base/input/device/touch_input.h similarity index 89% rename from src/ballistica/input/device/touch_input.h rename to src/ballistica/base/input/device/touch_input.h index a2d2f389..557ac19b 100644 --- a/src/ballistica/input/device/touch_input.h +++ b/src/ballistica/base/input/device/touch_input.h @@ -1,14 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_INPUT_DEVICE_TOUCH_INPUT_H_ -#define BALLISTICA_INPUT_DEVICE_TOUCH_INPUT_H_ +#ifndef BALLISTICA_BASE_INPUT_DEVICE_TOUCH_INPUT_H_ +#define BALLISTICA_BASE_INPUT_DEVICE_TOUCH_INPUT_H_ #include -#include "ballistica/input/device/input_device.h" -#include "ballistica/math/vector3f.h" +#include "ballistica/base/input/device/input_device.h" +#include "ballistica/shared/math/vector3f.h" -namespace ballistica { +namespace ballistica::base { /// A touchscreen based controller for mobile devices. class TouchInput : public InputDevice { @@ -90,6 +90,6 @@ class TouchInput : public InputDevice { millisecs_t update_time_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_INPUT_DEVICE_TOUCH_INPUT_H_ +#endif // BALLISTICA_BASE_INPUT_DEVICE_TOUCH_INPUT_H_ diff --git a/src/ballistica/input/input.cc b/src/ballistica/base/input/input.cc similarity index 61% rename from src/ballistica/input/input.cc rename to src/ballistica/base/input/input.cc index a6a1b289..1efa8bd5 100644 --- a/src/ballistica/input/input.cc +++ b/src/ballistica/base/input/input.cc @@ -1,331 +1,35 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/input/input.h" +#include "ballistica/base/input/input.h" -#include "ballistica/app/app.h" -#include "ballistica/app/app_config.h" -#include "ballistica/audio/audio.h" -#include "ballistica/core/thread.h" -#include "ballistica/graphics/camera.h" -#include "ballistica/input/device/joystick.h" -#include "ballistica/input/device/keyboard_input.h" -#include "ballistica/input/device/test_input.h" -#include "ballistica/input/device/touch_input.h" -#include "ballistica/logic/player.h" -#include "ballistica/python/python.h" -#include "ballistica/ui/console.h" -#include "ballistica/ui/root_ui.h" -#include "ballistica/ui/ui.h" -#include "ballistica/ui/widget/root_widget.h" +#include "ballistica/base/app/app_config.h" +#include "ballistica/base/app/app_mode.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/graphics/support/camera.h" +#include "ballistica/base/input/device/joystick_input.h" +#include "ballistica/base/input/device/keyboard_input.h" +#include "ballistica/base/input/device/test_input.h" +#include "ballistica/base/input/device/touch_input.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/ui/console.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/ui_v1/support/root_ui.h" +#include "ballistica/ui_v1/widget/root_widget.h" -namespace ballistica { +namespace ballistica::base { -// Though it seems strange, input is actually owned by the logic thread, not the -// app thread. This keeps things simple for game logic interacting with input -// stuff (controller names, counts, etc) but means we need to be prudent about -// properly passing stuff between the game and app thread as needed. - -// The following was pulled from sdl2 -#if BA_SDL2_BUILD || BA_MINSDL_BUILD -static const char* const scancode_names[SDL_NUM_SCANCODES] = { - nullptr, - nullptr, - nullptr, - nullptr, - "A", - "B", - "C", - "D", - "E", - "F", - "G", - "H", - "I", - "J", - "K", - "L", - "M", - "N", - "O", - "P", - "Q", - "R", - "S", - "T", - "U", - "V", - "W", - "X", - "Y", - "Z", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "0", - "Return", - "Escape", - "Backspace", - "Tab", - "Space", - "-", - "=", - "[", - "]", - "\\", - "#", - ";", - "'", - "`", - ",", - ".", - "/", - "CapsLock", - "F1", - "F2", - "F3", - "F4", - "F5", - "F6", - "F7", - "F8", - "F9", - "F10", - "F11", - "F12", - "PrintScreen", - "ScrollLock", - "Pause", - "Insert", - "Home", - "PageUp", - "Delete", - "End", - "PageDown", - "Right", - "Left", - "Down", - "Up", - "Numlock", - "Keypad /", - "Keypad *", - "Keypad -", - "Keypad +", - "Keypad Enter", - "Keypad 1", - "Keypad 2", - "Keypad 3", - "Keypad 4", - "Keypad 5", - "Keypad 6", - "Keypad 7", - "Keypad 8", - "Keypad 9", - "Keypad 0", - "Keypad .", - nullptr, - "Application", - "Power", - "Keypad =", - "F13", - "F14", - "F15", - "F16", - "F17", - "F18", - "F19", - "F20", - "F21", - "F22", - "F23", - "F24", - "Execute", - "Help", - "Menu", - "Select", - "Stop", - "Again", - "Undo", - "Cut", - "Copy", - "Paste", - "Find", - "Mute", - "VolumeUp", - "VolumeDown", - nullptr, - nullptr, - nullptr, - "Keypad ,", - "Keypad = (AS400)", - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - "AltErase", - "SysReq", - "Cancel", - "Clear", - "Prior", - "Return", - "Separator", - "Out", - "Oper", - "Clear / Again", - "CrSel", - "ExSel", - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - "Keypad 00", - "Keypad 000", - "ThousandsSeparator", - "DecimalSeparator", - "CurrencyUnit", - "CurrencySubUnit", - "Keypad (", - "Keypad )", - "Keypad {", - "Keypad }", - "Keypad Tab", - "Keypad Backspace", - "Keypad A", - "Keypad B", - "Keypad C", - "Keypad D", - "Keypad E", - "Keypad F", - "Keypad XOR", - "Keypad ^", - "Keypad %", - "Keypad <", - "Keypad >", - "Keypad &", - "Keypad &&", - "Keypad |", - "Keypad ||", - "Keypad :", - "Keypad #", - "Keypad Space", - "Keypad @", - "Keypad !", - "Keypad MemStore", - "Keypad MemRecall", - "Keypad MemClear", - "Keypad MemAdd", - "Keypad MemSubtract", - "Keypad MemMultiply", - "Keypad MemDivide", - "Keypad +/-", - "Keypad Clear", - "Keypad ClearEntry", - "Keypad Binary", - "Keypad Octal", - "Keypad Decimal", - "Keypad Hexadecimal", - nullptr, - nullptr, - "Left Ctrl", - "Left Shift", - "Left Alt", - "Left GUI", - "Right Ctrl", - "Right Shift", - "Right Alt", - "Right GUI", - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - "ModeSwitch", - "AudioNext", - "AudioPrev", - "AudioStop", - "AudioPlay", - "AudioMute", - "MediaSelect", - "WWW", - "Mail", - "Calculator", - "Computer", - "AC Search", - "AC Home", - "AC Back", - "AC Forward", - "AC Stop", - "AC Refresh", - "AC Bookmarks", - "BrightnessDown", - "BrightnessUp", - "DisplaySwitch", - "KBDIllumToggle", - "KBDIllumDown", - "KBDIllumUp", - "Eject", - "Sleep", - "App1", - "App2", - "AudioRewind", - "AudioFastForward", -}; -#endif // BA_SDL2_BUILD || BA_MINSDL_BUILD - -Input::Input() {} +Input::Input() = default; void Input::PushCreateKeyboardInputDevices() { - g_logic->thread()->PushCall([this] { CreateKeyboardInputDevices(); }); + g_base->logic->event_loop()->PushCall( + [this] { CreateKeyboardInputDevices(); }); } void Input::CreateKeyboardInputDevices() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (keyboard_input_ != nullptr || keyboard_input_2_ != nullptr) { Log(LogLevel::kError, "CreateKeyboardInputDevices called with existing kbs."); @@ -338,11 +42,12 @@ void Input::CreateKeyboardInputDevices() { } void Input::PushDestroyKeyboardInputDevices() { - g_logic->thread()->PushCall([this] { DestroyKeyboardInputDevices(); }); + g_base->logic->event_loop()->PushCall( + [this] { DestroyKeyboardInputDevices(); }); } void Input::DestroyKeyboardInputDevices() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (keyboard_input_ == nullptr || keyboard_input_2_ == nullptr) { Log(LogLevel::kError, "DestroyKeyboardInputDevices called with null kb(s)."); @@ -358,16 +63,16 @@ auto Input::GetInputDevice(int id) -> InputDevice* { if (id < 0 || id >= static_cast(input_devices_.size())) { return nullptr; } - return input_devices_[id].get(); + return input_devices_[id].Get(); } auto Input::GetInputDevice(const std::string& name, const std::string& unique_id) -> InputDevice* { - assert(InLogicThread()); + assert(g_base->InLogicThread()); for (auto&& i : input_devices_) { - if (i.exists() && (i->GetDeviceName() == name) + if (i.Exists() && (i->GetDeviceName() == name) && i->GetPersistentIdentifier() == unique_id) { - return i.get(); + return i.Get(); } } return nullptr; @@ -375,7 +80,7 @@ auto Input::GetInputDevice(const std::string& name, auto Input::GetNewNumberedIdentifier(const std::string& name, const std::string& identifier) -> int { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // Stuff like reserved_identifiers["JoyStickType"]["0x812312314"] = 2; @@ -399,7 +104,7 @@ auto Input::GetNewNumberedIdentifier(const std::string& name, // Scan other devices with the same device-name and find the first number // suffix that's not taken. for (auto&& i : input_devices_) { - if (i.exists()) { + if (i.Exists()) { if ((i->GetRawDeviceName() == name) && i->number() == num) { in_use = true; break; @@ -441,7 +146,7 @@ auto Input::GetNewNumberedIdentifier(const std::string& name, } void Input::CreateTouchInput() { - assert(InMainThread()); + assert(g_core->InMainThread()); assert(touch_input_ == nullptr); touch_input_ = Object::NewDeferred(); PushAddInputDeviceCall(touch_input_, false); @@ -452,7 +157,7 @@ void Input::AnnounceConnects() { // For the first announcement just say "X controllers detected" and don't have // a sound. - if (first_print && GetRealTime() < 10000) { + if (first_print && g_core->GetAppTimeMillisecs() < 10000) { first_print = false; // Disabling this completely for now; being more lenient with devices @@ -462,30 +167,34 @@ void Input::AnnounceConnects() { // If there's been several connected, just give a number. if (explicit_bool(do_print)) { if (newly_connected_controllers_.size() > 1) { - std::string s = g_logic->GetResourceString("controllersDetectedText"); + std::string s = + g_base->assets->GetResourceString("controllersDetectedText"); Utils::StringReplaceOne( &s, "${COUNT}", std::to_string(newly_connected_controllers_.size())); ScreenMessage(s); } else { - ScreenMessage(g_logic->GetResourceString("controllerDetectedText")); + ScreenMessage( + g_base->assets->GetResourceString("controllerDetectedText")); } } } else { // If there's been several connected, just give a number. if (newly_connected_controllers_.size() > 1) { - std::string s = g_logic->GetResourceString("controllersConnectedText"); + std::string s = + g_base->assets->GetResourceString("controllersConnectedText"); Utils::StringReplaceOne( &s, "${COUNT}", std::to_string(newly_connected_controllers_.size())); ScreenMessage(s); } else { // If its just one, name it. - std::string s = g_logic->GetResourceString("controllerConnectedText"); + std::string s = + g_base->assets->GetResourceString("controllerConnectedText"); Utils::StringReplaceOne(&s, "${CONTROLLER}", newly_connected_controllers_.front()); ScreenMessage(s); } - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kGunCock)); + g_base->audio->PlaySound(g_base->assets->SysSound(SysSoundID::kGunCock)); } newly_connected_controllers_.clear(); @@ -494,24 +203,26 @@ void Input::AnnounceConnects() { void Input::AnnounceDisconnects() { // If there's been several connected, just give a number. if (newly_disconnected_controllers_.size() > 1) { - std::string s = g_logic->GetResourceString("controllersDisconnectedText"); + std::string s = + g_base->assets->GetResourceString("controllersDisconnectedText"); Utils::StringReplaceOne( &s, "${COUNT}", std::to_string(newly_disconnected_controllers_.size())); ScreenMessage(s); } else { // If its just one, name it. - std::string s = g_logic->GetResourceString("controllerDisconnectedText"); + std::string s = + g_base->assets->GetResourceString("controllerDisconnectedText"); Utils::StringReplaceOne(&s, "${CONTROLLER}", newly_disconnected_controllers_.front()); ScreenMessage(s); } - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kCorkPop)); + g_base->audio->PlaySound(g_base->assets->SysSound(SysSoundID::kCorkPop)); newly_disconnected_controllers_.clear(); } void Input::ShowStandardInputDeviceConnectedMessage(InputDevice* j) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); std::string suffix; suffix += j->GetPersistentIdentifier(); suffix += j->GetDeviceExtraDescription(); @@ -522,14 +233,14 @@ void Input::ShowStandardInputDeviceConnectedMessage(InputDevice* j) { // Set a timer to go off and announce the accumulated additions. if (connect_print_timer_id_ != 0) { - g_logic->DeleteRealTimer(connect_print_timer_id_); + g_base->logic->DeleteAppTimer(connect_print_timer_id_); } - connect_print_timer_id_ = g_logic->NewRealTimer( + connect_print_timer_id_ = g_base->logic->NewAppTimer( 250, false, NewLambdaRunnable([this] { AnnounceConnects(); })); } void Input::ShowStandardInputDeviceDisconnectedMessage(InputDevice* j) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); newly_disconnected_controllers_.push_back(j->GetDeviceName() + " " + j->GetPersistentIdentifier() @@ -537,38 +248,44 @@ void Input::ShowStandardInputDeviceDisconnectedMessage(InputDevice* j) { // Set a timer to go off and announce the accumulated additions. if (disconnect_print_timer_id_ != 0) { - g_logic->DeleteRealTimer(disconnect_print_timer_id_); + g_base->logic->DeleteAppTimer(disconnect_print_timer_id_); } - disconnect_print_timer_id_ = g_logic->NewRealTimer( + disconnect_print_timer_id_ = g_base->logic->NewAppTimer( 250, false, NewLambdaRunnable([this] { AnnounceDisconnects(); })); } void Input::PushAddInputDeviceCall(InputDevice* input_device, bool standard_message) { - g_logic->thread()->PushCall([this, input_device, standard_message] { + g_base->logic->event_loop()->PushCall([this, input_device, standard_message] { AddInputDevice(input_device, standard_message); }); } -void Input::AddInputDevice(InputDevice* input, bool standard_message) { - assert(InLogicThread()); +void Input::AddInputDevice(InputDevice* device, bool standard_message) { + assert(g_base->InLogicThread()); + + // Let the current app-mode assign it a delegate. + auto delegate = Object::CompleteDeferred( + g_base->app_mode->CreateInputDeviceDelegate(device)); + device->set_delegate(delegate); + delegate->set_input_device(device); // Lets go through and find the first unused input-device id and use that // (might as well keep our list small if we can). int index = 0; bool found_slot = false; for (auto& input_device : input_devices_) { - if (!input_device.exists()) { - input_device = Object::MakeRefCounted(input); + if (!input_device.Exists()) { + input_device = Object::CompleteDeferred(device); found_slot = true; - input->set_index(index); + device->set_index(index); break; } index++; } if (!found_slot) { - input_devices_.push_back(Object::MakeRefCounted(input)); - input->set_index(static_cast(input_devices_.size() - 1)); + input_devices_.push_back(Object::CompleteDeferred(device)); + device->set_index(static_cast(input_devices_.size() - 1)); } // We also want to give this input-device as unique an identifier as @@ -576,16 +293,20 @@ void Input::AddInputDevice(InputDevice* input, bool standard_message) { // or something, but if it doesn't and thus matches an already-existing one, // we tack an index on to it. that way we can at least uniquely address them // based off how many are connected. - input->set_numbered_identifier(GetNewNumberedIdentifier( - input->GetRawDeviceName(), input->GetDeviceIdentifier())); - input->ConnectionComplete(); // Let it do any announcing it wants to. + device->set_number(GetNewNumberedIdentifier(device->GetRawDeviceName(), + device->GetDeviceIdentifier())); + device->ConnectionComplete(); // Let it do any announcing it wants to. - // Update controls for just this guy. - input->UpdateMapping(); + // Immediately apply controls if initial app-config has already been + // applied; otherwise it'll happen as part of that. + if (g_base->logic->applied_app_config()) { + // Update controls for just this guy. + device->UpdateMapping(); - // Need to do this after updating controls, as some control settings can - // affect things we count (such as whether start activates default button). - UpdateInputDeviceCounts(); + // Need to do this after updating controls, as some control settings can + // affect things we count (such as whether start activates default button). + UpdateInputDeviceCounts(); + } if (g_buildconfig.ostype_macos()) { // Special case: on mac, the first time a iOS/Mac controller is connected, @@ -594,62 +315,49 @@ void Input::AddInputDevice(InputDevice* input, bool standard_message) { // support). static bool printed_ios_mac_controller_warning = false; if (!printed_ios_mac_controller_warning && ignore_mfi_controllers_ - && input->IsMFiController()) { + && device->IsMFiController()) { ScreenMessage(R"({"r":"macControllerSubsystemMFiNoteText"})", {1, 1, 0}); printed_ios_mac_controller_warning = true; } } - if (standard_message && !input->ShouldBeHiddenFromUser()) { - ShowStandardInputDeviceConnectedMessage(input); + if (standard_message && !device->ShouldBeHiddenFromUser()) { + ShowStandardInputDeviceConnectedMessage(device); } } void Input::PushRemoveInputDeviceCall(InputDevice* input_device, bool standard_message) { - g_logic->thread()->PushCall([this, input_device, standard_message] { + g_base->logic->event_loop()->PushCall([this, input_device, standard_message] { RemoveInputDevice(input_device, standard_message); }); } void Input::RemoveInputDevice(InputDevice* input, bool standard_message) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (standard_message && !input->ShouldBeHiddenFromUser()) { ShowStandardInputDeviceDisconnectedMessage(input); } // Just look for it in our list.. if we find it, simply clear the ref - // (we need to keep the pointer around so our list indices don't change). + // (we need to keep the ref around so our list indices don't change). for (auto& input_device : input_devices_) { - if (input_device.exists() && (input_device.get() == input)) { - // Pull it off the list before killing it (in case it triggers another - // kill itself). + if (input_device.Exists() && (input_device.Get() == input)) { + // Pull it off the list before killing it (in case it tries to trigger + // another kill itself). Object::Ref device = input_device; // Ok we cleared its slot in our vector; now we just have // the local variable 'device' keeping it alive. input_device.Clear(); - // If we're attached to a local or remote player, kill the player. - if (input->attached_to_player()) { - if (input->GetPlayer() != nullptr) { - // NOTE: we now remove the player instantly instead of pushing - // a call to do it; otherwise its possible that someone tries - // to access the player's inputdevice before the call goes - // through which would lead to an exception. - g_logic->RemovePlayer(input->GetPlayer()); - // g_logic->PushRemovePlayerCall(input->GetPlayer()); - } - if (input->GetRemotePlayer() != nullptr) { - input->RemoveRemotePlayerFromGame(); - } - device->DetachFromPlayer(); - } + // Tell it to detach from anything it is controlling. + device->DetachFromPlayer(); // This should kill the device. // FIXME: since many devices get allocated in the main thread, - // should we not kill it there too?... + // should we not kill it there too?... device.Clear(); UpdateInputDeviceCounts(); return; @@ -659,8 +367,10 @@ void Input::RemoveInputDevice(InputDevice* input, bool standard_message) { } void Input::UpdateInputDeviceCounts() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); + auto current_time_millisecs = + static_cast(g_base->logic->display_time() * 1000.0); have_button_using_inputs_ = false; have_start_activated_default_button_inputs_ = false; have_non_touch_inputs_ = false; @@ -671,10 +381,11 @@ void Input::UpdateInputDeviceCounts() { // been active recently.. (we're starting to get lots of virtual devices and // other cruft on android; don't wanna show controller UIs just due to // those) - if (input_device.exists() + if (input_device.Exists() && ((*input_device).IsTouchScreen() || (*input_device).IsKeyboard() - || ((*input_device).last_input_time() != 0 - && g_logic->master_time() - (*input_device).last_input_time() + || ((*input_device).last_input_time_millisecs() != 0 + && current_time_millisecs + - (*input_device).last_input_time_millisecs() < 60000))) { total++; if (!(*input_device).IsTouchScreen()) { @@ -694,30 +405,36 @@ void Input::UpdateInputDeviceCounts() { if (controller_count > max_controller_count_so_far_) { max_controller_count_so_far_ = controller_count; if (max_controller_count_so_far_ == 1) { - g_python->PushObjCall(Python::ObjID::kAwardInControlAchievementCall); + g_base->python->objs().PushCall( + BasePython::ObjID::kAwardInControlAchievementCall); } else if (max_controller_count_so_far_ == 2) { - g_python->PushObjCall(Python::ObjID::kAwardDualWieldingAchievementCall); + g_base->python->objs().PushCall( + BasePython::ObjID::kAwardDualWieldingAchievementCall); } } } auto Input::GetLocalActiveInputDeviceCount() -> int { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // This can get called alot so lets cache the value. - millisecs_t current_time = g_logic->master_time(); - if (current_time != last_get_local_active_input_device_count_check_time_) { - last_get_local_active_input_device_count_check_time_ = current_time; + auto current_time_millisecs = + static_cast(g_base->logic->display_time() * 1000.0); + if (current_time_millisecs + != last_get_local_active_input_device_count_check_time_) { + last_get_local_active_input_device_count_check_time_ = + current_time_millisecs; int count = 0; for (auto& input_device : input_devices_) { // Tally up local non-keyboard, non-touchscreen devices that have been // used in the last minute. - if (input_device.exists() && !input_device->IsKeyboard() + if (input_device.Exists() && !input_device->IsKeyboard() && !input_device->IsTouchScreen() && !input_device->IsUIOnly() && input_device->IsLocal() - && (input_device->last_input_time() != 0 - && g_logic->master_time() - input_device->last_input_time() + && (input_device->last_input_time_millisecs() != 0 + && current_time_millisecs + - input_device->last_input_time_millisecs() < 60000)) { count++; } @@ -728,11 +445,11 @@ auto Input::GetLocalActiveInputDeviceCount() -> int { } auto Input::HaveControllerWithPlayer() -> bool { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // NOLINTNEXTLINE(readability-use-anyofallof) for (auto& input_device : input_devices_) { - if (input_device.exists() && (*input_device).IsController() - && (*input_device).attached_to_player()) { + if (input_device.Exists() && (*input_device).IsController() + && (*input_device).AttachedToPlayer()) { return true; } } @@ -740,10 +457,10 @@ auto Input::HaveControllerWithPlayer() -> bool { } auto Input::HaveRemoteAppController() -> bool { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // NOLINTNEXTLINE(readability-use-anyofallof) for (auto& input_device : input_devices_) { - if (input_device.exists() && (*input_device).IsRemoteApp()) { + if (input_device.Exists() && (*input_device).IsRemoteApp()) { return true; } } @@ -753,10 +470,10 @@ auto Input::HaveRemoteAppController() -> bool { auto Input::GetInputDevicesWithName(const std::string& name) -> std::vector { std::vector vals; - if (!HeadlessMode()) { + if (!g_core->HeadlessMode()) { for (auto& input_device : input_devices_) { - if (input_device.exists()) { - auto* js = dynamic_cast(input_device.get()); + if (input_device.Exists()) { + auto* js = dynamic_cast(input_device.Get()); if (js && js->GetDeviceName() == name) { vals.push_back(js); } @@ -767,12 +484,12 @@ auto Input::GetInputDevicesWithName(const std::string& name) } auto Input::GetConfigurableGamePads() -> std::vector { - assert(InLogicThread()); + assert(g_base->InLogicThread()); std::vector vals; - if (!HeadlessMode()) { + if (!g_core->HeadlessMode()) { for (auto& input_device : input_devices_) { - if (input_device.exists()) { - auto* js = dynamic_cast(input_device.get()); + if (input_device.Exists()) { + auto* js = dynamic_cast(input_device.Get()); if (js && js->GetAllowsConfiguring() && !js->ShouldBeHiddenFromUser()) { vals.push_back(js); } @@ -792,18 +509,14 @@ auto Input::ShouldCompletelyIgnoreInputDevice(InputDevice* input_device) return ignore_sdl_controllers_ && input_device->IsSDLController(); } -// auto Input::GetIdleTime() const -> millisecs_t { -// return GetRealTime() - last_input_time_; -// } - void Input::UpdateEnabledControllerSubsystems() { - assert(IsBootstrapped()); + assert(g_base); // First off, on mac, let's update whether we want to completely ignore either // the classic or the iOS/Mac controller subsystems. if (g_buildconfig.ostype_macos()) { - std::string sys = - g_app_config->Resolve(AppConfig::StringID::kMacControllerSubsystem); + std::string sys = g_base->app_config->Resolve( + AppConfig::StringID::kMacControllerSubsystem); if (sys == "Classic") { ignore_mfi_controllers_ = true; ignore_sdl_controllers_ = false; @@ -820,9 +533,17 @@ void Input::UpdateEnabledControllerSubsystems() { } } +void Input::OnAppStart() { assert(g_base->InLogicThread()); } + +void Input::OnAppPause() { assert(g_base->InLogicThread()); } + +void Input::OnAppResume() { assert(g_base->InLogicThread()); } + +void Input::OnAppShutdown() { assert(g_base->InLogicThread()); } + // Tells all inputs to update their controls based on the app config. void Input::ApplyAppConfig() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); UpdateEnabledControllerSubsystems(); @@ -831,16 +552,21 @@ void Input::ApplyAppConfig() { // it. std::vector > input_devices = input_devices_; for (auto& input_device : input_devices) { - if (input_device.exists()) { + if (input_device.Exists()) { input_device->UpdateMapping(); } } + + // Some config settings can affect this. + UpdateInputDeviceCounts(); } -void Input::Update() { - assert(InLogicThread()); +void Input::OnScreenSizeChange() { assert(g_base->InLogicThread()); } - millisecs_t real_time = GetRealTime(); +void Input::StepDisplayTime() { + assert(g_base->InLogicThread()); + + millisecs_t real_time = g_core->GetAppTimeMillisecs(); // If input has been locked an excessively long amount of time, unlock it. if (input_lock_count_temp_) { @@ -876,37 +602,44 @@ void Input::Update() { } for (auto& input_device : input_devices_) { - if (input_device.exists()) { + if (input_device.Exists()) { (*input_device).Update(); } } } void Input::Reset() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // Detach all inputs from players. for (auto& input_device : input_devices_) { - if (input_device.exists()) { + if (input_device.Exists()) { input_device->DetachFromPlayer(); } } } +void Input::ResetHoldStates() { + assert(g_base->InLogicThread()); + ResetKeyboardHeldKeys(); + ResetJoyStickHeldButtons(); +} + void Input::LockAllInput(bool permanent, const std::string& label) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (permanent) { input_lock_count_permanent_++; input_lock_permanent_labels_.push_back(label); } else { input_lock_count_temp_++; if (input_lock_count_temp_ == 1) { - last_input_temp_lock_time_ = GetRealTime(); + last_input_temp_lock_time_ = g_core->GetAppTimeMillisecs(); } input_lock_temp_labels_.push_back(label); - recent_input_locks_unlocks_.push_back("temp lock: " + label + " time " - + std::to_string(GetRealTime())); + recent_input_locks_unlocks_.push_back( + "temp lock: " + label + " time " + + std::to_string(g_core->GetAppTimeMillisecs())); while (recent_input_locks_unlocks_.size() > 10) { recent_input_locks_unlocks_.pop_front(); } @@ -914,12 +647,12 @@ void Input::LockAllInput(bool permanent, const std::string& label) { } void Input::UnlockAllInput(bool permanent, const std::string& label) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); recent_input_locks_unlocks_.push_back( - permanent - ? "permanent unlock: " - : "temp unlock: " + label + " time " + std::to_string(GetRealTime())); + permanent ? "permanent unlock: " + : "temp unlock: " + label + " time " + + std::to_string(g_core->GetAppTimeMillisecs())); while (recent_input_locks_unlocks_.size() > 10) recent_input_locks_unlocks_.pop_front(); @@ -942,9 +675,10 @@ void Input::UnlockAllInput(bool permanent, const std::string& label) { input_lock_count_temp_--; input_unlock_temp_labels_.push_back(label); if (input_lock_count_temp_ < 0) { - Log(LogLevel::kWarning, "temp input unlock at time " - + std::to_string(GetRealTime()) - + " with no active lock: '" + label + "'"); + Log(LogLevel::kWarning, + "temp input unlock at time " + + std::to_string(g_core->GetAppTimeMillisecs()) + + " with no active lock: '" + label + "'"); // This is to be expected since we can reset this to 0. input_lock_count_temp_ = 0; } @@ -959,8 +693,8 @@ void Input::UnlockAllInput(bool permanent, const std::string& label) { } void Input::PrintLockLabels() { - std::string s = - "INPUT LOCK REPORT (time=" + std::to_string(GetRealTime()) + "):"; + std::string s = "INPUT LOCK REPORT (time=" + + std::to_string(g_core->GetAppTimeMillisecs()) + "):"; int num; s += "\n " + std::to_string(input_lock_temp_labels_.size()) + " TEMP LOCKS:"; @@ -1000,10 +734,10 @@ void Input::PrintLockLabels() { } void Input::ProcessStressTesting(int player_count) { - assert(InMainThread()); + assert(g_core->InMainThread()); assert(player_count >= 0); - millisecs_t time = GetRealTime(); + millisecs_t time = g_core->GetAppTimeMillisecs(); // FIXME: If we don't check for stress_test_last_leave_time_ we totally // confuse the game.. need to be able to survive that. @@ -1051,31 +785,32 @@ void Input::ProcessStressTesting(int player_count) { } void Input::PushTextInputEvent(const std::string& text) { - g_logic->thread()->PushCall([this, text] { + g_base->logic->event_loop()->PushCall([this, text] { mark_input_active(); // Ignore if input is locked. if (IsInputLocked()) { return; } - if (g_app->console != nullptr && g_app->console->HandleTextEditing(text)) { + if (g_base && g_base->console() != nullptr + && g_base->console()->HandleTextEditing(text)) { return; } - g_ui->SendWidgetMessage(WidgetMessage(WidgetMessage::Type::kTextInput, - nullptr, 0, 0, 0, 0, text.c_str())); + g_base->ui->SendWidgetMessage(WidgetMessage( + WidgetMessage::Type::kTextInput, nullptr, 0, 0, 0, 0, text.c_str())); }); } -auto Input::PushJoystickEvent(const SDL_Event& event, InputDevice* input_device) - -> void { - g_logic->thread()->PushCall([this, event, input_device] { +void Input::PushJoystickEvent(const SDL_Event& event, + InputDevice* input_device) { + g_base->logic->event_loop()->PushCall([this, event, input_device] { HandleJoystickEvent(event, input_device); }); } void Input::HandleJoystickEvent(const SDL_Event& event, InputDevice* input_device) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); assert(input_device); if (ShouldCompletelyIgnoreInputDevice(input_device)) { @@ -1091,24 +826,57 @@ void Input::HandleJoystickEvent(const SDL_Event& event, // And that this particular device isn't idle either. input_device->UpdateLastInputTime(); - // Give Python a crack at it for captures, etc. - if (g_python->HandleJoystickEvent(event, input_device)) { - return; + // If someone is capturing these events, give them a crack at it. + if (joystick_input_capture_) { + if (joystick_input_capture_(event, input_device)) { + return; + } } input_device->HandleSDLEvent(&event); } void Input::PushKeyPressEvent(const SDL_Keysym& keysym) { - g_logic->thread()->PushCall([this, keysym] { HandleKeyPress(&keysym); }); + g_base->logic->event_loop()->PushCall( + [this, keysym] { HandleKeyPress(&keysym); }); } void Input::PushKeyReleaseEvent(const SDL_Keysym& keysym) { - g_logic->thread()->PushCall([this, keysym] { HandleKeyRelease(&keysym); }); + g_base->logic->event_loop()->PushCall( + [this, keysym] { HandleKeyRelease(&keysym); }); +} + +void Input::CaptureKeyboardInput(HandleKeyPressCall* press_call, + HandleKeyReleaseCall* release_call) { + assert(g_base->InLogicThread()); + if (keyboard_input_capture_press_ || keyboard_input_capture_release_) { + Log(LogLevel::kError, "Setting key capture redundantly."); + } + keyboard_input_capture_press_ = press_call; + keyboard_input_capture_release_ = release_call; +} + +void Input::ReleaseKeyboardInput() { + assert(g_base->InLogicThread()); + keyboard_input_capture_press_ = nullptr; + keyboard_input_capture_release_ = nullptr; +} + +void Input::CaptureJoystickInput(HandleJoystickEventCall* call) { + assert(g_base->InLogicThread()); + if (joystick_input_capture_) { + Log(LogLevel::kError, "Setting joystick capture redundantly."); + } + joystick_input_capture_ = call; +} + +void Input::ReleaseJoystickInput() { + assert(g_base->InLogicThread()); + joystick_input_capture_ = nullptr; } void Input::HandleKeyPress(const SDL_Keysym* keysym) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); mark_input_active(); @@ -1117,9 +885,11 @@ void Input::HandleKeyPress(const SDL_Keysym* keysym) { return; } - // Give Python a crack at it for captures, etc. - if (g_python->HandleKeyPressEvent(*keysym)) { - return; + // If someone is capturing these events, give them a crack at it. + if (keyboard_input_capture_press_) { + if (keyboard_input_capture_press_(*keysym)) { + return; + } } // Regardless of what else we do, keep track of mod key states. @@ -1148,7 +918,7 @@ void Input::HandleKeyPress(const SDL_Keysym* keysym) { // have hardware keyboards it crashes text fields by sending them a // TEXT_INPUT message with no string.. I made them resistant to that // case but wondering if we can take this out?... - g_ui->SendWidgetMessage( + g_base->ui->SendWidgetMessage( WidgetMessage(WidgetMessage::Type::kTextInput, keysym)); break; } @@ -1162,20 +932,23 @@ void Input::HandleKeyPress(const SDL_Keysym* keysym) { // Command-F or Control-F toggles full-screen. if (!repeat_press && keysym->sym == SDLK_f && ((keysym->mod & KMOD_CTRL) || (keysym->mod & KMOD_GUI))) { // NOLINT - g_python->obj(Python::ObjID::kToggleFullscreenCall).Call(); + g_base->python->objs() + .Get(BasePython::ObjID::kToggleFullscreenCall) + .Call(); return; } // Command-Q or Control-Q quits. if (!repeat_press && keysym->sym == SDLK_q && ((keysym->mod & KMOD_CTRL) || (keysym->mod & KMOD_GUI))) { // NOLINT - g_logic->PushConfirmQuitCall(); + g_base->ui->ConfirmQuit(); return; } } // Let the console intercept stuff if it wants at this point. - if (g_app->console != nullptr && g_app->console->HandleKeyPress(keysym)) { + if (g_base && g_base->console() != nullptr + && g_base->console()->HandleKeyPress(keysym)) { return; } @@ -1183,7 +956,7 @@ void Input::HandleKeyPress(const SDL_Keysym* keysym) { // 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)); + g_base->ui->SendWidgetMessage(WidgetMessage(WidgetMessage::Type::kPaste)); return; } @@ -1194,11 +967,11 @@ void Input::HandleKeyPress(const SDL_Keysym* keysym) { switch (keysym->sym) { // Menu button on android/etc. pops up the menu. case SDLK_MENU: { - if (g_ui && g_ui->screen_root_widget()) { + if (g_base && g_base->ui->screen_root_widget()) { // If there's no dialogs/windows up, ask for a menu (owned by the // touch-screen if available). - if (g_ui->screen_root_widget()->GetChildCount() == 0) { - g_ui->PushMainMenuPressCall(touch_input_); + if (g_base->ui->screen_root_widget()->GetChildCount() == 0) { + g_base->ui->PushMainMenuPressCall(touch_input_); } } handled = true; @@ -1207,55 +980,59 @@ void Input::HandleKeyPress(const SDL_Keysym* keysym) { case SDLK_EQUALS: case SDLK_PLUS: - g_logic->ChangeGameSpeed(1); + g_base->app_mode->ChangeGameSpeed(1); handled = true; break; case SDLK_MINUS: - g_logic->ChangeGameSpeed(-1); + g_base->app_mode->ChangeGameSpeed(-1); handled = true; break; case SDLK_F5: { - g_ui->root_ui()->TogglePartyWindowKeyPress(); + g_base->ui->root_ui()->TogglePartyWindowKeyPress(); handled = true; break; } case SDLK_F7: - g_logic->PushToggleManualCameraCall(); + g_base->logic->event_loop()->PushCall( + [] { g_base->graphics->ToggleManualCamera(); }); handled = true; break; case SDLK_F8: - g_logic->PushToggleDebugInfoDisplayCall(); + g_base->logic->event_loop()->PushCall( + [] { g_base->graphics->ToggleNetworkDebugDisplay(); }); handled = true; break; case SDLK_F9: - g_python->PushObjCall(Python::ObjID::kLanguageTestToggleCall); + g_base->python->objs().PushCall( + BasePython::ObjID::kLanguageTestToggleCall); handled = true; break; case SDLK_F10: - g_logic->PushToggleCollisionGeometryDisplayCall(); + g_base->logic->event_loop()->PushCall( + [] { g_base->graphics->ToggleDebugDraw(); }); handled = true; break; case SDLK_ESCAPE: - if (g_ui && g_ui->screen_root_widget() && g_ui->root_widget() - && g_ui->overlay_root_widget()) { + if (g_base && g_base->ui->screen_root_widget() + && g_base->ui->root_widget() && g_base->ui->overlay_root_widget()) { // If there's no dialogs/windows up, ask for a menu owned by the // keyboard. - if (g_ui->screen_root_widget()->GetChildCount() == 0 - && g_ui->overlay_root_widget()->GetChildCount() == 0) { + if (g_base->ui->screen_root_widget()->GetChildCount() == 0 + && g_base->ui->overlay_root_widget()->GetChildCount() == 0) { if (keyboard_input_) { - g_ui->PushMainMenuPressCall(keyboard_input_); + g_base->ui->PushMainMenuPressCall(keyboard_input_); } } else { // Ok there's a UI up.. send along a cancel message. - g_ui->root_widget()->HandleMessage( + g_base->ui->root_widget()->HandleMessage( WidgetMessage(WidgetMessage::Type::kCancel)); } } @@ -1276,15 +1053,17 @@ void Input::HandleKeyPress(const SDL_Keysym* keysym) { } void Input::HandleKeyRelease(const SDL_Keysym* keysym) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // Note: we want to let these through even if input is locked. mark_input_active(); - // Give Python a crack at it for captures, etc. - if (g_python->HandleKeyReleaseEvent(*keysym)) { - return; + // If someone is capturing these events, give them a crack at it. + if (keyboard_input_capture_release_) { + if (keyboard_input_capture_release_(*keysym)) { + return; + } } // Regardless of what else we do, keep track of mod key states. @@ -1307,7 +1086,8 @@ void Input::HandleKeyRelease(const SDL_Keysym* keysym) { bool handled = false; - if (g_app->console != nullptr && g_app->console->HandleKeyRelease(keysym)) { + if (g_base && g_base->console() != nullptr + && g_base->console()->HandleKeyRelease(keysym)) { handled = true; } @@ -1319,25 +1099,25 @@ void Input::HandleKeyRelease(const SDL_Keysym* keysym) { } } -auto Input::UpdateModKeyStates(const SDL_Keysym* keysym, bool press) -> void { +void Input::UpdateModKeyStates(const SDL_Keysym* keysym, bool press) { switch (keysym->sym) { case SDLK_LCTRL: case SDLK_RCTRL: { - if (Camera* c = g_graphics->camera()) { + if (Camera* c = g_base->graphics->camera()) { c->set_ctrl_down(press); } break; } case SDLK_LALT: case SDLK_RALT: { - if (Camera* c = g_graphics->camera()) { + if (Camera* c = g_base->graphics->camera()) { c->set_alt_down(press); } break; } case SDLK_LGUI: case SDLK_RGUI: { - if (Camera* c = g_graphics->camera()) { + if (Camera* c = g_base->graphics->camera()) { c->set_cmd_down(press); } break; @@ -1347,18 +1127,19 @@ auto Input::UpdateModKeyStates(const SDL_Keysym* keysym, bool press) -> void { } } -auto Input::PushMouseScrollEvent(const Vector2f& amount) -> void { - g_logic->thread()->PushCall([this, amount] { HandleMouseScroll(amount); }); +void Input::PushMouseScrollEvent(const Vector2f& amount) { + g_base->logic->event_loop()->PushCall( + [this, amount] { HandleMouseScroll(amount); }); } -auto Input::HandleMouseScroll(const Vector2f& amount) -> void { - assert(InLogicThread()); +void Input::HandleMouseScroll(const Vector2f& amount) { + assert(g_base->InLogicThread()); if (IsInputLocked()) { return; } mark_input_active(); - Widget* root_widget = g_ui->root_widget(); + ui_v1::Widget* root_widget = g_base->ui->root_widget(); if (std::abs(amount.y) > 0.0001f && root_widget) { root_widget->HandleMessage(WidgetMessage(WidgetMessage::Type::kMouseWheel, nullptr, cursor_pos_x_, @@ -1371,7 +1152,7 @@ auto Input::HandleMouseScroll(const Vector2f& amount) -> void { } mouse_move_count_++; - Camera* camera = g_graphics->camera(); + Camera* camera = g_base->graphics->camera(); if (camera) { if (camera->manual()) { camera->ManualHandleMouseWheel(0.005f * amount.y); @@ -1379,23 +1160,22 @@ auto Input::HandleMouseScroll(const Vector2f& amount) -> void { } } -auto Input::PushSmoothMouseScrollEvent(const Vector2f& velocity, bool momentum) - -> void { - g_logic->thread()->PushCall([this, velocity, momentum] { +void Input::PushSmoothMouseScrollEvent(const Vector2f& velocity, + bool momentum) { + g_base->logic->event_loop()->PushCall([this, velocity, momentum] { HandleSmoothMouseScroll(velocity, momentum); }); } -auto Input::HandleSmoothMouseScroll(const Vector2f& velocity, bool momentum) - -> void { - assert(InLogicThread()); +void Input::HandleSmoothMouseScroll(const Vector2f& velocity, bool momentum) { + assert(g_base->InLogicThread()); if (IsInputLocked()) { return; } mark_input_active(); bool handled = false; - Widget* root_widget = g_ui->root_widget(); + ui_v1::Widget* root_widget = g_base->ui->root_widget(); if (root_widget) { handled = root_widget->HandleMessage( WidgetMessage(WidgetMessage::Type::kMouseWheelVelocity, nullptr, @@ -1404,10 +1184,10 @@ auto Input::HandleSmoothMouseScroll(const Vector2f& velocity, bool momentum) WidgetMessage(WidgetMessage::Type::kMouseWheelVelocityH, nullptr, cursor_pos_x_, cursor_pos_y_, velocity.x, momentum)); } - last_mouse_move_time_ = GetRealTime(); + last_mouse_move_time_ = g_core->GetAppTimeMillisecs(); mouse_move_count_++; - Camera* camera = g_graphics->camera(); + Camera* camera = g_base->graphics->camera(); if (!handled && camera) { if (camera->manual()) { camera->ManualHandleMouseWheel(-0.25f * velocity.y); @@ -1415,26 +1195,26 @@ auto Input::HandleSmoothMouseScroll(const Vector2f& velocity, bool momentum) } } -auto Input::PushMouseMotionEvent(const Vector2f& position) -> void { - g_logic->thread()->PushCall( +void Input::PushMouseMotionEvent(const Vector2f& position) { + g_base->logic->event_loop()->PushCall( [this, position] { HandleMouseMotion(position); }); } -auto Input::HandleMouseMotion(const Vector2f& position) -> void { - assert(g_graphics); - assert(InLogicThread()); +void Input::HandleMouseMotion(const Vector2f& position) { + assert(g_base->graphics); + assert(g_base->InLogicThread()); mark_input_active(); float old_cursor_pos_x = cursor_pos_x_; float old_cursor_pos_y = cursor_pos_y_; // Convert normalized view coords to our virtual ones. - cursor_pos_x_ = g_graphics->PixelToVirtualX( - position.x * g_graphics->screen_pixel_width()); - cursor_pos_y_ = g_graphics->PixelToVirtualY( - position.y * g_graphics->screen_pixel_height()); + cursor_pos_x_ = g_base->graphics->PixelToVirtualX( + position.x * g_base->graphics->screen_pixel_width()); + cursor_pos_y_ = g_base->graphics->PixelToVirtualY( + position.y * g_base->graphics->screen_pixel_height()); - last_mouse_move_time_ = GetRealTime(); + last_mouse_move_time_ = g_core->GetAppTimeMillisecs(); mouse_move_count_++; bool handled2{}; @@ -1448,61 +1228,61 @@ auto Input::HandleMouseMotion(const Vector2f& position) -> void { } // UI interaction. - Widget* root_widget = g_ui->root_widget(); + ui_v1::Widget* root_widget = g_base->ui->root_widget(); if (root_widget && !IsInputLocked()) handled2 = root_widget->HandleMessage( WidgetMessage(WidgetMessage::Type::kMouseMove, nullptr, cursor_pos_x_, cursor_pos_y_)); // Manual camera motion. - Camera* camera = g_graphics->camera(); + Camera* camera = g_base->graphics->camera(); if (!handled2 && camera && camera->manual()) { - float move_h = - (cursor_pos_x_ - old_cursor_pos_x) / g_graphics->screen_virtual_width(); - float move_v = - (cursor_pos_y_ - old_cursor_pos_y) / g_graphics->screen_virtual_width(); + float move_h = (cursor_pos_x_ - old_cursor_pos_x) + / g_base->graphics->screen_virtual_width(); + float move_v = (cursor_pos_y_ - old_cursor_pos_y) + / g_base->graphics->screen_virtual_width(); camera->ManualHandleMouseMove(move_h, move_v); } - g_ui->root_ui()->HandleMouseMotion(cursor_pos_x_, cursor_pos_y_); + g_base->ui->root_ui()->HandleMouseMotion(cursor_pos_x_, cursor_pos_y_); } -auto Input::PushMouseDownEvent(int button, const Vector2f& position) -> void { - g_logic->thread()->PushCall( +void Input::PushMouseDownEvent(int button, const Vector2f& position) { + g_base->logic->event_loop()->PushCall( [this, button, position] { HandleMouseDown(button, position); }); } -auto Input::HandleMouseDown(int button, const Vector2f& position) -> void { - assert(g_graphics); - assert(InLogicThread()); +void Input::HandleMouseDown(int button, const Vector2f& position) { + assert(g_base->graphics); + assert(g_base->InLogicThread()); if (IsInputLocked()) { return; } - if (g_ui == nullptr || g_ui->screen_root_widget() == nullptr) { + if (g_base == nullptr || g_base->ui->screen_root_widget() == nullptr) { return; } mark_input_active(); - last_mouse_move_time_ = GetRealTime(); + last_mouse_move_time_ = g_core->GetAppTimeMillisecs(); mouse_move_count_++; // printf("Mouse down at %f %f\n", position.x, position.y); // Convert normalized view coords to our virtual ones. - cursor_pos_x_ = g_graphics->PixelToVirtualX( - position.x * g_graphics->screen_pixel_width()); - cursor_pos_y_ = g_graphics->PixelToVirtualY( - position.y * g_graphics->screen_pixel_height()); + cursor_pos_x_ = g_base->graphics->PixelToVirtualX( + position.x * g_base->graphics->screen_pixel_width()); + cursor_pos_y_ = g_base->graphics->PixelToVirtualY( + position.y * g_base->graphics->screen_pixel_height()); - millisecs_t click_time = GetRealTime(); + millisecs_t click_time = g_core->GetAppTimeMillisecs(); bool double_click = (click_time - last_click_time_ <= double_click_time_); last_click_time_ = click_time; bool handled2 = false; - Widget* root_widget = g_ui->root_widget(); + ui_v1::Widget* root_widget = g_base->ui->root_widget(); // If we have a touch-input in editing mode, pass along events to it. // (it usually handles its own events but here we want it to play nice @@ -1513,7 +1293,8 @@ auto Input::HandleMouseDown(int button, const Vector2f& position) -> void { } if (!handled2) { - if (g_ui->root_ui()->HandleMouseButtonDown(cursor_pos_x_, cursor_pos_y_)) { + if (g_base->ui->root_ui()->HandleMouseButtonDown(cursor_pos_x_, + cursor_pos_y_)) { handled2 = true; } } @@ -1525,7 +1306,7 @@ auto Input::HandleMouseDown(int button, const Vector2f& position) -> void { } // Manual camera input. - Camera* camera = g_graphics->camera(); + Camera* camera = g_base->graphics->camera(); if (!handled2 && camera) { switch (button) { case SDL_BUTTON_LEFT: @@ -1544,20 +1325,20 @@ auto Input::HandleMouseDown(int button, const Vector2f& position) -> void { } } -auto Input::PushMouseUpEvent(int button, const Vector2f& position) -> void { - g_logic->thread()->PushCall( +void Input::PushMouseUpEvent(int button, const Vector2f& position) { + g_base->logic->event_loop()->PushCall( [this, button, position] { HandleMouseUp(button, position); }); } -auto Input::HandleMouseUp(int button, const Vector2f& position) -> void { - assert(InLogicThread()); +void Input::HandleMouseUp(int button, const Vector2f& position) { + assert(g_base->InLogicThread()); mark_input_active(); // Convert normalized view coords to our virtual ones. - cursor_pos_x_ = g_graphics->PixelToVirtualX( - position.x * g_graphics->screen_pixel_width()); - cursor_pos_y_ = g_graphics->PixelToVirtualY( - position.y * g_graphics->screen_pixel_height()); + cursor_pos_x_ = g_base->graphics->PixelToVirtualX( + position.x * g_base->graphics->screen_pixel_width()); + cursor_pos_y_ = g_base->graphics->PixelToVirtualY( + position.y * g_base->graphics->screen_pixel_height()); bool handled2{}; @@ -1569,11 +1350,11 @@ auto Input::HandleMouseUp(int button, const Vector2f& position) -> void { cursor_pos_y_); } - Widget* root_widget = g_ui->root_widget(); + ui_v1::Widget* root_widget = g_base->ui->root_widget(); if (root_widget) handled2 = root_widget->HandleMessage(WidgetMessage( WidgetMessage::Type::kMouseUp, nullptr, cursor_pos_x_, cursor_pos_y_)); - Camera* camera = g_graphics->camera(); + Camera* camera = g_base->graphics->camera(); if (!handled2 && camera) { switch (button) { case SDL_BUTTON_LEFT: @@ -1590,16 +1371,16 @@ auto Input::HandleMouseUp(int button, const Vector2f& position) -> void { } camera->UpdateManualMode(); } - g_ui->root_ui()->HandleMouseButtonUp(cursor_pos_x_, cursor_pos_y_); + g_base->ui->root_ui()->HandleMouseButtonUp(cursor_pos_x_, cursor_pos_y_); } void Input::PushTouchEvent(const TouchEvent& e) { - g_logic->thread()->PushCall([e, this] { HandleTouchEvent(e); }); + g_base->logic->event_loop()->PushCall([e, this] { HandleTouchEvent(e); }); } void Input::HandleTouchEvent(const TouchEvent& e) { - assert(InLogicThread()); - assert(g_graphics); + assert(g_base->InLogicThread()); + assert(g_base->graphics); if (IsInputLocked()) { return; @@ -1614,9 +1395,10 @@ void Input::HandleTouchEvent(const TouchEvent& e) { printf("FIXME: update touch handling\n"); } - float x = g_graphics->PixelToVirtualX(e.x * g_graphics->screen_pixel_width()); - float y = - g_graphics->PixelToVirtualY(e.y * g_graphics->screen_pixel_height()); + float x = g_base->graphics->PixelToVirtualX( + e.x * g_base->graphics->screen_pixel_width()); + float y = g_base->graphics->PixelToVirtualY( + e.y * g_base->graphics->screen_pixel_height()); if (e.overall) { // Sanity test: if the OS tells us that this is the beginning of an, @@ -1664,7 +1446,7 @@ void Input::HandleTouchEvent(const TouchEvent& e) { void Input::ResetJoyStickHeldButtons() { for (auto&& i : input_devices_) { - if (i.exists()) { + if (i.Exists()) { i->ResetHeldStates(); } } @@ -1672,8 +1454,8 @@ void Input::ResetJoyStickHeldButtons() { // Send key-ups for any currently-held keys. void Input::ResetKeyboardHeldKeys() { - assert(InLogicThread()); - if (!HeadlessMode()) { + assert(g_base->InLogicThread()); + if (!g_core->HeadlessMode()) { // Synthesize key-ups for all our held keys. while (!keys_held_.empty()) { SDL_Keysym k; @@ -1692,11 +1474,11 @@ void Input::Draw(FrameDef* frame_def) { } auto Input::IsCursorVisible() const -> bool { - assert(InLogicThread()); - if (!g_ui) { + assert(g_base->InLogicThread()); + if (!g_base->ui) { return false; } - ContainerWidget* screen_root_widget = g_ui->screen_root_widget(); + ui_v1::ContainerWidget* screen_root_widget = g_base->ui->screen_root_widget(); // Keeps mouse hidden to start with.. if (mouse_move_count_ < 2) { @@ -1707,119 +1489,15 @@ auto Input::IsCursorVisible() const -> bool { // Show our cursor if any dialogs/windows are up or else if its been // moved very recently. if (screen_root_widget && screen_root_widget->GetChildCount() > 0) { - val = (GetRealTime() - last_mouse_move_time_ < 5000); + val = (g_core->GetAppTimeMillisecs() - last_mouse_move_time_ < 5000); } else { - val = (GetRealTime() - last_mouse_move_time_ < 1000); + val = (g_core->GetAppTimeMillisecs() - last_mouse_move_time_ < 1000); } return val; } -// The following was pulled from sdl2 -#if BA_SDL2_BUILD || BA_MINSDL_BUILD - -#pragma clang diagnostic push -#pragma ide diagnostic ignored "hicpp-signed-bitwise" - -static char* UCS4ToUTF8(uint32_t ch, char* dst) { - auto* p = reinterpret_cast(dst); - if (ch <= 0x7F) { - *p = static_cast(ch); - ++dst; - } else if (ch <= 0x7FF) { - p[0] = static_cast(0xC0 | static_cast((ch >> 6) & 0x1F)); - p[1] = static_cast(0x80 | static_cast(ch & 0x3F)); - dst += 2; - } else if (ch <= 0xFFFF) { - p[0] = static_cast(0xE0 | static_cast((ch >> 12) & 0x0F)); - p[1] = static_cast(0x80 | static_cast((ch >> 6) & 0x3F)); - p[2] = static_cast(0x80 | static_cast(ch & 0x3F)); - dst += 3; - } else if (ch <= 0x1FFFFF) { - p[0] = static_cast(0xF0 | static_cast((ch >> 18) & 0x07)); - p[1] = static_cast(0x80 | static_cast((ch >> 12) & 0x3F)); - p[2] = static_cast(0x80 | static_cast((ch >> 6) & 0x3F)); - p[3] = static_cast(0x80 | static_cast(ch & 0x3F)); - dst += 4; - } else if (ch <= 0x3FFFFFF) { - p[0] = static_cast(0xF8 | static_cast((ch >> 24) & 0x03)); - p[1] = static_cast(0x80 | static_cast((ch >> 18) & 0x3F)); - p[2] = static_cast(0x80 | static_cast((ch >> 12) & 0x3F)); - p[3] = static_cast(0x80 | static_cast((ch >> 6) & 0x3F)); - p[4] = static_cast(0x80 | static_cast(ch & 0x3F)); - dst += 5; - } else { - p[0] = static_cast(0xFC | static_cast((ch >> 30) & 0x01)); - p[1] = static_cast(0x80 | static_cast((ch >> 24) & 0x3F)); - p[2] = static_cast(0x80 | static_cast((ch >> 18) & 0x3F)); - p[3] = static_cast(0x80 | static_cast((ch >> 12) & 0x3F)); - p[4] = static_cast(0x80 | static_cast((ch >> 6) & 0x3F)); - p[5] = static_cast(0x80 | static_cast(ch & 0x3F)); - dst += 6; - } - return dst; -} -#pragma clang diagnostic pop - -const char* GetScancodeName(SDL_Scancode scancode) { - const char* name; - if (static_cast(scancode) < SDL_SCANCODE_UNKNOWN - || scancode >= SDL_NUM_SCANCODES) { - BA_LOG_ONCE(LogLevel::kError, - "GetScancodeName passed invalid scancode " - + std::to_string(static_cast(scancode))); - return ""; - } - - name = scancode_names[scancode]; - if (name) - return name; - else - return ""; -} - -#pragma clang diagnostic push -#pragma ide diagnostic ignored "hicpp-signed-bitwise" -auto Input::GetKeyName(int keycode) -> std::string { - SDL_Keycode key{keycode}; - static char name[8]; - char* end; - - if (key & SDLK_SCANCODE_MASK) { - return GetScancodeName((SDL_Scancode)(key & ~SDLK_SCANCODE_MASK)); - } - - switch (key) { - case SDLK_RETURN: - return GetScancodeName(SDL_SCANCODE_RETURN); - case SDLK_ESCAPE: - return GetScancodeName(SDL_SCANCODE_ESCAPE); - case SDLK_BACKSPACE: - return GetScancodeName(SDL_SCANCODE_BACKSPACE); - case SDLK_TAB: - return GetScancodeName(SDL_SCANCODE_TAB); - case SDLK_SPACE: - return GetScancodeName(SDL_SCANCODE_SPACE); - case SDLK_DELETE: - return GetScancodeName(SDL_SCANCODE_DELETE); - default: - /* Unaccented letter keys on latin keyboards are normally - labeled in upper case (and probably on others like Greek or - Cyrillic too, so if you happen to know for sure, please - adapt this). */ - if (key >= 'a' && key <= 'z') { - key -= 32; - } - - end = UCS4ToUTF8(static_cast(key), name); - *end = '\0'; - return name; - } -} -#pragma clang diagnostic pop -#endif // BA_SDL2_BUILD || BA_MINSDL_BUILD - -auto Input::LsInputDevices() -> void { - BA_PRECONDITION(InLogicThread()); +void Input::LsInputDevices() { + BA_PRECONDITION(g_base->InLogicThread()); std::string out; @@ -1851,10 +1529,7 @@ auto Input::LsInputDevices() -> void { out += (ind + "is-remote-app: " + std::to_string(device->IsRemoteApp()) + "\n"); - out += ind + "attached-to: " - + (device->GetRemotePlayer() != nullptr ? "remote-player" - : device->GetPlayer() != nullptr ? "local-player" - : "nothing"); + out += ind + "attached-to: " + device->delegate().DescribeAttachedTo(); ++index; } @@ -1862,4 +1537,4 @@ auto Input::LsInputDevices() -> void { Log(LogLevel::kInfo, out); } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/input/input.h b/src/ballistica/base/input/input.h similarity index 54% rename from src/ballistica/input/input.h rename to src/ballistica/base/input/input.h index 3c3659c7..38b9cc82 100644 --- a/src/ballistica/input/input.h +++ b/src/ballistica/base/input/input.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_INPUT_INPUT_H_ -#define BALLISTICA_INPUT_INPUT_H_ +#ifndef BALLISTICA_BASE_INPUT_INPUT_H_ +#define BALLISTICA_BASE_INPUT_INPUT_H_ #include #include @@ -9,44 +9,59 @@ #include #include -#include "ballistica/core/object.h" +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::base { -/// Class for managing input. -/// Should only be used in the logic thread unless otherwise specified. +/// Input subsystem. Mostly operates in the logic thread. class Input { public: Input(); + void OnAppStart(); + void OnAppPause(); + void OnAppResume(); + void OnAppShutdown(); + void StepDisplayTime(); + + void ApplyAppConfig(); + + void OnScreenSizeChange(); + // Add an input device. Must be called from the logic thread; otherwise use // PushAddInputDeviceCall. - auto AddInputDevice(InputDevice* input, bool standard_message) -> void; + void AddInputDevice(InputDevice* device, bool standard_message); // Removes a previously-added input-device. Must be called from the // logic thread; otherwise use PushRemoveInputDeviceCall. - auto RemoveInputDevice(InputDevice* input, bool standard_message) -> void; + void RemoveInputDevice(InputDevice* input, bool standard_message); // Given a device name and persistent identifier for it, returns a device or - // nullptr note that this can return hidden devices (ones the user has flagged - // as totally-ignored, etc). + // nullptr. Note that this can return hidden devices (ones the user has + // flagged as totally-ignored, etc). auto GetInputDevice(const std::string& name, const std::string& persistent_id) -> InputDevice*; - // Return a device by id. - // Note that this can return hidden devices (ones the user has flagged as - // totally-ignored, etc). + // Return a device by id, or nullptr for an invalid id. Note that this can + // return hidden devices (ones the user has flagged as totally-ignored, etc). auto GetInputDevice(int id) -> InputDevice*; // Return all input devices with this name. auto GetInputDevicesWithName(const std::string& name) -> std::vector; - auto Reset() -> void; - auto LockAllInput(bool permanent, const std::string& label) -> void; - auto UnlockAllInput(bool permanent, const std::string& label) -> void; + /// Release all held buttons/keys/etc. For use when directing input + /// to a new target (from in-game to UI, etc.) so that old targets + /// don't get stuck moving/etc. Should come up with a more elegant + /// way to handle this situation. + void ResetHoldStates(); + + void Reset(); + void LockAllInput(bool permanent, const std::string& label); + void UnlockAllInput(bool permanent, const std::string& label); auto IsInputLocked() const -> bool { - return ((input_lock_count_temp_ > 0) || (input_lock_count_permanent_ > 0)); + return input_lock_count_temp_ > 0 || input_lock_count_permanent_ > 0; } auto cursor_pos_x() const -> float { return cursor_pos_x_; } auto cursor_pos_y() const -> float { return cursor_pos_y_; } @@ -59,14 +74,11 @@ class Input { // Reset all keyboard keys to a non-held state and deal out associated // messages - used before switching keyboard focus to a new context // so that the old one is not stuck with a held key forever. - auto ResetKeyboardHeldKeys() -> void; - - auto GetKeyName(int keycode) -> std::string; + void ResetKeyboardHeldKeys(); // Same idea but for joysticks. - auto ResetJoyStickHeldButtons() -> void; + void ResetJoyStickHeldButtons(); auto ShouldCompletelyIgnoreInputDevice(InputDevice* input_device) -> bool; - auto ApplyAppConfig() -> void; auto touch_input() const -> TouchInput* { return touch_input_; } auto have_non_touch_inputs() const -> bool { return have_non_touch_inputs_; } @@ -76,19 +88,16 @@ class Input { auto have_start_activated_default_button_inputs() const -> bool { return have_start_activated_default_button_inputs_; } - auto Draw(FrameDef* frame_def) -> void; + void Draw(FrameDef* frame_def); // Get the total idle time for the system. // FIXME - should better coordinate this with InputDevice::getLastUsedTime(). // auto GetIdleTime() const -> millisecs_t; // Should be called whenever user-input of some form comes through. - // auto ResetIdleTime() -> void { last_input_time_ = GetRealTime(); } + // void ResetIdleTime() { last_input_time_ = GetAppTimeMillisecs(); } auto mark_input_active() { input_active_ = true; } - // Should be called regularly to update button repeats, etc. - auto Update() -> void; - // returns true if more than one non-keyboard device has been active recently // ..this is used to determine whether we need to have strict menu ownership // (otherwise menu use would be chaotic with 8 players connected) @@ -103,58 +112,69 @@ class Input { // something. auto HaveControllerWithPlayer() -> bool; auto HaveRemoteAppController() -> bool; - auto ProcessStressTesting(int player_count) -> void; + void ProcessStressTesting(int player_count); auto keyboard_input() const -> KeyboardInput* { return keyboard_input_; } auto keyboard_input_2() const -> KeyboardInput* { return keyboard_input_2_; } - auto CreateTouchInput() -> void; + void CreateTouchInput(); - auto PushTextInputEvent(const std::string& text) -> void; - auto PushKeyPressEvent(const SDL_Keysym& keysym) -> void; - auto PushKeyReleaseEvent(const SDL_Keysym& keysym) -> void; - auto PushMouseDownEvent(int button, const Vector2f& position) -> void; - auto PushMouseUpEvent(int button, const Vector2f& position) -> void; - auto PushMouseMotionEvent(const Vector2f& position) -> void; - auto PushSmoothMouseScrollEvent(const Vector2f& velocity, bool momentum) - -> void; - auto PushMouseScrollEvent(const Vector2f& amount) -> void; - auto PushJoystickEvent(const SDL_Event& event, InputDevice* input_device) - -> void; - auto PushAddInputDeviceCall(InputDevice* input_device, bool standard_message) - -> void; - auto PushRemoveInputDeviceCall(InputDevice* input_device, - bool standard_message) -> void; - auto PushTouchEvent(const TouchEvent& touch_event) -> void; - auto PushDestroyKeyboardInputDevices() -> void; - auto PushCreateKeyboardInputDevices() -> void; - auto LsInputDevices() -> void; + void PushTextInputEvent(const std::string& text); + void PushKeyPressEvent(const SDL_Keysym& keysym); + void PushKeyReleaseEvent(const SDL_Keysym& keysym); + void PushMouseDownEvent(int button, const Vector2f& position); + void PushMouseUpEvent(int button, const Vector2f& position); + void PushMouseMotionEvent(const Vector2f& position); + void PushSmoothMouseScrollEvent(const Vector2f& velocity, bool momentum); + void PushMouseScrollEvent(const Vector2f& amount); + void PushJoystickEvent(const SDL_Event& event, InputDevice* input_device); + void PushAddInputDeviceCall(InputDevice* input_device, bool standard_message); + void PushRemoveInputDeviceCall(InputDevice* input_device, + bool standard_message); + void PushTouchEvent(const TouchEvent& touch_event); + void PushDestroyKeyboardInputDevices(); + void PushCreateKeyboardInputDevices(); + void LsInputDevices(); /// Roughly how long in milliseconds have all input devices been idle. auto input_idle_time() const { return input_idle_time_; } + typedef bool(HandleJoystickEventCall)(const SDL_Event& event, + InputDevice* input_device); + typedef bool(HandleKeyPressCall)(const SDL_Keysym& keysym); + typedef bool(HandleKeyReleaseCall)(const SDL_Keysym& keysym); + + void CaptureKeyboardInput(HandleKeyPressCall* press_call, + HandleKeyReleaseCall* release_call); + void ReleaseKeyboardInput(); + + void CaptureJoystickInput(HandleJoystickEventCall* call); + void ReleaseJoystickInput(); + private: - auto UpdateInputDeviceCounts() -> void; + void UpdateInputDeviceCounts(); auto GetNewNumberedIdentifier(const std::string& name, const std::string& identifier) -> int; - auto UpdateEnabledControllerSubsystems() -> void; - auto AnnounceConnects() -> void; - auto AnnounceDisconnects() -> void; - auto HandleKeyPress(const SDL_Keysym* keysym) -> void; - auto HandleKeyRelease(const SDL_Keysym* keysym) -> void; - auto HandleMouseMotion(const Vector2f& position) -> void; - auto HandleMouseDown(int button, const Vector2f& position) -> void; - auto HandleMouseUp(int button, const Vector2f& position) -> void; - auto HandleMouseScroll(const Vector2f& amount) -> void; - auto HandleSmoothMouseScroll(const Vector2f& velocity, bool momentum) -> void; - auto HandleJoystickEvent(const SDL_Event& event, InputDevice* input_device) - -> void; - auto HandleTouchEvent(const TouchEvent& e) -> void; - auto ShowStandardInputDeviceConnectedMessage(InputDevice* j) -> void; - auto ShowStandardInputDeviceDisconnectedMessage(InputDevice* j) -> void; - auto PrintLockLabels() -> void; - auto UpdateModKeyStates(const SDL_Keysym* keysym, bool press) -> void; - auto CreateKeyboardInputDevices() -> void; - auto DestroyKeyboardInputDevices() -> void; + void UpdateEnabledControllerSubsystems(); + void AnnounceConnects(); + void AnnounceDisconnects(); + void HandleKeyPress(const SDL_Keysym* keysym); + void HandleKeyRelease(const SDL_Keysym* keysym); + void HandleMouseMotion(const Vector2f& position); + void HandleMouseDown(int button, const Vector2f& position); + void HandleMouseUp(int button, const Vector2f& position); + void HandleMouseScroll(const Vector2f& amount); + void HandleSmoothMouseScroll(const Vector2f& velocity, bool momentum); + void HandleJoystickEvent(const SDL_Event& event, InputDevice* input_device); + void HandleTouchEvent(const TouchEvent& e); + void ShowStandardInputDeviceConnectedMessage(InputDevice* j); + void ShowStandardInputDeviceDisconnectedMessage(InputDevice* j); + void PrintLockLabels(); + void UpdateModKeyStates(const SDL_Keysym* keysym, bool press); + void CreateKeyboardInputDevices(); + void DestroyKeyboardInputDevices(); + HandleKeyPressCall* keyboard_input_capture_press_{}; + HandleKeyReleaseCall* keyboard_input_capture_release_{}; + HandleJoystickEventCall* joystick_input_capture_{}; bool input_active_{}; millisecs_t input_idle_time_{}; int local_active_input_device_count_{}; @@ -197,6 +217,6 @@ class Input { void* single_touch_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_INPUT_INPUT_H_ +#endif // BALLISTICA_BASE_INPUT_INPUT_H_ diff --git a/src/ballistica/input/remote_app.cc b/src/ballistica/base/input/support/remote_app_server.cc similarity index 85% rename from src/ballistica/input/remote_app.cc rename to src/ballistica/base/input/support/remote_app_server.cc index 09320c24..1ab678df 100644 --- a/src/ballistica/input/remote_app.cc +++ b/src/ballistica/base/input/support/remote_app_server.cc @@ -1,25 +1,19 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/input/remote_app.h" +#include "ballistica/base/input/support/remote_app_server.h" -#if BA_OSTYPE_WINDOWS -#include -#pragma comment(lib, "ws2_32.lib") -#else +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/networking/network_reader.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/core/platform/support/min_sdl.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/utils.h" -#endif - -#include "ballistica/app/app.h" -#include "ballistica/assets/assets.h" -#include "ballistica/generic/utils.h" -#include "ballistica/input/input.h" -#include "ballistica/logic/logic.h" -#include "ballistica/math/vector3f.h" -#include "ballistica/networking/network_reader.h" -#include "ballistica/platform/min_sdl.h" -#include "ballistica/platform/platform.h" - -namespace ballistica { +namespace ballistica::base { // Just used privately by the remote-server machinery. enum class RemoteAppServer::RemoteEventType { @@ -55,14 +49,14 @@ void RemoteAppServer::HandleData(int socket, uint8_t* buffer, size_t amt, case BA_PACKET_REMOTE_GAME_QUERY: { // Ship them a response packet with our name. char msg[256]; - std::string name = g_platform->GetDeviceName(); + std::string name = g_core->platform->GetDeviceName(); msg[0] = BA_PACKET_REMOTE_GAME_RESPONSE; strncpy(msg + 1, name.c_str(), sizeof(msg) - 1); msg[255] = 0; size_t msg_len = 1 + strlen(msg + 1); // This needs to be locked during any sd changes/writes. - std::scoped_lock lock(g_network_reader->sd_mutex()); + std::scoped_lock lock(g_base->network_reader->sd_mutex()); sendto(socket, msg, static_cast_check_fit(msg_len), 0, addr, static_cast(addr_len)); @@ -91,7 +85,7 @@ void RemoteAppServer::HandleData(int socket, uint8_t* buffer, size_t amt, static_cast_check_fit(RemoteError::kVersionMismatch)}; // This needs to be locked during any sd changes/writes. - std::scoped_lock lock(g_network_reader->sd_mutex()); + std::scoped_lock lock(g_base->network_reader->sd_mutex()); sendto(socket, reinterpret_cast(data), sizeof(data), 0, addr, static_cast(addr_len)); break; @@ -135,7 +129,7 @@ void RemoteAppServer::HandleData(int socket, uint8_t* buffer, size_t amt, static_cast(protocol_response)}; // This needs to be locked during any sd changes/writes. - std::scoped_lock lock(g_network_reader->sd_mutex()); + std::scoped_lock lock(g_base->network_reader->sd_mutex()); sendto(socket, reinterpret_cast(data), sizeof(data), 0, addr, static_cast(addr_len)); } else { @@ -145,7 +139,7 @@ void RemoteAppServer::HandleData(int socket, uint8_t* buffer, size_t amt, RemoteError::kNotAcceptingConnections)}; // This needs to be locked during any sd changes/writes. - std::scoped_lock lock(g_network_reader->sd_mutex()); + std::scoped_lock lock(g_base->network_reader->sd_mutex()); sendto(socket, reinterpret_cast(data), sizeof(data), 0, addr, static_cast(addr_len)); } @@ -165,11 +159,16 @@ void RemoteAppServer::HandleData(int socket, uint8_t* buffer, size_t amt, // Replace ${CONTROLLER} with it in our message. std::string s = - g_logic->GetResourceString("controllerDisconnectedText"); + g_base->assets->GetResourceString("controllerDisconnectedText"); Utils::StringReplaceOne(&s, "${CONTROLLER}", m); - g_logic->PushScreenMessage(s, Vector3f(1, 1, 1)); - g_logic->PushPlaySoundCall(SystemSoundID::kCorkPop); - g_input->PushRemoveInputDeviceCall(client->joystick_, false); + g_base->logic->event_loop()->PushCall([s] { + g_base->graphics->AddScreenMessage(s, Vector3f(1, 1, 1)); + }); + g_base->logic->event_loop()->PushCall([] { + g_base->audio->PlaySound( + g_base->assets->SysSound(SysSoundID::kCorkPop)); + }); + g_base->input->PushRemoveInputDeviceCall(client->joystick_, false); client->joystick_ = nullptr; client->in_use = false; client->name[0] = 0; @@ -179,7 +178,7 @@ void RemoteAppServer::HandleData(int socket, uint8_t* buffer, size_t amt, uint8_t data[1] = {BA_PACKET_REMOTE_DISCONNECT_ACK}; // This needs to be locked during any sd changes/writes. - std::scoped_lock lock(g_network_reader->sd_mutex()); + std::scoped_lock lock(g_base->network_reader->sd_mutex()); sendto(socket, reinterpret_cast(data), 1, 0, addr, static_cast(addr_len)); } @@ -204,7 +203,7 @@ void RemoteAppServer::HandleData(int socket, uint8_t* buffer, size_t amt, static_cast_check_fit(RemoteError::kNotConnected)}; // This needs to be locked during any sd changes/writes. - std::scoped_lock lock(g_network_reader->sd_mutex()); + std::scoped_lock lock(g_base->network_reader->sd_mutex()); sendto(socket, reinterpret_cast(data), sizeof(data), 0, addr, static_cast(addr_len)); break; @@ -218,7 +217,7 @@ void RemoteAppServer::HandleData(int socket, uint8_t* buffer, size_t amt, RemoteAppClient* client = clients_ + joystick_id; // Take note that we heard from them. - client->last_contact_time = GetRealTime(); + client->last_contact_time = g_core->GetAppTimeMillisecs(); // Ok now iterate. uint8_t* val = buffer + 4; @@ -313,7 +312,7 @@ void RemoteAppServer::HandleData(int socket, uint8_t* buffer, size_t amt, uint8_t data[2] = {BA_PACKET_REMOTE_STATE_ACK, client->next_state_id}; // This needs to be locked during any sd changes/writes. - std::scoped_lock lock(g_network_reader->sd_mutex()); + std::scoped_lock lock(g_base->network_reader->sd_mutex()); sendto(socket, reinterpret_cast(data), 2, 0, addr, static_cast(addr_len)); @@ -331,7 +330,7 @@ void RemoteAppServer::HandleData(int socket, uint8_t* buffer, size_t amt, static_cast_check_fit(RemoteError::kVersionMismatch)}; // This needs to be locked during any sd changes/writes. - std::scoped_lock lock(g_network_reader->sd_mutex()); + std::scoped_lock lock(g_base->network_reader->sd_mutex()); sendto(socket, reinterpret_cast(data), sizeof(data), 0, addr, static_cast(addr_len)); break; @@ -346,7 +345,7 @@ auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr, size_t addr_len, const char* name, bool using_v2) -> int { // If we're not accepting connections at all, reject 'em. - if (!g_app->remote_server_accepting_connections) { + if (!g_base->networking->remote_server_accepting_connections()) { return -1; } @@ -367,10 +366,15 @@ auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr, snprintf(m, sizeof(m), "%s", clients_[i].display_name); // Replace ${CONTROLLER} with it in our message. - std::string s = g_logic->GetResourceString("controllerReconnectedText"); + std::string s = + g_base->assets->GetResourceString("controllerReconnectedText"); Utils::StringReplaceOne(&s, "${CONTROLLER}", m); - g_logic->PushScreenMessage(s, Vector3f(1, 1, 1)); - g_logic->PushPlaySoundCall(SystemSoundID::kGunCock); + g_base->logic->event_loop()->PushCall( + [s] { g_base->graphics->AddScreenMessage(s, Vector3f(1, 1, 1)); }); + g_base->logic->event_loop()->PushCall([] { + g_base->audio->PlaySound( + g_base->assets->SysSound(SysSoundID::kGunCock)); + }); } clients_[i].in_use = true; return i; @@ -378,7 +382,7 @@ auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr, } // Don't reuse a slot for 5 seconds (if its been heard from since this time). - millisecs_t cooldown_time = GetRealTime() - 5000; + millisecs_t cooldown_time = g_core->GetAppTimeMillisecs() - 5000; // Ok, not there already.. now look for a non-taken one and return that. for (int i = 0; i < kMaxRemoteAppClients; i++) { @@ -401,7 +405,7 @@ auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr, strcpy(clients_[i].display_name, clients_[i].name); // NOLINT char* c = strchr(clients_[i].display_name, '#'); if (c) *c = 0; - clients_[i].last_contact_time = GetRealTime(); + clients_[i].last_contact_time = g_core->GetAppTimeMillisecs(); clients_[i].request_id = request_id; char m[256]; @@ -409,12 +413,18 @@ auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr, snprintf(m, sizeof(m), "%s", clients_[i].display_name); // Replace ${CONTROLLER} with it in our message. - std::string s = g_logic->GetResourceString("controllerConnectedText"); + std::string s = + g_base->assets->GetResourceString("controllerConnectedText"); Utils::StringReplaceOne(&s, "${CONTROLLER}", m); - g_logic->PushScreenMessage(s, Vector3f(1, 1, 1)); - g_logic->PushPlaySoundCall(SystemSoundID::kGunCock); + g_base->logic->event_loop()->PushCall( + [s] { g_base->graphics->AddScreenMessage(s, Vector3f(1, 1, 1)); }); + + g_base->logic->event_loop()->PushCall([] { + g_base->audio->PlaySound( + g_base->assets->SysSound(SysSoundID::kGunCock)); + }); std::string utf8 = Utils::GetValidUTF8(clients_[i].display_name, "rsgc1"); - clients_[i].joystick_ = Object::NewDeferred( + clients_[i].joystick_ = Object::NewDeferred( -1, // not an sdl joystick "RemoteApp: " + utf8, // device name (we now incorporate the name they send us) @@ -423,12 +433,12 @@ auto RemoteAppServer::GetClient(int request_id, struct sockaddr* addr, clients_[i].joystick_->set_is_remote_app(true); // If they name they supplied was <= 10 characters, use it as our default - // character name. + // player name. if (Utils::UTF8StringLength(utf8.c_str()) <= 10) { clients_[i].joystick_->set_custom_default_player_name(utf8); } - assert(g_logic); - g_input->PushAddInputDeviceCall(clients_[i].joystick_, false); + assert(g_base->logic); + g_base->input->PushAddInputDeviceCall(clients_[i].joystick_, false); return i; } } @@ -514,8 +524,8 @@ void RemoteAppServer::HandleRemoteEvent(RemoteAppClient* client, break; } if (send) { - assert(g_logic); - g_input->PushJoystickEvent(e, client->joystick_); + assert(g_base->logic); + g_base->input->PushJoystickEvent(e, client->joystick_); } #pragma clang diagnostic pop } @@ -544,10 +554,10 @@ void RemoteAppServer::HandleRemoteFloatEvent(RemoteAppClient* client, break; } if (send) { - assert(g_logic); - g_input->PushJoystickEvent(e, client->joystick_); + assert(g_base->logic); + g_base->input->PushJoystickEvent(e, client->joystick_); } #pragma clang diagnostic pop } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/input/remote_app.h b/src/ballistica/base/input/support/remote_app_server.h similarity index 76% rename from src/ballistica/input/remote_app.h rename to src/ballistica/base/input/support/remote_app_server.h index 62a48625..4f3227d6 100644 --- a/src/ballistica/input/remote_app.h +++ b/src/ballistica/base/input/support/remote_app_server.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_INPUT_REMOTE_APP_H_ -#define BALLISTICA_INPUT_REMOTE_APP_H_ +#ifndef BALLISTICA_BASE_INPUT_SUPPORT_REMOTE_APP_SERVER_H_ +#define BALLISTICA_BASE_INPUT_SUPPORT_REMOTE_APP_SERVER_H_ -#include "ballistica/input/device/joystick.h" -#include "ballistica/networking/networking.h" -#include "ballistica/networking/networking_sys.h" +#include "ballistica/base/input/device/joystick_input.h" +#include "ballistica/base/networking/networking.h" +#include "ballistica/shared/networking/networking_sys.h" -namespace ballistica { +namespace ballistica::base { constexpr int kRemoteAppProtocolVersion = 121; constexpr int kMaxRemoteAppClients = 24; @@ -48,12 +48,12 @@ class RemoteAppServer { int request_id{}; char name[101]{}; char display_name[101]{}; - struct sockaddr_storage address {}; + sockaddr_storage address{}; size_t address_size{}; millisecs_t last_contact_time{}; uint8_t next_state_id{}; uint32_t state{}; - Joystick* joystick_{}; + JoystickInput* joystick_{}; }; RemoteAppClient clients_[kMaxRemoteAppClients]{}; enum class RemoteEventType; @@ -62,6 +62,6 @@ class RemoteAppServer { float val); }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_INPUT_REMOTE_APP_H_ +#endif // BALLISTICA_BASE_INPUT_SUPPORT_REMOTE_APP_SERVER_H_ diff --git a/src/ballistica/base/logic/logic.cc b/src/ballistica/base/logic/logic.cc new file mode 100644 index 00000000..3e206943 --- /dev/null +++ b/src/ballistica/base/logic/logic.cc @@ -0,0 +1,567 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/logic/logic.h" + +#include "ballistica/base/app/app.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/support/plus_soft.h" +#include "ballistica/base/ui/console.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python_command.h" +#include "ballistica/shared/python/python_sys.h" + +namespace ballistica::base { + +Logic::Logic() : display_timers_(new TimerList()) { + // Enable display-time debug logs via env var. + auto val = g_core->platform->GetEnv("BA_DEBUG_LOG_DISPLAY_TIME"); + if (val && *val == "1") { + debug_log_display_time_ = true; + } +} + +void Logic::OnMainThreadStartApp() { + event_loop_ = new EventLoop(EventLoopID::kLogic); + g_core->pausable_event_loops.push_back(event_loop_); + + // Sit and wait for our logic thread to run its startup stuff. + event_loop_->PushCallSynchronous([this] { OnAppStart(); }); +} + +void Logic::OnAppStart() { + assert(g_base->InLogicThread()); + g_core->BootLog("logic-thread on-app-start begin"); + try { + // Our thread should not be holding the GIL here at the start (and + // probably not have any Python state at all). So here we set both + // of those up. + assert(!PyGILState_Check()); + PyGILState_Ensure(); + + // Code running in the logic thread holds the GIL by default. + event_loop_->SetAcquiresPythonGIL(); + + // Keep informed when our thread's event loop is pausing/unpausing. + event_loop_->AddPauseCallback( + NewLambdaRunnableUnmanaged([this] { OnAppPause(); })); + event_loop_->AddResumeCallback( + NewLambdaRunnableUnmanaged([this] { OnAppResume(); })); + + // Running in a specific order here and should try to stick to it in + // other OnAppXXX callbacks so any subsystem interdependencies behave + // consistently. When pausing or shutting-down we use the opposite order for + // the same reason. Let's do Python last (or first when pausing, etc) since + // it will be the most variable; that way it will interact with other + // subsystems in their normal states which is less likely to lead to + // problems. + g_base->graphics->OnAppStart(); + g_base->audio->OnAppStart(); + g_base->input->OnAppStart(); + g_base->ui->OnAppStart(); + g_core->platform->OnAppStart(); + g_base->app_mode->OnAppStart(); + if (g_base->HavePlus()) { + g_base->Plus()->OnAppStart(); + } + g_base->python->OnAppStart(); + } catch (const std::exception& e) { + // If anything went wrong, trigger a deferred error. + // This way it is more likely we can show a fatal error dialog + // since the main thread won't be blocking waiting for us to init. + std::string what = e.what(); + this->event_loop()->PushCall([what] { + // Just throw a std exception since our 'what' probably already + // contains a stack trace; if we throw a ballistica Exception we + // wind up with a useless second one. + throw std::logic_error(what.c_str()); + }); + } + g_core->BootLog("logic-thread on-app-start end"); +} + +void Logic::OnAppPause() { + assert(g_base->CurrentContext().IsEmpty()); + + // Note: keep these in opposite order of OnAppStart. + g_base->python->OnAppPause(); + if (g_base->HavePlus()) { + g_base->Plus()->OnAppPause(); + } + g_base->app_mode->OnAppPause(); + g_core->platform->OnAppPause(); + g_base->ui->OnAppPause(); + g_base->input->OnAppPause(); + g_base->audio->OnAppPause(); + g_base->graphics->OnAppPause(); +} + +void Logic::OnAppResume() { + assert(g_base->CurrentContext().IsEmpty()); + + // Note: keep these in the same order as OnAppStart. + g_base->graphics->OnAppResume(); + g_base->audio->OnAppResume(); + g_base->input->OnAppResume(); + g_base->ui->OnAppResume(); + g_core->platform->OnAppResume(); + g_base->app_mode->OnAppResume(); + if (g_base->HavePlus()) { + g_base->Plus()->OnAppResume(); + } + g_base->python->OnAppResume(); +} + +void Logic::OnAppShutdown() { + assert(g_core); + assert(g_base->CurrentContext().IsEmpty()); + + // Nuke the app from orbit if we get stuck while shutting down. + g_core->StartSuicideTimer("shutdown", 10000); + + // Note: keep these in opposite order of OnAppStart. + g_base->python->OnAppShutdown(); + if (g_base->HavePlus()) { + g_base->Plus()->OnAppShutdown(); + } + g_base->app_mode->OnAppShutdown(); + g_core->platform->OnAppResume(); + g_base->ui->OnAppShutdown(); + g_base->input->OnAppShutdown(); + g_base->audio->OnAppShutdown(); + g_base->graphics->OnAppShutdown(); + + // FIXME: Should add a mechanism where we give the above subsystems + // a short bit of time to complete shutdown if they need it. + // For now just completing instantly. + g_base->app->event_loop()->PushCall( + [] { g_base->app->LogicThreadShutdownComplete(); }); +} + +void Logic::ApplyAppConfig() { + assert(g_base->InLogicThread()); + g_core->BootLog("apply-app-config begin"); + + // Give all our other subsystems a chance. + // Note: keep these in the same order as OnAppStart. + g_base->graphics->ApplyAppConfig(); + g_base->audio->ApplyAppConfig(); + g_base->input->ApplyAppConfig(); + g_base->ui->ApplyAppConfig(); + g_core->platform->ApplyAppConfig(); + g_base->app_mode->ApplyAppConfig(); + if (g_base->HavePlus()) { + g_base->Plus()->ApplyAppConfig(); + } + g_base->python->ApplyAppConfig(); + + // Give the app subsystem a chance too even though its main-thread based. + // We call it here in the logic thread, allowing it to read whatever + // it needs and pass it to itself in the main thread. + g_base->app->LogicThreadApplyAppConfig(); + + applied_app_config_ = true; + g_core->BootLog("apply-app-config end"); +} + +void Logic::OnInitialScreenCreated() { + assert(g_base->InLogicThread()); + + // Ok; graphics-server is telling us we've got a screen + // (or no screen in the case of headless-mode). + // We use this as a cue to kick off our business logic. + + // Let the assets system know it can start loading stuff now that + // we have a screen and thus know texture formats/etc. + // TODO(ericf): It might be nice to kick this off earlier if our logic is + // robust enough to create some sort of 'null' textures/meshes before + // the renderer is ready and then seamlessly create renderer-specific + // ones once the renderer is up. We could likely at least get a lot + // of preloads done in the meantime. Though this would require preloads + // to be renderer-agnostic; not sure if that's the case. + g_base->assets->StartLoading(); + + // Let base know it can create the console or other asset-dependent things. + g_base->OnScreenAndAssetsReady(); + + // Set up our timers. + process_pending_work_timer_ = event_loop()->NewTimer( + 0, true, NewLambdaRunnable([this] { ProcessPendingWork(); })); + asset_prune_timer_ = event_loop()->NewTimer( + 2345, true, NewLambdaRunnable([] { g_base->assets->Prune(); })); + + // Normally we step display-time as part of our frame-drawing process. If + // we're headless, we're not drawing any frames, but we still want to do + // minimal processing on any display-time timers. Let's run at a low-ish + // rate (10hz) to keep things efficient. Anyone dealing in display-time + // should be able to handle a wide variety of rates anyway. + if (g_core->HeadlessMode()) { + headless_display_time_step_timer_ = event_loop()->NewTimer( + 1000 / 10, true, NewLambdaRunnable([this] { StepDisplayTime(); })); + } + // Let our initial app-mode know it has become active. + g_base->app_mode->OnActivate(); + + // Let the Python layer know what's up. It will probably flip to + // 'Launching' state. + CompleteAppBootstrapping(); + + // Deliver an initial frame-def to the graphics thread. It will send us a + // request for a new one each time it grabs one we send it. + if (!g_core->HeadlessMode()) { + g_base->graphics->BuildAndPushFrameDef(); + } +} + +// Launch into main menu or whatever else. +void Logic::CompleteAppBootstrapping() { + assert(g_base->InLogicThread()); + assert(!app_bootstrapping_complete_); + assert(g_base->CurrentContext().IsEmpty()); + + g_core->BootLog("app bootstrapping complete"); + + // Let Python know we're done bootstrapping so it can flip the app + // into the 'launching' state. + g_base->python->objs() + .Get(BasePython::ObjID::kOnAppBootstrappingCompleteCall) + .Call(); + app_bootstrapping_complete_ = true; + + // TODO(ericf): update this for the shiny new app-mode world. + if (explicit_bool(true)) { + // If we were passed launch command args, run them. + if (g_core->core_config().exec_command.has_value()) { + bool success = PythonCommand(*g_core->core_config().exec_command, + BA_BUILD_COMMAND_FILENAME) + .Exec(true, nullptr, nullptr); + if (!success) { + exit(1); + } + } + // If the stuff we just ran didn't result in a session, create a default + // one. + auto* appmode = scene_v1::SceneV1AppMode::GetActiveOrFatal(); + if (!appmode->GetForegroundSession()) { + appmode->RunMainMenu(); + } + } + + UpdatePendingWorkTimer(); +} + +void Logic::OnScreenSizeChange(float virtual_width, float virtual_height, + float pixel_width, float pixel_height) { + assert(g_base->InLogicThread()); + + // First, pass the new values to the graphics subsystem. + // Then inform everyone else simply that they changed; they can ask + // g_graphics for whatever specific values they need. + // Note: keep these in the same order as OnAppStart. + g_base->graphics->OnScreenSizeChange(virtual_width, virtual_height, + pixel_width, pixel_height); + g_base->audio->OnScreenSizeChange(); + g_base->input->OnScreenSizeChange(); + g_base->ui->OnScreenSizeChange(); + g_core->platform->OnScreenSizeChange(); + g_base->app_mode->OnScreenSizeChange(); + if (g_base->HavePlus()) { + g_base->Plus()->OnScreenSizeChange(); + } + g_base->python->OnScreenSizeChange(); +} + +// Bring all logic-thread stuff up to date for a new visual frame. +void Logic::StepDisplayTime() { + assert(g_base->InLogicThread()); + + UpdateDisplayTime(); + + // Give all our subsystems some update love. + // Note: keep these in the same order as OnAppStart. + g_base->graphics->StepDisplayTime(); + g_base->audio->StepDisplayTime(); + g_base->input->StepDisplayTime(); + g_base->ui->StepDisplayTime(); + g_core->platform->StepDisplayTime(); + g_base->app_mode->StepDisplayTime(); + if (g_base->HavePlus()) { + g_base->Plus()->StepDisplayTime(); + } + g_base->python->StepDisplayTime(); + g_base->app->LogicThreadStepDisplayTime(); + + // Let's run display-timers *after* we step everything else so most things + // they interact with will be in an up-to-date state. + display_timers_->Run(g_core->GetAppTimeMillisecs()); +} + +void Logic::UpdateDisplayTime() { + // Here we update our smoothed display-time-increment based on how fast + // we are currently rendering frames. We want display-time to basically + // be progressing at the same rate as app-time but in as constant + // of a manner as possible so that animations, simulation-stepping/etc. + // appears smooth (app-time measurements at render times exhibit quite a bit + // of jitter). Though we also don't want it to be *too* smooth; drops in + // framerate should still be reflected quickly in display-time-increment + // otherwise it can look like the game is slowing down or speeding up. + + // Flip this on to debug this stuff. + // Things to look for: + // - 'final' value should mostly stay constant. + // - 'final' value should not be *too* far from 'used'. + // - 'use_avg' should mostly be 1. + // - these can vary briefly during load spikes/etc. but should quickly + // reconverge to stability. If not, this may need further calibration. + auto current_app_time = g_core->GetAppTimeSeconds(); + + // We handle the first measurement specially. + if (last_display_time_update_app_time_ < 0) { + last_display_time_update_app_time_ = current_app_time; + } else { + auto this_increment = current_app_time - last_display_time_update_app_time_; + last_display_time_update_app_time_ = current_app_time; + + // Store increments into a looping buffer. + if (recent_display_time_increments_index_ < 0) { + // For the first sample we fill all entries. + for (auto& recent_display_time_increment : + recent_display_time_increments_) { + recent_display_time_increment = this_increment; + } + recent_display_time_increments_index_ = 0; + } else { + recent_display_time_increments_[recent_display_time_increments_index_] = + this_increment; + recent_display_time_increments_index_ = + (recent_display_time_increments_index_ + 1) % kDisplayTimeSampleCount; + } + + // It seems that when things get thrown off it is often due to a single + // rogue sample being unusually long and often the next one being unusually + // short. Let's try to filter out some of these cases by ignoring both + // the longest and shortest sample in our set. + int max_index{}; + int min_index{}; + double max_val{recent_display_time_increments_[0]}; + double min_val{recent_display_time_increments_[0]}; + for (int i = 0; i < kDisplayTimeSampleCount; ++i) { + auto val = recent_display_time_increments_[i]; + if (val > max_val) { + max_val = val; + max_index = i; + } + if (val < min_val) { + min_val = val; + min_index = i; + } + } + + double avg{}; + double min{}; + double max{}; + int count{}; + for (int i = 0; i < kDisplayTimeSampleCount; ++i) { + if (i == min_index || i == max_index) { + continue; + } + auto val = recent_display_time_increments_[i]; + if (count == 0) { + // We may have skipped first index(es) so need to do this here + // instead of initing min/max to first value. + min = max = val; + } + avg += val; + min = std::min(min, val); + max = std::max(max, val); + count += 1; + } + avg /= count; + double range = max - min; + + // If our range of recent increment values is somewhat large relative to an + // average value, things are probably chaotic so just use the current value + // to respond quickly to changes. If things are more calm, use our nice + // smoothed value. + // So in a case where we're seeing an average increment of 16ms, we snap + // out of avg mode if there's more than 8ms between the longest and shortest + // increments. + double chaos = range / avg; + bool use_avg = chaos < 0.5; + auto used = use_avg ? avg : this_increment; + + // Lastly use this 'used' value to update our actual increment - our + // increment moves only if 'used' value gets farther than [trail_buffer] + // from it. So ideally it will sit in the middle of the smoothed value + // range. + + // How far the smoothed increment value needs to get away from the final + // value to actually start moving it. + // Example: If our avg increment is 16.6ms (60fps), don't change our + // increment until the 'used' value is more than 0.5ms (16.6 * 0.03) from it + // in either direction. + // Note: In practice I'm seeing that higher framerates + // like 120 need buffers that are larger relative to avg to remain stable. + // Though perhaps a bit of jitter is not noticeable at high frame rates; + // just something to keep an eye on. + auto trail_buffer{avg * 0.03}; + + auto trailing_diff = used - display_time_increment_; + auto trailing_dist = std::abs(trailing_diff); + if (trailing_dist > trail_buffer) { + auto offs = + (trailing_dist - trail_buffer) * (trailing_diff > 0.0 ? 1.0 : -1.0); + if (debug_log_display_time_) { + char buffer[256]; + snprintf(buffer, sizeof(buffer), + "trailing_dist %.6f > trail_buffer %.6f; will offset %.6f).", + trailing_dist, trail_buffer, offs); + Log(LogLevel::kDebug, buffer); + } + display_time_increment_ = display_time_increment_ + offs; + } + + if (debug_log_display_time_) { + char buffer[256]; + snprintf(buffer, sizeof(buffer), + "final %.5f used %.5f use_avg %d sample %.5f chaos %.5f", + display_time_increment_, used, static_cast(use_avg), + this_increment, chaos); + Log(LogLevel::kDebug, buffer); + } + } + // Lastly, apply our updated increment value to our time. + display_time_ += display_time_increment_; +} + +// Set up our sleeping based on what we're doing. +void Logic::UpdatePendingWorkTimer() { + assert(g_base->InLogicThread()); + + // This might get called before we set up our timer in some cases. (such as + // very early) should be safe to ignore since we update the interval + // explicitly after creating the timers. + if (!process_pending_work_timer_) { + return; + } + + // If there's loading to do, keep at it rather vigorously. + if (have_pending_loads_) { + assert(process_pending_work_timer_); + process_pending_work_timer_->SetLength(1); + } else { + // Otherwise we've got nothing to do; go to sleep until something changes. + assert(process_pending_work_timer_); + process_pending_work_timer_->SetLength(-1); + } +} + +void Logic::HandleInterruptSignal() { + assert(g_base->InLogicThread()); + + // Special case; when running under the server-wrapper, we completely + // ignore interrupt signals (the wrapper acts on them). + if (g_base->app->server_wrapper_managed()) { + return; + } + + // Go with a low level process shutdown here. In situations + // where we're getting interrupt signals I don't think we'd ever want + // high level 'soft' quits. + Shutdown(); +} + +void Logic::Draw() { + assert(g_base->InLogicThread()); + assert(!g_core->HeadlessMode()); + + // Push a snapshot of our current state to be rendered in the graphics thread. + g_base->graphics->BuildAndPushFrameDef(); + + // Now bring logic up to date. + // By doing this *after* fulfilling the draw request, we're minimizing the + // chance of long logic updates leading to delays in frame-def delivery + // leading to frame drops. The downside is that when logic updates are fast + // then logic is basically sitting around twiddling its thumbs and getting + // a full frame out of date before being drawn. But as high frame rates are + // becoming more normal this becomes less and less meaningful and its probably + // best to prioritize smooth visuals. + StepDisplayTime(); +} + +void Logic::NotifyOfPendingAssetLoads() { + assert(g_base->InLogicThread()); + have_pending_loads_ = true; + UpdatePendingWorkTimer(); +} + +void Logic::Shutdown() { + assert(g_base->InLogicThread()); + + if (!g_core->shutting_down) { + g_core->shutting_down = true; + OnAppShutdown(); + } +} + +auto Logic::NewAppTimer(millisecs_t length, bool repeat, + const Object::Ref& runnable) -> int { + // App-Timers simply get injected into our loop and run alongside our own + // stuff. + assert(g_base->InLogicThread()); + auto* timer = event_loop()->NewTimer(length, repeat, runnable); + return timer->id(); +} + +void Logic::DeleteAppTimer(int timer_id) { + assert(g_base->InLogicThread()); + event_loop()->DeleteTimer(timer_id); +} + +void Logic::SetAppTimerLength(int timer_id, millisecs_t length) { + assert(g_base->InLogicThread()); + Timer* t = event_loop()->GetTimer(timer_id); + if (t) { + t->SetLength(length); + } else { + Log(LogLevel::kError, + "Logic::SetAppTimerLength() called on nonexistent timer."); + } +} + +auto Logic::NewDisplayTimer(millisecs_t length, bool repeat, + const Object::Ref& runnable) -> int { + // Display-Timers go into a timer-list that we exec explicitly when we + // step display-time. + assert(g_base->InLogicThread()); + int offset = 0; + Timer* t = display_timers_->NewTimer(g_core->GetAppTimeMillisecs(), length, + offset, repeat ? -1 : 0, runnable); + return t->id(); +} + +void Logic::DeleteDisplayTimer(int timer_id) { + assert(g_base->InLogicThread()); + display_timers_->DeleteTimer(timer_id); +} + +void Logic::SetDisplayTimerLength(int timer_id, millisecs_t length) { + assert(g_base->InLogicThread()); + Timer* t = display_timers_->GetTimer(timer_id); + if (t) { + t->SetLength(length); + } else { + Log(LogLevel::kError, + "Logic::SetDisplayTimerLength() called on nonexistent timer."); + } +} + +void Logic::ProcessPendingWork() { + have_pending_loads_ = g_base->assets->RunPendingLoadsLogicThread(); + UpdatePendingWorkTimer(); +} + +} // namespace ballistica::base diff --git a/src/ballistica/base/logic/logic.h b/src/ballistica/base/logic/logic.h new file mode 100644 index 00000000..a3339ad5 --- /dev/null +++ b/src/ballistica/base/logic/logic.h @@ -0,0 +1,104 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_LOGIC_LOGIC_H_ +#define BALLISTICA_BASE_LOGIC_LOGIC_H_ + +#include +#include +#include + +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::base { + +const int kDisplayTimeSampleCount{15}; + +/// The logic subsystem of the app. This runs on a dedicated thread +/// and is where most high level app logic happens. Much app functionality +/// including UI calls must be run on the logic thread. +class Logic { + public: + Logic(); + + /// Where our stuff runs. Be aware this will return nullptr + /// if the app has not started running yet. + auto event_loop() const -> EventLoop* { return event_loop_; } + + void OnMainThreadStartApp(); + void OnInitialScreenCreated(); + void OnAppStart(); + void OnAppPause(); + void OnAppResume(); + void OnAppShutdown(); + + void ApplyAppConfig(); + void OnScreenSizeChange(float virtual_width, float virtual_height, + float pixel_width, float pixel_height); + + /// Called when we should ship a new frame-def to the graphics server. + /// In graphical builds we also use this opportunity to step our logic. + void Draw(); + + /// Kick off a low level app shutdown which will result in the process + /// exiting. Platform-agnostic code should generally not call this directly + /// and should instead use high level calls like babase.quit(). This allows + /// platforms such as mobile to take alternate actions which may involve + /// leaving the underlying process running. + /// FIXME: I feel like this should be in one of the App classes. + void Shutdown(); + + auto app_bootstrapping_complete() const { + return app_bootstrapping_complete_; + } + void NotifyOfPendingAssetLoads(); + void HandleInterruptSignal(); + + auto NewAppTimer(millisecs_t length, bool repeat, + const Object::Ref& runnable) -> int; + void DeleteAppTimer(int timer_id); + void SetAppTimerLength(int timer_id, millisecs_t length); + + auto NewDisplayTimer(millisecs_t length, bool repeat, + const Object::Ref& runnable) -> int; + void DeleteDisplayTimer(int timer_id); + void SetDisplayTimerLength(int timer_id, millisecs_t length); + + /// Get current display-time for the app (in seconds). + /// Display-time is a seconds value that increments smoothly with + /// frame draws. + auto display_time() { return display_time_; } + + /// Return current display-time increment (in seconds). This can shift with + /// framerate changes but should remain mostly constant. + auto display_time_increment() -> double { return display_time_increment_; } + + auto applied_app_config() const { return applied_app_config_; } + + private: + void UpdateDisplayTime(); + void CompleteAppBootstrapping(); + void ProcessPendingWork(); + void UpdatePendingWorkTimer(); + void StepDisplayTime(); + + double display_time_{}; + double display_time_increment_{1.0 / 60.0}; + double last_display_time_update_app_time_{-1.0}; + double recent_display_time_increments_[kDisplayTimeSampleCount]{}; + int recent_display_time_increments_index_{-1}; + + std::unique_ptr display_timers_; + EventLoop* event_loop_{}; + Timer* process_pending_work_timer_{}; + Timer* headless_display_time_step_timer_{}; + Timer* asset_prune_timer_{}; + Timer* debug_timer_{}; + bool app_bootstrapping_complete_{}; + bool have_pending_loads_{}; + bool debug_log_display_time_{}; + bool applied_app_config_{}; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_LOGIC_LOGIC_H_ diff --git a/src/ballistica/networking/network_reader.cc b/src/ballistica/base/networking/network_reader.cc similarity index 71% rename from src/ballistica/networking/network_reader.cc rename to src/ballistica/base/networking/network_reader.cc index dfd20e2a..489ca4db 100644 --- a/src/ballistica/networking/network_reader.cc +++ b/src/ballistica/base/networking/network_reader.cc @@ -1,28 +1,23 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/networking/network_reader.h" +#include "ballistica/base/networking/network_reader.h" -#include "ballistica/core/thread.h" -#include "ballistica/generic/json.h" -#include "ballistica/input/remote_app.h" -#include "ballistica/logic/connection/connection_set.h" -#include "ballistica/logic/logic.h" -#include "ballistica/logic/player_spec.h" -#include "ballistica/math/vector3f.h" -#include "ballistica/networking/network_writer.h" -#include "ballistica/networking/sockaddr.h" -#include "ballistica/platform/platform.h" -#include "ballistica/python/python.h" +#include "ballistica/base/app/app_mode.h" +#include "ballistica/base/input/support/remote_app_server.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/json.h" +#include "ballistica/shared/math/vector3f.h" +#include "ballistica/shared/networking/sockaddr.h" -namespace ballistica { +namespace ballistica::base { -NetworkReader::NetworkReader() { - // We're a singleton; make sure we don't exist. - assert(g_network_reader == nullptr); -} +NetworkReader::NetworkReader() = default; -auto NetworkReader::SetPort(int port) -> void { - assert(InMainThread()); +void NetworkReader::SetPort(int port) { + assert(g_core->InMainThread()); // Currently can't switch once this is set. if (port4_ != -1) { return; @@ -31,8 +26,8 @@ auto NetworkReader::SetPort(int port) -> void { thread_ = new std::thread(RunThreadStatic, this); } -auto NetworkReader::Pause() -> void { - assert(InMainThread()); +void NetworkReader::OnAppPause() { + assert(g_core->InMainThread()); assert(!paused_); { std::unique_lock lock(paused_mutex_); @@ -48,8 +43,8 @@ auto NetworkReader::Pause() -> void { } } -void NetworkReader::Resume() { - assert(InMainThread()); +void NetworkReader::OnAppResume() { + assert(g_core->InMainThread()); assert(paused_); { @@ -65,7 +60,7 @@ void NetworkReader::PokeSelf() { int sd = socket(AF_INET, SOCK_DGRAM, 0); if (sd < 0) { Log(LogLevel::kError, "Unable to create sleep ping socket; errno " - + g_platform->GetSocketErrorString()); + + g_core->platform->GetSocketErrorString()); } else { struct sockaddr_in serv_addr {}; memset(&serv_addr, 0, sizeof(serv_addr)); @@ -74,8 +69,8 @@ void NetworkReader::PokeSelf() { serv_addr.sin_port = 0; // any int bresult = ::bind(sd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); if (bresult == 1) { - Log(LogLevel::kError, - "Unable to bind sleep socket: " + g_platform->GetSocketErrorString()); + Log(LogLevel::kError, "Unable to bind sleep socket: " + + g_core->platform->GetSocketErrorString()); } else { struct sockaddr_in t_addr {}; memset(&t_addr, 0, sizeof(t_addr)); @@ -87,96 +82,14 @@ void NetworkReader::PokeSelf() { sendto(sd, b, 1, 0, (struct sockaddr*)(&t_addr), sizeof(t_addr)); if (sresult == -1) { Log(LogLevel::kError, "Error on sleep self-sendto: " - + g_platform->GetSocketErrorString()); + + g_core->platform->GetSocketErrorString()); } } - g_platform->CloseSocket(sd); + g_core->platform->CloseSocket(sd); } } -static auto HandleJSONPing(const std::string& data_str) -> std::string { - cJSON* data = cJSON_Parse(data_str.c_str()); - if (data == nullptr) { - return ""; - } - cJSON_Delete(data); - - // Ok lets include some basic info that might be pertinent to someone pinging - // us. Currently that includes our current/max connection count. - char buffer[256]; - int party_size = 0; - int party_size_max = 10; - if (g_python != nullptr) { - party_size = g_logic->public_party_size(); - party_size_max = g_logic->public_party_max_size(); - } - snprintf(buffer, sizeof(buffer), R"({"b":%d,"ps":%d,"psmx":%d})", - kAppBuildNumber, party_size, party_size_max); - return buffer; -} - -static auto HandleGameQuery(const char* buffer, size_t size, - struct sockaddr_storage* from) -> void { - if (size == 5) { - // If we're already in a party, don't advertise since they - // wouldn't be able to join us anyway. - if (g_logic->connections()->has_connection_to_host()) { - return; - } - - // Pull the query id from the packet. - uint32_t query_id; - memcpy(&query_id, buffer + 1, 4); - - // Ship them a response packet containing the query id, - // our protocol version, our unique-app-instance-id, and our - // player_spec. - char msg[400]; - - std::string usid = GetAppInstanceUUID(); - std::string player_spec_string; - - // If we're signed in, send our account spec. - // Otherwise just send a dummy made with our device name. - player_spec_string = PlayerSpec::GetAccountPlayerSpec().GetSpecString(); - - // This should always be the case (len needs to be 1 byte) - BA_PRECONDITION_FATAL(player_spec_string.size() < 256); - - BA_PRECONDITION_FATAL(!usid.empty()); - if (usid.size() > 100) { - Log(LogLevel::kError, "had to truncate session-id; shouldn't happen"); - usid.resize(100); - } - if (usid.empty()) { - usid = "error"; - } - - msg[0] = BA_PACKET_GAME_QUERY_RESPONSE; - memcpy(msg + 1, &query_id, 4); - uint32_t protocol_version = kProtocolVersion; - memcpy(msg + 5, &protocol_version, 4); - msg[9] = static_cast(usid.size()); - msg[10] = static_cast(player_spec_string.size()); - - memcpy(msg + 11, usid.c_str(), usid.size()); - memcpy(msg + 11 + usid.size(), player_spec_string.c_str(), - player_spec_string.size()); - size_t msg_len = 11 + player_spec_string.size() + usid.size(); - BA_PRECONDITION_FATAL(msg_len <= sizeof(msg)); - - std::vector msg_buffer(msg_len); - memcpy(msg_buffer.data(), msg, msg_len); - - g_network_writer->PushSendToCall(msg_buffer, SockAddr(*from)); - - } else { - Log(LogLevel::kError, "Got invalid game-query packet of len " - + std::to_string(size) + "; expected 5."); - } -} - -auto NetworkReader::CheckFDThreshold(int val) -> void { +void NetworkReader::CheckFDThreshold(int val) { if (passed_fd_threshold_) { return; } @@ -189,14 +102,16 @@ auto NetworkReader::CheckFDThreshold(int val) -> void { // If we pass the threshold, do a one-time dump of info // to try and debug it. passed_fd_threshold_ = true; - g_logic->thread()->PushCall([val] { - assert(InLogicThread()); - g_python->obj(Python::ObjID::kOnTooManyFileDescriptorsCall).Call(); + g_base->logic->event_loop()->PushCall([] { + assert(g_base->InLogicThread()); + g_base->python->objs() + .Get(BasePython::ObjID::kOnTooManyFileDescriptorsCall) + .Call(); }); } auto NetworkReader::RunThread() -> int { - if (!HeadlessMode()) { + if (!g_core->HeadlessMode()) { remote_server_ = std::make_unique(); } @@ -213,7 +128,7 @@ auto NetworkReader::RunThread() -> int { // Now just listen and forward messages along. char buffer[10000]; while (true) { - struct sockaddr_storage from {}; + sockaddr_storage from{}; socklen_t from_size = sizeof(from); fd_set readset; FD_ZERO(&readset); @@ -250,12 +165,12 @@ auto NetworkReader::RunThread() -> int { int sresult = select(maxfd + 1, &readset, nullptr, nullptr, nullptr); if (sresult == -1) { // No big deal if we get interrupted occasionally. - if (g_platform->GetSocketError() == EINTR) { + if (g_core->platform->GetSocketError() == EINTR) { // Aint no thang. } else { // Let's complain for anything else though. Log(LogLevel::kError, - "Error on select: " + g_platform->GetSocketErrorString()); + "Error on select: " + g_core->platform->GetSocketErrorString()); } } else { // Wait for any data on either of our sockets. @@ -277,11 +192,11 @@ auto NetworkReader::RunThread() -> int { // If either of our sockets goes down lets close *both* of // them. if (sd4_ != -1) { - g_platform->CloseSocket(sd4_); + g_core->platform->CloseSocket(sd4_); sd4_ = -1; } if (sd6_ != -1) { - g_platform->CloseSocket(sd6_); + g_core->platform->CloseSocket(sd6_); sd6_ = -1; } } else { @@ -293,11 +208,11 @@ auto NetworkReader::RunThread() -> int { // This needs to be locked during any sd changes/writes. std::scoped_lock lock(sd_mutex_); if (sd4_ != -1) { - g_platform->CloseSocket(sd4_); + g_core->platform->CloseSocket(sd4_); sd4_ = -1; } if (sd6_ != -1) { - g_platform->CloseSocket(sd6_); + g_core->platform->CloseSocket(sd6_); sd6_ = -1; } break; @@ -318,7 +233,8 @@ auto NetworkReader::RunThread() -> int { std::vector s_buffer(rresult2); memcpy(s_buffer.data(), buffer + 1, rresult2 - 1); s_buffer[rresult2 - 1] = 0; // terminate string - std::string response = HandleJSONPing(s_buffer.data()); + std::string response = + g_base->app_mode->HandleJSONPing(s_buffer.data()); if (!response.empty()) { std::vector msg(1 + response.size()); msg[0] = BA_PACKET_JSON_PONG; @@ -380,14 +296,15 @@ auto NetworkReader::RunThread() -> int { // These messages are associated with udp host/client // connections.. pass them to the logic thread to wrangle. std::vector msg_buffer(rresult2); - memcpy(&(msg_buffer[0]), buffer, rresult2); - g_logic->connections()->PushUDPConnectionPacketCall( - msg_buffer, SockAddr(from)); + memcpy(msg_buffer.data(), buffer, rresult2); + PushIncomingUDPPacketCall(msg_buffer, SockAddr(from)); break; } - case BA_PACKET_GAME_QUERY: { - HandleGameQuery(buffer, rresult2, &from); + case BA_PACKET_HOST_QUERY: { + g_base->app_mode->HandleGameQuery(buffer, rresult2, &from); + + // HandleGameQuery(buffer, rresult2, &from); break; } @@ -405,11 +322,27 @@ auto NetworkReader::RunThread() -> int { } // Sleep for a moment to keep us from running wild if we're unable to block. - Platform::SleepMS(1000); + core::CorePlatform::SleepMillisecs(1000); } } -auto NetworkReader::OpenSockets() -> void { +void NetworkReader::PushIncomingUDPPacketCall(const std::vector& data, + const SockAddr& addr) { + // Avoid buffer-full errors if something is causing us to write too often; + // these are unreliable messages so its ok to just drop them. + if (!g_base->logic->event_loop()->CheckPushSafety()) { + BA_LOG_ONCE( + LogLevel::kError, + "Ignoring excessive udp-connection input packets; (could this be a " + "flood attack?)."); + return; + } + + g_base->logic->event_loop()->PushCall( + [data, addr] { g_base->app_mode->HandleIncomingUDPPacket(data, addr); }); +} + +void NetworkReader::OpenSockets() { // This needs to be locked during any socket-descriptor changes/writes. std::scoped_lock lock(sd_mutex_); @@ -417,12 +350,20 @@ auto NetworkReader::OpenSockets() -> void { int print_port_unavailable = false; int initial_requested_port = port4_; + // If we're headless then we die if our requested port(s) are unavailable; + // we're useless otherwise. But we now allow overriding this behavior via + // env var for cases where we use headless builds for data crunching. + auto suppress_env_var = + g_core->platform->GetEnv("BA_SUPPRESS_HEADLESS_PORT_IN_USE_ERROR"); + auto suppress_headless_port_in_use_error = + (suppress_env_var && *suppress_env_var == "1"); + sd4_ = socket(AF_INET, SOCK_DGRAM, 0); if (sd4_ < 0) { Log(LogLevel::kError, "Unable to open host socket; errno " - + g_platform->GetSocketErrorString()); + + g_core->platform->GetSocketErrorString()); } else { - g_platform->SetSocketNonBlocking(sd4_); + g_core->platform->SetSocketNonBlocking(sd4_); // Bind to local server port. struct sockaddr_in serv_addr {}; @@ -434,9 +375,7 @@ auto NetworkReader::OpenSockets() -> void { serv_addr.sin_port = htons(port4_); // NOLINT result = ::bind(sd4_, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); if (result != 0) { - // If we're headless then we abort here; we're useless if we don't get - // the port we wanted. - if (HeadlessMode()) { + if (g_core->HeadlessMode() && !suppress_headless_port_in_use_error) { FatalError("Unable to bind to requested udp port " + std::to_string(port4_) + " (ipv4)"); } @@ -448,7 +387,7 @@ auto NetworkReader::OpenSockets() -> void { // Wuh oh; no ipv6 for us i guess. if (result != 0) { - g_platform->CloseSocket(sd4_); + g_core->platform->CloseSocket(sd4_); sd4_ = -1; } } @@ -473,8 +412,8 @@ auto NetworkReader::OpenSockets() -> void { // available everywhere (win XP, etc) so let's do this for now. sd6_ = socket(AF_INET6, SOCK_DGRAM, 0); if (sd6_ < 0) { - Log(LogLevel::kError, - "Unable to open ipv6 socket: " + g_platform->GetSocketErrorString()); + Log(LogLevel::kError, "Unable to open ipv6 socket: " + + g_core->platform->GetSocketErrorString()); } else { // Since we're explicitly creating both a v4 and v6 socket, tell the v6 // to *not* do both itself (not sure if this is necessary; on mac it @@ -486,7 +425,7 @@ auto NetworkReader::OpenSockets() -> void { Log(LogLevel::kError, "Error setting socket as ipv6-only"); } - g_platform->SetSocketNonBlocking(sd6_); + g_core->platform->SetSocketNonBlocking(sd6_); struct sockaddr_in6 serv_addr {}; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin6_family = AF_INET6; @@ -495,7 +434,7 @@ auto NetworkReader::OpenSockets() -> void { result = ::bind(sd6_, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); if (result != 0) { - if (HeadlessMode()) { + if (g_core->HeadlessMode() && !suppress_headless_port_in_use_error) { FatalError("Unable to bind to requested udp port " + std::to_string(port6_) + " (ipv6)"); } @@ -510,7 +449,7 @@ auto NetworkReader::OpenSockets() -> void { result = ::bind(sd6_, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); if (result != 0) { // Wuh oh; no ipv6 for us i guess. - g_platform->CloseSocket(sd6_); + g_core->platform->CloseSocket(sd6_); sd6_ = -1; } } @@ -536,4 +475,4 @@ auto NetworkReader::OpenSockets() -> void { } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/networking/network_reader.h b/src/ballistica/base/networking/network_reader.h similarity index 74% rename from src/ballistica/networking/network_reader.h rename to src/ballistica/base/networking/network_reader.h index 543a84e0..a1dcd946 100644 --- a/src/ballistica/networking/network_reader.h +++ b/src/ballistica/base/networking/network_reader.h @@ -1,17 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_NETWORKING_NETWORK_READER_H_ -#define BALLISTICA_NETWORKING_NETWORK_READER_H_ +#ifndef BALLISTICA_BASE_NETWORKING_NETWORK_READER_H_ +#define BALLISTICA_BASE_NETWORKING_NETWORK_READER_H_ #include #include #include #include #include +#include -#include "ballistica/ballistica.h" +#include "ballistica/base/base.h" -namespace ballistica { +namespace ballistica::base { // A subsystem that manages the game's main network sockets. // It handles creating/destroying them as well as listening for incoming @@ -22,9 +23,9 @@ namespace ballistica { class NetworkReader { public: NetworkReader(); - auto SetPort(int port) -> void; - auto Pause() -> void; - auto Resume() -> void; + void SetPort(int port); + void OnAppPause(); + void OnAppResume(); auto port4() const { return port4_; } auto port6() const { return port6_; } auto sd_mutex() -> std::mutex& { return sd_mutex_; } @@ -32,10 +33,12 @@ class NetworkReader { auto sd6() const { return sd6_; } private: - auto CheckFDThreshold(int val) -> void; - auto OpenSockets() -> void; - auto PokeSelf() -> void; + void CheckFDThreshold(int val); + void OpenSockets(); + void PokeSelf(); auto RunThread() -> int; + void PushIncomingUDPPacketCall(const std::vector& data, + const SockAddr& addr); static auto RunThreadStatic(void* self) -> int { return static_cast(self)->RunThread(); } @@ -57,6 +60,6 @@ class NetworkReader { bool passed_fd_threshold_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_NETWORKING_NETWORK_READER_H_ +#endif // BALLISTICA_BASE_NETWORKING_NETWORK_READER_H_ diff --git a/src/ballistica/base/networking/network_writer.cc b/src/ballistica/base/networking/network_writer.cc new file mode 100644 index 00000000..51f5719b --- /dev/null +++ b/src/ballistica/base/networking/network_writer.cc @@ -0,0 +1,35 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/networking/network_writer.h" + +#include "ballistica/base/base.h" +#include "ballistica/base/networking/networking.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/networking/sockaddr.h" + +namespace ballistica::base { + +NetworkWriter::NetworkWriter() {} + +void NetworkWriter::OnMainThreadStartApp() { + // Spin up our thread. + event_loop_ = new EventLoop(EventLoopID::kNetworkWrite); + g_core->pausable_event_loops.push_back(event_loop_); +} + +void NetworkWriter::PushSendToCall(const std::vector& msg, + const SockAddr& addr) { + // Avoid buffer-full errors if something is causing us to write too often; + // these are unreliable messages so its ok to just drop them. + if (!event_loop()->CheckPushSafety()) { + BA_LOG_ONCE(LogLevel::kError, + "Excessive send-to calls in net-write-module."); + return; + } + event_loop()->PushCall([msg, addr] { + assert(g_base->network_reader); + Networking::SendTo(msg, addr); + }); +} + +} // namespace ballistica::base diff --git a/src/ballistica/base/networking/network_writer.h b/src/ballistica/base/networking/network_writer.h new file mode 100644 index 00000000..30a73999 --- /dev/null +++ b/src/ballistica/base/networking/network_writer.h @@ -0,0 +1,27 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_NETWORKING_NETWORK_WRITER_H_ +#define BALLISTICA_BASE_NETWORKING_NETWORK_WRITER_H_ + +#include + +#include "ballistica/shared/ballistica.h" + +namespace ballistica::base { + +// A subsystem handling outbound network traffic. +class NetworkWriter { + public: + NetworkWriter(); + void OnMainThreadStartApp(); + + void PushSendToCall(const std::vector& msg, const SockAddr& addr); + auto event_loop() const -> EventLoop* { return event_loop_; } + + private: + EventLoop* event_loop_{}; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_NETWORKING_NETWORK_WRITER_H_ diff --git a/src/ballistica/base/networking/networking.cc b/src/ballistica/base/networking/networking.cc new file mode 100644 index 00000000..a3c7fc11 --- /dev/null +++ b/src/ballistica/base/networking/networking.cc @@ -0,0 +1,58 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/networking/networking.h" + +#include "ballistica/base/app/app.h" +#include "ballistica/base/app/app_config.h" +#include "ballistica/base/networking/network_reader.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/networking/sockaddr.h" + +namespace ballistica::base { + +Networking::Networking() = default; + +void Networking::ApplyAppConfig() { + // Be aware this runs in the logic thread; not the main thread like + // most of our stuff. + assert(g_base->InLogicThread()); + + // Grab network settings from config and kick them over to the main + // thread to be applied. + int port = g_base->app_config->Resolve(AppConfig::IntID::kPort); + g_base->app->event_loop()->PushCall([port] { + assert(g_core->InMainThread()); + g_base->network_reader->SetPort(port); + }); + + // This is thread-safe so just applying immediately. + if (!g_core->HeadlessMode()) { + remote_server_accepting_connections_ = + g_base->app_config->Resolve(AppConfig::BoolID::kEnableRemoteApp); + } +} + +void Networking::OnAppPause() {} + +void Networking::OnAppResume() {} + +void Networking::SendTo(const std::vector& buffer, + const SockAddr& addr) { + assert(g_base->network_reader); + assert(!buffer.empty()); + + // This needs to be locked during any sd changes/writes. + std::scoped_lock lock(g_base->network_reader->sd_mutex()); + + // Only send if the relevant socket is currently up; silently ignore + // otherwise. + int sd = addr.IsV6() ? g_base->network_reader->sd6() + : g_base->network_reader->sd4(); + if (sd != -1) { + sendto(sd, (const char*)&buffer[0], + static_cast_check_fit(buffer.size()), 0, + addr.GetSockAddr(), addr.GetSockAddrLen()); + } +} + +} // namespace ballistica::base diff --git a/src/ballistica/networking/networking.h b/src/ballistica/base/networking/networking.h similarity index 63% rename from src/ballistica/networking/networking.h rename to src/ballistica/base/networking/networking.h index b169c80a..5d8ef1c7 100644 --- a/src/ballistica/networking/networking.h +++ b/src/ballistica/base/networking/networking.h @@ -1,16 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_NETWORKING_NETWORKING_H_ -#define BALLISTICA_NETWORKING_NETWORKING_H_ +#ifndef BALLISTICA_BASE_NETWORKING_NETWORKING_H_ +#define BALLISTICA_BASE_NETWORKING_NETWORKING_H_ -#include -#include -#include #include -#include "ballistica/ballistica.h" +#include "ballistica/shared/ballistica.h" -namespace ballistica { +namespace ballistica::base { // Packet types (first byte of raw udp packet). // These packets can apply to our UDP connection layer, Remote App, etc. @@ -39,9 +36,11 @@ namespace ballistica { // Used on android to wake our socket up so we can kill it. #define BA_PACKET_POKE 21 -// Local network game scanning. -#define BA_PACKET_GAME_QUERY 22 -#define BA_PACKET_GAME_QUERY_RESPONSE 23 +// Local network scanning. +#define BA_PACKET_HOST_QUERY 22 +#define BA_PACKET_HOST_QUERY_RESPONSE 23 + +// Connection/disconnection. #define BA_PACKET_CLIENT_REQUEST 24 #define BA_PACKET_CLIENT_ACCEPT 25 #define BA_PACKET_CLIENT_DENY 26 @@ -52,23 +51,25 @@ namespace ballistica { #define BA_PACKET_DISCONNECT_FROM_CLIENT_ACK 33 #define BA_PACKET_DISCONNECT_FROM_HOST_REQUEST 34 #define BA_PACKET_DISCONNECT_FROM_HOST_ACK 35 + +// Scene-packets in huffman-compressed form. #define BA_PACKET_CLIENT_GAMEPACKET_COMPRESSED 36 #define BA_PACKET_HOST_GAMEPACKET_COMPRESSED 37 -// Gamepackets are chunks of compressed data that apply specifically to a -// ballistica game connection. These packets can be provided over the UDP -// connection layer or by any other transport layer. When decompressed they have -// the following types as their first byte. NOTE - these originally shared a -// domain with BA_PACKET, but now they're independent... so need to avoid value -// clashes.. (hmm did i mean to say NO need?) -#define BA_GAMEPACKET_HANDSHAKE 15 -#define BA_GAMEPACKET_HANDSHAKE_RESPONSE 16 -#define BA_GAMEPACKET_MESSAGE 17 -#define BA_GAMEPACKET_MESSAGE_UNRELIABLE 18 -#define BA_GAMEPACKET_DISCONNECT 19 -#define BA_GAMEPACKET_KEEPALIVE 20 +// Scene-packets are chunks of data that apply specifically to a +// ballistica scene connection. These packets can be provided over the UDP +// connection layer or by some other transport layer. When decompressed +// they have the types listed below as their first byte. +// NOTE: these originally shared a domain with BA_PACKET, but now they're +// independent, so no need to avoid value clashes if new types are added. +#define BA_SCENEPACKET_HANDSHAKE 15 +#define BA_SCENEPACKET_HANDSHAKE_RESPONSE 16 +#define BA_SCENEPACKET_MESSAGE 17 +#define BA_SCENEPACKET_MESSAGE_UNRELIABLE 18 +#define BA_SCENEPACKET_DISCONNECT 19 +#define BA_SCENEPACKET_KEEPALIVE 20 -// Messages is our high level layer that sits on top of gamepackets. +// Messages is our high level layer that sits on top of scene-packets. // They can be any size and will always arrive in the order they were sent // (though ones marked unreliable may be dropped). #define BA_MESSAGE_SESSION_RESET 0 @@ -84,10 +85,9 @@ namespace ballistica { #define BA_MESSAGE_CHAT 10 #define BA_MESSAGE_PARTY_MEMBER_JOINED 11 #define BA_MESSAGE_PARTY_MEMBER_LEFT 12 - -// Hmmm; should multipart logic exist at the gamepacket layer instead?... -// A: that would require the re-send logic to be aware of multi-packet messages -// so maybe this is best. +// Hmmm; should multipart logic exist at the scenepacket layer instead?... +// A: that would require message layer re-send logic to be aware of +// multi-packet messages so maybe this is simpler. #define BA_MESSAGE_MULTIPART 13 #define BA_MESSAGE_MULTIPART_END 14 #define BA_MESSAGE_CLIENT_PLAYER_PROFILES 15 @@ -113,43 +113,31 @@ namespace ballistica { #define HUFFMAN_TRAINING_MODE 0 #endif -// Bits used by the logic thread for network communication. +// Singleton based in the main thread for wrangling network stuff. class Networking { public: + // Called in the logic thread when the app is reading its config. + void ApplyAppConfig(); + // Send a message to an address. This may block for a brief moment, so it can // be more efficient to send a SendToMessage to the NetworkWrite thread which // will do this there. static void SendTo(const std::vector& buffer, const SockAddr& addr); Networking(); - // Run a cycle of host scanning (basically sending out a broadcast packet to - // see who's out there). - void HostScanCycle(); - void EndHostScanning(); - // Called on mobile platforms when going into the background, etc // (when all networking should be shut down) - void Pause(); - void Resume(); - struct ScanResultsEntry { - std::string display_string; - std::string address; - }; - auto GetScanResults() -> std::vector; + void OnAppPause(); + void OnAppResume(); + + auto remote_server_accepting_connections() -> bool { + return remote_server_accepting_connections_; + } private: - void PruneScanResults(); - struct ScanResultsEntryPriv; - - // Note: would use an unordered_map here but gcc doesn't seem to allow - // forward declarations of their template params. - std::map scan_results_; - std::mutex scan_results_mutex_; - uint32_t next_scan_query_id_{}; - int scan_socket_{-1}; - bool running_{true}; + bool remote_server_accepting_connections_{true}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_NETWORKING_NETWORKING_H_ +#endif // BALLISTICA_BASE_NETWORKING_NETWORKING_H_ diff --git a/src/ballistica/base/platform/android/amazon/base_plat_andr_amazon.cc b/src/ballistica/base/platform/android/amazon/base_plat_andr_amazon.cc new file mode 100644 index 00000000..b42c5398 --- /dev/null +++ b/src/ballistica/base/platform/android/amazon/base_plat_andr_amazon.cc @@ -0,0 +1,18 @@ +// Copyright (c) 2011-2022 Eric Froemling + +#if BA_AMAZON_BUILD + +#include "ballistica/base/platform/android/amazon/base_plat_andr_amazon.h" + +#include +#include + +#include "ballistica/shared/ballistica.h" + +namespace ballistica::base { + +BasePlatformAndroidAmazon::BasePlatformAndroidAmazon() {} + +} // namespace ballistica::base + +#endif // BA_AMAZON_BUILD diff --git a/src/ballistica/base/platform/android/amazon/base_plat_andr_amazon.h b/src/ballistica/base/platform/android/amazon/base_plat_andr_amazon.h new file mode 100644 index 00000000..45f997de --- /dev/null +++ b/src/ballistica/base/platform/android/amazon/base_plat_andr_amazon.h @@ -0,0 +1,21 @@ +// Copyright (c) 2011-2022 Eric Froemling + +#ifndef BALLISTICA_BASE_PLATFORM_ANDROID_AMAZON_BASE_PLAT_ANDR_AMAZON_H_ +#define BALLISTICA_BASE_PLATFORM_ANDROID_AMAZON_BASE_PLAT_ANDR_AMAZON_H_ +#if BA_AMAZON_BUILD + +#include + +#include "ballistica/base/platform/android/base_platform_android.h" + +namespace ballistica::base { + +class BasePlatformAndroidAmazon : public BasePlatformAndroid { + public: + BasePlatformAndroidAmazon(); +}; + +} // namespace ballistica::base + +#endif // BA_AMAZON_BUILD +#endif // BALLISTICA_BASE_PLATFORM_ANDROID_AMAZON_BASE_PLAT_ANDR_AMAZON_H_ diff --git a/src/ballistica/base/platform/android/base_platform_android.cc b/src/ballistica/base/platform/android/base_platform_android.cc new file mode 100644 index 00000000..83059b9a --- /dev/null +++ b/src/ballistica/base/platform/android/base_platform_android.cc @@ -0,0 +1,54 @@ +// Copyright (c) 2011-2022 Eric Froemling + +#if BA_OSTYPE_ANDROID +#include "ballistica/base/platform/android/base_platform_android.h" + +#include "ballistica/core/platform/android/android_utils.h" +#include "ballistica/core/platform/android/core_platform_android.h" + +namespace ballistica::base { + +BasePlatformAndroid::BasePlatformAndroid() {} + +void BasePlatformAndroid::LoginAdapterGetSignInToken( + const std::string& login_type, int attempt_id) { + core::CorePlatformAndroid::Get(g_core)->PushAndroidCommand3( + "LOGIN_ADAPTER_GET_SIGN_IN_TOKEN", login_type.c_str(), + std::to_string(attempt_id).c_str()); +} + +void BasePlatformAndroid::LoginAdapterBackEndActiveChange( + const std::string& login_type, bool active) { + core::CorePlatformAndroid::Get(g_core)->PushAndroidCommand3( + "LOGIN_ADAPTER_BACK_END_ACTIVE_CHANGE", login_type.c_str(), + active ? "1" : "0"); +} + +void BasePlatformAndroid::DoPurchase(const std::string& item) { + core::CorePlatformAndroid::Get(g_core)->PushAndroidCommand2("PURCHASE", + item.c_str()); +} + +void BasePlatformAndroid::PurchaseAck(const std::string& purchase, + const std::string& order_id) { + core::CorePlatformAndroid::Get(g_core)->PushAndroidCommand3( + "PURCHASE_ACK", purchase.c_str(), order_id.c_str()); +} + +void BasePlatformAndroid::DoOpenURL(const std::string& url) { + JNIEnv* env = core::CorePlatformAndroid::Get(g_core)->GetEnv(); + core::ScopedJNIReferenceFrame refs(env); + auto context_class{core::CorePlatformAndroid::ContextClass()}; + jmethodID mid{env->GetStaticMethodID(context_class, "fromNativeOpenURL", + "(Ljava/lang/String;)V")}; + assert(mid); + if (mid) { + jstring jurl = core::CorePlatformAndroid::NewJString(env, url); + env->CallStaticVoidMethod(context_class, mid, jurl); + env->DeleteLocalRef(jurl); + } +} + +} // namespace ballistica::base + +#endif // BA_OSTYPE_ANDROID diff --git a/src/ballistica/base/platform/android/base_platform_android.h b/src/ballistica/base/platform/android/base_platform_android.h new file mode 100644 index 00000000..883ad9f9 --- /dev/null +++ b/src/ballistica/base/platform/android/base_platform_android.h @@ -0,0 +1,28 @@ +// Copyright (c) 2011-2022 Eric Froemling + +#ifndef BALLISTICA_BASE_PLATFORM_ANDROID_BASE_PLATFORM_ANDROID_H_ +#define BALLISTICA_BASE_PLATFORM_ANDROID_BASE_PLATFORM_ANDROID_H_ +#if BA_OSTYPE_ANDROID + +#include "ballistica/base/platform/base_platform.h" + +namespace ballistica::base { + +class BasePlatformAndroid : public BasePlatform { + public: + BasePlatformAndroid(); + void LoginAdapterGetSignInToken(const std::string& login_type, + int attempt_id) override; + void LoginAdapterBackEndActiveChange(const std::string& login_type, + bool active) override; + void DoPurchase(const std::string& item) override; + void PurchaseAck(const std::string& purchase, + const std::string& order_id) override; + void DoOpenURL(const std::string& url) override; +}; + +} // namespace ballistica::base + +#endif // BA_OSTYPE_ANDROID + +#endif // BALLISTICA_BASE_PLATFORM_ANDROID_BASE_PLATFORM_ANDROID_H_ diff --git a/src/ballistica/base/platform/android/cardboard/base_pl_an_cardboard.cc b/src/ballistica/base/platform/android/cardboard/base_pl_an_cardboard.cc new file mode 100644 index 00000000..c56646e2 --- /dev/null +++ b/src/ballistica/base/platform/android/cardboard/base_pl_an_cardboard.cc @@ -0,0 +1,9 @@ +// Copyright (c) 2011-2022 Eric Froemling + +#if BA_CARDBOARD_BUILD + +#include "ballistica/base/platform/android/cardboard/base_pl_an_cardboard.h" + +namespace ballistica::base {} // namespace ballistica::base + +#endif // BA_CARDBOARD_BUILD diff --git a/src/ballistica/base/platform/android/cardboard/base_pl_an_cardboard.h b/src/ballistica/base/platform/android/cardboard/base_pl_an_cardboard.h new file mode 100644 index 00000000..1ff7f40e --- /dev/null +++ b/src/ballistica/base/platform/android/cardboard/base_pl_an_cardboard.h @@ -0,0 +1,20 @@ +// Copyright (c) 2011-2022 Eric Froemling + +#ifndef BALLISTICA_BASE_PLATFORM_ANDROID_CARDBOARD_BASE_PL_AN_CARDBOARD_H_ +#define BALLISTICA_BASE_PLATFORM_ANDROID_CARDBOARD_BASE_PL_AN_CARDBOARD_H_ +#if BA_CARDBOARD_BUILD + +#include + +#include "ballistica/base/platform/android/base_platform_android.h" + +namespace ballistica::base { + +class BasePlatformAndroidCardboard : public BasePlatformAndroid { + public: +}; + +} // namespace ballistica::base + +#endif // BA_CARDBOARD_BUILD +#endif // BALLISTICA_BASE_PLATFORM_ANDROID_CARDBOARD_BASE_PL_AN_CARDBOARD_H_ diff --git a/src/ballistica/base/platform/android/google/base_plat_andr_google.cc b/src/ballistica/base/platform/android/google/base_plat_andr_google.cc new file mode 100644 index 00000000..efbbb48e --- /dev/null +++ b/src/ballistica/base/platform/android/google/base_plat_andr_google.cc @@ -0,0 +1,12 @@ +// Copyright (c) 2011-2022 Eric Froemling + +#if BA_GOOGLE_BUILD +#include "ballistica/base/platform/android/google/base_plat_andr_google.h" + +namespace ballistica::base { + +BasePlatformAndroidGoogle::BasePlatformAndroidGoogle() {} + +} // namespace ballistica::base + +#endif // BA_GOOGLE_BUILD diff --git a/src/ballistica/base/platform/android/google/base_plat_andr_google.h b/src/ballistica/base/platform/android/google/base_plat_andr_google.h new file mode 100644 index 00000000..63026048 --- /dev/null +++ b/src/ballistica/base/platform/android/google/base_plat_andr_google.h @@ -0,0 +1,22 @@ +// Copyright (c) 2011-2022 Eric Froemling + +#ifndef BALLISTICA_BASE_PLATFORM_ANDROID_GOOGLE_BASE_PLAT_ANDR_GOOGLE_H_ +#define BALLISTICA_BASE_PLATFORM_ANDROID_GOOGLE_BASE_PLAT_ANDR_GOOGLE_H_ +#if BA_GOOGLE_BUILD + +#include +#include + +#include "ballistica/base/platform/android/base_platform_android.h" + +namespace ballistica::base { + +class BasePlatformAndroidGoogle : public BasePlatformAndroid { + public: + BasePlatformAndroidGoogle(); +}; + +} // namespace ballistica::base + +#endif // BA_GOOGLE_BUILD +#endif // BALLISTICA_BASE_PLATFORM_ANDROID_GOOGLE_BASE_PLAT_ANDR_GOOGLE_H_ diff --git a/src/ballistica/base/platform/apple/base_platform_apple.cc b/src/ballistica/base/platform/apple/base_platform_apple.cc new file mode 100644 index 00000000..a0f984ad --- /dev/null +++ b/src/ballistica/base/platform/apple/base_platform_apple.cc @@ -0,0 +1,68 @@ +// Copyright (c) 2011-2022 Eric Froemling + +#if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS +#include "ballistica/base/platform/apple/base_platform_apple.h" + +#if BA_XCODE_BUILD +#include +#endif +#include + +#if BA_XCODE_BUILD +#include "ballistica/core/platform/apple/apple_utils.h" +#endif + +namespace ballistica::base { + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-use-equals-default" + +BasePlatformApple::BasePlatformApple() { // NOLINT: trivial constructor false + // positive + // On iOS, keep the device from falling asleep in our app +#if BA_OSTYPE_IOS_TVOS + core::AppleUtils::DisableIdleTimer(); +#endif +} + +#pragma clang diagnostic pop + +void BasePlatformApple::DoPurchase(const std::string& item) { +#if BA_USE_STORE_KIT + core::AppleUtils::DoStoreKitPurchase(item); +#else + BasePlatform::DoPurchase(item); +#endif +} + +void BasePlatformApple::RestorePurchases() { +#if BA_USE_STORE_KIT + core::AppleUtils::DoStoreKitPurchaseRestore(); +#else + BasePlatform::RestorePurchases(); +#endif +} + +void BasePlatformApple::PurchaseAck(const std::string& purchase, + const std::string& order_id) { +#if BA_XCODE_BUILD + core::AppleUtils::PurchaseAck(purchase, order_id); +#else + BasePlatform::PurchaseAck(purchase, order_id); +#endif +} + +void BasePlatformApple::DoOpenURL(const std::string& url) { +#if BA_XCODE_BUILD + // Go ahead and do this ourself. Though perhaps the default + // Python path would be fine. + core::AppleUtils::OpenURL(url.c_str()); +#else + // Otherwise go with the default (Python webbrowser module). + BasePlatform::DoOpenURL(url); +#endif +} + +} // namespace ballistica::base + +#endif // BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS diff --git a/src/ballistica/base/platform/apple/base_platform_apple.h b/src/ballistica/base/platform/apple/base_platform_apple.h new file mode 100644 index 00000000..50cf78cc --- /dev/null +++ b/src/ballistica/base/platform/apple/base_platform_apple.h @@ -0,0 +1,28 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PLATFORM_APPLE_BASE_PLATFORM_APPLE_H_ +#define BALLISTICA_BASE_PLATFORM_APPLE_BASE_PLATFORM_APPLE_H_ +#if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS + +#include "ballistica/base/platform/base_platform.h" + +namespace ballistica::base { + +class BasePlatformApple : public BasePlatform { + public: + BasePlatformApple(); + + void DoPurchase(const std::string& item) override; + void RestorePurchases() override; + void PurchaseAck(const std::string& purchase, + const std::string& order_id) override; + + void DoOpenURL(const std::string& url) override; + + private: +}; + +} // namespace ballistica::base + +#endif // BA_XCODE_BUILD || BA_OSTYPE_MACOS +#endif // BALLISTICA_BASE_PLATFORM_APPLE_BASE_PLATFORM_APPLE_H_ diff --git a/src/ballistica/base/platform/base_platform.cc b/src/ballistica/base/platform/base_platform.cc new file mode 100644 index 00000000..48b484ea --- /dev/null +++ b/src/ballistica/base/platform/base_platform.cc @@ -0,0 +1,299 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/platform/base_platform.h" + +#include + +#include "ballistica/base/base.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/platform/support/min_sdl_key_names.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/core/core.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" + +#if BA_VR_BUILD +#include "ballistica/base/app/app_vr.h" +#endif + +#if BA_HEADLESS_BUILD +#include "ballistica/base/app/app_headless.h" +#endif + +#include "ballistica/base/app/app.h" +#include "ballistica/base/app/sdl_app.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/graphics/graphics_vr.h" + +// ------------------------- PLATFORM SELECTION -------------------------------- + +// This ugly chunk of macros simply pulls in the correct platform class header +// for each platform and defines the actual class g_base->platform will be. + +// Android --------------------------------------------------------------------- + +#if BA_OSTYPE_ANDROID +#if BA_GOOGLE_BUILD +#include "ballistica/base/platform/android/google/base_plat_andr_google.h" +#define BA_PLATFORM_CLASS BasePlatformAndroidGoogle +#elif BA_AMAZON_BUILD +#include "ballistica/base/platform/android/amazon/base_plat_andr_amazon.h" +#define BA_PLATFORM_CLASS BasePlatformAndroidAmazon +#elif BA_CARDBOARD_BUILD +#include "ballistica/base/platform/android/cardboard/base_pl_an_cardboard.h" +#define BA_PLATFORM_CLASS BasePlatformAndroidCardboard +#else // Generic android. +#include "ballistica/base/platform/android/base_platform_android.h" +#define BA_PLATFORM_CLASS BasePlatformAndroid +#endif // (Android subplatform) + +// Apple ----------------------------------------------------------------------- + +#elif BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS +#include "ballistica/base/platform/apple/base_platform_apple.h" +#define BA_PLATFORM_CLASS BasePlatformApple + +// Windows --------------------------------------------------------------------- + +#elif BA_OSTYPE_WINDOWS +#if BA_RIFT_BUILD +#include "ballistica/base/platform/windows/base_platform_windows_oculus.h" +#define BA_PLATFORM_CLASS BasePlatformWindowsOculus +#else // generic windows +#include "ballistica/base/platform/windows/base_platform_windows.h" +#define BA_PLATFORM_CLASS BasePlatformWindows +#endif // windows subtype + +// Linux ----------------------------------------------------------------------- + +#elif BA_OSTYPE_LINUX +#include "ballistica/base/platform/linux/base_platform_linux.h" +#define BA_PLATFORM_CLASS BasePlatformLinux +#else + +// Generic --------------------------------------------------------------------- + +#define BA_PLATFORM_CLASS BasePlatform + +#endif + +// ----------------------- END PLATFORM SELECTION ------------------------------ + +#ifndef BA_PLATFORM_CLASS +#error no BA_PLATFORM_CLASS defined for this platform +#endif + +namespace ballistica::base { + +auto BasePlatform::CreatePlatform() -> BasePlatform* { + auto platform = new BA_PLATFORM_CLASS(); + platform->PostInit(); + assert(platform->ran_base_post_init_); + return platform; +} + +BasePlatform::BasePlatform() = default; + +void BasePlatform::PostInit() { + // Make sure any overrides remember to call us. + ran_base_post_init_ = true; +} + +BasePlatform::~BasePlatform() = default; + +auto BasePlatform::CreateApp() -> App* { + assert(g_core); + // assert(InMainThread()); + // assert(g_main_thread); + +// TEMP - need to init sdl on our legacy mac build even though its not +// technically an SDL app. Kill this once the old mac build is gone. +#if BA_LEGACY_MACOS_BUILD + SDLApp::InitSDL(); +#endif + + App* app{}; + +#if BA_HEADLESS_BUILD + app = new AppHeadless(g_core->main_event_loop()); +#elif BA_RIFT_BUILD + // Rift build can spin up in either VR or regular mode. + if (g_core->vr_mode) { + app = new AppVR(g_core->main_event_loop()); + } else { + app = new SDLApp(g_core->main_event_loop()); + } +#elif BA_CARDBOARD_BUILD + app = new AppVR(g_core->main_event_loop()); +#elif BA_SDL_BUILD + app = new SDLApp(g_core->main_event_loop()); +#else + app = new App(g_core->main_event_loop()); +#endif + + assert(app); + app->PostInit(); + return app; +} + +auto BasePlatform::CreateGraphics() -> Graphics* { +#if BA_VR_BUILD + return new GraphicsVR(); +#else + return new Graphics(); +#endif +} + +auto BasePlatform::GetKeyName(int keycode) -> std::string { + // On our actual SDL platforms we're trying to be *pure* sdl so + // call their function for this. Otherwise we call our own version + // of it which is basically the same thing (at least for now). +#if BA_SDL_BUILD && !BA_MINSDL_BUILD + return SDL_GetKeyName(static_cast(keycode)); +#elif BA_MINSDL_BUILD + return MinSDL_GetKeyName(keycode); +#else + Log(LogLevel::kWarn, "CorePlatform::GetKeyName not implemented here."); + return "?"; +#endif +} + +void BasePlatform::LoginAdapterGetSignInToken(const std::string& login_type, + int attempt_id) { + // Default implementation simply calls completion callback immediately. + g_base->logic->event_loop()->PushCall([login_type, attempt_id] { + PythonRef args(Py_BuildValue("(sss)", login_type.c_str(), + std::to_string(attempt_id).c_str(), ""), + PythonRef::kSteal); + g_base->python->objs() + .Get(BasePython::ObjID::kLoginAdapterGetSignInTokenResponseCall) + .Call(args); + }); +} + +void BasePlatform::LoginAdapterBackEndActiveChange( + const std::string& login_type, bool active) { + // Default is no-op. +} + +auto BasePlatform::GetPublicDeviceUUID() -> std::string { + assert(g_core); + + if (public_device_uuid_.empty()) { + std::list inputs{g_core->platform->GetDeviceUUIDInputs()}; + + // This UUID is supposed to change periodically, so let's plug in + // some stuff to enforce that. + inputs.emplace_back(g_core->platform->GetOSVersionString()); + + // This part gets shuffled periodically by my version-increment tools. + // We used to plug version in directly here, but that caused uuids to + // shuffle too rapidly during periods of rapid development. This + // keeps it more constant. + // __last_rand_uuid_component_shuffle_date__ 2022 12 17 + auto rand_uuid_component{"BMCJPHH0SC22KB0WVJ1RAYD68TPEXL58"}; + + inputs.emplace_back(rand_uuid_component); + auto gil{Python::ScopedInterpreterLock()}; + auto pylist{Python::StringList(inputs)}; + auto args{Python::SingleMemberTuple(pylist)}; + auto result = g_base->python->objs() + .Get(base::BasePython::ObjID::kHashStringsCall) + .Call(args); + assert(result.UnicodeCheck()); + public_device_uuid_ = result.Str(); + } + return public_device_uuid_; +} + +void BasePlatform::Purchase(const std::string& item) { + // We use alternate _c ids for consumables in some cases where + // we originally used entitlements. We are all consumables now though + // so we can purchase for different accounts. + std::string item_filtered{item}; + if (g_buildconfig.amazon_build()) { + if (item == "bundle_bones" || item == "bundle_bernard" + || item == "bundle_frosty" || item == "bundle_santa" || item == "pro" + || item == "pro_sale") { + item_filtered = item + "_c"; + } + } + DoPurchase(item_filtered); +} + +void BasePlatform::DoPurchase(const std::string& item) { + // Just print 'unavailable' by default. + g_base->python->objs().PushCall( + base::BasePython::ObjID::kUnavailableMessageCall); +} + +void BasePlatform::RestorePurchases() { + Log(LogLevel::kError, "RestorePurchases() unimplemented"); +} + +void BasePlatform::PurchaseAck(const std::string& purchase, + const std::string& order_id) { + Log(LogLevel::kError, "PurchaseAck() unimplemented"); +} + +void BasePlatform::OpenURL(const std::string& url) { + // Can't open URLs in VR - just tell the Python layer to show the url in the + // gui. + if (g_core->IsVRMode()) { + g_base->ui->ShowURL(url); + return; + } + + // Otherwise fall back to our platform-specific handler. + g_base->platform->DoOpenURL(url); +} + +void BasePlatform::DoOpenURL(const std::string& url) { + // Kick this over to logic thread so we're safe to call from anywhere. + g_base->logic->event_loop()->PushCall( + [url] { g_base->python->OpenURLWithWebBrowserModule(url); }); +} + +#if !BA_OSTYPE_WINDOWS +static void HandleSIGINT(int s) { + if (g_base->logic) { + g_base->logic->event_loop()->PushCall( + [] { g_base->logic->HandleInterruptSignal(); }); + } else { + Log(LogLevel::kError, "SigInt handler called before g_logic exists."); + } +} +#endif + +void BasePlatform::SetupInterruptHandling() { +// This default implementation covers non-windows platforms. +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + struct sigaction handler {}; + handler.sa_handler = HandleSIGINT; + sigemptyset(&handler.sa_mask); + handler.sa_flags = 0; + sigaction(SIGINT, &handler, nullptr); +#endif +} + +void BasePlatform::GetCursorPosition(float* x, float* y) { + assert(x && y); + + // By default, just use our latest event-delivered cursor position; + // this should work everywhere though perhaps might not be most optimal. + if (g_base->input == nullptr) { + *x = 0.0f; + *y = 0.0f; + return; + } + *x = g_base->input->cursor_pos_x(); + *y = g_base->input->cursor_pos_y(); +} + +} // namespace ballistica::base diff --git a/src/ballistica/base/platform/base_platform.h b/src/ballistica/base/platform/base_platform.h new file mode 100644 index 00000000..efb8c7cd --- /dev/null +++ b/src/ballistica/base/platform/base_platform.h @@ -0,0 +1,91 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PLATFORM_BASE_PLATFORM_H_ +#define BALLISTICA_BASE_PLATFORM_BASE_PLATFORM_H_ + +#include + +#include "ballistica/base/base.h" + +namespace ballistica::base { + +class BasePlatform { + public: + /// Create the proper BasePlatform subclass for the current platform. + static auto CreatePlatform() -> BasePlatform*; + + /// Create the proper App module and add it to the main_event_loop. + static auto CreateApp() -> App*; + + /// Create the appropriate Graphics subclass for the app. + static auto CreateGraphics() -> Graphics*; + +#pragma mark IN APP PURCHASES -------------------------------------------------- + + void Purchase(const std::string& item); + + // Restore purchases (currently only relevant on Apple platforms). + virtual void RestorePurchases(); + + // Purchase was ack'ed by the master-server (so can consume). + virtual void PurchaseAck(const std::string& purchase, + const std::string& order_id); + +#pragma mark ENVIRONMENT ------------------------------------------------------- + + /// Get a UUID for the current device that is meant to be publicly shared. + /// This value will change occasionally due to OS updates, app updates, or + /// other factors, so it can not be used as a permanent identifier, but it + /// should remain constant over short periods and should not be easily + /// changeable by the user, making it useful for purposes such as temporary + /// server bans or spam prevention. + auto GetPublicDeviceUUID() -> std::string; + + /// Called when the app should set itself up to intercept ctrl-c presses. + virtual void SetupInterruptHandling(); + +#pragma mark INPUT DEVICES ----------------------------------------------------- + + // Return a name for a ballistica keycode. + virtual auto GetKeyName(int keycode) -> std::string; + +#pragma mark ACCOUNTS ---------------------------------------------------------- + + /// Called when a Python LoginAdapter is requesting an explicit sign-in. + virtual void LoginAdapterGetSignInToken(const std::string& login_type, + int attempt_id); + /// Called when a Python LoginAdapter is informing us that a back-end is + /// active/inactive. + virtual void LoginAdapterBackEndActiveChange(const std::string& login_type, + bool active); +#pragma mark MISC -------------------------------------------------------------- + + /// Open the provided URL in a browser or whatnot. + void OpenURL(const std::string& url); + + /// Get the most up-to-date cursor position. + void GetCursorPosition(float* x, float* y); + + protected: + /// Open the provided URL in a browser or whatnot. + virtual void DoOpenURL(const std::string& url); + + /// Make a purchase. + virtual void DoPurchase(const std::string& item); + + BasePlatform(); + virtual ~BasePlatform(); + + private: + /// Called after our singleton has been instantiated. + /// Any construction functionality requiring virtual functions resolving to + /// their final class versions can go here. + virtual void PostInit(); + + bool ran_base_post_init_{}; + std::string public_device_uuid_; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_PLATFORM_BASE_PLATFORM_H_ diff --git a/src/ballistica/base/platform/linux/base_platform_linux.cc b/src/ballistica/base/platform/linux/base_platform_linux.cc new file mode 100644 index 00000000..7e997ad1 --- /dev/null +++ b/src/ballistica/base/platform/linux/base_platform_linux.cc @@ -0,0 +1,22 @@ +// Released under the MIT License. See LICENSE for details. + +#if BA_OSTYPE_LINUX +#include "ballistica/base/platform/linux/base_platform_linux.h" + +#include + +#include + +namespace ballistica::base { + +BasePlatformLinux::BasePlatformLinux() = default; + +void BasePlatformLinux::DoOpenURL(const std::string& url) { + // UPDATE - just relying on default Python webbrowser path now. + // (technically could kill this override). + BasePlatform::DoOpenURL(url); +} + +} // namespace ballistica::base + +#endif // BA_OSTYPE_LINUX diff --git a/src/ballistica/base/platform/linux/base_platform_linux.h b/src/ballistica/base/platform/linux/base_platform_linux.h new file mode 100644 index 00000000..9be9c1b8 --- /dev/null +++ b/src/ballistica/base/platform/linux/base_platform_linux.h @@ -0,0 +1,22 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PLATFORM_LINUX_BASE_PLATFORM_LINUX_H_ +#define BALLISTICA_BASE_PLATFORM_LINUX_BASE_PLATFORM_LINUX_H_ +#if BA_OSTYPE_LINUX + +#include + +#include "ballistica/base/platform/base_platform.h" + +namespace ballistica::base { + +class BasePlatformLinux : public BasePlatform { + public: + BasePlatformLinux(); + void DoOpenURL(const std::string& url) override; +}; + +} // namespace ballistica::base + +#endif // BA_OSTYPE_LINUX +#endif // BALLISTICA_BASE_PLATFORM_LINUX_BASE_PLATFORM_LINUX_H_ diff --git a/src/ballistica/base/platform/oculus/main_rift.cc b/src/ballistica/base/platform/oculus/main_rift.cc new file mode 100644 index 00000000..c81adc7e --- /dev/null +++ b/src/ballistica/base/platform/oculus/main_rift.cc @@ -0,0 +1,1180 @@ +// Copyright (c) 2011-2022 Eric Froemling +// Derived from code licensed as follows: + +/***************************************************************************** + +Filename : main.cpp +Content : Simple minimal VR demo +Created : December 1, 2014 +Author : Tom Heath +Copyright : Copyright 2012 Oculus, Inc. All Rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +*****************************************************************************/ +/// This sample has not yet been fully assimilated into the framework +/// and also the GL support is not quite fully there yet, hence the VR +/// is not that great! + +#if BA_RIFT_BUILD + +// #include "..OculusRoomTiny_Advanced/Common/Win32_GLAppUtil.h" +#include "external/oculus/OculusSDK/Samples/OculusRoomTiny_Advanced/Common/Win32_GLAppUtil.h" + +// Include the Oculus SDK +#include "OVR_CAPI_Audio.h" +#include "OVR_CAPI_GL.h" + +#if defined(_WIN32) +#include // for GetDefaultAdapterLuid +#pragma comment(lib, "dxgi.lib") +#endif + +#include + +#include + +#include "Functiondiscoverykeys_devpkey.h" +#include "ballistica/base/app/app_vr.h" +#include "ballistica/base/input/device/joystick_input.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/core/core.h" +#include "ballistica/core/platform/oculus/oculus_utils.h" +#include "ballistica/core/platform/support/min_sdl.h" +#include "ballistica/core/support/core_config.h" +#include "ballistica/shared/ballistica.h" + +// Keeping this around just to see what we changed. +#define OLD_STUFF 0 + +#define OUR_GLOBALS_NAMESPACE ballistica::base + +using namespace OVR; + +struct OculusTextureBuffer { + ovrSession Session; + ovrTextureSwapChain ColorTextureChain; + ovrTextureSwapChain DepthTextureChain; + GLuint fboId; + Sizei texSize; + + OculusTextureBuffer(ovrSession session, Sizei size, int sampleCount) + : Session(session), + ColorTextureChain(nullptr), + DepthTextureChain(nullptr), + fboId(0), + texSize(0, 0) { + assert(sampleCount + <= 1); // The code doesn't currently handle MSAA textures. + + texSize = size; + + // This texture isn't necessarily going to be a rendertarget, but it usually + // is. + assert(session); // No HMD? A little odd. + + ovrTextureSwapChainDesc desc = {}; + desc.Type = ovrTexture_2D; + desc.ArraySize = 1; + desc.Width = size.w; + desc.Height = size.h; + desc.MipLevels = 1; + desc.Format = OVR_FORMAT_R8G8B8A8_UNORM_SRGB; + desc.SampleCount = sampleCount; + desc.StaticImage = ovrFalse; + + { + ovrResult result = + ovr_CreateTextureSwapChainGL(Session, &desc, &ColorTextureChain); + + int length = 0; + ovr_GetTextureSwapChainLength(session, ColorTextureChain, &length); + + if (OVR_SUCCESS(result)) { + for (int i = 0; i < length; ++i) { + GLuint chainTexId; + ovr_GetTextureSwapChainBufferGL(Session, ColorTextureChain, i, + &chainTexId); + glBindTexture(GL_TEXTURE_2D, chainTexId); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + } + } + + desc.Format = OVR_FORMAT_D32_FLOAT; + + { + ovrResult result = + ovr_CreateTextureSwapChainGL(Session, &desc, &DepthTextureChain); + + int length = 0; + ovr_GetTextureSwapChainLength(session, DepthTextureChain, &length); + + if (OVR_SUCCESS(result)) { + for (int i = 0; i < length; ++i) { + GLuint chainTexId; + ovr_GetTextureSwapChainBufferGL(Session, DepthTextureChain, i, + &chainTexId); + glBindTexture(GL_TEXTURE_2D, chainTexId); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + } + } + + glGenFramebuffers(1, &fboId); + } + + ~OculusTextureBuffer() { + if (ColorTextureChain) { + ovr_DestroyTextureSwapChain(Session, ColorTextureChain); + ColorTextureChain = nullptr; + } + if (DepthTextureChain) { + ovr_DestroyTextureSwapChain(Session, DepthTextureChain); + DepthTextureChain = nullptr; + } + if (fboId) { + glDeleteFramebuffers(1, &fboId); + fboId = 0; + } + } + + Sizei GetSize() const { return texSize; } + + void SetAndClearRenderSurface() { + GLuint curColorTexId; + GLuint curDepthTexId; + { + int curIndex; + ovr_GetTextureSwapChainCurrentIndex(Session, ColorTextureChain, + &curIndex); + ovr_GetTextureSwapChainBufferGL(Session, ColorTextureChain, curIndex, + &curColorTexId); + } + { + int curIndex; + ovr_GetTextureSwapChainCurrentIndex(Session, DepthTextureChain, + &curIndex); + ovr_GetTextureSwapChainBufferGL(Session, DepthTextureChain, curIndex, + &curDepthTexId); + } + + glBindFramebuffer(GL_FRAMEBUFFER, fboId); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + curColorTexId, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, + curDepthTexId, 0); + + glViewport(0, 0, texSize.w, texSize.h); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glEnable(GL_FRAMEBUFFER_SRGB); + } + + void UnsetRenderSurface() { + glBindFramebuffer(GL_FRAMEBUFFER, fboId); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + 0, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, + 0, 0); + } + + void Commit() { + ovr_CommitTextureSwapChain(Session, ColorTextureChain); + ovr_CommitTextureSwapChain(Session, DepthTextureChain); + } +}; + +static ovrGraphicsLuid GetDefaultAdapterLuid() { + ovrGraphicsLuid luid = ovrGraphicsLuid(); + +#if defined(_WIN32) + IDXGIFactory* factory = nullptr; + + if (SUCCEEDED(CreateDXGIFactory(IID_PPV_ARGS(&factory)))) { + IDXGIAdapter* adapter = nullptr; + + if (SUCCEEDED(factory->EnumAdapters(0, &adapter))) { + DXGI_ADAPTER_DESC desc; + + adapter->GetDesc(&desc); + memcpy(&luid, &desc.AdapterLuid, sizeof(luid)); + adapter->Release(); + } + + factory->Release(); + } +#endif + + return luid; +} + +static int Compare(const ovrGraphicsLuid& lhs, const ovrGraphicsLuid& rhs) { + return memcmp(&lhs, &rhs, sizeof(ovrGraphicsLuid)); +} + +static bool inited_ballistica = false; + +namespace ballistica::base { +std::string g_rift_audio_device_name; +} + +// From windows sample code: scans through audio output devices to find +// one with a certain guid and returns its name. +// (OpenAL lets us pick devices by name so that's what we're after) +#define EXIT_ON_ERROR(hres) \ + if (FAILED(hres)) { \ + goto Exit; \ + } +#define SAFE_RELEASE(punk) \ + if ((punk) != NULL) { \ + (punk)->Release(); \ + (punk) = NULL; \ + } + +const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator); +const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator); + +std::string GetAudioDeviceNameFromGUID(WCHAR* guid) { + std::string val; + HRESULT hr = S_OK; + IMMDeviceEnumerator* pEnumerator = NULL; + IMMDeviceCollection* pCollection = NULL; + IMMDevice* pEndpoint = NULL; + IPropertyStore* pProps = NULL; + LPWSTR pwszID = NULL; + + hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, + IID_IMMDeviceEnumerator, (void**)&pEnumerator); + EXIT_ON_ERROR(hr); + + hr = pEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, + &pCollection); + EXIT_ON_ERROR(hr); + + UINT count; + hr = pCollection->GetCount(&count); + EXIT_ON_ERROR(hr); + + // Each loop prints the name of an endpoint device. + for (ULONG i = 0; i < count; i++) { + // Get pointer to endpoint number i. + hr = pCollection->Item(i, &pEndpoint); + EXIT_ON_ERROR(hr); + + // Get the endpoint ID string. + hr = pEndpoint->GetId(&pwszID); + EXIT_ON_ERROR(hr); + + hr = pEndpoint->OpenPropertyStore(STGM_READ, &pProps); + EXIT_ON_ERROR(hr); + + PROPVARIANT varName; + + // Initialize container for property value. + PropVariantInit(&varName); + + // Get the endpoint's friendly-name property. + hr = pProps->GetValue(PKEY_Device_FriendlyName, &varName); + EXIT_ON_ERROR(hr); + + // Print endpoint friendly name and endpoint ID. + // printf("Endpoint %d: \"%S\" (%S)\n", + // i, varName.pwszVal, pwszID); + + // Ff we find the one we're looking for, return its name. + if (!wcscmp(guid, pwszID)) { + std::wstring ws = varName.pwszVal; + val.assign(ws.begin(), ws.end()); + } + CoTaskMemFree(pwszID); + pwszID = NULL; + PropVariantClear(&varName); + SAFE_RELEASE(pProps); + SAFE_RELEASE(pEndpoint); + } + SAFE_RELEASE(pEnumerator); + SAFE_RELEASE(pCollection); + return val; + +Exit: + ballistica::Log(ballistica::LogLevel::kError, + "Error enumerating audio devices."); + CoTaskMemFree(pwszID); + SAFE_RELEASE(pEnumerator); + SAFE_RELEASE(pCollection); + SAFE_RELEASE(pEndpoint); + SAFE_RELEASE(pProps); + return val; +} + +// Return true to retry later (e.g. after display lost). +static bool MainLoop(bool retryCreate) { + OculusTextureBuffer* eyeRenderTexture[2] = {nullptr, nullptr}; + // DepthBuffer* eyeDepthBuffer[2] = {nullptr, nullptr}; + ovrMirrorTexture mirrorTexture = nullptr; + GLuint mirrorFBO = 0; + +#if OLD_STUFF + Scene* roomScene = nullptr; +#endif + + bool isVisible = true; + long long frameIndex = 0; + + ovrSession session; + ovrGraphicsLuid luid; + ovrResult result = ovr_Create(&session, &luid); + if (!OVR_SUCCESS(result)) return retryCreate; + + if (Compare(luid, + GetDefaultAdapterLuid())) // If luid that the Rift is on is + // not the default adapter LUID... + { + VALIDATE(false, "OpenGL supports only the default graphics adapter."); + } + + ovrHmdDesc hmdDesc = ovr_GetHmdDesc(session); + + // Setup Window and Graphics. + // Note: the mirror window can be any size, for this sample we use 1/2 the HMD + // resolution. + ovrSizei windowSize = {hmdDesc.Resolution.w / 2, hmdDesc.Resolution.h / 2}; + if (!Platform.InitDevice(windowSize.w, windowSize.h, + reinterpret_cast(&luid))) + goto Done; + + // Make eye render buffers. + for (int eye = 0; eye < 2; ++eye) { + ovrSizei idealTextureSize = ovr_GetFovTextureSize( + session, ovrEyeType(eye), hmdDesc.DefaultEyeFov[eye], 1); + eyeRenderTexture[eye] = + new OculusTextureBuffer(session, idealTextureSize, 1); + if (!eyeRenderTexture[eye]->ColorTextureChain + || !eyeRenderTexture[eye]->DepthTextureChain) { + if (retryCreate) goto Done; + VALIDATE(false, "Failed to create texture."); + } + // eyeRenderTexture[eye] = + // new TextureBuffer(session, true, true, idealTextureSize, 1, NULL, 1); + // eyeDepthBuffer[eye] = new DepthBuffer(eyeRenderTexture[eye]->GetSize(), + // 0); + // if (!eyeRenderTexture[eye]->TextureChain) { + // if (retryCreate) goto Done; + // VALIDATE(false, "Failed to create texture."); + // } + } + + ovrMirrorTextureDesc desc; + memset(&desc, 0, sizeof(desc)); + desc.Width = windowSize.w; + desc.Height = windowSize.h; + desc.Format = OVR_FORMAT_R8G8B8A8_UNORM_SRGB; + + // Create mirror texture and an FBO used to copy mirror texture to back buffer + result = ovr_CreateMirrorTextureWithOptionsGL(session, &desc, &mirrorTexture); + if (!OVR_SUCCESS(result)) { + if (retryCreate) goto Done; + VALIDATE(false, "Failed to create mirror texture."); + } + // Create mirror texture and an FBO used to copy mirror texture to back + // buffer. + // result = ovr_CreateMirrorTextureGL(session, &desc, &mirrorTexture); + // if (!OVR_SUCCESS(result)) { + // if (retryCreate) goto Done; + // VALIDATE(false, "Failed to create mirror texture."); + // } + + // Configure the mirror read buffer. + GLuint texId; + ovr_GetMirrorTextureBufferGL(session, mirrorTexture, &texId); + + glGenFramebuffers(1, &mirrorFBO); + glBindFramebuffer(GL_READ_FRAMEBUFFER, mirrorFBO); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, texId, 0); + glFramebufferRenderbuffer(GL_READ_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, + GL_RENDERBUFFER, 0); + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + + // Turn off vsync to let the compositor do its magic. + wglSwapIntervalEXT(0); + + // Figure out which audio device is the rift + // (we'll use this when bringing up the ballistica audio context). + { + WCHAR buffer[OVR_AUDIO_MAX_DEVICE_STR_SIZE]; + ovr_GetAudioDeviceOutGuidStr(buffer); + ballistica::base::g_rift_audio_device_name = + GetAudioDeviceNameFromGUID(buffer); + } + + if (!inited_ballistica) { + // Ok, fire up ballistica in vr mode. + auto config = ballistica::core::CoreConfig(); + config.vr_mode = true; + ballistica::MonolithicMain(config); + assert(OUR_GLOBALS_NAMESPACE::g_core + && OUR_GLOBALS_NAMESPACE::g_core->vr_mode); + inited_ballistica = true; + } + + // Inform ballistica of our draw size. + ballistica::base::AppVR::get()->VRSetDrawDimensions( + eyeRenderTexture[0]->GetSize().w, eyeRenderTexture[0]->GetSize().h); + +#if OLD_STUFF + // Make scene - can simplify further if needed + roomScene = new Scene(false); +#endif + + // FloorLevel will give tracking poses where the floor height is 0 + // ovr_SetTrackingOriginType(session, ovrTrackingOrigin_FloorLevel); + ovr_SetTrackingOriginType(session, ovrTrackingOrigin_EyeLevel); + + // ericf: it's recommended with eye-level origin we call this when the user is + // in a comfortable position.. hmm; when should we do that?... + ovr_RecenterTrackingOrigin(session); + + // Add our custom controller. + ballistica::base::JoystickInput* joystick = + ballistica::Object::NewDeferred( + -1, // not an sdl joystick + "Oculus-Input", // device name + false, // dont allow configuring + false); // no calibration; oculus api handles dead-zones and whatnot + + // We don't bother retaining this shared pointer; g_input will retain it + // and it'll be nicely killed when we tell it to remove it from their list. + + joystick->SetStandardExtendedButtons(); + joystick->SetStartButtonActivatesDefaultWidget( + false); // xbone controller is more of a 'menu' button + static bool a_pressed = false; + static bool b_pressed = false; + static bool x_pressed = false; + static bool y_pressed = false; + static bool menu_pressed = false; + static bool left_pressed = false; + static bool right_pressed = false; + static bool up_pressed = false; + static bool down_pressed = false; + static float touch_thumbstick_x = 0.0f; + static float touch_thumbstick_y = 0.0f; + static float xbox_thumbstick_x = 0.0f; + static float xbox_thumbstick_y = 0.0f; + static bool touch_stickbutton_right_pressed = false; + static bool touch_stickbutton_left_pressed = false; + static bool touch_stickbutton_up_pressed = false; + static bool touch_stickbutton_down_pressed = false; + static bool xbox_lshoulder_pressed = false; + static bool xbox_rshoulder_pressed = false; + static float xbox_trigger_l = 0.0f; + static float xbox_trigger_r = 0.0f; + static float touch_trigger_l = 0.0f; + static float touch_trigger_r = 0.0f; + static bool back_pressed = false; + static bool remote_enter_pressed = false; + static bool touch_controllers_present = false; + + const float touch_stickbutton_threshold = 0.5f; + + assert(OUR_GLOBALS_NAMESPACE::g_base && OUR_GLOBALS_NAMESPACE::g_base->input); + OUR_GLOBALS_NAMESPACE::g_base->input->PushAddInputDeviceCall(joystick, true); + + // Main loop + while (Platform.HandleMessages()) { + ovrInputState input_state_xbox; + ovrInputState input_state_remote; + ovrInputState input_state_touch; + + ovrSessionStatus session_status; + ovr_GetSessionStatus(session, &session_status); + + // If either we can't get controller/remote state or aren't foregrounded, + // just act as if nothing is pressed. + + if (!OVR_SUCCESS(ovr_GetInputState(session, ovrControllerType_XBox, + &input_state_xbox)) + || !session_status.IsVisible) { + input_state_xbox.Buttons = 0; + input_state_xbox.IndexTrigger[0] = 0.0f; + input_state_xbox.IndexTrigger[1] = 0.0f; + input_state_xbox.Thumbstick[0].x = 0.0f; + input_state_xbox.Thumbstick[0].y = 0.0f; + } + if (!OVR_SUCCESS(ovr_GetInputState(session, ovrControllerType_Remote, + &input_state_remote)) + || !session_status.IsVisible) { + input_state_remote.Buttons = 0; + } + if (!OVR_SUCCESS(ovr_GetInputState(session, ovrControllerType_Touch, + &input_state_touch)) + || !session_status.IsVisible) { + touch_controllers_present = false; + input_state_touch.Buttons = 0; + input_state_touch.Thumbstick[0].x = 0.0f; + input_state_touch.Thumbstick[0].y = 0.0f; + input_state_touch.Thumbstick[1].x = 0.0f; + input_state_touch.Thumbstick[1].y = 0.0f; + input_state_touch.IndexTrigger[0] = 0.0f; + input_state_touch.IndexTrigger[1] = 0.0f; + } else { + touch_controllers_present = true; + } + + // Use the right touch thumbstick as 4 fake button presses. + + // Right. + if (!touch_stickbutton_right_pressed) { + if (input_state_touch.Thumbstick[1].x > touch_stickbutton_threshold) { + touch_stickbutton_right_pressed = true; + } + } else { + if (input_state_touch.Thumbstick[1].x <= touch_stickbutton_threshold) { + touch_stickbutton_right_pressed = false; + } + } + + // Left. + if (!touch_stickbutton_left_pressed) { + if (input_state_touch.Thumbstick[1].x < -touch_stickbutton_threshold) { + touch_stickbutton_left_pressed = true; + } + } else { + if (input_state_touch.Thumbstick[1].x >= -touch_stickbutton_threshold) { + touch_stickbutton_left_pressed = false; + } + } + + // Up. + if (!touch_stickbutton_up_pressed) { + if (input_state_touch.Thumbstick[1].y > touch_stickbutton_threshold) { + touch_stickbutton_up_pressed = true; + } + } else { + if (input_state_touch.Thumbstick[1].y <= touch_stickbutton_threshold) { + touch_stickbutton_up_pressed = false; + } + } + + // Down. + if (!touch_stickbutton_down_pressed) { + if (input_state_touch.Thumbstick[1].y < -touch_stickbutton_threshold) { + touch_stickbutton_down_pressed = true; + } + } else { + if (input_state_touch.Thumbstick[1].y >= -touch_stickbutton_threshold) { + touch_stickbutton_down_pressed = false; + } + } + + { + // Back button press/release + if ((input_state_xbox.Buttons & ovrButton_Back) + || (input_state_remote.Buttons & ovrButton_Back)) { + if (!back_pressed) { + back_pressed = true; + SDL_Event e; + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 12; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + } else if (back_pressed) { + back_pressed = false; + SDL_Event e; + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 12; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Jump button (A on xbox, A or right-thumbstick-down on touch). + if ((input_state_xbox.Buttons & ovrButton_A) + || (input_state_touch.Buttons & ovrButton_A) + || touch_stickbutton_down_pressed) { + if (!a_pressed) { // press + a_pressed = true; + SDL_Event e; + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 0; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + } else if (a_pressed) { + a_pressed = false; + SDL_Event e; + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 0; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Bomb button (B on xbox, right-thumbstick-right on touch). + if ((input_state_xbox.Buttons & ovrButton_B) + || touch_stickbutton_right_pressed) { + if (!b_pressed) { // press + b_pressed = true; + SDL_Event e; + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 2; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + } else if (b_pressed) { // release + b_pressed = false; + SDL_Event e; + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 2; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Punch button (X on xbox, B or right-thumbstick-left on touch)). + if ((input_state_xbox.Buttons & ovrButton_X) + || (input_state_touch.Buttons & ovrButton_B) + || touch_stickbutton_left_pressed) { // press + if (!x_pressed) { + x_pressed = true; + SDL_Event e; + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 1; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + } else if (x_pressed) { // press + x_pressed = false; + SDL_Event e; + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 1; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Pickup button (Y on xbox, right-thumbstick-up on touch). + if ((input_state_xbox.Buttons & ovrButton_Y) + || touch_stickbutton_up_pressed) { // press + if (!y_pressed) { + y_pressed = true; + SDL_Event e; + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 3; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + } else if (y_pressed) { // release + y_pressed = false; + SDL_Event e; + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 3; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Start/menu button down/up. + if ((input_state_xbox.Buttons & ovrButton_Enter) + || (input_state_touch.Buttons & ovrButton_Enter)) { // press + if (!menu_pressed) { + menu_pressed = true; + SDL_Event e; + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 5; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + } else if (menu_pressed) { // release + menu_pressed = false; + SDL_Event e; + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 5; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Remote enter button. + if (input_state_remote.Buttons & ovrButton_Enter) { + if (!remote_enter_pressed) { + remote_enter_pressed = true; + SDL_Event e; + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 13; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + } else if (remote_enter_pressed) { + remote_enter_pressed = false; + SDL_Event e; + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 13; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Dpad left press/release. + if ((input_state_xbox.Buttons & ovrButton_Left) + || (input_state_remote.Buttons & ovrButton_Left)) { // press + if (!left_pressed) { + left_pressed = true; + SDL_Event e; + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 22; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + } else if (left_pressed) { // release + left_pressed = false; + SDL_Event e; + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 22; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Dpad right press/release. + if ((input_state_xbox.Buttons & ovrButton_Right) + || (input_state_remote.Buttons & ovrButton_Right)) { // press + if (!right_pressed) { + right_pressed = true; + SDL_Event e; + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 23; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + } else if (right_pressed) { // release + right_pressed = false; + SDL_Event e; + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 23; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Dpad up press/release. + if ((input_state_xbox.Buttons & ovrButton_Up) + || (input_state_remote.Buttons & ovrButton_Up)) { // press + if (!up_pressed) { + up_pressed = true; + SDL_Event e; + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 20; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + } else if (up_pressed) { // release + up_pressed = false; + SDL_Event e; + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 20; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Dpad down press/release. + if ((input_state_xbox.Buttons & ovrButton_Down) + || (input_state_remote.Buttons & ovrButton_Down)) { // press + if (!down_pressed) { + down_pressed = true; + SDL_Event e; + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 21; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + } else if (down_pressed) { // release + down_pressed = false; + SDL_Event e; + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 21; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Left shoulder press/release. + if (input_state_xbox.Buttons & ovrButton_LShoulder) { // press + if (!xbox_lshoulder_pressed) { + xbox_lshoulder_pressed = true; + SDL_Event e; + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 30; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + } else if (xbox_lshoulder_pressed) { // release + xbox_lshoulder_pressed = false; + SDL_Event e; + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 30; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Right shoulder press/release. + if (input_state_xbox.Buttons & ovrButton_RShoulder) { // press + if (!xbox_rshoulder_pressed) { + xbox_rshoulder_pressed = true; + SDL_Event e; + e.type = SDL_JOYBUTTONDOWN; + e.jbutton.button = 31; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + } else if (xbox_rshoulder_pressed) { // release + xbox_rshoulder_pressed = false; + SDL_Event e; + e.type = SDL_JOYBUTTONUP; + e.jbutton.button = 31; + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Xbox left analog trigger. + if (input_state_xbox.IndexTrigger[0] != xbox_trigger_l) { + xbox_trigger_l = input_state_xbox.IndexTrigger[0]; + SDL_Event e; + e.type = SDL_JOYAXISMOTION; + e.jaxis.axis = 10; + e.jaxis.value = std::max( + 0, std::min(32767, static_cast(xbox_trigger_l * 32767))); + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Xbox right analog trigger. + if (input_state_xbox.IndexTrigger[1] != xbox_trigger_r) { + xbox_trigger_r = input_state_xbox.IndexTrigger[1]; + SDL_Event e; + e.type = SDL_JOYAXISMOTION; + e.jaxis.axis = 11; + e.jaxis.value = std::max( + 0, std::min(32767, static_cast(xbox_trigger_r * 32767))); + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Touch left analog trigger. + if (input_state_touch.IndexTrigger[0] != touch_trigger_l) { + touch_trigger_l = input_state_touch.IndexTrigger[0]; + SDL_Event e; + e.type = SDL_JOYAXISMOTION; + e.jaxis.axis = 10; + e.jaxis.value = std::max( + 0, std::min(32767, static_cast(touch_trigger_l * 32767))); + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Touch right analog trigger. + if (input_state_touch.IndexTrigger[1] != touch_trigger_r) { + touch_trigger_r = input_state_touch.IndexTrigger[1]; + SDL_Event e; + e.type = SDL_JOYAXISMOTION; + e.jaxis.axis = 11; + e.jaxis.value = std::max( + 0, std::min(32767, static_cast(touch_trigger_r * 32767))); + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Xbox thumbstick. + if (input_state_xbox.Thumbstick->x != xbox_thumbstick_x) { + xbox_thumbstick_x = input_state_xbox.Thumbstick->x; + SDL_Event e; + e.type = SDL_JOYAXISMOTION; + e.jaxis.axis = 0; + e.jaxis.value = std::max( + -32767, + std::min(32767, static_cast(xbox_thumbstick_x * 32767))); + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + if (input_state_xbox.Thumbstick->y != xbox_thumbstick_y) { + xbox_thumbstick_y = input_state_xbox.Thumbstick->y; + SDL_Event e; + e.type = SDL_JOYAXISMOTION; + e.jaxis.axis = 1; + e.jaxis.value = std::max( + -32767, + std::min(32767, static_cast(xbox_thumbstick_y * -32767))); + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + + // Touch thumbstick. + if (input_state_touch.Thumbstick->x != touch_thumbstick_x) { + touch_thumbstick_x = input_state_touch.Thumbstick->x; + SDL_Event e; + e.type = SDL_JOYAXISMOTION; + e.jaxis.axis = 0; + e.jaxis.value = std::max( + -32767, + std::min(32767, static_cast(touch_thumbstick_x * 32767))); + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + if (input_state_touch.Thumbstick->y != touch_thumbstick_y) { + touch_thumbstick_y = input_state_touch.Thumbstick->y; + SDL_Event e; + e.type = SDL_JOYAXISMOTION; + e.jaxis.axis = 1; + e.jaxis.value = std::max( + -32767, + std::min(32767, static_cast(touch_thumbstick_y * -32767))); + OUR_GLOBALS_NAMESPACE::g_base->input->PushJoystickEvent(e, joystick); + } + } + +#if OLD_STUFF + // Keyboard inputs to adjust player orientation + static float Yaw(3.141592f); + if (Platform.Key[VK_LEFT]) Yaw += 0.02f; + if (Platform.Key[VK_RIGHT]) Yaw -= 0.02f; + + // Keyboard inputs to adjust player position + static Vector3f Pos2(0.0f, 0.0f, -5.0f); + if (Platform.Key['W'] || Platform.Key[VK_UP]) + Pos2 += Matrix4f::RotationY(Yaw).Transform(Vector3f(0, 0, -0.05f)); + if (Platform.Key['S'] || Platform.Key[VK_DOWN]) + Pos2 += Matrix4f::RotationY(Yaw).Transform(Vector3f(0, 0, +0.05f)); + if (Platform.Key['D']) + Pos2 += Matrix4f::RotationY(Yaw).Transform(Vector3f(+0.05f, 0, 0)); + if (Platform.Key['A']) + Pos2 += Matrix4f::RotationY(Yaw).Transform(Vector3f(-0.05f, 0, 0)); + + // Animate the cube + static float cubeClock = 0; + roomScene->Models[0]->Pos = Vector3f(9 * (float)sinf(cubeClock), 3, + 9 * (float)cosf(cubeClock += 0.015f)); + +#endif // OLD_STUFF + + // Call ovr_GetRenderDesc each frame to get the ovrEyeRenderDesc, as the + // returned values (e.g. HmdToEyeOffset) may change at runtime. + ovrEyeRenderDesc eyeRenderDesc[2]; + eyeRenderDesc[0] = + ovr_GetRenderDesc(session, ovrEye_Left, hmdDesc.DefaultEyeFov[0]); + eyeRenderDesc[1] = + ovr_GetRenderDesc(session, ovrEye_Right, hmdDesc.DefaultEyeFov[1]); + + // Get eye poses, feeding in correct IPD offset + ovrPosef EyeRenderPose[2]; + ovrPosef HmdToEyePose[2] = {eyeRenderDesc[0].HmdToEyePose, + eyeRenderDesc[1].HmdToEyePose}; + + double sensorSampleTime; // sensorSampleTime is fed into the layer later + ovr_GetEyePoses(session, frameIndex, ovrTrue, HmdToEyePose, EyeRenderPose, + &sensorSampleTime); + + ovrTimewarpProjectionDesc posTimewarpProjectionDesc = {}; + + if (isVisible) { + double HmdFrameTiming = ovr_GetPredictedDisplayTime(session, frameIndex); + ovrTrackingState trackState = + ovr_GetTrackingState(session, HmdFrameTiming, ovrFalse); + + Matrix4f m = Matrix4f(trackState.HeadPose.ThePose.Orientation); + float hRoll, hPitch, hYaw; + m.ToEulerAngles( + &hYaw, &hPitch, &hRoll); + ballistica::base::AppVR::get()->VRSetHead( + trackState.HeadPose.ThePose.Position.x, + trackState.HeadPose.ThePose.Position.y, + trackState.HeadPose.ThePose.Position.z, hYaw, hPitch, hRoll); + + // if it looks like we've got touch controllers, send their latest poses + // and states to the game for drawing/etc + if (touch_controllers_present) { + // ew; should just be passing all this stuff in as matrices; for + // whatever reason it was simpler to set up as euler angles though.. + Matrix4f m = + Matrix4f(trackState.HandPoses[ovrHand_Right].ThePose.Orientation); + float rRoll, rPitch, rYaw; + m.ToEulerAngles( + &rYaw, &rPitch, &rRoll); + m = Matrix4f(trackState.HandPoses[ovrHand_Left].ThePose.Orientation); + float lRoll, lPitch, lYaw; + m.ToEulerAngles( + &lYaw, &lPitch, &lRoll); + + ballistica::base::VRHandsState s; + s.l.type = ballistica::base::VRHandType::kOculusTouchL; + s.l.tx = trackState.HandPoses[ovrHand_Left].ThePose.Position.x; + s.l.ty = trackState.HandPoses[ovrHand_Left].ThePose.Position.y; + s.l.tz = trackState.HandPoses[ovrHand_Left].ThePose.Position.z; + s.l.yaw = lYaw; + s.l.pitch = lPitch; + s.l.roll = lRoll; + + s.r.type = ballistica::base::VRHandType::kOculusTouchR; + s.r.tx = trackState.HandPoses[ovrHand_Right].ThePose.Position.x; + s.r.ty = trackState.HandPoses[ovrHand_Right].ThePose.Position.y; + s.r.tz = trackState.HandPoses[ovrHand_Right].ThePose.Position.z; + s.r.yaw = rYaw; + s.r.pitch = rPitch; + s.r.roll = rRoll; + + ballistica::base::AppVR::get()->VRSetHands(s); + + } else { + ballistica::base::VRHandsState s; + ballistica::base::AppVR::get()->VRSetHands(s); + } + + ballistica::base::AppVR::get()->VRPreDraw(); + for (int eye = 0; eye < 2; ++eye) { + // Switch to eye render target + eyeRenderTexture[eye]->SetAndClearRenderSurface(); + // eyeRenderTexture[eye]->SetAndClearRenderSurface(eyeDepthBuffer[eye]); + + if (eye == 0 || eye == 1) { + Matrix4f m = Matrix4f(EyeRenderPose[eye].Orientation); + float roll, pitch, yaw; + m.ToEulerAngles( + &yaw, &pitch, &roll); + auto& fov(hmdDesc.DefaultEyeFov[eye]); + auto& pos(EyeRenderPose[eye].Position); + ballistica::base::AppVR::get()->VRDrawEye( + eye, yaw, pitch, roll, fov.LeftTan, fov.RightTan, fov.DownTan, + fov.UpTan, pos.x, pos.y, pos.z, 0, 0); + } +#if OLD_STUFF + else { + // Get view and projection matrices + Matrix4f rollPitchYaw = Matrix4f::RotationY(Yaw); + Matrix4f finalRollPitchYaw = + rollPitchYaw * Matrix4f(EyeRenderPose[eye].Orientation); + Vector3f finalUp = finalRollPitchYaw.Transform(Vector3f(0, 1, 0)); + Vector3f finalForward = + finalRollPitchYaw.Transform(Vector3f(0, 0, -1)); + Vector3f shiftedEyePos = + Pos2 + rollPitchYaw.Transform(EyeRenderPose[eye].Position); + + Matrix4f view = Matrix4f::LookAtRH( + shiftedEyePos, shiftedEyePos + finalForward, finalUp); + Matrix4f proj = ovrMatrix4f_Projection( + hmdDesc.DefaultEyeFov[eye], 0.2f, 1000.0f, ovrProjection_None); + + // Render world + roomScene->Render(view, proj); + } +#endif // OLD_STUFF + + Matrix4f proj = ovrMatrix4f_Projection(hmdDesc.DefaultEyeFov[eye], 0.2f, + 1000.0f, ovrProjection_None); + posTimewarpProjectionDesc = + ovrTimewarpProjectionDesc_FromProjection(proj, ovrProjection_None); + + // Avoids an error when calling SetAndClearRenderSurface during next + // iteration. Without this, during the next while loop iteration + // SetAndClearRenderSurface would bind a framebuffer with an invalid + // COLOR_ATTACHMENT0 because the texture ID associated with + // COLOR_ATTACHMENT0 had been unlocked by calling wglDXUnlockObjectsNV. + eyeRenderTexture[eye]->UnsetRenderSurface(); + + // Commit changes to the textures so they get picked up frame + eyeRenderTexture[eye]->Commit(); + } + ballistica::base::AppVR::get()->VRPostDraw(); + } else { + // If we're not visible we still wanna let our app process events and + // whatnot. + OUR_GLOBALS_NAMESPACE::g_base->app->RunRenderUpkeepCycle(); + } + + // Do distortion rendering, Present and flush/sync + + ovrLayerEyeFovDepth ld; + ld.Header.Type = ovrLayerType_EyeFovDepth; + ld.Header.Flags = + ovrLayerFlag_TextureOriginAtBottomLeft; // Because OpenGL. + ld.ProjectionDesc = posTimewarpProjectionDesc; + ld.SensorSampleTime = sensorSampleTime; + + for (int eye = 0; eye < 2; ++eye) { + ld.ColorTexture[eye] = eyeRenderTexture[eye]->ColorTextureChain; + ld.DepthTexture[eye] = eyeRenderTexture[eye]->DepthTextureChain; + ld.Viewport[eye] = Recti(eyeRenderTexture[eye]->GetSize()); + ld.Fov[eye] = hmdDesc.DefaultEyeFov[eye]; + ld.RenderPose[eye] = EyeRenderPose[eye]; + } + + ovrLayerHeader* layers = &ld.Header; + ovrResult result = + ovr_SubmitFrame(session, frameIndex, nullptr, &layers, 1); + // exit the rendering loop if submit returns an error, will retry on + // ovrError_DisplayLost + if (!OVR_SUCCESS(result)) goto Done; + + isVisible = (result == ovrSuccess); + + if (session_status.ShouldQuit) { + // Ok, we currently route quit commands to ballistica + // which results in an exit(0) at some point; we probably + // should try to tear down more gracefully. + SDL_Event e; + e.type = SDL_QUIT; + SDL_PushEvent(&e); + + // exit(0); + } + if (session_status.ShouldRecenter) { + ovr_RecenterTrackingOrigin(session); + } + + // Blit mirror texture to back buffer + glBindFramebuffer(GL_READ_FRAMEBUFFER, mirrorFBO); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + GLint w = windowSize.w; + GLint h = windowSize.h; + glBlitFramebuffer(0, h, w, 0, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST); + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + + SwapBuffers(Platform.hDC); + + frameIndex++; + } + +Done: +#if OLD_STUFF + delete roomScene; +#endif // OLD_STUFF + + if (mirrorFBO) glDeleteFramebuffers(1, &mirrorFBO); + if (mirrorTexture) ovr_DestroyMirrorTexture(session, mirrorTexture); + for (int eye = 0; eye < 2; ++eye) { + delete eyeRenderTexture[eye]; + // delete eyeDepthBuffer[eye]; + } + Platform.ReleaseDevice(); + ovr_Destroy(session); + + // Retry on ovrError_DisplayLost + return retryCreate || OVR_SUCCESS(result) || (result == ovrError_DisplayLost); + + // need a test case before allowing retry... + // return false; +} + +//------------------------------------------------------------------------------------- +// int WINAPI WinMain(HINSTANCE hinst, HINSTANCE, LPSTR, int) { + +int SDL_main(int argc, char** argv) { + bool do2d = false; + for (int i = 0; i < argc; i++) { + if (!strcmp(argv[i], "-2d")) { + do2d = true; + } + } + + // if they want 2d, hand off to our regular 2d sdl pathway.. + if (do2d) { + // Fire up ballistica with a normal non-vr config. + ballistica::MonolithicMain(ballistica::core::CoreConfig()); + assert(!OUR_GLOBALS_NAMESPACE::g_core->vr_mode); + } else { + // otherwise do VR goodness... + + // Initializes LibOVR, and the Rift + ovrResult result = ovr_Initialize(nullptr); + + VALIDATE(OVR_SUCCESS(result), "Failed to initialize libOVR."); + + VALIDATE(Platform.InitWindow(GetModuleHandle(NULL), L"BallisticaKit"), + "Failed to open window."); + // VALIDATE(Platform.InitWindow(hinst, L"BallisticaKit VR"), + // "Failed to open window."); + + Platform.Run(MainLoop); + + ovr_Shutdown(); + } + return (0); +} + +#endif // BA_RIFT_BUILD diff --git a/src/ballistica/base/platform/support/min_sdl_key_names.h b/src/ballistica/base/platform/support/min_sdl_key_names.h new file mode 100644 index 00000000..f26912c8 --- /dev/null +++ b/src/ballistica/base/platform/support/min_sdl_key_names.h @@ -0,0 +1,407 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PLATFORM_SUPPORT_MIN_SDL_KEY_NAMES_H_ +#define BALLISTICA_BASE_PLATFORM_SUPPORT_MIN_SDL_KEY_NAMES_H_ + +#include "ballistica/base/input/device/keyboard_input.h" + +namespace ballistica::base { + +// The following was pulled from sdl2 +#if BA_MINSDL_BUILD +static const char* const scancode_names[SDL_NUM_SCANCODES] = { + nullptr, + nullptr, + nullptr, + nullptr, + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + "Return", + "Escape", + "Backspace", + "Tab", + "Space", + "-", + "=", + "[", + "]", + "\\", + "#", + ";", + "'", + "`", + ",", + ".", + "/", + "CapsLock", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12", + "PrintScreen", + "ScrollLock", + "OnAppPause", + "Insert", + "Home", + "PageUp", + "Delete", + "End", + "PageDown", + "Right", + "Left", + "Down", + "Up", + "Numlock", + "Keypad /", + "Keypad *", + "Keypad -", + "Keypad +", + "Keypad Enter", + "Keypad 1", + "Keypad 2", + "Keypad 3", + "Keypad 4", + "Keypad 5", + "Keypad 6", + "Keypad 7", + "Keypad 8", + "Keypad 9", + "Keypad 0", + "Keypad .", + nullptr, + "Application", + "Power", + "Keypad =", + "F13", + "F14", + "F15", + "F16", + "F17", + "F18", + "F19", + "F20", + "F21", + "F22", + "F23", + "F24", + "Execute", + "Help", + "Menu", + "Select", + "Stop", + "Again", + "Undo", + "Cut", + "Copy", + "Paste", + "Find", + "Mute", + "VolumeUp", + "VolumeDown", + nullptr, + nullptr, + nullptr, + "Keypad ,", + "Keypad = (AS400)", + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + "AltErase", + "SysReq", + "Cancel", + "Clear", + "Prior", + "Return", + "Separator", + "Out", + "Oper", + "Clear / Again", + "CrSel", + "ExSel", + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + "Keypad 00", + "Keypad 000", + "ThousandsSeparator", + "DecimalSeparator", + "CurrencyUnit", + "CurrencySubUnit", + "Keypad (", + "Keypad )", + "Keypad {", + "Keypad }", + "Keypad Tab", + "Keypad Backspace", + "Keypad A", + "Keypad B", + "Keypad C", + "Keypad D", + "Keypad E", + "Keypad F", + "Keypad XOR", + "Keypad ^", + "Keypad %", + "Keypad <", + "Keypad >", + "Keypad &", + "Keypad &&", + "Keypad |", + "Keypad ||", + "Keypad :", + "Keypad #", + "Keypad Space", + "Keypad @", + "Keypad !", + "Keypad MemStore", + "Keypad MemRecall", + "Keypad MemClear", + "Keypad MemAdd", + "Keypad MemSubtract", + "Keypad MemMultiply", + "Keypad MemDivide", + "Keypad +/-", + "Keypad Clear", + "Keypad ClearEntry", + "Keypad Binary", + "Keypad Octal", + "Keypad Decimal", + "Keypad Hexadecimal", + nullptr, + nullptr, + "Left Ctrl", + "Left Shift", + "Left Alt", + "Left GUI", + "Right Ctrl", + "Right Shift", + "Right Alt", + "Right GUI", + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + "ModeSwitch", + "AudioNext", + "AudioPrev", + "AudioStop", + "AudioPlay", + "AudioMute", + "MediaSelect", + "WWW", + "Mail", + "Calculator", + "Computer", + "AC Search", + "AC Home", + "AC Back", + "AC Forward", + "AC Stop", + "AC Refresh", + "AC Bookmarks", + "BrightnessDown", + "BrightnessUp", + "DisplaySwitch", + "KBDIllumToggle", + "KBDIllumDown", + "KBDIllumUp", + "Eject", + "Sleep", + "App1", + "App2", + "AudioRewind", + "AudioFastForward", +}; + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" + +static char* UCS4ToUTF8(uint32_t ch, char* dst) { + auto* p = reinterpret_cast(dst); + if (ch <= 0x7F) { + *p = static_cast(ch); + ++dst; + } else if (ch <= 0x7FF) { + p[0] = static_cast(0xC0 | static_cast((ch >> 6) & 0x1F)); + p[1] = static_cast(0x80 | static_cast(ch & 0x3F)); + dst += 2; + } else if (ch <= 0xFFFF) { + p[0] = static_cast(0xE0 | static_cast((ch >> 12) & 0x0F)); + p[1] = static_cast(0x80 | static_cast((ch >> 6) & 0x3F)); + p[2] = static_cast(0x80 | static_cast(ch & 0x3F)); + dst += 3; + } else if (ch <= 0x1FFFFF) { + p[0] = static_cast(0xF0 | static_cast((ch >> 18) & 0x07)); + p[1] = static_cast(0x80 | static_cast((ch >> 12) & 0x3F)); + p[2] = static_cast(0x80 | static_cast((ch >> 6) & 0x3F)); + p[3] = static_cast(0x80 | static_cast(ch & 0x3F)); + dst += 4; + } else if (ch <= 0x3FFFFFF) { + p[0] = static_cast(0xF8 | static_cast((ch >> 24) & 0x03)); + p[1] = static_cast(0x80 | static_cast((ch >> 18) & 0x3F)); + p[2] = static_cast(0x80 | static_cast((ch >> 12) & 0x3F)); + p[3] = static_cast(0x80 | static_cast((ch >> 6) & 0x3F)); + p[4] = static_cast(0x80 | static_cast(ch & 0x3F)); + dst += 5; + } else { + p[0] = static_cast(0xFC | static_cast((ch >> 30) & 0x01)); + p[1] = static_cast(0x80 | static_cast((ch >> 24) & 0x3F)); + p[2] = static_cast(0x80 | static_cast((ch >> 18) & 0x3F)); + p[3] = static_cast(0x80 | static_cast((ch >> 12) & 0x3F)); + p[4] = static_cast(0x80 | static_cast((ch >> 6) & 0x3F)); + p[5] = static_cast(0x80 | static_cast(ch & 0x3F)); + dst += 6; + } + return dst; +} +#pragma clang diagnostic pop + +static const char* GetScancodeName(SDL_Scancode scancode) { + const char* name; + if (static_cast(scancode) < SDL_SCANCODE_UNKNOWN + || scancode >= SDL_NUM_SCANCODES) { + BA_LOG_ONCE(LogLevel::kError, + "GetScancodeName passed invalid scancode " + + std::to_string(static_cast(scancode))); + return ""; + } + + name = scancode_names[scancode]; + if (name) { + return name; + } else { + return ""; + } +} + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +auto MinSDL_GetKeyName(int keycode) -> std::string { + SDL_Keycode key{keycode}; + static char name[8]; + char* end; + + if (key & SDLK_SCANCODE_MASK) { + return GetScancodeName((SDL_Scancode)(key & ~SDLK_SCANCODE_MASK)); + } + + switch (key) { + case SDLK_RETURN: + return GetScancodeName(SDL_SCANCODE_RETURN); + case SDLK_ESCAPE: + return GetScancodeName(SDL_SCANCODE_ESCAPE); + case SDLK_BACKSPACE: + return GetScancodeName(SDL_SCANCODE_BACKSPACE); + case SDLK_TAB: + return GetScancodeName(SDL_SCANCODE_TAB); + case SDLK_SPACE: + return GetScancodeName(SDL_SCANCODE_SPACE); + case SDLK_DELETE: + return GetScancodeName(SDL_SCANCODE_DELETE); + default: + /* Unaccented letter keys on latin keyboards are normally + labeled in upper case (and probably on others like Greek or + Cyrillic too, so if you happen to know for sure, please + adapt this). */ + if (key >= 'a' && key <= 'z') { + key -= 32; + } + + end = UCS4ToUTF8(static_cast(key), name); + *end = '\0'; + return name; + } +} +#pragma clang diagnostic pop + +#endif // BA_MINSDL_BUILD + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_PLATFORM_SUPPORT_MIN_SDL_KEY_NAMES_H_ diff --git a/src/ballistica/base/platform/windows/base_platform_windows.cc b/src/ballistica/base/platform/windows/base_platform_windows.cc new file mode 100644 index 00000000..ca24bf82 --- /dev/null +++ b/src/ballistica/base/platform/windows/base_platform_windows.cc @@ -0,0 +1,67 @@ +// Released under the MIT License. See LICENSE for details. + +#if BA_OSTYPE_WINDOWS +#include "ballistica/base/platform/windows/base_platform_windows.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ballistica/base/logic/logic.h" +#include "ballistica/core/platform/windows/core_platform_windows.h" +#include "ballistica/shared/foundation/event_loop.h" + +namespace ballistica::base { + +BasePlatformWindows::BasePlatformWindows() {} + +void BasePlatformWindows::DoOpenURL(const std::string& url) { + if (explicit_bool(true)) { + // Switching to default implementation with goes through + // Python's webbrowser module. If this works well enough we can + // kill this override completely. + BasePlatform::DoOpenURL(url); + } else { + auto r = reinterpret_cast(ShellExecute( + nullptr, _T("open"), core::CorePlatformWindows::UTF8Decode(url).c_str(), + nullptr, nullptr, SW_SHOWNORMAL)); + + // This should return > 32 on success. + if (r <= 32) { + Log(LogLevel::kError, + "Error " + std::to_string(r) + " opening URL '" + url + "'"); + } + } +} + +BOOL WINAPI CtrlHandler(DWORD fdwCtrlType) { + switch (fdwCtrlType) { + case CTRL_C_EVENT: + if (g_base && g_base->logic) { + g_base->logic->event_loop()->PushCall( + [] { g_base->logic->HandleInterruptSignal(); }); + } else { + Log(LogLevel::kError, "SigInt handler called before g_logic exists."); + } + return TRUE; + + default: + return FALSE; + } +} + +void BasePlatformWindows::SetupInterruptHandling() { + // Set up Ctrl-C handling. + if (!SetConsoleCtrlHandler(CtrlHandler, TRUE)) { + Log(LogLevel::kError, "Error on SetConsoleCtrlHandler()"); + } +} + +} // namespace ballistica::base + +#endif // BA_OSTYPE_WINDOWS diff --git a/src/ballistica/base/platform/windows/base_platform_windows.h b/src/ballistica/base/platform/windows/base_platform_windows.h new file mode 100644 index 00000000..8ce9b4a3 --- /dev/null +++ b/src/ballistica/base/platform/windows/base_platform_windows.h @@ -0,0 +1,24 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PLATFORM_WINDOWS_BASE_PLATFORM_WINDOWS_H_ +#define BALLISTICA_BASE_PLATFORM_WINDOWS_BASE_PLATFORM_WINDOWS_H_ +#if BA_OSTYPE_WINDOWS + +#include +#include + +#include "ballistica/base/platform/base_platform.h" + +namespace ballistica::base { + +class BasePlatformWindows : public BasePlatform { + public: + BasePlatformWindows(); + void DoOpenURL(const std::string& url) override; + void SetupInterruptHandling() override; +}; + +} // namespace ballistica::base + +#endif // BA_OSTYPE_WINDOWS +#endif // BALLISTICA_BASE_PLATFORM_WINDOWS_BASE_PLATFORM_WINDOWS_H_ diff --git a/src/ballistica/base/platform/windows/base_platform_windows_oculus.cc b/src/ballistica/base/platform/windows/base_platform_windows_oculus.cc new file mode 100644 index 00000000..d3ea8e75 --- /dev/null +++ b/src/ballistica/base/platform/windows/base_platform_windows_oculus.cc @@ -0,0 +1,23 @@ +// Copyright (c) 2011-2022 Eric Froemling + +#if BA_RIFT_BUILD +#include "ballistica/base/platform/windows/base_platform_windows_oculus.h" + +#include "ballistica/core/platform/oculus/oculus_utils.h" + +namespace ballistica::base { + +BasePlatformWindowsOculus::BasePlatformWindowsOculus() {} + +void BasePlatformWindowsOculus::DoPurchase(const std::string& item) { + core::OculusUtils::Purchase(item); +} + +void BasePlatformWindowsOculus::PurchaseAck(const std::string& purchase, + const std::string& order_id) { + core::OculusUtils::ConsumePurchase(purchase); +} + +} // namespace ballistica::base + +#endif // BA_RIFT_BUILD diff --git a/src/ballistica/base/platform/windows/base_platform_windows_oculus.h b/src/ballistica/base/platform/windows/base_platform_windows_oculus.h new file mode 100644 index 00000000..8a411cca --- /dev/null +++ b/src/ballistica/base/platform/windows/base_platform_windows_oculus.h @@ -0,0 +1,22 @@ +// Copyright (c) 2011-2022 Eric Froemling + +#ifndef BALLISTICA_BASE_PLATFORM_WINDOWS_BASE_PLATFORM_WINDOWS_OCULUS_H_ +#define BALLISTICA_BASE_PLATFORM_WINDOWS_BASE_PLATFORM_WINDOWS_OCULUS_H_ +#if BA_RIFT_BUILD + +#include "ballistica/base/platform/windows/base_platform_windows.h" + +namespace ballistica::base { + +class BasePlatformWindowsOculus : public BasePlatformWindows { + public: + BasePlatformWindowsOculus(); + void DoPurchase(const std::string& item) override; + void PurchaseAck(const std::string& purchase, + const std::string& order_id) override; +}; + +} // namespace ballistica::base + +#endif // BA_RIFT_BUILD +#endif // BALLISTICA_BASE_PLATFORM_WINDOWS_BASE_PLATFORM_WINDOWS_OCULUS_H_ diff --git a/src/ballistica/base/python/base_python.cc b/src/ballistica/base/python/base_python.cc new file mode 100644 index 00000000..b0d2c717 --- /dev/null +++ b/src/ballistica/base/python/base_python.cc @@ -0,0 +1,540 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/python/base_python.h" + +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/python/class/python_class_app_timer.h" +#include "ballistica/base/python/class/python_class_context_call.h" +#include "ballistica/base/python/class/python_class_context_ref.h" +#include "ballistica/base/python/class/python_class_display_timer.h" +#include "ballistica/base/python/class/python_class_feature_set_data.h" +#include "ballistica/base/python/class/python_class_simple_sound.h" +#include "ballistica/base/python/class/python_class_vec3.h" +#include "ballistica/base/python/methods/python_methods_app.h" +#include "ballistica/base/python/methods/python_methods_graphics.h" +#include "ballistica/base/python/methods/python_methods_misc.h" +#include "ballistica/core/python/core_python.h" +#include "ballistica/shared/python/python_command.h" +#include "ballistica/shared/python/python_module_builder.h" + +namespace ballistica::base { + +// Declare a plain c PyInit_XXX function for our Python module; +// this is how Python inits our binary module (and by extension, our +// entire feature-set). +extern "C" auto PyInit__babase() -> PyObject* { + auto* builder = + new PythonModuleBuilder("_babase", + { + PythonMethodsApp::GetMethods(), + PythonMethodsMisc::GetMethods(), + PythonMethodsGraphics::GetMethods(), + }, + [](PyObject* module) -> int { + BA_PYTHON_TRY; + BaseFeatureSet::OnModuleExec(module); + return 0; + BA_PYTHON_INT_CATCH; + }); + return builder->Build(); +} + +BasePython::BasePython() = default; + +void BasePython::AddPythonClasses(PyObject* module) { + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PyObject* vec3 = PythonModuleBuilder::AddClass(module); + // Register our Vec3 as an abc.Sequence + // FIXME: should be able to do this in Python bootstrapping + // code. + auto register_call = + PythonRef(PyImport_ImportModule("collections.abc"), PythonRef::kSteal) + .GetAttr("Sequence") + .GetAttr("register"); + PythonRef args(Py_BuildValue("(O)", vec3), PythonRef::kSteal); + BA_PRECONDITION(register_call.Call(args).Exists()); +} + +void BasePython::ImportPythonObjs() { + // Import and grab all the Python stuff we use from C++. + // Note: Binding .inc files expect 'ObjID' and 'objs_' to be defined. +#include "ballistica/base/mgen/pyembed/binding_base.inc" +} + +void BasePython::SoftImportPlus() { + auto gil{Python::ScopedInterpreterLock()}; + auto result = PythonRef::Stolen(PyImport_ImportModule("_baplus")); + if (!result.Exists()) { + // Ignore any errors here for now. All that will matter is whether plus + // gave us its interface. + PyErr_Clear(); + } +} + +void BasePython::ReadConfig() { + // Read the config file and store the config dict for easy access. + objs().Get(ObjID::kReadConfigCall).Call(); + objs_.Store(ObjID::kConfig, *objs().Get(ObjID::kApp).GetAttr("config")); + assert(PyDict_Check(*objs().Get(ObjID::kConfig))); +} + +void BasePython::Reset() { + assert(g_base->InLogicThread()); + assert(g_base); + // FIXME: This needs updating. + g_base->graphics->ReleaseFadeEndCommand(); +} + +void BasePython::OnMainThreadStartApp() { + // Let the baenv Python module know we're starting the app. + // This allows it to make significant env modifications such as capturing + // interrupt signals or tweaking garbage collection that we may not want to + // do until we know we're actually running an app (and not just using bit of + // _babase functionality for some other purpose). + auto gil{Python::ScopedInterpreterLock()}; + auto result = g_core->python->objs() + .Get(core::CorePython::ObjID::kBaEnvOnBaBaseStartAppCall) + .Call(); + if (!result.Exists()) { + FatalError("baenv.on_babase_start_app() failed."); + } +} + +void BasePython::OnAppStart() { + assert(g_base->InLogicThread()); + + // FIXME - THIS SHOULD PROBABLY REPLACE LOGIC-COMPLETE-BOOTSTRAPPING + // g_python->InitBallisticaPython(); +} + +void BasePython::OnAppPause() { + assert(g_base->InLogicThread()); + objs().Get(BasePython::ObjID::kOnAppPauseCall).Call(); +} + +void BasePython::OnAppResume() { + assert(g_base->InLogicThread()); + objs().Get(BasePython::ObjID::kOnAppResumeCall).Call(); +} + +void BasePython::OnAppShutdown() { + assert(g_base->InLogicThread()); + objs().Get(BasePython::ObjID::kShutdownCall).Call(); +} + +void BasePython::ApplyAppConfig() { assert(g_base->InLogicThread()); } + +void BasePython::OnScreenSizeChange() { assert(g_base->InLogicThread()); } + +void BasePython::StepDisplayTime() { assert(g_base->InLogicThread()); } + +void BasePython::EnsureContextAllowsDefaultTimerTypes() { + auto& cref = g_base->CurrentContext(); + if (auto* context = cref.Get()) { + if (!context->ContextAllowsDefaultTimerTypes()) { + throw Exception( + "The current context does not allow creation of" + " default timer types. There are probably timer types specific" + " to the context that you should use instead (scene-timers, " + "base-timers, etc.)"); + } + } +} + +void BasePython::OpenURLWithWebBrowserModule(const std::string& url) { + // We need to be in the logic thread because our hook does sounds/messages + // on errors. + BA_PRECONDITION(g_base->InLogicThread()); + auto args = PythonRef::Stolen(Py_BuildValue("(s)", url.c_str())); + g_base->python->objs() + .Get(base::BasePython::ObjID::kOpenURLWithWebBrowserModuleCall) + .Call(args); +} + +// Return whether GetPyLString() will succeed for an object. +auto BasePython::IsPyLString(PyObject* o) -> bool { + assert(Python::HaveGIL()); + assert(o != nullptr); + + // FIXME: Move this to base. + assert(base::g_base); + + return (PyUnicode_Check(o) + || PyObject_IsInstance(o, objs().Get(ObjID::kLStrClass).Get())); +} + +auto BasePython::GetPyLString(PyObject* o) -> std::string { + assert(Python::HaveGIL()); + assert(o != nullptr); + + PyExcType exctype{PyExcType::kType}; + if (PyUnicode_Check(o)) { + return PyUnicode_AsUTF8(o); + } else { + // Check if its a Lstr. If so; we pull its json string representation. + int result = PyObject_IsInstance(o, objs().Get(ObjID::kLStrClass).Get()); + if (result == -1) { + PyErr_Clear(); + result = 0; + } + if (result == 1) { + // At this point its not a simple type error if something goes wonky. + // Perhaps we should try to preserve any error type raised by + // the _get_json() call... + exctype = PyExcType::kRuntime; + PythonRef get_json_call(PyObject_GetAttrString(o, "_get_json"), + PythonRef::kSteal); + if (get_json_call.CallableCheck()) { + PythonRef json = get_json_call.Call(); + if (PyUnicode_Check(json.Get())) { + return PyUnicode_AsUTF8(json.Get()); + } + } + } + } + + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + throw Exception( + "Can't get string from value: " + Python::ObjToString(o) + ".", exctype); +} + +auto BasePython::GetPyLStrings(PyObject* o) -> std::vector { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.Exists()); + Py_ssize_t size = PySequence_Fast_GET_SIZE(sequence.Get()); + PyObject** py_objects = PySequence_Fast_ITEMS(sequence.Get()); + std::vector vals(static_cast(size)); + assert(vals.size() == size); + for (Py_ssize_t i = 0; i < size; i++) { + vals[i] = GetPyLString(py_objects[i]); + } + return vals; +} + +auto BasePython::CanGetPyVector3f(PyObject* o) -> bool { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (PythonClassVec3::Check(o)) { + return true; + } + if (!PySequence_Check(o)) { + return false; + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.Exists()); // Should always work; we checked seq. + if (PySequence_Fast_GET_SIZE(sequence.Get()) != 3) { + return false; + } + return ( + Python::CanGetPyDouble(PySequence_Fast_GET_ITEM(sequence.Get(), 0)) + && Python::CanGetPyDouble(PySequence_Fast_GET_ITEM(sequence.Get(), 1)) + && Python::CanGetPyDouble(PySequence_Fast_GET_ITEM(sequence.Get(), 2))); +} + +auto BasePython::GetPyVector3f(PyObject* o) -> Vector3f { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (PythonClassVec3::Check(o)) { + return (reinterpret_cast(o))->value; + } + if (!PySequence_Check(o)) { + throw Exception("Object is not a babase.Vec3 or sequence.", + PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.Exists()); // Should always work; we checked seq. + if (PySequence_Fast_GET_SIZE(sequence.Get()) != 3) { + throw Exception("Sequence is not of size 3.", PyExcType::kValue); + } + return {Python::GetPyFloat(PySequence_Fast_GET_ITEM(sequence.Get(), 0)), + Python::GetPyFloat(PySequence_Fast_GET_ITEM(sequence.Get(), 1)), + Python::GetPyFloat(PySequence_Fast_GET_ITEM(sequence.Get(), 2))}; +} + +void BasePython::StoreEnv(PyObject* obj) { objs_.Store(ObjID::kEnv, obj); } +void BasePython::StorePreEnv(PyObject* obj) { + objs_.Store(ObjID::kPreEnv, obj); +} + +void BasePython::SetRawConfigValue(const char* name, float value) { + assert(g_base->InLogicThread()); + assert(objs().Exists(ObjID::kConfig)); + PythonRef value_obj(PyFloat_FromDouble(value), PythonRef::kSteal); + int result = PyDict_SetItemString(objs().Get(ObjID::kConfig).Get(), name, + value_obj.Get()); + if (result == -1) { + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + throw Exception("Error setting config dict value."); + } +} + +auto BasePython::GetRawConfigValue(const char* name) -> PyObject* { + assert(g_base->InLogicThread()); + assert(objs().Exists(ObjID::kConfig)); + return PyDict_GetItemString(objs().Get(ObjID::kConfig).Get(), name); +} + +auto BasePython::GetRawConfigValue(const char* name, const char* default_value) + -> std::string { + assert(g_base->InLogicThread()); + assert(objs().Exists(ObjID::kConfig)); + PyObject* value = + PyDict_GetItemString(objs().Get(ObjID::kConfig).Get(), name); + if (value == nullptr || !PyUnicode_Check(value)) { + return default_value; + } + return PyUnicode_AsUTF8(value); +} + +auto BasePython::GetRawConfigValue(const char* name, float default_value) + -> float { + assert(g_base->InLogicThread()); + assert(objs().Exists(ObjID::kConfig)); + PyObject* value = + PyDict_GetItemString(objs().Get(ObjID::kConfig).Get(), name); + if (value == nullptr) { + return default_value; + } + try { + return Python::GetPyFloat(value); + } catch (const std::exception&) { + Log(LogLevel::kError, + "expected a float for config value '" + std::string(name) + "'"); + return default_value; + } +} + +auto BasePython::GetRawConfigValue(const char* name, + std::optional default_value) + -> std::optional { + assert(g_base->InLogicThread()); + assert(objs().Exists(ObjID::kConfig)); + PyObject* value = + PyDict_GetItemString(objs().Get(ObjID::kConfig).Get(), name); + if (value == nullptr) { + return default_value; + } + try { + if (value == Py_None) { + return {}; + } + return Python::GetPyFloat(value); + } catch (const std::exception&) { + Log(LogLevel::kError, + "expected a float for config value '" + std::string(name) + "'"); + return default_value; + } +} + +auto BasePython::GetRawConfigValue(const char* name, int default_value) -> int { + assert(g_base->InLogicThread()); + assert(objs().Exists(ObjID::kConfig)); + PyObject* value = + PyDict_GetItemString(objs().Get(ObjID::kConfig).Get(), name); + if (value == nullptr) { + return default_value; + } + try { + return static_cast_check_fit(Python::GetPyInt64(value)); + } catch (const std::exception&) { + Log(LogLevel::kError, + "Expected an int value for config value '" + std::string(name) + "'."); + return default_value; + } +} + +auto BasePython::GetRawConfigValue(const char* name, bool default_value) + -> bool { + assert(g_base->InLogicThread()); + assert(objs().Exists(ObjID::kConfig)); + PyObject* value = + PyDict_GetItemString(objs().Get(ObjID::kConfig).Get(), name); + if (value == nullptr) { + return default_value; + } + try { + return Python::GetPyBool(value); + } catch (const std::exception&) { + Log(LogLevel::kError, + "Expected a bool value for config value '" + std::string(name) + "'."); + return default_value; + } +} +template +auto IsPyEnum(BasePython::ObjID enum_class_id, PyObject* obj) -> bool { + PyObject* enum_class_obj = g_base->python->objs().Get(enum_class_id).Get(); + assert(enum_class_obj != nullptr && enum_class_obj != Py_None); + return static_cast(PyObject_IsInstance(obj, enum_class_obj)); +} + +template +auto GetPyEnum(BasePython::ObjID enum_class_id, PyObject* obj) -> T { + // First, make sure what they passed is an instance of the enum class + // we want. + PyObject* enum_class_obj = g_base->python->objs().Get(enum_class_id).Get(); + assert(enum_class_obj != nullptr && enum_class_obj != Py_None); + if (!PyObject_IsInstance(obj, enum_class_obj)) { + throw Exception(Python::ObjToString(obj) + " is not an instance of " + + Python::ObjToString(enum_class_obj) + ".", + PyExcType::kType); + } + + // Now get its value as an int and make sure its in range + // (based on its kLast member in C++ land). + PythonRef value_obj(PyObject_GetAttrString(obj, "value"), PythonRef::kSteal); + if (!value_obj.Exists() || !PyLong_Check(value_obj.Get())) { + throw Exception( + Python::ObjToString(obj) + " is not a valid int-valued enum.", + PyExcType::kType); + } + auto value = PyLong_AS_LONG(value_obj.Get()); + if (value < 0 || value >= static_cast(T::kLast)) { + throw Exception( + Python::ObjToString(obj) + " is an invalid out-of-range enum value.", + PyExcType::kValue); + } + return static_cast(value); +} + +auto BasePython::GetPyEnum_Permission(PyObject* obj) -> Permission { + return GetPyEnum(BasePython::ObjID::kPermissionClass, obj); +} + +auto BasePython::GetPyEnum_SpecialChar(PyObject* obj) -> SpecialChar { + return GetPyEnum(BasePython::ObjID::kSpecialCharClass, obj); +} + +auto BasePython::GetPyEnum_TimeType(PyObject* obj) -> TimeType { + return GetPyEnum(BasePython::ObjID::kTimeTypeClass, obj); +} + +auto BasePython::GetPyEnum_TimeFormat(PyObject* obj) -> TimeFormat { + return GetPyEnum(BasePython::ObjID::kTimeFormatClass, obj); +} + +auto BasePython::IsPyEnum_InputType(PyObject* obj) -> bool { + return IsPyEnum(BasePython::ObjID::kInputTypeClass, obj); +} + +auto BasePython::GetPyEnum_InputType(PyObject* obj) -> InputType { + return GetPyEnum(BasePython::ObjID::kInputTypeClass, obj); +} + +auto BasePython::GetResource(const char* key, const char* fallback_resource, + const char* fallback_value) -> std::string { + assert(Python::HaveGIL()); + PythonRef results; + BA_PRECONDITION(key != nullptr); + const PythonRef& get_resource_call( + g_base->python->objs().Get(base::BasePython::ObjID::kGetResourceCall)); + if (fallback_value != nullptr) { + if (fallback_resource == nullptr) { + BA_PRECONDITION(key != nullptr); + PythonRef args(Py_BuildValue("(sOs)", key, Py_None, fallback_value), + PythonRef::kSteal); + + // Don't print errors. + results = get_resource_call.Call(args, PythonRef(), false); + } else { + PythonRef args( + Py_BuildValue("(sss)", key, fallback_resource, fallback_value), + PythonRef::kSteal); + + // Don't print errors. + results = get_resource_call.Call(args, PythonRef(), false); + } + } else if (fallback_resource != nullptr) { + PythonRef args(Py_BuildValue("(ss)", key, fallback_resource), + PythonRef::kSteal); + + // Don't print errors + results = get_resource_call.Call(args, PythonRef(), false); + } else { + PythonRef args(Py_BuildValue("(s)", key), PythonRef::kSteal); + + // Don't print errors. + results = get_resource_call.Call(args, PythonRef(), false); + } + if (results.Exists()) { + try { + return g_base->python->GetPyLString(results.Get()); + } catch (const std::exception&) { + Log(LogLevel::kError, + "GetResource failed for '" + std::string(key) + "'"); + + // Hmm; I guess let's just return the key to help identify/fix the + // issue?.. + return std::string(""; + } + } else { + Log(LogLevel::kError, "GetResource failed for '" + std::string(key) + "'"); + } + + // Hmm; I guess let's just return the key to help identify/fix the issue?.. + return std::string(""; +} + +auto BasePython::GetTranslation(const char* category, const char* s) + -> std::string { + assert(Python::HaveGIL()); + PythonRef results; + PythonRef args(Py_BuildValue("(ss)", category, s), PythonRef::kSteal); + // Don't print errors. + results = g_base->python->objs() + .Get(base::BasePython::ObjID::kTranslateCall) + .Call(args, PythonRef(), false); + if (results.Exists()) { + try { + return g_base->python->GetPyLString(results.Get()); + } catch (const std::exception&) { + Log(LogLevel::kError, + "GetTranslation failed for '" + std::string(category) + "'"); + return ""; + } + } else { + Log(LogLevel::kError, + "GetTranslation failed for category '" + std::string(category) + "'"); + } + return ""; +} + +void BasePython::RunDeepLink(const std::string& url) { + BA_PRECONDITION(g_base->InLogicThread()); + if (g_base->python->objs().Exists(base::BasePython::ObjID::kDeepLinkCall)) { + ScopedSetContext ssc(nullptr); + PythonRef args(Py_BuildValue("(s)", url.c_str()), PythonRef::kSteal); + g_base->python->objs() + .Get(base::BasePython::ObjID::kDeepLinkCall) + .Call(args); + } else { + Log(LogLevel::kError, "Error on deep-link call"); + } +} + +auto BasePython::DoOnce() -> bool { + std::string location = Python::GetPythonFileLocation(false); + if (do_once_locations_.find(location) != do_once_locations_.end()) { + return false; + } + do_once_locations_.insert(location); + return true; +} + +} // namespace ballistica::base diff --git a/src/ballistica/base/python/base_python.h b/src/ballistica/base/python/base_python.h new file mode 100644 index 00000000..0482f7eb --- /dev/null +++ b/src/ballistica/base/python/base_python.h @@ -0,0 +1,168 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PYTHON_BASE_PYTHON_H_ +#define BALLISTICA_BASE_PYTHON_BASE_PYTHON_H_ + +#include "ballistica/base/base.h" +#include "ballistica/shared/python/python_object_set.h" + +namespace ballistica::base { + +/// General Python support class for the base feature-set. +class BasePython { + public: + BasePython(); + + void OnMainThreadStartApp(); + void OnAppStart(); + void OnAppPause(); + void OnAppResume(); + void OnAppShutdown(); + void ApplyAppConfig(); + void OnScreenSizeChange(); + void StepDisplayTime(); + + void Reset(); + + /// Specific Python objects we hold in objs_. + enum class ObjID { + kApp, + kEnv, + kDeepLinkCall, + kGetResourceCall, + kTranslateCall, + kLStrClass, + kCallClass, + kGarbageCollectSessionEndCall, + kConfig, + kOnAppBootstrappingCompleteCall, + kResetToMainMenuCall, + kSetConfigFullscreenOnCall, + kSetConfigFullscreenOffCall, + kNotSignedInScreenMessageCall, + kConnectingToPartyMessageCall, + kRejectingInviteAlreadyInPartyMessageCall, + kConnectionFailedMessageCall, + kTemporarilyUnavailableMessageCall, + kInProgressMessageCall, + kErrorMessageCall, + kPurchaseNotValidErrorCall, + kPurchaseAlreadyInProgressErrorCall, + kGearVRControllerWarningCall, + kVROrientationResetCBMessageCall, + kVROrientationResetMessageCall, + kHandleV1CloudLogCall, + kLanguageTestToggleCall, + kAwardInControlAchievementCall, + kAwardDualWieldingAchievementCall, + kPrintCorruptFileErrorCall, + kPlayGongSoundCall, + kLaunchCoopGameCall, + kPurchasesRestoredMessageCall, + kDismissWiiRemotesWindowCall, + kUnavailableMessageCall, + kSetLastAdNetworkCall, + kNoGameCircleMessageCall, + kGooglePlayPurchasesNotAvailableMessageCall, + kGooglePlayServicesNotAvailableMessageCall, + kEmptyCall, + kPrintTraceCall, + kToggleFullscreenCall, + kReadConfigCall, + kUIRemotePressCall, + kRemoveInGameAdsMessageCall, + kOnAppPauseCall, + kOnAppResumeCall, + kQuitCall, + kShutdownCall, + kShowPostPurchaseMessageCall, + kContextError, + kNotFoundError, + kNodeNotFoundError, + kSessionTeamNotFoundError, + kInputDeviceNotFoundError, + kDelegateNotFoundError, + kSessionPlayerNotFoundError, + kWidgetNotFoundError, + kActivityNotFoundError, + kSessionNotFoundError, + kTimeFormatClass, + kTimeTypeClass, + kInputTypeClass, + kPermissionClass, + kSpecialCharClass, + kLstrFromJsonCall, + kUUIDStrCall, + kHashStringsCall, + kHaveAccountV2CredentialsCall, + kImplicitSignInCall, + kImplicitSignOutCall, + kLoginAdapterGetSignInTokenResponseCall, + kOnTooManyFileDescriptorsCall, + kPreEnv, + kOpenURLWithWebBrowserModuleCall, + kLast // Sentinel; must be at end. + }; + + void AddPythonClasses(PyObject* module); + void ImportPythonObjs(); + void ReadConfig(); + + const auto& objs() { return objs_; } + + static void EnsureContextAllowsDefaultTimerTypes(); + + static auto CanGetPyVector3f(PyObject* o) -> bool; + static auto GetPyVector3f(PyObject* o) -> Vector3f; + + void StoreEnv(PyObject* obj); + void StorePreEnv(PyObject* obj); + + void RunDeepLink(const std::string& url); + auto GetResource(const char* key, const char* fallback_resource = nullptr, + const char* fallback_value = nullptr) -> std::string; + auto GetTranslation(const char* category, const char* s) -> std::string; + + // Fetch raw values from the config dict. The default value is returned if + // the requested value is not present or not of a compatible type. + // Note: to get app config values you should generally use the bs::AppConfig + // functions (which themselves call these functions) + auto GetRawConfigValue(const char* name) + -> PyObject*; // (returns a borrowed ref) + auto GetRawConfigValue(const char* name, const char* default_value) + -> std::string; + auto GetRawConfigValue(const char* name, float default_value) -> float; + auto GetRawConfigValue(const char* name, std::optional default_value) + -> std::optional; + auto GetRawConfigValue(const char* name, int default_value) -> int; + auto GetRawConfigValue(const char* name, bool default_value) -> bool; + void SetRawConfigValue(const char* name, float value); + + static auto GetPyEnum_Permission(PyObject* obj) -> Permission; + static auto GetPyEnum_SpecialChar(PyObject* obj) -> SpecialChar; + static auto GetPyEnum_TimeType(PyObject* obj) -> TimeType; + static auto GetPyEnum_TimeFormat(PyObject* obj) -> TimeFormat; + static auto IsPyEnum_InputType(PyObject* obj) -> bool; + static auto GetPyEnum_InputType(PyObject* obj) -> InputType; + + auto IsPyLString(PyObject* o) -> bool; + auto GetPyLString(PyObject* o) -> std::string; + auto GetPyLStrings(PyObject* o) -> std::vector; + + /// Call our hook to open a url via Python's webbrowser module. + void OpenURLWithWebBrowserModule(const std::string& url); + + /// Register Python source code location and returns true if it has not + /// yet been registered. (for print-once type stuff). + auto DoOnce() -> bool; + + void SoftImportPlus(); + + private: + std::set do_once_locations_; + PythonObjectSet objs_; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_PYTHON_BASE_PYTHON_H_ diff --git a/src/ballistica/base/python/class/python_class_app_timer.cc b/src/ballistica/base/python/class/python_class_app_timer.cc new file mode 100644 index 00000000..dd75823f --- /dev/null +++ b/src/ballistica/base/python/class/python_class_app_timer.cc @@ -0,0 +1,123 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/python/class/python_class_app_timer.h" + +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/python/support/python_context_call_runnable.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" + +namespace ballistica::base { + +auto PythonClassAppTimer::type_name() -> const char* { return "AppTimer"; } + +void PythonClassAppTimer::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "babase.AppTimer"; + cls->tp_basicsize = sizeof(PythonClassAppTimer); + cls->tp_doc = + "AppTimer(time: float, call: Callable[[], Any], repeat: bool = False)\n" + "\n" + "Timers are used to run code at later points in time.\n" + "\n" + "Category: **General Utility Classes**\n" + "\n" + "This class encapsulates a timer based on app-time.\n" + "The underlying timer will be destroyed when this object is no longer\n" + "referenced. If you do not want to worry about keeping a reference to\n" + "your timer around, use the babase.apptimer() function instead to get a\n" + "one-off timer.\n" + "\n" + "##### Arguments\n" + "###### time\n" + "> Length of time in seconds that the timer will wait before firing.\n" + "\n" + "###### call\n" + "> A callable Python object. Remember that the timer will retain a\n" + "strong reference to the callable for as long as it exists, so you\n" + "may want to look into concepts such as babase.WeakCall if that is not\n" + "desired.\n" + "\n" + "###### repeat\n" + "> If True, the timer will fire repeatedly, with each successive\n" + "firing having the same delay as the first.\n" + "\n" + "##### Example\n" + "\n" + "Use a Timer object to print repeatedly for a few seconds:\n" + "... def say_it():\n" + "... babase.screenmessage('BADGER!')\n" + "... def stop_saying_it():\n" + "... global g_timer\n" + "... g_timer = None\n" + "... babase.screenmessage('MUSHROOM MUSHROOM!')\n" + "... # Create our timer; it will run as long as we have the self.t ref.\n" + "... g_timer = babase.AppTimer(0.3, say_it, repeat=True)\n" + "... # Now fire off a one-shot timer to kill it.\n" + "... babase.apptimer(3.89, stop_saying_it)\n"; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; +} + +auto PythonClassAppTimer::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) { + return nullptr; + } + + BA_PYTHON_TRY; + + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + + double length; + int repeat{}; + PyObject* call_obj{}; + static const char* kwlist[] = {"time", "call", "repeat", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "dO|p", + const_cast(kwlist), &length, + &call_obj, &repeat)) { + return nullptr; + } + BasePython::EnsureContextAllowsDefaultTimerTypes(); + if (length < 0) { + throw Exception("Timer length cannot be < 0.", PyExcType::kValue); + } + + auto runnable(Object::New(call_obj)); + + self->timer_id_ = g_base->logic->NewAppTimer( + static_cast(length * 1000.0), repeat, + Object::New(call_obj)); + + self->have_timer_ = true; + + return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; +} + +void PythonClassAppTimer::tp_dealloc(PythonClassAppTimer* self) { + BA_PYTHON_TRY; + + // These have to be deleted in the logic thread. + if (g_base->InLogicThread()) { + g_base->logic->DeleteAppTimer(self->timer_id_); + } else { + g_base->logic->event_loop()->PushCall( + [tid = self->timer_id_] { g_base->logic->DeleteAppTimer(tid); }); + } + + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +PyTypeObject PythonClassAppTimer::type_obj; + +} // namespace ballistica::base diff --git a/src/ballistica/base/python/class/python_class_app_timer.h b/src/ballistica/base/python/class/python_class_app_timer.h new file mode 100644 index 00000000..7295988a --- /dev/null +++ b/src/ballistica/base/python/class/python_class_app_timer.h @@ -0,0 +1,30 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_APP_TIMER_H_ +#define BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_APP_TIMER_H_ + +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::base { + +class PythonClassAppTimer : public PythonClass { + public: + static auto type_name() -> const char*; + static void SetupType(PyTypeObject* cls); + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + + private: + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassAppTimer* self); + int timer_id_{}; + bool have_timer_{}; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_APP_TIMER_H_ diff --git a/src/ballistica/python/class/python_class_context_call.cc b/src/ballistica/base/python/class/python_class_context_call.cc similarity index 56% rename from src/ballistica/python/class/python_class_context_call.cc rename to src/ballistica/base/python/class/python_class_context_call.cc index 01d7c50b..3dad799a 100644 --- a/src/ballistica/python/class/python_class_context_call.cc +++ b/src/ballistica/base/python/class/python_class_context_call.cc @@ -1,19 +1,24 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/python/class/python_class_context_call.h" +#include "ballistica/base/python/class/python_class_context_call.h" -#include "ballistica/core/thread.h" -#include "ballistica/logic/logic.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_context_call.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" -namespace ballistica { +namespace ballistica::base { -void PythonClassContextCall::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_name = "ba.ContextCall"; - obj->tp_basicsize = sizeof(PythonClassContextCall); - obj->tp_doc = +auto PythonClassContextCall::type_name() -> const char* { + return "ContextCall"; +} + +void PythonClassContextCall::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "babase.ContextCall"; + cls->tp_basicsize = sizeof(PythonClassContextCall); + cls->tp_doc = "ContextCall(call: Callable)\n" "\n" "A context-preserving callable.\n" @@ -21,24 +26,25 @@ void PythonClassContextCall::SetupType(PyTypeObject* obj) { "Category: **General Utility Classes**\n" "\n" "A ContextCall wraps a callable object along with a reference\n" - "to the current context (see ba.Context); it handles restoring the\n" - "context when run and automatically clears itself if the context\n" - "it belongs to shuts down.\n" + "to the current context (see babase.ContextRef); it handles restoring\n" + "the context when run and automatically clears itself if the context\n" + "it belongs to dies.\n" "\n" "Generally you should not need to use this directly; all standard\n" "Ballistica callbacks involved with timers, materials, UI functions,\n" - "etc. handle this under-the-hood you don't have to worry about it.\n" + "etc. handle this under-the-hood so you don't have to worry about it.\n" "The only time it may be necessary is if you are implementing your\n" "own callbacks, such as a worker thread that does some action and then\n" "runs some game code when done. By wrapping said callback in one of\n" "these, you can ensure that you will not inadvertently be keeping the\n" "current activity alive or running code in a torn-down (expired)\n" - "context.\n" + "context_ref.\n" "\n" - "You can also use ba.WeakCall for similar functionality, but\n" - "ContextCall has the added bonus that it will not run during context\n" - "shutdown, whereas ba.WeakCall simply looks at whether the target\n" - "object still exists.\n" + "You can also use babase.WeakCall for similar functionality, but\n" + "ContextCall has the added bonus that it will not run during " + "context_ref\n" + "shutdown, whereas babase.WeakCall simply looks at whether the target\n" + "object instance still exists.\n" "\n" "##### Examples\n" "**Example A:** code like this can inadvertently prevent our activity\n" @@ -53,13 +59,36 @@ void PythonClassContextCall::SetupType(PyTypeObject* obj) { "to our activity.\n" "\n" ">>> start_long_action(\n" - "... callback_when_done=ba.ContextCall(self.mycallback))\n"; + "... callback_when_done=babase.ContextCall(self.mycallback))\n"; - obj->tp_new = tp_new; - obj->tp_dealloc = (destructor)tp_dealloc; - obj->tp_repr = (reprfunc)tp_repr; - obj->tp_methods = tp_methods; - obj->tp_call = (ternaryfunc)tp_call; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_methods = tp_methods; + cls->tp_call = (ternaryfunc)tp_call; +} + +auto PythonClassContextCall::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = + reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + + PyObject* source_obj = Py_None; + if (!PyArg_ParseTuple(args, "O", &source_obj)) return nullptr; + if (!g_base->InLogicThread()) { + throw Exception( + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + self->context_call_ = new Object::Ref( + Object::New(source_obj)); + return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; } auto PythonClassContextCall::tp_call(PythonClassContextCall* self, @@ -80,7 +109,7 @@ auto PythonClassContextCall::tp_call(PythonClassContextCall* self, auto PythonClassContextCall::tp_repr(PythonClassContextCall* self) -> PyObject* { BA_PYTHON_TRY; - assert(self->context_call_->exists()); + assert(self->context_call_->Exists()); return PyUnicode_FromString( ("") @@ -88,38 +117,13 @@ auto PythonClassContextCall::tp_repr(PythonClassContextCall* self) BA_PYTHON_CATCH; } -auto PythonClassContextCall::tp_new(PyTypeObject* type, PyObject* args, - PyObject* keywds) -> PyObject* { - auto* self = - reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; - - // try to do anything that may throw an exception before/during our - // placement-new so we don't have to worry about tearing it down if - // something goes wrong afterwards - PyObject* source_obj = Py_None; - if (!PyArg_ParseTuple(args, "O", &source_obj)) return nullptr; - if (!InLogicThread()) { - throw Exception( - std::string(type_obj.tp_name) - + " objects must only be created in the logic thread (current is (" - + GetCurrentThreadName() + ")."); - } - self->context_call_ = new Object::Ref( - Object::New(source_obj)); - BA_PYTHON_NEW_CATCH; - } - return reinterpret_cast(self); -} - void PythonClassContextCall::tp_dealloc(PythonClassContextCall* self) { BA_PYTHON_TRY; - // these have to be deleted in the logic thread - send the ptr along if need - // be; otherwise do it immediately - if (!InLogicThread()) { - Object::Ref* c = self->context_call_; - g_logic->thread()->PushCall([c] { delete c; }); + // These have to be deleted in the logic thread - send the ptr along if need + // be; otherwise can do it immediately. + if (!g_base->InLogicThread()) { + Object::Ref* ptr = self->context_call_; + g_base->logic->event_loop()->PushCall([ptr] { delete ptr; }); } else { delete self->context_call_; } @@ -130,4 +134,4 @@ void PythonClassContextCall::tp_dealloc(PythonClassContextCall* self) { PyTypeObject PythonClassContextCall::type_obj; PyMethodDef PythonClassContextCall::tp_methods[] = {{nullptr}}; -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/python/class/python_class_context_call.h b/src/ballistica/base/python/class/python_class_context_call.h similarity index 54% rename from src/ballistica/python/class/python_class_context_call.h rename to src/ballistica/base/python/class/python_class_context_call.h index 34e830fd..8d0c3020 100644 --- a/src/ballistica/python/class/python_class_context_call.h +++ b/src/ballistica/base/python/class/python_class_context_call.h @@ -1,17 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_CALL_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_CALL_H_ +#ifndef BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_CALL_H_ +#define BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_CALL_H_ -#include "ballistica/core/object.h" -#include "ballistica/python/class/python_class.h" +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_class.h" -namespace ballistica { +namespace ballistica::base { class PythonClassContextCall : public PythonClass { public: - static auto type_name() -> const char* { return "ContextCall"; } - static void SetupType(PyTypeObject* obj); + static auto type_name() -> const char*; + static void SetupType(PyTypeObject* cls); static auto Check(PyObject* o) -> bool { return PyObject_TypeCheck(o, &type_obj); } @@ -25,9 +26,9 @@ class PythonClassContextCall : public PythonClass { static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) -> PyObject*; static void tp_dealloc(PythonClassContextCall* self); - Object::Ref* context_call_; + Object::Ref* context_call_{}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_CALL_H_ +#endif // BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_CALL_H_ diff --git a/src/ballistica/base/python/class/python_class_context_ref.cc b/src/ballistica/base/python/class/python_class_context_ref.cc new file mode 100644 index 00000000..e32fb729 --- /dev/null +++ b/src/ballistica/base/python/class/python_class_context_ref.cc @@ -0,0 +1,229 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/python/class/python_class_context_ref.h" + +#include "ballistica/base/logic/logic.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" + +namespace ballistica::base { + +auto PythonClassContextRef::type_name() -> const char* { return "ContextRef"; } + +void PythonClassContextRef::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "babase.ContextRef"; + cls->tp_basicsize = sizeof(PythonClassContextRef); + cls->tp_doc = + "ContextRef()\n" + "\n" + "Store or use a ballistica context.\n" + "\n" + "Category: **General Utility Classes**\n" + "\n" + "Many operations such as bascenev1.newnode() or bascenev1.gettexture()\n" + "operate implicitly on a current 'context'. A context is some sort of\n" + "state that functionality can implicitly use. Context determines, for\n" + "example, which scene nodes or textures get added to without having to\n" + "specify it explicitly in the newnode()/gettexture() call. Contexts can\n" + "also affect object lifecycles; for example a babase.ContextCall will\n" + "become a no-op when the context it was created in is destroyed.\n" + "\n" + "In general, if you are a modder, you should not need to worry about\n" + "contexts; mod code should mostly be getting run in the correct\n" + "context and timers and other callbacks will take care of saving\n" + "and restoring contexts automatically. There may be rare cases,\n" + "however, where you need to deal directly with contexts, and that is\n" + "where this class comes in.\n" + "\n" + "Creating a babase.ContextRef() will capture a reference to the current\n" + "context. Other modules may provide ways to access their contexts; for\n" + "example a bascenev1.Activity instance has a 'context' attribute. You\n" + "can also use babase.ContextRef.empty() to create a reference to *no*\n" + "context. Some code such as UI calls may expect this and may complain\n" + "if you try to use them within a context.\n" + "\n" + "##### Usage\n" + "ContextRefs are generally used with the Python 'with' statement, which\n" + "sets the context they point to as current on entry and resets it to\n" + "the previous value on exit.\n" + "\n" + "##### Example\n" + "Explicitly create a few UI bits with no context set.\n" + "(UI stuff may complain if called within a context):\n" + ">>> with bui.ContextRef.empty():\n" + "... my_container = bui.containerwidget()\n"; + + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_richcompare = (richcmpfunc)tp_richcompare; + cls->tp_methods = tp_methods; +} + +auto PythonClassContextRef::tp_repr(PythonClassContextRef* self) -> PyObject* { + BA_PYTHON_TRY; + + auto context_str = + "context_ref_->GetDescription() + ")>"; + return PyUnicode_FromString(context_str.c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassContextRef::tp_richcompare(PythonClassContextRef* c1, + PyObject* c2, int op) -> PyObject* { + // always return false against other types + if (!Check(c2)) { + Py_RETURN_FALSE; + } + bool eq = (*c1->context_ref_ + == *reinterpret_cast(c2)->context_ref_); + if (op == Py_EQ) { + if (eq) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + } else if (op == Py_NE) { + if (!eq) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + } else { + // don't support other ops + Py_RETURN_NOTIMPLEMENTED; + } +} + +auto PythonClassContextRef::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + PythonClassContextRef* self{}; + BA_PYTHON_TRY; + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + + auto cs = g_base->CurrentContext(); + + self = reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) { + return nullptr; + } + self->context_ref_ = new ContextRef(cs); + self->context_ref_prev_ = new ContextRef(); + return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; +} + +void PythonClassContextRef::tp_dealloc(PythonClassContextRef* self) { + BA_PYTHON_TRY; + // Contexts have to be deleted in the logic thread; + // ship them to it for deletion if need be; otherwise do it immediately. + if (!g_base->InLogicThread()) { + ContextRef* c = self->context_ref_; + ContextRef* c2 = self->context_ref_prev_; + g_base->logic->event_loop()->PushCall([c, c2] { + delete c; + delete c2; + }); + } else { + delete self->context_ref_; + delete self->context_ref_prev_; + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +auto PythonClassContextRef::Enter(PythonClassContextRef* self) -> PyObject* { + BA_PYTHON_TRY; + *self->context_ref_prev_ = g_base->CurrentContext(); + g_base->SetCurrentContext(*self->context_ref_); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassContextRef::Exit(PythonClassContextRef* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + g_base->SetCurrentContext(*self->context_ref_prev_); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassContextRef::Create(Context* context) -> PyObject* { + assert(g_base->InLogicThread()); + assert(TypeIsSetUp(&type_obj)); + auto* py_cref = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + if (!py_cref) { + throw Exception("ContextRef creation failed."); + } + py_cref->context_ref_->SetTarget(context); + return reinterpret_cast(py_cref); +} + +auto PythonClassContextRef::Empty(PyObject* cls, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + return Create(nullptr); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassContextRef::IsEmpty(PythonClassContextRef* self) -> PyObject* { + BA_PYTHON_TRY; + if (self->context_ref_->IsEmpty()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +auto PythonClassContextRef::IsExpired(PythonClassContextRef* self) + -> PyObject* { + BA_PYTHON_TRY; + if (self->context_ref_->IsExpired()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +PyTypeObject PythonClassContextRef::type_obj; +PyMethodDef PythonClassContextRef::tp_methods[] = { + {"__enter__", (PyCFunction)Enter, METH_NOARGS, + "enter call for 'with' functionality"}, + {"__exit__", (PyCFunction)Exit, METH_VARARGS, + "exit call for 'with' functionality"}, + {"empty", (PyCFunction)Empty, METH_VARARGS | METH_CLASS, + "empty() -> ContextRef\n" + "\n" + "Return a ContextRef pointing to no context.\n" + "\n" + "This is useful when code should be run free of a context.\n" + "For example, UI code generally insists on being run this way.\n" + "Otherwise, callbacks set on the UI could inadvertently stop working\n" + "due to a game activity ending, which would be unintuitive " + "behavior."}, + {"is_empty", (PyCFunction)IsEmpty, METH_NOARGS, + "is_empty() -> bool\n" + "\n" + "Whether the context was created as empty."}, + {"is_expired", (PyCFunction)IsExpired, METH_NOARGS, + "is_expired() -> bool\n" + "\n" + "Whether the context has expired."}, + {nullptr}}; + +} // namespace ballistica::base diff --git a/src/ballistica/base/python/class/python_class_context_ref.h b/src/ballistica/base/python/class/python_class_context_ref.h new file mode 100644 index 00000000..b325dfca --- /dev/null +++ b/src/ballistica/base/python/class/python_class_context_ref.h @@ -0,0 +1,41 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_REF_H_ +#define BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_REF_H_ + +#include "ballistica/base/support/context.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::base { + +class PythonClassContextRef : public PythonClass { + public: + static auto type_name() -> const char*; + static void SetupType(PyTypeObject* cls); + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + auto context_ref() const -> const ContextRef& { return *context_ref_; } + static auto Create(Context* context) -> PyObject*; + + private: + static PyMethodDef tp_methods[]; + static auto tp_repr(PythonClassContextRef* self) -> PyObject*; + static auto tp_richcompare(PythonClassContextRef* c1, PyObject* c2, int op) + -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassContextRef* self); + static auto Enter(PythonClassContextRef* self) -> PyObject*; + static auto Exit(PythonClassContextRef* self, PyObject* args) -> PyObject*; + static auto Empty(PyObject* cls, PyObject* args) -> PyObject*; + static auto IsEmpty(PythonClassContextRef* self) -> PyObject*; + static auto IsExpired(PythonClassContextRef* self) -> PyObject*; + ContextRef* context_ref_{}; + ContextRef* context_ref_prev_{}; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_REF_H_ diff --git a/src/ballistica/base/python/class/python_class_display_timer.cc b/src/ballistica/base/python/class/python_class_display_timer.cc new file mode 100644 index 00000000..11c7654c --- /dev/null +++ b/src/ballistica/base/python/class/python_class_display_timer.cc @@ -0,0 +1,132 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/python/class/python_class_display_timer.h" + +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/python/support/python_context_call_runnable.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" + +namespace ballistica::base { + +auto PythonClassDisplayTimer::type_name() -> const char* { + return "DisplayTimer"; +} + +void PythonClassDisplayTimer::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "babase.DisplayTimer"; + cls->tp_basicsize = sizeof(PythonClassDisplayTimer); + cls->tp_doc = + "DisplayTimer(time: float, call: Callable[[], Any], repeat: bool = " + "False)\n" + "\n" + "Timers are used to run code at later points in time.\n" + "\n" + "Category: **General Utility Classes**\n" + "\n" + "This class encapsulates a timer based on display-time.\n" + "The underlying timer will be destroyed when this object is no longer\n" + "referenced. If you do not want to worry about keeping a reference to\n" + "your timer around, use the babase.displaytimer() function instead to " + "get a\n" + "one-off timer.\n" + "\n" + "Display-time is a time value intended to be used for animation and\n" + "other visual purposes. It will generally increment by a consistent\n" + "amount each frame. It will pass at an overall similar rate to AppTime,\n" + "but trades accuracy for smoothness.\n" + "\n" + "##### Arguments\n" + "###### time\n" + "> Length of time in seconds that the timer will wait before firing.\n" + "\n" + "###### call\n" + "> A callable Python object. Remember that the timer will retain a\n" + "strong reference to the callable for as long as it exists, so you\n" + "may want to look into concepts such as babase.WeakCall if that is not\n" + "desired.\n" + "\n" + "###### repeat\n" + "> If True, the timer will fire repeatedly, with each successive\n" + "firing having the same delay as the first.\n" + "\n" + "##### Example\n" + "\n" + "Use a Timer object to print repeatedly for a few seconds:\n" + "... def say_it():\n" + "... babase.screenmessage('BADGER!')\n" + "... def stop_saying_it():\n" + "... global g_timer\n" + "... g_timer = None\n" + "... babase.screenmessage('MUSHROOM MUSHROOM!')\n" + "... # Create our timer; it will run as long as we have the self.t ref.\n" + "... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True)\n" + "... # Now fire off a one-shot timer to kill it.\n" + "... babase.displaytimer(3.89, stop_saying_it)\n"; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; +} + +auto PythonClassDisplayTimer::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + PythonClassDisplayTimer* self{}; + BA_PYTHON_TRY; + + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + + double length; + int repeat{}; + PyObject* call_obj{}; + static const char* kwlist[] = {"time", "call", "repeat", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "dO|p", + const_cast(kwlist), &length, + &call_obj, &repeat)) { + return nullptr; + } + BasePython::EnsureContextAllowsDefaultTimerTypes(); + if (length < 0) { + throw Exception("Timer length cannot be < 0.", PyExcType::kValue); + } + + auto runnable(Object::New(call_obj)); + + self = reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) { + return nullptr; + } + self->timer_id_ = g_base->logic->NewDisplayTimer( + static_cast(length * 1000.0), repeat, + Object::New(call_obj)); + + self->have_timer_ = true; + + return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; +} + +void PythonClassDisplayTimer::tp_dealloc(PythonClassDisplayTimer* self) { + BA_PYTHON_TRY; + + // These have to be deleted in the logic thread. + if (g_base->InLogicThread()) { + g_base->logic->DeleteDisplayTimer(self->timer_id_); + } else { + g_base->logic->event_loop()->PushCall( + [tid = self->timer_id_] { g_base->logic->DeleteDisplayTimer(tid); }); + } + + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +PyTypeObject PythonClassDisplayTimer::type_obj; + +} // namespace ballistica::base diff --git a/src/ballistica/base/python/class/python_class_display_timer.h b/src/ballistica/base/python/class/python_class_display_timer.h new file mode 100644 index 00000000..61c25888 --- /dev/null +++ b/src/ballistica/base/python/class/python_class_display_timer.h @@ -0,0 +1,30 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_DISPLAY_TIMER_H_ +#define BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_DISPLAY_TIMER_H_ + +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::base { + +class PythonClassDisplayTimer : public PythonClass { + public: + static auto type_name() -> const char*; + static void SetupType(PyTypeObject* cls); + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + + private: + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassDisplayTimer* self); + int timer_id_; + bool have_timer_; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_DISPLAY_TIMER_H_ diff --git a/src/ballistica/base/python/class/python_class_feature_set_data.cc b/src/ballistica/base/python/class/python_class_feature_set_data.cc new file mode 100644 index 00000000..73eeb217 --- /dev/null +++ b/src/ballistica/base/python/class/python_class_feature_set_data.cc @@ -0,0 +1,52 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/python/class/python_class_feature_set_data.h" + +namespace ballistica::base { + +auto PythonClassFeatureSetData::type_name() -> const char* { + return "FeatureSetData"; +} + +void PythonClassFeatureSetData::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "_babase.FeatureSetData"; + cls->tp_basicsize = sizeof(PythonClassFeatureSetData); + cls->tp_doc = "Internal."; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_methods = tp_methods; +} + +auto PythonClassFeatureSetData::Create(FeatureSetFrontEnd* feature_set) + -> PyObject* { + assert(feature_set); + assert(TypeIsSetUp(&type_obj)); + auto* py_sound = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + if (!feature_set) { + throw Exception("FeatureSetData creation failed."); + } + + py_sound->feature_set_ = feature_set; + return reinterpret_cast(py_sound); +} + +auto PythonClassFeatureSetData::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = + reinterpret_cast(type->tp_alloc(type, 0)); + return reinterpret_cast(self); +} + +void PythonClassFeatureSetData::tp_dealloc(PythonClassFeatureSetData* self) { + BA_PYTHON_TRY; + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +PyTypeObject PythonClassFeatureSetData::type_obj; +PyMethodDef PythonClassFeatureSetData::tp_methods[] = {{nullptr}}; + +} // namespace ballistica::base diff --git a/src/ballistica/base/python/class/python_class_feature_set_data.h b/src/ballistica/base/python/class/python_class_feature_set_data.h new file mode 100644 index 00000000..3b9d0ebd --- /dev/null +++ b/src/ballistica/base/python/class/python_class_feature_set_data.h @@ -0,0 +1,53 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_FEATURE_SET_DATA_H_ +#define BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_FEATURE_SET_DATA_H_ + +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::base { + +/// A simple Python class we use to hold a pointer to a C++ +/// FeatureSetFrontEnd instance. This allows us to piggyback on Python's import +/// system in our C++ layer. +class PythonClassFeatureSetData : public PythonClass { + public: + static void SetupType(PyTypeObject* cls); + static auto type_name() -> const char*; + static auto Create(FeatureSetFrontEnd* feature_set) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + + /// Cast raw Python pointer to our type; throws an exception on wrong types. + static auto FromPyObj(PyObject* o) -> PythonClassFeatureSetData& { + if (Check(o)) { + return *reinterpret_cast(o); + } + throw Exception(std::string("Expected a ") + type_name() + "; got a " + + Python::ObjTypeToString(o), + PyExcType::kType); + } + + auto feature_set() const -> FeatureSetFrontEnd* { + assert(feature_set_); + return feature_set_; + } + + static PyTypeObject type_obj; + + private: + static PyMethodDef tp_methods[]; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassFeatureSetData* self); + FeatureSetFrontEnd* feature_set_{}; + static auto Play(PythonClassFeatureSetData* self, PyObject* args, + PyObject* keywds) -> PyObject*; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_FEATURE_SET_DATA_H_ diff --git a/src/ballistica/base/python/class/python_class_simple_sound.cc b/src/ballistica/base/python/class/python_class_simple_sound.cc new file mode 100644 index 00000000..5145243b --- /dev/null +++ b/src/ballistica/base/python/class/python_class_simple_sound.cc @@ -0,0 +1,112 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/python/class/python_class_simple_sound.h" + +#include "ballistica/base/assets/sound_asset.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/shared/foundation/event_loop.h" + +namespace ballistica::base { + +auto PythonClassSimpleSound::type_name() -> const char* { + return "SimpleSound"; +} + +void PythonClassSimpleSound::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "babase.SimpleSound"; + cls->tp_basicsize = sizeof(PythonClassSimpleSound); + cls->tp_doc = + "A simple sound wrapper for internal use.\n" + "\n" + "Do not use for gameplay code as it will only play locally."; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_methods = tp_methods; +} + +auto PythonClassSimpleSound::Create(const Object::Ref& sound) + -> PyObject* { + assert(TypeIsSetUp(&type_obj)); + auto* py_sound = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + if (!py_sound) { + throw Exception("SimpleSound creation failed."); + } + + *py_sound->sound_ = sound; + return reinterpret_cast(py_sound); +} + +auto PythonClassSimpleSound::tp_repr(PythonClassSimpleSound* self) + -> PyObject* { + BA_PYTHON_TRY; + SoundAsset* s = self->sound_->Get(); + return Py_BuildValue( + "s", (std::string("GetName()) + "'>") + .c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassSimpleSound::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = + reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + self->sound_ = new Object::Ref(); + return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; +} + +void PythonClassSimpleSound::tp_dealloc(PythonClassSimpleSound* self) { + BA_PYTHON_TRY; + // Our Object::Ref needs to be cleared in the logic thread. + auto* ptr = self->sound_; + if (g_base->InLogicThread()) { + delete ptr; + } else { + g_base->logic->event_loop()->PushCall([ptr] { delete ptr; }); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +auto PythonClassSimpleSound::Play(PythonClassSimpleSound* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + float volume{1.0f}; + static const char* kwlist[] = {"volume", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|f", + const_cast(kwlist), &volume)) { + return nullptr; + } + SoundAsset* s = self->sound_->Get(); + g_base->audio->PlaySound(s, volume); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +PyTypeObject PythonClassSimpleSound::type_obj; +PyMethodDef PythonClassSimpleSound::tp_methods[] = { + {"play", (PyCFunction)PythonClassSimpleSound::Play, + METH_VARARGS | METH_KEYWORDS, + "play() -> None\n" + "\n" + "Play the sound locally.\n" + ""}, + + {nullptr}}; + +} // namespace ballistica::base diff --git a/src/ballistica/base/python/class/python_class_simple_sound.h b/src/ballistica/base/python/class/python_class_simple_sound.h new file mode 100644 index 00000000..6f4e3e69 --- /dev/null +++ b/src/ballistica/base/python/class/python_class_simple_sound.h @@ -0,0 +1,55 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_SIMPLE_SOUND_H_ +#define BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_SIMPLE_SOUND_H_ + +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::base { + +/// A simple sound class we can use for minimal internal purposes. +/// This allows us to play sounds even if we are running without +/// a UI feature-set present. +class PythonClassSimpleSound : public PythonClass { + public: + static void SetupType(PyTypeObject* cls); + static auto type_name() -> const char*; + static auto Create(const Object::Ref& sound) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + + /// Cast raw Python pointer to our type; throws an exception on wrong types. + static auto FromPyObj(PyObject* o) -> PythonClassSimpleSound& { + if (Check(o)) { + return *reinterpret_cast(o); + } + throw Exception(std::string("Expected a ") + type_name() + "; got a " + + Python::ObjTypeToString(o), + PyExcType::kType); + } + + auto sound() const -> SoundAsset& { + assert(sound_); + return **sound_; + } + + static PyTypeObject type_obj; + + private: + static PyMethodDef tp_methods[]; + static auto tp_repr(PythonClassSimpleSound* self) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassSimpleSound* self); + Object::Ref* sound_; + static auto Play(PythonClassSimpleSound* self, PyObject* args, + PyObject* keywds) -> PyObject*; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_SIMPLE_SOUND_H_ diff --git a/src/ballistica/python/class/python_class_vec3.cc b/src/ballistica/base/python/class/python_class_vec3.cc similarity index 76% rename from src/ballistica/python/class/python_class_vec3.cc rename to src/ballistica/base/python/class/python_class_vec3.cc index b74ca9b4..cbf96e4d 100644 --- a/src/ballistica/python/class/python_class_vec3.cc +++ b/src/ballistica/base/python/class/python_class_vec3.cc @@ -1,15 +1,17 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/python/class/python_class_vec3.h" +#include "ballistica/base/python/class/python_class_vec3.h" -#include "ballistica/python/python.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/shared/python/python.h" // FIXME: -// We currently call abc.Sequence.register(_ba.Vec3) which registers us as +// We currently call abc.Sequence.register(_babase.Vec3) which registers us as // a Sequence type (so that isinstance(ba.Vec3(), abc.Sequence) == True). // However the abc module lists a few things as part of the Sequence interface // that we don't currently provide: index() and count() -namespace ballistica { + +namespace ballistica::base { // Ignore a few things that python macros do. #pragma clang diagnostic push @@ -22,11 +24,14 @@ PyTypeObject PythonClassVec3::type_obj; PySequenceMethods PythonClassVec3::as_sequence_; PyNumberMethods PythonClassVec3::as_number_; -void PythonClassVec3::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_name = "ba.Vec3"; - obj->tp_basicsize = sizeof(PythonClassVec3); - obj->tp_doc = +auto PythonClassVec3::type_name() -> const char* { return "Vec3"; } + +void PythonClassVec3::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "babase.Vec3"; + cls->tp_basicsize = sizeof(PythonClassVec3); + cls->tp_doc = "A vector of 3 floats.\n" "\n" "Category: **General Utility Classes**\n" @@ -47,19 +52,19 @@ void PythonClassVec3::SetupType(PyTypeObject* obj) { " z (float):\n" " The vector's Z component.\n"; - obj->tp_new = tp_new; - obj->tp_repr = (reprfunc)tp_repr; - obj->tp_methods = tp_methods; - obj->tp_getattro = (getattrofunc)tp_getattro; - obj->tp_setattro = (setattrofunc)tp_setattro; - obj->tp_richcompare = (richcmpfunc)tp_richcompare; + cls->tp_new = tp_new; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_methods = tp_methods; + cls->tp_getattro = (getattrofunc)tp_getattro; + cls->tp_setattro = (setattrofunc)tp_setattro; + cls->tp_richcompare = (richcmpfunc)tp_richcompare; // Sequence functionality. memset(&as_sequence_, 0, sizeof(as_sequence_)); as_sequence_.sq_length = (lenfunc)sq_length; as_sequence_.sq_item = (ssizeargfunc)sq_item; as_sequence_.sq_ass_item = (ssizeobjargproc)sq_ass_item; - obj->tp_as_sequence = &as_sequence_; + cls->tp_as_sequence = &as_sequence_; // Number functionality. memset(&as_number_, 0, sizeof(as_number_)); @@ -67,7 +72,7 @@ void PythonClassVec3::SetupType(PyTypeObject* obj) { as_number_.nb_subtract = (binaryfunc)nb_subtract; as_number_.nb_multiply = (binaryfunc)nb_multiply; as_number_.nb_negative = (unaryfunc)nb_negative; - obj->tp_as_number = &as_number_; + cls->tp_as_number = &as_number_; // Note: we could fill out the in-place versions of these // if we're not going for immutability.. @@ -85,45 +90,45 @@ auto PythonClassVec3::Create(const Vector3f& val) -> PyObject* { auto PythonClassVec3::tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) -> PyObject* { auto self = reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; - - // Accept a numeric sequence of length 3. - assert(args != nullptr); - assert(PyTuple_Check(args)); - Py_ssize_t numargs = PyTuple_GET_SIZE(args); - if (numargs == 1 && PySequence_Check(PyTuple_GET_ITEM(args, 0))) { - auto vals = Python::GetPyFloats(PyTuple_GET_ITEM(args, 0)); - if (vals.size() != 3) { - throw Exception("Expected a 3 member numeric sequence.", - PyExcType::kValue); - } - self->value.x = vals[0]; - self->value.y = vals[1]; - self->value.z = vals[2]; - } else if (numargs == 1 - && Python::CanGetPyDouble(PyTuple_GET_ITEM(args, 0))) { - float val = Python::GetPyFloat(PyTuple_GET_ITEM(args, 0)); - self->value.x = self->value.y = self->value.z = val; - } else { - // Otherwise interpret as individual x, y, z float vals defaulting to 0. - static const char* kwlist[] = {"x", "y", "z", nullptr}; - if (!PyArg_ParseTupleAndKeywords( - args, keywds, "|fff", const_cast(kwlist), &self->value.x, - &self->value.y, &self->value.z)) { - Py_TYPE(self)->tp_free(reinterpret_cast(self)); - return nullptr; - } + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + // Accept a numeric sequence of length 3. + assert(args != nullptr); + assert(PyTuple_Check(args)); + Py_ssize_t numargs = PyTuple_GET_SIZE(args); + if (numargs == 1 && PySequence_Check(PyTuple_GET_ITEM(args, 0))) { + auto vals = Python::GetPyFloats(PyTuple_GET_ITEM(args, 0)); + if (vals.size() != 3) { + throw Exception("Expected a 3 member numeric sequence.", + PyExcType::kValue); + } + self->value.x = vals[0]; + self->value.y = vals[1]; + self->value.z = vals[2]; + } else if (numargs == 1 + && Python::CanGetPyDouble(PyTuple_GET_ITEM(args, 0))) { + float val = Python::GetPyFloat(PyTuple_GET_ITEM(args, 0)); + self->value.x = self->value.y = self->value.z = val; + } else { + // Otherwise interpret as individual x, y, z float vals defaulting to 0. + static const char* kwlist[] = {"x", "y", "z", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|fff", + const_cast(kwlist), &self->value.x, + &self->value.y, &self->value.z)) { + Py_TYPE(self)->tp_free(reinterpret_cast(self)); + return nullptr; } - BA_PYTHON_NEW_CATCH; } return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; } auto PythonClassVec3::tp_repr(PythonClassVec3* self) -> PyObject* { BA_PYTHON_TRY; char buffer[128]; - snprintf(buffer, sizeof(buffer), "ba.Vec3(%f, %f, %f)", self->value.x, + snprintf(buffer, sizeof(buffer), "babase.Vec3(%f, %f, %f)", self->value.x, self->value.y, self->value.z); return Py_BuildValue("s", buffer); BA_PYTHON_CATCH; @@ -203,9 +208,9 @@ auto PythonClassVec3::nb_multiply(PyObject* l, PyObject* r) -> PyObject* { } // Try right as a vec3-able value. - if (Python::CanGetPyVector3f(r)) { + if (BasePython::CanGetPyVector3f(r)) { Vector3f& lvec(reinterpret_cast(l)->value); - Vector3f rvec(Python::GetPyVector3f(r)); + Vector3f rvec(BasePython::GetPyVector3f(r)); return Create( Vector3f(lvec.x * rvec.x, lvec.y * rvec.y, lvec.z * rvec.z)); } @@ -221,8 +226,8 @@ auto PythonClassVec3::nb_multiply(PyObject* l, PyObject* r) -> PyObject* { } // Try left as a vec3-able value. - if (Python::CanGetPyVector3f(l)) { - Vector3f lvec(Python::GetPyVector3f(l)); + if (BasePython::CanGetPyVector3f(l)) { + Vector3f lvec(BasePython::GetPyVector3f(l)); Vector3f& rvec(reinterpret_cast(r)->value); return Create( Vector3f(lvec.x * rvec.x, lvec.y * rvec.y, lvec.z * rvec.z)); @@ -274,14 +279,14 @@ auto PythonClassVec3::Normalized(PythonClassVec3* self) -> PyObject* { auto PythonClassVec3::Dot(PythonClassVec3* self, PyObject* other) -> PyObject* { BA_PYTHON_TRY; - return PyFloat_FromDouble(self->value.Dot(Python::GetPyVector3f(other))); + return PyFloat_FromDouble(self->value.Dot(BasePython::GetPyVector3f(other))); BA_PYTHON_CATCH; } auto PythonClassVec3::Cross(PythonClassVec3* self, PyObject* other) -> PyObject* { BA_PYTHON_TRY; - return Create(Vector3f::Cross(self->value, Python::GetPyVector3f(other))); + return Create(Vector3f::Cross(self->value, BasePython::GetPyVector3f(other))); BA_PYTHON_CATCH; } @@ -343,4 +348,4 @@ auto PythonClassVec3::tp_setattro(PythonClassVec3* self, PyObject* attrobj, #pragma clang diagnostic pop -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/python/class/python_class_vec3.h b/src/ballistica/base/python/class/python_class_vec3.h similarity index 79% rename from src/ballistica/python/class/python_class_vec3.h rename to src/ballistica/base/python/class/python_class_vec3.h index 7cbb02c5..1855a4cd 100644 --- a/src/ballistica/python/class/python_class_vec3.h +++ b/src/ballistica/base/python/class/python_class_vec3.h @@ -1,17 +1,17 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_VEC3_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_VEC3_H_ +#ifndef BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_VEC3_H_ +#define BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_VEC3_H_ -#include "ballistica/math/vector3f.h" -#include "ballistica/python/class/python_class.h" +#include "ballistica/shared/math/vector3f.h" +#include "ballistica/shared/python/python_class.h" -namespace ballistica { +namespace ballistica::base { class PythonClassVec3 : public PythonClass { public: - static auto type_name() -> const char* { return "Vec3"; } - static void SetupType(PyTypeObject* obj); + static auto type_name() -> const char*; + static void SetupType(PyTypeObject* cls); static auto Create(const Vector3f& val) -> PyObject*; static auto Check(PyObject* o) -> bool { return PyObject_TypeCheck(o, &type_obj); @@ -45,5 +45,5 @@ class PythonClassVec3 : public PythonClass { -> int; }; -} // namespace ballistica -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_VEC3_H_ +} // namespace ballistica::base +#endif // BALLISTICA_BASE_PYTHON_CLASS_PYTHON_CLASS_VEC3_H_ diff --git a/src/ballistica/base/python/methods/python_methods_app.cc b/src/ballistica/base/python/methods/python_methods_app.cc new file mode 100644 index 00000000..ae7c9dbf --- /dev/null +++ b/src/ballistica/base/python/methods/python_methods_app.cc @@ -0,0 +1,1289 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/python/methods/python_methods_app.h" + +#include "ballistica/base/app/app.h" +#include "ballistica/base/app/app_mode.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/python/support/python_context_call_runnable.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/scene_v1/assets/scene_texture.h" +#include "ballistica/scene_v1/python/class/python_class_activity_data.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/foundation/logging.h" +#include "ballistica/shared/python/python.h" + +namespace ballistica::base { + +// Python does lots of signed bitwise stuff; turn off those warnings here. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#pragma ide diagnostic ignored "RedundantCast" + +// --------------------------------- appname ----------------------------------- + +static auto PyAppName(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + + // This will get subbed out by standard filtering. + return PyUnicode_FromString("ballisticakit"); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyAppNameDef = { + "appname", // name + (PyCFunction)PyAppName, // method + METH_NOARGS, // flags + + "appname() -> str\n" + "\n" + "(internal)\n", +}; + +// --------------------------------- run_app ----------------------------------- + +static auto PyRunApp(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + + printf("WOULD RUN!\n"); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyRunAppDef = { + "run_app", // name + (PyCFunction)PyRunApp, // method + METH_NOARGS, // flags + + "run_app() -> None\n" + "\n" + "Run the app to completion.\n" + "\n" + "Note that this only works on platforms/builds where ballistica\n" + "manages its own event loop.", +}; + +// -------------------------------- appnameupper ------------------------------- + +static auto PyAppNameUpper(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + + // This will get subbed out by standard filtering. + return PyUnicode_FromString("BallisticaKit"); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyAppNameUpperDef = { + "appnameupper", // name + (PyCFunction)PyAppNameUpper, // method + METH_NOARGS, // flags + + "appnameupper() -> str\n" + "\n" + "(internal)\n" + "\n" + "Return whether this build of the game can display full unicode such " + "as\n" + "Emoji, Asian languages, etc.", +}; + +// ---------------------------- is_xcode_build --------------------------------- + +static auto PyIsXCodeBuild(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + if (g_buildconfig.xcode_build()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyIsXCodeBuildDef = { + "is_xcode_build", // name + (PyCFunction)PyIsXCodeBuild, // method + METH_NOARGS, // flags + + "is_xcode_build() -> bool\n" + "\n" + "(internal)\n", +}; + +// ----------------------- can_display_full_unicode ---------------------------- + +static auto PyCanDisplayFullUnicode(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + if (g_buildconfig.enable_os_font_rendering()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyCanDisplayFullUnicodeDef = { + "can_display_full_unicode", // name + (PyCFunction)PyCanDisplayFullUnicode, // method + METH_NOARGS, // flags + + "can_display_full_unicode() -> bool\n" + "\n" + "(internal)", +}; + +// -------------------------- app_instance_uuid -------------------------------- + +static auto PyAppInstanceUUID(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + return PyUnicode_FromString(g_base->GetAppInstanceUUID().c_str()); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyAppInstanceUUIDDef = { + "app_instance_uuid", // name + (PyCFunction)PyAppInstanceUUID, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "app_instance_uuid() -> str\n" + "\n" + "(internal)", +}; + +// --------------------------- user_ran_commands ------------------------------- + +static auto PyUserRanCommands(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + g_core->user_ran_commands = true; + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyUserRanCommandsDef = { + "user_ran_commands", // name + (PyCFunction)PyUserRanCommands, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "user_ran_commands() -> None\n" + "\n" + "(internal)", +}; + +// -------------------------------- pushcall ---------------------------------- + +static auto PyPushCall(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + PyObject* call_obj; + int from_other_thread{}; + int suppress_warning{}; + int other_thread_use_fg_context{}; + int raw{0}; + static const char* kwlist[] = {"call", + "from_other_thread", + "suppress_other_thread_warning", + "other_thread_use_fg_context", + "raw", + nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|pppp", + const_cast(kwlist), &call_obj, + &from_other_thread, &suppress_warning, + &other_thread_use_fg_context, &raw)) { + return nullptr; + } + + // 'raw' mode does no thread checking and no context saves/restores. + if (raw) { + assert(Python::HaveGIL()); + Py_INCREF(call_obj); + g_base->logic->event_loop()->PushCall([call_obj] { + assert(g_base->InLogicThread()); + PythonRef(call_obj, PythonRef::kSteal).Call(); + }); + } else if (from_other_thread) { + // Warn the user not to use this from the logic thread since it doesnt + // save/restore context. + if (!suppress_warning && g_base->InLogicThread()) { + Log(LogLevel::kWarning, + "babase.pushcall() called from the logic thread with " + "from_other_thread set to true (call " + + Python::ObjToString(call_obj) + " at " + + Python::GetPythonFileLocation() + + "). That arg should only be used from other threads."); + } + + assert(Python::HaveGIL()); + + // This can get called from other threads so we can't construct + // Objects and things here or we'll trip our thread-checks. Instead we + // just increment the python object's refcount and pass it along raw; + // the logic thread decrements it when its done. + Py_INCREF(call_obj); + + // Ship it off to the logic thread to get run. + g_base->logic->event_loop()->PushCall( + [call_obj, other_thread_use_fg_context] { + assert(g_base->InLogicThread()); + + // Run this with an empty context by default, or foreground if + // requested. + ScopedSetContext ssc(other_thread_use_fg_context + ? g_base->app_mode->GetForegroundContext() + : ContextRef(nullptr)); + + PythonRef(call_obj, PythonRef::kSteal).Call(); + }); + + } else { + if (!g_base->InLogicThread()) { + throw Exception("You must use from_other_thread mode."); + } + Object::New(call_obj)->Schedule(); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyPushCallDef = { + "pushcall", // name + (PyCFunction)PyPushCall, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "pushcall(call: Callable, from_other_thread: bool = False,\n" + " suppress_other_thread_warning: bool = False,\n" + " other_thread_use_fg_context: bool = False,\n" + " raw: bool = False) -> None\n" + "\n" + "Push a call to the logic event-loop.\n" + "Category: **General Utility Functions**\n" + "\n" + "This call expects to be used in the logic thread, and will " + "automatically\n" + "save and restore the babase.Context to behave seamlessly.\n" + "\n" + "If you want to push a call from outside of the logic thread,\n" + "however, you can pass 'from_other_thread' as True. In this case\n" + "the call will always run in the UI context_ref on the logic thread\n" + "or whichever context_ref is in the foreground if\n" + "other_thread_use_fg_context is True.\n" + "Passing raw=True will disable thread checks and context_ref" + " sets/restores."}; + +// ------------------------------ apptime -------------------------------------- + +static auto PyAppTime(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + return PyFloat_FromDouble( + 0.001 * static_cast(g_core->GetAppTimeMillisecs())); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyAppTimeDef = { + "apptime", // name + (PyCFunction)PyAppTime, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "apptime() -> babase.AppTime\n" + "\n" + "Return the current app-time in seconds.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "App-time is a monotonic time value; it starts at 0.0 when the app\n" + "launches and will never jump by large amounts or go backwards, even if\n" + "the system time changes. Its progression will pause when the app is in\n" + "a suspended state.\n" + "\n" + "Note that the AppTime returned here is simply float; it just has a\n" + "unique type in the type-checker's eyes to help prevent it from being\n" + "accidentally used with time functionality expecting other time types.", +}; + +// ------------------------------ apptimer ------------------------------------- + +static auto PyAppTimer(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + double length; + PyObject* call_obj; + static const char* kwlist[] = {"time", "call", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "dO", const_cast(kwlist), &length, &call_obj)) { + return nullptr; + } + BasePython::EnsureContextAllowsDefaultTimerTypes(); + if (length < 0) { + throw Exception("Timer length cannot be < 0.", PyExcType::kValue); + } + g_base->logic->NewDisplayTimer( + static_cast(length * 1000.0), false, + Object::New(call_obj)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyAppTimerDef = { + "apptimer", // name + (PyCFunction)PyAppTimer, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "apptimer(time: float, call: Callable[[], Any]) -> None\n" + "\n" + "Schedule a callable object to run based on app-time.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "This function creates a one-off timer which cannot be canceled or\n" + "modified once created. If you require the ability to do so, or need\n" + "a repeating timer, use the babase.AppTimer class instead.\n" + "\n" + "##### Arguments\n" + "###### time (float)\n" + "> Length of time in seconds that the timer will wait before firing.\n" + "\n" + "###### call (Callable[[], Any])\n" + "> A callable Python object. Note that the timer will retain a\n" + "strong reference to the callable for as long as the timer exists, so you\n" + "may want to look into concepts such as babase.WeakCall if that is not\n" + "desired.\n" + "\n" + "##### Examples\n" + "Print some stuff through time:\n" + ">>> babase.screenmessage('hello from now!')\n" + ">>> babase.apptimer(1.0, ba.Call(ba.screenmessage, 'hello from the " + "future!'))\n" + ">>> babase.apptimer(2.0, ba.Call(ba.screenmessage,\n" + "... 'hello from the future 2!'))\n", +}; + +// --------------------------- displaytime ------------------------------------- + +static auto PyDisplayTime(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + return PyFloat_FromDouble(g_base->logic->display_time()); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyDisplayTimeDef = { + "displaytime", // name + (PyCFunction)PyDisplayTime, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "displaytime() -> babase.DisplayTime\n" + "\n" + "Return the current display-time in seconds.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "Display-time is a time value intended to be used for animation and other\n" + "visual purposes. It will generally increment by a consistent amount each\n" + "frame. It will pass at an overall similar rate to AppTime, but trades\n" + "accuracy for smoothness.\n" + "\n" + "Note that the value returned here is simply a float; it just has a\n" + "unique type in the type-checker's eyes to help prevent it from being\n" + "accidentally used with time functionality expecting other time types.", +}; + +// ---------------------------- displaytimer ----------------------------------- + +static auto PyDisplayTimer(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + double length; + PyObject* call_obj; + static const char* kwlist[] = {"time", "call", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "dO", const_cast(kwlist), &length, &call_obj)) { + return nullptr; + } + BasePython::EnsureContextAllowsDefaultTimerTypes(); + if (length < 0) { + throw Exception("Timer length cannot be < 0.", PyExcType::kValue); + } + g_base->logic->NewAppTimer( + static_cast(length * 1000.0), false, + Object::New(call_obj)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyDisplayTimerDef = { + "displaytimer", // name + (PyCFunction)PyDisplayTimer, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "displaytimer(time: float, call: Callable[[], Any]) -> None\n" + "\n" + "Schedule a callable object to run based on display-time.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "This function creates a one-off timer which cannot be canceled or\n" + "modified once created. If you require the ability to do so, or need\n" + "a repeating timer, use the babase.DisplayTimer class instead.\n" + "\n" + "Display-time is a time value intended to be used for animation and other\n" + "visual purposes. It will generally increment by a consistent amount each\n" + "frame. It will pass at an overall similar rate to AppTime, but trades\n" + "accuracy for smoothness.\n" + "\n" + "##### Arguments\n" + "###### time (float)\n" + "> Length of time in seconds that the timer will wait before firing.\n" + "\n" + "###### call (Callable[[], Any])\n" + "> A callable Python object. Note that the timer will retain a\n" + "strong reference to the callable for as long as the timer exists, so you\n" + "may want to look into concepts such as babase.WeakCall if that is not\n" + "desired.\n" + "\n" + "##### Examples\n" + "Print some stuff through time:\n" + ">>> babase.screenmessage('hello from now!')\n" + ">>> babase.displaytimer(1.0, ba.Call(ba.screenmessage,\n" + "... 'hello from the future!'))\n" + ">>> babase.displaytimer(2.0, ba.Call(ba.screenmessage,\n" + "... 'hello from the future 2!'))\n", +}; + +// ----------------------------------- quit ------------------------------------ + +static auto PyQuit(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {"soft", "back", nullptr}; + int soft = 0; + int back = 0; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|ii", + const_cast(kwlist), &soft, &back)) { + return nullptr; + } + + // FIXME this should all just go through platform. + + if (g_buildconfig.ostype_ios_tvos()) { + // This should never be called on iOS + Log(LogLevel::kError, "Quit called."); + } + + bool handled = false; + + // A few types get handled specially on Android. + if (g_buildconfig.ostype_android()) { +#pragma clang diagnostic push +#pragma ide diagnostic ignored "ConstantConditionsOC" + + if (!handled && back) { + // Back-quit simply synthesizes a back press. + // Note to self: I remember this behaved slightly differently than + // doing a soft quit but I should remind myself how. + g_core->platform->AndroidSynthesizeBackPress(); + handled = true; + } + +#pragma clang diagnostic pop + + if (!handled && soft) { + // Soft-quit just kills our activity but doesn't run app shutdown. + // Thus we'll be able to spin back up (reset to the main menu) + // if the user re-launches us. + g_core->platform->AndroidQuitActivity(); + handled = true; + } + } + // In all other cases, kick off a standard app shutdown. + if (!handled) { + g_base->logic->event_loop()->PushCall([] { g_base->logic->Shutdown(); }); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyQuitDef = { + "quit", // name + (PyCFunction)PyQuit, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "quit(soft: bool = False, back: bool = False) -> None\n" + "\n" + "Quit the game.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "On systems like Android, 'soft' will end the activity but keep the\n" + "app running.", +}; + +// ----------------------------- apply_config ---------------------------------- + +static auto PyApplyConfig(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + + // Hmm; python runs in the logic thread; technically we could just run + // ApplyAppConfig() immediately (though pushing is probably safer). + g_base->logic->event_loop()->PushCall( + [] { g_base->logic->ApplyAppConfig(); }); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyApplyConfigDef = { + "apply_config", // name + PyApplyConfig, // method + METH_VARARGS, // flags + + "apply_config() -> None\n" + "\n" + "(internal)", +}; + +// ----------------------------- commit_config --------------------------------- + +static auto PyCommitConfig(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + PyObject* config_obj; + static const char* kwlist[] = {"config", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &config_obj)) { + return nullptr; + } + if (config_obj == nullptr || !g_base->python->IsPyLString(config_obj)) { + throw Exception("ERROR ON JSON DUMP"); + } + std::string final_str = g_base->python->GetPyLString(config_obj); + std::string path = g_core->platform->GetConfigFilePath(); + std::string path_temp = path + ".tmp"; + std::string path_prev = path + ".prev"; + if (explicit_bool(true)) { + FILE* f_out = g_core->platform->FOpen(path_temp.c_str(), "wb"); + if (f_out == nullptr) { + throw Exception("Error opening config file for writing: '" + path_temp + + "': " + g_core->platform->GetErrnoString()); + } + + // Write to temp file. + size_t result = fwrite(&final_str[0], final_str.size(), 1, f_out); + if (result != 1) { + fclose(f_out); + throw Exception("Error writing config file to '" + path_temp + + "': " + g_core->platform->GetErrnoString()); + } + fclose(f_out); + + // Now backup any existing config to .prev. + if (g_core->platform->FilePathExists(path)) { + // On windows, rename doesn't overwrite existing files.. need to kill + // the old explicitly. + // (hmm; should we just do this everywhere for consistency?) + if (g_buildconfig.ostype_windows()) { + if (g_core->platform->FilePathExists(path_prev)) { + int result2 = g_core->platform->Remove(path_prev.c_str()); + if (result2 != 0) { + throw Exception("Error removing prev config file '" + path_prev + + "': " + g_core->platform->GetErrnoString()); + } + } + } + int result2 = g_core->platform->Rename(path.c_str(), path_prev.c_str()); + if (result2 != 0) { + throw Exception("Error backing up config file to '" + path_prev + + "': " + g_core->platform->GetErrnoString()); + } + } + + // Now move temp into place. + int result2 = g_core->platform->Rename(path_temp.c_str(), path.c_str()); + if (result2 != 0) { + throw Exception("Error renaming temp config file to final '" + path + + "': " + g_core->platform->GetErrnoString()); + } + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyCommitConfigDef = { + "commit_config", // name + (PyCFunction)PyCommitConfig, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "commit_config(config: str) -> None\n" + "\n" + "(internal)", +}; + +// --------------------------------- env --------------------------------------- + +static auto PyPreEnv(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + + // This version only include a bare minimum of values but can be called + // before bootstrapping is complete. + + // Just build this once and recycle it. + if (!g_base->python->objs().Exists(BasePython::ObjID::kPreEnv)) { + // clang-format off + PyObject* env = Py_BuildValue( + "{" + "si" // build_number + "sO" // debug_build + "sO" // test_build + "}", + "build_number", kEngineBuildNumber, + "debug_build", g_buildconfig.debug_build() ? Py_True : Py_False, + "test_build", g_buildconfig.test_build() ? Py_True : Py_False); + // clang-format on + g_base->python->StorePreEnv(env); + } + return g_base->python->objs().Get(BasePython::ObjID::kPreEnv).NewRef(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyPreEnvDef = { + "pre_env", // name + (PyCFunction)PyPreEnv, // method + METH_NOARGS, // flags + + "pre_env() -> dict\n" + "\n" + "(internal)\n" + "\n" + "Returns a dict containing general info about the operating " + "environment\n" + "such as version, platform, etc.\n" + "This info is now exposed through babase.App; refer to those docs for\n" + "info on specific elements."}; + +// --------------------------------- env --------------------------------------- + +static auto PyEnv(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + assert(g_core); + assert(g_base); + + // Just build this once and recycle it. + if (!g_base->python->objs().Exists(BasePython::ObjID::kEnv)) { + const char* ui_scale; + switch (g_base->ui->scale()) { + case UIScale::kLarge: + ui_scale = "large"; + break; + case UIScale::kMedium: + ui_scale = "medium"; + break; + case UIScale::kSmall: + ui_scale = "small"; + break; + default: + throw Exception(); + } + std::optional user_py_dir = + g_core->platform->GetUserPythonDirectory(); + std::optional app_py_dir = + g_core->platform->GetAppPythonDirectory(); + std::optional site_py_dir = + g_core->platform->GetSitePythonDirectory(); + + // clang-format off + PyObject* env = Py_BuildValue( + "{" + "si" // build_number + "ss" // config_file_path + "ss" // locale + "ss" // user_agent_string + "ss" // version + "sO" // debug_build + "sO" // test_build + "sO" // python_directory_user + "sO" // python_directory_app + "ss" // platform + "ss" // subplatform + "ss" // ui_scale + "sO" // on_tv + "sO" // vr_mode + "sO" // toolbar_test + "sO" // demo_mode + "sO" // arcade_mode + "sO" // iircade_mode + "si" // protocol_version + "sO" // headless_mode + "sO" // python_directory_app_site + "ss" // device_name + "ss" // data_directory + "}", + "build_number", kEngineBuildNumber, + "config_file_path", g_core->platform->GetConfigFilePath().c_str(), + "locale", g_core->platform->GetLocale().c_str(), + "user_agent_string", g_core->user_agent_string.c_str(), + "version", kEngineVersion, + "debug_build", g_buildconfig.debug_build() ? Py_True : Py_False, + "test_build", g_buildconfig.test_build() ? Py_True : Py_False, + "python_directory_user", + user_py_dir ? *PythonRef::FromString(*user_py_dir) : Py_None, + "python_directory_app", + app_py_dir ? *PythonRef::FromString(*app_py_dir) : Py_None, + "platform", g_core->platform->GetPlatformName().c_str(), + "subplatform", g_core->platform->GetSubplatformName().c_str(), + "ui_scale", ui_scale, + "on_tv", g_core->platform->IsRunningOnTV() ? Py_True : Py_False, + "vr_mode", g_core->IsVRMode() ? Py_True : Py_False, + "toolbar_test", BA_TOOLBAR_TEST ? Py_True : Py_False, + "demo_mode", g_buildconfig.demo_build() ? Py_True : Py_False, + "arcade_mode", g_buildconfig.arcade_build() ? Py_True : Py_False, + "iircade_mode", g_buildconfig.iircade_build() ? Py_True: Py_False, + "protocol_version", kProtocolVersion, + "headless_mode", g_core->HeadlessMode() ? Py_True : Py_False, + "python_directory_app_site", + site_py_dir ? *PythonRef::FromString(*site_py_dir) : Py_None, + "device_name", + g_core->platform->GetDeviceName().c_str(), + "data_directory", + g_core->platform->GetDataDirectory().c_str()); + // clang-format on + g_base->python->StoreEnv(env); + } + return g_base->python->objs().Get(BasePython::ObjID::kEnv).NewRef(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyEnvDef = { + "env", // name + (PyCFunction)PyEnv, // method + METH_NOARGS, // flags + + "env() -> dict\n" + "\n" + "(internal)\n" + "\n" + "Returns a dict containing general info about the operating " + "environment\n" + "such as version, platform, etc.\n" + "This info is now exposed through babase.App; refer to those docs for\n" + "info on specific elements."}; + +// -------------------------- set_stress_testing ------------------------------- + +static auto PySetStressTesting(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + int testing; + int player_count; + if (!PyArg_ParseTuple(args, "pi", &testing, &player_count)) { + return nullptr; + } + g_base->app->PushSetStressTestingCall(static_cast(testing), + player_count); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetStressTestingDef = { + "set_stress_testing", // name + PySetStressTesting, // method + METH_VARARGS, // flags + + "set_stress_testing(testing: bool, player_count: int) -> None\n" + "\n" + "(internal)", +}; + +// ------------------------------ display_log ---------------------------------- + +static auto PyDisplayLog(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {"name", "level", "message", nullptr}; + const char* name; + const char* levelstr; + const char* message; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "sss", + const_cast(kwlist), &name, &levelstr, + &message)) { + return nullptr; + } + + // Calc LogLevel enum val from their string val. + LogLevel level; + if (levelstr == std::string("DEBUG")) { + level = LogLevel::kDebug; + } else if (levelstr == std::string("INFO")) { + level = LogLevel::kInfo; + } else if (levelstr == std::string("WARNING")) { + level = LogLevel::kWarning; + } else if (levelstr == std::string("ERROR")) { + level = LogLevel::kError; + } else if (levelstr == std::string("CRITICAL")) { + level = LogLevel::kCritical; + } else { + // Assume we should avoid Log() calls here since it could infinite loop. + fprintf(stderr, "Invalid log level to display_log(): %s\n", levelstr); + level = LogLevel::kInfo; + } + Logging::DisplayLog(name, level, message); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyDisplayLogDef = { + "display_log", // name + (PyCFunction)PyDisplayLog, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "display_log(name: str, level: str, message: str) -> None\n" + "\n" + "(internal)\n" + "\n" + "Sends a log message to the in-game console and any per-platform\n" + "log destinations (Android log, etc.). This generally is not called\n" + "directly and should instead be fed Python logging output.", +}; + +// ------------------------------- bootlog ------------------------------------- + +static auto PyBootLog(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {"message", nullptr}; + const char* message; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &message)) { + return nullptr; + } + + g_core->BootLog(message); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyBootLogDef = { + "bootlog", // name + (PyCFunction)PyBootLog, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "bootlog(message: str) -> None\n" + "\n" + "(internal)", +}; + +// ----------------------------- v1_cloud_log ---------------------------------- + +static auto PyV1CloudLog(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* message; + static const char* kwlist[] = {"message", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &message)) { + return nullptr; + } + Logging::V1CloudLog(message); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyV1CloudLogDef = { + "v1_cloud_log", // name + (PyCFunction)PyV1CloudLog, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "v1_cloud_log(message: str) -> None\n" + "\n" + "(internal)\n" + "\n" + "Push messages to the old v1 cloud log.", +}; + +// --------------------------- music_player_stop ------------------------------- + +static auto PyMusicPlayerStop(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + g_core->platform->MusicPlayerStop(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyMusicPlayerStopDef = { + "music_player_stop", // name + (PyCFunction)PyMusicPlayerStop, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "music_player_stop() -> None\n" + "\n" + "(internal)\n" + "\n" + "Stops internal music file playback (for internal use)"}; + +// ---------------------------- music_player_play ------------------------------ + +static auto PyMusicPlayerPlay(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + PyObject* files_obj; + static const char* kwlist[] = {"files", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &files_obj)) { + return nullptr; + } + g_core->platform->MusicPlayerPlay(files_obj); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyMusicPlayerPlayDef = { + "music_player_play", // name + (PyCFunction)PyMusicPlayerPlay, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "music_player_play(files: Any) -> None\n" + "\n" + "(internal)\n" + "\n" + "Starts internal music file playback (for internal use)", +}; + +// ----------------------- music_player_set_volume ----------------------------- + +static auto PyMusicPlayerSetVolume(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + float volume; + static const char* kwlist[] = {"volume", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "f", + const_cast(kwlist), &volume)) { + return nullptr; + } + g_core->platform->MusicPlayerSetVolume(volume); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyMusicPlayerSetVolumeDef = { + "music_player_set_volume", // name + (PyCFunction)PyMusicPlayerSetVolume, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "music_player_set_volume(volume: float) -> None\n" + "\n" + "(internal)\n" + "\n" + "Sets internal music player volume (for internal use)", +}; + +// ------------------------- music_player_shutdown ----------------------------- + +static auto PyMusicPlayerShutdown(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + g_core->platform->MusicPlayerShutdown(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyMusicPlayerShutdownDef = { + "music_player_shutdown", // name + (PyCFunction)PyMusicPlayerShutdown, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "music_player_shutdown() -> None\n" + "\n" + "(internal)\n" + "\n" + "Finalizes internal music file playback (for internal use)", +}; + +// ----------------------------- reload_media ---------------------------------- + +static auto PyReloadMedia(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->graphics_server); + g_base->graphics_server->PushReloadMediaCall(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyReloadMediaDef = { + "reload_media", // name + PyReloadMedia, // method + METH_VARARGS, // flags + + "reload_media() -> None\n" + "\n" + "(internal)\n" + "\n" + "Reload all currently loaded game media; useful for\n" + "development/debugging.", +}; + +// --------------------------- mac_music_app_init ------------------------------ + +static auto PyMacMusicAppInit(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + g_core->platform->MacMusicAppInit(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyMacMusicAppInitDef = { + "mac_music_app_init", // name + (PyCFunction)PyMacMusicAppInit, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "mac_music_app_init() -> None\n" + "\n" + "(internal)"}; + +// ------------------------- mac_music_app_get_volume -------------------------- + +static auto PyMacMusicAppGetVolume(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + return PyLong_FromLong(g_core->platform->MacMusicAppGetVolume()); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyMacMusicAppGetVolumeDef = { + "mac_music_app_get_volume", // name + (PyCFunction)PyMacMusicAppGetVolume, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "mac_music_app_get_volume() -> int\n" + "\n" + "(internal)", +}; + +// ------------------------- mac_music_app_set_volume -------------------------- + +static auto PyMacMusicAppSetVolume(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + int volume; + static const char* kwlist[] = {"volume", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "i", + const_cast(kwlist), &volume)) { + return nullptr; + } + g_core->platform->MacMusicAppSetVolume(volume); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyMacMusicAppSetVolumeDef = { + "mac_music_app_set_volume", // name + (PyCFunction)PyMacMusicAppSetVolume, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "mac_music_app_set_volume(volume: int) -> None\n" + "\n" + "(internal)", +}; + +// ------------------------ mac_music_app_get_library -------------------------- + +static auto PyMacMusicAppGetLibrarySource(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + g_core->platform->MacMusicAppGetLibrarySource(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyMacMusicAppGetLibrarySourceDef = { + "mac_music_app_get_library_source", // name + (PyCFunction)PyMacMusicAppGetLibrarySource, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "mac_music_app_get_library_source() -> None\n" + "\n" + "(internal)"}; + +// --------------------------- mac_music_app_stop ------------------------------ + +static auto PyMacMusicAppStop(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + g_core->platform->MacMusicAppStop(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyMacMusicAppStopDef = { + "mac_music_app_stop", // name + (PyCFunction)PyMacMusicAppStop, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "mac_music_app_stop() -> None\n" + "\n" + "(internal)", +}; + +// ----------------------- mac_music_app_play_playlist ------------------------- + +static auto PyMacMusicAppPlayPlaylist(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + std::string playlist; + PyObject* playlist_obj; + static const char* kwlist[] = {"playlist", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &playlist_obj)) { + return nullptr; + } + playlist = g_base->python->GetPyLString(playlist_obj); + if (g_core->platform->MacMusicAppPlayPlaylist(playlist)) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PyMacMusicAppPlayPlaylistDef = { + "mac_music_app_play_playlist", // name + (PyCFunction)PyMacMusicAppPlayPlaylist, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "mac_music_app_play_playlist(playlist: str) -> bool\n" + "\n" + "(internal)", +}; + +// ---------------------- mac_music_app_get_playlists -------------------------- + +static auto PyMacMusicAppGetPlaylists(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + PyObject* py_list = PyList_New(0); + std::list playlists = + g_core->platform->MacMusicAppGetPlaylists(); + for (auto&& i : playlists) { + PyObject* str_obj = PyUnicode_FromString(i.c_str()); + PyList_Append(py_list, str_obj); + Py_DECREF(str_obj); + } + return py_list; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyMacMusicAppGetPlaylistsDef = { + "mac_music_app_get_playlists", // name + (PyCFunction)PyMacMusicAppGetPlaylists, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "mac_music_app_get_playlists() -> list[str]\n" + "\n" + "(internal)", +}; + +// -------------------------- is_os_playing_music ------------------------------ + +static auto PyIsOSPlayingMusic(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + if (g_core->platform->IsOSPlayingMusic()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PyIsOSPlayingMusicDef = { + "is_os_playing_music", // name + (PyCFunction)PyIsOSPlayingMusic, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "is_os_playing_music() -> bool\n" + "\n" + "(internal)\n" + "\n" + "Tells whether the OS is currently playing music of some sort.\n" + "\n" + "(Used to determine whether the game should avoid playing its own)", +}; + +// ----------------------------------------------------------------------------- + +auto PythonMethodsApp::GetMethods() -> std::vector { + return { + PyAppNameDef, + PyRunAppDef, + PyAppNameUpperDef, + PyIsXCodeBuildDef, + PyCanDisplayFullUnicodeDef, + PyDisplayLogDef, + PyV1CloudLogDef, + PySetStressTestingDef, + PyEnvDef, + PyPreEnvDef, + PyCommitConfigDef, + PyApplyConfigDef, + PyQuitDef, + PyAppTimerDef, + PyAppTimeDef, + PyDisplayTimeDef, + PyDisplayTimerDef, + PyPushCallDef, + PyMusicPlayerShutdownDef, + PyMusicPlayerSetVolumeDef, + PyMusicPlayerPlayDef, + PyMusicPlayerStopDef, + PyAppInstanceUUIDDef, + PyUserRanCommandsDef, + PyReloadMediaDef, + PyMacMusicAppInitDef, + PyMacMusicAppGetVolumeDef, + PyMacMusicAppSetVolumeDef, + PyMacMusicAppGetLibrarySourceDef, + PyMacMusicAppStopDef, + PyMacMusicAppPlayPlaylistDef, + PyMacMusicAppGetPlaylistsDef, + PyIsOSPlayingMusicDef, + PyBootLogDef, + }; +} + +#pragma clang diagnostic pop + +} // namespace ballistica::base diff --git a/src/ballistica/base/python/methods/python_methods_app.h b/src/ballistica/base/python/methods/python_methods_app.h new file mode 100644 index 00000000..558a76ac --- /dev/null +++ b/src/ballistica/base/python/methods/python_methods_app.h @@ -0,0 +1,20 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PYTHON_METHODS_PYTHON_METHODS_APP_H_ +#define BALLISTICA_BASE_PYTHON_METHODS_PYTHON_METHODS_APP_H_ + +#include + +#include "ballistica/shared/ballistica.h" + +namespace ballistica::base { + +/// Python methods related to app functionality. +class PythonMethodsApp { + public: + static auto GetMethods() -> std::vector; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_PYTHON_METHODS_PYTHON_METHODS_APP_H_ diff --git a/src/ballistica/base/python/methods/python_methods_graphics.cc b/src/ballistica/base/python/methods/python_methods_graphics.cc new file mode 100644 index 00000000..38f82896 --- /dev/null +++ b/src/ballistica/base/python/methods/python_methods_graphics.cc @@ -0,0 +1,648 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/python/methods/python_methods_graphics.h" + +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/graphics/support/camera.h" +#include "ballistica/base/graphics/text/text_graphics.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/python/support/python_context_call_runnable.h" +#include "ballistica/core/core.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" + +namespace ballistica::base { + +// Ignore signed bitwise stuff; python macros do it quite a bit. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" + +// ---------------------------- screenmessage ---------------------------------- + +static auto PyScreenMessage(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + PyObject* color_obj = Py_None; + PyObject* message_obj; + int log{}; + static const char* kwlist[] = {"message", "color", "log", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|Op", + const_cast(kwlist), &message_obj, + &color_obj, &log)) { + return nullptr; + } + std::string message_str = g_base->python->GetPyLString(message_obj); + Vector3f color{1, 1, 1}; + if (color_obj != Py_None) { + color = BasePython::GetPyVector3f(color_obj); + } + if (log) { + Log(LogLevel::kInfo, message_str); + } + + // This version simply displays it locally. + g_base->graphics->AddScreenMessage(message_str, color); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyScreenMessageDef = { + "screenmessage", // name + (PyCFunction)PyScreenMessage, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "screenmessage(message: str | babase.Lstr,\n" + " color: Sequence[float] | None = None,\n" + " log: bool = False)\n" + " -> None\n" + "\n" + "Print a message to the local client's screen, in a given color.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "Note that this version of the function is purely for local display.\n" + "To broadcast screen messages in network play, see the versions of\n" + "this call provided by the scene-version packages.", +}; + +// -------------------------- get_camera_position ------------------------------ + +static auto PyGetCameraPosition(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + Camera* cam = g_base->graphics->camera(); + cam->get_position(&x, &y, &z); + return Py_BuildValue("(fff)", x, y, z); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetCameraPositionDef = { + "get_camera_position", // name + (PyCFunction)PyGetCameraPosition, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_camera_position() -> tuple[float, ...]\n" + "\n" + "(internal)\n" + "\n" + "WARNING: these camera controls will not apply to network clients\n" + "and may behave unpredictably in other ways. Use them only for\n" + "tinkering.", +}; + +// --------------------------- get_camera_target ------------------------------- + +static auto PyGetCameraTarget(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + Camera* cam = g_base->graphics->camera(); + cam->target_smoothed(&x, &y, &z); + return Py_BuildValue("(fff)", x, y, z); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetCameraTargetDef = { + "get_camera_target", // name + (PyCFunction)PyGetCameraTarget, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_camera_target() -> tuple[float, ...]\n" + "\n" + "(internal)\n" + "\n" + "WARNING: these camera controls will not apply to network clients\n" + "and may behave unpredictably in other ways. Use them only for\n" + "tinkering.", +}; + +// --------------------------- set_camera_position ----------------------------- + +static auto PySetCameraPosition(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + static const char* kwlist[] = {"x", "y", "z", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "fff", + const_cast(kwlist), &x, &y, &z)) { + return nullptr; + } + assert(g_base->logic); + g_base->graphics->camera()->SetPosition(x, y, z); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetCameraPositionDef = { + "set_camera_position", // name + (PyCFunction)PySetCameraPosition, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_camera_position(x: float, y: float, z: float) -> None\n" + "\n" + "(internal)\n" + "\n" + "WARNING: these camera controls will not apply to network clients\n" + "and may behave unpredictably in other ways. Use them only for\n" + "tinkering.", +}; + +// ---------------------------- set_camera_target ------------------------------ + +static auto PySetCameraTarget(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + static const char* kwlist[] = {"x", "y", "z", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "fff", + const_cast(kwlist), &x, &y, &z)) { + return nullptr; + } + assert(g_base->logic); + g_base->graphics->camera()->SetTarget(x, y, z); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetCameraTargetDef = { + "set_camera_target", // name + (PyCFunction)PySetCameraTarget, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_camera_target(x: float, y: float, z: float) -> None\n" + "\n" + "(internal)\n" + "\n" + "WARNING: these camera controls will not apply to network clients\n" + "and may behave unpredictably in other ways. Use them only for\n" + "tinkering.", +}; + +// ---------------------------- set_camera_manual ------------------------------ + +static auto PySetCameraManual(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + bool value = false; + static const char* kwlist[] = {"value", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "b", + const_cast(kwlist), &value)) { + return nullptr; + } + assert(g_base->logic); + g_base->graphics->camera()->SetManual(value); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetCameraManualDef = { + "set_camera_manual", // name + (PyCFunction)PySetCameraManual, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_camera_manual(value: bool) -> None\n" + "\n" + "(internal)\n" + "\n" + "WARNING: these camera controls will not apply to network clients\n" + "and may behave unpredictably in other ways. Use them only for\n" + "tinkering.", +}; + +// -------------------------------- charstr ------------------------------------ + +static auto PyCharStr(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + PyObject* name_obj; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &name_obj)) { + return nullptr; + } + assert(g_base->logic); + auto id(BasePython::GetPyEnum_SpecialChar(name_obj)); + assert(Utils::IsValidUTF8(g_base->assets->CharStr(id))); + return PyUnicode_FromString(g_base->assets->CharStr(id).c_str()); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyCharStrDef = { + "charstr", // name + (PyCFunction)PyCharStr, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "charstr(char_id: babase.SpecialChar) -> str\n" + "\n" + "Get a unicode string representing a special character.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "Note that these utilize the private-use block of unicode characters\n" + "(U+E000-U+F8FF) and are specific to the game; exporting or rendering\n" + "them elsewhere will be meaningless.\n" + "\n" + "See babase.SpecialChar for the list of available characters.", +}; + +// ------------------------------- safecolor ----------------------------------- + +static auto PySafeColor(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + PyObject* color_obj; + float red, green, blue; + float target_intensity = 0.6f; + static const char* kwlist[] = {"color", "target_intensity", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|f", + const_cast(kwlist), &color_obj, + &target_intensity)) { + return nullptr; + } + if (!PySequence_Check(color_obj)) { + throw Exception("Expected a sequence.", PyExcType::kType); + } + int len = static_cast(PySequence_Length(color_obj)); + if (len != 3 && len != 4) { + throw Exception("Expected a 3 or 4 length sequence; got " + + Python::ObjToString(color_obj) + ".", + PyExcType::kValue); + } + PythonRef red_obj(PySequence_GetItem(color_obj, 0), PythonRef::kSteal); + PythonRef green_obj(PySequence_GetItem(color_obj, 1), PythonRef::kSteal); + PythonRef blue_obj(PySequence_GetItem(color_obj, 2), PythonRef::kSteal); + red = Python::GetPyFloat(red_obj.Get()); + green = Python::GetPyFloat(green_obj.Get()); + blue = Python::GetPyFloat(blue_obj.Get()); + Graphics::GetSafeColor(&red, &green, &blue, target_intensity); + if (len == 3) { + return Py_BuildValue("(fff)", red, green, blue); + } else { + PythonRef alpha_obj(PySequence_GetItem(color_obj, 3), PythonRef::kSteal); + float alpha = Python::GetPyFloat(alpha_obj.Get()); + return Py_BuildValue("(ffff)", red, green, blue, alpha); + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PySafeColorDef = { + "safecolor", // name + (PyCFunction)PySafeColor, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "safecolor(color: Sequence[float], target_intensity: float = 0.6)\n" + " -> tuple[float, ...]\n" + "\n" + "Given a color tuple, return a color safe to display as text.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "Accepts tuples of length 3 or 4. This will slightly brighten very\n" + "dark colors, etc.", +}; + +// ------------------------ get_max_graphics_quality --------------------------- + +static auto PyGetMaxGraphicsQuality(PyObject* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + if (g_base->graphics + && g_base->graphics->has_supports_high_quality_graphics_value() + && g_base->graphics->supports_high_quality_graphics()) { + return Py_BuildValue("s", "High"); + } else { + return Py_BuildValue("s", "Medium"); + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetMaxGraphicsQualityDef = { + "get_max_graphics_quality", // name + PyGetMaxGraphicsQuality, // method + METH_VARARGS, // flags + + "get_max_graphics_quality() -> str\n" + "\n" + "(internal)\n" + "\n" + "Return the max graphics-quality supported on the current hardware.", +}; + +// ------------------------------ evaluate_lstr -------------------------------- + +static auto PyEvaluateLstr(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* value; + static const char* kwlist[] = {"value", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &value)) { + return nullptr; + } + return PyUnicode_FromString( + g_base->assets->CompileResourceString(value, "evaluate_lstr").c_str()); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyEvaluateLstrDef = { + "evaluate_lstr", // name + (PyCFunction)PyEvaluateLstr, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "evaluate_lstr(value: str) -> str\n" + "\n" + "(internal)", +}; + +// --------------------------- get_string_height ------------------------------- + +static auto PyGetStringHeight(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + std::string s; + int suppress_warning = 0; + PyObject* s_obj; + static const char* kwlist[] = {"string", "suppress_warning", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|i", + const_cast(kwlist), &s_obj, + &suppress_warning)) { + return nullptr; + } + if (!suppress_warning) { + BA_LOG_PYTHON_TRACE( + "get_string_height() use is heavily discouraged as it reduces " + "language-independence; pass suppress_warning=True if you must use " + "it."); + } + s = g_base->python->GetPyLString(s_obj); +#if BA_DEBUG_BUILD + if (g_base->assets->CompileResourceString(s, "get_string_height test") != s) { + BA_LOG_PYTHON_TRACE( + "resource-string passed to get_string_height; this should be avoided"); + } +#endif + assert(g_base->graphics); + return Py_BuildValue("f", g_base->text_graphics->GetStringHeight(s)); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetStringHeightDef = { + "get_string_height", // name + (PyCFunction)PyGetStringHeight, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_string_height(string: str, suppress_warning: bool = False) -> " + "float\n" + "\n" + "(internal)\n" + "\n" + "Given a string, returns its height using the standard small app\n" + "font.", +}; + +// ---------------------------- get_string_width ------------------------------- + +static auto PyGetStringWidth(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + std::string s; + PyObject* s_obj; + int suppress_warning = 0; + static const char* kwlist[] = {"string", "suppress_warning", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|i", + const_cast(kwlist), &s_obj, + &suppress_warning)) { + return nullptr; + } + if (!suppress_warning) { + BA_LOG_PYTHON_TRACE( + "get_string_width() use is heavily discouraged as it reduces " + "language-independence; pass suppress_warning=True if you must use " + "it."); + } + s = g_base->python->GetPyLString(s_obj); +#if BA_DEBUG_BUILD + if (g_base->assets->CompileResourceString(s, "get_string_width debug test") + != s) { + BA_LOG_PYTHON_TRACE( + "resource-string passed to get_string_width; this should be avoided"); + } +#endif + assert(g_base->graphics); + return Py_BuildValue("f", g_base->text_graphics->GetStringWidth(s)); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetStringWidthDef = { + "get_string_width", // name + (PyCFunction)PyGetStringWidth, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_string_width(string: str, suppress_warning: bool = False) -> " + "float\n" + "\n" + "(internal)\n" + "\n" + "Given a string, returns its width using the standard small app\n" + "font.", +}; + +// ------------------------------ have_chars ----------------------------------- + +static auto PyHaveChars(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + std::string text; + PyObject* text_obj; + static const char* kwlist[] = {"text", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &text_obj)) { + return nullptr; + } + text = g_base->python->GetPyLString(text_obj); + if (TextGraphics::HaveChars(text)) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PyHaveCharsDef = { + "have_chars", // name + (PyCFunction)PyHaveChars, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "have_chars(text: str) -> bool\n" + "\n" + "(internal)", +}; + +// ----------------------------- fade_screen ----------------------------------- + +static auto PyFadeScreen(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + // This can only be called in the UI context. + int fade{0}; + float time{0.25f}; + PyObject* endcall = nullptr; + static const char* kwlist[] = {"to", "time", "endcall", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|pfO", + const_cast(kwlist), &fade, &time, + &endcall)) { + return nullptr; + } + g_base->graphics->FadeScreen(static_cast(fade), + static_cast(1000.0f * time), endcall); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyFadeScreenDef = { + "fade_screen", // name + (PyCFunction)PyFadeScreen, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "fade_screen(to: int = 0, time: float = 0.25,\n" + " endcall: Callable[[], None] | None = None) -> None\n" + "\n" + "(internal)\n" + "\n" + "Fade the local game screen in our out from black over a duration of\n" + "time. if \"to\" is 0, the screen will fade out to black. Otherwise " + "it\n" + "will fade in from black. If endcall is provided, it will be run after " + "a\n" + "completely faded frame is drawn.", +}; + +// ---------------------- add_clean_frame_callback ----------------------------- + +static auto PyAddCleanFrameCallback(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + PyObject* call_obj; + static const char* kwlist[] = {"call", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &call_obj)) { + return nullptr; + } + g_base->graphics->AddCleanFrameCommand( + Object::New(call_obj)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyAddCleanFrameCallbackDef = { + "add_clean_frame_callback", // name + (PyCFunction)PyAddCleanFrameCallback, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "add_clean_frame_callback(call: Callable) -> None\n" + "\n" + "(internal)\n" + "\n" + "Provide an object to be called once the next non-progress-bar-frame " + "has\n" + "been rendered. Useful for queueing things to load in the background\n" + "without elongating any current progress-bar-load.", +}; + +// --------------------------- has_gamma_control ------------------------------- + +static auto PyHasGammaControl(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + // phasing this out; our old non-sdl2 mac has gamma controls but nothing newer + // does... +#if BA_OSTYPE_MACOS && !BA_SDL2_BUILD + Py_RETURN_TRUE; +#else + Py_RETURN_FALSE; +#endif + BA_PYTHON_CATCH; +} + +static PyMethodDef PyHasGammaControlDef = { + "has_gamma_control", // name + PyHasGammaControl, // method + METH_VARARGS, // flags + + "has_gamma_control() -> bool\n" + "\n" + "(internal)\n" + "\n" + "Returns whether the system can adjust overall screen gamma)", +}; + +// ------------------------- get_display_resolution ---------------------------- + +static auto PyGetDisplayResolution(PyObject* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + int x = 0; + int y = 0; + bool have_res = g_core->platform->GetDisplayResolution(&x, &y); + if (have_res) { + return Py_BuildValue("(ii)", x, y); + } else { + Py_RETURN_NONE; + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetDisplayResolutionDef = { + "get_display_resolution", // name + PyGetDisplayResolution, // method + METH_VARARGS, // flags + + "get_display_resolution() -> tuple[int, int] | None\n" + "\n" + "(internal)\n" + "\n" + "Return the currently selected display resolution for fullscreen\n" + "display. Returns None if resolutions cannot be directly set.", +}; + +// ----------------------------------------------------------------------------- + +auto PythonMethodsGraphics::GetMethods() -> std::vector { + return { + PyGetDisplayResolutionDef, + PyGetCameraPositionDef, + PyGetCameraTargetDef, + PySetCameraPositionDef, + PySetCameraTargetDef, + PySetCameraManualDef, + PyHasGammaControlDef, + PyAddCleanFrameCallbackDef, + PyHaveCharsDef, + PyFadeScreenDef, + PyScreenMessageDef, + PyGetStringWidthDef, + PyGetStringHeightDef, + PyEvaluateLstrDef, + PyGetMaxGraphicsQualityDef, + PySafeColorDef, + PyCharStrDef, + }; +} + +#pragma clang diagnostic pop + +} // namespace ballistica::base diff --git a/src/ballistica/base/python/methods/python_methods_graphics.h b/src/ballistica/base/python/methods/python_methods_graphics.h new file mode 100644 index 00000000..c8f661a1 --- /dev/null +++ b/src/ballistica/base/python/methods/python_methods_graphics.h @@ -0,0 +1,20 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PYTHON_METHODS_PYTHON_METHODS_GRAPHICS_H_ +#define BALLISTICA_BASE_PYTHON_METHODS_PYTHON_METHODS_GRAPHICS_H_ + +#include + +#include "ballistica/shared/ballistica.h" + +namespace ballistica::base { + +/// Graphics related individual python methods for our module. +class PythonMethodsGraphics { + public: + static auto GetMethods() -> std::vector; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_PYTHON_METHODS_PYTHON_METHODS_GRAPHICS_H_ diff --git a/src/ballistica/base/python/methods/python_methods_misc.cc b/src/ballistica/base/python/methods/python_methods_misc.cc new file mode 100644 index 00000000..9c5b0a67 --- /dev/null +++ b/src/ballistica/base/python/methods/python_methods_misc.cc @@ -0,0 +1,1444 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/python/methods/python_methods_misc.h" + +#include +#include + +#include "ballistica/base/app/app_config.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/assets/sound_asset.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/platform/base_platform.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/python/class/python_class_simple_sound.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/shared/generic/utils.h" + +namespace ballistica::base { + +// Ignore signed bitwise warnings; python macros do it quite a bit. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#pragma ide diagnostic ignored "RedundantCast" + +// ---------------------------- getsimplesound -------------------------------- + +static auto PyGetSimpleSound(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + { + Assets::AssetListLock lock; + return PythonClassSimpleSound::Create(g_base->assets->GetSound(name)); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetSimpleSoundDef = { + "getsimplesound", // name + (PyCFunction)PyGetSimpleSound, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "getsimplesound(name: str) -> SimpleSound\n" + "\n" + "(internal).", +}; + +// -------------------------- set_ui_input_device ------------------------------ + +static auto PySetUIInputDevice(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->InLogicThread()); + static const char* kwlist[] = {"input_device_id", nullptr}; + PyObject* input_device_id_obj = Py_None; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), + &input_device_id_obj)) { + return nullptr; + } + InputDevice* device{}; + if (input_device_id_obj != Py_None) { + device = + g_base->input->GetInputDevice(Python::GetPyInt(input_device_id_obj)); + if (!device) { + throw Exception("Invalid input-device id."); + } + } + g_base->ui->SetUIInputDevice(device); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetUIInputDeviceDef = { + "set_ui_input_device", // name + (PyCFunction)PySetUIInputDevice, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_ui_input_device(input_device_id: int | None)" + " -> None\n" + "\n" + "(internal)\n" + "\n" + "Sets the input-device that currently owns the user interface.", +}; + +// ----------------------------- hastouchscreen -------------------------------- + +static auto PyHasTouchScreen(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->InLogicThread()); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + BA_PRECONDITION(g_base->InLogicThread()); + if (g_base && g_base->input->touch_input() != nullptr) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyHasTouchScreenDef = { + "hastouchscreen", // name + (PyCFunction)PyHasTouchScreen, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "hastouchscreen() -> bool\n" + "\n" + "(internal)\n" + "\n" + "Return whether a touchscreen is present on the current device.", +}; + +// ------------------------- clipboard_is_supported ---------------------------- + +static auto PyClipboardIsSupported(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + if (g_core->platform->ClipboardIsSupported()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyClipboardIsSupportedDef = { + "clipboard_is_supported", // name + (PyCFunction)PyClipboardIsSupported, // method + METH_NOARGS, // flags + + "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 ------------------------------ + +static auto PyClipboardHasText(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + if (g_core->platform->ClipboardHasText()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyClipboardHasTextDef = { + "clipboard_has_text", // name + (PyCFunction)PyClipboardHasText, // method + METH_NOARGS, // flags + + "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 babase.clipboard_is_supported() separately.", +}; + +// --------------------------- clipboard_set_text ------------------------------ + +static auto PyClipboardSetText(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* value; + static const char* kwlist[] = {"value", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &value)) { + return nullptr; + } + g_core->platform->ClipboardSetText(value); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyClipboardSetTextDef = { + "clipboard_set_text", // name + (PyCFunction)PyClipboardSetText, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "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 babase.clipboard_is_supported() returns True before adding\n" + " buttons/etc. that make use of this functionality.", +}; + +// --------------------------- clipboard_get_text ------------------------------ + +static auto PyClipboardGetText(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + return PyUnicode_FromString(g_core->platform->ClipboardGetText().c_str()); + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyClipboardGetTextDef = { + "clipboard_get_text", // name + (PyCFunction)PyClipboardGetText, // method + METH_NOARGS, // flags + + "clipboard_get_text() -> str\n" + "\n" + "Return text currently on the system clipboard.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "Ensure that babase.clipboard_has_text() returns True before calling\n" + " this function.", +}; + +// ---------------------------- is_running_on_ouya ----------------------------- + +auto PyIsRunningOnOuya(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyIsRunningOnOuyaDef = { + "is_running_on_ouya", // name + PyIsRunningOnOuya, // method + METH_VARARGS, // flags + + "is_running_on_ouya() -> bool\n" + "\n" + "(internal)", +}; + +// ------------------------------ setup_sigint --------------------------------- + +static auto PySetUpSigInt(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + if (g_core) { + g_base->platform->SetupInterruptHandling(); + } else { + Log(LogLevel::kError, "SigInt handler called before g_core exists."); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetUpSigIntDef = { + "setup_sigint", // name + (PyCFunction)PySetUpSigInt, // method + METH_NOARGS, // flags + + "setup_sigint() -> None\n" + "\n" + "(internal)", +}; + +// -------------------------- is_running_on_fire_tv ---------------------------- + +static auto PyIsRunningOnFireTV(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + if (g_core->platform->IsRunningOnFireTV()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyIsRunningOnFireTVDef = { + "is_running_on_fire_tv", // name + PyIsRunningOnFireTV, // method + METH_VARARGS, // flags + + "is_running_on_fire_tv() -> bool\n" + "\n" + "(internal)", +}; + +// ---------------------------- have_permission -------------------------------- + +static auto PyHavePermission(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + Permission permission; + PyObject* permission_obj; + static const char* kwlist[] = {"permission", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "O", const_cast(kwlist), &permission_obj)) { + return nullptr; + } + + permission = BasePython::GetPyEnum_Permission(permission_obj); + + if (g_core->platform->HavePermission(permission)) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyHavePermissionDef = { + "have_permission", // name + (PyCFunction)PyHavePermission, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "have_permission(permission: babase.Permission) -> bool\n" + "\n" + "(internal)", +}; + +// --------------------------- request_permission ------------------------------ + +static auto PyRequestPermission(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + Permission permission; + PyObject* permission_obj; + static const char* kwlist[] = {"permission", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "O", const_cast(kwlist), &permission_obj)) { + return nullptr; + } + + permission = BasePython::GetPyEnum_Permission(permission_obj); + g_core->platform->RequestPermission(permission); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyRequestPermissionDef = { + "request_permission", // name + (PyCFunction)PyRequestPermission, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "request_permission(permission: babase.Permission) -> None\n" + "\n" + "(internal)", +}; + +// ----------------------------- in_logic_thread ------------------------------- + +static auto PyInLogicThread(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + if (g_base->InLogicThread()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyInLogicThreadDef = { + "in_logic_thread", // name + (PyCFunction)PyInLogicThread, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "in_logic_thread() -> bool\n" + "\n" + "(internal)\n" + "\n" + "Returns whether or not the current thread is the logic thread.", +}; + +// ----------------------------- set_thread_name ------------------------------- + +static auto PySetThreadName(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + g_core->platform->SetCurrentThreadName(name); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetThreadNameDef = { + "set_thread_name", // name + (PyCFunction)PySetThreadName, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_thread_name(name: str) -> None\n" + "\n" + "(internal)\n" + "\n" + "Sets the name of the current thread (on platforms where this is\n" + "available). EventLoop names are only for debugging and should\n" + "not be used in logic, as naming behavior can vary across platforms.\n", +}; + +// ------------------------------ get_thread_name ------------------------------ + +static auto PyGetThreadName(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + return PyUnicode_FromString(CurrentThreadName().c_str()); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetThreadNameDef = { + "get_thread_name", // name + (PyCFunction)PyGetThreadName, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_thread_name() -> str\n" + "\n" + "(internal)\n" + "\n" + "Returns the name of the current thread.\n" + "This may vary depending on platform and should not be used in logic;\n" + "only for debugging.", +}; + +// --------------------------------- ehv --------------------------------------- + +// returns an extra hash value that can be incorporated into security checks; +// this contains things like whether console commands have been run, etc. +auto PyExtraHashValue(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + const char* h = + ((g_core->user_ran_commands || g_core->workspaces_in_use) ? "cjief3l" + : "wofocj8"); + return PyUnicode_FromString(h); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyExtraHashValueDef = { + "ehv", // name + (PyCFunction)PyExtraHashValue, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "ehv() -> None\n" + "\n" + "(internal)", +}; + +// ----------------------------- get_idle_time --------------------------------- + +static auto PyGetIdleTime(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + return PyLong_FromLong(static_cast_check_fit( // NOLINT + g_base->input ? g_base->input->input_idle_time() : 0)); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetIdleTimeDef = { + "get_idle_time", // name + PyGetIdleTime, // method + METH_VARARGS, // flags + "get_idle_time() -> int\n" + "\n" + "(internal)\n" + "\n" + "Returns the amount of time since any game input has been received.", +}; + +// ------------------------- has_user_run_commands ----------------------------- + +static auto PyHasUserRunCommands(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + if (g_core->user_ran_commands) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyHasUserRunCommandsDef = { + "has_user_run_commands", // name + PyHasUserRunCommands, // method + METH_VARARGS, // flags + "has_user_run_commands() -> bool\n" + "\n" + "(internal)", +}; + +// ---------------------------- workspaces_in_use ------------------------------ + +static auto PyWorkspacesInUse(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + if (g_core->workspaces_in_use) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyWorkspacesInUseDef = { + "workspaces_in_use", // name + PyWorkspacesInUse, // method + METH_VARARGS, // flags + "workspaces_in_use() -> bool\n" + "\n" + "(internal)\n" + "\n" + "Returns whether workspaces functionality has been enabled at\n" + "any point this run.", +}; + +// ------------------------- contains_python_dist ------------------------------ + +static auto PyContainsPythonDist(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + if (g_buildconfig.contains_python_dist()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyContainsPythonDistDef = { + "contains_python_dist", // name + PyContainsPythonDist, // method + METH_VARARGS, // flags + "contains_python_dist() -> bool\n" + "\n" + "(internal)", +}; + +// ------------------------- debug_print_py_err -------------------------------- + +static auto PyDebugPrintPyErr(PyObject* self, PyObject* args) -> PyObject* { + if (PyErr_Occurred()) { + // we pass zero here to avoid grabbing references to this exception + // which can cause objects to stick around and trip up our deletion checks + // (nodes, actors existing after their games have ended) + PyErr_PrintEx(0); + PyErr_Clear(); + } + Py_RETURN_NONE; +} + +static PyMethodDef PyDebugPrintPyErrDef = { + "debug_print_py_err", // name + PyDebugPrintPyErr, // method + METH_VARARGS, // flags + + "debug_print_py_err() -> None\n" + "\n" + "(internal)\n" + "\n" + "Debugging func for tracking leaked Python errors in the C++ layer.", +}; + +// ----------------------------- print_context --------------------------------- + +static auto PyPrintContext(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + Python::PrintContextAuto(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyPrintContextDef = { + "print_context", // name + (PyCFunction)PyPrintContext, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "print_context() -> None\n" + "\n" + "(internal)\n" + "\n" + "Prints info about the current context_ref state; for debugging.\n", +}; + +// --------------------------- print_load_info --------------------------------- + +static auto PyPrintLoadInfo(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + g_base->assets->PrintLoadInfo(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyPrintLoadInfoDef = { + "print_load_info", // name + (PyCFunction)PyPrintLoadInfo, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "print_load_info() -> None\n" + "\n" + "(internal)\n" + "\n" + "Category: **General Utility Functions**", +}; + +// -------------------------- get_replays_dir ---------------------------------- + +static auto PyGetReplaysDir(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + return PyUnicode_FromString(g_core->platform->GetReplaysDir().c_str()); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetReplaysDirDef = { + "get_replays_dir", // name + (PyCFunction)PyGetReplaysDir, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_replays_dir() -> str\n" + "\n" + "(internal)", +}; + +// --------------------- get_appconfig_default_value --------------------------- + +static auto PyGetAppConfigDefaultValue(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + const char* key = ""; + static const char* kwlist[] = {"key", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &key)) { + return nullptr; + } + const AppConfig::Entry* entry = g_base->app_config->GetEntry(key); + if (entry == nullptr) { + throw Exception("Invalid config value '" + std::string(key) + "'", + PyExcType::kValue); + } + switch (entry->GetType()) { + case AppConfig::Entry::Type::kString: + return PyUnicode_FromString(entry->DefaultStringValue().c_str()); + case AppConfig::Entry::Type::kInt: + return PyLong_FromLong(entry->DefaultIntValue()); + case AppConfig::Entry::Type::kFloat: + return PyFloat_FromDouble(entry->DefaultFloatValue()); + case AppConfig::Entry::Type::kBool: + if (entry->DefaultBoolValue()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + default: + throw Exception(PyExcType::kValue); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetAppConfigDefaultValueDef = { + "get_appconfig_default_value", // name + (PyCFunction)PyGetAppConfigDefaultValue, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_appconfig_default_value(key: str) -> Any\n" + "\n" + "(internal)", +}; + +// ---------------------- get_appconfig_builtin_keys --------------------------- + +static auto PyAppConfigGetBuiltinKeys(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + PythonRef list(PyList_New(0), PythonRef::kSteal); + for (auto&& i : g_base->app_config->entries_by_name()) { + PyList_Append(list.Get(), PyUnicode_FromString(i.first.c_str())); + } + return list.HandOver(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyAppConfigGetBuiltinKeysDef = { + "get_appconfig_builtin_keys", // name + (PyCFunction)PyAppConfigGetBuiltinKeys, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_appconfig_builtin_keys() -> list[str]\n" + "\n" + "(internal)", +}; + +// ---------------------- resolve_appconfig_value ------------------------------ + +static auto PyResolveAppConfigValue(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + + const char* key; + static const char* kwlist[] = {"key", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &key)) { + return nullptr; + } + auto entry = g_base->app_config->GetEntry(key); + if (entry == nullptr) { + throw Exception("Invalid config value '" + std::string(key) + "'.", + PyExcType::kValue); + } + switch (entry->GetType()) { + case AppConfig::Entry::Type::kString: + return PyUnicode_FromString(entry->StringValue().c_str()); + case AppConfig::Entry::Type::kInt: + return PyLong_FromLong(entry->IntValue()); + case AppConfig::Entry::Type::kFloat: + return PyFloat_FromDouble(entry->FloatValue()); + case AppConfig::Entry::Type::kBool: + if (entry->BoolValue()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + + default: + throw Exception(PyExcType::kValue); + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PyResolveAppConfigValueDef = { + "resolve_appconfig_value", // name + (PyCFunction)PyResolveAppConfigValue, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "resolve_appconfig_value(key: str) -> Any\n" + "\n" + "(internal)", +}; + +// --------------------- get_low_level_config_value ---------------------------- + +static auto PyGetLowLevelConfigValue(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + const char* key; + int default_value; + static const char* kwlist[] = {"key", "default_value", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "si", const_cast(kwlist), &key, &default_value)) + return nullptr; + return PyLong_FromLong( + g_core->platform->GetLowLevelConfigValue(key, default_value)); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetLowLevelConfigValueDef = { + "get_low_level_config_value", // name + (PyCFunction)PyGetLowLevelConfigValue, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_low_level_config_value(key: str, default_value: int) -> int\n" + "\n" + "(internal)", +}; + +// --------------------- set_low_level_config_value ---------------------------- + +static auto PySetLowLevelConfigValue(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + const char* key; + int value; + static const char* kwlist[] = {"key", "value", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "si", + const_cast(kwlist), &key, &value)) + return nullptr; + g_core->platform->SetLowLevelConfigValue(key, value); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetLowLevelConfigValueDef = { + "set_low_level_config_value", // name + (PyCFunction)PySetLowLevelConfigValue, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_low_level_config_value(key: str, value: int) -> None\n" + "\n" + "(internal)", +}; + +// --------------------- set_platform_misc_read_vals --------------------------- + +static auto PySetPlatformMiscReadVals(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + PyObject* vals_obj; + static const char* kwlist[] = {"mode", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &vals_obj)) { + return nullptr; + } + std::string vals = g_base->python->GetPyLString(vals_obj); + g_core->platform->SetPlatformMiscReadVals(vals); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetPlatformMiscReadValsDef = { + "set_platform_misc_read_vals", // name + (PyCFunction)PySetPlatformMiscReadVals, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_platform_misc_read_vals(mode: str) -> None\n" + "\n" + "(internal)", +}; + +// --------------------- get_v1_cloud_log_file_path ---------------------------- + +static auto PyGetLogFilePath(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + std::string config_dir = g_core->platform->GetConfigDirectory(); + std::string logpath = config_dir + BA_DIRSLASH + "log.json"; + return PyUnicode_FromString(logpath.c_str()); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetLogFilePathDef = { + "get_v1_cloud_log_file_path", // name + PyGetLogFilePath, // method + METH_VARARGS, // flags + + "get_v1_cloud_log_file_path() -> str\n" + "\n" + "(internal)\n" + "\n" + "Return the path to the app log file.", +}; + +// --------------------- get_volatile_data_directory --------------------------- + +static auto PyGetVolatileDataDirectory(PyObject* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + return PyUnicode_FromString( + g_core->platform->GetVolatileDataDirectory().c_str()); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetVolatileDataDirectoryDef = { + "get_volatile_data_directory", // name + PyGetVolatileDataDirectory, // method + METH_VARARGS, // flags + + "get_volatile_data_directory() -> str\n" + "\n" + "(internal)\n" + "\n" + "Return the path to the app volatile data directory.\n" + "This directory is for data generated by the app that does not\n" + "need to be backed up and can be recreated if necessary.", +}; + +// ----------------------------- is_log_full ----------------------------------- +static auto PyIsLogFull(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + if (g_core->v1_cloud_log_full) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyIsLogFullDef = { + "is_log_full", // name + PyIsLogFull, // method + METH_VARARGS, // flags + + "is_log_full() -> bool\n" + "\n" + "(internal)", +}; + +// -------------------------- get_v1_cloud_log --------------------------------- + +static auto PyGetV1CloudLog(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + std::string log_fin; + { + std::scoped_lock lock(g_core->v1_cloud_log_mutex); + log_fin = g_core->v1_cloud_log; + } + // we want to use something with error handling here since the last + // bit of this string could be truncated utf8 chars.. + return PyUnicode_FromString( + Utils::GetValidUTF8(log_fin.c_str(), "_glg1").c_str()); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetV1CloudLogDef = { + "get_v1_cloud_log", // name + (PyCFunction)PyGetV1CloudLog, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_v1_cloud_log() -> str\n" + "\n" + "(internal)", +}; + +// ---------------------------- mark_log_sent ---------------------------------- + +static auto PyMarkLogSent(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + // This way we won't try to send it at shutdown time and whatnot + g_core->did_put_v1_cloud_log = true; + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyMarkLogSentDef = { + "mark_log_sent", // name + (PyCFunction)PyMarkLogSent, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "mark_log_sent() -> None\n" + "\n" + "(internal)", +}; + +// --------------------- increment_analytics_count ----------------------------- + +auto PyIncrementAnalyticsCount(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + int increment = 1; + static const char* kwlist[] = {"name", "increment", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "s|p", const_cast(kwlist), &name, &increment)) + g_core->platform->IncrementAnalyticsCount(name, increment); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyIncrementAnalyticsCountDef = { + "increment_analytics_count", // name + (PyCFunction)PyIncrementAnalyticsCount, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "increment_analytics_count(name: str, increment: int = 1) -> None\n" + "\n" + "(internal)", +}; + +// -------------------- increment_analytics_count_raw -------------------------- + +static auto PyIncrementAnalyticsCountRaw(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + const char* name; + int increment = 1; + static const char* kwlist[] = {"name", "increment", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "s|i", const_cast(kwlist), &name, &increment)) + g_core->platform->IncrementAnalyticsCountRaw(name, increment); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyIncrementAnalyticsCountRawDef = { + "increment_analytics_counts_raw", // name + (PyCFunction)PyIncrementAnalyticsCountRaw, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "increment_analytics_counts_raw(name: str, increment: int = 1) -> None\n" + "\n" + "(internal)", +}; + +// ------------------- increment_analytics_count_raw_2 ------------------------- + +static auto PyIncrementAnalyticsCountRaw2(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + const char* name; + int uses_increment = 1; + int increment = 1; + static const char* kwlist[] = {"name", "uses_increment", "increment", + nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s|ii", + const_cast(kwlist), &name, + &uses_increment, &increment)) { + return nullptr; + } + g_core->platform->IncrementAnalyticsCountRaw2(name, uses_increment, + increment); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyIncrementAnalyticsCountRaw2Def = { + "increment_analytics_count_raw_2", // name + (PyCFunction)PyIncrementAnalyticsCountRaw2, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "increment_analytics_count_raw_2(name: str,\n" + " uses_increment: bool = True, increment: int = 1) -> None\n" + "\n" + "(internal)", +}; + +// ---------------------- submit_analytics_counts ------------------------------ + +static auto PySubmitAnalyticsCounts(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + g_core->platform->SubmitAnalyticsCounts(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySubmitAnalyticsCountsDef = { + "submit_analytics_counts", // name + (PyCFunction)PySubmitAnalyticsCounts, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "submit_analytics_counts() -> None\n" + "\n" + "(internal)", +}; + +// ------------------------- set_analytics_screen ------------------------------ + +static auto PySetAnalyticsScreen(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + const char* screen; + static const char* kwlist[] = {"screen", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &screen)) { + return nullptr; + } + g_core->platform->SetAnalyticsScreen(screen); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetAnalyticsScreenDef = { + "set_analytics_screen", // name + (PyCFunction)PySetAnalyticsScreen, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_analytics_screen(screen: str) -> None\n" + "\n" + "Used for analytics to see where in the app players spend their time.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "Generally called when opening a new window or entering some UI.\n" + "'screen' should be a string description of an app location\n" + "('Main Menu', etc.)", +}; + +// ------------------ login_adapter_get_sign_in_token -------------------------- + +static auto PyLoginAdapterGetSignInToken(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + const char* login_type; + int attempt_id; + static const char* kwlist[] = {"login_type", "attempt_id", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "si", + const_cast(kwlist), &login_type, + &attempt_id)) { + return nullptr; + } + g_base->platform->LoginAdapterGetSignInToken(login_type, attempt_id); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyLoginAdapterGetSignInTokenDef = { + "login_adapter_get_sign_in_token", // name + (PyCFunction)PyLoginAdapterGetSignInToken, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "login_adapter_get_sign_in_token(login_type: str, attempt_id: int)" + " -> None\n" + "\n" + "(internal)", +}; + +// ----------------- login_adapter_back_end_active_change ---------------------- + +static auto PyLoginAdapterBackEndActiveChange(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + const char* login_type; + int active; + static const char* kwlist[] = {"login_type", "active", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "sp", + const_cast(kwlist), &login_type, + &active)) { + return nullptr; + } + g_base->platform->LoginAdapterBackEndActiveChange(login_type, active); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyLoginAdapterBackEndActiveChangeDef = { + "login_adapter_back_end_active_change", // name + (PyCFunction)PyLoginAdapterBackEndActiveChange, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "login_adapter_back_end_active_change(login_type: str, active: bool)" + " -> None\n" + "\n" + "(internal)", +}; + +// ---------------------- set_internal_language_keys --------------------------- + +static auto PySetInternalLanguageKeys(PyObject* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + PyObject* list_obj; + PyObject* random_names_list_obj; + if (!PyArg_ParseTuple(args, "OO", &list_obj, &random_names_list_obj)) { + return nullptr; + } + BA_PRECONDITION(PyList_Check(list_obj)); + BA_PRECONDITION(PyList_Check(random_names_list_obj)); + std::unordered_map language; + int size = static_cast(PyList_GET_SIZE(list_obj)); + for (int i = 0; i < size; i++) { + PyObject* entry = PyList_GET_ITEM(list_obj, i); + if (!PyTuple_Check(entry) || PyTuple_GET_SIZE(entry) != 2 + || !PyUnicode_Check(PyTuple_GET_ITEM(entry, 0)) + || !PyUnicode_Check(PyTuple_GET_ITEM(entry, 1))) { + throw Exception("Invalid root language data."); + } + language[PyUnicode_AsUTF8(PyTuple_GET_ITEM(entry, 0))] = + PyUnicode_AsUTF8(PyTuple_GET_ITEM(entry, 1)); + } + size = static_cast(PyList_GET_SIZE(random_names_list_obj)); + std::list random_names; + for (int i = 0; i < size; i++) { + PyObject* entry = PyList_GET_ITEM(random_names_list_obj, i); + if (!PyUnicode_Check(entry)) { + throw Exception("Got non-string in random name list.", PyExcType::kType); + } + random_names.emplace_back(PyUnicode_AsUTF8(entry)); + } + Utils::SetRandomNameList(random_names); + assert(g_base->logic); + g_base->assets->SetLanguageKeys(language); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetInternalLanguageKeysDef = { + "set_internal_language_keys", // name + PySetInternalLanguageKeys, // method + METH_VARARGS, // flags + + "set_internal_language_keys(listobj: list[tuple[str, str]],\n" + " random_names_list: list[tuple[str, str]]) -> None\n" + "\n" + "(internal)", +}; + +// -------------------- android_get_external_files_dir ------------------------- + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "ConstantFunctionResult" + +static auto PyAndroidGetExternalFilesDir(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + if (g_buildconfig.ostype_android()) { + std::string path = g_core->platform->AndroidGetExternalFilesDir(); + if (path.empty()) { + Py_RETURN_NONE; + } else { + assert(Utils::IsValidUTF8(path)); + return PyUnicode_FromString(path.c_str()); + } + } else { + throw Exception("Only valid on android."); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyAndroidGetExternalFilesDirDef = { + "android_get_external_files_dir", // name + (PyCFunction)PyAndroidGetExternalFilesDir, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "android_get_external_files_dir() -> str\n" + "\n" + "(internal)\n" + "\n" + "Returns the android external storage path, or None if there is none " + "on\n" + "this device", +}; + +#pragma clang diagnostic pop + +// --------------------- android_show_wifi_settings ---------------------------- + +static auto PyAndroidShowWifiSettings(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + g_core->platform->AndroidShowWifiSettings(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyAndroidShowWifiSettingsDef = { + "android_show_wifi_settings", // name + (PyCFunction)PyAndroidShowWifiSettings, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "android_show_wifi_settings() -> None\n" + "\n" + "(internal)", +}; + +// ------------------------------- do_once ------------------------------------- + +static auto PyDoOnce(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + if (g_base->python->DoOnce()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyDoOnceDef = { + "do_once", // name + (PyCFunction)PyDoOnce, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "do_once() -> bool\n" + "\n" + "Return whether this is the first time running a line of code.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "This is used by 'print_once()' type calls to keep from overflowing\n" + "logs. The call functions by registering the filename and line where\n" + "The call is made from. Returns True if this location has not been\n" + "registered already, and False if it has.\n" + "\n" + "##### Example\n" + "This print will only fire for the first loop iteration:\n" + ">>> for i in range(10):\n" + "... if babase.do_once():\n" + "... print('HelloWorld once from loop!')", +}; + +// -------------------------------- _app --------------------------------------- + +static auto PyApp(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + return g_base->python->objs().Get(BasePython::ObjID::kApp).NewRef(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyAppDef = { + "_app", // name + (PyCFunction)PyApp, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "_app() -> babase.App\n" + "\n" + "(internal)", +}; + +// ------------------------------ lock_all_input ------------------------------- + +static auto PyLockAllInput(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->InLogicThread()); + assert(g_base->input); + g_base->input->LockAllInput(false, Python::GetPythonFileLocation()); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyLockAllInputDef = { + "lock_all_input", // name + PyLockAllInput, // method + METH_VARARGS, // flags + + "lock_all_input() -> None\n" + "\n" + "(internal)\n" + "\n" + "Prevents all keyboard, mouse, and gamepad events from being " + "processed.", +}; + +// ---------------------------- unlock_all_input ------------------------------- + +static auto PyUnlockAllInput(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->InLogicThread()); + assert(g_base->input); + g_base->input->UnlockAllInput(false, Python::GetPythonFileLocation()); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyUnlockAllInputDef = { + "unlock_all_input", // name + PyUnlockAllInput, // method + METH_VARARGS, // flags + + "unlock_all_input() -> None\n" + "\n" + "(internal)\n" + "\n" + "Resumes normal keyboard, mouse, and gamepad event processing.", +}; + +// ----------------------------------------------------------------------------- + +auto PythonMethodsMisc::GetMethods() -> std::vector { + return { + PyClipboardIsSupportedDef, + PyClipboardHasTextDef, + PyClipboardSetTextDef, + PyClipboardGetTextDef, + PyDoOnceDef, + PyAppDef, + PyAndroidGetExternalFilesDirDef, + PyAndroidShowWifiSettingsDef, + PySetInternalLanguageKeysDef, + PySetAnalyticsScreenDef, + PyLoginAdapterGetSignInTokenDef, + PyLoginAdapterBackEndActiveChangeDef, + PySubmitAnalyticsCountsDef, + PyIncrementAnalyticsCountRawDef, + PyIncrementAnalyticsCountRaw2Def, + PyIncrementAnalyticsCountDef, + PyMarkLogSentDef, + PyGetV1CloudLogDef, + PyIsLogFullDef, + PyGetLogFilePathDef, + PyGetVolatileDataDirectoryDef, + PySetPlatformMiscReadValsDef, + PySetLowLevelConfigValueDef, + PyGetLowLevelConfigValueDef, + PyResolveAppConfigValueDef, + PyGetAppConfigDefaultValueDef, + PyAppConfigGetBuiltinKeysDef, + PyGetReplaysDirDef, + PyPrintLoadInfoDef, + PyPrintContextDef, + PyDebugPrintPyErrDef, + PyWorkspacesInUseDef, + PyHasUserRunCommandsDef, + PyContainsPythonDistDef, + PyGetIdleTimeDef, + PyExtraHashValueDef, + PySetUIInputDeviceDef, + PyGetThreadNameDef, + PySetThreadNameDef, + PyInLogicThreadDef, + PyRequestPermissionDef, + PyHavePermissionDef, + PyIsRunningOnFireTVDef, + PyIsRunningOnOuyaDef, + PyUnlockAllInputDef, + PyLockAllInputDef, + PySetUpSigIntDef, + PyGetSimpleSoundDef, + PyHasTouchScreenDef, + }; +} + +#pragma clang diagnostic pop + +} // namespace ballistica::base diff --git a/src/ballistica/base/python/methods/python_methods_misc.h b/src/ballistica/base/python/methods/python_methods_misc.h new file mode 100644 index 00000000..d3f0b57c --- /dev/null +++ b/src/ballistica/base/python/methods/python_methods_misc.h @@ -0,0 +1,20 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PYTHON_METHODS_PYTHON_METHODS_MISC_H_ +#define BALLISTICA_BASE_PYTHON_METHODS_PYTHON_METHODS_MISC_H_ + +#include + +#include "ballistica/shared/ballistica.h" + +namespace ballistica::base { + +/// Methods that don't have a clear home. Should try to clear this out. +class PythonMethodsMisc { + public: + static auto GetMethods() -> std::vector; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_PYTHON_METHODS_PYTHON_METHODS_MISC_H_ diff --git a/src/ballistica/base/python/support/python_context_call.cc b/src/ballistica/base/python/support/python_context_call.cc new file mode 100644 index 00000000..a37ddffb --- /dev/null +++ b/src/ballistica/base/python/support/python_context_call.cc @@ -0,0 +1,169 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/python/support/python_context_call.h" + +#include "ballistica/base/logic/logic.h" +#include "ballistica/core/python/core_python.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" + +namespace ballistica::base { + +PythonContextCall* PythonContextCall::current_call_{}; + +PythonContextCall::PythonContextCall(PyObject* obj_in) { + assert(g_base->InLogicThread()); + // As a sanity test, store the current context ptr just to make sure it + // hasn't changed when we run. Contexts dying should result in us getting + // invalidated so this should always match if we are running. + // if (g_buildconfig.debug_build()) { + // context_target_sanity_test_ = context_state_.Get(); + // } + BA_PRECONDITION(PyCallable_Check(obj_in)); + object_.Acquire(obj_in); + GetTrace(); + + // Inform the context that we are being added to it. It may want to + // grab a weak-ref to us and inform us when it is going down. + if (auto* context = context_state_.Get()) { + context->RegisterContextCall(this); + } +} + +PythonContextCall::~PythonContextCall() { + if (!context_state_.IsExpired()) { + // If our context still exists, let's use it while we take our stuff down + // (we may be holding refs to actors or whatnot). + base::ScopedSetContext ssc(context_state_); + object_.Release(); + } else { + // Otherwise go with an empty context I guess. + base::ScopedSetContext ssc(base::ContextRef(nullptr)); + object_.Release(); + } +} + +auto PythonContextCall::GetObjectDescription() const -> std::string { + return ""; +} + +void PythonContextCall::GetTrace() { + // Grab the file/line now in case we error + // (useful for debugging simple timers and callbacks and such). + file_loc_ = Python::GetPythonFileLocation(); +} + +// Called by our owning context when it goes down. +// We should clear ourself out to be a no-op if we still happen to be called. +void PythonContextCall::MarkDead() { + dead_ = true; + object_.Release(); +} + +void PythonContextCall::Run(PyObject* args) { + assert(this); + + // We implicitly use core globals; don't normally do this. + assert(g_core); + + if (dead_ || context_state_.IsExpired()) { + return; + } + + // Restore the context from when we were made. + base::ScopedSetContext ssc(context_state_); + + // Hold a ref to this call throughout this process + // so we know it'll still exist if we need to report + // exception info and whatnot. + Object::Ref keep_alive_ref(this); + + PythonContextCall* prev_call = current_call_; + current_call_ = this; + assert(Python::HaveGIL()); + PyObject* o = + PyObject_Call(object_.Get(), + args ? args + : g_core->python->objs() + .Get(core::CorePython::ObjID::kEmptyTuple) + .Get(), + nullptr); + current_call_ = prev_call; + + if (o) { + Py_DECREF(o); + } else { + // Save/restore python error or it can mess with context print calls. + BA_PYTHON_ERROR_SAVE; + + PySys_WriteStderr("Exception in Python call:\n"); + PrintContext(); + BA_PYTHON_ERROR_RESTORE; + + // We pass zero here to avoid grabbing references to this exception + // which can cause objects to stick around and trip up our deletion checks. + // (nodes, actors existing after their games have ended). + PyErr_PrintEx(0); + PyErr_Clear(); + } +} + +void PythonContextCall::PrintContext() { + assert(g_base->InLogicThread()); + std::string s = std::string(" root call: ") + object().Str() + "\n"; + s += (" root call origin: " + file_loc() + "\n"); + s += Python::GetContextBaseString(); + PySys_WriteStderr("%s\n", s.c_str()); +} + +void PythonContextCall::Schedule() { + // Since we're mucking with Object::Refs, need to limit to logic thread. + BA_PRECONDITION(g_base->InLogicThread()); + Object::Ref ref(this); + + assert(base::g_base); + base::g_base->logic->event_loop()->PushCall([ref] { + assert(ref.Exists()); + ref->Run(); + }); +} + +void PythonContextCall::Schedule(const PythonRef& args) { + // Since we're mucking with Object::Refs, need to limit to logic thread. + BA_PRECONDITION(g_base->InLogicThread()); + Object::Ref ref(this); + assert(base::g_base); + base::g_base->logic->event_loop()->PushCall([ref, args] { + assert(ref.Exists()); + ref->Run(args); + }); +} + +void PythonContextCall::ScheduleWeak() { + // Since we're mucking with Object::WeakRefs, need to limit to logic thread. + BA_PRECONDITION(g_base->InLogicThread()); + Object::WeakRef ref(this); + assert(base::g_base); + base::g_base->logic->event_loop()->PushCall([ref] { + if (auto* call = ref.Get()) { + call->Run(); + } + }); +} + +void PythonContextCall::ScheduleWeak(const PythonRef& args) { + // Since we're mucking with Object::WeakRefs, need to limit to logic thread. + BA_PRECONDITION(g_base->InLogicThread()); + Object::WeakRef ref(this); + assert(base::g_base); + base::g_base->logic->event_loop()->PushCall([ref, args] { + if (auto* call = ref.Get()) { + call->Run(args); + } + }); +} + +} // namespace ballistica::base diff --git a/src/ballistica/base/python/support/python_context_call.h b/src/ballistica/base/python/support/python_context_call.h new file mode 100644 index 00000000..e753740f --- /dev/null +++ b/src/ballistica/base/python/support/python_context_call.h @@ -0,0 +1,74 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PYTHON_SUPPORT_PYTHON_CONTEXT_CALL_H_ +#define BALLISTICA_BASE_PYTHON_SUPPORT_PYTHON_CONTEXT_CALL_H_ + +#include + +#include "ballistica/base/support/context.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_ref.h" + +namespace ballistica::base { + +// A callable and ballistica context-state wrapped up in a convenient package. +// Handy for use with user-submitted callbacks, as it restores context +// state from when it was created and prints various useful bits of context +// info on exceptions. +class PythonContextCall : public Object { + public: + static auto current_call() -> PythonContextCall* { return current_call_; } + PythonContextCall() = default; + ~PythonContextCall() override; + + /// Initialize with a raw callable Python object. + explicit PythonContextCall(PyObject* callable); + + /// Initialize with a callable PythonRef. + explicit PythonContextCall(const PythonRef& ref) + : PythonContextCall(ref.Get()) {} + + void Run(PyObject* args = nullptr); + void Run(const PythonRef& args) { Run(args.Get()); } + auto Exists() const -> bool { return object_.Exists(); } + auto GetObjectDescription() const -> std::string override; + void MarkDead(); + auto object() const -> const PythonRef& { return object_; } + auto file_loc() const -> const std::string& { return file_loc_; } + void PrintContext(); + + /// Run in an upcoming cycle of the logic thread. + /// Must be called from the logic thread. + /// This form creates a strong-reference so the context_ref-call is guaranteed + /// to exist until run. + void Schedule(); + /// Run in an upcoming cycle of the logic thread with provided args. + /// Must be called from the logic thread. + /// This form creates a strong-reference so the context_ref-call is guaranteed + /// to exist until run. + void Schedule(const PythonRef& args); + /// Run in an upcoming cycle of the logic thread. + /// Must be called from the logic thread. + /// This form creates a weak-reference and is a no-op if the context_ref-call + /// is destroyed before its scheduled run. + void ScheduleWeak(); + /// Run in an upcoming cycle of the logic thread with provided args. + /// Must be called from the logic thread. + /// This form creates a weak-reference and is a no-op if the context_ref-call + /// is destroyed before its scheduled run. + void ScheduleWeak(const PythonRef& args); + + private: + void GetTrace(); // we try to grab basic trace info + std::string file_loc_; + int line_{}; + bool dead_ = false; + PythonRef object_; + base::ContextRef context_state_; + // base::Context* context_target_sanity_test_{}; + static PythonContextCall* current_call_; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_PYTHON_SUPPORT_PYTHON_CONTEXT_CALL_H_ diff --git a/src/ballistica/base/python/support/python_context_call_runnable.h b/src/ballistica/base/python/support/python_context_call_runnable.h new file mode 100644 index 00000000..66235e83 --- /dev/null +++ b/src/ballistica/base/python/support/python_context_call_runnable.h @@ -0,0 +1,26 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_PYTHON_SUPPORT_PYTHON_CONTEXT_CALL_RUNNABLE_H_ +#define BALLISTICA_BASE_PYTHON_SUPPORT_PYTHON_CONTEXT_CALL_RUNNABLE_H_ + +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/shared/generic/runnable.h" + +namespace ballistica::base { + +// a simple runnable that stores and runs a python context call +class PythonContextCallRunnable : public Runnable { + public: + explicit PythonContextCallRunnable(PyObject* o) + : call(Object::New(o)) {} + Object::Ref call; + void Run() override { + assert(call.Exists()); + call->Run(); + } + ~PythonContextCallRunnable() override = default; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_PYTHON_SUPPORT_PYTHON_CONTEXT_CALL_RUNNABLE_H_ diff --git a/src/ballistica/base/support/app_timer.h b/src/ballistica/base/support/app_timer.h new file mode 100644 index 00000000..f6f8ba0e --- /dev/null +++ b/src/ballistica/base/support/app_timer.h @@ -0,0 +1,44 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_SUPPORT_APP_TIMER_H_ +#define BALLISTICA_BASE_SUPPORT_APP_TIMER_H_ + +#include "ballistica/base/base.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/generic/lambda_runnable.h" + +namespace ballistica::base { + +class AppTimer : public Object { + public: + AppTimer(millisecs_t length, bool repeat, + const Object::Ref& runnable) { + assert(g_base->InLogicThread()); + timer_id_ = base::g_base->logic->NewAppTimer(length, repeat, runnable); + } + + void SetLength(uint32_t length) { + assert(g_base->InLogicThread()); + base::g_base->logic->SetAppTimerLength(timer_id_, length); + } + ~AppTimer() override { + assert(g_base->InLogicThread()); + base::g_base->logic->DeleteAppTimer(timer_id_); + } + + private: + int timer_id_; +}; + +/// Create a AppTimer from a raw lambda. +template +auto NewAppTimer(millisecs_t length, bool repeat, const F& lambda) + -> Object::Ref { + return Object::New(length, repeat, NewLambdaRunnable(lambda)); +} + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_SUPPORT_APP_TIMER_H_ diff --git a/src/ballistica/base/support/context.cc b/src/ballistica/base/support/context.cc new file mode 100644 index 00000000..fefd6a3b --- /dev/null +++ b/src/ballistica/base/support/context.cc @@ -0,0 +1,66 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/base/support/context.h" + +namespace ballistica::base { + +ContextRef::ContextRef() + : target_(g_base->context_ref->target_), + empty_(g_base->context_ref->empty_) { + assert(g_base->InLogicThread()); +} + +auto ContextRef::operator==(const ContextRef& other) const -> bool { + // If our pointer matches theirs and our empty state matches theirs, + // we're equal. The one exception to this is if we're both pointing to + // targets that have died; in that case we have no way of knowing so + // we say we're unequal. + if (target_.Get() == other.target_.Get() && empty_ == other.empty_) { + if (!empty_ && target_.Get() == nullptr) { + return false; + } + return true; + } + return false; +} + +ContextRef::ContextRef(Context* target_in) + : target_(target_in), empty_(target_in == nullptr) {} + +auto ContextRef::GetDescription() const -> std::string { + if (auto* c = target_.Get()) { + return c->GetContextDescription(); + } + return "empty"; +} +void Context::RegisterContextCall(PythonContextCall* call) {} + +auto Context::GetContextDescription() -> std::string { + return GetObjectDescription(); +} + +auto Context::ContextAllowsDefaultTimerTypes() -> bool { return true; } + +ScopedSetContext::ScopedSetContext(const Object::Ref& target) + : context_prev_(g_base->CurrentContext()) { + g_base->context_ref->SetTarget(target.Get()); +} + +ScopedSetContext::ScopedSetContext(Context* target) + : context_prev_(g_base->CurrentContext()) { + g_base->context_ref->SetTarget(target); +} + +ScopedSetContext::ScopedSetContext(const ContextRef& context) + : context_prev_(g_base->CurrentContext()) { + *g_base->context_ref = context; +} + +ScopedSetContext::~ScopedSetContext() { + assert(g_base); + assert(g_base->InLogicThread()); + // Restore old. + *g_base->context_ref = context_prev_; +} + +} // namespace ballistica::base diff --git a/src/ballistica/base/support/context.h b/src/ballistica/base/support/context.h new file mode 100644 index 00000000..4f468467 --- /dev/null +++ b/src/ballistica/base/support/context.h @@ -0,0 +1,157 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_SUPPORT_CONTEXT_H_ +#define BALLISTICA_BASE_SUPPORT_CONTEXT_H_ + +#include + +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::base { + +// Ballistica's context system allows its various subsystems to provide +// arbitrary contextual data for commands to use. Standard callbacks and +// other mechanisms are set up to preserve and restore context before +// running, and objects can also be invalidated or otherwise cleaned up +// when the context they were created under dies. +// The end goal of all this is to support api styles for end users where +// standalone snippets of code can be useful; ie: something like +// bs.newnode() to create something meaningful without having to worry +// about acquiring a scene pointer or whatever. + +// FIXME: Once we have death-callbacks for objects, we should update this +// to be aware once a pointed-to context has died. Attempting to use the +// context-ref in any way after that point should error. Currently it just +// functions as an empty context in that case which is incorrect. + +/// A utility class wrapping a weak-reference to a context_ref with some +/// extra functionality. +class ContextRef { + public: + /// The current global context-ref; + // static auto Current() -> const ContextRef& { + // assert(g_base); + // assert(g_base->InLogicThread()); // Up to caller to ensure this. + // return *g_base->context_ref; + // } + + /// Set the current global context from this one. + // static void SetCurrent(const ContextRef& context) { + // assert(g_base); + // // Context system is currently limited to logic-thread. + // BA_PRECONDITION(g_base->InLogicThread()); + // g_base->context_ref->SetTarget(context.Get()); + // } + + /// Return a description of the context we're pointing at. + auto GetDescription() const -> std::string; + + /// Default constructor grabs the current context. + ContextRef(); + explicit ContextRef(Context* sgc); + + /// ContextRefs are considered equal if both are pointing to the exact same + /// Context object (or both are pointing to no Context). + auto operator==(const ContextRef& other) const -> bool; + + template + auto GetContextTyped() const -> T* { + // Ew; dynamic cast. + // Note: if it ever seems like speed is an issue here, we can + // cache the results with std::type_index entries. There should + // generally be a very small number of types involved. + return dynamic_cast(target_.Get()); + } + + /// An empty context-ref was explicitly set to an empty state. + /// Note that this is different than an expired context-ref, which + /// originally pointed to some context that has since died. + auto IsEmpty() const { return empty_; } + + /// Has this context died since it was set? + /// Note that a context created as empty is not considered expired. + auto IsExpired() const -> bool { + if (empty_) { + return false; // Can't kill what was never alive. + } + return !target_.Exists(); + } + + /// Return the context this ref points to. This will be nullptr for empty + /// contexts. Throws an exception if a target context was set but has expired. + auto Get() const -> Context* { + auto* target = target_.Get(); + if (target == nullptr && !empty_) { + // We once existed but now don't. + throw Exception("Context is expired.", PyExcType::kNotFound); + } + return target; + } + + void SetTarget(Context* target) { + target_ = target; + empty_ = (target == nullptr); + } + + private: + Object::WeakRef target_; + bool empty_; +}; + +/// Object containing the actual context_ref data/information. +/// App-modes can subclass this to provide the actual context_ref they desire, +/// and then code can use CurrentTyped() to safely retrieve context_ref as that +/// type. +class Context : public Object { + public: + /// Return the current context_ref cast to a desired type. + /// Throws an Exception if the context_ref is unset or is another type. + template + static auto CurrentTyped() -> T& { + T* t = g_base->CurrentContext().GetContextTyped(); + if (t == nullptr) { + throw Exception("Context of the provided type is not set.", + PyExcType::kContext); + } + return *t; + } + + /// Called when a PythonContextCall is created in this context_ref. + /// The context_ref class may want to store a weak-reference to the + /// call and inform the call when the context_ref is going down so that + /// resources may be freed. Other permanent contexts may not need to + /// bother. + /// FIXME: This mechanism can probably be generalized so that other + /// things such as assets and timers can use it. + virtual void RegisterContextCall(PythonContextCall* call); + + /// Return a short description of the context_ref; will be used when + /// printing context_ref debug information/etc. By default this uses + /// Object::GetObjectDescription(). + virtual auto GetContextDescription() -> std::string; + + /// Return whether this context should allow default timer-types to be + /// created within it (AppTimer, DisplayTimer). Scene type contexts + /// generally have their own timer types which are better integrated + /// with scenes (responding to changes in game speed/etc.) so this can + /// be used to encourage/enforce usage of those timers. + virtual auto ContextAllowsDefaultTimerTypes() -> bool; +}; + +// Use this to push/pop a change to the current context +class ScopedSetContext { + public: + explicit ScopedSetContext(const Object::Ref& context); + explicit ScopedSetContext(Context* context); + explicit ScopedSetContext(const ContextRef& context); + ~ScopedSetContext(); + + private: + BA_DISALLOW_CLASS_COPIES(ScopedSetContext); + ContextRef context_prev_; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_SUPPORT_CONTEXT_H_ diff --git a/src/ballistica/generic/huffman.cc b/src/ballistica/base/support/huffman.cc similarity index 99% rename from src/ballistica/generic/huffman.cc rename to src/ballistica/base/support/huffman.cc index 0aec4919..dd0f94d6 100644 --- a/src/ballistica/generic/huffman.cc +++ b/src/ballistica/base/support/huffman.cc @@ -1,10 +1,10 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/generic/huffman.h" +#include "ballistica/base/support/huffman.h" -#include "ballistica/networking/networking.h" +#include "ballistica/base/networking/networking.h" -namespace ballistica { +namespace ballistica::base { // Yes, I should clean this up to use unsigned vals, but it seems to work // fine for now so I don't want to touch it. @@ -585,4 +585,4 @@ void Huffman::build() { #pragma clang diagnostic pop -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/generic/huffman.h b/src/ballistica/base/support/huffman.h similarity index 82% rename from src/ballistica/generic/huffman.h rename to src/ballistica/base/support/huffman.h index 44a17f53..6c68e2ae 100644 --- a/src/ballistica/generic/huffman.h +++ b/src/ballistica/base/support/huffman.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GENERIC_HUFFMAN_H_ -#define BALLISTICA_GENERIC_HUFFMAN_H_ +#ifndef BALLISTICA_BASE_SUPPORT_HUFFMAN_H_ +#define BALLISTICA_BASE_SUPPORT_HUFFMAN_H_ #include -#include "ballistica/core/object.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::base { class Huffman { public: @@ -56,6 +56,6 @@ class Huffman { Node nodes_[511]; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_GENERIC_HUFFMAN_H_ +#endif // BALLISTICA_BASE_SUPPORT_HUFFMAN_H_ diff --git a/src/ballistica/base/support/plus_soft.h b/src/ballistica/base/support/plus_soft.h new file mode 100644 index 00000000..c9ff873c --- /dev/null +++ b/src/ballistica/base/support/plus_soft.h @@ -0,0 +1,65 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_SUPPORT_PLUS_SOFT_H_ +#define BALLISTICA_BASE_SUPPORT_PLUS_SOFT_H_ + +#include +#include +#include + +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/foundation/feature_set_front_end.h" + +namespace ballistica::base { + +/// 'Soft' interface to the plus feature-set. +/// Feature-sets listing plus as a soft requirement must limit their use of +/// plus to these methods and should be prepared to handle the not-present +/// case. +class PlusSoftInterface { + public: + virtual void OnAppStart() = 0; + virtual void OnAppPause() = 0; + virtual void OnAppResume() = 0; + virtual void OnAppShutdown() = 0; + virtual void ApplyAppConfig() = 0; + virtual void OnScreenSizeChange() = 0; + virtual void StepDisplayTime() = 0; + + virtual auto IsUnmodifiedBlessedBuild() -> bool = 0; + + virtual auto HasBlessingHash() -> bool = 0; + virtual auto PutLog(bool fatal) -> bool = 0; + virtual void AAT() = 0; + virtual void AATE() = 0; + virtual void V1LoginDidChange() = 0; + virtual void SetAdCompletionCall(PyObject* obj, + bool pass_actually_showed) = 0; + virtual void PushAdViewComplete(const std::string& purpose, + bool actually_showed) = 0; + virtual void PushPublicPartyState() = 0; + virtual void PushSetFriendListCall( + const std::vector& friends) = 0; + virtual void DispatchRemoteAchievementList( + const std::set& achs) = 0; + virtual void PushAnalyticsCall(const std::string& type, int increment) = 0; + virtual void PushPurchaseTransactionCall(const std::string& item, + const std::string& receipt, + const std::string& signature, + const std::string& order_id, + bool user_initiated) = 0; + virtual auto GetPublicV1AccountID() -> std::string = 0; + virtual void DirectSendV1CloudLogs(const std::string& prefix, + const std::string& suffix, bool instant, + int* result) = 0; + virtual void ClientInfoQuery(const std::string& val1, const std::string& val2, + const std::string& val3, int build_number) = 0; + virtual auto CalcV1PeerHash(const std::string& peer_hash_input) + -> std::string = 0; + virtual void V1SetClientInfo(JsonDict* dict) = 0; + virtual void DoPushSubmitAnalyticsCountsCall(const std::string& sval) = 0; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_SUPPORT_PLUS_SOFT_H_ diff --git a/src/ballistica/platform/stdio_console.cc b/src/ballistica/base/support/stdio_console.cc similarity index 52% rename from src/ballistica/platform/stdio_console.cc rename to src/ballistica/base/support/stdio_console.cc index fb1d9196..30084ca9 100644 --- a/src/ballistica/platform/stdio_console.cc +++ b/src/ballistica/base/support/stdio_console.cc @@ -1,39 +1,40 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/platform/stdio_console.h" +#include "ballistica/base/support/stdio_console.h" -#if BA_OSTYPE_LINUX #include -#endif -#include "ballistica/app/app.h" -#include "ballistica/core/thread.h" -#include "ballistica/logic/logic.h" -#include "ballistica/platform/platform.h" +#include "ballistica/base/app/app.h" +#include "ballistica/base/app/app_mode.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/support/context.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python_command.h" +#include "ballistica/shared/python/python_sys.h" -namespace ballistica { +namespace ballistica::base { -StdioConsole::StdioConsole() { - // We're a singleton; make sure we don't already exist. - assert(g_stdio_console == nullptr); +StdioConsole::StdioConsole() = default; + +void StdioConsole::OnMainThreadStartApp() { + // Ok, we're spinning up an app. // Spin up our thread. - thread_ = new Thread(ThreadTag::kAssets); - g_app->pausable_threads.push_back(thread_); -} + event_loop_ = new EventLoop(EventLoopID::kStdin); + g_core->pausable_event_loops.push_back(event_loop_); -auto StdioConsole::OnAppStart() -> void { // Tell our thread to start reading. - thread()->PushCall([this] { - bool stdin_is_terminal = g_platform->is_stdin_a_terminal(); + event_loop()->PushCall([this] { + bool stdin_is_terminal = g_core->platform->is_stdin_a_terminal(); while (true) { // Print a prompt if we're a tty. // We send this to the logic thread so it happens AFTER the // results of the last script-command message we may have just sent. if (stdin_is_terminal) { - g_logic->thread()->PushCall([] { - if (!g_app->shutting_down) { + g_base->logic->event_loop()->PushCall([] { + if (!g_core->shutting_down) { printf(">>> "); fflush(stdout); } @@ -49,11 +50,11 @@ auto StdioConsole::OnAppStart() -> void { if (val) { if (val == std::string("@clear\n")) { int retval{-1}; -#if BA_OSTYPE_MACOS || BA_OSTYPE_LINUX - // Attempt to run actual clear command on unix-y systems to - // plop our prompt back at the top of the screen. - retval = system("clear"); -#endif + if (g_buildconfig.ostype_macos() || g_buildconfig.ostype_linux()) { + // Attempt to run actual clear command on unix-y systems to + // plop our prompt back at the top of the screen. + retval = core::CorePlatform::System("clear"); + } // As a fallback, just spit out a bunch of newlines. if (retval != 0) { std::string space; @@ -70,7 +71,7 @@ auto StdioConsole::OnAppStart() -> void { && pending_input_[pending_input_.size() - 1] == '\n') { // Get rid of the last newline and ship it to the game. pending_input_.pop_back(); - g_logic->PushStdinScriptCommand(pending_input_); + PushCommand(pending_input_); pending_input_.clear(); } } else { @@ -86,9 +87,9 @@ auto StdioConsole::OnAppStart() -> void { // print for a second and see if a shutdown has begun first. // (or, more likely, just never print because the app has exited). if (g_buildconfig.windows_console_build()) { - Platform::SleepMS(250); + core::CorePlatform::SleepMillisecs(250); } - if (!g_app->shutting_down) { + if (!g_core->shutting_down) { printf("Stdin EOF reached. Use Ctrl-C to quit.\n"); fflush(stdout); } @@ -103,4 +104,33 @@ auto StdioConsole::OnAppStart() -> void { }); } -} // namespace ballistica +void StdioConsole::PushCommand(const std::string& command) { + g_base->logic->event_loop()->PushCall([command] { + // These are always run in whichever context is 'visible'. + ScopedSetContext ssc(g_base->app_mode->GetForegroundContext()); + PythonCommand cmd(command, ""); + if (!g_core->user_ran_commands) { + g_core->user_ran_commands = true; + } + + // Eval this if possible (so we can possibly print return value). + if (cmd.CanEval()) { + auto obj = cmd.Eval(true, nullptr, nullptr); + if (obj.Exists()) { + // Print the value if we're running directly from a terminal + // (or being run under the server-manager) + if ((g_core->platform->is_stdin_a_terminal() + || g_base->app->server_wrapper_managed()) + && obj.Get() != Py_None) { + printf("%s\n", obj.Repr().c_str()); + fflush(stdout); + } + } + } else { + // Can't eval it; exec it. + cmd.Exec(true, nullptr, nullptr); + } + }); +} + +} // namespace ballistica::base diff --git a/src/ballistica/base/support/stdio_console.h b/src/ballistica/base/support/stdio_console.h new file mode 100644 index 00000000..35e8e806 --- /dev/null +++ b/src/ballistica/base/support/stdio_console.h @@ -0,0 +1,24 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_SUPPORT_STDIO_CONSOLE_H_ +#define BALLISTICA_BASE_SUPPORT_STDIO_CONSOLE_H_ + +#include "ballistica/shared/ballistica.h" + +namespace ballistica::base { + +class StdioConsole { + public: + StdioConsole(); + void OnMainThreadStartApp(); + auto event_loop() const -> EventLoop* { return event_loop_; } + + private: + void PushCommand(const std::string& command); + EventLoop* event_loop_{}; + std::string pending_input_; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_SUPPORT_STDIO_CONSOLE_H_ diff --git a/src/ballistica/ui/console.cc b/src/ballistica/base/ui/console.cc similarity index 76% rename from src/ballistica/ui/console.cc rename to src/ballistica/base/ui/console.cc index 89cff2e0..a1d58b2d 100644 --- a/src/ballistica/ui/console.cc +++ b/src/ballistica/base/ui/console.cc @@ -1,16 +1,21 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/ui/console.h" +#include "ballistica/base/ui/console.h" -#include "ballistica/app/app.h" -#include "ballistica/audio/audio.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/graphics/text/text_graphics.h" -#include "ballistica/logic/logic.h" -#include "ballistica/platform/min_sdl.h" +#include "ballistica/base/app/app_mode.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/graphics/text/text_graphics.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/core/core.h" +#include "ballistica/core/platform/support/min_sdl.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python_command.h" +#include "ballistica/shared/python/python_sys.h" -namespace ballistica { +namespace ballistica::base { // How much of the screen the console covers when it is at full size. const float kConsoleSize = 0.9f; @@ -21,9 +26,9 @@ const int kActivateKey1 = SDLK_BACKQUOTE; const int kActivateKey2 = SDLK_F2; Console::Console() { - assert(InLogicThread()); - std::string title = std::string("BallisticaCore ") + kAppVersion + " (" - + std::to_string(kAppBuildNumber) + ")"; + assert(g_base->InLogicThread()); + std::string title = std::string("BallisticaKit ") + kEngineVersion + " (" + + std::to_string(kEngineBuildNumber) + ")"; if (g_buildconfig.debug_build()) { title += " (debug)"; } @@ -34,18 +39,12 @@ Console::Console() { title_text_group_.SetText(title); built_text_group_.SetText("Built: " __DATE__ " " __TIME__); prompt_text_group_.SetText(">"); - - // Print whatever is already in the log. - if (!g_app->console_startup_messages.empty()) { - Print(g_app->console_startup_messages); - g_app->console_startup_messages = ""; - } } Console::~Console() = default; auto Console::HandleKeyPress(const SDL_Keysym* keysym) -> bool { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // Handle our toggle buttons no matter whether we're active. switch (keysym->sym) { @@ -53,8 +52,10 @@ auto Console::HandleKeyPress(const SDL_Keysym* keysym) -> bool { case kActivateKey2: { if (!g_buildconfig.demo_build() && !g_buildconfig.arcade_build()) { // (reset input so characters don't continue walking and stuff) - g_logic->ResetInput(); - g_logic->ToggleConsole(); + g_base->input->ResetHoldStates(); + if (auto console = g_base->console()) { + console->ToggleState(); + } } return true; } @@ -113,7 +114,7 @@ auto Console::HandleKeyPress(const SDL_Keysym* keysym) -> bool { last_line_.clear(); lines_.clear(); } else { - g_logic->PushInGameConsoleScriptCommand(input_string_); + PushCommand(input_string_); } input_history_.push_front(input_string_); if (input_history_.size() > 100) input_history_.pop_back(); @@ -141,7 +142,29 @@ auto Console::HandleKeyPress(const SDL_Keysym* keysym) -> bool { return true; } +void Console::PushCommand(const std::string& command) { + assert(g_base); + g_base->logic->event_loop()->PushCall([command] { + // These are always run in whichever context is 'visible'. + ScopedSetContext ssc(g_base->app_mode->GetForegroundContext()); + PythonCommand cmd(command, ""); + if (!g_core->user_ran_commands) { + g_core->user_ran_commands = true; + } + if (cmd.CanEval()) { + auto obj = cmd.Eval(true, nullptr, nullptr); + if (obj.Exists() && obj.Get() != Py_None) { + g_base->console()->Print(obj.Repr() + "\n"); + } + } else { + // Not eval-able; just exec it. + cmd.Exec(true, nullptr, nullptr); + } + }); +} + void Console::ToggleState() { + assert(g_base->InLogicThread()); switch (state_) { case State::kInactive: state_ = State::kMini; @@ -153,12 +176,12 @@ void Console::ToggleState() { state_ = State::kInactive; break; } - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kBlip)); - transition_start_ = GetRealTime(); + g_base->audio->PlaySound(g_base->assets->SysSound(SysSoundID::kBlip)); + transition_start_ = g_core->GetAppTimeMillisecs(); } auto Console::HandleTextEditing(const std::string& text) -> bool { - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (state_ == State::kInactive) { return false; } @@ -186,16 +209,16 @@ auto Console::HandleKeyRelease(const SDL_Keysym* keysym) -> bool { #pragma ide diagnostic ignored "LocalValueEscapesScope" void Console::Print(const std::string& s_in) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); std::string s = Utils::GetValidUTF8(s_in.c_str(), "cspr"); last_line_ += s; std::vector broken_up; - g_text_graphics->BreakUpString(last_line_.c_str(), kStringBreakUpSize, - &broken_up); + g_base->text_graphics->BreakUpString(last_line_.c_str(), kStringBreakUpSize, + &broken_up); // Spit out all completed lines and keep the last one as lastline. for (size_t i = 0; i < broken_up.size() - 1; i++) { - lines_.emplace_back(broken_up[i], GetRealTime()); + lines_.emplace_back(broken_up[i], g_core->GetAppTimeMillisecs()); if (lines_.size() > kConsoleLineLimit) { lines_.pop_front(); } @@ -210,9 +233,11 @@ void Console::Draw(RenderPass* pass) { millisecs_t transition_ticks = 100; if ((transition_start_ != 0) && (state_ != State::kInactive - || ((GetRealTime() - transition_start_) < transition_ticks))) { - float ratio = (static_cast(GetRealTime() - transition_start_) - / static_cast(transition_ticks)); + || ((g_core->GetAppTimeMillisecs() - transition_start_) + < transition_ticks))) { + float ratio = + (static_cast(g_core->GetAppTimeMillisecs() - transition_start_) + / static_cast(transition_ticks)); float bottom; float mini_size = 90; if (state_ == State::kMini) { @@ -220,7 +245,7 @@ void Console::Draw(RenderPass* pass) { } else { bottom = pass->virtual_height() - pass->virtual_height() * kConsoleSize; } - if (GetRealTime() - transition_start_ < transition_ticks) { + if (g_core->GetAppTimeMillisecs() - transition_start_ < transition_ticks) { if (state_ == State::kMini) { bottom = pass->virtual_height() * (1.0f - ratio) + bottom * (ratio); } else if (state_ == State::kFull) { @@ -309,10 +334,11 @@ void Console::Draw(RenderPass* pass) { c.SetTransparent(true); c.SetColor(1, 1, 1, 0.7f); c.PushTransform(); - c.Translate(19.0f + g_text_graphics->GetStringWidth(input_string_) * 0.5f, - bottom + 23.0f, kConsoleZDepth); + c.Translate( + 19.0f + g_base->text_graphics->GetStringWidth(input_string_) * 0.5f, + bottom + 23.0f, kConsoleZDepth); c.Scale(5, 11, 1.0f); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1)); c.PopTransform(); c.Submit(); } @@ -324,12 +350,12 @@ void Console::Draw(RenderPass* pass) { c.SetTransparent(true); c.SetColor(1, 1, 1, 1); float h = 0.5f - * (g_graphics->screen_virtual_width() + * (g_base->graphics->screen_virtual_width() - (kStringBreakUpSize * draw_scale)); float v = bottom + 32.0f; if (!last_line_.empty()) { if (last_line_mesh_dirty_) { - if (!last_line_mesh_group_.exists()) { + if (!last_line_mesh_group_.Exists()) { last_line_mesh_group_ = Object::New(); } last_line_mesh_group_->SetText(last_line_); @@ -347,13 +373,13 @@ void Console::Draw(RenderPass* pass) { v += 14; } for (auto i = lines_.rbegin(); i != lines_.rend(); i++) { - int elem_count = i->getText().GetElementCount(); + int elem_count = i->GetText().GetElementCount(); for (int e = 0; e < elem_count; e++) { - c.SetTexture(i->getText().GetElementTexture(e)); + c.SetTexture(i->GetText().GetElementTexture(e)); c.PushTransform(); c.Translate(h, v + 2, kConsoleZDepth); c.Scale(draw_scale, draw_scale); - c.DrawMesh(i->getText().GetElementMesh(e)); + c.DrawMesh(i->GetText().GetElementMesh(e)); c.PopTransform(); } v += 14; @@ -366,4 +392,4 @@ void Console::Draw(RenderPass* pass) { } } -} // namespace ballistica +} // namespace ballistica::base diff --git a/src/ballistica/ui/console.h b/src/ballistica/base/ui/console.h similarity index 79% rename from src/ballistica/ui/console.h rename to src/ballistica/base/ui/console.h index 473422e5..3b3332d7 100644 --- a/src/ballistica/ui/console.h +++ b/src/ballistica/base/ui/console.h @@ -1,23 +1,22 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_UI_CONSOLE_H_ -#define BALLISTICA_UI_CONSOLE_H_ +#ifndef BALLISTICA_BASE_UI_CONSOLE_H_ +#define BALLISTICA_BASE_UI_CONSOLE_H_ #include #include #include -#include "ballistica/core/object.h" -#include "ballistica/graphics/renderer.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::base { class Console { public: Console(); ~Console(); auto active() const -> bool { return (state_ != State::kInactive); } - enum class State { kInactive, kMini, kFull }; auto transition_start() const -> millisecs_t { return transition_start_; } auto HandleTextEditing(const std::string& text) -> bool; auto HandleKeyPress(const SDL_Keysym* keysym) -> bool; @@ -27,6 +26,8 @@ class Console { void Draw(RenderPass* pass); private: + void PushCommand(const std::string& command); + enum class State { kInactive, kMini, kFull }; ImageMesh bg_mesh_; ImageMesh stripe_mesh_; ImageMesh shadow_mesh_; @@ -45,8 +46,8 @@ class Console { : creation_time(c), s(std::move(s_in)) {} millisecs_t creation_time; std::string s; - auto getText() -> TextGroup& { - if (!s_mesh_.exists()) { + auto GetText() -> TextGroup& { + if (!s_mesh_.Exists()) { s_mesh_ = Object::New(); s_mesh_->SetText(s); } @@ -56,6 +57,7 @@ class Console { private: Object::Ref s_mesh_; }; + std::string input_string_; std::list input_history_; int input_history_position_{}; @@ -65,6 +67,6 @@ class Console { bool last_line_mesh_dirty_{true}; }; -} // namespace ballistica +} // namespace ballistica::base -#endif // BALLISTICA_UI_CONSOLE_H_ +#endif // BALLISTICA_BASE_UI_CONSOLE_H_ diff --git a/src/ballistica/ui/ui.cc b/src/ballistica/base/ui/ui.cc similarity index 51% rename from src/ballistica/ui/ui.cc rename to src/ballistica/base/ui/ui.cc index 70ebca16..e33b423c 100644 --- a/src/ballistica/ui/ui.cc +++ b/src/ballistica/base/ui/ui.cc @@ -1,30 +1,30 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/ui/ui.h" +#include "ballistica/base/ui/ui.h" -#include "ballistica/app/app.h" -#include "ballistica/assets/component/data.h" -#include "ballistica/assets/component/sound.h" -#include "ballistica/audio/audio.h" -#include "ballistica/core/thread.h" -#include "ballistica/generic/lambda_runnable.h" -#include "ballistica/graphics/component/empty_component.h" -#include "ballistica/input/device/input_device.h" -#include "ballistica/input/input.h" -#include "ballistica/python/python.h" -#include "ballistica/scene/scene.h" -#include "ballistica/ui/root_ui.h" -#include "ballistica/ui/widget/button_widget.h" -#include "ballistica/ui/widget/root_widget.h" -#include "ballistica/ui/widget/stack_widget.h" +#include "ballistica/base/app/app_config.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/graphics/component/empty_component.h" +#include "ballistica/base/input/device/keyboard_input.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/ui/console.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/ui_v1/python/ui_v1_python.h" +#include "ballistica/ui_v1/support/root_ui.h" +#include "ballistica/ui_v1/widget/root_widget.h" +#include "ballistica/ui_v1/widget/stack_widget.h" +#include "ballistica/ui_v1/widget/text_widget.h" -namespace ballistica { +namespace ballistica::base { static const int kUIOwnerTimeoutSeconds = 30; UI::UI() { // Figure out our interface type. - assert(g_platform); + assert(g_core); // Allow overriding via an environment variable. auto* ui_override = getenv("BA_UI_SCALE"); @@ -44,19 +44,22 @@ UI::UI() { // Use automatic val. if (g_buildconfig.iircade_build()) { // NOLINT(bugprone-branch-clone) scale_ = UIScale::kMedium; - } else if (IsVRMode() || g_platform->IsRunningOnTV()) { + } else if (g_core->IsVRMode() || g_core->platform->IsRunningOnTV()) { // VR and tv builds always use medium. scale_ = UIScale::kMedium; } else { - scale_ = g_platform->GetUIScale(); + scale_ = g_core->platform->GetUIScale(); } } } -auto UI::OnAppStart() -> void { - assert(InLogicThread()); +void UI::StepDisplayTime() { assert(g_base->InLogicThread()); } + +void UI::OnAppStart() { + assert(g_base->InLogicThread()); + + root_ui_ = new ui_v1::RootUI(); - root_ui_ = new RootUI(); // Make sure we know when forced-ui-scale is enabled. if (force_scale_) { if (scale_ == UIScale::kSmall) { @@ -72,23 +75,27 @@ auto UI::OnAppStart() -> void { FatalError("Unhandled scale."); } } - - step_scene_timer_ = - base_timers_.NewTimer(base_time_, kGameStepMilliseconds, 0, -1, - NewLambdaRunnable([this] { StepScene(); })); - scene_ = Object::New(0); } -// Currently the UI never dies so we don't bother doing a clean tear-down.. -// (verifying scene cleanup, etc) -UI::~UI() { - assert(root_ui_); - delete root_ui_; +void UI::OnAppPause() { assert(g_base->InLogicThread()); } + +void UI::OnAppResume() { + assert(g_base->InLogicThread()); + SetUIInputDevice(nullptr); +} + +void UI::OnAppShutdown() { assert(g_base->InLogicThread()); } + +void UI::ApplyAppConfig() { + assert(g_base->InLogicThread()); + ui_v1::TextWidget::set_always_use_internal_keyboard( + g_base->app_config->Resolve( + AppConfig::BoolID::kAlwaysUseInternalKeyboard)); } void UI::PushBackButtonCall(InputDevice* input_device) { - g_logic->thread()->PushCall([this, input_device] { - assert(InLogicThread()); + g_base->logic->event_loop()->PushCall([this, input_device] { + assert(g_base->InLogicThread()); // Ignore if UI isn't up yet. if (!overlay_root_widget() || !screen_root_widget()) { @@ -108,87 +115,45 @@ void UI::PushBackButtonCall(InputDevice* input_device) { } void UI::PushMainMenuPressCall(InputDevice* device) { - g_logic->thread()->PushCall([this, device] { MainMenuPress(device); }); + g_base->logic->event_loop()->PushCall( + [this, device] { MainMenuPress(device); }); } void UI::MainMenuPress(InputDevice* device) { - assert(InLogicThread()); - g_python->HandleDeviceMenuPress(device); + assert(g_base->InLogicThread()); + g_ui_v1->python->HandleDeviceMenuPress(device); } auto UI::IsWindowPresent() const -> bool { - return ((screen_root_widget_.exists() && screen_root_widget_->HasChildren()) - || (overlay_root_widget_.exists() + return ((screen_root_widget_.Exists() && screen_root_widget_->HasChildren()) + || (overlay_root_widget_.Exists() && overlay_root_widget_->HasChildren())); } void UI::SetUIInputDevice(InputDevice* input_device) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); ui_input_device_ = input_device; // So they dont get stolen from immediately. - last_input_device_use_time_ = GetRealTime(); + last_input_device_use_time_ = g_core->GetAppTimeMillisecs(); } UI::UILock::UILock(bool write) { - assert(g_ui); - assert(InLogicThread()); + assert(g_base->ui); + assert(g_base->InLogicThread()); - if (write && g_ui->ui_lock_count_ != 0) { + if (write && g_base->ui->ui_lock_count_ != 0) { BA_LOG_ERROR_TRACE_ONCE("Illegal operation: UI is locked"); } - g_ui->ui_lock_count_++; + g_base->ui->ui_lock_count_++; } UI::UILock::~UILock() { - g_ui->ui_lock_count_--; - if (g_ui->ui_lock_count_ < 0) { + g_base->ui->ui_lock_count_--; + if (g_base->ui->ui_lock_count_ < 0) { BA_LOG_ERROR_TRACE_ONCE("ui_lock_count_ < 0"); - g_ui->ui_lock_count_ = 0; - } -} - -void UI::StepScene() { - auto s = scene(); - sim_timers_.Run(s->time()); - s->Step(); -} - -void UI::Update(millisecs_t time_advance) { - assert(InLogicThread()); - - millisecs_t target_base_time = base_time_ + time_advance; - while (!base_timers_.empty() - && (base_time_ + base_timers_.GetTimeToNextExpire(base_time_) - <= target_base_time)) { - base_time_ += base_timers_.GetTimeToNextExpire(base_time_); - base_timers_.Run(base_time_); - } - base_time_ = target_base_time; - - // Periodically prune various dead refs. - if (base_time_ > next_prune_time_) { - PruneDeadMapRefs(&textures_); - PruneDeadMapRefs(&sounds_); - PruneDeadMapRefs(&models_); - next_prune_time_ = base_time_ + 4920; - - // Since we never clear our scene, we need to watch for leaks. - // If there's more than a few nodes in existence for an extended period of - // time, complain. - if (scene_->nodes().size() > 10) { - node_warning_count_++; - if (node_warning_count_ > 3) { - static bool complained = false; - if (!complained) { - Log(LogLevel::kError, ">10 nodes in UI context!"); - complained = true; - } - } - } else { - node_warning_count_ = 0; - } + g_base->ui->ui_lock_count_ = 0; } } @@ -200,29 +165,29 @@ void UI::Reset() { screen_root_widget_.Clear(); // (Re)create our screen-root widget. - auto sw(Object::New()); + auto sw(Object::New()); sw->set_is_main_window_stack(true); - sw->SetWidth(g_graphics->screen_virtual_width()); - sw->SetHeight(g_graphics->screen_virtual_height()); + sw->SetWidth(g_base->graphics->screen_virtual_width()); + sw->SetHeight(g_base->graphics->screen_virtual_height()); sw->set_translate(0, 0); screen_root_widget_ = sw; // (Re)create our screen-overlay widget. - auto ow(Object::New()); + auto ow(Object::New()); ow->set_is_overlay_window_stack(true); - ow->SetWidth(g_graphics->screen_virtual_width()); - ow->SetHeight(g_graphics->screen_virtual_height()); + ow->SetWidth(g_base->graphics->screen_virtual_width()); + ow->SetHeight(g_base->graphics->screen_virtual_height()); ow->set_translate(0, 0); overlay_root_widget_ = ow; // (Re)create our abs-root widget. - auto rw(Object::New()); + auto rw(Object::New()); root_widget_ = rw; - rw->SetWidth(g_graphics->screen_virtual_width()); - rw->SetHeight(g_graphics->screen_virtual_height()); - rw->SetScreenWidget(sw.get()); + rw->SetWidth(g_base->graphics->screen_virtual_width()); + rw->SetHeight(g_base->graphics->screen_virtual_height()); + rw->SetScreenWidget(sw.Get()); rw->Setup(); - rw->SetOverlayWidget(ow.get()); + rw->SetOverlayWidget(ow.Get()); sw->GlobalSelect(); } @@ -232,77 +197,90 @@ auto UI::ShouldHighlightWidgets() const -> bool { // when the main UI is visible (dont want a selection highlight for toolbar // buttons during a game). return ( - g_input->have_non_touch_inputs() - && ((screen_root_widget_.exists() && screen_root_widget_->HasChildren()) - || (overlay_root_widget_.exists() + g_base->input->have_non_touch_inputs() + && ((screen_root_widget_.Exists() && screen_root_widget_->HasChildren()) + || (overlay_root_widget_.Exists() && overlay_root_widget_->HasChildren()))); } auto UI::ShouldShowButtonShortcuts() const -> bool { - return g_input->have_non_touch_inputs(); + return g_base->input->have_non_touch_inputs(); } -void UI::AddWidget(Widget* w, ContainerWidget* parent) { - assert(InLogicThread()); +void UI::AddWidget(ui_v1::Widget* w, ui_v1::ContainerWidget* parent) { + assert(g_base->InLogicThread()); BA_PRECONDITION(parent != nullptr); - // If they're adding an initial window/dialog to our screen-stack, - // send a reset-local-input message so that characters who have lost focus - // will not get stuck running or whatnot. - if (screen_root_widget_.exists() && !screen_root_widget_->HasChildren() - && parent == &(*screen_root_widget_)) { - g_logic->ResetInput(); + // If they're adding an initial window/dialog to our screen-stack + // or overlay stack, send a reset-local-input message so that characters + // who have lost focus will not get stuck running or whatnot. + // We should come up with a more generalized way to track this sort of + // focus as this is a bit hacky, but it works for now. + auto* screen_root_widget = screen_root_widget_.Get(); + auto* overlay_root_widget = overlay_root_widget_.Get(); + if ((screen_root_widget && !screen_root_widget->HasChildren() + && parent == screen_root_widget) + || (overlay_root_widget && !overlay_root_widget->HasChildren() + && parent == overlay_root_widget)) { + g_base->input->ResetHoldStates(); } parent->AddWidget(w); } auto UI::SendWidgetMessage(const WidgetMessage& m) -> int { - if (!root_widget_.exists()) { + if (!root_widget_.Exists()) { return false; } return root_widget_->HandleMessage(m); } -void UI::DeleteWidget(Widget* widget) { +void UI::DeleteWidget(ui_v1::Widget* widget) { assert(widget); if (widget) { - ContainerWidget* parent = widget->parent_widget(); + ui_v1::ContainerWidget* parent = widget->parent_widget(); if (parent) { parent->DeleteWidget(widget); } } } -void UI::ScreenSizeChanged() { - if (root_widget_.exists()) { - root_widget_->SetWidth(g_graphics->screen_virtual_width()); - root_widget_->SetHeight(g_graphics->screen_virtual_height()); +void UI::OnScreenSizeChange() { + if (root_widget_.Exists()) { + root_widget_->SetWidth(g_base->graphics->screen_virtual_width()); + root_widget_->SetHeight(g_base->graphics->screen_virtual_height()); + } +} + +void UI::LanguageChanged() { + // As well as existing UI stuff. + if (ui_v1::Widget* root_widget = g_base->ui->root_widget()) { + root_widget->OnLanguageChange(); } } auto UI::GetUIInputDevice() const -> InputDevice* { - assert(InLogicThread()); - return ui_input_device_.get(); + assert(g_base->InLogicThread()); + return ui_input_device_.Get(); } -auto UI::GetWidgetForInput(InputDevice* input_device) -> Widget* { +auto UI::GetWidgetForInput(InputDevice* input_device) -> ui_v1::Widget* { assert(input_device); - assert(InLogicThread()); + assert(g_base->InLogicThread()); // We only allow input-devices to control the UI when there's a window/dialog // on the screen (even though our top/bottom bars still exist). - if ((!screen_root_widget_.exists() || (!screen_root_widget_->HasChildren())) - && (!overlay_root_widget_.exists() + if ((!screen_root_widget_.Exists() || (!screen_root_widget_->HasChildren())) + && (!overlay_root_widget_.Exists() || (!overlay_root_widget_->HasChildren()))) { return nullptr; } - millisecs_t time = GetRealTime(); + millisecs_t time = g_core->GetAppTimeMillisecs(); bool print_menu_owner = false; - Widget* ret_val; + ui_v1::Widget* ret_val; // Ok here's the deal: // Because having 10 controllers attached to the UI is pure chaos, @@ -312,19 +290,20 @@ auto UI::GetWidgetForInput(InputDevice* input_device) -> Widget* { if ((GetUIInputDevice() == nullptr) || (input_device == GetUIInputDevice()) || (time - last_input_device_use_time_ > (1000 * kUIOwnerTimeoutSeconds)) - || !g_input->HaveManyLocalActiveInputDevices()) { + || !g_base->input->HaveManyLocalActiveInputDevices()) { // Don't actually assign yet; only update times and owners if there's a // widget to be had (we don't want some guy who moved his character 3 // seconds ago to automatically own a newly created widget). last_input_device_use_time_ = time; ui_input_device_ = input_device; - ret_val = screen_root_widget_.get(); + ret_val = screen_root_widget_.Get(); } else { // For rejected input devices, play error sounds sometimes so they know // they're not the chosen one. if (time - last_widget_input_reject_err_sound_time_ > 5000) { last_widget_input_reject_err_sound_time_ = time; - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kErrorBeep)); + g_base->audio->PlaySound( + g_base->assets->SysSound(SysSoundID::kErrorBeep)); print_menu_owner = true; } ret_val = nullptr; // Rejected! @@ -338,23 +317,24 @@ auto UI::GetWidgetForInput(InputDevice* input_device) -> Widget* { kUIOwnerTimeoutSeconds - (time - last_input_device_use_time_) / 1000; std::string time_out_str; if (timeout > 0 && timeout < (kUIOwnerTimeoutSeconds - 10)) { - time_out_str = " " + g_logic->GetResourceString("timeOutText"); + time_out_str = " " + g_base->assets->GetResourceString("timeOutText"); Utils::StringReplaceOne(&time_out_str, "${TIME}", std::to_string(timeout)); } else { - time_out_str = " " + g_logic->GetResourceString("willTimeOutText"); + time_out_str = + " " + g_base->assets->GetResourceString("willTimeOutText"); } std::string name; if (input->GetDeviceName() == "Keyboard") { - name = g_logic->GetResourceString("keyboardText"); + name = g_base->assets->GetResourceString("keyboardText"); } else if (input->GetDeviceName() == "TouchScreen") { - name = g_logic->GetResourceString("touchScreenText"); + name = g_base->assets->GetResourceString("touchScreenText"); } else { // We used to use player names here, but that's kinda sloppy and random; // lets just go with device names/numbers. auto devicesWithName = - g_input->GetInputDevicesWithName(input->GetDeviceName()); + g_base->input->GetInputDevicesWithName(input->GetDeviceName()); if (devicesWithName.size() == 1) { // If there's just one, no need to tack on the '#2' or whatever. name = input->GetDeviceName(); @@ -364,7 +344,7 @@ auto UI::GetWidgetForInput(InputDevice* input_device) -> Widget* { } } - std::string b = g_logic->GetResourceString("hasMenuControlText"); + std::string b = g_base->assets->GetResourceString("hasMenuControlText"); Utils::StringReplaceOne(&b, "${NAME}", name); ScreenMessage(b + time_out_str, {0.45f, 0.4f, 0.5f}); } @@ -372,74 +352,21 @@ auto UI::GetWidgetForInput(InputDevice* input_device) -> Widget* { return ret_val; } -auto UI::GetModel(const std::string& name) -> Object::Ref { - return Assets::GetAsset(&models_, name, scene()); -} - -auto UI::GetTexture(const std::string& name) -> Object::Ref { - return Assets::GetAsset(&textures_, name, scene()); -} - -auto UI::GetSound(const std::string& name) -> Object::Ref { - return Assets::GetAsset(&sounds_, name, scene()); -} - -auto UI::GetData(const std::string& name) -> Object::Ref { - return Assets::GetAsset(&datas_, name, scene()); -} - -auto UI::GetAsUIContext() -> UI* { return this; } - -auto UI::GetMutableScene() -> Scene* { - Scene* sg = scene_.get(); - assert(sg); - return sg; -} - -auto UI::NewTimer(TimeType timetype, TimerMedium length, bool repeat, - const Object::Ref& runnable) -> int { - // All of our stuff is just real-time; lets just map all timer options to - // that. - switch (timetype) { - case TimeType::kSim: - case TimeType::kBase: - case TimeType::kReal: - return g_logic->NewRealTimer(length, repeat, runnable); - default: - // Fall back to default for descriptive error otherwise. - return ContextTarget::NewTimer(timetype, length, repeat, runnable); - } -} - -void UI::DeleteTimer(TimeType timetype, int timer_id) { - switch (timetype) { - case TimeType::kSim: - case TimeType::kBase: - case TimeType::kReal: - g_logic->DeleteRealTimer(timer_id); - break; - default: - // Fall back to default for descriptive error otherwise. - ContextTarget::DeleteTimer(timetype, timer_id); - break; - } -} - -auto UI::Draw(FrameDef* frame_def) -> void { +void UI::Draw(FrameDef* frame_def) { RenderPass* overlay_flat_pass = frame_def->GetOverlayFlatPass(); // Draw interface elements. - auto* root_widget = root_widget_.get(); + auto* root_widget = root_widget_.Get(); if (root_widget && root_widget->HasChildren()) { // Draw our opaque and transparent parts separately. // This way we can draw front-to-back for opaque and back-to-front for // transparent. - g_graphics->set_drawing_opaque_only(true); + g_base->graphics->set_drawing_opaque_only(true); // Do a wee bit of shifting based on tilt just for fun. - Vector3f tilt = 0.1f * g_graphics->tilt(); + Vector3f tilt = 0.1f * g_base->graphics->tilt(); { EmptyComponent c(overlay_flat_pass); c.SetTransparent(false); @@ -454,8 +381,8 @@ auto UI::Draw(FrameDef* frame_def) -> void { c.Submit(); } - g_graphics->set_drawing_opaque_only(false); - g_graphics->set_drawing_transparent_only(true); + g_base->graphics->set_drawing_opaque_only(false); + g_base->graphics->set_drawing_transparent_only(true); { EmptyComponent c(overlay_flat_pass); @@ -471,17 +398,46 @@ auto UI::Draw(FrameDef* frame_def) -> void { c.Submit(); } - g_graphics->set_drawing_transparent_only(false); + g_base->graphics->set_drawing_transparent_only(false); } if (root_ui_) { root_ui_->Draw(frame_def); } } -// void UI::DrawExtras(FrameDef* frame_def) { -// assert(frame_def != nullptr); -// assert(root_ui_ != nullptr); -// root_ui_->Draw(frame_def); -// } +void UI::ShowURL(const std::string& url) { g_ui_v1->python->ShowURL(url); } -} // namespace ballistica +void UI::ConfirmQuit() { + g_base->logic->event_loop()->PushCall([] { + assert(g_base->InLogicThread()); + if (g_core->HeadlessMode()) { + Log(LogLevel::kError, "UI::ConfirmQuit() unhandled on headless."); + } else { + // If input is locked or the in-app-console is up, just quit immediately; + // a confirm screen wouldn't work anyway. + if (g_base->input->IsInputLocked() + || (g_base->console() != nullptr && g_base->console()->active())) { + // Just go through _babase.quit(). + // FIXME: Shouldn't need to go out to the Python layer here; + // once we've got a high level quit call in platform we can use + // that directly. + g_base->python->objs().Get(BasePython::ObjID::kQuitCall).Call(); + return; + } else { + ScopedSetContext ssc(nullptr); + g_base->audio->PlaySound(g_base->assets->SysSound(SysSoundID::kSwish)); + g_ui_v1->python->objs() + .Get(ui_v1::UIV1Python::ObjID::kQuitWindowCall) + .Call(); + + // If we have a keyboard, give it UI ownership. + InputDevice* keyboard = g_base->input->keyboard_input(); + if (keyboard) { + g_base->ui->SetUIInputDevice(keyboard); + } + } + } + }); +} + +} // namespace ballistica::base diff --git a/src/ballistica/base/ui/ui.h b/src/ballistica/base/ui/ui.h new file mode 100644 index 00000000..ea60e45e --- /dev/null +++ b/src/ballistica/base/ui/ui.h @@ -0,0 +1,147 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_UI_UI_H_ +#define BALLISTICA_BASE_UI_UI_H_ + +#include +#include + +#include "ballistica/base/support/context.h" +#include "ballistica/shared/generic/timer_list.h" +#include "ballistica/ui_v1/widget/widget.h" + +// UI-Locks: make sure widget-lists don't change under you. +// Use a read-lock if you just need to ensure lists remain intact but won't be +// changing anything. Use a write-lock whenever modifying a list. +#if BA_DEBUG_BUILD +#define BA_DEBUG_UI_READ_LOCK ::ballistica::base::UI::UILock ui_lock(false) +#define BA_DEBUG_UI_WRITE_LOCK ::ballistica::base::UI::UILock ui_lock(true) +#else +#define BA_DEBUG_UI_READ_LOCK +#define BA_DEBUG_UI_WRITE_LOCK +#endif +#define BA_UI_READ_LOCK UI::UILock ui_lock(false) +#define BA_UI_WRITE_LOCK UI::UILock ui_lock(true) + +namespace ballistica::base { + +// Our global UI subsystem. This wrangles all app +class UI { + public: + UI(); + + void OnAppStart(); + void OnAppPause(); + void OnAppResume(); + void OnAppShutdown(); + void ApplyAppConfig(); + void OnScreenSizeChange(); + void StepDisplayTime(); + + void LanguageChanged(); + + void Reset(); + + // auto IsCurrentContext() const -> bool; + + /// Pop up an in-game window to show a url (NOT in a browser). + /// Can be called from any thread. + void ShowURL(const std::string& url); + + /// High level call to request a quit ui (or in some cases quit immediately). + /// This can be called from any thread. + void ConfirmQuit(); + + // Return the root widget containing all windows & dialogs + // Whenever this contains children, the UI is considered to be in focus + auto screen_root_widget() -> ui_v1::ContainerWidget* { + return screen_root_widget_.Get(); + } + + auto overlay_root_widget() -> ui_v1::ContainerWidget* { + return overlay_root_widget_.Get(); + } + + /// Return whether there is UI present in either the main or overlay + /// stacks. Generally this implies the focus should be on the UI. + auto IsWindowPresent() const -> bool; + + // Return the absolute root widget; this includes persistent UI + // bits such as the top/bottom bars + auto root_widget() -> ui_v1::RootWidget* { return root_widget_.Get(); } + + void Draw(FrameDef* frame_def); + + // Returns the widget an input should send commands to, if any. + // Also potentially locks other inputs out of controlling the UI, + // so only call this if you intend on sending a message to that widget. + auto GetWidgetForInput(InputDevice* input_device) -> ui_v1::Widget*; + + // Add a widget to a container. + // If a parent is provided, the widget is added to it; otherwise it is added + // to the root widget. + void AddWidget(ui_v1::Widget* w, ui_v1::ContainerWidget* to); + + // Send message to the active widget. + auto SendWidgetMessage(const WidgetMessage& msg) -> int; + + // Use this to destroy any named widget (even those in containers). + void DeleteWidget(ui_v1::Widget* widget); + + void SetUIInputDevice(InputDevice* input_device); + + // Returns the input-device that currently owns the menu; otherwise nullptr. + auto GetUIInputDevice() const -> InputDevice*; + + void PushBackButtonCall(InputDevice* input_device); + + // Returns whether currently selected widgets should flash. + // This will be false in some situations such as when only touch screen + // control is active. + auto ShouldHighlightWidgets() const -> bool; + + // Same except for button shortcuts; these generally only get shown + // if a joystick of some form is present. + auto ShouldShowButtonShortcuts() const -> bool; + + // Used to ensure widgets are not created or destroyed at certain times + // (while traversing widget hierarchy, etc). + class UILock { + public: + explicit UILock(bool write); + ~UILock(); + + private: + BA_DISALLOW_CLASS_COPIES(UILock); + }; + + auto root_ui() const -> ui_v1::RootUI* { + assert(root_ui_); + return root_ui_; + } + + auto scale() const { return scale_; } + + /// Push a generic 'menu press' event, optionally associated with an + /// input device (nullptr to specify none). Note: caller must ensure + /// a RemoveInputDevice() call does not arrive at the logic thread + /// before this one. + void PushMainMenuPressCall(InputDevice* device); + + private: + void MainMenuPress(InputDevice* device); + ui_v1::RootUI* root_ui_{}; + Object::WeakRef ui_input_device_; + millisecs_t last_input_device_use_time_{}; + millisecs_t last_widget_input_reject_err_sound_time_{}; + Object::Ref screen_root_widget_; + Object::Ref overlay_root_widget_; + Object::Ref root_widget_; + int ui_lock_count_{}; + UIScale scale_{UIScale::kLarge}; + bool force_scale_{}; +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_UI_UI_H_ diff --git a/src/ballistica/base/ui/widget_message.h b/src/ballistica/base/ui/widget_message.h new file mode 100644 index 00000000..0e409662 --- /dev/null +++ b/src/ballistica/base/ui/widget_message.h @@ -0,0 +1,71 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_BASE_UI_WIDGET_MESSAGE_H_ +#define BALLISTICA_BASE_UI_WIDGET_MESSAGE_H_ + +#include + +#include "ballistica/core/platform/support/min_sdl.h" + +namespace ballistica::base { + +// Messages descriptions sent to widgets. +struct WidgetMessage { + enum class Type { + kEmptyMessage, + kMoveUp, + kMoveDown, + kMoveLeft, + kMoveRight, + kActivate, + kStart, + kCancel, + kShow, + // In order to work in all-joystick environments, + // don't rely on the following to be available (they're just a luxury). + kKey, + kTabNext, + kTabPrev, + kMouseDown, + kMouseUp, + kMouseWheel, + kMouseWheelH, + kMouseWheelVelocity, + kMouseWheelVelocityH, + kMouseMove, + kScrollMouseDown, + kTextInput, + kPaste + }; + + Type type{}; + bool has_keysym{}; + SDL_Keysym keysym{}; + float fval1{}; + float fval2{}; + float fval3{}; + float fval4{}; + std::string* sval{}; + + explicit WidgetMessage(Type t = Type::kEmptyMessage, + const SDL_Keysym* k = nullptr, float f1 = 0, + float f2 = 0, float f3 = 0, float f4 = 0, + const char* s = nullptr) + : type(t), has_keysym(false), fval1(f1), fval2(f2), fval3(f3), fval4(f4) { + if (k) { + keysym = *k; + has_keysym = true; + } + if (s) { + sval = new std::string(); + *sval = s; + } else { + sval = nullptr; + } + } + ~WidgetMessage() { delete sval; } +}; + +} // namespace ballistica::base + +#endif // BALLISTICA_BASE_UI_WIDGET_MESSAGE_H_ diff --git a/src/ballistica/classic/README.md b/src/ballistica/classic/README.md new file mode 100644 index 00000000..ad2e6be1 --- /dev/null +++ b/src/ballistica/classic/README.md @@ -0,0 +1,4 @@ +# Classic Feature Set + +This feature set contains bits which are required to keep old functionality going +but which should not generally be used for new features. \ No newline at end of file diff --git a/src/ballistica/classic/classic.cc b/src/ballistica/classic/classic.cc new file mode 100644 index 00000000..2acee019 --- /dev/null +++ b/src/ballistica/classic/classic.cc @@ -0,0 +1,59 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/classic/classic.h" + +#include "ballistica/classic/python/classic_python.h" +#include "ballistica/classic/support/v1_account.h" + +namespace ballistica::classic { + +core::CoreFeatureSet* g_core{}; +base::BaseFeatureSet* g_base{}; +ClassicFeatureSet* g_classic{}; + +void ClassicFeatureSet::OnModuleExec(PyObject* module) { + // Ok, our feature-set's Python module is getting imported. + // Like any normal Python module, we take this opportunity to + // import/create the stuff we use. + + // Importing core should always be the first thing we do. + // Various ballistica functionality will fail if this has not been done. + assert(g_core == nullptr); + g_core = core::CoreFeatureSet::Import(); + + g_core->BootLog("_baclassic exec begin"); + + // Create our feature-set's C++ front-end. + assert(g_classic == nullptr); + g_classic = new ClassicFeatureSet(); + + // Store our C++ front-end with our Python module. + // This lets anyone get at us by going through the Python + // import system (keeping things nice and consistent between + // Python and C++ worlds). + g_classic->StoreOnPythonModule(module); + + // Import any Python stuff we use into objs_. + g_classic->python->ImportPythonObjs(); + + // Import any other C++ feature-set-front-ends we use. + assert(g_base == nullptr); // Should be getting set once here. + g_base = base::BaseFeatureSet::Import(); + + g_core->BootLog("_baclassic exec end"); +} + +ClassicFeatureSet::ClassicFeatureSet() + : python{new ClassicPython()}, v1_account{new V1Account()} { + // We're a singleton. If there's already one of us, something's wrong. + assert(g_classic == nullptr); +} + +auto ClassicFeatureSet::Import() -> ClassicFeatureSet* { + // Since we provide a native Python module, we piggyback our C++ front-end + // on top of that. This way our C++ and Python dependencies are resolved + // consistently no matter which side we are imported from. + return ImportThroughPythonModule("_baclassic"); +} + +} // namespace ballistica::classic diff --git a/src/ballistica/classic/classic.h b/src/ballistica/classic/classic.h new file mode 100644 index 00000000..0b509d86 --- /dev/null +++ b/src/ballistica/classic/classic.h @@ -0,0 +1,78 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CLASSIC_CLASSIC_H_ +#define BALLISTICA_CLASSIC_CLASSIC_H_ + +#include "ballistica/shared/foundation/feature_set_front_end.h" + +// Common header that most everything using our feature-set should include. +// It predeclares our feature-set's various types and globals and other +// bits. + +// Predeclared types from other feature sets that we use. +namespace ballistica::core { +class CoreFeatureSet; +} +namespace ballistica::base { +class BaseFeatureSet; +} + +namespace ballistica::classic { + +// Predeclared types our feature-set provides. +class ClassicFeatureSet; +class ClassicPython; +class V1Account; + +enum class V1AccountType { + kInvalid, + kTest, + kGameCenter, + kGameCircle, + kGooglePlay, + kDevice, + kServer, + kOculus, + kSteam, + kNvidiaChina, + kV2, +}; + +enum class V1LoginState { + kSignedOut, + kSigningIn, + kSignedIn, +}; + +// Our feature-set's globals. +// Feature-sets should NEVER directly access globals in another feature-set's +// namespace. All functionality we need from other feature-sets should be +// imported into globals in our own namespace. Generally we do this when we +// are initially imported (just as regular Python modules do). +extern core::CoreFeatureSet* g_core; +extern base::BaseFeatureSet* g_base; +extern ClassicFeatureSet* g_classic; + +/// Our C++ front-end to our feature set. This is what other C++ +/// feature-sets can 'Import' from us. +class ClassicFeatureSet : public FeatureSetFrontEnd { + public: + /// Instantiate our FeatureSet if needed and return the single + /// instance of it. Basically a Python import statement. + static auto Import() -> ClassicFeatureSet*; + + /// Called when our associated Python module is instantiated. + static void OnModuleExec(PyObject* module); + + ClassicPython* const python; + V1Account* const v1_account; + + V1AccountType account_type{V1AccountType::kInvalid}; + + private: + ClassicFeatureSet(); +}; + +} // namespace ballistica::classic + +#endif // BALLISTICA_CLASSIC_CLASSIC_H_ diff --git a/src/ballistica/classic/python/classic_python.cc b/src/ballistica/classic/python/classic_python.cc new file mode 100644 index 00000000..5e5a41bb --- /dev/null +++ b/src/ballistica/classic/python/classic_python.cc @@ -0,0 +1,94 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/classic/python/classic_python.h" + +#include "ballistica/base/input/device/input_device.h" +#include "ballistica/classic/python/methods/python_methods_classic.h" +#include "ballistica/shared/python/python_command.h" +#include "ballistica/shared/python/python_module_builder.h" + +namespace ballistica::classic { + +ClassicPython::ClassicPython() = default; + +// Need to declare a plain c PyInit_XXX function with our module name in it so +// we're discoverable when compiled as a standalone binary Python module. +extern "C" auto PyInit__baclassic() -> PyObject* { + auto* builder = new PythonModuleBuilder( + "_baclassic", {PythonMethodsClassic::GetMethods()}, + [](PyObject* module) -> int { + BA_PYTHON_TRY; + ClassicFeatureSet::OnModuleExec(module); + return 0; + BA_PYTHON_INT_CATCH; + }); + return builder->Build(); +} + +void ClassicPython::ImportPythonObjs() { +#include "ballistica/classic/mgen/pyembed/binding_classic.inc" +} + +void ClassicPython::PlayMusic(const std::string& music_type, bool continuous) { + BA_PRECONDITION(g_base->InLogicThread()); + if (music_type.empty()) { + PythonRef args( + Py_BuildValue("(OO)", Py_None, continuous ? Py_True : Py_False), + PythonRef::kSteal); + objs().Get(ObjID::kDoPlayMusicCall).Call(args); + } else { + PythonRef args(Py_BuildValue("(sO)", music_type.c_str(), + continuous ? Py_True : Py_False), + PythonRef::kSteal); + objs().Get(ObjID::kDoPlayMusicCall).Call(args); + } +} + +auto ClassicPython::GetControllerValue(base::InputDevice* device, + const std::string& value_name) -> int { + assert(device); + assert(objs().Exists(ObjID::kGetInputDeviceMappedValueCall)); + + PythonRef args(Py_BuildValue("(sss)", device->GetDeviceName().c_str(), + device->GetPersistentIdentifier().c_str(), + value_name.c_str()), + PythonRef::kSteal); + PythonRef ret_val; + { + Python::ScopedCallLabel label("get_device_value"); + ret_val = objs().Get(ObjID::kGetInputDeviceMappedValueCall).Call(args); + } + BA_PRECONDITION(ret_val.Exists()); + if (!PyLong_Check(ret_val.Get())) { + throw Exception("Non-int returned from get_device_value call.", + PyExcType::kType); + } + return static_cast(PyLong_AsLong(ret_val.Get())); +} + +auto ClassicPython::GetControllerFloatValue(base::InputDevice* device, + const std::string& value_name) + -> float { + assert(device); + assert(objs().Exists(ObjID::kGetInputDeviceMappedValueCall)); + + PythonRef args(Py_BuildValue("(sss)", device->GetDeviceName().c_str(), + device->GetPersistentIdentifier().c_str(), + value_name.c_str()), + PythonRef::kSteal); + PythonRef ret_val = + objs().Get(ObjID::kGetInputDeviceMappedValueCall).Call(args); + BA_PRECONDITION(ret_val.Exists()); + if (!PyFloat_Check(ret_val.Get())) { + if (PyLong_Check(ret_val.Get())) { + return static_cast(PyLong_AsLong(ret_val.Get())); + } else { + throw Exception( + "Non float/int returned from GetControllerFloatValue call.", + PyExcType::kType); + } + } + return static_cast(PyFloat_AsDouble(ret_val.Get())); +} + +} // namespace ballistica::classic diff --git a/src/ballistica/classic/python/classic_python.h b/src/ballistica/classic/python/classic_python.h new file mode 100644 index 00000000..6af833ac --- /dev/null +++ b/src/ballistica/classic/python/classic_python.h @@ -0,0 +1,40 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CLASSIC_PYTHON_CLASSIC_PYTHON_H_ +#define BALLISTICA_CLASSIC_PYTHON_CLASSIC_PYTHON_H_ + +#include "ballistica/base/base.h" +#include "ballistica/classic/classic.h" +#include "ballistica/shared/python/python_object_set.h" + +namespace ballistica::classic { + +/// General Python support class for the classic feature-set. +class ClassicPython { + public: + ClassicPython(); + + /// Specific Python objects we hold in objs_. + enum class ObjID { + kDoPlayMusicCall, + kGetInputDeviceMappedValueCall, + kLast // Sentinel; must be at end. + }; + + void ImportPythonObjs(); + + void PlayMusic(const std::string& music_type, bool continuous); + auto GetControllerValue(base::InputDevice* device, + const std::string& value_name) -> int; + auto GetControllerFloatValue(base::InputDevice* device, + const std::string& value_name) -> float; + + const auto& objs() { return objs_; } + + private: + PythonObjectSet objs_; +}; + +} // namespace ballistica::classic + +#endif // BALLISTICA_CLASSIC_PYTHON_CLASSIC_PYTHON_H_ diff --git a/src/ballistica/classic/python/methods/python_methods_classic.cc b/src/ballistica/classic/python/methods/python_methods_classic.cc new file mode 100644 index 00000000..6f6765c0 --- /dev/null +++ b/src/ballistica/classic/python/methods/python_methods_classic.cc @@ -0,0 +1,156 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/classic/python/methods/python_methods_classic.h" + +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/graphics/support/camera.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" + +namespace ballistica::classic { + +// Ignore signed bitwise warnings; python macros do it quite a bit. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#pragma ide diagnostic ignored "RedundantCast" + +// -------------------------------- value_test --------------------------------- + +static auto PyValueTest(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* arg; + double change = 0.0f; + double absolute = 0.0f; + bool have_change = false; + bool have_absolute = false; + PyObject* change_obj = Py_None; + PyObject* absolute_obj = Py_None; + static const char* kwlist[] = {"arg", "change", "absolute", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s|OO", + const_cast(kwlist), &arg, + &change_obj, &absolute_obj)) { + return nullptr; + } + if (change_obj != Py_None) { + if (absolute_obj != Py_None) { + throw Exception("Can't provide both a change and absolute"); + } + have_change = true; + change = Python::GetPyDouble(change_obj); + } + if (absolute_obj != Py_None) { + have_absolute = true; + absolute = Python::GetPyDouble(absolute_obj); + } + double return_val = 0.0f; + if (!strcmp(arg, "bufferTime")) { + auto* appmode = scene_v1::SceneV1AppMode::GetSingleton(); + + if (have_change) { + appmode->set_buffer_time(appmode->buffer_time() + + static_cast(change)); + } + if (have_absolute) { + appmode->set_buffer_time(static_cast(absolute)); + } + appmode->set_buffer_time(std::max(0, appmode->buffer_time())); + return_val = appmode->buffer_time(); + } else if (!strcmp(arg, "delaySampling")) { + auto* appmode = scene_v1::SceneV1AppMode::GetSingleton(); + if (have_change) { + appmode->set_delay_bucket_samples(appmode->delay_bucket_samples() + + static_cast(change)); + } + if (have_absolute) { + appmode->set_buffer_time(static_cast(absolute)); + } + appmode->set_delay_bucket_samples( + std::max(1, appmode->delay_bucket_samples())); + return_val = appmode->delay_bucket_samples(); + } else if (!strcmp(arg, "dynamicsSyncTime")) { + auto* appmode = scene_v1::SceneV1AppMode::GetSingleton(); + if (have_change) { + appmode->set_dynamics_sync_time(appmode->dynamics_sync_time() + + static_cast(change)); + } + if (have_absolute) { + appmode->set_dynamics_sync_time(static_cast(absolute)); + } + appmode->set_dynamics_sync_time(std::max(0, appmode->dynamics_sync_time())); + return_val = appmode->dynamics_sync_time(); + } else if (!strcmp(arg, "showNetInfo")) { + if (have_change && change > 0.5f) { + g_base->graphics->set_show_net_info(true); + } + if (have_change && change < -0.5f) { + g_base->graphics->set_show_net_info(false); + } + if (have_absolute) { + g_base->graphics->set_show_net_info(static_cast(absolute)); + } + return_val = g_base->graphics->show_net_info(); + } else if (!strcmp(arg, "allowCameraMovement")) { + base::Camera* camera = g_base->graphics->camera(); + if (camera) { + if (have_change && change > 0.5f) { + camera->set_lock_panning(false); + } + if (have_change && change < -0.5f) { + camera->set_lock_panning(true); + } + if (have_absolute) { + camera->set_lock_panning(!static_cast(absolute)); + } + return_val = !camera->lock_panning(); + } + } else if (!strcmp(arg, "cameraPanSpeedScale")) { + base::Camera* camera = g_base->graphics->camera(); + if (camera) { + double val = camera->pan_speed_scale(); + if (have_change) { + camera->set_pan_speed_scale(static_cast(val + change)); + } + if (have_absolute) { + camera->set_pan_speed_scale(static_cast(absolute)); + } + if (camera->pan_speed_scale() < 0) { + camera->set_pan_speed_scale(0); + } + return_val = camera->pan_speed_scale(); + } + } else { + auto handled = g_base->graphics->ValueTest( + arg, have_absolute ? &absolute : nullptr, + have_change ? &change : nullptr, &return_val); + if (!handled) { + ScreenMessage("invalid arg: " + std::string(arg)); + } + } + + return PyFloat_FromDouble(return_val); + + BA_PYTHON_CATCH; +} + +static PyMethodDef PyValueTestDef = { + "value_test", // name + (PyCFunction)PyValueTest, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "value_test(arg: str, change: float | None = None,\n" + " absolute: float | None = None) -> float\n" + "\n" + "(internal)", +}; + +// ----------------------------------------------------------------------------- + +auto PythonMethodsClassic::GetMethods() -> std::vector { + return {PyValueTestDef}; +} + +#pragma clang diagnostic pop + +} // namespace ballistica::classic diff --git a/src/ballistica/classic/python/methods/python_methods_classic.h b/src/ballistica/classic/python/methods/python_methods_classic.h new file mode 100644 index 00000000..ef8526d3 --- /dev/null +++ b/src/ballistica/classic/python/methods/python_methods_classic.h @@ -0,0 +1,19 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CLASSIC_PYTHON_METHODS_PYTHON_METHODS_CLASSIC_H_ +#define BALLISTICA_CLASSIC_PYTHON_METHODS_PYTHON_METHODS_CLASSIC_H_ + +#include + +#include "ballistica/classic/classic.h" + +namespace ballistica::classic { + +class PythonMethodsClassic { + public: + static auto GetMethods() -> std::vector; +}; + +} // namespace ballistica::classic + +#endif // BALLISTICA_CLASSIC_PYTHON_METHODS_PYTHON_METHODS_CLASSIC_H_ diff --git a/src/ballistica/logic/v1_account.cc b/src/ballistica/classic/support/v1_account.cc similarity index 72% rename from src/ballistica/logic/v1_account.cc rename to src/ballistica/classic/support/v1_account.cc index 7a9fa184..eefd4d78 100644 --- a/src/ballistica/logic/v1_account.cc +++ b/src/ballistica/classic/support/v1_account.cc @@ -1,14 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/logic/v1_account.h" +#include "ballistica/classic/support/v1_account.h" -#include "ballistica/app/app.h" -#include "ballistica/generic/utils.h" -#include "ballistica/internal/app_internal.h" -#include "ballistica/logic/logic.h" -#include "ballistica/platform/platform.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/support/plus_soft.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::classic { auto V1Account::AccountTypeFromString(const std::string& val) -> V1AccountType { if (val == "Game Center") { @@ -66,24 +67,24 @@ auto V1Account::AccountTypeToString(V1AccountType type) -> std::string { auto V1Account::AccountTypeToIconString(V1AccountType type) -> std::string { switch (type) { case V1AccountType::kTest: - return g_logic->CharStr(SpecialChar::kTestAccount); + return g_base->assets->CharStr(SpecialChar::kTestAccount); case V1AccountType::kNvidiaChina: - return g_logic->CharStr(SpecialChar::kNvidiaLogo); + return g_base->assets->CharStr(SpecialChar::kNvidiaLogo); case V1AccountType::kGooglePlay: - return g_logic->CharStr(SpecialChar::kGooglePlayGamesLogo); + return g_base->assets->CharStr(SpecialChar::kGooglePlayGamesLogo); case V1AccountType::kSteam: - return g_logic->CharStr(SpecialChar::kSteamLogo); + return g_base->assets->CharStr(SpecialChar::kSteamLogo); case V1AccountType::kOculus: - return g_logic->CharStr(SpecialChar::kOculusLogo); + return g_base->assets->CharStr(SpecialChar::kOculusLogo); case V1AccountType::kGameCenter: - return g_logic->CharStr(SpecialChar::kGameCenterLogo); + return g_base->assets->CharStr(SpecialChar::kGameCenterLogo); case V1AccountType::kGameCircle: - return g_logic->CharStr(SpecialChar::kGameCircleLogo); + return g_base->assets->CharStr(SpecialChar::kGameCircleLogo); case V1AccountType::kDevice: case V1AccountType::kServer: - return g_logic->CharStr(SpecialChar::kLocalAccount); + return g_base->assets->CharStr(SpecialChar::kLocalAccount); case V1AccountType::kV2: - return g_logic->CharStr(SpecialChar::kV2Logo); + return g_base->assets->CharStr(SpecialChar::kV2Logo); default: return ""; } @@ -143,6 +144,17 @@ void V1Account::SetToken(const std::string& account_id, } } +void V1Account::PushSetV1LoginCall(V1AccountType account_type, + V1LoginState account_state, + const std::string& account_name, + const std::string& account_id) { + g_base->logic->event_loop()->PushCall( + [account_type, account_state, account_name, account_id] { + g_classic->v1_account->SetLogin(account_type, account_state, + account_name, account_id); + }); +} + void V1Account::SetLogin(V1AccountType account_type, V1LoginState login_state, const std::string& login_name, const std::string& login_id) { @@ -151,25 +163,25 @@ void V1Account::SetLogin(V1AccountType account_type, V1LoginState login_state, std::scoped_lock lock(mutex_); // We call out to Python so need to be in logic thread. - assert(InLogicThread()); + assert(g_base->InLogicThread()); // We want redundant sets to be no-ops. - if (login_state_ != login_state || g_app->account_type != account_type + if (login_state_ != login_state || g_classic->account_type != account_type || login_id_ != login_id || login_name_ != login_name) { // Special case: if they sent a sign-out for an account type that is // currently not signed in, ignore it. if (login_state == V1LoginState::kSignedOut - && (account_type != g_app->account_type)) { + && (account_type != g_classic->account_type)) { // No-op. } else { login_state_ = login_state; - g_app->account_type = account_type; + g_classic->account_type = account_type; login_id_ = login_id; login_name_ = Utils::GetValidUTF8(login_name.c_str(), "gthm"); // If they signed out of an account, account type switches to invalid. if (login_state == V1LoginState::kSignedOut) { - g_app->account_type = V1AccountType::kInvalid; + g_classic->account_type = V1AccountType::kInvalid; } login_state_num_ += 1; call_login_did_change = true; @@ -178,8 +190,8 @@ void V1Account::SetLogin(V1AccountType account_type, V1LoginState login_state, } if (call_login_did_change) { // Inform a few subsystems of the change. - g_app_internal->V1LoginDidChange(); - g_platform->V1LoginDidChange(); + g_base->Plus()->V1LoginDidChange(); + g_core->platform->V1LoginDidChange(); } } @@ -205,4 +217,4 @@ auto V1Account::GetProductPurchased(const std::string& product) -> bool { } } -} // namespace ballistica +} // namespace ballistica::classic diff --git a/src/ballistica/logic/v1_account.h b/src/ballistica/classic/support/v1_account.h similarity index 65% rename from src/ballistica/logic/v1_account.h rename to src/ballistica/classic/support/v1_account.h index 6b61243b..10667a98 100644 --- a/src/ballistica/logic/v1_account.h +++ b/src/ballistica/classic/support/v1_account.h @@ -1,20 +1,26 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_LOGIC_V1_ACCOUNT_H_ -#define BALLISTICA_LOGIC_V1_ACCOUNT_H_ +#ifndef BALLISTICA_CLASSIC_SUPPORT_V1_ACCOUNT_H_ +#define BALLISTICA_CLASSIC_SUPPORT_V1_ACCOUNT_H_ #include #include #include #include -#include "ballistica/ballistica.h" +#include "ballistica/classic/classic.h" +#include "ballistica/shared/ballistica.h" -namespace ballistica { +namespace ballistica::classic { /// Global account functionality. class V1Account { public: + void PushSetV1LoginCall(V1AccountType account_type, + V1LoginState account_state, + const std::string& account_name, + const std::string& account_id); + V1Account(); static auto AccountTypeFromString(const std::string& val) -> V1AccountType; static auto AccountTypeToString(V1AccountType type) -> std::string; @@ -32,16 +38,14 @@ class V1Account { // An extra value included when passing our account info to the server // ...(can be used for platform-specific install-signature stuff, etc.). - auto SetExtra(const std::string& extra) -> void; - auto SetExtra2(const std::string& extra) -> void; - auto SetToken(const std::string& account_id, const std::string& token) - -> void; + void SetExtra(const std::string& extra); + void SetExtra2(const std::string& extra); + void SetToken(const std::string& account_id, const std::string& token); - auto SetLogin(V1AccountType account_type, V1LoginState login_state, - const std::string& login_name, const std::string& login_id) - -> void; + void SetLogin(V1AccountType account_type, V1LoginState login_state, + const std::string& login_name, const std::string& login_id); - auto SetProductsPurchased(const std::vector& products) -> void; + void SetProductsPurchased(const std::vector& products); auto GetProductPurchased(const std::string& product) -> bool; auto product_purchases_state() const -> int { return product_purchases_state_; @@ -61,6 +65,6 @@ class V1Account { int login_state_num_{}; }; -} // namespace ballistica +} // namespace ballistica::classic -#endif // BALLISTICA_LOGIC_V1_ACCOUNT_H_ +#endif // BALLISTICA_CLASSIC_SUPPORT_V1_ACCOUNT_H_ diff --git a/src/ballistica/config/config_windows_generic.h b/src/ballistica/config/config_windows_generic.h deleted file mode 100644 index eadd9a58..00000000 --- a/src/ballistica/config/config_windows_generic.h +++ /dev/null @@ -1,19 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_CONFIG_CONFIG_WINDOWS_GENERIC_H_ -#define BALLISTICA_CONFIG_CONFIG_WINDOWS_GENERIC_H_ - -// note: define overrides BEFORE common makefile - -#define BA_ENABLE_STDIO_CONSOLE 1 - -#define BA_SDL_BUILD 1 -#define BA_SDL2_BUILD 1 -#define BA_ENABLE_SDL_JOYSTICKS 1 - -#include "ballistica/config/config_windows_common.h" - -// This must always be last. -#include "ballistica/config/config_common.h" - -#endif // BALLISTICA_CONFIG_CONFIG_WINDOWS_GENERIC_H_ diff --git a/src/ballistica/config/config_windows_headless.h b/src/ballistica/config/config_windows_headless.h deleted file mode 100644 index 1d205ed3..00000000 --- a/src/ballistica/config/config_windows_headless.h +++ /dev/null @@ -1,18 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_CONFIG_CONFIG_WINDOWS_HEADLESS_H_ -#define BALLISTICA_CONFIG_CONFIG_WINDOWS_HEADLESS_H_ - -// note: define overrides BEFORE common header -#define BA_HEADLESS_BUILD 1 - -#define BA_ENABLE_STDIO_CONSOLE 1 - -#define BA_MINSDL_BUILD 1 - -#include "ballistica/config/config_windows_common.h" - -// This must always be last. -#include "ballistica/config/config_common.h" - -#endif // BALLISTICA_CONFIG_CONFIG_WINDOWS_HEADLESS_H_ diff --git a/src/ballistica/core/README.md b/src/ballistica/core/README.md new file mode 100644 index 00000000..f54be9f2 --- /dev/null +++ b/src/ballistica/core/README.md @@ -0,0 +1,15 @@ +# Core Feature Set + +This feature set contains basic state and functionality for the overall +Ballistica system. + +**Core** is a unique feature set in that it is *not* associated with a Python module. +It instead directly allocates and/or returns itself when its `Import()` method is +called in C++. + +This is because, in 'monolithic' builds (where a complete Ballistica app is compiled into +a single binary), **core** itself is responsible for bootstrapping the Python environment. +One can't import something through Python when there's no Python. + +So the purpose of **core** is to be the bare minimum functionality that needs to exist +to bootstrap Python. \ No newline at end of file diff --git a/src/ballistica/core/context.cc b/src/ballistica/core/context.cc deleted file mode 100644 index c99d265e..00000000 --- a/src/ballistica/core/context.cc +++ /dev/null @@ -1,132 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/core/context.h" - -#include "ballistica/generic/runnable.h" -#include "ballistica/logic/host_activity.h" -#include "ballistica/ui/ui.h" - -namespace ballistica { - -ContextTarget::ContextTarget() = default; -ContextTarget::~ContextTarget() = default; - -auto ContextTarget::GetHostSession() -> HostSession* { return nullptr; } - -auto ContextTarget::GetAsHostActivity() -> HostActivity* { return nullptr; } -auto ContextTarget::GetAsUIContext() -> UI* { return nullptr; } -auto ContextTarget::GetMutableScene() -> Scene* { return nullptr; } - -Context::Context() : target(g_context->target) { assert(InLogicThread()); } - -auto Context::operator==(const Context& other) const -> bool { - return (target.get() == other.target.get()); -} - -Context::Context(ContextTarget* target_in) : target(target_in) {} - -auto Context::GetHostSession() const -> HostSession* { - assert(InLogicThread()); - if (target.exists()) return target->GetHostSession(); - return nullptr; -} - -auto Context::GetHostActivity() const -> HostActivity* { - ContextTarget* c = target.get(); - HostActivity* a = c ? c->GetAsHostActivity() : nullptr; - assert(a == dynamic_cast(c)); // This should always match. - return a; -} - -auto Context::GetMutableScene() const -> Scene* { - ContextTarget* c = target.get(); - Scene* sg = c ? c->GetMutableScene() : nullptr; - return sg; -} - -auto Context::GetUIContext() const -> UI* { - ContextTarget* c = target.get(); - UI* uiContext = c ? c->GetAsUIContext() : nullptr; - assert(uiContext == dynamic_cast(c)); - return uiContext; -} - -ScopedSetContext::ScopedSetContext(const Object::Ref& target) { - assert(InLogicThread()); - assert(g_context); - context_prev_ = *g_context; - g_context->target = target; -} - -ScopedSetContext::ScopedSetContext(ContextTarget* target) { - assert(InLogicThread()); - assert(g_context); - context_prev_ = *g_context; - g_context->target = target; -} - -ScopedSetContext::ScopedSetContext(const Context& context) { - assert(InLogicThread()); - assert(g_context); - context_prev_ = *g_context; - *g_context = context; -} - -ScopedSetContext::~ScopedSetContext() { - assert(InLogicThread()); - assert(g_context); - // Restore old. - *g_context = context_prev_; -} - -auto ContextTarget::NewTimer(TimeType timetype, TimerMedium length, bool repeat, - const Object::Ref& runnable) -> int { - // Make sure the passed runnable has a ref-count already - // (don't want them to rely on us to create initial one). - assert(runnable.exists()); - assert(runnable->is_valid_refcounted_object()); - - switch (timetype) { - case TimeType::kSim: - throw Exception("Can't create 'sim' type timers in this context"); - case TimeType::kBase: - throw Exception("Can't create 'base' type timers in this context"); - case TimeType::kReal: - throw Exception("Can't create 'real' type timers in this context"); - default: - throw Exception("Can't create that type timer in this context"); - } -} -void ContextTarget::DeleteTimer(TimeType timetype, int timer_id) { - // We throw on NewTimer; lets just ignore anything that comes - // through here to avoid messing up destructors. - Log(LogLevel::kError, "ContextTarget::DeleteTimer() called; unexpected."); -} - -auto ContextTarget::GetTime(TimeType timetype) -> millisecs_t { - throw Exception("Unsupported time type for this context"); -} - -auto ContextTarget::GetTexture(const std::string& name) - -> Object::Ref { - throw Exception("GetTexture() not supported in this context"); -} - -auto ContextTarget::GetSound(const std::string& name) -> Object::Ref { - throw Exception("GetSound() not supported in this context"); -} - -auto ContextTarget::GetData(const std::string& name) -> Object::Ref { - throw Exception("GetData() not supported in this context"); -} - -auto ContextTarget::GetModel(const std::string& name) -> Object::Ref { - throw Exception("GetModel() not supported in this context"); -} - -auto ContextTarget::GetCollideModel(const std::string& name) - -> Object::Ref { - throw Exception("GetCollideModel() not supported in this context"); -} - -} // namespace ballistica diff --git a/src/ballistica/core/context.h b/src/ballistica/core/context.h deleted file mode 100644 index bd96ec13..00000000 --- a/src/ballistica/core/context.h +++ /dev/null @@ -1,126 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_CORE_CONTEXT_H_ -#define BALLISTICA_CORE_CONTEXT_H_ - -#include - -#include "ballistica/core/object.h" - -namespace ballistica { - -// Stores important environmental state such as the recipient of commands. -// Callbacks and other mechanisms should save/restore the context so that their -// effects properly apply to the place they came from. -class Context { - public: - static auto current() -> const Context& { - assert(g_context); - - // Context can only be accessed from the logic thread. - BA_PRECONDITION(InLogicThread()); - - return *g_context; - } - static void set_current(const Context& context) { - // Context can only be accessed from the logic thread. - BA_PRECONDITION(InLogicThread()); - - *g_context = context; - } - - // Return the current context target, raising an Exception if there is none. - static auto current_target() -> ContextTarget& { - ContextTarget* t = current().target.get(); - if (t == nullptr) { - throw Exception("No context target set."); - } - return *t; - } - - // Default constructor will capture a copy of the current global context. - Context(); - explicit Context(ContextTarget* sgc); - auto operator==(const Context& other) const -> bool; - - Object::WeakRef target; - - // If the current Context is (or is part of) a HostSession, return it; - // otherwise return nullptr. be aware that this will return a session if the - // context is *either* a host-activity or a host-session - auto GetHostSession() const -> HostSession*; - - // return the current context as an HostActivity if it is one; otherwise - // nullptr (faster than a dynamic_cast) - auto GetHostActivity() const -> HostActivity*; - - // if the current context contains a scene that can be manipulated by - // standard commands, this returns it. This includes host-sessions, - // host-activities, and the UI context. - auto GetMutableScene() const -> Scene*; - - // return the current context as a UIContext if it is one; otherwise nullptr - // (faster than a dynamic_cst) - auto GetUIContext() const -> UI*; -}; - -// An interface for interaction with the engine; loading and wrangling media, -// nodes, etc. -// Note: it would seem like in an ideal world this could just be a pure -// virtual interface. -// However various things use WeakRef so technically they do -// all need to inherit from Object anyway. -class ContextTarget : public Object { - public: - ContextTarget(); - ~ContextTarget() override; - - // returns the HostSession associated with this context, (if there is one). - virtual auto GetHostSession() -> HostSession*; - - // Utility functions for casting; faster than dynamic_cast. - virtual auto GetAsHostActivity() -> HostActivity*; - virtual auto GetAsUIContext() -> UI*; - virtual auto GetMutableScene() -> Scene*; - - // Timer create/destroy functions. - // Times are specified in milliseconds. - // Exceptions should be thrown for unsupported timetypes in NewTimer. - // Default NewTimer implementation throws a descriptive error, so it can - // be useful to fall back on for unsupported cases. - // NOTE: make sure runnables passed in here already have non-zero - // ref-counts since a ref might not be grabbed here. - virtual auto NewTimer(TimeType timetype, TimerMedium length, bool repeat, - const Object::Ref& runnable) -> int; - virtual void DeleteTimer(TimeType timetype, int timer_id); - - virtual auto GetTexture(const std::string& name) -> Object::Ref; - virtual auto GetSound(const std::string& name) -> Object::Ref; - virtual auto GetData(const std::string& name) -> Object::Ref; - virtual auto GetModel(const std::string& name) -> Object::Ref; - virtual auto GetCollideModel(const std::string& name) - -> Object::Ref; - - // Return the current time of a given type in milliseconds. - // Exceptions should be thrown for unsupported timetypes. - // Default implementation throws a descriptive error so can be - // useful to fall back on for unsupported cases - virtual auto GetTime(TimeType timetype) -> millisecs_t; -}; - -// Use this to push/pop a change to the current context -class ScopedSetContext { - public: - explicit ScopedSetContext(const Object::Ref& context); - explicit ScopedSetContext(ContextTarget* context); - explicit ScopedSetContext(const Context& context); - ~ScopedSetContext(); - - private: - BA_DISALLOW_CLASS_COPIES(ScopedSetContext); - Context context_prev_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_CORE_CONTEXT_H_ diff --git a/src/ballistica/core/core.cc b/src/ballistica/core/core.cc new file mode 100644 index 00000000..71bb20aa --- /dev/null +++ b/src/ballistica/core/core.cc @@ -0,0 +1,247 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/core/core.h" + +#include +#include + +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/core/python/core_python.h" +#include "ballistica/shared/foundation/event_loop.h" + +namespace ballistica::core { + +CoreFeatureSet* g_core{}; +BaseSoftInterface* g_base_soft{}; + +auto CoreFeatureSet::Import(const CoreConfig* config) -> CoreFeatureSet* { + // Only accept a config in monolithic builds if this is the first import. + if (config != nullptr) { + if (!g_buildconfig.monolithic_build()) { + FatalError("CoreConfig can only be passed in monolithic builds."); + } + if (g_core != nullptr) { + FatalError("CoreConfig can only be passed on the first import call."); + } + + if (g_core == nullptr) { + DoImport(*config); + } + } else { + if (g_core == nullptr) { + DoImport({}); + } + } + return g_core; +} + +void CoreFeatureSet::DoImport(const CoreConfig& config) { + millisecs_t start_millisecs = CorePlatform::GetCurrentMillisecs(); + + assert(g_core == nullptr); + g_core = new CoreFeatureSet(config); + g_core->PostInit(); + + // Slightly hacky: have to report our begin time after the fact since + // core didn't exist before. Can at least add an offset to give an accurate + // time though. + auto seconds_since_actual_start = + static_cast(CorePlatform::GetCurrentMillisecs() - start_millisecs) + / 1000.0; + g_core->BootLog("core import begin", -seconds_since_actual_start); + g_core->BootLog("core import end"); +} + +CoreFeatureSet::CoreFeatureSet(CoreConfig config) + : main_thread_id{std::this_thread::get_id()}, + python{new CorePython()}, + platform{CorePlatform::Create()}, + core_config_{std::move(config)}, + last_app_time_measure_microsecs_{CorePlatform::GetCurrentMicrosecs()}, + vr_mode{config.vr_mode} { + // We're a singleton. If there's already one of us, something's wrong. + assert(g_core == nullptr); + + // Test our static-type-name functionality. + // This code runs at compile time and extracts human readable type names using + // __PRETTY_FUNCTION__ type functionality. However, it is dependent on + // specific compiler output and so could break easily if anything changes. + // Here we add some compile-time checks to alert us if that happens. + + // Remember that results can vary per compiler; make sure we match + // any one of the expected formats. + static_assert(static_type_name_constexpr() + == "ballistica::core::CoreFeatureSet *" + || static_type_name_constexpr() + == "ballistica::core::CoreFeatureSet*" + || static_type_name_constexpr() + == "class ballistica::core::CoreFeatureSet*" + || static_type_name_constexpr() + == "CoreFeatureSet*"); + Object::Ref testrunnable{}; + static_assert( + static_type_name_constexpr() + == "ballistica::Object::Ref" + || static_type_name_constexpr() + == "class ballistica::Object::Ref" + || static_type_name_constexpr() + == "Object::Ref"); + + // int testint{}; + // static_assert(static_type_name_constexpr() == "int"); + + // If anything above breaks, enable this code to debug/fix it. + // This will print a calculated type name as well as the full string + // it was parsed from. Use this to adjust the filtering as necessary so + // the resulting type name matches what is expected. + if (explicit_bool(false)) { + Log(LogLevel::kError, + "static_type_name check; name is '" + + static_type_name() + "' debug_full is '" + + static_type_name(true) + "'"); + } +} + +void CoreFeatureSet::PostInit() { + // Some of this stuff accesses g_core so we need to run it *after* + // assigning our singleton. + + // Enable extra timing logs via env var. + const char* debug_timing_env = getenv("BA_DEBUG_TIMING"); + if (debug_timing_env != nullptr && !strcmp(debug_timing_env, "1")) { + debug_timing = true; + } + + if (vr_mode && !g_buildconfig.vr_build()) { + FatalError("vr_mode enabled in core-config but we are not a vr build."); + } + + // Let's grab a string of the portion of __FILE__ before our project root. + // We can use it to make error messages/etc. more pretty by stripping out + // all but sub-project paths. + const char* f = __FILE__; + auto* f_end = strstr( + f, "src" BA_DIRSLASH "ballistica" BA_DIRSLASH "app" BA_DIRSLASH "app.cc"); + if (!f) { + Log(LogLevel::kWarning, "Unable to calc project dir from __FILE__."); + } else { + build_src_dir_ = std::string(f).substr(0, f_end - f); + } + + // Note: this checks g_core->main_thread_id which is why it must run in + // PostInit and not our constructor. + main_event_loop_ = new EventLoop(EventLoopID::kMain, ThreadSource::kWrapMain); + + // On monolithic builds we need to bring up Python itself. + if (g_buildconfig.monolithic_build()) { + python->InitPython(); + } + + // Make sure we're running an acceptable Python version/etc. + python->VerifyPythonEnvironment(); + + // Grab whatever Python stuff we use. + python->ImportPythonObjs(); + + // FIXME - MOVE THIS TO A RUN_APP_TO_COMPLETION() SORT OF FUNCTION + // For now it does the right thing here since all we have is monolithic + // builds but this will need to account for more situations later. + python->ReleaseMainThreadGIL(); +} + +auto CoreFeatureSet::SoftImportBase() -> BaseSoftInterface* { + if (!g_base_soft) { + python->SoftImportBase(); + } + return g_base_soft; +} + +void CoreFeatureSet::BootLog(const char* msg, double offset_seconds) { + if (!core_config_.log_boot_process) { + return; + } + char buffer[128]; + // It's not safe to use Log until + snprintf(buffer, sizeof(buffer), "BOOT: %s @ %.3fs.", msg, + g_core->GetAppTimeSeconds() + offset_seconds); + Log(LogLevel::kInfo, buffer); +} + +auto CoreFeatureSet::HeadlessMode() -> bool { + // This is currently a hard-coded value but could theoretically change + // later if we support running in headless mode from a gui build/etc. + return g_buildconfig.headless_build(); +} + +auto CoreFeatureSet::IsVRMode() -> bool { return core_config_.vr_mode; } + +static void WaitThenDie(millisecs_t wait, const std::string& action) { + CorePlatform::SleepMillisecs(wait); + FatalError("Timed out waiting for " + action + "."); +} + +auto CoreFeatureSet::GetAppTimeMillisecs() -> millisecs_t { + UpdateAppTime(); + return app_time_microsecs_ / 1000; +} + +auto CoreFeatureSet::GetAppTimeMicrosecs() -> microsecs_t { + UpdateAppTime(); + return app_time_microsecs_; +} + +auto CoreFeatureSet::GetAppTimeSeconds() -> double { + UpdateAppTime(); + return static_cast(app_time_microsecs_) / 1000000; +} + +void CoreFeatureSet::UpdateAppTime() { + microsecs_t t = CorePlatform::GetCurrentMicrosecs(); + + // If we're at a different time than our last query, do our funky math. + if (t != last_app_time_measure_microsecs_) { + std::scoped_lock lock(app_time_mutex_); + microsecs_t passed = t - last_app_time_measure_microsecs_; + + // The time calls we're using are supposed to be monotonic, but I've seen + // 'passed' equal -1 even when it is using std::chrono::steady_clock. Let's + // do our own filtering here to make 100% sure we don't go backwards. + if (passed < 0) { + passed = 0; + } else { + // Very large times-passed probably means we went to sleep or something; + // clamp to a reasonable value. + if (passed > 250000) { + passed = 250000; + } + } + app_time_microsecs_ += passed; + last_app_time_measure_microsecs_ = t; + } +} + +void CoreFeatureSet::UpdateMainThreadID() { + auto current_id = std::this_thread::get_id(); + + // This gets called a lot and it may happen before we are spun up, + // so just ignore it in that case.. + main_thread_id = current_id; + main_event_loop_->set_thread_id(current_id); +} + +void CoreFeatureSet::StartSuicideTimer(const std::string& action, + millisecs_t delay) { + if (!started_suicide_) { + new std::thread(WaitThenDie, delay, action); + started_suicide_ = true; + } +} + +auto CoreFeatureSet::InMainThread() -> bool { + if (main_event_loop_) { + return main_event_loop_->ThreadIsCurrent(); + } + return false; +} + +} // namespace ballistica::core diff --git a/src/ballistica/core/core.h b/src/ballistica/core/core.h new file mode 100644 index 00000000..a977cf4d --- /dev/null +++ b/src/ballistica/core/core.h @@ -0,0 +1,155 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CORE_CORE_H_ +#define BALLISTICA_CORE_CORE_H_ + +#include +#include +#include +#include +#include + +#include "ballistica/core/support/core_config.h" +#include "ballistica/shared/ballistica.h" + +namespace ballistica::core { + +// Predeclare types we use throughout our FeatureSet so most +// headers can get away with just including this header. +class CoreConfig; +class CorePython; +class CorePlatform; +class CoreFeatureSet; +class BaseSoftInterface; + +// Our feature-set's globals. +// Feature-sets should NEVER directly access globals in another feature-set's +// namespace. All functionality we need from other feature-sets should be +// imported into globals in our own namespace. Generally we do this when we +// are initially imported (just as regular Python modules do). +extern CoreFeatureSet* g_core; + +// We don't require the base feature-set but can use it if present. +// Base will supply us with this pointer if/when it spins up. +// So we must never assume this pointer is valid and must check for it +// with each use. +extern BaseSoftInterface* g_base_soft; + +/// Platform-agnostic global state for our overall system. +/// This gets created whenever we are used in any capacity, even if +/// we don't create/run an app. +/// Ideally most things here should be migrated to more specific +/// subsystems. +class CoreFeatureSet { + public: + /// Import the core feature set. A core-config can be passed ONLY + /// in monolithic builds when it is guaranteed that the Import will be + /// allocating the CoreFeatureSet singleton. + static auto Import(const CoreConfig* config = nullptr) -> CoreFeatureSet*; + + /// Attempt to import the base feature-set. Will return nullptr if it is + /// not available. This should only be used by code with soft dependencies + /// on base. Regular code should talk to base directly to get its full + /// interface. + auto SoftImportBase() -> BaseSoftInterface*; + + /// The core-config we were inited with. + const auto& core_config() const { return core_config_; } + + /// Start a timer to force-kill our process after the set length of time. + /// Can be used during shutdown or when trying to send a crash-report to + /// ensure we don't hang indefinitely. + void StartSuicideTimer(const std::string& action, millisecs_t delay); + + // Call this if the main thread changes. + // Fixme: Should come up with something less hacky feeling. + void UpdateMainThreadID(); + + auto* main_event_loop() const { return main_event_loop_; } + auto IsVRMode() -> bool; + + /// Are we running headless? + auto HeadlessMode() -> bool; + + /// Return current app-time in milliseconds. + /// App-time is basically the total time that the engine has been actively + /// running. (The 'App' here is a slight misnomer). It will stop progressing + /// while the app is suspended and will never go backwards. + auto GetAppTimeMillisecs() -> millisecs_t; + + /// Return current app-time in microseconds. + /// App-time is basically the total time that the engine has been actively + /// running. (The 'App' here is a slight misnomer). It will stop progressing + /// while the app is suspended and will never go backwards. + auto GetAppTimeMicrosecs() -> microsecs_t; + + /// Return current app-time in seconds. + /// App-time is basically the total time that the engine has been actively + /// running. (The 'App' here is a slight misnomer). It will stop progressing + /// while the app is suspended and will never go backwards. + auto GetAppTimeSeconds() -> double; + + /// Are we in the thread the main event loop is running on? + /// Generally this is the thread that runs graphics and os event processing. + auto InMainThread() -> bool; + + /// Log a boot-related message (only if core_config.log_boot_process is true). + void BootLog(const char* msg, double offset_seconds = 0.0); + + /// Base path of build src dir so we can attempt to remove it from + /// any source file paths we print. + auto build_src_dir() const { return build_src_dir_; } + + // Subsystems. + CorePython* const python; + CorePlatform* const platform; + + // The following are misc values that should be migrated to applicable + // subsystem classes. + bool threads_paused{}; + bool workspaces_in_use{}; + bool replay_open{}; + std::vector pausable_event_loops; + std::mutex v1_cloud_log_mutex; + std::string v1_cloud_log; + bool did_put_v1_cloud_log{}; + bool v1_cloud_log_full{}; + int master_server_source{}; + int session_count{}; + bool shutting_down{}; + bool have_incentivized_ad{false}; + bool should_pause{}; + bool reset_vr_orientation{}; + bool user_ran_commands{}; + std::string user_agent_string{"BA_USER_AGENT_UNSET (" BA_PLATFORM_STRING ")"}; + int return_value{}; + bool debug_timing{}; + std::thread::id main_thread_id{}; + + bool vr_mode; + std::mutex thread_name_map_mutex; + std::unordered_map thread_name_map; + +#if BA_DEBUG_BUILD + std::mutex object_list_mutex; + Object* object_list_first{}; + int object_count{}; +#endif + + private: + static void DoImport(const CoreConfig& config); + void UpdateAppTime(); + explicit CoreFeatureSet(CoreConfig config); + void PostInit(); + EventLoop* main_event_loop_{}; + CoreConfig core_config_; + bool started_suicide_{}; + std::string build_src_dir_; + microsecs_t app_time_microsecs_{}; + microsecs_t last_app_time_measure_microsecs_; + std::mutex app_time_mutex_; +}; + +} // namespace ballistica::core + +#endif // BALLISTICA_CORE_CORE_H_ diff --git a/src/ballistica/core/logging.cc b/src/ballistica/core/logging.cc deleted file mode 100644 index f6dced95..00000000 --- a/src/ballistica/core/logging.cc +++ /dev/null @@ -1,109 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/core/logging.h" - -#include - -#include "ballistica/app/app.h" -#include "ballistica/internal/app_internal.h" -#include "ballistica/logic/logic.h" -#include "ballistica/networking/telnet_server.h" -#include "ballistica/platform/platform.h" -#include "ballistica/python/python.h" - -namespace ballistica { - -auto Logging::Log(LogLevel level, const std::string& msg) -> void { - // This is basically up to Python to handle, but its up to us - // if Python's not up yet. - if (!g_python) { - // Make an attempt to get it at least seen (and note the fact - // that its super-early). - const char* errmsg{"Logging::Log() called before g_python exists."}; - if (g_platform) { - g_platform->DisplayLog("root", LogLevel::kError, errmsg); - g_platform->DisplayLog("root", level, msg); - } - fprintf(stderr, "%s\n%s\n", errmsg, msg.c_str()); - return; - } - - // All we want to do is call Python logging.XXX. That's on Python. - g_python->LoggingCall(level, msg); -} - -auto Logging::DisplayLog(const std::string& name, LogLevel level, - const std::string& msg) -> void { - auto msgnewline{msg + "\n"}; - - // Print to in-game console. - { - if (g_logic != nullptr) { - g_logic->PushConsolePrintCall(msgnewline); - } else { - if (g_platform != nullptr) { - g_platform->DisplayLog("root", LogLevel::kWarning, - "DisplayLog() called before logic-thread setup; " - "will not appear on in-game console."); - } - } - } - - // Print to any telnet clients. - if (g_app && g_app->telnet_server) { - g_app->telnet_server->PushPrint(msgnewline); - } - - // Ship to platform-specific display mechanisms (android log, etc). - g_platform->DisplayLog(name, level, msg); -} - -auto Logging::V1CloudLog(const std::string& msg) -> void { - // Route through platform-specific loggers if present. - // (things like Crashlytics crash-logging) - if (g_platform) { - Platform::DebugLog(msg); - } - - // Add to our complete v1-cloud-log. - if (g_app != nullptr) { - std::scoped_lock lock(g_app->v1_cloud_log_mutex); - if (!g_app->v1_cloud_log_full) { - (g_app->v1_cloud_log) += (msg + "\n"); - if ((g_app->v1_cloud_log).size() > 25000) { - // Allow some reasonable overflow for last statement. - if ((g_app->v1_cloud_log).size() > 250000) { - // FIXME: This could potentially chop up utf-8 chars. - (g_app->v1_cloud_log).resize(250000); - } - g_app->v1_cloud_log += "\n\n"; - g_app->v1_cloud_log_full = true; - } - } - } - - // If the game is fully bootstrapped, let the Python layer handle logs. - // It will group log messages intelligently and ship them to the - // master server with various other context info included. - if (g_app && g_app->is_bootstrapped) { - assert(g_python != nullptr); - g_python->PushObjCall(Python::ObjID::kHandleV1CloudLogCall); - } else { - // For log messages during bootstrapping we ship them immediately since - // we don't know if the Python layer is (or will be) able to. - if (g_early_v1_cloud_log_writes > 0) { - g_early_v1_cloud_log_writes -= 1; - std::string logprefix = "EARLY-LOG:"; - std::string logsuffix; - - // If we're an early enough error, our global log isn't even available, - // so include this specific message as a suffix instead. - if (g_app == nullptr) { - logsuffix = msg; - } - g_app_internal->DirectSendV1CloudLogs(logprefix, logsuffix, false); - } - } -} - -} // namespace ballistica diff --git a/src/ballistica/core/logging.h b/src/ballistica/core/logging.h deleted file mode 100644 index 6c7fb319..00000000 --- a/src/ballistica/core/logging.h +++ /dev/null @@ -1,39 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_CORE_LOGGING_H_ -#define BALLISTICA_CORE_LOGGING_H_ - -#include - -#include "ballistica/ballistica.h" - -namespace ballistica { - -class Logging { - public: - /// Write a message to the log. Intended for logging use in C++ code. - /// This is safe to call by any thread at any time. In general it simply - /// passes through to the equivalent Python log calls: logging.info, - /// logging.warning, etc. - /// Note that log messages often must go through some background - /// processing before being seen by the user, meaning they may not - /// always work well for tight debugging purposes. In cases such as these, - /// printf() type calls or calling DisplayLog() directly may work better. - /// (though both of these should be avoided in permanent code). - static auto Log(LogLevel level, const std::string& msg) -> void; - - /// Immediately display a log message in the in-game console, - /// platform-specific logs, etc. This generally should not be called - /// directly but instead wired up to display messages coming from the - /// Python logging system. - static auto DisplayLog(const std::string& name, LogLevel level, - const std::string& msg) -> void; - - /// Write a message to the v1 cloud log. This is considered legacy - /// and will be phased out eventually. - static auto V1CloudLog(const std::string& msg) -> void; -}; - -} // namespace ballistica - -#endif // BALLISTICA_CORE_LOGGING_H_ diff --git a/src/ballistica/core/macros.cc b/src/ballistica/core/macros.cc deleted file mode 100644 index 27f436bf..00000000 --- a/src/ballistica/core/macros.cc +++ /dev/null @@ -1,108 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/core/macros.h" - -#include "ballistica/platform/platform.h" -#include "ballistica/python/python.h" - -// Snippets of compiled functionality used by our evil macros. - -namespace ballistica { - -void MacroFunctionTimerEnd(millisecs_t starttime, millisecs_t time, - const char* funcname) { - // Currently disabling this for test builds; not really useful for - // the general public. - if (g_buildconfig.test_build()) { - return; - } - millisecs_t endtime = g_platform->GetTicks(); - if (endtime - starttime > time) { - Log(LogLevel::kWarning, std::to_string(endtime - starttime) - + " milliseconds spent in " + funcname); - } -} - -void MacroFunctionTimerEndThread(millisecs_t starttime, millisecs_t time, - const char* funcname) { - // Currently disabling this for test builds; not really useful for - // the general public. - if (g_buildconfig.test_build()) { - return; - } - millisecs_t endtime = g_platform->GetTicks(); - if (endtime - starttime > time) { - Log(LogLevel::kWarning, - std::to_string(endtime - starttime) + " milliseconds spent by " - + ballistica::GetCurrentThreadName() + " thread in " + funcname); - } -} - -void MacroFunctionTimerEndEx(millisecs_t starttime, millisecs_t time, - const char* funcname, const std::string& what) { - // Currently disabling this for test builds; not really useful for - // the general public. - if (g_buildconfig.test_build()) { - return; - } - millisecs_t endtime = g_platform->GetTicks(); - if (endtime - starttime > time) { - Log(LogLevel::kWarning, std::to_string(endtime - starttime) - + " milliseconds spent in " + funcname + " for " - + what); - } -} - -void MacroFunctionTimerEndThreadEx(millisecs_t starttime, millisecs_t time, - const char* funcname, - const std::string& what) { - // Currently disabling this for test builds; not really useful for - // the general public. - if (g_buildconfig.test_build()) { - return; - } - millisecs_t endtime = g_platform->GetTicks(); - if (endtime - starttime > time) { - Log(LogLevel::kWarning, std::to_string(endtime - starttime) - + " milliseconds spent by " - + ballistica::GetCurrentThreadName() - + " thread in " + funcname + " for " + what); - } -} - -void MacroTimeCheckEnd(millisecs_t starttime, millisecs_t time, - const char* name, const char* file, int line) { - // Currently disabling this for test builds; not really useful for - // the general public. - if (g_buildconfig.test_build()) { - return; - } - millisecs_t e = g_platform->GetTicks(); - if (e - starttime > time) { - Log(LogLevel::kWarning, - std::string(name) + " took " + std::to_string(e - starttime) - + " milliseconds; " + file + " line " + std::to_string(line)); - } -} - -void MacroLogErrorTrace(const std::string& msg, const char* fname, int line) { - char buffer[2048]; - snprintf(buffer, sizeof(buffer), "%s:%d:", fname, line); - buffer[sizeof(buffer) - 1] = 0; - Python::PrintStackTrace(); - Log(LogLevel::kError, std::string(buffer) + " error: " + msg); -} - -void MacroLogError(const std::string& msg, const char* fname, int line) { - char e_buffer[2048]; - snprintf(e_buffer, sizeof(e_buffer), "%s:%d:", fname, line); - e_buffer[sizeof(e_buffer) - 1] = 0; - Log(LogLevel::kError, std::string(e_buffer) + " error: " + msg); -} - -void MacroLogPythonTrace(const std::string& msg) { - Python::PrintStackTrace(); - Log(LogLevel::kError, msg); -} - -} // namespace ballistica diff --git a/src/ballistica/core/macros.h b/src/ballistica/core/macros.h deleted file mode 100644 index 85c70105..00000000 --- a/src/ballistica/core/macros.h +++ /dev/null @@ -1,166 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_CORE_MACROS_H_ -#define BALLISTICA_CORE_MACROS_H_ - -#ifdef __cplusplus -#include -#include -#endif - -#include "ballistica/core/types.h" - -// Various utility macros and related support calls. -// Trying to contain the evil in this one place. - -// Trailing-semicolon note: -// Some macros contain a ((void*) at the end. This is so the macro can be -// followed by a semicolon without triggering an 'empty statement' warning. -// I find standalone function-style macro invocations without semicolons -// tends to confuse code formatters. - -#define BA_STRINGIFY(x) #x - -#define BA_BUILD_COMMAND_FILENAME \ - "" -#define BA_BCFN BA_BUILD_COMMAND_FILENAME - -#if BA_OSTYPE_WINDOWS -#define BA_DIRSLASH "\\" -#else -#define BA_DIRSLASH "/" -#endif - -#if BA_DEBUG_BUILD -#define BA_IFDEBUG(a) a -#else -#define BA_IFDEBUG(a) ((void)0) -#endif - -// Useful for finding hitches. -// Call begin, followed at some point by any of the end versions. -// FIXME: Turn these into C++ classes. -#if BA_DEBUG_BUILD -#define BA_DEBUG_FUNCTION_TIMER_BEGIN() \ - millisecs_t _dfts = g_platform->GetTicks() -#define BA_DEBUG_FUNCTION_TIMER_END(time) \ - ballistica::MacroFunctionTimerEnd(_dfts, time, __PRETTY_FUNCTION__) -#define BA_DEBUG_FUNCTION_TIMER_END_THREAD(time) \ - ballistica::MacroFunctionTimerEndThread(_dfts, time, __PRETTY_FUNCTION__) -#define BA_DEBUG_FUNCTION_TIMER_END_EX(time, what) \ - MacroFunctionTimerEndEx(_dfts, time, __PRETTY_FUNCTION__, what) -#define BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(time, what) \ - ballistica::MacroFunctionTimerEndThreadEx(_dfts, time, __PRETTY_FUNCTION__, \ - what) -#define BA_DEBUG_TIME_CHECK_BEGIN(name) \ - millisecs_t name##_ts = g_platform->GetTicks() -#define BA_DEBUG_TIME_CHECK_END(name, time) \ - ballistica::MacroTimeCheckEnd(name##_ts, time, #name, __FILE__, __LINE__) -#else -#define BA_DEBUG_FUNCTION_TIMER_BEGIN() ((void)0) -#define BA_DEBUG_FUNCTION_TIMER_END(time) ((void)0) -#define BA_DEBUG_FUNCTION_TIMER_END_THREAD(time) ((void)0) -#define BA_DEBUG_FUNCTION_TIMER_END_EX(time, what) ((void)0) -#define BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(time, what) ((void)0) -#define BA_DEBUG_TIME_CHECK_BEGIN(name) ((void)0) -#define BA_DEBUG_TIME_CHECK_END(name, time) ((void)0) -#endif - -// Disallow copying for a class. -#define BA_DISALLOW_CLASS_COPIES(type) \ - type(const type& foo) = delete; \ - type& operator=(const type& src) = delete; /* NOLINT (macro parens) */ - -// Call this for errors which are non-fatal but should be noted so they can be -// fixed. -#define BA_LOG_ERROR_TRACE(msg) \ - ballistica::MacroLogErrorTrace(msg, __FILE__, __LINE__) - -#define BA_LOG_ERROR_TRACE_ONCE(msg) \ - { \ - static bool did_log_error_trace_here = false; \ - if (!did_log_error_trace_here) { \ - ballistica::MacroLogErrorTrace(msg, __FILE__, __LINE__); \ - did_log_error_trace_here = true; \ - } \ - } \ - ((void)0) // (see 'Trailing-semicolon note' at top) - -#define BA_LOG_ONCE(lvl, msg) \ - { \ - static bool did_log_here = false; \ - if (!did_log_here) { \ - ballistica::Log(lvl, msg); \ - did_log_here = true; \ - } \ - } \ - ((void)0) // (see 'Trailing-semicolon note' at top) - -#define BA_LOG_PYTHON_TRACE(msg) ballistica::MacroLogPythonTrace(msg) - -#define BA_LOG_PYTHON_TRACE_ONCE(msg) \ - { \ - static bool did_log_python_trace_here = false; \ - if (!did_log_python_trace_here) { \ - ballistica::MacroLogPythonTrace(msg); \ - did_log_python_trace_here = true; \ - } \ - } \ - ((void)0) // (see 'Trailing-semicolon note' at top) - -/// Test a condition and throw an exception if it fails (on both debug and -/// release builds) -#define BA_PRECONDITION(b) \ - { \ - if (!(b)) { \ - throw ballistica::Exception("Precondition failed: " #b); \ - } \ - } \ - ((void)0) // (see 'Trailing-semicolon note' at top) - -/// Test a condition and simply print a log message if it fails (on both debug -/// and release builds) -#define BA_PRECONDITION_LOG(b) \ - { \ - if (!(b)) { \ - Log(LogLevel::kError, "Precondition failed: " #b); \ - } \ - } \ - ((void)0) // (see 'Trailing-semicolon note' at top) - -/// Test a condition and abort the program if it fails (on both debug -/// and release builds) -#define BA_PRECONDITION_FATAL(b) \ - { \ - if (!(b)) { \ - FatalError("Precondition failed: " #b); \ - } \ - } \ - ((void)0) // (see 'Trailing-semicolon note' at top) - -#ifdef __cplusplus - -namespace ballistica { - -// Support functions used by some of our macros; not intended to be used -// directly. -void MacroFunctionTimerEnd(millisecs_t starttime, millisecs_t time, - const char* funcname); -void MacroFunctionTimerEndThread(millisecs_t starttime, millisecs_t time, - const char* funcname); -void MacroFunctionTimerEndEx(millisecs_t starttime, millisecs_t time, - const char* funcname, const std::string& what); -void MacroFunctionTimerEndThreadEx(millisecs_t starttime, millisecs_t time, - const char* funcname, - const std::string& what); -void MacroTimeCheckEnd(millisecs_t starttime, millisecs_t time, - const char* name, const char* file, int line); -void MacroLogErrorTrace(const std::string& msg, const char* fname, int line); -void MacroLogError(const std::string& msg, const char* fname, int line); -void MacroLogPythonTrace(const std::string& msg); - -} // namespace ballistica - -#endif // __cplusplus - -#endif // BALLISTICA_CORE_MACROS_H_ diff --git a/src/ballistica/core/platform/apple/core_platform_apple.h b/src/ballistica/core/platform/apple/core_platform_apple.h new file mode 100644 index 00000000..23619a8c --- /dev/null +++ b/src/ballistica/core/platform/apple/core_platform_apple.h @@ -0,0 +1,82 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CORE_PLATFORM_APPLE_CORE_PLATFORM_APPLE_H_ +#define BALLISTICA_CORE_PLATFORM_APPLE_CORE_PLATFORM_APPLE_H_ +#if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS + +#include +#include +#include +#include + +#include "ballistica/core/platform/core_platform.h" + +namespace ballistica::core { + +class CorePlatformApple : public CorePlatform { + public: + CorePlatformApple(); + auto GetDeviceV1AccountUUIDPrefix() -> std::string override; + auto GetRealLegacyDeviceUUID(std::string* uuid) -> bool override; + auto GenerateUUID() -> std::string override; + auto DoGetConfigDirectoryMonolithicDefault() + -> std::optional override; + auto GetLocale() -> std::string override; + auto DoGetDeviceName() -> std::string override; + auto DoHasTouchScreen() -> bool override; + auto GetUIScale() -> UIScale override; + auto IsRunningOnDesktop() -> bool override; + void DisplayLog(const std::string& name, LogLevel level, + const std::string& msg) override; + void GetTextBoundsAndWidth(const std::string& text, Rect* r, + float* width) override; + void FreeTextTexture(void* tex) override; + auto CreateTextTexture(int width, int height, + const std::vector& strings, + const std::vector& positions, + const std::vector& widths, float scale) + -> void* override; + auto GetTextTextureData(void* tex) -> uint8_t* override; + void SubmitScore(const std::string& game, const std::string& version, + int64_t score) override; + void ReportAchievement(const std::string& achievement) override; + auto HaveLeaderboard(const std::string& game, const std::string& config) + -> bool override; + void ShowOnlineScoreUI(const std::string& show, const std::string& game, + const std::string& game_version) override; + auto NewAutoReleasePool() -> void* override; + void DrainAutoReleasePool(void* pool) override; + void ResetAchievements() override; + void GameCenterLogin() override; + auto IsOSPlayingMusic() -> bool override; + void SetHardwareCursorVisible(bool visible) override; + void QuitApp() override; + void OpenFileExternally(const std::string& path) override; + void OpenDirExternally(const std::string& path) override; + void MacMusicAppInit() override; + auto MacMusicAppGetVolume() -> int override; + void MacMusicAppSetVolume(int volume) override; + void MacMusicAppGetLibrarySource() override; + void MacMusicAppStop() override; + auto MacMusicAppPlayPlaylist(const std::string& playlist) -> bool override; + auto MacMusicAppGetPlaylists() -> std::list override; + auto IsEventPushMode() -> bool override; + auto GetPlatformName() -> std::string override; + auto GetSubplatformName() -> std::string override; + + auto DoClipboardIsSupported() -> bool override; + auto DoClipboardHasText() -> bool override; + void DoClipboardSetText(const std::string& text) override; + auto DoClipboardGetText() -> std::string override; + auto GetDeviceUUIDInputs() -> std::list override; + + protected: + auto DoGetDataDirectoryMonolithicDefault() -> std::string override; + + private: +}; + +} // namespace ballistica::core + +#endif // BA_XCODE_BUILD || BA_OSTYPE_MACOS +#endif // BALLISTICA_CORE_PLATFORM_APPLE_CORE_PLATFORM_APPLE_H_ diff --git a/src/ballistica/platform/platform.cc b/src/ballistica/core/platform/core_platform.cc similarity index 50% rename from src/ballistica/platform/platform.cc rename to src/ballistica/core/platform/core_platform.cc index e43bbce2..f13e1916 100644 --- a/src/ballistica/platform/platform.cc +++ b/src/ballistica/core/platform/core_platform.cc @@ -1,6 +1,6 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/platform/platform.h" +#include "ballistica/core/platform/core_platform.h" #if !BA_OSTYPE_WINDOWS #include @@ -17,81 +17,63 @@ #include #endif -#include - -#include "ballistica/app/app_flavor.h" -#include "ballistica/core/thread.h" -#include "ballistica/dynamics/bg/bg_dynamics_server.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/camera.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/graphics/mesh/sprite_mesh.h" -#include "ballistica/graphics/vr_graphics.h" -#include "ballistica/input/input.h" -#include "ballistica/logic/logic.h" -#include "ballistica/networking/networking_sys.h" -#include "ballistica/platform/sdl/sdl_app.h" -#include "ballistica/platform/stdio_console.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_sys.h" - -#if BA_HEADLESS_BUILD -#include "ballistica/app/app_flavor_headless.h" -#endif - -#if BA_VR_BUILD -#include "ballistica/app/app_flavor_vr.h" -#endif +#include "ballistica/core/platform/support/min_sdl.h" +#include "ballistica/core/support/base_soft.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/networking/networking_sys.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" // ------------------------- PLATFORM SELECTION -------------------------------- // This ugly chunk of macros simply pulls in the correct platform class header -// for each platform and defines the actual class g_platform will be. +// for each platform and defines the actual class g_core->platform will be. // Android --------------------------------------------------------------------- #if BA_OSTYPE_ANDROID #if BA_GOOGLE_BUILD -#include "ballistica/platform/android/google/platform_android_google.h" -#define BA_PLATFORM_CLASS PlatformAndroidGoogle +#include "ballistica/core/platform/android/google/core_plat_andr_google.h" +#define BA_PLATFORM_CLASS CorePlatformAndroidGoogle #elif BA_AMAZON_BUILD -#include "ballistica/platform/android/amazon/platform_android_amazon.h" -#define BA_PLATFORM_CLASS PlatformAndroidAmazon +#include "ballistica/core/platform/android/amazon/core_plat_andr_amazon.h" +#define BA_PLATFORM_CLASS CorePlatformAndroidAmazon #elif BA_CARDBOARD_BUILD -#include "ballistica/platform/android/cardboard/platform_android_cardboard.h" -#define BA_PLATFORM_CLASS PlatformAndroidCardboard +#include "ballistica/core/platform/android/cardboard/core_pl_an_cardboard.h" +#define BA_PLATFORM_CLASS CorePlatformAndroidCardboard #else // Generic android. -#include "ballistica/platform/android/platform_android.h" -#define BA_PLATFORM_CLASS PlatformAndroid +#include "ballistica/core/platform/android/core_platform_android.h" +#define BA_PLATFORM_CLASS CorePlatformAndroid #endif // (Android subplatform) // Apple ----------------------------------------------------------------------- #elif BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS -#include "ballistica/platform/apple/platform_apple.h" -#define BA_PLATFORM_CLASS PlatformApple +#include "ballistica/core/platform/apple/core_platform_apple.h" +#define BA_PLATFORM_CLASS CorePlatformApple // Windows --------------------------------------------------------------------- #elif BA_OSTYPE_WINDOWS #if BA_RIFT_BUILD -#include "ballistica/platform/windows/platform_windows_oculus.h" -#define BA_PLATFORM_CLASS PlatformWindowsOculus +#include "ballistica/core/platform/windows/core_platform_windows_oculus.h" +#define BA_PLATFORM_CLASS CorePlatformWindowsOculus #else // generic windows -#include "ballistica/platform/windows/platform_windows.h" -#define BA_PLATFORM_CLASS PlatformWindows +#include "ballistica/core/platform/windows/core_platform_windows.h" +#define BA_PLATFORM_CLASS CorePlatformWindows #endif // windows subtype // Linux ----------------------------------------------------------------------- #elif BA_OSTYPE_LINUX -#include "ballistica/platform/linux/platform_linux.h" -#define BA_PLATFORM_CLASS PlatformLinux +#include "ballistica/core/platform/linux/core_platform_linux.h" +#define BA_PLATFORM_CLASS CorePlatformLinux #else // Generic --------------------------------------------------------------------- -#define BA_PLATFORM_CLASS Platform +#define BA_PLATFORM_CLASS CorePlatform #endif @@ -101,18 +83,20 @@ #error no BA_PLATFORM_CLASS defined for this platform #endif -namespace ballistica { +namespace ballistica::core { -auto Platform::Create() -> Platform* { +auto CorePlatform::Create() -> CorePlatform* { auto platform = new BA_PLATFORM_CLASS(); platform->PostInit(); assert(platform->ran_base_post_init_); return platform; } -Platform::Platform() : starttime_(GetCurrentMilliseconds()) {} +void CorePlatform::DebugLog(const std::string& msg) { HandleDebugLog(msg); } -auto Platform::PostInit() -> void { +CorePlatform::CorePlatform() : start_time_millisecs_(GetCurrentMillisecs()) {} + +void CorePlatform::PostInit() { // Hmm; we seem to get some funky invalid utf8 out of // this sometimes (mainly on windows). Should look into that // more closely or at least log it somewhere. @@ -127,9 +111,9 @@ auto Platform::PostInit() -> void { } } -Platform::~Platform() = default; +CorePlatform::~CorePlatform() = default; -auto Platform::GetLegacyDeviceUUID() -> const std::string& { +auto CorePlatform::GetLegacyDeviceUUID() -> const std::string& { if (!have_device_uuid_) { legacy_device_uuid_ = GetDeviceV1AccountUUIDPrefix(); @@ -183,87 +167,36 @@ auto Platform::GetLegacyDeviceUUID() -> const std::string& { return legacy_device_uuid_; } -auto Platform::LoginAdapterGetSignInToken(const std::string& login_type, - int attempt_id) -> void { - // Default implementation simply calls completion callback immediately. - g_logic->thread()->PushCall([login_type, attempt_id] { - PythonRef args(Py_BuildValue("(sss)", login_type.c_str(), - std::to_string(attempt_id).c_str(), ""), - PythonRef::kSteal); - g_python->obj(Python::ObjID::kLoginAdapterGetSignInTokenResponseCall) - .Call(args); - }); -} - -auto Platform::LoginAdapterBackEndActiveChange(const std::string& login_type, - bool active) -> void { - // Default is no-op. -} - -auto Platform::GetDeviceV1AccountUUIDPrefix() -> std::string { +auto CorePlatform::GetDeviceV1AccountUUIDPrefix() -> std::string { Log(LogLevel::kError, "GetDeviceV1AccountUUIDPrefix() unimplemented"); return "u"; } -auto Platform::GetRealLegacyDeviceUUID(std::string* uuid) -> bool { +auto CorePlatform::GetRealLegacyDeviceUUID(std::string* uuid) -> bool { return false; } -auto Platform::GenerateUUID() -> std::string { +auto CorePlatform::GenerateUUID() -> std::string { throw Exception("GenerateUUID() unimplemented"); } -auto Platform::GetPublicDeviceUUID() -> std::string { - assert(g_python); - if (public_device_uuid_.empty()) { - std::list inputs{GetDeviceUUIDInputs()}; - - // This UUID is supposed to change periodically, so let's plug in - // some stuff to enforce that. - inputs.emplace_back(GetOSVersionString()); - - // This part gets shuffled periodically by my version-increment tools. - // We used to plug version in directly here, but that caused uuids to - // shuffle too rapidly during periods of rapid development. This - // keeps it more constant. - // __last_rand_uuid_component_shuffle_date__ 2022 12 17 - auto rand_uuid_component{"BMCJPHH0SC22KB0WVJ1RAYD68TPEXL58"}; - - inputs.emplace_back(rand_uuid_component); - auto gil{Python::ScopedInterpreterLock()}; - auto pylist{g_python->StringList(inputs)}; - auto args{g_python->SingleMemberTuple(pylist)}; - auto result = g_python->obj(Python::ObjID::kHashStringsCall).Call(args); - assert(result.UnicodeCheck()); - public_device_uuid_ = result.Str(); - } - return public_device_uuid_; -} - -auto Platform::GetDeviceUUIDInputs() -> std::list { +auto CorePlatform::GetDeviceUUIDInputs() -> std::list { throw Exception("GetDeviceUUIDInputs unimplemented"); } -auto Platform::GetDefaultConfigDirectory() -> std::string { +auto CorePlatform::DoGetConfigDirectoryMonolithicDefault() + -> std::optional { std::string config_dir; - // As a default, look for a HOME env var and use that if present - // this will cover linux and command-line macOS. - char* home = getenv("HOME"); - if (home) { - config_dir = std::string(home) + "/.ballisticacore"; - } else { - printf("GetDefaultConfigDirectory: can't get env var \"HOME\"\n"); - fflush(stdout); - throw Exception(); - } - return config_dir; + // Go with unset here; let baenv handle it in Python-land. + return {}; } -auto Platform::GetConfigFilePath() -> std::string { +auto CorePlatform::GetConfigFilePath() -> std::string { return GetConfigDirectory() + BA_DIRSLASH + "config.json"; } -auto Platform::GetLowLevelConfigValue(const char* key, int default_value) +// FIXME: should make this unnecessary. +auto CorePlatform::GetLowLevelConfigValue(const char* key, int default_value) -> int { std::string path = GetConfigDirectory() + BA_DIRSLASH + ".cvar_" + key; int val = default_value; @@ -281,7 +214,8 @@ auto Platform::GetLowLevelConfigValue(const char* key, int default_value) return val; } -void Platform::SetLowLevelConfigValue(const char* key, int value) { +// FIXME: should make this unnecessary. +void CorePlatform::SetLowLevelConfigValue(const char* key, int value) { std::string path = GetConfigDirectory() + BA_DIRSLASH + ".cvar_" + key; std::string out = std::to_string(value); FILE* f = FOpen(path.c_str(), "w"); @@ -295,19 +229,12 @@ void Platform::SetLowLevelConfigValue(const char* key, int value) { } } -auto Platform::GetUserPythonDirectory() -> std::string { - // Make sure it exists the first time we run. - if (!attempted_to_make_user_scripts_dir_) { - user_scripts_dir_ = DoGetUserPythonDirectory(); - - // Attempt to make it. (it's ok if this fails) - MakeDir(user_scripts_dir_, true); - attempted_to_make_user_scripts_dir_ = true; - } - return user_scripts_dir_; +auto CorePlatform::GetUserPythonDirectory() -> std::optional { + BA_PRECONDITION(have_ba_env_vals_); + return ba_env_user_python_dir_; } -auto Platform::GetVolatileDataDirectory() -> std::string { +auto CorePlatform::GetVolatileDataDirectory() -> std::string { if (!made_volatile_data_dir_) { volatile_data_dir_ = GetDefaultVolatileDataDirectory(); MakeDir(volatile_data_dir_); @@ -316,51 +243,48 @@ auto Platform::GetVolatileDataDirectory() -> std::string { return volatile_data_dir_; } -auto Platform::GetDefaultVolatileDataDirectory() -> std::string { +auto CorePlatform::GetDefaultVolatileDataDirectory() -> std::string { // By default, stuff this in a subdir under our config dir. return GetConfigDirectory() + BA_DIRSLASH + "vdata"; } -auto Platform::GetAppPythonDirectory() -> std::string { - static bool checked_dir = false; - if (!checked_dir) { - checked_dir = true; +auto CorePlatform::GetAppPythonDirectory() -> std::optional { + BA_PRECONDITION(have_ba_env_vals_); + return ba_env_app_python_dir_; - // If there is a sys/VERSION in the user-python dir we use that. - app_python_dir_ = GetUserPythonDirectory() + BA_DIRSLASH + "sys" - + BA_DIRSLASH + kAppVersion; + // TODO(ericf) - recreate this behavior within baenv. + // // If there is a sys/VERSION in the user-python dir we use that. + // app_python_dir_ = GetUserPythonDirectoryMonolithicDefault() + BA_DIRSLASH + // + "sys" + BA_DIRSLASH + kEngineVersion; - // Fall back to our default if that doesn't exist. - if (FilePathExists(app_python_dir_)) { - using_custom_app_python_dir_ = true; - Log(LogLevel::kInfo, "Using custom app Python path: '" - + (GetUserPythonDirectory() + BA_DIRSLASH + "sys" - + BA_DIRSLASH + kAppVersion) - + "'."); + // // Fall back to our default if that doesn't exist. + // if (FilePathExists(app_python_dir_)) { + // using_custom_app_python_dir_ = true; + // Log(LogLevel::kInfo, + // "Using custom app Python path: '" + // + (GetUserPythonDirectoryMonolithicDefault() + BA_DIRSLASH + + // "sys" + // + BA_DIRSLASH + kEngineVersion) + // + "'."); - } else { - // Going with relative paths for cleaner tracebacks... - app_python_dir_ = std::string("ba_data") + BA_DIRSLASH + "python"; - } - } - return app_python_dir_; + // } else { + // // Special case: if CWD is '.', omit the './' for a prettier path. + // app_python_dir_ = std::string("ba_data") + BA_DIRSLASH + "python"; + // auto data_dir = GetDataDirectoryMonolithicDefault(); + // if (data_dir != ".") { + // app_python_dir_ = data_dir + BA_DIRSLASH + app_python_dir_; + // } + // } + // } + // return app_python_dir_; } -auto Platform::GetSitePythonDirectory() -> std::string { - static bool checked_dir = false; - if (!checked_dir) { - checked_dir = true; - - if (!FilePathExists(site_python_dir_)) { - // Going with relative paths for cleaner tracebacks... - site_python_dir_ = - std::string("ba_data") + BA_DIRSLASH + "python-site-packages"; - } - } - return site_python_dir_; +auto CorePlatform::GetSitePythonDirectory() -> std::optional { + BA_PRECONDITION(have_ba_env_vals_); + return ba_env_site_python_dir_; } -auto Platform::GetReplaysDir() -> std::string { +auto CorePlatform::GetReplaysDir() -> std::string { static bool made_dir = false; if (!made_dir) { replays_dir_ = GetConfigDirectory() + BA_DIRSLASH + "replays"; @@ -371,7 +295,7 @@ auto Platform::GetReplaysDir() -> std::string { } // rename() supporting UTF8 strings. -auto Platform::Rename(const char* oldname, const char* newname) -> int { +auto CorePlatform::Rename(const char* oldname, const char* newname) -> int { // This default implementation covers non-windows platforms. #if BA_OSTYPE_WINDOWS throw Exception(); @@ -380,7 +304,7 @@ auto Platform::Rename(const char* oldname, const char* newname) -> int { #endif } -auto Platform::Remove(const char* path) -> int { +auto CorePlatform::Remove(const char* path) -> int { // This default implementation covers non-windows platforms. #if BA_OSTYPE_WINDOWS throw Exception(); @@ -390,7 +314,7 @@ auto Platform::Remove(const char* path) -> int { } // stat() supporting UTF8 strings. -auto Platform::Stat(const char* path, struct BA_STAT* buffer) -> int { +auto CorePlatform::Stat(const char* path, struct BA_STAT* buffer) -> int { // This default implementation covers non-windows platforms. #if BA_OSTYPE_WINDOWS throw Exception(); @@ -400,7 +324,7 @@ auto Platform::Stat(const char* path, struct BA_STAT* buffer) -> int { } // fopen() supporting UTF8 strings. -auto Platform::FOpen(const char* path, const char* mode) -> FILE* { +auto CorePlatform::FOpen(const char* path, const char* mode) -> FILE* { // This default implementation covers non-windows platforms. #if BA_OSTYPE_WINDOWS throw Exception(); @@ -409,22 +333,22 @@ auto Platform::FOpen(const char* path, const char* mode) -> FILE* { #endif } -auto Platform::FilePathExists(const std::string& name) -> bool { +auto CorePlatform::FilePathExists(const std::string& name) -> bool { struct BA_STAT buffer {}; return (Stat(name.c_str(), &buffer) == 0); } -auto Platform::GetSocketErrorString() -> std::string { +auto CorePlatform::GetSocketErrorString() -> std::string { // On default platforms we just look at errno. return GetErrnoString(); } -auto Platform::GetSocketError() -> int { +auto CorePlatform::GetSocketError() -> int { // By default this is simply errno. return errno; } -auto Platform::GetErrnoString() -> std::string { +auto CorePlatform::GetErrnoString() -> std::string { // This default implementation covers non-windows platforms. #if BA_OSTYPE_WINDOWS throw Exception(); @@ -448,29 +372,36 @@ auto Platform::GetErrnoString() -> std::string { #endif } -// Return the ballisticacore config dir +// Return the ballisticakit config dir // This does not vary across versions. -auto Platform::GetConfigDirectory() -> std::string { - // Make sure args have been handled since we use them. - assert(g_app->args_handled); - - if (!have_config_dir_) { - // If the user provided cfgdir as an arg. - if (!g_app->user_config_dir.empty()) { - config_dir_ = g_app->user_config_dir; - } else { - config_dir_ = GetDefaultConfigDirectory(); - } - - // Try to make sure the config dir exists. - MakeDir(config_dir_); - - have_config_dir_ = true; - } - return config_dir_; +auto CorePlatform::GetConfigDirectory() -> std::string { + BA_PRECONDITION(have_ba_env_vals_); + return ba_env_config_dir_; } -void Platform::MakeDir(const std::string& dir, bool quiet) { +auto CorePlatform::GetConfigDirectoryMonolithicDefault() + -> std::optional { + // CoreConfig value trumps all. Otherwise go with platform-specific default. + if (g_core->core_config().config_dir.has_value()) { + return *g_core->core_config().config_dir; + } + return DoGetConfigDirectoryMonolithicDefault(); +} + +auto CorePlatform::GetDataDirectory() -> std::string { + BA_PRECONDITION(have_ba_env_vals_); + return ba_env_data_dir_; +} + +auto CorePlatform::GetDataDirectoryMonolithicDefault() -> std::string { + // CoreConfig arg trumps all. Otherwise ask for platform-specific value. + if (g_core->core_config().data_dir.has_value()) { + return *g_core->core_config().data_dir; + } + return DoGetDataDirectoryMonolithicDefault(); +} + +void CorePlatform::MakeDir(const std::string& dir, bool quiet) { bool exists = FilePathExists(dir); if (!exists) { DoMakeDir(dir, quiet); @@ -481,15 +412,26 @@ void Platform::MakeDir(const std::string& dir, bool quiet) { } } -auto Platform::AndroidGetExternalFilesDir() -> std::string { +auto CorePlatform::AndroidGetExternalFilesDir() -> std::string { throw Exception("AndroidGetExternalFilesDir() unimplemented"); } -auto Platform::DoGetUserPythonDirectory() -> std::string { - return GetConfigDirectory() + BA_DIRSLASH + "mods"; +auto CorePlatform::GetUserPythonDirectoryMonolithicDefault() + -> std::optional { + // CoreConfig arg trumps all. Otherwise ask for platform-specific value. + if (g_core->core_config().user_python_dir.has_value()) { + return *g_core->core_config().user_python_dir; + } + return DoGetUserPythonDirectoryMonolithicDefault(); } -void Platform::DoMakeDir(const std::string& dir, bool quiet) { +auto CorePlatform::DoGetUserPythonDirectoryMonolithicDefault() + -> std::optional { + // Go with unset; let baenv calc this in Python-land + return {}; +} + +void CorePlatform::DoMakeDir(const std::string& dir, bool quiet) { // This default implementation covers non-windows platforms. #if BA_OSTYPE_WINDOWS throw Exception(); @@ -504,7 +446,7 @@ void Platform::DoMakeDir(const std::string& dir, bool quiet) { #endif } -auto Platform::GetLocale() -> std::string { +auto CorePlatform::GetLocale() -> std::string { const char* lang = getenv("LANG"); if (lang) { return lang; @@ -517,16 +459,16 @@ auto Platform::GetLocale() -> std::string { } } -auto Platform::GetDeviceName() -> std::string { +auto CorePlatform::GetDeviceName() -> std::string { assert(ran_base_post_init_); return device_name_; } -auto Platform::DoGetDeviceName() -> std::string { +auto CorePlatform::DoGetDeviceName() -> std::string { // Check devicename in env_var char* devicename; devicename = getenv("BA_DEVICE_NAME"); - if (devicename != NULL) { + if (devicename != nullptr) { return devicename; } @@ -540,9 +482,9 @@ auto Platform::DoGetDeviceName() -> std::string { return "Untitled Device"; } -auto Platform::IsRunningOnTV() -> bool { return false; } +auto CorePlatform::IsRunningOnTV() -> bool { return false; } -auto Platform::HasTouchScreen() -> bool { +auto CorePlatform::HasTouchScreen() -> bool { if (!have_has_touchscreen_value_) { have_touchscreen_ = DoHasTouchScreen(); have_has_touchscreen_value_ = true; @@ -550,180 +492,53 @@ auto Platform::HasTouchScreen() -> bool { return have_touchscreen_; } -auto Platform::IsRunningOnFireTV() -> bool { return false; } +auto CorePlatform::IsRunningOnFireTV() -> bool { return false; } -auto Platform::IsRunningOnDaydream() -> bool { return false; } +auto CorePlatform::IsRunningOnDaydream() -> bool { return false; } -auto Platform::DoHasTouchScreen() -> bool { throw Exception("UNIMPLEMENTED"); } +auto CorePlatform::DoHasTouchScreen() -> bool { + throw Exception("UNIMPLEMENTED"); +} -auto Platform::IsRunningOnDesktop() -> bool { +auto CorePlatform::IsRunningOnDesktop() -> bool { // Default case to cover mac, win, etc. return true; } -void Platform::SleepMS(millisecs_t ms) { +void CorePlatform::SleepMillisecs(millisecs_t ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); } #pragma clang diagnostic push #pragma ide diagnostic ignored "NullDereferences" -static void HandleArgs(int argc, char** argv) { - assert(!g_app->args_handled); - g_app->args_handled = true; +void CorePlatform::WillExitMain(bool errored) {} - // If there's just one arg and it's "--version", return the version. - if (argc == 2 && !strcmp(argv[1], "--version")) { - printf("BallisticaCore %s build %d\n", kAppVersion, kAppBuildNumber); - fflush(stdout); - exit(0); - } - int dummyval{}; - for (int i = 1; i < argc; ++i) { - // In our rift build, a '-2d' arg causes us to run in regular 2d mode. - if (g_buildconfig.rift_build() && !strcmp(argv[i], "-2d")) { - g_app->vr_mode = false; - } else if (!strcmp(argv[i], "-exec")) { - if (i + 1 < argc) { - g_app->exec_command = argv[i + 1]; - } else { - printf("%s", "Error: expected arg after -exec\n"); - fflush(stdout); - exit(-1); - } - } else if (!strcmp(argv[i], "--crash")) { - int* invalid_ptr{&dummyval}; - - // A bit of obfuscation to try and keep linters quiet. - if (explicit_bool(true)) { - invalid_ptr = nullptr; - } - if (explicit_bool(true)) { - *invalid_ptr = 1; - } - } else if (!strcmp(argv[i], "-cfgdir")) { - if (i + 1 < argc) { - g_app->user_config_dir = argv[i + 1]; - - // Need to convert this to an abs path since we chdir soon. - bool success = - g_platform->AbsPath(argv[i + 1], &g_app->user_config_dir); - if (!success) { - // This can fail if the path doesn't exist. - if (!g_platform->FilePathExists(argv[i + 1])) { - printf("ERROR: provided config dir does not exist: '%s'\n", - argv[i + 1]); - } else { - printf( - "ERROR: unable to determine absolute path of config dir '%s'\n", - argv[i + 1]); - } - fflush(stdout); - exit(-1); - } - } else { - Log(LogLevel::kError, "Expected arg after -cfgdir."); - exit(-1); - } - } - } -#pragma clang diagnostic pop - - // In Android's case we have to pull our exec arg from the java/kotlin layer. - if (g_buildconfig.ostype_android()) { - g_app->exec_command = g_platform->GetAndroidExecArg(); - } - - // TEMP/HACK: hard code launch args. - if (explicit_bool(false)) { - if (g_buildconfig.ostype_android()) { - g_app->exec_command = "import ba.internal; ba.internal.run_stress_test()"; - } - } -} - -auto Platform::CreateAppFlavor() -> AppFlavor* { - assert(g_app); - assert(InMainThread()); - assert(g_main_thread); - - // Hmm do these belong here?... - HandleArgs(g_app->argc, g_app->argv); - -// TEMP - need to init sdl on our legacy mac build even though its not -// technically an SDL app. Kill this once the old mac build is gone. -#if BA_LEGACY_MACOS_BUILD - SDLApp::InitSDL(); -#endif - - AppFlavor* app_flavor{}; - -#if BA_HEADLESS_BUILD - app_flavor = new AppFlavorHeadless(g_main_thread); -#elif BA_RIFT_BUILD - // Rift build can spin up in either VR or regular mode. - if (g_app->vr_mode) { - app_flavor = new AppFlavorVR(g_main_thread); - } else { - app_flavor = new SDLApp(g_main_thread); - } -#elif BA_CARDBOARD_BUILD - app_flavor = new AppFlavorVR(g_main_thread); -#elif BA_SDL_BUILD - app_flavor = new SDLApp(g_main_thread); -#else - app_flavor = new AppFlavor(g_main_thread); -#endif - - assert(app_flavor); - app_flavor->PostInit(); - return app_flavor; -} - -auto Platform::CreateGraphics() -> Graphics* { -#if BA_VR_BUILD - return new VRGraphics(); -#else - return new Graphics(); -#endif -} - -auto Platform::GetKeyName(int keycode) -> std::string { - // On our actual SDL platforms we're trying to be *pure* sdl so - // call their function for this. Otherwise we call our own version - // of it which is basically the same thing (at least for now). -#if BA_SDL_BUILD && !BA_MINSDL_BUILD - return SDL_GetKeyName(static_cast(keycode)); -#else - return g_input->GetKeyName(keycode); -#endif -} - -void Platform::WillExitMain(bool errored) {} - -auto Platform::GetUIScale() -> UIScale { +auto CorePlatform::GetUIScale() -> UIScale { // Handles mac/pc/linux cases. return UIScale::kLarge; } -void Platform::DisplayLog(const std::string& name, LogLevel level, - const std::string& msg) { +void CorePlatform::DisplayLog(const std::string& name, LogLevel level, + const std::string& msg) { // Do nothing by default. } -auto Platform::ReportFatalError(const std::string& message, - bool in_top_level_exception_handler) -> bool { +auto CorePlatform::ReportFatalError(const std::string& message, + bool in_top_level_exception_handler) + -> bool { // Don't override handling by default. return false; } -auto Platform::HandleFatalError(bool exit_cleanly, - bool in_top_level_exception_handler) -> bool { +auto CorePlatform::HandleFatalError(bool exit_cleanly, + bool in_top_level_exception_handler) + -> bool { // Don't override handling by default. return false; } -auto Platform::CanShowBlockingFatalErrorDialog() -> bool { +auto CorePlatform::CanShowBlockingFatalErrorDialog() -> bool { if (g_buildconfig.sdl2_build()) { return true; } else { @@ -731,40 +546,22 @@ auto Platform::CanShowBlockingFatalErrorDialog() -> bool { } } -auto Platform::BlockingFatalErrorDialog(const std::string& message) -> void { +void CorePlatform::BlockingFatalErrorDialog(const std::string& message) { #if BA_SDL2_BUILD - assert(InMainThread()); - if (!HeadlessMode()) { + assert(g_core->InMainThread()); + if (!g_core->HeadlessMode()) { SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Fatal Error", message.c_str(), nullptr); } #endif } -void Platform::SetupDataDirectory() { -// This default implementation covers non-windows platforms. -#if BA_OSTYPE_WINDOWS - throw Exception(); -#else - // Default to './ba_data'. - DIR* d = opendir("ba_data"); - if (d == nullptr) { - throw Exception("ba_data directory not found."); - } - closedir(d); -#endif - - // Apparently Android NDK 22 includes std::filesystem; once that is out - // then we should be able to use this everywhere. - // Oh - and we also need to wait for GCC 8, so when we switch to Ubuntu20... - // Oh; and we should see if switch/etc. supports it before making it a hard - // requirement. - // if (!std::filesystem::is_directory("ba_data")) { - // throw Exception("ba_data directory not found."); - // } +auto CorePlatform::DoGetDataDirectoryMonolithicDefault() -> std::string { + // By default, assume we're in it. + return "."; } -void Platform::SetEnv(const std::string& name, const std::string& value) { +void CorePlatform::SetEnv(const std::string& name, const std::string& value) { // This default implementation covers non-windows platforms. #if BA_OSTYPE_WINDOWS throw Exception(); @@ -777,7 +574,21 @@ void Platform::SetEnv(const std::string& name, const std::string& value) { #endif } -auto Platform::GetIsStdinATerminal() -> bool { +auto CorePlatform::GetEnv(const std::string& name) + -> std::optional { + // This default implementation covers non-windows platforms. +#if BA_OSTYPE_WINDOWS + throw Exception(); +#else + std::optional out{}; + if (char* val = getenv(name.c_str())) { + out = val; + } + return out; +#endif +} + +auto CorePlatform::GetIsStdinATerminal() -> bool { // This covers non-windows cases. #if BA_OSTYPE_WINDOWS throw Exception(); @@ -786,9 +597,9 @@ auto Platform::GetIsStdinATerminal() -> bool { #endif } -auto Platform::GetOSVersionString() -> std::string { return ""; } +auto CorePlatform::GetOSVersionString() -> std::string { return ""; } -auto Platform::GetUserAgentString() -> std::string { +auto CorePlatform::GetUserAgentString() -> std::string { std::string device = GetDeviceName(); std::string version = GetOSVersionString(); if (!version.empty()) { @@ -832,8 +643,8 @@ auto Platform::GetUserAgentString() -> std::string { subplatform += " OnTV"; } - std::string out{std::string("BallisticaCore ") + kAppVersion + subplatform - + " (" + std::to_string(kAppBuildNumber) + ") (" + std::string out{std::string("BallisticaKit ") + kEngineVersion + subplatform + + " (" + std::to_string(kEngineBuildNumber) + ") (" + g_buildconfig.platform_string() + version + "; " + device + "; " + GetLocale() + ")"}; @@ -844,7 +655,7 @@ auto Platform::GetUserAgentString() -> std::string { return out; } -auto Platform::GetCWD() -> std::string { +auto CorePlatform::GetCWD() -> std::string { // This default implementation covers non-windows platforms. #if BA_OSTYPE_WINDOWS throw Exception(); @@ -854,95 +665,97 @@ auto Platform::GetCWD() -> std::string { #endif } -auto Platform::GetAndroidExecArg() -> std::string { return ""; } +auto CorePlatform::GetAndroidExecArg() -> std::string { return ""; } -void Platform::GetTextBoundsAndWidth(const std::string& text, Rect* r, - float* width) { +void CorePlatform::GetTextBoundsAndWidth(const std::string& text, Rect* r, + float* width) { throw Exception(); } -void Platform::FreeTextTexture(void* tex) { throw Exception(); } +void CorePlatform::FreeTextTexture(void* tex) { throw Exception(); } -auto Platform::CreateTextTexture(int width, int height, - const std::vector& strings, - const std::vector& positions, - const std::vector& widths, float scale) - -> void* { +auto CorePlatform::CreateTextTexture(int width, int height, + const std::vector& strings, + const std::vector& positions, + const std::vector& widths, + float scale) -> void* { throw Exception(); } -auto Platform::GetTextTextureData(void* tex) -> uint8_t* { throw Exception(); } +auto CorePlatform::GetTextTextureData(void* tex) -> uint8_t* { + throw Exception(); +} -void Platform::OnAppStart() {} +void CorePlatform::OnMainThreadStartApp() {} -auto Platform::ConvertIncomingLeaderboardScore( +void CorePlatform::OnAppStart() { + assert(g_base_soft && g_base_soft->InLogicThread()); +} + +void CorePlatform::OnAppPause() { + assert(g_base_soft && g_base_soft->InLogicThread()); +} + +void CorePlatform::OnAppResume() { + assert(g_base_soft && g_base_soft->InLogicThread()); +} + +void CorePlatform::OnAppShutdown() { + assert(g_base_soft && g_base_soft->InLogicThread()); +} + +void CorePlatform::OnScreenSizeChange() { + assert(g_base_soft && g_base_soft->InLogicThread()); +} + +void CorePlatform::StepDisplayTime() { + assert(g_base_soft && g_base_soft->InLogicThread()); +} + +auto CorePlatform::ConvertIncomingLeaderboardScore( const std::string& leaderboard_id, int score) -> int { return score; } -void Platform::SubmitScore(const std::string& game, const std::string& version, - int64_t score) { +void CorePlatform::SubmitScore(const std::string& game, + const std::string& version, int64_t score) { Log(LogLevel::kError, "FIXME: SubmitScore() unimplemented"); } -void Platform::ReportAchievement(const std::string& achievement) {} +void CorePlatform::ReportAchievement(const std::string& achievement) {} -auto Platform::HaveLeaderboard(const std::string& game, - const std::string& config) -> bool { +auto CorePlatform::HaveLeaderboard(const std::string& game, + const std::string& config) -> bool { return false; } -void Platform::EditText(const std::string& title, const std::string& value, - int max_chars) { +void CorePlatform::EditText(const std::string& title, const std::string& value, + int max_chars) { Log(LogLevel::kError, "FIXME: EditText() unimplemented"); } -void Platform::ShowOnlineScoreUI(const std::string& show, - const std::string& game, - const std::string& game_version) { +void CorePlatform::ShowOnlineScoreUI(const std::string& show, + const std::string& game, + const std::string& game_version) { Log(LogLevel::kError, "FIXME: ShowOnlineScoreUI() unimplemented"); } -auto Platform::Purchase(const std::string& item) -> void { - // We use alternate _c ids for consumables in some cases where - // we originally used entitlements. We are all consumables now though - // so we can purchase for different accounts. - std::string item_filtered{item}; - if (g_buildconfig.amazon_build()) { - if (item == "bundle_bones" || item == "bundle_bernard" - || item == "bundle_frosty" || item == "bundle_santa" || item == "pro" - || item == "pro_sale") { - item_filtered = item + "_c"; - } - } - DoPurchase(item_filtered); -} - -auto Platform::DoPurchase(const std::string& item) -> void { - // Just print 'unavailable' by default. - g_python->PushObjCall(Python::ObjID::kUnavailableMessageCall); -} - -void Platform::RestorePurchases() { - Log(LogLevel::kError, "RestorePurchases() unimplemented"); -} - -void Platform::AndroidSetResString(const std::string& res) { +void CorePlatform::AndroidSetResString(const std::string& res) { throw Exception(); } -void Platform::ApplyConfig() {} +void CorePlatform::ApplyAppConfig() {} -void Platform::AndroidSynthesizeBackPress() { +void CorePlatform::AndroidSynthesizeBackPress() { Log(LogLevel::kError, "AndroidSynthesizeBackPress() unimplemented"); } -void Platform::AndroidQuitActivity() { +void CorePlatform::AndroidQuitActivity() { Log(LogLevel::kError, "AndroidQuitActivity() unimplemented"); } -auto Platform::GetDeviceV1AccountID() -> std::string { - if (HeadlessMode()) { +auto CorePlatform::GetDeviceV1AccountID() -> std::string { + if (g_core->HeadlessMode()) { return "S-" + GetLegacyDeviceUUID(); } @@ -954,7 +767,7 @@ auto Platform::GetDeviceV1AccountID() -> std::string { return "L-" + GetLegacyDeviceUUID(); } -auto Platform::DemangleCXXSymbol(const std::string& s) -> std::string { +auto CorePlatform::DemangleCXXSymbol(const std::string& s) -> std::string { // Do __cxa_demangle on platforms that support it. // FIXME; I believe there's an equivalent call for windows; should research. #if !BA_OSTYPE_WINDOWS @@ -979,159 +792,140 @@ auto Platform::DemangleCXXSymbol(const std::string& s) -> std::string { #endif } -auto Platform::NewAutoReleasePool() -> void* { throw Exception(); } +auto CorePlatform::NewAutoReleasePool() -> void* { throw Exception(); } -void Platform::DrainAutoReleasePool(void* pool) { throw Exception(); } +void CorePlatform::DrainAutoReleasePool(void* pool) { throw Exception(); } -void Platform::OpenURL(const std::string& url) { - // Can't open URLs in VR - just tell the logic thread to show the url. - if (IsVRMode()) { - g_logic->PushShowURLCall(url); - return; - } - - // Otherwise fall back to our platform-specific handler. - g_platform->DoOpenURL(url); -} - -void Platform::DoOpenURL(const std::string& url) { - Log(LogLevel::kError, "DoOpenURL unimplemented on this platform."); -} - -void Platform::ResetAchievements() { +void CorePlatform::ResetAchievements() { Log(LogLevel::kError, "ResetAchievements() unimplemented"); } -void Platform::GameCenterLogin() { throw Exception(); } +void CorePlatform::GameCenterLogin() { throw Exception(); } -void Platform::PurchaseAck(const std::string& purchase, - const std::string& order_id) { - Log(LogLevel::kError, "PurchaseAck() unimplemented"); -} +void CorePlatform::RunEvents() {} -void Platform::RunEvents() {} +auto CorePlatform::GetMemUsageInfo() -> std::string { return "0,0,0"; } -auto Platform::GetMemUsageInfo() -> std::string { return "0,0,0"; } - -void Platform::OnAppPause() {} -void Platform::OnAppResume() {} - -void Platform::MusicPlayerPlay(PyObject* target) { +void CorePlatform::MusicPlayerPlay(PyObject* target) { Log(LogLevel::kError, "MusicPlayerPlay() unimplemented on this platform"); } -void Platform::MusicPlayerStop() { + +void CorePlatform::MusicPlayerStop() { Log(LogLevel::kError, "MusicPlayerStop() unimplemented on this platform"); } -void Platform::MusicPlayerShutdown() { + +void CorePlatform::MusicPlayerShutdown() { Log(LogLevel::kError, "MusicPlayerShutdown() unimplemented on this platform"); } -void Platform::MusicPlayerSetVolume(float volume) { +void CorePlatform::MusicPlayerSetVolume(float volume) { Log(LogLevel::kError, "MusicPlayerSetVolume() unimplemented on this platform"); } -auto Platform::IsOSPlayingMusic() -> bool { return false; } +auto CorePlatform::IsOSPlayingMusic() -> bool { return false; } -void Platform::AndroidShowAppInvite(const std::string& title, - const std::string& message, - const std::string& code) { +void CorePlatform::AndroidShowAppInvite(const std::string& title, + const std::string& message, + const std::string& code) { Log(LogLevel::kError, "AndroidShowAppInvite() unimplemented"); } -void Platform::IncrementAnalyticsCount(const std::string& name, int increment) { -} +void CorePlatform::IncrementAnalyticsCount(const std::string& name, + int increment) {} -void Platform::IncrementAnalyticsCountRaw(const std::string& name, - int increment) {} +void CorePlatform::IncrementAnalyticsCountRaw(const std::string& name, + int increment) {} -void Platform::IncrementAnalyticsCountRaw2(const std::string& name, - int uses_increment, int increment) {} +void CorePlatform::IncrementAnalyticsCountRaw2(const std::string& name, + int uses_increment, + int increment) {} -void Platform::SetAnalyticsScreen(const std::string& screen) {} +void CorePlatform::SetAnalyticsScreen(const std::string& screen) {} -void Platform::SubmitAnalyticsCounts() {} +void CorePlatform::SubmitAnalyticsCounts() {} -void Platform::SetPlatformMiscReadVals(const std::string& vals) {} +void CorePlatform::SetPlatformMiscReadVals(const std::string& vals) {} -void Platform::ShowAd(const std::string& purpose) { +void CorePlatform::ShowAd(const std::string& purpose) { Log(LogLevel::kError, "ShowAd() unimplemented"); } -auto Platform::GetHasAds() -> bool { return false; } +auto CorePlatform::GetHasAds() -> bool { return false; } -auto Platform::GetHasVideoAds() -> bool { +auto CorePlatform::GetHasVideoAds() -> bool { // By default we assume we have this anywhere we have ads. return GetHasAds(); } -void Platform::SignInV1(const std::string& account_type) { +void CorePlatform::SignInV1(const std::string& account_type) { Log(LogLevel::kError, "SignInV1() unimplemented"); } -void Platform::V1LoginDidChange() { +void CorePlatform::V1LoginDidChange() { // Default is no-op. } -void Platform::SignOutV1() { +void CorePlatform::SignOutV1() { Log(LogLevel::kError, "SignOutV1() unimplemented"); } -void Platform::AndroidShowWifiSettings() { +void CorePlatform::AndroidShowWifiSettings() { Log(LogLevel::kError, "AndroidShowWifiSettings() unimplemented"); } -void Platform::SetHardwareCursorVisible(bool visible) { +void CorePlatform::SetHardwareCursorVisible(bool visible) { // FIXME: Forward this to app?.. #if BA_SDL_BUILD SDL_ShowCursor(visible ? SDL_ENABLE : SDL_DISABLE); #endif } -auto Platform::QuitApp() -> void { exit(g_app->return_value); } +void CorePlatform::QuitApp() { exit(g_core->return_value); } -auto Platform::OpenFileExternally(const std::string& path) -> void { +void CorePlatform::OpenFileExternally(const std::string& path) { Log(LogLevel::kError, "OpenFileExternally() unimplemented"); } -auto Platform::OpenDirExternally(const std::string& path) -> void { +void CorePlatform::OpenDirExternally(const std::string& path) { Log(LogLevel::kError, "OpenDirExternally() unimplemented"); } -auto Platform::MacMusicAppInit() -> void { +void CorePlatform::MacMusicAppInit() { Log(LogLevel::kError, "MacMusicAppInit() unimplemented"); } -auto Platform::MacMusicAppGetVolume() -> int { +auto CorePlatform::MacMusicAppGetVolume() -> int { Log(LogLevel::kError, "MacMusicAppGetVolume() unimplemented"); return 0; } -auto Platform::MacMusicAppSetVolume(int volume) -> void { +void CorePlatform::MacMusicAppSetVolume(int volume) { Log(LogLevel::kError, "MacMusicAppSetVolume() unimplemented"); } -auto Platform::MacMusicAppGetLibrarySource() -> void { +void CorePlatform::MacMusicAppGetLibrarySource() { Log(LogLevel::kError, "MacMusicAppGetLibrarySource() unimplemented"); } -auto Platform::MacMusicAppStop() -> void { +void CorePlatform::MacMusicAppStop() { Log(LogLevel::kError, "MacMusicAppStop() unimplemented"); } -auto Platform::MacMusicAppPlayPlaylist(const std::string& playlist) -> bool { +auto CorePlatform::MacMusicAppPlayPlaylist(const std::string& playlist) + -> bool { Log(LogLevel::kError, "MacMusicAppPlayPlaylist() unimplemented"); return false; } -auto Platform::MacMusicAppGetPlaylists() -> std::list { +auto CorePlatform::MacMusicAppGetPlaylists() -> std::list { Log(LogLevel::kError, "MacMusicAppGetPlaylists() unimplemented"); return {}; } -auto Platform::SetCurrentThreadName(const std::string& name) -> void { +void CorePlatform::SetCurrentThreadName(const std::string& name) { // Currently we leave the main thread alone, otherwise we show up as // "BallisticaMainThread" under "top" on linux (should check other platforms). - if (InMainThread()) { + if (g_core->InMainThread()) { return; } #if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS @@ -1141,7 +935,7 @@ auto Platform::SetCurrentThreadName(const std::string& name) -> void { #endif } -auto Platform::Unlink(const char* path) -> void { +void CorePlatform::Unlink(const char* path) { // This default implementation covers non-windows platforms. #if BA_OSTYPE_WINDOWS throw Exception(); @@ -1150,7 +944,8 @@ auto Platform::Unlink(const char* path) -> void { #endif } -auto Platform::AbsPath(const std::string& path, std::string* outpath) -> bool { +auto CorePlatform::AbsPath(const std::string& path, std::string* outpath) + -> bool { // Ensure all implementations fail if the file does not exist. if (!FilePathExists(path)) { return false; @@ -1158,7 +953,7 @@ auto Platform::AbsPath(const std::string& path, std::string* outpath) -> bool { return DoAbsPath(path, outpath); } -auto Platform::DoAbsPath(const std::string& path, std::string* outpath) +auto CorePlatform::DoAbsPath(const std::string& path, std::string* outpath) -> bool { // This covers all but windows. #if BA_OSTYPE_WINDOWS @@ -1174,11 +969,13 @@ auto Platform::DoAbsPath(const std::string& path, std::string* outpath) #endif } -auto Platform::IsEventPushMode() -> bool { return false; } +auto CorePlatform::IsEventPushMode() -> bool { return false; } -auto Platform::GetDisplayResolution(int* x, int* y) -> bool { return false; } +auto CorePlatform::GetDisplayResolution(int* x, int* y) -> bool { + return false; +} -auto Platform::CloseSocket(int socket) -> void { +void CorePlatform::CloseSocket(int socket) { // This default implementation covers non-windows platforms. #if BA_OSTYPE_WINDOWS throw Exception(); @@ -1187,7 +984,7 @@ auto Platform::CloseSocket(int socket) -> void { #endif } -auto Platform::GetBroadcastAddrs() -> std::vector { +auto CorePlatform::GetBroadcastAddrs() -> std::vector { // This default implementation covers non-windows platforms. #if BA_OSTYPE_WINDOWS throw Exception(); @@ -1221,7 +1018,7 @@ auto Platform::GetBroadcastAddrs() -> std::vector { #endif } -auto Platform::SetSocketNonBlocking(int sd) -> bool { +auto CorePlatform::SetSocketNonBlocking(int sd) -> bool { // This default implementation covers non-windows platforms. #if BA_OSTYPE_WINDOWS throw Exception(); @@ -1229,28 +1026,26 @@ auto Platform::SetSocketNonBlocking(int sd) -> bool { int result = fcntl(sd, F_SETFL, O_NONBLOCK); if (result != 0) { Log(LogLevel::kError, "Error setting non-blocking socket: " - + g_platform->GetSocketErrorString()); + + g_core->platform->GetSocketErrorString()); return false; } return true; #endif } -auto Platform::GetTicks() const -> millisecs_t { - return GetCurrentMilliseconds() - starttime_; +auto CorePlatform::GetTicks() const -> millisecs_t { + return GetCurrentMillisecs() - start_time_millisecs_; } -auto Platform::GetPlatformName() -> std::string { +auto CorePlatform::GetPlatformName() -> std::string { throw Exception("UNIMPLEMENTED"); } -auto Platform::GetSubplatformName() -> std::string { +auto CorePlatform::GetSubplatformName() -> std::string { // This doesnt always have to be set. return ""; } -auto Platform::ContainsPythonDist() -> bool { return false; } - #pragma mark Stack Traces #if BA_ENABLE_EXECINFO_BACKTRACES @@ -1303,7 +1098,7 @@ class PlatformStackTraceExecInfo : public PlatformStackTrace { }; #endif -auto Platform::GetStackTrace() -> PlatformStackTrace* { +auto CorePlatform::GetStackTrace() -> PlatformStackTrace* { // Our default handler here supports execinfo backtraces where available // and gives nothing elsewhere. #if BA_ENABLE_EXECINFO_BACKTRACES @@ -1313,71 +1108,42 @@ auto Platform::GetStackTrace() -> PlatformStackTrace* { #endif } -void Platform::RequestPermission(Permission p) { +void CorePlatform::RequestPermission(Permission p) { // No-op. } -auto Platform::HavePermission(Permission p) -> bool { +auto CorePlatform::HavePermission(Permission p) -> bool { // Its assumed everything is accessible unless we override saying no. return true; } -#if !BA_OSTYPE_WINDOWS -static void HandleSIGINT(int s) { - if (g_logic) { - g_logic->PushInterruptSignalCall(); - } else { - Log(LogLevel::kError, "SigInt handler called before g_logic exists."); - } -} -#endif +void CorePlatform::SetDebugKey(const std::string& key, + const std::string& value) {} -void Platform::SetupInterruptHandling() { -// This default implementation covers non-windows platforms. -#if BA_OSTYPE_WINDOWS - throw Exception(); -#else - struct sigaction handler {}; - handler.sa_handler = HandleSIGINT; - sigemptyset(&handler.sa_mask); - handler.sa_flags = 0; - sigaction(SIGINT, &handler, nullptr); -#endif -} +void CorePlatform::HandleDebugLog(const std::string& msg) {} -void Platform::GetCursorPosition(float* x, float* y) { - assert(x && y); - - // By default, just use our latest event-delivered cursor position; - // this should work everywhere though perhaps might not be most optimal. - if (g_input == nullptr) { - *x = 0.0f; - *y = 0.0f; - return; - } - *x = g_input->cursor_pos_x(); - *y = g_input->cursor_pos_y(); -} -auto Platform::SetDebugKey(const std::string& key, const std::string& value) - -> void {} - -auto Platform::HandleDebugLog(const std::string& msg) -> void {} - -auto Platform::GetCurrentMilliseconds() -> millisecs_t { +auto CorePlatform::GetCurrentMillisecs() -> millisecs_t { return std::chrono::time_point_cast( std::chrono::steady_clock::now()) .time_since_epoch() .count(); } -auto Platform::GetCurrentSeconds() -> int64_t { +auto CorePlatform::GetCurrentMicrosecs() -> millisecs_t { + return std::chrono::time_point_cast( + std::chrono::steady_clock::now()) + .time_since_epoch() + .count(); +} + +auto CorePlatform::GetCurrentWholeSeconds() -> int64_t { return std::chrono::time_point_cast( std::chrono::steady_clock::now()) .time_since_epoch() .count(); } -auto Platform::ClipboardIsSupported() -> bool { +auto CorePlatform::ClipboardIsSupported() -> bool { // We only call our actual virtual function once. if (!have_clipboard_is_supported_) { clipboard_is_supported_ = DoClipboardIsSupported(); @@ -1386,7 +1152,7 @@ auto Platform::ClipboardIsSupported() -> bool { return clipboard_is_supported_; } -auto Platform::ClipboardHasText() -> bool { +auto CorePlatform::ClipboardHasText() -> bool { // If subplatform says they don't support clipboards, don't even ask. if (!ClipboardIsSupported()) { return false; @@ -1394,7 +1160,7 @@ auto Platform::ClipboardHasText() -> bool { return DoClipboardHasText(); } -auto Platform::ClipboardSetText(const std::string& text) -> void { +void CorePlatform::ClipboardSetText(const std::string& text) { // If subplatform says they don't support clipboards, this is an error. if (!ClipboardIsSupported()) { throw Exception("ClipboardSetText called with no clipboard support.", @@ -1403,7 +1169,7 @@ auto Platform::ClipboardSetText(const std::string& text) -> void { DoClipboardSetText(text); } -auto Platform::ClipboardGetText() -> std::string { +auto CorePlatform::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.", @@ -1412,7 +1178,7 @@ auto Platform::ClipboardGetText() -> std::string { return DoClipboardGetText(); } -auto Platform::DoClipboardIsSupported() -> bool { +auto CorePlatform::DoClipboardIsSupported() -> bool { // Go through SDL functionality on SDL based platforms; // otherwise default to no clipboard. #if BA_SDL2_BUILD && !BA_OSTYPE_IOS_TVOS @@ -1422,7 +1188,7 @@ auto Platform::DoClipboardIsSupported() -> bool { #endif } -auto Platform::DoClipboardHasText() -> bool { +auto CorePlatform::DoClipboardHasText() -> bool { // Go through SDL functionality on SDL based platforms; // otherwise default to no clipboard. #if BA_SDL2_BUILD && !BA_OSTYPE_IOS_TVOS @@ -1434,7 +1200,7 @@ auto Platform::DoClipboardHasText() -> bool { #endif } -auto Platform::DoClipboardSetText(const std::string& text) -> void { +void CorePlatform::DoClipboardSetText(const std::string& text) { // Go through SDL functionality on SDL based platforms; // otherwise default to no clipboard. #if BA_SDL2_BUILD && !BA_OSTYPE_IOS_TVOS @@ -1445,7 +1211,7 @@ auto Platform::DoClipboardSetText(const std::string& text) -> void { #endif } -auto Platform::DoClipboardGetText() -> std::string { +auto CorePlatform::DoClipboardGetText() -> std::string { // Go through SDL functionality on SDL based platforms; // otherwise default to no clipboard. #if BA_SDL2_BUILD && !BA_OSTYPE_IOS_TVOS @@ -1463,4 +1229,32 @@ auto Platform::DoClipboardGetText() -> std::string { #endif } -} // namespace ballistica +auto CorePlatform::System(const char* cmd) -> int { +#if BA_OSTYPE_IOS_TVOS + throw Exception("system() call is not supported on this OS."); +#else + return system(cmd); +#endif +} + +void CorePlatform::SetBaEnvVals(const PythonRef& ref) { + assert(!have_ba_env_vals_); + have_ba_env_vals_ = true; + + ba_env_config_dir_ = ref.GetAttr("config_dir").ValueAsString(); + ba_env_data_dir_ = ref.GetAttr("data_dir").ValueAsString(); + ba_env_app_python_dir_ = + ref.GetAttr("app_python_dir").ValueAsOptionalString(); + ba_env_user_python_dir_ = + ref.GetAttr("user_python_dir").ValueAsOptionalString(); + ba_env_site_python_dir_ = + ref.GetAttr("site_python_dir").ValueAsOptionalString(); + + // Ok, now look for the existence of ba_data in the dir we've got. + auto fullpath = ba_env_data_dir_ + BA_DIRSLASH + "ba_data"; + if (!FilePathExists(fullpath)) { + FatalError("ba_data directory not found at '" + fullpath + "'."); + } +} + +} // namespace ballistica::core diff --git a/src/ballistica/platform/platform.h b/src/ballistica/core/platform/core_platform.h similarity index 57% rename from src/ballistica/platform/platform.h rename to src/ballistica/core/platform/core_platform.h index 27b03ded..e5f3cc95 100644 --- a/src/ballistica/platform/platform.h +++ b/src/ballistica/core/platform/core_platform.h @@ -1,24 +1,25 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_PLATFORM_PLATFORM_H_ -#define BALLISTICA_PLATFORM_PLATFORM_H_ +#ifndef BALLISTICA_CORE_PLATFORM_CORE_PLATFORM_H_ +#define BALLISTICA_CORE_PLATFORM_CORE_PLATFORM_H_ #include #include +#include #include #include -#include "ballistica/ballistica.h" +#include "ballistica/shared/ballistica.h" -namespace ballistica { +namespace ballistica::core { -/// For capturing and printing stack-traces and related errors. -/// Platforms should subclass this and return instances in GetStackTrace(). -/// Stack trace classes should capture the stack state immediately upon +/// For capturing and printing stack-traces and related errors. Platforms +/// should subclass this and return instances in GetStackTrace(). Stack +/// trace classes should capture the stack state immediately upon /// construction but should do the bare minimum amount of work to store it. -/// Any expensive operations such as symbolification should be deferred until -/// GetDescription(). +/// Any expensive operations such as symbolification should be deferred +/// until GetDescription(). class PlatformStackTrace { public: virtual ~PlatformStackTrace() = default; @@ -32,46 +33,42 @@ class PlatformStackTrace { virtual auto copy() const noexcept -> PlatformStackTrace* = 0; }; -/// This class attempts to abstract away most platform-specific functionality. -/// Ideally we should need to pull in no platform-specific system headers -/// outside of the platform*.cc files and can just go through this. -class Platform { +/// This class attempts to abstract away most platform-specific +/// functionality. Ideally we should need to pull in no platform-specific +/// system headers outside of the platform*.cc files and can just go through +/// this. +class CorePlatform { public: - static auto Create() -> Platform*; - Platform(); - virtual ~Platform(); + /// Create the proper CorePlatform subclass for the current platform. + static auto Create() -> CorePlatform*; #pragma mark LIFECYCLE/SETTINGS ------------------------------------------------ - /// Called right after g_platform is created/assigned. Any platform - /// functionality depending on a complete g_platform object existing can - /// be run here. - virtual auto PostInit() -> void; + /// Called after our singleton has been instantiated. Any construction + /// functionality requiring virtual functions resolving to their final + /// class versions can go here. + virtual void PostInit(); - /// Create the proper App module and add it to the main_thread. - auto CreateAppFlavor() -> AppFlavor*; - - /// Create the appropriate Graphics subclass for the app. - auto CreateGraphics() -> Graphics*; - - virtual auto WillExitMain(bool errored) -> void; + virtual void WillExitMain(bool errored); /// Inform the platform that all subsystems are up and running and it can /// start talking to them. - virtual auto OnAppStart() -> void; + virtual void OnMainThreadStartApp(); - // Get/set values before standard game settings are available - // (for values needed before SDL init/etc). - // FIXME: We should have some sort of 'bootconfig.json' file for these. - // (or simply read the regular config in via c++ immediately) + virtual void OnAppStart(); + virtual void OnAppPause(); + virtual void OnAppResume(); + virtual void OnAppShutdown(); + virtual void ApplyAppConfig(); + virtual void OnScreenSizeChange(); + virtual void StepDisplayTime(); + + // Get/set values before standard game settings are available (for values + // needed before SDL init/etc). FIXME: We should have some sort of + // 'bootconfig.json' file for these. (or simply read the regular config in + // via c++ immediately) auto GetLowLevelConfigValue(const char* key, int default_value) -> int; - auto SetLowLevelConfigValue(const char* key, int value) -> void; - - /// Called when the app config is being read/applied. - virtual auto ApplyConfig() -> void; - - /// Called when the app should set itself up to intercept ctrl-c presses. - virtual auto SetupInterruptHandling() -> void; + void SetLowLevelConfigValue(const char* key, int value); #pragma mark FILES ------------------------------------------------------------- @@ -92,25 +89,25 @@ class Platform { /// Simple cross-platform check for existence of a file. auto FilePathExists(const std::string& name) -> bool; - /// Attempt to make a directory. Raise an Exception if unable, - /// unless quiet is true. Succeeds if the directory already exists. - auto MakeDir(const std::string& dir, bool quiet = false) -> void; + /// Attempt to make a directory. Raise an Exception if unable, unless + /// quiet is true. Succeeds if the directory already exists. + void MakeDir(const std::string& dir, bool quiet = false); /// Return the current working directory. virtual auto GetCWD() -> std::string; /// Unlink a file. - virtual auto Unlink(const char* path) -> void; + virtual void Unlink(const char* path); - /// Return the absolute path for the provided path. Note that this requires - /// the path to already exist. + /// Return the absolute path for the provided path. Note that this + /// requires the path to already exist. auto AbsPath(const std::string& path, std::string* outpath) -> bool; #pragma mark CLIPBOARD --------------------------------------------------------- - /// Return whether clipboard operations are supported at all. - /// This gets called when determining whether to display clipboard related - /// UI elements/etc. + /// 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. @@ -118,7 +115,7 @@ class Platform { /// Set current clipboard text. Raises an Exception if clipboard is /// unsupported. - auto ClipboardSetText(const std::string& text) -> void; + void ClipboardSetText(const std::string& text); /// Return current text from the clipboard. Raises an Exception if /// clipboard is unsupported or if there's no text on the clipboard. @@ -126,10 +123,10 @@ class Platform { #pragma mark PRINTING/LOGGING -------------------------------------------------- - /// Display a message to any default log for the platform (android log, etc.) - /// Note that this can be called from any thread. - virtual auto DisplayLog(const std::string& name, LogLevel level, - const std::string& msg) -> void; + /// Display a message to any default log for the platform (android log, + /// etc.) Note that this can be called from any thread. + virtual void DisplayLog(const std::string& name, LogLevel level, + const std::string& msg); #pragma mark ENVIRONMENT ------------------------------------------------------- @@ -139,40 +136,50 @@ class Platform { // Return a simple name for the subplatform: 'amazon', 'google', etc. virtual auto GetSubplatformName() -> std::string; - // Are we running in event-push-mode? - // With this on, we return from Main() and the system handles the event loop. - // With it off, we loop in Main() ourself. + // Are we running in event-push-mode? With this on, we return from Main() + // and the system handles the event loop. With it off, we loop in Main() + // ourself. virtual auto IsEventPushMode() -> bool; - /// Return the interface type based on the environment (phone, tablet, etc). + /// Return the interface type based on the environment (phone, tablet, + /// etc). virtual auto GetUIScale() -> UIScale; + /// Get the data directory. This dir contains ba_data and possibly other + /// platform-specific bits needed for the app to function. + auto GetDataDirectory() -> std::string; + + /// Return default DataDirectory value for monolithic builds. + auto GetDataDirectoryMonolithicDefault() -> std::string; + /// Get the root config directory. This dir contains the app config file /// and other data considered essential to the app install. This directory /// should be included in OS backups. auto GetConfigDirectory() -> std::string; + auto GetConfigDirectoryMonolithicDefault() -> std::optional; /// Get the path of the app config file. auto GetConfigFilePath() -> std::string; - /// Get a directory where the app can store internal generated data. - /// This directory should not be included in backups and the app - /// should remain functional if this directory is completely cleared - /// between runs (though it is expected that things stay intact here - /// *while* the app is running). - auto GetVolatileDataDirectory() -> std::string; - - /// Return a directory where the local user can manually place Python files - /// where they will be accessible by the app. When possible, this directory - /// should be in a place easily accessible to the user. - auto GetUserPythonDirectory() -> std::string; + /// Return a directory where the local user can manually place Python + /// files where they will be accessible by the app. When possible, this + /// directory should be in a place easily accessible to the user. + auto GetUserPythonDirectory() -> std::optional; + auto GetUserPythonDirectoryMonolithicDefault() -> std::optional; /// Return the directory where the app expects to find its bundled Python /// files. - auto GetAppPythonDirectory() -> std::string; + auto GetAppPythonDirectory() -> std::optional; /// Return the directory where bundled 3rd party Python files live. - auto GetSitePythonDirectory() -> std::string; + auto GetSitePythonDirectory() -> std::optional; + + /// Get a directory where the app can store internal generated data. This + /// directory should not be included in backups and the app should remain + /// functional if this directory is completely cleared between runs + /// (though it is expected that things stay intact here *while* the app is + /// running). + auto GetVolatileDataDirectory() -> std::string; /// Return the directory where game replay files live. auto GetReplaysDir() -> std::string; @@ -183,37 +190,27 @@ class Platform { virtual auto GetUserAgentString() -> std::string; virtual auto GetOSVersionString() -> std::string; - // Chdir to wherever our bundled data lives. - // (note to self: should rejigger this to avoid the chdir). - virtual auto SetupDataDirectory() -> void; - /// Set an environment variable as utf8, overwriting if it already exists. /// Raises an exception on errors. virtual void SetEnv(const std::string& name, const std::string& value); + virtual auto GetEnv(const std::string& name) -> std::optional; + /// Return hostname or other id suitable for displaying in network search /// results, etc. auto GetDeviceName() -> std::string; /// Get a UUID for use with things like device-accounts. This function /// should not be used for other purposes, should not be modified, and - /// eventually should go away after device accounts are phased out. - /// Also, this value should never be shared beyond the local device. + /// eventually should go away after device accounts are phased out. Also, + /// this value should never be shared beyond the local device. auto GetLegacyDeviceUUID() -> const std::string&; - /// Get a UUID for the current device that is meant to be publicly shared. - /// This value will change occasionally due to OS updates, app updates, or - /// other factors, so it can not be used as a permanent identifier, but it - /// should remain constant over short periods and should not be easily - /// changeable by the user, making it useful for purposes such as temporary - /// server bans or spam prevention. - auto GetPublicDeviceUUID() -> std::string; - /// Return values which can be hashed to create a public device uuid. - /// Ideally these values should come from an OS-provided guid. They - /// should not include anything that is easily user-changeable. - /// IMPORTANT: Only hashed/transformed versions of these values should - /// ever be shared beyond the local device. + /// Ideally these values should come from an OS-provided guid. They should + /// not include anything that is easily user-changeable. IMPORTANT: Only + /// hashed/transformed versions of these values should ever be shared + /// beyond the local device. virtual auto GetDeviceUUIDInputs() -> std::list; /// Return whether there is an actual legacy-device-uuid value for @@ -237,47 +234,31 @@ class Platform { // For enabling some special hardware optimizations for nvidia. auto is_tegra_k1() const -> bool { return is_tegra_k1_; } - auto set_is_tegra_k1(bool val) -> void { is_tegra_k1_ = val; } + void set_is_tegra_k1(bool val) { is_tegra_k1_ = val; } - /// Return whether this platform includes its own Python distribution - virtual auto ContainsPythonDist() -> bool; - -#pragma mark INPUT DEVICES ----------------------------------------------------- - - // Return a name for a ballistica keycode. - virtual auto GetKeyName(int keycode) -> std::string; - -#pragma mark IN APP PURCHASES -------------------------------------------------- - - auto Purchase(const std::string& item) -> void; - - // Restore purchases (currently only relevant on Apple platforms). - virtual auto RestorePurchases() -> void; - - // Purchase was ack'ed by the master-server (so can consume). - virtual auto PurchaseAck(const std::string& purchase, - const std::string& order_id) -> void; + /// Run system() command on OSs which support it. Throws exception + /// elsewhere. + static auto System(const char* cmd) -> int; #pragma mark ANDROID ----------------------------------------------------------- virtual auto GetAndroidExecArg() -> std::string; - virtual auto AndroidSetResString(const std::string& res) -> void; - virtual auto AndroidSynthesizeBackPress() -> void; - virtual auto AndroidQuitActivity() -> void; - virtual auto AndroidShowAppInvite(const std::string& title, + virtual void AndroidSetResString(const std::string& res); + virtual void AndroidSynthesizeBackPress(); + virtual void AndroidQuitActivity(); + virtual void AndroidShowAppInvite(const std::string& title, const std::string& message, - const std::string& code) -> void; - virtual auto AndroidShowWifiSettings() -> void; + const std::string& code); + virtual void AndroidShowWifiSettings(); virtual auto AndroidGetExternalFilesDir() -> std::string; #pragma mark PERMISSIONS ------------------------------------------------------- - /// Request the permission asynchronously. - /// If the permission cannot be requested (due to having been denied, etc) - /// then this may also present a message or pop-up instructing the user how - /// to manually grant the permission (up to individual platforms to - /// implement). - virtual auto RequestPermission(Permission p) -> void; + /// Request the permission asynchronously. If the permission cannot be + /// requested (due to having been denied, etc) then this may also present + /// a message or pop-up instructing the user how to manually grant the + /// permission (up to individual platforms to implement). + virtual void RequestPermission(Permission p); /// Returns true if this permission has been granted (or if asking is not /// required for it). @@ -285,36 +266,34 @@ class Platform { #pragma mark ANALYTICS --------------------------------------------------------- - virtual auto SetAnalyticsScreen(const std::string& screen) -> void; - virtual auto IncrementAnalyticsCount(const std::string& name, int increment) - -> void; - virtual auto IncrementAnalyticsCountRaw(const std::string& name, - int increment) -> void; - virtual auto IncrementAnalyticsCountRaw2(const std::string& name, - int uses_increment, int increment) - -> void; - virtual auto SubmitAnalyticsCounts() -> void; + virtual void SetAnalyticsScreen(const std::string& screen); + virtual void IncrementAnalyticsCount(const std::string& name, int increment); + virtual void IncrementAnalyticsCountRaw(const std::string& name, + int increment); + virtual void IncrementAnalyticsCountRaw2(const std::string& name, + int uses_increment, int increment); + virtual void SubmitAnalyticsCounts(); #pragma mark APPLE ------------------------------------------------------------- virtual auto NewAutoReleasePool() -> void*; - virtual auto DrainAutoReleasePool(void* pool) -> void; + virtual void DrainAutoReleasePool(void* pool); // FIXME: Can we consolidate these with the general music playback calls? - virtual auto MacMusicAppInit() -> void; + virtual void MacMusicAppInit(); virtual auto MacMusicAppGetVolume() -> int; - virtual auto MacMusicAppSetVolume(int volume) -> void; - virtual auto MacMusicAppGetLibrarySource() -> void; - virtual auto MacMusicAppStop() -> void; + virtual void MacMusicAppSetVolume(int volume); + virtual void MacMusicAppGetLibrarySource(); + virtual void MacMusicAppStop(); virtual auto MacMusicAppPlayPlaylist(const std::string& playlist) -> bool; virtual auto MacMusicAppGetPlaylists() -> std::list; #pragma mark TEXT RENDERING ---------------------------------------------------- - // Set bounds/width info for a bit of text. - // (will only be called in BA_ENABLE_OS_FONT_RENDERING is set) - virtual auto GetTextBoundsAndWidth(const std::string& text, Rect* r, - float* width) -> void; - virtual auto FreeTextTexture(void* tex) -> void; + // Set bounds/width info for a bit of text. (will only be called in + // BA_ENABLE_OS_FONT_RENDERING is set) + virtual void GetTextBoundsAndWidth(const std::string& text, Rect* r, + float* width); + virtual void FreeTextTexture(void* tex); virtual auto CreateTextTexture(int width, int height, const std::vector& strings, const std::vector& positions, @@ -324,11 +303,11 @@ class Platform { #pragma mark ACCOUNTS ---------------------------------------------------------- - virtual auto SignInV1(const std::string& account_type) -> void; - virtual auto SignOutV1() -> void; + virtual void SignInV1(const std::string& account_type); + virtual void SignOutV1(); - virtual auto GameCenterLogin() -> void; - virtual auto V1LoginDidChange() -> void; + virtual void GameCenterLogin(); + virtual void V1LoginDidChange(); /// Returns the ID to use for the device account. auto GetDeviceV1AccountID() -> std::string; @@ -336,26 +315,18 @@ class Platform { /// Return the prefix to use for device-account ids on this platform. virtual auto GetDeviceV1AccountUUIDPrefix() -> std::string; - /// Called when a Python LoginAdapter is requesting an explicit sign-in. - virtual auto LoginAdapterGetSignInToken(const std::string& login_type, - int attempt_id) -> void; - /// Called when a Python LoginAdapter is informing us that a back-end is - /// active/inactive. - virtual auto LoginAdapterBackEndActiveChange(const std::string& login_type, - bool active) -> void; - #pragma mark MUSIC PLAYBACK ---------------------------------------------------- // FIXME: currently these are wired up on Android; need to generalize // to support mac/itunes or other music player types. - virtual auto MusicPlayerPlay(PyObject* target) -> void; - virtual auto MusicPlayerStop() -> void; - virtual auto MusicPlayerShutdown() -> void; - virtual auto MusicPlayerSetVolume(float volume) -> void; + virtual void MusicPlayerPlay(PyObject* target); + virtual void MusicPlayerStop(); + virtual void MusicPlayerShutdown(); + virtual void MusicPlayerSetVolume(float volume); #pragma mark ADS --------------------------------------------------------------- - virtual auto ShowAd(const std::string& purpose) -> void; + virtual void ShowAd(const std::string& purpose); // Return whether we have the ability to show *any* ads. virtual auto GetHasAds() -> bool; @@ -372,20 +343,20 @@ class Platform { virtual auto ConvertIncomingLeaderboardScore( const std::string& leaderboard_id, int score) -> int; - virtual auto SubmitScore(const std::string& game, const std::string& version, - int64_t score) -> void; - virtual auto ReportAchievement(const std::string& achievement) -> void; + virtual void SubmitScore(const std::string& game, const std::string& version, + int64_t score); + virtual void ReportAchievement(const std::string& achievement); virtual auto HaveLeaderboard(const std::string& game, const std::string& config) -> bool; - virtual auto ShowOnlineScoreUI(const std::string& show, + virtual void ShowOnlineScoreUI(const std::string& show, const std::string& game, - const std::string& game_version) -> void; - virtual auto ResetAchievements() -> void; + const std::string& game_version); + virtual void ResetAchievements(); #pragma mark NETWORKING -------------------------------------------------------- - virtual auto CloseSocket(int socket) -> void; + virtual void CloseSocket(int socket); virtual auto GetBroadcastAddrs() -> std::vector; virtual auto SetSocketNonBlocking(int sd) -> bool; @@ -414,7 +385,7 @@ class Platform { /// Called on the main thread when a fatal error occurs. /// Will only be called if CanShowBlockingFatalErrorDialog() is true. - virtual auto BlockingFatalErrorDialog(const std::string& message) -> void; + virtual void BlockingFatalErrorDialog(const std::string& message); /// Use this instead of looking at errno (translates winsock errors to errno). virtual auto GetSocketError() -> int; @@ -427,80 +398,73 @@ class Platform { /// Set a key to be included in crash logs or other debug cases. /// This is expected to be lightweight as it may be called often. - virtual auto SetDebugKey(const std::string& key, const std::string& value) - -> void; + virtual void SetDebugKey(const std::string& key, const std::string& value); - /// Print a log message to be included in crash logs or other debug - /// mechanisms (example: Crashlytics). V1-cloud-log messages get forwarded - /// to here as well. It can be useful to call this directly to report extra - /// details that may help in debugging, as these calls are not considered - /// 'noteworthy' or presented to the user as standard Log() calls are. - virtual auto HandleDebugLog(const std::string& msg) -> void; - - static auto DebugLog(const std::string& msg) -> void { - if (g_platform) { - g_platform->HandleDebugLog(msg); - } - } + void DebugLog(const std::string& msg); #pragma mark MISC -------------------------------------------------------------- - // Return a monotonic time measurement in milliseconds since launch. - // To get a time value that is guaranteed to not jump around or go backwards, - // use ballistica::GetRealTime() (which is an abstraction around this). + /// Return a time measurement in milliseconds since launch. + /// It *should* be monotonic. + /// For most purposes, AppTime values are preferable since their progression + /// pauses during app suspension and they are 100% guaranteed to not go + /// backwards. auto GetTicks() const -> millisecs_t; - // A raw milliseconds value (not relative to launch time). - static auto GetCurrentMilliseconds() -> millisecs_t; - static auto GetCurrentSeconds() -> int64_t; + /// Return a raw current milliseconds value. It *should* be monotonic. + /// It is relative to an undefined start point; only use it for time + /// differences. Generally the AppTime values are preferable since their + /// progression pauses during app suspension and they are 100% guaranteed + /// to not go backwards. + static auto GetCurrentMillisecs() -> millisecs_t; - static auto SleepMS(millisecs_t ms) -> void; + /// Return a raw current microseconds value. It *should* be monotonic. + /// It is relative to an undefined start point; only use it for time + /// differences. Generally the AppTime values are preferable since their + /// progression pauses during app suspension and they are 100% guaranteed + /// to not go backwards. + static auto GetCurrentMicrosecs() -> microsecs_t; + + /// Return a raw current seconds integer value. It *should* be monotonic. + /// It is relative to an undefined start point; only use it for time + /// differences. Generally the AppTime values are preferable since their + /// progression pauses during app suspension and they are 100% guaranteed + /// to not go backwards. + static auto GetCurrentWholeSeconds() -> int64_t; + + static void SleepMillisecs(millisecs_t ms); /// Pop up a text edit dialog. - virtual auto EditText(const std::string& title, const std::string& value, - int max_chars) -> void; - - /// Open the provided URL in a browser or whatnot. - auto OpenURL(const std::string& url) -> void; + virtual void EditText(const std::string& title, const std::string& value, + int max_chars); /// Given a C++ symbol, attempt to return a pretty one. virtual auto DemangleCXXSymbol(const std::string& s) -> std::string; /// Called each time through the main event loop; /// for custom pumping/handling. - virtual auto RunEvents() -> void; - - /// Called when the app module is pausing. - /// Note: only app-thread (main thread) stuff should happen here. - /// (don't push calls to other threads/etc). - virtual auto OnAppPause() -> void; - - /// Called when the app module is resuming. - virtual auto OnAppResume() -> void; + virtual void RunEvents(); /// Is the OS currently playing music? (so we can avoid doing so). virtual auto IsOSPlayingMusic() -> bool; /// Pass platform-specific misc-read-vals along to the OS (as a json string). - virtual auto SetPlatformMiscReadVals(const std::string& vals) -> void; + virtual void SetPlatformMiscReadVals(const std::string& vals); /// Show/hide the hardware cursor. - virtual auto SetHardwareCursorVisible(bool visible) -> void; - - /// Get the most up-to-date cursor position. - virtual auto GetCursorPosition(float* x, float* y) -> void; + virtual void SetHardwareCursorVisible(bool visible); /// Quit the app (can be immediate or via posting some high level event). - virtual auto QuitApp() -> void; + virtual void QuitApp(); /// Open a file using the system default method (in another app, etc.) - virtual auto OpenFileExternally(const std::string& path) -> void; + virtual void OpenFileExternally(const std::string& path); /// Open a directory using the system default method (Finder, etc.) - virtual auto OpenDirExternally(const std::string& path) -> void; + virtual void OpenDirExternally(const std::string& path); /// Set the name of the current thread (for debugging). - virtual auto SetCurrentThreadName(const std::string& name) -> void; + virtual void SetCurrentThreadName(const std::string& name); // If display-resolution can be directly set on this platform, // return true and set the native full res here. Otherwise return false; @@ -513,16 +477,17 @@ class Platform { /// Are we being run from a terminal? (should we show prompts, etc?). auto is_stdin_a_terminal() const { return is_stdin_a_terminal_; } - protected: - /// Make a purchase. - virtual auto DoPurchase(const std::string& item) -> void; + void SetBaEnvVals(const PythonRef& ref); + /// Return true if baenv values have been locked in: python paths, log + /// handling, etc. Early-running code may wish to explicitly avoid making log + /// calls until this condition is met to ensure predictable behavior. + auto HaveBaEnvVals() const { return have_ba_env_vals_; } + + protected: /// Are we being run from a terminal? (should we show prompts, etc?). virtual auto GetIsStdinATerminal() -> bool; - /// Open the provided URL in a browser or whatnot. - virtual auto DoOpenURL(const std::string& url) -> void; - /// Called once per platform to determine touchscreen presence. virtual auto DoHasTouchScreen() -> bool; @@ -531,7 +496,7 @@ class Platform { /// Attempt to actually create a directory. /// Should *not* raise Exceptions if it already exists or if quiet is true. - virtual auto DoMakeDir(const std::string& dir, bool quiet) -> void; + virtual void DoMakeDir(const std::string& dir, bool quiet); /// Attempt to actually get an abs path. This will only be called if /// the path is valid and exists. @@ -539,12 +504,19 @@ class Platform { /// Calc the user scripts dir path for this platform. /// This will be called once and the path cached. - virtual auto DoGetUserPythonDirectory() -> std::string; + virtual auto DoGetUserPythonDirectoryMonolithicDefault() + -> std::optional; /// Return the default config directory for this platform. /// This will be used as the config dir if not overridden via command /// line options, etc. - virtual auto GetDefaultConfigDirectory() -> std::string; + virtual auto DoGetConfigDirectoryMonolithicDefault() + -> std::optional; + + /// Return the default data directory for this platform. + /// This will be used as the data dir if not overridden by core-config, etc. + /// This is the one monolithic-default value that is not optional. + virtual auto DoGetDataDirectoryMonolithicDefault() -> std::string; /// Return the default Volatile data dir for this platform. /// This will be used as the volatile-data-dir if not overridden via command @@ -556,34 +528,44 @@ class Platform { virtual auto DoClipboardIsSupported() -> bool; virtual auto DoClipboardHasText() -> bool; - virtual auto DoClipboardSetText(const std::string& text) -> void; + virtual void DoClipboardSetText(const std::string& text); virtual auto DoClipboardGetText() -> std::string; + /// Print a log message to be included in crash logs or other debug + /// mechanisms (example: Crashlytics). V1-cloud-log messages get forwarded + /// to here as well. It can be useful to call this directly to report extra + /// details that may help in debugging, as these calls are not considered + /// 'noteworthy' or presented to the user as standard Log() calls are. + virtual void HandleDebugLog(const std::string& msg); + + protected: + CorePlatform(); + virtual ~CorePlatform(); + private: bool is_stdin_a_terminal_{}; - bool using_custom_app_python_dir_{}; - bool have_config_dir_{}; + bool using_custom_app_python_dir_{}; // FIXME not wired up currently. bool have_has_touchscreen_value_{}; bool have_touchscreen_{}; bool is_tegra_k1_{}; bool have_clipboard_is_supported_{}; bool clipboard_is_supported_{}; - bool attempted_to_make_user_scripts_dir_{}; bool made_volatile_data_dir_{}; bool have_device_uuid_{}; bool ran_base_post_init_{}; - millisecs_t starttime_{}; + bool have_ba_env_vals_{}; + millisecs_t start_time_millisecs_{}; std::string device_name_; std::string legacy_device_uuid_; - std::string config_dir_; - std::string user_scripts_dir_; std::string volatile_data_dir_; - std::string app_python_dir_; - std::string site_python_dir_; std::string replays_dir_; - std::string public_device_uuid_; + std::string ba_env_config_dir_; + std::string ba_env_data_dir_; + std::optional ba_env_app_python_dir_; + std::optional ba_env_user_python_dir_; + std::optional ba_env_site_python_dir_; }; -} // namespace ballistica +} // namespace ballistica::core -#endif // BALLISTICA_PLATFORM_PLATFORM_H_ +#endif // BALLISTICA_CORE_PLATFORM_CORE_PLATFORM_H_ diff --git a/src/ballistica/platform/linux/platform_linux.cc b/src/ballistica/core/platform/linux/core_platform_linux.cc similarity index 66% rename from src/ballistica/platform/linux/platform_linux.cc rename to src/ballistica/core/platform/linux/core_platform_linux.cc index 58af4402..855503ac 100644 --- a/src/ballistica/platform/linux/platform_linux.cc +++ b/src/ballistica/core/platform/linux/core_platform_linux.cc @@ -1,17 +1,17 @@ // Released under the MIT License. See LICENSE for details. #if BA_OSTYPE_LINUX -#include "ballistica/platform/linux/platform_linux.h" +#include "ballistica/core/platform/linux/core_platform_linux.h" #include #include -namespace ballistica { +namespace ballistica::core { -PlatformLinux::PlatformLinux() {} +CorePlatformLinux::CorePlatformLinux() {} -std::string PlatformLinux::GenerateUUID() { +std::string CorePlatformLinux::GenerateUUID() { std::string val; char buffer[100]; FILE* fd_out = popen("cat /proc/sys/kernel/random/uuid", "r"); @@ -29,7 +29,7 @@ std::string PlatformLinux::GenerateUUID() { return val; } -auto PlatformLinux::GetDeviceUUIDInputs() -> std::list { +auto CorePlatformLinux::GetDeviceUUIDInputs() -> std::list { std::list out; // For now let's just go with machine-id. @@ -49,17 +49,9 @@ auto PlatformLinux::GetDeviceUUIDInputs() -> std::list { return out; }; -bool PlatformLinux::DoHasTouchScreen() { return false; } +bool CorePlatformLinux::DoHasTouchScreen() { return false; } -void PlatformLinux::DoOpenURL(const std::string& url) { - // hmmm is there a more universal option than this?... - int result = system((std::string("xdg-open \"") + url + "\"").c_str()); - if (result != 0) { - ScreenMessage("error on xdg-open"); - } -} - -void PlatformLinux::OpenFileExternally(const std::string& path) { +void CorePlatformLinux::OpenFileExternally(const std::string& path) { std::string cmd = std::string("xdg-open \"") + path + "\""; int result = system(cmd.c_str()); if (result != 0) { @@ -68,7 +60,7 @@ void PlatformLinux::OpenFileExternally(const std::string& path) { } } -void PlatformLinux::OpenDirExternally(const std::string& path) { +void CorePlatformLinux::OpenDirExternally(const std::string& path) { std::string cmd = std::string("xdg-open \"") + path + "\""; int result = system(cmd.c_str()); if (result != 0) { @@ -77,9 +69,9 @@ void PlatformLinux::OpenDirExternally(const std::string& path) { } } -std::string PlatformLinux::GetPlatformName() { return "linux"; } +std::string CorePlatformLinux::GetPlatformName() { return "linux"; } -std::string PlatformLinux::GetSubplatformName() { +std::string CorePlatformLinux::GetSubplatformName() { #if BA_TEST_BUILD return "test"; #else @@ -87,6 +79,6 @@ std::string PlatformLinux::GetSubplatformName() { #endif } -} // namespace ballistica +} // namespace ballistica::core #endif // BA_OSTYPE_LINUX diff --git a/src/ballistica/core/platform/linux/core_platform_linux.h b/src/ballistica/core/platform/linux/core_platform_linux.h new file mode 100644 index 00000000..2bdcbf97 --- /dev/null +++ b/src/ballistica/core/platform/linux/core_platform_linux.h @@ -0,0 +1,29 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CORE_PLATFORM_LINUX_CORE_PLATFORM_LINUX_H_ +#define BALLISTICA_CORE_PLATFORM_LINUX_CORE_PLATFORM_LINUX_H_ +#if BA_OSTYPE_LINUX + +#include + +#include "ballistica/core/platform/core_platform.h" + +namespace ballistica::core { + +class CorePlatformLinux : public CorePlatform { + public: + CorePlatformLinux(); + auto GetDeviceV1AccountUUIDPrefix() -> std::string override { return "l"; } + auto GenerateUUID() -> std::string override; + auto DoHasTouchScreen() -> bool override; + void OpenFileExternally(const std::string& path) override; + void OpenDirExternally(const std::string& path) override; + auto GetPlatformName() -> std::string override; + auto GetSubplatformName() -> std::string override; + auto GetDeviceUUIDInputs() -> std::list override; +}; + +} // namespace ballistica::core + +#endif // BA_OSTYPE_LINUX +#endif // BALLISTICA_CORE_PLATFORM_LINUX_CORE_PLATFORM_LINUX_H_ diff --git a/src/ballistica/platform/min_sdl.h b/src/ballistica/core/platform/support/min_sdl.h similarity index 99% rename from src/ballistica/platform/min_sdl.h rename to src/ballistica/core/platform/support/min_sdl.h index 8b6abfb3..3f0ff685 100644 --- a/src/ballistica/platform/min_sdl.h +++ b/src/ballistica/core/platform/support/min_sdl.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_PLATFORM_MIN_SDL_H_ -#define BALLISTICA_PLATFORM_MIN_SDL_H_ +#ifndef BALLISTICA_CORE_PLATFORM_SUPPORT_MIN_SDL_H_ +#define BALLISTICA_CORE_PLATFORM_SUPPORT_MIN_SDL_H_ // A bit of history: // @@ -789,4 +789,4 @@ typedef union SDL_Event { } SDL_Event; #endif // BA_SDL_BUILD -#endif // BALLISTICA_PLATFORM_MIN_SDL_H_ +#endif // BALLISTICA_CORE_PLATFORM_SUPPORT_MIN_SDL_H_ diff --git a/src/ballistica/platform/windows/platform_windows.cc b/src/ballistica/core/platform/windows/core_platform_windows.cc similarity index 77% rename from src/ballistica/platform/windows/platform_windows.cc rename to src/ballistica/core/platform/windows/core_platform_windows.cc index 81376aa1..0cfaa3e7 100644 --- a/src/ballistica/platform/windows/platform_windows.cc +++ b/src/ballistica/core/platform/windows/core_platform_windows.cc @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. #if BA_OSTYPE_WINDOWS -#include "ballistica/platform/windows/platform_windows.h" +#include "ballistica/core/platform/windows/core_platform_windows.h" #include #include @@ -12,15 +12,13 @@ #include #include -#include - #pragma comment(lib, "Rpcrt4.lib") #pragma comment(lib, "ws2_32.lib") #pragma comment(lib, "iphlpapi.lib") #if BA_DEBUG_BUILD -#pragma comment(lib, "python310_d.lib") +#pragma comment(lib, "python311_d.lib") #else -#pragma comment(lib, "python310.lib") +#pragma comment(lib, "python311.lib") #endif #if !BA_HEADLESS_BUILD @@ -32,18 +30,17 @@ #pragma comment(lib, "SDL2main.lib") #endif -#include "ballistica/logic/logic.h" -#include "ballistica/networking/networking_sys.h" -#include "ballistica/platform/min_sdl.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/networking/networking_sys.h" #if !defined(UNICODE) || !defined(_UNICODE) #error Unicode not defined. #endif -namespace ballistica { +namespace ballistica::core { // Convert a wide Unicode string to an UTF8 string. -static std::string utf8_encode(const std::wstring& wstr) { +auto CorePlatformWindows::UTF8Encode(const std::wstring& wstr) -> std::string { if (wstr.empty()) return std::string(); int size_needed = WideCharToMultiByte( CP_UTF8, 0, &wstr[0], static_cast(wstr.size()), NULL, 0, NULL, NULL); @@ -54,7 +51,7 @@ static std::string utf8_encode(const std::wstring& wstr) { } // Convert an UTF8 string to a wide Unicode String. -static std::wstring utf8_decode(const std::string& str) { +auto CorePlatformWindows::UTF8Decode(const std::string& str) -> std::wstring { if (str.empty()) return std::wstring(); int size_needed = MultiByteToWideChar(CP_UTF8, 0, &str[0], static_cast(str.size()), NULL, 0); @@ -64,7 +61,7 @@ static std::wstring utf8_decode(const std::string& str) { return wstr; } -PlatformWindows::PlatformWindows() { +CorePlatformWindows::CorePlatformWindows() { // We should be built in unicode mode. assert(sizeof(TCHAR) == 2); @@ -86,7 +83,7 @@ PlatformWindows::PlatformWindows() { // stick with just using the console subsystem mostly. // Specifically: // - Can only seem to get stdinput from the parent console if launched - // via start /wait BallisticaCoreXXX... + // via start /wait BallisticaKitXXX... // - Am seeing garbled stdout lines in some builds when run from // WSL (namely Release builds for whatever reason). if (AttachConsole(ATTACH_PARENT_PROCESS)) { @@ -105,29 +102,7 @@ PlatformWindows::PlatformWindows() { } } -BOOL WINAPI CtrlHandler(DWORD fdwCtrlType) { - switch (fdwCtrlType) { - case CTRL_C_EVENT: - if (g_logic) { - g_logic->PushInterruptSignalCall(); - } else { - Log(LogLevel::kError, "SigInt handler called before g_logic exists."); - } - return TRUE; - - default: - return FALSE; - } -} - -void PlatformWindows::SetupInterruptHandling() { - // Set up Ctrl-C handling. - if (!SetConsoleCtrlHandler(CtrlHandler, TRUE)) { - Log(LogLevel::kError, "Error on SetConsoleCtrlHandler()"); - } -} - -auto PlatformWindows::GetDeviceUUIDInputs() -> std::list { +auto CorePlatformWindows::GetDeviceUUIDInputs() -> std::list { std::list out; std::string ret; @@ -149,7 +124,7 @@ auto PlatformWindows::GetDeviceUUIDInputs() -> std::list { return out; } -std::string PlatformWindows::GenerateUUID() { +std::string CorePlatformWindows::GenerateUUID() { std::string val; UUID uuid; ZeroMemory(&uuid, sizeof(UUID)); @@ -166,18 +141,19 @@ std::string PlatformWindows::GenerateUUID() { return val; } -std::string PlatformWindows::GetDefaultConfigDirectory() { +auto CorePlatformWindows::DoGetConfigDirectoryMonolithicDefault() + -> std::optional { std::string config_dir; wchar_t* path; auto result = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &path); if (result != S_OK) { throw Exception("Unable to get user local-app-data dir."); } - std::string configdir = utf8_encode(std::wstring(path)) + "\\BallisticaCore"; + std::string configdir = UTF8Encode(std::wstring(path)) + "\\BallisticaKit"; return configdir; } -std::string PlatformWindows::GetErrnoString() { +std::string CorePlatformWindows::GetErrnoString() { switch (errno) { case EPERM: return "operation not permitted"; @@ -209,17 +185,13 @@ std::string PlatformWindows::GetErrnoString() { } } -std::string PlatformWindows::GetSocketErrorString() { +std::string CorePlatformWindows::GetSocketErrorString() { // on windows, socket errors are returned via WSAGetLastError, // (while they're just errno elsewhere..) return std::to_string(WSAGetLastError()); } -// auto PlatformWindows::FilePathExists(const std::string& name) -> bool { -// return std::filesystem::exists(utf8_decode(name)); -// } - -int PlatformWindows::GetSocketError() { +int CorePlatformWindows::GetSocketError() { int val = WSAGetLastError(); switch (val) { case WSAEINTR: @@ -231,43 +203,45 @@ int PlatformWindows::GetSocketError() { } } -auto PlatformWindows::Remove(const char* path) -> int { - return _wremove(utf8_decode(path).c_str()); +auto CorePlatformWindows::Remove(const char* path) -> int { + return _wremove(UTF8Decode(path).c_str()); } -auto PlatformWindows::Stat(const char* path, struct BA_STAT* buffer) -> int { - return _wstat(utf8_decode(path).c_str(), buffer); +auto CorePlatformWindows::Stat(const char* path, struct BA_STAT* buffer) + -> int { + return _wstat(UTF8Decode(path).c_str(), buffer); } -auto PlatformWindows::Rename(const char* oldname, const char* newname) -> int { +auto CorePlatformWindows::Rename(const char* oldname, const char* newname) + -> int { // Unlike other platforms, windows will error if the target file already // exists instead of simply overwriting it. So let's attempt to blow away // anything there first. - auto new_name_utf8 = utf8_decode(newname); + auto new_name_utf8 = UTF8Decode(newname); _wremove(new_name_utf8.c_str()); - return _wrename(utf8_decode(oldname).c_str(), new_name_utf8.c_str()); + return _wrename(UTF8Decode(oldname).c_str(), new_name_utf8.c_str()); } -auto PlatformWindows::DoAbsPath(const std::string& path, std::string* outpath) - -> bool { +auto CorePlatformWindows::DoAbsPath(const std::string& path, + std::string* outpath) -> bool { wchar_t abspath[MAX_PATH + 1]; - auto path_utf8 = utf8_decode(path); + auto path_utf8 = UTF8Decode(path); uint32_t pathlen = GetFullPathNameW(path_utf8.c_str(), MAX_PATH, abspath, nullptr); if (pathlen >= MAX_PATH) { // Buffer not big enough. Should handle this case. return false; } - *outpath = utf8_encode(std::wstring(abspath)); + *outpath = UTF8Encode(std::wstring(abspath)); return true; } -auto PlatformWindows::FOpen(const char* path, const char* mode) -> FILE* { - return _wfopen(utf8_decode(path).c_str(), utf8_decode(mode).c_str()); +auto CorePlatformWindows::FOpen(const char* path, const char* mode) -> FILE* { + return _wfopen(UTF8Decode(path).c_str(), UTF8Decode(mode).c_str()); } -void PlatformWindows::DoMakeDir(const std::string& dir, bool quiet) { - std::wstring stemp = utf8_decode(dir); +void CorePlatformWindows::DoMakeDir(const std::string& dir, bool quiet) { + std::wstring stemp = UTF8Decode(dir); int result = CreateDirectory(stemp.c_str(), 0); if (result == 0) { DWORD err = GetLastError(); @@ -277,7 +251,7 @@ void PlatformWindows::DoMakeDir(const std::string& dir, bool quiet) { } } -std::string PlatformWindows::GetLocale() { +std::string CorePlatformWindows::GetLocale() { // Get the windows locale. // (see http://msdn.microsoft.com/en-us/goglobal/bb895996.aspx) // theres a func to convert this to a string but its not available on xp @@ -701,41 +675,42 @@ std::string PlatformWindows::GetLocale() { } } -std::string PlatformWindows::DoGetDeviceName() { +std::string CorePlatformWindows::DoGetDeviceName() { std::string device_name; wchar_t computer_name[256]; DWORD computer_name_size = 256; int result = GetComputerName(computer_name, &computer_name_size); if (result == 0) { - device_name = "BallisticaCore Game"; + device_name = "BallisticaKit Game"; } else { - device_name = utf8_encode(std::wstring(computer_name)); + device_name = UTF8Encode(std::wstring(computer_name)); if (device_name.size() == 0) { - device_name = "BallisticaCore Game"; + device_name = "BallisticaKit Game"; } } return device_name; } -bool PlatformWindows::DoHasTouchScreen() { return false; } +bool CorePlatformWindows::DoHasTouchScreen() { return false; } -void PlatformWindows::DisplayLog(const std::string& name, LogLevel level, - const std::string& msg) { +void CorePlatformWindows::DisplayLog(const std::string& name, LogLevel level, + const std::string& msg) { // if (have_stdin_stdout_) { // // On headless builds we use default handler (simple stdout). - // return Platform::DisplayLog(msg); + // return CorePlatform::DisplayLog(msg); // } // Also spit this out as a debug-string for when running from msvc. - OutputDebugString(utf8_decode(msg).c_str()); + OutputDebugString(UTF8Decode(msg).c_str()); } // (The default SDL handler now covers us) -// bool PlatformWindows::BlockingFatalErrorDialog(const std::string& message) { +// bool CorePlatformWindows::BlockingFatalErrorDialog(const std::string& +// message) { // if (HeadlessMode()) { -// return Platform::BlockingFatalErrorDialog(message); +// return CorePlatform::BlockingFatalErrorDialog(message); // } -// MessageBoxA(nullptr, (message.c_str()), "BallisticaCore", +// MessageBoxA(nullptr, (message.c_str()), "BallisticaKit", // MB_ICONERROR | MB_OK); // // Our message-box call is blocking so we can return false here @@ -743,49 +718,83 @@ void PlatformWindows::DisplayLog(const std::string& name, LogLevel level, // return false; // } -void PlatformWindows::SetupDataDirectory() { - // We always want to launch with the working directory where our executable - // is, but for some reason that's not the default when visual studio - // debugging. (and overriding it is a per-user setting; ew). ...so - // let's force the issue: grab the path to our executable, lop it off - // at the last \, and chdir to that. - { - wchar_t sz_file_name[MAX_PATH + 1]; - GetModuleFileName(nullptr, sz_file_name, MAX_PATH + 1); - wchar_t* last_slash = nullptr; - for (wchar_t* s = sz_file_name; *s != 0; ++s) { - if (*s == '\\') { - last_slash = s; - } - } - if (last_slash != nullptr) { - *last_slash = 0; - int result = _wchdir(sz_file_name); - if (result != 0) { - throw Exception("Unable to chdir to application directory."); - } +auto CorePlatformWindows::DoGetDataDirectoryMonolithicDefault() -> std::string { + wchar_t sz_file_name[MAX_PATH + 1]; + GetModuleFileName(nullptr, sz_file_name, MAX_PATH + 1); + wchar_t* last_slash = nullptr; + for (wchar_t* s = sz_file_name; *s != 0; ++s) { + if (*s == '\\') { + last_slash = s; } } + if (last_slash != nullptr) { + *last_slash = 0; - // Simply complain if ba_data isn't here. - if (!std::filesystem::is_directory("ba_data")) { - throw Exception("ba_data directory not found."); + // If the app path happens to be the current dir, return + // the default of "." which gives us cleaner looking paths in + // stack traces/etc. + auto out = UTF8Encode(std::wstring(sz_file_name)); + if (out == GetCWD()) { + return CorePlatform::DoGetDataDirectoryMonolithicDefault(); + } + return out; + } else { + FatalError("Unable to deduce application path."); + return CorePlatform::DoGetDataDirectoryMonolithicDefault(); } } -void PlatformWindows::SetEnv(const std::string& name, - const std::string& value) { - auto result = SetEnvironmentVariableW(utf8_decode(name).c_str(), - utf8_decode(value).c_str()); +auto CorePlatformWindows::GetEnv(const std::string& name) + -> std::optional { + // Start with a small static buffer for a quick-out. Most stuff we're + // fetching should fit in this. + const int kStaticBufferSize{256}; + wchar_t buffer[kStaticBufferSize]; + auto result = GetEnvironmentVariableW(UTF8Decode(name).c_str(), buffer, + kStaticBufferSize); + + // 0 means var wasn't found. This seems like it would clash with zero-length + // var values that *are* found, but apparently those can't exist on windows? + // (empty value deletes a var). + if (result == 0) { + return {}; + } + + // If it was found and fits in our small static buffer, we're done. + if (result <= kStaticBufferSize) { + return UTF8Encode(std::wstring(buffer)); + } + + // Ok; apparently its big. Allocate a buffer big enough to hold it and try + // again. + std::vector big_buffer(result); + assert(big_buffer.size() == result); + result = GetEnvironmentVariableW(UTF8Decode(name).c_str(), big_buffer.data(), + big_buffer.size()); + + // This should always succeed at this point; make noise if not. + if (result == 0 || result > big_buffer.size()) { + Log(LogLevel::kError, "GetEnv to allocated buffer failed; unexpected."); + return {}; + } + return UTF8Encode(std::wstring(big_buffer.data())); +} + +void CorePlatformWindows::SetEnv(const std::string& name, + const std::string& value) { + auto result = SetEnvironmentVariableW(UTF8Decode(name).c_str(), + UTF8Decode(value).c_str()); if (result == 0) { throw Exception("SetEnvironmentVariable failed for '" + name + "'; error=" + std::to_string(GetLastError())); } } -bool PlatformWindows::GetIsStdinATerminal() { return _isatty(_fileno(stdin)); } +bool CorePlatformWindows::GetIsStdinATerminal() { + return _isatty(_fileno(stdin)); +} -std::string PlatformWindows::GetOSVersionString() { +std::string CorePlatformWindows::GetOSVersionString() { DWORD dw_version = 0; DWORD dw_major_version = 0; DWORD dw_minor_version = 0; @@ -807,52 +816,40 @@ std::string PlatformWindows::GetOSVersionString() { return version; } -std::string PlatformWindows::GetCWD() { +std::string CorePlatformWindows::GetCWD() { wchar_t buffer[MAX_PATH]; wchar_t* result = _wgetcwd(buffer, MAX_PATH); if (result == nullptr) { throw Exception("Error getting CWD; errno=" + std::to_string(errno)); } - return utf8_encode(std::wstring(buffer)); + return UTF8Encode(std::wstring(buffer)); } -void PlatformWindows::DoOpenURL(const std::string& url) { - auto r = reinterpret_cast( - ShellExecute(nullptr, _T("open"), utf8_decode(url).c_str(), nullptr, - nullptr, SW_SHOWNORMAL)); - - // This should return > 32 on success. - if (r <= 32) { - Log(LogLevel::kError, - "Error " + std::to_string(r) + " opening URL '" + url + "'"); - } -} - -void PlatformWindows::OpenFileExternally(const std::string& path) { +void CorePlatformWindows::OpenFileExternally(const std::string& path) { auto r = reinterpret_cast( ShellExecute(nullptr, _T("open"), _T("notepad.exe"), - utf8_decode(path).c_str(), nullptr, SW_SHOWNORMAL)); + UTF8Decode(path).c_str(), nullptr, SW_SHOWNORMAL)); if (r <= 32) { Log(LogLevel::kError, "Error " + std::to_string(r) + " on open_file_externally for '" + path + "'"); } } -void PlatformWindows::OpenDirExternally(const std::string& path) { +void CorePlatformWindows::OpenDirExternally(const std::string& path) { auto r = reinterpret_cast( ShellExecute(nullptr, _T("open"), _T("explorer.exe"), - utf8_decode(path).c_str(), nullptr, SW_SHOWNORMAL)); + UTF8Decode(path).c_str(), nullptr, SW_SHOWNORMAL)); if (r <= 32) { Log(LogLevel::kError, "Error " + std::to_string(r) + " on open_dir_externally for '" + path + "'"); } } -void PlatformWindows::Unlink(const char* path) { _unlink(path); } +void CorePlatformWindows::Unlink(const char* path) { _unlink(path); } -void PlatformWindows::CloseSocket(int socket) { closesocket(socket); } +void CorePlatformWindows::CloseSocket(int socket) { closesocket(socket); } -std::vector PlatformWindows::GetBroadcastAddrs() { +std::vector CorePlatformWindows::GetBroadcastAddrs() { #define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x)) #define FREE(x) HeapFree(GetProcessHeap(), 0, (x)) std::vector addrs; @@ -914,20 +911,20 @@ std::vector PlatformWindows::GetBroadcastAddrs() { #undef FREE } -bool PlatformWindows::SetSocketNonBlocking(int sd) { +bool CorePlatformWindows::SetSocketNonBlocking(int sd) { unsigned long dataval = 1; // NOLINT (func signature wants long) int result = ioctlsocket(sd, FIONBIO, &dataval); if (result != 0) { Log(LogLevel::kError, "Error setting non-blocking socket: " - + g_platform->GetSocketErrorString()); + + g_core->platform->GetSocketErrorString()); return false; } return true; } -std::string PlatformWindows::GetPlatformName() { return "windows"; } +std::string CorePlatformWindows::GetPlatformName() { return "windows"; } -std::string PlatformWindows::GetSubplatformName() { +std::string CorePlatformWindows::GetSubplatformName() { #if BA_TEST_BUILD return "test"; #else @@ -935,8 +932,6 @@ std::string PlatformWindows::GetSubplatformName() { #endif } -bool PlatformWindows::ContainsPythonDist() { return true; } - -} // namespace ballistica +} // namespace ballistica::core #endif // BA_OSTYPE_WINDOWS diff --git a/src/ballistica/platform/windows/platform_windows.h b/src/ballistica/core/platform/windows/core_platform_windows.h similarity index 70% rename from src/ballistica/platform/windows/platform_windows.h rename to src/ballistica/core/platform/windows/core_platform_windows.h index 7857aca8..ac14b3ff 100644 --- a/src/ballistica/platform/windows/platform_windows.h +++ b/src/ballistica/core/platform/windows/core_platform_windows.h @@ -1,24 +1,29 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_PLATFORM_WINDOWS_PLATFORM_WINDOWS_H_ -#define BALLISTICA_PLATFORM_WINDOWS_PLATFORM_WINDOWS_H_ +#ifndef BALLISTICA_CORE_PLATFORM_WINDOWS_CORE_PLATFORM_WINDOWS_H_ +#define BALLISTICA_CORE_PLATFORM_WINDOWS_CORE_PLATFORM_WINDOWS_H_ #if BA_OSTYPE_WINDOWS #include #include -#include "ballistica/platform/platform.h" +#include "ballistica/core/platform/core_platform.h" -namespace ballistica { +namespace ballistica::core { -class PlatformWindows : public Platform { +class CorePlatformWindows : public CorePlatform { public: - PlatformWindows(); - void SetupInterruptHandling() override; + CorePlatformWindows(); + + static auto UTF8Encode(const std::wstring& wstr) -> std::string; + static auto UTF8Decode(const std::string& str) -> std::wstring; + auto GetDeviceV1AccountUUIDPrefix() -> std::string override { return "w"; } auto GetDeviceUUIDInputs() -> std::list override; auto GenerateUUID() -> std::string override; - auto GetDefaultConfigDirectory() -> std::string override; + auto DoGetConfigDirectoryMonolithicDefault() + -> std::optional override; + auto DoGetDataDirectoryMonolithicDefault() -> std::string override; auto Remove(const char* path) -> int; auto Stat(const char* path, struct BA_STAT* buffer) -> int; auto Rename(const char* oldname, const char* newname) -> int; @@ -34,12 +39,11 @@ class PlatformWindows : public Platform { auto DoHasTouchScreen() -> bool override; void DisplayLog(const std::string& name, LogLevel level, const std::string& msg) override; - void SetupDataDirectory() override; void SetEnv(const std::string& name, const std::string& value) override; + auto GetEnv(const std::string& name) -> std::optional override; auto GetIsStdinATerminal() -> bool override; auto GetOSVersionString() -> std::string override; auto GetCWD() -> std::string override; - void DoOpenURL(const std::string& url) override; void OpenFileExternally(const std::string& path) override; void OpenDirExternally(const std::string& path) override; void Unlink(const char* path) override; @@ -48,11 +52,10 @@ class PlatformWindows : public Platform { auto SetSocketNonBlocking(int sd) -> bool override; auto GetPlatformName() -> std::string override; auto GetSubplatformName() -> std::string override; - virtual auto ContainsPythonDist() -> bool; bool have_stdin_stdout_ = false; }; -} // namespace ballistica +} // namespace ballistica::core #endif // BA_OSTYPE_WINDOWS -#endif // BALLISTICA_PLATFORM_WINDOWS_PLATFORM_WINDOWS_H_ +#endif // BALLISTICA_CORE_PLATFORM_WINDOWS_CORE_PLATFORM_WINDOWS_H_ diff --git a/src/ballistica/core/python/core_python.cc b/src/ballistica/core/python/core_python.cc new file mode 100644 index 00000000..cf455aea --- /dev/null +++ b/src/ballistica/core/python/core_python.cc @@ -0,0 +1,355 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/core/python/core_python.h" + +#include "ballistica/core/mgen/python_modules_monolithic.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/core/support/base_soft.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_command.h" + +namespace ballistica::core { + +void CorePython::ApplyBaEnvConfig() { + // Fetch the env-config (creates it if need be). + auto envcfg = objs().Get(core::CorePython::ObjID::kBaEnvGetConfigCall).Call(); + BA_PRECONDITION(envcfg.Exists()); + g_core->platform->SetBaEnvVals(envcfg); +} + +void CorePython::RunBaEnvOnBaBaseImport() { + // Runs at the end of our import; just for checks/etc. + auto result = + objs().Get(core::CorePython::ObjID::kBaEnvOnBaBaseImportCall).Call(); + if (!result.Exists()) { + FatalError("baenv.on_babase_import() failed."); + } +} + +void CorePython::InitPython() { + assert(g_core->InMainThread()); + assert(g_buildconfig.monolithic_build()); + + // Flip on some extra runtime debugging options in debug builds. + // https://docs.python.org/3/library/devmode.html#devmode + int dev_mode{g_buildconfig.debug_build()}; + + // Pre-config as isolated if we include our own Python and as standard + // otherwise. + PyPreConfig preconfig; + if (g_buildconfig.contains_python_dist()) { + PyPreConfig_InitIsolatedConfig(&preconfig); + } else { + PyPreConfig_InitPythonConfig(&preconfig); + } + preconfig.dev_mode = dev_mode; + + // We want consistent utf-8 everywhere (Python used to default to + // windows-specific file encodings, etc.) + preconfig.utf8_mode = 1; + + PyStatus status = Py_PreInitialize(&preconfig); + BA_PRECONDITION(!PyStatus_Exception(status)); + + // Configure as isolated if we include our own Python and as standard + // otherwise. + PyConfig config; + if (g_buildconfig.contains_python_dist()) { + PyConfig_InitIsolatedConfig(&config); + // We manage paths 100% ourself in this case and don't want any site + // stuff (neither site nor user-site). + config.site_import = 0; + } else { + PyConfig_InitPythonConfig(&config); + } + config.dev_mode = dev_mode; + if (!g_buildconfig.debug_build()) { + config.optimization_level = 1; + } + + // In cases where we bundle Python, set up all paths explicitly. + // https://docs.python.org/3/c-api/init_config.html#path-configuration + if (g_buildconfig.contains_python_dist()) { + PyConfig_SetBytesString(&config, &config.base_exec_prefix, ""); + PyConfig_SetBytesString(&config, &config.base_executable, ""); + PyConfig_SetBytesString(&config, &config.base_prefix, ""); + PyConfig_SetBytesString(&config, &config.exec_prefix, ""); + PyConfig_SetBytesString(&config, &config.executable, ""); + PyConfig_SetBytesString(&config, &config.prefix, ""); + + // Note: we're using utf-8 mode above so Py_DecodeLocale will convert + // from utf-8. + + // Interesting note: it seems we can pass relative paths here but + // they wind up in sys.path as absolute paths (unlike entries we add + // to sys.path *after* things are up and running). + // Though nowadays we want to use abs paths anyway to avoid doing chdir + // so its a moot point. + if (g_buildconfig.ostype_windows()) { + // Windows Python looks for Lib and DLLs dirs by default, along with + // some others, but we want to be more explicit in limiting to these. It + // also seems that windows Python's paths can be incorrect if we're in + // strange dirs such as \\wsl$\Ubuntu-18.04\ that we get with WSL build + // setups. + + // NOTE: Python for windows actually comes with 'Lib', not 'lib', but + // it seems the interpreter defaults point to ./lib (as of 3.8.5). + // Normally this doesn't matter since windows is case-insensitive but + // under WSL it does. + // So we currently bundle the dir as 'lib' and use that in our path so + // that everything is happy (both with us and with python.exe). + PyWideStringList_Append(&config.module_search_paths, + Py_DecodeLocale("lib", nullptr)); + PyWideStringList_Append(&config.module_search_paths, + Py_DecodeLocale("DLLs", nullptr)); + } else { + auto pylibpath = g_core->platform->GetDataDirectoryMonolithicDefault() + + BA_DIRSLASH + "pylib"; + PyWideStringList_Append(&config.module_search_paths, + Py_DecodeLocale(pylibpath.c_str(), nullptr)); + } + config.module_search_paths_set = 1; + } + + // In monolithic builds, let Python know how to import all our built in + // modules. + if (g_buildconfig.monolithic_build()) { + MonolithicRegisterPythonModules(); + } else { + FatalError("FIXME UNIMPLEMENTED"); + } + + // Init Python. + status = Py_InitializeFromConfig(&config); + BA_PRECONDITION_FATAL(!PyStatus_Exception(status)); +} + +void CorePython::EnablePythonLoggingCalls() { + if (python_logging_calls_enabled_) { + return; + } + auto gil{Python::ScopedInterpreterLock()}; + + assert(objs().Exists(ObjID::kLoggingDebugCall)); + assert(objs().Exists(ObjID::kLoggingInfoCall)); + assert(objs().Exists(ObjID::kLoggingWarningCall)); + assert(objs().Exists(ObjID::kLoggingErrorCall)); + assert(objs().Exists(ObjID::kLoggingCriticalCall)); + + // Now that we've grabbed Python logging calls above, we can push any + // early log calls along to Python. They're no longer our problem. + { + std::scoped_lock lock(early_log_lock_); + python_logging_calls_enabled_ = true; + for (auto&& entry : early_logs_) { + LoggingCall(entry.first, "[HELD] " + entry.second); + } + early_logs_.clear(); + } +} + +void CorePython::ImportPythonObjs() { + // Define a few calls we can use for environment setup. + + // Grab core stuff that we might need before our _babase module gets spun up + // (which is when all the stuff in binding_base gets fetched). + // Note: Binding .inc files expect 'ObjID' and 'objs_' to be defined. +#include "ballistica/core/mgen/pyembed/binding_core.inc" + + // Normally we wait until babase is imported to push early logs through to + // Python (so that our log handling is fully bootstrapped), but technically + // we can push things out to Python any time now since we grabbed the logging + // calls above. Do so if asked. + if (!g_core->core_config().hold_early_logs) { + EnablePythonLoggingCalls(); + } + + { +#include "ballistica/core/mgen/pyembed/env.inc" + auto ctx = PythonRef(PyDict_New(), PythonRef::kSteal); + if (!PythonCommand(env_code, "bameta/pyembed/env.py") + .Exec(true, *ctx, *ctx)) { + FatalError("Error in ba Python env code. See log for details."); + } + objs_.StoreCallable(ObjID::kPrependSysPathCall, + *ctx.DictGetItem("prepend_sys_path")); + objs_.StoreCallable(ObjID::kBaEnvConfigureCall, + *ctx.DictGetItem("import_baenv_and_run_configure")); + objs_.StoreCallable(ObjID::kBaEnvGetConfigCall, + *ctx.DictGetItem("get_env_config")); + objs_.StoreCallable(ObjID::kBaEnvOnBaBaseImportCall, + *ctx.DictGetItem("on_babase_import")); + objs_.StoreCallable(ObjID::kBaEnvOnBaBaseStartAppCall, + *ctx.DictGetItem("on_babase_start_app")); + } +} + +void CorePython::ReleaseMainThreadGIL() { + assert(g_core->InMainThread()); + // After we bootstrap Python here in the main thread we release the GIL. + // We'll explicitly reacquire it anytime we need it (mainly in the logic + // thread once that comes up later). + PyEval_SaveThread(); +} + +void CorePython::SoftImportBase() { + auto gil{Python::ScopedInterpreterLock()}; + auto result = PythonRef::Stolen(PyImport_ImportModule("_babase")); + if (!result.Exists()) { + // Ignore any errors here for now. All that will matter is whether base + // gave us its interface. + PyErr_Clear(); + } +} + +void CorePython::VerifyPythonEnvironment() { + // Make sure we're running the Python version we require. + const char* ver = Py_GetVersion(); + if (strncmp(ver, "3.11", 4) != 0) { + FatalError("We require Python 3.11.x; instead found " + std::string(ver)); + } +} + +void CorePython::MonolithicModeBaEnvConfigure() { + assert(g_buildconfig.monolithic_build()); + assert(g_core); + g_core->BootLog("baenv.configure() begin"); + + auto gil{Python::ScopedInterpreterLock()}; + + // To start with, stuff a single path into python-paths which should + // allow us to find our baenv module which will do the rest of the work + // (adding our full set of paths, etc). + // data-dir is the one monolithic-default value that MUST be defined + // so we base it on this. + auto default_py_dir = std::string("ba_data") + BA_DIRSLASH + "python"; + auto data_dir_mono_default = + g_core->platform->GetDataDirectoryMonolithicDefault(); + // Keep path clean if data-dir val is ".". + if (data_dir_mono_default != ".") { + default_py_dir = data_dir_mono_default + BA_DIRSLASH + default_py_dir; + } + auto args = PythonRef::Stolen(Py_BuildValue("(s)", default_py_dir.c_str())); + objs().Get(ObjID::kPrependSysPathCall).Call(args); + + // Import and run baenv.configure() using our 'monolithic' values for all + // paths. + std::optional config_dir = + g_core->platform->GetConfigDirectoryMonolithicDefault(); + std::optional data_dir = + g_core->platform->GetDataDirectoryMonolithicDefault(); + std::optional user_python_dir = + g_core->platform->GetUserPythonDirectoryMonolithicDefault(); + auto kwargs = + // clang-format off + PythonRef::Stolen(Py_BuildValue( + "{" + "sO" // config_dir + "sO" // data_dir + "sO" // user_python_dir + "sO" // contains_python_dist + "}", + "config_dir", + config_dir ? *PythonRef::FromString(*config_dir) : Py_None, + "data_dir", + data_dir ? *PythonRef::FromString(*data_dir) : Py_None, + "user_python_dir", + user_python_dir ? *PythonRef::FromString(*user_python_dir) : Py_None, + "contains_python_dist", + g_buildconfig.contains_python_dist() ? Py_True : Py_False)); + // clang-format on + auto result = objs() + .Get(ObjID::kBaEnvConfigureCall) + .Call(objs().Get(ObjID::kEmptyTuple), kwargs); + if (!result.Exists()) { + FatalError( + "Environment setup failed.\n" + "This usually means you are running the app from the wrong location.\n" + "See log for details."); + } + g_core->BootLog("baenv.configure() end"); +} + +void CorePython::LoggingCall(LogLevel loglevel, const std::string& msg) { + // If we're not yet sending logs to Python, store this one away until we are. + if (!python_logging_calls_enabled_) { + std::scoped_lock lock(early_log_lock_); + early_logs_.emplace_back(loglevel, msg); + + // UPDATE - trying to disable this for now to make the concept of delayed + // logs a bit less scary. Perhaps we can update fatal-error to dump these or + // have a mode to immediate-print them as needed. + if (explicit_bool(false)) { + // There's a chance that we're going down in flames and this log + // might be useful to see even if we never get a chance to chip it to + // Python. So let's make an attempt to get it at least seen now in + // whatever way we can. (platform display-log call and stderr). + const char* errmsg{ + "CorePython::LoggingCall() called before Python" + " logging available."}; + if (g_core->platform) { + g_core->platform->DisplayLog("root", LogLevel::kError, errmsg); + g_core->platform->DisplayLog("root", loglevel, msg); + } + fprintf(stderr, "%s\n%s\n", errmsg, msg.c_str()); + } + return; + } + + // Ok; seems we've got Python calls. Run the right one for our log level. + ObjID logcallobj; + switch (loglevel) { + case LogLevel::kDebug: + logcallobj = ObjID::kLoggingDebugCall; + break; + case LogLevel::kInfo: + logcallobj = ObjID::kLoggingInfoCall; + break; + case LogLevel::kWarning: + logcallobj = ObjID::kLoggingWarningCall; + break; + case LogLevel::kError: + logcallobj = ObjID::kLoggingErrorCall; + break; + case LogLevel::kCritical: + logcallobj = ObjID::kLoggingCriticalCall; + break; + default: + logcallobj = ObjID::kLoggingInfoCall; + fprintf(stderr, "Unexpected LogLevel %d\n", static_cast(loglevel)); + break; + } + + // Make sure we're good to go from any thread. + Python::ScopedInterpreterLock lock; + + PythonRef args(Py_BuildValue("(s)", msg.c_str()), PythonRef::kSteal); + objs().Get(logcallobj).Call(args); +} + +void CorePython::AcquireGIL() { + assert(g_base_soft && g_base_soft->InLogicThread()); + auto debug_timing{g_core->debug_timing}; + millisecs_t startms{debug_timing ? CorePlatform::GetCurrentMillisecs() : 0}; + + if (logic_thread_state_) { + PyEval_RestoreThread(logic_thread_state_); + logic_thread_state_ = nullptr; + } + + if (debug_timing) { + auto duration{CorePlatform::GetCurrentMillisecs() - startms}; + if (duration > (1000 / 120)) { + Log(LogLevel::kInfo, + "GIL acquire took too long (" + std::to_string(duration) + " ms)."); + } + } +} + +void CorePython::ReleaseGIL() { + assert(g_base_soft && g_base_soft->InLogicThread()); + assert(logic_thread_state_ == nullptr); + logic_thread_state_ = PyEval_SaveThread(); +} + +} // namespace ballistica::core diff --git a/src/ballistica/core/python/core_python.h b/src/ballistica/core/python/core_python.h new file mode 100644 index 00000000..98811832 --- /dev/null +++ b/src/ballistica/core/python/core_python.h @@ -0,0 +1,84 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CORE_PYTHON_CORE_PYTHON_H_ +#define BALLISTICA_CORE_PYTHON_CORE_PYTHON_H_ + +#include +#include + +#include "ballistica/core/core.h" +#include "ballistica/shared/python/python_object_set.h" + +namespace ballistica::core { + +/// General Python support class for our feature-set. +class CorePython { + public: + /// Specific Python objects we hold in objs_. + enum class ObjID { + kMainDict, + kShallowCopyCall, + kDeepCopyCall, + kJsonDumpsCall, + kJsonLoadsCall, + kEmptyTuple, + kLoggingDebugCall, + kLoggingInfoCall, + kLoggingWarningCall, + kLoggingErrorCall, + kLoggingCriticalCall, + kPrependSysPathCall, + kBaEnvConfigureCall, + kBaEnvGetConfigCall, + kBaEnvOnBaBaseImportCall, + kBaEnvOnBaBaseStartAppCall, + kLast // Sentinel; must be at end. + }; + + /// Bring up Python itself. Only needed in Monolithic builds. + void InitPython(); + + /// Run baenv.configure() with all of our monolithic-mode paths/etc. + void MonolithicModeBaEnvConfigure(); + + /// Call once we should start forwarding our Log calls (along with all + /// pent up ones) to Python. + void EnablePythonLoggingCalls(); + + /// Should be called just before base feature set import; locks in the + /// baenv environment and runs some checks. + void ApplyBaEnvConfig(); + + /// Should be called at the end of base import; runs a few checks/etc. + /// that cannot be run until _babase is actually imported. + void RunBaEnvOnBaBaseImport(); + + /// Calls Python logging function (logging.error, logging.warning, etc.) + /// Can be called from any thread at any time. If called before Python + /// logging is available, logs locally using Logging::DisplayLog() + /// (with an added warning). + void LoggingCall(LogLevel loglevel, const std::string& msg); + void AcquireGIL(); + void ReleaseGIL(); + void ImportPythonObjs(); + void VerifyPythonEnvironment(); + void ReleaseMainThreadGIL(); + void SoftImportBase(); + + const auto& objs() { return objs_; } + + private: + PythonObjectSet objs_; + + PyThreadState* logic_thread_state_{}; + + // Log calls we make before we're set up to ship logs through Python + // go here. They all get shipped at once as soon as it is possible. + bool python_logging_calls_enabled_{}; + std::mutex early_log_lock_; + std::list> early_logs_; +}; + +} // namespace ballistica::core + +#endif // BALLISTICA_CORE_PYTHON_CORE_PYTHON_H_ diff --git a/src/ballistica/core/support/base_soft.h b/src/ballistica/core/support/base_soft.h new file mode 100644 index 00000000..78f35e73 --- /dev/null +++ b/src/ballistica/core/support/base_soft.h @@ -0,0 +1,50 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CORE_SUPPORT_BASE_SOFT_H_ +#define BALLISTICA_CORE_SUPPORT_BASE_SOFT_H_ + +#include "ballistica/core/core.h" +#include "ballistica/shared/ballistica.h" + +namespace ballistica::core { + +/// 'Soft' interface to the base feature-set. +/// Feature-sets listing base as a soft requirement must limit their use of +/// base to these methods and should be prepared to handle the not-present +/// case. +class BaseSoftInterface { + public: + virtual void ScreenMessage(const std::string& s, const Vector3f& color) = 0; + virtual auto IsUnmodifiedBlessedBuild() -> bool = 0; + virtual void StartApp() = 0; + virtual auto AppManagesEventLoop() -> bool = 0; + virtual void RunAppToCompletion() = 0; + virtual void PrimeAppMainThreadEventPump() = 0; + virtual auto InAssetsThread() const -> bool = 0; + virtual auto InLogicThread() const -> bool = 0; + virtual auto InGraphicsThread() const -> bool = 0; + virtual auto InAudioThread() const -> bool = 0; + virtual auto InBGDynamicsThread() const -> bool = 0; + virtual auto InNetworkWriteThread() const -> bool = 0; + virtual void PlusDirectSendV1CloudLogs(const std::string& prefix, + const std::string& suffix, + bool instant, int* result) = 0; + virtual auto CreateFeatureSetData(FeatureSetFrontEnd* featureset) + -> PyObject* = 0; + virtual auto FeatureSetFromData(PyObject* obj) -> FeatureSetFrontEnd* = 0; + virtual void V1CloudLog(const std::string& msg) = 0; + virtual void PushConsolePrintCall(const std::string& msg) = 0; + virtual auto GetPyExceptionType(PyExcType exctype) -> PyObject* = 0; + virtual auto PrintPythonStackTrace() -> bool = 0; + virtual auto GetPyLString(PyObject* obj) -> std::string = 0; + virtual auto DoGetContextBaseString() -> std::string = 0; + virtual void DoPrintContextAuto() = 0; + virtual void DoPushObjCall(const PythonObjectSetBase* objset, int id) = 0; + virtual void DoPushObjCall(const PythonObjectSetBase* objset, int id, + const std::string& arg) = 0; + virtual auto IsAppRunning() const -> bool = 0; +}; + +} // namespace ballistica::core + +#endif // BALLISTICA_CORE_SUPPORT_BASE_SOFT_H_ diff --git a/src/ballistica/core/support/core_config.cc b/src/ballistica/core/support/core_config.cc new file mode 100644 index 00000000..8f119996 --- /dev/null +++ b/src/ballistica/core/support/core_config.cc @@ -0,0 +1,175 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/core/support/core_config.h" + +#include +#include + +// Note to self: this stuff gets used before *any* of the engine is inited +// so we can't use engine functionality at all here. + +namespace ballistica::core { + +// Kicks out of arg processing and tells the app to return an error code. +class BadArgsException : public ::std::exception {}; + +static auto IsSingleArgSpecialCase(int argc, char** argv, const char* arg_long, + const char* arg_short = nullptr) -> bool { + // See if the args exists *anywhere*. + for (int i = 1; i < argc; ++i) { + for (const char* arg : {arg_short, arg_long}) { + if (arg == nullptr) { + continue; + } + if (!strcmp(argv[i], arg)) { + // These args are designed to not coexist with others. + if (argc != 2) { + printf("Error: Arg '%s' cannot be used with other args.\n", argv[i]); + throw BadArgsException(); + } + return true; + } + } + } + return false; +} + +static void PrintHelp() { + printf( + "ballisticakit help:\n" + " -h, --help Print this help.\n" + " -v, --version Print app version information.\n" + " -c, --command Run a Python command instead of the normal" + " app loop.\n" + " -e, --exec Run a Python command from within" + " the app loop.\n" + " -C, --config-dir Override the app config directory.\n" + " -d, --data-dir Override the app data directory.\n" + " -m, --mods-dir Override the app mods directory.\n"); +} + +/// If the arg at the provided index matches the long/short names given, +/// returns the value in the next arg place and increments the index. +static auto ParseArgValue(int argc, char** argv, int* i, const char* arg_long, + const char* arg_short = nullptr) + -> std::optional { + assert(i); + assert(*i < argc); + for (const char* arg : {arg_short, arg_long}) { + if (arg == nullptr) { + continue; + } + if (!strcmp(argv[*i], arg)) { + // Ok; we match! + auto val_index = *i + 1; + if (val_index >= argc) { + printf("Error: No value provided following arg '%s'.\n", argv[*i]); + throw BadArgsException(); + } + *i += 2; + return argv[val_index]; + } + } + // No match. + return {}; +} + +auto CoreConfig::FromCommandLineAndEnv(int argc, char** argv) -> CoreConfig { + auto cfg = CoreConfig(); + + // First set any values we allow env-vars for. + // We want explicitly passed values to override these in any cases where both + // forms are accepted. + if (auto* envval = getenv("BA_BOOT_LOG")) { + if (!strcmp(envval, "1")) { + cfg.log_boot_process = true; + } + } + if (auto* envval = getenv("BA_DEBUGGER_ATTACHED")) { + if (!strcmp(envval, "1")) { + cfg.debugger_attached = true; + } + } + + // REMOVE ME FOR 1.7.20 FINAL. + printf("TEMP: forcing log_boot_process true.\n"); + cfg.log_boot_process = true; + + try { + // First handle single-arg special cases like --help or --version. + if (IsSingleArgSpecialCase(argc, argv, "--help", "-h")) { + PrintHelp(); + cfg.immediate_return_code = 0; + return cfg; + } + if (IsSingleArgSpecialCase(argc, argv, "--version", "-v")) { + printf("BallisticaKit %s build %d\n", kEngineVersion, kEngineBuildNumber); + cfg.immediate_return_code = 0; + return cfg; + } + if (IsSingleArgSpecialCase(argc, argv, "--crash")) { + int dummyval{}; + int* invalid_ptr{&dummyval}; + + // A bit of obfuscation to try and keep linters quiet. + if (explicit_bool(true)) { + invalid_ptr = nullptr; + } + if (explicit_bool(true)) { + *invalid_ptr = 1; + } + return cfg; + } + + // Ok, all single-arg cases handled; now go through everything else + // parsing flags/values from left to right. + int i = 1; + std::optional value; + while (i < argc) { + if ((value = ParseArgValue(argc, argv, &i, "--command", "-c"))) { + cfg.call_command = *value; + } else if ((value = ParseArgValue(argc, argv, &i, "--exec", "-e"))) { + cfg.exec_command = *value; + } else if ((value = + ParseArgValue(argc, argv, &i, "--config-dir", "-C"))) { + cfg.config_dir = *value; + // Make sure what they passed exists. + // Note: Normally baenv will try to create whatever the config dir is; + // do we just want to allow that to happen in this case? But perhaps + // being more strict is ok when accepting user input. + if (!std::filesystem::exists(*cfg.config_dir)) { + printf("Error: Provided config dir does not exist: '%s'.", + cfg.config_dir->c_str()); + throw BadArgsException(); + } + } else if ((value = ParseArgValue(argc, argv, &i, "--data-dir", "-d"))) { + cfg.data_dir = *value; + // Make sure what they passed exists. + if (!std::filesystem::exists(*cfg.data_dir)) { + printf("Error: Provided data dir does not exist: '%s'.", + cfg.data_dir->c_str()); + throw BadArgsException(); + } + } else if ((value = ParseArgValue(argc, argv, &i, "--mods-dir", "-m"))) { + cfg.user_python_dir = *value; + // Make sure what they passed exists. + if (!std::filesystem::exists(*cfg.user_python_dir)) { + printf("Error: Provided mods dir does not exist: '%s'.", + cfg.user_python_dir->c_str()); + throw BadArgsException(); + } + } else { + printf( + "Error: Invalid arg '%s'.\n" + "Run 'ballisticakit --help' to see available args.\n", + argv[i]); + throw BadArgsException(); + } + } + } catch (const BadArgsException&) { + cfg.immediate_return_code = 1; + } + return cfg; +} + +} // namespace ballistica::core diff --git a/src/ballistica/core/support/core_config.h b/src/ballistica/core/support/core_config.h new file mode 100644 index 00000000..9468d49c --- /dev/null +++ b/src/ballistica/core/support/core_config.h @@ -0,0 +1,61 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CORE_SUPPORT_CORE_CONFIG_H_ +#define BALLISTICA_CORE_SUPPORT_CORE_CONFIG_H_ + +#include +#include + +#include "ballistica/shared/ballistica.h" + +namespace ballistica::core { + +/// Collection of low level options for a run of the engine; passed +/// when initing the core feature-set. +class CoreConfig { + public: + static auto FromCommandLineAndEnv(int argc, char** argv) -> CoreConfig; + + /// Enable vr mode on supported platforms. + bool vr_mode{}; + + /// If set, the app should exit immediately with this return code (on + /// applicable platforms). This can be set by command-line parsing in + /// response to arguments such as 'version' or 'help' which are processed + /// immediately in their entirety. + std::optional immediate_return_code{}; + + /// If set, this single Python command will be run instead of the + /// normal app loop. + std::optional call_command{}; + + /// Python command to be run within the normal app loop. + std::optional exec_command{}; + + /// Explicitly set config dir. + std::optional config_dir{}; + + /// Explicitly set data dir. + std::optional data_dir{}; + + /// Explicitly set user-python (mods) dir. + std::optional user_python_dir{}; + + /// Log various stages/times in the bootstrapping process. + bool log_boot_process{}; + + /// Normally early C++ Log() calls are held until babase has been imported + /// so that when they are pushed out to the Python logging calls they are + /// properly routed through the full engine. If you are not using babase + /// or are trying to debug early issues you can flip this off to push + /// things to Python as soon as technically possible. + bool hold_early_logs{true}; + + /// Let the engine know there's a debugger attached so it should do things + /// like abort() instead of exiting with error codes. + bool debugger_attached{}; +}; + +} // namespace ballistica::core + +#endif // BALLISTICA_CORE_SUPPORT_CORE_CONFIG_H_ diff --git a/src/ballistica/core/types.h b/src/ballistica/core/types.h deleted file mode 100644 index 956d63f8..00000000 --- a/src/ballistica/core/types.h +++ /dev/null @@ -1,1074 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_CORE_TYPES_H_ -#define BALLISTICA_CORE_TYPES_H_ - -// Types used throughout the project. -// This header should not depend on any others in the project. -// Types can be defined (or predeclared) here if the are used -// in a significant number of places. The aim is to reduce the -// overall number of headers a given source file needs to pull in, -// helping to keep compile times down. - -#ifdef __cplusplus - -// Predeclare a few global namespace things -// (just enough to pass some pointers around without -// requiring system-ish headers). -typedef struct _object PyObject; -typedef struct _ts PyThreadState; -typedef struct PyMethodDef PyMethodDef; - -#if BA_SDL_BUILD || BA_MINSDL_BUILD -union SDL_Event; -struct SDL_Keysym; -typedef struct _SDL_Joystick SDL_Joystick; -#endif - -namespace ballistica { - -// Used internally for time values. -typedef int64_t millisecs_t; - -// We predeclare all our main ba classes here so that we can -// avoid pulling in their full headers as much as possible -// to keep compile times down. - -class AppFlavor; -class AppConfig; -class App; -class AppInternal; -class AreaOfInterest; -class Assets; -class Audio; -class AudioServer; -class AudioStreamer; -class AudioSource; -class BGDynamics; -class BGDynamicsServer; -class BGDynamicsDrawSnapshot; -class BGDynamicsEmission; -class BGDynamicsFuse; -struct BGDynamicsFuseData; -class BGDynamicsHeightCache; -class BGDynamicsShadow; -struct BGDynamicsShadowData; -class BGDynamicsVolumeLight; -struct BGDynamicsVolumeLightData; -class ButtonWidget; -struct cJSON; -class Camera; -class ClientControllerInterface; -class ClientInputDevice; -class ClientSession; -class CollideModel; -class CollideModelData; -class Collision; -class CollisionCache; -class Connection; -class ConnectionSet; -class ConnectionToClient; -class Context; -class ContextTarget; -class ConnectionToClientUDP; -class ConnectionToHost; -class ConnectionToHostUDP; -class ContainerWidget; -class Console; -class CubeMapTexture; -class Data; -class DataData; -class Dynamics; -class FrameDef; -class GLContext; -class GlobalsNode; -class Graphics; -class GraphicsServer; -class HostActivity; -class HostSession; -class Huffman; -class ImageMesh; -class ImageWidget; -class Input; -class InputDevice; -struct JointFixedEF; -class Joystick; -class JsonDict; -class KeyboardInput; -class Logic; -class Material; -class MaterialAction; -class MaterialComponent; -class MaterialConditionNode; -class MaterialContext; -class Matrix44f; -class AssetComponentData; -class AssetsServer; -class MeshBufferBase; -class MeshBufferVertexSprite; -class MeshBufferVertexSimpleFull; -class MeshBufferVertexSmokeFull; -class Mesh; -class MeshData; -class MeshDataClientHandle; -class MeshIndexBuffer16; -class MeshIndexedSimpleFull; -class MeshIndexedSmokeFull; -class MeshRendererData; -class Model; -class ModelData; -class ModelRendererData; -class NetClientThread; -class NetGraph; -class Networking; -class NetworkReader; -class NetworkWriter; -class Node; -class NodeType; -class NodeAttribute; -class NodeAttributeConnection; -class NodeAttributeUnbound; -class Object; -class ObjectComponent; -class Part; -class Python; -class Platform; -class Player; -class PlayerNode; -class PlayerSpec; -class PythonClassCollideModel; -class PythonClassMaterial; -class PythonClassModel; -class PythonClassSound; -class PythonClassTexture; -class Python; -class PythonRef; -class PythonCommand; -class PythonContextCall; -template -class RealTimer; -class Rect; -class Renderer; -class RenderComponent; -class RenderCommandBuffer; -class RenderPass; -class RenderTarget; -class ReplayClientSession; -class RemoteAppServer; -class RemoteControlInput; -class RigidBody; -class RootUI; -class RootWidget; -class Runnable; -class Scene; -class SceneV1; -class SceneStream; -class ScoreToBeat; -class SDLApp; -class SDLContext; -class Session; -class SockAddr; -class Sound; -class SoundData; -class SpriteMesh; -class StackWidget; -class StressTest; -class StdioConsole; -class Module; -class TelnetServer; -class TestInput; -class TextGroup; -class TextGraphics; -class TextMesh; -class TextPacker; -class Texture; -class TextureData; -class TexturePreloadData; -class TextureRendererData; -class TextWidget; -class Thread; -class Timer; -class TimerList; -class TouchInput; -class UI; -class Utils; -class Vector2f; -class Vector3f; -class Vector4f; -class AppFlavorVR; -class V1Account; -class VRGraphics; -class Widget; - -// BA_EXPORT_PYTHON_ENUM -/// Types of input a controller can send to the game. -/// -/// Category: Enums -/// -enum class InputType { - kUpDown = 2, - kLeftRight, - kJumpPress, - kJumpRelease, - kPunchPress, - kPunchRelease, - kBombPress, - kBombRelease, - kPickUpPress, - kPickUpRelease, - kRun, - kFlyPress, - kFlyRelease, - kStartPress, - kStartRelease, - kHoldPositionPress, - kHoldPositionRelease, - kLeftPress, - kLeftRelease, - kRightPress, - kRightRelease, - kUpPress, - kUpRelease, - kDownPress, - kDownRelease, - kLast // Sentinel -}; - -typedef int64_t TimerMedium; - -// BA_EXPORT_PYTHON_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 -/// display content at will vary significantly. -/// -/// Category: Enums -/// -/// 'large' is used for devices such as desktop PCs where fine details can -/// be clearly seen. UI elements are generally smaller on the screen -/// and more content can be seen at once. -/// -/// 'medium' is used for devices such as tablets, TVs, or VR headsets. -/// This mode strikes a balance between clean readability and amount of -/// content visible. -/// -/// 'small' is used primarily for phones or other small devices where -/// content needs to be presented as large and clear in order to remain -/// readable from an average distance. -enum class UIScale { - kLarge, - kMedium, - kSmall, - kLast // Sentinel. -}; - -// BA_EXPORT_PYTHON_ENUM -/// Specifies the type of time for various operations to target/use. -/// -/// Category: Enums -/// -/// 'sim' time is the local simulation time for an activity or session. -/// It can proceed at different rates depending on game speed, stops -/// for pauses, etc. -/// -/// 'base' is the baseline time for an activity or session. It proceeds -/// consistently regardless of game speed or pausing, but may stop during -/// occurrences such as network outages. -/// -/// 'real' time is mostly based on clock time, with a few exceptions. It may -/// not advance while the app is backgrounded for instance. (the engine -/// attempts to prevent single large time jumps from occurring) -enum class TimeType { - kSim, - kBase, - kReal, - kLast // Sentinel. -}; - -// BA_EXPORT_PYTHON_ENUM -/// Specifies the format time values are provided in. -/// -/// Category: Enums -enum class TimeFormat { - kSeconds, - kMilliseconds, - kLast // Sentinel. -}; - -// BA_EXPORT_PYTHON_ENUM -/// Permissions that can be requested from the OS. -/// -/// Category: Enums -enum class Permission { - kStorage, - kLast // Sentinel. -}; - -// BA_EXPORT_PYTHON_ENUM -/// Special characters the game can print. -/// -/// Category: Enums -enum class SpecialChar { - kDownArrow, - kUpArrow, - kLeftArrow, - kRightArrow, - kTopButton, - kLeftButton, - kRightButton, - kBottomButton, - kDelete, - kShift, - kBack, - kLogoFlat, - kRewindButton, - kPlayPauseButton, - kFastForwardButton, - kDpadCenterButton, - kOuyaButtonO, - kOuyaButtonU, - kOuyaButtonY, - kOuyaButtonA, - kOuyaLogo, - kLogo, - kTicket, - kGooglePlayGamesLogo, - kGameCenterLogo, - kDiceButton1, - kDiceButton2, - kDiceButton3, - kDiceButton4, - kGameCircleLogo, - kPartyIcon, - kTestAccount, - kTicketBacking, - kTrophy1, - kTrophy2, - kTrophy3, - kTrophy0a, - kTrophy0b, - kTrophy4, - kLocalAccount, - kAlibabaLogo, - kFlagUnitedStates, - kFlagMexico, - kFlagGermany, - kFlagBrazil, - kFlagRussia, - kFlagChina, - kFlagUnitedKingdom, - kFlagCanada, - kFlagIndia, - kFlagJapan, - kFlagFrance, - kFlagIndonesia, - kFlagItaly, - kFlagSouthKorea, - kFlagNetherlands, - kFedora, - kHal, - kCrown, - kYinYang, - kEyeBall, - kSkull, - kHeart, - kDragon, - kHelmet, - kMushroom, - kNinjaStar, - kVikingHelmet, - kMoon, - kSpider, - kFireball, - kFlagUnitedArabEmirates, - kFlagQatar, - kFlagEgypt, - kFlagKuwait, - kFlagAlgeria, - kFlagSaudiArabia, - kFlagMalaysia, - kFlagCzechRepublic, - kFlagAustralia, - kFlagSingapore, - kOculusLogo, - kSteamLogo, - kNvidiaLogo, - kFlagIran, - kFlagPoland, - kFlagArgentina, - kFlagPhilippines, - kFlagChile, - kMikirog, - kV2Logo, - kLast // Sentinel -}; - -enum class AssetType { kTexture, kCollideModel, kModel, kSound, kData, kLast }; - -/// Python exception types we can raise from our own exceptions. -enum class PyExcType { - kRuntime, - kAttribute, - kIndex, - kType, - kValue, - kContext, - kNotFound, - kNodeNotFound, - kActivityNotFound, - kSessionNotFound, - kSessionPlayerNotFound, - kInputDeviceNotFound, - kDelegateNotFound, - kWidgetNotFound -}; - -enum class LogLevel { kDebug, kInfo, kWarning, kError, kCritical }; - -enum class SystemTextureID { - kUIAtlas, - kButtonSquare, - kWhite, - kFontSmall0, - kFontBig, - kCursor, - kBoxingGlove, - kShield, - kExplosion, - kTextClearButton, - kWindowHSmallVMed, - kWindowHSmallVSmall, - kGlow, - kScrollWidget, - kScrollWidgetGlow, - kFlagPole, - kScorch, - kScorchBig, - kShadow, - kLight, - kShadowSharp, - kLightSharp, - kShadowSoft, - kLightSoft, - kSparks, - kEye, - kEyeTint, - kFuse, - kShrapnel1, - kSmoke, - kCircle, - kCircleOutline, - kCircleNoAlpha, - kCircleOutlineNoAlpha, - kCircleShadow, - kSoftRect, - kSoftRect2, - kSoftRectVertical, - kStartButton, - kBombButton, - kOuyaAButton, - kBackIcon, - kNub, - kArrow, - kMenuButton, - kUsersButton, - kActionButtons, - kTouchArrows, - kTouchArrowsActions, - kRGBStripes, - kUIAtlas2, - kFontSmall1, - kFontSmall2, - kFontSmall3, - kFontSmall4, - kFontSmall5, - kFontSmall6, - kFontSmall7, - kFontExtras, - kFontExtras2, - kFontExtras3, - kFontExtras4, - kCharacterIconMask, - kBlack, - kWings -}; - -enum class SystemCubeMapTextureID { - kReflectionChar, - kReflectionPowerup, - kReflectionSoft, - kReflectionSharp, - kReflectionSharper, - kReflectionSharpest -}; - -enum class SystemSoundID { - kDeek = 0, - kBlip, - kBlank, - kPunch, - kClick, - kErrorBeep, - kSwish, - kSwish2, - kSwish3, - kTap, - kCorkPop, - kGunCock, - kTickingCrazy, - kSparkle, - kSparkle2, - kSparkle3 -}; - -enum class SystemDataID {}; - -enum class SystemModelID { - kButtonSmallTransparent, - kButtonSmallOpaque, - kButtonMediumTransparent, - kButtonMediumOpaque, - kButtonBackTransparent, - kButtonBackOpaque, - kButtonBackSmallTransparent, - kButtonBackSmallOpaque, - kButtonTabTransparent, - kButtonTabOpaque, - kButtonLargeTransparent, - kButtonLargeOpaque, - kButtonLargerTransparent, - kButtonLargerOpaque, - kButtonSquareTransparent, - kButtonSquareOpaque, - kCheckTransparent, - kScrollBarThumbTransparent, - kScrollBarThumbOpaque, - kScrollBarThumbSimple, - kScrollBarThumbShortTransparent, - kScrollBarThumbShortOpaque, - kScrollBarThumbShortSimple, - kScrollBarTroughTransparent, - kTextBoxTransparent, - kImage1x1, - kImage1x1FullScreen, - kImage2x1, - kImage4x1, - kImage16x1, -#if BA_VR_BUILD - kImage1x1VRFullScreen, - kVROverlay, - kVRFade, -#endif - kOverlayGuide, - kWindowHSmallVMedTransparent, - kWindowHSmallVMedOpaque, - kWindowHSmallVSmallTransparent, - kWindowHSmallVSmallOpaque, - kSoftEdgeOutside, - kSoftEdgeInside, - kBoxingGlove, - kShield, - kFlagPole, - kFlagStand, - kScorch, - kEyeBall, - kEyeBallIris, - kEyeLid, - kHairTuft1, - kHairTuft1b, - kHairTuft2, - kHairTuft3, - kHairTuft4, - kShrapnel1, - kShrapnelSlime, - kShrapnelBoard, - kShockWave, - kFlash, - kCylinder, - kArrowFront, - kArrowBack, - kActionButtonLeft, - kActionButtonTop, - kActionButtonRight, - kActionButtonBottom, - kBox, - kLocator, - kLocatorBox, - kLocatorCircle, - kLocatorCircleOutline, - kCrossOut, - kWing -}; - -enum class NodeCollideAttr { - /// Whether or not a collision should occur at all. - /// If this is false for either node in the final context, - /// no collide events are run. - kCollideNode -}; - -enum class PartCollideAttr { - /// Whether or not a collision should occur at all. - /// If this is false for either surface in the final context, - /// no collide events are run. - kCollide, - - /// Whether to honor node-collisions. - /// Turn this on if you want a collision to occur even if - /// The part is ignoring collisions with your node due - /// to an existing NodeModAction. - kUseNodeCollide, - - /// Whether a physical collision happens. - kPhysical, - - /// Friction for physical collisions. - kFriction, - - /// Stiffness for physical collisions. - kStiffness, - - /// Damping for physical collisions. - kDamping, - - /// Bounce for physical collisions. - kBounce -}; - -enum class MaterialCondition { - /// Always evaluates to true. - kTrue, - - /// Always evaluates to false. - kFalse, - - /// Dst part contains specified material; requires 1 arg - material id. - kDstIsMaterial, - - /// Dst part does not contain specified material; requires 1 arg - material - /// id. - kDstNotMaterial, - - /// Dst part is in specified node; requires 1 arg - node id. - kDstIsNode, - - /// Dst part not in specified node; requires 1 arg - node id. - kDstNotNode, - - /// Dst part is specified part; requires 2 args, node id, part id. - kDstIsPart, - - /// Dst part not specified part; requires 2 args, node id, part id. - kDstNotPart, - - /// Dst part contains src material; no args. - kSrcDstSameMaterial, - - /// Dst part does not contain the src material; no args. - kSrcDstDiffMaterial, - - /// Dst and src parts in same node; no args. - kSrcDstSameNode, - - /// Dst and src parts in different node; no args. - kSrcDstDiffNode, - - /// Src part younger than specified value; requires 1 arg - age. - kSrcYoungerThan, - - /// Src part equal to or older than specified value; requires 1 arg - age. - kSrcOlderThan, - - /// Dst part younger than specified value; requires 1 arg - age. - kDstYoungerThan, - - /// Dst part equal to or older than specified value; requires 1 arg - age. - kDstOlderThan, - - /// Src part is already colliding with a part on dst node; no args. - kCollidingDstNode, - - /// Src part is not already colliding with a part on dst node; no args. - kNotCollidingDstNode, - - /// Set to collide at current point in rule evaluation. - kEvalColliding, - - /// Set to not collide at current point in rule evaluation. - kEvalNotColliding -}; - -/// Types of shading. -/// These do not necessarily correspond to actual shader objects in the renderer -/// (a single shader may handle more than one of these, etc). -/// These are simply categories of looks. -enum class ShadingType { - kSimpleColor, - kSimpleColorTransparent, - kSimpleColorTransparentDoubleSided, - kSimpleTexture, - kSimpleTextureModulated, - kSimpleTextureModulatedColorized, - kSimpleTextureModulatedColorized2, - kSimpleTextureModulatedColorized2Masked, - kSimpleTextureModulatedTransparent, - kSimpleTextureModulatedTransFlatness, - kSimpleTextureModulatedTransparentDoubleSided, - kSimpleTextureModulatedTransparentColorized, - kSimpleTextureModulatedTransparentColorized2, - kSimpleTextureModulatedTransparentColorized2Masked, - kSimpleTextureModulatedTransparentShadow, - kSimpleTexModulatedTransShadowFlatness, - kSimpleTextureModulatedTransparentGlow, - kSimpleTextureModulatedTransparentGlowMaskUV2, - kObject, - kObjectTransparent, - kObjectLightShadowTransparent, - kSpecial, - kShield, - kObjectReflect, - kObjectReflectTransparent, - kObjectReflectAddTransparent, - kObjectLightShadow, - kObjectReflectLightShadow, - kObjectReflectLightShadowDoubleSided, - kObjectReflectLightShadowColorized, - kObjectReflectLightShadowColorized2, - kObjectReflectLightShadowAdd, - kObjectReflectLightShadowAddColorized, - kObjectReflectLightShadowAddColorized2, - kSmoke, - kSmokeOverlay, - kPostProcess, - kPostProcessEyes, - kPostProcessNormalDistort, - kSprite, - kCount -}; - -enum class DrawType { kTriangles, kPoints }; - -/// Hints to the renderer - stuff that is changed rarely should be static, -/// and stuff changed often should be dynamic. -enum class MeshDrawType { kStatic, kDynamic }; - -enum class ReflectionType { - kNone, - kChar, - kPowerup, - kSoft, - kSharp, - kSharper, - kSharpest -}; - -/// Command values sent across the wire in netplay. -/// Must remain consistent across versions! -enum class SessionCommand { - kBaseTimeStep, - kStepSceneGraph, - kAddSceneGraph, - kRemoveSceneGraph, - kAddNode, - kNodeOnCreate, - kSetForegroundSceneGraph, - kRemoveNode, - kAddMaterial, - kRemoveMaterial, - kAddMaterialComponent, - kAddTexture, - kRemoveTexture, - kAddModel, - kRemoveModel, - kAddSound, - kRemoveSound, - kAddCollideModel, - kRemoveCollideModel, - kConnectNodeAttribute, - kNodeMessage, - kSetNodeAttrFloat, - kSetNodeAttrInt32, - kSetNodeAttrBool, - kSetNodeAttrFloats, - kSetNodeAttrInt32s, - kSetNodeAttrString, - kSetNodeAttrNode, - kSetNodeAttrNodeNull, - kSetNodeAttrNodes, - kSetNodeAttrPlayer, - kSetNodeAttrPlayerNull, - kSetNodeAttrMaterials, - kSetNodeAttrTexture, - kSetNodeAttrTextureNull, - kSetNodeAttrTextures, - kSetNodeAttrSound, - kSetNodeAttrSoundNull, - kSetNodeAttrSounds, - kSetNodeAttrModel, - kSetNodeAttrModelNull, - kSetNodeAttrModels, - kSetNodeAttrCollideModel, - kSetNodeAttrCollideModelNull, - kSetNodeAttrCollideModels, - kPlaySoundAtPosition, - kPlaySound, - kEmitBGDynamics, - kEndOfFile, - kDynamicsCorrection, - kScreenMessageBottom, - kScreenMessageTop, - kAddData, - kRemoveData -}; - -/// Standard messages to send to nodes. -/// Note: the names of these in python are their camelback forms, -/// so SELF_STATE is "selfState", etc. -enum class NodeMessageType { - /// Generic flash - no args. - kFlash, - /// Celebrate message - one int arg for duration. - kCelebrate, - /// Left-hand celebrate message - one int arg for duration. - kCelebrateL, - /// Right-hand celebrate message - one int arg for duration. - kCelebrateR, - /// Instantaneous impulse 3 vector floats. - kImpulse, - kKickback, - /// Knock the target out for an amount of time. - kKnockout, - /// Make a hurt sound. - kHurtSound, - /// You've been picked up.. lose balance or whatever. - kPickedUp, - /// Make a jump sound. - kJumpSound, - /// Make an attack sound. - kAttackSound, - /// Tell the player to scream. - kScreamSound, - /// Move to stand upon the given point facing the given angle. - /// 3 position floats and one angle float. - kStand, - /// Add or remove footing from a node. - /// First arg is an int - either 1 or -1 for add or subtract. - kFooting -}; - -enum class V1LoginState { kSignedOut, kSigningIn, kSignedIn }; - -enum class CameraMode { kFollow, kOrbit }; - -enum class MeshDataType { - kIndexedSimpleSplit, - kIndexedObjectSplit, - kIndexedSimpleFull, - kIndexedDualTextureFull, - kIndexedSmokeFull, - kSprite -}; - -struct TouchEvent { - enum class Type { kDown, kUp, kMoved, kCanceled }; - Type type{}; - void* touch{}; - bool overall{}; // For sanity-checks. - float x{}; - float y{}; -}; - -// Standard vertex structs used in rendering/fileIO/etc. -// Remember to make sure components are on 4 byte boundaries. -// (need to find out how strict we need to be on Metal, Vulkan, etc). - -struct VertexSimpleSplitStatic { - uint16_t uv[2]; -}; - -struct VertexSimpleSplitDynamic { - float position[3]; -}; - -struct VertexSimpleFull { - float position[3]; - uint16_t uv[2]; -}; - -struct VertexDualTextureFull { - float position[3]; - uint16_t uv[2]; - uint16_t uv2[2]; -}; - -struct VertexObjectSplitStatic { - uint16_t uv[2]; -}; - -struct VertexObjectSplitDynamic { - float position[3]; - int16_t normal[3]; - int8_t padding[2]; -}; - -struct VertexObjectFull { - float position[3]; - uint16_t uv[2]; - int16_t normal[3]; - uint8_t padding[2]; -}; - -struct VertexSmokeFull { - float position[3]; - float uv[2]; - uint8_t color[4]; - uint8_t diffuse; - uint8_t padding1[3]; - uint8_t erode; - uint8_t padding2[3]; -}; - -struct VertexSprite { - float position[3]; - uint16_t uv[2]; - float size; - float color[4]; -}; - -enum class MeshFormat { - /// 16bit UV, 8bit normal, 8bit pt-index. - kUV16N8Index8, - /// 16bit UV, 8bit normal, 16bit pt-index. - kUV16N8Index16, - /// 16bit UV, 8bit normal, 32bit pt-index. - kUV16N8Index32 -}; - -enum class TextureType { k2D, kCubeMap }; - -enum class TextureFormat { - kNone, - kRGBA_8888, - kRGB_888, - kRGBA_4444, - kRGB_565, - kDXT1, - kDXT5, - kETC1, - kPVR2, - kPVR4, - kETC2_RGB, - kETC2_RGBA -}; - -enum class TextureCompressionType { kS3TC, kPVR, kETC1, kETC2 }; - -enum class TextureMinQuality { kLow, kMedium, kHigh }; - -enum NodeAttributeFlag { kNodeAttributeFlagReadOnly = 1u }; - -enum class NodeAttributeType { - kFloat, - kFloatArray, - kInt, - kIntArray, - kBool, - kString, - kNode, - kNodeArray, - kPlayer, - kMaterialArray, - kTexture, - kTextureArray, - kSound, - kSoundArray, - kModel, - kModelArray, - kCollideModel, - kCollideModelArray -}; - -enum class ThreadSource { - /// A normal thread spun up by us. - kCreate, - /// For wrapping a ballistica thread around the existing main thread. - kWrapMain -}; - -/// Used for module-thread identification. -/// Mostly just for debugging, through a few things are affected by this -/// (the Logic thread manages the python GIL, etc). -enum class ThreadTag { - kInvalid, - kLogic, - kAssets, - kFileOut, - kMain, - kAudio, - kNetworkWrite, - kSuicide, - kStdin, - kBGDynamics -}; - -enum class V1AccountType { - kInvalid, - kTest, - kGameCenter, - kGameCircle, - kGooglePlay, - kDevice, - kServer, - kOculus, - kSteam, - kNvidiaChina, - kV2 -}; - -enum class GraphicsQuality { - /// Bare minimum graphics. - kLow, - /// Basic graphics; no post-processing. - kMedium, - /// Graphics with bare minimum post-processing. - kHigh, - /// Graphics with full post-processing. - kHigher, - /// Select graphics options automatically. - kAuto -}; - -enum class TextMeshEntryType { kRegular, kExtras, kOSRendered }; - -enum ModelDrawFlags { kModelDrawFlagNoReflection = 1 }; - -enum class LightShadowType { kNone, kTerrain, kObject }; - -enum class TextureQuality { kAuto, kHigh, kMedium, kLow }; - -typedef Node* NodeCreateFunc(Scene* sg); - -enum class BenchmarkType { kNone, kCPU, kGPU }; - -#if BA_VR_BUILD -enum class VRHandType { kNone, kDaydreamRemote, kOculusTouchL, kOculusTouchR }; -struct VRHandState { - VRHandType type = VRHandType::kNone; - float tx = 0.0f; - float ty = 0.0f; - float tz = 0.0f; - float yaw = 0.0f; - float pitch = 0.0f; - float roll = 0.0f; -}; -struct VRHandsState { - VRHandState l; - VRHandState r; -}; -#endif // BA_VR_BUILD - -} // namespace ballistica - -#endif // __cplusplus - -#endif // BALLISTICA_CORE_TYPES_H_ diff --git a/src/ballistica/dynamics/material/sound_material_action.h b/src/ballistica/dynamics/material/sound_material_action.h deleted file mode 100644 index 8e6089b5..00000000 --- a/src/ballistica/dynamics/material/sound_material_action.h +++ /dev/null @@ -1,32 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_DYNAMICS_MATERIAL_SOUND_MATERIAL_ACTION_H_ -#define BALLISTICA_DYNAMICS_MATERIAL_SOUND_MATERIAL_ACTION_H_ - -#include "ballistica/assets/component/sound.h" -#include "ballistica/ballistica.h" -#include "ballistica/dynamics/material/material_action.h" - -namespace ballistica { - -class SoundMaterialAction : public MaterialAction { - public: - SoundMaterialAction() = default; - SoundMaterialAction(Sound* sound_in, float volume_in) - : sound_(sound_in), volume_(volume_in) {} - void Apply(MaterialContext* context, const Part* src_part, - const Part* dst_part, - const Object::Ref& p) override; - auto GetType() const -> Type override { return Type::SOUND; } - auto GetFlattenedSize() -> size_t override; - void Flatten(char** buffer, SceneStream* output_stream) override; - void Restore(const char** buffer, ClientSession* cs) override; - - private: - Object::Ref sound_; - float volume_{}; -}; - -} // namespace ballistica - -#endif // BALLISTICA_DYNAMICS_MATERIAL_SOUND_MATERIAL_ACTION_H_ diff --git a/src/ballistica/generic/real_timer.h b/src/ballistica/generic/real_timer.h deleted file mode 100644 index 88ce26f4..00000000 --- a/src/ballistica/generic/real_timer.h +++ /dev/null @@ -1,49 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GENERIC_REAL_TIMER_H_ -#define BALLISTICA_GENERIC_REAL_TIMER_H_ - -#include "ballistica/ballistica.h" -#include "ballistica/core/object.h" -#include "ballistica/generic/runnable.h" -#include "ballistica/logic/logic.h" - -namespace ballistica { - -// Manages a timer which runs on real time and calls a -// 'HandleRealTimerExpired' method on the provided pointer. -template -class RealTimer : public Object { - public: - RealTimer(millisecs_t length, bool repeat, T* delegate) { - assert(g_logic); - assert(InLogicThread()); - timer_id_ = g_logic->NewRealTimer( - length, repeat, Object::New(delegate, this)); - } - void SetLength(uint32_t length) { - assert(InLogicThread()); - g_logic->SetRealTimerLength(timer_id_, length); - } - ~RealTimer() override { - assert(InLogicThread()); - g_logic->DeleteRealTimer(timer_id_); - } - - private: - class Callback : public Runnable { - public: - Callback(T* delegate, RealTimer* timer) - : delegate_(delegate), timer_(timer) {} - void Run() override { delegate_->HandleRealTimerExpired(timer_); } - - private: - RealTimer* timer_; - T* delegate_; - }; - int timer_id_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_GENERIC_REAL_TIMER_H_ diff --git a/src/ballistica/generic/timer.cc b/src/ballistica/generic/timer.cc deleted file mode 100644 index 882a3cb5..00000000 --- a/src/ballistica/generic/timer.cc +++ /dev/null @@ -1,55 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/generic/timer.h" - -#include "ballistica/generic/timer_list.h" - -namespace ballistica { - -Timer::Timer(TimerList* list_in, int id_in, TimerMedium current_time, - TimerMedium length_in, TimerMedium offset_in, int repeat_count_in) - : list_(list_in), - on_list_(false), - initial_(true), - dead_(false), - list_died_(false), - last_run_time_(current_time), - expire_time_(current_time + offset_in), - id_(id_in), - length_(length_in), - repeat_count_(repeat_count_in) { - list_->timer_count_total_++; -} - -Timer::~Timer() { - // If the list is dead, dont touch the corpse. - if (!list_died_) { - if (on_list_) { - list_->PullTimer(id_); - } else { - // Should never be explicitly deleting the current client timer - // (it should just get marked as dead so the loop can kill it when - // re-submitted). - assert(list_->client_timer_ != this); - } - list_->timer_count_total_--; - } -} - -void Timer::SetLength(TimerMedium l, bool set_start_time, - TimerMedium starttime) { - if (on_list_) { - assert(id_ != 0); // zero denotes "no-id" - Timer* t = list_->PullTimer(id_); - BA_PRECONDITION(t == this); - length_ = l; - if (set_start_time) last_run_time_ = starttime; - expire_time_ = last_run_time_ + length_; - list_->AddTimer(this); - } else { - length_ = l; - if (set_start_time) last_run_time_ = starttime; - } -} - -} // namespace ballistica diff --git a/src/ballistica/generic/timer.h b/src/ballistica/generic/timer.h deleted file mode 100644 index 9609286b..00000000 --- a/src/ballistica/generic/timer.h +++ /dev/null @@ -1,41 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GENERIC_TIMER_H_ -#define BALLISTICA_GENERIC_TIMER_H_ - -#include "ballistica/ballistica.h" -#include "ballistica/core/object.h" -#include "ballistica/generic/runnable.h" - -namespace ballistica { - -class Timer { - public: - auto id() const -> int { return id_; } - auto length() const -> TimerMedium { return length_; } - void SetLength(TimerMedium l, bool set_start_time = false, - TimerMedium starttime = 0); - - private: - Timer(TimerList* list_in, int id_in, TimerMedium current_time, - TimerMedium length_in, TimerMedium offset_in, int repeat_count_in); - virtual ~Timer(); - TimerList* list_{}; - bool on_list_{}; - Timer* next_{}; - bool initial_{}; - bool dead_{}; - bool list_died_{}; - TimerMedium last_run_time_{}; - TimerMedium expire_time_{}; - int id_{}; - TimerMedium length_{}; - int repeat_count_{}; - Object::Ref runnable_; - // FIXME: Shouldn't have friend classes in different files. - friend class TimerList; -}; - -} // namespace ballistica - -#endif // BALLISTICA_GENERIC_TIMER_H_ diff --git a/src/ballistica/generic/timer_list.h b/src/ballistica/generic/timer_list.h deleted file mode 100644 index daf5d7df..00000000 --- a/src/ballistica/generic/timer_list.h +++ /dev/null @@ -1,68 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GENERIC_TIMER_LIST_H_ -#define BALLISTICA_GENERIC_TIMER_LIST_H_ - -#include -#include - -#include "ballistica/ballistica.h" -#include "ballistica/core/object.h" - -namespace ballistica { - -class TimerList { - public: - TimerList(); - ~TimerList(); - - // Run timers up to the provided target time. - void Run(TimerMedium target_time); - - // Create a timer with provided runnable. - auto NewTimer(TimerMedium current_time, TimerMedium length, - TimerMedium offset, int repeat_count, - const Object::Ref& runnable) -> Timer*; - - // Return a timer by its id, or nullptr if the timer no longer exists. - auto GetTimer(int id) -> Timer*; - - // Delete a currently-queued timer via its id. - void DeleteTimer(int timer_id); - - // Return the time until the next timer goes off. - // If no timers are present, -1 is returned. - auto GetTimeToNextExpire(TimerMedium current_time) -> TimerMedium; - - // Return the active timer count. Note that this does not include the client - // timer (a timer returned via getExpiredTimer() but not yet re-submitted). - auto active_timer_count() const -> int { return timer_count_active_; } - - auto empty() -> bool { return (timers_ == nullptr); } - - void Clear(); - - private: - // Returns the next expired timer. When done with the timer, - // return it to the list with Timer::submit() - // (this will either put it back in line or delete it) - auto GetExpiredTimer(TimerMedium target_time) -> Timer*; - auto GetExpiredCount(TimerMedium target_time) -> int; - auto PullTimer(int timer_id, bool remove = true) -> Timer*; - auto SubmitTimer(Timer* t) -> Timer*; - void AddTimer(Timer* t); - int timer_count_active_ = 0; - int timer_count_inactive_ = 0; - int timer_count_total_ = 0; - Timer* client_timer_ = nullptr; - Timer* timers_ = nullptr; - Timer* timers_inactive_ = nullptr; - int next_timer_id_ = 1; - bool running_ = false; - bool are_clearing_ = false; - friend class Timer; -}; - -} // namespace ballistica - -#endif // BALLISTICA_GENERIC_TIMER_LIST_H_ diff --git a/src/ballistica/graphics/component/render_component.cc b/src/ballistica/graphics/component/render_component.cc deleted file mode 100644 index 65c0caa0..00000000 --- a/src/ballistica/graphics/component/render_component.cc +++ /dev/null @@ -1,82 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/graphics/component/render_component.h" - -#include "ballistica/dynamics/rigid_body.h" - -namespace ballistica { - -void RenderComponent::ScissorPush(const Rect& rIn) { - EnsureDrawing(); - cmd_buffer_->PutCommand(RenderCommandBuffer::Command::kScissorPush); - cmd_buffer_->PutFloats(rIn.l, rIn.b, rIn.r, rIn.t); -} - -#if BA_DEBUG_BUILD -void RenderComponent::ConfigForEmptyDebugChecks(bool transparent) { - assert(InLogicThread()); - if (g_graphics->drawing_opaque_only() && transparent) { - throw Exception("Transparent component submitted in opaque-only section"); - } - if (g_graphics->drawing_transparent_only() && !transparent) { - throw Exception("Opaque component submitted in transparent-only section"); - } -} - -void RenderComponent::ConfigForShadingDebugChecks(ShadingType shading_type) { - assert(InLogicThread()); - if (g_graphics->drawing_opaque_only() - && Graphics::IsShaderTransparent(shading_type)) { - throw Exception("Transparent component submitted in opaque-only section"); - } - if (g_graphics->drawing_transparent_only() - && !Graphics::IsShaderTransparent(shading_type)) { - throw Exception("Opaque component submitted in transparent-only section"); - } -} -#endif // BA_DEBUG_BUILD - -void RenderComponent::TransformToBody(const RigidBody& b) { - dBodyID body = b.body(); - dGeomID geom = b.geom(); - const dReal* pos_in; - const dReal* r_in; - if (b.type() == RigidBody::Type::kBody) { - pos_in = dBodyGetPosition(body); - r_in = dBodyGetRotation(body); - } else { - pos_in = dGeomGetPosition(geom); - r_in = dGeomGetRotation(geom); - } - float pos[3]; - float r[12]; - for (int x = 0; x < 3; x++) { - pos[x] = pos_in[x]; - } - pos[0] += b.blend_offset().x; - pos[1] += b.blend_offset().y; - pos[2] += b.blend_offset().z; - for (int x = 0; x < 12; x++) { - r[x] = r_in[x]; - } - float matrix[16]; - matrix[0] = r[0]; - matrix[1] = r[4]; - matrix[2] = r[8]; - matrix[3] = 0; - matrix[4] = r[1]; - matrix[5] = r[5]; - matrix[6] = r[9]; - matrix[7] = 0; - matrix[8] = r[2]; - matrix[9] = r[6]; - matrix[10] = r[10]; - matrix[11] = 0; - matrix[12] = pos[0]; - matrix[13] = pos[1]; - matrix[14] = pos[2]; - matrix[15] = 1; - MultMatrix(matrix); -} - -} // namespace ballistica diff --git a/src/ballistica/graphics/component/shield_component.h b/src/ballistica/graphics/component/shield_component.h deleted file mode 100644 index 447df953..00000000 --- a/src/ballistica/graphics/component/shield_component.h +++ /dev/null @@ -1,21 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GRAPHICS_COMPONENT_SHIELD_COMPONENT_H_ -#define BALLISTICA_GRAPHICS_COMPONENT_SHIELD_COMPONENT_H_ - -#include "ballistica/graphics/component/render_component.h" - -namespace ballistica { - -// handles special cases such as drawing light/shadow/back buffers. -class ShieldComponent : public RenderComponent { - public: - explicit ShieldComponent(RenderPass* pass) : RenderComponent(pass) {} - - protected: - void WriteConfig() override; -}; - -} // namespace ballistica - -#endif // BALLISTICA_GRAPHICS_COMPONENT_SHIELD_COMPONENT_H_ diff --git a/src/ballistica/graphics/framebuffer.h b/src/ballistica/graphics/framebuffer.h deleted file mode 100644 index df552233..00000000 --- a/src/ballistica/graphics/framebuffer.h +++ /dev/null @@ -1,19 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GRAPHICS_FRAMEBUFFER_H_ -#define BALLISTICA_GRAPHICS_FRAMEBUFFER_H_ - -#include "ballistica/core/object.h" - -namespace ballistica { - -class Framebuffer : public Object { - public: - auto GetDefaultOwnerThread() const -> ThreadTag override { - return ThreadTag::kMain; - } -}; - -} // namespace ballistica - -#endif // BALLISTICA_GRAPHICS_FRAMEBUFFER_H_ diff --git a/src/ballistica/graphics/mesh/mesh_buffer_vertex_simple_full.h b/src/ballistica/graphics/mesh/mesh_buffer_vertex_simple_full.h deleted file mode 100644 index 27f65892..00000000 --- a/src/ballistica/graphics/mesh/mesh_buffer_vertex_simple_full.h +++ /dev/null @@ -1,18 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SIMPLE_FULL_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SIMPLE_FULL_H_ - -#include "ballistica/graphics/mesh/mesh_buffer.h" - -namespace ballistica { - -// just make this a vanilla child class of our template -// (simply so we could predeclare this) -class MeshBufferVertexSimpleFull : public MeshBuffer { - using MeshBuffer::MeshBuffer; -}; - -} // namespace ballistica - -#endif // BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SIMPLE_FULL_H_ diff --git a/src/ballistica/graphics/mesh/mesh_buffer_vertex_smoke_full.h b/src/ballistica/graphics/mesh/mesh_buffer_vertex_smoke_full.h deleted file mode 100644 index 31ef65ec..00000000 --- a/src/ballistica/graphics/mesh/mesh_buffer_vertex_smoke_full.h +++ /dev/null @@ -1,18 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SMOKE_FULL_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SMOKE_FULL_H_ - -#include "ballistica/graphics/mesh/mesh_buffer.h" - -namespace ballistica { - -// just make this a vanilla child class of our template -// (simply so we could predeclare this) -class MeshBufferVertexSmokeFull : public MeshBuffer { - using MeshBuffer::MeshBuffer; -}; - -} // namespace ballistica - -#endif // BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SMOKE_FULL_H_ diff --git a/src/ballistica/graphics/mesh/mesh_buffer_vertex_sprite.h b/src/ballistica/graphics/mesh/mesh_buffer_vertex_sprite.h deleted file mode 100644 index e2288f9e..00000000 --- a/src/ballistica/graphics/mesh/mesh_buffer_vertex_sprite.h +++ /dev/null @@ -1,18 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SPRITE_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SPRITE_H_ - -#include "ballistica/graphics/mesh/mesh_buffer.h" - -namespace ballistica { - -// just make this a vanilla child class of our template -// (simply so we could predeclare this) -class MeshBufferVertexSprite : public MeshBuffer { - using MeshBuffer::MeshBuffer; -}; - -} // namespace ballistica - -#endif // BALLISTICA_GRAPHICS_MESH_MESH_BUFFER_VERTEX_SPRITE_H_ diff --git a/src/ballistica/graphics/mesh/mesh_data_client_handle.cc b/src/ballistica/graphics/mesh/mesh_data_client_handle.cc deleted file mode 100644 index 79c073c8..00000000 --- a/src/ballistica/graphics/mesh/mesh_data_client_handle.cc +++ /dev/null @@ -1,17 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/graphics/mesh/mesh_data_client_handle.h" - -#include "ballistica/graphics/graphics.h" - -namespace ballistica { - -MeshDataClientHandle::MeshDataClientHandle(MeshData* d) : mesh_data(d) { - g_graphics->AddMeshDataCreate(mesh_data); -} - -MeshDataClientHandle::~MeshDataClientHandle() { - g_graphics->AddMeshDataDestroy(mesh_data); -} - -} // namespace ballistica diff --git a/src/ballistica/graphics/mesh/mesh_index_buffer_16.h b/src/ballistica/graphics/mesh/mesh_index_buffer_16.h deleted file mode 100644 index 84e21a39..00000000 --- a/src/ballistica/graphics/mesh/mesh_index_buffer_16.h +++ /dev/null @@ -1,17 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEX_BUFFER_16_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_INDEX_BUFFER_16_H_ - -#include "ballistica/graphics/mesh/mesh_buffer.h" - -namespace ballistica { - -// standard buffer for indices -class MeshIndexBuffer16 : public MeshBuffer { - using MeshBuffer::MeshBuffer; -}; - -} // namespace ballistica - -#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEX_BUFFER_16_H_ diff --git a/src/ballistica/graphics/mesh/mesh_index_buffer_32.h b/src/ballistica/graphics/mesh/mesh_index_buffer_32.h deleted file mode 100644 index 3cf64da5..00000000 --- a/src/ballistica/graphics/mesh/mesh_index_buffer_32.h +++ /dev/null @@ -1,17 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEX_BUFFER_32_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_INDEX_BUFFER_32_H_ - -#include "ballistica/graphics/mesh/mesh_buffer.h" - -namespace ballistica { - -// standard buffer for indices -class MeshIndexBuffer32 : public MeshBuffer { - using MeshBuffer::MeshBuffer; -}; - -} // namespace ballistica - -#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEX_BUFFER_32_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed_dual_texture_full.h b/src/ballistica/graphics/mesh/mesh_indexed_dual_texture_full.h deleted file mode 100644 index 8194061d..00000000 --- a/src/ballistica/graphics/mesh/mesh_indexed_dual_texture_full.h +++ /dev/null @@ -1,18 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_DUAL_TEXTURE_FULL_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_DUAL_TEXTURE_FULL_H_ - -#include "ballistica/graphics/mesh/mesh_indexed.h" - -namespace ballistica { - -class MeshIndexedDualTextureFull - : public MeshIndexed { - using MeshIndexed::MeshIndexed; // wheee c++11 magic -}; - -} // namespace ballistica - -#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_DUAL_TEXTURE_FULL_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed_simple_full.h b/src/ballistica/graphics/mesh/mesh_indexed_simple_full.h deleted file mode 100644 index 9e7190c5..00000000 --- a/src/ballistica/graphics/mesh/mesh_indexed_simple_full.h +++ /dev/null @@ -1,18 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_FULL_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_FULL_H_ - -#include "ballistica/graphics/mesh/mesh_indexed.h" - -namespace ballistica { - -// a simple mesh with all data provided together (either static or dynamic) -class MeshIndexedSimpleFull - : public MeshIndexed { - using MeshIndexed::MeshIndexed; // wheee c++11 magic -}; - -} // namespace ballistica - -#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SIMPLE_FULL_H_ diff --git a/src/ballistica/graphics/mesh/mesh_indexed_smoke_full.h b/src/ballistica/graphics/mesh/mesh_indexed_smoke_full.h deleted file mode 100644 index a4e4bd26..00000000 --- a/src/ballistica/graphics/mesh/mesh_indexed_smoke_full.h +++ /dev/null @@ -1,18 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SMOKE_FULL_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SMOKE_FULL_H_ - -#include "ballistica/graphics/mesh/mesh_indexed.h" - -namespace ballistica { - -// a mesh with all data provided together (either static or dynamic) -class MeshIndexedSmokeFull - : public MeshIndexed { - using MeshIndexed::MeshIndexed; // wheee c++11 magic -}; - -} // namespace ballistica - -#endif // BALLISTICA_GRAPHICS_MESH_MESH_INDEXED_SMOKE_FULL_H_ diff --git a/src/ballistica/graphics/mesh/mesh_renderer_data.h b/src/ballistica/graphics/mesh/mesh_renderer_data.h deleted file mode 100644 index 67b1eba7..00000000 --- a/src/ballistica/graphics/mesh/mesh_renderer_data.h +++ /dev/null @@ -1,15 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GRAPHICS_MESH_MESH_RENDERER_DATA_H_ -#define BALLISTICA_GRAPHICS_MESH_MESH_RENDERER_DATA_H_ - -namespace ballistica { - -class MeshRendererData { - public: - virtual ~MeshRendererData() = default; -}; - -} // namespace ballistica - -#endif // BALLISTICA_GRAPHICS_MESH_MESH_RENDERER_DATA_H_ diff --git a/src/ballistica/graphics/mesh/sprite_mesh.h b/src/ballistica/graphics/mesh/sprite_mesh.h deleted file mode 100644 index e07bc502..00000000 --- a/src/ballistica/graphics/mesh/sprite_mesh.h +++ /dev/null @@ -1,17 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GRAPHICS_MESH_SPRITE_MESH_H_ -#define BALLISTICA_GRAPHICS_MESH_SPRITE_MESH_H_ - -#include "ballistica/graphics/mesh/mesh_indexed.h" - -namespace ballistica { - -// an indexed sprite-mesh -class SpriteMesh : public MeshIndexed { - using MeshIndexed::MeshIndexed; // wheeee c++11 magic -}; - -} // namespace ballistica - -#endif // BALLISTICA_GRAPHICS_MESH_SPRITE_MESH_H_ diff --git a/src/ballistica/graphics/net_graph.h b/src/ballistica/graphics/net_graph.h deleted file mode 100644 index 80ca1ede..00000000 --- a/src/ballistica/graphics/net_graph.h +++ /dev/null @@ -1,32 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_GRAPHICS_NET_GRAPH_H_ -#define BALLISTICA_GRAPHICS_NET_GRAPH_H_ - -#include -#include - -#include "ballistica/core/object.h" - -namespace ballistica { - -class NetGraph : public Object { - public: - NetGraph(); - ~NetGraph() override; - auto AddSample(double time, double value) -> void; - auto SetLabel(const std::string& label) -> void; - auto SetLastUsedTime(millisecs_t real_time) -> void; - auto LastUsedTime() -> millisecs_t; - auto SetSmoothed(bool smoothed) -> void; - auto Draw(RenderPass* pass, double time, double x, double y, double w, - double h) -> void; - - private: - class Impl; - std::unique_ptr impl_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_GRAPHICS_NET_GRAPH_H_ diff --git a/src/ballistica/input/device/client_input_device.cc b/src/ballistica/input/device/client_input_device.cc deleted file mode 100644 index d146c1a1..00000000 --- a/src/ballistica/input/device/client_input_device.cc +++ /dev/null @@ -1,100 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/input/device/client_input_device.h" - -#include "ballistica/logic/connection/connection_to_client.h" -#include "ballistica/logic/player.h" -#include "ballistica/networking/networking.h" - -namespace ballistica { - -ClientInputDevice::ClientInputDevice(int remote_device_id, - ConnectionToClient* connection_to_client) - : remote_device_id_(remote_device_id), - connection_to_client_(connection_to_client) {} - -// Hmm; do we need to send a remote-detach in this case? -// I don't think so; if we're dying it means the connection is dying -// which means we probably couldn't communicate anyway and -// the other end will free the input-device up -ClientInputDevice::~ClientInputDevice() = default; - -auto ClientInputDevice::GetRawDeviceName() -> std::string { - return "Client Input Device"; -} - -auto ClientInputDevice::GetClientID() const -> int { - if (ConnectionToClient* c = connection_to_client_.get()) { - return c->id(); - } else { - Log(LogLevel::kError, - "ClientInputDevice::get_client_id(): connection_to_client no longer " - "exists; returning -1.."); - return -1; - } -} - -auto ClientInputDevice::GetPlayerProfiles() const -> PyObject* { - if (connection_to_client_.exists()) { - return connection_to_client_->GetPlayerProfiles(); - } - return nullptr; -} - -auto ClientInputDevice::GetAccountName(bool full) const -> std::string { - assert(InLogicThread()); - if (connection_to_client_.exists()) { - if (full) { - return connection_to_client_->peer_spec().GetDisplayString(); - } else { - return connection_to_client_->peer_spec().GetShortName(); - } - } - return "???"; -} - -auto ClientInputDevice::GetPublicV1AccountID() const -> std::string { - assert(InLogicThread()); - if (connection_to_client_.exists()) { - return connection_to_client_->peer_public_account_id(); - } - return ""; -} - -void ClientInputDevice::AttachToLocalPlayer(Player* player) { - if (ConnectionToClient* c = connection_to_client_.get()) { - // Send a new-style message with a 32 bit player-id. - // (added during protocol 29; not always present) - { - std::vector data(6); - data[0] = BA_MESSAGE_ATTACH_REMOTE_PLAYER_2; - data[1] = static_cast_check_fit(remote_device_id_); - int val = player->id(); - memcpy(&(data[2]), &val, sizeof(val)); - c->SendReliableMessage(data); - } - - // We also need to send an old-style message as a fallback. - // FIXME: Can remove this once backwards-compat-protocol is > 29. - { - std::vector data(3); - data[0] = BA_MESSAGE_ATTACH_REMOTE_PLAYER; - data[1] = static_cast_check_fit(remote_device_id_); - data[2] = static_cast_check_fit(player->id()); - c->SendReliableMessage(data); - } - } - InputDevice::AttachToLocalPlayer(player); -} - -void ClientInputDevice::DetachFromPlayer() { - if (ConnectionToClient* c = connection_to_client_.get()) { - std::vector data(2); - data[0] = BA_MESSAGE_DETACH_REMOTE_PLAYER; - data[1] = static_cast_check_fit(remote_device_id_); - c->SendReliableMessage(data); - } - InputDevice::DetachFromPlayer(); -} - -} // namespace ballistica diff --git a/src/ballistica/input/device/client_input_device.h b/src/ballistica/input/device/client_input_device.h deleted file mode 100644 index 526164e3..00000000 --- a/src/ballistica/input/device/client_input_device.h +++ /dev/null @@ -1,44 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_INPUT_DEVICE_CLIENT_INPUT_DEVICE_H_ -#define BALLISTICA_INPUT_DEVICE_CLIENT_INPUT_DEVICE_H_ - -#include - -#include "ballistica/input/device/input_device.h" - -namespace ballistica { - -/// Represents a remote player on a client connected to us. -class ClientInputDevice : public InputDevice { - public: - ClientInputDevice(int remote_device_id, - ConnectionToClient* connection_to_client); - ~ClientInputDevice() override; - - auto GetRawDeviceName() -> std::string override; - auto IsRemoteClient() const -> bool override { return true; } - auto GetClientID() const -> int override; - auto IsLocal() -> bool override { return false; } - - // Return player-profiles dict if available; otherwise nullptr. - auto GetPlayerProfiles() const -> PyObject* override; - auto GetAccountName(bool full) const -> std::string override; - auto GetPublicV1AccountID() const -> std::string override; - void AttachToLocalPlayer(Player* player) override; - void DetachFromPlayer() override; - void PassInputCommand(InputType type, float value) { - InputCommand(type, value); - } - auto connection_to_client() const -> ConnectionToClient* { - return connection_to_client_.get(); - } - - private: - Object::WeakRef connection_to_client_; - int remote_device_id_{}; -}; - -} // namespace ballistica - -#endif // BALLISTICA_INPUT_DEVICE_CLIENT_INPUT_DEVICE_H_ diff --git a/src/ballistica/input/device/input_device.cc b/src/ballistica/input/device/input_device.cc deleted file mode 100644 index 9c7f4859..00000000 --- a/src/ballistica/input/device/input_device.cc +++ /dev/null @@ -1,327 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/input/device/input_device.h" - -#include -#include - -#include "ballistica/app/app.h" -#include "ballistica/internal/app_internal.h" -#include "ballistica/logic/connection/connection_to_host.h" -#include "ballistica/logic/player.h" -#include "ballistica/logic/session/host_session.h" -#include "ballistica/logic/session/net_client_session.h" -#include "ballistica/networking/networking.h" -#include "ballistica/python/class/python_class_input_device.h" -#include "ballistica/python/python.h" - -namespace ballistica { - -static std::unordered_map* g_rand_name_registry = - nullptr; -std::list g_default_names; - -InputDevice::InputDevice() = default; - -auto InputDevice::ShouldBeHiddenFromUser() -> bool { - // Ask the input system whether they want to ignore us.. - return g_input->ShouldCompletelyIgnoreInputDevice(this); -} - -auto InputDevice::GetDeviceName() -> std::string { - assert(InLogicThread()); - return GetRawDeviceName(); -} - -void InputDevice::ResetRandomNames() { - assert(InLogicThread()); - if (g_rand_name_registry == nullptr) return; - g_rand_name_registry->clear(); -} - -// Given a full name "SomeJoyStick #3" etc, reserves/returns a persistent random -// name for it. -static auto GetRandomName(const std::string& full_name) -> std::string { - assert(InLogicThread()); - - // Hmm; statically allocating this is giving some crashes on shutdown :-( - if (g_rand_name_registry == nullptr) { - g_rand_name_registry = new std::unordered_map(); - } - - auto i = g_rand_name_registry->find(full_name); - if (i == g_rand_name_registry->end()) { - // Doesn't exist. Pull a random one and add it. - // Refill the global list if its empty. - if (g_default_names.empty()) { - const std::list& random_name_list = - Utils::GetRandomNameList(); - for (const auto& i2 : random_name_list) { - g_default_names.push_back(i2); - } - } - - // Ok now pull a random one off the list and assign it to us - int index = static_cast(rand() % g_default_names.size()); // NOLINT - auto i3 = g_default_names.begin(); - for (int j = 0; j < index; j++) { - i3++; - } - (*g_rand_name_registry)[full_name] = *i3; - g_default_names.erase(i3); - } - return (*g_rand_name_registry)[full_name]; -} - -auto InputDevice::GetPlayerProfiles() const -> PyObject* { return nullptr; } - -auto InputDevice::GetPublicV1AccountID() const -> std::string { - assert(InLogicThread()); - - // This default implementation assumes the device is local - // so just returns the locally signed in account's public id. - - return g_app_internal->GetPublicV1AccountID(); -} - -auto InputDevice::GetAccountName(bool full) const -> std::string { - assert(InLogicThread()); - if (full) { - return PlayerSpec::GetAccountPlayerSpec().GetDisplayString(); - } else { - return PlayerSpec::GetAccountPlayerSpec().GetShortName(); - } -} - -auto InputDevice::IsRemoteClient() const -> bool { return false; } - -auto InputDevice::GetClientID() const -> int { return -1; } - -auto InputDevice::GetDefaultPlayerName() -> std::string { - assert(InLogicThread()); - char buffer[256]; - snprintf(buffer, sizeof(buffer), "%s %s", GetDeviceName().c_str(), - GetPersistentIdentifier().c_str()); - std::string default_name = GetRandomName(buffer); - return default_name; -} - -auto InputDevice::GetButtonName(int id) -> std::string { - // By default just say 'button 1' or whatnot. - // FIXME: should return this in Lstr json form. - return g_logic->GetResourceString("buttonText") + " " + std::to_string(id); -} - -auto InputDevice::GetAxisName(int id) -> std::string { - // By default just return 'axis 5' or whatnot. - // FIXME: should return this in Lstr json form. - return g_logic->GetResourceString("axisText") + " " + std::to_string(id); -} - -auto InputDevice::HasMeaningfulButtonNames() -> bool { return false; } - -auto InputDevice::GetPersistentIdentifier() const -> std::string { - assert(InLogicThread()); - char buffer[128]; - snprintf(buffer, sizeof(buffer), "#%d", number_); - return buffer; -} - -InputDevice::~InputDevice() { - assert(InLogicThread()); - assert(!player_.exists()); - // release our python ref to ourself if we have one - if (py_ref_) { - Py_DECREF(py_ref_); - } -} - -// when the host-session tells us to attach to a player -void InputDevice::AttachToLocalPlayer(Player* player) { - if (player_.exists()) { - Log(LogLevel::kError, - "InputDevice::AttachToLocalPlayer() called with already " - "existing " - "player"); - return; - } - if (remote_player_.exists()) { - Log(LogLevel::kError, - "InputDevice::AttachToLocalPlayer() called with already " - "existing " - "remote-player"); - return; - } - player_ = player; - player_->SetInputDevice(this); -} - -void InputDevice::AttachToRemotePlayer(ConnectionToHost* connection_to_host, - int remote_player_id) { - assert(connection_to_host); - if (player_.exists()) { - Log(LogLevel::kError, - "InputDevice::AttachToRemotePlayer()" - " called with already existing " - "player"); - return; - } - if (remote_player_.exists()) { - Log(LogLevel::kError, - "InputDevice::AttachToRemotePlayer()" - " called with already existing " - "remote-player"); - return; - } - remote_player_ = connection_to_host; - remote_player_id_ = remote_player_id; -} - -void InputDevice::RemoveRemotePlayerFromGame() { - if (ConnectionToHost* connection_to_host = remote_player_.get()) { - std::vector data(2); - data[0] = BA_MESSAGE_REMOVE_REMOTE_PLAYER; - data[1] = static_cast_check_fit(index()); - connection_to_host->SendReliableMessage(data); - } else { - Log(LogLevel::kError, - "RemoveRemotePlayerFromGame called without remote player"); - } -} - -void InputDevice::DetachFromPlayer() { - if (player_.exists()) { - player_->SetInputDevice(nullptr); - player_.Clear(); - } - // Hmmm.. DetachFromPlayer() doesn't get called if the remote connection dies, - // but since its a weak-ref it should be all good since we don't do anything - // here except clear the weak-ref anyway... - if (remote_player_.exists()) { - remote_player_.Clear(); - } -} - -auto InputDevice::GetRemotePlayer() const -> ConnectionToHost* { - return remote_player_.get(); -} - -// Called to let the current host/client-session know that we'd like to control -// something please. -void InputDevice::RequestPlayer() { - assert(InLogicThread()); - - // Make note that we're being used in some way. - last_input_time_ = g_logic->master_time(); - - if (player_.exists()) { - Log(LogLevel::kError, - "InputDevice::RequestPlayer()" - " called with already-existing player"); - return; - } - if (remote_player_.exists()) { - Log(LogLevel::kError, - "InputDevice::RequestPlayer() called with already-existing " - "remote-player"); - return; - } - - // If we have a local host-session, ask it for a player.. otherwise if we have - // a client-session, ask it for a player. - assert(g_logic); - if (auto* hs = dynamic_cast(g_logic->GetForegroundSession())) { - { - Python::ScopedCallLabel label("requestPlayer"); - hs->RequestPlayer(this); - } - } else if (auto* client_session = dynamic_cast( - g_logic->GetForegroundSession())) { - if (ConnectionToHost* connection_to_host = - client_session->connection_to_host()) { - std::vector data(2); - data[0] = BA_MESSAGE_REQUEST_REMOTE_PLAYER; - data[1] = static_cast_check_fit(index()); - connection_to_host->SendReliableMessage(data); - } - } - // If we're in a replay or the game is still bootstrapping, just ignore.. -} - -void InputDevice::ShipBufferIfFull() { - assert(remote_player_.exists()); - ConnectionToHost* hc = remote_player_.get(); - - // Ship the buffer once it gets big enough or once enough time has passed. - millisecs_t real_time = GetRealTime(); - - size_t size = remote_input_commands_buffer_.size(); - if (size > 2 - && (static_cast(real_time - last_remote_input_commands_send_time_) - >= g_app->buffer_time - || size > 400)) { - last_remote_input_commands_send_time_ = real_time; - hc->SendReliableMessage(remote_input_commands_buffer_); - remote_input_commands_buffer_.clear(); - } -} - -// If we're attached to a remote player, ship completed packets every now and -// then. -void InputDevice::Update() { - if (remote_player_.exists()) { - ShipBufferIfFull(); - } -} - -void InputDevice::UpdateLastInputTime() { - // Keep our own individual time, and also let - // the overall input system know something happened. - last_input_time_ = g_logic->master_time(); - g_input->mark_input_active(); -} - -void InputDevice::InputCommand(InputType type, float value) { - assert(InLogicThread()); - - // Make note that we're being used in some way. - UpdateLastInputTime(); - - if (Player* p = player_.get()) { - p->InputCommand(type, value); - } else if (remote_player_.exists()) { - // Add to existing buffer of input-commands. - { - size_t size = remote_input_commands_buffer_.size(); - // Init if empty; we'll fill in count(bytes 2+3) later. - if (size == 0) { - size = 2; - remote_input_commands_buffer_.resize(size); - remote_input_commands_buffer_[0] = - BA_MESSAGE_REMOTE_PLAYER_INPUT_COMMANDS; - remote_input_commands_buffer_[1] = - static_cast_check_fit(index()); - } - // Now add this command; add 1 byte for type, 4 for value. - remote_input_commands_buffer_.resize(remote_input_commands_buffer_.size() - + 5); - remote_input_commands_buffer_[size] = static_cast(type); - memcpy(&(remote_input_commands_buffer_[size + 1]), &value, 4); - } - } -} - -void InputDevice::ResetHeldStates() {} - -auto InputDevice::GetPyInputDevice(bool new_ref) -> PyObject* { - assert(InLogicThread()); - if (py_ref_ == nullptr) { - py_ref_ = PythonClassInputDevice::Create(this); - } - if (new_ref) Py_INCREF(py_ref_); - return py_ref_; -} - -auto InputDevice::GetPartyButtonName() const -> std::string { return ""; } - -} // namespace ballistica diff --git a/src/ballistica/input/device/input_device.h b/src/ballistica/input/device/input_device.h deleted file mode 100644 index 012efa0c..00000000 --- a/src/ballistica/input/device/input_device.h +++ /dev/null @@ -1,187 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_INPUT_DEVICE_INPUT_DEVICE_H_ -#define BALLISTICA_INPUT_DEVICE_INPUT_DEVICE_H_ - -#include -#include - -#include "ballistica/core/object.h" - -namespace ballistica { - -/// Base class for game input devices (keyboard, joystick, etc). -/// InputDevices can be allocated in any thread (generally on the main -/// thread in response to some system event). An AddInputDevice() call -/// should then be pushed to the logic thread to inform it of the new device. -/// Deletion of the input-device is then handled by the logic thread -/// and can be triggered by pushing a RemoveInputDevice() call to it. -class InputDevice : public Object { - public: - InputDevice(); - ~InputDevice() override; - - /// Called when the device is attached/detached to a local player. - virtual auto AttachToLocalPlayer(Player* player) -> void; - virtual auto AttachToRemotePlayer(ConnectionToHost* connection_to_host, - int remote_player_id) -> void; - virtual auto DetachFromPlayer() -> void; - - /// Issues a command to the remote game to remove the player we're attached - /// to. - auto RemoveRemotePlayerFromGame() -> void; - - /// Return the (not necessarily unique) name of the input device. - auto GetDeviceName() -> std::string; - virtual auto ResetHeldStates() -> void; - - /// Return the default base player name for players using this input device. - virtual auto GetDefaultPlayerName() -> std::string; - - /// Return the name of the signed-in account associated with this device - /// (for remote players, returns their account). - virtual auto GetAccountName(bool full) const -> std::string; - - /// Return the public V1 Account ID of the signed-in account associated - /// with this device, or an empty string if not (yet) available. - /// Note that in some cases there may be a delay before this value - /// is available. (remote player account IDs are verified with the - /// master server before becoming available, etc) - virtual auto GetPublicV1AccountID() const -> std::string; - - /// Returns player-profiles dict if available; otherwise nullptr. - virtual auto GetPlayerProfiles() const -> PyObject*; - - /// Return the name of the button used to evoke the party menu. - virtual auto GetPartyButtonName() const -> std::string; - - /// Returns a number specific to this device type (saying this is the Nth - /// device of this type). - auto device_number() const -> int { return number_; } - auto GetPersistentIdentifier() const -> std::string; - auto attached_to_player() const -> bool { - return player_.exists() || remote_player_.exists(); - } - auto GetRemotePlayer() const -> ConnectionToHost*; - auto GetPlayer() const -> Player* { return player_.get(); } - - /// Return the overall device index; unique to all devices. - auto index() const -> int { return index_; } - - /// Read new control values from config. - virtual auto UpdateMapping() -> void {} - - /// Called during the game loop - for manual button repeats, etc. - virtual auto Update() -> void; - - /// Return client id or -1 if local. - virtual auto GetClientID() const -> int; - - // FIXME: redundant. - virtual auto IsRemoteClient() const -> bool; - -#if BA_SDL_BUILD || BA_MINSDL_BUILD - virtual auto HandleSDLEvent(const SDL_Event* e) -> void {} -#endif - virtual auto GetAllowsConfiguring() -> bool { return true; } - - virtual auto IsController() -> bool { return false; } - virtual auto IsSDLController() -> bool { return false; } - virtual auto IsTouchScreen() -> bool { return false; } - virtual auto IsRemoteControl() -> bool { return false; } - virtual auto IsTestInput() -> bool { return false; } - virtual auto IsKeyboard() -> bool { return false; } - virtual auto IsMFiController() -> bool { return false; } - virtual auto IsLocal() -> bool { return true; } - virtual auto IsUIOnly() -> bool { return false; } - virtual auto IsRemoteApp() -> bool { return false; } - - /// Override this to return true if you implement get_button_name(). - // virtual auto HasButtonNames() -> bool { return false; } - - /// Return a human-readable name for a button/key. - virtual auto GetButtonName(int index) -> std::string; - - /// Return a human-readable name for an axis. - virtual auto GetAxisName(int index) -> std::string; - - /// Return whether button-names returned by GetButtonName() for this - /// device are identifiable to the user on the input-device itself. - /// For example, if a gamepad returns 'A', 'B', 'X', 'Y', etc. as names, - /// this should return true, but if it returns 'button 123', 'button 124', - /// etc. then it should return false. - virtual auto HasMeaningfulButtonNames() -> bool; - - /// Should return true if the input device has a start button and - /// that button activates default widgets (will cause a start icon to show up - /// on them). - virtual auto start_button_activates_default_widget() -> bool { return false; } - auto NewPyRef() -> PyObject* { return GetPyInputDevice(true); } - auto BorrowPyRef() -> PyObject* { return GetPyInputDevice(false); } - auto has_py_ref() -> bool { return (py_ref_ != nullptr); } - auto last_input_time() const -> millisecs_t { return last_input_time_; } - virtual auto ShouldBeHiddenFromUser() -> bool; - static auto ResetRandomNames() -> void; - - /// Return a human-readable name for the device's type. - /// This is used for display and also for storing configs/etc. - virtual auto GetRawDeviceName() -> std::string { return "Input Device"; } - - auto number() const { return number_; } - - /// Return any extra description for the device. - /// This portion is only used for display and not for storing configs. - /// An example is Mac PS3 controllers; they return "(bluetooth)" or "(usb)" - /// here depending on how they are connected. - virtual auto GetDeviceExtraDescription() -> std::string { return ""; } - - /// Devices that have a way of identifying uniquely against other devices of - /// the same type (a serial number, usb-port, etc) should return that here as - /// a string. - virtual auto GetDeviceIdentifier() -> std::string { return ""; } - - /// Called for all devices when they've successfully been added - /// to the input-device list, have a valid ID, name, etc. - virtual auto ConnectionComplete() -> void {} - - auto UpdateLastInputTime() -> void; - - auto set_index(int index_in) -> void { index_ = index_in; } - auto set_numbered_identifier(int n) -> void { number_ = n; } - - protected: - auto ShipBufferIfFull() -> void; - - /// Pass some input command on to whatever we're connected to - /// (player or remote-player). - auto InputCommand(InputType type, float value = 0.0f) -> void; - - /// Subclasses should call this to request a player in the local game. - auto RequestPlayer() -> void; - - auto remote_player_id() const -> int { return remote_player_id_; } - - private: - auto GetPyInputDevice(bool new_ref) -> PyObject*; - - millisecs_t last_remote_input_commands_send_time_{}; - std::vector remote_input_commands_buffer_; - - // note: this is in base-net-time - millisecs_t last_input_time_{}; - - // We're attached to *one* of these two. - Object::WeakRef player_; - Object::WeakRef remote_player_; - - int remote_player_id_{-1}; - PyObject* py_ref_{}; - int index_{-1}; // Our overall device index. - int number_{-1}; // Our type-specific number. - - BA_DISALLOW_CLASS_COPIES(InputDevice); -}; - -} // namespace ballistica - -#endif // BALLISTICA_INPUT_DEVICE_INPUT_DEVICE_H_ diff --git a/src/ballistica/internal/app_internal.h b/src/ballistica/internal/app_internal.h deleted file mode 100644 index 6421c66d..00000000 --- a/src/ballistica/internal/app_internal.h +++ /dev/null @@ -1,57 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_INTERNAL_APP_INTERNAL_H_ -#define BALLISTICA_INTERNAL_APP_INTERNAL_H_ - -#include -#include - -#include "ballistica/core/types.h" - -namespace ballistica { - -auto CreateAppInternal() -> AppInternal*; - -class AppInternal { - public: - virtual ~AppInternal() {} - - virtual auto DefineInternalModule() -> void = 0; - virtual auto PythonPostInit() -> void = 0; - virtual auto HasBlessingHash() -> bool = 0; - virtual auto PutLog(bool fatal) -> bool = 0; - virtual auto AAT() -> void = 0; - virtual auto AATE() -> void = 0; - virtual auto V1LoginDidChange() -> void = 0; - virtual auto SetAdCompletionCall(PyObject* obj, bool pass_actually_showed) - -> void = 0; - virtual auto PushAdViewComplete(const std::string& purpose, - bool actually_showed) -> void = 0; - virtual auto PushPublicPartyState() -> void = 0; - virtual auto PushSetFriendListCall(const std::vector& friends) - -> void = 0; - virtual auto DispatchRemoteAchievementList(const std::set& achs) - -> void = 0; - virtual auto PushAnalyticsCall(const std::string& type, int increment) - -> void = 0; - virtual auto PushPurchaseTransactionCall(const std::string& item, - const std::string& receipt, - const std::string& signature, - const std::string& order_id, - bool user_initiated) -> void = 0; - virtual auto GetPublicV1AccountID() -> std::string = 0; - virtual auto OnLogicThreadPause() -> void = 0; - virtual auto DirectSendV1CloudLogs(const std::string& prefix, - const std::string& suffix, bool instant, - int* result = nullptr) -> void = 0; - virtual auto ClientInfoQuery(const std::string& val1, const std::string& val2, - const std::string& val3, int build_number) - -> void = 0; - virtual auto CalcV1PeerHash(const std::string& peer_hash_input) - -> std::string = 0; - virtual auto V1SetClientInfo(JsonDict* dict) -> void = 0; -}; - -} // namespace ballistica - -#endif // BALLISTICA_INTERNAL_APP_INTERNAL_H_ diff --git a/src/ballistica/logic/client_controller_interface.h b/src/ballistica/logic/client_controller_interface.h deleted file mode 100644 index 3f1a2cf4..00000000 --- a/src/ballistica/logic/client_controller_interface.h +++ /dev/null @@ -1,22 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_LOGIC_CLIENT_CONTROLLER_INTERFACE_H_ -#define BALLISTICA_LOGIC_CLIENT_CONTROLLER_INTERFACE_H_ - -#include "ballistica/ballistica.h" - -namespace ballistica { - -/// An interface for something that can control client-connections -/// (such as an output-stream or a replay-client-session). -/// Objects can register themselves as the current client-connection-controller -/// and then they will get control of all existing (and forthcoming) clients. -class ClientControllerInterface { - public: - virtual auto OnClientConnected(ConnectionToClient* c) -> void = 0; - virtual auto OnClientDisconnected(ConnectionToClient* c) -> void = 0; -}; - -} // namespace ballistica - -#endif // BALLISTICA_LOGIC_CLIENT_CONTROLLER_INTERFACE_H_ diff --git a/src/ballistica/logic/host_activity.h b/src/ballistica/logic/host_activity.h deleted file mode 100644 index f9dfca16..00000000 --- a/src/ballistica/logic/host_activity.h +++ /dev/null @@ -1,123 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_LOGIC_HOST_ACTIVITY_H_ -#define BALLISTICA_LOGIC_HOST_ACTIVITY_H_ - -#include -#include -#include - -#include "ballistica/core/context.h" -#include "ballistica/generic/timer_list.h" -#include "ballistica/python/python_ref.h" - -namespace ballistica { - -class HostActivity : public ContextTarget { - public: - explicit HostActivity(HostSession* host_session); - ~HostActivity() override; - auto GetHostSession() -> HostSession* override; - auto SetGameSpeed(float speed) -> void; - auto game_speed() const -> float { return game_speed_; } - - // ContextTarget time/timer support. - auto NewTimer(TimeType timetype, TimerMedium length, bool repeat, - const Object::Ref& runnable) -> int override; - auto DeleteTimer(TimeType timetype, int timer_id) -> void override; - auto GetTime(TimeType timetype) -> millisecs_t override; - - /// Return a borrowed ref to the python activity; Py_None if nonexistent. - auto GetPyActivity() const -> PyObject*; - - // All these commands are propagated into the output stream - // in addition to being applied locally. - auto NewMaterial(const std::string& name) -> Object::Ref; - auto GetTexture(const std::string& name) -> Object::Ref override; - auto GetSound(const std::string& name) -> Object::Ref override; - auto GetData(const std::string& name) -> Object::Ref override; - auto GetModel(const std::string& name) -> Object::Ref override; - auto GetCollideModel(const std::string& name) - -> Object::Ref override; - auto Update(millisecs_t time_advance) -> millisecs_t; - auto base_time() const -> millisecs_t { return base_time_; } - auto scene() -> Scene* { - assert(scene_.exists()); - return scene_.get(); - } - auto start() -> void; - - // A utility function; faster than dynamic_cast. - auto GetAsHostActivity() -> HostActivity* override; - auto GetMutableScene() -> Scene* override; - auto Draw(FrameDef* frame_def) -> void; - auto ScreenSizeChanged() -> void; - auto LanguageChanged() -> void; - auto DebugSpeedMultChanged() -> void; - auto GraphicsQualityChanged(GraphicsQuality q) -> void; - - // Used to register python calls created in this context so we can make sure - // they got properly cleaned up. - auto RegisterCall(PythonContextCall* call) -> void; - auto shutting_down() const -> bool { return shutting_down_; } - auto globals_node() const -> GlobalsNode*; - auto SetPaused(bool val) -> void; - auto paused() const -> bool { return paused_; } - auto set_allow_kick_idle_players(bool val) -> void { - allow_kick_idle_players_ = val; - } - auto getAllowKickIdlePlayers() const -> bool { - return allow_kick_idle_players_; - } - auto GetSceneStream() const -> SceneStream*; - auto DumpFullState(SceneStream* out) -> void; - auto SetGlobalsNode(GlobalsNode* node) -> void; - auto SetIsForeground(bool val) -> void; - auto RegisterPyActivity(PyObject* pyActivity) -> void; - - private: - auto HandleOutOfBoundsNodes() -> void; - auto NewSimTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> int; - auto DeleteSimTimer(int timer_id) -> void; - auto NewBaseTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> int; - auto DeleteBaseTimer(int timer_id) -> void; - auto UpdateStepTimerLength() -> void; - auto StepScene() -> void; - - Object::WeakRef globals_node_; - bool allow_kick_idle_players_{}; - Timer* step_scene_timer_{}; - std::unordered_map > textures_; - std::unordered_map > sounds_; - std::unordered_map > datas_; - std::unordered_map > - collide_models_; - std::unordered_map > models_; - std::list > materials_; - bool shutting_down_{}; - - // Our list of python calls created in the context of this activity; - // we clear them as we are shutting down and ensure nothing runs after - // that point. - std::list > python_calls_; - millisecs_t next_prune_time_{}; - bool _started{}; - int out_of_bounds_in_a_row_{}; - bool paused_{}; - float game_speed_{}; - millisecs_t base_time_{}; - Object::Ref scene_; - Object::WeakRef host_session_; - PythonRef py_activity_weak_ref_; - - // Want this at the bottom so it dies first since this may cause Python - // stuff to access us. - TimerList sim_timers_; - TimerList base_timers_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_LOGIC_HOST_ACTIVITY_H_ diff --git a/src/ballistica/logic/logic.cc b/src/ballistica/logic/logic.cc deleted file mode 100644 index a502d320..00000000 --- a/src/ballistica/logic/logic.cc +++ /dev/null @@ -1,2183 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/logic/logic.h" - -#include "ballistica/app/app_config.h" -#include "ballistica/app/app_flavor.h" -#include "ballistica/audio/audio.h" -#include "ballistica/core/thread.h" -#include "ballistica/dynamics/bg/bg_dynamics.h" -#include "ballistica/generic/json.h" -#include "ballistica/generic/timer.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/text/text_graphics.h" -#include "ballistica/input/device/client_input_device.h" -#include "ballistica/input/device/keyboard_input.h" -#include "ballistica/input/device/touch_input.h" -#include "ballistica/internal/app_internal.h" -#include "ballistica/logic/connection/connection_set.h" -#include "ballistica/logic/connection/connection_to_client_udp.h" -#include "ballistica/logic/connection/connection_to_host_udp.h" -#include "ballistica/logic/host_activity.h" -#include "ballistica/logic/player.h" -#include "ballistica/logic/session/client_session.h" -#include "ballistica/logic/session/host_session.h" -#include "ballistica/logic/session/net_client_session.h" -#include "ballistica/logic/session/replay_client_session.h" -#include "ballistica/logic/v1_account.h" -#include "ballistica/networking/network_writer.h" -#include "ballistica/networking/sockaddr.h" -#include "ballistica/networking/telnet_server.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_command.h" -#include "ballistica/python/python_context_call.h" -#include "ballistica/python/python_sys.h" -#include "ballistica/scene/node/globals_node.h" -#include "ballistica/ui/console.h" -#include "ballistica/ui/root_ui.h" -#include "ballistica/ui/ui.h" -#include "ballistica/ui/widget/root_widget.h" -#include "ballistica/ui/widget/text_widget.h" - -namespace ballistica { - -/// How long a kick vote lasts. -const int kKickVoteDuration = 30000; - -/// How long everyone has to wait to start a new kick vote after a failed one. -const int kKickVoteFailRetryDelay = 60000; - -/// Extra delay for the initiator of a failed vote. -const int kKickVoteFailRetryDelayInitiatorExtra = 120000; - -// Minimum clients that must be present for a kick vote to count. -// (for non-headless builds we require more votes since the host doesn't count -// but may be playing (in a 2on2 with 3 clients, don't want 2 clients able to -// kick). -// NOLINTNEXTLINE(cert-err58-cpp) -const int kKickVoteMinimumClients = (g_buildconfig.headless_build() ? 3 : 4); - -const int kMaxChatMessages = 40; - -// Go with 5 minute ban. -const int kKickBanSeconds = 5 * 60; - -Logic::Logic() - : game_roster_(cJSON_CreateArray()), - realtimers_(new TimerList()), - connections_(std::make_unique()) { - // We're a singleton; make sure we don't already exist. - assert(g_logic == nullptr); - - InitSpecialChars(); - - // Spin up our thread. - thread_ = new Thread(ThreadTag::kLogic); - g_app->pausable_threads.push_back(thread_); -} -auto Logic::OnAppStart() -> void { - thread_->PushCallSynchronous([this] { OnAppStartInThread(); }); -} - -auto Logic::OnAppStartInThread() -> void { - try { - // Our thread should not be holding the GIL here at the start (and - // probably not have any Python state at all). So here we set both - // of those up. - assert(!PyGILState_Check()); - PyGILState_Ensure(); - - // Tell our thread that it should grab the Python GIL any time it - // is running something and release it when done. - // TODO(ericf): It could be good to explore the idea of having - // individual runnables grab the GIL instead of doing it here at the - // thread level. Though its a bit freeing to know that we can run - // Python code at any time in the logic thread without worry. We can - // also consider explicitly releasing the GIL in the logic thread - // during long operations where we know no Python will occur. - thread_->SetAcquiresPythonGIL(); - - // We want to be informed when our thread is pausing. - thread()->AddPauseCallback( - NewLambdaRunnableRaw([this] { OnThreadPause(); })); - - g_ui->OnAppStart(); - - // Init python and apply our settings immediately. - // This way we can get started loading stuff in the background - // and it'll come in with the correct texture quality etc. - g_python->InitBallisticaPython(); - } catch (const std::exception& e) { - // If anything went wrong, trigger a deferred error. - // This way it is more likely we can show a fatal error dialog - // since the main thread won't be blocking waiting for us to init. - std::string what = e.what(); - this->thread()->PushCall([what] { - // Just throw a standard exception since our what already - // contains a stack trace; if we throw an Exception we wind - // up with a useless second one. - throw std::logic_error(what.c_str()); - }); - } -} - -auto Logic::OnThreadPause() -> void { - ScopedSetContext cp(GetUIContextTarget()); - - // Let Python and internal layers do their thing. - g_python->obj(Python::ObjID::kOnAppPauseCall).Call(); - g_app_internal->OnLogicThreadPause(); -} - -void Logic::InitSpecialChars() { - std::scoped_lock lock(special_char_mutex_); - - special_char_strings_[SpecialChar::kDownArrow] = "\xee\x80\x84"; - special_char_strings_[SpecialChar::kUpArrow] = "\xee\x80\x83"; - special_char_strings_[SpecialChar::kLeftArrow] = "\xee\x80\x81"; - special_char_strings_[SpecialChar::kRightArrow] = "\xee\x80\x82"; - special_char_strings_[SpecialChar::kTopButton] = "\xee\x80\x86"; - special_char_strings_[SpecialChar::kLeftButton] = "\xee\x80\x85"; - special_char_strings_[SpecialChar::kRightButton] = "\xee\x80\x87"; - special_char_strings_[SpecialChar::kBottomButton] = "\xee\x80\x88"; - special_char_strings_[SpecialChar::kDelete] = "\xee\x80\x89"; - special_char_strings_[SpecialChar::kShift] = "\xee\x80\x8A"; - special_char_strings_[SpecialChar::kBack] = "\xee\x80\x8B"; - special_char_strings_[SpecialChar::kLogoFlat] = "\xee\x80\x8C"; - special_char_strings_[SpecialChar::kRewindButton] = "\xee\x80\x8D"; - special_char_strings_[SpecialChar::kPlayPauseButton] = "\xee\x80\x8E"; - special_char_strings_[SpecialChar::kFastForwardButton] = "\xee\x80\x8F"; - special_char_strings_[SpecialChar::kDpadCenterButton] = "\xee\x80\x90"; - - special_char_strings_[SpecialChar::kOuyaButtonO] = "\xee\x80\x99"; - special_char_strings_[SpecialChar::kOuyaButtonU] = "\xee\x80\x9A"; - special_char_strings_[SpecialChar::kOuyaButtonY] = "\xee\x80\x9B"; - special_char_strings_[SpecialChar::kOuyaButtonA] = "\xee\x80\x9C"; - special_char_strings_[SpecialChar::kOuyaLogo] = "\xee\x80\x9D"; - special_char_strings_[SpecialChar::kLogo] = "\xee\x80\x9E"; - special_char_strings_[SpecialChar::kTicket] = "\xee\x80\x9F"; - special_char_strings_[SpecialChar::kGooglePlayGamesLogo] = "\xee\x80\xA0"; - special_char_strings_[SpecialChar::kGameCenterLogo] = "\xee\x80\xA1"; - special_char_strings_[SpecialChar::kDiceButton1] = "\xee\x80\xA2"; - special_char_strings_[SpecialChar::kDiceButton2] = "\xee\x80\xA3"; - special_char_strings_[SpecialChar::kDiceButton3] = "\xee\x80\xA4"; - special_char_strings_[SpecialChar::kDiceButton4] = "\xee\x80\xA5"; - special_char_strings_[SpecialChar::kGameCircleLogo] = "\xee\x80\xA6"; - special_char_strings_[SpecialChar::kPartyIcon] = "\xee\x80\xA7"; - special_char_strings_[SpecialChar::kTestAccount] = "\xee\x80\xA8"; - special_char_strings_[SpecialChar::kTicketBacking] = "\xee\x80\xA9"; - special_char_strings_[SpecialChar::kTrophy1] = "\xee\x80\xAA"; - special_char_strings_[SpecialChar::kTrophy2] = "\xee\x80\xAB"; - special_char_strings_[SpecialChar::kTrophy3] = "\xee\x80\xAC"; - special_char_strings_[SpecialChar::kTrophy0a] = "\xee\x80\xAD"; - special_char_strings_[SpecialChar::kTrophy0b] = "\xee\x80\xAE"; - special_char_strings_[SpecialChar::kTrophy4] = "\xee\x80\xAF"; - special_char_strings_[SpecialChar::kLocalAccount] = "\xee\x80\xB0"; - special_char_strings_[SpecialChar::kAlibabaLogo] = "\xee\x80\xB1"; - - special_char_strings_[SpecialChar::kFlagUnitedStates] = "\xee\x80\xB2"; - special_char_strings_[SpecialChar::kFlagMexico] = "\xee\x80\xB3"; - special_char_strings_[SpecialChar::kFlagGermany] = "\xee\x80\xB4"; - special_char_strings_[SpecialChar::kFlagBrazil] = "\xee\x80\xB5"; - special_char_strings_[SpecialChar::kFlagRussia] = "\xee\x80\xB6"; - special_char_strings_[SpecialChar::kFlagChina] = "\xee\x80\xB7"; - special_char_strings_[SpecialChar::kFlagUnitedKingdom] = "\xee\x80\xB8"; - special_char_strings_[SpecialChar::kFlagCanada] = "\xee\x80\xB9"; - special_char_strings_[SpecialChar::kFlagIndia] = "\xee\x80\xBA"; - special_char_strings_[SpecialChar::kFlagJapan] = "\xee\x80\xBB"; - special_char_strings_[SpecialChar::kFlagFrance] = "\xee\x80\xBC"; - special_char_strings_[SpecialChar::kFlagIndonesia] = "\xee\x80\xBD"; - special_char_strings_[SpecialChar::kFlagItaly] = "\xee\x80\xBE"; - special_char_strings_[SpecialChar::kFlagSouthKorea] = "\xee\x80\xBF"; - special_char_strings_[SpecialChar::kFlagNetherlands] = "\xee\x81\x80"; - - special_char_strings_[SpecialChar::kFedora] = "\xee\x81\x81"; - special_char_strings_[SpecialChar::kHal] = "\xee\x81\x82"; - special_char_strings_[SpecialChar::kCrown] = "\xee\x81\x83"; - special_char_strings_[SpecialChar::kYinYang] = "\xee\x81\x84"; - special_char_strings_[SpecialChar::kEyeBall] = "\xee\x81\x85"; - special_char_strings_[SpecialChar::kSkull] = "\xee\x81\x86"; - special_char_strings_[SpecialChar::kHeart] = "\xee\x81\x87"; - special_char_strings_[SpecialChar::kDragon] = "\xee\x81\x88"; - special_char_strings_[SpecialChar::kHelmet] = "\xee\x81\x89"; - special_char_strings_[SpecialChar::kMushroom] = "\xee\x81\x8A"; - - special_char_strings_[SpecialChar::kNinjaStar] = "\xee\x81\x8B"; - special_char_strings_[SpecialChar::kVikingHelmet] = "\xee\x81\x8C"; - special_char_strings_[SpecialChar::kMoon] = "\xee\x81\x8D"; - special_char_strings_[SpecialChar::kSpider] = "\xee\x81\x8E"; - special_char_strings_[SpecialChar::kFireball] = "\xee\x81\x8F"; - - special_char_strings_[SpecialChar::kFlagUnitedArabEmirates] = "\xee\x81\x90"; - special_char_strings_[SpecialChar::kFlagQatar] = "\xee\x81\x91"; - special_char_strings_[SpecialChar::kFlagEgypt] = "\xee\x81\x92"; - special_char_strings_[SpecialChar::kFlagKuwait] = "\xee\x81\x93"; - special_char_strings_[SpecialChar::kFlagAlgeria] = "\xee\x81\x94"; - special_char_strings_[SpecialChar::kFlagSaudiArabia] = "\xee\x81\x95"; - special_char_strings_[SpecialChar::kFlagMalaysia] = "\xee\x81\x96"; - special_char_strings_[SpecialChar::kFlagCzechRepublic] = "\xee\x81\x97"; - special_char_strings_[SpecialChar::kFlagAustralia] = "\xee\x81\x98"; - special_char_strings_[SpecialChar::kFlagSingapore] = "\xee\x81\x99"; - - special_char_strings_[SpecialChar::kOculusLogo] = "\xee\x81\x9A"; - special_char_strings_[SpecialChar::kSteamLogo] = "\xee\x81\x9B"; - special_char_strings_[SpecialChar::kNvidiaLogo] = "\xee\x81\x9C"; - - special_char_strings_[SpecialChar::kFlagIran] = "\xee\x81\x9D"; - special_char_strings_[SpecialChar::kFlagPoland] = "\xee\x81\x9E"; - special_char_strings_[SpecialChar::kFlagArgentina] = "\xee\x81\x9F"; - special_char_strings_[SpecialChar::kFlagPhilippines] = "\xee\x81\xA0"; - special_char_strings_[SpecialChar::kFlagChile] = "\xee\x81\xA1"; - - special_char_strings_[SpecialChar::kMikirog] = "\xee\x81\xA2"; - special_char_strings_[SpecialChar::kV2Logo] = "\xee\x81\xA3"; -} - -void Logic::SetGameRoster(cJSON* r) { - if (game_roster_ != nullptr) { - cJSON_Delete(game_roster_); - } - game_roster_ = r; -} - -void Logic::ResetActivityTracking() { - largest_draw_time_increment_since_last_reset_ = 0; - first_draw_real_time_ = last_draw_real_time_ = g_platform->GetTicks(); -} - -#if BA_VR_BUILD - -void Logic::PushVRHandsState(const VRHandsState& state) { - thread()->PushCall([this, state] { vr_hands_state_ = state; }); -} - -#endif // BA_VR_BUILD - -void Logic::PushMediaPruneCall(int level) { - thread()->PushCall([level] { - assert(InLogicThread()); - g_assets->Prune(level); - }); -} - -void Logic::PushSetV1LoginCall(V1AccountType account_type, - V1LoginState account_state, - const std::string& account_name, - const std::string& account_id) { - thread()->PushCall( - [this, account_type, account_state, account_name, account_id] { - g_v1_account->SetLogin(account_type, account_state, account_name, - account_id); - }); -} - -void Logic::PushInitialScreenCreatedCall() { - thread()->PushCall([this] { InitialScreenCreated(); }); -} - -void Logic::InitialScreenCreated() { - assert(InLogicThread()); - - // Ok; graphics-server is telling us we've got a screen. - - // We can now let the media thread go to town pre-loading system media - // while we wait. - g_assets->LoadSystemAssets(); - - // FIXME: ideally we should create this as part of bootstrapping, but - // we need it to be possible to load textures/etc. before the renderer - // exists. - if (!HeadlessMode()) { - assert(!g_app->console); - g_app->console = new Console(); - } - - // Set up our timers. - process_timer_ = - thread()->NewTimer(0, true, NewLambdaRunnable([this] { Process(); })); - media_prune_timer_ = thread()->NewTimer( - 2345, true, NewLambdaRunnable([this] { PruneMedia(); })); - - // Normally we schedule updates when we're asked to draw a frame. - // In headless mode, however, we're not drawing, so we need a dedicated - // timer to take its place. - if (HeadlessMode()) { - headless_update_timer_ = - thread()->NewTimer(8, true, NewLambdaRunnable([this] { Update(); })); - } - - RunAppLaunchCommands(); -} - -void Logic::PruneMedia() { g_assets->Prune(); } - -// Launch into main menu or whatever else. -void Logic::RunAppLaunchCommands() { - assert(InLogicThread()); - assert(!ran_app_launch_commands_); - - // First off, run our python app-launch call. - { - // Run this in the UI context. - ScopedSetContext cp(GetUIContext()); - g_python->obj(Python::ObjID::kFinishBootstrappingCall).Call(); - } - ran_app_launch_commands_ = true; - - // If we were passed launch command args, run them. - if (!g_app->exec_command.empty()) { - bool success = PythonCommand(g_app->exec_command, BA_BCFN).Run(); - if (!success) { - exit(1); - } - } - - // If the stuff we just ran didn't result in a session, create a default one. - if (!foreground_session_.exists()) { - RunMainMenu(); - } - - UpdateProcessTimer(); -} - -// Set up our sleeping based on what we're doing. -void Logic::UpdateProcessTimer() { - assert(InLogicThread()); - - // This might get called before we set up our timer in some cases. (such as - // very early) should be safe to ignore since we update the interval - // explicitly after creating the timers. - if (!process_timer_) { - return; - } - - // If there's loading to do, keep at it rather vigorously. - if (have_pending_loads_) { - assert(process_timer_); - process_timer_->SetLength(1); - } else { - // Otherwise we've got nothing to do; go to sleep until something changes. - assert(process_timer_); - process_timer_->SetLength(-1); - } -} - -void Logic::PruneSessions() { - bool have_dead_session = false; - for (auto&& i : sessions_) { - if (i.exists()) { - // If this session is no longer foreground and is ready to die, kill it. - if (i.exists() && i.get() != foreground_session_.get()) { - try { - i.Clear(); - } catch (const std::exception& e) { - Log(LogLevel::kError, - "Exception killing Session: " + std::string(e.what())); - } - have_dead_session = true; - } - } else { - have_dead_session = true; - } - } - if (have_dead_session) { - std::vector > live_list; - for (auto&& i : sessions_) { - if (i.exists()) { - live_list.push_back(i); - } - } - sessions_.swap(live_list); - } -} - -void Logic::UpdateKickVote() { - if (!kick_vote_in_progress_) { - return; - } - ConnectionToClient* kick_vote_starter = kick_vote_starter_.get(); - ConnectionToClient* kick_vote_target = kick_vote_target_.get(); - - // If the target is no longer with us, silently end. - if (kick_vote_target == nullptr) { - kick_vote_in_progress_ = false; - return; - } - millisecs_t current_time{GetRealTime()}; - int total_client_count = 0; - int yes_votes = 0; - int no_votes = 0; - - // Tally current votes for connected clients; if anything has changed, print - // the update and possibly perform the kick. - for (ConnectionToClient* client : connections()->GetConnectionsToClients()) { - ++total_client_count; - if (client->kick_voted()) { - if (client->kick_vote_choice()) { - ++yes_votes; - } else { - ++no_votes; - } - } - } - bool vote_failed = false; - - // If we've fallen below the minimum necessary voters or time has run out, - // fail. - if (total_client_count < kKickVoteMinimumClients) { - vote_failed = true; - } - if (current_time > kick_vote_end_time_) { - vote_failed = true; - } - - if (vote_failed) { - connections()->SendScreenMessageToClients(R"({"r":"kickVoteFailedText"})", - 1, 1, 0); - kick_vote_in_progress_ = false; - - // Disallow kicking for a while for everyone.. but ESPECIALLY so for the guy - // who launched the failed vote. - for (ConnectionToClient* client : - connections()->GetConnectionsToClients()) { - millisecs_t delay = kKickVoteFailRetryDelay; - if (client == kick_vote_starter) { - delay += kKickVoteFailRetryDelayInitiatorExtra; - } - client->set_next_kick_vote_allow_time( - std::max(client->next_kick_vote_allow_time(), current_time + delay)); - } - } else { - int votes_required; - switch (total_client_count) { - case 1: - case 2: - votes_required = 2; // Shouldn't actually be possible. - break; - case 3: - votes_required = HeadlessMode() ? 2 : 3; - break; - case 4: - votes_required = 3; - break; - case 5: - votes_required = HeadlessMode() ? 3 : 4; - break; - case 6: - votes_required = 4; - break; - case 7: - votes_required = HeadlessMode() ? 4 : 5; - break; - default: - votes_required = total_client_count - 3; - break; - } - int votes_needed = votes_required - yes_votes; - if (votes_needed <= 0) { - // ZOMG the vote passed; perform the kick. - connections()->SendScreenMessageToClients( - R"({"r":"kickOccurredText","s":[["${NAME}",)" - + Utils::GetJSONString(kick_vote_target->GetCombinedSpec() - .GetDisplayString() - .c_str()) - + "]]}", - 1, 1, 0); - kick_vote_in_progress_ = false; - connections_->DisconnectClient(kick_vote_target->id(), kKickBanSeconds); - - } else if (votes_needed != last_kick_votes_needed_) { - last_kick_votes_needed_ = votes_needed; - connections()->SendScreenMessageToClients( - R"({"r":"votesNeededText","s":[["${NUMBER}",")" - + std::to_string(votes_needed) + "\"]]}", - 1, 1, 0); - } - } -} - -void Logic::HandleQuitOnIdle() { - if (idle_exit_minutes_) { - float idle_seconds{g_input->input_idle_time() * 0.001f}; - if (!idle_exiting_ && idle_seconds > (idle_exit_minutes_.value() * 60.0f)) { - idle_exiting_ = true; - - thread()->PushCall([this, idle_seconds] { - assert(InLogicThread()); - - // Just go through _ba.quit() - // FIXME: Shouldn't need to go out to the python layer here... - g_python->obj(Python::ObjID::kQuitCall).Call(); - }); - } - } -} - -// Bring our scenes, real-time timers, etc up to date. -void Logic::Update() { - auto startms{Platform::GetCurrentMilliseconds()}; - assert(InLogicThread()); - millisecs_t real_time = GetRealTime(); - g_platform->SetDebugKey("LastUpdateTime", std::to_string(startms)); - if (first_update_) { - master_time_offset_ = master_time_ - real_time; - first_update_ = false; - } - in_update_ = true; - g_input->Update(); - UpdateKickVote(); - - HandleQuitOnIdle(); - - // Send the game roster to our clients if it's changed recently. - if (game_roster_dirty_) { - if (real_time > last_game_roster_send_time_ + 2500) { - // Now send it to all connected clients. - std::vector msg = GetGameRosterMessage(); - for (auto&& c : connections()->GetConnectionsToClients()) { - c->SendReliableMessage(msg); - } - game_roster_dirty_ = false; - last_game_roster_send_time_ = real_time; - } - } - - connections_->Update(); - - // Ok, here's the deal: - // This is where we regulate the speed of everything that's running under us - // (sessions, activities, frame_def-creation, etc) - // we have a master_time which we try to have match real-time as closely - // as possible (unless we physically aren't fast enough to get everything - // done, in which case it'll be slower). We also increment our underlying - // machinery in 8ms increments (1/120 of a second) and try to do 2 updates - // each time we're called, since we're usually being called in a 60hz refresh - // cycle and that'll line our draws up perfectly with our sim steps. - - // TODO(ericf): On modern systems (VR and otherwise) we'll see 80hz, 90hz, - // 120hz, 240hz, etc. It would be great to generalize this to gravitate - // towards clean step patterns in all cases, not just the 60hz and 90hz - // cases we handle now. In general we want stuff like 1,1,2,1,1,2,1,1,2, - // not 1,1,1,2,1,2,2,1,1. - - // Figure out where our net-time *should* be getting to to match real-time. - millisecs_t target_master_time = real_time + master_time_offset_; - millisecs_t amount_behind = target_master_time - master_time_; - - // Normally we assume 60hz so we gravitate towards 2 steps per update to line - // up with our 120hz update timing. - int target_steps = 2; - -#if BA_RIFT_BUILD - // On Rift VR mode we're running 90hz, so lets aim for 1/2/1/2 steps to hit - // our 120hz target. - if (IsVRMode()) { - target_steps = rift_step_index_ + 1; - rift_step_index_ = !rift_step_index_; - } -#endif // BA_RIFT_BUILD - - // Ideally we should be behind by 16 (or 8 for single steps); if its - // *slightly* more than that, let our timing slip a tiny bit to maintain sync. - // This lets us match framerates that are a tiny bit slower than 60hz, such as - // seems to be the case with the Gear VR. - if (amount_behind > 16) { - master_time_offset_ -= 1; - - //.. and recalc these.. - target_master_time = real_time + master_time_offset_; - amount_behind = target_master_time - master_time_; - } - - // if we've fallen behind by a lot, just cut our losses - if (amount_behind > 50) { - master_time_offset_ -= (amount_behind - 50); - target_master_time = real_time + master_time_offset_; - } - - // min/max net-time targets we can aim for; gives us about a steps worth of - // wiggle room to try and keep our exact target cadence - millisecs_t min_target_master_time = - target_master_time >= 8 ? (target_master_time - 8) : 0; - millisecs_t max_target_master_time = target_master_time + 8; - - // run up our real-time timers - realtimers_->Run(real_time); - - // Run session updates until we catch up with projected base time (or run out - // of time). - int step = 1; - - while (true) { - // Try to stick to our target step count whenever possible, but if we get - // too far off target we may need to bail earlier/later. - if (step > target_steps) { - // As long as we're within a step of where we should be, bail now. - if (master_time_ >= min_target_master_time) break; - } else { - // If we've gone too far already, bail. - if (master_time_ >= max_target_master_time) { - // Log(LogLevel::kError, "BAILING EARLY"); - // On rift if this is a 2-step and we bailed after 1, aim for 2 again - // next time (otherwise we'll always get 3 singles in a row when this - // happens). -#if BA_RIFT_BUILD - if (IsVRMode() && target_steps == 2 && step == 2) { - rift_step_index_ = !rift_step_index_; - } -#endif // BA_RIFT_BUILD - break; - } - } - - // Update our UI scene/etc. - g_ui->Update(8); - - // Update all of our sessions. - for (auto&& i : sessions_) { - assert(i.exists()); - i->Update(8); - } - - last_session_update_master_time_ = master_time_; - - // Go ahead and prune dead ones. - PruneSessions(); - - // Advance master time.. - master_time_ += 8; - - // Bail if we spend too much time in here. - millisecs_t new_real_time = GetRealTime(); - if (new_real_time - real_time > 30) { - break; - } - step++; - } - in_update_ = false; - - // Report excessively long updates. - if (g_app->debug_timing && real_time >= next_long_update_report_time_) { - auto duration{Platform::GetCurrentMilliseconds() - startms}; - - // Complain when our full update takes longer than 1/60th second. - if (duration > (1000 / 60)) { - Log(LogLevel::kInfo, - "Logic update took too long (" + std::to_string(duration) + " ms)."); - - // Limit these if we want (not doing so for now). - next_long_update_report_time_ = real_time; - } - } -} - -// Reset the game to a blank slate. -void Logic::Reset() { - assert(InLogicThread()); - - // Tear down any existing setup. - // This should allow high-level objects to die gracefully. - assert(g_python->inited()); - - // Tear down our existing session. - foreground_session_.Clear(); - PruneSessions(); - - // If all is well our sessions should all be dead. - if (g_app->session_count != 0) { - Log(LogLevel::kError, "Session-count is non-zero (" - + std::to_string(g_app->session_count) - + ") on Logic::Reset."); - } - - // Note: we don't clear real-time timers anymore. Should we?.. - g_ui->Reset(); - g_input->Reset(); - g_graphics->Reset(); - g_python->Reset(); - g_audio->Reset(); - - if (!HeadlessMode()) { - // If we haven't, send a first frame_def to the graphics thread to kick - // things off (it'll start sending us requests for more after it gets the - // first). - if (!have_sent_initial_frame_def_) { - g_graphics->BuildAndPushFrameDef(); - have_sent_initial_frame_def_ = true; - } - } -} - -auto Logic::IsInUIContext() const -> bool { - return (g_ui && Context::current().target.get() == g_ui); -} - -void Logic::PushShowURLCall(const std::string& url) { - thread()->PushCall([url] { - assert(InLogicThread()); - assert(g_python); - g_python->ShowURL(url); - }); -} - -auto Logic::GetForegroundContext() -> Context { - Session* s = GetForegroundSession(); - if (s) { - return s->GetForegroundContext(); - } else { - return Context(); - } -} - -void Logic::PushStringEditSetCall(const std::string& value) { - thread()->PushCall([value] { - if (!g_ui) { - Log(LogLevel::kError, "No ui on StringEditSetEvent."); - return; - } -#if BA_OSTYPE_ANDROID - TextWidget* w = TextWidget::GetAndroidStringEditWidget(); - if (w) { - w->SetText(value); - } -#else - throw Exception(); // Shouldn't get here. -#endif - }); -} - -void Logic::PushStringEditCancelCall() { - thread()->PushCall([] { - if (!g_ui) { - Log(LogLevel::kError, "No ui in PushStringEditCancelCall."); - return; - } - }); -} - -// Called by a newly made Session instance to set itself as the current -// session. -void Logic::SetForegroundSession(Session* s) { - assert(InLogicThread()); - foreground_session_ = s; -} - -void Logic::SetForegroundScene(Scene* sg) { - assert(InLogicThread()); - if (foreground_scene_.get() != sg) { - foreground_scene_ = sg; - - // If this scene has a globals-node, put it in charge of stuff. - if (GlobalsNode* g = sg->globals_node()) { - g->SetAsForeground(); - } - } -} - -void Logic::LaunchClientSession() { - if (in_update_) { - throw Exception( - "can't launch a session from within a session update; use " - "ba.pushcall()"); - } - assert(InLogicThread()); - - // Don't want to pick up any old stuff in here. - ScopedSetContext cp(nullptr); - - // This should kill any current session and get us back to a blank slate. - Reset(); - - // Create the new session. - Object::WeakRef old_foreground_session(foreground_session_); - try { - auto s(Object::New()); - sessions_.push_back(s); - - // It should have set itself as FG. - assert(foreground_session_ == s); - } catch (const std::exception& e) { - // If it failed, restore the previous current session and re-throw. - SetForegroundSession(old_foreground_session.get()); - throw Exception(std::string("HostSession failed: ") + e.what()); - } -} - -void Logic::LaunchReplaySession(const std::string& file_name) { - if (in_update_) - throw Exception( - "can't launch a session from within a session update; use " - "ba.pushcall()"); - - assert(InLogicThread()); - - // Don't want to pick up any old stuff in here. - ScopedSetContext cp(nullptr); - - // This should kill any current session and get us back to a blank slate. - Reset(); - - // Create the new session. - Object::WeakRef old_foreground_session(foreground_session_); - try { - auto s(Object::New(file_name)); - sessions_.push_back(s); - - // It should have set itself as FG. - assert(foreground_session_ == s); - } catch (const std::exception& e) { - // If it failed, restore the previous current session and re-throw the - // exception. - SetForegroundSession(old_foreground_session.get()); - throw Exception(std::string("HostSession failed: ") + e.what()); - } -} - -void Logic::LaunchHostSession(PyObject* session_type_obj, - BenchmarkType benchmark_type) { - if (in_update_) { - throw Exception( - "can't call host_session() from within session update; use " - "ba.pushcall()"); - } - - assert(InLogicThread()); - - connections_->PrepareForLaunchHostSession(); - - // Don't want to pick up any old stuff in here. - ScopedSetContext cp(nullptr); - - // This should kill any current session and get us back to a blank slate. - Reset(); - - Object::WeakRef old_foreground_session(foreground_session_); - try { - // Create the new session. - auto s(Object::New(session_type_obj)); - s->set_benchmark_type(benchmark_type); - sessions_.emplace_back(s); - - // It should have set itself as FG. - assert(foreground_session_ == s); - } catch (const std::exception& e) { - // If it failed, restore the previous session context and re-throw the - // exception. - SetForegroundSession(old_foreground_session.get()); - throw Exception(std::string("HostSession failed: ") + e.what()); - } -} - -void Logic::RunMainMenu() { - assert(InLogicThread()); - if (g_app->shutting_down) { - return; - } - assert(g_python); - assert(InLogicThread()); - PythonRef result = - g_python->obj(Python::ObjID::kLaunchMainMenuSessionCall).Call(); - if (!result.exists()) { - throw Exception("error running main menu"); - } -} - -// Commands run via the in-game console. These are a bit more 'casual' and run -// in the current visible context. - -void Logic::PushInGameConsoleScriptCommand(const std::string& command) { - thread()->PushCall([this, command] { - // These are always run in whichever context is 'visible'. - ScopedSetContext cp(GetForegroundContext()); - PythonCommand cmd(command, ""); - if (!g_app->user_ran_commands) { - g_app->user_ran_commands = true; - } - if (cmd.CanEval()) { - PyObject* obj = cmd.RunReturnObj(true, nullptr); - if (obj && obj != Py_None) { - PyObject* s = PyObject_Repr(obj); - if (s) { - const char* c = PyUnicode_AsUTF8(s); - if (g_app->console) { - g_app->console->Print(std::string(c) + "\n"); - } - Py_DECREF(s); - } - Py_DECREF(obj); - } - } else { - // Not eval-able; just run it. - cmd.Run(); - } - }); -} - -// Commands run via stdin. -void Logic::PushStdinScriptCommand(const std::string& command) { - thread()->PushCall([this, command] { - // These are always run in whichever context is 'visible'. - ScopedSetContext cp(GetForegroundContext()); - PythonCommand cmd(command, ""); - if (!g_app->user_ran_commands) { - g_app->user_ran_commands = true; - } - - // Eval this if possible (so we can possibly print return value). - if (cmd.CanEval()) { - if (PyObject* obj = cmd.RunReturnObj(true, nullptr)) { - // Print the value if we're running directly from a terminal - // (or being run under the server-manager) - if ((g_platform->is_stdin_a_terminal() - || g_app_flavor->server_wrapper_managed()) - && obj != Py_None) { - PyObject* s = PyObject_Repr(obj); - if (s) { - const char* c = PyUnicode_AsUTF8(s); - printf("%s\n", c); - fflush(stdout); - Py_DECREF(s); - } - } - Py_DECREF(obj); - } - } else { - // Can't eval it; just run it. - cmd.Run(); - } - }); -} - -void Logic::PushInterruptSignalCall() { - thread()->PushCall([this] { - assert(InLogicThread()); - - // Special case; when running under the server-wrapper, we completely - // ignore interrupt signals (the wrapper acts on them). - if (g_app_flavor->server_wrapper_managed()) { - return; - } - - // FIXME: Shouldn't need to go out to the Python layer here... - g_python->obj(Python::ObjID::kQuitCall).Call(); - }); -} - -void Logic::PushAskUserForTelnetAccessCall() { - thread()->PushCall([this] { - assert(InLogicThread()); - ScopedSetContext cp(GetUIContext()); - g_python->obj(Python::ObjID::kTelnetAccessRequestCall).Call(); - }); -} - -void Logic::PushPythonCall(const Object::Ref& call) { - // Since we're mucking with refs, need to limit to logic thread. - BA_PRECONDITION(InLogicThread()); - BA_PRECONDITION(call->object_strong_ref_count() > 0); - thread()->PushCall([call] { - assert(call.exists()); - call->Run(); - }); -} - -void Logic::PushPythonCallArgs(const Object::Ref& call, - const PythonRef& args) { - // Since we're mucking with refs, need to limit to logic thread. - BA_PRECONDITION(InLogicThread()); - BA_PRECONDITION(call->object_strong_ref_count() > 0); - thread()->PushCall([call, args] { - assert(call.exists()); - call->Run(args.get()); - }); -} - -void Logic::PushPythonWeakCall(const Object::WeakRef& call) { - // Since we're mucking with refs, need to limit to logic thread. - BA_PRECONDITION(InLogicThread()); - - // Even though we only hold a weak ref, we expect a valid strong-reffed - // object to be passed in. - assert(call.exists() && call->object_strong_ref_count() > 0); - - thread()->PushCall([call] { - if (call.exists()) { - Python::ScopedCallLabel label("PythonWeakCallMessage"); - call->Run(); - } - }); -} - -void Logic::PushPythonWeakCallArgs( - const Object::WeakRef& call, const PythonRef& args) { - // Since we're mucking with refs, need to limit to logic thread. - BA_PRECONDITION(InLogicThread()); - - // Even though we only hold a weak ref, we expect a valid strong-reffed - // object to be passed in. - assert(call.exists() && call->object_strong_ref_count() > 0); - - thread()->PushCall([call, args] { - if (call.exists()) call->Run(args.get()); - }); -} - -void Logic::PushPythonRawCallable(PyObject* callable, bool fg_context) { - thread()->PushCall([this, callable, fg_context] { - assert(InLogicThread()); - - // Run this in the UI context by default, or foreground if requested. - ScopedSetContext cp(fg_context ? GetForegroundContext() : GetUIContext()); - - PythonRef(callable, PythonRef::kSteal).Call(); - }); -} - -void Logic::PushScreenMessage(const std::string& message, - const Vector3f& color) { - thread()->PushCall( - [message, color] { g_graphics->AddScreenMessage(message, color); }); -} - -void Logic::SetReplaySpeedExponent(int val) { - replay_speed_exponent_ = std::min(3, std::max(-3, val)); - replay_speed_mult_ = powf(2.0f, static_cast(replay_speed_exponent_)); -} - -void Logic::SetDebugSpeedExponent(int val) { - debug_speed_exponent_ = val; - debug_speed_mult_ = powf(2.0f, static_cast(debug_speed_exponent_)); - - Session* s = GetForegroundSession(); - if (s) s->DebugSpeedMultChanged(); -} - -void Logic::ChangeGameSpeed(int offs) { - assert(InLogicThread()); - - // If we're in a replay session, adjust playback speed there. - if (dynamic_cast(GetForegroundSession())) { - int old_speed = replay_speed_exponent(); - SetReplaySpeedExponent(replay_speed_exponent() + offs); - if (old_speed != replay_speed_exponent()) { - ScreenMessage( - "{\"r\":\"watchWindow.playbackSpeedText\"," - "\"s\":[[\"${SPEED}\",\"" - + std::to_string(replay_speed_mult()) + "\"]]}"); - } - return; - } - // Otherwise, in debug build, we allow speeding/slowing anything. - if (g_buildconfig.debug_build()) { - debug_speed_exponent_ += offs; - debug_speed_mult_ = powf(2.0f, static_cast(debug_speed_exponent_)); - ScreenMessage("DEBUG GAME SPEED TO " + std::to_string(debug_speed_mult_)); - Session* s = GetForegroundSession(); - if (s) { - s->DebugSpeedMultChanged(); - } - } -} - -auto Logic::GetUIContext() const -> Context { - return Context(GetUIContextTarget()); -} - -void Logic::PushToggleManualCameraCall() { - thread()->PushCall([] { g_graphics->ToggleManualCamera(); }); -} - -void Logic::PushToggleDebugInfoDisplayCall() { - thread()->PushCall([] { g_graphics->ToggleNetworkDebugDisplay(); }); -} - -void Logic::PushToggleCollisionGeometryDisplayCall() { - thread()->PushCall([] { g_graphics->ToggleDebugDraw(); }); -} - -void Logic::PushScreenResizeCall(float virtual_width, float virtual_height, - float pixel_width, float pixel_height) { - thread()->PushCall([=] { - ScreenResize(virtual_width, virtual_height, pixel_width, pixel_height); - }); -} - -void Logic::ScreenResize(float virtual_width, float virtual_height, - float pixel_width, float pixel_height) { - assert(InLogicThread()); - assert(g_graphics != nullptr); - if (g_graphics) { - g_graphics->ScreenResize(virtual_width, virtual_height, pixel_width, - pixel_height); - } - if (g_ui) { - g_ui->ScreenSizeChanged(); - } - if (Session* session = GetForegroundSession()) { - session->ScreenSizeChanged(); - } -} - -void Logic::PushGameServiceAchievementListCall( - const std::set& achievements) { - thread()->PushCall( - [this, achievements] { GameServiceAchievementList(achievements); }); -} - -void Logic::GameServiceAchievementList( - const std::set& achievements) { - assert(g_python); - assert(InLogicThread()); - g_app_internal->DispatchRemoteAchievementList(achievements); -} - -void Logic::PushPlaySoundCall(SystemSoundID sound) { - thread()->PushCall( - [sound] { g_audio->PlaySound(g_assets->GetSound(sound)); }); -} - -void Logic::PushConfirmQuitCall() { - thread()->PushCall([this] { - assert(InLogicThread()); - if (HeadlessMode()) { - Log(LogLevel::kError, "PushConfirmQuitCall() unhandled on headless."); - } else { - // If input is locked, just quit immediately.. a confirm screen wouldn't - // work anyway - if (g_input->IsInputLocked() - || (g_app->console != nullptr && g_app->console->active())) { - // Just go through _ba.quit() - // FIXME: Shouldn't need to go out to the python layer here... - g_python->obj(Python::ObjID::kQuitCall).Call(); - return; - } else { - // this needs to be run in the UI context - ScopedSetContext cp(GetUIContextTarget()); - - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kSwish)); - g_python->obj(Python::ObjID::kQuitWindowCall).Call(); - - // if we have a keyboard, give it UI ownership - InputDevice* keyboard = g_input->keyboard_input(); - if (keyboard) { - g_ui->SetUIInputDevice(keyboard); - } - } - } - }); -} - -void Logic::Draw() { - g_graphics->BuildAndPushFrameDef(); - - // Now bring the game up to date. - // By doing this *after* shipping a new frame-def we're reducing the - // chance of frame drops at the expense of adding a bit of visual latency. - // Could maybe try to be smart about which to do first, but not sure - // if its worth it. - Update(); - - // Update our cheat tests. - millisecs_t now = g_platform->GetTicks(); - millisecs_t elapsed = now - last_draw_real_time_; - if (elapsed > largest_draw_time_increment_since_last_reset_) { - largest_draw_time_increment_since_last_reset_ = elapsed; - } - last_draw_real_time_ = now; - - // Sanity test: can make sure our scene is taking exactly 2 steps - // per frame here.. (should generally be the case on 60hz devices). - if (explicit_bool(false)) { - static int64_t last_step = 0; - HostActivity* ha = GetForegroundContext().GetHostActivity(); - if (ha) { - int64_t step = ha->scene()->stepnum(); - Log(LogLevel::kInfo, std::to_string(step - last_step)); - last_step = step; - } - } -} - -void Logic::PushFrameDefRequest() { - thread()->PushCall([this] { Draw(); }); -} - -void Logic::PushOnAppResumeCall() { - thread()->PushCall([] { - // Wipe out whatever input device was in control of the UI. - assert(g_ui); - g_ui->SetUIInputDevice(nullptr); - }); -} - -void Logic::PushApplyConfigCall() { - thread()->PushCall([this] { ApplyConfig(); }); -} - -// Look through everything in our config dict and act on it. -void Logic::ApplyConfig() { - assert(InLogicThread()); - - // Not relevant for fullscreen anymore - // since we're fullscreen windows everywhere. - int width = 800; - int height = 600; - - // Texture quality. - TextureQuality texture_quality_requested; - std::string texqualstr = - g_app_config->Resolve(AppConfig::StringID::kTextureQuality); - - if (texqualstr == "Auto") { - texture_quality_requested = TextureQuality::kAuto; - } else if (texqualstr == "High") { - texture_quality_requested = TextureQuality::kHigh; - } else if (texqualstr == "Medium") { - texture_quality_requested = TextureQuality::kMedium; - } else if (texqualstr == "Low") { - texture_quality_requested = TextureQuality::kLow; - } else { - Log(LogLevel::kError, - "Invalid texture quality: '" + texqualstr + "'; defaulting to low."); - texture_quality_requested = TextureQuality::kLow; - } - - // Graphics quality. - std::string gqualstr = - g_app_config->Resolve(AppConfig::StringID::kGraphicsQuality); - GraphicsQuality graphics_quality_requested; - - if (gqualstr == "Auto") { - graphics_quality_requested = GraphicsQuality::kAuto; - } else if (gqualstr == "Higher") { - graphics_quality_requested = GraphicsQuality::kHigher; - } else if (gqualstr == "High") { - graphics_quality_requested = GraphicsQuality::kHigh; - } else if (gqualstr == "Medium") { - graphics_quality_requested = GraphicsQuality::kMedium; - } else if (gqualstr == "Low") { - graphics_quality_requested = GraphicsQuality::kLow; - } else { - Log(LogLevel::kError, - "Invalid graphics quality: '" + gqualstr + "'; defaulting to auto."); - graphics_quality_requested = GraphicsQuality::kAuto; - } - - // Android res string. - std::string android_res = - g_app_config->Resolve(AppConfig::StringID::kResolutionAndroid); - - bool fullscreen = g_app_config->Resolve(AppConfig::BoolID::kFullscreen); - - // Note: when the graphics-thread applies the first set-screen event it will - // trigger the remainder of startup such as media-loading; make sure nothing - // below this will affect that. - g_graphics_server->PushSetScreenCall(fullscreen, width, height, - texture_quality_requested, - graphics_quality_requested, android_res); - - // FIXME: The graphics server should kick this off *AFTER* it sets the actual - // quality values; here we're just sending along our requested values which - // is wrong. If there's a session up, inform it of the (potential) change. - Session* session = GetForegroundSession(); - if (session) { - session->GraphicsQualityChanged(graphics_quality_requested); - } - - if (!HeadlessMode()) { - g_app->remote_server_accepting_connections = - g_app_config->Resolve(AppConfig::BoolID::kEnableRemoteApp); - } - - chat_muted_ = g_app_config->Resolve(AppConfig::BoolID::kChatMuted); - g_graphics->set_show_fps(g_app_config->Resolve(AppConfig::BoolID::kShowFPS)); - g_graphics->set_show_ping( - g_app_config->Resolve(AppConfig::BoolID::kShowPing)); - - // Set tv border (for both client and server). - // FIXME: this should exist either on the client or the server; not both. - // (and should be communicated via frameldefs/etc.) - bool tv_border = g_app_config->Resolve(AppConfig::BoolID::kTVBorder); - g_graphics_server->thread()->PushCall( - [tv_border] { g_graphics_server->set_tv_border(tv_border); }); - g_graphics->set_tv_border(tv_border); - - g_graphics_server->PushSetScreenGammaCall( - g_app_config->Resolve(AppConfig::FloatID::kScreenGamma)); - g_graphics_server->PushSetScreenPixelScaleCall( - g_app_config->Resolve(AppConfig::FloatID::kScreenPixelScale)); - - TextWidget::set_always_use_internal_keyboard( - g_app_config->Resolve(AppConfig::BoolID::kAlwaysUseInternalKeyboard)); - - // V-sync setting. - std::string v_sync = - g_app_config->Resolve(AppConfig::StringID::kVerticalSync); - bool do_v_sync{}; - bool auto_v_sync{}; - if (v_sync == "Auto") { - do_v_sync = true; - auto_v_sync = true; - } else if (v_sync == "Always") { - do_v_sync = true; - auto_v_sync = false; - } else if (v_sync == "Never") { - do_v_sync = false; - auto_v_sync = false; - } else { - do_v_sync = false; - auto_v_sync = false; - Log(LogLevel::kError, "Invalid 'Vertical Sync' value: '" + v_sync + "'"); - } - g_graphics_server->PushSetVSyncCall(do_v_sync, auto_v_sync); - - g_audio->SetVolumes(g_app_config->Resolve(AppConfig::FloatID::kMusicVolume), - g_app_config->Resolve(AppConfig::FloatID::kSoundVolume)); - - // Kick-idle-players setting (hmm is this still relevant?). - auto* host_session = dynamic_cast(foreground_session_.get()); - kick_idle_players_ = - g_app_config->Resolve(AppConfig::BoolID::kKickIdlePlayers); - if (host_session) { - host_session->SetKickIdlePlayers(kick_idle_players_); - } - - assert(g_input); - g_input->ApplyAppConfig(); - - // Set up network ports/states. - int port = g_app_config->Resolve(AppConfig::IntID::kPort); - int telnet_port = g_app_config->Resolve(AppConfig::IntID::kTelnetPort); - - // NOTE: Hard disabling telnet for now in headless builds; - // it was being exploited to own servers. - bool enable_telnet = - g_buildconfig.headless_build() - ? false - : g_app_config->Resolve(AppConfig::BoolID::kEnableTelnet); - std::string telnet_password = - g_app_config->Resolve(AppConfig::StringID::kTelnetPassword); - - g_app_flavor->PushNetworkSetupCall(port, telnet_port, enable_telnet, - telnet_password); - - bool disable_camera_shake = - g_app_config->Resolve(AppConfig::BoolID::kDisableCameraShake); - g_graphics->set_camera_shake_disabled(disable_camera_shake); - - bool disable_camera_gyro = - g_app_config->Resolve(AppConfig::BoolID::kDisableCameraGyro); - g_graphics->set_camera_gyro_explicitly_disabled(disable_camera_gyro); - - idle_exit_minutes_ = - g_app_config->Resolve(AppConfig::OptionalFloatID::kIdleExitMinutes); - - // Any platform-specific settings. - g_platform->ApplyConfig(); -} - -void Logic::PushRemoveGraphicsServerRenderHoldCall() { - thread()->PushCall([] { - // This call acts as a flush of sorts; when it goes through, - // we push a call to the graphics server saying its ok for it - // to start rendering again. Thus any already-queued-up - // frame_defs or whatnot will be ignored. - g_graphics_server->PushRemoveRenderHoldCall(); - }); -} - -void Logic::PushFreeAssetComponentRefsCall( - const std::vector*>& components) { - thread()->PushCall([components] { - for (auto&& i : components) { - delete i; - } - }); -} - -void Logic::PushHavePendingLoadsDoneCall() { - thread()->PushCall([] { g_assets->ClearPendingLoadsDoneList(); }); -} - -void Logic::ToggleConsole() { - assert(InLogicThread()); - if (auto console = g_app->console) { - console->ToggleState(); - } -} - -void Logic::PushConsolePrintCall(const std::string& msg) { - thread()->PushCall([msg] { - // Send them to the console if its been created or store them - // for when it is (unless we're headless in which case it never will). - if (auto console = g_app->console) { - console->Print(msg); - } else if (!HeadlessMode()) { - g_app->console_startup_messages += msg; - } - }); -} - -void Logic::PushHavePendingLoadsCall() { - thread()->PushCall([this] { - have_pending_loads_ = true; - UpdateProcessTimer(); - }); -} - -void Logic::PushShutdownCall(bool soft) { - thread()->PushCall([this, soft] { Shutdown(soft); }); -} - -void Logic::Shutdown(bool soft) { - assert(InLogicThread()); - - if (!g_app->shutting_down) { - g_app->shutting_down = true; - - // Nuke the app if we get stuck shutting down. - Utils::StartSuicideTimer("shutdown", 10000); - - // Call our shutdown callback. - g_python->obj(Python::ObjID::kShutdownCall).Call(); - - connections_->Shutdown(); - - // Let's do the same stuff we do when our thread is pausing. (committing - // account-client to disk, etc). - OnThreadPause(); - - // Attempt to report/store outstanding log stuff. - g_app_internal->PutLog(false); - - // Ideally we'd want to give some of the above stuff - // a few seconds to complete, but just calling it done for now. - g_app_flavor->PushShutdownCompleteCall(); - } -} - -void Logic::ResetInput() { - assert(InLogicThread()); - g_input->ResetKeyboardHeldKeys(); - g_input->ResetJoyStickHeldButtons(); -} - -auto Logic::RemovePlayer(Player* player) -> void { - assert(InLogicThread()); - if (HostSession* host_session = player->GetHostSession()) { - host_session->RemovePlayer(player); - } else { - Log(LogLevel::kError, "Got RemovePlayer call but have no host_session"); - } -} - -auto Logic::NewRealTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> int { - int offset = 0; - Timer* t = realtimers_->NewTimer(GetRealTime(), length, offset, - repeat ? -1 : 0, runnable); - return t->id(); -} - -void Logic::DeleteRealTimer(int timer_id) { - realtimers_->DeleteTimer(timer_id); -} - -void Logic::SetRealTimerLength(int timer_id, millisecs_t length) { - Timer* t = realtimers_->GetTimer(timer_id); - if (t) { - t->SetLength(length); - } else { - Log(LogLevel::kError, - "Logic::SetRealTimerLength() called on nonexistent timer."); - } -} - -void Logic::Process() { - have_pending_loads_ = g_assets->RunPendingLoadsLogicThread(); - UpdateProcessTimer(); -} - -void Logic::SetLanguageKeys( - const std::unordered_map& language) { - assert(InLogicThread()); - { - std::scoped_lock lock(language_mutex_); - language_ = language; - } - - // Let's also inform existing session stuff so it can update itself. - if (Session* session = GetForegroundSession()) { - session->LanguageChanged(); - } - - // As well as existing UI stuff. - if (Widget* root_widget = g_ui->root_widget()) { - root_widget->OnLanguageChange(); - } - - // Also clear translations on all screen-messages. - g_graphics->ClearScreenMessageTranslations(); -} - -auto DoCompileResourceString(cJSON* obj) -> std::string { - // NOTE: We currently talk to Python here so need to be sure - // we're holding the GIL. Perhaps in the future we could handle this - // stuff completely in C++ and be free of this limitation. - assert(Python::HaveGIL()); - assert(obj != nullptr); - - std::string result; - - // If its got a "r" key, look it up as a resource.. (with optional fallback). - cJSON* resource = cJSON_GetObjectItem(obj, "r"); - if (resource == nullptr) { - resource = cJSON_GetObjectItem(obj, "resource"); - // As of build 14318, complain if we find long key names; hope to remove - // them soon. - if (resource != nullptr) { - static bool printed = false; - if (!printed) { - printed = true; - char* c = cJSON_Print(obj); - BA_LOG_ONCE( - LogLevel::kError, - "found long key 'resource' in raw lstr json: " + std::string(c)); - free(c); - } - } - } - if (resource != nullptr) { - // Look for fallback-resource. - cJSON* fallback_resource = cJSON_GetObjectItem(obj, "f"); - if (fallback_resource == nullptr) { - fallback_resource = cJSON_GetObjectItem(obj, "fallback"); - - // As of build 14318, complain if we find old long key names; hope to - // remove them soon. - if (fallback_resource != nullptr) { - static bool printed = false; - if (!printed) { - printed = true; - char* c = cJSON_Print(obj); - BA_LOG_ONCE( - LogLevel::kError, - "found long key 'fallback' in raw lstr json: " + std::string(c)); - free(c); - } - } - } - cJSON* fallback_value = cJSON_GetObjectItem(obj, "fv"); - result = g_python->GetResource( - resource->valuestring, - fallback_resource ? fallback_resource->valuestring : nullptr, - fallback_value ? fallback_value->valuestring : nullptr); - } else { - // Apparently not a resource; lets try as a translation ("t" keys). - cJSON* translate = cJSON_GetObjectItem(obj, "t"); - if (translate == nullptr) { - translate = cJSON_GetObjectItem(obj, "translate"); - - // As of build 14318, complain if we find long key names; hope to remove - // them soon. - if (translate != nullptr) { - static bool printed = false; - if (!printed) { - printed = true; - char* c = cJSON_Print(obj); - BA_LOG_ONCE( - LogLevel::kError, - "found long key 'translate' in raw lstr json: " + std::string(c)); - free(c); - } - } - } - if (translate != nullptr) { - if (translate->type != cJSON_Array - || cJSON_GetArraySize(translate) != 2) { - throw Exception("Expected a 2 member array for translate"); - } - cJSON* category = cJSON_GetArrayItem(translate, 0); - if (category->type != cJSON_String) { - throw Exception( - "First member of translate array (category) must be a string"); - } - cJSON* value = cJSON_GetArrayItem(translate, 1); - if (value->type != cJSON_String) { - throw Exception( - "Second member of translate array (value) must be a string"); - } - result = - g_python->GetTranslation(category->valuestring, value->valuestring); - } else { - // Lastly try it as a value ("value" or "v"). - // (can be useful for feeding explicit strings while still allowing - // translated subs - cJSON* value = cJSON_GetObjectItem(obj, "v"); - if (value == nullptr) { - value = cJSON_GetObjectItem(obj, "value"); - - // As of build 14318, complain if we find long key names; hope to remove - // them soon. - if (value != nullptr) { - static bool printed = false; - if (!printed) { - printed = true; - char* c = cJSON_Print(obj); - BA_LOG_ONCE( - LogLevel::kError, - "found long key 'value' in raw lstr json: " + std::string(c)); - free(c); - } - } - } - if (value != nullptr) { - if (value->type != cJSON_String) { - throw Exception("Expected a string for value"); - } - result = value->valuestring; - } else { - throw Exception("no 'resource', 'translate', or 'value' keys found"); - } - } - } - - // Ok; now no matter what it was, see if it contains any subs and replace - // them. - // ("subs" or "s") - cJSON* subs = cJSON_GetObjectItem(obj, "s"); - if (subs == nullptr) { - subs = cJSON_GetObjectItem(obj, "subs"); - - // As of build 14318, complain if we find long key names; hope to remove - // them soon. - if (subs != nullptr) { - static bool printed = false; - if (!printed) { - printed = true; - char* c = cJSON_Print(obj); - BA_LOG_ONCE(LogLevel::kError, "found long key 'subs' in raw lstr json: " - + std::string(c)); - free(c); - } - } - } - if (subs != nullptr) { - if (subs->type != cJSON_Array) { - throw Exception("expected an array for 'subs'"); - } - int subs_count = cJSON_GetArraySize(subs); - for (int i = 0; i < subs_count; i++) { - cJSON* sub = cJSON_GetArrayItem(subs, i); - if (sub->type != cJSON_Array || cJSON_GetArraySize(sub) != 2) { - throw Exception( - "Invalid subs entry; expected length 2 list of sub/replacement."); - } - - // First item should be a string. - cJSON* key = cJSON_GetArrayItem(sub, 0); - if (key->type != cJSON_String) { - throw Exception("Sub keys must be strings."); - } - std::string s_key = key->valuestring; - - // Second item can be a string or a dict; if its a dict, we go recursive. - cJSON* value = cJSON_GetArrayItem(sub, 1); - std::string s_val; - if (value->type == cJSON_String) { - s_val = value->valuestring; - } else if (value->type == cJSON_Object) { - s_val = DoCompileResourceString(value); - } else { - throw Exception("Sub values must be strings or dicts."); - } - - // Replace *ALL* occurrences. - // FIXME: Using this simple logic, If our replace value contains our - // search value we get an infinite loop. For now, just error in that case. - if (s_val.find(s_key) != std::string::npos) { - throw Exception("Subs replace string cannot contain search string."); - } - while (true) { - size_t pos = result.find(s_key); - if (pos == std::string::npos) { - break; - } - result.replace(pos, s_key.size(), s_val); - } - } - } - return result; -} - -auto Logic::CompileResourceString(const std::string& s, const std::string& loc, - bool* valid) -> std::string { - assert(g_python != nullptr); - - bool dummyvalid; - if (valid == nullptr) { - valid = &dummyvalid; - } - - // Quick out: if it doesn't start with a { and end with a }, treat it as a - // literal and just return it as-is. - if (s.size() < 2 || s[0] != '{' || s[s.size() - 1] != '}') { - *valid = true; - return s; - } - - cJSON* root = cJSON_Parse(s.c_str()); - if (root == nullptr) { - Log(LogLevel::kError, "CompileResourceString failed (loc " + loc - + "); invalid json: '" + s + "'"); - *valid = false; - return ""; - } - std::string result; - try { - result = DoCompileResourceString(root); - *valid = true; - } catch (const std::exception& e) { - Log(LogLevel::kError, "CompileResourceString failed (loc " + loc + "): " - + std::string(e.what()) + "; str='" + s + "'"); - result = ""; - *valid = false; - } - cJSON_Delete(root); - return result; -} - -auto Logic::GetResourceString(const std::string& key) -> std::string { - std::string val; - { - std::scoped_lock lock(language_mutex_); - auto i = language_.find(key); - if (i != language_.end()) { - val = i->second; - } - } - return val; -} - -auto Logic::CharStr(SpecialChar id) -> std::string { - std::scoped_lock lock(special_char_mutex_); - std::string val; - auto i = special_char_strings_.find(id); - if (i != special_char_strings_.end()) { - val = i->second; - } else { - BA_LOG_PYTHON_TRACE_ONCE("invalid key in CharStr(): '" - + std::to_string(static_cast(id)) + "'"); - val = "?"; - } - return val; -} - -auto Logic::ShouldAnnouncePartyJoinsAndLeaves() -> bool { - assert(InLogicThread()); - - // At the moment we don't announce these for public internet parties.. (too - // much noise). - return !public_party_enabled(); -} - -void Logic::CleanUpBeforeConnectingToHost() { - // We can't have connected clients and a host-connection at the same time. - // Make a minimal attempt to disconnect any client connections we have, but - // get them off the list immediately. - // FIXME: Should we have a 'purgatory' for dying client connections?.. - // (they may not get the single 'go away' packet we send here) - connections_->ForceDisconnectClients(); - - // Also make sure our public party state is off; this will inform the server - // that it should not be handing out our address to anyone. - assert(g_python); - SetPublicPartyEnabled(false); -} - -auto Logic::GetPartySize() const -> int { - assert(InLogicThread()); - assert(game_roster_ != nullptr); - return cJSON_GetArraySize(game_roster_); -} - -void Logic::LocalDisplayChatMessage(const std::vector& buffer) { - // 1 type byte, 1 spec-len byte, 1 or more spec chars, 0 or more msg chars. - if (buffer.size() > 3) { - size_t spec_len = buffer[1]; - if (spec_len > 0 && spec_len + 2 <= buffer.size()) { - size_t msg_len = buffer.size() - spec_len - 2; - std::vector b1(spec_len + 1); - memcpy(&(b1[0]), &(buffer[2]), spec_len); - b1[spec_len] = 0; - std::vector b2(msg_len + 1); - if (msg_len > 0) { - memcpy(&(b2[0]), &(buffer[2 + spec_len]), msg_len); - } - b2[msg_len] = 0; - - std::string final_message = - PlayerSpec(b1.data()).GetDisplayString() + ": " + b2.data(); - - // Store it locally. - chat_messages_.push_back(final_message); - while (chat_messages_.size() > kMaxChatMessages) { - chat_messages_.pop_front(); - } - - // Show it on the screen if they don't have their chat window open - // (and don't have chat muted). - if (!g_ui->root_ui()->party_window_open()) { - if (!chat_muted_) { - ScreenMessage(final_message, {0.7f, 1.0f, 0.7f}); - } - } else { - // Party window is open - notify it that there's a new message. - g_python->HandleLocalChatMessage(final_message); - } - if (!chat_muted_) { - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kTap)); - } - } - } -} - -auto Logic::GetGameRosterMessage() -> std::vector { - // This message is simply a flattened json string of our roster (including - // terminating char). - char* s = cJSON_PrintUnformatted(game_roster_); - auto s_len = strlen(s); - std::vector msg(1 + s_len + 1); - msg[0] = BA_MESSAGE_PARTY_ROSTER; - memcpy(&(msg[1]), s, s_len + 1); - free(s); - - return msg; -} - -auto Logic::IsPlayerBanned(const PlayerSpec& spec) -> bool { - millisecs_t current_time = GetRealTime(); - - // Now is a good time to prune no-longer-banned specs. - while (!banned_players_.empty() - && banned_players_.front().first < current_time) { - banned_players_.pop_front(); - } - // NOLINTNEXTLINE(readability-use-anyofallof) - for (auto&& test_spec : banned_players_) { - if (test_spec.second == spec) { - return true; - } - } - return false; -} - -void Logic::StartKickVote(ConnectionToClient* starter, - ConnectionToClient* target) { - // Restrict votes per client. - millisecs_t current_time = GetRealTime(); - - if (starter == target) { - // Don't let anyone kick themselves. - starter->SendScreenMessage(R"({"r":"kickVoteCantKickSelfText",)" - R"("f":"kickVoteFailedText"})", - 1, 0, 0); - } else if (target->IsAdmin()) { - // Admins are immune to kicking - starter->SendScreenMessage(R"({"r":"kickVoteCantKickAdminText",)" - R"("f":"kickVoteFailedText"})", - 1, 0, 0); - } else if (starter->IsAdmin()) { - // Admin doing the kicking succeeds instantly. - connections()->SendScreenMessageToClients( - R"({"r":"kickOccurredText","s":[["${NAME}",)" - + Utils::GetJSONString( - target->GetCombinedSpec().GetDisplayString().c_str()) - + "]]}", - 1, 1, 0); - connections()->DisconnectClient(target->id(), kKickBanSeconds); - starter->SendScreenMessage(R"({"r":"kickVoteCantKickAdminText",)" - R"("f":"kickVoteFailedText"})", - 1, 0, 0); - } else if (!kick_voting_enabled_) { - // No kicking otherwise if its disabled. - starter->SendScreenMessage(R"({"r":"kickVotingDisabledText",)" - R"("f":"kickVoteFailedText"})", - 1, 0, 0); - } else if (kick_vote_in_progress_) { - // Vote in progress error. - starter->SendScreenMessage(R"({"r":"voteInProgressText"})", 1, 0, 0); - } else if (connections()->GetConnectedClientCount() - < kKickVoteMinimumClients) { - // There's too few clients to effectively vote. - starter->SendScreenMessage(R"({"r":"kickVoteFailedNotEnoughVotersText",)" - R"("f":"kickVoteFailedText"})", - 1, 0, 0); - } else if (current_time < starter->next_kick_vote_allow_time()) { - // Not yet allowed error. - starter->SendScreenMessage( - R"({"r":"voteDelayText","s":[["${NUMBER}",")" - + std::to_string(std::max( - millisecs_t{1}, - (starter->next_kick_vote_allow_time() - current_time) / 1000)) - + "\"]]}", - 1, 0, 0); - } else { - std::vector connected_clients = - connections()->GetConnectionsToClients(); - - // Ok, kick off a vote.. (send the question and instructions to everyone - // except the starter and the target). - for (auto&& client : connected_clients) { - if (client != starter && client != target) { - client->SendScreenMessage( - R"({"r":"kickQuestionText","s":[["${NAME}",)" - + Utils::GetJSONString( - target->GetCombinedSpec().GetDisplayString().c_str()) - + "]]}", - 1, 1, 0); - client->SendScreenMessage(R"({"r":"kickWithChatText","s":)" - R"([["${YES}","'1'"],["${NO}","'0'"]]})", - 1, 1, 0); - } else { - // For the kicker/kickee, simply print that a kick vote has been - // started. - client->SendScreenMessage( - R"({"r":"kickVoteStartedText","s":[["${NAME}",)" - + Utils::GetJSONString( - target->GetCombinedSpec().GetDisplayString().c_str()) - + "]]}", - 1, 1, 0); - } - } - kick_vote_end_time_ = current_time + kKickVoteDuration; - kick_vote_in_progress_ = true; - last_kick_votes_needed_ = -1; // make sure we print starting num - - // Keep track of who started the vote. - kick_vote_starter_ = starter; - kick_vote_target_ = target; - - // Reset votes for all connected clients. - for (ConnectionToClient* client : - connections()->GetConnectionsToClients()) { - if (client == starter) { - client->set_kick_voted(true); - client->set_kick_vote_choice(true); - } else { - client->set_kick_voted(false); - } - } - } -} - -void Logic::BanPlayer(const PlayerSpec& spec, millisecs_t duration) { - banned_players_.emplace_back(GetRealTime() + duration, spec); -} - -void Logic::UpdateGameRoster() { - assert(InLogicThread()); - - assert(game_roster_ != nullptr); - if (game_roster_ != nullptr) { - cJSON_Delete(game_roster_); - } - - // Our party-roster is just a json array of dicts containing player-specs. - game_roster_ = cJSON_CreateArray(); - - int total_party_size = 1; // include ourself here.. - - // Add ourself first (that's currently how they know we're the party leader) - // ..but only if we have a connected client (otherwise our party is - // considered 'empty'). - - // UPDATE: starting with our big ui revision we'll always include ourself - // here - bool include_self = (connections()->GetConnectedClientCount() > 0); - -#if BA_TOOLBAR_TEST - include_self = true; -#endif // BA_TOOLBAR_TEST - - if (auto* hs = dynamic_cast(GetForegroundSession())) { - // Add our host-y self. - if (include_self) { - cJSON* client_dict = cJSON_CreateObject(); - cJSON_AddItemToObject( - client_dict, "spec", - cJSON_CreateString( - PlayerSpec::GetAccountPlayerSpec().GetSpecString().c_str())); - - // Add our list of local players. - cJSON* player_array = cJSON_CreateArray(); - for (auto&& p : hs->players()) { - InputDevice* input_device = p->GetInputDevice(); - - // Add some basic info for each local player (only ones with real - // names though; don't wanna send , etc). - if (p->accepted() && p->name_is_real() && input_device != nullptr - && !input_device->IsRemoteClient()) { - cJSON* player_dict = cJSON_CreateObject(); - cJSON_AddItemToObject(player_dict, "n", - cJSON_CreateString(p->GetName().c_str())); - cJSON_AddItemToObject(player_dict, "nf", - cJSON_CreateString(p->GetName(true).c_str())); - cJSON_AddItemToObject(player_dict, "i", cJSON_CreateNumber(p->id())); - cJSON_AddItemToArray(player_array, player_dict); - } - } - cJSON_AddItemToObject(client_dict, "p", player_array); - cJSON_AddItemToObject( - client_dict, "i", - cJSON_CreateNumber(-1)); // -1 client_id means we're the host. - cJSON_AddItemToArray(game_roster_, client_dict); - } - - // Add all connected clients. - for (auto&& i : connections()->connections_to_clients()) { - if (i.second->can_communicate()) { - cJSON* client_dict = cJSON_CreateObject(); - cJSON_AddItemToObject( - client_dict, "spec", - cJSON_CreateString(i.second->peer_spec().GetSpecString().c_str())); - - // Add their list of players. - cJSON* player_array = cJSON_CreateArray(); - - // Include all players that are remote and coming from this same - // client connection. - for (auto&& p : hs->players()) { - InputDevice* input_device = p->GetInputDevice(); - if (p->accepted() && p->name_is_real() && input_device != nullptr - && input_device->IsRemoteClient()) { - auto* cid = static_cast(input_device); - ConnectionToClient* ctc = cid->connection_to_client(); - - // Add some basic info for each remote player. - if (ctc != nullptr && ctc == i.second.get()) { - cJSON* player_dict = cJSON_CreateObject(); - cJSON_AddItemToObject(player_dict, "n", - cJSON_CreateString(p->GetName().c_str())); - cJSON_AddItemToObject( - player_dict, "nf", - cJSON_CreateString(p->GetName(true).c_str())); - cJSON_AddItemToObject(player_dict, "i", - cJSON_CreateNumber(p->id())); - cJSON_AddItemToArray(player_array, player_dict); - } - } - } - cJSON_AddItemToObject(client_dict, "p", player_array); - cJSON_AddItemToObject(client_dict, "i", - cJSON_CreateNumber(i.second->id())); - cJSON_AddItemToArray(game_roster_, client_dict); - total_party_size += 1; - } - } - } - - // Keep the Python layer informed on our number of connections; it may want - // to pass the info along to the master server if we're hosting a public - // party. - SetPublicPartySize(total_party_size); - - // Mark the roster as dirty so we know we need to send it to everyone soon. - game_roster_dirty_ = true; -} - -void Logic::SetPublicPartyEnabled(bool val) { - assert(InLogicThread()); - if (val == public_party_enabled_) { - return; - } - public_party_enabled_ = val; - g_app_internal->PushPublicPartyState(); -} - -void Logic::SetPublicPartySize(int count) { - assert(InLogicThread()); - if (count == public_party_size_) { - return; - } - public_party_size_ = count; - - // Push our new state to the server *ONLY* if public-party is turned on - // (wasteful otherwise). - if (public_party_enabled_) { - g_app_internal->PushPublicPartyState(); - } -} - -auto Logic::SetPublicPartyQueueEnabled(bool enabled) -> void { - assert(InLogicThread()); - if (enabled == public_party_queue_enabled_) { - return; - } - public_party_queue_enabled_ = enabled; - - // Push our new state to the server *ONLY* if public-party is turned on - // (wasteful otherwise). - if (public_party_enabled_) { - g_app_internal->PushPublicPartyState(); - } -} - -void Logic::SetPublicPartyMaxSize(int count) { - assert(InLogicThread()); - if (count == public_party_max_size_) { - return; - } - public_party_max_size_ = count; - - // Push our new state to the server *ONLY* if public-party is turned on - // (wasteful otherwise). - if (public_party_enabled_) { - g_app_internal->PushPublicPartyState(); - } -} - -void Logic::SetPublicPartyName(const std::string& name) { - assert(InLogicThread()); - if (name == public_party_name_) { - return; - } - public_party_name_ = name; - - // Push our new state to the server *ONLY* if public-party is turned on - // (wasteful otherwise). - if (public_party_enabled_) { - g_app_internal->PushPublicPartyState(); - } -} - -void Logic::SetPublicPartyStatsURL(const std::string& url) { - assert(InLogicThread()); - if (url == public_party_stats_url_) { - return; - } - public_party_stats_url_ = url; - - // Push our new state to the server *ONLY* if public-party is turned on - // (wasteful otherwise). - if (public_party_enabled_) { - g_app_internal->PushPublicPartyState(); - } -} - -void Logic::SetPublicPartyPlayerCount(int count) { - assert(InLogicThread()); - if (count == public_party_player_count_) { - return; - } - public_party_player_count_ = count; - - // Push our new state to the server *ONLY* if public-party is turned on - // (wasteful otherwise). - if (public_party_enabled_) { - g_app_internal->PushPublicPartyState(); - } -} - -} // namespace ballistica diff --git a/src/ballistica/logic/logic.h b/src/ballistica/logic/logic.h deleted file mode 100644 index 7ba31f1d..00000000 --- a/src/ballistica/logic/logic.h +++ /dev/null @@ -1,339 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_LOGIC_LOGIC_H_ -#define BALLISTICA_LOGIC_LOGIC_H_ - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ballistica/core/object.h" - -namespace ballistica { - -const int kMaxPartyNameCombinedSize = 25; - -/// The logic subsystem of the app. This runs on a dedicated thread -/// and is where high level app logic happens. Much app functionality -/// including UI calls must be run on the logic thread. -class Logic { - public: - Logic(); - auto OnAppStart() -> void; - - auto LaunchHostSession(PyObject* session_type_obj, - BenchmarkType benchmark_type = BenchmarkType::kNone) - -> void; - auto LaunchClientSession() -> void; - auto LaunchReplaySession(const std::string& file_name) -> void; - - auto PushSetV1LoginCall(V1AccountType account_type, - V1LoginState account_state, - const std::string& account_name, - const std::string& account_id) -> void; - auto PushInitialScreenCreatedCall() -> void; - auto PushApplyConfigCall() -> void; - auto PushRemoveGraphicsServerRenderHoldCall() -> void; - auto PushInterruptSignalCall() -> void; - - /// Notify the game of a screen-size change (used by the graphics server). - auto PushScreenResizeCall(float virtual_width, float virtual_height, - float physical_width, float physical_height) - -> void; - - auto PushGameServiceAchievementListCall( - const std::set& achievements) -> void; - auto PushToggleCollisionGeometryDisplayCall() -> void; - auto PushToggleDebugInfoDisplayCall() -> void; - auto PushToggleManualCameraCall() -> void; - auto PushHavePendingLoadsDoneCall() -> void; - auto PushFreeAssetComponentRefsCall( - const std::vector*>& components) -> void; - auto PushHavePendingLoadsCall() -> void; - auto PushShutdownCall(bool soft) -> void; - - auto PushInGameConsoleScriptCommand(const std::string& command) -> void; - auto ToggleConsole() -> void; - auto PushConsolePrintCall(const std::string& msg) -> void; - auto PushStdinScriptCommand(const std::string& command) -> void; - auto PushMediaPruneCall(int level) -> void; - auto PushAskUserForTelnetAccessCall() -> void; - - // Push Python call and keep it alive; must be called from logic thread. - auto PushPythonCall(const Object::Ref& call) -> void; - auto PushPythonCallArgs(const Object::Ref& call, - const PythonRef& args) -> void; - - // Push Python call without keeping it alive; must be called from logic - // thread. - auto PushPythonWeakCall(const Object::WeakRef& call) - -> void; - auto PushPythonWeakCallArgs(const Object::WeakRef& call, - const PythonRef& args) -> void; - - // Push a raw Python call from any thread. Refcount should be incremented - // beforehand and will be decremented after running. - auto PushPythonRawCallable(PyObject* callable, bool fg_context = false) - -> void; - auto PushScreenMessage(const std::string& message, const Vector3f& color) - -> void; - auto RemovePlayer(Player* player) -> void; - auto PushPlaySoundCall(SystemSoundID sound) -> void; - auto PushConfirmQuitCall() -> void; - auto PushStringEditSetCall(const std::string& value) -> void; - auto PushStringEditCancelCall() -> void; - auto PushShowURLCall(const std::string& url) -> void; - auto PushOnAppResumeCall() -> void; - auto PushFrameDefRequest() -> void; - auto ChangeGameSpeed(int offs) -> void; - auto ResetInput() -> void; - auto RunMainMenu() -> void; - auto OnThreadPause() -> void; - -#if BA_VR_BUILD - auto PushVRHandsState(const VRHandsState& state) -> void; - const VRHandsState& vr_hands_state() const { return vr_hands_state_; } -#endif - - // Resets tracking used to detect cheating and tampering in local tournaments. - auto ResetActivityTracking() -> void; - - // Return whichever context is front and center. - auto GetForegroundContext() -> Context; - - // Return whichever session is front and center. - auto GetForegroundSession() const -> Session* { - return foreground_session_.get(); - } - - auto NewRealTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> int; - auto DeleteRealTimer(int timer_id) -> void; - auto SetRealTimerLength(int timer_id, millisecs_t length) -> void; - auto SetLanguageKeys( - const std::unordered_map& language) -> void; - auto GetResourceString(const std::string& key) -> std::string; - auto CharStr(SpecialChar id) -> std::string; - auto CompileResourceString(const std::string& s, const std::string& loc, - bool* valid = nullptr) -> std::string; - auto kick_idle_players() const -> bool { return kick_idle_players_; } - auto IsInUIContext() const -> bool; - - // Return the actual UI context (hmm couldn't we just use g_ui?). - auto GetUIContextTarget() const -> UI* { - assert(g_ui); - return g_ui; - } - - // Simply return a context-state pointing to the ui-context (so you don't have - // to include the ui header). - auto GetUIContext() const -> Context; - - // Returns the base time used to drive local sims/etc. This generally tries - // to match real-time but has a bit of leeway to sync up with frame drawing or - // slow down if things are behind (it tries to progress by exactly 1000/60 ms - // each frame, provided we're rendering 60hz). - auto master_time() const -> millisecs_t { return master_time_; } - - auto debug_speed_mult() const -> float { return debug_speed_mult_; } - auto SetDebugSpeedExponent(int val) -> void; - - auto SetReplaySpeedExponent(int val) -> void; - auto replay_speed_exponent() const -> int { return replay_speed_exponent_; } - auto replay_speed_mult() const -> float { return replay_speed_mult_; } - - auto GetPartySize() const -> int; - auto last_connection_to_client_join_time() const -> millisecs_t { - return last_connection_to_client_join_time_; - } - auto set_last_connection_to_client_join_time(millisecs_t val) -> void { - last_connection_to_client_join_time_ = val; - } - - auto game_roster() const -> cJSON* { return game_roster_; } - - auto chat_messages() const -> const std::list& { - return chat_messages_; - } - - // Used to know which globals is in control currently/etc. - auto GetForegroundScene() const -> Scene* { - assert(InLogicThread()); - return foreground_scene_.get(); - } - auto SetForegroundScene(Scene* sg) -> void; - - auto UpdateGameRoster() -> void; - auto IsPlayerBanned(const PlayerSpec& spec) -> bool; - auto BanPlayer(const PlayerSpec& spec, millisecs_t duration) -> void; - - // For cheat detection. Returns the largest amount of time that has passed - // between frames since our last reset (for detecting memory modification - // UIs/etc). - auto largest_draw_time_increment() const -> millisecs_t { - return largest_draw_time_increment_since_last_reset_; - } - - // Anti-hacker stuff. - auto GetTotalTimeSinceReset() const -> millisecs_t { - return last_draw_real_time_ - first_draw_real_time_; - } - auto SetForegroundSession(Session* s) -> void; - auto SetGameRoster(cJSON* r) -> void; - auto LocalDisplayChatMessage(const std::vector& buffer) -> void; - auto ShouldAnnouncePartyJoinsAndLeaves() -> bool; - - auto StartKickVote(ConnectionToClient* starter, ConnectionToClient* target) - -> void; - auto require_client_authentication() const { - return require_client_authentication_; - } - auto set_require_client_authentication(bool enable) -> void { - require_client_authentication_ = enable; - } - auto set_kick_voting_enabled(bool enable) -> void { - kick_voting_enabled_ = enable; - } - auto set_admin_public_ids(const std::set& ids) -> void { - admin_public_ids_ = ids; - } - const std::set& admin_public_ids() const { - return admin_public_ids_; - } - - auto kick_vote_in_progress() const -> bool { return kick_vote_in_progress_; } - - auto SetPublicPartyEnabled(bool val) -> void; - auto public_party_enabled() const { return public_party_enabled_; } - auto public_party_size() const { return public_party_size_; } - auto SetPublicPartySize(int count) -> void; - auto public_party_max_size() const { return public_party_max_size_; } - auto SetPublicPartyQueueEnabled(bool enabled) -> void; - auto public_party_queue_enabled() const { - return public_party_queue_enabled_; - } - auto public_party_max_player_count() const { - return public_party_max_player_count_; - } - auto public_party_min_league() const -> const std::string& { - return public_party_min_league_; - } - auto public_party_stats_url() const -> const std::string& { - return public_party_stats_url_; - } - auto SetPublicPartyMaxSize(int count) -> void; - auto SetPublicPartyName(const std::string& name) -> void; - auto SetPublicPartyStatsURL(const std::string& name) -> void; - auto public_party_name() const { return public_party_name_; } - auto public_party_player_count() const { return public_party_player_count_; } - auto SetPublicPartyPlayerCount(int count) -> void; - auto ran_app_launch_commands() const { return ran_app_launch_commands_; } - auto CleanUpBeforeConnectingToHost() -> void; - auto connections() -> ConnectionSet* { - assert(connections_.get()); - return connections_.get(); - } - auto mark_game_roster_dirty() -> void { game_roster_dirty_ = true; } - auto thread() const -> Thread* { return thread_; } - - private: - auto OnAppStartInThread() -> void; - auto HandleQuitOnIdle() -> void; - auto InitSpecialChars() -> void; - auto Draw() -> void; - auto InitialScreenCreated() -> void; - auto ScreenResize(float virtual_width, float virtual_height, - float pixel_width, float pixel_height) -> void; - auto GameServiceAchievementList(const std::set& achievements) - -> void; - - auto PruneMedia() -> void; - auto Update() -> void; - auto Process() -> void; - auto UpdateKickVote() -> void; - auto RunAppLaunchCommands() -> void; - auto PruneSessions() -> void; - auto ApplyConfig() -> void; - auto UpdateProcessTimer() -> void; - auto Reset() -> void; - auto GetGameRosterMessage() -> std::vector; - auto Shutdown(bool soft) -> void; - -#if BA_VR_BUILD - VRHandsState vr_hands_state_; -#endif -#if BA_RIFT_BUILD - int rift_step_index_{}; -#endif - - Thread* thread_{}; - std::unique_ptr connections_; - std::list > banned_players_; - std::list chat_messages_; - bool chat_muted_{}; - bool first_update_{true}; - bool game_roster_dirty_{}; - millisecs_t last_connection_to_client_join_time_{}; - int debug_speed_exponent_{}; - float debug_speed_mult_{1.0f}; - int replay_speed_exponent_{}; - float replay_speed_mult_{1.0f}; - bool have_sent_initial_frame_def_{}; - millisecs_t master_time_{}; - millisecs_t master_time_offset_{}; - millisecs_t last_session_update_master_time_{}; - millisecs_t last_game_roster_send_time_{}; - millisecs_t largest_draw_time_increment_since_last_reset_{}; - millisecs_t last_draw_real_time_{}; - millisecs_t first_draw_real_time_{}; - millisecs_t next_long_update_report_time_{}; - - // *All* existing sessions (including old ones waiting to shut down). - std::vector > sessions_; - Object::WeakRef foreground_scene_; - Object::WeakRef foreground_session_; - std::mutex language_mutex_; - std::unordered_map language_; - std::mutex special_char_mutex_; - std::unordered_map special_char_strings_; - bool ran_app_launch_commands_{}; - bool kick_idle_players_{}; - std::optional idle_exit_minutes_{}; - bool idle_exiting_{}; - std::unique_ptr realtimers_; - Timer* process_timer_{}; - Timer* headless_update_timer_{}; - Timer* media_prune_timer_{}; - Timer* debug_timer_{}; - bool have_pending_loads_{}; - bool in_update_{}; - bool require_client_authentication_{}; - bool kick_voting_enabled_{true}; - std::set admin_public_ids_; - - cJSON* game_roster_{}; - millisecs_t kick_vote_end_time_{}; - bool kick_vote_in_progress_{}; - int last_kick_votes_needed_{-1}; - Object::WeakRef kick_vote_starter_; - Object::WeakRef kick_vote_target_; - bool public_party_enabled_{}; - int public_party_size_{1}; // Always count ourself (is that what we want?). - int public_party_max_size_{8}; - bool public_party_queue_enabled_{true}; - int public_party_player_count_{0}; - int public_party_max_player_count_{8}; - std::string public_party_name_; - std::string public_party_min_league_; - std::string public_party_stats_url_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_LOGIC_LOGIC_H_ diff --git a/src/ballistica/logic/session/client_session.h b/src/ballistica/logic/session/client_session.h deleted file mode 100644 index 2536848d..00000000 --- a/src/ballistica/logic/session/client_session.h +++ /dev/null @@ -1,138 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_LOGIC_SESSION_CLIENT_SESSION_H_ -#define BALLISTICA_LOGIC_SESSION_CLIENT_SESSION_H_ - -#include -#include -#include - -#include "ballistica/logic/client_controller_interface.h" -#include "ballistica/logic/session/session.h" - -namespace ballistica { - -class ClientSession : public Session { - public: - ClientSession(); - ~ClientSession() override; - - // Allows for things like replay speed. - virtual auto GetActualTimeAdvance(int advance_in) -> int { - return advance_in; - } - auto Update(int time_advance) -> void override; - auto Draw(FrameDef* f) -> void override; - virtual auto HandleSessionMessage(const std::vector& buffer) -> void; - auto Reset(bool rewind) -> void; - auto GetForegroundContext() -> Context override; - auto DoesFillScreen() const -> bool override; - auto ScreenSizeChanged() -> void override; - auto LanguageChanged() -> void override; - auto GetCorrectionMessages(bool blend, - std::vector >* messages) - -> void; - - /// Called when attempting to step without input data available. - virtual auto OnCommandBufferUnderrun() -> void {} - - virtual auto OnBaseTimeStepAdded(int step) -> void {} - - // Returns existing objects; throws exceptions if not available. - auto GetScene(int id) const -> Scene*; - auto GetNode(int id) const -> Node*; - auto GetTexture(int id) const -> Texture*; - auto GetModel(int id) const -> Model*; - auto GetCollideModel(int id) const -> CollideModel*; - auto GetMaterial(int id) const -> Material*; - auto GetSound(int id) const -> Sound*; - - auto base_time_buffered() const { return base_time_buffered_; } - auto consume_rate() const { return consume_rate_; } - auto set_consume_rate(float val) { consume_rate_ = val; } - auto target_base_time() const { return target_base_time_; } - auto base_time() const { return base_time_; } - auto shutting_down() const { return shutting_down_; } - - auto scenes() const -> const std::vector >& { - return scenes_; - } - auto nodes() const -> const std::vector >& { - return nodes_; - } - auto textures() const -> const std::vector >& { - return textures_; - } - auto models() const -> const std::vector >& { - return models_; - } - auto sounds() const -> const std::vector >& { - return sounds_; - } - auto collide_models() const - -> const std::vector >& { - return collide_models_; - } - auto materials() const -> const std::vector >& { - return materials_; - } - auto commands() const -> const std::list >& { - return commands_; - } - auto add_end_of_file_command() { - commands_.emplace_back(1, static_cast(SessionCommand::kEndOfFile)); - } - virtual auto OnReset(bool rewind) -> void; - virtual auto FetchMessages() -> void {} - virtual void Error(const std::string& description); - auto End() -> void; - auto DumpFullState(SceneStream* out) -> void override; - - /// Reset target base time to equal current. This can be used during command - /// buffer underruns to cause playback to pause momentarily instead of - /// skipping ahead to catch up. Generally desired for replays but not for - /// net-play. - auto ResetTargetBaseTime() -> void { target_base_time_ = base_time_; } - - private: - auto ClearSessionObjs() -> void; - auto AddCommand(const std::vector& command) -> void; - - auto ReadByte() -> uint8_t; - auto ReadInt32() -> int32_t; - auto ReadInt32_2(int32_t* vals) -> void; - auto ReadInt32_3(int32_t* vals) -> void; - auto ReadInt32_4(int32_t* vals) -> void; - auto ReadString() -> std::string; - auto ReadFloat() -> float; - auto ReadFloats(int count, float* vals) -> void; - auto ReadInt32s(int count, int32_t* vals) -> void; - auto ReadChars(int count, char* vals) -> void; - - // Ready-to-go commands. - std::list > commands_; - - // Commands being built up for the next time step (we need to ship timesteps - // as a whole). - std::list > commands_pending_; - std::vector current_cmd_; - uint8_t* current_cmd_ptr_{}; - int base_time_buffered_{}; - bool shutting_down_{}; - - millisecs_t base_time_{}; - double target_base_time_{}; - float consume_rate_{1.0f}; - - std::vector > scenes_; - std::vector > nodes_; - std::vector > textures_; - std::vector > models_; - std::vector > sounds_; - std::vector > collide_models_; - std::vector > materials_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_LOGIC_SESSION_CLIENT_SESSION_H_ diff --git a/src/ballistica/logic/session/replay_client_session.h b/src/ballistica/logic/session/replay_client_session.h deleted file mode 100644 index 5e03044a..00000000 --- a/src/ballistica/logic/session/replay_client_session.h +++ /dev/null @@ -1,42 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_LOGIC_SESSION_REPLAY_CLIENT_SESSION_H_ -#define BALLISTICA_LOGIC_SESSION_REPLAY_CLIENT_SESSION_H_ - -#include -#include - -#include "ballistica/logic/client_controller_interface.h" -#include "ballistica/logic/session/client_session.h" - -namespace ballistica { - -// A client-session fed by a connection to a host. -class ReplayClientSession : public ClientSession, - public ClientControllerInterface { - public: - explicit ReplayClientSession(std::string filename); - ~ReplayClientSession() override; - auto OnReset(bool rewind) -> void override; - - // Our ClientControllerInterface implementation. - auto GetActualTimeAdvance(int advance_in) -> int override; - auto OnClientConnected(ConnectionToClient* c) -> void override; - auto OnClientDisconnected(ConnectionToClient* c) -> void override; - auto OnCommandBufferUnderrun() -> void override; - - auto Error(const std::string& description) -> void override; - auto FetchMessages() -> void override; - - private: - uint32_t message_fetch_num_{}; - bool have_sent_client_message_{}; - std::vector connections_to_clients_; - std::vector connections_to_clients_ignored_; - std::string file_name_; - FILE* file_{}; -}; - -} // namespace ballistica - -#endif // BALLISTICA_LOGIC_SESSION_REPLAY_CLIENT_SESSION_H_ diff --git a/src/ballistica/logic/session/session.cc b/src/ballistica/logic/session/session.cc deleted file mode 100644 index a785e291..00000000 --- a/src/ballistica/logic/session/session.cc +++ /dev/null @@ -1,38 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/logic/session/session.h" - -#include "ballistica/app/app.h" -#include "ballistica/logic/logic.h" - -namespace ballistica { - -Session::Session() { - g_app->session_count++; - - // New sessions immediately become foreground. - g_logic->SetForegroundSession(this); -} - -Session::~Session() { g_app->session_count--; } - -void Session::Update(int time_advance) {} - -auto Session::GetForegroundContext() -> Context { return Context(); } - -void Session::Draw(FrameDef*) {} - -void Session::ScreenSizeChanged() {} - -void Session::LanguageChanged() {} - -void Session::GraphicsQualityChanged(GraphicsQuality q) {} - -void Session::DebugSpeedMultChanged() {} - -void Session::DumpFullState(SceneStream* out) { - Log(LogLevel::kError, - "Session::DumpFullState() being called; shouldn't happen."); -} - -} // namespace ballistica diff --git a/src/ballistica/logic/session/session.h b/src/ballistica/logic/session/session.h deleted file mode 100644 index e212b79f..00000000 --- a/src/ballistica/logic/session/session.h +++ /dev/null @@ -1,44 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_LOGIC_SESSION_SESSION_H_ -#define BALLISTICA_LOGIC_SESSION_SESSION_H_ - -#include "ballistica/core/context.h" -#include "ballistica/core/object.h" - -namespace ballistica { - -class Session : public ContextTarget { - public: - Session(); - ~Session() override; - - // Update the session. Should return real milliseconds until next - // update is needed. - virtual void Update(int time_advance); - - // If this returns false, the screen will be cleared as part of rendering. - virtual auto DoesFillScreen() const -> bool = 0; - - // Draw!!! - virtual void Draw(FrameDef* f); - - // Return the 'frontmost' context in the session. - // This is used for executing console command or other UI hotkeys that should - // apply to whatever the user is seeing. - virtual auto GetForegroundContext() -> Context; - virtual void ScreenSizeChanged(); - virtual void LanguageChanged(); - virtual void GraphicsQualityChanged(GraphicsQuality q); - virtual void DebugSpeedMultChanged(); - auto benchmark_type() const -> BenchmarkType { return benchmark_type_; } - void set_benchmark_type(BenchmarkType val) { benchmark_type_ = val; } - virtual void DumpFullState(SceneStream* s); - - private: - BenchmarkType benchmark_type_ = BenchmarkType::kNone; -}; - -} // namespace ballistica - -#endif // BALLISTICA_LOGIC_SESSION_SESSION_H_ diff --git a/src/ballistica/math/random.h b/src/ballistica/math/random.h deleted file mode 100644 index 4418dfd0..00000000 --- a/src/ballistica/math/random.h +++ /dev/null @@ -1,17 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_MATH_RANDOM_H_ -#define BALLISTICA_MATH_RANDOM_H_ - -namespace ballistica { - -class Random { - public: - static void GenList1D(float* list, int size); - static void GenList2D(float (*list)[2], int size); - static void GenList3D(float (*list)[3], int size); -}; - -} // namespace ballistica - -#endif // BALLISTICA_MATH_RANDOM_H_ diff --git a/src/ballistica/networking/network_writer.cc b/src/ballistica/networking/network_writer.cc deleted file mode 100644 index 3050e8b5..00000000 --- a/src/ballistica/networking/network_writer.cc +++ /dev/null @@ -1,35 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/networking/network_writer.h" - -#include "ballistica/core/thread.h" -#include "ballistica/networking/networking.h" -#include "ballistica/networking/sockaddr.h" - -namespace ballistica { - -NetworkWriter::NetworkWriter() { - // We're a singleton; make sure we don't already exist. - assert(g_network_writer == nullptr); - - // Spin up our thread. - thread_ = new Thread(ThreadTag::kNetworkWrite); - g_app->pausable_threads.push_back(thread_); -} - -void NetworkWriter::PushSendToCall(const std::vector& msg, - const SockAddr& addr) { - // Avoid buffer-full errors if something is causing us to write too often; - // these are unreliable messages so its ok to just drop them. - if (!thread()->CheckPushSafety()) { - BA_LOG_ONCE(LogLevel::kError, - "Excessive send-to calls in net-write-module."); - return; - } - thread()->PushCall([this, msg, addr] { - assert(g_network_reader); - Networking::SendTo(msg, addr); - }); -} - -} // namespace ballistica diff --git a/src/ballistica/networking/network_writer.h b/src/ballistica/networking/network_writer.h deleted file mode 100644 index a67c336b..00000000 --- a/src/ballistica/networking/network_writer.h +++ /dev/null @@ -1,25 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_NETWORKING_NETWORK_WRITER_H_ -#define BALLISTICA_NETWORKING_NETWORK_WRITER_H_ - -#include - -#include "ballistica/ballistica.h" - -namespace ballistica { - -// A subsystem handling outbound network traffic. -class NetworkWriter { - public: - NetworkWriter(); - void PushSendToCall(const std::vector& msg, const SockAddr& addr); - auto thread() const -> Thread* { return thread_; } - - private: - Thread* thread_{}; -}; - -} // namespace ballistica - -#endif // BALLISTICA_NETWORKING_NETWORK_WRITER_H_ diff --git a/src/ballistica/networking/networking.cc b/src/ballistica/networking/networking.cc deleted file mode 100644 index 7af90cf3..00000000 --- a/src/ballistica/networking/networking.cc +++ /dev/null @@ -1,270 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/networking/networking.h" - -#include "ballistica/app/app.h" -#include "ballistica/logic/player_spec.h" -#include "ballistica/networking/network_reader.h" -#include "ballistica/networking/sockaddr.h" -#include "ballistica/platform/platform.h" - -namespace ballistica { - -struct Networking::ScanResultsEntryPriv { - PlayerSpec player_spec; - std::string address; - uint32_t last_query_id{}; - millisecs_t last_contact_time{}; -}; - -Networking::Networking() {} - -// Note: for now we're making our host-scan network calls directly from the game -// thread. This is generally not a good idea since it appears that even in -// non-blocking mode they're still blocking for 3-4ms sometimes. But for now -// since this is only used minimally and only while in the UI i guess it's ok. -void Networking::HostScanCycle() { - assert(InLogicThread()); - - // We need to create a scanner socket - an ipv4 socket we can send out - // broadcast messages from. - if (scan_socket_ == -1) { - scan_socket_ = socket(AF_INET, SOCK_DGRAM, 0); - - if (scan_socket_ == -1) { - Log(LogLevel::kError, "Error opening scan socket: " - + g_platform->GetSocketErrorString() + "."); - return; - } - - // Since this guy lives in the game-thread we need it to not block. - if (!g_platform->SetSocketNonBlocking(scan_socket_)) { - Log(LogLevel::kError, "Error setting socket non-blocking."); - g_platform->CloseSocket(scan_socket_); - scan_socket_ = -1; - return; - } - - // Bind to whatever. - struct sockaddr_in serv_addr {}; - memset(&serv_addr, 0, sizeof(serv_addr)); - serv_addr.sin_family = AF_INET; - serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // NOLINT - serv_addr.sin_port = 0; // any - int result = - ::bind(scan_socket_, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); - if (result == 1) { - Log(LogLevel::kError, - "Error binding socket: " + g_platform->GetSocketErrorString() + "."); - g_platform->CloseSocket(scan_socket_); - scan_socket_ = -1; - return; - } - - // Enable broadcast on the socket. - BA_SOCKET_SETSOCKOPT_VAL_TYPE op_val{1}; - result = setsockopt(scan_socket_, SOL_SOCKET, SO_BROADCAST, &op_val, - sizeof(op_val)); - - if (result != 0) { - Log(LogLevel::kError, "Error enabling broadcast for scan-socket: " - + g_platform->GetSocketErrorString() + "."); - g_platform->CloseSocket(scan_socket_); - scan_socket_ = -1; - return; - } - } - - // Ok we've got a valid scanner socket. Now lets send out broadcast pings on - // all available networks. - std::vector addrs = g_platform->GetBroadcastAddrs(); - for (auto&& i : addrs) { - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_port = htons(kDefaultPort); // NOLINT - addr.sin_addr.s_addr = htonl(i); // NOLINT - - // Include our query id (so we can sort out which responses come back - // quickest). - uint8_t data[5]; - data[0] = BA_PACKET_GAME_QUERY; - memcpy(data + 1, &next_scan_query_id_, 4); - BA_DEBUG_TIME_CHECK_BEGIN(sendto); - ssize_t result = sendto( - scan_socket_, reinterpret_cast(data), sizeof(data), - 0, reinterpret_cast(&addr), sizeof(addr)); - BA_DEBUG_TIME_CHECK_END(sendto, 10); - if (result == -1) { - int err = g_platform->GetSocketError(); - switch (err) { // NOLINT(hicpp-multiway-paths-covered) - case ENETUNREACH: - break; - default: - Log(LogLevel::kError, "Error on scanSocket sendto: " - + g_platform->GetSocketErrorString()); - } - } - } - next_scan_query_id_++; - - // ..and see if any responses came in from previous sends. - char buffer[256]; - struct sockaddr_storage from {}; - socklen_t from_size = sizeof(from); - while (true) { - BA_DEBUG_TIME_CHECK_BEGIN(recvfrom); - ssize_t result = recvfrom(scan_socket_, buffer, sizeof(buffer), 0, - reinterpret_cast(&from), &from_size); - BA_DEBUG_TIME_CHECK_END(recvfrom, 10); - - if (result == -1) { - int err = g_platform->GetSocketError(); - switch (err) { // NOLINT(hicpp-multiway-paths-covered) - case EWOULDBLOCK: - break; - default: - Log(LogLevel::kError, - "Error: recvfrom error: " + g_platform->GetSocketErrorString()); - break; - } - break; - } - - if (result > 2 && buffer[0] == BA_PACKET_GAME_QUERY_RESPONSE) { - // Size should be between 13 and 366 (1 byte type, 4 byte query_id, 4 - // byte protocol_id, 1 byte id_len, 1 byte player_spec_len, 1-100 byte - // id, 1-255 byte player-spec). - if (result >= 14 && result <= 366) { - uint32_t protocol_version; - uint32_t query_id; - - memcpy(&query_id, buffer + 1, 4); - memcpy(&protocol_version, buffer + 5, 4); - auto id_len = static_cast(buffer[9]); - auto player_spec_len = static_cast(buffer[10]); - - if (id_len > 0 && id_len <= 100 && player_spec_len > 0 - && player_spec_len <= 255 - && (11 + id_len + player_spec_len == result)) { - char id[101]; - char player_spec_str[256]; - memcpy(id, buffer + 11, id_len); - memcpy(player_spec_str, buffer + 11 + id_len, player_spec_len); - - id[id_len] = 0; - player_spec_str[player_spec_len] = 0; - - // Add or modify an entry for this. - { - std::scoped_lock lock(scan_results_mutex_); - - // Ignore if it looks like its us. - if (id != GetAppInstanceUUID()) { - std::string key = id; - auto i = scan_results_.find(key); - - // Make a new entry if its not there. - bool do_update_entry = (i == scan_results_.end() - || i->second.last_query_id != query_id); - if (do_update_entry) { - ScanResultsEntryPriv& entry(scan_results_[key]); - entry.player_spec = PlayerSpec(player_spec_str); - char buffer2[256]; - entry.address = inet_ntop( - AF_INET, - &((reinterpret_cast(&from))->sin_addr), - buffer2, sizeof(buffer2)); - entry.last_query_id = query_id; - entry.last_contact_time = GetRealTime(); - } - } - PruneScanResults(); - } - } else { - Log(LogLevel::kError, - "Got invalid BA_PACKET_GAME_QUERY_RESPONSE packet"); - } - } else { - Log(LogLevel::kError, - "Got invalid BA_PACKET_GAME_QUERY_RESPONSE packet"); - } - } - } -} - -auto Networking::GetScanResults() -> std::vector { - std::vector results; - results.resize(scan_results_.size()); - { - std::scoped_lock lock(scan_results_mutex_); - int out_num = 0; - for (auto&& i : scan_results_) { - ScanResultsEntryPriv& in(i.second); - ScanResultsEntry& out(results[out_num]); - out.display_string = in.player_spec.GetDisplayString(); - out.address = in.address; - out_num++; - } - PruneScanResults(); - } - return results; -} - -void Networking::PruneScanResults() { - millisecs_t t = GetRealTime(); - auto i = scan_results_.begin(); - while (i != scan_results_.end()) { - auto i_next = i; - i_next++; - if (t - i->second.last_contact_time > 3000) { - scan_results_.erase(i); - } - i = i_next; - } -} - -void Networking::EndHostScanning() { - if (scan_socket_ != -1) { - g_platform->CloseSocket(scan_socket_); - scan_socket_ = -1; - } -} - -void Networking::Pause() { - if (!running_) { - Log(LogLevel::kError, - "Networking::pause() called with running_ already false"); - } - running_ = false; - - // Game is going into background or whatnot. Kill any sockets/etc. - EndHostScanning(); -} - -void Networking::Resume() { - if (running_) { - Log(LogLevel::kError, - "Networking::resume() called with running_ already true"); - } - running_ = true; -} - -void Networking::SendTo(const std::vector& buffer, - const SockAddr& addr) { - assert(g_network_reader); - assert(!buffer.empty()); - - // This needs to be locked during any sd changes/writes. - std::scoped_lock lock(g_network_reader->sd_mutex()); - - // Only send if the relevant socket is currently up.. silently ignore - // otherwise. - int sd = addr.IsV6() ? g_network_reader->sd6() : g_network_reader->sd4(); - if (sd != -1) { - sendto(sd, (const char*)&buffer[0], - static_cast_check_fit(buffer.size()), 0, - addr.GetSockAddr(), addr.GetSockAddrLen()); - } -} - -} // namespace ballistica diff --git a/src/ballistica/networking/telnet_server.cc b/src/ballistica/networking/telnet_server.cc deleted file mode 100644 index 7a75d1f9..00000000 --- a/src/ballistica/networking/telnet_server.cc +++ /dev/null @@ -1,247 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/networking/telnet_server.h" - -#include "ballistica/app/app.h" -#include "ballistica/core/context.h" -#include "ballistica/core/thread.h" -#include "ballistica/logic/logic.h" -#include "ballistica/networking/networking.h" -#include "ballistica/networking/networking_sys.h" -#include "ballistica/platform/platform.h" -#include "ballistica/python/python_command.h" -#include "ballistica/python/python_sys.h" - -namespace ballistica { - -TelnetServer::TelnetServer(int port) : port_(port) { - thread_ = new std::thread(RunThreadStatic, this); - assert(g_app->telnet_server == nullptr); - g_app->telnet_server = this; - - // NOTE: we consider access implicitly granted on headless builds - // since we can't pop up the request dialog. - // There is still password protection and we now don't even spin - // up the telnet socket by default on servers. - if (HeadlessMode()) { - user_has_granted_access_ = true; - } -} - -void TelnetServer::Pause() { - assert(InMainThread()); - assert(!paused_); - { - std::unique_lock lock(paused_mutex_); - paused_ = true; - } - - // FIXME - need a way to kill these sockets; - // On iOS they die automagically but not android. - // attempted to force-close at some point but it didn't work (on android at - // least) -} - -void TelnetServer::Resume() { - assert(InMainThread()); - assert(paused_); - { - std::unique_lock lock(paused_mutex_); - paused_ = false; - } - - // Poke our thread so it can go on its way. - paused_cv_.notify_all(); -} - -#pragma clang diagnostic push -#pragma ide diagnostic ignored "ConstantFunctionResult" - -auto TelnetServer::RunThread() -> int { - // Do this whole thing in a loop. - // If we get put to sleep we just start over. - while (true) { - // Sleep until we're unpaused. - if (paused_) { - std::unique_lock lock(paused_mutex_); - paused_cv_.wait(lock, [this] { return (!paused_); }); - } - - sd_ = socket(AF_INET, SOCK_STREAM, 0); - if (sd_ < 0) { - Log(LogLevel::kError, - "Unable to open host socket; errno " + std::to_string(errno)); - return 1; - } - - // Make it reusable. - int on = 1; - int status = - setsockopt(sd_, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on)); - if (-1 == status) { - Log(LogLevel::kError, "Error setting SO_REUSEADDR on telnet server"); - } - - // Bind to local server port. - struct sockaddr_in serv_addr {}; - serv_addr.sin_family = AF_INET; - serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // NOLINT - int result; - serv_addr.sin_port = htons(port_); // NOLINT - result = ::bind(sd_, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); - if (result != 0) { - return 1; - } - char buffer[10000]; - const char* prompt = "ballisticacore> "; - const char* password_prompt = "password:"; - - // Now just listen and forward msg along to people. - while (true) { - struct sockaddr_storage from {}; - socklen_t from_size = sizeof(from); - if (listen(sd_, 0) == 0) { - client_sd_ = accept(sd_, (struct sockaddr*)&from, &from_size); - if (client_sd_ < 0) { - break; - } - - // If we dont have access and havnt asked the user for it yet, ask them. - if (!user_has_granted_access_ && g_logic - && !have_asked_user_for_access_) { - g_logic->PushAskUserForTelnetAccessCall(); - have_asked_user_for_access_ = true; - } - - // Require password for each connection if we have one - reading_password_ = require_password_; - - if (g_logic) { - if (reading_password_) { - PushPrint(password_prompt); - } else { - PushPrint(prompt); - } - } - while (true) { - result = - static_cast(recv(client_sd_, buffer, sizeof(buffer) - 1, 0)); - - // Socket closed/disconnected. - if (result == 0 || result == -1) { - // We got closed for whatever reason. - if (client_sd_ != -1) { - g_platform->CloseSocket(client_sd_); - } - client_sd_ = -1; - break; - } else { - buffer[result] = 0; - - // Looks like these come in with '\r\n' at the end.. lets strip - // that. - if (result > 0 && (buffer[result - 1] == '\n')) { - buffer[result - 1] = 0; - if (result > 1 && (buffer[result - 2] == '\r')) - buffer[result - 2] = 0; - } - if (g_logic) { - if (user_has_granted_access_) { - if (reading_password_) { - if (GetRealTime() - last_try_time_ < 2000) { - PushPrint( - std::string("retried too soon; please wait a moment " - "and try again.\n") - + password_prompt); - } else if (buffer == password_) { - reading_password_ = false; - PushPrint(prompt); - } else { - last_try_time_ = GetRealTime(); - PushPrint(std::string("incorrect.\n") + password_prompt); - } - } else { - PushTelnetScriptCommand(buffer); - } - } else { - PushPrint(g_logic->GetResourceString("telnetAccessDeniedText")); - } - } - } - } - } else { - // Listening failed; abort. - if (sd_ != -1) { - g_platform->CloseSocket(sd_); - } - break; - } - } - - // Sleep for a moment to keep us from running wild if we're unable to block. - Platform::SleepMS(1000); - } -} - -#pragma clang diagnostic pop - -void TelnetServer::PushTelnetScriptCommand(const std::string& command) { - assert(g_logic); - if (g_logic == nullptr) { - return; - } - g_logic->thread()->PushCall([this, command] { - // These are always run in whichever context is 'visible'. - ScopedSetContext cp(g_logic->GetForegroundContext()); - if (!g_app->user_ran_commands) { - g_app->user_ran_commands = true; - } - PythonCommand cmd(command, ""); - if (cmd.CanEval()) { - PyObject* obj = cmd.RunReturnObj(true, nullptr); - if (obj && obj != Py_None) { - PyObject* s = PyObject_Repr(obj); - if (s) { - const char* c = PyUnicode_AsUTF8(s); - PushPrint(std::string(c) + "\n"); - Py_DECREF(s); - } - Py_DECREF(obj); - } - } else { - // Not eval-able; just run it. - cmd.Run(); - } - PushPrint("ballisticacore> "); - }); -} - -void TelnetServer::PushPrint(const std::string& s) { - assert(g_logic); - g_logic->thread()->PushCall([this, s] { Print(s); }); -} - -void TelnetServer::Print(const std::string& s) { - // Currently we make the assumption that *only* the logic thread writes to our - // socket. - assert(InLogicThread()); - if (client_sd_ != -1) { - send(client_sd_, s.c_str(), - static_cast_check_fit(s.size()), 0); - } -} - -TelnetServer::~TelnetServer() = default; - -void TelnetServer::SetAccessEnabled(bool v) { user_has_granted_access_ = v; } - -void TelnetServer::SetPassword(const char* password) { - if (password != nullptr) { - password_ = password; - require_password_ = true; - } else { - require_password_ = false; - } -} - -} // namespace ballistica diff --git a/src/ballistica/networking/telnet_server.h b/src/ballistica/networking/telnet_server.h deleted file mode 100644 index fd0b0d09..00000000 --- a/src/ballistica/networking/telnet_server.h +++ /dev/null @@ -1,49 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_NETWORKING_TELNET_SERVER_H_ -#define BALLISTICA_NETWORKING_TELNET_SERVER_H_ - -#include -#include -#include -#include - -#include "ballistica/ballistica.h" - -namespace ballistica { - -class TelnetServer { - public: - explicit TelnetServer(int port); - ~TelnetServer(); - auto Pause() -> void; - auto Resume() -> void; - auto PushTelnetScriptCommand(const std::string& command) -> void; - auto PushPrint(const std::string& s) -> void; - auto SetAccessEnabled(bool v) -> void; - auto SetPassword(const char* password) -> void; // nullptr == no password - - private: - auto RunThread() -> int; - auto Print(const std::string& s) -> void; - static auto RunThreadStatic(void* self) -> int { - return static_cast(self)->RunThread(); - } - int sd_{-1}; - int client_sd_{-1}; - int port_{}; - std::thread* thread_{}; - bool have_asked_user_for_access_{}; - bool user_has_granted_access_{}; - bool paused_{}; - bool reading_password_{}; - bool require_password_{}; - millisecs_t last_try_time_{}; - std::string password_; - std::mutex paused_mutex_; - std::condition_variable paused_cv_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_NETWORKING_TELNET_SERVER_H_ diff --git a/src/ballistica/platform/apple/platform_apple.h b/src/ballistica/platform/apple/platform_apple.h deleted file mode 100644 index 5c819577..00000000 --- a/src/ballistica/platform/apple/platform_apple.h +++ /dev/null @@ -1,85 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PLATFORM_APPLE_PLATFORM_APPLE_H_ -#define BALLISTICA_PLATFORM_APPLE_PLATFORM_APPLE_H_ -#if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS - -#include -#include -#include -#include - -#include "ballistica/platform/platform.h" - -namespace ballistica { - -class PlatformApple : public Platform { - public: - PlatformApple(); - auto GetDeviceV1AccountUUIDPrefix() -> std::string override; - auto GetRealLegacyDeviceUUID(std::string* uuid) -> bool override; - auto GenerateUUID() -> std::string override; - auto GetDefaultConfigDirectory() -> std::string override; - auto GetLocale() -> std::string override; - auto DoGetDeviceName() -> std::string override; - auto DoHasTouchScreen() -> bool override; - auto GetUIScale() -> UIScale override; - auto IsRunningOnDesktop() -> bool override; - auto DisplayLog(const std::string& name, LogLevel level, - const std::string& msg) -> void override; - auto SetupDataDirectory() -> void override; - auto GetTextBoundsAndWidth(const std::string& text, Rect* r, float* width) - -> void override; - auto FreeTextTexture(void* tex) -> void override; - auto CreateTextTexture(int width, int height, - const std::vector& strings, - const std::vector& positions, - const std::vector& widths, float scale) - -> void* override; - auto GetTextTextureData(void* tex) -> uint8_t* override; - auto SubmitScore(const std::string& game, const std::string& version, - int64_t score) -> void override; - auto ReportAchievement(const std::string& achievement) -> void override; - auto HaveLeaderboard(const std::string& game, const std::string& config) - -> bool override; - auto ShowOnlineScoreUI(const std::string& show, const std::string& game, - const std::string& game_version) -> void override; - auto DoPurchase(const std::string& item) -> void override; - auto RestorePurchases() -> void override; - auto NewAutoReleasePool() -> void* override; - auto DrainAutoReleasePool(void* pool) -> void override; - auto DoOpenURL(const std::string& url) -> void override; - auto ResetAchievements() -> void override; - auto GameCenterLogin() -> void override; - auto PurchaseAck(const std::string& purchase, const std::string& order_id) - -> void override; - auto IsOSPlayingMusic() -> bool override; - auto SetHardwareCursorVisible(bool visible) -> void override; - auto QuitApp() -> void override; - auto OpenFileExternally(const std::string& path) -> void override; - auto OpenDirExternally(const std::string& path) -> void override; - auto MacMusicAppInit() -> void override; - auto MacMusicAppGetVolume() -> int override; - auto MacMusicAppSetVolume(int volume) -> void override; - auto MacMusicAppGetLibrarySource() -> void override; - auto MacMusicAppStop() -> void override; - auto MacMusicAppPlayPlaylist(const std::string& playlist) -> bool override; - auto MacMusicAppGetPlaylists() -> std::list override; - auto IsEventPushMode() -> bool override; - auto ContainsPythonDist() -> bool override; - auto GetPlatformName() -> std::string override; - auto GetSubplatformName() -> std::string override; - - auto DoClipboardIsSupported() -> bool override; - auto DoClipboardHasText() -> bool override; - auto DoClipboardSetText(const std::string& text) -> void override; - auto DoClipboardGetText() -> std::string override; - auto GetDeviceUUIDInputs() -> std::list override; - - private: -}; - -} // namespace ballistica - -#endif // BA_XCODE_BUILD || BA_OSTYPE_MACOS -#endif // BALLISTICA_PLATFORM_APPLE_PLATFORM_APPLE_H_ diff --git a/src/ballistica/platform/linux/platform_linux.h b/src/ballistica/platform/linux/platform_linux.h deleted file mode 100644 index 71f89f15..00000000 --- a/src/ballistica/platform/linux/platform_linux.h +++ /dev/null @@ -1,30 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PLATFORM_LINUX_PLATFORM_LINUX_H_ -#define BALLISTICA_PLATFORM_LINUX_PLATFORM_LINUX_H_ -#if BA_OSTYPE_LINUX - -#include - -#include "ballistica/platform/platform.h" - -namespace ballistica { - -class PlatformLinux : public Platform { - public: - PlatformLinux(); - auto GetDeviceV1AccountUUIDPrefix() -> std::string override { return "l"; } - auto GenerateUUID() -> std::string override; - auto DoHasTouchScreen() -> bool override; - auto DoOpenURL(const std::string& url) -> void override; - auto OpenFileExternally(const std::string& path) -> void override; - auto OpenDirExternally(const std::string& path) -> void override; - auto GetPlatformName() -> std::string override; - auto GetSubplatformName() -> std::string override; - auto GetDeviceUUIDInputs() -> std::list override; -}; - -} // namespace ballistica - -#endif // BA_OSTYPE_LINUX -#endif // BALLISTICA_PLATFORM_LINUX_PLATFORM_LINUX_H_ diff --git a/src/ballistica/platform/sdl/sdl_app.h b/src/ballistica/platform/sdl/sdl_app.h deleted file mode 100644 index 367061e5..00000000 --- a/src/ballistica/platform/sdl/sdl_app.h +++ /dev/null @@ -1,66 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PLATFORM_SDL_SDL_APP_H_ -#define BALLISTICA_PLATFORM_SDL_SDL_APP_H_ - -#if BA_SDL_BUILD - -#include - -#include "ballistica/app/app_flavor.h" -#include "ballistica/math/vector2f.h" - -namespace ballistica { - -class SDLApp : public AppFlavor { - public: - static auto InitSDL() -> void; - explicit SDLApp(Thread* thread); - auto HandleSDLEvent(const SDL_Event& event) -> void; - auto RunEvents() -> void override; - auto DidFinishRenderingFrame(FrameDef* frame) -> void override; - auto SetAutoVSync(bool enable) -> void; - static auto SDLJoystickConnected(int index) -> void; - static auto SDLJoystickDisconnected(int index) -> void; - auto OnAppStart() -> void override; - - /// Return g_app_flavor as a SDLApp. (assumes it actually is one). - static SDLApp* get() { - assert(g_app_flavor != nullptr); - assert(dynamic_cast(g_app_flavor) - == static_cast(g_app_flavor)); - return static_cast(g_app_flavor); - } - auto SetInitialScreenDimensions(const Vector2f& dimensions) -> void; - - private: - // Given an sdl joystick ID, returns our ballistica input for it. - auto GetSDLJoyStickInput(int sdl_joystick_id) const -> Joystick*; - - // The same but using sdl events. - auto GetSDLJoyStickInput(const SDL_Event* e) const -> Joystick*; - - auto DoSwap() -> void; - auto SwapBuffers() -> void; - auto UpdateAutoVSync(int diff) -> void; - auto AddSDLInputDevice(Joystick* input, int index) -> void; - auto RemoveSDLInputDevice(int index) -> void; - millisecs_t last_swap_time_{}; - millisecs_t swap_start_time_{}; - int too_slow_frame_count_{}; - bool auto_vsync_{}; - bool vsync_enabled_{true}; - float average_vsync_fps_{60.0f}; - int vsync_good_frame_count_{}; - int vsync_bad_frame_count_{}; - std::vector sdl_joysticks_; - - /// This is in points; not pixels. - Vector2f screen_dimensions_{1.0f, 1.0f}; -}; - -} // namespace ballistica - -#endif // BA_SDL_BUILD - -#endif // BALLISTICA_PLATFORM_SDL_SDL_APP_H_ diff --git a/src/ballistica/platform/stdio_console.h b/src/ballistica/platform/stdio_console.h deleted file mode 100644 index 1ff0f2d6..00000000 --- a/src/ballistica/platform/stdio_console.h +++ /dev/null @@ -1,23 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PLATFORM_STDIO_CONSOLE_H_ -#define BALLISTICA_PLATFORM_STDIO_CONSOLE_H_ - -#include "ballistica/ballistica.h" - -namespace ballistica { - -class StdioConsole { - public: - StdioConsole(); - void OnAppStart(); - auto thread() const -> Thread* { return thread_; } - - private: - Thread* thread_{}; - std::string pending_input_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PLATFORM_STDIO_CONSOLE_H_ diff --git a/src/ballistica/plus/README.md b/src/ballistica/plus/README.md new file mode 100644 index 00000000..8c689781 --- /dev/null +++ b/src/ballistica/plus/README.md @@ -0,0 +1,6 @@ +# Plus Feature Set + +Bits of the engine related to accounts and cloud functionality. In prefab builds +the compiled code for this feature set is contained in the pre-compiled static +ballisticakit_internal library. The plus feature set can also be removed from +spinoff projects if desired to remove the need for that library. diff --git a/src/ballistica/python/class/python_class.h b/src/ballistica/python/class/python_class.h deleted file mode 100644 index b25834b2..00000000 --- a/src/ballistica/python/class/python_class.h +++ /dev/null @@ -1,28 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_H_ - -#include "ballistica/python/python_sys.h" - -namespace ballistica { - -// a convenient base class for defining custom python types -class PythonClass { - public: - PyObject_HEAD; - static void SetupType(PyTypeObject* obj); - - private: - static auto tp_repr(PythonClass* self) -> PyObject*; - static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) - -> PyObject*; - static void tp_dealloc(PythonClass* self); - static auto tp_getattro(PythonClass* node, PyObject* attr) -> PyObject*; - static auto tp_setattro(PythonClass* node, PyObject* attr, PyObject* val) - -> int; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_H_ diff --git a/src/ballistica/python/class/python_class_activity_data.h b/src/ballistica/python/class/python_class_activity_data.h deleted file mode 100644 index 4da2a532..00000000 --- a/src/ballistica/python/class/python_class_activity_data.h +++ /dev/null @@ -1,39 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_ACTIVITY_DATA_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_ACTIVITY_DATA_H_ - -#include "ballistica/core/object.h" -#include "ballistica/python/class/python_class.h" - -namespace ballistica { - -class PythonClassActivityData : public PythonClass { - public: - static auto type_name() -> const char* { return "ActivityData"; } - static void SetupType(PyTypeObject* obj); - static auto Create(HostActivity* host_activity) -> PyObject*; - static auto Check(PyObject* o) -> bool { - return PyObject_TypeCheck(o, &type_obj); - } - static PyTypeObject type_obj; - auto GetHostActivity() const -> HostActivity*; - - private: - static PyMethodDef tp_methods[]; - static auto tp_repr(PythonClassActivityData* self) -> PyObject*; - static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) - -> PyObject*; - static void tp_dealloc(PythonClassActivityData* self); - static auto exists(PythonClassActivityData* self) -> PyObject*; - static auto make_foreground(PythonClassActivityData* self) -> PyObject*; - static auto start(PythonClassActivityData* self) -> PyObject*; - static auto expire(PythonClassActivityData* self) -> PyObject*; - Object::WeakRef* host_activity_; - static auto nb_bool(PythonClassActivityData* self) -> int; - static PyNumberMethods as_number_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_ACTIVITY_DATA_H_ diff --git a/src/ballistica/python/class/python_class_collide_model.cc b/src/ballistica/python/class/python_class_collide_model.cc deleted file mode 100644 index 086c3b35..00000000 --- a/src/ballistica/python/class/python_class_collide_model.cc +++ /dev/null @@ -1,119 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/class/python_class_collide_model.h" - -#include "ballistica/assets/component/collide_model.h" -#include "ballistica/core/thread.h" -#include "ballistica/logic/logic.h" -#include "ballistica/python/python.h" - -namespace ballistica { - -auto PythonClassCollideModel::tp_repr(PythonClassCollideModel* self) - -> PyObject* { - BA_PYTHON_TRY; - Object::Ref m = *(self->collide_model_); - return Py_BuildValue( - "s", (std::string("name() + "\"") : "(empty ref)") + ">") - .c_str()); - BA_PYTHON_CATCH; -} - -void PythonClassCollideModel::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_name = "ba.CollideModel"; - obj->tp_basicsize = sizeof(PythonClassCollideModel); - obj->tp_doc = - "A reference to a collide-model.\n" - "\n" - "Category: **Asset Classes**\n" - "\n" - "Use ba.getcollidemodel() to instantiate one."; - obj->tp_repr = (reprfunc)tp_repr; - obj->tp_new = tp_new; - obj->tp_dealloc = (destructor)tp_dealloc; -} - -auto PythonClassCollideModel::Create(CollideModel* collide_model) -> PyObject* { - s_create_empty_ = true; // prevent class from erroring on create - auto* t = reinterpret_cast( - PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); - s_create_empty_ = false; - if (!t) { - throw Exception("ba.CollideModel creation failed."); - } - *(t->collide_model_) = collide_model; - return reinterpret_cast(t); -} - -auto PythonClassCollideModel::GetCollideModel(bool doraise) const - -> CollideModel* { - CollideModel* collide_model = collide_model_->get(); - if (!collide_model && doraise) { - throw Exception("Invalid CollideModel.", PyExcType::kNotFound); - } - return collide_model; -} - -// Clion makes some incorrect inferences here. -#pragma clang diagnostic push -#pragma ide diagnostic ignored "UnreachableCode" -#pragma ide diagnostic ignored "ConstantConditionsOC" -#pragma ide diagnostic ignored "ConstantFunctionResult" - -auto PythonClassCollideModel::tp_new(PyTypeObject* type, PyObject* args, - PyObject* kwds) -> PyObject* { - auto* self = - reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; - if (!InLogicThread()) { - throw Exception( - "ERROR: " + std::string(type_obj.tp_name) - + " objects must only be created in the logic thread (current is (" - + GetCurrentThreadName() + ")."); - } - - if (!s_create_empty_) { - throw Exception( - "Can't instantiate CollideModels directly; use " - "ba.getcollidemodel() to get them."); - } - self->collide_model_ = new Object::Ref(); - BA_PYTHON_NEW_CATCH; - } - return reinterpret_cast(self); -} - -#pragma clang diagnostic pop - -void PythonClassCollideModel::Delete(Object::Ref* ref) { - assert(InLogicThread()); - // if we're the py-object for a collide_model, clear them out - // (FIXME - we should pass the old pointer in here to sanity-test that we - // were their ref) - if (ref->exists()) { - (*ref)->ClearPyObject(); - } - delete ref; -} - -void PythonClassCollideModel::tp_dealloc(PythonClassCollideModel* self) { - BA_PYTHON_TRY; - // these have to be deleted in the logic thread - send the ptr along if need - // be; otherwise do it immediately - if (!InLogicThread()) { - Object::Ref* c = self->collide_model_; - g_logic->thread()->PushCall([c] { Delete(c); }); - } else { - Delete(self->collide_model_); - } - BA_PYTHON_DEALLOC_CATCH; - Py_TYPE(self)->tp_free(reinterpret_cast(self)); -} - -bool PythonClassCollideModel::s_create_empty_ = false; -PyTypeObject PythonClassCollideModel::type_obj; - -} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_collide_model.h b/src/ballistica/python/class/python_class_collide_model.h deleted file mode 100644 index d59c3442..00000000 --- a/src/ballistica/python/class/python_class_collide_model.h +++ /dev/null @@ -1,34 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_COLLIDE_MODEL_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_COLLIDE_MODEL_H_ - -#include "ballistica/core/object.h" -#include "ballistica/python/class/python_class.h" - -namespace ballistica { - -class PythonClassCollideModel : public PythonClass { - public: - static auto type_name() -> const char* { return "CollideModel"; } - static auto tp_repr(PythonClassCollideModel* self) -> PyObject*; - static void SetupType(PyTypeObject* obj); - static PyTypeObject type_obj; - static auto Create(CollideModel* collide_model) -> PyObject*; - static auto Check(PyObject* o) -> bool { - return PyObject_TypeCheck(o, &type_obj); - } - auto GetCollideModel(bool doraise = true) const -> CollideModel*; - - private: - static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) - -> PyObject*; - static void Delete(Object::Ref* ref); - static void tp_dealloc(PythonClassCollideModel* self); - static bool s_create_empty_; - Object::Ref* collide_model_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_COLLIDE_MODEL_H_ diff --git a/src/ballistica/python/class/python_class_context.cc b/src/ballistica/python/class/python_class_context.cc deleted file mode 100644 index 21915540..00000000 --- a/src/ballistica/python/class/python_class_context.cc +++ /dev/null @@ -1,228 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/class/python_class_context.h" - -#include "ballistica/core/thread.h" -#include "ballistica/logic/host_activity.h" -#include "ballistica/logic/logic.h" -#include "ballistica/logic/session/host_session.h" -#include "ballistica/python/python.h" -#include "ballistica/ui/ui.h" - -namespace ballistica { - -void PythonClassContext::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_name = "ba.Context"; - obj->tp_basicsize = sizeof(PythonClassContext); - obj->tp_doc = - "Context(source: Any)\n" - "\n" - "A game context state.\n" - "\n" - "Category: **General Utility Classes**\n" - "\n" - "Many operations such as ba.newnode() or ba.gettexture() operate\n" - "implicitly on the current context. Each ba.Activity has its own\n" - "Context and objects within that activity (nodes, media, etc) can only\n" - "interact with other objects from that context.\n" - "\n" - "In general, as a modder, you should not need to worry about contexts,\n" - "since timers and other callbacks will take care of saving and\n" - "restoring the context automatically, but there may be rare cases where\n" - "you need to deal with them, such as when loading media in for use in\n" - "the UI (there is a special `'ui'` context for all\n" - "user-interface-related functionality).\n" - "\n" - "When instantiating a ba.Context instance, a single `'source'` " - "argument\n" - "is passed, which can be one of the following strings/objects:\n\n" - "###### `'empty'`\n" - "> Gives an empty context; it can be handy to run code here to ensure\n" - "it does no loading of media, creation of nodes, etc.\n" - "\n" - "###### `'current'`\n" - "> Sets the context object to the current context.\n" - "\n" - "###### `'ui'`\n" - "> Sets to the UI context. UI functions as well as loading of media to\n" - "be used in said functions must happen in the UI context.\n" - "\n" - "###### A ba.Activity instance\n" - "> Gives the context for the provided ba.Activity.\n" - " Most all code run during a game happens in an Activity's Context.\n" - "\n" - "###### A ba.Session instance\n" - "> Gives the context for the provided ba.Session.\n" - "Generally a user should not need to run anything here.\n" - "\n" - "\n" - "##### Usage\n" - "Contexts are generally used with the python 'with' statement, which\n" - "sets the context as current on entry and resets it to the previous\n" - "value on exit.\n" - "\n" - "##### Example\n" - "Load a few textures into the UI context\n" - "(for use in widgets, etc):\n" - ">>> with ba.Context('ui'):\n" - "... tex1 = ba.gettexture('foo_tex_1')\n" - "... tex2 = ba.gettexture('foo_tex_2')\n"; - - obj->tp_new = tp_new; - obj->tp_dealloc = (destructor)tp_dealloc; - obj->tp_repr = (reprfunc)tp_repr; - obj->tp_richcompare = (richcmpfunc)tp_richcompare; - obj->tp_methods = tp_methods; -} - -auto PythonClassContext::tp_repr(PythonClassContext* self) -> PyObject* { - BA_PYTHON_TRY; - - std::string context_str; - if (self->context_->GetUIContext()) { - context_str = "ui"; - } else if (HostActivity* ha = self->context_->GetHostActivity()) { - PythonRef ha_obj(ha->GetPyActivity(), PythonRef::kAcquire); - if (ha_obj.get() != Py_None) { - context_str = ha_obj.Str(); - } else { - context_str = ha->GetObjectDescription(); - } - } else if (self->context_->target.exists()) { - context_str = self->context_->target->GetObjectDescription(); - } else { - context_str = "empty"; - } - context_str = ""; - return PyUnicode_FromString(context_str.c_str()); - BA_PYTHON_CATCH; -} - -auto PythonClassContext::tp_richcompare(PythonClassContext* c1, PyObject* c2, - int op) -> PyObject* { - // always return false against other types - if (!Check(c2)) { - Py_RETURN_FALSE; - } - bool eq = (*(c1->context_) - == *((reinterpret_cast(c2))->context_)); - if (op == Py_EQ) { - if (eq) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } - } else if (op == Py_NE) { - if (!eq) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } - } else { - // don't support other ops - Py_RETURN_NOTIMPLEMENTED; - } -} - -auto PythonClassContext::tp_new(PyTypeObject* type, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - PyObject* source_obj = Py_None; - if (!PyArg_ParseTuple(args, "O", &source_obj)) { - return nullptr; - } - - if (!InLogicThread()) { - throw Exception( - "ERROR: " + std::string(type_obj.tp_name) - + " objects must only be created in the logic thread (current is (" - + GetCurrentThreadName() + ")."); - } - - Context cs(nullptr); - - if (Python::IsPyString(source_obj)) { - std::string source = Python::GetPyString(source_obj); - if (source == "ui") { - cs = Context(g_logic->GetUIContextTarget()); - } else if (source == "UI") { - BA_LOG_ONCE(LogLevel::kError, - "'UI' context-target option is deprecated; please use 'ui'"); - Python::PrintStackTrace(); - cs = Context(g_logic->GetUIContextTarget()); - } else if (source == "current") { - cs = Context::current(); - } else if (source == "empty") { - cs = Context(nullptr); - } else { - throw Exception("invalid context identifier: '" + source + "'"); - } - } else if (Python::IsPyHostActivity(source_obj)) { - cs = Context(Python::GetPyHostActivity(source_obj)); - } else if (Python::IsPySession(source_obj)) { - auto* hs = dynamic_cast(Python::GetPySession(source_obj)); - assert(hs != nullptr); - cs = Context(hs); - } else { - throw Exception( - "Invalid argument to ba.Context(): " + Python::ObjToString(source_obj) - + "; expected 'ui', 'current', 'empty', a ba.Activity, or a " - "ba.Session"); - } - - auto* self = reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; - self->context_ = new Context(cs); - self->context_prev_ = new Context(); - BA_PYTHON_NEW_CATCH; - } - return reinterpret_cast(self); - BA_PYTHON_CATCH; -} - -void PythonClassContext::tp_dealloc(PythonClassContext* self) { - BA_PYTHON_TRY; - // Contexts have to be deleted in the logic thread; - // ship them to it for deletion if need be; otherwise do it immediately. - if (!InLogicThread()) { - Context* c = self->context_; - Context* c2 = self->context_prev_; - g_logic->thread()->PushCall([c, c2] { - delete c; - delete c2; - }); - } else { - delete self->context_; - delete self->context_prev_; - } - BA_PYTHON_DEALLOC_CATCH; - Py_TYPE(self)->tp_free(reinterpret_cast(self)); -} - -auto PythonClassContext::__enter__(PythonClassContext* self) -> PyObject* { - BA_PYTHON_TRY; - *(self->context_prev_) = Context::current(); - Context::set_current(*(self->context_)); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PythonClassContext::__exit__(PythonClassContext* self, PyObject* args) - -> PyObject* { - BA_PYTHON_TRY; - Context::set_current(*(self->context_prev_)); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -PyTypeObject PythonClassContext::type_obj; -PyMethodDef PythonClassContext::tp_methods[] = { - {"__enter__", (PyCFunction)__enter__, METH_NOARGS, - "enter call for 'with' functionality"}, - {"__exit__", (PyCFunction)__exit__, METH_VARARGS, - "exit call for 'with' functionality"}, - {nullptr}}; - -} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_context.h b/src/ballistica/python/class/python_class_context.h deleted file mode 100644 index 9065fb37..00000000 --- a/src/ballistica/python/class/python_class_context.h +++ /dev/null @@ -1,37 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_H_ - -#include "ballistica/core/context.h" -#include "ballistica/python/class/python_class.h" - -namespace ballistica { - -class PythonClassContext : public PythonClass { - public: - static auto type_name() -> const char* { return "Context"; } - static void SetupType(PyTypeObject* obj); - static auto Check(PyObject* o) -> bool { - return PyObject_TypeCheck(o, &type_obj); - } - static PyTypeObject type_obj; - auto context() const -> const Context& { return *context_; } - - private: - static PyMethodDef tp_methods[]; - static auto tp_repr(PythonClassContext* self) -> PyObject*; - static auto tp_richcompare(PythonClassContext* c1, PyObject* c2, int op) - -> PyObject*; - static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) - -> PyObject*; - static void tp_dealloc(PythonClassContext* self); - static auto __enter__(PythonClassContext* self) -> PyObject*; - static auto __exit__(PythonClassContext* self, PyObject* args) -> PyObject*; - Context* context_; - Context* context_prev_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_H_ diff --git a/src/ballistica/python/class/python_class_data.cc b/src/ballistica/python/class/python_class_data.cc deleted file mode 100644 index eaebd5f9..00000000 --- a/src/ballistica/python/class/python_class_data.cc +++ /dev/null @@ -1,146 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/class/python_class_data.h" - -#include "ballistica/assets/component/data.h" -#include "ballistica/core/thread.h" -#include "ballistica/logic/logic.h" -#include "ballistica/python/python.h" - -namespace ballistica { - -auto PythonClassData::tp_repr(PythonClassData* self) -> PyObject* { - BA_PYTHON_TRY; - Object::Ref m = *(self->data_); - return Py_BuildValue( - "s", (std::string("name() + "\"") : "(empty ref)") + ">") - .c_str()); - BA_PYTHON_CATCH; -} - -void PythonClassData::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_name = "ba.Data"; - obj->tp_basicsize = sizeof(PythonClassData); - obj->tp_doc = - "A reference to a data object.\n" - "\n" - "Category: **Asset Classes**\n" - "\n" - "Use ba.getdata() to instantiate one."; - obj->tp_repr = (reprfunc)tp_repr; - obj->tp_new = tp_new; - obj->tp_dealloc = (destructor)tp_dealloc; - obj->tp_methods = tp_methods; -} - -auto PythonClassData::Create(Data* data) -> PyObject* { - s_create_empty_ = true; // prevent class from erroring on create - auto* t = reinterpret_cast( - PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); - s_create_empty_ = false; - if (!t) { - throw Exception("ba.Data creation failed."); - } - *(t->data_) = data; - return reinterpret_cast(t); -} - -auto PythonClassData::GetData(bool doraise) const -> Data* { - Data* data = data_->get(); - if (!data && doraise) { - throw Exception("Invalid Data.", PyExcType::kNotFound); - } - return data; -} -// Clion makes some incorrect inferences here. -#pragma clang diagnostic push -#pragma ide diagnostic ignored "UnreachableCode" -#pragma ide diagnostic ignored "ConstantConditionsOC" -#pragma ide diagnostic ignored "ConstantFunctionResult" - -auto PythonClassData::tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) - -> PyObject* { - auto* self = reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; - if (!InLogicThread()) { - throw Exception( - "ERROR: " + std::string(type_obj.tp_name) - + " objects must only be created in the logic thread (current is (" - + GetCurrentThreadName() + ")."); - } - - if (!s_create_empty_) { - throw Exception( - "Can't instantiate Datas directly; use ba.getdata() to get " - "them."); - } - self->data_ = new Object::Ref(); - BA_PYTHON_NEW_CATCH; - } - return reinterpret_cast(self); -} -#pragma clang diagnostic pop - -void PythonClassData::Delete(Object::Ref* ref) { - assert(InLogicThread()); - - // if we're the py-object for a data, clear them out - // (FIXME - wej should pass the old pointer in here to sanity-test that we - // were their ref) - if (ref->exists()) { - (*ref)->ClearPyObject(); - } - delete ref; -} - -void PythonClassData::tp_dealloc(PythonClassData* self) { - BA_PYTHON_TRY; - // these have to be deleted in the logic thread - send the ptr along if need - // be; otherwise do it immediately - if (!InLogicThread()) { - Object::Ref* s = self->data_; - g_logic->thread()->PushCall([s] { Delete(s); }); - } else { - Delete(self->data_); - } - BA_PYTHON_DEALLOC_CATCH; - Py_TYPE(self)->tp_free(reinterpret_cast(self)); -} - -auto PythonClassData::GetValue(PythonClassData* self) -> PyObject* { - BA_PYTHON_TRY; - Data* data = self->data_->get(); - if (data == nullptr) { - throw Exception("Invalid data object.", PyExcType::kNotFound); - } - // haha really need to rename this class. - DataData* datadata = data->data_data(); - datadata->Load(); - datadata->set_last_used_time(GetRealTime()); - PyObject* obj = datadata->object().get(); - assert(obj); - Py_INCREF(obj); - return obj; - BA_PYTHON_CATCH; -} - -bool PythonClassData::s_create_empty_ = false; -PyTypeObject PythonClassData::type_obj; - -PyMethodDef PythonClassData::tp_methods[] = { - {"getvalue", (PyCFunction)GetValue, METH_NOARGS, - "getvalue() -> Any\n" - "\n" - "Return the data object's value.\n" - "\n" - "This can consist of anything representable by json (dicts, lists,\n" - "numbers, bools, None, etc).\n" - "Note that this call will block if the data has not yet been loaded,\n" - "so it can be beneficial to plan a short bit of time between when\n" - "the data object is requested and when it's value is accessed.\n"}, - {nullptr}}; - -} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_data.h b/src/ballistica/python/class/python_class_data.h deleted file mode 100644 index 075fbad6..00000000 --- a/src/ballistica/python/class/python_class_data.h +++ /dev/null @@ -1,36 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_DATA_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_DATA_H_ - -#include "ballistica/core/object.h" -#include "ballistica/python/class/python_class.h" - -namespace ballistica { - -class PythonClassData : public PythonClass { - public: - static auto type_name() -> const char* { return "Data"; } - static PyTypeObject type_obj; - static auto tp_repr(PythonClassData* self) -> PyObject*; - static void SetupType(PyTypeObject* obj); - static auto Create(Data* data) -> PyObject*; - static auto Check(PyObject* o) -> bool { - return PyObject_TypeCheck(o, &type_obj); - } - auto GetData(bool doraise = true) const -> Data*; - - private: - static PyMethodDef tp_methods[]; - static auto GetValue(PythonClassData* self) -> PyObject*; - static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) - -> PyObject*; - static void tp_dealloc(PythonClassData* self); - static void Delete(Object::Ref* ref); - static bool s_create_empty_; - Object::Ref* data_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_DATA_H_ diff --git a/src/ballistica/python/class/python_class_model.cc b/src/ballistica/python/class/python_class_model.cc deleted file mode 100644 index 6da13d4e..00000000 --- a/src/ballistica/python/class/python_class_model.cc +++ /dev/null @@ -1,117 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/class/python_class_model.h" - -#include "ballistica/assets/component/model.h" -#include "ballistica/core/thread.h" -#include "ballistica/logic/logic.h" -#include "ballistica/python/python.h" - -namespace ballistica { - -auto PythonClassModel::tp_repr(PythonClassModel* self) -> PyObject* { - BA_PYTHON_TRY; - Object::Ref m = *(self->model_); - return Py_BuildValue( - "s", (std::string("name() + "\"") : "(empty ref)") + ">") - .c_str()); - BA_PYTHON_CATCH; -} - -void PythonClassModel::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_name = "ba.Model"; - obj->tp_basicsize = sizeof(PythonClassModel); - obj->tp_doc = - "A reference to a model.\n" - "\n" - "Category: **Asset Classes**\n" - "\n" - "Models are used for drawing.\n" - "Use ba.getmodel() to instantiate one."; - obj->tp_repr = (reprfunc)tp_repr; - obj->tp_new = tp_new; - obj->tp_dealloc = (destructor)tp_dealloc; -} - -auto PythonClassModel::Create(Model* model) -> PyObject* { - s_create_empty_ = true; // prevent class from erroring on create - auto* t = reinterpret_cast( - PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); - s_create_empty_ = false; - if (!t) { - throw Exception("ba.Model creation failed."); - } - *(t->model_) = model; - return reinterpret_cast(t); -} - -auto PythonClassModel::GetModel(bool doraise) const -> Model* { - Model* model = model_->get(); - if (!model && doraise) { - throw Exception("Invalid Model.", PyExcType::kNotFound); - } - return model; -} - -// Clion makes some incorrect inferences here. -#pragma clang diagnostic push -#pragma ide diagnostic ignored "UnreachableCode" -#pragma ide diagnostic ignored "ConstantConditionsOC" -#pragma ide diagnostic ignored "ConstantFunctionResult" - -auto PythonClassModel::tp_new(PyTypeObject* type, PyObject* args, - PyObject* kwds) -> PyObject* { - auto* self = reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; - if (!InLogicThread()) { - throw Exception( - "ERROR: " + std::string(type_obj.tp_name) - + " objects must only be created in the logic thread (current is (" - + GetCurrentThreadName() + ")."); - } - if (!s_create_empty_) { - throw Exception( - "Can't instantiate Models directly; use ba.getmodel() to get " - "them."); - } - self->model_ = new Object::Ref(); - BA_PYTHON_NEW_CATCH; - } - return reinterpret_cast(self); -} - -#pragma clang diagnostic pop - -void PythonClassModel::Delete(Object::Ref* ref) { - assert(InLogicThread()); - - // if we're the py-object for a model, clear them out - // (FIXME - we should pass the old pointer in here to sanity-test that we - // were their ref) - if (ref->exists()) { - (*ref)->ClearPyObject(); - } - delete ref; -} - -void PythonClassModel::tp_dealloc(PythonClassModel* self) { - BA_PYTHON_TRY; - // these have to be deleted in the logic thread - send the ptr along if need - // be; otherwise do it immediately - if (!InLogicThread()) { - Object::Ref* m = self->model_; - g_logic->thread()->PushCall([m] { Delete(m); }); - } else { - Delete(self->model_); - } - BA_PYTHON_DEALLOC_CATCH; - Py_TYPE(self)->tp_free(reinterpret_cast(self)); -} - -bool PythonClassModel::s_create_empty_ = false; -PyTypeObject PythonClassModel::type_obj; - -} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_model.h b/src/ballistica/python/class/python_class_model.h deleted file mode 100644 index b6ca9f7c..00000000 --- a/src/ballistica/python/class/python_class_model.h +++ /dev/null @@ -1,34 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_MODEL_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_MODEL_H_ - -#include "ballistica/core/object.h" -#include "ballistica/python/class/python_class.h" - -namespace ballistica { - -class PythonClassModel : public PythonClass { - public: - static auto type_name() -> const char* { return "Model"; } - static auto tp_repr(PythonClassModel* self) -> PyObject*; - static void SetupType(PyTypeObject* obj); - static PyTypeObject type_obj; - static auto Create(Model* model) -> PyObject*; - static auto Check(PyObject* o) -> bool { - return PyObject_TypeCheck(o, &type_obj); - } - auto GetModel(bool doraise = true) const -> Model*; - - private: - static bool s_create_empty_; - static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) - -> PyObject*; - static void tp_dealloc(PythonClassModel* self); - static void Delete(Object::Ref* ref); - Object::Ref* model_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_MODEL_H_ diff --git a/src/ballistica/python/class/python_class_sound.cc b/src/ballistica/python/class/python_class_sound.cc deleted file mode 100644 index 6a76d646..00000000 --- a/src/ballistica/python/class/python_class_sound.cc +++ /dev/null @@ -1,116 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/class/python_class_sound.h" - -#include "ballistica/assets/component/sound.h" -#include "ballistica/core/thread.h" -#include "ballistica/logic/logic.h" -#include "ballistica/python/python.h" - -namespace ballistica { - -auto PythonClassSound::tp_repr(PythonClassSound* self) -> PyObject* { - BA_PYTHON_TRY; - Object::Ref m = *(self->sound_); - return Py_BuildValue( - "s", (std::string("name() + "\"") : "(empty ref)") + ">") - .c_str()); - BA_PYTHON_CATCH; -} - -void PythonClassSound::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_name = "ba.Sound"; - obj->tp_basicsize = sizeof(PythonClassSound); - obj->tp_doc = - "A reference to a sound.\n" - "\n" - "Category: **Asset Classes**\n" - "\n" - "Use ba.getsound() to instantiate one."; - obj->tp_repr = (reprfunc)tp_repr; - obj->tp_new = tp_new; - obj->tp_dealloc = (destructor)tp_dealloc; -} - -auto PythonClassSound::Create(Sound* sound) -> PyObject* { - s_create_empty_ = true; // prevent class from erroring on create - auto* t = reinterpret_cast( - PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); - s_create_empty_ = false; - if (!t) { - throw Exception("ba.Sound creation failed."); - } - *(t->sound_) = sound; - return reinterpret_cast(t); -} - -auto PythonClassSound::GetSound(bool doraise) const -> Sound* { - Sound* sound = sound_->get(); - if (!sound && doraise) { - throw Exception("Invalid Sound.", PyExcType::kNotFound); - } - return sound; -} - -// Clion makes some incorrect inferences here. -#pragma clang diagnostic push -#pragma ide diagnostic ignored "UnreachableCode" -#pragma ide diagnostic ignored "ConstantConditionsOC" -#pragma ide diagnostic ignored "ConstantFunctionResult" - -auto PythonClassSound::tp_new(PyTypeObject* type, PyObject* args, - PyObject* kwds) -> PyObject* { - auto* self = reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; - if (!InLogicThread()) { - throw Exception( - "ERROR: " + std::string(type_obj.tp_name) - + " objects must only be created in the logic thread (current is (" - + GetCurrentThreadName() + ")."); - } - if (!s_create_empty_) { - throw Exception( - "Can't instantiate Sounds directly; use ba.getsound() to get " - "them."); - } - self->sound_ = new Object::Ref(); - BA_PYTHON_NEW_CATCH; - } - return reinterpret_cast(self); -} - -#pragma clang diagnostic pop - -void PythonClassSound::Delete(Object::Ref* ref) { - assert(InLogicThread()); - - // if we're the py-object for a sound, clear them out - // (FIXME - wej should pass the old pointer in here to sanity-test that we - // were their ref) - if (ref->exists()) { - (*ref)->ClearPyObject(); - } - delete ref; -} - -void PythonClassSound::tp_dealloc(PythonClassSound* self) { - BA_PYTHON_TRY; - // these have to be deleted in the logic thread - send the ptr along if need - // be; otherwise do it immediately - if (!InLogicThread()) { - Object::Ref* s = self->sound_; - g_logic->thread()->PushCall([s] { Delete(s); }); - } else { - Delete(self->sound_); - } - BA_PYTHON_DEALLOC_CATCH; - Py_TYPE(self)->tp_free(reinterpret_cast(self)); -} - -bool PythonClassSound::s_create_empty_ = false; -PyTypeObject PythonClassSound::type_obj; - -} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_sound.h b/src/ballistica/python/class/python_class_sound.h deleted file mode 100644 index ccb68bb7..00000000 --- a/src/ballistica/python/class/python_class_sound.h +++ /dev/null @@ -1,34 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SOUND_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SOUND_H_ - -#include "ballistica/core/object.h" -#include "ballistica/python/class/python_class.h" - -namespace ballistica { - -class PythonClassSound : public PythonClass { - public: - static auto type_name() -> const char* { return "Sound"; } - static PyTypeObject type_obj; - static auto tp_repr(PythonClassSound* self) -> PyObject*; - static void SetupType(PyTypeObject* obj); - static auto Create(Sound* sound) -> PyObject*; - static auto Check(PyObject* o) -> bool { - return PyObject_TypeCheck(o, &type_obj); - } - auto GetSound(bool doraise = true) const -> Sound*; - - private: - static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) - -> PyObject*; - static void tp_dealloc(PythonClassSound* self); - static void Delete(Object::Ref* ref); - static bool s_create_empty_; - Object::Ref* sound_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SOUND_H_ diff --git a/src/ballistica/python/class/python_class_texture.cc b/src/ballistica/python/class/python_class_texture.cc deleted file mode 100644 index adb4b4b5..00000000 --- a/src/ballistica/python/class/python_class_texture.cc +++ /dev/null @@ -1,109 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/class/python_class_texture.h" - -#include "ballistica/assets/component/texture.h" -#include "ballistica/core/thread.h" -#include "ballistica/logic/logic.h" -#include "ballistica/python/python.h" - -namespace ballistica { - -auto PythonClassTexture::tp_repr(PythonClassTexture* self) -> PyObject* { - BA_PYTHON_TRY; - Object::Ref t = *(self->texture_); - return Py_BuildValue( - "s", (std::string("name() + "\"") : "(empty ref)") + ">") - .c_str()); - BA_PYTHON_CATCH; -} - -void PythonClassTexture::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_name = "ba.Texture"; - obj->tp_basicsize = sizeof(PythonClassTexture); - obj->tp_doc = - "A reference to a texture.\n" - "\n" - "Category: **Asset Classes**\n" - "\n" - "Use ba.gettexture() to instantiate one."; - obj->tp_repr = (reprfunc)tp_repr; - obj->tp_new = tp_new; - obj->tp_dealloc = (destructor)tp_dealloc; -} - -auto PythonClassTexture::Create(Texture* texture) -> PyObject* { - s_create_empty_ = true; // prevent class from erroring on create - assert(texture != nullptr); - auto* t = reinterpret_cast( - PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); - s_create_empty_ = false; - if (!t) { - throw Exception("ba.Texture creation failed."); - } - *(t->texture_) = texture; - return reinterpret_cast(t); -} - -auto PythonClassTexture::GetTexture(bool doraise) const -> Texture* { - Texture* texture = texture_->get(); - if (!texture && doraise) { - throw Exception("Invalid Texture.", PyExcType::kNotFound); - } - return texture; -} - -auto PythonClassTexture::tp_new(PyTypeObject* type, PyObject* args, - PyObject* keywds) -> PyObject* { - auto* self = reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; - if (!InLogicThread()) { - throw Exception( - "ERROR: " + std::string(type_obj.tp_name) - + " objects must only be created in the logic thread (current is (" - + GetCurrentThreadName() + ")."); - } - if (!s_create_empty_) { - throw Exception( - "Can't instantiate Textures directly; use ba.gettexture() to get " - "them."); - } - self->texture_ = new Object::Ref(); - BA_PYTHON_NEW_CATCH; - } - return reinterpret_cast(self); -} - -void PythonClassTexture::Delete(Object::Ref* ref) { - assert(InLogicThread()); - - // If we're the py-object for a texture, kill our reference to it. - // (FIXME - we should pass the old py obj pointer in here to - // make sure that we were their python obj as a sanity test) - if (ref->exists()) { - (*ref)->ClearPyObject(); - } - delete ref; -} - -void PythonClassTexture::tp_dealloc(PythonClassTexture* self) { - BA_PYTHON_TRY; - // These have to be deleted in the logic thread - send the ptr along if need - // be; otherwise do it immediately. - if (!InLogicThread()) { - Object::Ref* t = self->texture_; - g_logic->thread()->PushCall([t] { Delete(t); }); - } else { - Delete(self->texture_); - } - BA_PYTHON_DEALLOC_CATCH; - Py_TYPE(self)->tp_free(reinterpret_cast(self)); -} - -bool PythonClassTexture::s_create_empty_ = false; -PyTypeObject PythonClassTexture::type_obj; - -} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_texture.h b/src/ballistica/python/class/python_class_texture.h deleted file mode 100644 index e0cb92c8..00000000 --- a/src/ballistica/python/class/python_class_texture.h +++ /dev/null @@ -1,34 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_TEXTURE_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_TEXTURE_H_ - -#include "ballistica/core/object.h" -#include "ballistica/python/class/python_class.h" - -namespace ballistica { - -class PythonClassTexture : public PythonClass { - public: - static auto type_name() -> const char* { return "Texture"; } - static auto tp_repr(PythonClassTexture* self) -> PyObject*; - static void SetupType(PyTypeObject* obj); - static PyTypeObject type_obj; - static auto Create(Texture* texture) -> PyObject*; - static auto Check(PyObject* o) -> bool { - return PyObject_TypeCheck(o, &type_obj); - } - auto GetTexture(bool doraise = true) const -> Texture*; - - private: - static bool s_create_empty_; - static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) - -> PyObject*; - static void tp_dealloc(PythonClassTexture* self); - static void Delete(Object::Ref* ref); - Object::Ref* texture_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_TEXTURE_H_ diff --git a/src/ballistica/python/class/python_class_timer.cc b/src/ballistica/python/class/python_class_timer.cc deleted file mode 100644 index 1895f793..00000000 --- a/src/ballistica/python/class/python_class_timer.cc +++ /dev/null @@ -1,182 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/class/python_class_timer.h" - -#include "ballistica/core/thread.h" -#include "ballistica/logic/logic.h" -#include "ballistica/python/python_context_call_runnable.h" - -namespace ballistica { - -void PythonClassTimer::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_name = "ba.Timer"; - obj->tp_basicsize = sizeof(PythonClassTimer); - obj->tp_doc = - "Timer(time: float, call: Callable[[], Any], repeat: bool = False,\n" - " timetype: ba.TimeType = TimeType.SIM,\n" - " timeformat: ba.TimeFormat = TimeFormat.SECONDS,\n" - " suppress_format_warning: bool = False)\n" - "\n" - "Timers are used to run code at later points in time.\n" - "\n" - "Category: **General Utility Classes**\n" - "\n" - "This class encapsulates a timer in the current ba.Context.\n" - "The underlying timer will be destroyed when either this object is\n" - "no longer referenced or when its Context (Activity, etc.) dies. If you\n" - "do not want to worry about keeping a reference to your timer around,\n" - "you should use the ba.timer() function instead.\n" - "\n" - "###### time\n" - "> Length of time (in seconds by default) that the timer will wait\n" - "before firing. Note that the actual delay experienced may vary\n" - "depending on the timetype. (see below)\n" - "\n" - "###### call\n" - "> A callable Python object. Note that the timer will retain a\n" - "strong reference to the callable for as long as it exists, so you\n" - "may want to look into concepts such as ba.WeakCall if that is not\n" - "desired.\n" - "\n" - "###### repeat\n" - "> If True, the timer will fire repeatedly, with each successive\n" - "firing having the same delay as the first.\n" - "\n" - "###### timetype\n" - "> A ba.TimeType value determining which timeline the timer is\n" - "placed onto.\n" - "\n" - "###### timeformat\n" - "> A ba.TimeFormat value determining how the passed time is\n" - "interpreted.\n" - "\n" - "##### Example\n" - "\n" - "Use a Timer object to print repeatedly for a few seconds:\n" - ">>> def say_it():\n" - "... ba.screenmessage('BADGER!')\n" - "... def stop_saying_it():\n" - "... self.t = None\n" - "... ba.screenmessage('MUSHROOM MUSHROOM!')\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" - "... ba.timer(3.89, stop_saying_it)\n"; - obj->tp_new = tp_new; - obj->tp_dealloc = (destructor)tp_dealloc; -} - -auto PythonClassTimer::tp_new(PyTypeObject* type, PyObject* args, - PyObject* keywds) -> PyObject* { - auto* self = reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; - - if (!InLogicThread()) { - throw Exception( - "ERROR: " + std::string(type_obj.tp_name) - + " objects must only be created in the logic thread (current is (" - + GetCurrentThreadName() + ")."); - } - - self->context_ = new Context(); - - PyObject* length_obj{}; - int64_t length; - int repeat{}; - int suppress_format_warning{}; - PyObject* call_obj{}; - PyObject* time_type_obj{}; - PyObject* time_format_obj{}; - static const char* kwlist[] = {"time", "call", - "repeat", "timetype", - "timeformat", "suppress_format_warning", - nullptr}; - if (!PyArg_ParseTupleAndKeywords( - args, keywds, "OO|pOOp", const_cast(kwlist), &length_obj, - &call_obj, &repeat, &time_type_obj, &time_format_obj, - &suppress_format_warning)) { - return nullptr; - } - - auto time_type = TimeType::kSim; - if (time_type_obj != nullptr) { - time_type = Python::GetPyEnum_TimeType(time_type_obj); - } - auto time_format = TimeFormat::kSeconds; - if (time_format_obj != nullptr) { - time_format = Python::GetPyEnum_TimeFormat(time_format_obj); - } - -#if BA_TEST_BUILD || BA_DEBUG_BUILD - if (!suppress_format_warning) { - g_python->TimeFormatCheck(time_format, length_obj); - } -#endif - - // We currently work with integer milliseconds internally. - if (time_format == TimeFormat::kSeconds) { - length = static_cast(Python::GetPyDouble(length_obj) * 1000.0); - } else if (time_format == TimeFormat::kMilliseconds) { - length = Python::GetPyInt64(length_obj); - } else { - throw Exception("Invalid timeformat: '" - + std::to_string(static_cast(time_format)) - + "'.", - PyExcType::kValue); - } - if (length < 0) { - throw Exception("Timer length < 0.", PyExcType::kValue); - } - - auto runnable(Object::New(call_obj)); - - self->time_type_ = time_type; - - // Now just make sure we've got a valid context-target and ask us to - // make us a timer. - if (!self->context_->target.exists()) { - throw Exception("Invalid current context.", PyExcType::kContext); - } - self->timer_id_ = self->context_->target->NewTimer( - self->time_type_, length, static_cast(repeat), runnable); - self->have_timer_ = true; - - BA_PYTHON_NEW_CATCH; - } - return reinterpret_cast(self); -} -void PythonClassTimer::DoDelete(bool have_timer, TimeType time_type, - int timer_id, Context* context) { - assert(InLogicThread()); - if (!context) { - return; - } - if (context->target.exists() && have_timer) { - context->target->DeleteTimer(time_type, timer_id); - } - delete context; -} - -void PythonClassTimer::tp_dealloc(PythonClassTimer* self) { - BA_PYTHON_TRY; - // These have to be deleted in the logic thread. - if (!InLogicThread()) { - auto a0 = self->have_timer_; - auto a1 = self->time_type_; - auto a2 = self->timer_id_; - auto a3 = self->context_; - g_logic->thread()->PushCall( - [a0, a1, a2, a3] { PythonClassTimer::DoDelete(a0, a1, a2, a3); }); - } else { - DoDelete(self->have_timer_, self->time_type_, self->timer_id_, - self->context_); - } - BA_PYTHON_DEALLOC_CATCH; - Py_TYPE(self)->tp_free(reinterpret_cast(self)); -} - -PyTypeObject PythonClassTimer::type_obj; - -} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_timer.h b/src/ballistica/python/class/python_class_timer.h deleted file mode 100644 index 7c04f93d..00000000 --- a/src/ballistica/python/class/python_class_timer.h +++ /dev/null @@ -1,35 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_TIMER_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_TIMER_H_ - -#include "ballistica/ballistica.h" -#include "ballistica/python/class/python_class.h" -#include "ballistica/python/python.h" - -namespace ballistica { - -class PythonClassTimer : public PythonClass { - public: - static auto type_name() -> const char* { return "Timer"; } - static void SetupType(PyTypeObject* obj); - static auto Check(PyObject* o) -> bool { - return PyObject_TypeCheck(o, &type_obj); - } - static PyTypeObject type_obj; - - private: - static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) - -> PyObject*; - static void tp_dealloc(PythonClassTimer* self); - static void DoDelete(bool have_timer, TimeType time_type, int timer_id, - Context* context); - TimeType time_type_; - int timer_id_; - Context* context_; - bool have_timer_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_TIMER_H_ diff --git a/src/ballistica/python/methods/python_methods_app.cc b/src/ballistica/python/methods/python_methods_app.cc deleted file mode 100644 index 2de0a6dc..00000000 --- a/src/ballistica/python/methods/python_methods_app.cc +++ /dev/null @@ -1,1260 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/methods/python_methods_app.h" - -#include "ballistica/app/app.h" -#include "ballistica/app/app_flavor.h" -#include "ballistica/assets/component/texture.h" -#include "ballistica/core/logging.h" -#include "ballistica/core/thread.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/logic/connection/connection_set.h" -#include "ballistica/logic/host_activity.h" -#include "ballistica/logic/session/host_session.h" -#include "ballistica/logic/session/replay_client_session.h" -#include "ballistica/python/class/python_class_activity_data.h" -#include "ballistica/python/class/python_class_session_data.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_context_call_runnable.h" -#include "ballistica/scene/scene.h" -#include "ballistica/scene/scene_stream.h" -#include "ballistica/ui/ui.h" - -namespace ballistica { - -// Python does lots of signed bitwise stuff; turn off those warnings here. -#pragma clang diagnostic push -#pragma ide diagnostic ignored "hicpp-signed-bitwise" -#pragma ide diagnostic ignored "RedundantCast" - -auto PyAppName(PyObject* self) -> PyObject* { - BA_PYTHON_TRY; - - // This will get subbed out by standard filtering. - return PyUnicode_FromString("ballisticacore"); - BA_PYTHON_CATCH; -} - -auto PyAppNameUpper(PyObject* self) -> PyObject* { - BA_PYTHON_TRY; - - // This will get subbed out by standard filtering. - return PyUnicode_FromString("BallisticaCore"); - BA_PYTHON_CATCH; -} - -auto PyIsXCodeBuild(PyObject* self) -> PyObject* { - BA_PYTHON_TRY; - if (g_buildconfig.xcode_build()) { - Py_RETURN_TRUE; - } - Py_RETURN_FALSE; - BA_PYTHON_CATCH; -} - -auto PyCanDisplayFullUnicode(PyObject* self) -> PyObject* { - BA_PYTHON_TRY; - if (g_buildconfig.enable_os_font_rendering()) { - Py_RETURN_TRUE; - } - Py_RETURN_FALSE; - BA_PYTHON_CATCH; -} - -auto PyGetSession(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - int raise = true; - static const char* kwlist[] = {"doraise", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "|i", - const_cast(kwlist), &raise)) { - return nullptr; - } - if (HostSession* hs = Context::current().GetHostSession()) { - PyObject* obj = hs->GetSessionPyObj(); - if (obj) { - Py_INCREF(obj); - return obj; - } - } else { - if (raise) { - throw Exception(PyExcType::kSessionNotFound); - } - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyNewHostSession(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* benchmark_type_str = nullptr; - static const char* kwlist[] = {"sessiontype", "benchmark_type", nullptr}; - PyObject* sessiontype_obj; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|s", - const_cast(kwlist), &sessiontype_obj, - &benchmark_type_str)) { - return nullptr; - } - BenchmarkType benchmark_type = BenchmarkType::kNone; - if (benchmark_type_str != nullptr) { - if (!strcmp(benchmark_type_str, "cpu")) { - benchmark_type = BenchmarkType::kCPU; - } else if (!strcmp(benchmark_type_str, "gpu")) { - benchmark_type = BenchmarkType::kGPU; - } else { - throw Exception( - "Invalid benchmark type: '" + std::string(benchmark_type_str) + "'", - PyExcType::kValue); - } - } - g_logic->LaunchHostSession(sessiontype_obj, benchmark_type); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyNewReplaySession(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - std::string file_name; - PyObject* file_name_obj; - static const char* kwlist[] = {"file_name", nullptr}; - if (!PyArg_ParseTupleAndKeywords( - args, keywds, "O", const_cast(kwlist), &file_name_obj)) { - return nullptr; - } - file_name = Python::GetPyString(file_name_obj); - g_logic->LaunchReplaySession(file_name); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyIsInReplay(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - BA_PRECONDITION(InLogicThread()); - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - if (dynamic_cast(g_logic->GetForegroundSession())) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } - BA_PYTHON_CATCH; -} - -auto PyAppInstanceUUID(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - return PyUnicode_FromString(GetAppInstanceUUID().c_str()); - BA_PYTHON_CATCH; -} - -auto PyUserRanCommands(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - g_app->user_ran_commands = true; - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyRegisterSession(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - assert(InLogicThread()); - PyObject* session_obj; - static const char* kwlist[] = {"session", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", - const_cast(kwlist), &session_obj)) { - return nullptr; - } - HostSession* hsc = Context::current().GetHostSession(); - if (!hsc) { - throw Exception("No HostSession found."); - } - - // Store our py obj with our HostSession and return - // the HostSession to be stored with our py obj. - hsc->RegisterPySession(session_obj); - return PythonClassSessionData::Create(hsc); - BA_PYTHON_CATCH; -} - -auto PyRegisterActivity(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - assert(InLogicThread()); - PyObject* activity_obj; - static const char* kwlist[] = {"activity", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", - const_cast(kwlist), &activity_obj)) { - return nullptr; - } - HostSession* hs = Context::current().GetHostSession(); - if (!hs) { - throw Exception("No HostSession found"); - } - - // Generate and return an ActivityData for this guy.. - // (basically just a link to its C++ equivalent). - return PythonClassActivityData::Create(hs->RegisterPyActivity(activity_obj)); - BA_PYTHON_CATCH; -} - -auto PyGetForegroundHostSession(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - - // Note: we return None if not in the logic thread. - HostSession* s = InLogicThread() - ? g_logic->GetForegroundContext().GetHostSession() - : nullptr; - if (s != nullptr) { - PyObject* obj = s->GetSessionPyObj(); - Py_INCREF(obj); - return obj; - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyNewActivity(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - - static const char* kwlist[] = {"activity_type", "settings", nullptr}; - PyObject* activity_type_obj; - PyObject* settings_obj = Py_None; - PythonRef settings; - - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|O", - const_cast(kwlist), - &activity_type_obj, &settings_obj)) { - return nullptr; - } - - // If they passed a settings dict, make a shallow copy of it (so we dont - // inadvertently mess up level lists or whatever the settings came from). - if (settings_obj != Py_None) { - if (!PyDict_Check(settings_obj)) { - throw Exception("Expected a dict for settings.", PyExcType::kType); - } - PythonRef args2(Py_BuildValue("(O)", settings_obj), PythonRef::kSteal); - settings = g_python->obj(Python::ObjID::kShallowCopyCall).Call(args2); - if (!settings.exists()) { - throw Exception("Unable to shallow-copy settings."); - } - } else { - settings.Acquire(settings_obj); - } - - HostSession* hs = Context::current().GetHostSession(); - if (!hs) { - throw Exception("No HostSession found.", PyExcType::kContext); - } - return hs->NewHostActivity(activity_type_obj, settings.get()); - - BA_PYTHON_CATCH; -} - -auto PyGetActivity(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - int raise = true; - static const char* kwlist[] = {"doraise", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "|i", - const_cast(kwlist), &raise)) { - return nullptr; - } - - // Fail gracefully if called from outside the logic thread. - if (!InLogicThread()) { - Py_RETURN_NONE; - } - - if (HostActivity* hostactivity = Context::current().GetHostActivity()) { - PyObject* obj = hostactivity->GetPyActivity(); - Py_INCREF(obj); - return obj; - } else { - if (raise) { - throw Exception(PyExcType::kActivityNotFound); - } - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyPushCall(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - PyObject* call_obj; - int from_other_thread{}; - int suppress_warning{}; - int other_thread_use_fg_context{}; - int raw{0}; - static const char* kwlist[] = {"call", - "from_other_thread", - "suppress_other_thread_warning", - "other_thread_use_fg_context", - "raw", - nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|pppp", - const_cast(kwlist), &call_obj, - &from_other_thread, &suppress_warning, - &other_thread_use_fg_context, &raw)) { - return nullptr; - } - - // 'raw' mode does no thread checking and no context saves/restores. - if (raw) { - Py_INCREF(call_obj); - g_logic->thread()->PushCall([call_obj] { - assert(InLogicThread()); - - PythonRef(call_obj, PythonRef::kSteal).Call(); - }); - } else if (from_other_thread) { - // Warn the user not to use this from the logic thread since it doesnt - // save/restore context. - if (!suppress_warning && InLogicThread()) { - g_python->IssueCallInLogicThreadWarning(call_obj); - } - - // This gets called from other python threads so we can't construct - // Objects and things here or we'll trip our thread-checks. Instead we - // just increment the python object's refcount and pass it along raw; - // the logic thread decrements it on the other end. - Py_INCREF(call_obj); - g_logic->PushPythonRawCallable(call_obj, other_thread_use_fg_context); - } else { - if (!InLogicThread()) { - throw Exception("You must use from_other_thread mode."); - } - g_logic->PushPythonCall(Object::New(call_obj)); - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyTime(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - - PyObject* time_type_obj = nullptr; - PyObject* time_format_obj = nullptr; - static const char* kwlist[] = {"timetype", "timeformat", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "|OO", - const_cast(kwlist), &time_type_obj, - &time_format_obj)) { - return nullptr; - } - - auto time_type = TimeType::kSim; - if (time_type_obj != nullptr) { - time_type = Python::GetPyEnum_TimeType(time_type_obj); - } - auto time_format = TimeFormat::kSeconds; - if (time_format_obj != nullptr) { - time_format = Python::GetPyEnum_TimeFormat(time_format_obj); - } - - millisecs_t timeval; - if (time_type == TimeType::kReal) { - // Special case; we don't require a context for 'real'. - timeval = GetRealTime(); - } else { - // Make sure we've got a valid context-target and ask it for - // this type of time. - if (!Context::current().target.exists()) { - throw Exception(PyExcType::kContext); - } - timeval = Context::current().target->GetTime(time_type); - } - - if (time_format == TimeFormat::kSeconds) { - return PyFloat_FromDouble(0.001 * timeval); - } else if (time_format == TimeFormat::kMilliseconds) { - return PyLong_FromLong(static_cast_check_fit(timeval)); // NOLINT - } else { - throw Exception( - "Invalid timeformat: " + std::to_string(static_cast(time_format)), - PyExcType::kValue); - } - BA_PYTHON_CATCH; -} - -auto PyTimer(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - assert(InLogicThread()); - - PyObject* length_obj; - int64_t length; - int repeat = 0; - int suppress_format_warning = 0; - PyObject* call_obj; - PyObject* time_type_obj = nullptr; - PyObject* time_format_obj = nullptr; - static const char* kwlist[] = {"time", "call", - "repeat", "timetype", - "timeformat", "suppress_format_warning", - nullptr}; - if (!PyArg_ParseTupleAndKeywords( - args, keywds, "OO|pOOp", const_cast(kwlist), &length_obj, - &call_obj, &repeat, &time_type_obj, &time_format_obj, - &suppress_format_warning)) { - return nullptr; - } - - auto time_type = TimeType::kSim; - if (time_type_obj != nullptr) { - time_type = Python::GetPyEnum_TimeType(time_type_obj); - } - auto time_format = TimeFormat::kSeconds; - if (time_format_obj != nullptr) { - time_format = Python::GetPyEnum_TimeFormat(time_format_obj); - } - -#if BA_TEST_BUILD || BA_DEBUG_BUILD - if (!suppress_format_warning) { - g_python->TimeFormatCheck(time_format, length_obj); - } -#endif - - // We currently work with integer milliseconds internally. - if (time_format == TimeFormat::kSeconds) { - length = static_cast(Python::GetPyDouble(length_obj) * 1000.0); - } else if (time_format == TimeFormat::kMilliseconds) { - length = Python::GetPyInt64(length_obj); - } else { - throw Exception("invalid timeformat: '" - + std::to_string(static_cast(time_format)) + "'", - PyExcType::kValue); - } - if (length < 0) { - throw Exception("Timer length < 0", PyExcType::kValue); - } - - // Grab a ref to this here so it doesn't leak on exceptions. - auto runnable(Object::New(call_obj)); - - // Special case; we disallow repeating real timers currently. - if (time_type == TimeType::kReal && repeat) { - throw Exception("Repeating real timers not allowed here; use ba.Timer().", - PyExcType::kValue); - } - - // Now just make sure we've got a valid context-target and ask us to - // make us a timer. - if (!Context::current().target.exists()) { - throw Exception(PyExcType::kContext); - } - Context::current().target->NewTimer(time_type, length, - static_cast(repeat), runnable); - - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyScreenMessage(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* message = nullptr; - PyObject* color_obj = Py_None; - int top = 0; - int transient = 0; - PyObject* image_obj = Py_None; - PyObject* message_obj; - PyObject* clients_obj = Py_None; - int log = 0; - static const char* kwlist[] = {"message", "color", "top", "image", - "log", "clients", "transient", nullptr}; - if (!PyArg_ParseTupleAndKeywords( - args, keywds, "O|OpOiOi", const_cast(kwlist), &message_obj, - &color_obj, &top, &image_obj, &log, &clients_obj, &transient)) { - return nullptr; - } - std::string message_str = Python::GetPyString(message_obj); - message = message_str.c_str(); - Vector3f color{1, 1, 1}; - if (color_obj != Py_None) { - color = Python::GetPyVector3f(color_obj); - } - if (message == nullptr) { - PyErr_SetString(PyExc_AttributeError, "No message provided"); - return nullptr; - } - if (log) { - Log(LogLevel::kInfo, message); - } - - // Transient messages get sent to clients as high-level messages instead of - // being embedded into the game-stream. - if (transient) { - // This option doesn't support top or icons currently. - if (image_obj != Py_None) { - throw Exception( - "The 'image' option is not currently supported for transient mode " - "messages.", - PyExcType::kValue); - } - if (top) { - throw Exception( - "The 'top' option is not currently supported for transient mode " - "messages.", - PyExcType::kValue); - } - std::vector client_ids; - if (clients_obj != Py_None) { - std::vector client_ids2 = Python::GetPyInts(clients_obj); - g_logic->connections()->SendScreenMessageToSpecificClients( - message, color.x, color.y, color.z, client_ids2); - } else { - g_logic->connections()->SendScreenMessageToAll(message, color.x, color.y, - color.z); - } - } else { - // Currently specifying client_ids only works for transient messages; we'd - // need a protocol change to support that in game output streams. - // (or maintaining separate streams per client; yuck) - if (clients_obj != Py_None) { - throw Exception( - "Specifying clients only works when using the 'transient' option", - PyExcType::kValue); - } - Scene* context_scene = Context::current().GetMutableScene(); - SceneStream* output_stream = - context_scene ? context_scene->GetSceneStream() : nullptr; - - Texture* texture = nullptr; - Texture* tint_texture = nullptr; - Vector3f tint_color{1.0f, 1.0f, 1.0f}; - Vector3f tint2_color{1.0f, 1.0f, 1.0f}; - if (image_obj != Py_None) { - if (PyDict_Check(image_obj)) { - PyObject* obj = PyDict_GetItemString(image_obj, "texture"); - if (!obj) - throw Exception("Provided image dict contains no 'texture' entry.", - PyExcType::kValue); - texture = Python::GetPyTexture(obj); - - obj = PyDict_GetItemString(image_obj, "tint_texture"); - if (!obj) - throw Exception( - "Provided image dict contains no 'tint_texture' entry.", - PyExcType::kValue); - tint_texture = Python::GetPyTexture(obj); - - obj = PyDict_GetItemString(image_obj, "tint_color"); - if (!obj) - throw Exception("Provided image dict contains no 'tint_color' entry", - PyExcType::kValue); - tint_color = Python::GetPyVector3f(obj); - obj = PyDict_GetItemString(image_obj, "tint2_color"); - if (!obj) - throw Exception("Provided image dict contains no 'tint2_color' entry", - PyExcType::kValue); - tint2_color = Python::GetPyVector3f(obj); - } else { - texture = Python::GetPyTexture(image_obj); - } - } - if (output_stream) { - // FIXME: for now we just do bottom messages. - if (texture == nullptr && !top) { - output_stream->ScreenMessageBottom(message, color.x, color.y, color.z); - } else if (top && texture != nullptr && tint_texture != nullptr) { - if (texture->scene() != context_scene) { - throw Exception("Texture is not from the current context.", - PyExcType::kContext); - } - if (tint_texture->scene() != context_scene) - throw Exception("Tint-texture is not from the current context.", - PyExcType::kContext); - output_stream->ScreenMessageTop( - message, color.x, color.y, color.z, texture, tint_texture, - tint_color.x, tint_color.y, tint_color.z, tint2_color.x, - tint2_color.y, tint2_color.z); - } else { - Log(LogLevel::kError, "Unhandled screenmessage output_stream case."); - } - } - - // Now display it locally. - g_graphics->AddScreenMessage(message, color, static_cast(top), - texture, tint_texture, tint_color, - tint2_color); - } - - Py_RETURN_NONE; - - BA_PYTHON_CATCH; -} - -auto PyQuit(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {"soft", "back", nullptr}; - int soft = 0; - int back = 0; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "|ii", - const_cast(kwlist), &soft, &back)) { - return nullptr; - } - - // FIXME this should all just go through platform - - if (g_buildconfig.ostype_ios_tvos()) { - // This should never be called on iOS - Log(LogLevel::kError, "Quit called."); - } - - bool handled = false; - - // A few types get handled specially on android. - if (g_buildconfig.ostype_android()) { -#pragma clang diagnostic push -#pragma ide diagnostic ignored "ConstantConditionsOC" - - if (!handled && back) { - // Back-quit simply synthesizes a back press. - // Note to self: I remember this behaved slightly differently than - // doing a soft quit but I should remind myself how... - g_platform->AndroidSynthesizeBackPress(); - handled = true; - } - -#pragma clang diagnostic pop - - if (!handled && soft) { - // Soft-quit just kills our activity but doesn't run app shutdown. - // Thus we'll be able to spin back up (reset to the main menu) - // if the user re-launches us. - g_platform->AndroidQuitActivity(); - handled = true; - } - } - if (!handled) { - g_logic->PushShutdownCall(false); - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -#if BA_DEBUG_BUILD -auto PyBless(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - ScreenMessage("WOULD BLESS BUILD " + std::to_string(kAppBuildNumber)); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} -#endif // BA_DEBUG_BUILD - -auto PyApplyConfig(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - - // Hmm; python runs in the logic thread; technically we could just run - // ApplyConfig() immediately (though pushing is probably safer). - g_logic->PushApplyConfigCall(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyCommitConfig(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - PyObject* config_obj; - static const char* kwlist[] = {"config", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", - const_cast(kwlist), &config_obj)) { - return nullptr; - } - if (config_obj == nullptr || !Python::IsPyString(config_obj)) { - throw Exception("ERROR ON JSON DUMP"); - } - std::string final_str = Python::GetPyString(config_obj); - std::string path = g_platform->GetConfigFilePath(); - std::string path_temp = path + ".tmp"; - std::string path_prev = path + ".prev"; - if (explicit_bool(true)) { - FILE* f_out = g_platform->FOpen(path_temp.c_str(), "wb"); - if (f_out == nullptr) { - throw Exception("Error opening config file for writing: '" + path_temp - + "': " + g_platform->GetErrnoString()); - } - - // Write to temp file. - size_t result = fwrite(&final_str[0], final_str.size(), 1, f_out); - if (result != 1) { - fclose(f_out); - throw Exception("Error writing config file to '" + path_temp - + "': " + g_platform->GetErrnoString()); - } - fclose(f_out); - - // Now backup any existing config to .prev. - if (g_platform->FilePathExists(path)) { - // On windows, rename doesn't overwrite existing files.. need to kill - // the old explicitly. - // (hmm; should we just do this everywhere for consistency?) - if (g_buildconfig.ostype_windows()) { - if (g_platform->FilePathExists(path_prev)) { - int result2 = g_platform->Remove(path_prev.c_str()); - if (result2 != 0) { - throw Exception("Error removing prev config file '" + path_prev - + "': " + g_platform->GetErrnoString()); - } - } - } - int result2 = g_platform->Rename(path.c_str(), path_prev.c_str()); - if (result2 != 0) { - throw Exception("Error backing up config file to '" + path_prev - + "': " + g_platform->GetErrnoString()); - } - } - - // Now move temp into place. - int result2 = g_platform->Rename(path_temp.c_str(), path.c_str()); - if (result2 != 0) { - throw Exception("Error renaming temp config file to final '" + path - + "': " + g_platform->GetErrnoString()); - } - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyEnv(PyObject* self) -> PyObject* { - BA_PYTHON_TRY; - assert(g_app->is_bootstrapped); - - static PyObject* env_obj = nullptr; - - // Just build this once and recycle it. - if (env_obj == nullptr) { - std::string config_path = g_platform->GetConfigFilePath(); - PyObject* is_debug_build_obj; -#if BA_DEBUG_BUILD - is_debug_build_obj = Py_True; -#else - is_debug_build_obj = Py_False; -#endif - PyObject* is_test_build_obj; -#if BA_TEST_BUILD - is_test_build_obj = Py_True; -#else - is_test_build_obj = Py_False; -#endif - bool demo_mode{g_buildconfig.demo_build()}; - - const char* ui_scale; - switch (g_ui->scale()) { - case UIScale::kLarge: - ui_scale = "large"; - break; - case UIScale::kMedium: - ui_scale = "medium"; - break; - case UIScale::kSmall: - ui_scale = "small"; - break; - default: - throw Exception(); - } - // clang-format off - env_obj = Py_BuildValue( - "{" - "si" // build_number - "ss" // config_file_path - "ss" // locale - "ss" // user_agent_string - "ss" // version - "sO" // debug_build - "sO" // test_build - "ss" // python_directory_user - "ss" // python_directory_app - "ss" // platform - "ss" // subplatform - "ss" // ui_scale - "sO" // on_tv - "sO" // vr_mode - "sO" // toolbar_test - "sO" // demo_mode - "sO" // arcade_mode - "sO" // iircade_mode - "si" // protocol_version - "sO" // headless_mode - "ss" // python_directory_app_site - "ss" // device_name - "}", - "build_number", kAppBuildNumber, - "config_file_path", config_path.c_str(), - "locale", g_platform->GetLocale().c_str(), - "user_agent_string", g_app->user_agent_string.c_str(), - "version", kAppVersion, - "debug_build", is_debug_build_obj, - "test_build", is_test_build_obj, - "python_directory_user", g_platform->GetUserPythonDirectory().c_str(), - "python_directory_app", g_platform->GetAppPythonDirectory().c_str(), - "platform", g_platform->GetPlatformName().c_str(), - "subplatform", g_platform->GetSubplatformName().c_str(), - "ui_scale", ui_scale, - "on_tv", g_platform->IsRunningOnTV() ? Py_True : Py_False, - "vr_mode", IsVRMode() ? Py_True : Py_False, - "toolbar_test", BA_TOOLBAR_TEST ? Py_True : Py_False, - "demo_mode", demo_mode ? Py_True : Py_False, - "arcade_mode", g_buildconfig.arcade_build() ? Py_True : Py_False, - "iircade_mode", g_buildconfig.iircade_build() ? Py_True: Py_False, - "protocol_version", kProtocolVersion, - "headless_mode", HeadlessMode() ? Py_True : Py_False, - "python_directory_app_site", - g_platform->GetSitePythonDirectory().c_str(), - "device_name", - g_platform->GetDeviceName().c_str()); - // clang-format on - } - Py_INCREF(env_obj); - g_python->set_env_obj(env_obj); - return env_obj; - BA_PYTHON_CATCH; -} - -auto PySetStressTesting(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - int testing; - int player_count; - if (!PyArg_ParseTuple(args, "pi", &testing, &player_count)) { - return nullptr; - } - g_app_flavor->PushSetStressTestingCall(static_cast(testing), - player_count); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyDisplayLog(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {"name", "level", "message", nullptr}; - const char* name; - const char* levelstr; - const char* message; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "sss", - const_cast(kwlist), &name, &levelstr, - &message)) { - return nullptr; - } - - // Calc LogLevel enum val from their string val. - LogLevel level; - if (levelstr == std::string("DEBUG")) { - level = LogLevel::kDebug; - } else if (levelstr == std::string("INFO")) { - level = LogLevel::kInfo; - } else if (levelstr == std::string("WARNING")) { - level = LogLevel::kWarning; - } else if (levelstr == std::string("ERROR")) { - level = LogLevel::kError; - } else if (levelstr == std::string("CRITICAL")) { - level = LogLevel::kCritical; - } else { - // Assume we should avoid Log() calls here since it could infinite loop. - fprintf(stderr, "Invalid log level to display_log(): %s\n", levelstr); - level = LogLevel::kInfo; - } - Logging::DisplayLog(name, level, message); - - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyV1CloudLog(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* message; - static const char* kwlist[] = {"message", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &message)) { - return nullptr; - } - Logging::V1CloudLog(message); - - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyTimeFormatCheck(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {"time_format", "length", nullptr}; - PyObject* time_format_obj; - PyObject* length_obj; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "OO", - const_cast(kwlist), &time_format_obj, - &length_obj)) { - return nullptr; - } - auto time_format = Python::GetPyEnum_TimeFormat(time_format_obj); - - g_python->TimeFormatCheck(time_format, length_obj); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PythonMethodsApp::GetMethods() -> std::vector { - return { - {"appname", (PyCFunction)PyAppName, METH_NOARGS, - "appname() -> str\n" - "\n" - "(internal)\n"}, - {"appnameupper", (PyCFunction)PyAppNameUpper, METH_NOARGS, - "appnameupper() -> str\n" - "\n" - "(internal)\n" - "\n" - "Return whether this build of the game can display full unicode such " - "as\n" - "Emoji, Asian languages, etc.\n"}, - {"is_xcode_build", (PyCFunction)PyIsXCodeBuild, METH_NOARGS, - "is_xcode_build() -> bool\n" - "\n" - "(internal)\n"}, - {"can_display_full_unicode", (PyCFunction)PyCanDisplayFullUnicode, - METH_NOARGS, - "can_display_full_unicode() -> bool\n" - "\n" - "(internal)\n"}, - {"time_format_check", (PyCFunction)PyTimeFormatCheck, - METH_VARARGS | METH_KEYWORDS, - "time_format_check(time_format: ba.TimeFormat, length: float | int)\n" - " -> None\n" - "\n" - "(internal)\n" - "\n" - "Logs suspicious time values for timers or animate calls.\n" - "\n" - "(for helping with the transition from milliseconds-based time calls\n" - "to seconds-based ones)"}, - - {"display_log", (PyCFunction)PyDisplayLog, METH_VARARGS | METH_KEYWORDS, - "display_log(name: str, level: str, message: str) -> None\n" - "\n" - "(internal)\n" - "\n" - "Sends a log message to the in-game console and any per-platform\n" - "log destinations (Android log, etc.). This generally is not called\n" - "directly and should instead be fed Python logging output."}, - - {"v1_cloud_log", (PyCFunction)PyV1CloudLog, - METH_VARARGS | METH_KEYWORDS, - "v1_cloud_log(message: str) -> None\n" - "\n" - "(internal)\n" - "\n" - "Push messages to the old v1 cloud log."}, - - {"set_stress_testing", PySetStressTesting, METH_VARARGS, - "set_stress_testing(testing: bool, player_count: int) -> None\n" - "\n" - "(internal)"}, - - {"env", (PyCFunction)PyEnv, METH_NOARGS, - "env() -> dict\n" - "\n" - "(internal)\n" - "\n" - "Returns a dict containing general info about the operating " - "environment\n" - "such as version, platform, etc.\n" - "This info is now exposed through ba.App; refer to those docs for\n" - "info on specific elements."}, - - {"commit_config", (PyCFunction)PyCommitConfig, - METH_VARARGS | METH_KEYWORDS, - "commit_config(config: str) -> None\n" - "\n" - "(internal)"}, - - {"apply_config", PyApplyConfig, METH_VARARGS, - "apply_config() -> None\n" - "\n" - "(internal)"}, - -#if BA_DEBUG_BUILD - {"bless", (PyCFunction)PyBless, METH_VARARGS | METH_KEYWORDS, - "bless() -> None\n" - "\n" - "(internal)"}, -#endif - - {"quit", (PyCFunction)PyQuit, METH_VARARGS | METH_KEYWORDS, - "quit(soft: bool = False, back: bool = False) -> None\n" - "\n" - "Quit the game.\n" - "\n" - "Category: **General Utility Functions**\n" - "\n" - "On systems like android, 'soft' will end the activity but keep the\n" - "app running."}, - - {"screenmessage", (PyCFunction)PyScreenMessage, - METH_VARARGS | METH_KEYWORDS, - "screenmessage(message: str | ba.Lstr,\n" - " color: Sequence[float] | None = None,\n" - " top: bool = False,\n" - " image: dict[str, Any] | None = None,\n" - " log: bool = False,\n" - " clients: Sequence[int] | None = None,\n" - " transient: bool = False)" - " -> None\n" - "\n" - "Print a message to the local client's screen, in a given color.\n" - "\n" - "Category: **General Utility Functions**\n" - "\n" - "If 'top' is True, the message will go to the top message area.\n" - "For 'top' messages, 'image' must be a dict containing 'texture'\n" - "and 'tint_texture' textures and 'tint_color' and 'tint2_color'\n" - "colors. This defines an icon to display alongside the message.\n" - "If 'log' is True, the message will also be submitted to the log.\n" - "'clients' can be a list of client-ids the message should be sent\n" - "to, or None to specify that everyone should receive it.\n" - "If 'transient' is True, the message will not be included in the\n" - "game-stream and thus will not show up when viewing replays.\n" - "Currently the 'clients' option only works for transient messages."}, - - {"timer", (PyCFunction)PyTimer, METH_VARARGS | METH_KEYWORDS, - "timer(time: float, call: Callable[[], Any], repeat: bool = False,\n" - " timetype: ba.TimeType = TimeType.SIM,\n" - " timeformat: ba.TimeFormat = TimeFormat.SECONDS,\n" - " suppress_format_warning: bool = False)\n" - " -> None\n" - "\n" - "Schedule a call to run at a later point in time.\n" - "\n" - "Category: **General Utility Functions**\n" - "\n" - "This function adds a timer to the current ba.Context.\n" - "This timer cannot be canceled or modified once created. If you\n" - " require the ability to do so, use the ba.Timer class instead.\n" - "\n" - "##### Arguments\n" - "###### time (float)\n" - "> Length of time (in seconds by default) that the timer will wait\n" - "before firing. Note that the actual delay experienced may vary\n " - "depending on the timetype. (see below)\n" - "\n" - "###### call (Callable[[], Any])\n" - "> A callable Python object. Note that the timer will retain a\n" - "strong reference to the callable for as long as it exists, so you\n" - "may want to look into concepts such as ba.WeakCall if that is not\n" - "desired.\n" - "\n" - "###### repeat (bool)\n" - "> If True, the timer will fire repeatedly, with each successive\n" - "firing having the same delay as the first.\n" - "\n" - "###### timetype (ba.TimeType)\n" - "> Can be either `SIM`, `BASE`, or `REAL`. It defaults to\n" - "`SIM`.\n" - "\n" - "###### timeformat (ba.TimeFormat)\n" - "> Defaults to seconds but can also be milliseconds.\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" - "- 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" - "- 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" - "##### Examples\n" - "Print some stuff through time:\n" - ">>> ba.screenmessage('hello from now!')\n" - ">>> ba.timer(1.0, ba.Call(ba.screenmessage, 'hello from the " - "future!'))\n" - ">>> ba.timer(2.0, ba.Call(ba.screenmessage,\n" - "... 'hello from the future 2!'))\n"}, - - {"time", (PyCFunction)PyTime, METH_VARARGS | METH_KEYWORDS, - "time(timetype: ba.TimeType = TimeType.SIM,\n" - " timeformat: ba.TimeFormat = TimeFormat.SECONDS)\n" - " -> \n" - "\n" - "Return the current time.\n" - "\n" - "Category: **General Utility Functions**\n" - "\n" - "The time returned depends on the current ba.Context and timetype.\n" - "\n" - "timetype can be either SIM, BASE, or REAL. It defaults to\n" - "SIM. Types are explained below:\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" - "- 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" - "- 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 which returns float " - "seconds,\n" - "but it can also be MILLISECONDS to return integer milliseconds.\n" - "\n" - "Note: If you need pure unfiltered clock time, just use the standard\n" - "Python functions such as time.time()."}, - - {"pushcall", (PyCFunction)PyPushCall, METH_VARARGS | METH_KEYWORDS, - "pushcall(call: Callable, from_other_thread: bool = False,\n" - " suppress_other_thread_warning: bool = False,\n" - " other_thread_use_fg_context: bool = False,\n" - " raw: bool = False) -> None\n" - "\n" - "Push a call to the logic event-loop.\n" - "Category: **General Utility Functions**\n" - "\n" - "This call expects to be used in the logic thread, and will " - "automatically\n" - "save and restore the ba.Context to behave seamlessly.\n" - "\n" - "If you want to push a call from outside of the logic thread,\n" - "however, you can pass 'from_other_thread' as True. In this case\n" - "the call will always run in the UI context on the logic thread\n" - "or whichever context is in the foreground if\n" - "other_thread_use_fg_context is True.\n" - "Passing raw=True will disable thread checks and context" - " sets/restores."}, - {"getactivity", (PyCFunction)PyGetActivity, - METH_VARARGS | METH_KEYWORDS, - "getactivity(doraise: bool = True) -> \n" - "\n" - "Return the current ba.Activity instance.\n" - "\n" - "Category: **Gameplay Functions**\n" - "\n" - "Note that this is based on context; thus code run in a timer " - "generated\n" - "in Activity 'foo' will properly return 'foo' here, even if another\n" - "Activity has since been created or is transitioning in.\n" - "If there is no current Activity, raises a ba.ActivityNotFoundError.\n" - "If doraise is False, None will be returned instead in that case."}, - - {"newactivity", (PyCFunction)PyNewActivity, - METH_VARARGS | METH_KEYWORDS, - "newactivity(activity_type: type[ba.Activity],\n" - " settings: dict | None = None) -> ba.Activity\n" - "\n" - "Instantiates a ba.Activity given a type object.\n" - "\n" - "Category: **General Utility Functions**\n" - "\n" - "Activities require special setup and thus cannot be directly\n" - "instantiated; you must go through this function."}, - - {"get_foreground_host_session", (PyCFunction)PyGetForegroundHostSession, - METH_VARARGS | METH_KEYWORDS, - "get_foreground_host_session() -> ba.Session | None\n" - "\n" - "(internal)\n" - "\n" - "Return the ba.Session currently being displayed, or None if there " - "is\n" - "none."}, - - {"register_activity", (PyCFunction)PyRegisterActivity, - METH_VARARGS | METH_KEYWORDS, - "register_activity(activity: ba.Activity) -> ActivityData\n" - "\n" - "(internal)"}, - - {"register_session", (PyCFunction)PyRegisterSession, - METH_VARARGS | METH_KEYWORDS, - "register_session(session: ba.Session) -> SessionData\n" - "\n" - "(internal)"}, - - {"is_in_replay", (PyCFunction)PyIsInReplay, - METH_VARARGS | METH_KEYWORDS, - "is_in_replay() -> bool\n" - "\n" - "(internal)"}, - - {"app_instance_uuid", (PyCFunction)PyAppInstanceUUID, - METH_VARARGS | METH_KEYWORDS, - "app_instance_uuid() -> str\n" - "\n" - "(internal)"}, - - {"user_ran_commands", (PyCFunction)PyUserRanCommands, - METH_VARARGS | METH_KEYWORDS, - "user_ran_commands() -> None\n" - "\n" - "(internal)"}, - - {"new_replay_session", (PyCFunction)PyNewReplaySession, - METH_VARARGS | METH_KEYWORDS, - "new_replay_session(file_name: str) -> None\n" - "\n" - "(internal)"}, - - {"new_host_session", (PyCFunction)PyNewHostSession, - METH_VARARGS | METH_KEYWORDS, - "new_host_session(sessiontype: type[ba.Session],\n" - " benchmark_type: str | None = None) -> None\n" - "\n" - "(internal)"}, - - {"getsession", (PyCFunction)PyGetSession, METH_VARARGS | METH_KEYWORDS, - "getsession(doraise: bool = True) -> \n" - "\n" - "Category: **Gameplay Functions**\n" - "\n" - "Returns the current ba.Session instance.\n" - "Note that this is based on context; thus code being run in the UI\n" - "context will return the UI context here even if a game Session also\n" - "exists, etc. If there is no current Session, an Exception is raised, " - "or\n" - "if doraise is False then None is returned instead."}, - }; -} - -#pragma clang diagnostic pop - -} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_app.h b/src/ballistica/python/methods/python_methods_app.h deleted file mode 100644 index 5ed747b2..00000000 --- a/src/ballistica/python/methods/python_methods_app.h +++ /dev/null @@ -1,20 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_APP_H_ -#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_APP_H_ - -#include - -#include "ballistica/ballistica.h" - -namespace ballistica { - -/// App related individual python methods for our module. -class PythonMethodsApp { - public: - static auto GetMethods() -> std::vector; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_APP_H_ diff --git a/src/ballistica/python/methods/python_methods_assets.cc b/src/ballistica/python/methods/python_methods_assets.cc deleted file mode 100644 index 0eb9ba88..00000000 --- a/src/ballistica/python/methods/python_methods_assets.cc +++ /dev/null @@ -1,546 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/methods/python_methods_assets.h" - -#include -#if 0 // Cpplint errs w/o this, CLion errs with it. Hard to please everybody. -#include -#endif - -#include "ballistica/assets/component/collide_model.h" -#include "ballistica/assets/component/data.h" -#include "ballistica/assets/component/model.h" -#include "ballistica/assets/component/sound.h" -#include "ballistica/assets/component/texture.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/logic/host_activity.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_sys.h" -#include "ballistica/ui/ui.h" - -namespace ballistica { - -// Ignore signed bitwise stuff; python macros do it quite a bit. -#pragma clang diagnostic push -#pragma ide diagnostic ignored "hicpp-signed-bitwise" - -auto PyGetTexture(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* name; - static const char* kwlist[] = {"name", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &name)) { - return nullptr; - } - return Context::current_target().GetTexture(name)->NewPyRef(); - BA_PYTHON_CATCH; -} - -auto PyGetPackageTexture(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* name; - PyObject* package_obj; - static const char* kwlist[] = {"package", "name", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os", - const_cast(kwlist), &package_obj, - &name)) { - return nullptr; - } - auto fullname = g_python->ValidatedPackageAssetName(package_obj, name); - return Context::current_target().GetTexture(fullname)->NewPyRef(); - BA_PYTHON_CATCH; -} - -auto PyGetSound(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - const char* name; - static const char* kwlist[] = {"name", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &name)) { - return nullptr; - } - return Context::current_target().GetSound(name)->NewPyRef(); - BA_PYTHON_CATCH; -} - -auto PyGetPackageSound(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* name; - PyObject* package_obj; - static const char* kwlist[] = {"package", "name", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os", - const_cast(kwlist), &package_obj, - &name)) { - return nullptr; - } - auto fullname = g_python->ValidatedPackageAssetName(package_obj, name); - return Context::current_target().GetSound(fullname)->NewPyRef(); - BA_PYTHON_CATCH; -} - -auto PyGetData(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - const char* name; - static const char* kwlist[] = {"name", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &name)) { - return nullptr; - } - return Context::current_target().GetData(name)->NewPyRef(); - BA_PYTHON_CATCH; -} - -auto PyGetPackageData(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* name; - PyObject* package_obj; - static const char* kwlist[] = {"package", "name", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os", - const_cast(kwlist), &package_obj, - &name)) { - return nullptr; - } - auto fullname = g_python->ValidatedPackageAssetName(package_obj, name); - return Context::current_target().GetData(fullname)->NewPyRef(); - BA_PYTHON_CATCH; -} - -auto PyGetModel(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - const char* name; - static const char* kwlist[] = {"name", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &name)) { - return nullptr; - } - return Context::current_target().GetModel(name)->NewPyRef(); - BA_PYTHON_CATCH; -} - -auto PyGetPackageModel(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* name; - PyObject* package_obj; - static const char* kwlist[] = {"package", "name", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os", - const_cast(kwlist), &package_obj, - &name)) { - return nullptr; - } - auto fullname = g_python->ValidatedPackageAssetName(package_obj, name); - return Context::current_target().GetTexture(fullname)->NewPyRef(); - BA_PYTHON_CATCH; -} - -auto PyGetCollideModel(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* name; - static const char* kwlist[] = {"name", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &name)) { - return nullptr; - } - return Context::current_target().GetCollideModel(name)->NewPyRef(); - BA_PYTHON_CATCH; -} - -auto PyGetPackageCollideModel(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* name; - PyObject* package_obj; - static const char* kwlist[] = {"package", "name", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os", - const_cast(kwlist), &package_obj, - &name)) { - return nullptr; - } - auto fullname = g_python->ValidatedPackageAssetName(package_obj, name); - return Context::current_target().GetCollideModel(fullname)->NewPyRef(); - BA_PYTHON_CATCH; -} - -auto PyMusicPlayerStop(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - g_platform->MusicPlayerStop(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyMusicPlayerPlay(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - PyObject* files_obj; - static const char* kwlist[] = {"files", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", - const_cast(kwlist), &files_obj)) { - return nullptr; - } - g_platform->MusicPlayerPlay(files_obj); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyMusicPlayerSetVolume(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - float volume; - static const char* kwlist[] = {"volume", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "f", - const_cast(kwlist), &volume)) { - return nullptr; - } - g_platform->MusicPlayerSetVolume(volume); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyMusicPlayerShutdown(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - g_platform->MusicPlayerShutdown(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyReloadMedia(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - assert(g_graphics_server); - g_graphics_server->PushReloadMediaCall(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetQRCodeTexture(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* url; - static const char* kwlist[] = {"url", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &url)) { - return nullptr; - } - // FIXME - should add this to context; for now just hard-coded for UI though - if (Context::current().GetUIContext() != nullptr) { - // these textures aren't actually stored in the UI context; - // we just make sure we're here so we're not corrupting a game/session. - return Object::New(url)->NewPyRef(); - } else { - throw Exception("QR-Code textures can only be created in the UI context.", - PyExcType::kContext); - } - BA_PYTHON_CATCH; -} - -auto PyMacMusicAppInit(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - g_platform->MacMusicAppInit(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyMacMusicAppGetVolume(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - return PyLong_FromLong(g_platform->MacMusicAppGetVolume()); - BA_PYTHON_CATCH; -} - -auto PyMacMusicAppSetVolume(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - int volume; - static const char* kwlist[] = {"volume", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "i", - const_cast(kwlist), &volume)) { - return nullptr; - } - g_platform->MacMusicAppSetVolume(volume); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyMacMusicAppGetLibrarySource(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - g_platform->MacMusicAppGetLibrarySource(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyMacMusicAppStop(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - g_platform->MacMusicAppStop(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyMacMusicAppPlayPlaylist(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - std::string playlist; - PyObject* playlist_obj; - static const char* kwlist[] = {"playlist", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", - const_cast(kwlist), &playlist_obj)) { - return nullptr; - } - playlist = Python::GetPyString(playlist_obj); - if (g_platform->MacMusicAppPlayPlaylist(playlist)) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } - BA_PYTHON_CATCH; -} - -auto PyMacMusicAppGetPlaylists(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - PyObject* py_list = PyList_New(0); - std::list playlists = g_platform->MacMusicAppGetPlaylists(); - for (auto&& i : playlists) { - PyObject* str_obj = PyUnicode_FromString(i.c_str()); - PyList_Append(py_list, str_obj); - Py_DECREF(str_obj); - } - return py_list; - BA_PYTHON_CATCH; -} - -auto PyIsOSPlayingMusic(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - if (g_platform->IsOSPlayingMusic()) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } - BA_PYTHON_CATCH; -} - -auto PythonMethodsMedia::GetMethods() -> std::vector { - return { - {"is_os_playing_music", (PyCFunction)PyIsOSPlayingMusic, - METH_VARARGS | METH_KEYWORDS, - "is_os_playing_music() -> bool\n" - "\n" - "(internal)\n" - "\n" - "Tells whether the OS is currently playing music of some sort.\n" - "\n" - "(Used to determine whether the game should avoid playing its own)"}, - - {"mac_music_app_init", (PyCFunction)PyMacMusicAppInit, - METH_VARARGS | METH_KEYWORDS, - "mac_music_app_init() -> None\n" - "\n" - "(internal)"}, - - {"mac_music_app_get_volume", (PyCFunction)PyMacMusicAppGetVolume, - METH_VARARGS | METH_KEYWORDS, - "mac_music_app_get_volume() -> int\n" - "\n" - "(internal)"}, - - {"mac_music_app_set_volume", (PyCFunction)PyMacMusicAppSetVolume, - METH_VARARGS | METH_KEYWORDS, - "mac_music_app_set_volume(volume: int) -> None\n" - "\n" - "(internal)"}, - - {"mac_music_app_get_library_source", - (PyCFunction)PyMacMusicAppGetLibrarySource, METH_VARARGS | METH_KEYWORDS, - "mac_music_app_get_library_source() -> None\n" - "\n" - "(internal)"}, - - {"mac_music_app_stop", (PyCFunction)PyMacMusicAppStop, - METH_VARARGS | METH_KEYWORDS, - "mac_music_app_stop() -> None\n" - "\n" - "(internal)"}, - - {"mac_music_app_play_playlist", (PyCFunction)PyMacMusicAppPlayPlaylist, - METH_VARARGS | METH_KEYWORDS, - "mac_music_app_play_playlist(playlist: str) -> bool\n" - "\n" - "(internal)"}, - - {"mac_music_app_get_playlists", (PyCFunction)PyMacMusicAppGetPlaylists, - METH_VARARGS | METH_KEYWORDS, - "mac_music_app_get_playlists() -> list[str]\n" - "\n" - "(internal)"}, - - {"get_qrcode_texture", (PyCFunction)PyGetQRCodeTexture, - METH_VARARGS | METH_KEYWORDS, - "get_qrcode_texture(url: str) -> ba.Texture\n" - "\n" - "(internal)"}, - - {"reload_media", PyReloadMedia, METH_VARARGS, - "reload_media() -> None\n" - "\n" - "(internal)\n" - "\n" - "Reload all currently loaded game media; useful for\n" - "development/debugging."}, - - {"music_player_shutdown", (PyCFunction)PyMusicPlayerShutdown, - METH_VARARGS | METH_KEYWORDS, - "music_player_shutdown() -> None\n" - "\n" - "(internal)\n" - "\n" - "Finalizes internal music file playback (for internal use)"}, - - {"music_player_set_volume", (PyCFunction)PyMusicPlayerSetVolume, - METH_VARARGS | METH_KEYWORDS, - "music_player_set_volume(volume: float) -> None\n" - "\n" - "(internal)\n" - "\n" - "Sets internal music player volume (for internal use)"}, - - {"music_player_play", (PyCFunction)PyMusicPlayerPlay, - METH_VARARGS | METH_KEYWORDS, - "music_player_play(files: Any) -> None\n" - "\n" - "(internal)\n" - "\n" - "Starts internal music file playback (for internal use)"}, - - {"music_player_stop", (PyCFunction)PyMusicPlayerStop, - METH_VARARGS | METH_KEYWORDS, - "music_player_stop() -> None\n" - "\n" - "(internal)\n" - "\n" - "Stops internal music file playback (for internal use)"}, - - {"getcollidemodel", (PyCFunction)PyGetCollideModel, - METH_VARARGS | METH_KEYWORDS, - "getcollidemodel(name: str) -> ba.CollideModel\n" - "\n" - "Return a collide-model, loading it if necessary.\n" - "\n" - "Category: **Asset Functions**\n" - "\n" - "Collide-models are used in physics calculations for such things as\n" - "terrain.\n" - "\n" - "Note that this function returns immediately even if the media has yet\n" - "to be loaded. To avoid hitches, instantiate your media objects in\n" - "advance of when you will be using them, allowing time for them to " - "load\n" - "in the background if necessary."}, - - {"get_package_collide_model", (PyCFunction)PyGetPackageCollideModel, - METH_VARARGS | METH_KEYWORDS, - "get_package_collide_model(package: ba.AssetPackage, name: str)\n" - "-> ba.CollideModel\n" - "\n" - "(internal)\n"}, - - {"getmodel", (PyCFunction)PyGetModel, METH_VARARGS | METH_KEYWORDS, - "getmodel(name: str) -> ba.Model\n" - "\n" - "Return a model, loading it if necessary.\n" - "\n" - "Category: **Asset Functions**\n" - "\n" - "Note that this function returns immediately even if the media has yet\n" - "to be loaded. To avoid hitches, instantiate your media objects in\n" - "advance of when you will be using them, allowing time for them to " - "load\n" - "in the background if necessary."}, - - {"get_package_model", (PyCFunction)PyGetPackageModel, - METH_VARARGS | METH_KEYWORDS, - "get_package_model(package: ba.AssetPackage, name: str) -> ba.Model\n" - "\n" - "(internal)\n"}, - - {"getsound", (PyCFunction)PyGetSound, METH_VARARGS | METH_KEYWORDS, - "getsound(name: str) -> ba.Sound\n" - "\n" - "Return a sound, loading it if necessary.\n" - "\n" - "Category: **Asset Functions**\n" - "\n" - "Note that this function returns immediately even if the media has yet\n" - "to be loaded. To avoid hitches, instantiate your media objects in\n" - "advance of when you will be using them, allowing time for them to " - "load\n" - "in the background if necessary."}, - - {"get_package_sound", (PyCFunction)PyGetPackageSound, - METH_VARARGS | METH_KEYWORDS, - "get_package_sound(package: ba.AssetPackage, name: str) -> ba.Sound\n" - "\n" - "(internal).\n"}, - - {"getdata", (PyCFunction)PyGetData, METH_VARARGS | METH_KEYWORDS, - "getdata(name: str) -> ba.Data\n" - "\n" - "Return a data, loading it if necessary.\n" - "\n" - "Category: **Asset Functions**\n" - "\n" - "Note that this function returns immediately even if the media has yet\n" - "to be loaded. To avoid hitches, instantiate your media objects in\n" - "advance of when you will be using them, allowing time for them to " - "load\n" - "in the background if necessary."}, - - {"get_package_data", (PyCFunction)PyGetPackageData, - METH_VARARGS | METH_KEYWORDS, - "get_package_data(package: ba.AssetPackage, name: str) -> ba.Data\n" - "\n" - "(internal).\n"}, - - {"gettexture", (PyCFunction)PyGetTexture, METH_VARARGS | METH_KEYWORDS, - "gettexture(name: str) -> ba.Texture\n" - "\n" - "Return a texture, loading it if necessary.\n" - "\n" - "Category: **Asset Functions**\n" - "\n" - "Note that this function returns immediately even if the media has yet\n" - "to be loaded. To avoid hitches, instantiate your media objects in\n" - "advance of when you will be using them, allowing time for them to " - "load\n" - "in the background if necessary."}, - - {"get_package_texture", (PyCFunction)PyGetPackageTexture, - METH_VARARGS | METH_KEYWORDS, - "get_package_texture(package: ba.AssetPackage, name: str) -> " - "ba.Texture\n" - "\n" - "(internal)"}, - }; -} - -#pragma clang diagnostic pop - -} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_assets.h b/src/ballistica/python/methods/python_methods_assets.h deleted file mode 100644 index eb91a052..00000000 --- a/src/ballistica/python/methods/python_methods_assets.h +++ /dev/null @@ -1,20 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_ASSETS_H_ -#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_ASSETS_H_ - -#include - -#include "ballistica/ballistica.h" - -namespace ballistica { - -/// Media related individual python methods for our module. -class PythonMethodsMedia { - public: - static auto GetMethods() -> std::vector; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_ASSETS_H_ diff --git a/src/ballistica/python/methods/python_methods_gameplay.cc b/src/ballistica/python/methods/python_methods_gameplay.cc deleted file mode 100644 index 73bf636b..00000000 --- a/src/ballistica/python/methods/python_methods_gameplay.cc +++ /dev/null @@ -1,739 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/methods/python_methods_gameplay.h" - -#include - -#include "ballistica/app/app_flavor.h" -#include "ballistica/assets/component/sound.h" -#include "ballistica/dynamics/bg/bg_dynamics.h" -#include "ballistica/dynamics/collision.h" -#include "ballistica/dynamics/dynamics.h" -#include "ballistica/dynamics/material/material_action.h" -#include "ballistica/generic/json.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/input/device/input_device.h" -#include "ballistica/internal/app_internal.h" -#include "ballistica/logic/connection/connection_set.h" -#include "ballistica/logic/connection/connection_to_client.h" -#include "ballistica/logic/host_activity.h" -#include "ballistica/platform/platform.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_context_call_runnable.h" -#include "ballistica/python/python_sys.h" -#include "ballistica/scene/node/node.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" -#include "ballistica/scene/scene_stream.h" - -namespace ballistica { - -// Ignore signed bitwise stuff; python macros do it quite a bit. -#pragma clang diagnostic push -#pragma ide diagnostic ignored "hicpp-signed-bitwise" - -auto PyNewNode(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - Node* n = g_python->DoNewNode(args, keywds); - if (!n) { - return nullptr; - } - return n->NewPyRef(); - BA_PYTHON_CATCH; -} - -auto PyPrintNodes(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - HostActivity* host_activity = - g_logic->GetForegroundContext().GetHostActivity(); - if (!host_activity) { - throw Exception(PyExcType::kContext); - } - Scene* scene = host_activity->scene(); - std::string s; - int count = 1; - for (auto&& i : scene->nodes()) { - char buffer[128]; - snprintf(buffer, sizeof(buffer), "#%d: type: %-14s desc: %s", count, - i->type()->name().c_str(), i->label().c_str()); - s += buffer; - Log(LogLevel::kInfo, buffer); - count++; - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetNodes(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - HostActivity* host_activity = Context::current().GetHostActivity(); - if (!host_activity) { - throw Exception(PyExcType::kContext); - } - Scene* scene = host_activity->scene(); - PyObject* py_list = PyList_New(0); - for (auto&& i : scene->nodes()) { - PyList_Append(py_list, i->BorrowPyRef()); - } - return py_list; - BA_PYTHON_CATCH; -} - -static auto DoGetCollideValue(Dynamics* dynamics, const Collision* c, - const char* name) -> PyObject* { - BA_PYTHON_TRY; - if (!strcmp(name, "depth")) { - return Py_BuildValue("f", c->depth); - } else if (!strcmp(name, "position")) { - return Py_BuildValue("(fff)", c->x, c->y, c->z); - } else if (!strcmp(name, "sourcenode")) { - if (!dynamics->in_collide_message()) { - PyErr_SetString( - PyExc_AttributeError, - "collide value 'sourcenode' is only valid while processing " - "collide messages"); - return nullptr; - } - Node* n = dynamics->GetActiveCollideSrcNode(); - if (n) { - return n->NewPyRef(); - } else { - Py_RETURN_NONE; - } - } else if (!strcmp(name, "opposingnode")) { - if (!dynamics->in_collide_message()) { - PyErr_SetString( - PyExc_AttributeError, - "collide value 'opposingnode' is only valid while processing " - "collide messages"); - return nullptr; - } - Node* n = dynamics->GetActiveCollideDstNode(); - if (n) { - return n->NewPyRef(); - } else { - Py_RETURN_NONE; - } - } else if (!strcmp(name, "opposingbody")) { - return Py_BuildValue("i", dynamics->GetCollideMessageReverseOrder() - ? c->body_id_2 - : c->body_id_1); - } else { - PyErr_SetString( - PyExc_AttributeError, - (std::string("\"") + name + "\" is not a valid collide value name") - .c_str()); - return nullptr; - } - BA_PYTHON_CATCH; -} - -auto PyGetCollisionInfo(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - HostActivity* host_activity = Context::current().GetHostActivity(); - if (!host_activity) { - throw Exception(PyExcType::kContext); - } - Dynamics* dynamics = host_activity->scene()->dynamics(); - assert(dynamics); - PyObject* obj = nullptr; - - // Take arg list as individual items or possibly a single tuple - Py_ssize_t argc = PyTuple_GET_SIZE(args); - if (argc > 1) { - obj = args; - } else if (argc == 1) { - obj = PyTuple_GET_ITEM(args, 0); - } - Collision* c = dynamics->active_collision(); - if (!c) { - PyErr_SetString(PyExc_RuntimeError, - "This must be called from a collision callback."); - return nullptr; - } - if (PyUnicode_Check(obj)) { - return DoGetCollideValue(dynamics, c, PyUnicode_AsUTF8(obj)); - } else if (PyTuple_Check(obj)) { - Py_ssize_t size = PyTuple_GET_SIZE(obj); - - // NOTE: Need to make sure we never release the GIL or call out to - // code that could access gc stuff while building this. Ideally should - // create contents first and then create/fill the tuple as last step. - // See https://bugs.python.org/issue15108. - PyObject* return_tuple = PyTuple_New(size); - for (Py_ssize_t i = 0; i < size; i++) { - PyObject* o = PyTuple_GET_ITEM(obj, i); - if (PyUnicode_Check(o)) { - PyObject* val_obj = DoGetCollideValue(dynamics, c, PyUnicode_AsUTF8(o)); - if (val_obj) { - PyTuple_SET_ITEM(return_tuple, i, val_obj); - } else { - Py_DECREF(return_tuple); - return nullptr; - } - } else { - Py_DECREF(return_tuple); - PyErr_SetString(PyExc_TypeError, "Expected a string as tuple member."); - return nullptr; - } - } - return return_tuple; - } else { - PyErr_SetString(PyExc_TypeError, "Expected a string or tuple."); - return nullptr; - } - BA_PYTHON_CATCH; -} - -auto PyCameraShake(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - assert(InLogicThread()); - float intensity = 1.0f; - static const char* kwlist[] = {"intensity", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "|f", - const_cast(kwlist), &intensity)) { - return nullptr; - } - g_graphics->LocalCameraShake(intensity); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyPlaySound(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - - assert(InLogicThread()); - PyObject* sound_obj; - float volume = 1.0f; - int host_only = 0; - PyObject* pos_obj = Py_None; - static const char* kwlist[] = {"sound", "volume", "position", "host_only", - nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|fOp", - const_cast(kwlist), &sound_obj, - &volume, &pos_obj, &host_only)) { - return nullptr; - } - - Sound* sound = Python::GetPySound(sound_obj); - - // Can play sounds in a host scene context. - if (Scene* scene = Context::current().GetMutableScene()) { - if (sound->scene() != scene) { - throw Exception("Sound was not loaded in this context.", - PyExcType::kContext); - } - if (pos_obj != Py_None) { - std::vector vals = Python::GetPyFloats(pos_obj); - if (vals.size() != 3) { - throw Exception("Expected 3 floats for pos (got " - + std::to_string(vals.size()) + ")", - PyExcType::kValue); - } - scene->PlaySoundAtPosition(sound, volume, vals[0], vals[1], vals[2], - static_cast(host_only)); - } else { - scene->PlaySound(sound, volume, static_cast(host_only)); - } - } else { - throw Exception("Can't play sounds in this context.", PyExcType::kContext); - } - - Py_RETURN_NONE; - - BA_PYTHON_CATCH; -} - -auto PyEmitFx(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {"position", "velocity", "count", - "scale", "spread", "chunk_type", - "emit_type", "tendril_type", nullptr}; - PyObject* pos_obj = Py_None; - PyObject* vel_obj = Py_None; - int count = 10; - float scale = 1.0f; - float spread = 1.0f; - const char* chunk_type_str = "rock"; - const char* emit_type_str = "chunks"; - const char* tendril_type_str = "smoke"; - assert(InLogicThread()); - if (!PyArg_ParseTupleAndKeywords( - args, keywds, "O|Oiffsss", const_cast(kwlist), &pos_obj, - &vel_obj, &count, &scale, &spread, &chunk_type_str, &emit_type_str, - &tendril_type_str)) { - return nullptr; - } - float x, y, z; - assert(pos_obj); - { - std::vector vals = Python::GetPyFloats(pos_obj); - if (vals.size() != 3) { - throw Exception("Expected 3 floats for position.", PyExcType::kValue); - } - x = vals[0]; - y = vals[1]; - z = vals[2]; - } - float vx = 0.0f; - float vy = 0.0f; - float vz = 0.0f; - if (vel_obj != Py_None) { - std::vector vals = Python::GetPyFloats(vel_obj); - if (vals.size() != 3) { - throw Exception("Expected 3 floats for velocity.", PyExcType::kValue); - } - vx = vals[0]; - vy = vals[1]; - vz = vals[2]; - } - BGDynamicsChunkType chunk_type; - if (!strcmp(chunk_type_str, "rock")) { - chunk_type = BGDynamicsChunkType::kRock; - } else if (!strcmp(chunk_type_str, "ice")) { - chunk_type = BGDynamicsChunkType::kIce; - } else if (!strcmp(chunk_type_str, "slime")) { - chunk_type = BGDynamicsChunkType::kSlime; - } else if (!strcmp(chunk_type_str, "metal")) { - chunk_type = BGDynamicsChunkType::kMetal; - } else if (!strcmp(chunk_type_str, "spark")) { - chunk_type = BGDynamicsChunkType::kSpark; - } else if (!strcmp(chunk_type_str, "splinter")) { - chunk_type = BGDynamicsChunkType::kSplinter; - } else if (!strcmp(chunk_type_str, "sweat")) { - chunk_type = BGDynamicsChunkType::kSweat; - } else { - throw Exception( - "Invalid chunk type: '" + std::string(chunk_type_str) + "'.", - PyExcType::kValue); - } - BGDynamicsTendrilType tendril_type; - if (!strcmp(tendril_type_str, "smoke")) { - tendril_type = BGDynamicsTendrilType::kSmoke; - } else if (!strcmp(tendril_type_str, "thin_smoke")) { - tendril_type = BGDynamicsTendrilType::kThinSmoke; - } else if (!strcmp(tendril_type_str, "ice")) { - tendril_type = BGDynamicsTendrilType::kIce; - } else { - throw Exception( - "Invalid tendril type: '" + std::string(tendril_type_str) + "'.", - PyExcType::kValue); - } - BGDynamicsEmitType emit_type; - if (!strcmp(emit_type_str, "chunks")) { - emit_type = BGDynamicsEmitType::kChunks; - } else if (!strcmp(emit_type_str, "stickers")) { - emit_type = BGDynamicsEmitType::kStickers; - } else if (!strcmp(emit_type_str, "tendrils")) { - emit_type = BGDynamicsEmitType::kTendrils; - } else if (!strcmp(emit_type_str, "distortion")) { - emit_type = BGDynamicsEmitType::kDistortion; - } else if (!strcmp(emit_type_str, "flag_stand")) { - emit_type = BGDynamicsEmitType::kFlagStand; - } else if (!strcmp(emit_type_str, "fairydust")) { - emit_type = BGDynamicsEmitType::kFairyDust; - } else { - throw Exception("Invalid emit type: '" + std::string(emit_type_str) + "'.", - PyExcType::kValue); - } - if (Scene* scene = Context::current().GetMutableScene()) { - BGDynamicsEmission e; - e.emit_type = emit_type; - e.position = Vector3f(x, y, z); - e.velocity = Vector3f(vx, vy, vz); - e.count = count; - e.scale = scale; - e.spread = spread; - e.chunk_type = chunk_type; - e.tendril_type = tendril_type; - if (SceneStream* output_stream = scene->GetSceneStream()) { - output_stream->EmitBGDynamics(e); - } -#if !BA_HEADLESS_BUILD - g_bg_dynamics->Emit(e); -#endif // !BA_HEADLESS_BUILD - } else { - throw Exception("Can't emit bg dynamics in this context.", - PyExcType::kContext); - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetMapBounds(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - HostActivity* host_activity = Context::current().GetHostActivity(); - if (!host_activity) { - throw Exception(PyExcType::kContext); - } - float xmin, ymin, zmin, xmax, ymax, zmax; - assert(InLogicThread()); - if (!PyArg_ParseTuple(args, "(ffffff)", &xmin, &ymin, &zmin, &xmax, &ymax, - &zmax)) { - return nullptr; - } - host_activity->scene()->SetMapBounds(xmin, ymin, zmin, xmax, ymax, zmax); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetForegroundHostActivity(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - - // Note: we return None if not in the logic thread. - HostActivity* h = InLogicThread() - ? g_logic->GetForegroundContext().GetHostActivity() - : nullptr; - if (h != nullptr) { - PyObject* obj = h->GetPyActivity(); - Py_INCREF(obj); - return obj; - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetGameRoster(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - BA_PRECONDITION(InLogicThread()); - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - PythonRef py_client_list(PyList_New(0), PythonRef::kSteal); - cJSON* party = g_logic->game_roster(); - assert(party); - int len = cJSON_GetArraySize(party); - for (int i = 0; i < len; i++) { - cJSON* client = cJSON_GetArrayItem(party, i); - assert(client); - cJSON* spec = cJSON_GetObjectItem(client, "spec"); - cJSON* players = cJSON_GetObjectItem(client, "p"); - PythonRef py_player_list(PyList_New(0), PythonRef::kSteal); - if (players != nullptr) { - int plen = cJSON_GetArraySize(players); - for (int j = 0; j < plen; ++j) { - cJSON* player = cJSON_GetArrayItem(players, j); - if (player != nullptr) { - cJSON* name = cJSON_GetObjectItem(player, "n"); - cJSON* py_name_full = cJSON_GetObjectItem(player, "nf"); - cJSON* id_obj = cJSON_GetObjectItem(player, "i"); - int id_val = id_obj ? id_obj->valueint : -1; - if (name != nullptr && name->valuestring != nullptr - && py_name_full != nullptr && py_name_full->valuestring != nullptr - && id_val != -1) { - PythonRef py_player( - Py_BuildValue( - "{sssssi}", "name", - Utils::GetValidUTF8(name->valuestring, "ggr1").c_str(), - "name_full", - Utils::GetValidUTF8(py_name_full->valuestring, "ggr2") - .c_str(), - "id", id_val), - PythonRef::kSteal); - // This increments ref. - PyList_Append(py_player_list.get(), py_player.get()); - } - } - } - } - - // If there's a client_id with this data, include it; otherwise pass None. - cJSON* client_id = cJSON_GetObjectItem(client, "i"); - int clientid{}; - PythonRef client_id_ref; - if (client_id != nullptr) { - clientid = client_id->valueint; - client_id_ref.Steal(PyLong_FromLong(clientid)); - } else { - client_id_ref.Acquire(Py_None); - } - - // Let's also include a public account-id if we have one. - std::string account_id; - if (clientid == -1) { - account_id = g_app_internal->GetPublicV1AccountID(); - } else { - auto client2 = - g_logic->connections()->connections_to_clients().find(clientid); - if (client2 != g_logic->connections()->connections_to_clients().end()) { - account_id = client2->second->peer_public_account_id(); - } - } - PythonRef account_id_ref; - if (account_id.empty()) { - account_id_ref.Acquire(Py_None); - } else { - account_id_ref.Steal(PyUnicode_FromString(account_id.c_str())); - } - - // Py_BuildValue steals a ref; gotta increment ourself (edit: NO IT DOESNT) - // Py_INCREF(py_player_list.get()); - PythonRef py_client( - Py_BuildValue( - "{sssssOsOsO}", "display_string", - (spec && spec->valuestring) - ? PlayerSpec(spec->valuestring).GetDisplayString().c_str() - : "", - "spec_string", (spec && spec->valuestring) ? spec->valuestring : "", - "players", py_player_list.get(), "client_id", client_id_ref.get(), - "account_id", account_id_ref.get()), - PythonRef::kSteal); - PyList_Append(py_client_list.get(), - py_client.get()); // this increments ref - } - return py_client_list.NewRef(); - BA_PYTHON_CATCH; -} - -auto PySetDebugSpeedExponent(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - int speed; - if (!PyArg_ParseTuple(args, "i", &speed)) { - return nullptr; - } - HostActivity* host_activity = Context::current().GetHostActivity(); - if (!host_activity) { - throw Exception(PyExcType::kContext); - } -#if BA_DEBUG_BUILD - g_logic->SetDebugSpeedExponent(speed); -#else - throw Exception("This call only functions in the debug build."); -#endif - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetReplaySpeedExponent(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - assert(g_logic); - return PyLong_FromLong(g_logic->replay_speed_exponent()); - BA_PYTHON_CATCH; -} - -auto PySetReplaySpeedExponent(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - int speed; - if (!PyArg_ParseTuple(args, "i", &speed)) return nullptr; - assert(g_logic); - g_logic->SetReplaySpeedExponent(speed); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyResetGameActivityTracking(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - if (g_logic) { - g_logic->ResetActivityTracking(); - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyResetRandomPlayerNames(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - InputDevice::ResetRandomNames(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetRandomNames(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - PyObject* list = PyList_New(0); - const std::list& random_name_list = Utils::GetRandomNameList(); - for (const auto& i : random_name_list) { - assert(Utils::IsValidUTF8(i)); - PyObject* obj = PyUnicode_FromString(i.c_str()); - PyList_Append(list, obj); - Py_DECREF(obj); - } - return list; - BA_PYTHON_CATCH; -} - -auto PythonMethodsGameplay::GetMethods() -> std::vector { - return { - {"get_random_names", PyGetRandomNames, METH_VARARGS, - "get_random_names() -> list\n" - "\n" - "(internal)\n" - "\n" - "Returns the random names used by the game."}, - - {"reset_random_player_names", (PyCFunction)PyResetRandomPlayerNames, - METH_VARARGS | METH_KEYWORDS, - "reset_random_player_names() -> None\n" - "\n" - "(internal)"}, - - {"reset_game_activity_tracking", (PyCFunction)PyResetGameActivityTracking, - METH_VARARGS | METH_KEYWORDS, - "reset_game_activity_tracking() -> None\n" - "\n" - "(internal)"}, - - {"set_replay_speed_exponent", PySetReplaySpeedExponent, METH_VARARGS, - "set_replay_speed_exponent(speed: int) -> None\n" - "\n" - "(internal)\n" - "\n" - "Set replay speed. Actual displayed speed is pow(2, speed)."}, - - {"get_replay_speed_exponent", PyGetReplaySpeedExponent, METH_VARARGS, - "get_replay_speed_exponent() -> int\n" - "\n" - "(internal)\n" - "\n" - "Returns current replay speed value. Actual displayed speed is " - "pow(2,speed)."}, - - {"set_debug_speed_exponent", PySetDebugSpeedExponent, METH_VARARGS, - "set_debug_speed_exponent(speed: int) -> None\n" - "\n" - "(internal)\n" - "\n" - "Sets the debug speed scale for the game. Actual speed is " - "pow(2,speed)."}, - - {"get_game_roster", (PyCFunction)PyGetGameRoster, - METH_VARARGS | METH_KEYWORDS, - "get_game_roster() -> list[dict[str, Any]]\n" - "\n" - "(internal)"}, - - {"get_foreground_host_activity", (PyCFunction)PyGetForegroundHostActivity, - METH_VARARGS | METH_KEYWORDS, - "get_foreground_host_activity() -> ba.Activity | None\n" - "\n" - "(internal)\n" - "\n" - "Returns the ba.Activity currently in the foreground, or None if there\n" - "is none.\n"}, - - {"set_map_bounds", (PyCFunction)PySetMapBounds, - METH_VARARGS | METH_KEYWORDS, - "set_map_bounds(bounds: tuple[float, float, float, float, float, " - "float])\n" - " -> None\n" - "\n" - "(internal)\n" - "\n" - "Set map bounds. Generally nodes that go outside of this box are " - "killed."}, - - {"emitfx", (PyCFunction)PyEmitFx, METH_VARARGS | METH_KEYWORDS, - "emitfx(position: Sequence[float],\n" - " velocity: Sequence[float] | None = None,\n" - " count: int = 10, scale: float = 1.0, spread: float = 1.0,\n" - " chunk_type: str = 'rock', emit_type: str ='chunks',\n" - " tendril_type: str = 'smoke') -> None\n" - "\n" - "Emit particles, smoke, etc. into the fx sim layer.\n" - "\n" - "Category: **Gameplay Functions**\n" - "\n" - "The fx sim layer is a secondary dynamics simulation that runs in\n" - "the background and just looks pretty; it does not affect gameplay.\n" - "Note that the actual amount emitted may vary depending on graphics\n" - "settings, exiting element counts, or other factors."}, - - {"playsound", (PyCFunction)PyPlaySound, METH_VARARGS | METH_KEYWORDS, - "playsound(sound: Sound,\n" - " volume: float = 1.0,\n" - " position: Sequence[float] | None = None,\n" - " host_only: bool = False) -> None\n" - "\n" - "Play a ba.Sound a single time.\n" - "\n" - "Category: **Gameplay Functions**\n" - "\n" - "If position is not provided, the sound will be at a constant volume\n" - "everywhere. Position should be a float tuple of size 3."}, - - {"camerashake", (PyCFunction)PyCameraShake, METH_VARARGS | METH_KEYWORDS, - "camerashake(intensity: float = 1.0) -> None\n" - "\n" - "Shake the camera.\n" - "\n" - "Category: **Gameplay Functions**\n" - "\n" - "Note that some cameras and/or platforms (such as VR) may not display\n" - "camera-shake, so do not rely on this always being visible to the\n" - "player as a gameplay cue."}, - - {"get_collision_info", PyGetCollisionInfo, METH_VARARGS, - "get_collision_info(*args: Any) -> Any\n" - "\n" - "Return collision related values\n" - "\n" - "Category: **Gameplay Functions**\n" - "\n" - "Returns a single collision value or tuple of values such as location,\n" - "depth, nodes involved, etc. Only call this in the handler of a\n" - "collision-triggered callback or message"}, - - {"getnodes", PyGetNodes, METH_VARARGS, - "getnodes() -> list\n" - "\n" - "Return all nodes in the current ba.Context.\n" - "\n" - "Category: **Gameplay Functions**"}, - - {"printnodes", PyPrintNodes, METH_VARARGS, - "printnodes() -> None\n" - "\n" - "Print various info about existing nodes; useful for debugging.\n" - "\n" - "Category: **Gameplay Functions**"}, - - {"newnode", (PyCFunction)PyNewNode, METH_VARARGS | METH_KEYWORDS, - "newnode(type: str, owner: ba.Node | None = None,\n" - " attrs: dict | None = None,\n" - " name: str | None = None,\n" - " delegate: Any = None) -> Node\n" - "\n" - "Add a node of the given type to the game.\n" - "\n" - "Category: **Gameplay Functions**\n" - "\n" - "If a dict is provided for 'attributes', the node's initial attributes\n" - "will be set based on them.\n" - "\n" - "'name', if provided, will be stored with the node purely for " - "debugging\n" - "purposes. If no name is provided, an automatic one will be generated\n" - "such as 'terrain@foo.py:30'.\n" - "\n" - "If 'delegate' is provided, Python messages sent to the node will go " - "to\n" - "that object's handlemessage() method. Note that the delegate is " - "stored\n" - "as a weak-ref, so the node itself will not keep the object alive.\n" - "\n" - "if 'owner' is provided, the node will be automatically killed when " - "that\n" - "object dies. 'owner' can be another node or a ba.Actor"}, - }; -} - -#pragma clang diagnostic pop - -} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_gameplay.h b/src/ballistica/python/methods/python_methods_gameplay.h deleted file mode 100644 index e0773426..00000000 --- a/src/ballistica/python/methods/python_methods_gameplay.h +++ /dev/null @@ -1,20 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GAMEPLAY_H_ -#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GAMEPLAY_H_ - -#include - -#include "ballistica/ballistica.h" - -namespace ballistica { - -/// Gameplay related individual python methods for our module. -class PythonMethodsGameplay { - public: - static auto GetMethods() -> std::vector; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GAMEPLAY_H_ diff --git a/src/ballistica/python/methods/python_methods_graphics.cc b/src/ballistica/python/methods/python_methods_graphics.cc deleted file mode 100644 index 0e59a559..00000000 --- a/src/ballistica/python/methods/python_methods_graphics.cc +++ /dev/null @@ -1,433 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/methods/python_methods_graphics.h" - -#include "ballistica/graphics/camera.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/graphics/text/text_graphics.h" -#include "ballistica/logic/logic.h" -#include "ballistica/platform/platform.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_context_call_runnable.h" -#include "ballistica/python/python_sys.h" - -namespace ballistica { - -// Ignore signed bitwise stuff; python macros do it quite a bit. -#pragma clang diagnostic push -#pragma ide diagnostic ignored "hicpp-signed-bitwise" - -auto PyGetCameraPosition(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - float x = 0.0f; - float y = 0.0f; - float z = 0.0f; - Camera* cam = g_graphics->camera(); - cam->get_position(&x, &y, &z); - return Py_BuildValue("(fff)", x, y, z); - BA_PYTHON_CATCH; -} - -auto PyGetCameraTarget(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - float x = 0.0f; - float y = 0.0f; - float z = 0.0f; - Camera* cam = g_graphics->camera(); - cam->target_smoothed(&x, &y, &z); - return Py_BuildValue("(fff)", x, y, z); - BA_PYTHON_CATCH; -} - -auto PySetCameraPosition(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - float x = 0.0f; - float y = 0.0f; - float z = 0.0f; - static const char* kwlist[] = {"x", "y", "z", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "fff", - const_cast(kwlist), &x, &y, &z)) { - return nullptr; - } - assert(g_logic); - g_graphics->camera()->SetPosition(x, y, z); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetCameraTarget(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - float x = 0.0f; - float y = 0.0f; - float z = 0.0f; - static const char* kwlist[] = {"x", "y", "z", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "fff", - const_cast(kwlist), &x, &y, &z)) { - return nullptr; - } - assert(g_logic); - g_graphics->camera()->SetTarget(x, y, z); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetCameraManual(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - bool value = false; - static const char* kwlist[] = {"value", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "b", - const_cast(kwlist), &value)) { - return nullptr; - } - assert(g_logic); - g_graphics->camera()->SetManual(value); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyCharStr(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - PyObject* name_obj; - static const char* kwlist[] = {"name", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", - const_cast(kwlist), &name_obj)) { - return nullptr; - } - assert(g_logic); - auto id(Python::GetPyEnum_SpecialChar(name_obj)); - assert(Utils::IsValidUTF8(g_logic->CharStr(id))); - return PyUnicode_FromString(g_logic->CharStr(id).c_str()); - BA_PYTHON_CATCH; -} - -auto PySafeColor(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - PyObject* color_obj; - float red, green, blue; - float target_intensity = 0.6f; - static const char* kwlist[] = {"color", "target_intensity", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|f", - const_cast(kwlist), &color_obj, - &target_intensity)) { - return nullptr; - } - if (!PySequence_Check(color_obj)) { - throw Exception("Expected a sequence.", PyExcType::kType); - } - int len = static_cast(PySequence_Length(color_obj)); - if (len != 3 && len != 4) { - throw Exception("Expected a 3 or 4 length sequence; got " - + Python::ObjToString(color_obj) + ".", - PyExcType::kValue); - } - PythonRef red_obj(PySequence_GetItem(color_obj, 0), PythonRef::kSteal); - PythonRef green_obj(PySequence_GetItem(color_obj, 1), PythonRef::kSteal); - PythonRef blue_obj(PySequence_GetItem(color_obj, 2), PythonRef::kSteal); - red = Python::GetPyFloat(red_obj.get()); - green = Python::GetPyFloat(green_obj.get()); - blue = Python::GetPyFloat(blue_obj.get()); - Graphics::GetSafeColor(&red, &green, &blue, target_intensity); - if (len == 3) { - return Py_BuildValue("(fff)", red, green, blue); - } else { - PythonRef alpha_obj(PySequence_GetItem(color_obj, 3), PythonRef::kSteal); - float alpha = Python::GetPyFloat(alpha_obj.get()); - return Py_BuildValue("(ffff)", red, green, blue, alpha); - } - BA_PYTHON_CATCH; -} - -auto PyGetMaxGraphicsQuality(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - if (g_graphics && g_graphics->has_supports_high_quality_graphics_value() - && g_graphics->supports_high_quality_graphics()) { - return Py_BuildValue("s", "High"); - } else { - return Py_BuildValue("s", "Medium"); - } - BA_PYTHON_CATCH; -} - -auto PyEvaluateLstr(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* value; - static const char* kwlist[] = {"value", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &value)) { - return nullptr; - } - return PyUnicode_FromString( - g_logic->CompileResourceString(value, "evaluate_lstr").c_str()); - BA_PYTHON_CATCH; -} - -auto PyGetStringHeight(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - std::string s; - int suppress_warning = 0; - PyObject* s_obj; - static const char* kwlist[] = {"string", "suppress_warning", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|i", - const_cast(kwlist), &s_obj, - &suppress_warning)) { - return nullptr; - } - if (!suppress_warning) { - BA_LOG_PYTHON_TRACE( - "get_string_height() use is heavily discouraged as it reduces " - "language-independence; pass suppress_warning=True if you must use " - "it."); - } - s = Python::GetPyString(s_obj); -#if BA_DEBUG_BUILD - if (g_logic->CompileResourceString(s, "get_string_height test") != s) { - BA_LOG_PYTHON_TRACE( - "resource-string passed to get_string_height; this should be avoided"); - } -#endif - assert(g_graphics); - return Py_BuildValue("f", g_text_graphics->GetStringHeight(s)); - BA_PYTHON_CATCH; -} - -auto PyGetStringWidth(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - std::string s; - PyObject* s_obj; - int suppress_warning = 0; - static const char* kwlist[] = {"string", "suppress_warning", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|i", - const_cast(kwlist), &s_obj, - &suppress_warning)) { - return nullptr; - } - if (!suppress_warning) { - BA_LOG_PYTHON_TRACE( - "get_string_width() use is heavily discouraged as it reduces " - "language-independence; pass suppress_warning=True if you must use " - "it."); - } - s = Python::GetPyString(s_obj); -#if BA_DEBUG_BUILD - if (g_logic->CompileResourceString(s, "get_string_width debug test") != s) { - BA_LOG_PYTHON_TRACE( - "resource-string passed to get_string_width; this should be avoided"); - } -#endif - assert(g_graphics); - return Py_BuildValue("f", g_text_graphics->GetStringWidth(s)); - BA_PYTHON_CATCH; -} - -auto PyHaveChars(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - std::string text; - PyObject* text_obj; - static const char* kwlist[] = {"text", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", - const_cast(kwlist), &text_obj)) { - return nullptr; - } - text = Python::GetPyString(text_obj); - if (TextGraphics::HaveChars(text)) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } - BA_PYTHON_CATCH; -} - -auto PyAddCleanFrameCallback(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - PyObject* call_obj; - static const char* kwlist[] = {"call", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", - const_cast(kwlist), &call_obj)) { - return nullptr; - } - g_python->AddCleanFrameCommand(Object::New(call_obj)); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyHasGammaControl(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - // phasing this out; our old non-sdl2 mac has gamma controls but nothing newer - // does... -#if BA_OSTYPE_MACOS && !BA_SDL2_BUILD - Py_RETURN_TRUE; -#else - Py_RETURN_FALSE; -#endif - BA_PYTHON_CATCH; -} - -auto PyGetDisplayResolution(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - int x = 0; - int y = 0; - bool have_res = g_platform->GetDisplayResolution(&x, &y); - if (have_res) { - return Py_BuildValue("(ii)", x, y); - } else { - Py_RETURN_NONE; - } - BA_PYTHON_CATCH; -} - -auto PythonMethodsGraphics::GetMethods() -> std::vector { - return { - {"get_display_resolution", PyGetDisplayResolution, METH_VARARGS, - "get_display_resolution() -> tuple[int, int] | None\n" - "\n" - "(internal)\n" - "\n" - "Return the currently selected display resolution for fullscreen\n" - "display. Returns None if resolutions cannot be directly set."}, - - {"get_camera_position", (PyCFunction)PyGetCameraPosition, - METH_VARARGS | METH_KEYWORDS, - "get_camera_position() -> tuple[float, ...]\n" - "\n" - "(internal)\n" - "\n" - "WARNING: these camera controls will not apply to network clients\n" - "and may behave unpredictably in other ways. Use them only for\n" - "tinkering."}, - - {"get_camera_target", (PyCFunction)PyGetCameraTarget, - METH_VARARGS | METH_KEYWORDS, - "get_camera_target() -> tuple[float, ...]\n" - "\n" - "(internal)\n" - "\n" - "WARNING: these camera controls will not apply to network clients\n" - "and may behave unpredictably in other ways. Use them only for\n" - "tinkering."}, - - {"set_camera_position", (PyCFunction)PySetCameraPosition, - METH_VARARGS | METH_KEYWORDS, - "set_camera_position(x: float, y: float, z: float) -> None\n" - "\n" - "(internal)\n" - "\n" - "WARNING: these camera controls will not apply to network clients\n" - "and may behave unpredictably in other ways. Use them only for\n" - "tinkering."}, - - {"set_camera_target", (PyCFunction)PySetCameraTarget, - METH_VARARGS | METH_KEYWORDS, - "set_camera_target(x: float, y: float, z: float) -> None\n" - "\n" - "(internal)\n" - "\n" - "WARNING: these camera controls will not apply to network clients\n" - "and may behave unpredictably in other ways. Use them only for\n" - "tinkering."}, - - {"set_camera_manual", (PyCFunction)PySetCameraManual, - METH_VARARGS | METH_KEYWORDS, - "set_camera_manual(value: bool) -> None\n" - "\n" - "(internal)\n" - "\n" - "WARNING: these camera controls will not apply to network clients\n" - "and may behave unpredictably in other ways. Use them only for\n" - "tinkering."}, - - {"has_gamma_control", PyHasGammaControl, METH_VARARGS, - "has_gamma_control() -> bool\n" - "\n" - "(internal)\n" - "\n" - "Returns whether the system can adjust overall screen gamma)"}, - - {"add_clean_frame_callback", (PyCFunction)PyAddCleanFrameCallback, - METH_VARARGS | METH_KEYWORDS, - "add_clean_frame_callback(call: Callable) -> None\n" - "\n" - "(internal)\n" - "\n" - "Provide an object to be called once the next non-progress-bar-frame " - "has\n" - "been rendered. Useful for queueing things to load in the background\n" - "without elongating any current progress-bar-load."}, - - {"have_chars", (PyCFunction)PyHaveChars, METH_VARARGS | METH_KEYWORDS, - "have_chars(text: str) -> bool\n" - "\n" - "(internal)"}, - - {"get_string_width", (PyCFunction)PyGetStringWidth, - METH_VARARGS | METH_KEYWORDS, - "get_string_width(string: str, suppress_warning: bool = False) -> " - "float\n" - "\n" - "(internal)\n" - "\n" - "Given a string, returns its width using the standard small app\n" - "font."}, - - {"get_string_height", (PyCFunction)PyGetStringHeight, - METH_VARARGS | METH_KEYWORDS, - "get_string_height(string: str, suppress_warning: bool = False) -> " - "float\n" - "\n" - "(internal)\n" - "\n" - "Given a string, returns its height using the standard small app\n" - "font."}, - - {"evaluate_lstr", (PyCFunction)PyEvaluateLstr, - METH_VARARGS | METH_KEYWORDS, - "evaluate_lstr(value: str) -> str\n" - "\n" - "(internal)"}, - - {"get_max_graphics_quality", PyGetMaxGraphicsQuality, METH_VARARGS, - "get_max_graphics_quality() -> str\n" - "\n" - "(internal)\n" - "\n" - "Return the max graphics-quality supported on the current hardware."}, - - {"safecolor", (PyCFunction)PySafeColor, METH_VARARGS | METH_KEYWORDS, - "safecolor(color: Sequence[float], target_intensity: float = 0.6)\n" - " -> tuple[float, ...]\n" - "\n" - "Given a color tuple, return a color safe to display as text.\n" - "\n" - "Category: **General Utility Functions**\n" - "\n" - "Accepts tuples of length 3 or 4. This will slightly brighten very\n" - "dark colors, etc."}, - - {"charstr", (PyCFunction)PyCharStr, METH_VARARGS | METH_KEYWORDS, - "charstr(char_id: ba.SpecialChar) -> str\n" - "\n" - "Get a unicode string representing a special character.\n" - "\n" - "Category: **General Utility Functions**\n" - "\n" - "Note that these utilize the private-use block of unicode characters\n" - "(U+E000-U+F8FF) and are specific to the game; exporting or rendering\n" - "them elsewhere will be meaningless.\n" - "\n" - "See ba.SpecialChar for the list of available characters."}, - }; -} - -#pragma clang diagnostic pop - -} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_graphics.h b/src/ballistica/python/methods/python_methods_graphics.h deleted file mode 100644 index 1f3577ba..00000000 --- a/src/ballistica/python/methods/python_methods_graphics.h +++ /dev/null @@ -1,20 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GRAPHICS_H_ -#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GRAPHICS_H_ - -#include - -#include "ballistica/ballistica.h" - -namespace ballistica { - -/// Graphics related individual python methods for our module. -class PythonMethodsGraphics { - public: - static auto GetMethods() -> std::vector; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GRAPHICS_H_ diff --git a/src/ballistica/python/methods/python_methods_input.cc b/src/ballistica/python/methods/python_methods_input.cc deleted file mode 100644 index 8f1cde34..00000000 --- a/src/ballistica/python/methods/python_methods_input.cc +++ /dev/null @@ -1,306 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/methods/python_methods_input.h" - -#include "ballistica/app/app.h" -#include "ballistica/input/device/input_device.h" -#include "ballistica/input/device/touch_input.h" -#include "ballistica/input/input.h" -#include "ballistica/logic/logic.h" -#include "ballistica/platform/platform.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_sys.h" -#include "ballistica/ui/ui.h" - -namespace ballistica { - -// Ignore signed bitwise stuff; python macros do it quite a bit. -#pragma clang diagnostic push -#pragma ide diagnostic ignored "hicpp-signed-bitwise" - -auto PyGetConfigurableGamePads(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - std::vector gamepads = g_input->GetConfigurableGamePads(); - PyObject* list = PyList_New(0); - for (auto&& i : gamepads) { - PyObject* obj = i->NewPyRef(); - PyList_Append(list, obj); - Py_DECREF(obj); - } - return list; - BA_PYTHON_CATCH; -} - -auto PyHaveTouchScreenInput(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - if (g_app->touch_input) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } - BA_PYTHON_CATCH; -} - -auto PySetTouchscreenEditing(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - int editing; - if (!PyArg_ParseTuple(args, "p", &editing)) { - return nullptr; - } - if (g_app->touch_input) { - g_app->touch_input->set_editing(static_cast(editing)); - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyCaptureGamePadInput(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - assert(InLogicThread()); - assert(g_python); - PyObject* obj; - if (!PyArg_ParseTuple(args, "O", &obj)) { - return nullptr; - } - g_python->CaptureGamePadInput(obj); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyReleaseGamePadInput(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - assert(InLogicThread()); - assert(g_python); - g_python->ReleaseGamePadInput(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyCaptureKeyboardInput(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - assert(InLogicThread()); - if (!g_python) { - return nullptr; - } - PyObject* obj; - if (!PyArg_ParseTuple(args, "O", &obj)) { - return nullptr; - } - g_python->CaptureKeyboardInput(obj); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyReleaseKeyboardInput(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - assert(InLogicThread()); - if (!g_python) { - return nullptr; - } - g_python->ReleaseKeyboardInput(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyLockAllInput(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - assert(InLogicThread()); - assert(g_input); - g_input->LockAllInput(false, Python::GetPythonFileLocation()); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyUnlockAllInput(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - assert(InLogicThread()); - assert(g_input); - g_input->UnlockAllInput(false, Python::GetPythonFileLocation()); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetUIInputDevice(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - assert(InLogicThread()); - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - InputDevice* d = g_ui->GetUIInputDevice(); - if (d) { - return d->NewPyRef(); - } else { - Py_RETURN_NONE; - } - BA_PYTHON_CATCH; -} - -auto PySetUIInputDevice(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - assert(InLogicThread()); - static const char* kwlist[] = {"input", nullptr}; - PyObject* input_device_obj = Py_None; - if (!PyArg_ParseTupleAndKeywords( - args, keywds, "O", const_cast(kwlist), &input_device_obj)) { - return nullptr; - } - g_ui->SetUIInputDevice((input_device_obj == Py_None) - ? nullptr - : Python::GetPyInputDevice(input_device_obj)); - - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetInputDevice(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - assert(InLogicThread()); - const char* name; - const char* unique_id; - int doraise = true; - static const char* kwlist[] = {"name", "unique_id", "doraise", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "ss|i", - const_cast(kwlist), &name, - &unique_id, &doraise)) { - return nullptr; - } - InputDevice* d = g_input->GetInputDevice(name, unique_id); - if (d) { - return d->NewPyRef(); - } else { - if (doraise) { - throw Exception(std::string("Input device not found: '") + name + " " - + unique_id + "'.", - PyExcType::kInputDeviceNotFound); - } else { - Py_RETURN_NONE; - } - } - BA_PYTHON_CATCH; -} - -auto PyGetLocalActiveInputDevicesCount(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - BA_PRECONDITION(g_input); - return PyLong_FromLong(g_input->GetLocalActiveInputDeviceCount()); - BA_PYTHON_CATCH; -} - -auto PythonMethodsInput::GetMethods() -> std::vector { - return { - {"get_local_active_input_devices_count", - (PyCFunction)PyGetLocalActiveInputDevicesCount, - METH_VARARGS | METH_KEYWORDS, - "get_local_active_input_devices_count() -> int\n" - "\n" - "(internal)"}, - - {"getinputdevice", (PyCFunction)PyGetInputDevice, - METH_VARARGS | METH_KEYWORDS, - "getinputdevice(name: str, unique_id: str, doraise: bool = True)\n" - " -> \n" - "\n" - "(internal)\n" - "\n" - "Given a type name and a unique identifier, returns an InputDevice.\n" - "Throws an Exception if the input-device is not found, or returns None\n" - "if 'doraise' is False.\n"}, - - {"set_ui_input_device", (PyCFunction)PySetUIInputDevice, - METH_VARARGS | METH_KEYWORDS, - "set_ui_input_device(input_device: ba.InputDevice | None) -> None\n" - "\n" - "(internal)\n" - "\n" - "Sets the input-device that currently owns the user interface."}, - - {"get_ui_input_device", (PyCFunction)PyGetUIInputDevice, - METH_VARARGS | METH_KEYWORDS, - "get_ui_input_device() -> ba.InputDevice\n" - "\n" - "(internal)\n" - "\n" - "Returns the input-device that currently owns the user interface, or\n" - "None if there is none."}, - - {"unlock_all_input", PyUnlockAllInput, METH_VARARGS, - "unlock_all_input() -> None\n" - "\n" - "(internal)\n" - "\n" - "Resumes normal keyboard, mouse, and gamepad event processing."}, - - {"lock_all_input", PyLockAllInput, METH_VARARGS, - "lock_all_input() -> None\n" - "\n" - "(internal)\n" - "\n" - "Prevents all keyboard, mouse, and gamepad events from being " - "processed."}, - - {"release_keyboard_input", PyReleaseKeyboardInput, METH_VARARGS, - "release_keyboard_input() -> None\n" - "\n" - "(internal)\n" - "\n" - "Resumes normal keyboard event processing."}, - - {"capture_keyboard_input", PyCaptureKeyboardInput, METH_VARARGS, - "capture_keyboard_input(call: Callable[[dict], None]) -> None\n" - "\n" - "(internal)\n" - "\n" - "Add a callable to be called for subsequent keyboard-game-pad events.\n" - "The method is passed a dict containing info about the event."}, - - {"release_gamepad_input", PyReleaseGamePadInput, METH_VARARGS, - "release_gamepad_input() -> None\n" - "\n" - "(internal)\n" - "\n" - "Resumes normal gamepad event processing."}, - - {"capture_gamepad_input", PyCaptureGamePadInput, METH_VARARGS, - "capture_gamepad_input(call: Callable[[dict], None]) -> None\n" - "\n" - "(internal)\n" - "\n" - "Add a callable to be called for subsequent gamepad events.\n" - "The method is passed a dict containing info about the event."}, - - {"set_touchscreen_editing", PySetTouchscreenEditing, METH_VARARGS, - "set_touchscreen_editing(editing: bool) -> None\n" - "\n" - "(internal)"}, - - {"have_touchscreen_input", PyHaveTouchScreenInput, METH_VARARGS, - "have_touchscreen_input() -> bool\n" - "\n" - "(internal)\n" - "\n" - "Returns whether or not a touch-screen input is present"}, - - {"get_configurable_game_pads", PyGetConfigurableGamePads, METH_VARARGS, - "get_configurable_game_pads() -> list\n" - "\n" - "(internal)\n" - "\n" - "Returns a list of the currently connected gamepads that can be\n" - "configured."}, - }; -} - -#pragma clang diagnostic pop - -} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_input.h b/src/ballistica/python/methods/python_methods_input.h deleted file mode 100644 index 1825192f..00000000 --- a/src/ballistica/python/methods/python_methods_input.h +++ /dev/null @@ -1,20 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_INPUT_H_ -#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_INPUT_H_ - -#include - -#include "ballistica/ballistica.h" - -namespace ballistica { - -// Input related individual python methods for our module. -class PythonMethodsInput { - public: - static auto GetMethods() -> std::vector; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_INPUT_H_ diff --git a/src/ballistica/python/methods/python_methods_networking.cc b/src/ballistica/python/methods/python_methods_networking.cc deleted file mode 100644 index bd59b102..00000000 --- a/src/ballistica/python/methods/python_methods_networking.cc +++ /dev/null @@ -1,559 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/methods/python_methods_networking.h" - -#include "ballistica/app/app.h" -#include "ballistica/logic/connection/connection_set.h" -#include "ballistica/logic/connection/connection_to_client.h" -#include "ballistica/logic/connection/connection_to_host.h" -#include "ballistica/logic/logic.h" -#include "ballistica/math/vector3f.h" -#include "ballistica/networking/network_reader.h" -#include "ballistica/networking/networking.h" -#include "ballistica/networking/sockaddr.h" -#include "ballistica/networking/telnet_server.h" -#include "ballistica/platform/platform.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_sys.h" - -namespace ballistica { - -// Ignore signed bitwise stuff; python macros do it quite a bit. -#pragma clang diagnostic push -#pragma ide diagnostic ignored "hicpp-signed-bitwise" - -auto PyGetPublicPartyEnabled(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) - return nullptr; - assert(g_python); - if (g_logic->public_party_enabled()) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } - BA_PYTHON_CATCH; -} - -auto PySetPublicPartyEnabled(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - int enable; - static const char* kwlist[] = {"enabled", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", - const_cast(kwlist), &enable)) { - return nullptr; - } - assert(g_python); - g_logic->SetPublicPartyEnabled(static_cast(enable)); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetPublicPartyName(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - PyObject* name_obj; - static const char* kwlist[] = {"name", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", - const_cast(kwlist), &name_obj)) { - return nullptr; - } - std::string name = Python::GetPyString(name_obj); - assert(g_python); - g_logic->SetPublicPartyName(name); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetPublicPartyStatsURL(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - PyObject* url_obj; - static const char* kwlist[] = {"url", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", - const_cast(kwlist), &url_obj)) { - return nullptr; - } - // The call expects an empty string for the no-url option. - std::string url = (url_obj == Py_None) ? "" : Python::GetPyString(url_obj); - assert(g_python); - g_logic->SetPublicPartyStatsURL(url); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetPublicPartyMaxSize(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - assert(g_python); - return PyLong_FromLong(g_logic->public_party_max_size()); - BA_PYTHON_CATCH; -} - -auto PySetPublicPartyMaxSize(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - int max_size; - static const char* kwlist[] = {"max_size", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "i", - const_cast(kwlist), &max_size)) { - return nullptr; - } - assert(g_python); - g_logic->SetPublicPartyMaxSize(max_size); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetPublicPartyQueueEnabled(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - int enabled; - static const char* kwlist[] = {"enabled", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", - const_cast(kwlist), &enabled)) { - return nullptr; - } - assert(g_python); - g_logic->SetPublicPartyQueueEnabled(enabled); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetAuthenticateClients(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - int enable; - static const char* kwlist[] = {"enable", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", - const_cast(kwlist), &enable)) { - return nullptr; - } - assert(g_logic); - g_logic->set_require_client_authentication(static_cast(enable)); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetAdmins(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - PyObject* admins_obj; - static const char* kwlist[] = {"admins", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", - const_cast(kwlist), &admins_obj)) { - return nullptr; - } - assert(g_logic); - - auto admins = Python::GetPyStrings(admins_obj); - std::set adminset; - for (auto&& admin : admins) { - adminset.insert(admin); - } - g_logic->set_admin_public_ids(adminset); - - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetEnableDefaultKickVoting(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - int enable; - static const char* kwlist[] = {"enable", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", - const_cast(kwlist), &enable)) { - return nullptr; - } - assert(g_logic); - g_logic->set_kick_voting_enabled(static_cast(enable)); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyConnectToParty(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - std::string address; - PyObject* address_obj; - int port = kDefaultPort; - - // Whether we should print standard 'connecting...' and 'party full..' - // messages when false, only odd errors such as version incompatibility will - // be printed and most connection attempts will be silent todo: could - // generalize this to pass all results to a callback instead - int print_progress = 1; - static const char* kwlist[] = {"address", "port", "print_progress", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|ip", - const_cast(kwlist), &address_obj, - &port, &print_progress)) { - return nullptr; - } - address = Python::GetPyString(address_obj); - - // Disallow in headless build (people were using this for spam-bots). - - if (HeadlessMode()) { - throw Exception("Not available in headless mode."); - } - - SockAddr s; - try { - s = SockAddr(address, port); - - // HACK: CLion currently flags our catch clause as unreachable even - // though SockAddr constructor can throw exceptions. Work around that here. - if (explicit_bool(false)) { - throw Exception(); - } - } catch (const std::exception&) { - ScreenMessage(g_logic->GetResourceString("invalidAddressErrorText"), - {1, 0, 0}); - Py_RETURN_NONE; - } - g_logic->connections()->PushHostConnectedUDPCall( - s, static_cast(print_progress)); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyClientInfoQueryResponse(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* token; - PyObject* response_obj; - static const char* kwlist[] = {"token", "response", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "sO", - const_cast(kwlist), &token, - &response_obj)) { - return nullptr; - } - g_logic->connections()->SetClientInfoFromMasterServer(token, response_obj); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetConnectionToHostInfo(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - ConnectionToHost* hc = g_logic->connections()->connection_to_host(); - if (hc) { - return Py_BuildValue("{sssi}", "name", hc->party_name().c_str(), - "build_number", hc->build_number()); - } else { - return Py_BuildValue("{}"); - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyDisconnectFromHost(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - g_logic->connections()->PushDisconnectFromHostCall(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyDisconnectClient(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - int client_id; - int ban_time = 300; // Old default before we exposed this. - static const char* kwlist[] = {"client_id", "ban_time", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|i", - const_cast(kwlist), &client_id, - &ban_time)) { - return nullptr; - } - bool kickable = g_logic->connections()->DisconnectClient(client_id, ban_time); - if (kickable) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } - BA_PYTHON_CATCH; -} - -auto PyGetClientPublicDeviceUUID(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - int client_id; - static const char* kwlist[] = {"client_id", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "i", - const_cast(kwlist), &client_id)) { - return nullptr; - } - auto&& connection{ - g_logic->connections()->connections_to_clients().find(client_id)}; - - // Does this connection exist? - if (connection == g_logic->connections()->connections_to_clients().end()) { - Py_RETURN_NONE; - } - - // Connections should always be valid refs. - assert(connection->second.exists()); - - // Old clients don't assign this; it will be empty. - if (connection->second->public_device_id().empty()) { - Py_RETURN_NONE; - } - return PyUnicode_FromString(connection->second->public_device_id().c_str()); - BA_PYTHON_CATCH; -} - -auto PyGetGamePort(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - int port = 0; - if (g_network_reader != nullptr) { - // Hmmm; we're just fetching the ipv4 port here; 6 could be different. - port = g_network_reader->port4(); - } - return Py_BuildValue("i", port); - BA_PYTHON_CATCH; -} - -auto PySetMasterServerSource(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - int source; - if (!PyArg_ParseTuple(args, "i", &source)) return nullptr; - if (source != 0 && source != 1) { - BA_LOG_ONCE(LogLevel::kError, - "Invalid server source: " + std::to_string(source) + "."); - source = 1; - } - g_app->master_server_source = source; - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetTelnetAccessEnabled(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - assert(InLogicThread()); - int enable; - static const char* kwlist[] = {"enable", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", - const_cast(kwlist), &enable)) { - return nullptr; - } - if (g_app->telnet_server) { - g_app->telnet_server->SetAccessEnabled(static_cast(enable)); - } else { - throw Exception("Telnet server not enabled."); - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyHostScanCycle(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - g_networking->HostScanCycle(); - std::vector results = - g_networking->GetScanResults(); - PyObject* py_list = PyList_New(0); - for (auto&& i : results) { - PyList_Append(py_list, Py_BuildValue("{ssss}", "display_string", - i.display_string.c_str(), "address", - i.address.c_str())); - } - return py_list; - BA_PYTHON_CATCH; -} - -auto PyEndHostScanning(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - g_networking->EndHostScanning(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyHaveConnectedClients(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - if (g_logic->connections()->GetConnectedClientCount() > 0) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } - BA_PYTHON_CATCH; -} - -auto PythonMethodsNetworking::GetMethods() -> std::vector { - return { - {"have_connected_clients", (PyCFunction)PyHaveConnectedClients, - METH_VARARGS | METH_KEYWORDS, - "have_connected_clients() -> bool\n" - "\n" - "(internal)\n" - "\n" - "Category: General Utility Functions"}, - - {"end_host_scanning", (PyCFunction)PyEndHostScanning, - METH_VARARGS | METH_KEYWORDS, - "end_host_scanning() -> None\n" - "\n" - "(internal)\n" - "\n" - "Category: General Utility Functions"}, - - {"host_scan_cycle", (PyCFunction)PyHostScanCycle, - METH_VARARGS | METH_KEYWORDS, - "host_scan_cycle() -> list\n" - "\n" - "(internal)"}, - - {"set_telnet_access_enabled", (PyCFunction)PySetTelnetAccessEnabled, - METH_VARARGS | METH_KEYWORDS, - "set_telnet_access_enabled(enable: bool)\n" - " -> None\n" - "\n" - "(internal)"}, - - {"set_master_server_source", PySetMasterServerSource, METH_VARARGS, - "set_master_server_source(source: int) -> None\n" - "\n" - "(internal)"}, - - {"get_game_port", PyGetGamePort, METH_VARARGS, - "get_game_port() -> int\n" - "\n" - "(internal)\n" - "\n" - "Return the port ballistica is hosting on."}, - - {"disconnect_from_host", (PyCFunction)PyDisconnectFromHost, - METH_VARARGS | METH_KEYWORDS, - "disconnect_from_host() -> None\n" - "\n" - "(internal)\n" - "\n" - "Category: General Utility Functions"}, - - {"disconnect_client", (PyCFunction)PyDisconnectClient, - METH_VARARGS | METH_KEYWORDS, - "disconnect_client(client_id: int, ban_time: int = 300) -> bool\n" - "\n" - "(internal)"}, - - {"get_client_public_device_uuid", - (PyCFunction)PyGetClientPublicDeviceUUID, METH_VARARGS | METH_KEYWORDS, - "get_client_public_device_uuid(client_id: int) -> str | None\n" - "\n" - "(internal)\n" - "\n" - "Category: General Utility Functions\n" - "\n" - "Return a public device UUID for a client. If the client does not\n" - "exist or is running a version older than 1.6.10, returns None.\n" - "Public device UUID uniquely identifies the device the client is\n" - "using in a semi-permanent way. The UUID value will change\n" - "periodically with updates to the game or operating system."}, - - {"get_connection_to_host_info", (PyCFunction)PyGetConnectionToHostInfo, - METH_VARARGS | METH_KEYWORDS, - "get_connection_to_host_info() -> dict\n" - "\n" - "(internal)"}, - - {"client_info_query_response", (PyCFunction)PyClientInfoQueryResponse, - METH_VARARGS | METH_KEYWORDS, - "client_info_query_response(token: str, response: Any) -> None\n" - "\n" - "(internal)"}, - - {"connect_to_party", (PyCFunction)PyConnectToParty, - METH_VARARGS | METH_KEYWORDS, - "connect_to_party(address: str, port: int | None = None,\n" - " print_progress: bool = True) -> None\n" - "\n" - "(internal)"}, - - {"set_authenticate_clients", (PyCFunction)PySetAuthenticateClients, - METH_VARARGS | METH_KEYWORDS, - "set_authenticate_clients(enable: bool) -> None\n" - "\n" - "(internal)"}, - - {"set_admins", (PyCFunction)PySetAdmins, METH_VARARGS | METH_KEYWORDS, - "set_admins(admins: list[str]) -> None\n" - "\n" - "(internal)"}, - - {"set_enable_default_kick_voting", - (PyCFunction)PySetEnableDefaultKickVoting, METH_VARARGS | METH_KEYWORDS, - "set_enable_default_kick_voting(enable: bool) -> None\n" - "\n" - "(internal)"}, - - {"set_public_party_max_size", (PyCFunction)PySetPublicPartyMaxSize, - METH_VARARGS | METH_KEYWORDS, - "set_public_party_max_size(max_size: int) -> None\n" - "\n" - - "(internal)"}, - - {"set_public_party_queue_enabled", - (PyCFunction)PySetPublicPartyQueueEnabled, METH_VARARGS | METH_KEYWORDS, - "set_public_party_queue_enabled(max_size: bool) -> None\n" - "\n" - "(internal)"}, - - {"get_public_party_max_size", (PyCFunction)PyGetPublicPartyMaxSize, - METH_VARARGS | METH_KEYWORDS, - "get_public_party_max_size() -> int\n" - "\n" - "(internal)"}, - - {"set_public_party_stats_url", (PyCFunction)PySetPublicPartyStatsURL, - METH_VARARGS | METH_KEYWORDS, - "set_public_party_stats_url(url: str | None) -> None\n" - "\n" - "(internal)"}, - - {"set_public_party_name", (PyCFunction)PySetPublicPartyName, - METH_VARARGS | METH_KEYWORDS, - "set_public_party_name(name: str) -> None\n" - "\n" - "(internal)"}, - - {"set_public_party_enabled", (PyCFunction)PySetPublicPartyEnabled, - METH_VARARGS | METH_KEYWORDS, - "set_public_party_enabled(enabled: bool) -> None\n" - "\n" - "(internal)"}, - - {"get_public_party_enabled", (PyCFunction)PyGetPublicPartyEnabled, - METH_VARARGS | METH_KEYWORDS, - "get_public_party_enabled() -> bool\n" - "\n" - "(internal)"}, - }; -} - -#pragma clang diagnostic pop - -} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_networking.h b/src/ballistica/python/methods/python_methods_networking.h deleted file mode 100644 index bf86808f..00000000 --- a/src/ballistica/python/methods/python_methods_networking.h +++ /dev/null @@ -1,20 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_NETWORKING_H_ -#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_NETWORKING_H_ - -#include - -#include "ballistica/ballistica.h" - -namespace ballistica { - -/// Networking related individual python methods for our module. -class PythonMethodsNetworking { - public: - static auto GetMethods() -> std::vector; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_NETWORKING_H_ diff --git a/src/ballistica/python/methods/python_methods_system.cc b/src/ballistica/python/methods/python_methods_system.cc deleted file mode 100644 index 1d9f879b..00000000 --- a/src/ballistica/python/methods/python_methods_system.cc +++ /dev/null @@ -1,1126 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/methods/python_methods_system.h" - -#include -#include - -#include "ballistica/app/app.h" -#include "ballistica/app/app_config.h" -#include "ballistica/app/app_flavor.h" -#include "ballistica/assets/assets.h" -#include "ballistica/assets/component/texture.h" -#include "ballistica/graphics/camera.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/input/input.h" -#include "ballistica/logic/host_activity.h" -#include "ballistica/logic/session/host_session.h" -#include "ballistica/logic/session/replay_client_session.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_context_call_runnable.h" -#include "ballistica/python/python_sys.h" -#include "ballistica/scene/scene.h" -#include "ballistica/scene/scene_stream.h" - -namespace ballistica { - -// Ignore signed bitwise warnings; python macros do it quite a bit. -#pragma clang diagnostic push -#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; - 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; - Py_RETURN_FALSE; - BA_PYTHON_CATCH; -} - -auto PySetUpSigInt(PyObject* self) -> PyObject* { - BA_PYTHON_TRY; - if (g_app_flavor) { - g_platform->SetupInterruptHandling(); - } else { - Log(LogLevel::kError, "SigInt handler called before g_app_flavor exists."); - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyIsRunningOnFireTV(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - if (g_platform->IsRunningOnFireTV()) { - Py_RETURN_TRUE; - } - Py_RETURN_FALSE; - BA_PYTHON_CATCH; -} - -auto PyHavePermission(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - BA_PRECONDITION(InLogicThread()); - Permission permission; - PyObject* permission_obj; - static const char* kwlist[] = {"permission", nullptr}; - if (!PyArg_ParseTupleAndKeywords( - args, keywds, "O", const_cast(kwlist), &permission_obj)) { - return nullptr; - } - - permission = Python::GetPyEnum_Permission(permission_obj); - - if (g_platform->HavePermission(permission)) { - Py_RETURN_TRUE; - } - Py_RETURN_FALSE; - BA_PYTHON_CATCH; -} - -auto PyRequestPermission(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - BA_PRECONDITION(InLogicThread()); - Permission permission; - PyObject* permission_obj; - static const char* kwlist[] = {"permission", nullptr}; - if (!PyArg_ParseTupleAndKeywords( - args, keywds, "O", const_cast(kwlist), &permission_obj)) { - return nullptr; - } - - permission = Python::GetPyEnum_Permission(permission_obj); - g_platform->RequestPermission(permission); - - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyInLogicThread(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - if (InLogicThread()) { - Py_RETURN_TRUE; - } - Py_RETURN_FALSE; - BA_PYTHON_CATCH; -} - -auto PySetThreadName(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* name; - static const char* kwlist[] = {"name", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &name)) { - return nullptr; - } - g_platform->SetCurrentThreadName(name); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetThreadName(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - return PyUnicode_FromString(GetCurrentThreadName().c_str()); - BA_PYTHON_CATCH; -} - -// returns an extra hash value that can be incorporated into security checks; -// this contains things like whether console commands have been run, etc. -auto PyExtraHashValue(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - const char* h = - ((g_app->user_ran_commands || g_app->workspaces_in_use) ? "cjief3l" - : "wofocj8"); - return PyUnicode_FromString(h); - BA_PYTHON_CATCH; -} - -auto PyGetIdleTime(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - return PyLong_FromLong(static_cast_check_fit( // NOLINT - g_input ? g_input->input_idle_time() : 0)); - BA_PYTHON_CATCH; -} - -auto PyHasUserRunCommands(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - if (g_app->user_ran_commands) { - Py_RETURN_TRUE; - } - Py_RETURN_FALSE; - BA_PYTHON_CATCH; -} - -auto PyWorkspacesInUse(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - if (g_app->workspaces_in_use) { - Py_RETURN_TRUE; - } - Py_RETURN_FALSE; - BA_PYTHON_CATCH; -} - -auto PyContainsPythonDist(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - if (g_platform->ContainsPythonDist()) { - Py_RETURN_TRUE; - } - Py_RETURN_FALSE; - BA_PYTHON_CATCH; -} - -auto PyValueTest(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* arg; - double change = 0.0f; - double absolute = 0.0f; - bool have_change = false; - bool have_absolute = false; - PyObject* change_obj = Py_None; - PyObject* absolute_obj = Py_None; - static const char* kwlist[] = {"arg", "change", "absolute", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s|OO", - const_cast(kwlist), &arg, - &change_obj, &absolute_obj)) { - return nullptr; - } - if (change_obj != Py_None) { - if (absolute_obj != Py_None) { - throw Exception("Can't provide both a change and absolute"); - } - have_change = true; - change = Python::GetPyDouble(change_obj); - } - if (absolute_obj != Py_None) { - have_absolute = true; - absolute = Python::GetPyDouble(absolute_obj); - } - double return_val = 0.0f; - if (!strcmp(arg, "bufferTime")) { - if (have_change) { - g_app->buffer_time += static_cast(change); - } - if (have_absolute) { - g_app->buffer_time = static_cast(absolute); - } - g_app->buffer_time = std::max(0, g_app->buffer_time); - return_val = g_app->buffer_time; - } else if (!strcmp(arg, "delaySampling")) { - if (have_change) { - g_app->delay_bucket_samples += static_cast(change); - } - if (have_absolute) { - g_app->buffer_time = static_cast(absolute); - } - g_app->delay_bucket_samples = std::max(1, g_app->delay_bucket_samples); - return_val = g_app->delay_bucket_samples; - } else if (!strcmp(arg, "dynamicsSyncTime")) { - if (have_change) { - g_app->dynamics_sync_time += static_cast(change); - } - if (have_absolute) { - g_app->dynamics_sync_time = static_cast(absolute); - } - g_app->dynamics_sync_time = std::max(0, g_app->dynamics_sync_time); - return_val = g_app->dynamics_sync_time; - } else if (!strcmp(arg, "showNetInfo")) { - if (have_change && change > 0.5f) { - g_graphics->set_show_net_info(true); - } - if (have_change && change < -0.5f) { - g_graphics->set_show_net_info(false); - } - if (have_absolute) { - g_graphics->set_show_net_info(static_cast(absolute)); - } - return_val = g_graphics->show_net_info(); - } else if (!strcmp(arg, "allowCameraMovement")) { - Camera* camera = g_graphics->camera(); - if (camera) { - if (have_change && change > 0.5f) { - camera->set_lock_panning(false); - } - if (have_change && change < -0.5f) { - camera->set_lock_panning(true); - } - if (have_absolute) { - camera->set_lock_panning(!static_cast(absolute)); - } - return_val = !camera->lock_panning(); - } - } else if (!strcmp(arg, "cameraPanSpeedScale")) { - Camera* camera = g_graphics->camera(); - if (camera) { - double val = camera->pan_speed_scale(); - if (have_change) { - camera->set_pan_speed_scale(static_cast(val + change)); - } - if (have_absolute) { - camera->set_pan_speed_scale(static_cast(absolute)); - } - if (camera->pan_speed_scale() < 0) { - camera->set_pan_speed_scale(0); - } - return_val = camera->pan_speed_scale(); - } - } else { - auto handled = - g_graphics->ValueTest(arg, have_absolute ? &absolute : nullptr, - have_change ? &change : nullptr, &return_val); - if (!handled) { - ScreenMessage("invalid arg: " + std::string(arg)); - } - } - - return PyFloat_FromDouble(return_val); - - BA_PYTHON_CATCH; -} - -auto PyDebugPrintPyErr(PyObject* self, PyObject* args) -> PyObject* { - if (PyErr_Occurred()) { - // we pass zero here to avoid grabbing references to this exception - // which can cause objects to stick around and trip up our deletion checks - // (nodes, actors existing after their games have ended) - PyErr_PrintEx(0); - PyErr_Clear(); - } - Py_RETURN_NONE; -} - -auto PyPrintContext(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - Python::PrintContextAuto(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyPrintLoadInfo(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - g_assets->PrintLoadInfo(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetReplaysDir(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - return PyUnicode_FromString(g_platform->GetReplaysDir().c_str()); - BA_PYTHON_CATCH; -} - -auto PyGetAppConfigDefaultValue(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - const char* key = ""; - static const char* kwlist[] = {"key", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &key)) { - return nullptr; - } - const AppConfig::Entry* entry = g_app_config->GetEntry(key); - if (entry == nullptr) { - throw Exception("Invalid config value '" + std::string(key) + "'", - PyExcType::kValue); - } - switch (entry->GetType()) { - case AppConfig::Entry::Type::kString: - return PyUnicode_FromString(entry->DefaultStringValue().c_str()); - case AppConfig::Entry::Type::kInt: - return PyLong_FromLong(entry->DefaultIntValue()); - case AppConfig::Entry::Type::kFloat: - return PyFloat_FromDouble(entry->DefaultFloatValue()); - case AppConfig::Entry::Type::kBool: - if (entry->DefaultBoolValue()) { - Py_RETURN_TRUE; - } - Py_RETURN_FALSE; - default: - throw Exception(PyExcType::kValue); - } - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyAppConfigGetBuiltinKeys(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - PythonRef list(PyList_New(0), PythonRef::kSteal); - for (auto&& i : g_app_config->entries_by_name()) { - PyList_Append(list.get(), PyUnicode_FromString(i.first.c_str())); - } - return list.HandOver(); - BA_PYTHON_CATCH; -} - -auto PyResolveAppConfigValue(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - - const char* key; - static const char* kwlist[] = {"key", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &key)) { - return nullptr; - } - auto entry = g_app_config->GetEntry(key); - if (entry == nullptr) { - throw Exception("Invalid config value '" + std::string(key) + "'.", - PyExcType::kValue); - } - switch (entry->GetType()) { - case AppConfig::Entry::Type::kString: - return PyUnicode_FromString(entry->StringValue().c_str()); - case AppConfig::Entry::Type::kInt: - return PyLong_FromLong(entry->IntValue()); - case AppConfig::Entry::Type::kFloat: - return PyFloat_FromDouble(entry->FloatValue()); - case AppConfig::Entry::Type::kBool: - if (entry->BoolValue()) { - Py_RETURN_TRUE; - } - Py_RETURN_FALSE; - - default: - throw Exception(PyExcType::kValue); - } - BA_PYTHON_CATCH; -} - -auto PyGetLowLevelConfigValue(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* key; - int default_value; - static const char* kwlist[] = {"key", "default_value", nullptr}; - if (!PyArg_ParseTupleAndKeywords( - args, keywds, "si", const_cast(kwlist), &key, &default_value)) - return nullptr; - return PyLong_FromLong( - g_platform->GetLowLevelConfigValue(key, default_value)); - BA_PYTHON_CATCH; -} - -auto PySetLowLevelConfigValue(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* key; - int value; - static const char* kwlist[] = {"key", "value", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "si", - const_cast(kwlist), &key, &value)) - return nullptr; - g_platform->SetLowLevelConfigValue(key, value); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetPlatformMiscReadVals(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - PyObject* vals_obj; - static const char* kwlist[] = {"mode", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", - const_cast(kwlist), &vals_obj)) { - return nullptr; - } - std::string vals = Python::GetPyString(vals_obj); - g_platform->SetPlatformMiscReadVals(vals); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetLogFilePath(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - std::string config_dir = g_platform->GetConfigDirectory(); - std::string logpath = config_dir + BA_DIRSLASH + "log.json"; - return PyUnicode_FromString(logpath.c_str()); - BA_PYTHON_CATCH; -} - -auto PyGetVolatileDataDirectory(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - return PyUnicode_FromString(g_platform->GetVolatileDataDirectory().c_str()); - BA_PYTHON_CATCH; -} - -auto PyIsLogFull(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - if (g_app->v1_cloud_log_full) { - Py_RETURN_TRUE; - } - Py_RETURN_FALSE; - BA_PYTHON_CATCH; -} - -auto PyGetV1CloudLog(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - std::string log_fin; - { - std::scoped_lock lock(g_app->v1_cloud_log_mutex); - log_fin = g_app->v1_cloud_log; - } - // we want to use something with error handling here since the last - // bit of this string could be truncated utf8 chars.. - return PyUnicode_FromString( - Utils::GetValidUTF8(log_fin.c_str(), "_glg1").c_str()); - BA_PYTHON_CATCH; -} - -auto PyMarkLogSent(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - // This way we won't try to send it at shutdown time and whatnot - g_app->did_put_v1_cloud_log = true; - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyIncrementAnalyticsCount(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* name; - int increment = 1; - static const char* kwlist[] = {"name", "increment", nullptr}; - if (!PyArg_ParseTupleAndKeywords( - args, keywds, "s|p", const_cast(kwlist), &name, &increment)) { - return nullptr; - } - g_platform->IncrementAnalyticsCount(name, increment); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyIncrementAnalyticsCountRaw(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - const char* name; - int increment = 1; - static const char* kwlist[] = {"name", "increment", nullptr}; - if (!PyArg_ParseTupleAndKeywords( - args, keywds, "s|i", const_cast(kwlist), &name, &increment)) { - return nullptr; - } - g_platform->IncrementAnalyticsCountRaw(name, increment); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyIncrementAnalyticsCountRaw2(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - const char* name; - int uses_increment = 1; - int increment = 1; - static const char* kwlist[] = {"name", "uses_increment", "increment", - nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s|ii", - const_cast(kwlist), &name, - &uses_increment, &increment)) { - return nullptr; - } - g_platform->IncrementAnalyticsCountRaw2(name, uses_increment, increment); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySubmitAnalyticsCounts(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - g_platform->SubmitAnalyticsCounts(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetAnalyticsScreen(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - const char* screen; - static const char* kwlist[] = {"screen", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &screen)) { - return nullptr; - } - g_platform->SetAnalyticsScreen(screen); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyLoginAdapterGetSignInToken(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - const char* login_type; - int attempt_id; - static const char* kwlist[] = {"login_type", "attempt_id", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "si", - const_cast(kwlist), &login_type, - &attempt_id)) { - return nullptr; - } - g_platform->LoginAdapterGetSignInToken(login_type, attempt_id); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyLoginAdapterBackEndActiveChange(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - const char* login_type; - int active; - static const char* kwlist[] = {"login_type", "active", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "sp", - const_cast(kwlist), &login_type, - &active)) { - return nullptr; - } - g_platform->LoginAdapterBackEndActiveChange(login_type, active); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PySetInternalLanguageKeys(PyObject* self, PyObject* args) -> PyObject* { - BA_PYTHON_TRY; - PyObject* list_obj; - PyObject* random_names_list_obj; - if (!PyArg_ParseTuple(args, "OO", &list_obj, &random_names_list_obj)) { - return nullptr; - } - BA_PRECONDITION(PyList_Check(list_obj)); - BA_PRECONDITION(PyList_Check(random_names_list_obj)); - std::unordered_map language; - int size = static_cast(PyList_GET_SIZE(list_obj)); - for (int i = 0; i < size; i++) { - PyObject* entry = PyList_GET_ITEM(list_obj, i); - if (!PyTuple_Check(entry) || PyTuple_GET_SIZE(entry) != 2 - || !PyUnicode_Check(PyTuple_GET_ITEM(entry, 0)) - || !PyUnicode_Check(PyTuple_GET_ITEM(entry, 1))) { - throw Exception("Invalid root language data."); - } - language[PyUnicode_AsUTF8(PyTuple_GET_ITEM(entry, 0))] = - PyUnicode_AsUTF8(PyTuple_GET_ITEM(entry, 1)); - } - size = static_cast(PyList_GET_SIZE(random_names_list_obj)); - std::list random_names; - for (int i = 0; i < size; i++) { - PyObject* entry = PyList_GET_ITEM(random_names_list_obj, i); - if (!PyUnicode_Check(entry)) { - throw Exception("Got non-string in random name list.", PyExcType::kType); - } - random_names.emplace_back(PyUnicode_AsUTF8(entry)); - } - Utils::SetRandomNameList(random_names); - assert(g_logic); - g_logic->SetLanguageKeys(language); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -#pragma clang diagnostic push -#pragma ide diagnostic ignored "ConstantFunctionResult" - -auto PyAndroidGetExternalFilesDir(PyObject* self, PyObject* args, - PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } -#if BA_OSTYPE_ANDROID - std::string path = g_platform->AndroidGetExternalFilesDir(); - if (path.empty()) { - Py_RETURN_NONE; - } else { - assert(Utils::IsValidUTF8(path)); - return PyUnicode_FromString(path.c_str()); - } -#else // BA_OSTYPE_ANDROID - throw Exception("Only valid on android."); -#endif // BA_OSTYPE_ANDROID - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} -#pragma clang diagnostic pop - -auto PyAndroidShowWifiSettings(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - g_platform->AndroidShowWifiSettings(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyLsObjects(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - Object::LsObjects(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyLsInputDevices(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - g_input->LsInputDevices(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyDoOnce(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - if (g_python->DoOnce()) { - Py_RETURN_TRUE; - } - Py_RETURN_FALSE; - BA_PYTHON_CATCH; -} - -auto PyApp(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - return g_python->obj(Python::ObjID::kApp).NewRef(); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -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_is_supported() 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_is_supported() 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."}, - - {"ls_objects", (PyCFunction)PyLsObjects, METH_VARARGS | METH_KEYWORDS, - "ls_objects() -> None\n" - "\n" - "Log debugging info about C++ level objects.\n" - "\n" - "Category: **General Utility Functions**\n" - "\n" - "This call only functions in debug builds of the game.\n" - "It prints various info about the current object count, etc."}, - - {"ls_input_devices", (PyCFunction)PyLsInputDevices, - METH_VARARGS | METH_KEYWORDS, - "ls_input_devices() -> None\n" - "\n" - "Print debugging info about game objects.\n" - "\n" - "Category: **General Utility Functions**\n" - "\n" - "This call only functions in debug builds of the game.\n" - "It prints various info about the current object count, etc."}, - - {"do_once", (PyCFunction)PyDoOnce, METH_VARARGS | METH_KEYWORDS, - "do_once() -> bool\n" - "\n" - "Return whether this is the first time running a line of code.\n" - "\n" - "Category: **General Utility Functions**\n" - "\n" - "This is used by 'print_once()' type calls to keep from overflowing\n" - "logs. The call functions by registering the filename and line where\n" - "The call is made from. Returns True if this location has not been\n" - "registered already, and False if it has.\n" - "\n" - "##### Example\n" - "This print will only fire for the first loop iteration:\n" - ">>> for i in range(10):\n" - "... if ba.do_once():\n" - "... print('Hello once from loop!')\n"}, - - {"_app", (PyCFunction)PyApp, METH_VARARGS | METH_KEYWORDS, - "_app() -> ba.App\n" - "\n" - "(internal)"}, - - {"android_get_external_files_dir", - (PyCFunction)PyAndroidGetExternalFilesDir, METH_VARARGS | METH_KEYWORDS, - "android_get_external_files_dir() -> str\n" - "\n" - "(internal)\n" - "\n" - "Returns the android external storage path, or None if there is none " - "on\n" - "this device"}, - - {"android_show_wifi_settings", (PyCFunction)PyAndroidShowWifiSettings, - METH_VARARGS | METH_KEYWORDS, - "android_show_wifi_settings() -> None\n" - "\n" - "(internal)"}, - - {"set_internal_language_keys", PySetInternalLanguageKeys, METH_VARARGS, - "set_internal_language_keys(listobj: list[tuple[str, str]],\n" - " random_names_list: list[tuple[str, str]]) -> None\n" - "\n" - "(internal)"}, - - {"set_analytics_screen", (PyCFunction)PySetAnalyticsScreen, - METH_VARARGS | METH_KEYWORDS, - "set_analytics_screen(screen: str) -> None\n" - "\n" - "Used for analytics to see where in the app players spend their time.\n" - "\n" - "Category: **General Utility Functions**\n" - "\n" - "Generally called when opening a new window or entering some UI.\n" - "'screen' should be a string description of an app location\n" - "('Main Menu', etc.)"}, - - {"login_adapter_get_sign_in_token", - (PyCFunction)PyLoginAdapterGetSignInToken, METH_VARARGS | METH_KEYWORDS, - "login_adapter_get_sign_in_token(login_type: str, attempt_id: int)" - " -> None\n" - "\n" - "(internal)"}, - - {"login_adapter_back_end_active_change", - (PyCFunction)PyLoginAdapterBackEndActiveChange, - METH_VARARGS | METH_KEYWORDS, - "login_adapter_back_end_active_change(login_type: str, active: bool)" - " -> None\n" - "\n" - "(internal)"}, - - {"submit_analytics_counts", (PyCFunction)PySubmitAnalyticsCounts, - METH_VARARGS | METH_KEYWORDS, - "submit_analytics_counts() -> None\n" - "\n" - "(internal)"}, - - {"increment_analytics_count_raw_2", - (PyCFunction)PyIncrementAnalyticsCountRaw2, METH_VARARGS | METH_KEYWORDS, - "increment_analytics_count_raw_2(name: str,\n" - " uses_increment: bool = True, increment: int = 1) -> None\n" - "\n" - "(internal)"}, - - {"increment_analytics_counts_raw", - (PyCFunction)PyIncrementAnalyticsCountRaw, METH_VARARGS | METH_KEYWORDS, - "increment_analytics_counts_raw(name: str, increment: int = 1) -> None\n" - "\n" - "(internal)"}, - - {"increment_analytics_count", (PyCFunction)PyIncrementAnalyticsCount, - METH_VARARGS | METH_KEYWORDS, - "increment_analytics_count(name: str, increment: int = 1) -> None\n" - "\n" - "(internal)"}, - - {"mark_log_sent", (PyCFunction)PyMarkLogSent, - METH_VARARGS | METH_KEYWORDS, - "mark_log_sent() -> None\n" - "\n" - "(internal)"}, - - {"get_v1_cloud_log", (PyCFunction)PyGetV1CloudLog, - METH_VARARGS | METH_KEYWORDS, - "get_v1_cloud_log() -> str\n" - "\n" - "(internal)"}, - - {"is_log_full", PyIsLogFull, METH_VARARGS, - "is_log_full() -> bool\n" - "\n" - "(internal)"}, - - {"get_v1_cloud_log_file_path", PyGetLogFilePath, METH_VARARGS, - "get_v1_cloud_log_file_path() -> str\n" - "\n" - "(internal)\n" - "\n" - "Return the path to the app log file."}, - - {"get_volatile_data_directory", PyGetVolatileDataDirectory, METH_VARARGS, - "get_volatile_data_directory() -> str\n" - "\n" - "(internal)\n" - "\n" - "Return the path to the app volatile data directory.\n" - "This directory is for data generated by the app that does not\n" - "need to be backed up and can be recreated if necessary."}, - - {"set_platform_misc_read_vals", (PyCFunction)PySetPlatformMiscReadVals, - METH_VARARGS | METH_KEYWORDS, - "set_platform_misc_read_vals(mode: str) -> None\n" - "\n" - "(internal)"}, - - {"set_low_level_config_value", (PyCFunction)PySetLowLevelConfigValue, - METH_VARARGS | METH_KEYWORDS, - "set_low_level_config_value(key: str, value: int) -> None\n" - "\n" - "(internal)"}, - - {"get_low_level_config_value", (PyCFunction)PyGetLowLevelConfigValue, - METH_VARARGS | METH_KEYWORDS, - "get_low_level_config_value(key: str, default_value: int) -> int\n" - "\n" - "(internal)"}, - - {"resolve_appconfig_value", (PyCFunction)PyResolveAppConfigValue, - METH_VARARGS | METH_KEYWORDS, - "resolve_appconfig_value(key: str) -> Any\n" - "\n" - "(internal)"}, - - {"get_appconfig_default_value", (PyCFunction)PyGetAppConfigDefaultValue, - METH_VARARGS | METH_KEYWORDS, - "get_appconfig_default_value(key: str) -> Any\n" - "\n" - "(internal)"}, - - {"get_appconfig_builtin_keys", (PyCFunction)PyAppConfigGetBuiltinKeys, - METH_VARARGS | METH_KEYWORDS, - "get_appconfig_builtin_keys() -> list[str]\n" - "\n" - "(internal)"}, - - {"get_replays_dir", (PyCFunction)PyGetReplaysDir, - METH_VARARGS | METH_KEYWORDS, - "get_replays_dir() -> str\n" - "\n" - "(internal)"}, - - {"print_load_info", (PyCFunction)PyPrintLoadInfo, - METH_VARARGS | METH_KEYWORDS, - "print_load_info() -> None\n" - "\n" - "(internal)\n" - "\n" - "Category: **General Utility Functions**"}, - - {"print_context", (PyCFunction)PyPrintContext, - METH_VARARGS | METH_KEYWORDS, - "print_context() -> None\n" - "\n" - "(internal)\n" - "\n" - "Prints info about the current context state; for debugging.\n"}, - - {"debug_print_py_err", PyDebugPrintPyErr, METH_VARARGS, - "debug_print_py_err() -> None\n" - "\n" - "(internal)\n" - "\n" - "Debugging func for tracking leaked Python errors in the C++ layer.."}, - - {"value_test", (PyCFunction)PyValueTest, METH_VARARGS | METH_KEYWORDS, - "value_test(arg: str, change: float | None = None,\n" - " absolute: float | None = None) -> float\n" - "\n" - "(internal)"}, - - {"workspaces_in_use", PyWorkspacesInUse, METH_VARARGS, - "workspaces_in_use() -> bool\n" - "\n" - "(internal)\n" - "\n" - "Returns whether workspaces functionality has been enabled at\n" - "any point this run."}, - - {"has_user_run_commands", PyHasUserRunCommands, METH_VARARGS, - "has_user_run_commands() -> bool\n" - "\n" - "(internal)"}, - - {"contains_python_dist", PyContainsPythonDist, METH_VARARGS, - "contains_python_dist() -> bool\n" - "\n" - "(internal)"}, - - {"get_idle_time", PyGetIdleTime, METH_VARARGS, - "get_idle_time() -> int\n" - "\n" - "(internal)\n" - "\n" - "Returns the amount of time since any game input has been received."}, - - {"ehv", (PyCFunction)PyExtraHashValue, METH_VARARGS | METH_KEYWORDS, - "ehv() -> None\n" - "\n" - "(internal)"}, - - {"get_thread_name", (PyCFunction)PyGetThreadName, - METH_VARARGS | METH_KEYWORDS, - "get_thread_name() -> str\n" - "\n" - "(internal)\n" - "\n" - "Returns the name of the current thread.\n" - "This may vary depending on platform and should not be used in logic;\n" - "only for debugging."}, - - {"set_thread_name", (PyCFunction)PySetThreadName, - METH_VARARGS | METH_KEYWORDS, - "set_thread_name(name: str) -> None\n" - "\n" - "(internal)\n" - "\n" - "Sets the name of the current thread (on platforms where this is\n" - "available). Thread names are only for debugging and should not be\n" - "used in logic, as naming behavior can vary across platforms.\n"}, - - {"in_logic_thread", (PyCFunction)PyInLogicThread, - METH_VARARGS | METH_KEYWORDS, - "in_logic_thread() -> bool\n" - "\n" - "(internal)\n" - "\n" - "Returns whether or not the current thread is the logic thread."}, - - {"request_permission", (PyCFunction)PyRequestPermission, - METH_VARARGS | METH_KEYWORDS, - "request_permission(permission: ba.Permission) -> None\n" - "\n" - "(internal)"}, - - {"have_permission", (PyCFunction)PyHavePermission, - METH_VARARGS | METH_KEYWORDS, - "have_permission(permission: ba.Permission) -> bool\n" - "\n" - "(internal)"}, - - {"is_running_on_fire_tv", PyIsRunningOnFireTV, METH_VARARGS, - "is_running_on_fire_tv() -> bool\n" - "\n" - "(internal)"}, - - {"is_running_on_ouya", PyIsRunningOnOuya, METH_VARARGS, - "is_running_on_ouya() -> bool\n" - "\n" - "(internal)"}, - - {"setup_sigint", (PyCFunction)PySetUpSigInt, METH_NOARGS, - "setup_sigint() -> None\n" - "\n" - "(internal)"}, - }; -} - -#pragma clang diagnostic pop - -} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_system.h b/src/ballistica/python/methods/python_methods_system.h deleted file mode 100644 index ffbbbd00..00000000 --- a/src/ballistica/python/methods/python_methods_system.h +++ /dev/null @@ -1,20 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_SYSTEM_H_ -#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_SYSTEM_H_ - -#include - -#include "ballistica/ballistica.h" - -namespace ballistica { - -/// System related individual python methods for our module. -class PythonMethodsSystem { - public: - static auto GetMethods() -> std::vector; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_SYSTEM_H_ diff --git a/src/ballistica/python/methods/python_methods_ui.h b/src/ballistica/python/methods/python_methods_ui.h deleted file mode 100644 index 7ec46118..00000000 --- a/src/ballistica/python/methods/python_methods_ui.h +++ /dev/null @@ -1,20 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_UI_H_ -#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_UI_H_ - -#include - -#include "ballistica/ballistica.h" - -namespace ballistica { - -/// UI related individual python methods for our module. -class PythonMethodsUI { - public: - static auto GetMethods() -> std::vector; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_UI_H_ diff --git a/src/ballistica/python/python.cc b/src/ballistica/python/python.cc deleted file mode 100644 index 4d2c57cc..00000000 --- a/src/ballistica/python/python.cc +++ /dev/null @@ -1,2881 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/python.h" - -#include "ballistica/app/app.h" -#include "ballistica/assets/component/collide_model.h" -#include "ballistica/assets/component/model.h" -#include "ballistica/assets/component/sound.h" -#include "ballistica/assets/component/texture.h" -#include "ballistica/audio/audio.h" -#include "ballistica/core/thread.h" -#include "ballistica/dynamics/material/material.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/input/device/joystick.h" -#include "ballistica/input/device/keyboard_input.h" -#include "ballistica/internal/app_internal.h" -#include "ballistica/logic/host_activity.h" -#include "ballistica/logic/player.h" -#include "ballistica/logic/v1_account.h" -#include "ballistica/python/class/python_class_activity_data.h" -#include "ballistica/python/class/python_class_collide_model.h" -#include "ballistica/python/class/python_class_context.h" -#include "ballistica/python/class/python_class_context_call.h" -#include "ballistica/python/class/python_class_data.h" -#include "ballistica/python/class/python_class_input_device.h" -#include "ballistica/python/class/python_class_material.h" -#include "ballistica/python/class/python_class_model.h" -#include "ballistica/python/class/python_class_node.h" -#include "ballistica/python/class/python_class_session_data.h" -#include "ballistica/python/class/python_class_session_player.h" -#include "ballistica/python/class/python_class_sound.h" -#include "ballistica/python/class/python_class_texture.h" -#include "ballistica/python/class/python_class_timer.h" -#include "ballistica/python/class/python_class_vec3.h" -#include "ballistica/python/class/python_class_widget.h" -#include "ballistica/python/methods/python_methods_app.h" -#include "ballistica/python/methods/python_methods_assets.h" -#include "ballistica/python/methods/python_methods_gameplay.h" -#include "ballistica/python/methods/python_methods_graphics.h" -#include "ballistica/python/methods/python_methods_input.h" -#include "ballistica/python/methods/python_methods_networking.h" -#include "ballistica/python/methods/python_methods_system.h" -#include "ballistica/python/methods/python_methods_ui.h" -#include "ballistica/python/python_command.h" -#include "ballistica/python/python_context_call_runnable.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/scene_stream.h" -#include "ballistica/ui/ui.h" -#include "ballistica/ui/widget/text_widget.h" - -// Sanity test: our XCode, Android, and Windows builds should be -// using a debug build of the python library. -// Todo: could also verify this at runtime by checking for -// existence of sys.gettotalrefcount(). (is that still valid in 3.8?) -#if BA_DEBUG_BUILD -#if BA_XCODE_BUILD || BA_OSTYPE_ANDROID || BA_OSTYPE_WINDOWS -#ifndef Py_DEBUG -#error Expected Py_DEBUG to be defined for this build. -#endif // Py_DEBUG -#endif // BA_XCODE_BUILD || BA_OSTYPE_ANDROID -#endif // BA_DEBUG_BUILD - -namespace ballistica { - -// Ignore signed bitwise stuff; python macros do it quite a bit. -#pragma clang diagnostic push -#pragma ide diagnostic ignored "hicpp-signed-bitwise" -#pragma ide diagnostic ignored "RedundantCast" - -auto Python::LoggingCall(LogLevel loglevel, const std::string& msg) -> void { - // If we've not yet captured our Python logging calls, stash this call away. - // We'll submit all accumulated entries after we bootstrap Python. - if (!objexists(ObjID::kLoggingCriticalCall)) { - std::scoped_lock lock(early_log_lock_); - early_logs_.emplace_back(std::make_pair(loglevel, msg)); - return; - } - - // Ok; seems we've got Python calls. Run the right one for our log level. - ObjID logcallobj; - switch (loglevel) { - case LogLevel::kDebug: - logcallobj = Python::ObjID::kLoggingDebugCall; - break; - case LogLevel::kInfo: - logcallobj = Python::ObjID::kLoggingInfoCall; - break; - case LogLevel::kWarning: - logcallobj = Python::ObjID::kLoggingWarningCall; - break; - case LogLevel::kError: - logcallobj = Python::ObjID::kLoggingErrorCall; - break; - case LogLevel::kCritical: - logcallobj = Python::ObjID::kLoggingCriticalCall; - break; - default: - logcallobj = Python::ObjID::kLoggingInfoCall; - fprintf(stderr, "Unexpected LogLevel %d\n", static_cast(loglevel)); - break; - } - - // Make sure we're good to go from any thread. - ScopedInterpreterLock lock; - - PythonRef args(Py_BuildValue("(s)", msg.c_str()), PythonRef::kSteal); - obj(logcallobj).Call(args); -} - -void Python::SetPythonException(const Exception& exc) { - PyExcType exctype{exc.python_type()}; - const char* description{GetShortExceptionDescription(exc)}; - PyObject* pytype{}; - switch (exctype) { - case PyExcType::kRuntime: - pytype = PyExc_RuntimeError; - break; - case PyExcType::kAttribute: - pytype = PyExc_AttributeError; - break; - case PyExcType::kIndex: - pytype = PyExc_IndexError; - break; - case PyExcType::kValue: - pytype = PyExc_ValueError; - break; - case PyExcType::kType: - pytype = PyExc_TypeError; - break; - case PyExcType::kContext: - pytype = g_python->obj(Python::ObjID::kContextError).get(); - break; - case PyExcType::kNotFound: - pytype = g_python->obj(Python::ObjID::kNotFoundError).get(); - break; - case PyExcType::kNodeNotFound: - pytype = g_python->obj(Python::ObjID::kNodeNotFoundError).get(); - break; - case PyExcType::kSessionPlayerNotFound: - pytype = g_python->obj(Python::ObjID::kSessionPlayerNotFoundError).get(); - break; - case PyExcType::kInputDeviceNotFound: - pytype = g_python->obj(Python::ObjID::kInputDeviceNotFoundError).get(); - break; - case PyExcType::kDelegateNotFound: - pytype = g_python->obj(Python::ObjID::kDelegateNotFoundError).get(); - break; - case PyExcType::kWidgetNotFound: - pytype = g_python->obj(Python::ObjID::kWidgetNotFoundError).get(); - break; - case PyExcType::kActivityNotFound: - pytype = g_python->obj(Python::ObjID::kActivityNotFoundError).get(); - break; - case PyExcType::kSessionNotFound: - pytype = g_python->obj(Python::ObjID::kSessionNotFoundError).get(); - break; - } - assert(pytype != nullptr && PyType_Check(pytype)); - PyErr_SetString(pytype, description); -} - -const char* Python::ScopedCallLabel::current_label_ = nullptr; - -auto Python::HaveGIL() -> bool { return static_cast(PyGILState_Check()); } - -void Python::PrintStackTrace() { - ScopedInterpreterLock lock; - auto objid{Python::ObjID::kPrintTraceCall}; - if (g_python->objexists(objid)) { - g_python->obj(objid).Call(); - } else { - Log(LogLevel::kWarning, - "Python::PrintStackTrace() called before bootstrap complete; " - "not printing."); - } -} - -// Return whether GetPyString() will succeed for an object. -auto Python::IsPyString(PyObject* o) -> bool { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - return (PyUnicode_Check(o) - || PyObject_IsInstance( - o, g_python->obj(Python::ObjID::kLStrClass).get())); -} - -auto Python::GetPyString(PyObject* o) -> std::string { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - PyExcType exctype{PyExcType::kType}; - if (PyUnicode_Check(o)) { - return PyUnicode_AsUTF8(o); - } else { - // Check if its a Lstr. If so; we pull its json string representation. - int result = - PyObject_IsInstance(o, g_python->obj(Python::ObjID::kLStrClass).get()); - if (result == -1) { - PyErr_Clear(); - result = 0; - } - if (result == 1) { - // At this point its not a simple type error if something goes wonky. - // Perhaps we should try to preserve any error type raised by - // the _get_json() call... - exctype = PyExcType::kRuntime; - PythonRef get_json_call(PyObject_GetAttrString(o, "_get_json"), - PythonRef::kSteal); - if (get_json_call.CallableCheck()) { - PythonRef json = get_json_call.Call(); - if (PyUnicode_Check(json.get())) { - return PyUnicode_AsUTF8(json.get()); - } - } - } - } - - // Failed, we have. - // Clear any Python error that got us here; we're in C++ Exception land now. - PyErr_Clear(); - throw Exception( - "Can't get string from value: " + Python::ObjToString(o) + ".", exctype); -} - -template -auto GetPyIntT(PyObject* o) -> T { - assert(Python::HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (PyLong_Check(o)) { - return static_cast_check_fit(PyLong_AS_LONG(o)); - } - if (PyNumber_Check(o)) { - PyObject* f = PyNumber_Long(o); - if (f) { - auto val = static_cast_check_fit(PyLong_AS_LONG(f)); - Py_DECREF(f); - return val; - } - } - - // Failed, we have. - // Clear any Python error that got us here; we're in C++ Exception land now. - PyErr_Clear(); - - // Assuming any failure here was type related. - throw Exception("Can't get int from value: " + Python::ObjToString(o) + ".", - PyExcType::kType); -} - -auto Python::GetPyInt64(PyObject* o) -> int64_t { - return GetPyIntT(o); -} - -auto Python::GetPyInt(PyObject* o) -> int { return GetPyIntT(o); } - -auto Python::GetPyBool(PyObject* o) -> bool { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (o == Py_True) { - return true; - } - if (o == Py_False) { - return false; - } - if (PyLong_Check(o)) { - return (PyLong_AS_LONG(o) != 0); - } - if (PyNumber_Check(o)) { - if (PyObject* o2 = PyNumber_Long(o)) { - auto val = PyLong_AS_LONG(o2); - Py_DECREF(o2); - return (val != 0); - } - } - - // Failed, we have. - // Clear any Python error that got us here; we're in C++ Exception land now. - PyErr_Clear(); - - // Assuming any failure here was type related. - throw Exception("Can't get bool from value: " + Python::ObjToString(o) + ".", - PyExcType::kType); -} - -auto Python::IsPySession(PyObject* o) -> bool { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - int result = - PyObject_IsInstance(o, g_python->obj(ObjID::kSessionClass).get()); - if (result == -1) { - PyErr_Clear(); - result = 0; - } - return static_cast(result); -} - -auto Python::GetPySession(PyObject* o) -> Session* { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - PyExcType pyexctype{PyExcType::kType}; - if (IsPySession(o)) { - // Look for an _sessiondata attr on it. - if (PyObject* sessiondata = PyObject_GetAttrString(o, "_sessiondata")) { - // This will deallocate for us. - PythonRef ref(sessiondata, PythonRef::kSteal); - if (PythonClassSessionData::Check(sessiondata)) { - // This will succeed or throw its own Exception. - return (reinterpret_cast(sessiondata)) - ->GetSession(); - } - } else { - pyexctype = PyExcType::kRuntime; // Wonky session obj. - } - } - - // Failed, we have. - // Clear any Python error that got us here; we're in C++ Exception land now. - PyErr_Clear(); - throw Exception( - "Can't get Session from value: " + Python::ObjToString(o) + ".", - pyexctype); -} - -auto Python::IsPyPlayer(PyObject* o) -> bool { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - int result = PyObject_IsInstance(o, g_python->obj(ObjID::kPlayerClass).get()); - if (result == -1) { - result = 0; - PyErr_Clear(); - } - return static_cast(result); -} - -auto Python::GetPyPlayer(PyObject* o, bool allow_empty_ref, bool allow_none) - -> Player* { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - PyExcType pyexctype{PyExcType::kType}; - - if (allow_none && (o == Py_None)) { - return nullptr; - } - - // Make sure it's a subclass of ba.Player. - if (IsPyPlayer(o)) { - // Look for an sessionplayer attr on it. - if (PyObject* sessionplayer = PyObject_GetAttrString(o, "sessionplayer")) { - // This will deallocate for us. - PythonRef ref(sessionplayer, PythonRef::kSteal); - - if (PythonClassSessionPlayer::Check(sessionplayer)) { - // This will succeed or throw an exception itself. - return (reinterpret_cast(sessionplayer)) - ->GetPlayer(!allow_empty_ref); - } - } else { - pyexctype = PyExcType::kRuntime; // We've got a wonky object. - } - } - - // Failed, we have. - // Clear any Python error that got us here; we're in C++ Exception land now. - PyErr_Clear(); - throw Exception( - "Can't get player from value: " + Python::ObjToString(o) + ".", - pyexctype); -} - -auto Python::IsPyHostActivity(PyObject* o) -> bool { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - int result = - PyObject_IsInstance(o, g_python->obj(ObjID::kActivityClass).get()); - if (result == -1) { - result = 0; - PyErr_Clear(); - } - return static_cast(result); -} - -auto Python::GetPyHostActivity(PyObject* o) -> HostActivity* { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - PyExcType pyexctype{PyExcType::kType}; - - // Make sure it's a subclass of ba.Activity. - if (IsPyHostActivity(o)) { - // Look for an _activity_data attr on it. - if (PyObject* activity_data = PyObject_GetAttrString(o, "_activity_data")) { - // This will deallocate for us. - PythonRef ref(activity_data, PythonRef::kSteal); - if (PythonClassActivityData::Check(activity_data)) { - return (reinterpret_cast(activity_data)) - ->GetHostActivity(); - } - } else { - pyexctype = PyExcType::kRuntime; // activity obj is wonky. - } - } - - // Failed, we have. - // Clear any Python error that got us here; we're in C++ Exception land now. - PyErr_Clear(); - throw Exception( - "Can't get activity from value: " + Python::ObjToString(o) + ".", - pyexctype); -} - -auto Python::GetPyNode(PyObject* o, bool allow_empty_ref, bool allow_none) - -> Node* { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (allow_none && (o == Py_None)) { - return nullptr; - } - if (PythonClassNode::Check(o)) { - // This will succeed or throw its own Exception. - return (reinterpret_cast(o))->GetNode(!allow_empty_ref); - } - - // Nothing here should have led to an unresolved Python error state. - assert(!PyErr_Occurred()); - - throw Exception("Can't get node from value: " + Python::ObjToString(o) + ".", - PyExcType::kType); -} - -auto Python::GetPyInputDevice(PyObject* o) -> InputDevice* { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (PythonClassInputDevice::Check(o)) { - // This will succeed or throw its own Exception. - return reinterpret_cast(o)->GetInputDevice(); - } - - // Nothing here should have led to an unresolved Python error state. - assert(!PyErr_Occurred()); - - throw Exception( - "Can't get input-device from value: " + Python::ObjToString(o) + ".", - PyExcType::kType); -} - -auto Python::GetPySessionPlayer(PyObject* o, bool allow_empty_ref, - bool allow_none) -> Player* { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (allow_none && (o == Py_None)) { - return nullptr; - } - if (PythonClassSessionPlayer::Check(o)) { - // This will succeed or throw its own Exception. - return reinterpret_cast(o)->GetPlayer( - !allow_empty_ref); - } - - // Nothing here should have led to an unresolved Python error state. - assert(!PyErr_Occurred()); - - throw Exception( - "Can't get ba.SessionPlayer from value: " + Python::ObjToString(o) + ".", - PyExcType::kType); -} - -auto Python::GetPyTexture(PyObject* o, bool allow_empty_ref, bool allow_none) - -> Texture* { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (allow_none && (o == Py_None)) { - return nullptr; - } - if (PythonClassTexture::Check(o)) { - // This will succeed or throw its own Exception. - return reinterpret_cast(o)->GetTexture( - !allow_empty_ref); - } - - // Nothing here should have led to an unresolved Python error state. - assert(!PyErr_Occurred()); - - throw Exception( - "Can't get ba.Texture from value: " + Python::ObjToString(o) + ".", - PyExcType::kType); -} - -auto Python::GetPyModel(PyObject* o, bool allow_empty_ref, bool allow_none) - -> Model* { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (allow_none && (o == Py_None)) { - return nullptr; - } - if (PythonClassModel::Check(o)) { - // This will succeed or throw its own Exception. - return reinterpret_cast(o)->GetModel(!allow_empty_ref); - } - - // Nothing here should have led to an unresolved Python error state. - assert(!PyErr_Occurred()); - - throw Exception( - "Can't get ba.Model from value: " + Python::ObjToString(o) + ".", - PyExcType::kType); -} - -auto Python::GetPySound(PyObject* o, bool allow_empty_ref, bool allow_none) - -> Sound* { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (allow_none && (o == Py_None)) { - return nullptr; - } - if (PythonClassSound::Check(o)) { - // This will succeed or throw its own Exception. - return reinterpret_cast(o)->GetSound(!allow_empty_ref); - } - - // Nothing here should have led to an unresolved Python error state. - assert(!PyErr_Occurred()); - - throw Exception( - "Can't get ba.Sound from value: " + Python::ObjToString(o) + ".", - PyExcType::kType); -} - -auto Python::GetPyData(PyObject* o, bool allow_empty_ref, bool allow_none) - -> Data* { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (allow_none && (o == Py_None)) { - return nullptr; - } - if (PythonClassData::Check(o)) { - // This will succeed or throw its own Exception. - return reinterpret_cast(o)->GetData(!allow_empty_ref); - } - - // Nothing here should have led to an unresolved Python error state. - assert(!PyErr_Occurred()); - - throw Exception( - "Can't get ba.Data from value: " + Python::ObjToString(o) + ".", - PyExcType::kType); -} - -auto Python::GetPyCollideModel(PyObject* o, bool allow_empty_ref, - bool allow_none) -> CollideModel* { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (allow_none && (o == Py_None)) { - return nullptr; - } - if (PythonClassCollideModel::Check(o)) { - // This will succeed or throw its own Exception. - return reinterpret_cast(o)->GetCollideModel( - !allow_empty_ref); - } - - // Nothing here should have led to an unresolved Python error state. - assert(!PyErr_Occurred()); - - throw Exception( - "Can't get ba.CollideModel from value: " + Python::ObjToString(o) + ".", - PyExcType::kType); -} - -auto Python::GetPyWidget(PyObject* o) -> Widget* { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (PythonClassWidget::Check(o)) { - // This will succeed or throw its own Exception. - return reinterpret_cast(o)->GetWidget(); - } - - // Nothing here should have led to an unresolved Python error state. - assert(!PyErr_Occurred()); - - throw Exception( - "Can't get widget from value: " + Python::ObjToString(o) + ".", - PyExcType::kType); -} - -auto Python::GetPyMaterial(PyObject* o, bool allow_empty_ref, bool allow_none) - -> Material* { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (allow_none && (o == Py_None)) { - return nullptr; - } - if (PythonClassMaterial::Check(o)) { - // This will succeed or throw its own Exception. - return reinterpret_cast(o)->GetMaterial( - !allow_empty_ref); - } - - // Nothing here should have led to an unresolved Python error state. - assert(!PyErr_Occurred()); - - throw Exception( - "Can't get material from value: " + Python::ObjToString(o) + ".", - PyExcType::kType); -} - -auto Python::CanGetPyDouble(PyObject* o) -> bool { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - return static_cast(PyNumber_Check(o)); -} - -auto Python::GetPyDouble(PyObject* o) -> double { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - // Try to take the fast path if its a float. - if (PyFloat_Check(o)) { - return PyFloat_AS_DOUBLE(o); - } - if (PyNumber_Check(o)) { - if (PyObject* f = PyNumber_Float(o)) { - double val = PyFloat_AS_DOUBLE(f); - Py_DECREF(f); - return val; - } - } - - // Failed, we have. - // Clear any Python error that got us here; we're in C++ Exception land now. - PyErr_Clear(); - throw Exception( - "Can't get double from value: " + Python::ObjToString(o) + ".", - PyExcType::kType); -} - -auto Python::GetPyFloats(PyObject* o) -> std::vector { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (!PySequence_Check(o)) { - throw Exception("Object is not a sequence.", PyExcType::kType); - } - PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); - assert(sequence.exists()); - Py_ssize_t size = PySequence_Fast_GET_SIZE(sequence.get()); - PyObject** py_objects = PySequence_Fast_ITEMS(sequence.get()); - std::vector vals(static_cast(size)); - assert(vals.size() == size); - for (Py_ssize_t i = 0; i < size; i++) { - vals[i] = Python::GetPyFloat(py_objects[i]); - } - return vals; -} - -auto Python::GetPyStrings(PyObject* o) -> std::vector { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (!PySequence_Check(o)) { - throw Exception("Object is not a sequence.", PyExcType::kType); - } - PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); - assert(sequence.exists()); - Py_ssize_t size = PySequence_Fast_GET_SIZE(sequence.get()); - PyObject** py_objects = PySequence_Fast_ITEMS(sequence.get()); - std::vector vals(static_cast(size)); - assert(vals.size() == size); - for (Py_ssize_t i = 0; i < size; i++) { - vals[i] = Python::GetPyString(py_objects[i]); - } - return vals; -} - -template -auto GetPyIntsT(PyObject* o) -> std::vector { - assert(Python::HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (!PySequence_Check(o)) { - throw Exception("Object is not a sequence.", PyExcType::kType); - } - PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); - assert(sequence.exists()); - Py_ssize_t size = PySequence_Fast_GET_SIZE(sequence.get()); - PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.get()); - std::vector vals(static_cast(size)); - assert(vals.size() == size); - for (Py_ssize_t i = 0; i < size; i++) { - vals[i] = GetPyIntT(pyobjs[i]); - } - return vals; -} - -auto Python::GetPyInts64(PyObject* o) -> std::vector { - return GetPyIntsT(o); -} - -auto Python::GetPyInts(PyObject* o) -> std::vector { - return GetPyIntsT(o); -} - -// Hmm should just template the above func? -auto Python::GetPyUInts64(PyObject* o) -> std::vector { - return GetPyIntsT(o); -} - -auto Python::GetPyNodes(PyObject* o) -> std::vector { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (!PySequence_Check(o)) { - throw Exception("Object is not a sequence.", PyExcType::kType); - } - PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); - assert(sequence.exists()); - auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.get())); - PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.get()); - std::vector vals(size); - assert(vals.size() == size); - for (size_t i = 0; i < size; i++) { - vals[i] = Python::GetPyNode(pyobjs[i]); - } - return vals; -} - -auto Python::GetPyMaterials(PyObject* o) -> std::vector { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (!PySequence_Check(o)) { - throw Exception("Object is not a sequence.", PyExcType::kType); - } - PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); - assert(sequence.exists()); - auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.get())); - PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.get()); - std::vector vals(size); - assert(vals.size() == size); - for (size_t i = 0; i < size; i++) { - vals[i] = GetPyMaterial(pyobjs[i]); // DON'T allow nullptr refs. - } - return vals; -} - -auto Python::GetPyTextures(PyObject* o) -> std::vector { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (!PySequence_Check(o)) { - throw Exception("Object is not a sequence.", PyExcType::kType); - } - PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); - assert(sequence.exists()); - auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.get())); - PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.get()); - std::vector vals(size); - assert(vals.size() == size); - for (size_t i = 0; i < size; i++) { - vals[i] = GetPyTexture(pyobjs[i]); // DON'T allow nullptr refs or None. - } - return vals; -} - -auto Python::GetPySounds(PyObject* o) -> std::vector { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (!PySequence_Check(o)) { - throw Exception("Object is not a sequence.", PyExcType::kType); - } - PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); - assert(sequence.exists()); - auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.get())); - PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.get()); - std::vector vals(size); - assert(vals.size() == size); - for (size_t i = 0; i < size; i++) { - vals[i] = GetPySound(pyobjs[i]); // DON'T allow nullptr refs - } - return vals; -} - -auto Python::GetPyModels(PyObject* o) -> std::vector { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (!PySequence_Check(o)) { - throw Exception("Object is not a sequence.", PyExcType::kType); - } - PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); - assert(sequence.exists()); - auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.get())); - PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.get()); - std::vector vals(size); - assert(vals.size() == size); - for (size_t i = 0; i < size; i++) { - vals[i] = GetPyModel(pyobjs[i], false); // DON'T allow nullptr refs. - } - return vals; -} - -auto Python::GetPyCollideModels(PyObject* o) -> std::vector { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (!PySequence_Check(o)) { - throw Exception("Object is not a sequence.", PyExcType::kType); - } - PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); - assert(sequence.exists()); - auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.get())); - PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.get()); - std::vector vals(size); - assert(vals.size() == size); - for (size_t i = 0; i < size; i++) { - vals[i] = GetPyCollideModel(pyobjs[i]); // DON'T allow nullptr refs. - } - return vals; -} - -auto Python::GetPyPoint2D(PyObject* o) -> Point2D { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - Point2D p; - if (!PyTuple_Check(o) || (PyTuple_GET_SIZE(o) != 2)) { - throw Exception("Expected 2 member tuple for point.", PyExcType::kType); - } - p.x = Python::GetPyFloat(PyTuple_GET_ITEM(o, 0)); - p.y = Python::GetPyFloat(PyTuple_GET_ITEM(o, 1)); - return p; -} - -auto Python::CanGetPyVector3f(PyObject* o) -> bool { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (PythonClassVec3::Check(o)) { - return true; - } - if (!PySequence_Check(o)) { - return false; - } - PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); - assert(sequence.exists()); // Should always work; we checked seq. - if (PySequence_Fast_GET_SIZE(sequence.get()) != 3) { - return false; - } - return ( - Python::CanGetPyDouble(PySequence_Fast_GET_ITEM(sequence.get(), 0)) - && Python::CanGetPyDouble(PySequence_Fast_GET_ITEM(sequence.get(), 1)) - && Python::CanGetPyDouble(PySequence_Fast_GET_ITEM(sequence.get(), 2))); -} - -auto Python::GetPyVector3f(PyObject* o) -> Vector3f { - assert(HaveGIL()); - BA_PRECONDITION_FATAL(o != nullptr); - - if (PythonClassVec3::Check(o)) { - return (reinterpret_cast(o))->value; - } - if (!PySequence_Check(o)) { - throw Exception("Object is not a ba.Vec3 or sequence.", PyExcType::kType); - } - PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); - assert(sequence.exists()); // Should always work; we checked seq. - if (PySequence_Fast_GET_SIZE(sequence.get()) != 3) { - throw Exception("Sequence is not of size 3.", PyExcType::kValue); - } - return {Python::GetPyFloat(PySequence_Fast_GET_ITEM(sequence.get(), 0)), - Python::GetPyFloat(PySequence_Fast_GET_ITEM(sequence.get(), 1)), - Python::GetPyFloat(PySequence_Fast_GET_ITEM(sequence.get(), 2))}; -} - -Python::Python() = default; - -auto Python::Create() -> Python* { - assert(InMainThread()); - - Python* python = new Python(); - python->InitCorePython(); - - // After we bootstrap Python here in the main thread we release the GIL. - // We'll explicitly reacquire it anytime we need it (mainly in the logic - // thread once that comes up later). - PyEval_SaveThread(); - return python; -} - -static struct PyModuleDef ba_module_def = {PyModuleDef_HEAD_INIT}; - -static auto ba_exec(PyObject* module) -> int { - Python::InitModuleClasses(module); - return 0; -} - -static PyModuleDef_Slot ba_slots[] = { - {Py_mod_exec, reinterpret_cast(ba_exec)}, {0, NULL}}; - -// Called when our _ba module is getting spun up. -static auto PyInit__ba() -> PyObject* { - assert(Python::HaveGIL()); - - // We should be able to assign these in the initializer above, - // but older g++ chokes on it at the moment... - // (and this is still more readable than setting ALL values positionally) - assert(ba_module_def.m_size == 0); // should all be zeroed though... - - // Gather our methods into a static null-terminated list. - auto* all_methods = new std::vector{Python::GetModuleMethods()}; - all_methods->push_back(PyMethodDef{nullptr, nullptr, 0, nullptr}); - - ba_module_def.m_methods = all_methods->data(); - ba_module_def.m_slots = ba_slots; - - PyObject* module = PyModuleDef_Init(&ba_module_def); - BA_PRECONDITION(module); - - return module; -} - -auto Python::InitCorePython() -> void { - assert(!inited_); - assert(InMainThread()); - // Flip on some extra runtime debugging options in debug builds. - // https://docs.python.org/3/library/devmode.html#devmode - int dev_mode{g_buildconfig.debug_build()}; - - // Pre-config as isolated if we include our own Python and as standard - // otherwise. - PyPreConfig preconfig; - if (g_platform->ContainsPythonDist()) { - PyPreConfig_InitIsolatedConfig(&preconfig); - } else { - PyPreConfig_InitPythonConfig(&preconfig); - } - preconfig.dev_mode = dev_mode; - - // We want consistent utf-8 everywhere (Python used to default to - // windows-specific file encodings, etc.) - preconfig.utf8_mode = 1; - - PyStatus status = Py_PreInitialize(&preconfig); - BA_PRECONDITION(!PyStatus_Exception(status)); - - // Configure as isolated if we include our own Python and as standard - // otherwise. - PyConfig config; - if (g_platform->ContainsPythonDist()) { - PyConfig_InitIsolatedConfig(&config); - } else { - PyConfig_InitPythonConfig(&config); - } - config.dev_mode = dev_mode; - if (!g_buildconfig.debug_build()) { - config.optimization_level = 1; - } - - // In cases where we bundle Python, set up all paths explicitly. - // https://docs.python.org/3/c-api/init_config.html#path-configuration - if (g_platform->ContainsPythonDist()) { - PyConfig_SetBytesString(&config, &config.base_exec_prefix, ""); - PyConfig_SetBytesString(&config, &config.base_executable, ""); - PyConfig_SetBytesString(&config, &config.base_prefix, ""); - PyConfig_SetBytesString(&config, &config.exec_prefix, ""); - PyConfig_SetBytesString(&config, &config.executable, ""); - PyConfig_SetBytesString(&config, &config.prefix, ""); - - // Interesting note: it seems we can pass relative paths here but - // they wind up in sys.path as absolute paths (unlike entries we add - // to sys.path after things are up and running). - if (g_buildconfig.ostype_windows()) { - // Windows Python looks for Lib and DLLs dirs by default, along with - // some others, but we want to be more explicit in limiting to these. It - // also seems that windows Python's paths can be incorrect if we're in - // strange dirs such as \\wsl$\Ubuntu-18.04\ that we get with WSL build - // setups. - - // NOTE: Python for windows actually comes with 'Lib', not 'lib', but - // it seems the interpreter defaults point to ./lib (as of 3.8.5). - // Normally this doesn't matter since windows is case-insensitive but - // under WSL it does. - // So we currently bundle the dir as 'lib' and use that in our path so - // that everything is happy (both with us and with python.exe). - PyWideStringList_Append(&config.module_search_paths, - Py_DecodeLocale("lib", nullptr)); - PyWideStringList_Append(&config.module_search_paths, - Py_DecodeLocale("DLLs", nullptr)); - } else { - PyWideStringList_Append(&config.module_search_paths, - Py_DecodeLocale("pylib", nullptr)); - } - config.module_search_paths_set = 1; - } - - // Let Python know how to spin up our _ba module. - PyImport_AppendInittab("_ba", &PyInit__ba); - - // Let Python know how to spin up the _bainternal module. - g_app_internal->DefineInternalModule(); - - // Init Python. - status = Py_InitializeFromConfig(&config); - BA_PRECONDITION(!PyStatus_Exception(status)); - - // Create a dict for execing just this bootstrap code in so - // we don't pollute the __main__ namespace. - auto bootstrap_context{PythonRef(PyDict_New(), PythonRef::kSteal)}; - - // Do a bit of basic bootstrapping here in the main thread. -#include "ballistica/generated/python_embedded/bootstrap_monolithic.inc" - PyObject* result = - PyRun_String(bootstrap_monolithic_code, Py_file_input, - bootstrap_context.get(), bootstrap_context.get()); - if (result == nullptr) { - PyErr_PrintEx(0); - - // Throw a simple exception so we don't get a stack trace. - throw std::logic_error( - "Error in ba Python bootstrapping. See log for details."); - } - Py_DECREF(result); - - // Grab __main__ in case we need to use it later. - // FIXME: put this in objs_ with everything else. - PyObject* m; - BA_PRECONDITION(m = PyImport_AddModule("__main__")); - BA_PRECONDITION(main_dict_ = PyModule_GetDict(m)); - - // Make sure we're running the Python version we require. - const char* ver = Py_GetVersion(); - if (strncmp(ver, "3.10", 4) != 0) { - FatalError("We require Python 3.10.x; instead found " + std::string(ver)); - } -} - -auto Python::InitBallisticaPython() -> void { - assert(InLogicThread()); - - // Create a dict for execing just this bootstrap code in so - // we don't pollute the __main__ namespace. - auto bootstrap_context{PythonRef(PyDict_New(), PythonRef::kSteal)}; - - // Get the app up and running. - // Run a few core bootstrappy things first: - // - get stdout/stderr redirection up so we can intercept python output - // - add our user and system script dirs to python path - // - create the ba.app instance. - -#include "ballistica/generated/python_embedded/bootstrap.inc" - PyObject* result = - PyRun_String(bootstrap_code, Py_file_input, bootstrap_context.get(), - bootstrap_context.get()); - if (result == nullptr) { - PyErr_PrintEx(0); - - // Throw a simple exception so we don't get a stack trace. - throw std::logic_error( - "Error in ba Python bootstrapping. See log for details."); - } - Py_DECREF(result); - - // Import and grab all the Python stuff we use from C++. -#include "ballistica/generated/python_embedded/binding.inc" - - // If we've got any early log calls, it is now safe to push them along - // to Python. - assert(objexists(ObjID::kLoggingCriticalCall)); - { - std::scoped_lock lock(early_log_lock_); - for (auto&& entry : early_logs_) { - LoggingCall(entry.first, "DEFERRED-EARLY-LOG: " + entry.second); - } - early_logs_.clear(); - } - g_app_internal->PythonPostInit(); - - // Alright I guess let's pull ba in to main, since pretty - // much all interactive commands will be using it. - // If we ever build the game as a pure python module we should - // of course not do this. - BA_PRECONDITION(PyRun_SimpleString("import ba") == 0); - - // Read the config file and store the config dict for easy access. - obj(ObjID::kReadConfigCall).Call(); - StoreObj(ObjID::kConfig, obj(ObjID::kApp).GetAttr("config").get()); - assert(PyDict_Check(obj(ObjID::kConfig).get())); - - // Turn off fancy-pants cyclic garbage-collection. - // We run it only at explicit times to avoid random hitches and keep - // things more deterministic. - // Non-reference-looped objects will still get cleaned up - // immediately, so we should try to structure things to avoid - // reference loops (just like Swift, ObjC, etc). - // FIXME - move this to Python code. - g_python->obj(Python::ObjID::kGCDisableCall).Call(); - inited_ = true; -} - -auto Python::Reset() -> void { - assert(InLogicThread()); - assert(g_python); - assert(g_platform); - assert(inited_); - - ReleaseGamePadInput(); - ReleaseKeyboardInput(); - g_graphics->ReleaseFadeEndCommand(); -} - -auto Python::GetModuleMethods() -> std::vector { - std::vector all_methods; - for (auto&& methods : { - PythonMethodsNetworking::GetMethods(), - PythonMethodsUI::GetMethods(), - PythonMethodsInput::GetMethods(), - PythonMethodsApp::GetMethods(), - PythonMethodsGameplay::GetMethods(), - PythonMethodsGraphics::GetMethods(), - PythonMethodsMedia::GetMethods(), - PythonMethodsSystem::GetMethods(), - }) { - all_methods.insert(all_methods.end(), methods.begin(), methods.end()); - } - return all_methods; -} - -template -auto AddClass(PyObject* module) -> PyObject* { - T::SetupType(&T::type_obj); - BA_PRECONDITION(PyType_Ready(&T::type_obj) == 0); - Py_INCREF(&T::type_obj); - int r = PyModule_AddObject(module, T::type_name(), - reinterpret_cast(&T::type_obj)); - BA_PRECONDITION(r == 0); - return reinterpret_cast(&T::type_obj); -} - -auto Python::InitModuleClasses(PyObject* module) -> void { - // Init our classes and add them to our module. - AddClass(module); - AddClass(module); - AddClass(module); - AddClass(module); - AddClass(module); - AddClass(module); - AddClass(module); - AddClass(module); - AddClass(module); - AddClass(module); - AddClass(module); - AddClass(module); - AddClass(module); - AddClass(module); - AddClass(module); - PyObject* vec3 = AddClass(module); - - // Register our vec3 as an abc.Sequence - auto register_call = - PythonRef(PyImport_ImportModule("collections.abc"), PythonRef::kSteal) - .GetAttr("Sequence") - .GetAttr("register"); - PythonRef args(Py_BuildValue("(O)", vec3), PythonRef::kSteal); - BA_PRECONDITION(register_call.Call(args).exists()); -} - -void Python::PushObjCall(ObjID obj_id) { - g_logic->thread()->PushCall([obj_id] { - ScopedSetContext cp(g_logic->GetUIContext()); - g_python->obj(obj_id).Call(); - }); -} - -void Python::PushObjCall(ObjID obj_id, const std::string& arg) { - g_logic->thread()->PushCall([this, obj_id, arg] { - ScopedSetContext cp(g_logic->GetUIContext()); - PythonRef args(Py_BuildValue("(s)", arg.c_str()), - ballistica::PythonRef::kSteal); - obj(obj_id).Call(args); - }); -} - -auto Python::GetResource(const char* key, const char* fallback_resource, - const char* fallback_value) -> std::string { - assert(HaveGIL()); - PythonRef results; - BA_PRECONDITION(key != nullptr); - const PythonRef& get_resource_call(obj(ObjID::kGetResourceCall)); - if (fallback_value != nullptr) { - if (fallback_resource == nullptr) { - BA_PRECONDITION(key != nullptr); - PythonRef args(Py_BuildValue("(sOs)", key, Py_None, fallback_value), - PythonRef::kSteal); - - // Don't print errors. - results = get_resource_call.Call(args, PythonRef(), false); - } else { - PythonRef args( - Py_BuildValue("(sss)", key, fallback_resource, fallback_value), - PythonRef::kSteal); - - // Don't print errors. - results = get_resource_call.Call(args, PythonRef(), false); - } - } else if (fallback_resource != nullptr) { - PythonRef args(Py_BuildValue("(ss)", key, fallback_resource), - PythonRef::kSteal); - - // Don't print errors - results = get_resource_call.Call(args, PythonRef(), false); - } else { - PythonRef args(Py_BuildValue("(s)", key), PythonRef::kSteal); - - // Don't print errors. - results = get_resource_call.Call(args, PythonRef(), false); - } - if (results.exists()) { - try { - return GetPyString(results.get()); - } catch (const std::exception&) { - Log(LogLevel::kError, - "GetResource failed for '" + std::string(key) + "'"); - - // Hmm; I guess let's just return the key to help identify/fix the - // issue?.. - return std::string(""; - } - } else { - Log(LogLevel::kError, "GetResource failed for '" + std::string(key) + "'"); - } - - // Hmm; I guess let's just return the key to help identify/fix the issue?.. - return std::string(""; -} - -auto Python::GetTranslation(const char* category, const char* s) - -> std::string { - assert(HaveGIL()); - PythonRef results; - PythonRef args(Py_BuildValue("(ss)", category, s), PythonRef::kSteal); - // Don't print errors. - results = obj(ObjID::kTranslateCall).Call(args, PythonRef(), false); - if (results.exists()) { - try { - return GetPyString(results.get()); - } catch (const std::exception&) { - Log(LogLevel::kError, - "GetTranslation failed for '" + std::string(category) + "'"); - return ""; - } - } else { - Log(LogLevel::kError, - "GetTranslation failed for category '" + std::string(category) + "'"); - } - return ""; -} - -void Python::RunDeepLink(const std::string& url) { - assert(InLogicThread()); - if (objexists(ObjID::kDeepLinkCall)) { - ScopedSetContext cp(g_logic->GetUIContext()); - PythonRef args(Py_BuildValue("(s)", url.c_str()), PythonRef::kSteal); - obj(ObjID::kDeepLinkCall).Call(args); - } else { - Log(LogLevel::kError, "Error on deep-link call"); - } -} - -void Python::PlayMusic(const std::string& music_type, bool continuous) { - assert(InLogicThread()); - if (music_type.empty()) { - PythonRef args( - Py_BuildValue("(OO)", Py_None, continuous ? Py_True : Py_False), - PythonRef::kSteal); - obj(ObjID::kDoPlayMusicCall).Call(args); - } else { - PythonRef args(Py_BuildValue("(sO)", music_type.c_str(), - continuous ? Py_True : Py_False), - PythonRef::kSteal); - obj(ObjID::kDoPlayMusicCall).Call(args); - } -} - -void Python::ShowURL(const std::string& url) { - if (objexists(ObjID::kShowURLWindowCall)) { - ScopedSetContext cp(g_logic->GetUIContext()); - PythonRef args(Py_BuildValue("(s)", url.c_str()), PythonRef::kSteal); - obj(ObjID::kShowURLWindowCall).Call(args); - } else { - Log(LogLevel::kError, "ShowURLWindowCall nonexistent."); - } -} - -auto Python::StringList(const std::list& values) -> PythonRef { - assert(HaveGIL()); - PythonRef pylist{PyList_New(values.size()), PythonRef::kSteal}; - int i{}; - for (auto&& value : values) { - PyObject* item{PyUnicode_FromString(value.c_str())}; - assert(item); - PyList_SET_ITEM(pylist.get(), i, item); - ++i; - } - return pylist; -} - -auto Python::SingleMemberTuple(const PythonRef& member) -> PythonRef { - assert(HaveGIL()); - return PythonRef(Py_BuildValue("(O)", member.NewRef()), PythonRef::kSteal); -} - -auto Python::FilterChatMessage(std::string* message, int client_id) -> bool { - assert(message); - ScopedSetContext cp(g_logic->GetUIContext()); - PythonRef args(Py_BuildValue("(si)", message->c_str(), client_id), - PythonRef::kSteal); - PythonRef result = obj(ObjID::kFilterChatMessageCall).Call(args); - - // If something went wrong, just allow all messages through verbatim. - if (!result.exists()) { - return true; - } - - // If they returned None, they want to ignore the message. - if (result.get() == Py_None) { - return false; - } - - // Replace the message string with whatever they gave us. - try { - *message = Python::GetPyString(result.get()); - } catch (const std::exception& e) { - Log(LogLevel::kError, - "Error getting string from chat filter: " + std::string(e.what())); - } - return true; -} - -void Python::HandleLocalChatMessage(const std::string& message) { - ScopedSetContext cp(g_logic->GetUIContext()); - PythonRef args(Py_BuildValue("(s)", message.c_str()), PythonRef::kSteal); - obj(ObjID::kHandleLocalChatMessageCall).Call(args); -} - -// Put together a node message with all args on the provided tuple (starting -// with arg_offset) returns false on failure, true on success. -void Python::DoBuildNodeMessage(PyObject* args, int arg_offset, Buffer* b, - PyObject** user_message_obj) { - Py_ssize_t tuple_size = PyTuple_GET_SIZE(args); - if (tuple_size - arg_offset < 1) { - throw Exception("Got message of size zero.", PyExcType::kValue); - } - std::string type; - PyObject* obj; - - // Pull first arg. - obj = PyTuple_GET_ITEM(args, arg_offset); - BA_PRECONDITION(obj); - if (!PyUnicode_Check(obj)) { - // If first arg is not a string, its an actual message itself. - (*user_message_obj) = obj; - return; - } else { - (*user_message_obj) = nullptr; - } - type = Python::GetPyString(obj); - NodeMessageType ac = Scene::GetNodeMessageType(type); - const char* format = Scene::GetNodeMessageFormat(ac); - assert(format); - const char* f = format; - - // Allow space for 1 type byte (fixme - may need more than 1). - size_t full_size = 1; - for (Py_ssize_t i = arg_offset + 1; i < tuple_size; i++) { - // Make sure our format string ends the same time as our arg count. - if (*f == 0) { - throw Exception( - "Wrong number of arguments on node message '" + type + "'.", - PyExcType::kValue); - } - obj = PyTuple_GET_ITEM(args, i); - BA_PRECONDITION(obj); - switch (*f) { - case 'I': - - // 4 byte int - if (!PyNumber_Check(obj)) { - throw Exception("Expected an int for node message arg " - + std::to_string(i - (arg_offset + 1)) + ".", - PyExcType::kType); - } - full_size += 4; - break; - case 'i': - - // 2 byte int. - if (!PyNumber_Check(obj)) { - throw Exception("Expected an int for node message arg " - + std::to_string(i - (arg_offset + 1)) + ".", - PyExcType::kType); - } - full_size += 2; - break; - case 'c': // NOLINT(bugprone-branch-clone) - - // 1 byte int. - if (!PyNumber_Check(obj)) { - throw Exception("Expected an int for node message arg " - + std::to_string(i - (arg_offset + 1)) + ".", - PyExcType::kType); - } - full_size += 1; - break; - case 'b': - - // bool (currently 1 byte int). - if (!PyNumber_Check(obj)) { - throw Exception("Expected an int for node message arg " - + std::to_string(i - (arg_offset + 1)) + ".", - PyExcType::kType); - } - full_size += 1; - break; - case 'F': - - // 32 bit float. - if (!PyNumber_Check(obj)) { - throw Exception("Expected a float for node message arg " - + std::to_string(i - (arg_offset + 1)) + ".", - PyExcType::kType); - } - full_size += 4; - break; - case 'f': - - // 16 bit float. - if (!PyNumber_Check(obj)) { - throw Exception("Expected a float for node message arg " - + std::to_string(i - (arg_offset + 1)) + ".", - PyExcType::kType); - } - full_size += 2; - break; - case 's': - if (!PyUnicode_Check(obj)) { - throw Exception("Expected a string for node message arg " - + std::to_string(i - (arg_offset + 1)) + ".", - PyExcType::kType); - } - full_size += strlen(PyUnicode_AsUTF8(obj)) + 1; - break; - default: - throw Exception("Invalid argument type: " + std::to_string(*f) + ".", - PyExcType::kValue); - break; - } - f++; - } - - // Make sure our format string ends the same time as our arg count. - if (*f != 0) { - throw Exception("Wrong number of arguments on node message '" + type + "'.", - PyExcType::kValue); - } - (*b).Resize(full_size); - char* ptr = (*b).data(); - *ptr = static_cast(ac); - ptr++; - f = format; - for (Py_ssize_t i = arg_offset + 1; i < tuple_size; i++) { - obj = PyTuple_GET_ITEM(args, i); - BA_PRECONDITION(obj); - switch (*f) { - case 'I': - Utils::EmbedInt32NBO( - &ptr, static_cast_check_fit(Python::GetPyInt64(obj))); - break; - case 'i': - Utils::EmbedInt16NBO( - &ptr, static_cast_check_fit(Python::GetPyInt64(obj))); - break; - case 'c': // NOLINT(bugprone-branch-clone) - Utils::EmbedInt8( - &ptr, static_cast_check_fit(Python::GetPyInt64(obj))); - break; - case 'b': - Utils::EmbedInt8( - &ptr, static_cast_check_fit(Python::GetPyInt64(obj))); - break; - case 'F': - Utils::EmbedFloat32(&ptr, Python::GetPyFloat(obj)); - break; - case 'f': - Utils::EmbedFloat16NBO(&ptr, Python::GetPyFloat(obj)); - break; - case 's': - Utils::EmbedString(&ptr, PyUnicode_AsUTF8(obj)); - break; - default: - throw Exception(PyExcType::kValue); - break; - } - f++; - } -} - -auto Python::GetPythonFileLocation(bool pretty) -> std::string { - PyFrameObject* f = PyEval_GetFrame(); - if (f) { - const char* path; - if (f->f_code && f->f_code->co_filename) { - assert(PyUnicode_Check(f->f_code->co_filename)); - path = PyUnicode_AsUTF8(f->f_code->co_filename); - if (pretty) { - if (path[0] == '<') { - // Filter stuff like :1 - return ""; - } else { - // Advance past any '/' and '\'s - while (true) { - const char* s = strchr(path, '/'); - if (s) { - path = s + 1; - } else { - const char* s2 = strchr(path, '\\'); - if (s2) { - path = s2 + 1; - } else { - break; - } - } - } - } - } - } else { - path = ""; - } - std::string name = - std::string(path) + ":" + std::to_string(PyFrame_GetLineNumber(f)); - return name; - } - return ""; -} - -void Python::SetNodeAttr(Node* node, const char* attr_name, - PyObject* value_obj) { - assert(node); - SceneStream* out_stream = node->scene()->GetSceneStream(); - NodeAttribute attr = node->GetAttribute(attr_name); - switch (attr.type()) { - case NodeAttributeType::kFloat: { - float val = Python::GetPyFloat(value_obj); - if (out_stream) { - out_stream->SetNodeAttr(attr, val); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(val); - break; - } - case NodeAttributeType::kInt: { - int64_t val = Python::GetPyInt64(value_obj); - if (out_stream) { - out_stream->SetNodeAttr(attr, val); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(val); - break; - } - case NodeAttributeType::kBool: { - bool val = Python::GetPyBool(value_obj); - if (out_stream) { - out_stream->SetNodeAttr(attr, val); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(val); - break; - } - case NodeAttributeType::kFloatArray: { - std::vector vals = Python::GetPyFloats(value_obj); - if (out_stream) { - out_stream->SetNodeAttr(attr, vals); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(vals); - break; - } - case NodeAttributeType::kIntArray: { - std::vector vals = Python::GetPyInts64(value_obj); - if (out_stream) { - out_stream->SetNodeAttr(attr, vals); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(vals); - break; - } - case NodeAttributeType::kString: { - std::string val = Python::GetPyString(value_obj); - if (out_stream) { - out_stream->SetNodeAttr(attr, val); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(val); - break; - } - case NodeAttributeType::kNode: { - // Allow dead-refs or None. - Node* val = Python::GetPyNode(value_obj, true, true); - if (out_stream) { - out_stream->SetNodeAttr(attr, val); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(val); - break; - } - case NodeAttributeType::kNodeArray: { - std::vector vals = Python::GetPyNodes(value_obj); - if (out_stream) { - out_stream->SetNodeAttr(attr, vals); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(vals); - break; - } - case NodeAttributeType::kPlayer: { - // Allow dead-refs and None. - Player* val = Python::GetPyPlayer(value_obj, true, true); - if (out_stream) { - out_stream->SetNodeAttr(attr, val); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(val); - break; - } - case NodeAttributeType::kMaterialArray: { - std::vector vals = Python::GetPyMaterials(value_obj); - if (out_stream) { - out_stream->SetNodeAttr(attr, vals); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(vals); - break; - } - case NodeAttributeType::kTexture: { - // Don't allow dead-refs, do allow None. - Texture* val = Python::GetPyTexture(value_obj, false, true); - if (out_stream) { - out_stream->SetNodeAttr(attr, val); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(val); - break; - } - case NodeAttributeType::kTextureArray: { - std::vector vals = Python::GetPyTextures(value_obj); - if (out_stream) { - out_stream->SetNodeAttr(attr, vals); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(vals); - break; - } - case NodeAttributeType::kSound: { - // Don't allow dead-refs, do allow None. - Sound* val = Python::GetPySound(value_obj, false, true); - if (out_stream) { - out_stream->SetNodeAttr(attr, val); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(val); - break; - } - case NodeAttributeType::kSoundArray: { - std::vector vals = Python::GetPySounds(value_obj); - if (out_stream) { - out_stream->SetNodeAttr(attr, vals); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(vals); - break; - } - case NodeAttributeType::kModel: { - // Don't allow dead-refs, do allow None. - Model* val = Python::GetPyModel(value_obj, false, true); - if (out_stream) { - out_stream->SetNodeAttr(attr, val); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(val); - break; - } - case NodeAttributeType::kModelArray: { - std::vector vals = Python::GetPyModels(value_obj); - if (out_stream) { - out_stream->SetNodeAttr(attr, vals); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(vals); - break; - } - case NodeAttributeType::kCollideModel: { - // Don't allow dead-refs, do allow None. - CollideModel* val = Python::GetPyCollideModel(value_obj, false, true); - if (out_stream) { - out_stream->SetNodeAttr(attr, val); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(val); - break; - } - case NodeAttributeType::kCollideModelArray: { - std::vector vals = Python::GetPyCollideModels(value_obj); - if (out_stream) { - out_stream->SetNodeAttr(attr, vals); - } - - // If something was driving this attr, disconnect it. - attr.DisconnectIncoming(); - attr.Set(vals); - break; - } - default: - throw Exception("FIXME: unhandled attr type in SetNodeAttr: '" - + attr.GetTypeName() + "'."); - } -} - -static auto CompareAttrIndices( - const std::pair& first, - const std::pair& second) -> bool { - return (first.first->index() < second.first->index()); -} - -auto Python::DoNewNode(PyObject* args, PyObject* keywds) -> Node* { - PyObject* delegate_obj = Py_None; - PyObject* owner_obj = Py_None; - PyObject* name_obj = Py_None; - static const char* kwlist[] = {"type", "owner", "attrs", - "name", "delegate", nullptr}; - char* type; - PyObject* dict = nullptr; - if (!PyArg_ParseTupleAndKeywords( - args, keywds, "s|OOOO", const_cast(kwlist), &type, &owner_obj, - &dict, &name_obj, &delegate_obj)) { - return nullptr; - } - - std::string name; - if (name_obj != Py_None) { - name = GetPyString(name_obj); - } else { - // By default do something like 'text@foo.py:20'. - name = std::string(type) + "@" + GetPythonFileLocation(); - } - - Scene* scene = Context::current().GetMutableScene(); - if (!scene) { - throw Exception("Can't create nodes in this context.", PyExcType::kContext); - } - - Node* node = scene->NewNode(type, name, delegate_obj); - - // Handle attr values fed in. - if (dict) { - if (!PyDict_Check(dict)) { - throw Exception("Expected dict for arg 2.", PyExcType::kType); - } - NodeType* t = node->type(); - PyObject* key{}; - PyObject* value{}; - Py_ssize_t pos{}; - - // We want to set initial attrs in order based on their attr indices. - std::list > attr_vals; - - // Grab all initial attr/values and add them to a list. - while (PyDict_Next(dict, &pos, &key, &value)) { - if (!PyUnicode_Check(key)) { - throw Exception("Expected string key in attr dict.", PyExcType::kType); - } - try { - attr_vals.emplace_back( - t->GetAttribute(std::string(PyUnicode_AsUTF8(key))), value); - } catch (const std::exception&) { - Log(LogLevel::kError, "Attr not found on initial attr set: '" - + std::string(PyUnicode_AsUTF8(key)) + "' on " - + type + " node '" + name + "'"); - } - } - - // Run the sets in the order of attr indices. - attr_vals.sort(CompareAttrIndices); - for (auto&& i : attr_vals) { - try { - SetNodeAttr(node, i.first->name().c_str(), i.second); - } catch (const std::exception& e) { - Log(LogLevel::kError, "Exception in initial attr set for attr '" - + i.first->name() + "' on " + type + " node '" - + name + "':" + e.what()); - } - } - } - - // If an owner was provided, set it up. - if (owner_obj != Py_None) { - // If its a node, set up a dependency at the scene level - // (then we just have to delete the owner node and the scene does the - // rest). - if (PythonClassNode::Check(owner_obj)) { - Node* owner_node = GetPyNode(owner_obj, true); - if (owner_node == nullptr) { - Log(LogLevel::kError, - "Empty node-ref passed for 'owner'; pass None if you want " - "no owner."); - } else if (owner_node->scene() != node->scene()) { - Log(LogLevel::kError, - "Owner node is from a different scene; ignoring."); - } else { - owner_node->AddDependentNode(node); - } - } else { - throw Exception( - "Invalid node owner: " + Python::ObjToString(owner_obj) + ".", - PyExcType::kType); - } - } - - // Lastly, call this node's OnCreate method for any final setup it may want to - // do. - try { - // Tell clients to do the same. - if (SceneStream* output_stream = scene->GetSceneStream()) { - output_stream->NodeOnCreate(node); - } - node->OnCreate(); - } catch (const std::exception& e) { - Log(LogLevel::kError, "Exception in OnCreate() for node " - + ballistica::ObjToString(node) - + "':" + e.what()); - } - - return node; -} - -// Return the node attr as a PyObject, or nullptr if the node doesn't have that -// attr. -auto Python::GetNodeAttr(Node* node, const char* attr_name) -> PyObject* { - assert(node); - NodeAttribute attr = node->GetAttribute(attr_name); - switch (attr.type()) { - case NodeAttributeType::kFloat: - return PyFloat_FromDouble(attr.GetAsFloat()); - break; - case NodeAttributeType::kInt: - return PyLong_FromLong( - static_cast_check_fit(attr.GetAsInt())); // NOLINT - break; - case NodeAttributeType::kBool: - if (attr.GetAsBool()) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } - break; - case NodeAttributeType::kString: { - if (g_buildconfig.debug_build()) { - std::string s = attr.GetAsString(); - assert(Utils::IsValidUTF8(s)); - return PyUnicode_FromString(s.c_str()); - } else { - return PyUnicode_FromString(attr.GetAsString().c_str()); - } - break; - } - case NodeAttributeType::kNode: { - // Return a new py ref to this node or create a new empty ref. - Node* n = attr.GetAsNode(); - return n ? n->NewPyRef() : PythonClassNode::Create(nullptr); - break; - } - case NodeAttributeType::kPlayer: { - // Player attrs deal with custom user ba.Player classes; - // not our internal SessionPlayer class. - Player* p = attr.GetAsPlayer(); - if (p == nullptr) { - Py_RETURN_NONE; - } - PyObject* gameplayer = p->GetPyActivityPlayer(); - Py_INCREF(gameplayer); - return gameplayer; - // return p ? p->NewPyRef() : PythonClassSessionPlayer::Create(nullptr); - break; - } - case NodeAttributeType::kFloatArray: { - std::vector vals = attr.GetAsFloats(); - Py_ssize_t size = vals.size(); - PyObject* vals_obj = PyTuple_New(size); - assert(vals_obj); - for (Py_ssize_t i = 0; i < size; i++) { - PyTuple_SET_ITEM(vals_obj, i, PyFloat_FromDouble(vals[i])); - } - return vals_obj; - break; - } - case NodeAttributeType::kIntArray: { - std::vector vals = attr.GetAsInts(); - Py_ssize_t size = vals.size(); - PyObject* vals_obj = PyTuple_New(size); - assert(vals_obj); - for (Py_ssize_t i = 0; i < size; i++) { - PyTuple_SET_ITEM(vals_obj, i, - PyLong_FromLong(static_cast_check_fit( // NOLINT - vals[i]))); - } - return vals_obj; - break; - } - case NodeAttributeType::kNodeArray: { - std::vector vals = attr.GetAsNodes(); - Py_ssize_t size = vals.size(); - PyObject* vals_obj = PyTuple_New(size); - assert(vals_obj); - for (Py_ssize_t i = 0; i < size; i++) { - Node* n = vals[i]; - PyTuple_SET_ITEM(vals_obj, i, - n ? n->NewPyRef() : PythonClassNode::Create(nullptr)); - } - return vals_obj; - break; - } - case NodeAttributeType::kTexture: { - Texture* t = attr.GetAsTexture(); - if (!t) { - Py_RETURN_NONE; - } - return t->NewPyRef(); - break; - } - case NodeAttributeType::kSound: { - Sound* s = attr.GetAsSound(); - if (!s) { - Py_RETURN_NONE; - } - return s->NewPyRef(); - break; - } - case NodeAttributeType::kModel: { - Model* m = attr.GetAsModel(); - if (!m) { - Py_RETURN_NONE; - } - return m->NewPyRef(); - break; - } - case NodeAttributeType::kCollideModel: { - CollideModel* c = attr.GetAsCollideModel(); - if (!c) { - Py_RETURN_NONE; - } - return c->NewPyRef(); - break; - } - case NodeAttributeType::kMaterialArray: { - std::vector vals = attr.GetAsMaterials(); - Py_ssize_t size = vals.size(); - PyObject* vals_obj = PyTuple_New(size); - assert(vals_obj); - for (Py_ssize_t i = 0; i < size; i++) { - Material* m = vals[i]; - - // Array attrs should never return nullptr materials. - assert(m); - PyTuple_SET_ITEM(vals_obj, i, m->NewPyRef()); - } - return vals_obj; - break; - } - case NodeAttributeType::kTextureArray: { - std::vector vals = attr.GetAsTextures(); - Py_ssize_t size = vals.size(); - PyObject* vals_obj = PyTuple_New(size); - assert(vals_obj); - for (Py_ssize_t i = 0; i < size; i++) { - Texture* t = vals[i]; - - // Array attrs should never return nullptr textures. - assert(t); - PyTuple_SET_ITEM(vals_obj, i, t->NewPyRef()); - } - return vals_obj; - break; - } - case NodeAttributeType::kSoundArray: { - std::vector vals = attr.GetAsSounds(); - Py_ssize_t size = vals.size(); - PyObject* vals_obj = PyTuple_New(size); - assert(vals_obj); - for (Py_ssize_t i = 0; i < size; i++) { - Sound* s = vals[i]; - - // Array attrs should never return nullptr sounds. - assert(s); - PyTuple_SET_ITEM(vals_obj, i, s->NewPyRef()); - } - return vals_obj; - break; - } - case NodeAttributeType::kModelArray: { - std::vector vals = attr.GetAsModels(); - Py_ssize_t size = vals.size(); - PyObject* vals_obj = PyTuple_New(size); - assert(vals_obj); - for (Py_ssize_t i = 0; i < size; i++) { - Model* m = vals[i]; - - // Array attrs should never return nullptr models. - assert(m); - PyTuple_SET_ITEM(vals_obj, i, m->NewPyRef()); - } - return vals_obj; - break; - } - case NodeAttributeType::kCollideModelArray: { - std::vector vals = attr.GetAsCollideModels(); - Py_ssize_t size = vals.size(); - PyObject* vals_obj = PyTuple_New(size); - assert(vals_obj); - for (Py_ssize_t i = 0; i < size; i++) { - CollideModel* c = vals[i]; - - // Array attrs should never return nullptr collide-models. - assert(c); - PyTuple_SET_ITEM(vals_obj, i, c->NewPyRef()); - } - return vals_obj; - break; - } - - default: - throw Exception("FIXME: unhandled attr type in GetNodeAttr: '" - + attr.GetTypeName() + "'."); - } - return nullptr; -} - -void Python::IssueCallInLogicThreadWarning(PyObject* call_obj) { - Log(LogLevel::kWarning, - "ba.pushcall() called from the logic thread with " - "from_other_thread set to true (call " - + ObjToString(call_obj) + " at " + GetPythonFileLocation() - + "). That arg should only be used from other threads."); -} - -void Python::LaunchStringEdit(TextWidget* w) { - assert(InLogicThread()); - BA_PRECONDITION(w); - - ScopedSetContext cp(g_logic->GetUIContext()); - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kSwish)); - - // Gotta run this in the next cycle. - PythonRef args(Py_BuildValue("(Osi)", w->BorrowPyRef(), - w->description().c_str(), w->max_chars()), - PythonRef::kSteal); - g_logic->PushPythonCallArgs( - Object::New(obj(ObjID::kOnScreenKeyboardClass).get()), - args); -} - -void Python::CaptureGamePadInput(PyObject* obj) { - assert(InLogicThread()); - ReleaseGamePadInput(); - if (PyCallable_Check(obj)) { - game_pad_call_.Acquire(obj); - } else { - throw Exception("Object is not callable.", PyExcType::kType); - } -} - -void Python::ReleaseGamePadInput() { game_pad_call_.Release(); } - -void Python::CaptureKeyboardInput(PyObject* obj) { - assert(InLogicThread()); - ReleaseKeyboardInput(); - if (PyCallable_Check(obj)) { - keyboard_call_.Acquire(obj); - } else { - throw Exception("Object is not callable.", PyExcType::kType); - } -} -void Python::ReleaseKeyboardInput() { keyboard_call_.Release(); } - -auto Python::HandleKeyPressEvent(const SDL_Keysym& keysym) -> bool { - assert(InLogicThread()); - if (!keyboard_call_.exists()) { - return false; - } - ScopedSetContext cp(g_logic->GetUIContextTarget()); - InputDevice* keyboard = g_input->keyboard_input(); - PythonRef args( - Py_BuildValue("({s:s,s:i,s:O})", "type", "BUTTONDOWN", "button", - static_cast(keysym.sym), "input_device", - keyboard ? keyboard->BorrowPyRef() : Py_None), - PythonRef::kSteal); - keyboard_call_.Call(args); - return true; -} -auto Python::HandleKeyReleaseEvent(const SDL_Keysym& keysym) -> bool { - assert(InLogicThread()); - if (!keyboard_call_.exists()) { - return false; - } - ScopedSetContext cp(g_logic->GetUIContextTarget()); - InputDevice* keyboard = g_input->keyboard_input(); - PythonRef args(Py_BuildValue("({s:s,s:i,s:O})", "type", "BUTTONUP", "button", - static_cast(keysym.sym), "input_device", - keyboard ? keyboard->BorrowPyRef() : Py_None), - PythonRef::kSteal); - keyboard_call_.Call(args); - return true; -} - -auto Python::HandleJoystickEvent(const SDL_Event& event, - InputDevice* input_device) -> bool { - assert(InLogicThread()); - assert(input_device != nullptr); - if (!game_pad_call_.exists()) { - return false; - } - ScopedSetContext cp(g_logic->GetUIContextTarget()); - InputDevice* device{}; - - device = input_device; - - // If we got a device we can pass events. - if (device) { - switch (event.type) { - case SDL_JOYBUTTONDOWN: { - PythonRef args( - Py_BuildValue( - "({s:s,s:i,s:O})", "type", "BUTTONDOWN", "button", - static_cast(event.jbutton.button) + 1, // give them base-1 - "input_device", device->BorrowPyRef()), - PythonRef::kSteal); - game_pad_call_.Call(args); - break; - } - case SDL_JOYBUTTONUP: { - PythonRef args( - Py_BuildValue( - "({s:s,s:i,s:O})", "type", "BUTTONUP", "button", - static_cast(event.jbutton.button) + 1, // give them base-1 - "input_device", device->BorrowPyRef()), - PythonRef::kSteal); - game_pad_call_.Call(args); - break; - } - case SDL_JOYHATMOTION: { - PythonRef args( - Py_BuildValue( - "({s:s,s:i,s:i,s:O})", "type", "HATMOTION", "hat", - static_cast(event.jhat.hat) + 1, // give them base-1 - "value", event.jhat.value, "input_device", - device->BorrowPyRef()), - PythonRef::kSteal); - game_pad_call_.Call(args); - break; - } - case SDL_JOYAXISMOTION: { - PythonRef args( - Py_BuildValue( - "({s:s,s:i,s:f,s:O})", "type", "AXISMOTION", "axis", - static_cast(event.jaxis.axis) + 1, // give them base-1 - "value", - std::min(1.0f, - std::max(-1.0f, static_cast(event.jaxis.value) - / 32767.0f)), - "input_device", device->BorrowPyRef()), - PythonRef::kSteal); - game_pad_call_.Call(args); - break; - } - default: - break; - } - } - return true; -} - -auto Python::GetContextBaseString() -> std::string { - std::string context_str; - std::string sim_time_string; - std::string base_time_string; - try { - sim_time_string = - std::to_string(Context::current().target->GetTime(TimeType::kSim)); - } catch (const std::exception&) { - sim_time_string = ""; - } - try { - base_time_string = - std::to_string(Context::current().target->GetTime(TimeType::kBase)); - } catch (const std::exception&) { - base_time_string = ""; - } - - if (Context::current().GetUIContext()) { - context_str = ""; - } else if (HostActivity* ha = Context::current().GetHostActivity()) { - // If its a HostActivity, print the Python obj. - PythonRef ha_obj(ha->GetPyActivity(), PythonRef::kAcquire); - if (ha_obj.get() != Py_None) { - context_str = ha_obj.Str(); - } else { - context_str = ha->GetObjectDescription(); - } - } else if (Context::current().target.exists()) { - context_str = Context::current().target->GetObjectDescription(); - } else { - context_str = ""; - } - std::string s = "\n context: " + context_str + "\n real-time: " - + std::to_string(GetRealTime()) + "\n sim-time: " - + sim_time_string + "\n base-time: " + base_time_string; - return s; -} - -void Python::PrintContextForCallableLabel(const char* label) { - assert(InLogicThread()); - assert(label); - std::string s = std::string(" root call: ") + label; - s += g_python->GetContextBaseString(); - PySys_WriteStderr("%s\n", s.c_str()); -} - -void Python::PrintContextNonLogicThread() { - std::string s = - std::string(" root call: "); - PySys_WriteStderr("%s\n", s.c_str()); -} - -void Python::PrintContextEmpty() { - assert(InLogicThread()); - std::string s = std::string(" root call: "); - s += g_python->GetContextBaseString(); - PySys_WriteStderr("%s\n", s.c_str()); -} - -void Python::PrintContextAuto() { - // Lets print whatever context info is available. - // FIXME: If we have recursive calls this may not print - // the context we'd expect; we'd need a unified stack. - if (!InLogicThread()) { - PrintContextNonLogicThread(); - } else if (const char* label = ScopedCallLabel::current_label()) { - PrintContextForCallableLabel(label); - } else if (PythonCommand* cmd = PythonCommand::current_command()) { - cmd->PrintContext(); - } else if (PythonContextCall* call = PythonContextCall::current_call()) { - call->PrintContext(); - } else { - PrintContextEmpty(); - } -} - -void Python::AcquireGIL() { - assert(InLogicThread()); - auto debug_timing{g_app->debug_timing}; - millisecs_t startms{debug_timing ? Platform::GetCurrentMilliseconds() : 0}; - - if (logic_thread_state_) { - PyEval_RestoreThread(logic_thread_state_); - logic_thread_state_ = nullptr; - } - - if (debug_timing) { - auto duration{Platform::GetCurrentMilliseconds() - startms}; - if (duration > (1000 / 120)) { - Log(LogLevel::kInfo, - "GIL acquire took too long (" + std::to_string(duration) + " ms)."); - } - } -} - -void Python::ReleaseGIL() { - assert(InLogicThread()); - assert(logic_thread_state_ == nullptr); - logic_thread_state_ = PyEval_SaveThread(); -} - -void Python::AddCleanFrameCommand(const Object::Ref& c) { - clean_frame_commands_.push_back(c); -} - -void Python::RunCleanFrameCommands() { - for (auto&& i : clean_frame_commands_) { - i->Run(); - } - clean_frame_commands_.clear(); -} - -auto Python::GetControllerValue(InputDevice* input_device, - const std::string& value_name) -> int { - assert(objexists(ObjID::kGetDeviceValueCall)); - PythonRef args( - Py_BuildValue("(Os)", input_device->BorrowPyRef(), value_name.c_str()), - PythonRef::kSteal); - PythonRef ret_val; - { - Python::ScopedCallLabel label("get_device_value"); - ret_val = obj(ObjID::kGetDeviceValueCall).Call(args); - } - if (!PyLong_Check(ret_val.get())) { - throw Exception("Non-int returned from get_device_value call.", - PyExcType::kType); - } - return static_cast(PyLong_AsLong(ret_val.get())); -} - -auto Python::GetControllerFloatValue(InputDevice* input_device, - const std::string& value_name) -> float { - assert(objexists(ObjID::kGetDeviceValueCall)); - PythonRef args( - Py_BuildValue("(Os)", input_device->BorrowPyRef(), value_name.c_str()), - PythonRef::kSteal); - PythonRef ret_val = obj(ObjID::kGetDeviceValueCall).Call(args); - if (!PyFloat_Check(ret_val.get())) { - if (PyLong_Check(ret_val.get())) { - return static_cast(PyLong_AsLong(ret_val.get())); - } else { - throw Exception( - "Non float/int returned from GetControllerFloatValue call.", - PyExcType::kType); - } - } - return static_cast(PyFloat_AsDouble(ret_val.get())); -} - -void Python::HandleDeviceMenuPress(InputDevice* input_device) { - assert(objexists(ObjID::kDeviceMenuPressCall)); - - // Ignore if input is locked... - if (g_input->IsInputLocked()) { - return; - } - ScopedSetContext cp(g_logic->GetUIContext()); - PythonRef args(Py_BuildValue("(O)", input_device ? input_device->BorrowPyRef() - : Py_None), - PythonRef::kSteal); - { - Python::ScopedCallLabel label("handleDeviceMenuPress"); - obj(ObjID::kDeviceMenuPressCall).Call(args); - } -} - -auto Python::GetLastPlayerNameFromInputDevice(InputDevice* device) - -> std::string { - assert(objexists(ObjID::kGetLastPlayerNameFromInputDeviceCall)); - PythonRef args(Py_BuildValue("(O)", device ? device->BorrowPyRef() : Py_None), - PythonRef::kSteal); - try { - return Python::GetPyString( - obj(ObjID::kGetLastPlayerNameFromInputDeviceCall).Call(args).get()); - } catch (const std::exception&) { - return ""; - } -} - -auto Python::ObjToString(PyObject* obj) -> std::string { - if (obj) { - return PythonRef(obj, PythonRef::kAcquire).Str(); - } else { - return ""; - } -} - -void Python::StoreObj(ObjID id, PyObject* pyobj, bool incref) { - assert(id < ObjID::kLast); - assert(pyobj); - if (g_buildconfig.debug_build()) { - // Assuming we're setting everything once - // (make sure we don't accidentally overwrite things we don't intend to). - if (objs_[static_cast(id)].exists()) { - throw Exception("Python::StoreObj() called twice for val '" - + std::to_string(static_cast(id)) + "'."); - } - - // Also make sure we're not storing an object that's already been stored. - for (auto&& i : objs_) { - if (i.get() != nullptr && i.get() == pyobj) { - throw Exception("Python::StoreObj() called twice for same ptr; id=" - + std::to_string(static_cast(id)) + "."); - } - } - } - if (incref) { - Py_INCREF(pyobj); - } - objs_[static_cast(id)].Steal(pyobj); -} - -void Python::StoreObjCallable(ObjID id, PyObject* pyobj, bool incref) { - StoreObj(id, pyobj, incref); - BA_PRECONDITION(obj(id).CallableCheck()); -} - -void Python::StoreObj(ObjID id, const char* expr, PyObject* context) { - PyObject* obj = - PythonCommand(expr, "").RunReturnObj(false, context); - if (obj == nullptr) { - throw Exception("Unable to get value: '" + std::string(expr) + "'."); - } - StoreObj(id, obj); -} - -void Python::StoreObjCallable(ObjID id, const char* expr, PyObject* context) { - PyObject* obj = - PythonCommand(expr, "").RunReturnObj(false, context); - if (obj == nullptr) { - throw Exception("Unable to get value: '" + std::string(expr) + "'."); - } - StoreObjCallable(id, obj); -} - -void Python::SetRawConfigValue(const char* name, float value) { - assert(InLogicThread()); - assert(objexists(ObjID::kConfig)); - PythonRef value_obj(PyFloat_FromDouble(value), PythonRef::kSteal); - int result = - PyDict_SetItemString(obj(ObjID::kConfig).get(), name, value_obj.get()); - if (result == -1) { - // Failed, we have. - // Clear any Python error that got us here; we're in C++ Exception land now. - PyErr_Clear(); - throw Exception("Error setting config dict value."); - } -} - -auto Python::GetRawConfigValue(const char* name) -> PyObject* { - assert(InLogicThread()); - assert(objexists(ObjID::kConfig)); - return PyDict_GetItemString(obj(ObjID::kConfig).get(), name); -} - -auto Python::GetRawConfigValue(const char* name, const char* default_value) - -> std::string { - assert(InLogicThread()); - assert(objexists(ObjID::kConfig)); - PyObject* value = PyDict_GetItemString(obj(ObjID::kConfig).get(), name); - if (value == nullptr || !PyUnicode_Check(value)) { - return default_value; - } - return PyUnicode_AsUTF8(value); -} - -auto Python::GetRawConfigValue(const char* name, float default_value) -> float { - assert(InLogicThread()); - assert(objexists(ObjID::kConfig)); - PyObject* value = PyDict_GetItemString(obj(ObjID::kConfig).get(), name); - if (value == nullptr) { - return default_value; - } - try { - return GetPyFloat(value); - } catch (const std::exception&) { - Log(LogLevel::kError, - "expected a float for config value '" + std::string(name) + "'"); - return default_value; - } -} - -auto Python::GetRawConfigValue(const char* name, - std::optional default_value) - -> std::optional { - assert(InLogicThread()); - assert(objexists(ObjID::kConfig)); - PyObject* value = PyDict_GetItemString(obj(ObjID::kConfig).get(), name); - if (value == nullptr) { - return default_value; - } - try { - if (value == Py_None) { - return std::optional(); - } - return GetPyFloat(value); - } catch (const std::exception&) { - Log(LogLevel::kError, - "expected a float for config value '" + std::string(name) + "'"); - return default_value; - } -} - -auto Python::GetRawConfigValue(const char* name, int default_value) -> int { - assert(InLogicThread()); - assert(objexists(ObjID::kConfig)); - PyObject* value = PyDict_GetItemString(obj(ObjID::kConfig).get(), name); - if (value == nullptr) { - return default_value; - } - try { - return static_cast_check_fit(GetPyInt64(value)); - } catch (const std::exception&) { - Log(LogLevel::kError, - "Expected an int value for config value '" + std::string(name) + "'."); - return default_value; - } -} - -auto Python::GetRawConfigValue(const char* name, bool default_value) -> bool { - assert(InLogicThread()); - assert(objexists(ObjID::kConfig)); - PyObject* value = PyDict_GetItemString(obj(ObjID::kConfig).get(), name); - if (value == nullptr) { - return default_value; - } - try { - return GetPyBool(value); - } catch (const std::exception&) { - Log(LogLevel::kError, - "Expected a bool value for config value '" + std::string(name) + "'."); - return default_value; - } -} - -auto Python::DoOnce() -> bool { - std::string location = GetPythonFileLocation(false); - if (do_once_locations_.find(location) != do_once_locations_.end()) { - return false; - } - do_once_locations_.insert(location); - return true; -} - -void Python::TimeFormatCheck(TimeFormat time_format, PyObject* length_obj) { - std::string warn_msg; - double length = Python::GetPyDouble(length_obj); - if (time_format == TimeFormat::kSeconds) { - // If we get a value more than a few hundred seconds, they might - // have meant milliseconds. - if (length >= 200.0) { - static bool warned = false; - if (!warned) { - Log(LogLevel::kWarning, "Time value " - +std::to_string(length)+" passed as seconds;" - " did you mean milliseconds?" - " (if so, pass suppress_format_warning=True to stop this warning)"); - PrintStackTrace(); - warned = true; - } - } - } else if (time_format == TimeFormat::kMilliseconds) { - // If we get a value less than 1 millisecond, they might have meant - // seconds. (also ignore 0 which could be valid) - if (length < 1.0 && length > 0.0000001) { - static bool warned = false; - if (!warned) { - Log(LogLevel::kWarning, "Time value " - + std::to_string(length) + " passed as milliseconds;" - " did you mean seconds?" - " (if so, pass suppress_format_warning=True to stop this warning)"); - PrintStackTrace(); - warned = true; - } - } - } else { - static bool warned = false; - if (!warned) { - BA_LOG_ONCE(LogLevel::kError, - "TimeFormatCheck got timeformat value: '" - + std::to_string(static_cast(time_format)) + "'"); - warned = true; - } - } -} - -auto Python::ValidatedPackageAssetName(PyObject* package, const char* name) - -> std::string { - assert(InLogicThread()); - assert(objexists(ObjID::kAssetPackageClass)); - - if (!PyObject_IsInstance(package, obj(ObjID::kAssetPackageClass).get())) { - throw Exception("Object is not an AssetPackage.", PyExcType::kType); - } - - // Ok; they've passed us an asset-package object. - // Now validate that its context is current... - PythonRef context_obj(PyObject_GetAttrString(package, "context"), - PythonRef::kSteal); - if (!context_obj.exists() - || !(PyObject_IsInstance( - context_obj.get(), - reinterpret_cast(&PythonClassContext::type_obj)))) { - throw Exception("Asset package context not found.", PyExcType::kNotFound); - } - auto* pycontext = reinterpret_cast(context_obj.get()); - Object::WeakRef ctargetref = pycontext->context().target; - if (!ctargetref.exists()) { - throw Exception("Asset package context does not exist.", - PyExcType::kNotFound); - } - Object::WeakRef ctargetref2 = Context::current().target; - if (ctargetref.get() != ctargetref2.get()) { - throw Exception("Asset package context is not current."); - } - - // Hooray; the asset package's context exists and is current. - // Ok; now pull the package id... - PythonRef package_id(PyObject_GetAttrString(package, "package_id"), - PythonRef::kSteal); - if (!PyUnicode_Check(package_id.get())) { - throw Exception("Got non-string AssetPackage ID.", PyExcType::kType); - } - - // TODO(ericf): make sure the package is valid for this context, - // and return a fully qualified name with the package included. - - printf("would give %s:%s\n", PyUnicode_AsUTF8(package_id.get()), name); - return name; -} - -class Python::ScopedInterpreterLock::Impl { - public: - Impl() { - if (need_lock_) { - // Grab the python GIL. - gstate_ = PyGILState_Ensure(); - } - } - ~Impl() { - if (need_lock_) { - // Release the python GIL. - PyGILState_Release(gstate_); - } - } - - private: - bool need_lock_{true}; - PyGILState_STATE gstate_{PyGILState_UNLOCKED}; -}; - -Python::ScopedInterpreterLock::ScopedInterpreterLock() - : impl_{new Python::ScopedInterpreterLock::Impl()} -// impl_{std::make_unique()} -{} - -Python::ScopedInterpreterLock::~ScopedInterpreterLock() { delete impl_; } - -template -auto IsPyEnum(Python::ObjID enum_class_id, PyObject* obj) -> bool { - PyObject* enum_class_obj = g_python->obj(enum_class_id).get(); - assert(enum_class_obj != nullptr && enum_class_obj != Py_None); - return static_cast(PyObject_IsInstance(obj, enum_class_obj)); -} - -template -auto GetPyEnum(Python::ObjID enum_class_id, PyObject* obj) -> T { - // First, make sure what they passed is an instance of the enum class - // we want. - PyObject* enum_class_obj = g_python->obj(enum_class_id).get(); - assert(enum_class_obj != nullptr && enum_class_obj != Py_None); - if (!PyObject_IsInstance(obj, enum_class_obj)) { - throw Exception(Python::ObjToString(obj) + " is not an instance of " - + Python::ObjToString(enum_class_obj) + ".", - PyExcType::kType); - } - - // Now get its value as an int and make sure its in range - // (based on its kLast member in C++ land). - PythonRef value_obj(PyObject_GetAttrString(obj, "value"), PythonRef::kSteal); - if (!value_obj.exists() || !PyLong_Check(value_obj.get())) { - throw Exception( - Python::ObjToString(obj) + " is not a valid int-valued enum.", - PyExcType::kType); - } - auto value = PyLong_AS_LONG(value_obj.get()); - if (value < 0 || value >= static_cast(T::kLast)) { - throw Exception( - Python::ObjToString(obj) + " is an invalid out-of-range enum value.", - PyExcType::kValue); - } - return static_cast(value); -} - -// Explicitly instantiate the few variations we use. -// (so we can avoid putting the full function in the header) -// template TimeFormat Python::GetPyEnum(Python::ObjID enum_class, PyObject* -// obj); template TimeType Python::GetPyEnum(Python::ObjID enum_class, PyObject* -// obj); template SpecialChar Python::GetPyEnum(Python::ObjID enum_class, -// PyObject* obj); template Permission Python::GetPyEnum(Python::ObjID -// enum_class, PyObject* obj); - -auto Python::GetPyEnum_Permission(PyObject* obj) -> Permission { - return GetPyEnum(Python::ObjID::kPermissionClass, obj); -} - -auto Python::GetPyEnum_SpecialChar(PyObject* obj) -> SpecialChar { - return GetPyEnum(Python::ObjID::kSpecialCharClass, obj); -} - -auto Python::GetPyEnum_TimeType(PyObject* obj) -> TimeType { - return GetPyEnum(Python::ObjID::kTimeTypeClass, obj); -} - -auto Python::GetPyEnum_TimeFormat(PyObject* obj) -> TimeFormat { - return GetPyEnum(Python::ObjID::kTimeFormatClass, obj); -} - -auto Python::IsPyEnum_InputType(PyObject* obj) -> bool { - return IsPyEnum(Python::ObjID::kInputTypeClass, obj); -} - -auto Python::GetPyEnum_InputType(PyObject* obj) -> InputType { - return GetPyEnum(Python::ObjID::kInputTypeClass, obj); -} - -// (some stuff borrowed from python's source code - used in our overriding of -// objects' dir() results) - -/* alphabetical order */ -_Py_IDENTIFIER(__class__); -_Py_IDENTIFIER(__dict__); - -/* ------------------------- PyObject_Dir() helpers ------------------------- */ - -/* - Merge the __dict__ of aclass into dict, and recursively also all - the __dict__s of aclass's base classes. The order of merging isn't - defined, as it's expected that only the final set of dict keys is - interesting. - Return 0 on success, -1 on error. - */ - -static auto merge_class_dict(PyObject* dict, PyObject* aclass) -> int { - PyObject* classdict; - PyObject* bases; - _Py_IDENTIFIER(__bases__); - - assert(PyDict_Check(dict)); - assert(aclass); - - /* Merge in the type's dict (if any). */ - classdict = _PyObject_GetAttrId(aclass, &PyId___dict__); - if (classdict == nullptr) { - PyErr_Clear(); - } else { - int status = PyDict_Update(dict, classdict); - Py_DECREF(classdict); - if (status < 0) return -1; - } - - /* Recursively merge in the base types' (if any) dicts. */ - bases = _PyObject_GetAttrId(aclass, &PyId___bases__); - if (bases == nullptr) { - PyErr_Clear(); - } else { - /* We have no guarantee that bases is a real tuple */ - Py_ssize_t i; - Py_ssize_t n; - n = PySequence_Size(bases); /* This better be right */ - if (n < 0) { - PyErr_Clear(); - } else { - for (i = 0; i < n; i++) { - int status; - PyObject* base = PySequence_GetItem(bases, i); - if (base == nullptr) { - Py_DECREF(bases); - return -1; - } - status = merge_class_dict(dict, base); - Py_DECREF(base); - if (status < 0) { - Py_DECREF(bases); - return -1; - } - } - } - Py_DECREF(bases); - } - return 0; -} - -/* __dir__ for generic objects: returns __dict__, __class__, - and recursively up the __class__.__bases__ chain. - */ -auto Python::generic_dir(PyObject* self) -> PyObject* { - PyObject* result = nullptr; - PyObject* dict = nullptr; - PyObject* itsclass = nullptr; - - /* Get __dict__ (which may or may not be a real dict...) */ - dict = _PyObject_GetAttrId(self, &PyId___dict__); - if (dict == nullptr) { - PyErr_Clear(); - dict = PyDict_New(); - } else if (!PyDict_Check(dict)) { - Py_DECREF(dict); - dict = PyDict_New(); - } else { - /* Copy __dict__ to avoid mutating it. */ - PyObject* temp = PyDict_Copy(dict); - Py_DECREF(dict); - dict = temp; - } - - if (dict == nullptr) goto error; - - /* Merge in attrs reachable from its class. */ - itsclass = _PyObject_GetAttrId(self, &PyId___class__); - if (itsclass == nullptr) - /* XXX(tomer): Perhaps fall back to obj->ob_type if no - __class__ exists? */ - PyErr_Clear(); - else if (merge_class_dict(dict, itsclass) != 0) - goto error; - - result = PyDict_Keys(dict); - /* fall through */ -error: - Py_XDECREF(itsclass); - Py_XDECREF(dict); - return result; -} -//////////////// end __dir__ helpers - -#pragma clang diagnostic pop - -} // namespace ballistica diff --git a/src/ballistica/python/python.h b/src/ballistica/python/python.h deleted file mode 100644 index 40a2abac..00000000 --- a/src/ballistica/python/python.h +++ /dev/null @@ -1,453 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_PYTHON_H_ -#define BALLISTICA_PYTHON_PYTHON_H_ - -#include -#include -#include -#include -#include -#include -#include - -#include "ballistica/ballistica.h" -#include "ballistica/core/context.h" -#include "ballistica/core/object.h" -#include "ballistica/generic/buffer.h" -#include "ballistica/generic/runnable.h" -#include "ballistica/math/point2d.h" -#include "ballistica/platform/min_sdl.h" -#include "ballistica/python/python_ref.h" - -namespace ballistica { - -/// General python support/infrastructure class. -class Python { - public: - /// When calling a python callable directly, you can use the following - /// to push and pop a text label which will be printed as 'call' in errors. - class ScopedCallLabel { - public: - explicit ScopedCallLabel(const char* label) { - prev_label_ = current_label_; - } - ~ScopedCallLabel() { current_label_ = prev_label_; } - static auto current_label() -> const char* { return current_label_; } - - private: - const char* prev_label_{}; - static const char* current_label_; - BA_DISALLOW_CLASS_COPIES(ScopedCallLabel); - }; - - /// Use this to protect Python code that may be run in cases where we don't - /// hold the Global Interpreter Lock (GIL) (basically anything outside of the - /// logic thread). - class ScopedInterpreterLock { - public: - ScopedInterpreterLock(); - ~ScopedInterpreterLock(); - - private: - class Impl; - // Note: should use unique_ptr for this, but build fails on raspberry pi - // (gcc 8.3.0). Works on Ubuntu 9.3 so should try again later. - // std::unique_ptr impl_{}; - Impl* impl_{}; - }; - - /// Return whether the current thread holds the global-interpreter-lock. - /// We must always hold the GIL while running python code. - /// This *should* generally be the case by default, but this can be handy for - /// sanity checking that. - static auto HaveGIL() -> bool; - - /// Attempt to print the python stack trace. - static auto PrintStackTrace() -> void; - - /// Pass any PyObject* (including nullptr) to get a readable string - /// (basically equivalent of str(foo)). - static auto ObjToString(PyObject* obj) -> std::string; - - /// Given an asset-package python object and a media name, verify - /// that the asset-package is valid in the current context and return - /// its fully qualified name if so. Throw an Exception if not. - auto ValidatedPackageAssetName(PyObject* package, const char* name) - -> std::string; - - /// Calls Python logging function (logging.error, logging.warning, etc.) - /// Can be called from any thread at any time. If called before Python - /// logging is available, logs locally using Logging::DisplayLog() - /// (with an added warning). - auto LoggingCall(LogLevel loglevel, const std::string& msg) -> void; - - // Print various context debugging bits to Python's sys.stderr. - static auto PrintContextForCallableLabel(const char* label) -> void; - static auto PrintContextEmpty() -> void; - static auto PrintContextAuto() -> void; - static auto PrintContextNonLogicThread() -> void; - Python(); - static auto Create() -> Python*; - - auto InitCorePython() -> void; - auto InitBallisticaPython() -> void; - auto Reset() -> void; - - /// Add classes to the newly created ba module. - static auto InitModuleClasses(PyObject* module) -> void; - static auto GetModuleMethods() -> std::vector; - - auto GetContextBaseString() -> std::string; - auto GetControllerValue(InputDevice* input_device, - const std::string& value_name) -> int; - auto GetControllerFloatValue(InputDevice* input_device, - const std::string& value_name) -> float; - auto HandleDeviceMenuPress(InputDevice* input_device) -> void; - auto GetLastPlayerNameFromInputDevice(InputDevice* input_device) - -> std::string; - auto AcquireGIL() -> void; - auto ReleaseGIL() -> void; - - auto LaunchStringEdit(TextWidget* w) -> void; - auto CaptureGamePadInput(PyObject* obj) -> void; - auto ReleaseGamePadInput() -> void; - auto CaptureKeyboardInput(PyObject* obj) -> void; - auto ReleaseKeyboardInput() -> void; - auto IssueCallInLogicThreadWarning(PyObject* call) -> void; - - /// Borrowed from python's source code: used in overriding of objects' dir() - /// results. - static auto generic_dir(PyObject* self) -> PyObject*; - - /// For use by g_logic in passing events along to the python layer (for - /// captured input, etc). - auto HandleJoystickEvent(const SDL_Event& event, - InputDevice* input_device = nullptr) -> bool; - auto HandleKeyPressEvent(const SDL_Keysym& keysym) -> bool; - auto HandleKeyReleaseEvent(const SDL_Keysym& keysym) -> bool; - - auto inited() const -> bool { return inited_; } - - /// Filter incoming chat message from client. - /// If returns false, message should be ignored. - auto FilterChatMessage(std::string* message, int client_id) -> bool; - - /// Pass a chat message along to the python UI layer for handling.. - auto HandleLocalChatMessage(const std::string& message) -> void; - - /// Pop up an in-game window to show a url (NOT in a browser). - auto ShowURL(const std::string& url) -> void; - - auto AddCleanFrameCommand(const Object::Ref& c) -> void; - auto RunCleanFrameCommands() -> void; - - /// Return a minimal filename/position string such as 'foo.py:201' based - /// on the Python stack state. This shouldn't be too expensive to fetch and - /// is useful as an object identifier/etc. - static auto GetPythonFileLocation(bool pretty = true) -> std::string; - - auto set_env_obj(PyObject* obj) -> void { env_ = obj; } - auto env_obj() const -> PyObject* { - assert(env_); - return env_; - } - auto main_dict() const -> PyObject* { - assert(main_dict_); - return main_dict_; - } - auto PlayMusic(const std::string& music_type, bool continuous) -> void; - - // Fetch raw values from the config dict. The default value is returned if - // the requested value is not present or not of a compatible type. - // Note: to get app config values you should generally use the bs::AppConfig - // functions (which themselves call these functions) - auto GetRawConfigValue(const char* name) - -> PyObject*; // (returns a borrowed ref) - auto GetRawConfigValue(const char* name, const char* default_value) - -> std::string; - auto GetRawConfigValue(const char* name, float default_value) -> float; - auto GetRawConfigValue(const char* name, std::optional default_value) - -> std::optional; - auto GetRawConfigValue(const char* name, int default_value) -> int; - auto GetRawConfigValue(const char* name, bool default_value) -> bool; - auto SetRawConfigValue(const char* name, float value) -> void; - - auto RunDeepLink(const std::string& url) -> void; - auto GetResource(const char* key, const char* fallback_resource = nullptr, - const char* fallback_value = nullptr) -> std::string; - auto GetTranslation(const char* category, const char* s) -> std::string; - - // For checking and pulling values out of Python objects. - // These will all throw Exceptions on errors. - static auto GetPyString(PyObject* o) -> std::string; - static auto GetPyInt64(PyObject* o) -> int64_t; - static auto GetPyInt(PyObject* o) -> int; - static auto GetPyNode(PyObject* o, bool allow_empty_ref = false, - bool allow_none = false) -> Node*; - static auto GetPyNodes(PyObject* o) -> std::vector; - static auto GetPyMaterials(PyObject* o) -> std::vector; - static auto GetPyTextures(PyObject* o) -> std::vector; - static auto GetPyModels(PyObject* o) -> std::vector; - static auto GetPySounds(PyObject* o) -> std::vector; - static auto GetPyCollideModels(PyObject* o) -> std::vector; - static auto GetPyCollideModel(PyObject* o, bool allow_empty_ref = false, - bool allow_none = false) -> CollideModel*; - static auto IsPySession(PyObject* o) -> bool; - static auto GetPySession(PyObject* o) -> Session*; - static auto IsPyString(PyObject* o) -> bool; - static auto GetPyBool(PyObject* o) -> bool; - static auto GetPyHostActivity(PyObject* o) -> HostActivity*; - static auto IsPyHostActivity(PyObject* o) -> bool; - static auto GetPyInputDevice(PyObject* o) -> InputDevice*; - static auto IsPyPlayer(PyObject* o) -> bool; - static auto GetPyPlayer(PyObject* o, bool allow_empty_ref = false, - bool allow_none = false) -> Player*; - static auto GetPySessionPlayer(PyObject* o, bool allow_empty_ref = false, - bool allow_none = false) -> Player*; - static auto GetPyMaterial(PyObject* o, bool allow_empty_ref = false, - bool allow_none = false) -> Material*; - static auto GetPyTexture(PyObject* o, bool allow_empty_ref = false, - bool allow_none = false) -> Texture*; - static auto GetPyModel(PyObject* o, bool allow_empty_ref = false, - bool allow_none = false) -> Model*; - static auto GetPySound(PyObject* o, bool allow_empty_ref = false, - bool allow_none = false) -> Sound*; - static auto GetPyData(PyObject* o, bool allow_empty_ref = false, - bool allow_none = false) -> Data*; - static auto GetPyWidget(PyObject* o) -> Widget*; - static auto CanGetPyDouble(PyObject* o) -> bool; - static auto GetPyFloat(PyObject* o) -> float { - return static_cast(GetPyDouble(o)); - } - static auto GetPyDouble(PyObject* o) -> double; - static auto GetPyFloats(PyObject* o) -> std::vector; - static auto GetPyInts64(PyObject* o) -> std::vector; - static auto GetPyInts(PyObject* o) -> std::vector; - static auto GetPyStrings(PyObject* o) -> std::vector; - static auto GetPyUInts64(PyObject* o) -> std::vector; - static auto GetPyPoint2D(PyObject* o) -> Point2D; - static auto CanGetPyVector3f(PyObject* o) -> bool; - static auto GetPyVector3f(PyObject* o) -> Vector3f; - - static auto GetPyEnum_Permission(PyObject* obj) -> Permission; - static auto GetPyEnum_SpecialChar(PyObject* obj) -> SpecialChar; - static auto GetPyEnum_TimeType(PyObject* obj) -> TimeType; - static auto GetPyEnum_TimeFormat(PyObject* obj) -> TimeFormat; - static auto IsPyEnum_InputType(PyObject* obj) -> bool; - static auto GetPyEnum_InputType(PyObject* obj) -> InputType; - - static auto GetNodeAttr(Node* node, const char* attribute_name) -> PyObject*; - static auto SetNodeAttr(Node* node, const char* attr_name, - PyObject* value_obj) -> void; - - /// Set Python exception from C++ Exception. - static auto SetPythonException(const Exception& exc) -> void; - - static auto DoBuildNodeMessage(PyObject* args, int arg_offset, - Buffer* b, PyObject** user_message_obj) - -> void; - auto DoNewNode(PyObject* args, PyObject* keywds) -> Node*; - - /// Identifiers for specific Python objects we grab references to for easy - /// access. - enum class ObjID { - kEmptyTuple, - kApp, - kEnv, - kDeepCopyCall, - kShallowCopyCall, - kShouldShatterMessageClass, - kImpactDamageMessageClass, - kPickedUpMessageClass, - kDroppedMessageClass, - kOutOfBoundsMessageClass, - kPickUpMessageClass, - kDropMessageClass, - kShowURLWindowCall, - kActivityClass, - kSessionClass, - kJsonDumpsCall, - kJsonLoadsCall, - kGetDeviceValueCall, - kDeviceMenuPressCall, - kGetLastPlayerNameFromInputDeviceCall, - kOnScreenKeyboardClass, - kFilterChatMessageCall, - kHandleLocalChatMessageCall, - kHandlePartyInviteRevokeCall, - kDoPlayMusicCall, - kDeepLinkCall, - kGetResourceCall, - kTranslateCall, - kLStrClass, - kCallClass, - kGarbageCollectSessionEndCall, - kConfig, - kFinishBootstrappingCall, - kClientInfoQueryResponseCall, - kResetToMainMenuCall, - kSetConfigFullscreenOnCall, - kSetConfigFullscreenOffCall, - kNotSignedInScreenMessageCall, - kConnectingToPartyMessageCall, - kRejectingInviteAlreadyInPartyMessageCall, - kConnectionFailedMessageCall, - kTemporarilyUnavailableMessageCall, - kInProgressMessageCall, - kErrorMessageCall, - kPurchaseNotValidErrorCall, - kPurchaseAlreadyInProgressErrorCall, - kGearVRControllerWarningCall, - kVROrientationResetCBMessageCall, - kVROrientationResetMessageCall, - kHandleAppResumeCall, - kHandleV1CloudLogCall, - kLaunchMainMenuSessionCall, - kLanguageTestToggleCall, - kAwardInControlAchievementCall, - kAwardDualWieldingAchievementCall, - kPrintCorruptFileErrorCall, - kPlayGongSoundCall, - kLaunchCoopGameCall, - kPurchasesRestoredMessageCall, - kDismissWiiRemotesWindowCall, - kUnavailableMessageCall, - kSubmitAnalyticsCountsCall, - kSetLastAdNetworkCall, - kNoGameCircleMessageCall, - kGooglePlayPurchasesNotAvailableMessageCall, - kGooglePlayServicesNotAvailableMessageCall, - kEmptyCall, - kLevelIconPressCall, - kTrophyIconPressCall, - kCoinIconPressCall, - kTicketIconPressCall, - kBackButtonPressCall, - kFriendsButtonPressCall, - kPrintTraceCall, - kToggleFullscreenCall, - kPartyIconActivateCall, - kReadConfigCall, - kUIRemotePressCall, - kQuitWindowCall, - kRemoveInGameAdsMessageCall, - kTelnetAccessRequestCall, - kOnAppPauseCall, - kQuitCall, - kShutdownCall, - kGCDisableCall, - kShowPostPurchaseMessageCall, - kContextError, - kNotFoundError, - kNodeNotFoundError, - kSessionTeamNotFoundError, - kInputDeviceNotFoundError, - kDelegateNotFoundError, - kSessionPlayerNotFoundError, - kWidgetNotFoundError, - kActivityNotFoundError, - kSessionNotFoundError, - kAssetPackageClass, - kTimeFormatClass, - kTimeTypeClass, - kInputTypeClass, - kPermissionClass, - kSpecialCharClass, - kPlayerClass, - kGetPlayerIconCall, - kLstrFromJsonCall, - kUUIDStrCall, - kHashStringsCall, - kHaveAccountV2CredentialsCall, - kLoggingDebugCall, - kLoggingInfoCall, - kLoggingWarningCall, - kLoggingErrorCall, - kLoggingCriticalCall, - kImplicitSignInCall, - kImplicitSignOutCall, - kLoginAdapterGetSignInTokenResponseCall, - kOnTooManyFileDescriptorsCall, - kLast // Sentinel; must be at end. - }; - - /// Access a particular Python object we've grabbed/stored. - auto obj(ObjID id) const -> const PythonRef& { - assert(id < ObjID::kLast); - if (g_buildconfig.debug_build()) { - if (!objs_[static_cast(id)].exists()) { - throw Exception("Python::obj() called on nonexistent val " - + std::to_string(static_cast(id))); - } - } - return objs_[static_cast(id)]; - } - - /// Return whether we have a particular Python object. - auto objexists(ObjID id) const -> bool { - assert(id < ObjID::kLast); - return objs_[static_cast(id)].exists(); - } - - /// Create a Python list of strings. - auto StringList(const std::list& values) -> PythonRef; - - /// Create a Python single-member tuple. - auto SingleMemberTuple(const PythonRef& member) -> PythonRef; - - /// Push a call to a preset obj to the logic thread - /// (will be run in the UI context). - auto PushObjCall(ObjID obj) -> void; - - /// Push a call with a single string arg. - auto PushObjCall(ObjID obj, const std::string& arg) -> void; - - /// Register Python location and returns true if it has not - /// yet been registered. (for print-once type stuff). - auto DoOnce() -> bool; - - /// Check values passed to timer functions; triggers warnings - /// for cases that look like they're passing milliseconds as seconds - /// or vice versa... (can remove this once things are settled in). - auto TimeFormatCheck(TimeFormat time_format, PyObject* length_obj) -> void; - - private: - /// Check/set debug related initialization. - auto SetupInterpreterDebugState() -> void; - - /// Set up system paths if needed (for embedded builds). - auto SetupPythonHome() -> void; - - /// Set the value for a named object. - auto StoreObj(ObjID id, PyObject* pyobj, bool incref = false) -> void; - - /// Set the value for a named object and verify that it is a callable. - auto StoreObjCallable(ObjID id, PyObject* pyobj, bool incref = false) -> void; - - /// Set the value for a named object to the result of a Python expression. - auto StoreObj(ObjID id, const char* expression, PyObject* context = nullptr) - -> void; - - /// Set the value for a named object to the result of a Python expression - /// and verify that it is callable. - auto StoreObjCallable(ObjID id, const char* expression, - PyObject* context = nullptr) -> void; - - std::set do_once_locations_; - PythonRef objs_[static_cast(ObjID::kLast)]; - bool inited_{}; - std::list> clean_frame_commands_; - std::mutex early_log_lock_; - std::list> early_logs_; - PythonRef game_pad_call_; - PythonRef keyboard_call_; - PyObject* empty_dict_object_{}; - PyObject* main_dict_{}; - PyObject* env_{}; - PyThreadState* logic_thread_state_{}; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_PYTHON_H_ diff --git a/src/ballistica/python/python_context_call.cc b/src/ballistica/python/python_context_call.cc deleted file mode 100644 index 3a3980cc..00000000 --- a/src/ballistica/python/python_context_call.cc +++ /dev/null @@ -1,136 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/python/python_context_call.h" - -#include "ballistica/logic/host_activity.h" -#include "ballistica/logic/session/host_session.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_sys.h" - -namespace ballistica { - -// FIXME - should be static member var -PythonContextCall* PythonContextCall::current_call_{}; - -PythonContextCall::PythonContextCall(PyObject* obj_in) { - assert(InLogicThread()); - // As a sanity test, store the current context ptr just to make sure it - // hasn't changed when we run. -#if BA_DEBUG_BUILD - context_target_sanity_test_ = context_.target.get(); -#endif // BA_DEBUG_BUILD - BA_PRECONDITION(PyCallable_Check(obj_in)); - object_.Acquire(obj_in); - GetTrace(); - // Ok now we need to register this call with whatever the context is; - // it can be stored in a host-activity, a host-session, or the UI context. - // Whoever it is registered with will explicitly release its contents on - // shutdown and ensure that nothing gets run after that point. - if (HostActivity* ha = context_.GetHostActivity()) { - ha->RegisterCall(this); - } else if (HostSession* hs = context_.GetHostSession()) { - hs->RegisterCall(this); - } else if (context_.GetUIContext()) { - // UI context never currently dies so no registering necessary here.. - } else { - throw Exception( - "Invalid context; ContextCalls must be created in a non-expired " - "Activity, Session, or UI context. (call obj = " - + Python::ObjToString(obj_in) + ").", - PyExcType::kContext); - } -} - -PythonContextCall::~PythonContextCall() { - // lets set up context while we take our stuff down - // (we may be holding refs to actors or whatnot) - ScopedSetContext cp(context_); - object_.Release(); -} - -auto PythonContextCall::GetObjectDescription() const -> std::string { - return ""; -} - -void PythonContextCall::GetTrace() { - PyFrameObject* f = PyThreadState_GET()->frame; - if (f) { - // grab the file/line now in case we error - // (useful for debugging simple timers and callbacks and such) - file_loc_ = Python::GetPythonFileLocation(); - } -} - -// called by our owning context when it goes down -// we should clear ourself out to be a no-op if we still happen to be called -void PythonContextCall::MarkDead() { - dead_ = true; - object_.Release(); -} - -void PythonContextCall::Run(PyObject* args) { - assert(this); - - if (!g_python) { - // This probably means the game is dying; let's not - // throw an exception here so we don't mask the original error. - Log(LogLevel::kError, "PythonCommand: not running due to null g_python"); - return; - } - - if (dead_) { - return; - } - - // Sanity test: make sure our context didn't go away. -#if BA_DEBUG_BUILD - if (context_.target.get() != context_target_sanity_test_) { - Log(LogLevel::kWarning, - "Running Call after it's context has died: " + object_.Str()); - } -#endif // BA_DEBUG_BUILD - - // Restore the context from when we were made. - ScopedSetContext cp(context_); - - // Hold a ref to this call throughout this process - // so we know it'll still exist if we need to report - // exception info and whatnot. - Object::Ref keep_alive_ref(this); - - PythonContextCall* prev_call = current_call_; - current_call_ = this; - assert(Python::HaveGIL()); - PyObject* o = PyObject_Call( - object_.get(), - args ? args : g_python->obj(Python::ObjID::kEmptyTuple).get(), nullptr); - current_call_ = prev_call; - - if (o) { - Py_DECREF(o); - } else { - // Save/restore python error or it can mess with context print calls. - BA_PYTHON_ERROR_SAVE; - - PySys_WriteStderr("Exception in Python call:\n"); - PrintContext(); - BA_PYTHON_ERROR_RESTORE; - - // We pass zero here to avoid grabbing references to this exception - // which can cause objects to stick around and trip up our deletion checks. - // (nodes, actors existing after their games have ended). - PyErr_PrintEx(0); - PyErr_Clear(); - } -} - -void PythonContextCall::PrintContext() { - assert(InLogicThread()); - std::string s = std::string(" root call: ") + object().Str(); - s += ("\n root call origin: " + file_loc()); - s += g_python->GetContextBaseString(); - PySys_WriteStderr("%s\n", s.c_str()); -} - -} // namespace ballistica diff --git a/src/ballistica/python/python_context_call.h b/src/ballistica/python/python_context_call.h deleted file mode 100644 index 9b04ff1a..00000000 --- a/src/ballistica/python/python_context_call.h +++ /dev/null @@ -1,54 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_PYTHON_CONTEXT_CALL_H_ -#define BALLISTICA_PYTHON_PYTHON_CONTEXT_CALL_H_ - -#include - -#include "ballistica/core/context.h" -#include "ballistica/core/object.h" -#include "ballistica/python/python_ref.h" - -namespace ballistica { - -// A callable and context-state wrapped up in a convenient package. -// Handy for use with user-submitted callbacks, as it restores context -// state from when it was created and prints various useful bits of info -// on exceptions. -class PythonContextCall : public Object { - public: - static auto current_call() -> PythonContextCall* { return current_call_; } - PythonContextCall() = default; - ~PythonContextCall() override; - - /// Initialize from either a single callable object, or a tuple with a - /// callable and optionally args and keywords - explicit PythonContextCall(PyObject* callable); - void Run(PyObject* args = nullptr); - void Run(const PythonRef& args) { Run(args.get()); } - auto Exists() const -> bool { return object_.exists(); } - auto GetObjectDescription() const -> std::string override; - void MarkDead(); - auto object() const -> const PythonRef& { return object_; } - auto file_loc() const -> const std::string& { return file_loc_; } - auto PrintContext() -> void; - - private: - void GetTrace(); // we try to grab basic trace info - std::string file_loc_; - int line_{}; - bool dead_ = false; - PythonRef object_; - Context context_; -#if BA_DEBUG_BUILD - ContextTarget* context_target_sanity_test_{}; -#endif - static PythonContextCall* current_call_; -}; - -// FIXME: this should be static member var -extern PythonContextCall* g_current_python_call; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_PYTHON_CONTEXT_CALL_H_ diff --git a/src/ballistica/python/python_context_call_runnable.h b/src/ballistica/python/python_context_call_runnable.h deleted file mode 100644 index 0d1a99a2..00000000 --- a/src/ballistica/python/python_context_call_runnable.h +++ /dev/null @@ -1,25 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_PYTHON_PYTHON_CONTEXT_CALL_RUNNABLE_H_ -#define BALLISTICA_PYTHON_PYTHON_CONTEXT_CALL_RUNNABLE_H_ - -#include "ballistica/python/python_context_call.h" - -namespace ballistica { - -// a simple runnable that stores and runs a python context call -class PythonContextCallRunnable : public Runnable { - public: - explicit PythonContextCallRunnable(PyObject* o) - : call(Object::New(o)) {} - Object::Ref call; - void Run() override { - assert(call.exists()); - call->Run(); - } - virtual ~PythonContextCallRunnable() = default; -}; - -} // namespace ballistica - -#endif // BALLISTICA_PYTHON_PYTHON_CONTEXT_CALL_RUNNABLE_H_ diff --git a/src/ballistica/scene/node/session_globals_node.h b/src/ballistica/scene/node/session_globals_node.h deleted file mode 100644 index 4ed2c303..00000000 --- a/src/ballistica/scene/node/session_globals_node.h +++ /dev/null @@ -1,22 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_SCENE_NODE_SESSION_GLOBALS_NODE_H_ -#define BALLISTICA_SCENE_NODE_SESSION_GLOBALS_NODE_H_ - -#include "ballistica/scene/node/node.h" - -namespace ballistica { - -class SessionGlobalsNode : public Node { - public: - static auto InitType() -> NodeType*; - explicit SessionGlobalsNode(Scene* scene); - ~SessionGlobalsNode() override; - auto GetRealTime() -> millisecs_t; - auto GetTime() -> millisecs_t; - auto GetStep() -> int64_t; -}; - -} // namespace ballistica - -#endif // BALLISTICA_SCENE_NODE_SESSION_GLOBALS_NODE_H_ diff --git a/src/ballistica/scene/node/texture_sequence_node.h b/src/ballistica/scene/node/texture_sequence_node.h deleted file mode 100644 index 72cf0ccd..00000000 --- a/src/ballistica/scene/node/texture_sequence_node.h +++ /dev/null @@ -1,33 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_SCENE_NODE_TEXTURE_SEQUENCE_NODE_H_ -#define BALLISTICA_SCENE_NODE_TEXTURE_SEQUENCE_NODE_H_ - -#include - -#include "ballistica/ballistica.h" -#include "ballistica/scene/node/node.h" - -namespace ballistica { - -class TextureSequenceNode : public Node { - public: - static auto InitType() -> NodeType*; - explicit TextureSequenceNode(Scene* scene); - void Step() override; - auto rate() const -> int { return rate_; } - void set_rate(int val); - auto input_textures() const -> std::vector; - void set_input_textures(const std::vector& vals); - auto output_texture() const -> Texture*; - - private: - int sleep_count_{}; - int index_{}; - int rate_{}; - std::vector > input_textures_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_SCENE_NODE_TEXTURE_SEQUENCE_NODE_H_ diff --git a/src/ballistica/scene/scene_stream.h b/src/ballistica/scene/scene_stream.h deleted file mode 100644 index a43906a4..00000000 --- a/src/ballistica/scene/scene_stream.h +++ /dev/null @@ -1,172 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_SCENE_SCENE_STREAM_H_ -#define BALLISTICA_SCENE_SCENE_STREAM_H_ - -#include -#include - -#include "ballistica/core/object.h" -#include "ballistica/logic/client_controller_interface.h" - -namespace ballistica { - -// A mechanism for dumping a live session or session-creation-commands to a -// stream of messages that can be saved to file or sent over the network. -class SceneStream : public Object, public ClientControllerInterface { - public: - SceneStream(HostSession* host_session, bool save_replay); - ~SceneStream() override; - auto SetTime(millisecs_t t) -> void; - auto AddScene(Scene* s) -> void; - auto RemoveScene(Scene* s) -> void; - auto StepScene(Scene* s) -> void; - auto AddNode(Node* n) -> void; - auto NodeOnCreate(Node* n) -> void; - auto RemoveNode(Node* n) -> void; - auto SetForegroundScene(Scene* sg) -> void; - auto AddMaterial(Material* m) -> void; - auto RemoveMaterial(Material* m) -> void; - auto AddMaterialComponent(Material* m, MaterialComponent* c) -> void; - auto AddTexture(Texture* t) -> void; - auto RemoveTexture(Texture* t) -> void; - auto AddModel(Model* t) -> void; - auto RemoveModel(Model* t) -> void; - auto AddSound(Sound* t) -> void; - auto RemoveSound(Sound* t) -> void; - auto AddData(Data* d) -> void; - auto RemoveData(Data* d) -> void; - auto AddCollideModel(CollideModel* t) -> void; - auto RemoveCollideModel(CollideModel* t) -> void; - auto ConnectNodeAttribute(Node* src_node, NodeAttributeUnbound* src_attr, - Node* dst_node, NodeAttributeUnbound* dst_attr) - -> void; - auto NodeMessage(Node* node, const char* buffer, size_t size) -> void; - auto SetNodeAttr(const NodeAttribute& attr, float val) -> void; - auto SetNodeAttr(const NodeAttribute& attr, int64_t val) -> void; - auto SetNodeAttr(const NodeAttribute& attr, bool val) -> void; - auto SetNodeAttr(const NodeAttribute& attr, const std::vector& vals) - -> void; - auto SetNodeAttr(const NodeAttribute& attr, const std::vector& vals) - -> void; - auto SetNodeAttr(const NodeAttribute& attr, const std::string& val) -> void; - auto SetNodeAttr(const NodeAttribute& attr, Node* n) -> void; - auto SetNodeAttr(const NodeAttribute& attr, const std::vector& vals) - -> void; - auto SetNodeAttr(const NodeAttribute& attr, Player* n) -> void; - auto SetNodeAttr(const NodeAttribute& attr, - const std::vector& vals) -> void; - auto SetNodeAttr(const NodeAttribute& attr, Texture* n) -> void; - auto SetNodeAttr(const NodeAttribute& attr, const std::vector& vals) - -> void; - auto SetNodeAttr(const NodeAttribute& attr, Sound* n) -> void; - auto SetNodeAttr(const NodeAttribute& attr, const std::vector& vals) - -> void; - auto SetNodeAttr(const NodeAttribute& attr, Model* n) -> void; - auto SetNodeAttr(const NodeAttribute& attr, const std::vector& vals) - -> void; - auto SetNodeAttr(const NodeAttribute& attr, CollideModel* n) -> void; - auto SetNodeAttr(const NodeAttribute& attr, - const std::vector& vals) -> void; - auto PlaySoundAtPosition(Sound* sound, float volume, float x, float y, - float z) -> void; - auto PlaySound(Sound* sound, float volume) -> void; - auto EmitBGDynamics(const BGDynamicsEmission& e) -> void; - auto GetSoundID(Sound* s) -> int64_t; - auto GetMaterialID(Material* m) -> int64_t; - auto ScreenMessageBottom(const std::string& val, float r, float g, float b) - -> void; - auto ScreenMessageTop(const std::string& val, float r, float g, float b, - Texture* texture, Texture* tint_texture, float tint_r, - float tint_g, float tint_b, float tint2_r, - float tint2_g, float tint2_b) -> void; - auto OnClientConnected(ConnectionToClient* c) -> void override; - auto OnClientDisconnected(ConnectionToClient* c) -> void override; - auto GetOutMessage() const -> std::vector; - - private: - // Make sure various components are part of our stream. - auto IsValidScene(Scene* val) -> bool; - auto IsValidNode(Node* val) -> bool; - auto IsValidTexture(Texture* val) -> bool; - auto IsValidModel(Model* val) -> bool; - auto IsValidSound(Sound* val) -> bool; - auto IsValidData(Data* val) -> bool; - auto IsValidCollideModel(CollideModel* val) -> bool; - auto IsValidMaterial(Material* val) -> bool; - - auto Flush() -> void; - auto AddMessageToReplay(const std::vector& message) -> void; - auto Fail() -> void; - - auto ShipSessionCommandsMessage() -> void; - auto SendPhysicsCorrection(bool blend) -> void; - auto EndCommand(bool is_time_set = false) -> void; - auto WriteString(const std::string& s) -> void; - auto WriteFloat(float val) -> void; - auto WriteFloats(size_t count, const float* vals) -> void; - auto WriteInts32(size_t count, const int32_t* vals) -> void; - auto WriteInts64(size_t count, const int64_t* vals) -> void; - auto WriteChars(size_t count, const char* vals) -> void; - auto WriteCommand(SessionCommand cmd) -> void; - auto WriteCommandInt32(SessionCommand cmd, int32_t value) -> void; - auto WriteCommandInt64(SessionCommand cmd, int64_t value) -> void; - auto WriteCommandInt32_2(SessionCommand cmd, int32_t value1, int32_t value2) - -> void; - auto WriteCommandInt64_2(SessionCommand cmd, int64_t value1, int64_t value2) - -> void; - auto WriteCommandInt32_3(SessionCommand cmd, int32_t value1, int32_t value2, - int32_t value3) -> void; - auto WriteCommandInt64_3(SessionCommand cmd, int64_t value1, int64_t value2, - int64_t value3) -> void; - auto WriteCommandInt32_4(SessionCommand cmd, int32_t value1, int32_t value2, - int32_t value3, int32_t value4) -> void; - auto WriteCommandInt64_4(SessionCommand cmd, int64_t value1, int64_t value2, - int64_t value3, int64_t value4) -> void; - template - auto GetPointerCount(const std::vector& vec) -> size_t; - template - auto GetFreeIndex(std::vector* vec, std::vector* free_indices) - -> size_t; - template - auto Add(T* val, std::vector* vec, std::vector* free_indices) - -> void; - template - auto Remove(T* val, std::vector* vec, std::vector* free_indices) - -> void; - - HostSession* host_session_; - millisecs_t next_flush_time_; - - // Individual command going into the commands-messages. - std::vector out_command_; - - // The complete message full of commands. - std::vector out_message_; - std::vector connections_to_clients_; - std::vector connections_to_clients_ignored_; - bool writing_replay_; - millisecs_t last_physics_correction_time_; - millisecs_t last_send_time_; - millisecs_t time_; - std::vector scenes_; - std::vector free_indices_scene_graphs_; - std::vector nodes_; - std::vector free_indices_nodes_; - std::vector materials_; - std::vector free_indices_materials_; - std::vector textures_; - std::vector free_indices_textures_; - std::vector models_; - std::vector free_indices_models_; - std::vector sounds_; - std::vector free_indices_sounds_; - std::vector datas_; - std::vector free_indices_datas_; - std::vector collide_models_; - std::vector free_indices_collide_models_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_SCENE_SCENE_STREAM_H_ diff --git a/src/ballistica/scene/v1/scene_v1.cc b/src/ballistica/scene/v1/scene_v1.cc deleted file mode 100644 index ad532926..00000000 --- a/src/ballistica/scene/v1/scene_v1.cc +++ /dev/null @@ -1,96 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#include "ballistica/scene/v1/scene_v1.h" - -#include "ballistica/app/app.h" -#include "ballistica/scene/node/anim_curve_node.h" -#include "ballistica/scene/node/bomb_node.h" -#include "ballistica/scene/node/combine_node.h" -#include "ballistica/scene/node/explosion_node.h" -#include "ballistica/scene/node/flag_node.h" -#include "ballistica/scene/node/flash_node.h" -#include "ballistica/scene/node/globals_node.h" -#include "ballistica/scene/node/image_node.h" -#include "ballistica/scene/node/light_node.h" -#include "ballistica/scene/node/locator_node.h" -#include "ballistica/scene/node/math_node.h" -#include "ballistica/scene/node/null_node.h" -#include "ballistica/scene/node/player_node.h" -#include "ballistica/scene/node/region_node.h" -#include "ballistica/scene/node/scorch_node.h" -#include "ballistica/scene/node/session_globals_node.h" -#include "ballistica/scene/node/shield_node.h" -#include "ballistica/scene/node/sound_node.h" -#include "ballistica/scene/node/spaz_node.h" -#include "ballistica/scene/node/terrain_node.h" -#include "ballistica/scene/node/text_node.h" -#include "ballistica/scene/node/texture_sequence_node.h" -#include "ballistica/scene/node/time_display_node.h" - -namespace ballistica { - -static void SetupNodeMessageType(const std::string& name, NodeMessageType val, - const std::string& format) { - assert(g_app != nullptr); - g_app->node_message_types[name] = val; - assert(static_cast(val) >= 0); - if (g_app->node_message_formats.size() <= static_cast(val)) { - g_app->node_message_formats.resize(static_cast(val) + 1); - } - g_app->node_message_formats[static_cast(val)] = format; -} - -SceneV1::SceneV1() { - NodeType* node_types[] = {NullNode::InitType(), - GlobalsNode::InitType(), - SessionGlobalsNode::InitType(), - PropNode::InitType(), - FlagNode::InitType(), - BombNode::InitType(), - ExplosionNode::InitType(), - ShieldNode::InitType(), - LightNode::InitType(), - TextNode::InitType(), - AnimCurveNode::InitType(), - ImageNode::InitType(), - TerrainNode::InitType(), - MathNode::InitType(), - LocatorNode::InitType(), - PlayerNode::InitType(), - CombineNode::InitType(), - SoundNode::InitType(), - SpazNode::InitType(), - RegionNode::InitType(), - ScorchNode::InitType(), - FlashNode::InitType(), - TextureSequenceNode::InitType(), - TimeDisplayNode::InitType()}; - - int next_type_id = 0; - assert(g_app != nullptr); - for (auto* t : node_types) { - g_app->node_types[t->name()] = t; - g_app->node_types_by_id[next_type_id] = t; - t->set_id(next_type_id++); - } - - // Types: I is 32 bit int, i is 16 bit int, c is 8 bit int, - // F is 32 bit float, f is 16 bit float, - // s is string, b is bool. - SetupNodeMessageType("flash", NodeMessageType::kFlash, ""); - SetupNodeMessageType("footing", NodeMessageType::kFooting, "c"); - SetupNodeMessageType("impulse", NodeMessageType::kImpulse, "fffffffffifff"); - SetupNodeMessageType("kick_back", NodeMessageType::kKickback, "fffffff"); - SetupNodeMessageType("celebrate", NodeMessageType::kCelebrate, "i"); - SetupNodeMessageType("celebrate_l", NodeMessageType::kCelebrateL, "i"); - SetupNodeMessageType("celebrate_r", NodeMessageType::kCelebrateR, "i"); - SetupNodeMessageType("knockout", NodeMessageType::kKnockout, "f"); - SetupNodeMessageType("hurt_sound", NodeMessageType::kHurtSound, ""); - SetupNodeMessageType("picked_up", NodeMessageType::kPickedUp, ""); - SetupNodeMessageType("jump_sound", NodeMessageType::kJumpSound, ""); - SetupNodeMessageType("attack_sound", NodeMessageType::kAttackSound, ""); - SetupNodeMessageType("scream_sound", NodeMessageType::kScreamSound, ""); - SetupNodeMessageType("stand", NodeMessageType::kStand, "ffff"); -} - -} // namespace ballistica diff --git a/src/ballistica/scene/v1/scene_v1.h b/src/ballistica/scene/v1/scene_v1.h deleted file mode 100644 index ffed4963..00000000 --- a/src/ballistica/scene/v1/scene_v1.h +++ /dev/null @@ -1,16 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_SCENE_V1_SCENE_V1_H_ -#define BALLISTICA_SCENE_V1_SCENE_V1_H_ - -namespace ballistica { - -// Overall scene subsystem. -class SceneV1 { - public: - SceneV1(); -}; - -} // namespace ballistica - -#endif // BALLISTICA_SCENE_V1_SCENE_V1_H_ diff --git a/src/ballistica/scene_v1/README.md b/src/ballistica/scene_v1/README.md new file mode 100644 index 00000000..9f2718be --- /dev/null +++ b/src/ballistica/scene_v1/README.md @@ -0,0 +1,3 @@ +# Scene V1 Feature Set + +Gameplay code for classic BombSquad diff --git a/src/ballistica/scene_v1/assets/scene_asset.cc b/src/ballistica/scene_v1/assets/scene_asset.cc new file mode 100644 index 00000000..9c1312dd --- /dev/null +++ b/src/ballistica/scene_v1/assets/scene_asset.cc @@ -0,0 +1,39 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/assets/scene_asset.h" + +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/shared/python/python_sys.h" + +namespace ballistica::scene_v1 { + +SceneAsset::SceneAsset(std::string name, Scene* scene) + : name_(std::move(name)), scene_(scene) {} + +auto SceneAsset::GetPyRef(bool new_ref) -> PyObject* { + assert(!dead()); + if (!py_object_) { + // If we have no associated Python object, create it. + py_object_ = CreatePyObject(); + assert(py_object_ != nullptr); + } + if (new_ref) { + Py_INCREF(py_object_); + } + return py_object_; +} + +auto SceneAsset::GetObjectDescription() const -> std::string { + return ""; +} + +void SceneAsset::ReleasePyObj() { + assert(g_base->InLogicThread()); + if (py_object_) { + auto* obj = py_object_; + py_object_ = nullptr; + Py_DECREF(obj); + } +} + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/assets/scene_asset.h b/src/ballistica/scene_v1/assets/scene_asset.h new file mode 100644 index 00000000..a9d46fc9 --- /dev/null +++ b/src/ballistica/scene_v1/assets/scene_asset.h @@ -0,0 +1,84 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_ASSETS_SCENE_ASSET_H_ +#define BALLISTICA_SCENE_V1_ASSETS_SCENE_ASSET_H_ + +#include +#include +#include + +#include "ballistica/scene_v1/support/scene_v1_context.h" +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::scene_v1 { + +/// Handy function to try to return an asset from a std::unordered_map +/// of weak-refs, loading/adding it if need be. +template +static auto GetAsset(std::unordered_map >* list, + const std::string& name, Scene* scene) -> Object::Ref { + assert(g_base->InLogicThread()); + assert(list); + auto i = list->find(name); + + // If we have an entry pointing to a live component, just return a new ref + // to it. + if (i != list->end() && i->second.Exists()) { + return Object::Ref(i->second.Get()); + } else { + // Otherwise make a new one, pop a weak-ref on our list, and return a + // strong-ref to it. + auto t(Object::New(name, scene)); + (*list)[name] = t; + return t; + } +} + +/// A usage of an asset in a scene context_ref. +class SceneAsset : public Object { + public: + SceneAsset(std::string name, Scene* scene); + auto name() const -> std::string { return name_; } + + auto has_py_object() const -> bool { return (py_object_ != nullptr); } + auto NewPyRef() -> PyObject* { return GetPyRef(true); } + auto BorrowPyRef() -> PyObject* { return GetPyRef(false); } + auto GetObjectDescription() const -> std::string override; + auto scene() const -> Scene* { return scene_.Get(); } + + auto stream_id() const -> int64_t { return stream_id_; } + void set_stream_id(int64_t val) { + assert(stream_id_ == -1); + stream_id_ = val; + } + + void clear_stream_id() { + assert(stream_id_ != -1); + stream_id_ = -1; + } + + auto dead() const { return dead_; } + auto set_dead(bool val) { dead_ = val; } + + protected: + void ReleasePyObj(); + virtual auto GetAssetTypeName() const -> std::string = 0; + + // Create a python representation of this object. + virtual auto CreatePyObject() -> PyObject* = 0; + + private: + int64_t stream_id_{-1}; + Object::WeakRef scene_; + PyObject* py_object_{}; + + // Return a Python reference to the object, (creating Python obj if needed). + auto GetPyRef(bool new_ref = true) -> PyObject*; + std::string name_; + ContextRefSceneV1 context_; + bool dead_{false}; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_ASSETS_SCENE_ASSET_H_ diff --git a/src/ballistica/scene_v1/assets/scene_collision_mesh.cc b/src/ballistica/scene_v1/assets/scene_collision_mesh.cc new file mode 100644 index 00000000..61ca8323 --- /dev/null +++ b/src/ballistica/scene_v1/assets/scene_collision_mesh.cc @@ -0,0 +1,50 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/assets/scene_collision_mesh.h" + +#include "ballistica/scene_v1/python/class/python_class_scene_collision_mesh.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/scene_v1/support/session_stream.h" + +namespace ballistica::scene_v1 { + +SceneCollisionMesh::SceneCollisionMesh(const std::string& name, Scene* scene) + : SceneAsset(name, scene) { + assert(g_base->InLogicThread()); + if (scene) { + if (SessionStream* os = scene->GetSceneStream()) { + os->AddCollisionMesh(this); + } + } + { + base::Assets::AssetListLock lock; + collision_mesh_data_ = g_base->assets->GetCollisionMesh(name); + } + assert(collision_mesh_data_.Exists()); +} + +SceneCollisionMesh::~SceneCollisionMesh() { MarkDead(); } + +void SceneCollisionMesh::MarkDead() { + if (dead()) { + return; + } + set_dead(true); + + if (Scene* s = scene()) { + if (SessionStream* os = s->GetSceneStream()) { + os->RemoveCollisionMesh(this); + } + } + + // If we've created a Python ref, it's likewise holding a ref + // to us, which is a dependency loop. Break the loop to allow us + // to go down cleanly. + ReleasePyObj(); +} + +auto SceneCollisionMesh::CreatePyObject() -> PyObject* { + return PythonClassSceneCollisionMesh::Create(this); +} + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/assets/scene_collision_mesh.h b/src/ballistica/scene_v1/assets/scene_collision_mesh.h new file mode 100644 index 00000000..c7953aab --- /dev/null +++ b/src/ballistica/scene_v1/assets/scene_collision_mesh.h @@ -0,0 +1,37 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_ASSETS_SCENE_COLLISION_MESH_H_ +#define BALLISTICA_SCENE_V1_ASSETS_SCENE_COLLISION_MESH_H_ + +#include + +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/assets/collision_mesh_asset.h" +#include "ballistica/scene_v1/assets/scene_asset.h" + +namespace ballistica::scene_v1 { + +// Usage of a collision-mesh in a scene. +class SceneCollisionMesh : public SceneAsset { + public: + SceneCollisionMesh(const std::string& name, Scene* scene); + ~SceneCollisionMesh() override; + + auto collision_mesh_data() const -> base::CollisionMeshAsset* { + return collision_mesh_data_.Get(); + } + auto GetAssetTypeName() const -> std::string override { + return "CollisionMesh"; + } + void MarkDead(); + + protected: + auto CreatePyObject() -> PyObject* override; + + private: + Object::Ref collision_mesh_data_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_ASSETS_SCENE_COLLISION_MESH_H_ diff --git a/src/ballistica/scene_v1/assets/scene_cube_map_texture.cc b/src/ballistica/scene_v1/assets/scene_cube_map_texture.cc new file mode 100644 index 00000000..cb1c7629 --- /dev/null +++ b/src/ballistica/scene_v1/assets/scene_cube_map_texture.cc @@ -0,0 +1,21 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/assets/scene_cube_map_texture.h" + +#include "ballistica/base/assets/assets.h" + +namespace ballistica::scene_v1 { + +SceneCubeMapTexture::SceneCubeMapTexture(const std::string& name, Scene* scene) + : SceneAsset(name, scene) { + assert(g_base->InLogicThread()); + + // cant currently add these to scenes so nothing to do here.. + { + base::Assets::AssetListLock lock; + texture_data_ = g_base->assets->GetCubeMapTexture(name); + } + assert(texture_data_.Exists()); +} + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/assets/scene_cube_map_texture.h b/src/ballistica/scene_v1/assets/scene_cube_map_texture.h new file mode 100644 index 00000000..13b7c450 --- /dev/null +++ b/src/ballistica/scene_v1/assets/scene_cube_map_texture.h @@ -0,0 +1,33 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_ASSETS_SCENE_CUBE_MAP_TEXTURE_H_ +#define BALLISTICA_SCENE_V1_ASSETS_SCENE_CUBE_MAP_TEXTURE_H_ + +#include + +#include "ballistica/base/assets/texture_asset.h" +#include "ballistica/scene_v1/assets/scene_asset.h" + +namespace ballistica::scene_v1 { + +class SceneCubeMapTexture : public SceneAsset { + public: + SceneCubeMapTexture(const std::string& name, Scene* s); + + // return the TextureData currently associated with this texture + // note that a texture's data can change over time as different + // versions are spooled in/out/etc + auto GetTextureData() const -> base::TextureAsset* { + return texture_data_.Get(); + } + auto GetAssetTypeName() const -> std::string override { + return "CubeMapTexture"; + } + + private: + Object::Ref texture_data_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_ASSETS_SCENE_CUBE_MAP_TEXTURE_H_ diff --git a/src/ballistica/scene_v1/assets/scene_data_asset.cc b/src/ballistica/scene_v1/assets/scene_data_asset.cc new file mode 100644 index 00000000..f4c34596 --- /dev/null +++ b/src/ballistica/scene_v1/assets/scene_data_asset.cc @@ -0,0 +1,51 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/assets/scene_data_asset.h" + +#include "ballistica/scene_v1/python/class/python_class_scene_data_asset.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/scene_v1/support/session_stream.h" + +namespace ballistica::scene_v1 { + +SceneDataAsset::SceneDataAsset(const std::string& name, Scene* scene) + : SceneAsset(name, scene) { + assert(g_base->InLogicThread()); + + if (scene) { + if (SessionStream* os = scene->GetSceneStream()) { + os->AddData(this); + } + } + { + base::Assets::AssetListLock lock; + data_data_ = g_base->assets->GetDataAsset(name); + } + assert(data_data_.Exists()); +} + +SceneDataAsset::~SceneDataAsset() { MarkDead(); } + +void SceneDataAsset::MarkDead() { + if (dead()) { + return; + } + set_dead(true); + + if (Scene* s = scene()) { + if (SessionStream* os = s->GetSceneStream()) { + os->RemoveData(this); + } + } + + // If we've created a Python ref, it's likewise holding a ref + // to us, which is a dependency loop. Break the loop to allow us + // to go down cleanly. + ReleasePyObj(); +} + +auto SceneDataAsset::CreatePyObject() -> PyObject* { + return PythonClassSceneDataAsset::Create(this); +} + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/assets/scene_data_asset.h b/src/ballistica/scene_v1/assets/scene_data_asset.h new file mode 100644 index 00000000..863bf885 --- /dev/null +++ b/src/ballistica/scene_v1/assets/scene_data_asset.h @@ -0,0 +1,40 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_ASSETS_SCENE_DATA_ASSET_H_ +#define BALLISTICA_SCENE_V1_ASSETS_SCENE_DATA_ASSET_H_ + +#include +#include + +#include "ballistica/base/assets/asset.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/assets/data_asset.h" +#include "ballistica/scene_v1/assets/scene_asset.h" +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::scene_v1 { + +// user-facing data class +class SceneDataAsset : public SceneAsset { + public: + SceneDataAsset(const std::string& name, Scene* scene); + ~SceneDataAsset() override; + + // return the DataData currently associated with this data + // note that a data's data can change over time as different + // versions are spooled in/out/etc. + auto data_data() const -> base::DataAsset* { return data_data_.Get(); } + auto GetAssetTypeName() const -> std::string override { return "Data"; } + void MarkDead(); + + protected: + auto CreatePyObject() -> PyObject* override; + + private: + Object::Ref data_data_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_ASSETS_SCENE_DATA_ASSET_H_ diff --git a/src/ballistica/scene_v1/assets/scene_mesh.cc b/src/ballistica/scene_v1/assets/scene_mesh.cc new file mode 100644 index 00000000..be0e13f1 --- /dev/null +++ b/src/ballistica/scene_v1/assets/scene_mesh.cc @@ -0,0 +1,50 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/assets/scene_mesh.h" + +#include "ballistica/scene_v1/python/class/python_class_scene_mesh.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/scene_v1/support/session_stream.h" + +namespace ballistica::scene_v1 { + +SceneMesh::SceneMesh(const std::string& name, Scene* scene) + : SceneAsset(name, scene) { + assert(g_base->InLogicThread()); + + if (scene) { + if (SessionStream* os = scene->GetSceneStream()) { + os->AddMesh(this); + } + } + { + base::Assets::AssetListLock lock; + mesh_data_ = g_base->assets->GetMesh(name); + } + assert(mesh_data_.Exists()); +} + +SceneMesh::~SceneMesh() { MarkDead(); } + +void SceneMesh::MarkDead() { + if (dead()) { + return; + } + set_dead(true); + if (Scene* s = scene()) { + if (SessionStream* os = s->GetSceneStream()) { + os->RemoveMesh(this); + } + } + + // If we've created a Python ref, it's likewise holding a ref + // to us, which is a dependency loop. Break the loop to allow us + // to go down cleanly. + ReleasePyObj(); +} + +auto SceneMesh::CreatePyObject() -> PyObject* { + return PythonClassSceneMesh::Create(this); +} + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/assets/scene_mesh.h b/src/ballistica/scene_v1/assets/scene_mesh.h new file mode 100644 index 00000000..adf384e7 --- /dev/null +++ b/src/ballistica/scene_v1/assets/scene_mesh.h @@ -0,0 +1,38 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_ASSETS_SCENE_MESH_H_ +#define BALLISTICA_SCENE_V1_ASSETS_SCENE_MESH_H_ + +#include +#include + +#include "ballistica/base/assets/asset.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/assets/mesh_asset.h" +#include "ballistica/base/assets/mesh_asset_renderer_data.h" +#include "ballistica/scene_v1/assets/scene_asset.h" +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::scene_v1 { + +// Usage of a mesh in a scene. +class SceneMesh : public SceneAsset { + public: + SceneMesh(const std::string& name, Scene* scene); + ~SceneMesh() override; + + auto mesh_data() const -> base::MeshAsset* { return mesh_data_.Get(); } + auto GetAssetTypeName() const -> std::string override { return "Mesh"; } + void MarkDead(); + + protected: + auto CreatePyObject() -> PyObject* override; + + private: + Object::Ref mesh_data_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_ASSETS_SCENE_MESH_H_ diff --git a/src/ballistica/scene_v1/assets/scene_sound.cc b/src/ballistica/scene_v1/assets/scene_sound.cc new file mode 100644 index 00000000..bc8a4dfd --- /dev/null +++ b/src/ballistica/scene_v1/assets/scene_sound.cc @@ -0,0 +1,53 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/assets/scene_sound.h" + +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/assets/sound_asset.h" +#include "ballistica/scene_v1/python/class/python_class_scene_sound.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/scene_v1/support/session_stream.h" + +namespace ballistica::scene_v1 { + +SceneSound::SceneSound(const std::string& name, Scene* scene) + : SceneAsset(name, scene) { + assert(g_base->InLogicThread()); + + if (scene) { + if (SessionStream* os = scene->GetSceneStream()) { + os->AddSound(this); + } + } + { + base::Assets::AssetListLock lock; + sound_data_ = g_base->assets->GetSound(name); + } + assert(sound_data_.Exists()); +} + +SceneSound::~SceneSound() { MarkDead(); } + +void SceneSound::MarkDead() { + if (dead()) { + return; + } + set_dead(true); + + if (Scene* s = scene()) { + if (SessionStream* os = s->GetSceneStream()) { + os->RemoveSound(this); + } + } + + // If we've created a Python ref, it's likewise holding a ref + // to us, which is a dependency loop. Break the loop to allow us + // to go down cleanly. + ReleasePyObj(); +} + +auto SceneSound::CreatePyObject() -> PyObject* { + return PythonClassSceneSound::Create(this); +} + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/assets/scene_sound.h b/src/ballistica/scene_v1/assets/scene_sound.h new file mode 100644 index 00000000..4ba8d360 --- /dev/null +++ b/src/ballistica/scene_v1/assets/scene_sound.h @@ -0,0 +1,35 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_ASSETS_SCENE_SOUND_H_ +#define BALLISTICA_SCENE_V1_ASSETS_SCENE_SOUND_H_ + +#include +#include + +#include "ballistica/base/base.h" +#include "ballistica/scene_v1/assets/scene_asset.h" + +namespace ballistica::scene_v1 { + +class SceneSound : public SceneAsset { + public: + SceneSound(const std::string& name, Scene* scene); + ~SceneSound() override; + + // Return the SoundData currently associated with this sound. + // Note that a sound's data can change over time as different + // versions are spooled in/out/etc. + auto GetSoundData() const -> base::SoundAsset* { return sound_data_.Get(); } + auto GetAssetTypeName() const -> std::string override { return "Sound"; } + void MarkDead(); + + protected: + auto CreatePyObject() -> PyObject* override; + + private: + Object::Ref sound_data_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_ASSETS_SCENE_SOUND_H_ diff --git a/src/ballistica/scene_v1/assets/scene_texture.cc b/src/ballistica/scene_v1/assets/scene_texture.cc new file mode 100644 index 00000000..a7309927 --- /dev/null +++ b/src/ballistica/scene_v1/assets/scene_texture.cc @@ -0,0 +1,64 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/assets/scene_texture.h" + +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/scene_v1/python/class/python_class_scene_texture.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/scene_v1/support/session_stream.h" + +namespace ballistica::scene_v1 { + +SceneTexture::SceneTexture(const std::string& name, Scene* scene) + : SceneAsset(name, scene) { + assert(g_base->InLogicThread()); + + // Add to the provided scene to get a numeric ID. + if (scene) { + if (SessionStream* os = scene->GetSceneStream()) { + os->AddTexture(this); + } + } + { + base::Assets::AssetListLock lock; + texture_data_ = g_base->assets->GetTexture(name); + } + assert(texture_data_.Exists()); +} + +// qrcode version +SceneTexture::SceneTexture(const std::string& qr_url) + : SceneAsset(qr_url, nullptr) { + assert(g_base->InLogicThread()); + { + base::Assets::AssetListLock lock; + texture_data_ = g_base->assets->GetQRCodeTexture(qr_url); + } + assert(texture_data_.Exists()); +} + +SceneTexture::~SceneTexture() { MarkDead(); } + +void SceneTexture::MarkDead() { + if (dead()) { + return; + } + set_dead(true); + + if (Scene* s = scene()) { + if (SessionStream* os = s->GetSceneStream()) { + os->RemoveTexture(this); + } + } + + // If we've created a Python ref, it's likewise holding a ref + // to us, which is a dependency loop. Break the loop to allow us + // to go down cleanly. + ReleasePyObj(); +} + +auto SceneTexture::CreatePyObject() -> PyObject* { + return PythonClassSceneTexture::Create(this); +} + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/assets/scene_texture.h b/src/ballistica/scene_v1/assets/scene_texture.h new file mode 100644 index 00000000..764e1a34 --- /dev/null +++ b/src/ballistica/scene_v1/assets/scene_texture.h @@ -0,0 +1,38 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_ASSETS_SCENE_TEXTURE_H_ +#define BALLISTICA_SCENE_V1_ASSETS_SCENE_TEXTURE_H_ + +#include + +#include "ballistica/base/assets/texture_asset.h" +#include "ballistica/scene_v1/assets/scene_asset.h" + +namespace ballistica::scene_v1 { + +// User-facing texture class. +class SceneTexture : public SceneAsset { + public: + SceneTexture(const std::string& name, Scene* scene); + explicit SceneTexture(const std::string& qr_url); + ~SceneTexture() override; + + // Return the TextureData currently associated with this texture. + // Note that a texture's data can change over time as different + // versions are spooled in/out/etc. + auto texture_data() const -> base::TextureAsset* { + return texture_data_.Get(); + } + auto GetAssetTypeName() const -> std::string override { return "Texture"; } + void MarkDead(); + + protected: + auto CreatePyObject() -> PyObject* override; + + private: + Object::Ref texture_data_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_ASSETS_SCENE_TEXTURE_H_ diff --git a/src/ballistica/logic/connection/connection.cc b/src/ballistica/scene_v1/connection/connection.cc similarity index 89% rename from src/ballistica/logic/connection/connection.cc rename to src/ballistica/scene_v1/connection/connection.cc index cf7739a3..02e81301 100644 --- a/src/ballistica/logic/connection/connection.cc +++ b/src/ballistica/scene_v1/connection/connection.cc @@ -1,15 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/logic/connection/connection.h" +#include "ballistica/scene_v1/connection/connection.h" -#include "ballistica/generic/huffman.h" -#include "ballistica/generic/json.h" -#include "ballistica/generic/utils.h" -#include "ballistica/math/vector3f.h" -#include "ballistica/networking/networking.h" -#include "ballistica/platform/platform.h" +#include "ballistica/base/base.h" +#include "ballistica/base/networking/networking.h" +#include "ballistica/base/support/huffman.h" +#include "ballistica/core/core.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/generic/json.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/math/vector3f.h" -namespace ballistica { +namespace ballistica::scene_v1 { // How long to go without sending a state packet before // we send keepalives. Keepalives contain the latest ack info. @@ -29,7 +32,7 @@ const int kPingMeasureInterval = 2000; Connection::Connection() { // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer) - creation_time_ = last_average_update_time_ = GetRealTime(); + creation_time_ = last_average_update_time_ = g_core->GetAppTimeMillisecs(); } void Connection::ProcessWaitingMessages() { @@ -134,7 +137,7 @@ void Connection::HandleResends(millisecs_t real_time, // Add our header/acks and go ahead and send this one out. // 1 byte for type, 2 for packet-num, 3 for acks std::vector data_out(msg.data.size() + kMessagePacketHeaderSize); - data_out[0] = BA_GAMEPACKET_MESSAGE; + data_out[0] = BA_SCENEPACKET_MESSAGE; memcpy(data_out.data() + 1, &num, sizeof(num)); EmbedAcks(real_time, &data_out, 3); memcpy(&(data_out[6]), &(msg.data[0]), msg.data.size()); @@ -149,7 +152,7 @@ void Connection::HandleResends(millisecs_t real_time, void Connection::HandleGamePacketCompressed(const std::vector& data) { std::vector data_decompressed; try { - data_decompressed = g_utils->huffman()->decompress(data); + data_decompressed = g_base->huffman->decompress(data); } catch (const std::exception& e) { Log(LogLevel::kError, std::string("Error in huffman decompression for packet: ") + e.what()); @@ -169,19 +172,19 @@ void Connection::HandleGamePacket(const std::vector& data) { assert(!data.empty()); switch (data[0]) { - case BA_GAMEPACKET_KEEPALIVE: { + case BA_SCENEPACKET_KEEPALIVE: { if (data.size() != 4) { BA_LOG_ONCE(LogLevel::kError, - "Error: got invalid BA_GAMEPACKET_KEEPALIVE packet."); + "Error: got invalid BA_SCENEPACKET_KEEPALIVE packet."); return; } - millisecs_t real_time = GetRealTime(); + millisecs_t real_time = g_core->GetAppTimeMillisecs(); HandleResends(real_time, data, 1); break; } - case BA_GAMEPACKET_MESSAGE: { - millisecs_t real_time = GetRealTime(); + case BA_SCENEPACKET_MESSAGE: { + millisecs_t real_time = g_core->GetAppTimeMillisecs(); // Expect 1 byte type, 2 byte num, 3 byte acks, at least 1 byte payload. if (data.size() < 7) { @@ -204,7 +207,7 @@ void Connection::HandleGamePacket(const std::vector& data) { ReliableMessageIn& msg(in_messages_[num]); msg.data.resize(data.size() - 6); memcpy(&(msg.data[0]), &(data[6]), msg.data.size()); - msg.arrival_time = GetRealTime(); + msg.arrival_time = g_core->GetAppTimeMillisecs(); // Now run all in-order packets we've got. ProcessWaitingMessages(); @@ -212,7 +215,7 @@ void Connection::HandleGamePacket(const std::vector& data) { break; } - case BA_GAMEPACKET_MESSAGE_UNRELIABLE: { + case BA_SCENEPACKET_MESSAGE_UNRELIABLE: { // Expect 1 byte type, 2 byte num, 2 byte unreliable-num, 3 byte acks, // at least 1 byte payload. if (data.size() < 9) { @@ -299,7 +302,7 @@ void Connection::SendReliableMessage(const std::vector& data) { assert(out_messages_.find(num) == out_messages_.end()); ReliableMessageOut& msg(out_messages_[num]); - millisecs_t real_time = GetRealTime(); + millisecs_t real_time = g_core->GetAppTimeMillisecs(); msg.data = data; msg.first_send_time = msg.last_send_time = real_time; @@ -310,7 +313,7 @@ void Connection::SendReliableMessage(const std::vector& data) { // 1 byte for type, 2 for packet-num, 3 for acks std::vector data_out(data.size() + kMessagePacketHeaderSize); - data_out[0] = BA_GAMEPACKET_MESSAGE; + data_out[0] = BA_SCENEPACKET_MESSAGE; memcpy(data_out.data() + 1, &num, sizeof(num)); EmbedAcks(real_time, &data_out, 3); memcpy(&(data_out[6]), &(data[0]), data.size()); @@ -332,13 +335,13 @@ void Connection::SendUnreliableMessage(const std::vector& data) { } uint16_t num = next_out_unreliable_message_num_++; - millisecs_t real_time = GetRealTime(); + millisecs_t real_time = g_core->GetAppTimeMillisecs(); // Add our header/acks and go ahead and send this one out. // 1 byte for type, 2 for packet-num, 2 for unreliable packet-num, 3 for acks. std::vector data_out(data.size() + 8); - data_out[0] = BA_GAMEPACKET_MESSAGE_UNRELIABLE; + data_out[0] = BA_SCENEPACKET_MESSAGE_UNRELIABLE; memcpy(data_out.data() + 1, &next_out_message_num_, sizeof(next_out_message_num_)); memcpy(data_out.data() + 3, &num, sizeof(num)); @@ -358,7 +361,7 @@ void Connection::SendJMessage(cJSON* val) { } void Connection::Update() { - millisecs_t real_time = GetRealTime(); + millisecs_t real_time = g_core->GetAppTimeMillisecs(); // Update our averages once per second. while (real_time - last_average_update_time_ > 1000) { @@ -382,7 +385,7 @@ void Connection::Update() { // 1 byte type, 2 byte next-expected, 1 byte extra-acks. std::vector data(4); - data[0] = BA_GAMEPACKET_KEEPALIVE; + data[0] = BA_SCENEPACKET_KEEPALIVE; EmbedAcks(real_time, &data, 1); SendGamePacket(data); } @@ -466,21 +469,21 @@ void Connection::SendGamePacket(const std::vector& data) { // (if we don't speak the same language we still need to be // able to tell them to buzz off) bool can_send = can_communicate(); - if (data[0] == BA_GAMEPACKET_DISCONNECT) { + if (data[0] == BA_SCENEPACKET_DISCONNECT) { can_send = true; } // We aren't allowed to send anything out except handshakes until // we've established that we can speak their language. // If something does come through, just ignore it. - if (!can_send && data[0] != BA_GAMEPACKET_HANDSHAKE - && data[0] != BA_GAMEPACKET_HANDSHAKE_RESPONSE) { + if (!can_send && data[0] != BA_SCENEPACKET_HANDSHAKE + && data[0] != BA_SCENEPACKET_HANDSHAKE_RESPONSE) { if (explicit_bool(false)) { - BA_LOG_ONCE(LogLevel::kError, - "SendGamePacket() called before can_communicate set (" - + g_platform->DemangleCXXSymbol(typeid(*this).name()) - + " ptype " + std::to_string(static_cast(data[0])) - + ")"); + BA_LOG_ONCE( + LogLevel::kError, + "SendGamePacket() called before can_communicate set (" + + g_core->platform->DemangleCXXSymbol(typeid(*this).name()) + + " ptype " + std::to_string(static_cast(data[0])) + ")"); } return; } @@ -489,7 +492,7 @@ void Connection::SendGamePacket(const std::vector& data) { bytes_out_ += data.size(); // We huffman-compress gamepackets on their way out. - std::vector data_compressed = g_utils->huffman()->compress(data); + std::vector data_compressed = g_base->huffman->compress(data); #if kTestPacketDrops if (rand() % 100 < kTestPacketDropPercent) { // NOLINT @@ -501,4 +504,4 @@ void Connection::SendGamePacket(const std::vector& data) { SendGamePacketCompressed(data_compressed); } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/logic/connection/connection.h b/src/ballistica/scene_v1/connection/connection.h similarity index 92% rename from src/ballistica/logic/connection/connection.h rename to src/ballistica/scene_v1/connection/connection.h index 2f0e73ce..2a1f093e 100644 --- a/src/ballistica/logic/connection/connection.h +++ b/src/ballistica/scene_v1/connection/connection.h @@ -1,17 +1,17 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_LOGIC_CONNECTION_CONNECTION_H_ -#define BALLISTICA_LOGIC_CONNECTION_CONNECTION_H_ +#ifndef BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_H_ +#define BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_H_ #include #include #include -#include "ballistica/core/object.h" -#include "ballistica/logic/player_spec.h" -#include "ballistica/python/python_ref.h" +#include "ballistica/scene_v1/support/player_spec.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_ref.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Start near the top of the range to make sure looping works as expected. const int kFirstConnectionStateNum = 65520; @@ -140,6 +140,6 @@ class Connection : public Object { uint16_t next_in_unreliable_message_num_{}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_LOGIC_CONNECTION_CONNECTION_H_ +#endif // BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_H_ diff --git a/src/ballistica/logic/connection/connection_set.cc b/src/ballistica/scene_v1/connection/connection_set.cc similarity index 82% rename from src/ballistica/logic/connection/connection_set.cc rename to src/ballistica/scene_v1/connection/connection_set.cc index 23797bf6..e83c66b2 100644 --- a/src/ballistica/logic/connection/connection_set.cc +++ b/src/ballistica/scene_v1/connection/connection_set.cc @@ -1,24 +1,25 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/logic/connection/connection_set.h" +#include "ballistica/scene_v1/connection/connection_set.h" -#include "ballistica/core/thread.h" -#include "ballistica/input/device/input_device.h" -#include "ballistica/logic/connection/connection_to_client_udp.h" -#include "ballistica/logic/connection/connection_to_host_udp.h" -#include "ballistica/logic/logic.h" -#include "ballistica/logic/player.h" -#include "ballistica/logic/session/host_session.h" -#include "ballistica/networking/network_writer.h" -#include "ballistica/networking/sockaddr.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_sys.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/networking/network_writer.h" +#include "ballistica/scene_v1/connection/connection_to_client_udp.h" +#include "ballistica/scene_v1/connection/connection_to_host_udp.h" +#include "ballistica/scene_v1/python/scene_v1_python.h" +#include "ballistica/scene_v1/support/host_session.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" + +namespace ballistica::scene_v1 { -namespace ballistica { ConnectionSet::ConnectionSet() = default; auto ConnectionSet::GetConnectionToHostUDP() -> ConnectionToHostUDP* { - ConnectionToHost* h = connection_to_host_.get(); + ConnectionToHost* h = connection_to_host_.Get(); return h ? h->GetAsUDP() : nullptr; } @@ -30,7 +31,7 @@ void ConnectionSet::RegisterClientController(ClientControllerInterface* c) { "RegisterClientController() called " "but already have a controller; bad."); for (auto&& i : connections_to_clients_) { - assert(i.second.exists()); + assert(i.second.Exists()); i.second->SetController(nullptr); } } @@ -39,7 +40,7 @@ void ConnectionSet::RegisterClientController(ClientControllerInterface* c) { client_controller_ = c; if (client_controller_) { for (auto&& i : connections_to_clients_) { - assert(i.second.exists()); + assert(i.second.Exists()); if (i.second->can_communicate()) { i.second->SetController(client_controller_); } @@ -47,26 +48,26 @@ void ConnectionSet::RegisterClientController(ClientControllerInterface* c) { } } -auto ConnectionSet::Update() -> void { +void ConnectionSet::Update() { // First do housekeeping on our client/host connections. for (auto&& i : connections_to_clients_) { BA_IFDEBUG(Object::WeakRef test_ref(i.second)); i.second->Update(); // Make sure the connection didn't kill itself in the update. - assert(test_ref.exists()); + assert(test_ref.Exists()); } - if (connection_to_host_.exists()) { + if (connection_to_host_.Exists()) { connection_to_host_->Update(); } } auto ConnectionSet::GetConnectedClientCount() const -> int { - assert(InLogicThread()); + assert(g_base->InLogicThread()); int count = 0; for (auto&& i : connections_to_clients_) { - if (i.second.exists() && i.second->can_communicate()) { + if (i.second.Exists() && i.second->can_communicate()) { count++; } } @@ -87,6 +88,8 @@ void ConnectionSet::SendChatMessage(const std::string& message, "Can't send chat message with sender_override as a client."); } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + std::string our_spec_string; if (sender_override != nullptr) { @@ -114,11 +117,11 @@ void ConnectionSet::SendChatMessage(const std::string& message, // spec out of their name(s). std::string p_name_combined; if (auto* hs = - dynamic_cast(g_logic->GetForegroundSession())) { + dynamic_cast(appmode->GetForegroundSession())) { for (auto&& p : hs->players()) { - InputDevice* input_device = p->GetInputDevice(); - if (p->accepted() && p->name_is_real() && input_device != nullptr - && !input_device->IsRemoteClient()) { + auto* delegate = p->input_device_delegate(); + if (p->accepted() && p->name_is_real() && delegate != nullptr + && !delegate->IsRemoteClient()) { if (!p_name_combined.empty()) { p_name_combined += "/"; } @@ -149,7 +152,7 @@ void ConnectionSet::SendChatMessage(const std::string& message, // If we're the host, run filters before we send the message out. // If the filter kills the message, don't send. - bool allow_message = g_python->FilterChatMessage(&message2, -1); + bool allow_message = g_scene_v1->python->FilterChatMessage(&message2, -1); if (!allow_message) { return; } @@ -193,7 +196,7 @@ void ConnectionSet::SendChatMessage(const std::string& message, // And display locally if the message is addressed to all. if (clients == nullptr) { - g_logic->LocalDisplayChatMessage(msg_out); + appmode->LocalDisplayChatMessage(msg_out); } } } @@ -204,8 +207,8 @@ auto ConnectionSet::GetConnectionsToClients() std::vector connections; connections.reserve(connections_to_clients_.size()); for (auto& connections_to_client : connections_to_clients_) { - if (connections_to_client.second.exists()) { - connections.push_back(connections_to_client.second.get()); + if (connections_to_client.second.Exists()) { + connections.push_back(connections_to_client.second.Get()); } else { Log(LogLevel::kError, "HAVE NONEXISTENT CONNECTION_TO_CLIENT IN LIST; UNEXPECTED"); @@ -214,29 +217,13 @@ auto ConnectionSet::GetConnectionsToClients() return connections; } -void ConnectionSet::PushUDPConnectionPacketCall( - const std::vector& data, const SockAddr& addr) { - // Avoid buffer-full errors if something is causing us to write too often; - // these are unreliable messages so its ok to just drop them. - if (!g_logic->thread()->CheckPushSafety()) { - BA_LOG_ONCE( - LogLevel::kError, - "Ignoring excessive udp-connection input packets; (could this be a " - "flood attack?)."); - return; - } - - g_logic->thread()->PushCall( - [this, data, addr] { UDPConnectionPacket(data, addr); }); -} - -auto ConnectionSet::Shutdown() -> void { +void ConnectionSet::Shutdown() { // If we have any client/host connections, give them // a chance to shoot off disconnect packets or whatnot. for (auto& connection : connections_to_clients_) { connection.second->RequestDisconnect(); } - if (connection_to_host_.exists()) { + if (connection_to_host_.Exists()) { connection_to_host_->RequestDisconnect(); } } @@ -244,7 +231,7 @@ auto ConnectionSet::Shutdown() -> void { void ConnectionSet::SendScreenMessageToClients(const std::string& s, float r, float g, float b) { for (auto&& i : connections_to_clients_) { - if (i.second.exists() && i.second->can_communicate()) { + if (i.second.Exists() && i.second->can_communicate()) { i.second->SendScreenMessage(s, r, g, b); } } @@ -254,7 +241,7 @@ void ConnectionSet::SendScreenMessageToSpecificClients( const std::string& s, float r, float g, float b, const std::vector& clients) { for (auto&& i : connections_to_clients_) { - if (i.second.exists() && i.second->can_communicate()) { + if (i.second.Exists() && i.second->can_communicate()) { // Only send if this client is in our list. for (auto c : clients) { if (c == i.second->id()) { @@ -280,19 +267,22 @@ void ConnectionSet::SendScreenMessageToAll(const std::string& s, float r, ScreenMessage(s, {r, g, b}); } -auto ConnectionSet::PrepareForLaunchHostSession() -> void { +void ConnectionSet::PrepareForLaunchHostSession() { // If for some reason we're still attached to a host, kill the connection. - if (connection_to_host_.exists()) { + if (connection_to_host_.Exists()) { Log(LogLevel::kError, "Had host-connection during LaunchHostSession(); shouldn't happen."); connection_to_host_->RequestDisconnect(); connection_to_host_.Clear(); has_connection_to_host_ = false; - g_logic->UpdateGameRoster(); + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + appmode->UpdateGameRoster(); + } } } -auto ConnectionSet::HandleClientDisconnected(int id) -> void { +void ConnectionSet::HandleClientDisconnected(int id) { + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); auto i = connections_to_clients_.find(id); if (i != connections_to_clients_.end()) { bool was_connected = i->second->can_communicate(); @@ -307,9 +297,9 @@ auto ConnectionSet::HandleClientDisconnected(int id) -> void { // gone. Also inform everyone who just left so they can announce it // (technically could consolidate these messages but whatever...). if (was_connected) { - g_logic->UpdateGameRoster(); + appmode->UpdateGameRoster(); for (auto&& connection : connections_to_clients_) { - if (g_logic->ShouldAnnouncePartyJoinsAndLeaves()) { + if (appmode->ShouldAnnouncePartyJoinsAndLeaves()) { connection.second->SendReliableMessage(leave_msg); } } @@ -318,9 +308,9 @@ auto ConnectionSet::HandleClientDisconnected(int id) -> void { } auto ConnectionSet::DisconnectClient(int client_id, int ban_seconds) -> bool { - assert(InLogicThread()); + assert(g_base->InLogicThread()); - if (connection_to_host_.exists()) { + if (connection_to_host_.Exists()) { // Kick-votes first appeared in 14248 if (connection_to_host_->build_number() < 14248) { return false; @@ -343,7 +333,9 @@ auto ConnectionSet::DisconnectClient(int client_id, int ban_seconds) -> bool { // If this is considered a kick, add an entry to our banned list so we // know not to let them back in for a while. if (ban_seconds > 0) { - g_logic->BanPlayer(i->second->peer_spec(), 1000 * ban_seconds); + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + appmode->BanPlayer(i->second->peer_spec(), 1000 * ban_seconds); + } } i->second->RequestDisconnect(); @@ -358,24 +350,28 @@ auto ConnectionSet::DisconnectClient(int client_id, int ban_seconds) -> bool { } void ConnectionSet::PushClientDisconnectedCall(int id) { - g_logic->thread()->PushCall([this, id] { HandleClientDisconnected(id); }); + g_base->logic->event_loop()->PushCall( + [this, id] { HandleClientDisconnected(id); }); } void ConnectionSet::PushDisconnectedFromHostCall() { - g_logic->thread()->PushCall([this] { - if (connection_to_host_.exists()) { + g_base->logic->event_loop()->PushCall([this] { + if (connection_to_host_.Exists()) { bool was_connected = connection_to_host_->can_communicate(); connection_to_host_.Clear(); has_connection_to_host_ = false; // Clear out our party roster. - g_logic->UpdateGameRoster(); - // Go back to main menu *if* the connection was fully connected. - // Otherwise we're still probably sitting at the main menu - // so no need to reset it. - if (was_connected) { - g_logic->RunMainMenu(); + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + appmode->UpdateGameRoster(); + + // Go back to main menu *if* the connection was fully connected. + // Otherwise we're still probably sitting at the main menu + // so no need to reset it. + if (was_connected) { + appmode->RunMainMenu(); + } } } }); @@ -383,10 +379,12 @@ void ConnectionSet::PushDisconnectedFromHostCall() { void ConnectionSet::PushHostConnectedUDPCall(const SockAddr& addr, bool print_connect_progress) { - g_logic->thread()->PushCall([this, addr, print_connect_progress] { + g_base->logic->event_loop()->PushCall([this, addr, print_connect_progress] { // Attempt to disconnect any clients we have, turn off public-party // advertising, etc. - g_logic->CleanUpBeforeConnectingToHost(); + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + appmode->CleanUpBeforeConnectingToHost(); + } print_udp_connect_progress_ = print_connect_progress; connection_to_host_ = Object::New(addr); has_connection_to_host_ = true; @@ -395,15 +393,14 @@ void ConnectionSet::PushHostConnectedUDPCall(const SockAddr& addr, } void ConnectionSet::PushDisconnectFromHostCall() { - g_logic->thread()->PushCall([this] { - if (connection_to_host_.exists()) { + g_base->logic->event_loop()->PushCall([this] { + if (connection_to_host_.Exists()) { connection_to_host_->RequestDisconnect(); } }); } -auto ConnectionSet::UnregisterClientController(ClientControllerInterface* c) - -> void { +void ConnectionSet::UnregisterClientController(ClientControllerInterface* c) { assert(c); // This shouldn't happen. @@ -425,7 +422,7 @@ auto ConnectionSet::UnregisterClientController(ClientControllerInterface* c) void ConnectionSet::ForceDisconnectClients() { for (auto&& i : connections_to_clients_) { - if (ConnectionToClient* client = i.second.get()) { + if (ConnectionToClient* client = i.second.Get()) { client->RequestDisconnect(); } } @@ -434,9 +431,10 @@ void ConnectionSet::ForceDisconnectClients() { // Called for low level packets coming in pertaining to udp // host/client-connections. -auto ConnectionSet::UDPConnectionPacket(const std::vector& data_in, - const SockAddr& addr) -> void { +void ConnectionSet::HandleIncomingUDPPacket(const std::vector& data_in, + const SockAddr& addr) { assert(!data_in.empty()); + auto* appmode = SceneV1AppMode::GetActiveOrFatal(); const uint8_t* data = &(data_in[0]); auto data_size = static_cast(data_in.size()); @@ -468,7 +466,7 @@ auto ConnectionSet::UDPConnectionPacket(const std::vector& data_in, PushClientDisconnectedCall(client_id); // Now send an ack so they know it's been taken care of. - g_network_writer->PushSendToCall( + g_base->network_writer->PushSendToCall( {BA_PACKET_DISCONNECT_FROM_CLIENT_ACK, client_id}, addr); } break; @@ -495,7 +493,7 @@ auto ConnectionSet::UDPConnectionPacket(const std::vector& data_in, } // Now send an ack so they know it's been taken care of. - g_network_writer->PushSendToCall( + g_base->network_writer->PushSendToCall( {BA_PACKET_DISCONNECT_FROM_HOST_ACK, client_id}, addr); } break; @@ -521,7 +519,7 @@ auto ConnectionSet::UDPConnectionPacket(const std::vector& data_in, return; } else { // Send a disconnect request aimed at them. - g_network_writer->PushSendToCall( + g_base->network_writer->PushSendToCall( {BA_PACKET_DISCONNECT_FROM_HOST_REQUEST, client_id}, addr); } } @@ -567,7 +565,7 @@ auto ConnectionSet::UDPConnectionPacket(const std::vector& data_in, keep_trying = hc->SwitchProtocol(); if (!keep_trying) { if (!printed_host_disconnect_) { - ScreenMessage(g_logic->GetResourceString( + ScreenMessage(g_base->assets->GetResourceString( "connectionFailedVersionMismatchText"), {1, 0, 0}); printed_host_disconnect_ = true; @@ -576,15 +574,15 @@ auto ConnectionSet::UDPConnectionPacket(const std::vector& data_in, } else if (data[0] == BA_PACKET_CLIENT_DENY_PARTY_FULL) { if (!printed_host_disconnect_) { if (print_udp_connect_progress_) { - ScreenMessage( - g_logic->GetResourceString("connectionFailedPartyFullText"), - {1, 0, 0}); + ScreenMessage(g_base->assets->GetResourceString( + "connectionFailedPartyFullText"), + {1, 0, 0}); } printed_host_disconnect_ = true; } } else if (data[0] == BA_PACKET_CLIENT_DENY_ALREADY_IN_PARTY) { if (!printed_host_disconnect_) { - ScreenMessage(g_logic->GetResourceString( + ScreenMessage(g_base->assets->GetResourceString( "connectionFailedHostAlreadyInPartyText"), {1, 0, 0}); printed_host_disconnect_ = true; @@ -592,7 +590,7 @@ auto ConnectionSet::UDPConnectionPacket(const std::vector& data_in, } else { if (!printed_host_disconnect_) { ScreenMessage( - g_logic->GetResourceString("connectionRejectedText"), + g_base->assets->GetResourceString("connectionRejectedText"), {1, 0, 0}); printed_host_disconnect_ = true; } @@ -621,21 +619,21 @@ auto ConnectionSet::UDPConnectionPacket(const std::vector& data_in, std::string client_instance_uuid = &(client_instance_buffer[0]); if (static_cast(connections_to_clients_.size() + 1) - >= g_logic->public_party_max_size()) { + >= appmode->public_party_max_size()) { // If we've reached our party size limit (including ourself in that // count), reject. // Newer version have a specific party-full message; send that first // but also follow up with a generic deny message for older clients. - g_network_writer->PushSendToCall( + g_base->network_writer->PushSendToCall( {BA_PACKET_CLIENT_DENY_PARTY_FULL, request_id}, addr); - g_network_writer->PushSendToCall({BA_PACKET_CLIENT_DENY, request_id}, - addr); + g_base->network_writer->PushSendToCall( + {BA_PACKET_CLIENT_DENY, request_id}, addr); - } else if (connection_to_host_.exists()) { + } else if (connection_to_host_.Exists()) { // If we're connected to someone else, we can't have clients. - g_network_writer->PushSendToCall( + g_base->network_writer->PushSendToCall( {BA_PACKET_CLIENT_DENY_ALREADY_IN_PARTY, request_id}, addr); } else { // Otherwise go ahead and make them a new client connection. @@ -651,7 +649,7 @@ auto ConnectionSet::UDPConnectionPacket(const std::vector& data_in, } } } - if (!connection_to_client.exists()) { + if (!connection_to_client.Exists()) { // Create them a client object. // Try to find an unused client-id in the range 0-255. int client_id = 0; @@ -672,7 +670,7 @@ auto ConnectionSet::UDPConnectionPacket(const std::vector& data_in, std::vector msg_out(2); msg_out[0] = BA_PACKET_CLIENT_DENY; msg_out[1] = request_id; - g_network_writer->PushSendToCall(msg_out, addr); + g_base->network_writer->PushSendToCall(msg_out, addr); Log(LogLevel::kError, "All client slots full; really?.."); break; } @@ -690,7 +688,7 @@ auto ConnectionSet::UDPConnectionPacket(const std::vector& data_in, msg_out[1] = static_cast_check_fit(connection_to_client->id()); msg_out[2] = request_id; - g_network_writer->PushSendToCall(msg_out, addr); + g_base->network_writer->PushSendToCall(msg_out, addr); } } break; @@ -738,10 +736,12 @@ void ConnectionSet::SetClientInfoFromMasterServer( client->HandleMasterServerClientInfo(info_obj); // Roster will now include account-id... - g_logic->mark_game_roster_dirty(); + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + appmode->MarkGameRosterDirty(); + } break; } } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/logic/connection/connection_set.h b/src/ballistica/scene_v1/connection/connection_set.h similarity index 62% rename from src/ballistica/logic/connection/connection_set.h rename to src/ballistica/scene_v1/connection/connection_set.h index 2d44e1da..4decd65b 100644 --- a/src/ballistica/logic/connection/connection_set.h +++ b/src/ballistica/scene_v1/connection/connection_set.h @@ -1,15 +1,17 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_LOGIC_CONNECTION_CONNECTION_SET_H_ -#define BALLISTICA_LOGIC_CONNECTION_CONNECTION_SET_H_ +#ifndef BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_SET_H_ +#define BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_SET_H_ #include #include #include -#include "ballistica/core/object.h" +#include "ballistica/base/base.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::scene_v1 { class ConnectionSet { public: @@ -18,19 +20,19 @@ class ConnectionSet { // Whoever wants to wrangle current client connections should call this // to register itself. Note that it must explicitly call unregister when // unregistering itself. - auto RegisterClientController(ClientControllerInterface* c) -> void; - auto UnregisterClientController(ClientControllerInterface* c) -> void; + void RegisterClientController(ClientControllerInterface* c); + void UnregisterClientController(ClientControllerInterface* c); // Quick test as to whether there are clients. Does not check if they are // fully connected. - auto has_connection_to_clients() const -> bool { - assert(InLogicThread()); + auto HasConnectionToClients() const -> bool { + assert(g_base->InLogicThread()); return (!connections_to_clients_.empty()); } // Returns our host-connection or nullptr if there is none. auto connection_to_host() -> ConnectionToHost* { - return connection_to_host_.get(); + return connection_to_host_.Get(); } auto GetConnectionToHostUDP() -> ConnectionToHostUDP*; @@ -47,22 +49,22 @@ class ConnectionSet { return has_connection_to_host_; } - auto Update() -> void; - auto Shutdown() -> void; - auto PrepareForLaunchHostSession() -> void; - auto HandleClientDisconnected(int id) -> void; + void Update(); + void Shutdown(); + void PrepareForLaunchHostSession(); + void HandleClientDisconnected(int id); // Returns true if disconnect attempts are supported. auto DisconnectClient(int client_id, int ban_seconds) -> bool; - auto ForceDisconnectClients() -> void; - auto PushHostConnectedUDPCall(const SockAddr& addr, - bool print_connect_progress) -> void; - auto PushDisconnectFromHostCall() -> void; - auto PushDisconnectedFromHostCall() -> void; + void ForceDisconnectClients(); + void PushHostConnectedUDPCall(const SockAddr& addr, + bool print_connect_progress); + void PushDisconnectFromHostCall(); + void PushDisconnectedFromHostCall(); auto GetPrintUDPConnectProgress() const -> bool { return print_udp_connect_progress_; } - auto PushUDPConnectionPacketCall(const std::vector& data, - const SockAddr& addr) -> void; + void PushIncomingUDPPacketCall(const std::vector& data, + const SockAddr& addr); // Return our client connections (if any). // FIXME: this prunes invalid connections, but it is necessary? // Can we just use connections_to_clients() for direct access? @@ -72,31 +74,29 @@ class ConnectionSet { auto GetConnectedClientCount() const -> int; // For applying player-profiles data from the master-server. - auto SetClientInfoFromMasterServer(const std::string& client_token, - PyObject* info) -> void; + void SetClientInfoFromMasterServer(const std::string& client_token, + PyObject* info); - auto SendChatMessage(const std::string& message, + void SendChatMessage(const std::string& message, const std::vector* clients = nullptr, - const std::string* sender_override = nullptr) -> void; + const std::string* sender_override = nullptr); // Send a screen message to all connected clients AND print it on the host. - auto SendScreenMessageToAll(const std::string& s, float r, float g, float b) - -> void; + void SendScreenMessageToAll(const std::string& s, float r, float g, float b); // send a screen message to all connected clients - auto SendScreenMessageToClients(const std::string& s, float r, float g, - float b) -> void; + void SendScreenMessageToClients(const std::string& s, float r, float g, + float b); // Send a screen message to specific connected clients (those matching the IDs // specified) the id -1 can be used to specify the host. - auto SendScreenMessageToSpecificClients(const std::string& s, float r, + void SendScreenMessageToSpecificClients(const std::string& s, float r, float g, float b, - const std::vector& clients) - -> void; + const std::vector& clients); - auto UDPConnectionPacket(const std::vector& data, - const SockAddr& addr) -> void; - auto PushClientDisconnectedCall(int id) -> void; + void HandleIncomingUDPPacket(const std::vector& data_in, + const SockAddr& addr); + void PushClientDisconnectedCall(int id); private: auto VerifyClientAddr(uint8_t client_id, const SockAddr& addr) -> bool; @@ -116,6 +116,6 @@ class ConnectionSet { bool printed_host_disconnect_{}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_LOGIC_CONNECTION_CONNECTION_SET_H_ +#endif // BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_SET_H_ diff --git a/src/ballistica/logic/connection/connection_to_client.cc b/src/ballistica/scene_v1/connection/connection_to_client.cc similarity index 78% rename from src/ballistica/logic/connection/connection_to_client.cc rename to src/ballistica/scene_v1/connection/connection_to_client.cc index 10d52a4c..c5cf2fb9 100644 --- a/src/ballistica/logic/connection/connection_to_client.cc +++ b/src/ballistica/scene_v1/connection/connection_to_client.cc @@ -1,22 +1,26 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/logic/connection/connection_to_client.h" +#include "ballistica/scene_v1/connection/connection_to_client.h" -#include "ballistica/assets/assets.h" -#include "ballistica/audio/audio.h" -#include "ballistica/generic/json.h" -#include "ballistica/input/device/client_input_device.h" -#include "ballistica/internal/app_internal.h" -#include "ballistica/logic/client_controller_interface.h" -#include "ballistica/logic/connection/connection_set.h" -#include "ballistica/logic/logic.h" -#include "ballistica/logic/player.h" -#include "ballistica/logic/session/host_session.h" -#include "ballistica/networking/networking.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_sys.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/networking/networking.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/support/plus_soft.h" +#include "ballistica/core/python/core_python.h" +#include "ballistica/scene_v1/connection/connection_set.h" +#include "ballistica/scene_v1/python/scene_v1_python.h" +#include "ballistica/scene_v1/support/client_controller_interface.h" +#include "ballistica/scene_v1/support/client_input_device.h" +#include "ballistica/scene_v1/support/client_input_device_delegate.h" +#include "ballistica/scene_v1/support/host_session.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/shared/generic/json.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" -namespace ballistica { +namespace ballistica::scene_v1 { // How long new clients have to wait before starting a kick vote. const int kNewClientKickVoteDelay = 60000; @@ -65,22 +69,26 @@ ConnectionToClient::~ConnectionToClient() { // If we had made any input-devices, they're just pointers that // we have to pass along to g_input to delete for us. for (auto&& i : client_input_devices_) { - g_input->RemoveInputDevice(i.second, false); + g_base->input->RemoveInputDevice(i.second, false); } // If they had been announced as connected, announce their departure. - if (can_communicate() && g_logic->ShouldAnnouncePartyJoinsAndLeaves()) { - std::string s = g_logic->GetResourceString("playerLeftPartyText"); + // It's also expected our app mode may no longer be active here; that's ok. + auto* appmode = SceneV1AppMode::GetActive(); + if (appmode && can_communicate() + && appmode->ShouldAnnouncePartyJoinsAndLeaves()) { + std::string s = g_base->assets->GetResourceString("playerLeftPartyText"); Utils::StringReplaceOne(&s, "${NAME}", peer_spec().GetDisplayString()); ScreenMessage(s, {1, 0.5f, 0.0f}); - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kCorkPop)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kCorkPop)); } } void ConnectionToClient::Update() { Connection::Update(); // Handles common stuff. - millisecs_t real_time = GetRealTime(); + millisecs_t real_time = g_core->GetAppTimeMillisecs(); // If we're waiting for handshake response still, keep sending out handshake // attempts. @@ -98,7 +106,7 @@ void ConnectionToClient::Update() { std::string out = dict.PrintUnformatted(); std::vector data(3 + out.size()); - data[0] = BA_GAMEPACKET_HANDSHAKE; + data[0] = BA_SCENEPACKET_HANDSHAKE; uint16_t val = kProtocolVersion; memcpy(data.data() + 1, &val, sizeof(val)); memcpy(data.data() + 3, out.c_str(), out.size()); @@ -108,7 +116,7 @@ void ConnectionToClient::Update() { // on older protocols, we simply embedded our spec-string as the second // part of the handshake packet std::vector data(3 + our_handshake_player_spec_str_.size()); - data[0] = BA_GAMEPACKET_HANDSHAKE; + data[0] = BA_SCENEPACKET_HANDSHAKE; uint16_t val = kProtocolVersion; memcpy(data.data() + 1, &val, sizeof(val)); memcpy(data.data() + 3, our_handshake_player_spec_str_.c_str(), @@ -123,7 +131,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { // If we've errored, just respond to everything with 'GO AWAY!'. if (errored()) { std::vector data2(1); - data2[0] = BA_GAMEPACKET_DISCONNECT; + data2[0] = BA_SCENEPACKET_DISCONNECT; SendGamePacket(data2); return; } @@ -132,11 +140,17 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { Log(LogLevel::kError, "ConnectionToClient got data size 0."); return; } + + auto* appmode = SceneV1AppMode::GetActiveOrWarn(); + if (!appmode) { + return; + } + switch (data[0]) { - case BA_GAMEPACKET_HANDSHAKE_RESPONSE: { + case BA_SCENEPACKET_HANDSHAKE_RESPONSE: { // We sent the client a handshake and they're responding. if (data.size() < 3) { - Log(LogLevel::kError, "got invalid BA_GAMEPACKET_HANDSHAKE_RESPONSE"); + Log(LogLevel::kError, "got invalid BA_SCENEPACKET_HANDSHAKE_RESPONSE"); return; } @@ -174,7 +188,7 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { // Compare this against our blocked specs.. if there's a match, reject // them. - if (g_logic->IsPlayerBanned(peer_spec())) { + if (appmode->IsPlayerBanned(peer_spec())) { Error(""); return; } @@ -193,7 +207,8 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { // connection attempt so this will only apply to things like Google // Play invites where we probably want to be more verbose as // to why the game just died. - s = g_logic->GetResourceString("incompatibleVersionPlayerText"); + s = g_base->assets->GetResourceString( + "incompatibleVersionPlayerText"); Utils::StringReplaceOne(&s, "${NAME}", peer_spec().GetDisplayString()); } @@ -207,20 +222,24 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { set_can_communicate(true); // Don't allow fresh clients to start kick votes for a while. - next_kick_vote_allow_time_ = GetRealTime() + kNewClientKickVoteDelay; + next_kick_vote_allow_time_ = + g_core->GetAppTimeMillisecs() + kNewClientKickVoteDelay; // At this point we have their name, so lets announce their arrival. - if (g_logic->ShouldAnnouncePartyJoinsAndLeaves()) { - std::string s = g_logic->GetResourceString("playerJoinedPartyText"); + if (appmode->ShouldAnnouncePartyJoinsAndLeaves()) { + std::string s = + g_base->assets->GetResourceString("playerJoinedPartyText"); Utils::StringReplaceOne(&s, "${NAME}", peer_spec().GetDisplayString()); ScreenMessage(s, {0.5f, 1, 0.5f}); - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kGunCock)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kGunCock)); } // Also mark the time for flashing the 'someone just joined your // party' message in the corner. - g_logic->set_last_connection_to_client_join_time(GetRealTime()); + appmode->set_last_connection_to_client_join_time( + g_core->GetAppTimeMillisecs()); // Added midway through protocol 29: // We now send a json dict of info about ourself first thing. This @@ -231,13 +250,13 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { { cJSON* info_dict = cJSON_CreateObject(); cJSON_AddItemToObject(info_dict, "b", - cJSON_CreateNumber(kAppBuildNumber)); + cJSON_CreateNumber(kEngineBuildNumber)); // Add a name entry if we've got a public party name set. - if (!g_logic->public_party_name().empty()) { + if (!appmode->public_party_name().empty()) { cJSON_AddItemToObject( info_dict, "n", - cJSON_CreateString(g_logic->public_party_name().c_str())); + cJSON_CreateString(appmode->public_party_name().c_str())); } std::string info = cJSON_PrintUnformatted(info_dict); cJSON_Delete(info_dict); @@ -253,23 +272,23 @@ void ConnectionToClient::HandleGamePacket(const std::vector& data) { join_msg[0] = BA_MESSAGE_PARTY_MEMBER_JOINED; memcpy(&(join_msg[1]), joiner_spec.c_str(), joiner_spec.size()); - for (auto&& i : g_logic->connections()->connections_to_clients()) { + for (auto&& i : appmode->connections()->connections_to_clients()) { // Also send a 'party-member-joined' notification to all clients // *except* the new one. - if (i.second.exists() && i.second.get() != this - && g_logic->ShouldAnnouncePartyJoinsAndLeaves()) { + if (i.second.Exists() && i.second.Get() != this + && appmode->ShouldAnnouncePartyJoinsAndLeaves()) { i.second->SendReliableMessage(join_msg); } } // Update the game party roster and send it to all clients (including // this new one). - g_logic->UpdateGameRoster(); + appmode->UpdateGameRoster(); // Lastly, we hand this connection over to whoever is currently // feeding client connections. - if (g_logic->connections()->client_controller()) { - SetController(g_logic->connections()->client_controller()); + if (appmode->connections()->client_controller()) { + SetController(appmode->connections()->client_controller()); } } break; @@ -295,7 +314,8 @@ void ConnectionToClient::SendScreenMessage(const std::string& s, float r, // Older clients don't support the screen-message message, so in that case // we just send it as a chat-message from . if (build_number() < 14248) { - std::string value = g_logic->CompileResourceString(s, "sendScreenMessage"); + std::string value = + g_base->assets->CompileResourceString(s, "sendScreenMessage"); std::string our_spec_string = PlayerSpec::GetDummyPlayerSpec("").GetSpecString(); std::vector msg_out(1 + 1 + our_spec_string.size() + value.size()); @@ -326,6 +346,11 @@ void ConnectionToClient::HandleMessagePacket( return; } + auto* appmode = SceneV1AppMode::GetActiveOrWarn(); + if (!appmode) { + return; + } + // If the first message we get is not client-info, it means we're talking to // an older client that won't be sending us info. if (!got_client_info_ && buffer[0] != BA_MESSAGE_CLIENT_INFO) { @@ -347,10 +372,10 @@ void ConnectionToClient::HandleMessagePacket( case BA_MESSAGE_KICK_VOTE: { if (buffer.size() == 2) { - for (auto&& i : g_logic->connections()->connections_to_clients()) { - ConnectionToClient* client = i.second.get(); + for (auto&& i : appmode->connections()->connections_to_clients()) { + ConnectionToClient* client = i.second.Get(); if (client->id() == static_cast(buffer[1])) { - g_logic->StartKickVote(this, client); + appmode->StartKickVote(this, client); break; } } @@ -391,7 +416,7 @@ void ConnectionToClient::HandleMessagePacket( if (!token_.empty()) { // Kick off a query to the master-server for this client's info. // FIXME: we need to add retries for this in case of failure. - g_app_internal->ClientInfoQuery( + g_base->Plus()->ClientInfoQuery( token_, our_handshake_player_spec_str_ + our_handshake_salt_, peer_hash_, build_number_); } @@ -411,15 +436,16 @@ void ConnectionToClient::HandleMessagePacket( // Newer type using json. // Only accept peer info if we've not gotten official info from // the master server (and if we're allowing it in general). - if (!g_logic->require_client_authentication() + if (!appmode->require_client_authentication() && !got_info_from_master_server_) { std::vector b2(buffer.size()); memcpy(&(b2[0]), &(buffer[1]), buffer.size() - 1); b2[buffer.size() - 1] = 0; PythonRef args(Py_BuildValue("(s)", b2.data()), PythonRef::kSteal); - PythonRef results = - g_python->obj(Python::ObjID::kJsonLoadsCall).Call(args); - if (results.exists()) { + PythonRef results = g_core->python->objs() + .Get(core::CorePython::ObjID::kJsonLoadsCall) + .Call(args); + if (results.Exists()) { player_profiles_ = results; } } @@ -444,7 +470,7 @@ void ConnectionToClient::HandleMessagePacket( case BA_MESSAGE_CHAT: { // We got a chat message from a client. - millisecs_t now = GetRealTime(); + millisecs_t now = g_core->GetAppTimeMillisecs(); // Ignore this if they're chat blocked. if (now >= chat_block_time_) { @@ -463,14 +489,14 @@ void ConnectionToClient::HandleMessagePacket( // If we require client-info and don't have it from this guy yet, // ignore their chat messages (prevent bots from jumping in and // spamming before we can verify their identities) - if (g_logic->require_client_authentication() + if (appmode->require_client_authentication() && !got_info_from_master_server_) { Log(LogLevel::kError, "Ignoring chat message from peer with no client info."); SendScreenMessage(R"({"r":"loadingTryAgainText"})", 1, 0, 0); } else if (last_chat_times_.size() >= 5) { chat_block_time_ = now + next_chat_block_seconds_ * 1000; - g_logic->connections()->SendScreenMessageToAll( + appmode->connections()->SendScreenMessageToAll( R"({"r":"internal.chatBlockedText","s":[["${NAME}",)" + Utils::GetJSONString( GetCombinedSpec().GetDisplayString().c_str()) @@ -495,6 +521,9 @@ void ConnectionToClient::HandleMessagePacket( } b2[msg_len] = 0; + bool kick_vote_in_progress{}; + kick_vote_in_progress = appmode->kick_vote_in_progress(); + // Clamp messages at a reasonable size // (yes, people used this to try and crash machines). if (b2.size() > 100) { @@ -502,7 +531,7 @@ void ConnectionToClient::HandleMessagePacket( "{\"t\":[\"serverResponses\"," "\"Message is too long.\"]}", 1, 0, 0); - } else if (g_logic->kick_vote_in_progress() + } else if (kick_vote_in_progress && (!strcmp(b2.data(), "1") || !strcmp(b2.data(), "2"))) { // Special case - if there's a kick vote going on, take '1' or @@ -520,7 +549,7 @@ void ConnectionToClient::HandleMessagePacket( // If the filter tells us to ignore it, we're done. std::string message = b2.data(); bool allow_message = - g_python->FilterChatMessage(&message, id()); + g_scene_v1->python->FilterChatMessage(&message, id()); if (!allow_message) { break; } @@ -539,14 +568,14 @@ void ConnectionToClient::HandleMessagePacket( // Send it out to all clients. for (auto&& i : - g_logic->connections()->connections_to_clients()) { + appmode->connections()->connections_to_clients()) { if (i.second->can_communicate()) { i.second->SendReliableMessage(msg_out); } } // Display it locally. - g_logic->LocalDisplayChatMessage(msg_out); + appmode->LocalDisplayChatMessage(msg_out); } } } @@ -576,16 +605,25 @@ void ConnectionToClient::HandleMessagePacket( } case BA_MESSAGE_REMOVE_REMOTE_PLAYER: { - last_remove_player_time_ = GetRealTime(); + last_remove_player_time_ = g_core->GetAppTimeMillisecs(); if (buffer.size() != 2) { Log(LogLevel::kError, "Error: invalid remove-remote-player packet"); break; } if (ClientInputDevice* cid = GetClientInputDevice(buffer[1])) { - if (Player* player = cid->GetPlayer()) { - HostSession* host_session = player->GetHostSession(); - if (!host_session) throw Exception("Player's host-session not found"); - host_session->RemovePlayer(player); + // It should have one of our special client delegates attached. + if (auto* cid_delegate = + dynamic_cast(&cid->delegate())) { + if (Player* player = cid_delegate->GetPlayer()) { + HostSession* host_session = player->GetHostSession(); + if (host_session) { + throw Exception("Player's host-session not found"); + } + host_session->RemovePlayer(player); + } + } else { + Log(LogLevel::kError, + "Unable to get ClientInputDevice for remove-remote-player msg."); } } break; @@ -601,11 +639,19 @@ void ConnectionToClient::HandleMessagePacket( // and submit a player-request on it's behalf. ClientInputDevice* cid = GetClientInputDevice(buffer[1]); + // It should have one of our special client delegates attached. + auto* cid_d = dynamic_cast(&cid->delegate()); + if (!cid_d) { + Log(LogLevel::kError, + "Can't get client-input-device-delegate in request-remote-player " + "msg."); + break; + } if (auto* hs = - dynamic_cast(g_logic->GetForegroundSession())) { - if (!cid->attached_to_player()) { + dynamic_cast(appmode->GetForegroundSession())) { + if (!cid->AttachedToPlayer()) { millisecs_t seconds_since_last_left = - (GetRealTime() - last_remove_player_time_) / 1000; + (g_core->GetAppTimeMillisecs() - last_remove_player_time_) / 1000; int min_seconds_since_left = 10; // If someone on this connection left less than 10 seconds ago, @@ -619,12 +665,13 @@ void ConnectionToClient::HandleMessagePacket( + "\"]]}", 1, 1, 0); } else { - bool still_waiting = (g_logic->require_client_authentication() + bool still_waiting = (appmode->require_client_authentication() && !got_info_from_master_server_); // If we're not allowing peer client-info and have yet to get // master-server info for this client, delay their join (we'll // eventually give up and just give them a blank slate). - if (still_waiting && (GetRealTime() - creation_time() < 10000)) { + if (still_waiting + && (g_core->GetAppTimeMillisecs() - creation_time() < 10000)) { SendScreenMessage( "{\"v\":\"${A}...\",\"s\":[[\"${A}\",{\"r\":" "\"loadingTryAgainText\",\"f\":\"loadingText\"}]]}", @@ -637,7 +684,7 @@ void ConnectionToClient::HandleMessagePacket( "info (build " + std::to_string(build_number_) + ")"); } - hs->RequestPlayer(cid); + hs->RequestPlayer(cid_d); } } } @@ -659,7 +706,7 @@ void ConnectionToClient::HandleMessagePacket( Log(LogLevel::kError, "Client data limit exceeded by '" + peer_spec().GetShortName() + "'; kicking."); - g_logic->BanPlayer(peer_spec(), 1000 * 60); + appmode->BanPlayer(peer_spec(), 1000 * 60); Error(""); return; } @@ -671,23 +718,26 @@ void ConnectionToClient::HandleMessagePacket( } auto ConnectionToClient::GetCombinedSpec() -> PlayerSpec { + auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + // Look for players coming from this client-connection. // If we find any, make a spec out of their name(s). - if (auto* hs = dynamic_cast(g_logic->GetForegroundSession())) { + if (auto* hs = dynamic_cast(appmode->GetForegroundSession())) { std::string p_name_combined; for (auto&& p : hs->players()) { - InputDevice* input_device = p->GetInputDevice(); + auto* delegate = p->input_device_delegate(); if (!p->GetName().empty() && p->name_is_real() && p->accepted() - && input_device != nullptr && input_device->IsRemoteClient()) { - auto* cid = static_cast(input_device); - ConnectionToClient* ctc = cid->connection_to_client(); + && delegate != nullptr && delegate->IsRemoteClient()) { + if (auto* cid = dynamic_cast(delegate)) { + ConnectionToClient* ctc = cid->connection_to_client(); - // Add some basic info for each remote player. - if (ctc != nullptr && ctc == this) { - if (!p_name_combined.empty()) { - p_name_combined += "/"; + // Add some basic info for each remote player. + if (ctc != nullptr && ctc == this) { + if (!p_name_combined.empty()) { + p_name_combined += "/"; + } + p_name_combined += p->GetName(); } - p_name_combined += p->GetName(); } } } @@ -709,11 +759,10 @@ auto ConnectionToClient::GetClientInputDevice(int remote_id) -> ClientInputDevice* { auto i = client_input_devices_.find(remote_id); if (i == client_input_devices_.end()) { - // InputDevices need to be manually allocated and passed to g_input to - // store. + // InputDevices get allocated as deferred and passed to g_input to store. auto cid = Object::NewDeferred(remote_id, this); client_input_devices_[remote_id] = cid; - g_input->AddInputDevice(cid, false); + g_base->input->AddInputDevice(cid, false); return cid; } return i->second; @@ -724,6 +773,8 @@ auto ConnectionToClient::GetAsUDP() -> ConnectionToClientUDP* { } void ConnectionToClient::HandleMasterServerClientInfo(PyObject* info_obj) { + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + PyObject* profiles_obj = PyDict_GetItemString(info_obj, "p"); if (profiles_obj != nullptr) { player_profiles_.Acquire(profiles_obj); @@ -732,15 +783,15 @@ void ConnectionToClient::HandleMasterServerClientInfo(PyObject* info_obj) { // This will also contain a public account-id (if the query was valid). // Store it away for whoever wants it. PyObject* public_id_obj = PyDict_GetItemString(info_obj, "u"); - if (public_id_obj != nullptr && Python::IsPyString(public_id_obj)) { - peer_public_account_id_ = Python::GetPyString(public_id_obj); + if (public_id_obj != nullptr && g_base->python->IsPyLString(public_id_obj)) { + peer_public_account_id_ = g_base->python->GetPyLString(public_id_obj); } else { peer_public_account_id_ = ""; // If the server returned no valid account info for them // and we're not trusting peers, kick this fella right out // and ban him for a short bit (to hopefully limit rejoin spam). - if (g_logic->require_client_authentication()) { + if (appmode->require_client_authentication()) { SendScreenMessage( "{\"t\":[\"serverResponses\"," "\"Your account was rejected. Are you signed in?\"]}", @@ -759,11 +810,12 @@ void ConnectionToClient::HandleMasterServerClientInfo(PyObject* info_obj) { } auto ConnectionToClient::IsAdmin() const -> bool { + auto* appmode = SceneV1AppMode::GetActiveOrFatal(); if (peer_public_account_id_.empty()) { return false; } - return (g_logic->admin_public_ids().find(peer_public_account_id_) - != g_logic->admin_public_ids().end()); + return (appmode->admin_public_ids().find(peer_public_account_id_) + != appmode->admin_public_ids().end()); } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/logic/connection/connection_to_client.h b/src/ballistica/scene_v1/connection/connection_to_client.h similarity index 89% rename from src/ballistica/logic/connection/connection_to_client.h rename to src/ballistica/scene_v1/connection/connection_to_client.h index be548a94..bf27b1af 100644 --- a/src/ballistica/logic/connection/connection_to_client.h +++ b/src/ballistica/scene_v1/connection/connection_to_client.h @@ -1,15 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_LOGIC_CONNECTION_CONNECTION_TO_CLIENT_H_ -#define BALLISTICA_LOGIC_CONNECTION_CONNECTION_TO_CLIENT_H_ +#ifndef BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_TO_CLIENT_H_ +#define BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_TO_CLIENT_H_ #include #include #include -#include "ballistica/logic/connection/connection.h" +#include "ballistica/scene_v1/connection/connection.h" +#include "ballistica/scene_v1/scene_v1.h" -namespace ballistica { +namespace ballistica::scene_v1 { /// Connection to a party client if we're the host. class ConnectionToClient : public Connection { @@ -24,7 +25,7 @@ class ConnectionToClient : public Connection { // More efficient than dynamic_cast (hmm do we still want this?). virtual auto GetAsUDP() -> ConnectionToClientUDP*; void SetController(ClientControllerInterface* c); - auto GetPlayerProfiles() const -> PyObject* { return player_profiles_.get(); } + auto GetPlayerProfiles() const -> PyObject* { return player_profiles_.Get(); } auto build_number() const -> int { return build_number_; } void SendScreenMessage(const std::string& s, float r = 1.0f, float g = 1.0f, float b = 1.0f); @@ -81,6 +82,6 @@ class ConnectionToClient : public Connection { int next_chat_block_seconds_{10}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_LOGIC_CONNECTION_CONNECTION_TO_CLIENT_H_ +#endif // BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_TO_CLIENT_H_ diff --git a/src/ballistica/logic/connection/connection_to_client_udp.cc b/src/ballistica/scene_v1/connection/connection_to_client_udp.cc similarity index 71% rename from src/ballistica/logic/connection/connection_to_client_udp.cc rename to src/ballistica/scene_v1/connection/connection_to_client_udp.cc index 4dbbf839..424e09a9 100644 --- a/src/ballistica/logic/connection/connection_to_client_udp.cc +++ b/src/ballistica/scene_v1/connection/connection_to_client_udp.cc @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/logic/connection/connection_to_client_udp.h" +#include "ballistica/scene_v1/connection/connection_to_client_udp.h" -#include "ballistica/logic/connection/connection_set.h" -#include "ballistica/logic/logic.h" -#include "ballistica/networking/network_writer.h" -#include "ballistica/networking/sockaddr.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/networking/network_writer.h" +#include "ballistica/scene_v1/connection/connection_set.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" -namespace ballistica { +namespace ballistica::scene_v1 { ConnectionToClientUDP::ConnectionToClientUDP(const SockAddr& addr, std::string client_name, @@ -16,7 +16,8 @@ ConnectionToClientUDP::ConnectionToClientUDP(const SockAddr& addr, request_id_(request_id), addr_(new SockAddr(addr)), client_instance_uuid_(std::move(client_name)), - last_client_response_time_(g_logic->master_time()), + last_client_response_time_millisecs_( + static_cast(g_base->logic->display_time() * 1000.0)), did_die_(false) {} ConnectionToClientUDP::~ConnectionToClientUDP() { @@ -39,17 +40,17 @@ void ConnectionToClientUDP::SendGamePacketCompressed( // Ship this off to the net-out thread to send; at this point we don't know // or case what happens to it. - assert(g_network_writer); - g_network_writer->PushSendToCall(data_full, *addr_); + assert(g_base->network_writer); + g_base->network_writer->PushSendToCall(data_full, *addr_); } void ConnectionToClientUDP::Update() { ConnectionToClient::Update(); - - millisecs_t current_time = g_logic->master_time(); + auto current_time_millisecs = + static_cast(g_base->logic->display_time() * 1000.0); // if its been long enough since we've heard anything from the host, error. - if (current_time - last_client_response_time_ + if (current_time_millisecs - last_client_response_time_millisecs_ > (can_communicate() ? 10000u : 5000u)) { // die immediately in this case; no use trying to wait for a // disconnect-ack since we've already given up hope of hearing from them.. @@ -60,7 +61,8 @@ void ConnectionToClientUDP::Update() { void ConnectionToClientUDP::HandleGamePacket( const std::vector& buffer) { // keep track of when we last heard from the host for disconnect purposes - last_client_response_time_ = g_logic->master_time(); + last_client_response_time_millisecs_ = + static_cast(g_base->logic->display_time() * 1000.0); ConnectionToClient::HandleGamePacket(buffer); } @@ -70,7 +72,9 @@ void ConnectionToClientUDP::Die() { return; } // this will actually clear the object.. - g_logic->connections()->PushClientDisconnectedCall(id()); + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + appmode->connections()->PushClientDisconnectedCall(id()); + } did_die_ = true; } @@ -89,7 +93,7 @@ void ConnectionToClientUDP::SendDisconnectRequest() { std::vector data(2); data[0] = BA_PACKET_DISCONNECT_FROM_HOST_REQUEST; data[1] = static_cast(id()); - g_network_writer->PushSendToCall(data, *addr_); + g_base->network_writer->PushSendToCall(data, *addr_); } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/logic/connection/connection_to_client_udp.h b/src/ballistica/scene_v1/connection/connection_to_client_udp.h similarity index 60% rename from src/ballistica/logic/connection/connection_to_client_udp.h rename to src/ballistica/scene_v1/connection/connection_to_client_udp.h index c6a99237..39d85667 100644 --- a/src/ballistica/logic/connection/connection_to_client_udp.h +++ b/src/ballistica/scene_v1/connection/connection_to_client_udp.h @@ -1,17 +1,17 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_LOGIC_CONNECTION_CONNECTION_TO_CLIENT_UDP_H_ -#define BALLISTICA_LOGIC_CONNECTION_CONNECTION_TO_CLIENT_UDP_H_ +#ifndef BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_TO_CLIENT_UDP_H_ +#define BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_TO_CLIENT_UDP_H_ #include #include #include -#include "ballistica/logic/connection/connection_to_client.h" -#include "ballistica/networking/networking.h" -#include "ballistica/networking/sockaddr.h" +#include "ballistica/base/networking/networking.h" +#include "ballistica/scene_v1/connection/connection_to_client.h" +#include "ballistica/shared/networking/sockaddr.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Connection to a party client if we're the host. class ConnectionToClientUDP : public ConnectionToClient { @@ -26,8 +26,7 @@ class ConnectionToClientUDP : public ConnectionToClient { void RequestDisconnect() override; void Die(); void SendDisconnectRequest(); - auto SendGamePacketCompressed(const std::vector& data) - -> void override; + void SendGamePacketCompressed(const std::vector& data) override; auto addr() { return *addr_; } private: @@ -35,9 +34,9 @@ class ConnectionToClientUDP : public ConnectionToClient { std::unique_ptr addr_; std::string client_instance_uuid_; bool did_die_; - millisecs_t last_client_response_time_; + millisecs_t last_client_response_time_millisecs_; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_LOGIC_CONNECTION_CONNECTION_TO_CLIENT_UDP_H_ +#endif // BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_TO_CLIENT_UDP_H_ diff --git a/src/ballistica/logic/connection/connection_to_host.cc b/src/ballistica/scene_v1/connection/connection_to_host.cc similarity index 73% rename from src/ballistica/logic/connection/connection_to_host.cc rename to src/ballistica/scene_v1/connection/connection_to_host.cc index 88726023..0f51d8b0 100644 --- a/src/ballistica/logic/connection/connection_to_host.cc +++ b/src/ballistica/scene_v1/connection/connection_to_host.cc @@ -1,22 +1,23 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/logic/connection/connection_to_host.h" +#include "ballistica/scene_v1/connection/connection_to_host.h" -#include "ballistica/assets/assets.h" -#include "ballistica/audio/audio.h" -#include "ballistica/generic/json.h" -#include "ballistica/input/device/input_device.h" -#include "ballistica/input/input.h" -#include "ballistica/internal/app_internal.h" -#include "ballistica/logic/logic.h" -#include "ballistica/logic/session/net_client_session.h" -#include "ballistica/math/vector3f.h" -#include "ballistica/networking/networking.h" -#include "ballistica/platform/platform.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_sys.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/input/device/input_device.h" +#include "ballistica/base/networking/networking.h" +#include "ballistica/base/platform/base_platform.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/support/plus_soft.h" +#include "ballistica/core/python/core_python.h" +#include "ballistica/scene_v1/support/client_session_net.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" +#include "ballistica/shared/generic/json.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python_sys.h" -namespace ballistica { +namespace ballistica::scene_v1 { // How long to go between sending out null packets for pings. const int kPingSendInterval = 2000; @@ -35,23 +36,24 @@ ConnectionToHost::~ConnectionToHost() { // '${PEER-NAME}'s party'. std::string s; if (!party_name_.empty()) { - s = g_logic->GetResourceString("leftGameText"); + s = g_base->assets->GetResourceString("leftGameText"); Utils::StringReplaceOne(&s, "${NAME}", party_name_); } else { - s = g_logic->GetResourceString("leftPartyText"); + s = g_base->assets->GetResourceString("leftPartyText"); Utils::StringReplaceOne(&s, "${NAME}", peer_spec().GetDisplayString()); } ScreenMessage(s, {1, 0.5f, 0.0f}); - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kCorkPop)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kCorkPop)); } else { - ScreenMessage(g_logic->GetResourceString("connectionRejectedText"), + ScreenMessage(g_base->assets->GetResourceString("connectionRejectedText"), {1, 0, 0}); } } } void ConnectionToHost::Update() { - millisecs_t real_time = GetRealTime(); + millisecs_t real_time = g_core->GetAppTimeMillisecs(); // Send out null messages occasionally for ping measurement purposes. // Note that we currently only do this from the client since we might not @@ -85,11 +87,13 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { } switch (data[0]) { - case BA_GAMEPACKET_HANDSHAKE: { + case BA_SCENEPACKET_HANDSHAKE: { if (data.size() <= 3) { break; } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + // We expect a > 3 byte handshake packet with protocol version as the // second and third bytes and name/info beyond that. // (player-spec for protocol <= 32 and info json dict for 33+). @@ -121,12 +125,12 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { // Also add our public device id. Servers can // use this to combat spammers. - dict.AddString("d", g_platform->GetPublicDeviceUUID()); + dict.AddString("d", g_base->platform->GetPublicDeviceUUID()); std::string out = dict.PrintUnformatted(); std::vector data2(3 + out.size()); - data2[0] = BA_GAMEPACKET_HANDSHAKE_RESPONSE; + data2[0] = BA_SCENEPACKET_HANDSHAKE_RESPONSE; auto val = static_cast(protocol_version_); memcpy(data2.data() + 1, &val, sizeof(val)); memcpy(data2.data() + 3, out.c_str(), out.size()); @@ -136,7 +140,7 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { std::string our_spec_str = PlayerSpec::GetAccountPlayerSpec().GetSpecString(); std::vector response(3 + our_spec_str.size()); - response[0] = BA_GAMEPACKET_HANDSHAKE_RESPONSE; + response[0] = BA_SCENEPACKET_HANDSHAKE_RESPONSE; auto val = static_cast(protocol_version_); memcpy(response.data() + 1, &val, sizeof(val)); memcpy(response.data() + 3, our_spec_str.c_str(), our_spec_str.size()); @@ -145,9 +149,11 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { if (!compatible) { if (their_protocol_version > kProtocolVersion) { - Error(g_logic->GetResourceString("incompatibleNewerVersionHostText")); + Error(g_base->assets->GetResourceString( + "incompatibleNewerVersionHostText")); } else { - Error(g_logic->GetResourceString("incompatibleVersionHostText")); + Error( + g_base->assets->GetResourceString("incompatibleVersionHostText")); } return; } @@ -191,10 +197,10 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { set_peer_spec(PlayerSpec(string_buffer.data())); } - peer_hash_ = g_app_internal->CalcV1PeerHash(peer_hash_input_); + peer_hash_ = g_base->Plus()->CalcV1PeerHash(peer_hash_input_); set_can_communicate(true); - g_logic->LaunchClientSession(); + appmode->LaunchClientSession(); // NOTE: // we don't actually print a 'connected' message until after @@ -204,7 +210,7 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { // Wire ourselves up to drive the client-session we're in. auto* cs = - dynamic_cast(g_logic->GetForegroundSession()); + dynamic_cast(appmode->GetForegroundSession()); assert(cs); assert(!cs->connection_to_host()); client_session_ = cs; @@ -214,9 +220,9 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { // which is a json dict with arbitrary data. { JsonDict dict; - dict.AddNumber("b", kAppBuildNumber); + dict.AddNumber("b", kEngineBuildNumber); - g_app_internal->V1SetClientInfo(&dict); + g_base->Plus()->V1SetClientInfo(&dict); // Pass the hash we generated from their handshake; they can use // this to make sure we're who we say we are. @@ -235,13 +241,14 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { // On newer hosts we send these as json. if (protocol_version_ >= 32) { // (This is a borrowed ref) - PyObject* profiles = g_python->GetRawConfigValue("Player Profiles"); + PyObject* profiles = + g_base->python->GetRawConfigValue("Player Profiles"); PythonRef empty_dict; if (!profiles) { Log(LogLevel::kError, "No profiles found; sending empty list to host"); empty_dict.Steal(PyDict_New()); - profiles = empty_dict.get(); + profiles = empty_dict.Get(); } if (profiles != nullptr) { // Dump them to a json string. @@ -249,14 +256,16 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { PythonRef keywds(Py_BuildValue("{s(ss)}", "separators", ",", ":"), PythonRef::kSteal); PythonRef results = - g_python->obj(Python::ObjID::kJsonDumpsCall).Call(args, keywds); - if (!results.exists()) { + g_core->python->objs() + .Get(core::CorePython::ObjID::kJsonDumpsCall) + .Call(args, keywds); + if (!results.Exists()) { Log(LogLevel::kError, "Error getting json dump of local profiles"); } else { try { // Pull the string as utf8 and send. - std::string s = results.ValueAsString(); + std::string s = results.ValueAsLString(); std::vector msg(s.size() + 1); msg[0] = BA_MESSAGE_CLIENT_PLAYER_PROFILES_JSON; memcpy(&(msg[1]), &s[0], s.size()); @@ -276,7 +285,7 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { break; } - case BA_GAMEPACKET_DISCONNECT: { + case BA_SCENEPACKET_DISCONNECT: { // They told us to leave, so lets do so :-( ErrorSilent(); break; @@ -294,7 +303,7 @@ void ConnectionToHost::HandleGamePacket(const std::vector& data) { #pragma clang diagnostic pop void ConnectionToHost::HandleMessagePacket(const std::vector& buffer) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (buffer.empty()) { Log(LogLevel::kError, "Got invalid HandleMessagePacket"); @@ -344,7 +353,9 @@ void ConnectionToHost::HandleMessagePacket(const std::vector& buffer) { cJSON* new_roster = cJSON_Parse(reinterpret_cast(&(buffer[1]))); if (new_roster) { - g_logic->SetGameRoster(new_roster); + if (auto* appmode = SceneV1AppMode::GetActive()) { + appmode->SetGameRoster(new_roster); + } } } break; @@ -395,11 +406,13 @@ void ConnectionToHost::HandleMessagePacket(const std::vector& buffer) { std::vector str_buffer(buffer.size()); memcpy(&(str_buffer[0]), &(buffer[1]), buffer.size() - 1); str_buffer[str_buffer.size() - 1] = 0; - std::string s = g_logic->GetResourceString("playerJoinedPartyText"); + std::string s = + g_base->assets->GetResourceString("playerJoinedPartyText"); Utils::StringReplaceOne( &s, "${NAME}", PlayerSpec(str_buffer.data()).GetDisplayString()); ScreenMessage(s, {0.5f, 1.0f, 0.5f}); - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kGunCock)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kGunCock)); } break; } @@ -410,11 +423,13 @@ void ConnectionToHost::HandleMessagePacket(const std::vector& buffer) { std::vector str_buffer(buffer.size()); memcpy(&(str_buffer[0]), &(buffer[1]), buffer.size() - 1); str_buffer[str_buffer.size() - 1] = 0; - std::string s = g_logic->GetResourceString("playerLeftPartyText"); + std::string s = + g_base->assets->GetResourceString("playerLeftPartyText"); Utils::StringReplaceOne( &s, "${NAME}", PlayerSpec(&(str_buffer[0])).GetDisplayString()); ScreenMessage(s, {1, 0.5f, 0.0f}); - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kCorkPop)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kCorkPop)); } break; } @@ -428,12 +443,21 @@ void ConnectionToHost::HandleMessagePacket(const std::vector& buffer) { // Grab this local input-device and tell it its controlling something on // the host. - InputDevice* input_device = g_input->GetInputDevice(buffer[1]); + base::InputDevice* input_device = + g_base->input->GetInputDevice(buffer[1]); if (input_device) { - uint32_t player_id; - memcpy(&player_id, &(buffer[2]), sizeof(player_id)); - input_device->AttachToRemotePlayer( - this, static_cast_check_fit(player_id)); + // We expect this device to be rocking our delegate type. + if (auto* delegate = dynamic_cast( + &input_device->delegate())) { + uint32_t player_id; + memcpy(&player_id, &(buffer[2]), sizeof(player_id)); + delegate->AttachToRemotePlayer(this, + static_cast_check_fit(player_id)); + } else { + Log(LogLevel::kError, + "InputDevice does not have a SceneV1 delegate as expected " + "(loc1)."); + } } // Once we've gotten one of these we know to ignore the old style. @@ -455,16 +479,27 @@ void ConnectionToHost::HandleMessagePacket(const std::vector& buffer) { // Grab this local input-device and tell it its controlling something // on the host. - InputDevice* input_device = g_input->GetInputDevice(buffer[1]); + base::InputDevice* input_device = + g_base->input->GetInputDevice(buffer[1]); if (input_device) { - input_device->AttachToRemotePlayer(this, buffer[2]); + // We expect this device to be rocking our delegate type. + if (auto* delegate = dynamic_cast( + &input_device->delegate())) { + delegate->AttachToRemotePlayer(this, buffer[2]); + } else { + Log(LogLevel::kError, + "InputDevice does not have a SceneV1 delegate as expected " + "(loc2)."); + } } } break; } case BA_MESSAGE_CHAT: { - g_logic->LocalDisplayChatMessage(buffer); + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + appmode->LocalDisplayChatMessage(buffer); + } break; } @@ -473,9 +508,34 @@ void ConnectionToHost::HandleMessagePacket(const std::vector& buffer) { Log(LogLevel::kError, "Invalid detach-remote-player msg"); break; } - InputDevice* input_device = g_input->GetInputDevice(buffer[1]); - if (input_device && input_device->GetRemotePlayer() == this) - input_device->DetachFromPlayer(); + // Server is telling us that our local input device is no longer + // controlling a player. + base::InputDevice* input_device = + g_base->input->GetInputDevice(buffer[1]); + if (input_device) { + // We expect this device to be rocking our delegate type. + if (auto* delegate = dynamic_cast( + &input_device->delegate())) { + auto* connection_to_host = delegate->GetRemotePlayer(); + if (connection_to_host == this) { + // Normally detaching triggers a message to the server, + // but that would be redundant. This will prevent that. + delegate->InvalidateConnectionToHost(); + + delegate->DetachFromPlayer(); + + } else { + // If we detached from our end, connection-to-host will already + // be cleared out at this point. Just complain if that's not + // the case. + if (connection_to_host != nullptr) { + Log(LogLevel::kError, + "InputDevice does not have a SceneV1 delegate as expected " + "(loc3)."); + } + } + } + } break; } @@ -483,7 +543,7 @@ void ConnectionToHost::HandleMessagePacket(const std::vector& buffer) { case BA_MESSAGE_SESSION_RESET: case BA_MESSAGE_SESSION_DYNAMICS_CORRECTION: { // These commands are consumed directly by the session. - if (client_session_.exists()) { + if (client_session_.Exists()) { client_session_->HandleSessionMessage(buffer); } break; @@ -502,17 +562,18 @@ void ConnectionToHost::HandleMessagePacket(const std::vector& buffer) { // If we've got a name for their party, use it; otherwise call it // '${NAME}'s party'. if (!party_name_.empty()) { - s = g_logic->GetResourceString("connectedToGameText"); + s = g_base->assets->GetResourceString("connectedToGameText"); Utils::StringReplaceOne(&s, "${NAME}", party_name_); } else { - s = g_logic->GetResourceString("connectedToPartyText"); + s = g_base->assets->GetResourceString("connectedToPartyText"); Utils::StringReplaceOne(&s, "${NAME}", peer_spec().GetDisplayString()); } ScreenMessage(s, {0.5f, 1, 0.5f}); - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kGunCock)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kGunCock)); printed_connect_message_ = true; } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/logic/connection/connection_to_host.h b/src/ballistica/scene_v1/connection/connection_to_host.h similarity index 79% rename from src/ballistica/logic/connection/connection_to_host.h rename to src/ballistica/scene_v1/connection/connection_to_host.h index 73a2c41f..978e8da1 100644 --- a/src/ballistica/logic/connection/connection_to_host.h +++ b/src/ballistica/scene_v1/connection/connection_to_host.h @@ -1,14 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_LOGIC_CONNECTION_CONNECTION_TO_HOST_H_ -#define BALLISTICA_LOGIC_CONNECTION_CONNECTION_TO_HOST_H_ +#ifndef BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_TO_HOST_H_ +#define BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_TO_HOST_H_ #include #include -#include "ballistica/logic/connection/connection.h" +#include "ballistica/scene_v1/connection/connection.h" +#include "ballistica/scene_v1/scene_v1.h" -namespace ballistica { +namespace ballistica::scene_v1 { // connection to the party host if we're a client class ConnectionToHost : public Connection { @@ -43,6 +44,6 @@ class ConnectionToHost : public Connection { Object::WeakRef client_session_; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_LOGIC_CONNECTION_CONNECTION_TO_HOST_H_ +#endif // BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_TO_HOST_H_ diff --git a/src/ballistica/logic/connection/connection_to_host_udp.cc b/src/ballistica/scene_v1/connection/connection_to_host_udp.cc similarity index 68% rename from src/ballistica/logic/connection/connection_to_host_udp.cc rename to src/ballistica/scene_v1/connection/connection_to_host_udp.cc index 8e332355..d73a7800 100644 --- a/src/ballistica/logic/connection/connection_to_host_udp.cc +++ b/src/ballistica/scene_v1/connection/connection_to_host_udp.cc @@ -1,14 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/logic/connection/connection_to_host_udp.h" +#include "ballistica/scene_v1/connection/connection_to_host_udp.h" -#include "ballistica/logic/connection/connection_set.h" -#include "ballistica/logic/logic.h" -#include "ballistica/math/vector3f.h" -#include "ballistica/networking/network_writer.h" -#include "ballistica/networking/sockaddr.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/networking/network_writer.h" +#include "ballistica/scene_v1/connection/connection_set.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/shared/math/vector3f.h" +#include "ballistica/shared/networking/sockaddr.h" -namespace ballistica { +namespace ballistica::scene_v1 { auto ConnectionToHostUDP::SwitchProtocol() -> bool { if (protocol_version() > kProtocolVersionMin) { @@ -28,10 +30,13 @@ ConnectionToHostUDP::ConnectionToHostUDP(const SockAddr& addr) last_client_id_request_time_(0), last_disconnect_request_time_(0), did_die_(false), - last_host_response_time_(g_logic->master_time()) { + last_host_response_time_millisecs_( + static_cast(g_base->logic->display_time() * 1000.0)) { GetRequestID(); - if (g_logic->connections()->GetPrintUDPConnectProgress()) { - ScreenMessage(g_logic->GetResourceString("connectingToPartyText")); + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (appmode->connections()->GetPrintUDPConnectProgress()) { + ScreenMessage(g_base->assets->GetResourceString("connectingToPartyText")); + } } } @@ -53,34 +58,40 @@ void ConnectionToHostUDP::GetRequestID() { void ConnectionToHostUDP::Update() { ConnectionToHost::Update(); + auto* appmode = SceneV1AppMode::GetActiveOrWarn(); + if (!appmode) { + return; + } - millisecs_t current_time = g_logic->master_time(); + auto current_time_millisecs = + static_cast(g_base->logic->display_time() * 1000.0); // If we've not gotten a client_id from the host yet, keep pestering it. if (!errored()) { - if (client_id_ == -1 && current_time - last_client_id_request_time_ > 500) { - last_client_id_request_time_ = current_time; + if (client_id_ == -1 + && current_time_millisecs - last_client_id_request_time_ > 500) { + last_client_id_request_time_ = current_time_millisecs; // Client request packet: contains our protocol version (2 bytes), our // request id (1 byte), and our session-identifier (remainder of the // message). - const std::string& uuid{GetAppInstanceUUID()}; + const std::string& uuid{g_base->GetAppInstanceUUID()}; std::vector msg(4 + uuid.size()); msg[0] = BA_PACKET_CLIENT_REQUEST; auto p_version = static_cast(protocol_version()); memcpy(&(msg[1]), &p_version, 2); msg[3] = request_id_; memcpy(&(msg[4]), uuid.c_str(), uuid.size()); - g_network_writer->PushSendToCall(msg, *addr_); + g_base->network_writer->PushSendToCall(msg, *addr_); } } // If its been long enough since we've heard anything from the host, error. - if (current_time - last_host_response_time_ + if (current_time_millisecs - last_host_response_time_millisecs_ > (can_communicate() ? 10000u : 5000u)) { // If the connection never got established, announce it failed. if (!can_communicate()) { - ScreenMessage(g_logic->GetResourceString("connectionFailedText"), + ScreenMessage(g_base->assets->GetResourceString("connectionFailedText"), {1, 0, 0}); } @@ -91,8 +102,8 @@ void ConnectionToHostUDP::Update() { } else if (errored()) { // If we've errored, keep sending disconnect-requests periodically. // Once we get a response (or time out in the above code) we'll die. - if (current_time - last_disconnect_request_time_ > 1000) { - last_disconnect_request_time_ = current_time; + if (current_time_millisecs - last_disconnect_request_time_ > 1000) { + last_disconnect_request_time_ = current_time_millisecs; // If we haven't even got a client id yet, we can't send disconnect // requests; just die. @@ -113,13 +124,15 @@ void ConnectionToHostUDP::Die() { Log(LogLevel::kError, "Posting multiple die messages; probably not good."); return; } - if (g_logic->connections()->connection_to_host() == this) { - g_logic->connections()->PushDisconnectedFromHostCall(); - did_die_ = true; - } else { - Log(LogLevel::kError, - "Running update for non-current host-connection; shouldn't " - "happen."); + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (appmode->connections()->connection_to_host() == this) { + appmode->connections()->PushDisconnectedFromHostCall(); + did_die_ = true; + } else { + Log(LogLevel::kError, + "Running update for non-current host-connection; shouldn't " + "happen."); + } } } @@ -129,13 +142,14 @@ void ConnectionToHostUDP::SendDisconnectRequest() { std::vector data(2); data[0] = BA_PACKET_DISCONNECT_FROM_CLIENT_REQUEST; data[1] = static_cast_check_fit(client_id_); - g_network_writer->PushSendToCall(data, *addr_); + g_base->network_writer->PushSendToCall(data, *addr_); } } void ConnectionToHostUDP::HandleGamePacket(const std::vector& buffer) { // Keep track of when we last heard from the host for time-out purposes. - last_host_response_time_ = g_logic->master_time(); + last_host_response_time_millisecs_ = + static_cast(g_base->logic->display_time() * 1000.0); ConnectionToHost::HandleGamePacket(buffer); } @@ -153,8 +167,8 @@ void ConnectionToHostUDP::SendGamePacketCompressed( // Ship this off to the net-out thread to send; at this point we don't know // or care what happens to it. - assert(g_network_writer); - g_network_writer->PushSendToCall(data_full, *addr_); + assert(g_base->network_writer); + g_base->network_writer->PushSendToCall(data_full, *addr_); } void ConnectionToHostUDP::Error(const std::string& msg) { @@ -183,4 +197,4 @@ void ConnectionToHostUDP::RequestDisconnect() { } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/logic/connection/connection_to_host_udp.h b/src/ballistica/scene_v1/connection/connection_to_host_udp.h similarity index 72% rename from src/ballistica/logic/connection/connection_to_host_udp.h rename to src/ballistica/scene_v1/connection/connection_to_host_udp.h index 40de91e0..b085b759 100644 --- a/src/ballistica/logic/connection/connection_to_host_udp.h +++ b/src/ballistica/scene_v1/connection/connection_to_host_udp.h @@ -1,16 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_LOGIC_CONNECTION_CONNECTION_TO_HOST_UDP_H_ -#define BALLISTICA_LOGIC_CONNECTION_CONNECTION_TO_HOST_UDP_H_ +#ifndef BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_TO_HOST_UDP_H_ +#define BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_TO_HOST_UDP_H_ #include #include #include -#include "ballistica/logic/connection/connection_to_host.h" -#include "ballistica/networking/networking.h" +#include "ballistica/base/networking/networking.h" +#include "ballistica/scene_v1/connection/connection_to_host.h" -namespace ballistica { +namespace ballistica::scene_v1 { class ConnectionToHostUDP : public ConnectionToHost { public: @@ -41,9 +41,9 @@ class ConnectionToHostUDP : public ConnectionToHost { millisecs_t last_client_id_request_time_{}; millisecs_t last_disconnect_request_time_{}; int client_id_{}; - millisecs_t last_host_response_time_{}; + millisecs_t last_host_response_time_millisecs_{}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_LOGIC_CONNECTION_CONNECTION_TO_HOST_UDP_H_ +#endif // BALLISTICA_SCENE_V1_CONNECTION_CONNECTION_TO_HOST_UDP_H_ diff --git a/src/ballistica/dynamics/collision.h b/src/ballistica/scene_v1/dynamics/collision.h similarity index 74% rename from src/ballistica/dynamics/collision.h rename to src/ballistica/scene_v1/dynamics/collision.h index 8f93fc47..65add53b 100644 --- a/src/ballistica/dynamics/collision.h +++ b/src/ballistica/scene_v1/dynamics/collision.h @@ -1,16 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_COLLISION_H_ -#define BALLISTICA_DYNAMICS_COLLISION_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_COLLISION_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_COLLISION_H_ #include -#include "ballistica/ballistica.h" -#include "ballistica/core/object.h" -#include "ballistica/dynamics/material/material_context.h" +#include "ballistica/scene_v1/dynamics/material/material_context.h" +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/foundation/object.h" #include "ode/ode.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Stores info about an occurring collision. // Note than just because a collision exists between two parts doesn't mean @@ -39,6 +39,6 @@ class Collision : public Object { MaterialContext dst_context; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_COLLISION_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_COLLISION_H_ diff --git a/src/ballistica/dynamics/dynamics.cc b/src/ballistica/scene_v1/dynamics/dynamics.cc similarity index 92% rename from src/ballistica/dynamics/dynamics.cc rename to src/ballistica/scene_v1/dynamics/dynamics.cc index 3b9aa0b1..c6adcf6f 100644 --- a/src/ballistica/dynamics/dynamics.cc +++ b/src/ballistica/scene_v1/dynamics/dynamics.cc @@ -1,20 +1,21 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/dynamics.h" +#include "ballistica/scene_v1/dynamics/dynamics.h" -#include "ballistica/assets/component/sound.h" -#include "ballistica/audio/audio.h" -#include "ballistica/audio/audio_source.h" -#include "ballistica/dynamics/collision.h" -#include "ballistica/dynamics/collision_cache.h" -#include "ballistica/dynamics/material/material_action.h" -#include "ballistica/dynamics/part.h" -#include "ballistica/graphics/renderer.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/audio/audio_source.h" +#include "ballistica/base/dynamics/collision_cache.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/core/core.h" +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/dynamics/collision.h" +#include "ballistica/scene_v1/dynamics/material/material_action.h" +#include "ballistica/scene_v1/dynamics/part.h" +#include "ballistica/scene_v1/support/scene.h" #include "ode/ode_collision_kernel.h" #include "ode/ode_collision_util.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Max contacts for rigid body collisions. // TODO(ericf): Probably a good idea to accept more than this @@ -116,14 +117,13 @@ class Dynamics::Impl { // gcc currently chokes on unordered_maps with forward-declared types, // so we can't have this in our header without pushing all our map/collision // types there too. - auto HandleDisconnect( - const std::unordered_map< - int64_t, ballistica::Dynamics::SrcNodeCollideMap>::iterator& i, - const std::unordered_map< - int64_t, ballistica::Dynamics::DstNodeCollideMap>::iterator& j, + void HandleDisconnect( + const std::unordered_map::iterator& + i, + const std::unordered_map::iterator& + j, const std::unordered_map::iterator& k, - const std::unordered_map >::iterator& l) - -> void; + const std::unordered_map >::iterator& l); private: Dynamics* dynamics_{}; @@ -134,7 +134,7 @@ class Dynamics::Impl { Dynamics::Dynamics(Scene* scene_in) : scene_(scene_in), - collision_cache_(new CollisionCache()), + collision_cache_(new base::CollisionCache()), impl_(std::make_unique(this)) { ResetODE(); } @@ -148,7 +148,7 @@ Dynamics::~Dynamics() { ShutdownODE(); } -void Dynamics::Draw(FrameDef* frame_def) { +void Dynamics::Draw(base::FrameDef* frame_def) { // draw collisions if desired.. #if BA_DEBUG_BUILD && 0 SimpleComponent c(frame_def->overlay_3d_pass()); @@ -158,7 +158,7 @@ void Dynamics::Draw(FrameDef* frame_def) { c.PushTransform(); c.Translate(i.x(), i.y(), i.z()); c.scaleUniform(0.05f); - c.DrawModel(g_assets->GetModel(Assets::BOX_MODEL)); + c.DrawMeshAsset(g_assets->GetMesh(Assets::BOX_MESH)); c.PopTransform(); } c.Submit(); @@ -258,7 +258,7 @@ auto Dynamics::GetCollision(Part* p1_in, Part* p2_in, MaterialContext** cc1, // If it didnt exist, go ahead and set up the collision. if (i.second) { i.first->second = Object::New(scene_); - new_collision = i.first->second.get(); + new_collision = i.first->second.Get(); } else { new_collision = nullptr; } @@ -321,10 +321,8 @@ auto Dynamics::GetCollision(Part* p1_in, Part* p2_in, MaterialContext** cc1, } void Dynamics::Impl::HandleDisconnect( - const std::unordered_map< - int64_t, ballistica::Dynamics::SrcNodeCollideMap>::iterator& i, - const std::unordered_map< - int64_t, ballistica::Dynamics::DstNodeCollideMap>::iterator& j, + const std::unordered_map::iterator& i, + const std::unordered_map::iterator& j, const std::unordered_map::iterator& k, const std::unordered_map >::iterator& l) { // Handle disconnect equivalents if they were colliding. @@ -332,8 +330,8 @@ void Dynamics::Impl::HandleDisconnect( // Add the contexts' disconnect commands to be executed. for (auto m = l->second->src_context.disconnect_actions.begin(); m != l->second->src_context.disconnect_actions.end(); m++) { - Part* src_part = l->second->src_part.get(); - Part* dst_part = l->second->dst_part.get(); + Part* src_part = l->second->src_part.Get(); + Part* dst_part = l->second->dst_part.Get(); dynamics_->collision_events_.emplace_back( src_part ? src_part->node() : nullptr, dst_part ? dst_part->node() : nullptr, *m, l->second); @@ -341,8 +339,8 @@ void Dynamics::Impl::HandleDisconnect( for (auto m = l->second->dst_context.disconnect_actions.begin(); m != l->second->dst_context.disconnect_actions.end(); m++) { - Part* src_part = l->second->src_part.get(); - Part* dst_part = l->second->dst_part.get(); + Part* src_part = l->second->src_part.Get(); + Part* dst_part = l->second->dst_part.Get(); dynamics_->collision_events_.emplace_back( dst_part ? dst_part->node() : nullptr, src_part ? src_part->node() : nullptr, *m, l->second); @@ -352,14 +350,14 @@ void Dynamics::Impl::HandleDisconnect( // tell them they're no longer colliding with the other. bool physical = l->second->src_context.physical && l->second->dst_context.physical; - Part* p1 = l->second->dst_part.get(); - Part* p2 = l->second->src_part.get(); + Part* p1 = l->second->dst_part.Get(); + Part* p2 = l->second->src_part.Get(); if (p1) { - assert(p1 == l->second->dst_part.get()); + assert(p1 == l->second->dst_part.Get()); p1->SetCollidingWith(i->first, k->first, false, physical); // NOLINT } if (p2) { - assert(p2 == l->second->src_part.get()); + assert(p2 == l->second->src_part.Get()); } if (p2 && (p2 != p1)) { p2->SetCollidingWith(j->first, l->first, false, physical); // NOLINT @@ -501,10 +499,10 @@ void Dynamics::ProcessCollisions() { // Execute all events that we built up due to collisions. for (auto&& i : collision_events_) { - active_collision_ = i.collision.get(); + active_collision_ = i.collision.Get(); active_collide_src_node_ = i.node1; active_collide_dst_node_ = i.node2; - i.action->Execute(i.node1.get(), i.node2.get(), scene_); + i.action->Execute(i.node1.Get(), i.node2.Get(), scene_); } active_collision_ = nullptr; collision_events_.clear(); @@ -512,7 +510,8 @@ void Dynamics::ProcessCollisions() { void Dynamics::process() { in_process_ = true; - real_time_ = GetRealTime(); // Update this once so we can recycle results. + // Update this once so we can recycle results. + real_time_ = g_core->GetAppTimeMillisecs(); ProcessCollisions(); dWorldQuickStep(ode_world_, kGameStepSeconds); dJointGroupEmpty(ode_contact_group_); @@ -872,8 +871,9 @@ void Dynamics::CollideCallback(dGeomID o1, dGeomID o2) { : 1.0f; if (volume > 1) volume = 1; - assert(i.sound.exists()); - if (AudioSource* source = g_audio->SourceBeginNew()) { + assert(i.sound.Exists()); + if (base::AudioSource* source = + g_base->audio->SourceBeginNew()) { source->SetGain(volume * i.volume); source->SetPosition(apx, apy, apz); source->Play(i.sound->GetSoundData()); @@ -903,7 +903,8 @@ void Dynamics::CollideCallback(dGeomID o1, dGeomID o2) { // If we're already playing, just adjust volume // and position - otherwise get a sound started. if (i.playing) { - AudioSource* s = g_audio->SourceBeginExisting(i.play_id, 101); + base::AudioSource* s = + g_base->audio->SourceBeginExisting(i.play_id, 101); if (s) { s->SetGain(volume * i.volume); s->SetPosition(apx, apy, apz); @@ -914,8 +915,9 @@ void Dynamics::CollideCallback(dGeomID o1, dGeomID o2) { } } else if (real_time - p1->last_skid_sound_time() >= 250 || real_time - p2->last_skid_sound_time() > 250) { - assert(i.sound.exists()); - if (AudioSource* source = g_audio->SourceBeginNew()) { + assert(i.sound.Exists()); + if (base::AudioSource* source = + g_base->audio->SourceBeginNew()) { source->SetLooping(true); source->SetGain(volume * i.volume); source->SetPosition(apx, apy, apz); @@ -929,7 +931,7 @@ void Dynamics::CollideCallback(dGeomID o1, dGeomID o2) { } else { // Skid values are low - stop any playing skid sounds. if (i.playing) { - g_audio->PushSourceFadeOutCall(i.play_id, 200); + g_base->audio->PushSourceFadeOutCall(i.play_id, 200); i.playing = false; } } @@ -953,7 +955,8 @@ void Dynamics::CollideCallback(dGeomID o1, dGeomID o2) { // If we're already playing, just adjust volume // and position; otherwise get a sound started. if (i.playing) { - AudioSource* s = g_audio->SourceBeginExisting(i.play_id, 102); + base::AudioSource* s = + g_base->audio->SourceBeginExisting(i.play_id, 102); if (s) { s->SetGain(volume * i.volume); s->SetPosition(apx, apy, apz); @@ -964,8 +967,9 @@ void Dynamics::CollideCallback(dGeomID o1, dGeomID o2) { } } else if (real_time - p1->last_roll_sound_time() >= 250 || real_time - p2->last_roll_sound_time() > 250) { - assert(i.sound.exists()); - if (AudioSource* source = g_audio->SourceBeginNew()) { + assert(i.sound.Exists()); + if (base::AudioSource* source = + g_base->audio->SourceBeginNew()) { source->SetLooping(true); source->SetGain(volume * i.volume); source->SetPosition(apx, apy, apz); @@ -979,7 +983,7 @@ void Dynamics::CollideCallback(dGeomID o1, dGeomID o2) { } else { // roll values are low - stop any playing roll sounds if (i.playing) { - g_audio->PushSourceFadeOutCall(i.play_id, 200); + g_base->audio->PushSourceFadeOutCall(i.play_id, 200); i.playing = false; } } @@ -1010,8 +1014,8 @@ void Dynamics::CollideCallback(dGeomID o1, dGeomID o2) { if (play_collide_sounds) { for (auto&& i : cc1->connect_sounds) { - assert(i.sound.exists()); - if (AudioSource* source = g_audio->SourceBeginNew()) { + assert(i.sound.Exists()); + if (base::AudioSource* source = g_base->audio->SourceBeginNew()) { source->SetPosition(apx, apy, apz); source->SetGain(i.volume); source->Play(i.sound->GetSoundData()); @@ -1019,8 +1023,8 @@ void Dynamics::CollideCallback(dGeomID o1, dGeomID o2) { } } for (auto&& i : cc2->connect_sounds) { - assert(i.sound.exists()); - if (AudioSource* source = g_audio->SourceBeginNew()) { + assert(i.sound.Exists()); + if (base::AudioSource* source = g_base->audio->SourceBeginNew()) { source->SetPosition(apx, apy, apz); source->SetGain(i.volume); source->Play(i.sound->GetSoundData()); @@ -1135,4 +1139,4 @@ void Dynamics::ResetODE() { dRandSetSeed(5432); } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/dynamics/dynamics.h b/src/ballistica/scene_v1/dynamics/dynamics.h similarity index 72% rename from src/ballistica/dynamics/dynamics.h rename to src/ballistica/scene_v1/dynamics/dynamics.h index 61584782..9e29780c 100644 --- a/src/ballistica/dynamics/dynamics.h +++ b/src/ballistica/scene_v1/dynamics/dynamics.h @@ -1,30 +1,31 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_DYNAMICS_H_ -#define BALLISTICA_DYNAMICS_DYNAMICS_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_DYNAMICS_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_DYNAMICS_H_ #include #include #include -#include "ballistica/core/object.h" +#include "ballistica/base/base.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" #include "ode/ode.h" -namespace ballistica { +namespace ballistica::scene_v1 { class Dynamics : public Object { public: explicit Dynamics(Scene* scene_in); ~Dynamics() override; - auto Draw(FrameDef* frame_def) -> void; // Draw any debug stuff, etc. + void Draw(base::FrameDef* frame_def); // Draw any debug stuff, etc. auto ode_world() -> dWorldID { return ode_world_; } auto getContactGroup() -> dJointGroupID { return ode_contact_group_; } auto space() -> dSpaceID { return ode_space_; } // Discontinues a collision. Used by parts when changing materials // so that new collisions may enter effect. - auto ResetCollision(int64_t node1, int part1, int64_t node2, int part2) - -> void; + void ResetCollision(int64_t node1, int part1, int64_t node2, int part2); // Used by collision callbacks - internal. auto active_collision() const -> Collision* { return active_collision_; } @@ -34,39 +35,39 @@ class Dynamics : public Object { assert(active_collision_); return (collide_message_reverse_order_ ? active_collide_dst_node_ : active_collide_src_node_) - .get(); + .Get(); } // Used by collision callbacks - internal. auto GetActiveCollideDstNode() -> Node* { assert(active_collision_); return (collide_message_reverse_order_ ? active_collide_src_node_ : active_collide_dst_node_) - .get(); + .Get(); } auto GetCollideMessageReverseOrder() const -> bool { return collide_message_reverse_order_; } // Used by collide message handlers. - auto set_collide_message_state(bool inCollideMessageIn, - bool target_other_in = false) -> void { + void set_collide_message_state(bool inCollideMessageIn, + bool target_other_in = false) { in_collide_message_ = inCollideMessageIn; collide_message_reverse_order_ = target_other_in; } auto in_collide_message() const -> bool { return in_collide_message_; } - auto process() -> void; - auto increment_skid_sound_count() -> void { skid_sound_count_++; } - auto decrement_skid_sound_count() -> void { skid_sound_count_--; } + void process(); + void increment_skid_sound_count() { skid_sound_count_++; } + void decrement_skid_sound_count() { skid_sound_count_--; } auto skid_sound_count() const -> int { return skid_sound_count_; } - auto incrementRollSoundCount() -> void { roll_sound_count_++; } - auto decrement_roll_sound_count() -> void { roll_sound_count_--; } + void incrementRollSoundCount() { roll_sound_count_++; } + void decrement_roll_sound_count() { roll_sound_count_--; } auto getRollSoundCount() const -> int { return roll_sound_count_; } // We do some fancy collision testing stuff for trimeshes instead // of going through regular ODE space collision testing.. so we have // to keep track of these ourself. - auto AddTrimesh(dGeomID g) -> void; - auto RemoveTrimesh(dGeomID g) -> void; + void AddTrimesh(dGeomID g); + void RemoveTrimesh(dGeomID g); auto collision_count() const -> int { return collision_count_; } auto process_real_time() const -> millisecs_t { return real_time_; } @@ -91,11 +92,11 @@ class Dynamics : public Object { MaterialContext** cc2) -> Collision*; std::vector collision_events_; - auto ResetODE() -> void; - auto ShutdownODE() -> void; - static auto DoCollideCallback(void* data, dGeomID o1, dGeomID o2) -> void; - auto CollideCallback(dGeomID o1, dGeomID o2) -> void; - auto ProcessCollisions() -> void; + void ResetODE(); + void ShutdownODE(); + static void DoCollideCallback(void* data, dGeomID o1, dGeomID o2); + void CollideCallback(dGeomID o1, dGeomID o2); + void ProcessCollisions(); std::unique_ptr impl_; bool processing_collisions_{}; @@ -115,10 +116,10 @@ class Dynamics : public Object { Collision* active_collision_{}; Object::WeakRef active_collide_src_node_; Object::WeakRef active_collide_dst_node_; - std::unique_ptr collision_cache_; + std::unique_ptr collision_cache_; friend class Impl; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_DYNAMICS_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_DYNAMICS_H_ diff --git a/src/ballistica/dynamics/material/impact_sound_material_action.cc b/src/ballistica/scene_v1/dynamics/material/impact_sound_material_action.cc similarity index 71% rename from src/ballistica/dynamics/material/impact_sound_material_action.cc rename to src/ballistica/scene_v1/dynamics/material/impact_sound_material_action.cc index fd4952a4..baae54f2 100644 --- a/src/ballistica/dynamics/material/impact_sound_material_action.cc +++ b/src/ballistica/scene_v1/dynamics/material/impact_sound_material_action.cc @@ -1,15 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/material/impact_sound_material_action.h" +#include "ballistica/scene_v1/dynamics/material/impact_sound_material_action.h" -#include "ballistica/dynamics/dynamics.h" -#include "ballistica/dynamics/material/material_context.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/logic/session/client_session.h" -#include "ballistica/scene/scene_stream.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/scene_v1/dynamics/dynamics.h" +#include "ballistica/scene_v1/dynamics/material/material_context.h" +#include "ballistica/scene_v1/support/client_session.h" +#include "ballistica/scene_v1/support/session_stream.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::scene_v1 { auto ImpactSoundMaterialAction::GetFlattenedSize() -> size_t { // 1 byte for number of sounds plus 1 int per sound @@ -17,14 +17,14 @@ auto ImpactSoundMaterialAction::GetFlattenedSize() -> size_t { } void ImpactSoundMaterialAction::Flatten(char** buffer, - SceneStream* output_stream) { + SessionStream* output_stream) { assert(sounds.size() < 100); auto sound_count{static_cast(sounds.size())}; Utils::EmbedInt8(buffer, sound_count); for (int i = 0; i < sound_count; i++) { Utils::EmbedInt32NBO(buffer, static_cast_check_fit( - output_stream->GetSoundID(sounds[i].get()))); + output_stream->GetSoundID(sounds[i].Get()))); } Utils::EmbedFloat16NBO(buffer, target_impulse_); Utils::EmbedFloat16NBO(buffer, volume_); @@ -47,13 +47,13 @@ void ImpactSoundMaterialAction::Apply(MaterialContext* context, const Part* dst_part, const Object::Ref& p) { assert(context && src_part && dst_part); - assert(context->dynamics.exists()); + assert(context->dynamics.Exists()); assert(context->dynamics->in_process()); // For now lets avoid this in low-quality graphics mode (should we make // a low-quality sound mode?) - if (g_graphics_server - && g_graphics_server->quality() < GraphicsQuality::kMedium) { + if (g_base->graphics_server + && g_base->graphics_server->quality() < base::GraphicsQuality::kMedium) { return; } @@ -64,10 +64,10 @@ void ImpactSoundMaterialAction::Apply(MaterialContext* context, > 100) { assert(!sounds.empty()); context->impact_sounds.emplace_back( - context, sounds[rand() % sounds.size()].get(), // NOLINT + context, sounds[rand() % sounds.size()].Get(), // NOLINT target_impulse_, volume_); context->complex_sound = true; } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/dynamics/material/impact_sound_material_action.h b/src/ballistica/scene_v1/dynamics/material/impact_sound_material_action.h similarity index 56% rename from src/ballistica/dynamics/material/impact_sound_material_action.h rename to src/ballistica/scene_v1/dynamics/material/impact_sound_material_action.h index c3b2398a..c38cb1ba 100644 --- a/src/ballistica/dynamics/material/impact_sound_material_action.h +++ b/src/ballistica/scene_v1/dynamics/material/impact_sound_material_action.h @@ -1,32 +1,32 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_MATERIAL_IMPACT_SOUND_MATERIAL_ACTION_H_ -#define BALLISTICA_DYNAMICS_MATERIAL_IMPACT_SOUND_MATERIAL_ACTION_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_IMPACT_SOUND_MATERIAL_ACTION_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_IMPACT_SOUND_MATERIAL_ACTION_H_ #include -#include "ballistica/assets/component/sound.h" -#include "ballistica/ballistica.h" -#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/dynamics/material/material_action.h" +#include "ballistica/shared/ballistica.h" -namespace ballistica { +namespace ballistica::scene_v1 { // A sound created based on collision forces parallel to the collision normal. class ImpactSoundMaterialAction : public MaterialAction { public: ImpactSoundMaterialAction() = default; - ImpactSoundMaterialAction(const std::vector& sounds_in, + ImpactSoundMaterialAction(const std::vector& sounds_in, float target_impulse_in, float volume_in) : sounds(PointersToRefs(sounds_in)), target_impulse_(target_impulse_in), volume_(volume_in) {} - std::vector > sounds; + std::vector > sounds; void Apply(MaterialContext* context, const Part* src_part, const Part* dst_part, const Object::Ref& p) override; auto GetType() const -> Type override { return Type::IMPACT_SOUND; } auto GetFlattenedSize() -> size_t override; - void Flatten(char** buffer, SceneStream* output_stream) override; + void Flatten(char** buffer, SessionStream* output_stream) override; void Restore(const char** buffer, ClientSession* cs) override; private: @@ -34,6 +34,6 @@ class ImpactSoundMaterialAction : public MaterialAction { float volume_{}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_MATERIAL_IMPACT_SOUND_MATERIAL_ACTION_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_IMPACT_SOUND_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/material.cc b/src/ballistica/scene_v1/dynamics/material/material.cc similarity index 63% rename from src/ballistica/dynamics/material/material.cc rename to src/ballistica/scene_v1/dynamics/material/material.cc index 4a6e07a5..f519c4e2 100644 --- a/src/ballistica/dynamics/material/material.cc +++ b/src/ballistica/scene_v1/dynamics/material/material.cc @@ -1,22 +1,20 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/material/material.h" +#include "ballistica/scene_v1/dynamics/material/material.h" -// #include "ballistica/dynamics/material/material_action.h" -#include "ballistica/dynamics/material/material_component.h" -#include "ballistica/dynamics/material/material_condition_node.h" -#include "ballistica/python/python_sys.h" -#include "ballistica/scene/scene.h" -#include "ballistica/scene/scene_stream.h" +#include "ballistica/scene_v1/dynamics/material/material_component.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/scene_v1/support/session_stream.h" +#include "ballistica/shared/python/python_sys.h" -namespace ballistica { +namespace ballistica::scene_v1 { Material::Material(std::string name_in, Scene* scene) : label_(std::move(name_in)), scene_(scene) { // If we're being made in a scene with an output stream, // write ourself to it. assert(scene); - if (SceneStream* os = scene->GetSceneStream()) { + if (SessionStream* os = scene->GetSceneStream()) { os->AddMaterial(this); } } @@ -28,9 +26,9 @@ void Material::MarkDead() { components_.clear(); // If we're in a scene with an output-stream, inform them of our demise. - Scene* scene = scene_.get(); + Scene* scene = scene_.Get(); if (scene) { - if (SceneStream* os = scene->GetSceneStream()) { + if (SessionStream* os = scene->GetSceneStream()) { os->RemoveMaterial(this); } } @@ -62,17 +60,17 @@ void Material::Apply(MaterialContext* s, const Part* src_part, void Material::AddComponent(const Object::Ref& c) { // If there's an output stream, push this to that first - if (SceneStream* output_stream = scene()->GetSceneStream()) { - output_stream->AddMaterialComponent(this, c.get()); + if (SessionStream* output_stream = scene()->GetSceneStream()) { + output_stream->AddMaterialComponent(this, c.Get()); } components_.push_back(c); } -void Material::DumpComponents(SceneStream* out) { +void Material::DumpComponents(SessionStream* out) { for (auto& i : components_) { - assert(i.exists()); - out->AddMaterialComponent(this, i.get()); + assert(i.Exists()); + out->AddMaterialComponent(this, i.Get()); } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/dynamics/material/material.h b/src/ballistica/scene_v1/dynamics/material/material.h similarity index 77% rename from src/ballistica/dynamics/material/material.h rename to src/ballistica/scene_v1/dynamics/material/material.h index a8f86199..b0e4e09d 100644 --- a/src/ballistica/dynamics/material/material.h +++ b/src/ballistica/scene_v1/dynamics/material/material.h @@ -1,14 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_H_ -#define BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_MATERIAL_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_MATERIAL_H_ #include #include -#include "ballistica/core/object.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::scene_v1 { /// A material defines actions that occur when a part collides with another part /// (or separates from it after colliding). Materials can set up any number of @@ -23,14 +24,14 @@ class Material : public Object { /// Pass a component allocated via new. void AddComponent(const Object::Ref& c); - /// Apply the material to a context. + /// Apply the material to a context_ref. void Apply(MaterialContext* s, const Part* src_part, const Part* dst_part); auto label() const -> const std::string& { return label_; } auto NewPyRef() -> PyObject* { return GetPyRef(true); } auto BorrowPyRef() -> PyObject* { return GetPyRef(false); } void MarkDead(); - auto scene() const -> Scene* { return scene_.get(); } - void DumpComponents(SceneStream* out); + auto scene() const -> Scene* { return scene_.Get(); } + void DumpComponents(SessionStream* out); auto stream_id() const -> int64_t { return stream_id_; } void set_stream_id(int64_t val) { assert(stream_id_ == -1); @@ -55,6 +56,6 @@ class Material : public Object { friend class ClientSession; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_MATERIAL_H_ diff --git a/src/ballistica/dynamics/material/material_action.h b/src/ballistica/scene_v1/dynamics/material/material_action.h similarity index 71% rename from src/ballistica/dynamics/material/material_action.h rename to src/ballistica/scene_v1/dynamics/material/material_action.h index b722bf73..2f2b2777 100644 --- a/src/ballistica/dynamics/material/material_action.h +++ b/src/ballistica/scene_v1/dynamics/material/material_action.h @@ -1,11 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_ACTION_H_ -#define BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_ACTION_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_MATERIAL_ACTION_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_MATERIAL_ACTION_H_ -#include "ballistica/core/object.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::scene_v1 { class MaterialAction : public Object { public: @@ -28,7 +29,7 @@ class MaterialAction : public Object { const Object::Ref& p) = 0; virtual void Execute(Node* node1, Node* node2, Scene* scene) {} virtual auto GetFlattenedSize() -> size_t { return 0; } - virtual void Flatten(char** buffer, SceneStream* output_stream) {} + virtual void Flatten(char** buffer, SessionStream* output_stream) {} virtual void Restore(const char** buffer, ClientSession* cs) {} auto IsNeededOnClient() -> bool { switch (GetType()) { @@ -46,6 +47,6 @@ class MaterialAction : public Object { } }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_ACTION_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/material_component.cc b/src/ballistica/scene_v1/dynamics/material/material_component.cc similarity index 84% rename from src/ballistica/dynamics/material/material_component.cc rename to src/ballistica/scene_v1/dynamics/material/material_component.cc index 76f4ace9..55b9963d 100644 --- a/src/ballistica/dynamics/material/material_component.cc +++ b/src/ballistica/scene_v1/dynamics/material/material_component.cc @@ -1,21 +1,21 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/material/material_component.h" +#include "ballistica/scene_v1/dynamics/material/material_component.h" -#include "ballistica/dynamics/material/impact_sound_material_action.h" -#include "ballistica/dynamics/material/material.h" -#include "ballistica/dynamics/material/material_condition_node.h" -#include "ballistica/dynamics/material/material_context.h" -#include "ballistica/dynamics/material/node_message_material_action.h" -#include "ballistica/dynamics/material/node_mod_material_action.h" -#include "ballistica/dynamics/material/part_mod_material_action.h" -#include "ballistica/dynamics/material/roll_sound_material_action.h" -#include "ballistica/dynamics/material/skid_sound_material_action.h" -#include "ballistica/dynamics/material/sound_material_action.h" -#include "ballistica/dynamics/part.h" -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/dynamics/material/impact_sound_material_action.h" +#include "ballistica/scene_v1/dynamics/material/material.h" +#include "ballistica/scene_v1/dynamics/material/material_condition_node.h" +#include "ballistica/scene_v1/dynamics/material/material_context.h" +#include "ballistica/scene_v1/dynamics/material/node_message_material_action.h" +#include "ballistica/scene_v1/dynamics/material/node_mod_material_action.h" +#include "ballistica/scene_v1/dynamics/material/part_mod_material_action.h" +#include "ballistica/scene_v1/dynamics/material/roll_sound_material_action.h" +#include "ballistica/scene_v1/dynamics/material/skid_sound_material_action.h" +#include "ballistica/scene_v1/dynamics/material/sound_material_action.h" +#include "ballistica/scene_v1/dynamics/part.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { MaterialComponent::MaterialComponent() {} @@ -31,7 +31,7 @@ auto MaterialComponent::eval_conditions( const Part* part, const Part* opposing_part, const MaterialContext& s) -> bool { // If there's no condition, succeed. - if (!condition.exists()) { + if (!condition.Exists()) { return true; } @@ -44,10 +44,10 @@ auto MaterialComponent::eval_conditions( return false; case MaterialCondition::kDstIsMaterial: return ( - (opposing_part->ContainsMaterial(condition->val1_material.get()))); + (opposing_part->ContainsMaterial(condition->val1_material.Get()))); case MaterialCondition::kDstNotMaterial: return ( - !(opposing_part->ContainsMaterial(condition->val1_material.get()))); + !(opposing_part->ContainsMaterial(condition->val1_material.Get()))); case MaterialCondition::kDstIsPart: return ((opposing_part->id() == condition->val1)); case MaterialCondition::kDstNotPart: @@ -82,8 +82,8 @@ auto MaterialComponent::eval_conditions( } else { // A trunk node; eval our left and right children and return // the boolean operation between them. - assert(condition->left_child.exists()); - assert(condition->right_child.exists()); + assert(condition->left_child.Exists()); + assert(condition->right_child.Exists()); bool left_result = eval_conditions(condition->left_child, c, part, opposing_part, s); @@ -126,7 +126,7 @@ auto MaterialComponent::GetFlattenedSize() -> size_t { size += 1; // Embed the size of the condition tree. - if (conditions.exists()) { + if (conditions.Exists()) { size += conditions->GetFlattenedSize(); } @@ -143,12 +143,12 @@ auto MaterialComponent::GetFlattenedSize() -> size_t { return size; } -void MaterialComponent::Flatten(char** buffer, SceneStream* output_stream) { +void MaterialComponent::Flatten(char** buffer, SessionStream* output_stream) { // Embed a byte telling whether we have conditions. - Utils::EmbedInt8(buffer, conditions.exists()); + Utils::EmbedInt8(buffer, conditions.Exists()); // If we have conditions, have the tree embed itself. - if (conditions.exists()) { + if (conditions.Exists()) { conditions->Flatten(buffer, output_stream); } @@ -231,4 +231,4 @@ void MaterialComponent::Apply(MaterialContext* context, const Part* src_part, } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/dynamics/material/material_component.h b/src/ballistica/scene_v1/dynamics/material/material_component.h similarity index 68% rename from src/ballistica/dynamics/material/material_component.h rename to src/ballistica/scene_v1/dynamics/material/material_component.h index ee6870aa..bbd06761 100644 --- a/src/ballistica/dynamics/material/material_component.h +++ b/src/ballistica/scene_v1/dynamics/material/material_component.h @@ -1,24 +1,25 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_COMPONENT_H_ -#define BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_COMPONENT_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_MATERIAL_COMPONENT_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_MATERIAL_COMPONENT_H_ #include #include -#include "ballistica/core/object.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::scene_v1 { // A component of a material - comprises one or more conditions and actions. class MaterialComponent : public Object { public: - auto GetDefaultOwnerThread() const -> ThreadTag override { - return ThreadTag::kLogic; + auto GetDefaultOwnerThread() const -> EventLoopID override { + return EventLoopID::kLogic; } auto GetFlattenedSize() -> size_t; - void Flatten(char** buffer, SceneStream* output_stream); + void Flatten(char** buffer, SessionStream* output_stream); void Restore(const char** buffer, ClientSession* cs); // Actions are stored as shared pointers so references @@ -40,6 +41,6 @@ class MaterialComponent : public Object { ~MaterialComponent(); }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_COMPONENT_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_MATERIAL_COMPONENT_H_ diff --git a/src/ballistica/dynamics/material/material_condition_node.cc b/src/ballistica/scene_v1/dynamics/material/material_condition_node.cc similarity index 84% rename from src/ballistica/dynamics/material/material_condition_node.cc rename to src/ballistica/scene_v1/dynamics/material/material_condition_node.cc index 6053ffbf..5cb14b31 100644 --- a/src/ballistica/dynamics/material/material_condition_node.cc +++ b/src/ballistica/scene_v1/dynamics/material/material_condition_node.cc @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/material/material_condition_node.h" +#include "ballistica/scene_v1/dynamics/material/material_condition_node.h" -#include "ballistica/dynamics/material/material.h" -#include "ballistica/generic/utils.h" -#include "ballistica/logic/session/client_session.h" -#include "ballistica/scene/scene_stream.h" +#include "ballistica/scene_v1/dynamics/material/material.h" +#include "ballistica/scene_v1/support/client_session.h" +#include "ballistica/scene_v1/support/session_stream.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::scene_v1 { auto MaterialConditionNode::GetFlattenedSize() -> size_t { // we need one byte for our opmode @@ -22,7 +22,8 @@ auto MaterialConditionNode::GetFlattenedSize() -> size_t { return size; } -void MaterialConditionNode::Flatten(char** buffer, SceneStream* output_stream) { +void MaterialConditionNode::Flatten(char** buffer, + SessionStream* output_stream) { // Pack our opmode in. Or if we're a leaf note stick zero in. Utils::EmbedInt8(buffer, static_cast(opmode)); if (opmode == OpMode::LEAF_NODE) { @@ -36,7 +37,7 @@ void MaterialConditionNode::Flatten(char** buffer, SceneStream* output_stream) { || cond == MaterialCondition::kDstNotMaterial) { Utils::EmbedInt32NBO( buffer, static_cast_check_fit( - output_stream->GetMaterialID(val1_material.get()))); + output_stream->GetMaterialID(val1_material.Get()))); } else { Utils::EmbedInt32NBO(buffer, val1); } @@ -92,4 +93,4 @@ void MaterialConditionNode::Restore(const char** buffer, ClientSession* cs) { } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/dynamics/material/material_condition_node.h b/src/ballistica/scene_v1/dynamics/material/material_condition_node.h similarity index 77% rename from src/ballistica/dynamics/material/material_condition_node.h rename to src/ballistica/scene_v1/dynamics/material/material_condition_node.h index dc6ea0b5..056aa7dc 100644 --- a/src/ballistica/dynamics/material/material_condition_node.h +++ b/src/ballistica/scene_v1/dynamics/material/material_condition_node.h @@ -1,12 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_CONDITION_NODE_H_ -#define BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_CONDITION_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_MATERIAL_CONDITION_NODE_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_MATERIAL_CONDITION_NODE_H_ -#include "ballistica/ballistica.h" -#include "ballistica/core/object.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::scene_v1 { class MaterialConditionNode : public Object { public: @@ -50,10 +51,10 @@ class MaterialConditionNode : public Object { } } auto GetFlattenedSize() -> size_t; - void Flatten(char** buffer, SceneStream* output_stream); + void Flatten(char** buffer, SessionStream* output_stream); void Restore(const char** buffer, ClientSession* cs); }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_CONDITION_NODE_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_MATERIAL_CONDITION_NODE_H_ diff --git a/src/ballistica/dynamics/material/material_context.cc b/src/ballistica/scene_v1/dynamics/material/material_context.cc similarity index 70% rename from src/ballistica/dynamics/material/material_context.cc rename to src/ballistica/scene_v1/dynamics/material/material_context.cc index 6d50a332..42ea5284 100644 --- a/src/ballistica/dynamics/material/material_context.cc +++ b/src/ballistica/scene_v1/dynamics/material/material_context.cc @@ -1,14 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/material/material_context.h" +#include "ballistica/scene_v1/dynamics/material/material_context.h" -#include "ballistica/assets/component/sound.h" -#include "ballistica/audio/audio.h" -#include "ballistica/dynamics/dynamics.h" -#include "ballistica/dynamics/material/material_action.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/dynamics/dynamics.h" +#include "ballistica/scene_v1/dynamics/material/material_action.h" +#include "ballistica/scene_v1/support/scene.h" -namespace ballistica { +namespace ballistica::scene_v1 { MaterialContext::MaterialContext(Scene* scene) : dynamics(scene->dynamics()), @@ -22,11 +22,11 @@ MaterialContext::MaterialContext(Scene* scene) physical(true), complex_sound(false) {} -MaterialContext::SoundEntry::SoundEntry(Sound* sound_in, float volume_in) +MaterialContext::SoundEntry::SoundEntry(SceneSound* sound_in, float volume_in) : sound(sound_in), volume(volume_in) {} MaterialContext::ImpactSoundEntry::ImpactSoundEntry(MaterialContext* context, - Sound* sound_in, + SceneSound* sound_in, float target_impulse_in, float volume_in) : context(context), @@ -39,14 +39,14 @@ MaterialContext::SkidSoundEntry::SkidSoundEntry( *this = other; assert(context); #if BA_DEBUG_BUILD - assert(context->dynamics.exists()); + assert(context->dynamics.Exists()); #endif assert(context->dynamics->in_process()); context->dynamics->increment_skid_sound_count(); } MaterialContext::SkidSoundEntry::SkidSoundEntry(MaterialContext* context_in, - Sound* sound_in, + SceneSound* sound_in, float target_impulse_in, float volume_in) : context(context_in), @@ -55,22 +55,22 @@ MaterialContext::SkidSoundEntry::SkidSoundEntry(MaterialContext* context_in, volume(volume_in), playing(false) { assert(context); - assert(context->dynamics.exists()); + assert(context->dynamics.Exists()); assert(context->dynamics->in_process()); context->dynamics->increment_skid_sound_count(); } MaterialContext::SkidSoundEntry::~SkidSoundEntry() { assert(context); - assert(context->dynamics.exists()); + assert(context->dynamics.Exists()); context->dynamics->decrement_skid_sound_count(); if (playing) { - g_audio->PushSourceFadeOutCall(play_id, 200); + g_base->audio->PushSourceFadeOutCall(play_id, 200); } } MaterialContext::RollSoundEntry::RollSoundEntry(MaterialContext* context_in, - Sound* sound_in, + SceneSound* sound_in, float target_impulse_in, float volume_in) : context(context_in), @@ -79,7 +79,7 @@ MaterialContext::RollSoundEntry::RollSoundEntry(MaterialContext* context_in, volume(volume_in), playing(false) { assert(context); - assert(context->dynamics.exists()); + assert(context->dynamics.Exists()); assert(context->dynamics->in_process()); context->dynamics->incrementRollSoundCount(); } @@ -88,18 +88,18 @@ MaterialContext::RollSoundEntry::RollSoundEntry( const MaterialContext::RollSoundEntry& other) { *this = other; assert(context); - assert(context->dynamics.exists()); + assert(context->dynamics.Exists()); assert(context->dynamics->in_process()); context->dynamics->incrementRollSoundCount(); } MaterialContext::RollSoundEntry::~RollSoundEntry() { assert(context); - assert(context->dynamics.exists()); + assert(context->dynamics.Exists()); context->dynamics->decrement_roll_sound_count(); if (playing) { - g_audio->PushSourceFadeOutCall(play_id, 200); + g_base->audio->PushSourceFadeOutCall(play_id, 200); } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/dynamics/material/material_context.h b/src/ballistica/scene_v1/dynamics/material/material_context.h similarity index 71% rename from src/ballistica/dynamics/material/material_context.h rename to src/ballistica/scene_v1/dynamics/material/material_context.h index b1849337..15934349 100644 --- a/src/ballistica/dynamics/material/material_context.h +++ b/src/ballistica/scene_v1/dynamics/material/material_context.h @@ -1,13 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_CONTEXT_H_ -#define BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_CONTEXT_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_MATERIAL_CONTEXT_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_MATERIAL_CONTEXT_H_ #include -#include "ballistica/core/object.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Contexts materials use when getting and setting collision data class MaterialContext { @@ -29,29 +30,29 @@ class MaterialContext { std::vector > connect_actions; std::vector > disconnect_actions; struct SoundEntry { - Object::Ref sound; + Object::Ref sound; float volume; - SoundEntry(Sound* sound_in, float volume_in); + SoundEntry(SceneSound* sound_in, float volume_in); }; class ImpactSoundEntry { public: MaterialContext* context; - Object::Ref sound; + Object::Ref sound; float volume; float target_impulse; - ImpactSoundEntry(MaterialContext* context, Sound* sound_in, + ImpactSoundEntry(MaterialContext* context, SceneSound* sound_in, float target_impulse_in, float volume_in); }; class SkidSoundEntry { public: MaterialContext* context{}; - Object::Ref sound; + Object::Ref sound; float volume{}; float target_impulse{}; // Used to keep track of the playing sound. uint32_t play_id{}; bool playing{}; - SkidSoundEntry(MaterialContext* context, Sound* sound_in, + SkidSoundEntry(MaterialContext* context, SceneSound* sound_in, float target_impulse_in, float volume_in); ~SkidSoundEntry(); SkidSoundEntry(const SkidSoundEntry& other); @@ -59,13 +60,13 @@ class MaterialContext { class RollSoundEntry { public: MaterialContext* context{}; - Object::Ref sound; + Object::Ref sound; float volume{}; float target_impulse{}; // Used to keep track of the playing sound. uint32_t play_id{}; bool playing{}; - RollSoundEntry(MaterialContext* context, Sound* sound_in, + RollSoundEntry(MaterialContext* context, SceneSound* sound_in, float target_impulse_in, float volume_in); RollSoundEntry(const RollSoundEntry& other); ~RollSoundEntry(); @@ -80,6 +81,6 @@ class MaterialContext { BA_DISALLOW_CLASS_COPIES(MaterialContext); }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_MATERIAL_MATERIAL_CONTEXT_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_MATERIAL_CONTEXT_H_ diff --git a/src/ballistica/dynamics/material/node_message_material_action.cc b/src/ballistica/scene_v1/dynamics/material/node_message_material_action.cc similarity index 80% rename from src/ballistica/dynamics/material/node_message_material_action.cc rename to src/ballistica/scene_v1/dynamics/material/node_message_material_action.cc index cfad5692..f9a3c4c3 100644 --- a/src/ballistica/dynamics/material/node_message_material_action.cc +++ b/src/ballistica/scene_v1/dynamics/material/node_message_material_action.cc @@ -1,12 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/material/node_message_material_action.h" +#include "ballistica/scene_v1/dynamics/material/node_message_material_action.h" -#include "ballistica/dynamics/dynamics.h" -#include "ballistica/dynamics/material/material_context.h" -#include "ballistica/scene/scene.h" +#include "ballistica/scene_v1/dynamics/dynamics.h" +#include "ballistica/scene_v1/dynamics/material/material_context.h" +#include "ballistica/scene_v1/support/scene.h" + +namespace ballistica::scene_v1 { -namespace ballistica { NodeMessageMaterialAction::NodeMessageMaterialAction(bool target_other_in, bool at_disconnect_in, const char* data_in, @@ -41,4 +42,4 @@ void NodeMessageMaterialAction::Execute(Node* node1, Node* node2, } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/dynamics/material/node_message_material_action.h b/src/ballistica/scene_v1/dynamics/material/node_message_material_action.h similarity index 65% rename from src/ballistica/dynamics/material/node_message_material_action.h rename to src/ballistica/scene_v1/dynamics/material/node_message_material_action.h index 64eae91c..616e5ba1 100644 --- a/src/ballistica/dynamics/material/node_message_material_action.h +++ b/src/ballistica/scene_v1/dynamics/material/node_message_material_action.h @@ -1,13 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_MATERIAL_NODE_MESSAGE_MATERIAL_ACTION_H_ -#define BALLISTICA_DYNAMICS_MATERIAL_NODE_MESSAGE_MATERIAL_ACTION_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_NODE_MESSAGE_MATERIAL_ACTION_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_NODE_MESSAGE_MATERIAL_ACTION_H_ -#include "ballistica/ballistica.h" -#include "ballistica/dynamics/material/material_action.h" -#include "ballistica/generic/buffer.h" +#include "ballistica/scene_v1/dynamics/material/material_action.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/generic/buffer.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Regular message. class NodeMessageMaterialAction : public MaterialAction { @@ -27,7 +28,7 @@ class NodeMessageMaterialAction : public MaterialAction { // 1 byte for bools + data return static_cast(1 + data.GetFlattenedSize()); } - void Flatten(char** buffer, SceneStream* output_stream) override { + void Flatten(char** buffer, SessionStream* output_stream) override { Utils::EmbedBools(buffer, target_other, at_disconnect); data.embed(buffer); } @@ -37,6 +38,6 @@ class NodeMessageMaterialAction : public MaterialAction { } }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_MATERIAL_NODE_MESSAGE_MATERIAL_ACTION_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_NODE_MESSAGE_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/node_mod_material_action.cc b/src/ballistica/scene_v1/dynamics/material/node_mod_material_action.cc similarity index 72% rename from src/ballistica/dynamics/material/node_mod_material_action.cc rename to src/ballistica/scene_v1/dynamics/material/node_mod_material_action.cc index 0e207f45..1cd8225d 100644 --- a/src/ballistica/dynamics/material/node_mod_material_action.cc +++ b/src/ballistica/scene_v1/dynamics/material/node_mod_material_action.cc @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/material/node_mod_material_action.h" +#include "ballistica/scene_v1/dynamics/material/node_mod_material_action.h" -#include "ballistica/dynamics/material/material_context.h" -#include "ballistica/generic/utils.h" +#include "ballistica/scene_v1/dynamics/material/material_context.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::scene_v1 { auto NodeModMaterialAction::GetType() const -> MaterialAction::Type { return Type::NODE_MOD; @@ -13,7 +13,8 @@ auto NodeModMaterialAction::GetType() const -> MaterialAction::Type { auto NodeModMaterialAction::GetFlattenedSize() -> size_t { return 1 + 4; } -void NodeModMaterialAction::Flatten(char** buffer, SceneStream* output_stream) { +void NodeModMaterialAction::Flatten(char** buffer, + SessionStream* output_stream) { Utils::EmbedInt8(buffer, static_cast(attr)); Utils::EmbedFloat32(buffer, attr_val); } @@ -37,4 +38,4 @@ void NodeModMaterialAction::Apply(MaterialContext* context, } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/dynamics/material/node_mod_material_action.h b/src/ballistica/scene_v1/dynamics/material/node_mod_material_action.h similarity index 59% rename from src/ballistica/dynamics/material/node_mod_material_action.h rename to src/ballistica/scene_v1/dynamics/material/node_mod_material_action.h index 77902e5f..67f9ce75 100644 --- a/src/ballistica/dynamics/material/node_mod_material_action.h +++ b/src/ballistica/scene_v1/dynamics/material/node_mod_material_action.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_MATERIAL_NODE_MOD_MATERIAL_ACTION_H_ -#define BALLISTICA_DYNAMICS_MATERIAL_NODE_MOD_MATERIAL_ACTION_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_NODE_MOD_MATERIAL_ACTION_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_NODE_MOD_MATERIAL_ACTION_H_ -#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/scene_v1/dynamics/material/material_action.h" -namespace ballistica { +namespace ballistica::scene_v1 { class NodeModMaterialAction : public MaterialAction { public: @@ -19,10 +19,10 @@ class NodeModMaterialAction : public MaterialAction { const Object::Ref& p) override; auto GetType() const -> Type override; auto GetFlattenedSize() -> size_t override; - void Flatten(char** buffer, SceneStream* output_stream) override; + void Flatten(char** buffer, SessionStream* output_stream) override; void Restore(const char** buffer, ClientSession* cs) override; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_MATERIAL_NODE_MOD_MATERIAL_ACTION_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_NODE_MOD_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/node_user_message_material_action.cc b/src/ballistica/scene_v1/dynamics/material/node_user_msg_mat_action.cc similarity index 78% rename from src/ballistica/dynamics/material/node_user_message_material_action.cc rename to src/ballistica/scene_v1/dynamics/material/node_user_msg_mat_action.cc index a690dbe0..1dbc4eb4 100644 --- a/src/ballistica/dynamics/material/node_user_message_material_action.cc +++ b/src/ballistica/scene_v1/dynamics/material/node_user_msg_mat_action.cc @@ -1,14 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/material/node_user_message_material_action.h" +#include "ballistica/scene_v1/dynamics/material/node_user_msg_mat_action.h" -#include "ballistica/core/context.h" -#include "ballistica/dynamics/dynamics.h" -#include "ballistica/dynamics/material/material_context.h" -#include "ballistica/scene/node/node.h" -#include "ballistica/scene/scene.h" +#include "ballistica/scene_v1/dynamics/dynamics.h" +#include "ballistica/scene_v1/dynamics/material/material_context.h" +#include "ballistica/scene_v1/support/scene.h" -namespace ballistica { +namespace ballistica::scene_v1 { NodeUserMessageMaterialAction::NodeUserMessageMaterialAction( bool target_other_in, bool at_disconnect_in, PyObject* user_message_obj_in) @@ -50,11 +48,11 @@ void NodeUserMessageMaterialAction::Execute(Node* node1, Node* node2, } } - ScopedSetContext cp(target_node->context()); + base::ScopedSetContext ssc(target_node->context_ref()); scene->dynamics()->set_collide_message_state(true, target_other); - target_node->DispatchUserMessage(user_message_obj.get(), + target_node->DispatchUserMessage(user_message_obj.Get(), "Material User-Message dispatch"); scene->dynamics()->set_collide_message_state(false); } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/dynamics/material/node_user_message_material_action.h b/src/ballistica/scene_v1/dynamics/material/node_user_msg_mat_action.h similarity index 63% rename from src/ballistica/dynamics/material/node_user_message_material_action.h rename to src/ballistica/scene_v1/dynamics/material/node_user_msg_mat_action.h index bed94691..d3b9d7aa 100644 --- a/src/ballistica/dynamics/material/node_user_message_material_action.h +++ b/src/ballistica/scene_v1/dynamics/material/node_user_msg_mat_action.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_MATERIAL_NODE_USER_MESSAGE_MATERIAL_ACTION_H_ -#define BALLISTICA_DYNAMICS_MATERIAL_NODE_USER_MESSAGE_MATERIAL_ACTION_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_NODE_USER_MSG_MAT_ACTION_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_NODE_USER_MSG_MAT_ACTION_H_ -#include "ballistica/ballistica.h" -#include "ballistica/dynamics/material/material_action.h" -#include "ballistica/python/python_ref.h" +#include "ballistica/scene_v1/dynamics/material/material_action.h" +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/python/python_ref.h" -namespace ballistica { +namespace ballistica::scene_v1 { // a user message - encapsulates a python object class NodeUserMessageMaterialAction : public MaterialAction { @@ -25,6 +25,6 @@ class NodeUserMessageMaterialAction : public MaterialAction { auto GetType() const -> Type override { return Type::NODE_USER_MESSAGE; } }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_MATERIAL_NODE_USER_MESSAGE_MATERIAL_ACTION_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_NODE_USER_MSG_MAT_ACTION_H_ diff --git a/src/ballistica/dynamics/material/part_mod_material_action.cc b/src/ballistica/scene_v1/dynamics/material/part_mod_material_action.cc similarity index 80% rename from src/ballistica/dynamics/material/part_mod_material_action.cc rename to src/ballistica/scene_v1/dynamics/material/part_mod_material_action.cc index 62a18200..1cc2c73d 100644 --- a/src/ballistica/dynamics/material/part_mod_material_action.cc +++ b/src/ballistica/scene_v1/dynamics/material/part_mod_material_action.cc @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/material/part_mod_material_action.h" +#include "ballistica/scene_v1/dynamics/material/part_mod_material_action.h" -#include "ballistica/dynamics/material/material_context.h" -#include "ballistica/generic/utils.h" +#include "ballistica/scene_v1/dynamics/material/material_context.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::scene_v1 { auto PartModMaterialAction::GetType() const -> MaterialAction::Type { return Type::PART_MOD; @@ -13,7 +13,8 @@ auto PartModMaterialAction::GetType() const -> MaterialAction::Type { auto PartModMaterialAction::GetFlattenedSize() -> size_t { return 1 + 4; } -void PartModMaterialAction::Flatten(char** buffer, SceneStream* output_stream) { +void PartModMaterialAction::Flatten(char** buffer, + SessionStream* output_stream) { Utils::EmbedInt8(buffer, static_cast(attr)); Utils::EmbedFloat32(buffer, attr_val); } @@ -54,4 +55,4 @@ void PartModMaterialAction::Apply(MaterialContext* context, } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/dynamics/material/part_mod_material_action.h b/src/ballistica/scene_v1/dynamics/material/part_mod_material_action.h similarity index 59% rename from src/ballistica/dynamics/material/part_mod_material_action.h rename to src/ballistica/scene_v1/dynamics/material/part_mod_material_action.h index f192e59d..46a1392b 100644 --- a/src/ballistica/dynamics/material/part_mod_material_action.h +++ b/src/ballistica/scene_v1/dynamics/material/part_mod_material_action.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_MATERIAL_PART_MOD_MATERIAL_ACTION_H_ -#define BALLISTICA_DYNAMICS_MATERIAL_PART_MOD_MATERIAL_ACTION_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_PART_MOD_MATERIAL_ACTION_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_PART_MOD_MATERIAL_ACTION_H_ -#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/scene_v1/dynamics/material/material_action.h" -namespace ballistica { +namespace ballistica::scene_v1 { class PartModMaterialAction : public MaterialAction { public: @@ -19,10 +19,10 @@ class PartModMaterialAction : public MaterialAction { const Object::Ref& p) override; auto GetType() const -> Type override; auto GetFlattenedSize() -> size_t override; - void Flatten(char** buffer, SceneStream* output_stream) override; + void Flatten(char** buffer, SessionStream* output_stream) override; void Restore(const char** buffer, ClientSession* cs) override; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_MATERIAL_PART_MOD_MATERIAL_ACTION_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_PART_MOD_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/python_call_material_action.cc b/src/ballistica/scene_v1/dynamics/material/python_call_material_action.cc similarity index 77% rename from src/ballistica/dynamics/material/python_call_material_action.cc rename to src/ballistica/scene_v1/dynamics/material/python_call_material_action.cc index 89edb19d..3d36581f 100644 --- a/src/ballistica/dynamics/material/python_call_material_action.cc +++ b/src/ballistica/scene_v1/dynamics/material/python_call_material_action.cc @@ -1,17 +1,17 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/material/python_call_material_action.h" +#include "ballistica/scene_v1/dynamics/material/python_call_material_action.h" -#include "ballistica/dynamics/dynamics.h" -#include "ballistica/dynamics/material/material_context.h" -#include "ballistica/scene/scene.h" +#include "ballistica/scene_v1/dynamics/dynamics.h" +#include "ballistica/scene_v1/dynamics/material/material_context.h" +#include "ballistica/scene_v1/support/scene.h" -namespace ballistica { +namespace ballistica::scene_v1 { PythonCallMaterialAction::PythonCallMaterialAction(bool at_disconnect_in, PyObject* call_obj_in) : at_disconnect(at_disconnect_in), - call(Object::New(call_obj_in)) {} + call(Object::New(call_obj_in)) {} void PythonCallMaterialAction::Apply(MaterialContext* context, const Part* src_part, const Part* dst_part, @@ -45,4 +45,4 @@ void PythonCallMaterialAction::Execute(Node* node1, Node* node2, Scene* scene) { scene->dynamics()->set_collide_message_state(false); } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/dynamics/material/python_call_material_action.h b/src/ballistica/scene_v1/dynamics/material/python_call_material_action.h similarity index 50% rename from src/ballistica/dynamics/material/python_call_material_action.h rename to src/ballistica/scene_v1/dynamics/material/python_call_material_action.h index 0a0d86a8..b289f0b9 100644 --- a/src/ballistica/dynamics/material/python_call_material_action.h +++ b/src/ballistica/scene_v1/dynamics/material/python_call_material_action.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_MATERIAL_PYTHON_CALL_MATERIAL_ACTION_H_ -#define BALLISTICA_DYNAMICS_MATERIAL_PYTHON_CALL_MATERIAL_ACTION_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_PYTHON_CALL_MATERIAL_ACTION_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_PYTHON_CALL_MATERIAL_ACTION_H_ -#include "ballistica/ballistica.h" -#include "ballistica/dynamics/material/material_action.h" -#include "ballistica/python/python_context_call.h" +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/scene_v1/dynamics/material/material_action.h" +#include "ballistica/shared/ballistica.h" -namespace ballistica { +namespace ballistica::scene_v1 { class PythonCallMaterialAction : public MaterialAction { public: @@ -17,10 +17,10 @@ class PythonCallMaterialAction : public MaterialAction { const Object::Ref& p) override; void Execute(Node* node1, Node* node2, Scene* scene) override; bool at_disconnect; - Object::Ref call; + Object::Ref call; auto GetType() const -> Type override { return Type::SCRIPT_CALL; } }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_MATERIAL_PYTHON_CALL_MATERIAL_ACTION_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_PYTHON_CALL_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/roll_sound_material_action.cc b/src/ballistica/scene_v1/dynamics/material/roll_sound_material_action.cc similarity index 64% rename from src/ballistica/dynamics/material/roll_sound_material_action.cc rename to src/ballistica/scene_v1/dynamics/material/roll_sound_material_action.cc index 363e9492..763dc28b 100644 --- a/src/ballistica/dynamics/material/roll_sound_material_action.cc +++ b/src/ballistica/scene_v1/dynamics/material/roll_sound_material_action.cc @@ -1,23 +1,22 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/material/roll_sound_material_action.h" +#include "ballistica/scene_v1/dynamics/material/roll_sound_material_action.h" -#include "ballistica/assets/component/sound.h" -#include "ballistica/dynamics/dynamics.h" -#include "ballistica/dynamics/material/material_context.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/logic/session/client_session.h" -#include "ballistica/scene/scene_stream.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/scene_v1/dynamics/dynamics.h" +#include "ballistica/scene_v1/dynamics/material/material_context.h" +#include "ballistica/scene_v1/support/client_session.h" +#include "ballistica/scene_v1/support/session_stream.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::scene_v1 { auto RollSoundMaterialAction::GetFlattenedSize() -> size_t { return 4 + 2 + 2; } void RollSoundMaterialAction::Flatten(char** buffer, - SceneStream* output_stream) { + SessionStream* output_stream) { Utils::EmbedInt32NBO(buffer, static_cast_check_fit( - output_stream->GetSoundID(sound.get()))); + output_stream->GetSoundID(sound.Get()))); Utils::EmbedFloat16NBO(buffer, target_impulse); Utils::EmbedFloat16NBO(buffer, volume); } @@ -32,22 +31,23 @@ void RollSoundMaterialAction::Apply(MaterialContext* context, const Part* src_part, const Part* dst_part, const Object::Ref& p) { assert(context && src_part && dst_part); - assert(context->dynamics.exists()); + assert(context->dynamics.Exists()); assert(context->dynamics->in_process()); // For now lets avoid this in low-quality graphics mode // (should we make a low-quality sound mode?) - if (g_graphics && g_graphics_server->quality() < GraphicsQuality::kMedium) { + if (g_base->graphics + && g_base->graphics_server->quality() < base::GraphicsQuality::kMedium) { return; } // Let's limit the amount of skid-sounds we spawn, otherwise we'll // start using up all our sound resources on skids when things get messy if (context->dynamics->getRollSoundCount() < 2) { - context->roll_sounds.emplace_back(context, sound.get(), target_impulse, + context->roll_sounds.emplace_back(context, sound.Get(), target_impulse, volume); context->complex_sound = true; } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/dynamics/material/roll_sound_material_action.h b/src/ballistica/scene_v1/dynamics/material/roll_sound_material_action.h similarity index 54% rename from src/ballistica/dynamics/material/roll_sound_material_action.h rename to src/ballistica/scene_v1/dynamics/material/roll_sound_material_action.h index 88e25b80..eaece3d7 100644 --- a/src/ballistica/dynamics/material/roll_sound_material_action.h +++ b/src/ballistica/scene_v1/dynamics/material/roll_sound_material_action.h @@ -1,22 +1,22 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_MATERIAL_ROLL_SOUND_MATERIAL_ACTION_H_ -#define BALLISTICA_DYNAMICS_MATERIAL_ROLL_SOUND_MATERIAL_ACTION_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_ROLL_SOUND_MATERIAL_ACTION_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_ROLL_SOUND_MATERIAL_ACTION_H_ -#include "ballistica/assets/component/sound.h" -#include "ballistica/ballistica.h" -#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/dynamics/material/material_action.h" +#include "ballistica/shared/ballistica.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Sound created based on velocity perpendicular to the collision normal. class RollSoundMaterialAction : public MaterialAction { public: RollSoundMaterialAction() = default; - RollSoundMaterialAction(Sound* sound_in, float target_impulse_in, + RollSoundMaterialAction(SceneSound* sound_in, float target_impulse_in, float volume_in) : sound(sound_in), target_impulse(target_impulse_in), volume(volume_in) {} - Object::Ref sound; + Object::Ref sound; float target_impulse{}; float volume{}; void Apply(MaterialContext* context, const Part* src_part, @@ -24,10 +24,10 @@ class RollSoundMaterialAction : public MaterialAction { const Object::Ref& p) override; auto GetType() const -> Type override { return Type::ROLL_SOUND; } auto GetFlattenedSize() -> size_t override; - void Flatten(char** buffer, SceneStream* output_stream) override; + void Flatten(char** buffer, SessionStream* output_stream) override; void Restore(const char** buffer, ClientSession* cs) override; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_MATERIAL_ROLL_SOUND_MATERIAL_ACTION_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_ROLL_SOUND_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/skid_sound_material_action.cc b/src/ballistica/scene_v1/dynamics/material/skid_sound_material_action.cc similarity index 64% rename from src/ballistica/dynamics/material/skid_sound_material_action.cc rename to src/ballistica/scene_v1/dynamics/material/skid_sound_material_action.cc index 838e3b7f..a8c8f381 100644 --- a/src/ballistica/dynamics/material/skid_sound_material_action.cc +++ b/src/ballistica/scene_v1/dynamics/material/skid_sound_material_action.cc @@ -1,22 +1,22 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/material/skid_sound_material_action.h" +#include "ballistica/scene_v1/dynamics/material/skid_sound_material_action.h" -#include "ballistica/dynamics/dynamics.h" -#include "ballistica/dynamics/material/material_context.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/logic/session/client_session.h" -#include "ballistica/scene/scene_stream.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/scene_v1/dynamics/dynamics.h" +#include "ballistica/scene_v1/dynamics/material/material_context.h" +#include "ballistica/scene_v1/support/client_session.h" +#include "ballistica/scene_v1/support/session_stream.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::scene_v1 { auto SkidSoundMaterialAction::GetFlattenedSize() -> size_t { return 4 + 2 + 2; } void SkidSoundMaterialAction::Flatten(char** buffer, - SceneStream* output_stream) { + SessionStream* output_stream) { Utils::EmbedInt32NBO(buffer, static_cast_check_fit( - output_stream->GetSoundID(sound.get()))); + output_stream->GetSoundID(sound.Get()))); Utils::EmbedFloat16NBO(buffer, target_impulse); Utils::EmbedFloat16NBO(buffer, volume); } @@ -31,23 +31,23 @@ void SkidSoundMaterialAction::Apply(MaterialContext* context, const Part* src_part, const Part* dst_part, const Object::Ref& p) { assert(context && src_part && dst_part); - assert(context->dynamics.exists()); + assert(context->dynamics.Exists()); assert(context->dynamics->in_process()); // For now lets avoid this in low-quality graphics mode // (should we make a low-quality sound mode?). - if (g_graphics_server - && g_graphics_server->quality() < GraphicsQuality::kMedium) { + if (g_base->graphics_server + && g_base->graphics_server->quality() < base::GraphicsQuality::kMedium) { return; } // Let's limit the amount of skid-sounds we spawn, otherwise we'll start // using up all our sound resources on skids when things get messy. if (context->dynamics->skid_sound_count() < 2) { - context->skid_sounds.emplace_back(context, sound.get(), target_impulse, + context->skid_sounds.emplace_back(context, sound.Get(), target_impulse, volume); context->complex_sound = true; } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/dynamics/material/skid_sound_material_action.h b/src/ballistica/scene_v1/dynamics/material/skid_sound_material_action.h similarity index 54% rename from src/ballistica/dynamics/material/skid_sound_material_action.h rename to src/ballistica/scene_v1/dynamics/material/skid_sound_material_action.h index 63723629..432c388a 100644 --- a/src/ballistica/dynamics/material/skid_sound_material_action.h +++ b/src/ballistica/scene_v1/dynamics/material/skid_sound_material_action.h @@ -1,22 +1,22 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_MATERIAL_SKID_SOUND_MATERIAL_ACTION_H_ -#define BALLISTICA_DYNAMICS_MATERIAL_SKID_SOUND_MATERIAL_ACTION_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_SKID_SOUND_MATERIAL_ACTION_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_SKID_SOUND_MATERIAL_ACTION_H_ -#include "ballistica/assets/component/sound.h" -#include "ballistica/ballistica.h" -#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/dynamics/material/material_action.h" +#include "ballistica/shared/ballistica.h" -namespace ballistica { +namespace ballistica::scene_v1 { // sound created based on collision forces perpendicular to the collision normal class SkidSoundMaterialAction : public MaterialAction { public: SkidSoundMaterialAction() = default; - SkidSoundMaterialAction(Sound* sound_in, float target_impulse_in, + SkidSoundMaterialAction(SceneSound* sound_in, float target_impulse_in, float volume_in) : sound(sound_in), target_impulse(target_impulse_in), volume(volume_in) {} - Object::Ref sound; + Object::Ref sound; float target_impulse{}; float volume{}; void Apply(MaterialContext* context, const Part* src_part, @@ -24,10 +24,10 @@ class SkidSoundMaterialAction : public MaterialAction { const Object::Ref& p) override; auto GetType() const -> Type override { return Type::SKID_SOUND; } auto GetFlattenedSize() -> size_t override; - void Flatten(char** buffer, SceneStream* output_stream) override; + void Flatten(char** buffer, SessionStream* output_stream) override; void Restore(const char** buffer, ClientSession* cs) override; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_MATERIAL_SKID_SOUND_MATERIAL_ACTION_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_SKID_SOUND_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/material/sound_material_action.cc b/src/ballistica/scene_v1/dynamics/material/sound_material_action.cc similarity index 58% rename from src/ballistica/dynamics/material/sound_material_action.cc rename to src/ballistica/scene_v1/dynamics/material/sound_material_action.cc index 30d12696..ac0e5cce 100644 --- a/src/ballistica/dynamics/material/sound_material_action.cc +++ b/src/ballistica/scene_v1/dynamics/material/sound_material_action.cc @@ -1,26 +1,26 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/material/sound_material_action.h" +#include "ballistica/scene_v1/dynamics/material/sound_material_action.h" -#include "ballistica/dynamics/material/material_context.h" -#include "ballistica/generic/utils.h" -#include "ballistica/logic/session/client_session.h" -#include "ballistica/scene/scene_stream.h" +#include "ballistica/scene_v1/dynamics/material/material_context.h" +#include "ballistica/scene_v1/support/client_session.h" +#include "ballistica/scene_v1/support/session_stream.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::scene_v1 { void SoundMaterialAction::Apply(MaterialContext* context, const Part* src_part, const Part* dst_part, const Object::Ref& p) { assert(context && src_part && dst_part); - context->connect_sounds.emplace_back(sound_.get(), volume_); + context->connect_sounds.emplace_back(sound_.Get(), volume_); } auto SoundMaterialAction::GetFlattenedSize() -> size_t { return 4 + 2; } -void SoundMaterialAction::Flatten(char** buffer, SceneStream* output_stream) { +void SoundMaterialAction::Flatten(char** buffer, SessionStream* output_stream) { Utils::EmbedInt32NBO(buffer, static_cast_check_fit( - output_stream->GetSoundID(sound_.get()))); + output_stream->GetSoundID(sound_.Get()))); Utils::EmbedFloat16NBO(buffer, volume_); } @@ -29,4 +29,4 @@ void SoundMaterialAction::Restore(const char** buffer, ClientSession* cs) { volume_ = Utils::ExtractFloat16NBO(buffer); } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/dynamics/material/sound_material_action.h b/src/ballistica/scene_v1/dynamics/material/sound_material_action.h new file mode 100644 index 00000000..0ae48ce9 --- /dev/null +++ b/src/ballistica/scene_v1/dynamics/material/sound_material_action.h @@ -0,0 +1,32 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_SOUND_MATERIAL_ACTION_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_SOUND_MATERIAL_ACTION_H_ + +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/dynamics/material/material_action.h" +#include "ballistica/shared/ballistica.h" + +namespace ballistica::scene_v1 { + +class SoundMaterialAction : public MaterialAction { + public: + SoundMaterialAction() = default; + SoundMaterialAction(SceneSound* sound_in, float volume_in) + : sound_(sound_in), volume_(volume_in) {} + void Apply(MaterialContext* context, const Part* src_part, + const Part* dst_part, + const Object::Ref& p) override; + auto GetType() const -> Type override { return Type::SOUND; } + auto GetFlattenedSize() -> size_t override; + void Flatten(char** buffer, SessionStream* output_stream) override; + void Restore(const char** buffer, ClientSession* cs) override; + + private: + Object::Ref sound_; + float volume_{}; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_DYNAMICS_MATERIAL_SOUND_MATERIAL_ACTION_H_ diff --git a/src/ballistica/dynamics/part.cc b/src/ballistica/scene_v1/dynamics/part.cc similarity index 87% rename from src/ballistica/dynamics/part.cc rename to src/ballistica/scene_v1/dynamics/part.cc index 17263e5e..21476cb3 100644 --- a/src/ballistica/dynamics/part.cc +++ b/src/ballistica/scene_v1/dynamics/part.cc @@ -1,19 +1,19 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/part.h" +#include "ballistica/scene_v1/dynamics/part.h" -#include "ballistica/dynamics/dynamics.h" -#include "ballistica/dynamics/material/material.h" -#include "ballistica/generic/buffer.h" -#include "ballistica/scene/scene.h" +#include "ballistica/scene_v1/dynamics/dynamics.h" +#include "ballistica/scene_v1/dynamics/material/material.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::scene_v1 { Part::Part(Node* node, bool default_collide) : our_id_(node->AddPart(this)), default_collides_(default_collide), node_(node) { - assert(node_.exists()); + assert(node_.Exists()); birth_time_ = node_->scene()->time(); dynamics_ = node_->scene()->dynamics(); } @@ -58,7 +58,7 @@ void Part::SetMaterials(const std::vector& vals) { void Part::ApplyMaterials(MaterialContext* s, const Part* src_part, const Part* dst_part) { for (auto&& i : materials_) { - assert(i.exists()); + assert(i.Exists()); i->Apply(s, src_part, dst_part); } } @@ -66,8 +66,8 @@ void Part::ApplyMaterials(MaterialContext* s, const Part* src_part, auto Part::ContainsMaterial(const Material* m) const -> bool { assert(m); for (auto&& i : materials_) { - assert(i.exists()); - if (m == i.get()) { + assert(i.Exists()); + if (m == i.Get()) { return true; } } @@ -125,9 +125,9 @@ void Part::SetCollidingWith(int64_t node_id, int part, bool colliding, } auto Part::GetAge() const -> millisecs_t { - assert(node_.exists()); + assert(node_.Exists()); assert(node_->scene()->time() >= birth_time_); return node_->scene()->time() - birth_time_; } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/dynamics/part.h b/src/ballistica/scene_v1/dynamics/part.h similarity index 92% rename from src/ballistica/dynamics/part.h rename to src/ballistica/scene_v1/dynamics/part.h index 042ab6d1..67a82980 100644 --- a/src/ballistica/dynamics/part.h +++ b/src/ballistica/scene_v1/dynamics/part.h @@ -1,14 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_PART_H_ -#define BALLISTICA_DYNAMICS_PART_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_PART_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_PART_H_ #include -#include "ballistica/core/object.h" -#include "ballistica/dynamics/rigid_body.h" +#include "ballistica/scene_v1/dynamics/rigid_body.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::scene_v1 { // A categorized "part" of a node which contains collision and other grouping // information for a set of rigid bodies composing the part. @@ -42,8 +42,8 @@ class Part : public Object { } } auto node() const -> Node* { - assert(node_.exists()); - return node_.get(); + assert(node_.Exists()); + return node_.Get(); } // Apply a set of materials to the part. @@ -134,6 +134,6 @@ class Part : public Object { millisecs_t last_roll_sound_time_ = 0; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_PART_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_PART_H_ diff --git a/src/ballistica/dynamics/rigid_body.cc b/src/ballistica/scene_v1/dynamics/rigid_body.cc similarity index 85% rename from src/ballistica/dynamics/rigid_body.cc rename to src/ballistica/scene_v1/dynamics/rigid_body.cc index 2e795adb..84588e9a 100644 --- a/src/ballistica/dynamics/rigid_body.cc +++ b/src/ballistica/scene_v1/dynamics/rigid_body.cc @@ -1,16 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/dynamics/rigid_body.h" +#include "ballistica/scene_v1/dynamics/rigid_body.h" -#include "ballistica/assets/component/collide_model.h" -#include "ballistica/dynamics/dynamics.h" -#include "ballistica/dynamics/part.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/renderer.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/graphics/component/render_component.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/scene_v1/assets/scene_collision_mesh.h" +#include "ballistica/scene_v1/dynamics/dynamics.h" +#include "ballistica/scene_v1/dynamics/part.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/math/random.h" #include "ode/ode_collision_util.h" -namespace ballistica { +namespace ballistica::scene_v1 { // whether to send our net states as half float format #define USE_HALF_FLOATS 1 @@ -33,13 +35,13 @@ namespace ballistica { RigidBody::RigidBody(int id_in, Part* part_in, Type type_in, Shape shape_in, uint32_t collide_type_in, uint32_t collide_mask_in, - CollideModel* collide_model_in, uint32_t flags) + SceneCollisionMesh* collision_mesh_in, uint32_t flags) : type_(type_in), id_(id_in), creation_time_(part_in->node()->scene()->time()), shape_(shape_in), part_(part_in), - collide_model_(collide_model_in), + collision_mesh_(collision_mesh_in), collide_type_(collide_type_in), collide_mask_(collide_mask_in), flags_(flags) { @@ -51,7 +53,7 @@ RigidBody::RigidBody(int id_in, Part* part_in, Type type_in, Shape shape_in, } #endif // BA_DEBUG_BUILD - assert(part_.exists()); + assert(part_.Exists()); birth_time_ = part_->node()->scene()->stepnum(); dynamics_ = part_->node()->scene()->dynamics(); @@ -112,11 +114,11 @@ RigidBody::RigidBody(int id_in, Part* part_in, Type type_in, Shape shape_in, // NOTE - we don't add trimeshes do the collision space - we handle them // specially.. dimensions_[0] = dimensions_[1] = dimensions_[2] = 0.6f; - assert(collide_model_.exists()); - collide_model_->collide_model_data()->Load(); + assert(collision_mesh_.Exists()); + collision_mesh_->collision_mesh_data()->Load(); dGeomID g = dCreateTriMesh( - nullptr, collide_model_->collide_model_data()->GetMeshData(), nullptr, - nullptr, nullptr); + nullptr, collision_mesh_->collision_mesh_data()->GetMeshData(), + nullptr, nullptr, nullptr); geoms_.push_back(g); dynamics_->AddTrimesh(g); break; @@ -148,7 +150,49 @@ RigidBody::RigidBody(int id_in, Part* part_in, Type type_in, Shape shape_in, SetDimensions(dimensions_[0], dimensions_[1], dimensions_[2]); } -auto RigidBody::Check() -> void { +void RigidBody::ApplyToRenderComponent(base::RenderComponent* c) { + const dReal* pos_in; + const dReal* r_in; + auto geom = geoms_[0]; + if (type() == scene_v1::RigidBody::Type::kBody) { + pos_in = dBodyGetPosition(body_); + r_in = dBodyGetRotation(body_); + } else { + pos_in = dGeomGetPosition(geom); + r_in = dGeomGetRotation(geom); + } + float pos[3]; + float r[12]; + for (int x = 0; x < 3; x++) { + pos[x] = pos_in[x]; + } + pos[0] += blend_offset().x; + pos[1] += blend_offset().y; + pos[2] += blend_offset().z; + for (int x = 0; x < 12; x++) { + r[x] = r_in[x]; + } + float matrix[16]; + matrix[0] = r[0]; + matrix[1] = r[4]; + matrix[2] = r[8]; + matrix[3] = 0; + matrix[4] = r[1]; + matrix[5] = r[5]; + matrix[6] = r[9]; + matrix[7] = 0; + matrix[8] = r[2]; + matrix[9] = r[6]; + matrix[10] = r[10]; + matrix[11] = 0; + matrix[12] = pos[0]; + matrix[13] = pos[1]; + matrix[14] = pos[2]; + matrix[15] = 1; + c->MultMatrix(matrix); +} + +void RigidBody::Check() { if (type_ == Type::kBody) { const dReal* p = dBodyGetPosition(body_); const dReal* q = dBodyGetQuaternion(body_); @@ -190,7 +234,7 @@ RigidBody::~RigidBody() { KillConstraints(); // remove ourself from our parent part if we have one - if (part_.exists()) { + if (part_.Exists()) { part_->RemoveBody(this); } if (type_ == Type::kBody) { @@ -204,7 +248,7 @@ RigidBody::~RigidBody() { } } -auto RigidBody::KillConstraints() -> void { +void RigidBody::KillConstraints() { while (joints_.begin() != joints_.end()) { (**joints_.begin()).Kill(); } @@ -231,7 +275,7 @@ auto RigidBody::GetEmbeddedSizeFull() -> int { // store a body to a buffer // FIXME - theoretically we should embed birth-time // as this can affect collisions with this object -auto RigidBody::EmbedFull(char** buffer) -> void { +void RigidBody::EmbedFull(char** buffer) { assert(type_ == Type::kBody); const dReal* p = dBodyGetPosition(body_); @@ -274,7 +318,7 @@ auto RigidBody::EmbedFull(char** buffer) -> void { } // Position a body from buffer data. -auto RigidBody::ExtractFull(const char** buffer) -> void { +void RigidBody::ExtractFull(const char** buffer) { assert(type_ == Type::kBody); dReal p[3], lv[3], av[3]; @@ -324,23 +368,22 @@ auto RigidBody::ExtractFull(const char** buffer) -> void { } } -auto RigidBody::Draw(RenderPass* pass, bool shaded) -> void { +void RigidBody::Draw(base::RenderPass* pass, bool shaded) { assert(pass); - RenderPass::Type pass_type = pass->type(); + base::RenderPass::Type pass_type = pass->type(); // only passes we draw in are light_shadow and beauty - if (pass_type != RenderPass::Type::kLightShadowPass - && pass_type != RenderPass::Type::kBeautyPass) { + if (pass_type != base::RenderPass::Type::kLightShadowPass + && pass_type != base::RenderPass::Type::kBeautyPass) { return; } // assume trimeshes are landscapes and shouldn't be in shadow passes.. if (shape_ == Shape::kTrimesh - && (pass_type != RenderPass::Type::kBeautyPass)) { + && (pass_type != base::RenderPass::Type::kBeautyPass)) { return; } } -auto RigidBody::AddCallback(CollideCallbackFunc callbackIn, void* data_in) - -> void { +void RigidBody::AddCallback(CollideCallbackFunc callbackIn, void* data_in) { CollideCallback c{}; c.callback = callbackIn; c.data = data_in; @@ -358,8 +401,8 @@ auto RigidBody::CallCollideCallbacks(dContact* contacts, int count, return true; } -auto RigidBody::SetDimensions(float d1, float d2, float d3, float m1, float m2, - float m3, float density_mult) -> void { +void RigidBody::SetDimensions(float d1, float d2, float d3, float m1, float m2, + float m3, float density_mult) { dimensions_[0] = d1; dimensions_[1] = d2; dimensions_[2] = d3; @@ -567,8 +610,8 @@ auto RigidBody::ApplyImpulse(float px, float py, float pz, float vx, float vy, return total_mag; } -auto RigidBody::ApplyGlobalImpulse(float px, float py, float pz, float fx, - float fy, float fz) -> void { +void RigidBody::ApplyGlobalImpulse(float px, float py, float pz, float fx, + float fy, float fz) { if (type_ != Type::kBody) { return; } @@ -579,7 +622,7 @@ auto RigidBody::ApplyGlobalImpulse(float px, float py, float pz, float fx, RigidBody::Joint::Joint() = default; -auto RigidBody::Joint::SetJoint(dxJointFixed* id_in, Scene* scene) -> void { +void RigidBody::Joint::SetJoint(dxJointFixed* id_in, Scene* scene) { Kill(); creation_time_ = scene->time(); id_ = id_in; @@ -587,8 +630,7 @@ auto RigidBody::Joint::SetJoint(dxJointFixed* id_in, Scene* scene) -> void { RigidBody::Joint::~Joint() { Kill(); } -auto RigidBody::Joint::AttachToBodies(RigidBody* b1_in, RigidBody* b2_in) - -> void { +void RigidBody::Joint::AttachToBodies(RigidBody* b1_in, RigidBody* b2_in) { assert(id_); b1_ = b1_in; b2_ = b2_in; @@ -607,7 +649,7 @@ auto RigidBody::Joint::AttachToBodies(RigidBody* b1_in, RigidBody* b2_in) dJointAttach(id_, b_id_1, b_id_2); } -auto RigidBody::Joint::Kill() -> void { +void RigidBody::Joint::Kill() { if (id_) { if (b1_) { b1_->RemoveJoint(this); @@ -670,13 +712,13 @@ auto RigidBody::GetTransform() -> Matrix44f { return matrix; } -auto RigidBody::AddBlendOffset(float x, float y, float z) -> void { +void RigidBody::AddBlendOffset(float x, float y, float z) { // blend_offset_.x += x; // blend_offset_.y += y; // blend_offset_.z += z; } -auto RigidBody::UpdateBlending() -> void { +void RigidBody::UpdateBlending() { // FIXME - this seems broken. We never update blend_time_ currently // and its also set to time whereas we're comparing it with steps. // Should revisit. @@ -689,4 +731,4 @@ auto RigidBody::UpdateBlending() -> void { // } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/dynamics/rigid_body.h b/src/ballistica/scene_v1/dynamics/rigid_body.h similarity index 81% rename from src/ballistica/dynamics/rigid_body.h rename to src/ballistica/scene_v1/dynamics/rigid_body.h index d3fe6831..a7c90622 100644 --- a/src/ballistica/dynamics/rigid_body.h +++ b/src/ballistica/scene_v1/dynamics/rigid_body.h @@ -1,17 +1,19 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_DYNAMICS_RIGID_BODY_H_ -#define BALLISTICA_DYNAMICS_RIGID_BODY_H_ +#ifndef BALLISTICA_SCENE_V1_DYNAMICS_RIGID_BODY_H_ +#define BALLISTICA_SCENE_V1_DYNAMICS_RIGID_BODY_H_ #include #include -#include "ballistica/core/object.h" -#include "ballistica/math/matrix44f.h" +#include "ballistica/base/base.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/math/matrix44f.h" #include "ode/ode.h" #include "ode/ode_joint.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Wrapper for ode rigid bodies which implements collision tracking, // flattening/restoring, and other extras. @@ -78,49 +80,48 @@ class RigidBody : public Object { // these are needed for full states auto GetEmbeddedSizeFull() -> int; - auto ExtractFull(const char** buffer) -> void; - auto EmbedFull(char** buffer) -> void; + void ExtractFull(const char** buffer); + void EmbedFull(char** buffer); RigidBody(int id_in, Part* part_in, Type type_in, Shape shape_in, uint32_t collide_type_in, uint32_t collide_mask_in, - CollideModel* collide_model_in = nullptr, uint32_t flags = 0); + SceneCollisionMesh* collision_mesh_in = nullptr, + uint32_t flags = 0); ~RigidBody() override; auto body() const -> dBodyID { return body_; } auto geom(int i = 0) const -> dGeomID { return geoms_[i]; } // Draw a representation of the rigid body for debugging. - auto Draw(RenderPass* pass, bool shaded = true) -> void; + void Draw(base::RenderPass* pass, bool shaded = true); auto part() const -> Part* { - assert(part_.exists()); - return part_.get(); + assert(part_.Exists()); + return part_.Get(); } - auto Wake() -> void { + void Wake() { if (body_) { dBodyEnable(body_); } } - auto AddCallback(CollideCallbackFunc callback_in, void* data_in) -> void; + void AddCallback(CollideCallbackFunc callback_in, void* data_in); auto CallCollideCallbacks(dContact* contacts, int count, RigidBody* opposingbody) -> bool; - auto SetDimensions( + void SetDimensions( float d1, float d2 = 0.0f, float d3 = 0.0f, // body dimensions float m1 = 0.0f, float m2 = 0.0f, float m3 = 0.0f, // Mass dimensions (default to regular if zero). - float density = 1.0f) -> void; + float density = 1.0f); // If geomWakeOnCollide is true, a GEOM_ONLY object colliding with a sleeping // body will wake it up. Generally this should be true if the geom is moving // or changing. - auto set_geom_wake_on_collide(bool enable) -> void { - geom_wake_on_collide_ = enable; - } + void set_geom_wake_on_collide(bool enable) { geom_wake_on_collide_ = enable; } auto geom_wake_on_collide() const -> bool { return geom_wake_on_collide_; } auto id() const -> int { return id_; } - auto ApplyGlobalImpulse(float px, float py, float pz, float fx, float fy, - float fz) -> void; + void ApplyGlobalImpulse(float px, float py, float pz, float fx, float fy, + float fz); auto ApplyImpulse(float px, float py, float pz, float vx, float vy, float vz, float fdirx, float fdiry, float fdirz, float mag, float v_mag, float radiusm, bool calc_only) -> float; - auto KillConstraints() -> void; + void KillConstraints(); // Rigid body joint wrapper. This takes ownership of joints it is passed // all joints should use this mechanism so they are automatically @@ -153,8 +154,8 @@ class RigidBody : public Object { }; // Used by Joint. - auto AddJoint(Joint* j) -> void { joints_.push_back(j); } - auto RemoveJoint(Joint* j) -> void { + void AddJoint(Joint* j) { joints_.push_back(j); } + void RemoveJoint(Joint* j) { for (auto i = joints_.begin(); i != joints_.end(); i++) { if ((*i) == j) { joints_.erase(i); @@ -162,26 +163,26 @@ class RigidBody : public Object { } } } - auto Check() -> void; + void Check(); auto type() const -> Type { return type_; } auto collide_type() const -> uint32_t { return collide_type_; } auto collide_mask() const -> uint32_t { return collide_mask_; } auto flags() const -> uint32_t { return flags_; } - auto set_flags(uint32_t flags) -> void { flags_ = flags; } + void set_flags(uint32_t flags) { flags_ = flags; } auto can_cause_impact_damage() const -> bool { return can_cause_impact_damage_; } - auto set_can_cause_impact_damage(bool val) -> void { - can_cause_impact_damage_ = val; - } + void set_can_cause_impact_damage(bool val) { can_cause_impact_damage_ = val; } // Applies to spheres. auto radius() const -> float { return dimensions_[0]; } auto GetTransform() -> Matrix44f; - auto UpdateBlending() -> void; - auto AddBlendOffset(float x, float y, float z) -> void; + void UpdateBlending(); + void AddBlendOffset(float x, float y, float z); auto blend_offset() const -> const Vector3f& { return blend_offset_; } + void ApplyToRenderComponent(base::RenderComponent* c); + private: Vector3f blend_offset_{0.0f, 0.0f, 0.0f}; millisecs_t blend_time_{}; @@ -198,7 +199,7 @@ class RigidBody : public Object { std::list joints_; bool geom_wake_on_collide_{}; int id_{}; - Object::Ref collide_model_; + Object::Ref collision_mesh_; float dimensions_[3]{}; Type type_{}; Shape shape_{}; @@ -214,6 +215,6 @@ class RigidBody : public Object { uint32_t flags_{}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_DYNAMICS_RIGID_BODY_H_ +#endif // BALLISTICA_SCENE_V1_DYNAMICS_RIGID_BODY_H_ diff --git a/src/ballistica/scene/node/anim_curve_node.cc b/src/ballistica/scene_v1/node/anim_curve_node.cc similarity index 92% rename from src/ballistica/scene/node/anim_curve_node.cc rename to src/ballistica/scene_v1/node/anim_curve_node.cc index 94c796e8..9b1361eb 100644 --- a/src/ballistica/scene/node/anim_curve_node.cc +++ b/src/ballistica/scene_v1/node/anim_curve_node.cc @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/anim_curve_node.h" +#include "ballistica/scene_v1/node/anim_curve_node.h" #include -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" -namespace ballistica { +namespace ballistica::scene_v1 { class AnimCurveNodeType : public NodeType { public: @@ -91,7 +91,7 @@ auto AnimCurveNode::GetOut() -> float { } } if (!got) { - out_ = keyframes_[0].value; + // out_ = keyframes_[0].value; // Ok we know we've got at least 2 keyframes. auto i1 = keyframes_.begin(); @@ -133,4 +133,4 @@ auto AnimCurveNode::GetOut() -> float { return out_; } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/anim_curve_node.h b/src/ballistica/scene_v1/node/anim_curve_node.h similarity index 84% rename from src/ballistica/scene/node/anim_curve_node.h rename to src/ballistica/scene_v1/node/anim_curve_node.h index 23ca037d..dec0c765 100644 --- a/src/ballistica/scene/node/anim_curve_node.h +++ b/src/ballistica/scene_v1/node/anim_curve_node.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_ANIM_CURVE_NODE_H_ -#define BALLISTICA_SCENE_NODE_ANIM_CURVE_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_ANIM_CURVE_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_ANIM_CURVE_NODE_H_ #include -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Node containing a keyframe graph associating an input value with an output // value. @@ -70,6 +70,6 @@ class AnimCurveNode : public Node { float offset_ = 0.0f; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_ANIM_CURVE_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_ANIM_CURVE_NODE_H_ diff --git a/src/ballistica/scene/node/bomb_node.cc b/src/ballistica/scene_v1/node/bomb_node.cc similarity index 79% rename from src/ballistica/scene/node/bomb_node.cc rename to src/ballistica/scene_v1/node/bomb_node.cc index 844267c1..2aef4e06 100644 --- a/src/ballistica/scene/node/bomb_node.cc +++ b/src/ballistica/scene_v1/node/bomb_node.cc @@ -1,12 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/bomb_node.h" +#include "ballistica/scene_v1/node/bomb_node.h" -#include "ballistica/assets/component/collide_model.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/scene_v1/assets/scene_collision_mesh.h" +#include "ballistica/scene_v1/support/scene.h" -namespace ballistica { +namespace ballistica::scene_v1 { const float kFuseOffset = 0.35f; @@ -48,7 +48,7 @@ void BombNode::OnCreate() { void BombNode::Step() { PropNode::Step(); - if (body_.exists()) { + if (body_.Exists()) { // Update our fuse and light position. dVector3 fuse_tip_pos; dGeomGetRelPointPos(body_->geom(), 0, (fuse_length_ + kFuseOffset), 0, @@ -57,14 +57,14 @@ void BombNode::Step() { light_translate_.y = fuse_tip_pos[1] + body_->blend_offset().y; light_translate_.z = fuse_tip_pos[2] + body_->blend_offset().z; #if !BA_HEADLESS_BUILD - fuse_.SetTransform(Matrix44fTranslate(0, kFuseOffset * model_scale_, 0) + fuse_.SetTransform(Matrix44fTranslate(0, kFuseOffset * mesh_scale_, 0) * body_->GetTransform()); fuse_.SetLength(fuse_length_); #endif // !BA_HEADLESS_BUILD } } -void BombNode::Draw(FrameDef* frame_def) { +void BombNode::Draw(base::FrameDef* frame_def) { #if !BA_HEADLESS_BUILD PropNode::Draw(frame_def); float s_scale, s_density; @@ -76,9 +76,9 @@ void BombNode::Draw(FrameDef* frame_def) { float g = 0.1f * intensity; float b = 0.1f * intensity; float a = 0.0f; - g_graphics->DrawBlotchSoft(light_translate_, s, r, g, b, a); - g_graphics->DrawBlotchSoftObj(light_translate_, s, r, g, b, a); + g_base->graphics->DrawBlotchSoft(light_translate_, s, r, g, b, a); + g_base->graphics->DrawBlotchSoftObj(light_translate_, s, r, g, b, a); #endif // !BA_HEADLESS_BUILD } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/bomb_node.h b/src/ballistica/scene_v1/node/bomb_node.h similarity index 54% rename from src/ballistica/scene/node/bomb_node.h rename to src/ballistica/scene_v1/node/bomb_node.h index 285a3586..4ce6791b 100644 --- a/src/ballistica/scene/node/bomb_node.h +++ b/src/ballistica/scene_v1/node/bomb_node.h @@ -1,31 +1,31 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_BOMB_NODE_H_ -#define BALLISTICA_SCENE_NODE_BOMB_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_BOMB_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_BOMB_NODE_H_ -#include "ballistica/dynamics/bg/bg_dynamics_fuse.h" -#include "ballistica/scene/node/prop_node.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_fuse.h" +#include "ballistica/scene_v1/node/prop_node.h" -namespace ballistica { +namespace ballistica::scene_v1 { class BombNode : public PropNode { public: static auto InitType() -> NodeType*; explicit BombNode(Scene* scene); void Step() override; - void Draw(FrameDef* frame_def) override; + void Draw(base::FrameDef* frame_def) override; void OnCreate() override; auto fuse_length() const -> float { return fuse_length_; } void set_fuse_length(float val) { fuse_length_ = val; } protected: #if !BA_HEADLESS_BUILD - BGDynamicsFuse fuse_; + base::BGDynamicsFuse fuse_; #endif float fuse_length_ = 1.0f; Vector3f light_translate_ = {0.0f, 0.0f, 0.0f}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_BOMB_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_BOMB_NODE_H_ diff --git a/src/ballistica/scene/node/combine_node.cc b/src/ballistica/scene_v1/node/combine_node.cc similarity index 87% rename from src/ballistica/scene/node/combine_node.cc rename to src/ballistica/scene_v1/node/combine_node.cc index 0d94572d..10f5941b 100644 --- a/src/ballistica/scene/node/combine_node.cc +++ b/src/ballistica/scene_v1/node/combine_node.cc @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/combine_node.h" +#include "ballistica/scene_v1/node/combine_node.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" -namespace ballistica { +namespace ballistica::scene_v1 { class CombineNodeType : public NodeType { public: @@ -62,4 +62,4 @@ auto CombineNode::GetOutput() -> std::vector { return output_; } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/combine_node.h b/src/ballistica/scene_v1/node/combine_node.h similarity index 82% rename from src/ballistica/scene/node/combine_node.h rename to src/ballistica/scene_v1/node/combine_node.h index 97a784f2..9848998a 100644 --- a/src/ballistica/scene/node/combine_node.h +++ b/src/ballistica/scene_v1/node/combine_node.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_COMBINE_NODE_H_ -#define BALLISTICA_SCENE_NODE_COMBINE_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_COMBINE_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_COMBINE_NODE_H_ #include -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { // A node used to combine individual input values into one array output value class CombineNode : public Node { @@ -53,6 +53,6 @@ class CombineNode : public Node { bool dirty_ = true; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_COMBINE_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_COMBINE_NODE_H_ diff --git a/src/ballistica/scene/node/explosion_node.cc b/src/ballistica/scene_v1/node/explosion_node.cc similarity index 71% rename from src/ballistica/scene/node/explosion_node.cc rename to src/ballistica/scene_v1/node/explosion_node.cc index 27f6367e..3cabd8b8 100644 --- a/src/ballistica/scene/node/explosion_node.cc +++ b/src/ballistica/scene_v1/node/explosion_node.cc @@ -1,15 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/explosion_node.h" +#include "ballistica/scene_v1/node/explosion_node.h" -#include "ballistica/graphics/camera.h" -#include "ballistica/graphics/component/object_component.h" -#include "ballistica/graphics/component/post_process_component.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/graphics/component/object_component.h" +#include "ballistica/base/graphics/component/post_process_component.h" +#include "ballistica/base/graphics/support/camera.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/scene_v1/support/scene.h" -namespace ballistica { +namespace ballistica::scene_v1 { class ExplosionNodeType : public NodeType { public: @@ -37,15 +37,15 @@ auto ExplosionNode::InitType() -> NodeType* { return node_type; } -ExplosionNode* gExplosionDistortLock = nullptr; +ExplosionNode* g_explosion_distort_lock{}; ExplosionNode::ExplosionNode(Scene* scene) : Node(scene, node_type), birth_time_(scene->time()) {} ExplosionNode::~ExplosionNode() { if (draw_distortion_ && have_distortion_lock_) { - assert(gExplosionDistortLock == this); - gExplosionDistortLock = nullptr; + assert(g_explosion_distort_lock == this); + g_explosion_distort_lock = nullptr; } } @@ -94,9 +94,9 @@ void ExplosionNode::Step() { } } -void ExplosionNode::Draw(FrameDef* frame_def) { +void ExplosionNode::Draw(base::FrameDef* frame_def) { { - bool high_quality = (frame_def->quality() >= GraphicsQuality::kHigh); + bool high_quality = (frame_def->quality() >= base::GraphicsQuality::kHigh); // we only draw distortion if we're the only bomb.. // (it gets expensive..) if (check_draw_distortion_) { @@ -104,17 +104,17 @@ void ExplosionNode::Draw(FrameDef* frame_def) { { if (big_) { // Steal distortion handle. - if (gExplosionDistortLock != nullptr) { - gExplosionDistortLock->draw_distortion_ = false; - gExplosionDistortLock = this; + if (g_explosion_distort_lock != nullptr) { + g_explosion_distort_lock->draw_distortion_ = false; + g_explosion_distort_lock = this; have_distortion_lock_ = true; draw_distortion_ = true; } } else { // Play nice and only distort if no one else currently is. - if (gExplosionDistortLock == nullptr) { + if (g_explosion_distort_lock == nullptr) { draw_distortion_ = true; - gExplosionDistortLock = this; + g_explosion_distort_lock = this; have_distortion_lock_ = true; } else { draw_distortion_ = false; @@ -135,7 +135,7 @@ void ExplosionNode::Draw(FrameDef* frame_def) { } float s = 1.0f; if (high_quality) { - PostProcessComponent c(frame_def->blit_pass()); + base::PostProcessComponent c(frame_def->blit_pass()); c.setNormalDistort(0.5f * amt); c.PushTransform(); c.Translate(position_[0], position_[1], position_[2]); @@ -143,17 +143,17 @@ void ExplosionNode::Draw(FrameDef* frame_def) { 1.0f + s * 0.8f * 0.0015f * age, 1.0f + s * 0.8f * 0.025f * age); c.Scale(0.7f, 0.7f, 0.7f); - c.DrawModel(g_assets->GetModel(SystemModelID::kShockWave), - kModelDrawFlagNoReflection); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kShockWave), + base::kMeshDrawFlagNoReflection); c.PopTransform(); c.Submit(); } else { - // simpler transparent shock wave - // draw our distortion wave in the overlay pass - ObjectComponent c(frame_def->beauty_pass()); + // Simpler transparent shock wave. + // Draw our distortion wave in the overlay pass. + base::ObjectComponent c(frame_def->beauty_pass()); c.SetTransparent(true); - c.SetLightShadow(LightShadowType::kNone); - // eww hacky - the shock wave model uses color as distortion amount + c.SetLightShadow(base::LightShadowType::kNone); + // Eww hacky - the shock wave shader uses color as distortion amount. c.SetColor(1.0f, 0.7f, 0.7f, 0.06f * amt); c.PushTransform(); c.Translate(position_[0], position_[1], position_[2]); @@ -161,8 +161,8 @@ void ExplosionNode::Draw(FrameDef* frame_def) { 1.0f + s * 0.8f * 0.0015f * age, 1.0f + s * 0.8f * 0.025f * age); c.Scale(0.7f, 0.7f, 0.7f); - c.DrawModel(g_assets->GetModel(SystemModelID::kShockWave), - kModelDrawFlagNoReflection); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kShockWave), + base::kMeshDrawFlagNoReflection); c.PopTransform(); c.Submit(); } @@ -193,12 +193,12 @@ void ExplosionNode::Draw(FrameDef* frame_def) { } s *= 0.75f; float cx, cy, cz; - g_graphics->camera()->get_position(&cx, &cy, &cz); - ObjectComponent c(frame_def->beauty_pass()); + g_base->graphics->camera()->get_position(&cx, &cy, &cz); + base::ObjectComponent c(frame_def->beauty_pass()); c.SetTransparent(true); - c.SetLightShadow(LightShadowType::kNone); + c.SetLightShadow(base::LightShadowType::kNone); c.SetPremultiplied(true); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kExplosion)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kExplosion)); c.SetColor(1.3f * o * color_[0] * b, o * color_[1] * b, o * color_[2] * b, 0.0f); c.PushTransform(); @@ -211,17 +211,17 @@ void ExplosionNode::Draw(FrameDef* frame_def) { Matrix44f om = Matrix44fOrient(right, to_cam, up); c.MultMatrix((om * m).m); c.Scale(0.9f * s, 0.9f * s, 0.9f * s); - c.DrawModel(g_assets->GetModel(SystemModelID::kShield), - kModelDrawFlagNoReflection); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kShield), + base::kMeshDrawFlagNoReflection); c.Scale(0.6f, 0.6f, 0.6f); c.Rotate(33, 0, 1, 0); c.SetColor(o * 7.0f * color_[0], o * 7.0f * color_[1], o * 7.0f * color_[2], 0); - c.DrawModel(g_assets->GetModel(SystemModelID::kShield), - kModelDrawFlagNoReflection); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kShield), + base::kMeshDrawFlagNoReflection); c.PopTransform(); c.Submit(); } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/explosion_node.h b/src/ballistica/scene_v1/node/explosion_node.h similarity index 77% rename from src/ballistica/scene/node/explosion_node.h rename to src/ballistica/scene_v1/node/explosion_node.h index 05c8c7a7..fb35d203 100644 --- a/src/ballistica/scene/node/explosion_node.h +++ b/src/ballistica/scene_v1/node/explosion_node.h @@ -1,20 +1,20 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_EXPLOSION_NODE_H_ -#define BALLISTICA_SCENE_NODE_EXPLOSION_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_EXPLOSION_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_EXPLOSION_NODE_H_ #include -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { class ExplosionNode : public Node { public: static auto InitType() -> NodeType*; explicit ExplosionNode(Scene* scene); ~ExplosionNode() override; - void Draw(FrameDef* frame_def) override; + void Draw(base::FrameDef* frame_def) override; void Step() override; auto position() const -> std::vector { return position_; } void set_position(const std::vector& vals); @@ -39,6 +39,6 @@ class ExplosionNode : public Node { std::vector color_{0.9f, 0.3f, 0.1f}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_EXPLOSION_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_EXPLOSION_NODE_H_ diff --git a/src/ballistica/scene/node/flag_node.cc b/src/ballistica/scene_v1/node/flag_node.cc similarity index 80% rename from src/ballistica/scene/node/flag_node.cc rename to src/ballistica/scene_v1/node/flag_node.cc index b67367a4..2a79c3a5 100644 --- a/src/ballistica/scene/node/flag_node.cc +++ b/src/ballistica/scene_v1/node/flag_node.cc @@ -1,19 +1,20 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/flag_node.h" +#include "ballistica/scene_v1/node/flag_node.h" -#include "ballistica/dynamics/bg/bg_dynamics_shadow.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/area_of_interest.h" -#include "ballistica/graphics/camera.h" -#include "ballistica/graphics/component/object_component.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_shadow.h" +#include "ballistica/base/graphics/component/object_component.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/graphics/support/area_of_interest.h" +#include "ballistica/base/graphics/support/camera.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/math/random.h" -namespace ballistica { +namespace ballistica::scene_v1 { const int kFlagSizeX{5}; const int kFlagSizeY{5}; @@ -43,19 +44,17 @@ const float kGravityStrength{0.0012f}; const float kDampingStrength{0.0f}; const float kDragStrength{0.1f}; -#if !BA_HEADLESS_BUILD class FlagNode::FullShadowSet : public Object { public: - BGDynamicsShadow shadow_pole_bottom_; - BGDynamicsShadow shadow_pole_middle_; - BGDynamicsShadow shadow_pole_top_; - BGDynamicsShadow shadow_flag_; + base::BGDynamicsShadow shadow_pole_bottom_; + base::BGDynamicsShadow shadow_pole_middle_; + base::BGDynamicsShadow shadow_pole_top_; + base::BGDynamicsShadow shadow_flag_; }; class FlagNode::SimpleShadowSet : public Object { public: - BGDynamicsShadow shadow_; + base::BGDynamicsShadow shadow_; }; -#endif // !BA_HEADLESS_BUILD class FlagNodeType : public NodeType { public: @@ -100,12 +99,12 @@ FlagNode::FlagNode(Scene* scene) : Node(scene, node_type), part_(this) { ResetFlagMesh(); // Set our mesh static data and indices once. - auto indices( - Object::New(6 * (kFlagSizeX - 1) * (kFlagSizeY - 1))); + auto indices(Object::New(6 * (kFlagSizeX - 1) + * (kFlagSizeY - 1))); uint16_t* index = &indices->elements[0]; - auto v_static(Object::New>(kFlagSizeX - * kFlagSizeY)); - VertexObjectSplitStatic* vs = &v_static->elements[0]; + auto v_static(Object::New>( + kFlagSizeX * kFlagSizeY)); + base::VertexObjectSplitStatic* vs = &v_static->elements[0]; int x_inc = 65535 / (kFlagSizeX - 1); int y_inc = 65535 / (kFlagSizeY - 1); @@ -131,7 +130,7 @@ FlagNode::FlagNode(Scene* scene) : Node(scene, node_type), part_(this) { mesh_.SetStaticData(v_static); // Create our shadow set. - UpdateForGraphicsQuality(g_graphics_server->quality()); + UpdateForGraphicsQuality(g_base->graphics_server->quality()); } auto FlagNode::getPosition() const -> std::vector { @@ -176,10 +175,10 @@ void FlagNode::SetIsAreaOfInterest(bool val) { // Either make one or kill the one we had. if (val) { assert(area_of_interest_ == nullptr); - area_of_interest_ = g_graphics->camera()->NewAreaOfInterest(false); + area_of_interest_ = g_base->graphics->camera()->NewAreaOfInterest(false); } else { assert(area_of_interest_ != nullptr); - g_graphics->camera()->DeleteAreaOfInterest(area_of_interest_); + g_base->graphics->camera()->DeleteAreaOfInterest(area_of_interest_); area_of_interest_ = nullptr; } } @@ -195,7 +194,7 @@ void FlagNode::SetMaterials(const std::vector& vals) { void FlagNode::UpdateDimensions() { float density_scale = - (g_graphics->camera()->happy_thoughts_mode()) ? 0.3f : 1.0f; + (g_base->graphics->camera()->happy_thoughts_mode()) ? 0.3f : 1.0f; body_->SetDimensions(kFlagRadius, kFlagHeight - 2 * kFlagRadius, 0, kFlagMassRadius, kFlagMassHeight, 0.0f, kFlagDensity * density_scale); @@ -203,7 +202,7 @@ void FlagNode::UpdateDimensions() { FlagNode::~FlagNode() { if (area_of_interest_) - g_graphics->camera()->DeleteAreaOfInterest(area_of_interest_); + g_base->graphics->camera()->DeleteAreaOfInterest(area_of_interest_); } void FlagNode::HandleMessage(const char* data_in) { @@ -256,16 +255,15 @@ void FlagNode::HandleMessage(const char* data_in) { if (!handled) Node::HandleMessage(data_in); } -void FlagNode::Draw(FrameDef* frame_def) { -#if !BA_HEADLESS_BUILD - +void FlagNode::Draw(base::FrameDef* frame_def) { // Flag cloth. { // Update the dynamic portion of our mesh data. // FIXME - should move this all to BG dynamics thread - auto v_dynamic(Object::New>(25)); + auto v_dynamic( + Object::New>(25)); - VertexObjectSplitDynamic* vd = &v_dynamic->elements[0]; + base::VertexObjectSplitDynamic* vd = &v_dynamic->elements[0]; for (int i = 0; i < 25; i++) { vd[i].position[0] = flag_points_[i].x; vd[i].position[1] = flag_points_[i].y; @@ -283,8 +281,8 @@ void FlagNode::Draw(FrameDef* frame_def) { mesh_.SetDynamicData(v_dynamic); // Render a subtle sharp shadow in higher quality modes. - if (frame_def->quality() > GraphicsQuality::kLow) { - SimpleComponent c(frame_def->light_shadow_pass()); + if (frame_def->quality() > base::GraphicsQuality::kLow) { + base::SimpleComponent c(frame_def->light_shadow_pass()); c.SetTransparent(true); c.SetColor(color_[0] * 0.1f, color_[1] * 0.1f, color_[2] * 0.1f, 0.02f); c.SetDoubleSided(true); @@ -294,89 +292,92 @@ void FlagNode::Draw(FrameDef* frame_def) { // Now beauty pass. { - ObjectComponent c(frame_def->beauty_pass()); + base::ObjectComponent c(frame_def->beauty_pass()); c.SetWorldSpace(true); c.SetColor(color_[0], color_[1], color_[2]); - c.SetReflection(ReflectionType::kSoft); + c.SetReflection(base::ReflectionType::kSoft); c.SetReflectionScale(0.05f, 0.05f, 0.05f); c.SetDoubleSided(true); - c.SetTexture(color_texture_); + c.SetTexture(color_texture_->texture_data()); c.DrawMesh(&mesh_); c.Submit(); } float s_scale, s_density; - SimpleComponent c(frame_def->light_shadow_pass()); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kShadow)); + base::SimpleComponent c(frame_def->light_shadow_pass()); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kShadow)); c.SetTransparent(true); - FullShadowSet* full_shadows = full_shadow_set_.get(); + FullShadowSet* full_shadows = full_shadow_set_.Get(); if (full_shadows) { // Pole bottom. { full_shadows->shadow_pole_bottom_.GetValues(&s_scale, &s_density); const Vector3f& p(full_shadows->shadow_pole_bottom_.GetPosition()); - g_graphics->DrawBlotch(p, 0.4f * s_scale, 0, 0, 0, s_density * 0.25f); + g_base->graphics->DrawBlotch(p, 0.4f * s_scale, 0, 0, 0, + s_density * 0.25f); } // Pole middle. { full_shadows->shadow_pole_middle_.GetValues(&s_scale, &s_density); const Vector3f& p(full_shadows->shadow_pole_middle_.GetPosition()); - g_graphics->DrawBlotch(p, 0.4f * s_scale, 0, 0, 0, s_density * 0.25f); + g_base->graphics->DrawBlotch(p, 0.4f * s_scale, 0, 0, 0, + s_density * 0.25f); } // Pole top. { full_shadows->shadow_pole_middle_.GetValues(&s_scale, &s_density); const Vector3f& p(full_shadows->shadow_pole_top_.GetPosition()); - g_graphics->DrawBlotch(p, 0.4f * s_scale, 0, 0, 0, s_density * 0.25f); + g_base->graphics->DrawBlotch(p, 0.4f * s_scale, 0, 0, 0, + s_density * 0.25f); } // Flag center. { full_shadows->shadow_flag_.GetValues(&s_scale, &s_density); const Vector3f& p(full_shadows->shadow_flag_.GetPosition()); - g_graphics->DrawBlotch(p, 0.8f * s_scale, 0, 0, 0, s_density * 0.3f); + g_base->graphics->DrawBlotch(p, 0.8f * s_scale, 0, 0, 0, + s_density * 0.3f); } } else { - SimpleShadowSet* simple_shadows = simple_shadow_set_.get(); + SimpleShadowSet* simple_shadows = simple_shadow_set_.Get(); assert(simple_shadows); simple_shadows->shadow_.GetValues(&s_scale, &s_density); const Vector3f& p(simple_shadows->shadow_.GetPosition()); - g_graphics->DrawBlotch(p, 0.8f * s_scale, 0, 0, 0, s_density * 0.5f); + g_base->graphics->DrawBlotch(p, 0.8f * s_scale, 0, 0, 0, + s_density * 0.5f); } c.Submit(); } // Flag pole. { - ObjectComponent c(frame_def->beauty_pass()); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kFlagPole)); - c.SetReflection(ReflectionType::kSharp); + base::ObjectComponent c(frame_def->beauty_pass()); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kFlagPole)); + c.SetReflection(base::ReflectionType::kSharp); c.SetReflectionScale(0.1f, 0.1f, 0.1f); c.PushTransform(); - c.TransformToBody(*body_); - c.DrawModel(g_assets->GetModel(SystemModelID::kFlagPole)); + body_->ApplyToRenderComponent(&c); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kFlagPole)); c.PopTransform(); c.Submit(); } - -#endif // !BA_HEADLESS_BUILD } void FlagNode::UpdateAreaOfInterest() { - AreaOfInterest* aoi = area_of_interest_; + base::AreaOfInterest* aoi = area_of_interest_; if (!aoi) return; - assert(body_.exists()); + assert(body_.Exists()); aoi->set_position(Vector3f(dGeomGetPosition(body_->geom()))); aoi->SetRadius(5.0f); } void FlagNode::Step() { // On happy thoughts, keep us on the 2d plane. - if (g_graphics->camera()->happy_thoughts_mode() && body_.exists()) { + if (g_base->graphics->camera()->happy_thoughts_mode() && body_.Exists()) { dBodyID b; const dReal *p, *v; b = body_->body(); @@ -384,7 +385,7 @@ void FlagNode::Step() { float smoothing = 0.98f; dBodySetPosition( b, p[0], p[1], - p[2] * smoothing + (1.0f - smoothing) * kHappyThoughtsZPlane); + p[2] * smoothing + (1.0f - smoothing) * base::kHappyThoughtsZPlane); v = dBodyGetLinearVel(b); dBodySetLinearVel(b, v[0], v[1], v[2] * smoothing); } @@ -399,25 +400,25 @@ void FlagNode::Step() { dBodyID b = body_->body(); assert(b); -#if !BA_HEADLESS_BUILD - dVector3 p; - FullShadowSet* full_shadows = full_shadow_set_.get(); - if (full_shadows) { - full_shadows->shadow_flag_.SetPosition( - flag_points_[kFlagSizeX * (kFlagSizeY / 2) + (kFlagSizeX / 2)]); - dBodyGetRelPointPos(b, 0, 0, kFlagHeight * -0.4f, p); - full_shadows->shadow_pole_bottom_.SetPosition(Vector3f(p)); - full_shadows->shadow_pole_middle_.SetPosition( - Vector3f(dBodyGetPosition(b))); - dBodyGetRelPointPos(b, 0, 0, kFlagHeight * 0.4f, p); - full_shadows->shadow_pole_top_.SetPosition(Vector3f(p)); - } else { - SimpleShadowSet* simple_shadows = simple_shadow_set_.get(); - assert(simple_shadows); - dBodyGetRelPointPos(b, 0, 0, kFlagHeight * -0.3f, p); - simple_shadows->shadow_.SetPosition(Vector3f(p)); + if (g_core->HeadlessMode()) { + dVector3 p; + FullShadowSet* full_shadows = full_shadow_set_.Get(); + if (full_shadows) { + full_shadows->shadow_flag_.SetPosition( + flag_points_[kFlagSizeX * (kFlagSizeY / 2) + (kFlagSizeX / 2)]); + dBodyGetRelPointPos(b, 0, 0, kFlagHeight * -0.4f, p); + full_shadows->shadow_pole_bottom_.SetPosition(Vector3f(p)); + full_shadows->shadow_pole_middle_.SetPosition( + Vector3f(dBodyGetPosition(b))); + dBodyGetRelPointPos(b, 0, 0, kFlagHeight * 0.4f, p); + full_shadows->shadow_pole_top_.SetPosition(Vector3f(p)); + } else { + SimpleShadowSet* simple_shadows = simple_shadow_set_.Get(); + assert(simple_shadows); + dBodyGetRelPointPos(b, 0, 0, kFlagHeight * -0.3f, p); + simple_shadows->shadow_.SetPosition(Vector3f(p)); + } } -#endif // !BA_HEADLESS_BUILD if (dBodyIsEnabled(body_->body())) { // Try to keep upright by pushing the top of the @@ -497,7 +498,7 @@ void FlagNode::Step() { UpdateFlagMesh(); } -auto FlagNode::GetRigidBody(int id) -> RigidBody* { return body_.get(); } +auto FlagNode::GetRigidBody(int id) -> RigidBody* { return body_.Get(); } static auto FlagPointIndex(int x, int y) -> int { return kFlagSizeX * (y) + (x); @@ -672,20 +673,20 @@ void FlagNode::GetRigidBodyPickupLocations(int id, float* obj, float* character, hand1[2] = -0.05f; } -void FlagNode::OnGraphicsQualityChanged(GraphicsQuality q) { +void FlagNode::OnGraphicsQualityChanged(base::GraphicsQuality q) { UpdateForGraphicsQuality(q); } -void FlagNode::UpdateForGraphicsQuality(GraphicsQuality quality) { -#if !BA_HEADLESS_BUILD - if (quality >= GraphicsQuality::kMedium) { - full_shadow_set_ = Object::New(); - simple_shadow_set_.Clear(); - } else { - simple_shadow_set_ = Object::New(); - full_shadow_set_.Clear(); +void FlagNode::UpdateForGraphicsQuality(base::GraphicsQuality quality) { + if (!g_core->HeadlessMode()) { + if (quality >= base::GraphicsQuality::kMedium) { + full_shadow_set_ = Object::New(); + simple_shadow_set_.Clear(); + } else { + simple_shadow_set_ = Object::New(); + full_shadow_set_.Clear(); + } } -#endif } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/flag_node.h b/src/ballistica/scene_v1/node/flag_node.h similarity index 69% rename from src/ballistica/scene/node/flag_node.h rename to src/ballistica/scene_v1/node/flag_node.h index 19c19e8c..ad2caff5 100644 --- a/src/ballistica/scene/node/flag_node.h +++ b/src/ballistica/scene_v1/node/flag_node.h @@ -1,15 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_FLAG_NODE_H_ -#define BALLISTICA_SCENE_NODE_FLAG_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_FLAG_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_FLAG_NODE_H_ #include -#include "ballistica/dynamics/part.h" -#include "ballistica/graphics/renderer.h" -#include "ballistica/scene/node/node.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/scene_v1/dynamics/part.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { class FlagNode : public Node { public: @@ -17,7 +17,7 @@ class FlagNode : public Node { explicit FlagNode(Scene* scene); ~FlagNode() override; void HandleMessage(const char* data) override; - void Draw(FrameDef* frame_def) override; + void Draw(base::FrameDef* frame_def) override; void Step() override; auto GetRigidBody(int id) -> RigidBody* override; auto is_area_of_interest() const -> bool { @@ -26,8 +26,8 @@ class FlagNode : public Node { void SetIsAreaOfInterest(bool val); auto getPosition() const -> std::vector; void SetPosition(const std::vector& vals); - auto color_texture() const -> Texture* { return color_texture_.get(); } - void set_color_texture(Texture* val) { color_texture_ = val; } + auto color_texture() const -> SceneTexture* { return color_texture_.Get(); } + void set_color_texture(SceneTexture* val) { color_texture_ = val; } auto light_weight() const -> bool { return light_weight_; } void SetLightWeight(bool val); auto color() const -> std::vector { return color_; } @@ -44,19 +44,17 @@ class FlagNode : public Node { void UpdateDimensions(); void ResetFlagMesh(); void UpdateFlagMesh(); - void OnGraphicsQualityChanged(GraphicsQuality q) override; - void UpdateForGraphicsQuality(GraphicsQuality q); + void OnGraphicsQualityChanged(base::GraphicsQuality q) override; + void UpdateForGraphicsQuality(base::GraphicsQuality q); void UpdateSpringPoint(int p1, int p2, float rest_length); - AreaOfInterest* area_of_interest_ = nullptr; + base::AreaOfInterest* area_of_interest_ = nullptr; Part part_; std::vector color_ = {1.0f, 1.0f, 1.0f}; Object::Ref body_{nullptr}; - Object::Ref color_texture_; - MeshIndexedObjectSplit mesh_; -#if !BA_HEADLESS_BUILD + Object::Ref color_texture_; + base::MeshIndexedObjectSplit mesh_; Object::Ref full_shadow_set_; Object::Ref simple_shadow_set_; -#endif // !BA_HEADLESS_BUILD int wind_rand_{}; float wind_rand_x_{}; float wind_rand_y_{}; @@ -72,6 +70,6 @@ class FlagNode : public Node { Vector3f flag_velocities_[25]{}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_FLAG_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_FLAG_NODE_H_ diff --git a/src/ballistica/scene/node/flash_node.cc b/src/ballistica/scene_v1/node/flash_node.cc similarity index 68% rename from src/ballistica/scene/node/flash_node.cc rename to src/ballistica/scene_v1/node/flash_node.cc index dd1bf589..238aec63 100644 --- a/src/ballistica/scene/node/flash_node.cc +++ b/src/ballistica/scene_v1/node/flash_node.cc @@ -1,12 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/flash_node.h" +#include "ballistica/scene_v1/node/flash_node.h" -#include "ballistica/graphics/component/object_component.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" +#include "ballistica/base/graphics/component/object_component.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/shared/math/random.h" -namespace ballistica { +namespace ballistica::scene_v1 { class FlashNodeType : public NodeType { public: @@ -42,17 +43,17 @@ void FlashNode::SetPosition(const std::vector& vals) { position_ = vals; } -void FlashNode::Draw(FrameDef* frame_def) { - ObjectComponent c(frame_def->beauty_pass()); - c.SetLightShadow(LightShadowType::kNone); +void FlashNode::Draw(base::FrameDef* frame_def) { + base::ObjectComponent c(frame_def->beauty_pass()); + c.SetLightShadow(base::LightShadowType::kNone); c.SetColor(color_[0], color_[1], color_[2], 1.0f); c.PushTransform(); c.Translate(position_[0], position_[1], position_[2]); c.Scale(size_, size_, size_); c.Rotate(RandomFloat() * 360.0f, 1, 1, 0); - c.DrawModel(g_assets->GetModel(SystemModelID::kFlash)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kFlash)); c.PopTransform(); c.Submit(); } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/flash_node.h b/src/ballistica/scene_v1/node/flash_node.h similarity index 69% rename from src/ballistica/scene/node/flash_node.h rename to src/ballistica/scene_v1/node/flash_node.h index b86280ac..94ce8d0d 100644 --- a/src/ballistica/scene/node/flash_node.h +++ b/src/ballistica/scene_v1/node/flash_node.h @@ -1,20 +1,20 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_FLASH_NODE_H_ -#define BALLISTICA_SCENE_NODE_FLASH_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_FLASH_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_FLASH_NODE_H_ #include -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { class FlashNode : public Node { public: static auto InitType() -> NodeType*; explicit FlashNode(Scene* scene); ~FlashNode() override; - void Draw(FrameDef* frame_def) override; + void Draw(base::FrameDef* frame_def) override; auto position() const -> std::vector { return position_; } void SetPosition(const std::vector& vals); auto size() const -> float { return size_; } @@ -28,6 +28,6 @@ class FlashNode : public Node { std::vector color_ = {0.5f, 0.5f, 0.5f}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_FLASH_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_FLASH_NODE_H_ diff --git a/src/ballistica/scene/node/globals_node.cc b/src/ballistica/scene_v1/node/globals_node.cc similarity index 70% rename from src/ballistica/scene/node/globals_node.cc rename to src/ballistica/scene_v1/node/globals_node.cc index be43714b..da53803a 100644 --- a/src/ballistica/scene/node/globals_node.cc +++ b/src/ballistica/scene_v1/node/globals_node.cc @@ -1,29 +1,32 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/globals_node.h" +#include "ballistica/scene_v1/node/globals_node.h" -#include "ballistica/audio/audio.h" -#include "ballistica/dynamics/bg/bg_dynamics.h" -#include "ballistica/graphics/camera.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/logic/host_activity.h" -#include "ballistica/python/python.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/dynamics/bg/bg_dynamics.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/graphics/support/camera.h" +#include "ballistica/classic/python/classic_python.h" +#include "ballistica/core/core.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/scene_v1/support/host_activity.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/shared/python/python.h" // FIXME: should not need this here. #if BA_VR_BUILD -#include "ballistica/graphics/vr_graphics.h" +#include "ballistica/base/graphics/graphics_vr.h" #endif -namespace ballistica { +namespace ballistica::scene_v1 { class GlobalsNodeType : public NodeType { public: #define BA_NODE_TYPE_CLASS GlobalsNode BA_NODE_CREATE_CALL(CreateGlobals); - BA_INT64_ATTR_READONLY(real_time, GetRealTime); + BA_INT64_ATTR_READONLY(real_time, GetAppTimeMillisecs); BA_INT64_ATTR_READONLY(time, GetTime); BA_INT64_ATTR_READONLY(step, GetStep); BA_FLOAT_ATTR(debris_friction, debris_friction, SetDebrisFriction); @@ -100,11 +103,13 @@ GlobalsNode::GlobalsNode(Scene* scene) : Node(scene, node_type) { // Set ourself as the current globals node for our scene. this->scene()->set_globals_node(this); + auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + // If we're being made in a host-activity, also set ourself as the current // globals node for our activity. (there should only be one, so complain if // there already is one). // FIXME: Need to update this for non-host activities at some point. - if (HostActivity* ha = context().GetHostActivity()) { + if (HostActivity* ha = context_ref().GetHostActivity()) { if (ha->globals_node()) { Log(LogLevel::kWarning, "More than one globals node created in HostActivity; this " @@ -122,7 +127,7 @@ GlobalsNode::GlobalsNode(Scene* scene) : Node(scene, node_type) { // If our scene is currently the game's foreground one, go ahead and // push our values globally. - if (g_logic->GetForegroundScene() == this->scene()) { + if (appmode->GetForegroundScene() == this->scene()) { SetAsForeground(); } } @@ -138,28 +143,58 @@ GlobalsNode::~GlobalsNode() { // values to the global state (since there can be multiple scenes in // existence, there has to be a single "foreground" globals node in control). void GlobalsNode::SetAsForeground() { - if (g_bg_dynamics != nullptr) { - g_bg_dynamics->SetDebrisFriction(debris_friction_); - g_bg_dynamics->SetDebrisKillHeight(debris_kill_height_); + if (g_base && g_base->bg_dynamics != nullptr) { + g_base->bg_dynamics->SetDebrisFriction(debris_friction_); + g_base->bg_dynamics->SetDebrisKillHeight(debris_kill_height_); } - g_graphics->ApplyGlobals(this); + auto* cam = g_base->graphics->camera(); - g_audio->SetSoundPitch(slow_motion_ ? 0.4f : 1.0f); + g_base->graphics->set_floor_reflection(floor_reflection()); + cam->SetMode(camera_mode()); + cam->set_vr_offset(Vector3f(vr_camera_offset())); + cam->set_happy_thoughts_mode(happy_thoughts_mode()); + g_base->graphics->set_shadow_scale(shadow_scale()[0], shadow_scale()[1]); + cam->set_area_of_interest_bounds( + area_of_interest_bounds_[0], area_of_interest_bounds_[1], + area_of_interest_bounds_[2], area_of_interest_bounds_[3], + area_of_interest_bounds_[4], area_of_interest_bounds_[5]); + g_base->graphics->SetShadowRange(shadow_range_[0], shadow_range_[1], + shadow_range_[2], shadow_range_[3]); + g_base->graphics->set_shadow_offset(Vector3f(shadow_offset())); + g_base->graphics->set_shadow_ortho(shadow_ortho()); + g_base->graphics->set_tint(Vector3f(tint())); + + g_base->graphics->set_ambient_color(Vector3f(ambient_color())); + g_base->graphics->set_vignette_outer(Vector3f(vignette_outer())); + g_base->graphics->set_vignette_inner(Vector3f(vignette_inner())); + +#if BA_VR_BUILD + if (g_core->IsVRMode()) { + auto* graphics_vr = base::GraphicsVR::get(); + graphics_vr->set_vr_near_clip(vr_near_clip()); + graphics_vr->set_vr_overlay_center(Vector3f(vr_overlay_center())); + graphics_vr->set_vr_overlay_center_enabled(vr_overlay_center_enabled()); + } +#endif + + g_base->audio->SetSoundPitch(slow_motion_ ? 0.4f : 1.0f); // Tell the scripting layer to play our current music. - g_python->PlayMusic(music_, music_continuous_); + g_classic->python->PlayMusic(music_, music_continuous_); } auto GlobalsNode::IsCurrentGlobals() const -> bool { // We're current if our scene is the foreground one and we're the globals // node for our scene. + auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + Scene* scene = this->scene(); assert(scene); - return (g_logic->GetForegroundScene() == this->scene() + return (appmode->GetForegroundScene() == this->scene() && scene->globals_node() == this); } -auto GlobalsNode::GetRealTime() -> millisecs_t { +auto GlobalsNode::GetAppTimeMillisecs() -> millisecs_t { // Pull this from our scene so we return consistent values throughout a step. return scene()->last_step_real_time(); } @@ -170,8 +205,8 @@ auto GlobalsNode::GetStep() -> int64_t { return scene()->stepnum(); } void GlobalsNode::SetDebrisFriction(float val) { debris_friction_ = val; if (IsCurrentGlobals()) { - if (g_bg_dynamics != nullptr) { - g_bg_dynamics->SetDebrisFriction(debris_friction_); + if (g_base && g_base->bg_dynamics != nullptr) { + g_base->bg_dynamics->SetDebrisFriction(debris_friction_); } } } @@ -179,9 +214,9 @@ void GlobalsNode::SetDebrisFriction(float val) { void GlobalsNode::SetVRNearClip(float val) { vr_near_clip_ = val; #if BA_VR_BUILD - if (IsVRMode()) { + if (g_core->IsVRMode()) { if (IsCurrentGlobals()) { - VRGraphics::get()->set_vr_near_clip(vr_near_clip_); + base::GraphicsVR::get()->set_vr_near_clip(vr_near_clip_); } } #endif @@ -190,15 +225,15 @@ void GlobalsNode::SetVRNearClip(float val) { void GlobalsNode::SetFloorReflection(bool val) { floor_reflection_ = val; if (IsCurrentGlobals()) { - g_graphics->set_floor_reflection(floor_reflection_); + g_base->graphics->set_floor_reflection(floor_reflection_); } } void GlobalsNode::SetDebrisKillHeight(float val) { debris_kill_height_ = val; if (IsCurrentGlobals()) { - if (g_bg_dynamics != nullptr) { - g_bg_dynamics->SetDebrisKillHeight(debris_kill_height_); + if (g_base && g_base->bg_dynamics != nullptr) { + g_base->bg_dynamics->SetDebrisKillHeight(debris_kill_height_); } } } @@ -206,7 +241,7 @@ void GlobalsNode::SetDebrisKillHeight(float val) { void GlobalsNode::SetHappyThoughtsMode(bool val) { happy_thoughts_mode_ = val; if (IsCurrentGlobals()) { - g_graphics->camera()->set_happy_thoughts_mode(happy_thoughts_mode_); + g_base->graphics->camera()->set_happy_thoughts_mode(happy_thoughts_mode_); } } @@ -217,7 +252,7 @@ void GlobalsNode::SetShadowScale(const std::vector& vals) { } shadow_scale_ = vals; if (IsCurrentGlobals()) { - g_graphics->set_shadow_scale(shadow_scale_[0], shadow_scale_[1]); + g_base->graphics->set_shadow_scale(shadow_scale_[0], shadow_scale_[1]); } } @@ -229,9 +264,9 @@ void GlobalsNode::set_area_of_interest_bounds(const std::vector& vals) { } area_of_interest_bounds_ = vals; - assert(g_graphics->camera()); + assert(g_base->graphics->camera()); if (IsCurrentGlobals()) { - g_graphics->camera()->set_area_of_interest_bounds( + g_base->graphics->camera()->set_area_of_interest_bounds( area_of_interest_bounds_[0], area_of_interest_bounds_[1], area_of_interest_bounds_[2], area_of_interest_bounds_[3], area_of_interest_bounds_[4], area_of_interest_bounds_[5]); @@ -245,8 +280,8 @@ void GlobalsNode::SetShadowRange(const std::vector& vals) { } shadow_range_ = vals; if (IsCurrentGlobals()) { - g_graphics->SetShadowRange(shadow_range_[0], shadow_range_[1], - shadow_range_[2], shadow_range_[3]); + g_base->graphics->SetShadowRange(shadow_range_[0], shadow_range_[1], + shadow_range_[2], shadow_range_[3]); } } @@ -257,7 +292,7 @@ void GlobalsNode::SetShadowOffset(const std::vector& vals) { } shadow_offset_ = vals; if (IsCurrentGlobals()) { - g_graphics->set_shadow_offset(Vector3f(shadow_offset_)); + g_base->graphics->set_shadow_offset(Vector3f(shadow_offset_)); } } @@ -268,14 +303,14 @@ void GlobalsNode::SetVRCameraOffset(const std::vector& vals) { } vr_camera_offset_ = vals; if (IsCurrentGlobals()) { - g_graphics->camera()->set_vr_offset(Vector3f(vr_camera_offset_)); + g_base->graphics->camera()->set_vr_offset(Vector3f(vr_camera_offset_)); } } void GlobalsNode::SetShadowOrtho(bool val) { shadow_ortho_ = val; if (IsCurrentGlobals()) { - g_graphics->set_shadow_ortho(shadow_ortho_); + g_base->graphics->set_shadow_ortho(shadow_ortho_); } } @@ -286,7 +321,7 @@ void GlobalsNode::SetTint(const std::vector& vals) { } tint_ = vals; if (IsCurrentGlobals()) { - g_graphics->set_tint(Vector3f(tint_[0], tint_[1], tint_[2])); + g_base->graphics->set_tint(Vector3f(tint_[0], tint_[1], tint_[2])); } } @@ -298,7 +333,8 @@ void GlobalsNode::SetVROverlayCenter(const std::vector& vals) { vr_overlay_center_ = vals; #if BA_VR_BUILD if (IsCurrentGlobals()) { - VRGraphics::get()->set_vr_overlay_center(Vector3f(vr_overlay_center_)); + base::GraphicsVR::get()->set_vr_overlay_center( + Vector3f(vr_overlay_center_)); } #endif } @@ -307,7 +343,7 @@ void GlobalsNode::SetVROverlayCenterEnabled(bool val) { vr_overlay_center_enabled_ = val; #if BA_VR_BUILD if (IsCurrentGlobals()) { - VRGraphics::get()->set_vr_overlay_center_enabled( + base::GraphicsVR::get()->set_vr_overlay_center_enabled( vr_overlay_center_enabled_); } #endif @@ -320,7 +356,7 @@ void GlobalsNode::SetAmbientColor(const std::vector& vals) { } ambient_color_ = vals; if (IsCurrentGlobals()) { - g_graphics->set_ambient_color(Vector3f(ambient_color_)); + g_base->graphics->set_ambient_color(Vector3f(ambient_color_)); } } @@ -331,7 +367,7 @@ void GlobalsNode::SetVignetteOuter(const std::vector& vals) { } vignette_outer_ = vals; if (IsCurrentGlobals()) { - g_graphics->set_vignette_outer(Vector3f(vignette_outer_)); + g_base->graphics->set_vignette_outer(Vector3f(vignette_outer_)); } } @@ -342,15 +378,15 @@ void GlobalsNode::SetVignetteInner(const std::vector& vals) { } vignette_inner_ = vals; if (IsCurrentGlobals()) { - g_graphics->set_vignette_inner(Vector3f(vignette_inner_)); + g_base->graphics->set_vignette_inner(Vector3f(vignette_inner_)); } } auto GlobalsNode::GetCameraMode() const -> std::string { switch (camera_mode_) { - case CameraMode::kOrbit: + case base::CameraMode::kOrbit: return "rotate"; - case CameraMode::kFollow: + case base::CameraMode::kFollow: return "follow"; } @@ -364,21 +400,21 @@ auto GlobalsNode::GetCameraMode() const -> std::string { void GlobalsNode::SetCameraMode(const std::string& val) { if (val == "rotate") { - camera_mode_ = CameraMode::kOrbit; + camera_mode_ = base::CameraMode::kOrbit; } else if (val == "follow") { - camera_mode_ = CameraMode::kFollow; + camera_mode_ = base::CameraMode::kFollow; } else { throw Exception("Invalid camera mode: '" + val + R"('; expected "rotate" or "follow")"); } - if (IsCurrentGlobals()) g_graphics->camera()->SetMode(camera_mode_); + if (IsCurrentGlobals()) g_base->graphics->camera()->SetMode(camera_mode_); } void GlobalsNode::SetAllowKickIdlePlayers(bool val) { allow_kick_idle_players_ = val; // This only means something if we're in a host-activity. - if (HostActivity* ha = context().GetHostActivity()) { + if (HostActivity* ha = context_ref().GetHostActivity()) { // Set speed on our activity even if we're not the current globals node. if (ha->globals_node() == this) { ha->set_allow_kick_idle_players(allow_kick_idle_players_); @@ -391,7 +427,7 @@ void GlobalsNode::SetSlowMotion(bool val) { // This only matters if we're in a host-activity. // (clients are just driven by whatever steps are in the input-stream) - if (HostActivity* ha = context().GetHostActivity()) { + if (HostActivity* ha = context_ref().GetHostActivity()) { // Set speed on *our* activity regardless of whether we're the current // globals node. if (ha->globals_node() == this) { @@ -402,7 +438,7 @@ void GlobalsNode::SetSlowMotion(bool val) { // Only set pitch if we are the current globals node. // (FIXME - need to make this per-sound or something) if (IsCurrentGlobals()) { - g_audio->SetSoundPitch(slow_motion_ ? 0.4f : 1.0f); + g_base->audio->SetSoundPitch(slow_motion_ ? 0.4f : 1.0f); } } @@ -411,7 +447,7 @@ void GlobalsNode::SetPaused(bool val) { // This only matters in a host-activity. // (clients are just driven by whatever steps are in the input-stream) - if (HostActivity* ha = context().GetHostActivity()) { + if (HostActivity* ha = context_ref().GetHostActivity()) { // Set speed on our activity even if we're not the current globals node. if (ha->globals_node() == this) { ha->SetPaused(paused_); @@ -428,9 +464,9 @@ void GlobalsNode::SetUseFixedVROverlay(bool val) { void GlobalsNode::SetMusicCount(int val) { if (music_count_ != val && IsCurrentGlobals()) { - g_python->PlayMusic(music_, music_continuous_); + g_classic->python->PlayMusic(music_, music_continuous_); } music_count_ = val; } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/globals_node.h b/src/ballistica/scene_v1/node/globals_node.h similarity index 92% rename from src/ballistica/scene/node/globals_node.h rename to src/ballistica/scene_v1/node/globals_node.h index 686871a4..b1d487e8 100644 --- a/src/ballistica/scene/node/globals_node.h +++ b/src/ballistica/scene_v1/node/globals_node.h @@ -1,14 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_GLOBALS_NODE_H_ -#define BALLISTICA_SCENE_NODE_GLOBALS_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_GLOBALS_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_GLOBALS_NODE_H_ #include #include -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { class GlobalsNode : public Node { public: @@ -17,7 +17,7 @@ class GlobalsNode : public Node { ~GlobalsNode() override; void SetAsForeground(); auto IsCurrentGlobals() const -> bool; - auto GetRealTime() -> millisecs_t; + auto GetAppTimeMillisecs() -> millisecs_t; auto GetTime() -> millisecs_t; auto GetStep() -> int64_t; auto debris_friction() const -> float { return debris_friction_; } @@ -99,7 +99,7 @@ class GlobalsNode : public Node { auto camera_mode() const { return camera_mode_; } private: - CameraMode camera_mode_{CameraMode::kFollow}; + base::CameraMode camera_mode_{base::CameraMode::kFollow}; float vr_near_clip_{4.0f}; float debris_friction_{1.0f}; bool floor_reflection_{}; @@ -127,6 +127,6 @@ class GlobalsNode : public Node { bool paused_{}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_GLOBALS_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_GLOBALS_NODE_H_ diff --git a/src/ballistica/scene/node/image_node.cc b/src/ballistica/scene_v1/node/image_node.cc similarity index 78% rename from src/ballistica/scene/node/image_node.cc rename to src/ballistica/scene_v1/node/image_node.cc index fd312a88..fd7734fa 100644 --- a/src/ballistica/scene/node/image_node.cc +++ b/src/ballistica/scene_v1/node/image_node.cc @@ -1,13 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/image_node.h" +#include "ballistica/scene_v1/node/image_node.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/core/core.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/scene_v1/support/scene.h" -namespace ballistica { +namespace ballistica::scene_v1 { class ImageNodeType : public NodeType { public: @@ -29,8 +30,8 @@ class ImageNodeType : public NodeType { BA_TEXTURE_ATTR(texture, texture, set_texture); BA_TEXTURE_ATTR(tint_texture, tint_texture, set_tint_texture); BA_TEXTURE_ATTR(mask_texture, mask_texture, set_mask_texture); - BA_MODEL_ATTR(model_opaque, model_opaque, set_model_opaque); - BA_MODEL_ATTR(model_transparent, model_transparent, set_model_transparent); + BA_MESH_ATTR(mesh_opaque, mesh_opaque, set_mesh_opaque); + BA_MESH_ATTR(mesh_transparent, mesh_transparent, set_mesh_transparent); BA_FLOAT_ATTR(vr_depth, vr_depth, set_vr_depth); BA_BOOL_ATTR(host_only, host_only, set_host_only); BA_BOOL_ATTR(front, front, set_front); @@ -54,8 +55,8 @@ class ImageNodeType : public NodeType { texture(this), tint_texture(this), mask_texture(this), - model_opaque(this), - model_transparent(this), + mesh_opaque(this), + mesh_transparent(this), vr_depth(this), host_only(this), front(this) {} @@ -193,16 +194,22 @@ void ImageNode::SetFillScreen(bool val) { // Help the scene keep track of stuff that covers the whole background // (so it knows it doesnt have to clear). - if (!old && fill_screen_) scene()->increment_bg_cover_count(); - if (old && !fill_screen_) scene()->decrement_bg_cover_count(); + if (!old && fill_screen_) { + scene()->increment_bg_cover_count(); + } + if (old && !fill_screen_) { + scene()->decrement_bg_cover_count(); + } // We keep track of how many full-screen images are present at any given time. // vr-mode uses this to lock down the overlay layer's position in that case. } -void ImageNode::Draw(FrameDef* frame_def) { - if (host_only_ && !context().GetHostSession()) return; - bool vr = (IsVRMode()); +void ImageNode::Draw(base::FrameDef* frame_def) { + if (host_only_ && !context_ref().GetHostSession()) { + return; + } + bool vr = (g_core->IsVRMode()); // In vr mode we use the fixed overlay position if our scene // is set for that. @@ -213,9 +220,9 @@ void ImageNode::Draw(FrameDef* frame_def) { vr_use_fixed = false; } - RenderPass& pass(*(vr_use_fixed ? frame_def->GetOverlayFixedPass() - : front_ ? frame_def->overlay_front_pass() - : frame_def->overlay_pass())); + base::RenderPass& pass(*(vr_use_fixed ? frame_def->GetOverlayFixedPass() + : front_ ? frame_def->overlay_front_pass() + : frame_def->overlay_pass())); // If the pass we're drawing into changes dimensions, recalc. // Otherwise we break if a window is resized. @@ -299,7 +306,7 @@ void ImageNode::Draw(FrameDef* frame_def) { // Tilt-translate doesn't happen in vr mode. if (tilt_translate_ != 0.0f && !vr) { - Vector3f tilt = g_graphics->tilt(); + Vector3f tilt = g_base->graphics->tilt(); fin_center_x -= tilt.y * tilt_translate_; fin_center_y += tilt.x * tilt_translate_; @@ -319,38 +326,38 @@ void ImageNode::Draw(FrameDef* frame_def) { if (alpha < 0) { alpha = 0; } - ModelData* model_opaque_used = nullptr; - if (model_opaque_.exists()) model_opaque_used = model_opaque_->model_data(); - ModelData* model_transparent_used = nullptr; - if (model_transparent_.exists()) { - model_transparent_used = model_transparent_->model_data(); + base::MeshAsset* mesh_opaque_used = nullptr; + if (mesh_opaque_.Exists()) mesh_opaque_used = mesh_opaque_->mesh_data(); + base::MeshAsset* mesh_transparent_used = nullptr; + if (mesh_transparent_.Exists()) { + mesh_transparent_used = mesh_transparent_->mesh_data(); } - // If no meshes were provided, use default image models. - if (!model_opaque_.exists() && !model_transparent_.exists()) { + // If no meshes were provided, use default image meshes. + if (!mesh_opaque_.Exists() && !mesh_transparent_.Exists()) { if (vr && fill_screen_) { #if BA_VR_BUILD - model_opaque_used = - g_assets->GetModel(SystemModelID::kImage1x1VRFullScreen); + mesh_opaque_used = + g_base->assets->SysMesh(base::SysMeshID::kImage1x1VRFullScreen); #else throw Exception(); #endif // BA_VR_BUILD } else { - SystemModelID m = fill_screen_ ? SystemModelID::kImage1x1FullScreen - : SystemModelID::kImage1x1; + base::SysMeshID m = fill_screen_ ? base::SysMeshID::kImage1x1FullScreen + : base::SysMeshID::kImage1x1; if (has_alpha_channel) { - model_transparent_used = g_assets->GetModel(m); + mesh_transparent_used = g_base->assets->SysMesh(m); } else { - model_opaque_used = g_assets->GetModel(m); + mesh_opaque_used = g_base->assets->SysMesh(m); } } } // Draw opaque portion either opaque or transparent depending on our // global opacity. - if (model_opaque_used) { + if (mesh_opaque_used) { // Draw in opaque pass if possible. - SimpleComponent c(&pass); + base::SimpleComponent c(&pass); bool draw_transparent = (alpha < 0.999f); // Stuff in the fixed vr overlay pass may inadvertently @@ -358,45 +365,47 @@ void ImageNode::Draw(FrameDef* frame_def) { // transparent to avoid that. c.SetTransparent(draw_transparent); c.SetPremultiplied(premultiplied_); - c.SetTexture(texture_); + c.SetTexture(texture_.Exists() ? texture_->texture_data() : nullptr); c.SetColor(red_, green_, blue_, alpha); - if (tint_texture_.exists()) { - c.SetColorizeTexture(tint_texture_); + if (tint_texture_.Exists()) { + c.SetColorizeTexture(tint_texture_->texture_data()); c.SetColorizeColor(tint_red_, tint_green_, tint_blue_); c.SetColorizeColor2(tint2_red_, tint2_green_, tint2_blue_); } - c.SetMaskTexture(mask_texture_); + c.SetMaskTexture(mask_texture_.Exists() ? mask_texture_->texture_data() + : nullptr); c.PushTransform(); c.Translate(fin_center_x, fin_center_y, - vr ? vr_depth_ : g_graphics->overlay_node_z_depth()); + vr ? vr_depth_ : g_base->graphics->overlay_node_z_depth()); if (rotate_ != 0.0f) c.Rotate(rotate_, 0, 0, 1); c.Scale(fin_width, fin_height, fin_width); - c.DrawModel(model_opaque_used); + c.DrawMeshAsset(mesh_opaque_used); c.PopTransform(); c.Submit(); } // Transparent portion. - if (model_transparent_used) { - SimpleComponent c(&pass); + if (mesh_transparent_used) { + base::SimpleComponent c(&pass); c.SetTransparent(true); c.SetPremultiplied(premultiplied_); - c.SetTexture(texture_); + c.SetTexture(texture_.Exists() ? texture_->texture_data() : nullptr); c.SetColor(red_, green_, blue_, alpha); - if (tint_texture_.exists()) { - c.SetColorizeTexture(tint_texture_); + if (tint_texture_.Exists()) { + c.SetColorizeTexture(tint_texture_->texture_data()); c.SetColorizeColor(tint_red_, tint_green_, tint_blue_); c.SetColorizeColor2(tint2_red_, tint2_green_, tint2_blue_); } - c.SetMaskTexture(mask_texture_); + c.SetMaskTexture(mask_texture_.Exists() ? mask_texture_->texture_data() + : nullptr); c.PushTransform(); c.Translate(fin_center_x, fin_center_y, - vr ? vr_depth_ : g_graphics->overlay_node_z_depth()); + vr ? vr_depth_ : g_base->graphics->overlay_node_z_depth()); if (rotate_ != 0.0f) c.Rotate(rotate_, 0, 0, 1); c.Scale(fin_width, fin_height, fin_width); - c.DrawModel(model_transparent_used); + c.DrawMeshAsset(mesh_transparent_used); c.PopTransform(); c.Submit(); } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/image_node.h b/src/ballistica/scene_v1/node/image_node.h similarity index 71% rename from src/ballistica/scene/node/image_node.h rename to src/ballistica/scene_v1/node/image_node.h index 689d8280..f4c36437 100644 --- a/src/ballistica/scene/node/image_node.h +++ b/src/ballistica/scene_v1/node/image_node.h @@ -1,16 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_IMAGE_NODE_H_ -#define BALLISTICA_SCENE_NODE_IMAGE_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_IMAGE_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_IMAGE_NODE_H_ #include #include -#include "ballistica/assets/component/model.h" -#include "ballistica/assets/component/texture.h" -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/assets/scene_mesh.h" +#include "ballistica/scene_v1/assets/scene_texture.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Node used to draw 2d image overlays on-screen. class ImageNode : public Node { @@ -18,7 +18,7 @@ class ImageNode : public Node { static auto InitType() -> NodeType*; explicit ImageNode(Scene* scene); ~ImageNode() override; - void Draw(FrameDef* frame_def) override; + void Draw(base::FrameDef* frame_def) override; auto scale() const -> std::vector { return scale_; } void SetScale(const std::vector& scale); auto position() const -> std::vector { return position_; } @@ -48,17 +48,19 @@ class ImageNode : public Node { void set_premultiplied(bool val) { premultiplied_ = val; } auto GetAttach() const -> std::string; void SetAttach(const std::string& val); - auto texture() const -> Texture* { return texture_.get(); } - void set_texture(Texture* t) { texture_ = t; } - auto tint_texture() const -> Texture* { return tint_texture_.get(); } - void set_tint_texture(Texture* t) { tint_texture_ = t; } - auto mask_texture() const -> Texture* { return mask_texture_.get(); } - void set_mask_texture(Texture* t) { mask_texture_ = t; } - auto model_opaque() const -> Model* { return model_opaque_.get(); } - void set_model_opaque(Model* m) { model_opaque_ = m; } - auto model_transparent() const -> Model* { return model_transparent_.get(); } - void set_model_transparent(Model* m) { - model_transparent_ = m; + auto texture() const -> SceneTexture* { return texture_.Get(); } + void set_texture(SceneTexture* t) { texture_ = t; } + auto tint_texture() const -> SceneTexture* { return tint_texture_.Get(); } + void set_tint_texture(SceneTexture* t) { tint_texture_ = t; } + auto mask_texture() const -> SceneTexture* { return mask_texture_.Get(); } + void set_mask_texture(SceneTexture* t) { mask_texture_ = t; } + auto mesh_opaque() const -> SceneMesh* { return mesh_opaque_.Get(); } + void set_mesh_opaque(SceneMesh* m) { mesh_opaque_ = m; } + auto mesh_transparent() const -> SceneMesh* { + return mesh_transparent_.Get(); + } + void set_mesh_transparent(SceneMesh* m) { + mesh_transparent_ = m; dirty_ = true; } auto vr_depth() const -> float { return vr_depth_; } @@ -89,11 +91,11 @@ class ImageNode : public Node { std::vector color_{1.0f, 1.0f, 1.0f}; std::vector tint_color_{1.0f, 1.0f, 1.0f}; std::vector tint2_color_{1.0f, 1.0f, 1.0f}; - Object::Ref texture_; - Object::Ref tint_texture_; - Object::Ref mask_texture_; - Object::Ref model_opaque_; - Object::Ref model_transparent_; + Object::Ref texture_; + Object::Ref tint_texture_; + Object::Ref mask_texture_; + Object::Ref mesh_opaque_; + Object::Ref mesh_transparent_; bool fill_screen_{}; bool has_alpha_channel_{true}; bool dirty_{true}; @@ -119,6 +121,6 @@ class ImageNode : public Node { float tint2_blue_{1.0f}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_IMAGE_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_IMAGE_NODE_H_ diff --git a/src/ballistica/scene/node/light_node.cc b/src/ballistica/scene_v1/node/light_node.cc similarity index 75% rename from src/ballistica/scene/node/light_node.cc rename to src/ballistica/scene_v1/node/light_node.cc index 0baed19e..f9cc2603 100644 --- a/src/ballistica/scene/node/light_node.cc +++ b/src/ballistica/scene_v1/node/light_node.cc @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/light_node.h" +#include "ballistica/scene_v1/node/light_node.h" -#include "ballistica/dynamics/bg/bg_dynamics_volume_light.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_volume_light.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" -namespace ballistica { +namespace ballistica::scene_v1 { class LightNodeType : public NodeType { public: @@ -49,13 +49,13 @@ void LightNode::Step() { #if !BA_HEADLESS_BUILD // create or destroy our light-volume as needed // (minimize redundant create/destroy/sets this way) - if (lights_volumes_ && !volume_light_.exists()) { - volume_light_ = Object::New(); + if (lights_volumes_ && !volume_light_.Exists()) { + volume_light_ = Object::New(); float i = GetVolumeLightIntensity(); volume_light_->SetColor(color_[0] * i, color_[1] * i, color_[2] * i); volume_light_->SetPosition( Vector3f(position_[0], position_[1], position_[2])); - } else if (!lights_volumes_ && volume_light_.exists()) { + } else if (!lights_volumes_ && volume_light_.Exists()) { volume_light_.Clear(); } #endif // BA_HEADLESS_BUILD @@ -64,7 +64,7 @@ void LightNode::Step() { void LightNode::SetRadius(float val) { radius_ = std::max(0.0f, val); #if !BA_HEADLESS_BUILD - if (volume_light_.exists()) { + if (volume_light_.Exists()) { volume_light_->SetRadius(radius_); } #endif // BA_HEADLESS_BUILD @@ -76,7 +76,7 @@ void LightNode::SetColor(const std::vector& vals) { } color_ = vals; #if !BA_HEADLESS_BUILD - if (volume_light_.exists()) { + if (volume_light_.Exists()) { float i = GetVolumeLightIntensity(); volume_light_->SetColor(color_[0] * i, color_[1] * i, color_[2] * i); } @@ -91,7 +91,7 @@ void LightNode::SetPosition(const std::vector& vals) { #if !BA_HEADLESS_BUILD shadow_.SetPosition(Vector3f(position_[0], position_[1], position_[2])); - if (volume_light_.exists()) { + if (volume_light_.Exists()) { volume_light_->SetPosition( Vector3f(position_[0], position_[1], position_[2])); } @@ -101,7 +101,7 @@ void LightNode::SetPosition(const std::vector& vals) { void LightNode::SetIntensity(float val) { intensity_ = std::max(0.0f, val); #if !BA_HEADLESS_BUILD - if (volume_light_.exists()) { + if (volume_light_.Exists()) { float i = GetVolumeLightIntensity(); volume_light_->SetColor(color_[0] * i, color_[1] * i, color_[2] * i); } @@ -112,14 +112,14 @@ void LightNode::SetVolumeIntensityScale(float val) { volume_intensity_scale_ = std::max(0.0f, val); #if !BA_HEADLESS_BUILD - if (volume_light_.exists()) { + if (volume_light_.Exists()) { float i = GetVolumeLightIntensity(); volume_light_->SetColor(color_[0] * i, color_[1] * i, color_[2] * i); } #endif // BA_HEADLESS_BUILD } -void LightNode::Draw(FrameDef* frame_def) { +void LightNode::Draw(base::FrameDef* frame_def) { #if !BA_HEADLESS_BUILD // if we haven't gotten our initial attributes, dont draw assert(position_.size() == 3); @@ -137,15 +137,16 @@ void LightNode::Draw(FrameDef* frame_def) { float brightness = s_density * 0.65f * intensity_; // draw our light on both terrain and objects - g_graphics->DrawBlotchSoft(Vector3f(&position_[0]), 20.0f * radius_ * s_scale, - color_[0] * brightness, color_[1] * brightness, - color_[2] * brightness, 0.0f); + g_base->graphics->DrawBlotchSoft( + Vector3f(&position_[0]), 20.0f * radius_ * s_scale, + color_[0] * brightness, color_[1] * brightness, color_[2] * brightness, + 0.0f); - g_graphics->DrawBlotchSoftObj(Vector3f(&position_[0]), - 20.0f * radius_ * s_scale, - color_[0] * brightness, color_[1] * brightness, - color_[2] * brightness, 0.0f); + g_base->graphics->DrawBlotchSoftObj( + Vector3f(&position_[0]), 20.0f * radius_ * s_scale, + color_[0] * brightness, color_[1] * brightness, color_[2] * brightness, + 0.0f); #endif // BA_HEADLESS_BUILD } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/light_node.h b/src/ballistica/scene_v1/node/light_node.h similarity index 74% rename from src/ballistica/scene/node/light_node.h rename to src/ballistica/scene_v1/node/light_node.h index 93a5a592..30a00e2b 100644 --- a/src/ballistica/scene/node/light_node.h +++ b/src/ballistica/scene_v1/node/light_node.h @@ -1,21 +1,21 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_LIGHT_NODE_H_ -#define BALLISTICA_SCENE_NODE_LIGHT_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_LIGHT_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_LIGHT_NODE_H_ #include -#include "ballistica/dynamics/bg/bg_dynamics_shadow.h" -#include "ballistica/scene/node/node.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_shadow.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { // A light source class LightNode : public Node { public: static auto InitType() -> NodeType*; explicit LightNode(Scene* scene); - void Draw(FrameDef* frame_def) override; + void Draw(base::FrameDef* frame_def) override; void Step() override; auto position() const -> std::vector { return position_; } void SetPosition(const std::vector& val); @@ -37,8 +37,8 @@ class LightNode : public Node { private: auto GetVolumeLightIntensity() -> float; #if !BA_HEADLESS_BUILD - BGDynamicsShadow shadow_{0.2f}; - Object::Ref volume_light_; + base::BGDynamicsShadow shadow_{0.2f}; + Object::Ref volume_light_; #endif std::vector position_ = {0.0f, 0.0f, 0.0f}; std::vector color_ = {1.0f, 1.0f, 1.0f}; @@ -49,6 +49,6 @@ class LightNode : public Node { bool lights_volumes_ = true; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_LIGHT_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_LIGHT_NODE_H_ diff --git a/src/ballistica/scene/node/locator_node.cc b/src/ballistica/scene_v1/node/locator_node.cc similarity index 77% rename from src/ballistica/scene/node/locator_node.cc rename to src/ballistica/scene_v1/node/locator_node.cc index d54c3d54..1c544325 100644 --- a/src/ballistica/scene/node/locator_node.cc +++ b/src/ballistica/scene_v1/node/locator_node.cc @@ -1,12 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/locator_node.h" +#include "ballistica/scene_v1/node/locator_node.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" -namespace ballistica { +namespace ballistica::scene_v1 { class LocatorNodeType : public NodeType { public: @@ -105,27 +105,27 @@ void LocatorNode::SetSize(const std::vector& vals) { } } -void LocatorNode::Draw(FrameDef* frame_def) { - SystemModelID model; +void LocatorNode::Draw(base::FrameDef* frame_def) { + base::SysMeshID mesh; if (shape_ == Shape::kBox) { - model = SystemModelID::kLocatorBox; + mesh = base::SysMeshID::kLocatorBox; } else if (shape_ == Shape::kCircle) { - model = SystemModelID::kLocatorCircle; + mesh = base::SysMeshID::kLocatorCircle; } else if (shape_ == Shape::kCircleOutline) { - model = SystemModelID::kLocatorCircleOutline; + mesh = base::SysMeshID::kLocatorCircleOutline; } else { - model = SystemModelID::kLocator; + mesh = base::SysMeshID::kLocator; } - SystemTextureID texture; + base::SysTextureID texture; if (shape_ == Shape::kCircle) { - texture = - additive_ ? SystemTextureID::kCircleNoAlpha : SystemTextureID::kCircle; + texture = additive_ ? base::SysTextureID::kCircleNoAlpha + : base::SysTextureID::kCircle; } else if (shape_ == Shape::kCircleOutline) { - texture = additive_ ? SystemTextureID::kCircleOutlineNoAlpha - : SystemTextureID::kCircleOutline; + texture = additive_ ? base::SysTextureID::kCircleOutlineNoAlpha + : base::SysTextureID::kCircleOutline; } else { - texture = SystemTextureID::kRGBStripes; + texture = base::SysTextureID::kRGBStripes; } bool transparent = false; @@ -135,16 +135,16 @@ void LocatorNode::Draw(FrameDef* frame_def) { // beauty if (draw_beauty_) { - SimpleComponent c(frame_def->beauty_pass()); + base::SimpleComponent c(frame_def->beauty_pass()); if (transparent) { c.SetTransparent(true); } c.SetColor(color_[0], color_[1], color_[2], opacity_); - c.SetTexture(g_assets->GetTexture(texture)); + c.SetTexture(g_base->assets->SysTexture(texture)); c.PushTransform(); c.Translate(position_[0], position_[1], position_[2]); c.Scale(size_[0], size_[1], size_[2]); - c.DrawModel(g_assets->GetModel(model)); + c.DrawMeshAsset(g_base->assets->SysMesh(mesh)); c.PopTransform(); c.Submit(); } @@ -152,7 +152,7 @@ void LocatorNode::Draw(FrameDef* frame_def) { if (draw_shadow_) { // colored shadow for circle if (shape_ == Shape::kCircle || shape_ == Shape::kCircleOutline) { - SimpleComponent c(frame_def->light_shadow_pass()); + base::SimpleComponent c(frame_def->light_shadow_pass()); assert(transparent); c.SetTransparent(true); if (additive_) { @@ -164,26 +164,26 @@ void LocatorNode::Draw(FrameDef* frame_def) { } else { c.SetColor(color_[0], color_[1], color_[2], opacity_); } - c.SetTexture(g_assets->GetTexture(texture)); + c.SetTexture(g_base->assets->SysTexture(texture)); c.PushTransform(); c.Translate(position_[0], position_[1], position_[2]); c.Scale(size_[0], size_[1], size_[2]); - c.DrawModel(g_assets->GetModel(model)); + c.DrawMeshAsset(g_base->assets->SysMesh(mesh)); c.PopTransform(); c.Submit(); } else { // simple black shadow for locator/box - SimpleComponent c(frame_def->light_shadow_pass()); + base::SimpleComponent c(frame_def->light_shadow_pass()); c.SetTransparent(true); c.SetColor(0.4f, 0.4f, 0.4f, 0.7f); c.PushTransform(); c.Translate(position_[0], position_[1], position_[2]); c.Scale(size_[0], size_[1], size_[2]); - c.DrawModel(g_assets->GetModel(model)); + c.DrawMeshAsset(g_base->assets->SysMesh(mesh)); c.PopTransform(); c.Submit(); } } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/locator_node.h b/src/ballistica/scene_v1/node/locator_node.h similarity index 83% rename from src/ballistica/scene/node/locator_node.h rename to src/ballistica/scene_v1/node/locator_node.h index 0f8fcdde..4bf6ad8e 100644 --- a/src/ballistica/scene/node/locator_node.h +++ b/src/ballistica/scene_v1/node/locator_node.h @@ -1,21 +1,21 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_LOCATOR_NODE_H_ -#define BALLISTICA_SCENE_NODE_LOCATOR_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_LOCATOR_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_LOCATOR_NODE_H_ #include #include -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { class LocatorNode : public Node { public: static auto InitType() -> NodeType*; explicit LocatorNode(Scene* scene); - void Draw(FrameDef* frame_def) override; + void Draw(base::FrameDef* frame_def) override; auto position() const -> std::vector { return position_; } void SetPosition(const std::vector& vals); @@ -58,6 +58,6 @@ class LocatorNode : public Node { bool draw_shadow_ = true; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_LOCATOR_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_LOCATOR_NODE_H_ diff --git a/src/ballistica/scene/node/math_node.cc b/src/ballistica/scene_v1/node/math_node.cc similarity index 92% rename from src/ballistica/scene/node/math_node.cc rename to src/ballistica/scene_v1/node/math_node.cc index 2108b0b6..0aa59409 100644 --- a/src/ballistica/scene/node/math_node.cc +++ b/src/ballistica/scene_v1/node/math_node.cc @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/math_node.h" +#include "ballistica/scene_v1/node/math_node.h" #include -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" -namespace ballistica { +namespace ballistica::scene_v1 { class MathNodeType : public NodeType { public: @@ -112,4 +112,4 @@ auto MathNode::GetOutput() -> std::vector { return outputs; } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/math_node.h b/src/ballistica/scene_v1/node/math_node.h similarity index 78% rename from src/ballistica/scene/node/math_node.h rename to src/ballistica/scene_v1/node/math_node.h index c9f7b5ca..407f7a26 100644 --- a/src/ballistica/scene/node/math_node.h +++ b/src/ballistica/scene_v1/node/math_node.h @@ -1,14 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_MATH_NODE_H_ -#define BALLISTICA_SCENE_NODE_MATH_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_MATH_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_MATH_NODE_H_ #include #include -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { // An node used to create simple mathematical relationships via // attribute connections @@ -31,6 +31,6 @@ class MathNode : public Node { Operation operation_ = Operation::kAdd; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_MATH_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_MATH_NODE_H_ diff --git a/src/ballistica/scene/node/node.cc b/src/ballistica/scene_v1/node/node.cc similarity index 78% rename from src/ballistica/scene/node/node.cc rename to src/ballistica/scene_v1/node/node.cc index 9f369442..a07b78b1 100644 --- a/src/ballistica/scene/node/node.cc +++ b/src/ballistica/scene_v1/node/node.cc @@ -1,17 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/node/node.h" -#include "ballistica/dynamics/part.h" -#include "ballistica/python/class/python_class_node.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_context_call.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_attribute_connection.h" -#include "ballistica/scene/scene.h" -#include "ballistica/scene/scene_stream.h" +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/scene_v1/dynamics/part.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_attribute_connection.h" +#include "ballistica/scene_v1/python/class/python_class_node.h" +#include "ballistica/scene_v1/python/scene_v1_python.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/scene_v1/support/session_stream.h" +#include "ballistica/shared/python/python.h" -namespace ballistica { +namespace ballistica::scene_v1 { NodeType::~NodeType() { Log(LogLevel::kError, @@ -31,7 +32,7 @@ void Node::AddToScene(Scene* scene) { // id_ = scene->next_node_id_++; // our_iterator_ = // scene->nodes_.insert(scene->nodes_.end(), Object::Ref(this)); - if (SceneStream* os = scene->GetSceneStream()) { + if (SessionStream* os = scene->GetSceneStream()) { os->AddNode(this); } } @@ -39,8 +40,8 @@ void Node::AddToScene(Scene* scene) { Node::~Node() { // Kill any incoming/outgoing attr connections. for (auto& i : attribute_connections_incoming_) { - NodeAttributeConnection* a = i.second.get(); - assert(a && a->src_node.exists()); + NodeAttributeConnection* a = i.second.Get(); + assert(a && a->src_node.Exists()); // Remove from src node's outgoing list. a->src_node->attribute_connections_.erase(a->src_iterator); @@ -49,8 +50,8 @@ Node::~Node() { // Kill all refs on our side; this should kill the connections. attribute_connections_incoming_.clear(); for (auto& attribute_connection : attribute_connections_) { - NodeAttributeConnection* a = attribute_connection.get(); - assert(a && a->dst_node.exists()); + NodeAttributeConnection* a = attribute_connection.Get(); + assert(a && a->dst_node.Exists()); // Remove from dst node's incoming list. auto j = @@ -72,19 +73,17 @@ Node::~Node() { // If we were going to an output stream, inform them of our demise. assert(scene()); - if (SceneStream* output_stream = scene()->GetSceneStream()) { + if (SessionStream* output_stream = scene()->GetSceneStream()) { output_stream->RemoveNode(this); } } auto Node::GetResyncDataSize() -> int { return 0; } -auto Node::GetResyncData() -> std::vector { - return std::vector(); -} +auto Node::GetResyncData() -> std::vector { return {}; } void Node::ApplyResyncData(const std::vector& data) {} -void Node::Draw(FrameDef* frame_def) {} +void Node::Draw(base::FrameDef* frame_def) {} void Node::OnCreate() {} auto Node::GetObjectDescription() const -> std::string { @@ -185,14 +184,14 @@ void Node::UpdateConnections() { for (auto& attribute_connection : attribute_connections_) { // Connections should go away when either node dies; make sure that's // working. - assert(attribute_connection->src_node.exists() - && attribute_connection->dst_node.exists()); + assert(attribute_connection->src_node.Exists() + && attribute_connection->dst_node.Exists()); attribute_connection->Update(); } } void Node::AddNodeDeathAction(PyObject* call_obj) { - death_actions_.push_back(Object::New(call_obj)); + death_actions_.push_back(Object::New(call_obj)); } void Node::AddDependentNode(Node* node) { @@ -207,7 +206,7 @@ void Node::AddDependentNode(Node* node) { if (!dependent_nodes_.empty()) { std::vector > live_nodes; for (auto& dependent_node : dependent_nodes_) { - if (dependent_node.exists()) live_nodes.push_back(dependent_node); + if (dependent_node.Exists()) live_nodes.push_back(dependent_node); } dependent_nodes_.swap(live_nodes); } @@ -223,7 +222,7 @@ void Node::SetDelegate(PyObject* delegate_obj) { } auto Node::GetPyRef(bool new_ref) -> PyObject* { - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (py_ref_ == nullptr) { py_ref_ = PythonClassNode::Create(this); } @@ -234,7 +233,7 @@ auto Node::GetPyRef(bool new_ref) -> PyObject* { } auto Node::GetDelegate() -> PyObject* { - PyObject* ref = delegate_.get(); + PyObject* ref = delegate_.Get(); if (!ref) { return nullptr; } @@ -256,10 +255,12 @@ void Node::DispatchOutOfBoundsMessage() { PythonRef instance; { Python::ScopedCallLabel label("OutOfBoundsMessage instantiation"); - instance = g_python->obj(Python::ObjID::kOutOfBoundsMessageClass).Call(); + instance = g_scene_v1->python->objs() + .Get(SceneV1Python::ObjID::kOutOfBoundsMessageClass) + .Call(); } - if (instance.exists()) { - DispatchUserMessage(instance.get(), "Node OutOfBoundsMessage dispatch"); + if (instance.Exists()) { + DispatchUserMessage(instance.Get(), "Node OutOfBoundsMessage dispatch"); } else { Log(LogLevel::kError, "Error creating OutOfBoundsMessage"); } @@ -271,10 +272,12 @@ void Node::DispatchPickUpMessage(Node* node) { PythonRef instance; { Python::ScopedCallLabel label("PickUpMessage instantiation"); - instance = g_python->obj(Python::ObjID::kPickUpMessageClass).Call(args); + instance = g_scene_v1->python->objs() + .Get(SceneV1Python::ObjID::kPickUpMessageClass) + .Call(args); } - if (instance.exists()) { - DispatchUserMessage(instance.get(), "Node PickUpMessage dispatch"); + if (instance.Exists()) { + DispatchUserMessage(instance.Get(), "Node PickUpMessage dispatch"); } else { Log(LogLevel::kError, "Error creating PickUpMessage"); } @@ -284,10 +287,12 @@ void Node::DispatchDropMessage() { PythonRef instance; { Python::ScopedCallLabel label("DropMessage instantiation"); - instance = g_python->obj(Python::ObjID::kDropMessageClass).Call(); + instance = g_scene_v1->python->objs() + .Get(SceneV1Python::ObjID::kDropMessageClass) + .Call(); } - if (instance.exists()) { - DispatchUserMessage(instance.get(), "Node DropMessage dispatch"); + if (instance.Exists()) { + DispatchUserMessage(instance.Get(), "Node DropMessage dispatch"); } else { Log(LogLevel::kError, "Error creating DropMessage"); } @@ -300,10 +305,12 @@ void Node::DispatchPickedUpMessage(Node* by_node) { PythonRef instance; { Python::ScopedCallLabel label("PickedUpMessage instantiation"); - instance = g_python->obj(Python::ObjID::kPickedUpMessageClass).Call(args); + instance = g_scene_v1->python->objs() + .Get(SceneV1Python::ObjID::kPickedUpMessageClass) + .Call(args); } - if (instance.exists()) { - DispatchUserMessage(instance.get(), "Node PickedUpMessage dispatch"); + if (instance.Exists()) { + DispatchUserMessage(instance.Get(), "Node PickedUpMessage dispatch"); } else { Log(LogLevel::kError, "Error creating PickedUpMessage"); } @@ -316,10 +323,12 @@ void Node::DispatchDroppedMessage(Node* by_node) { PythonRef instance; { Python::ScopedCallLabel label("DroppedMessage instantiation"); - instance = g_python->obj(Python::ObjID::kDroppedMessageClass).Call(args); + instance = g_scene_v1->python->objs() + .Get(SceneV1Python::ObjID::kDroppedMessageClass) + .Call(args); } - if (instance.exists()) { - DispatchUserMessage(instance.get(), "Node DroppedMessage dispatch"); + if (instance.Exists()) { + DispatchUserMessage(instance.Get(), "Node DroppedMessage dispatch"); } else { Log(LogLevel::kError, "Error creating DroppedMessage"); } @@ -329,10 +338,12 @@ void Node::DispatchShouldShatterMessage() { PythonRef instance; { Python::ScopedCallLabel label("ShouldShatterMessage instantiation"); - instance = g_python->obj(Python::ObjID::kShouldShatterMessageClass).Call(); + instance = g_scene_v1->python->objs() + .Get(SceneV1Python::ObjID::kShouldShatterMessageClass) + .Call(); } - if (instance.exists()) { - DispatchUserMessage(instance.get(), "Node ShouldShatterMessage dispatch"); + if (instance.Exists()) { + DispatchUserMessage(instance.Get(), "Node ShouldShatterMessage dispatch"); } else { Log(LogLevel::kError, "Error creating ShouldShatterMessage"); } @@ -343,23 +354,24 @@ void Node::DispatchImpactDamageMessage(float intensity) { PythonRef instance; { Python::ScopedCallLabel label("ImpactDamageMessage instantiation"); - instance = - g_python->obj(Python::ObjID::kImpactDamageMessageClass).Call(args); + instance = g_scene_v1->python->objs() + .Get(SceneV1Python::ObjID::kImpactDamageMessageClass) + .Call(args); } - if (instance.exists()) { - DispatchUserMessage(instance.get(), "Node ImpactDamageMessage dispatch"); + if (instance.Exists()) { + DispatchUserMessage(instance.Get(), "Node ImpactDamageMessage dispatch"); } else { Log(LogLevel::kError, "Error creating ImpactDamageMessage"); } } void Node::DispatchUserMessage(PyObject* obj, const char* label) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (scene_->shutting_down()) { return; } - ScopedSetContext cp(context()); + base::ScopedSetContext ssc(context_ref()); PyObject* delegate = GetDelegate(); if (delegate && delegate != Py_None) { try { @@ -401,6 +413,7 @@ void Node::CheckBodies() { auto NodeType::GetAttributeNames() const -> std::vector { std::vector names; + names.reserve(attributes_by_name_.size()); for (auto&& i : attributes_by_name_) { names.push_back(i.second->name()); } @@ -426,4 +439,4 @@ void Node::GetRigidBodyPickupLocations(int id, float* pos_obj, float* pos_char, hand_offset_2[0] = hand_offset_2[1] = hand_offset_2[2] = 0; } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/node.h b/src/ballistica/scene_v1/node/node.h similarity index 89% rename from src/ballistica/scene/node/node.h rename to src/ballistica/scene_v1/node/node.h index ba8776a5..c1a74b82 100644 --- a/src/ballistica/scene/node/node.h +++ b/src/ballistica/scene_v1/node/node.h @@ -1,19 +1,19 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_NODE_H_ -#define BALLISTICA_SCENE_NODE_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_NODE_H_ #include #include #include #include -#include "ballistica/ballistica.h" -#include "ballistica/core/context.h" -#include "ballistica/core/object.h" -#include "ballistica/python/python_ref.h" +#include "ballistica/base/base.h" +#include "ballistica/scene_v1/support/scene_v1_context.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_ref.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Define a static creation call for this node type #define BA_NODE_CREATE_CALL(FUNC) \ @@ -34,7 +34,7 @@ class Node : public Object { virtual void Step() {} // Called for each step of the sim. virtual void OnScreenSizeChange() {} // Called when screen size changes. virtual void OnLanguageChange() {} // Called when the language changes. - virtual void OnGraphicsQualityChanged(GraphicsQuality q) {} + virtual void OnGraphicsQualityChanged(base::GraphicsQuality q) {} // The node can rule out collisions between particular bodies using this. virtual auto PreFilterCollision(RigidBody* b1, RigidBody* r2) -> bool { @@ -113,7 +113,7 @@ class Node : public Object { float* hand_offset_2); // Called for each Node when it should render itself. - virtual void Draw(FrameDef* frame_def); + virtual void Draw(base::FrameDef* frame_def); // Called for each node once construction is completed // this can be a good time to create things from the initial attr set, etc @@ -128,7 +128,7 @@ class Node : public Object { virtual auto GetResyncDataSize() -> int; virtual auto GetResyncData() -> std::vector; virtual void ApplyResyncData(const std::vector& data); - auto context() const -> const Context& { return context_; } + auto context_ref() const -> const ContextRefSceneV1& { return context_ref_; } // Node labels are purely for local debugging - they aren't unique or sent // across the network or anything. @@ -157,7 +157,7 @@ class Node : public Object { auto parts() const -> const std::vector& { return parts_; } auto death_actions() const - -> const std::vector >& { + -> const std::vector >& { return death_actions_; } auto dependent_nodes() const -> const std::vector >& { @@ -199,7 +199,7 @@ class Node : public Object { // FIXME - We can get by with *just* a pointer to our scene // if we add a way to pull context from a scene. - Context context_; + ContextRefSceneV1 context_ref_; Scene* scene_{}; std::string label_; std::vector > dependent_nodes_; @@ -209,7 +209,7 @@ class Node : public Object { // Put this stuff at the bottom so it gets killed first PythonRef delegate_; - std::vector > death_actions_; + std::vector > death_actions_; // Outgoing attr connections in order created. std::list > attribute_connections_; @@ -221,6 +221,6 @@ class Node : public Object { friend class NodeAttributeUnbound; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_NODE_H_ diff --git a/src/ballistica/scene/node/node_attribute.cc b/src/ballistica/scene_v1/node/node_attribute.cc similarity index 77% rename from src/ballistica/scene/node/node_attribute.cc rename to src/ballistica/scene_v1/node/node_attribute.cc index 42c382b2..4411ca2f 100644 --- a/src/ballistica/scene/node/node_attribute.cc +++ b/src/ballistica/scene_v1/node/node_attribute.cc @@ -1,12 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_attribute.h" -#include "ballistica/scene/node/node.h" -#include "ballistica/scene/node/node_attribute_connection.h" -#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene_v1/node/node.h" +#include "ballistica/scene_v1/node/node_attribute_connection.h" +#include "ballistica/scene_v1/node/node_type.h" -namespace ballistica { +namespace ballistica::scene_v1 { auto NodeAttributeUnbound::GetNodeAttributeTypeName(NodeAttributeType t) -> std::string { @@ -39,14 +39,14 @@ auto NodeAttributeUnbound::GetNodeAttributeTypeName(NodeAttributeType t) return "sound"; case NodeAttributeType::kSoundArray: return "sound-array"; - case NodeAttributeType::kModel: - return "model"; - case NodeAttributeType::kModelArray: - return "model-array"; - case NodeAttributeType::kCollideModel: - return "collide-model"; - case NodeAttributeType::kCollideModelArray: - return "collide-model-array"; + case NodeAttributeType::kMesh: + return "mesh"; + case NodeAttributeType::kMeshArray: + return "mesh-array"; + case NodeAttributeType::kCollisionMesh: + return "collision-model"; + case NodeAttributeType::kCollisionMeshArray: + return "collision-mesh-array"; default: Log(LogLevel::kError, "Unknown attr type name: " + std::to_string(static_cast(t))); @@ -81,14 +81,14 @@ void NodeAttributeUnbound::DisconnectIncoming(Node* node) { assert(node); auto i = node->attribute_connections_incoming().find(index()); if (i != node->attribute_connections_incoming().end()) { - NodeAttributeConnection* a = i->second.get(); + NodeAttributeConnection* a = i->second.Get(); #if BA_DEBUG_BUILD Object::WeakRef test_ref(a); #endif assert(a != nullptr); - assert(a->src_node.exists()); + assert(a->src_node.Exists()); // Remove from src node's outgoing list. a->src_node->attribute_connections_.erase(a->src_iterator); @@ -97,7 +97,7 @@ void NodeAttributeUnbound::DisconnectIncoming(Node* node) { node->attribute_connections_incoming_.erase(i); #if BA_DEBUG_BUILD - if (test_ref.exists()) { + if (test_ref.Exists()) { Log(LogLevel::kError, "Attr connection still exists after ref releases!"); } #endif @@ -192,78 +192,83 @@ void NodeAttributeUnbound::Set(Node* node, + node_type()->name() + "' as a material array."); } -auto NodeAttributeUnbound::GetAsTexture(Node* node) -> Texture* { +auto NodeAttributeUnbound::GetAsTexture(Node* node) -> SceneTexture* { throw Exception("Can't get attr '" + name() + "' on node type '" + node_type()->name() + "' as a texture."); } -void NodeAttributeUnbound::Set(Node* node, Texture* value) { +void NodeAttributeUnbound::Set(Node* node, SceneTexture* value) { throw Exception("Can't set attr '" + name() + "' on node type '" + node_type()->name() + "' as a texture."); } -auto NodeAttributeUnbound::GetAsTextures(Node* node) -> std::vector { +auto NodeAttributeUnbound::GetAsTextures(Node* node) + -> std::vector { throw Exception("Can't get attr '" + name() + "' on node type '" + node_type()->name() + "' as a texture array."); } -void NodeAttributeUnbound::Set(Node* node, const std::vector& value) { - throw Exception("Can't set attr '" + name() + "' on node type '" - + node_type()->name() + "' as a texture array."); -} - -auto NodeAttributeUnbound::GetAsSound(Node* node) -> Sound* { - throw Exception("Can't get attr '" + name() + "' on node type '" - + node_type()->name() + "' as a sound."); -} -void NodeAttributeUnbound::Set(Node* node, Sound* value) { - throw Exception("Can't set attr '" + name() + "' on node type '" - + node_type()->name() + "' as a sound."); -} - -auto NodeAttributeUnbound::GetAsSounds(Node* node) -> std::vector { - throw Exception("Can't get attr '" + name() + "' on node type '" - + node_type()->name() + "' as a sound array."); -} -void NodeAttributeUnbound::Set(Node* node, const std::vector& value) { - throw Exception("Can't set attr '" + name() + "' on node type '" - + node_type()->name() + "' as a sound array."); -} - -auto NodeAttributeUnbound::GetAsModel(Node* node) -> Model* { - throw Exception("Can't get attr '" + name() + "' on node type '" - + node_type()->name() + "' as a model."); -} -void NodeAttributeUnbound::Set(Node* node, Model* value) { - throw Exception("Can't set attr '" + name() + "' on node type '" - + node_type()->name() + "' as a model."); -} - -auto NodeAttributeUnbound::GetAsModels(Node* node) -> std::vector { - throw Exception("Can't get attr '" + name() + "' on node type '" - + node_type()->name() + "' as a model array."); -} -void NodeAttributeUnbound::Set(Node* node, const std::vector& value) { - throw Exception("Can't set attr '" + name() + "' on node type '" - + node_type()->name() + "' as a model array."); -} - -auto NodeAttributeUnbound::GetAsCollideModel(Node* node) -> CollideModel* { - throw Exception("Can't get attr '" + name() + "' on node type '" - + node_type()->name() + "' as a collide-model."); -} -void NodeAttributeUnbound::Set(Node* node, CollideModel* value) { - throw Exception("Can't set attr '" + name() + "' on node type '" - + node_type()->name() + "' as a collide-model."); -} - -auto NodeAttributeUnbound::GetAsCollideModels(Node* node) - -> std::vector { - throw Exception("Can't get attr '" + name() + "' on node type '" - + node_type()->name() + "' as a collide-model array."); -} void NodeAttributeUnbound::Set(Node* node, - const std::vector& value) { + const std::vector& value) { throw Exception("Can't set attr '" + name() + "' on node type '" - + node_type()->name() + "' as a collide-model array."); + + node_type()->name() + "' as a texture array."); } -} // namespace ballistica +auto NodeAttributeUnbound::GetAsSound(Node* node) -> SceneSound* { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a sound."); +} +void NodeAttributeUnbound::Set(Node* node, SceneSound* value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a sound."); +} + +auto NodeAttributeUnbound::GetAsSounds(Node* node) -> std::vector { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a sound array."); +} +void NodeAttributeUnbound::Set(Node* node, + const std::vector& value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a sound array."); +} + +auto NodeAttributeUnbound::GetAsMesh(Node* node) -> SceneMesh* { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a mesh."); +} +void NodeAttributeUnbound::Set(Node* node, SceneMesh* value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a mesh."); +} + +auto NodeAttributeUnbound::GetAsMeshes(Node* node) -> std::vector { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a mesh array."); +} +void NodeAttributeUnbound::Set(Node* node, + const std::vector& value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a mesh array."); +} + +auto NodeAttributeUnbound::GetAsCollisionMesh(Node* node) + -> SceneCollisionMesh* { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a collision-mesh."); +} +void NodeAttributeUnbound::Set(Node* node, SceneCollisionMesh* value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a collision-mesh."); +} + +auto NodeAttributeUnbound::GetAsCollisionMeshes(Node* node) + -> std::vector { + throw Exception("Can't get attr '" + name() + "' on node type '" + + node_type()->name() + "' as a collision-mesh array."); +} +void NodeAttributeUnbound::Set(Node* node, + const std::vector& value) { + throw Exception("Can't set attr '" + name() + "' on node type '" + + node_type()->name() + "' as a collision-mesh array."); +} + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/node_attribute.h b/src/ballistica/scene_v1/node/node_attribute.h similarity index 85% rename from src/ballistica/scene/node/node_attribute.h rename to src/ballistica/scene_v1/node/node_attribute.h index 92f73064..a6ffb747 100644 --- a/src/ballistica/scene/node/node_attribute.h +++ b/src/ballistica/scene_v1/node/node_attribute.h @@ -1,14 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_NODE_ATTRIBUTE_H_ -#define BALLISTICA_SCENE_NODE_NODE_ATTRIBUTE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_NODE_ATTRIBUTE_H_ +#define BALLISTICA_SCENE_V1_NODE_NODE_ATTRIBUTE_H_ #include #include -#include "ballistica/ballistica.h" +#include "ballistica/scene_v1/scene_v1.h" -namespace ballistica { +namespace ballistica::scene_v1 { #pragma clang diagnostic push #pragma ide diagnostic ignored "OCUnusedMacroInspection" @@ -60,29 +60,30 @@ class NodeAttributeUnbound { virtual auto GetAsMaterials(Node* node) -> std::vector; virtual void Set(Node* node, const std::vector& value); - virtual auto GetAsTexture(Node* node) -> Texture*; - virtual void Set(Node* node, Texture* value); + virtual auto GetAsTexture(Node* node) -> SceneTexture*; + virtual void Set(Node* node, SceneTexture* value); - virtual auto GetAsTextures(Node* node) -> std::vector; - virtual void Set(Node* node, const std::vector& values); + virtual auto GetAsTextures(Node* node) -> std::vector; + virtual void Set(Node* node, const std::vector& values); - virtual auto GetAsSound(Node* node) -> Sound*; - virtual void Set(Node* node, Sound* value); + virtual auto GetAsSound(Node* node) -> SceneSound*; + virtual void Set(Node* node, SceneSound* value); - virtual auto GetAsSounds(Node* node) -> std::vector; - virtual void Set(Node* node, const std::vector& values); + virtual auto GetAsSounds(Node* node) -> std::vector; + virtual void Set(Node* node, const std::vector& values); - virtual auto GetAsModel(Node* node) -> Model*; - virtual void Set(Node* node, Model* value); + virtual auto GetAsMesh(Node* node) -> SceneMesh*; + virtual void Set(Node* node, SceneMesh* value); - virtual auto GetAsModels(Node* node) -> std::vector; - virtual void Set(Node* node, const std::vector& values); + virtual auto GetAsMeshes(Node* node) -> std::vector; + virtual void Set(Node* node, const std::vector& values); - virtual auto GetAsCollideModel(Node* node) -> CollideModel*; - virtual void Set(Node* node, CollideModel* value); + virtual auto GetAsCollisionMesh(Node* node) -> SceneCollisionMesh*; + virtual void Set(Node* node, SceneCollisionMesh* value); - virtual auto GetAsCollideModels(Node* node) -> std::vector; - virtual void Set(Node* node, const std::vector& values); + virtual auto GetAsCollisionMeshes(Node* node) + -> std::vector; + virtual void Set(Node* node, const std::vector& values); auto is_read_only() const -> bool { return static_cast(flags_ & kNodeAttributeFlagReadOnly); @@ -159,34 +160,40 @@ class NodeAttribute { void Set(const std::vector& value) const { attr->Set(node, value); } - auto GetAsTexture() const -> Texture* { return attr->GetAsTexture(node); } - void Set(Texture* value) const { attr->Set(node, value); } - auto GetAsTextures() const -> std::vector { + auto GetAsTexture() const -> SceneTexture* { + return attr->GetAsTexture(node); + } + void Set(SceneTexture* value) const { attr->Set(node, value); } + auto GetAsTextures() const -> std::vector { return attr->GetAsTextures(node); } - void Set(const std::vector& values) const { + void Set(const std::vector& values) const { attr->Set(node, values); } - auto GetAsSound() const -> Sound* { return attr->GetAsSound(node); } - void Set(Sound* value) const { attr->Set(node, value); } - auto GetAsSounds() const -> std::vector { + auto GetAsSound() const -> SceneSound* { return attr->GetAsSound(node); } + void Set(SceneSound* value) const { attr->Set(node, value); } + auto GetAsSounds() const -> std::vector { return attr->GetAsSounds(node); } - void Set(const std::vector& values) const { attr->Set(node, values); } - auto GetAsModel() const -> Model* { return attr->GetAsModel(node); } - void Set(Model* value) const { attr->Set(node, value); } - auto GetAsModels() const -> std::vector { - return attr->GetAsModels(node); + void Set(const std::vector& values) const { + attr->Set(node, values); } - void Set(const std::vector& values) const { attr->Set(node, values); } - auto GetAsCollideModel() const -> CollideModel* { - return attr->GetAsCollideModel(node); + auto GetAsMesh() const -> SceneMesh* { return attr->GetAsMesh(node); } + void Set(SceneMesh* value) const { attr->Set(node, value); } + auto GetAsMeshes() const -> std::vector { + return attr->GetAsMeshes(node); } - void Set(CollideModel* value) const { attr->Set(node, value); } - auto GetAsCollideModels() const -> std::vector { - return attr->GetAsCollideModels(node); + void Set(const std::vector& values) const { + attr->Set(node, values); } - void Set(const std::vector& values) const { + auto GetAsCollisionMesh() const -> SceneCollisionMesh* { + return attr->GetAsCollisionMesh(node); + } + void Set(SceneCollisionMesh* value) const { attr->Set(node, value); } + auto GetAsCollisionMeshes() const -> std::vector { + return attr->GetAsCollisionMeshes(node); + } + void Set(const std::vector& values) const { attr->Set(node, values); } }; @@ -396,11 +403,11 @@ class NodeAttributeUnboundTexture : public NodeAttributeUnbound { : NodeAttributeUnbound(node_type, NodeAttributeType::kTexture, name, flags) {} // Override these: - auto GetAsTexture(Node* node) -> Texture* override { + auto GetAsTexture(Node* node) -> SceneTexture* override { NotReadableError(node); return nullptr; } - void Set(Node* node, Texture* val) override { NotWritableError(node); } + void Set(Node* node, SceneTexture* val) override { NotWritableError(node); } }; // Texture array attr. @@ -411,11 +418,11 @@ class NodeAttributeUnboundTextureArray : public NodeAttributeUnbound { : NodeAttributeUnbound(node_type, NodeAttributeType::kTextureArray, name, flags) {} // Override these: - auto GetAsTextures(Node* node) -> std::vector override { + auto GetAsTextures(Node* node) -> std::vector override { NotReadableError(node); - return std::vector(); + return std::vector(); } - void Set(Node* node, const std::vector& vals) override { + void Set(Node* node, const std::vector& vals) override { NotWritableError(node); } }; @@ -428,11 +435,11 @@ class NodeAttributeUnboundSound : public NodeAttributeUnbound { : NodeAttributeUnbound(node_type, NodeAttributeType::kSound, name, flags) {} // override these: - auto GetAsSound(Node* node) -> Sound* override { + auto GetAsSound(Node* node) -> SceneSound* override { NotReadableError(node); return nullptr; } - void Set(Node* node, Sound* val) override { NotWritableError(node); } + void Set(Node* node, SceneSound* val) override { NotWritableError(node); } }; // Sound array attr. @@ -443,75 +450,79 @@ class NodeAttributeUnboundSoundArray : public NodeAttributeUnbound { : NodeAttributeUnbound(node_type, NodeAttributeType::kSoundArray, name, flags) {} // Override these: - auto GetAsSounds(Node* node) -> std::vector override { + auto GetAsSounds(Node* node) -> std::vector override { NotReadableError(node); - return std::vector(); + return std::vector(); } - void Set(Node* node, const std::vector& vals) override { + void Set(Node* node, const std::vector& vals) override { NotWritableError(node); } }; -// Model attr. -class NodeAttributeUnboundModel : public NodeAttributeUnbound { +// Mesh attr. +class NodeAttributeUnboundMesh : public NodeAttributeUnbound { public: - NodeAttributeUnboundModel(NodeType* node_type, const std::string& name, - uint32_t flags) - : NodeAttributeUnbound(node_type, NodeAttributeType::kModel, name, - flags) {} + NodeAttributeUnboundMesh(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kMesh, name, flags) { + } // Override these: - auto GetAsModel(Node* node) -> Model* override { + auto GetAsMesh(Node* node) -> SceneMesh* override { NotReadableError(node); return nullptr; } - void Set(Node* node, Model* val) override { NotWritableError(node); } + void Set(Node* node, SceneMesh* val) override { NotWritableError(node); } }; -// Model array attr. -class NodeAttributeUnboundModelArray : public NodeAttributeUnbound { +// Mesh array attr. +class NodeAttributeUnboundMeshArray : public NodeAttributeUnbound { public: - NodeAttributeUnboundModelArray(NodeType* node_type, const std::string& name, - uint32_t flags) - : NodeAttributeUnbound(node_type, NodeAttributeType::kModelArray, name, + NodeAttributeUnboundMeshArray(NodeType* node_type, const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kMeshArray, name, flags) {} // Override these: - auto GetAsModels(Node* node) -> std::vector override { + auto GetAsMeshes(Node* node) -> std::vector override { NotReadableError(node); - return std::vector(); + return std::vector(); } - void Set(Node* node, const std::vector& vals) override { + void Set(Node* node, const std::vector& vals) override { NotWritableError(node); } }; -// Collide_model attr. -class NodeAttributeUnboundCollideModel : public NodeAttributeUnbound { +// Collision-mesh attr. +class NodeAttributeUnboundCollisionMesh : public NodeAttributeUnbound { public: - NodeAttributeUnboundCollideModel(NodeType* node_type, const std::string& name, - uint32_t flags) - : NodeAttributeUnbound(node_type, NodeAttributeType::kCollideModel, name, + NodeAttributeUnboundCollisionMesh(NodeType* node_type, + const std::string& name, uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kCollisionMesh, name, flags) {} // Override these: - auto GetAsCollideModel(Node* node) -> CollideModel* override { + auto GetAsCollisionMesh(Node* node) -> SceneCollisionMesh* override { NotReadableError(node); return nullptr; } - void Set(Node* node, CollideModel* val) override { NotWritableError(node); } + void Set(Node* node, SceneCollisionMesh* val) override { + NotWritableError(node); + } }; -// Collide_model array attr. -class NodeAttributeUnboundCollideModelArray : public NodeAttributeUnbound { +// Collision-mesh array attr. +class NodeAttributeUnboundCollisionMeshArray : public NodeAttributeUnbound { public: - NodeAttributeUnboundCollideModelArray(NodeType* node_type, - const std::string& name, uint32_t flags) - : NodeAttributeUnbound(node_type, NodeAttributeType::kCollideModelArray, + NodeAttributeUnboundCollisionMeshArray(NodeType* node_type, + const std::string& name, + uint32_t flags) + : NodeAttributeUnbound(node_type, NodeAttributeType::kCollisionMeshArray, name, flags) {} // Override these: - auto GetAsCollideModels(Node* node) -> std::vector override { + auto GetAsCollisionMeshes(Node* node) + -> std::vector override { NotReadableError(node); - return std::vector(); + return std::vector(); } - void Set(Node* node, const std::vector& vals) override { + void Set(Node* node, const std::vector& vals) override { NotWritableError(node); } }; @@ -839,12 +850,12 @@ class NodeAttributeUnboundCollideModelArray : public NodeAttributeUnbound { public: \ explicit Attr_##NAME(NodeType* node_type) \ : NodeAttributeUnboundTexture(node_type, #NAME, 0) {} \ - auto GetAsTexture(Node* node) -> Texture* override { \ + auto GetAsTexture(Node* node) -> SceneTexture* override { \ BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ assert(dynamic_cast(node) == tnode); \ return tnode->GETTER(); \ } \ - void Set(Node* node, Texture* val) override { \ + void Set(Node* node, SceneTexture* val) override { \ BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ assert(dynamic_cast(node) == tnode); \ tnode->SETTER(val); \ @@ -860,7 +871,7 @@ class NodeAttributeUnboundCollideModelArray : public NodeAttributeUnbound { explicit Attr_##NAME(NodeType* node_type) \ : NodeAttributeUnboundTexture(node_type, #NAME, \ kNodeAttributeFlagReadOnly) {} \ - auto GetAsTexture(Node* node) -> Texture* override { \ + auto GetAsTexture(Node* node) -> SceneTexture* override { \ BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ assert(dynamic_cast(node) == tnode); \ return tnode->GETTER(); \ @@ -870,22 +881,22 @@ class NodeAttributeUnboundCollideModelArray : public NodeAttributeUnbound { // Defines a texture attr subclass that interfaces with specific getter/setter // calls. -#define BA_TEXTURE_ARRAY_ATTR(NAME, GETTER, SETTER) \ - class Attr_##NAME : public NodeAttributeUnboundTextureArray { \ - public: \ - explicit Attr_##NAME(NodeType* node_type) \ - : NodeAttributeUnboundTextureArray(node_type, #NAME, 0) {} \ - auto GetAsTextures(Node* node) -> std::vector override { \ - BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ - assert(dynamic_cast(node) == tnode); \ - return tnode->GETTER(); \ - } \ - void Set(Node* node, const std::vector& vals) override { \ - BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ - assert(dynamic_cast(node) == tnode); \ - tnode->SETTER(vals); \ - } \ - }; \ +#define BA_TEXTURE_ARRAY_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundTextureArray { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundTextureArray(node_type, #NAME, 0) {} \ + auto GetAsTextures(Node* node) -> std::vector override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, const std::vector& vals) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(vals); \ + } \ + }; \ Attr_##NAME NAME; // Defines a sound attr subclass that interfaces with specific getter/setter @@ -895,12 +906,12 @@ class NodeAttributeUnboundCollideModelArray : public NodeAttributeUnbound { public: \ explicit Attr_##NAME(NodeType* node_type) \ : NodeAttributeUnboundSound(node_type, #NAME, 0) {} \ - auto GetAsSound(Node* node) -> Sound* override { \ + auto GetAsSound(Node* node) -> SceneSound* override { \ BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ assert(dynamic_cast(node) == tnode); \ return tnode->GETTER(); \ } \ - void Set(Node* node, Sound* val) override { \ + void Set(Node* node, SceneSound* val) override { \ BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ assert(dynamic_cast(node) == tnode); \ tnode->SETTER(val); \ @@ -915,12 +926,12 @@ class NodeAttributeUnboundCollideModelArray : public NodeAttributeUnbound { public: \ explicit Attr_##NAME(NodeType* node_type) \ : NodeAttributeUnboundSoundArray(node_type, #NAME, 0) {} \ - auto GetAsSounds(Node* node) -> std::vector override { \ + auto GetAsSounds(Node* node) -> std::vector override { \ BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ assert(dynamic_cast(node) == tnode); \ return tnode->GETTER(); \ } \ - void Set(Node* node, const std::vector& vals) override { \ + void Set(Node* node, const std::vector& vals) override { \ BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ assert(dynamic_cast(node) == tnode); \ tnode->SETTER(vals); \ @@ -928,19 +939,19 @@ class NodeAttributeUnboundCollideModelArray : public NodeAttributeUnbound { }; \ Attr_##NAME NAME; -// Defines a model attr subclass that interfaces with specific getter/setter +// Defines a mesh attr subclass that interfaces with specific getter/setter // calls. -#define BA_MODEL_ATTR(NAME, GETTER, SETTER) \ - class Attr_##NAME : public NodeAttributeUnboundModel { \ +#define BA_MESH_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundMesh { \ public: \ explicit Attr_##NAME(NodeType* node_type) \ - : NodeAttributeUnboundModel(node_type, #NAME, 0) {} \ - auto GetAsModel(Node* node) -> Model* override { \ + : NodeAttributeUnboundMesh(node_type, #NAME, 0) {} \ + auto GetAsMesh(Node* node) -> SceneMesh* override { \ BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ assert(dynamic_cast(node) == tnode); \ return tnode->GETTER(); \ } \ - void Set(Node* node, Model* val) override { \ + void Set(Node* node, SceneMesh* val) override { \ BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ assert(dynamic_cast(node) == tnode); \ tnode->SETTER(val); \ @@ -948,19 +959,19 @@ class NodeAttributeUnboundCollideModelArray : public NodeAttributeUnbound { }; \ Attr_##NAME NAME; -// Defines a model attr subclass that interfaces with specific getter/setter +// Defines a mesh attr subclass that interfaces with specific getter/setter // calls. -#define BA_MODEL_ARRAY_ATTR(NAME, GETTER, SETTER) \ - class Attr_##NAME : public NodeAttributeUnboundModelArray { \ +#define BA_MESH_ARRAY_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundMeshArray { \ public: \ explicit Attr_##NAME(NodeType* node_type) \ - : NodeAttributeUnboundModelArray(node_type, #NAME, 0) {} \ - auto GetAsModels(Node* node) -> std::vector override { \ + : NodeAttributeUnboundMeshArray(node_type, #NAME, 0) {} \ + auto GetAsMeshes(Node* node) -> std::vector override { \ BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ assert(dynamic_cast(node) == tnode); \ return tnode->GETTER(); \ } \ - void Set(Node* node, const std::vector& vals) override { \ + void Set(Node* node, const std::vector& vals) override { \ BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ assert(dynamic_cast(node) == tnode); \ tnode->SETTER(vals); \ @@ -968,19 +979,19 @@ class NodeAttributeUnboundCollideModelArray : public NodeAttributeUnbound { }; \ Attr_##NAME NAME; -// Defines a collide_model attr subclass that interfaces with specific +// Defines a collision_mesh attr subclass that interfaces with specific // getter/setter calls. -#define BA_COLLIDE_MODEL_ATTR(NAME, GETTER, SETTER) \ - class Attr_##NAME : public NodeAttributeUnboundCollideModel { \ +#define BA_COLLISION_MESH_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundCollisionMesh { \ public: \ explicit Attr_##NAME(NodeType* node_type) \ - : NodeAttributeUnboundCollideModel(node_type, #NAME, 0) {} \ - auto GetAsCollideModel(Node* node) -> CollideModel* override { \ + : NodeAttributeUnboundCollisionMesh(node_type, #NAME, 0) {} \ + auto GetAsCollisionMesh(Node* node) -> SceneCollisionMesh* override { \ BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ assert(dynamic_cast(node) == tnode); \ return tnode->GETTER(); \ } \ - void Set(Node* node, CollideModel* val) override { \ + void Set(Node* node, SceneCollisionMesh* val) override { \ BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ assert(dynamic_cast(node) == tnode); \ tnode->SETTER(val); \ @@ -988,29 +999,29 @@ class NodeAttributeUnboundCollideModelArray : public NodeAttributeUnbound { }; \ Attr_##NAME NAME; -// Defines a collide_model attr subclass that interfaces with specific +// Defines a collision_mesh attr subclass that interfaces with specific // getter/setter calls. -#define BA_COLLIDE_MODEL_ARRAY_ATTR(NAME, GETTER, SETTER) \ - class Attr_##NAME : public NodeAttributeUnboundCollideModelArray { \ - public: \ - explicit Attr_##NAME(NodeType* node_type) \ - : NodeAttributeUnboundCollideModelArray(node_type, #NAME, 0) {} \ - auto GetAsCollideModels(Node* node) \ - -> std::vector override { \ - BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ - assert(dynamic_cast(node) == tnode); \ - return tnode->GETTER(); \ - } \ - void Set(Node* node, const std::vector& vals) override { \ - BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ - assert(dynamic_cast(node) == tnode); \ - tnode->SETTER(vals); \ - } \ - }; \ +#define BA_COLLISION_MESH_ARRAY_ATTR(NAME, GETTER, SETTER) \ + class Attr_##NAME : public NodeAttributeUnboundCollisionMeshArray { \ + public: \ + explicit Attr_##NAME(NodeType* node_type) \ + : NodeAttributeUnboundCollisionMeshArray(node_type, #NAME, 0) {} \ + auto GetAsCollisionMeshes(Node* node) \ + -> std::vector override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + return tnode->GETTER(); \ + } \ + void Set(Node* node, const std::vector& vals) override { \ + BA_NODE_TYPE_CLASS* tnode = static_cast(node); \ + assert(dynamic_cast(node) == tnode); \ + tnode->SETTER(vals); \ + } \ + }; \ Attr_##NAME NAME; #pragma clang diagnostic pop -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_NODE_ATTRIBUTE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_NODE_ATTRIBUTE_H_ diff --git a/src/ballistica/scene/node/node_attribute_connection.cc b/src/ballistica/scene_v1/node/node_attribute_connection.cc similarity index 59% rename from src/ballistica/scene/node/node_attribute_connection.cc rename to src/ballistica/scene_v1/node/node_attribute_connection.cc index d3b308f9..7303c125 100644 --- a/src/ballistica/scene/node/node_attribute_connection.cc +++ b/src/ballistica/scene_v1/node/node_attribute_connection.cc @@ -1,16 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/node_attribute_connection.h" +#include "ballistica/scene_v1/node/node_attribute_connection.h" -#include "ballistica/scene/node/node.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene_v1/node/node.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" -namespace ballistica { +namespace ballistica::scene_v1 { void NodeAttributeConnection::Update() { - assert(src_node.exists() && dst_node.exists()); - auto* src_node_p{src_node.get()}; + assert(src_node.Exists() && dst_node.Exists()); + auto* src_node_p{src_node.Get()}; // We no longer update after errors now. // (the constant stream of exceptions slows things down too much) @@ -28,58 +28,59 @@ void NodeAttributeConnection::Update() { assert(dst_attr); switch (dst_attr->type()) { case NodeAttributeType::kFloat: - dst_attr->Set(dst_node.get(), src_attr->GetAsFloat(src_node_p)); + dst_attr->Set(dst_node.Get(), src_attr->GetAsFloat(src_node_p)); break; case NodeAttributeType::kInt: - dst_attr->Set(dst_node.get(), src_attr->GetAsInt(src_node_p)); + dst_attr->Set(dst_node.Get(), src_attr->GetAsInt(src_node_p)); break; case NodeAttributeType::kBool: - dst_attr->Set(dst_node.get(), src_attr->GetAsBool(src_node_p)); + dst_attr->Set(dst_node.Get(), src_attr->GetAsBool(src_node_p)); break; case NodeAttributeType::kString: - dst_attr->Set(dst_node.get(), src_attr->GetAsString(src_node_p)); + dst_attr->Set(dst_node.Get(), src_attr->GetAsString(src_node_p)); break; case NodeAttributeType::kIntArray: - dst_attr->Set(dst_node.get(), src_attr->GetAsInts(src_node_p)); + dst_attr->Set(dst_node.Get(), src_attr->GetAsInts(src_node_p)); break; case NodeAttributeType::kFloatArray: - dst_attr->Set(dst_node.get(), src_attr->GetAsFloats(src_node_p)); + dst_attr->Set(dst_node.Get(), src_attr->GetAsFloats(src_node_p)); break; case NodeAttributeType::kNode: - dst_attr->Set(dst_node.get(), src_attr->GetAsNode(src_node_p)); + dst_attr->Set(dst_node.Get(), src_attr->GetAsNode(src_node_p)); break; case NodeAttributeType::kNodeArray: - dst_attr->Set(dst_node.get(), src_attr->GetAsNodes(src_node_p)); + dst_attr->Set(dst_node.Get(), src_attr->GetAsNodes(src_node_p)); break; case NodeAttributeType::kPlayer: - dst_attr->Set(dst_node.get(), src_attr->GetAsPlayer(src_node_p)); + dst_attr->Set(dst_node.Get(), src_attr->GetAsPlayer(src_node_p)); break; case NodeAttributeType::kMaterialArray: - dst_attr->Set(dst_node.get(), src_attr->GetAsMaterials(src_node_p)); + dst_attr->Set(dst_node.Get(), src_attr->GetAsMaterials(src_node_p)); break; case NodeAttributeType::kTexture: - dst_attr->Set(dst_node.get(), src_attr->GetAsTexture(src_node_p)); + dst_attr->Set(dst_node.Get(), src_attr->GetAsTexture(src_node_p)); break; case NodeAttributeType::kTextureArray: - dst_attr->Set(dst_node.get(), src_attr->GetAsTextures(src_node_p)); + dst_attr->Set(dst_node.Get(), src_attr->GetAsTextures(src_node_p)); break; case NodeAttributeType::kSound: - dst_attr->Set(dst_node.get(), src_attr->GetAsSound(src_node_p)); + dst_attr->Set(dst_node.Get(), src_attr->GetAsSound(src_node_p)); break; case NodeAttributeType::kSoundArray: - dst_attr->Set(dst_node.get(), src_attr->GetAsSounds(src_node_p)); + dst_attr->Set(dst_node.Get(), src_attr->GetAsSounds(src_node_p)); break; - case NodeAttributeType::kModel: - dst_attr->Set(dst_node.get(), src_attr->GetAsModel(src_node_p)); + case NodeAttributeType::kMesh: + dst_attr->Set(dst_node.Get(), src_attr->GetAsMesh(src_node_p)); break; - case NodeAttributeType::kModelArray: - dst_attr->Set(dst_node.get(), src_attr->GetAsModels(src_node_p)); + case NodeAttributeType::kMeshArray: + dst_attr->Set(dst_node.Get(), src_attr->GetAsMeshes(src_node_p)); break; - case NodeAttributeType::kCollideModel: - dst_attr->Set(dst_node.get(), src_attr->GetAsCollideModel(src_node_p)); + case NodeAttributeType::kCollisionMesh: + dst_attr->Set(dst_node.Get(), src_attr->GetAsCollisionMesh(src_node_p)); break; - case NodeAttributeType::kCollideModelArray: - dst_attr->Set(dst_node.get(), src_attr->GetAsCollideModels(src_node_p)); + case NodeAttributeType::kCollisionMeshArray: + dst_attr->Set(dst_node.Get(), + src_attr->GetAsCollisionMeshes(src_node_p)); break; default: throw Exception("FIXME: unimplemented for attr type: '" @@ -104,4 +105,5 @@ void NodeAttributeConnection::Update() { } } } -} // namespace ballistica + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/node_attribute_connection.h b/src/ballistica/scene_v1/node/node_attribute_connection.h similarity index 54% rename from src/ballistica/scene/node/node_attribute_connection.h rename to src/ballistica/scene_v1/node/node_attribute_connection.h index 07b3737b..3485b5fb 100644 --- a/src/ballistica/scene/node/node_attribute_connection.h +++ b/src/ballistica/scene_v1/node/node_attribute_connection.h @@ -1,13 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_NODE_ATTRIBUTE_CONNECTION_H_ -#define BALLISTICA_SCENE_NODE_NODE_ATTRIBUTE_CONNECTION_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_NODE_ATTRIBUTE_CONNECTION_H_ +#define BALLISTICA_SCENE_V1_NODE_NODE_ATTRIBUTE_CONNECTION_H_ #include -#include "ballistica/core/object.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::scene_v1 { class NodeAttributeConnection : public Object { public: @@ -21,6 +22,6 @@ class NodeAttributeConnection : public Object { std::list >::iterator src_iterator; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_NODE_ATTRIBUTE_CONNECTION_H_ +#endif // BALLISTICA_SCENE_V1_NODE_NODE_ATTRIBUTE_CONNECTION_H_ diff --git a/src/ballistica/scene/node/node_type.h b/src/ballistica/scene_v1/node/node_type.h similarity index 88% rename from src/ballistica/scene/node/node_type.h rename to src/ballistica/scene_v1/node/node_type.h index 106b77f7..aa390e38 100644 --- a/src/ballistica/scene/node/node_type.h +++ b/src/ballistica/scene_v1/node/node_type.h @@ -1,16 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_NODE_TYPE_H_ -#define BALLISTICA_SCENE_NODE_NODE_TYPE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_NODE_TYPE_H_ +#define BALLISTICA_SCENE_V1_NODE_NODE_TYPE_H_ #include #include #include #include -#include "ballistica/ballistica.h" +#include "ballistica/scene_v1/scene_v1.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Type structure for a node, storing attribute lists and other static type // data. @@ -77,6 +77,6 @@ class NodeType { friend class Node; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_NODE_TYPE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_NODE_TYPE_H_ diff --git a/src/ballistica/scene/node/null_node.cc b/src/ballistica/scene_v1/node/null_node.cc similarity index 75% rename from src/ballistica/scene/node/null_node.cc rename to src/ballistica/scene_v1/node/null_node.cc index 10bb727e..8ee9e1de 100644 --- a/src/ballistica/scene/node/null_node.cc +++ b/src/ballistica/scene_v1/node/null_node.cc @@ -1,10 +1,10 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/null_node.h" +#include "ballistica/scene_v1/node/null_node.h" -#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene_v1/node/node_type.h" -namespace ballistica { +namespace ballistica::scene_v1 { // nothing to see here folks... move along class NullNodeType : public NodeType { @@ -25,4 +25,4 @@ auto NullNode::InitType() -> NodeType* { NullNode::NullNode(Scene* scene) : Node(scene, node_type) {} -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/null_node.h b/src/ballistica/scene_v1/node/null_node.h similarity index 51% rename from src/ballistica/scene/node/null_node.h rename to src/ballistica/scene_v1/node/null_node.h index e135e113..fb3a49b2 100644 --- a/src/ballistica/scene/node/null_node.h +++ b/src/ballistica/scene_v1/node/null_node.h @@ -1,12 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_NULL_NODE_H_ -#define BALLISTICA_SCENE_NODE_NULL_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_NULL_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_NULL_NODE_H_ -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/node/node.h" // empty node type - just used as a building block -namespace ballistica { +namespace ballistica::scene_v1 { class Scene; @@ -17,6 +17,6 @@ class NullNode : public Node { explicit NullNode(Scene* scene); }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_NULL_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_NULL_NODE_H_ diff --git a/src/ballistica/scene/node/player_node.cc b/src/ballistica/scene_v1/node/player_node.cc similarity index 79% rename from src/ballistica/scene/node/player_node.cc rename to src/ballistica/scene_v1/node/player_node.cc index cf8f1ba4..04a1e8db 100644 --- a/src/ballistica/scene/node/player_node.cc +++ b/src/ballistica/scene_v1/node/player_node.cc @@ -1,12 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/player_node.h" +#include "ballistica/scene_v1/node/player_node.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/scene_v1/support/scene.h" -namespace ballistica { +namespace ballistica::scene_v1 { class PlayerNodeType : public NodeType { public: @@ -44,4 +44,4 @@ void PlayerNode::SetPlayerID(int val) { scene()->SetPlayerNode(player_id_, this); } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/player_node.h b/src/ballistica/scene_v1/node/player_node.h similarity index 66% rename from src/ballistica/scene/node/player_node.h rename to src/ballistica/scene_v1/node/player_node.h index 45886b7e..82c18a10 100644 --- a/src/ballistica/scene/node/player_node.h +++ b/src/ballistica/scene_v1/node/player_node.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_PLAYER_NODE_H_ -#define BALLISTICA_SCENE_NODE_PLAYER_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_PLAYER_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_PLAYER_NODE_H_ #include -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { class PlayerNode : public Node { public: @@ -24,6 +24,6 @@ class PlayerNode : public Node { std::vector position_{0.0f, 0.0f, 0.0f}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_PLAYER_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_PLAYER_NODE_H_ diff --git a/src/ballistica/scene/node/prop_node.cc b/src/ballistica/scene_v1/node/prop_node.cc similarity index 86% rename from src/ballistica/scene/node/prop_node.cc rename to src/ballistica/scene_v1/node/prop_node.cc index 3e5b534c..2f285608 100644 --- a/src/ballistica/scene/node/prop_node.cc +++ b/src/ballistica/scene_v1/node/prop_node.cc @@ -1,16 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/prop_node.h" +#include "ballistica/scene_v1/node/prop_node.h" -#include "ballistica/dynamics/dynamics.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/area_of_interest.h" -#include "ballistica/graphics/camera.h" -#include "ballistica/graphics/component/object_component.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/graphics/component/object_component.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/graphics/support/area_of_interest.h" +#include "ballistica/base/graphics/support/camera.h" +#include "ballistica/scene_v1/dynamics/dynamics.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::scene_v1 { static void _doCalcERPCFM(float stiffness, float damping, float* erp, float* cfm) { @@ -38,8 +38,8 @@ PropNode::PropNode(Scene* scene, NodeType* override_node_type) PropNode::~PropNode() { if (area_of_interest_) { - g_graphics->camera()->DeleteAreaOfInterest( - static_cast(area_of_interest_)); + g_base->graphics->camera()->DeleteAreaOfInterest( + static_cast(area_of_interest_)); } } @@ -87,28 +87,29 @@ void PropNode::SetIsAreaOfInterest(bool val) { // either make one or kill the one we had if (val) { assert(area_of_interest_ == nullptr); - area_of_interest_ = g_graphics->camera()->NewAreaOfInterest(false); + area_of_interest_ = g_base->graphics->camera()->NewAreaOfInterest(false); } else { assert(area_of_interest_ != nullptr); - g_graphics->camera()->DeleteAreaOfInterest( - static_cast(area_of_interest_)); + g_base->graphics->camera()->DeleteAreaOfInterest( + static_cast(area_of_interest_)); area_of_interest_ = nullptr; } } } -void PropNode::Draw(FrameDef* frame_def) { +void PropNode::Draw(base::FrameDef* frame_def) { #if !BA_HEADLESS_BUILD - // need our texture, model, and body to be present to draw.. - if ((!model_.exists()) || (!color_texture_.exists()) || (!body_.exists())) { + // need our texture, mesh, and body to be present to draw.. + if ((!mesh_.Exists()) || (!color_texture_.Exists()) || (!body_.Exists())) { return; } - ObjectComponent c(frame_def->beauty_pass()); - c.SetTexture(color_texture_); - c.SetLightShadow(LightShadowType::kObject); - if (reflection_ != ReflectionType::kNone) { + base::ObjectComponent c(frame_def->beauty_pass()); + c.SetTexture(color_texture_.Exists() ? color_texture_->texture_data() + : nullptr); + c.SetLightShadow(base::LightShadowType::kObject); + if (reflection_ != base::ReflectionType::kNone) { c.SetReflection(reflection_); c.SetReflectionScale(reflection_scale_r_, reflection_scale_g_, reflection_scale_b_); @@ -117,15 +118,15 @@ void PropNode::Draw(FrameDef* frame_def) { c.SetColor(1.2f, 1.2f, 1.2f); } c.PushTransform(); - c.TransformToBody(*body_); - float s = model_scale_ * extra_model_scale_; + body_->ApplyToRenderComponent(&c); + float s = mesh_scale_ * extra_mesh_scale_; c.Scale(s, s, s); - c.DrawModel(model_->model_data()); + c.DrawMeshAsset(mesh_->mesh_data()); c.PopTransform(); c.Submit(); { // shadow - assert(body_.exists()); + assert(body_.Exists()); const dReal* pos_raw = dGeomGetPosition(body_->geom()); float pos[3]; pos[0] = pos_raw[0] + body_->blend_offset().x; @@ -141,32 +142,33 @@ void PropNode::Draw(FrameDef* frame_def) { } s_density *= 0.34f; { - GraphicsQuality quality = frame_def->quality(); + base::GraphicsQuality quality = frame_def->quality(); // fancy new cheap shadows { - float rs = shadow_size_ * model_scale_ * extra_model_scale_ * s_scale; - float d = (quality == GraphicsQuality::kLow ? 1.1f : 0.8f) * s_density; - g_graphics->DrawBlotch(Vector3f(pos), rs * 2.0f, 0.22f * d, 0.16f * d, - 0.10f * d, d); + float rs = shadow_size_ * mesh_scale_ * extra_mesh_scale_ * s_scale; + float d = + (quality == base::GraphicsQuality::kLow ? 1.1f : 0.8f) * s_density; + g_base->graphics->DrawBlotch(Vector3f(pos), rs * 2.0f, 0.22f * d, + 0.16f * d, 0.10f * d, d); } - if (quality > GraphicsQuality::kLow) { + if (quality > base::GraphicsQuality::kLow) { // More sharp accurate shadow. - if (light_model_.exists()) { - SimpleComponent c2(frame_def->light_shadow_pass()); + if (light_mesh_.Exists()) { + base::SimpleComponent c2(frame_def->light_shadow_pass()); c2.SetTransparent(true); float dd = body_type_ == BodyType::LANDMINE ? 0.5f : 1.0f; c2.SetColor(0.3f, 0.2f, 0.1f, 0.08f * s_density * dd); c2.PushTransform(); - c2.TransformToBody(*body_); + body_->ApplyToRenderComponent(&c2); float ss = body_type_ == BodyType::LANDMINE ? 0.9f : 1.0f; for (int i = 0; i < 4; i++) { c2.PushTransform(); - float s2 = ss * model_scale_ * extra_model_scale_ + float s2 = ss * mesh_scale_ * extra_mesh_scale_ * (1.3f - 0.08f * static_cast(i)); c2.Scale(s2, s2, s2); - c2.DrawModel(light_model_->model_data()); + c2.DrawMeshAsset(light_mesh_->mesh_data()); c2.PopTransform(); } c2.PopTransform(); @@ -175,12 +177,13 @@ void PropNode::Draw(FrameDef* frame_def) { // In fancy-pants mode we can do a softened version of ourself // for fake caustic effects. - if (light_model_.exists()) { - assert(color_texture_.exists()); - SimpleComponent c2(frame_def->light_shadow_pass()); + if (light_mesh_.Exists()) { + assert(color_texture_.Exists()); + base::SimpleComponent c2(frame_def->light_shadow_pass()); c2.SetTransparent(true); c2.SetPremultiplied(true); - c2.SetTexture(color_texture_); + c2.SetTexture(color_texture_.Exists() ? color_texture_->texture_data() + : nullptr); if (flashing_ && frame_def->frame_number() % 10 < 5) { c2.SetColor(0.026f * s_density, 0.026f * s_density, 0.026f * s_density, 0.0f); @@ -189,13 +192,13 @@ void PropNode::Draw(FrameDef* frame_def) { 0.022f * s_density, 0.0f); } c2.PushTransform(); - c2.TransformToBody(*body_); + body_->ApplyToRenderComponent(&c2); for (int i = 0; i < 4; i++) { c2.PushTransform(); - float s2 = model_scale_ * extra_model_scale_ * 1.7f; + float s2 = mesh_scale_ * extra_mesh_scale_ * 1.7f; c2.Scale(s2, s2, s2); c2.Rotate(-50.0f + 43.0f * static_cast(i), 0.2f, 0.4f, 0.6f); - c2.DrawModel(light_model_->model_data()); + c2.DrawMeshAsset(light_mesh_->mesh_data()); c2.PopTransform(); } c2.PopTransform(); @@ -231,7 +234,7 @@ auto PropNode::GetBody() const -> std::string { void PropNode::SetBodyScale(float val) { // this can be set exactly once - if (body_.exists()) { + if (body_.Exists()) { throw Exception("body_scale can't be set once body exists"); } body_scale_ = std::max(0.01f, val); @@ -263,7 +266,7 @@ void PropNode::SetBody(const std::string& val) { } // we're ok with redundant sets, but complain/ignore if they try to switch.. - if (body_.exists()) { + if (body_.Exists()) { if (body_type_ != body_type || shape_ != shape) { Log(LogLevel::kError, "body attr can not be changed from its initial value"); @@ -317,11 +320,11 @@ void PropNode::SetBody(const std::string& val) { } void PropNode::UpdateAreaOfInterest() { - auto* aoi = static_cast(area_of_interest_); + auto* aoi = static_cast(area_of_interest_); if (!aoi) { return; } - assert(body_.exists()); + assert(body_.Exists()); aoi->set_position(Vector3f(dGeomGetPosition(body_->geom()))); aoi->SetRadius(5.0f); } @@ -345,11 +348,11 @@ void PropNode::SetReflectionScale(const std::vector& vals) { } auto PropNode::GetReflection() const -> std::string { - return Graphics::StringFromReflectionType(reflection_); + return base::Graphics::StringFromReflectionType(reflection_); } void PropNode::SetReflection(const std::string& val) { - reflection_ = Graphics::ReflectionTypeFromString(val); + reflection_ = base::Graphics::ReflectionTypeFromString(val); } auto PropNode::GetMaterials() const -> std::vector { @@ -362,7 +365,7 @@ void PropNode::SetMaterials(const std::vector& vals) { auto PropNode::GetVelocity() const -> std::vector { // if we've got a body, return its velocity - if (body_.exists()) { + if (body_.Exists()) { const dReal* v = dBodyGetLinearVel(body_->body()); std::vector vv(3); vv[0] = v[0]; @@ -382,7 +385,7 @@ void PropNode::SetVelocity(const std::vector& vals) { PyExcType::kValue); } // if we've got a body, apply the velocity to that - if (body_.exists()) { + if (body_.Exists()) { dBodySetLinearVel(body_->body(), vals[0], vals[1], vals[2]); } else { // otherwise just store it in our internal vector in @@ -393,7 +396,7 @@ void PropNode::SetVelocity(const std::vector& vals) { auto PropNode::GetPosition() const -> std::vector { // if we've got a body, return its position - if (body_.exists()) { + if (body_.Exists()) { const dReal* p = dGeomGetPosition(body_->geom()); std::vector f(3); f[0] = p[0]; @@ -414,7 +417,7 @@ void PropNode::SetPosition(const std::vector& vals) { PyExcType::kValue); } // if we've got a body, apply the position to that - if (body_.exists()) { + if (body_.Exists()) { dBodySetPosition(body_->body(), vals[0], vals[1], vals[2]); } else { // otherwise just store it in our internal vector @@ -434,18 +437,18 @@ void PropNode::Step() { } BA_DEBUG_CHECK_BODIES(); - assert(body_.exists()); + assert(body_.Exists()); // FIXME - this should probably happen for RBDs automatically?... body_->UpdateBlending(); // on happy thoughts, keep us on the 2d plane.. - if (g_graphics->camera()->happy_thoughts_mode() && body_.exists()) { + if (g_base->graphics->camera()->happy_thoughts_mode() && body_.Exists()) { dBodyID b; const dReal *p, *v; b = body_->body(); p = dBodyGetPosition(b); - dBodySetPosition(b, p[0], p[1], kHappyThoughtsZPlane); + dBodySetPosition(b, p[0], p[1], base::kHappyThoughtsZPlane); v = dBodyGetLinearVel(b); dBodySetLinearVel(b, v[0], v[1], 0.0f); } @@ -482,7 +485,7 @@ void PropNode::Step() { } // if we're out of bounds, arrange to have ourself informed - if (body_.exists()) { + if (body_.Exists()) { const dReal* p = dBodyGetPosition(body_->body()); if (scene()->IsOutOfBounds(p[0], p[1], p[2])) { scene()->AddOutOfBoundsNode(this); @@ -533,7 +536,7 @@ void PropNode::Step() { auto PropNode::GetRigidBody(int id) -> RigidBody* { if (id == 0) { - return body_.get(); + return body_.Get(); } return nullptr; } @@ -551,9 +554,9 @@ auto PropNode::CollideCallback(dContact* c, int count, // this should never happen, right?.. assert(opposingbody->part()->node() != nullptr); - if ((stick_to_owner_ || opposingbody->part()->node() != owner_.get()) + if ((stick_to_owner_ || opposingbody->part()->node() != owner_.Get()) && !(f & RigidBody::kIsBumper)) { - if (body_.exists()) { + if (body_.Exists()) { // stick to static stuff: if (opposingbody->type() == RigidBody::Type::kGeomOnly) { const dReal* v; @@ -708,10 +711,10 @@ void PropNode::GetRigidBodyPickupLocations(int id, float* obj, float* character, } void PropNode::SetDensity(float val) { - if (body_.exists()) { + if (body_.Exists()) { throw Exception("can't set density after body has been set"); } density_ = std::max(0.01f, std::min(100.0f, val)); } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/prop_node.h b/src/ballistica/scene_v1/node/prop_node.h similarity index 78% rename from src/ballistica/scene/node/prop_node.h rename to src/ballistica/scene_v1/node/prop_node.h index d50c5122..697ca96c 100644 --- a/src/ballistica/scene/node/prop_node.h +++ b/src/ballistica/scene_v1/node/prop_node.h @@ -1,20 +1,20 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_PROP_NODE_H_ -#define BALLISTICA_SCENE_NODE_PROP_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_PROP_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_PROP_NODE_H_ #include #include -#include "ballistica/assets/component/model.h" -#include "ballistica/assets/component/texture.h" -#include "ballistica/dynamics/bg/bg_dynamics_shadow.h" -#include "ballistica/dynamics/part.h" -#include "ballistica/scene/node/node.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_shadow.h" +#include "ballistica/scene_v1/assets/scene_mesh.h" +#include "ballistica/scene_v1/assets/scene_texture.h" +#include "ballistica/scene_v1/dynamics/part.h" +#include "ballistica/scene_v1/node/node.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" -namespace ballistica { +namespace ballistica::scene_v1 { class PropNode : public Node { public: @@ -22,7 +22,7 @@ class PropNode : public Node { explicit PropNode(Scene* scene, NodeType* node_type = nullptr); ~PropNode() override; void HandleMessage(const char* data) override; - void Draw(FrameDef* frame_def) override; + void Draw(base::FrameDef* frame_def) override; void Step() override; auto GetRigidBody(int id) -> RigidBody* override; auto is_area_of_interest() const -> bool { @@ -35,23 +35,23 @@ class PropNode : public Node { void SetReflectionScale(const std::vector& vals); auto GetReflection() const -> std::string; void SetReflection(const std::string& val); - auto color_texture() const -> Texture* { return color_texture_.get(); } - void set_color_texture(Texture* val) { color_texture_ = val; } - auto GetModel() const -> Model* { return model_.get(); } - void set_model(Model* val) { model_ = val; } - auto light_model() const -> Model* { return light_model_.get(); } - void set_light_model(Model* val) { light_model_ = val; } + auto color_texture() const -> SceneTexture* { return color_texture_.Get(); } + void set_color_texture(SceneTexture* val) { color_texture_ = val; } + auto GetMesh() const -> SceneMesh* { return mesh_.Get(); } + void set_mesh(SceneMesh* val) { mesh_ = val; } + auto light_mesh() const -> SceneMesh* { return light_mesh_.Get(); } + void set_light_mesh(SceneMesh* val) { light_mesh_ = val; } auto sticky() const -> bool { return sticky_; } void set_sticky(bool val) { sticky_ = val; } auto shadow_size() const -> float { return shadow_size_; } void set_shadow_size(float val) { shadow_size_ = val; } auto stick_to_owner() const -> bool { return stick_to_owner_; } void set_stick_to_owner(bool val) { stick_to_owner_ = val; } - auto model_scale() const -> float { return model_scale_; } - void set_model_scale(float val) { model_scale_ = val; } + auto mesh_scale() const -> float { return mesh_scale_; } + void set_mesh_scale(float val) { mesh_scale_ = val; } auto flashing() const -> bool { return flashing_; } void set_flashing(bool val) { flashing_ = val; } - auto owner() const -> Node* { return owner_.get(); } + auto owner() const -> Node* { return owner_.Get(); } void set_owner(Node* val) { owner_ = val; } auto GetMaterials() const -> std::vector; void SetMaterials(const std::vector& materials); @@ -82,19 +82,19 @@ class PropNode : public Node { enum class BodyType { UNSET, SPHERE, BOX, LANDMINE, CRATE, CAPSULE, PUCK }; void UpdateAreaOfInterest(); #if !BA_HEADLESS_BUILD - BGDynamicsShadow shadow_; + base::BGDynamicsShadow shadow_; #endif Part part_; void* area_of_interest_{}; - float model_scale_{1.0f}; + float mesh_scale_{1.0f}; float shadow_size_{1.0f}; int color_texture_Val{}; float gravity_scale_{1.0f}; Object::Ref body_; RigidBody::Shape shape_{RigidBody::Shape::kSphere}; - Object::Ref color_texture_; - Object::Ref model_; - Object::Ref light_model_; + Object::Ref color_texture_; + Object::Ref mesh_; + Object::Ref light_mesh_; float density_{1.0f}; float body_scale_{1.0f}; float damping_{}; @@ -102,14 +102,14 @@ class PropNode : public Node { std::vector velocity_{0.0f, 0.0f, 0.0f}; std::vector position_{0.0f, 0.0f, 0.0f}; std::vector extra_acceleration_{0.0, 0.0, 0.0}; - float extra_model_scale_{1.0f}; // For use by subclasses. + float extra_mesh_scale_{1.0f}; // For use by subclasses. bool sticky_{}; Object::WeakRef owner_; bool flashing_{}; bool stick_to_owner_{}; BodyType body_type_{BodyType::UNSET}; bool reported_unset_body_type_{}; - ReflectionType reflection_{ReflectionType::kNone}; + base::ReflectionType reflection_{base::ReflectionType::kNone}; std::vector reflection_scale_{1.0f, 1.0f, 1.0f}; float reflection_scale_r_{1.0f}; float reflection_scale_g_{1.0f}; @@ -134,12 +134,12 @@ class PropNodeType : public NodeType { BA_FLOAT_ARRAY_ATTR(reflection_scale, reflection_scale, SetReflectionScale); BA_STRING_ATTR(reflection, GetReflection, SetReflection); BA_TEXTURE_ATTR(color_texture, color_texture, set_color_texture); - BA_MODEL_ATTR(model, GetModel, set_model); - BA_MODEL_ATTR(light_model, light_model, set_light_model); + BA_MESH_ATTR(mesh, GetMesh, set_mesh); + BA_MESH_ATTR(light_mesh, light_mesh, set_light_mesh); BA_BOOL_ATTR(sticky, sticky, set_sticky); BA_FLOAT_ATTR(shadow_size, shadow_size, set_shadow_size); BA_BOOL_ATTR(stick_to_owner, stick_to_owner, set_stick_to_owner); - BA_FLOAT_ATTR(model_scale, model_scale, set_model_scale); + BA_FLOAT_ATTR(mesh_scale, mesh_scale, set_mesh_scale); BA_BOOL_ATTR(flashing, flashing, set_flashing); BA_NODE_ATTR(owner, owner, set_owner); BA_MATERIAL_ARRAY_ATTR(materials, GetMaterials, SetMaterials); @@ -163,12 +163,12 @@ class PropNodeType : public NodeType { reflection_scale(this), reflection(this), color_texture(this), - model(this), - light_model(this), + mesh(this), + light_mesh(this), sticky(this), shadow_size(this), stick_to_owner(this), - model_scale(this), + mesh_scale(this), flashing(this), owner(this), materials(this), @@ -183,6 +183,6 @@ class PropNodeType : public NodeType { gravity_scale(this) {} }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_PROP_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_PROP_NODE_H_ diff --git a/src/ballistica/scene/node/region_node.cc b/src/ballistica/scene_v1/node/region_node.cc similarity index 84% rename from src/ballistica/scene/node/region_node.cc rename to src/ballistica/scene_v1/node/region_node.cc index e509bdc8..3b026da3 100644 --- a/src/ballistica/scene/node/region_node.cc +++ b/src/ballistica/scene_v1/node/region_node.cc @@ -1,14 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/region_node.h" +#include "ballistica/scene_v1/node/region_node.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/renderer.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" -namespace ballistica { +namespace ballistica::scene_v1 { class RegionNodeType : public NodeType { public: @@ -38,10 +37,10 @@ auto RegionNode::InitType() -> NodeType* { RegionNode::RegionNode(Scene* scene) : Node(scene, node_type), part_(this, false) {} -void RegionNode::Draw(FrameDef* frame_def) { - if (g_graphics_server->renderer()->debug_draw_mode()) { +void RegionNode::Draw(base::FrameDef* frame_def) { + if (g_base->graphics_server->renderer()->debug_draw_mode()) { // if (frame_def->renderer()->debug_draw_mode()) { - if (body_.exists()) { + if (body_.Exists()) { body_->Draw(frame_def->beauty_pass(), false); } } @@ -83,7 +82,7 @@ void RegionNode::SetScale(const std::vector& vals) { void RegionNode::Step() { // create our body if we have none - if (!body_.exists()) { + if (!body_.Exists()) { if (region_type_ == "sphere") { body_ = Object::New( 0, &part_, RigidBody::Type::kGeomOnly, RigidBody::Shape::kSphere, @@ -106,4 +105,4 @@ void RegionNode::Step() { } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/region_node.h b/src/ballistica/scene_v1/node/region_node.h similarity index 73% rename from src/ballistica/scene/node/region_node.h rename to src/ballistica/scene_v1/node/region_node.h index 2f53b53e..09d40236 100644 --- a/src/ballistica/scene/node/region_node.h +++ b/src/ballistica/scene_v1/node/region_node.h @@ -1,22 +1,22 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_REGION_NODE_H_ -#define BALLISTICA_SCENE_NODE_REGION_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_REGION_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_REGION_NODE_H_ #include #include -#include "ballistica/dynamics/part.h" -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/dynamics/part.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { // A region node - used to detect if an object is in a certain area class RegionNode : public Node { public: static auto InitType() -> NodeType*; explicit RegionNode(Scene* scene); - void Draw(FrameDef* frame_def) override; + void Draw(base::FrameDef* frame_def) override; void Step() override; auto position() const -> std::vector { return position_; } void SetPosition(const std::vector& vals); @@ -36,6 +36,6 @@ class RegionNode : public Node { Object::Ref body_; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_REGION_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_REGION_NODE_H_ diff --git a/src/ballistica/scene/node/scorch_node.cc b/src/ballistica/scene_v1/node/scorch_node.cc similarity index 69% rename from src/ballistica/scene/node/scorch_node.cc rename to src/ballistica/scene_v1/node/scorch_node.cc index a3d025f7..ca87da72 100644 --- a/src/ballistica/scene/node/scorch_node.cc +++ b/src/ballistica/scene_v1/node/scorch_node.cc @@ -1,13 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/scorch_node.h" +#include "ballistica/scene_v1/node/scorch_node.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/math/random.h" -namespace ballistica { +namespace ballistica::scene_v1 { class ScorchNodeType : public NodeType { public: @@ -59,23 +60,24 @@ void ScorchNode::SetPosition(const std::vector& vals) { position_ = vals; } -void ScorchNode::Draw(FrameDef* frame_def) { +void ScorchNode::Draw(base::FrameDef* frame_def) { float o = presence_; // modulate opacity by local shadow density - o *= g_graphics->GetShadowDensity(position_[0], position_[1], position_[2]); - SimpleComponent c(frame_def->light_shadow_pass()); + o *= g_base->graphics->GetShadowDensity(position_[0], position_[1], + position_[2]); + base::SimpleComponent c(frame_def->light_shadow_pass()); c.SetTransparent(true); c.SetColor(color_[0], color_[1], color_[2], o * 0.35f); - c.SetTexture(g_assets->GetTexture(big_ ? SystemTextureID::kScorchBig - : SystemTextureID::kScorch)); + c.SetTexture(g_base->assets->SysTexture(big_ ? base::SysTextureID::kScorchBig + : base::SysTextureID::kScorch)); c.PushTransform(); c.Translate(position_[0], position_[1], position_[2]); c.Scale(o * size_ * rand_size_[0], o * size_ * rand_size_[1], o * size_ * rand_size_[2]); c.Rotate(Utils::precalc_rand_1(id() % kPrecalcRandsCount) * 360.0f, 0, 1, 0); - c.DrawModel(g_assets->GetModel(SystemModelID::kScorch)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kScorch)); c.PopTransform(); c.Submit(); } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/scorch_node.h b/src/ballistica/scene_v1/node/scorch_node.h similarity index 75% rename from src/ballistica/scene/node/scorch_node.h rename to src/ballistica/scene_v1/node/scorch_node.h index 3859d60f..f933ab6e 100644 --- a/src/ballistica/scene/node/scorch_node.h +++ b/src/ballistica/scene_v1/node/scorch_node.h @@ -1,20 +1,20 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_SCORCH_NODE_H_ -#define BALLISTICA_SCENE_NODE_SCORCH_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_SCORCH_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_SCORCH_NODE_H_ #include -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { class ScorchNode : public Node { public: static auto InitType() -> NodeType*; explicit ScorchNode(Scene* scene); ~ScorchNode() override; - void Draw(FrameDef* frame_def) override; + void Draw(base::FrameDef* frame_def) override; auto position() const -> std::vector { return position_; } void SetPosition(const std::vector& vals); auto presence() const -> float { return presence_; } @@ -35,6 +35,6 @@ class ScorchNode : public Node { float rand_size_[3]{}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_SCORCH_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_SCORCH_NODE_H_ diff --git a/src/ballistica/scene/node/session_globals_node.cc b/src/ballistica/scene_v1/node/session_globals_node.cc similarity index 75% rename from src/ballistica/scene/node/session_globals_node.cc rename to src/ballistica/scene_v1/node/session_globals_node.cc index aa21c49d..7479632b 100644 --- a/src/ballistica/scene/node/session_globals_node.cc +++ b/src/ballistica/scene_v1/node/session_globals_node.cc @@ -1,18 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/session_globals_node.h" +#include "ballistica/scene_v1/node/session_globals_node.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/scene_v1/support/scene.h" -namespace ballistica { +namespace ballistica::scene_v1 { class SessionGlobalsNodeType : public NodeType { public: #define BA_NODE_TYPE_CLASS SessionGlobalsNode BA_NODE_CREATE_CALL(CreateSessionGlobals); - BA_INT64_ATTR_READONLY(real_time, GetRealTime); + BA_INT64_ATTR_READONLY(real_time, GetAppTimeMillisecs); BA_INT64_ATTR_READONLY(time, GetTime); BA_INT64_ATTR_READONLY(step, GetStep); #undef BA_NODE_TYPE_CLASS @@ -38,7 +38,7 @@ SessionGlobalsNode::SessionGlobalsNode(Scene* scene) : Node(scene, node_type) { SessionGlobalsNode::~SessionGlobalsNode() = default; -auto SessionGlobalsNode::GetRealTime() -> millisecs_t { +auto SessionGlobalsNode::GetAppTimeMillisecs() -> millisecs_t { // Pull this from our scene so we return consistent values throughout a step. return scene()->last_step_real_time(); } @@ -47,4 +47,4 @@ auto SessionGlobalsNode::GetTime() -> millisecs_t { return scene()->time(); } auto SessionGlobalsNode::GetStep() -> int64_t { return scene()->stepnum(); } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/node/session_globals_node.h b/src/ballistica/scene_v1/node/session_globals_node.h new file mode 100644 index 00000000..dcc07552 --- /dev/null +++ b/src/ballistica/scene_v1/node/session_globals_node.h @@ -0,0 +1,22 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_NODE_SESSION_GLOBALS_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_SESSION_GLOBALS_NODE_H_ + +#include "ballistica/scene_v1/node/node.h" + +namespace ballistica::scene_v1 { + +class SessionGlobalsNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit SessionGlobalsNode(Scene* scene); + ~SessionGlobalsNode() override; + auto GetAppTimeMillisecs() -> millisecs_t; + auto GetTime() -> millisecs_t; + auto GetStep() -> int64_t; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_NODE_SESSION_GLOBALS_NODE_H_ diff --git a/src/ballistica/scene/node/shield_node.cc b/src/ballistica/scene_v1/node/shield_node.cc similarity index 77% rename from src/ballistica/scene/node/shield_node.cc rename to src/ballistica/scene_v1/node/shield_node.cc index 44db758c..5018b4d1 100644 --- a/src/ballistica/scene/node/shield_node.cc +++ b/src/ballistica/scene_v1/node/shield_node.cc @@ -1,18 +1,19 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/shield_node.h" +#include "ballistica/scene_v1/node/shield_node.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/camera.h" -#include "ballistica/graphics/component/object_component.h" -#include "ballistica/graphics/component/post_process_component.h" -#include "ballistica/graphics/component/shield_component.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/graphics/component/object_component.h" +#include "ballistica/base/graphics/component/post_process_component.h" +#include "ballistica/base/graphics/component/shield_component.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/graphics/support/camera.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/math/random.h" -namespace ballistica { +namespace ballistica::scene_v1 { class ShieldNodeType : public NodeType { public: @@ -109,7 +110,7 @@ void ShieldNode::Step() { #endif // !BA_HEADLESS_BUILD } -void ShieldNode::Draw(FrameDef* frame_def) { +void ShieldNode::Draw(base::FrameDef* frame_def) { #if !BA_HEADLESS_BUILD { @@ -124,28 +125,30 @@ void ShieldNode::Draw(FrameDef* frame_def) { float rs = (0.6f + hurt_rand_ * 0.05f) * radius_ * s_scale * r_scale_; // draw our light on both terrain and objects - g_graphics->DrawBlotchSoft(Vector3f(&position_[0]), 3.4f * rs, - color_[0] * brightness, color_[1] * brightness, - color_[2] * brightness, 0.0f); + g_base->graphics->DrawBlotchSoft( + Vector3f(&position_[0]), 3.4f * rs, color_[0] * brightness, + color_[1] * brightness, color_[2] * brightness, 0.0f); // draw our light on both terrain and objects - g_graphics->DrawBlotchSoftObj( + g_base->graphics->DrawBlotchSoftObj( Vector3f(&position_[0]), 3.4f * rs, color_[0] * brightness * 0.4f, color_[1] * brightness * 0.4f, color_[2] * brightness * 0.4f, 0.0f); } // Life bar. { - uint32_t fade_time = 2000; + millisecs_t fade_time = 2000; millisecs_t since_last_hurt_change = scene()->time() - last_hurt_change_time_; if (since_last_hurt_change < fade_time || always_show_health_bar_) { - SimpleComponent c(frame_def->overlay_3d_pass()); + base::SimpleComponent c(frame_def->overlay_3d_pass()); c.SetTransparent(true); c.SetPremultiplied(true); c.PushTransform(); - float o = 1.0f - static_cast(since_last_hurt_change) / fade_time; + float o = 1.0f + - static_cast(since_last_hurt_change) + / static_cast(fade_time); if (always_show_health_bar_) { o = std::max(o, 0.5f); } @@ -174,25 +177,25 @@ void ShieldNode::Draw(FrameDef* frame_def) { c.PushTransform(); c.Translate(0.5f, half_height); c.Scale(1.1f, height + 0.1f); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.PopTransform(); c.SetColor(0.4f * o, 0.4f * o, 0.8f * o, 0.0f * o); c.PushTransform(); c.Translate(p_left * 0.5f, half_height); c.Scale(p_left, height); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.PopTransform(); c.SetColor(1.0f * o, 1.0f * o, 1.0f * o, 0.0f); c.PushTransform(); c.Translate((p_left + p_right) * 0.5f, half_height); c.Scale(p_right - p_left, height); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.PopTransform(); c.SetColor(0.1f * o, 0.1f * o, 0.2f * o, 0.4f * o); c.PushTransform(); c.Translate((p_right + 1.0f) * 0.5f, half_height); c.Scale(1.0f - p_right, height); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.PopTransform(); c.PopTransform(); c.Submit(); @@ -204,7 +207,7 @@ void ShieldNode::Draw(FrameDef* frame_def) { float o = (1.0f - hurt_) * 1.0f + hurt_ * (1.0f * r * r * r); o *= 0.3f; float cx, cy, cz; - g_graphics->camera()->get_position(&cx, &cy, &cz); + g_base->graphics->camera()->get_position(&cx, &cy, &cz); float col[4]; col[0] = color_[0] * o; col[1] = color_[1] * o; @@ -218,13 +221,13 @@ void ShieldNode::Draw(FrameDef* frame_def) { } { - ObjectComponent c(frame_def->beauty_pass()); + base::ObjectComponent c(frame_def->beauty_pass()); c.SetTransparent(true); c.SetPremultiplied(true); - c.SetLightShadow(LightShadowType::kNone); - c.SetReflection(ReflectionType::kSharp); + c.SetLightShadow(base::LightShadowType::kNone); + c.SetReflection(base::ReflectionType::kSharp); c.SetReflectionScale(0.34f * o, 0.34f * o, 0.34f * o); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kShield)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kShield)); c.SetColor(col[0], col[1], col[2], 0.13f * o); c.PushTransform(); Vector3f to_cam = @@ -245,26 +248,26 @@ void ShieldNode::Draw(FrameDef* frame_def) { * (0.97f + 0.05f * Utils::precalc_rand_2(rot_count_ % kPrecalcRandsCount)); c.Scale(r2, r2, r2); - c.DrawModel(g_assets->GetModel(SystemModelID::kShield), - kModelDrawFlagNoReflection); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kShield), + base::kMeshDrawFlagNoReflection); c.PopTransform(); c.Submit(); // Nifty intersection effects in fancy graphics mode. if (frame_def->has_depth_texture()) { - ShieldComponent c2(frame_def->overlay_3d_pass()); + base::ShieldComponent c2(frame_def->overlay_3d_pass()); c2.PushTransform(); c2.MultMatrix((om * m).m); c2.Scale(s, s, s); c2.Rotate(Utils::precalc_rand_1(rot_count_ % kPrecalcRandsCount) * 360, 0, 1, 0); c2.Scale(r2, r2, r2); - c2.DrawModel(g_assets->GetModel(SystemModelID::kShield)); + c2.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kShield)); c2.PopTransform(); c2.Submit(); } if (frame_def->has_depth_texture()) { - PostProcessComponent c2(frame_def->blit_pass()); + base::PostProcessComponent c2(frame_def->blit_pass()); c2.setNormalDistort(distort); c2.PushTransform(); c2.MultMatrix((om * m).m); @@ -273,7 +276,7 @@ void ShieldNode::Draw(FrameDef* frame_def) { 1, 0); float sc = r2 * 1.1f; c2.Scale(sc, sc, sc); - c2.DrawModel(g_assets->GetModel(SystemModelID::kShield)); + c2.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kShield)); c2.PopTransform(); c2.Submit(); } @@ -281,4 +284,4 @@ void ShieldNode::Draw(FrameDef* frame_def) { #endif // BA_HEADLESS_BUILD } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/shield_node.h b/src/ballistica/scene_v1/node/shield_node.h similarity index 75% rename from src/ballistica/scene/node/shield_node.h rename to src/ballistica/scene_v1/node/shield_node.h index 44c317da..11f8054d 100644 --- a/src/ballistica/scene/node/shield_node.h +++ b/src/ballistica/scene_v1/node/shield_node.h @@ -1,21 +1,21 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_SHIELD_NODE_H_ -#define BALLISTICA_SCENE_NODE_SHIELD_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_SHIELD_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_SHIELD_NODE_H_ #include -#include "ballistica/dynamics/bg/bg_dynamics_shadow.h" -#include "ballistica/scene/node/node.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_shadow.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { class ShieldNode : public Node { public: static auto InitType() -> NodeType*; explicit ShieldNode(Scene* scene); ~ShieldNode() override; - void Draw(FrameDef* frame_def) override; + void Draw(base::FrameDef* frame_def) override; void Step() override; auto position() const -> std::vector { return position_; } void SetPosition(const std::vector& vals); @@ -32,7 +32,7 @@ class ShieldNode : public Node { private: #if !BA_HEADLESS_BUILD - BGDynamicsShadow shadow_; + base::BGDynamicsShadow shadow_; #endif // BA_HEADLESS_BUILD bool always_show_health_bar_ = false; float hurt_smoothed_ = 1.0f; @@ -48,6 +48,6 @@ class ShieldNode : public Node { int rot_count_ = 0; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_SHIELD_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_SHIELD_NODE_H_ diff --git a/src/ballistica/scene/node/sound_node.cc b/src/ballistica/scene_v1/node/sound_node.cc similarity index 74% rename from src/ballistica/scene/node/sound_node.cc rename to src/ballistica/scene_v1/node/sound_node.cc index da6b7834..61a13726 100644 --- a/src/ballistica/scene/node/sound_node.cc +++ b/src/ballistica/scene_v1/node/sound_node.cc @@ -1,15 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/sound_node.h" +#include "ballistica/scene_v1/node/sound_node.h" -#include "ballistica/assets/component/sound.h" -#include "ballistica/audio/audio.h" -#include "ballistica/audio/audio_source.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/audio/audio_source.h" +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" -namespace ballistica { +namespace ballistica::scene_v1 { class SoundNodeType : public NodeType { public: @@ -42,7 +41,7 @@ SoundNode::SoundNode(Scene* scene) : Node(scene, node_type) {} SoundNode::~SoundNode() { if (playing_) { - g_audio->PushSourceStopSoundCall(play_id_); + g_base->audio->PushSourceStopSoundCall(play_id_); } } @@ -67,7 +66,7 @@ void SoundNode::SetVolume(float val) { // FIXME we could probably update this in an infrequent manner in case its // being driven by another attr. if (playing_) { - AudioSource* s = g_audio->SourceBeginExisting(play_id_, 106); + base::AudioSource* s = g_base->audio->SourceBeginExisting(play_id_, 106); if (s) { s->SetGain(volume_); s->End(); @@ -76,17 +75,22 @@ void SoundNode::SetVolume(float val) { } void SoundNode::SetLoop(bool val) { - if (loop_ == val) return; + if (loop_ == val) { + return; + } loop_ = val; // We don't actually update looping on a playing sound. - if (playing_) + if (playing_) { BA_LOG_ONCE(LogLevel::kError, "Can't set 'loop' attr on already-playing sound."); + } } -void SoundNode::SetSound(Sound* s) { - if (s == sound_.get()) return; +void SoundNode::SetSound(SceneSound* s) { + if (s == sound_.Get()) { + return; + } sound_ = s; // We'll start playing in our next Step; this allows @@ -95,18 +99,23 @@ void SoundNode::SetSound(Sound* s) { } void SoundNode::SetPositional(bool val) { - if (val == positional_) return; + if (val == positional_) { + return; + } positional_ = val; - if (playing_) + if (playing_) { BA_LOG_ONCE(LogLevel::kError, "Can't set 'positional' attr on already-playing sound"); + } } void SoundNode::SetMusic(bool val) { - if (val == music_) return; + if (val == music_) { + return; + } music_ = val; if (playing_) { - AudioSource* s = g_audio->SourceBeginExisting(play_id_, 104); + base::AudioSource* s = g_base->audio->SourceBeginExisting(play_id_, 104); if (s) { s->SetIsMusic(music_); s->End(); @@ -116,8 +125,8 @@ void SoundNode::SetMusic(bool val) { void SoundNode::Step() { // If we want to start playing, do so. - if (!playing_ && sound_.exists()) { - AudioSource* s = g_audio->SourceBeginNew(); + if (!playing_ && sound_.Exists()) { + base::AudioSource* s = g_base->audio->SourceBeginNew(); if (s) { assert(position_.size() == 3); s->SetPosition(position_[0], position_[1], position_[2]); @@ -131,9 +140,9 @@ void SoundNode::Step() { } } if (positional_ && position_dirty_ && playing_) { - millisecs_t t = GetRealTime(); + millisecs_t t = g_core->GetAppTimeMillisecs(); if (t - last_position_update_time_ > 100) { - AudioSource* s = g_audio->SourceBeginExisting(play_id_, 107); + base::AudioSource* s = g_base->audio->SourceBeginExisting(play_id_, 107); if (s) { s->SetPosition(position_[0], position_[1], position_[2]); s->End(); @@ -144,4 +153,4 @@ void SoundNode::Step() { } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/sound_node.h b/src/ballistica/scene_v1/node/sound_node.h similarity index 71% rename from src/ballistica/scene/node/sound_node.h rename to src/ballistica/scene_v1/node/sound_node.h index 7921415e..2ce642e7 100644 --- a/src/ballistica/scene/node/sound_node.h +++ b/src/ballistica/scene_v1/node/sound_node.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_SOUND_NODE_H_ -#define BALLISTICA_SCENE_NODE_SOUND_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_SOUND_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_SOUND_NODE_H_ #include -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { class SoundNode : public Node { public: @@ -25,11 +25,11 @@ class SoundNode : public Node { void SetMusic(bool val); auto loop() const -> bool { return loop_; } void SetLoop(bool val); - auto sound() const -> Sound* { return sound_.get(); } - void SetSound(Sound* s); + auto sound() const -> SceneSound* { return sound_.Get(); } + void SetSound(SceneSound* s); private: - Object::Ref sound_; + Object::Ref sound_; millisecs_t last_position_update_time_{}; std::vector position_{0.0f, 0.0f, 0.0f}; float volume_{1.0f}; @@ -41,6 +41,6 @@ class SoundNode : public Node { bool playing_{}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_SOUND_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_SOUND_NODE_H_ diff --git a/src/ballistica/scene/node/spaz_node.cc b/src/ballistica/scene_v1/node/spaz_node.cc similarity index 90% rename from src/ballistica/scene/node/spaz_node.cc rename to src/ballistica/scene_v1/node/spaz_node.cc index c887d049..cf8e2a29 100644 --- a/src/ballistica/scene/node/spaz_node.cc +++ b/src/ballistica/scene_v1/node/spaz_node.cc @@ -1,36 +1,38 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/spaz_node.h" +#include "ballistica/scene_v1/node/spaz_node.h" -#include "ballistica/assets/component/sound.h" -#include "ballistica/audio/audio.h" -#include "ballistica/audio/audio_source.h" -#include "ballistica/dynamics/bg/bg_dynamics_shadow.h" -#include "ballistica/dynamics/collision.h" -#include "ballistica/dynamics/dynamics.h" -#include "ballistica/dynamics/material/material_action.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/area_of_interest.h" -#include "ballistica/graphics/camera.h" -#include "ballistica/graphics/component/object_component.h" -#include "ballistica/graphics/component/post_process_component.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/text/text_graphics.h" -#include "ballistica/input/device/input_device.h" -#include "ballistica/python/python.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/ui/ui.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/audio/audio_source.h" +#include "ballistica/base/dynamics/bg/bg_dynamics_shadow.h" +#include "ballistica/base/graphics/component/object_component.h" +#include "ballistica/base/graphics/component/post_process_component.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/graphics/graphics_server.h" +#include "ballistica/base/graphics/support/area_of_interest.h" +#include "ballistica/base/graphics/support/camera.h" +#include "ballistica/base/graphics/text/text_graphics.h" +#include "ballistica/base/input/device/input_device.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/core/core.h" +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/dynamics/collision.h" +#include "ballistica/scene_v1/dynamics/dynamics.h" +#include "ballistica/scene_v1/dynamics/material/material_action.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/math/random.h" +#include "ballistica/shared/python/python.h" #include "ode/ode_collision_util.h" -namespace ballistica { +namespace ballistica::scene_v1 { // pull a random pointer from a ref-vector template auto GetRandomMedia(const std::vector >& list) -> T* { if (list.empty()) return nullptr; - return list[rand() % list.size()].get(); // NOLINT yes I know; rand bad. + return list[rand() % list.size()].Get(); // NOLINT yes I know; rand bad. } const float kSantaEyeScale = 0.9f; @@ -377,22 +379,22 @@ dxJoint::Vtable fixed_vtable_ = { class SpazNode::FullShadowSet : public Object { public: ~FullShadowSet() override = default; - BGDynamicsShadow torso_shadow_; - BGDynamicsShadow head_shadow_; - BGDynamicsShadow pelvis_shadow_; - BGDynamicsShadow lower_left_leg_shadow_; - BGDynamicsShadow lower_right_leg_shadow_; - BGDynamicsShadow upper_left_leg_shadow_; - BGDynamicsShadow upper_right_leg_shadow_; - BGDynamicsShadow lower_left_arm_shadow_; - BGDynamicsShadow lower_right_arm_shadow_; - BGDynamicsShadow upper_left_arm_shadow_; - BGDynamicsShadow upper_right_arm_shadow_; + base::BGDynamicsShadow torso_shadow_; + base::BGDynamicsShadow head_shadow_; + base::BGDynamicsShadow pelvis_shadow_; + base::BGDynamicsShadow lower_left_leg_shadow_; + base::BGDynamicsShadow lower_right_leg_shadow_; + base::BGDynamicsShadow upper_left_leg_shadow_; + base::BGDynamicsShadow upper_right_leg_shadow_; + base::BGDynamicsShadow lower_left_arm_shadow_; + base::BGDynamicsShadow lower_right_arm_shadow_; + base::BGDynamicsShadow upper_left_arm_shadow_; + base::BGDynamicsShadow upper_right_arm_shadow_; }; class SpazNode::SimpleShadowSet : public Object { public: - BGDynamicsShadow shadow_; + base::BGDynamicsShadow shadow_; }; #endif // !BA_HEADLESS_BUILD @@ -474,15 +476,15 @@ class SpazNodeType : public NodeType { BA_TEXTURE_ATTR(color_texture, color_texture, set_color_texture); BA_TEXTURE_ATTR(color_mask_texture, color_mask_texture, set_color_mask_texture); - BA_MODEL_ATTR(head_model, head_model, set_head_model); - BA_MODEL_ATTR(torso_model, torso_model, set_torso_model); - BA_MODEL_ATTR(pelvis_model, pelvis_model, set_pelvis_model); - BA_MODEL_ATTR(upper_arm_model, upper_arm_model, set_upper_arm_model); - BA_MODEL_ATTR(forearm_model, forearm_model, set_forearm_model); - BA_MODEL_ATTR(hand_model, hand_model, set_hand_model); - BA_MODEL_ATTR(upper_leg_model, upper_leg_model, set_upper_leg_model); - BA_MODEL_ATTR(lower_leg_model, lower_leg_model, set_lower_leg_model); - BA_MODEL_ATTR(toes_model, toes_model, set_toes_model); + BA_MESH_ATTR(head_mesh, head_mesh, set_head_mesh); + BA_MESH_ATTR(torso_mesh, torso_mesh, set_torso_mesh); + BA_MESH_ATTR(pelvis_mesh, pelvis_mesh, set_pelvis_mesh); + BA_MESH_ATTR(upper_arm_mesh, upper_arm_mesh, set_upper_arm_mesh); + BA_MESH_ATTR(forearm_mesh, forearm_mesh, set_forearm_mesh); + BA_MESH_ATTR(hand_mesh, hand_mesh, set_hand_mesh); + BA_MESH_ATTR(upper_leg_mesh, upper_leg_mesh, set_upper_leg_mesh); + BA_MESH_ATTR(lower_leg_mesh, lower_leg_mesh, set_lower_leg_mesh); + BA_MESH_ATTR(toes_mesh, toes_mesh, set_toes_mesh); BA_BOOL_ATTR(billboard_cross_out, billboard_cross_out, set_billboard_cross_out); BA_BOOL_ATTR(jump_pressed, jump_pressed, SetJumpPressed); @@ -560,15 +562,15 @@ class SpazNodeType : public NodeType { fall_sounds(this), color_texture(this), color_mask_texture(this), - head_model(this), - torso_model(this), - pelvis_model(this), - upper_arm_model(this), - forearm_model(this), - hand_model(this), - upper_leg_model(this), - lower_leg_model(this), - toes_model(this), + head_mesh(this), + torso_mesh(this), + pelvis_mesh(this), + upper_arm_mesh(this), + forearm_mesh(this), + hand_mesh(this), + upper_leg_mesh(this), + lower_leg_mesh(this), + toes_mesh(this), billboard_cross_out(this), jump_pressed(this), punch_pressed(this), @@ -724,7 +726,7 @@ SpazNode::SpazNode(Scene* scene) Stand(0, 0, 0, 0); // Attach head to torso. - neck_joint_ = CreateFixedJoint(body_head_.get(), body_torso_.get(), 1000, + neck_joint_ = CreateFixedJoint(body_head_.Get(), body_torso_.Get(), 1000, 1, // linear stiff/damp 20.0f, 0.3f); // angular stiff/damp @@ -734,7 +736,7 @@ SpazNode::SpazNode(Scene* scene) neck_joint_->anchor2[1] += 0.2f; // Attach torso to pelvis. - pelvis_joint_ = CreateFixedJoint(body_pelvis_.get(), body_torso_.get(), 0, + pelvis_joint_ = CreateFixedJoint(body_pelvis_.Get(), body_torso_.Get(), 0, 0, // lin stiff/damp 0, 0); // ang stiff/damp @@ -747,7 +749,7 @@ SpazNode::SpazNode(Scene* scene) // Attach upper right arm to torso. upper_right_arm_joint_ = - CreateFixedJoint(body_torso_.get(), upper_right_arm_body_.get(), 0, + CreateFixedJoint(body_torso_.Get(), upper_right_arm_body_.Get(), 0, 0, // linear stiff/damp 0, 0); // angular stiff/damp @@ -758,8 +760,8 @@ SpazNode::SpazNode(Scene* scene) upper_right_arm_joint_->anchor2[0] += 0.02f; // Attach lower right arm to upper right arm. - lower_right_arm_joint_ = CreateFixedJoint(upper_right_arm_body_.get(), - lower_right_arm_body_.get(), 0, + lower_right_arm_joint_ = CreateFixedJoint(upper_right_arm_body_.Get(), + lower_right_arm_body_.Get(), 0, 0, // linear stiff/damp 0, 0); // angular stiff/damp @@ -767,7 +769,7 @@ SpazNode::SpazNode(Scene* scene) // Attach upper left arm to torso. upper_left_arm_joint_ = CreateFixedJoint( - body_torso_.get(), upper_left_arm_body_.get(), 0, 0, // linear stiff/damp + body_torso_.Get(), upper_left_arm_body_.Get(), 0, 0, // linear stiff/damp 0, 0); // Angular stiff/damp. // Move anchor to top of arm. @@ -777,8 +779,8 @@ SpazNode::SpazNode(Scene* scene) upper_left_arm_joint_->anchor2[0] += -0.02f; // Attach lower arm to upper arm. - lower_left_arm_joint_ = CreateFixedJoint(upper_left_arm_body_.get(), - lower_left_arm_body_.get(), 0, + lower_left_arm_joint_ = CreateFixedJoint(upper_left_arm_body_.Get(), + lower_left_arm_body_.Get(), 0, 0, // linear stiff/damp 0, 0); // angular stiff/damp @@ -786,15 +788,15 @@ SpazNode::SpazNode(Scene* scene) // Attach upper right leg to leg-mass. upper_right_leg_joint_ = - CreateFixedJoint(body_pelvis_.get(), upper_right_leg_body_.get(), 0, + CreateFixedJoint(body_pelvis_.Get(), upper_right_leg_body_.Get(), 0, 0, // linear stiff/damp 0, 0); // angular stiff/damp upper_right_leg_joint_->anchor2[2] = -0.05f; // Attach lower right leg to upper right leg. - lower_right_leg_joint_ = CreateFixedJoint(upper_right_leg_body_.get(), - lower_right_leg_body_.get(), 0, + lower_right_leg_joint_ = CreateFixedJoint(upper_right_leg_body_.Get(), + lower_right_leg_body_.Get(), 0, 0, // linear stiff/damp 0, 0); // angular stiff/damp @@ -802,7 +804,7 @@ SpazNode::SpazNode(Scene* scene) // Attach bottom of lower leg to pelvis. right_leg_ik_joint_ = - CreateFixedJoint(body_pelvis_.get(), lower_right_leg_body_.get(), 0.3f, + CreateFixedJoint(body_pelvis_.Get(), lower_right_leg_body_.Get(), 0.3f, 0.001f, // linear stiff/damp 0, 0); // angular stiff/damp dQFromAxisAndAngle(right_leg_ik_joint_->qrel, 1, 0, 0, 1.0f); @@ -816,7 +818,7 @@ SpazNode::SpazNode(Scene* scene) // Attach toes to lower right foot. right_toes_joint_ = - CreateFixedJoint(lower_right_leg_body_.get(), right_toes_body_.get(), 0, + CreateFixedJoint(lower_right_leg_body_.Get(), right_toes_body_.Get(), 0, 0, // linear stiff/damp 0, 0); // angular stiff/damp @@ -826,7 +828,7 @@ SpazNode::SpazNode(Scene* scene) // And an anchor off to the side to make it hinge-like. right_toes_joint_2_ = nullptr; right_toes_joint_2_ = - CreateFixedJoint(lower_right_leg_body_.get(), right_toes_body_.get(), 0, + CreateFixedJoint(lower_right_leg_body_.Get(), right_toes_body_.Get(), 0, 0, // linear stiff/damp 0, 0); // angular stiff/damp @@ -838,15 +840,15 @@ SpazNode::SpazNode(Scene* scene) // Attach upper left leg to leg-mass. upper_left_leg_joint_ = - CreateFixedJoint(body_pelvis_.get(), upper_left_leg_body_.get(), 0, + CreateFixedJoint(body_pelvis_.Get(), upper_left_leg_body_.Get(), 0, 0, // linear stiff/damp 0, 0); // angular stiff/damp upper_left_leg_joint_->anchor2[2] = -0.05f; // Attach lower left leg to upper left leg. - lower_left_leg_joint_ = CreateFixedJoint(upper_left_leg_body_.get(), - lower_left_leg_body_.get(), 0, + lower_left_leg_joint_ = CreateFixedJoint(upper_left_leg_body_.Get(), + lower_left_leg_body_.Get(), 0, 0, // linear stiff/damp 0, 0); // angular stiff/damp @@ -854,7 +856,7 @@ SpazNode::SpazNode(Scene* scene) // Attach bottom of lower leg to pelvis. left_leg_ik_joint_ = - CreateFixedJoint(body_pelvis_.get(), lower_left_leg_body_.get(), 0.3f, + CreateFixedJoint(body_pelvis_.Get(), lower_left_leg_body_.Get(), 0.3f, 0.001f, // linear stiff/damp 0, 0); // angular stiff/damp @@ -869,7 +871,7 @@ SpazNode::SpazNode(Scene* scene) // Attach toes to lower left foot. left_toes_joint_ = - CreateFixedJoint(lower_left_leg_body_.get(), left_toes_body_.get(), 0, + CreateFixedJoint(lower_left_leg_body_.Get(), left_toes_body_.Get(), 0, 0, // linear stiff/damp 0, 0); // angular stiff/damp @@ -879,7 +881,7 @@ SpazNode::SpazNode(Scene* scene) // And an anchor off to the side to make it hinge-like. left_toes_joint_2_ = nullptr; left_toes_joint_2_ = - CreateFixedJoint(lower_left_leg_body_.get(), left_toes_body_.get(), 0, + CreateFixedJoint(lower_left_leg_body_.Get(), left_toes_body_.Get(), 0, 0, // linear stiff/damp 0, 0); // angular stiff/damp @@ -890,7 +892,7 @@ SpazNode::SpazNode(Scene* scene) // Attach end of right arm to torso. right_arm_ik_joint_ = - CreateFixedJoint(body_torso_.get(), lower_right_arm_body_.get(), 0.0f, + CreateFixedJoint(body_torso_.Get(), lower_right_arm_body_.Get(), 0.0f, 0.0f, // linear stiff/damp 0, 0, // angular stiff/damp -0.2f, -0.2f, 0.1f, // anchor1 @@ -898,7 +900,7 @@ SpazNode::SpazNode(Scene* scene) false); left_arm_ik_joint_ = - CreateFixedJoint(body_torso_.get(), lower_left_arm_body_.get(), 0.0f, + CreateFixedJoint(body_torso_.Get(), lower_left_arm_body_.Get(), 0.0f, 0.0f, // linear stiff/damp 0, 0, // angular stiff/damp 0.2f, -0.2f, 0.1f, // anchor1 @@ -906,14 +908,14 @@ SpazNode::SpazNode(Scene* scene) false); // Roller ball joint. - roller_ball_joint_ = CreateFixedJoint(body_torso_.get(), body_roller_.get(), + roller_ball_joint_ = CreateFixedJoint(body_torso_.Get(), body_roller_.Get(), kRollerBallLinearStiffness, kRollerBallLinearDamping, 0, 0); base_pelvis_roller_anchor_offset_ = roller_ball_joint_->anchor1[1]; // Stand joint on our torso. stand_joint_ = - CreateFixedJoint(body_torso_.get(), stand_body_.get(), 100, 1, 200, 10); + CreateFixedJoint(body_torso_.Get(), stand_body_.Get(), 100, 1, 200, 10); // Roller motor. a_motor_roller_ = dJointCreateAMotor(scene->dynamics()->ode_world(), nullptr); @@ -948,7 +950,7 @@ SpazNode::SpazNode(Scene* scene) UpdateJoints(); // FIXME should do this on draw - UpdateForGraphicsQuality(g_graphics_server->quality()); + UpdateForGraphicsQuality(g_base->graphics_server->quality()); // we want to have an area of interest by default.. SetIsAreaOfInterest(true); @@ -1062,10 +1064,10 @@ void SpazNode::SetPunchPressed(bool val) { punch_right_ = a_vel_y_smoothed_ > 0.0f; } last_punch_time_ = scene()->time(); - if (Sound* sound = GetRandomMedia(attack_sounds_)) { - if (AudioSource* source = g_audio->SourceBeginNew()) { + if (SceneSound* sound = GetRandomMedia(attack_sounds_)) { + if (auto* source = g_base->audio->SourceBeginNew()) { const dReal* p_head = dGeomGetPosition(body_head_->geom()); - g_audio->PushSourceStopSoundCall(voice_play_id_); + g_base->audio->PushSourceStopSoundCall(voice_play_id_); source->SetPosition(p_head[0], p_head[1], p_head[2]); voice_play_id_ = source->Play(sound->GetSoundData()); source->End(); @@ -1085,10 +1087,10 @@ void SpazNode::SetJumpPressed(bool val) { jump_pressed_ = val; if (jump_pressed_) { if (!can_fly_ && !knockout_ && !frozen_) { - if (Sound* sound = GetRandomMedia(jump_sounds_)) { - if (AudioSource* source = g_audio->SourceBeginNew()) { + if (SceneSound* sound = GetRandomMedia(jump_sounds_)) { + if (auto* source = g_base->audio->SourceBeginNew()) { const dReal* p_top = dGeomGetPosition(body_head_->geom()); - g_audio->PushSourceStopSoundCall(voice_play_id_); + g_base->audio->PushSourceStopSoundCall(voice_play_id_); source->SetPosition(p_top[0], p_top[1], p_top[2]); voice_play_id_ = source->Play(sound->GetSoundData()); source->End(); @@ -1459,7 +1461,7 @@ SpazNode::~SpazNode() { DropHeldObject(); if (area_of_interest_) { - g_graphics->camera()->DeleteAreaOfInterest(area_of_interest_); + g_base->graphics->camera()->DeleteAreaOfInterest(area_of_interest_); area_of_interest_ = nullptr; } @@ -1497,10 +1499,10 @@ SpazNode::~SpazNode() { // stop any sounds that may be looping.. if (tick_play_id_ != 0xFFFFFFFF) { - g_audio->PushSourceStopSoundCall(tick_play_id_); + g_base->audio->PushSourceStopSoundCall(tick_play_id_); } if (voice_play_id_ != 0xFFFFFFFF) { - g_audio->PushSourceStopSoundCall(voice_play_id_); + g_base->audio->PushSourceStopSoundCall(voice_play_id_); } } @@ -1547,10 +1549,10 @@ void SpazNode::Throw(bool with_bomb_button) { throw_start_ = scene()->time(); have_thrown_ = true; - if (Sound* sound = GetRandomMedia(attack_sounds_)) { - if (AudioSource* s = g_audio->SourceBeginNew()) { + if (SceneSound* sound = GetRandomMedia(attack_sounds_)) { + if (auto* s = g_base->audio->SourceBeginNew()) { const dReal* p = dGeomGetPosition(body_head_->geom()); - g_audio->PushSourceStopSoundCall(voice_play_id_); + g_base->audio->PushSourceStopSoundCall(voice_play_id_); s->SetPosition(p[0], p[1], p[2]); voice_play_id_ = s->Play(sound->GetSoundData()); s->End(); @@ -1613,10 +1615,10 @@ void SpazNode::HandleMessage(const char* data_in) { if (knockout_ || frozen_) { break; } - if (Sound* sound = GetRandomMedia(attack_sounds_)) { - if (AudioSource* source = g_audio->SourceBeginNew()) { + if (SceneSound* sound = GetRandomMedia(attack_sounds_)) { + if (auto* source = g_base->audio->SourceBeginNew()) { const dReal* p_top = dGeomGetPosition(body_head_->geom()); - g_audio->PushSourceStopSoundCall(voice_play_id_); + g_base->audio->PushSourceStopSoundCall(voice_play_id_); source->SetPosition(p_top[0], p_top[1], p_top[2]); voice_play_id_ = source->Play(sound->GetSoundData()); source->End(); @@ -1628,10 +1630,10 @@ void SpazNode::HandleMessage(const char* data_in) { if (knockout_ || frozen_) { break; } - if (Sound* sound = GetRandomMedia(jump_sounds_)) { - if (AudioSource* s = g_audio->SourceBeginNew()) { + if (SceneSound* sound = GetRandomMedia(jump_sounds_)) { + if (auto* s = g_base->audio->SourceBeginNew()) { const dReal* p_top = dGeomGetPosition(body_head_->geom()); - g_audio->PushSourceStopSoundCall(voice_play_id_); + g_base->audio->PushSourceStopSoundCall(voice_play_id_); s->SetPosition(p_top[0], p_top[1], p_top[2]); voice_play_id_ = s->Play(sound->GetSoundData()); s->End(); @@ -1822,24 +1824,24 @@ void SpazNode::DoFlyPress() { // keep from doing too many sparkles.. static millisecs_t last_sparkle_time = 0; - millisecs_t t = GetRealTime(); + millisecs_t t = g_core->GetAppTimeMillisecs(); if (t - last_sparkle_time > 200) { last_sparkle_time = t; - AudioSource* s = g_audio->SourceBeginNew(); + auto* s = g_base->audio->SourceBeginNew(); if (s) { const dReal* p_torso = dGeomGetPosition(body_torso_->geom()); s->SetPosition(p_torso[0], p_torso[1], p_torso[2]); s->SetGain(0.3f); - SystemSoundID s_id; + base::SysSoundID s_id; int r = rand() % 100; // NOLINT if (r < 33) { - s_id = SystemSoundID::kSparkle; + s_id = base::SysSoundID::kSparkle; } else if (r < 66) { - s_id = SystemSoundID::kSparkle2; + s_id = base::SysSoundID::kSparkle2; } else { - s_id = SystemSoundID::kSparkle3; + s_id = base::SysSoundID::kSparkle3; } - s->Play(g_assets->GetSound(s_id)); + s->Play(g_base->assets->SysSound(s_id)); s->End(); } } @@ -1874,7 +1876,7 @@ void SpazNode::Step() { nullptr}; for (Object::Ref** body = bodies; *body != nullptr; body++) { - if (RigidBody* bodyptr = (**body).get()) { + if (RigidBody* bodyptr = (**body).Get()) { bodyptr->UpdateBlending(); } } @@ -1890,7 +1892,7 @@ void SpazNode::Step() { // if we're associated with a player, let the game know where that player is // FIXME: this should simply be an attr connection established on the // python layer... - if (source_player_.exists()) { + if (source_player_.Exists()) { source_player_->SetPosition(Vector3f(p_torso)); } @@ -2099,7 +2101,7 @@ void SpazNode::Step() { // Update shadows. #if !BA_HEADLESS_BUILD - FullShadowSet* full_shadows = full_shadow_set_.get(); + FullShadowSet* full_shadows = full_shadow_set_.Get(); if (full_shadows) { full_shadows->torso_shadow_.SetPosition( Vector3f(dBodyGetPosition(body_torso_->body()))); @@ -2124,7 +2126,7 @@ void SpazNode::Step() { full_shadows->upper_left_arm_shadow_.SetPosition( Vector3f(dBodyGetPosition(upper_left_arm_body_->body()))); } else { - SimpleShadowSet* simple_shadows = simple_shadow_set_.get(); + SimpleShadowSet* simple_shadows = simple_shadow_set_.Get(); assert(simple_shadows); simple_shadows->shadow_.SetPosition( Vector3f(dBodyGetPosition(body_pelvis_->body()))); @@ -2303,7 +2305,7 @@ void SpazNode::Step() { &hair_ponytail_top_body_, &hair_ponytail_bottom_body_, nullptr}; float drag = 0.94f; for (Object::Ref** body = bodies2; *body != nullptr; body++) { - if ((**body).exists()) { + if ((**body).Exists()) { dBodyID b = (**body)->body(); const dReal* lVel = dBodyGetLinearVel(b); dBodySetLinearVel(b, lVel[0] * drag, lVel[1] * drag, lVel[2] * drag); @@ -2602,7 +2604,7 @@ void SpazNode::Step() { z *= 1.4f * run_gas_ + (1.0f - run_gas_) * 1.0f; dBodyGetRelPointPos(stand_body_->body(), step_separation, y, z, p_world); - assert(body_pelvis_.exists()); + assert(body_pelvis_.Exists()); dBodyGetPosRelPoint(body_pelvis_->body(), p_world[0], p_world[1], p_world[2], p_pelvis); left_leg_ik_joint_->anchor1[0] = p_pelvis[0]; @@ -2623,7 +2625,7 @@ void SpazNode::Step() { z *= 1.3f * run_gas_ + (1.0f - run_gas_) * 1.0f; dBodyGetRelPointPos(stand_body_->body(), -step_separation, y, z, p_world); - assert(body_pelvis_.exists()); + assert(body_pelvis_.Exists()); dBodyGetPosRelPoint(body_pelvis_->body(), p_world[0], p_world[1], p_world[2], p_pelvis); right_leg_ik_joint_->anchor1[0] = p_pelvis[0]; @@ -2746,8 +2748,8 @@ void SpazNode::Step() { left_arm_ik_joint_->angularDamping = 0; } else { bool haveHeldThing = false; - if (holding_something_ && hold_node_.exists()) { - Node* a = hold_node_.get(); + if (holding_something_ && hold_node_.Exists()) { + Node* a = hold_node_.Get(); RigidBody* b = a->GetRigidBody(hold_body_); if (b) { haveHeldThing = true; @@ -2769,7 +2771,7 @@ void SpazNode::Step() { dBodyGetRelPointPos(heldBody, hold_hand_offset_right_[0], hold_hand_offset_right_[1], hold_hand_offset_right_[2], p_world); - assert(body_torso_.exists()); + assert(body_torso_.Exists()); dBodyGetPosRelPoint(body_torso_->body(), p_world[0], p_world[1], p_world[2], p_torso2); jf->anchor1[0] = p_torso2[0]; @@ -2779,7 +2781,7 @@ void SpazNode::Step() { dBodyGetRelPointPos(heldBody, hold_hand_offset_left_[0], hold_hand_offset_left_[1], hold_hand_offset_left_[2], p_world); - assert(body_torso_.exists()); + assert(body_torso_.Exists()); dBodyGetPosRelPoint(body_torso_->body(), p_world[0], p_world[1], p_world[2], p_torso2); jf->anchor1[0] = p_torso2[0]; @@ -2842,7 +2844,7 @@ void SpazNode::Step() { p_world[1] += 0.13f; // Now translate back to torso space for setting our anchor. - assert(body_torso_.exists()); + assert(body_torso_.Exists()); dBodyGetPosRelPoint(body_torso_->body(), p_world[0], p_world[1], p_world[2], p_torso2); @@ -2925,8 +2927,10 @@ void SpazNode::Step() { right_arm_ik_joint_->linearStiffness = 30.0f; right_arm_ik_joint_->linearDamping = 0.08f; - float v1 = sinf(scenetime * 0.05f) * 0.12f; - float v2 = cosf(scenetime * 0.04f) * 0.12f; + float v1 = + sinf(static_cast(scenetime) * 0.05f) * 0.12f; + float v2 = + cosf(static_cast(scenetime) * 0.04f) * 0.12f; JointFixedEF* jf; jf = left_arm_ik_joint_; @@ -2942,8 +2946,8 @@ void SpazNode::Step() { && (scenetime < celebrate_until_time_left_ || scenetime < celebrate_until_time_right_)) { // Celebrating - hold arms in air. - float v1 = sinf(scenetime * 0.04f) * 0.1f; - float v2 = cosf(scenetime * 0.03f) * 0.1f; + float v1 = sinf(static_cast(scenetime) * 0.04f) * 0.1f; + float v2 = cosf(static_cast(scenetime) * 0.03f) * 0.1f; JointFixedEF* jf; if (scenetime < celebrate_until_time_left_) { left_arm_ik_joint_->linearStiffness = 30.0f; @@ -3092,19 +3096,19 @@ void SpazNode::Step() { b = body_torso_->body(); p = dBodyGetPosition(b); - dBodySetPosition(b, p[0], p[1], kHappyThoughtsZPlane); + dBodySetPosition(b, p[0], p[1], base::kHappyThoughtsZPlane); v = dBodyGetLinearVel(b); dBodySetLinearVel(b, v[0], v[1], 0.0f); b = body_pelvis_->body(); p = dBodyGetPosition(b); - dBodySetPosition(b, p[0], p[1], kHappyThoughtsZPlane); + dBodySetPosition(b, p[0], p[1], base::kHappyThoughtsZPlane); v = dBodyGetLinearVel(b); dBodySetLinearVel(b, v[0], v[1], 0.0f); b = body_head_->body(); p = dBodyGetPosition(b); - dBodySetPosition(b, p[0], p[1], kHappyThoughtsZPlane); + dBodySetPosition(b, p[0], p[1], base::kHappyThoughtsZPlane); v = dBodyGetLinearVel(b); dBodySetLinearVel(b, v[0], v[1], 0.0f); } @@ -3320,7 +3324,7 @@ void SpazNode::Step() { // also add some force to what we're holding so popping out a bomb doesnt // send us spiraling down to death if (holding_something_) { - Node* a = hold_node_.get(); + Node* a = hold_node_.Get(); if (a) { float scale = 0.2f; RigidBody* b = a->GetRigidBody(hold_body_); @@ -3715,7 +3719,7 @@ void SpazNode::Step() { } if (punch_ > 0) { - if (!body_punch_.exists() && since_last_punch > 80 && !knockout_) { + if (!body_punch_.Exists() && since_last_punch > 80 && !knockout_) { body_punch_ = Object::New( kPunchBodyID, &punch_part_, RigidBody::Type::kGeomOnly, RigidBody::Shape::kSphere, RigidBody::kCollideRegion, @@ -3723,7 +3727,7 @@ void SpazNode::Step() { body_punch_->SetDimensions(0.25f); } - if (body_punch_.exists()) { + if (body_punch_.Exists()) { // Move the punch body to the end of our punching arm. dBodyID fist_body = punch_right_ ? lower_right_arm_body_->body() : lower_left_arm_body_->body(); @@ -3738,7 +3742,7 @@ void SpazNode::Step() { } } else { - if (body_punch_.exists()) { + if (body_punch_.Exists()) { body_punch_.Clear(); } } @@ -3758,10 +3762,10 @@ void SpazNode::Step() { if (scene()->time() - last_fall_time_ > 1000) { // If we're not still screaming, start one up. if (!(voice_play_id_ == fall_play_id_ - && g_audio->IsSoundPlaying(fall_play_id_))) { - if (Sound* sound = GetRandomMedia(fall_sounds_)) { - if (AudioSource* source = g_audio->SourceBeginNew()) { - g_audio->PushSourceStopSoundCall(voice_play_id_); + && g_base->audio->IsSoundPlaying(fall_play_id_))) { + if (SceneSound* sound = GetRandomMedia(fall_sounds_)) { + if (auto* source = g_base->audio->SourceBeginNew()) { + g_base->audio->PushSourceStopSoundCall(voice_play_id_); source->SetPosition(p_head[0], p_head[1], p_head[2]); voice_play_id_ = source->Play(sound->GetSoundData()); fall_play_id_ = voice_play_id_; @@ -3779,10 +3783,10 @@ void SpazNode::Step() { if ((footing_ && !force_scream_) || (force_scream_ && scene()->time() - last_force_scream_time_ > 2000)) { - g_audio->PushSourceStopSoundCall(voice_play_id_); + g_base->audio->PushSourceStopSoundCall(voice_play_id_); voice_play_id_ = 0xFFFFFFFF; } else { - AudioSource* s = g_audio->SourceBeginExisting(fall_play_id_, 108); + auto* s = g_base->audio->SourceBeginExisting(fall_play_id_, 108); if (s) { s->SetPosition(p_head[0], p_head[1], p_head[2]); s->End(); @@ -3792,7 +3796,7 @@ void SpazNode::Step() { // Update ticking. if (tick_play_id_ != 0xFFFFFFFF) { - AudioSource* s = g_audio->SourceBeginExisting(tick_play_id_, 109); + auto* s = g_base->audio->SourceBeginExisting(tick_play_id_, 109); if (s) { s->SetPosition(p_head[0], p_head[1], p_head[2]); s->End(); @@ -3803,7 +3807,7 @@ void SpazNode::Step() { // ( we need to check have_thrown_ because otherwise we'll always think // we're throwing at game-time 0 since throw_start_ inits to that.) if (have_thrown_ && scene()->time() - throw_start_ < 50) { - Node* a = hold_node_.get(); + Node* a = hold_node_.Get(); if (a) { RigidBody* b = a->GetRigidBody(hold_body_); if (b) { @@ -3881,11 +3885,11 @@ void SpazNode::Step() { } else { // If we're no longer holding something and our throw is over, clear any ref // we might have. - if (!holding_something_ && hold_node_.exists()) hold_node_.Clear(); + if (!holding_something_ && hold_node_.Exists()) hold_node_.Clear(); } if (pickup_ == kPickupCooldown - 4) { - if (!body_pickup_.exists()) { + if (!body_pickup_.Exists()) { body_pickup_ = Object::New( kPickupBodyID, &pickup_part_, RigidBody::Type::kGeomOnly, RigidBody::Shape::kSphere, RigidBody::kCollideRegion, @@ -3893,12 +3897,12 @@ void SpazNode::Step() { body_pickup_->SetDimensions(0.7f); } } else { - if (body_pickup_.exists()) { + if (body_pickup_.Exists()) { body_pickup_.Clear(); } } - if (body_pickup_.exists()) { + if (body_pickup_.Exists()) { // A unit vector forward. dVector3 f; float z = 0.3f; @@ -3934,14 +3938,14 @@ void SpazNode::Step() { #if !BA_HEADLESS_BUILD if (fly_power_ > 20.0f && scene()->stepnum() % 3 == 1) { for (int i = 0; i < 1; i++) { - BGDynamicsEmission e; - e.emit_type = BGDynamicsEmitType::kFairyDust; + base::BGDynamicsEmission e; + e.emit_type = base::BGDynamicsEmitType::kFairyDust; e.position = Vector3f(dGeomGetPosition(body_torso_->geom())); e.velocity = Vector3f(dBodyGetLinearVel(body_torso_->body())); e.count = 1; e.scale = 1.0f; e.spread = 1.0f; - g_bg_dynamics->Emit(e); + g_base->bg_dynamics->Emit(e); } } #endif // !BA_HEADLESS_BUILD @@ -3972,60 +3976,65 @@ void SpazNode::Step() { } // NOLINT (yeah i know, this is too long) #if !BA_HEADLESS_BUILD -static void DrawShadow(const BGDynamicsShadow& shadow, float radius, +static void DrawShadow(const base::BGDynamicsShadow& shadow, float radius, float density, const float* shadow_color) { float s_scale, s_density; shadow.GetValues(&s_scale, &s_density); float d = s_density * density; - g_graphics->DrawBlotch(shadow.GetPosition(), radius * s_scale * 4.0f, - (0.08f + 0.04f * shadow_color[0]) * d, - (0.07f + 0.04f * shadow_color[1]) * d, - (0.065f + 0.04f * shadow_color[2]) * d, 0.32f * d); + g_base->graphics->DrawBlotch(shadow.GetPosition(), radius * s_scale * 4.0f, + (0.08f + 0.04f * shadow_color[0]) * d, + (0.07f + 0.04f * shadow_color[1]) * d, + (0.065f + 0.04f * shadow_color[2]) * d, + 0.32f * d); } -static void DrawBrightSpot(const BGDynamicsShadow& shadow, float radius, +static void DrawBrightSpot(const base::BGDynamicsShadow& shadow, float radius, float density, const float* shadow_color) { float s_scale, s_density; shadow.GetValues(&s_scale, &s_density); float d = s_density * density * 0.3f; - g_graphics->DrawBlotch(shadow.GetPosition(), radius * s_scale * 4.0f, - shadow_color[0] * d, shadow_color[1] * d, - shadow_color[2] * d, 0.0f); + g_base->graphics->DrawBlotch(shadow.GetPosition(), radius * s_scale * 4.0f, + shadow_color[0] * d, shadow_color[1] * d, + shadow_color[2] * d, 0.0f); } #endif // !BA_HEADLESS_BUILD -void SpazNode::DrawEyeBalls(RenderComponent* c, ObjectComponent* oc, +void SpazNode::DrawEyeBalls(base::RenderComponent* c, base::ObjectComponent* oc, bool shading, float death_fade, float death_scale, float* add_color) { // Eyeballs. if (blink_smooth_ < 0.9f) { if (shading) { - oc->SetLightShadow(LightShadowType::kObject); - oc->SetTexture(g_assets->GetTexture(SystemTextureID::kEye)); + oc->SetLightShadow(base::LightShadowType::kObject); + oc->SetTexture(g_base->assets->SysTexture(base::SysTextureID::kEye)); oc->SetColorizeColor(eye_color_red_, eye_color_green_, eye_color_blue_); - oc->SetColorizeTexture(g_assets->GetTexture(SystemTextureID::kEyeTint)); - oc->SetReflection(ReflectionType::kSharpest); + oc->SetColorizeTexture( + g_base->assets->SysTexture(base::SysTextureID::kEyeTint)); + oc->SetReflection(base::ReflectionType::kSharpest); oc->SetReflectionScale(3, 3, 3); oc->SetAddColor(add_color[0], add_color[1], add_color[2]); oc->SetColor(eye_ball_color_red_, eye_ball_color_green_, eye_ball_color_blue_); } c->PushTransform(); - c->TransformToBody(*body_head_); - if (eye_scale_ != 1.0f) c->Scale(eye_scale_, eye_scale_, eye_scale_); + body_head_->ApplyToRenderComponent(c); + if (eye_scale_ != 1.0f) { + c->Scale(eye_scale_, eye_scale_, eye_scale_); + } c->PushTransform(); c->Translate(eye_offset_x_, eye_offset_y_, eye_offset_z_); c->Rotate(-10 + eyes_ud_smooth_, 1, 0, 0); c->Rotate(eyes_lr_smooth_, 0, 1, 0); c->Scale(0.09f, 0.09f, 0.09f); - if (death_scale != 1.0f) c->Scale(death_scale, death_scale, death_scale); - + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, death_scale); + } if (!frosty_ && !eyeless_) { - c->DrawModel(g_assets->GetModel(SystemModelID::kEyeBall)); + c->DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kEyeBall)); if (shading) { oc->SetReflectionScale(2, 2, 2); } if (death_scale != 1.0f) c->Scale(death_scale, death_scale, death_scale); - c->DrawModel(g_assets->GetModel(SystemModelID::kEyeBallIris)); + c->DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kEyeBallIris)); } c->PopTransform(); @@ -4039,22 +4048,26 @@ void SpazNode::DrawEyeBalls(RenderComponent* c, ObjectComponent* oc, c->Rotate(-10 + eyes_ud_smooth_, 1, 0, 0); c->Rotate(eyes_lr_smooth_, 0, 1, 0); c->Scale(0.09f, 0.09f, 0.09f); - if (death_scale != 1.0f) c->Scale(death_scale, death_scale, death_scale); - c->DrawModel(g_assets->GetModel(SystemModelID::kEyeBall)); - if (death_scale != 1.0f) c->Scale(death_scale, death_scale, death_scale); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, death_scale); + } + c->DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kEyeBall)); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, death_scale); + } if (shading) { oc->SetReflectionScale(2, 2, 2); } - c->DrawModel(g_assets->GetModel(SystemModelID::kEyeBallIris)); + c->DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kEyeBallIris)); c->PopTransform(); } c->PopTransform(); } } -void SpazNode::SetupEyeLidShading(ObjectComponent* c, float death_fade, +void SpazNode::SetupEyeLidShading(base::ObjectComponent* c, float death_fade, float* add_color) { - c->SetTexture(g_assets->GetTexture(SystemTextureID::kEye)); + c->SetTexture(g_base->assets->SysTexture(base::SysTextureID::kEye)); c->SetColorizeTexture(nullptr); float r, g, b; r = eye_lid_color_red_; @@ -4069,16 +4082,18 @@ void SpazNode::SetupEyeLidShading(ObjectComponent* c, float death_fade, } c->SetColor(r, g, b); c->SetAddColor(add_color[0], add_color[1], add_color[2]); - c->SetReflection(ReflectionType::kChar); + c->SetReflection(base::ReflectionType::kChar); c->SetReflectionScale(0.05f, 0.05f, 0.05f); } -void SpazNode::DrawEyeLids(RenderComponent* c, float death_fade, +void SpazNode::DrawEyeLids(base::RenderComponent* c, float death_fade, float death_scale) { - if (!has_eyelids_ && blink_smooth_ < 0.1f) return; + if (!has_eyelids_ && blink_smooth_ < 0.1f) { + return; + } c->PushTransform(); - c->TransformToBody(*body_head_); + body_head_->ApplyToRenderComponent(c); if (eye_scale_ != 1.0f) { c->Scale(eye_scale_, eye_scale_, eye_scale_); } @@ -4097,16 +4112,17 @@ void SpazNode::DrawEyeLids(RenderComponent* c, float death_fade, } if (!frosty_ && !eyeless_) { - c->DrawModel(g_assets->GetModel(SystemModelID::kEyeLid)); + c->DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kEyeLid)); } c->PopTransform(); // Left eyelid. c->FlipCullFace(); c->PushTransform(); - c->TransformToBody(*body_head_); - if (eye_scale_ != 1.0f) c->Scale(eye_scale_, eye_scale_, eye_scale_); - + body_head_->ApplyToRenderComponent(c); + if (eye_scale_ != 1.0f) { + c->Scale(eye_scale_, eye_scale_, eye_scale_); + } c->Translate(-eye_offset_x_, eye_offset_y_, eye_offset_z_); a = eyelid_right_ud_smooth_ + 0.5f * eyes_ud_smooth_; if (blink_smooth_ > 0.001f) { @@ -4115,23 +4131,30 @@ void SpazNode::DrawEyeLids(RenderComponent* c, float death_fade, c->Rotate(-eye_lid_angle_, 0, 0, 1); c->Rotate(a, 1, 0, 0); c->Scale(-0.09f, 0.09f, 0.09f); - if (death_scale != 1.0f) c->Scale(death_scale, death_scale, death_scale); - if (!pirate_ && !frosty_ && !eyeless_) - c->DrawModel(g_assets->GetModel(SystemModelID::kEyeLid)); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, death_scale); + } + if (!pirate_ && !frosty_ && !eyeless_) { + c->DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kEyeLid)); + } c->PopTransform(); c->FlipCullFace(); // back to normal } -void SpazNode::DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, - float death_scale, float* add_color) { +void SpazNode::DrawBodyParts(base::ObjectComponent* c, bool shading, + float death_fade, float death_scale, + float* add_color) { // Set up shading. if (shading) { - c->SetTexture(color_texture_); - c->SetColorizeTexture(color_mask_texture_); + c->SetTexture(color_texture_.Exists() ? color_texture_->texture_data() + : nullptr); + c->SetColorizeTexture(color_mask_texture_.Exists() + ? color_mask_texture_->texture_data() + : nullptr); c->SetColorizeColor(color_[0], color_[1], color_[2]); assert(highlight_.size() == 3); c->SetColorizeColor2(highlight_[0], highlight_[1], highlight_[2]); - c->SetLightShadow(LightShadowType::kObject); + c->SetLightShadow(base::LightShadowType::kObject); c->SetAddColor(add_color[0], add_color[1], add_color[2]); // Tint blueish when frozen. @@ -4146,15 +4169,15 @@ void SpazNode::DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, } if (frozen_) { - c->SetReflection(ReflectionType::kSharper); + c->SetReflection(base::ReflectionType::kSharper); c->SetReflectionScale(1.5f, 1.5f, 1.5f); } else { if (dead_) { // Go mostly matte when dead. - c->SetReflection(ReflectionType::kSoft); + c->SetReflection(base::ReflectionType::kSoft); c->SetReflectionScale(0.03f, 0.03f, 0.03f); } else { - c->SetReflection(ReflectionType::kChar); + c->SetReflection(base::ReflectionType::kChar); c->SetReflectionScale(reflection_scale_, reflection_scale_, reflection_scale_); } @@ -4163,23 +4186,23 @@ void SpazNode::DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, // Head. c->PushTransform(); - c->TransformToBody(*body_head_); + body_head_->ApplyToRenderComponent(c); if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, death_scale); } - if (head_model_.exists()) { - c->DrawModel(head_model_->model_data()); + if (head_mesh_.Exists()) { + c->DrawMeshAsset(head_mesh_->mesh_data()); } c->PopTransform(); // Hair tuft 1. - if (hair_front_right_body_.exists()) { + if (hair_front_right_body_.Exists()) { c->PushTransform(); - c->TransformToBody(*hair_front_right_body_); + hair_front_right_body_->ApplyToRenderComponent(c); if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, death_scale); } - c->DrawModel(g_assets->GetModel(SystemModelID::kHairTuft1)); + c->DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kHairTuft1)); c->PopTransform(); // Hair tuft 1b; just reuse tuft 1 with some extra translating. @@ -4189,70 +4212,70 @@ void SpazNode::DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, c->Translate(offs[0] * m[0] + offs[1] * m[1] + offs[2] * m[2], offs[0] * m[4] + offs[1] * m[5] + offs[2] * m[6], offs[0] * m[8] + offs[1] * m[9] + offs[2] * m[10]); - c->TransformToBody(*hair_front_right_body_); + hair_front_right_body_->ApplyToRenderComponent(c); if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, death_scale); } - c->DrawModel(g_assets->GetModel(SystemModelID::kHairTuft1b)); + c->DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kHairTuft1b)); c->PopTransform(); } // Hair tuft 2. - if (hair_front_left_body_.exists()) { + if (hair_front_left_body_.Exists()) { c->PushTransform(); - c->TransformToBody(*hair_front_left_body_); + hair_front_left_body_->ApplyToRenderComponent(c); if (death_scale != 1.0f) c->Scale(death_scale, death_scale, death_scale); - c->DrawModel(g_assets->GetModel(SystemModelID::kHairTuft2)); + c->DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kHairTuft2)); c->PopTransform(); } // Hair tuft 3. - if (hair_ponytail_top_body_.exists()) { + if (hair_ponytail_top_body_.Exists()) { c->PushTransform(); - c->TransformToBody(*hair_ponytail_top_body_); + hair_ponytail_top_body_->ApplyToRenderComponent(c); if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, death_scale); } - c->DrawModel(g_assets->GetModel(SystemModelID::kHairTuft3)); + c->DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kHairTuft3)); c->PopTransform(); } // Hair tuft 4. - if (hair_ponytail_bottom_body_.exists()) { + if (hair_ponytail_bottom_body_.Exists()) { c->PushTransform(); - c->TransformToBody(*hair_ponytail_bottom_body_); + hair_ponytail_bottom_body_->ApplyToRenderComponent(c); if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, death_scale); } - c->DrawModel(g_assets->GetModel(SystemModelID::kHairTuft4)); + c->DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kHairTuft4)); c->PopTransform(); } // Torso. c->PushTransform(); - c->TransformToBody(*body_torso_); + body_torso_->ApplyToRenderComponent(c); if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, death_scale); } - if (torso_model_.exists()) { - c->DrawModel(torso_model_->model_data()); + if (torso_mesh_.Exists()) { + c->DrawMeshAsset(torso_mesh_->mesh_data()); } c->PopTransform(); // Pelvis. c->PushTransform(); - c->TransformToBody(*body_pelvis_); + body_pelvis_->ApplyToRenderComponent(c); if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, death_scale); } - if (pelvis_model_.exists()) { - c->DrawModel(pelvis_model_->model_data()); + if (pelvis_mesh_.Exists()) { + c->DrawMeshAsset(pelvis_mesh_->mesh_data()); } c->PopTransform(); // Right upper arm. c->PushTransform(); - c->TransformToBody(*upper_right_arm_body_); + upper_right_arm_body_->ApplyToRenderComponent(c); // Get the distance between the shoulder joint socket and the fore-arm // socket.. we'll use this to stretch our upper-arm to fill the gap. @@ -4283,14 +4306,14 @@ void SpazNode::DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); } - if (upper_arm_model_.exists()) { - c->DrawModel(upper_arm_model_->model_data()); + if (upper_arm_mesh_.Exists()) { + c->DrawMeshAsset(upper_arm_mesh_->mesh_data()); } c->PopTransform(); // Right lower arm. c->PushTransform(); - c->TransformToBody(*lower_right_arm_body_); + lower_right_arm_body_->ApplyToRenderComponent(c); c->PushTransform(); c->Translate(0, 0, 0.1f); c->Scale(1.0f, 1.0f, right_stretch); @@ -4298,8 +4321,8 @@ void SpazNode::DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); } - if (forearm_model_.exists() && !flippers_) { - c->DrawModel(forearm_model_->model_data()); + if (forearm_mesh_.Exists() && !flippers_) { + c->DrawMeshAsset(forearm_mesh_->mesh_data()); } c->PopTransform(); if (!have_boxing_gloves_) { @@ -4312,15 +4335,15 @@ void SpazNode::DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); } - if (hand_model_.exists() && !flippers_) { - c->DrawModel(hand_model_->model_data()); + if (hand_mesh_.Exists() && !flippers_) { + c->DrawMeshAsset(hand_mesh_->mesh_data()); } } c->PopTransform(); // Right upper leg. c->PushTransform(); - c->TransformToBody(*upper_right_leg_body_); + upper_right_leg_body_->ApplyToRenderComponent(c); // Apply stretching if still intact. if (!shattered_) { @@ -4341,29 +4364,29 @@ void SpazNode::DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); } - if (upper_leg_model_.exists()) { - c->DrawModel(upper_leg_model_->model_data()); + if (upper_leg_mesh_.Exists()) { + c->DrawMeshAsset(upper_leg_mesh_->mesh_data()); } c->PopTransform(); // Right lower leg. c->PushTransform(); - c->TransformToBody(*lower_right_leg_body_); + lower_right_leg_body_->ApplyToRenderComponent(c); if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); } - if (lower_leg_model_.exists()) { - c->DrawModel(lower_leg_model_->model_data()); + if (lower_leg_mesh_.Exists()) { + c->DrawMeshAsset(lower_leg_mesh_->mesh_data()); } c->PopTransform(); c->PushTransform(); - c->TransformToBody(*right_toes_body_); + right_toes_body_->ApplyToRenderComponent(c); if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, death_scale); } - if (toes_model_.exists()) { - c->DrawModel(toes_model_->model_data()); + if (toes_mesh_.Exists()) { + c->DrawMeshAsset(toes_mesh_->mesh_data()); } c->PopTransform(); @@ -4372,7 +4395,7 @@ void SpazNode::DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, // Left upper arm. c->PushTransform(); - c->TransformToBody(*upper_left_arm_body_); + upper_left_arm_body_->ApplyToRenderComponent(c); float left_stretch = 1.0f; // Stretch if not shattered. @@ -4399,12 +4422,14 @@ void SpazNode::DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); } - if (upper_arm_model_.exists()) c->DrawModel(upper_arm_model_->model_data()); + if (upper_arm_mesh_.Exists()) { + c->DrawMeshAsset(upper_arm_mesh_->mesh_data()); + } c->PopTransform(); // Left lower arm. c->PushTransform(); - c->TransformToBody(*lower_left_arm_body_); + lower_left_arm_body_->ApplyToRenderComponent(c); c->Scale(-1, 1, 1); c->PushTransform(); c->Translate(0, 0, 0.1f); @@ -4413,8 +4438,8 @@ void SpazNode::DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); } - if (forearm_model_.exists() && !flippers_) { - c->DrawModel(forearm_model_->model_data()); + if (forearm_mesh_.Exists() && !flippers_) { + c->DrawMeshAsset(forearm_mesh_->mesh_data()); } c->PopTransform(); if (!have_boxing_gloves_) { @@ -4427,15 +4452,15 @@ void SpazNode::DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, if (death_scale != 1.0f) { c->Scale(death_scale, death_scale, death_scale); } - if (hand_model_.exists() && !flippers_) { - c->DrawModel(hand_model_->model_data()); + if (hand_mesh_.Exists() && !flippers_) { + c->DrawMeshAsset(hand_mesh_->mesh_data()); } } c->PopTransform(); // Left upper leg. c->PushTransform(); - c->TransformToBody(*upper_left_leg_body_); + upper_left_leg_body_->ApplyToRenderComponent(c); // Stretch if not shattered. if (!shattered_) { @@ -4454,55 +4479,59 @@ void SpazNode::DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, } if (death_scale != 1.0f) c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); - if (upper_leg_model_.exists()) c->DrawModel(upper_leg_model_->model_data()); + if (upper_leg_mesh_.Exists()) c->DrawMeshAsset(upper_leg_mesh_->mesh_data()); c->PopTransform(); // Lower leg. c->PushTransform(); - c->TransformToBody(*lower_left_leg_body_); + lower_left_leg_body_->ApplyToRenderComponent(c); c->Scale(-1.0f, 1.0f, 1.0f); if (death_scale != 1.0f) c->Scale(death_scale, death_scale, 0.5f + death_scale * 0.5f); - if (lower_leg_model_.exists()) { - c->DrawModel(lower_leg_model_->model_data()); + if (lower_leg_mesh_.Exists()) { + c->DrawMeshAsset(lower_leg_mesh_->mesh_data()); } c->PopTransform(); // Toes. c->PushTransform(); - c->TransformToBody(*left_toes_body_); + left_toes_body_->ApplyToRenderComponent(c); c->Scale(-1, 1, 1); - if (death_scale != 1.0f) c->Scale(death_scale, death_scale, death_scale); - if (toes_model_.exists()) c->DrawModel(toes_model_->model_data()); + if (death_scale != 1.0f) { + c->Scale(death_scale, death_scale, death_scale); + } + if (toes_mesh_.Exists()) { + c->DrawMeshAsset(toes_mesh_->mesh_data()); + } c->PopTransform(); // RESTORE CULL c->FlipCullFace(); } -static void DrawRadialMeter(MeshIndexedSimpleFull* m, SimpleComponent* c, - float amt, bool flash) { +static void DrawRadialMeter(base::MeshIndexedSimpleFull* m, + base::SimpleComponent* c, float amt, bool flash) { if (flash) { c->SetColor(1, 1, 0.4f, 0.7f); } else { c->SetColor(1, 1, 1, 0.6f); } - Graphics::DrawRadialMeter(m, amt); + base::Graphics::DrawRadialMeter(m, amt); c->DrawMesh(m); } -void SpazNode::Draw(FrameDef* frame_def) { +void SpazNode::Draw(base::FrameDef* frame_def) { #if !BA_HEADLESS_BUILD #if BA_OSTYPE_MACOS - if (g_graphics_server->renderer()->debug_draw_mode()) { - SimpleComponent c(frame_def->overlay_3d_pass()); + if (g_base->graphics_server->renderer()->debug_draw_mode()) { + base::SimpleComponent c(frame_def->overlay_3d_pass()); c.SetTransparent(true); c.SetDoubleSided(true); c.SetColor(1, 0, 0, 0.5f); c.PushTransform(); - c.TransformToBody(*body_head_); + body_head_->ApplyToRenderComponent(&c); c.BeginDebugDrawTriangles(); c.Vertex(0, 0.5f, 0); @@ -4512,7 +4541,7 @@ void SpazNode::Draw(FrameDef* frame_def) { c.PopTransform(); c.PushTransform(); - c.TransformToBody(*body_torso_); + body_torso_->ApplyToRenderComponent(&c); c.BeginDebugDrawTriangles(); c.Vertex(0, 0.2f, 0); c.Vertex(0, 0, 0.2f); @@ -4521,7 +4550,7 @@ void SpazNode::Draw(FrameDef* frame_def) { c.PopTransform(); c.PushTransform(); - c.TransformToBody(*body_pelvis_); + body_pelvis_->ApplyToRenderComponent(&c); c.BeginDebugDrawTriangles(); c.Vertex(0, 0.2f, 0); c.Vertex(0, 0, 0.2f); @@ -4531,7 +4560,7 @@ void SpazNode::Draw(FrameDef* frame_def) { c.SetColor(0.4f, 1.0f, 0.4f, 0.2f); c.PushTransform(); - c.TransformToBody(*stand_body_); + stand_body_->ApplyToRenderComponent(&c); c.BeginDebugDrawTriangles(); c.Vertex(0, 0.2f, 0); c.Vertex(0, 0, 0.5f); @@ -4573,7 +4602,7 @@ void SpazNode::Draw(FrameDef* frame_def) { if (explicit_bool(true)) { c.SetColor(1, 0, 0); c.PushTransform(); - c.TransformToBody(*lower_left_leg_body_); + lower_left_leg_body_->ApplyToRenderComponent(&c); JointFixedEF* j = left_leg_ik_joint_; c.Translate(j->anchor2[0], j->anchor2[1], j->anchor2[2]); c.Rotate(90, 1, 0, 0); @@ -4593,7 +4622,7 @@ void SpazNode::Draw(FrameDef* frame_def) { if (explicit_bool(true)) { c.SetColor(0, 0, 1); c.PushTransform(); - c.TransformToBody(*body_pelvis_); + body_pelvis_->ApplyToRenderComponent(&c); JointFixedEF* j = left_leg_ik_joint_; c.Translate(j->anchor1[0], j->anchor1[1], j->anchor1[2]); c.Rotate(90, 1, 0, 0); @@ -4615,7 +4644,7 @@ void SpazNode::Draw(FrameDef* frame_def) { millisecs_t scenetime = scene()->time(); int64_t render_frame_count = frame_def->frame_number(); - RenderPass* beauty_pass = frame_def->beauty_pass(); + auto* beauty_pass = frame_def->beauty_pass(); float death_fade = 1.0f; float death_scale = 1.0f; @@ -4764,7 +4793,7 @@ void SpazNode::Draw(FrameDef* frame_def) { g = 0.0f; b = 0.0f; } - SimpleComponent c(frame_def->overlay_3d_pass()); + base::SimpleComponent c(frame_def->overlay_3d_pass()); c.SetTransparent(true); c.SetColor(r, g, b); @@ -4794,12 +4823,14 @@ void SpazNode::Draw(FrameDef* frame_def) { / static_cast(mini_billboard_1_end_time_ - mini_billboard_1_start_time_); if (amt > 0.0001f && amt <= 1.0f) { - SimpleComponent c(frame_def->overlay_3d_pass()); + base::SimpleComponent c(frame_def->overlay_3d_pass()); c.SetTransparent(true); bool flash = (scenetime - mini_billboard_1_start_time_ < 200 && render_frame_count % 6 < 3); if (!flash) { - c.SetTexture(mini_billboard_1_texture_); + c.SetTexture(mini_billboard_1_texture_.Exists() + ? mini_billboard_1_texture_->texture_data() + : nullptr); } c.PushTransform(); c.Translate(torso_pos[0] - 0.2f, torso_pos[1] + 1.2f, @@ -4816,13 +4847,17 @@ void SpazNode::Draw(FrameDef* frame_def) { / static_cast(mini_billboard_2_end_time_ - mini_billboard_2_start_time_); if (amt > 0.0001f && amt <= 1.0f) { - SimpleComponent c(frame_def->overlay_3d_pass()); + base::SimpleComponent c(frame_def->overlay_3d_pass()); c.SetTransparent(true); bool flash = (scenetime - mini_billboard_2_start_time_ < 200 && render_frame_count % 6 < 3); // if (!flash) // c.SetTexture(mediaSet->GetTexture(mini_billboard_2_texture_)); - if (!flash) c.SetTexture(mini_billboard_2_texture_); + if (!flash) { + c.SetTexture(mini_billboard_2_texture_.Exists() + ? mini_billboard_2_texture_->texture_data() + : nullptr); + } c.PushTransform(); c.Translate(torso_pos[0], torso_pos[1] + 1.2f, torso_pos[2] - 0.2f); c.Scale(0.09f, 0.09f, 0.09f); @@ -4837,12 +4872,14 @@ void SpazNode::Draw(FrameDef* frame_def) { / static_cast(mini_billboard_3_end_time_ - mini_billboard_3_start_time_); if (amt > 0.0001f && amt <= 1.0f) { - SimpleComponent c(frame_def->overlay_3d_pass()); + base::SimpleComponent c(frame_def->overlay_3d_pass()); c.SetTransparent(true); bool flash = (scenetime - mini_billboard_3_start_time_ < 200 && render_frame_count % 6 < 3); if (!flash) { - c.SetTexture(mini_billboard_3_texture_); + c.SetTexture(mini_billboard_3_texture_.Exists() + ? mini_billboard_3_texture_->texture_data() + : nullptr); } c.PushTransform(); c.Translate(torso_pos[0] + 0.2f, torso_pos[1] + 1.2f, @@ -4857,14 +4894,15 @@ void SpazNode::Draw(FrameDef* frame_def) { /// draw our counter if (!counter_text_.empty() && !dead_) { { // icon - SimpleComponent c(frame_def->overlay_3d_pass()); + base::SimpleComponent c(frame_def->overlay_3d_pass()); c.SetTransparent(true); - c.SetTexture(counter_texture_); + c.SetTexture(counter_texture_.Exists() ? counter_texture_->texture_data() + : nullptr); c.PushTransform(); c.Translate(torso_pos[0] - 0.3f, torso_pos[1] + 1.47f, torso_pos[2] - 0.2f); c.Scale(1.5f * 0.2f, 1.5f * 0.2f, 1.5f * 0.2f); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.PopTransform(); c.Submit(); } @@ -4873,7 +4911,7 @@ void SpazNode::Draw(FrameDef* frame_def) { counter_mesh_text_ = counter_text_; counter_text_group_.SetText(counter_mesh_text_); } - SimpleComponent c(frame_def->overlay_3d_pass()); + base::SimpleComponent c(frame_def->overlay_3d_pass()); c.SetTransparent(true); int elem_count = counter_text_group_.GetElementCount(); for (int e = 0; e < elem_count; e++) { @@ -4900,10 +4938,11 @@ void SpazNode::Draw(FrameDef* frame_def) { if (explicit_bool(true)) { if (name_mesh_txt_ != name_) { name_mesh_txt_ = name_; - name_text_group_.SetText(name_mesh_txt_, TextMesh::HAlign::kCenter, - TextMesh::VAlign::kCenter); + name_text_group_.SetText(name_mesh_txt_, + base::TextMesh::HAlign::kCenter, + base::TextMesh::VAlign::kCenter); } - SimpleComponent c(frame_def->overlay_3d_pass()); + base::SimpleComponent c(frame_def->overlay_3d_pass()); c.SetTransparent(true); float extra; if (age < 200) { @@ -4926,11 +4965,12 @@ void SpazNode::Draw(FrameDef* frame_def) { int elem_count = name_text_group_.GetElementCount(); float s_extra = - (IsVRMode() || g_ui->scale() == UIScale::kSmall) ? 1.2f : 1.0f; + (g_core->IsVRMode() || g_base->ui->scale() == UIScale::kSmall) ? 1.2f + : 1.0f; for (int e = 0; e < elem_count; e++) { // Gracefully skip unloaded textures. - TextureData* t = name_text_group_.GetElementTexture(e); + auto* t = name_text_group_.GetElementTexture(e); if (!t->preloaded()) continue; c.SetTexture(t); c.SetMaskUV2Texture(name_text_group_.GetElementMaskUV2Texture(e)); @@ -4942,7 +4982,7 @@ void SpazNode::Draw(FrameDef* frame_def) { c.Translate(torso_pos[0] - 0.0f, torso_pos[1] + 0.89f + 0.4f * extra, torso_pos[2] - 0.2f); float s = (0.01f + 0.01f * extra) * death_scale; - float w = g_text_graphics->GetStringWidth(name_.c_str()); + float w = g_base->text_graphics->GetStringWidth(name_.c_str()); if (w > 100.0f) s *= (100.0f / w); s *= s_extra; c.Scale(s, s, s); @@ -4959,14 +4999,16 @@ void SpazNode::Draw(FrameDef* frame_def) { float s = o; if (billboard_cross_out_) o *= (render_frame_count % 14 < 7) ? 0.8f : 0.2f; const dReal* pos = dBodyGetPosition(body_torso_->body()); - SimpleComponent c(frame_def->overlay_3d_pass()); + base::SimpleComponent c(frame_def->overlay_3d_pass()); c.SetTransparent(true); c.SetColor(1, 1, 1, o); - c.SetTexture(billboard_texture_); + c.SetTexture(billboard_texture_.Exists() + ? billboard_texture_->texture_data() + : nullptr); c.PushTransform(); c.Translate(pos[0], pos[1] + 1.6f, pos[2] - 0.2f); c.Scale(2.3f * 0.2f * s, 2.3f * 0.2f * s, 2.3f * 0.2f * s); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.PopTransform(); c.Submit(); @@ -4974,13 +5016,13 @@ void SpazNode::Draw(FrameDef* frame_def) { if (billboard_cross_out_) { float o2 = billboard_opacity_ * ((render_frame_count % 14 < 7) ? 0.4f : 0.1f); - SimpleComponent c2(frame_def->overlay_3d_pass()); + base::SimpleComponent c2(frame_def->overlay_3d_pass()); c2.SetTransparent(true); c2.SetColor(1, 0, 0, o2); c2.PushTransform(); c2.Translate(pos[0], pos[1] + 1.6f, pos[2] - 0.2f); c2.Scale(2.3f * 0.2f * s, 2.3f * 0.2f * s, 2.3f * 0.2f * s); - c2.DrawModel(g_assets->GetModel(SystemModelID::kCrossOut)); + c2.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kCrossOut)); c2.PopTransform(); c2.Submit(); } @@ -4992,12 +5034,14 @@ void SpazNode::Draw(FrameDef* frame_def) { float o{1.0f}; millisecs_t since_last_hurt_change = scenetime - last_hurt_change_time_; if (since_last_hurt_change < fade_time) { - SimpleComponent c(frame_def->overlay_3d_pass()); + base::SimpleComponent c(frame_def->overlay_3d_pass()); c.SetTransparent(true); c.SetPremultiplied(true); c.PushTransform(); - o = 1.0f - static_cast(since_last_hurt_change) / fade_time; + o = 1.0f + - static_cast(since_last_hurt_change) + / static_cast(fade_time); o *= o; const dReal* pos = dBodyGetPosition(body_torso_->body()); @@ -5028,7 +5072,7 @@ void SpazNode::Draw(FrameDef* frame_def) { c.PushTransform(); c.Translate(0.5f, half_height); c.Scale(1.1f, height + 0.1f); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.PopTransform(); c.SetColor(0, 0.35f * o, 0, 0.3f * o); @@ -5036,7 +5080,7 @@ void SpazNode::Draw(FrameDef* frame_def) { c.PushTransform(); c.Translate(p_left * 0.5f, half_height); c.Scale(p_left, height); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.PopTransform(); if (dead_ && scene()->stepnum() % 10 < 5) { @@ -5048,7 +5092,7 @@ void SpazNode::Draw(FrameDef* frame_def) { c.PushTransform(); c.Translate((p_left + p_right) * 0.5f, half_height); c.Scale(p_right - p_left, height); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.PopTransform(); c.SetColor((dead_ && scene()->stepnum() % 10 < 5) ? 0.55f * o : 0.01f * o, @@ -5057,7 +5101,7 @@ void SpazNode::Draw(FrameDef* frame_def) { c.PushTransform(); c.Translate((p_right + 1.0f) * 0.5f, half_height); c.Scale(1.0f - p_right, height); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.PopTransform(); c.PopTransform(); @@ -5068,22 +5112,22 @@ void SpazNode::Draw(FrameDef* frame_def) { // Draw all body parts with normal shading. { { - ObjectComponent c(beauty_pass); + base::ObjectComponent c(beauty_pass); DrawBodyParts(&c, true, death_fade, death_scale, add_color); SetupEyeLidShading(&c, death_fade, add_color); DrawEyeLids(&c, death_fade, death_scale); c.Submit(); } { - ObjectComponent c(beauty_pass); + base::ObjectComponent c(beauty_pass); DrawEyeBalls(&c, &c, true, death_fade, death_scale, add_color); c.Submit(); } // In higher-quality mode, blur our eyeballs and eyelids a bit to look more // fleshy. - if (frame_def->quality() >= GraphicsQuality::kHigher) { - PostProcessComponent c(frame_def->blit_pass()); + if (frame_def->quality() >= base::GraphicsQuality::kHigher) { + base::PostProcessComponent c(frame_def->blit_pass()); c.setEyes(true); DrawEyeLids(&c, death_fade, death_scale); DrawEyeBalls(&c, nullptr, false, death_fade, death_scale, add_color); @@ -5093,12 +5137,12 @@ void SpazNode::Draw(FrameDef* frame_def) { // Wings. if (wings_) { - ObjectComponent c(beauty_pass); + base::ObjectComponent c(beauty_pass); c.SetTransparent(false); c.SetColor(1, 1, 1, 1.0f); - c.SetReflection(ReflectionType::kSoft); + c.SetReflection(base::ReflectionType::kSoft); c.SetReflectionScale(0.4f, 0.4f, 0.4f); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kWings)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kWings)); // Fade to reddish on death. if (dead_ && !frozen_) { @@ -5118,14 +5162,14 @@ void SpazNode::Draw(FrameDef* frame_def) { c.PushTransform(); c.Translate(p_wing_l[0], p_wing_l[1], p_wing_l[2]); c.Scale(0.05f, 0.05f, 0.05f); - c.DrawModel(g_assets->GetModel(SystemModelID::kBox)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kBox)); c.PopTransform(); // Draw wing point. c.PushTransform(); c.Translate(wing_pos_left_.x, wing_pos_left_.y, wing_pos_left_.z); c.Scale(0.1f, 0.1f, 0.1f); - c.DrawModel(g_assets->GetModel(SystemModelID::kBox)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kBox)); c.PopTransform(); // Draw target. @@ -5134,14 +5178,14 @@ void SpazNode::Draw(FrameDef* frame_def) { c.PushTransform(); c.Translate(p_wing_r[0], p_wing_r[1], p_wing_r[2]); c.Scale(0.05f, 0.05f, 0.05f); - c.DrawModel(g_assets->GetModel(SystemModelID::kBox)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kBox)); c.PopTransform(); // Draw wing point. c.PushTransform(); c.Translate(wing_pos_right_.x, wing_pos_right_.y, wing_pos_right_.z); c.Scale(0.1f, 0.1f, 0.1f); - c.DrawModel(g_assets->GetModel(SystemModelID::kBox)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kBox)); c.PopTransform(); } @@ -5167,7 +5211,7 @@ void SpazNode::Draw(FrameDef* frame_def) { if (death_scale != 1.0f) { c.Scale(death_scale, death_scale, death_scale); } - c.DrawModel(g_assets->GetModel(SystemModelID::kWing)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kWing)); c.PopTransform(); Vector3f to_right_wing = wing_pos_right_ - torso_pos2; @@ -5185,20 +5229,20 @@ void SpazNode::Draw(FrameDef* frame_def) { if (death_scale != 1.0f) { c.Scale(death_scale, death_scale, death_scale); } - c.DrawModel(g_assets->GetModel(SystemModelID::kWing)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kWing)); c.PopTransform(); c.Submit(); } // Boxing gloves. if (have_boxing_gloves_) { - ObjectComponent c(beauty_pass); + base::ObjectComponent c(beauty_pass); if (frozen_) { c.SetAddColor(0.1f, 0.1f, 0.4f); - c.SetReflection(ReflectionType::kSharper); + c.SetReflection(base::ReflectionType::kSharper); c.SetReflectionScale(1.4f, 1.4f, 1.4f); } else { - c.SetReflection(ReflectionType::kChar); + c.SetReflection(base::ReflectionType::kChar); c.SetReflectionScale(0.6f * death_fade, 0.55f * death_fade, 0.55f * death_fade); @@ -5221,25 +5265,25 @@ void SpazNode::Draw(FrameDef* frame_def) { } } } - c.SetLightShadow(LightShadowType::kObject); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kBoxingGlove)); + c.SetLightShadow(base::LightShadowType::kObject); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kBoxingGlove)); c.PushTransform(); - c.TransformToBody(*lower_right_arm_body_); + lower_right_arm_body_->ApplyToRenderComponent(&c); if (death_scale != 1.0f) { c.Scale(death_scale, death_scale, death_scale); } - c.DrawModel(g_assets->GetModel(SystemModelID::kBoxingGlove)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kBoxingGlove)); c.PopTransform(); c.FlipCullFace(); c.PushTransform(); - c.TransformToBody(*lower_left_arm_body_); + lower_left_arm_body_->ApplyToRenderComponent(&c); c.Scale(-1.0f, 1.0f, 1.0f); if (death_scale != 1.0f) { c.Scale(death_scale, death_scale, death_scale); } - c.DrawModel(g_assets->GetModel(SystemModelID::kBoxingGlove)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kBoxingGlove)); c.FlipCullFace(); c.PopTransform(); c.Submit(); @@ -5257,7 +5301,7 @@ void SpazNode::Draw(FrameDef* frame_def) { sc[2] = weight * freeze_color[2] + (1.0f - weight) * sc[2]; } - FullShadowSet* full_shadows = full_shadow_set_.get(); + FullShadowSet* full_shadows = full_shadow_set_.Get(); if (full_shadows) { DrawBrightSpot(full_shadows->lower_left_leg_shadow_, 0.3f * death_scale, death_fade * (frozen_ ? 0.3f : 0.2f), sc); @@ -5288,7 +5332,7 @@ void SpazNode::Draw(FrameDef* frame_def) { DrawShadow(full_shadows->upper_right_arm_shadow_, 0.08f * death_scale, 0.5f, sc); } else { - SimpleShadowSet* simple_shadows = simple_shadow_set_.get(); + SimpleShadowSet* simple_shadows = simple_shadow_set_.Get(); assert(simple_shadows); DrawShadow(simple_shadows->shadow_, 0.2f * death_scale, 2.0f, sc); } @@ -5296,13 +5340,13 @@ void SpazNode::Draw(FrameDef* frame_def) { #endif // !BA_HEADLESS_BUILD } // NOLINT (yes i know this is too big) -void SpazNode::OnGraphicsQualityChanged(GraphicsQuality q) { +void SpazNode::OnGraphicsQualityChanged(base::GraphicsQuality q) { UpdateForGraphicsQuality(q); } -void SpazNode::UpdateForGraphicsQuality(GraphicsQuality quality) { +void SpazNode::UpdateForGraphicsQuality(base::GraphicsQuality quality) { #if !BA_HEADLESS_BUILD - if (quality >= GraphicsQuality::kMedium) { + if (quality >= base::GraphicsQuality::kMedium) { full_shadow_set_ = Object::New(); simple_shadow_set_.Clear(); } else { @@ -5458,11 +5502,11 @@ auto SpazNode::CollideCallback(dContact* c, int count, RigidBody* colliding_body, RigidBody* opposingbody) -> bool { // Keep track of whether our toes are touching something besides us - // if (colliding_body == left_toes_body_.get() and opposingbody->getNode() != - // this) _toesTouchingL = true; if (colliding_body == right_toes_body_.get() + // if (colliding_body == left_toes_body_.Get() and opposingbody->getNode() != + // this) _toesTouchingL = true; if (colliding_body == right_toes_body_.Get() // and opposingbody->getNode() != this) _toesTouchingR = true; _toesTouchingL - // = (colliding_body == left_toes_body_.get() and opposingbody->getNode() != - // this); _toesTouchingR = (colliding_body == right_toes_body_.get() and + // = (colliding_body == left_toes_body_.Get() and opposingbody->getNode() != + // this); _toesTouchingR = (colliding_body == right_toes_body_.Get() and // opposingbody->getNode() != this); // hair collide with most anything but weakly.. @@ -5490,8 +5534,8 @@ auto SpazNode::CollideCallback(dContact* c, int count, if (colliding_body->part() == &limbs_part_lower_) { // Drop friction if lower arms are hitting upper legs. - if ((colliding_body == lower_left_arm_body_.get() - || colliding_body == lower_right_arm_body_.get()) + if ((colliding_body == lower_left_arm_body_.Get() + || colliding_body == lower_right_arm_body_.Get()) && !shattered_) { for (int i = 0; i < count; i++) { c[i].surface.mu = 0.0f; @@ -5502,8 +5546,8 @@ auto SpazNode::CollideCallback(dContact* c, int count, float stiffness = 10.0f; float damping = 1.0f; - if (colliding_body == left_toes_body_.get() - || colliding_body == right_toes_body_.get()) { + if (colliding_body == left_toes_body_.Get() + || colliding_body == right_toes_body_.Get()) { stiffness *= kToesCollideStiffness; damping *= kToesCollideDamping; @@ -5512,8 +5556,8 @@ auto SpazNode::CollideCallback(dContact* c, int count, c[i].surface.mu *= 0.1f; } } - if (colliding_body == lower_right_leg_body_.get() - || colliding_body == lower_left_leg_body_.get()) { + if (colliding_body == lower_right_leg_body_.Get() + || colliding_body == lower_left_leg_body_.Get()) { stiffness *= kLowerLegCollideStiffness; damping *= kLowerLegCollideDamping; } @@ -5533,8 +5577,8 @@ auto SpazNode::CollideCallback(dContact* c, int count, // If we're punching, lets crank up stiffness on our punching hand // so it looks like its responding to stuff its hitting. if (punch_ && !dead_) { - if ((colliding_body == lower_right_arm_body_.get() && punch_right_) - || (colliding_body == lower_left_arm_body_.get() && !punch_right_)) { + if ((colliding_body == lower_right_arm_body_.Get() && punch_right_) + || (colliding_body == lower_left_arm_body_.Get() && !punch_right_)) { stiffness *= 200.0f; damping *= 20.0f; } @@ -5550,8 +5594,8 @@ auto SpazNode::CollideCallback(dContact* c, int count, float stiffness = 10; float damping = 1; float erp, cfm; - if (colliding_body == upper_right_leg_body_.get() - || colliding_body == upper_left_leg_body_.get()) { + if (colliding_body == upper_right_leg_body_.Get() + || colliding_body == upper_left_leg_body_.Get()) { stiffness *= kUpperLegCollideStiffness; damping *= kUpperLegCollideDamping; } @@ -5645,7 +5689,7 @@ auto SpazNode::CollideCallback(dContact* c, int count, // Keep track of when stuff is hitting our head, so we know when to calc // damage from head whiplash. - if (colliding_body == body_head_.get() && opposingbody->part()->node() != this + if (colliding_body == body_head_.Get() && opposingbody->part()->node() != this && opposingbody->can_cause_impact_damage()) { last_head_collide_time_ = scene()->time(); } @@ -5822,67 +5866,67 @@ auto SpazNode::GetRigidBody(int id) -> RigidBody* { // Ewwww this should be automatic. switch (id) { case kHeadBodyID: - return body_head_.get(); + return body_head_.Get(); break; case kTorsoBodyID: - return body_torso_.get(); + return body_torso_.Get(); break; case kPunchBodyID: - return body_punch_.get(); + return body_punch_.Get(); break; case kPickupBodyID: - return body_pickup_.get(); + return body_pickup_.Get(); break; case kPelvisBodyID: - return body_pelvis_.get(); + return body_pelvis_.Get(); break; case kRollerBodyID: - return body_roller_.get(); + return body_roller_.Get(); break; case kStandBodyID: - return stand_body_.get(); + return stand_body_.Get(); break; case kUpperRightArmBodyID: - return upper_right_arm_body_.get(); + return upper_right_arm_body_.Get(); break; case kLowerRightArmBodyID: - return lower_right_arm_body_.get(); + return lower_right_arm_body_.Get(); break; case kUpperLeftArmBodyID: - return upper_left_arm_body_.get(); + return upper_left_arm_body_.Get(); break; case kLowerLeftArmBodyID: - return lower_left_arm_body_.get(); + return lower_left_arm_body_.Get(); break; case kUpperRightLegBodyID: - return upper_right_leg_body_.get(); + return upper_right_leg_body_.Get(); break; case kLowerRightLegBodyID: - return lower_right_leg_body_.get(); + return lower_right_leg_body_.Get(); break; case kUpperLeftLegBodyID: - return upper_left_leg_body_.get(); + return upper_left_leg_body_.Get(); break; case kLowerLeftLegBodyID: - return lower_left_leg_body_.get(); + return lower_left_leg_body_.Get(); break; case kLeftToesBodyID: - return left_toes_body_.get(); + return left_toes_body_.Get(); break; case kRightToesBodyID: - return right_toes_body_.get(); + return right_toes_body_.Get(); break; case kHairFrontRightBodyID: - return hair_front_right_body_.get(); + return hair_front_right_body_.Get(); break; case kHairFrontLeftBodyID: - return hair_front_left_body_.get(); + return hair_front_left_body_.Get(); break; case kHairPonyTailTopBodyID: - return hair_ponytail_top_body_.get(); + return hair_ponytail_top_body_.Get(); break; case kHairPonyTailBottomBodyID: - return hair_ponytail_bottom_body_.get(); + return hair_ponytail_bottom_body_.Get(); break; default: Log(LogLevel::kError, @@ -5912,7 +5956,7 @@ void SpazNode::GetRigidBodyPickupLocations(int id, float* obj, float* character, } void SpazNode::DropHeldObject() { if (holding_something_) { - if (hold_node_.exists()) { + if (hold_node_.Exists()) { assert(pickup_joint_.IsAlive()); pickup_joint_.Kill(); } @@ -5922,7 +5966,7 @@ void SpazNode::DropHeldObject() { hold_body_ = 0; // Dispatch user messages last now that all is in place. - if (hold_node_.exists()) { + if (hold_node_.Exists()) { hold_node_->DispatchDroppedMessage(this); } DispatchDropMessage(); @@ -5931,7 +5975,7 @@ void SpazNode::DropHeldObject() { void SpazNode::CreateHair() { // Assume all already exists in this case. - if (hair_front_right_body_.exists()) return; + if (hair_front_right_body_.Exists()) return; // Front right tuft. hair_front_right_body_ = @@ -5942,7 +5986,7 @@ void SpazNode::CreateHair() { hair_front_right_body_->SetDimensions(0.07f, 0.13f, 0, 0, 0, 0, 0.01f); hair_front_right_joint_ = CreateFixedJoint( - body_head_.get(), hair_front_right_body_.get(), 0, 0, // lin stiff/damp + body_head_.Get(), hair_front_right_body_.Get(), 0, 0, // lin stiff/damp 0, 0, // ang stiff/damp -0.17f, 0.19f, 0.18f, // b1 anchor 0, -0.08f, -0.12f // b2 anchor @@ -5960,7 +6004,7 @@ void SpazNode::CreateHair() { hair_front_left_body_->SetDimensions(0.04f, 0.13f, 0, 0.07f, 0.13f, 0, 0.01f); hair_front_left_joint_ = CreateFixedJoint( - body_head_.get(), hair_front_left_body_.get(), 0, 0, // lin stiff/damp + body_head_.Get(), hair_front_left_body_.Get(), 0, 0, // lin stiff/damp 0, 0, // ang stiff/damp 0.13f, 0.11f, 0.13f, // b1 anchor 0, -0.08f, -0.12f // b2 anchor @@ -5978,7 +6022,7 @@ void SpazNode::CreateHair() { hair_ponytail_top_body_->SetDimensions(0.09f, 0.1f, 0, 0, 0, 0, 0.01f); hair_ponytail_top_joint_ = CreateFixedJoint( - body_head_.get(), hair_ponytail_top_body_.get(), 0, 0, // lin stiff/damp + body_head_.Get(), hair_ponytail_top_body_.Get(), 0, 0, // lin stiff/damp 0, 0, // ang stiff/damp 0, 0.3f, -0.21f, // b1 anchor 0, -0.01f, 0.1f // b2 anchor @@ -5995,7 +6039,7 @@ void SpazNode::CreateHair() { hair_ponytail_bottom_body_->SetDimensions(0.09f, 0.13f, 0, 0, 0, 0, 0.01f); hair_ponytail_bottom_joint_ = CreateFixedJoint( - hair_ponytail_top_body_.get(), hair_ponytail_bottom_body_.get(), 0, + hair_ponytail_top_body_.Get(), hair_ponytail_bottom_body_.Get(), 0, 0, // lin stiff/damp 0, 0, // ang stiff/damp 0, 0.01f, -0.1f, // b1 anchor @@ -6127,13 +6171,13 @@ void SpazNode::SetHaveBoxingGloves(bool val) { void SpazNode::SetIsAreaOfInterest(bool val) { // Create if need be. if (val && area_of_interest_ == nullptr) { - area_of_interest_ = g_graphics->camera()->NewAreaOfInterest(); + area_of_interest_ = g_base->graphics->camera()->NewAreaOfInterest(); UpdateAreaOfInterest(); } // Destroy if need be. if (!val && area_of_interest_) { - g_graphics->camera()->DeleteAreaOfInterest(area_of_interest_); + g_base->graphics->camera()->DeleteAreaOfInterest(area_of_interest_); area_of_interest_ = nullptr; } } @@ -6144,20 +6188,20 @@ void SpazNode::SetCurseDeathTime(millisecs_t val) { // Start ticking sound. if (curse_death_time_ != 0) { if (tick_play_id_ == 0xFFFFFFFF) { - AudioSource* s = g_audio->SourceBeginNew(); + base::AudioSource* s = g_base->audio->SourceBeginNew(); if (s) { s->SetLooping(true); const dReal* p_head = dGeomGetPosition(body_head_->geom()); s->SetPosition(p_head[0], p_head[1], p_head[2]); tick_play_id_ = - s->Play(g_assets->GetSound(SystemSoundID::kTickingCrazy)); + s->Play(g_base->assets->SysSound(base::SysSoundID::kTickingCrazy)); s->End(); } } } else { // Stop ticking sound. if (tick_play_id_ != 0xFFFFFFFF) { - g_audio->PushSourceStopSoundCall(tick_play_id_); + g_base->audio->PushSourceStopSoundCall(tick_play_id_); tick_play_id_ = 0xFFFFFFFF; } } @@ -6241,9 +6285,9 @@ void SpazNode::SetShattered(int val) { // Stop any sound we're making if we're shattering. if (!was_shattered) { - g_audio->PushSourceStopSoundCall(voice_play_id_); + g_base->audio->PushSourceStopSoundCall(voice_play_id_); if (tick_play_id_ != 0xFFFFFFFF) { - g_audio->PushSourceStopSoundCall(tick_play_id_); + g_base->audio->PushSourceStopSoundCall(tick_play_id_); tick_play_id_ = 0xFFFFFFFF; } } @@ -6258,7 +6302,7 @@ void SpazNode::SetDead(bool val) { // Lose our area-of-interest. if (area_of_interest_) { - g_graphics->camera()->DeleteAreaOfInterest(area_of_interest_); + g_base->graphics->camera()->DeleteAreaOfInterest(area_of_interest_); area_of_interest_ = nullptr; } @@ -6268,13 +6312,13 @@ void SpazNode::SetDead(bool val) { // Scream on death unless we're already doing our fall scream, // in which case we just keep on doing that. if (voice_play_id_ != fall_play_id_ - || !g_audio->IsSoundPlaying(fall_play_id_)) { - g_audio->PushSourceStopSoundCall(voice_play_id_); + || !g_base->audio->IsSoundPlaying(fall_play_id_)) { + g_base->audio->PushSourceStopSoundCall(voice_play_id_); // Only make sound if we're not shattered. if (!shattered_) { - if (Sound* sound = GetRandomMedia(death_sounds_)) { - if (AudioSource* source = g_audio->SourceBeginNew()) { + if (SceneSound* sound = GetRandomMedia(death_sounds_)) { + if (base::AudioSource* source = g_base->audio->SourceBeginNew()) { const dReal* p_head = dGeomGetPosition(body_head_->geom()); source->SetPosition(p_head[0], p_head[1], p_head[2]); voice_play_id_ = source->Play(sound->GetSoundData()); @@ -6284,7 +6328,7 @@ void SpazNode::SetDead(bool val) { } } if (tick_play_id_ != 0xFFFFFFFF) { - g_audio->PushSourceStopSoundCall(tick_play_id_); + g_base->audio->PushSourceStopSoundCall(tick_play_id_); tick_play_id_ = 0xFFFFFFFF; } } @@ -6533,10 +6577,10 @@ auto SpazNode::GetPositionCenter() const -> std::vector { } auto SpazNode::GetPunchPosition() const -> std::vector { - if (!body_punch_.exists()) { + if (!body_punch_.Exists()) { BA_LOG_PYTHON_TRACE_ONCE( "WARNING: querying spaz punch_position without punch body"); - return std::vector(3, 0.0f); + return {0.0f, 0.0f, 0.0f}; } std::vector vals(3); const dReal* p = dGeomGetPosition(body_punch_->geom()); @@ -6547,10 +6591,10 @@ auto SpazNode::GetPunchPosition() const -> std::vector { } auto SpazNode::GetPunchVelocity() const -> std::vector { - if (!body_punch_.exists()) { + if (!body_punch_.Exists()) { BA_LOG_PYTHON_TRACE_ONCE( "WARNING: querying spaz punch_velocity without punch body"); - return std::vector(3, 0.0f); + return {0.0f, 0.0f, 0.0f}; } std::vector vals(3); const dReal* p = dGeomGetPosition(body_punch_->geom()); @@ -6565,10 +6609,10 @@ auto SpazNode::GetPunchVelocity() const -> std::vector { } auto SpazNode::GetPunchMomentumLinear() const -> std::vector { - if (!body_punch_.exists()) { + if (!body_punch_.Exists()) { BA_LOG_PYTHON_TRACE_ONCE( "WARNING: querying spaz punch_velocity without punch body"); - return std::vector(3, 0.0f); + return {0.0f, 0.0f, 0.0f}; } std::vector vals(3); @@ -6650,9 +6694,9 @@ void SpazNode::SetHoldNode(Node* val) { assert(a && b); { - g_audio->PushSourceStopSoundCall(voice_play_id_); - if (Sound* sound = GetRandomMedia(pickup_sounds_)) { - if (AudioSource* source = g_audio->SourceBeginNew()) { + g_base->audio->PushSourceStopSoundCall(voice_play_id_); + if (SceneSound* sound = GetRandomMedia(pickup_sounds_)) { + if (auto* source = g_base->audio->SourceBeginNew()) { const dReal* p_head = dGeomGetPosition(body_head_->geom()); source->SetPosition(p_head[0], p_head[1], p_head[2]); voice_play_id_ = source->Play(sound->GetSoundData()); @@ -6708,7 +6752,7 @@ void SpazNode::SetHoldNode(Node* val) { dJointCreateFixed(scene()->dynamics()->ode_world(), nullptr)); pickup_joint_.SetJoint(j, scene()); - pickup_joint_.AttachToBodies(body_torso_.get(), b); + pickup_joint_.AttachToBodies(body_torso_.Get(), b); dJointSetFixed(j); dJointSetFixedSpringMode(j, 1, 1, true); dJointSetFixedAnchor(j, 0, hold_height, hold_forward, false); @@ -6745,39 +6789,39 @@ void SpazNode::SetHoldNode(Node* val) { } } -auto SpazNode::GetJumpSounds() const -> std::vector { +auto SpazNode::GetJumpSounds() const -> std::vector { return RefsToPointers(jump_sounds_); } -void SpazNode::SetJumpSounds(const std::vector& vals) { +void SpazNode::SetJumpSounds(const std::vector& vals) { jump_sounds_ = PointersToRefs(vals); } -auto SpazNode::GetAttackSounds() const -> std::vector { +auto SpazNode::GetAttackSounds() const -> std::vector { return RefsToPointers(attack_sounds_); } -void SpazNode::SetAttackSounds(const std::vector& vals) { +void SpazNode::SetAttackSounds(const std::vector& vals) { attack_sounds_ = PointersToRefs(vals); } -auto SpazNode::GetImpactSounds() const -> std::vector { +auto SpazNode::GetImpactSounds() const -> std::vector { return RefsToPointers(impact_sounds_); } -void SpazNode::SetImpactSounds(const std::vector& vals) { +void SpazNode::SetImpactSounds(const std::vector& vals) { impact_sounds_ = PointersToRefs(vals); } -auto SpazNode::GetDeathSounds() const -> std::vector { +auto SpazNode::GetDeathSounds() const -> std::vector { return RefsToPointers(death_sounds_); } -void SpazNode::SetDeathSounds(const std::vector& vals) { +void SpazNode::SetDeathSounds(const std::vector& vals) { death_sounds_ = PointersToRefs(vals); } -auto SpazNode::GetPickupSounds() const -> std::vector { +auto SpazNode::GetPickupSounds() const -> std::vector { return RefsToPointers(pickup_sounds_); } -void SpazNode::SetPickupSounds(const std::vector& vals) { +void SpazNode::SetPickupSounds(const std::vector& vals) { pickup_sounds_ = PointersToRefs(vals); } -void SpazNode::SetFallSounds(const std::vector& vals) { +void SpazNode::SetFallSounds(const std::vector& vals) { fall_sounds_ = PointersToRefs(vals); } @@ -6802,10 +6846,10 @@ void SpazNode::PlayHurtSound() { if (dead_ || invincible_) { return; } - if (Sound* sound = GetRandomMedia(impact_sounds_)) { - if (AudioSource* source = g_audio->SourceBeginNew()) { + if (SceneSound* sound = GetRandomMedia(impact_sounds_)) { + if (auto* source = g_base->audio->SourceBeginNew()) { const dReal* p_top = dGeomGetPosition(body_head_->geom()); - g_audio->PushSourceStopSoundCall(voice_play_id_); + g_base->audio->PushSourceStopSoundCall(voice_play_id_); source->SetPosition(p_top[0], p_top[1], p_top[2]); voice_play_id_ = source->Play(sound->GetSoundData()); source->End(); @@ -6813,4 +6857,4 @@ void SpazNode::PlayHurtSound() { } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/spaz_node.h b/src/ballistica/scene_v1/node/spaz_node.h similarity index 75% rename from src/ballistica/scene/node/spaz_node.h rename to src/ballistica/scene_v1/node/spaz_node.h index 4133c127..b20529f7 100644 --- a/src/ballistica/scene/node/spaz_node.h +++ b/src/ballistica/scene_v1/node/spaz_node.h @@ -1,17 +1,17 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_SPAZ_NODE_H_ -#define BALLISTICA_SCENE_NODE_SPAZ_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_SPAZ_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_SPAZ_NODE_H_ #include #include -#include "ballistica/dynamics/part.h" -#include "ballistica/graphics/renderer.h" -#include "ballistica/logic/player.h" -#include "ballistica/scene/node/node.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/scene_v1/dynamics/part.h" +#include "ballistica/scene_v1/node/node.h" +#include "ballistica/scene_v1/support/player.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Current player character spaz node. class SpazNode : public Node { @@ -21,7 +21,7 @@ class SpazNode : public Node { ~SpazNode() override; void Step() override; void HandleMessage(const char* data) override; - void Draw(FrameDef* frame_def) override; + void Draw(base::FrameDef* frame_def) override; auto GetRigidBody(int id) -> RigidBody* override; void GetRigidBodyPickupLocations(int id, float* obj, float* character, float* hand1, float* hand2) override; @@ -52,22 +52,22 @@ class SpazNode : public Node { void set_name(const std::string& val) { name_ = val; } auto counter_text() const -> std::string { return counter_text_; } void set_counter_text(const std::string& val) { counter_text_ = val; } - auto mini_billboard_1_texture() const -> Texture* { - return mini_billboard_1_texture_.get(); + auto mini_billboard_1_texture() const -> SceneTexture* { + return mini_billboard_1_texture_.Get(); } - void set_mini_billboard_1_texture(Texture* val) { + void set_mini_billboard_1_texture(SceneTexture* val) { mini_billboard_1_texture_ = val; } - auto mini_billboard_2_texture() const -> Texture* { - return mini_billboard_2_texture_.get(); + auto mini_billboard_2_texture() const -> SceneTexture* { + return mini_billboard_2_texture_.Get(); } - void set_mini_billboard_2_texture(Texture* val) { + void set_mini_billboard_2_texture(SceneTexture* val) { mini_billboard_2_texture_ = val; } - auto mini_billboard_3_texture() const -> Texture* { - return mini_billboard_3_texture_.get(); + auto mini_billboard_3_texture() const -> SceneTexture* { + return mini_billboard_3_texture_.Get(); } - void set_mini_billboard_3_texture(Texture* val) { + void set_mini_billboard_3_texture(SceneTexture* val) { mini_billboard_3_texture_ = val; } auto mini_billboard_1_start_time() const -> millisecs_t { @@ -106,14 +106,16 @@ class SpazNode : public Node { void set_mini_billboard_3_end_time(millisecs_t val) { mini_billboard_3_end_time_ = val; } - auto billboard_texture() const -> Texture* { - return billboard_texture_.get(); + auto billboard_texture() const -> SceneTexture* { + return billboard_texture_.Get(); } - void set_billboard_texture(Texture* val) { billboard_texture_ = val; } + void set_billboard_texture(SceneTexture* val) { billboard_texture_ = val; } auto billboard_opacity() const -> float { return billboard_opacity_; } void set_billboard_opacity(float val) { billboard_opacity_ = val; } - auto counter_texture() const -> Texture* { return counter_texture_.get(); } - void set_counter_texture(Texture* val) { counter_texture_ = val; } + auto counter_texture() const -> SceneTexture* { + return counter_texture_.Get(); + } + void set_counter_texture(SceneTexture* val) { counter_texture_ = val; } auto invincible() const -> bool { return invincible_; } void set_invincible(bool val) { invincible_ = val; } auto name_color() const -> std::vector { return name_color_; } @@ -128,7 +130,7 @@ class SpazNode : public Node { return boxing_gloves_flashing_; } void set_boxing_gloves_flashing(bool val) { boxing_gloves_flashing_ = val; } - auto source_player() const -> Player* { return source_player_.get(); } + auto source_player() const -> Player* { return source_player_.Get(); } void set_source_player(Player* val) { source_player_ = val; } auto frozen() const -> bool { return frozen_; } void SetFrozen(bool val); @@ -165,46 +167,46 @@ class SpazNode : public Node { auto GetPosition() const -> std::vector; auto hold_body() const -> int { return hold_body_; } void set_hold_body(int val) { hold_body_ = val; } - auto hold_node() const -> Node* { return hold_node_.get(); } + auto hold_node() const -> Node* { return hold_node_.Get(); } void SetHoldNode(Node* val); - auto GetJumpSounds() const -> std::vector; - void SetJumpSounds(const std::vector& vals); - auto GetAttackSounds() const -> std::vector; - void SetAttackSounds(const std::vector& vals); - auto GetImpactSounds() const -> std::vector; - void SetImpactSounds(const std::vector& vals); - auto GetDeathSounds() const -> std::vector; - void SetDeathSounds(const std::vector& vals); - auto GetPickupSounds() const -> std::vector; - void SetPickupSounds(const std::vector& vals); - auto GetFallSounds() const -> std::vector { + auto GetJumpSounds() const -> std::vector; + void SetJumpSounds(const std::vector& vals); + auto GetAttackSounds() const -> std::vector; + void SetAttackSounds(const std::vector& vals); + auto GetImpactSounds() const -> std::vector; + void SetImpactSounds(const std::vector& vals); + auto GetDeathSounds() const -> std::vector; + void SetDeathSounds(const std::vector& vals); + auto GetPickupSounds() const -> std::vector; + void SetPickupSounds(const std::vector& vals); + auto GetFallSounds() const -> std::vector { return RefsToPointers(fall_sounds_); } - void SetFallSounds(const std::vector& vals); - auto color_texture() const -> Texture* { return color_texture_.get(); } - void set_color_texture(Texture* val) { color_texture_ = val; } - auto color_mask_texture() const -> Texture* { - return color_mask_texture_.get(); + void SetFallSounds(const std::vector& vals); + auto color_texture() const -> SceneTexture* { return color_texture_.Get(); } + void set_color_texture(SceneTexture* val) { color_texture_ = val; } + auto color_mask_texture() const -> SceneTexture* { + return color_mask_texture_.Get(); } - void set_color_mask_texture(Texture* val) { color_mask_texture_ = val; } - auto head_model() const -> Model* { return head_model_.get(); } - void set_head_model(Model* val) { head_model_ = val; } - auto torso_model() const -> Model* { return torso_model_.get(); } - void set_torso_model(Model* val) { torso_model_ = val; } - auto pelvis_model() const -> Model* { return pelvis_model_.get(); } - void set_pelvis_model(Model* val) { pelvis_model_ = val; } - auto upper_arm_model() const -> Model* { return upper_arm_model_.get(); } - void set_upper_arm_model(Model* val) { upper_arm_model_ = val; } - auto forearm_model() const -> Model* { return forearm_model_.get(); } - void set_forearm_model(Model* val) { forearm_model_ = val; } - auto hand_model() const -> Model* { return hand_model_.get(); } - void set_hand_model(Model* val) { hand_model_ = val; } - auto upper_leg_model() const -> Model* { return upper_leg_model_.get(); } - void set_upper_leg_model(Model* val) { upper_leg_model_ = val; } - auto lower_leg_model() const -> Model* { return lower_leg_model_.get(); } - void set_lower_leg_model(Model* val) { lower_leg_model_ = val; } - auto toes_model() const -> Model* { return toes_model_.get(); } - void set_toes_model(Model* val) { toes_model_ = val; } + void set_color_mask_texture(SceneTexture* val) { color_mask_texture_ = val; } + auto head_mesh() const -> SceneMesh* { return head_mesh_.Get(); } + void set_head_mesh(SceneMesh* val) { head_mesh_ = val; } + auto torso_mesh() const -> SceneMesh* { return torso_mesh_.Get(); } + void set_torso_mesh(SceneMesh* val) { torso_mesh_ = val; } + auto pelvis_mesh() const -> SceneMesh* { return pelvis_mesh_.Get(); } + void set_pelvis_mesh(SceneMesh* val) { pelvis_mesh_ = val; } + auto upper_arm_mesh() const -> SceneMesh* { return upper_arm_mesh_.Get(); } + void set_upper_arm_mesh(SceneMesh* val) { upper_arm_mesh_ = val; } + auto forearm_mesh() const -> SceneMesh* { return forearm_mesh_.Get(); } + void set_forearm_mesh(SceneMesh* val) { forearm_mesh_ = val; } + auto hand_mesh() const -> SceneMesh* { return hand_mesh_.Get(); } + void set_hand_mesh(SceneMesh* val) { hand_mesh_ = val; } + auto upper_leg_mesh() const -> SceneMesh* { return upper_leg_mesh_.Get(); } + void set_upper_leg_mesh(SceneMesh* val) { upper_leg_mesh_ = val; } + auto lower_leg_mesh() const -> SceneMesh* { return lower_leg_mesh_.Get(); } + void set_lower_leg_mesh(SceneMesh* val) { lower_leg_mesh_ = val; } + auto toes_mesh() const -> SceneMesh* { return toes_mesh_.Get(); } + void set_toes_mesh(SceneMesh* val) { toes_mesh_ = val; } auto billboard_cross_out() const -> bool { return billboard_cross_out_; } void set_billboard_cross_out(bool val) { billboard_cross_out_ = val; } auto jump_pressed() const -> bool { return jump_pressed_; } @@ -248,13 +250,15 @@ class SpazNode : public Node { kLowerRightArmJointBroken = 1u << 9u }; void PlayHurtSound(); - void DrawBodyParts(ObjectComponent* c, bool shading, float death_fade, + void DrawBodyParts(base::ObjectComponent* c, bool shading, float death_fade, float death_scale, float* add_color); - void SetupEyeLidShading(ObjectComponent* c, float death_fade, + void SetupEyeLidShading(base::ObjectComponent* c, float death_fade, float* add_color); - void DrawEyeLids(RenderComponent* c, float death_fade, float death_scale); - void DrawEyeBalls(RenderComponent* c, ObjectComponent* oc, bool shading, - float death_fade, float death_scale, float* add_color); + void DrawEyeLids(base::RenderComponent* c, float death_fade, + float death_scale); + void DrawEyeBalls(base::RenderComponent* c, base::ObjectComponent* oc, + bool shading, float death_fade, float death_scale, + float* add_color); void DoFlyPress(); // Create a fixed joint between two bodies. @@ -273,8 +277,8 @@ class SpazNode : public Node { // Reset to a standing, non-moving state at the given point. void Stand(float x, float y, float z, float angle); - void OnGraphicsQualityChanged(GraphicsQuality q) override; - void UpdateForGraphicsQuality(GraphicsQuality q); + void OnGraphicsQualityChanged(base::GraphicsQuality q) override; + void UpdateForGraphicsQuality(base::GraphicsQuality q); void UpdateAreaOfInterest(); auto CollideCallback(dContact* c, int count, RigidBody* colliding_body, RigidBody* opposingbody) -> bool; @@ -305,40 +309,40 @@ class SpazNode : public Node { float pickup_q2_[4]{0.0f, 0.0f, 0.0f, 0.0f}; uint32_t step_count_{}; millisecs_t birth_time_{}; - Object::Ref color_texture_; - Object::Ref color_mask_texture_; - Object::Ref head_model_; - Object::Ref torso_model_; - Object::Ref pelvis_model_; - Object::Ref upper_arm_model_; - Object::Ref forearm_model_; - Object::Ref hand_model_; - Object::Ref upper_leg_model_; - Object::Ref lower_leg_model_; - Object::Ref toes_model_; - std::vector > jump_sounds_; - std::vector > attack_sounds_; - std::vector > impact_sounds_; - std::vector > death_sounds_; - std::vector > pickup_sounds_; - std::vector > fall_sounds_; + Object::Ref color_texture_; + Object::Ref color_mask_texture_; + Object::Ref head_mesh_; + Object::Ref torso_mesh_; + Object::Ref pelvis_mesh_; + Object::Ref upper_arm_mesh_; + Object::Ref forearm_mesh_; + Object::Ref hand_mesh_; + Object::Ref upper_leg_mesh_; + Object::Ref lower_leg_mesh_; + Object::Ref toes_mesh_; + std::vector > jump_sounds_; + std::vector > attack_sounds_; + std::vector > impact_sounds_; + std::vector > death_sounds_; + std::vector > pickup_sounds_; + std::vector > fall_sounds_; Object::WeakRef hold_node_; std::string style_{"spaz"}; Object::WeakRef source_player_; bool clamp_move_values_to_circle_{true}; bool demo_mode_{}; std::string curse_timer_txt_; - TextGroup curse_timer_text_group_; + base::TextGroup curse_timer_text_group_; std::string counter_mesh_text_; - TextGroup counter_text_group_; + base::TextGroup counter_text_group_; std::string counter_text_; std::vector name_color_{1.0f, 1.0f, 1.0f}; std::string name_; std::string name_mesh_txt_; - TextGroup name_text_group_; - MeshIndexedSimpleFull billboard_1_mesh_; - MeshIndexedSimpleFull billboard_2_mesh_; - MeshIndexedSimpleFull billboard_3_mesh_; + base::TextGroup name_text_group_; + base::MeshIndexedSimpleFull billboard_1_mesh_; + base::MeshIndexedSimpleFull billboard_2_mesh_; + base::MeshIndexedSimpleFull billboard_3_mesh_; float punch_power_{}; float impact_damage_accum_{}; Part spaz_part_; @@ -368,17 +372,17 @@ class SpazNode : public Node { millisecs_t last_head_collide_time_{}; millisecs_t last_external_impulse_time_{}; millisecs_t last_impact_damage_dispatch_time_{}; - Object::Ref billboard_texture_; + Object::Ref billboard_texture_; float billboard_opacity_{}; float area_of_interest_radius_{5.0f}; - Object::Ref counter_texture_; - Object::Ref mini_billboard_1_texture_; + Object::Ref counter_texture_; + Object::Ref mini_billboard_1_texture_; millisecs_t mini_billboard_1_start_time_{}; millisecs_t mini_billboard_1_end_time_{}; - Object::Ref mini_billboard_2_texture_; + Object::Ref mini_billboard_2_texture_; millisecs_t mini_billboard_2_start_time_{}; millisecs_t mini_billboard_2_end_time_{}; - Object::Ref mini_billboard_3_texture_; + Object::Ref mini_billboard_3_texture_; millisecs_t mini_billboard_3_start_time_{}; millisecs_t mini_billboard_3_end_time_{}; millisecs_t curse_death_time_{}; @@ -396,7 +400,7 @@ class SpazNode : public Node { uint32_t tick_play_id_{0xFFFFFFFF}; millisecs_t last_fall_time_{}; uint32_t fall_play_id_{}; - AreaOfInterest* area_of_interest_{}; + base::AreaOfInterest* area_of_interest_{}; millisecs_t celebrate_until_time_left_{}; millisecs_t celebrate_until_time_right_{}; millisecs_t last_fly_time_{}; @@ -565,6 +569,6 @@ class SpazNode : public Node { millisecs_t death_time_{}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_SPAZ_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_SPAZ_NODE_H_ diff --git a/src/ballistica/scene/node/terrain_node.cc b/src/ballistica/scene_v1/node/terrain_node.cc similarity index 65% rename from src/ballistica/scene/node/terrain_node.cc rename to src/ballistica/scene_v1/node/terrain_node.cc index b8b831ad..eb7faec2 100644 --- a/src/ballistica/scene/node/terrain_node.cc +++ b/src/ballistica/scene_v1/node/terrain_node.cc @@ -1,16 +1,17 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/terrain_node.h" +#include "ballistica/scene_v1/node/terrain_node.h" -#include "ballistica/assets/component/collide_model.h" -#include "ballistica/dynamics/bg/bg_dynamics.h" -#include "ballistica/dynamics/material/material.h" -#include "ballistica/graphics/component/object_component.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/dynamics/bg/bg_dynamics.h" +#include "ballistica/base/graphics/component/object_component.h" +#include "ballistica/core/core.h" +#include "ballistica/scene_v1/assets/scene_collision_mesh.h" +#include "ballistica/scene_v1/dynamics/material/material.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/scene_v1/support/scene.h" -namespace ballistica { +namespace ballistica::scene_v1 { class TerrainNodeType : public NodeType { public: @@ -31,10 +32,10 @@ class TerrainNodeType : public NodeType { BA_FLOAT_ARRAY_ATTR(reflection_scale, reflection_scale, SetReflectionScale); BA_BOOL_ATTR(lighting, getLighting, setLighting); BA_FLOAT_ARRAY_ATTR(color, color, SetColor); - BA_MODEL_ATTR(model, model, SetModel); + BA_MESH_ATTR(mesh, mesh, set_mesh); BA_TEXTURE_ATTR(color_texture, color_texture, SetColorTexture); - BA_COLLIDE_MODEL_ATTR(collide_model, collide_model, SetCollideModel); - BA_MATERIAL_ARRAY_ATTR(materials, GetMaterials, SetMaterials); + BA_COLLISION_MESH_ATTR(collision_mesh, collision_mesh, set_collision_mesh); + BA_MATERIAL_ARRAY_ATTR(materials, materials, set_materials); BA_BOOL_ATTR(vr_only, vr_only, set_vr_only); #undef BA_NODE_TYPE_CLASS @@ -51,9 +52,9 @@ class TerrainNodeType : public NodeType { reflection_scale(this), lighting(this), color(this), - model(this), + mesh(this), color_texture(this), - collide_model(this), + collision_mesh(this), materials(this), vr_only(this) {} }; @@ -75,8 +76,8 @@ TerrainNode::TerrainNode(Scene* scene) lighting_(true), bumper_(false), affect_bg_dynamics_(true), - bg_dynamics_collide_model_(nullptr), - reflection_(ReflectionType::kNone), + bg_dynamics_collision_mesh_(nullptr), + reflection_(base::ReflectionType::kNone), reflection_scale_(3, 1.0f), reflection_scale_r_(1.0f), reflection_scale_g_(1.0f), @@ -93,43 +94,45 @@ TerrainNode::~TerrainNode() { scene()->decrement_bg_cover_count(); RemoveFromBGDynamics(); - // If we've got a collide-model, this is a good time to mark + // If we've got a collision-mesh, this is a good time to mark // it as used since it may be getting opened up to pruning // without our reference. - if (collide_model_.exists()) { - collide_model_->collide_model_data()->set_last_used_time(GetRealTime()); + if (collision_mesh_.Exists()) { + collision_mesh_->collision_mesh_data()->set_last_used_time( + g_core->GetAppTimeMillisecs()); } } -auto TerrainNode::GetMaterials() const -> std::vector { +auto TerrainNode::materials() const -> std::vector { return RefsToPointers(materials_); } -void TerrainNode::SetMaterials(const std::vector& vals) { +void TerrainNode::set_materials(const std::vector& vals) { materials_ = PointersToRefs(vals); terrain_part_.SetMaterials(vals); } -void TerrainNode::SetModel(Model* val) { model_ = val; } +void TerrainNode::set_mesh(SceneMesh* val) { mesh_ = val; } -void TerrainNode::SetCollideModel(CollideModel* val) { +void TerrainNode::set_collision_mesh(SceneCollisionMesh* val) { // if we had an old one, mark its last-used time so caching works properly.. - if (collide_model_.exists()) { - collide_model_->collide_model_data()->set_last_used_time(GetRealTime()); + if (collision_mesh_.Exists()) { + collision_mesh_->collision_mesh_data()->set_last_used_time( + g_core->GetAppTimeMillisecs()); } - collide_model_ = val; + collision_mesh_ = val; // remove any existing.. RemoveFromBGDynamics(); - if (collide_model_.exists()) { + if (collision_mesh_.Exists()) { uint32_t flags = bumper_ ? RigidBody::kIsBumper : 0; flags |= RigidBody::kIsTerrain; body_ = Object::New( 0, &terrain_part_, RigidBody::Type::kGeomOnly, RigidBody::Shape::kTrimesh, RigidBody::kCollideBackground, RigidBody::kCollideAll ^ RigidBody::kCollideBackground, - collide_model_.get(), flags); + collision_mesh_.Get(), flags); body_->set_can_cause_impact_damage(true); // also ship it to the BG-Dynamics thread.. @@ -141,7 +144,7 @@ void TerrainNode::SetCollideModel(CollideModel* val) { } } -void TerrainNode::SetColorTexture(Texture* val) { color_texture_ = val; } +void TerrainNode::SetColorTexture(SceneTexture* val) { color_texture_ = val; } void TerrainNode::SetReflectionScale(const std::vector& vals) { if (vals.size() != 1 && vals.size() != 3) { @@ -175,15 +178,15 @@ void TerrainNode::SetColor(const std::vector& vals) { } auto TerrainNode::GetReflection() const -> std::string { - return Graphics::StringFromReflectionType(reflection_); + return base::Graphics::StringFromReflectionType(reflection_); } void TerrainNode::SetReflection(const std::string& val) { - reflection_ = Graphics::ReflectionTypeFromString(val); + reflection_ = base::Graphics::ReflectionTypeFromString(val); } void TerrainNode::SetBumper(bool val) { bumper_ = val; - if (body_.exists()) { + if (body_.Exists()) { uint32_t is_bumper{RigidBody::kIsBumper}; if (bumper_) { body_->set_flags(body_->flags() | is_bumper); // on @@ -194,48 +197,49 @@ void TerrainNode::SetBumper(bool val) { } void TerrainNode::AddToBGDynamics() { - assert(bg_dynamics_collide_model_ == nullptr && collide_model_.exists() + assert(bg_dynamics_collision_mesh_ == nullptr && collision_mesh_.Exists() && !bumper_ && affect_bg_dynamics_); - bg_dynamics_collide_model_ = collide_model_.get(); + bg_dynamics_collision_mesh_ = collision_mesh_.Get(); #if !BA_HEADLESS_BUILD - g_bg_dynamics->AddTerrain(bg_dynamics_collide_model_->collide_model_data()); + g_base->bg_dynamics->AddTerrain( + bg_dynamics_collision_mesh_->collision_mesh_data()); #endif // !BA_HEADLESS_BUILD } void TerrainNode::RemoveFromBGDynamics() { - if (bg_dynamics_collide_model_ != nullptr) { + if (bg_dynamics_collision_mesh_ != nullptr) { #if !BA_HEADLESS_BUILD - g_bg_dynamics->RemoveTerrain( - bg_dynamics_collide_model_->collide_model_data()); + g_base->bg_dynamics->RemoveTerrain( + bg_dynamics_collision_mesh_->collision_mesh_data()); #endif // !BA_HEADLESS_BUILD - bg_dynamics_collide_model_ = nullptr; + bg_dynamics_collision_mesh_ = nullptr; } } -void TerrainNode::Draw(FrameDef* frame_def) { - if (!model_.exists()) { +void TerrainNode::Draw(base::FrameDef* frame_def) { + if (!mesh_.Exists()) { return; } - if (vr_only_ && !IsVRMode()) { + if (vr_only_ && !g_core->IsVRMode()) { return; } - ObjectComponent c(overlay_ ? frame_def->overlay_3d_pass() - : background_ ? frame_def->beauty_pass_bg() - : frame_def->beauty_pass()); + base::ObjectComponent c(overlay_ ? frame_def->overlay_3d_pass() + : background_ ? frame_def->beauty_pass_bg() + : frame_def->beauty_pass()); c.SetWorldSpace(true); - c.SetTexture(color_texture_); + c.SetTexture(color_texture_->texture_data()); if (lighting_) { - c.SetLightShadow(LightShadowType::kTerrain); + c.SetLightShadow(base::LightShadowType::kTerrain); } else { - c.SetLightShadow(LightShadowType::kNone); + c.SetLightShadow(base::LightShadowType::kNone); } - if (reflection_ != ReflectionType::kNone) { + if (reflection_ != base::ReflectionType::kNone) { c.SetReflection(reflection_); c.SetReflectionScale(reflection_scale_r_, reflection_scale_g_, reflection_scale_b_); } float opacity; - if (frame_def->quality() <= GraphicsQuality::kHigh + if (frame_def->quality() <= base::GraphicsQuality::kHigh && opacity_in_low_or_medium_quality_ >= 0.0f) { opacity = opacity_in_low_or_medium_quality_; } else { @@ -250,12 +254,12 @@ void TerrainNode::Draw(FrameDef* frame_def) { } else { c.SetColor(color_r_, color_g_, color_b_, 1.0f); } - uint32_t drawFlags = 0; + uint32_t draw_flags = 0; if (!visible_in_reflections_) { - drawFlags |= kModelDrawFlagNoReflection; + draw_flags |= base::kMeshDrawFlagNoReflection; } - c.DrawModel(model_->model_data(), drawFlags); + c.DrawMeshAsset(mesh_->mesh_data(), draw_flags); c.Submit(); } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/terrain_node.h b/src/ballistica/scene_v1/node/terrain_node.h similarity index 68% rename from src/ballistica/scene/node/terrain_node.h rename to src/ballistica/scene_v1/node/terrain_node.h index a8f2b87d..0350e693 100644 --- a/src/ballistica/scene/node/terrain_node.h +++ b/src/ballistica/scene_v1/node/terrain_node.h @@ -1,22 +1,22 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_TERRAIN_NODE_H_ -#define BALLISTICA_SCENE_NODE_TERRAIN_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_TERRAIN_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_TERRAIN_NODE_H_ #include #include -#include "ballistica/dynamics/part.h" -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/dynamics/part.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { class TerrainNode : public Node { public: static auto InitType() -> NodeType*; explicit TerrainNode(Scene* scene); ~TerrainNode() override; - void Draw(FrameDef* frame_def) override; + void Draw(base::FrameDef* frame_def) override; auto visible_in_reflections() const -> bool { return visible_in_reflections_; } @@ -47,21 +47,23 @@ class TerrainNode : public Node { void setLighting(bool val) { lighting_ = val; } auto color() const -> const std::vector& { return color_; } void SetColor(const std::vector& vals); - auto model() const -> Model* { return model_.get(); } - void SetModel(Model* m); - auto color_texture() const -> Texture* { return color_texture_.get(); } - void SetColorTexture(Texture* val); - auto collide_model() const -> CollideModel* { return collide_model_.get(); } - void SetCollideModel(CollideModel* val); - auto GetMaterials() const -> std::vector; - void SetMaterials(const std::vector& vals); + auto mesh() const -> SceneMesh* { return mesh_.Get(); } + void set_mesh(SceneMesh* m); + auto color_texture() const -> SceneTexture* { return color_texture_.Get(); } + void SetColorTexture(SceneTexture* val); + auto collision_mesh() const -> SceneCollisionMesh* { + return collision_mesh_.Get(); + } + void set_collision_mesh(SceneCollisionMesh* val); + auto materials() const -> std::vector; + void set_materials(const std::vector& vals); auto vr_only() const -> bool { return vr_only_; } void set_vr_only(bool val) { vr_only_ = val; } private: void AddToBGDynamics(); void RemoveFromBGDynamics(); - CollideModel* bg_dynamics_collide_model_; + SceneCollisionMesh* bg_dynamics_collision_mesh_; bool vr_only_; bool bumper_; bool affect_bg_dynamics_; @@ -70,20 +72,20 @@ class TerrainNode : public Node { bool overlay_; float opacity_; float opacity_in_low_or_medium_quality_; - Object::Ref model_; - Object::Ref collide_model_; - Object::Ref color_texture_; + Object::Ref mesh_; + Object::Ref collision_mesh_; + Object::Ref color_texture_; std::vector > materials_; Part terrain_part_; Object::Ref body_; bool visible_in_reflections_; - ReflectionType reflection_; + base::ReflectionType reflection_; std::vector reflection_scale_; float reflection_scale_r_, reflection_scale_g_, reflection_scale_b_; std::vector color_; float color_r_, color_g_, color_b_; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_TERRAIN_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_TERRAIN_NODE_H_ diff --git a/src/ballistica/scene/node/text_node.cc b/src/ballistica/scene_v1/node/text_node.cc similarity index 85% rename from src/ballistica/scene/node/text_node.cc rename to src/ballistica/scene_v1/node/text_node.cc index 7be19570..4b3c4349 100644 --- a/src/ballistica/scene/node/text_node.cc +++ b/src/ballistica/scene_v1/node/text_node.cc @@ -1,16 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/text_node.h" +#include "ballistica/scene_v1/node/text_node.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/graphics/text/text_graphics.h" -#include "ballistica/python/python.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/graphics/text/text_graphics.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python.h" -namespace ballistica { +namespace ballistica::scene_v1 { class TextNodeType : public NodeType { public: @@ -113,7 +113,8 @@ void TextNode::SetText(const std::string& val) { if (do_format_check) { bool valid; - g_logic->CompileResourceString(val, "setText format check", &valid); + g_base->assets->CompileResourceString(val, "setText format check", + &valid); if (!valid) { BA_LOG_ONCE(LogLevel::kError, "Invalid resource string: '" + val + "' on node '" + label() + "'"); @@ -310,21 +311,21 @@ void TextNode::Update() { offset_h = 0; break; case HAttach::kRight: - offset_h = g_graphics->screen_virtual_width(); + offset_h = g_base->graphics->screen_virtual_width(); break; case HAttach::kCenter: - offset_h = g_graphics->screen_virtual_width() / 2; + offset_h = g_base->graphics->screen_virtual_width() / 2; break; } switch (v_attach_) { case VAttach::kTop: - offset_v = g_graphics->screen_virtual_height(); + offset_v = g_base->graphics->screen_virtual_height(); break; case VAttach::kBottom: offset_v = 0; break; case VAttach::kCenter: - offset_v = g_graphics->screen_virtual_height() / 2; + offset_v = g_base->graphics->screen_virtual_height() / 2; break; } } @@ -338,18 +339,18 @@ void TextNode::Update() { } } -void TextNode::Draw(FrameDef* frame_def) { - if (client_only_ && context().GetHostSession()) { +void TextNode::Draw(base::FrameDef* frame_def) { + if (client_only_ && context_ref().GetHostSession()) { return; } - if (host_only_ && !context().GetHostSession()) { + if (host_only_ && !context_ref().GetHostSession()) { return; } // Apply subs/resources to get our actual text if need be. if (text_translation_dirty_) { text_translated_ = - g_logic->CompileResourceString(text_raw_, "TextNode::OnDraw"); + g_base->assets->CompileResourceString(text_raw_, "TextNode::OnDraw"); text_translation_dirty_ = false; text_group_dirty_ = true; text_width_dirty_ = true; @@ -362,15 +363,15 @@ void TextNode::Draw(FrameDef* frame_def) { // recalc our text width if need be.. if (text_width_dirty_) { text_width_ = - g_text_graphics->GetStringWidth(text_translated_.c_str(), big_); + g_base->text_graphics->GetStringWidth(text_translated_.c_str(), big_); text_width_dirty_ = false; } - bool vr_2d_text = (IsVRMode() && !in_world_); + bool vr_2d_text = (g_core->IsVRMode() && !in_world_); // in vr mode we use the fixed overlay position if our scene is set for // that - bool vr_use_fixed = (IsVRMode() && scene()->use_fixed_vr_overlay()); + bool vr_use_fixed = (g_core->IsVRMode() && scene()->use_fixed_vr_overlay()); // FIXME - in VR, fixed and front are currently mutually exclusive; need to // implement that. @@ -380,39 +381,39 @@ void TextNode::Draw(FrameDef* frame_def) { // make sure we're up to date Update(); - RenderPass& pass(*(in_world_ - ? frame_def->overlay_3d_pass() - : (vr_use_fixed ? frame_def->GetOverlayFixedPass() - : front_ ? frame_def->overlay_front_pass() - : frame_def->overlay_pass()))); + base::RenderPass& pass( + *(in_world_ ? frame_def->overlay_3d_pass() + : (vr_use_fixed ? frame_def->GetOverlayFixedPass() + : front_ ? frame_def->overlay_front_pass() + : frame_def->overlay_pass()))); if (big_) { if (text_group_dirty_) { - TextMesh::HAlign h_align; + base::TextMesh::HAlign h_align; switch (h_align_) { case HAlign::kLeft: - h_align = TextMesh::HAlign::kLeft; + h_align = base::TextMesh::HAlign::kLeft; break; case HAlign::kRight: - h_align = TextMesh::HAlign::kRight; + h_align = base::TextMesh::HAlign::kRight; break; case HAlign::kCenter: - h_align = TextMesh::HAlign::kCenter; + h_align = base::TextMesh::HAlign::kCenter; break; } - TextMesh::VAlign v_align; + base::TextMesh::VAlign v_align; switch (v_align_) { case VAlign::kNone: - v_align = TextMesh::VAlign::kNone; + v_align = base::TextMesh::VAlign::kNone; break; case VAlign::kCenter: - v_align = TextMesh::VAlign::kCenter; + v_align = base::TextMesh::VAlign::kCenter; break; case VAlign::kTop: - v_align = TextMesh::VAlign::kTop; + v_align = base::TextMesh::VAlign::kTop; break; case VAlign::kBottom: - v_align = TextMesh::VAlign::kBottom; + v_align = base::TextMesh::VAlign::kBottom; break; } @@ -421,7 +422,7 @@ void TextNode::Draw(FrameDef* frame_def) { text_group_dirty_ = false; } - float z = vr_2d_text ? 0.0f : g_graphics->overlay_node_z_depth(); + float z = vr_2d_text ? 0.0f : g_base->graphics->overlay_node_z_depth(); assert(!text_width_dirty_); float tx = position_final_[0]; @@ -431,7 +432,7 @@ void TextNode::Draw(FrameDef* frame_def) { // left/rigth shift from tilting the device if (tilt_translate_ != 0.0f) { - Vector3f tilt = g_graphics->tilt(); + Vector3f tilt = g_base->graphics->tilt(); tx_tilt = -tilt.y * tilt_translate_; ty_tilt = tilt.x * tilt_translate_; } @@ -469,7 +470,7 @@ void TextNode::Draw(FrameDef* frame_def) { + static_cast(i) * (project_scale_ - trail_project_scale_) / passes_f); - SimpleComponent c(&pass); + base::SimpleComponent c(&pass); c.SetTransparent(true); c.SetPremultiplied(true); c.SetColor(trail_color_[0] * o, trail_color_[1] * o, @@ -480,7 +481,7 @@ void TextNode::Draw(FrameDef* frame_def) { int elem_count = text_group_.GetElementCount(); for (int e = 0; e < elem_count; e++) { // gracefully skip unloaded textures.. - TextureData* t = text_group_.GetElementTexture(e); + base::TextureAsset* t = text_group_.GetElementTexture(e); if (!t->preloaded()) continue; c.SetTexture(t); c.SetMaskUV2Texture(text_group_.GetElementMaskUV2Texture(e)); @@ -506,7 +507,7 @@ void TextNode::Draw(FrameDef* frame_def) { } } - SimpleComponent c(&pass); + base::SimpleComponent c(&pass); c.SetTransparent(true); c.SetColor(color_[0], color_[1], color_[2], color_[3] * opacity_); @@ -514,7 +515,7 @@ void TextNode::Draw(FrameDef* frame_def) { bool did_submit = false; for (int e = 0; e < elem_count; e++) { // Gracefully skip unloaded textures. - TextureData* t = text_group_.GetElementTexture(e); + base::TextureAsset* t = text_group_.GetElementTexture(e); if (!t->preloaded()) continue; c.SetTexture(t); float shadow_opacity = shadow_; @@ -558,32 +559,32 @@ void TextNode::Draw(FrameDef* frame_def) { } else { // small text if (text_group_dirty_) { - TextMesh::HAlign h_align; + base::TextMesh::HAlign h_align; switch (h_align_) { case HAlign::kLeft: - h_align = TextMesh::HAlign::kLeft; + h_align = base::TextMesh::HAlign::kLeft; break; case HAlign::kRight: - h_align = TextMesh::HAlign::kRight; + h_align = base::TextMesh::HAlign::kRight; break; case HAlign::kCenter: - h_align = TextMesh::HAlign::kCenter; + h_align = base::TextMesh::HAlign::kCenter; break; } - TextMesh::VAlign v_align; + base::TextMesh::VAlign v_align; switch (v_align_) { case VAlign::kNone: - v_align = TextMesh::VAlign::kNone; + v_align = base::TextMesh::VAlign::kNone; break; case VAlign::kCenter: - v_align = TextMesh::VAlign::kCenter; + v_align = base::TextMesh::VAlign::kCenter; break; case VAlign::kTop: - v_align = TextMesh::VAlign::kTop; + v_align = base::TextMesh::VAlign::kTop; break; case VAlign::kBottom: - v_align = TextMesh::VAlign::kBottom; + v_align = base::TextMesh::VAlign::kBottom; break; } @@ -591,9 +592,10 @@ void TextNode::Draw(FrameDef* frame_def) { text_group_.SetText(text_translated_, h_align, v_align); text_group_dirty_ = false; } - float z = vr_2d_text ? 0.0f - : (in_world_ ? position_final_[2] - : g_graphics->overlay_node_z_depth()); + float z = vr_2d_text + ? 0.0f + : (in_world_ ? position_final_[2] + : g_base->graphics->overlay_node_z_depth()); assert(!text_width_dirty_); float extrascale; @@ -603,13 +605,13 @@ void TextNode::Draw(FrameDef* frame_def) { extrascale = 1.0f; } - SimpleComponent c(&pass); + base::SimpleComponent c(&pass); c.SetTransparent(true); float fin_a = color_[3] * opacity_; int elem_count = text_group_.GetElementCount(); for (int e = 0; e < elem_count; e++) { // Gracefully skip unloaded textures. - TextureData* t = text_group_.GetElementTexture(e); + base::TextureAsset* t = text_group_.GetElementTexture(e); if (!t->preloaded()) continue; c.SetTexture(t); float shadow_opacity = shadow_; @@ -630,7 +632,7 @@ void TextNode::Draw(FrameDef* frame_def) { } else { c.SetColor(1, 1, 1, fin_a); } - if (IsVRMode()) { + if (g_core->IsVRMode()) { c.SetFlatness(text_group_.GetElementMaxFlatness(e)); } else { c.SetFlatness( @@ -656,4 +658,4 @@ void TextNode::OnLanguageChange() { text_translation_dirty_ = true; } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/text_node.h b/src/ballistica/scene_v1/node/text_node.h similarity index 91% rename from src/ballistica/scene/node/text_node.h rename to src/ballistica/scene_v1/node/text_node.h index 2b9a7ae3..22593a01 100644 --- a/src/ballistica/scene/node/text_node.h +++ b/src/ballistica/scene_v1/node/text_node.h @@ -1,22 +1,22 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_TEXT_NODE_H_ -#define BALLISTICA_SCENE_NODE_TEXT_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_TEXT_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_TEXT_NODE_H_ #include #include -#include "ballistica/graphics/renderer.h" -#include "ballistica/scene/node/node.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { class TextNode : public Node { public: static auto InitType() -> NodeType*; explicit TextNode(Scene* scene); ~TextNode() override; - void Draw(FrameDef* frame_def) override; + void Draw(base::FrameDef* frame_def) override; void OnLanguageChange() override; void OnScreenSizeChange() override; auto opacity() const -> float { return opacity_; } @@ -81,7 +81,7 @@ class TextNode : public Node { enum class HAttach { kLeft, kCenter, kRight }; enum class VAttach { kTop, kCenter, kBottom }; void Update(); - TextGroup text_group_; + base::TextGroup text_group_; bool text_group_dirty_ = true; bool text_width_dirty_ = true; bool text_translation_dirty_ = true; @@ -117,6 +117,6 @@ class TextNode : public Node { float text_width_ = 0.0f; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_TEXT_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_TEXT_NODE_H_ diff --git a/src/ballistica/scene/node/texture_sequence_node.cc b/src/ballistica/scene_v1/node/texture_sequence_node.cc similarity index 78% rename from src/ballistica/scene/node/texture_sequence_node.cc rename to src/ballistica/scene_v1/node/texture_sequence_node.cc index e5bc8821..320ce8c7 100644 --- a/src/ballistica/scene/node/texture_sequence_node.cc +++ b/src/ballistica/scene_v1/node/texture_sequence_node.cc @@ -1,13 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/texture_sequence_node.h" +#include "ballistica/scene_v1/node/texture_sequence_node.h" -#include "ballistica/assets/component/texture.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" +#include "ballistica/scene_v1/assets/scene_texture.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" -namespace ballistica { +namespace ballistica::scene_v1 { class TextureSequenceNodeType : public NodeType { public: @@ -34,12 +33,12 @@ auto TextureSequenceNode::InitType() -> NodeType* { TextureSequenceNode::TextureSequenceNode(Scene* scene) : Node(scene, node_type), index_(0), rate_(1000), sleep_count_(0) {} -auto TextureSequenceNode::input_textures() const -> std::vector { +auto TextureSequenceNode::input_textures() const -> std::vector { return RefsToPointers(input_textures_); } void TextureSequenceNode::set_input_textures( - const std::vector& vals) { + const std::vector& vals) { input_textures_ = PointersToRefs(vals); // Make sure index_ doesnt go out of range. @@ -48,12 +47,12 @@ void TextureSequenceNode::set_input_textures( } } -auto TextureSequenceNode::output_texture() const -> Texture* { +auto TextureSequenceNode::output_texture() const -> SceneTexture* { if (input_textures_.empty()) { return nullptr; } assert(index_ < static_cast(input_textures_.size())); - return input_textures_[index_].get(); + return input_textures_[index_].Get(); } void TextureSequenceNode::Step() { @@ -73,4 +72,4 @@ void TextureSequenceNode::set_rate(int val) { } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/node/texture_sequence_node.h b/src/ballistica/scene_v1/node/texture_sequence_node.h new file mode 100644 index 00000000..5b15652b --- /dev/null +++ b/src/ballistica/scene_v1/node/texture_sequence_node.h @@ -0,0 +1,33 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_NODE_TEXTURE_SEQUENCE_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_TEXTURE_SEQUENCE_NODE_H_ + +#include + +#include "ballistica/scene_v1/node/node.h" +#include "ballistica/shared/ballistica.h" + +namespace ballistica::scene_v1 { + +class TextureSequenceNode : public Node { + public: + static auto InitType() -> NodeType*; + explicit TextureSequenceNode(Scene* scene); + void Step() override; + auto rate() const -> int { return rate_; } + void set_rate(int val); + auto input_textures() const -> std::vector; + void set_input_textures(const std::vector& vals); + auto output_texture() const -> SceneTexture*; + + private: + int sleep_count_{}; + int index_{}; + int rate_{}; + std::vector > input_textures_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_NODE_TEXTURE_SEQUENCE_NODE_H_ diff --git a/src/ballistica/scene/node/time_display_node.cc b/src/ballistica/scene_v1/node/time_display_node.cc similarity index 85% rename from src/ballistica/scene/node/time_display_node.cc rename to src/ballistica/scene_v1/node/time_display_node.cc index 17bc7ebf..7bbbb14c 100644 --- a/src/ballistica/scene/node/time_display_node.cc +++ b/src/ballistica/scene_v1/node/time_display_node.cc @@ -1,15 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/node/time_display_node.h" +#include "ballistica/scene_v1/node/time_display_node.h" #include -#include "ballistica/generic/utils.h" -#include "ballistica/logic/logic.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::scene_v1 { class TimeDisplayNodeType : public NodeType { public: @@ -45,13 +45,13 @@ TimeDisplayNode::TimeDisplayNode(Scene* scene) : Node(scene, node_type) {} TimeDisplayNode::~TimeDisplayNode() = default; auto TimeDisplayNode::GetOutput() -> std::string { - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (translations_dirty_) { - time_suffix_hours_ = - g_logic->CompileResourceString(R"({"r":"timeSuffixHoursText"})", "tda"); - time_suffix_minutes_ = g_logic->CompileResourceString( + time_suffix_hours_ = g_base->assets->CompileResourceString( + R"({"r":"timeSuffixHoursText"})", "tda"); + time_suffix_minutes_ = g_base->assets->CompileResourceString( R"({"r":"timeSuffixMinutesText"})", "tdb"); - time_suffix_seconds_ = g_logic->CompileResourceString( + time_suffix_seconds_ = g_base->assets->CompileResourceString( R"({"r":"timeSuffixSecondsText"})", "tdc"); translations_dirty_ = false; output_dirty_ = true; @@ -133,4 +133,4 @@ auto TimeDisplayNode::GetOutput() -> std::string { void TimeDisplayNode::OnLanguageChange() { translations_dirty_ = true; } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/node/time_display_node.h b/src/ballistica/scene_v1/node/time_display_node.h similarity index 85% rename from src/ballistica/scene/node/time_display_node.h rename to src/ballistica/scene_v1/node/time_display_node.h index 78cea7c9..2b236f4a 100644 --- a/src/ballistica/scene/node/time_display_node.h +++ b/src/ballistica/scene_v1/node/time_display_node.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_NODE_TIME_DISPLAY_NODE_H_ -#define BALLISTICA_SCENE_NODE_TIME_DISPLAY_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_NODE_TIME_DISPLAY_NODE_H_ +#define BALLISTICA_SCENE_V1_NODE_TIME_DISPLAY_NODE_H_ #include -#include "ballistica/scene/node/node.h" +#include "ballistica/scene_v1/node/node.h" -namespace ballistica { +namespace ballistica::scene_v1 { class TimeDisplayNode : public Node { public: @@ -66,6 +66,6 @@ class TimeDisplayNode : public Node { bool translations_dirty_ = true; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_NODE_TIME_DISPLAY_NODE_H_ +#endif // BALLISTICA_SCENE_V1_NODE_TIME_DISPLAY_NODE_H_ diff --git a/src/ballistica/python/class/python_class_activity_data.cc b/src/ballistica/scene_v1/python/class/python_class_activity_data.cc similarity index 52% rename from src/ballistica/python/class/python_class_activity_data.cc rename to src/ballistica/scene_v1/python/class/python_class_activity_data.cc index 14bd3ffc..fc6f0d0e 100644 --- a/src/ballistica/python/class/python_class_activity_data.cc +++ b/src/ballistica/scene_v1/python/class/python_class_activity_data.cc @@ -1,48 +1,55 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/python/class/python_class_activity_data.h" +#include "ballistica/scene_v1/python/class/python_class_activity_data.h" -#include "ballistica/core/thread.h" -#include "ballistica/generic/utils.h" -#include "ballistica/logic/host_activity.h" -#include "ballistica/logic/logic.h" -#include "ballistica/logic/session/host_session.h" -#include "ballistica/python/python.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/class/python_class_context_ref.h" +#include "ballistica/scene_v1/support/host_activity.h" +#include "ballistica/scene_v1/support/host_session.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python.h" -namespace ballistica { +namespace ballistica::scene_v1 { auto PythonClassActivityData::nb_bool(PythonClassActivityData* self) -> int { - return self->host_activity_->exists(); + return self->host_activity_->Exists(); } PyNumberMethods PythonClassActivityData::as_number_; -void PythonClassActivityData::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_name = "_ba.ActivityData"; - obj->tp_basicsize = sizeof(PythonClassActivityData); - obj->tp_doc = "(internal)"; - obj->tp_new = tp_new; - obj->tp_dealloc = (destructor)tp_dealloc; - obj->tp_repr = (reprfunc)tp_repr; - obj->tp_methods = tp_methods; +auto PythonClassActivityData::type_name() -> const char* { + return "ActivityData"; +} + +void PythonClassActivityData::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "bascenev1.ActivityData"; + cls->tp_basicsize = sizeof(PythonClassActivityData); + cls->tp_doc = "(internal)"; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_methods = tp_methods; // We provide number methods only for bool functionality. memset(&as_number_, 0, sizeof(as_number_)); as_number_.nb_bool = (inquiry)nb_bool; - obj->tp_as_number = &as_number_; + cls->tp_as_number = &as_number_; } auto PythonClassActivityData::Create(HostActivity* host_activity) -> PyObject* { + assert(TypeIsSetUp(&type_obj)); auto* py_activity_data = reinterpret_cast( PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); BA_PRECONDITION(py_activity_data); - *(py_activity_data->host_activity_) = host_activity; + *py_activity_data->host_activity_ = host_activity; return reinterpret_cast(py_activity_data); } auto PythonClassActivityData::GetHostActivity() const -> HostActivity* { - HostActivity* host_activity = host_activity_->get(); + HostActivity* host_activity = host_activity_->Get(); if (!host_activity) throw Exception( "Invalid ActivityData; this activity has probably been expired and " @@ -55,7 +62,7 @@ auto PythonClassActivityData::tp_repr(PythonClassActivityData* self) BA_PYTHON_TRY; return Py_BuildValue( "s", (std::string("host_activity_->get()) + " >") + + Utils::PtrToString(self->host_activity_->Get()) + " >") .c_str()); BA_PYTHON_CATCH; } @@ -64,18 +71,19 @@ auto PythonClassActivityData::tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) -> PyObject* { auto* self = reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; - if (!InLogicThread()) { - throw Exception( - "ERROR: " + std::string(type_obj.tp_name) - + " objects must only be created in the logic thread (current is (" - + GetCurrentThreadName() + ")."); - } - self->host_activity_ = new Object::WeakRef(); - BA_PYTHON_NEW_CATCH; + if (!self) { + return nullptr; } + BA_PYTHON_TRY; + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + self->host_activity_ = new Object::WeakRef(); return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; } void PythonClassActivityData::tp_dealloc(PythonClassActivityData* self) { @@ -83,9 +91,9 @@ void PythonClassActivityData::tp_dealloc(PythonClassActivityData* self) { // These have to be destructed in the logic thread; send them along to // it if need be; otherwise do it immediately. - if (!InLogicThread()) { + if (!g_base->InLogicThread()) { Object::WeakRef* h = self->host_activity_; - g_logic->thread()->PushCall([h] { delete h; }); + g_base->logic->event_loop()->PushCall([h] { delete h; }); } else { delete self->host_activity_; } @@ -93,11 +101,11 @@ void PythonClassActivityData::tp_dealloc(PythonClassActivityData* self) { Py_TYPE(self)->tp_free(reinterpret_cast(self)); } -auto PythonClassActivityData::exists(PythonClassActivityData* self) +auto PythonClassActivityData::Exists(PythonClassActivityData* self) -> PyObject* { BA_PYTHON_TRY; - - HostActivity* host_activity = self->host_activity_->get(); + BA_PRECONDITION(g_base->InLogicThread()); + HostActivity* host_activity = self->host_activity_->Get(); if (host_activity) { Py_RETURN_TRUE; } else { @@ -107,11 +115,11 @@ auto PythonClassActivityData::exists(PythonClassActivityData* self) BA_PYTHON_CATCH; } -auto PythonClassActivityData::make_foreground(PythonClassActivityData* self) +auto PythonClassActivityData::MakeForeground(PythonClassActivityData* self) -> PyObject* { BA_PYTHON_TRY; - - HostActivity* a = self->host_activity_->get(); + BA_PRECONDITION(g_base->InLogicThread()); + HostActivity* a = self->host_activity_->Get(); if (!a) { throw Exception("Invalid activity.", PyExcType::kActivityNotFound); } @@ -126,11 +134,12 @@ auto PythonClassActivityData::make_foreground(PythonClassActivityData* self) BA_PYTHON_CATCH; } -auto PythonClassActivityData::start(PythonClassActivityData* self) +auto PythonClassActivityData::Start(PythonClassActivityData* self) -> PyObject* { BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); - HostActivity* a = self->host_activity_->get(); + HostActivity* a = self->host_activity_->Get(); if (!a) { throw Exception("Invalid activity data.", PyExcType::kActivityNotFound); } @@ -140,12 +149,13 @@ auto PythonClassActivityData::start(PythonClassActivityData* self) BA_PYTHON_CATCH; } -auto PythonClassActivityData::expire(PythonClassActivityData* self) +auto PythonClassActivityData::Expire(PythonClassActivityData* self) -> PyObject* { BA_PYTHON_TRY; - HostActivity* a = self->host_activity_->get(); + BA_PRECONDITION(g_base->InLogicThread()); + HostActivity* a = self->host_activity_->Get(); - // The python side may have stuck around after our c++ side was + // The Python side may have stuck around after our c++ side was // torn down; that's ok. if (a) { HostSession* session = a->GetHostSession(); @@ -159,25 +169,43 @@ auto PythonClassActivityData::expire(PythonClassActivityData* self) BA_PYTHON_CATCH; } +auto PythonClassActivityData::Context(PythonClassActivityData* self) + -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + HostActivity* a = self->host_activity_->Get(); + if (!a) { + throw Exception("Activity is not valid."); + } + return base::PythonClassContextRef::Create(a); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + PyTypeObject PythonClassActivityData::type_obj; PyMethodDef PythonClassActivityData::tp_methods[] = { - {"exists", (PyCFunction)exists, METH_NOARGS, + {"exists", (PyCFunction)Exists, METH_NOARGS, "exists() -> bool\n" "\n" "Returns whether the ActivityData still exists.\n" "Most functionality will fail on a nonexistent instance."}, - {"make_foreground", (PyCFunction)make_foreground, METH_NOARGS, + {"make_foreground", (PyCFunction)MakeForeground, METH_NOARGS, "make_foreground() -> None\n" "\n" "Sets this activity as the foreground one in its session."}, - {"expire", (PyCFunction)expire, METH_NOARGS, + {"expire", (PyCFunction)Expire, METH_NOARGS, "expire() -> None\n" "\n" "Expires the internal data for the activity"}, - {"start", (PyCFunction)start, METH_NOARGS, + {"start", (PyCFunction)Start, METH_NOARGS, "start() -> None\n" "\n" "Begins the activity running"}, + {"context", (PyCFunction)Context, METH_NOARGS, + "context() -> bascenev1.ContextRef\n" + "\n" + "Return a context-ref pointing to the activity."}, + {nullptr}}; -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/python/class/python_class_activity_data.h b/src/ballistica/scene_v1/python/class/python_class_activity_data.h new file mode 100644 index 00000000..bb7a7122 --- /dev/null +++ b/src/ballistica/scene_v1/python/class/python_class_activity_data.h @@ -0,0 +1,42 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_ACTIVITY_DATA_H_ +#define BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_ACTIVITY_DATA_H_ + +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::scene_v1 { + +class PythonClassActivityData : public PythonClass { + public: + static auto type_name() -> const char*; + static void SetupType(PyTypeObject* cls); + static auto Create(HostActivity* host_activity) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + auto GetHostActivity() const -> HostActivity*; + + private: + static PyMethodDef tp_methods[]; + static PyNumberMethods as_number_; + static auto tp_repr(PythonClassActivityData* self) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassActivityData* self); + static auto nb_bool(PythonClassActivityData* self) -> int; + + static auto Exists(PythonClassActivityData* self) -> PyObject*; + static auto MakeForeground(PythonClassActivityData* self) -> PyObject*; + static auto Start(PythonClassActivityData* self) -> PyObject*; + static auto Expire(PythonClassActivityData* self) -> PyObject*; + static auto Context(PythonClassActivityData* self) -> PyObject*; + Object::WeakRef* host_activity_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_ACTIVITY_DATA_H_ diff --git a/src/ballistica/scene_v1/python/class/python_class_base_timer.cc b/src/ballistica/scene_v1/python/class/python_class_base_timer.cc new file mode 100644 index 00000000..0e095b7a --- /dev/null +++ b/src/ballistica/scene_v1/python/class/python_class_base_timer.cc @@ -0,0 +1,135 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/python/class/python_class_base_timer.h" + +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/support/python_context_call_runnable.h" +#include "ballistica/scene_v1/support/scene_v1_context.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" + +namespace ballistica::scene_v1 { + +auto PythonClassBaseTimer::type_name() -> const char* { return "BaseTimer"; } + +void PythonClassBaseTimer::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "bascenev1.BaseTimer"; + cls->tp_basicsize = sizeof(PythonClassBaseTimer); + cls->tp_doc = + "BaseTimer(time: float, call: Callable[[], Any], repeat: bool = False)\n" + "\n" + "Timers are used to run code at later points in time.\n" + "\n" + "Category: **General Utility Classes**\n" + "\n" + "This class encapsulates a base-time timer in the current scene\n" + "context.\n" + "The underlying timer will be destroyed when either this object is\n" + "no longer referenced or when its Context (Activity, etc.) dies. If you\n" + "do not want to worry about keeping a reference to your timer around,\n" + "you should use the bascenev1.basetimer() function instead.\n" + "\n" + "###### time (float)\n" + "> Length of time in seconds that the timer will wait\n" + "before firing.\n" + "\n" + "###### call (Callable[[], Any])\n" + "> A callable Python object. Remember that the timer will retain a\n" + "strong reference to the callable for as long as it exists, so you\n" + "may want to look into concepts such as babase.WeakCall if that is not\n" + "desired.\n" + "\n" + "###### repeat (bool)\n" + "> If True, the timer will fire repeatedly, with each successive\n" + "firing having the same delay as the first.\n" + "\n" + "##### Example\n" + "\n" + "Use a BaseTimer object to print repeatedly for a few seconds:\n" + ">>> import bascenev1 as bs\n" + "... def say_it():\n" + "... bs.screenmessage('BADGER!')\n" + "... def stop_saying_it():\n" + "... global g_timer\n" + "... g_timer = None\n" + "... bs.screenmessage('MUSHROOM MUSHROOM!')\n" + "... # Create our timer; it will run as long as we have the self.t ref.\n" + "... g_timer = bs.BaseTimer(0.3, say_it, repeat=True)\n" + "... # Now fire off a one-shot timer to kill it.\n" + "... bs.basetimer(3.89, stop_saying_it)\n"; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; +} + +auto PythonClassBaseTimer::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + + double length; + int repeat{}; + PyObject* call_obj{}; + static const char* kwlist[] = {"time", "call", "repeat", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "dO|p", + const_cast(kwlist), &length, + &call_obj, &repeat)) { + return nullptr; + } + if (length < 0.0) { + throw Exception("Timer length cannot be < 0.", PyExcType::kValue); + } + self->context_ref_ = new ContextRefSceneV1(); + self->timer_id_ = SceneV1Context::Current().NewTimer( + TimeType::kBase, static_cast(length * 1000.0), + static_cast(repeat), + Object::New(call_obj)); + self->have_timer_ = true; + + return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; +} + +static void DoDelete(bool have_timer, int timer_id, + ContextRefSceneV1* context_state) { + assert(g_base->InLogicThread()); + if (!context_state) { + return; + } + auto* context = context_state->GetContextTyped(); + if (have_timer && context) { + context->DeleteTimer(TimeType::kBase, timer_id); + } + delete context_state; +} + +void PythonClassBaseTimer::tp_dealloc(PythonClassBaseTimer* self) { + BA_PYTHON_TRY; + // These have to be deleted in the logic thread. + if (!g_base->InLogicThread()) { + auto a0 = self->have_timer_; + auto a1 = self->timer_id_; + auto a2 = self->context_ref_; + g_base->logic->event_loop()->PushCall( + [a0, a1, a2] { DoDelete(a0, a1, a2); }); + } else { + DoDelete(self->have_timer_, self->timer_id_, self->context_ref_); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +PyTypeObject PythonClassBaseTimer::type_obj; + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/python/class/python_class_base_timer.h b/src/ballistica/scene_v1/python/class/python_class_base_timer.h new file mode 100644 index 00000000..bcd1179e --- /dev/null +++ b/src/ballistica/scene_v1/python/class/python_class_base_timer.h @@ -0,0 +1,31 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_BASE_TIMER_H_ +#define BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_BASE_TIMER_H_ + +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::scene_v1 { + +class PythonClassBaseTimer : public PythonClass { + public: + static auto type_name() -> const char*; + static void SetupType(PyTypeObject* cls); + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + + private: + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassBaseTimer* self); + int timer_id_; + ContextRefSceneV1* context_ref_; + bool have_timer_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_BASE_TIMER_H_ diff --git a/src/ballistica/python/class/python_class_input_device.cc b/src/ballistica/scene_v1/python/class/python_class_input_device.cc similarity index 67% rename from src/ballistica/python/class/python_class_input_device.cc rename to src/ballistica/scene_v1/python/class/python_class_input_device.cc index 8f262fe3..77e473bf 100644 --- a/src/ballistica/python/class/python_class_input_device.cc +++ b/src/ballistica/scene_v1/python/class/python_class_input_device.cc @@ -1,23 +1,30 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/python/class/python_class_input_device.h" +#include "ballistica/scene_v1/python/class/python_class_input_device.h" -#include "ballistica/core/thread.h" -#include "ballistica/input/device/input_device.h" -#include "ballistica/logic/player.h" -#include "ballistica/python/python.h" +#include "ballistica/base/input/device/input_device.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Ignore a few things that python macros do. #pragma clang diagnostic push #pragma ide diagnostic ignored "RedundantCast" -void PythonClassInputDevice::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_name = "ba.InputDevice"; - obj->tp_basicsize = sizeof(PythonClassInputDevice); - obj->tp_doc = +auto PythonClassInputDevice::type_name() -> const char* { + return "InputDevice"; +} + +void PythonClassInputDevice::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "babase.InputDevice"; + cls->tp_basicsize = sizeof(PythonClassInputDevice); + cls->tp_doc = "An input-device such as a gamepad, touchscreen, or keyboard.\n" "\n" "Category: **Gameplay Classes**\n" @@ -32,7 +39,7 @@ void PythonClassInputDevice::SetupType(PyTypeObject* obj) { " on the actual device. (Can be used to determine whether to show\n" " them in controls-overlays, etc.).\n" "\n" - " player (ba.SessionPlayer | None):\n" + " player (bascenev1.SessionPlayer | None):\n" " The player associated with this input device.\n" "\n" " client_id (int):\n" @@ -63,35 +70,38 @@ void PythonClassInputDevice::SetupType(PyTypeObject* obj) { " client.\n" "\n"; - obj->tp_new = tp_new; - obj->tp_dealloc = (destructor)tp_dealloc; - obj->tp_repr = (reprfunc)tp_repr; - obj->tp_methods = tp_methods; - obj->tp_getattro = (getattrofunc)tp_getattro; - obj->tp_setattro = (setattrofunc)tp_setattro; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_methods = tp_methods; + cls->tp_getattro = (getattrofunc)tp_getattro; + cls->tp_setattro = (setattrofunc)tp_setattro; // We provide number methods only for bool functionality. memset(&as_number_, 0, sizeof(as_number_)); as_number_.nb_bool = (inquiry)nb_bool; - obj->tp_as_number = &as_number_; + cls->tp_as_number = &as_number_; } -auto PythonClassInputDevice::Create(InputDevice* input_device) -> PyObject* { +auto PythonClassInputDevice::Create(SceneV1InputDeviceDelegate* input_device) + -> PyObject* { // Make sure we only have one python ref per material. if (input_device) { - assert(!input_device->has_py_ref()); + assert(!input_device->HasPyRef()); } + assert(TypeIsSetUp(&type_obj)); auto* py_input_device = reinterpret_cast( PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); if (!py_input_device) { - throw Exception("ba.InputDevice creation failed."); + throw Exception("babase.InputDevice creation failed."); } - *(py_input_device->input_device_) = input_device; + *py_input_device->input_device_delegate_ = input_device; return reinterpret_cast(py_input_device); } -auto PythonClassInputDevice::GetInputDevice() const -> InputDevice* { - InputDevice* input_device = input_device_->get(); +auto PythonClassInputDevice::GetInputDevice() const + -> SceneV1InputDeviceDelegate* { + SceneV1InputDeviceDelegate* input_device = input_device_delegate_->Get(); if (!input_device) { throw Exception(PyExcType::kInputDeviceNotFound); } @@ -101,9 +111,9 @@ auto PythonClassInputDevice::GetInputDevice() const -> InputDevice* { auto PythonClassInputDevice::tp_repr(PythonClassInputDevice* self) -> PyObject* { BA_PYTHON_TRY; - InputDevice* d = self->input_device_->get(); - int input_device_id = d ? d->index() : -1; - std::string dname = d ? d->GetDeviceName() : "invalid device"; + SceneV1InputDeviceDelegate* d = self->input_device_delegate_->Get(); + int input_device_id = d ? d->input_device().index() : -1; + std::string dname = d ? d->input_device().GetDeviceName() : "invalid device"; return Py_BuildValue("s", (std::string("") @@ -112,7 +122,7 @@ auto PythonClassInputDevice::tp_repr(PythonClassInputDevice* self) } auto PythonClassInputDevice::nb_bool(PythonClassInputDevice* self) -> int { - return self->input_device_->exists(); + return self->input_device_delegate_->Exists(); } PyNumberMethods PythonClassInputDevice::as_number_; @@ -121,18 +131,20 @@ auto PythonClassInputDevice::tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) -> PyObject* { auto* self = reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; - if (!InLogicThread()) { - throw Exception( - "ERROR: " + std::string(type_obj.tp_name) - + " objects must only be created in the logic thread (current is (" - + GetCurrentThreadName() + ")."); - } - self->input_device_ = new Object::WeakRef(); - BA_PYTHON_NEW_CATCH; + if (!self) { + return nullptr; } + BA_PYTHON_TRY; + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + self->input_device_delegate_ = + new Object::WeakRef(); return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; } void PythonClassInputDevice::tp_dealloc(PythonClassInputDevice* self) { @@ -141,11 +153,12 @@ void PythonClassInputDevice::tp_dealloc(PythonClassInputDevice* self) { // need be. // FIXME: Technically the main thread has a pointer to a dead PyObject // until the delete goes through; could that ever be a problem? - if (!InLogicThread()) { - Object::WeakRef* d = self->input_device_; - g_logic->thread()->PushCall([d] { delete d; }); + if (!g_base->InLogicThread()) { + Object::WeakRef* d = + self->input_device_delegate_; + g_base->logic->event_loop()->PushCall([d] { delete d; }); } else { - delete self->input_device_; + delete self->input_device_delegate_; } BA_PYTHON_DEALLOC_CATCH; Py_TYPE(self)->tp_free(reinterpret_cast(self)); @@ -157,81 +170,82 @@ auto PythonClassInputDevice::tp_getattro(PythonClassInputDevice* self, assert(PyUnicode_Check(attr)); // NOLINT (signed bitwise ops) const char* s = PyUnicode_AsUTF8(attr); if (!strcmp(s, "player")) { - InputDevice* input_device = self->input_device_->get(); - if (!input_device) { + auto* d = self->input_device_delegate_->Get(); + if (!d) { throw Exception(PyExcType::kInputDeviceNotFound); } - Player* player = input_device->GetPlayer(); + Player* player = d->GetPlayer(); if (player != nullptr) { return player->NewPyRef(); } Py_RETURN_NONE; } else if (!strcmp(s, "allows_configuring")) { - InputDevice* d = self->input_device_->get(); + auto* d = self->input_device_delegate_->Get(); if (!d) { throw Exception(PyExcType::kInputDeviceNotFound); } - if (d->GetAllowsConfiguring()) { + if (d->input_device().GetAllowsConfiguring()) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; } } else if (!strcmp(s, "has_meaningful_button_names")) { - InputDevice* d = self->input_device_->get(); + auto* d = self->input_device_delegate_->Get(); if (!d) { throw Exception(PyExcType::kInputDeviceNotFound); } - if (d->HasMeaningfulButtonNames()) { + if (d->input_device().HasMeaningfulButtonNames()) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; } } else if (!strcmp(s, "client_id")) { - InputDevice* d = self->input_device_->get(); + auto* d = self->input_device_delegate_->Get(); if (!d) { throw Exception(PyExcType::kInputDeviceNotFound); } return PyLong_FromLong(d->GetClientID()); } else if (!strcmp(s, "name")) { - InputDevice* d = self->input_device_->get(); + auto* d = self->input_device_delegate_->Get(); if (!d) { throw Exception(PyExcType::kInputDeviceNotFound); } - return PyUnicode_FromString(d->GetDeviceName().c_str()); + return PyUnicode_FromString(d->input_device().GetDeviceName().c_str()); } else if (!strcmp(s, "unique_identifier")) { - InputDevice* d = self->input_device_->get(); + auto* d = self->input_device_delegate_->Get(); if (!d) { throw Exception(PyExcType::kInputDeviceNotFound); } - return PyUnicode_FromString(d->GetPersistentIdentifier().c_str()); + return PyUnicode_FromString( + d->input_device().GetPersistentIdentifier().c_str()); } else if (!strcmp(s, "id")) { - InputDevice* d = self->input_device_->get(); + auto* d = self->input_device_delegate_->Get(); if (!d) { throw Exception(PyExcType::kInputDeviceNotFound); } - return PyLong_FromLong(d->index()); + return PyLong_FromLong(d->input_device().index()); } else if (!strcmp(s, "instance_number")) { - InputDevice* d = self->input_device_->get(); + auto* d = self->input_device_delegate_->Get(); if (!d) { throw Exception(PyExcType::kInputDeviceNotFound); } - return PyLong_FromLong(d->device_number()); + return PyLong_FromLong(d->input_device().device_number()); } else if (!strcmp(s, "is_controller_app")) { - InputDevice* input_device = self->input_device_->get(); - if (!input_device) { + auto* d = self->input_device_delegate_->Get(); + if (!d) { throw Exception(PyExcType::kInputDeviceNotFound); } - if (input_device->IsRemoteApp()) { + if (d->input_device().IsRemoteApp()) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; } } else if (!strcmp(s, "is_remote_client")) { - InputDevice* input_device = self->input_device_->get(); - if (!input_device) { + auto* delegate = self->input_device_delegate_->Get(); + if (!delegate) { throw Exception(PyExcType::kInputDeviceNotFound); } - if (input_device->IsRemoteClient()) { + if (delegate->IsRemoteClient()) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; @@ -262,14 +276,14 @@ auto PythonClassInputDevice::tp_setattro(PythonClassInputDevice* self, #pragma clang diagnostic pop -auto PythonClassInputDevice::RemoveRemotePlayerFromGame( - PythonClassInputDevice* self) -> PyObject* { +auto PythonClassInputDevice::DetachFromPlayer(PythonClassInputDevice* self) + -> PyObject* { BA_PYTHON_TRY; - InputDevice* d = self->input_device_->get(); + SceneV1InputDeviceDelegate* d = self->input_device_delegate_->Get(); if (!d) { throw Exception(PyExcType::kInputDeviceNotFound); } - d->RemoveRemotePlayerFromGame(); + d->DetachFromPlayer(); Py_RETURN_NONE; BA_PYTHON_CATCH; } @@ -277,7 +291,7 @@ auto PythonClassInputDevice::RemoveRemotePlayerFromGame( auto PythonClassInputDevice::GetDefaultPlayerName(PythonClassInputDevice* self) -> PyObject* { BA_PYTHON_TRY; - InputDevice* d = self->input_device_->get(); + SceneV1InputDeviceDelegate* d = self->input_device_delegate_->Get(); if (!d) { throw Exception(PyExcType::kInputDeviceNotFound); } @@ -288,7 +302,7 @@ auto PythonClassInputDevice::GetDefaultPlayerName(PythonClassInputDevice* self) auto PythonClassInputDevice::GetPlayerProfiles(PythonClassInputDevice* self) -> PyObject* { BA_PYTHON_TRY; - InputDevice* d = self->input_device_->get(); + SceneV1InputDeviceDelegate* d = self->input_device_delegate_->Get(); if (!d) { throw Exception(PyExcType::kInputDeviceNotFound); } @@ -311,7 +325,7 @@ auto PythonClassInputDevice::GetV1AccountName(PythonClassInputDevice* self, const_cast(kwlist), &full)) { return nullptr; } - InputDevice* d = self->input_device_->get(); + SceneV1InputDeviceDelegate* d = self->input_device_delegate_->Get(); if (!d) { throw Exception(PyExcType::kInputDeviceNotFound); } @@ -320,14 +334,15 @@ auto PythonClassInputDevice::GetV1AccountName(PythonClassInputDevice* self, BA_PYTHON_CATCH; } -auto PythonClassInputDevice::IsConnectedToRemotePlayer( - PythonClassInputDevice* self) -> PyObject* { +auto PythonClassInputDevice::IsAttachedToPlayer(PythonClassInputDevice* self) + -> PyObject* { BA_PYTHON_TRY; - InputDevice* input_device = self->input_device_->get(); + SceneV1InputDeviceDelegate* input_device = + self->input_device_delegate_->Get(); if (!input_device) { throw Exception(PyExcType::kInputDeviceNotFound); } - if (input_device->GetRemotePlayer()) { + if (input_device->AttachedToPlayer()) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; @@ -337,7 +352,7 @@ auto PythonClassInputDevice::IsConnectedToRemotePlayer( auto PythonClassInputDevice::Exists(PythonClassInputDevice* self) -> PyObject* { BA_PYTHON_TRY; - if (self->input_device_->exists()) { + if (self->input_device_delegate_->Exists()) { Py_RETURN_TRUE; } Py_RETURN_FALSE; @@ -348,18 +363,20 @@ auto PythonClassInputDevice::GetAxisName(PythonClassInputDevice* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); + assert(g_base->InLogicThread()); int id; static const char* kwlist[] = {"axis_id", nullptr}; if (!PyArg_ParseTupleAndKeywords(args, keywds, "i", const_cast(kwlist), &id)) { return nullptr; } - InputDevice* input_device = self->input_device_->get(); + SceneV1InputDeviceDelegate* input_device = + self->input_device_delegate_->Get(); if (!input_device) { throw Exception(PyExcType::kInputDeviceNotFound); } - return PyUnicode_FromString(input_device->GetAxisName(id).c_str()); + return PyUnicode_FromString( + input_device->input_device().GetAxisName(id).c_str()); BA_PYTHON_CATCH; } @@ -367,20 +384,20 @@ auto PythonClassInputDevice::GetButtonName(PythonClassInputDevice* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); + assert(g_base->InLogicThread()); int id{}; static const char* kwlist[] = {"button_id", nullptr}; if (!PyArg_ParseTupleAndKeywords(args, keywds, "i", const_cast(kwlist), &id)) { return nullptr; } - InputDevice* d = self->input_device_->get(); + SceneV1InputDeviceDelegate* d = self->input_device_delegate_->Get(); if (!d) { throw Exception(PyExcType::kInputDeviceNotFound); } // Ask the input-device for the button name. - std::string bname = d->GetButtonName(id); + std::string bname = d->input_device().GetButtonName(id); // If this doesn't appear to be lstr json itself, convert it to that. if (bname.length() < 1 || bname.c_str()[0] != '{') { @@ -388,15 +405,18 @@ auto PythonClassInputDevice::GetButtonName(PythonClassInputDevice* self, bname = R"({"v":")" + bname + "\"}"; } PythonRef args2(Py_BuildValue("(s)", bname.c_str()), PythonRef::kSteal); - PythonRef results = - g_python->obj(Python::ObjID::kLstrFromJsonCall).Call(args2); - if (!results.exists()) { + PythonRef results = g_base->python->objs() + .Get(base::BasePython::ObjID::kLstrFromJsonCall) + .Call(args2); + if (!results.Exists()) { Log(LogLevel::kError, "Error creating Lstr from raw button name: '" + bname + "'"); PythonRef args3(Py_BuildValue("(s)", "?"), PythonRef::kSteal); - results = g_python->obj(Python::ObjID::kLstrFromJsonCall).Call(args3); + results = g_base->python->objs() + .Get(base::BasePython::ObjID::kLstrFromJsonCall) + .Call(args3); } - if (!results.exists()) { + if (!results.Exists()) { throw Exception("Internal error creating Lstr."); } return results.NewRef(); @@ -406,16 +426,18 @@ auto PythonClassInputDevice::GetButtonName(PythonClassInputDevice* self, PyTypeObject PythonClassInputDevice::type_obj; PyMethodDef PythonClassInputDevice::tp_methods[] = { - {"remove_remote_player_from_game", (PyCFunction)RemoveRemotePlayerFromGame, - METH_NOARGS, - "remove_remote_player_from_game() -> None\n" + {"detach_from_player", (PyCFunction)DetachFromPlayer, METH_NOARGS, + "detach_from_player() -> None\n" "\n" - "(internal)"}, - {"is_connected_to_remote_player", (PyCFunction)IsConnectedToRemotePlayer, - METH_NOARGS, - "is_connected_to_remote_player() -> bool\n" + "Detach the device from any player it is controlling.\n" "\n" - "(internal)"}, + "This applies both to local players and remote players."}, + {"is_attached_to_player", (PyCFunction)IsAttachedToPlayer, METH_NOARGS, + "is_attached_to_player() -> bool\n" + "\n" + "Return whether this device is controlling a player of some sort.\n" + "\n" + "This can mean either a local player or a remote player."}, {"exists", (PyCFunction)Exists, METH_NOARGS, "exists() -> bool\n" "\n" @@ -423,7 +445,7 @@ PyMethodDef PythonClassInputDevice::tp_methods[] = { "still present.\n"}, {"get_button_name", (PyCFunction)GetButtonName, METH_VARARGS | METH_KEYWORDS, // NOLINT (signed bitwise ops) - "get_button_name(button_id: int) -> ba.Lstr\n" + "get_button_name(button_id: int) -> babase.Lstr\n" "\n" "Given a button ID, return a human-readable name for that key/button.\n" "\n" @@ -457,4 +479,4 @@ PyMethodDef PythonClassInputDevice::tp_methods[] = { #pragma clang diagnostic pop -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/python/class/python_class_input_device.h b/src/ballistica/scene_v1/python/class/python_class_input_device.h similarity index 62% rename from src/ballistica/python/class/python_class_input_device.h rename to src/ballistica/scene_v1/python/class/python_class_input_device.h index d360c876..987b2ece 100644 --- a/src/ballistica/python/class/python_class_input_device.h +++ b/src/ballistica/scene_v1/python/class/python_class_input_device.h @@ -1,23 +1,24 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_INPUT_DEVICE_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_INPUT_DEVICE_H_ +#ifndef BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_INPUT_DEVICE_H_ +#define BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_INPUT_DEVICE_H_ -#include "ballistica/core/object.h" -#include "ballistica/python/class/python_class.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_class.h" -namespace ballistica { +namespace ballistica::scene_v1 { class PythonClassInputDevice : public PythonClass { public: - static auto type_name() -> const char* { return "InputDevice"; } - static void SetupType(PyTypeObject* obj); - static auto Create(InputDevice* input_device) -> PyObject*; + static auto type_name() -> const char*; + static void SetupType(PyTypeObject* cls); + static auto Create(SceneV1InputDeviceDelegate* input_device) -> PyObject*; static auto Check(PyObject* o) -> bool { return PyObject_TypeCheck(o, &type_obj); } static PyTypeObject type_obj; - auto GetInputDevice() const -> InputDevice*; + auto GetInputDevice() const -> SceneV1InputDeviceDelegate*; private: static PyMethodDef tp_methods[]; @@ -30,23 +31,21 @@ class PythonClassInputDevice : public PythonClass { -> PyObject*; static void tp_dealloc(PythonClassInputDevice* self); static auto nb_bool(PythonClassInputDevice* self) -> int; - static auto RemoveRemotePlayerFromGame(PythonClassInputDevice* self) - -> PyObject*; + static auto DetachFromPlayer(PythonClassInputDevice* self) -> PyObject*; static auto GetDefaultPlayerName(PythonClassInputDevice* self) -> PyObject*; static auto GetPlayerProfiles(PythonClassInputDevice* self) -> PyObject*; static auto GetV1AccountName(PythonClassInputDevice* self, PyObject* args, PyObject* keywds) -> PyObject*; - static auto IsConnectedToRemotePlayer(PythonClassInputDevice* self) - -> PyObject*; + static auto IsAttachedToPlayer(PythonClassInputDevice* self) -> PyObject*; static auto Exists(PythonClassInputDevice* self) -> PyObject*; static auto GetAxisName(PythonClassInputDevice* self, PyObject* args, PyObject* keywds) -> PyObject*; static auto GetButtonName(PythonClassInputDevice* self, PyObject* args, PyObject* keywds) -> PyObject*; static PyNumberMethods as_number_; - Object::WeakRef* input_device_; + Object::WeakRef* input_device_delegate_; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_INPUT_DEVICE_H_ +#endif // BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_INPUT_DEVICE_H_ diff --git a/src/ballistica/python/class/python_class_material.cc b/src/ballistica/scene_v1/python/class/python_class_material.cc similarity index 79% rename from src/ballistica/python/class/python_class_material.cc rename to src/ballistica/scene_v1/python/class/python_class_material.cc index a1c6cfe1..b491f1fb 100644 --- a/src/ballistica/python/class/python_class_material.cc +++ b/src/ballistica/scene_v1/python/class/python_class_material.cc @@ -1,25 +1,26 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/python/class/python_class_material.h" +#include "ballistica/scene_v1/python/class/python_class_material.h" -#include "ballistica/core/thread.h" -#include "ballistica/dynamics/material/impact_sound_material_action.h" -#include "ballistica/dynamics/material/material.h" -#include "ballistica/dynamics/material/material_component.h" -#include "ballistica/dynamics/material/material_condition_node.h" -#include "ballistica/dynamics/material/node_message_material_action.h" -#include "ballistica/dynamics/material/node_mod_material_action.h" -#include "ballistica/dynamics/material/node_user_message_material_action.h" -#include "ballistica/dynamics/material/part_mod_material_action.h" -#include "ballistica/dynamics/material/python_call_material_action.h" -#include "ballistica/dynamics/material/roll_sound_material_action.h" -#include "ballistica/dynamics/material/skid_sound_material_action.h" -#include "ballistica/dynamics/material/sound_material_action.h" -#include "ballistica/logic/host_activity.h" -#include "ballistica/logic/logic.h" -#include "ballistica/python/python.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/scene_v1/dynamics/material/impact_sound_material_action.h" +#include "ballistica/scene_v1/dynamics/material/material.h" +#include "ballistica/scene_v1/dynamics/material/material_component.h" +#include "ballistica/scene_v1/dynamics/material/material_condition_node.h" +#include "ballistica/scene_v1/dynamics/material/node_message_material_action.h" +#include "ballistica/scene_v1/dynamics/material/node_mod_material_action.h" +#include "ballistica/scene_v1/dynamics/material/node_user_msg_mat_action.h" +#include "ballistica/scene_v1/dynamics/material/part_mod_material_action.h" +#include "ballistica/scene_v1/dynamics/material/python_call_material_action.h" +#include "ballistica/scene_v1/dynamics/material/roll_sound_material_action.h" +#include "ballistica/scene_v1/dynamics/material/skid_sound_material_action.h" +#include "ballistica/scene_v1/dynamics/material/sound_material_action.h" +#include "ballistica/scene_v1/python/scene_v1_python.h" +#include "ballistica/scene_v1/support/host_activity.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Ignore signed bitwise stuff since python macros do a lot of it. #pragma clang diagnostic push @@ -40,14 +41,17 @@ static void DoAddAction(PyObject* actions_obj, // The set we expose via dir(). static const char* extra_dir_attrs[] = {ATTR_LABEL, nullptr}; -void PythonClassMaterial::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_name = "ba.Material"; - obj->tp_repr = (reprfunc)tp_repr; - obj->tp_basicsize = sizeof(PythonClassMaterial); +auto PythonClassMaterial::type_name() -> const char* { return "Material"; } + +void PythonClassMaterial::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "bascenev1.Material"; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_basicsize = sizeof(PythonClassMaterial); // clang-format off - obj->tp_doc = + cls->tp_doc = "Material(label: str | None = None)\n" "\n" "An entity applied to game objects to modify collision behavior.\n" @@ -58,15 +62,16 @@ void PythonClassMaterial::SetupType(PyTypeObject* obj) { "or trigger callback functions when collisions occur.\n" "\n" "Materials are applied to 'parts', which are groups of one or more\n" - "rigid bodies created as part of a ba.Node. Nodes can have any number\n" - "of parts, each with its own set of materials. Generally materials are\n" - "specified as array attributes on the Node. The `spaz` node, for\n" - "example, has various attributes such as `materials`,\n" + "rigid bodies created as part of a bascenev1.Node. Nodes can have any\n" + "number of parts, each with its own set of materials. Generally\n" + "materials are specified as array attributes on the Node. The `spaz`\n" + "node, for example, has various attributes such as `materials`,\n" "`roller_materials`, and `punch_materials`, which correspond\n" "to the various parts it creates.\n" "\n" - "Use ba.Material to instantiate a blank material, and then use its\n" - "ba.Material.add_actions() method to define what the material does.\n" + "Use bascenev1.Material to instantiate a blank material, and then use\n" + "its babase.Material.add_actions() method to define what the material\n" + "does.\n" "\n" "Attributes:\n" "\n" @@ -74,67 +79,72 @@ void PythonClassMaterial::SetupType(PyTypeObject* obj) { " A label for the material; only used for debugging.\n"; // clang-format on - obj->tp_new = tp_new; - obj->tp_dealloc = (destructor)tp_dealloc; - obj->tp_methods = tp_methods; - obj->tp_getattro = (getattrofunc)tp_getattro; - obj->tp_setattro = (setattrofunc)tp_setattro; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_methods = tp_methods; + cls->tp_getattro = (getattrofunc)tp_getattro; + cls->tp_setattro = (setattrofunc)tp_setattro; } auto PythonClassMaterial::tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) -> PyObject* { auto* self = reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; - // Do anything that might throw an exception *before* our placement-new - // stuff so we don't have to worry about cleaning it up on errors. - if (!InLogicThread()) { - throw Exception( - "ERROR: " + std::string(type_obj.tp_name) - + " objects must only be created in the logic thread (current is (" - + GetCurrentThreadName() + ")."); - } - PyObject* name_obj = Py_None; - std::string name; - Object::Ref m; + // Do anything that might throw an exception *before* our placement-new + // stuff so we don't have to worry about cleaning it up on errors. + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + PyObject* name_obj = Py_None; + std::string name; + Object::Ref m; - // Clion incorrectly things s_create_empty will always be false. + // Clion incorrectly things s_create_empty will always be false. #pragma clang diagnostic push #pragma ide diagnostic ignored "ConstantConditionsOC" - if (!s_create_empty_) { - static const char* kwlist[] = {"label", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "|O", - const_cast(kwlist), &name_obj)) { - return nullptr; - } - if (name_obj != Py_None) { - name = Python::GetPyString(name_obj); - } else { - name = Python::GetPythonFileLocation(); - } - - if (HostActivity* host_activity = Context::current().GetHostActivity()) { - m = host_activity->NewMaterial(name); - m->set_py_object(reinterpret_cast(self)); - } else { - throw Exception("Can't create materials in this context.", - PyExcType::kContext); - } + if (!s_create_empty_) { + static const char* kwlist[] = {"label", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|O", + const_cast(kwlist), &name_obj)) { + // ew; can't throw an exception here because we want to keep the + // python one that was just set. Just manually do a free I guess. + type->tp_free(self); + return nullptr; + } + if (name_obj != Py_None) { + name = Python::GetPyString(name_obj); + } else { + name = Python::GetPythonFileLocation(); + } + + if (HostActivity* host_activity = + ContextRefSceneV1::FromCurrent().GetHostActivity()) { + m = host_activity->NewMaterial(name); + m->set_py_object(reinterpret_cast(self)); + } else { + throw Exception("Can't create materials in this context_ref.", + PyExcType::kContext); } - self->material_ = new Object::Ref(m); - BA_PYTHON_NEW_CATCH; -#pragma clang diagnostic pop } + self->material_ = new Object::Ref(m); +#pragma clang diagnostic pop return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; } void PythonClassMaterial::Delete(Object::Ref* m) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // If we're the py-object for a material, clear them out. - if (m->exists()) { + if (m->Exists()) { assert((*m)->py_object() != nullptr); (*m)->set_py_object(nullptr); } @@ -146,9 +156,9 @@ void PythonClassMaterial::tp_dealloc(PythonClassMaterial* self) { // These have to be deleted in the logic thread - push a call if // need be.. otherwise do it immediately. - if (!InLogicThread()) { + if (!g_base->InLogicThread()) { Object::Ref* ptr = self->material_; - g_logic->thread()->PushCall([ptr] { Delete(ptr); }); + g_base->logic->event_loop()->PushCall([ptr] { Delete(ptr); }); } else { Delete(self->material_); } @@ -174,7 +184,7 @@ auto PythonClassMaterial::tp_getattro(PythonClassMaterial* self, PyObject* attr) const char* s = PyUnicode_AsUTF8(attr); if (!strcmp(s, ATTR_LABEL)) { - Material* material = self->material_->get(); + Material* material = self->material_->Get(); if (!material) { throw Exception("Invalid Material.", PyExcType::kNotFound); } @@ -219,7 +229,7 @@ auto PythonClassMaterial::Dir(PythonClassMaterial* self) -> PyObject* { for (const char** name = extra_dir_attrs; *name != nullptr; name++) { PyList_Append( dir_list, - PythonRef(PyUnicode_FromString(*name), PythonRef::kSteal).get()); + PythonRef(PyUnicode_FromString(*name), PythonRef::kSteal).Get()); } PyList_Sort(dir_list); return dir_list; @@ -230,7 +240,7 @@ auto PythonClassMaterial::Dir(PythonClassMaterial* self) -> PyObject* { auto PythonClassMaterial::AddActions(PythonClassMaterial* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); + assert(g_base->InLogicThread()); PyObject* conditions_obj{Py_None}; PyObject* actions_obj{nullptr}; const char* kwlist[] = {"actions", "conditions", nullptr}; @@ -245,7 +255,7 @@ auto PythonClassMaterial::AddActions(PythonClassMaterial* self, PyObject* args, DoAddConditions(conditions_obj, &conditions); } - Material* m = self->material_->get(); + Material* m = self->material_->Get(); if (!m) { throw Exception("Invalid Material.", PyExcType::kNotFound); } @@ -293,10 +303,10 @@ PyMethodDef PythonClassMaterial::tp_methods[] = { "\n" "##### Available Conditions\n" "###### `('they_have_material', material)`\n" - "> Does the part we\'re hitting have a given ba.Material?\n" + "> Does the part we\'re hitting have a given bascenev1.Material?\n" "\n" "###### `('they_dont_have_material', material)`\n" - "> Does the part we\'re hitting not have a given ba.Material?\n" + "> Does the part we\'re hitting not have a given bascenev1.Material?\n" "\n" "###### `('eval_colliding')`\n" "> Is `'collide'` true at this point\n" @@ -319,10 +329,10 @@ PyMethodDef PythonClassMaterial::tp_methods[] = { "> Is the part we're hitting older than `age` (in milliseconds)?\n" "\n" "###### `('they_are_same_node_as_us')`\n" - "> Does the part we're hitting belong to the same ba.Node as us?\n" + "> Does the part we're hitting belong to the same bascenev1.Node as us?\n" "\n" "###### `('they_are_different_node_than_us')`\n" - "> Does the part we're hitting belong to a different ba.Node than us?\n" + "> Does the part we're hitting belong to a different bascenev1.Node?\n" "\n" "##### Actions\n" "In a similar manner, actions are specified as tuples.\n" @@ -343,7 +353,7 @@ PyMethodDef PythonClassMaterial::tp_methods[] = { "`'at_connect'` or `'at_disconnect'`, and `message_obj` is the message\n" "object to send.\n" "This has the same effect as calling the node's\n" - "ba.Node.handlemessage() method.\n" + "babase.Node.handlemessage() method.\n" "\n" "###### `('modify_part_collision', attr, value)`\n" "> Changes some\n" @@ -380,50 +390,51 @@ PyMethodDef PythonClassMaterial::tp_methods[] = { "available here is `'collide'` (a boolean value).\n" "\n" "###### `('sound', sound, volume)`\n" - "> Plays a ba.Sound when a collision\n" + "> Plays a bascenev1.Sound when a collision\n" "occurs, at a given volume, regardless of the collision speed/etc.\n" "\n" "###### `('impact_sound', sound, targetImpulse, volume)`\n" "> Plays a sound\n" "when a collision occurs, based on the speed of impact.\n" - "Provide a ba.Sound, a target-impulse, and a volume.\n" + "Provide a bascenev1.Sound, a target-impulse, and a volume.\n" "\n" "###### `('skid_sound', sound, targetImpulse, volume)`\n" "> Plays a sound\n" "during a collision when parts are 'scraping' against each other.\n" - "Provide a ba.Sound, a target-impulse, and a volume.\n" + "Provide a bascenev1.Sound, a target-impulse, and a volume.\n" "\n" "###### `('roll_sound', sound, targetImpulse, volume)`\n" "> Plays a sound\n" "during a collision when parts are 'rolling' against each other.\n" - "Provide a ba.Sound, a target-impulse, and a volume.\n" + "Provide a bascenev1.Sound, a target-impulse, and a volume.\n" "\n" "##### Examples\n" "**Example 1:** create a material that lets us ignore\n" "collisions against any nodes we touch in the first\n" "100 ms of our existence; handy for preventing us from\n" "exploding outward if we spawn on top of another object:\n" - ">>> m = ba.Material()\n" + ">>> m = bascenev1.Material()\n" "... m.add_actions(\n" "... conditions=(('we_are_younger_than', 100),\n" "... 'or', ('they_are_younger_than', 100)),\n" "... actions=('modify_node_collision', 'collide', False))\n" "\n" - "**Example 2:** send a ba.DieMessage to anything we touch, but cause\n" - "no physical response. This should cause any ba.Actor to drop dead:\n" - ">>> m = ba.Material()\n" + "**Example 2:** send a bascenev1.DieMessage to anything we touch, but\n" + "cause no physical response. This should cause any bascenev1.Actor to\n" + "drop dead:\n" + ">>> m = bascenev1.Material()\n" "... m.add_actions(\n" "... actions=(('modify_part_collision', 'physical', False),\n" "... ('message', 'their_node', 'at_connect',\n" - "... ba.DieMessage())))\n" + "... bascenev1.DieMessage())))\n" "\n" "**Example 3:** play some sounds when we're contacting the ground:\n" - ">>> m = ba.Material()\n" + ">>> m = bascenev1.Material()\n" "... m.add_actions(\n" - "... conditions=('they_have_material',\n" - "... shared.footing_material),\n" - "... actions=(('impact_sound', ba.getsound('metalHit'), 2, 5),\n" - "... ('skid_sound', ba.getsound('metalSkid'), 2, 5)))\n"}, + "... conditions=('they_have_material', shared.footing_material),\n" + "... actions=(\n" + " ('impact_sound', bascenev1.getsound('metalHit'), 2, 5),\n" + " ('skid_sound', bascenev1.getsound('metalSkid'), 2, 5)))\n"}, {"__dir__", (PyCFunction)Dir, METH_NOARGS, "allows inclusion of our custom attrs in standard python dir()"}, @@ -431,7 +442,7 @@ PyMethodDef PythonClassMaterial::tp_methods[] = { void DoAddConditions(PyObject* cond_obj, Object::Ref* c) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (PyTuple_Check(cond_obj)) { Py_ssize_t size = PyTuple_GET_SIZE(cond_obj); if (size < 1) { @@ -495,7 +506,7 @@ void DoAddConditions(PyObject* cond_obj, if (argc > 0) { if (first_arg_is_material) { (*c)->val1_material = - Python::GetPyMaterial(PyTuple_GET_ITEM(cond_obj, 1)); + SceneV1Python::GetPyMaterial(PyTuple_GET_ITEM(cond_obj, 1)); } else { PyObject* o = PyTuple_GET_ITEM(cond_obj, 1); if (!PyLong_Check(o)) { @@ -504,7 +515,7 @@ void DoAddConditions(PyObject* cond_obj, + cond_str + "\".", PyExcType::kType); } - (*c)->val1 = static_cast(PyLong_AsLong(o)); + (*c)->val1 = static_cast(PyLong_AsLong(o)); } } if (argc > 1) { @@ -515,7 +526,7 @@ void DoAddConditions(PyObject* cond_obj, + cond_str + "\".", PyExcType::kType); } - (*c)->val1 = static_cast(PyLong_AsLong(o)); + (*c)->val1 = static_cast(PyLong_AsLong(o)); } } else if (PyTuple_Check(first)) { // First item is a tuple - assume its a tuple of size 3+2*n @@ -527,7 +538,7 @@ void DoAddConditions(PyObject* cond_obj, Object::Ref c2_prev; for (Py_ssize_t i = 0; i < (size - 1); i += 2) { c2 = Object::New(); - if (c2_prev.exists()) { + if (c2_prev.Exists()) { c2->left_child = c2_prev; } else { DoAddConditions(PyTuple_GET_ITEM(cond_obj, i), &c2->left_child); @@ -561,7 +572,7 @@ void DoAddConditions(PyObject* cond_obj, void DoAddAction(PyObject* actions_obj, std::vector >* actions) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (!PyTuple_Check(actions_obj)) { throw Exception("Expected a tuple.", PyExcType::kType); } @@ -614,14 +625,14 @@ void DoAddAction(PyObject* actions_obj, } // Pull the rest of the message. - Buffer b; + std::vector b; PyObject* user_message_obj = nullptr; - Python::DoBuildNodeMessage(actions_obj, 3, &b, &user_message_obj); + SceneV1Python::DoBuildNodeMessage(actions_obj, 3, &b, &user_message_obj); if (user_message_obj) { (*actions).push_back( Object::New( target_other_val, at_disconnect, user_message_obj)); - } else if (b.size() > 0) { + } else if (!b.empty()) { (*actions).push_back( Object::New( target_other_val, at_disconnect, b.data(), b.size())); @@ -679,7 +690,8 @@ void DoAddAction(PyObject* actions_obj, throw Exception("Expected 3 values for sound action tuple.", PyExcType::kValue); } - Sound* sound = Python::GetPySound(PyTuple_GET_ITEM(actions_obj, 1)); + SceneSound* sound = + SceneV1Python::GetPySceneSound(PyTuple_GET_ITEM(actions_obj, 1)); float volume = Python::GetPyFloat(PyTuple_GET_ITEM(actions_obj, 2)); (*actions).push_back( Object::New(sound, volume)); @@ -689,11 +701,13 @@ void DoAddAction(PyObject* actions_obj, PyExcType::kValue); } PyObject* sounds_obj = PyTuple_GET_ITEM(actions_obj, 1); - std::vector sounds; + std::vector sounds; if (PySequence_Check(sounds_obj)) { - sounds = Python::GetPySounds(sounds_obj); // Sequence of sounds. + sounds = + SceneV1Python::GetPySceneSounds(sounds_obj); // Sequence of sounds. } else { - sounds.push_back(Python::GetPySound(sounds_obj)); // Single sound. + sounds.push_back( + SceneV1Python::GetPySceneSound(sounds_obj)); // Single sound. } if (sounds.empty()) { throw Exception("Require at least 1 sound.", PyExcType::kValue); @@ -711,7 +725,8 @@ void DoAddAction(PyObject* actions_obj, throw Exception("Expected 4 values for skid_sound action tuple.", PyExcType::kValue); } - Sound* sound = Python::GetPySound(PyTuple_GET_ITEM(actions_obj, 1)); + SceneSound* sound = + SceneV1Python::GetPySceneSound(PyTuple_GET_ITEM(actions_obj, 1)); float target_impulse = Python::GetPyFloat(PyTuple_GET_ITEM(actions_obj, 2)); float volume = Python::GetPyFloat(PyTuple_GET_ITEM(actions_obj, 3)); (*actions).push_back(Object::New( @@ -721,7 +736,8 @@ void DoAddAction(PyObject* actions_obj, throw Exception("Expected 4 values for roll_sound action tuple.", PyExcType::kValue); } - Sound* sound = Python::GetPySound(PyTuple_GET_ITEM(actions_obj, 1)); + SceneSound* sound = + SceneV1Python::GetPySceneSound(PyTuple_GET_ITEM(actions_obj, 1)); float target_impulse = Python::GetPyFloat(PyTuple_GET_ITEM(actions_obj, 2)); float volume = Python::GetPyFloat(PyTuple_GET_ITEM(actions_obj, 3)); (*actions).push_back(Object::New( @@ -733,4 +749,4 @@ void DoAddAction(PyObject* actions_obj, #pragma clang diagnostic pop -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/python/class/python_class_material.h b/src/ballistica/scene_v1/python/class/python_class_material.h similarity index 68% rename from src/ballistica/python/class/python_class_material.h rename to src/ballistica/scene_v1/python/class/python_class_material.h index 7e759903..fc452860 100644 --- a/src/ballistica/python/class/python_class_material.h +++ b/src/ballistica/scene_v1/python/class/python_class_material.h @@ -1,24 +1,25 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_MATERIAL_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_MATERIAL_H_ +#ifndef BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_MATERIAL_H_ +#define BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_MATERIAL_H_ -#include "ballistica/core/object.h" -#include "ballistica/python/class/python_class.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_class.h" -namespace ballistica { +namespace ballistica::scene_v1 { class PythonClassMaterial : public PythonClass { public: - static auto type_name() -> const char* { return "Material"; } - static void SetupType(PyTypeObject* obj); + static auto type_name() -> const char*; + static void SetupType(PyTypeObject* cls); static auto Check(PyObject* o) -> bool { return PyObject_TypeCheck(o, &type_obj); } static PyTypeObject type_obj; auto GetMaterial(bool doraise = true) const -> Material* { - Material* m = material_->get(); + Material* m = material_->Get(); if ((!m) && doraise) throw Exception("Invalid Material"); return m; } @@ -41,6 +42,6 @@ class PythonClassMaterial : public PythonClass { Object::Ref* material_; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_MATERIAL_H_ +#endif // BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_MATERIAL_H_ diff --git a/src/ballistica/python/class/python_class_node.cc b/src/ballistica/scene_v1/python/class/python_class_node.cc similarity index 74% rename from src/ballistica/python/class/python_class_node.cc rename to src/ballistica/scene_v1/python/class/python_class_node.cc index e8a31a32..fad31158 100644 --- a/src/ballistica/python/class/python_class_node.cc +++ b/src/ballistica/scene_v1/python/class/python_class_node.cc @@ -1,15 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/python/class/python_class_node.h" +#include "ballistica/scene_v1/python/class/python_class_node.h" #include -#include "ballistica/core/thread.h" -#include "ballistica/python/python.h" -#include "ballistica/scene/scene.h" -#include "ballistica/scene/scene_stream.h" +#include "ballistica/scene_v1/python/scene_v1_python.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/scene_v1/support/session_stream.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Ignore a few things that python macros do. #pragma clang diagnostic push @@ -18,39 +19,43 @@ namespace ballistica { PyNumberMethods PythonClassNode::as_number_; -void PythonClassNode::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_repr = (reprfunc)tp_repr; - obj->tp_name = "ba.Node"; - obj->tp_basicsize = sizeof(PythonClassNode); - obj->tp_doc = - "Reference to a Node; the low level building block of the game.\n" +auto PythonClassNode::type_name() -> const char* { return "Node"; } + +void PythonClassNode::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + cls->tp_repr = (reprfunc)tp_repr; + // Fully qualified type path we will be exposed as: + cls->tp_name = "babase.Node"; + cls->tp_basicsize = sizeof(PythonClassNode); + cls->tp_doc = + "Reference to a Node; the low level building block of a game.\n" "\n" "Category: **Gameplay Classes**\n" "\n" "At its core, a game is nothing more than a scene of Nodes\n" "with attributes getting interconnected or set over time.\n" "\n" - "A ba.Node instance should be thought of as a weak-reference\n" + "A bascenev1.Node instance should be thought of as a weak-reference\n" "to a game node; *not* the node itself. This means a Node's\n" "lifecycle is completely independent of how many Python references\n" "to it exist. To explicitly add a new node to the game, use\n" - "ba.newnode(), and to explicitly delete one, use ba.Node.delete().\n" - "ba.Node.exists() can be used to determine if a Node still points to\n" - "a live node in the game.\n" + "bascenev1.newnode(), and to explicitly delete one,\n" + " use bascenev1.Node.delete().\n" + "babase.Node.exists() can be used to determine if a Node still points\n" + "to a live node in the game.\n" "\n" "You can use `ba.Node(None)` to instantiate an invalid\n" "Node reference (sometimes used as attr values/etc)."; - obj->tp_new = tp_new; - obj->tp_dealloc = (destructor)tp_dealloc; - obj->tp_getattro = (getattrofunc)tp_getattro; - obj->tp_setattro = (setattrofunc)tp_setattro; - obj->tp_methods = tp_methods; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_getattro = (getattrofunc)tp_getattro; + cls->tp_setattro = (setattrofunc)tp_setattro; + cls->tp_methods = tp_methods; // We provide number methods only for bool functionality. memset(&as_number_, 0, sizeof(as_number_)); as_number_.nb_bool = (inquiry)nb_bool; - obj->tp_as_number = &as_number_; + cls->tp_as_number = &as_number_; } auto PythonClassNode::Create(Node* node) -> PyObject* { @@ -60,19 +65,20 @@ auto PythonClassNode::Create(Node* node) -> PyObject* { } s_create_empty_ = true; // Prevent class from erroring on create. + assert(TypeIsSetUp(&type_obj)); auto* py_node = reinterpret_cast( PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); s_create_empty_ = false; if (!py_node) { - throw Exception("ba.Node creation failed."); + throw Exception("bascenev1.Node creation failed."); } - *(py_node->node_) = node; + *py_node->node_ = node; return reinterpret_cast(py_node); } auto PythonClassNode::GetNode(bool doraise) const -> Node* { - Node* n = node_->get(); + Node* n = node_->Get(); if (!n && doraise) { throw Exception(PyExcType::kNodeNotFound); } @@ -82,39 +88,40 @@ auto PythonClassNode::GetNode(bool doraise) const -> Node* { auto PythonClassNode::tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) -> PyObject* { auto* self = reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; - if (!InLogicThread()) { - throw Exception( - "ERROR: " + std::string(type_obj.tp_name) - + " objects must only be created in the logic thread (current is (" - + GetCurrentThreadName() + ")."); - } - // Clion incorrectly things s_create_empty will always be false. + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + // Clion incorrectly things s_create_empty will always be false. #pragma clang diagnostic push #pragma ide diagnostic ignored "ConstantConditionsOC" - if (!s_create_empty_) { - if (!PyTuple_Check(args) || (PyTuple_GET_SIZE(args) != 1) - || (keywds != nullptr) || (PyTuple_GET_ITEM(args, 0) != Py_None)) { - throw Exception( - "Can't create Nodes this way; use ba.newnode() or use " - "ba.Node(None) to get an invalid reference."); - } + if (!s_create_empty_) { + if (!PyTuple_Check(args) || (PyTuple_GET_SIZE(args) != 1) + || (keywds != nullptr) || (PyTuple_GET_ITEM(args, 0) != Py_None)) { + throw Exception( + "Can't create Nodes this way; use bascenev1.newnode() or use " + "bascenev1.Node(None) to get an invalid reference."); } - self->node_ = new Object::WeakRef(); - BA_PYTHON_NEW_CATCH; -#pragma clang diagnostic pop } + self->node_ = new Object::WeakRef(); +#pragma clang diagnostic pop return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; } void PythonClassNode::tp_dealloc(PythonClassNode* self) { BA_PYTHON_TRY; // These have to be deleted in the logic thread; send the ptr along if need // be; otherwise do it immediately. - if (!InLogicThread()) { + if (!g_base->InLogicThread()) { Object::WeakRef* n = self->node_; - g_logic->thread()->PushCall([n] { delete n; }); + g_base->logic->event_loop()->PushCall([n] { delete n; }); } else { delete self->node_; } @@ -124,10 +131,10 @@ void PythonClassNode::tp_dealloc(PythonClassNode* self) { auto PythonClassNode::tp_repr(PythonClassNode* self) -> PyObject* { BA_PYTHON_TRY; - Node* node = self->node_->get(); + Node* node = self->node_->Get(); return Py_BuildValue( "s", - std::string("id()) + " ") : "") + (node ? ("'" + node->label() + "'") : "(empty ref)") + ">") .c_str()); @@ -143,10 +150,10 @@ auto PythonClassNode::tp_getattro(PythonClassNode* self, PyObject* attr) // If our node exists and has this attr, return it. // Otherwise do default python path. - Node* node = self->node_->get(); + Node* node = self->node_->Get(); const char* attr_name = PyUnicode_AsUTF8(attr); if (node && node->HasAttribute(attr_name)) { - return Python::GetNodeAttr(node, attr_name); + return SceneV1Python::GetNodeAttr(node, attr_name); } else { return PyObject_GenericGetAttr(reinterpret_cast(self), attr); } @@ -155,7 +162,7 @@ auto PythonClassNode::tp_getattro(PythonClassNode* self, PyObject* attr) auto PythonClassNode::Exists(PythonClassNode* self) -> PyObject* { BA_PYTHON_TRY; - if (self->node_->exists()) { + if (self->node_->Exists()) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; @@ -166,7 +173,7 @@ auto PythonClassNode::Exists(PythonClassNode* self) -> PyObject* { auto PythonClassNode::GetNodeType(PythonClassNode* self) -> PyObject* { BA_PYTHON_TRY; - Node* node = self->node_->get(); + Node* node = self->node_->Get(); if (!node) { throw Exception(PyExcType::kNodeNotFound); } @@ -178,7 +185,7 @@ auto PythonClassNode::GetNodeType(PythonClassNode* self) -> PyObject* { auto PythonClassNode::GetName(PythonClassNode* self) -> PyObject* { BA_PYTHON_TRY; - Node* node = self->node_->get(); + Node* node = self->node_->Get(); if (!node) { throw Exception(PyExcType::kNodeNotFound); } @@ -197,7 +204,7 @@ auto PythonClassNode::GetDelegate(PythonClassNode* self, PyObject* args, args, keywds, "O|p", const_cast(kwlist), &tp_obj, &doraise)) { return nullptr; } - Node* node = self->node_->get(); + Node* node = self->node_->Get(); if (!node) { throw Exception(PyExcType::kNodeNotFound); } @@ -240,7 +247,7 @@ auto PythonClassNode::Delete(PythonClassNode* self, PyObject* args, args, keywds, "|i", const_cast(kwlist), &ignore_missing)) { return nullptr; } - Node* node = self->node_->get(); + Node* node = self->node_->Get(); if (!node) { if (!ignore_missing) { throw Exception(PyExcType::kNodeNotFound); @@ -260,16 +267,16 @@ auto PythonClassNode::HandleMessage(PythonClassNode* self, PyObject* args) PyErr_SetString(PyExc_AttributeError, "must provide at least 1 arg"); return nullptr; } - Buffer b; + std::vector b; PyObject* user_message_obj; - Python::DoBuildNodeMessage(args, 0, &b, &user_message_obj); + SceneV1Python::DoBuildNodeMessage(args, 0, &b, &user_message_obj); // Should we fail if the node doesn't exist?? - Node* node = self->node_->get(); + Node* node = self->node_->Get(); if (node) { - HostActivity* host_activity = node->context().GetHostActivity(); + HostActivity* host_activity = node->context_ref().GetHostActivity(); if (!host_activity) { - throw Exception("Invalid context.", PyExcType::kContext); + throw Exception("Invalid context_ref.", PyExcType::kContext); } // For user messages we pass them directly to the node // since by their nature they don't go out over the network and are just @@ -277,7 +284,7 @@ auto PythonClassNode::HandleMessage(PythonClassNode* self, PyObject* args) if (user_message_obj) { node->DispatchUserMessage(user_message_obj, "Node User-Message dispatch"); } else { - if (SceneStream* output_stream = node->scene()->GetSceneStream()) { + if (SessionStream* output_stream = node->scene()->GetSceneStream()) { output_stream->NodeMessage(node, b.data(), b.size()); } node->DispatchNodeMessage(b.data()); @@ -294,16 +301,16 @@ auto PythonClassNode::AddDeathAction(PythonClassNode* self, PyObject* args) if (!PyArg_ParseTuple(args, "O", &call_obj)) { return nullptr; } - Node* n = self->node_->get(); + Node* n = self->node_->Get(); if (!n) { throw Exception(PyExcType::kNodeNotFound); } // We don't have to go through a host-activity but lets make sure we're in // one. - HostActivity* host_activity = n->context().GetHostActivity(); + HostActivity* host_activity = n->context_ref().GetHostActivity(); if (!host_activity) { - throw Exception("Invalid context.", PyExcType::kContext); + throw Exception("Invalid context_ref.", PyExcType::kContext); } n->AddNodeDeathAction(call_obj); Py_RETURN_NONE; @@ -314,7 +321,7 @@ auto PythonClassNode::ConnectAttr(PythonClassNode* self, PyObject* args) -> PyObject* { BA_PYTHON_TRY; PyObject* dst_node_obj; - Node* node = self->node_->get(); + Node* node = self->node_->Get(); if (!node) { throw Exception(PyExcType::kNodeNotFound); } @@ -325,7 +332,7 @@ auto PythonClassNode::ConnectAttr(PythonClassNode* self, PyObject* args) } // Allow dead-refs and None. - Node* dst_node = Python::GetPyNode(dst_node_obj, true, true); + Node* dst_node = SceneV1Python::GetPyNode(dst_node_obj, true, true); if (!dst_node) { throw Exception(PyExcType::kNodeNotFound); } @@ -335,7 +342,7 @@ auto PythonClassNode::ConnectAttr(PythonClassNode* self, PyObject* args) dst_node->type()->GetAttribute(std::string(dst_attr_name)); // Push to output_stream first to catch scene mismatch errors. - if (SceneStream* output_stream = node->scene()->GetSceneStream()) { + if (SessionStream* output_stream = node->scene()->GetSceneStream()) { output_stream->ConnectNodeAttribute(node, src_attr, dst_node, dst_attr); } @@ -353,14 +360,14 @@ auto PythonClassNode::Dir(PythonClassNode* self) -> PyObject* { assert(PyList_Check(dir_list)); // ..now grab all this guy's BA attributes and add them in. - Node* node = self->node_->get(); + Node* node = self->node_->Get(); if (node) { std::list attrs; node->ListAttributes(&attrs); for (auto& attr : attrs) { PyList_Append(dir_list, PythonRef(PyUnicode_FromString(attr.c_str()), PythonRef::kSteal) - .get()); + .Get()); } } PyList_Sort(dir_list); @@ -369,7 +376,7 @@ auto PythonClassNode::Dir(PythonClassNode* self) -> PyObject* { } auto PythonClassNode::nb_bool(PythonClassNode* self) -> int { - return self->node_->exists(); + return self->node_->Exists(); } auto PythonClassNode::tp_setattro(PythonClassNode* self, PyObject* attr, @@ -378,11 +385,11 @@ auto PythonClassNode::tp_setattro(PythonClassNode* self, PyObject* attr, // FIXME: do we need to support other attr types? assert(PyUnicode_Check(attr)); - Node* n = self->node_->get(); + Node* n = self->node_->Get(); if (!n) { throw Exception(PyExcType::kNodeNotFound); } - Python::SetNodeAttr(n, PyUnicode_AsUTF8(attr), val); + SceneV1Python::SetNodeAttr(n, PyUnicode_AsUTF8(attr), val); return 0; BA_PYTHON_INT_CATCH; } @@ -402,7 +409,8 @@ PyMethodDef PythonClassNode::tp_methods[] = { "getnodetype() -> str\n" "\n" "Return the type of Node referenced by this object as a string.\n" - "(Note this is different from the Python type which is always ba.Node)"}, + "(Note this is different from the Python type which is always\n" + " bascenev1.Node)"}, {"getname", (PyCFunction)GetName, METH_NOARGS, "getname() -> str\n" "\n" @@ -415,24 +423,25 @@ PyMethodDef PythonClassNode::tp_methods[] = { "\n" "If the node has no delegate or it is not an instance of the passed\n" "type, then None will be returned. If 'doraise' is True, then an\n" - "ba.DelegateNotFoundError will be raised instead."}, + "babase.DelegateNotFoundError will be raised instead."}, {"delete", (PyCFunction)Delete, METH_VARARGS | METH_KEYWORDS, "delete(ignore_missing: bool = True) -> None\n" "\n" "Delete the node. Ignores already-deleted nodes if `ignore_missing`\n" - "is True; otherwise a ba.NodeNotFoundError is thrown."}, + "is True; otherwise a bascenev1.NodeNotFoundError is thrown."}, {"handlemessage", (PyCFunction)HandleMessage, METH_VARARGS, "handlemessage(*args: Any) -> None\n" "\n" "General message handling; can be passed any message object.\n" "\n" - "All standard message objects are forwarded along to the ba.Node's\n" - "delegate for handling (generally the ba.Actor that made the node).\n" + "All standard message objects are forwarded along to the\n" + "bascenev1.Node's delegate for handling (generally the bascenev1.Actor\n" + "that made the node).\n" "\n" - "ba.Node-s are unique, however, in that they can be passed a second\n" - "form of message; 'node-messages'. These consist of a string type-name\n" - "as a first argument along with the args specific to that type name\n" - "as additional arguments.\n" + "bascenev1.Node-s are unique, however, in that they can be passed a\n" + "second form of message; 'node-messages'. These consist of a string\n" + "type-name as a first argument along with the args specific to that type\n" + "name as additional arguments.\n" "Node-messages communicate directly with the low-level node layer\n" "and are delivered simultaneously on all game clients,\n" "acting as an alternative to setting node attributes."}, @@ -453,8 +462,8 @@ PyMethodDef PythonClassNode::tp_methods[] = { "\n" "##### Example\n" "Create a locator and attach a light to it:\n" - ">>> light = ba.newnode('light')\n" - "... loc = ba.newnode('locator', attrs={'position': (0, 10, 0)})\n" + ">>> light = bascenev1.newnode('light')\n" + "... loc = bascenev1.newnode('locator', attrs={'position': (0, 10, 0)})\n" "... loc.connectattr('position', light, 'position')\n"}, {"__dir__", (PyCFunction)Dir, METH_NOARGS, "allows inclusion of our custom attrs in standard python dir()"}, @@ -465,4 +474,4 @@ PyTypeObject PythonClassNode::type_obj; #pragma clang diagnostic pop -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/python/class/python_class_node.h b/src/ballistica/scene_v1/python/class/python_class_node.h similarity index 76% rename from src/ballistica/python/class/python_class_node.h rename to src/ballistica/scene_v1/python/class/python_class_node.h index 6e19773b..0765b87e 100644 --- a/src/ballistica/python/class/python_class_node.h +++ b/src/ballistica/scene_v1/python/class/python_class_node.h @@ -1,18 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_NODE_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_NODE_H_ +#ifndef BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_NODE_H_ +#define BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_NODE_H_ -#include "ballistica/core/object.h" -#include "ballistica/python/class/python_class.h" -#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_class.h" -namespace ballistica { +namespace ballistica::scene_v1 { class PythonClassNode : public PythonClass { public: - static auto type_name() -> const char* { return "Node"; } - static void SetupType(PyTypeObject* obj); + static auto type_name() -> const char*; + static void SetupType(PyTypeObject* cls); static auto Create(Node* node) -> PyObject*; static auto Check(PyObject* o) -> bool { return PyObject_TypeCheck(o, &type_obj); @@ -47,6 +47,6 @@ class PythonClassNode : public PythonClass { static PyNumberMethods as_number_; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_NODE_H_ +#endif // BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_NODE_H_ diff --git a/src/ballistica/scene_v1/python/class/python_class_scene_collision_mesh.cc b/src/ballistica/scene_v1/python/class/python_class_scene_collision_mesh.cc new file mode 100644 index 00000000..7453bedb --- /dev/null +++ b/src/ballistica/scene_v1/python/class/python_class_scene_collision_mesh.cc @@ -0,0 +1,116 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/python/class/python_class_scene_collision_mesh.h" + +#include "ballistica/base/logic/logic.h" +#include "ballistica/scene_v1/assets/scene_collision_mesh.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" + +namespace ballistica::scene_v1 { + +auto PythonClassSceneCollisionMesh::tp_repr(PythonClassSceneCollisionMesh* self) + -> PyObject* { + BA_PYTHON_TRY; + Object::Ref m = *self->collision_mesh_; + return Py_BuildValue( + "s", (std::string("name() + "\"") : "(empty ref)") + ">") + .c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassSceneCollisionMesh::type_name() -> const char* { + return "CollisionMesh"; +} + +void PythonClassSceneCollisionMesh::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "bascenev1.CollisionMesh"; + cls->tp_basicsize = sizeof(PythonClassSceneCollisionMesh); + cls->tp_doc = + "A reference to a collision-mesh.\n" + "\n" + "Category: **Asset Classes**\n" + "\n" + "Use bascenev1.getcollisionmesh() to instantiate one."; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; +} + +auto PythonClassSceneCollisionMesh::Create(SceneCollisionMesh* collision_mesh) + -> PyObject* { + s_create_empty_ = true; // prevent class from erroring on create + assert(TypeIsSetUp(&type_obj)); + auto* t = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + s_create_empty_ = false; + if (!t) { + throw Exception("babase.CollisionMesh creation failed."); + } + *t->collision_mesh_ = collision_mesh; + return reinterpret_cast(t); +} + +auto PythonClassSceneCollisionMesh::GetCollisionMesh(bool doraise) const + -> SceneCollisionMesh* { + SceneCollisionMesh* collision_mesh = collision_mesh_->Get(); + if (!collision_mesh && doraise) { + throw Exception("Invalid CollisionMesh.", PyExcType::kNotFound); + } + return collision_mesh; +} + +// Clion makes some incorrect inferences here. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "UnreachableCode" +#pragma ide diagnostic ignored "ConstantConditionsOC" +#pragma ide diagnostic ignored "ConstantFunctionResult" + +auto PythonClassSceneCollisionMesh::tp_new(PyTypeObject* type, PyObject* args, + PyObject* kwds) -> PyObject* { + auto* self = + reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + + if (!s_create_empty_) { + throw Exception( + "Can't instantiate CollisionMeshes directly; use " + "babase.getcollisionmesh() to get them."); + } + self->collision_mesh_ = new Object::Ref(); + return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; +} + +#pragma clang diagnostic pop + +void PythonClassSceneCollisionMesh::tp_dealloc( + PythonClassSceneCollisionMesh* self) { + BA_PYTHON_TRY; + // Our Object::Ref needs to be released in the logic thread. + auto* ptr = self->collision_mesh_; + if (g_base->InLogicThread()) { + delete ptr; + } else { + g_base->logic->event_loop()->PushCall([ptr] { delete ptr; }); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +bool PythonClassSceneCollisionMesh::s_create_empty_ = false; +PyTypeObject PythonClassSceneCollisionMesh::type_obj; + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/python/class/python_class_scene_collision_mesh.h b/src/ballistica/scene_v1/python/class/python_class_scene_collision_mesh.h new file mode 100644 index 00000000..c6ec83a8 --- /dev/null +++ b/src/ballistica/scene_v1/python/class/python_class_scene_collision_mesh.h @@ -0,0 +1,34 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_COLLISION_MESH_H_ +#define BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_COLLISION_MESH_H_ + +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::scene_v1 { + +class PythonClassSceneCollisionMesh : public PythonClass { + public: + static auto type_name() -> const char*; + static auto tp_repr(PythonClassSceneCollisionMesh* self) -> PyObject*; + static void SetupType(PyTypeObject* cls); + static PyTypeObject type_obj; + static auto Create(SceneCollisionMesh* collision_mesh) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + auto GetCollisionMesh(bool doraise = true) const -> SceneCollisionMesh*; + + private: + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) + -> PyObject*; + static void tp_dealloc(PythonClassSceneCollisionMesh* self); + static bool s_create_empty_; + Object::Ref* collision_mesh_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_COLLISION_MESH_H_ diff --git a/src/ballistica/scene_v1/python/class/python_class_scene_data_asset.cc b/src/ballistica/scene_v1/python/class/python_class_scene_data_asset.cc new file mode 100644 index 00000000..7ed427a7 --- /dev/null +++ b/src/ballistica/scene_v1/python/class/python_class_scene_data_asset.cc @@ -0,0 +1,141 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/python/class/python_class_scene_data_asset.h" + +#include "ballistica/base/logic/logic.h" +#include "ballistica/scene_v1/assets/scene_data_asset.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" + +namespace ballistica::scene_v1 { + +auto PythonClassSceneDataAsset::tp_repr(PythonClassSceneDataAsset* self) + -> PyObject* { + BA_PYTHON_TRY; + Object::Ref m = *self->data_; + return Py_BuildValue( + "s", (std::string("name() + "\"") : "(empty ref)") + ">") + .c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassSceneDataAsset::type_name() -> const char* { return "Data"; } + +void PythonClassSceneDataAsset::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "bascenev1.Data"; + cls->tp_basicsize = sizeof(PythonClassSceneDataAsset); + cls->tp_doc = + "A reference to a data object.\n" + "\n" + "Category: **Asset Classes**\n" + "\n" + "Use bascenev1.getdata() to instantiate one."; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_methods = tp_methods; +} + +auto PythonClassSceneDataAsset::Create(SceneDataAsset* data) -> PyObject* { + s_create_empty_ = true; // prevent class from erroring on create + assert(TypeIsSetUp(&type_obj)); + auto* t = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + s_create_empty_ = false; + if (!t) { + throw Exception("babase.Data creation failed."); + } + *t->data_ = data; + return reinterpret_cast(t); +} + +auto PythonClassSceneDataAsset::GetData(bool doraise) const -> SceneDataAsset* { + SceneDataAsset* data = data_->Get(); + if (!data && doraise) { + throw Exception("Invalid Data.", PyExcType::kNotFound); + } + return data; +} +// Clion makes some incorrect inferences here. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "UnreachableCode" +#pragma ide diagnostic ignored "ConstantConditionsOC" +#pragma ide diagnostic ignored "ConstantFunctionResult" + +auto PythonClassSceneDataAsset::tp_new(PyTypeObject* type, PyObject* args, + PyObject* kwds) -> PyObject* { + auto* self = + reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + + if (!s_create_empty_) { + throw Exception( + "Can't instantiate Datas directly; use bascenev1.getdata() to get " + "them."); + } + self->data_ = new Object::Ref(); + return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; +} +#pragma clang diagnostic pop + +void PythonClassSceneDataAsset::tp_dealloc(PythonClassSceneDataAsset* self) { + BA_PYTHON_TRY; + // Our Object::Ref needs to be released in the logic thread. + auto* ptr = self->data_; + if (g_base->InLogicThread()) { + delete ptr; + } else { + g_base->logic->event_loop()->PushCall([ptr] { delete ptr; }); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +auto PythonClassSceneDataAsset::GetValue(PythonClassSceneDataAsset* self) + -> PyObject* { + BA_PYTHON_TRY; + SceneDataAsset* data = self->data_->Get(); + if (data == nullptr) { + throw Exception("Invalid data object.", PyExcType::kNotFound); + } + // haha really need to rename this class. + base::DataAsset* datadata = data->data_data(); + datadata->Load(); + datadata->set_last_used_time(g_core->GetAppTimeMillisecs()); + PyObject* obj = datadata->object().Get(); + assert(obj); + Py_INCREF(obj); + return obj; + BA_PYTHON_CATCH; +} + +bool PythonClassSceneDataAsset::s_create_empty_ = false; +PyTypeObject PythonClassSceneDataAsset::type_obj; + +PyMethodDef PythonClassSceneDataAsset::tp_methods[] = { + {"getvalue", (PyCFunction)GetValue, METH_NOARGS, + "getvalue() -> Any\n" + "\n" + "Return the data object's value.\n" + "\n" + "This can consist of anything representable by json (dicts, lists,\n" + "numbers, bools, None, etc).\n" + "Note that this call will block if the data has not yet been loaded,\n" + "so it can be beneficial to plan a short bit of time between when\n" + "the data object is requested and when it's value is accessed.\n"}, + {nullptr}}; + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/python/class/python_class_scene_data_asset.h b/src/ballistica/scene_v1/python/class/python_class_scene_data_asset.h new file mode 100644 index 00000000..58df60e2 --- /dev/null +++ b/src/ballistica/scene_v1/python/class/python_class_scene_data_asset.h @@ -0,0 +1,36 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_DATA_ASSET_H_ +#define BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_DATA_ASSET_H_ + +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::scene_v1 { + +class PythonClassSceneDataAsset : public PythonClass { + public: + static auto type_name() -> const char*; + static PyTypeObject type_obj; + static auto tp_repr(PythonClassSceneDataAsset* self) -> PyObject*; + static void SetupType(PyTypeObject* cls); + static auto Create(SceneDataAsset* data) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + auto GetData(bool doraise = true) const -> SceneDataAsset*; + + private: + static PyMethodDef tp_methods[]; + static auto GetValue(PythonClassSceneDataAsset* self) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) + -> PyObject*; + static void tp_dealloc(PythonClassSceneDataAsset* self); + static bool s_create_empty_; + Object::Ref* data_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_DATA_ASSET_H_ diff --git a/src/ballistica/scene_v1/python/class/python_class_scene_mesh.cc b/src/ballistica/scene_v1/python/class/python_class_scene_mesh.cc new file mode 100644 index 00000000..303dc1f2 --- /dev/null +++ b/src/ballistica/scene_v1/python/class/python_class_scene_mesh.cc @@ -0,0 +1,109 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/python/class/python_class_scene_mesh.h" + +#include "ballistica/base/logic/logic.h" +#include "ballistica/scene_v1/assets/scene_mesh.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" + +namespace ballistica::scene_v1 { + +auto PythonClassSceneMesh::tp_repr(PythonClassSceneMesh* self) -> PyObject* { + BA_PYTHON_TRY; + Object::Ref m = *(self->mesh_); + return Py_BuildValue( + "s", (std::string("name() + "\"") : "(empty ref)") + ">") + .c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassSceneMesh::type_name() -> const char* { return "Mesh"; } + +void PythonClassSceneMesh::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "bascenev1.Mesh"; + cls->tp_basicsize = sizeof(PythonClassSceneMesh); + cls->tp_doc = + "A reference to a mesh.\n" + "\n" + "Category: **Asset Classes**\n" + "\n" + "Meshes are used for drawing.\n" + "Use bascenev1.getmesh() to instantiate one."; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; +} + +auto PythonClassSceneMesh::Create(SceneMesh* mesh) -> PyObject* { + s_create_empty_ = true; // prevent class from erroring on create + assert(TypeIsSetUp(&type_obj)); + auto* t = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + s_create_empty_ = false; + if (!t) { + throw Exception("bascenev1.Mesh creation failed."); + } + *t->mesh_ = mesh; + return reinterpret_cast(t); +} + +auto PythonClassSceneMesh::GetMesh(bool doraise) const -> SceneMesh* { + SceneMesh* mesh = mesh_->Get(); + if (!mesh && doraise) { + throw Exception("Invalid mesh.", PyExcType::kNotFound); + } + return mesh; +} + +// Clion makes some incorrect inferences here. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "UnreachableCode" +#pragma ide diagnostic ignored "ConstantConditionsOC" +#pragma ide diagnostic ignored "ConstantFunctionResult" + +auto PythonClassSceneMesh::tp_new(PyTypeObject* type, PyObject* args, + PyObject* kwds) -> PyObject* { + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + if (!s_create_empty_) { + throw Exception( + "Can't instantiate Meshes directly; use bascenev1.getmesh() to get " + "them."); + } + self->mesh_ = new Object::Ref(); + return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; +} + +#pragma clang diagnostic pop + +void PythonClassSceneMesh::tp_dealloc(PythonClassSceneMesh* self) { + BA_PYTHON_TRY; + // Our Object::Ref needs to be released in the logic thread. + auto* ptr = self->mesh_; + if (g_base->InLogicThread()) { + delete ptr; + } else { + g_base->logic->event_loop()->PushCall([ptr] { delete ptr; }); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +bool PythonClassSceneMesh::s_create_empty_ = false; +PyTypeObject PythonClassSceneMesh::type_obj; + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/python/class/python_class_scene_mesh.h b/src/ballistica/scene_v1/python/class/python_class_scene_mesh.h new file mode 100644 index 00000000..b703b5d1 --- /dev/null +++ b/src/ballistica/scene_v1/python/class/python_class_scene_mesh.h @@ -0,0 +1,34 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_MESH_H_ +#define BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_MESH_H_ + +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::scene_v1 { + +class PythonClassSceneMesh : public PythonClass { + public: + static auto type_name() -> const char*; + static auto tp_repr(PythonClassSceneMesh* self) -> PyObject*; + static void SetupType(PyTypeObject* cls); + static PyTypeObject type_obj; + static auto Create(SceneMesh* mesh) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + auto GetMesh(bool doraise = true) const -> SceneMesh*; + + private: + static bool s_create_empty_; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) + -> PyObject*; + static void tp_dealloc(PythonClassSceneMesh* self); + Object::Ref* mesh_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_MESH_H_ diff --git a/src/ballistica/scene_v1/python/class/python_class_scene_sound.cc b/src/ballistica/scene_v1/python/class/python_class_scene_sound.cc new file mode 100644 index 00000000..7b660e38 --- /dev/null +++ b/src/ballistica/scene_v1/python/class/python_class_scene_sound.cc @@ -0,0 +1,167 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/python/class/python_class_scene_sound.h" + +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" + +namespace ballistica::scene_v1 { + +auto PythonClassSceneSound::type_name() -> const char* { return "Sound"; } + +void PythonClassSceneSound::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "bascenev1.Sound"; + cls->tp_basicsize = sizeof(PythonClassSceneSound); + cls->tp_doc = + "A reference to a sound.\n" + "\n" + "Category: **Asset Classes**\n" + "\n" + "Use bascenev1.getsound() to instantiate one."; + cls->tp_methods = tp_methods; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; +} + +auto PythonClassSceneSound::tp_repr(PythonClassSceneSound* self) -> PyObject* { + BA_PYTHON_TRY; + Object::Ref m = *(self->sound_); + return Py_BuildValue( + "s", (std::string("name() + "\"") : "(empty ref)") + ">") + .c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassSceneSound::Create(SceneSound* sound) -> PyObject* { + s_create_empty_ = true; // prevent class from erroring on create + assert(TypeIsSetUp(&type_obj)); + auto* t = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + s_create_empty_ = false; + if (!t) { + throw Exception("babase.Sound creation failed."); + } + *t->sound_ = sound; + return reinterpret_cast(t); +} + +auto PythonClassSceneSound::GetSound(bool doraise) const -> SceneSound* { + SceneSound* sound = sound_->Get(); + if (!sound && doraise) { + throw Exception("Invalid Sound.", PyExcType::kNotFound); + } + return sound; +} + +// Clion makes some incorrect inferences here. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "UnreachableCode" +#pragma ide diagnostic ignored "ConstantConditionsOC" +#pragma ide diagnostic ignored "ConstantFunctionResult" + +auto PythonClassSceneSound::tp_new(PyTypeObject* type, PyObject* args, + PyObject* kwds) -> PyObject* { + auto* self = + reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + if (!s_create_empty_) { + throw Exception( + "Can't instantiate Sounds directly; use bascenev1.getsound() to get " + "them."); + } + self->sound_ = new Object::Ref(); + return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; +} + +#pragma clang diagnostic pop + +auto PythonClassSceneSound::Play(PythonClassSceneSound* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + float volume = 1.0f; + int host_only = 0; + PyObject* pos_obj = Py_None; + static const char* kwlist[] = {"volume", "position", "host_only", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|fOp", + const_cast(kwlist), &volume, + &pos_obj, &host_only)) { + return nullptr; + } + auto* sound = self->GetSound(); + + // Can play sounds in a host scene context. + if (Scene* scene = ContextRefSceneV1::FromCurrent().GetMutableScene()) { + if (sound->scene() != scene) { + throw Exception("Sound was not loaded in this context_ref.", + PyExcType::kContext); + } + if (pos_obj != Py_None) { + std::vector vals = Python::GetPyFloats(pos_obj); + if (vals.size() != 3) { + throw Exception("Expected 3 floats for pos (got " + + std::to_string(vals.size()) + ")", + PyExcType::kValue); + } + scene->PlaySoundAtPosition(sound, volume, vals[0], vals[1], vals[2], + static_cast(host_only)); + } else { + scene->PlaySound(sound, volume, static_cast(host_only)); + } + } else { + throw Exception("Can't play sounds in this context_ref.", + PyExcType::kContext); + } + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +void PythonClassSceneSound::tp_dealloc(PythonClassSceneSound* self) { + BA_PYTHON_TRY; + // Our Object::Ref needs to be released in the logic thread. + auto* ptr = self->sound_; + if (g_base->InLogicThread()) { + delete ptr; + } else { + g_base->logic->event_loop()->PushCall([ptr] { delete ptr; }); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +bool PythonClassSceneSound::s_create_empty_ = false; +PyTypeObject PythonClassSceneSound::type_obj; +PyMethodDef PythonClassSceneSound::tp_methods[] = { + { + "play", + (PyCFunction)Play, + METH_VARARGS | METH_KEYWORDS, + "play(volume: float = 1.0, position: Sequence[float] | None = None,\n" + " host_only: bool = False) -> None\n" + "\n" + "Play the sound a single time.\n" + "\n" + "Category: **Gameplay Functions**\n" + "\n" + "If position is not provided, the sound will be at a constant volume\n" + "everywhere. Position should be a float tuple of size 3.", + }, + {nullptr}}; +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/python/class/python_class_scene_sound.h b/src/ballistica/scene_v1/python/class/python_class_scene_sound.h new file mode 100644 index 00000000..9be96fed --- /dev/null +++ b/src/ballistica/scene_v1/python/class/python_class_scene_sound.h @@ -0,0 +1,37 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_SOUND_H_ +#define BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_SOUND_H_ + +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::scene_v1 { + +class PythonClassSceneSound : public PythonClass { + public: + static auto type_name() -> const char*; + static PyTypeObject type_obj; + static auto tp_repr(PythonClassSceneSound* self) -> PyObject*; + static void SetupType(PyTypeObject* cls); + static auto Create(SceneSound* sound) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + auto GetSound(bool doraise = true) const -> SceneSound*; + + private: + static auto Play(PythonClassSceneSound* self, PyObject* args, + PyObject* keywds) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) + -> PyObject*; + static void tp_dealloc(PythonClassSceneSound* self); + static PyMethodDef tp_methods[]; + static bool s_create_empty_; + Object::Ref* sound_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_SOUND_H_ diff --git a/src/ballistica/scene_v1/python/class/python_class_scene_texture.cc b/src/ballistica/scene_v1/python/class/python_class_scene_texture.cc new file mode 100644 index 00000000..0fad3617 --- /dev/null +++ b/src/ballistica/scene_v1/python/class/python_class_scene_texture.cc @@ -0,0 +1,107 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/python/class/python_class_scene_texture.h" + +#include "ballistica/base/logic/logic.h" +#include "ballistica/scene_v1/assets/scene_texture.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" + +namespace ballistica::scene_v1 { + +auto PythonClassSceneTexture::tp_repr(PythonClassSceneTexture* self) + -> PyObject* { + BA_PYTHON_TRY; + Object::Ref t = *(self->texture_); + return Py_BuildValue( + "s", (std::string("name() + "\"") : "(empty ref)") + ">") + .c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassSceneTexture::type_name() -> const char* { return "Texture"; } + +void PythonClassSceneTexture::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "babase.Texture"; + cls->tp_basicsize = sizeof(PythonClassSceneTexture); + cls->tp_doc = + "A reference to a texture.\n" + "\n" + "Category: **Asset Classes**\n" + "\n" + "Use bascenev1.gettexture() to instantiate one."; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; +} + +auto PythonClassSceneTexture::Create(SceneTexture* texture) -> PyObject* { + assert(texture != nullptr); + + // Ask Python to create one of us, which will call our tp_new method. + s_create_empty_ = true; // prevent class from erroring on create + assert(TypeIsSetUp(&type_obj)); + auto* t = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + s_create_empty_ = false; + if (!t) { + throw Exception("bascenev1.Texture creation failed."); + } + + // Store a reference to the provided ballistica object. + *t->texture_ = texture; + return reinterpret_cast(t); +} + +auto PythonClassSceneTexture::GetTexture(bool doraise) const -> SceneTexture* { + SceneTexture* texture = texture_->Get(); + if (!texture && doraise) { + throw Exception("Invalid Texture.", PyExcType::kNotFound); + } + return texture; +} + +auto PythonClassSceneTexture::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = + reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + if (!s_create_empty_) { + throw Exception( + "Can't instantiate Textures directly; use bascenev1.gettexture()" + " to get them."); + } + self->texture_ = new Object::Ref(); + return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; +} + +void PythonClassSceneTexture::tp_dealloc(PythonClassSceneTexture* self) { + BA_PYTHON_TRY; + // Our Object::Ref needs to be released in the logic thread. + auto* ptr = self->texture_; + if (g_base->InLogicThread()) { + delete self->texture_; + } else { + g_base->logic->event_loop()->PushCall([ptr] { delete ptr; }); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +bool PythonClassSceneTexture::s_create_empty_ = false; +PyTypeObject PythonClassSceneTexture::type_obj; + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/python/class/python_class_scene_texture.h b/src/ballistica/scene_v1/python/class/python_class_scene_texture.h new file mode 100644 index 00000000..2f78a1da --- /dev/null +++ b/src/ballistica/scene_v1/python/class/python_class_scene_texture.h @@ -0,0 +1,34 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_TEXTURE_H_ +#define BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_TEXTURE_H_ + +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::scene_v1 { + +class PythonClassSceneTexture : public PythonClass { + public: + static auto type_name() -> const char*; + static auto tp_repr(PythonClassSceneTexture* self) -> PyObject*; + static void SetupType(PyTypeObject* cls); + static PyTypeObject type_obj; + static auto Create(SceneTexture* texture) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + auto GetTexture(bool doraise = true) const -> SceneTexture*; + + private: + static bool s_create_empty_; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassSceneTexture* self); + Object::Ref* texture_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_TEXTURE_H_ diff --git a/src/ballistica/scene_v1/python/class/python_class_scene_timer.cc b/src/ballistica/scene_v1/python/class/python_class_scene_timer.cc new file mode 100644 index 00000000..e505b6ad --- /dev/null +++ b/src/ballistica/scene_v1/python/class/python_class_scene_timer.cc @@ -0,0 +1,141 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/python/class/python_class_scene_timer.h" + +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/support/python_context_call_runnable.h" +#include "ballistica/scene_v1/support/scene_v1_context.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" + +namespace ballistica::scene_v1 { + +auto PythonClassSceneTimer::type_name() -> const char* { return "Timer"; } + +void PythonClassSceneTimer::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "bascenev1.Timer"; + cls->tp_basicsize = sizeof(PythonClassSceneTimer); + cls->tp_doc = + "Timer(time: float, call: Callable[[], Any], repeat: bool = False)\n" + "\n" + "Timers are used to run code at later points in time.\n" + "\n" + "Category: **General Utility Classes**\n" + "\n" + "This class encapsulates a scene-time timer in the current\n" + "bascenev1.Context. The underlying timer will be destroyed when either\n" + "this object is no longer referenced or when its Context (Activity,\n" + "etc.) dies. If you do not want to worry about keeping a reference to\n" + "your timer around,\n" + "you should use the bs.timer() function instead.\n" + "\n" + "Scene time maps to local simulation time in bascenev1.Activity or\n" + "bascenev1.Session Contexts. This means that it may progress slower\n" + "in slow-motion play modes, stop when the game is paused, etc.\n" + "\n" + "###### time\n" + "> Length of time (in seconds by default) that the timer will wait\n" + "before firing. Note that the actual delay experienced may vary\n" + "depending on the timetype. (see below)\n" + "\n" + "###### call\n" + "> A callable Python object. Note that the timer will retain a\n" + "strong reference to the callable for as long as it exists, so you\n" + "may want to look into concepts such as babase.WeakCall if that is not\n" + "desired.\n" + "\n" + "###### repeat\n" + "> If True, the timer will fire repeatedly, with each successive\n" + "firing having the same delay as the first.\n" + "\n" + "##### Example\n" + "\n" + "Use a Timer object to print repeatedly for a few seconds:\n" + ">>> import bascenev1 as bs\n" + "... def say_it():\n" + "... bs.screenmessage('BADGER!')\n" + "... def stop_saying_it():\n" + "... global g_timer\n" + "... g_timer = None\n" + "... bs.screenmessage('MUSHROOM MUSHROOM!')\n" + "... # Create our timer; it will run as long as we have the self.t ref.\n" + "... g_timer = bs.Timer(0.3, say_it, repeat=True)\n" + "... # Now fire off a one-shot timer to kill it.\n" + "... bs.timer(3.89, stop_saying_it)\n"; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; +} + +auto PythonClassSceneTimer::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = + reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + + double length; + int repeat{}; + PyObject* call_obj{}; + static const char* kwlist[] = {"time", "call", "repeat", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "dO|p", + const_cast(kwlist), &length, + &call_obj, &repeat)) { + return nullptr; + } + if (length < 0.0) { + throw Exception("Timer length cannot be < 0.", PyExcType::kValue); + } + self->context_ref_ = new ContextRefSceneV1(); + self->timer_id_ = SceneV1Context::Current().NewTimer( + TimeType::kSim, static_cast(length * 1000.0), + static_cast(repeat), + Object::New(call_obj)); + self->have_timer_ = true; + + return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; +} + +static void DoDelete(bool have_timer, int timer_id, + ContextRefSceneV1* context_ref) { + assert(g_base->InLogicThread()); + if (!context_ref) { + return; + } + auto* context = context_ref->GetContextTyped(); + if (have_timer && context) { + context->DeleteTimer(TimeType::kSim, timer_id); + } + delete context_ref; +} + +void PythonClassSceneTimer::tp_dealloc(PythonClassSceneTimer* self) { + BA_PYTHON_TRY; + // These have to be deleted in the logic thread. + if (!g_base->InLogicThread()) { + auto a0 = self->have_timer_; + auto a1 = self->timer_id_; + auto a2 = self->context_ref_; + g_base->logic->event_loop()->PushCall( + [a0, a1, a2] { DoDelete(a0, a1, a2); }); + } else { + DoDelete(self->have_timer_, self->timer_id_, self->context_ref_); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +PyTypeObject PythonClassSceneTimer::type_obj; + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/python/class/python_class_scene_timer.h b/src/ballistica/scene_v1/python/class/python_class_scene_timer.h new file mode 100644 index 00000000..51005bae --- /dev/null +++ b/src/ballistica/scene_v1/python/class/python_class_scene_timer.h @@ -0,0 +1,31 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_TIMER_H_ +#define BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_TIMER_H_ + +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::scene_v1 { + +class PythonClassSceneTimer : public PythonClass { + public: + static auto type_name() -> const char*; + static void SetupType(PyTypeObject* cls); + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + + private: + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassSceneTimer* self); + int timer_id_; + ContextRefSceneV1* context_ref_; + bool have_timer_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SCENE_TIMER_H_ diff --git a/src/ballistica/python/class/python_class_session_data.cc b/src/ballistica/scene_v1/python/class/python_class_session_data.cc similarity index 53% rename from src/ballistica/python/class/python_class_session_data.cc rename to src/ballistica/scene_v1/python/class/python_class_session_data.cc index 1dc4c396..fb10d8ae 100644 --- a/src/ballistica/python/class/python_class_session_data.cc +++ b/src/ballistica/scene_v1/python/class/python_class_session_data.cc @@ -1,58 +1,78 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/python/class/python_class_session_data.h" +#include "ballistica/scene_v1/python/class/python_class_session_data.h" -#include "ballistica/core/thread.h" -#include "ballistica/generic/utils.h" -#include "ballistica/logic/logic.h" -#include "ballistica/logic/session/session.h" -#include "ballistica/python/python.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/class/python_class_context_ref.h" +#include "ballistica/scene_v1/support/session.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python.h" -namespace ballistica { +namespace ballistica::scene_v1 { auto PythonClassSessionData::nb_bool(PythonClassSessionData* self) -> int { - return self->session_->exists(); + return self->session_->Exists(); } PyNumberMethods PythonClassSessionData::as_number_; -void PythonClassSessionData::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_name = "_ba.SessionData"; - obj->tp_basicsize = sizeof(PythonClassSessionData); - obj->tp_doc = "(internal)"; - obj->tp_new = tp_new; - obj->tp_dealloc = (destructor)tp_dealloc; - obj->tp_repr = (reprfunc)tp_repr; - obj->tp_methods = tp_methods; +auto PythonClassSessionData::type_name() -> const char* { + return "SessionData"; +} + +void PythonClassSessionData::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "bascenev1.SessionData"; + cls->tp_basicsize = sizeof(PythonClassSessionData); + cls->tp_doc = "(internal)"; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_methods = tp_methods; // We provide number methods only for bool functionality. memset(&as_number_, 0, sizeof(as_number_)); as_number_.nb_bool = (inquiry)nb_bool; - obj->tp_as_number = &as_number_; + cls->tp_as_number = &as_number_; } auto PythonClassSessionData::Create(Session* session) -> PyObject* { + assert(TypeIsSetUp(&type_obj)); auto* py_session_data = reinterpret_cast( PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); BA_PRECONDITION(py_session_data); - *(py_session_data->session_) = session; + *py_session_data->session_ = session; return reinterpret_cast(py_session_data); } auto PythonClassSessionData::GetSession() const -> Session* { - Session* session = session_->get(); + Session* session = session_->Get(); if (!session) { throw Exception("Invalid SessionData.", PyExcType::kSessionNotFound); } return session; } +auto PythonClassSessionData::Context(PythonClassSessionData* self) + -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + Session* s = self->session_->Get(); + if (!s) { + throw Exception("Session is not valid."); + } + return base::PythonClassContextRef::Create(s); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + auto PythonClassSessionData::tp_repr(PythonClassSessionData* self) -> PyObject* { BA_PYTHON_TRY; return Py_BuildValue("s", (std::string("session_->get()) + " >") + + Utils::PtrToString(self->session_->Get()) + " >") .c_str()); BA_PYTHON_CATCH; } @@ -61,18 +81,19 @@ auto PythonClassSessionData::tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) -> PyObject* { auto* self = reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; - if (!InLogicThread()) { - throw Exception( - "ERROR: " + std::string(type_obj.tp_name) - + " objects must only be created in the logic thread (current is (" - + GetCurrentThreadName() + ")."); - } - self->session_ = new Object::WeakRef(); - BA_PYTHON_NEW_CATCH; + if (!self) { + return nullptr; } + BA_PYTHON_TRY; + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + self->session_ = new Object::WeakRef(); return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; } void PythonClassSessionData::tp_dealloc(PythonClassSessionData* self) { @@ -81,9 +102,9 @@ void PythonClassSessionData::tp_dealloc(PythonClassSessionData* self) { // ...send the ptr along if need be. // FIXME: technically the main thread has a pointer to a dead PyObject // until the delete goes through; could that ever be a problem? - if (!InLogicThread()) { + if (!g_base->InLogicThread()) { Object::WeakRef* s = self->session_; - g_logic->thread()->PushCall([s] { delete s; }); + g_base->logic->event_loop()->PushCall([s] { delete s; }); } else { delete self->session_; } @@ -93,7 +114,7 @@ void PythonClassSessionData::tp_dealloc(PythonClassSessionData* self) { auto PythonClassSessionData::Exists(PythonClassSessionData* self) -> PyObject* { BA_PYTHON_TRY; - Session* sgc = self->session_->get(); + Session* sgc = self->session_->Get(); if (sgc) { Py_RETURN_TRUE; } else { @@ -109,6 +130,11 @@ PyMethodDef PythonClassSessionData::tp_methods[] = { "\n" "Returns whether the SessionData still exists.\n" "Most functionality will fail on a nonexistent instance."}, + {"context", (PyCFunction)Context, METH_NOARGS, + "context() -> bascenev1.ContextRef\n" + "\n" + "Return a context-ref pointing to the session."}, + {nullptr}}; -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/python/class/python_class_session_data.h b/src/ballistica/scene_v1/python/class/python_class_session_data.h similarity index 58% rename from src/ballistica/python/class/python_class_session_data.h rename to src/ballistica/scene_v1/python/class/python_class_session_data.h index 1d32287b..a4de57bc 100644 --- a/src/ballistica/python/class/python_class_session_data.h +++ b/src/ballistica/scene_v1/python/class/python_class_session_data.h @@ -1,17 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SESSION_DATA_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SESSION_DATA_H_ +#ifndef BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SESSION_DATA_H_ +#define BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SESSION_DATA_H_ -#include "ballistica/core/object.h" -#include "ballistica/python/class/python_class.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_class.h" -namespace ballistica { +namespace ballistica::scene_v1 { class PythonClassSessionData : public PythonClass { public: - static auto type_name() -> const char* { return "SessionData"; } - static void SetupType(PyTypeObject* obj); + static auto type_name() -> const char*; + static void SetupType(PyTypeObject* cls); static auto Create(Session* session) -> PyObject*; static auto Check(PyObject* o) -> bool { return PyObject_TypeCheck(o, &type_obj); @@ -21,16 +22,18 @@ class PythonClassSessionData : public PythonClass { private: static PyMethodDef tp_methods[]; + static PyNumberMethods as_number_; static auto tp_repr(PythonClassSessionData* self) -> PyObject*; static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) -> PyObject*; static void tp_dealloc(PythonClassSessionData* self); + static auto Exists(PythonClassSessionData* self) -> PyObject*; - Object::WeakRef* session_; static auto nb_bool(PythonClassSessionData* self) -> int; - static PyNumberMethods as_number_; + static auto Context(PythonClassSessionData* self) -> PyObject*; + Object::WeakRef* session_; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SESSION_DATA_H_ +#endif // BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SESSION_DATA_H_ diff --git a/src/ballistica/python/class/python_class_session_player.cc b/src/ballistica/scene_v1/python/class/python_class_session_player.cc similarity index 78% rename from src/ballistica/python/class/python_class_session_player.cc rename to src/ballistica/scene_v1/python/class/python_class_session_player.cc index 66f6f36d..76b7c1c8 100644 --- a/src/ballistica/python/class/python_class_session_player.cc +++ b/src/ballistica/scene_v1/python/class/python_class_session_player.cc @@ -1,15 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/python/class/python_class_session_player.h" +#include "ballistica/scene_v1/python/class/python_class_session_player.h" -#include "ballistica/core/thread.h" -#include "ballistica/input/device/input_device.h" -#include "ballistica/logic/host_activity.h" -#include "ballistica/logic/player.h" -#include "ballistica/logic/session/host_session.h" -#include "ballistica/python/python.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/scene_v1/python/scene_v1_python.h" +#include "ballistica/scene_v1/support/host_session.h" +#include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python.h" -namespace ballistica { +namespace ballistica::scene_v1 { // Ignore signed bitwise stuff; python macros do it quite a bit. #pragma clang diagnostic push @@ -17,7 +17,7 @@ namespace ballistica { #pragma ide diagnostic ignored "RedundantCast" auto PythonClassSessionPlayer::nb_bool(PythonClassSessionPlayer* self) -> int { - return self->player_->exists(); + return self->player_->Exists(); } PyNumberMethods PythonClassSessionPlayer::as_number_; @@ -37,24 +37,29 @@ static const char* extra_dir_attrs[] = { ATTR_ID, ATTR_IN_GAME, ATTR_SESSIONTEAM, ATTR_COLOR, ATTR_HIGHLIGHT, ATTR_CHARACTER, ATTR_INPUT_DEVICE, nullptr}; -void PythonClassSessionPlayer::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_name = "ba.SessionPlayer"; - obj->tp_basicsize = sizeof(PythonClassSessionPlayer); +auto PythonClassSessionPlayer::type_name() -> const char* { + return "SessionPlayer"; +} + +void PythonClassSessionPlayer::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "bascenev1.SessionPlayer"; + cls->tp_basicsize = sizeof(PythonClassSessionPlayer); // clang-format off - obj->tp_doc = - "A reference to a player in the ba.Session.\n" + cls->tp_doc = + "A reference to a player in the bascenev1.Session.\n" "\n" "Category: **Gameplay Classes**\n" "\n" "These are created and managed internally and\n" - "provided to your ba.Session/ba.Activity instances.\n" - "Be aware that, like `ba.Node`s, ba.SessionPlayer objects are 'weak'\n" - "references under-the-hood; a player can leave the game at\n" + "provided to your bascenev1.Session/bascenev1.Activity instances.\n" + "Be aware that, like `ba.Node`s, bascenev1.SessionPlayer objects are\n" + "'weak' references under-the-hood; a player can leave the game at\n" " any point. For this reason, you should make judicious use of the\n" - "ba.SessionPlayer.exists() method (or boolean operator) to ensure\n" + "babase.SessionPlayer.exists() method (or boolean operator) to ensure\n" "that a SessionPlayer is still present if retaining references to one\n" "for any length of time.\n" "\n" @@ -70,17 +75,18 @@ void PythonClassSessionPlayer::SetupType(PyTypeObject* obj) { " This bool value will be True once the Player has completed\n" " any lobby character/team selection.\n" "\n" - " " ATTR_SESSIONTEAM " (ba.SessionTeam):\n" - " The ba.SessionTeam this Player is on. If the SessionPlayer\n" - " is still in its lobby selecting a team/etc. then a\n" - " ba.SessionTeamNotFoundError will be raised.\n" + " " ATTR_SESSIONTEAM " (bascenev1.SessionTeam):\n" + " The bascenev1.SessionTeam this Player is on. If the\n" + " SessionPlayer is still in its lobby selecting a team/etc.\n" + " then a bascenev1.SessionTeamNotFoundError will be raised.\n" "\n" - " " ATTR_INPUT_DEVICE " (ba.InputDevice):\n" + " " ATTR_INPUT_DEVICE " (bascenev1.InputDevice):\n" " The input device associated with the player.\n" "\n" " " ATTR_COLOR " (Sequence[float]):\n" " The base color for this Player.\n" - " In team games this will match the ba.SessionTeam's color.\n" + " In team games this will match the bascenev1.SessionTeam's\n" + " color.\n" "\n" " " ATTR_HIGHLIGHT " (Sequence[float]):\n" " A secondary color for this player.\n" @@ -91,22 +97,22 @@ void PythonClassSessionPlayer::SetupType(PyTypeObject* obj) { " " ATTR_CHARACTER " (str):\n" " The character this player has selected in their profile.\n" "\n" - " " ATTR_ACTIVITYPLAYER " (ba.Player | None):\n" + " " ATTR_ACTIVITYPLAYER " (bascenev1.Player | None):\n" " The current game-specific instance for this player.\n"; // clang-format on - obj->tp_new = tp_new; - obj->tp_repr = (reprfunc)tp_repr; - obj->tp_methods = tp_methods; - obj->tp_dealloc = (destructor)tp_dealloc; - obj->tp_getattro = (getattrofunc)tp_getattro; - obj->tp_setattro = (setattrofunc)tp_setattro; + cls->tp_new = tp_new; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_methods = tp_methods; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_getattro = (getattrofunc)tp_getattro; + cls->tp_setattro = (setattrofunc)tp_setattro; // We provide number methods only for bool functionality. memset(&as_number_, 0, sizeof(as_number_)); as_number_.nb_bool = (inquiry)nb_bool; - obj->tp_as_number = &as_number_; + cls->tp_as_number = &as_number_; } auto PythonClassSessionPlayer::Create(Player* player) -> PyObject* { @@ -115,19 +121,20 @@ auto PythonClassSessionPlayer::Create(Player* player) -> PyObject* { assert(!player->has_py_ref()); } s_create_empty_ = true; // Prevent class from erroring on create. + assert(TypeIsSetUp(&type_obj)); auto* py_player = reinterpret_cast( PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); s_create_empty_ = false; if (!py_player) { - throw Exception("ba.Player creation failed."); + throw Exception("bascenev1.Player creation failed."); } - *(py_player->player_) = player; + *py_player->player_ = player; return reinterpret_cast(py_player); } auto PythonClassSessionPlayer::GetPlayer(bool doraise) const -> Player* { - Player* player = player_->get(); + Player* player = player_->Get(); if ((!player) && doraise) { throw Exception("Invalid SessionPlayer.", PyExcType::kSessionPlayerNotFound); @@ -138,7 +145,7 @@ auto PythonClassSessionPlayer::GetPlayer(bool doraise) const -> Player* { auto PythonClassSessionPlayer::tp_repr(PythonClassSessionPlayer* self) -> PyObject* { BA_PYTHON_TRY; - Player* p = self->player_->get(); + Player* p = self->player_->Get(); int player_id = p ? p->id() : -1; std::string p_name = p ? p->GetName() : "invalid"; return Py_BuildValue("s", @@ -152,33 +159,34 @@ auto PythonClassSessionPlayer::tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) -> PyObject* { auto* self = reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; - if (!InLogicThread()) { - throw Exception( - "ERROR: " + std::string(type_obj.tp_name) - + " objects must only be created in the logic thread (current is (" - + GetCurrentThreadName() + ")."); - } + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } - // If the user is creating one, make sure they passed None to get an - // invalid ref. - // Clion incorrectly things s_create_empty will always be false. + // If the user is creating one, make sure they passed None to get an + // invalid ref. + // Clion incorrectly things s_create_empty will always be false. #pragma clang diagnostic push #pragma ide diagnostic ignored "ConstantConditionsOC" - if (!s_create_empty_) { - if (!PyTuple_Check(args) || (PyTuple_GET_SIZE(args) != 1) - || (keywds != nullptr) || (PyTuple_GET_ITEM(args, 0) != Py_None)) - throw Exception( - "Can't instantiate SessionPlayers. To create an invalid" - " SessionPlayer reference, call ba.SessionPlayer(None)."); - } - self->player_ = new Object::WeakRef(); - BA_PYTHON_NEW_CATCH; -#pragma clang diagnostic pop + if (!s_create_empty_) { + if (!PyTuple_Check(args) || (PyTuple_GET_SIZE(args) != 1) + || (keywds != nullptr) || (PyTuple_GET_ITEM(args, 0) != Py_None)) + throw Exception( + "Can't instantiate SessionPlayers. To create an invalid" + " SessionPlayer reference, call bascenev1.SessionPlayer(None)."); } + self->player_ = new Object::WeakRef(); +#pragma clang diagnostic pop return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; } void PythonClassSessionPlayer::tp_dealloc(PythonClassSessionPlayer* self) { @@ -186,9 +194,9 @@ void PythonClassSessionPlayer::tp_dealloc(PythonClassSessionPlayer* self) { // These have to be deleted in the logic thread - send the ptr along if need // be; otherwise do it immediately. - if (!InLogicThread()) { + if (!g_base->InLogicThread()) { Object::WeakRef* p = self->player_; - g_logic->thread()->PushCall([p] { delete p; }); + g_base->logic->event_loop()->PushCall([p] { delete p; }); } else { delete self->player_; } @@ -200,14 +208,14 @@ auto PythonClassSessionPlayer::tp_getattro(PythonClassSessionPlayer* self, PyObject* attr) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); + assert(g_base->InLogicThread()); // Assuming this will always be a str? assert(PyUnicode_Check(attr)); const char* s = PyUnicode_AsUTF8(attr); if (!strcmp(s, ATTR_IN_GAME)) { - Player* p = self->player_->get(); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -222,23 +230,23 @@ auto PythonClassSessionPlayer::tp_getattro(PythonClassSessionPlayer* self, Py_RETURN_TRUE; } } else if (!strcmp(s, ATTR_ID)) { - Player* p = self->player_->get(); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } return PyLong_FromLong(p->id()); } else if (!strcmp(s, ATTR_INPUT_DEVICE)) { - Player* player = self->player_->get(); + Player* player = self->player_->Get(); if (!player) { throw Exception(PyExcType::kSessionPlayerNotFound); } - InputDevice* input_device = player->GetInputDevice(); - if (input_device) { - return input_device->NewPyRef(); + + if (auto* delegate = player->input_device_delegate()) { + return delegate->NewPyRef(); } throw Exception(PyExcType::kInputDeviceNotFound); } else if (!strcmp(s, ATTR_SESSIONTEAM)) { - Player* p = self->player_->get(); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -246,14 +254,16 @@ auto PythonClassSessionPlayer::tp_getattro(PythonClassSessionPlayer* self, assert(team != nullptr); if (team == Py_None) { PyErr_SetString( - g_python->obj(Python::ObjID::kSessionTeamNotFoundError).get(), + g_base->python->objs() + .Get(base::BasePython::ObjID::kSessionTeamNotFoundError) + .Get(), "SessionTeam does not exist."); return nullptr; } Py_INCREF(team); return team; } else if (!strcmp(s, ATTR_CHARACTER)) { - Player* p = self->player_->get(); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -266,7 +276,7 @@ auto PythonClassSessionPlayer::tp_getattro(PythonClassSessionPlayer* self, Py_INCREF(obj); return obj; } else if (!strcmp(s, ATTR_COLOR)) { - Player* p = self->player_->get(); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -279,7 +289,7 @@ auto PythonClassSessionPlayer::tp_getattro(PythonClassSessionPlayer* self, Py_INCREF(obj); return obj; } else if (!strcmp(s, ATTR_HIGHLIGHT)) { - Player* p = self->player_->get(); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -292,7 +302,7 @@ auto PythonClassSessionPlayer::tp_getattro(PythonClassSessionPlayer* self, Py_INCREF(obj); return obj; } else if (!strcmp(s, ATTR_ACTIVITYPLAYER)) { - Player* p = self->player_->get(); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -322,7 +332,7 @@ auto PythonClassSessionPlayer::tp_setattro(PythonClassSessionPlayer* self, const char* s = PyUnicode_AsUTF8(attr); if (!strcmp(s, ATTR_ACTIVITYPLAYER)) { - Player* p = self->player_->get(); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -341,7 +351,7 @@ auto PythonClassSessionPlayer::GetName(PythonClassSessionPlayer* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); + assert(g_base->InLogicThread()); int full = false; int icon = true; static const char* kwlist[] = {"full", "icon", nullptr}; @@ -349,7 +359,7 @@ auto PythonClassSessionPlayer::GetName(PythonClassSessionPlayer* self, const_cast(kwlist), &full, &icon)) { return nullptr; } - Player* p = self->player_->get(); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -361,8 +371,8 @@ auto PythonClassSessionPlayer::GetName(PythonClassSessionPlayer* self, auto PythonClassSessionPlayer::Exists(PythonClassSessionPlayer* self) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); - if (self->player_->exists()) { + assert(g_base->InLogicThread()); + if (self->player_->Exists()) { Py_RETURN_TRUE; } Py_RETURN_FALSE; @@ -373,7 +383,7 @@ auto PythonClassSessionPlayer::SetName(PythonClassSessionPlayer* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); + assert(g_base->InLogicThread()); PyObject* name_obj; PyObject* full_name_obj = Py_None; @@ -385,10 +395,11 @@ auto PythonClassSessionPlayer::SetName(PythonClassSessionPlayer* self, &full_name_obj, &real)) { return nullptr; } - std::string name = Python::GetPyString(name_obj); - std::string full_name = - (full_name_obj == Py_None) ? name : Python::GetPyString(full_name_obj); - Player* p = self->player_->get(); + std::string name = g_base->python->GetPyLString(name_obj); + std::string full_name = (full_name_obj == Py_None) + ? name + : g_base->python->GetPyLString(full_name_obj); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -400,8 +411,8 @@ auto PythonClassSessionPlayer::SetName(PythonClassSessionPlayer* self, auto PythonClassSessionPlayer::ResetInput(PythonClassSessionPlayer* self) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); - Player* p = self->player_->get(); + assert(g_base->InLogicThread()); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -414,7 +425,7 @@ auto PythonClassSessionPlayer::AssignInputCall(PythonClassSessionPlayer* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); + assert(g_base->InLogicThread()); PyObject* input_type_obj; PyObject* call_obj; static const char* kwlist[] = {"type", "call", nullptr}; @@ -423,12 +434,13 @@ auto PythonClassSessionPlayer::AssignInputCall(PythonClassSessionPlayer* self, &call_obj)) { return nullptr; } - Player* player = self->player_->get(); + Player* player = self->player_->Get(); if (!player) { throw Exception(PyExcType::kSessionPlayerNotFound); } - if (Python::IsPyEnum_InputType(input_type_obj)) { - InputType input_type = Python::GetPyEnum_InputType(input_type_obj); + if (base::BasePython::IsPyEnum_InputType(input_type_obj)) { + InputType input_type = + base::BasePython::GetPyEnum_InputType(input_type_obj); player->AssignInputCall(input_type, call_obj); } else { if (!PyTuple_Check(input_type_obj)) { @@ -439,11 +451,11 @@ auto PythonClassSessionPlayer::AssignInputCall(PythonClassSessionPlayer* self, Py_ssize_t tuple_size = PyTuple_GET_SIZE(input_type_obj); for (Py_ssize_t i = 0; i < tuple_size; i++) { PyObject* obj = PyTuple_GET_ITEM(input_type_obj, i); - if (!Python::IsPyEnum_InputType(obj)) { + if (!base::BasePython::IsPyEnum_InputType(obj)) { PyErr_SetString(PyExc_TypeError, "Expected tuple of InputTypes."); return nullptr; } - InputType input_type = Python::GetPyEnum_InputType(obj); + InputType input_type = base::BasePython::GetPyEnum_InputType(obj); player->AssignInputCall(input_type, call_obj); } } @@ -454,8 +466,8 @@ auto PythonClassSessionPlayer::AssignInputCall(PythonClassSessionPlayer* self, auto PythonClassSessionPlayer::RemoveFromGame(PythonClassSessionPlayer* self) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); - Player* player = self->player_->get(); + assert(g_base->InLogicThread()); + Player* player = self->player_->Get(); if (!player) { throw Exception(PyExcType::kSessionPlayerNotFound); } else { @@ -473,8 +485,8 @@ auto PythonClassSessionPlayer::RemoveFromGame(PythonClassSessionPlayer* self) auto PythonClassSessionPlayer::GetTeam(PythonClassSessionPlayer* self) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); - Player* p = self->player_->get(); + assert(g_base->InLogicThread()); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -489,8 +501,8 @@ auto PythonClassSessionPlayer::GetTeam(PythonClassSessionPlayer* self) auto PythonClassSessionPlayer::GetV1AccountID(PythonClassSessionPlayer* self) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); - Player* p = self->player_->get(); + assert(g_base->InLogicThread()); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -506,7 +518,7 @@ auto PythonClassSessionPlayer::SetData(PythonClassSessionPlayer* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); + assert(g_base->InLogicThread()); PyObject* team_obj; PyObject* character_obj; PyObject* color_obj; @@ -518,7 +530,7 @@ auto PythonClassSessionPlayer::SetData(PythonClassSessionPlayer* self, &character_obj, &color_obj, &highlight_obj)) { return nullptr; } - Player* p = self->player_->get(); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -534,8 +546,8 @@ auto PythonClassSessionPlayer::SetData(PythonClassSessionPlayer* self, auto PythonClassSessionPlayer::GetIconInfo(PythonClassSessionPlayer* self) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); - Player* p = self->player_->get(); + assert(g_base->InLogicThread()); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -552,7 +564,7 @@ auto PythonClassSessionPlayer::SetIconInfo(PythonClassSessionPlayer* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); + assert(g_base->InLogicThread()); PyObject* texture_name_obj; PyObject* tint_texture_name_obj; PyObject* tint_color_obj; @@ -564,7 +576,7 @@ auto PythonClassSessionPlayer::SetIconInfo(PythonClassSessionPlayer* self, &tint_texture_name_obj, &tint_color_obj, &tint2_color_obj)) { return nullptr; } - Player* p = self->player_->get(); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -587,14 +599,14 @@ auto PythonClassSessionPlayer::SetActivity(PythonClassSessionPlayer* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); + assert(g_base->InLogicThread()); PyObject* activity_obj; static const char* kwlist[] = {"activity", nullptr}; if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", const_cast(kwlist), &activity_obj)) { return nullptr; } - Player* p = self->player_->get(); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -602,7 +614,7 @@ auto PythonClassSessionPlayer::SetActivity(PythonClassSessionPlayer* self, if (activity_obj == Py_None) { a = nullptr; } else { - a = Python::GetPyHostActivity(activity_obj); + a = SceneV1Python::GetPyHostActivity(activity_obj); } p->SetHostActivity(a); Py_RETURN_NONE; @@ -613,14 +625,14 @@ auto PythonClassSessionPlayer::SetNode(PythonClassSessionPlayer* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); + assert(g_base->InLogicThread()); PyObject* node_obj; static const char* kwlist[] = {"node", nullptr}; if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", const_cast(kwlist), &node_obj)) { return nullptr; } - Player* p = self->player_->get(); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -628,7 +640,7 @@ auto PythonClassSessionPlayer::SetNode(PythonClassSessionPlayer* self, if (node_obj == Py_None) { node = nullptr; } else { - node = Python::GetPyNode(node_obj); + node = SceneV1Python::GetPyNode(node_obj); } p->set_node(node); @@ -639,8 +651,8 @@ auto PythonClassSessionPlayer::SetNode(PythonClassSessionPlayer* self, auto PythonClassSessionPlayer::GetIcon(PythonClassSessionPlayer* self) -> PyObject* { BA_PYTHON_TRY; - assert(InLogicThread()); - Player* p = self->player_->get(); + assert(g_base->InLogicThread()); + Player* p = self->player_->Get(); if (!p) { throw Exception(PyExcType::kSessionPlayerNotFound); } @@ -650,7 +662,9 @@ auto PythonClassSessionPlayer::GetIcon(PythonClassSessionPlayer* self) PythonRef results; { Python::ScopedCallLabel label("get_player_icon"); - results = g_python->obj(Python::ObjID::kGetPlayerIconCall).Call(args); + results = g_scene_v1->python->objs() + .Get(SceneV1Python::ObjID::kGetPlayerIconCall) + .Call(args); } return results.NewRef(); BA_PYTHON_CATCH; @@ -668,7 +682,7 @@ auto PythonClassSessionPlayer::Dir(PythonClassSessionPlayer* self) for (const char** name = extra_dir_attrs; *name != nullptr; name++) { PyList_Append( dir_list, - PythonRef(PyUnicode_FromString(*name), PythonRef::kSteal).get()); + PythonRef(PyUnicode_FromString(*name), PythonRef::kSteal).Get()); } PyList_Sort(dir_list); return dir_list; @@ -700,8 +714,8 @@ PyMethodDef PythonClassSessionPlayer::tp_methods[] = { "\n" "Return whether the underlying player is still in the game."}, {"assigninput", (PyCFunction)AssignInputCall, METH_VARARGS | METH_KEYWORDS, - "assigninput(type: ba.InputType | tuple[ba.InputType, ...],\n" - " call: Callable) -> None\n" + "assigninput(type: bascenev1.InputType\n" + " | tuple[bascenev1.InputType, ...], call: Callable) -> None\n" "\n" "Set the python callable to be run for one or more types of input."}, {"remove_from_game", (PyCFunction)RemoveFromGame, METH_NOARGS, @@ -719,7 +733,7 @@ PyMethodDef PythonClassSessionPlayer::tp_methods[] = { "and may return None for a short while after a player initially\n" "joins (while verification occurs)."}, {"setdata", (PyCFunction)SetData, METH_VARARGS | METH_KEYWORDS, - "setdata(team: ba.SessionTeam, character: str,\n" + "setdata(team: bascenev1.SessionTeam, character: str,\n" " color: Sequence[float], highlight: Sequence[float]) -> None\n" "\n" "(internal)"}, @@ -729,11 +743,11 @@ PyMethodDef PythonClassSessionPlayer::tp_methods[] = { "\n" "(internal)"}, {"setactivity", (PyCFunction)SetActivity, METH_VARARGS | METH_KEYWORDS, - "setactivity(activity: ba.Activity | None) -> None\n" + "setactivity(activity: bascenev1.Activity | None) -> None\n" "\n" "(internal)"}, {"setnode", (PyCFunction)SetNode, METH_VARARGS | METH_KEYWORDS, - "setnode(node: Node | None) -> None\n" + "setnode(node: bascenev1.Node | None) -> None\n" "\n" "(internal)"}, {"get_icon", (PyCFunction)GetIcon, METH_NOARGS, @@ -751,4 +765,4 @@ PyMethodDef PythonClassSessionPlayer::tp_methods[] = { #pragma clang diagnostic pop -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/python/class/python_class_session_player.h b/src/ballistica/scene_v1/python/class/python_class_session_player.h similarity index 82% rename from src/ballistica/python/class/python_class_session_player.h rename to src/ballistica/scene_v1/python/class/python_class_session_player.h index bedee741..dbe92676 100644 --- a/src/ballistica/python/class/python_class_session_player.h +++ b/src/ballistica/scene_v1/python/class/python_class_session_player.h @@ -1,17 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SESSION_PLAYER_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SESSION_PLAYER_H_ +#ifndef BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SESSION_PLAYER_H_ +#define BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SESSION_PLAYER_H_ -#include "ballistica/core/object.h" -#include "ballistica/python/class/python_class.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_class.h" -namespace ballistica { +namespace ballistica::scene_v1 { class PythonClassSessionPlayer : public PythonClass { public: - static auto type_name() -> const char* { return "SessionPlayer"; } - static void SetupType(PyTypeObject* obj); + static auto type_name() -> const char*; + static void SetupType(PyTypeObject* cls); static auto Create(Player* player) -> PyObject*; static auto Check(PyObject* o) -> bool { return PyObject_TypeCheck(o, &type_obj); @@ -57,6 +58,6 @@ class PythonClassSessionPlayer : public PythonClass { static PyNumberMethods as_number_; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SESSION_PLAYER_H_ +#endif // BALLISTICA_SCENE_V1_PYTHON_CLASS_PYTHON_CLASS_SESSION_PLAYER_H_ diff --git a/src/ballistica/scene_v1/python/methods/python_methods_assets.cc b/src/ballistica/scene_v1/python/methods/python_methods_assets.cc new file mode 100644 index 00000000..58f63c19 --- /dev/null +++ b/src/ballistica/scene_v1/python/methods/python_methods_assets.cc @@ -0,0 +1,343 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/python/methods/python_methods_assets.h" + +#include +#include + +#include "ballistica/scene_v1/assets/scene_collision_mesh.h" +#include "ballistica/scene_v1/assets/scene_data_asset.h" +#include "ballistica/scene_v1/assets/scene_mesh.h" +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/assets/scene_texture.h" +#include "ballistica/scene_v1/python/scene_v1_python.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" + +namespace ballistica::scene_v1 { + +// Ignore signed bitwise stuff; python macros do it quite a bit. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" + +// ------------------------------- gettexture ---------------------------------- + +static auto PyGetTexture(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + return SceneV1Context::Current().GetTexture(name)->NewPyRef(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetTextureDef = { + "gettexture", // name + (PyCFunction)PyGetTexture, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "gettexture(name: str) -> bascenev1.Texture\n" + "\n" + "Return a texture, loading it if necessary.\n" + "\n" + "Category: **Asset Functions**\n" + "\n" + "Note that this function returns immediately even if the asset has yet\n" + "to be loaded. To avoid hitches, instantiate your asset objects in\n" + "advance of when you will be using them, allowing time for them to\n" + "load in the background if necessary."}; + +// -------------------------- get_package_texture ------------------------------ + +static auto PyGetPackageTexture(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + const char* name; + PyObject* package_obj; + static const char* kwlist[] = {"package", "name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os", + const_cast(kwlist), &package_obj, + &name)) { + return nullptr; + } + auto fullname = + g_scene_v1->python->ValidatedPackageAssetName(package_obj, name); + return SceneV1Context::Current().GetTexture(fullname)->NewPyRef(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetPackageTextureDef = { + "get_package_texture", // name + (PyCFunction)PyGetPackageTexture, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_package_texture(package: bascenev1.AssetPackage, name: str) -> " + "bascenev1.Texture\n" + "\n" + "(internal)"}; + +// ------------------------------- getsound ------------------------------------ + +static auto PyGetSound(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + return SceneV1Context::Current().GetSound(name)->NewPyRef(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetSoundDef = { + "getsound", // name + (PyCFunction)PyGetSound, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "getsound(name: str) -> bascenev1.Sound\n" + "\n" + "Return a sound, loading it if necessary.\n" + "\n" + "Category: **Asset Functions**\n" + "\n" + "Note that this function returns immediately even if the asset has yet\n" + "to be loaded. To avoid hitches, instantiate your asset objects in\n" + "advance of when you will be using them, allowing time for them to\n" + "load in the background if necessary."}; + +// --------------------------- get_package_sound ------------------------------- + +static auto PyGetPackageSound(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + PyObject* package_obj; + static const char* kwlist[] = {"package", "name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os", + const_cast(kwlist), &package_obj, + &name)) { + return nullptr; + } + auto fullname = + g_scene_v1->python->ValidatedPackageAssetName(package_obj, name); + return SceneV1Context::Current().GetSound(fullname)->NewPyRef(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetPackageSoundDef = { + "get_package_sound", // name + (PyCFunction)PyGetPackageSound, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_package_sound(package: bascenev1.AssetPackage, name: str)" + " -> bascenev1.Sound\n" + "\n" + "(internal).\n"}; + +// ------------------------------- getdata ------------------------------------- + +static auto PyGetData(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + return SceneV1Context::Current().GetData(name)->NewPyRef(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetDataDef = { + "getdata", // name + (PyCFunction)PyGetData, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "getdata(name: str) -> bascenev1.Data\n" + "\n" + "Return a data, loading it if necessary.\n" + "\n" + "Category: **Asset Functions**\n" + "\n" + "Note that this function returns immediately even if the asset has yet\n" + "to be loaded. To avoid hitches, instantiate your asset objects in\n" + "advance of when you will be using them, allowing time for them to\n" + "load in the background if necessary."}; + +// --------------------------- get_package_data -------------------------------- + +static auto PyGetPackageData(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + PyObject* package_obj; + static const char* kwlist[] = {"package", "name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os", + const_cast(kwlist), &package_obj, + &name)) { + return nullptr; + } + auto fullname = + g_scene_v1->python->ValidatedPackageAssetName(package_obj, name); + return SceneV1Context::Current().GetData(fullname)->NewPyRef(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetPackageDataDef = { + "get_package_data", // name + (PyCFunction)PyGetPackageData, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_package_data(package: bascenev1.AssetPackage, name: str)" + " -> bascenev1.Data\n" + "\n" + "(internal).\n"}; + +// -------------------------------- getmesh ------------------------------------ + +static auto PyGetMesh(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + return SceneV1Context::Current().GetMesh(name)->NewPyRef(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetMeshDef = { + "getmesh", // name + (PyCFunction)PyGetMesh, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "getmesh(name: str) -> bascenev1.Mesh\n" + "\n" + "Return a mesh, loading it if necessary.\n" + "\n" + "Category: **Asset Functions**\n" + "\n" + "Note that this function returns immediately even if the asset has yet\n" + "to be loaded. To avoid hitches, instantiate your asset objects in\n" + "advance of when you will be using them, allowing time for them to\n " + "load in the background if necessary."}; + +// ---------------------------- get_package_mesh ------------------------------- + +static auto PyGetPackageMesh(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + PyObject* package_obj; + static const char* kwlist[] = {"package", "name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os", + const_cast(kwlist), &package_obj, + &name)) { + return nullptr; + } + auto fullname = + g_scene_v1->python->ValidatedPackageAssetName(package_obj, name); + return SceneV1Context::Current().GetMesh(fullname)->NewPyRef(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetPackageMeshDef = { + "get_package_mesh", // name + (PyCFunction)PyGetPackageMesh, // method + + METH_VARARGS | METH_KEYWORDS, // flags + + "get_package_mesh(package: bascenev1.AssetPackage, name: str)" + " -> bascenev1.Mesh\n" + "\n" + "(internal)\n"}; + +// ----------------------------- getcollisionmesh ------------------------------ + +static auto PyGetCollisionMesh(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + return SceneV1Context::Current().GetCollisionMesh(name)->NewPyRef(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetCollisionMeshDef = { + "getcollisionmesh", // name + (PyCFunction)PyGetCollisionMesh, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "getcollisionmesh(name: str) -> bascenev1.CollisionMesh\n" + "\n" + "Return a collision-mesh, loading it if necessary.\n" + "\n" + "Category: **Asset Functions**\n" + "\n" + "Collision-meshes are used in physics calculations for such things as\n" + "terrain.\n" + "\n" + "Note that this function returns immediately even if the asset has yet\n" + "to be loaded. To avoid hitches, instantiate your asset objects in\n" + "advance of when you will be using them, allowing time for them to\n" + "load in the background if necessary."}; + +// ------------------------ get_package_collision_mesh ------------------------- + +static auto PyGetPackageCollisionMesh(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + const char* name; + PyObject* package_obj; + static const char* kwlist[] = {"package", "name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os", + const_cast(kwlist), &package_obj, + &name)) { + return nullptr; + } + auto fullname = + g_scene_v1->python->ValidatedPackageAssetName(package_obj, name); + return SceneV1Context::Current().GetCollisionMesh(fullname)->NewPyRef(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetPackageCollisionMeshDef = { + "get_package_collision_mesh", // name + (PyCFunction)PyGetPackageCollisionMesh, // method + + METH_VARARGS | METH_KEYWORDS, // flags + + "get_package_collision_mesh(package: bascenev1.AssetPackage, name: " + "str)\n" + "-> bascenev1.CollisionMesh\n" + "\n" + "(internal)\n"}; + +// ----------------------------------------------------------------------------- + +auto PythonMethodsAssets::GetMethods() -> std::vector { + return { + PyGetCollisionMeshDef, PyGetPackageCollisionMeshDef, + PyGetMeshDef, PyGetPackageMeshDef, + PyGetSoundDef, PyGetPackageSoundDef, + PyGetDataDef, PyGetPackageDataDef, + PyGetTextureDef, PyGetPackageTextureDef, + }; +} + +#pragma clang diagnostic pop + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/python/methods/python_methods_assets.h b/src/ballistica/scene_v1/python/methods/python_methods_assets.h new file mode 100644 index 00000000..9fe06a71 --- /dev/null +++ b/src/ballistica/scene_v1/python/methods/python_methods_assets.h @@ -0,0 +1,20 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_PYTHON_METHODS_PYTHON_METHODS_ASSETS_H_ +#define BALLISTICA_SCENE_V1_PYTHON_METHODS_PYTHON_METHODS_ASSETS_H_ + +#include + +#include "ballistica/shared/ballistica.h" + +namespace ballistica::scene_v1 { + +/// Asset related individual python methods for our module. +class PythonMethodsAssets { + public: + static auto GetMethods() -> std::vector; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_PYTHON_METHODS_PYTHON_METHODS_ASSETS_H_ diff --git a/src/ballistica/scene_v1/python/methods/python_methods_input.cc b/src/ballistica/scene_v1/python/methods/python_methods_input.cc new file mode 100644 index 00000000..9e989fc1 --- /dev/null +++ b/src/ballistica/scene_v1/python/methods/python_methods_input.cc @@ -0,0 +1,346 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/python/methods/python_methods_input.h" + +#include "ballistica/base/input/device/touch_input.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/scene_v1/python/scene_v1_python.h" +#include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" + +namespace ballistica::scene_v1 { + +// Ignore signed bitwise stuff; python macros do it quite a bit. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" + +// ----------------------- get_configurable_game_pads -------------------------- + +static auto PyGetConfigurableGamePads(PyObject* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + std::vector gamepads = + g_base->input->GetConfigurableGamePads(); + PyObject* list = PyList_New(0); + for (auto&& i : gamepads) { + // We require scene-v1 input-devices; try to cast. + base::InputDeviceDelegate* delegate = &i->delegate(); + if (auto* c_delegate = + dynamic_cast(delegate)) { + PyObject* obj = c_delegate->NewPyRef(); + PyList_Append(list, obj); + Py_DECREF(obj); + } + } + return list; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetConfigurableGamePadsDef = { + "get_configurable_game_pads", // name + PyGetConfigurableGamePads, // method + METH_VARARGS, // flags + + "get_configurable_game_pads() -> list\n" + "\n" + "(internal)\n" + "\n" + "Returns a list of the currently connected gamepads that can be\n" + "configured.", +}; + +// ------------------------ have_touchscreen_input ----------------------------- + +static auto PyHaveTouchScreenInput(PyObject* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + if (g_base->touch_input) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PyHaveTouchScreenInputDef = { + "have_touchscreen_input", // name + PyHaveTouchScreenInput, // method + METH_VARARGS, // flags + + "have_touchscreen_input() -> bool\n" + "\n" + "(internal)\n" + "\n" + "Returns whether or not a touch-screen input is present", +}; + +// ------------------------- set_touchscreen_editing --------------------------- + +static auto PySetTouchscreenEditing(PyObject* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + int editing; + if (!PyArg_ParseTuple(args, "p", &editing)) { + return nullptr; + } + if (g_base->touch_input) { + g_base->touch_input->set_editing(static_cast(editing)); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetTouchscreenEditingDef = { + "set_touchscreen_editing", // name + PySetTouchscreenEditing, // method + METH_VARARGS, // flags + + "set_touchscreen_editing(editing: bool) -> None\n" + "\n" + "(internal)", +}; + +// ------------------------- capture_gamepad_input ----------------------------- + +static auto PyCaptureGamePadInput(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->InLogicThread()); + PyObject* obj; + if (!PyArg_ParseTuple(args, "O", &obj)) { + return nullptr; + } + g_scene_v1->python->CaptureJoystickInput(obj); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyCaptureGamePadInputDef = { + "capture_gamepad_input", // name + PyCaptureGamePadInput, // method + METH_VARARGS, // flags + + "capture_gamepad_input(call: Callable[[dict], None]) -> None\n" + "\n" + "(internal)\n" + "\n" + "Add a callable to be called for subsequent gamepad events.\n" + "The method is passed a dict containing info about the event.", +}; + +// ------------------------- release_gamepad_input ----------------------------- + +static auto PyReleaseGamePadInput(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->InLogicThread()); + g_scene_v1->python->ReleaseJoystickInputCapture(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyReleaseGamePadInputDef = { + "release_gamepad_input", // name + PyReleaseGamePadInput, // method + METH_VARARGS, // flags + + "release_gamepad_input() -> None\n" + "\n" + "(internal)\n" + "\n" + "Resumes normal gamepad event processing.", +}; + +// ------------------------ capture_keyboard_input ----------------------------- + +static auto PyCaptureKeyboardInput(PyObject* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->InLogicThread()); + assert(g_scene_v1); + PyObject* obj; + if (!PyArg_ParseTuple(args, "O", &obj)) { + return nullptr; + } + g_scene_v1->python->CaptureKeyboardInput(obj); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyCaptureKeyboardInputDef = { + "capture_keyboard_input", // name + PyCaptureKeyboardInput, // method + METH_VARARGS, // flags + + "capture_keyboard_input(call: Callable[[dict], None]) -> None\n" + "\n" + "(internal)\n" + "\n" + "Add a callable to be called for subsequent keyboard-game-pad events.\n" + "The method is passed a dict containing info about the event.", +}; + +// ------------------------- release_keyboard_input ---------------------------- + +static auto PyReleaseKeyboardInput(PyObject* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->InLogicThread()); + assert(g_scene_v1); + g_scene_v1->python->ReleaseKeyboardInputCapture(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyReleaseKeyboardInputDef = { + "release_keyboard_input", // name + PyReleaseKeyboardInput, // method + METH_VARARGS, // flags + + "release_keyboard_input() -> None\n" + "\n" + "(internal)\n" + "\n" + "Resumes normal keyboard event processing.", +}; + +// --------------------------- get_ui_input_device ----------------------------- + +static auto PyGetUIInputDevice(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->InLogicThread()); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + base::InputDevice* d = g_base->ui->GetUIInputDevice(); + if (d) { + // We require scene-v1 input-devices; try to cast. + auto* delegate = &d->delegate(); + if (auto* c_delegate = + dynamic_cast(delegate)) { + return c_delegate->NewPyRef(); + } else { + // Perhaps will want to return None in this case once we've got + // newer versions of InputDevice; we'll see... + throw Exception("Unexpected input-device type owns the UI."); + } + } else { + Py_RETURN_NONE; + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetUIInputDeviceDef = { + "get_ui_input_device", // name + (PyCFunction)PyGetUIInputDevice, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_ui_input_device() -> bascenev1.InputDevice\n" + "\n" + "(internal)\n" + "\n" + "Returns the input-device that currently owns the user interface, or\n" + "None if there is none.", +}; + +// ---------------------------- getinputdevice --------------------------------- + +static auto PyGetInputDevice(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->InLogicThread()); + const char* name; + const char* unique_id; + int doraise = true; + static const char* kwlist[] = {"name", "unique_id", "doraise", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "ss|i", + const_cast(kwlist), &name, + &unique_id, &doraise)) { + return nullptr; + } + base::InputDevice* d = g_base->input->GetInputDevice(name, unique_id); + if (d) { + // We require scene-v1 input-devices; try to cast. + auto* delegate = &d->delegate(); + if (auto* c_delegate = + dynamic_cast(delegate)) { + return c_delegate->NewPyRef(); + } else { + // Perhaps will want to return None in this case once we've got + // newer versions of InputDevice; we'll see... + throw Exception("Unexpected input-device type owns the UI."); + } + } else { + if (doraise) { + throw Exception(std::string("Input device not found: '") + name + " " + + unique_id + "'.", + PyExcType::kInputDeviceNotFound); + } else { + Py_RETURN_NONE; + } + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetInputDeviceDef = { + "getinputdevice", // name + (PyCFunction)PyGetInputDevice, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "getinputdevice(name: str, unique_id: str, doraise: bool = True)\n" + " -> \n" + "\n" + "(internal)\n" + "\n" + "Given a type name and a unique identifier, returns an InputDevice.\n" + "Throws an Exception if the input-device is not found, or returns None\n" + "if 'doraise' is False.\n", +}; + +// ------------------ get_local_active_input_devices_count --------------------- + +static auto PyGetLocalActiveInputDevicesCount(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + BA_PRECONDITION(g_base->input); + return PyLong_FromLong(g_base->input->GetLocalActiveInputDeviceCount()); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetLocalActiveInputDevicesCountDef = { + "get_local_active_input_devices_count", // name + (PyCFunction)PyGetLocalActiveInputDevicesCount, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_local_active_input_devices_count() -> int\n" + "\n" + "(internal)", +}; + +// ----------------------------------------------------------------------------- + +auto PythonMethodsInput::GetMethods() -> std::vector { + return { + PyGetLocalActiveInputDevicesCountDef, + PyGetInputDeviceDef, + PyGetUIInputDeviceDef, + PyReleaseKeyboardInputDef, + PyCaptureKeyboardInputDef, + PyReleaseGamePadInputDef, + PyCaptureGamePadInputDef, + PySetTouchscreenEditingDef, + PyHaveTouchScreenInputDef, + PyGetConfigurableGamePadsDef, + }; +} + +#pragma clang diagnostic pop + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/python/methods/python_methods_input.h b/src/ballistica/scene_v1/python/methods/python_methods_input.h new file mode 100644 index 00000000..f2b0945e --- /dev/null +++ b/src/ballistica/scene_v1/python/methods/python_methods_input.h @@ -0,0 +1,20 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_PYTHON_METHODS_PYTHON_METHODS_INPUT_H_ +#define BALLISTICA_SCENE_V1_PYTHON_METHODS_PYTHON_METHODS_INPUT_H_ + +#include + +#include "ballistica/shared/ballistica.h" + +namespace ballistica::scene_v1 { + +// Input related individual python methods for our module. +class PythonMethodsInput { + public: + static auto GetMethods() -> std::vector; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_PYTHON_METHODS_PYTHON_METHODS_INPUT_H_ diff --git a/src/ballistica/scene_v1/python/methods/python_methods_networking.cc b/src/ballistica/scene_v1/python/methods/python_methods_networking.cc new file mode 100644 index 00000000..c9230427 --- /dev/null +++ b/src/ballistica/scene_v1/python/methods/python_methods_networking.cc @@ -0,0 +1,797 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/python/methods/python_methods_networking.h" + +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/networking/network_reader.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/scene_v1/connection/connection_set.h" +#include "ballistica/scene_v1/connection/connection_to_client.h" +#include "ballistica/scene_v1/connection/connection_to_host.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/shared/math/vector3f.h" +#include "ballistica/shared/networking/sockaddr.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" + +namespace ballistica::scene_v1 { + +// Ignore signed bitwise stuff; python macros do it quite a bit. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" + +// ------------------------- get_public_party_enabled +// --------------------------- + +static auto PyGetPublicPartyEnabled(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + if (appmode->public_party_enabled()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetPublicPartyEnabledDef = { + "get_public_party_enabled", // name + (PyCFunction)PyGetPublicPartyEnabled, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_public_party_enabled() -> bool\n" + "\n" + "(internal)", +}; + +// ----------------------- set_public_party_enabled ---------------------------- + +static auto PySetPublicPartyEnabled(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + int enable; + static const char* kwlist[] = {"enabled", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", + const_cast(kwlist), &enable)) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + appmode->SetPublicPartyEnabled(static_cast(enable)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetPublicPartyEnabledDef = { + "set_public_party_enabled", // name + (PyCFunction)PySetPublicPartyEnabled, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_public_party_enabled(enabled: bool) -> None\n" + "\n" + "(internal)", +}; + +// ------------------------- set_public_party_name ----------------------------- + +static auto PySetPublicPartyName(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + PyObject* name_obj; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &name_obj)) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + std::string name = g_base->python->GetPyLString(name_obj); + appmode->SetPublicPartyName(name); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetPublicPartyNameDef = { + "set_public_party_name", // name + (PyCFunction)PySetPublicPartyName, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_public_party_name(name: str) -> None\n" + "\n" + "(internal)", +}; + +// ----------------------- set_public_party_stats_url -------------------------- + +static auto PySetPublicPartyStatsURL(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + PyObject* url_obj; + static const char* kwlist[] = {"url", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &url_obj)) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + + // The call expects an empty string for the no-url option. + std::string url = (url_obj == Py_None) ? "" : Python::GetPyString(url_obj); + appmode->SetPublicPartyStatsURL(url); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetPublicPartyStatsURLDef = { + "set_public_party_stats_url", // name + (PyCFunction)PySetPublicPartyStatsURL, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_public_party_stats_url(url: str | None) -> None\n" + "\n" + "(internal)", +}; + +// ----------------------- get_public_party_max_size --------------------------- + +static auto PyGetPublicPartyMaxSize(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + return PyLong_FromLong(appmode->public_party_max_size()); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetPublicPartyMaxSizeDef = { + "get_public_party_max_size", // name + (PyCFunction)PyGetPublicPartyMaxSize, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_public_party_max_size() -> int\n" + "\n" + "(internal)", +}; + +// ----------------------- set_public_party_max_size --------------------------- + +static auto PySetPublicPartyMaxSize(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + int max_size; + static const char* kwlist[] = {"max_size", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "i", + const_cast(kwlist), &max_size)) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + appmode->SetPublicPartyMaxSize(max_size); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetPublicPartyMaxSizeDef = { + "set_public_party_max_size", // name + (PyCFunction)PySetPublicPartyMaxSize, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_public_party_max_size(max_size: int) -> None\n" + "\n" + + "(internal)", +}; + +// --------------------- set_public_party_queue_enabled ------------------------ + +static auto PySetPublicPartyQueueEnabled(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + int enabled; + static const char* kwlist[] = {"enabled", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", + const_cast(kwlist), &enabled)) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + appmode->SetPublicPartyQueueEnabled(enabled); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetPublicPartyQueueEnabledDef = { + "set_public_party_queue_enabled", // name + (PyCFunction)PySetPublicPartyQueueEnabled, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_public_party_queue_enabled(max_size: bool) -> None\n" + "\n" + "(internal)", +}; + +// ------------------------ set_authenticate_clients --------------------------- + +static auto PySetAuthenticateClients(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + int enable; + static const char* kwlist[] = {"enable", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", + const_cast(kwlist), &enable)) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + appmode->set_require_client_authentication(static_cast(enable)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetAuthenticateClientsDef = { + "set_authenticate_clients", // name + (PyCFunction)PySetAuthenticateClients, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_authenticate_clients(enable: bool) -> None\n" + "\n" + "(internal)", +}; + +// ------------------------------- set_admins ---------------------------------- + +static auto PySetAdmins(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + PyObject* admins_obj; + static const char* kwlist[] = {"admins", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &admins_obj)) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + + auto admins = g_base->python->GetPyLStrings(admins_obj); + std::set adminset; + for (auto&& admin : admins) { + adminset.insert(admin); + } + appmode->set_admin_public_ids(adminset); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetAdminsDef = { + "set_admins", // name + (PyCFunction)PySetAdmins, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_admins(admins: list[str]) -> None\n" + "\n" + "(internal)", +}; + +// --------------------- set_enable_default_kick_voting ------------------------ + +static auto PySetEnableDefaultKickVoting(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + int enable; + static const char* kwlist[] = {"enable", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", + const_cast(kwlist), &enable)) { + return nullptr; + } + assert(g_base->logic); + + if (auto* appmode{SceneV1AppMode::GetActiveOrWarn()}) { + appmode->set_kick_voting_enabled(static_cast(enable)); + } + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetEnableDefaultKickVotingDef = { + "set_enable_default_kick_voting", // name + (PyCFunction)PySetEnableDefaultKickVoting, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_enable_default_kick_voting(enable: bool) -> None\n" + "\n" + "(internal)", +}; + +// --------------------------- connect_to_party -------------------------------- + +static auto PyConnectToParty(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + std::string address; + PyObject* address_obj; + int port = kDefaultPort; + + // Whether we should print standard 'connecting...' and 'party full..' + // messages when false, only odd errors such as version incompatibility will + // be printed and most connection attempts will be silent todo: could + // generalize this to pass all results to a callback instead + int print_progress = 1; + static const char* kwlist[] = {"address", "port", "print_progress", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|ip", + const_cast(kwlist), &address_obj, + &port, &print_progress)) { + return nullptr; + } + + // Error if we're not in our app-mode. + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + + address = Python::GetPyString(address_obj); + + // Disallow in headless build (people were using this for spam-bots). + + if (g_core->HeadlessMode()) { + throw Exception("Not available in headless mode."); + } + + SockAddr s; + try { + s = SockAddr(address, port); + + // HACK: CLion currently flags our catch clause as unreachable even + // though SockAddr constructor can throw exceptions. Work around that here. + if (explicit_bool(false)) { + throw Exception(); + } + } catch (const std::exception&) { + ScreenMessage(g_base->assets->GetResourceString("invalidAddressErrorText"), + {1, 0, 0}); + Py_RETURN_NONE; + } + appmode->connections()->PushHostConnectedUDPCall( + s, static_cast(print_progress)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyConnectToPartyDef = { + "connect_to_party", // name + (PyCFunction)PyConnectToParty, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "connect_to_party(address: str, port: int | None = None,\n" + " print_progress: bool = True) -> None\n" + "\n" + "(internal)", +}; + +// ---------------------- client_info_query_response --------------------------- + +static auto PyClientInfoQueryResponse(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + const char* token; + PyObject* response_obj; + static const char* kwlist[] = {"token", "response", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "sO", + const_cast(kwlist), &token, + &response_obj)) { + return nullptr; + } + // Error if we're not in our app-mode. + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + + appmode->connections()->SetClientInfoFromMasterServer(token, response_obj); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyClientInfoQueryResponseDef = { + "client_info_query_response", // name + (PyCFunction)PyClientInfoQueryResponse, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "client_info_query_response(token: str, response: Any) -> None\n" + "\n" + "(internal)", +}; + +// ---------------------- get_connection_to_host_info -------------------------- + +static auto PyGetConnectionToHostInfo(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + // Error if we're not in our app-mode. + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + + ConnectionToHost* hc = appmode->connections()->connection_to_host(); + if (hc) { + return Py_BuildValue("{sssi}", "name", hc->party_name().c_str(), + "build_number", hc->build_number()); + } else { + return Py_BuildValue("{}"); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetConnectionToHostInfoDef = { + "get_connection_to_host_info", // name + (PyCFunction)PyGetConnectionToHostInfo, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_connection_to_host_info() -> dict\n" + "\n" + "(internal)", +}; + +// --------------------------- disconnect_from_host ---------------------------- + +static auto PyDisconnectFromHost(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + // Error if we're not in our app-mode. + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + + appmode->connections()->PushDisconnectFromHostCall(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyDisconnectFromHostDef = { + "disconnect_from_host", // name + (PyCFunction)PyDisconnectFromHost, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "disconnect_from_host() -> None\n" + "\n" + "(internal)\n" + "\n" + "Category: General Utility Functions", +}; + +// --------------------------- disconnect_client ------------------------------- + +static auto PyDisconnectClient(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + int client_id; + int ban_time = 300; // Old default before we exposed this. + static const char* kwlist[] = {"client_id", "ban_time", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|i", + const_cast(kwlist), &client_id, + &ban_time)) { + return nullptr; + } + // Error if we're not in our app-mode. + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + + bool kickable = appmode->connections()->DisconnectClient(client_id, ban_time); + if (kickable) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PyDisconnectClientDef = { + "disconnect_client", // name + (PyCFunction)PyDisconnectClient, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "disconnect_client(client_id: int, ban_time: int = 300) -> bool\n" + "\n" + "(internal)", +}; + +// --------------------- get_client_public_device_uuid ------------------------- + +static auto PyGetClientPublicDeviceUUID(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + int client_id; + static const char* kwlist[] = {"client_id", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "i", + const_cast(kwlist), &client_id)) { + return nullptr; + } + // Error if we're not in our app-mode. + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + + auto&& connection{ + appmode->connections()->connections_to_clients().find(client_id)}; + + // Does this connection exist? + if (connection == appmode->connections()->connections_to_clients().end()) { + Py_RETURN_NONE; + } + + // Connections should always be valid refs. + assert(connection->second.Exists()); + + // Old clients don't assign this; it will be empty. + if (connection->second->public_device_id().empty()) { + Py_RETURN_NONE; + } + return PyUnicode_FromString(connection->second->public_device_id().c_str()); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetClientPublicDeviceUUIDDef = { + "get_client_public_device_uuid", // name + (PyCFunction)PyGetClientPublicDeviceUUID, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_client_public_device_uuid(client_id: int) -> str | None\n" + "\n" + "(internal)\n" + "\n" + "Category: General Utility Functions\n" + "\n" + "Return a public device UUID for a client. If the client does not\n" + "exist or is running a version older than 1.6.10, returns None.\n" + "Public device UUID uniquely identifies the device the client is\n" + "using in a semi-permanent way. The UUID value will change\n" + "periodically with updates to the game or operating system.", +}; + +// ----------------------------- get_game_port --------------------------------- + +static auto PyGetGamePort(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + int port = 0; + if (g_base && g_base->network_reader != nullptr) { + // Hmmm; we're just fetching the ipv4 port here; 6 could be different. + port = g_base->network_reader->port4(); + } + return Py_BuildValue("i", port); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetGamePortDef = { + "get_game_port", // name + PyGetGamePort, // method + METH_VARARGS, // flags + + "get_game_port() -> int\n" + "\n" + "(internal)\n" + "\n" + "Return the port ballistica is hosting on.", +}; + +// ------------------------ set_master_server_source --------------------------- + +static auto PySetMasterServerSource(PyObject* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + int source; + if (!PyArg_ParseTuple(args, "i", &source)) return nullptr; + if (source != 0 && source != 1) { + BA_LOG_ONCE(LogLevel::kError, + "Invalid server source: " + std::to_string(source) + "."); + source = 1; + } + g_core->master_server_source = source; + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetMasterServerSourceDef = { + "set_master_server_source", // name + PySetMasterServerSource, // method + METH_VARARGS, // flags + + "set_master_server_source(source: int) -> None\n" + "\n" + "(internal)", +}; + +// ----------------------------- host_scan_cycle ------------------------------- + +static auto PyHostScanCycle(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + appmode->HostScanCycle(); + std::vector results = + appmode->GetScanResults(); + PyObject* py_list = PyList_New(0); + for (auto&& i : results) { + PyList_Append(py_list, Py_BuildValue("{ssss}", "display_string", + i.display_string.c_str(), "address", + i.address.c_str())); + } + return py_list; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyHostScanCycleDef = { + "host_scan_cycle", // name + (PyCFunction)PyHostScanCycle, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "host_scan_cycle() -> list\n" + "\n" + "(internal)", +}; + +// ---------------------------- end_host_scanning ------------------------------ + +static auto PyEndHostScanning(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + appmode->EndHostScanning(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyEndHostScanningDef = { + "end_host_scanning", // name + (PyCFunction)PyEndHostScanning, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "end_host_scanning() -> None\n" + "\n" + "(internal)\n" + "\n" + "Category: General Utility Functions", +}; + +// ------------------------- have_connected_clients ---------------------------- + +static auto PyHaveConnectedClients(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + if (g_base->app_mode->HasConnectionToClients()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PyHaveConnectedClientsDef = { + "have_connected_clients", // name + (PyCFunction)PyHaveConnectedClients, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "have_connected_clients() -> bool\n" + "\n" + "(internal)\n" + "\n" + "Category: General Utility Functions", +}; + +// ------------------------------ chatmessage ---------------------------------- + +static auto PyChatMessage(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + std::string message; + PyObject* message_obj; + PyObject* clients_obj = Py_None; + PyObject* sender_override_obj = Py_None; + std::string sender_override; + const std::string* sender_override_p{}; + std::vector clients; + std::vector* clients_p{}; + + static const char* kwlist[] = {"message", "clients", "sender_override", + nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|OO", + const_cast(kwlist), &message_obj, + &clients_obj, &sender_override_obj)) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + + message = g_base->python->GetPyLString(message_obj); + if (sender_override_obj != Py_None) { + sender_override = g_base->python->GetPyLString(sender_override_obj); + sender_override_p = &sender_override; + } + + if (clients_obj != Py_None) { + clients = Python::GetPyInts(clients_obj); + clients_p = &clients; + } + appmode->connections()->SendChatMessage(message, clients_p, + sender_override_p); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyChatMessageDef = { + "chatmessage", // name + (PyCFunction)PyChatMessage, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "chatmessage(message: str | babase.Lstr,\n" + " clients: Sequence[int] | None = None,\n" + " sender_override: str | None = None) -> None\n" + "\n" + "(internal)", +}; + +// --------------------------- get_chat_messages ------------------------------- + +static auto PyGetChatMessages(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + BA_PRECONDITION(g_base->InLogicThread()); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + PyObject* py_list = PyList_New(0); + for (auto&& i : appmode->chat_messages()) { + PyList_Append(py_list, PyUnicode_FromString(i.c_str())); + } + return py_list; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetChatMessagesDef = { + "get_chat_messages", // name + (PyCFunction)PyGetChatMessages, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_chat_messages() -> list[str]\n" + "\n" + "(internal)", +}; + +// ----------------------------------------------------------------------------- + +auto PythonMethodsNetworking::GetMethods() -> std::vector { + return { + PyHaveConnectedClientsDef, + PyEndHostScanningDef, + PyHostScanCycleDef, + PySetMasterServerSourceDef, + PyGetGamePortDef, + PyDisconnectFromHostDef, + PyDisconnectClientDef, + PyGetClientPublicDeviceUUIDDef, + PyGetConnectionToHostInfoDef, + PyClientInfoQueryResponseDef, + PyConnectToPartyDef, + PySetAuthenticateClientsDef, + PySetAdminsDef, + PySetEnableDefaultKickVotingDef, + PySetPublicPartyMaxSizeDef, + PySetPublicPartyQueueEnabledDef, + PyGetPublicPartyMaxSizeDef, + PySetPublicPartyStatsURLDef, + PySetPublicPartyNameDef, + PySetPublicPartyEnabledDef, + PyGetPublicPartyEnabledDef, + PyChatMessageDef, + PyGetChatMessagesDef, + }; +} + +#pragma clang diagnostic pop + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/python/methods/python_methods_networking.h b/src/ballistica/scene_v1/python/methods/python_methods_networking.h new file mode 100644 index 00000000..3963bcb5 --- /dev/null +++ b/src/ballistica/scene_v1/python/methods/python_methods_networking.h @@ -0,0 +1,19 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_PYTHON_METHODS_PYTHON_METHODS_NETWORKING_H_ +#define BALLISTICA_SCENE_V1_PYTHON_METHODS_PYTHON_METHODS_NETWORKING_H_ + +#include + +#include "ballistica/shared/ballistica.h" + +namespace ballistica::scene_v1 { + +class PythonMethodsNetworking { + public: + static auto GetMethods() -> std::vector; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_PYTHON_METHODS_PYTHON_METHODS_NETWORKING_H_ diff --git a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc new file mode 100644 index 00000000..2fdf91f3 --- /dev/null +++ b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc @@ -0,0 +1,1654 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/python/methods/python_methods_scene.h" + +#include + +#include "ballistica/base/dynamics/bg/bg_dynamics.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/python/class/python_class_simple_sound.h" +#include "ballistica/base/python/support/python_context_call_runnable.h" +#include "ballistica/base/support/plus_soft.h" +#include "ballistica/core/python/core_python.h" +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/assets/scene_texture.h" +#include "ballistica/scene_v1/connection/connection_set.h" +#include "ballistica/scene_v1/connection/connection_to_client.h" +#include "ballistica/scene_v1/dynamics/collision.h" +#include "ballistica/scene_v1/dynamics/dynamics.h" +#include "ballistica/scene_v1/dynamics/material/material_action.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/scene_v1/python/class/python_class_activity_data.h" +#include "ballistica/scene_v1/python/class/python_class_session_data.h" +#include "ballistica/scene_v1/python/scene_v1_python.h" +#include "ballistica/scene_v1/support/client_session_replay.h" +#include "ballistica/scene_v1/support/host_activity.h" +#include "ballistica/scene_v1/support/host_session.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" +#include "ballistica/scene_v1/support/session_stream.h" +#include "ballistica/shared/generic/json.h" +#include "ballistica/shared/generic/utils.h" + +namespace ballistica::scene_v1 { + +// Ignore signed bitwise stuff; python macros do it quite a bit. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" + +// --------------------------------- time -------------------------------------- + +static auto PyTime(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + + return PyFloat_FromDouble( + 0.001 + * static_cast(SceneV1Context::Current().GetTime(TimeType::kSim))); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyTimeDef = { + "time", // name + (PyCFunction)PyTime, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "time() -> bascenev1.Time\n" + "\n" + "Return the current scene time in seconds.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "Scene time maps to local simulation time in bascenev1.Activity or\n" + "bascenev1.Session Contexts. This means that it may progress slower\n" + "in slow-motion play modes, stop when the game is paused, etc.\n" + "\n" + "Note that the value returned here is simply a float; it just has a\n" + "unique type in the type-checker's eyes to help prevent it from being\n" + "accidentally used with time functionality expecting other time types.", +}; + +// --------------------------------- timer ------------------------------------- + +static auto PyTimer(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->InLogicThread()); + + double length; + int repeat = 0; + PyObject* call_obj; + static const char* kwlist[] = {"time", "call", "repeat", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "dO|p", + const_cast(kwlist), &length, + &call_obj, &repeat)) { + return nullptr; + } + if (length < 0.0) { + throw Exception("Timer length cannot be < 0.", PyExcType::kValue); + } + SceneV1Context::Current().NewTimer( + TimeType::kSim, static_cast(length * 1000.0), + static_cast(repeat), + Object::New(call_obj)); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyTimerDef = { + "timer", // name + (PyCFunction)PyTimer, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "timer(time: float, call: Callable[[], Any], repeat: bool = False)\n" + " -> None\n" + "\n" + "Schedule a call to run at a later point in time.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "This function adds a scene-time timer to the current babase.Context.\n" + "This timer cannot be canceled or modified once created. If you\n" + " require the ability to do so, use the babase.Timer class instead.\n" + "\n" + "Scene time maps to local simulation time in bascenev1.Activity or\n" + "bascenev1.Session Contexts. This means that it may progress slower\n" + "in slow-motion play modes, stop when the game is paused, etc.\n" + "\n" + "##### Arguments\n" + "###### time (float)\n" + "> Length of scene time in seconds that the timer will wait\n" + "before firing.\n" + "\n" + "###### call (Callable[[], Any])\n" + "> A callable Python object. Note that the timer will retain a\n" + "strong reference to the callable for as long as it exists, so you\n" + "may want to look into concepts such as babase.WeakCall if that is not\n" + "desired.\n" + "\n" + "###### repeat (bool)\n" + "> If True, the timer will fire repeatedly, with each successive\n" + "firing having the same delay as the first.\n" + "\n" + "##### Examples\n" + "Print some stuff through time:\n" + ">>> import bascenev1 as bs\n" + ">>> bs.screenmessage('hello from now!')\n" + ">>> bs.timer(1.0, bs.Call(bs.screenmessage, 'hello from the " + "future!'))\n" + ">>> bs.timer(2.0, bs.Call(bs.screenmessage,\n" + "... 'hello from the future 2!'))\n", +}; + +// ----------------------------- basetime ----------------------------------- + +static auto PyBaseTime(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + auto timeval = SceneV1Context::Current().GetTime(TimeType::kBase); + return PyFloat_FromDouble(0.001 * static_cast(timeval)); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyBaseTimeDef = { + "basetime", // name + (PyCFunction)PyBaseTime, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "basetime() -> bascenev1.BaseTime\n" + "\n" + "Return the base-time in seconds for the current scene-v1 context.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "Base-time is a time value that progresses at a constant rate for a " + "scene,\n" + "even when the scene is sped up, slowed down, or paused. It may, however,\n" + "speed up or slow down due to replay speed adjustments or may slow down\n" + "if the cpu is overloaded." + "\n" + "Note that the value returned here is simply a float; it just has a\n" + "unique type in the type-checker's eyes to help prevent it from being\n" + "accidentally used with time functionality expecting other time types.", +}; + +// --------------------------------- timer ------------------------------------- + +static auto PyBaseTimer(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->InLogicThread()); + + double length{}; + int repeat{}; + PyObject* call_obj{}; + static const char* kwlist[] = {"time", "call", "repeat", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "dO|p", + const_cast(kwlist), &length, + &call_obj, &repeat)) { + return nullptr; + } + + SceneV1Context::Current().NewTimer( + TimeType::kBase, static_cast(length * 1000.0), + static_cast(repeat), + Object::New(call_obj)); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyBaseTimerDef = { + "basetimer", // name + (PyCFunction)PyBaseTimer, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "basetimer(time: float, call: Callable[[], Any], repeat: bool = False)\n" + " -> None\n" + "\n" + "Schedule a call to run at a later point in scene base-time.\n" + "Base-time is a value that progresses at a constant rate for a scene,\n" + " even when the scene is sped up, slowed down, or paused. It may,\n" + " however, speed up or slow down due to replay speed adjustments or may\n" + " slow down if the cpu is overloaded.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "This function adds a timer to the current scene context.\n" + "This timer cannot be canceled or modified once created. If you\n" + " require the ability to do so, use the bascenev1.BaseTimer class\n " + "instead.\n" + "\n" + "##### Arguments\n" + "###### time (float)\n" + "> Length of time in seconds that the timer will wait before firing.\n" + "\n" + "###### call (Callable[[], Any])\n" + "> A callable Python object. Remember that the timer will retain a\n" + "strong reference to the callable for the duration of the timer, so you\n" + "may want to look into concepts such as babase.WeakCall if that is not\n" + "desired.\n" + "\n" + "###### repeat (bool)\n" + "> If True, the timer will fire repeatedly, with each successive\n" + "firing having the same delay as the first.\n" + "\n" + "##### Examples\n" + "Print some stuff through time:\n" + ">>> import bascenev1 as bs\n" + ">>> bs.screenmessage('hello from now!')\n" + ">>> bs.basetimer(1.0, bs.Call(bs.screenmessage, 'hello from the " + "future!'))\n" + ">>> bs.basetimer(2.0, bs.Call(bs.screenmessage,\n" + "... 'hello from the future 2!'))\n", +}; + +// ------------------------------- getsession ---------------------------------- + +static auto PyGetSession(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + int raise = true; + static const char* kwlist[] = {"doraise", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|i", + const_cast(kwlist), &raise)) { + return nullptr; + } + if (HostSession* hs = ContextRefSceneV1::FromCurrent().GetHostSession()) { + PyObject* obj = hs->GetSessionPyObj(); + if (obj) { + Py_INCREF(obj); + return obj; + } + } else { + if (raise) { + throw Exception(PyExcType::kSessionNotFound); + } + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetSessionDef = { + "getsession", // name + (PyCFunction)PyGetSession, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "getsession(doraise: bool = True) -> \n" + "\n" + "Category: **Gameplay Functions**\n" + "\n" + "Returns the current bascenev1.Session instance.\n" + "Note that this is based on context_ref; thus code being run in the UI\n" + "context will return the UI context_ref here even if a game Session " + "also\n" + "exists, etc. If there is no current Session, an Exception is raised, " + "or\n" + "if doraise is False then None is returned instead.", +}; + +// --------------------------- new_host_session -------------------------------- + +static auto PyNewHostSession(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* benchmark_type_str = nullptr; + static const char* kwlist[] = {"sessiontype", "benchmark_type", nullptr}; + PyObject* sessiontype_obj; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|s", + const_cast(kwlist), &sessiontype_obj, + &benchmark_type_str)) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + base::BenchmarkType benchmark_type = base::BenchmarkType::kNone; + if (benchmark_type_str != nullptr) { + if (!strcmp(benchmark_type_str, "cpu")) { + benchmark_type = base::BenchmarkType::kCPU; + } else if (!strcmp(benchmark_type_str, "gpu")) { + benchmark_type = base::BenchmarkType::kGPU; + } else { + throw Exception( + "Invalid benchmark type: '" + std::string(benchmark_type_str) + "'", + PyExcType::kValue); + } + } + appmode->LaunchHostSession(sessiontype_obj, benchmark_type); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyNewHostSessionDef = { + "new_host_session", // name + (PyCFunction)PyNewHostSession, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "new_host_session(sessiontype: type[bascenev1.Session],\n" + " benchmark_type: str | None = None) -> None\n" + "\n" + "(internal)", +}; + +// -------------------------- new_replay_session ------------------------------- + +static auto PyNewReplaySession(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + std::string file_name; + PyObject* file_name_obj; + static const char* kwlist[] = {"file_name", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "O", const_cast(kwlist), &file_name_obj)) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + + file_name = Python::GetPyString(file_name_obj); + appmode->LaunchReplaySession(file_name); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyNewReplaySessionDef = { + "new_replay_session", // name + (PyCFunction)PyNewReplaySession, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "new_replay_session(file_name: str) -> None\n" + "\n" + "(internal)", +}; + +// ------------------------------ is_in_replay --------------------------------- + +static auto PyIsInReplay(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + if (dynamic_cast(appmode->GetForegroundSession())) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PyIsInReplayDef = { + "is_in_replay", // name + (PyCFunction)PyIsInReplay, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "is_in_replay() -> bool\n" + "\n" + "(internal)", +}; + +// -------------------------- register_session-------- ------------------------- + +static auto PyRegisterSession(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->InLogicThread()); + PyObject* session_obj; + static const char* kwlist[] = {"session", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &session_obj)) { + return nullptr; + } + HostSession* hsc = ContextRefSceneV1::FromCurrent().GetHostSession(); + if (!hsc) { + throw Exception("No HostSession found."); + } + + // Store our py obj with our HostSession and return + // the HostSession to be stored with our py obj. + hsc->RegisterPySession(session_obj); + return PythonClassSessionData::Create(hsc); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyRegisterSessionDef = { + "register_session", // name + (PyCFunction)PyRegisterSession, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "register_session(session: bascenev1.Session)" + " -> bascenev1.SessionData\n" + "\n" + "(internal)", +}; + +// --------------------------- register_activity ------------------------------- + +static auto PyRegisterActivity(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->InLogicThread()); + PyObject* activity_obj; + static const char* kwlist[] = {"activity", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &activity_obj)) { + return nullptr; + } + HostSession* hs = ContextRefSceneV1::FromCurrent().GetHostSession(); + if (!hs) { + throw Exception("No HostSession found"); + } + + // Generate and return an ActivityData for this guy.. + // (basically just a link to its C++ equivalent). + return PythonClassActivityData::Create(hs->RegisterPyActivity(activity_obj)); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyRegisterActivityDef = { + "register_activity", // name + (PyCFunction)PyRegisterActivity, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "register_activity(activity: bascenev1.Activity)" + " -> bascenev1.ActivityData\n" + "\n" + "(internal)", +}; + +// ---------------------- get_foreground_host_session -------------------------- + +static auto PyGetForegroundHostSession(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + + // Note: we return None if not in the logic thread. + HostSession* s = + g_base->InLogicThread() + ? ContextRefSceneV1::FromAppForegroundContext().GetHostSession() + : nullptr; + if (s != nullptr) { + PyObject* obj = s->GetSessionPyObj(); + Py_INCREF(obj); + return obj; + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetForegroundHostSessionDef = { + "get_foreground_host_session", // name + (PyCFunction)PyGetForegroundHostSession, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_foreground_host_session() -> bascenev1.Session | None\n" + "\n" + "(internal)\n" + "\n" + "Return the bascenev1.Session currently being displayed," + " or None if there is\n" + "none.", +}; + +// ----------------------------- newactivity ----------------------------------- + +static auto PyNewActivity(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + static const char* kwlist[] = {"activity_type", "settings", nullptr}; + PyObject* activity_type_obj; + PyObject* settings_obj = Py_None; + PythonRef settings; + + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|O", + const_cast(kwlist), + &activity_type_obj, &settings_obj)) { + return nullptr; + } + + // If they passed a settings dict, make a shallow copy of it (so we dont + // inadvertently mess up level lists or whatever the settings came from). + if (settings_obj != Py_None) { + if (!PyDict_Check(settings_obj)) { + throw Exception("Expected a dict for settings.", PyExcType::kType); + } + PythonRef args2(Py_BuildValue("(O)", settings_obj), PythonRef::kSteal); + settings = g_core->python->objs() + .Get(core::CorePython::ObjID::kShallowCopyCall) + .Call(args2); + if (!settings.Exists()) { + throw Exception("Unable to shallow-copy settings."); + } + } else { + settings.Acquire(settings_obj); + } + + HostSession* hs = ContextRefSceneV1::FromCurrent().GetHostSession(); + if (!hs) { + throw Exception("No HostSession found.", PyExcType::kContext); + } + return hs->NewHostActivity(activity_type_obj, settings.Get()); + + BA_PYTHON_CATCH; +} + +static PyMethodDef PyNewActivityDef = { + "newactivity", // name + (PyCFunction)PyNewActivity, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "newactivity(activity_type: type[bascenev1.Activity],\n" + " settings: dict | None = None) -> bascenev1.Activity\n" + "\n" + "Instantiates a bascenev1.Activity given a type object.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "Activities require special setup and thus cannot be directly\n" + "instantiated; you must go through this function.", +}; + +// ----------------------------- getactivity ----------------------------------- + +static auto PyGetActivity(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + int raise = true; + static const char* kwlist[] = {"doraise", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|i", + const_cast(kwlist), &raise)) { + return nullptr; + } + + // Fail gracefully if called from outside the logic thread. + if (!g_base->InLogicThread()) { + Py_RETURN_NONE; + } + + if (HostActivity* hostactivity = + ContextRefSceneV1::FromCurrent().GetHostActivity()) { + PyObject* obj = hostactivity->GetPyActivity(); + Py_INCREF(obj); + return obj; + } else { + if (raise) { + throw Exception(PyExcType::kActivityNotFound); + } + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetActivityDef = { + "getactivity", // name + (PyCFunction)PyGetActivity, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "getactivity(doraise: bool = True) -> \n" + "\n" + "Return the current bascenev1.Activity instance.\n" + "\n" + "Category: **Gameplay Functions**\n" + "\n" + "Note that this is based on context_ref; thus code run in a timer\n" + "generated in Activity 'foo' will properly return 'foo' here, even if\n" + "another Activity has since been created or is transitioning in.\n" + "If there is no current Activity, raises a babase.ActivityNotFoundError.\n" + "If doraise is False, None will be returned instead in that case.", +}; + +// ---------------------------- screenmessage ---------------------------------- + +static auto PyScreenMessage(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* message = nullptr; + PyObject* color_obj = Py_None; + int top = 0; + int transient = 0; + PyObject* image_obj = Py_None; + PyObject* message_obj; + PyObject* clients_obj = Py_None; + int log = 0; + static const char* kwlist[] = {"message", "color", "top", "image", + "log", "clients", "transient", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "O|OpOiOi", const_cast(kwlist), &message_obj, + &color_obj, &top, &image_obj, &log, &clients_obj, &transient)) { + return nullptr; + } + std::string message_str = g_base->python->GetPyLString(message_obj); + message = message_str.c_str(); + Vector3f color{1, 1, 1}; + if (color_obj != Py_None) { + color = base::BasePython::GetPyVector3f(color_obj); + } + if (message == nullptr) { + PyErr_SetString(PyExc_AttributeError, "No message provided"); + return nullptr; + } + if (log) { + Log(LogLevel::kInfo, message); + } + + // Transient messages get sent to clients as high-level messages instead of + // being embedded into the game-stream. + if (transient) { + // This option doesn't support top or icons currently. + if (image_obj != Py_None) { + throw Exception( + "The 'image' option is not currently supported for transient mode " + "messages.", + PyExcType::kValue); + } + if (top) { + throw Exception( + "The 'top' option is not currently supported for transient mode " + "messages.", + PyExcType::kValue); + } + std::vector client_ids; + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (clients_obj != Py_None) { + std::vector client_ids2 = Python::GetPyInts(clients_obj); + appmode->connections()->SendScreenMessageToSpecificClients( + message, color.x, color.y, color.z, client_ids2); + } else { + appmode->connections()->SendScreenMessageToAll(message, color.x, + color.y, color.z); + } + } + } else { + // Currently specifying client_ids only works for transient messages; we'd + // need a protocol change to support that in game output streams. + // (or maintaining separate streams per client; yuck) + if (clients_obj != Py_None) { + throw Exception( + "Specifying clients only works when using the 'transient' option", + PyExcType::kValue); + } + Scene* context_scene = ContextRefSceneV1::FromCurrent().GetMutableScene(); + SessionStream* output_stream = + context_scene ? context_scene->GetSceneStream() : nullptr; + + SceneTexture* texture = nullptr; + SceneTexture* tint_texture = nullptr; + Vector3f tint_color{1.0f, 1.0f, 1.0f}; + Vector3f tint2_color{1.0f, 1.0f, 1.0f}; + if (image_obj != Py_None) { + if (PyDict_Check(image_obj)) { + PyObject* obj = PyDict_GetItemString(image_obj, "texture"); + if (!obj) { + throw Exception("Provided image dict contains no 'texture' entry.", + PyExcType::kValue); + } + texture = SceneV1Python::GetPySceneTexture(obj); + + obj = PyDict_GetItemString(image_obj, "tint_texture"); + if (!obj) { + throw Exception( + "Provided image dict contains no 'tint_texture' entry.", + PyExcType::kValue); + } + tint_texture = SceneV1Python::GetPySceneTexture(obj); + + obj = PyDict_GetItemString(image_obj, "tint_color"); + if (!obj) { + throw Exception("Provided image dict contains no 'tint_color' entry", + PyExcType::kValue); + } + tint_color = base::BasePython::GetPyVector3f(obj); + obj = PyDict_GetItemString(image_obj, "tint2_color"); + if (!obj) { + throw Exception("Provided image dict contains no 'tint2_color' entry", + PyExcType::kValue); + } + tint2_color = base::BasePython::GetPyVector3f(obj); + } else { + texture = SceneV1Python::GetPySceneTexture(image_obj); + } + } + if (output_stream) { + // FIXME: for now we just do bottom messages. + if (texture == nullptr && !top) { + output_stream->ScreenMessageBottom(message, color.x, color.y, color.z); + } else if (top && texture != nullptr && tint_texture != nullptr) { + if (texture->scene() != context_scene) { + throw Exception("Texture is not from the current context_ref.", + PyExcType::kContext); + } + if (tint_texture->scene() != context_scene) + throw Exception("Tint-texture is not from the current context_ref.", + PyExcType::kContext); + output_stream->ScreenMessageTop( + message, color.x, color.y, color.z, texture, tint_texture, + tint_color.x, tint_color.y, tint_color.z, tint2_color.x, + tint2_color.y, tint2_color.z); + } else { + Log(LogLevel::kError, "Unhandled screenmessage output_stream case."); + } + } + + // Now display it locally. + g_base->graphics->AddScreenMessage( + message, color, static_cast(top), + texture ? texture->texture_data() : nullptr, + tint_texture ? tint_texture->texture_data() : nullptr, tint_color, + tint2_color); + } + + Py_RETURN_NONE; + + BA_PYTHON_CATCH; +} + +static PyMethodDef PyScreenMessageDef = { + "screenmessage", // name + (PyCFunction)PyScreenMessage, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "screenmessage(message: str | babase.Lstr,\n" + " color: Sequence[float] | None = None,\n" + " top: bool = False,\n" + " image: dict[str, Any] | None = None,\n" + " log: bool = False,\n" + " clients: Sequence[int] | None = None,\n" + " transient: bool = False)" + " -> None\n" + "\n" + "Print a message to the local client's screen, in a given color.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "If 'top' is True, the message will go to the top message area.\n" + "For 'top' messages, 'image' must be a dict containing 'texture'\n" + "and 'tint_texture' textures and 'tint_color' and 'tint2_color'\n" + "colors. This defines an icon to display alongside the message.\n" + "If 'log' is True, the message will also be submitted to the log.\n" + "'clients' can be a list of client-ids the message should be sent\n" + "to, or None to specify that everyone should receive it.\n" + "If 'transient' is True, the message will not be included in the\n" + "game-stream and thus will not show up when viewing replays.\n" + "Currently the 'clients' option only works for transient messages.", +}; + +// ------------------------------- newnode ------------------------------------- + +static auto PyNewNode(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Node* n = SceneV1Python::DoNewNode(args, keywds); + if (!n) { + return nullptr; + } + return n->NewPyRef(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyNewNodeDef = { + "newnode", // name + (PyCFunction)PyNewNode, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "newnode(type: str, owner: bascenev1.Node | None = None,\n" + " attrs: dict | None = None,\n" + " name: str | None = None,\n" + " delegate: Any = None) -> bascenev1.Node\n" + "\n" + "Add a node of the given type to the game.\n" + "\n" + "Category: **Gameplay Functions**\n" + "\n" + "If a dict is provided for 'attributes', the node's initial attributes\n" + "will be set based on them.\n" + "\n" + "'name', if provided, will be stored with the node purely for " + "debugging\n" + "purposes. If no name is provided, an automatic one will be generated\n" + "such as 'terrain@foo.py:30'.\n" + "\n" + "If 'delegate' is provided, Python messages sent to the node will go " + "to\n" + "that object's handlemessage() method. Note that the delegate is " + "stored\n" + "as a weak-ref, so the node itself will not keep the object alive.\n" + "\n" + "if 'owner' is provided, the node will be automatically killed when " + "that\n" + "object dies. 'owner' can be another node or a bascenev1.Actor", +}; + +// ----------------------------- printnodes ------------------------------------ + +static auto PyPrintNodes(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + HostActivity* host_activity = + ContextRefSceneV1::FromAppForegroundContext().GetHostActivity(); + if (!host_activity) { + throw Exception(PyExcType::kContext); + } + Scene* scene = host_activity->scene(); + std::string s; + int count = 1; + for (auto&& i : scene->nodes()) { + char buffer[128]; + snprintf(buffer, sizeof(buffer), "#%d: type: %-14s desc: %s", count, + i->type()->name().c_str(), i->label().c_str()); + s += buffer; + Log(LogLevel::kInfo, buffer); + count++; + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyPrintNodesDef = { + "printnodes", // name + PyPrintNodes, // method + METH_VARARGS, // flags + + "printnodes() -> None\n" + "\n" + "Print various info about existing nodes; useful for debugging.\n" + "\n" + "Category: **Gameplay Functions**", +}; + +// -------------------------------- getnodes ----------------------------------- + +static auto PyGetNodes(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + HostActivity* host_activity = + ContextRefSceneV1::FromCurrent().GetHostActivity(); + if (!host_activity) { + throw Exception(PyExcType::kContext); + } + Scene* scene = host_activity->scene(); + PyObject* py_list = PyList_New(0); + for (auto&& i : scene->nodes()) { + PyList_Append(py_list, i->BorrowPyRef()); + } + return py_list; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetNodesDef = { + "getnodes", // name + PyGetNodes, // method + METH_VARARGS, // flags + + "getnodes() -> list\n" + "\n" + "Return all nodes in the current bascenev1.Context.\n" + "\n" + "Category: **Gameplay Functions**", +}; + +// -------------------------- get_collision_info ------------------------------- + +static auto DoGetCollideValue(Dynamics* dynamics, const Collision* c, + const char* name) -> PyObject* { + BA_PYTHON_TRY; + if (!strcmp(name, "depth")) { + return Py_BuildValue("f", c->depth); + } else if (!strcmp(name, "position")) { + return Py_BuildValue("(fff)", c->x, c->y, c->z); + } else if (!strcmp(name, "sourcenode")) { + if (!dynamics->in_collide_message()) { + PyErr_SetString( + PyExc_AttributeError, + "collide value 'sourcenode' is only valid while processing " + "collide messages"); + return nullptr; + } + Node* n = dynamics->GetActiveCollideSrcNode(); + if (n) { + return n->NewPyRef(); + } else { + Py_RETURN_NONE; + } + } else if (!strcmp(name, "opposingnode")) { + if (!dynamics->in_collide_message()) { + PyErr_SetString( + PyExc_AttributeError, + "collide value 'opposingnode' is only valid while processing " + "collide messages"); + return nullptr; + } + Node* n = dynamics->GetActiveCollideDstNode(); + if (n) { + return n->NewPyRef(); + } else { + Py_RETURN_NONE; + } + } else if (!strcmp(name, "opposingbody")) { + return Py_BuildValue("i", dynamics->GetCollideMessageReverseOrder() + ? c->body_id_2 + : c->body_id_1); + } else { + PyErr_SetString( + PyExc_AttributeError, + (std::string("\"") + name + "\" is not a valid collide value name") + .c_str()); + return nullptr; + } + BA_PYTHON_CATCH; +} + +static auto PyGetCollisionInfo(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + HostActivity* host_activity = + ContextRefSceneV1::FromCurrent().GetHostActivity(); + if (!host_activity) { + throw Exception(PyExcType::kContext); + } + Dynamics* dynamics = host_activity->scene()->dynamics(); + assert(dynamics); + PyObject* obj = nullptr; + + // Take arg list as individual items or possibly a single tuple + Py_ssize_t argc = PyTuple_GET_SIZE(args); + if (argc > 1) { + obj = args; + } else if (argc == 1) { + obj = PyTuple_GET_ITEM(args, 0); + } + Collision* c = dynamics->active_collision(); + if (!c) { + PyErr_SetString(PyExc_RuntimeError, + "This must be called from a collision callback."); + return nullptr; + } + if (PyUnicode_Check(obj)) { + return DoGetCollideValue(dynamics, c, PyUnicode_AsUTF8(obj)); + } else if (PyTuple_Check(obj)) { + Py_ssize_t size = PyTuple_GET_SIZE(obj); + + // NOTE: Need to make sure we never release the GIL or call out to + // code that could access gc stuff while building this. Ideally should + // create contents first and then create/fill the tuple as last step. + // See https://bugs.python.org/issue15108. + PyObject* return_tuple = PyTuple_New(size); + for (Py_ssize_t i = 0; i < size; i++) { + PyObject* o = PyTuple_GET_ITEM(obj, i); + if (PyUnicode_Check(o)) { + PyObject* val_obj = DoGetCollideValue(dynamics, c, PyUnicode_AsUTF8(o)); + if (val_obj) { + PyTuple_SET_ITEM(return_tuple, i, val_obj); + } else { + Py_DECREF(return_tuple); + return nullptr; + } + } else { + Py_DECREF(return_tuple); + PyErr_SetString(PyExc_TypeError, "Expected a string as tuple member."); + return nullptr; + } + } + return return_tuple; + } else { + PyErr_SetString(PyExc_TypeError, "Expected a string or tuple."); + return nullptr; + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetCollisionInfoDef = { + "get_collision_info", // name + PyGetCollisionInfo, // method + METH_VARARGS, // flags + + "get_collision_info(*args: Any) -> Any\n" + "\n" + "Return collision related values\n" + "\n" + "Category: **Gameplay Functions**\n" + "\n" + "Returns a single collision value or tuple of values such as location,\n" + "depth, nodes involved, etc. Only call this in the handler of a\n" + "collision-triggered callback or message", +}; + +// ------------------------------ camerashake ---------------------------------- + +static auto PyCameraShake(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(g_base->InLogicThread()); + float intensity = 1.0f; + static const char* kwlist[] = {"intensity", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|f", + const_cast(kwlist), &intensity)) { + return nullptr; + } + g_base->graphics->LocalCameraShake(intensity); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyCameraShakeDef = { + "camerashake", // name + (PyCFunction)PyCameraShake, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "camerashake(intensity: float = 1.0) -> None\n" + "\n" + "Shake the camera.\n" + "\n" + "Category: **Gameplay Functions**\n" + "\n" + "Note that some cameras and/or platforms (such as VR) may not display\n" + "camera-shake, so do not rely on this always being visible to the\n" + "player as a gameplay cue.", +}; + +// -------------------------------- emitfx ------------------------------------- + +static auto PyEmitFx(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {"position", "velocity", "count", + "scale", "spread", "chunk_type", + "emit_type", "tendril_type", nullptr}; + PyObject* pos_obj = Py_None; + PyObject* vel_obj = Py_None; + int count = 10; + float scale = 1.0f; + float spread = 1.0f; + const char* chunk_type_str = "rock"; + const char* emit_type_str = "chunks"; + const char* tendril_type_str = "smoke"; + assert(g_base->InLogicThread()); + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "O|Oiffsss", const_cast(kwlist), &pos_obj, + &vel_obj, &count, &scale, &spread, &chunk_type_str, &emit_type_str, + &tendril_type_str)) { + return nullptr; + } + float x, y, z; + assert(pos_obj); + { + std::vector vals = Python::GetPyFloats(pos_obj); + if (vals.size() != 3) { + throw Exception("Expected 3 floats for position.", PyExcType::kValue); + } + x = vals[0]; + y = vals[1]; + z = vals[2]; + } + float vx = 0.0f; + float vy = 0.0f; + float vz = 0.0f; + if (vel_obj != Py_None) { + std::vector vals = Python::GetPyFloats(vel_obj); + if (vals.size() != 3) { + throw Exception("Expected 3 floats for velocity.", PyExcType::kValue); + } + vx = vals[0]; + vy = vals[1]; + vz = vals[2]; + } + base::BGDynamicsChunkType chunk_type; + if (!strcmp(chunk_type_str, "rock")) { + chunk_type = base::BGDynamicsChunkType::kRock; + } else if (!strcmp(chunk_type_str, "ice")) { + chunk_type = base::BGDynamicsChunkType::kIce; + } else if (!strcmp(chunk_type_str, "slime")) { + chunk_type = base::BGDynamicsChunkType::kSlime; + } else if (!strcmp(chunk_type_str, "metal")) { + chunk_type = base::BGDynamicsChunkType::kMetal; + } else if (!strcmp(chunk_type_str, "spark")) { + chunk_type = base::BGDynamicsChunkType::kSpark; + } else if (!strcmp(chunk_type_str, "splinter")) { + chunk_type = base::BGDynamicsChunkType::kSplinter; + } else if (!strcmp(chunk_type_str, "sweat")) { + chunk_type = base::BGDynamicsChunkType::kSweat; + } else { + throw Exception( + "Invalid chunk type: '" + std::string(chunk_type_str) + "'.", + PyExcType::kValue); + } + base::BGDynamicsTendrilType tendril_type; + if (!strcmp(tendril_type_str, "smoke")) { + tendril_type = base::BGDynamicsTendrilType::kSmoke; + } else if (!strcmp(tendril_type_str, "thin_smoke")) { + tendril_type = base::BGDynamicsTendrilType::kThinSmoke; + } else if (!strcmp(tendril_type_str, "ice")) { + tendril_type = base::BGDynamicsTendrilType::kIce; + } else { + throw Exception( + "Invalid tendril type: '" + std::string(tendril_type_str) + "'.", + PyExcType::kValue); + } + base::BGDynamicsEmitType emit_type; + if (!strcmp(emit_type_str, "chunks")) { + emit_type = base::BGDynamicsEmitType::kChunks; + } else if (!strcmp(emit_type_str, "stickers")) { + emit_type = base::BGDynamicsEmitType::kStickers; + } else if (!strcmp(emit_type_str, "tendrils")) { + emit_type = base::BGDynamicsEmitType::kTendrils; + } else if (!strcmp(emit_type_str, "distortion")) { + emit_type = base::BGDynamicsEmitType::kDistortion; + } else if (!strcmp(emit_type_str, "flag_stand")) { + emit_type = base::BGDynamicsEmitType::kFlagStand; + } else if (!strcmp(emit_type_str, "fairydust")) { + emit_type = base::BGDynamicsEmitType::kFairyDust; + } else { + throw Exception("Invalid emit type: '" + std::string(emit_type_str) + "'.", + PyExcType::kValue); + } + if (Scene* scene = ContextRefSceneV1::FromCurrent().GetMutableScene()) { + base::BGDynamicsEmission e; + e.emit_type = emit_type; + e.position = Vector3f(x, y, z); + e.velocity = Vector3f(vx, vy, vz); + e.count = count; + e.scale = scale; + e.spread = spread; + e.chunk_type = chunk_type; + e.tendril_type = tendril_type; + if (SessionStream* output_stream = scene->GetSceneStream()) { + output_stream->EmitBGDynamics(e); + } + if (!g_core->HeadlessMode()) { + g_base->bg_dynamics->Emit(e); + } + } else { + throw Exception("Can't emit bg dynamics in this context_ref.", + PyExcType::kContext); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyEmitFxDef = { + "emitfx", // name + (PyCFunction)PyEmitFx, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "emitfx(position: Sequence[float],\n" + " velocity: Sequence[float] | None = None,\n" + " count: int = 10, scale: float = 1.0, spread: float = 1.0,\n" + " chunk_type: str = 'rock', emit_type: str ='chunks',\n" + " tendril_type: str = 'smoke') -> None\n" + "\n" + "Emit particles, smoke, etc. into the fx sim layer.\n" + "\n" + "Category: **Gameplay Functions**\n" + "\n" + "The fx sim layer is a secondary dynamics simulation that runs in\n" + "the background and just looks pretty; it does not affect gameplay.\n" + "Note that the actual amount emitted may vary depending on graphics\n" + "settings, exiting element counts, or other factors.", +}; + +// ----------------------------- set_map_bounds -------------------------------- + +static auto PySetMapBounds(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + HostActivity* host_activity = + ContextRefSceneV1::FromCurrent().GetHostActivity(); + if (!host_activity) { + throw Exception(PyExcType::kContext); + } + float xmin, ymin, zmin, xmax, ymax, zmax; + assert(g_base->InLogicThread()); + if (!PyArg_ParseTuple(args, "(ffffff)", &xmin, &ymin, &zmin, &xmax, &ymax, + &zmax)) { + return nullptr; + } + host_activity->scene()->SetMapBounds(xmin, ymin, zmin, xmax, ymax, zmax); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetMapBoundsDef = { + "set_map_bounds", // name + (PyCFunction)PySetMapBounds, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_map_bounds(bounds: tuple[float, float, float, float, float, " + "float])\n" + " -> None\n" + "\n" + "(internal)\n" + "\n" + "Set map bounds. Generally nodes that go outside of this box are " + "killed.", +}; + +// -------------------- get_foreground_host_activities ------------------------- + +static auto PyGetForegroundHostActivity(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + + // Note: we return None if not in the logic thread. + HostActivity* h = + g_base->InLogicThread() + ? ContextRefSceneV1::FromAppForegroundContext().GetHostActivity() + : nullptr; + if (h != nullptr) { + PyObject* obj = h->GetPyActivity(); + Py_INCREF(obj); + return obj; + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetForegroundHostActivityDef = { + "get_foreground_host_activity", // name + (PyCFunction)PyGetForegroundHostActivity, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_foreground_host_activity() -> bascenev1.Activity | None\n" + "\n" + "(internal)\n" + "\n" + "Returns the bascenev1.Activity currently in the foreground,\n" + "or None if there is none.\n"}; + +// --------------------------- get_game_roster --------------------------------- + +static auto PyGetGameRoster(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + PythonRef py_client_list(PyList_New(0), PythonRef::kSteal); + + cJSON* party = SceneV1AppMode::GetSingleton()->game_roster(); + assert(party); + int len = cJSON_GetArraySize(party); + for (int i = 0; i < len; i++) { + cJSON* client = cJSON_GetArrayItem(party, i); + assert(client); + cJSON* spec = cJSON_GetObjectItem(client, "spec"); + cJSON* players = cJSON_GetObjectItem(client, "p"); + PythonRef py_player_list(PyList_New(0), PythonRef::kSteal); + if (players != nullptr) { + int plen = cJSON_GetArraySize(players); + for (int j = 0; j < plen; ++j) { + cJSON* player = cJSON_GetArrayItem(players, j); + if (player != nullptr) { + cJSON* name = cJSON_GetObjectItem(player, "n"); + cJSON* py_name_full = cJSON_GetObjectItem(player, "nf"); + cJSON* id_obj = cJSON_GetObjectItem(player, "i"); + int id_val = id_obj ? id_obj->valueint : -1; + if (name != nullptr && name->valuestring != nullptr + && py_name_full != nullptr && py_name_full->valuestring != nullptr + && id_val != -1) { + PythonRef py_player( + Py_BuildValue( + "{sssssi}", "name", + Utils::GetValidUTF8(name->valuestring, "ggr1").c_str(), + "name_full", + Utils::GetValidUTF8(py_name_full->valuestring, "ggr2") + .c_str(), + "id", id_val), + PythonRef::kSteal); + // This increments ref. + PyList_Append(py_player_list.Get(), py_player.Get()); + } + } + } + } + + // If there's a client_id with this data, include it; otherwise pass None. + cJSON* client_id = cJSON_GetObjectItem(client, "i"); + int clientid{}; + PythonRef client_id_ref; + if (client_id != nullptr) { + clientid = client_id->valueint; + client_id_ref.Steal(PyLong_FromLong(clientid)); + } else { + client_id_ref.Acquire(Py_None); + } + + // Let's also include a public account-id if we have one. + std::string account_id; + if (clientid == -1) { + account_id = g_base->Plus()->GetPublicV1AccountID(); + } else { + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + auto client2 = + appmode->connections()->connections_to_clients().find(clientid); + if (client2 != appmode->connections()->connections_to_clients().end()) { + account_id = client2->second->peer_public_account_id(); + } + } + } + PythonRef account_id_ref; + if (account_id.empty()) { + account_id_ref.Acquire(Py_None); + } else { + account_id_ref.Steal(PyUnicode_FromString(account_id.c_str())); + } + + // Py_BuildValue steals a ref; gotta increment ourself (edit: NO IT DOESNT) + // Py_INCREF(py_player_list.get()); + PythonRef py_client( + Py_BuildValue( + "{sssssOsOsO}", "display_string", + (spec && spec->valuestring) + ? PlayerSpec(spec->valuestring).GetDisplayString().c_str() + : "", + "spec_string", (spec && spec->valuestring) ? spec->valuestring : "", + "players", py_player_list.Get(), "client_id", client_id_ref.Get(), + "account_id", account_id_ref.Get()), + PythonRef::kSteal); + PyList_Append(py_client_list.Get(), + py_client.Get()); // this increments ref + } + return py_client_list.NewRef(); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetGameRosterDef = { + "get_game_roster", // name + (PyCFunction)PyGetGameRoster, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_game_roster() -> list[dict[str, Any]]\n" + "\n" + "(internal)", +}; + +// ----------------------- set_debug_speed_exponent ---------------------------- + +static auto PySetDebugSpeedExponent(PyObject* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + int speed; + if (!PyArg_ParseTuple(args, "i", &speed)) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + + HostActivity* host_activity = + ContextRefSceneV1::FromCurrent().GetHostActivity(); + if (!host_activity) { + throw Exception(PyExcType::kContext); + } + if (g_buildconfig.debug_build()) { + appmode->SetDebugSpeedExponent(speed); + } else { + throw Exception("This call only functions in the debug build."); + } + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetDebugSpeedExponentDef = { + "set_debug_speed_exponent", // name + PySetDebugSpeedExponent, // method + METH_VARARGS, // flags + + "set_debug_speed_exponent(speed: int) -> None\n" + "\n" + "(internal)\n" + "\n" + "Sets the debug speed scale for the game. Actual speed is " + "pow(2,speed).", +}; + +// ----------------------- get_replay_speed_exponent --------------------------- + +static auto PyGetReplaySpeedExponent(PyObject* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + return PyLong_FromLong(appmode->replay_speed_exponent()); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetReplaySpeedExponentDef = { + "get_replay_speed_exponent", // name + PyGetReplaySpeedExponent, // method + METH_VARARGS, // flags + + "get_replay_speed_exponent() -> int\n" + "\n" + "(internal)\n" + "\n" + "Returns current replay speed value. Actual displayed speed is " + "pow(2,speed).", +}; + +// ------------------------ set_replay_speed_exponent -------------------------- + +static auto PySetReplaySpeedExponent(PyObject* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + int speed; + if (!PyArg_ParseTuple(args, "i", &speed)) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + appmode->SetReplaySpeedExponent(speed); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetReplaySpeedExponentDef = { + "set_replay_speed_exponent", // name + PySetReplaySpeedExponent, // method + METH_VARARGS, // flags + + "set_replay_speed_exponent(speed: int) -> None\n" + "\n" + "(internal)\n" + "\n" + "Set replay speed. Actual displayed speed is pow(2, speed).", +}; + +// ----------------------- reset_random_player_names --------------------------- + +static auto PyResetRandomPlayerNames(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + SceneV1InputDeviceDelegate::ResetRandomNames(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyResetRandomPlayerNamesDef = { + "reset_random_player_names", // name + (PyCFunction)PyResetRandomPlayerNames, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "reset_random_player_names() -> None\n" + "\n" + "(internal)", +}; + +// --------------------------- get_random_names -------------------------------- + +static auto PyGetRandomNames(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + PyObject* list = PyList_New(0); + const std::list& random_name_list = Utils::GetRandomNameList(); + for (const auto& i : random_name_list) { + assert(Utils::IsValidUTF8(i)); + PyObject* obj = PyUnicode_FromString(i.c_str()); + PyList_Append(list, obj); + Py_DECREF(obj); + } + return list; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetRandomNamesDef = { + "get_random_names", // name + PyGetRandomNames, // method + METH_VARARGS, // flags + + "get_random_names() -> list\n" + "\n" + "(internal)\n" + "\n" + "Returns the random names used by the game.", +}; + +// -------------------------------- ls_objects --------------------------------- + +static auto PyLsObjects(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Object::LsObjects(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyLsObjectsDef = { + "ls_objects", // name + (PyCFunction)PyLsObjects, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "ls_objects() -> None\n" + "\n" + "Log debugging info about C++ level objects.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "This call only functions in debug builds of the game.\n" + "It prints various info about the current object count, etc.", +}; + +// --------------------------- ls_input_devices -------------------------------- + +static auto PyLsInputDevices(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + g_base->input->LsInputDevices(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyLsInputDevicesDef = { + "ls_input_devices", // name + (PyCFunction)PyLsInputDevices, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "ls_input_devices() -> None\n" + "\n" + "Print debugging info about game objects.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "This call only functions in debug builds of the game.\n" + "It prints various info about the current object count, etc.", +}; + +// -------------------------- set_internal_music ------------------------------- + +static auto PySetInternalMusic(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + PyObject* music_obj; + float volume{1.0}; + int loop{1}; + static const char* kwlist[] = {"music", "volume", "loop", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|fp", + const_cast(kwlist), &music_obj, + &volume, &loop)) { + return nullptr; + } + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + + if (music_obj == Py_None) { + appmode->SetInternalMusic(nullptr); + } else { + auto& sound = base::PythonClassSimpleSound::FromPyObj(music_obj).sound(); + appmode->SetInternalMusic(&sound, volume, loop); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetInternalMusicDef = { + "set_internal_music", // name + (PyCFunction)PySetInternalMusic, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_internal_music(music: babase.SimpleSound | None,\n" + " volume: float = 1.0, loop: bool = True) -> None\n" + "\n" + "(internal).", +}; + +// ----------------------------------------------------------------------------- + +auto PythonMethodsScene::GetMethods() -> std::vector { + return { + PyNewReplaySessionDef, + PyNewHostSessionDef, + PyGetSessionDef, + PyGetActivityDef, + PyNewActivityDef, + PyGetForegroundHostSessionDef, + PyRegisterActivityDef, + PyRegisterSessionDef, + PyIsInReplayDef, + PyScreenMessageDef, + PyGetRandomNamesDef, + PyResetRandomPlayerNamesDef, + PySetReplaySpeedExponentDef, + PyGetReplaySpeedExponentDef, + PySetDebugSpeedExponentDef, + PyGetGameRosterDef, + PyGetForegroundHostActivityDef, + PySetMapBoundsDef, + PyEmitFxDef, + PyCameraShakeDef, + PyGetCollisionInfoDef, + PyGetNodesDef, + PySetInternalMusicDef, + PyPrintNodesDef, + PyNewNodeDef, + PyLsObjectsDef, + PyTimeDef, + PyTimerDef, + PyBaseTimeDef, + PyBaseTimerDef, + PyLsInputDevicesDef, + }; +} + +#pragma clang diagnostic pop + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/python/methods/python_methods_scene.h b/src/ballistica/scene_v1/python/methods/python_methods_scene.h new file mode 100644 index 00000000..05763e49 --- /dev/null +++ b/src/ballistica/scene_v1/python/methods/python_methods_scene.h @@ -0,0 +1,20 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_PYTHON_METHODS_PYTHON_METHODS_SCENE_H_ +#define BALLISTICA_SCENE_V1_PYTHON_METHODS_PYTHON_METHODS_SCENE_H_ + +#include + +#include "ballistica/shared/ballistica.h" + +namespace ballistica::scene_v1 { + +/// Gameplay related individual python methods for our module. +class PythonMethodsScene { + public: + static auto GetMethods() -> std::vector; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_PYTHON_METHODS_PYTHON_METHODS_SCENE_H_ diff --git a/src/ballistica/scene_v1/python/scene_v1_python.cc b/src/ballistica/scene_v1/python/scene_v1_python.cc new file mode 100644 index 00000000..9f185252 --- /dev/null +++ b/src/ballistica/scene_v1/python/scene_v1_python.cc @@ -0,0 +1,1490 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/python/scene_v1_python.h" + +#include "ballistica/base/input/device/keyboard_input.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/python/class/python_class_context_ref.h" +#include "ballistica/scene_v1/assets/scene_collision_mesh.h" +#include "ballistica/scene_v1/assets/scene_mesh.h" +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/assets/scene_texture.h" +#include "ballistica/scene_v1/dynamics/material/material.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/python/class/python_class_activity_data.h" +#include "ballistica/scene_v1/python/class/python_class_base_timer.h" +#include "ballistica/scene_v1/python/class/python_class_input_device.h" +#include "ballistica/scene_v1/python/class/python_class_material.h" +#include "ballistica/scene_v1/python/class/python_class_node.h" +#include "ballistica/scene_v1/python/class/python_class_scene_collision_mesh.h" +#include "ballistica/scene_v1/python/class/python_class_scene_data_asset.h" +#include "ballistica/scene_v1/python/class/python_class_scene_mesh.h" +#include "ballistica/scene_v1/python/class/python_class_scene_sound.h" +#include "ballistica/scene_v1/python/class/python_class_scene_texture.h" +#include "ballistica/scene_v1/python/class/python_class_scene_timer.h" +#include "ballistica/scene_v1/python/class/python_class_session_data.h" +#include "ballistica/scene_v1/python/class/python_class_session_player.h" +#include "ballistica/scene_v1/python/methods/python_methods_assets.h" +#include "ballistica/scene_v1/python/methods/python_methods_input.h" +#include "ballistica/scene_v1/python/methods/python_methods_networking.h" +#include "ballistica/scene_v1/python/methods/python_methods_scene.h" +#include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" +#include "ballistica/scene_v1/support/session_stream.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python_command.h" +#include "ballistica/shared/python/python_module_builder.h" + +namespace ballistica::scene_v1 { + +SceneV1Python::SceneV1Python() = default; + +// Need to declare a plain c PyInit_XXX function with our module name in it so +// we're discoverable when compiled as a standalone binary Python module. +extern "C" auto PyInit__bascenev1() -> PyObject* { + auto* builder = + new PythonModuleBuilder("_bascenev1", + { + PythonMethodsInput::GetMethods(), + PythonMethodsAssets::GetMethods(), + PythonMethodsNetworking::GetMethods(), + PythonMethodsScene::GetMethods(), + }, + [](PyObject* module) -> int { + BA_PYTHON_TRY; + SceneV1FeatureSet::OnModuleExec(module); + return 0; + BA_PYTHON_INT_CATCH; + }); + return builder->Build(); +} + +void SceneV1Python::AddPythonClasses(PyObject* module) { + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); +} + +void SceneV1Python::ImportPythonObjs() { +#include "ballistica/scene_v1/mgen/pyembed/binding_scene_v1.inc" +} + +void SceneV1Python::Reset() { + assert(g_base->InLogicThread()); + ReleaseJoystickInputCapture(); + ReleaseKeyboardInputCapture(); +} + +void SceneV1Python::SetNodeAttr(Node* node, const char* attr_name, + PyObject* value_obj) { + assert(node); + SessionStream* out_stream = node->scene()->GetSceneStream(); + NodeAttribute attr = node->GetAttribute(attr_name); + switch (attr.type()) { + case NodeAttributeType::kFloat: { + float val = Python::GetPyFloat(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kInt: { + int64_t val = Python::GetPyInt64(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kBool: { + bool val = Python::GetPyBool(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kFloatArray: { + std::vector vals = Python::GetPyFloats(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + case NodeAttributeType::kIntArray: { + std::vector vals = Python::GetPyInts64(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + case NodeAttributeType::kString: { + std::string val = g_base->python->GetPyLString(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kNode: { + // Allow dead-refs or None. + Node* val = GetPyNode(value_obj, true, true); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kNodeArray: { + std::vector vals = GetPyNodes(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + case NodeAttributeType::kPlayer: { + // Allow dead-refs and None. + Player* val = GetPyPlayer(value_obj, true, true); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kMaterialArray: { + std::vector vals = GetPyMaterials(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + case NodeAttributeType::kTexture: { + // Don't allow dead-refs, do allow None. + SceneTexture* val = GetPySceneTexture(value_obj, false, true); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kTextureArray: { + std::vector vals = GetPySceneTextures(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + case NodeAttributeType::kSound: { + // Don't allow dead-refs, do allow None. + SceneSound* val = GetPySceneSound(value_obj, false, true); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kSoundArray: { + std::vector vals = GetPySceneSounds(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + case NodeAttributeType::kMesh: { + // Don't allow dead-refs, do allow None. + SceneMesh* val = GetPySceneMesh(value_obj, false, true); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kMeshArray: { + std::vector vals = GetPySceneMeshes(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + case NodeAttributeType::kCollisionMesh: { + // Don't allow dead-refs, do allow None. + SceneCollisionMesh* val = GetPySceneCollisionMesh(value_obj, false, true); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kCollisionMeshArray: { + std::vector vals = + GetPySceneCollisionMeshes(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + default: + throw Exception("FIXME: unhandled attr type in SetNodeAttr: '" + + attr.GetTypeName() + "'."); + } +} + +static auto CompareAttrIndices( + const std::pair& first, + const std::pair& second) -> bool { + return (first.first->index() < second.first->index()); +} + +auto SceneV1Python::DoNewNode(PyObject* args, PyObject* keywds) -> Node* { + BA_PRECONDITION(g_base->InLogicThread()); + PyObject* delegate_obj = Py_None; + PyObject* owner_obj = Py_None; + PyObject* name_obj = Py_None; + static const char* kwlist[] = {"type", "owner", "attrs", + "name", "delegate", nullptr}; + char* type; + PyObject* dict = nullptr; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "s|OOOO", const_cast(kwlist), &type, &owner_obj, + &dict, &name_obj, &delegate_obj)) { + return nullptr; + } + + std::string name; + if (name_obj != Py_None) { + name = Python::GetPyString(name_obj); + } else { + // By default do something like 'text@foo.py:20'. + name = std::string(type) + "@" + Python::GetPythonFileLocation(); + } + + Scene* scene = ContextRefSceneV1::FromCurrent().GetMutableScene(); + if (!scene) { + throw Exception("Can't create nodes in this context_ref.", + PyExcType::kContext); + } + + Node* node = scene->NewNode(type, name, delegate_obj); + + // Handle attr values fed in. + if (dict) { + if (!PyDict_Check(dict)) { + throw Exception("Expected dict for arg 2.", PyExcType::kType); + } + NodeType* t = node->type(); + PyObject* key{}; + PyObject* value{}; + Py_ssize_t pos{}; + + // We want to set initial attrs in order based on their attr indices. + std::list> attr_vals; + + // Grab all initial attr/values and add them to a list. + while (PyDict_Next(dict, &pos, &key, &value)) { + if (!PyUnicode_Check(key)) { + throw Exception("Expected string key in attr dict.", PyExcType::kType); + } + try { + attr_vals.emplace_back( + t->GetAttribute(std::string(PyUnicode_AsUTF8(key))), value); + } catch (const std::exception&) { + Log(LogLevel::kError, "Attr not found on initial attr set: '" + + std::string(PyUnicode_AsUTF8(key)) + "' on " + + type + " node '" + name + "'"); + } + } + + // Run the sets in the order of attr indices. + attr_vals.sort(CompareAttrIndices); + for (auto&& i : attr_vals) { + try { + SetNodeAttr(node, i.first->name().c_str(), i.second); + } catch (const std::exception& e) { + Log(LogLevel::kError, "Exception in initial attr set for attr '" + + i.first->name() + "' on " + type + " node '" + + name + "':" + e.what()); + } + } + } + + // If an owner was provided, set it up. + if (owner_obj != Py_None) { + // If its a node, set up a dependency at the scene level + // (then we just have to delete the owner node and the scene does the + // rest). + if (PythonClassNode::Check(owner_obj)) { + Node* owner_node = GetPyNode(owner_obj, true); + if (owner_node == nullptr) { + Log(LogLevel::kError, + "Empty node-ref passed for 'owner'; pass None if you want " + "no owner."); + } else if (owner_node->scene() != node->scene()) { + Log(LogLevel::kError, + "Owner node is from a different scene; ignoring."); + } else { + owner_node->AddDependentNode(node); + } + } else { + throw Exception( + "Invalid node owner: " + Python::ObjToString(owner_obj) + ".", + PyExcType::kType); + } + } + + // Lastly, call this node's OnCreate method for any final setup it may want to + // do. + try { + // Tell clients to do the same. + if (SessionStream* output_stream = scene->GetSceneStream()) { + output_stream->NodeOnCreate(node); + } + node->OnCreate(); + } catch (const std::exception& e) { + Log(LogLevel::kError, "Exception in OnCreate() for node " + + ballistica::ObjToString(node) + + "':" + e.what()); + } + + return node; +} + +// Return the node attr as a PyObject, or nullptr if the node doesn't have that +// attr. +auto SceneV1Python::GetNodeAttr(Node* node, const char* attr_name) + -> PyObject* { + assert(node); + NodeAttribute attr = node->GetAttribute(attr_name); + switch (attr.type()) { + case NodeAttributeType::kFloat: + return PyFloat_FromDouble(attr.GetAsFloat()); + break; + case NodeAttributeType::kInt: + return PyLong_FromLong( + static_cast_check_fit(attr.GetAsInt())); // NOLINT + break; + case NodeAttributeType::kBool: + if (attr.GetAsBool()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + break; + case NodeAttributeType::kString: { + if (g_buildconfig.debug_build()) { + std::string s = attr.GetAsString(); + assert(Utils::IsValidUTF8(s)); + return PyUnicode_FromString(s.c_str()); + } else { + return PyUnicode_FromString(attr.GetAsString().c_str()); + } + break; + } + case NodeAttributeType::kNode: { + // Return a new py ref to this node or create a new empty ref. + Node* n = attr.GetAsNode(); + return n ? n->NewPyRef() : PythonClassNode::Create(nullptr); + break; + } + case NodeAttributeType::kPlayer: { + // Player attrs deal with custom user bascenev1.Player classes; + // not our internal SessionPlayer class. + Player* p = attr.GetAsPlayer(); + if (p == nullptr) { + Py_RETURN_NONE; + } + PyObject* gameplayer = p->GetPyActivityPlayer(); + Py_INCREF(gameplayer); + return gameplayer; + // return p ? p->NewPyRef() : PythonClassSessionPlayer::Create(nullptr); + break; + } + case NodeAttributeType::kFloatArray: { + std::vector vals = attr.GetAsFloats(); + auto size{static_cast(vals.size())}; + PyObject* vals_obj = PyTuple_New(size); + assert(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + PyTuple_SET_ITEM(vals_obj, i, PyFloat_FromDouble(vals[i])); + } + return vals_obj; + break; + } + case NodeAttributeType::kIntArray: { + std::vector vals = attr.GetAsInts(); + auto size{static_cast(vals.size())}; + PyObject* vals_obj = PyTuple_New(size); + assert(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + PyTuple_SET_ITEM(vals_obj, i, + PyLong_FromLong(static_cast_check_fit( // NOLINT + vals[i]))); + } + return vals_obj; + break; + } + case NodeAttributeType::kNodeArray: { + std::vector vals = attr.GetAsNodes(); + auto size{static_cast(vals.size())}; + PyObject* vals_obj = PyTuple_New(size); + assert(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + Node* n = vals[i]; + PyTuple_SET_ITEM(vals_obj, i, + n ? n->NewPyRef() : PythonClassNode::Create(nullptr)); + } + return vals_obj; + break; + } + case NodeAttributeType::kTexture: { + SceneTexture* t = attr.GetAsTexture(); + if (!t) { + Py_RETURN_NONE; + } + return t->NewPyRef(); + break; + } + case NodeAttributeType::kSound: { + SceneSound* s = attr.GetAsSound(); + if (!s) { + Py_RETURN_NONE; + } + return s->NewPyRef(); + break; + } + case NodeAttributeType::kMesh: { + SceneMesh* m = attr.GetAsMesh(); + if (!m) { + Py_RETURN_NONE; + } + return m->NewPyRef(); + break; + } + case NodeAttributeType::kCollisionMesh: { + SceneCollisionMesh* c = attr.GetAsCollisionMesh(); + if (!c) { + Py_RETURN_NONE; + } + return c->NewPyRef(); + break; + } + case NodeAttributeType::kMaterialArray: { + std::vector vals = attr.GetAsMaterials(); + auto size{static_cast(vals.size())}; + PyObject* vals_obj = PyTuple_New(size); + assert(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + Material* m = vals[i]; + + // Array attrs should never return nullptr materials. + assert(m); + PyTuple_SET_ITEM(vals_obj, i, m->NewPyRef()); + } + return vals_obj; + break; + } + case NodeAttributeType::kTextureArray: { + std::vector vals = attr.GetAsTextures(); + auto size{static_cast(vals.size())}; + PyObject* vals_obj = PyTuple_New(size); + assert(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + SceneTexture* t = vals[i]; + + // Array attrs should never return nullptr textures. + assert(t); + PyTuple_SET_ITEM(vals_obj, i, t->NewPyRef()); + } + return vals_obj; + break; + } + case NodeAttributeType::kSoundArray: { + std::vector vals = attr.GetAsSounds(); + auto size{static_cast(vals.size())}; + PyObject* vals_obj = PyTuple_New(size); + assert(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + SceneSound* s = vals[i]; + + // Array attrs should never return nullptr sounds. + assert(s); + PyTuple_SET_ITEM(vals_obj, i, s->NewPyRef()); + } + return vals_obj; + break; + } + case NodeAttributeType::kMeshArray: { + std::vector vals = attr.GetAsMeshes(); + auto size{static_cast(vals.size())}; + PyObject* vals_obj = PyTuple_New(size); + assert(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + SceneMesh* m = vals[i]; + + // Array attrs should never return nullptr meshes. + assert(m); + PyTuple_SET_ITEM(vals_obj, i, m->NewPyRef()); + } + return vals_obj; + break; + } + case NodeAttributeType::kCollisionMeshArray: { + std::vector vals = attr.GetAsCollisionMeshes(); + auto size{static_cast(vals.size())}; + PyObject* vals_obj = PyTuple_New(size); + assert(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + SceneCollisionMesh* c = vals[i]; + + // Array attrs should never return nullptr collision-meshes. + assert(c); + PyTuple_SET_ITEM(vals_obj, i, c->NewPyRef()); + } + return vals_obj; + break; + } + + default: + throw Exception("FIXME: unhandled attr type in GetNodeAttr: '" + + attr.GetTypeName() + "'."); + } + return nullptr; +} + +auto SceneV1Python::IsPyHostActivity(PyObject* o) -> bool { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + int result = + PyObject_IsInstance(o, g_scene_v1->python->objs() + .Get(SceneV1Python::ObjID::kActivityClass) + .Get()); + if (result == -1) { + result = 0; + PyErr_Clear(); + } + return static_cast(result); +} + +auto SceneV1Python::GetPyHostActivity(PyObject* o) -> HostActivity* { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + PyExcType pyexctype{PyExcType::kType}; + + // Make sure it's a subclass of bascenev1.Activity. + if (IsPyHostActivity(o)) { + // Look for an _activity_data attr on it. + if (PyObject* activity_data = PyObject_GetAttrString(o, "_activity_data")) { + // This will deallocate for us. + PythonRef ref(activity_data, PythonRef::kSteal); + if (PythonClassActivityData::Check(activity_data)) { + return (reinterpret_cast(activity_data)) + ->GetHostActivity(); + } + } else { + pyexctype = PyExcType::kRuntime; // activity Obj is wonky. + } + } + + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + throw Exception( + "Can't get activity from value: " + Python::ObjToString(o) + ".", + pyexctype); +} + +auto SceneV1Python::GetPyNode(PyObject* o, bool allow_empty_ref, + bool allow_none) -> Node* { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassNode::Check(o)) { + // This will succeed or throw its own Exception. + return (reinterpret_cast(o))->GetNode(!allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception("Can't get node from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto SceneV1Python::GetPyNodes(PyObject* o) -> std::vector { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.Exists()); + auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.Get())); + PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.Get()); + std::vector vals(size); + assert(vals.size() == size); + for (size_t i = 0; i < size; i++) { + vals[i] = GetPyNode(pyobjs[i]); + } + return vals; +} + +auto SceneV1Python::GetPyMaterial(PyObject* o, bool allow_empty_ref, + bool allow_none) -> Material* { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassMaterial::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetMaterial( + !allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get material from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto SceneV1Python::GetPyMaterials(PyObject* o) -> std::vector { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.Exists()); + auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.Get())); + PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.Get()); + std::vector vals(size); + assert(vals.size() == size); + for (size_t i = 0; i < size; i++) { + vals[i] = GetPyMaterial(pyobjs[i]); // DON'T allow nullptr refs. + } + return vals; +} + +auto SceneV1Python::GetPySceneTexture(PyObject* o, bool allow_empty_ref, + bool allow_none) -> SceneTexture* { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassSceneTexture::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetTexture( + !allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get bascenev1.Texture from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto SceneV1Python::GetPySceneTextures(PyObject* o) + -> std::vector { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.Exists()); + auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.Get())); + PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.Get()); + std::vector vals(size); + assert(vals.size() == size); + for (size_t i = 0; i < size; i++) { + vals[i] = + GetPySceneTexture(pyobjs[i]); // DON'T allow nullptr refs or None. + } + return vals; +} + +auto SceneV1Python::GetPySceneMesh(PyObject* o, bool allow_empty_ref, + bool allow_none) -> SceneMesh* { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassSceneMesh::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetMesh( + !allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get bascenev1.Mesh from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto SceneV1Python::GetPySceneMeshes(PyObject* o) -> std::vector { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.Exists()); + auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.Get())); + PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.Get()); + std::vector vals(size); + assert(vals.size() == size); + for (size_t i = 0; i < size; i++) { + vals[i] = GetPySceneMesh(pyobjs[i], false); // DON'T allow nullptr refs. + } + return vals; +} + +auto SceneV1Python::IsPyPlayer(PyObject* o) -> bool { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + int result = PyObject_IsInstance( + o, + g_scene_v1->python->objs().Get(SceneV1Python::ObjID::kPlayerClass).Get()); + if (result == -1) { + result = 0; + PyErr_Clear(); + } + return static_cast(result); +} + +auto SceneV1Python::GetPyPlayer(PyObject* o, bool allow_empty_ref, + bool allow_none) -> Player* { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + PyExcType pyexctype{PyExcType::kType}; + + if (allow_none && (o == Py_None)) { + return nullptr; + } + + // Make sure it's a subclass of bascenev1.Player. + if (IsPyPlayer(o)) { + // Look for an sessionplayer attr on it. + if (PyObject* sessionplayer = PyObject_GetAttrString(o, "sessionplayer")) { + // This will deallocate for us. + PythonRef ref(sessionplayer, PythonRef::kSteal); + + if (PythonClassSessionPlayer::Check(sessionplayer)) { + // This will succeed or throw an exception itself. + return (reinterpret_cast(sessionplayer)) + ->GetPlayer(!allow_empty_ref); + } + } else { + pyexctype = PyExcType::kRuntime; // We've got a wonky object. + } + } + + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + throw Exception( + "Can't get player from value: " + Python::ObjToString(o) + ".", + pyexctype); +} + +auto SceneV1Python::ValidatedPackageAssetName(PyObject* package, + const char* name) -> std::string { + assert(g_base->InLogicThread()); + assert(g_scene_v1->python->objs().Exists( + SceneV1Python::ObjID::kAssetPackageClass)); + + if (!PyObject_IsInstance(package, + g_scene_v1->python->objs() + .Get(SceneV1Python::ObjID::kAssetPackageClass) + .Get())) { + throw Exception("Object is not an AssetPackage.", PyExcType::kType); + } + + // Ok; they've passed us an asset-package object. + // Now validate that its context is current... + PythonRef context_obj(PyObject_GetAttrString(package, "context_ref"), + PythonRef::kSteal); + if (!context_obj.Exists() + || !(PyObject_IsInstance(context_obj.Get(), + reinterpret_cast( + &base::PythonClassContextRef::type_obj)))) { + throw Exception("Asset package context_ref not found.", + PyExcType::kNotFound); + } + auto* pycontext = + reinterpret_cast(context_obj.Get()); + auto* ctargetref = pycontext->context_ref().Get(); + if (!ctargetref) { + throw Exception("Asset package context_ref does not exist.", + PyExcType::kNotFound); + } + auto* ctargetref2 = g_base->CurrentContext().Get(); + if (ctargetref != ctargetref2) { + throw Exception("Asset package context_ref is not current."); + } + + // Hooray; the asset package's context exists and is current. + // Ok; now pull the package id... + PythonRef package_id(PyObject_GetAttrString(package, "package_id"), + PythonRef::kSteal); + if (!PyUnicode_Check(package_id.Get())) { + throw Exception("Got non-string AssetPackage ID.", PyExcType::kType); + } + + // TODO(ericf): make sure the package is valid for this context, + // and return a fully qualified name with the package included. + + printf("would give %s:%s\n", PyUnicode_AsUTF8(package_id.Get()), name); + return name; +} + +auto SceneV1Python::GetPySceneSound(PyObject* o, bool allow_empty_ref, + bool allow_none) -> SceneSound* { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassSceneSound::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetSound( + !allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get bascenev1.Sound from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto SceneV1Python::GetPySceneSounds(PyObject* o) -> std::vector { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.Exists()); + auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.Get())); + PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.Get()); + std::vector vals(size); + assert(vals.size() == size); + for (size_t i = 0; i < size; i++) { + vals[i] = GetPySceneSound(pyobjs[i]); // DON'T allow nullptr refs + } + return vals; +} + +auto SceneV1Python::GetPySceneCollisionMesh(PyObject* o, bool allow_empty_ref, + bool allow_none) + -> SceneCollisionMesh* { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassSceneCollisionMesh::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o) + ->GetCollisionMesh(!allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception("Can't get bascenev1.CollisionMesh from value: " + + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto SceneV1Python::GetPySceneCollisionMeshes(PyObject* o) + -> std::vector { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.Exists()); + auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.Get())); + PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.Get()); + std::vector vals(size); + assert(vals.size() == size); + for (size_t i = 0; i < size; i++) { + vals[i] = GetPySceneCollisionMesh(pyobjs[i]); // DON'T allow nullptr refs. + } + return vals; +} + +auto SceneV1Python::IsPySession(PyObject* o) -> bool { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + int result = PyObject_IsInstance( + o, g_scene_v1->python->objs() + .Get(SceneV1Python::ObjID::kSceneV1SessionClass) + .Get()); + if (result == -1) { + PyErr_Clear(); + result = 0; + } + return static_cast(result); +} + +auto SceneV1Python::GetPySession(PyObject* o) -> Session* { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + PyExcType pyexctype{PyExcType::kType}; + if (IsPySession(o)) { + // Look for an _sessiondata attr on it. + if (PyObject* sessiondata = PyObject_GetAttrString(o, "_sessiondata")) { + // This will deallocate for us. + PythonRef ref(sessiondata, PythonRef::kSteal); + if (PythonClassSessionData::Check(sessiondata)) { + // This will succeed or throw its own Exception. + return (reinterpret_cast(sessiondata)) + ->GetSession(); + } + } else { + pyexctype = PyExcType::kRuntime; // Wonky session obj. + } + } + + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + throw Exception( + "Can't get Session from value: " + Python::ObjToString(o) + ".", + pyexctype); +} + +auto SceneV1Python::GetPySessionPlayer(PyObject* o, bool allow_empty_ref, + bool allow_none) -> Player* { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassSessionPlayer::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetPlayer( + !allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception("Can't get bascenev1.SessionPlayer from value: " + + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto SceneV1Python::GetPySceneDataAsset(PyObject* o, bool allow_empty_ref, + bool allow_none) -> SceneDataAsset* { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassSceneDataAsset::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetData( + !allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get bascenev1.Data from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto SceneV1Python::FilterChatMessage(std::string* message, int client_id) + -> bool { + assert(message); + base::ScopedSetContext ssc(nullptr); + PythonRef args(Py_BuildValue("(si)", message->c_str(), client_id), + PythonRef::kSteal); + PythonRef result = objs().Get(ObjID::kFilterChatMessageCall).Call(args); + + // If something went wrong, just allow all messages through verbatim. + if (!result.Exists()) { + return true; + } + + // If they returned None, they want to ignore the message. + if (result.Get() == Py_None) { + return false; + } + + // Replace the message string with whatever they gave us. + try { + *message = g_base->python->GetPyLString(result.Get()); + } catch (const std::exception& e) { + Log(LogLevel::kError, + "Error getting string from chat filter: " + std::string(e.what())); + } + return true; +} + +void SceneV1Python::HandleLocalChatMessage(const std::string& message) { + base::ScopedSetContext ssc(nullptr); + PythonRef args(Py_BuildValue("(s)", message.c_str()), PythonRef::kSteal); + objs().Get(ObjID::kHandleLocalChatMessageCall).Call(args); +} + +// Put together a node message with all args on the provided tuple (starting +// with arg_offset) returns false on failure, true on success. +void SceneV1Python::DoBuildNodeMessage(PyObject* args, int arg_offset, + std::vector* b, + PyObject** user_message_obj) { + Py_ssize_t tuple_size = PyTuple_GET_SIZE(args); + if (tuple_size - arg_offset < 1) { + throw Exception("Got message of size zero.", PyExcType::kValue); + } + std::string type; + PyObject* obj; + + // Pull first arg. + obj = PyTuple_GET_ITEM(args, arg_offset); + BA_PRECONDITION(obj); + if (!PyUnicode_Check(obj)) { + // If first arg is not a string, its an actual message itself. + (*user_message_obj) = obj; + return; + } else { + (*user_message_obj) = nullptr; + } + type = Python::GetPyString(obj); + NodeMessageType ac = Scene::GetNodeMessageType(type); + const char* format = Scene::GetNodeMessageFormat(ac); + assert(format); + const char* f = format; + + // Allow space for 1 type byte (fixme - may need more than 1). + size_t full_size = 1; + for (Py_ssize_t i = arg_offset + 1; i < tuple_size; i++) { + // Make sure our format string ends the same time as our arg count. + if (*f == 0) { + throw Exception( + "Wrong number of arguments on node message '" + type + "'.", + PyExcType::kValue); + } + obj = PyTuple_GET_ITEM(args, i); + BA_PRECONDITION(obj); + switch (*f) { + case 'I': + + // 4 byte int + if (!PyNumber_Check(obj)) { + throw Exception("Expected an int for node message arg " + + std::to_string(i - (arg_offset + 1)) + ".", + PyExcType::kType); + } + full_size += 4; + break; + case 'i': + + // 2 byte int. + if (!PyNumber_Check(obj)) { + throw Exception("Expected an int for node message arg " + + std::to_string(i - (arg_offset + 1)) + ".", + PyExcType::kType); + } + full_size += 2; + break; + case 'c': // NOLINT(bugprone-branch-clone) + + // 1 byte int. + if (!PyNumber_Check(obj)) { + throw Exception("Expected an int for node message arg " + + std::to_string(i - (arg_offset + 1)) + ".", + PyExcType::kType); + } + full_size += 1; + break; + case 'b': + + // bool (currently 1 byte int). + if (!PyNumber_Check(obj)) { + throw Exception("Expected an int for node message arg " + + std::to_string(i - (arg_offset + 1)) + ".", + PyExcType::kType); + } + full_size += 1; + break; + case 'F': + + // 32 bit float. + if (!PyNumber_Check(obj)) { + throw Exception("Expected a float for node message arg " + + std::to_string(i - (arg_offset + 1)) + ".", + PyExcType::kType); + } + full_size += 4; + break; + case 'f': + + // 16 bit float. + if (!PyNumber_Check(obj)) { + throw Exception("Expected a float for node message arg " + + std::to_string(i - (arg_offset + 1)) + ".", + PyExcType::kType); + } + full_size += 2; + break; + case 's': + if (!PyUnicode_Check(obj)) { + throw Exception("Expected a string for node message arg " + + std::to_string(i - (arg_offset + 1)) + ".", + PyExcType::kType); + } + full_size += strlen(PyUnicode_AsUTF8(obj)) + 1; + break; + default: + throw Exception("Invalid argument type: " + std::to_string(*f) + ".", + PyExcType::kValue); + break; + } + f++; + } + + // Make sure our format string ends the same time as our arg count. + if (*f != 0) { + throw Exception("Wrong number of arguments on node message '" + type + "'.", + PyExcType::kValue); + } + b->resize(full_size); + char* ptr = b->data(); + *ptr = static_cast(ac); + ptr++; + f = format; + for (Py_ssize_t i = arg_offset + 1; i < tuple_size; i++) { + obj = PyTuple_GET_ITEM(args, i); + BA_PRECONDITION(obj); + switch (*f) { + case 'I': + Utils::EmbedInt32NBO( + &ptr, static_cast_check_fit(Python::GetPyInt64(obj))); + break; + case 'i': + Utils::EmbedInt16NBO( + &ptr, static_cast_check_fit(Python::GetPyInt64(obj))); + break; + case 'c': // NOLINT(bugprone-branch-clone) + Utils::EmbedInt8( + &ptr, static_cast_check_fit(Python::GetPyInt64(obj))); + break; + case 'b': + Utils::EmbedInt8( + &ptr, static_cast_check_fit(Python::GetPyInt64(obj))); + break; + case 'F': + Utils::EmbedFloat32(&ptr, Python::GetPyFloat(obj)); + break; + case 'f': + Utils::EmbedFloat16NBO(&ptr, Python::GetPyFloat(obj)); + break; + case 's': + Utils::EmbedString(&ptr, PyUnicode_AsUTF8(obj)); + break; + default: + throw Exception(PyExcType::kValue); + break; + } + f++; + } +} + +auto SceneV1Python::GetPyInputDevice(PyObject* o) + -> SceneV1InputDeviceDelegate* { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (PythonClassInputDevice::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetInputDevice(); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get input-device from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +void SceneV1Python::CaptureJoystickInput(PyObject* obj) { + assert(g_base->InLogicThread()); + ReleaseJoystickInputCapture(); + if (PyCallable_Check(obj)) { + joystick_capture_call_.Acquire(obj); + g_base->input->CaptureJoystickInput(HandleCapturedJoystickEventCall); + } else { + throw Exception("Object is not callable.", PyExcType::kType); + } +} + +void SceneV1Python::ReleaseJoystickInputCapture() { + joystick_capture_call_.Release(); + g_base->input->ReleaseJoystickInput(); +} + +void SceneV1Python::CaptureKeyboardInput(PyObject* obj) { + assert(g_base->InLogicThread()); + ReleaseKeyboardInputCapture(); + if (PyCallable_Check(obj)) { + keyboard_capture_call_.Acquire(obj); + g_base->input->CaptureKeyboardInput(HandleCapturedKeyPressCall, + HandleCapturedKeyReleaseCall); + } else { + throw Exception("Object is not callable.", PyExcType::kType); + } +} +void SceneV1Python::ReleaseKeyboardInputCapture() { + keyboard_capture_call_.Release(); + g_base->input->ReleaseKeyboardInput(); +} + +auto SceneV1Python::HandleCapturedJoystickEventCall( + const SDL_Event& event, base::InputDevice* input_device) -> bool { + return g_scene_v1->python->HandleCapturedJoystickEvent(event, input_device); +} + +auto SceneV1Python::HandleCapturedKeyPressCall(const SDL_Keysym& keysym) + -> bool { + return g_scene_v1->python->HandleCapturedKeyPress(keysym); +} + +auto SceneV1Python::HandleCapturedKeyReleaseCall(const SDL_Keysym& keysym) + -> bool { + return g_scene_v1->python->HandleCapturedKeyRelease(keysym); +} + +auto SceneV1Python::HandleCapturedKeyPress(const SDL_Keysym& keysym) -> bool { + assert(g_base->InLogicThread()); + if (!keyboard_capture_call_.Exists()) { + return false; + } + base::ScopedSetContext ssc(nullptr); + auto* keyboard = g_base->input->keyboard_input(); + BA_PRECONDITION(keyboard); + + // This currently only works with the scene_v1 input-device classes. + if (auto* delegate = + dynamic_cast(&keyboard->delegate())) { + PythonRef args( + Py_BuildValue("({s:s,s:i,s:O})", "type", "BUTTONDOWN", "button", + static_cast(keysym.sym), "input_device", + keyboard ? delegate->BorrowPyRef() : Py_None), + PythonRef::kSteal); + keyboard_capture_call_.Call(args); + } else { + BA_LOG_ONCE( + LogLevel::kWarning, + "Python key-press callbacks do not work with this input-device class."); + } + return true; +} +auto SceneV1Python::HandleCapturedKeyRelease(const SDL_Keysym& keysym) -> bool { + assert(g_base->InLogicThread()); + if (!keyboard_capture_call_.Exists()) { + return false; + } + base::ScopedSetContext ssc(nullptr); + auto* keyboard = g_base->input->keyboard_input(); + BA_PRECONDITION(keyboard); + + // This currently only works with the scene_v1 input-device classes. + if (auto* delegate = + dynamic_cast(&keyboard->delegate())) { + PythonRef args( + Py_BuildValue("({s:s,s:i,s:O})", "type", "BUTTONUP", "button", + static_cast(keysym.sym), "input_device", + keyboard ? delegate->BorrowPyRef() : Py_None), + PythonRef::kSteal); + keyboard_capture_call_.Call(args); + } else { + BA_LOG_ONCE( + LogLevel::kWarning, + "Python key-press callbacks do not work with this input-device class."); + } + return true; +} + +auto SceneV1Python::HandleCapturedJoystickEvent(const SDL_Event& event, + base::InputDevice* input_device) + -> bool { + assert(g_base->InLogicThread()); + assert(input_device != nullptr); + if (!joystick_capture_call_.Exists()) { + return false; + } + // This currently only works with the scene_v1 input-device classes. + if (auto* delegate = dynamic_cast( + &input_device->delegate())) { + base::ScopedSetContext ssc(nullptr); + // If we got a device we can pass events. + if (input_device) { + switch (event.type) { + case SDL_JOYBUTTONDOWN: { + PythonRef args( + Py_BuildValue("({s:s,s:i,s:O})", "type", "BUTTONDOWN", "button", + static_cast(event.jbutton.button) + + 1, // give them base-1 + "input_device", delegate->BorrowPyRef()), + PythonRef::kSteal); + joystick_capture_call_.Call(args); + break; + } + case SDL_JOYBUTTONUP: { + PythonRef args( + Py_BuildValue("({s:s,s:i,s:O})", "type", "BUTTONUP", "button", + static_cast(event.jbutton.button) + + 1, // give them base-1 + "input_device", delegate->BorrowPyRef()), + PythonRef::kSteal); + joystick_capture_call_.Call(args); + break; + } + case SDL_JOYHATMOTION: { + PythonRef args( + Py_BuildValue( + "({s:s,s:i,s:i,s:O})", "type", "HATMOTION", "hat", + static_cast(event.jhat.hat) + 1, // give them base-1 + "value", event.jhat.value, "input_device", + delegate->BorrowPyRef()), + PythonRef::kSteal); + joystick_capture_call_.Call(args); + break; + } + case SDL_JOYAXISMOTION: { + PythonRef args( + Py_BuildValue( + "({s:s,s:i,s:f,s:O})", "type", "AXISMOTION", "axis", + static_cast(event.jaxis.axis) + 1, // give them base-1 + "value", + std::min(1.0f, + std::max(-1.0f, static_cast(event.jaxis.value) + / 32767.0f)), + "input_device", delegate->BorrowPyRef()), + PythonRef::kSteal); + joystick_capture_call_.Call(args); + break; + } + default: + break; + } + } + } else { + BA_LOG_ONCE( + LogLevel::kWarning, + "Python key-press callbacks do not work with this input-device class."); + } + return true; +} + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/python/scene_v1_python.h b/src/ballistica/scene_v1/python/scene_v1_python.h new file mode 100644 index 00000000..404e72f7 --- /dev/null +++ b/src/ballistica/scene_v1/python/scene_v1_python.h @@ -0,0 +1,121 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_PYTHON_SCENE_V1_PYTHON_H_ +#define BALLISTICA_SCENE_V1_PYTHON_SCENE_V1_PYTHON_H_ + +#include "ballistica/base/base.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/python/python_object_set.h" + +namespace ballistica::scene_v1 { + +/// General Python support class for SceneV1. +class SceneV1Python { + public: + SceneV1Python(); + void Reset(); + + static void SetNodeAttr(Node* node, const char* attr_name, + PyObject* value_obj); + static auto DoNewNode(PyObject* args, PyObject* keywds) -> Node*; + static auto GetNodeAttr(Node* node, const char* attr_name) -> PyObject*; + static auto GetPyHostActivity(PyObject* o) -> HostActivity*; + static auto IsPyHostActivity(PyObject* o) -> bool; + static auto GetPyNode(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> Node*; + static auto GetPyNodes(PyObject* o) -> std::vector; + static auto GetPyMaterial(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> Material*; + static auto GetPyMaterials(PyObject* o) -> std::vector; + static auto GetPySceneTexture(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> SceneTexture*; + static auto GetPySceneTextures(PyObject* o) -> std::vector; + static auto GetPySceneMesh(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> SceneMesh*; + static auto GetPySceneMeshes(PyObject* o) -> std::vector; + static auto IsPyPlayer(PyObject* o) -> bool; + static auto GetPyPlayer(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> Player*; + static auto GetPySceneSound(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> SceneSound*; + static auto GetPySceneSounds(PyObject* o) -> std::vector; + static auto GetPySceneCollisionMesh(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) + -> SceneCollisionMesh*; + static auto GetPySceneCollisionMeshes(PyObject* o) + -> std::vector; + static auto IsPySession(PyObject* o) -> bool; + static auto GetPySession(PyObject* o) -> Session*; + static auto GetPySessionPlayer(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> Player*; + static auto GetPySceneDataAsset(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> SceneDataAsset*; + static void DoBuildNodeMessage(PyObject* args, int arg_offset, + std::vector* b, + PyObject** user_message_obj); + static auto GetPyInputDevice(PyObject* o) -> SceneV1InputDeviceDelegate*; + + void CaptureJoystickInput(PyObject* obj); + void ReleaseJoystickInputCapture(); + void CaptureKeyboardInput(PyObject* obj); + void ReleaseKeyboardInputCapture(); + + /// Filter incoming chat message from client. + /// If returns false, message should be ignored. + auto FilterChatMessage(std::string* message, int client_id) -> bool; + + /// Pass a chat message along to the python UI layer for handling.. + void HandleLocalChatMessage(const std::string& message); + + /// Given an asset-package python object and a media name, verify + /// that the asset-package is valid in the current context_ref and return + /// its fully qualified name if so. Throw an Exception if not. + auto ValidatedPackageAssetName(PyObject* package, const char* name) + -> std::string; + + /// Specific Python objects we hold in objs_. + enum class ObjID { + kClientInfoQueryResponseCall, + kShouldShatterMessageClass, + kImpactDamageMessageClass, + kPickedUpMessageClass, + kDroppedMessageClass, + kOutOfBoundsMessageClass, + kPickUpMessageClass, + kDropMessageClass, + kPlayerClass, + kAssetPackageClass, + kActivityClass, + kSceneV1SessionClass, + kLaunchMainMenuSessionCall, + kGetPlayerIconCall, + kFilterChatMessageCall, + kHandleLocalChatMessageCall, + kLast // Sentinel; must be at end. + }; + + void AddPythonClasses(PyObject* module); + void ImportPythonObjs(); + + const auto& objs() { return objs_; } + + private: + static auto HandleCapturedJoystickEventCall(const SDL_Event& event, + base::InputDevice* input_device) + -> bool; + static auto HandleCapturedKeyPressCall(const SDL_Keysym& keysym) -> bool; + static auto HandleCapturedKeyReleaseCall(const SDL_Keysym& keysym) -> bool; + auto HandleCapturedJoystickEvent(const SDL_Event& event, + base::InputDevice* input_device = nullptr) + -> bool; + auto HandleCapturedKeyPress(const SDL_Keysym& keysym) -> bool; + auto HandleCapturedKeyRelease(const SDL_Keysym& keysym) -> bool; + + PythonObjectSet objs_; + PythonRef joystick_capture_call_; + PythonRef keyboard_capture_call_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_PYTHON_SCENE_V1_PYTHON_H_ diff --git a/src/ballistica/scene_v1/scene_v1.cc b/src/ballistica/scene_v1/scene_v1.cc new file mode 100644 index 00000000..1996dbda --- /dev/null +++ b/src/ballistica/scene_v1/scene_v1.cc @@ -0,0 +1,189 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/scene_v1.h" + +#include "ballistica/classic/classic.h" +#include "ballistica/scene_v1/node/anim_curve_node.h" +#include "ballistica/scene_v1/node/bomb_node.h" +#include "ballistica/scene_v1/node/combine_node.h" +#include "ballistica/scene_v1/node/explosion_node.h" +#include "ballistica/scene_v1/node/flag_node.h" +#include "ballistica/scene_v1/node/flash_node.h" +#include "ballistica/scene_v1/node/globals_node.h" +#include "ballistica/scene_v1/node/image_node.h" +#include "ballistica/scene_v1/node/light_node.h" +#include "ballistica/scene_v1/node/locator_node.h" +#include "ballistica/scene_v1/node/math_node.h" +#include "ballistica/scene_v1/node/null_node.h" +#include "ballistica/scene_v1/node/player_node.h" +#include "ballistica/scene_v1/node/region_node.h" +#include "ballistica/scene_v1/node/scorch_node.h" +#include "ballistica/scene_v1/node/session_globals_node.h" +#include "ballistica/scene_v1/node/shield_node.h" +#include "ballistica/scene_v1/node/sound_node.h" +#include "ballistica/scene_v1/node/spaz_node.h" +#include "ballistica/scene_v1/node/terrain_node.h" +#include "ballistica/scene_v1/node/text_node.h" +#include "ballistica/scene_v1/node/texture_sequence_node.h" +#include "ballistica/scene_v1/node/time_display_node.h" +#include "ballistica/scene_v1/python/scene_v1_python.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/shared/generic/utils.h" + +// FIXME: TEMP; REMOVE THIS SOON. +auto TempSV1CreateAppMode() -> ballistica::base::AppMode* { + return ballistica::scene_v1::SceneV1AppMode::GetSingleton(); +} + +namespace ballistica::scene_v1 { + +core::CoreFeatureSet* g_core{}; +base::BaseFeatureSet* g_base{}; +SceneV1FeatureSet* g_scene_v1{}; +classic::ClassicFeatureSet* g_classic{}; + +void SceneV1FeatureSet::OnModuleExec(PyObject* module) { + // Ok, our feature-set's Python module is getting imported. + // Like any normal Python module, we take this opportunity to + // import/create the stuff we use. + + // Importing core should always be the first thing we do. + // Various ballistica functionality will fail if this has not been done. + assert(g_core == nullptr); + g_core = core::CoreFeatureSet::Import(); + + g_core->BootLog("_bascenev1 exec begin"); + + // Create our feature-set's C++ front-end. + assert(g_scene_v1 == nullptr); + g_scene_v1 = new SceneV1FeatureSet(); + g_scene_v1->python->AddPythonClasses(module); + + // Store our C++ front-end with our Python module. + // This lets anyone get at us by going through the Python + // import system (keeping things nice and consistent between + // Python and C++ worlds). + g_scene_v1->StoreOnPythonModule(module); + + // Import any Python stuff we use into objs_. + g_scene_v1->python->ImportPythonObjs(); + + // Import any other C++ feature-set-front-ends we use. + assert(g_base == nullptr); + g_base = base::BaseFeatureSet::Import(); + assert(g_classic == nullptr); + g_classic = classic::ClassicFeatureSet::Import(); + + g_core->BootLog("_bascenev1 exec end"); +} + +SceneV1FeatureSet::SceneV1FeatureSet() : python{new SceneV1Python()} { + NodeType* init_node_types[] = {NullNode::InitType(), + GlobalsNode::InitType(), + SessionGlobalsNode::InitType(), + PropNode::InitType(), + FlagNode::InitType(), + BombNode::InitType(), + ExplosionNode::InitType(), + ShieldNode::InitType(), + LightNode::InitType(), + TextNode::InitType(), + AnimCurveNode::InitType(), + ImageNode::InitType(), + TerrainNode::InitType(), + MathNode::InitType(), + LocatorNode::InitType(), + PlayerNode::InitType(), + CombineNode::InitType(), + SoundNode::InitType(), + SpazNode::InitType(), + RegionNode::InitType(), + ScorchNode::InitType(), + FlashNode::InitType(), + TextureSequenceNode::InitType(), + TimeDisplayNode::InitType()}; + + int next_type_id{}; + for (auto* t : init_node_types) { + node_types_[t->name()] = t; + node_types_by_id_[next_type_id] = t; + t->set_id(next_type_id++); + } + + // Types: I is 32 bit int, i is 16 bit int, c is 8 bit int, + // F is 32 bit float, f is 16 bit float, + // s is string, b is bool. + SetupNodeMessageType("flash", NodeMessageType::kFlash, ""); + SetupNodeMessageType("footing", NodeMessageType::kFooting, "c"); + SetupNodeMessageType("impulse", NodeMessageType::kImpulse, "fffffffffifff"); + SetupNodeMessageType("kick_back", NodeMessageType::kKickback, "fffffff"); + SetupNodeMessageType("celebrate", NodeMessageType::kCelebrate, "i"); + SetupNodeMessageType("celebrate_l", NodeMessageType::kCelebrateL, "i"); + SetupNodeMessageType("celebrate_r", NodeMessageType::kCelebrateR, "i"); + SetupNodeMessageType("knockout", NodeMessageType::kKnockout, "f"); + SetupNodeMessageType("hurt_sound", NodeMessageType::kHurtSound, ""); + SetupNodeMessageType("picked_up", NodeMessageType::kPickedUp, ""); + SetupNodeMessageType("jump_sound", NodeMessageType::kJumpSound, ""); + SetupNodeMessageType("attack_sound", NodeMessageType::kAttackSound, ""); + SetupNodeMessageType("scream_sound", NodeMessageType::kScreamSound, ""); + SetupNodeMessageType("stand", NodeMessageType::kStand, "ffff"); +} + +void SceneV1FeatureSet::Reset() { + assert(g_base->InLogicThread()); + g_scene_v1->python->Reset(); +} + +void SceneV1FeatureSet::ResetRandomNames() { + assert(g_base->InLogicThread()); + if (random_name_registry_ == nullptr) { + return; + } + random_name_registry_->clear(); +} + +auto SceneV1FeatureSet::GetRandomName(const std::string& full_name) + -> std::string { + assert(g_base->InLogicThread()); + + // Hmm; statically allocating this is giving some crashes on shutdown :-( + if (random_name_registry_ == nullptr) { + random_name_registry_ = new std::unordered_map(); + } + + auto i = random_name_registry_->find(full_name); + if (i == random_name_registry_->end()) { + // Doesn't exist. Pull a random one and add it. + // Refill the global list if its empty. + if (default_names_.empty()) { + const std::list& random_name_list = + Utils::GetRandomNameList(); + for (const auto& i2 : random_name_list) { + default_names_.push_back(i2); + } + } + + // Ok now pull a random one off the list and assign it to us + int index = static_cast(rand() % default_names_.size()); // NOLINT + auto i3 = default_names_.begin(); + for (int j = 0; j < index; j++) { + i3++; + } + (*random_name_registry_)[full_name] = *i3; + default_names_.erase(i3); + } + return (*random_name_registry_)[full_name]; +} + +void SceneV1FeatureSet::SetupNodeMessageType(const std::string& name, + NodeMessageType val, + const std::string& format) { + node_message_types_[name] = val; + assert(static_cast(val) >= 0); + if (node_message_formats_.size() <= static_cast(val)) { + node_message_formats_.resize(static_cast(val) + 1); + } + node_message_formats_[static_cast(val)] = format; +} + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/scene_v1.h b/src/ballistica/scene_v1/scene_v1.h new file mode 100644 index 00000000..11c0549d --- /dev/null +++ b/src/ballistica/scene_v1/scene_v1.h @@ -0,0 +1,353 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_SCENE_V1_H_ +#define BALLISTICA_SCENE_V1_SCENE_V1_H_ + +#include +#include +#include + +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/foundation/feature_set_front_end.h" +#include "ballistica/shared/python/python_ref.h" + +// Common header that most everything using our feature-set should include. +// It predeclares our feature-set's various types and globals and other +// bits. + +// Predeclared types from other feature sets that we use. +namespace ballistica::core { +class CoreFeatureSet; +} +namespace ballistica::base { +class BaseFeatureSet; +} +namespace ballistica::classic { +class ClassicFeatureSet; +} + +namespace ballistica::scene_v1 { + +// Sim step size in milliseconds. +const int kGameStepMilliseconds = 8; + +// Sim step size in seconds. +const float kGameStepSeconds = + (static_cast(kGameStepMilliseconds) / 1000.0f); + +// Predeclared types our feature-set provides. +class ClientControllerInterface; +class ClientInputDevice; +class ClientSession; +class SceneCollisionMesh; +class Collision; +class Connection; +class ConnectionToClient; +class ConnectionToClientUDP; +class ConnectionToHost; +class ConnectionToHostUDP; +class ConnectionSet; +class SceneV1Context; +class ContextRefSceneV1; +class SceneCubeMapTexture; +class SceneDataAsset; +class Dynamics; +class SceneV1FeatureSet; +class GlobalsNode; +class HostSession; +struct JointFixedEF; +class SceneV1InputDeviceDelegate; +class MaterialAction; +class SceneMesh; +class HostActivity; +class Material; +class MaterialComponent; +class MaterialConditionNode; +class MaterialContext; +class Node; +class NodeAttribute; +class NodeAttributeConnection; +class NodeAttributeUnbound; +class NodeType; +class Part; +class Player; +class PlayerNode; +class PlayerSpec; +class PythonClassSceneDataAsset; +class PythonClassSceneCollisionMesh; +class PythonClassMaterial; +class PythonClassSceneMesh; +class PythonClassSessionPlayer; +class PythonClassSceneSound; +class PythonClassSceneTexture; +class SceneV1Python; +class ClientSessionReplay; +class RigidBody; +class SessionStream; +class Scene; +class SceneV1AppMode; +class SceneV1FeatureSet; +class Session; +class SceneSound; +class SceneTexture; +typedef Node* NodeCreateFunc(Scene* sg); + +/// Standard messages to send to nodes. +enum class NodeMessageType { + /// Generic flash - no args. + kFlash, + /// Celebrate message - one int arg for duration. + kCelebrate, + /// Left-hand celebrate message - one int arg for duration. + kCelebrateL, + /// Right-hand celebrate message - one int arg for duration. + kCelebrateR, + /// Instantaneous impulse 3 vector floats. + kImpulse, + kKickback, + /// Knock the target out for an amount of time. + kKnockout, + /// Make a hurt sound. + kHurtSound, + /// You've been picked up.. lose balance or whatever. + kPickedUp, + /// Make a jump sound. + kJumpSound, + /// Make an attack sound. + kAttackSound, + /// Tell the player to scream. + kScreamSound, + /// Move to stand upon the given point facing the given angle. + /// 3 position floats and one angle float. + kStand, + /// Add or remove footing from a node. + /// First arg is an int - either 1 or -1 for add or subtract. + kFooting +}; + +/// Command values sent across the wire in netplay. +/// Must remain consistent across versions! +enum class SessionCommand { + kBaseTimeStep, + kStepSceneGraph, + kAddSceneGraph, + kRemoveSceneGraph, + kAddNode, + kNodeOnCreate, + kSetForegroundScene, + kRemoveNode, + kAddMaterial, + kRemoveMaterial, + kAddMaterialComponent, + kAddTexture, + kRemoveTexture, + kAddMesh, + kRemoveMesh, + kAddSound, + kRemoveSound, + kAddCollisionMesh, + kRemoveCollisionMesh, + kConnectNodeAttribute, + kNodeMessage, + kSetNodeAttrFloat, + kSetNodeAttrInt32, + kSetNodeAttrBool, + kSetNodeAttrFloats, + kSetNodeAttrInt32s, + kSetNodeAttrString, + kSetNodeAttrNode, + kSetNodeAttrNodeNull, + kSetNodeAttrNodes, + kSetNodeAttrPlayer, + kSetNodeAttrPlayerNull, + kSetNodeAttrMaterials, + kSetNodeAttrTexture, + kSetNodeAttrTextureNull, + kSetNodeAttrTextures, + kSetNodeAttrSound, + kSetNodeAttrSoundNull, + kSetNodeAttrSounds, + kSetNodeAttrMesh, + kSetNodeAttrMeshNull, + kSetNodeAttrMeshes, + kSetNodeAttrCollisionMesh, + kSetNodeAttrCollisionMeshNull, + kSetNodeAttrCollisionMeshes, + kPlaySoundAtPosition, + kPlaySound, + kEmitBGDynamics, + kEndOfFile, + kDynamicsCorrection, + kScreenMessageBottom, + kScreenMessageTop, + kAddData, + kRemoveData +}; + +enum class NodeCollideAttr { + /// Whether or not a collision should occur at all. + /// If this is false for either node in the final context_ref, + /// no collide events are run. + kCollideNode +}; + +enum class PartCollideAttr { + /// Whether or not a collision should occur at all. + /// If this is false for either surface in the final context_ref, + /// no collide events are run. + kCollide, + + /// Whether to honor node-collisions. + /// Turn this on if you want a collision to occur even if + /// The part is ignoring collisions with your node due + /// to an existing NodeModAction. + kUseNodeCollide, + + /// Whether a physical collision happens. + kPhysical, + + /// Friction for physical collisions. + kFriction, + + /// Stiffness for physical collisions. + kStiffness, + + /// Damping for physical collisions. + kDamping, + + /// Bounce for physical collisions. + kBounce +}; + +enum class MaterialCondition { + /// Always evaluates to true. + kTrue, + + /// Always evaluates to false. + kFalse, + + /// Dst part contains specified material; requires 1 arg - material id. + kDstIsMaterial, + + /// Dst part does not contain specified material; requires 1 arg - material + /// id. + kDstNotMaterial, + + /// Dst part is in specified node; requires 1 arg - node id. + kDstIsNode, + + /// Dst part not in specified node; requires 1 arg - node id. + kDstNotNode, + + /// Dst part is specified part; requires 2 args, node id, part id. + kDstIsPart, + + /// Dst part not specified part; requires 2 args, node id, part id. + kDstNotPart, + + /// Dst part contains src material; no args. + kSrcDstSameMaterial, + + /// Dst part does not contain the src material; no args. + kSrcDstDiffMaterial, + + /// Dst and src parts in same node; no args. + kSrcDstSameNode, + + /// Dst and src parts in different node; no args. + kSrcDstDiffNode, + + /// Src part younger than specified value; requires 1 arg - age. + kSrcYoungerThan, + + /// Src part equal to or older than specified value; requires 1 arg - age. + kSrcOlderThan, + + /// Dst part younger than specified value; requires 1 arg - age. + kDstYoungerThan, + + /// Dst part equal to or older than specified value; requires 1 arg - age. + kDstOlderThan, + + /// Src part is already colliding with a part on dst node; no args. + kCollidingDstNode, + + /// Src part is not already colliding with a part on dst node; no args. + kNotCollidingDstNode, + + /// Set to collide at current point in rule evaluation. + kEvalColliding, + + /// Set to not collide at current point in rule evaluation. + kEvalNotColliding +}; + +enum NodeAttributeFlag { kNodeAttributeFlagReadOnly = 1u }; + +enum class NodeAttributeType { + kFloat, + kFloatArray, + kInt, + kIntArray, + kBool, + kString, + kNode, + kNodeArray, + kPlayer, + kMaterialArray, + kTexture, + kTextureArray, + kSound, + kSoundArray, + kMesh, + kMeshArray, + kCollisionMesh, + kCollisionMeshArray +}; + +// Our feature-set's globals. +// Feature-sets should NEVER directly access globals in another feature-set's +// namespace. All functionality we need from other feature-sets should be +// imported into globals in our own namespace. Generally we do this when we +// are initially imported (just as regular Python modules do). +extern core::CoreFeatureSet* g_core; +extern base::BaseFeatureSet* g_base; +extern SceneV1FeatureSet* g_scene_v1; +extern classic::ClassicFeatureSet* g_classic; + +class SceneV1FeatureSet : public FeatureSetFrontEnd { + public: + /// Called when our associated Python module is instantiated. + static void OnModuleExec(PyObject* module); + + void Reset(); + + void ResetRandomNames(); + // Given a full name "SomeJoyStick #3" etc, reserves/returns a persistent + // random name for it. + auto GetRandomName(const std::string& full_name) -> std::string; + + const auto& node_types_by_id() const { return node_types_by_id_; } + const auto& node_message_types() const { return node_message_types_; } + const auto& node_message_formats() const { return node_message_formats_; } + const auto& node_types() const { return node_types_; } + + // Our subcomponents. + SceneV1Python* const python; + + private: + void SetupNodeMessageType(const std::string& name, NodeMessageType val, + const std::string& format); + + SceneV1FeatureSet(); + std::unordered_map node_types_; + std::unordered_map node_types_by_id_; + std::unordered_map node_message_types_; + std::vector node_message_formats_; + std::unordered_map* random_name_registry_{}; + std::list default_names_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_SCENE_V1_H_ diff --git a/src/ballistica/scene_v1/support/client_controller_interface.h b/src/ballistica/scene_v1/support/client_controller_interface.h new file mode 100644 index 00000000..2d90eb91 --- /dev/null +++ b/src/ballistica/scene_v1/support/client_controller_interface.h @@ -0,0 +1,22 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_SUPPORT_CLIENT_CONTROLLER_INTERFACE_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_CLIENT_CONTROLLER_INTERFACE_H_ + +#include "ballistica/scene_v1/scene_v1.h" + +namespace ballistica::scene_v1 { + +/// An interface for something that can control client-connections +/// (such as an output-stream or a replay-client-session). +/// Objects can register themselves as the current client-connection-controller +/// and then they will get control of all existing (and forthcoming) clients. +class ClientControllerInterface { + public: + virtual void OnClientConnected(ConnectionToClient* c) = 0; + virtual void OnClientDisconnected(ConnectionToClient* c) = 0; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_SUPPORT_CLIENT_CONTROLLER_INTERFACE_H_ diff --git a/src/ballistica/scene_v1/support/client_input_device.cc b/src/ballistica/scene_v1/support/client_input_device.cc new file mode 100644 index 00000000..df4d562e --- /dev/null +++ b/src/ballistica/scene_v1/support/client_input_device.cc @@ -0,0 +1,24 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/support/client_input_device.h" + +#include "ballistica/scene_v1/connection/connection_to_client.h" + +namespace ballistica::scene_v1 { + +ClientInputDevice::ClientInputDevice(int remote_device_id, + ConnectionToClient* connection_to_client) + : remote_device_id_(remote_device_id), + connection_to_client_(connection_to_client) {} + +// Hmm; do we need to send a remote-detach in this case? +// I don't think so; if we're dying it means the connection is dying +// which means we probably couldn't communicate anyway and +// the other end will free the input-device up +ClientInputDevice::~ClientInputDevice() = default; + +auto ClientInputDevice::GetRawDeviceName() -> std::string { + return "Client Input Device"; +} + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/support/client_input_device.h b/src/ballistica/scene_v1/support/client_input_device.h new file mode 100644 index 00000000..9b9a295e --- /dev/null +++ b/src/ballistica/scene_v1/support/client_input_device.h @@ -0,0 +1,38 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_SUPPORT_CLIENT_INPUT_DEVICE_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_CLIENT_INPUT_DEVICE_H_ + +#include + +#include "ballistica/base/input/device/input_device.h" +#include "ballistica/scene_v1/scene_v1.h" + +namespace ballistica::scene_v1 { + +/// Represents a remote player on a client connected to us. +class ClientInputDevice : public base::InputDevice { + public: + ClientInputDevice(int remote_device_id, + ConnectionToClient* connection_to_client); + ~ClientInputDevice() override; + + auto GetRawDeviceName() -> std::string override; + auto IsLocal() -> bool override { return false; } + + void PassInputCommand(InputType type, float value) { + InputCommand(type, value); + } + auto connection_to_client() const -> ConnectionToClient* { + return connection_to_client_.Get(); + } + auto remote_device_id() const { return remote_device_id_; } + + private: + Object::WeakRef connection_to_client_; + int remote_device_id_{}; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_SUPPORT_CLIENT_INPUT_DEVICE_H_ diff --git a/src/ballistica/scene_v1/support/client_input_device_delegate.cc b/src/ballistica/scene_v1/support/client_input_device_delegate.cc new file mode 100644 index 00000000..8c0972ca --- /dev/null +++ b/src/ballistica/scene_v1/support/client_input_device_delegate.cc @@ -0,0 +1,95 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/support/client_input_device_delegate.h" + +#include "ballistica/base/networking/networking.h" +#include "ballistica/scene_v1/connection/connection_to_client.h" +#include "ballistica/scene_v1/support/client_input_device.h" + +namespace ballistica::scene_v1 { + +void ClientInputDeviceDelegate::StoreClientDeviceInfo( + ClientInputDevice* device) { + assert(!connection_to_client_.Exists()); + connection_to_client_ = device->connection_to_client(); + remote_device_id_ = device->remote_device_id(); +} + +void ClientInputDeviceDelegate::AttachToLocalPlayer(Player* player) { + if (ConnectionToClient* c = connection_to_client_.Get()) { + // Send a new-style message with a 32 bit player-id. + // (added during protocol 29; not always present) + { + std::vector data(6); + data[0] = BA_MESSAGE_ATTACH_REMOTE_PLAYER_2; + data[1] = static_cast_check_fit(remote_device_id_); + int val = player->id(); + memcpy(&(data[2]), &val, sizeof(val)); + c->SendReliableMessage(data); + } + + // We also need to send an old-style message as a fallback. + // FIXME: Can remove this once backwards-compat-protocol is > 29. + { + std::vector data(3); + data[0] = BA_MESSAGE_ATTACH_REMOTE_PLAYER; + data[1] = static_cast_check_fit(remote_device_id_); + data[2] = static_cast_check_fit(player->id()); + c->SendReliableMessage(data); + } + } + SceneV1InputDeviceDelegate::AttachToLocalPlayer(player); +} + +void ClientInputDeviceDelegate::DetachFromPlayer() { + // Tell the client that their device is no longer attached to a player. + if (ConnectionToClient* c = connection_to_client_.Get()) { + std::vector data(2); + data[0] = BA_MESSAGE_DETACH_REMOTE_PLAYER; + data[1] = static_cast_check_fit(remote_device_id_); + c->SendReliableMessage(data); + } + SceneV1InputDeviceDelegate::DetachFromPlayer(); +} + +auto ClientInputDeviceDelegate::GetClientID() const -> int { + if (ConnectionToClient* c = connection_to_client_.Get()) { + return c->id(); + } else { + Log(LogLevel::kError, + "ClientInputDevice::get_client_id(): connection_to_client no longer " + "exists; returning -1.."); + return -1; + } +} + +auto ClientInputDeviceDelegate::GetPublicV1AccountID() const -> std::string { + assert(g_base->InLogicThread()); + if (connection_to_client_.Exists()) { + return connection_to_client_->peer_public_account_id(); + } + return ""; +} + +auto ClientInputDeviceDelegate::GetAccountName(bool full) const -> std::string { + assert(g_base->InLogicThread()); + if (connection_to_client_.Exists()) { + if (full) { + return connection_to_client_->peer_spec().GetDisplayString(); + } else { + return connection_to_client_->peer_spec().GetShortName(); + } + } + return "???"; +} + +auto ClientInputDeviceDelegate::GetPlayerProfiles() const -> PyObject* { + if (connection_to_client_.Exists()) { + return connection_to_client_->GetPlayerProfiles(); + } + return nullptr; +} + +auto ClientInputDeviceDelegate::IsRemoteClient() const -> bool { return true; } + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/support/client_input_device_delegate.h b/src/ballistica/scene_v1/support/client_input_device_delegate.h new file mode 100644 index 00000000..03f6c8f2 --- /dev/null +++ b/src/ballistica/scene_v1/support/client_input_device_delegate.h @@ -0,0 +1,33 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_SUPPORT_CLIENT_INPUT_DEVICE_DELEGATE_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_CLIENT_INPUT_DEVICE_DELEGATE_H_ + +#include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" + +namespace ballistica::scene_v1 { + +class ClientInputDeviceDelegate : public SceneV1InputDeviceDelegate { + public: + void AttachToLocalPlayer(Player* player) override; + void DetachFromPlayer() override; + auto connection_to_client() const -> ConnectionToClient* { + return connection_to_client_.Get(); + } + + void StoreClientDeviceInfo(ClientInputDevice* device); + auto GetClientID() const -> int override; + auto GetPublicV1AccountID() const -> std::string override; + auto GetAccountName(bool full) const -> std::string override; + // Return player-profiles dict if available; otherwise nullptr. + auto GetPlayerProfiles() const -> PyObject* override; + auto IsRemoteClient() const -> bool override; + + private: + Object::WeakRef connection_to_client_; + int remote_device_id_{}; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_SUPPORT_CLIENT_INPUT_DEVICE_DELEGATE_H_ diff --git a/src/ballistica/logic/session/client_session.cc b/src/ballistica/scene_v1/support/client_session.cc similarity index 79% rename from src/ballistica/logic/session/client_session.cc rename to src/ballistica/scene_v1/support/client_session.cc index 73e5b67a..3dd3cd0a 100644 --- a/src/ballistica/logic/session/client_session.cc +++ b/src/ballistica/scene_v1/support/client_session.cc @@ -1,28 +1,26 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/logic/session/client_session.h" +#include "ballistica/scene_v1/support/client_session.h" -#include "ballistica/app/app.h" -#include "ballistica/assets/component/collide_model.h" -#include "ballistica/assets/component/model.h" -#include "ballistica/assets/component/sound.h" -#include "ballistica/assets/component/texture.h" -#include "ballistica/audio/audio.h" -#include "ballistica/dynamics/bg/bg_dynamics.h" -#include "ballistica/dynamics/material/material.h" -#include "ballistica/dynamics/material/material_action.h" -#include "ballistica/dynamics/material/material_component.h" -#include "ballistica/dynamics/material/material_condition_node.h" -#include "ballistica/dynamics/rigid_body.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/networking/networking.h" -#include "ballistica/python/python.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" -#include "ballistica/scene/scene_stream.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/dynamics/bg/bg_dynamics.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/networking/networking.h" +#include "ballistica/scene_v1/assets/scene_collision_mesh.h" +#include "ballistica/scene_v1/assets/scene_mesh.h" +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/assets/scene_texture.h" +#include "ballistica/scene_v1/dynamics/material/material.h" +#include "ballistica/scene_v1/dynamics/material/material_component.h" +#include "ballistica/scene_v1/dynamics/rigid_body.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/scene_v1/python/scene_v1_python.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/scene_v1/support/session_stream.h" -namespace ballistica { +namespace ballistica::scene_v1 { ClientSession::ClientSession() { ClearSessionObjs(); } @@ -33,17 +31,17 @@ void ClientSession::Reset(bool rewind) { void ClientSession::OnReset(bool rewind) { ClearSessionObjs(); - target_base_time_ = 0.0; - base_time_ = 0; + target_base_time_millisecs_ = 0.0; + base_time_millisecs_ = 0; } void ClientSession::ClearSessionObjs() { scenes_.clear(); nodes_.clear(); textures_.clear(); - models_.clear(); + meshes_.clear(); sounds_.clear(); - collide_models_.clear(); + collision_meshes_.clear(); materials_.clear(); commands_pending_.clear(); commands_.clear(); @@ -54,21 +52,21 @@ auto ClientSession::DoesFillScreen() const -> bool { // Look for any scene that has something that covers the background. // NOLINTNEXTLINE(readability-use-anyofallof) for (const auto& scene : scenes_) { - if ((scene.exists()) && (*scene).has_bg_cover()) { + if ((scene.Exists()) && (*scene).has_bg_cover()) { return true; } } return false; } -void ClientSession::Draw(FrameDef* f) { +void ClientSession::Draw(base::FrameDef* f) { // Just go through and draw all of our scenes. for (auto&& i : scenes_) { // NOTE - here we draw scenes in the order they were created, but // in a host-session we draw session first followed by activities // (that should be the same order in both cases, but just something to keep // in mind...) - if (i.exists()) { + if (i.Exists()) { i->Draw(f); } } @@ -171,20 +169,22 @@ auto ClientSession::ReadString() -> std::string { return &(buffer[0]); } -void ClientSession::Update(int time_advance) { +void ClientSession::Update(int time_advance_millisecs, double time_advance) { if (shutting_down_) { return; } // Allow replays to modulate speed, etc. - // QUESTION: can we just use consume_rate_ for this? - time_advance = GetActualTimeAdvance(time_advance); + // Also plug in our more exact time-advance here instead of the old int one. + double actual_advance_millisecs = + GetActualTimeAdvanceMillisecs(time_advance * 1000.0); - target_base_time_ += static_cast(time_advance) * consume_rate_; + target_base_time_millisecs_ += actual_advance_millisecs * consume_rate_; try { // Read and run all events up to our target time. - while (base_time_ < target_base_time_) { + while (static_cast(base_time_millisecs_) + < target_base_time_millisecs_) { // If we need to do something explicit to keep messages flowing in. // (informing the replay thread to feed us more, etc.). FetchMessages(); @@ -229,7 +229,7 @@ void ClientSession::Update(int time_advance) { } base_time_buffered_ -= stepsize; BA_PRECONDITION(base_time_buffered_ >= 0); - base_time_ += stepsize; + base_time_millisecs_ += stepsize; break; } case SessionCommand::kDynamicsCorrection: { @@ -244,7 +244,7 @@ void ClientSession::Update(int time_advance) { offset += 4; int body_count = current_cmd_[offset++]; Node* n = - (node_id < nodes_.size()) ? nodes_[node_id].get() : nullptr; + (node_id < nodes_.size()) ? nodes_[node_id].Get() : nullptr; for (int j = 0; j < body_count; j++) { int bodyid = current_cmd_[offset++]; uint16_t body_data_len; @@ -314,7 +314,7 @@ void ClientSession::Update(int time_advance) { if (static_cast(scenes_.size()) < (id + 1)) { scenes_.resize(static_cast(id) + 1); } - assert(!scenes_[id].exists()); + assert(!scenes_[id].Exists()); scenes_[id] = Object::New(starttime); scenes_[id]->set_stream_id(id); break; @@ -335,13 +335,14 @@ void ClientSession::Update(int time_advance) { int32_t vals[3]; // scene-id, nodetype-id, node-id ReadInt32_3(vals); Scene* scene = GetScene(vals[0]); - assert(g_app != nullptr); + assert(g_core != nullptr); if (vals[1] < 0 - || vals[1] >= static_cast(g_app->node_types_by_id.size())) { + || vals[1] >= static_cast( + g_scene_v1->node_types_by_id().size())) { throw Exception("invalid node type id"); } - NodeType* node_type = g_app->node_types_by_id[vals[1]]; + NodeType* node_type = g_scene_v1->node_types_by_id().at(vals[1]); // Fail if we get a ridiculous number of nodes. // FIXME: should enforce this on the server side too. @@ -352,17 +353,19 @@ void ClientSession::Update(int time_advance) { if (static_cast(nodes_.size()) < (id + 1)) { nodes_.resize(static_cast(id) + 1); } - assert(!nodes_[id].exists()); + assert(!nodes_[id].Exists()); { - ScopedSetContext _cp(this); + base::ScopedSetContext ssc(this); nodes_[id] = scene->NewNode(node_type->name(), "", nullptr); nodes_[id]->set_stream_id(id); } break; } - case SessionCommand::kSetForegroundSceneGraph: { + case SessionCommand::kSetForegroundScene: { Scene* scene = GetScene(ReadInt32()); - g_logic->SetForegroundScene(scene); + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + appmode->SetForegroundScene(scene); + } break; } case SessionCommand::kNodeMessage: { @@ -384,9 +387,9 @@ void ClientSession::Update(int time_advance) { Node* src_node = GetNode(vals[0]); Node* dst_node = GetNode(vals[2]); NodeAttributeUnbound* src_attr = - src_node->type()->GetAttribute(static_cast(vals[1])); + src_node->type()->GetAttribute(vals[1]); NodeAttributeUnbound* dst_attr = - dst_node->type()->GetAttribute(static_cast(vals[3])); + dst_node->type()->GetAttribute(vals[3]); src_node->ConnectAttribute(src_attr, dst_node, dst_attr); break; } @@ -408,7 +411,7 @@ void ClientSession::Update(int time_advance) { if (static_cast(materials_.size()) < (id + 1)) { materials_.resize(static_cast(id) + 1); } - assert(!materials_[id].exists()); + assert(!materials_[id].Exists()); materials_[id] = Object::New("", scene); materials_[id]->stream_id_ = id; break; @@ -451,9 +454,9 @@ void ClientSession::Update(int time_advance) { if (static_cast(textures_.size()) < (id + 1)) { textures_.resize(static_cast(id) + 1); } - assert(!textures_[id].exists()); - textures_[id] = Object::New(name, scene); - textures_[id]->stream_id_ = id; + assert(!textures_[id].Exists()); + textures_[id] = Object::New(name, scene); + textures_[id]->set_stream_id(id); break; } case SessionCommand::kRemoveTexture: { @@ -462,30 +465,30 @@ void ClientSession::Update(int time_advance) { textures_[id].Clear(); break; } - case SessionCommand::kAddModel: { - int32_t vals[2]; // scene-id, model-id + case SessionCommand::kAddMesh: { + int32_t vals[2]; // scene-id, mesh-id ReadInt32_2(vals); std::string name = ReadString(); Scene* scene = GetScene(vals[0]); - // Fail if we get a ridiculous number of models. + // Fail if we get a ridiculous number of meshes. // FIXME: Should enforce this on the server side too. int id = vals[1]; if (vals[1] < 0 || vals[1] >= 1000) { - throw Exception("invalid model id"); + throw Exception("invalid mesh id"); } - if (static_cast(models_.size()) < (id + 1)) { - models_.resize(static_cast(id) + 1); + if (static_cast(meshes_.size()) < (id + 1)) { + meshes_.resize(static_cast(id) + 1); } - assert(!models_[id].exists()); - models_[id] = Object::New(name, scene); - models_[id]->stream_id_ = id; + assert(!meshes_[id].Exists()); + meshes_[id] = Object::New(name, scene); + meshes_[id]->set_stream_id(id); break; } - case SessionCommand::kRemoveModel: { + case SessionCommand::kRemoveMesh: { int id = ReadInt32(); - GetModel(id); // make sure its valid - models_[id].Clear(); + GetMesh(id); // make sure its valid + meshes_[id].Clear(); break; } case SessionCommand::kAddSound: { @@ -502,9 +505,9 @@ void ClientSession::Update(int time_advance) { if (static_cast(sounds_.size()) < (id + 1)) { sounds_.resize(static_cast(id) + 1); } - assert(!sounds_[id].exists()); - sounds_[id] = Object::New(name, scene); - sounds_[id]->stream_id_ = id; + assert(!sounds_[id].Exists()); + sounds_[id] = Object::New(name, scene); + sounds_[id]->set_stream_id(id); break; } case SessionCommand::kRemoveSound: { @@ -513,37 +516,37 @@ void ClientSession::Update(int time_advance) { sounds_[id].Clear(); break; } - case SessionCommand::kAddCollideModel: { - int32_t vals[2]; // scene-id, collide_model-id + case SessionCommand::kAddCollisionMesh: { + int32_t vals[2]; // scene-id, collision_mesh-id ReadInt32_2(vals); std::string name = ReadString(); Scene* scene = GetScene(vals[0]); - // Fail if we get a ridiculous number of collide_models. + // Fail if we get a ridiculous number of collision_meshes. // FIXME: Should enforce this on the server side too. int id = vals[1]; if (vals[1] < 0 || vals[1] >= 1000) { - throw Exception("invalid collide_model id"); + throw Exception("invalid collision_mesh id"); } - if (static_cast(collide_models_.size()) < (id + 1)) { - collide_models_.resize(static_cast(id) + 1); + if (static_cast(collision_meshes_.size()) < (id + 1)) { + collision_meshes_.resize(static_cast(id) + 1); } - assert(!collide_models_[id].exists()); - collide_models_[id] = Object::New(name, scene); - collide_models_[id]->stream_id_ = id; + assert(!collision_meshes_[id].Exists()); + collision_meshes_[id] = Object::New(name, scene); + collision_meshes_[id]->set_stream_id(id); break; } - case SessionCommand::kRemoveCollideModel: { + case SessionCommand::kRemoveCollisionMesh: { int id = ReadInt32(); - GetCollideModel(id); // make sure its valid - collide_models_[id].Clear(); + GetCollisionMesh(id); // make sure its valid + collision_meshes_[id].Clear(); break; } case SessionCommand::kRemoveNode: { int id = ReadInt32(); Node* n = GetNode(id); n->scene()->DeleteNode(n); - assert(!nodes_[id].exists()); + assert(!nodes_[id].Exists()); break; } case SessionCommand::kSetNodeAttrFloat: { @@ -628,28 +631,28 @@ void ClientSession::Update(int time_advance) { case SessionCommand::kSetNodeAttrTextureNull: { int cmdvals[2]; ReadInt32_2(cmdvals); - Texture* val = nullptr; + SceneTexture* val = nullptr; GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); break; } case SessionCommand::kSetNodeAttrSoundNull: { int cmdvals[2]; ReadInt32_2(cmdvals); - Sound* val = nullptr; + SceneSound* val = nullptr; GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); break; } - case SessionCommand::kSetNodeAttrModelNull: { + case SessionCommand::kSetNodeAttrMeshNull: { int cmdvals[2]; ReadInt32_2(cmdvals); - Model* val = nullptr; + SceneMesh* val = nullptr; GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); break; } - case SessionCommand::kSetNodeAttrCollideModelNull: { + case SessionCommand::kSetNodeAttrCollisionMeshNull: { int cmdvals[2]; ReadInt32_2(cmdvals); - CollideModel* val = nullptr; + SceneCollisionMesh* val = nullptr; GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); break; } @@ -675,7 +678,7 @@ void ClientSession::Update(int time_advance) { case SessionCommand::kSetNodeAttrTexture: { int cmdvals[3]; ReadInt32_3(cmdvals); - Texture* val = GetTexture(cmdvals[2]); + SceneTexture* val = GetTexture(cmdvals[2]); GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); break; } @@ -688,7 +691,7 @@ void ClientSession::Update(int time_advance) { + ")"); } std::vector vals_in(static_cast(count)); - std::vector vals(static_cast(count)); + std::vector vals(static_cast(count)); if (count > 0) { ReadInt32s(count, &(vals_in[0])); } @@ -701,7 +704,7 @@ void ClientSession::Update(int time_advance) { case SessionCommand::kSetNodeAttrSound: { int cmdvals[3]; ReadInt32_3(cmdvals); - Sound* val = GetSound(cmdvals[2]); + SceneSound* val = GetSound(cmdvals[2]); GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); break; } @@ -714,7 +717,7 @@ void ClientSession::Update(int time_advance) { + ")"); } std::vector vals_in(static_cast(count)); - std::vector vals(static_cast(count)); + std::vector vals(static_cast(count)); if (count > 0) { ReadInt32s(count, &(vals_in[0])); } @@ -724,14 +727,14 @@ void ClientSession::Update(int time_advance) { GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); break; } - case SessionCommand::kSetNodeAttrModel: { + case SessionCommand::kSetNodeAttrMesh: { int cmdvals[3]; ReadInt32_3(cmdvals); - Model* val = GetModel(cmdvals[2]); + SceneMesh* val = GetMesh(cmdvals[2]); GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); break; } - case SessionCommand::kSetNodeAttrModels: { + case SessionCommand::kSetNodeAttrMeshes: { int cmdvals[3]; ReadInt32_3(cmdvals); int count = cmdvals[2]; @@ -740,24 +743,24 @@ void ClientSession::Update(int time_advance) { + ")"); } std::vector vals_in(static_cast(count)); - std::vector vals(static_cast(count)); + std::vector vals(static_cast(count)); if (count > 0) { ReadInt32s(count, &(vals_in[0])); } for (int i = 0; i < count; i++) { - vals[i] = GetModel(vals_in[i]); + vals[i] = GetMesh(vals_in[i]); } GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); break; } - case SessionCommand::kSetNodeAttrCollideModel: { + case SessionCommand::kSetNodeAttrCollisionMesh: { int cmdvals[3]; ReadInt32_3(cmdvals); - CollideModel* val = GetCollideModel(cmdvals[2]); + SceneCollisionMesh* val = GetCollisionMesh(cmdvals[2]); GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); break; } - case SessionCommand::kSetNodeAttrCollideModels: { + case SessionCommand::kSetNodeAttrCollisionMeshes: { int cmdvals[3]; ReadInt32_3(cmdvals); int count = cmdvals[2]; @@ -766,12 +769,12 @@ void ClientSession::Update(int time_advance) { + ")"); } std::vector vals_in(static_cast(count)); - std::vector vals(static_cast(count)); + std::vector vals(static_cast(count)); if (count > 0) { ReadInt32s(count, &(vals_in[0])); } for (int i = 0; i < count; i++) { - vals[i] = GetCollideModel(vals_in[i]); + vals[i] = GetCollisionMesh(vals_in[i]); } GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); break; @@ -796,9 +799,9 @@ void ClientSession::Update(int time_advance) { break; } case SessionCommand::kPlaySound: { - Sound* sound = GetSound(ReadInt32()); + SceneSound* sound = GetSound(ReadInt32()); float volume = ReadFloat(); - g_audio->PlaySound(sound->GetSoundData(), volume); + g_base->audio->PlaySound(sound->GetSoundData(), volume); break; } case SessionCommand::kScreenMessageBottom: { @@ -811,23 +814,25 @@ void ClientSession::Update(int time_advance) { case SessionCommand::kScreenMessageTop: { int cmdvals[2]; ReadInt32_2(cmdvals); - Texture* texture = GetTexture(cmdvals[0]); - Texture* tint_texture = GetTexture(cmdvals[1]); + SceneTexture* texture = GetTexture(cmdvals[0]); + SceneTexture* tint_texture = GetTexture(cmdvals[1]); std::string s = ReadString(); float f[9]; ReadFloats(9, f); - g_graphics->AddScreenMessage( - s, Vector3f(f[0], f[1], f[2]), true, texture, tint_texture, - Vector3f(f[3], f[4], f[5]), Vector3f(f[6], f[7], f[8])); + g_base->graphics->AddScreenMessage( + s, Vector3f(f[0], f[1], f[2]), true, texture->texture_data(), + tint_texture->texture_data(), Vector3f(f[3], f[4], f[5]), + Vector3f(f[6], f[7], f[8])); break; } case SessionCommand::kPlaySoundAtPosition: { - Sound* sound = GetSound(ReadInt32()); + SceneSound* sound = GetSound(ReadInt32()); float volume = ReadFloat(); float x = ReadFloat(); float y = ReadFloat(); float z = ReadFloat(); - g_audio->PlaySoundAtPosition(sound->GetSoundData(), volume, x, y, z); + g_base->audio->PlaySoundAtPosition(sound->GetSoundData(), volume, x, + y, z); break; } case SessionCommand::kEmitBGDynamics: { @@ -835,12 +840,12 @@ void ClientSession::Update(int time_advance) { ReadInt32_4(cmdvals); float vals[8]; ReadFloats(8, vals); - if (g_bg_dynamics != nullptr) { - BGDynamicsEmission e; - e.emit_type = (BGDynamicsEmitType)cmdvals[0]; + if (g_base && g_base->bg_dynamics != nullptr) { + base::BGDynamicsEmission e; + e.emit_type = (base::BGDynamicsEmitType)cmdvals[0]; e.count = cmdvals[1]; - e.chunk_type = (BGDynamicsChunkType)cmdvals[2]; - e.tendril_type = (BGDynamicsTendrilType)cmdvals[3]; + e.chunk_type = (base::BGDynamicsChunkType)cmdvals[2]; + e.tendril_type = (base::BGDynamicsTendrilType)cmdvals[3]; e.position.x = vals[0]; e.position.y = vals[1]; e.position.z = vals[2]; @@ -849,7 +854,7 @@ void ClientSession::Update(int time_advance) { e.velocity.z = vals[5]; e.scale = vals[6]; e.spread = vals[7]; - g_bg_dynamics->Emit(e); + g_base->bg_dynamics->Emit(e); } break; } @@ -868,7 +873,7 @@ ClientSession::~ClientSession() = default; void ClientSession::ScreenSizeChanged() { // Let all our scenes know. for (auto&& i : scenes_) { - if (Scene* sg = i.get()) { + if (Scene* sg = i.Get()) { sg->ScreenSizeChanged(); } } @@ -877,7 +882,7 @@ void ClientSession::ScreenSizeChanged() { void ClientSession::LanguageChanged() { // Let all our scenes know. for (auto&& i : scenes_) { - if (Scene* sg = i.get()) { + if (Scene* sg = i.Get()) { sg->LanguageChanged(); } } @@ -887,7 +892,7 @@ auto ClientSession::GetScene(int id) const -> Scene* { if (id < 0 || id >= static_cast(scenes_.size())) { throw Exception("Invalid scene id"); } - Scene* sg = scenes_[id].get(); + Scene* sg = scenes_[id].Get(); if (!sg) { throw Exception("Invalid scene id"); } @@ -897,7 +902,7 @@ auto ClientSession::GetNode(int id) const -> Node* { if (id < 0 || id >= static_cast(nodes_.size())) { throw Exception("Invalid node (out of range)"); } - Node* n = nodes_[id].get(); + Node* n = nodes_[id].Get(); if (!n) { throw Exception("Invalid node id (empty slot)"); } @@ -907,49 +912,49 @@ auto ClientSession::GetMaterial(int id) const -> Material* { if (id < 0 || id >= static_cast(materials_.size())) { throw Exception("Invalid material (out of range)"); } - Material* n = materials_[id].get(); + Material* n = materials_[id].Get(); if (!n) { throw Exception("Invalid material id (empty slot)"); } return n; } -auto ClientSession::GetTexture(int id) const -> Texture* { +auto ClientSession::GetTexture(int id) const -> SceneTexture* { if (id < 0 || id >= static_cast(textures_.size())) { throw Exception("Invalid texture (out of range)"); } - Texture* n = textures_[id].get(); + SceneTexture* n = textures_[id].Get(); if (!n) { throw Exception("Invalid texture id (empty slot)"); } return n; } -auto ClientSession::GetModel(int id) const -> Model* { - if (id < 0 || id >= static_cast(models_.size())) { - throw Exception("Invalid model (out of range)"); +auto ClientSession::GetMesh(int id) const -> SceneMesh* { + if (id < 0 || id >= static_cast(meshes_.size())) { + throw Exception("Invalid mesh (out of range)"); } - Model* n = models_[id].get(); + SceneMesh* n = meshes_[id].Get(); if (!n) { - throw Exception("Invalid model id (empty slot)"); + throw Exception("Invalid mesh id (empty slot)"); } return n; } -auto ClientSession::GetSound(int id) const -> Sound* { +auto ClientSession::GetSound(int id) const -> SceneSound* { if (id < 0 || id >= static_cast(sounds_.size())) { throw Exception("Invalid sound (out of range)"); } - Sound* n = sounds_[id].get(); + SceneSound* n = sounds_[id].Get(); if (!n) { throw Exception("Invalid sound id (empty slot)"); } return n; } -auto ClientSession::GetCollideModel(int id) const -> CollideModel* { - if (id < 0 || id >= static_cast(collide_models_.size())) { - throw Exception("Invalid collide_model (out of range)"); +auto ClientSession::GetCollisionMesh(int id) const -> SceneCollisionMesh* { + if (id < 0 || id >= static_cast(collision_meshes_.size())) { + throw Exception("Invalid collision_mesh (out of range)"); } - CollideModel* n = collide_models_[id].get(); + SceneCollisionMesh* n = collision_meshes_[id].Get(); if (!n) { - throw Exception("Invalid collide_model id (empty slot)"); + throw Exception("Invalid collision_mesh id (empty slot)"); } return n; } @@ -962,11 +967,12 @@ void ClientSession::Error(const std::string& description) { void ClientSession::End() { if (shutting_down_) return; shutting_down_ = true; - g_python->PushObjCall(Python::ObjID::kLaunchMainMenuSessionCall); + g_scene_v1->python->objs().PushCall( + SceneV1Python::ObjID::kLaunchMainMenuSessionCall); } void ClientSession::HandleSessionMessage(const std::vector& buffer) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); BA_PRECONDITION(!buffer.empty()); @@ -998,7 +1004,7 @@ void ClientSession::HandleSessionMessage(const std::vector& buffer) { // let's also use this opportunity to graph our command-buffer size // for network debugging... if (NetGraph *graph = // g_graphics->GetClientSessionStepBufferGraph()) { - // graph->addSample(GetRealTime(), steps_on_list_); + // graph->addSample(GetAppTimeMillisecs(), steps_on_list_); // } break; @@ -1049,13 +1055,15 @@ void ClientSession::AddCommand(const std::vector& command) { } } -auto ClientSession::GetForegroundContext() -> Context { return Context(this); } +auto ClientSession::GetForegroundContext() -> base::ContextRef { + return base::ContextRef(this); +} void ClientSession::GetCorrectionMessages( bool blend, std::vector >* messages) { std::vector message; for (auto&& i : scenes_) { - if (Scene* sg = i.get()) { + if (Scene* sg = i.Get()) { message = sg->GetCorrectionMessage(blend); // A correction packet of size 4 is empty; ignore it. if (message.size() > 4) { @@ -1065,10 +1073,10 @@ void ClientSession::GetCorrectionMessages( } } -void ClientSession::DumpFullState(SceneStream* out) { +void ClientSession::DumpFullState(SessionStream* out) { // Add all scenes. for (auto&& i : scenes()) { - if (Scene* sg = i.get()) { + if (Scene* sg = i.Get()) { sg->Dump(out); } } @@ -1077,36 +1085,36 @@ void ClientSession::DumpFullState(SceneStream* out) { // (but *not* their components, which may reference the nodes that we haven't // made yet) for (auto&& i : materials()) { - if (Material* m = i.get()) { + if (Material* m = i.Get()) { out->AddMaterial(m); } } // Add all media. for (auto&& i : textures()) { - if (Texture* t = i.get()) { + if (SceneTexture* t = i.Get()) { out->AddTexture(t); } } - for (auto&& i : models()) { - if (Model* s = i.get()) { - out->AddModel(s); + for (auto&& i : meshes()) { + if (SceneMesh* s = i.Get()) { + out->AddMesh(s); } } for (auto&& i : sounds()) { - if (Sound* s = i.get()) { + if (SceneSound* s = i.Get()) { out->AddSound(s); } } - for (auto&& i : collide_models()) { - if (CollideModel* s = i.get()) { - out->AddCollideModel(s); + for (auto&& i : collision_meshes()) { + if (SceneCollisionMesh* s = i.Get()) { + out->AddCollisionMesh(s); } } // Add all scene nodes. for (auto&& i : scenes()) { - if (Scene* sg = i.get()) { + if (Scene* sg = i.Get()) { sg->DumpNodes(out); } } @@ -1114,10 +1122,10 @@ void ClientSession::DumpFullState(SceneStream* out) { // Now fill out materials since we know all the nodes/etc. that they // refer to exist. for (auto&& i : materials()) { - if (Material* m = i.get()) { + if (Material* m = i.Get()) { m->DumpComponents(out); } } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/support/client_session.h b/src/ballistica/scene_v1/support/client_session.h new file mode 100644 index 00000000..990e8138 --- /dev/null +++ b/src/ballistica/scene_v1/support/client_session.h @@ -0,0 +1,140 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_SUPPORT_CLIENT_SESSION_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_CLIENT_SESSION_H_ + +#include +#include +#include + +#include "ballistica/scene_v1/support/client_controller_interface.h" +#include "ballistica/scene_v1/support/session.h" + +namespace ballistica::scene_v1 { + +class ClientSession : public Session { + public: + ClientSession(); + ~ClientSession() override; + + // Allows for things like replay speed. + virtual auto GetActualTimeAdvanceMillisecs(double base_advance_millisecs) + -> double { + return base_advance_millisecs; + } + void Update(int time_advance_millisecs, double time_advance) override; + void Draw(base::FrameDef* f) override; + virtual void HandleSessionMessage(const std::vector& buffer); + void Reset(bool rewind); + auto GetForegroundContext() -> base::ContextRef override; + auto DoesFillScreen() const -> bool override; + void ScreenSizeChanged() override; + void LanguageChanged() override; + void GetCorrectionMessages(bool blend, + std::vector >* messages); + + /// Called when attempting to step without input data available. + virtual void OnCommandBufferUnderrun() {} + + virtual void OnBaseTimeStepAdded(int step) {} + + // Returns existing objects; throws exceptions if not available. + auto GetScene(int id) const -> Scene*; + auto GetNode(int id) const -> Node*; + auto GetTexture(int id) const -> SceneTexture*; + auto GetMesh(int id) const -> SceneMesh*; + auto GetCollisionMesh(int id) const -> SceneCollisionMesh*; + auto GetMaterial(int id) const -> Material*; + auto GetSound(int id) const -> SceneSound*; + + auto base_time_buffered() const { return base_time_buffered_; } + auto consume_rate() const { return consume_rate_; } + auto set_consume_rate(float val) { consume_rate_ = val; } + auto target_base_time() const { return target_base_time_millisecs_; } + auto base_time() const { return base_time_millisecs_; } + auto shutting_down() const { return shutting_down_; } + + auto scenes() const -> const std::vector >& { + return scenes_; + } + auto nodes() const -> const std::vector >& { + return nodes_; + } + auto textures() const -> const std::vector >& { + return textures_; + } + auto meshes() const -> const std::vector >& { + return meshes_; + } + auto sounds() const -> const std::vector >& { + return sounds_; + } + auto collision_meshes() const + -> const std::vector >& { + return collision_meshes_; + } + auto materials() const -> const std::vector >& { + return materials_; + } + auto commands() const -> const std::list >& { + return commands_; + } + auto add_end_of_file_command() { + commands_.emplace_back(1, static_cast(SessionCommand::kEndOfFile)); + } + virtual void OnReset(bool rewind); + virtual void FetchMessages() {} + virtual void Error(const std::string& description); + void End(); + void DumpFullState(SessionStream* out) override; + + /// Reset target base time to equal current. This can be used during command + /// buffer underruns to cause playback to pause momentarily instead of + /// skipping ahead to catch up. Generally desired for replays but not for + /// net-play. + void ResetTargetBaseTime() { + target_base_time_millisecs_ = base_time_millisecs_; + } + + private: + void ClearSessionObjs(); + void AddCommand(const std::vector& command); + + auto ReadByte() -> uint8_t; + auto ReadInt32() -> int32_t; + void ReadInt32_2(int32_t* vals); + void ReadInt32_3(int32_t* vals); + void ReadInt32_4(int32_t* vals); + auto ReadString() -> std::string; + auto ReadFloat() -> float; + void ReadFloats(int count, float* vals); + void ReadInt32s(int count, int32_t* vals); + void ReadChars(int count, char* vals); + + // Ready-to-go commands. + std::list > commands_; + + // Commands being built up for the next time step (we need to ship timesteps + // as a whole). + std::list > commands_pending_; + std::vector current_cmd_; + uint8_t* current_cmd_ptr_{}; + int base_time_buffered_{}; + bool shutting_down_{}; + + millisecs_t base_time_millisecs_{}; + double target_base_time_millisecs_{}; + float consume_rate_{1.0f}; + + std::vector > scenes_; + std::vector > nodes_; + std::vector > textures_; + std::vector > meshes_; + std::vector > sounds_; + std::vector > collision_meshes_; + std::vector > materials_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_SUPPORT_CLIENT_SESSION_H_ diff --git a/src/ballistica/logic/session/net_client_session.cc b/src/ballistica/scene_v1/support/client_session_net.cc similarity index 67% rename from src/ballistica/logic/session/net_client_session.cc rename to src/ballistica/scene_v1/support/client_session_net.cc index a1f6b7b0..a8c40c4b 100644 --- a/src/ballistica/logic/session/net_client_session.cc +++ b/src/ballistica/scene_v1/support/client_session_net.cc @@ -1,77 +1,80 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/logic/session/net_client_session.h" +#include "ballistica/scene_v1/support/client_session_net.h" -#include "ballistica/app/app.h" -#include "ballistica/assets/assets_server.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/graphics/net_graph.h" -#include "ballistica/logic/connection/connection_to_host.h" +#include "ballistica/base/assets/assets_server.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/graphics/support/net_graph.h" +#include "ballistica/scene_v1/connection/connection_to_host.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" -namespace ballistica { +namespace ballistica::scene_v1 { -NetClientSession::NetClientSession() { +ClientSessionNet::ClientSessionNet() { // Sanity check: we should only ever be writing one replay at once. - if (g_app->replay_open) { + if (g_core->replay_open) { Log(LogLevel::kError, "g_replay_open true at netclient start; shouldn't happen."); } - assert(g_assets_server); - g_assets_server->PushBeginWriteReplayCall(); + assert(g_base->assets_server); + g_base->assets_server->PushBeginWriteReplayCall(); writing_replay_ = true; - g_app->replay_open = true; + g_core->replay_open = true; } -NetClientSession::~NetClientSession() { +ClientSessionNet::~ClientSessionNet() { if (writing_replay_) { // Sanity check: we should only ever be writing one replay at once. - if (!g_app->replay_open) { + if (!g_core->replay_open) { Log(LogLevel::kError, "g_replay_open false at net-client close; shouldn't happen."); } - g_app->replay_open = false; - assert(g_assets_server); - g_assets_server->PushEndWriteReplayCall(); + g_core->replay_open = false; + assert(g_base->assets_server); + g_base->assets_server->PushEndWriteReplayCall(); writing_replay_ = false; } } -void NetClientSession::SetConnectionToHost(ConnectionToHost* c) { +void ClientSessionNet::SetConnectionToHost(ConnectionToHost* c) { connection_to_host_ = c; } -void NetClientSession::OnCommandBufferUnderrun() { +void ClientSessionNet::OnCommandBufferUnderrun() { // We currently don't do anything here; we want to just power // through hitches and keep aiming for our target time. // (though perhaps we could take note here for analytics purposes). - // printf("Underrun at %d\n", GetRealTime()); + // printf("Underrun at %d\n", GetAppTimeMillisecs()); // fflush(stdout); } -void NetClientSession::Update(int time_advance) { +void ClientSessionNet::Update(int time_advance_millisecs, double time_advance) { if (shutting_down()) { return; } // Now do standard step. - ClientSession::Update(time_advance); + ClientSession::Update(time_advance_millisecs, time_advance); // And update our timing to try and ensure we don't run out of buffer. UpdateBuffering(); } -auto NetClientSession::GetBucketNum() -> int { - return (delay_sample_counter_ / g_app->delay_bucket_samples) +auto ClientSessionNet::GetBucketNum() -> int { + auto* appmode = scene_v1::SceneV1AppMode::GetSingleton(); + return (delay_sample_counter_ / appmode->delay_bucket_samples()) % static_cast(buckets_.size()); } -auto NetClientSession::UpdateBuffering() -> void { +void ClientSessionNet::UpdateBuffering() { + auto* appmode = scene_v1::SceneV1AppMode::GetSingleton(); // Keep record of the most and least amount of time we've had buffered // recently, and slow down/speed up a bit based on that. { // Change bucket every `g_delay_samples` samples. int bucketnum{GetBucketNum()}; - int bucket_iteration = delay_sample_counter_ % g_app->delay_bucket_samples; + int bucket_iteration = + delay_sample_counter_ % appmode->delay_bucket_samples(); delay_sample_counter_++; SampleBucket& bucket{buckets_[bucketnum]}; if (bucket_iteration == 0) { @@ -80,7 +83,7 @@ auto NetClientSession::UpdateBuffering() -> void { // After the last sample in each bucket, update our smoothed values with // the full sample set in the bucket. - if (bucket_iteration == g_app->delay_bucket_samples - 1) { + if (bucket_iteration == appmode->delay_bucket_samples() - 1) { float smoothing = 0.7f; last_bucket_max_delay_ = static_cast(bucket.max_delay_from_projection); @@ -89,7 +92,7 @@ auto NetClientSession::UpdateBuffering() -> void { + (1.0f - smoothing) * static_cast(bucket.max_delay_from_projection); } - auto now = GetRealTime(); + auto now = g_core->GetAppTimeMillisecs(); // We want target-base-time to wind up at our projected time minus some // safety offset to account for buffering fluctuations. @@ -112,31 +115,31 @@ auto NetClientSession::UpdateBuffering() -> void { std::max(0.5f, 1.0f + speed_change_aggression * to_ideal_offset)); set_consume_rate(new_consume_rate); - if (g_graphics->network_debug_info_display_enabled()) { - if (NetGraph* graph = - g_graphics->GetDebugGraph("1: packet delay", false)) { + if (g_base->graphics->network_debug_info_display_enabled()) { + if (auto* graph = + g_base->graphics->GetDebugGraph("1: packet delay", false)) { graph->AddSample(now, current_delay_); } - if (NetGraph* graph = - g_graphics->GetDebugGraph("2: max delay bucketed", false)) { + if (auto* graph = + g_base->graphics->GetDebugGraph("2: max delay bucketed", false)) { graph->AddSample(now, last_bucket_max_delay_); } - if (NetGraph* graph = - g_graphics->GetDebugGraph("3: filtered delay", false)) { + if (auto* graph = + g_base->graphics->GetDebugGraph("3: filtered delay", false)) { graph->AddSample(now, max_delay_smoothed_); } - if (NetGraph* graph = g_graphics->GetDebugGraph("4: run rate", false)) { + if (auto* graph = g_base->graphics->GetDebugGraph("4: run rate", false)) { graph->AddSample(now, new_consume_rate); } - if (NetGraph* graph = - g_graphics->GetDebugGraph("5: time buffered", true)) { + if (auto* graph = + g_base->graphics->GetDebugGraph("5: time buffered", true)) { graph->AddSample(now, base_time_buffered()); } } } } -auto NetClientSession::OnReset(bool rewind) -> void { +void ClientSessionNet::OnReset(bool rewind) { // Resets should never happen for us after we start, right?... base_time_received_ = 0; last_base_time_receive_time_ = 0; @@ -145,8 +148,8 @@ auto NetClientSession::OnReset(bool rewind) -> void { ClientSession::OnReset(rewind); } -auto NetClientSession::OnBaseTimeStepAdded(int step) -> void { - auto now = GetRealTime(); +void ClientSessionNet::OnBaseTimeStepAdded(int step) { + auto now = g_core->GetAppTimeMillisecs(); millisecs_t new_base_time_received = base_time_received_ + step; @@ -187,16 +190,16 @@ auto NetClientSession::OnBaseTimeStepAdded(int step) -> void { } } -void NetClientSession::HandleSessionMessage( +void ClientSessionNet::HandleSessionMessage( const std::vector& message) { // Do the standard thing, but also write this message straight to our replay // stream if we have one. ClientSession::HandleSessionMessage(message); if (writing_replay_) { - assert(g_assets_server); - g_assets_server->PushAddMessageToReplayCall(message); + assert(g_base->assets_server); + g_base->assets_server->PushAddMessageToReplayCall(message); } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/logic/session/net_client_session.h b/src/ballistica/scene_v1/support/client_session_net.h similarity index 57% rename from src/ballistica/logic/session/net_client_session.h rename to src/ballistica/scene_v1/support/client_session_net.h index c53e9b0f..a7c7051c 100644 --- a/src/ballistica/logic/session/net_client_session.h +++ b/src/ballistica/scene_v1/support/client_session_net.h @@ -1,29 +1,28 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_LOGIC_SESSION_NET_CLIENT_SESSION_H_ -#define BALLISTICA_LOGIC_SESSION_NET_CLIENT_SESSION_H_ +#ifndef BALLISTICA_SCENE_V1_SUPPORT_CLIENT_SESSION_NET_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_CLIENT_SESSION_NET_H_ #include -#include "ballistica/logic/session/client_session.h" +#include "ballistica/scene_v1/support/client_session.h" -namespace ballistica { +namespace ballistica::scene_v1 { // A client-session fed by a connection to a host. -class NetClientSession : public ClientSession { +class ClientSessionNet : public ClientSession { public: - NetClientSession(); - ~NetClientSession() override; + ClientSessionNet(); + ~ClientSessionNet() override; auto connection_to_host() const -> ConnectionToHost* { - return connection_to_host_.get(); + return connection_to_host_.Get(); } - auto SetConnectionToHost(ConnectionToHost* c) -> void; - auto HandleSessionMessage(const std::vector& buffer) - -> void override; - auto OnCommandBufferUnderrun() -> void override; - auto Update(int time_advance) -> void override; - auto OnReset(bool rewind) -> void override; - auto OnBaseTimeStepAdded(int step) -> void override; + void SetConnectionToHost(ConnectionToHost* c); + void HandleSessionMessage(const std::vector& buffer) override; + void OnCommandBufferUnderrun() override; + void Update(int time_advance_millisecs, double time_advance) override; + void OnReset(bool rewind) override; + void OnBaseTimeStepAdded(int step) override; private: struct SampleBucket { @@ -36,7 +35,7 @@ class NetClientSession : public ClientSession { return leading_base_time_received_ + (now - leading_base_time_receive_time_); } - auto UpdateBuffering() -> void; + void UpdateBuffering(); auto GetBucketNum() -> int; bool writing_replay_{}; @@ -57,6 +56,6 @@ class NetClientSession : public ClientSession { // int adjust_counter_{}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_LOGIC_SESSION_NET_CLIENT_SESSION_H_ +#endif // BALLISTICA_SCENE_V1_SUPPORT_CLIENT_SESSION_NET_H_ diff --git a/src/ballistica/logic/session/replay_client_session.cc b/src/ballistica/scene_v1/support/client_session_replay.cc similarity index 75% rename from src/ballistica/logic/session/replay_client_session.cc rename to src/ballistica/scene_v1/support/client_session_replay.cc index d422cb4d..2e6de6f5 100644 --- a/src/ballistica/logic/session/replay_client_session.cc +++ b/src/ballistica/scene_v1/support/client_session_replay.cc @@ -1,37 +1,42 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/logic/session/replay_client_session.h" +#include "ballistica/scene_v1/support/client_session_replay.h" -#include "ballistica/dynamics/material/material.h" -#include "ballistica/generic/huffman.h" -#include "ballistica/generic/utils.h" -#include "ballistica/logic/connection/connection_set.h" -#include "ballistica/logic/connection/connection_to_client.h" -#include "ballistica/math/vector3f.h" -#include "ballistica/networking/networking.h" -#include "ballistica/platform/platform.h" -#include "ballistica/scene/scene.h" -#include "ballistica/scene/scene_stream.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/networking/networking.h" +#include "ballistica/base/support/huffman.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/scene_v1/connection/connection_set.h" +#include "ballistica/scene_v1/connection/connection_to_client.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/scene_v1/support/session_stream.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/math/vector3f.h" -namespace ballistica { +namespace ballistica::scene_v1 { -auto ReplayClientSession::GetActualTimeAdvance(int advance_in) -> int { - return static_cast( - round(advance_in * pow(2.0f, g_logic->replay_speed_exponent()))); +auto ClientSessionReplay::GetActualTimeAdvanceMillisecs( + double base_advance_millisecs) -> double { + auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + return base_advance_millisecs * pow(2.0f, appmode->replay_speed_exponent()); } -ReplayClientSession::ReplayClientSession(std::string filename) +ClientSessionReplay::ClientSessionReplay(std::string filename) : file_name_(std::move(filename)) { + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + // take responsibility for feeding all clients to this device.. - g_logic->connections()->RegisterClientController(this); + appmode->connections()->RegisterClientController(this); // go ahead and just do a reset here, which will get things going.. Reset(true); } -ReplayClientSession::~ReplayClientSession() { +ClientSessionReplay::~ClientSessionReplay() { + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + // we no longer are responsible for feeding clients to this device.. - g_logic->connections()->UnregisterClientController(this); + appmode->connections()->UnregisterClientController(this); if (file_) { fclose(file_); @@ -39,9 +44,9 @@ ReplayClientSession::~ReplayClientSession() { } } -void ReplayClientSession::OnCommandBufferUnderrun() { ResetTargetBaseTime(); } +void ClientSessionReplay::OnCommandBufferUnderrun() { ResetTargetBaseTime(); } -void ReplayClientSession::OnClientConnected(ConnectionToClient* c) { +void ClientSessionReplay::OnClientConnected(ConnectionToClient* c) { // sanity check - abort if its on either of our lists already for (ConnectionToClient* i : connections_to_clients_) { if (i == c) { @@ -69,7 +74,7 @@ void ReplayClientSession::OnClientConnected(ConnectionToClient* c) { // we create a temporary output stream just for the purpose of building // a giant session-commands message that we can send to the client // to build its state up to where we are currently. - SceneStream out(nullptr, false); + SessionStream out(nullptr, false); // go ahead and dump our full state.. DumpFullState(&out); @@ -99,7 +104,7 @@ void ReplayClientSession::OnClientConnected(ConnectionToClient* c) { } } -void ReplayClientSession::OnClientDisconnected(ConnectionToClient* c) { +void ClientSessionReplay::OnClientDisconnected(ConnectionToClient* c) { // Search for it on either our ignored or regular lists. for (auto i = connections_to_clients_.begin(); i != connections_to_clients_.end(); i++) { @@ -120,7 +125,7 @@ void ReplayClientSession::OnClientDisconnected(ConnectionToClient* c) { " called for connection not on lists"); } -void ReplayClientSession::FetchMessages() { +void ClientSessionReplay::FetchMessages() { if (!file_ || shutting_down()) { return; } @@ -150,8 +155,8 @@ void ReplayClientSession::FetchMessages() { if (len8 == 254) { uint16_t len16; if (fread(&len16, 2, 1, file_) != 1) { - // so they know to be done when they reach the end of the command list - // (instead of just waiting for more commands) + // so they know to be done when they reach the end of the command + // list (instead of just waiting for more commands) add_end_of_file_command(); fclose(file_); file_ = nullptr; @@ -162,8 +167,8 @@ void ReplayClientSession::FetchMessages() { } else { // Pull 32 bit len. if (fread(&len32, 4, 1, file_) != 1) { - // so they know to be done when they reach the end of the command list - // (instead of just waiting for more commands) + // so they know to be done when they reach the end of the command + // list (instead of just waiting for more commands) add_end_of_file_command(); fclose(file_); file_ = nullptr; @@ -183,7 +188,7 @@ void ReplayClientSession::FetchMessages() { return; } std::vector data_decompressed = - g_utils->huffman()->decompress(buffer); + g_base->huffman->decompress(buffer); HandleSessionMessage(data_decompressed); // Also send it to all client-connections we're attached to. @@ -198,10 +203,11 @@ void ReplayClientSession::FetchMessages() { } } -void ReplayClientSession::Error(const std::string& description) { +void ClientSessionReplay::Error(const std::string& description) { // Close the replay, announce something went wrong with it, and then do // standard error response.. - ScreenMessage(g_logic->GetResourceString("replayReadErrorText"), {1, 0, 0}); + ScreenMessage(g_base->assets->GetResourceString("replayReadErrorText"), + {1, 0, 0}); if (file_) { fclose(file_); file_ = nullptr; @@ -209,7 +215,7 @@ void ReplayClientSession::Error(const std::string& description) { ClientSession::Error(description); } -void ReplayClientSession::OnReset(bool rewind) { +void ClientSessionReplay::OnReset(bool rewind) { // Handles base resetting. ClientSession::OnReset(rewind); @@ -225,7 +231,7 @@ void ReplayClientSession::OnReset(bool rewind) { file_ = nullptr; } - file_ = g_platform->FOpen(file_name_.c_str(), "rb"); + file_ = g_core->platform->FOpen(file_name_.c_str(), "rb"); if (!file_) { Error("can't open file for reading"); return; @@ -249,7 +255,7 @@ void ReplayClientSession::OnReset(bool rewind) { return; } if (version > kProtocolVersion || version < kProtocolVersionMin) { - ScreenMessage(g_logic->GetResourceString("replayVersionErrorText"), + ScreenMessage(g_base->assets->GetResourceString("replayVersionErrorText"), {1, 0, 0}); End(); return; @@ -257,4 +263,4 @@ void ReplayClientSession::OnReset(bool rewind) { } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/support/client_session_replay.h b/src/ballistica/scene_v1/support/client_session_replay.h new file mode 100644 index 00000000..4bd82e82 --- /dev/null +++ b/src/ballistica/scene_v1/support/client_session_replay.h @@ -0,0 +1,43 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_SUPPORT_CLIENT_SESSION_REPLAY_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_CLIENT_SESSION_REPLAY_H_ + +#include +#include + +#include "ballistica/scene_v1/support/client_controller_interface.h" +#include "ballistica/scene_v1/support/client_session.h" + +namespace ballistica::scene_v1 { + +// A client-session fed by a replay file. +class ClientSessionReplay : public ClientSession, + public ClientControllerInterface { + public: + explicit ClientSessionReplay(std::string filename); + ~ClientSessionReplay() override; + void OnReset(bool rewind) override; + + // Our ClientControllerInterface implementation. + auto GetActualTimeAdvanceMillisecs(double base_advance_millisecs) + -> double override; + void OnClientConnected(ConnectionToClient* c) override; + void OnClientDisconnected(ConnectionToClient* c) override; + void OnCommandBufferUnderrun() override; + + void Error(const std::string& description) override; + void FetchMessages() override; + + private: + uint32_t message_fetch_num_{}; + bool have_sent_client_message_{}; + std::vector connections_to_clients_; + std::vector connections_to_clients_ignored_; + std::string file_name_; + FILE* file_{}; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_SUPPORT_CLIENT_SESSION_REPLAY_H_ diff --git a/src/ballistica/logic/host_activity.cc b/src/ballistica/scene_v1/support/host_activity.cc similarity index 54% rename from src/ballistica/logic/host_activity.cc rename to src/ballistica/scene_v1/support/host_activity.cc index be77634f..b47b037d 100644 --- a/src/ballistica/logic/host_activity.cc +++ b/src/ballistica/scene_v1/support/host_activity.cc @@ -1,43 +1,37 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/logic/host_activity.h" +#include "ballistica/scene_v1/support/host_activity.h" -#include "ballistica/assets/component/collide_model.h" -#include "ballistica/assets/component/data.h" -#include "ballistica/assets/component/model.h" -#include "ballistica/assets/component/sound.h" -#include "ballistica/assets/component/texture.h" -#include "ballistica/dynamics/material/material.h" -#include "ballistica/generic/lambda_runnable.h" -#include "ballistica/generic/timer.h" -#include "ballistica/input/device/input_device.h" -#include "ballistica/logic/player.h" -#include "ballistica/logic/session/host_session.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_context_call.h" -#include "ballistica/python/python_sys.h" -#include "ballistica/scene/node/globals_node.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene_stream.h" +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/scene_v1/assets/scene_collision_mesh.h" +#include "ballistica/scene_v1/assets/scene_data_asset.h" +#include "ballistica/scene_v1/assets/scene_mesh.h" +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/assets/scene_texture.h" +#include "ballistica/scene_v1/dynamics/material/material.h" +#include "ballistica/scene_v1/node/globals_node.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/scene_v1/support/host_session.h" +#include "ballistica/scene_v1/support/player.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/scene_v1/support/session_stream.h" +#include "ballistica/shared/generic/lambda_runnable.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python_sys.h" -namespace ballistica { +namespace ballistica::scene_v1 { HostActivity::HostActivity(HostSession* host_session) { // Store a link to the HostSession and add ourself to it. host_session_ = host_session; - // Create our game timer - gets called whenever game should step. - step_scene_timer_ = - base_timers_.NewTimer(base_time_, kGameStepMilliseconds, 0, -1, - NewLambdaRunnable([this] { StepScene(); })); - SetGameSpeed(1.0f); { - ScopedSetContext cp(this); // So scene picks us up as context. + base::ScopedSetContext ssc(this); // So scene picks us up as context. scene_ = Object::New(0); // If there's an output stream, add to it. - if (SceneStream* out = host_session->GetSceneStream()) { - out->AddScene(scene_.get()); + if (SessionStream* out = host_session->GetSceneStream()) { + out->AddScene(scene_.Get()); } } } @@ -49,60 +43,68 @@ HostActivity::~HostActivity() { // (this generates warnings, suppresses messages, etc) scene_->set_shutting_down(true); - // Clear out all python calls registered in our context. + // Clear out all Python calls registered in our context. // (should wipe out refs to our activity and prevent them from running without // a valid activity context) - for (auto&& i : python_calls_) { - if (i.exists()) { + for (auto&& i : context_calls_) { + if (i.Exists()) { i->MarkDead(); } } // Mark all our media dead to clear it out of our output-stream cleanly for (auto&& i : textures_) { - if (i.second.exists()) { + if (i.second.Exists()) { i.second->MarkDead(); } } - for (auto&& i : models_) { - if (i.second.exists()) { + for (auto&& i : meshes_) { + if (i.second.Exists()) { i.second->MarkDead(); } } for (auto&& i : sounds_) { - if (i.second.exists()) { + if (i.second.Exists()) { i.second->MarkDead(); } } - for (auto&& i : collide_models_) { - if (i.second.exists()) { + for (auto&& i : collision_meshes_) { + if (i.second.Exists()) { i.second->MarkDead(); } } for (auto&& i : materials_) { - if (i.exists()) { + if (i.Exists()) { i->MarkDead(); } } + // If the host-session is outliving us, kill all the base-timers we created + // in it. + if (auto* host_session = host_session_.Get()) { + for (auto timer_id : session_base_timer_ids_) { + // printf("WOULD KILL BASE TIMER %d\n", timer_id); + host_session->DeleteTimer(TimeType::kBase, timer_id); + } + } // Clear our timers and scene; this should wipe out any remaining refs to our - // python activity, allowing it to die. - base_timers_.Clear(); - sim_timers_.Clear(); + // Python activity, allowing it to die. + // base_timers_.Clear(); + scene_timers_.Clear(); scene_.Clear(); // Report outstanding calls. There shouldn't be any at this point. Actually it // turns out there's generally 1; whichever call was responsible for killing - // this activity will still be in progress.. so let's report on 2 or more I + // this activity will still be in progress. So let's report on 2 or more I // guess. if (g_buildconfig.debug_build()) { - PruneDeadRefs(&python_calls_); - if (python_calls_.size() > 1) { - std::string s = std::to_string(python_calls_.size()) + PruneDeadRefs(&context_calls_); + if (context_calls_.size() > 1) { + std::string s = std::to_string(context_calls_.size()) + " live PythonContextCalls at shutdown for " + "HostActivity" + " (1 call is expected):"; int count = 1; - for (auto& python_call : python_calls_) + for (auto& python_call : context_calls_) s += "\n " + std::to_string(count++) + ": " + (*python_call).GetObjectDescription(); Log(LogLevel::kWarning, s); @@ -110,34 +112,32 @@ HostActivity::~HostActivity() { } } -auto HostActivity::GetSceneStream() const -> SceneStream* { - if (!host_session_.exists()) return nullptr; +auto HostActivity::GetSceneStream() const -> SessionStream* { + if (!host_session_.Exists()) return nullptr; return host_session_->GetSceneStream(); } -auto HostActivity::SetGlobalsNode(GlobalsNode* node) -> void { - globals_node_ = node; -} +void HostActivity::SetGlobalsNode(GlobalsNode* node) { globals_node_ = node; } void HostActivity::StepScene() { int cycle_count = 1; - if (host_session_->benchmark_type() == BenchmarkType::kCPU) { + if (host_session_->benchmark_type() == base::BenchmarkType::kCPU) { cycle_count = 100; } for (int cycle = 0; cycle < cycle_count; ++cycle) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // Clear our player-positions for this step. // FIXME: Move this to scene and/or player node. - assert(host_session_.exists()); + assert(host_session_.Exists()); for (auto&& player : host_session_->players()) { - assert(player.exists()); + assert(player.Exists()); player->set_have_position(false); } // Run our sim-time timers. - sim_timers_.Run(scene()->time()); + scene_timers_.Run(scene()->time()); // Send die-messages/etc to out-of-bounds stuff. HandleOutOfBoundsNodes(); @@ -146,9 +146,9 @@ void HostActivity::StepScene() { } } -void HostActivity::RegisterCall(PythonContextCall* call) { +void HostActivity::RegisterContextCall(base::PythonContextCall* call) { assert(call); - python_calls_.emplace_back(call); + context_calls_.emplace_back(call); // If we're shutting down, just kill the call immediately. // (we turn all of our calls to no-ops as we shut down) @@ -161,10 +161,27 @@ void HostActivity::RegisterCall(PythonContextCall* call) { } void HostActivity::start() { - if (_started) { - Log(LogLevel::kError, "Start called twice for activity."); + if (started_) { + Log(LogLevel::kError, "HostActivity::Start() called twice."); + return; } - _started = true; + started_ = true; + if (shutting_down_) { + Log(LogLevel::kError, + "HostActivity::Start() called for shutting-down activity."); + return; + } + auto* host_session = host_session_.Get(); + if (!host_session) { + Log(LogLevel::kError, "HostActivity::Start() called with dead session."); + return; + } + // Create our step timer - gets called whenever scene should step. + step_scene_timer_id_ = + host_session->NewTimer(TimeType::kBase, kGameStepMilliseconds, true, + NewLambdaRunnable([this] { StepScene(); })); + session_base_timer_ids_.push_back(step_scene_timer_id_); + SetGameSpeed(1.0f); } auto HostActivity::GetAsHostActivity() -> HostActivity* { return this; } @@ -180,40 +197,43 @@ auto HostActivity::NewMaterial(const std::string& name) return m; } -auto HostActivity::GetTexture(const std::string& name) -> Object::Ref { +auto HostActivity::GetTexture(const std::string& name) + -> Object::Ref { if (shutting_down_) { throw Exception("can't load assets during activity shutdown"); } - return Assets::GetAsset(&textures_, name, scene()); + return GetAsset(&textures_, name, scene()); } -auto HostActivity::GetSound(const std::string& name) -> Object::Ref { +auto HostActivity::GetSound(const std::string& name) + -> Object::Ref { if (shutting_down_) { throw Exception("can't load assets during activity shutdown"); } - return Assets::GetAsset(&sounds_, name, scene()); + return GetAsset(&sounds_, name, scene()); } -auto HostActivity::GetData(const std::string& name) -> Object::Ref { +auto HostActivity::GetData(const std::string& name) + -> Object::Ref { if (shutting_down_) { throw Exception("can't load assets during activity shutdown"); } - return Assets::GetAsset(&datas_, name, scene()); + return GetAsset(&datas_, name, scene()); } -auto HostActivity::GetModel(const std::string& name) -> Object::Ref { +auto HostActivity::GetMesh(const std::string& name) -> Object::Ref { if (shutting_down_) { throw Exception("can't load assets during activity shutdown"); } - return Assets::GetAsset(&models_, name, scene()); + return GetAsset(&meshes_, name, scene()); } -auto HostActivity::GetCollideModel(const std::string& name) - -> Object::Ref { +auto HostActivity::GetCollisionMesh(const std::string& name) + -> Object::Ref { if (shutting_down_) { throw Exception("can't load assets during activity shutdown"); } - return Assets::GetAsset(&collide_models_, name, scene()); + return GetAsset(&collision_meshes_, name, scene()); } void HostActivity::SetPaused(bool val) { @@ -225,6 +245,9 @@ void HostActivity::SetPaused(bool val) { } void HostActivity::SetGameSpeed(float speed) { + if (!started_) { + return; + } if (speed == game_speed_) { return; } @@ -234,14 +257,22 @@ void HostActivity::SetGameSpeed(float speed) { } void HostActivity::UpdateStepTimerLength() { + if (!started_) { + return; + } + auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + auto* host_session = host_session_.Get(); + if (!host_session) { + return; + } if (game_speed_ == 0.0f || paused_) { - step_scene_timer_->SetLength(-1, true, base_time_); + host_session->SetBaseTimerLength(step_scene_timer_id_, -1); } else { - step_scene_timer_->SetLength( + host_session->SetBaseTimerLength( + step_scene_timer_id_, std::max(1, static_cast( round(static_cast(kGameStepMilliseconds) - / (game_speed_ * g_logic->debug_speed_mult())))), - true, base_time_); + / (game_speed_ * appmode->debug_speed_mult()))))); } } @@ -260,7 +291,7 @@ void HostActivity::HandleOutOfBoundsNodes() { int j = 0; for (auto&& i : scene()->out_of_bounds_nodes()) { j++; - Node* n = i.get(); + Node* n = i.Get(); if (n) { std::string dstr; PyObject* delegate = n->GetDelegate(); @@ -269,7 +300,7 @@ void HostActivity::HandleOutOfBoundsNodes() { } Log(LogLevel::kWarning, " node #" + std::to_string(j) + ": type='" + n->type()->name() - + "' addr=" + Utils::PtrToString(i.get()) + " name='" + + "' addr=" + Utils::PtrToString(i.Get()) + " name='" + n->label() + "' delegate=" + dstr); } } @@ -278,7 +309,7 @@ void HostActivity::HandleOutOfBoundsNodes() { // Send out-of-bounds messages to newly out-of-bounds nodes. for (auto&& i : scene()->out_of_bounds_nodes()) { - Node* n = i.get(); + Node* n = i.Get(); if (n) { n->DispatchOutOfBoundsMessage(); } @@ -287,24 +318,26 @@ void HostActivity::HandleOutOfBoundsNodes() { void HostActivity::RegisterPyActivity(PyObject* pyActivityObj) { assert(pyActivityObj && pyActivityObj != Py_None); - assert(!py_activity_weak_ref_.exists()); + assert(!py_activity_weak_ref_.Exists()); // Store a python weak-ref to this activity. py_activity_weak_ref_.Steal(PyWeakref_NewRef(pyActivityObj, nullptr)); } auto HostActivity::GetPyActivity() const -> PyObject* { - PyObject* obj = py_activity_weak_ref_.get(); - if (!obj) return Py_None; + PyObject* obj = py_activity_weak_ref_.Get(); + if (!obj) { + return Py_None; + } return PyWeakref_GetObject(obj); } auto HostActivity::GetHostSession() -> HostSession* { - return host_session_.get(); + return host_session_.Get(); } auto HostActivity::GetMutableScene() -> Scene* { - Scene* sg = scene_.get(); + Scene* sg = scene_.Get(); assert(sg); return sg; } @@ -314,17 +347,20 @@ void HostActivity::SetIsForeground(bool val) { Scene* sg = scene(); if (val && sg) { // Set it locally. - g_logic->SetForegroundScene(sg); + + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + appmode->SetForegroundScene(sg); + } // Also push it to clients. - if (SceneStream* out = GetSceneStream()) { - out->SetForegroundScene(scene_.get()); + if (SessionStream* out = GetSceneStream()) { + out->SetForegroundScene(scene_.Get()); } } } auto HostActivity::globals_node() const -> GlobalsNode* { - return globals_node_.get(); + return globals_node_.Get(); } auto HostActivity::NewSimTimer(millisecs_t length, bool repeat, @@ -343,8 +379,8 @@ auto HostActivity::NewSimTimer(millisecs_t length, bool repeat, } int offset = 0; - Timer* t = sim_timers_.NewTimer(scene()->time(), length, offset, - repeat ? -1 : 0, runnable); + Timer* t = scene_timers_.NewTimer(scene()->time(), length, offset, + repeat ? -1 : 0, runnable); return t->id(); } @@ -361,84 +397,107 @@ auto HostActivity::NewBaseTimer(millisecs_t length, bool repeat, if (length < 0) { throw Exception("Timer length cannot be < 0"); } + auto* host_session = host_session_.Get(); + if (!host_session) { + BA_LOG_PYTHON_TRACE_ONCE( + "WARNING: Creating session-time timer in activity but host is dead."); + return 123; // dummy... + } - int offset = 0; - Timer* t = base_timers_.NewTimer(base_time_, length, offset, repeat ? -1 : 0, - runnable); - return t->id(); + int timer_id = + host_session->NewTimer(TimeType::kBase, length, repeat, runnable); + + session_base_timer_ids_.push_back(timer_id); + return timer_id; } void HostActivity::DeleteSimTimer(int timer_id) { - assert(InLogicThread()); - if (shutting_down_) return; - sim_timers_.DeleteTimer(timer_id); + assert(g_base->InLogicThread()); + if (shutting_down_) { + return; + } + scene_timers_.DeleteTimer(timer_id); } void HostActivity::DeleteBaseTimer(int timer_id) { - assert(InLogicThread()); - if (shutting_down_) return; - base_timers_.DeleteTimer(timer_id); + assert(g_base->InLogicThread()); + if (shutting_down_) { + return; + } + if (auto* host_session = host_session_.Get()) { + host_session->DeleteTimer(TimeType::kBase, timer_id); + } } -auto HostActivity::Update(millisecs_t time_advance) -> millisecs_t { - assert(InLogicThread()); - - // We can be killed at any time, so let's keep an eye out for that. - WeakRef test_ref(this); - assert(test_ref.exists()); +void HostActivity::StepDisplayTime(millisecs_t time_advance) { + assert(g_base->InLogicThread()); // If we haven't been told to start yet, don't do anything more. - if (!_started) { - return 100; + if (!started_) { + return; } - // Advance base time by the specified amount, stopping at all timers along the - // way. - millisecs_t target_base_time = base_time_ + time_advance; - while (!base_timers_.empty() - && (base_time_ + base_timers_.GetTimeToNextExpire(base_time_) - <= target_base_time)) { - base_time_ += base_timers_.GetTimeToNextExpire(base_time_); - base_timers_.Run(base_time_); - if (!test_ref.exists()) { - return 1000; // The last timer run might have killed us. - } - } - base_time_ = target_base_time; + base_time_ += time_advance; // Periodically prune various dead refs. if (base_time_ > next_prune_time_) { PruneDeadMapRefs(&textures_); PruneDeadMapRefs(&sounds_); - PruneDeadMapRefs(&collide_models_); - PruneDeadMapRefs(&models_); + PruneDeadMapRefs(&collision_meshes_); + PruneDeadMapRefs(&meshes_); PruneDeadRefs(&materials_); - PruneDeadRefs(&python_calls_); - next_prune_time_ = base_time_ + 5000; + PruneDeadRefs(&context_calls_); + PruneSessionBaseTimers(); + next_prune_time_ = base_time_ + 5379; + } +} + +void HostActivity::PruneSessionBaseTimers() { + auto* host_session = host_session_.Get(); + if (!host_session) { + return; } - // Return the time until the next timer goes off. - return base_timers_.empty() ? 1000 - : base_timers_.GetTimeToNextExpire(base_time_); + // Quick-out; if all timers still exist, do nothing. This will usually + // be the case. + bool found_dead{}; + for (auto timer_id : session_base_timer_ids_) { + if (!host_session->BaseTimerExists(timer_id)) { + found_dead = true; + break; + } + } + if (!found_dead) { + return; + } + + // Ok, something died. Rebuild the list. + std::vector remaining_timer_ids; + for (auto timer_id : session_base_timer_ids_) { + if (host_session->BaseTimerExists(timer_id)) { + remaining_timer_ids.push_back(timer_id); + } + } + remaining_timer_ids.swap(session_base_timer_ids_); } void HostActivity::ScreenSizeChanged() { scene()->ScreenSizeChanged(); } void HostActivity::LanguageChanged() { scene()->LanguageChanged(); } void HostActivity::DebugSpeedMultChanged() { UpdateStepTimerLength(); } -void HostActivity::GraphicsQualityChanged(GraphicsQuality q) { +void HostActivity::GraphicsQualityChanged(base::GraphicsQuality q) { scene()->GraphicsQualityChanged(q); } -void HostActivity::Draw(FrameDef* frame_def) { - if (!_started) { +void HostActivity::Draw(base::FrameDef* frame_def) { + if (!started_) { return; } scene()->Draw(frame_def); } -void HostActivity::DumpFullState(SceneStream* out) { +void HostActivity::DumpFullState(SessionStream* out) { // Add our scene. - if (scene_.exists()) { + if (scene_.Exists()) { scene_->Dump(out); } @@ -446,42 +505,42 @@ void HostActivity::DumpFullState(SceneStream* out) { // (but *not* their components, which may reference the nodes that we haven't // made yet) for (auto&& i : materials_) { - if (Material* m = i.get()) { + if (Material* m = i.Get()) { out->AddMaterial(m); } } // Add our media. for (auto&& i : textures_) { - if (Texture* t = i.second.get()) { + if (SceneTexture* t = i.second.Get()) { out->AddTexture(t); } } for (auto&& i : sounds_) { - if (Sound* s = i.second.get()) { + if (SceneSound* s = i.second.Get()) { out->AddSound(s); } } - for (auto&& i : models_) { - if (Model* s = i.second.get()) { - out->AddModel(s); + for (auto&& i : meshes_) { + if (SceneMesh* s = i.second.Get()) { + out->AddMesh(s); } } - for (auto&& i : collide_models_) { - if (CollideModel* m = i.second.get()) { - out->AddCollideModel(m); + for (auto&& i : collision_meshes_) { + if (SceneCollisionMesh* m = i.second.Get()) { + out->AddCollisionMesh(m); } } // Add scene's nodes. - if (scene_.exists()) { + if (scene_.Exists()) { scene_->DumpNodes(out); } // Ok, now we can fill out our materials since nodes/etc they reference // exists. for (auto&& i : materials_) { - if (Material* m = i.get()) { + if (Material* m = i.Get()) { m->DumpComponents(out); } } @@ -491,7 +550,7 @@ auto HostActivity::NewTimer(TimeType timetype, TimerMedium length, bool repeat, const Object::Ref& runnable) -> int { // Make sure the runnable passed in is reference-managed already. // (we may not add an initial reference ourself) - assert(runnable->is_valid_refcounted_object()); + assert(runnable.IsValidManagedObject()); // We currently support game and base timers. switch (timetype) { @@ -501,7 +560,7 @@ auto HostActivity::NewTimer(TimeType timetype, TimerMedium length, bool repeat, return NewBaseTimer(length, repeat, runnable); default: // Fall back to default for descriptive error otherwise. - return ContextTarget::NewTimer(timetype, length, repeat, runnable); + return SceneV1Context::NewTimer(timetype, length, repeat, runnable); } } @@ -515,7 +574,7 @@ void HostActivity::DeleteTimer(TimeType timetype, int timer_id) { break; default: // Fall back to default for descriptive error otherwise. - ContextTarget::DeleteTimer(timetype, timer_id); + SceneV1Context::DeleteTimer(timetype, timer_id); break; } } @@ -528,8 +587,8 @@ auto HostActivity::GetTime(TimeType timetype) -> millisecs_t { return base_time(); default: // Fall back to default for descriptive error otherwise. - return ContextTarget::GetTime(timetype); + return SceneV1Context::GetTime(timetype); } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/support/host_activity.h b/src/ballistica/scene_v1/support/host_activity.h new file mode 100644 index 00000000..d323425d --- /dev/null +++ b/src/ballistica/scene_v1/support/host_activity.h @@ -0,0 +1,123 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_SUPPORT_HOST_ACTIVITY_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_HOST_ACTIVITY_H_ + +#include +#include +#include + +#include "ballistica/base/base.h" +#include "ballistica/base/support/context.h" +#include "ballistica/scene_v1/support/scene_v1_context.h" +#include "ballistica/shared/generic/timer_list.h" +#include "ballistica/shared/python/python_ref.h" + +namespace ballistica::scene_v1 { + +class HostActivity : public SceneV1Context { + public: + explicit HostActivity(HostSession* host_session); + ~HostActivity() override; + auto GetHostSession() -> HostSession* override; + void SetGameSpeed(float speed); + auto game_speed() const -> float { return game_speed_; } + + // ContextTarget time/timer support. + auto NewTimer(TimeType timetype, TimerMedium length, bool repeat, + const Object::Ref& runnable) -> int override; + void DeleteTimer(TimeType timetype, int timer_id) override; + auto GetTime(TimeType timetype) -> millisecs_t override; + + /// Return a borrowed ref to the python activity; Py_None if nonexistent. + auto GetPyActivity() const -> PyObject*; + + // All these commands are propagated into the output stream + // in addition to being applied locally. + auto NewMaterial(const std::string& name) -> Object::Ref; + auto GetTexture(const std::string& name) + -> Object::Ref override; + auto GetSound(const std::string& name) -> Object::Ref override; + auto GetData(const std::string& name) -> Object::Ref override; + auto GetMesh(const std::string& name) -> Object::Ref override; + auto GetCollisionMesh(const std::string& name) + -> Object::Ref override; + void StepDisplayTime(millisecs_t time_advance); + auto base_time() const -> millisecs_t { return base_time_; } + auto scene() -> Scene* { + assert(scene_.Exists()); + return scene_.Get(); + } + void start(); + + // A utility function; faster than dynamic_cast. + auto GetAsHostActivity() -> HostActivity* override; + auto GetMutableScene() -> Scene* override; + void Draw(base::FrameDef* frame_def); + void ScreenSizeChanged(); + void LanguageChanged(); + void DebugSpeedMultChanged(); + void GraphicsQualityChanged(base::GraphicsQuality q); + + // Used to register python calls created in this context so we can make sure + // they got properly cleaned up. + void RegisterContextCall(base::PythonContextCall* call) override; + auto shutting_down() const -> bool { return shutting_down_; } + auto globals_node() const -> GlobalsNode*; + void SetPaused(bool val); + auto paused() const -> bool { return paused_; } + void set_allow_kick_idle_players(bool val) { allow_kick_idle_players_ = val; } + auto getAllowKickIdlePlayers() const -> bool { + return allow_kick_idle_players_; + } + auto GetSceneStream() const -> SessionStream*; + void DumpFullState(SessionStream* out); + void SetGlobalsNode(GlobalsNode* node); + void SetIsForeground(bool val); + void RegisterPyActivity(PyObject* pyActivity); + + private: + void HandleOutOfBoundsNodes(); + auto NewSimTimer(millisecs_t length, bool repeat, + const Object::Ref& runnable) -> int; + void DeleteSimTimer(int timer_id); + auto NewBaseTimer(millisecs_t length, bool repeat, + const Object::Ref& runnable) -> int; + void DeleteBaseTimer(int timer_id); + void UpdateStepTimerLength(); + void StepScene(); + void PruneSessionBaseTimers(); + + /// Keep track of timers we've created in our session's base-timeline. + std::vector session_base_timer_ids_; + Object::WeakRef globals_node_; + bool allow_kick_idle_players_{}; + int step_scene_timer_id_{}; + std::unordered_map > textures_; + std::unordered_map > sounds_; + std::unordered_map > datas_; + std::unordered_map > + collision_meshes_; + std::unordered_map > meshes_; + std::list > materials_; + bool shutting_down_{}; + + // Our list of Python calls created in the context of this activity; + // we clear them as we are shutting down and ensure nothing runs after + // that point. + std::list > context_calls_; + millisecs_t next_prune_time_{}; + bool started_{}; + int out_of_bounds_in_a_row_{}; + bool paused_{}; + float game_speed_{}; + millisecs_t base_time_{}; + Object::Ref scene_; + Object::WeakRef host_session_; + PythonRef py_activity_weak_ref_; + TimerList scene_timers_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_SUPPORT_HOST_ACTIVITY_H_ diff --git a/src/ballistica/logic/session/host_session.cc b/src/ballistica/scene_v1/support/host_session.cc similarity index 61% rename from src/ballistica/logic/session/host_session.cc rename to src/ballistica/scene_v1/support/host_session.cc index 57f4f2be..2116833b 100644 --- a/src/ballistica/logic/session/host_session.cc +++ b/src/ballistica/scene_v1/support/host_session.cc @@ -1,32 +1,33 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/logic/session/host_session.h" +#include "ballistica/scene_v1/support/host_session.h" -#include "ballistica/assets/component/data.h" -#include "ballistica/assets/component/model.h" -#include "ballistica/assets/component/sound.h" -#include "ballistica/assets/component/texture.h" -#include "ballistica/generic/lambda_runnable.h" -#include "ballistica/generic/timer.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/input/device/input_device.h" -#include "ballistica/logic/host_activity.h" -#include "ballistica/logic/player.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_command.h" -#include "ballistica/python/python_context_call.h" -#include "ballistica/python/python_sys.h" -#include "ballistica/scene/scene_stream.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/scene_v1/assets/scene_data_asset.h" +#include "ballistica/scene_v1/assets/scene_mesh.h" +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/assets/scene_texture.h" +#include "ballistica/scene_v1/support/host_activity.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" +#include "ballistica/scene_v1/support/session_stream.h" +#include "ballistica/shared/generic/lambda_runnable.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" -namespace ballistica { +namespace ballistica::scene_v1 { HostSession::HostSession(PyObject* session_type_obj) - : last_kick_idle_players_decrement_time_(GetRealTime()) { - assert(g_logic); - assert(InLogicThread()); + : last_kick_idle_players_decrement_time_(g_core->GetAppTimeMillisecs()) { + assert(g_base->logic); + assert(g_base->InLogicThread()); assert(session_type_obj != nullptr); - ScopedSetContext cp(this); + auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + base::ScopedSetContext ssc(this); // FIXME: Should be an attr of the session class, not hard-coded. is_main_menu_ = @@ -34,11 +35,11 @@ HostSession::HostSession(PyObject* session_type_obj) "bastd.mainmenu.MainMenuSession")); // Log(LogLevel::kInfo, "MAIN MENU? " + std::to_string(is_main_menu())); - kick_idle_players_ = g_logic->kick_idle_players(); + kick_idle_players_ = appmode->kick_idle_players(); // Create a timer to step our session scene. step_scene_timer_ = - base_timers_.NewTimer(base_time_, kGameStepMilliseconds, 0, -1, + base_timers_.NewTimer(base_time_millisecs_, kGameStepMilliseconds, 0, -1, NewLambdaRunnable([this] { StepScene(); })); // Set up our output-stream, which will go to a replay and/or the network. @@ -47,28 +48,30 @@ HostSession::HostSession(PyObject* session_type_obj) bool do_replay = !is_main_menu_; // At the moment headless-server don't write replays. - if (HeadlessMode()) { + if (g_core->HeadlessMode()) { do_replay = false; } - output_stream_ = Object::New(this, do_replay); + output_stream_ = Object::New(this, do_replay); // Make a scene for our session-level nodes, etc. scene_ = Object::New(0); - if (output_stream_.exists()) { - output_stream_->AddScene(scene_.get()); + if (output_stream_.Exists()) { + output_stream_->AddScene(scene_.Get()); } // Fade in from our current blackness. - g_graphics->FadeScreen(true, 250, nullptr); + g_base->graphics->FadeScreen(true, 250, nullptr); // Start by showing the progress bar instead of hitching. - g_graphics->EnableProgressBar(true); + g_base->graphics->EnableProgressBar(true); // Now's a good time to run garbage collection; there should be pretty much // no game stuff to speak of in existence (provided the last session went // down peacefully). - g_python->obj(Python::ObjID::kGarbageCollectSessionEndCall).Call(); + g_base->python->objs() + .Get(base::BasePython::ObjID::kGarbageCollectSessionEndCall) + .Call(); // Instantiate our Python Session instance. PythonRef obj; @@ -77,20 +80,20 @@ HostSession::HostSession(PyObject* session_type_obj) Python::ScopedCallLabel label("Session instantiation"); obj = session_type.Call(); } - if (!obj.exists()) { + if (!obj.Exists()) { throw Exception("Error creating game session: '" + session_type.Str() + "'"); } // The session python object should have called - // _ba.register_session() in its constructor to set session_py_obj_. + // _babase.register_session() in its constructor to set session_py_obj_. if (session_py_obj_ != obj) { throw Exception("session not set up correctly"); } // Lastly, keep the python layer fed with our latest player count in case // it is updating the master-server with our current/max player counts. - g_logic->SetPublicPartyPlayerCount(static_cast(players_.size())); + appmode->SetPublicPartyPlayerCount(static_cast(players_.size())); } auto HostSession::GetHostSession() -> HostSession* { return this; } @@ -98,13 +101,13 @@ auto HostSession::GetHostSession() -> HostSession* { return this; } void HostSession::DestroyHostActivity(HostActivity* a) { BA_PRECONDITION(a); BA_PRECONDITION(a->GetHostSession() == this); - if (a == foreground_host_activity_.get()) { + if (a == foreground_host_activity_.Get()) { foreground_host_activity_.Clear(); } // Clear it from our activities list if its still on there. for (auto i = host_activities_.begin(); i < host_activities_.end(); i++) { - if (i->get() == a) { + if (i->Get() == a) { host_activities_.erase(i); return; } @@ -117,8 +120,8 @@ void HostSession::DestroyHostActivity(HostActivity* a) { } auto HostSession::GetMutableScene() -> Scene* { - assert(scene_.exists()); - return scene_.get(); + assert(scene_.Exists()); + return scene_.Get(); } void HostSession::DebugSpeedMultChanged() { @@ -151,7 +154,7 @@ void HostSession::LanguageChanged() { } } -void HostSession::GraphicsQualityChanged(GraphicsQuality q) { +void HostSession::GraphicsQualityChanged(base::GraphicsQuality q) { // Let our internal scene know. scene()->GraphicsQualityChanged(q); @@ -166,7 +169,7 @@ auto HostSession::DoesFillScreen() const -> bool { return true; } -void HostSession::Draw(FrameDef* f) { +void HostSession::Draw(base::FrameDef* f) { // First draw our session scene. scene()->Draw(f); @@ -176,71 +179,49 @@ void HostSession::Draw(FrameDef* f) { } } -auto HostSession::NewTimer(TimerMedium length, bool repeat, - const Object::Ref& runnable) -> int { - if (shutting_down_) { - BA_LOG_PYTHON_TRACE_ONCE( - "WARNING: Creating game timer during host-session shutdown"); - return 123; // dummy... - } - if (length == 0 && repeat) { - throw Exception("Can't add game-timer with length 0 and repeat on"); - } - if (length < 0) { - throw Exception("Timer length cannot be < 0 (got " + std::to_string(length) - + ")"); - } - int offset = 0; - Timer* t = sim_timers_.NewTimer(scene()->time(), length, offset, - repeat ? -1 : 0, runnable); - return t->id(); -} - -void HostSession::DeleteTimer(int timer_id) { - assert(InLogicThread()); - if (shutting_down_) return; - sim_timers_.DeleteTimer(timer_id); -} - -auto HostSession::GetSound(const std::string& name) -> Object::Ref { +auto HostSession::GetSound(const std::string& name) -> Object::Ref { if (shutting_down_) { throw Exception("can't load assets during session shutdown"); } - return Assets::GetAsset(&sounds_, name, scene()); + return GetAsset(&sounds_, name, scene()); } -auto HostSession::GetData(const std::string& name) -> Object::Ref { +auto HostSession::GetData(const std::string& name) + -> Object::Ref { if (shutting_down_) { throw Exception("can't load assets during session shutdown"); } - return Assets::GetAsset(&datas_, name, scene()); + return GetAsset(&datas_, name, scene()); } -auto HostSession::GetTexture(const std::string& name) -> Object::Ref { +auto HostSession::GetTexture(const std::string& name) + -> Object::Ref { if (shutting_down_) { throw Exception("can't load assets during session shutdown"); } - return Assets::GetAsset(&textures_, name, scene()); + return GetAsset(&textures_, name, scene()); } -auto HostSession::GetModel(const std::string& name) -> Object::Ref { + +auto HostSession::GetMesh(const std::string& name) -> Object::Ref { if (shutting_down_) { throw Exception("can't load media during session shutdown"); } - return Assets::GetAsset(&models_, name, scene()); + return GetAsset(&meshes_, name, scene()); } -auto HostSession::GetForegroundContext() -> Context { - HostActivity* a = foreground_host_activity_.get(); +auto HostSession::GetForegroundContext() -> base::ContextRef { + HostActivity* a = foreground_host_activity_.Get(); if (a) { - return Context(a); + return base::ContextRef(a); } - return Context(this); + return base::ContextRef(this); } -void HostSession::RequestPlayer(InputDevice* device) { - assert(InLogicThread()); +void HostSession::RequestPlayer(SceneV1InputDeviceDelegate* device) { + assert(g_base->InLogicThread()); + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); - // Ignore if we have no Python session obj. + // Ignore if we have no Python session Obj. if (!GetSessionPyObj()) { Log(LogLevel::kError, "HostSession::RequestPlayer() called w/no session_py_obj_."); @@ -252,13 +233,13 @@ void HostSession::RequestPlayer(InputDevice* device) { int player_id = next_player_id_++; auto player(Object::New(player_id, this)); players_.push_back(player); - device->AttachToLocalPlayer(player.get()); + device->AttachToLocalPlayer(player.Get()); // Ask the python layer to accept/deny this guy. bool accept; { // Set the session as context. - ScopedSetContext cp(this); + base::ScopedSetContext ssc(this); accept = static_cast( session_py_obj_.GetAttr("_request_player") .Call(PythonRef(Py_BuildValue("(O)", player->BorrowPyRef()), @@ -267,41 +248,51 @@ void HostSession::RequestPlayer(InputDevice* device) { if (accept) { player->set_accepted(true); } else { - RemovePlayer(player.get()); + RemovePlayer(player.Get()); } } // If he was accepted, update our game roster with the new info. if (accept) { - g_logic->UpdateGameRoster(); + appmode->UpdateGameRoster(); } // Lastly, keep the python layer fed with our latest player count in case it // is updating the master-server with our current/max player counts. - g_logic->SetPublicPartyPlayerCount(static_cast(players_.size())); + appmode->SetPublicPartyPlayerCount(static_cast(players_.size())); } void HostSession::RemovePlayer(Player* player) { assert(player); + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + // If we find the player amongst our ranks, remove them. + // Note that it is expected to get redundant calls for this that + // we silently ignore (for instance if a session removes a player + // then the player will still try to remove themself from their session + // as they are going down). for (auto i = players_.begin(); i != players_.end(); ++i) { - if (i->get() == player) { + if (i->Get() == player) { // Grab a ref to keep the player alive, pull him off the list, then call // his leaving callback. Object::Ref player2 = *i; players_.erase(i); + // Clear the player's attachment to its host-session so it doesn't + // redundantly ask the host-session to remove it as it is dying. + player->ClearHostSessionForTearDown(); + // Only make the callback for this player if they were accepted. if (player2->accepted()) { - IssuePlayerLeft(player2.get()); + IssuePlayerLeft(player2.Get()); } // Update our game roster with the departure. - g_logic->UpdateGameRoster(); + appmode->UpdateGameRoster(); // Lastly, keep the python layer fed with our latest player count in case // it is updating the master-server with our current/max player counts. - g_logic->SetPublicPartyPlayerCount(static_cast(players_.size())); + appmode->SetPublicPartyPlayerCount(static_cast(players_.size())); return; } @@ -311,13 +302,13 @@ void HostSession::RemovePlayer(Player* player) { void HostSession::IssuePlayerLeft(Player* player) { assert(player); - assert(InLogicThread()); + assert(g_base->InLogicThread()); try { if (GetSessionPyObj()) { if (player) { // Make sure we're the context for session callbacks. - ScopedSetContext cp(this); + base::ScopedSetContext ssc(this); Python::ScopedCallLabel label("Session on_player_leave"); session_py_obj_.GetAttr("on_player_leave") .Call(PythonRef(Py_BuildValue("(O)", player->BorrowPyRef()), @@ -338,16 +329,18 @@ void HostSession::IssuePlayerLeft(Player* player) { void HostSession::SetKickIdlePlayers(bool enable) { // If this has changed, reset our disconnect-time reporting. - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (enable != kick_idle_players_) { - last_kick_idle_players_decrement_time_ = GetRealTime(); + last_kick_idle_players_decrement_time_ = g_core->GetAppTimeMillisecs(); } kick_idle_players_ = enable; } void HostSession::SetForegroundHostActivity(HostActivity* a) { assert(a); - assert(InLogicThread()); + assert(g_base->InLogicThread()); + + auto* appmode = SceneV1AppMode::GetActiveOrFatal(); if (shutting_down_) { Log(LogLevel::kWarning, @@ -373,7 +366,7 @@ void HostSession::SetForegroundHostActivity(HostActivity* a) { // Now go through telling each host-activity whether it's foregrounded or not. // FIXME: Dying sessions never get told they're un-foregrounded.. could that // ever be a problem? - bool session_is_foreground = (g_logic->GetForegroundSession() != nullptr); + bool session_is_foreground = (appmode->GetForegroundSession() != nullptr); for (auto&& i : host_activities_) { i->SetIsForeground(session_is_foreground && (i == a)); } @@ -398,9 +391,9 @@ auto HostSession::NewHostActivity(PyObject* activity_type_obj, // First generate our C++ activity instance and point the context at it. auto activity(Object::New(this)); - AddHostActivity(activity.get()); + AddHostActivity(activity.Get()); - ScopedSetContext cp(activity.get()); + base::ScopedSetContext ssc(activity.Get()); // Now instantiate the Python instance.. pass args if some were provided, or // an empty dict otherwise. @@ -412,18 +405,18 @@ auto HostSession::NewHostActivity(PyObject* activity_type_obj, } PythonRef result = activity_type.Call(args); - if (!result.exists()) { + if (!result.Exists()) { throw Exception("HostActivity creation failed"); } // If all went well, the python activity constructor should have called - // _ba.register_activity(), so we should be able to get at the same python + // register_activity(), so we should be able to get at the same python // activity we just instantiated through the c++ class. - if (activity->GetPyActivity() != result.get()) { + if (activity->GetPyActivity() != result.Get()) { throw Exception("Error on HostActivity construction"); } - PyObject* obj = result.get(); + PyObject* obj = result.Get(); Py_INCREF(obj); return obj; } @@ -431,22 +424,22 @@ auto HostSession::NewHostActivity(PyObject* activity_type_obj, auto HostSession::RegisterPyActivity(PyObject* activity_obj) -> HostActivity* { // The context should be pointing to an unregistered HostActivity; // register and return it. - HostActivity* activity = Context::current().GetHostActivity(); + HostActivity* activity = ContextRefSceneV1::FromCurrent().GetHostActivity(); if (!activity) throw Exception( "No current activity in RegisterPyActivity; did you remember to call " - "ba.newHostActivity() to instantiate your activity?"); + "babase.newHostActivity() to instantiate your activity?"); activity->RegisterPyActivity(activity_obj); return activity; } void HostSession::DecrementPlayerTimeOuts(millisecs_t millisecs) { for (auto&& i : players_) { - Player* player = i.get(); + Player* player = i.Get(); assert(player); if (player->time_out() < millisecs) { std::string kick_str = - g_logic->GetResourceString("kickIdlePlayersKickedText"); + g_base->assets->GetResourceString("kickIdlePlayersKickedText"); Utils::StringReplaceOne(&kick_str, "${NAME}", player->GetName()); ScreenMessage(kick_str); RemovePlayer(player); @@ -454,21 +447,22 @@ void HostSession::DecrementPlayerTimeOuts(millisecs_t millisecs) { } else if (player->time_out() > BA_PLAYER_TIME_OUT_WARN && (player->time_out() - millisecs <= BA_PLAYER_TIME_OUT_WARN)) { std::string kick_str_1 = - g_logic->GetResourceString("kickIdlePlayersWarning1Text"); + g_base->assets->GetResourceString("kickIdlePlayersWarning1Text"); Utils::StringReplaceOne(&kick_str_1, "${NAME}", player->GetName()); Utils::StringReplaceOne(&kick_str_1, "${COUNT}", std::to_string(BA_PLAYER_TIME_OUT_WARN / 1000)); ScreenMessage(kick_str_1); - ScreenMessage(g_logic->GetResourceString("kickIdlePlayersWarning2Text")); + ScreenMessage( + g_base->assets->GetResourceString("kickIdlePlayersWarning2Text")); } player->set_time_out(player->time_out() - millisecs); } } void HostSession::ProcessPlayerTimeOuts() { - millisecs_t real_time = GetRealTime(); + millisecs_t real_time = g_core->GetAppTimeMillisecs(); - if (foreground_host_activity_.exists() + if (foreground_host_activity_.Exists() && foreground_host_activity_->game_speed() > 0.0 && !foreground_host_activity_->paused() && foreground_host_activity_->getAllowKickIdlePlayers() @@ -494,59 +488,98 @@ void HostSession::StepScene() { scene()->Step(); } -void HostSession::Update(int time_advance) { - assert(InLogicThread()); +void HostSession::Update(int time_advance_millisecs, double time_advance) { + assert(g_base->InLogicThread()); + + millisecs_t update_time_start = core::CorePlatform::GetCurrentMillisecs(); + + // HACK: we used to do a bunch of fudging to try and advance time by + // exactly 16 milliseconds per frame which would give us a clean 2 sim + // steps per frame on 60hz devices. These days we're trying to be more + // exact and general since non-60hz devices are becoming more common, + // but we're somewhat limited in our ability to do that here since + // our base-timer-list here and our scene-commands system both use + // milliseconds. Ideally if our sim were stepping by 8.3333 milliseconds and + // display-time were advancing by a constant 16.6666 then it would do the + // right thing, but with only integer millisecond precision we'll get aliasing + // and stuttering and some frames advancing by 1 sim step and others by 3, + // etc. So until we can upgrade everything to have finer precision (perhaps in + // scene_v2), let's just using the old trick of forcing 16 millisecond steps + // if it looks like we're probably running at 60hz. + if (time_advance_millisecs >= 15 && time_advance_millisecs <= 17) { + time_advance_millisecs = 16; + } else { + if (explicit_bool(false)) { + printf("NOT: %d %.5f\n", time_advance_millisecs, time_advance); + } + } // We can be killed at any time, so let's keep an eye out for that. WeakRef test_ref(this); - assert(test_ref.exists()); + assert(test_ref.Exists()); ProcessPlayerTimeOuts(); - SceneStream* output_stream = GetSceneStream(); + SessionStream* output_stream = GetSceneStream(); + auto too_slow{false}; - // Advance base time by the specified amount, + // Try to advance our base time by the provided amount, // firing all timers along the way. - millisecs_t target_base_time = base_time_ + time_advance; - while (!base_timers_.empty() - && (base_time_ + base_timers_.GetTimeToNextExpire(base_time_) - <= target_base_time)) { - base_time_ += base_timers_.GetTimeToNextExpire(base_time_); + millisecs_t target_base_time_millisecs = + base_time_millisecs_ + time_advance_millisecs; + while (!base_timers_.Empty() + && (base_time_millisecs_ + + base_timers_.TimeToNextExpire(base_time_millisecs_) + <= target_base_time_millisecs)) { + base_time_millisecs_ += base_timers_.TimeToNextExpire(base_time_millisecs_); if (output_stream) { - output_stream->SetTime(base_time_); + output_stream->SetTime(base_time_millisecs_); } - base_timers_.Run(base_time_); - } - base_time_ = target_base_time; - if (output_stream) { - output_stream->SetTime(base_time_); - } - assert(test_ref.exists()); + base_timers_.Run(base_time_millisecs_); - // Update our activities (iterate via weak-refs as this list may change under - // us at any time). - std::vector > activities = - PointersToWeakRefs(RefsToPointers(host_activities_)); - for (auto&& i : activities) { - if (i.exists()) { - i->Update(time_advance); - assert(test_ref.exists()); + // After each time we step time, abort if we're taking too long. This way we + // slow down if we're overloaded and have a better chance at maintaining + // a reasonable frame-rate/etc. + auto elapsed = + core::CorePlatform::GetCurrentMillisecs() - update_time_start; + if (elapsed >= 1000 / 30) { + too_slow = true; + break; } } - assert(test_ref.exists()); + + // If we didn't abort, set our time to where we were aiming for. + if (!too_slow) { + base_time_millisecs_ = target_base_time_millisecs; + if (output_stream) { + output_stream->SetTime(base_time_millisecs_); + } + } + assert(test_ref.Exists()); + + // Let our activities update too (iterate via weak-refs as this list may + // change under us at any time). + for (auto&& i : PointersToWeakRefs(RefsToPointers(host_activities_))) { + if (i.Exists()) { + i->StepDisplayTime(time_advance_millisecs); + assert(test_ref.Exists()); + } + } + assert(test_ref.Exists()); // Periodically prune various dead refs. - if (base_time_ > next_prune_time_) { + if (base_time_millisecs_ > next_prune_time_) { PruneDeadMapRefs(&textures_); PruneDeadMapRefs(&sounds_); - PruneDeadMapRefs(&models_); + PruneDeadMapRefs(&meshes_); PruneDeadRefs(&python_calls_); - next_prune_time_ = base_time_ + 5000; + next_prune_time_ = base_time_millisecs_ + 5000; } - assert(test_ref.exists()); + assert(test_ref.Exists()); } HostSession::~HostSession() { + assert(g_base->InLogicThread()); try { shutting_down_ = true; @@ -554,29 +587,34 @@ HostSession::~HostSession() { // (this generates warnings, suppresses messages, etc). scene_->set_shutting_down(true); - // Clear out all python calls registered in our context + // Tell all players not to inform us when they go down. + for (auto&& player : players_) { + player->ClearHostSessionForTearDown(); + } + + // Clear out all Python calls registered in our context // (should wipe out refs to our session and prevent them from running // without a valid session context). for (auto&& i : python_calls_) { - if (i.exists()) { - i->MarkDead(); + if (auto* j = i.Get()) { + j->MarkDead(); } } // Mark all our media dead to clear it out of our output-stream cleanly. for (auto&& i : textures_) { - if (i.second.exists()) { - i.second->MarkDead(); + if (auto* j = i.second.Get()) { + j->MarkDead(); } } - for (auto&& i : models_) { - if (i.second.exists()) { - i.second->MarkDead(); + for (auto&& i : meshes_) { + if (auto* j = i.second.Get()) { + j->MarkDead(); } } for (auto&& i : sounds_) { - if (i.second.exists()) { - i.second->MarkDead(); + if (auto* j = i.second.Get()) { + j->MarkDead(); } } @@ -586,9 +624,9 @@ HostSession::~HostSession() { sim_timers_.Clear(); scene_.Clear(); - // Kill our python session object. + // Kill our Python session object. { - ScopedSetContext cp(this); + base::ScopedSetContext ssc(this); session_py_obj_.Release(); } @@ -596,7 +634,7 @@ HostSession::~HostSession() { // when the session python object goes down, but lets clean up in case any // didn't. for (auto&& i : host_activities_) { - ScopedSetContext cp{Object::Ref(i)}; + base::ScopedSetContext ssc{Object::Ref(i)}; i.Clear(); } @@ -623,8 +661,14 @@ HostSession::~HostSession() { "Exception in HostSession destructor: " + std::string(e.what())); } } +auto HostSession::ContextAllowsDefaultTimerTypes() -> bool { + // We want to discourage the use of app-timers and display-timers + // in gameplay code; scene-timers and base-timers should be used instead + // since they properly support game speed changes, slowdowns, etc. + return false; +} -void HostSession::RegisterCall(PythonContextCall* call) { +void HostSession::RegisterContextCall(base::PythonContextCall* call) { assert(call); python_calls_.emplace_back(call); @@ -651,7 +695,7 @@ auto HostSession::GetUnusedPlayerName(Player* p, const std::string& base_name) } bool name_found = false; for (auto&& j : players_) { - if ((j->GetName() == name_test) && (j.get() != p)) { + if ((j->GetName() == name_test) && (j.Get() != p)) { name_found = true; break; } @@ -662,31 +706,31 @@ auto HostSession::GetUnusedPlayerName(Player* p, const std::string& base_name) return name_test; } -void HostSession::DumpFullState(SceneStream* out) { +void HostSession::DumpFullState(SessionStream* out) { // Add session-scene. - if (scene_.exists()) { + if (scene_.Exists()) { scene_->Dump(out); } // Dump media associated with session-scene. for (auto&& i : textures_) { - if (Texture* t = i.second.get()) { + if (SceneTexture* t = i.second.Get()) { out->AddTexture(t); } } for (auto&& i : sounds_) { - if (Sound* s = i.second.get()) { + if (SceneSound* s = i.second.Get()) { out->AddSound(s); } } - for (auto&& i : models_) { - if (Model* s = i.second.get()) { - out->AddModel(s); + for (auto&& i : meshes_) { + if (SceneMesh* s = i.second.Get()) { + out->AddMesh(s); } } // Dump session-scene's nodes. - if (scene_.exists()) { + if (scene_.Exists()) { scene_->DumpNodes(out); } @@ -701,7 +745,7 @@ void HostSession::GetCorrectionMessages( std::vector message; // Grab correction for session scene (though there shouldn't be one). - if (scene_.exists()) { + if (scene_.Exists()) { message = scene_->GetCorrectionMessage(blend); if (message.size() > 4) { // A correction packet of size 4 is empty; ignore it. @@ -711,7 +755,7 @@ void HostSession::GetCorrectionMessages( // Now do same for activity scenes. for (auto&& i : host_activities_) { - if (HostActivity* ha = i.get()) { + if (HostActivity* ha = i.Get()) { if (Scene* sg = ha->scene()) { message = sg->GetCorrectionMessage(blend); if (message.size() > 4) { @@ -725,32 +769,54 @@ void HostSession::GetCorrectionMessages( auto HostSession::NewTimer(TimeType timetype, TimerMedium length, bool repeat, const Object::Ref& runnable) -> int { - // Make sure the runnable passed in is reference-managed already - // (we may not add an initial reference ourself). - assert(runnable->is_valid_refcounted_object()); + assert(runnable.IsValidManagedObject()); // We currently support game and base timers. switch (timetype) { case TimeType::kSim: - case TimeType::kBase: - // Game and base timers are the same thing for us. - return NewTimer(length, repeat, runnable); + case TimeType::kBase: { + if (shutting_down_) { + BA_LOG_PYTHON_TRACE_ONCE( + "WARNING: Creating game timer during host-session shutdown"); + return 123; // dummy... + } + if (length == 0 && repeat) { + throw Exception("Can't add game-timer with length 0 and repeat on"); + } + if (length < 0) { + throw Exception("Timer length cannot be < 0 (got " + + std::to_string(length) + ")"); + } + int offset = 0; + auto&& timerlist = + timetype == TimeType::kSim ? sim_timers_ : base_timers_; + + Timer* t = timerlist.NewTimer(scene()->time(), length, offset, + repeat ? -1 : 0, runnable); + return t->id(); + } default: // Gall back to default for descriptive error otherwise. - return ContextTarget::NewTimer(timetype, length, repeat, runnable); + return SceneV1Context::NewTimer(timetype, length, repeat, runnable); } } void HostSession::DeleteTimer(TimeType timetype, int timer_id) { + assert(g_base->InLogicThread()); + if (shutting_down_) { + return; + } switch (timetype) { case TimeType::kSim: + sim_timers_.DeleteTimer(timer_id); + break; case TimeType::kBase: // Game and base timers are the same thing for us. - DeleteTimer(timer_id); + base_timers_.DeleteTimer(timer_id); break; default: // Fall back to default for descriptive error otherwise. - ContextTarget::DeleteTimer(timetype, timer_id); + SceneV1Context::DeleteTimer(timetype, timer_id); break; } } @@ -762,8 +828,8 @@ auto HostSession::GetTime(TimeType timetype) -> millisecs_t { return scene_->time(); default: // Fall back to default for descriptive error otherwise. - return ContextTarget::GetTime(timetype); + return SceneV1Context::GetTime(timetype); } } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/logic/session/host_session.h b/src/ballistica/scene_v1/support/host_session.h similarity index 54% rename from src/ballistica/logic/session/host_session.h rename to src/ballistica/scene_v1/support/host_session.h index 20729dfa..7ba2460f 100644 --- a/src/ballistica/logic/session/host_session.h +++ b/src/ballistica/scene_v1/support/host_session.h @@ -1,19 +1,19 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_LOGIC_SESSION_HOST_SESSION_H_ -#define BALLISTICA_LOGIC_SESSION_HOST_SESSION_H_ +#ifndef BALLISTICA_SCENE_V1_SUPPORT_HOST_SESSION_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_HOST_SESSION_H_ #include #include #include #include -#include "ballistica/core/context.h" -#include "ballistica/generic/timer_list.h" -#include "ballistica/logic/session/session.h" -#include "ballistica/python/python_ref.h" +#include "ballistica/base/support/context.h" +#include "ballistica/scene_v1/support/session.h" +#include "ballistica/shared/generic/timer_list.h" +#include "ballistica/shared/python/python_ref.h" -namespace ballistica { +namespace ballistica::scene_v1 { class HostSession : public Session { public: @@ -21,19 +21,20 @@ class HostSession : public Session { ~HostSession() override; // Return a borrowed python ref. - auto GetSessionPyObj() const -> PyObject* { return session_py_obj_.get(); } + auto GetSessionPyObj() const -> PyObject* { return session_py_obj_.Get(); } // Set focus to a Context (it must belong to this session). void SetForegroundHostActivity(HostActivity* sgc); - auto GetSound(const std::string& name) -> Object::Ref override; - auto GetData(const std::string& name) -> Object::Ref override; - auto GetTexture(const std::string& name) -> Object::Ref override; - auto GetModel(const std::string& name) -> Object::Ref override; + auto GetSound(const std::string& name) -> Object::Ref override; + auto GetData(const std::string& name) -> Object::Ref override; + auto GetTexture(const std::string& name) + -> Object::Ref override; + auto GetMesh(const std::string& name) -> Object::Ref override; void SetKickIdlePlayers(bool enable); // Update the session. - void Update(int time_advance) override; + void Update(int time_advance_millisecs, double time_advance) override; // ContextTarget time/timer support auto NewTimer(TimeType timetype, TimerMedium length, bool repeat, @@ -41,37 +42,51 @@ class HostSession : public Session { void DeleteTimer(TimeType timetype, int timer_id) override; auto GetTime(TimeType timetype) -> millisecs_t override; + void SetBaseTimerLength(int timer_id, int length) { + if (shutting_down_) { + return; + } + auto* timer = base_timers_.GetTimer(timer_id); + assert(timer); + if (!timer) { + return; + } + timer->SetLength(length, true, base_time_millisecs_); + } + auto BaseTimerExists(int timer_id) -> bool { + return base_timers_.GetTimer(timer_id) != nullptr; + } // Given an activity python type, instantiate a new activity // and return a new reference. auto NewHostActivity(PyObject* activity_type_obj, PyObject* settings_obj) -> PyObject*; void DestroyHostActivity(HostActivity* a); void RemovePlayer(Player* player); - void RequestPlayer(InputDevice* device); + void RequestPlayer(SceneV1InputDeviceDelegate* device); // Return either a host-activity context or the session-context. - auto GetForegroundContext() -> Context override; + auto GetForegroundContext() -> base::ContextRef override; auto DoesFillScreen() const -> bool override; - void Draw(FrameDef* f) override; + void Draw(base::FrameDef* f) override; void ScreenSizeChanged() override; void LanguageChanged() override; - void GraphicsQualityChanged(GraphicsQuality q) override; + void GraphicsQualityChanged(base::GraphicsQuality q) override; void DebugSpeedMultChanged() override; auto GetHostSession() -> HostSession* override; auto GetMutableScene() -> Scene* override; auto scene() -> Scene* { - assert(scene_.exists()); - return scene_.get(); + assert(scene_.Exists()); + return scene_.Get(); } - void RegisterCall(PythonContextCall* call); - auto GetSceneStream() const -> SceneStream* { return output_stream_.get(); } + void RegisterContextCall(base::PythonContextCall* call) override; + auto GetSceneStream() const -> SessionStream* { return output_stream_.Get(); } auto is_main_menu() const -> bool { return is_main_menu_; } // fixme remove this - void DumpFullState(SceneStream* out) override; + void DumpFullState(SessionStream* out) override; void GetCorrectionMessages(bool blend, std::vector >* messages); - auto base_time() const -> millisecs_t { return base_time_; } + auto base_time() const -> millisecs_t { return base_time_millisecs_; } auto players() const -> const std::vector >& { return players_; } @@ -87,45 +102,43 @@ class HostSession : public Session { auto GetUnusedPlayerName(Player* p, const std::string& base_name) -> std::string; + auto ContextAllowsDefaultTimerTypes() -> bool override; private: - auto NewTimer(TimerMedium length, bool repeat, - const Object::Ref& runnable) -> int; - void DeleteTimer(int timer_id); void StepScene(); void ProcessPlayerTimeOuts(); void DecrementPlayerTimeOuts(millisecs_t millisecs); void IssuePlayerLeft(Player* player); bool is_main_menu_; // FIXME: Remove this. - Object::Ref output_stream_; + Object::Ref output_stream_; Timer* step_scene_timer_; - millisecs_t base_time_ = 0; + millisecs_t base_time_millisecs_{}; TimerList sim_timers_; TimerList base_timers_; Object::Ref scene_; - bool shutting_down_ = false; + bool shutting_down_{}; - // Our list of python calls created in the context of this activity. We + // Our list of Python calls created in the context of this activity. We // clear them as we are shutting down and ensure nothing runs after that // point. - std::list > python_calls_; + std::list > python_calls_; std::vector > players_; - int next_player_id_ = 0; + int next_player_id_{}; // Which host-activity has focus at the moment (Players talking to it, etc). Object::WeakRef foreground_host_activity_; std::vector > host_activities_; PythonRef session_py_obj_; - bool kick_idle_players_ = false; + bool kick_idle_players_{}; millisecs_t last_kick_idle_players_decrement_time_; - millisecs_t next_prune_time_ = 0; - std::unordered_map > textures_; - std::unordered_map > sounds_; - std::unordered_map > datas_; - std::unordered_map > models_; + millisecs_t next_prune_time_{}; + std::unordered_map > textures_; + std::unordered_map > sounds_; + std::unordered_map > datas_; + std::unordered_map > meshes_; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_LOGIC_SESSION_HOST_SESSION_H_ +#endif // BALLISTICA_SCENE_V1_SUPPORT_HOST_SESSION_H_ diff --git a/src/ballistica/logic/player.cc b/src/ballistica/scene_v1/support/player.cc similarity index 81% rename from src/ballistica/logic/player.cc rename to src/ballistica/scene_v1/support/player.cc index 4571e24b..f9c855c9 100644 --- a/src/ballistica/logic/player.cc +++ b/src/ballistica/scene_v1/support/player.cc @@ -1,32 +1,32 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/logic/player.h" +#include "ballistica/scene_v1/support/player.h" -#include "ballistica/generic/utils.h" -#include "ballistica/input/device/joystick.h" -#include "ballistica/logic/host_activity.h" -#include "ballistica/logic/session/host_session.h" -#include "ballistica/python/class/python_class_session_player.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_context_call.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" +#include "ballistica/base/input/device/joystick_input.h" +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/scene_v1/python/class/python_class_session_player.h" +#include "ballistica/scene_v1/support/host_activity.h" +#include "ballistica/scene_v1/support/host_session.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::scene_v1 { Player::Player(int id_in, HostSession* host_session) - : id_(id_in), creation_time_(GetRealTime()), host_session_(host_session) { + : id_(id_in), + creation_time_(g_core->GetAppTimeMillisecs()), + host_session_(host_session) { assert(host_session); - assert(InLogicThread()); + assert(g_base->InLogicThread()); } Player::~Player() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); - // If we have an input-device attached to us, detach it. - InputDevice* input_device = input_device_.get(); - if (input_device) { - input_device->DetachFromPlayer(); + // If we have an input-device driving us, detach it. + if (auto* delegate = input_device_delegate_.Get()) { + delegate->DetachFromPlayer(); } // Release our ref to ourself if we have one. @@ -35,6 +35,10 @@ Player::~Player() { } } +auto Player::GetAge() const -> millisecs_t { + return g_core->GetAppTimeMillisecs() - creation_time_; +} + auto Player::GetName(bool full, bool icon) const -> std::string { std::string n = full ? full_name_ : name_; @@ -52,16 +56,16 @@ auto Player::GetName(bool full, bool icon) const -> std::string { } auto Player::GetHostActivity() const -> HostActivity* { - return host_activity_.get(); + return host_activity_.Get(); } void Player::SetHostActivity(HostActivity* a) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // Make sure we get pulled out of one activity before being added to another. if (a && in_activity_) { std::string old_name = - host_activity_.exists() + host_activity_.Exists() ? PythonRef(host_activity_->GetPyActivity(), PythonRef::kAcquire) .Str() : ""; @@ -79,6 +83,8 @@ void Player::SetHostActivity(HostActivity* a) { in_activity_ = (a != nullptr); } +void Player::ClearHostSessionForTearDown() { host_session_.Clear(); } + void Player::SetPosition(const Vector3f& position) { position_ = position; have_position_ = true; @@ -102,7 +108,7 @@ void Player::SetPyTeam(PyObject* team) { } auto Player::GetPyTeam() -> PyObject* { - PyObject* obj = py_team_weak_ref_.get(); + PyObject* obj = py_team_weak_ref_.Get(); if (!obj) { return Py_None; } @@ -118,26 +124,26 @@ void Player::SetPyCharacter(PyObject* character) { } auto Player::GetPyCharacter() -> PyObject* { - return py_character_.exists() ? py_character_.get() : Py_None; + return py_character_.Exists() ? py_character_.Get() : Py_None; } void Player::SetPyColor(PyObject* c) { py_color_.Acquire(c); } auto Player::GetPyColor() -> PyObject* { - return py_color_.exists() ? py_color_.get() : Py_None; + return py_color_.Exists() ? py_color_.Get() : Py_None; } void Player::SetPyHighlight(PyObject* c) { py_highlight_.Acquire(c); } auto Player::GetPyHighlight() -> PyObject* { - return py_highlight_.exists() ? py_highlight_.get() : Py_None; + return py_highlight_.Exists() ? py_highlight_.Get() : Py_None; } void Player::SetPyActivityPlayer(PyObject* c) { py_activityplayer_.Acquire(c); } auto Player::GetPyActivityPlayer() -> PyObject* { - return py_activityplayer_.exists() ? py_activityplayer_.get() : Py_None; + return py_activityplayer_.Exists() ? py_activityplayer_.Get() : Py_None; } auto Player::GetPyRef(bool new_ref) -> PyObject* { - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (py_ref_ == nullptr) { py_ref_ = PythonClassSessionPlayer::Create(this); } @@ -148,7 +154,7 @@ auto Player::GetPyRef(bool new_ref) -> PyObject* { } void Player::AssignInputCall(InputType type, PyObject* call_obj) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); assert(static_cast(type) >= 0 && static_cast(type) < static_cast(InputType::kLast)); @@ -176,7 +182,8 @@ void Player::AssignInputCall(InputType type, PyObject* call_obj) { break; } if (call_obj) { - calls_[static_cast(type)] = Object::New(call_obj); + calls_[static_cast(type)] = + Object::New(call_obj); } else { calls_[static_cast(type)].Clear(); } @@ -203,9 +210,9 @@ void Player::AssignInputCall(InputType type, PyObject* call_obj) { } void Player::RunInput(InputType type, float value) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); - const float threshold = kJoystickDiscreteThresholdFloat; + const float threshold = base::kJoystickDiscreteThresholdFloat; // Most input commands cause us to reset the player's time-out // there are a few exceptions though - very small analog values @@ -334,17 +341,17 @@ void Player::RunInput(InputType type, float value) { } auto j = calls_.find(static_cast(type)); - if (j != calls_.end() && j->second.exists()) { + if (j != calls_.end() && j->second.Exists()) { if (type == InputType::kRun) { PythonRef args( Py_BuildValue("(f)", std::min(1.0f, std::max(0.0f, value))), PythonRef::kSteal); - j->second->Run(args.get()); + j->second->Run(args.Get()); } else if (type == InputType::kLeftRight || type == InputType::kUpDown) { PythonRef args( Py_BuildValue("(f)", std::min(1.0f, std::max(-1.0f, value))), PythonRef::kSteal); - j->second->Run(args.get()); + j->second->Run(args.Get()); } else { j->second->Run(); } @@ -352,12 +359,12 @@ void Player::RunInput(InputType type, float value) { } auto Player::GetHostSession() const -> HostSession* { - return host_session_.get(); + return host_session_.Get(); } void Player::SetName(const std::string& name, const std::string& full_name, bool is_real) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); HostSession* host_session = GetHostSession(); BA_PRECONDITION(host_session); name_is_real_ = is_real; @@ -367,12 +374,14 @@ void Player::SetName(const std::string& name, const std::string& full_name, // If we're already in the game and our name is changing, we need to update // the roster. if (accepted_) { - g_logic->UpdateGameRoster(); + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + appmode->UpdateGameRoster(); + } } } void Player::InputCommand(InputType type, float value) { - assert(InLogicThread()); + assert(g_base->InLogicThread()); switch (type) { case InputType::kUpDown: case InputType::kLeftRight: @@ -388,14 +397,15 @@ void Player::InputCommand(InputType type, float value) { } } -void Player::SetInputDevice(InputDevice* input_device) { - input_device_ = input_device; +void Player::set_input_device_delegate( + SceneV1InputDeviceDelegate* input_device) { + input_device_delegate_ = input_device; } auto Player::GetPublicV1AccountID() const -> std::string { - assert(InLogicThread()); - if (input_device_.exists()) { - return input_device_->GetPublicV1AccountID(); + assert(g_base->InLogicThread()); + if (input_device_delegate_.Exists()) { + return input_device_delegate_->GetPublicV1AccountID(); } return ""; } @@ -413,4 +423,4 @@ void Player::SetIcon(const std::string& tex_name, icon_set_ = true; } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/logic/player.h b/src/ballistica/scene_v1/support/player.h similarity index 65% rename from src/ballistica/logic/player.h rename to src/ballistica/scene_v1/support/player.h index e9d6682c..653db317 100644 --- a/src/ballistica/logic/player.h +++ b/src/ballistica/scene_v1/support/player.h @@ -1,22 +1,22 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_LOGIC_PLAYER_H_ -#define BALLISTICA_LOGIC_PLAYER_H_ +#ifndef BALLISTICA_SCENE_V1_SUPPORT_PLAYER_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_PLAYER_H_ #include #include #include -#include "ballistica/core/object.h" -#include "ballistica/input/input.h" -#include "ballistica/math/vector3f.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/input/input.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/math/vector3f.h" // How much time should pass before we kick idle players (in milliseconds). #define BA_PLAYER_TIME_OUT 60000 #define BA_PLAYER_TIME_OUT_WARN 10000 -namespace ballistica { +namespace ballistica::scene_v1 { // A player (from the game's point of view). class Player : public Object { @@ -24,15 +24,15 @@ class Player : public Object { Player(int id, HostSession* host_session); ~Player() override; - auto SetInputDevice(InputDevice* input_device) -> void; - auto AssignInputCall(InputType type, PyObject* call_obj) -> void; - auto InputCommand(InputType type, float value = 0.0f) -> void; + void AssignInputCall(InputType type, PyObject* call_obj); + void InputCommand(InputType type, float value = 0.0f); - auto SetName(const std::string& name, const std::string& full_name, bool real) - -> void; auto GetName(bool full = false, bool icon = true) const -> std::string; + void SetName(const std::string& name, const std::string& full_name, + bool real); auto name_is_real() const -> bool { return name_is_real_; } - auto ResetInput() -> void; + + void ResetInput(); auto GetHostSession() const -> HostSession*; auto id() const -> int { return id_; } @@ -40,53 +40,58 @@ class Player : public Object { auto NewPyRef() -> PyObject* { return GetPyRef(true); } auto BorrowPyRef() -> PyObject* { return GetPyRef(false); } - // Set the player node for the current activity. - auto set_node(Node* node) -> void { - assert(InLogicThread()); + /// The player node for the current activity. + auto node() const -> Node* { + assert(g_base->InLogicThread()); + return node_.Get(); + } + /// Set the player node for the current activity. + void set_node(Node* node) { + assert(g_base->InLogicThread()); node_ = node; } - auto node() const -> Node* { - assert(InLogicThread()); - return node_.get(); - } - auto SetPyTeam(PyObject* team) -> void; auto GetPyTeam() -> PyObject*; // Returns a borrowed ref. + void SetPyTeam(PyObject* team); - auto SetPyCharacter(PyObject* team) -> void; auto GetPyCharacter() -> PyObject*; // Returns a borrowed ref. + void SetPyCharacter(PyObject* team); - auto SetPyColor(PyObject* team) -> void; auto GetPyColor() -> PyObject*; // Returns a borrowed ref. + void SetPyColor(PyObject* team); - auto SetPyHighlight(PyObject* team) -> void; auto GetPyHighlight() -> PyObject*; // Returns a borrowed ref. + void SetPyHighlight(PyObject* team); - auto SetPyActivityPlayer(PyObject* team) -> void; auto GetPyActivityPlayer() -> PyObject*; // Returns a borrowed ref. + void SetPyActivityPlayer(PyObject* team); - auto set_has_py_data(bool has) -> void { has_py_data_ = has; } auto has_py_data() const -> bool { return has_py_data_; } + void set_has_py_data(bool has) { has_py_data_ = has; } - auto GetInputDevice() const -> InputDevice* { return input_device_.get(); } - auto GetAge() const -> millisecs_t { return GetRealTime() - creation_time_; } + auto input_device_delegate() const -> SceneV1InputDeviceDelegate* { + return input_device_delegate_.Get(); + } + void set_input_device_delegate(SceneV1InputDeviceDelegate* input_device); + + auto GetAge() const -> millisecs_t; auto accepted() const -> bool { return accepted_; } - auto SetPosition(const Vector3f& position) -> void; + void SetPosition(const Vector3f& position); // If an public account-id can be determined with relative // certainty for this player, returns it. Otherwise returns // an empty string. auto GetPublicV1AccountID() const -> std::string; - auto SetHostActivity(HostActivity* host_activity) -> void; + void SetHostActivity(HostActivity* host_activity); auto GetHostActivity() const -> HostActivity*; auto has_py_ref() -> bool { return (py_ref_ != nullptr); } - auto SetIcon(const std::string& tex_name, const std::string& tint_tex_name, + void SetIcon(const std::string& tex_name, const std::string& tint_tex_name, const std::vector& tint_color, - const std::vector& tint2_color) -> void; + const std::vector& tint2_color); auto icon_tex_name() const -> const std::string& { BA_PRECONDITION(icon_set_); @@ -104,14 +109,16 @@ class Player : public Object { BA_PRECONDITION(icon_set_); return icon_tint2_color_; } - auto set_accepted(bool value) -> void { accepted_ = value; } + void set_accepted(bool value) { accepted_ = value; } auto time_out() const -> millisecs_t { return time_out_; } - auto set_time_out(millisecs_t value) -> void { time_out_ = value; } - auto set_have_position(bool value) -> void { have_position_ = value; } + void set_time_out(millisecs_t value) { time_out_ = value; } + void set_have_position(bool value) { have_position_ = value; } + + void ClearHostSessionForTearDown(); private: auto GetPyRef(bool new_ref) -> PyObject*; - auto RunInput(InputType type, float value = 0.0f) -> void; + void RunInput(InputType type, float value = 0.0f); bool icon_set_{}; std::string icon_tex_name_; std::string icon_tint_tex_name_; @@ -121,7 +128,7 @@ class Player : public Object { Object::WeakRef host_activity_; Object::WeakRef node_; bool in_activity_{}; - Object::WeakRef input_device_; + Object::WeakRef input_device_delegate_; PyObject* py_ref_{}; bool accepted_{}; bool has_py_data_{}; @@ -159,9 +166,9 @@ class Player : public Object { PythonRef py_color_; PythonRef py_highlight_; PythonRef py_activityplayer_; - std::unordered_map > calls_; + std::unordered_map > calls_; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_LOGIC_PLAYER_H_ +#endif // BALLISTICA_SCENE_V1_SUPPORT_PLAYER_H_ diff --git a/src/ballistica/logic/player_spec.cc b/src/ballistica/scene_v1/support/player_spec.cc similarity index 71% rename from src/ballistica/logic/player_spec.cc rename to src/ballistica/scene_v1/support/player_spec.cc index 8f8f28c9..9fb5c3d8 100644 --- a/src/ballistica/logic/player_spec.cc +++ b/src/ballistica/scene_v1/support/player_spec.cc @@ -1,15 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/logic/player_spec.h" +#include "ballistica/scene_v1/support/player_spec.h" -#include "ballistica/app/app.h" -#include "ballistica/generic/json.h" -#include "ballistica/generic/utils.h" -#include "ballistica/logic/logic.h" -#include "ballistica/logic/v1_account.h" -#include "ballistica/platform/platform.h" +#include "ballistica/classic/support/v1_account.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/shared/generic/json.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::scene_v1 { PlayerSpec::PlayerSpec() = default; @@ -27,7 +26,7 @@ PlayerSpec::PlayerSpec(const std::string& s) { // Account type may technically be something we don't recognize, // but that's ok.. it'll just be 'invalid' to us in that case account_type_ = - V1Account::AccountTypeFromString(account_obj->valuestring); + classic::V1Account::AccountTypeFromString(account_obj->valuestring); success = true; } cJSON_Delete(root_obj); @@ -36,12 +35,12 @@ PlayerSpec::PlayerSpec(const std::string& s) { Log(LogLevel::kError, "Error creating PlayerSpec from string: '" + s + "'"); name_ = ""; short_name_ = ""; - account_type_ = V1AccountType::kInvalid; + account_type_ = classic::V1AccountType::kInvalid; } } auto PlayerSpec::GetDisplayString() const -> std::string { - return V1Account::AccountTypeToIconString(account_type_) + name_; + return classic::V1Account::AccountTypeToIconString(account_type_) + name_; } auto PlayerSpec::GetShortName() const -> std::string { @@ -62,7 +61,8 @@ auto PlayerSpec::GetSpecString() const -> std::string { root = cJSON_CreateObject(); cJSON_AddStringToObject(root, "n", name_.c_str()); cJSON_AddStringToObject( - root, "a", V1Account::AccountTypeToString(account_type_).c_str()); + root, "a", + classic::V1Account::AccountTypeToString(account_type_).c_str()); cJSON_AddStringToObject(root, "sn", short_name_.c_str()); char* out = cJSON_PrintUnformatted(root); std::string out_s = out; @@ -76,21 +76,23 @@ auto PlayerSpec::GetSpecString() const -> std::string { } auto PlayerSpec::GetAccountPlayerSpec() -> PlayerSpec { + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); PlayerSpec spec; - if (g_v1_account->GetLoginState() == V1LoginState::kSignedIn) { - spec.account_type_ = g_app->account_type; - spec.name_ = - Utils::GetValidUTF8(g_v1_account->GetLoginName().c_str(), "bsgaps"); + if (g_classic->v1_account->GetLoginState() + == classic::V1LoginState::kSignedIn) { + spec.account_type_ = g_classic->account_type; + spec.name_ = Utils::GetValidUTF8( + g_classic->v1_account->GetLoginName().c_str(), "bsgaps"); } else { // Headless builds fall back to V1 public-party name if that's available. if (g_buildconfig.headless_build() - && !g_logic->public_party_name().empty()) { + && !appmode->public_party_name().empty()) { spec.name_ = - Utils::GetValidUTF8(g_logic->public_party_name().c_str(), "bsgp3r"); + Utils::GetValidUTF8(appmode->public_party_name().c_str(), "bsgp3r"); } else { // Or lastly fall back to device name. - spec.name_ = - Utils::GetValidUTF8(g_platform->GetDeviceName().c_str(), "bsgaps2"); + spec.name_ = Utils::GetValidUTF8( + g_core->platform->GetDeviceName().c_str(), "bsgaps2"); } } if (spec.name_.size() > 100) { @@ -115,4 +117,4 @@ auto PlayerSpec::GetDummyPlayerSpec(const std::string& name) -> PlayerSpec { return spec; } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/logic/player_spec.h b/src/ballistica/scene_v1/support/player_spec.h similarity index 83% rename from src/ballistica/logic/player_spec.h rename to src/ballistica/scene_v1/support/player_spec.h index 8d1bbc49..213225f2 100644 --- a/src/ballistica/logic/player_spec.h +++ b/src/ballistica/scene_v1/support/player_spec.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_LOGIC_PLAYER_SPEC_H_ -#define BALLISTICA_LOGIC_PLAYER_SPEC_H_ +#ifndef BALLISTICA_SCENE_V1_SUPPORT_PLAYER_SPEC_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_PLAYER_SPEC_H_ #include -#include "ballistica/ballistica.h" +#include "ballistica/classic/classic.h" -namespace ballistica { +namespace ballistica::scene_v1 { /// a PlayerSpec is a portable description of an entity such as a player or /// client. It can contain long and short names, optional info linking it to a @@ -51,9 +51,9 @@ class PlayerSpec { private: std::string name_; std::string short_name_; - V1AccountType account_type_{V1AccountType::kInvalid}; + classic::V1AccountType account_type_{classic::V1AccountType::kInvalid}; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_LOGIC_PLAYER_SPEC_H_ +#endif // BALLISTICA_SCENE_V1_SUPPORT_PLAYER_SPEC_H_ diff --git a/src/ballistica/scene/scene.cc b/src/ballistica/scene_v1/support/scene.cc similarity index 76% rename from src/ballistica/scene/scene.cc rename to src/ballistica/scene_v1/support/scene.cc index acf77a96..20d895f3 100644 --- a/src/ballistica/scene/scene.cc +++ b/src/ballistica/scene_v1/support/scene.cc @@ -1,24 +1,24 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/scene.h" +#include "ballistica/scene_v1/support/scene.h" -#include "ballistica/app/app.h" -#include "ballistica/assets/component/sound.h" -#include "ballistica/audio/audio.h" -#include "ballistica/dynamics/dynamics.h" -#include "ballistica/graphics/camera.h" -#include "ballistica/networking/networking.h" -#include "ballistica/python/python_context_call.h" -#include "ballistica/scene/node/bomb_node.h" -#include "ballistica/scene/node/node_attribute_connection.h" -#include "ballistica/scene/node/player_node.h" -#include "ballistica/scene/node/text_node.h" -#include "ballistica/scene/scene_stream.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/graphics/support/camera.h" +#include "ballistica/base/networking/networking.h" +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/dynamics/dynamics.h" +#include "ballistica/scene_v1/node/bomb_node.h" +#include "ballistica/scene_v1/node/node_attribute_connection.h" +#include "ballistica/scene_v1/node/player_node.h" +#include "ballistica/scene_v1/node/text_node.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/scene_v1/support/session_stream.h" -namespace ballistica { +namespace ballistica::scene_v1 { -auto Scene::GetSceneStream() const -> SceneStream* { - return output_stream_.get(); +auto Scene::GetSceneStream() const -> SessionStream* { + return output_stream_.Get(); } void Scene::SetMapBounds(float xmin, float ymin, float zmin, float xmax, @@ -34,7 +34,7 @@ void Scene::SetMapBounds(float xmin, float ymin, float zmin, float xmax, Scene::Scene(millisecs_t start_time) : time_(start_time), stepnum_(start_time / kGameStepMilliseconds), - last_step_real_time_(GetRealTime()) { + last_step_real_time_(g_core->GetAppTimeMillisecs()) { dynamics_ = Object::New(this); // Reset world bounds to default. @@ -58,24 +58,24 @@ Scene::~Scene() { dynamics_.Clear(); // If we were associated with an output-stream, inform it of our demise. - if (output_stream_.exists()) { + if (output_stream_.Exists()) { output_stream_->RemoveScene(this); } } -void Scene::PlaySoundAtPosition(Sound* sound, float volume, float x, float y, - float z, bool host_only) { - if (output_stream_.exists() && !host_only) { +void Scene::PlaySoundAtPosition(SceneSound* sound, float volume, float x, + float y, float z, bool host_only) { + if (output_stream_.Exists() && !host_only) { output_stream_->PlaySoundAtPosition(sound, volume, x, y, z); } - g_audio->PlaySoundAtPosition(sound->GetSoundData(), volume, x, y, z); + g_base->audio->PlaySoundAtPosition(sound->GetSoundData(), volume, x, y, z); } -void Scene::PlaySound(Sound* sound, float volume, bool host_only) { - if (output_stream_.exists() && !host_only) { +void Scene::PlaySound(SceneSound* sound, float volume, bool host_only) { + if (output_stream_.Exists() && !host_only) { output_stream_->PlaySound(sound, volume); } - g_audio->PlaySound(sound->GetSoundData(), volume); + g_base->audio->PlaySound(sound->GetSoundData(), volume); } auto Scene::IsOutOfBounds(float x, float y, float z) -> bool { @@ -89,12 +89,12 @@ auto Scene::IsOutOfBounds(float x, float y, float z) -> bool { || std::isnan(z) || std::isinf(x) || std::isinf(y) || std::isinf(z)); } -void Scene::Draw(FrameDef* frame_def) { +void Scene::Draw(base::FrameDef* frame_def) { // Draw our nodes. for (auto&& i : nodes_) { - g_graphics->PreNodeDraw(); + g_base->graphics->PreNodeDraw(); i->Draw(frame_def); - g_graphics->PostNodeDraw(); + g_base->graphics->PostNodeDraw(); } // Draw any dynamics debugging extras. @@ -102,17 +102,17 @@ void Scene::Draw(FrameDef* frame_def) { } auto Scene::GetNodeMessageType(const std::string& type) -> NodeMessageType { - assert(g_app != nullptr); - auto i = g_app->node_message_types.find(type); - if (i == g_app->node_message_types.end()) { + assert(g_scene_v1 != nullptr); + auto i = g_scene_v1->node_message_types().find(type); + if (i == g_scene_v1->node_message_types().end()) { throw Exception("Invalid node-message type: '" + type + "'"); } return i->second; } auto Scene::GetNodeMessageTypeName(NodeMessageType t) -> std::string { - assert(g_app != nullptr); - for (auto&& i : g_app->node_message_types) { + assert(g_scene_v1 != nullptr); + for (auto&& i : g_scene_v1->node_message_types()) { if (i.second == t) { return i.first; } @@ -125,7 +125,7 @@ void Scene::SetPlayerNode(int id, PlayerNode* n) { player_nodes_[id] = n; } auto Scene::GetPlayerNode(int id) -> PlayerNode* { auto i = player_nodes_.find(id); if (i != player_nodes_.end()) { - return i->second.get(); + return i->second.Get(); } return nullptr; } @@ -133,12 +133,14 @@ auto Scene::GetPlayerNode(int id) -> PlayerNode* { void Scene::Step() { out_of_bounds_nodes_.clear(); + auto* appmode = SceneV1AppMode::GetActiveOrFatal(); + // Step all our nodes. { in_step_ = true; - last_step_real_time_ = GetRealTime(); + last_step_real_time_ = g_core->GetAppTimeMillisecs(); for (auto&& i : nodes_) { - Node* node = i.get(); + Node* node = i.Get(); node->Step(); // Now that it's stepped, pump new values to any nodes it's connected to. @@ -146,20 +148,21 @@ void Scene::Step() { } in_step_ = false; } - bool is_foreground = (g_logic->GetForegroundScene() == this); + bool is_foreground = (appmode->GetForegroundScene() == this); // Add a step command to the output stream. - if (output_stream_.exists()) { + if (output_stream_.Exists()) { output_stream_->StepScene(this); } // And step things locally. if (is_foreground) { Vector3f cam_pos = {0.0f, 0.0f, 0.0f}; - g_graphics->camera()->get_position(&cam_pos.x, &cam_pos.y, &cam_pos.z); -#if !BA_HEADLESS_BUILD - g_bg_dynamics->Step(cam_pos); -#endif // !BA_HEADLESS_BUILD + g_base->graphics->camera()->get_position(&cam_pos.x, &cam_pos.y, + &cam_pos.z); + if (!g_core->HeadlessMode()) { + g_base->bg_dynamics->Step(cam_pos, kGameStepMilliseconds); + } } // Lastly step our sim. @@ -186,14 +189,14 @@ void Scene::DeleteNode(Node* node) { // Copy refs to its death-actions and dependent-nodes; we'll deal with these // after the node is dead so we're sure they don't muck with the node. - std::vector > death_actions = + std::vector > death_actions = node->death_actions(); std::vector > dependent_nodes = node->dependent_nodes(); // Sanity test to make sure it dies when we ask. #if BA_DEBUG_BUILD Object::WeakRef temp_weak_ref(node); - BA_PRECONDITION(temp_weak_ref.exists()); + BA_PRECONDITION(temp_weak_ref.Exists()); #endif // Copy a strong ref to this node to keep it alive until we've wiped it from @@ -205,7 +208,7 @@ void Scene::DeleteNode(Node* node) { // Sanity test: at this point the node should be dead. #if BA_DEBUG_BUILD - if (temp_weak_ref.exists()) { + if (temp_weak_ref.Exists()) { Log(LogLevel::kError, "Node still exists after ref release!!"); } #endif // BA_DEBUG_BUILD @@ -216,7 +219,7 @@ void Scene::DeleteNode(Node* node) { i->Run(); } for (auto&& i : dependent_nodes) { - Node* node2 = i.get(); + Node* node2 = i.Get(); if (node2) { node2->scene()->DeleteNode(node2); } @@ -224,38 +227,38 @@ void Scene::DeleteNode(Node* node) { } } -void Scene::GraphicsQualityChanged(GraphicsQuality q) { - assert(InLogicThread()); +void Scene::GraphicsQualityChanged(base::GraphicsQuality q) { + assert(g_base->InLogicThread()); for (auto&& i : nodes_) { i->OnGraphicsQualityChanged(q); } } void Scene::ScreenSizeChanged() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); for (auto&& i : nodes_) { i->OnScreenSizeChange(); // New. } } void Scene::LanguageChanged() { - assert(InLogicThread()); + assert(g_base->InLogicThread()); for (auto&& i : nodes_) { i->OnLanguageChange(); // New. } } auto Scene::GetNodeMessageFormat(NodeMessageType type) -> const char* { - assert(g_app != nullptr); - if ((unsigned int)type >= g_app->node_message_formats.size()) { + assert(g_scene_v1 != nullptr); + if ((unsigned int)type >= g_scene_v1->node_message_formats().size()) { return nullptr; } - return g_app->node_message_formats[static_cast(type)].c_str(); + return g_scene_v1->node_message_formats().at(static_cast(type)).c_str(); } auto Scene::NewNode(const std::string& type_string, const std::string& name, PyObject* delegate) -> Node* { - assert(InLogicThread()); + assert(g_base->InLogicThread()); // Clion incorrectly things in_step_ will always be false. #pragma clang diagnostic push @@ -270,30 +273,31 @@ auto Scene::NewNode(const std::string& type_string, const std::string& name, // Should never change the scene while we're stepping it. assert(!in_step_); - assert(g_app != nullptr); - auto i = g_app->node_types.find(type_string); - if (i == g_app->node_types.end()) { + assert(g_core != nullptr); + auto i = g_scene_v1->node_types().find(type_string); + if (i == g_scene_v1->node_types().end()) { throw Exception("Invalid node type: '" + type_string + "'"); } - auto node = Object::MakeRefCounted(i->second->Create(this)); - assert(node.exists()); + auto node = Object::CompleteDeferred(i->second->Create(this)); + assert(node.Exists()); node->AddToScene(this); node->set_label(name); node->SetDelegate(delegate); - return node.get(); // NOLINT + return node.Get(); // NOLINT } -void Scene::Dump(SceneStream* stream) { - assert(InLogicThread()); +void Scene::Dump(SessionStream* stream) { + assert(g_base->InLogicThread()); + auto* appmode = SceneV1AppMode::GetActiveOrFatal(); stream->AddScene(this); // If we're the foreground one, communicate that fact as well. - if (g_logic->GetForegroundScene() == this) { + if (appmode->GetForegroundScene() == this) { stream->SetForegroundScene(this); } } -void Scene::DumpNodes(SceneStream* out) { +void Scene::DumpNodes(SessionStream* out) { // Dumps commands to the output stream to recreate scene's nodes // in their current state. @@ -301,10 +305,10 @@ void Scene::DumpNodes(SceneStream* out) { // We have to do this all at once before setting attrs since any node // can refer to any other in an attr set. for (auto&& i : nodes_) { - Node* node = i.get(); + Node* node = i.Get(); assert(node); - // add the node + // Add the node. out->AddNode(node); } @@ -312,7 +316,7 @@ void Scene::DumpNodes(SceneStream* out) { // Now go through and set *most* node attr values. for (auto&& i1 : nodes_) { - Node* node = i1.get(); + Node* node = i1.Get(); assert(node); // Now we need to set *all* of its attrs in order. @@ -379,20 +383,20 @@ void Scene::DumpNodes(SceneStream* out) { out->SetNodeAttr(attr, attr.GetAsSounds()); break; } - case NodeAttributeType::kModel: { - out->SetNodeAttr(attr, attr.GetAsModel()); + case NodeAttributeType::kMesh: { + out->SetNodeAttr(attr, attr.GetAsMesh()); break; } - case NodeAttributeType::kModelArray: { - out->SetNodeAttr(attr, attr.GetAsModels()); + case NodeAttributeType::kMeshArray: { + out->SetNodeAttr(attr, attr.GetAsMeshes()); break; } - case NodeAttributeType::kCollideModel: { - out->SetNodeAttr(attr, attr.GetAsCollideModel()); + case NodeAttributeType::kCollisionMesh: { + out->SetNodeAttr(attr, attr.GetAsCollisionMesh()); break; } - case NodeAttributeType::kCollideModelArray: { - out->SetNodeAttr(attr, attr.GetAsCollideModels()); + case NodeAttributeType::kCollisionMeshArray: { + out->SetNodeAttr(attr, attr.GetAsCollisionMeshes()); break; } default: @@ -408,7 +412,7 @@ void Scene::DumpNodes(SceneStream* out) { // Now run through all nodes once more and add an OnCreate() call // so they can do any post-create setup they need to. for (auto&& i : nodes_) { - Node* node = i.get(); + Node* node = i.Get(); assert(node); out->NodeOnCreate(node); } @@ -420,13 +424,13 @@ void Scene::DumpNodes(SceneStream* out) { // And lastly re-establish node attribute-connections. for (auto&& i : nodes_) { - Node* node = i.get(); + Node* node = i.Get(); assert(node); for (auto&& j : node->attribute_connections()) { - assert(j.exists()); - Node* src_node = j->src_node.get(); + assert(j.Exists()); + Node* src_node = j->src_node.Get(); assert(src_node); - Node* dst_node = j->dst_node.get(); + Node* dst_node = j->dst_node.Get(); assert(dst_node); NodeAttributeUnbound* src_attr = src_node->type()->GetAttribute(j->src_attr_index); @@ -452,7 +456,7 @@ auto Scene::GetCorrectionMessage(bool blended) -> std::vector { std::vector dynamic_bodies; for (auto&& i : nodes_) { - Node* n = i.get(); + Node* n = i.Get(); assert(n); if (n && !n->parts().empty()) { dynamic_bodies.clear(); @@ -535,13 +539,12 @@ auto Scene::GetCorrectionMessage(bool blended) -> std::vector { return message; } -void Scene::SetOutputStream(SceneStream* val) { output_stream_ = val; } +void Scene::SetOutputStream(SessionStream* val) { output_stream_ = val; } -auto Scene::AddNode(Node* node, int64_t* node_id, NodeList::iterator* i) - -> void { +void Scene::AddNode(Node* node, int64_t* node_id, NodeList::iterator* i) { assert(node && node_id && i); *node_id = next_node_id_++; *i = nodes_.insert(nodes_.end(), Object::Ref(node)); } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene/scene.h b/src/ballistica/scene_v1/support/scene.h similarity index 56% rename from src/ballistica/scene/scene.h rename to src/ballistica/scene_v1/support/scene.h index e9557eee..d79a99b0 100644 --- a/src/ballistica/scene/scene.h +++ b/src/ballistica/scene_v1/support/scene.h @@ -1,29 +1,30 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_SCENE_SCENE_H_ -#define BALLISTICA_SCENE_SCENE_H_ +#ifndef BALLISTICA_SCENE_V1_SUPPORT_SCENE_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_SCENE_H_ #include #include #include -#include "ballistica/core/object.h" -#include "ballistica/logic/logic.h" -#include "ballistica/scene/node/node.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/scene_v1/node/node.h" +#include "ballistica/shared/foundation/object.h" -namespace ballistica { +namespace ballistica::scene_v1 { +/// A place where nodes/actors/etc. live. class Scene : public Object { public: explicit Scene(millisecs_t starttime); ~Scene() override; - auto Step() -> void; - auto Draw(FrameDef* frame_def) -> void; + void Step(); + void Draw(base::FrameDef* frame_def); auto NewNode(const std::string& type, const std::string& name, PyObject* delegate) -> Node*; - auto PlaySoundAtPosition(Sound* sound, float volume, float x, float y, - float z, bool host_only = false) -> void; - auto PlaySound(Sound* sound, float volume, bool host_only = false) -> void; + void PlaySoundAtPosition(SceneSound* sound, float volume, float x, float y, + float z, bool host_only = false); + void PlaySound(SceneSound* sound, float volume, bool host_only = false); static auto GetNodeMessageType(const std::string& type_name) -> NodeMessageType; static auto GetNodeMessageTypeName(NodeMessageType t) -> std::string; @@ -31,48 +32,43 @@ class Scene : public Object { auto time() const -> millisecs_t { return time_; } auto stepnum() const -> int64_t { return stepnum_; } auto nodes() const -> const NodeList& { return nodes_; } - auto AddNode(Node*, int64_t* node_id, NodeList::iterator* i) -> void; - auto AddOutOfBoundsNode(Node* n) -> void { - out_of_bounds_nodes_.emplace_back(n); - } + void AddNode(Node*, int64_t* node_id, NodeList::iterator* i); + void AddOutOfBoundsNode(Node* n) { out_of_bounds_nodes_.emplace_back(n); } auto IsOutOfBounds(float x, float y, float z) -> bool; auto dynamics() const -> Dynamics* { - assert(dynamics_.exists()); - return dynamics_.get(); + assert(dynamics_.Exists()); + return dynamics_.Get(); } auto in_step() const -> bool { return in_step_; } - auto SetMapBounds(float x, float y, float z, float X, float Y, float Z) - -> void; - auto ScreenSizeChanged() -> void; - auto LanguageChanged() -> void; - auto GraphicsQualityChanged(GraphicsQuality q) -> void; + void SetMapBounds(float x, float y, float z, float X, float Y, float Z); + void ScreenSizeChanged(); + void LanguageChanged(); + void GraphicsQualityChanged(base::GraphicsQuality q); auto out_of_bounds_nodes() -> const std::vector >& { return out_of_bounds_nodes_; } - auto DeleteNode(Node* node) -> void; + void DeleteNode(Node* node); auto shutting_down() const -> bool { return shutting_down_; } - auto set_shutting_down(bool val) -> void { shutting_down_ = val; } - auto GetSceneStream() const -> SceneStream*; - auto SetPlayerNode(int id, PlayerNode* n) -> void; + void set_shutting_down(bool val) { shutting_down_ = val; } + auto GetSceneStream() const -> SessionStream*; + void SetPlayerNode(int id, PlayerNode* n); auto GetPlayerNode(int id) -> PlayerNode*; auto use_fixed_vr_overlay() const -> bool { return use_fixed_vr_overlay_; } - auto set_use_fixed_vr_overlay(bool val) -> void { - use_fixed_vr_overlay_ = val; - } - auto increment_bg_cover_count() -> void { bg_cover_count_++; } - auto decrement_bg_cover_count() -> void { bg_cover_count_--; } + void set_use_fixed_vr_overlay(bool val) { use_fixed_vr_overlay_ = val; } + void increment_bg_cover_count() { bg_cover_count_++; } + void decrement_bg_cover_count() { bg_cover_count_--; } auto has_bg_cover() const -> bool { return (bg_cover_count_ > 0); } - auto Dump(SceneStream* out) -> void; - auto DumpNodes(SceneStream* out) -> void; + void Dump(SessionStream* out); + void DumpNodes(SessionStream* out); auto GetCorrectionMessage(bool blended) -> std::vector; - auto SetOutputStream(SceneStream* val) -> void; + void SetOutputStream(SessionStream* val); auto stream_id() const -> int64_t { return stream_id_; } - auto set_stream_id(int64_t val) -> void { + void set_stream_id(int64_t val) { assert(stream_id_ == -1); stream_id_ = val; } - auto clear_stream_id() -> void { + void clear_stream_id() { assert(stream_id_ != -1); stream_id_ = -1; } @@ -81,15 +77,15 @@ class Scene : public Object { return last_step_real_time_; } auto globals_node() const -> GlobalsNode* { return globals_node_; } - auto set_globals_node(GlobalsNode* node) -> void { globals_node_ = node; } + void set_globals_node(GlobalsNode* node) { globals_node_ = node; } private: GlobalsNode* globals_node_{}; // Current globals node (if any). std::unordered_map > player_nodes_; int64_t stream_id_{-1}; - Object::WeakRef output_stream_; + Object::WeakRef output_stream_; bool use_fixed_vr_overlay_{}; - Context context_; // Context we were made in. + base::ContextRef context_; // Context we were made in. millisecs_t time_{}; int64_t stepnum_{}; bool in_step_{}; @@ -106,6 +102,6 @@ class Scene : public Object { Object::Ref dynamics_; }; -} // namespace ballistica +} // namespace ballistica::scene_v1 -#endif // BALLISTICA_SCENE_SCENE_H_ +#endif // BALLISTICA_SCENE_V1_SUPPORT_SCENE_H_ diff --git a/src/ballistica/scene_v1/support/scene_v1_app_mode.cc b/src/ballistica/scene_v1/support/scene_v1_app_mode.cc new file mode 100644 index 00000000..c2fb1542 --- /dev/null +++ b/src/ballistica/scene_v1/support/scene_v1_app_mode.cc @@ -0,0 +1,1463 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" + +#include "ballistica/base/app/app_config.h" +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/audio/audio_source.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/networking/network_writer.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/support/plus_soft.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/scene_v1/connection/connection_set.h" +#include "ballistica/scene_v1/connection/connection_to_client_udp.h" +#include "ballistica/scene_v1/connection/connection_to_host.h" +#include "ballistica/scene_v1/node/globals_node.h" +#include "ballistica/scene_v1/python/scene_v1_python.h" +#include "ballistica/scene_v1/support/client_input_device.h" +#include "ballistica/scene_v1/support/client_input_device_delegate.h" +#include "ballistica/scene_v1/support/client_session_net.h" +#include "ballistica/scene_v1/support/client_session_replay.h" +#include "ballistica/scene_v1/support/host_session.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/json.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/ui_v1/support/root_ui.h" + +namespace ballistica::scene_v1 { + +const int kMaxChatMessages = 40; + +/// How long a kick vote lasts. +const int kKickVoteDuration = 30000; + +/// How long everyone has to wait to start a new kick vote after a failed one. +const int kKickVoteFailRetryDelay = 60000; + +/// Extra delay for the initiator of a failed vote. +const int kKickVoteFailRetryDelayInitiatorExtra = 120000; + +// Minimum clients that must be present for a kick vote to count. +// (for non-headless builds we require more votes since the host doesn't count +// but may be playing (in a 2on2 with 3 clients, don't want 2 clients able to +// kick). +// NOLINTNEXTLINE(cert-err58-cpp) +const int kKickVoteMinimumClients = (g_buildconfig.headless_build() ? 3 : 4); + +struct SceneV1AppMode::ScanResultsEntryPriv { + scene_v1::PlayerSpec player_spec; + std::string address; + uint32_t last_query_id{}; + millisecs_t last_contact_time{}; +}; + +base::InputDeviceDelegate* SceneV1AppMode::CreateInputDeviceDelegate( + base::InputDevice* device) { + // We create a special delegate for our special ClientInputDevice types; + // everything else gets our regular delegate. + if (auto* client_device = dynamic_cast(device)) { + auto* obj = Object::NewDeferred(); + obj->StoreClientDeviceInfo(client_device); + return obj; + } + return Object::NewDeferred(); +} + +// Go with 5 minute ban. +const int kKickBanSeconds = 5 * 60; + +bool SceneV1AppMode::InMainMenu() const { + HostSession* hostsession = + ContextRefSceneV1::FromAppForegroundContext().GetHostSession(); + return (hostsession && hostsession->is_main_menu()); +} + +static SceneV1AppMode* g_scene_v1_app_mode{}; + +void SceneV1AppMode::OnActivate() { Reset(); } + +void SceneV1AppMode::OnAppStart() { assert(g_base->InLogicThread()); } + +void SceneV1AppMode::OnAppShutdown() { + assert(g_base->InLogicThread()); + connections_->Shutdown(); +} + +void SceneV1AppMode::OnAppPause() { + assert(g_base->InLogicThread()); + + // App is going into background or whatnot. Kill any sockets/etc. + EndHostScanning(); +} + +void SceneV1AppMode::OnAppResume() { assert(g_base->InLogicThread()); } + +// Note: for now we're making our host-scan network calls directly from the +// logic thread. This is generally not a good idea since it appears that even in +// non-blocking mode they're still blocking for 3-4ms sometimes. But for now +// since this is only used minimally and only while in the UI I guess it's ok. +void SceneV1AppMode::HostScanCycle() { + assert(g_base->InLogicThread()); + + // We need to create a scanner socket - an ipv4 socket we can send out + // broadcast messages from. + if (scan_socket_ == -1) { + scan_socket_ = socket(AF_INET, SOCK_DGRAM, 0); + + if (scan_socket_ == -1) { + Log(LogLevel::kError, "Error opening scan socket: " + + g_core->platform->GetSocketErrorString() + + "."); + return; + } + + // Since this guy lives in the logic-thread we need it to not block. + if (!g_core->platform->SetSocketNonBlocking(scan_socket_)) { + Log(LogLevel::kError, "Error setting socket non-blocking."); + g_core->platform->CloseSocket(scan_socket_); + scan_socket_ = -1; + return; + } + + // Bind to whatever. + struct sockaddr_in serv_addr {}; + memset(&serv_addr, 0, sizeof(serv_addr)); + serv_addr.sin_family = AF_INET; + serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // NOLINT + serv_addr.sin_port = 0; // any + int result = + ::bind(scan_socket_, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); + if (result == 1) { + Log(LogLevel::kError, + "Error binding socket: " + g_core->platform->GetSocketErrorString() + + "."); + g_core->platform->CloseSocket(scan_socket_); + scan_socket_ = -1; + return; + } + + // Enable broadcast on the socket. + BA_SOCKET_SETSOCKOPT_VAL_TYPE op_val{1}; + result = setsockopt(scan_socket_, SOL_SOCKET, SO_BROADCAST, &op_val, + sizeof(op_val)); + + if (result != 0) { + Log(LogLevel::kError, "Error enabling broadcast for scan-socket: " + + g_core->platform->GetSocketErrorString() + + "."); + g_core->platform->CloseSocket(scan_socket_); + scan_socket_ = -1; + return; + } + } + + // Ok we've got a valid scanner socket. Now lets send out broadcast pings on + // all available networks. + std::vector addrs = g_core->platform->GetBroadcastAddrs(); + for (auto&& i : addrs) { + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(kDefaultPort); // NOLINT + addr.sin_addr.s_addr = htonl(i); // NOLINT + + // Include our query id (so we can sort out which responses come back + // quickest). + uint8_t data[5]; + data[0] = BA_PACKET_HOST_QUERY; + memcpy(data + 1, &next_scan_query_id_, 4); + BA_DEBUG_TIME_CHECK_BEGIN(sendto); + ssize_t result = sendto( + scan_socket_, reinterpret_cast(data), sizeof(data), + 0, reinterpret_cast(&addr), sizeof(addr)); + BA_DEBUG_TIME_CHECK_END(sendto, 10); + if (result == -1) { + int err = g_core->platform->GetSocketError(); + switch (err) { // NOLINT(hicpp-multiway-paths-covered) + case ENETUNREACH: + break; + default: + Log(LogLevel::kError, "Error on scanSocket sendto: " + + g_core->platform->GetSocketErrorString()); + } + } + } + next_scan_query_id_++; + + // ..and see if any responses came in from previous sends. + char buffer[256]; + sockaddr_storage from{}; + socklen_t from_size = sizeof(from); + while (true) { + BA_DEBUG_TIME_CHECK_BEGIN(recvfrom); + ssize_t result = recvfrom(scan_socket_, buffer, sizeof(buffer), 0, + reinterpret_cast(&from), &from_size); + BA_DEBUG_TIME_CHECK_END(recvfrom, 10); + + if (result == -1) { + int err = g_core->platform->GetSocketError(); + switch (err) { // NOLINT(hicpp-multiway-paths-covered) + case EWOULDBLOCK: + break; + default: + Log(LogLevel::kError, "Error: recvfrom error: " + + g_core->platform->GetSocketErrorString()); + break; + } + break; + } + + if (result > 2 && buffer[0] == BA_PACKET_HOST_QUERY_RESPONSE) { + // Size should be between 13 and 366 (1 byte type, 4 byte query_id, 4 + // byte protocol_id, 1 byte id_len, 1 byte player_spec_len, 1-100 byte + // id, 1-255 byte player-spec). + if (result >= 14 && result <= 366) { + uint32_t protocol_version; + uint32_t query_id; + + memcpy(&query_id, buffer + 1, 4); + memcpy(&protocol_version, buffer + 5, 4); + auto id_len = static_cast(buffer[9]); + auto player_spec_len = static_cast(buffer[10]); + + if (id_len > 0 && id_len <= 100 && player_spec_len > 0 + && player_spec_len <= 255 + && (11 + id_len + player_spec_len == result)) { + char id[101]; + char player_spec_str[256]; + memcpy(id, buffer + 11, id_len); + memcpy(player_spec_str, buffer + 11 + id_len, player_spec_len); + + id[id_len] = 0; + player_spec_str[player_spec_len] = 0; + + // Add or modify an entry for this. + { + std::scoped_lock lock(scan_results_mutex_); + + // Ignore if it looks like its us. + if (id != g_base->GetAppInstanceUUID()) { + std::string key = id; + auto i = scan_results_.find(key); + + // Make a new entry if its not there. + bool do_update_entry = (i == scan_results_.end() + || i->second.last_query_id != query_id); + if (do_update_entry) { + ScanResultsEntryPriv& entry(scan_results_[key]); + entry.player_spec = scene_v1::PlayerSpec(player_spec_str); + char buffer2[256]; + entry.address = inet_ntop( + AF_INET, + &((reinterpret_cast(&from))->sin_addr), + buffer2, sizeof(buffer2)); + entry.last_query_id = query_id; + entry.last_contact_time = g_core->GetAppTimeMillisecs(); + } + } + PruneScanResults(); + } + } else { + Log(LogLevel::kError, + "Got invalid BA_PACKET_HOST_QUERY_RESPONSE packet"); + } + } else { + Log(LogLevel::kError, + "Got invalid BA_PACKET_HOST_QUERY_RESPONSE packet"); + } + } + } +} + +void SceneV1AppMode::EndHostScanning() { + if (scan_socket_ != -1) { + g_core->platform->CloseSocket(scan_socket_); + scan_socket_ = -1; + } +} + +void SceneV1AppMode::PruneScanResults() { + millisecs_t t = g_core->GetAppTimeMillisecs(); + auto i = scan_results_.begin(); + while (i != scan_results_.end()) { + auto i_next = i; + i_next++; + if (t - i->second.last_contact_time > 3000) { + scan_results_.erase(i); + } + i = i_next; + } +} + +auto SceneV1AppMode::GetScanResults() + -> std::vector { + std::vector results; + results.resize(scan_results_.size()); + { + std::scoped_lock lock(scan_results_mutex_); + int out_num = 0; + for (auto&& i : scan_results_) { + ScanResultsEntryPriv& in(i.second); + ScanResultsEntry& out(results[out_num]); + out.display_string = in.player_spec.GetDisplayString(); + out.address = in.address; + out_num++; + } + PruneScanResults(); + } + return results; +} + +auto SceneV1AppMode::GetActive() -> SceneV1AppMode* { + // Note: this gets called by non-logic threads, and not + // doing any locking here so bg thread callers should + // keep in mind that app-mode may change under them. + + // Otherwise return our singleton only if it is current. + if (g_base->app_mode == g_scene_v1_app_mode) { + return g_scene_v1_app_mode; + } + return nullptr; +} + +bool SceneV1AppMode::HasConnectionToHost() const { + return connections()->has_connection_to_host(); +} + +millisecs_t SceneV1AppMode::LastClientJoinTime() const { + return last_connection_to_client_join_time(); +} + +bool SceneV1AppMode::HasConnectionToClients() const { + return connections()->HasConnectionToClients(); +} + +auto SceneV1AppMode::GetActiveOrWarn() -> SceneV1AppMode* { + auto* val{GetActive()}; + if (val == nullptr) { + Log(LogLevel::kWarning, + "Attempting to access SceneAppMode while it is inactive."); + } + return val; +} + +auto SceneV1AppMode::GetActiveOrThrow() -> SceneV1AppMode* { + auto* val{GetActive()}; + if (val == nullptr) { + throw Exception("Attempting to access SceneAppMode while it is inactive."); + } + return val; +} + +auto SceneV1AppMode::GetActiveOrFatal() -> SceneV1AppMode* { + auto* val{GetActive()}; + if (val == nullptr) { + FatalError("Attempting to access SceneAppMode while it is inactive."); + } + return val; +} + +auto SceneV1AppMode::GetSingleton() -> SceneV1AppMode* { + // TODO(ericf): Turn this back on once we're creating in logic thread. + + // assert(g_base->InLogicThread()); // Can relax this if need be. + if (g_scene_v1_app_mode == nullptr) { + g_scene_v1_app_mode = new SceneV1AppMode(); + } + return g_scene_v1_app_mode; +} + +SceneV1AppMode::SceneV1AppMode() + : game_roster_(cJSON_CreateArray()), + connections_(std::make_unique()) {} + +void SceneV1AppMode::HandleIncomingUDPPacket(const std::vector& data, + const SockAddr& addr) { + // Just forward it along to our connection-set to handle. + connections()->HandleIncomingUDPPacket(data, addr); +} + +auto SceneV1AppMode::HandleJSONPing(const std::string& data_str) + -> std::string { + // Note to self - this is called in a non-logic thread. + cJSON* data = cJSON_Parse(data_str.c_str()); + if (data == nullptr) { + return ""; + } + cJSON_Delete(data); + + // Ok lets include some basic info that might be pertinent to someone pinging + // us. Currently that includes our current/max connection count. + char buffer[256]; + snprintf(buffer, sizeof(buffer), R"({"b":%d,"ps":%d,"psmx":%d})", + kEngineBuildNumber, public_party_size(), public_party_max_size()); + return buffer; +} + +void SceneV1AppMode::SetGameRoster(cJSON* r) { + if (game_roster_ != nullptr) { + cJSON_Delete(game_roster_); + } + game_roster_ = r; +} + +auto SceneV1AppMode::GetPartySize() const -> int { + assert(g_base->InLogicThread()); + assert(game_roster_ != nullptr); + return cJSON_GetArraySize(game_roster_); +} + +void SceneV1AppMode::StepDisplayTime() { + assert(g_base->InLogicThread()); + + auto startms{core::CorePlatform::GetCurrentMillisecs()}; + millisecs_t app_time = g_core->GetAppTimeMillisecs(); + g_core->platform->SetDebugKey("LastUpdateTime", std::to_string(startms)); + in_update_ = true; + + // NOTE: We now simply drive our old milliseconds time using display-time. + legacy_display_time_millisecs_ = + static_cast(g_base->logic->display_time() * 1000.0); + + // Calc our integer increment using our previous millisecs conversion. + // (don't want to simply round g_logic->display_time_increment() each time + // since that would accumulate precision loss; ie: 16.6 would round up to 17 + // each time). + millisecs_t legacy_display_time_millisecs_inc; + if (legacy_display_time_millisecs_prev_ < 0) { + // Convert directly *only* the first time when we don't have prev available. + legacy_display_time_millisecs_inc = static_cast( + g_base->logic->display_time_increment() * 1000.0); + + } else { + legacy_display_time_millisecs_inc = + legacy_display_time_millisecs_ - legacy_display_time_millisecs_prev_; + } + legacy_display_time_millisecs_prev_ = legacy_display_time_millisecs_; + + UpdateKickVote(); + + // Send the game roster to our clients if it's changed recently. + if (game_roster_dirty_) { + if (app_time > last_game_roster_send_time_ + 2500) { + // Now send it to all connected clients. + std::vector msg = GetGameRosterMessage(); + for (auto&& c : connections()->GetConnectionsToClients()) { + c->SendReliableMessage(msg); + } + game_roster_dirty_ = false; + last_game_roster_send_time_ = app_time; + } + } + + connections_->Update(); + + // Update all of our sessions. + for (auto&& i : sessions_) { + if (!i.Exists()) { + continue; + } + // Pass our old int milliseconds time vals for legacy purposes + // along with the newer exact ones for anyone who wants to use them. + // (ideally at some point we can pass neither of these and anyone who + // needs this can just use g_logic->display_time() directly). + i->Update(static_cast(legacy_display_time_millisecs_inc), + g_base->logic->display_time_increment()); + } + + // Go ahead and prune dead ones. + PruneSessions(); + + in_update_ = false; + + // Report excessively long updates. + if (g_core->debug_timing && app_time >= next_long_update_report_time_) { + auto duration{core::CorePlatform::GetCurrentMillisecs() - startms}; + + // Complain when our full update takes longer than 1/60th second. + if (duration > (1000 / 60)) { + Log(LogLevel::kInfo, "Logic::StepDisplayTime update took too long (" + + std::to_string(duration) + " ms)."); + + // Limit these if we want (not doing so for now). + next_long_update_report_time_ = app_time; + } + } +} + +auto SceneV1AppMode::GetGameRosterMessage() -> std::vector { + // This message is simply a flattened json string of our roster (including + // terminating char). + char* s = cJSON_PrintUnformatted(game_roster_); + auto s_len = strlen(s); + std::vector msg(1 + s_len + 1); + msg[0] = BA_MESSAGE_PARTY_ROSTER; + memcpy(&(msg[1]), s, s_len + 1); + free(s); + + return msg; +} + +base::ContextRef SceneV1AppMode::GetForegroundContext() { + Session* s = GetForegroundSession(); + if (s) { + return s->GetForegroundContext(); + } else { + return {}; + } +} + +void SceneV1AppMode::UpdateGameRoster() { + assert(g_base->InLogicThread()); + + assert(game_roster_ != nullptr); + if (game_roster_ != nullptr) { + cJSON_Delete(game_roster_); + } + + // Our party-roster is just a json array of dicts containing player-specs. + game_roster_ = cJSON_CreateArray(); + + int total_party_size = 1; // include ourself here.. + + // Add ourself first (that's currently how they know we're the party leader) + // ..but only if we have a connected client (otherwise our party is + // considered 'empty'). + + // UPDATE: starting with our big ui revision we'll always include ourself + // here. + bool include_self = (connections()->GetConnectedClientCount() > 0); + +#if BA_TOOLBAR_TEST + include_self = true; +#endif // BA_TOOLBAR_TEST + + if (auto* hs = dynamic_cast(GetForegroundSession())) { + // Add our host-y self. + if (include_self) { + cJSON* client_dict = cJSON_CreateObject(); + cJSON_AddItemToObject( + client_dict, "spec", + cJSON_CreateString( + PlayerSpec::GetAccountPlayerSpec().GetSpecString().c_str())); + + // Add our list of local players. + cJSON* player_array = cJSON_CreateArray(); + for (auto&& p : hs->players()) { + auto* delegate = p->input_device_delegate(); + if (delegate == nullptr || !delegate->InputDeviceExists()) { + BA_LOG_ONCE(LogLevel::kWarning, + "Found player with no/invalid input-device-delegate in " + "UpdateGameRoster."); + continue; + } + + // Add some basic info for each local player (only ones with real + // names though; don't wanna send , etc). + if (p->accepted() && p->name_is_real() && !delegate->IsRemoteClient()) { + cJSON* player_dict = cJSON_CreateObject(); + cJSON_AddItemToObject(player_dict, "n", + cJSON_CreateString(p->GetName().c_str())); + cJSON_AddItemToObject(player_dict, "nf", + cJSON_CreateString(p->GetName(true).c_str())); + cJSON_AddItemToObject(player_dict, "i", cJSON_CreateNumber(p->id())); + cJSON_AddItemToArray(player_array, player_dict); + } + } + cJSON_AddItemToObject(client_dict, "p", player_array); + cJSON_AddItemToObject( + client_dict, "i", + cJSON_CreateNumber(-1)); // -1 client_id means we're the host. + cJSON_AddItemToArray(game_roster_, client_dict); + } + + // Add all connected clients. + for (auto&& i : connections()->connections_to_clients()) { + if (i.second->can_communicate()) { + cJSON* client_dict = cJSON_CreateObject(); + cJSON_AddItemToObject( + client_dict, "spec", + cJSON_CreateString(i.second->peer_spec().GetSpecString().c_str())); + + // Add their list of players. + cJSON* player_array = cJSON_CreateArray(); + + // Include all players that are remote and coming from this same + // client connection. + for (auto&& p : hs->players()) { + auto* delegate = p->input_device_delegate(); + if (delegate == nullptr || !delegate->InputDeviceExists()) { + // Logged this above; would be redundant here. + continue; + } + + if (p->accepted() && p->name_is_real() + && delegate->IsRemoteClient()) { + auto* client_delegate = + static_cast(delegate); + assert(dynamic_cast(delegate) + == client_delegate); + ConnectionToClient* ctc = client_delegate->connection_to_client(); + + // Add some basic info for each remote player. + if (ctc != nullptr && ctc == i.second.Get()) { + cJSON* player_dict = cJSON_CreateObject(); + cJSON_AddItemToObject(player_dict, "n", + cJSON_CreateString(p->GetName().c_str())); + cJSON_AddItemToObject( + player_dict, "nf", + cJSON_CreateString(p->GetName(true).c_str())); + cJSON_AddItemToObject(player_dict, "i", + cJSON_CreateNumber(p->id())); + cJSON_AddItemToArray(player_array, player_dict); + } + } + } + cJSON_AddItemToObject(client_dict, "p", player_array); + cJSON_AddItemToObject(client_dict, "i", + cJSON_CreateNumber(i.second->id())); + cJSON_AddItemToArray(game_roster_, client_dict); + total_party_size += 1; + } + } + } + + // Keep the Python layer informed on our number of connections; it may want + // to pass the info along to the master server if we're hosting a public + // party. + SetPublicPartySize(total_party_size); + + // Mark the roster as dirty so we know we need to send it to everyone soon. + game_roster_dirty_ = true; +} + +void SceneV1AppMode::UpdateKickVote() { + if (!kick_vote_in_progress_) { + return; + } + ConnectionToClient* kick_vote_starter = kick_vote_starter_.Get(); + ConnectionToClient* kick_vote_target = kick_vote_target_.Get(); + + // If the target is no longer with us, silently end. + if (kick_vote_target == nullptr) { + kick_vote_in_progress_ = false; + return; + } + millisecs_t current_time{g_core->GetAppTimeMillisecs()}; + int total_client_count = 0; + int yes_votes = 0; + int no_votes = 0; + + // Tally current votes for connected clients; if anything has changed, print + // the update and possibly perform the kick. + for (ConnectionToClient* client : connections()->GetConnectionsToClients()) { + ++total_client_count; + if (client->kick_voted()) { + if (client->kick_vote_choice()) { + ++yes_votes; + } else { + ++no_votes; + } + } + } + bool vote_failed = false; + + // If we've fallen below the minimum necessary voters or time has run out, + // fail. + if (total_client_count < kKickVoteMinimumClients) { + vote_failed = true; + } + if (current_time > kick_vote_end_time_) { + vote_failed = true; + } + + if (vote_failed) { + connections()->SendScreenMessageToClients(R"({"r":"kickVoteFailedText"})", + 1, 1, 0); + kick_vote_in_progress_ = false; + + // Disallow kicking for a while for everyone.. but ESPECIALLY so for the guy + // who launched the failed vote. + for (ConnectionToClient* client : + connections()->GetConnectionsToClients()) { + millisecs_t delay = kKickVoteFailRetryDelay; + if (client == kick_vote_starter) { + delay += kKickVoteFailRetryDelayInitiatorExtra; + } + client->set_next_kick_vote_allow_time( + std::max(client->next_kick_vote_allow_time(), current_time + delay)); + } + } else { + int votes_required; + switch (total_client_count) { + case 1: + case 2: + votes_required = 2; // Shouldn't actually be possible. + break; + case 3: + votes_required = g_core->HeadlessMode() ? 2 : 3; + break; + case 4: + votes_required = 3; + break; + case 5: + votes_required = g_core->HeadlessMode() ? 3 : 4; + break; + case 6: + votes_required = 4; + break; + case 7: + votes_required = g_core->HeadlessMode() ? 4 : 5; + break; + default: + votes_required = total_client_count - 3; + break; + } + int votes_needed = votes_required - yes_votes; + if (votes_needed <= 0) { + // ZOMG the vote passed; perform the kick. + connections()->SendScreenMessageToClients( + R"({"r":"kickOccurredText","s":[["${NAME}",)" + + Utils::GetJSONString(kick_vote_target->GetCombinedSpec() + .GetDisplayString() + .c_str()) + + "]]}", + 1, 1, 0); + kick_vote_in_progress_ = false; + connections()->DisconnectClient(kick_vote_target->id(), kKickBanSeconds); + + } else if (votes_needed != last_kick_votes_needed_) { + last_kick_votes_needed_ = votes_needed; + connections()->SendScreenMessageToClients( + R"({"r":"votesNeededText","s":[["${NUMBER}",")" + + std::to_string(votes_needed) + "\"]]}", + 1, 1, 0); + } + } +} + +void SceneV1AppMode::StartKickVote(ConnectionToClient* starter, + ConnectionToClient* target) { + // Restrict votes per client. + millisecs_t current_time = g_core->GetAppTimeMillisecs(); + + if (starter == target) { + // Don't let anyone kick themselves. + starter->SendScreenMessage(R"({"r":"kickVoteCantKickSelfText",)" + R"("f":"kickVoteFailedText"})", + 1, 0, 0); + } else if (target->IsAdmin()) { + // Admins are immune to kicking + starter->SendScreenMessage(R"({"r":"kickVoteCantKickAdminText",)" + R"("f":"kickVoteFailedText"})", + 1, 0, 0); + } else if (starter->IsAdmin()) { + // Admin doing the kicking succeeds instantly. + connections()->SendScreenMessageToClients( + R"({"r":"kickOccurredText","s":[["${NAME}",)" + + Utils::GetJSONString( + target->GetCombinedSpec().GetDisplayString().c_str()) + + "]]}", + 1, 1, 0); + connections()->DisconnectClient(target->id(), kKickBanSeconds); + starter->SendScreenMessage(R"({"r":"kickVoteCantKickAdminText",)" + R"("f":"kickVoteFailedText"})", + 1, 0, 0); + } else if (!kick_voting_enabled_) { + // No kicking otherwise if its disabled. + starter->SendScreenMessage(R"({"r":"kickVotingDisabledText",)" + R"("f":"kickVoteFailedText"})", + 1, 0, 0); + } else if (kick_vote_in_progress_) { + // Vote in progress error. + starter->SendScreenMessage(R"({"r":"voteInProgressText"})", 1, 0, 0); + } else if (connections()->GetConnectedClientCount() + < kKickVoteMinimumClients) { + // There's too few clients to effectively vote. + starter->SendScreenMessage(R"({"r":"kickVoteFailedNotEnoughVotersText",)" + R"("f":"kickVoteFailedText"})", + 1, 0, 0); + } else if (current_time < starter->next_kick_vote_allow_time()) { + // Not yet allowed error. + starter->SendScreenMessage( + R"({"r":"voteDelayText","s":[["${NUMBER}",")" + + std::to_string(std::max( + millisecs_t{1}, + (starter->next_kick_vote_allow_time() - current_time) / 1000)) + + "\"]]}", + 1, 0, 0); + } else { + std::vector connected_clients = + connections()->GetConnectionsToClients(); + + // Ok, kick off a vote.. (send the question and instructions to everyone + // except the starter and the target). + for (auto&& client : connected_clients) { + if (client != starter && client != target) { + client->SendScreenMessage( + R"({"r":"kickQuestionText","s":[["${NAME}",)" + + Utils::GetJSONString( + target->GetCombinedSpec().GetDisplayString().c_str()) + + "]]}", + 1, 1, 0); + client->SendScreenMessage(R"({"r":"kickWithChatText","s":)" + R"([["${YES}","'1'"],["${NO}","'0'"]]})", + 1, 1, 0); + } else { + // For the kicker/kickee, simply print that a kick vote has been + // started. + client->SendScreenMessage( + R"({"r":"kickVoteStartedText","s":[["${NAME}",)" + + Utils::GetJSONString( + target->GetCombinedSpec().GetDisplayString().c_str()) + + "]]}", + 1, 1, 0); + } + } + kick_vote_end_time_ = current_time + kKickVoteDuration; + kick_vote_in_progress_ = true; + last_kick_votes_needed_ = -1; // make sure we print starting num + + // Keep track of who started the vote. + kick_vote_starter_ = starter; + kick_vote_target_ = target; + + // Reset votes for all connected clients. + for (ConnectionToClient* client : + connections()->GetConnectionsToClients()) { + if (client == starter) { + client->set_kick_voted(true); + client->set_kick_vote_choice(true); + } else { + client->set_kick_voted(false); + } + } + } +} + +void SceneV1AppMode::SetForegroundScene(Scene* sg) { + assert(g_base->InLogicThread()); + if (foreground_scene_.Get() != sg) { + foreground_scene_ = sg; + + // If this scene has a globals-node, put it in charge of stuff. + if (GlobalsNode* g = sg->globals_node()) { + g->SetAsForeground(); + } + } +} + +auto SceneV1AppMode::GetNetworkDebugString() -> std::string { + char net_info_str[128]; + int64_t in_count = 0; + int64_t in_size = 0; + int64_t in_size_compressed = 0; + int64_t outCount = 0; + int64_t out_size = 0; + int64_t out_size_compressed = 0; + int64_t resends = 0; + int64_t resends_size = 0; + bool show = false; + + // Add in/out data for any host connection. + if (ConnectionToHost* connection_to_host = + connections()->connection_to_host()) { + if (connection_to_host->can_communicate()) show = true; + in_size += connection_to_host->GetBytesInPerSecond(); + in_size_compressed += connection_to_host->GetBytesInPerSecondCompressed(); + in_count += connection_to_host->GetMessagesInPerSecond(); + out_size += connection_to_host->GetBytesOutPerSecond(); + out_size_compressed += connection_to_host->GetBytesOutPerSecondCompressed(); + outCount += connection_to_host->GetMessagesOutPerSecond(); + resends += connection_to_host->GetMessageResendsPerSecond(); + resends_size += connection_to_host->GetBytesResentPerSecond(); + } else { + int connected_count = 0; + for (auto&& i : connections()->connections_to_clients()) { + ConnectionToClient* client = i.second.Get(); + if (client->can_communicate()) { + show = true; + connected_count += 1; + } + in_size += client->GetBytesInPerSecond(); + in_size_compressed += client->GetBytesInPerSecondCompressed(); + in_count += client->GetMessagesInPerSecond(); + out_size += client->GetBytesOutPerSecond(); + out_size_compressed += client->GetBytesOutPerSecondCompressed(); + outCount += client->GetMessagesOutPerSecond(); + resends += client->GetMessageResendsPerSecond(); + resends_size += client->GetBytesResentPerSecond(); + } + } + if (!show) { + return ""; + } + snprintf(net_info_str, sizeof(net_info_str), + "in: %d/%d/%d\nout: %d/%d/%d\nrpt: %d/%d", + static_cast_check_fit(in_size), + static_cast_check_fit(in_size_compressed), + static_cast_check_fit(in_count), + static_cast_check_fit(out_size), + static_cast_check_fit(out_size_compressed), + static_cast_check_fit(outCount), + static_cast_check_fit(resends_size), + static_cast_check_fit(resends)); + return net_info_str; +} +std::string SceneV1AppMode::GetPingString() { + float ping{}; + char ping_str[32]; + if (ConnectionToHost* connection_to_host = + connections()->connection_to_host()) { + if (connection_to_host->can_communicate()) { + ping = connection_to_host->current_ping(); + snprintf(ping_str, sizeof(ping_str), "%.0f ms", ping); + return ping_str; + } + } + return ""; +} + +void SceneV1AppMode::CleanUpBeforeConnectingToHost() { + // We can't have connected clients and a host-connection at the same time. + // Make a minimal attempt to disconnect any client connections we have, but + // get them off the list immediately. + // FIXME: Should we have a 'purgatory' for dying client connections?.. + // (they may not get the single 'go away' packet we send here) + connections_->ForceDisconnectClients(); + + // Also make sure our public party state is off; this will inform the server + // that it should not be handing out our address to anyone. + SetPublicPartyEnabled(false); +} + +void SceneV1AppMode::LaunchHostSession(PyObject* session_type_obj, + base::BenchmarkType benchmark_type) { + if (in_update_) { + throw Exception( + "can't call host_session() from within session update; use " + "babase.pushcall()"); + } + + assert(g_base->InLogicThread()); + + connections_->PrepareForLaunchHostSession(); + + // Don't want to pick up any old stuff in here. + base::ScopedSetContext ssc(nullptr); + + // This should kill any current session and get us back to a blank slate. + Reset(); + + Object::WeakRef old_foreground_session(foreground_session_); + try { + // Create the new session. + auto s(Object::New(session_type_obj)); + s->set_benchmark_type(benchmark_type); + sessions_.emplace_back(s); + + // It should have set itself as FG. + assert(foreground_session_ == s); + } catch (const std::exception& e) { + // If it failed, restore the previous session context and re-throw the + // exception. + SetForegroundSession(old_foreground_session.Get()); + throw Exception(std::string("HostSession failed: ") + e.what()); + } +} + +void SceneV1AppMode::LaunchReplaySession(const std::string& file_name) { + if (in_update_) + throw Exception( + "can't launch a session from within a session update; use " + "babase.pushcall()"); + + assert(g_base->InLogicThread()); + + // Don't want to pick up any old stuff in here. + base::ScopedSetContext ssc(nullptr); + + // This should kill any current session and get us back to a blank slate. + Reset(); + + // Create the new session. + Object::WeakRef old_foreground_session(foreground_session_); + try { + auto s(Object::New(file_name)); + sessions_.push_back(s); + + // It should have set itself as FG. + assert(foreground_session_ == s); + } catch (const std::exception& e) { + // If it failed, restore the previous current session and re-throw the + // exception. + SetForegroundSession(old_foreground_session.Get()); + throw Exception(std::string("HostSession failed: ") + e.what()); + } +} + +void SceneV1AppMode::LaunchClientSession() { + if (in_update_) { + throw Exception( + "can't launch a session from within a session update; use " + "babase.pushcall()"); + } + assert(g_base->InLogicThread()); + + // Don't want to pick up any old stuff in here. + base::ScopedSetContext ssc(nullptr); + + // This should kill any current session and get us back to a blank slate. + Reset(); + + // Create the new session. + Object::WeakRef old_foreground_session(foreground_session_); + try { + auto s(Object::New()); + sessions_.push_back(s); + + // It should have set itself as FG. + assert(foreground_session_ == s); + } catch (const std::exception& e) { + // If it failed, restore the previous current session and re-throw. + SetForegroundSession(old_foreground_session.Get()); + throw Exception(std::string("HostSession failed: ") + e.what()); + } +} + +// Reset to a blank slate. +void SceneV1AppMode::Reset() { + assert(g_base); + assert(g_base->InLogicThread()); + + // Tear down our existing session. + foreground_session_.Clear(); + PruneSessions(); + + // If all is well our sessions should all be dead. + if (g_core->session_count != 0) { + Log(LogLevel::kError, "Session-count is non-zero (" + + std::to_string(g_core->session_count) + + ") on Logic::Reset."); + } + + g_scene_v1->Reset(); + g_base->ui->Reset(); + g_base->input->Reset(); + g_base->graphics->Reset(); + g_base->python->Reset(); + g_base->audio->Reset(); +} + +void SceneV1AppMode::ChangeGameSpeed(int offs) { + assert(g_base->InLogicThread()); + + // If we're in a replay session, adjust playback speed there. + if (dynamic_cast(GetForegroundSession())) { + int old_speed = replay_speed_exponent(); + SetReplaySpeedExponent(replay_speed_exponent() + offs); + if (old_speed != replay_speed_exponent()) { + ScreenMessage( + "{\"r\":\"watchWindow.playbackSpeedText\"," + "\"s\":[[\"${SPEED}\",\"" + + std::to_string(replay_speed_mult()) + "\"]]}"); + } + return; + } + // Otherwise, in debug builds, we allow speeding/slowing anything. + if (g_buildconfig.debug_build()) { + debug_speed_exponent_ += offs; + debug_speed_mult_ = powf(2.0f, static_cast(debug_speed_exponent_)); + ScreenMessage("DEBUG GAME SPEED TO " + std::to_string(debug_speed_mult_)); + Session* s = GetForegroundSession(); + if (s) { + s->DebugSpeedMultChanged(); + } + } +} + +void SceneV1AppMode::OnScreenSizeChange() { + if (Session* session = GetForegroundSession()) { + session->ScreenSizeChanged(); + } +} + +// Called by a newly made Session instance to set itself as the current +// session. +void SceneV1AppMode::SetForegroundSession(Session* s) { + assert(g_base->InLogicThread()); + foreground_session_ = s; +} + +void SceneV1AppMode::LocalDisplayChatMessage( + const std::vector& buffer) { + // 1 type byte, 1 spec-len byte, 1 or more spec chars, 0 or more msg chars. + if (buffer.size() > 3) { + size_t spec_len = buffer[1]; + if (spec_len > 0 && spec_len + 2 <= buffer.size()) { + size_t msg_len = buffer.size() - spec_len - 2; + std::vector b1(spec_len + 1); + memcpy(&(b1[0]), &(buffer[2]), spec_len); + b1[spec_len] = 0; + std::vector b2(msg_len + 1); + if (msg_len > 0) { + memcpy(&(b2[0]), &(buffer[2 + spec_len]), msg_len); + } + b2[msg_len] = 0; + + std::string final_message = + PlayerSpec(b1.data()).GetDisplayString() + ": " + b2.data(); + + // Store it locally. + chat_messages_.push_back(final_message); + while (chat_messages_.size() > kMaxChatMessages) { + chat_messages_.pop_front(); + } + + // Show it on the screen if they don't have their chat window open + // (and don't have chat muted). + if (!g_base->ui->root_ui()->party_window_open()) { + if (!chat_muted_) { + ScreenMessage(final_message, {0.7f, 1.0f, 0.7f}); + } + } else { + // Party window is open - notify it that there's a new message. + g_scene_v1->python->HandleLocalChatMessage(final_message); + } + if (!chat_muted_) { + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kTap)); + } + } + } +} + +void SceneV1AppMode::ApplyAppConfig() { + // Kick-idle-players setting (hmm is this still relevant?). + auto* host_session = dynamic_cast(foreground_session_.Get()); + kick_idle_players_ = + g_base->app_config->Resolve(base::AppConfig::BoolID::kKickIdlePlayers); + if (host_session) { + host_session->SetKickIdlePlayers(kick_idle_players_); + } + + chat_muted_ = + g_base->app_config->Resolve(base::AppConfig::BoolID::kChatMuted); + + idle_exit_minutes_ = g_base->app_config->Resolve( + base::AppConfig::OptionalFloatID::kIdleExitMinutes); +} + +void SceneV1AppMode::PruneSessions() { + bool have_dead_session = false; + for (auto&& i : sessions_) { + if (i.Exists()) { + // If this session is no longer foreground and is ready to die, kill it. + if (i.Exists() && i.Get() != foreground_session_.Get()) { + try { + i.Clear(); + } catch (const std::exception& e) { + Log(LogLevel::kError, + "Exception killing Session: " + std::string(e.what())); + } + have_dead_session = true; + } + } else { + have_dead_session = true; + } + } + if (have_dead_session) { + std::vector > live_list; + for (auto&& i : sessions_) { + if (i.Exists()) { + live_list.push_back(i); + } + } + sessions_.swap(live_list); + } +} + +void SceneV1AppMode::LanguageChanged() { + if (Session* session = GetForegroundSession()) { + session->LanguageChanged(); + } +} + +void SceneV1AppMode::SetReplaySpeedExponent(int val) { + replay_speed_exponent_ = std::min(3, std::max(-3, val)); + replay_speed_mult_ = powf(2.0f, static_cast(replay_speed_exponent_)); +} + +void SceneV1AppMode::SetDebugSpeedExponent(int val) { + debug_speed_exponent_ = val; + debug_speed_mult_ = powf(2.0f, static_cast(debug_speed_exponent_)); + + Session* s = GetForegroundSession(); + if (s) s->DebugSpeedMultChanged(); +} + +void SceneV1AppMode::SetPublicPartyEnabled(bool val) { + assert(g_base->InLogicThread()); + if (val == public_party_enabled_) { + return; + } + public_party_enabled_ = val; + g_base->Plus()->PushPublicPartyState(); +} + +void SceneV1AppMode::SetPublicPartySize(int count) { + assert(g_base->InLogicThread()); + if (count == public_party_size_) { + return; + } + public_party_size_ = count; + + // Push our new state to the server *ONLY* if public-party is turned on + // (wasteful otherwise). + if (public_party_enabled_) { + g_base->Plus()->PushPublicPartyState(); + } +} + +void SceneV1AppMode::SetPublicPartyQueueEnabled(bool enabled) { + assert(g_base->InLogicThread()); + if (enabled == public_party_queue_enabled_) { + return; + } + public_party_queue_enabled_ = enabled; + + // Push our new state to the server *ONLY* if public-party is turned on + // (wasteful otherwise). + if (public_party_enabled_) { + g_base->Plus()->PushPublicPartyState(); + } +} + +void SceneV1AppMode::SetPublicPartyMaxSize(int count) { + assert(g_base->InLogicThread()); + if (count == public_party_max_size_) { + return; + } + public_party_max_size_ = count; + + // Push our new state to the server *ONLY* if public-party is turned on + // (wasteful otherwise). + if (public_party_enabled_) { + g_base->Plus()->PushPublicPartyState(); + } +} + +void SceneV1AppMode::SetPublicPartyName(const std::string& name) { + assert(g_base->InLogicThread()); + if (name == public_party_name_) { + return; + } + public_party_name_ = name; + + // Push our new state to the server *ONLY* if public-party is turned on + // (wasteful otherwise). + if (public_party_enabled_) { + g_base->Plus()->PushPublicPartyState(); + } +} + +void SceneV1AppMode::SetPublicPartyStatsURL(const std::string& url) { + assert(g_base->InLogicThread()); + if (url == public_party_stats_url_) { + return; + } + public_party_stats_url_ = url; + + // Push our new state to the server *ONLY* if public-party is turned on + // (wasteful otherwise). + if (public_party_enabled_) { + g_base->Plus()->PushPublicPartyState(); + } +} + +void SceneV1AppMode::GraphicsQualityChanged(base::GraphicsQuality quality) { + for (auto&& i : sessions_) { + if (!i.Exists()) { + continue; + } + i->GraphicsQualityChanged(quality); + } +} + +void SceneV1AppMode::SetPublicPartyPlayerCount(int count) { + assert(g_base->InLogicThread()); + if (count == public_party_player_count_) { + return; + } + public_party_player_count_ = count; + + // Push our new state to the server *ONLY* if public-party is turned on + // (wasteful otherwise). + if (public_party_enabled_) { + g_base->Plus()->PushPublicPartyState(); + } +} + +auto SceneV1AppMode::DoesWorldFillScreen() -> bool { + if (auto* session = GetForegroundSession()) { + return session->DoesFillScreen(); + } + return false; +} + +void SceneV1AppMode::DrawWorld(base::FrameDef* frame_def) { + AppMode::DrawWorld(frame_def); + + if (auto* session = GetForegroundSession()) { + session->Draw(frame_def); + frame_def->set_benchmark_type(session->benchmark_type()); + } +} + +auto SceneV1AppMode::ShouldAnnouncePartyJoinsAndLeaves() -> bool { + assert(g_base->InLogicThread()); + + // At the moment we don't announce these for public internet parties.. (too + // much noise). + return !public_party_enabled(); +} + +auto SceneV1AppMode::IsPlayerBanned(const PlayerSpec& spec) -> bool { + millisecs_t current_time = g_core->GetAppTimeMillisecs(); + + // Now is a good time to prune no-longer-banned specs. + while (!banned_players_.empty() + && banned_players_.front().first < current_time) { + banned_players_.pop_front(); + } + // NOLINTNEXTLINE(readability-use-anyofallof) + for (auto&& test_spec : banned_players_) { + if (test_spec.second == spec) { + return true; + } + } + return false; +} + +void SceneV1AppMode::BanPlayer(const PlayerSpec& spec, millisecs_t duration) { + banned_players_.emplace_back(g_core->GetAppTimeMillisecs() + duration, spec); +} + +void SceneV1AppMode::HandleQuitOnIdle() { + if (idle_exit_minutes_) { + auto idle_seconds{static_cast(g_base->input->input_idle_time()) + * 0.001f}; + if (!idle_exiting_ && idle_seconds > (idle_exit_minutes_.value() * 60.0f)) { + idle_exiting_ = true; + + g_base->logic->event_loop()->PushCall([] { + assert(g_base->InLogicThread()); + + // Just go through _babase.quit() + // FIXME: Shouldn't need to go out to the python layer here... + g_base->python->objs().Get(base::BasePython::ObjID::kQuitCall).Call(); + }); + } + } +} + +void SceneV1AppMode::SetInternalMusic(base::SoundAsset* music, float volume, + bool loop) { + // Stop any playing music. + if (internal_music_play_id_) { + g_base->audio->PushSourceStopSoundCall(*internal_music_play_id_); + internal_music_play_id_.reset(); + } + // Start any new music provided. + if (music) { + assert(!internal_music_play_id_); + base::AudioSource* s = g_base->audio->SourceBeginNew(); + if (s) { + s->SetLooping(loop); + s->SetPositional(false); + s->SetGain(volume); + s->SetIsMusic(true); + internal_music_play_id_ = s->Play(music); + s->End(); + } + } +} + +void SceneV1AppMode::HandleGameQuery(const char* buffer, size_t size, + sockaddr_storage* from) { + if (size == 5) { + // If we're already in a party, don't advertise since they + // wouldn't be able to join us anyway. + if (g_base->app_mode->HasConnectionToHost()) { + return; + } + + // Pull the query id from the packet. + uint32_t query_id; + memcpy(&query_id, buffer + 1, 4); + + // Ship them a response packet containing the query id, + // our protocol version, our unique-app-instance-id, and our + // player_spec. + char msg[400]; + + std::string usid = g_base->GetAppInstanceUUID(); + std::string player_spec_string; + + // If we're signed in, send our account spec. + // Otherwise just send a dummy made with our device name. + player_spec_string = + scene_v1::PlayerSpec::GetAccountPlayerSpec().GetSpecString(); + + // This should always be the case (len needs to be 1 byte) + BA_PRECONDITION_FATAL(player_spec_string.size() < 256); + + BA_PRECONDITION_FATAL(!usid.empty()); + if (usid.size() > 100) { + Log(LogLevel::kError, "had to truncate session-id; shouldn't happen"); + usid.resize(100); + } + if (usid.empty()) { + usid = "error"; + } + + msg[0] = BA_PACKET_HOST_QUERY_RESPONSE; + memcpy(msg + 1, &query_id, 4); + uint32_t protocol_version = kProtocolVersion; + memcpy(msg + 5, &protocol_version, 4); + msg[9] = static_cast(usid.size()); + msg[10] = static_cast(player_spec_string.size()); + + memcpy(msg + 11, usid.c_str(), usid.size()); + memcpy(msg + 11 + usid.size(), player_spec_string.c_str(), + player_spec_string.size()); + size_t msg_len = 11 + player_spec_string.size() + usid.size(); + BA_PRECONDITION_FATAL(msg_len <= sizeof(msg)); + + std::vector msg_buffer(msg_len); + memcpy(msg_buffer.data(), msg, msg_len); + + g_base->network_writer->PushSendToCall(msg_buffer, SockAddr(*from)); + + } else { + Log(LogLevel::kError, "Got invalid game-query packet of len " + + std::to_string(size) + "; expected 5."); + } +} + +void SceneV1AppMode::RunMainMenu() { + assert(g_base->InLogicThread()); + if (g_core->shutting_down) { + return; + } + assert(g_base->InLogicThread()); + PythonRef result = g_scene_v1->python->objs() + .Get(SceneV1Python::ObjID::kLaunchMainMenuSessionCall) + .Call(); + if (!result.Exists()) { + throw Exception("Error running scene_v1 main menu."); + } +} + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/support/scene_v1_app_mode.h b/src/ballistica/scene_v1/support/scene_v1_app_mode.h new file mode 100644 index 00000000..c8641cff --- /dev/null +++ b/src/ballistica/scene_v1/support/scene_v1_app_mode.h @@ -0,0 +1,258 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_SUPPORT_SCENE_V1_APP_MODE_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_SCENE_V1_APP_MODE_H_ + +#include +#include +#include +#include +#include +#include + +#include "ballistica/base/app/app_mode.h" +#include "ballistica/base/base.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::scene_v1 { + +const int kMaxPartyNameCombinedSize = 25; + +/// Defines high level app behavior when we're active. +class SceneV1AppMode : public base::AppMode { + public: + /// Create or return our singleton (regardless of active state). + /// Will never return nullptr. + static auto GetSingleton() -> SceneV1AppMode*; + + /// Return our singleton if it is active and nullptr otherwise. + /// Be sure to handle the case where it is not. + static auto GetActive() -> SceneV1AppMode*; + + /// Return our singleton if it is active and log a warning and return nullptr + /// if not. Use when you're gracefully handling the nullptr case but don't + /// expect it to ever occur. + static auto GetActiveOrWarn() -> SceneV1AppMode*; + + /// Return our singleton if it is active and throw an Exception if not. + /// Use when exception logic can gracefully handle the fail case. + static auto GetActiveOrThrow() -> SceneV1AppMode*; + + /// Return our singleton if it is active and fatal-error otherwise. + /// Use when you are not handling the nullptr case and don't expect + /// it to ever occur. + static auto GetActiveOrFatal() -> SceneV1AppMode*; + + auto HandleJSONPing(const std::string& data_str) -> std::string override; + void HandleIncomingUDPPacket(const std::vector& data_in, + const SockAddr& addr) override; + void StepDisplayTime() override; + void OnAppShutdown() override; + auto game_roster() const -> cJSON* { return game_roster_; } + void UpdateGameRoster(); + void MarkGameRosterDirty() { game_roster_dirty_ = true; } + void SetGameRoster(cJSON* r); + auto GetPartySize() const -> int override; + auto kick_vote_in_progress() const -> bool { return kick_vote_in_progress_; } + void StartKickVote(ConnectionToClient* starter, ConnectionToClient* target); + void set_kick_voting_enabled(bool enable) { kick_voting_enabled_ = enable; } + void SetForegroundScene(Scene* sg); + + void LaunchHostSession( + PyObject* session_type_obj, + base::BenchmarkType benchmark_type = base::BenchmarkType::kNone); + void LaunchReplaySession(const std::string& file_name); + void LaunchClientSession(); + + auto GetNetworkDebugString() -> std::string override; + auto GetPingString() -> std::string override; + auto HasConnectionToHost() const -> bool override; + auto HasConnectionToClients() const -> bool override; + auto connections() const -> ConnectionSet* { + assert(connections_.get()); + return connections_.get(); + } + void CleanUpBeforeConnectingToHost(); + void ChangeGameSpeed(int offs) override; + void SetForegroundSession(Session* s); + void LocalDisplayChatMessage(const std::vector& buffer); + auto chat_messages() const -> const std::list& { + return chat_messages_; + } + void ApplyAppConfig() override; + + // Return whichever session is front and center. + auto GetForegroundSession() const -> Session* { + return foreground_session_.Get(); + } + + // Used to know which globals is in control currently/etc. + auto GetForegroundScene() const -> Scene* { + assert(g_base->InLogicThread()); + return foreground_scene_.Get(); + } + auto GetForegroundContext() -> base::ContextRef override; + auto debug_speed_mult() const -> float { return debug_speed_mult_; } + auto replay_speed_exponent() const -> int { return replay_speed_exponent_; } + auto replay_speed_mult() const -> float { return replay_speed_mult_; } + void OnScreenSizeChange() override; + auto kick_idle_players() const -> bool { return kick_idle_players_; } + void LanguageChanged() override; + void SetDebugSpeedExponent(int val); + void SetReplaySpeedExponent(int val); + void set_admin_public_ids(const std::set& ids) { + admin_public_ids_ = ids; + } + const std::set& admin_public_ids() const { + return admin_public_ids_; + } + auto last_connection_to_client_join_time() const -> millisecs_t { + return last_connection_to_client_join_time_; + } + void set_last_connection_to_client_join_time(millisecs_t val) { + last_connection_to_client_join_time_ = val; + } + auto LastClientJoinTime() const -> millisecs_t override; + void SetPublicPartyEnabled(bool val); + auto public_party_enabled() const { return public_party_enabled_; } + auto public_party_size() const { return public_party_size_; } + void SetPublicPartySize(int count); + auto public_party_max_size() const { return public_party_max_size_; } + void SetPublicPartyQueueEnabled(bool enabled); + auto public_party_queue_enabled() const { + return public_party_queue_enabled_; + } + auto public_party_max_player_count() const { + return public_party_max_player_count_; + } + auto public_party_min_league() const -> const std::string& { + return public_party_min_league_; + } + auto public_party_stats_url() const -> const std::string& { + return public_party_stats_url_; + } + void SetPublicPartyMaxSize(int count); + void SetPublicPartyName(const std::string& name); + void SetPublicPartyStatsURL(const std::string& name); + auto public_party_name() const { return public_party_name_; } + auto public_party_player_count() const { return public_party_player_count_; } + void SetPublicPartyPlayerCount(int count); + auto ShouldAnnouncePartyJoinsAndLeaves() -> bool; + auto require_client_authentication() const { + return require_client_authentication_; + } + void set_require_client_authentication(bool enable) { + require_client_authentication_ = enable; + } + auto IsPlayerBanned(const PlayerSpec& spec) -> bool; + void BanPlayer(const PlayerSpec& spec, millisecs_t duration); + void OnAppStart() override; + void OnAppPause() override; + void OnAppResume() override; + auto InMainMenu() const -> bool override; + auto CreateInputDeviceDelegate(base::InputDevice* device) + -> base::InputDeviceDelegate* override; + + void SetInternalMusic(base::SoundAsset* music, float volume = 1.0, + bool loop = true); + + // Run a cycle of host scanning (basically sending out a broadcast packet to + // see who's out there). + void HostScanCycle(); + void EndHostScanning(); + + struct ScanResultsEntry { + std::string display_string; + std::string address; + }; + + auto GetScanResults() -> std::vector; + void HandleGameQuery(const char* buffer, size_t size, + sockaddr_storage* from) override; + void DrawWorld(base::FrameDef* frame_def) override; + auto DoesWorldFillScreen() -> bool override; + void GraphicsQualityChanged(base::GraphicsQuality quality) override; + void RunMainMenu(); + + auto dynamics_sync_time() const { return dynamics_sync_time_; } + void set_dynamics_sync_time(int val) { dynamics_sync_time_ = val; } + auto delay_bucket_samples() const { return delay_bucket_samples_; } + void set_delay_bucket_samples(int val) { delay_bucket_samples_ = val; } + auto buffer_time() const { return buffer_time_; } + void set_buffer_time(int val) { buffer_time_ = val; } + void OnActivate() override; + + private: + void PruneScanResults(); + void UpdateKickVote(); + SceneV1AppMode(); + auto GetGameRosterMessage() -> std::vector; + void Reset(); + void PruneSessions(); + void HandleQuitOnIdle(); + struct ScanResultsEntryPriv; + // Note: would use an unordered_map here but gcc doesn't seem to allow + // forward declarations of their template params. + std::map scan_results_; + std::mutex scan_results_mutex_; + uint32_t next_scan_query_id_{}; + int scan_socket_{-1}; + + std::list chat_messages_; + bool chat_muted_{}; + // *All* existing sessions (including old ones waiting to shut down). + std::vector > sessions_; + Object::WeakRef foreground_scene_; + Object::WeakRef foreground_session_; + + bool game_roster_dirty_{}; + millisecs_t last_game_roster_send_time_{}; + std::unique_ptr connections_; + cJSON* game_roster_{}; + Object::WeakRef kick_vote_starter_; + Object::WeakRef kick_vote_target_; + millisecs_t kick_vote_end_time_{}; + bool kick_vote_in_progress_{}; + int last_kick_votes_needed_{-1}; + bool kick_voting_enabled_{true}; + millisecs_t legacy_display_time_millisecs_{}; + millisecs_t legacy_display_time_millisecs_prev_{-1}; + + // How often we send dynamics resync messages. + int dynamics_sync_time_{500}; + // How many steps we sample for each bucket. + int delay_bucket_samples_{60}; + + // Maximum time in milliseconds to buffer game input/output before sending + // it over the network. + int buffer_time_{}; + + bool in_update_{}; + millisecs_t next_long_update_report_time_{}; + int debug_speed_exponent_{}; + float debug_speed_mult_{1.0f}; + int replay_speed_exponent_{}; + float replay_speed_mult_{1.0f}; + bool kick_idle_players_{}; + std::set admin_public_ids_; + millisecs_t last_connection_to_client_join_time_{}; + bool public_party_enabled_{}; + int public_party_size_{1}; // Always count ourself (is that what we want?). + int public_party_max_size_{8}; + bool public_party_queue_enabled_{true}; + int public_party_player_count_{0}; + int public_party_max_player_count_{8}; + std::string public_party_name_; + std::string public_party_min_league_; + std::string public_party_stats_url_; + bool require_client_authentication_{}; + std::list > banned_players_; + std::optional idle_exit_minutes_{}; + bool idle_exiting_{}; + std::optional internal_music_play_id_{}; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_SUPPORT_SCENE_V1_APP_MODE_H_ diff --git a/src/ballistica/scene_v1/support/scene_v1_context.cc b/src/ballistica/scene_v1/support/scene_v1_context.cc new file mode 100644 index 00000000..8f1dc681 --- /dev/null +++ b/src/ballistica/scene_v1/support/scene_v1_context.cc @@ -0,0 +1,111 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/support/scene_v1_context.h" + +#include "ballistica/base/app/app_mode.h" +#include "ballistica/scene_v1/support/host_activity.h" +#include "ballistica/shared/generic/runnable.h" +#include "ballistica/shared/python/python_sys.h" + +namespace ballistica::scene_v1 { + +auto ContextRefSceneV1::FromAppForegroundContext() -> ContextRefSceneV1 { + auto* c = g_base->app_mode->GetForegroundContext().Get(); + return ContextRefSceneV1(c); +} + +auto ContextRefSceneV1::GetHostSession() const -> HostSession* { + assert(g_base->InLogicThread()); + if (auto* c = GetContextTyped()) { + return c->GetHostSession(); + } + return nullptr; +} + +auto ContextRefSceneV1::GetHostActivity() const -> HostActivity* { + assert(g_base->InLogicThread()); + auto* c = GetContextTyped(); + HostActivity* a = c ? c->GetAsHostActivity() : nullptr; + // This should always match. + assert(a == dynamic_cast(c)); + return a; +} + +auto ContextRefSceneV1::GetMutableScene() const -> Scene* { + assert(g_base->InLogicThread()); + auto* c = GetContextTyped(); + Scene* s = c ? c->GetMutableScene() : nullptr; + return s; +} + +auto SceneV1Context::GetContextDescription() -> std::string { + if (HostActivity* ha = GetAsHostActivity()) { + // Return our Python activity class description if possible. + PythonRef ha_obj(ha->GetPyActivity(), PythonRef::kAcquire); + if (ha_obj.Get() != Py_None) { + return ha_obj.Str(); + } + } + return Context::GetContextDescription(); +} + +auto SceneV1Context::GetHostSession() -> HostSession* { return nullptr; } + +auto SceneV1Context::GetAsHostActivity() -> HostActivity* { return nullptr; } +auto SceneV1Context::GetMutableScene() -> Scene* { return nullptr; } + +auto SceneV1Context::NewTimer(TimeType timetype, TimerMedium length, + bool repeat, + const Object::Ref& runnable) -> int { + // Make sure the passed runnable has a ref-count already + // (don't want them to rely on us to create initial one). + assert(runnable.Exists()); + assert(Object::IsValidManagedObject(runnable.Get())); + + switch (timetype) { + case TimeType::kSim: + throw Exception("Can't create 'sim' type timers in this context_ref"); + case TimeType::kBase: + throw Exception("Can't create 'base' type timers in this context_ref"); + case TimeType::kReal: + throw Exception("Can't create 'real' type timers in this context_ref"); + default: + throw Exception("Can't create that type timer in this context_ref"); + } +} +void SceneV1Context::DeleteTimer(TimeType timetype, int timer_id) { + // We throw on NewTimer; lets just ignore anything that comes + // through here to avoid messing up destructors. + Log(LogLevel::kError, "ContextTarget::DeleteTimer() called; unexpected."); +} + +auto SceneV1Context::GetTime(TimeType timetype) -> millisecs_t { + throw Exception("Unsupported time type for this context_ref"); +} + +auto SceneV1Context::GetTexture(const std::string& name) + -> Object::Ref { + throw Exception("SysTexture() not supported in this context_ref"); +} + +auto SceneV1Context::GetSound(const std::string& name) + -> Object::Ref { + throw Exception("sound() not supported in this context_ref"); +} + +auto SceneV1Context::GetData(const std::string& name) + -> Object::Ref { + throw Exception("GetData() not supported in this context_ref"); +} + +auto SceneV1Context::GetMesh(const std::string& name) + -> Object::Ref { + throw Exception("SysMesh() not supported in this context_ref"); +} + +auto SceneV1Context::GetCollisionMesh(const std::string& name) + -> Object::Ref { + throw Exception("GetCollisionMesh() not supported in this context_ref"); +} + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/support/scene_v1_context.h b/src/ballistica/scene_v1/support/scene_v1_context.h new file mode 100644 index 00000000..e792a1e7 --- /dev/null +++ b/src/ballistica/scene_v1/support/scene_v1_context.h @@ -0,0 +1,84 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_SUPPORT_SCENE_V1_CONTEXT_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_SCENE_V1_CONTEXT_H_ + +#include "ballistica/base/support/context.h" +#include "ballistica/scene_v1/scene_v1.h" + +namespace ballistica::scene_v1 { + +/// Wraps a weak-ref to a context_ref with functionality specific to scene_v1. +class ContextRefSceneV1 : public base::ContextRef { + public: + ContextRefSceneV1() : ContextRef() {} + explicit ContextRefSceneV1(base::Context* sgc) : ContextRef(sgc) {} + + /// Return a scene_v1 version of the current context_ref-ref. + static auto FromCurrent() -> ContextRefSceneV1 { + return ContextRefSceneV1(g_base->CurrentContext().Get()); + } + + /// Creates from g_app_mode->GetForegroundContext(). + static auto FromAppForegroundContext() -> ContextRefSceneV1; + + // If the current Context is (or is part of) a HostSession, return it; + // otherwise return nullptr. be aware that this will return a session if the + // context is *either* a host-activity or a host-session + auto GetHostSession() const -> HostSession*; + + // Return the current context as an HostActivity if it is one; otherwise + // nullptr (faster than a dynamic_cast) + auto GetHostActivity() const -> HostActivity*; + + // If the current context contains a scene that can be manipulated by + // standard commands, this returns it. This includes host-sessions, + // host-activities, and the UI context. + auto GetMutableScene() const -> Scene*; +}; + +/// Object containing some sort of context_ref. +/// App-modes can subclass this to provide the actual context_ref they desire, +/// and then code can use GetTyped() to safely retrieve context_ref as that +/// type. +class SceneV1Context : public base::Context { + public: + static auto Current() -> SceneV1Context& { + return Context::CurrentTyped(); + } + + auto GetContextDescription() -> std::string override; + + // Return the HostSession associated with this context, (if there is one). + virtual auto GetHostSession() -> HostSession*; + + // Utility functions for casting; faster than dynamic_cast. + virtual auto GetAsHostActivity() -> HostActivity*; + virtual auto GetMutableScene() -> Scene*; + + // Timer create/destroy functions. + // Times are specified in milliseconds. + // Exceptions should be thrown for unsupported timetypes in NewTimer. + // Default NewTimer implementation throws a descriptive error, so it can + // be useful to fall back on for unsupported cases. + virtual auto NewTimer(TimeType timetype, TimerMedium length, bool repeat, + const Object::Ref& runnable) -> int; + virtual void DeleteTimer(TimeType timetype, int timer_id); + + virtual auto GetTexture(const std::string& name) -> Object::Ref; + virtual auto GetSound(const std::string& name) -> Object::Ref; + virtual auto GetData(const std::string& name) -> Object::Ref; + virtual auto GetMesh(const std::string& name) -> Object::Ref; + virtual auto GetCollisionMesh(const std::string& name) + -> Object::Ref; + + // Return the current time of a given type in milliseconds. + // Exceptions should be thrown for unsupported timetypes. + // Default implementation throws a descriptive error so can be + // useful to fall back on for unsupported cases + virtual auto GetTime(TimeType timetype) -> millisecs_t; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_SUPPORT_SCENE_V1_CONTEXT_H_ diff --git a/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc b/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc new file mode 100644 index 00000000..20cdc20c --- /dev/null +++ b/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.cc @@ -0,0 +1,289 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/support/scene_v1_input_device_delegate.h" + +#include "ballistica/base/input/device/input_device.h" +#include "ballistica/base/networking/networking.h" +#include "ballistica/base/support/plus_soft.h" +#include "ballistica/scene_v1/connection/connection_to_host.h" +#include "ballistica/scene_v1/node/player_node.h" +#include "ballistica/scene_v1/python/class/python_class_input_device.h" +#include "ballistica/scene_v1/support/client_session_net.h" +#include "ballistica/scene_v1/support/host_activity.h" +#include "ballistica/scene_v1/support/host_session.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" +#include "ballistica/shared/python/python.h" + +namespace ballistica::scene_v1 { + +SceneV1InputDeviceDelegate::SceneV1InputDeviceDelegate() = default; + +SceneV1InputDeviceDelegate::~SceneV1InputDeviceDelegate() { + assert(g_base->InLogicThread()); + assert(!player_.Exists()); + // Release our Python ref to ourself if we have one. + if (py_ref_) { + Py_DECREF(py_ref_); + } +} +std::optional SceneV1InputDeviceDelegate::GetPlayerPosition() { + PlayerNode* player_node{}; + + // Try to come up with whichever scene is in the foreground, and try + // to pull a node for the player we're attached to. + + if (HostActivity* host_activity = + ContextRefSceneV1::FromAppForegroundContext().GetHostActivity()) { + if (Player* player = GetPlayer()) { + player_node = host_activity->scene()->GetPlayerNode(player->id()); + } + } else { + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + if (Scene* scene = appmode->GetForegroundScene()) { + player_node = scene->GetPlayerNode(remote_player_id()); + } + } + } + if (player_node) { + return Vector3f(player_node->position()); + } + return {}; +} + +auto SceneV1InputDeviceDelegate::AttachedToPlayer() const -> bool { + return player_.Exists() || remote_player_.Exists(); +} + +void SceneV1InputDeviceDelegate::RequestPlayer() { + assert(g_base->InLogicThread()); + + auto* appmode = SceneV1AppMode::GetActive(); + BA_PRECONDITION_FATAL(appmode); + + if (player_.Exists()) { + Log(LogLevel::kError, + "InputDevice::RequestPlayer()" + " called with already-existing player"); + return; + } + if (remote_player_.Exists()) { + Log(LogLevel::kError, + "InputDevice::RequestPlayer() called with already-existing " + "remote-player"); + return; + } + + // If we have a local host-session, ask it for a player.. otherwise if we have + // a client-session, ask it for a player. + assert(g_base->logic); + if (auto* hs = dynamic_cast(appmode->GetForegroundSession())) { + { + Python::ScopedCallLabel label("requestPlayer"); + hs->RequestPlayer(this); + } + } else if (auto* client_session = dynamic_cast( + appmode->GetForegroundSession())) { + if (ConnectionToHost* connection_to_host = + client_session->connection_to_host()) { + std::vector data(2); + data[0] = BA_MESSAGE_REQUEST_REMOTE_PLAYER; + data[1] = static_cast_check_fit(input_device().index()); + connection_to_host->SendReliableMessage(data); + } + } + // If we're in a replay or the game is still bootstrapping, just ignore.. +} + +// When the host-session tells us to attach to a player +void SceneV1InputDeviceDelegate::AttachToLocalPlayer(Player* player) { + if (player_.Exists()) { + Log(LogLevel::kError, + "InputDevice::AttachToLocalPlayer() called with already " + "existing " + "player"); + return; + } + if (remote_player_.Exists()) { + Log(LogLevel::kError, + "InputDevice::AttachToLocalPlayer() called with already " + "existing " + "remote-player"); + return; + } + player_ = player; + player_->set_input_device_delegate(this); +} + +void SceneV1InputDeviceDelegate::AttachToRemotePlayer( + ConnectionToHost* connection_to_host, int remote_player_id) { + assert(connection_to_host); + if (player_.Exists()) { + Log(LogLevel::kError, + "InputDevice::AttachToRemotePlayer()" + " called with already existing " + "player"); + return; + } + if (remote_player_.Exists()) { + Log(LogLevel::kError, + "InputDevice::AttachToRemotePlayer()" + " called with already existing " + "remote-player"); + return; + } + remote_player_ = connection_to_host; + remote_player_id_ = remote_player_id; +} + +void SceneV1InputDeviceDelegate::DetachFromPlayer() { + // Handle local player. + if (auto* player = player_.Get()) { + // NOTE: we now remove the player instantly instead of pushing + // a call to do it; otherwise its possible that someone tries + // to access the player's inputdevice before the call goes + // through which would lead to an exception. + player_->set_input_device_delegate(nullptr); + player_.Clear(); + if (HostSession* host_session = player->GetHostSession()) { + host_session->RemovePlayer(player); + } + } + + // Handle remote player. + if (auto* connection_to_host = remote_player_.Get()) { + std::vector data(2); + data[0] = BA_MESSAGE_REMOVE_REMOTE_PLAYER; + data[1] = static_cast_check_fit(input_device().index()); + connection_to_host->SendReliableMessage(data); + remote_player_.Clear(); + } +} + +std::string SceneV1InputDeviceDelegate::DescribeAttachedTo() const { + return (GetRemotePlayer() != nullptr ? "remote-player" + : GetPlayer() != nullptr ? "local-player" + : "nothing"); +} + +void SceneV1InputDeviceDelegate::InputCommand(InputType type, float value) { + if (Player* p = player_.Get()) { + p->InputCommand(type, value); + } else if (remote_player_.Exists()) { + // Add to existing buffer of input-commands. + { + size_t size = remote_input_commands_buffer_.size(); + // Init if empty; we'll fill in count(bytes 2+3) later. + if (size == 0) { + size = 2; + remote_input_commands_buffer_.resize(size); + remote_input_commands_buffer_[0] = + BA_MESSAGE_REMOTE_PLAYER_INPUT_COMMANDS; + remote_input_commands_buffer_[1] = + static_cast_check_fit(input_device().index()); + } + // Now add this command; add 1 byte for type, 4 for value. + remote_input_commands_buffer_.resize(remote_input_commands_buffer_.size() + + 5); + remote_input_commands_buffer_[size] = static_cast(type); + memcpy(&(remote_input_commands_buffer_[size + 1]), &value, 4); + } + } +} + +void SceneV1InputDeviceDelegate::ShipBufferIfFull() { + assert(remote_player_.Exists()); + auto* appmode = SceneV1AppMode::GetSingleton(); + + ConnectionToHost* hc = remote_player_.Get(); + + // Ship the buffer once it gets big enough or once enough time has passed. + millisecs_t real_time = g_core->GetAppTimeMillisecs(); + + size_t size = remote_input_commands_buffer_.size(); + if (size > 2 + && (static_cast(real_time - last_remote_input_commands_send_time_) + >= appmode->buffer_time() + || size > 400)) { + last_remote_input_commands_send_time_ = real_time; + hc->SendReliableMessage(remote_input_commands_buffer_); + remote_input_commands_buffer_.clear(); + } +} + +auto SceneV1InputDeviceDelegate::GetClientID() const -> int { return -1; } + +void SceneV1InputDeviceDelegate::Update() { + if (remote_player_.Exists()) { + ShipBufferIfFull(); + } +} + +auto SceneV1InputDeviceDelegate::GetRemotePlayer() const -> ConnectionToHost* { + return remote_player_.Get(); +} + +auto SceneV1InputDeviceDelegate::GetPyInputDevice(bool new_ref) -> PyObject* { + assert(g_base->InLogicThread()); + if (py_ref_ == nullptr) { + py_ref_ = PythonClassInputDevice::Create(this); + } + if (new_ref) { + Py_INCREF(py_ref_); + } + return py_ref_; +} + +void SceneV1InputDeviceDelegate::InvalidateConnectionToHost() { + remote_player_.Clear(); +} + +auto SceneV1InputDeviceDelegate::GetPublicV1AccountID() const -> std::string { + assert(g_base->InLogicThread()); + + // This default implementation assumes the device is local + // so just returns the locally signed in account's public id. + + return g_base->Plus()->GetPublicV1AccountID(); +} + +auto SceneV1InputDeviceDelegate::GetPlayerProfiles() const -> PyObject* { + return nullptr; +} + +auto SceneV1InputDeviceDelegate::GetAccountName(bool full) const + -> std::string { + assert(g_base->InLogicThread()); + if (full) { + return PlayerSpec::GetAccountPlayerSpec().GetDisplayString(); + } else { + return PlayerSpec::GetAccountPlayerSpec().GetShortName(); + } +} + +auto SceneV1InputDeviceDelegate::IsRemoteClient() const -> bool { + return false; +} + +void SceneV1InputDeviceDelegate::ResetRandomNames() { + assert(g_scene_v1); + g_scene_v1->ResetRandomNames(); +} + +auto SceneV1InputDeviceDelegate::GetDefaultPlayerName() -> std::string { + assert(g_base->InLogicThread()); + assert(g_scene_v1); + auto&& device = input_device(); + + // Custom name, if present, trumps default. + if (!device.custom_default_player_name().empty()) { + return device.custom_default_player_name(); + } + + char buffer[256]; + snprintf(buffer, sizeof(buffer), "%s %s", device.GetDeviceName().c_str(), + device.GetPersistentIdentifier().c_str()); + std::string default_name = g_scene_v1->GetRandomName(buffer); + return default_name; +} + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.h b/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.h new file mode 100644 index 00000000..1363c7c0 --- /dev/null +++ b/src/ballistica/scene_v1/support/scene_v1_input_device_delegate.h @@ -0,0 +1,83 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_SUPPORT_SCENE_V1_INPUT_DEVICE_DELEGATE_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_SCENE_V1_INPUT_DEVICE_DELEGATE_H_ + +#include "ballistica/base/input/device/input_device_delegate.h" +#include "ballistica/scene_v1/support/player.h" + +namespace ballistica::scene_v1 { + +class SceneV1InputDeviceDelegate : public base::InputDeviceDelegate { + public: + SceneV1InputDeviceDelegate(); + ~SceneV1InputDeviceDelegate() override; + + void RequestPlayer() override; + void InputCommand(InputType type, float value) override; + auto GetPlayerPosition() -> std::optional override; + auto AttachedToPlayer() const -> bool override; + + virtual void AttachToLocalPlayer(Player* player); + virtual void AttachToRemotePlayer(ConnectionToHost* connection_to_host, + int remote_player_id); + void DetachFromPlayer() override; + + void Update() override; + auto GetPlayer() const -> Player* { return player_.Get(); } + auto GetRemotePlayer() const -> ConnectionToHost*; + auto remote_player_id() const -> int { return remote_player_id_; } + + auto DescribeAttachedTo() const -> std::string override; + + auto NewPyRef() -> PyObject* { return GetPyInputDevice(true); } + auto BorrowPyRef() -> PyObject* { return GetPyInputDevice(false); } + auto HasPyRef() -> bool { return (py_ref_ != nullptr); } + + void InvalidateConnectionToHost(); + + /// Return client id or -1 if local. + virtual auto GetClientID() const -> int; + + /// Return the name of the signed-in account associated with this device + /// (for remote players, returns their account). + virtual auto GetAccountName(bool full) const -> std::string; + + /// Return the public V1 Account ID of the signed-in account associated + /// with this device, or an empty string if not (yet) available. + /// Note that in some cases there may be a delay before this value + /// is available. (remote player account IDs are verified with the + /// master server before becoming available, etc) + virtual auto GetPublicV1AccountID() const -> std::string; + + /// Returns player-profiles dict if available; otherwise nullptr. + virtual auto GetPlayerProfiles() const -> PyObject*; + + // FIXME: redundant. + virtual auto IsRemoteClient() const -> bool; + + static void ResetRandomNames(); + + /// Return the default base player name for players using this input device. + virtual auto GetDefaultPlayerName() -> std::string; + + private: + auto GetPyInputDevice(bool new_ref) -> PyObject*; + void ShipBufferIfFull(); + + PyObject* py_ref_{}; + + // We're attached to *one* of these two. + Object::WeakRef player_; + Object::WeakRef remote_player_; + + millisecs_t last_remote_input_commands_send_time_{}; + std::vector remote_input_commands_buffer_; + int remote_player_id_{-1}; + + BA_DISALLOW_CLASS_COPIES(SceneV1InputDeviceDelegate); +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_SUPPORT_SCENE_V1_INPUT_DEVICE_DELEGATE_H_ diff --git a/src/ballistica/scene_v1/support/session.cc b/src/ballistica/scene_v1/support/session.cc new file mode 100644 index 00000000..4115b44d --- /dev/null +++ b/src/ballistica/scene_v1/support/session.cc @@ -0,0 +1,39 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/scene_v1/support/session.h" + +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" + +namespace ballistica::scene_v1 { + +Session::Session() { + g_core->session_count++; + + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + + // New sessions immediately become foreground. + appmode->SetForegroundSession(this); +} + +Session::~Session() { g_core->session_count--; } + +void Session::Update(int time_advance_millisecs, double time_advance) {} + +auto Session::GetForegroundContext() -> base::ContextRef { return {}; } + +void Session::Draw(base::FrameDef*) {} + +void Session::ScreenSizeChanged() {} + +void Session::LanguageChanged() {} + +void Session::GraphicsQualityChanged(base::GraphicsQuality q) {} + +void Session::DebugSpeedMultChanged() {} + +void Session::DumpFullState(SessionStream* out) { + Log(LogLevel::kError, + "Session::DumpFullState() being called; shouldn't happen."); +} + +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/support/session.h b/src/ballistica/scene_v1/support/session.h new file mode 100644 index 00000000..c15735ca --- /dev/null +++ b/src/ballistica/scene_v1/support/session.h @@ -0,0 +1,46 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_SUPPORT_SESSION_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_SESSION_H_ + +#include "ballistica/base/base.h" +#include "ballistica/base/support/context.h" +#include "ballistica/scene_v1/support/scene_v1_context.h" +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::scene_v1 { + +class Session : public SceneV1Context { + public: + Session(); + ~Session() override; + + /// Update the session. Passed a legacy millisecs advance and + /// a modern seconds advance. + virtual void Update(int time_advance_millisecs, double time_advance); + + // If this returns false, the screen will be cleared as part of rendering. + virtual auto DoesFillScreen() const -> bool = 0; + + // Draw!!! + virtual void Draw(base::FrameDef* f); + + // Return the 'frontmost' context in the session. + // This is used for executing console command or other UI hotkeys that should + // apply to whatever the user is seeing. + virtual auto GetForegroundContext() -> base::ContextRef; + virtual void ScreenSizeChanged(); + virtual void LanguageChanged(); + virtual void GraphicsQualityChanged(base::GraphicsQuality q); + virtual void DebugSpeedMultChanged(); + auto benchmark_type() const -> base::BenchmarkType { return benchmark_type_; } + void set_benchmark_type(base::BenchmarkType val) { benchmark_type_ = val; } + virtual void DumpFullState(SessionStream* s); + + private: + base::BenchmarkType benchmark_type_ = base::BenchmarkType::kNone; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_SUPPORT_SESSION_H_ diff --git a/src/ballistica/scene/scene_stream.cc b/src/ballistica/scene_v1/support/session_stream.cc similarity index 72% rename from src/ballistica/scene/scene_stream.cc rename to src/ballistica/scene_v1/support/session_stream.cc index ff1aff8b..9ff31248 100644 --- a/src/ballistica/scene/scene_stream.cc +++ b/src/ballistica/scene_v1/support/session_stream.cc @@ -1,75 +1,71 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/scene/scene_stream.h" +#include "ballistica/scene_v1/support/session_stream.h" -#include "ballistica/app/app.h" -#include "ballistica/assets/assets_server.h" -#include "ballistica/assets/component/collide_model.h" -#include "ballistica/assets/component/data.h" -#include "ballistica/assets/component/model.h" -#include "ballistica/assets/component/sound.h" -#include "ballistica/assets/component/texture.h" -#include "ballistica/dynamics/bg/bg_dynamics.h" -#include "ballistica/dynamics/material/material.h" -#include "ballistica/dynamics/material/material_action.h" -#include "ballistica/dynamics/material/material_component.h" -#include "ballistica/dynamics/material/material_condition_node.h" -#include "ballistica/dynamics/part.h" -#include "ballistica/logic/connection/connection_set.h" -#include "ballistica/logic/connection/connection_to_client.h" -#include "ballistica/logic/session/host_session.h" -#include "ballistica/networking/networking.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" +#include "ballistica/base/assets/assets_server.h" +#include "ballistica/base/dynamics/bg/bg_dynamics.h" +#include "ballistica/base/networking/networking.h" +#include "ballistica/scene_v1/assets/scene_collision_mesh.h" +#include "ballistica/scene_v1/assets/scene_data_asset.h" +#include "ballistica/scene_v1/assets/scene_mesh.h" +#include "ballistica/scene_v1/assets/scene_sound.h" +#include "ballistica/scene_v1/assets/scene_texture.h" +#include "ballistica/scene_v1/connection/connection_set.h" +#include "ballistica/scene_v1/connection/connection_to_client.h" +#include "ballistica/scene_v1/dynamics/material/material.h" +#include "ballistica/scene_v1/dynamics/material/material_component.h" +#include "ballistica/scene_v1/node/node_attribute.h" +#include "ballistica/scene_v1/node/node_type.h" +#include "ballistica/scene_v1/support/host_session.h" +#include "ballistica/scene_v1/support/scene.h" +#include "ballistica/scene_v1/support/scene_v1_app_mode.h" -namespace ballistica { +namespace ballistica::scene_v1 { -SceneStream::SceneStream(HostSession* host_session, bool save_replay) - : time_(0), - host_session_(host_session), - next_flush_time_(0), - last_physics_correction_time_(0), - last_send_time_(0), - writing_replay_(false) { +SessionStream::SessionStream(HostSession* host_session, bool save_replay) + : app_mode_{SceneV1AppMode::GetActiveOrThrow()}, + host_session_{host_session} { if (save_replay) { // Sanity check - we should only ever be writing one replay at once. - if (g_app->replay_open) { + if (g_core->replay_open) { Log(LogLevel::kError, "g_replay_open true at replay start; shouldn't happen."); } - assert(g_assets_server); - g_assets_server->PushBeginWriteReplayCall(); + assert(g_base->assets_server); + g_base->assets_server->PushBeginWriteReplayCall(); writing_replay_ = true; - g_app->replay_open = true; + g_core->replay_open = true; } // If we're the live output-stream from a host-session, // take responsibility for feeding all clients to this device. if (host_session_) { - g_logic->connections()->RegisterClientController(this); + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + appmode->connections()->RegisterClientController(this); } } -SceneStream::~SceneStream() { +SessionStream::~SessionStream() { // Ship our last commands (if it matters..) Flush(); if (writing_replay_) { // Sanity check: We should only ever be writing one replay at once. - if (!g_app->replay_open) { + if (!g_core->replay_open) { Log(LogLevel::kError, "g_replay_open false at replay close; shouldn't happen."); } - g_app->replay_open = false; - assert(g_assets_server); - g_assets_server->PushEndWriteReplayCall(); + g_core->replay_open = false; + assert(g_base->assets_server); + g_base->assets_server->PushEndWriteReplayCall(); writing_replay_ = false; } // If we're wired to the host-session, go ahead and release clients. if (host_session_) { - g_logic->connections()->UnregisterClientController(this); + if (auto* appmode = SceneV1AppMode::GetActiveOrWarn()) { + appmode->connections()->UnregisterClientController(this); + } #pragma clang diagnostic push #pragma ide diagnostic ignored "UnreachableCode" @@ -98,21 +94,21 @@ SceneStream::~SceneStream() { Log(LogLevel::kError, std::to_string(count) + " textures in output stream at shutdown"); } - count = GetPointerCount(models_); + count = GetPointerCount(meshes_); if (count != 0) { Log(LogLevel::kError, - std::to_string(count) + " models in output stream at shutdown"); + std::to_string(count) + " meshes in output stream at shutdown"); } count = GetPointerCount(sounds_); if (count != 0) { Log(LogLevel::kError, std::to_string(count) + " sounds in output stream at shutdown"); } - count = GetPointerCount(collide_models_); + count = GetPointerCount(collision_meshes_); if (count != 0) { Log(LogLevel::kError, std::to_string(count) - + " collide_models in output stream at shutdown"); + + " collision_meshes in output stream at shutdown"); } } } @@ -121,7 +117,7 @@ SceneStream::~SceneStream() { } // Pull the current built-up message. -auto SceneStream::GetOutMessage() const -> std::vector { +auto SessionStream::GetOutMessage() const -> std::vector { assert(!host_session_); // this should only be getting used for // standalone temp ones.. if (!out_command_.empty()) { @@ -132,7 +128,7 @@ auto SceneStream::GetOutMessage() const -> std::vector { } template -auto SceneStream::GetPointerCount(const std::vector& vec) -> size_t { +auto SessionStream::GetPointerCount(const std::vector& vec) -> size_t { size_t count = 0; auto size = vec.size(); @@ -148,8 +144,8 @@ auto SceneStream::GetPointerCount(const std::vector& vec) -> size_t { // Given a vector of pointers, return an index to an available (nullptr) entry, // expanding the vector if need be. template -auto SceneStream::GetFreeIndex(std::vector* vec, - std::vector* free_indices) -> size_t { +auto SessionStream::GetFreeIndex(std::vector* vec, + std::vector* free_indices) -> size_t { // If we have any free indices, use one of them. if (!free_indices->empty()) { size_t val = free_indices->back(); @@ -164,8 +160,8 @@ auto SceneStream::GetFreeIndex(std::vector* vec, // Add an entry. template -void SceneStream::Add(T* val, std::vector* vec, - std::vector* free_indices) { +void SessionStream::Add(T* val, std::vector* vec, + std::vector* free_indices) { // This should only get used when we're being driven by the host-session. assert(host_session_); assert(val); @@ -177,8 +173,8 @@ void SceneStream::Add(T* val, std::vector* vec, // Remove an entry. template -void SceneStream::Remove(T* val, std::vector* vec, - std::vector* free_indices) { +void SessionStream::Remove(T* val, std::vector* vec, + std::vector* free_indices) { assert(val); assert(val->stream_id() >= 0); assert(static_cast(vec->size()) > val->stream_id()); @@ -190,22 +186,22 @@ void SceneStream::Remove(T* val, std::vector* vec, val->clear_stream_id(); } -void SceneStream::Fail() { +void SessionStream::Fail() { Log(LogLevel::kError, "Error writing replay file"); if (writing_replay_) { // Sanity check: We should only ever be writing one replay at once. - if (!g_app->replay_open) { + if (!g_core->replay_open) { Log(LogLevel::kError, "g_replay_open false at replay close; shouldn't happen."); } - assert(g_assets_server); - g_assets_server->PushEndWriteReplayCall(); + assert(g_base->assets_server); + g_base->assets_server->PushEndWriteReplayCall(); writing_replay_ = false; - g_app->replay_open = false; + g_core->replay_open = false; } } -void SceneStream::Flush() { +void SessionStream::Flush() { if (!out_command_.empty()) Log(LogLevel::kError, "SceneStream flushing down with non-empty outCommand"); @@ -218,7 +214,7 @@ void SceneStream::Flush() { #pragma ide diagnostic ignored "ConstantParameter" // Writes just a command. -void SceneStream::WriteCommand(SessionCommand cmd) { +void SessionStream::WriteCommand(SessionCommand cmd) { assert(out_command_.empty()); // For now just use full size values. @@ -231,7 +227,7 @@ void SceneStream::WriteCommand(SessionCommand cmd) { #pragma clang diagnostic pop // Writes a command plus an int to the stream, using whatever size is optimal. -void SceneStream::WriteCommandInt32(SessionCommand cmd, int32_t value) { +void SessionStream::WriteCommandInt32(SessionCommand cmd, int32_t value) { assert(out_command_.empty()); // For now just use full size values. @@ -243,8 +239,8 @@ void SceneStream::WriteCommandInt32(SessionCommand cmd, int32_t value) { memcpy(ptr, vals, 4); } -void SceneStream::WriteCommandInt32_2(SessionCommand cmd, int32_t value1, - int32_t value2) { +void SessionStream::WriteCommandInt32_2(SessionCommand cmd, int32_t value1, + int32_t value2) { assert(out_command_.empty()); // For now just use full size vals. @@ -256,8 +252,8 @@ void SceneStream::WriteCommandInt32_2(SessionCommand cmd, int32_t value1, memcpy(ptr, vals, 8); } -void SceneStream::WriteCommandInt32_3(SessionCommand cmd, int32_t value1, - int32_t value2, int32_t value3) { +void SessionStream::WriteCommandInt32_3(SessionCommand cmd, int32_t value1, + int32_t value2, int32_t value3) { assert(out_command_.empty()); // For now just use full size vals. @@ -269,9 +265,9 @@ void SceneStream::WriteCommandInt32_3(SessionCommand cmd, int32_t value1, memcpy(ptr, vals, 12); } -void SceneStream::WriteCommandInt32_4(SessionCommand cmd, int32_t value1, - int32_t value2, int32_t value3, - int32_t value4) { +void SessionStream::WriteCommandInt32_4(SessionCommand cmd, int32_t value1, + int32_t value2, int32_t value3, + int32_t value4) { assert(out_command_.empty()); // For now just use full size vals. @@ -287,33 +283,33 @@ void SceneStream::WriteCommandInt32_4(SessionCommand cmd, int32_t value1, // adding these placeholders for if/when we do. // They will also catch values greater than 32 bits in debug mode. // We'll need a protocol update to add support for 64 bit over the wire. -void SceneStream::WriteCommandInt64(SessionCommand cmd, int64_t value) { +void SessionStream::WriteCommandInt64(SessionCommand cmd, int64_t value) { WriteCommandInt32(cmd, static_cast_check_fit(value)); } -void SceneStream::WriteCommandInt64_2(SessionCommand cmd, int64_t value1, - int64_t value2) { +void SessionStream::WriteCommandInt64_2(SessionCommand cmd, int64_t value1, + int64_t value2) { WriteCommandInt32_2(cmd, static_cast_check_fit(value1), static_cast_check_fit(value2)); } -void SceneStream::WriteCommandInt64_3(SessionCommand cmd, int64_t value1, - int64_t value2, int64_t value3) { +void SessionStream::WriteCommandInt64_3(SessionCommand cmd, int64_t value1, + int64_t value2, int64_t value3) { WriteCommandInt32_3(cmd, static_cast_check_fit(value1), static_cast_check_fit(value2), static_cast_check_fit(value3)); } -void SceneStream::WriteCommandInt64_4(SessionCommand cmd, int64_t value1, - int64_t value2, int64_t value3, - int64_t value4) { +void SessionStream::WriteCommandInt64_4(SessionCommand cmd, int64_t value1, + int64_t value2, int64_t value3, + int64_t value4) { WriteCommandInt32_4(cmd, static_cast_check_fit(value1), static_cast_check_fit(value2), static_cast_check_fit(value3), static_cast_check_fit(value4)); } -void SceneStream::WriteString(const std::string& s) { +void SessionStream::WriteString(const std::string& s) { // Write length int. auto string_size = s.size(); auto size = out_command_.size(); @@ -324,13 +320,13 @@ void SceneStream::WriteString(const std::string& s) { } } -void SceneStream::WriteFloat(float val) { +void SessionStream::WriteFloat(float val) { auto size = static_cast(out_command_.size()); out_command_.resize(size + sizeof(val)); memcpy(&out_command_[size], &val, 4); } -void SceneStream::WriteFloats(size_t count, const float* vals) { +void SessionStream::WriteFloats(size_t count, const float* vals) { assert(count > 0); auto size = out_command_.size(); size_t vals_size = sizeof(float) * count; @@ -338,7 +334,7 @@ void SceneStream::WriteFloats(size_t count, const float* vals) { memcpy(&(out_command_[size]), vals, vals_size); } -void SceneStream::WriteInts32(size_t count, const int32_t* vals) { +void SessionStream::WriteInts32(size_t count, const int32_t* vals) { assert(count > 0); auto size = out_command_.size(); size_t vals_size = sizeof(int32_t) * count; @@ -346,7 +342,7 @@ void SceneStream::WriteInts32(size_t count, const int32_t* vals) { memcpy(&(out_command_[size]), vals, vals_size); } -void SceneStream::WriteInts64(size_t count, const int64_t* vals) { +void SessionStream::WriteInts64(size_t count, const int64_t* vals) { // FIXME: we don't actually support writing 64 bit values to the wire // at the moment; will need a protocol update for that. // This is just implemented as a placeholder. @@ -357,7 +353,7 @@ void SceneStream::WriteInts64(size_t count, const int64_t* vals) { WriteInts32(count, vals32.data()); } -void SceneStream::WriteChars(size_t count, const char* vals) { +void SessionStream::WriteChars(size_t count, const char* vals) { assert(count > 0); auto size = out_command_.size(); auto vals_size = static_cast(count); @@ -365,7 +361,7 @@ void SceneStream::WriteChars(size_t count, const char* vals) { memcpy(&(out_command_[size]), vals, vals_size); } -void SceneStream::ShipSessionCommandsMessage() { +void SessionStream::ShipSessionCommandsMessage() { BA_PRECONDITION(!out_message_.empty()); // Send this message to all client-connections we're attached to. @@ -376,12 +372,12 @@ void SceneStream::ShipSessionCommandsMessage() { AddMessageToReplay(out_message_); } out_message_.clear(); - last_send_time_ = GetRealTime(); + last_send_time_ = g_core->GetAppTimeMillisecs(); } -void SceneStream::AddMessageToReplay(const std::vector& message) { +void SessionStream::AddMessageToReplay(const std::vector& message) { assert(writing_replay_); - assert(g_assets_server); + assert(g_base->assets_server); assert(!message.empty()); if (g_buildconfig.debug_build()) { @@ -396,10 +392,10 @@ void SceneStream::AddMessageToReplay(const std::vector& message) { } } - g_assets_server->PushAddMessageToReplayCall(message); + g_base->assets_server->PushAddMessageToReplayCall(message); } -void SceneStream::SendPhysicsCorrection(bool blend) { +void SessionStream::SendPhysicsCorrection(bool blend) { assert(host_session_); std::vector > messages; @@ -417,7 +413,7 @@ void SceneStream::SendPhysicsCorrection(bool blend) { } } -void SceneStream::EndCommand(bool is_time_set) { +void SessionStream::EndCommand(bool is_time_set) { assert(!out_command_.empty()); int out_message_size; @@ -441,10 +437,11 @@ void SceneStream::EndCommand(bool is_time_set) { // When attached to a host-session, send this message to clients if it's been // long enough. Also send off occasional correction packets. if (host_session_) { + auto* appmode = SceneV1AppMode::GetSingleton(); // Now if its been long enough *AND* this is a time-step command, send. - millisecs_t real_time = GetRealTime(); + millisecs_t real_time = g_core->GetAppTimeMillisecs(); millisecs_t diff = real_time - last_send_time_; - if (is_time_set && diff >= g_app->buffer_time) { + if (is_time_set && diff >= app_mode_->buffer_time()) { ShipSessionCommandsMessage(); // Also, as long as we're here, fire off a physics-correction packet every @@ -454,7 +451,7 @@ void SceneStream::EndCommand(bool is_time_set) { // commands; otherwise the client will get the correction that accounts // for commands that they haven't been sent yet. diff = real_time - last_physics_correction_time_; - if (diff >= g_app->dynamics_sync_time) { + if (diff >= appmode->dynamics_sync_time()) { last_physics_correction_time_ = real_time; SendPhysicsCorrection(true); } @@ -463,7 +460,7 @@ void SceneStream::EndCommand(bool is_time_set) { out_command_.clear(); } -auto SceneStream::IsValidScene(Scene* s) -> bool { +auto SessionStream::IsValidScene(Scene* s) -> bool { if (!host_session_) { return true; // We don't build lists in this mode so can't verify this. } @@ -472,7 +469,7 @@ auto SceneStream::IsValidScene(Scene* s) -> bool { && scenes_[s->stream_id()] == s); } -auto SceneStream::IsValidNode(Node* n) -> bool { +auto SessionStream::IsValidNode(Node* n) -> bool { if (!host_session_) { return true; // We don't build lists in this mode so can't verify this. } @@ -481,7 +478,7 @@ auto SceneStream::IsValidNode(Node* n) -> bool { && nodes_[n->stream_id()] == n); } -auto SceneStream::IsValidTexture(Texture* n) -> bool { +auto SessionStream::IsValidTexture(SceneTexture* n) -> bool { if (!host_session_) { return true; // We don't build lists in this mode so can't verify this. } @@ -490,16 +487,16 @@ auto SceneStream::IsValidTexture(Texture* n) -> bool { && textures_[n->stream_id()] == n); } -auto SceneStream::IsValidModel(Model* n) -> bool { +auto SessionStream::IsValidMesh(SceneMesh* n) -> bool { if (!host_session_) { return true; // We don't build lists in this mode so can't verify this. } return (n != nullptr && n->stream_id() >= 0 - && n->stream_id() < static_cast(models_.size()) - && models_[n->stream_id()] == n); + && n->stream_id() < static_cast(meshes_.size()) + && meshes_[n->stream_id()] == n); } -auto SceneStream::IsValidSound(Sound* n) -> bool { +auto SessionStream::IsValidSound(SceneSound* n) -> bool { if (!host_session_) { return true; // We don't build lists in this mode so can't verify this. } @@ -508,7 +505,7 @@ auto SceneStream::IsValidSound(Sound* n) -> bool { && sounds_[n->stream_id()] == n); } -auto SceneStream::IsValidData(Data* n) -> bool { +auto SessionStream::IsValidData(SceneDataAsset* n) -> bool { if (!host_session_) { return true; // We don't build lists in this mode so can't verify this. } @@ -517,16 +514,16 @@ auto SceneStream::IsValidData(Data* n) -> bool { && datas_[n->stream_id()] == n); } -auto SceneStream::IsValidCollideModel(CollideModel* n) -> bool { +auto SessionStream::IsValidCollisionMesh(SceneCollisionMesh* n) -> bool { if (!host_session_) { return true; // We don't build lists in this mode so can't verify this. } return (n != nullptr && n->stream_id() >= 0 - && n->stream_id() < static_cast(collide_models_.size()) - && collide_models_[n->stream_id()] == n); + && n->stream_id() < static_cast(collision_meshes_.size()) + && collision_meshes_[n->stream_id()] == n); } -auto SceneStream::IsValidMaterial(Material* n) -> bool { +auto SessionStream::IsValidMaterial(Material* n) -> bool { if (!host_session_) { return true; // We don't build lists in this mode so can't verify this. } @@ -535,7 +532,7 @@ auto SceneStream::IsValidMaterial(Material* n) -> bool { && materials_[n->stream_id()] == n); } -void SceneStream::SetTime(millisecs_t t) { +void SessionStream::SetTime(millisecs_t t) { if (time_ == t) { return; // Ignore redundants. } @@ -549,7 +546,7 @@ void SceneStream::SetTime(millisecs_t t) { EndCommand(true); } -void SceneStream::AddScene(Scene* s) { +void SessionStream::AddScene(Scene* s) { // Host mode. if (host_session_) { Add(s, &scenes_, &free_indices_scene_graphs_); @@ -563,19 +560,19 @@ void SceneStream::AddScene(Scene* s) { EndCommand(); } -void SceneStream::RemoveScene(Scene* s) { +void SessionStream::RemoveScene(Scene* s) { WriteCommandInt64(SessionCommand::kRemoveSceneGraph, s->stream_id()); Remove(s, &scenes_, &free_indices_scene_graphs_); EndCommand(); } -void SceneStream::StepScene(Scene* s) { +void SessionStream::StepScene(Scene* s) { assert(IsValidScene(s)); WriteCommandInt64(SessionCommand::kStepSceneGraph, s->stream_id()); EndCommand(); } -void SceneStream::AddNode(Node* n) { +void SessionStream::AddNode(Node* n) { assert(n); if (host_session_) { Add(n, &nodes_, &free_indices_nodes_); @@ -590,26 +587,26 @@ void SceneStream::AddNode(Node* n) { EndCommand(); } -void SceneStream::NodeOnCreate(Node* n) { +void SessionStream::NodeOnCreate(Node* n) { assert(IsValidNode(n)); WriteCommandInt64(SessionCommand::kNodeOnCreate, n->stream_id()); EndCommand(); } -void SceneStream::SetForegroundScene(Scene* sg) { +void SessionStream::SetForegroundScene(Scene* sg) { assert(IsValidScene(sg)); - WriteCommandInt64(SessionCommand::kSetForegroundSceneGraph, sg->stream_id()); + WriteCommandInt64(SessionCommand::kSetForegroundScene, sg->stream_id()); EndCommand(); } -void SceneStream::RemoveNode(Node* n) { +void SessionStream::RemoveNode(Node* n) { assert(IsValidNode(n)); WriteCommandInt64(SessionCommand::kRemoveNode, n->stream_id()); Remove(n, &nodes_, &free_indices_nodes_); EndCommand(); } -void SceneStream::AddTexture(Texture* t) { +void SessionStream::AddTexture(SceneTexture* t) { // Register an ID in host mode. if (host_session_) { Add(t, &textures_, &free_indices_textures_); @@ -624,36 +621,36 @@ void SceneStream::AddTexture(Texture* t) { EndCommand(); } -void SceneStream::RemoveTexture(Texture* t) { +void SessionStream::RemoveTexture(SceneTexture* t) { assert(IsValidTexture(t)); WriteCommandInt64(SessionCommand::kRemoveTexture, t->stream_id()); Remove(t, &textures_, &free_indices_textures_); EndCommand(); } -void SceneStream::AddModel(Model* t) { +void SessionStream::AddMesh(SceneMesh* t) { // Register an ID in host mode. if (host_session_) { - Add(t, &models_, &free_indices_models_); + Add(t, &meshes_, &free_indices_meshes_); } else { assert(t && t->stream_id() != -1); } Scene* sg = t->scene(); assert(IsValidScene(sg)); - WriteCommandInt64_2(SessionCommand::kAddModel, sg->stream_id(), + WriteCommandInt64_2(SessionCommand::kAddMesh, sg->stream_id(), t->stream_id()); WriteString(t->name()); EndCommand(); } -void SceneStream::RemoveModel(Model* t) { - assert(IsValidModel(t)); - WriteCommandInt64(SessionCommand::kRemoveModel, t->stream_id()); - Remove(t, &models_, &free_indices_models_); +void SessionStream::RemoveMesh(SceneMesh* t) { + assert(IsValidMesh(t)); + WriteCommandInt64(SessionCommand::kRemoveMesh, t->stream_id()); + Remove(t, &meshes_, &free_indices_meshes_); EndCommand(); } -void SceneStream::AddSound(Sound* t) { +void SessionStream::AddSound(SceneSound* t) { // Register an ID in host mode. if (host_session_) { Add(t, &sounds_, &free_indices_sounds_); @@ -668,14 +665,14 @@ void SceneStream::AddSound(Sound* t) { EndCommand(); } -void SceneStream::RemoveSound(Sound* t) { +void SessionStream::RemoveSound(SceneSound* t) { assert(IsValidSound(t)); WriteCommandInt64(SessionCommand::kRemoveSound, t->stream_id()); Remove(t, &sounds_, &free_indices_sounds_); EndCommand(); } -void SceneStream::AddData(Data* t) { +void SessionStream::AddData(SceneDataAsset* t) { // Register an ID in host mode. if (host_session_) { Add(t, &datas_, &free_indices_datas_); @@ -690,35 +687,35 @@ void SceneStream::AddData(Data* t) { EndCommand(); } -void SceneStream::RemoveData(Data* t) { +void SessionStream::RemoveData(SceneDataAsset* t) { assert(IsValidData(t)); WriteCommandInt64(SessionCommand::kRemoveData, t->stream_id()); Remove(t, &datas_, &free_indices_datas_); EndCommand(); } -void SceneStream::AddCollideModel(CollideModel* t) { +void SessionStream::AddCollisionMesh(SceneCollisionMesh* t) { if (host_session_) { - Add(t, &collide_models_, &free_indices_collide_models_); + Add(t, &collision_meshes_, &free_indices_collision_meshes_); } else { assert(t && t->stream_id() != -1); } Scene* sg = t->scene(); assert(IsValidScene(sg)); - WriteCommandInt64_2(SessionCommand::kAddCollideModel, sg->stream_id(), + WriteCommandInt64_2(SessionCommand::kAddCollisionMesh, sg->stream_id(), t->stream_id()); WriteString(t->name()); EndCommand(); } -void SceneStream::RemoveCollideModel(CollideModel* t) { - assert(IsValidCollideModel(t)); - WriteCommandInt64(SessionCommand::kRemoveCollideModel, t->stream_id()); - Remove(t, &collide_models_, &free_indices_collide_models_); +void SessionStream::RemoveCollisionMesh(SceneCollisionMesh* t) { + assert(IsValidCollisionMesh(t)); + WriteCommandInt64(SessionCommand::kRemoveCollisionMesh, t->stream_id()); + Remove(t, &collision_meshes_, &free_indices_collision_meshes_); EndCommand(); } -void SceneStream::AddMaterial(Material* m) { +void SessionStream::AddMaterial(Material* m) { if (host_session_) { Add(m, &materials_, &free_indices_materials_); } else { @@ -731,14 +728,14 @@ void SceneStream::AddMaterial(Material* m) { EndCommand(); } -void SceneStream::RemoveMaterial(Material* m) { +void SessionStream::RemoveMaterial(Material* m) { assert(IsValidMaterial(m)); WriteCommandInt64(SessionCommand::kRemoveMaterial, m->stream_id()); Remove(m, &materials_, &free_indices_materials_); EndCommand(); } -void SceneStream::AddMaterialComponent(Material* m, MaterialComponent* c) { +void SessionStream::AddMaterialComponent(Material* m, MaterialComponent* c) { assert(IsValidMaterial(m)); auto flattened_size = c->GetFlattenedSize(); assert(flattened_size > 0 && flattened_size < 10000); @@ -757,10 +754,10 @@ void SceneStream::AddMaterialComponent(Material* m, MaterialComponent* c) { EndCommand(); } -void SceneStream::ConnectNodeAttribute(Node* src_node, - NodeAttributeUnbound* src_attr, - Node* dst_node, - NodeAttributeUnbound* dst_attr) { +void SessionStream::ConnectNodeAttribute(Node* src_node, + NodeAttributeUnbound* src_attr, + Node* dst_node, + NodeAttributeUnbound* dst_attr) { assert(IsValidNode(src_node)); assert(IsValidNode(dst_node)); assert(src_attr->node_type() == src_node->type()); @@ -775,7 +772,7 @@ void SceneStream::ConnectNodeAttribute(Node* src_node, EndCommand(); } -void SceneStream::NodeMessage(Node* node, const char* buffer, size_t size) { +void SessionStream::NodeMessage(Node* node, const char* buffer, size_t size) { assert(IsValidNode(node)); BA_PRECONDITION(size > 0 && size < 10000); WriteCommandInt64_2(SessionCommand::kNodeMessage, node->stream_id(), @@ -784,7 +781,7 @@ void SceneStream::NodeMessage(Node* node, const char* buffer, size_t size) { EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, float val) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, float val) { assert(IsValidNode(attr.node)); WriteCommandInt64_2(SessionCommand::kSetNodeAttrFloat, attr.node->stream_id(), attr.index()); @@ -792,22 +789,22 @@ void SceneStream::SetNodeAttr(const NodeAttribute& attr, float val) { EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, int64_t val) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, int64_t val) { assert(IsValidNode(attr.node)); WriteCommandInt64_3(SessionCommand::kSetNodeAttrInt32, attr.node->stream_id(), attr.index(), val); EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, bool val) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, bool val) { assert(IsValidNode(attr.node)); WriteCommandInt64_3(SessionCommand::kSetNodeAttrBool, attr.node->stream_id(), attr.index(), val); EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, - const std::vector& vals) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { assert(IsValidNode(attr.node)); size_t count{vals.size()}; WriteCommandInt64_3(SessionCommand::kSetNodeAttrFloats, @@ -819,8 +816,8 @@ void SceneStream::SetNodeAttr(const NodeAttribute& attr, EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, - const std::vector& vals) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { assert(IsValidNode(attr.node)); size_t count{vals.size()}; WriteCommandInt64_3(SessionCommand::kSetNodeAttrInt32s, @@ -832,8 +829,8 @@ void SceneStream::SetNodeAttr(const NodeAttribute& attr, EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, - const std::string& val) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, + const std::string& val) { assert(IsValidNode(attr.node)); WriteCommandInt64_2(SessionCommand::kSetNodeAttrString, attr.node->stream_id(), attr.index()); @@ -841,7 +838,7 @@ void SceneStream::SetNodeAttr(const NodeAttribute& attr, EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, Node* val) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, Node* val) { assert(IsValidNode(attr.node)); if (val) { assert(IsValidNode(val)); @@ -857,8 +854,8 @@ void SceneStream::SetNodeAttr(const NodeAttribute& attr, Node* val) { EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, - const std::vector& vals) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { assert(IsValidNode(attr.node)); if (g_buildconfig.debug_build()) { for (auto val : vals) { @@ -885,12 +882,12 @@ void SceneStream::SetNodeAttr(const NodeAttribute& attr, EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, Player* val) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, Player* val) { // cout << "SET PLAYER ATTR " << attr.getIndex() << endl; } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, - const std::vector& vals) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { assert(IsValidNode(attr.node)); if (g_buildconfig.debug_build()) { for (auto val : vals) { @@ -918,7 +915,7 @@ void SceneStream::SetNodeAttr(const NodeAttribute& attr, EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, Texture* val) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, SceneTexture* val) { if (val) { assert(IsValidNode(attr.node)); assert(IsValidTexture(val)); @@ -934,8 +931,8 @@ void SceneStream::SetNodeAttr(const NodeAttribute& attr, Texture* val) { EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, - const std::vector& vals) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { assert(IsValidNode(attr.node)); if (g_buildconfig.debug_build()) { for (auto val : vals) { @@ -963,7 +960,7 @@ void SceneStream::SetNodeAttr(const NodeAttribute& attr, EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, Sound* val) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, SceneSound* val) { if (val) { assert(IsValidNode(attr.node)); assert(IsValidSound(val)); @@ -979,8 +976,8 @@ void SceneStream::SetNodeAttr(const NodeAttribute& attr, Sound* val) { EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, - const std::vector& vals) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { assert(IsValidNode(attr.node)); if (g_buildconfig.debug_build()) { for (auto val : vals) { @@ -1008,28 +1005,28 @@ void SceneStream::SetNodeAttr(const NodeAttribute& attr, EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, Model* val) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, SceneMesh* val) { if (val) { assert(IsValidNode(attr.node)); - assert(IsValidModel(val)); + assert(IsValidMesh(val)); if (attr.node->scene() != val->scene()) { - throw Exception("model/node are from different scenes"); + throw Exception("mesh/node are from different scenes"); } - WriteCommandInt64_3(SessionCommand::kSetNodeAttrModel, + WriteCommandInt64_3(SessionCommand::kSetNodeAttrMesh, attr.node->stream_id(), attr.index(), val->stream_id()); } else { - WriteCommandInt64_2(SessionCommand::kSetNodeAttrModelNull, + WriteCommandInt64_2(SessionCommand::kSetNodeAttrMeshNull, attr.node->stream_id(), attr.index()); } EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, - const std::vector& vals) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { assert(IsValidNode(attr.node)); if (g_buildconfig.debug_build()) { for (auto val : vals) { - assert(IsValidModel(val)); + assert(IsValidMesh(val)); } } size_t count = vals.size(); @@ -1039,12 +1036,12 @@ void SceneStream::SetNodeAttr(const NodeAttribute& attr, Scene* scene = attr.node->scene(); for (size_t i = 0; i < count; i++) { if (vals[i]->scene() != scene) { - throw Exception("model/node are from different scenes"); + throw Exception("mesh/node are from different scenes"); } vals_out[i] = static_cast_check_fit(vals[i]->stream_id()); } } - WriteCommandInt64_3(SessionCommand::kSetNodeAttrModels, + WriteCommandInt64_3(SessionCommand::kSetNodeAttrMeshes, attr.node->stream_id(), attr.index(), static_cast_check_fit(count)); if (count > 0) { @@ -1052,27 +1049,28 @@ void SceneStream::SetNodeAttr(const NodeAttribute& attr, } EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, CollideModel* val) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, + SceneCollisionMesh* val) { if (val) { assert(IsValidNode(attr.node)); - assert(IsValidCollideModel(val)); + assert(IsValidCollisionMesh(val)); if (attr.node->scene() != val->scene()) { - throw Exception("collide_model/node are from different scenes"); + throw Exception("collision_mesh/node are from different scenes"); } - WriteCommandInt64_3(SessionCommand::kSetNodeAttrCollideModel, + WriteCommandInt64_3(SessionCommand::kSetNodeAttrCollisionMesh, attr.node->stream_id(), attr.index(), val->stream_id()); } else { - WriteCommandInt64_2(SessionCommand::kSetNodeAttrCollideModelNull, + WriteCommandInt64_2(SessionCommand::kSetNodeAttrCollisionMeshNull, attr.node->stream_id(), attr.index()); } EndCommand(); } -void SceneStream::SetNodeAttr(const NodeAttribute& attr, - const std::vector& vals) { +void SessionStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { assert(IsValidNode(attr.node)); if (g_buildconfig.debug_build()) { for (auto val : vals) { - assert(IsValidCollideModel(val)); + assert(IsValidCollisionMesh(val)); } } size_t count = vals.size(); @@ -1082,12 +1080,12 @@ void SceneStream::SetNodeAttr(const NodeAttribute& attr, Scene* scene = attr.node->scene(); for (size_t i = 0; i < count; i++) { if (vals[i]->scene() != scene) { - throw Exception("collide_model/node are from different scenes"); + throw Exception("collision_mesh/node are from different scenes"); } vals_out[i] = static_cast_check_fit(vals[i]->stream_id()); } } - WriteCommandInt64_3(SessionCommand::kSetNodeAttrCollideModels, + WriteCommandInt64_3(SessionCommand::kSetNodeAttrCollisionMeshes, attr.node->stream_id(), attr.index(), static_cast_check_fit(count)); if (count > 0) { @@ -1096,8 +1094,8 @@ void SceneStream::SetNodeAttr(const NodeAttribute& attr, EndCommand(); } -void SceneStream::PlaySoundAtPosition(Sound* sound, float volume, float x, - float y, float z) { +void SessionStream::PlaySoundAtPosition(SceneSound* sound, float volume, + float x, float y, float z) { assert(IsValidSound(sound)); assert(IsValidScene(sound->scene())); @@ -1110,7 +1108,7 @@ void SceneStream::PlaySoundAtPosition(Sound* sound, float volume, float x, EndCommand(); } -void SceneStream::EmitBGDynamics(const BGDynamicsEmission& e) { +void SessionStream::EmitBGDynamics(const base::BGDynamicsEmission& e) { WriteCommandInt64_4(SessionCommand::kEmitBGDynamics, static_cast(e.emit_type), e.count, static_cast(e.chunk_type), @@ -1128,7 +1126,7 @@ void SceneStream::EmitBGDynamics(const BGDynamicsEmission& e) { EndCommand(); } -void SceneStream::PlaySound(Sound* sound, float volume) { +void SessionStream::PlaySound(SceneSound* sound, float volume) { assert(IsValidSound(sound)); assert(IsValidScene(sound->scene())); @@ -1138,11 +1136,11 @@ void SceneStream::PlaySound(Sound* sound, float volume) { EndCommand(); } -void SceneStream::ScreenMessageTop(const std::string& val, float r, float g, - float b, Texture* texture, - Texture* tint_texture, float tint_r, - float tint_g, float tint_b, float tint2_r, - float tint2_g, float tint2_b) { +void SessionStream::ScreenMessageTop(const std::string& val, float r, float g, + float b, SceneTexture* texture, + SceneTexture* tint_texture, float tint_r, + float tint_g, float tint_b, float tint2_r, + float tint2_g, float tint2_b) { assert(IsValidTexture(texture)); assert(IsValidTexture(tint_texture)); assert(IsValidScene(texture->scene())); @@ -1164,8 +1162,8 @@ void SceneStream::ScreenMessageTop(const std::string& val, float r, float g, EndCommand(); } -void SceneStream::ScreenMessageBottom(const std::string& val, float r, float g, - float b) { +void SessionStream::ScreenMessageBottom(const std::string& val, float r, + float g, float b) { WriteCommand(SessionCommand::kScreenMessageBottom); WriteString(val); float color[3]; @@ -1176,17 +1174,17 @@ void SceneStream::ScreenMessageBottom(const std::string& val, float r, float g, EndCommand(); } -auto SceneStream::GetSoundID(Sound* s) -> int64_t { +auto SessionStream::GetSoundID(SceneSound* s) -> int64_t { assert(IsValidSound(s)); return s->stream_id(); } -auto SceneStream::GetMaterialID(Material* m) -> int64_t { +auto SessionStream::GetMaterialID(Material* m) -> int64_t { assert(IsValidMaterial(m)); return m->stream_id(); } -void SceneStream::OnClientConnected(ConnectionToClient* c) { +void SessionStream::OnClientConnected(ConnectionToClient* c) { // Sanity check - abort if its on either of our lists already. for (auto& connections_to_client : connections_to_clients_) { if (connections_to_client == c) { @@ -1217,7 +1215,7 @@ void SceneStream::OnClientConnected(ConnectionToClient* c) { // We create a temporary output stream just for the purpose of building // a giant session-commands message to reconstruct everything in our // host-session in its current form. - SceneStream out(nullptr, false); + SessionStream out(nullptr, false); // Ask the host-session that we came from to dump it's complete state. host_session_->DumpFullState(&out); @@ -1235,7 +1233,7 @@ void SceneStream::OnClientConnected(ConnectionToClient* c) { } } -void SceneStream::OnClientDisconnected(ConnectionToClient* c) { +void SessionStream::OnClientDisconnected(ConnectionToClient* c) { // Search for it on either our ignored or regular lists. for (auto i = connections_to_clients_.begin(); i != connections_to_clients_.end(); i++) { @@ -1256,4 +1254,4 @@ void SceneStream::OnClientDisconnected(ConnectionToClient* c) { "lists"); } -} // namespace ballistica +} // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/support/session_stream.h b/src/ballistica/scene_v1/support/session_stream.h new file mode 100644 index 00000000..f34e02f2 --- /dev/null +++ b/src/ballistica/scene_v1/support/session_stream.h @@ -0,0 +1,165 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SCENE_V1_SUPPORT_SESSION_STREAM_H_ +#define BALLISTICA_SCENE_V1_SUPPORT_SESSION_STREAM_H_ + +#include +#include + +#include "ballistica/base/base.h" +#include "ballistica/scene_v1/support/client_controller_interface.h" +#include "ballistica/shared/foundation/object.h" + +namespace ballistica::scene_v1 { + +// A mechanism for dumping a live session or session-creation-commands to a +// stream of messages that can be saved to file or sent over the network. +class SessionStream : public Object, public ClientControllerInterface { + public: + SessionStream(HostSession* host_session, bool save_replay); + ~SessionStream() override; + void SetTime(millisecs_t t); + void AddScene(Scene* s); + void RemoveScene(Scene* s); + void StepScene(Scene* s); + void AddNode(Node* n); + void NodeOnCreate(Node* n); + void RemoveNode(Node* n); + void SetForegroundScene(Scene* sg); + void AddMaterial(Material* m); + void RemoveMaterial(Material* m); + void AddMaterialComponent(Material* m, MaterialComponent* c); + void AddTexture(SceneTexture* t); + void RemoveTexture(SceneTexture* t); + void AddMesh(SceneMesh* t); + void RemoveMesh(SceneMesh* t); + void AddSound(SceneSound* t); + void RemoveSound(SceneSound* t); + void AddData(SceneDataAsset* d); + void RemoveData(SceneDataAsset* d); + void AddCollisionMesh(SceneCollisionMesh* t); + void RemoveCollisionMesh(SceneCollisionMesh* t); + void ConnectNodeAttribute(Node* src_node, NodeAttributeUnbound* src_attr, + Node* dst_node, NodeAttributeUnbound* dst_attr); + void NodeMessage(Node* node, const char* buffer, size_t size); + void SetNodeAttr(const NodeAttribute& attr, float val); + void SetNodeAttr(const NodeAttribute& attr, int64_t val); + void SetNodeAttr(const NodeAttribute& attr, bool val); + void SetNodeAttr(const NodeAttribute& attr, const std::vector& vals); + void SetNodeAttr(const NodeAttribute& attr, const std::vector& vals); + void SetNodeAttr(const NodeAttribute& attr, const std::string& val); + void SetNodeAttr(const NodeAttribute& attr, Node* n); + void SetNodeAttr(const NodeAttribute& attr, const std::vector& vals); + void SetNodeAttr(const NodeAttribute& attr, Player* n); + void SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals); + void SetNodeAttr(const NodeAttribute& attr, SceneTexture* n); + void SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals); + void SetNodeAttr(const NodeAttribute& attr, SceneSound* n); + void SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals); + void SetNodeAttr(const NodeAttribute& attr, SceneMesh* n); + void SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals); + void SetNodeAttr(const NodeAttribute& attr, SceneCollisionMesh* n); + void SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals); + void PlaySoundAtPosition(SceneSound* sound, float volume, float x, float y, + float z); + void PlaySound(SceneSound* sound, float volume); + void EmitBGDynamics(const base::BGDynamicsEmission& e); + auto GetSoundID(SceneSound* s) -> int64_t; + auto GetMaterialID(Material* m) -> int64_t; + void ScreenMessageBottom(const std::string& val, float r, float g, float b); + void ScreenMessageTop(const std::string& val, float r, float g, float b, + SceneTexture* texture, SceneTexture* tint_texture, + float tint_r, float tint_g, float tint_b, float tint2_r, + float tint2_g, float tint2_b); + void OnClientConnected(ConnectionToClient* c) override; + void OnClientDisconnected(ConnectionToClient* c) override; + auto GetOutMessage() const -> std::vector; + + private: + // Make sure various components are part of our stream. + auto IsValidScene(Scene* val) -> bool; + auto IsValidNode(Node* val) -> bool; + auto IsValidTexture(SceneTexture* val) -> bool; + auto IsValidMesh(SceneMesh* val) -> bool; + auto IsValidSound(SceneSound* val) -> bool; + auto IsValidData(SceneDataAsset* val) -> bool; + auto IsValidCollisionMesh(SceneCollisionMesh* val) -> bool; + auto IsValidMaterial(Material* val) -> bool; + + void Flush(); + void AddMessageToReplay(const std::vector& message); + void Fail(); + + void ShipSessionCommandsMessage(); + void SendPhysicsCorrection(bool blend); + void EndCommand(bool is_time_set = false); + void WriteString(const std::string& s); + void WriteFloat(float val); + void WriteFloats(size_t count, const float* vals); + void WriteInts32(size_t count, const int32_t* vals); + void WriteInts64(size_t count, const int64_t* vals); + void WriteChars(size_t count, const char* vals); + void WriteCommand(SessionCommand cmd); + void WriteCommandInt32(SessionCommand cmd, int32_t value); + void WriteCommandInt64(SessionCommand cmd, int64_t value); + void WriteCommandInt32_2(SessionCommand cmd, int32_t value1, int32_t value2); + void WriteCommandInt64_2(SessionCommand cmd, int64_t value1, int64_t value2); + void WriteCommandInt32_3(SessionCommand cmd, int32_t value1, int32_t value2, + int32_t value3); + void WriteCommandInt64_3(SessionCommand cmd, int64_t value1, int64_t value2, + int64_t value3); + void WriteCommandInt32_4(SessionCommand cmd, int32_t value1, int32_t value2, + int32_t value3, int32_t value4); + void WriteCommandInt64_4(SessionCommand cmd, int64_t value1, int64_t value2, + int64_t value3, int64_t value4); + template + auto GetPointerCount(const std::vector& vec) -> size_t; + template + auto GetFreeIndex(std::vector* vec, std::vector* free_indices) + -> size_t; + template + void Add(T* val, std::vector* vec, std::vector* free_indices); + template + void Remove(T* val, std::vector* vec, std::vector* free_indices); + + HostSession* host_session_; + millisecs_t next_flush_time_{}; + + // Individual command going into the commands-messages. + std::vector out_command_; + + // The complete message full of commands. + std::vector out_message_; + std::vector connections_to_clients_; + std::vector connections_to_clients_ignored_; + SceneV1AppMode* app_mode_; + bool writing_replay_{}; + millisecs_t last_physics_correction_time_{}; + millisecs_t last_send_time_{}; + millisecs_t time_{}; + std::vector scenes_; + std::vector free_indices_scene_graphs_; + std::vector nodes_; + std::vector free_indices_nodes_; + std::vector materials_; + std::vector free_indices_materials_; + std::vector textures_; + std::vector free_indices_textures_; + std::vector meshes_; + std::vector free_indices_meshes_; + std::vector sounds_; + std::vector free_indices_sounds_; + std::vector datas_; + std::vector free_indices_datas_; + std::vector collision_meshes_; + std::vector free_indices_collision_meshes_; +}; + +} // namespace ballistica::scene_v1 + +#endif // BALLISTICA_SCENE_V1_SUPPORT_SESSION_STREAM_H_ diff --git a/src/ballistica/shared/README.md b/src/ballistica/shared/README.md new file mode 100644 index 00000000..418d0284 --- /dev/null +++ b/src/ballistica/shared/README.md @@ -0,0 +1,8 @@ +# Shared Source + +This is the one directory under **src/ballistica** that does *not* correspond +to a feature-set. All code in this directory lives in the root 'ballistica' +namespace and can be used by any feature-set. + +Likewise, code here must be sure to not have any dependencies on any +feature sets aside from 'core'. \ No newline at end of file diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc new file mode 100644 index 00000000..9af9e9cf --- /dev/null +++ b/src/ballistica/shared/ballistica.cc @@ -0,0 +1,196 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/shared/ballistica.h" + +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/core/platform/support/min_sdl.h" +#include "ballistica/core/python/core_python.h" +#include "ballistica/core/support/base_soft.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/foundation/fatal_error.h" +#include "ballistica/shared/foundation/logging.h" +#include "ballistica/shared/math/vector3f.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_command.h" + +// Make sure min_sdl.h stays in here even though this file compile fine +// without it. On some platforms it does a bit of magic to redefine main as +// SDL_main which leads us to a tricky-to-diagnose linker error if it +// removed from here. +#ifndef BALLISTICA_CORE_PLATFORM_SUPPORT_MIN_SDL_H_ +#error Please include min_sdl.h here. +#endif + +// If desired, define main() in the global namespace. +#if BA_DEFINE_MAIN +auto main(int argc, char** argv) -> int { + auto core_config = + ballistica::core::CoreConfig::FromCommandLineAndEnv(argc, argv); + + // Arg-parsing may have yielded an error or printed simple output for things + // such as '--help', in which case we're done. + if (core_config.immediate_return_code.has_value()) { + return *core_config.immediate_return_code; + } + return ballistica::MonolithicMain(core_config); +} +#endif + +namespace ballistica { + +// These are set automatically via script; don't modify them here. +const int kEngineBuildNumber = 21015; +const char* kEngineVersion = "1.7.20"; + +auto MonolithicMain(const core::CoreConfig& core_config) -> int { + // This code is meant to be run standalone so won't inherit any feature-set's + // globals; we'll need to collect anything we need explicitly. + core::CoreFeatureSet* l_core{}; + core::BaseSoftInterface* l_base{}; + + try { + // Even at the absolute start of execution we should be able to + // reasonably log errors. Set env var BA_CRASH_TEST=1 to test this. + if (const char* crashenv = getenv("BA_CRASH_TEST")) { + if (!strcmp(crashenv, "1")) { + FatalError("Fatal-Error-Test"); + } + } + + // No matter what we're doing, we need the core feature set. Some ballistica + // functionality implicitly uses core, so we should always import it first + // thing even if we don't explicitly use it. + l_core = core::CoreFeatureSet::Import(&core_config); + + // If a command was passed, simply run it and exit. We want to act simply + // as a Python interpreter in that case; we don't do any environment setup + // (aside from the bits core does automatically such as making our built + // in binary modules available). + if (l_core->core_config().call_command.has_value()) { + auto gil{Python::ScopedInterpreterLock()}; + bool success = PythonCommand(*l_core->core_config().call_command, + "") + .Exec(true, nullptr, nullptr); + exit(success ? 0 : 1); + } + + // Ok, looks like we're doing a standard monolithic-mode app run. + + // ------------------------------------------------------------------------- + // Phase 1: "The board is set." + // ------------------------------------------------------------------------- + + // First, set up our environment using our internal paths and whatnot + // (essentially the baenv.configure() call). This needs to be done before + // any other ba* modules are imported since it may affect where those + // modules get loaded from in the first place. + l_core->python->MonolithicModeBaEnvConfigure(); + + // We need the base feature-set to run a full app but we don't have a hard + // dependency to it. Let's see if it's available. + l_base = l_core->SoftImportBase(); + if (!l_base) { + FatalError("Base module unavailable; can't run app."); + } + + // ------------------------------------------------------------------------- + // Phase 2: "The pieces are moving." + // ------------------------------------------------------------------------- + + // Spin up all app machinery such as threads and subsystems. This gets + // things ready to rock, but there's no actual rocking quite yet. + l_base->StartApp(); + + // ------------------------------------------------------------------------- + // Phase 3: "We come to it at last; the great battle of our time." + // ------------------------------------------------------------------------- + + // At this point we unleash the beast and then simply process + // events until the app exits (or we return from this function and let the + // environment do that part). + + if (l_base->AppManagesEventLoop()) { + // In environments where we control the event loop... do that. + l_base->RunAppToCompletion(); + } else { + // Under managed environments we now simply return and let the environment + // feed us events until the app exits. However, we may need to first + // 'prime the pump' here for our main thread event loop. For instance, if + // our event loop is driven by frame draws, we may need to manually pump + // events until we receive the 'create-screen' message from the logic + // thread which gets our frame draws going. + l_base->PrimeAppMainThreadEventPump(); + } + } catch (const std::exception& exc) { + std::string error_msg = + std::string("Unhandled exception in MonolithicMain(): ") + exc.what(); + + // Let the user and/or master-server know we're dying. + FatalError::ReportFatalError(error_msg, true); + + // Exiting the app via an exception leads to crash reports on various + // platforms. If it seems we're not on an official live build then we'd + // rather just exit cleanly with an error code and avoid polluting crash + // report logs with reports from dev builds. + bool try_to_exit_cleanly = !(l_base && l_base->IsUnmodifiedBlessedBuild()); + + // If this is true it means the app is handling things (showing a fatal + // error dialog, etc.) and it's out of our hands. + bool handled = FatalError::HandleFatalError(try_to_exit_cleanly, true); + + // Do the default thing if it's not been handled. + if (!handled) { + if (try_to_exit_cleanly) { + exit(1); + } else { + throw; // Crash report here we come! + } + } + } + + if (l_core) { + l_core->platform->WillExitMain(false); + return l_core->return_value; + } + return -1; // Didn't even get core; something clearly wrong. +} + +void FatalError(const std::string& message) { + // Let the user and/or master-server know we're dying. + FatalError::ReportFatalError(message, false); + + // Exiting the app via an exception leads to crash reports on various + // platforms. If it seems we're not on an official live build then we'd + // rather just exit cleanly with an error code and avoid polluting crash + // report logs with reports from dev builds. + bool try_to_exit_cleanly = + !(core::g_base_soft && core::g_base_soft->IsUnmodifiedBlessedBuild()); + bool handled = FatalError::HandleFatalError(try_to_exit_cleanly, false); + if (!handled) { + throw Exception("A fatal error occurred."); + } +} + +void Log(LogLevel level, const std::string& msg) { Logging::Log(level, msg); } + +void ScreenMessage(const std::string& s, const Vector3f& color) { + if (core::g_base_soft) { + core::g_base_soft->ScreenMessage(s, color); + } else { + Log(LogLevel::kError, + "ScreenMessage called without base feature-set loaded (will be lost): '" + + s + "'"); + } +} + +void ScreenMessage(const std::string& msg) { + ScreenMessage(msg, {1.0f, 1.0f, 1.0f}); +} + +auto CurrentThreadName() -> std::string { + // Currently just ask event-loop for this but perhaps should be talking + // more directly to the OS/etc. to cover more cases. + return EventLoop::CurrentThreadName(); +} + +} // namespace ballistica diff --git a/src/ballistica/ballistica.h b/src/ballistica/shared/ballistica.h similarity index 51% rename from src/ballistica/ballistica.h rename to src/ballistica/shared/ballistica.h index d4aef692..9fe8712c 100644 --- a/src/ballistica/ballistica.h +++ b/src/ballistica/shared/ballistica.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_BALLISTICA_H_ -#define BALLISTICA_BALLISTICA_H_ +#ifndef BALLISTICA_SHARED_BALLISTICA_H_ +#define BALLISTICA_SHARED_BALLISTICA_H_ // Try to ensure they're providing proper config stuff. #ifndef BA_HAVE_CONFIG @@ -13,20 +13,22 @@ #endif // Minimum functionality we want available everywhere we are included. -#include "ballistica/core/exception.h" -#include "ballistica/core/inline.h" -#include "ballistica/core/macros.h" -#include "ballistica/core/types.h" +#include "ballistica/shared/foundation/exception.h" +#include "ballistica/shared/foundation/inline.h" +#include "ballistica/shared/foundation/macros.h" +#include "ballistica/shared/foundation/types.h" // BA 2.0 UI testing. #define BA_TOOLBAR_TEST 0 +// There are one or two places where we include this from regular C +// or Objective-C code so want to gracefully handle that case. #ifdef __cplusplus namespace ballistica { -extern const int kAppBuildNumber; -extern const char* kAppVersion; +extern const int kEngineBuildNumber; +extern const char* kEngineVersion; // Protocol version we host games with and write replays to. // This should be incremented whenever there are changes made to the @@ -66,7 +68,6 @@ const int kProtocolVersionMin = 24; // 34: new image_node enums, data assets. const int kDefaultPort = 43210; -const int kDefaultTelnetPort = 43250; const float kTVBorder = 0.075f; const float kVRBorder = 0.085f; @@ -94,85 +95,27 @@ const float kPi = 3.1415926535897932384626433832795028841971693993751f; const float kPiDeg = kPi / 180.0f; const float kDegPi = 180.0f / kPi; -// Sim step size in milliseconds. -const int kGameStepMilliseconds = 8; - -// Sim step size in seconds. -const float kGameStepSeconds = - (static_cast(kGameStepMilliseconds) / 1000.0f); - -// Globals. -extern int g_early_v1_cloud_log_writes; -extern V1Account* g_v1_account; -extern AppFlavor* g_app_flavor; -extern AppConfig* g_app_config; -extern App* g_app; -extern AppInternal* g_app_internal; -extern Audio* g_audio; -extern AudioServer* g_audio_server; -extern BGDynamics* g_bg_dynamics; -extern BGDynamicsServer* g_bg_dynamics_server; -extern Context* g_context; -extern Graphics* g_graphics; -extern GraphicsServer* g_graphics_server; -extern Input* g_input; -extern Logic* g_logic; -extern Thread* g_main_thread; -extern Assets* g_assets; -extern AssetsServer* g_assets_server; -extern Networking* g_networking; -extern NetworkReader* g_network_reader; -extern NetworkWriter* g_network_writer; -extern Platform* g_platform; -extern Python* g_python; -extern SceneV1* g_scene_v1; -extern StdioConsole* g_stdio_console; -extern TextGraphics* g_text_graphics; -extern UI* g_ui; -extern Utils* g_utils; - -/// Main ballistica entry point. -auto BallisticaMain(int argc, char** argv) -> int; - -/// Return a string that should be universally unique to this particular -/// running instance of the app. -auto GetAppInstanceUUID() -> const std::string&; - -/// Have our main threads/modules all been inited yet? -auto IsBootstrapped() -> bool; - -/// Does it appear that we are a blessed build with no known user-modifications? -auto IsUnmodifiedBlessedBuild() -> bool; +namespace core { +class CoreConfig; +} // The following is a smattering of convenience functions declared in our top // level namespace. Functionality can be exposed here if it is used often // enough that avoiding the extra class includes seems like an overall // compile-time/convenience win. -// Print a momentary message on the screen. -auto ScreenMessage(const std::string& msg) -> void; -auto ScreenMessage(const std::string& msg, const Vector3f& color) -> void; +/// Entry point for standard monolithic builds. Handles all initing and running. +auto MonolithicMain(const core::CoreConfig& config) -> int; -// Check current-threads. -auto InMainThread() -> bool; // (main and graphics are same currently) -auto InGraphicsThread() -> bool; // (main and graphics are same currently) -auto InLogicThread() -> bool; -auto InAudioThread() -> bool; -auto InBGDynamicsThread() -> bool; -auto InAssetsThread() -> bool; -auto InNetworkWriteThread() -> bool; +// Print a momentary message on the screen. +void ScreenMessage(const std::string& msg); +void ScreenMessage(const std::string& msg, const Vector3f& color); /// Return a human-readable name for the current thread. -auto GetCurrentThreadName() -> std::string; +auto CurrentThreadName() -> std::string; -/// Submit a log entry. -/// Can be called from any thread at any time. -/// Use either this or Python printing functionality for anything -/// that should be seen by the user, as both of those will end up -/// in the in-app console, cloud based consoles, android log, etc. -/// Regular C level prints to stdout/stderr will not and will only -/// be visible on some platforms. -auto Log(LogLevel level, const std::string& msg) -> void; +/// Convenient access to Logging::Log. +void Log(LogLevel level, const std::string& msg); /// Log a fatal error and kill the app. /// Can be called from any thread at any time. @@ -180,32 +123,10 @@ auto Log(LogLevel level, const std::string& msg) -> void; /// This will attempt to ship all accumulated logs to the master-server /// so the standard Log() call can be used before this to include extra /// info not relevant to the end user. -auto FatalError(const std::string& message = "") -> void; - -/// Are we running in a VR environment? -auto IsVRMode() -> bool; - -/// Are we running headless? -inline auto HeadlessMode() -> bool { - // (currently a build-time value but this could change later) - return g_buildconfig.headless_build(); -} - -/// Return a lightly-filtered 'real' time value in milliseconds. -/// The value returned here will never go backwards or skip ahead -/// by significant amounts (even if the app has been sleeping or whatnot). -auto GetRealTime() -> millisecs_t; - -/// Return a random float value. Not guaranteed to be deterministic or -/// consistent across platforms. -inline auto RandomFloat() -> float { - // FIXME: should convert this to something thread-safe. - return static_cast( - (static_cast(rand()) / RAND_MAX)); // NOLINT -} +void FatalError(const std::string& message = ""); } // namespace ballistica #endif // __cplusplus -#endif // BALLISTICA_BALLISTICA_H_ +#endif // BALLISTICA_SHARED_BALLISTICA_H_ diff --git a/src/ballistica/config/config_cmake.h b/src/ballistica/shared/buildconfig/buildconfig_cmake.h similarity index 87% rename from src/ballistica/config/config_cmake.h rename to src/ballistica/shared/buildconfig/buildconfig_cmake.h index ececdaf0..8b3a2e0b 100644 --- a/src/ballistica/config/config_cmake.h +++ b/src/ballistica/shared/buildconfig/buildconfig_cmake.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_CONFIG_CONFIG_CMAKE_H_ -#define BALLISTICA_CONFIG_CONFIG_CMAKE_H_ +#ifndef BALLISTICA_SHARED_BUILDCONFIG_BUILDCONFIG_CMAKE_H_ +#define BALLISTICA_SHARED_BUILDCONFIG_BUILDCONFIG_CMAKE_H_ // For cmake builds, attempt to figure out what architecture we're running on // and define stuff accordingly. @@ -82,6 +82,6 @@ #endif // __cplusplus // This must always be last. -#include "ballistica/config/config_common.h" +#include "ballistica/shared/buildconfig/buildconfig_common.h" -#endif // BALLISTICA_CONFIG_CONFIG_CMAKE_H_ +#endif // BALLISTICA_SHARED_BUILDCONFIG_BUILDCONFIG_CMAKE_H_ diff --git a/src/ballistica/config/config_common.h b/src/ballistica/shared/buildconfig/buildconfig_common.h similarity index 85% rename from src/ballistica/config/config_common.h rename to src/ballistica/shared/buildconfig/buildconfig_common.h index 4a00f930..11caaa7c 100644 --- a/src/ballistica/config/config_common.h +++ b/src/ballistica/shared/buildconfig/buildconfig_common.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_CONFIG_CONFIG_COMMON_H_ -#define BALLISTICA_CONFIG_CONFIG_COMMON_H_ +#ifndef BALLISTICA_SHARED_BUILDCONFIG_BUILDCONFIG_COMMON_H_ +#define BALLISTICA_SHARED_BUILDCONFIG_BUILDCONFIG_COMMON_H_ #ifdef __cplusplus @@ -22,6 +22,13 @@ namespace ballistica { // Default definitions for various things. Per-platform configs // can override any of these before this is included. +// Monolithic builds consist of a single binary that inits and manages +// Python itself, as opposed to modular builds which are made up of +// Python binary modules which are run under a standard Python runtime. +#ifndef BA_MONOLITHIC_BUILD +#define BA_MONOLITHIC_BUILD 1 +#endif + #ifndef BA_STAT #define BA_STAT stat #endif @@ -106,6 +113,12 @@ namespace ballistica { #define BA_TEST_BUILD 0 #endif +// Does this build include its own full Python distribution? +// Builds such as linux may use rely on system provided ones. +#ifndef BA_CONTAINS_PYTHON_DIST +#define BA_CONTAINS_PYTHON_DIST 0 +#endif + #ifndef BA_ENABLE_SDL_JOYSTICKS #define BA_ENABLE_SDL_JOYSTICKS 0 #endif @@ -118,6 +131,10 @@ namespace ballistica { #define BA_USE_GAME_CENTER 0 #endif +#ifndef BA_USE_GOOGLE_PLAY_GAME_SERVICES +#define BA_USE_GOOGLE_PLAY_GAME_SERVICES 0 +#endif + #ifndef BA_PLATFORM_STRING #error platform string undefined #endif @@ -194,7 +211,7 @@ bool InlineDebugExplicitBool(bool val); #define EXPBOOL_(val) val #endif -// We define a compile-time value g_config which contains the same config +// We define a compile-time value g_buildconfig which contains the same config // values as our config #defines. We should migrate towards using these values // whenever possible instead of #if blocks, which should improve support for // code introspection/refactoring tools and type safety while still optimizing @@ -208,6 +225,7 @@ class BuildConfig { bool debug_build() const { return EXPBOOL_(BA_DEBUG_BUILD); } bool test_build() const { return EXPBOOL_(BA_TEST_BUILD); } bool headless_build() const { return EXPBOOL_(BA_HEADLESS_BUILD); } + bool monolithic_build() const { return EXPBOOL_(BA_MONOLITHIC_BUILD); } bool windows_console_build() const { return EXPBOOL_(BA_WINDOWS_CONSOLE_BUILD); } @@ -237,8 +255,13 @@ class BuildConfig { bool demo_build() const { return EXPBOOL_(BA_DEMO_BUILD); } bool arcade_build() const { return EXPBOOL_(BA_ARCADE_BUILD); } bool iircade_build() const { return EXPBOOL_(BA_IIRCADE_BUILD); } - + bool contains_python_dist() const { + return EXPBOOL_(BA_CONTAINS_PYTHON_DIST); + } bool use_store_kit() const { return EXPBOOL_(BA_USE_STORE_KIT); } + bool use_google_play_game_services() const { + return EXPBOOL_(BA_USE_GOOGLE_PLAY_GAME_SERVICES); + } bool use_game_center() const { return EXPBOOL_(BA_USE_GAME_CENTER); } bool enable_stdio_console() const { return EXPBOOL_(BA_ENABLE_STDIO_CONSOLE); @@ -259,4 +282,4 @@ constexpr BuildConfig g_buildconfig; #define BA_HAVE_CONFIG -#endif // BALLISTICA_CONFIG_CONFIG_COMMON_H_ +#endif // BALLISTICA_SHARED_BUILDCONFIG_BUILDCONFIG_COMMON_H_ diff --git a/src/ballistica/config/config_windows_common.h b/src/ballistica/shared/buildconfig/buildconfig_windows_common.h similarity index 87% rename from src/ballistica/config/config_windows_common.h rename to src/ballistica/shared/buildconfig/buildconfig_windows_common.h index 8ddf1e5a..2f745cce 100644 --- a/src/ballistica/config/config_windows_common.h +++ b/src/ballistica/shared/buildconfig/buildconfig_windows_common.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_CONFIG_CONFIG_WINDOWS_COMMON_H_ -#define BALLISTICA_CONFIG_CONFIG_WINDOWS_COMMON_H_ +#ifndef BALLISTICA_SHARED_BUILDCONFIG_BUILDCONFIG_WINDOWS_COMMON_H_ +#define BALLISTICA_SHARED_BUILDCONFIG_BUILDCONFIG_WINDOWS_COMMON_H_ #if _DEBUG #define BA_DEBUG_BUILD 1 @@ -39,6 +39,9 @@ #define BA_SOCKET_SEND_LENGTH_TYPE int #define BA_SOCKET_SETSOCKOPT_VAL_TYPE char +// On windows we always bundle python. +#define BA_CONTAINS_PYTHON_DIST 1 + // Make ssize_t available (copy/paste from mMinGW) #ifdef _MSC_VER #ifndef _SSIZE_T_DEFINED @@ -86,4 +89,4 @@ typedef int ssize_t; #include -#endif // BALLISTICA_CONFIG_CONFIG_WINDOWS_COMMON_H_ +#endif // BALLISTICA_SHARED_BUILDCONFIG_BUILDCONFIG_WINDOWS_COMMON_H_ diff --git a/src/ballistica/shared/buildconfig/buildconfig_windows_generic.h b/src/ballistica/shared/buildconfig/buildconfig_windows_generic.h new file mode 100644 index 00000000..fa6d8f52 --- /dev/null +++ b/src/ballistica/shared/buildconfig/buildconfig_windows_generic.h @@ -0,0 +1,19 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SHARED_BUILDCONFIG_BUILDCONFIG_WINDOWS_GENERIC_H_ +#define BALLISTICA_SHARED_BUILDCONFIG_BUILDCONFIG_WINDOWS_GENERIC_H_ + +// note: define overrides BEFORE common makefile + +#define BA_ENABLE_STDIO_CONSOLE 1 + +#define BA_SDL_BUILD 1 +#define BA_SDL2_BUILD 1 +#define BA_ENABLE_SDL_JOYSTICKS 1 + +#include "ballistica/shared/buildconfig/buildconfig_windows_common.h" + +// This must always be last. +#include "ballistica/shared/buildconfig/buildconfig_common.h" + +#endif // BALLISTICA_SHARED_BUILDCONFIG_BUILDCONFIG_WINDOWS_GENERIC_H_ diff --git a/src/ballistica/shared/buildconfig/buildconfig_windows_headless.h b/src/ballistica/shared/buildconfig/buildconfig_windows_headless.h new file mode 100644 index 00000000..bc68d0ab --- /dev/null +++ b/src/ballistica/shared/buildconfig/buildconfig_windows_headless.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SHARED_BUILDCONFIG_BUILDCONFIG_WINDOWS_HEADLESS_H_ +#define BALLISTICA_SHARED_BUILDCONFIG_BUILDCONFIG_WINDOWS_HEADLESS_H_ + +// note: define overrides BEFORE common header +#define BA_HEADLESS_BUILD 1 + +#define BA_ENABLE_STDIO_CONSOLE 1 + +#define BA_MINSDL_BUILD 1 + +#include "ballistica/shared/buildconfig/buildconfig_windows_common.h" + +// This must always be last. +#include "ballistica/shared/buildconfig/buildconfig_common.h" + +#endif // BALLISTICA_SHARED_BUILDCONFIG_BUILDCONFIG_WINDOWS_HEADLESS_H_ diff --git a/src/ballistica/core/thread.cc b/src/ballistica/shared/foundation/event_loop.cc similarity index 65% rename from src/ballistica/core/thread.cc rename to src/ballistica/shared/foundation/event_loop.cc index c1fae4a8..be44c4a3 100644 --- a/src/ballistica/core/thread.cc +++ b/src/ballistica/shared/foundation/event_loop.cc @@ -1,285 +1,53 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/core/thread.h" +#include "ballistica/shared/foundation/event_loop.h" -#include "ballistica/app/app_flavor.h" -#include "ballistica/core/fatal_error.h" -#include "ballistica/platform/platform.h" -#include "ballistica/python/python.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/core/python/core_python.h" +#include "ballistica/core/support/base_soft.h" +#include "ballistica/shared/foundation/fatal_error.h" +#include "ballistica/shared/python/python.h" namespace ballistica { -void Thread::SetInternalThreadName(const std::string& name) { - std::scoped_lock lock(g_app->thread_name_map_mutex); - g_app->thread_name_map[std::this_thread::get_id()] = name; -} +// Note: implicitly using core here so will fail spectacularly if that has +// not been imported by someone. +using core::g_base_soft; +using core::g_core; -void Thread::ClearCurrentThreadName() { - std::scoped_lock lock(g_app->thread_name_map_mutex); - auto i = g_app->thread_name_map.find(std::this_thread::get_id()); - if (i != g_app->thread_name_map.end()) { - g_app->thread_name_map.erase(i); - } -} - -void Thread::UpdateMainThreadID() { - auto current_id = std::this_thread::get_id(); - - // This gets called a lot and it may happen before we are spun up, - // so just ignore it in that case.. - if (g_app) { - g_app->main_thread_id = current_id; - } - if (g_app_flavor) { - g_app_flavor->thread()->set_thread_id(current_id); - } -} - -// These are all exactly the same; its just a way to try and clarify -// in stack traces which thread is running in case it is not otherwise -// evident. - -auto Thread::RunLogicThread(void* data) -> int { - return static_cast(data)->ThreadMain(); -} - -auto Thread::RunLogicThreadP(void* data) -> void* { - static_cast(data)->ThreadMain(); - return nullptr; -} - -auto Thread::RunAudioThread(void* data) -> int { - return static_cast(data)->ThreadMain(); -} -auto Thread::RunAudioThreadP(void* data) -> void* { - static_cast(data)->ThreadMain(); - return nullptr; -} - -auto Thread::RunBGDynamicThread(void* data) -> int { - return static_cast(data)->ThreadMain(); -} - -auto Thread::RunBGDynamicThreadP(void* data) -> void* { - static_cast(data)->ThreadMain(); - return nullptr; -} - -auto Thread::RunNetworkWriteThread(void* data) -> int { - return static_cast(data)->ThreadMain(); -} - -auto Thread::RunNetworkWriteThreadP(void* data) -> void* { - static_cast(data)->ThreadMain(); - return nullptr; -} - -auto Thread::RunStdInputThread(void* data) -> int { - return static_cast(data)->ThreadMain(); -} -auto Thread::RunStdInputThreadP(void* data) -> void* { - static_cast(data)->ThreadMain(); - return nullptr; -} - -auto Thread::RunAssetsThread(void* data) -> int { - return static_cast(data)->ThreadMain(); -} - -auto Thread::RunAssetsThreadP(void* data) -> void* { - static_cast(data)->ThreadMain(); - return nullptr; -} - -void Thread::PushSetPaused(bool paused) { - // Can be toggled from the main thread only. - assert(std::this_thread::get_id() == g_app->main_thread_id); - PushThreadMessage(ThreadMessage(paused ? ThreadMessage::Type::kPause - : ThreadMessage::Type::kResume)); -} - -void Thread::WaitForNextEvent(bool single_cycle) { - // If we're running a single cycle we never stop to wait. - if (single_cycle) { - // Need to revisit this if we ever do single-cycle for - // the gil-holding thread so we don't starve other Python threads. - assert(!acquires_python_gil_); - return; - } - - // We also never wait if we have pending runnables; we wan't to run - // things as soon as we can. We chew through all runnables at the end - // of the loop so it might seem like there should never be any here, - // but runnables can add other runnables that won't get processed until - // the next time through. - // BUG FIX: We now skip this if we're paused since we don't run runnables - // in that case. This was preventing us from releasing the GIL while paused - // (and I assume causing us to spin full-speed through the loop; ugh). - // NOTE: It is theoretically possible for a runnable to add another runnable - // each time through the loop which would effectively starve the GIL as - // well; do we need to worry about that case? - if (has_pending_runnables() && !paused_) { - return; - } - - // While we're waiting, allow other python threads to run. - if (acquires_python_gil_) { - g_python->ReleaseGIL(); - } - - // If we've got active timers, wait for messages with a timeout so we can - // run the next timer payload. - if (!paused_ && timers_.active_timer_count() > 0) { - millisecs_t real_time = GetRealTime(); - millisecs_t wait_time = timers_.GetTimeToNextExpire(real_time); - if (wait_time > 0) { - std::unique_lock lock(thread_message_mutex_); - if (thread_messages_.empty()) { - thread_message_cv_.wait_for(lock, std::chrono::milliseconds(wait_time), - [this] { - // Go back to sleep on spurious wakeups - // if we didn't wind up with any new - // messages. - return !thread_messages_.empty(); - }); - } - } - } else { - // Not running timers; just wait indefinitely for the next message. - std::unique_lock lock(thread_message_mutex_); - if (thread_messages_.empty()) { - thread_message_cv_.wait(lock, [this] { - // Go back to sleep on spurious wakeups - // (if we didn't wind up with any new messages). - return !(thread_messages_.empty()); - }); - } - } - - if (acquires_python_gil_) { - g_python->AcquireGIL(); - } -} - -void Thread::LoopUpkeep(bool single_cycle) { - // Keep our autorelease pool clean on mac/ios - // FIXME: Should define a Platform::ThreadHelper or something - // so we don't have platform-specific code here. -#if BA_XCODE_BUILD - // Let's not do autorelease pools when being called ad-hoc, - // since in that case we're part of another run loop - // (and its crashing on drain for some reason) - if (!single_cycle) { - if (auto_release_pool_) { - g_platform->DrainAutoReleasePool(auto_release_pool_); - auto_release_pool_ = nullptr; - } - auto_release_pool_ = g_platform->NewAutoReleasePool(); - } -#endif -} - -auto Thread::RunEventLoop(bool single_cycle) -> int { - while (true) { - LoopUpkeep(single_cycle); - - WaitForNextEvent(single_cycle); - - // Process all queued thread messages. - std::list thread_messages; - GetThreadMessages(&thread_messages); - for (auto& thread_message : thread_messages) { - switch (thread_message.type) { - case ThreadMessage::Type::kRunnable: { - PushLocalRunnable(thread_message.runnable, - thread_message.completion_flag); - break; - } - case ThreadMessage::Type::kShutdown: { - done_ = true; - break; - } - case ThreadMessage::Type::kPause: { - assert(!paused_); - RunPauseCallbacks(); - paused_ = true; - last_pause_time_ = GetRealTime(); - messages_since_paused_ = 0; - break; - } - case ThreadMessage::Type::kResume: { - assert(paused_); - RunResumeCallbacks(); - paused_ = false; - break; - } - default: { - throw Exception(); - } - } - - if (done_) { - break; - } - } - - if (!paused_) { - timers_.Run(GetRealTime()); - RunPendingRunnables(); - } - - if (done_ || single_cycle) { - break; - } - } - return 0; -} - -void Thread::GetThreadMessages(std::list* messages) { - assert(messages); - assert(std::this_thread::get_id() == thread_id()); - - // Make sure they passed an empty one in. - assert(messages->empty()); - std::scoped_lock lock(thread_message_mutex_); - if (!thread_messages_.empty()) { - messages->swap(thread_messages_); - } -} - -Thread::Thread(ThreadTag identifier_in, ThreadSource source) +EventLoop::EventLoop(EventLoopID identifier_in, ThreadSource source) : source_(source), identifier_(identifier_in) { switch (source_) { case ThreadSource::kCreate: { int (*func)(void*); void* (*funcp)(void*); switch (identifier_) { - case ThreadTag::kLogic: - func = RunLogicThread; - funcp = RunLogicThreadP; + case EventLoopID::kLogic: + func = ThreadMainLogic; + funcp = ThreadMainLogicP; break; - case ThreadTag::kAssets: - func = RunAssetsThread; - funcp = RunAssetsThreadP; + case EventLoopID::kAssets: + func = ThreadMainAssets; + funcp = ThreadMainAssetsP; break; - case ThreadTag::kMain: + case EventLoopID::kMain: // Shouldn't happen; this thread gets wrapped; not launched. throw Exception(); - case ThreadTag::kAudio: - func = RunAudioThread; - funcp = RunAudioThreadP; + case EventLoopID::kAudio: + func = ThreadMainAudio; + funcp = ThreadMainAudioP; break; - case ThreadTag::kBGDynamics: - func = RunBGDynamicThread; - funcp = RunBGDynamicThreadP; + case EventLoopID::kBGDynamics: + func = ThreadMainBGDynamics; + funcp = ThreadMainBGDynamicsP; break; - case ThreadTag::kNetworkWrite: - func = RunNetworkWriteThread; - funcp = RunNetworkWriteThreadP; + case EventLoopID::kNetworkWrite: + func = ThreadMainNetworkWrite; + funcp = ThreadMainNetworkWriteP; break; - case ThreadTag::kStdin: - func = RunStdInputThread; - funcp = RunStdInputThreadP; + case EventLoopID::kStdin: + func = ThreadMainStdInput; + funcp = ThreadMainStdInputP; break; default: throw Exception(); @@ -316,7 +84,7 @@ Thread::Thread(ThreadTag identifier_in, ThreadSource source) case ThreadSource::kWrapMain: { // We've got no thread of our own to launch // so we run our setup stuff right here instead of off in some. - assert(std::this_thread::get_id() == g_app->main_thread_id); + assert(std::this_thread::get_id() == g_core->main_thread_id); thread_id_ = std::this_thread::get_id(); // Set our own thread-id-to-name mapping. @@ -331,7 +99,241 @@ Thread::Thread(ThreadTag identifier_in, ThreadSource source) } } -auto Thread::ThreadMain() -> int { +void EventLoop::SetInternalThreadName(const std::string& name) { + assert(g_core); + std::scoped_lock lock(g_core->thread_name_map_mutex); + g_core->thread_name_map[std::this_thread::get_id()] = name; +} + +void EventLoop::ClearCurrentThreadName() { + assert(g_core); + std::scoped_lock lock(g_core->thread_name_map_mutex); + auto i = g_core->thread_name_map.find(std::this_thread::get_id()); + if (i != g_core->thread_name_map.end()) { + g_core->thread_name_map.erase(i); + } +} + +// These are all exactly the same; its just a way to try and clarify +// in stack traces which thread is running in case it is not otherwise +// evident. + +auto EventLoop::ThreadMainLogic(void* data) -> int { + return static_cast(data)->ThreadMain(); +} + +auto EventLoop::ThreadMainLogicP(void* data) -> void* { + static_cast(data)->ThreadMain(); + return nullptr; +} + +auto EventLoop::ThreadMainAudio(void* data) -> int { + return static_cast(data)->ThreadMain(); +} +auto EventLoop::ThreadMainAudioP(void* data) -> void* { + static_cast(data)->ThreadMain(); + return nullptr; +} + +auto EventLoop::ThreadMainBGDynamics(void* data) -> int { + return static_cast(data)->ThreadMain(); +} + +auto EventLoop::ThreadMainBGDynamicsP(void* data) -> void* { + static_cast(data)->ThreadMain(); + return nullptr; +} + +auto EventLoop::ThreadMainNetworkWrite(void* data) -> int { + return static_cast(data)->ThreadMain(); +} + +auto EventLoop::ThreadMainNetworkWriteP(void* data) -> void* { + static_cast(data)->ThreadMain(); + return nullptr; +} + +auto EventLoop::ThreadMainStdInput(void* data) -> int { + return static_cast(data)->ThreadMain(); +} + +auto EventLoop::ThreadMainStdInputP(void* data) -> void* { + static_cast(data)->ThreadMain(); + return nullptr; +} + +auto EventLoop::ThreadMainAssets(void* data) -> int { + return static_cast(data)->ThreadMain(); +} + +auto EventLoop::ThreadMainAssetsP(void* data) -> void* { + static_cast(data)->ThreadMain(); + return nullptr; +} + +void EventLoop::PushSetPaused(bool paused) { + assert(g_core); + // Can be toggled from the main thread only. + assert(std::this_thread::get_id() == g_core->main_thread_id); + PushThreadMessage(ThreadMessage(paused ? ThreadMessage::Type::kPause + : ThreadMessage::Type::kResume)); +} + +void EventLoop::WaitForNextEvent(bool single_cycle) { + assert(g_core); + + // If we're running a single cycle we never stop to wait. + if (single_cycle) { + // Need to revisit this if we ever do single-cycle for + // the gil-holding thread so we don't starve other Python threads. + assert(!acquires_python_gil_); + return; + } + + // We also never wait if we have pending runnables; we want to run + // things as soon as we can. We chew through all runnables at the end + // of the loop so it might seem like there should never be any here, + // but runnables can add other runnables that won't get processed until + // the next time through. + // BUG FIX: We now skip this if we're paused since we don't run runnables + // in that case. This was preventing us from releasing the GIL while paused + // (and I assume causing us to spin full-speed through the loop; ugh). + // NOTE: It is theoretically possible for a runnable to add another runnable + // each time through the loop which would effectively starve the GIL as + // well; do we need to worry about that case? + if (has_pending_runnables() && !paused_) { + return; + } + + // While we're waiting, allow other python threads to run. + if (acquires_python_gil_) { + g_core->python->ReleaseGIL(); + } + + // If we've got active timers, wait for messages with a timeout so we can + // run the next timer payload. + if (!paused_ && timers_.ActiveTimerCount() > 0) { + millisecs_t apptime = g_core->GetAppTimeMillisecs(); + millisecs_t wait_time = timers_.TimeToNextExpire(apptime); + if (wait_time > 0) { + std::unique_lock lock(thread_message_mutex_); + if (thread_messages_.empty()) { + thread_message_cv_.wait_for(lock, std::chrono::milliseconds(wait_time), + [this] { + // Go back to sleep on spurious wakeups + // if we didn't wind up with any new + // messages. + return !thread_messages_.empty(); + }); + } + } + } else { + // Not running timers; just wait indefinitely for the next message. + std::unique_lock lock(thread_message_mutex_); + if (thread_messages_.empty()) { + thread_message_cv_.wait(lock, [this] { + // Go back to sleep on spurious wakeups + // (if we didn't wind up with any new messages). + return !(thread_messages_.empty()); + }); + } + } + + if (acquires_python_gil_) { + g_core->python->AcquireGIL(); + } +} + +void EventLoop::LoopUpkeep(bool single_cycle) { + assert(g_core); + // Keep our autorelease pool clean on mac/ios + // FIXME: Should define a CorePlatform::ThreadHelper or something + // so we don't have platform-specific code here. +#if BA_XCODE_BUILD + // Let's not do autorelease pools when being called ad-hoc, + // since in that case we're part of another run loop + // (and its crashing on drain for some reason) + if (!single_cycle) { + if (auto_release_pool_) { + g_core->platform->DrainAutoReleasePool(auto_release_pool_); + auto_release_pool_ = nullptr; + } + auto_release_pool_ = g_core->platform->NewAutoReleasePool(); + } +#endif +} + +auto EventLoop::RunEventLoop(bool single_cycle) -> int { + assert(g_core); + while (true) { + LoopUpkeep(single_cycle); + + WaitForNextEvent(single_cycle); + + // Process all queued thread messages. + std::list thread_messages; + GetThreadMessages(&thread_messages); + for (auto& thread_message : thread_messages) { + switch (thread_message.type) { + case ThreadMessage::Type::kRunnable: { + PushLocalRunnable(thread_message.runnable, + thread_message.completion_flag); + break; + } + case ThreadMessage::Type::kShutdown: { + done_ = true; + break; + } + case ThreadMessage::Type::kPause: { + assert(!paused_); + RunPauseCallbacks(); + paused_ = true; + last_pause_time_ = g_core->GetAppTimeMillisecs(); + messages_since_paused_ = 0; + break; + } + case ThreadMessage::Type::kResume: { + assert(paused_); + RunResumeCallbacks(); + paused_ = false; + break; + } + default: { + throw Exception(); + } + } + + if (done_) { + break; + } + } + + if (!paused_) { + timers_.Run(g_core->GetAppTimeMillisecs()); + RunPendingRunnables(); + } + + if (done_ || single_cycle) { + break; + } + } + return 0; +} + +void EventLoop::GetThreadMessages(std::list* messages) { + assert(messages); + assert(std::this_thread::get_id() == thread_id()); + + // Make sure they passed an empty one in. + assert(messages->empty()); + std::scoped_lock lock(thread_message_mutex_); + if (!thread_messages_.empty()) { + messages->swap(thread_messages_); + } +} + +auto EventLoop::ThreadMain() -> int { + assert(g_core); try { assert(source_ == ThreadSource::kCreate); thread_id_ = std::this_thread::get_id(); @@ -339,35 +341,35 @@ auto Thread::ThreadMain() -> int { const char* id_string; switch (identifier_) { - case ThreadTag::kLogic: + case EventLoopID::kLogic: name = "logic"; id_string = "ballistica logic"; break; - case ThreadTag::kStdin: + case EventLoopID::kStdin: name = "stdin"; id_string = "ballistica stdin"; break; - case ThreadTag::kAssets: + case EventLoopID::kAssets: name = "assets"; id_string = "ballistica assets"; break; - case ThreadTag::kFileOut: + case EventLoopID::kFileOut: name = "fileout"; id_string = "ballistica file-out"; break; - case ThreadTag::kMain: + case EventLoopID::kMain: name = "main"; id_string = "ballistica main"; break; - case ThreadTag::kAudio: + case EventLoopID::kAudio: name = "audio"; id_string = "ballistica audio"; break; - case ThreadTag::kBGDynamics: + case EventLoopID::kBGDynamics: name = "bgdynamics"; id_string = "ballistica bg-dynamics"; break; - case ThreadTag::kNetworkWrite: + case EventLoopID::kNetworkWrite: name = "networkwrite"; id_string = "ballistica network writing"; break; @@ -376,7 +378,7 @@ auto Thread::ThreadMain() -> int { } assert(name && id_string); SetInternalThreadName(name); - g_platform->SetCurrentThreadName(id_string); + g_core->platform->SetCurrentThreadName(id_string); // Mark ourself as bootstrapped and signal listeners so // anyone waiting for us to spin up can move along. @@ -390,15 +392,21 @@ auto Thread::ThreadMain() -> int { return result; } catch (const std::exception& e) { auto error_msg = std::string("Unhandled exception in ") - + GetCurrentThreadName() + " thread:\n" + e.what(); + + CurrentThreadName() + " thread:\n" + e.what(); FatalError::ReportFatalError(error_msg, true); - bool exit_cleanly = !IsUnmodifiedBlessedBuild(); - bool handled = FatalError::HandleFatalError(exit_cleanly, true); + + // Exiting the app via an exception leads to crash reports on various + // platforms. If it seems we're not on an official live build then we'd + // rather just exit cleanly with an error code and avoid polluting crash + // report logs with reports from dev builds. + bool try_to_exit_cleanly = + !(g_base_soft && g_base_soft->IsUnmodifiedBlessedBuild()); + bool handled = FatalError::HandleFatalError(try_to_exit_cleanly, true); // Do the default thing if platform didn't handle it. if (!handled) { - if (exit_cleanly) { + if (try_to_exit_cleanly) { exit(1); } else { throw; @@ -413,35 +421,37 @@ auto Thread::ThreadMain() -> int { } } -void Thread::SetAcquiresPythonGIL() { +void EventLoop::SetAcquiresPythonGIL() { + assert(g_core); assert(!acquires_python_gil_); // This should be called exactly once. - assert(IsCurrent()); + assert(ThreadIsCurrent()); acquires_python_gil_ = true; - g_python->AcquireGIL(); + g_core->python->AcquireGIL(); } // Explicitly kill the main thread. -void Thread::Quit() { +void EventLoop::Quit() { assert(source_ == ThreadSource::kWrapMain); if (source_ == ThreadSource::kWrapMain) { done_ = true; } } -Thread::~Thread() = default; +EventLoop::~EventLoop() = default; #pragma clang diagnostic push #pragma ide diagnostic ignored "ConstantConditionsOC" -void Thread::LogThreadMessageTally( +void EventLoop::LogThreadMessageTally( std::vector>* log_entries) { + assert(g_core); // Prevent recursion. if (!writing_tally_) { writing_tally_ = true; std::unordered_map tally; log_entries->emplace_back(std::make_pair( - LogLevel::kError, "Thread message tally (" + LogLevel::kError, "EventLoop message tally (" + std::to_string(thread_messages_.size()) + " in list):")); for (auto&& m : thread_messages_) { @@ -465,7 +475,7 @@ void Thread::LogThreadMessageTally( } if (m.type == ThreadMessage::Type::kRunnable) { std::string m_name = - g_platform->DemangleCXXSymbol(typeid(*(m.runnable)).name()); + g_core->platform->DemangleCXXSymbol(typeid(*(m.runnable)).name()); s += std::string(": ") + m_name; } auto j = tally.find(s); @@ -486,7 +496,8 @@ void Thread::LogThreadMessageTally( } #pragma clang diagnostic pop -void Thread::PushThreadMessage(const ThreadMessage& t) { +void EventLoop::PushThreadMessage(const ThreadMessage& t) { + assert(g_core); // We don't want to make log calls while holding this mutex; // log calls acquire the GIL and if the GIL-holder (generally // the logic thread) is trying to send a thread message to the @@ -513,11 +524,11 @@ void Thread::PushThreadMessage(const ThreadMessage& t) { } // Show count periodically. - if ((std::this_thread::get_id() == g_app->main_thread_id) && foo > 100) { + if ((std::this_thread::get_id() == g_core->main_thread_id) && foo > 100) { foo = 0; - log_entries.emplace_back(std::make_pair( + log_entries.emplace_back( LogLevel::kInfo, - "MSG COUNT " + std::to_string(thread_messages_.size()))); + "MSG COUNT " + std::to_string(thread_messages_.size())); } } @@ -525,9 +536,9 @@ void Thread::PushThreadMessage(const ThreadMessage& t) { static bool sent_error = false; if (!sent_error) { sent_error = true; - log_entries.emplace_back(std::make_pair( + log_entries.emplace_back( LogLevel::kError, - "ThreadMessage list > 1000 in thread: " + GetCurrentThreadName())); + "ThreadMessage list > 1000 in thread: " + CurrentThreadName()); LogThreadMessageTally(&log_entries); } @@ -536,7 +547,7 @@ void Thread::PushThreadMessage(const ThreadMessage& t) { // Prevent runaway mem usage if the list gets out of control. if (thread_messages_.size() > 10000) { FatalError("ThreadMessage list > 10000 in thread: " - + GetCurrentThreadName()); + + CurrentThreadName()); } // Unlock thread-message list and inform thread that there's something @@ -550,21 +561,23 @@ void Thread::PushThreadMessage(const ThreadMessage& t) { } } -auto Thread::SetThreadsPaused(bool paused) -> void { - assert(std::this_thread::get_id() == g_app->main_thread_id); - g_app->threads_paused = paused; - for (auto&& i : g_app->pausable_threads) { +void EventLoop::SetThreadsPaused(bool paused) { + assert(g_core); + assert(std::this_thread::get_id() == g_core->main_thread_id); + g_core->threads_paused = paused; + for (auto&& i : g_core->pausable_event_loops) { i->PushSetPaused(paused); } } -auto Thread::GetStillPausingThreads() -> std::vector { - std::vector threads; - assert(std::this_thread::get_id() == g_app->main_thread_id); +auto EventLoop::GetStillPausingThreads() -> std::vector { + assert(g_core); + std::vector threads; + assert(std::this_thread::get_id() == g_core->main_thread_id); // Only return results if an actual pause is in effect. - if (g_app->threads_paused) { - for (auto&& i : g_app->pausable_threads) { + if (g_core->threads_paused) { + for (auto&& i : g_core->pausable_event_loops) { if (!i->paused()) { threads.push_back(i); } @@ -573,23 +586,38 @@ auto Thread::GetStillPausingThreads() -> std::vector { return threads; } -auto Thread::AreThreadsPaused() -> bool { return g_app->threads_paused; } - -auto Thread::NewTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> Timer* { - assert(IsCurrent()); - assert(runnable.exists()); - return timers_.NewTimer(GetRealTime(), length, 0, repeat ? -1 : 0, runnable); +auto EventLoop::AreThreadsPaused() -> bool { + assert(g_core); + return g_core->threads_paused; } -auto Thread::GetCurrentThreadName() -> std::string { - if (g_app == nullptr) { +auto EventLoop::NewTimer(millisecs_t length, bool repeat, + const Object::Ref& runnable) -> Timer* { + assert(g_core); + assert(ThreadIsCurrent()); + assert(runnable.Exists()); + return timers_.NewTimer(g_core->GetAppTimeMillisecs(), length, 0, + repeat ? -1 : 0, runnable); +} + +Timer* EventLoop::GetTimer(int id) { + assert(ThreadIsCurrent()); + return timers_.GetTimer(id); +} + +void EventLoop::DeleteTimer(int id) { + assert(ThreadIsCurrent()); + timers_.DeleteTimer(id); +} + +auto EventLoop::CurrentThreadName() -> std::string { + if (g_core == nullptr) { return "unknown(not-yet-inited)"; } { - std::scoped_lock lock(g_app->thread_name_map_mutex); - auto i = g_app->thread_name_map.find(std::this_thread::get_id()); - if (i != g_app->thread_name_map.end()) { + std::scoped_lock lock(g_core->thread_name_map_mutex); + auto i = g_core->thread_name_map.find(std::this_thread::get_id()); + if (i != g_core->thread_name_map.end()) { return i->second; } } @@ -611,7 +639,7 @@ auto Thread::GetCurrentThreadName() -> std::string { #endif } -void Thread::RunPendingRunnables() { +void EventLoop::RunPendingRunnables() { // Pull all runnables off the list first (its possible for one of these // runnables to add more) and then process them. assert(std::this_thread::get_id() == thread_id()); @@ -634,40 +662,41 @@ void Thread::RunPendingRunnables() { } } -void Thread::RunPauseCallbacks() { +void EventLoop::RunPauseCallbacks() { for (Runnable* i : pause_callbacks_) { i->Run(); } } -void Thread::RunResumeCallbacks() { +void EventLoop::RunResumeCallbacks() { for (Runnable* i : resume_callbacks_) { i->Run(); } } -void Thread::PushLocalRunnable(Runnable* runnable, bool* completion_flag) { +void EventLoop::PushLocalRunnable(Runnable* runnable, bool* completion_flag) { assert(std::this_thread::get_id() == thread_id()); - runnables_.push_back(std::make_pair(runnable, completion_flag)); + runnables_.emplace_back(runnable, completion_flag); } -void Thread::PushCrossThreadRunnable(Runnable* runnable, - bool* completion_flag) { - PushThreadMessage(Thread::ThreadMessage( - Thread::ThreadMessage::Type::kRunnable, runnable, completion_flag)); +void EventLoop::PushCrossThreadRunnable(Runnable* runnable, + bool* completion_flag) { + PushThreadMessage(EventLoop::ThreadMessage( + EventLoop::ThreadMessage::Type::kRunnable, runnable, completion_flag)); } -void Thread::AddPauseCallback(Runnable* runnable) { +void EventLoop::AddPauseCallback(Runnable* runnable) { assert(std::this_thread::get_id() == thread_id()); pause_callbacks_.push_back(runnable); } -void Thread::AddResumeCallback(Runnable* runnable) { +void EventLoop::AddResumeCallback(Runnable* runnable) { assert(std::this_thread::get_id() == thread_id()); resume_callbacks_.push_back(runnable); } -void Thread::PushRunnable(Runnable* runnable) { +void EventLoop::PushRunnable(Runnable* runnable) { + assert(Object::IsValidUnmanagedObject(runnable)); // If we're being called from withing our thread, just drop it in the list. // otherwise send it as a message to the other thread. if (std::this_thread::get_id() == thread_id()) { @@ -677,7 +706,7 @@ void Thread::PushRunnable(Runnable* runnable) { } } -void Thread::PushRunnableSynchronous(Runnable* runnable) { +void EventLoop::PushRunnableSynchronous(Runnable* runnable) { bool complete{}; bool* complete_ptr{&complete}; if (std::this_thread::get_id() == thread_id()) { @@ -697,7 +726,7 @@ void Thread::PushRunnableSynchronous(Runnable* runnable) { }); } -auto Thread::CheckPushSafety() -> bool { +auto EventLoop::CheckPushSafety() -> bool { if (std::this_thread::get_id() == thread_id()) { // behave the same as the thread-message safety check. return (runnables_.size() < kThreadMessageSafetyThreshold); @@ -705,7 +734,7 @@ auto Thread::CheckPushSafety() -> bool { return CheckPushRunnableSafety(); } } -auto Thread::CheckPushRunnableSafety() -> bool { +auto EventLoop::CheckPushRunnableSafety() -> bool { std::scoped_lock lock(client_listener_mutex_); // We first complain when we get to 1000 queued messages so diff --git a/src/ballistica/core/thread.h b/src/ballistica/shared/foundation/event_loop.h similarity index 54% rename from src/ballistica/core/thread.h rename to src/ballistica/shared/foundation/event_loop.h index a0c5c594..ff93384b 100644 --- a/src/ballistica/core/thread.h +++ b/src/ballistica/shared/foundation/event_loop.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_CORE_THREAD_H_ -#define BALLISTICA_CORE_THREAD_H_ +#ifndef BALLISTICA_SHARED_FOUNDATION_EVENT_LOOP_H_ +#define BALLISTICA_SHARED_FOUNDATION_EVENT_LOOP_H_ #include #include @@ -10,33 +10,30 @@ #include #include -#include "ballistica/app/app.h" -#include "ballistica/ballistica.h" -#include "ballistica/generic/lambda_runnable.h" -#include "ballistica/generic/timer_list.h" -#include "ballistica/platform/min_sdl.h" +#include "ballistica/core/core.h" +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/generic/lambda_runnable.h" +#include "ballistica/shared/generic/timer_list.h" namespace ballistica { const int kThreadMessageSafetyThreshold{500}; // A thread with a built-in event loop. -class Thread { +class EventLoop { public: - explicit Thread(ThreadTag id, ThreadSource source = ThreadSource::kCreate); - virtual ~Thread(); + explicit EventLoop(EventLoopID id, + ThreadSource source = ThreadSource::kCreate); + virtual ~EventLoop(); - auto ClearCurrentThreadName() -> void; + void ClearCurrentThreadName(); - static auto GetCurrentThreadName() -> std::string; + static auto CurrentThreadName() -> std::string; - /// Call this if the main thread changes. - static auto UpdateMainThreadID() -> void; - - static auto SetThreadsPaused(bool enable) -> void; + static void SetThreadsPaused(bool enable); static auto AreThreadsPaused() -> bool; - auto IsCurrent() const -> bool { + auto ThreadIsCurrent() const -> bool { return std::this_thread::get_id() == thread_id(); } @@ -55,38 +52,39 @@ class Thread { void set_thread_id(std::thread::id id) { thread_id_ = id; } auto RunEventLoop(bool single_cycle = false) -> int; - auto identifier() const -> ThreadTag { return identifier_; } + auto identifier() const -> EventLoopID { return identifier_; } // Register a timer to run on the thread. auto NewTimer(millisecs_t length, bool repeat, const Object::Ref& runnable) -> Timer*; + Timer* GetTimer(int id); + void DeleteTimer(int id); /// Add a runnable to this thread's event-loop. - /// Pass a Runnable that has been allocated with new(). - /// There must be no existing strong refs to it. - /// It will be owned by the thread - auto PushRunnable(Runnable* runnable) -> void; + /// Pass a Runnable that has been allocated with NewUnmanaged(). + /// It will be owned and disposed of by the thread. + void PushRunnable(Runnable* runnable); /// Convenience function to push a lambda as a runnable. template - auto PushCall(const F& lambda) -> void { - PushRunnable(NewLambdaRunnableRaw(lambda)); + void PushCall(const F& lambda) { + PushRunnable(NewLambdaRunnableUnmanaged(lambda)); } /// Add a runnable to this thread's event-loop and wait until it completes. - auto PushRunnableSynchronous(Runnable* runnable) -> void; + void PushRunnableSynchronous(Runnable* runnable); /// Convenience function to push a lambda as a runnable. template - auto PushCallSynchronous(const F& lambda) -> void { - PushRunnableSynchronous(NewLambdaRunnableRaw(lambda)); + void PushCallSynchronous(const F& lambda) { + PushRunnableSynchronous(NewLambdaRunnableUnmanaged(lambda)); } /// Add a callback to be run on event-loop pauses. - auto AddPauseCallback(Runnable* runnable) -> void; + void AddPauseCallback(Runnable* runnable); /// Add a callback to be run on event-loop resumes. - auto AddResumeCallback(Runnable* runnable) -> void; + void AddResumeCallback(Runnable* runnable); auto has_pending_runnables() const -> bool { return !runnables_.empty(); } @@ -98,7 +96,7 @@ class Thread { /// the app through a flood of packets. auto CheckPushSafety() -> bool; - static auto GetStillPausingThreads() -> std::vector; + static auto GetStillPausingThreads() -> std::vector; auto paused() { return paused_; } @@ -107,7 +105,7 @@ class Thread { enum class Type { kShutdown = 999, kRunnable, kPause, kResume }; Type type; union { - Runnable* runnable; + Runnable* runnable{}; }; bool* completion_flag{}; explicit ThreadMessage(Type type_in) : type(type_in) {} @@ -115,15 +113,14 @@ class Thread { : type(type), runnable(runnable), completion_flag{completion_flag} {} }; auto CheckPushRunnableSafety() -> bool; - auto SetInternalThreadName(const std::string& name) -> void; - auto WaitForNextEvent(bool single_cycle) -> void; - auto LoopUpkeep(bool once) -> void; - auto LogThreadMessageTally( - std::vector>* log_entries) -> void; - auto PushLocalRunnable(Runnable* runnable, bool* completion_flag) -> void; - auto PushCrossThreadRunnable(Runnable* runnable, bool* completion_flag) - -> void; - auto NotifyClientListeners() -> void; + void SetInternalThreadName(const std::string& name); + void WaitForNextEvent(bool single_cycle); + void LoopUpkeep(bool once); + void LogThreadMessageTally( + std::vector>* log_entries); + void PushLocalRunnable(Runnable* runnable, bool* completion_flag); + void PushCrossThreadRunnable(Runnable* runnable, bool* completion_flag); + void NotifyClientListeners(); bool writing_tally_{}; bool paused_{}; @@ -134,7 +131,7 @@ class Thread { ThreadSource source_; int listen_sd_{}; std::thread::id thread_id_{}; - ThreadTag identifier_{ThreadTag::kInvalid}; + EventLoopID identifier_{EventLoopID::kInvalid}; millisecs_t last_complaint_time_{}; bool acquires_python_gil_{}; @@ -143,35 +140,34 @@ class Thread { void* auto_release_pool_{}; #endif - // These are all exactly the same, but by running different ones for - // different thread groups makes its easy to see which thread is which - // in profilers, backtraces, etc. - static auto RunLogicThread(void* data) -> int; - static auto RunLogicThreadP(void* data) -> void*; - static auto RunAudioThread(void* data) -> int; - static auto RunAudioThreadP(void* data) -> void*; - static auto RunBGDynamicThread(void* data) -> int; - static auto RunBGDynamicThreadP(void* data) -> void*; - static auto RunNetworkWriteThread(void* data) -> int; - static auto RunNetworkWriteThreadP(void* data) -> void*; - static auto RunStdInputThread(void* data) -> int; - static auto RunStdInputThreadP(void* data) -> void*; - static auto RunAssetsThread(void* data) -> int; - static auto RunAssetsThreadP(void* data) -> void*; + // These are all exactly the same, but running different ones for + // different threads can help identify threads in profilers, backtraces, + // etc. + static auto ThreadMainLogic(void* data) -> int; + static auto ThreadMainLogicP(void* data) -> void*; + static auto ThreadMainAudio(void* data) -> int; + static auto ThreadMainAudioP(void* data) -> void*; + static auto ThreadMainBGDynamics(void* data) -> int; + static auto ThreadMainBGDynamicsP(void* data) -> void*; + static auto ThreadMainNetworkWrite(void* data) -> int; + static auto ThreadMainNetworkWriteP(void* data) -> void*; + static auto ThreadMainStdInput(void* data) -> int; + static auto ThreadMainStdInputP(void* data) -> void*; + static auto ThreadMainAssets(void* data) -> int; + static auto ThreadMainAssetsP(void* data) -> void*; auto ThreadMain() -> int; - auto GetThreadMessages(std::list* messages) -> void; - auto PushThreadMessage(const ThreadMessage& t) -> void; + void GetThreadMessages(std::list* messages); + void PushThreadMessage(const ThreadMessage& t); - auto RunPendingRunnables() -> void; - auto RunPauseCallbacks() -> void; - auto RunResumeCallbacks() -> void; + void RunPendingRunnables(); + void RunPauseCallbacks(); + void RunResumeCallbacks(); bool bootstrapped_{}; std::list> runnables_; std::list pause_callbacks_; std::list resume_callbacks_; - // std::thread* thread_{}; std::condition_variable thread_message_cv_; std::mutex thread_message_mutex_; std::list thread_messages_; @@ -183,4 +179,4 @@ class Thread { } // namespace ballistica -#endif // BALLISTICA_CORE_THREAD_H_ +#endif // BALLISTICA_SHARED_FOUNDATION_EVENT_LOOP_H_ diff --git a/src/ballistica/core/exception.cc b/src/ballistica/shared/foundation/exception.cc similarity index 76% rename from src/ballistica/core/exception.cc rename to src/ballistica/shared/foundation/exception.cc index 630e824b..ee60233a 100644 --- a/src/ballistica/core/exception.cc +++ b/src/ballistica/shared/foundation/exception.cc @@ -1,9 +1,9 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/core/exception.h" +#include "ballistica/shared/foundation/exception.h" -#include "ballistica/ballistica.h" -#include "ballistica/platform/platform.h" +#include "ballistica/core/core.h" +#include "ballistica/core/platform/core_platform.h" namespace ballistica { @@ -16,19 +16,21 @@ auto GetShortExceptionDescription(const std::exception& exc) -> const char* { Exception::Exception(std::string message_in, PyExcType python_type) : message_(std::move(message_in)), python_type_(python_type) { - thread_name_ = GetCurrentThreadName(); + thread_name_ = CurrentThreadName(); - // Attempt to capture a stack-trace here we can print out later if desired. - if (g_platform != nullptr) { - stack_trace_ = g_platform->GetStackTrace(); + // If core has been inited, attempt to capture a stack-trace here we + // can print out later if desired. + if (core::g_core) { + stack_trace_ = core::g_core->platform->GetStackTrace(); } } Exception::Exception(PyExcType python_type) : python_type_(python_type) { - thread_name_ = GetCurrentThreadName(); + thread_name_ = CurrentThreadName(); - // Attempt to capture a stack-trace here we can print out later if desired. - if (g_platform != nullptr) { - stack_trace_ = g_platform->GetStackTrace(); + // If core has been inited, attempt to capture a stack-trace here we + // can print out later if desired. + if (core::g_core) { + stack_trace_ = core::g_core->platform->GetStackTrace(); } } diff --git a/src/ballistica/core/exception.h b/src/ballistica/shared/foundation/exception.h similarity index 89% rename from src/ballistica/core/exception.h rename to src/ballistica/shared/foundation/exception.h index 04aa8b4d..6e384151 100644 --- a/src/ballistica/core/exception.h +++ b/src/ballistica/shared/foundation/exception.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_CORE_EXCEPTION_H_ -#define BALLISTICA_CORE_EXCEPTION_H_ +#ifndef BALLISTICA_SHARED_FOUNDATION_EXCEPTION_H_ +#define BALLISTICA_SHARED_FOUNDATION_EXCEPTION_H_ #ifdef __cplusplus #include #include -#include "ballistica/core/types.h" +#include "ballistica/shared/foundation/types.h" namespace ballistica { @@ -36,14 +36,16 @@ namespace ballistica { // std::runtime_error at a later time and also catches exceptions coming from // std itself. +namespace core { class PlatformStackTrace; +} /// Get a short description for an exception. /// By default, our Exception classes provide what() values that may include /// backtraces of the throw location or other extended info that can be useful /// to have printed in crash reports/etc. In some cases this extended info is /// not desired, however, such as when converting a C++ exception to a Python -/// one (which will have its own backtrace and other context). This function +/// one (which will have its own backtrace and other context_ref). This function /// will return the raw message only if passed one of our Exceptions, and /// simply what() in other cases. auto GetShortExceptionDescription(const std::exception& exc) -> const char*; @@ -75,10 +77,10 @@ class Exception : public std::exception { std::string message_; std::string full_description_; PyExcType python_type_; - PlatformStackTrace* stack_trace_{}; + core::PlatformStackTrace* stack_trace_{}; }; } // namespace ballistica #endif // __cplusplus -#endif // BALLISTICA_CORE_EXCEPTION_H_ +#endif // BALLISTICA_SHARED_FOUNDATION_EXCEPTION_H_ diff --git a/src/ballistica/core/fatal_error.cc b/src/ballistica/shared/foundation/fatal_error.cc similarity index 59% rename from src/ballistica/core/fatal_error.cc rename to src/ballistica/shared/foundation/fatal_error.cc index 9df548db..748eb1ba 100644 --- a/src/ballistica/core/fatal_error.cc +++ b/src/ballistica/shared/foundation/fatal_error.cc @@ -1,16 +1,21 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/core/fatal_error.h" +#include "ballistica/shared/foundation/fatal_error.h" -#include "ballistica/app/app_flavor.h" -#include "ballistica/core/logging.h" -#include "ballistica/core/thread.h" -#include "ballistica/internal/app_internal.h" -#include "ballistica/platform/platform.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/core/support/base_soft.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/foundation/logging.h" namespace ballistica { -auto FatalError::ReportFatalError(const std::string& message, - bool in_top_level_exception_handler) -> void { + +// Note: implicitly using core's internal globals here, so our behavior is +// undefined if core has not been imported by *someone*. +using core::g_base_soft; +using core::g_core; + +void FatalError::ReportFatalError(const std::string& message, + bool in_top_level_exception_handler) { // We want to report the first fatal error that happens; if further ones // happen they are probably red herrings. static bool ran = false; @@ -23,32 +28,38 @@ auto FatalError::ReportFatalError(const std::string& message, // blessed build. If we are, our main goal is to communicate as much info // about the error to the master server, and communicating to the user is // a stretch goal. + // If we are unblessed or modified, the main goals are communicating the // error to the user and exiting the app cleanly (so we don't pollute our // crash records with results of user tinkering). + // Special case: if we've got a debugger attached we simply abort() + // immediately in order to get the debugger's attention. + if (g_core && g_core->core_config().debugger_attached) { + if (!message.empty()) { + printf("FATAL ERROR: %s\n", message.c_str()); + fflush(stdout); + } + abort(); + } + // Give the platform the opportunity to augment or override our handling. - if (g_platform) { - auto handled = - g_platform->ReportFatalError(message, in_top_level_exception_handler); + if (g_core) { + auto handled = g_core->platform->ReportFatalError( + message, in_top_level_exception_handler); if (handled) { return; } } - std::string dialog_msg = message; - if (!dialog_msg.empty()) { - // Why was this here? - // dialog_msg += "\n"; - } - auto starttime = time(nullptr); // Launch a thread and give it a chance to directly send our logs to the // master-server. The standard mechanism probably won't get the job done - // since it relies on the logic thread loop and we're likely blocking that. - // But generally we want to stay in this function and call abort() or whatnot - // from here so that our stack trace makes it into platform logs. + // since it relies on the logic thread loop and we're likely blocking + // that. But generally we want to stay in this function and call abort() + // or whatnot from here so that our stack trace makes it into platform + // logs. int result{}; std::string logmsg = @@ -58,12 +69,13 @@ auto FatalError::ReportFatalError(const std::string& message, // top-level exception handler. Otherwise the trace isn't really useful // since we know where those are anyway. if (!in_top_level_exception_handler) { - if (g_platform) { - PlatformStackTrace* trace{g_platform->GetStackTrace()}; + if (g_core && g_core->platform) { + core::PlatformStackTrace* trace{g_core->platform->GetStackTrace()}; if (trace) { std::string tracestr = trace->GetDescription(); if (!tracestr.empty()) { - logmsg += ("\nSTACK-TRACE-BEGIN:\n" + tracestr + "\nSTACK-TRACE-END"); + logmsg += ("\nCPP-STACK-TRACE-BEGIN:\n" + tracestr + + "\nCPP-STACK-TRACE-END"); } delete trace; } @@ -75,8 +87,9 @@ auto FatalError::ReportFatalError(const std::string& message, // logs twice). g_early_v1_cloud_log_writes = 0; - // Add this to our V1CloudLog which we'll be attempting to send momentarily, - // and also go to platform-specific logging and good ol' stderr. + // Add this to our V1CloudLog which we'll be attempting to send + // momentarily, and also go to platform-specific logging and good ol' + // stderr. Logging::V1CloudLog(logmsg); Logging::DisplayLog("root", LogLevel::kCritical, logmsg); fprintf(stderr, "%s\n", logmsg.c_str()); @@ -84,16 +97,20 @@ auto FatalError::ReportFatalError(const std::string& message, std::string prefix = "FATAL-ERROR-LOG:"; std::string suffix; - // If we have no globals yet, include this message explicitly + // If we have no core state yet, include this message explicitly // since it won't be part of the standard log. - if (g_app == nullptr) { + if (g_core == nullptr) { suffix = logmsg; } - g_app_internal->DirectSendV1CloudLogs(prefix, suffix, true, &result); + + if (g_base_soft) { + g_base_soft->PlusDirectSendV1CloudLogs(prefix, suffix, true, &result); + } // If we're able to show a fatal-error dialog synchronously, do so. - if (g_platform && g_platform->CanShowBlockingFatalErrorDialog()) { - DoBlockingFatalErrorDialog(dialog_msg); + if (g_core && g_core->platform + && g_core->platform->CanShowBlockingFatalErrorDialog()) { + DoBlockingFatalErrorDialog(message); } // Wait until the log submit has finished or a bit of time has passed.. @@ -101,24 +118,26 @@ auto FatalError::ReportFatalError(const std::string& message, if (result != 0) { break; } - Platform::SleepMS(100); + core::CorePlatform::SleepMillisecs(100); } } -auto FatalError::DoBlockingFatalErrorDialog(const std::string& message) - -> void { +void FatalError::DoBlockingFatalErrorDialog(const std::string& message) { + // We should not get here without this intact. + assert(g_core); // If we're in the main thread; just fire off the dialog directly. - // Otherwise tell the main thread to do it and wait around until it's done. - if (InMainThread()) { - g_platform->BlockingFatalErrorDialog(message); + // Otherwise tell the main thread to do it and wait around until it's + // done. + if (g_core->InMainThread()) { + g_core->platform->BlockingFatalErrorDialog(message); } else { bool started{}; bool finished{}; bool* startedptr{&started}; bool* finishedptr{&finished}; - g_app_flavor->thread()->PushCall([message, startedptr, finishedptr] { + g_core->main_event_loop()->PushCall([message, startedptr, finishedptr] { *startedptr = true; - g_platform->BlockingFatalErrorDialog(message); + g_core->platform->BlockingFatalErrorDialog(message); *finishedptr = true; }); @@ -126,15 +145,15 @@ auto FatalError::DoBlockingFatalErrorDialog(const std::string& message) // There's a chance that it can't (if threads are paused, if it is // blocked on a synchronous call to another thread, etc.) so if we don't // see something happening soon, just give up on showing a dialog. - auto starttime = Platform::GetCurrentMilliseconds(); + auto starttime = core::CorePlatform::GetCurrentMillisecs(); while (!started) { - if (Platform::GetCurrentMilliseconds() - starttime > 1000) { + if (core::CorePlatform::GetCurrentMillisecs() - starttime > 1000) { return; } - Platform::SleepMS(10); + core::CorePlatform::SleepMillisecs(10); } while (!finished) { - Platform::SleepMS(10); + core::CorePlatform::SleepMillisecs(10); } } } @@ -142,9 +161,9 @@ auto FatalError::DoBlockingFatalErrorDialog(const std::string& message) auto FatalError::HandleFatalError(bool exit_cleanly, bool in_top_level_exception_handler) -> bool { // Give the platform the opportunity to completely override our handling. - if (g_platform) { - auto handled = g_platform->HandleFatalError(exit_cleanly, - in_top_level_exception_handler); + if (g_core) { + auto handled = g_core->platform->HandleFatalError( + exit_cleanly, in_top_level_exception_handler); if (handled) { return true; } @@ -162,8 +181,8 @@ auto FatalError::HandleFatalError(bool exit_cleanly, } } - // Otherwise its up to who called us - // (they might let the caught exception bubble up) + // Otherwise its up to who called us (they might let the caught exception + // bubble up) return false; } diff --git a/src/ballistica/core/fatal_error.h b/src/ballistica/shared/foundation/fatal_error.h similarity index 77% rename from src/ballistica/core/fatal_error.h rename to src/ballistica/shared/foundation/fatal_error.h index 3388287c..3572619f 100644 --- a/src/ballistica/core/fatal_error.h +++ b/src/ballistica/shared/foundation/fatal_error.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_CORE_FATAL_ERROR_H_ -#define BALLISTICA_CORE_FATAL_ERROR_H_ +#ifndef BALLISTICA_SHARED_FOUNDATION_FATAL_ERROR_H_ +#define BALLISTICA_SHARED_FOUNDATION_FATAL_ERROR_H_ #include @@ -12,8 +12,8 @@ class FatalError { /// Report a fatal error to the master-server/user/etc. Note that reporting /// only happens for the first invocation of this call; additional calls /// are no-ops. - static auto ReportFatalError(const std::string& message, - bool in_top_level_exception_handler) -> void; + static void ReportFatalError(const std::string& message, + bool in_top_level_exception_handler); /// Handle a fatal error. This can involve calling exit(), abort(), setting /// up an asynchronous quit, etc. Returns true if the fatal-error has been @@ -26,8 +26,8 @@ class FatalError { bool in_top_level_exception_handler) -> bool; private: - static auto DoBlockingFatalErrorDialog(const std::string& message) -> void; + static void DoBlockingFatalErrorDialog(const std::string& message); }; } // namespace ballistica -#endif // BALLISTICA_CORE_FATAL_ERROR_H_ +#endif // BALLISTICA_SHARED_FOUNDATION_FATAL_ERROR_H_ diff --git a/src/ballistica/shared/foundation/feature_set_front_end.cc b/src/ballistica/shared/foundation/feature_set_front_end.cc new file mode 100644 index 00000000..d2ad6a9c --- /dev/null +++ b/src/ballistica/shared/foundation/feature_set_front_end.cc @@ -0,0 +1,78 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/shared/foundation/feature_set_front_end.h" + +#include "ballistica/core/support/base_soft.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" + +namespace ballistica { + +const char* kFeatureSetDataAttrName = "_ba_feature_set_data"; + +FeatureSetFrontEnd::~FeatureSetFrontEnd() = default; + +auto FeatureSetFrontEnd::BaseImportThroughPythonModule(const char* modulename) + -> FeatureSetFrontEnd* { + // Our feature-set has an associated Python module, so we want all + // importing to go through Python. This keeps things consistent no + // matter whether we are used from C++ or Python. We simply import + // our Python module and then return the FeatureSet pointer that it + // has stored with itself. + + // Make sure we're holding the GIL so can be run from any thread. + auto gil{Python::ScopedInterpreterLock()}; + + PyObject* module = PyImport_ImportModule(modulename); + if (!module) { + // we pass zero here to avoid grabbing references to this exception + // which can cause objects to stick around and trip up our deletion checks + // (nodes, actors existing after their games have ended) + PyErr_PrintEx(0); + // Currently not going to attempt to recover if we can't get at our own + // stuff. + FatalError("Unable to import Python module '" + std::string(modulename) + + "'."); + } + + // Grab the wrapper to our C++ pointer from the module. + auto fs_data_obj = PythonRef::StolenSoft( + PyObject_GetAttrString(module, kFeatureSetDataAttrName)); + if (!fs_data_obj.Exists()) { + FatalError("Did not find expected feature-set data in module " + + std::string(modulename)); + } + + // We need our feature-set-data class from _babase for this. + assert(core::g_core); + auto* basefs = core::g_core->SoftImportBase(); + if (!basefs) { + FatalError( + "_babase is unavailable; can't import ballistica c++ interfaces"); + } + + // Make sure its pointing to an instance and return it. + auto* feature_set = basefs->FeatureSetFromData(*fs_data_obj); + BA_PRECONDITION_FATAL(feature_set); + + return feature_set; +} + +void FeatureSetFrontEnd::StoreOnPythonModule(PyObject* module) { + // We need our feature-set-data class from _babase for this. + assert(core::g_core); + auto* basefs = core::g_core->SoftImportBase(); + if (!basefs) { + FatalError( + "_babase is unavailable; can't import ballistica c++ interfaces"); + } + + // Stuff a pointer to ourself into a Python object and add that to our + // module. This is how our fellow C++ stuff will get at us. + PyObject* fsdata = basefs->CreateFeatureSetData(this); + BA_PRECONDITION_FATAL(fsdata); + BA_PRECONDITION_FATAL( + PyObject_SetAttrString(module, kFeatureSetDataAttrName, fsdata) == 0); +} + +} // namespace ballistica diff --git a/src/ballistica/shared/foundation/feature_set_front_end.h b/src/ballistica/shared/foundation/feature_set_front_end.h new file mode 100644 index 00000000..baad03b8 --- /dev/null +++ b/src/ballistica/shared/foundation/feature_set_front_end.h @@ -0,0 +1,46 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SHARED_FOUNDATION_FEATURE_SET_FRONT_END_H_ +#define BALLISTICA_SHARED_FOUNDATION_FEATURE_SET_FRONT_END_H_ + +#include "ballistica/core/core.h" +#include "ballistica/shared/ballistica.h" + +namespace ballistica { + +extern const char* kFeatureSetDataAttrName; + +/// Base-class for portions of feature-sets exposed directly to C++. +/// Using this, one can 'import' feature-sets directly in C++ without +/// worrying about wrangling the Python layer (or whether the feature-set +/// even has a Python component to it). +class FeatureSetFrontEnd { + public: + virtual ~FeatureSetFrontEnd(); + + /// FeatureSets with C++ front-ends AND Python binary module components + /// should use this during module exec to store themselves with the module. + /// Then their Import() method should use ImportThroughPythonModule below + /// to fetch the front-end. This keeps the C++ and Python layers nicely + /// synced. + void StoreOnPythonModule(PyObject* module); + + protected: + /// Should be used by FeatureSets in their Import() methods to pull their + /// data from their associated Python module. + template + static auto ImportThroughPythonModule(const char* modulename) -> T* { + auto* fs_typed = + dynamic_cast(BaseImportThroughPythonModule(modulename)); + BA_PRECONDITION_FATAL(fs_typed); + return fs_typed; + } + + private: + static auto BaseImportThroughPythonModule(const char* modulename) + -> FeatureSetFrontEnd*; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SHARED_FOUNDATION_FEATURE_SET_FRONT_END_H_ diff --git a/src/ballistica/core/inline.cc b/src/ballistica/shared/foundation/inline.cc similarity index 82% rename from src/ballistica/core/inline.cc rename to src/ballistica/shared/foundation/inline.cc index 2eef8180..edbe36d7 100644 --- a/src/ballistica/core/inline.cc +++ b/src/ballistica/shared/foundation/inline.cc @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. #if 0 // Satisfy both CppLint and CLang.. -#include "ballistica/core/inline.h" +#include "ballistica/shared/foundation/inline.h" #endif namespace ballistica { diff --git a/src/ballistica/core/inline.h b/src/ballistica/shared/foundation/inline.h similarity index 93% rename from src/ballistica/core/inline.h rename to src/ballistica/shared/foundation/inline.h index 7ef26632..a49a7a10 100644 --- a/src/ballistica/core/inline.h +++ b/src/ballistica/shared/foundation/inline.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_CORE_INLINE_H_ -#define BALLISTICA_CORE_INLINE_H_ +#ifndef BALLISTICA_SHARED_FOUNDATION_INLINE_H_ +#define BALLISTICA_SHARED_FOUNDATION_INLINE_H_ #ifdef __cplusplus @@ -25,6 +25,13 @@ inline auto explicit_bool(bool val) -> bool { } } +/// assert() that the provided pointer is not nullptr. +template +auto AssertNotNull(T* ptr) -> T* { + assert(ptr != nullptr); + return ptr; +} + template auto check_static_cast_fit(IN_TYPE in) -> bool { // Make sure we don't try to use this when casting to or from floats or @@ -47,9 +54,7 @@ auto check_static_cast_fit(IN_TYPE in) -> bool { /// stuffing a 32 bit value into a 16 bit container, etc. template auto static_cast_check_fit(IN_TYPE in) -> OUT_TYPE { - if (g_buildconfig.debug_build()) { - assert(check_static_cast_fit(in)); - } + assert(check_static_cast_fit(in)); return static_cast(in); } @@ -145,4 +150,4 @@ static auto static_type_name(bool debug_full = false) -> std::string { #endif // __cplusplus -#endif // BALLISTICA_CORE_INLINE_H_ +#endif // BALLISTICA_SHARED_FOUNDATION_INLINE_H_ diff --git a/src/ballistica/shared/foundation/logging.cc b/src/ballistica/shared/foundation/logging.cc new file mode 100644 index 00000000..1cf4ef6f --- /dev/null +++ b/src/ballistica/shared/foundation/logging.cc @@ -0,0 +1,65 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/shared/foundation/logging.h" + +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/core/python/core_python.h" +#include "ballistica/core/support/base_soft.h" + +namespace ballistica { + +// Note: we implicitly use core functionality. Our behavior is undefined +// if nobody has imported core yet. +using core::g_base_soft; +using core::g_core; + +int g_early_v1_cloud_log_writes{10}; + +void Logging::Log(LogLevel level, const std::string& msg) { + BA_PRECONDITION(g_core); + g_core->python->LoggingCall(level, msg); +} + +void Logging::DisplayLog(const std::string& name, LogLevel level, + const std::string& msg) { + // Print to the in-app console (with a newline added). + if (g_base_soft) { + g_base_soft->PushConsolePrintCall(msg + "\n"); + } + + // Ship to platform-specific display mechanisms (android log, etc). + if (g_core) { + g_core->platform->DisplayLog(name, level, msg); + } +} + +void Logging::V1CloudLog(const std::string& msg) { + // Route through platform-specific loggers if present. + + if (g_core) { + // (ship to things like Crashlytics crash-logging) + g_core->platform->DebugLog(msg); + + // Add to our complete v1-cloud-log. + std::scoped_lock lock(g_core->v1_cloud_log_mutex); + if (!g_core->v1_cloud_log_full) { + (g_core->v1_cloud_log) += (msg + "\n"); + if ((g_core->v1_cloud_log).size() > 25000) { + // Allow some reasonable overflow for last statement. + if ((g_core->v1_cloud_log).size() > 250000) { + // FIXME: This could potentially chop up utf-8 chars. + (g_core->v1_cloud_log).resize(250000); + } + g_core->v1_cloud_log += "\n\n"; + g_core->v1_cloud_log_full = true; + } + } + } + + // If the base feature-set is up, ship it off there for further handling. + if (g_base_soft) { + g_base_soft->V1CloudLog(msg); + } +} + +} // namespace ballistica diff --git a/src/ballistica/shared/foundation/logging.h b/src/ballistica/shared/foundation/logging.h new file mode 100644 index 00000000..792665d2 --- /dev/null +++ b/src/ballistica/shared/foundation/logging.h @@ -0,0 +1,46 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SHARED_FOUNDATION_LOGGING_H_ +#define BALLISTICA_SHARED_FOUNDATION_LOGGING_H_ + +#include + +#include "ballistica/shared/ballistica.h" + +namespace ballistica { + +// Slightly hacky, but don't want to store this with any of our normal +// global classes because it might be needed before they are allocated. +extern int g_early_v1_cloud_log_writes; + +class Logging { + public: + /// Write a message to the log. Intended for logging use in C++ code. This + /// is safe to call by any thread at any time as long as core has been + /// inited. In general it simply passes through to the equivalent Python + /// logging call: logging.info, logging.warning, etc. + /// + /// Be aware that Log() calls made before babase is imported will be + /// stored and submitted all at once to Python once babase is imported + /// (with a [HELD] prefix). Ballistica's log/print redirection gets + /// finalized at that point and this system ensures all C++ Log() calls + /// ever made will be routed through the app, visible in in-app consoles, + /// etc. Note that direct Python logging calls or prints occurring before + /// babase is imported may not be visible in the app for that same reason. + static void Log(LogLevel level, const std::string& msg); + + /// Immediately display a log message in the in-app console, + /// platform-specific logs, etc. This generally should not be called + /// directly but instead wired up to display messages coming from the + /// Python logging system. + static void DisplayLog(const std::string& name, LogLevel level, + const std::string& msg); + + /// Write a message to the v1 cloud log. This is considered legacy and + /// will be phased out eventually. + static void V1CloudLog(const std::string& msg); +}; + +} // namespace ballistica + +#endif // BALLISTICA_SHARED_FOUNDATION_LOGGING_H_ diff --git a/src/ballistica/shared/foundation/macros.cc b/src/ballistica/shared/foundation/macros.cc new file mode 100644 index 00000000..97734ce9 --- /dev/null +++ b/src/ballistica/shared/foundation/macros.cc @@ -0,0 +1,140 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/shared/foundation/macros.h" + +#include + +#include "ballistica/core/core.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/shared/python/python.h" + +// Snippets of compiled functionality used by our evil macros. + +namespace ballistica { + +void MacroFunctionTimerEnd(core::CoreFeatureSet* corefs, millisecs_t starttime, + millisecs_t time, const char* funcname) { + // Currently disabling this for test builds; not really useful for + // the general public. + if (g_buildconfig.test_build()) { + return; + } + assert(corefs); + millisecs_t endtime = corefs->platform->GetTicks(); + if (endtime - starttime > time) { + Log(LogLevel::kWarning, std::to_string(endtime - starttime) + + " milliseconds spent in " + funcname); + } +} + +void MacroFunctionTimerEndThread(core::CoreFeatureSet* corefs, + millisecs_t starttime, millisecs_t time, + const char* funcname) { + // Currently disabling this for test builds; not really useful for + // the general public. + if (g_buildconfig.test_build()) { + return; + } + // EW: Using core's internal globals directly; shouldn't do this. + assert(corefs); + millisecs_t endtime = corefs->platform->GetTicks(); + if (endtime - starttime > time) { + Log(LogLevel::kWarning, + std::to_string(endtime - starttime) + " milliseconds spent by " + + ballistica::CurrentThreadName() + " thread in " + funcname); + } +} + +void MacroFunctionTimerEndEx(core::CoreFeatureSet* corefs, + millisecs_t starttime, millisecs_t time, + const char* funcname, const std::string& what) { + // Currently disabling this for test builds; not really useful for + // the general public. + if (g_buildconfig.test_build()) { + return; + } + // EW: Using core's internal globals directly; shouldn't do this. + assert(corefs); + millisecs_t endtime = corefs->platform->GetTicks(); + if (endtime - starttime > time) { + Log(LogLevel::kWarning, std::to_string(endtime - starttime) + + " milliseconds spent in " + funcname + " for " + + what); + } +} + +void MacroFunctionTimerEndThreadEx(core::CoreFeatureSet* corefs, + millisecs_t starttime, millisecs_t time, + const char* funcname, + const std::string& what) { + // Currently disabling this for test builds; not really useful for + // the general public. + if (g_buildconfig.test_build()) { + return; + } + // EW: Using core's internal globals directly; shouldn't do this. + assert(corefs); + millisecs_t endtime = corefs->platform->GetTicks(); + if (endtime - starttime > time) { + Log(LogLevel::kWarning, std::to_string(endtime - starttime) + + " milliseconds spent by " + + ballistica::CurrentThreadName() + + " thread in " + funcname + " for " + what); + } +} + +void MacroTimeCheckEnd(core::CoreFeatureSet* corefs, millisecs_t starttime, + millisecs_t time, const char* name, const char* file, + int line) { + // Currently disabling this for test builds; not really useful for + // the general public. + if (g_buildconfig.test_build()) { + return; + } + // EW: Using core's internal globals directly; shouldn't do this. + assert(corefs); + millisecs_t e = corefs->platform->GetTicks(); + if (e - starttime > time) { + Log(LogLevel::kWarning, + std::string(name) + " took " + std::to_string(e - starttime) + + " milliseconds; " + MacroPathFilter(corefs, file) + " line " + + std::to_string(line)); + } +} + +void MacroLogErrorTrace(core::CoreFeatureSet* corefs, const std::string& msg, + const char* fname, int line) { + char buffer[2048]; + snprintf(buffer, sizeof(buffer), "%s:%d:", MacroPathFilter(corefs, fname), + line); + buffer[sizeof(buffer) - 1] = 0; + Python::PrintStackTrace(); + Log(LogLevel::kError, std::string(buffer) + " error: " + msg); +} + +void MacroLogError(core::CoreFeatureSet* corefs, const std::string& msg, + const char* fname, int line) { + char e_buffer[2048]; + snprintf(e_buffer, sizeof(e_buffer), "%s:%d:", MacroPathFilter(corefs, fname), + line); + e_buffer[sizeof(e_buffer) - 1] = 0; + Log(LogLevel::kError, std::string(e_buffer) + " error: " + msg); +} + +void MacroLogPythonTrace(core::CoreFeatureSet* corefs, const std::string& msg) { + Python::PrintStackTrace(); + Log(LogLevel::kError, msg); +} + +auto MacroPathFilter(core::CoreFeatureSet* corefs, const char* filename) + -> const char* { + // If we've got a build_src_dir set and filename starts with it, skip past it. + assert(corefs); + if (corefs && !corefs->build_src_dir().empty() + && strstr(filename, core::g_core->build_src_dir().c_str()) == filename) { + return filename + core::g_core->build_src_dir().size(); + } + return filename; +} + +} // namespace ballistica diff --git a/src/ballistica/shared/foundation/macros.h b/src/ballistica/shared/foundation/macros.h new file mode 100644 index 00000000..2e8fcf09 --- /dev/null +++ b/src/ballistica/shared/foundation/macros.h @@ -0,0 +1,182 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SHARED_FOUNDATION_MACROS_H_ +#define BALLISTICA_SHARED_FOUNDATION_MACROS_H_ + +#ifdef __cplusplus +#include +#include +#endif + +#include "ballistica/shared/foundation/types.h" + +// Various utility macros and related support calls. +// Trying to contain the evil to this one place. + +// Trailing-semicolon note: +// Some macros contain a ((void*) at the end. This is so the macro can be +// followed by a semicolon without triggering an 'empty statement' warning. +// I find standalone function-style macro invocations without semicolons +// tend to confuse code formatters. + +#define BA_STRINGIFY(x) #x +// Looks redundant at first but strangely necessary. +// See https://www.decompile.com/cpp/faq/file_and_line_error_string.htm +#define BA_TOSTRING(x) BA_STRINGIFY(x) + +#define BA_BUILD_COMMAND_FILENAME \ + "" + +#if BA_OSTYPE_WINDOWS +#define BA_DIRSLASH "\\" +#else +#define BA_DIRSLASH "/" +#endif + +#if BA_DEBUG_BUILD +#define BA_IFDEBUG(a) a +#else +#define BA_IFDEBUG(a) ((void)0) +#endif + +// Useful for finding hitches. +// Call begin, followed at some point by any of the end versions. +// FIXME: Turn these into C++ classes. +#if BA_DEBUG_BUILD +#define BA_DEBUG_FUNCTION_TIMER_BEGIN() \ + millisecs_t _dfts = g_core->platform->GetTicks() +#define BA_DEBUG_FUNCTION_TIMER_END(time) \ + ::ballistica::MacroFunctionTimerEnd(g_core, _dfts, time, __PRETTY_FUNCTION__) +#define BA_DEBUG_FUNCTION_TIMER_END_THREAD(time) \ + ::ballistica::MacroFunctionTimerEndThread(g_core, _dfts, time, \ + __PRETTY_FUNCTION__) +#define BA_DEBUG_FUNCTION_TIMER_END_EX(time, what) \ + MacroFunctionTimerEndEx(g_core, _dfts, time, __PRETTY_FUNCTION__, what) +#define BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(time, what) \ + ::ballistica::MacroFunctionTimerEndThreadEx(g_core, _dfts, time, \ + __PRETTY_FUNCTION__, what) +#define BA_DEBUG_TIME_CHECK_BEGIN(name) \ + millisecs_t name##_ts = g_core->platform->GetTicks() +#define BA_DEBUG_TIME_CHECK_END(name, time) \ + ::ballistica::MacroTimeCheckEnd(g_core, name##_ts, time, #name, __FILE__, \ + __LINE__) +#else +#define BA_DEBUG_FUNCTION_TIMER_BEGIN() ((void)0) +#define BA_DEBUG_FUNCTION_TIMER_END(time) ((void)0) +#define BA_DEBUG_FUNCTION_TIMER_END_THREAD(time) ((void)0) +#define BA_DEBUG_FUNCTION_TIMER_END_EX(time, what) ((void)0) +#define BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(time, what) ((void)0) +#define BA_DEBUG_TIME_CHECK_BEGIN(name) ((void)0) +#define BA_DEBUG_TIME_CHECK_END(name, time) ((void)0) +#endif + +// Disallow copying for a class. +#define BA_DISALLOW_CLASS_COPIES(type) \ + type(const type& foo) = delete; \ + type& operator=(const type& src) = delete; /* NOLINT (macro parens) */ + +// Call this for errors which are non-fatal but should be noted so they can be +// fixed. +#define BA_LOG_ERROR_TRACE(msg) \ + ::ballistica::MacroLogErrorTrace(g_core, msg, __FILE__, __LINE__) + +#define BA_LOG_ERROR_TRACE_ONCE(msg) \ + { \ + static bool did_log_error_trace_here = false; \ + if (!did_log_error_trace_here) { \ + ::ballistica::MacroLogErrorTrace(g_core, msg, __FILE__, __LINE__); \ + did_log_error_trace_here = true; \ + } \ + } \ + ((void)0) // (see 'Trailing-semicolon note' at top) + +#define BA_LOG_ONCE(lvl, msg) \ + { \ + static bool did_log_here = false; \ + if (!did_log_here) { \ + ::ballistica::Log(lvl, msg); \ + did_log_here = true; \ + } \ + } \ + ((void)0) // (see 'Trailing-semicolon note' at top) + +#define BA_LOG_PYTHON_TRACE(msg) ::ballistica::MacroLogPythonTrace(g_core, msg) + +#define BA_LOG_PYTHON_TRACE_ONCE(msg) \ + { \ + static bool did_log_python_trace_here = false; \ + if (!did_log_python_trace_here) { \ + ::ballistica::MacroLogPythonTrace(g_core, msg); \ + did_log_python_trace_here = true; \ + } \ + } \ + ((void)0) // (see 'Trailing-semicolon note' at top) + +/// Test a condition and throw an exception if it fails (on both debug and +/// release builds) +#define BA_PRECONDITION(b) \ + { \ + if (!(b)) { \ + throw ::ballistica::Exception("Precondition failed: " #b); \ + } \ + } \ + ((void)0) // (see 'Trailing-semicolon note' at top) + +/// Test a condition and simply print a log message if it fails (on both debug +/// and release builds) +#define BA_PRECONDITION_LOG(b) \ + { \ + if (!(b)) { \ + Log(LogLevel::kError, "Precondition failed: " #b); \ + } \ + } \ + ((void)0) // (see 'Trailing-semicolon note' at top) + +/// Test a condition and abort the program if it fails (on both debug +/// and release builds) +#define BA_PRECONDITION_FATAL(b) \ + { \ + if (!(b)) { \ + FatalError("Precondition failed: " #b); \ + } \ + } \ + ((void)0) // (see 'Trailing-semicolon note' at top) + +#ifdef __cplusplus + +namespace ballistica::core { +class CoreFeatureSet; +} + +namespace ballistica { + +// Support functions used by some of our macros; not intended to be used +// directly. +auto MacroPathFilter(core::CoreFeatureSet* corefs, const char* filename) + -> const char*; +void MacroFunctionTimerEnd(core::CoreFeatureSet* corefs, millisecs_t starttime, + millisecs_t time, const char* funcname); +void MacroFunctionTimerEndThread(core::CoreFeatureSet* corefs, + millisecs_t starttime, millisecs_t time, + const char* funcname); +void MacroFunctionTimerEndEx(core::CoreFeatureSet* corefs, + millisecs_t starttime, millisecs_t time, + const char* funcname, const std::string& what); +void MacroFunctionTimerEndThreadEx(core::CoreFeatureSet* corefs, + millisecs_t starttime, millisecs_t time, + const char* funcname, + const std::string& what); +void MacroTimeCheckEnd(core::CoreFeatureSet* corefs, millisecs_t starttime, + millisecs_t time, const char* name, const char* file, + int line); +void MacroLogErrorTrace(core::CoreFeatureSet* corefs, const std::string& msg, + const char* fname, int line); +void MacroLogError(core::CoreFeatureSet* corefs, const std::string& msg, + const char* fname, int line); +void MacroLogPythonTrace(core::CoreFeatureSet* corefs, const std::string& msg); + +} // namespace ballistica + +#endif // __cplusplus + +#endif // BALLISTICA_SHARED_FOUNDATION_MACROS_H_ diff --git a/src/ballistica/core/object.cc b/src/ballistica/shared/foundation/object.cc similarity index 50% rename from src/ballistica/core/object.cc rename to src/ballistica/shared/foundation/object.cc index 6efc0098..9b140109 100644 --- a/src/ballistica/core/object.cc +++ b/src/ballistica/shared/foundation/object.cc @@ -1,30 +1,131 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/core/object.h" +#include "ballistica/shared/foundation/object.h" #include #include -#include "ballistica/app/app.h" -#include "ballistica/generic/utils.h" -#include "ballistica/platform/platform.h" +#include "ballistica/core/core.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/core/support/base_soft.h" +#include "ballistica/shared/generic/utils.h" namespace ballistica { +// Note: Functionality here assumes that someone has imported core and will +// fail hard if not (hence us using core's internal globals below). +using core::g_base_soft; +using core::g_core; + +Object::Object() { +#if BA_DEBUG_BUILD + // Mark when we were born. + // NOTE: Using core's internal globals here; don't do this normally. + assert(g_core); + object_birth_time_ = g_core->GetAppTimeMillisecs(); + + // Add ourself to the global object list. + { + std::scoped_lock lock(g_core->object_list_mutex); + object_prev_ = nullptr; + object_next_ = g_core->object_list_first; + g_core->object_list_first = this; + if (object_next_) { + object_next_->object_prev_ = this; + } + g_core->object_count++; + } +#endif // BA_DEBUG_BUILD +} + +Object::~Object() { +#if BA_DEBUG_BUILD + { + // NOTE: using core's internal globals here; don't do this normally! + assert(g_core); + // Pull ourself from the global obj list. + std::scoped_lock lock(g_core->object_list_mutex); + if (object_next_) { + object_next_->object_prev_ = object_prev_; + } + if (object_prev_) { + object_prev_->object_next_ = object_next_; + } else { + g_core->object_list_first = object_next_; + } + g_core->object_count--; + } + + // Objects should never be dying with non-zero reference counts. + if (object_strong_ref_count_ != 0) { + FatalError("Object is dying with non-zero ref-count."); + } + + // Objects set up as ref-counted shouldn't be dying before getting reffed. + if (object_is_ref_counted_ && !object_has_been_strong_reffed_) { + FatalError( + "Object set as ref-counted but dying without ever having a ref."); + } + +#endif // BA_DEBUG_BUILD + + // Invalidate all our weak refs. + // We could call Release() on each but we'd have to deactivate the + // thread-check since virtual functions won't work as expected in a + // destructor. Also we can take a few shortcuts here since we know + // we're deleting the entire list, not just one object. + while (object_weak_refs_) { + auto tmp{object_weak_refs_}; + object_weak_refs_ = tmp->next_; + tmp->prev_ = nullptr; + tmp->next_ = nullptr; + tmp->obj_ = nullptr; + } +} + +auto Object::GetObjectTypeName() const -> std::string { + // Default implementation just returns type name. + // Note: using core's globals directly; don't normally do this! + if (g_core) { + return g_core->platform->DemangleCXXSymbol(typeid(*this).name()); + } + return "(unknown-no-core)"; +} + +auto Object::GetObjectDescription() const -> std::string { + return "<" + GetObjectTypeName() + " object at " + Utils::PtrToString(this) + + ">"; +} + +auto Object::GetDefaultOwnerThread() const -> EventLoopID { + return EventLoopID::kLogic; +} + +auto Object::GetThreadOwnership() const -> Object::ThreadOwnership { +#if BA_DEBUG_BUILD + return thread_ownership_; +#else + FatalError("Should not be called in release builds."); + return ThreadOwnership::kClassDefault; +#endif +} + void Object::LsObjects() { #if BA_DEBUG_BUILD + // Note: using core's internal globals here; don't normally do this. + assert(g_core); std::string s; { - std::scoped_lock lock(g_app->object_list_mutex); - s = std::to_string(g_app->object_count) + " Objects at time " - + std::to_string(GetRealTime()) + ";"; + std::scoped_lock lock(g_core->object_list_mutex); + s = std::to_string(g_core->object_count) + " Objects at time " + + std::to_string(g_core->GetAppTimeMillisecs()) + ";"; if (explicit_bool(true)) { std::unordered_map obj_map; // Tally up counts for all types. int count = 0; - for (Object* o = g_app->object_list_first; o != nullptr; + for (Object* o = g_core->object_list_first; o != nullptr; o = o->object_next_) { count++; std::string obj_name = o->GetObjectTypeName(); @@ -51,7 +152,7 @@ void Object::LsObjects() { for (auto&& i : sorted) { s += "\n " + std::to_string(i.first) + ": " + i.second; } - assert(count == g_app->object_count); + assert(count == g_core->object_count); } } Log(LogLevel::kInfo, s); @@ -60,125 +161,45 @@ void Object::LsObjects() { #endif // BA_DEBUG_BUILD } -Object::Object() { #if BA_DEBUG_BUILD - // Mark when we were born. - object_birth_time_ = GetRealTime(); - // Add ourself to the global object list. - std::scoped_lock lock(g_app->object_list_mutex); - object_prev_ = nullptr; - object_next_ = g_app->object_list_first; - g_app->object_list_first = this; - if (object_next_) { - object_next_->object_prev_ = this; - } - g_app->object_count++; -#endif // BA_DEBUG_BUILD -} - -Object::~Object() { -#if BA_DEBUG_BUILD - // Pull ourself from the global obj list. - std::scoped_lock lock(g_app->object_list_mutex); - if (object_next_) { - object_next_->object_prev_ = object_prev_; - } - if (object_prev_) { - object_prev_->object_next_ = object_next_; +static auto GetCurrentEventLoopID() -> EventLoopID { + if (g_core->InMainThread()) { + return EventLoopID::kMain; + } else if (g_base_soft && g_base_soft->InLogicThread()) { + return EventLoopID::kLogic; + } else if (g_base_soft && g_base_soft->InAudioThread()) { + return EventLoopID::kAudio; + } else if (g_base_soft && g_base_soft->InNetworkWriteThread()) { + return EventLoopID::kNetworkWrite; + } else if (g_base_soft && g_base_soft->InAssetsThread()) { + return EventLoopID::kAssets; + } else if (g_base_soft && g_base_soft->InBGDynamicsThread()) { + return EventLoopID::kBGDynamics; } else { - g_app->object_list_first = object_next_; - } - g_app->object_count--; - - // More sanity checks. - if (object_strong_ref_count_ != 0) { - // Avoiding Log for these low level errors; can lead to deadlock. - printf( - "Warning: Object is dying with non-zero ref-count; this is bad. " - "(this might mean the object raised an exception in its constructor" - " after being strong-referenced first).\n"); - } - -#endif // BA_DEBUG_BUILD - - // Invalidate all our weak refs. - // We could call Release() on each but we'd have to deactivate the - // thread-check since virtual functions won't work as expected in a - // destructor. Also we can take a few shortcuts here since we know - // we're deleting the entire list, not just one object. - while (object_weak_refs_) { - auto tmp{object_weak_refs_}; - object_weak_refs_ = tmp->next_; - tmp->prev_ = nullptr; - tmp->next_ = nullptr; - tmp->obj_ = nullptr; + throw Exception(std::string("unrecognized thread: ") + CurrentThreadName()); } } -auto Object::GetObjectTypeName() const -> std::string { - // Default implementation just returns type name. - return g_platform->DemangleCXXSymbol(typeid(*this).name()); -} - -auto Object::GetObjectDescription() const -> std::string { - return "<" + GetObjectTypeName() + " object at " + Utils::PtrToString(this) - + ">"; -} - -auto Object::GetDefaultOwnerThread() const -> ThreadTag { - return ThreadTag::kLogic; -} - -auto Object::GetThreadOwnership() const -> Object::ThreadOwnership { -#if BA_DEBUG_BUILD - return thread_ownership_; -#else - FatalError("Should not be called in release builds."); - return ThreadOwnership::kClassDefault; -#endif -} - -#if BA_DEBUG_BUILD - -static auto GetCurrentThreadTag() -> ThreadTag { - if (InMainThread()) { - return ThreadTag::kMain; - } else if (InLogicThread()) { - return ThreadTag::kLogic; - } else if (InAudioThread()) { - return ThreadTag::kAudio; - } else if (InNetworkWriteThread()) { - return ThreadTag::kNetworkWrite; - } else if (InAssetsThread()) { - return ThreadTag::kAssets; - } else if (InBGDynamicsThread()) { - return ThreadTag::kBGDynamics; - } else { - throw Exception(std::string("unrecognized thread: ") - + GetCurrentThreadName()); - } -} - -auto Object::ObjectUpdateForAcquire() -> void { +void Object::ObjectUpdateForAcquire() { ThreadOwnership thread_ownership = GetThreadOwnership(); // If we're set to use the next-referencing thread and haven't set one // yet, do so. if (thread_ownership == ThreadOwnership::kNextReferencing - && owner_thread_ == ThreadTag::kInvalid) { - owner_thread_ = GetCurrentThreadTag(); + && owner_thread_ == EventLoopID::kInvalid) { + owner_thread_ = GetCurrentEventLoopID(); } } -auto Object::ObjectThreadCheck() -> void { +void Object::ObjectThreadCheck() { if (!thread_checks_enabled_) { return; } ThreadOwnership thread_ownership = GetThreadOwnership(); - ThreadTag t; + EventLoopID t; if (thread_ownership == ThreadOwnership::kClassDefault) { t = GetDefaultOwnerThread(); } else { @@ -187,35 +208,35 @@ auto Object::ObjectThreadCheck() -> void { #define DO_FAIL(THREADNAME) \ throw Exception("ObjectThreadCheck failed for " + GetObjectDescription() \ + "; expected " THREADNAME " thread; got " \ - + GetCurrentThreadName()) + + CurrentThreadName()) switch (t) { - case ThreadTag::kMain: - if (!InMainThread()) { + case EventLoopID::kMain: + if (!g_core->InMainThread()) { DO_FAIL("Main"); } break; - case ThreadTag::kLogic: - if (!InLogicThread()) { + case EventLoopID::kLogic: + if (!(g_base_soft && g_base_soft->InLogicThread())) { DO_FAIL("Logic"); } break; - case ThreadTag::kAudio: - if (!InAudioThread()) { + case EventLoopID::kAudio: + if (!(g_base_soft && g_base_soft->InAudioThread())) { DO_FAIL("Audio"); } break; - case ThreadTag::kNetworkWrite: - if (!InNetworkWriteThread()) { + case EventLoopID::kNetworkWrite: + if (!(g_base_soft && g_base_soft->InNetworkWriteThread())) { DO_FAIL("NetworkWrite"); } break; - case ThreadTag::kAssets: - if (!InAssetsThread()) { + case EventLoopID::kAssets: + if (!(g_base_soft && g_base_soft->InAssetsThread())) { DO_FAIL("Assets"); } break; - case ThreadTag::kBGDynamics: - if (!InBGDynamicsThread()) { + case EventLoopID::kBGDynamics: + if (!(g_base_soft && g_base_soft->InBGDynamicsThread())) { DO_FAIL("BGDynamics"); } break; diff --git a/src/ballistica/core/object.h b/src/ballistica/shared/foundation/object.h similarity index 68% rename from src/ballistica/core/object.h rename to src/ballistica/shared/foundation/object.h index 5cb03e1b..67b3743b 100644 --- a/src/ballistica/core/object.h +++ b/src/ballistica/shared/foundation/object.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_CORE_OBJECT_H_ -#define BALLISTICA_CORE_OBJECT_H_ +#ifndef BALLISTICA_SHARED_FOUNDATION_OBJECT_H_ +#define BALLISTICA_SHARED_FOUNDATION_OBJECT_H_ #include #include #include -#include "ballistica/ballistica.h" +#include "ballistica/shared/ballistica.h" namespace ballistica { @@ -21,9 +21,6 @@ class Object { Object(); virtual ~Object(); - /// Logs a tally of ba::Object types and counts (debug build only). - static void LsObjects(); - // Object classes can provide descriptive names for themselves; // these are used for debugging and other purposes. // The default is to use the C++ symbol name, demangling it when possible. @@ -45,7 +42,7 @@ class Object { /// it can perform sanity-tests to make sure references are not being /// added at incorrect times or from incorrect threads. /// The default implementation uses the per-object - /// ThreadOwnership/ThreadTag values accessible below. NOTE: this + /// ThreadOwnership/EventLoopID values accessible below. NOTE: this /// check runs only in the debug build so don't add any logical side-effects! virtual void ObjectThreadCheck(); @@ -59,32 +56,57 @@ class Object { /// Return the exact thread to check for with ThreadOwnership::kClassDefault /// (in the default ObjectThreadCheck implementation at least). - /// Default returns ThreadTag::kLogic - virtual auto GetDefaultOwnerThread() const -> ThreadTag; + /// Default returns EventLoopID::kLogic + virtual auto GetDefaultOwnerThread() const -> EventLoopID; /// Set thread ownership for an individual object. void SetThreadOwnership(ThreadOwnership ownership) { #if BA_DEBUG_BUILD thread_ownership_ = ownership; if (thread_ownership_ == ThreadOwnership::kNextReferencing) { - owner_thread_ = ThreadTag::kInvalid; + owner_thread_ = EventLoopID::kInvalid; } #endif } - // Return true if the object is ref-counted and has at least 1 strong ref. - // This is generally a good thing for calls accepting object ptrs to check. - // Note that this can return false positives in release builds so should - // mainly be used as a debug sanity check (erroring if false) - auto is_valid_refcounted_object() const -> bool { + // Return true if the provided obj ptr is not null, is ref-counted, and has at + // least 1 strong ref. This is generally a good thing for calls accepting + // object ptrs to check. It is considered bad practice to perform operations + // with not-yet-reffed objects. Note that in some cases this may return + // false positives, so only use this as a sanity check and only take action + // for a negative result. + static auto IsValidManagedObject(Object* obj) -> bool { + if (!obj) { + return false; + } #if BA_DEBUG_BUILD - if (object_is_dead_) { + if (obj->object_is_dead_) { return false; } #endif - return (object_strong_ref_count_ > 0); + return (obj->object_strong_ref_count_ > 0); } + // Return true if the object seems to be valid and was allocated as unmanaged. + // Code that plans to explicitly delete raw passed pointers can check this + // for peace of mind. Note that for some build types this will return false + // positives, so only use this as a sanity check and only take action for + // negative results. + static auto IsValidUnmanagedObject(Object* obj) -> bool { + if (!obj) { + return false; + } +#if BA_DEBUG_BUILD + if (obj->object_is_dead_) { + return false; + } + if (!obj->object_is_unmanaged_) { + return false; + } +#endif + // We don't store specifics in release builds; assume everything is peachy. + return true; + } auto object_strong_ref_count() const -> int { return object_strong_ref_count_; } @@ -118,6 +140,10 @@ class Object { } } + auto Exists() const -> bool { return (obj_ != nullptr); } + + void Clear() { Release(); } + private: Object* obj_{}; WeakRefBase* prev_{}; @@ -129,12 +155,24 @@ class Object { template class WeakRef : public WeakRefBase { public: - auto exists() const -> bool { return (obj_ != nullptr); } + /// Convenience wrapper for Object::IsValidManagedObject. + auto IsValidManagedObject() const -> bool { + if (auto* obj = Get()) { + return Object::IsValidManagedObject(obj); + } + return false; + } - void Clear() { Release(); } + /// Convenience wrapper for Object::IsValidUnmanagedObject. + auto IsValidUnmanagedObject() const -> bool { + if (auto* obj = Get()) { + return Object::IsValidUnmanagedObject(obj); + } + return false; + } // Return a pointer or nullptr. - auto get() const -> T* { + auto Get() const -> T* { // Yes, reinterpret_cast is evil, but we make sure // we only operate on cases where this is valid // (see Acquire()). @@ -144,7 +182,8 @@ class Object { // These operators throw exceptions if the object is dead. auto operator*() const -> T& { if (!obj_) { - throw Exception("Invalid dereference of " + static_type_name()); + throw Exception("Dereferencing invalid " + static_type_name() + + " ref."); } // Yes, reinterpret_cast is evil, but we make sure @@ -154,12 +193,12 @@ class Object { } auto operator->() const -> T* { if (!obj_) { - throw Exception("Invalid dereference of " + static_type_name()); + throw Exception("Dereferencing invalid " + static_type_name() + + " ref."); } - // Yes, reinterpret_cast is evil, but we make sure - // we only operate on cases where this is valid - // (see Acquire()). + // Yes, reinterpret_cast is evil, but we make sure we only operate + // on cases where this is valid (see Acquire()). return reinterpret_cast(obj_); } @@ -179,60 +218,64 @@ class Object { assert(dynamic_cast(obj_) == ptr); return *this; } + template auto operator==(U* ptr) -> bool { - return (get() == ptr); + return (Get() == ptr); } + template auto operator!=(U* ptr) -> bool { - return (get() != ptr); + return (Get() != ptr); } // Assign/compare with same type ref (apparently the template below doesn't // cover this case?). auto operator=(const WeakRef& ref) -> WeakRef& { - *this = ref.get(); + *this = ref.Get(); return *this; } + auto operator==(const WeakRef& ref) -> bool { - return (get() == ref.get()); + return (Get() == ref.Get()); } + auto operator!=(const WeakRef& ref) -> bool { - return (get() != ref.get()); + return (Get() != ref.Get()); } // Assign/compare with any compatible strong-ref. template auto operator=(const Ref& ref) -> WeakRef& { - *this = ref.get(); + *this = ref.Get(); return *this; } template auto operator==(const Ref& ref) -> bool { - return (get() == ref.get()); + return (Get() == ref.Get()); } template auto operator!=(const Ref& ref) -> bool { - return (get() != ref.get()); + return (Get() != ref.Get()); } // Assign/compare with any compatible weak-ref. template auto operator=(const WeakRef& ref) -> WeakRef& { - *this = ref.get(); + *this = ref.Get(); return *this; } template auto operator==(const WeakRef& ref) -> bool { - return (get() == ref.get()); + return (Get() == ref.Get()); } template auto operator!=(const WeakRef& ref) -> bool { - return (get() != ref.get()); + return (Get() != ref.Get()); } // Various constructors: @@ -244,7 +287,7 @@ class Object { explicit WeakRef(T* obj) { *this = obj; } // Copy constructor (only non-explicit one). - WeakRef(const WeakRef& ref) { *this = ref.get(); } + WeakRef(const WeakRef& ref) { *this = ref.Get(); } // From a compatible pointer. template @@ -274,7 +317,7 @@ class Object { // Seems like it'd be a good idea to prevent creation of weak-refs to // objects in their destructors, but it turns out we're currently // doing this (session points contexts at itself as it dies, etc.) - // Perhaps later can untangle this and change the behavior. + // Perhaps later can untangle that mess and change this behavior. obj->ObjectThreadCheck(); assert(obj_ == nullptr && next_ == nullptr && prev_ == nullptr); #endif @@ -304,24 +347,34 @@ class Object { class Ref { public: ~Ref() { Release(); } - auto get() const -> T* { return obj_; } + auto Get() const -> T* { return obj_; } // These operators throw an Exception if the object is dead. auto operator*() const -> T& { if (!obj_) { - throw Exception("Invalid dereference of " + static_type_name()); + throw Exception("Dereferencing invalid " + static_type_name() + + " ref."); } return *obj_; } auto operator->() const -> T* { if (!obj_) { - throw Exception("Invalid dereference of " + static_type_name()); + throw Exception("Dereferencing invalid " + static_type_name() + + " ref."); } return obj_; } - auto exists() const -> bool { return (obj_ != nullptr); } + auto Exists() const -> bool { return (obj_ != nullptr); } void Clear() { Release(); } + /// Convenience wrapper for Object::IsValidManagedObject. + auto IsValidManagedObject() const -> bool { + if (auto* obj = Get()) { + return Object::IsValidManagedObject(obj); + } + return false; + } + // Assign/compare with any compatible pointer. template auto operator=(U* ptr) -> Ref& { @@ -333,15 +386,15 @@ class Object { } template auto operator==(U* ptr) -> bool { - return (get() == ptr); + return (Get() == ptr); } template auto operator!=(U* ptr) -> bool { - return (get() != ptr); + return (Get() != ptr); } - auto operator==(const Ref& ref) -> bool { return (get() == ref.get()); } - auto operator!=(const Ref& ref) -> bool { return (get() != ref.get()); } + auto operator==(const Ref& ref) -> bool { return (Get() == ref.Get()); } + auto operator!=(const Ref& ref) -> bool { return (Get() != ref.Get()); } // Assign/compare with same type ref (apparently the generic template below // doesn't cover that case?..) @@ -349,42 +402,42 @@ class Object { // Should get to the bottom of that. auto operator=(const Ref& ref) -> Ref& { assert(this != &ref); // Shouldn't be self-assigning. - *this = ref.get(); + *this = ref.Get(); return *this; } // Assign/compare with any compatible strong-ref. template auto operator=(const Ref& ref) -> Ref& { - *this = ref.get(); + *this = ref.Get(); return *this; } template auto operator==(const Ref& ref) -> bool { - return (get() == ref.get()); + return (Get() == ref.Get()); } template auto operator!=(const Ref& ref) -> bool { - return (get() != ref.get()); + return (Get() != ref.Get()); } // Assign/compare from any compatible weak-ref. template auto operator=(const WeakRef& ref) -> Ref& { - *this = ref.get(); + *this = ref.Get(); return *this; } template auto operator==(const WeakRef& ref) -> bool { - return (get() == ref.get()); + return (Get() == ref.Get()); } template auto operator!=(const WeakRef& ref) -> bool { - return (get() != ref.get()); + return (Get() != ref.Get()); } // Various constructors: @@ -396,7 +449,7 @@ class Object { explicit Ref(T* obj) { *this = obj; } // Copy constructor (only non-explicit one). - Ref(const Ref& ref) { *this = ref.get(); } + Ref(const Ref& ref) { *this = ref.Get(); } // From a compatible pointer. template @@ -429,25 +482,18 @@ class Object { // Obvs shouldn't be referencing dead stuff. assert(!obj->object_is_dead_); - // Complain if creating an initial strong-ref to something - // not marked as ref-counted. - // (should make this an error once we know these are out of the system) - if (!obj->object_has_strong_ref_ - && !obj->object_creating_strong_reffed_) { - // Log only to system log for these low-level errors; - // console or server can cause deadlock due to recursive - // ref-list locks. - FatalError( - "Incorrectly creating initial strong-ref; use " - "New() or MakeRefCounted(): " - + obj->GetObjectDescription()); + // Complain if trying ot create a ref to a non-ref-counted obj. + if (!obj->object_is_ref_counted_) { + FatalError("Attempting to create a strong-ref to non-refcounted obj: " + + obj->GetObjectDescription()); } - obj->object_has_strong_ref_ = true; + obj->object_has_been_strong_reffed_ = true; #endif // BA_DEBUG_BUILD obj->object_strong_ref_count_++; obj_ = obj; } + void Release() { if (obj_ != nullptr) { #if BA_DEBUG_BUILD @@ -481,54 +527,70 @@ class Object { [[nodiscard]] static auto New(ARGS&&... args) -> Object::Ref { auto* ptr = new TALLOC(std::forward(args)...); #if BA_DEBUG_BUILD - if (ptr->object_creating_strong_reffed_) { - // Avoiding Log for these low level errors; can lead to deadlock. - FatalError("ballistica::Object already set up as reffed in New: " + /// Make sure things aren't creating strong refs to themselves in their + /// constructors. + if (ptr->object_has_been_strong_reffed_) { + FatalError("ballistica::Object has already been strong reffed in New: " + ptr->GetObjectDescription()); } - if (ptr->object_strong_ref_count_ > 0) { - FatalError("ballistica::Object hs strong-ref in constructor: " - + ptr->GetObjectDescription()); - } - ptr->object_in_constructor_ = false; - ptr->object_creating_strong_reffed_ = true; + ptr->object_is_ref_counted_ = true; #endif return Object::Ref(ptr); } /// In some cases it may be handy to allocate an object for ref-counting - /// but not actually create references yet. (Such as when creating an object - /// in one thread to be passed to another which will own said object) - /// For such cases, allocate using NewDeferred() and then use MakeRefCounted() - /// on the raw Object* to create its initial reference. - /// Note that in debug builds this will run checks to make sure the object - /// wound up being ref-counted. To allocate an object for manual - /// deallocation, use NewUnmanaged() + /// but not actually create references yet. An example is when creating an + /// object in one thread to be passed to another which will own said object. + /// For such cases, allocate using NewDeferred() and then create the initial + /// strong ref in the desired thread using CompleteDeferred(). + /// Note that in debug builds, checks may be run to make sure deferred + /// objects wind up with references added to them at some point. If you + /// want to allocate an object for manual deallocation or permanent + /// existence, use NewUnmanaged() instead. template [[nodiscard]] static auto NewDeferred(ARGS&&... args) -> T* { T* ptr = new T(std::forward(args)...); #if BA_DEBUG_BUILD - if (ptr->object_strong_ref_count_ > 0) { - FatalError("ballistica::Object has strong-ref in constructor: " - + ptr->GetObjectDescription()); + /// Make sure things aren't creating strong refs to themselves in their + /// constructors. + if (ptr->object_has_been_strong_reffed_) { + FatalError( + "ballistica::Object has already been strong reffed in NewDeferred: " + + ptr->GetObjectDescription()); } - ptr->object_in_constructor_ = false; + ptr->object_is_pending_deferred_ = true; #endif return ptr; } + /// Complete a new-deferred operation, creating an initial strong reference. + /// One might ask why we require this call and don't simply allow creating an + /// initial strong ref the 'normal' way. The answer is that we don't want + /// to encourage a pattern where not-yet-referenced raw pointers are being + /// passed around casually. This open up too many possibilities for leaks + /// due to an expected exception preventing a raw pointer from ever getting + /// its first reference. Deferred allocation should be treated as a very + /// explicit two-part process with the object unusable until completion. template - static auto MakeRefCounted(T* ptr) -> Object::Ref { + static auto CompleteDeferred(T* ptr) -> Object::Ref { #if BA_DEBUG_BUILD - // Make sure we're operating on a fresh object. - assert(ptr->object_strong_ref_count_ == 0); - if (ptr->object_creating_strong_reffed_) { + /// Make sure we're operating on a fresh object created as deferred. + if (ptr->object_has_been_strong_reffed_) { FatalError( - "ballistica::Object already set up as reffed in MakeRefCounted: " + "ballistica::Object has already been strong reffed in " + "CompleteDeferred: " + ptr->GetObjectDescription()); } - ptr->object_creating_strong_reffed_ = true; + if (!ptr->object_is_pending_deferred_) { + FatalError( + "ballistica::Object passed to CompleteDeferred was not created as " + "deferred: " + + ptr->GetObjectDescription()); + } + ptr->object_is_pending_deferred_ = false; + ptr->object_is_ref_counted_ = true; #endif + return Object::Ref(ptr); } @@ -540,27 +602,31 @@ class Object { [[nodiscard]] static auto NewUnmanaged(ARGS&&... args) -> T* { T* ptr = new T(std::forward(args)...); #if BA_DEBUG_BUILD - ptr->object_in_constructor_ = false; + ptr->object_is_unmanaged_ = true; #endif return ptr; } + /// Logs a tally of ba::Object types and counts (debug build only). + static void LsObjects(); + private: #if BA_DEBUG_BUILD // Making operator new private here to help ensure all of our dynamic // allocation/deallocation goes through our special functions (New(), // NewDeferred(), etc.). However, sticking with original new for release - // builds since it may handle corner cases this does not. + // builds since it may handle corner cases that this does not. auto operator new(size_t size) -> void* { return new char[size]; } - auto ObjectUpdateForAcquire() -> void; - bool object_has_strong_ref_{}; - bool object_creating_strong_reffed_{}; + void ObjectUpdateForAcquire(); + bool object_has_been_strong_reffed_{}; + bool object_is_ref_counted_{}; + bool object_is_pending_deferred_{}; + bool object_is_unmanaged_{}; bool object_is_dead_{}; - bool object_in_constructor_{true}; Object* object_next_{}; Object* object_prev_{}; ThreadOwnership thread_ownership_{ThreadOwnership::kClassDefault}; - ThreadTag owner_thread_{ThreadTag::kInvalid}; + EventLoopID owner_thread_{EventLoopID::kInvalid}; bool thread_checks_enabled_{true}; millisecs_t object_birth_time_{}; bool object_printed_warning_{}; @@ -608,7 +674,7 @@ auto RefsToPointers(const std::vector >& refs) // Let's just access the memory directly; potentially faster? T** p = &(ptrs[0]); for (size_t i = 0; i < refs_size; i++) { - p[i] = refs[i].get(); + p[i] = refs[i].Get(); } } return ptrs; @@ -618,7 +684,7 @@ auto RefsToPointers(const std::vector >& refs) template void PruneDeadRefs(T* list) { for (typename T::iterator i = list->begin(); i != list->end();) { - if (!i->exists()) { + if (!i->Exists()) { i = list->erase(i); } else { i++; @@ -630,7 +696,7 @@ void PruneDeadRefs(T* list) { template void PruneDeadMapRefs(T* map) { for (typename T::iterator i = map->begin(); i != map->end();) { - if (!i->second.exists()) { + if (!i->second.Exists()) { typename T::iterator i_next = i; i_next++; map->erase(i); @@ -649,7 +715,7 @@ inline auto ObjToString(Object* obj) -> std::string { // A handy utility which creates a weak-ref in debug mode // and a simple pointer in release mode. // This can be used when a pointer *should* always be valid -// but its nice to be sure when the cpu cycles don't matter +// but its nice to be sure when the cpu cycles don't matter. #if BA_DEBUG_BUILD #define BA_DEBUG_PTR(TYPE) Object::WeakRef #else @@ -658,4 +724,4 @@ inline auto ObjToString(Object* obj) -> std::string { } // namespace ballistica -#endif // BALLISTICA_CORE_OBJECT_H_ +#endif // BALLISTICA_SHARED_FOUNDATION_OBJECT_H_ diff --git a/src/ballistica/shared/foundation/types.h b/src/ballistica/shared/foundation/types.h new file mode 100644 index 00000000..b0ed04f6 --- /dev/null +++ b/src/ballistica/shared/foundation/types.h @@ -0,0 +1,321 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SHARED_FOUNDATION_TYPES_H_ +#define BALLISTICA_SHARED_FOUNDATION_TYPES_H_ + +// Types used throughout the project. +// This header should not depend on any others in the project. +// Types can be defined (or predeclared) here if the are used +// in a significant number of places. The aim is to reduce the +// overall number of headers a given source file needs to pull in, +// helping to keep compile times down. + +#ifdef __cplusplus + +// Predeclare a few global namespace things +// (just enough to pass some pointers around without +// requiring a bunch of library/system headers). +typedef struct _object PyObject; +typedef struct _typeobject PyTypeObject; +typedef struct _ts PyThreadState; +struct PyMethodDef; +struct sockaddr_storage; + +#if BA_SDL_BUILD || BA_MINSDL_BUILD +union SDL_Event; +struct SDL_Keysym; +typedef struct _SDL_Joystick SDL_Joystick; +#endif + +namespace ballistica { + +// Used internally for time values. +typedef int64_t millisecs_t; +typedef int64_t microsecs_t; + +// We predeclare all our main ba classes here so that we can +// avoid pulling in their full headers as much as possible +// to keep compile times down. +struct cJSON; +class EventLoop; +class FeatureSetFrontEnd; +class JsonDict; +class Matrix44f; +class Object; +class Python; +class PythonRef; +class PythonCommand; +class PythonObjectSetBase; +class PythonModuleBuilder; +class Rect; +class Runnable; +class SockAddr; +class Timer; +class TimerList; +class Utils; +class Vector2f; +class Vector3f; +class Vector4f; + +// FIXME: remove this - a few base things we need. +namespace base { +class App; +class Graphics; +} // namespace base + +// BA_EXPORT_PYTHON_ENUM +/// Types of input a controller can send to the game. +/// +/// Category: Enums +/// +enum class InputType { + kUpDown = 2, + kLeftRight, + kJumpPress, + kJumpRelease, + kPunchPress, + kPunchRelease, + kBombPress, + kBombRelease, + kPickUpPress, + kPickUpRelease, + kRun, + kFlyPress, + kFlyRelease, + kStartPress, + kStartRelease, + kHoldPositionPress, + kHoldPositionRelease, + kLeftPress, + kLeftRelease, + kRightPress, + kRightRelease, + kUpPress, + kUpRelease, + kDownPress, + kDownRelease, + kLast // Sentinel +}; + +typedef int64_t TimerMedium; + +// BA_EXPORT_PYTHON_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 +/// display content at will vary significantly. +/// +/// Category: Enums +/// +/// 'large' is used for devices such as desktop PCs where fine details can +/// be clearly seen. UI elements are generally smaller on the screen +/// and more content can be seen at once. +/// +/// 'medium' is used for devices such as tablets, TVs, or VR headsets. +/// This mode strikes a balance between clean readability and amount of +/// content visible. +/// +/// 'small' is used primarily for phones or other small devices where +/// content needs to be presented as large and clear in order to remain +/// readable from an average distance. +enum class UIScale { + kLarge, + kMedium, + kSmall, + kLast // Sentinel. +}; + +// BA_EXPORT_PYTHON_ENUM +/// Specifies the type of time for various operations to target/use. +/// +/// Category: Enums +/// +/// 'sim' time is the local simulation time for an activity or session. +/// It can proceed at different rates depending on game speed, stops +/// for pauses, etc. +/// +/// 'base' is the baseline time for an activity or session. It proceeds +/// consistently regardless of game speed or pausing, but may stop during +/// occurrences such as network outages. +/// +/// 'real' time is mostly based on clock time, with a few exceptions. It may +/// not advance while the app is backgrounded for instance. (the engine +/// attempts to prevent single large time jumps from occurring) +enum class TimeType { + kSim, + kBase, + kReal, + kLast // Sentinel. +}; + +// BA_EXPORT_PYTHON_ENUM +/// Specifies the format time values are provided in. +/// +/// Category: Enums +enum class TimeFormat { + kSeconds, + kMilliseconds, + kLast // Sentinel. +}; + +// BA_EXPORT_PYTHON_ENUM +/// Permissions that can be requested from the OS. +/// +/// Category: Enums +enum class Permission { + kStorage, + kLast // Sentinel. +}; + +// BA_EXPORT_PYTHON_ENUM +/// Special characters the game can print. +/// +/// Category: Enums +enum class SpecialChar { + kDownArrow, + kUpArrow, + kLeftArrow, + kRightArrow, + kTopButton, + kLeftButton, + kRightButton, + kBottomButton, + kDelete, + kShift, + kBack, + kLogoFlat, + kRewindButton, + kPlayPauseButton, + kFastForwardButton, + kDpadCenterButton, + kOuyaButtonO, + kOuyaButtonU, + kOuyaButtonY, + kOuyaButtonA, + kOuyaLogo, + kLogo, + kTicket, + kGooglePlayGamesLogo, + kGameCenterLogo, + kDiceButton1, + kDiceButton2, + kDiceButton3, + kDiceButton4, + kGameCircleLogo, + kPartyIcon, + kTestAccount, + kTicketBacking, + kTrophy1, + kTrophy2, + kTrophy3, + kTrophy0a, + kTrophy0b, + kTrophy4, + kLocalAccount, + kAlibabaLogo, + kFlagUnitedStates, + kFlagMexico, + kFlagGermany, + kFlagBrazil, + kFlagRussia, + kFlagChina, + kFlagUnitedKingdom, + kFlagCanada, + kFlagIndia, + kFlagJapan, + kFlagFrance, + kFlagIndonesia, + kFlagItaly, + kFlagSouthKorea, + kFlagNetherlands, + kFedora, + kHal, + kCrown, + kYinYang, + kEyeBall, + kSkull, + kHeart, + kDragon, + kHelmet, + kMushroom, + kNinjaStar, + kVikingHelmet, + kMoon, + kSpider, + kFireball, + kFlagUnitedArabEmirates, + kFlagQatar, + kFlagEgypt, + kFlagKuwait, + kFlagAlgeria, + kFlagSaudiArabia, + kFlagMalaysia, + kFlagCzechRepublic, + kFlagAustralia, + kFlagSingapore, + kOculusLogo, + kSteamLogo, + kNvidiaLogo, + kFlagIran, + kFlagPoland, + kFlagArgentina, + kFlagPhilippines, + kFlagChile, + kMikirog, + kV2Logo, + kLast // Sentinel +}; + +/// Python exception types we can raise from our own exceptions. +enum class PyExcType { + kRuntime, + kAttribute, + kIndex, + kType, + kValue, + kContext, + kNotFound, + kNodeNotFound, + kActivityNotFound, + kSessionNotFound, + kSessionPlayerNotFound, + kInputDeviceNotFound, + kDelegateNotFound, + kWidgetNotFound +}; + +enum class LogLevel { + kDebug, + kInfo, + kWarning, + kError, + kCritical, +}; + +enum class ThreadSource { + /// A normal thread spun up by us. + kCreate, + /// For wrapping a ballistica thread around the existing main thread. + kWrapMain +}; + +/// Used for thread identification. +/// Mostly just for debugging. +enum class EventLoopID { + kInvalid, + kLogic, + kAssets, + kFileOut, + kMain, + kAudio, + kNetworkWrite, + kSuicide, + kStdin, + kBGDynamics +}; + +} // namespace ballistica + +#endif // __cplusplus + +#endif // BALLISTICA_SHARED_FOUNDATION_TYPES_H_ diff --git a/src/ballistica/generic/base64.cc b/src/ballistica/shared/generic/base64.cc similarity index 99% rename from src/ballistica/generic/base64.cc rename to src/ballistica/shared/generic/base64.cc index c775c522..77df7e20 100644 --- a/src/ballistica/generic/base64.cc +++ b/src/ballistica/shared/generic/base64.cc @@ -27,7 +27,7 @@ René Nyffenegger rene.nyffenegger@adp-gmbh.ch */ -#include "ballistica/generic/base64.h" +#include "ballistica/shared/generic/base64.h" namespace ballistica { diff --git a/src/ballistica/generic/base64.h b/src/ballistica/shared/generic/base64.h similarity index 69% rename from src/ballistica/generic/base64.h rename to src/ballistica/shared/generic/base64.h index dcc5deef..0df02824 100644 --- a/src/ballistica/generic/base64.h +++ b/src/ballistica/shared/generic/base64.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GENERIC_BASE64_H_ -#define BALLISTICA_GENERIC_BASE64_H_ +#ifndef BALLISTICA_SHARED_GENERIC_BASE64_H_ +#define BALLISTICA_SHARED_GENERIC_BASE64_H_ #include @@ -13,4 +13,4 @@ auto base64_decode(const std::string& s, bool urlsafe = false) -> std::string; } // namespace ballistica -#endif // BALLISTICA_GENERIC_BASE64_H_ +#endif // BALLISTICA_SHARED_GENERIC_BASE64_H_ diff --git a/src/ballistica/generic/buffer.h b/src/ballistica/shared/generic/buffer.h similarity index 91% rename from src/ballistica/generic/buffer.h rename to src/ballistica/shared/generic/buffer.h index 7ac2399b..15594641 100644 --- a/src/ballistica/generic/buffer.h +++ b/src/ballistica/shared/generic/buffer.h @@ -1,17 +1,17 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GENERIC_BUFFER_H_ -#define BALLISTICA_GENERIC_BUFFER_H_ +#ifndef BALLISTICA_SHARED_GENERIC_BUFFER_H_ +#define BALLISTICA_SHARED_GENERIC_BUFFER_H_ #include -#include "ballistica/generic/utils.h" +#include "ballistica/shared/generic/utils.h" namespace ballistica { // Simple data-holding buffer class. // (FIXME: should kill this and just use std::vector for this purpose) -template +template class Buffer { public: Buffer(const Buffer& b) : data_(nullptr), size_(0) { @@ -92,4 +92,4 @@ class Buffer { } // namespace ballistica -#endif // BALLISTICA_GENERIC_BUFFER_H_ +#endif // BALLISTICA_SHARED_GENERIC_BUFFER_H_ diff --git a/src/ballistica/generic/json.cc b/src/ballistica/shared/generic/json.cc similarity index 99% rename from src/ballistica/generic/json.cc rename to src/ballistica/shared/generic/json.cc index cd7ba946..bedbba5c 100644 --- a/src/ballistica/generic/json.cc +++ b/src/ballistica/shared/generic/json.cc @@ -26,7 +26,7 @@ /* cJSON */ /* JSON parser in C. */ -#include "ballistica/generic/json.h" +#include "ballistica/shared/generic/json.h" #include #include diff --git a/src/ballistica/generic/json.h b/src/ballistica/shared/generic/json.h similarity index 98% rename from src/ballistica/generic/json.h rename to src/ballistica/shared/generic/json.h index 7577f178..cde7ff73 100644 --- a/src/ballistica/generic/json.h +++ b/src/ballistica/shared/generic/json.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GENERIC_JSON_H_ -#define BALLISTICA_GENERIC_JSON_H_ +#ifndef BALLISTICA_SHARED_GENERIC_JSON_H_ +#define BALLISTICA_SHARED_GENERIC_JSON_H_ /* Copyright (c) 2009 Dave Gamble @@ -25,7 +25,7 @@ THE SOFTWARE. */ -#include "ballistica/ballistica.h" +#include "ballistica/shared/ballistica.h" namespace ballistica { @@ -236,4 +236,4 @@ class JsonDict : public JsonObject { } // namespace ballistica -#endif // BALLISTICA_GENERIC_JSON_H_ +#endif // BALLISTICA_SHARED_GENERIC_JSON_H_ diff --git a/src/ballistica/generic/lambda_runnable.h b/src/ballistica/shared/generic/lambda_runnable.h similarity index 56% rename from src/ballistica/generic/lambda_runnable.h rename to src/ballistica/shared/generic/lambda_runnable.h index c89ed72d..01724a74 100644 --- a/src/ballistica/generic/lambda_runnable.h +++ b/src/ballistica/shared/generic/lambda_runnable.h @@ -1,9 +1,9 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GENERIC_LAMBDA_RUNNABLE_H_ -#define BALLISTICA_GENERIC_LAMBDA_RUNNABLE_H_ +#ifndef BALLISTICA_SHARED_GENERIC_LAMBDA_RUNNABLE_H_ +#define BALLISTICA_SHARED_GENERIC_LAMBDA_RUNNABLE_H_ -#include "ballistica/generic/runnable.h" +#include "ballistica/shared/generic/runnable.h" namespace ballistica { @@ -20,19 +20,19 @@ class LambdaRunnable : public Runnable { F lambda_; }; -// Call this to allocate and return a raw lambda runnable +/// Create a LambdaRunnable from a raw lambda. template auto NewLambdaRunnable(const F& lambda) -> Object::Ref { return Object::New>(lambda); } -// Same but returns the raw pointer instead of a ref; -// (used when passing across threads). +/// Create an unmanaged LambdaRunnable from a raw lambda. +/// Use this with functionality that explicitly asks for unmanaged objects. template -auto NewLambdaRunnableRaw(const F& lambda) -> Runnable* { - return Object::NewDeferred>(lambda); +auto NewLambdaRunnableUnmanaged(const F& lambda) -> Runnable* { + return Object::NewUnmanaged>(lambda); } } // namespace ballistica -#endif // BALLISTICA_GENERIC_LAMBDA_RUNNABLE_H_ +#endif // BALLISTICA_SHARED_GENERIC_LAMBDA_RUNNABLE_H_ diff --git a/src/ballistica/generic/runnable.cc b/src/ballistica/shared/generic/runnable.cc similarity index 82% rename from src/ballistica/generic/runnable.cc rename to src/ballistica/shared/generic/runnable.cc index b6a8d79d..8dea3fb9 100644 --- a/src/ballistica/generic/runnable.cc +++ b/src/ballistica/shared/generic/runnable.cc @@ -1,6 +1,6 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/generic/runnable.h" +#include "ballistica/shared/generic/runnable.h" namespace ballistica { diff --git a/src/ballistica/generic/runnable.h b/src/ballistica/shared/generic/runnable.h similarity index 66% rename from src/ballistica/generic/runnable.h rename to src/ballistica/shared/generic/runnable.h index a0fe102c..c849f617 100644 --- a/src/ballistica/generic/runnable.h +++ b/src/ballistica/shared/generic/runnable.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GENERIC_RUNNABLE_H_ -#define BALLISTICA_GENERIC_RUNNABLE_H_ +#ifndef BALLISTICA_SHARED_GENERIC_RUNNABLE_H_ +#define BALLISTICA_SHARED_GENERIC_RUNNABLE_H_ #include -#include "ballistica/core/object.h" +#include "ballistica/shared/foundation/object.h" namespace ballistica { @@ -20,4 +20,4 @@ class Runnable : public Object { } // namespace ballistica -#endif // BALLISTICA_GENERIC_RUNNABLE_H_ +#endif // BALLISTICA_SHARED_GENERIC_RUNNABLE_H_ diff --git a/src/ballistica/generic/timer_list.cc b/src/ballistica/shared/generic/timer_list.cc similarity index 81% rename from src/ballistica/generic/timer_list.cc rename to src/ballistica/shared/generic/timer_list.cc index b92b8b5d..5d522467 100644 --- a/src/ballistica/generic/timer_list.cc +++ b/src/ballistica/shared/generic/timer_list.cc @@ -1,9 +1,8 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/generic/timer_list.h" +#include "ballistica/shared/generic/timer_list.h" -#include "ballistica/generic/runnable.h" -#include "ballistica/generic/timer.h" +#include "ballistica/shared/generic/runnable.h" namespace ballistica { @@ -186,7 +185,7 @@ auto TimerList::NewTimer(TimerMedium current_time, TimerMedium length, #pragma clang diagnostic pop } -auto TimerList::GetTimeToNextExpire(TimerMedium current_time) -> TimerMedium { +auto TimerList::TimeToNextExpire(TimerMedium current_time) -> TimerMedium { assert(!are_clearing_); if (!timers_) { return (TimerMedium)-1; @@ -200,6 +199,9 @@ auto TimerList::GetTimer(int id) -> Timer* { assert(id != 0); // Zero denotes "no-id". Timer* t = PullTimer(id, false); + if (!t) { + return nullptr; + } return t->dead_ ? nullptr : t; } @@ -265,7 +267,9 @@ void TimerList::AddTimer(Timer* t) { // Go along till we find an expire time later than ourself. while (*list != nullptr) { - if ((*list)->expire_time_ > t->expire_time_) break; + if ((*list)->expire_time_ > t->expire_time_) { + break; + } list = &((*list)->next_); } Timer* tmp = (*list); @@ -276,4 +280,54 @@ void TimerList::AddTimer(Timer* t) { t->on_list_ = true; } +Timer::Timer(TimerList* list, int id, TimerMedium current_time, + TimerMedium length, TimerMedium offset, int repeat_count) + : list_(list), + on_list_(false), + initial_(true), + dead_(false), + list_died_(false), + last_run_time_(current_time), + expire_time_(current_time + offset), + id_(id), + length_(length), + repeat_count_(repeat_count) { + list_->timer_count_total_++; +} + +Timer::~Timer() { + // If the list is going down, dont touch the corpse. + if (!list_died_) { + if (on_list_) { + list_->PullTimer(id_); + } else { + // Should never be explicitly deleting the current client timer + // (it should just get marked as dead so the loop can kill it when + // re-submitted). + assert(list_->client_timer_ != this); + } + list_->timer_count_total_--; + } +} + +void Timer::SetLength(TimerMedium l, bool set_start_time, + TimerMedium starttime) { + if (on_list_) { + assert(id_ != 0); // Zero denotes "no-id". + Timer* t = list_->PullTimer(id_); + BA_PRECONDITION(t == this); + length_ = l; + if (set_start_time) { + last_run_time_ = starttime; + } + expire_time_ = last_run_time_ + length_; + list_->AddTimer(this); + } else { + length_ = l; + if (set_start_time) { + last_run_time_ = starttime; + } + } +} + } // namespace ballistica diff --git a/src/ballistica/shared/generic/timer_list.h b/src/ballistica/shared/generic/timer_list.h new file mode 100644 index 00000000..3516cc52 --- /dev/null +++ b/src/ballistica/shared/generic/timer_list.h @@ -0,0 +1,95 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SHARED_GENERIC_TIMER_LIST_H_ +#define BALLISTICA_SHARED_GENERIC_TIMER_LIST_H_ + +#include +#include + +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/foundation/object.h" + +namespace ballistica { + +class TimerList { + public: + TimerList(); + ~TimerList(); + + // Run timers up to the provided target time. + void Run(TimerMedium target_time); + + // Create a timer with provided runnable. + auto NewTimer(TimerMedium current_time, TimerMedium length, + TimerMedium offset, int repeat_count, + const Object::Ref& runnable) -> Timer*; + + // Return a timer by its id, or nullptr if the timer no longer exists. + auto GetTimer(int id) -> Timer*; + + // Delete a currently-queued timer via its id. + void DeleteTimer(int timer_id); + + // Return the time until the next timer goes off. + // If no timers are present, -1 is returned. + auto TimeToNextExpire(TimerMedium current_time) -> TimerMedium; + + // Return the active timer count. Note that this does not include the client + // timer (a timer returned via GetExpiredTimer() but not yet re-submitted). + auto ActiveTimerCount() const -> int { return timer_count_active_; } + + auto Empty() -> bool { return (timers_ == nullptr); } + + void Clear(); + + private: + // Returns the next expired timer. When finished with the timer, + // return it to the list with Timer::submit() + // (this will either put it back in line or delete it) + auto GetExpiredTimer(TimerMedium target_time) -> Timer*; + auto GetExpiredCount(TimerMedium target_time) -> int; + auto PullTimer(int timer_id, bool remove = true) -> Timer*; + auto SubmitTimer(Timer* t) -> Timer*; + void AddTimer(Timer* t); + + int timer_count_active_{}; + int timer_count_inactive_{}; + int timer_count_total_{}; + Timer* client_timer_{}; + Timer* timers_{}; + Timer* timers_inactive_{}; + int next_timer_id_{1}; + bool running_{}; + bool are_clearing_{}; + friend class Timer; +}; + +class Timer { + public: + auto id() const -> int { return id_; } + auto length() const -> TimerMedium { return length_; } + void SetLength(TimerMedium l, bool set_start_time = false, + TimerMedium starttime = 0); + + private: + Timer(TimerList* list, int id, TimerMedium current_time, TimerMedium length, + TimerMedium offset, int repeat_count); + virtual ~Timer(); + TimerList* list_{}; + bool on_list_{}; + Timer* next_{}; + bool initial_{}; + bool dead_{}; + bool list_died_{}; + TimerMedium last_run_time_{}; + TimerMedium expire_time_{}; + int id_{}; + TimerMedium length_{}; + int repeat_count_{}; + Object::Ref runnable_; + friend class TimerList; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SHARED_GENERIC_TIMER_LIST_H_ diff --git a/src/ballistica/generic/utf8.cc b/src/ballistica/shared/generic/utf8.cc similarity index 99% rename from src/ballistica/generic/utf8.cc rename to src/ballistica/shared/generic/utf8.cc index ad52c8a3..6687f1fa 100644 --- a/src/ballistica/generic/utf8.cc +++ b/src/ballistica/shared/generic/utf8.cc @@ -14,7 +14,7 @@ with these routines reserved for higher performance on data known to be valid. */ -#include "ballistica/generic/utf8.h" +#include "utf8.h" #if _WIN32 || _WIN64 #include diff --git a/src/ballistica/generic/utf8.h b/src/ballistica/shared/generic/utf8.h similarity index 94% rename from src/ballistica/generic/utf8.h rename to src/ballistica/shared/generic/utf8.h index 1d7a46ed..79c5c678 100644 --- a/src/ballistica/generic/utf8.h +++ b/src/ballistica/shared/generic/utf8.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GENERIC_UTF8_H_ -#define BALLISTICA_GENERIC_UTF8_H_ +#ifndef BALLISTICA_SHARED_GENERIC_UTF8_H_ +#define BALLISTICA_SHARED_GENERIC_UTF8_H_ #include -#include "ballistica/ballistica.h" +#include "ballistica/shared/ballistica.h" // ericf note: i think this is cutef8?... namespace ballistica { @@ -82,4 +82,4 @@ auto u8_printf(char* fmt, ...) -> int; } // namespace ballistica -#endif // BALLISTICA_GENERIC_UTF8_H_ +#endif // BALLISTICA_SHARED_GENERIC_UTF8_H_ diff --git a/src/ballistica/generic/utils.cc b/src/ballistica/shared/generic/utils.cc similarity index 86% rename from src/ballistica/generic/utils.cc rename to src/ballistica/shared/generic/utils.cc index 2ac25a04..10025b03 100644 --- a/src/ballistica/generic/utils.cc +++ b/src/ballistica/shared/generic/utils.cc @@ -1,22 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/generic/utils.h" +#include "ballistica/shared/generic/utils.h" #include #include -#include "ballistica/app/app.h" -#include "ballistica/generic/huffman.h" -#include "ballistica/generic/json.h" -#include "ballistica/generic/utf8.h" -#include "ballistica/math/vector3f.h" -#include "ballistica/platform/platform.h" -#include "ballistica/scene/scene.h" - -// FIXME: Cleaner to add the lib to the project(s) instead? -#if BA_OSTYPE_WINDOWS -#pragma comment(lib, "Ws2_32.lib") -#endif +#include "ballistica/core/core.h" +#include "ballistica/core/platform/core_platform.h" +#include "ballistica/core/support/base_soft.h" +#include "ballistica/shared/generic/json.h" +#include "ballistica/shared/generic/runnable.h" +#include "ballistica/shared/generic/utf8.h" +#include "ballistica/shared/math/random.h" +#include "ballistica/shared/math/vector3f.h" namespace ballistica { @@ -105,43 +101,6 @@ Utils::Utils() { // Is this gonna be consistent cross-platform?... :-/ srand(543); // NOLINT - // Test our static-type-name functionality. - // This code runs at compile time and extracts human readable type names using - // __PRETTY_FUNCTION__ type functionality. However, it is dependent on - // specific compiler output and so could break easily if anything changes. - // Here we add some compile-time checks to alert us if that happens. - - // Remember that results can vary per compiler; make sure we match - // any one of the expected formats. - static_assert( - static_type_name_constexpr() == "ballistica::App *" - || static_type_name_constexpr() == "ballistica::App*" - || static_type_name_constexpr() - == "class ballistica::App*" - || static_type_name_constexpr() == "App*"); - Object::Ref testnode{}; - static_assert( - static_type_name_constexpr() - == "ballistica::Object::Ref" - || static_type_name_constexpr() - == "class ballistica::Object::Ref" - || static_type_name_constexpr() - == "Object::Ref"); - - // int testint{}; - // static_assert(static_type_name_constexpr() == "int"); - - // If anything above breaks, enable this code to debug/fix it. - // This will print a calculated type name as well as the full string - // it was parsed from. Use this to adjust the filtering as necessary so - // the resulting type name matches what is expected. - if (explicit_bool(false)) { - Log(LogLevel::kError, - "static_type_name check; name is '" - + static_type_name() + "' debug_full is '" - + static_type_name(true) + "'"); - } - // We now bake these in so they match across platforms... #if USE_BAKED_RANDS #else @@ -152,7 +111,7 @@ Utils::Utils() { precalc_rands_3_[i] = static_cast(rand()) / RAND_MAX; // NOLINT } #endif - huffman_ = std::make_unique(); + // huffman_ = std::make_unique(); } Utils::~Utils() = default; @@ -170,8 +129,8 @@ auto Utils::StringReplaceOne(std::string* target, const std::string& key, // from https://stackoverflow.com/questions/5343190/ // how-do-i-replace-all-instances-of-a-string-with-another-string/14678800 -auto Utils::StringReplaceAll(std::string* target, const std::string& key, - const std::string& replacement) -> void { +void Utils::StringReplaceAll(std::string* target, const std::string& key, + const std::string& replacement) { assert(target != nullptr); if (key.empty()) { return; @@ -428,7 +387,7 @@ static const char* g_default_random_names[] = { static std::list* g_random_names_list = nullptr; auto Utils::GetRandomNameList() -> const std::list& { - assert(InLogicThread()); + assert(core::g_base_soft && core::g_base_soft->InLogicThread()); if (g_random_names_list == nullptr) { // This will init the list with our default english names. SetRandomNameList(std::list(1, "DEFAULT_NAMES")); @@ -445,7 +404,7 @@ auto Utils::GetRandomNameList() -> const std::list& { } void Utils::SetRandomNameList(const std::list& custom_names) { - assert(InLogicThread()); + assert(core::g_base_soft && core::g_base_soft->InLogicThread()); if (!g_random_names_list) { g_random_names_list = new std::list; } else { @@ -496,18 +455,6 @@ auto Utils::FileToString(const std::string& file_name) -> std::string { return str_stream.str(); } -static void WaitThenDie(millisecs_t wait, const std::string& action) { - Platform::SleepMS(wait); - throw std::runtime_error("Timed out waiting for " + action + "; aborting."); -} - -void Utils::StartSuicideTimer(const std::string& action, millisecs_t delay) { - if (!g_app->started_suicide) { - new std::thread(WaitThenDie, delay, action); - g_app->started_suicide = true; - } -} - auto Utils::BaseName(const std::string& val) -> std::string { const char* c = val.c_str(); const char* lastvalid = c; diff --git a/src/ballistica/generic/utils.h b/src/ballistica/shared/generic/utils.h similarity index 94% rename from src/ballistica/generic/utils.h rename to src/ballistica/shared/generic/utils.h index dfec54ca..db5088ae 100644 --- a/src/ballistica/generic/utils.h +++ b/src/ballistica/shared/generic/utils.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_GENERIC_UTILS_H_ -#define BALLISTICA_GENERIC_UTILS_H_ +#ifndef BALLISTICA_SHARED_GENERIC_UTILS_H_ +#define BALLISTICA_SHARED_GENERIC_UTILS_H_ #include #include @@ -18,10 +18,14 @@ #endif #include -#include "ballistica/ballistica.h" +#include "ballistica/shared/ballistica.h" namespace ballistica { +// FIXME: Currently this lives in shared for easy access to static bits +// but the singleton of it lives under base. Should perhaps split into +// two classes. + const int kPrecalcRandsCount = 128; /// A holding tank for miscellaneous functionality not extensive enough to @@ -50,18 +54,13 @@ class Utils { static auto UTF8FromUnicodeChar(uint32_t c) -> std::string; static auto UTF8StringLength(const char* val) -> int; - /// Start a timer to kill the app after the set length of time. - /// Use this during shutdown or when trying to send a crash-report before - /// dying just to ensure we don't hang indefinitely. - static void StartSuicideTimer(const std::string& action, millisecs_t delay); - /// Replace a single occurrence of key with replacement in the target string. /// Returns whether a replacement occurred. static auto StringReplaceOne(std::string* target, const std::string& key, const std::string& replacement) -> bool; - static auto StringReplaceAll(std::string* target, const std::string& key, - const std::string& replacement) -> void; + static void StringReplaceAll(std::string* target, const std::string& key, + const std::string& replacement); /// Strip out or corrects invalid utf8. /// This is run under the hood for all the above calls but in some cases @@ -358,7 +357,7 @@ class Utils { assert(index < kPrecalcRandsCount); return precalc_rands_3_[index]; } - auto huffman() -> Huffman* { return huffman_.get(); } + // auto huffman() -> Huffman* { return huffman_.get(); } // FIXME - move to a nice math-y place static auto Sphrand(float radius = 1.0f) -> Vector3f; @@ -377,9 +376,9 @@ class Utils { static float precalc_rands_1_[]; static float precalc_rands_2_[]; static float precalc_rands_3_[]; - std::unique_ptr huffman_; + // std::unique_ptr huffman_; }; } // namespace ballistica -#endif // BALLISTICA_GENERIC_UTILS_H_ +#endif // BALLISTICA_SHARED_GENERIC_UTILS_H_ diff --git a/src/ballistica/math/matrix44f.cc b/src/ballistica/shared/math/matrix44f.cc similarity index 99% rename from src/ballistica/math/matrix44f.cc rename to src/ballistica/shared/math/matrix44f.cc index 1477feb0..a347c652 100644 --- a/src/ballistica/math/matrix44f.cc +++ b/src/ballistica/shared/math/matrix44f.cc @@ -1,6 +1,6 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/math/matrix44f.h" +#include "ballistica/shared/math/matrix44f.h" namespace ballistica { diff --git a/src/ballistica/math/matrix44f.h b/src/ballistica/shared/math/matrix44f.h similarity index 96% rename from src/ballistica/math/matrix44f.h rename to src/ballistica/shared/math/matrix44f.h index 077e5050..e8bcd52c 100644 --- a/src/ballistica/math/matrix44f.h +++ b/src/ballistica/shared/math/matrix44f.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_MATH_MATRIX44F_H_ -#define BALLISTICA_MATH_MATRIX44F_H_ +#ifndef BALLISTICA_SHARED_MATH_MATRIX44F_H_ +#define BALLISTICA_SHARED_MATH_MATRIX44F_H_ #include // for memcpy -#include "ballistica/math/vector3f.h" +#include "ballistica/shared/math/vector3f.h" namespace ballistica { @@ -199,4 +199,4 @@ auto Matrix44fFrustum(float left, float right, float bottom, float top, } // namespace ballistica -#endif // BALLISTICA_MATH_MATRIX44F_H_ +#endif // BALLISTICA_SHARED_MATH_MATRIX44F_H_ diff --git a/src/ballistica/math/point2d.h b/src/ballistica/shared/math/point2d.h similarity index 68% rename from src/ballistica/math/point2d.h rename to src/ballistica/shared/math/point2d.h index 4a0cacb4..c02f21ac 100644 --- a/src/ballistica/math/point2d.h +++ b/src/ballistica/shared/math/point2d.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_MATH_POINT2D_H_ -#define BALLISTICA_MATH_POINT2D_H_ +#ifndef BALLISTICA_SHARED_MATH_POINT2D_H_ +#define BALLISTICA_SHARED_MATH_POINT2D_H_ namespace ballistica { @@ -14,4 +14,4 @@ struct Point2D { } // namespace ballistica -#endif // BALLISTICA_MATH_POINT2D_H_ +#endif // BALLISTICA_SHARED_MATH_POINT2D_H_ diff --git a/src/ballistica/math/random.cc b/src/ballistica/shared/math/random.cc similarity index 99% rename from src/ballistica/math/random.cc rename to src/ballistica/shared/math/random.cc index cd0b0309..9e2dbbfa 100644 --- a/src/ballistica/math/random.cc +++ b/src/ballistica/shared/math/random.cc @@ -1,6 +1,6 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/math/random.h" +#include "ballistica/shared/math/random.h" #include #include diff --git a/src/ballistica/shared/math/random.h b/src/ballistica/shared/math/random.h new file mode 100644 index 00000000..184ea53f --- /dev/null +++ b/src/ballistica/shared/math/random.h @@ -0,0 +1,25 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SHARED_MATH_RANDOM_H_ +#define BALLISTICA_SHARED_MATH_RANDOM_H_ + +namespace ballistica { + +/// Return a random float value. Not guaranteed to be deterministic or +/// consistent across platforms. Should do something better than this. +inline auto RandomFloat() -> float { + // FIXME: should convert this to something thread-safe. + return static_cast( + (static_cast(rand()) / RAND_MAX)); // NOLINT +} + +class Random { + public: + static void GenList1D(float* list, int size); + static void GenList2D(float (*list)[2], int size); + static void GenList3D(float (*list)[3], int size); +}; + +} // namespace ballistica + +#endif // BALLISTICA_SHARED_MATH_RANDOM_H_ diff --git a/src/ballistica/math/rect.h b/src/ballistica/shared/math/rect.h similarity index 77% rename from src/ballistica/math/rect.h rename to src/ballistica/shared/math/rect.h index bd7778cc..0898589f 100644 --- a/src/ballistica/math/rect.h +++ b/src/ballistica/shared/math/rect.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_MATH_RECT_H_ -#define BALLISTICA_MATH_RECT_H_ +#ifndef BALLISTICA_SHARED_MATH_RECT_H_ +#define BALLISTICA_SHARED_MATH_RECT_H_ // A Generic 2d rect. namespace ballistica { @@ -18,4 +18,4 @@ class Rect { } // namespace ballistica -#endif // BALLISTICA_MATH_RECT_H_ +#endif // BALLISTICA_SHARED_MATH_RECT_H_ diff --git a/src/ballistica/math/vector2f.h b/src/ballistica/shared/math/vector2f.h similarity index 76% rename from src/ballistica/math/vector2f.h rename to src/ballistica/shared/math/vector2f.h index d994ae2d..08605c97 100644 --- a/src/ballistica/math/vector2f.h +++ b/src/ballistica/shared/math/vector2f.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_MATH_VECTOR2F_H_ -#define BALLISTICA_MATH_VECTOR2F_H_ +#ifndef BALLISTICA_SHARED_MATH_VECTOR2F_H_ +#define BALLISTICA_SHARED_MATH_VECTOR2F_H_ namespace ballistica { @@ -25,4 +25,4 @@ const Vector2f kVector2f0{0.0f, 0.0f}; } // namespace ballistica -#endif // BALLISTICA_MATH_VECTOR2F_H_ +#endif // BALLISTICA_SHARED_MATH_VECTOR2F_H_ diff --git a/src/ballistica/math/vector3f.cc b/src/ballistica/shared/math/vector3f.cc similarity index 96% rename from src/ballistica/math/vector3f.cc rename to src/ballistica/shared/math/vector3f.cc index bee04ae9..9e13d123 100644 --- a/src/ballistica/math/vector3f.cc +++ b/src/ballistica/shared/math/vector3f.cc @@ -1,6 +1,6 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/math/vector3f.h" +#include "ballistica/shared/math/vector3f.h" namespace ballistica { diff --git a/src/ballistica/math/vector3f.h b/src/ballistica/shared/math/vector3f.h similarity index 95% rename from src/ballistica/math/vector3f.h rename to src/ballistica/shared/math/vector3f.h index f931f393..f47feb71 100644 --- a/src/ballistica/math/vector3f.h +++ b/src/ballistica/shared/math/vector3f.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_MATH_VECTOR3F_H_ -#define BALLISTICA_MATH_VECTOR3F_H_ +#ifndef BALLISTICA_SHARED_MATH_VECTOR3F_H_ +#define BALLISTICA_SHARED_MATH_VECTOR3F_H_ #include #include // for memcpy #include -#include "ballistica/ballistica.h" +#include "ballistica/shared/ballistica.h" namespace ballistica { @@ -95,6 +95,8 @@ class Vector3f { return *this; } + auto AsStdVector() const -> std::vector { return {x, y, z}; } + auto Dot(const Vector3f& other) const -> float { return x * other.x + y * other.y + z * other.z; } @@ -197,4 +199,4 @@ const Vector3f kVector3f1{1.0f, 1.0f, 1.0f}; // NOLINT(cert-err58-cpp) } // namespace ballistica -#endif // BALLISTICA_MATH_VECTOR3F_H_ +#endif // BALLISTICA_SHARED_MATH_VECTOR3F_H_ diff --git a/src/ballistica/math/vector4f.h b/src/ballistica/shared/math/vector4f.h similarity index 75% rename from src/ballistica/math/vector4f.h rename to src/ballistica/shared/math/vector4f.h index 9447657e..8a36dcd2 100644 --- a/src/ballistica/math/vector4f.h +++ b/src/ballistica/shared/math/vector4f.h @@ -1,9 +1,9 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_MATH_VECTOR4F_H_ -#define BALLISTICA_MATH_VECTOR4F_H_ +#ifndef BALLISTICA_SHARED_MATH_VECTOR4F_H_ +#define BALLISTICA_SHARED_MATH_VECTOR4F_H_ -#include "ballistica/math/vector3f.h" +#include "ballistica/shared/math/vector3f.h" namespace ballistica { @@ -30,4 +30,4 @@ const Vector4f kVector4f0{0.0f, 0.0f, 0.0f, 0.0f}; // NOLINT(cert-err58-cpp) } // namespace ballistica -#endif // BALLISTICA_MATH_VECTOR4F_H_ +#endif // BALLISTICA_SHARED_MATH_VECTOR4F_H_ diff --git a/src/ballistica/networking/networking_sys.h b/src/ballistica/shared/networking/networking_sys.h similarity index 72% rename from src/ballistica/networking/networking_sys.h rename to src/ballistica/shared/networking/networking_sys.h index fc44c6b2..9e2ccbbf 100644 --- a/src/ballistica/networking/networking_sys.h +++ b/src/ballistica/shared/networking/networking_sys.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_NETWORKING_NETWORKING_SYS_H_ -#define BALLISTICA_NETWORKING_NETWORKING_SYS_H_ +#ifndef BALLISTICA_SHARED_NETWORKING_NETWORKING_SYS_H_ +#define BALLISTICA_SHARED_NETWORKING_NETWORKING_SYS_H_ // Include everything needed for standard sockets api usage. @@ -20,7 +20,7 @@ #include #if BA_OSTYPE_ANDROID // NOTE TO SELF: Apparently once we target API 24, ifaddrs.h is available. -#include "ballistica/platform/android/ifaddrs_android_ext.h" +#include "ballistica/core/platform/android/ifaddrs_android_ext.h" #else #include #endif @@ -28,4 +28,4 @@ #include #include -#endif // BALLISTICA_NETWORKING_NETWORKING_SYS_H_ +#endif // BALLISTICA_SHARED_NETWORKING_NETWORKING_SYS_H_ diff --git a/src/ballistica/networking/sockaddr.cc b/src/ballistica/shared/networking/sockaddr.cc similarity index 94% rename from src/ballistica/networking/sockaddr.cc rename to src/ballistica/shared/networking/sockaddr.cc index cc38770c..68fdf993 100644 --- a/src/ballistica/networking/sockaddr.cc +++ b/src/ballistica/shared/networking/sockaddr.cc @@ -1,6 +1,6 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/networking/sockaddr.h" +#include "ballistica/shared/networking/sockaddr.h" namespace ballistica { diff --git a/src/ballistica/networking/sockaddr.h b/src/ballistica/shared/networking/sockaddr.h similarity index 89% rename from src/ballistica/networking/sockaddr.h rename to src/ballistica/shared/networking/sockaddr.h index ff112459..f372bf70 100644 --- a/src/ballistica/networking/sockaddr.h +++ b/src/ballistica/shared/networking/sockaddr.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_NETWORKING_SOCKADDR_H_ -#define BALLISTICA_NETWORKING_SOCKADDR_H_ +#ifndef BALLISTICA_SHARED_NETWORKING_SOCKADDR_H_ +#define BALLISTICA_SHARED_NETWORKING_SOCKADDR_H_ #include #include -#include "ballistica/ballistica.h" -#include "ballistica/networking/networking_sys.h" +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/networking/networking_sys.h" namespace ballistica { @@ -72,4 +72,4 @@ class SockAddr { } // namespace ballistica -#endif // BALLISTICA_NETWORKING_SOCKADDR_H_ +#endif // BALLISTICA_SHARED_NETWORKING_SOCKADDR_H_ diff --git a/src/ballistica/shared/python/python.cc b/src/ballistica/shared/python/python.cc new file mode 100644 index 00000000..414e2ccc --- /dev/null +++ b/src/ballistica/shared/python/python.cc @@ -0,0 +1,516 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/shared/python/python.h" + +#include "ballistica/core/support/base_soft.h" +#include "ballistica/shared/python/python_command.h" +#include "ballistica/shared/python/python_sys.h" + +// Sanity test: our XCode, Android, and Windows builds should be +// using a debug build of the Python library. +// Todo: could also verify this at runtime by checking for +// existence of sys.gettotalrefcount(). (is that still valid in 3.8?) +#if BA_XCODE_BUILD || BA_OSTYPE_ANDROID || BA_OSTYPE_WINDOWS +#if BA_DEBUG_BUILD +#ifndef Py_DEBUG +#error Expected Py_DEBUG to be defined for this build. +#endif // Py_DEBUG +#else // BA_DEBUG_BUILD +#ifdef Py_DEBUG +#error Expected Py_DEBUG to NOT be defined for this build. +#endif // Py_DEBUG +#endif // BA_DEBUG_BUILD +#endif // BA_XCODE_BUILD || BA_OSTYPE_ANDROID + +namespace ballistica { + +// We implicitly use core functionality here; our behavior is undefined +// if nobody has imported core yet. +using core::g_base_soft; + +// Ignore signed bitwise stuff; python macros do it quite a bit. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#pragma ide diagnostic ignored "RedundantCast" + +void Python::SetPythonException(const Exception& exc) { + PyExcType exctype{exc.python_type()}; + const char* description{GetShortExceptionDescription(exc)}; + PyObject* pytype{}; + switch (exctype) { + case PyExcType::kRuntime: + pytype = PyExc_RuntimeError; + break; + case PyExcType::kAttribute: + pytype = PyExc_AttributeError; + break; + case PyExcType::kIndex: + pytype = PyExc_IndexError; + break; + case PyExcType::kValue: + pytype = PyExc_ValueError; + break; + case PyExcType::kType: + pytype = PyExc_TypeError; + break; + default: + // That's it for builtin exception types; all other values map + // to custom error types defined in base. + if (g_base_soft) { + pytype = g_base_soft->GetPyExceptionType(exctype); + } + break; + } + // If base wasn't available or whatnot, tweak our error to make that known. + // We should not be setting those extended types here in core so + // this should never happen. + if (pytype == nullptr) { + description = + "SetPythonException error type unavailable; should not happen."; + pytype = PyExc_RuntimeError; + } + assert(pytype != nullptr && PyType_Check(pytype)); + PyErr_SetString(pytype, description); +} + +const char* Python::ScopedCallLabel::current_label_ = nullptr; + +auto Python::HaveGIL() -> bool { return static_cast(PyGILState_Check()); } + +void Python::PrintStackTrace() { + bool available{}; + if (g_base_soft) { + available = g_base_soft->PrintPythonStackTrace(); + } + if (!available) { + Log(LogLevel::kWarning, + "Python::PrintStackTrace() called before _babase set up; " + "not printing."); + } +} + +auto Python::IsPyString(PyObject* o) -> bool { + assert(HaveGIL()); + assert(o != nullptr); + + return PyUnicode_Check(o); +} + +auto Python::GetPyString(PyObject* o) -> std::string { + assert(HaveGIL()); + assert(o != nullptr); + + PyExcType exctype{PyExcType::kType}; + if (PyUnicode_Check(o)) { + return PyUnicode_AsUTF8(o); + } + throw Exception( + "Can't get string from value: " + Python::ObjToString(o) + ".", exctype); +} + +template +auto GetPyIntT(PyObject* o) -> T { + assert(Python::HaveGIL()); + assert(o != nullptr); + + if (PyLong_Check(o)) { + return static_cast_check_fit(PyLong_AS_LONG(o)); + } + if (PyNumber_Check(o)) { + PyObject* f = PyNumber_Long(o); + if (f) { + auto val = static_cast_check_fit(PyLong_AS_LONG(f)); + Py_DECREF(f); + return val; + } + } + + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + + // Assuming any failure here was type related. + throw Exception("Can't get int from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::GetPyInt64(PyObject* o) -> int64_t { + return GetPyIntT(o); +} + +auto Python::GetPyInt(PyObject* o) -> int { return GetPyIntT(o); } + +auto Python::GetPyBool(PyObject* o) -> bool { + assert(HaveGIL()); + assert(o != nullptr); + + if (o == Py_True) { + return true; + } + if (o == Py_False) { + return false; + } + if (PyLong_Check(o)) { + return (PyLong_AS_LONG(o) != 0); + } + if (PyNumber_Check(o)) { + if (PyObject* o2 = PyNumber_Long(o)) { + auto val = PyLong_AS_LONG(o2); + Py_DECREF(o2); + return (val != 0); + } + } + + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + + // Assuming any failure here was type related. + throw Exception("Can't get bool from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::CanGetPyDouble(PyObject* o) -> bool { + assert(HaveGIL()); + assert(o != nullptr); + + return static_cast(PyNumber_Check(o)); +} + +auto Python::GetPyDouble(PyObject* o) -> double { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + // Try to take the fast path if its a float. + if (PyFloat_Check(o)) { + return PyFloat_AS_DOUBLE(o); + } + if (PyNumber_Check(o)) { + if (PyObject* f = PyNumber_Float(o)) { + double val = PyFloat_AS_DOUBLE(f); + Py_DECREF(f); + return val; + } + } + + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + throw Exception( + "Can't get double from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::GetPyFloats(PyObject* o) -> std::vector { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.Exists()); + Py_ssize_t size = PySequence_Fast_GET_SIZE(sequence.Get()); + PyObject** py_objects = PySequence_Fast_ITEMS(sequence.Get()); + std::vector vals(static_cast(size)); + assert(vals.size() == size); + for (Py_ssize_t i = 0; i < size; i++) { + vals[i] = Python::GetPyFloat(py_objects[i]); + } + return vals; +} + +template +auto GetPyIntsT(PyObject* o) -> std::vector { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.Exists()); + Py_ssize_t size = PySequence_Fast_GET_SIZE(sequence.Get()); + PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.Get()); + std::vector vals(static_cast(size)); + assert(vals.size() == size); + for (Py_ssize_t i = 0; i < size; i++) { + vals[i] = GetPyIntT(pyobjs[i]); + } + return vals; +} + +auto Python::GetPyInts64(PyObject* o) -> std::vector { + return GetPyIntsT(o); +} + +auto Python::GetPyInts(PyObject* o) -> std::vector { + return GetPyIntsT(o); +} + +// Hmm should just template the above func? +auto Python::GetPyUInts64(PyObject* o) -> std::vector { + return GetPyIntsT(o); +} + +auto Python::GetPyPoint2D(PyObject* o) -> Point2D { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + Point2D p; + if (!PyTuple_Check(o) || (PyTuple_GET_SIZE(o) != 2)) { + throw Exception("Expected 2 member tuple for point.", PyExcType::kType); + } + p.x = Python::GetPyFloat(PyTuple_GET_ITEM(o, 0)); + p.y = Python::GetPyFloat(PyTuple_GET_ITEM(o, 1)); + return p; +} + +auto Python::StringList(const std::list& values) -> PythonRef { + assert(HaveGIL()); + auto size{static_cast(values.size())}; + PythonRef pylist{PyList_New(size), PythonRef::kSteal}; + int i{}; + for (auto&& value : values) { + PyObject* item{PyUnicode_FromString(value.c_str())}; + assert(item); + PyList_SET_ITEM(pylist.Get(), i, item); + ++i; + } + return pylist; +} + +auto Python::SingleMemberTuple(const PythonRef& member) -> PythonRef { + assert(HaveGIL()); + return {Py_BuildValue("(O)", member.NewRef()), PythonRef::kSteal}; +} + +auto Python::GetPythonFileLocation(bool pretty) -> std::string { + assert(HaveGIL()); + if (PyFrameObject* f = PyEval_GetFrame()) { + const char* path; + auto code_obj = + PythonRef::Stolen(reinterpret_cast(PyFrame_GetCode(f))); + auto* code = reinterpret_cast(code_obj.Get()); + assert(code); + if (code && code->co_filename) { + assert(PyUnicode_Check(code->co_filename)); + path = PyUnicode_AsUTF8(code->co_filename); + if (pretty) { + if (path[0] == '<') { + // Don't attempt to parse stuff like :1 + return ""; + } else { + // Advance past any '/' and '\'s + while (true) { + const char* s = strchr(path, '/'); + if (s) { + path = s + 1; + } else { + const char* s2 = strchr(path, '\\'); + if (s2) { + path = s2 + 1; + } else { + break; + } + } + } + } + } + } else { + path = ""; + } + std::string name = + std::string(path) + ":" + std::to_string(PyFrame_GetLineNumber(f)); + return name; + } + return ""; +} + +auto Python::GetContextBaseString() -> std::string { + // Allow this to survive before core is bootstrapped. + if (!g_base_soft) { + return " context_ref: "; + } + return g_base_soft->DoGetContextBaseString(); +} + +void Python::PrintContextNotYetBootstrapped() { + // (no logic-thread-check here; can be called early or from other threads) + std::string s = std::string(" root call: \n"); + s += Python::GetContextBaseString(); + PySys_WriteStderr("%s\n", s.c_str()); +} + +void Python::PrintContextAuto() { + // Lets print whatever context info is available. + // FIXME: If we have recursive calls this may not print + // the context we'd expect; we'd need a unified stack. + if (!g_base_soft) { + PrintContextNotYetBootstrapped(); + } +} + +auto Python::ObjToString(PyObject* obj) -> std::string { + if (obj) { + return PythonRef(obj, PythonRef::kAcquire).Str(); + } else { + return ""; + } +} + +auto Python::ObjTypeToString(PyObject* obj) -> std::string { + if (obj) { + return PythonRef(obj, PythonRef::kAcquire).Type().Str(); + } else { + return ""; + } +} + +void Python::MarkReachedEndOfModule(PyObject* module) { + auto* val = Py_True; + Py_INCREF(val); + auto result = PyObject_SetAttrString(module, "_REACHED_END_OF_MODULE", val); + assert(result == 0); +} + +class Python::ScopedInterpreterLock::Impl { + public: + Impl() { + if (need_lock_) { + // Grab the python GIL. + gstate_ = PyGILState_Ensure(); + } + } + ~Impl() { + if (need_lock_) { + // Release the python GIL. + PyGILState_Release(gstate_); + } + } + + private: + bool need_lock_{true}; + PyGILState_STATE gstate_{PyGILState_UNLOCKED}; +}; + +Python::ScopedInterpreterLock::ScopedInterpreterLock() + : impl_{new Python::ScopedInterpreterLock::Impl()} +// impl_{std::make_unique()} +{} + +Python::ScopedInterpreterLock::~ScopedInterpreterLock() { delete impl_; } + +// (some stuff borrowed from python's source code - used in our overriding of +// objects' dir() results) + +/* alphabetical order */ +_Py_IDENTIFIER(__class__); +_Py_IDENTIFIER(__dict__); + +/* ------------------------- PyObject_Dir() helpers ------------------------- */ + +/* + Merge the __dict__ of aclass into dict, and recursively also all + the __dict__s of aclass's base classes. The order of merging isn't + defined, as it's expected that only the final set of dict keys is + interesting. + Return 0 on success, -1 on error. + */ + +static auto merge_class_dict(PyObject* dict, PyObject* aclass) -> int { + PyObject* classdict; + PyObject* bases; + _Py_IDENTIFIER(__bases__); + + assert(PyDict_Check(dict)); + assert(aclass); + + /* Merge in the type's dict (if any). */ + classdict = _PyObject_GetAttrId(aclass, &PyId___dict__); + if (classdict == nullptr) { + PyErr_Clear(); + } else { + int status = PyDict_Update(dict, classdict); + Py_DECREF(classdict); + if (status < 0) return -1; + } + + /* Recursively merge in the base types' (if any) dicts. */ + bases = _PyObject_GetAttrId(aclass, &PyId___bases__); + if (bases == nullptr) { + PyErr_Clear(); + } else { + /* We have no guarantee that bases is a real tuple */ + Py_ssize_t i; + Py_ssize_t n; + n = PySequence_Size(bases); /* This better be right */ + if (n < 0) { + PyErr_Clear(); + } else { + for (i = 0; i < n; i++) { + int status; + PyObject* base = PySequence_GetItem(bases, i); + if (base == nullptr) { + Py_DECREF(bases); + return -1; + } + status = merge_class_dict(dict, base); + Py_DECREF(base); + if (status < 0) { + Py_DECREF(bases); + return -1; + } + } + } + Py_DECREF(bases); + } + return 0; +} + +/* __dir__ for generic objects: returns __dict__, __class__, + and recursively up the __class__.__bases__ chain. + */ +auto Python::generic_dir(PyObject* self) -> PyObject* { + PyObject* result = nullptr; + PyObject* dict = nullptr; + PyObject* itsclass = nullptr; + + /* Get __dict__ (which may or may not be a real dict...) */ + dict = _PyObject_GetAttrId(self, &PyId___dict__); + if (dict == nullptr) { + PyErr_Clear(); + dict = PyDict_New(); + } else if (!PyDict_Check(dict)) { + Py_DECREF(dict); + dict = PyDict_New(); + } else { + /* Copy __dict__ to avoid mutating it. */ + PyObject* temp = PyDict_Copy(dict); + Py_DECREF(dict); + dict = temp; + } + + if (dict == nullptr) goto error; + + /* Merge in attrs reachable from its class. */ + itsclass = _PyObject_GetAttrId(self, &PyId___class__); + if (itsclass == nullptr) + /* XXX(tomer): Perhaps fall back to obj->ob_type if no + __class__ exists? */ + PyErr_Clear(); + else if (merge_class_dict(dict, itsclass) != 0) + goto error; + + result = PyDict_Keys(dict); + /* fall through */ +error: + Py_XDECREF(itsclass); + Py_XDECREF(dict); + return result; +} +//////////////// end __dir__ helpers + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/shared/python/python.h b/src/ballistica/shared/python/python.h new file mode 100644 index 00000000..40344e80 --- /dev/null +++ b/src/ballistica/shared/python/python.h @@ -0,0 +1,126 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SHARED_PYTHON_PYTHON_H_ +#define BALLISTICA_SHARED_PYTHON_PYTHON_H_ + +#include +#include +#include +#include +#include +#include +#include + +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/generic/runnable.h" +#include "ballistica/shared/math/point2d.h" +#include "ballistica/shared/python/python_object_set.h" +#include "ballistica/shared/python/python_ref.h" + +namespace ballistica { + +/// Core Python support/infrastructure class. +class Python { + public: + /// When calling a python callable directly, you can use the following + /// to push and pop a text label which will be printed as 'call' in errors. + class ScopedCallLabel { + public: + explicit ScopedCallLabel(const char* label) { + prev_label_ = current_label_; + } + ~ScopedCallLabel() { current_label_ = prev_label_; } + static auto current_label() -> const char* { return current_label_; } + + private: + const char* prev_label_{}; + static const char* current_label_; + BA_DISALLOW_CLASS_COPIES(ScopedCallLabel); + }; + + /// Use this to protect Python code that may be run in cases where we don't + /// hold the Global Interpreter Lock (GIL) (basically anything outside of the + /// logic thread). + class ScopedInterpreterLock { + public: + ScopedInterpreterLock(); + ~ScopedInterpreterLock(); + + private: + class Impl; + // Note: should use unique_ptr for this, but build fails on raspberry pi + // (gcc 8.3.0). Works on Ubuntu 9.3 so should try again later. + // std::unique_ptr impl_{}; + Impl* impl_{}; + }; + + // static auto Create() -> Python*; + + /// Return whether the current thread holds the global-interpreter-lock. + /// We must always hold the GIL while running python code. + /// This *should* generally be the case by default, but this can be handy for + /// sanity checking that. + static auto HaveGIL() -> bool; + + /// Attempt to print the python stack trace. + static void PrintStackTrace(); + + /// Pass any PyObject* (including nullptr) to get a readable string + /// (basically equivalent of str(foo)). + static auto ObjToString(PyObject* obj) -> std::string; + + /// Pass any PyObject* (including nullptr) to get a readable string + /// for its type (basically equivalent of str(type(foo)). + static auto ObjTypeToString(PyObject* obj) -> std::string; + + // Print various context debugging bits to Python's sys.stderr. + static void PrintContextNotYetBootstrapped(); + static void PrintContextAuto(); + + static auto GetContextBaseString() -> std::string; + + /// Borrowed from Python's source code: used in overriding of objects' dir() + /// results. + static auto generic_dir(PyObject* self) -> PyObject*; + + /// Return a minimal filename/position string such as 'foo.py:201' based + /// on the Python stack state. This shouldn't be too expensive to fetch and + /// is useful as an object identifier/etc. + static auto GetPythonFileLocation(bool pretty = true) -> std::string; + + // For checking and pulling values out of Python objects. + // These will all throw Exceptions on errors. + + static auto GetPyString(PyObject* o) -> std::string; + /// Get string with Lstr objs converted to json. + static auto GetPyInt64(PyObject* o) -> int64_t; + static auto GetPyInt(PyObject* o) -> int; + static auto IsPyString(PyObject* o) -> bool; + static auto GetPyBool(PyObject* o) -> bool; + static auto CanGetPyDouble(PyObject* o) -> bool; + static auto GetPyFloat(PyObject* o) -> float { + return static_cast(GetPyDouble(o)); + } + static auto GetPyDouble(PyObject* o) -> double; + static auto GetPyFloats(PyObject* o) -> std::vector; + static auto GetPyInts64(PyObject* o) -> std::vector; + static auto GetPyInts(PyObject* o) -> std::vector; + static auto GetPyUInts64(PyObject* o) -> std::vector; + static auto GetPyPoint2D(PyObject* o) -> Point2D; + + /// Set Python exception from C++ Exception. + static void SetPythonException(const Exception& exc); + + /// Create a Python list of strings. + static auto StringList(const std::list& values) -> PythonRef; + + /// Create a Python single-member tuple. + static auto SingleMemberTuple(const PythonRef& member) -> PythonRef; + + static void MarkReachedEndOfModule(PyObject* module); +}; + +} // namespace ballistica + +#endif // BALLISTICA_SHARED_PYTHON_PYTHON_H_ diff --git a/src/ballistica/python/class/python_class.cc b/src/ballistica/shared/python/python_class.cc similarity index 59% rename from src/ballistica/python/class/python_class.cc rename to src/ballistica/shared/python/python_class.cc index 6ce9de3e..b6530162 100644 --- a/src/ballistica/python/class/python_class.cc +++ b/src/ballistica/shared/python/python_class.cc @@ -1,37 +1,25 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/python/class/python_class.h" +#include "ballistica/shared/python/python_class.h" -#include "ballistica/ballistica.h" -#include "ballistica/python/python.h" +#include "ballistica/shared/python/python.h" namespace ballistica { -void PythonClass::SetupType(PyTypeObject* obj) { - PyTypeObject t = { - PyVarObject_HEAD_INIT(nullptr, 0) - // .tp_name = "ba.Object", - // .tp_basicsize = sizeof(PythonClass), - // .tp_itemsize = 0, - // .tp_dealloc = (destructor)tp_dealloc, - // .tp_repr = (reprfunc)tp_repr, - // .tp_getattro = (getattrofunc)tp_getattro, - // .tp_setattro = (setattrofunc)tp_setattro, - // .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, - // .tp_doc = "A ballistica object.", - // .tp_new = tp_new, - }; +void PythonClass::SetupType(PyTypeObject* cls) { + assert(Py_REFCNT(cls) == 0); - // python samples use the initializer style above, but it fails + PyTypeObject t = {PyVarObject_HEAD_INIT(nullptr, 0)}; + + // Python samples use the initializer style above, but it fails // in g++ and sounds like it might not be allowed in c++ anyway, // ..so this is close enough... // (and still more readable than setting ALL values positionally) assert(t.tp_itemsize == 0); // should all be zeroed though.. - t.tp_name = "ba.Object"; + t.tp_name = "babase.FixmeClassShouldOverride"; t.tp_basicsize = sizeof(PythonClass); t.tp_itemsize = 0; t.tp_dealloc = (destructor)tp_dealloc; - // t.tp_repr = (reprfunc)tp_repr; t.tp_getattro = (getattrofunc)tp_getattro; t.tp_setattro = (setattrofunc)tp_setattro; // NOLINTNEXTLINE (signed bitwise ops) @@ -39,27 +27,36 @@ void PythonClass::SetupType(PyTypeObject* obj) { t.tp_doc = "A ballistica object."; t.tp_new = tp_new; - memcpy(obj, &t, sizeof(t)); + memcpy(cls, &t, sizeof(t)); } -auto PythonClass::tp_repr(PythonClass* self) -> PyObject* { - BA_PYTHON_TRY; - return Py_BuildValue("s", ""); - BA_PYTHON_CATCH; +auto PythonClass::TypeIsSetUp(PyTypeObject* cls) -> bool { + // Let's just look at its refcount. + return Py_REFCNT(cls) > 0; } + auto PythonClass::tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* { + // Simply allocating and returning a zeroed instance of our class here. + // If subclasses need to construct/destruct any other values in the object + // they can either do it manually here and in tp_dealloc *or* they can get + // fancy and use placement-new to allow arbitrary C++ stuff to live in the + // class. auto* self = reinterpret_cast(type->tp_alloc(type, 0)); return reinterpret_cast(self); } + void PythonClass::tp_dealloc(PythonClass* self) { + // Vanilla Python deallocation. Py_TYPE(self)->tp_free(reinterpret_cast(self)); } + auto PythonClass::tp_getattro(PythonClass* node, PyObject* attr) -> PyObject* { BA_PYTHON_TRY; return PyObject_GenericGetAttr(reinterpret_cast(node), attr); BA_PYTHON_CATCH; } + auto PythonClass::tp_setattro(PythonClass* node, PyObject* attr, PyObject* val) -> int { BA_PYTHON_TRY; diff --git a/src/ballistica/shared/python/python_class.h b/src/ballistica/shared/python/python_class.h new file mode 100644 index 00000000..572fea36 --- /dev/null +++ b/src/ballistica/shared/python/python_class.h @@ -0,0 +1,48 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SHARED_PYTHON_PYTHON_CLASS_H_ +#define BALLISTICA_SHARED_PYTHON_PYTHON_CLASS_H_ + +#include "ballistica/shared/python/python_sys.h" + +namespace ballistica { + +// A convenient class for defining Python C types. +// Subclasses should include a static type object and can then +// provide/override whichever aspects of it they want. +// Be aware that if this class is added to multiple Python modules it will +// be considered the exact same type to Python since the static type object +// is the same for each. +class PythonClass { + public: + PyObject_HEAD; + static void SetupType(PyTypeObject* cls); + + /// For sanity checking; to make sure classes aren't used before + /// being inited. + static auto TypeIsSetUp(PyTypeObject* cls) -> bool; + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "cppcoreguidelines-pro-type-member-init" + // This ugly mess is just to define a constructor that does nothing. + // Otherwise we get a default constructor that zeroes out the stuff in + // PyObject_HEAD. Pretty much the only time our constructors actually fire is + // if we're doing a placement-new on top of tp_alloc() results and in that + // case we don't want to touch anything Python already set or we get a nice + // crash. + PythonClass() {} // NOLINT(modernize-use-equals-default) +#pragma clang diagnostic pop + + private: + static auto tp_repr(PythonClass* self) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) + -> PyObject*; + static void tp_dealloc(PythonClass* self); + static auto tp_getattro(PythonClass* node, PyObject* attr) -> PyObject*; + static auto tp_setattro(PythonClass* node, PyObject* attr, PyObject* val) + -> int; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SHARED_PYTHON_PYTHON_CLASS_H_ diff --git a/src/ballistica/python/python_command.cc b/src/ballistica/shared/python/python_command.cc similarity index 58% rename from src/ballistica/python/python_command.cc rename to src/ballistica/shared/python/python_command.cc index 54553315..04d82510 100644 --- a/src/ballistica/python/python_command.cc +++ b/src/ballistica/shared/python/python_command.cc @@ -1,9 +1,10 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/python/python_command.h" +#include "ballistica/shared/python/python_command.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_sys.h" +#include "ballistica/core/python/core_python.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" // Save/restore current command for logging/etc. // this isn't exception-safe, but we should never let @@ -32,6 +33,8 @@ auto PythonCommand::operator=(const PythonCommand& src) -> PythonCommand& { if (&src == this) { return *this; } + // TODO(ericf): we should just grab refs to their code objs if they have + // them, right? file_code_obj_.Release(); eval_code_obj_.Release(); command_ = src.command_; @@ -47,7 +50,7 @@ auto PythonCommand::operator=(const std::string& src) -> PythonCommand& { void PythonCommand::CompileForExec() { assert(Python::HaveGIL()); - assert(file_code_obj_.get() == nullptr); + assert(file_code_obj_.Get() == nullptr); PyObject* o = Py_CompileString(command_.c_str(), file_name_.c_str(), Py_file_input); if (o == nullptr) { @@ -62,7 +65,7 @@ void PythonCommand::CompileForExec() { void PythonCommand::CompileForEval(bool print_errors) { assert(Python::HaveGIL()); - assert(eval_code_obj_.get() == nullptr); + assert(eval_code_obj_.Get() == nullptr); PyObject* o = Py_CompileString(command_.c_str(), file_name_.c_str(), Py_eval_input); if (o == nullptr) { @@ -80,39 +83,76 @@ void PythonCommand::CompileForEval(bool print_errors) { PythonCommand::~PythonCommand() { dead_ = true; } -auto PythonCommand::Run() -> bool { +auto PythonCommand::CanEval() -> bool { assert(Python::HaveGIL()); - if (!g_python) { - // This probably means the game is dying; let's not - // throw an exception here so we don't mask the original error. - Log(LogLevel::kError, "PythonCommand: not running due to null g_python."); + if (!eval_code_obj_.Get()) { + CompileForEval(false); + } + if (!eval_code_obj_.Get()) { + PyErr_Clear(); return false; } + PyErr_Clear(); + return true; +} + +auto PythonCommand::Exec(bool print_errors, PyObject* globals, PyObject* locals) + -> bool { + assert(Python::HaveGIL()); + + // If we're being used before core is up, we need both global and + // locals to be passed. + // Note: accessing core globals directly here; normally don't do this. + assert(core::g_core != nullptr || (globals != nullptr && locals != nullptr)); + assert(!dead_); - if (!file_code_obj_.get()) { + + if (globals == nullptr) { + globals = core::g_core->python->objs() + .Get(core::CorePython::ObjID::kMainDict) + .Get(); + } + if (locals == nullptr) { + locals = core::g_core->python->objs() + .Get(core::CorePython::ObjID::kMainDict) + .Get(); + } + + if (!file_code_obj_.Get()) { CompileForExec(); assert(!dead_); } - if (file_code_obj_.get()) { + if (file_code_obj_.Get()) { PUSH_PYCOMMAND(this); - PyObject* v = PyEval_EvalCode(file_code_obj_.get(), g_python->main_dict(), - g_python->main_dict()); + PyObject* v = PyEval_EvalCode(file_code_obj_.Get(), globals, locals); POP_PYCOMMAND(); // Technically the Python call could have killed us; // make sure that didn't happen. assert(!dead_); - if (v == nullptr) { - // Save/restore error or it can mess with context print calls. - BA_PYTHON_ERROR_SAVE; - PySys_WriteStderr("Exception in Python call:\n"); - PrintContext(); - BA_PYTHON_ERROR_RESTORE; + // TODO(ericf): we shouldn't care. Should grab everything we might need + // for the rest of this call before doing that so it doesn't matter. - // We pass zero here to avoid grabbing references to this exception - // which can cause objects to stick around and trip up our deletion - // checks (nodes, actors existing after their games have ended). - PyErr_PrintEx(0); + if (v == nullptr) { + // Special case; when a SystemExit is thrown, we always just + // do a PyErr_Print. As per Python docs, that silently exits + // the process with the SystemExit's error code. + if (PyErr_ExceptionMatches(PyExc_SystemExit)) { + PyErr_Print(); + } + + if (print_errors) { + // Save/restore error or it can mess with context print calls. + BA_PYTHON_ERROR_SAVE; + PySys_WriteStderr("Exception in Python call:\n"); + PrintContext(); + BA_PYTHON_ERROR_RESTORE; + + // We pass zero here to avoid grabbing references to this exception + // which can cause objects to stick around and trip up our deletion + // checks (nodes, actors existing after their games have ended). + PyErr_PrintEx(0); + } PyErr_Clear(); } else { Py_DECREF(v); @@ -122,39 +162,37 @@ auto PythonCommand::Run() -> bool { return false; } -auto PythonCommand::CanEval() -> bool { +auto PythonCommand::Eval(bool print_errors, PyObject* globals, PyObject* locals) + -> PythonRef { assert(Python::HaveGIL()); - assert(g_python); - if (!eval_code_obj_.get()) { - CompileForEval(false); - } - if (!eval_code_obj_.get()) { - PyErr_Clear(); - return false; - } - PyErr_Clear(); - return true; -} - -auto PythonCommand::RunReturnObj(bool print_errors, PyObject* context) - -> PyObject* { - assert(Python::HaveGIL()); - assert(g_python); assert(!dead_); - if (context == nullptr) { - context = g_python->main_dict(); + + if (globals == nullptr) { + assert(core::g_core); + globals = core::g_core->python->objs() + .Get(core::CorePython::ObjID::kMainDict) + .Get(); + } + if (locals == nullptr) { + assert(core::g_core); + locals = core::g_core->python->objs() + .Get(core::CorePython::ObjID::kMainDict) + .Get(); } #pragma clang diagnostic push #pragma ide diagnostic ignored "RedundantCast" - assert(PyDict_Check(context)); + assert(PyDict_Check(globals)); + assert(PyDict_Check(locals)); #pragma clang diagnostic pop - if (!eval_code_obj_.get()) { + if (!eval_code_obj_.Get()) { CompileForEval(print_errors); assert(!dead_); } - if (!eval_code_obj_.get()) { + + // Attempt to compile for eval if necessary. + if (!eval_code_obj_.Get()) { if (print_errors) { // Save/restore error or it can mess with context print calls. BA_PYTHON_ERROR_SAVE; @@ -167,14 +205,14 @@ auto PythonCommand::RunReturnObj(bool print_errors, PyObject* context) PyErr_PrintEx(0); } - // Consider the python error handled at this point. + // Consider the Python error handled at this point. // If C++ land wants to throw an exception or whatnot based on this result, // that's a totally different thing. PyErr_Clear(); - return nullptr; + return {}; } PUSH_PYCOMMAND(this); - PyObject* v = PyEval_EvalCode(eval_code_obj_.get(), context, context); + PyObject* v = PyEval_EvalCode(eval_code_obj_.Get(), globals, locals); POP_PYCOMMAND(); assert(!dead_); if (v == nullptr) { @@ -190,19 +228,24 @@ auto PythonCommand::RunReturnObj(bool print_errors, PyObject* context) PyErr_PrintEx(0); } - // Consider the python error handled at this point. + // Consider the Python error handled at this point. // If C++ land wants to throw an exception or whatnot based on this result, // that's a totally different thing. PyErr_Clear(); - return nullptr; + return {}; } - return v; + return {v, PythonRef::kSteal}; } void PythonCommand::PrintContext() { assert(Python::HaveGIL()); - std::string s = std::string(" call: ") + command(); - s += g_python->GetContextBaseString(); + std::string s; + + // Add the call only if its a one-liner. + if (command().find('\n') == std::string::npos) { + s += std::string(" call: ") + command() + "\n"; + } + s += Python::GetContextBaseString(); PySys_WriteStderr("%s\n", s.c_str()); } diff --git a/src/ballistica/python/python_command.h b/src/ballistica/shared/python/python_command.h similarity index 71% rename from src/ballistica/python/python_command.h rename to src/ballistica/shared/python/python_command.h index 8926e2d5..71e200da 100644 --- a/src/ballistica/python/python_command.h +++ b/src/ballistica/shared/python/python_command.h @@ -1,18 +1,16 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_PYTHON_PYTHON_COMMAND_H_ -#define BALLISTICA_PYTHON_PYTHON_COMMAND_H_ +#ifndef BALLISTICA_SHARED_PYTHON_PYTHON_COMMAND_H_ +#define BALLISTICA_SHARED_PYTHON_PYTHON_COMMAND_H_ #include -#include "ballistica/ballistica.h" -#include "ballistica/python/python_ref.h" +#include "ballistica/shared/ballistica.h" +#include "ballistica/shared/python/python_ref.h" namespace ballistica { -// String based python commands. -// Does not save/restore context or anything; -// for that functionality use PythonContextCall; +// String based Python commands. // Note to self: originally I though I'd be using this in a lot of places, // so I added the ability to compile once and run repeatedly, quietly capture @@ -40,16 +38,16 @@ class PythonCommand { ~PythonCommand(); auto command() -> const std::string& { return command_; } - /// Run the command. - /// return true if the command was successfully run - /// (not to be confused with the command's result) - /// This works for non-eval-able commands. - auto Run() -> bool; + /// Run the command in exec mode. Return true if the command completed + /// successfully (not to be confused with the command's result). + /// This works for both eval-able and non-eval-able commands. + auto Exec(bool print_errors, PyObject* globals, PyObject* locals) -> bool; - /// Run thecommand and return the result as a new Python reference. + /// Run the command and return the result as a new Python reference. /// Only works for eval-able commands. /// Returns nullptr on errors, but Python error state will be cleared. - auto RunReturnObj(bool print_errors, PyObject* context) -> PyObject*; + auto Eval(bool print_errors, PyObject* globals, PyObject* locals) + -> PythonRef; void PrintContext(); @@ -70,4 +68,4 @@ class PythonCommand { } // namespace ballistica -#endif // BALLISTICA_PYTHON_PYTHON_COMMAND_H_ +#endif // BALLISTICA_SHARED_PYTHON_PYTHON_COMMAND_H_ diff --git a/src/ballistica/shared/python/python_module_builder.h b/src/ballistica/shared/python/python_module_builder.h new file mode 100644 index 00000000..edd95f12 --- /dev/null +++ b/src/ballistica/shared/python/python_module_builder.h @@ -0,0 +1,72 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SHARED_PYTHON_PYTHON_MODULE_BUILDER_H_ +#define BALLISTICA_SHARED_PYTHON_PYTHON_MODULE_BUILDER_H_ + +#include + +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" + +namespace ballistica { + +/// Utility class for defining together Python C modules. +/// To use, allocate an instance and call its Build() method. The instance +/// should remain allocated, as it is used as 'static' storage for the Python +/// type. +class PythonModuleBuilder { + public: + PythonModuleBuilder(const char* name, + const std::vector>& method_lists, + int (*exec_call)(PyObject* module)) + : name_{name}, + module_def_{PyModuleDef_HEAD_INIT}, + slots_{{Py_mod_exec, reinterpret_cast(exec_call)}, + {0, nullptr}} { + // Slight optimization; calc size we'll need for our combined list. + size_t total_size{}; + for (auto&& methods : method_lists) { + total_size += methods.size(); + } + all_methods_.reserve(total_size + 1); + + // Build our single combined method list. + for (auto&& methods : method_lists) { + all_methods_.insert(all_methods_.end(), methods.begin(), methods.end()); + } + // Cap the end. + all_methods_.push_back(PyMethodDef{nullptr, nullptr, 0, nullptr}); + } + + template + static auto AddClass(PyObject* module) -> PyObject* { + assert(!T::TypeIsSetUp(&T::type_obj)); + T::SetupType(&T::type_obj); + BA_PRECONDITION_FATAL(PyType_Ready(&T::type_obj) == 0); + assert(T::TypeIsSetUp(&T::type_obj)); + int r = PyModule_AddObjectRef(module, T::type_name(), + reinterpret_cast(&T::type_obj)); + BA_PRECONDITION_FATAL(r == 0); + return reinterpret_cast(&T::type_obj); + } + + auto Build() -> PyObject* { + assert(Python::HaveGIL()); + assert(module_def_.m_size == 0); // make sure things start zeroed.. + module_def_.m_methods = all_methods_.data(); + module_def_.m_slots = slots_.data(); + PyObject* module = PyModuleDef_Init(&module_def_); + BA_PRECONDITION_FATAL(module); + return module; + } + + private: + std::string name_; + PyModuleDef module_def_; + std::vector slots_; + std::vector all_methods_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_SHARED_PYTHON_PYTHON_MODULE_BUILDER_H_ diff --git a/src/ballistica/shared/python/python_object_set.cc b/src/ballistica/shared/python/python_object_set.cc new file mode 100644 index 00000000..ea723bbf --- /dev/null +++ b/src/ballistica/shared/python/python_object_set.cc @@ -0,0 +1,97 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/shared/python/python_object_set.h" + +#include + +#include "ballistica/core/support/base_soft.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python_command.h" +#include "ballistica/shared/python/python_sys.h" + +namespace ballistica { + +// Using core stuff implicitly here. Our behavior is undefined if core +// has not yet been imported by anyone. +using core::g_base_soft; + +PythonObjectSetBase::~PythonObjectSetBase() { + // We make assumptions that ids remain valid once established. + // Raise a fuss if that is ever not the case. + FatalError("PythonObjectSets are expected to live forever."); +} + +void PythonObjectSetBase::StoreObj(int id, PyObject* pyobj) { + BA_PRECONDITION(pyobj); + assert(id >= 0); + assert(id < static_cast(objs_.size())); + + if (g_buildconfig.debug_build()) { + // Assuming we're setting everything once + // (make sure we don't accidentally overwrite things we don't intend to). + if (objs_[id].Exists()) { + throw Exception("Python::StoreObj() called twice for id '" + + std::to_string(id) + "' (existing val: '" + + objs_[id].Str() + "')."); + } + + // Also make sure we're not storing an object that's already been stored. + for (auto&& i : objs_) { + if (i.Get() != nullptr && i.Get() == pyobj) { + Log(LogLevel::kWarning, + "Python::StoreObj() called twice for same ptr; id=" + + std::to_string(id) + "."); + } + } + } + + // Note: This used to be optional (and false by default) but now we always + // acquire a ref to what we're storing. We hold on to this stuff permanently + // so the worst thing that can happen is a harmless extra refcount increment + // if someone passes us a new ref; that's better than the opposite case where + // we are passed a borrowed ref and fail to keep it alive. + Py_INCREF(pyobj); + + objs_[static_cast(id)].Steal(pyobj); +} + +void PythonObjectSetBase::StoreObjCallable(int id, PyObject* pyobj) { + StoreObj(id, pyobj); + BA_PRECONDITION(Obj(id).CallableCheck()); +} + +void PythonObjectSetBase::StoreObj(int id, const char* expr, + PyObject* context) { + auto obj = PythonCommand(expr, "").Eval(false, context, context); + if (!obj.Exists()) { + FatalError("Unable to get value: '" + std::string(expr) + "'."); + } + StoreObj(id, obj.Get()); +} + +void PythonObjectSetBase::StoreObjCallable(int id, const char* expr, + PyObject* context) { + auto obj = PythonCommand(expr, "").Eval(false, context, context); + if (!obj.Exists()) { + throw Exception("Unable to get value: '" + std::string(expr) + "'."); + } + StoreObjCallable(id, obj.Get()); +} + +void PythonObjectSetBase::PushObjCall(int id) const { + // Easier to debug invalid objs here at the call site. + assert(ObjExists(id)); + + BA_PRECONDITION(g_base_soft); + g_base_soft->DoPushObjCall(this, id); +} + +void PythonObjectSetBase::PushObjCall(int id, const std::string& arg) const { + // Easier to debug invalid objs here at the call site. + assert(ObjExists(id)); + + BA_PRECONDITION(g_base_soft); + g_base_soft->DoPushObjCall(this, id, arg); +} + +} // namespace ballistica diff --git a/src/ballistica/shared/python/python_object_set.h b/src/ballistica/shared/python/python_object_set.h new file mode 100644 index 00000000..32075412 --- /dev/null +++ b/src/ballistica/shared/python/python_object_set.h @@ -0,0 +1,107 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_SHARED_PYTHON_PYTHON_OBJECT_SET_H_ +#define BALLISTICA_SHARED_PYTHON_PYTHON_OBJECT_SET_H_ + +#include +#include + +#include "ballistica/shared/python/python_ref.h" + +namespace ballistica { + +class PythonObjectSetBase { + public: + explicit PythonObjectSetBase(int objcount) : objs_(objcount) {} + ~PythonObjectSetBase(); + void StoreObj(int id, PyObject* pyobj); + void StoreObjCallable(int id, PyObject* pyobj); + void StoreObj(int id, const char* expression, PyObject* context = nullptr); + void StoreObjCallable(int id, const char* expression, + PyObject* context = nullptr); + + /// Access a particular Python object we've grabbed/stored. + auto Obj(int id) const -> const PythonRef& { + assert(id >= 0); + assert(id < static_cast(objs_.size())); + assert(objs_[id].Exists()); + return objs_[id]; + } + + /// Return whether we have a particular Python object. + auto ObjExists(int id) const -> bool { + assert(id >= 0); + assert(id < static_cast(objs_.size())); + return objs_[static_cast(id)].Exists(); + } + + /// Push a call to a preset obj to the logic thread. + void PushObjCall(int id) const; + + /// Push a call with a single string arg. + void PushObjCall(int id, const std::string& arg) const; + + private: + std::vector objs_; +}; + +/// A class to store and retrieve different Python objects based on enums. +/// Object values can be set manually, or a binding_FOO.py file can be used +/// to provide values in a way that integrates cleanly with Python +/// type-checking. +template +class PythonObjectSet : public PythonObjectSetBase { + public: + PythonObjectSet() : PythonObjectSetBase(static_cast(T::kLast)) {} + + /// Set the value for a named object. + void Store(T id, PyObject* pyobj) { StoreObj(static_cast(id), pyobj); } + + /// Set the value for a named object and verify that it is a callable. + /// This grabs a new reference to the passed PyObject. + void StoreCallable(T id, PyObject* pyobj) { + StoreObjCallable(static_cast(id), pyobj); + } + + /// Set the value for a named object to the result of a Python expression. + /// This grabs a new reference to the passed PyObject. + void Store(T id, const char* expression, PyObject* context = nullptr) { + StoreObj(static_cast(id), expression, context); + } + + /// Set the value for a named object to the result of a Python expression + /// and verify that it is callable. + /// This grabs a new reference to the passed PyObject. + void StoreCallable(T id, const char* expression, + PyObject* context = nullptr) { + StoreObjCallable(static_cast(id), expression, context); + } + + /// Access a particular Python object we've grabbed/stored. + auto Get(T id) const -> const PythonRef& { return Obj(static_cast(id)); } + + /// Return whether we have a particular Python object. + auto Exists(T id) const -> bool { return ObjExists(static_cast(id)); } + + // Note to self: future-me might wonder why we don't simply add PushCall() + // methods to PythonRef instead of here. The reason is that we would need to + // jump through more hoops to manage the gil and ref-counts in that case: + // grabbing the gil in the calling thread, incrementing the obj ref + // count so it doesn't die before being called, and then decrementing it + // later from the logic thread. By implementing push-calls at the set level, + // however, we can simply kick an id over to the logic thread since we + // know the gil will be held there and we know the obj will remain referenced + // by the set. + + /// Convenience function to push a call to an obj to the logic thread. + void PushCall(T id) const { PushObjCall(static_cast(id)); } + + /// Push a call with a single string arg. + void PushCall(T id, const std::string& arg) const { + PushObjCall(static_cast(id), arg); + } +}; + +} // namespace ballistica + +#endif // BALLISTICA_SHARED_PYTHON_PYTHON_OBJECT_SET_H_ diff --git a/src/ballistica/python/python_ref.cc b/src/ballistica/shared/python/python_ref.cc similarity index 56% rename from src/ballistica/python/python_ref.cc rename to src/ballistica/shared/python/python_ref.cc index 6121b029..37667d95 100644 --- a/src/ballistica/python/python_ref.cc +++ b/src/ballistica/shared/python/python_ref.cc @@ -1,13 +1,20 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/python/python_ref.h" +#include "ballistica/shared/python/python_ref.h" -#include "ballistica/math/vector2f.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_sys.h" +#include "ballistica/core/python/core_python.h" +#include "ballistica/core/support/base_soft.h" +#include "ballistica/shared/math/vector2f.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" namespace ballistica { +// Note: implicitly using core globals here; our behavior is undefined +// if core has not been imported by anyone yet. +using core::g_base_soft; +using core::g_core; + // Ignore a few things that python macros do. #pragma clang diagnostic push #pragma ide diagnostic ignored "RedundantCast" @@ -37,7 +44,6 @@ PythonRef::PythonRef(PyObject* obj_in, ReferenceBehavior b) { void PythonRef::Acquire(PyObject* obj_in) { BA_PRECONDITION(obj_in); - assert(g_python); assert(Python::HaveGIL()); // Assign and increment the new one before decrementing our old @@ -51,6 +57,14 @@ void PythonRef::Acquire(PyObject* obj_in) { } } +void PythonRef::AcquireSoft(PyObject* obj_in) { + if (!obj_in) { + Release(); + return; + } + Acquire(obj_in); +} + void PythonRef::Steal(PyObject* obj_in) { BA_PRECONDITION(obj_in); assert(Python::HaveGIL()); @@ -64,6 +78,14 @@ void PythonRef::Steal(PyObject* obj_in) { } } +void PythonRef::StealSoft(PyObject* obj_in) { + if (!obj_in) { + Release(); + return; + } + Steal(obj_in); +} + void PythonRef::Release() { assert(Python::HaveGIL()); @@ -75,69 +97,124 @@ void PythonRef::Release() { } } +auto PythonRef::FromString(const std::string& val) -> PythonRef { + return Stolen(PyUnicode_FromString(val.c_str())); +} + auto PythonRef::Str() const -> std::string { assert(Python::HaveGIL()); if (!obj_) { return ""; } - PyObject* obj = PyObject_Str(obj_); - if (!obj) { - return ""; + PyObject* str_obj = PyObject_Str(obj_); + if (!str_obj) { + PyErr_Clear(); + return ""; } - PythonRef s(obj, PythonRef::kSteal); - assert(PyUnicode_Check(obj)); // NOLINT (signed with bitwise) - return PyUnicode_AsUTF8(s.get()); + auto s = PythonRef::Stolen(str_obj); + assert(PyUnicode_Check(str_obj)); // NOLINT (signed with bitwise) + return PyUnicode_AsUTF8(s.Get()); } auto PythonRef::Repr() const -> std::string { assert(Python::HaveGIL()); - BA_PRECONDITION(obj_); - PythonRef s(PyObject_Repr(obj_), PythonRef::kSteal); - assert(PyUnicode_Check(s.get())); // NOLINT (signed with bitwise) - return PyUnicode_AsUTF8(s.get()); + ThrowIfUnset(); + auto s = PythonRef::Stolen(PyObject_Repr(obj_)); + assert(PyUnicode_Check(s.Get())); // NOLINT (signed with bitwise) + return PyUnicode_AsUTF8(s.Get()); +} + +auto PythonRef::Type() const -> PythonRef { + assert(Python::HaveGIL()); + ThrowIfUnset(); + return {PyObject_Type(obj_), PythonRef::kSteal}; +} + +auto PythonRef::ValueAsLString() const -> std::string { + assert(Python::HaveGIL()); + ThrowIfUnset(); + if (g_base_soft) { + return g_base_soft->GetPyLString(obj_); + } + throw Exception("Can't return as LString; _babase not imported."); } auto PythonRef::ValueAsString() const -> std::string { assert(Python::HaveGIL()); - BA_PRECONDITION(obj_); + ThrowIfUnset(); + return Python::GetPyString(obj_); +} + +void PythonRef::ThrowIfUnset() const { + if (!obj_) { + throw Exception("PythonRef is unset.", PyExcType::kValue); + } +} + +auto PythonRef::ValueAsOptionalString() const -> std::optional { + assert(Python::HaveGIL()); + ThrowIfUnset(); + if (obj_ == Py_None) { + return {}; + } return Python::GetPyString(obj_); } auto PythonRef::ValueAsInt() const -> int64_t { assert(Python::HaveGIL()); - BA_PRECONDITION(obj_); + ThrowIfUnset(); return Python::GetPyInt64(obj_); } auto PythonRef::GetAttr(const char* name) const -> PythonRef { assert(Python::HaveGIL()); - BA_PRECONDITION(obj_); - PyObject* val = PyObject_GetAttrString(get(), name); + ThrowIfUnset(); + PyObject* val = PyObject_GetAttrString(Get(), name); if (!val) { PyErr_Clear(); throw Exception("Attribute not found: '" + std::string(name) + "'.", PyExcType::kAttribute); } - return PythonRef(val, PythonRef::kSteal); + return {val, PythonRef::kSteal}; +} + +auto PythonRef::DictGetItem(const char* name) const -> PythonRef { + assert(Python::HaveGIL()); + ThrowIfUnset(); + PyObject* key = PyUnicode_FromString(name); + PyObject* out = PyDict_GetItemWithError(obj_, key); + Py_DECREF(key); + if (out) { + // 'out' is a borrowed ref. + return PythonRef::Acquired(out); + } + // Ok; we failed. If its because of an error, raise an exception. + if (PyErr_Occurred()) { + // Hmm; should we print the Python error here or translate it into our + // C++ exception str? + PyErr_Clear(); + throw Exception( + "PythonRef::DictGetItem() errored. Is your obj not a dict?"); + } + // Must be because dict key didn't exist. Return empty ref. + return {}; } auto PythonRef::NewRef() const -> PyObject* { assert(Python::HaveGIL()); - if (obj_ == nullptr) { - throw Exception("PythonRef::NewRef() called with nullptr obj_"); - } + ThrowIfUnset(); Py_INCREF(obj_); return obj_; } auto PythonRef::CallableCheck() const -> bool { - BA_PRECONDITION(obj_); assert(Python::HaveGIL()); + ThrowIfUnset(); return static_cast(PyCallable_Check(obj_)); } auto PythonRef::UnicodeCheck() const -> bool { - BA_PRECONDITION(obj_); + ThrowIfUnset(); assert(Python::HaveGIL()); return static_cast(PyUnicode_Check(obj_)); } @@ -170,7 +247,10 @@ auto PythonRef::Call(PyObject* args, PyObject* keywds, bool print_errors) const } auto PythonRef::Call() const -> PythonRef { - return Call(g_python->obj(Python::ObjID::kEmptyTuple).get()); + // NOTE: Using core globals directly here; normally don't do this. + assert(g_core); + return Call( + g_core->python->objs().Get(core::CorePython::ObjID::kEmptyTuple).Get()); } auto PythonRef::Call(const Vector2f& val) const -> PythonRef { diff --git a/src/ballistica/python/python_ref.h b/src/ballistica/shared/python/python_ref.h similarity index 58% rename from src/ballistica/python/python_ref.h rename to src/ballistica/shared/python/python_ref.h index 80eebbec..460e711f 100644 --- a/src/ballistica/python/python_ref.h +++ b/src/ballistica/shared/python/python_ref.h @@ -1,11 +1,12 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_PYTHON_PYTHON_REF_H_ -#define BALLISTICA_PYTHON_PYTHON_REF_H_ +#ifndef BALLISTICA_SHARED_PYTHON_PYTHON_REF_H_ +#define BALLISTICA_SHARED_PYTHON_PYTHON_REF_H_ +#include #include -#include "ballistica/ballistica.h" +#include "ballistica/shared/ballistica.h" namespace ballistica { @@ -32,20 +33,41 @@ class PythonRef { PythonRef() {} // NOLINT (using '= default' here errors) /// See ReferenceBehavior docs. - PythonRef(PyObject* other, ReferenceBehavior behavior); + PythonRef(PyObject* obj, ReferenceBehavior behavior); + + /// Shortcut to create a new PythonRef using ReferenceBehavior::kSteal. + static auto Stolen(PyObject* obj) -> PythonRef { + return PythonRef(obj, ReferenceBehavior::kSteal); + } + + static auto StolenSoft(PyObject* obj) -> PythonRef { + return PythonRef(obj, ReferenceBehavior::kStealSoft); + } + + /// Shortcut to create a new PythonRef using ReferenceBehavior::kAcquire. + static auto Acquired(PyObject* obj) -> PythonRef { + return PythonRef(obj, ReferenceBehavior::kAcquire); + } + + static auto AcquiredSoft(PyObject* obj) -> PythonRef { + return PythonRef(obj, ReferenceBehavior::kAcquireSoft); + } /// Copy constructor acquires a new reference (or sets as unreferenced) /// depending on other. PythonRef(const PythonRef& other) { *this = other; } virtual ~PythonRef(); + /// Shortcut to create a string object. + static auto FromString(const std::string& val) -> PythonRef; + /// Assignment from another PythonRef acquires a reference to the object /// referenced by other if there is one. If other has no reference, any /// reference of ours is cleared to match. auto operator=(const PythonRef& other) -> PythonRef& { assert(this != &other); // Shouldn't be self-assigning. - if (other.exists()) { - Acquire(other.get()); + if (other.Exists()) { + Acquire(other.Get()); } else { Release(); } @@ -56,7 +78,7 @@ class PythonRef { /// (so basically the 'is' keyword in Python). /// Note that two unreferenced PythonRefs will be equal. auto operator==(const PythonRef& other) const -> bool { - return (get() == other.get()); + return (Get() == other.Get()); } auto operator!=(const PythonRef& other) const -> bool { return !(*this == other); @@ -66,9 +88,16 @@ class PythonRef { /// nullptr is passed. void Acquire(PyObject* obj); + /// Acquire a new reference to the passed object. Sets to null reference + /// if nullptr is passed. + void AcquireSoft(PyObject* obj); + /// Steal the passed reference. Throws an Exception if nullptr is passed. void Steal(PyObject* obj); + /// Steal the passed reference. Sets to null reference if nullptr is passed. + void StealSoft(PyObject* obj); + /// Release the held reference (if one is held). void Release(); @@ -81,33 +110,56 @@ class PythonRef { } /// Return the underlying PyObject pointer. - auto get() const -> PyObject* { return obj_; } + auto Get() const -> PyObject* { return obj_; } + + /// Return the underlying PyObject pointer. Throws an Exception if not set. + auto operator*() const -> PyObject* { + if (!obj_) { + throw Exception("Dereferencing invalid PythonRef"); + } + return obj_; + }; /// Increment the ref-count for the underlying PyObject and return it as a /// pointer. auto NewRef() const -> PyObject*; /// Return whether we are pointing to a PyObject. - auto exists() const -> bool { return obj_ != nullptr; } + auto Exists() const -> bool { return obj_ != nullptr; } /// Return a ref to an attribute on our PyObject or throw an Exception. auto GetAttr(const char* name) const -> PythonRef; - /// The equivalent of calling python str() on the contained PyObject. + /// Return an item from a dict obj. Returns empty ref if nonexistent. + /// Throws Exception if an error occurs. + auto DictGetItem(const char* name) const -> PythonRef; + + /// The equivalent of calling Python str() on the contained PyObject. + /// Gracefully handles invalid refs. auto Str() const -> std::string; /// The equivalent of calling repr() on the contained PyObject. + /// Throws Exception on invalid refs. auto Repr() const -> std::string; - /// For unicode, string, and ba.Lstr types, returns a utf8 string. + /// Return the object's Python type object. + auto Type() const -> PythonRef; + + /// For string and babase.Lstr types, returns a utf8 string. /// Throws an exception for other types. + auto ValueAsLString() const -> std::string; + auto ValueAsString() const -> std::string; + auto ValueAsOptionalString() const -> std::optional; + auto ValueAsInt() const -> int64_t; /// Returns whether the underlying PyObject is callable. + /// Throws an exception if unset. auto CallableCheck() const -> bool; /// Return whether the underlying PyObject is unicode. + /// Throws an exception if unset. auto UnicodeCheck() const -> bool; /// Call the PyObject. On error, (optionally) prints errors and returns empty @@ -116,18 +168,18 @@ class PythonRef { bool print_errors = true) const -> PythonRef; auto Call(const PythonRef& args, const PythonRef& keywds = PythonRef(), bool print_errors = true) const -> PythonRef { - return Call(args.get(), keywds.get(), print_errors); + return Call(args.Get(), keywds.Get(), print_errors); } auto Call() const -> PythonRef; - /// Call with various args.. - auto Call(const Vector2f& val) const - -> PythonRef; // (val will be passed as tuple) + /// Call with Vector2f passed as a tuple. + auto Call(const Vector2f& val) const -> PythonRef; private: + void ThrowIfUnset() const; PyObject* obj_{}; }; } // namespace ballistica -#endif // BALLISTICA_PYTHON_PYTHON_REF_H_ +#endif // BALLISTICA_SHARED_PYTHON_PYTHON_REF_H_ diff --git a/src/ballistica/python/python_sys.h b/src/ballistica/shared/python/python_sys.h similarity index 87% rename from src/ballistica/python/python_sys.h rename to src/ballistica/shared/python/python_sys.h index 0e53b373..220a4726 100644 --- a/src/ballistica/python/python_sys.h +++ b/src/ballistica/shared/python/python_sys.h @@ -1,7 +1,7 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_PYTHON_PYTHON_SYS_H_ -#define BALLISTICA_PYTHON_PYTHON_SYS_H_ +#ifndef BALLISTICA_SHARED_PYTHON_PYTHON_SYS_H_ +#define BALLISTICA_SHARED_PYTHON_PYTHON_SYS_H_ // Any code that actually runs any Python logic should include this. // This header pulls in the actual Python includes and also defines some handy @@ -49,12 +49,16 @@ } \ catch (const Exception& e) { \ Python::SetPythonException(e); \ - Py_TYPE(self)->tp_free(reinterpret_cast(self)); \ + if (self) { \ + Py_TYPE(self)->tp_free(reinterpret_cast(self)); \ + } \ return nullptr; \ } \ catch (const std::exception& e) { \ PyErr_SetString(PyExc_RuntimeError, GetShortExceptionDescription(e)); \ - Py_TYPE(self)->tp_free(reinterpret_cast(self)); \ + if (self) { \ + Py_TYPE(self)->tp_free(reinterpret_cast(self)); \ + } \ return nullptr; \ } \ ((void)0) @@ -81,4 +85,4 @@ } \ ((void)0) -#endif // BALLISTICA_PYTHON_PYTHON_SYS_H_ +#endif // BALLISTICA_SHARED_PYTHON_PYTHON_SYS_H_ diff --git a/src/ballistica/template_fs/README.md b/src/ballistica/template_fs/README.md new file mode 100644 index 00000000..36701061 --- /dev/null +++ b/src/ballistica/template_fs/README.md @@ -0,0 +1,3 @@ +# Template Feature Set + +This is an empty feature-set for use a starting point when implementing new ones. diff --git a/src/ballistica/template_fs/python/class/python_class_hello.cc b/src/ballistica/template_fs/python/class/python_class_hello.cc new file mode 100644 index 00000000..c8c76b74 --- /dev/null +++ b/src/ballistica/template_fs/python/class/python_class_hello.cc @@ -0,0 +1,64 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/template_fs/python/class/python_class_hello.h" + +namespace ballistica::template_fs { + +auto PythonClassHello::type_name() -> const char* { return "Hello"; } + +void PythonClassHello::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "_batemplatefs.Hello"; + cls->tp_basicsize = sizeof(PythonClassHello); + cls->tp_doc = "Simple example."; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_methods = tp_methods; +} + +auto PythonClassHello::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = type->tp_alloc(type, 0); + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + // Being a bit fancy here and using placement-new on top of the + // python-allocated memory. This lets C++-y parts of our class such as + // constructors/destructors and embedded objects work as expected. + new (self) PythonClassHello(); + return self; + BA_PYTHON_NEW_CATCH; +} + +void PythonClassHello::tp_dealloc(PythonClassHello* self) { + BA_PYTHON_TRY; + + // Because we used placement-new we need to manually run the equivalent + // destructor to balance things. Note that if anything goes wrong here it'll + // simply print an error; we don't set any Python error state. Not sure if + // that is ever even allowed from destructors anyway. + + // IMPORTANT: With Python objects we can't guarantee that this destructor runs + // in a particular thread; if our object contains anything that must be + // destructed in a particular thread then we should manually allocate & + // deallocate things so we can ship it off to the proper thread for cleanup as + // needed. + self->~PythonClassHello(); + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +PythonClassHello::PythonClassHello() { + Log(LogLevel::kInfo, "Hello from PythonClassHello constructor!!!"); +} + +PythonClassHello::~PythonClassHello() { + Log(LogLevel::kInfo, "Goodbye from PythonClassHello destructor!!!"); +} + +PyTypeObject PythonClassHello::type_obj; +PyMethodDef PythonClassHello::tp_methods[] = {{nullptr}}; + +} // namespace ballistica::template_fs diff --git a/src/ballistica/template_fs/python/class/python_class_hello.h b/src/ballistica/template_fs/python/class/python_class_hello.h new file mode 100644 index 00000000..e72b6d63 --- /dev/null +++ b/src/ballistica/template_fs/python/class/python_class_hello.h @@ -0,0 +1,45 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_TEMPLATE_FS_PYTHON_CLASS_PYTHON_CLASS_HELLO_H_ +#define BALLISTICA_TEMPLATE_FS_PYTHON_CLASS_PYTHON_CLASS_HELLO_H_ + +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::template_fs { + +/// A simple example native class. +class PythonClassHello : public PythonClass { + public: + static void SetupType(PyTypeObject* cls); + static auto type_name() -> const char*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + + /// Cast raw Python pointer to our type; throws an exception on wrong types. + static auto FromPyObj(PyObject* o) -> PythonClassHello& { + if (Check(o)) { + return *reinterpret_cast(o); + } + throw Exception(std::string("Expected a ") + type_name() + "; got a " + + Python::ObjTypeToString(o), + PyExcType::kType); + } + + static PyTypeObject type_obj; + + private: + PythonClassHello(); + ~PythonClassHello(); + static PyMethodDef tp_methods[]; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassHello* self); + static auto Play(PythonClassHello* self, PyObject* args, PyObject* keywds) + -> PyObject*; +}; + +} // namespace ballistica::template_fs + +#endif // BALLISTICA_TEMPLATE_FS_PYTHON_CLASS_PYTHON_CLASS_HELLO_H_ diff --git a/src/ballistica/template_fs/python/methods/python_methods_template_fs.cc b/src/ballistica/template_fs/python/methods/python_methods_template_fs.cc new file mode 100644 index 00000000..2e0015ad --- /dev/null +++ b/src/ballistica/template_fs/python/methods/python_methods_template_fs.cc @@ -0,0 +1,42 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/template_fs/python/methods/python_methods_template_fs.h" + +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" + +namespace ballistica::template_fs { + +// -------------------------- hello_again_world -------------------------------- + +static auto PyHelloAgainWorld(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", const_cast(kwlist), + &name)) { + return nullptr; + } + Log(LogLevel::kInfo, "HELLO AGAIN WORLD!"); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyHelloAgainWorldDef = { + "hello_again_world", // name + (PyCFunction)PyHelloAgainWorld, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "hello_again_world() -> None\n" + "\n" + "Another hello world print.", +}; + +// ----------------------------------------------------------------------------- + +auto PythonMethodsTemplateFs::GetMethods() -> std::vector { + return {PyHelloAgainWorldDef}; +} + +} // namespace ballistica::template_fs diff --git a/src/ballistica/template_fs/python/methods/python_methods_template_fs.h b/src/ballistica/template_fs/python/methods/python_methods_template_fs.h new file mode 100644 index 00000000..237defeb --- /dev/null +++ b/src/ballistica/template_fs/python/methods/python_methods_template_fs.h @@ -0,0 +1,19 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_TEMPLATE_FS_PYTHON_METHODS_PYTHON_METHODS_TEMPLATE_FS_H_ +#define BALLISTICA_TEMPLATE_FS_PYTHON_METHODS_PYTHON_METHODS_TEMPLATE_FS_H_ + +#include + +#include "ballistica/template_fs/template_fs.h" + +namespace ballistica::template_fs { + +class PythonMethodsTemplateFs { + public: + static auto GetMethods() -> std::vector; +}; + +} // namespace ballistica::template_fs + +#endif // BALLISTICA_TEMPLATE_FS_PYTHON_METHODS_PYTHON_METHODS_TEMPLATE_FS_H_ diff --git a/src/ballistica/template_fs/python/template_fs_python.cc b/src/ballistica/template_fs/python/template_fs_python.cc new file mode 100644 index 00000000..bd5b2b13 --- /dev/null +++ b/src/ballistica/template_fs/python/template_fs_python.cc @@ -0,0 +1,52 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/template_fs/python/template_fs_python.h" + +#include "ballistica/shared/python/python_command.h" +#include "ballistica/shared/python/python_module_builder.h" +#include "ballistica/template_fs/python/class/python_class_hello.h" +#include "ballistica/template_fs/python/methods/python_methods_template_fs.h" + +namespace ballistica::template_fs { + +// Declare a plain c PyInit_XXX function for our Python module; +// this is how Python inits our binary module (and by extension, our +// entire feature-set). +extern "C" auto PyInit__batemplatefs() -> PyObject* { + auto* builder = new PythonModuleBuilder( + "_batemplatefs", + // Native methods to add. + {PythonMethodsTemplateFs::GetMethods()}, + // Our module exec. Here we can add classes, import other modules, + // or whatever else (same as a regular Python script module). + [](PyObject* module) -> int { + BA_PYTHON_TRY; + TemplateFsFeatureSet::OnModuleExec(module); + return 0; + BA_PYTHON_INT_CATCH; + }); + return builder->Build(); +} + +void TemplateFsPython::AddPythonClasses(PyObject* module) { + PythonModuleBuilder::AddClass(module); +} + +void TemplateFsPython::ImportPythonObjs() { +#include "ballistica/template_fs/mgen/pyembed/binding_template_fs.inc" +} + +void TemplateFsPython::HelloWorld() { + // Hold the GIL throughout this call so we can run in any thread. + // Alternately we could limit this function to the logic thread + // which always holds the GIL. In that case we'd want to + // stick a BA_PRECONDITION(InLogicThread()) here to be sure. + auto gil{Python::ScopedInterpreterLock()}; + + // Run the Python callable we grabbed. This will simply print any + // errors, but we could disable that print and look at the call + // results if any logic depended on this code running successfully. + objs_.Get(ObjID::kHelloWorldCall).Call(); +} + +} // namespace ballistica::template_fs diff --git a/src/ballistica/template_fs/python/template_fs_python.h b/src/ballistica/template_fs/python/template_fs_python.h new file mode 100644 index 00000000..78ecd356 --- /dev/null +++ b/src/ballistica/template_fs/python/template_fs_python.h @@ -0,0 +1,33 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_TEMPLATE_FS_PYTHON_TEMPLATE_FS_PYTHON_H_ +#define BALLISTICA_TEMPLATE_FS_PYTHON_TEMPLATE_FS_PYTHON_H_ + +#include "ballistica/shared/python/python_object_set.h" +#include "ballistica/template_fs/template_fs.h" + +namespace ballistica::template_fs { + +/// General Python support class for our feature-set. +class TemplateFsPython { + public: + /// Call our hello-world call we grabbed from Python. + void HelloWorld(); + + /// Specific Python objects we hold in objs_. + enum class ObjID { + kHelloWorldCall, + kLast // Sentinel; must be at end. + }; + + void AddPythonClasses(PyObject* module); + void ImportPythonObjs(); + const auto& objs() { return objs_; } + + private: + PythonObjectSet objs_; +}; + +} // namespace ballistica::template_fs + +#endif // BALLISTICA_TEMPLATE_FS_PYTHON_TEMPLATE_FS_PYTHON_H_ diff --git a/src/ballistica/template_fs/template_fs.cc b/src/ballistica/template_fs/template_fs.cc new file mode 100644 index 00000000..b0ac2758 --- /dev/null +++ b/src/ballistica/template_fs/template_fs.cc @@ -0,0 +1,55 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/template_fs/template_fs.h" + +#include "ballistica/base/base.h" +#include "ballistica/template_fs/python/template_fs_python.h" + +namespace ballistica::template_fs { + +TemplateFsFeatureSet* g_template_fs{}; +base::BaseFeatureSet* g_base{}; +core::CoreFeatureSet* g_core{}; + +void TemplateFsFeatureSet::OnModuleExec(PyObject* module) { + // Ok, our feature-set's Python module is getting imported. + // Like any normal Python module, we take this opportunity to + // import and/or create the stuff we use. + + // Importing core should always be the first thing we do. + // Various ballistica functionality will fail if this has not been done. + g_core = core::CoreFeatureSet::Import(); + + // Create our feature-set's C++ front-end. + g_template_fs = new TemplateFsFeatureSet(); + g_template_fs->python->AddPythonClasses(module); + + // Store our C++ front-end on our Python module. + // This lets anyone get at us by going through the Python + // import system (keeping things nice and consistent between + // Python and C++ worlds). + g_template_fs->StoreOnPythonModule(module); + + // Import any Python stuff we use into objs_. + g_template_fs->python->ImportPythonObjs(); + + // Import any other C++ feature-set-front-ends we use. + assert(g_base == nullptr); // Should be getting set once here. + g_base = base::BaseFeatureSet::Import(); +} + +TemplateFsFeatureSet::TemplateFsFeatureSet() : python{new TemplateFsPython()} { + // We're a singleton. If there's already one of us, something's wrong. + assert(g_template_fs == nullptr); +} + +auto TemplateFsFeatureSet::Import() -> TemplateFsFeatureSet* { + // Since we provide a native Python module, we piggyback our C++ front-end + // on top of that. This way our C++ and Python dependencies are resolved + // consistently no matter which side we are imported from. + return ImportThroughPythonModule("_batemplatefs"); +} + +void TemplateFsFeatureSet::HelloWorld() const { python->HelloWorld(); } + +} // namespace ballistica::template_fs diff --git a/src/ballistica/template_fs/template_fs.h b/src/ballistica/template_fs/template_fs.h new file mode 100644 index 00000000..5c975b3a --- /dev/null +++ b/src/ballistica/template_fs/template_fs.h @@ -0,0 +1,59 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_TEMPLATE_FS_TEMPLATE_FS_H_ +#define BALLISTICA_TEMPLATE_FS_TEMPLATE_FS_H_ + +#include "ballistica/shared/foundation/feature_set_front_end.h" + +// Common header that most everything using our feature-set should include. +// It predeclares our feature-set's various types and globals and other +// bits. + +// Predeclared types from other feature sets that we use. +namespace ballistica::core { +class CoreFeatureSet; +} +namespace ballistica::base { +class BaseFeatureSet; +} + +// Feature-sets have their own unique namespace under the ballistica namespace. +namespace ballistica::template_fs { + +// Predeclared types our feature-set provides. +class TemplateFsFeatureSet; +class TemplateFsPython; + +// Our feature-set's globals. +// Feature-sets should NEVER directly access globals in another feature-set's +// namespace. All functionality we need from other feature-sets should be +// imported into globals in our own namespace. Generally we do this when we +// are initially imported (just as regular Python modules do). +extern core::CoreFeatureSet* g_core; +extern base::BaseFeatureSet* g_base; +extern TemplateFsFeatureSet* g_template_fs; + +/// Our C++ front-end to our feature set. This is what other C++ +/// feature-sets can 'Import' from us. +class TemplateFsFeatureSet : public FeatureSetFrontEnd { + public: + /// Instantiate our FeatureSet if needed and return the single + /// instance of it. Basically a Python import statement. + static auto Import() -> TemplateFsFeatureSet*; + + /// Called when our binary Python module first gets imported. + static void OnModuleExec(PyObject* module); + + /// Ye olde hello world test. + void HelloWorld() const; + + // Our sub-components. + TemplateFsPython* const python; + + private: + TemplateFsFeatureSet(); +}; + +} // namespace ballistica::template_fs + +#endif // BALLISTICA_TEMPLATE_FS_TEMPLATE_FS_H_ diff --git a/src/ballistica/ui/ui.h b/src/ballistica/ui/ui.h deleted file mode 100644 index e1af0ed4..00000000 --- a/src/ballistica/ui/ui.h +++ /dev/null @@ -1,165 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_UI_UI_H_ -#define BALLISTICA_UI_UI_H_ - -#include -#include - -#include "ballistica/core/context.h" -#include "ballistica/generic/timer_list.h" -#include "ballistica/ui/widget/widget.h" - -// UI-Locks: make sure widget-lists don't change under you. -// Use a read-lock if you just need to ensure lists remain intact but won't be -// changing anything. Use a write-lock whenever modifying a list. -#if BA_DEBUG_BUILD -#define BA_DEBUG_UI_READ_LOCK UI::UILock ui_lock(false) -#define BA_DEBUG_UI_WRITE_LOCK UI::UILock ui_lock(true) -#else -#define BA_DEBUG_UI_READ_LOCK -#define BA_DEBUG_UI_WRITE_LOCK -#endif -#define BA_UI_READ_LOCK UI::UILock ui_lock(false) -#define BA_UI_WRITE_LOCK UI::UILock ui_lock(true) - -namespace ballistica { - -// Note: this stuff is generally intended to be used solely from -// the logic thread. - -class UI : public ContextTarget { - public: - UI(); - auto OnAppStart() -> void; - ~UI() override; - auto Reset() -> void; - - // Return the root widget containing all windows & dialogs - // Whenever this contains children, the UI is considered to be in focus - auto screen_root_widget() -> ContainerWidget* { - return screen_root_widget_.get(); - } - - auto overlay_root_widget() -> ContainerWidget* { - return overlay_root_widget_.get(); - } - - /// Return whether there is UI present in either the main or overlay - /// stacks. Generally this implies the focus should be on the UI. - auto IsWindowPresent() const -> bool; - - // Return the absolute root widget; this includes persistent UI - // bits such as the top/bottom bars - auto root_widget() -> RootWidget* { return root_widget_.get(); } - - auto Draw(FrameDef* frame_def) -> void; - - // Returns the widget an input should send commands to, if any. - // Also potentially locks other inputs out of controlling the UI, - // so only call this if you intend on sending a message to that widget. - auto GetWidgetForInput(InputDevice* input_device) -> Widget*; - - // Add a widget to a container. - // If a parent is provided, the widget is added to it; otherwise it is added - // to the root widget. - auto AddWidget(Widget* w, ContainerWidget* to) -> void; - - // Send message to the active widget. - auto SendWidgetMessage(const WidgetMessage& msg) -> int; - - // Use this to destroy any named widget (even those in containers). - auto DeleteWidget(Widget* widget) -> void; - - auto ScreenSizeChanged() -> void; - - auto SetUIInputDevice(InputDevice* input_device) -> void; - - // Returns the input-device that currently owns the menu; otherwise nullptr. - auto GetUIInputDevice() const -> InputDevice*; - - auto PushBackButtonCall(InputDevice* input_device) -> void; - - // Returns whether currently selected widgets should flash. - // This will be false in some situations such as when only touch screen - // control is active. - auto ShouldHighlightWidgets() const -> bool; - - // Same except for button shortcuts; these generally only get shown - // if a joystick of some form is present. - auto ShouldShowButtonShortcuts() const -> bool; - - // void DrawExtras(FrameDef* frame_def); - - // Used to ensure widgets are not created or destroyed at certain times - // (while traversing widget hierarchy, etc). - class UILock { - public: - explicit UILock(bool write); - ~UILock(); - - private: - BA_DISALLOW_CLASS_COPIES(UILock); - }; - - auto GetSound(const std::string& name) -> Object::Ref override; - auto GetData(const std::string& name) -> Object::Ref override; - auto GetModel(const std::string& name) -> Object::Ref override; - auto GetTexture(const std::string& name) -> Object::Ref override; - auto GetAsUIContext() -> UI* override; - auto scene() -> Scene* { - assert(scene_.exists()); - return scene_.get(); - } - auto Update(millisecs_t time_advance) -> void; - auto GetMutableScene() -> Scene* override; - - // Context-target timer support. - auto NewTimer(TimeType timetype, TimerMedium length, bool repeat, - const Object::Ref& runnable) -> int override; - auto DeleteTimer(TimeType timetype, int timer_id) -> void override; - - auto root_ui() -> RootUI* const { - assert(root_ui_); - return root_ui_; - } - - auto scale() const { return scale_; } - - /// Push a generic 'menu press' event, optionally associated with an - /// input device (nullptr to specify none). Note: caller must ensure - /// a RemoveInputDevice() call does not arrive at the logic thread - /// before this one. - auto PushMainMenuPressCall(InputDevice* device) -> void; - - private: - auto MainMenuPress(InputDevice* device) -> void; - auto StepScene() -> void; - RootUI* root_ui_{}; - millisecs_t next_prune_time_{}; - int node_warning_count_{}; - Timer* step_scene_timer_{}; - millisecs_t base_time_{}; - TimerList sim_timers_; - TimerList base_timers_; - Object::Ref scene_; - Object::WeakRef ui_input_device_; - millisecs_t last_input_device_use_time_{}; - millisecs_t last_widget_input_reject_err_sound_time_{}; - Object::Ref screen_root_widget_; - Object::Ref overlay_root_widget_; - Object::Ref root_widget_; - int ui_lock_count_{}; - UIScale scale_{UIScale::kLarge}; - bool force_scale_{}; - - // Media loaded in the UI context. - std::unordered_map > textures_; - std::unordered_map > sounds_; - std::unordered_map > datas_; - std::unordered_map > models_; -}; - -} // namespace ballistica - -#endif // BALLISTICA_UI_UI_H_ diff --git a/src/ballistica/ui/widget/row_widget.h b/src/ballistica/ui/widget/row_widget.h deleted file mode 100644 index 583dd81c..00000000 --- a/src/ballistica/ui/widget/row_widget.h +++ /dev/null @@ -1,26 +0,0 @@ -// Released under the MIT License. See LICENSE for details. - -#ifndef BALLISTICA_UI_WIDGET_ROW_WIDGET_H_ -#define BALLISTICA_UI_WIDGET_ROW_WIDGET_H_ - -#include - -#include "ballistica/ui/widget/container_widget.h" - -namespace ballistica { - -// Layout widget for organizing widgets in a row -class RowWidget : public ContainerWidget { - public: - RowWidget(); - ~RowWidget() override; - auto HandleMessage(const WidgetMessage& m) -> bool override; - auto GetWidgetTypeName() -> std::string override; - - protected: - void UpdateLayout() override; -}; - -} // namespace ballistica - -#endif // BALLISTICA_UI_WIDGET_ROW_WIDGET_H_ diff --git a/src/ballistica/ui_v1/python/class/python_class_ui_mesh.cc b/src/ballistica/ui_v1/python/class/python_class_ui_mesh.cc new file mode 100644 index 00000000..da5d6d24 --- /dev/null +++ b/src/ballistica/ui_v1/python/class/python_class_ui_mesh.cc @@ -0,0 +1,84 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui_v1/python/class/python_class_ui_mesh.h" + +#include "ballistica/base/assets/mesh_asset.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/ui_v1/ui_v1.h" + +namespace ballistica::ui_v1 { + +auto PythonClassUIMesh::type_name() -> const char* { return "Mesh"; } + +void PythonClassUIMesh::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "babase.Mesh"; + cls->tp_basicsize = sizeof(PythonClassUIMesh); + cls->tp_doc = + "Mesh asset for local user interface purposes.\n" + "\n" + "Category: **User Interface Classes**"; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_methods = tp_methods; +} + +auto PythonClassUIMesh::Create(const Object::Ref& mesh) + -> PyObject* { + assert(TypeIsSetUp(&type_obj)); + auto* py_mesh = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + if (!py_mesh) { + throw Exception("Mesh creation failed"); + } + + *py_mesh->mesh_ = mesh; + return reinterpret_cast(py_mesh); +} + +auto PythonClassUIMesh::tp_repr(PythonClassUIMesh* self) -> PyObject* { + BA_PYTHON_TRY; + base::MeshAsset* s = self->mesh_->Get(); + return Py_BuildValue( + "s", (std::string("GetName()) + "'>").c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassUIMesh::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + self->mesh_ = new Object::Ref(); + return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; +} + +void PythonClassUIMesh::tp_dealloc(PythonClassUIMesh* self) { + BA_PYTHON_TRY; + // Our Object::Ref needs to be cleared in the logic thread. + auto* ptr = self->mesh_; + if (g_base->InLogicThread()) { + delete ptr; + } else { + g_base->logic->event_loop()->PushCall([ptr] { delete ptr; }); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +PyTypeObject PythonClassUIMesh::type_obj; +PyMethodDef PythonClassUIMesh::tp_methods[] = {{nullptr}}; + +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui_v1/python/class/python_class_ui_mesh.h b/src/ballistica/ui_v1/python/class/python_class_ui_mesh.h new file mode 100644 index 00000000..09edebc9 --- /dev/null +++ b/src/ballistica/ui_v1/python/class/python_class_ui_mesh.h @@ -0,0 +1,50 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_V1_PYTHON_CLASS_PYTHON_CLASS_UI_MESH_H_ +#define BALLISTICA_UI_V1_PYTHON_CLASS_PYTHON_CLASS_UI_MESH_H_ + +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::ui_v1 { + +class PythonClassUIMesh : public PythonClass { + public: + static void SetupType(PyTypeObject* cls); + static auto type_name() -> const char*; + static auto Create(const Object::Ref& mesh) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + + /// Cast raw Python pointer to our type; throws an exception on wrong types. + static auto FromPyObj(PyObject* o) -> PythonClassUIMesh& { + if (Check(o)) { + return *reinterpret_cast(o); + } + throw Exception(std::string("Expected a ") + type_name() + "; got a " + + Python::ObjTypeToString(o), + PyExcType::kType); + } + + auto mesh() const -> base::MeshAsset& { + assert(mesh_); + return **mesh_; + } + + static PyTypeObject type_obj; + + private: + static PyMethodDef tp_methods[]; + static auto tp_repr(PythonClassUIMesh* self) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassUIMesh* self); + Object::Ref* mesh_; +}; + +} // namespace ballistica::ui_v1 + +#endif // BALLISTICA_UI_V1_PYTHON_CLASS_PYTHON_CLASS_UI_MESH_H_ diff --git a/src/ballistica/ui_v1/python/class/python_class_ui_sound.cc b/src/ballistica/ui_v1/python/class/python_class_ui_sound.cc new file mode 100644 index 00000000..eb0e4ccf --- /dev/null +++ b/src/ballistica/ui_v1/python/class/python_class_ui_sound.cc @@ -0,0 +1,138 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui_v1/python/class/python_class_ui_sound.h" + +#include "ballistica/base/assets/sound_asset.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/ui_v1/ui_v1.h" + +namespace ballistica::ui_v1 { + +auto PythonClassUISound::type_name() -> const char* { return "Sound"; } + +void PythonClassUISound::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "babase.Sound"; + cls->tp_basicsize = sizeof(PythonClassUISound); + cls->tp_doc = + "Sound asset for local user interface purposes.\n" + "\n" + "Category: **User Interface Classes**"; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_methods = tp_methods; +} + +auto PythonClassUISound::Create(const Object::Ref& sound) + -> PyObject* { + assert(TypeIsSetUp(&type_obj)); + auto* py_sound = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + if (!py_sound) { + throw Exception("Sound creation failed"); + } + + *py_sound->sound_ = sound; + return reinterpret_cast(py_sound); +} + +auto PythonClassUISound::tp_repr(PythonClassUISound* self) -> PyObject* { + BA_PYTHON_TRY; + base::SoundAsset* s = self->sound_->Get(); + return Py_BuildValue( + "s", (std::string("GetName()) + "'>").c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassUISound::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + self->sound_ = new Object::Ref(); + return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; +} + +void PythonClassUISound::tp_dealloc(PythonClassUISound* self) { + BA_PYTHON_TRY; + // Our Object::Ref needs to be cleared in the logic thread. + auto* ptr = self->sound_; + if (g_base->InLogicThread()) { + delete ptr; + } else { + g_base->logic->event_loop()->PushCall([ptr] { delete ptr; }); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +auto PythonClassUISound::Play(PythonClassUISound* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + float volume{1.0f}; + static const char* kwlist[] = {"volume", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|f", + const_cast(kwlist), &volume)) { + return nullptr; + } + base::SoundAsset* s = self->sound_->Get(); + auto play_id = g_base->audio->PlaySound(s, volume); + if (play_id) { + self->playing_ = true; + self->play_id_ = *play_id; + } else { + self->playing_ = false; + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassUISound::Stop(PythonClassUISound* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + if (self->playing_) { + g_base->audio->PushSourceStopSoundCall(self->play_id_); + self->playing_ = false; + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +PyTypeObject PythonClassUISound::type_obj; +PyMethodDef PythonClassUISound::tp_methods[] = { + {"play", (PyCFunction)PythonClassUISound::Play, + METH_VARARGS | METH_KEYWORDS, + "play() -> None\n" + "\n" + "Play the sound locally.\n" + ""}, + {"stop", (PyCFunction)PythonClassUISound::Stop, + METH_VARARGS | METH_KEYWORDS, + "stop() -> None\n" + "\n" + "Stop the sound if it is playing.\n" + ""}, + + {nullptr}}; + +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui_v1/python/class/python_class_ui_sound.h b/src/ballistica/ui_v1/python/class/python_class_ui_sound.h new file mode 100644 index 00000000..50aefb51 --- /dev/null +++ b/src/ballistica/ui_v1/python/class/python_class_ui_sound.h @@ -0,0 +1,57 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_V1_PYTHON_CLASS_PYTHON_CLASS_UI_SOUND_H_ +#define BALLISTICA_UI_V1_PYTHON_CLASS_PYTHON_CLASS_UI_SOUND_H_ + +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::ui_v1 { + +class PythonClassUISound : public PythonClass { + public: + static void SetupType(PyTypeObject* cls); + static auto type_name() -> const char*; + static auto Create(const Object::Ref& sound) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + + /// Cast raw Python pointer to our type; throws an exception on wrong types. + static auto FromPyObj(PyObject* o) -> PythonClassUISound& { + if (Check(o)) { + return *reinterpret_cast(o); + } + throw Exception(std::string("Expected a ") + type_name() + "; got a " + + Python::ObjTypeToString(o), + PyExcType::kType); + } + + auto sound() const -> base::SoundAsset& { + assert(sound_); + return **sound_; + } + + static PyTypeObject type_obj; + + private: + static PyMethodDef tp_methods[]; + static auto tp_repr(PythonClassUISound* self) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassUISound* self); + static auto Play(PythonClassUISound* self, PyObject* args, PyObject* keywds) + -> PyObject*; + static auto Stop(PythonClassUISound* self, PyObject* args, PyObject* keywds) + -> PyObject*; + + Object::Ref* sound_; + bool playing_; + uint32_t play_id_; +}; + +} // namespace ballistica::ui_v1 + +#endif // BALLISTICA_UI_V1_PYTHON_CLASS_PYTHON_CLASS_UI_SOUND_H_ diff --git a/src/ballistica/ui_v1/python/class/python_class_ui_texture.cc b/src/ballistica/ui_v1/python/class/python_class_ui_texture.cc new file mode 100644 index 00000000..7697a97f --- /dev/null +++ b/src/ballistica/ui_v1/python/class/python_class_ui_texture.cc @@ -0,0 +1,84 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui_v1/python/class/python_class_ui_texture.h" + +#include "ballistica/base/assets/texture_asset.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/ui_v1/ui_v1.h" + +namespace ballistica::ui_v1 { + +auto PythonClassUITexture::type_name() -> const char* { return "Texture"; } + +void PythonClassUITexture::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "babase.Texture"; + cls->tp_basicsize = sizeof(PythonClassUITexture); + cls->tp_doc = + "Texture asset for local user interface purposes.\n" + "\n" + "Category: **User Interface Classes**"; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_methods = tp_methods; +} + +auto PythonClassUITexture::Create( + const Object::Ref& texture) -> PyObject* { + assert(TypeIsSetUp(&type_obj)); + auto* py_texture = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + if (!py_texture) { + throw Exception("Texture creation failed"); + } + + *py_texture->texture_ = texture; + return reinterpret_cast(py_texture); +} + +auto PythonClassUITexture::tp_repr(PythonClassUITexture* self) -> PyObject* { + BA_PYTHON_TRY; + base::TextureAsset* s = self->texture_->Get(); + return Py_BuildValue( + "s", (std::string("GetName()) + "'>").c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassUITexture::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (!self) { + return nullptr; + } + BA_PYTHON_TRY; + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + self->texture_ = new Object::Ref(); + return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; +} + +void PythonClassUITexture::tp_dealloc(PythonClassUITexture* self) { + BA_PYTHON_TRY; + // Our Object::Ref needs to be cleared in the logic thread. + auto* ptr = self->texture_; + if (g_base->InLogicThread()) { + delete ptr; + } else { + g_base->logic->event_loop()->PushCall([ptr] { delete ptr; }); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +PyTypeObject PythonClassUITexture::type_obj; +PyMethodDef PythonClassUITexture::tp_methods[] = {{nullptr}}; + +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui_v1/python/class/python_class_ui_texture.h b/src/ballistica/ui_v1/python/class/python_class_ui_texture.h new file mode 100644 index 00000000..56e720b6 --- /dev/null +++ b/src/ballistica/ui_v1/python/class/python_class_ui_texture.h @@ -0,0 +1,51 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_V1_PYTHON_CLASS_PYTHON_CLASS_UI_TEXTURE_H_ +#define BALLISTICA_UI_V1_PYTHON_CLASS_PYTHON_CLASS_UI_TEXTURE_H_ + +#include "ballistica/base/base.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_class.h" + +namespace ballistica::ui_v1 { + +class PythonClassUITexture : public PythonClass { + public: + static void SetupType(PyTypeObject* cls); + static auto type_name() -> const char*; + static auto Create(const Object::Ref& texture) + -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + + /// Cast raw Python pointer to our type; throws an exception on wrong types. + static auto FromPyObj(PyObject* o) -> PythonClassUITexture& { + if (Check(o)) { + return *reinterpret_cast(o); + } + throw Exception(std::string("Expected a ") + type_name() + "; got a " + + Python::ObjTypeToString(o), + PyExcType::kType); + } + + auto texture() const -> base::TextureAsset& { + assert(texture_); + return **texture_; + } + + static PyTypeObject type_obj; + + private: + static PyMethodDef tp_methods[]; + static auto tp_repr(PythonClassUITexture* self) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassUITexture* self); + Object::Ref* texture_; +}; + +} // namespace ballistica::ui_v1 + +#endif // BALLISTICA_UI_V1_PYTHON_CLASS_PYTHON_CLASS_UI_TEXTURE_H_ diff --git a/src/ballistica/python/class/python_class_widget.cc b/src/ballistica/ui_v1/python/class/python_class_widget.cc similarity index 75% rename from src/ballistica/python/class/python_class_widget.cc rename to src/ballistica/ui_v1/python/class/python_class_widget.cc index 684fa4c1..7a31bb13 100644 --- a/src/ballistica/python/class/python_class_widget.cc +++ b/src/ballistica/ui_v1/python/class/python_class_widget.cc @@ -1,43 +1,46 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/python/class/python_class_widget.h" +#include "ballistica/ui_v1/python/class/python_class_widget.h" -#include "ballistica/core/thread.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/logic/logic.h" -#include "ballistica/python/python.h" -#include "ballistica/ui/widget/container_widget.h" +#include "ballistica/base/graphics/graphics.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/ui_v1/widget/container_widget.h" -namespace ballistica { +namespace ballistica::ui_v1 { auto PythonClassWidget::nb_bool(PythonClassWidget* self) -> int { - return self->widget_->exists(); + return self->widget_->Exists(); } PyNumberMethods PythonClassWidget::as_number_; -void PythonClassWidget::SetupType(PyTypeObject* obj) { - PythonClass::SetupType(obj); - obj->tp_name = "ba.Widget"; - obj->tp_basicsize = sizeof(PythonClassWidget); - obj->tp_doc = +auto PythonClassWidget::type_name() -> const char* { return "Widget"; } + +void PythonClassWidget::SetupType(PyTypeObject* cls) { + PythonClass::SetupType(cls); + // Fully qualified type path we will be exposed as: + cls->tp_name = "bauiv1.Widget"; + cls->tp_basicsize = sizeof(PythonClassWidget); + cls->tp_doc = "Internal type for low level UI elements; buttons, windows, etc.\n" "\n" "Category: **User Interface Classes**\n" "\n" "This class represents a weak reference to a widget object\n" "in the internal C++ layer. Currently, functions such as\n" - "ba.buttonwidget() must be used to instantiate or edit these."; - obj->tp_new = tp_new; - obj->tp_dealloc = (destructor)tp_dealloc; - obj->tp_repr = (reprfunc)tp_repr; - obj->tp_methods = tp_methods; + "babase.buttonwidget() must be used to instantiate or edit these."; + cls->tp_new = tp_new; + cls->tp_dealloc = (destructor)tp_dealloc; + cls->tp_repr = (reprfunc)tp_repr; + cls->tp_methods = tp_methods; // we provide number methods only for bool functionality memset(&as_number_, 0, sizeof(as_number_)); as_number_.nb_bool = (inquiry)nb_bool; - obj->tp_as_number = &as_number_; + cls->tp_as_number = &as_number_; } auto PythonClassWidget::Create(Widget* widget) -> PyObject* { @@ -46,24 +49,27 @@ auto PythonClassWidget::Create(Widget* widget) -> PyObject* { assert(!widget->has_py_ref()); } + assert(TypeIsSetUp(&type_obj)); auto* py_widget = reinterpret_cast( PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); - if (!py_widget) throw Exception("ba.Widget creation failed"); + if (!py_widget) throw Exception("babase.Widget creation failed"); - *(py_widget->widget_) = widget; - return reinterpret_cast(py_widget); + *py_widget->widget_ = widget; + + auto* out = reinterpret_cast(py_widget); + return out; } auto PythonClassWidget::GetWidget() const -> Widget* { - Widget* w = widget_->get(); + Widget* w = widget_->Get(); if (!w) throw Exception("Invalid widget"); return w; } auto PythonClassWidget::tp_repr(PythonClassWidget* self) -> PyObject* { BA_PYTHON_TRY; - Widget* w = self->widget_->get(); - return Py_BuildValue("s", (std::string("widget_->Get(); + return Py_BuildValue("s", (std::string("GetWidgetTypeName() : "") + "' widget " + Utils::PtrToString(w) + ">") .c_str()); @@ -73,27 +79,28 @@ auto PythonClassWidget::tp_repr(PythonClassWidget* self) -> PyObject* { auto PythonClassWidget::tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) -> PyObject* { auto* self = reinterpret_cast(type->tp_alloc(type, 0)); - if (self) { - BA_PYTHON_TRY; - if (!InLogicThread()) { - throw Exception( - "ERROR: " + std::string(type_obj.tp_name) - + " objects must only be created in the logic thread (current is (" - + GetCurrentThreadName() + ")."); - } - self->widget_ = new Object::WeakRef(); - BA_PYTHON_NEW_CATCH; + if (!self) { + return nullptr; } + BA_PYTHON_TRY; + if (!g_base->InLogicThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the logic thread (current is (" + + CurrentThreadName() + ")."); + } + self->widget_ = new Object::WeakRef(); return reinterpret_cast(self); + BA_PYTHON_NEW_CATCH; } void PythonClassWidget::tp_dealloc(PythonClassWidget* self) { BA_PYTHON_TRY; // these have to be destructed in the logic thread - send them along to it if // need be - if (!InLogicThread()) { + if (!g_base->InLogicThread()) { Object::WeakRef* w = self->widget_; - g_logic->thread()->PushCall([w] { delete w; }); + g_base->logic->event_loop()->PushCall([w] { delete w; }); } else { delete self->widget_; } @@ -103,7 +110,7 @@ void PythonClassWidget::tp_dealloc(PythonClassWidget* self) { auto PythonClassWidget::Exists(PythonClassWidget* self) -> PyObject* { BA_PYTHON_TRY; - Widget* w = self->widget_->get(); + Widget* w = self->widget_->Get(); if (w) { Py_RETURN_TRUE; } else { @@ -114,7 +121,7 @@ auto PythonClassWidget::Exists(PythonClassWidget* self) -> PyObject* { auto PythonClassWidget::GetWidgetType(PythonClassWidget* self) -> PyObject* { BA_PYTHON_TRY; - Widget* w = self->widget_->get(); + Widget* w = self->widget_->Get(); if (!w) { throw Exception(PyExcType::kWidgetNotFound); } @@ -124,7 +131,7 @@ auto PythonClassWidget::GetWidgetType(PythonClassWidget* self) -> PyObject* { auto PythonClassWidget::Activate(PythonClassWidget* self) -> PyObject* { BA_PYTHON_TRY; - Widget* w = self->widget_->get(); + Widget* w = self->widget_->Get(); if (!w) { throw Exception(PyExcType::kWidgetNotFound); } @@ -135,7 +142,7 @@ auto PythonClassWidget::Activate(PythonClassWidget* self) -> PyObject* { auto PythonClassWidget::GetChildren(PythonClassWidget* self) -> PyObject* { BA_PYTHON_TRY; - Widget* w = self->widget_->get(); + Widget* w = self->widget_->Get(); if (!w) { throw Exception(PyExcType::kWidgetNotFound); } @@ -149,7 +156,7 @@ auto PythonClassWidget::GetChildren(PythonClassWidget* self) -> PyObject* { if (cw) { #pragma clang diagnostic pop for (auto&& i : cw->widgets()) { - assert(i.exists()); + assert(i.Exists()); PyList_Append(py_list, i->BorrowPyRef()); } } @@ -159,7 +166,7 @@ auto PythonClassWidget::GetChildren(PythonClassWidget* self) -> PyObject* { auto PythonClassWidget::GetSelectedChild(PythonClassWidget* self) -> PyObject* { BA_PYTHON_TRY; - Widget* w = self->widget_->get(); + Widget* w = self->widget_->Get(); if (!w) { throw Exception(PyExcType::kWidgetNotFound); } @@ -181,7 +188,7 @@ auto PythonClassWidget::GetSelectedChild(PythonClassWidget* self) -> PyObject* { auto PythonClassWidget::GetScreenSpaceCenter(PythonClassWidget* self) -> PyObject* { BA_PYTHON_TRY; - Widget* w = self->widget_->get(); + Widget* w = self->widget_->Get(); if (!w) { throw Exception(PyExcType::kWidgetNotFound); } @@ -195,8 +202,8 @@ auto PythonClassWidget::GetScreenSpaceCenter(PythonClassWidget* self) } // ..but we actually want to return points relative to the center of the // screen (so they're useful as stack-offset values) - float screen_width = g_graphics->screen_virtual_width(); - float screen_height = g_graphics->screen_virtual_height(); + float screen_width = g_base->graphics->screen_virtual_width(); + float screen_height = g_base->graphics->screen_virtual_height(); x -= screen_width * 0.5f; y -= screen_height * 0.5f; return Py_BuildValue("(ff)", x, y); @@ -212,7 +219,7 @@ auto PythonClassWidget::Delete(PythonClassWidget* self, PyObject* args, args, keywds, "|i", const_cast(kwlist), &ignore_missing)) { return nullptr; } - Widget* w = self->widget_->get(); + Widget* w = self->widget_->Get(); if (!w) { if (!ignore_missing) { throw Exception(PyExcType::kWidgetNotFound); @@ -239,7 +246,7 @@ auto PythonClassWidget::AddDeleteCallback(PythonClassWidget* self, const_cast(kwlist), &call_obj)) { return nullptr; } - Widget* w = self->widget_->get(); + Widget* w = self->widget_->Get(); if (!w) { throw Exception(PyExcType::kWidgetNotFound); } @@ -263,24 +270,24 @@ PyMethodDef PythonClassWidget::tp_methods[] = { "get_widget_type() -> str\n" "\n" "Return the internal type of the Widget as a string. Note that this\n" - "is different from the Python ba.Widget type, which is the same for\n" + "is different from the Python bauiv1.Widget type, which is the same for\n" "all widgets."}, {"activate", (PyCFunction)Activate, METH_NOARGS, "activate() -> None\n" "\n" "Activates a widget; the same as if it had been clicked."}, {"get_children", (PyCFunction)GetChildren, METH_NOARGS, - "get_children() -> list[ba.Widget]\n" + "get_children() -> list[bauiv1.Widget]\n" "\n" "Returns any child Widgets of this Widget."}, {"get_screen_space_center", (PyCFunction)GetScreenSpaceCenter, METH_NOARGS, "get_screen_space_center() -> tuple[float, float]\n" "\n" - "Returns the coords of the ba.Widget center relative to the center\n" + "Returns the coords of the bauiv1.Widget center relative to the center\n" "of the screen. This can be useful for placing pop-up windows and other\n" "special cases."}, {"get_selected_child", (PyCFunction)GetSelectedChild, METH_NOARGS, - "get_selected_child() -> ba.Widget | None\n" + "get_selected_child() -> bauiv1.Widget | None\n" "\n" "Returns the selected child Widget or None if nothing is selected."}, // NOLINTNEXTLINE (signed bitwise stuff) @@ -296,4 +303,4 @@ PyMethodDef PythonClassWidget::tp_methods[] = { "Add a call to be run immediately after this widget is destroyed."}, {nullptr}}; -} // namespace ballistica +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/python/class/python_class_widget.h b/src/ballistica/ui_v1/python/class/python_class_widget.h similarity index 74% rename from src/ballistica/python/class/python_class_widget.h rename to src/ballistica/ui_v1/python/class/python_class_widget.h index 5b74f127..012612da 100644 --- a/src/ballistica/python/class/python_class_widget.h +++ b/src/ballistica/ui_v1/python/class/python_class_widget.h @@ -1,17 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_WIDGET_H_ -#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_WIDGET_H_ +#ifndef BALLISTICA_UI_V1_PYTHON_CLASS_PYTHON_CLASS_WIDGET_H_ +#define BALLISTICA_UI_V1_PYTHON_CLASS_PYTHON_CLASS_WIDGET_H_ -#include "ballistica/core/object.h" -#include "ballistica/python/class/python_class.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_class.h" +#include "ballistica/ui_v1/ui_v1.h" -namespace ballistica { +namespace ballistica::ui_v1 { class PythonClassWidget : public PythonClass { public: - static void SetupType(PyTypeObject* obj); - static auto type_name() -> const char* { return "Widget"; } + static void SetupType(PyTypeObject* cls); + static auto type_name() -> const char*; static auto Create(Widget* widget) -> PyObject*; static auto Check(PyObject* o) -> bool { return PyObject_TypeCheck(o, &type_obj); @@ -40,6 +41,6 @@ class PythonClassWidget : public PythonClass { static PyNumberMethods as_number_; }; -} // namespace ballistica +} // namespace ballistica::ui_v1 -#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_WIDGET_H_ +#endif // BALLISTICA_UI_V1_PYTHON_CLASS_PYTHON_CLASS_WIDGET_H_ diff --git a/src/ballistica/python/methods/python_methods_ui.cc b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc similarity index 60% rename from src/ballistica/python/methods/python_methods_ui.cc rename to src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc index f02c5995..f15a3192 100644 --- a/src/ballistica/python/methods/python_methods_ui.cc +++ b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc @@ -1,38 +1,157 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/python/methods/python_methods_ui.h" +#include "ballistica/ui_v1/python/methods/python_methods_ui_v1.h" -#include "ballistica/app/app.h" -#include "ballistica/app/app_flavor.h" -#include "ballistica/input/input.h" -#include "ballistica/internal/app_internal.h" -#include "ballistica/logic/connection/connection_set.h" -#include "ballistica/logic/logic.h" -#include "ballistica/logic/v1_account.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_sys.h" -#include "ballistica/ui/root_ui.h" -#include "ballistica/ui/ui.h" -#include "ballistica/ui/widget/button_widget.h" -#include "ballistica/ui/widget/check_box_widget.h" -#include "ballistica/ui/widget/column_widget.h" -#include "ballistica/ui/widget/h_scroll_widget.h" -#include "ballistica/ui/widget/image_widget.h" -#include "ballistica/ui/widget/root_widget.h" -#include "ballistica/ui/widget/row_widget.h" -#include "ballistica/ui/widget/scroll_widget.h" +#include "ballistica/base/app/app.h" +#include "ballistica/base/app/app_mode.h" +#include "ballistica/base/assets/sound_asset.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/python/base_python.h" +#include "ballistica/base/support/plus_soft.h" +#include "ballistica/base/ui/console.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/ui_v1/python/class/python_class_ui_mesh.h" +#include "ballistica/ui_v1/python/class/python_class_ui_sound.h" +#include "ballistica/ui_v1/python/class/python_class_ui_texture.h" +#include "ballistica/ui_v1/python/ui_v1_python.h" +#include "ballistica/ui_v1/support/root_ui.h" +#include "ballistica/ui_v1/widget/button_widget.h" +#include "ballistica/ui_v1/widget/check_box_widget.h" +#include "ballistica/ui_v1/widget/column_widget.h" +#include "ballistica/ui_v1/widget/h_scroll_widget.h" +#include "ballistica/ui_v1/widget/image_widget.h" +#include "ballistica/ui_v1/widget/root_widget.h" +#include "ballistica/ui_v1/widget/row_widget.h" +#include "ballistica/ui_v1/widget/scroll_widget.h" #if !BA_HEADLESS_BUILD && !BA_XCODE_NEW_PROJECT extern "C" void SDL_ericf_focus(void); #endif -namespace ballistica { +namespace ballistica::ui_v1 { // Ignore signed bitwise stuff; python macros do it quite a bit. #pragma clang diagnostic push #pragma ide diagnostic ignored "hicpp-signed-bitwise" -auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) +// ------------------------------ getsound ------------------------------------- + +static auto PyGetSound(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + { + base::Assets::AssetListLock lock; + return PythonClassUISound::Create(g_base->assets->GetSound(name)); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetSoundDef = { + "getsound", // name + (PyCFunction)PyGetSound, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "getsound(name: str) -> bauiv1.Sound\n" + "\n" + "Load a sound for use in the ui.", +}; + +// ----------------------------- gettexture ------------------------------------ + +static auto PyGetTexture(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + { + base::Assets::AssetListLock lock; + return PythonClassUITexture::Create(g_base->assets->GetTexture(name)); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetTextureDef = { + "gettexture", // name + (PyCFunction)PyGetTexture, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "gettexture(name: str) -> bauiv1.Texture\n" + "\n" + "Load a texture for use in the ui.", +}; + +// ------------------------------- getmesh ------------------------------------- + +static auto PyGetMesh(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + { + base::Assets::AssetListLock lock; + return PythonClassUIMesh::Create(g_base->assets->GetMesh(name)); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetMeshDef = { + "getmesh", // name + (PyCFunction)PyGetMesh, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "getmesh(name: str) -> bauiv1.Mesh\n" + "\n" + "Load a mesh for use solely in the local user interface.", +}; + +// -------------------------- get_qrcode_texture ------------------------------- + +static auto PyGetQRCodeTexture(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + const char* url; + static const char* kwlist[] = {"url", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &url)) { + return nullptr; + } + { + base::Assets::AssetListLock lock; + return PythonClassUITexture::Create(g_base->assets->GetQRCodeTexture(url)); + } + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetQRCodeTextureDef = { + "get_qrcode_texture", // name + (PyCFunction)PyGetQRCodeTexture, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_qrcode_texture(url: str) -> bauiv1.Texture\n" + "\n" + "(internal)", +}; + +// ----------------------------- buttonwidget ---------------------------------- + +static auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; PyObject* size_obj = Py_None; @@ -56,8 +175,8 @@ auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) PyObject* text_scale_obj = Py_None; PyObject* textcolor_obj = Py_None; PyObject* enable_sound_obj = Py_None; - PyObject* model_transparent_obj = Py_None; - PyObject* model_opaque_obj = Py_None; + PyObject* mesh_transparent_obj = Py_None; + PyObject* mesh_opaque_obj = Py_None; PyObject* repeat_obj = Py_None; PyObject* scale_obj = Py_None; PyObject* transition_delay_obj = Py_None; @@ -67,7 +186,7 @@ auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) PyObject* selectable_obj = Py_None; PyObject* show_buffer_top_obj = Py_None; PyObject* icon_obj = Py_None; - PyObject* iconscale_obj = Py_None; + PyObject* icon_scale_obj = Py_None; PyObject* icon_tint_obj = Py_None; PyObject* icon_color_obj = Py_None; PyObject* autoselect_obj = Py_None; @@ -92,8 +211,8 @@ auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) "text_scale", "textcolor", "enable_sound", - "model_transparent", - "model_opaque", + "mesh_transparent", + "mesh_opaque", "repeat", "scale", "transition_delay", @@ -121,35 +240,32 @@ auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) &pos_obj, &on_activate_call_obj, &label_obj, &color_obj, &down_widget_obj, &up_widget_obj, &left_widget_obj, &right_widget_obj, &texture_obj, &text_scale_obj, &textcolor_obj, &enable_sound_obj, - &model_transparent_obj, &model_opaque_obj, &repeat_obj, &scale_obj, + &mesh_transparent_obj, &mesh_opaque_obj, &repeat_obj, &scale_obj, &transition_delay_obj, &on_select_call_obj, &button_type_obj, &extra_touch_border_scale_obj, &selectable_obj, &show_buffer_top_obj, - &icon_obj, &iconscale_obj, &icon_tint_obj, &icon_color_obj, + &icon_obj, &icon_scale_obj, &icon_tint_obj, &icon_color_obj, &autoselect_obj, &mask_texture_obj, &tint_texture_obj, &tint_color_obj, &tint2_color_obj, &text_flatness_obj, &text_res_scale_obj, &enabled_obj)) return nullptr; - if (!g_logic->IsInUIContext()) { - throw Exception( - "This must be called within the UI context (see ba.Context docs)", - PyExcType::kContext); + if (!g_base->CurrentContext().IsEmpty()) { + throw Exception("UI functions must be called with no context set."); } - ScopedSetContext cp(g_logic->GetUIContextTarget()); - // grab the edited widget or create a new one --------------------- + // Grab the edited widget or create a new one. Object::Ref b; if (edit_obj != Py_None) { - b = dynamic_cast(Python::GetPyWidget(edit_obj)); - if (!b.exists()) { + b = dynamic_cast(UIV1Python::GetPyWidget(edit_obj)); + if (!b.Exists()) { throw Exception("Invalid or nonexistent widget.", PyExcType::kWidgetNotFound); } } else { - parent_widget = - parent_obj == Py_None - ? g_ui->screen_root_widget() - : dynamic_cast(Python::GetPyWidget(parent_obj)); + parent_widget = parent_obj == Py_None + ? g_base->ui->screen_root_widget() + : dynamic_cast( + UIV1Python::GetPyWidget(parent_obj)); if (parent_widget == nullptr) { throw Exception("Parent widget nonexistent or not a container.", PyExcType::kWidgetNotFound); @@ -157,23 +273,23 @@ auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) b = Object::New(); } - // set applicable values ---------------------------- + // Set applicable values. if (label_obj != Py_None) { - b->SetText(Python::GetPyString(label_obj)); + b->set_text(g_base->python->GetPyLString(label_obj)); } if (on_activate_call_obj != Py_None) { b->set_on_activate_call(on_activate_call_obj); } if (down_widget_obj != Py_None) { - down_widget = Python::GetPyWidget(down_widget_obj); + down_widget = UIV1Python::GetPyWidget(down_widget_obj); if (!down_widget) { throw Exception("Invalid down widget.", PyExcType::kWidgetNotFound); } b->set_down_widget(down_widget); } if (up_widget_obj != Py_None) { - up_widget = Python::GetPyWidget(up_widget_obj); + up_widget = UIV1Python::GetPyWidget(up_widget_obj); if (!up_widget) { throw Exception("Invalid up widget.", PyExcType::kWidgetNotFound); } @@ -183,27 +299,29 @@ auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) b->set_auto_select(Python::GetPyBool(autoselect_obj)); } if (left_widget_obj != Py_None) { - left_widget = Python::GetPyWidget(left_widget_obj); + left_widget = UIV1Python::GetPyWidget(left_widget_obj); if (!left_widget) { throw Exception("Invalid left widget.", PyExcType::kWidgetNotFound); } b->set_left_widget(left_widget); } if (right_widget_obj != Py_None) { - right_widget = Python::GetPyWidget(right_widget_obj); + right_widget = UIV1Python::GetPyWidget(right_widget_obj); if (!right_widget) { throw Exception("Invalid right widget.", PyExcType::kWidgetNotFound); } b->set_right_widget(right_widget); } - if (model_transparent_obj != Py_None) { - b->SetModelTransparent(Python::GetPyModel(model_transparent_obj)); + if (mesh_transparent_obj != Py_None) { + b->SetMeshTransparent( + &PythonClassUIMesh::FromPyObj(mesh_transparent_obj).mesh()); } if (show_buffer_top_obj != Py_None) { b->set_show_buffer_top(Python::GetPyFloat(show_buffer_top_obj)); } - if (model_opaque_obj != Py_None) { - b->SetModelOpaque(Python::GetPyModel(model_opaque_obj)); + if (mesh_opaque_obj != Py_None) { + b->SetMeshTransparent( + &PythonClassUIMesh::FromPyObj(mesh_opaque_obj).mesh()); } if (on_select_call_obj != Py_None) { b->SetOnSelectCall(on_select_call_obj); @@ -223,8 +341,8 @@ auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) if (scale_obj != Py_None) { b->set_scale(Python::GetPyFloat(scale_obj)); } - if (iconscale_obj != Py_None) { - b->set_icon_scale(Python::GetPyFloat(iconscale_obj)); + if (icon_scale_obj != Py_None) { + b->set_icon_scale(Python::GetPyFloat(icon_scale_obj)); } if (icon_tint_obj != Py_None) { b->set_icon_tint(Python::GetPyFloat(icon_tint_obj)); @@ -242,16 +360,18 @@ auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) Python::GetPyFloat(extra_touch_border_scale_obj)); } if (texture_obj != Py_None) { - b->SetTexture(Python::GetPyTexture(texture_obj)); + b->SetTexture(&PythonClassUITexture::FromPyObj(texture_obj).texture()); } if (mask_texture_obj != Py_None) { - b->SetMaskTexture(Python::GetPyTexture(mask_texture_obj)); + b->SetMaskTexture( + &PythonClassUITexture::FromPyObj(mask_texture_obj).texture()); } if (tint_texture_obj != Py_None) { - b->SetTintTexture(Python::GetPyTexture(tint_texture_obj)); + b->SetTintTexture( + &PythonClassUITexture::FromPyObj(tint_texture_obj).texture()); } if (icon_obj != Py_None) { - b->SetIcon(Python::GetPyTexture(icon_obj)); + b->SetIcon(&PythonClassUITexture::FromPyObj(icon_obj).texture()); } if (button_type_obj != Py_None) { std::string button_type = Python::GetPyString(button_type_obj); @@ -313,9 +433,6 @@ auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) } if (transition_delay_obj != Py_None) { // We accept this as seconds; widget takes milliseconds. -#if BA_TEST_BUILD - g_python->TimeFormatCheck(TimeFormat::kSeconds, transition_delay_obj); -#endif b->set_transition_delay(static_cast( 1000.0f * Python::GetPyFloat(transition_delay_obj))); } @@ -328,7 +445,7 @@ auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) // If making a new widget add it at the end. if (edit_obj == Py_None) { - g_ui->AddWidget(b.get(), parent_widget); + g_base->ui->AddWidget(b.Get(), parent_widget); } return b->NewPyRef(); @@ -336,7 +453,61 @@ auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) BA_PYTHON_CATCH; } -auto PyCheckBoxWidget(PyObject* self, PyObject* args, PyObject* keywds) +static PyMethodDef PyButtonWidgetDef = { + "buttonwidget", // name + (PyCFunction)PyButtonWidget, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "buttonwidget(edit: bauiv1.Widget | None = None,\n" + " parent: bauiv1.Widget | None = None,\n" + " size: Sequence[float] | None = None,\n" + " position: Sequence[float] | None = None,\n" + " on_activate_call: Callable | None = None,\n" + " label: str | bauiv1.Lstr | None = None,\n" + " color: Sequence[float] | None = None,\n" + " down_widget: bauiv1.Widget | None = None,\n" + " up_widget: bauiv1.Widget | None = None,\n" + " left_widget: bauiv1.Widget | None = None,\n" + " right_widget: bauiv1.Widget | None = None,\n" + " texture: bauiv1.Texture | None = None,\n" + " text_scale: float | None = None,\n" + " textcolor: Sequence[float] | None = None,\n" + " enable_sound: bool | None = None,\n" + " mesh_transparent: bauiv1.Mesh | None = None,\n" + " mesh_opaque: bauiv1.Mesh | None = None,\n" + " repeat: bool | None = None,\n" + " scale: float | None = None,\n" + " transition_delay: float | None = None,\n" + " on_select_call: Callable | None = None,\n" + " button_type: str | None = None,\n" + " extra_touch_border_scale: float | None = None,\n" + " selectable: bool | None = None,\n" + " show_buffer_top: float | None = None,\n" + " icon: bauiv1.Texture | None = None,\n" + " iconscale: float | None = None,\n" + " icon_tint: float | None = None,\n" + " icon_color: Sequence[float] | None = None,\n" + " autoselect: bool | None = None,\n" + " mask_texture: bauiv1.Texture | None = None,\n" + " tint_texture: bauiv1.Texture | None = None,\n" + " tint_color: Sequence[float] | None = None,\n" + " tint2_color: Sequence[float] | None = None,\n" + " text_flatness: float | None = None,\n" + " text_res_scale: float | None = None,\n" + " enabled: bool | None = None) -> bauiv1.Widget\n" + "\n" + "Create or edit a button widget.\n" + "\n" + "Category: **User Interface Functions**\n" + "\n" + "Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget.", +}; + +// --------------------------- checkboxwidget ---------------------------------- + +static auto PyCheckBoxWidget(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; PyObject* size_obj = Py_None; @@ -381,26 +552,23 @@ auto PyCheckBoxWidget(PyObject* self, PyObject* args, PyObject* keywds) return nullptr; } - if (!g_logic->IsInUIContext()) { - throw Exception( - "This must be called within the UI context (see ba.Context docs).", - PyExcType::kContext); + if (!g_base->CurrentContext().IsEmpty()) { + throw Exception("UI functions must be called with no context set."); } - ScopedSetContext cp(g_logic->GetUIContextTarget()); - // grab the edited widget or create a new one --------------------- + // Grab the edited widget or create a new one. Object::Ref widget; if (edit_obj != Py_None) { - widget = dynamic_cast(Python::GetPyWidget(edit_obj)); - if (!widget.exists()) { + widget = dynamic_cast(UIV1Python::GetPyWidget(edit_obj)); + if (!widget.Exists()) { throw Exception("Invalid or nonexistent widget.", PyExcType::kWidgetNotFound); } } else { - parent_widget = - parent_obj == Py_None - ? g_ui->screen_root_widget() - : dynamic_cast(Python::GetPyWidget(parent_obj)); + parent_widget = parent_obj == Py_None + ? g_base->ui->screen_root_widget() + : dynamic_cast( + UIV1Python::GetPyWidget(parent_obj)); if (parent_widget == nullptr) { throw Exception("Parent widget nonexistent or not a container.", PyExcType::kWidgetNotFound); @@ -422,7 +590,7 @@ auto PyCheckBoxWidget(PyObject* self, PyObject* args, PyObject* keywds) widget->set_auto_select(Python::GetPyBool(autoselect_obj)); } if (text_obj != Py_None) { - widget->SetText(Python::GetPyString(text_obj)); + widget->SetText(g_base->python->GetPyLString(text_obj)); } if (value_obj != Py_None) { widget->SetValue(Python::GetPyBool(value_obj)); @@ -466,7 +634,7 @@ auto PyCheckBoxWidget(PyObject* self, PyObject* args, PyObject* keywds) // if making a new widget add it at the end if (edit_obj == Py_None) { - g_ui->AddWidget(widget.get(), parent_widget); + g_base->ui->AddWidget(widget.Get(), parent_widget); } return widget->NewPyRef(); @@ -474,7 +642,39 @@ auto PyCheckBoxWidget(PyObject* self, PyObject* args, PyObject* keywds) BA_PYTHON_CATCH; } -auto PyImageWidget(PyObject* self, PyObject* args, PyObject* keywds) +static PyMethodDef PyCheckBoxWidgetDef = { + "checkboxwidget", // name + (PyCFunction)PyCheckBoxWidget, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "checkboxwidget(edit: bauiv1.Widget | None = None,\n" + " parent: bauiv1.Widget | None = None,\n" + " size: Sequence[float] | None = None,\n" + " position: Sequence[float] | None = None,\n" + " text: str | bauiv1.Lstr | None = None,\n" + " value: bool | None = None,\n" + " on_value_change_call: Callable[[bool], None] | None = None,\n" + " on_select_call: Callable[[], None] | None = None,\n" + " text_scale: float | None = None,\n" + " textcolor: Sequence[float] | None = None,\n" + " scale: float | None = None,\n" + " is_radio_button: bool | None = None,\n" + " maxwidth: float | None = None,\n" + " autoselect: bool | None = None,\n" + " color: Sequence[float] | None = None) -> bauiv1.Widget\n" + "\n" + "Create or edit a check-box widget.\n" + "\n" + "Category: **User Interface Functions**\n" + "\n" + "Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget.", +}; + +// ----------------------------- imagewidget ----------------------------------- + +static auto PyImageWidget(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; PyObject* size_obj = Py_None; @@ -488,8 +688,8 @@ auto PyImageWidget(PyObject* self, PyObject* args, PyObject* keywds) PyObject* tint_color_obj = Py_None; PyObject* tint2_color_obj = Py_None; PyObject* opacity_obj = Py_None; - PyObject* model_transparent_obj = Py_None; - PyObject* model_opaque_obj = Py_None; + PyObject* mesh_transparent_obj = Py_None; + PyObject* mesh_opaque_obj = Py_None; PyObject* has_alpha_channel_obj = Py_None; PyObject* transition_delay_obj = Py_None; PyObject* draw_controller_obj = Py_None; @@ -504,8 +704,8 @@ auto PyImageWidget(PyObject* self, PyObject* args, PyObject* keywds) "color", "texture", "opacity", - "model_transparent", - "model_opaque", + "mesh_transparent", + "mesh_opaque", "has_alpha_channel", "tint_texture", "tint_color", @@ -519,31 +719,28 @@ auto PyImageWidget(PyObject* self, PyObject* args, PyObject* keywds) if (!PyArg_ParseTupleAndKeywords( args, keywds, "|OOOOOOOOOOOOOOOOOO", const_cast(kwlist), &edit_obj, &parent_obj, &size_obj, &pos_obj, &color_obj, &texture_obj, - &opacity_obj, &model_transparent_obj, &model_opaque_obj, + &opacity_obj, &mesh_transparent_obj, &mesh_opaque_obj, &has_alpha_channel_obj, &tint_texture_obj, &tint_color_obj, &transition_delay_obj, &draw_controller_obj, &tint2_color_obj, &tilt_scale_obj, &mask_texture_obj, &radial_amount_obj)) return nullptr; - if (!g_logic->IsInUIContext()) { - throw Exception( - "This must be called within the UI context (see ba.Context docs).", - PyExcType::kContext); + if (!g_base->CurrentContext().IsEmpty()) { + throw Exception("UI functions must be called with no context set."); } - ScopedSetContext cp(g_logic->GetUIContextTarget()); - // grab the edited widget or create a new one --------------------- + // Grab the edited widget or create a new one. Object::Ref b; if (edit_obj != Py_None) { - b = dynamic_cast(Python::GetPyWidget(edit_obj)); - if (!b.exists()) + b = dynamic_cast(UIV1Python::GetPyWidget(edit_obj)); + if (!b.Exists()) throw Exception("Invalid or nonexistent widget.", PyExcType::kWidgetNotFound); } else { - parent_widget = - parent_obj == Py_None - ? g_ui->screen_root_widget() - : dynamic_cast(Python::GetPyWidget(parent_obj)); + parent_widget = parent_obj == Py_None + ? g_base->ui->screen_root_widget() + : dynamic_cast( + UIV1Python::GetPyWidget(parent_obj)); if (parent_widget == nullptr) { throw Exception("Parent widget nonexistent or not a container.", PyExcType::kWidgetNotFound); @@ -556,22 +753,25 @@ auto PyImageWidget(PyObject* self, PyObject* args, PyObject* keywds) b->set_height(p.y); } if (texture_obj != Py_None) { - b->SetTexture(Python::GetPyTexture(texture_obj)); + b->SetTexture(&PythonClassUITexture::FromPyObj(texture_obj).texture()); } if (tint_texture_obj != Py_None) { - b->SetTintTexture(Python::GetPyTexture(tint_texture_obj)); + b->SetTintTexture( + &PythonClassUITexture::FromPyObj(tint_texture_obj).texture()); } if (mask_texture_obj != Py_None) { - b->SetMaskTexture(Python::GetPyTexture(mask_texture_obj)); + b->SetMaskTexture( + &PythonClassUITexture::FromPyObj(mask_texture_obj).texture()); } - if (model_opaque_obj != Py_None) { - b->SetModelOpaque(Python::GetPyModel(model_opaque_obj)); + if (mesh_opaque_obj != Py_None) { + b->SetMeshOpaque(&PythonClassUIMesh::FromPyObj(mesh_opaque_obj).mesh()); } - if (model_transparent_obj != Py_None) { - b->SetModelTransparent(Python::GetPyModel(model_transparent_obj)); + if (mesh_transparent_obj != Py_None) { + b->SetMeshTransparent( + &PythonClassUIMesh::FromPyObj(mesh_transparent_obj).mesh()); } if (draw_controller_obj != Py_None) { - auto* dcw = Python::GetPyWidget(draw_controller_obj); + auto* dcw = UIV1Python::GetPyWidget(draw_controller_obj); if (!dcw) { throw Exception("Invalid or nonexistent draw-controller widget.", PyExcType::kWidgetNotFound); @@ -593,9 +793,6 @@ auto PyImageWidget(PyObject* self, PyObject* args, PyObject* keywds) } if (transition_delay_obj != Py_None) { // We accept this as seconds; widget takes milliseconds. -#if BA_TEST_BUILD - g_python->TimeFormatCheck(TimeFormat::kSeconds, transition_delay_obj); -#endif b->set_transition_delay(1000.0f * Python::GetPyFloat(transition_delay_obj)); } if (color_obj != Py_None) { @@ -625,14 +822,50 @@ auto PyImageWidget(PyObject* self, PyObject* args, PyObject* keywds) // if making a new widget add it at the end if (edit_obj == Py_None) { - g_ui->AddWidget(b.get(), parent_widget); + g_base->ui->AddWidget(b.Get(), parent_widget); } return b->NewPyRef(); BA_PYTHON_CATCH; } -auto PyColumnWidget(PyObject* self, PyObject* args, PyObject* keywds) +static PyMethodDef PyImageWidgetDef = { + "imagewidget", // name + (PyCFunction)PyImageWidget, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "imagewidget(edit: bauiv1.Widget | None = None,\n" + " parent: bauiv1.Widget | None = None,\n" + " size: Sequence[float] | None = None,\n" + " position: Sequence[float] | None = None,\n" + " color: Sequence[float] | None = None,\n" + " texture: bauiv1.Texture | None = None,\n" + " opacity: float | None = None,\n" + " mesh_transparent: bauiv1.Mesh | None = None,\n" + " mesh_opaque: bauiv1.Mesh | None = None,\n" + " has_alpha_channel: bool = True,\n" + " tint_texture: bauiv1.Texture | None = None,\n" + " tint_color: Sequence[float] | None = None,\n" + " transition_delay: float | None = None,\n" + " draw_controller: bauiv1.Widget | None = None,\n" + " tint2_color: Sequence[float] | None = None,\n" + " tilt_scale: float | None = None,\n" + " mask_texture: bauiv1.Texture | None = None,\n" + " radial_amount: float | None = None)\n" + " -> bauiv1.Widget\n" + "\n" + "Create or edit an image widget.\n" + "\n" + "Category: **User Interface Functions**\n" + "\n" + "Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget.", +}; + +// ----------------------------- columnwidget ---------------------------------- + +static auto PyColumnWidget(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; @@ -681,29 +914,23 @@ auto PyColumnWidget(PyObject* self, PyObject* args, PyObject* keywds) &margin_obj, &claims_left_right_obj, &claims_tab_obj)) return nullptr; - if (!g_logic->IsInUIContext()) { - throw Exception( - "This must be called within the UI context (see ba.Context " - "docs).", - PyExcType::kContext); + if (!g_base->CurrentContext().IsEmpty()) { + throw Exception("UI functions must be called with no context set."); } - // if (!g_logic->IsInUIContext()) { BA_LOG_PYTHON_TRACE("ERROR: This should be - // called within the UI context (see ba.Context docs)");} - ScopedSetContext cp(g_logic->GetUIContextTarget()); - // grab the edited widget or create a new one --------------------- + // Grab the edited widget or create a new one. Object::Ref widget; if (edit_obj != Py_None) { - widget = dynamic_cast(Python::GetPyWidget(edit_obj)); - if (!widget.exists()) { + widget = dynamic_cast(UIV1Python::GetPyWidget(edit_obj)); + if (!widget.Exists()) { throw Exception("Invalid or nonexistent widget.", PyExcType::kWidgetNotFound); } } else { - parent_widget = - parent_obj == Py_None - ? g_ui->screen_root_widget() - : dynamic_cast(Python::GetPyWidget(parent_obj)); + parent_widget = parent_obj == Py_None + ? g_base->ui->screen_root_widget() + : dynamic_cast( + UIV1Python::GetPyWidget(parent_obj)); if (!parent_widget) { throw Exception("Invalid or nonexistent parent widget.", PyExcType::kWidgetNotFound); @@ -711,7 +938,7 @@ auto PyColumnWidget(PyObject* self, PyObject* args, PyObject* keywds) widget = Object::New(); } - // Set applicable values ---------------------------- + // Set applicable values. if (size_obj != Py_None) { Point2D p = Python::GetPyPoint2D(size_obj); widget->SetWidth(p.x); @@ -747,10 +974,10 @@ auto PyColumnWidget(PyObject* self, PyObject* args, PyObject* keywds) widget->set_background(Python::GetPyBool(background_obj)); } if (selected_child_obj != Py_None) { - widget->SelectWidget(Python::GetPyWidget(selected_child_obj)); + widget->SelectWidget(UIV1Python::GetPyWidget(selected_child_obj)); } if (visible_child_obj != Py_None) { - widget->ShowWidget(Python::GetPyWidget(visible_child_obj)); + widget->ShowWidget(UIV1Python::GetPyWidget(visible_child_obj)); } if (selection_loops_to_parent_obj != Py_None) { widget->set_selection_loops_to_parent( @@ -765,7 +992,7 @@ auto PyColumnWidget(PyObject* self, PyObject* args, PyObject* keywds) // if making a new widget add it at the end if (edit_obj == Py_None) { - g_ui->AddWidget(widget.get(), parent_widget); + g_base->ui->AddWidget(widget.Get(), parent_widget); } return widget->NewPyRef(); @@ -773,7 +1000,41 @@ auto PyColumnWidget(PyObject* self, PyObject* args, PyObject* keywds) BA_PYTHON_CATCH; } -auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) +static PyMethodDef PyColumnWidgetDef = { + "columnwidget", // name + (PyCFunction)PyColumnWidget, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "columnwidget(edit: bauiv1.Widget | None = None,\n" + " parent: bauiv1.Widget | None = None,\n" + " size: Sequence[float] | None = None,\n" + " position: Sequence[float] | None = None,\n" + " background: bool | None = None,\n" + " selected_child: bauiv1.Widget | None = None,\n" + " visible_child: bauiv1.Widget | None = None,\n" + " single_depth: bool | None = None,\n" + " print_list_exit_instructions: bool | None = None,\n" + " left_border: float | None = None,\n" + " top_border: float | None = None,\n" + " bottom_border: float | None = None,\n" + " selection_loops_to_parent: bool | None = None,\n" + " border: float | None = None,\n" + " margin: float | None = None,\n" + " claims_left_right: bool | None = None,\n" + " claims_tab: bool | None = None) -> bauiv1.Widget\n" + "\n" + "Create or edit a column widget.\n" + "\n" + "Category: **User Interface Functions**\n" + "\n" + "Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget.", +}; + +// ---------------------------- containerwidget -------------------------------- + +static auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; PyObject* size_obj = Py_None; @@ -859,34 +1120,33 @@ auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) return nullptr; } - if (!g_logic->IsInUIContext()) - throw Exception( - "This must be called within the UI context (see ba.Context docs).", - PyExcType::kContext); - ScopedSetContext cp(g_logic->GetUIContextTarget()); + if (!g_base->CurrentContext().IsEmpty()) { + throw Exception("UI functions must be called with no context set."); + } - // grab the edited widget or create a new one --------------------- + // Grab the edited widget or create a new one. Object::Ref widget; if (edit_obj != Py_None) { - widget = dynamic_cast(Python::GetPyWidget(edit_obj)); - if (!widget.exists()) { + widget = dynamic_cast(UIV1Python::GetPyWidget(edit_obj)); + if (!widget.Exists()) { throw Exception("Invalid or nonexistent widget.", PyExcType::kWidgetNotFound); } } else { if (parent_obj == Py_None) { - BA_PRECONDITION(g_ui && g_ui->screen_root_widget() != nullptr); + BA_PRECONDITION(g_base && g_base->ui + && g_base->ui->screen_root_widget() != nullptr); } - parent_widget = - parent_obj == Py_None - ? g_ui->screen_root_widget() - : dynamic_cast(Python::GetPyWidget(parent_obj)); + parent_widget = parent_obj == Py_None + ? g_base->ui->screen_root_widget() + : dynamic_cast( + UIV1Python::GetPyWidget(parent_obj)); if (!parent_widget) { throw Exception("Invalid or nonexistent parent widget.", PyExcType::kWidgetNotFound); } widget = Object::New(); - g_ui->AddWidget(widget.get(), parent_widget); + g_base->ui->AddWidget(widget.Get(), parent_widget); } // Set applicable values. @@ -923,7 +1183,7 @@ auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) widget->SetScaleOriginStackOffset(p.x, p.y); } if (visible_child_obj != Py_None) { - widget->ShowWidget(Python::GetPyWidget(visible_child_obj)); + widget->ShowWidget(UIV1Python::GetPyWidget(visible_child_obj)); } if (color_obj != Py_None) { std::vector c = Python::GetPyFloats(color_obj); @@ -957,7 +1217,7 @@ auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) && (PyLong_AsLong(selected_child_obj) == 0)) { widget->SelectWidget(nullptr); } else { - widget->SelectWidget(Python::GetPyWidget(selected_child_obj)); + widget->SelectWidget(UIV1Python::GetPyWidget(selected_child_obj)); } } @@ -979,7 +1239,7 @@ auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) if (cancel_button_obj != Py_None) { auto* button_widget = - dynamic_cast(Python::GetPyWidget(cancel_button_obj)); + dynamic_cast(UIV1Python::GetPyWidget(cancel_button_obj)); if (!button_widget) { throw Exception("Invalid cancel_button.", PyExcType::kWidgetNotFound); } @@ -987,7 +1247,7 @@ auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) } if (start_button_obj != Py_None) { auto* button_widget = - dynamic_cast(Python::GetPyWidget(start_button_obj)); + dynamic_cast(UIV1Python::GetPyWidget(start_button_obj)); if (!button_widget) { throw Exception("Invalid start_button.", PyExcType::kWidgetNotFound); } @@ -1050,7 +1310,55 @@ auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) BA_PYTHON_CATCH; } -auto PyRowWidget(PyObject* /* self */, PyObject* args, PyObject* keywds) +static PyMethodDef PyContainerWidgetDef = { + "containerwidget", // name + (PyCFunction)PyContainerWidget, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "containerwidget(edit: bauiv1.Widget | None = None,\n" + " parent: bauiv1.Widget | None = None,\n" + " size: Sequence[float] | None = None,\n" + " position: Sequence[float] | None = None,\n" + " background: bool | None = None,\n" + " selected_child: bauiv1.Widget | None = None,\n" + " transition: str | None = None,\n" + " cancel_button: bauiv1.Widget | None = None,\n" + " start_button: bauiv1.Widget | None = None,\n" + " root_selectable: bool | None = None,\n" + " on_activate_call: Callable[[], None] | None = None,\n" + " claims_left_right: bool | None = None,\n" + " claims_tab: bool | None = None,\n" + " selection_loops: bool | None = None,\n" + " selection_loops_to_parent: bool | None = None,\n" + " scale: float | None = None,\n" + " on_outside_click_call: Callable[[], None] | None = None,\n" + " single_depth: bool | None = None,\n" + " visible_child: bauiv1.Widget | None = None,\n" + " stack_offset: Sequence[float] | None = None,\n" + " color: Sequence[float] | None = None,\n" + " on_cancel_call: Callable[[], None] | None = None,\n" + " print_list_exit_instructions: bool | None = None,\n" + " click_activate: bool | None = None,\n" + " always_highlight: bool | None = None,\n" + " selectable: bool | None = None,\n" + " scale_origin_stack_offset: Sequence[float] | None = None,\n" + " toolbar_visibility: str | None = None,\n" + " on_select_call: Callable[[], None] | None = None,\n" + " claim_outside_clicks: bool | None = None,\n" + " claims_up_down: bool | None = None) -> bauiv1.Widget\n" + "\n" + "Create or edit a container widget.\n" + "\n" + "Category: **User Interface Functions**\n" + "\n" + "Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget.", +}; + +// ------------------------------ rowwidget ------------------------------------ + +static auto PyRowWidget(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; @@ -1080,27 +1388,23 @@ auto PyRowWidget(PyObject* /* self */, PyObject* args, PyObject* keywds) &claims_tab_obj, &selection_loops_to_parent_obj)) return nullptr; - if (!g_logic->IsInUIContext()) - throw Exception( - "This must be called within the UI context (see ba.Context docs).", - PyExcType::kContext); - - // Called within the UI context (see ba.Context docs)");} - ScopedSetContext cp(g_logic->GetUIContextTarget()); + if (!g_base->CurrentContext().IsEmpty()) { + throw Exception("UI functions must be called with no context set."); + } // Grab the edited widget or create a new one. Object::Ref widget; if (edit_obj != Py_None) { - widget = dynamic_cast(Python::GetPyWidget(edit_obj)); - if (!widget.exists()) { + widget = dynamic_cast(UIV1Python::GetPyWidget(edit_obj)); + if (!widget.Exists()) { throw Exception("Invalid or nonexistent widget.", PyExcType::kWidgetNotFound); } } else { - parent_widget = - parent_obj == Py_None - ? g_ui->screen_root_widget() - : dynamic_cast(Python::GetPyWidget(parent_obj)); + parent_widget = parent_obj == Py_None + ? g_base->ui->screen_root_widget() + : dynamic_cast( + UIV1Python::GetPyWidget(parent_obj)); if (!parent_widget) { throw Exception("invalid or nonexistent parent widget.", PyExcType::kWidgetNotFound); @@ -1123,10 +1427,10 @@ auto PyRowWidget(PyObject* /* self */, PyObject* args, PyObject* keywds) widget->set_background(Python::GetPyBool(background_obj)); } if (selected_child_obj != Py_None) { - widget->SelectWidget(Python::GetPyWidget(selected_child_obj)); + widget->SelectWidget(UIV1Python::GetPyWidget(selected_child_obj)); } if (visible_child_obj != Py_None) { - widget->ShowWidget(Python::GetPyWidget(visible_child_obj)); + widget->ShowWidget(UIV1Python::GetPyWidget(visible_child_obj)); } if (claims_left_right_obj != Py_None) { widget->set_claims_left_right(Python::GetPyBool(claims_left_right_obj)); @@ -1141,7 +1445,7 @@ auto PyRowWidget(PyObject* /* self */, PyObject* args, PyObject* keywds) // If making a new widget, add it to the parent. if (edit_obj == Py_None) { - g_ui->AddWidget(widget.get(), parent_widget); + g_base->ui->AddWidget(widget.Get(), parent_widget); } return widget->NewPyRef(); @@ -1149,7 +1453,34 @@ auto PyRowWidget(PyObject* /* self */, PyObject* args, PyObject* keywds) BA_PYTHON_CATCH; } -auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) +static PyMethodDef PyRowWidgetDef = { + "rowwidget", // name + (PyCFunction)PyRowWidget, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "rowwidget(edit: bauiv1.Widget | None = None,\n" + " parent: bauiv1.Widget | None = None,\n" + " size: Sequence[float] | None = None,\n" + " position: Sequence[float] | None = None,\n" + " background: bool | None = None,\n" + " selected_child: bauiv1.Widget | None = None,\n" + " visible_child: bauiv1.Widget | None = None,\n" + " claims_left_right: bool | None = None,\n" + " claims_tab: bool | None = None,\n" + " selection_loops_to_parent: bool | None = None) -> bauiv1.Widget\n" + "\n" + "Create or edit a row widget.\n" + "\n" + "Category: **User Interface Functions**\n" + "\n" + "Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget.", +}; + +// ---------------------------- scrollwidget ----------------------------------- + +static auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; PyObject* size_obj{Py_None}; @@ -1202,26 +1533,23 @@ auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) &claims_up_down_obj, &claims_tab_obj, &autoselect_obj)) return nullptr; - if (!g_logic->IsInUIContext()) { - throw Exception( - "This must be called within the UI context (see ba.Context docs).", - PyExcType::kContext); + if (!g_base->CurrentContext().IsEmpty()) { + throw Exception("UI functions must be called with no context set."); } - ScopedSetContext cp(g_logic->GetUIContextTarget()); - // Grab the edited widget or create a new one. --------------------- + // Grab the edited widget or create a new one. Object::Ref widget; if (edit_obj != Py_None) { - widget = dynamic_cast(Python::GetPyWidget(edit_obj)); - if (!widget.exists()) { + widget = dynamic_cast(UIV1Python::GetPyWidget(edit_obj)); + if (!widget.Exists()) { throw Exception("Invalid or nonexistent edit widget.", PyExcType::kWidgetNotFound); } } else { - parent_widget = - parent_obj == Py_None - ? g_ui->screen_root_widget() - : dynamic_cast(Python::GetPyWidget(parent_obj)); + parent_widget = parent_obj == Py_None + ? g_base->ui->screen_root_widget() + : dynamic_cast( + UIV1Python::GetPyWidget(parent_obj)); if (!parent_widget) { throw Exception("Invalid or nonexistent parent widget.", PyExcType::kWidgetNotFound); @@ -1269,7 +1597,7 @@ auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) widget->set_simple_culling_v(Python::GetPyFloat(simple_culling_v_obj)); } if (selected_child_obj != Py_None) { - widget->SelectWidget(Python::GetPyWidget(selected_child_obj)); + widget->SelectWidget(UIV1Python::GetPyWidget(selected_child_obj)); } if (selection_loops_to_parent_obj != Py_None) { widget->set_selection_loops_to_parent( @@ -1290,14 +1618,49 @@ auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) // If making a new widget add it at the end. if (edit_obj == Py_None) { - g_ui->AddWidget(widget.get(), parent_widget); + g_base->ui->AddWidget(widget.Get(), parent_widget); } return widget->NewPyRef(); BA_PYTHON_CATCH; } -auto PyHScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) +static PyMethodDef PyScrollWidgetDef = { + "scrollwidget", // name + (PyCFunction)PyScrollWidget, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "scrollwidget(edit: bauiv1.Widget | None = None,\n" + " parent: bauiv1.Widget | None = None,\n" + " size: Sequence[float] | None = None,\n" + " position: Sequence[float] | None = None,\n" + " background: bool | None = None,\n" + " selected_child: bauiv1.Widget | None = None,\n" + " capture_arrows: bool = False,\n" + " on_select_call: Callable | None = None,\n" + " center_small_content: bool | None = None,\n" + " color: Sequence[float] | None = None,\n" + " highlight: bool | None = None,\n" + " border_opacity: float | None = None,\n" + " simple_culling_v: float | None = None,\n" + " selection_loops_to_parent: bool | None = None,\n" + " claims_left_right: bool | None = None,\n" + " claims_up_down: bool | None = None,\n" + " claims_tab: bool | None = None,\n" + " autoselect: bool | None = None) -> bauiv1.Widget\n" + "\n" + "Create or edit a scroll widget.\n" + "\n" + "Category: **User Interface Functions**\n" + "\n" + "Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget.", +}; + +// ---------------------------- hscrollwidget ---------------------------------- + +static auto PyHScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; @@ -1348,26 +1711,23 @@ auto PyHScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) &claims_up_down_obj, &claims_tab_obj, &autoselect_obj)) return nullptr; - if (!g_logic->IsInUIContext()) { - throw Exception( - "This must be called within the UI context (see ba.Context docs).", - PyExcType::kContext); + if (!g_base->CurrentContext().IsEmpty()) { + throw Exception("UI functions must be called with no context set."); } - ScopedSetContext cp(g_logic->GetUIContextTarget()); - // grab the edited widget or create a new one --------------------- + // Grab the edited widget or create a new one. Object::Ref widget; if (edit_obj != Py_None) { - widget = dynamic_cast(Python::GetPyWidget(edit_obj)); - if (!widget.exists()) { + widget = dynamic_cast(UIV1Python::GetPyWidget(edit_obj)); + if (!widget.Exists()) { throw Exception("Invalid or nonexistent edit widget.", PyExcType::kWidgetNotFound); } } else { - parent_widget = - parent_obj == Py_None - ? g_ui->screen_root_widget() - : dynamic_cast(Python::GetPyWidget(parent_obj)); + parent_widget = parent_obj == Py_None + ? g_base->ui->screen_root_widget() + : dynamic_cast( + UIV1Python::GetPyWidget(parent_obj)); if (!parent_widget) { throw Exception("Invalid or nonexistent parent widget.", PyExcType::kWidgetNotFound); @@ -1414,7 +1774,7 @@ auto PyHScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) widget->set_simple_culling_h(Python::GetPyFloat(simple_culling_h_obj)); } if (selected_child_obj != Py_None) { - widget->SelectWidget(Python::GetPyWidget(selected_child_obj)); + widget->SelectWidget(UIV1Python::GetPyWidget(selected_child_obj)); } if (claims_left_right_obj != Py_None) { widget->set_claims_left_right(Python::GetPyBool(claims_left_right_obj)); @@ -1431,14 +1791,47 @@ auto PyHScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) // if making a new widget add it at the end if (edit_obj == Py_None) { - g_ui->AddWidget(widget.get(), parent_widget); + g_base->ui->AddWidget(widget.Get(), parent_widget); } return widget->NewPyRef(); BA_PYTHON_CATCH; } -auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) +static PyMethodDef PyHScrollWidgetDef = { + "hscrollwidget", // name + (PyCFunction)PyHScrollWidget, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "hscrollwidget(edit: bauiv1.Widget | None = None,\n" + " parent: bauiv1.Widget | None = None,\n" + " size: Sequence[float] | None = None,\n" + " position: Sequence[float] | None = None,\n" + " background: bool | None = None,\n" + " selected_child: bauiv1.Widget | None = None,\n" + " capture_arrows: bool | None = None,\n" + " on_select_call: Callable[[], None] | None = None,\n" + " center_small_content: bool | None = None,\n" + " color: Sequence[float] | None = None,\n" + " highlight: bool | None = None,\n" + " border_opacity: float | None = None,\n" + " simple_culling_h: float | None = None,\n" + " claims_left_right: bool | None = None,\n" + " claims_up_down: bool | None = None,\n" + " claims_tab: bool | None = None) -> bauiv1.Widget\n" + "\n" + "Create or edit a horizontal scroll widget.\n" + "\n" + "Category: **User Interface Functions**\n" + "\n" + "Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget.", +}; + +// ------------------------------ textwidget ----------------------------------- + +static auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; PyObject* size_obj = Py_None; @@ -1528,35 +1921,31 @@ auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) &extra_touch_border_scale_obj, &res_scale_obj)) return nullptr; - if (!g_logic->IsInUIContext()) - throw Exception( - "This must be called within the UI context (see ba.Context docs).", - PyExcType::kContext); - // if (!g_logic->IsInUIContext()) { BA_LOG_PYTHON_TRACE("ERROR: This should be - // called within the UI context (see ba.Context docs)");} - ScopedSetContext cp(g_logic->GetUIContextTarget()); + if (!g_base->CurrentContext().IsEmpty()) { + throw Exception("UI functions must be called with no context set."); + } - // grab the edited widget or create a new one --------------------- + // Grab the edited widget or create a new one. Object::Ref widget; if (query_obj != Py_None) { - widget = dynamic_cast(Python::GetPyWidget(query_obj)); - if (!widget.exists()) { + widget = dynamic_cast(UIV1Python::GetPyWidget(query_obj)); + if (!widget.Exists()) { throw Exception("Invalid or nonexistent widget.", PyExcType::kWidgetNotFound); } return PyUnicode_FromString(widget->text_raw().c_str()); } if (edit_obj != Py_None) { - widget = dynamic_cast(Python::GetPyWidget(edit_obj)); - if (!widget.exists()) { + widget = dynamic_cast(UIV1Python::GetPyWidget(edit_obj)); + if (!widget.Exists()) { throw Exception("Invalid or nonexistent widget.", PyExcType::kWidgetNotFound); } } else { - parent_widget = - parent_obj == Py_None - ? g_ui->screen_root_widget() - : dynamic_cast(Python::GetPyWidget(parent_obj)); + parent_widget = parent_obj == Py_None + ? g_base->ui->screen_root_widget() + : dynamic_cast( + UIV1Python::GetPyWidget(parent_obj)); if (!parent_widget) { throw Exception("Invalid or nonexistent parent widget.", PyExcType::kWidgetNotFound); @@ -1576,19 +1965,16 @@ auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) } if (description_obj != Py_None) { // FIXME - compiling Lstr values to flat strings before passing them in; - // we should probably extend TextWidget to handle this internally, but - // punting on that for now.. - widget->set_description(g_logic->CompileResourceString( - Python::GetPyString(description_obj), "textwidget set desc")); + // we should probably extend TextWidget to handle this internally, but + // punting on that for now. + widget->set_description(g_base->assets->CompileResourceString( + g_base->python->GetPyLString(description_obj), "textwidget set desc")); } if (autoselect_obj != Py_None) { widget->set_auto_select(Python::GetPyBool(autoselect_obj)); } if (transition_delay_obj != Py_None) { // we accept this as seconds; widget takes milliseconds -#if BA_TEST_BUILD - g_python->TimeFormatCheck(TimeFormat::kSeconds, transition_delay_obj); -#endif widget->set_transition_delay(1000.0f * Python::GetPyFloat(transition_delay_obj)); } @@ -1631,7 +2017,7 @@ auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) } if (text_obj != Py_None) { - widget->SetText(Python::GetPyString(text_obj)); + widget->SetText(g_base->python->GetPyLString(text_obj)); } if (h_align_obj != Py_None) { std::string halign = Python::GetPyString(h_align_obj); @@ -1671,7 +2057,7 @@ auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) widget->set_scale(Python::GetPyFloat(corner_scale_obj)); } if (draw_controller_obj != Py_None) { - auto* dcw = Python::GetPyWidget(draw_controller_obj); + auto* dcw = UIV1Python::GetPyWidget(draw_controller_obj); if (!dcw) { throw Exception("Invalid or nonexistent draw-controller widget.", PyExcType::kWidgetNotFound); @@ -1713,14 +2099,67 @@ auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) // if making a new widget add it at the end if (edit_obj == Py_None) { - g_ui->AddWidget(widget.get(), parent_widget); + g_base->ui->AddWidget(widget.Get(), parent_widget); } return widget->NewPyRef(); BA_PYTHON_CATCH; } -auto PyWidgetCall(PyObject* self, PyObject* args, PyObject* keywds) +static PyMethodDef PyTextWidgetDef = { + "textwidget", // name + (PyCFunction)PyTextWidget, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "textwidget(edit: bauiv1.Widget | None = None,\n" + " parent: bauiv1.Widget | None = None,\n" + " size: Sequence[float] | None = None,\n" + " position: Sequence[float] | None = None,\n" + " text: str | bauiv1.Lstr | None = None,\n" + " v_align: str | None = None,\n" + " h_align: str | None = None,\n" + " editable: bool | None = None,\n" + " padding: float | None = None,\n" + " on_return_press_call: Callable[[], None] | None = None,\n" + " on_activate_call: Callable[[], None] | None = None,\n" + " selectable: bool | None = None,\n" + " query: bauiv1.Widget | None = None,\n" + " max_chars: int | None = None,\n" + " color: Sequence[float] | None = None,\n" + " click_activate: bool | None = None,\n" + " on_select_call: Callable[[], None] | None = None,\n" + " always_highlight: bool | None = None,\n" + " draw_controller: bauiv1.Widget | None = None,\n" + " scale: float | None = None,\n" + " corner_scale: float | None = None,\n" + " description: str | bauiv1.Lstr | None = None,\n" + " transition_delay: float | None = None,\n" + " maxwidth: float | None = None,\n" + " max_height: float | None = None,\n" + " flatness: float | None = None,\n" + " shadow: float | None = None,\n" + " autoselect: bool | None = None,\n" + " rotate: float | None = None,\n" + " enabled: bool | None = None,\n" + " force_internal_editing: bool | None = None,\n" + " always_show_carat: bool | None = None,\n" + " big: bool | None = None,\n" + " extra_touch_border_scale: float | None = None,\n" + " res_scale: float | None = None)\n" + " -> bauiv1.Widget\n" + "\n" + "Create or edit a text widget.\n" + "\n" + "Category: **User Interface Functions**\n" + "\n" + "Pass a valid existing bauiv1.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget.", +}; + +// ------------------------------- widget -------------------------------------- + +static auto PyWidgetCall(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; @@ -1753,44 +2192,41 @@ auto PyWidgetCall(PyObject* self, PyObject* args, PyObject* keywds) &show_buffer_right_obj, &autoselect_obj)) return nullptr; - if (!g_logic->IsInUIContext()) { - throw Exception( - "This must be called within the UI context (see ba.Context docs).", - PyExcType::kContext); + if (!g_base->CurrentContext().IsEmpty()) { + throw Exception("UI functions must be called with no context set."); } - ScopedSetContext cp(g_logic->GetUIContextTarget()); Widget* widget = nullptr; if (edit_obj != Py_None) { - widget = Python::GetPyWidget(edit_obj); + widget = UIV1Python::GetPyWidget(edit_obj); } if (!widget) throw Exception("Invalid or nonexistent widget passed.", PyExcType::kWidgetNotFound); if (down_widget_obj != Py_None) { - Widget* down_widget = Python::GetPyWidget(down_widget_obj); + Widget* down_widget = UIV1Python::GetPyWidget(down_widget_obj); if (!down_widget) { throw Exception("Invalid down widget.", PyExcType::kWidgetNotFound); } widget->set_down_widget(down_widget); } if (up_widget_obj != Py_None) { - Widget* up_widget = Python::GetPyWidget(up_widget_obj); + Widget* up_widget = UIV1Python::GetPyWidget(up_widget_obj); if (!up_widget) { throw Exception("Invalid up widget.", PyExcType::kWidgetNotFound); } widget->set_up_widget(up_widget); } if (left_widget_obj != Py_None) { - Widget* left_widget = Python::GetPyWidget(left_widget_obj); + Widget* left_widget = UIV1Python::GetPyWidget(left_widget_obj); if (!left_widget) { throw Exception("Invalid left widget.", PyExcType::kWidgetNotFound); } widget->set_left_widget(left_widget); } if (right_widget_obj != Py_None) { - Widget* right_widget = Python::GetPyWidget(right_widget_obj); + Widget* right_widget = UIV1Python::GetPyWidget(right_widget_obj); if (!right_widget) { throw Exception("Invalid right widget.", PyExcType::kWidgetNotFound); } @@ -1816,6 +2252,32 @@ auto PyWidgetCall(PyObject* self, PyObject* args, PyObject* keywds) BA_PYTHON_CATCH; } +static PyMethodDef PyWidgetDef = { + "widget", // name + (PyCFunction)PyWidgetCall, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "widget(edit: bauiv1.Widget | None = None,\n" + " up_widget: bauiv1.Widget | None = None,\n" + " down_widget: bauiv1.Widget | None = None,\n" + " left_widget: bauiv1.Widget | None = None,\n" + " right_widget: bauiv1.Widget | None = None,\n" + " show_buffer_top: float | None = None,\n" + " show_buffer_bottom: float | None = None,\n" + " show_buffer_left: float | None = None,\n" + " show_buffer_right: float | None = None,\n" + " autoselect: bool | None = None) -> None\n" + "\n" + "Edit common attributes of any widget.\n" + "\n" + "Category: **User Interface Functions**\n" + "\n" + "Unlike other UI calls, this can only be used to edit, not to " + "create.", +}; + +// ------------------------------- uibounds ------------------------------------ + auto PyUIBounds(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; static const char* kwlist[] = {nullptr}; @@ -1823,9 +2285,9 @@ auto PyUIBounds(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { const_cast(kwlist))) { return nullptr; } - assert(g_graphics); - // note: to be safe, we return our min guaranteed screen bounds; not our - // current (which can be bigger) + assert(g_base->graphics); + // Note: to be safe, we return our min guaranteed screen bounds; not our + // current (which can be bigger). float x = 0.5f * kBaseVirtualResX; float virtual_res_y = kBaseVirtualResY; float y = 0.5f * virtual_res_y; @@ -1833,7 +2295,25 @@ auto PyUIBounds(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_CATCH; } -auto PyFocusWindow(PyObject* self, PyObject* args, PyObject* keywds) +static PyMethodDef PyUIBoundsDef = { + "uibounds", // name + (PyCFunction)PyUIBounds, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "uibounds() -> tuple[float, float, float, float]\n" + "\n" + "(internal)\n" + "\n" + "Returns a tuple of 4 values: (x-min, x-max, y-min, y-max) " + "representing\n" + "the range of values that can be plugged into a root level\n" + "babase.ContainerWidget's stack_offset value while guaranteeing that its\n" + "center remains onscreen.", +}; + +// ----------------------------- focus_window ---------------------------------- + +static auto PyFocusWindow(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; @@ -1842,7 +2322,7 @@ auto PyFocusWindow(PyObject* self, PyObject* args, PyObject* keywds) const_cast(kwlist))) { return nullptr; } - assert(InLogicThread()); + assert(g_base->InLogicThread()); #if BA_OSTYPE_MACOS && BA_XCODE_BUILD && !BA_HEADLESS_BUILD \ && !BA_XCODE_NEW_PROJECT SDL_ericf_focus(); @@ -1852,8 +2332,22 @@ auto PyFocusWindow(PyObject* self, PyObject* args, PyObject* keywds) BA_PYTHON_CATCH; } -auto PyShowOnlineScoreUI(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { +static PyMethodDef PyFocusWindowDef = { + "focus_window", // name + (PyCFunction)PyFocusWindow, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "focus_window() -> None\n" + "\n" + "(internal)\n" + "\n" + "A workaround for some unintentional backgrounding that occurs on mac", +}; + +// ------------------------ show_online_score_ui ------------------------------- + +static auto PyShowOnlineScoreUI(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; const char* show = "general"; PyObject* game_obj = Py_None; @@ -1872,34 +2366,28 @@ auto PyShowOnlineScoreUI(PyObject* self, PyObject* args, PyObject* keywds) if (game_version_obj != Py_None) { game_version = Python::GetPyString(game_version_obj); } - g_app_flavor->PushShowOnlineScoreUICall(show, game, game_version); + g_base->app->PushShowOnlineScoreUICall(show, game, game_version); Py_RETURN_NONE; BA_PYTHON_CATCH; } -auto PyFadeScreen(PyObject* self, PyObject* args, PyObject* keywds) +static PyMethodDef PyShowOnlineScoreUIDef = { + "show_online_score_ui", // name + (PyCFunction)PyShowOnlineScoreUI, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "show_online_score_ui(show: str = 'general', game: str | None = None,\n" + " game_version: str | None = None) -> None\n" + "\n" + "(internal)", +}; + +// -------------------------------- show_ad ------------------------------------ + +static auto PyShowAd(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - - // This can only be called in the UI context. - int fade = 0; - float time = 0.25; - PyObject* endcall = nullptr; - static const char* kwlist[] = {"to", "time", "endcall", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "|pfO", - const_cast(kwlist), &fade, &time, - &endcall)) { - return nullptr; - } - g_graphics->FadeScreen(static_cast(fade), - static_cast(1000.0f * time), endcall); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyShowAd(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { - BA_PYTHON_TRY; - BA_PRECONDITION(InLogicThread()); + BA_PRECONDITION(g_base->InLogicThread()); const char* purpose; PyObject* on_completion_call_obj = Py_None; int pass_actually_showed = false; @@ -1909,27 +2397,42 @@ auto PyShowAd(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { &on_completion_call_obj, &pass_actually_showed)) { return nullptr; } - g_app_internal->SetAdCompletionCall(on_completion_call_obj, + g_base->Plus()->SetAdCompletionCall(on_completion_call_obj, static_cast(pass_actually_showed)); // In cases where we support ads, store our callback and kick one off. // We'll then fire our callback once its done. // If we *don't* support ads, just store our callback and then kick off // an ad-view-complete message ourself so the event flow is similar.. - if (g_platform->GetHasAds()) { - g_platform->ShowAd(purpose); + if (g_core->platform->GetHasAds()) { + g_core->platform->ShowAd(purpose); } else { - g_app_internal->PushAdViewComplete(purpose, false); + g_base->Plus()->PushAdViewComplete(purpose, false); } Py_RETURN_NONE; BA_PYTHON_CATCH; } +static PyMethodDef PyShowAdDef = { + "show_ad", // name + (PyCFunction)PyShowAd, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "show_ad(purpose: str,\n" + " on_completion_call: Callable[[], None] | None = None)\n" + " -> None\n" + "\n" + "(internal)", +}; + +// ------------------------------ show_ad_2 ------------------------------------ + // (same as PyShowAd but passes actually_showed arg in callback) -auto PyShowAd2(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { +static auto PyShowAd2(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { BA_PYTHON_TRY; - BA_PRECONDITION(InLogicThread()); + BA_PRECONDITION(g_base->InLogicThread()); const char* purpose; PyObject* on_completion_call_obj = Py_None; int pass_actually_showed = true; @@ -1939,23 +2442,37 @@ auto PyShowAd2(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { &on_completion_call_obj, &pass_actually_showed)) { return nullptr; } - g_app_internal->SetAdCompletionCall(on_completion_call_obj, + g_base->Plus()->SetAdCompletionCall(on_completion_call_obj, static_cast(pass_actually_showed)); // In cases where we support ads, store our callback and kick one off. // We'll then fire our callback once its done. // If we *don't* support ads, just store our callback and then kick off // an ad-view-complete message ourself so the event flow is similar.. - if (g_platform->GetHasAds()) { - g_platform->ShowAd(purpose); + if (g_core->platform->GetHasAds()) { + g_core->platform->ShowAd(purpose); } else { - g_app_internal->PushAdViewComplete(purpose, false); + g_base->Plus()->PushAdViewComplete(purpose, false); } Py_RETURN_NONE; BA_PYTHON_CATCH; } -auto PyShowAppInvite(PyObject* self, PyObject* args, PyObject* keywds) +static PyMethodDef PyShowAd2Def = { + "show_ad_2", // name + (PyCFunction)PyShowAd2, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "show_ad_2(purpose: str,\n" + " on_completion_call: Callable[[bool], None] | None = None)\n" + " -> None\n" + "\n" + "(internal)", +}; + +// --------------------------- show_app_invite --------------------------------- + +static auto PyShowAppInvite(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; std::string title; @@ -1970,108 +2487,109 @@ auto PyShowAppInvite(PyObject* self, PyObject* args, PyObject* keywds) &message_obj, &code_obj)) { return nullptr; } - title = Python::GetPyString(title_obj); - message = Python::GetPyString(message_obj); - code = Python::GetPyString(code_obj); - g_platform->AndroidShowAppInvite(title, message, code); + title = g_base->python->GetPyLString(title_obj); + message = g_base->python->GetPyLString(message_obj); + code = g_base->python->GetPyLString(code_obj); + g_core->platform->AndroidShowAppInvite(title, message, code); Py_RETURN_NONE; BA_PYTHON_CATCH; } -auto PyShowProgressBar(PyObject* self, PyObject* args, PyObject* keywds) +static PyMethodDef PyShowAppInviteDef = { + "show_app_invite", // name + (PyCFunction)PyShowAppInvite, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "show_app_invite(title: str | bauiv1.Lstr,\n" + " message: str | bauiv1.Lstr,\n" + " code: str) -> None\n" + "\n" + "(internal)\n" + "\n" + "Category: **General Utility Functions**", +}; + +// --------------------------- show_progress_bar ------------------------------- + +static auto PyShowProgressBar(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - g_graphics->EnableProgressBar(false); + g_base->graphics->EnableProgressBar(false); Py_RETURN_NONE; BA_PYTHON_CATCH; } -auto PySetPartyIconAlwaysVisible(PyObject* self, PyObject* args, +static PyMethodDef PyShowProgressBarDef = { + "show_progress_bar", // name + (PyCFunction)PyShowProgressBar, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "show_progress_bar() -> None\n" + "\n" + "(internal)\n" + "\n" + "Category: **General Utility Functions**", +}; + +// --------------------- set_party_icon_always_visible ------------------------- + +static auto PySetPartyIconAlwaysVisible(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + + int value; + static const char* kwlist[] = {"value", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", + const_cast(kwlist), &value)) { + return nullptr; + } + assert(g_base->input); + g_base->ui->root_ui()->set_always_draw_party_icon(static_cast(value)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetPartyIconAlwaysVisibleDef = { + "set_party_icon_always_visible", // name + (PyCFunction)PySetPartyIconAlwaysVisible, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_party_icon_always_visible(value: bool) -> None\n" + "\n" + "(internal)", +}; + +// ------------------------ set_party_window_open ------------------------------ + +static auto PySetPartyWindowOpen(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - int value; static const char* kwlist[] = {"value", nullptr}; if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", const_cast(kwlist), &value)) { return nullptr; } - assert(g_input); - g_ui->root_ui()->set_always_draw_party_icon(static_cast(value)); + assert(g_base->input); + g_base->ui->root_ui()->set_party_window_open(static_cast(value)); Py_RETURN_NONE; BA_PYTHON_CATCH; } -auto PyChatMessage(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - std::string message; - PyObject* message_obj; - PyObject* clients_obj = Py_None; - PyObject* sender_override_obj = Py_None; - std::string sender_override; - const std::string* sender_override_p{}; - std::vector clients; - std::vector* clients_p{}; +static PyMethodDef PySetPartyWindowOpenDef = { + "set_party_window_open", // name + (PyCFunction)PySetPartyWindowOpen, // method + METH_VARARGS | METH_KEYWORDS, // flags - static const char* kwlist[] = {"message", "clients", "sender_override", - nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|OO", - const_cast(kwlist), &message_obj, - &clients_obj, &sender_override_obj)) { - return nullptr; - } - message = Python::GetPyString(message_obj); - if (sender_override_obj != Py_None) { - sender_override = Python::GetPyString(sender_override_obj); - sender_override_p = &sender_override; - } + "set_party_window_open(value: bool) -> None\n" + "\n" + "(internal)", +}; - if (clients_obj != Py_None) { - clients = Python::GetPyInts(clients_obj); - clients_p = &clients; - } - g_logic->connections()->SendChatMessage(message, clients_p, - sender_override_p); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} +// -------------------------- get_special_widget ------------------------------- -auto PyGetChatMessages(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - - BA_PRECONDITION(InLogicThread()); - static const char* kwlist[] = {nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "", - const_cast(kwlist))) { - return nullptr; - } - PyObject* py_list = PyList_New(0); - for (auto&& i : g_logic->chat_messages()) { - PyList_Append(py_list, PyUnicode_FromString(i.c_str())); - } - return py_list; - BA_PYTHON_CATCH; -} - -auto PySetPartyWindowOpen(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { - BA_PYTHON_TRY; - int value; - static const char* kwlist[] = {"value", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", - const_cast(kwlist), &value)) { - return nullptr; - } - assert(g_input); - g_ui->root_ui()->set_party_window_open(static_cast(value)); - Py_RETURN_NONE; - BA_PYTHON_CATCH; -} - -auto PyGetSpecialWidget(PyObject* self, PyObject* args, PyObject* keywds) +static auto PyGetSpecialWidget(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; @@ -2081,8 +2599,9 @@ auto PyGetSpecialWidget(PyObject* self, PyObject* args, PyObject* keywds) const_cast(kwlist), &name)) { return nullptr; } - RootWidget* root_widget = g_ui->root_widget(); - assert(root_widget); + BA_PRECONDITION(g_base->InLogicThread()); + RootWidget* root_widget = g_base->ui->root_widget(); + BA_PRECONDITION(root_widget); Widget* w = root_widget->GetSpecialWidget(name); if (w == nullptr) { throw Exception("Invalid special widget name '" + std::string(name) + "'.", @@ -2093,17 +2612,29 @@ auto PyGetSpecialWidget(PyObject* self, PyObject* args, PyObject* keywds) BA_PYTHON_CATCH; } +static PyMethodDef PyGetSpecialWidgetDef = { + "get_special_widget", // name + (PyCFunction)PyGetSpecialWidget, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "get_special_widget(name: str) -> bauiv1.Widget\n" + "\n" + "(internal)", +}; + +// -------------------------- have_incentivized_ad ----------------------------- + // returns an extra hash value that can be incorporated into security checks; // this contains things like whether console commands have been run, etc. -auto PyHaveIncentivizedAd(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { +static auto PyHaveIncentivizedAd(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; static const char* kwlist[] = {nullptr}; if (!PyArg_ParseTupleAndKeywords(args, keywds, "", const_cast(kwlist))) { return nullptr; } - if (g_app->have_incentivized_ad) { + if (g_core->have_incentivized_ad) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; @@ -2111,30 +2642,53 @@ auto PyHaveIncentivizedAd(PyObject* self, PyObject* args, PyObject* keywds) BA_PYTHON_CATCH; } +static PyMethodDef PyHaveIncentivizedAdDef = { + "have_incentivized_ad", // name + (PyCFunction)PyHaveIncentivizedAd, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "have_incentivized_ad() -> bool\n" + "\n" + "(internal)", +}; + +// ----------------------------- can_show_ad ----------------------------------- + // this returns whether it makes sense to show an currently -auto PyCanShowAd(PyObject* self, PyObject* args, PyObject* keywds) +static auto PyCanShowAd(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - BA_PRECONDITION(InLogicThread()); - // if we've got any network connections, no ads.. + BA_PRECONDITION(g_base->InLogicThread()); + // if we've got any network connections, no ads. // (don't want to make someone on the other end wait or risk disconnecting - // them or whatnot) also disallow ads if remote apps are connected; at least - // on android ads pause our activity which disconnects the remote app.. (could - // potentially still allow on other platforms; should verify..) - if (g_logic->connections()->connection_to_host() - || g_logic->connections()->has_connection_to_clients() - || g_input->HaveRemoteAppController()) { + // them or whatnot). Also disallow ads if remote apps are connected; at least + // on Android, ads pause our activity which disconnects the remote app. + // (need to fix this). + if (g_base->app_mode->HasConnectionToHost() + || g_base->app_mode->HasConnectionToClients() + || g_base->input->HaveRemoteAppController()) { Py_RETURN_FALSE; } Py_RETURN_TRUE; // all systems go.. BA_PYTHON_CATCH; } -auto PyHasVideoAds(PyObject* self, PyObject* args, PyObject* keywds) +static PyMethodDef PyCanShowAdDef = { + "can_show_ad", // name + (PyCFunction)PyCanShowAd, // method + METH_VARARGS | METH_KEYWORDS, // flags + "can_show_ad() -> bool\n" + "\n" + "(internal)", +}; + +// ---------------------------- has_video_ads ---------------------------------- + +static auto PyHasVideoAds(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; - if (g_platform->GetHasVideoAds()) { + if (g_core->platform->GetHasVideoAds()) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; @@ -2142,7 +2696,19 @@ auto PyHasVideoAds(PyObject* self, PyObject* args, PyObject* keywds) BA_PYTHON_CATCH; } -auto PyBackPress(PyObject* self, PyObject* args, PyObject* keywds) +static PyMethodDef PyHasVideoAdsDef = { + "has_video_ads", // name + (PyCFunction)PyHasVideoAds, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "has_video_ads() -> bool\n" + "\n" + "(internal)", +}; + +// ------------------------------ back_press ----------------------------------- + +static auto PyBackPress(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; @@ -2151,12 +2717,25 @@ auto PyBackPress(PyObject* self, PyObject* args, PyObject* keywds) const_cast(kwlist))) { return nullptr; } - g_ui->PushBackButtonCall(nullptr); + g_base->ui->PushBackButtonCall(nullptr); Py_RETURN_NONE; BA_PYTHON_CATCH; } -auto PyOpenURL(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { +static PyMethodDef PyBackPressDef = { + "back_press", // name + (PyCFunction)PyBackPress, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "back_press() -> None\n" + "\n" + "(internal)", +}; + +// ------------------------------- open_url ------------------------------------ + +static auto PyOpenURL(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { BA_PYTHON_TRY; const char* address = nullptr; int force_internal{0}; @@ -2166,18 +2745,36 @@ auto PyOpenURL(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { &force_internal)) { return nullptr; } - assert(g_app_flavor); + assert(g_base->app); if (force_internal) { - g_logic->PushShowURLCall(address); + g_base->ui->ShowURL(address); } else { - g_app_flavor->PushOpenURLCall(address); + g_base->app->PushOpenURLCall(address); } Py_RETURN_NONE; BA_PYTHON_CATCH; } -auto PyOpenFileExternally(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { +static PyMethodDef PyOpenURLDef = { + "open_url", // name + (PyCFunction)PyOpenURL, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "open_url(address: str, force_internal: bool = False) -> None\n" + "\n" + "Open a provided URL.\n" + "\n" + "Category: **General Utility Functions**\n" + "\n" + "Open the provided url in a web-browser, or display the URL\n" + "string in a window if that isn't possible (or if force_internal\n" + "is True).", +}; + +// ------------------------- open_file_externally ------------------------------ + +static auto PyOpenFileExternally(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; char* path = nullptr; @@ -2186,13 +2783,27 @@ auto PyOpenFileExternally(PyObject* self, PyObject* args, PyObject* keywds) const_cast(kwlist), &path)) { return nullptr; } - g_platform->OpenFileExternally(path); + g_core->platform->OpenFileExternally(path); Py_RETURN_NONE; BA_PYTHON_CATCH; } -auto PyOpenDirExternally(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { +static PyMethodDef PyOpenFileExternallyDef = { + "open_file_externally", // name + (PyCFunction)PyOpenFileExternally, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "open_file_externally(path: str) -> None\n" + "\n" + "(internal)\n" + "\n" + "Open the provided file in the default external app.", +}; + +// -------------------------- open_dir_externally ------------------------------ + +static auto PyOpenDirExternally(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; char* path = nullptr; static const char* kwlist[] = {"path", nullptr}; @@ -2200,40 +2811,71 @@ auto PyOpenDirExternally(PyObject* self, PyObject* args, PyObject* keywds) const_cast(kwlist), &path)) { return nullptr; } - g_platform->OpenDirExternally(path); + g_core->platform->OpenDirExternally(path); Py_RETURN_NONE; BA_PYTHON_CATCH; } -auto PyConsolePrint(PyObject* self, PyObject* args) -> PyObject* { +static PyMethodDef PyOpenDirExternallyDef = { + "open_dir_externally", // name + (PyCFunction)PyOpenDirExternally, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "open_dir_externally(path: str) -> None\n" + "\n" + "(internal)\n" + "\n" + "Open the provided dir in the default external app.", +}; + +// ----------------------------- console_print --------------------------------- + +static auto PyConsolePrint(PyObject* self, PyObject* args) -> PyObject* { BA_PYTHON_TRY; -#if !BA_HEADLESS_BUILD - Py_ssize_t tuple_size = PyTuple_GET_SIZE(args); - PyObject* obj; - for (Py_ssize_t i = 0; i < tuple_size; i++) { - obj = PyTuple_GET_ITEM(args, i); - PyObject* str_obj = PyObject_Str(obj); - if (!str_obj) { - PyErr_Clear(); // In case this is caught without setting the py exc. - throw Exception(); + if (!g_core->HeadlessMode()) { + Py_ssize_t tuple_size = PyTuple_GET_SIZE(args); + PyObject* obj; + for (Py_ssize_t i = 0; i < tuple_size; i++) { + obj = PyTuple_GET_ITEM(args, i); + PyObject* str_obj = PyObject_Str(obj); + if (!str_obj) { + PyErr_Clear(); // In case this is caught without setting the py exc. + throw Exception(); + } + const char* c = PyUnicode_AsUTF8(str_obj); + g_base->PushConsolePrintCall(c); + Py_DECREF(str_obj); } - const char* c = PyUnicode_AsUTF8(str_obj); - g_logic->PushConsolePrintCall(c); - Py_DECREF(str_obj); } -#endif // !BA_HEADLESS_BUILD Py_RETURN_NONE; BA_PYTHON_CATCH; } -auto PyIsPartyIconVisible(PyObject* self, PyObject* args, PyObject* keywds) - -> PyObject* { +static PyMethodDef PyConsolePrintDef = { + "console_print", // name + PyConsolePrint, // method + METH_VARARGS, // flags + + "console_print(*args: Any) -> None\n" + "\n" + "(internal)\n" + "\n" + "Print the provided args to the game console (using str()).\n" + "For most debugging/info purposes you should just use Python's " + "standard\n" + "print, which will show up in the game console as well.", +}; + +// ------------------------ is_party_icon_visible ------------------------------ + +static auto PyIsPartyIconVisible(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; bool party_button_active = - (g_logic->connections()->GetConnectedClientCount() > 0 - || g_logic->connections()->connection_to_host() - || g_ui->root_ui()->always_draw_party_icon()); + (g_base->app_mode->HasConnectionToClients() + || g_base->app_mode->HasConnectionToHost() + || g_base->ui->root_ui()->always_draw_party_icon()); if (party_button_active) { Py_RETURN_TRUE; } else { @@ -2242,490 +2884,56 @@ auto PyIsPartyIconVisible(PyObject* self, PyObject* args, PyObject* keywds) BA_PYTHON_CATCH; } -auto PythonMethodsUI::GetMethods() -> std::vector { +static PyMethodDef PyIsPartyIconVisibleDef = { + "is_party_icon_visible", // name + (PyCFunction)PyIsPartyIconVisible, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "is_party_icon_visible() -> bool\n" + "\n" + "(internal)", +}; + +// ----------------------------------------------------------------------------- + +auto PythonMethodsUIV1::GetMethods() -> std::vector { return { - {"is_party_icon_visible", (PyCFunction)PyIsPartyIconVisible, - METH_VARARGS | METH_KEYWORDS, - "is_party_icon_visible() -> bool\n" - "\n" - "(internal)"}, - - {"console_print", PyConsolePrint, METH_VARARGS, - "console_print(*args: Any) -> None\n" - "\n" - "(internal)\n" - "\n" - "Print the provided args to the game console (using str()).\n" - "For most debugging/info purposes you should just use Python's " - "standard\n" - "print, which will show up in the game console as well."}, - - {"open_dir_externally", (PyCFunction)PyOpenDirExternally, - METH_VARARGS | METH_KEYWORDS, - "open_dir_externally(path: str) -> None\n" - "\n" - "(internal)\n" - "\n" - "Open the provided dir in the default external app."}, - - {"open_file_externally", (PyCFunction)PyOpenFileExternally, - METH_VARARGS | METH_KEYWORDS, - "open_file_externally(path: str) -> None\n" - "\n" - "(internal)\n" - "\n" - "Open the provided file in the default external app."}, - - {"open_url", (PyCFunction)PyOpenURL, METH_VARARGS | METH_KEYWORDS, - "open_url(address: str, force_internal: bool = False) -> None\n" - "\n" - "Open a provided URL.\n" - "\n" - "Category: **General Utility Functions**\n" - "\n" - "Open the provided url in a web-browser, or display the URL\n" - "string in a window if that isn't possible (or if force_internal\n" - "is True).\n"}, - - {"back_press", (PyCFunction)PyBackPress, METH_VARARGS | METH_KEYWORDS, - "back_press() -> None\n" - "\n" - "(internal)"}, - - {"has_video_ads", (PyCFunction)PyHasVideoAds, - METH_VARARGS | METH_KEYWORDS, - "has_video_ads() -> bool\n" - "\n" - "(internal)"}, - - {"can_show_ad", (PyCFunction)PyCanShowAd, METH_VARARGS | METH_KEYWORDS, - "can_show_ad() -> bool\n" - "\n" - "(internal)"}, - - {"have_incentivized_ad", (PyCFunction)PyHaveIncentivizedAd, - METH_VARARGS | METH_KEYWORDS, - "have_incentivized_ad() -> bool\n" - "\n" - "(internal)"}, - - {"get_special_widget", (PyCFunction)PyGetSpecialWidget, - METH_VARARGS | METH_KEYWORDS, - "get_special_widget(name: str) -> Widget\n" - "\n" - "(internal)"}, - - {"set_party_window_open", (PyCFunction)PySetPartyWindowOpen, - METH_VARARGS | METH_KEYWORDS, - "set_party_window_open(value: bool) -> None\n" - "\n" - "(internal)"}, - - {"get_chat_messages", (PyCFunction)PyGetChatMessages, - METH_VARARGS | METH_KEYWORDS, - "get_chat_messages() -> list[str]\n" - "\n" - "(internal)"}, - - {"chatmessage", (PyCFunction)PyChatMessage, METH_VARARGS | METH_KEYWORDS, - "chatmessage(message: str | ba.Lstr,\n" - " clients: Sequence[int] | None = None,\n" - " sender_override: str | None = None) -> None\n" - "\n" - "(internal)"}, - - {"set_party_icon_always_visible", - (PyCFunction)PySetPartyIconAlwaysVisible, METH_VARARGS | METH_KEYWORDS, - "set_party_icon_always_visible(value: bool) -> None\n" - "\n" - "(internal)"}, - - {"show_progress_bar", (PyCFunction)PyShowProgressBar, - METH_VARARGS | METH_KEYWORDS, - "show_progress_bar() -> None\n" - "\n" - "(internal)\n" - "\n" - "Category: **General Utility Functions**"}, - - {"show_app_invite", (PyCFunction)PyShowAppInvite, - METH_VARARGS | METH_KEYWORDS, - "show_app_invite(title: str | ba.Lstr,\n" - " message: str | ba.Lstr,\n" - " code: str) -> None\n" - "\n" - "(internal)\n" - "\n" - "Category: **General Utility Functions**"}, - - {"show_ad", (PyCFunction)PyShowAd, METH_VARARGS | METH_KEYWORDS, - "show_ad(purpose: str,\n" - " on_completion_call: Callable[[], None] | None = None)\n" - " -> None\n" - "\n" - "(internal)"}, - - {"show_ad_2", (PyCFunction)PyShowAd2, METH_VARARGS | METH_KEYWORDS, - "show_ad_2(purpose: str,\n" - " on_completion_call: Callable[[bool], None] | None = None)\n" - " -> None\n" - "\n" - "(internal)"}, - - {"fade_screen", (PyCFunction)PyFadeScreen, METH_VARARGS | METH_KEYWORDS, - "fade_screen(to: int = 0, time: float = 0.25,\n" - " endcall: Callable[[], None] | None = None) -> None\n" - "\n" - "(internal)\n" - "\n" - "Fade the local game screen in our out from black over a duration of\n" - "time. if \"to\" is 0, the screen will fade out to black. Otherwise " - "it\n" - "will fade in from black. If endcall is provided, it will be run after " - "a\n" - "completely faded frame is drawn."}, - - {"show_online_score_ui", (PyCFunction)PyShowOnlineScoreUI, - METH_VARARGS | METH_KEYWORDS, - "show_online_score_ui(show: str = 'general', game: str | None = None,\n" - " game_version: str | None = None) -> None\n" - "\n" - "(internal)"}, - - {"focus_window", (PyCFunction)PyFocusWindow, METH_VARARGS | METH_KEYWORDS, - "focus_window() -> None\n" - "\n" - "(internal)\n" - "\n" - "A workaround for some unintentional backgrounding that occurs on mac"}, - - {"uibounds", (PyCFunction)PyUIBounds, METH_VARARGS | METH_KEYWORDS, - "uibounds() -> tuple[float, float, float, float]\n" - "\n" - "(internal)\n" - "\n" - "Returns a tuple of 4 values: (x-min, x-max, y-min, y-max) " - "representing\n" - "the range of values that can be plugged into a root level\n" - "ba.ContainerWidget's stack_offset value while guaranteeing that its\n" - "center remains onscreen.\n"}, - - {"buttonwidget", (PyCFunction)PyButtonWidget, - METH_VARARGS | METH_KEYWORDS, - "buttonwidget(edit: ba.Widget | None = None,\n" - " parent: ba.Widget | None = None,\n" - " size: Sequence[float] | None = None,\n" - " position: Sequence[float] | None = None,\n" - " on_activate_call: Callable | None = None,\n" - " label: str | ba.Lstr | None = None,\n" - " color: Sequence[float] | None = None,\n" - " down_widget: ba.Widget | None = None,\n" - " up_widget: ba.Widget | None = None,\n" - " left_widget: ba.Widget | None = None,\n" - " right_widget: ba.Widget | None = None,\n" - " texture: ba.Texture | None = None,\n" - " text_scale: float | None = None,\n" - " textcolor: Sequence[float] | None = None,\n" - " enable_sound: bool | None = None,\n" - " model_transparent: ba.Model | None = None,\n" - " model_opaque: ba.Model | None = None,\n" - " repeat: bool | None = None,\n" - " scale: float | None = None,\n" - " transition_delay: float | None = None,\n" - " on_select_call: Callable | None = None,\n" - " button_type: str | None = None,\n" - " extra_touch_border_scale: float | None = None,\n" - " selectable: bool | None = None,\n" - " show_buffer_top: float | None = None,\n" - " icon: ba.Texture | None = None,\n" - " iconscale: float | None = None,\n" - " icon_tint: float | None = None,\n" - " icon_color: Sequence[float] | None = None,\n" - " autoselect: bool | None = None,\n" - " mask_texture: ba.Texture | None = None,\n" - " tint_texture: ba.Texture | None = None,\n" - " tint_color: Sequence[float] | None = None,\n" - " tint2_color: Sequence[float] | None = None,\n" - " text_flatness: float | None = None,\n" - " text_res_scale: float | None = None,\n" - " enabled: bool | None = None) -> ba.Widget\n" - "\n" - "Create or edit a button widget.\n" - "\n" - "Category: **User Interface Functions**\n" - "\n" - "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" - "a new one is created and returned. Arguments that are not set to None\n" - "are applied to the Widget."}, - - {"checkboxwidget", (PyCFunction)PyCheckBoxWidget, - METH_VARARGS | METH_KEYWORDS, - "checkboxwidget(edit: ba.Widget | None = None,\n" - " parent: ba.Widget | None = None,\n" - " size: Sequence[float] | None = None,\n" - " position: Sequence[float] | None = None,\n" - " text: str | ba.Lstr | None = None,\n" - " value: bool | None = None,\n" - " on_value_change_call: Callable[[bool], None] | None = None,\n" - " on_select_call: Callable[[], None] | None = None,\n" - " text_scale: float | None = None,\n" - " textcolor: Sequence[float] | None = None,\n" - " scale: float | None = None,\n" - " is_radio_button: bool | None = None,\n" - " maxwidth: float | None = None,\n" - " autoselect: bool | None = None,\n" - " color: Sequence[float] | None = None) -> ba.Widget\n" - "\n" - "Create or edit a check-box widget.\n" - "\n" - "Category: **User Interface Functions**\n" - "\n" - "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" - "a new one is created and returned. Arguments that are not set to None\n" - "are applied to the Widget."}, - - {"imagewidget", (PyCFunction)PyImageWidget, METH_VARARGS | METH_KEYWORDS, - "imagewidget(edit: ba.Widget | None = None,\n" - " parent: ba.Widget | None = None,\n" - " size: Sequence[float] | None = None,\n" - " position: Sequence[float] | None = None,\n" - " color: Sequence[float] | None = None,\n" - " texture: ba.Texture | None = None,\n" - " opacity: float | None = None,\n" - " model_transparent: ba.Model | None = None,\n" - " model_opaque: ba.Model | None = None,\n" - " has_alpha_channel: bool = True,\n" - " tint_texture: ba.Texture | None = None,\n" - " tint_color: Sequence[float] | None = None,\n" - " transition_delay: float | None = None,\n" - " draw_controller: ba.Widget | None = None,\n" - " tint2_color: Sequence[float] | None = None,\n" - " tilt_scale: float | None = None,\n" - " mask_texture: ba.Texture | None = None,\n" - " radial_amount: float | None = None)\n" - " -> ba.Widget\n" - "\n" - "Create or edit an image widget.\n" - "\n" - "Category: **User Interface Functions**\n" - "\n" - "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" - "a new one is created and returned. Arguments that are not set to None\n" - "are applied to the Widget."}, - - {"columnwidget", (PyCFunction)PyColumnWidget, - METH_VARARGS | METH_KEYWORDS, - "columnwidget(edit: ba.Widget | None = None,\n" - " parent: ba.Widget | None = None,\n" - " size: Sequence[float] | None = None,\n" - " position: Sequence[float] | None = None,\n" - " background: bool | None = None,\n" - " selected_child: ba.Widget | None = None,\n" - " visible_child: ba.Widget | None = None,\n" - " single_depth: bool | None = None,\n" - " print_list_exit_instructions: bool | None = None,\n" - " left_border: float | None = None,\n" - " top_border: float | None = None,\n" - " bottom_border: float | None = None,\n" - " selection_loops_to_parent: bool | None = None,\n" - " border: float | None = None,\n" - " margin: float | None = None,\n" - " claims_left_right: bool | None = None,\n" - " claims_tab: bool | None = None) -> ba.Widget\n" - "\n" - "Create or edit a column widget.\n" - "\n" - "Category: **User Interface Functions**\n" - "\n" - "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" - "a new one is created and returned. Arguments that are not set to None\n" - "are applied to the Widget."}, - - {"containerwidget", (PyCFunction)PyContainerWidget, - METH_VARARGS | METH_KEYWORDS, - "containerwidget(edit: ba.Widget | None = None,\n" - " parent: ba.Widget | None = None,\n" - " size: Sequence[float] | None = None,\n" - " position: Sequence[float] | None = None,\n" - " background: bool | None = None,\n" - " selected_child: ba.Widget | None = None,\n" - " transition: str | None = None,\n" - " cancel_button: ba.Widget | None = None,\n" - " start_button: ba.Widget | None = None,\n" - " root_selectable: bool | None = None,\n" - " on_activate_call: Callable[[], None] | None = None,\n" - " claims_left_right: bool | None = None,\n" - " claims_tab: bool | None = None,\n" - " selection_loops: bool | None = None,\n" - " selection_loops_to_parent: bool | None = None,\n" - " scale: float | None = None,\n" - " on_outside_click_call: Callable[[], None] | None = None,\n" - " single_depth: bool | None = None,\n" - " visible_child: ba.Widget | None = None,\n" - " stack_offset: Sequence[float] | None = None,\n" - " color: Sequence[float] | None = None,\n" - " on_cancel_call: Callable[[], None] | None = None,\n" - " print_list_exit_instructions: bool | None = None,\n" - " click_activate: bool | None = None,\n" - " always_highlight: bool | None = None,\n" - " selectable: bool | None = None,\n" - " scale_origin_stack_offset: Sequence[float] | None = None,\n" - " toolbar_visibility: str | None = None,\n" - " on_select_call: Callable[[], None] | None = None,\n" - " claim_outside_clicks: bool | None = None,\n" - " claims_up_down: bool | None = None) -> ba.Widget\n" - "\n" - "Create or edit a container widget.\n" - "\n" - "Category: **User Interface Functions**\n" - "\n" - "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" - "a new one is created and returned. Arguments that are not set to None\n" - "are applied to the Widget."}, - - {"rowwidget", (PyCFunction)PyRowWidget, METH_VARARGS | METH_KEYWORDS, - "rowwidget(edit: ba.Widget | None = None,\n" - " parent: ba.Widget | None = None,\n" - " size: Sequence[float] | None = None,\n" - " position: Sequence[float] | None = None,\n" - " background: bool | None = None,\n" - " selected_child: ba.Widget | None = None,\n" - " visible_child: ba.Widget | None = None,\n" - " claims_left_right: bool | None = None,\n" - " claims_tab: bool | None = None,\n" - " selection_loops_to_parent: bool | None = None) -> ba.Widget\n" - "\n" - "Create or edit a row widget.\n" - "\n" - "Category: **User Interface Functions**\n" - "\n" - "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" - "a new one is created and returned. Arguments that are not set to None\n" - "are applied to the Widget."}, - - {"scrollwidget", (PyCFunction)PyScrollWidget, - METH_VARARGS | METH_KEYWORDS, - "scrollwidget(edit: ba.Widget | None = None,\n" - " parent: ba.Widget | None = None,\n" - " size: Sequence[float] | None = None,\n" - " position: Sequence[float] | None = None,\n" - " background: bool | None = None,\n" - " selected_child: ba.Widget | None = None,\n" - " capture_arrows: bool = False,\n" - " on_select_call: Callable | None = None,\n" - " center_small_content: bool | None = None,\n" - " color: Sequence[float] | None = None,\n" - " highlight: bool | None = None,\n" - " border_opacity: float | None = None,\n" - " simple_culling_v: float | None = None,\n" - " selection_loops_to_parent: bool | None = None,\n" - " claims_left_right: bool | None = None,\n" - " claims_up_down: bool | None = None,\n" - " claims_tab: bool | None = None,\n" - " autoselect: bool | None = None) -> ba.Widget\n" - "\n" - "Create or edit a scroll widget.\n" - "\n" - "Category: **User Interface Functions**\n" - "\n" - "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" - "a new one is created and returned. Arguments that are not set to None\n" - "are applied to the Widget."}, - - {"hscrollwidget", (PyCFunction)PyHScrollWidget, - METH_VARARGS | METH_KEYWORDS, - "hscrollwidget(edit: ba.Widget | None = None,\n" - " parent: ba.Widget | None = None,\n" - " size: Sequence[float] | None = None,\n" - " position: Sequence[float] | None = None,\n" - " background: bool | None = None,\n" - " selected_child: ba.Widget | None = None,\n" - " capture_arrows: bool | None = None,\n" - " on_select_call: Callable[[], None] | None = None,\n" - " center_small_content: bool | None = None,\n" - " color: Sequence[float] | None = None,\n" - " highlight: bool | None = None,\n" - " border_opacity: float | None = None,\n" - " simple_culling_h: float | None = None,\n" - " claims_left_right: bool | None = None,\n" - " claims_up_down: bool | None = None,\n" - " claims_tab: bool | None = None) -> ba.Widget\n" - "\n" - "Create or edit a horizontal scroll widget.\n" - "\n" - "Category: **User Interface Functions**\n" - "\n" - "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" - "a new one is created and returned. Arguments that are not set to None\n" - "are applied to the Widget."}, - - {"textwidget", (PyCFunction)PyTextWidget, METH_VARARGS | METH_KEYWORDS, - "textwidget(edit: ba.Widget | None = None,\n" - " parent: ba.Widget | None = None,\n" - " size: Sequence[float] | None = None,\n" - " position: Sequence[float] | None = None,\n" - " text: str | ba.Lstr | None = None,\n" - " v_align: str | None = None,\n" - " h_align: str | None = None,\n" - " editable: bool | None = None,\n" - " padding: float | None = None,\n" - " on_return_press_call: Callable[[], None] | None = None,\n" - " on_activate_call: Callable[[], None] | None = None,\n" - " selectable: bool | None = None,\n" - " query: ba.Widget | None = None,\n" - " max_chars: int | None = None,\n" - " color: Sequence[float] | None = None,\n" - " click_activate: bool | None = None,\n" - " on_select_call: Callable[[], None] | None = None,\n" - " always_highlight: bool | None = None,\n" - " draw_controller: ba.Widget | None = None,\n" - " scale: float | None = None,\n" - " corner_scale: float | None = None,\n" - " description: str | ba.Lstr | None = None,\n" - " transition_delay: float | None = None,\n" - " maxwidth: float | None = None,\n" - " max_height: float | None = None,\n" - " flatness: float | None = None,\n" - " shadow: float | None = None,\n" - " autoselect: bool | None = None,\n" - " rotate: float | None = None,\n" - " enabled: bool | None = None,\n" - " force_internal_editing: bool | None = None,\n" - " always_show_carat: bool | None = None,\n" - " big: bool | None = None,\n" - " extra_touch_border_scale: float | None = None,\n" - " res_scale: float | None = None)\n" - " -> Widget\n" - "\n" - "Create or edit a text widget.\n" - "\n" - "Category: **User Interface Functions**\n" - "\n" - "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" - "a new one is created and returned. Arguments that are not set to None\n" - "are applied to the Widget."}, - - {"widget", (PyCFunction)PyWidgetCall, METH_VARARGS | METH_KEYWORDS, - "widget(edit: ba.Widget | None = None,\n" - " up_widget: ba.Widget | None = None,\n" - " down_widget: ba.Widget | None = None,\n" - " left_widget: ba.Widget | None = None,\n" - " right_widget: ba.Widget | None = None,\n" - " show_buffer_top: float | None = None,\n" - " show_buffer_bottom: float | None = None,\n" - " show_buffer_left: float | None = None,\n" - " show_buffer_right: float | None = None,\n" - " autoselect: bool | None = None) -> None\n" - "\n" - "Edit common attributes of any widget.\n" - "\n" - "Category: **User Interface Functions**\n" - "\n" - "Unlike other UI calls, this can only be used to edit, not to " - "create.\n"}, + PyGetQRCodeTextureDef, + PyIsPartyIconVisibleDef, + PyConsolePrintDef, + PyOpenDirExternallyDef, + PyOpenFileExternallyDef, + PyOpenURLDef, + PyBackPressDef, + PyHasVideoAdsDef, + PyCanShowAdDef, + PyHaveIncentivizedAdDef, + PyGetSpecialWidgetDef, + PySetPartyWindowOpenDef, + PySetPartyIconAlwaysVisibleDef, + PyShowProgressBarDef, + PyShowAppInviteDef, + PyShowAdDef, + PyShowAd2Def, + PyShowOnlineScoreUIDef, + PyFocusWindowDef, + PyButtonWidgetDef, + PyCheckBoxWidgetDef, + PyImageWidgetDef, + PyColumnWidgetDef, + PyContainerWidgetDef, + PyRowWidgetDef, + PyScrollWidgetDef, + PyHScrollWidgetDef, + PyTextWidgetDef, + PyWidgetDef, + PyUIBoundsDef, + PyGetSoundDef, + PyGetTextureDef, + PyGetMeshDef, }; } #pragma clang diagnostic pop -} // namespace ballistica +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.h b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.h new file mode 100644 index 00000000..3c50f6c6 --- /dev/null +++ b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.h @@ -0,0 +1,20 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_V1_PYTHON_METHODS_PYTHON_METHODS_UI_V1_H_ +#define BALLISTICA_UI_V1_PYTHON_METHODS_PYTHON_METHODS_UI_V1_H_ + +#include + +#include "ballistica/shared/ballistica.h" + +namespace ballistica::ui_v1 { + +/// UI related individual python methods for our module. +class PythonMethodsUIV1 { + public: + static auto GetMethods() -> std::vector; +}; + +} // namespace ballistica::ui_v1 + +#endif // BALLISTICA_UI_V1_PYTHON_METHODS_PYTHON_METHODS_UI_V1_H_ diff --git a/src/ballistica/ui_v1/python/ui_v1_python.cc b/src/ballistica/ui_v1/python/ui_v1_python.cc new file mode 100644 index 00000000..88e86d70 --- /dev/null +++ b/src/ballistica/ui_v1/python/ui_v1_python.cc @@ -0,0 +1,119 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui_v1/python/ui_v1_python.h" + +#include "ballistica/base/assets/assets.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/input/device/input_device.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/python/python_command.h" +#include "ballistica/shared/python/python_module_builder.h" +#include "ballistica/ui_v1/python/class/python_class_ui_mesh.h" +#include "ballistica/ui_v1/python/class/python_class_ui_sound.h" +#include "ballistica/ui_v1/python/class/python_class_ui_texture.h" +#include "ballistica/ui_v1/python/class/python_class_widget.h" +#include "ballistica/ui_v1/python/methods/python_methods_ui_v1.h" +#include "ballistica/ui_v1/widget/text_widget.h" + +namespace ballistica::ui_v1 { + +UIV1Python::UIV1Python() = default; + +// Declare a plain c PyInit_XXX function for our Python module; +// this is how Python inits our binary module (and by extension, our +// entire feature-set). +extern "C" auto PyInit__bauiv1() -> PyObject* { + auto* builder = + new PythonModuleBuilder("_bauiv1", + { + PythonMethodsUIV1::GetMethods(), + }, + [](PyObject* module) -> int { + BA_PYTHON_TRY; + UIV1FeatureSet::OnModuleExec(module); + return 0; + BA_PYTHON_INT_CATCH; + }); + return builder->Build(); +} + +void UIV1Python::AddPythonClasses(PyObject* module) { + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); + PythonModuleBuilder::AddClass(module); +} + +void UIV1Python::ImportPythonObjs() { + // Import and grab all our objs_. + // This code blob expects 'ObjID' and 'objs_' to be defined. +#include "ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc" +} + +auto UIV1Python::GetPyWidget(PyObject* o) -> Widget* { + assert(Python::HaveGIL()); + assert(o != nullptr); + + if (PythonClassWidget::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetWidget(); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get widget from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} +void UIV1Python::ShowURL(const std::string& url) { + g_base->logic->event_loop()->PushCall([this, url] { + assert(g_base->InLogicThread()); + if (objs().Exists(ObjID::kShowURLWindowCall)) { + base::ScopedSetContext ssc(nullptr); + PythonRef args(Py_BuildValue("(s)", url.c_str()), PythonRef::kSteal); + objs().Get(ObjID::kShowURLWindowCall).Call(args); + } else { + Log(LogLevel::kError, "ShowURLWindowCall nonexistent."); + } + }); +} + +void UIV1Python::HandleDeviceMenuPress(base::InputDevice* device) { + assert(device); + assert(objs().Exists(ObjID::kDeviceMenuPressCall)); + + // Ignore if input is locked... + if (g_base->input->IsInputLocked()) { + return; + } + base::ScopedSetContext ssc(nullptr); + PythonRef args(device ? Py_BuildValue("(i)", device->index()) + : Py_BuildValue("(O)", Py_None), + PythonRef::kSteal); + { + Python::ScopedCallLabel label("handleDeviceMenuPress"); + objs().Get(ObjID::kDeviceMenuPressCall).Call(args); + } +} + +void UIV1Python::LaunchStringEdit(TextWidget* w) { + assert(g_base->InLogicThread()); + BA_PRECONDITION(w); + + base::ScopedSetContext ssc(nullptr); + g_base->audio->PlaySound(g_base->assets->SysSound(base::SysSoundID::kSwish)); + + // Gotta run this in the next cycle. + PythonRef args(Py_BuildValue("(Osi)", w->BorrowPyRef(), + w->description().c_str(), w->max_chars()), + PythonRef::kSteal); + Object::New( + objs().Get(ObjID::kOnScreenKeyboardClass)) + ->Schedule(args); +} + +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui_v1/python/ui_v1_python.h b/src/ballistica/ui_v1/python/ui_v1_python.h new file mode 100644 index 00000000..a0ea33ff --- /dev/null +++ b/src/ballistica/ui_v1/python/ui_v1_python.h @@ -0,0 +1,51 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_V1_PYTHON_UI_V1_PYTHON_H_ +#define BALLISTICA_UI_V1_PYTHON_UI_V1_PYTHON_H_ + +#include "ballistica/base/base.h" +#include "ballistica/shared/python/python_object_set.h" +#include "ballistica/ui_v1/ui_v1.h" + +namespace ballistica::ui_v1 { + +/// General Python support class for UIV1. +class UIV1Python { + public: + UIV1Python(); + + void AddPythonClasses(PyObject* module); + void ImportPythonObjs(); + + void LaunchStringEdit(TextWidget* w); + void HandleDeviceMenuPress(base::InputDevice* device); + void ShowURL(const std::string& url); + + static auto GetPyWidget(PyObject* o) -> Widget*; + + /// Specific Python objects we hold in objs_. + enum class ObjID { + kOnScreenKeyboardClass, + kTicketIconPressCall, + kTrophyIconPressCall, + kLevelIconPressCall, + kCoinIconPressCall, + kEmptyCall, + kBackButtonPressCall, + kFriendsButtonPressCall, + kPartyIconActivateCall, + kQuitWindowCall, + kDeviceMenuPressCall, + kShowURLWindowCall, + kLast // Sentinel; must be at end. + }; + + const auto& objs() { return objs_; } + + private: + PythonObjectSet objs_; +}; + +} // namespace ballistica::ui_v1 + +#endif // BALLISTICA_UI_V1_PYTHON_UI_V1_PYTHON_H_ diff --git a/src/ballistica/ui/root_ui.cc b/src/ballistica/ui_v1/support/root_ui.cc similarity index 67% rename from src/ballistica/ui/root_ui.cc rename to src/ballistica/ui_v1/support/root_ui.cc index 1890b316..d93bfbf1 100644 --- a/src/ballistica/ui/root_ui.cc +++ b/src/ballistica/ui_v1/support/root_ui.cc @@ -1,19 +1,17 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/ui/root_ui.h" +#include "ballistica/ui_v1/support/root_ui.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/input/device/keyboard_input.h" -#include "ballistica/input/device/touch_input.h" -#include "ballistica/input/input.h" -#include "ballistica/logic/connection/connection_set.h" -#include "ballistica/logic/logic.h" -#include "ballistica/logic/session/host_session.h" -#include "ballistica/python/python.h" -#include "ballistica/ui/ui.h" -#include "ballistica/ui/widget/container_widget.h" +#include "ballistica/base/app/app_mode.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/input/device/keyboard_input.h" +#include "ballistica/base/input/device/touch_input.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/ui_v1/python/ui_v1_python.h" +#include "ballistica/ui_v1/widget/container_widget.h" -namespace ballistica { +namespace ballistica::ui_v1 { // Phasing these out; replaced by buttons in our rootwidget. #define DO_OLD_MENU_PARTY_BUTTONS (!BA_TOOLBAR_TEST) @@ -23,7 +21,7 @@ const float kMenuButtonDrawDepth = -0.07f; RootUI::RootUI() { float base_scale; - switch (g_ui->scale()) { + switch (g_base->ui->scale()) { case UIScale::kLarge: base_scale = 1.0f; break; @@ -39,56 +37,57 @@ RootUI::RootUI() { } menu_button_size_ = kMenuButtonSize * base_scale; } + RootUI::~RootUI() = default; void RootUI::TogglePartyWindowKeyPress() { - assert(InLogicThread()); - if (g_logic->GetPartySize() > 1 - || g_logic->connections()->connection_to_host() - || always_draw_party_icon()) { + assert(g_base->InLogicThread()); + if (g_base->app_mode->GetPartySize() > 1 + || g_base->app_mode->HasConnectionToHost() || always_draw_party_icon()) { ActivatePartyIcon(); } } void RootUI::ActivatePartyIcon() const { - assert(InLogicThread()); - ScopedSetContext cp(g_logic->GetUIContext()); + assert(g_base->InLogicThread()); + base::ScopedSetContext ssc(nullptr); // Originate from center of party icon. If menu button is shown, it is to the // left of that. - float icon_pos_h = - g_graphics->screen_virtual_width() * 0.5f - menu_button_size_ * 0.5f; - float icon_pos_v = - g_graphics->screen_virtual_height() * 0.5f - menu_button_size_ * 0.5f; - bool menu_active = !(g_ui && g_ui->screen_root_widget() - && g_ui->screen_root_widget()->HasChildren()); + float icon_pos_h = g_base->graphics->screen_virtual_width() * 0.5f + - menu_button_size_ * 0.5f; + float icon_pos_v = g_base->graphics->screen_virtual_height() * 0.5f + - menu_button_size_ * 0.5f; + bool menu_active = !(g_base->ui && g_base->ui->screen_root_widget() + && g_base->ui->screen_root_widget()->HasChildren()); if (menu_active) { icon_pos_h -= menu_button_size_; } - g_python->obj(Python::ObjID::kPartyIconActivateCall) + g_ui_v1->python->objs() + .Get(UIV1Python::ObjID::kPartyIconActivateCall) .Call(Vector2f(icon_pos_h, icon_pos_v)); } auto RootUI::HandleMouseButtonDown(float x, float y) -> bool { // Whether the menu button is visible/active. - bool menu_active = !(g_ui && g_ui->screen_root_widget() - && g_ui->screen_root_widget()->HasChildren()); + bool menu_active = !(g_base->ui && g_base->ui->screen_root_widget() + && g_base->ui->screen_root_widget()->HasChildren()); // Handle party button presses (need to do this before UI since it // floats over the top). Party button is to the left of menu button. if (explicit_bool(DO_OLD_MENU_PARTY_BUTTONS)) { - bool party_button_active = - (!party_window_open_ - && (g_logic->connections()->GetConnectedClientCount() > 0 - || g_logic->connections()->connection_to_host() - || always_draw_party_icon())); + bool party_button_active = (!party_window_open_ + && (g_base->app_mode->HasConnectionToClients() + || g_base->app_mode->HasConnectionToHost() + || always_draw_party_icon())); float party_button_left = menu_active ? 2 * menu_button_size_ : menu_button_size_; float party_button_right = menu_active ? menu_button_size_ : 0; if (party_button_active - && (g_graphics->screen_virtual_width() - x < party_button_left) - && (g_graphics->screen_virtual_width() - x >= party_button_right) - && (g_graphics->screen_virtual_height() - y < menu_button_size_)) { + && (g_base->graphics->screen_virtual_width() - x < party_button_left) + && (g_base->graphics->screen_virtual_width() - x >= party_button_right) + && (g_base->graphics->screen_virtual_height() - y + < menu_button_size_)) { ActivatePartyIcon(); return true; } @@ -96,8 +95,9 @@ auto RootUI::HandleMouseButtonDown(float x, float y) -> bool { // Menu button. if (explicit_bool(DO_OLD_MENU_PARTY_BUTTONS)) { if (menu_active - && (g_graphics->screen_virtual_width() - x < menu_button_size_) - && (g_graphics->screen_virtual_height() - y < menu_button_size_)) { + && (g_base->graphics->screen_virtual_width() - x < menu_button_size_) + && (g_base->graphics->screen_virtual_height() - y + < menu_button_size_)) { menu_button_pressed_ = true; menu_button_hover_ = true; return true; @@ -114,18 +114,19 @@ void RootUI::HandleMouseButtonUp(float x, float y) { // If we've got a touch input, bring the menu up in its name.. // otherwise go with keyboard input. - InputDevice* input_device = nullptr; - auto* touch_input = g_input->touch_input(); - auto* keyboard_input = g_input->keyboard_input(); + base::InputDevice* input_device = nullptr; + auto* touch_input = g_base->input->touch_input(); + auto* keyboard_input = g_base->input->keyboard_input(); if (touch_input) { input_device = touch_input; } else if (keyboard_input) { input_device = keyboard_input; } - if ((g_graphics->screen_virtual_width() - x < menu_button_size_) - && (g_graphics->screen_virtual_height() - y < menu_button_size_)) { - g_ui->PushMainMenuPressCall(input_device); - last_menu_button_press_time_ = GetRealTime(); + if ((g_base->graphics->screen_virtual_width() - x < menu_button_size_) + && (g_base->graphics->screen_virtual_height() - y + < menu_button_size_)) { + g_base->ui->PushMainMenuPressCall(input_device); + last_menu_button_press_time_ = g_core->GetAppTimeMillisecs(); } } } @@ -134,19 +135,20 @@ void RootUI::HandleMouseMotion(float x, float y) { // Menu button hover. if (menu_button_pressed_) { menu_button_hover_ = - ((g_graphics->screen_virtual_width() - x < menu_button_size_) - && (g_graphics->screen_virtual_height() - y < menu_button_size_)); + ((g_base->graphics->screen_virtual_width() - x < menu_button_size_) + && (g_base->graphics->screen_virtual_height() - y + < menu_button_size_)); } } -void RootUI::Draw(FrameDef* frame_def) { +void RootUI::Draw(base::FrameDef* frame_def) { if (explicit_bool(DO_OLD_MENU_PARTY_BUTTONS)) { millisecs_t real_time = frame_def->real_time(); // Menu button. // Update time-dependent stuff to this point. - bool active = !(g_ui && g_ui->screen_root_widget() - && g_ui->screen_root_widget()->HasChildren()); + bool active = !(g_base->ui && g_base->ui->screen_root_widget() + && g_base->ui->screen_root_widget()->HasChildren()); if (real_time - menu_update_time_ > 500) { menu_update_time_ = real_time - 500; } @@ -164,24 +166,24 @@ void RootUI::Draw(FrameDef* frame_def) { if (g_buildconfig.ostype_android()) { // Draw if we have a touchscreen or are in desktop mode. - if (g_input->touch_input() == nullptr - && !g_platform->IsRunningOnDesktop()) { + if (g_base->input->touch_input() == nullptr + && !g_core->platform->IsRunningOnDesktop()) { draw_menu_button = false; } } else if (g_buildconfig.rift_build()) { - if (IsVRMode()) { + if (g_core->IsVRMode()) { draw_menu_button = false; } } if (draw_menu_button) { - SimpleComponent c(frame_def->overlay_pass()); + base::SimpleComponent c(frame_def->overlay_pass()); c.SetTransparent(true); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kMenuButton)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kMenuButton)); // Draw menu button. - float width = g_graphics->screen_virtual_width(); - float height = g_graphics->screen_virtual_height(); + float width = g_base->graphics->screen_virtual_width(); + float height = g_base->graphics->screen_virtual_height(); if ((menu_button_pressed_ && menu_button_hover_) || real_time - last_menu_button_press_time_ < 100) { c.SetColor(1, 2, 0.5f, 1); @@ -192,7 +194,7 @@ void RootUI::Draw(FrameDef* frame_def) { c.Translate(width - menu_button_size_ * 0.5f, height - menu_button_size_ * 0.38f, kMenuButtonDrawDepth); c.Scale(menu_button_size_ * 0.8f, menu_button_size_ * 0.8f); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.PopTransform(); c.Submit(); } @@ -200,17 +202,17 @@ void RootUI::Draw(FrameDef* frame_def) { // To the left of the menu button, draw our connected-players indicator // (this probably shouldn't live here). bool draw_connected_players_icon = false; - int party_size = g_logic->GetPartySize(); - bool is_host = (g_logic->connections()->connection_to_host() == nullptr); + int party_size = g_base->app_mode->GetPartySize(); + bool is_host = (!g_base->app_mode->HasConnectionToHost()); millisecs_t last_connection_to_client_join_time = - g_logic->last_connection_to_client_join_time(); + g_base->app_mode->LastClientJoinTime(); bool show_client_joined = (is_host && last_connection_to_client_join_time != 0 && real_time - last_connection_to_client_join_time < 5000); if (!party_window_open_ - && (party_size != 0 || g_logic->connections()->connection_to_host() + && (party_size != 0 || g_base->app_mode->HasConnectionToHost() || always_draw_party_icon_)) { draw_connected_players_icon = true; } @@ -219,17 +221,18 @@ void RootUI::Draw(FrameDef* frame_def) { // Flash and show a message if we're in the main menu instructing the // player to start a game. bool flash = false; - HostSession* s = g_logic->GetForegroundContext().GetHostSession(); - if (s && s->is_main_menu() && party_size > 0 && show_client_joined) - flash = true; + bool in_main_menu = g_base->app_mode->InMainMenu(); - SimpleComponent c(frame_def->overlay_pass()); + if (in_main_menu && party_size > 0 && show_client_joined) flash = true; + + base::SimpleComponent c(frame_def->overlay_pass()); c.SetTransparent(true); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kUsersButton)); + c.SetTexture( + g_base->assets->SysTexture(base::SysTextureID::kUsersButton)); // Draw button. - float width = g_graphics->screen_virtual_width(); - float height = g_graphics->screen_virtual_height(); + float width = g_base->graphics->screen_virtual_width(); + float height = g_base->graphics->screen_virtual_height(); c.PushTransform(); float extra_offset = (draw_menu_button && menu_fade_ > 0.0f) ? -menu_button_size_ : 0.0f; @@ -246,23 +249,23 @@ void RootUI::Draw(FrameDef* frame_def) { if (flash && frame_def->base_time() % 250 < 125) { c.SetColor(1.0f, 1.4f, 1.0f); } - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.PopTransform(); c.Submit(); // Based on who has menu control, we may show a key/button below the party // icon. if (!active) { - if (InputDevice* uiid = g_ui->GetUIInputDevice()) { + if (base::InputDevice* uiid = g_base->ui->GetUIInputDevice()) { std::string party_button_name = uiid->GetPartyButtonName(); if (!party_button_name.empty()) { - if (!party_button_text_group_.exists()) { - party_button_text_group_ = Object::New(); + if (!party_button_text_group_.Exists()) { + party_button_text_group_ = Object::New(); } if (party_button_name != party_button_text_group_->getText()) { party_button_text_group_->SetText(party_button_name, - TextMesh::HAlign::kCenter, - TextMesh::VAlign::kTop); + base::TextMesh::HAlign::kCenter, + base::TextMesh::VAlign::kTop); } int text_elem_count = party_button_text_group_->GetElementCount(); for (int e = 0; e < text_elem_count; e++) { @@ -293,8 +296,8 @@ void RootUI::Draw(FrameDef* frame_def) { // Update party count text if party size has changed. if (party_size_text_group_num_ != party_size) { party_size_text_group_num_ = party_size; - if (!party_size_text_group_.exists()) { - party_size_text_group_ = Object::New(); + if (!party_size_text_group_.Exists()) { + party_size_text_group_ = Object::New(); } party_size_text_group_->SetText( std::to_string(party_size_text_group_num_)); @@ -302,19 +305,20 @@ void RootUI::Draw(FrameDef* frame_def) { // ..we also may want to update our 'someone joined' message if we're // host if (is_host) { - if (!start_a_game_text_group_.exists()) { - start_a_game_text_group_ = Object::New(); + if (!start_a_game_text_group_.Exists()) { + start_a_game_text_group_ = Object::New(); } if (party_size == 2) { // (includes us as host) start_a_game_text_group_->SetText( - g_logic->GetResourceString("joinedPartyInstructionsText"), - TextMesh::HAlign::kRight, TextMesh::VAlign::kTop); + g_base->assets->GetResourceString( + "joinedPartyInstructionsText"), + base::TextMesh::HAlign::kRight, base::TextMesh::VAlign::kTop); } else if (party_size > 2) { start_a_game_text_group_->SetText( std::to_string(party_size - 1) + " friends have joined your party.\nGo to 'Play' to start " "a game.", - TextMesh::HAlign::kRight, TextMesh::VAlign::kTop); + base::TextMesh::HAlign::kRight, base::TextMesh::VAlign::kTop); } } } @@ -390,4 +394,4 @@ void RootUI::Draw(FrameDef* frame_def) { } } -} // namespace ballistica +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui/root_ui.h b/src/ballistica/ui_v1/support/root_ui.h similarity index 71% rename from src/ballistica/ui/root_ui.h rename to src/ballistica/ui_v1/support/root_ui.h index 77f81137..548af0c4 100644 --- a/src/ballistica/ui/root_ui.h +++ b/src/ballistica/ui_v1/support/root_ui.h @@ -1,11 +1,11 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_UI_ROOT_UI_H_ -#define BALLISTICA_UI_ROOT_UI_H_ +#ifndef BALLISTICA_UI_V1_SUPPORT_ROOT_UI_H_ +#define BALLISTICA_UI_V1_SUPPORT_ROOT_UI_H_ -#include "ballistica/graphics/frame_def.h" +#include "ballistica/base/graphics/support/frame_def.h" -namespace ballistica { +namespace ballistica::ui_v1 { /// Manages root level UI such as the menu button, party button, etc. /// This is set to be replaced by RootWidget. @@ -13,7 +13,7 @@ class RootUI { public: RootUI(); virtual ~RootUI(); - void Draw(FrameDef* frame_def); + void Draw(base::FrameDef* frame_def); auto HandleMouseButtonDown(float x, float y) -> bool; void HandleMouseButtonUp(float x, float y); @@ -37,13 +37,13 @@ class RootUI { bool party_window_open_{}; bool always_draw_party_icon_{}; float connected_client_extra_offset_smoothed_{}; - Object::Ref party_button_text_group_; - Object::Ref party_size_text_group_; + Object::Ref party_button_text_group_; + Object::Ref party_size_text_group_; int party_size_text_group_num_{-1}; - Object::Ref start_a_game_text_group_; + Object::Ref start_a_game_text_group_; float start_a_game_text_scale_{}; }; -} // namespace ballistica +} // namespace ballistica::ui_v1 -#endif // BALLISTICA_UI_ROOT_UI_H_ +#endif // BALLISTICA_UI_V1_SUPPORT_ROOT_UI_H_ diff --git a/src/ballistica/ui_v1/ui_v1.cc b/src/ballistica/ui_v1/ui_v1.cc new file mode 100644 index 00000000..359178a2 --- /dev/null +++ b/src/ballistica/ui_v1/ui_v1.cc @@ -0,0 +1,57 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/ui_v1/ui_v1.h" + +#include "ballistica/ui_v1/python/ui_v1_python.h" + +namespace ballistica::ui_v1 { + +core::CoreFeatureSet* g_core{}; +base::BaseFeatureSet* g_base{}; +UIV1FeatureSet* g_ui_v1{}; + +UIV1FeatureSet::UIV1FeatureSet() : python(new UIV1Python()) { + // We're a singleton. If there's already one of us, something's wrong. + assert(g_ui_v1 == nullptr); +} + +void UIV1FeatureSet::OnModuleExec(PyObject* module) { + // Ok, our feature-set's Python module is getting imported. + // Like any normal Python module, we take this opportunity to + // import/create the stuff we use. + + // Importing core should always be the first thing we do. + // Various ballistica functionality will fail if this has not been done. + g_core = core::CoreFeatureSet::Import(); + + g_core->BootLog("_bauiv1 exec begin"); + + // Create our feature-set's C++ front-end. + assert(g_ui_v1 == nullptr); + g_ui_v1 = new UIV1FeatureSet(); + g_ui_v1->python->AddPythonClasses(module); + + // Store our C++ front-end with our Python module. + // This lets anyone get at us by going through the Python + // import system (keeping things nice and consistent between + // Python and C++ worlds). + g_ui_v1->StoreOnPythonModule(module); + + // Import any Python stuff we use into objs_. + g_ui_v1->python->ImportPythonObjs(); + + // Import any other C++ feature-set-front-ends we use. + assert(g_base == nullptr); // Should be getting set once here. + g_base = base::BaseFeatureSet::Import(); + + g_core->BootLog("_bauiv1 exec end"); +} + +auto UIV1FeatureSet::Import() -> UIV1FeatureSet* { + // Since we provide a native Python module, we piggyback our C++ front-end + // on top of that. This way our C++ and Python dependencies are resolved + // consistently no matter which side we are imported from. + return ImportThroughPythonModule("_bauiv1"); +} + +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui_v1/ui_v1.h b/src/ballistica/ui_v1/ui_v1.h new file mode 100644 index 00000000..c0e77159 --- /dev/null +++ b/src/ballistica/ui_v1/ui_v1.h @@ -0,0 +1,62 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_V1_UI_V1_H_ +#define BALLISTICA_UI_V1_UI_V1_H_ + +#include "ballistica/shared/foundation/feature_set_front_end.h" + +// Common header that most everything using our feature-set should include. +// It predeclares our feature-set's various types and globals and other +// bits. + +// Predeclared types from other feature sets that we use. +namespace ballistica::core { +class CoreFeatureSet; +} +namespace ballistica::base { +class BaseFeatureSet; +} + +namespace ballistica::ui_v1 { + +// Predeclared types our feature-set provides. +class UIV1FeatureSet; +class UIV1Python; +class Widget; +class ButtonWidget; +class ContainerWidget; +class ImageWidget; +class RootUI; +class RootWidget; +class StackWidget; +class TextWidget; + +// Our feature-set's globals. +// Feature-sets should NEVER directly access globals in another feature-set's +// namespace. All functionality we need from other feature-sets should be +// imported into globals in our own namespace. Generally we do this when we +// are initially imported (just as regular Python modules do). +extern core::CoreFeatureSet* g_core; +extern base::BaseFeatureSet* g_base; +extern UIV1FeatureSet* g_ui_v1; + +/// Our C++ front-end to our feature set. This is what other C++ +/// feature-sets can 'Import' from us. +class UIV1FeatureSet : public FeatureSetFrontEnd { + public: + /// Instantiate our FeatureSet if needed and return the single + /// instance of it. Basically a Python import statement. + static auto Import() -> UIV1FeatureSet*; + + /// Called when our associated Python module is instantiated. + static void OnModuleExec(PyObject* module); + + UIV1Python* const python; + + private: + UIV1FeatureSet(); +}; + +} // namespace ballistica::ui_v1 + +#endif // BALLISTICA_UI_V1_UI_V1_H_ diff --git a/src/ballistica/ui/widget/button_widget.cc b/src/ballistica/ui_v1/widget/button_widget.cc similarity index 62% rename from src/ballistica/ui/widget/button_widget.cc rename to src/ballistica/ui_v1/widget/button_widget.cc index cfefdad9..ad9cfe6d 100644 --- a/src/ballistica/ui/widget/button_widget.cc +++ b/src/ballistica/ui_v1/widget/button_widget.cc @@ -1,22 +1,23 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/ui/widget/button_widget.h" +#include "ballistica/ui_v1/widget/button_widget.h" -#include "ballistica/audio/audio.h" -#include "ballistica/generic/real_timer.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/component/empty_component.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/input/device/input_device.h" -#include "ballistica/input/input.h" -#include "ballistica/python/python_context_call.h" -#include "ballistica/ui/ui.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/graphics/component/empty_component.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/input/device/input_device.h" +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/base/support/app_timer.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/shared/generic/utils.h" -namespace ballistica { +namespace ballistica::ui_v1 { -ButtonWidget::ButtonWidget() : birth_time_{g_logic->master_time()} { +ButtonWidget::ButtonWidget() + : birth_time_millisecs_{ + static_cast(g_base->logic->display_time() * 1000.0)} { text_ = Object::New(); - SetText("Button"); + set_text("Button"); text_->set_valign(TextWidget::VAlign::kCenter); text_->set_halign(TextWidget::HAlign::kCenter); text_->SetWidth(0.0f); @@ -28,10 +29,10 @@ ButtonWidget::~ButtonWidget() = default; void ButtonWidget::SetTextResScale(float val) { text_->set_res_scale(val); } void ButtonWidget::set_on_activate_call(PyObject* call_obj) { - on_activate_call_ = Object::New(call_obj); + on_activate_call_ = Object::New(call_obj); } -void ButtonWidget::SetText(const std::string& text_in) { +void ButtonWidget::set_text(const std::string& text_in) { std::string text = Utils::GetValidUTF8(text_in.c_str(), "bwst"); text_->SetText(text); @@ -40,59 +41,34 @@ void ButtonWidget::SetText(const std::string& text_in) { text_width_dirty_ = true; } -void ButtonWidget::SetTexture(Texture* val) { - if (val && !val->IsFromUIContext()) { - throw Exception("texture is not from the UI context: " + ObjToString(val)); - } - texture_ = val; -} +void ButtonWidget::SetTexture(base::TextureAsset* val) { texture_ = val; } -void ButtonWidget::SetMaskTexture(Texture* val) { - if (val && !val->IsFromUIContext()) { - throw Exception("texture is not from the UI context: " + ObjToString(val)); - } +void ButtonWidget::SetMaskTexture(base::TextureAsset* val) { mask_texture_ = val; } -void ButtonWidget::SetTintTexture(Texture* val) { - if (val && !val->IsFromUIContext()) { - throw Exception("texture is not from the UI context: " + ObjToString(val)); - } +void ButtonWidget::SetTintTexture(base::TextureAsset* val) { tint_texture_ = val; } -void ButtonWidget::SetIcon(Texture* val) { - if (val && !val->IsFromUIContext()) { - throw Exception("icon texture is not from the UI context: " - + val->GetObjectDescription()); - } - icon_ = val; -} +void ButtonWidget::SetIcon(base::TextureAsset* val) { icon_ = val; } -void ButtonWidget::HandleRealTimerExpired(RealTimer* t) { +void ButtonWidget::OnRepeatTimerExpired() { // Repeat our action unless we somehow lost focus but didn't get a mouse-up. if (IsHierarchySelected() && pressed_) { DoActivate(true); // Speed up repeats after the first. - t->SetLength(150); + repeat_timer_->SetLength(150); } else { repeat_timer_.Clear(); } } -void ButtonWidget::SetModelOpaque(Model* val) { - if (val && !val->IsFromUIContext()) { - throw Exception("model_opaque is not from the UI context"); - } - model_opaque_ = val; -} +void ButtonWidget::SetMeshOpaque(base::MeshAsset* val) { mesh_opaque_ = val; } -void ButtonWidget::SetModelTransparent(Model* val) { - if (val && !val->IsFromUIContext()) { - throw Exception("model_transparent is not from the UI context"); - } - model_transparent_ = val; +void ButtonWidget::SetMeshTransparent(base::MeshAsset* val) { + mesh_transparent_ = val; } auto ButtonWidget::GetWidth() -> float { return width_; } @@ -100,30 +76,32 @@ auto ButtonWidget::GetHeight() -> float { return height_; } auto ButtonWidget::GetMult(millisecs_t current_time) const -> float { float mult = 1.0f; - if ((pressed_ && mouse_over_) || (current_time - last_activate_time_ < 200)) { + if ((pressed_ && mouse_over_) + || (current_time - last_activate_time_millisecs_ < 200)) { if (pressed_ && mouse_over_) { mult = 3.0f; } else { - float x = static_cast(current_time - last_activate_time_) / 200.0f; + float x = static_cast(current_time - last_activate_time_millisecs_) + / 200.0f; mult = 1.0f + 3.0f * (1.0f - x * x); } - } else if ((IsHierarchySelected() && g_ui->ShouldHighlightWidgets())) { + } else if ((IsHierarchySelected() && g_base->ui->ShouldHighlightWidgets())) { mult = 0.8f + std::abs(sinf(static_cast(current_time) * 0.006467f)) * 0.2f; - if (!texture_.exists()) { + if (!texture_.Exists()) { mult *= 1.7f; } else { // Let's make custom textures pulsate brighter since they can be dark/etc. mult *= 2.0f; } } else { - if (!texture_.exists()) { + if (!texture_.Exists()) { } else { // In desktop mode we want image buttons to light up when we // mouse over them. - if (!g_platform->IsRunningOnDesktop()) { + if (!g_core->platform->IsRunningOnDesktop()) { if (mouse_over_) { mult = 1.4f; } @@ -137,20 +115,20 @@ auto ButtonWidget::GetDrawBrightness(millisecs_t time) const -> float { return GetMult(time); } -void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { +void ButtonWidget::Draw(base::RenderPass* pass, bool draw_transparent) { millisecs_t current_time = pass->frame_def()->base_time(); - Vector3f tilt = 0.02f * g_graphics->tilt(); + Vector3f tilt = 0.02f * g_base->graphics->tilt(); float extra_offs_x = -tilt.y; float extra_offs_y = tilt.x; - assert(g_input); + assert(g_base->input); bool show_icons = false; - InputDevice* device = g_ui->GetUIInputDevice(); + auto* device = g_base->ui->GetUIInputDevice(); // If there's an explicit user-set icon we always show. - if (icon_.exists()) { + if (icon_.Exists()) { show_icons = true; } @@ -167,7 +145,8 @@ void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { } // Simple transition. - millisecs_t transition = (birth_time_ + transition_delay_) - current_time; + millisecs_t transition = + (birth_time_millisecs_ + transition_delay_) - current_time; if (transition > 0) { extra_offs_x -= static_cast(transition) * 4.0f; } @@ -216,39 +195,38 @@ void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { // For normal buttons we draw both transparent and opaque. // With custom ones we only draw what we're given. - Object::Ref custom_model; - bool do_draw_model; + Object::Ref custom_mesh; + bool do_draw_mesh; // Normal buttons draw in both transparent and opaque passes. - if (!texture_.exists()) { - do_draw_model = true; + if (!texture_.Exists()) { + do_draw_mesh = true; } else { - // If we're supplying any custom models, draw whichever is provided. - if (model_opaque_.exists() || model_transparent_.exists()) { - if (draw_transparent && model_transparent_.exists()) { - do_draw_model = true; - custom_model = model_transparent_; - } else if ((!draw_transparent) && model_opaque_.exists()) { - do_draw_model = true; - custom_model = model_opaque_; + // If we're supplying any custom meshes, draw whichever is provided. + if (mesh_opaque_.Exists() || mesh_transparent_.Exists()) { + if (draw_transparent && mesh_transparent_.Exists()) { + do_draw_mesh = true; + custom_mesh = mesh_transparent_; + } else if ((!draw_transparent) && mesh_opaque_.Exists()) { + do_draw_mesh = true; + custom_mesh = mesh_opaque_; } else { - do_draw_model = false; // Skip this pass. + do_draw_mesh = false; // Skip this pass. } } else { - // With no custom models we just draw a plain square in the + // With no custom meshes we just draw a plain square in the // transparent pass. - do_draw_model = draw_transparent; + do_draw_mesh = draw_transparent; } } - if (do_draw_model) { - SimpleComponent c(pass); + if (do_draw_mesh) { + base::SimpleComponent c(pass); c.SetTransparent(draw_transparent); // We currently only support non-1.0 opacity values when using - // custom textures and no custom opaque model. - assert(opacity_ == 1.0f - || (texture_.exists() && !model_opaque_.exists())); + // custom textures and no custom opaque mesh. + assert(opacity_ == 1.0f || (texture_.Exists() && !mesh_opaque_.Exists())); c.SetColor(mult * color_red_, mult * color_green_, mult * color_blue_, opacity_); @@ -257,29 +235,27 @@ void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { bool doDraw = true; - ModelData* model; + base::MeshAsset* mesh; // Custom button texture. - if (texture_.exists()) { - if (!custom_model.exists()) { - model = g_assets->GetModel(SystemModelID::kImage1x1); + if (texture_.Exists()) { + if (!custom_mesh.Exists()) { + mesh = g_base->assets->SysMesh(base::SysMeshID::kImage1x1); } else { - model = custom_model->model_data(); + mesh = custom_mesh.Get(); } - if (texture_->texture_data()->loaded() && model->loaded() - && (!mask_texture_.exists() - || mask_texture_->texture_data()->loaded()) - && (!tint_texture_.exists() - || tint_texture_->texture_data()->loaded())) { + if (texture_->loaded() && mesh->loaded() + && (!mask_texture_.Exists() || mask_texture_->loaded()) + && (!tint_texture_.Exists() || tint_texture_->loaded())) { c.SetTexture(texture_); - if (tint_texture_.exists()) { - c.SetColorizeTexture(tint_texture_); + if (tint_texture_.Exists()) { + c.SetColorizeTexture(tint_texture_.Get()); c.SetColorizeColor(tint_color_red_, tint_color_green_, tint_color_blue_); c.SetColorizeColor2(tint2_color_red_, tint2_color_green_, tint2_color_blue_); } - c.SetMaskTexture(mask_texture_); + c.SetMaskTexture(mask_texture_.Get()); } else { doDraw = false; } @@ -287,14 +263,14 @@ void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { b_border = t_border = 0.04f * height_; } else { // Standard button texture. - SystemModelID model_id; - SystemTextureID tex_id; + base::SysMeshID mesh_id; + base::SysTextureID tex_id; switch (style_) { case Style::kBack: { - tex_id = SystemTextureID::kUIAtlas; - model_id = draw_transparent ? SystemModelID::kButtonBackTransparent - : SystemModelID::kButtonBackOpaque; + tex_id = base::SysTextureID::kUIAtlas; + mesh_id = draw_transparent ? base::SysMeshID::kButtonBackTransparent + : base::SysMeshID::kButtonBackOpaque; l_border = 10; r_border = 6; b_border = 6; @@ -302,10 +278,10 @@ void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { break; } case Style::kBackSmall: { - tex_id = SystemTextureID::kUIAtlas; - model_id = draw_transparent - ? SystemModelID::kButtonBackSmallTransparent - : SystemModelID::kButtonBackSmallOpaque; + tex_id = base::SysTextureID::kUIAtlas; + mesh_id = draw_transparent + ? base::SysMeshID::kButtonBackSmallTransparent + : base::SysMeshID::kButtonBackSmallOpaque; l_border = 10; r_border = 14; b_border = 9; @@ -313,9 +289,9 @@ void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { break; } case Style::kTab: { - tex_id = SystemTextureID::kUIAtlas2; - model_id = draw_transparent ? SystemModelID::kButtonTabTransparent - : SystemModelID::kButtonTabOpaque; + tex_id = base::SysTextureID::kUIAtlas2; + mesh_id = draw_transparent ? base::SysMeshID::kButtonTabTransparent + : base::SysMeshID::kButtonTabOpaque; l_border = 6; r_border = 10; b_border = 5; @@ -323,10 +299,10 @@ void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { break; } case Style::kSquare: { - tex_id = SystemTextureID::kButtonSquare; - model_id = draw_transparent - ? SystemModelID::kButtonSquareTransparent - : SystemModelID::kButtonSquareOpaque; + tex_id = base::SysTextureID::kButtonSquare; + mesh_id = draw_transparent + ? base::SysMeshID::kButtonSquareTransparent + : base::SysMeshID::kButtonSquareOpaque; l_border = 6; r_border = 9; b_border = 6; @@ -335,37 +311,37 @@ void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { } default: { if ((r_orig - l_orig) / (t_orig - b_orig) < 50.0f / 30.0f) { - tex_id = SystemTextureID::kUIAtlas; - model_id = draw_transparent - ? SystemModelID::kButtonSmallTransparent - : SystemModelID::kButtonSmallOpaque; + tex_id = base::SysTextureID::kUIAtlas; + mesh_id = draw_transparent + ? base::SysMeshID::kButtonSmallTransparent + : base::SysMeshID::kButtonSmallOpaque; l_border = 10; r_border = 14; b_border = 9; t_border = 5; } else if ((r_orig - l_orig) / (t_orig - b_orig) < 200.0f / 35.0f) { - tex_id = SystemTextureID::kUIAtlas; - model_id = draw_transparent - ? SystemModelID::kButtonMediumTransparent - : SystemModelID::kButtonMediumOpaque; + tex_id = base::SysTextureID::kUIAtlas; + mesh_id = draw_transparent + ? base::SysMeshID::kButtonMediumTransparent + : base::SysMeshID::kButtonMediumOpaque; l_border = 6; r_border = 10; b_border = 5; t_border = 2; } else if ((r_orig - l_orig) / (t_orig - b_orig) < 300.0f / 35.0f) { - tex_id = SystemTextureID::kUIAtlas; - model_id = draw_transparent - ? SystemModelID::kButtonLargeTransparent - : SystemModelID::kButtonLargeOpaque; + tex_id = base::SysTextureID::kUIAtlas; + mesh_id = draw_transparent + ? base::SysMeshID::kButtonLargeTransparent + : base::SysMeshID::kButtonLargeOpaque; l_border = 7; r_border = 10; b_border = 10; t_border = 5; } else { - tex_id = SystemTextureID::kUIAtlas; - model_id = draw_transparent - ? SystemModelID::kButtonLargerTransparent - : SystemModelID::kButtonLargerOpaque; + tex_id = base::SysTextureID::kUIAtlas; + mesh_id = draw_transparent + ? base::SysMeshID::kButtonLargerTransparent + : base::SysMeshID::kButtonLargerOpaque; l_border = 7; r_border = 11; b_border = 10; @@ -374,15 +350,15 @@ void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { break; } } - c.SetTexture(g_assets->GetTexture(tex_id)); - model = g_assets->GetModel(model_id); + c.SetTexture(g_base->assets->SysTexture(tex_id)); + mesh = g_base->assets->SysMesh(mesh_id); } if (doDraw) { c.PushTransform(); c.Translate((l - l_border + r + r_border) * 0.5f + extra_offs_x, (b - b_border + t + t_border) * 0.5f + extra_offs_y, 0); c.Scale(r - l + l_border + r_border, t - b + b_border + t_border, 1.0f); - c.DrawModel(model); + c.DrawMeshAsset(mesh); c.PopTransform(); } @@ -392,22 +368,26 @@ void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { if (icon_type_ == IconType::kStart) { c.SetColor(1.4f * mult * (color_red_), 1.4f * mult * (color_green_), 1.4f * mult * (color_blue_), 1.0f); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kStartButton)); + c.SetTexture( + g_base->assets->SysTexture(base::SysTextureID::kStartButton)); } else if (icon_type_ == IconType::kCancel) { if (remote_icons) { c.SetColor(1.0f * mult * (1.0f), 1.0f * mult * (1.0f), 1.0f * mult * (1.0f), 1.0f); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kBackIcon)); + c.SetTexture( + g_base->assets->SysTexture(base::SysTextureID::kBackIcon)); } else if (ouya_icons) { c.SetColor(1.0f * mult * (1.0f), 1.0f * mult * (1.0f), 1.0f * mult * (1.0f), 1.0f); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kOuyaAButton)); + c.SetTexture( + g_base->assets->SysTexture(base::SysTextureID::kOuyaAButton)); } else { c.SetColor(1.5f * mult * (color_red_), 1.5f * mult * (color_green_), 1.5f * mult * (color_blue_), 1.0f); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kBombButton)); + c.SetTexture( + g_base->assets->SysTexture(base::SysTextureID::kBombButton)); } - } else if (icon_.exists()) { + } else if (icon_.Exists()) { c.SetColor(icon_color_red_ * (icon_tint_ * (1.7f * mult * (color_red_)) + (1.0f - icon_tint_) * mult), @@ -418,14 +398,14 @@ void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { * (icon_tint_ * (1.7f * mult * (color_blue_)) + (1.0f - icon_tint_) * mult), icon_color_alpha_); - if (!icon_->texture_data()->loaded()) { + if (!icon_->loaded()) { doDrawIcon = false; } else { c.SetTexture(icon_); } } else { c.SetColor(1, 1, 1); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kCircle)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kCircle)); } if (doDrawIcon) { c.PushTransform(); @@ -433,7 +413,7 @@ void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { - (string_width * string_scale) * 0.5f - 5.0f, (b + t) * 0.5f + extra_offs_y, 0.001f); c.Scale(34.0f * icon_scale_, 34.f * icon_scale_, 1.0f); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.PopTransform(); } } @@ -443,7 +423,7 @@ void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { // Draw our text at z depth 0.5-1. if (!string_too_small_to_draw) { - EmptyComponent c(pass); + base::EmptyComponent c(pass); c.SetTransparent(draw_transparent); c.PushTransform(); c.Translate(1.0f * extra_offs_x, 1.0f * extra_offs_y, 0.5f); @@ -468,10 +448,10 @@ void ButtonWidget::Draw(RenderPass* pass, bool draw_transparent) { } } -auto ButtonWidget::HandleMessage(const WidgetMessage& m) -> bool { +auto ButtonWidget::HandleMessage(const base::WidgetMessage& m) -> bool { // How far outside button touches register. float left_overlap, top_overlap, right_overlap, bottom_overlap; - if (g_platform->IsRunningOnDesktop()) { + if (g_core->platform->IsRunningOnDesktop()) { left_overlap = 3.0f; top_overlap = 1.0f; right_overlap = 0.0f; @@ -484,7 +464,7 @@ auto ButtonWidget::HandleMessage(const WidgetMessage& m) -> bool { } switch (m.type) { - case WidgetMessage::Type::kMouseMove: { + case base::WidgetMessage::Type::kMouseMove: { float x = m.fval1; float y = m.fval2; bool claimed = (m.fval3 > 0.0f); @@ -498,7 +478,7 @@ auto ButtonWidget::HandleMessage(const WidgetMessage& m) -> bool { return mouse_over_; } - case WidgetMessage::Type::kMouseDown: { + case base::WidgetMessage::Type::kMouseDown: { float x = m.fval1; float y = m.fval2; if (enabled_ && (x >= (-left_overlap)) && (x < (width_ + right_overlap)) @@ -507,7 +487,9 @@ auto ButtonWidget::HandleMessage(const WidgetMessage& m) -> bool { pressed_ = true; if (repeat_) { - repeat_timer_ = Object::New>(300, true, this); + repeat_timer_ = + base::NewAppTimer(300, true, [this] { OnRepeatTimerExpired(); }); + // If we're a repeat button we trigger immediately. // (waiting till mouse up sort of defeats the purpose here) Activate(); @@ -520,7 +502,7 @@ auto ButtonWidget::HandleMessage(const WidgetMessage& m) -> bool { return false; } } - case WidgetMessage::Type::kMouseUp: { + case base::WidgetMessage::Type::kMouseUp: { float x = m.fval1; float y = m.fval2; bool claimed = (m.fval3 > 0.0f); @@ -552,7 +534,7 @@ auto ButtonWidget::HandleMessage(const WidgetMessage& m) -> bool { void ButtonWidget::Activate() { DoActivate(); } -void ButtonWidget::DoActivate(bool isRepeat) { +void ButtonWidget::DoActivate(bool is_repeat) { if (!enabled_) { Log(LogLevel::kWarning, "ButtonWidget::DoActivate() called on disabled button"); @@ -560,24 +542,27 @@ void ButtonWidget::DoActivate(bool isRepeat) { } // We don't want holding down a repeat-button to keep flashing it. - if (!isRepeat) { - last_activate_time_ = g_logic->master_time(); + if (!is_repeat) { + last_activate_time_millisecs_ = + static_cast(g_base->logic->display_time() * 1000.0); } if (sound_enabled_) { int r = rand() % 3; // NOLINT if (r == 0) { - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kSwish)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kSwish)); } else if (r == 1) { - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kSwish2)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kSwish2)); } else { - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kSwish3)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kSwish3)); } } - if (on_activate_call_.exists()) { + if (auto* call = on_activate_call_.Get()) { // Call this in the next cycle (don't want to risk mucking with UI from // within a UI loop.) - g_logic->PushPythonWeakCall( - Object::WeakRef(on_activate_call_)); + call->ScheduleWeak(); return; } } @@ -587,4 +572,4 @@ void ButtonWidget::OnLanguageChange() { text_width_dirty_ = true; } -} // namespace ballistica +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui/widget/button_widget.h b/src/ballistica/ui_v1/widget/button_widget.h similarity index 59% rename from src/ballistica/ui/widget/button_widget.h rename to src/ballistica/ui_v1/widget/button_widget.h index bb381d27..fd176bff 100644 --- a/src/ballistica/ui/widget/button_widget.h +++ b/src/ballistica/ui_v1/widget/button_widget.h @@ -1,20 +1,21 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_UI_WIDGET_BUTTON_WIDGET_H_ -#define BALLISTICA_UI_WIDGET_BUTTON_WIDGET_H_ +#ifndef BALLISTICA_UI_V1_WIDGET_BUTTON_WIDGET_H_ +#define BALLISTICA_UI_V1_WIDGET_BUTTON_WIDGET_H_ #include -#include "ballistica/ui/widget/text_widget.h" +#include "ballistica/scene_v1/scene_v1.h" +#include "ballistica/ui_v1/widget/text_widget.h" -namespace ballistica { +namespace ballistica::ui_v1 { class ButtonWidget : public Widget { public: ButtonWidget(); ~ButtonWidget() override; - void Draw(RenderPass* pass, bool transparent) override; - auto HandleMessage(const WidgetMessage& m) -> bool override; + void Draw(base::RenderPass* pass, bool transparent) override; + auto HandleMessage(const base::WidgetMessage& m) -> bool override; void set_width(float width) { width_ = width; } void set_height(float height) { height_ = height; } auto GetWidth() -> float override; @@ -47,41 +48,41 @@ class ButtonWidget : public Widget { icon_color_blue_ = b; icon_color_alpha_ = a; } - void set_text_flatness(float f) { text_flatness_ = f; } + auto set_text_flatness(float f) { text_flatness_ = f; } enum class Style { kRegular, kBack, kBackSmall, kTab, kSquare }; - void set_style(Style s) { style_ = s; } + auto set_style(Style s) { style_ = s; } enum class IconType { kNone, kCancel, kStart }; - void SetText(const std::string& text); + void set_text(const std::string& text); auto text() const -> std::string { return text_->text_raw(); } - void set_icon_type(IconType i) { icon_type_ = i; } - void set_repeat(bool repeat) { repeat_ = repeat; } - void set_text_scale(float val) { text_scale_ = val; } - void SetTexture(Texture* t); - void SetMaskTexture(Texture* t); - void SetTintTexture(Texture* val); - void SetIcon(Texture* t); - auto icon() const -> Texture* { return icon_.get(); } + auto set_icon_type(IconType i) { icon_type_ = i; } + auto set_repeat(bool repeat) { repeat_ = repeat; } + auto set_text_scale(float val) { text_scale_ = val; } + void SetTexture(base::TextureAsset* t); + void SetMaskTexture(base::TextureAsset* t); + void SetTintTexture(base::TextureAsset* val); + void SetIcon(base::TextureAsset* t); + auto icon() const -> base::TextureAsset* { return icon_.Get(); } void set_on_activate_call(PyObject* call_obj); void Activate() override; auto IsSelectable() -> bool override { return selectable_; } auto GetWidgetTypeName() -> std::string override { return "button"; } - void set_enable_sound(bool enable) { sound_enabled_ = enable; } - void SetModelTransparent(Model* val); - void SetModelOpaque(Model* val); - void set_transition_delay(millisecs_t val) { transition_delay_ = val; } - void HandleRealTimerExpired(RealTimer* t); - void set_extra_touch_border_scale(float scale) { + auto set_enable_sound(bool enable) { sound_enabled_ = enable; } + void SetMeshTransparent(base::MeshAsset* val); + void SetMeshOpaque(base::MeshAsset* val); + auto set_transition_delay(millisecs_t val) { transition_delay_ = val; } + void OnRepeatTimerExpired(); + auto set_extra_touch_border_scale(float scale) { extra_touch_border_scale_ = scale; } - void set_selectable(bool s) { selectable_ = s; } - void set_icon_scale(float s) { icon_scale_ = s; } - void set_icon_tint(float tint) { icon_tint_ = tint; } + auto set_selectable(bool s) { selectable_ = s; } + auto set_icon_scale(float s) { icon_scale_ = s; } + auto set_icon_tint(float tint) { icon_tint_ = tint; } void SetTextResScale(float val); // Disabled buttons can't be clicked or otherwise activated. - void set_enabled(bool val) { enabled_ = val; } + auto set_enabled(bool val) { enabled_ = val; } auto enabled() const -> bool { return enabled_; } - void set_opacity(float val) { opacity_ = val; } + auto set_opacity(float val) { opacity_ = val; } auto GetDrawBrightness(millisecs_t time) const -> float override; auto is_color_set() const -> bool { return color_set_; } void OnLanguageChange() override; @@ -89,7 +90,7 @@ class ButtonWidget : public Widget { private: bool text_width_dirty_ = true; bool color_set_ = false; - void DoActivate(bool isRepeat = false); + void DoActivate(bool is_repeat = false); auto GetMult(millisecs_t current_time) const -> float; IconType icon_type_ = IconType::kNone; bool enabled_ = true; @@ -112,15 +113,15 @@ class ButtonWidget : public Widget { float icon_color_green_ = 1.0f; float icon_color_blue_ = 1.0f; float icon_color_alpha_ = 1.0f; - Object::Ref texture_; - Object::Ref icon_; - Object::Ref tint_texture_; - Object::Ref mask_texture_; - Object::Ref model_transparent_; - Object::Ref model_opaque_; + Object::Ref texture_; + Object::Ref icon_; + Object::Ref tint_texture_; + Object::Ref mask_texture_; + Object::Ref mesh_transparent_; + Object::Ref mesh_opaque_; float icon_scale_{1.0f}; - millisecs_t last_activate_time_{}; - millisecs_t birth_time_{}; + millisecs_t last_activate_time_millisecs_{}; + millisecs_t birth_time_millisecs_{}; millisecs_t transition_delay_{}; float opacity_{1.0f}; float text_flatness_{0.5f}; @@ -137,10 +138,11 @@ class ButtonWidget : public Widget { // Keep these at the bottom, so they're torn down first. Object::Ref text_; - Object::Ref on_activate_call_; - Object::Ref > repeat_timer_; + Object::Ref on_activate_call_; + // Object::Ref > repeat_timer_; + Object::Ref repeat_timer_; }; -} // namespace ballistica +} // namespace ballistica::ui_v1 -#endif // BALLISTICA_UI_WIDGET_BUTTON_WIDGET_H_ +#endif // BALLISTICA_UI_V1_WIDGET_BUTTON_WIDGET_H_ diff --git a/src/ballistica/ui/widget/check_box_widget.cc b/src/ballistica/ui_v1/widget/check_box_widget.cc similarity index 76% rename from src/ballistica/ui/widget/check_box_widget.cc rename to src/ballistica/ui_v1/widget/check_box_widget.cc index 8341fffd..3366a1df 100644 --- a/src/ballistica/ui/widget/check_box_widget.cc +++ b/src/ballistica/ui_v1/widget/check_box_widget.cc @@ -1,16 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/ui/widget/check_box_widget.h" +#include "ballistica/ui_v1/widget/check_box_widget.h" -#include "ballistica/audio/audio.h" -#include "ballistica/graphics/component/empty_component.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/logic/logic.h" -#include "ballistica/python/python_context_call.h" -#include "ballistica/python/python_sys.h" -#include "ballistica/ui/ui.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/graphics/component/empty_component.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/shared/python/python_sys.h" -namespace ballistica { +namespace ballistica::ui_v1 { CheckBoxWidget::CheckBoxWidget() { SetText("CheckBox"); @@ -22,7 +21,7 @@ CheckBoxWidget::CheckBoxWidget() { CheckBoxWidget::~CheckBoxWidget() = default; void CheckBoxWidget::SetOnValueChangeCall(PyObject* call_tuple) { - on_value_change_call_ = Object::New(call_tuple); + on_value_change_call_ = Object::New(call_tuple); } void CheckBoxWidget::SetText(const std::string& text) { @@ -42,8 +41,8 @@ void CheckBoxWidget::SetHeight(float height_in) { text_.SetHeight(height_in); } -void CheckBoxWidget::Draw(RenderPass* pass, bool draw_transparent) { - millisecs_t real_time = GetRealTime(); +void CheckBoxWidget::Draw(base::RenderPass* pass, bool draw_transparent) { + millisecs_t real_time = g_core->GetAppTimeMillisecs(); have_drawn_ = true; float l = 0.0f; @@ -51,15 +50,15 @@ void CheckBoxWidget::Draw(RenderPass* pass, bool draw_transparent) { float b = 0.0f; float t = b + height_; - Vector3f tilt = 0.01f * g_graphics->tilt(); + Vector3f tilt = 0.01f * g_base->graphics->tilt(); if (draw_control_parent()) { - tilt += 0.02f * g_graphics->tilt(); + tilt += 0.02f * g_base->graphics->tilt(); } float extra_offs_x = -tilt.y; float extra_offs_y = tilt.x; if (have_text_ && draw_transparent - && ((selected() && g_ui->ShouldHighlightWidgets()) + && ((selected() && g_base->ui->ShouldHighlightWidgets()) || (pressed_ && mouse_over_))) { // Draw glow (at depth 0.9f). float m; @@ -84,15 +83,15 @@ void CheckBoxWidget::Draw(RenderPass* pass, bool draw_transparent) { highlight_center_y_ = b - b_border + highlight_height_ * 0.5f; highlight_dirty_ = false; } - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(true); c.SetPremultiplied(true); c.SetColor(0.25f * m, 0.3f * m, 0, 0.3f * m); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kGlow)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kGlow)); c.PushTransform(); c.Translate(highlight_center_x_, highlight_center_y_); c.Scale(highlight_width_, highlight_height_); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage4x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage4x1)); c.PopTransform(); c.Submit(); } @@ -107,7 +106,7 @@ void CheckBoxWidget::Draw(RenderPass* pass, bool draw_transparent) { if (pressed_ && mouse_over_) { glow_amt = 2.0f; - } else if (IsHierarchySelected() && g_ui->ShouldHighlightWidgets()) { + } else if (IsHierarchySelected() && g_base->ui->ShouldHighlightWidgets()) { glow_amt = 0.8f + std::abs(sinf(static_cast(real_time) * 0.006467f) * 0.3f); @@ -128,18 +127,18 @@ void CheckBoxWidget::Draw(RenderPass* pass, bool draw_transparent) { box_dirty_ = false; } - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(draw_transparent); c.SetColor(glow_amt * color_r_, glow_amt * color_g_, glow_amt * color_b_, 1); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kUIAtlas)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kUIAtlas)); c.PushTransform(); c.Translate(box_center_x_ + extra_offs_x, box_center_y_ + extra_offs_y, 0.1f); c.Scale(box_width_, box_height_, 0.4f); - c.DrawModel(g_assets->GetModel( - draw_transparent ? SystemModelID::kButtonSmallTransparent - : SystemModelID::kButtonSmallOpaque)); + c.DrawMeshAsset(g_base->assets->SysMesh( + draw_transparent ? base::SysMeshID::kButtonSmallTransparent + : base::SysMeshID::kButtonSmallOpaque)); c.PopTransform(); c.Submit(); } @@ -168,15 +167,15 @@ void CheckBoxWidget::Draw(RenderPass* pass, bool draw_transparent) { } // Draw check in z depth from 0.5f to 1. - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(draw_transparent); if (is_radio_button_) { - c.SetTexture(g_assets->GetTexture(SystemTextureID::kNub)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kNub)); } else { - c.SetTexture(g_assets->GetTexture(SystemTextureID::kUIAtlas)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kUIAtlas)); } - if (mouse_over_ && g_platform->IsRunningOnDesktop()) { + if (mouse_over_ && g_core->platform->IsRunningOnDesktop()) { c.SetColor(1.0f * glow_amt, 0.7f * glow_amt, 0, 1); } else { c.SetColor(1.0f * glow_amt, 0.6f * glow_amt, 0, 1); @@ -188,12 +187,13 @@ void CheckBoxWidget::Draw(RenderPass* pass, bool draw_transparent) { check_center_y_ + 2 + 3.0f * extra_offs_y, 0.5f); c.Scale(check_width_ * 0.45f, check_height_ * 0.45f, 0.5f); c.Translate(-0.17f, -0.17f, 0.5f); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); } else { c.Translate(check_center_x_ + 3.0f * extra_offs_x, check_center_y_ + 3.0f * extra_offs_y, 0.5f); c.Scale(check_width_, check_height_, 0.5f); - c.DrawModel(g_assets->GetModel(SystemModelID::kCheckTransparent)); + c.DrawMeshAsset( + g_base->assets->SysMesh(base::SysMeshID::kCheckTransparent)); } c.PopTransform(); c.Submit(); @@ -201,7 +201,7 @@ void CheckBoxWidget::Draw(RenderPass* pass, bool draw_transparent) { } // Draw our text in z depth 0.5f to 1. - EmptyComponent c(pass); + base::EmptyComponent c(pass); c.SetTransparent(draw_transparent); c.PushTransform(); c.Translate(2 * box_padding_ + box_size_ + 10, 0, 0.5f); @@ -229,31 +229,30 @@ void CheckBoxWidget::SetValue(bool value) { // Don't animate if we're setting initial values. if (checked_ != value && have_drawn_) { - last_change_time_ = GetRealTime(); + last_change_time_ = g_core->GetAppTimeMillisecs(); } checked_ = value; } void CheckBoxWidget::Activate() { - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kSwish3)); + g_base->audio->PlaySound(g_base->assets->SysSound(base::SysSoundID::kSwish3)); checked_ = !checked_; check_dirty_ = true; - last_change_time_ = GetRealTime(); - if (on_value_change_call_.exists()) { + last_change_time_ = g_core->GetAppTimeMillisecs(); + if (auto* call = on_value_change_call_.Get()) { PythonRef args(Py_BuildValue("(O)", checked_ ? Py_True : Py_False), PythonRef::kSteal); // Call this in the next cycle (don't want to risk mucking with UI from // within a UI loop) - g_logic->PushPythonWeakCallArgs( - Object::WeakRef(on_value_change_call_), args); + call->ScheduleWeak(args); } } -auto CheckBoxWidget::HandleMessage(const WidgetMessage& m) -> bool { +auto CheckBoxWidget::HandleMessage(const base::WidgetMessage& m) -> bool { // How far outside button touches register. float left_overlap, top_overlap, right_overlap, bottom_overlap; - if (g_platform->IsRunningOnDesktop()) { + if (g_core->platform->IsRunningOnDesktop()) { left_overlap = 3.0f; top_overlap = 1.0f; right_overlap = 0.0f; @@ -266,7 +265,7 @@ auto CheckBoxWidget::HandleMessage(const WidgetMessage& m) -> bool { } switch (m.type) { - case WidgetMessage::Type::kMouseMove: { + case base::WidgetMessage::Type::kMouseMove: { float x = m.fval1; float y = m.fval2; bool claimed = (m.fval3 > 0.0f); @@ -278,7 +277,7 @@ auto CheckBoxWidget::HandleMessage(const WidgetMessage& m) -> bool { && (y >= (-bottom_overlap)) && (y < (height_ + top_overlap))); return mouse_over_; } - case WidgetMessage::Type::kMouseDown: { + case base::WidgetMessage::Type::kMouseDown: { float x = m.fval1; float y = m.fval2; if ((x >= (-left_overlap)) && (x < (width_ + right_overlap)) @@ -290,7 +289,7 @@ auto CheckBoxWidget::HandleMessage(const WidgetMessage& m) -> bool { return false; } } - case WidgetMessage::Type::kMouseUp: { + case base::WidgetMessage::Type::kMouseUp: { float x = m.fval1; float y = m.fval2; bool claimed = (m.fval3 > 0.0f); @@ -320,4 +319,4 @@ auto CheckBoxWidget::HandleMessage(const WidgetMessage& m) -> bool { void CheckBoxWidget::OnLanguageChange() { text_.OnLanguageChange(); } -} // namespace ballistica +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui/widget/check_box_widget.h b/src/ballistica/ui_v1/widget/check_box_widget.h similarity index 79% rename from src/ballistica/ui/widget/check_box_widget.h rename to src/ballistica/ui_v1/widget/check_box_widget.h index af697509..a2bb3c1f 100644 --- a/src/ballistica/ui/widget/check_box_widget.h +++ b/src/ballistica/ui_v1/widget/check_box_widget.h @@ -1,21 +1,21 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_UI_WIDGET_CHECK_BOX_WIDGET_H_ -#define BALLISTICA_UI_WIDGET_CHECK_BOX_WIDGET_H_ +#ifndef BALLISTICA_UI_V1_WIDGET_CHECK_BOX_WIDGET_H_ +#define BALLISTICA_UI_V1_WIDGET_CHECK_BOX_WIDGET_H_ #include -#include "ballistica/graphics/renderer.h" -#include "ballistica/ui/widget/text_widget.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/ui_v1/widget/text_widget.h" -namespace ballistica { +namespace ballistica::ui_v1 { // Check box interface widget. class CheckBoxWidget : public Widget { public: CheckBoxWidget(); ~CheckBoxWidget() override; - void Draw(RenderPass* pass, bool transparent) override; + void Draw(base::RenderPass* pass, bool transparent) override; void SetWidth(float widthIn); void SetHeight(float heightIn); auto GetWidth() -> float override { return width_; } @@ -35,7 +35,7 @@ class CheckBoxWidget : public Widget { color_g_ = g; color_b_ = b; } - auto HandleMessage(const WidgetMessage& m) -> bool override; + auto HandleMessage(const base::WidgetMessage& m) -> bool override; void Activate() override; auto IsSelectable() -> bool override { return true; } auto GetWidgetTypeName() -> std::string override { return "checkbox"; } @@ -53,7 +53,7 @@ class CheckBoxWidget : public Widget { float color_r_{0.4f}; float color_g_{0.6f}; float color_b_{0.2f}; - ImageMesh box_image_mesh_; + base::ImageMesh box_image_mesh_; float check_width_{}; float check_height_{}; float check_center_x_{}; @@ -84,9 +84,9 @@ class CheckBoxWidget : public Widget { bool is_radio_button_{}; // Keep these at the bottom, so they'll be torn down first. - Object::Ref on_value_change_call_; + Object::Ref on_value_change_call_; }; -} // namespace ballistica +} // namespace ballistica::ui_v1 -#endif // BALLISTICA_UI_WIDGET_CHECK_BOX_WIDGET_H_ +#endif // BALLISTICA_UI_V1_WIDGET_CHECK_BOX_WIDGET_H_ diff --git a/src/ballistica/ui/widget/column_widget.cc b/src/ballistica/ui_v1/widget/column_widget.cc similarity index 82% rename from src/ballistica/ui/widget/column_widget.cc rename to src/ballistica/ui_v1/widget/column_widget.cc index 46f0e491..627fa4d2 100644 --- a/src/ballistica/ui/widget/column_widget.cc +++ b/src/ballistica/ui_v1/widget/column_widget.cc @@ -1,10 +1,10 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/ui/widget/column_widget.h" +#include "ballistica/ui_v1/widget/column_widget.h" -#include "ballistica/ui/ui.h" +#include "ballistica/base/ui/ui.h" -namespace ballistica { +namespace ballistica::ui_v1 { ColumnWidget::ColumnWidget() { set_background(false); // Influences default event handling; ew. @@ -16,9 +16,9 @@ ColumnWidget::ColumnWidget() { ColumnWidget::~ColumnWidget() = default; -auto ColumnWidget::HandleMessage(const WidgetMessage& m) -> bool { +auto ColumnWidget::HandleMessage(const base::WidgetMessage& m) -> bool { switch (m.type) { - case WidgetMessage::Type::kShow: { + case base::WidgetMessage::Type::kShow: { // Told to show something... send this along to our parent (we can't do // anything). Widget* w = parent_widget(); @@ -58,4 +58,4 @@ void ColumnWidget::UpdateLayout() { } } -} // namespace ballistica +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui/widget/column_widget.h b/src/ballistica/ui_v1/widget/column_widget.h similarity index 74% rename from src/ballistica/ui/widget/column_widget.h rename to src/ballistica/ui_v1/widget/column_widget.h index 91d2e432..55c6cb54 100644 --- a/src/ballistica/ui/widget/column_widget.h +++ b/src/ballistica/ui_v1/widget/column_widget.h @@ -1,20 +1,20 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_UI_WIDGET_COLUMN_WIDGET_H_ -#define BALLISTICA_UI_WIDGET_COLUMN_WIDGET_H_ +#ifndef BALLISTICA_UI_V1_WIDGET_COLUMN_WIDGET_H_ +#define BALLISTICA_UI_V1_WIDGET_COLUMN_WIDGET_H_ #include -#include "ballistica/ui/widget/container_widget.h" +#include "ballistica/ui_v1/widget/container_widget.h" -namespace ballistica { +namespace ballistica::ui_v1 { // Widget that arranges its children in a column. class ColumnWidget : public ContainerWidget { public: ColumnWidget(); ~ColumnWidget() override; - auto HandleMessage(const WidgetMessage& m) -> bool override; + auto HandleMessage(const base::WidgetMessage& m) -> bool override; auto GetWidgetTypeName() -> std::string override { return "column"; } auto set_left_border(float val) { left_border_ = val; } @@ -37,6 +37,6 @@ class ColumnWidget : public ContainerWidget { float bottom_border_{}; }; -} // namespace ballistica +} // namespace ballistica::ui_v1 -#endif // BALLISTICA_UI_WIDGET_COLUMN_WIDGET_H_ +#endif // BALLISTICA_UI_V1_WIDGET_COLUMN_WIDGET_H_ diff --git a/src/ballistica/ui/widget/container_widget.cc b/src/ballistica/ui_v1/widget/container_widget.cc similarity index 83% rename from src/ballistica/ui/widget/container_widget.cc rename to src/ballistica/ui_v1/widget/container_widget.cc index 5cdd262e..2ab1cd83 100644 --- a/src/ballistica/ui/widget/container_widget.cc +++ b/src/ballistica/ui_v1/widget/container_widget.cc @@ -1,21 +1,22 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/ui/widget/container_widget.h" +#include "ballistica/ui_v1/widget/container_widget.h" -#include "ballistica/audio/audio.h" -#include "ballistica/core/thread.h" -#include "ballistica/graphics/component/empty_component.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/input/input.h" -#include "ballistica/logic/logic.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_context_call.h" -#include "ballistica/ui/ui.h" -#include "ballistica/ui/widget/button_widget.h" -#include "ballistica/ui/widget/root_widget.h" -#include "ballistica/ui/widget/stack_widget.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/graphics/component/empty_component.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/math/random.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/ui_v1/widget/button_widget.h" +#include "ballistica/ui_v1/widget/root_widget.h" +#include "ballistica/ui_v1/widget/stack_widget.h" -namespace ballistica { +namespace ballistica::ui_v1 { // Set this to -100 or so to make sure culling is active // (things should visibly pop in & out of existence in that case). @@ -32,7 +33,8 @@ namespace ballistica { ContainerWidget::ContainerWidget(float width_in, float height_in) : width_(width_in), height_(height_in), - dynamics_update_time_(g_logic->master_time()) {} + dynamics_update_time_millisecs_( + static_cast(g_base->logic->display_time() * 1000.0)) {} ContainerWidget::~ContainerWidget() { BA_DEBUG_UI_READ_LOCK; @@ -41,16 +43,16 @@ ContainerWidget::~ContainerWidget() { } void ContainerWidget::SetOnActivateCall(PyObject* c) { - on_activate_call_ = Object::New(c); + on_activate_call_ = Object::New(c); } void ContainerWidget::SetOnOutsideClickCall(PyObject* c) { - on_outside_click_call_ = Object::New(c); + on_outside_click_call_ = Object::New(c); } -void ContainerWidget::DrawChildren(RenderPass* pass, bool draw_transparent, - float x_offset, float y_offset, - float scale) { +void ContainerWidget::DrawChildren(base::RenderPass* pass, + bool draw_transparent, float x_offset, + float y_offset, float scale) { BA_DEBUG_UI_READ_LOCK; // We're expected to fill z space 0..1 when we draw... so we need to divide @@ -104,7 +106,7 @@ void ContainerWidget::DrawChildren(RenderPass* pass, bool draw_transparent, layer_spacing = 0.0f; } } else { - layer_thickness = 1.0f / (widgets_.size()); + layer_thickness = 1.0f / static_cast(widgets_.size()); layer_spacing = layer_thickness; base_offset = 0; } @@ -138,7 +140,7 @@ void ContainerWidget::DrawChildren(RenderPass* pass, bool draw_transparent, // In opaque mode, draw our child widgets immediately front-to-back to best // make use of the z buffer. if (draw_transparent) { - EmptyComponent c(pass); + base::EmptyComponent c(pass); c.SetTransparent(true); for (size_t i = 0; i < w_count; i++) { @@ -185,7 +187,7 @@ void ContainerWidget::DrawChildren(RenderPass* pass, bool draw_transparent, } } c.PushTransform(); - float z_offs = base_offset + i * layer_spacing; + float z_offs = base_offset + static_cast(i) * layer_spacing; if (transition_scale_ != 1.0f) { c.Translate(bg_center_x_, bg_center_y_, 0); c.Scale(transition_scale_, transition_scale_, 1.0f); @@ -209,7 +211,7 @@ void ContainerWidget::DrawChildren(RenderPass* pass, bool draw_transparent, c.Submit(); } else { - EmptyComponent c(&(*pass)); + base::EmptyComponent c(&(*pass)); c.SetTransparent(false); for (int i = static_cast(w_count - 1); i >= 0; i--) { @@ -282,7 +284,7 @@ void ContainerWidget::DrawChildren(RenderPass* pass, bool draw_transparent, } } -auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { +auto ContainerWidget::HandleMessage(const base::WidgetMessage& m) -> bool { BA_DEBUG_UI_READ_LOCK; bool claimed = false; @@ -291,9 +293,9 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { } switch (m.type) { - case WidgetMessage::Type::kTextInput: - case WidgetMessage::Type::kKey: - case WidgetMessage::Type::kPaste: + case base::WidgetMessage::Type::kTextInput: + case base::WidgetMessage::Type::kKey: + case base::WidgetMessage::Type::kPaste: if (selected_widget_) { bool val = selected_widget_->HandleMessage(m); if (val != 0) { @@ -304,8 +306,8 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { // Ewww we dont want subclasses to do this // but we need to ourself for standalone containers - // ...reaaaly need to make ba.container() a subclass. - case WidgetMessage::Type::kShow: { + // ...reaaaly need to make babase.container() a subclass. + case base::WidgetMessage::Type::kShow: { // Told to show something.. send this along to our parent (we can't do // anything). Widget* w = parent_widget(); @@ -315,36 +317,35 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { return true; } - case WidgetMessage::Type::kStart: { + case base::WidgetMessage::Type::kStart: { if (selected_widget_) { if (selected_widget_->HandleMessage(m)) { claimed = true; } } - if (!claimed && start_button_.exists()) { + if (!claimed && start_button_.Exists()) { claimed = true; start_button_->Activate(); } break; } - case WidgetMessage::Type::kCancel: { + case base::WidgetMessage::Type::kCancel: { if (selected_widget_) { if (selected_widget_->HandleMessage(m)) { claimed = true; } } if (!claimed) { - if (cancel_button_.exists()) { + if (cancel_button_.Exists()) { claimed = true; cancel_button_->Activate(); - } else if (on_cancel_call_.exists()) { + } else if (auto* call = on_cancel_call_.Get()) { claimed = true; // Call this in the next cycle (don't wanna risk mucking with UI from // within a UI loop). - g_logic->PushPythonWeakCall( - Object::WeakRef(on_cancel_call_)); + call->ScheduleWeak(); } else { OnCancelCustom(); } @@ -352,16 +353,17 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { break; } - case WidgetMessage::Type::kTabNext: - case WidgetMessage::Type::kMoveRight: - case WidgetMessage::Type::kMoveDown: { - if (m.type == WidgetMessage::Type::kTabNext && !claims_tab_) { + case base::WidgetMessage::Type::kTabNext: + case base::WidgetMessage::Type::kMoveRight: + case base::WidgetMessage::Type::kMoveDown: { + if (m.type == base::WidgetMessage::Type::kTabNext && !claims_tab_) { break; } - if (m.type == WidgetMessage::Type::kMoveRight && !claims_left_right_) { + if (m.type == base::WidgetMessage::Type::kMoveRight + && !claims_left_right_) { break; } - if (m.type == WidgetMessage::Type::kMoveDown && !claims_up_down_) { + if (m.type == base::WidgetMessage::Type::kMoveDown && !claims_up_down_) { break; } if (selected_widget_) { @@ -371,9 +373,9 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { } if (!claimed) { if (!root_selectable_) { - if (m.type == WidgetMessage::Type::kMoveDown) { + if (m.type == base::WidgetMessage::Type::kMoveDown) { SelectDownWidget(); - } else if (m.type == WidgetMessage::Type::kMoveRight) { + } else if (m.type == base::WidgetMessage::Type::kMoveRight) { SelectRightWidget(); } else { SelectNextWidget(); @@ -387,16 +389,17 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { break; } - case WidgetMessage::Type::kTabPrev: - case WidgetMessage::Type::kMoveLeft: - case WidgetMessage::Type::kMoveUp: { - if (m.type == WidgetMessage::Type::kTabPrev && !claims_tab_) { + case base::WidgetMessage::Type::kTabPrev: + case base::WidgetMessage::Type::kMoveLeft: + case base::WidgetMessage::Type::kMoveUp: { + if (m.type == base::WidgetMessage::Type::kTabPrev && !claims_tab_) { break; } - if (m.type == WidgetMessage::Type::kMoveLeft && !claims_left_right_) { + if (m.type == base::WidgetMessage::Type::kMoveLeft + && !claims_left_right_) { break; } - if (m.type == WidgetMessage::Type::kMoveUp && !claims_up_down_) { + if (m.type == base::WidgetMessage::Type::kMoveUp && !claims_up_down_) { break; } if (selected_widget_) { @@ -406,9 +409,9 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { } if (!claimed) { if (!root_selectable_) { - if (m.type == WidgetMessage::Type::kMoveUp) { + if (m.type == base::WidgetMessage::Type::kMoveUp) { SelectUpWidget(); - } else if (m.type == WidgetMessage::Type::kMoveLeft) { + } else if (m.type == base::WidgetMessage::Type::kMoveLeft) { SelectLeftWidget(); } else { SelectPrevWidget(); @@ -422,7 +425,7 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { break; } - case WidgetMessage::Type::kActivate: { + case base::WidgetMessage::Type::kActivate: { if (root_selectable_) { Activate(); claimed = true; @@ -442,7 +445,7 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { break; } - case WidgetMessage::Type::kMouseMove: { + case base::WidgetMessage::Type::kMouseMove: { CheckLayout(); // Ignore mouse stuff while transitioning out. @@ -474,7 +477,7 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { float cy = y; TransformPointToChild(&cx, &cy, **i); if ((**i).HandleMessage( - WidgetMessage(m.type, nullptr, cx, cy, claimed))) { + base::WidgetMessage(m.type, nullptr, cx, cy, claimed))) { claimed = true; } if (modal_children_) { @@ -500,10 +503,10 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { break; } - case WidgetMessage::Type::kMouseWheel: - case WidgetMessage::Type::kMouseWheelH: - case WidgetMessage::Type::kMouseWheelVelocity: - case WidgetMessage::Type::kMouseWheelVelocityH: { + case base::WidgetMessage::Type::kMouseWheel: + case base::WidgetMessage::Type::kMouseWheelH: + case base::WidgetMessage::Type::kMouseWheelVelocity: + case base::WidgetMessage::Type::kMouseWheelVelocityH: { CheckLayout(); // Ignore mouse stuff while transitioning. @@ -526,8 +529,8 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { float cx = x; float cy = y; TransformPointToChild(&cx, &cy, ((**i))); - if ((**i).HandleMessage( - WidgetMessage(m.type, nullptr, cx, cy, amount, momentum))) { + if ((**i).HandleMessage(base::WidgetMessage(m.type, nullptr, cx, cy, + amount, momentum))) { claimed = true; break; } @@ -545,8 +548,8 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { } break; } - case WidgetMessage::Type::kScrollMouseDown: - case WidgetMessage::Type::kMouseDown: { + case base::WidgetMessage::Type::kScrollMouseDown: + case base::WidgetMessage::Type::kMouseDown: { CheckLayout(); // Ignore mouse stuff while transitioning. @@ -569,8 +572,8 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { float cx = x; float cy = y; TransformPointToChild(&cx, &cy, **i); - if ((**i).HandleMessage( - WidgetMessage(m.type, nullptr, cx, cy, click_count))) { + if ((**i).HandleMessage(base::WidgetMessage( + m.type, nullptr, cx, cy, static_cast(click_count)))) { claimed = true; break; } @@ -583,7 +586,7 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { // If its not yet claimed, see if its within our contained region, in // which case we claim it (only for regular mouse-downs). - if (!claimed && m.type == WidgetMessage::Type::kMouseDown) { + if (!claimed && m.type == base::WidgetMessage::Type::kMouseDown) { float bottom_overlap = 2; float top_overlap = 2; @@ -602,7 +605,8 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { // First click just selects. if (click_count == 1) { - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kTap)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kTap)); } } else { // Special case: If we've got a child text widget that's @@ -622,11 +626,10 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { } // Call our outside-click callback if unclaimed. - if (!claimed && on_outside_click_call_.exists()) { + if (!claimed && on_outside_click_call_.Exists()) { // Call this in the next cycle (don't wanna risk mucking with UI from // within a UI loop). - g_logic->PushPythonWeakCall( - Object::WeakRef(on_outside_click_call_)); + on_outside_click_call_->ScheduleWeak(); } // Always claim if they want. @@ -636,7 +639,7 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { } break; } - case WidgetMessage::Type::kMouseUp: { + case base::WidgetMessage::Type::kMouseUp: { CheckLayout(); dragging_ = false; float x = m.fval1; @@ -656,7 +659,7 @@ auto ContainerWidget::HandleMessage(const WidgetMessage& m) -> bool { float cy = y; TransformPointToChild(&cx, &cy, ((**i))); if ((**i).HandleMessage( - WidgetMessage(m.type, nullptr, cx, cy, claimed))) { + base::WidgetMessage(m.type, nullptr, cx, cy, claimed))) { claimed = true; } if (modal_children_) { @@ -700,7 +703,7 @@ auto ContainerWidget::GetMult(millisecs_t current_time, bool for_glow) const float m; // Only pulsate if regular widget highlighting is on and we're selected. - if (g_ui->ShouldHighlightWidgets()) { + if (g_base->ui->ShouldHighlightWidgets()) { if (IsHierarchySelected()) { m = 0.5f + std::abs(sinf(static_cast(current_time) * 0.006467f) @@ -717,10 +720,10 @@ auto ContainerWidget::GetMult(millisecs_t current_time, bool for_glow) const // Current or recent presses jack things up. if ((mouse_over_ && pressed_) - || (current_time - last_activate_time_ < 200)) { + || (current_time - last_activate_time_millisecs_ < 200)) { m *= 1.7f; m2 *= 1.1f; - } else if (g_ui->ShouldHighlightWidgets()) { + } else if (g_base->ui->ShouldHighlightWidgets()) { // Otherwise if we're supposed to always highlight all widgets, pulsate // when directly selected and glow softly when indirectly. if (IsHierarchySelected()) { @@ -764,7 +767,7 @@ auto ContainerWidget::GetDrawBrightness(millisecs_t current_time) const } void ContainerWidget::SetOnCancelCall(PyObject* call_tuple) { - on_cancel_call_ = Object::New(call_tuple); + on_cancel_call_ = Object::New(call_tuple); } void ContainerWidget::SetRootSelectable(bool enable) { @@ -776,7 +779,7 @@ void ContainerWidget::SetRootSelectable(bool enable) { } } -void ContainerWidget::Draw(RenderPass* pass, bool draw_transparent) { +void ContainerWidget::Draw(base::RenderPass* pass, bool draw_transparent) { BA_DEBUG_UI_READ_LOCK; CheckLayout(); @@ -789,10 +792,10 @@ void ContainerWidget::Draw(RenderPass* pass, bool draw_transparent) { if (!draw_transparent) { if (transition_type_ == TRANSITION_IN_SCALE) { - if (net_time - dynamics_update_time_ > 1000) - dynamics_update_time_ = net_time - 1000; - while (net_time - dynamics_update_time_ > 5) { - dynamics_update_time_ += 5; + if (net_time - dynamics_update_time_millisecs_ > 1000) + dynamics_update_time_millisecs_ = net_time - 1000; + while (net_time - dynamics_update_time_millisecs_ > 5) { + dynamics_update_time_millisecs_ += 5; d_transition_scale_ += std::min(0.2f, (1.0f - transition_scale_)) * 0.04f; d_transition_scale_ *= 0.87f; @@ -804,10 +807,10 @@ void ContainerWidget::Draw(RenderPass* pass, bool draw_transparent) { } } } else if (transition_type_ == TRANSITION_OUT_SCALE) { - if (net_time - dynamics_update_time_ > 1000) - dynamics_update_time_ = net_time - 1000; - while (net_time - dynamics_update_time_ > 5) { - dynamics_update_time_ += 5; + if (net_time - dynamics_update_time_millisecs_ > 1000) + dynamics_update_time_millisecs_ = net_time - 1000; + while (net_time - dynamics_update_time_millisecs_ > 5) { + dynamics_update_time_millisecs_ += 5; transition_scale_ -= 0.04f; if (transition_scale_ <= 0.0f) { transition_scale_ = 0.0f; @@ -815,22 +818,22 @@ void ContainerWidget::Draw(RenderPass* pass, bool draw_transparent) { // Probably not safe to delete ourself here since we're in // the draw loop, but we can push a call to do it. Object::WeakRef weakref(this); - g_logic->thread()->PushCall([weakref] { - Widget* w = weakref.get(); - if (w) g_ui->DeleteWidget(w); + g_base->logic->event_loop()->PushCall([weakref] { + Widget* w = weakref.Get(); + if (w) g_base->ui->DeleteWidget(w); }); return; } } } else { // Step our dynamics up to the present. - if (net_time - dynamics_update_time_ > 1000) - dynamics_update_time_ = net_time - 1000; - while (net_time - dynamics_update_time_ > 5) { - dynamics_update_time_ += 5; + if (net_time - dynamics_update_time_millisecs_ > 1000) + dynamics_update_time_millisecs_ = net_time - 1000; + while (net_time - dynamics_update_time_millisecs_ > 5) { + dynamics_update_time_millisecs_ += 5; if (transitioning_) { - millisecs_t t = dynamics_update_time_; + millisecs_t t = dynamics_update_time_millisecs_; if (t - transition_start_time_ < TRANSITION_DURATION) { float amt = static_cast(t - transition_start_time_) / TRANSITION_DURATION; @@ -870,9 +873,9 @@ void ContainerWidget::Draw(RenderPass* pass, bool draw_transparent) { // Probably not safe to delete ourself here since we're in the // draw loop, but we can set up an event to do it. Object::WeakRef weakref(this); - g_logic->thread()->PushCall([weakref] { - Widget* w = weakref.get(); - if (w) g_ui->DeleteWidget(w); + g_base->logic->event_loop()->PushCall([weakref] { + Widget* w = weakref.Get(); + if (w) g_base->ui->DeleteWidget(w); }); return; } @@ -930,22 +933,23 @@ void ContainerWidget::Draw(RenderPass* pass, bool draw_transparent) { // Update bg vals if need be // (we may need these even if bg is turned off so always calc them). if (bg_dirty_) { - SystemTextureID tex_id; + base::SysTextureID tex_id; float l_border, r_border, b_border, t_border; float width = r - l; float height = t - b; if (height > width * 0.6f) { - tex_id = SystemTextureID::kWindowHSmallVMed; - bg_model_transparent_i_d_ = SystemModelID::kWindowHSmallVMedTransparent; - bg_model_opaque_i_d_ = SystemModelID::kWindowHSmallVMedOpaque; + tex_id = base::SysTextureID::kWindowHSmallVMed; + bg_mesh_transparent_i_d_ = base::SysMeshID::kWindowHSmallVMedTransparent; + bg_mesh_opaque_i_d_ = base::SysMeshID::kWindowHSmallVMedOpaque; l_border = width * 0.07f; r_border = width * 0.19f; b_border = height * 0.1f; t_border = height * 0.07f; } else { - tex_id = SystemTextureID::kWindowHSmallVSmall; - bg_model_transparent_i_d_ = SystemModelID::kWindowHSmallVSmallTransparent; - bg_model_opaque_i_d_ = SystemModelID::kWindowHSmallVSmallOpaque; + tex_id = base::SysTextureID::kWindowHSmallVSmall; + bg_mesh_transparent_i_d_ = + base::SysMeshID::kWindowHSmallVSmallTransparent; + bg_mesh_opaque_i_d_ = base::SysMeshID::kWindowHSmallVSmallOpaque; l_border = width * 0.12f; r_border = width * 0.19f; b_border = height * 0.45f; @@ -956,7 +960,7 @@ void ContainerWidget::Draw(RenderPass* pass, bool draw_transparent) { bg_center_x_ = l - l_border + bg_width_ * 0.5f; bg_center_y_ = b - b_border + bg_height_ * 0.5f; if (background_) { - tex_ = g_assets->GetTexture(tex_id); + tex_ = g_base->assets->SysTexture(tex_id); } bg_dirty_ = false; } @@ -970,7 +974,7 @@ void ContainerWidget::Draw(RenderPass* pass, bool draw_transparent) { // Draw our window backing if we have one. if ((w > 0) && (h > 0)) { if (background_) { - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(draw_transparent); float s = 1.0f; if (transition_scale_ <= 0.9f && !transitioning_out_) { @@ -978,12 +982,12 @@ void ContainerWidget::Draw(RenderPass* pass, bool draw_transparent) { s = std::min((1.0f - amt) * 4.0f, 2.5f) + amt * 1.0f; } c.SetColor(red_ * s, green_ * s, blue_ * s, alpha_); - c.SetTexture(tex_.get()); + c.SetTexture(tex_.Get()); c.PushTransform(); c.Translate(bg_center_x_, bg_center_y_); c.Scale(bg_width_ * transition_scale_, bg_height_ * transition_scale_); - c.DrawModel(g_assets->GetModel( - draw_transparent ? bg_model_transparent_i_d_ : bg_model_opaque_i_d_)); + c.DrawMeshAsset(g_base->assets->SysMesh( + draw_transparent ? bg_mesh_transparent_i_d_ : bg_mesh_opaque_i_d_)); c.PopTransform(); c.Submit(); } @@ -1010,15 +1014,15 @@ void ContainerWidget::Draw(RenderPass* pass, bool draw_transparent) { glow_center_y_ = b - b_border + glow_height_ * 0.5f; glow_dirty_ = false; } - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(true); c.SetPremultiplied(true); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kGlow)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kGlow)); c.SetColor(0.25f * m, 0.25f * m, 0, 0.3f * m); c.PushTransform(); c.Translate(glow_center_x_, glow_center_y_); c.Scale(glow_width_, glow_height_); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage4x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage4x1)); c.PopTransform(); c.Submit(); } @@ -1054,17 +1058,17 @@ void ContainerWidget::TransformPointFromChild(float* x, float* y, } void ContainerWidget::Activate() { - last_activate_time_ = g_logic->master_time(); - if (on_activate_call_.exists()) { + last_activate_time_millisecs_ = + static_cast(g_base->logic->display_time() * 1000.0); + if (auto* call = on_activate_call_.Get()) { // Call this in the next cycle (don't wanna risk mucking with UI from within // a UI loop). - g_logic->PushPythonWeakCall( - Object::WeakRef(on_activate_call_)); + call->ScheduleWeak(); } } void ContainerWidget::AddWidget(Widget* w) { - BA_PRECONDITION(InLogicThread()); + BA_PRECONDITION(g_base->InLogicThread()); Object::WeakRef weakthis(this); { BA_DEBUG_UI_WRITE_LOCK; @@ -1082,13 +1086,15 @@ void ContainerWidget::AddWidget(Widget* w) { // direct selected child (which may not affect the global selection). if (is_window_stack_ && (is_overlay_window_stack_ - || !g_ui->root_widget()->overlay_window_stack()->HasChildren())) { + || !g_base->ui->root_widget() + ->overlay_window_stack() + ->HasChildren())) { w->GlobalSelect(); // Special case for the main window stack; whenever a window is added, // update the toolbar state for the topmost living container. if (is_main_window_stack_) { - g_ui->root_widget()->UpdateForFocusedWindow(); + g_base->ui->root_widget()->UpdateForFocusedWindow(); } } else { SelectWidget(w); @@ -1097,7 +1103,7 @@ void ContainerWidget::AddWidget(Widget* w) { } // Select actions we run above may trigger user code which may kill us. - if (!weakthis.exists()) { + if (!weakthis.Exists()) { return; } @@ -1151,20 +1157,21 @@ void ContainerWidget::SetTransition(TransitionType t) { return; } parent->CheckLayout(); - millisecs_t net_time = g_logic->master_time(); + auto display_time_millisecs = + static_cast(g_base->logic->display_time() * 1000.0); transition_type_ = t; // Scale transitions are simpler. if (t == TRANSITION_IN_SCALE) { - transition_start_time_ = net_time; - dynamics_update_time_ = net_time; + transition_start_time_ = display_time_millisecs; + dynamics_update_time_millisecs_ = display_time_millisecs; transitioning_ = true; transitioning_out_ = false; transition_scale_ = 0.0f; d_transition_scale_ = 0.0f; } else if (t == TRANSITION_OUT_SCALE) { - transition_start_time_ = net_time; - dynamics_update_time_ = net_time; + transition_start_time_ = display_time_millisecs; + dynamics_update_time_millisecs_ = display_time_millisecs; transitioning_ = true; transitioning_out_ = true; } else { @@ -1172,49 +1179,49 @@ void ContainerWidget::SetTransition(TransitionType t) { // animate an offset to slide on/off screen. float screen_min_x = 0.0f; float screen_min_y = 0.0f; - float screen_max_x = g_graphics->screen_virtual_width(); - float screen_max_y = g_graphics->screen_virtual_height(); + float screen_max_x = g_base->graphics->screen_virtual_width(); + float screen_max_y = g_base->graphics->screen_virtual_height(); ScreenPointToWidget(&screen_min_x, &screen_min_y); ScreenPointToWidget(&screen_max_x, &screen_max_y); // In case we're mid-transition, this avoids hitches. float y_offs = 2.0f; if (t == TRANSITION_IN_LEFT) { - transition_start_time_ = net_time; + transition_start_time_ = display_time_millisecs; transition_start_offset_ = screen_min_x - width_ - 100; transition_offset_x_smoothed_ = transition_start_offset_; transition_offset_y_smoothed_ = (RandomFloat() > 0.5f) ? y_offs : -y_offs; transition_target_offset_ = 0; transitioning_ = true; - dynamics_update_time_ = net_time; + dynamics_update_time_millisecs_ = display_time_millisecs; transitioning_out_ = false; } else if (t == TRANSITION_IN_RIGHT) { - transition_start_time_ = net_time; + transition_start_time_ = display_time_millisecs; transition_start_offset_ = screen_max_x + 100; transition_offset_x_smoothed_ = transition_start_offset_; transition_offset_y_smoothed_ = (RandomFloat() > 0.5f) ? y_offs : -y_offs; transition_target_offset_ = 0; transitioning_ = true; - dynamics_update_time_ = net_time; + dynamics_update_time_millisecs_ = display_time_millisecs; transitioning_out_ = false; } else if (t == TRANSITION_OUT_LEFT) { - transition_start_time_ = net_time; + transition_start_time_ = display_time_millisecs; transition_start_offset_ = transition_offset_x_; transition_target_offset_ = -2.0f * (screen_max_x - screen_min_x); transition_offset_x_smoothed_ = transition_start_offset_; transition_offset_y_smoothed_ = 0.0f; transitioning_ = true; - dynamics_update_time_ = net_time; + dynamics_update_time_millisecs_ = display_time_millisecs; transitioning_out_ = true; ignore_input_ = true; } else if (t == TRANSITION_OUT_RIGHT) { - transition_start_time_ = net_time; + transition_start_time_ = display_time_millisecs; transition_start_offset_ = transition_offset_x_; transition_target_offset_ = 2.0f * (screen_max_x - screen_min_x); transition_offset_x_smoothed_ = transition_start_offset_; transition_offset_y_smoothed_ = 0.0f; transitioning_ = true; - dynamics_update_time_ = net_time; + dynamics_update_time_millisecs_ = display_time_millisecs; transitioning_out_ = true; ignore_input_ = true; } @@ -1225,7 +1232,7 @@ void ContainerWidget::SetTransition(TransitionType t) { // *immediately* (otherwise we'd have to wait for our transition to complete // before the toolbar switches). if (transitioning_ && transitioning_out_ && parent->is_main_window_stack_) { - g_ui->root_widget()->UpdateForFocusedWindow(); + g_base->ui->root_widget()->UpdateForFocusedWindow(); } } @@ -1268,7 +1275,7 @@ void ContainerWidget::DeleteWidget(Widget* w) { if (is_overlay_window_stack_) { if (widgets_.empty()) { // Eww this logic should be in some sort of controller. - g_ui->root_widget()->ReselectLastSelectedWidget(); + g_base->ui->root_widget()->ReselectLastSelectedWidget(); return; } } @@ -1286,7 +1293,7 @@ void ContainerWidget::DeleteWidget(Widget* w) { // direct selected child (which may not affect the global selection). if (is_window_stack_ && (is_overlay_window_stack_ - || !g_ui->root_widget() + || !g_base->ui->root_widget() ->overlay_window_stack() ->HasChildren())) { (**i).GlobalSelect(); @@ -1301,7 +1308,7 @@ void ContainerWidget::DeleteWidget(Widget* w) { // Special case: if we're the main window stack, // update the active toolbar/etc. if (is_main_window_stack_) { - g_ui->root_widget()->UpdateForFocusedWindow(); + g_base->ui->root_widget()->UpdateForFocusedWindow(); } } @@ -1339,8 +1346,8 @@ void ContainerWidget::ShowWidget(Widget* w) { float ty = (w->ty() - buffer_bottom) * s; float width = (w->GetWidth() + buffer_left + buffer_right) * s; float height = (w->GetHeight() + buffer_bottom + buffer_top) * s; - HandleMessage(WidgetMessage(WidgetMessage::Type::kShow, nullptr, tx, ty, - width, height)); + HandleMessage(base::WidgetMessage(base::WidgetMessage::Type::kShow, nullptr, + tx, ty, width, height)); } void ContainerWidget::SelectWidget(Widget* w, SelectionCause c) { @@ -1435,14 +1442,14 @@ auto ContainerWidget::GetClosestLeftWidget(float our_x, float our_y, float x, y; float closest_val = 9999.0f; for (auto i = widgets_.begin(); i != widgets_.end(); i++) { - assert(i->exists()); + assert(i->Exists()); (**i).GetCenter(&x, &y); float slope = std::abs(x - our_x) / (std::max(0.001f, std::abs(y - our_y))); slope = std::min( slope, AUTO_SELECT_SLOPE_CLAMP); // Beyond this, just go by distance. float slope_weighted = AUTO_SELECT_SLOPE_WEIGHT * slope + (1.0f - AUTO_SELECT_SLOPE_WEIGHT) * 1.0f; - if (i->get() != ignore_widget && x < our_x && slope > AUTO_SELECT_MIN_SLOPE + if (i->Get() != ignore_widget && x < our_x && slope > AUTO_SELECT_MIN_SLOPE && (**i).IsSelectable() && (**i).IsSelectableViaKeys()) { // Take distance diff and multiply by our slope. float xdist = x - our_x; @@ -1452,7 +1459,7 @@ auto ContainerWidget::GetClosestLeftWidget(float our_x, float our_y, dist / std::max(0.001f, slope_weighted + AUTO_SELECT_SLOPE_OFFSET); if (val < closest_val || w == nullptr) { closest_val = val; - w = i->get(); + w = i->Get(); } } } @@ -1465,14 +1472,14 @@ auto ContainerWidget::GetClosestRightWidget(float our_x, float our_y, float x, y; float closest_val = 9999.0f; for (auto i = widgets_.begin(); i != widgets_.end(); i++) { - assert(i->exists()); + assert(i->Exists()); (**i).GetCenter(&x, &y); float slope = std::abs(x - our_x) / (std::max(0.001f, std::abs(y - our_y))); slope = std::min( slope, AUTO_SELECT_SLOPE_CLAMP); // beyond this, just go by distance float slopeWeighted = AUTO_SELECT_SLOPE_WEIGHT * slope + (1.0f - AUTO_SELECT_SLOPE_WEIGHT) * 1.0f; - if (i->get() != ignore_widget && x > our_x && slope > AUTO_SELECT_MIN_SLOPE + if (i->Get() != ignore_widget && x > our_x && slope > AUTO_SELECT_MIN_SLOPE && (**i).IsSelectable() && (**i).IsSelectableViaKeys()) { // Take distance diff and multiply by our slope. float xDist = x - our_x; @@ -1482,7 +1489,7 @@ auto ContainerWidget::GetClosestRightWidget(float our_x, float our_y, dist / std::max(0.001f, slopeWeighted + AUTO_SELECT_SLOPE_OFFSET); if (val < closest_val || w == nullptr) { closest_val = val; - w = i->get(); + w = i->Get(); } } } @@ -1495,14 +1502,14 @@ auto ContainerWidget::GetClosestUpWidget(float our_x, float our_y, float x, y; float closest_val = 9999.0f; for (auto i = widgets_.begin(); i != widgets_.end(); i++) { - assert(i->exists()); + assert(i->Exists()); (**i).GetCenter(&x, &y); float slope = std::abs(y - our_y) / (std::max(0.001f, std::abs(x - our_x))); slope = std::min( slope, AUTO_SELECT_SLOPE_CLAMP); // Beyond this, just go by distance. float slopeWeighted = AUTO_SELECT_SLOPE_WEIGHT * slope + (1.0f - AUTO_SELECT_SLOPE_WEIGHT) * 1.0f; - if (i->get() != ignoreWidget && y > our_y && slope > AUTO_SELECT_MIN_SLOPE + if (i->Get() != ignoreWidget && y > our_y && slope > AUTO_SELECT_MIN_SLOPE && (**i).IsSelectable() && (**i).IsSelectableViaKeys()) { // Take distance diff and multiply by our slope. float xDist = x - our_x; @@ -1512,7 +1519,7 @@ auto ContainerWidget::GetClosestUpWidget(float our_x, float our_y, dist / std::max(0.001f, slopeWeighted + AUTO_SELECT_SLOPE_OFFSET); if (val < closest_val || w == nullptr) { closest_val = val; - w = i->get(); + w = i->Get(); } } } @@ -1525,14 +1532,14 @@ auto ContainerWidget::GetClosestDownWidget(float our_x, float our_y, float x, y; float closest_val = 9999.0f; for (auto i = widgets_.begin(); i != widgets_.end(); i++) { - assert(i->exists()); + assert(i->Exists()); (**i).GetCenter(&x, &y); float slope = std::abs(y - our_y) / (std::max(0.001f, std::abs(x - our_x))); slope = std::min( slope, AUTO_SELECT_SLOPE_CLAMP); // Beyond this, just go by distance. float slopeWeighted = AUTO_SELECT_SLOPE_WEIGHT * slope + (1.0f - AUTO_SELECT_SLOPE_WEIGHT) * 1.0f; - if (i->get() != ignoreWidget && y < our_y && slope > AUTO_SELECT_MIN_SLOPE + if (i->Get() != ignoreWidget && y < our_y && slope > AUTO_SELECT_MIN_SLOPE && (**i).IsSelectable() && (**i).IsSelectableViaKeys()) { // Take distance diff and multiply by our slope. float xDist = x - our_x; @@ -1542,7 +1549,7 @@ auto ContainerWidget::GetClosestDownWidget(float our_x, float our_y, dist / std::max(0.001f, slopeWeighted + AUTO_SELECT_SLOPE_OFFSET); if (val < closest_val || w == nullptr) { closest_val = val; - w = i->get(); + w = i->Get(); } } } @@ -1552,7 +1559,8 @@ auto ContainerWidget::GetClosestDownWidget(float our_x, float our_y, void ContainerWidget::SelectDownWidget() { BA_DEBUG_UI_READ_LOCK; - if (!g_ui || !g_ui->root_widget() || !g_ui->screen_root_widget()) { + if (!g_base->ui || !g_base->ui->root_widget() + || !g_base->ui->screen_root_widget()) { BA_LOG_ONCE(LogLevel::kError, "SelectDownWidget called before UI init."); return; } @@ -1573,9 +1581,9 @@ void ContainerWidget::SelectDownWidget() { float x = our_x; float y = our_y; WidgetPointToScreen(&x, &y); - g_ui->root_widget()->ScreenPointToWidget(&x, &y); - w = g_ui->root_widget()->GetClosestDownWidget( - x, y, g_ui->screen_root_widget()); + g_base->ui->root_widget()->ScreenPointToWidget(&x, &y); + w = g_base->ui->root_widget()->GetClosestDownWidget( + x, y, g_base->ui->screen_root_widget()); } // When we find no viable targets for an autoselect widget we do // nothing. @@ -1592,7 +1600,8 @@ void ContainerWidget::SelectDownWidget() { // Avoid tap sounds and whatnot if we're just re-selecting ourself. if (w != selected_widget_) { w->GlobalSelect(); - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kTap)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kTap)); } } } else { @@ -1616,7 +1625,8 @@ void ContainerWidget::SelectDownWidget() { void ContainerWidget::SelectUpWidget() { BA_DEBUG_UI_READ_LOCK; - if (!g_ui || !g_ui->root_widget() || !g_ui->screen_root_widget()) { + if (!g_base->ui || !g_base->ui->root_widget() + || !g_base->ui->screen_root_widget()) { BA_LOG_ONCE(LogLevel::kError, "SelectUpWidget called before UI init."); return; } @@ -1637,9 +1647,9 @@ void ContainerWidget::SelectUpWidget() { float x = our_x; float y = our_y; WidgetPointToScreen(&x, &y); - g_ui->root_widget()->ScreenPointToWidget(&x, &y); - w = g_ui->root_widget()->GetClosestUpWidget( - x, y, g_ui->screen_root_widget()); + g_base->ui->root_widget()->ScreenPointToWidget(&x, &y); + w = g_base->ui->root_widget()->GetClosestUpWidget( + x, y, g_base->ui->screen_root_widget()); } // When we find no viable targets for an autoselect widget we do // nothing. @@ -1656,7 +1666,8 @@ void ContainerWidget::SelectUpWidget() { // Avoid tap sounds and whatnot if we're just re-selecting ourself. if (w != selected_widget_) { w->GlobalSelect(); - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kTap)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kTap)); } } } else { @@ -1680,7 +1691,8 @@ void ContainerWidget::SelectUpWidget() { void ContainerWidget::SelectLeftWidget() { BA_DEBUG_UI_READ_LOCK; - if (!g_ui || !g_ui->root_widget() || !g_ui->screen_root_widget()) { + if (!g_base->ui || !g_base->ui->root_widget() + || !g_base->ui->screen_root_widget()) { BA_LOG_ONCE(LogLevel::kError, "SelectLeftWidget called before UI init."); return; } @@ -1707,7 +1719,8 @@ void ContainerWidget::SelectLeftWidget() { // Avoid tap sounds and whatnot if we're just re-selecting ourself. if (w != selected_widget_) { w->GlobalSelect(); - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kTap)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kTap)); } } } else { @@ -1730,7 +1743,8 @@ void ContainerWidget::SelectLeftWidget() { void ContainerWidget::SelectRightWidget() { BA_DEBUG_UI_READ_LOCK; - if (!g_ui || !g_ui->root_widget() || !g_ui->screen_root_widget()) { + if (!g_base->ui || !g_base->ui->root_widget() + || !g_base->ui->screen_root_widget()) { BA_LOG_ONCE(LogLevel::kError, "SelectRightWidget called before UI init."); return; } @@ -1758,7 +1772,8 @@ void ContainerWidget::SelectRightWidget() { // Avoid tap sounds and whatnot if we're just re-selecting ourself. if (w != selected_widget_) { w->GlobalSelect(); - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kTap)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kTap)); } } } else { @@ -1782,14 +1797,16 @@ void ContainerWidget::SelectRightWidget() { void ContainerWidget::SelectNextWidget() { BA_DEBUG_UI_READ_LOCK; - if (!g_ui || !g_ui->root_widget() || !g_ui->screen_root_widget()) { + if (!g_base->ui || !g_base->ui->root_widget() + || !g_base->ui->screen_root_widget()) { BA_LOG_ONCE(LogLevel::kError, "SelectNextWidget called before UI init."); return; } - millisecs_t old_last_prev_next_time = last_prev_next_time_; + millisecs_t old_last_prev_next_time = last_prev_next_time_millisecs_; if (should_print_list_exit_instructions_) { - last_prev_next_time_ = g_logic->master_time(); + last_prev_next_time_millisecs_ = + static_cast(g_base->logic->display_time() * 1000.0); } // Grab the iterator for our selected widget if possible. @@ -1838,7 +1855,8 @@ void ContainerWidget::SelectNextWidget() { } if ((**i).IsSelectable() && (**i).IsSelectableViaKeys()) { SelectWidget(&(**i), SelectionCause::NEXT_SELECTED); - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kTap)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kTap)); return; } i++; @@ -1849,21 +1867,22 @@ void ContainerWidget::SelectNextWidget() { void ContainerWidget::PrintExitListInstructions( millisecs_t old_last_prev_next_time) { if (should_print_list_exit_instructions_) { - millisecs_t t = g_logic->master_time(); + auto t = static_cast(g_base->logic->display_time() * 1000.0); if ((t - old_last_prev_next_time > 250) && (t - last_list_exit_instructions_print_time_ > 5000)) { last_list_exit_instructions_print_time_ = t; - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kErrorBeep)); - std::string s = g_logic->GetResourceString("arrowsToExitListText"); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kErrorBeep)); + std::string s = g_base->assets->GetResourceString("arrowsToExitListText"); { // Left arrow. - Utils::StringReplaceOne(&s, "${LEFT}", - g_logic->CharStr(SpecialChar::kLeftArrow)); + Utils::StringReplaceOne( + &s, "${LEFT}", g_base->assets->CharStr(SpecialChar::kLeftArrow)); } { // Right arrow. - Utils::StringReplaceOne(&s, "${RIGHT}", - g_logic->CharStr(SpecialChar::kRightArrow)); + Utils::StringReplaceOne( + &s, "${RIGHT}", g_base->assets->CharStr(SpecialChar::kRightArrow)); } ScreenMessage(s); } @@ -1873,9 +1892,10 @@ void ContainerWidget::PrintExitListInstructions( void ContainerWidget::SelectPrevWidget() { BA_DEBUG_UI_READ_LOCK; - millisecs_t old_last_prev_next_time = last_prev_next_time_; + millisecs_t old_last_prev_next_time = last_prev_next_time_millisecs_; if (should_print_list_exit_instructions_) { - last_prev_next_time_ = g_logic->master_time(); + last_prev_next_time_millisecs_ = + static_cast(g_base->logic->display_time() * 1000.0); } // Grab the iterator for our selected widget if possible. @@ -1925,7 +1945,8 @@ void ContainerWidget::SelectPrevWidget() { if ((**i).IsSelectable() && (**i).IsSelectableViaKeys()) { SelectWidget(&(**i), SelectionCause::PREV_SELECTED); - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kTap)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kTap)); return; } i++; @@ -1934,7 +1955,7 @@ void ContainerWidget::SelectPrevWidget() { auto ContainerWidget::HasKeySelectableChild() const -> bool { for (auto i = widgets_.begin(); i != widgets_.end(); i++) { - assert(i->exists()); + assert(i->Exists()); if ((**i).IsSelectable() && (**i).IsSelectableViaKeys()) { return true; } @@ -1964,10 +1985,10 @@ void ContainerWidget::MarkForUpdate() { void ContainerWidget::OnLanguageChange() { for (auto&& widget : widgets_) { - if (widget.exists()) { + if (widget.Exists()) { widget->OnLanguageChange(); } } } -} // namespace ballistica +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui/widget/container_widget.h b/src/ballistica/ui_v1/widget/container_widget.h similarity index 89% rename from src/ballistica/ui/widget/container_widget.h rename to src/ballistica/ui_v1/widget/container_widget.h index 21df774c..30946207 100644 --- a/src/ballistica/ui/widget/container_widget.h +++ b/src/ballistica/ui_v1/widget/container_widget.h @@ -1,14 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_UI_WIDGET_CONTAINER_WIDGET_H_ -#define BALLISTICA_UI_WIDGET_CONTAINER_WIDGET_H_ +#ifndef BALLISTICA_UI_V1_WIDGET_CONTAINER_WIDGET_H_ +#define BALLISTICA_UI_V1_WIDGET_CONTAINER_WIDGET_H_ #include #include -#include "ballistica/ui/widget/widget.h" +#include "ballistica/base/base.h" +#include "ballistica/ui_v1/widget/widget.h" -namespace ballistica { +namespace ballistica::ui_v1 { // Base class for widgets that contain other widgets. class ContainerWidget : public Widget { @@ -16,9 +17,9 @@ class ContainerWidget : public Widget { explicit ContainerWidget(float width = 0, float height = 0); ~ContainerWidget() override; - void Draw(RenderPass* pass, bool transparent) override; + void Draw(base::RenderPass* pass, bool transparent) override; - auto HandleMessage(const WidgetMessage& m) -> bool override; + auto HandleMessage(const base::WidgetMessage& m) -> bool override; enum TransitionType { TRANSITION_OUT_LEFT, @@ -79,7 +80,7 @@ class ContainerWidget : public Widget { auto is_window_stack() const -> bool { return is_window_stack_; } auto GetChildCount() const -> int { - assert(InLogicThread()); + assert(g_base->InLogicThread()); return static_cast(widgets_.size()); } void Clear(); @@ -172,7 +173,7 @@ class ContainerWidget : public Widget { // Note that the offsets here are purely for visual transitions and things; // the UI itself only knows about the standard widget transform values. - void DrawChildren(RenderPass* pass, bool transparent, float x_offset, + void DrawChildren(base::RenderPass* pass, bool transparent, float x_offset, float y_offset, float scale); void SetSelected(bool s, SelectionCause cause) override; void MarkForUpdate(); @@ -217,12 +218,12 @@ class ContainerWidget : public Widget { float green_{0.37f}; float blue_{0.49f}; float alpha_{1.0f}; - Object::Ref tex_; - SystemModelID bg_model_transparent_i_d_{}; - SystemModelID bg_model_opaque_i_d_{}; + Object::Ref tex_; + base::SysMeshID bg_mesh_transparent_i_d_{}; + base::SysMeshID bg_mesh_opaque_i_d_{}; float glow_width_{}, glow_height_{}, glow_center_x_{}, glow_center_y_{}; float bg_width_{}, bg_height_{}, bg_center_x_{}, bg_center_y_{}; - millisecs_t last_activate_time_{}; + millisecs_t last_activate_time_millisecs_{}; millisecs_t transition_start_time_{}; float transition_target_offset_{}; float drag_x_{}, drag_y_{}; @@ -235,7 +236,7 @@ class ContainerWidget : public Widget { float transition_start_offset_{}; float transition_scale_{1.0f}; float d_transition_scale_{}; - millisecs_t dynamics_update_time_{}; + millisecs_t dynamics_update_time_millisecs_{}; bool bg_dirty_{true}; bool glow_dirty_{true}; bool transitioning_{}; @@ -257,7 +258,7 @@ class ContainerWidget : public Widget { bool single_depth_{true}; bool single_depth_root_{}; bool should_print_list_exit_instructions_{}; - millisecs_t last_prev_next_time_{}; + millisecs_t last_prev_next_time_millisecs_{}; millisecs_t last_list_exit_instructions_print_time_{}; Widget* selected_widget_{}; Widget* prev_selected_widget_{}; @@ -267,11 +268,11 @@ class ContainerWidget : public Widget { // Keep these at the bottom so they're torn down first. // ...hmm that seems fragile; should I add explicit code to kill them? - Object::Ref on_activate_call_; - Object::Ref on_outside_click_call_; - Object::Ref on_cancel_call_; + Object::Ref on_activate_call_; + Object::Ref on_outside_click_call_; + Object::Ref on_cancel_call_; }; -} // namespace ballistica +} // namespace ballistica::ui_v1 -#endif // BALLISTICA_UI_WIDGET_CONTAINER_WIDGET_H_ +#endif // BALLISTICA_UI_V1_WIDGET_CONTAINER_WIDGET_H_ diff --git a/src/ballistica/ui/widget/h_scroll_widget.cc b/src/ballistica/ui_v1/widget/h_scroll_widget.cc similarity index 87% rename from src/ballistica/ui/widget/h_scroll_widget.cc rename to src/ballistica/ui_v1/widget/h_scroll_widget.cc index 49e0e2b7..c054ddd6 100644 --- a/src/ballistica/ui/widget/h_scroll_widget.cc +++ b/src/ballistica/ui_v1/widget/h_scroll_widget.cc @@ -1,18 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/ui/widget/h_scroll_widget.h" +#include "ballistica/ui_v1/widget/h_scroll_widget.h" -#include "ballistica/generic/real_timer.h" -#include "ballistica/graphics/component/empty_component.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/ui/ui.h" +#include "ballistica/base/graphics/component/empty_component.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/support/app_timer.h" +#include "ballistica/base/ui/ui.h" -namespace ballistica { +namespace ballistica::ui_v1 { const float kHMargin = 5.0f; HScrollWidget::HScrollWidget() - : touch_mode_(!g_platform->IsRunningOnDesktop()) { + : touch_mode_(!g_core->platform->IsRunningOnDesktop()) { set_draggable(false); set_claims_left_right(false); set_claims_tab(false); @@ -20,13 +20,13 @@ HScrollWidget::HScrollWidget() HScrollWidget::~HScrollWidget() = default; -void HScrollWidget::HandleRealTimerExpired(RealTimer* t) { +void HScrollWidget::OnTouchDelayTimerExpired() { if (touch_held_) { // Pass a mouse-down event if we haven't moved. if (!touch_is_scrolling_ && !touch_down_sent_) { - ContainerWidget::HandleMessage( - WidgetMessage(WidgetMessage::Type::kMouseDown, nullptr, touch_x_, - touch_y_, static_cast(touch_held_click_count_))); + ContainerWidget::HandleMessage(base::WidgetMessage( + base::WidgetMessage::Type::kMouseDown, nullptr, touch_x_, touch_y_, + static_cast(touch_held_click_count_))); touch_down_sent_ = true; } else { } @@ -93,13 +93,13 @@ void HScrollWidget::ClampThumb(bool velocity_clamp, bool position_clamp) { } } -auto HScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { +auto HScrollWidget::HandleMessage(const base::WidgetMessage& m) -> bool { BA_DEBUG_UI_READ_LOCK; bool claimed = false; bool pass = true; float bottom_overlap = 3; switch (m.type) { - case WidgetMessage::Type::kShow: { + case base::WidgetMessage::Type::kShow: { claimed = true; pass = false; auto i = widgets().begin(); @@ -148,7 +148,7 @@ auto HScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { MarkForUpdate(); break; } - case WidgetMessage::Type::kMouseMove: { + case base::WidgetMessage::Type::kMouseMove: { float x = m.fval1; float y = m.fval2; bool claimed2 = (m.fval3 > 0.0f); @@ -206,8 +206,8 @@ auto HScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { // eyes the click is canceled. if (touch_down_sent_ && !touch_up_sent_) { ContainerWidget::HandleMessage( - WidgetMessage(WidgetMessage::Type::kMouseUp, nullptr, - m.fval1, m.fval2, true)); + base::WidgetMessage(base::WidgetMessage::Type::kMouseUp, + nullptr, m.fval1, m.fval2, true)); touch_up_sent_ = true; } } @@ -254,7 +254,7 @@ auto HScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { } break; } - case WidgetMessage::Type::kMouseUp: { + case base::WidgetMessage::Type::kMouseUp: { mouse_held_scroll_down_ = false; mouse_held_scroll_up_ = false; mouse_held_thumb_ = false; @@ -274,15 +274,15 @@ auto HScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { // If we're not claiming it and we haven't sent a mouse_down yet due // to our delay, send that first. if (!claimed2 && !touch_down_sent_) { - ContainerWidget::HandleMessage(WidgetMessage( - WidgetMessage::Type::kMouseDown, nullptr, m.fval1, m.fval2, - static_cast(touch_held_click_count_))); + ContainerWidget::HandleMessage(base::WidgetMessage( + base::WidgetMessage::Type::kMouseDown, nullptr, m.fval1, + m.fval2, static_cast(touch_held_click_count_))); touch_down_sent_ = true; } if (touch_down_sent_ && !touch_up_sent_) { ContainerWidget::HandleMessage( - WidgetMessage(WidgetMessage::Type::kMouseUp, nullptr, m.fval1, - m.fval2, claimed2)); + base::WidgetMessage(base::WidgetMessage::Type::kMouseUp, + nullptr, m.fval1, m.fval2, claimed2)); touch_up_sent_ = true; } return true; @@ -296,14 +296,15 @@ auto HScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { float y = m.fval2; if (!((y >= 0.0f) && (y < height()) && (x >= 0.0f) && (x < width()))) { pass = false; - ContainerWidget::HandleMessage(WidgetMessage( - WidgetMessage::Type::kMouseUp, nullptr, m.fval1, m.fval2, true)); + ContainerWidget::HandleMessage( + base::WidgetMessage(base::WidgetMessage::Type::kMouseUp, nullptr, + m.fval1, m.fval2, true)); } break; } - case WidgetMessage::Type::kMouseWheelVelocityH: { + case base::WidgetMessage::Type::kMouseWheelVelocityH: { float x = m.fval1; float y = m.fval2; if ((x >= 0.0f) && (x < width()) && (y >= 0.0f) && (y < height())) { @@ -348,7 +349,8 @@ auto HScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { inertia_scroll_rate_ = smoothing * inertia_scroll_rate_ + (1.0f - smoothing) * new_val; } - last_velocity_event_time_ = g_logic->master_time(); + last_velocity_event_time_millisecs_ = + static_cast(g_base->logic->display_time() * 1000.0); MarkForUpdate(); } else { // Not within our widget; dont allow children to claim. @@ -356,7 +358,7 @@ auto HScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { } break; } - case WidgetMessage::Type::kMouseWheelH: { + case base::WidgetMessage::Type::kMouseWheelH: { float x = m.fval1; float y = m.fval2; if ((x >= 0.0f) && (x < width()) && (y >= 0.0f) && (y < height())) { @@ -370,8 +372,8 @@ auto HScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { } break; } - case WidgetMessage::Type::kScrollMouseDown: - case WidgetMessage::Type::kMouseDown: { + case base::WidgetMessage::Type::kScrollMouseDown: + case base::WidgetMessage::Type::kMouseDown: { float x = m.fval1; float y = m.fval2; @@ -404,13 +406,13 @@ auto HScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { // Top level touches eventually get passed as mouse-downs if no // scrolling has started. if (static_cast(m.type)) { - touch_delay_timer_ = - Object::New>(150, false, this); + touch_delay_timer_ = base::NewAppTimer( + 150, false, [this] { OnTouchDelayTimerExpired(); }); } // If we're handling a scroll-touch, take note that we need to // decide whether to disown the touch or not. - if (m.type == WidgetMessage::Type::kScrollMouseDown) { + if (m.type == base::WidgetMessage::Type::kScrollMouseDown) { new_scroll_touch_ = true; } } @@ -464,7 +466,7 @@ auto HScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { } // If it was a mouse-down and we claimed it, set ourself as selected. - if (m.type == WidgetMessage::Type::kMouseDown && claimed) { + if (m.type == base::WidgetMessage::Type::kMouseDown && claimed) { GlobalSelect(); } return claimed; @@ -510,7 +512,7 @@ void HScrollWidget::UpdateLayout() { thumb_dirty_ = true; } -void HScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { +void HScrollWidget::Draw(base::RenderPass* pass, bool draw_transparent) { have_drawn_ = true; millisecs_t current_time = pass->frame_def()->base_time(); float prev_child_offset_h_smoothed = child_offset_h_smoothed_; @@ -542,7 +544,7 @@ void HScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { child_offset_h_ += inertia_scroll_rate_; if (!has_momentum_ - && (current_time - last_velocity_event_time_ > 1000 / 30)) { + && (current_time - last_velocity_event_time_millisecs_ > 1000 / 30)) { inertia_scroll_rate_ = 0; } @@ -567,7 +569,7 @@ void HScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { CheckLayout(); - Vector3f tilt = 0.02f * g_graphics->tilt(); + Vector3f tilt = 0.02f * g_base->graphics->tilt(); float extra_offs_x = tilt.y; float extra_offs_y = -tilt.x; @@ -578,7 +580,7 @@ void HScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { // Begin clipping for children. { - EmptyComponent c(pass); + base::EmptyComponent c(pass); c.SetTransparent(draw_transparent); c.ScissorPush(Rect(l + border_width_, b + border_height_ + 1, l + (width() - border_width_ - 0), @@ -595,7 +597,7 @@ void HScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { // End clipping. { - EmptyComponent c(pass); + base::EmptyComponent c(pass); c.SetTransparent(draw_transparent); c.ScissorPop(); c.Submit(); @@ -622,15 +624,16 @@ void HScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { trough_dirty_ = false; } - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(true); c.SetColor(1, 1, 1, border_opacity_); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kUIAtlas)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kUIAtlas)); c.PushTransform(); c.Translate(trough_center_x_, trough_center_y_, 0.7f); c.Scale(trough_width_, trough_height_, 0.1f); c.Rotate(-90, 0, 0, 1); - c.DrawModel(g_assets->GetModel(SystemModelID::kScrollBarTroughTransparent)); + c.DrawMeshAsset( + g_base->assets->SysMesh(base::SysMeshID::kScrollBarTroughTransparent)); c.PopTransform(); c.Submit(); } @@ -669,7 +672,7 @@ void HScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { thumb_dirty_ = false; } - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(draw_transparent); // float c_scale = 1.0f; // if (mouse_held_thumb_) { @@ -707,26 +710,12 @@ void HScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { c.FlipCullFace(); c.Rotate(-90, 0, 0, 1); - // on touch, just draw these transiently -#if 1 if (draw_transparent) { - c.DrawModel(g_assets->GetModel( - sb_thumb_width > 100 ? SystemModelID::kScrollBarThumbSimple - : SystemModelID::kScrollBarThumbShortSimple)); - } -#else - if (draw_transparent) { - c.DrawModel(g_assets->GetModel( + c.DrawMeshAsset(g_base->assets->SysMesh( sb_thumb_width > 100 - ? Assets::SCROLL_BAR_THUMB_TRANSPARENT_MODEL - : Assets::SCROLL_BAR_THUMB_SHORT_TRANSPARENT_MODEL)); - } else { - c.DrawModel(g_assets->GetModel( - sb_thumb_width > 100 - ? Assets::SCROLL_BAR_THUMB_OPAQUE_MODEL - : Assets::SCROLL_BAR_THUMB_SHORT_OPAQUE_MODEL)); + ? base::SysMeshID::kScrollBarThumbSimple + : base::SysMeshID::kScrollBarThumbShortSimple)); } -#endif c.FlipCullFace(); c.PopTransform(); @@ -753,21 +742,21 @@ void HScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { outline_center_y_ = b2 - b_border + 0.5f * outline_height_; shadow_dirty_ = false; } - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(true); c.SetColor(1, 1, 1, border_opacity_); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kScrollWidget)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kScrollWidget)); c.PushTransform(); c.Translate(outline_center_x_, outline_center_y_, 0.9f); c.Scale(outline_width_, outline_height_, 0.1f); - c.DrawModel(g_assets->GetModel(SystemModelID::kSoftEdgeOutside)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kSoftEdgeOutside)); c.PopTransform(); c.Submit(); } // if selected, do glow at depth 0.9-1.0 if (draw_transparent && IsHierarchySelected() - && g_ui->ShouldHighlightWidgets() && highlight_ + && g_base->ui->ShouldHighlightWidgets() && highlight_ && border_opacity_ > 0.0f) { float m = 0.8f + std::abs(sinf(static_cast(current_time) * 0.006467f)) @@ -789,18 +778,19 @@ void HScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { glow_center_y_ = b2 - b_border + 0.5f * glow_height_; glow_dirty_ = false; } - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(true); c.SetPremultiplied(true); c.SetColor(0.4f * m, 0.5f * m, 0.05f * m, 0.0f); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kScrollWidgetGlow)); + c.SetTexture( + g_base->assets->SysTexture(base::SysTextureID::kScrollWidgetGlow)); c.PushTransform(); c.Translate(glow_center_x_, glow_center_y_, 0.9f); c.Scale(glow_width_, glow_height_, 0.1f); - c.DrawModel(g_assets->GetModel(SystemModelID::kSoftEdgeOutside)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kSoftEdgeOutside)); c.PopTransform(); c.Submit(); } } -} // namespace ballistica +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui/widget/h_scroll_widget.h b/src/ballistica/ui_v1/widget/h_scroll_widget.h similarity index 83% rename from src/ballistica/ui/widget/h_scroll_widget.h rename to src/ballistica/ui_v1/widget/h_scroll_widget.h index 6ff5a375..5655fab4 100644 --- a/src/ballistica/ui/widget/h_scroll_widget.h +++ b/src/ballistica/ui_v1/widget/h_scroll_widget.h @@ -1,24 +1,21 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_UI_WIDGET_H_SCROLL_WIDGET_H_ -#define BALLISTICA_UI_WIDGET_H_SCROLL_WIDGET_H_ +#ifndef BALLISTICA_UI_V1_WIDGET_H_SCROLL_WIDGET_H_ +#define BALLISTICA_UI_V1_WIDGET_H_SCROLL_WIDGET_H_ #include -#include "ballistica/ui/widget/container_widget.h" +#include "ballistica/ui_v1/widget/container_widget.h" -namespace ballistica { - -template -class RealTimer; +namespace ballistica::ui_v1 { // A scroll-box container widget. class HScrollWidget : public ContainerWidget { public: HScrollWidget(); ~HScrollWidget() override; - void Draw(RenderPass* pass, bool transparent) override; - auto HandleMessage(const WidgetMessage& m) -> bool override; + void Draw(base::RenderPass* pass, bool transparent) override; + auto HandleMessage(const base::WidgetMessage& m) -> bool override; auto GetWidgetTypeName() -> std::string override { return "scroll"; } void set_capture_arrows(bool val) { capture_arrows_ = val; } void SetWidth(float w) override { @@ -35,7 +32,7 @@ class HScrollWidget : public ContainerWidget { center_small_content_ = val; MarkForUpdate(); } - void HandleRealTimerExpired(RealTimer* t); + void OnTouchDelayTimerExpired(); void setColor(float r, float g, float b) { color_red_ = r; color_green_ = g; @@ -61,7 +58,7 @@ class HScrollWidget : public ContainerWidget { bool shadow_dirty_{true}; bool glow_dirty_{true}; bool thumb_dirty_{true}; - millisecs_t last_velocity_event_time_{}; + millisecs_t last_velocity_event_time_millisecs_{}; float touch_fade_{}; bool center_small_content_{}; float center_offset_x_{}; @@ -112,9 +109,9 @@ class HScrollWidget : public ContainerWidget { bool have_drawn_{}; millisecs_t inertia_scroll_update_time_{}; float inertia_scroll_rate_{}; - Object::Ref > touch_delay_timer_; + Object::Ref touch_delay_timer_; }; -} // namespace ballistica +} // namespace ballistica::ui_v1 -#endif // BALLISTICA_UI_WIDGET_H_SCROLL_WIDGET_H_ +#endif // BALLISTICA_UI_V1_WIDGET_H_SCROLL_WIDGET_H_ diff --git a/src/ballistica/ui/widget/image_widget.cc b/src/ballistica/ui_v1/widget/image_widget.cc similarity index 56% rename from src/ballistica/ui/widget/image_widget.cc rename to src/ballistica/ui_v1/widget/image_widget.cc index 28b2a369..dfee1c10 100644 --- a/src/ballistica/ui/widget/image_widget.cc +++ b/src/ballistica/ui_v1/widget/image_widget.cc @@ -1,33 +1,37 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/ui/widget/image_widget.h" +#include "ballistica/ui_v1/widget/image_widget.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/logic/logic.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/logic/logic.h" -namespace ballistica { +namespace ballistica::ui_v1 { -ImageWidget::ImageWidget() : birth_time_{g_logic->master_time()} {} +ImageWidget::ImageWidget() + : birth_time_millisecs_{ + static_cast(g_base->logic->display_time() * 1000.0)} {} ImageWidget::~ImageWidget() = default; auto ImageWidget::GetWidth() -> float { return width_; } auto ImageWidget::GetHeight() -> float { return height_; } -void ImageWidget::Draw(RenderPass* pass, bool draw_transparent) { +void ImageWidget::Draw(base::RenderPass* pass, bool draw_transparent) { if (opacity_ < 0.001f) { return; } millisecs_t current_time = pass->frame_def()->base_time(); - Vector3f tilt = tilt_scale_ * 0.01f * g_graphics->tilt(); - if (draw_control_parent()) tilt += 0.02f * g_graphics->tilt(); + Vector3f tilt = tilt_scale_ * 0.01f * g_base->graphics->tilt(); + if (draw_control_parent()) tilt += 0.02f * g_base->graphics->tilt(); float extra_offs_x = -tilt.y; float extra_offs_y = tilt.x; // Simple transition. - float transition = (birth_time_ + transition_delay_) - current_time; + float transition = + (static_cast(birth_time_millisecs_) + transition_delay_) + - static_cast(current_time); if (transition > 0) { extra_offs_x -= transition * 4.0f; } @@ -37,12 +41,10 @@ void ImageWidget::Draw(RenderPass* pass, bool draw_transparent) { float b = 0; float t = b + height_; - if (texture_.exists()) { - if (texture_->texture_data()->loaded() - && ((!tint_texture_.exists()) - || tint_texture_->texture_data()->loaded()) - && ((!mask_texture_.exists()) - || mask_texture_->texture_data()->loaded())) { + if (texture_.Exists()) { + if (texture_->loaded() + && ((!tint_texture_.Exists()) || tint_texture_->loaded()) + && ((!mask_texture_.Exists()) || mask_texture_->loaded())) { if (image_dirty_) { image_width_ = r - l; image_height_ = t - b; @@ -51,32 +53,33 @@ void ImageWidget::Draw(RenderPass* pass, bool draw_transparent) { image_dirty_ = false; } - Object::Ref model_opaque_used; - if (model_opaque_.exists()) { - model_opaque_used = model_opaque_->model_data(); + Object::Ref mesh_opaque_used; + if (mesh_opaque_.Exists()) { + mesh_opaque_used = mesh_opaque_; } - Object::Ref model_transparent_used; - if (model_transparent_.exists()) { - model_transparent_used = model_transparent_->model_data(); + Object::Ref mesh_transparent_used; + if (mesh_transparent_.Exists()) { + mesh_transparent_used = mesh_transparent_; } bool draw_radial_opaque = false; bool draw_radial_transparent = false; - // if no meshes were provided, use default image models - if ((!model_opaque_.exists()) && (!model_transparent_.exists())) { + // If no meshes were provided, use default image meshes. + if ((!mesh_opaque_.Exists()) && (!mesh_transparent_.Exists())) { if (has_alpha_channel_) { if (radial_amount_ < 1.0f) { draw_radial_transparent = true; } else { - model_transparent_used = - g_assets->GetModel(SystemModelID::kImage1x1); + mesh_transparent_used = + g_base->assets->SysMesh(base::SysMeshID::kImage1x1); } } else { if (radial_amount_ < 1.0f) { draw_radial_opaque = true; } else { - model_opaque_used = g_assets->GetModel(SystemModelID::kImage1x1); + mesh_opaque_used = + g_base->assets->SysMesh(base::SysMeshID::kImage1x1); } } } @@ -89,47 +92,47 @@ void ImageWidget::Draw(RenderPass* pass, bool draw_transparent) { // Opaque portion may get drawn transparent or opaque depending on our // global opacity. - if (model_opaque_used.exists() || draw_radial_opaque) { + if (mesh_opaque_used.Exists() || draw_radial_opaque) { bool should_draw = false; bool should_draw_transparent = false; - // Draw our opaque model in the opaque pass. + // Draw our opaque mesh in the opaque pass. if (!draw_transparent && opacity_ > 0.999f) { should_draw = true; should_draw_transparent = false; } else if (draw_transparent && opacity_ <= 0.999f) { - // Draw our opaque model in the transparent pass. + // Draw our opaque mesh in the transparent pass. should_draw = true; should_draw_transparent = true; } if (should_draw) { - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(should_draw_transparent); c.SetColor(color_red_ * db, color_green_ * db, color_blue_ * db, opacity_); c.SetTexture(texture_); - if (tint_texture_.exists()) { - c.SetColorizeTexture(tint_texture_); + if (tint_texture_.Exists()) { + c.SetColorizeTexture(tint_texture_.Get()); c.SetColorizeColor(tint_color_red_, tint_color_green_, tint_color_blue_); c.SetColorizeColor2(tint2_color_red_, tint2_color_green_, tint2_color_blue_); } - c.SetMaskTexture(mask_texture_); + c.SetMaskTexture(mask_texture_.Get()); c.PushTransform(); c.Translate(image_center_x_ + extra_offs_x, image_center_y_ + extra_offs_y); c.Scale(image_width_, image_height_, 1.0f); if (draw_radial_opaque) { - if (!radial_mesh_.exists()) { - radial_mesh_ = Object::NewDeferred(); + if (!radial_mesh_.Exists()) { + radial_mesh_ = Object::NewDeferred(); } - Graphics::DrawRadialMeter(&(*radial_mesh_), radial_amount_); + base::Graphics::DrawRadialMeter(&(*radial_mesh_), radial_amount_); c.Scale(0.5f, 0.5f, 1.0f); - c.DrawMesh(radial_mesh_.get()); + c.DrawMesh(radial_mesh_.Get()); } else { - c.DrawModel(model_opaque_used.get()); + c.DrawMeshAsset(mesh_opaque_used.Get()); } c.PopTransform(); c.Submit(); @@ -137,34 +140,34 @@ void ImageWidget::Draw(RenderPass* pass, bool draw_transparent) { } // Always-transparent portion. - if ((model_transparent_used.exists() || draw_radial_transparent) + if ((mesh_transparent_used.Exists() || draw_radial_transparent) && draw_transparent) { - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(true); c.SetColor(color_red_ * db, color_green_ * db, color_blue_ * db, opacity_); c.SetTexture(texture_); - if (tint_texture_.exists()) { - c.SetColorizeTexture(tint_texture_); + if (tint_texture_.Exists()) { + c.SetColorizeTexture(tint_texture_.Get()); c.SetColorizeColor(tint_color_red_, tint_color_green_, tint_color_blue_); c.SetColorizeColor2(tint2_color_red_, tint2_color_green_, tint2_color_blue_); } - c.SetMaskTexture(mask_texture_); + c.SetMaskTexture(mask_texture_.Get()); c.PushTransform(); c.Translate(image_center_x_ + extra_offs_x, image_center_y_ + extra_offs_y); c.Scale(image_width_, image_height_, 1.0f); if (draw_radial_transparent) { - if (!radial_mesh_.exists()) { - radial_mesh_ = Object::New(); + if (!radial_mesh_.Exists()) { + radial_mesh_ = Object::New(); } - Graphics::DrawRadialMeter(&(*radial_mesh_), radial_amount_); + base::Graphics::DrawRadialMeter(&(*radial_mesh_), radial_amount_); c.Scale(0.5f, 0.5f, 1.0f); - c.DrawMesh(radial_mesh_.get()); + c.DrawMesh(radial_mesh_.Get()); } else { - c.DrawModel(model_transparent_used.get()); + c.DrawMeshAsset(mesh_transparent_used.Get()); } c.PopTransform(); c.Submit(); @@ -173,8 +176,8 @@ void ImageWidget::Draw(RenderPass* pass, bool draw_transparent) { } } -auto ImageWidget::HandleMessage(const WidgetMessage& m) -> bool { +auto ImageWidget::HandleMessage(const base::WidgetMessage& m) -> bool { return false; } -} // namespace ballistica +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui/widget/image_widget.h b/src/ballistica/ui_v1/widget/image_widget.h similarity index 51% rename from src/ballistica/ui/widget/image_widget.h rename to src/ballistica/ui_v1/widget/image_widget.h index 18dc66a2..51376de7 100644 --- a/src/ballistica/ui/widget/image_widget.h +++ b/src/ballistica/ui_v1/widget/image_widget.h @@ -1,22 +1,22 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_UI_WIDGET_IMAGE_WIDGET_H_ -#define BALLISTICA_UI_WIDGET_IMAGE_WIDGET_H_ +#ifndef BALLISTICA_UI_V1_WIDGET_IMAGE_WIDGET_H_ +#define BALLISTICA_UI_V1_WIDGET_IMAGE_WIDGET_H_ #include -#include "ballistica/assets/component/model.h" -#include "ballistica/assets/component/texture.h" -#include "ballistica/ui/widget/widget.h" +#include "ballistica/base/assets/mesh_asset.h" +#include "ballistica/base/assets/texture_asset.h" +#include "ballistica/ui_v1/widget/widget.h" -namespace ballistica { +namespace ballistica::ui_v1 { class ImageWidget : public Widget { public: ImageWidget(); ~ImageWidget() override; - void Draw(RenderPass* pass, bool transparent) override; - auto HandleMessage(const WidgetMessage& m) -> bool override; + void Draw(base::RenderPass* pass, bool transparent) override; + auto HandleMessage(const base::WidgetMessage& m) -> bool override; void set_width(float width) { image_dirty_ = true; width_ = width; @@ -44,38 +44,16 @@ class ImageWidget : public Widget { tint2_color_blue_ = b; } void set_opacity(float o) { opacity_ = o; } - void SetTexture(Texture* val) { - if (val && !val->IsFromUIContext()) - throw Exception("texture is not from the UI context: " - + val->GetObjectDescription()); - texture_ = val; - } - void SetTintTexture(Texture* val) { - if (val && !val->IsFromUIContext()) - throw Exception("texture is not from the UI context: " - + val->GetObjectDescription()); - tint_texture_ = val; - } - void SetMaskTexture(Texture* val) { - if (val && !val->IsFromUIContext()) { - throw Exception("texture is not from the UI context: " - + val->GetObjectDescription()); - } - mask_texture_ = val; - } - void SetModelTransparent(Model* val) { - if (val && !val->IsFromUIContext()) { - throw Exception("model_transparent is not from UI context"); - } + void SetTexture(base::TextureAsset* val) { texture_ = val; } + void SetTintTexture(base::TextureAsset* val) { tint_texture_ = val; } + void SetMaskTexture(base::TextureAsset* val) { mask_texture_ = val; } + void SetMeshTransparent(base::MeshAsset* val) { image_dirty_ = true; - model_transparent_ = val; + mesh_transparent_ = val; } - void SetModelOpaque(Model* val) { - if (val && !val->IsFromUIContext()) { - throw Exception("model_opaque is not from UI context"); - } + void SetMeshOpaque(base::MeshAsset* val) { image_dirty_ = true; - model_opaque_ = val; + mesh_opaque_ = val; } auto GetWidgetTypeName() -> std::string override { return "image"; } void set_transition_delay(float val) { transition_delay_ = val; } @@ -85,13 +63,13 @@ class ImageWidget : public Widget { private: float tilt_scale_{1.0f}; float transition_delay_{}; - millisecs_t birth_time_{}; - Object::Ref texture_; - Object::Ref tint_texture_; - Object::Ref mask_texture_; - Object::Ref model_transparent_; - Object::Ref model_opaque_; - Object::Ref radial_mesh_; + millisecs_t birth_time_millisecs_{}; + Object::Ref texture_; + Object::Ref tint_texture_; + Object::Ref mask_texture_; + Object::Ref mesh_transparent_; + Object::Ref mesh_opaque_; + Object::Ref radial_mesh_; float image_width_{}; float image_height_{}; float image_center_x_{}; @@ -113,6 +91,6 @@ class ImageWidget : public Widget { float opacity_{1.0f}; }; -} // namespace ballistica +} // namespace ballistica::ui_v1 -#endif // BALLISTICA_UI_WIDGET_IMAGE_WIDGET_H_ +#endif // BALLISTICA_UI_V1_WIDGET_IMAGE_WIDGET_H_ diff --git a/src/ballistica/ui/widget/root_widget.cc b/src/ballistica/ui_v1/widget/root_widget.cc similarity index 87% rename from src/ballistica/ui/widget/root_widget.cc rename to src/ballistica/ui_v1/widget/root_widget.cc index c9028fc9..72e59c2b 100644 --- a/src/ballistica/ui/widget/root_widget.cc +++ b/src/ballistica/ui_v1/widget/root_widget.cc @@ -1,17 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/ui/widget/root_widget.h" +#include "ballistica/ui_v1/widget/root_widget.h" -#include "ballistica/graphics/renderer.h" -#include "ballistica/input/input.h" -#include "ballistica/logic/logic.h" -#include "ballistica/logic/session/host_session.h" -#include "ballistica/python/python.h" -#include "ballistica/ui/ui.h" -#include "ballistica/ui/widget/button_widget.h" -#include "ballistica/ui/widget/stack_widget.h" +#include "ballistica/base/app/app_mode.h" +#include "ballistica/base/graphics/renderer/renderer.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/ui_v1/python/ui_v1_python.h" +#include "ballistica/ui_v1/widget/button_widget.h" +#include "ballistica/ui_v1/widget/stack_widget.h" -namespace ballistica { +namespace ballistica::ui_v1 { // color we mult toolbars by in medium and large ui modes // (in small mode we keep them more the normal window color since everything @@ -47,9 +45,9 @@ struct RootWidget::ButtonDef { float depth_max{1.0f}; std::string label; std::string img; - std::string model_transparent; - std::string model_opaque; - Python::ObjID call{Python::ObjID::kEmptyCall}; + std::string mesh_transparent; + std::string mesh_opaque; + UIV1Python::ObjID call{UIV1Python::ObjID::kEmptyCall}; float color_r{1.0f}; float color_g{1.0f}; float color_b{1.0f}; @@ -114,7 +112,7 @@ RootWidget::~RootWidget() = default; auto RootWidget::AddCover(float h_align, VAlign v_align, float x, float y, float w, float h, float o) -> RootWidget::Button* { // currently just not doing these in vr mode - if (IsVRMode()) { + if (g_core->IsVRMode()) { return nullptr; } @@ -131,14 +129,14 @@ auto RootWidget::AddCover(float h_align, VAlign v_align, float x, float y, bd.color_g = 0.0f; bd.color_b = 0.0f; bd.opacity = o; - bd.call = Python::ObjID::kEmptyCall; + bd.call = UIV1Python::ObjID::kEmptyCall; bd.visibility_mask = static_cast(Widget::ToolbarVisibility::kMenuFullRoot); // when the user specifies no backing it means they intend to cover the screen // with a flat-ish window texture.. however this only applies to phone-size; // for other sizes we always draw a backing. - if (g_ui->scale() != UIScale::kSmall) { + if (g_base->ui->scale() != UIScale::kSmall) { bd.visibility_mask |= static_cast(Widget::ToolbarVisibility::kMenuFull); } @@ -152,7 +150,7 @@ auto RootWidget::AddCover(float h_align, VAlign v_align, float x, float y, void RootWidget::AddMeter(float h_align, float x, int type, float r, float g, float b, bool plus, const std::string& s) { - float yoffs = (g_ui->scale() == UIScale::kSmall) ? 0.0f : -7.0f; + float yoffs = (g_base->ui->scale() == UIScale::kSmall) ? 0.0f : -7.0f; float width = type == 1 ? 80.0f : 110.0f; // bar @@ -165,18 +163,18 @@ void RootWidget::AddMeter(float h_align, float x, int type, float r, float g, bd.x = x; bd.y = -36.0f + 10.0f + yoffs; bd.img = "uiAtlas2"; - bd.model_transparent = "currencyMeter"; + bd.mesh_transparent = "currencyMeter"; bd.selectable = false; bd.color_r = 0.32f; bd.color_g = 0.30f; bd.color_b = 0.4f; - if (g_ui->scale() != UIScale::kSmall) { + if (g_base->ui->scale() != UIScale::kSmall) { bd.color_r *= TOOLBAR_COLOR_R; bd.color_g *= TOOLBAR_COLOR_G; bd.color_b *= TOOLBAR_COLOR_B; } bd.depth_min = 0.3f; - bd.call = Python::ObjID::kEmptyCall; + bd.call = UIV1Python::ObjID::kEmptyCall; bd.visibility_mask = (static_cast(Widget::ToolbarVisibility::kMenuFull) | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); @@ -220,19 +218,19 @@ void RootWidget::AddMeter(float h_align, float x, int type, float r, float g, switch (type) { case 0: bd.img = "levelIcon"; - bd.call = Python::ObjID::kLevelIconPressCall; + bd.call = UIV1Python::ObjID::kLevelIconPressCall; break; case 1: bd.img = "trophy"; - bd.call = Python::ObjID::kTrophyIconPressCall; + bd.call = UIV1Python::ObjID::kTrophyIconPressCall; break; case 2: bd.img = "coin"; - bd.call = Python::ObjID::kCoinIconPressCall; + bd.call = UIV1Python::ObjID::kCoinIconPressCall; break; case 3: bd.img = "tickets"; - bd.call = Python::ObjID::kTicketIconPressCall; + bd.call = UIV1Python::ObjID::kTicketIconPressCall; break; #pragma clang diagnostic push @@ -286,17 +284,17 @@ void RootWidget::AddMeter(float h_align, float x, int type, float r, float g, bd.x = x - 68; bd.y = -36.0f + 11.0f + yoffs; bd.img = "uiAtlas2"; - bd.model_transparent = "currencyPlusButton"; + bd.mesh_transparent = "currencyPlusButton"; bd.color_r = 0.35f; bd.color_g = 0.35f; bd.color_b = 0.55f; - if (g_ui->scale() != UIScale::kSmall) { + if (g_base->ui->scale() != UIScale::kSmall) { bd.color_r *= TOOLBAR_COLOR_R; bd.color_g *= TOOLBAR_COLOR_G; bd.color_b *= TOOLBAR_COLOR_B; } bd.depth_min = 0.3f; - bd.call = Python::ObjID::kEmptyCall; + bd.call = UIV1Python::ObjID::kEmptyCall; bd.visibility_mask = (static_cast(Widget::ToolbarVisibility::kMenuFull) | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); @@ -333,7 +331,7 @@ void RootWidget::Setup() { bd.x = 40.0f; bd.y = -40.0f; bd.img = "nub"; - bd.call = Python::ObjID::kBackButtonPressCall; + bd.call = UIV1Python::ObjID::kBackButtonPressCall; bd.visibility_mask = (static_cast(Widget::ToolbarVisibility::kMenuMinimal) | static_cast(Widget::ToolbarVisibility::kMenuFull)); @@ -346,7 +344,7 @@ void RootWidget::Setup() { td.x = 5.0f; td.y = 3.0f; td.width = bd.width * 0.9f; - td.text = g_logic->CharStr(SpecialChar::kBack); + td.text = g_base->assets->CharStr(SpecialChar::kBack); td.color_a = 1.0f; td.scale = 2.0f; td.flatness = 0.0f; @@ -381,9 +379,9 @@ void RootWidget::Setup() { bd.y = -20.0f; bd.img = "uiAtlas2"; // if (g_ui->scale() != UIScale::kSmall) { - // bd.model_transparent = "toolbarBackingTop"; + // bd.mesh_transparent = "toolbarBackingTop"; // } else { - bd.model_transparent = "toolbarBackingTop2"; + bd.mesh_transparent = "toolbarBackingTop2"; // } bd.selectable = false; bd.color_r = 0.44f; @@ -400,7 +398,7 @@ void RootWidget::Setup() { // } bd.depth_min = 0.2f; // bd.call = ""; - bd.call = Python::ObjID::kEmptyCall; + bd.call = UIV1Python::ObjID::kEmptyCall; // bd.visibility_mask = // static_cast(Widget::ToolbarVisibility::kMenuFullRoot); @@ -417,20 +415,20 @@ void RootWidget::Setup() { bd.h_align = 0.5f; bd.v_align = VAlign::kTop; bd.width = 850.0f; - if (g_ui->scale() != UIScale::kSmall) { + if (g_base->ui->scale() != UIScale::kSmall) { bd.width = 850.0f; } bd.height = 90.0f; bd.x = 0.0f; bd.y = -20.0f; bd.img = "uiAtlas2"; - bd.model_transparent = "toolbarBackingTop2"; + bd.mesh_transparent = "toolbarBackingTop2"; bd.selectable = false; bd.color_r = 0.44f; bd.color_g = 0.41f; bd.color_b = 0.56f; bd.opacity = 1.0f; - if (g_ui->scale() != UIScale::kSmall) { + if (g_base->ui->scale() != UIScale::kSmall) { bd.color_r *= TOOLBAR_COLOR_R * TOOLBAR_BACK_COLOR_R; bd.color_g *= TOOLBAR_COLOR_G * TOOLBAR_BACK_COLOR_G; bd.color_b *= TOOLBAR_COLOR_B * TOOLBAR_BACK_COLOR_B; @@ -440,7 +438,7 @@ void RootWidget::Setup() { } bd.depth_min = 0.2f; // bd.call = ""; - bd.call = Python::ObjID::kEmptyCall; + bd.call = UIV1Python::ObjID::kEmptyCall; bd.visibility_mask = static_cast(Widget::ToolbarVisibility::kMenuFullRoot); bd.visibility_mask |= @@ -450,7 +448,7 @@ void RootWidget::Setup() { AddButton(bd); } - float yoffs = (g_ui->scale() == UIScale::kSmall) ? 0.0f : -10.0f; + float yoffs = (g_base->ui->scale() == UIScale::kSmall) ? 0.0f : -10.0f; // account button { @@ -460,18 +458,18 @@ void RootWidget::Setup() { bd.width = 160.0f; bd.height = 60.0f; bd.depth_min = 0.3f; - bd.x = (g_ui->scale() == UIScale::kSmall) ? 100.0f : -50.0f; + bd.x = (g_base->ui->scale() == UIScale::kSmall) ? 100.0f : -50.0f; bd.y = -24.0f + yoffs; bd.color_r = 0.56f; bd.color_g = 0.5f; bd.color_b = 0.73f; - if (g_ui->scale() != UIScale::kSmall) { + if (g_base->ui->scale() != UIScale::kSmall) { bd.color_r *= TOOLBAR_COLOR_R; bd.color_g *= TOOLBAR_COLOR_G; bd.color_b *= TOOLBAR_COLOR_B; } // bd.call = ""; - bd.call = Python::ObjID::kEmptyCall; + bd.call = UIV1Python::ObjID::kEmptyCall; bd.visibility_mask = (static_cast(Widget::ToolbarVisibility::kMenuFull) | static_cast(Widget::ToolbarVisibility::kMenuFullRoot)); @@ -515,7 +513,7 @@ void RootWidget::Setup() { } } - float anchorx = (g_ui->scale() == UIScale::kSmall) ? 0.3f : 0.25f; + float anchorx = (g_base->ui->scale() == UIScale::kSmall) ? 0.3f : 0.25f; AddMeter(anchorx, 200.0f - 148.0f, 0, 1.0f, 1.0f, 1.0f, false, "456/1000"); AddMeter(anchorx, 200.0f, 1, 1.0f, 1.0f, 1.0f, false, "123"); @@ -532,7 +530,7 @@ void RootWidget::Setup() { b.x = -110.0f; b.y = b.height * -0.41f; b.img = "usersButton"; - b.call = Python::ObjID::kFriendsButtonPressCall; + b.call = UIV1Python::ObjID::kFriendsButtonPressCall; b.visibility_mask = (static_cast(Widget::ToolbarVisibility::kInGame) | static_cast(Widget::ToolbarVisibility::kMenuMinimal) @@ -553,7 +551,7 @@ void RootWidget::Setup() { b.x = -36.0f; b.y = b.height * -0.48f; b.img = "menuButton"; - b.call = Python::ObjID::kBackButtonPressCall; + b.call = UIV1Python::ObjID::kBackButtonPressCall; b.color_r = 0.3f; b.color_g = 0.5f; b.color_b = 0.2f; @@ -661,7 +659,7 @@ void RootWidget::Setup() { float backingCoverG = backingG; float backingCoverB = backingB; float backingA = 1.0f; - if (g_ui->scale() != UIScale::kSmall) { + if (g_base->ui->scale() != UIScale::kSmall) { backingR *= TOOLBAR_COLOR_R * TOOLBAR_BACK_COLOR_R; backingG *= TOOLBAR_COLOR_G * TOOLBAR_BACK_COLOR_G; backingB *= TOOLBAR_COLOR_B * TOOLBAR_BACK_COLOR_B; @@ -689,7 +687,7 @@ void RootWidget::Setup() { bd.x = 0.0f; bd.y = 41.0f; bd.img = "uiAtlas2"; - bd.model_transparent = "toolbarBackingBottom2"; + bd.mesh_transparent = "toolbarBackingBottom2"; bd.selectable = false; bd.color_r = backingR; bd.color_g = backingG; @@ -698,7 +696,7 @@ void RootWidget::Setup() { bd.depth_min = 0.2f; // bd.call = ""; - bd.call = Python::ObjID::kEmptyCall; + bd.call = UIV1Python::ObjID::kEmptyCall; bd.visibility_mask = static_cast(Widget::ToolbarVisibility::kMenuFullRoot); bd.visibility_mask |= @@ -798,7 +796,7 @@ void RootWidget::Setup() { UpdateForFocusedWindow(nullptr); } -void RootWidget::Draw(RenderPass* pass, bool transparent) { +void RootWidget::Draw(base::RenderPass* pass, bool transparent) { // Opaque pass gets drawn first; use that as an opportunity to step up our // motion. if (!transparent) { @@ -812,7 +810,7 @@ void RootWidget::Draw(RenderPass* pass, bool transparent) { } auto RootWidget::AddButton(const ButtonDef& def) -> RootWidget::Button* { - ScopedSetContext cp(g_logic->GetUIContextTarget()); + base::ScopedSetContext ssc(nullptr); buttons_.emplace_back(); Button& b(buttons_.back()); b.x = b.x_smoothed = b.x_target = def.x; @@ -828,12 +826,12 @@ auto RootWidget::AddButton(const ButtonDef& def) -> RootWidget::Button* { b.widget->SetColor(def.color_r, def.color_g, def.color_b); b.widget->set_opacity(def.opacity); b.widget->set_auto_select(true); - b.widget->SetText(def.label); + b.widget->set_text(def.label); b.widget->set_enabled(def.selectable); b.widget->set_selectable(def.selectable); b.widget->SetDepthRange(def.depth_min, def.depth_max); - // make sure up/down moves focus into the main stack + // Make sure up/down moves focus into the main stack. assert(screen_stack_widget_ != nullptr); assert(b.v_align != VAlign::kCenter); if (b.v_align == VAlign::kTop) { @@ -841,28 +839,29 @@ auto RootWidget::AddButton(const ButtonDef& def) -> RootWidget::Button* { } else { b.widget->set_up_widget(screen_stack_widget_); } - // we wanna prevent anyone from redirecting these to point to outside widgets - // since we'll probably outlive those outside widgets + // We wanna prevent anyone from redirecting these to point to outside widgets + // since we'll probably outlive those outside widgets. b.widget->set_neighbors_locked(true); if (!def.img.empty()) { - b.widget->SetTexture(g_ui->GetTexture(def.img).get()); + b.widget->SetTexture(g_base->assets->GetTexture(def.img).Get()); } - if (!def.model_transparent.empty()) { - b.widget->SetModelTransparent(g_ui->GetModel(def.model_transparent).get()); + if (!def.mesh_transparent.empty()) { + b.widget->SetMeshTransparent( + g_base->assets->GetMesh(def.mesh_transparent).Get()); } - if (!def.model_opaque.empty()) { - b.widget->SetModelOpaque(g_ui->GetModel(def.model_opaque).get()); + if (!def.mesh_opaque.empty()) { + b.widget->SetMeshOpaque(g_base->assets->GetMesh(def.mesh_opaque).Get()); } - if (Python::ObjID::kEmptyCall != def.call) { - b.widget->set_on_activate_call(g_python->obj(def.call).get()); + if (def.call != UIV1Python::ObjID::kEmptyCall) { + b.widget->set_on_activate_call(g_ui_v1->python->objs().Get(def.call).Get()); } - AddWidget(b.widget.get()); + AddWidget(b.widget.Get()); return &b; } auto RootWidget::AddText(const TextDef& def) -> RootWidget::Text* { - ScopedSetContext cp(g_logic->GetUIContextTarget()); + base::ScopedSetContext ssc(nullptr); texts_.emplace_back(); Text& t(texts_.back()); t.button = def.button; @@ -878,11 +877,11 @@ auto RootWidget::AddText(const TextDef& def) -> RootWidget::Text* { t.widget->set_shadow(def.shadow); t.widget->set_flatness(def.flatness); t.widget->SetDepthRange(def.depth_min, def.depth_max); - assert(def.button->widget.exists()); - t.widget->set_draw_control_parent(def.button->widget.get()); + assert(def.button->widget.Exists()); + t.widget->set_draw_control_parent(def.button->widget.Get()); t.x = def.x; t.y = def.y; - AddWidget(t.widget.get()); + AddWidget(t.widget.Get()); return &t; } @@ -896,8 +895,7 @@ void RootWidget::UpdateForFocusedWindow() { void RootWidget::UpdateForFocusedWindow(Widget* widget) { // Take note if the current session is the main menu; we do a few things // differently there. - HostSession* s = g_logic->GetForegroundContext().GetHostSession(); - in_main_menu_ = (s ? s->is_main_menu() : false); + in_main_menu_ = g_base->app_mode->InMainMenu(); if (widget == nullptr) { toolbar_visibility_ = ToolbarVisibility::kInGame; @@ -932,7 +930,7 @@ void RootWidget::StepPositions(float dt) { static_cast(static_cast(toolbar_visibility_) & static_cast(b.visibility_mask)); - // when we're in the main menu, always disable the menu button + // When we're in the main menu, always disable the menu button // and shift the party button a bit to the right if (in_main_menu_) { if (&b == menu_button_) { @@ -944,15 +942,15 @@ void RootWidget::StepPositions(float dt) { } if (&b == back_button_) { // back button is always disabled in medium/large UI - if (g_ui->scale() != UIScale::kSmall) { + if (g_base->ui->scale() != UIScale::kSmall) { enable_button = false; } // whenever back button is enabled, left on account button should go to // it; otherwise it goes nowhere. - Widget* ab = account_button_->widget.get(); + Widget* ab = account_button_->widget.Get(); ab->set_neighbors_locked(false); - ab->set_left_widget(enable_button ? back_button_->widget.get() : ab); + ab->set_left_widget(enable_button ? back_button_->widget.Get() : ab); account_button_->widget->set_neighbors_locked(true); } @@ -1033,7 +1031,7 @@ void RootWidget::StepPositions(float dt) { void RootWidget::UpdateLayout() { // Now actually put things in place. base_scale_ = 1.0f; - switch (g_ui->scale()) { + switch (g_base->ui->scale()) { case UIScale::kLarge: base_scale_ = 0.6f; break; @@ -1045,10 +1043,9 @@ void RootWidget::UpdateLayout() { break; } - // TEST - cycle through our scales -#if 0 - { - int foo = time(nullptr) % 3; + // TEST - cycle through our scales + if (explicit_bool(false)) { + auto foo = time(nullptr) % 3; if (foo == 0) { base_scale_ = 1.0f; } else if (foo == 1) { @@ -1057,7 +1054,6 @@ void RootWidget::UpdateLayout() { base_scale_ = 0.5f; } } -#endif // Update the window stack. BA_DEBUG_UI_READ_LOCK; @@ -1078,11 +1074,11 @@ void RootWidget::UpdateLayout() { StepPositions(0.0f); } -auto RootWidget::HandleMessage(const WidgetMessage& m) -> bool { +auto RootWidget::HandleMessage(const base::WidgetMessage& m) -> bool { // If a cancel message comes through and our back button is active, fire our // back button. // ..in all other cases just do the default. - if (m.type == WidgetMessage::Type::kCancel && back_button_ != nullptr + if (m.type == base::WidgetMessage::Type::kCancel && back_button_ != nullptr && back_button_->widget->enabled() && !overlay_stack_widget_->HasChildren()) { back_button_->widget->Activate(); @@ -1120,21 +1116,21 @@ void RootWidget::OnCancelCustom() { auto RootWidget::GetSpecialWidget(const std::string& s) const -> Widget* { if (s == "party_button") { - return party_button_ ? party_button_->widget.get() : nullptr; + return party_button_ ? party_button_->widget.Get() : nullptr; } else if (s == "tickets_plus_button") { - return tickets_plus_button_ ? tickets_plus_button_->widget.get() : nullptr; + return tickets_plus_button_ ? tickets_plus_button_->widget.Get() : nullptr; } else if (s == "back_button") { - return back_button_ ? back_button_->widget.get() : nullptr; + return back_button_ ? back_button_->widget.Get() : nullptr; } else if (s == "account_button") { - return account_button_ ? account_button_->widget.get() : nullptr; + return account_button_ ? account_button_->widget.Get() : nullptr; } else if (s == "settings_button") { - return settings_button_ ? settings_button_->widget.get() : nullptr; + return settings_button_ ? settings_button_->widget.Get() : nullptr; } else if (s == "tickets_info_button") { - return tickets_info_button_ ? tickets_info_button_->widget.get() : nullptr; + return tickets_info_button_ ? tickets_info_button_->widget.Get() : nullptr; } else if (s == "overlay_stack") { return overlay_stack_widget_; } return nullptr; } -} // namespace ballistica +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui/widget/root_widget.h b/src/ballistica/ui_v1/widget/root_widget.h similarity index 82% rename from src/ballistica/ui/widget/root_widget.h rename to src/ballistica/ui_v1/widget/root_widget.h index 3949f373..fb9fa692 100644 --- a/src/ballistica/ui/widget/root_widget.h +++ b/src/ballistica/ui_v1/widget/root_widget.h @@ -1,14 +1,14 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_UI_WIDGET_ROOT_WIDGET_H_ -#define BALLISTICA_UI_WIDGET_ROOT_WIDGET_H_ +#ifndef BALLISTICA_UI_V1_WIDGET_ROOT_WIDGET_H_ +#define BALLISTICA_UI_V1_WIDGET_ROOT_WIDGET_H_ #include #include -#include "ballistica/ui/widget/container_widget.h" +#include "ballistica/ui_v1/widget/container_widget.h" -namespace ballistica { +namespace ballistica::ui_v1 { // Root-level widget; contains a top-bar, screen-stack, bottom-bar, menu-button, // etc. This is intended to replace RootUI. @@ -21,8 +21,8 @@ class RootWidget : public ContainerWidget { void SetOverlayWidget(StackWidget* w); void UpdateForFocusedWindow(); void Setup(); - auto HandleMessage(const WidgetMessage& m) -> bool override; - void Draw(RenderPass* pass, bool transparent) override; + auto HandleMessage(const base::WidgetMessage& m) -> bool override; + void Draw(base::RenderPass* pass, bool transparent) override; auto GetSpecialWidget(const std::string& s) const -> Widget*; auto base_scale() const -> float { return base_scale_; } auto overlay_window_stack() const -> StackWidget* { @@ -63,6 +63,6 @@ class RootWidget : public ContainerWidget { ToolbarVisibility toolbar_visibility_{ToolbarVisibility::kInGame}; }; -} // namespace ballistica +} // namespace ballistica::ui_v1 -#endif // BALLISTICA_UI_WIDGET_ROOT_WIDGET_H_ +#endif // BALLISTICA_UI_V1_WIDGET_ROOT_WIDGET_H_ diff --git a/src/ballistica/ui/widget/row_widget.cc b/src/ballistica/ui_v1/widget/row_widget.cc similarity index 75% rename from src/ballistica/ui/widget/row_widget.cc rename to src/ballistica/ui_v1/widget/row_widget.cc index 870b7b4b..cc985438 100644 --- a/src/ballistica/ui/widget/row_widget.cc +++ b/src/ballistica/ui_v1/widget/row_widget.cc @@ -1,10 +1,10 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/ui/widget/row_widget.h" +#include "ballistica/ui_v1/widget/row_widget.h" -#include "ballistica/ui/ui.h" +#include "ballistica/base/ui/ui.h" -namespace ballistica { +namespace ballistica::ui_v1 { RowWidget::RowWidget() { set_background(false); // Influences default event handling. @@ -19,9 +19,9 @@ RowWidget::~RowWidget() = default; auto RowWidget::GetWidgetTypeName() -> std::string { return "row"; } -auto RowWidget::HandleMessage(const WidgetMessage& m) -> bool { +auto RowWidget::HandleMessage(const base::WidgetMessage& m) -> bool { switch (m.type) { - case WidgetMessage::Type::kShow: { + case base::WidgetMessage::Type::kShow: { return false; } default: @@ -43,4 +43,4 @@ void RowWidget::UpdateLayout() { set_width(l); } -} // namespace ballistica +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui_v1/widget/row_widget.h b/src/ballistica/ui_v1/widget/row_widget.h new file mode 100644 index 00000000..89bce182 --- /dev/null +++ b/src/ballistica/ui_v1/widget/row_widget.h @@ -0,0 +1,26 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_UI_V1_WIDGET_ROW_WIDGET_H_ +#define BALLISTICA_UI_V1_WIDGET_ROW_WIDGET_H_ + +#include + +#include "ballistica/ui_v1/widget/container_widget.h" + +namespace ballistica::ui_v1 { + +// Layout widget for organizing widgets in a row +class RowWidget : public ContainerWidget { + public: + RowWidget(); + ~RowWidget() override; + auto HandleMessage(const base::WidgetMessage& m) -> bool override; + auto GetWidgetTypeName() -> std::string override; + + protected: + void UpdateLayout() override; +}; + +} // namespace ballistica::ui_v1 + +#endif // BALLISTICA_UI_V1_WIDGET_ROW_WIDGET_H_ diff --git a/src/ballistica/ui/widget/scroll_widget.cc b/src/ballistica/ui_v1/widget/scroll_widget.cc similarity index 85% rename from src/ballistica/ui/widget/scroll_widget.cc rename to src/ballistica/ui_v1/widget/scroll_widget.cc index 9c3b29f3..77dfe11a 100644 --- a/src/ballistica/ui/widget/scroll_widget.cc +++ b/src/ballistica/ui_v1/widget/scroll_widget.cc @@ -1,17 +1,18 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/ui/widget/scroll_widget.h" +#include "ballistica/ui_v1/widget/scroll_widget.h" -#include "ballistica/generic/real_timer.h" -#include "ballistica/graphics/component/empty_component.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/ui/ui.h" +#include "ballistica/base/graphics/component/empty_component.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/support/app_timer.h" +#include "ballistica/base/ui/ui.h" -namespace ballistica { +namespace ballistica::ui_v1 { #define V_MARGIN 5 -ScrollWidget::ScrollWidget() : touch_mode_(!g_platform->IsRunningOnDesktop()) { +ScrollWidget::ScrollWidget() + : touch_mode_(!g_core->platform->IsRunningOnDesktop()) { set_background(false); // Influences default event handling. set_draggable(false); set_claims_left_right(false); @@ -20,13 +21,13 @@ ScrollWidget::ScrollWidget() : touch_mode_(!g_platform->IsRunningOnDesktop()) { ScrollWidget::~ScrollWidget() = default; -void ScrollWidget::HandleRealTimerExpired(RealTimer* t) { +void ScrollWidget::OnTouchDelayTimerExpired() { if (touch_held_) { // Pass a mouse-down event if we haven't moved. if (!touch_is_scrolling_ && !touch_down_sent_) { - ContainerWidget::HandleMessage( - WidgetMessage(WidgetMessage::Type::kMouseDown, nullptr, touch_x_, - touch_y_, static_cast(touch_held_click_count_))); + ContainerWidget::HandleMessage(base::WidgetMessage( + base::WidgetMessage::Type::kMouseDown, nullptr, touch_x_, touch_y_, + static_cast(touch_held_click_count_))); touch_down_sent_ = true; } } @@ -99,14 +100,14 @@ void ScrollWidget::ClampThumb(bool velocity_clamp, bool position_clamp) { } } -auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { +auto ScrollWidget::HandleMessage(const base::WidgetMessage& m) -> bool { BA_DEBUG_UI_READ_LOCK; bool claimed = false; bool pass = true; float right_overlap = 0; float left_overlap = 3; switch (m.type) { - case WidgetMessage::Type::kMoveUp: + case base::WidgetMessage::Type::kMoveUp: if (capture_arrows_) { smoothing_amount_ = 1.0f; // So we can see the transition. child_offset_v_ -= (60); @@ -115,7 +116,7 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { } break; - case WidgetMessage::Type::kMoveDown: + case base::WidgetMessage::Type::kMoveDown: if (capture_arrows_) { smoothing_amount_ = 1.0f; // So we can see the transition. child_offset_v_ += (60); @@ -124,7 +125,7 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { } break; - case WidgetMessage::Type::kShow: { + case base::WidgetMessage::Type::kShow: { claimed = true; pass = false; auto i = widgets().begin(); @@ -176,7 +177,7 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { MarkForUpdate(); break; } - case WidgetMessage::Type::kMouseWheelVelocityH: { + case base::WidgetMessage::Type::kMouseWheelVelocityH: { if (ContainerWidget::HandleMessage(m)) { claimed = true; @@ -191,12 +192,12 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { avg_scroll_speed_v_ = smoothing * avg_scroll_speed_v_ + (1.0f - smoothing) * 0.0f; } - last_sub_widget_h_scroll_claim_time_ = GetRealTime(); + last_sub_widget_h_scroll_claim_time_ = g_core->GetAppTimeMillisecs(); } pass = false; break; } - case WidgetMessage::Type::kMouseWheelVelocity: { + case base::WidgetMessage::Type::kMouseWheelVelocity: { float x = m.fval1; float y = m.fval2; @@ -217,7 +218,8 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { // scrolling (should probably make this less fuzzy). bool ignore_regular_scrolling = false; bool child_claimed_h_scroll_recently = - (GetRealTime() - last_sub_widget_h_scroll_claim_time_ < 100); + (g_core->GetAppTimeMillisecs() - last_sub_widget_h_scroll_claim_time_ + < 100); if (child_claimed_h_scroll_recently && std::abs(avg_scroll_speed_h_) > std::abs(avg_scroll_speed_v_)) ignore_regular_scrolling = true; @@ -265,7 +267,8 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { inertia_scroll_rate_ = smoothing * inertia_scroll_rate_ + (1.0f - smoothing) * new_val; } - last_velocity_event_time_ = g_logic->master_time(); + last_velocity_event_time_millisecs_ = + static_cast(g_base->logic->display_time() * 1000.0); MarkForUpdate(); } else { // Not within our widget; don't allow children to claim. @@ -273,7 +276,7 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { } break; } - case WidgetMessage::Type::kMouseWheel: { + case base::WidgetMessage::Type::kMouseWheel: { float x = m.fval1; float y = m.fval2; if ((x >= 0.0f) && (x < width()) && (y >= 0.0f) && (y < height())) { @@ -287,7 +290,7 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { } break; } - case WidgetMessage::Type::kMouseDown: { + case base::WidgetMessage::Type::kMouseDown: { float x = m.fval1; float y = m.fval2; if ((x >= 0.0f) && (x < width() + right_overlap) && (y >= 0.0f) @@ -322,14 +325,14 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { // Give children a chance to claim this for their own scrolling // before we do so. child_is_scrolling_ = ContainerWidget::HandleMessage( - WidgetMessage(WidgetMessage::Type::kScrollMouseDown, nullptr, - m.fval1, m.fval2, m.fval3)); + base::WidgetMessage(base::WidgetMessage::Type::kScrollMouseDown, + nullptr, m.fval1, m.fval2, m.fval3)); // After a short delay we go ahead and handle this as a regular // click if it hasn't turned into a scroll or a child scroll. if (!child_is_scrolling_) { - touch_delay_timer_ = - Object::New>(150, false, this); + touch_delay_timer_ = base::NewAppTimer( + 150, false, [this] { OnTouchDelayTimerExpired(); }); } } } @@ -374,7 +377,7 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { } break; } - case WidgetMessage::Type::kMouseMove: { + case base::WidgetMessage::Type::kMouseMove: { float x = m.fval1; float y = m.fval2; bool was_claimed = (m.fval3 > 0.0f); @@ -399,14 +402,14 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { // just keep passing them the events as long as they get claimed. if (child_is_scrolling_ && !child_disowned_scroll_) { bool move_claimed = ContainerWidget::HandleMessage( - WidgetMessage(WidgetMessage::Type::kMouseMove, nullptr, - m.fval1, m.fval2, m.fval3)); + base::WidgetMessage(base::WidgetMessage::Type::kMouseMove, + nullptr, m.fval1, m.fval2, m.fval3)); // If they stopped claiming them, send a scroll-mouse-up to tie // things up. if (!move_claimed) { ContainerWidget::HandleMessage( - WidgetMessage(WidgetMessage::Type::kMouseUp, nullptr, - m.fval1, m.fval2, true)); + base::WidgetMessage(base::WidgetMessage::Type::kMouseUp, + nullptr, m.fval1, m.fval2, true)); child_disowned_scroll_ = true; } } else { @@ -425,8 +428,8 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { // eyes the click is canceled. if (touch_down_sent_ && !touch_up_sent_) { ContainerWidget::HandleMessage( - WidgetMessage(WidgetMessage::Type::kMouseUp, nullptr, - m.fval1, m.fval2, true)); + base::WidgetMessage(base::WidgetMessage::Type::kMouseUp, + nullptr, m.fval1, m.fval2, true)); touch_up_sent_ = true; } } @@ -472,7 +475,7 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { } break; } - case WidgetMessage::Type::kMouseUp: { + case base::WidgetMessage::Type::kMouseUp: { mouse_held_scroll_down_ = false; mouse_held_scroll_up_ = false; mouse_held_thumb_ = false; @@ -489,22 +492,22 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { // if a child is still scrolling, send them a scroll-mouse-up if (child_is_scrolling_ && !child_disowned_scroll_) { ContainerWidget::HandleMessage( - WidgetMessage(WidgetMessage::Type::kMouseUp, nullptr, m.fval1, - m.fval2, false)); + base::WidgetMessage(base::WidgetMessage::Type::kMouseUp, + nullptr, m.fval1, m.fval2, false)); } // If we're not claiming it and we haven't sent a mouse_down yet due // to our delay, send that first.. if (!claimed2 && !touch_down_sent_) { - ContainerWidget::HandleMessage(WidgetMessage( - WidgetMessage::Type::kMouseDown, nullptr, m.fval1, m.fval2, - static_cast(touch_held_click_count_))); + ContainerWidget::HandleMessage(base::WidgetMessage( + base::WidgetMessage::Type::kMouseDown, nullptr, m.fval1, + m.fval2, static_cast(touch_held_click_count_))); touch_down_sent_ = true; } if (touch_down_sent_ && !touch_up_sent_) { ContainerWidget::HandleMessage( - WidgetMessage(WidgetMessage::Type::kMouseUp, nullptr, m.fval1, - m.fval2, claimed2)); + base::WidgetMessage(base::WidgetMessage::Type::kMouseUp, + nullptr, m.fval1, m.fval2, claimed2)); touch_up_sent_ = true; } @@ -520,8 +523,9 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { if (!((x >= 0.0f) && (x < width() + right_overlap) && (y >= 0.0f) && (y < height()))) { pass = false; - ContainerWidget::HandleMessage(WidgetMessage( - WidgetMessage::Type::kMouseUp, nullptr, m.fval1, m.fval2, true)); + ContainerWidget::HandleMessage( + base::WidgetMessage(base::WidgetMessage::Type::kMouseUp, nullptr, + m.fval1, m.fval2, true)); } break; @@ -538,7 +542,7 @@ auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool { } // If it was a mouse-down and we claimed it, set ourself as selected - if (m.type == WidgetMessage::Type::kMouseDown && claimed) { + if (m.type == base::WidgetMessage::Type::kMouseDown && claimed) { GlobalSelect(); } @@ -585,7 +589,7 @@ void ScrollWidget::UpdateLayout() { thumb_dirty_ = true; } -void ScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { +void ScrollWidget::Draw(base::RenderPass* pass, bool draw_transparent) { have_drawn_ = true; millisecs_t current_time = pass->frame_def()->base_time(); float prev_child_offset_v_smoothed = child_offset_v_smoothed_; @@ -616,7 +620,7 @@ void ScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { ClampThumb(true, mouse_held_thumb_); child_offset_v_ += inertia_scroll_rate_; if (!has_momentum_ - && (current_time - last_velocity_event_time_ > 1000 / 30)) + && (current_time - last_velocity_event_time_millisecs_ > 1000 / 30)) inertia_scroll_rate_ = 0; // lastly we apply smoothing so that if we're snapping to a specific place @@ -640,7 +644,7 @@ void ScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { CheckLayout(); - Vector3f tilt = 0.02f * g_graphics->tilt(); + Vector3f tilt = 0.02f * g_base->graphics->tilt(); float extra_offs_x = tilt.y; float extra_offs_y = -tilt.x; @@ -650,7 +654,7 @@ void ScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { // begin clipping for children { - EmptyComponent c(pass); + base::EmptyComponent c(pass); c.SetTransparent(draw_transparent); c.ScissorPush(Rect(l + border_width_, b + border_height_ + 1, l + (width() - border_width_ - 0), @@ -667,7 +671,7 @@ void ScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { // end clipping... { - EmptyComponent c(pass); + base::EmptyComponent c(pass); c.SetTransparent(draw_transparent); c.ScissorPop(); c.Submit(); @@ -693,14 +697,15 @@ void ScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { trough_center_y_ = b2 - b_border + trough_height_ * 0.5f; trough_dirty_ = false; } - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(true); c.SetColor(1, 1, 1, border_opacity_); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kUIAtlas)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kUIAtlas)); c.PushTransform(); c.Translate(trough_center_x_, trough_center_y_, 0.7f); c.Scale(trough_width_, trough_height_, 0.1f); - c.DrawModel(g_assets->GetModel(SystemModelID::kScrollBarTroughTransparent)); + c.DrawMeshAsset( + g_base->assets->SysMesh(base::SysMeshID::kScrollBarTroughTransparent)); c.PopTransform(); c.Submit(); } @@ -736,7 +741,7 @@ void ScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { thumb_dirty_ = false; } - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(draw_transparent); float c_scale = 1.0f; if (mouse_held_thumb_) { @@ -748,21 +753,22 @@ void ScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { c.SetColor(color_red_ * c_scale, color_green_ * c_scale, color_blue_ * c_scale, 1.0f); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kUIAtlas)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kUIAtlas)); c.ScissorPush(Rect(l + border_width_, b + border_height_ + 1, l + (width()), b + (height() * 0.995f))); c.PushTransform(); c.Translate(thumb_center_x_, thumb_center_y_, 0.8f); c.Scale(thumb_width_, thumb_height_, 0.1f); if (draw_transparent) { - c.DrawModel(g_assets->GetModel( + c.DrawMeshAsset(g_base->assets->SysMesh( sb_thumb_height > 100 - ? SystemModelID::kScrollBarThumbTransparent - : SystemModelID::kScrollBarThumbShortTransparent)); + ? base::SysMeshID::kScrollBarThumbTransparent + : base::SysMeshID::kScrollBarThumbShortTransparent)); } else { - c.DrawModel(g_assets->GetModel( - sb_thumb_height > 100 ? SystemModelID::kScrollBarThumbOpaque - : SystemModelID::kScrollBarThumbShortOpaque)); + c.DrawMeshAsset(g_base->assets->SysMesh( + sb_thumb_height > 100 + ? base::SysMeshID::kScrollBarThumbOpaque + : base::SysMeshID::kScrollBarThumbShortOpaque)); } c.PopTransform(); c.ScissorPop(); @@ -788,21 +794,21 @@ void ScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { outline_center_y_ = b2 - b_border + 0.5f * outline_height_; shadow_dirty_ = false; } - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(true); c.SetColor(1, 1, 1, border_opacity_); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kScrollWidget)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kScrollWidget)); c.PushTransform(); c.Translate(outline_center_x_, outline_center_y_, 0.9f); c.Scale(outline_width_, outline_height_, 0.1f); - c.DrawModel(g_assets->GetModel(SystemModelID::kSoftEdgeOutside)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kSoftEdgeOutside)); c.PopTransform(); c.Submit(); } // if selected, do glow at depth 0.9f-1.0 if (draw_transparent && IsHierarchySelected() - && g_ui->ShouldHighlightWidgets() && highlight_) { + && g_base->ui->ShouldHighlightWidgets() && highlight_) { float m = 0.8f + std::abs(sinf(static_cast(current_time) * 0.006467f)) * 0.2f * border_opacity_; @@ -823,18 +829,19 @@ void ScrollWidget::Draw(RenderPass* pass, bool draw_transparent) { glow_dirty_ = false; } - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(true); c.SetPremultiplied(true); c.SetColor(0.4f * m, 0.5f * m, 0.05f * m, 0.0f); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kScrollWidgetGlow)); + c.SetTexture( + g_base->assets->SysTexture(base::SysTextureID::kScrollWidgetGlow)); c.PushTransform(); c.Translate(glow_center_x_, glow_center_y_, 0.9f); c.Scale(glow_width_, glow_height_, 0.1f); - c.DrawModel(g_assets->GetModel(SystemModelID::kSoftEdgeOutside)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kSoftEdgeOutside)); c.PopTransform(); c.Submit(); } } -} // namespace ballistica +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui/widget/scroll_widget.h b/src/ballistica/ui_v1/widget/scroll_widget.h similarity index 77% rename from src/ballistica/ui/widget/scroll_widget.h rename to src/ballistica/ui_v1/widget/scroll_widget.h index 1829751e..af21b0cd 100644 --- a/src/ballistica/ui/widget/scroll_widget.h +++ b/src/ballistica/ui_v1/widget/scroll_widget.h @@ -1,26 +1,23 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_UI_WIDGET_SCROLL_WIDGET_H_ -#define BALLISTICA_UI_WIDGET_SCROLL_WIDGET_H_ +#ifndef BALLISTICA_UI_V1_WIDGET_SCROLL_WIDGET_H_ +#define BALLISTICA_UI_V1_WIDGET_SCROLL_WIDGET_H_ #include -#include "ballistica/ui/widget/container_widget.h" +#include "ballistica/ui_v1/widget/container_widget.h" -namespace ballistica { - -template -class RealTimer; +namespace ballistica::ui_v1 { // A scroll-box container widget. class ScrollWidget : public ContainerWidget { public: ScrollWidget(); ~ScrollWidget() override; - void Draw(RenderPass* pass, bool transparent) override; - auto HandleMessage(const WidgetMessage& m) -> bool override; + void Draw(base::RenderPass* pass, bool transparent) override; + auto HandleMessage(const base::WidgetMessage& m) -> bool override; auto GetWidgetTypeName() -> std::string override { return "scroll"; } - void set_capture_arrows(bool val) { capture_arrows_ = val; } + auto set_capture_arrows(bool val) { capture_arrows_ = val; } void SetWidth(float w) override { trough_dirty_ = shadow_dirty_ = glow_dirty_ = thumb_dirty_ = true; set_width(w); @@ -31,19 +28,19 @@ class ScrollWidget : public ContainerWidget { set_height(h); MarkForUpdate(); } - void set_center_small_content(bool val) { + auto set_center_small_content(bool val) { center_small_content_ = val; MarkForUpdate(); } - void HandleRealTimerExpired(RealTimer* t); - void set_color(float r, float g, float b) { + void OnTouchDelayTimerExpired(); + auto set_color(float r, float g, float b) { color_red_ = r; color_green_ = g; color_blue_ = b; } - void set_highlight(bool val) { highlight_ = val; } + auto set_highlight(bool val) { highlight_ = val; } auto highlight() const -> bool { return highlight_; } - void set_border_opacity(float val) { border_opacity_ = val; } + auto set_border_opacity(float val) { border_opacity_ = val; } auto border_opacity() const -> float { return border_opacity_; } protected: @@ -61,7 +58,7 @@ class ScrollWidget : public ContainerWidget { bool glow_dirty_{true}; bool thumb_dirty_{true}; millisecs_t last_sub_widget_h_scroll_claim_time_{}; - millisecs_t last_velocity_event_time_{}; + millisecs_t last_velocity_event_time_millisecs_{}; float avg_scroll_speed_h_{}; float avg_scroll_speed_v_{}; bool center_small_content_{}; @@ -112,9 +109,9 @@ class ScrollWidget : public ContainerWidget { bool child_disowned_scroll_{}; millisecs_t inertia_scroll_update_time_{}; float inertia_scroll_rate_{}; - Object::Ref > touch_delay_timer_; + Object::Ref touch_delay_timer_; }; -} // namespace ballistica +} // namespace ballistica::ui_v1 -#endif // BALLISTICA_UI_WIDGET_SCROLL_WIDGET_H_ +#endif // BALLISTICA_UI_V1_WIDGET_SCROLL_WIDGET_H_ diff --git a/src/ballistica/ui/widget/stack_widget.cc b/src/ballistica/ui_v1/widget/stack_widget.cc similarity index 83% rename from src/ballistica/ui/widget/stack_widget.cc rename to src/ballistica/ui_v1/widget/stack_widget.cc index 76a4536f..8e27cdd0 100644 --- a/src/ballistica/ui/widget/stack_widget.cc +++ b/src/ballistica/ui_v1/widget/stack_widget.cc @@ -1,10 +1,10 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/ui/widget/stack_widget.h" +#include "ballistica/ui_v1/widget/stack_widget.h" -#include "ballistica/ui/ui.h" +#include "ballistica/base/ui/ui.h" -namespace ballistica { +namespace ballistica::ui_v1 { StackWidget::StackWidget() { set_modal_children(true); @@ -32,4 +32,4 @@ void StackWidget::UpdateLayout() { } } -} // namespace ballistica +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui/widget/stack_widget.h b/src/ballistica/ui_v1/widget/stack_widget.h similarity index 75% rename from src/ballistica/ui/widget/stack_widget.h rename to src/ballistica/ui_v1/widget/stack_widget.h index 52472444..7d97dd4b 100644 --- a/src/ballistica/ui/widget/stack_widget.h +++ b/src/ballistica/ui_v1/widget/stack_widget.h @@ -1,13 +1,13 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_UI_WIDGET_STACK_WIDGET_H_ -#define BALLISTICA_UI_WIDGET_STACK_WIDGET_H_ +#ifndef BALLISTICA_UI_V1_WIDGET_STACK_WIDGET_H_ +#define BALLISTICA_UI_V1_WIDGET_STACK_WIDGET_H_ #include -#include "ballistica/ui/widget/container_widget.h" +#include "ballistica/ui_v1/widget/container_widget.h" -namespace ballistica { +namespace ballistica::ui_v1 { // Organizational widget for stacking sub-widgets. class StackWidget : public ContainerWidget { @@ -37,6 +37,6 @@ class StackWidget : public ContainerWidget { bool _sizeDirty = false; }; -} // namespace ballistica +} // namespace ballistica::ui_v1 -#endif // BALLISTICA_UI_WIDGET_STACK_WIDGET_H_ +#endif // BALLISTICA_UI_V1_WIDGET_STACK_WIDGET_H_ diff --git a/src/ballistica/ui/widget/text_widget.cc b/src/ballistica/ui_v1/widget/text_widget.cc similarity index 81% rename from src/ballistica/ui/widget/text_widget.cc rename to src/ballistica/ui_v1/widget/text_widget.cc index ab40826d..95208fbb 100644 --- a/src/ballistica/ui/widget/text_widget.cc +++ b/src/ballistica/ui_v1/widget/text_widget.cc @@ -1,22 +1,24 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/ui/widget/text_widget.h" +#include "ballistica/ui_v1/widget/text_widget.h" -#include "ballistica/app/app_flavor.h" -#include "ballistica/audio/audio.h" -#include "ballistica/generic/utils.h" -#include "ballistica/graphics/component/empty_component.h" -#include "ballistica/graphics/component/simple_component.h" -#include "ballistica/graphics/text/text_graphics.h" -#include "ballistica/input/device/keyboard_input.h" -#include "ballistica/input/input.h" -#include "ballistica/logic/logic.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_context_call.h" -#include "ballistica/ui/ui.h" -#include "ballistica/ui/widget/container_widget.h" +#include "ballistica/base/app/app.h" +#include "ballistica/base/audio/audio.h" +#include "ballistica/base/graphics/component/empty_component.h" +#include "ballistica/base/graphics/component/simple_component.h" +#include "ballistica/base/graphics/text/text_graphics.h" +#include "ballistica/base/input/device/keyboard_input.h" +#include "ballistica/base/input/input.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/core/core.h" +#include "ballistica/shared/generic/utils.h" +#include "ballistica/shared/python/python.h" +#include "ballistica/ui_v1/python/ui_v1_python.h" +#include "ballistica/ui_v1/widget/container_widget.h" -namespace ballistica { +namespace ballistica::ui_v1 { const float kClearMargin{13.0f}; @@ -25,8 +27,8 @@ bool TextWidget::always_use_internal_keyboard_{false}; // FIXME: Move this to g_ui or something; not a global. Object::WeakRef TextWidget::android_string_edit_widget_; TextWidget* TextWidget::GetAndroidStringEditWidget() { - assert(InLogicThread()); - return android_string_edit_widget_.get(); + assert(g_base->InLogicThread()); + return android_string_edit_widget_.Get(); } TextWidget::TextWidget() { @@ -34,22 +36,23 @@ TextWidget::TextWidget() { // have a touchscreen (android-tv type situations). // FIXME - should generalize this to any controller-only situation. if (g_buildconfig.ostype_android()) { - if (g_input->touch_input() == nullptr) { + if (g_base->input->touch_input() == nullptr) { do_clear_button_ = false; } } - birth_time_ = g_logic->master_time(); + birth_time_millisecs_ = + static_cast(g_base->logic->display_time() * 1000.0); } TextWidget::~TextWidget() = default; void TextWidget::set_on_return_press_call(PyObject* call_tuple) { - on_return_press_call_ = Object::New(call_tuple); + on_return_press_call_ = Object::New(call_tuple); } void TextWidget::set_on_activate_call(PyObject* call_tuple) { - on_activate_call_ = Object::New(call_tuple); + on_activate_call_ = Object::New(call_tuple); } void TextWidget::SetWidth(float width_in) { @@ -85,7 +88,7 @@ void TextWidget::SetEnabled(bool val) { } } -void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { +void TextWidget::Draw(base::RenderPass* pass, bool draw_transparent) { millisecs_t current_time = pass->frame_def()->base_time(); // All our stuff currently happens in the transparent pass. @@ -101,7 +104,7 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { // If we're on a button or something, add tilt. { float tilt_scale = draw_control_parent() ? 0.04f : 0.01f; - Vector3f tilt = tilt_scale * g_graphics->tilt(); + Vector3f tilt = tilt_scale * g_base->graphics->tilt(); l -= tilt.y; r -= tilt.y; b += tilt.x; @@ -112,7 +115,7 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { { // We should really be scaling our bounds and things, // but for now lets just do a hacky overall scale. - EmptyComponent c(pass); + base::EmptyComponent c(pass); c.SetTransparent(true); c.PushTransform(); @@ -136,13 +139,13 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { // Draw highlight. if ((IsSelectable() && ((selected() && always_highlight_) || IsHierarchySelected()) - && (always_highlight_ || g_ui->ShouldHighlightWidgets())) + && (always_highlight_ || g_base->ui->ShouldHighlightWidgets())) || ((pressed_ && mouse_over_) - || (current_time - last_activate_time_ < 200))) { + || (current_time - last_activate_time_millisecs_ < 200))) { float m; // Only pulsate if regular widget highlighting is on. - if (g_ui->ShouldHighlightWidgets()) { + if (g_base->ui->ShouldHighlightWidgets()) { if (IsHierarchySelected()) { m = 0.5f + std::abs(sinf(static_cast(current_time) * 0.006467f) @@ -173,15 +176,15 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { highlight_dirty_ = false; } - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(true); c.SetPremultiplied(true); c.SetColor(0.25f * m, 0.3f * m, 0, 0.3f * m); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kGlow)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kGlow)); c.PushTransform(); c.Translate(highlight_center_x_, highlight_center_y_, 0.1f); c.Scale(highlight_width_, highlight_height_); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage4x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage4x1)); c.PopTransform(); c.Submit(); } @@ -199,14 +202,15 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { outline_center_y_ = b - b_border + outline_height_ * 0.5f; outline_dirty_ = false; } - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(true); c.SetColor(1, 1, 1, 1); - c.SetTexture(g_assets->GetTexture(SystemTextureID::kUIAtlas)); + c.SetTexture(g_base->assets->SysTexture(base::SysTextureID::kUIAtlas)); c.PushTransform(); c.Translate(outline_center_x_, outline_center_y_, 0.1f); c.Scale(outline_width_, outline_height_); - c.DrawModel(g_assets->GetModel(SystemModelID::kTextBoxTransparent)); + c.DrawMeshAsset( + g_base->assets->SysMesh(base::SysMeshID::kTextBoxTransparent)); c.PopTransform(); c.Submit(); } @@ -214,29 +218,30 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { // Clear button. if (editable() && (IsHierarchySelected() || always_show_carat_) && !text_raw_.empty() && do_clear_button_) { - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(true); if (clear_pressed_ && clear_mouse_over_) { c.SetColor(0.3f, 0.3f, 0.3f, 1); } else { c.SetColor(0.5f, 0.5f, 0.5f, 1); } - c.SetTexture(g_assets->GetTexture(SystemTextureID::kTextClearButton)); + c.SetTexture( + g_base->assets->SysTexture(base::SysTextureID::kTextClearButton)); c.PushTransform(); c.Translate(r - 20, b * 0.5f + t * 0.5f, 0.1f); - if (g_ui->scale() == UIScale::kSmall) { + if (g_base->ui->scale() == UIScale::kSmall) { c.Scale(30, 30); } else { c.Scale(25, 25); } - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.PopTransform(); c.Submit(); } // Constrain drawing to our bounds. if (editable()) { - EmptyComponent c(pass); + base::EmptyComponent c(pass); c.SetTransparent(true); c.ScissorPush(Rect(l + border_width, b + border_height, r - border_width, t - border_height)); @@ -246,21 +251,21 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { float x_offset, y_offset; - TextMesh::HAlign align_h; - TextMesh::VAlign align_v; + base::TextMesh::HAlign align_h; + base::TextMesh::VAlign align_v; switch (alignment_h_) { case HAlign::kLeft: x_offset = l; - align_h = TextMesh::HAlign::kLeft; + align_h = base::TextMesh::HAlign::kLeft; break; case HAlign::kCenter: x_offset = (l + r) * 0.5f; - align_h = TextMesh::HAlign::kCenter; + align_h = base::TextMesh::HAlign::kCenter; break; case HAlign::kRight: x_offset = r; - align_h = TextMesh::HAlign::kRight; + align_h = base::TextMesh::HAlign::kRight; break; default: throw Exception("Invalid HAlign"); @@ -268,21 +273,23 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { switch (alignment_v_) { case VAlign::kTop: y_offset = t; - align_v = TextMesh::VAlign::kTop; + align_v = base::TextMesh::VAlign::kTop; break; case VAlign::kCenter: y_offset = (b + t) * 0.5f; - align_v = TextMesh::VAlign::kCenter; + align_v = base::TextMesh::VAlign::kCenter; break; case VAlign::kBottom: y_offset = b; - align_v = TextMesh::VAlign::kBottom; + align_v = base::TextMesh::VAlign::kBottom; break; default: throw Exception("Invalid VAlign"); } - float transition = (birth_time_ + transition_delay_) - current_time; + float transition = + (static_cast(birth_time_millisecs_) + transition_delay_) + - static_cast(current_time); if (transition > 0) { x_offset -= transition * 4.0f / (std::max(0.001f, center_scale_)); } @@ -290,15 +297,15 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { // Apply subs/resources to get our actual text if need be. UpdateTranslation(); - if (!text_group_.exists()) { - text_group_ = Object::New(); + if (!text_group_.Exists()) { + text_group_ = Object::New(); } if (text_group_dirty_) { text_group_->SetText(text_translated_, align_h, align_v, big_, res_scale_); - text_width_ = g_text_graphics->GetStringWidth(text_translated_, big_); + text_width_ = g_base->text_graphics->GetStringWidth(text_translated_, big_); // FIXME: doesnt support big. - text_height_ = g_text_graphics->GetStringHeight(text_translated_); + text_height_ = g_base->text_graphics->GetStringHeight(text_translated_); text_group_dirty_ = false; } @@ -313,11 +320,11 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { float fin_a = enabled_ ? color_a_ : 0.4f * color_a_; - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetTransparent(true); if ((pressed_ && mouse_over_) - || (current_time - last_activate_time_ < 200)) { + || (current_time - last_activate_time_millisecs_ < 200)) { color_mult *= 2.0f; } else if (always_highlight_ && selected()) { color_mult *= 1.4f; @@ -329,7 +336,7 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { int elem_count = text_group_->GetElementCount(); for (int e = 0; e < elem_count; e++) { // Gracefully skip unloaded textures.. - TextureData* t2 = text_group_->GetElementTexture(e); + base::TextureAsset* t2 = text_group_->GetElementTexture(e); if (!t2->preloaded()) { continue; } @@ -344,7 +351,7 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { c.SetColor(1, 1, 1, fin_a); } - if (IsVRMode()) { + if (g_core->IsVRMode()) { c.SetFlatness(text_group_->GetElementMaxFlatness(e)); } else { c.SetFlatness( @@ -383,7 +390,7 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { } if (show_cursor && ((current_time / 100) % 2 == 0 - || (current_time - last_carat_change_time_ < 250))) { + || (current_time - last_carat_change_time_millisecs_ < 250))) { int str_size = Utils::UTF8StringLength(text_raw_.c_str()); if (carat_position_ > str_size) { carat_position_ = str_size; @@ -391,7 +398,7 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { float h, v; text_group_->GetCaratPts(text_raw_, align_h, align_v, carat_position_, &h, &v); - SimpleComponent c(pass); + base::SimpleComponent c(pass); c.SetPremultiplied(true); c.SetTransparent(true); c.PushTransform(); @@ -401,15 +408,15 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { c.Scale(max_width_height_scale, max_width_height_scale); c.Translate(h + 4, v + 17.0f); c.Scale(6, 27); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.SetColor(1, 1, 1, 0); c.Scale(0.3f, 0.8f); - c.DrawModel(g_assets->GetModel(SystemModelID::kImage1x1)); + c.DrawMeshAsset(g_base->assets->SysMesh(base::SysMeshID::kImage1x1)); c.PopTransform(); c.Submit(); } } - EmptyComponent c(pass); + base::EmptyComponent c(pass); c.SetTransparent(true); c.ScissorPop(); c.Submit(); @@ -417,7 +424,7 @@ void TextWidget::Draw(RenderPass* pass, bool draw_transparent) { // Pop initial positioning. { - EmptyComponent c(pass); + base::EmptyComponent c(pass); c.SetTransparent(true); c.PopTransform(); c.Submit(); @@ -464,8 +471,8 @@ void TextWidget::SetText(const std::string& text_in_raw) { if (do_format_check) { bool valid; - g_logic->CompileResourceString(text_in_raw, - "TextWidget::SetText format check", &valid); + g_base->assets->CompileResourceString( + text_in_raw, "TextWidget::set_text format check", &valid); if (!valid) { BA_LOG_ONCE(LogLevel::kError, "Invalid resource string: '" + text_in_raw + "'"); @@ -513,7 +520,7 @@ auto TextWidget::GetHeight() -> float { } auto TextWidget::ShouldUseStringEditDialog() const -> bool { - if (HeadlessMode()) { + if (g_core->HeadlessMode()) { return false; } if (force_internal_editing_) { @@ -528,9 +535,9 @@ auto TextWidget::ShouldUseStringEditDialog() const -> bool { // is the mouse or keyboard if (g_buildconfig.ostype_macos() || g_buildconfig.ostype_windows() || g_buildconfig.ostype_linux()) { - InputDevice* ui_input_device = g_ui->GetUIInputDevice(); + base::InputDevice* ui_input_device = g_base->ui->GetUIInputDevice(); return !(ui_input_device == nullptr - || ui_input_device == g_input->keyboard_input()); + || ui_input_device == g_base->input->keyboard_input()); } else { return true; } @@ -540,7 +547,7 @@ void TextWidget::BringUpEditDialog() { bool use_internal_dialog = true; // in vr we always use our own dialog.. - if (IsVRMode()) { + if (g_core->IsVRMode()) { use_internal_dialog = true; } else { // on android, use the android keyboard unless the user want to use ours.. @@ -552,23 +559,23 @@ void TextWidget::BringUpEditDialog() { use_internal_dialog = false; // store ourself as the current text-widget and kick off an edit android_string_edit_widget_ = this; - g_app_flavor->PushStringEditCall(description_, text_raw_, max_chars_); + g_base->app->PushStringEditCall(description_, text_raw_, max_chars_); } } } if (explicit_bool(use_internal_dialog)) { - g_python->LaunchStringEdit(this); + g_ui_v1->python->LaunchStringEdit(this); } } void TextWidget::Activate() { - last_activate_time_ = g_logic->master_time(); + last_activate_time_millisecs_ = + static_cast(g_base->logic->display_time() * 1000.0); - if (on_activate_call_.exists()) { + if (auto* call = on_activate_call_.Get()) { // Call this in the next cycle (don't wanna risk mucking with UI from within // a UI loop). - g_logic->PushPythonWeakCall( - Object::WeakRef(on_activate_call_)); + call->ScheduleWeak(); } // If we're on ouya and this is editable, it brings up our string-editor. @@ -577,14 +584,14 @@ void TextWidget::Activate() { } } -auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool { - if (HeadlessMode()) { +auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool { + if (g_core->HeadlessMode()) { return false; } // How far outside our bounds touches register. float left_overlap, top_overlap, right_overlap, bottom_overlap; - if (g_platform->IsRunningOnDesktop()) { + if (g_core->platform->IsRunningOnDesktop()) { left_overlap = 0.0f; top_overlap = 0.0f; right_overlap = 0.0f; @@ -598,17 +605,18 @@ auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool { // 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()) { + && m.type == base::WidgetMessage::Type::kPaste) { + if (g_core->platform->ClipboardIsSupported()) { + if (g_core->platform->ClipboardHasText()) { // Just enter it char by char as if we had typed it... - AddCharsToText(g_platform->ClipboardGetText()); + AddCharsToText(g_core->platform->ClipboardGetText()); } } } // If we're doing inline editing, handle some key events. if (m.has_keysym && !ShouldUseStringEditDialog()) { - last_carat_change_time_ = g_logic->master_time(); + last_carat_change_time_millisecs_ = + static_cast(g_base->logic->display_time() * 1000.0); text_group_dirty_ = true; bool claimed = false; @@ -622,18 +630,16 @@ auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool { case SDLK_KP_ENTER: if (g_buildconfig.ostype_ios_tvos() || g_buildconfig.ostype_android()) { // On mobile, return currently just deselects us. - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kSwish)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kSwish)); parent_widget()->SelectWidget(nullptr); return true; } else { - if (on_return_press_call_.exists()) { + if (auto* call = on_return_press_call_.Get()) { claimed = true; - if (on_return_press_call_.exists()) { - // Call this in the next cycle (don't wanna risk mucking with UI - // from within a UI loop) - g_logic->PushPythonWeakCall( - Object::WeakRef(on_return_press_call_)); - } + // Call this in the next cycle (don't wanna risk mucking with UI + // from within a UI loop) + call->ScheduleWeak(); } } break; @@ -771,7 +777,7 @@ auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool { return claimed; } switch (m.type) { - case WidgetMessage::Type::kTextInput: { + case base::WidgetMessage::Type::kTextInput: { // If we're using an edit dialog, any attempted text input just kicks us // over to that. if (editable() && ShouldUseStringEditDialog()) { @@ -785,7 +791,7 @@ auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool { } break; } - case WidgetMessage::Type::kMouseMove: { + case base::WidgetMessage::Type::kMouseMove: { if (!IsSelectable()) { return false; } @@ -804,7 +810,7 @@ auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool { } return mouse_over_; } - case WidgetMessage::Type::kMouseDown: { + case base::WidgetMessage::Type::kMouseDown: { if (!IsSelectable()) { return false; } @@ -831,8 +837,8 @@ auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool { // FIXME: may need to test/tweak this behavior for cases where // we pop up a UI dialog for text input.. if (editable()) { - if (InputDevice* kb = g_input->keyboard_input()) { - g_ui->SetUIInputDevice(kb); + if (auto* kb = g_base->input->keyboard_input()) { + g_base->ui->SetUIInputDevice(kb); } } GlobalSelect(); @@ -843,7 +849,8 @@ auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool { pressed_activate_ = (click_count == 2 || click_activate_) && !editable_; if (click_count == 1) { - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kTap)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kTap)); } } return true; @@ -851,7 +858,7 @@ auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool { return false; } } - case WidgetMessage::Type::kMouseUp: { + case base::WidgetMessage::Type::kMouseUp: { float x{ScaleAdjustedX(m.fval1)}; float y{ScaleAdjustedY(m.fval2)}; bool claimed = (m.fval3 > 0.0f); @@ -866,7 +873,8 @@ auto TextWidget::HandleMessage(const WidgetMessage& m) -> bool { carat_position_ = 0; text_group_dirty_ = true; clear_pressed_ = false; - g_audio->PlaySound(g_assets->GetSound(SystemSoundID::kTap)); + g_base->audio->PlaySound( + g_base->assets->SysSound(base::SysSoundID::kTap)); return true; } clear_pressed_ = false; @@ -911,7 +919,7 @@ auto TextWidget::ScaleAdjustedY(float y) -> float { return height_ * 0.5f + offsy / center_scale_; } -auto TextWidget::AddCharsToText(const std::string& addchars) -> void { +void TextWidget::AddCharsToText(const std::string& addchars) { assert(editable()); std::vector unichars = Utils::UnicodeFromUTF8(text_raw_, "jcjwf8f"); int len = static_cast(unichars.size()); @@ -938,7 +946,7 @@ void TextWidget::UpdateTranslation() { if (editable()) { text_translated_ = text_raw_; } else { - text_translated_ = g_logic->CompileResourceString( + text_translated_ = g_base->assets->CompileResourceString( text_raw_, "TextWidget::UpdateTranslation"); } text_translation_dirty_ = false; @@ -950,9 +958,9 @@ auto TextWidget::GetTextWidth() -> float { UpdateTranslation(); // Should we cache this? - return g_text_graphics->GetStringWidth(text_translated_, big_); + return g_base->text_graphics->GetStringWidth(text_translated_, big_); } void TextWidget::OnLanguageChange() { text_translation_dirty_ = true; } -} // namespace ballistica +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui/widget/text_widget.h b/src/ballistica/ui_v1/widget/text_widget.h similarity index 86% rename from src/ballistica/ui/widget/text_widget.h rename to src/ballistica/ui_v1/widget/text_widget.h index 8359d89b..0cde54f0 100644 --- a/src/ballistica/ui/widget/text_widget.h +++ b/src/ballistica/ui_v1/widget/text_widget.h @@ -1,27 +1,27 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_UI_WIDGET_TEXT_WIDGET_H_ -#define BALLISTICA_UI_WIDGET_TEXT_WIDGET_H_ +#ifndef BALLISTICA_UI_V1_WIDGET_TEXT_WIDGET_H_ +#define BALLISTICA_UI_V1_WIDGET_TEXT_WIDGET_H_ #include -#include "ballistica/ui/widget/widget.h" +#include "ballistica/ui_v1/widget/widget.h" -namespace ballistica { +namespace ballistica::ui_v1 { // widget for drawing static text as well as text input class TextWidget : public Widget { public: TextWidget(); ~TextWidget() override; - void Draw(RenderPass* pass, bool transparent) override; + void Draw(base::RenderPass* pass, bool transparent) override; void SetWidth(float widthIn); void SetHeight(float heightIn); auto GetWidth() -> float override; auto GetHeight() -> float override; enum class HAlign { kLeft, kCenter, kRight }; enum class VAlign { kTop, kCenter, kBottom }; - auto HandleMessage(const WidgetMessage& m) -> bool override; + auto HandleMessage(const base::WidgetMessage& m) -> bool override; auto IsSelectable() -> bool override { return (enabled_ && (editable_ || selectable_)); } @@ -91,7 +91,7 @@ class TextWidget : public Widget { private: auto ScaleAdjustedX(float x) -> float; auto ScaleAdjustedY(float y) -> float; - auto AddCharsToText(const std::string& addchars) -> void; + void AddCharsToText(const std::string& addchars); auto ShouldUseStringEditDialog() const -> bool; void BringUpEditDialog(); void UpdateTranslation(); @@ -99,12 +99,12 @@ class TextWidget : public Widget { static Object::WeakRef android_string_edit_widget_; float res_scale_{1.0f}; bool enabled_{true}; - millisecs_t birth_time_{}; + millisecs_t birth_time_millisecs_{}; float transition_delay_{}; float max_width_{-1.0f}; float max_height_{-1.0f}; float extra_touch_border_scale_{1.0f}; - Object::Ref text_group_; + Object::Ref text_group_; bool big_{}; bool force_internal_editing_{}; bool always_show_carat_{}; @@ -150,14 +150,14 @@ class TextWidget : public Widget { VAlign alignment_v_{VAlign::kTop}; float flatness_{}; float shadow_{0.5f}; - millisecs_t last_activate_time_{}; - millisecs_t last_carat_change_time_{}; + millisecs_t last_activate_time_millisecs_{}; + millisecs_t last_carat_change_time_millisecs_{}; // we keep these at the bottom so they're torn down first.. - Object::Ref on_return_press_call_; - Object::Ref on_activate_call_; + Object::Ref on_return_press_call_; + Object::Ref on_activate_call_; }; -} // namespace ballistica +} // namespace ballistica::ui_v1 -#endif // BALLISTICA_UI_WIDGET_TEXT_WIDGET_H_ +#endif // BALLISTICA_UI_V1_WIDGET_TEXT_WIDGET_H_ diff --git a/src/ballistica/ui/widget/widget.cc b/src/ballistica/ui_v1/widget/widget.cc similarity index 83% rename from src/ballistica/ui/widget/widget.cc rename to src/ballistica/ui_v1/widget/widget.cc index 094c35a9..1d646dd6 100644 --- a/src/ballistica/ui/widget/widget.cc +++ b/src/ballistica/ui_v1/widget/widget.cc @@ -1,15 +1,15 @@ // Released under the MIT License. See LICENSE for details. -#include "ballistica/ui/widget/widget.h" +#include "ballistica/ui_v1/widget/widget.h" -#include "ballistica/logic/logic.h" -#include "ballistica/python/class/python_class_widget.h" -#include "ballistica/python/python_context_call.h" -#include "ballistica/ui/ui.h" -#include "ballistica/ui/widget/container_widget.h" -#include "ballistica/ui/widget/root_widget.h" +#include "ballistica/base/logic/logic.h" +#include "ballistica/base/python/support/python_context_call.h" +#include "ballistica/base/ui/ui.h" +#include "ballistica/ui_v1/python/class/python_class_widget.h" +#include "ballistica/ui_v1/widget/container_widget.h" +#include "ballistica/ui_v1/widget/root_widget.h" -namespace ballistica { +namespace ballistica::ui_v1 { Widget::Widget() = default; @@ -33,7 +33,7 @@ void Widget::SetToolbarVisibility(ToolbarVisibility v) { // Most widgets can never influence the global toolbar so we can // do a quick out. if (parent_widget_ != nullptr && parent_widget_->is_window_stack()) { - g_ui->root_widget()->UpdateForFocusedWindow(); + g_base->ui->root_widget()->UpdateForFocusedWindow(); } } @@ -45,14 +45,14 @@ void Widget::SetDepthRange(float min_depth, float max_depth) { } auto Widget::IsInMainStack() const -> bool { - if (!g_ui) { + if (!g_base->ui) { BA_LOG_ONCE(LogLevel::kError, "Widget::IsInMainStack() called before ui creation."); return false; } // Navigate up to the top of the hierarchy and see if the // screen-root widget is in there somewhere. - ContainerWidget* screen_root = g_ui->screen_root_widget(); + ContainerWidget* screen_root = g_base->ui->screen_root_widget(); assert(screen_root); if (!screen_root) { return false; @@ -70,7 +70,7 @@ auto Widget::IsInMainStack() const -> bool { auto Widget::IsInOverlayStack() const -> bool { // Navigate up to the top of the hierarchy and see if the overlay-root widget // is in there somewhere. - ContainerWidget* overlay_root = g_ui->overlay_root_widget(); + ContainerWidget* overlay_root = g_base->ui->overlay_root_widget(); assert(overlay_root); ContainerWidget* parent = parent_widget_; while (parent != nullptr) { @@ -85,11 +85,10 @@ auto Widget::IsInOverlayStack() const -> bool { void Widget::SetSelected(bool s, SelectionCause cause) { if (selected_ == s) return; selected_ = s; - if (selected_ && on_select_call_.exists()) { + if (selected_ && on_select_call_.Exists()) { // Call this in the next cycle (don't wanna risk mucking // with UI from within a UI loop). - g_logic->PushPythonWeakCall( - Object::WeakRef(on_select_call_)); + on_select_call_->ScheduleWeak(); } } @@ -100,7 +99,7 @@ auto Widget::IsHierarchySelected() const -> bool { return false; } p = p->GetOwnerWidget(); - if (!p || p == g_ui->root_widget()) { + if (!p || p == g_base->ui->root_widget()) { break; } } @@ -108,11 +107,11 @@ auto Widget::IsHierarchySelected() const -> bool { } void Widget::SetOnSelectCall(PyObject* call_obj) { - on_select_call_ = Object::New(call_obj); + on_select_call_ = Object::New(call_obj); } void Widget::AddOnDeleteCall(PyObject* call_obj) { - on_delete_calls_.push_back(Object::New(call_obj)); + on_delete_calls_.push_back(Object::New(call_obj)); } void Widget::GlobalSelect() { @@ -210,7 +209,7 @@ void Widget::ScreenPointToWidget(float* x, float* y) const { } auto Widget::GetPyWidget(bool new_ref) -> PyObject* { - assert(InLogicThread()); + assert(g_base->InLogicThread()); if (py_ref_ == nullptr) { py_ref_ = PythonClassWidget::Create(this); } @@ -225,9 +224,11 @@ void Widget::GetCenter(float* x, float* y) { *y = ty() + scale() * GetHeight() * 0.5f; } -auto Widget::HandleMessage(const WidgetMessage& m) -> bool { return false; } +auto Widget::HandleMessage(const base::WidgetMessage& m) -> bool { + return false; +} -void Widget::Draw(RenderPass* pass, bool transparent) {} +void Widget::Draw(base::RenderPass* pass, bool transparent) {} auto Widget::IsSelectable() -> bool { return false; } @@ -237,4 +238,4 @@ auto Widget::IsAcceptingInput() const -> bool { return true; } void Widget::Activate() {} -} // namespace ballistica +} // namespace ballistica::ui_v1 diff --git a/src/ballistica/ui/widget/widget.h b/src/ballistica/ui_v1/widget/widget.h similarity index 82% rename from src/ballistica/ui/widget/widget.h rename to src/ballistica/ui_v1/widget/widget.h index c6e7fdda..56e7e18b 100644 --- a/src/ballistica/ui/widget/widget.h +++ b/src/ballistica/ui_v1/widget/widget.h @@ -1,73 +1,17 @@ // Released under the MIT License. See LICENSE for details. -#ifndef BALLISTICA_UI_WIDGET_WIDGET_H_ -#define BALLISTICA_UI_WIDGET_WIDGET_H_ +#ifndef BALLISTICA_UI_V1_WIDGET_WIDGET_H_ +#define BALLISTICA_UI_V1_WIDGET_WIDGET_H_ #include #include -#include "ballistica/ballistica.h" -#include "ballistica/core/object.h" -#include "ballistica/platform/min_sdl.h" +#include "ballistica/base/base.h" +#include "ballistica/base/ui/widget_message.h" +#include "ballistica/shared/foundation/object.h" +#include "ballistica/ui_v1/ui_v1.h" -namespace ballistica { - -// Messages descriptions sent to widgets. -struct WidgetMessage { - enum class Type { - kEmptyMessage, - kMoveUp, - kMoveDown, - kMoveLeft, - kMoveRight, - kActivate, - kStart, - kCancel, - kShow, - // In order to work in all-joystick environments, - // don't rely on the following to be available (they're just a luxury). - kKey, - kTabNext, - kTabPrev, - kMouseDown, - kMouseUp, - kMouseWheel, - kMouseWheelH, - kMouseWheelVelocity, - kMouseWheelVelocityH, - kMouseMove, - kScrollMouseDown, - kTextInput, - kPaste - }; - - Type type{}; - bool has_keysym{}; - SDL_Keysym keysym{}; - float fval1{}; - float fval2{}; - float fval3{}; - float fval4{}; - std::string* sval{}; - - explicit WidgetMessage(Type t = Type::kEmptyMessage, - const SDL_Keysym* k = nullptr, float f1 = 0, - float f2 = 0, float f3 = 0, float f4 = 0, - const char* s = nullptr) - : type(t), has_keysym(false), fval1(f1), fval2(f2), fval3(f3), fval4(f4) { - if (k) { - keysym = *k; - has_keysym = true; - } - if (s) { - sval = new std::string(); - *sval = s; - } else { - sval = nullptr; - } - } - ~WidgetMessage() { delete sval; } -}; +namespace ballistica::ui_v1 { // Base class for interface widgets. class Widget : public Object { @@ -94,10 +38,10 @@ class Widget : public Object { // Widgets are drawn in 2 passes. The first is a front-to-back pass where // opaque parts should be drawn and the second is back-to-front where // transparent stuff should be drawn. - virtual void Draw(RenderPass* pass, bool transparent); + virtual void Draw(base::RenderPass* pass, bool transparent); // Send a message to the widget; returns whether it was handled. - virtual auto HandleMessage(const WidgetMessage& m) -> bool; + virtual auto HandleMessage(const base::WidgetMessage& m) -> bool; // Whether the widget (or its children) is selectable in any way. virtual auto IsSelectable() -> bool; @@ -143,22 +87,22 @@ class Widget : public Object { // there is no parent. auto GetOwnerWidget() const -> Widget*; - auto down_widget() const -> Widget* { return down_widget_.get(); } + auto down_widget() const -> Widget* { return down_widget_.Get(); } void set_down_widget(Widget* w) { BA_PRECONDITION(!neighbors_locked_); down_widget_ = w; } - auto up_widget() const -> Widget* { return up_widget_.get(); } + auto up_widget() const -> Widget* { return up_widget_.Get(); } void set_up_widget(Widget* w) { BA_PRECONDITION(!neighbors_locked_); up_widget_ = w; } - auto left_widget() const -> Widget* { return left_widget_.get(); } + auto left_widget() const -> Widget* { return left_widget_.Get(); } void set_left_widget(Widget* w) { BA_PRECONDITION(!neighbors_locked_); left_widget_ = w; } - auto right_widget() const -> Widget* { return right_widget_.get(); } + auto right_widget() const -> Widget* { return right_widget_.Get(); } void set_right_widget(Widget* w) { BA_PRECONDITION(!neighbors_locked_); right_widget_ = w; @@ -229,7 +173,7 @@ class Widget : public Object { // Ideally we'd probably want to extend the parent mechanism for this, but // this works for now. auto draw_control_parent() const -> Widget* { - return draw_control_parent_.get(); + return draw_control_parent_.Get(); } void set_draw_control_parent(Widget* w) { draw_control_parent_ = w; } @@ -310,12 +254,12 @@ class Widget : public Object { float scale_{1.0f}; float depth_range_min_{}; float depth_range_max_{1.0f}; - Object::Ref on_select_call_; - std::vector > on_delete_calls_; + Object::Ref on_select_call_; + std::vector > on_delete_calls_; // FIXME: Should move container widget's functionality into ourself. friend class ContainerWidget; }; -} // namespace ballistica +} // namespace ballistica::ui_v1 -#endif // BALLISTICA_UI_WIDGET_WIDGET_H_ +#endif // BALLISTICA_UI_V1_WIDGET_WIDGET_H_ diff --git a/src/external/README.md b/src/external/README.md new file mode 100644 index 00000000..322b24bd --- /dev/null +++ b/src/external/README.md @@ -0,0 +1,4 @@ +# External Source + +This directory contains sources/libraries that originate outside of the +Ballistica project. diff --git a/src/external/open_dynamics_engine-ef/ode/ode_collision_space.cpp b/src/external/open_dynamics_engine-ef/ode/ode_collision_space.cpp index e37f29e4..b82f664b 100644 --- a/src/external/open_dynamics_engine-ef/ode/ode_collision_space.cpp +++ b/src/external/open_dynamics_engine-ef/ode/ode_collision_space.cpp @@ -33,7 +33,7 @@ spaces #include "ode/ode_collision_kernel.h" #include #include "ode/ode_util.h" -#include "ballistica/ballistica.h" +#include "ballistica/shared/ballistica.h" #include "ode/ode_collision_space_internal.h" diff --git a/src/external/windows/include/python/Python.h b/src/external/windows/include/python/Python.h index 3568bbac..fa1c4186 100755 --- a/src/external/windows/include/python/Python.h +++ b/src/external/windows/include/python/Python.h @@ -1,88 +1,56 @@ +// Entry point of the Python C API. +// C extensions should only #include , and not include directly +// the other Python header files included by . + #ifndef Py_PYTHON_H #define Py_PYTHON_H -/* Since this is a "meta-include" file, no #ifdef __cplusplus / extern "C" { */ -/* Include nearly all Python header files */ +// Since this is a "meta-include" file, no #ifdef __cplusplus / extern "C" { +// Include Python header files #include "patchlevel.h" #include "pyconfig.h" #include "pymacconfig.h" -#include - -#ifndef UCHAR_MAX -#error "Something's broken. UCHAR_MAX should be defined in limits.h." -#endif - -#if UCHAR_MAX != 255 -#error "Python's source code assumes C's unsigned char is an 8-bit type." -#endif - #if defined(__sgi) && !defined(_SGI_MP_SOURCE) -#define _SGI_MP_SOURCE +# define _SGI_MP_SOURCE #endif -#include -#ifndef NULL -# error "Python.h requires that stdio.h define NULL." +// stdlib.h, stdio.h, errno.h and string.h headers are not used by Python +// headers, but kept for backward compatibility. They are excluded from the +// limited C API of Python 3.11. +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# include +# include // FILE* +# include // errno +# include // memcpy() #endif - -#include -#ifdef HAVE_ERRNO_H -#include -#endif -#include #ifndef MS_WINDOWS -#include +# include #endif - -/* For size_t? */ #ifdef HAVE_STDDEF_H -#include +# include // size_t #endif -/* CAUTION: Build setups should ensure that NDEBUG is defined on the - * compiler command line when building Python in release mode; else - * assert() calls won't be removed. - */ -#include +#include // assert() +#include // wchar_t #include "pyport.h" #include "pymacro.h" - -/* A convenient way for code to know if sanitizers are enabled. */ -#if defined(__has_feature) -# if __has_feature(memory_sanitizer) -# if !defined(_Py_MEMORY_SANITIZER) -# define _Py_MEMORY_SANITIZER -# endif -# endif -# if __has_feature(address_sanitizer) -# if !defined(_Py_ADDRESS_SANITIZER) -# define _Py_ADDRESS_SANITIZER -# endif -# endif -#elif defined(__GNUC__) -# if defined(__SANITIZE_ADDRESS__) -# define _Py_ADDRESS_SANITIZER -# endif -#endif - #include "pymath.h" #include "pymem.h" - +#include "pytypedefs.h" +#include "pybuffer.h" #include "object.h" #include "objimpl.h" #include "typeslots.h" #include "pyhash.h" - #include "cpython/pydebug.h" - #include "bytearrayobject.h" #include "bytesobject.h" #include "unicodeobject.h" #include "longobject.h" -#include "longintrepr.h" +#include "cpython/longintrepr.h" #include "boolobject.h" #include "floatobject.h" #include "complexobject.h" @@ -96,33 +64,30 @@ #include "setobject.h" #include "methodobject.h" #include "moduleobject.h" -#include "funcobject.h" -#include "classobject.h" +#include "cpython/funcobject.h" +#include "cpython/classobject.h" #include "fileobject.h" #include "pycapsule.h" -#include "code.h" +#include "cpython/code.h" #include "pyframe.h" #include "traceback.h" #include "sliceobject.h" -#include "cellobject.h" +#include "cpython/cellobject.h" #include "iterobject.h" #include "cpython/initconfig.h" -#include "genobject.h" +#include "pystate.h" +#include "cpython/genobject.h" #include "descrobject.h" #include "genericaliasobject.h" #include "warnings.h" #include "weakrefobject.h" #include "structseq.h" -#include "namespaceobject.h" #include "cpython/picklebufobject.h" #include "cpython/pytime.h" - #include "codecs.h" #include "pyerrors.h" #include "pythread.h" -#include "pystate.h" -#include "context.h" - +#include "cpython/context.h" #include "modsupport.h" #include "compile.h" #include "pythonrun.h" @@ -132,12 +97,8 @@ #include "osmodule.h" #include "intrcheck.h" #include "import.h" - #include "abstract.h" #include "bltinmodule.h" - -#include "eval.h" - #include "cpython/pyctype.h" #include "pystrtod.h" #include "pystrcmp.h" diff --git a/src/external/windows/include/python/abstract.h b/src/external/windows/include/python/abstract.h index 8da8d983..1c6ef365 100755 --- a/src/external/windows/include/python/abstract.h +++ b/src/external/windows/include/python/abstract.h @@ -863,7 +863,7 @@ PyAPI_FUNC(int) PyObject_IsSubclass(PyObject *object, PyObject *typeorclass); #ifndef Py_LIMITED_API # define Py_CPYTHON_ABSTRACTOBJECT_H -# include "cpython/abstract.h" +# include "cpython/abstract.h" # undef Py_CPYTHON_ABSTRACTOBJECT_H #endif diff --git a/src/external/windows/include/python/boolobject.h b/src/external/windows/include/python/boolobject.h index b4860659..e42d60ff 100755 --- a/src/external/windows/include/python/boolobject.h +++ b/src/external/windows/include/python/boolobject.h @@ -15,8 +15,8 @@ PyAPI_DATA(PyTypeObject) PyBool_Type; Don't forget to apply Py_INCREF() when returning either!!! */ /* Don't use these directly */ -PyAPI_DATA(struct _longobject) _Py_FalseStruct; -PyAPI_DATA(struct _longobject) _Py_TrueStruct; +PyAPI_DATA(PyLongObject) _Py_FalseStruct; +PyAPI_DATA(PyLongObject) _Py_TrueStruct; /* Use these macros */ #define Py_False ((PyObject *) &_Py_FalseStruct) diff --git a/src/external/windows/include/python/bytearrayobject.h b/src/external/windows/include/python/bytearrayobject.h index 1ef0cf3b..bc69fe21 100755 --- a/src/external/windows/include/python/bytearrayobject.h +++ b/src/external/windows/include/python/bytearrayobject.h @@ -6,8 +6,6 @@ extern "C" { #endif -#include - /* Type PyByteArrayObject represents a mutable array of bytes. * The Python API is that of a sequence; * the bytes are mapped to ints in [0, 256). @@ -36,7 +34,7 @@ PyAPI_FUNC(int) PyByteArray_Resize(PyObject *, Py_ssize_t); #ifndef Py_LIMITED_API # define Py_CPYTHON_BYTEARRAYOBJECT_H -# include "cpython/bytearrayobject.h" +# include "cpython/bytearrayobject.h" # undef Py_CPYTHON_BYTEARRAYOBJECT_H #endif diff --git a/src/external/windows/include/python/bytesobject.h b/src/external/windows/include/python/bytesobject.h index 373ee9a5..b2128ddb 100755 --- a/src/external/windows/include/python/bytesobject.h +++ b/src/external/windows/include/python/bytesobject.h @@ -7,7 +7,7 @@ extern "C" { #endif -#include +#include // va_list /* Type PyBytesObject represents a byte string. An extra zero byte is @@ -59,7 +59,7 @@ PyAPI_FUNC(int) PyBytes_AsStringAndSize( #ifndef Py_LIMITED_API # define Py_CPYTHON_BYTESOBJECT_H -# include "cpython/bytesobject.h" +# include "cpython/bytesobject.h" # undef Py_CPYTHON_BYTESOBJECT_H #endif diff --git a/src/external/windows/include/python/ceval.h b/src/external/windows/include/python/ceval.h index b2a14cee..da1e4425 100755 --- a/src/external/windows/include/python/ceval.h +++ b/src/external/windows/include/python/ceval.h @@ -1,3 +1,5 @@ +/* Interface to random parts in ceval.c */ + #ifndef Py_CEVAL_H #define Py_CEVAL_H #ifdef __cplusplus @@ -5,7 +7,15 @@ extern "C" { #endif -/* Interface to random parts in ceval.c */ +PyAPI_FUNC(PyObject *) PyEval_EvalCode(PyObject *, PyObject *, PyObject *); + +PyAPI_FUNC(PyObject *) PyEval_EvalCodeEx(PyObject *co, + PyObject *globals, + PyObject *locals, + PyObject *const *args, int argc, + PyObject *const *kwds, int kwdc, + PyObject *const *defs, int defc, + PyObject *kwdefs, PyObject *closure); /* PyEval_CallObjectWithKeywords(), PyEval_CallObject(), PyEval_CallFunction * and PyEval_CallMethod are deprecated. Since they are officially part of the @@ -148,7 +158,7 @@ PyAPI_FUNC(void) PyEval_ReleaseThread(PyThreadState *tstate); #ifndef Py_LIMITED_API # define Py_CPYTHON_CEVAL_H -# include "cpython/ceval.h" +# include "cpython/ceval.h" # undef Py_CPYTHON_CEVAL_H #endif diff --git a/src/external/windows/include/python/code.h b/src/external/windows/include/python/code.h deleted file mode 100755 index 1f948e52..00000000 --- a/src/external/windows/include/python/code.h +++ /dev/null @@ -1,20 +0,0 @@ -/* Definitions for bytecode */ - -#ifndef Py_CODE_H -#define Py_CODE_H -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct PyCodeObject PyCodeObject; - -#ifndef Py_LIMITED_API -# define Py_CPYTHON_CODE_H -# include "cpython/code.h" -# undef Py_CPYTHON_CODE_H -#endif - -#ifdef __cplusplus -} -#endif -#endif /* !Py_CODE_H */ diff --git a/src/external/windows/include/python/complexobject.h b/src/external/windows/include/python/complexobject.h index a912a55f..8525f855 100755 --- a/src/external/windows/include/python/complexobject.h +++ b/src/external/windows/include/python/complexobject.h @@ -6,61 +6,22 @@ extern "C" { #endif -#ifndef Py_LIMITED_API -typedef struct { - double real; - double imag; -} Py_complex; - -/* Operations on complex numbers from complexmodule.c */ - -PyAPI_FUNC(Py_complex) _Py_c_sum(Py_complex, Py_complex); -PyAPI_FUNC(Py_complex) _Py_c_diff(Py_complex, Py_complex); -PyAPI_FUNC(Py_complex) _Py_c_neg(Py_complex); -PyAPI_FUNC(Py_complex) _Py_c_prod(Py_complex, Py_complex); -PyAPI_FUNC(Py_complex) _Py_c_quot(Py_complex, Py_complex); -PyAPI_FUNC(Py_complex) _Py_c_pow(Py_complex, Py_complex); -PyAPI_FUNC(double) _Py_c_abs(Py_complex); -#endif - /* Complex object interface */ -/* -PyComplexObject represents a complex number with double-precision -real and imaginary parts. -*/ -#ifndef Py_LIMITED_API -typedef struct { - PyObject_HEAD - Py_complex cval; -} PyComplexObject; -#endif - PyAPI_DATA(PyTypeObject) PyComplex_Type; #define PyComplex_Check(op) PyObject_TypeCheck(op, &PyComplex_Type) #define PyComplex_CheckExact(op) Py_IS_TYPE(op, &PyComplex_Type) -#ifndef Py_LIMITED_API -PyAPI_FUNC(PyObject *) PyComplex_FromCComplex(Py_complex); -#endif PyAPI_FUNC(PyObject *) PyComplex_FromDoubles(double real, double imag); PyAPI_FUNC(double) PyComplex_RealAsDouble(PyObject *op); PyAPI_FUNC(double) PyComplex_ImagAsDouble(PyObject *op); -#ifndef Py_LIMITED_API -PyAPI_FUNC(Py_complex) PyComplex_AsCComplex(PyObject *op); -#endif -/* Format the object based on the format_spec, as defined in PEP 3101 - (Advanced String Formatting). */ #ifndef Py_LIMITED_API -PyAPI_FUNC(int) _PyComplex_FormatAdvancedWriter( - _PyUnicodeWriter *writer, - PyObject *obj, - PyObject *format_spec, - Py_ssize_t start, - Py_ssize_t end); +# define Py_CPYTHON_COMPLEXOBJECT_H +# include "cpython/complexobject.h" +# undef Py_CPYTHON_COMPLEXOBJECT_H #endif #ifdef __cplusplus diff --git a/src/external/windows/include/python/cpython/abstract.h b/src/external/windows/include/python/cpython/abstract.h index 01afb81a..adbe2683 100755 --- a/src/external/windows/include/python/cpython/abstract.h +++ b/src/external/windows/include/python/cpython/abstract.h @@ -50,7 +50,8 @@ PyAPI_FUNC(PyObject *) _PyObject_MakeTpCall( PyObject *const *args, Py_ssize_t nargs, PyObject *keywords); -#define PY_VECTORCALL_ARGUMENTS_OFFSET ((size_t)1 << (8 * sizeof(size_t) - 1)) +#define PY_VECTORCALL_ARGUMENTS_OFFSET \ + (_Py_STATIC_CAST(size_t, 1) << (8 * sizeof(size_t) - 1)) static inline Py_ssize_t PyVectorcall_NARGS(size_t n) @@ -58,71 +59,13 @@ PyVectorcall_NARGS(size_t n) return n & ~PY_VECTORCALL_ARGUMENTS_OFFSET; } -static inline vectorcallfunc -PyVectorcall_Function(PyObject *callable) -{ - PyTypeObject *tp; - Py_ssize_t offset; - vectorcallfunc ptr; +PyAPI_FUNC(vectorcallfunc) PyVectorcall_Function(PyObject *callable); - assert(callable != NULL); - tp = Py_TYPE(callable); - if (!PyType_HasFeature(tp, Py_TPFLAGS_HAVE_VECTORCALL)) { - return NULL; - } - assert(PyCallable_Check(callable)); - offset = tp->tp_vectorcall_offset; - assert(offset > 0); - memcpy(&ptr, (char *) callable + offset, sizeof(ptr)); - return ptr; -} - -/* Call the callable object 'callable' with the "vectorcall" calling - convention. - - args is a C array for positional arguments. - - nargsf is the number of positional arguments plus optionally the flag - PY_VECTORCALL_ARGUMENTS_OFFSET which means that the caller is allowed to - modify args[-1]. - - kwnames is a tuple of keyword names. The values of the keyword arguments - are stored in "args" after the positional arguments (note that the number - of keyword arguments does not change nargsf). kwnames can also be NULL if - there are no keyword arguments. - - keywords must only contain strings and all keys must be unique. - - Return the result on success. Raise an exception and return NULL on - error. */ -static inline PyObject * -_PyObject_VectorcallTstate(PyThreadState *tstate, PyObject *callable, - PyObject *const *args, size_t nargsf, - PyObject *kwnames) -{ - vectorcallfunc func; - PyObject *res; - - assert(kwnames == NULL || PyTuple_Check(kwnames)); - assert(args != NULL || PyVectorcall_NARGS(nargsf) == 0); - - func = PyVectorcall_Function(callable); - if (func == NULL) { - Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); - return _PyObject_MakeTpCall(tstate, callable, args, nargs, kwnames); - } - res = func(callable, args, nargsf, kwnames); - return _Py_CheckFunctionResult(tstate, callable, res, NULL); -} - -static inline PyObject * -PyObject_Vectorcall(PyObject *callable, PyObject *const *args, - size_t nargsf, PyObject *kwnames) -{ - PyThreadState *tstate = PyThreadState_Get(); - return _PyObject_VectorcallTstate(tstate, callable, - args, nargsf, kwnames); -} +PyAPI_FUNC(PyObject *) PyObject_Vectorcall( + PyObject *callable, + PyObject *const *args, + size_t nargsf, + PyObject *kwnames); // Backwards compatibility aliases for API that was provisional in Python 3.8 #define _PyObject_Vectorcall PyObject_Vectorcall @@ -145,44 +88,13 @@ PyAPI_FUNC(PyObject *) PyObject_VectorcallDict( "tuple" and keyword arguments "dict". "dict" may also be NULL */ PyAPI_FUNC(PyObject *) PyVectorcall_Call(PyObject *callable, PyObject *tuple, PyObject *dict); -static inline PyObject * -_PyObject_FastCallTstate(PyThreadState *tstate, PyObject *func, PyObject *const *args, Py_ssize_t nargs) -{ - return _PyObject_VectorcallTstate(tstate, func, args, (size_t)nargs, NULL); -} +// Same as PyObject_Vectorcall(), except without keyword arguments +PyAPI_FUNC(PyObject *) _PyObject_FastCall( + PyObject *func, + PyObject *const *args, + Py_ssize_t nargs); -/* Same as PyObject_Vectorcall except without keyword arguments */ -static inline PyObject * -_PyObject_FastCall(PyObject *func, PyObject *const *args, Py_ssize_t nargs) -{ - PyThreadState *tstate = PyThreadState_Get(); - return _PyObject_FastCallTstate(tstate, func, args, nargs); -} - -/* Call a callable without any arguments - Private static inline function variant of public function - PyObject_CallNoArgs(). */ -static inline PyObject * -_PyObject_CallNoArg(PyObject *func) { - PyThreadState *tstate = PyThreadState_Get(); - return _PyObject_VectorcallTstate(tstate, func, NULL, 0, NULL); -} - -static inline PyObject * -PyObject_CallOneArg(PyObject *func, PyObject *arg) -{ - PyObject *_args[2]; - PyObject **args; - PyThreadState *tstate; - size_t nargsf; - - assert(arg != NULL); - args = _args + 1; // For PY_VECTORCALL_ARGUMENTS_OFFSET - args[0] = arg; - tstate = PyThreadState_Get(); - nargsf = 1 | PY_VECTORCALL_ARGUMENTS_OFFSET; - return _PyObject_VectorcallTstate(tstate, func, args, nargsf, NULL); -} +PyAPI_FUNC(PyObject *) PyObject_CallOneArg(PyObject *func, PyObject *arg); PyAPI_FUNC(PyObject *) PyObject_VectorcallMethod( PyObject *name, PyObject *const *args, @@ -191,20 +103,23 @@ PyAPI_FUNC(PyObject *) PyObject_VectorcallMethod( static inline PyObject * PyObject_CallMethodNoArgs(PyObject *self, PyObject *name) { - return PyObject_VectorcallMethod(name, &self, - 1 | PY_VECTORCALL_ARGUMENTS_OFFSET, NULL); + size_t nargsf = 1 | PY_VECTORCALL_ARGUMENTS_OFFSET; + return PyObject_VectorcallMethod(name, &self, nargsf, _Py_NULL); } static inline PyObject * PyObject_CallMethodOneArg(PyObject *self, PyObject *name, PyObject *arg) { PyObject *args[2] = {self, arg}; - + size_t nargsf = 2 | PY_VECTORCALL_ARGUMENTS_OFFSET; assert(arg != NULL); - return PyObject_VectorcallMethod(name, args, - 2 | PY_VECTORCALL_ARGUMENTS_OFFSET, NULL); + return PyObject_VectorcallMethod(name, args, nargsf, _Py_NULL); } +PyAPI_FUNC(PyObject *) _PyObject_CallMethod(PyObject *obj, + PyObject *name, + const char *format, ...); + /* Like PyObject_CallMethod(), but expect a _Py_Identifier* as the method name. */ PyAPI_FUNC(PyObject *) _PyObject_CallMethodId(PyObject *obj, @@ -218,7 +133,7 @@ PyAPI_FUNC(PyObject *) _PyObject_CallMethodId_SizeT(PyObject *obj, PyAPI_FUNC(PyObject *) _PyObject_CallMethodIdObjArgs( PyObject *obj, - struct _Py_Identifier *name, + _Py_Identifier *name, ...); static inline PyObject * @@ -228,7 +143,7 @@ _PyObject_VectorcallMethodId( { PyObject *oname = _PyUnicode_FromId(name); /* borrowed */ if (!oname) { - return NULL; + return _Py_NULL; } return PyObject_VectorcallMethod(oname, args, nargsf, kwnames); } @@ -236,18 +151,17 @@ _PyObject_VectorcallMethodId( static inline PyObject * _PyObject_CallMethodIdNoArgs(PyObject *self, _Py_Identifier *name) { - return _PyObject_VectorcallMethodId(name, &self, - 1 | PY_VECTORCALL_ARGUMENTS_OFFSET, NULL); + size_t nargsf = 1 | PY_VECTORCALL_ARGUMENTS_OFFSET; + return _PyObject_VectorcallMethodId(name, &self, nargsf, _Py_NULL); } static inline PyObject * _PyObject_CallMethodIdOneArg(PyObject *self, _Py_Identifier *name, PyObject *arg) { PyObject *args[2] = {self, arg}; - + size_t nargsf = 2 | PY_VECTORCALL_ARGUMENTS_OFFSET; assert(arg != NULL); - return _PyObject_VectorcallMethodId(name, args, - 2 | PY_VECTORCALL_ARGUMENTS_OFFSET, NULL); + return _PyObject_VectorcallMethodId(name, args, nargsf, _Py_NULL); } PyAPI_FUNC(int) _PyObject_HasLen(PyObject *o); @@ -257,74 +171,6 @@ PyAPI_FUNC(int) _PyObject_HasLen(PyObject *o); value. If one of the calls fails, this function returns -1. */ PyAPI_FUNC(Py_ssize_t) PyObject_LengthHint(PyObject *o, Py_ssize_t); -/* === New Buffer API ============================================ */ - -/* Return 1 if the getbuffer function is available, otherwise return 0. */ -PyAPI_FUNC(int) PyObject_CheckBuffer(PyObject *obj); - -/* This is a C-API version of the getbuffer function call. It checks - to make sure object has the required function pointer and issues the - call. - - Returns -1 and raises an error on failure and returns 0 on success. */ -PyAPI_FUNC(int) PyObject_GetBuffer(PyObject *obj, Py_buffer *view, - int flags); - -/* Get the memory area pointed to by the indices for the buffer given. - Note that view->ndim is the assumed size of indices. */ -PyAPI_FUNC(void *) PyBuffer_GetPointer(Py_buffer *view, Py_ssize_t *indices); - -/* Return the implied itemsize of the data-format area from a - struct-style description. */ -PyAPI_FUNC(Py_ssize_t) PyBuffer_SizeFromFormat(const char *format); - -/* Implementation in memoryobject.c */ -PyAPI_FUNC(int) PyBuffer_ToContiguous(void *buf, Py_buffer *view, - Py_ssize_t len, char order); - -PyAPI_FUNC(int) PyBuffer_FromContiguous(Py_buffer *view, void *buf, - Py_ssize_t len, char order); - -/* Copy len bytes of data from the contiguous chunk of memory - pointed to by buf into the buffer exported by obj. Return - 0 on success and return -1 and raise a PyBuffer_Error on - error (i.e. the object does not have a buffer interface or - it is not working). - - If fort is 'F', then if the object is multi-dimensional, - then the data will be copied into the array in - Fortran-style (first dimension varies the fastest). If - fort is 'C', then the data will be copied into the array - in C-style (last dimension varies the fastest). If fort - is 'A', then it does not matter and the copy will be made - in whatever way is more efficient. */ -PyAPI_FUNC(int) PyObject_CopyData(PyObject *dest, PyObject *src); - -/* Copy the data from the src buffer to the buffer of destination. */ -PyAPI_FUNC(int) PyBuffer_IsContiguous(const Py_buffer *view, char fort); - -/*Fill the strides array with byte-strides of a contiguous - (Fortran-style if fort is 'F' or C-style otherwise) - array of the given shape with the given number of bytes - per element. */ -PyAPI_FUNC(void) PyBuffer_FillContiguousStrides(int ndims, - Py_ssize_t *shape, - Py_ssize_t *strides, - int itemsize, - char fort); - -/* Fills in a buffer-info structure correctly for an exporter - that can only share a contiguous chunk of memory of - "unsigned bytes" of the given length. - - Returns 0 on success and -1 (with raising an error) on error. */ -PyAPI_FUNC(int) PyBuffer_FillInfo(Py_buffer *view, PyObject *o, void *buf, - Py_ssize_t len, int readonly, - int flags); - -/* Releases a Py_buffer obtained from getbuffer ParseTuple's "s*". */ -PyAPI_FUNC(void) PyBuffer_Release(Py_buffer *view); - /* === Sequence protocol ================================================ */ /* Assume tp_as_sequence and sq_item exist and that 'i' does not diff --git a/src/external/windows/include/python/cpython/bytearrayobject.h b/src/external/windows/include/python/cpython/bytearrayobject.h index 061de138..90c06c57 100755 --- a/src/external/windows/include/python/cpython/bytearrayobject.h +++ b/src/external/windows/include/python/cpython/bytearrayobject.h @@ -11,10 +11,28 @@ typedef struct { Py_ssize_t ob_exports; /* How many buffer exports */ } PyByteArrayObject; -/* Macros, trading safety for speed */ -#define PyByteArray_AS_STRING(self) \ - (assert(PyByteArray_Check(self)), \ - Py_SIZE(self) ? ((PyByteArrayObject *)(self))->ob_start : _PyByteArray_empty_string) -#define PyByteArray_GET_SIZE(self) (assert(PyByteArray_Check(self)), Py_SIZE(self)) - PyAPI_DATA(char) _PyByteArray_empty_string[]; + +/* Macros and static inline functions, trading safety for speed */ +#define _PyByteArray_CAST(op) \ + (assert(PyByteArray_Check(op)), _Py_CAST(PyByteArrayObject*, op)) + +static inline char* PyByteArray_AS_STRING(PyObject *op) +{ + PyByteArrayObject *self = _PyByteArray_CAST(op); + if (Py_SIZE(self)) { + return self->ob_start; + } + return _PyByteArray_empty_string; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyByteArray_AS_STRING(self) PyByteArray_AS_STRING(_PyObject_CAST(self)) +#endif + +static inline Py_ssize_t PyByteArray_GET_SIZE(PyObject *op) { + PyByteArrayObject *self = _PyByteArray_CAST(op); + return Py_SIZE(self); +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyByteArray_GET_SIZE(self) PyByteArray_GET_SIZE(_PyObject_CAST(self)) +#endif diff --git a/src/external/windows/include/python/cpython/bytesobject.h b/src/external/windows/include/python/cpython/bytesobject.h index b134904e..6f8e9ff5 100755 --- a/src/external/windows/include/python/cpython/bytesobject.h +++ b/src/external/windows/include/python/cpython/bytesobject.h @@ -4,7 +4,7 @@ typedef struct { PyObject_VAR_HEAD - Py_hash_t ob_shash; + Py_DEPRECATED(3.11) Py_hash_t ob_shash; char ob_sval[1]; /* Invariants: @@ -28,10 +28,25 @@ PyAPI_FUNC(PyObject*) _PyBytes_FromHex( PyAPI_FUNC(PyObject *) _PyBytes_DecodeEscape(const char *, Py_ssize_t, const char *, const char **); -/* Macro, trading safety for speed */ -#define PyBytes_AS_STRING(op) (assert(PyBytes_Check(op)), \ - (((PyBytesObject *)(op))->ob_sval)) -#define PyBytes_GET_SIZE(op) (assert(PyBytes_Check(op)),Py_SIZE(op)) +/* Macros and static inline functions, trading safety for speed */ +#define _PyBytes_CAST(op) \ + (assert(PyBytes_Check(op)), _Py_CAST(PyBytesObject*, op)) + +static inline char* PyBytes_AS_STRING(PyObject *op) +{ + return _PyBytes_CAST(op)->ob_sval; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyBytes_AS_STRING(op) PyBytes_AS_STRING(_PyObject_CAST(op)) +#endif + +static inline Py_ssize_t PyBytes_GET_SIZE(PyObject *op) { + PyBytesObject *self = _PyBytes_CAST(op); + return Py_SIZE(self); +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyBytes_GET_SIZE(self) PyBytes_GET_SIZE(_PyObject_CAST(self)) +#endif /* _PyBytes_Join(sep, x) is like sep.join(x). sep must be PyBytesObject*, x must be an iterable object. */ diff --git a/src/external/windows/include/python/cellobject.h b/src/external/windows/include/python/cpython/cellobject.h old mode 100755 new mode 100644 similarity index 76% rename from src/external/windows/include/python/cellobject.h rename to src/external/windows/include/python/cpython/cellobject.h index 9411eb42..e9405e1d --- a/src/external/windows/include/python/cellobject.h +++ b/src/external/windows/include/python/cpython/cellobject.h @@ -1,4 +1,5 @@ /* Cell object interface */ + #ifndef Py_LIMITED_API #ifndef Py_CELLOBJECT_H #define Py_CELLOBJECT_H @@ -8,7 +9,8 @@ extern "C" { typedef struct { PyObject_HEAD - PyObject *ob_ref; /* Content of the cell or NULL when empty */ + /* Content of the cell or NULL when empty */ + PyObject *ob_ref; } PyCellObject; PyAPI_DATA(PyTypeObject) PyCell_Type; @@ -20,7 +22,7 @@ PyAPI_FUNC(PyObject *) PyCell_Get(PyObject *); PyAPI_FUNC(int) PyCell_Set(PyObject *, PyObject *); #define PyCell_GET(op) (((PyCellObject *)(op))->ob_ref) -#define PyCell_SET(op, v) ((void)(((PyCellObject *)(op))->ob_ref = v)) +#define PyCell_SET(op, v) _Py_RVALUE(((PyCellObject *)(op))->ob_ref = (v)) #ifdef __cplusplus } diff --git a/src/external/windows/include/python/cpython/ceval.h b/src/external/windows/include/python/cpython/ceval.h index bd996113..e63f0a90 100755 --- a/src/external/windows/include/python/cpython/ceval.h +++ b/src/external/windows/include/python/cpython/ceval.h @@ -6,20 +6,16 @@ PyAPI_FUNC(void) PyEval_SetProfile(Py_tracefunc, PyObject *); PyAPI_DATA(int) _PyEval_SetProfile(PyThreadState *tstate, Py_tracefunc func, PyObject *arg); PyAPI_FUNC(void) PyEval_SetTrace(Py_tracefunc, PyObject *); PyAPI_FUNC(int) _PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg); -PyAPI_FUNC(int) _PyEval_GetCoroutineOriginTrackingDepth(void); -PyAPI_FUNC(int) _PyEval_SetAsyncGenFirstiter(PyObject *); -PyAPI_FUNC(PyObject *) _PyEval_GetAsyncGenFirstiter(void); -PyAPI_FUNC(int) _PyEval_SetAsyncGenFinalizer(PyObject *); -PyAPI_FUNC(PyObject *) _PyEval_GetAsyncGenFinalizer(void); /* Helper to look up a builtin object */ +PyAPI_FUNC(PyObject *) _PyEval_GetBuiltin(PyObject *); PyAPI_FUNC(PyObject *) _PyEval_GetBuiltinId(_Py_Identifier *); /* Look at the current frame's (if any) code's co_flags, and turn on the corresponding compiler flags in cf->cf_flags. Return 1 if any flag was set, else return 0. */ PyAPI_FUNC(int) PyEval_MergeCompilerFlags(PyCompilerFlags *cf); -PyAPI_FUNC(PyObject *) _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int exc); +PyAPI_FUNC(PyObject *) _PyEval_EvalFrameDefault(PyThreadState *tstate, struct _PyInterpreterFrame *f, int exc); PyAPI_FUNC(void) _PyEval_SetSwitchInterval(unsigned long microseconds); PyAPI_FUNC(unsigned long) _PyEval_GetSwitchInterval(void); diff --git a/src/external/windows/include/python/classobject.h b/src/external/windows/include/python/cpython/classobject.h old mode 100755 new mode 100644 similarity index 93% rename from src/external/windows/include/python/classobject.h rename to src/external/windows/include/python/cpython/classobject.h index 5ee99b3b..9541f1d0 --- a/src/external/windows/include/python/classobject.h +++ b/src/external/windows/include/python/cpython/classobject.h @@ -53,5 +53,5 @@ PyAPI_FUNC(PyObject *) PyInstanceMethod_Function(PyObject *); #ifdef __cplusplus } #endif -#endif /* !Py_CLASSOBJECT_H */ -#endif /* Py_LIMITED_API */ +#endif // !Py_CLASSOBJECT_H +#endif // !Py_LIMITED_API diff --git a/src/external/windows/include/python/cpython/code.h b/src/external/windows/include/python/cpython/code.h index 1381564b..3cacfb6b 100755 --- a/src/external/windows/include/python/cpython/code.h +++ b/src/external/windows/include/python/cpython/code.h @@ -1,67 +1,106 @@ -#ifndef Py_CPYTHON_CODE_H -# error "this header file must not be included directly" +/* Definitions for bytecode */ + +#ifndef Py_LIMITED_API +#ifndef Py_CODE_H +#define Py_CODE_H +#ifdef __cplusplus +extern "C" { #endif +/* Each instruction in a code object is a fixed-width value, + * currently 2 bytes: 1-byte opcode + 1-byte oparg. The EXTENDED_ARG + * opcode allows for larger values but the current limit is 3 uses + * of EXTENDED_ARG (see Python/compile.c), for a maximum + * 32-bit value. This aligns with the note in Python/compile.c + * (compiler_addop_i_line) indicating that the max oparg value is + * 2**32 - 1, rather than INT_MAX. + */ + typedef uint16_t _Py_CODEUNIT; #ifdef WORDS_BIGENDIAN # define _Py_OPCODE(word) ((word) >> 8) # define _Py_OPARG(word) ((word) & 255) +# define _Py_MAKECODEUNIT(opcode, oparg) (((opcode)<<8)|(oparg)) #else # define _Py_OPCODE(word) ((word) & 255) # define _Py_OPARG(word) ((word) >> 8) +# define _Py_MAKECODEUNIT(opcode, oparg) ((opcode)|((oparg)<<8)) #endif -typedef struct _PyOpcache _PyOpcache; +// Use "unsigned char" instead of "uint8_t" here to avoid illegal aliasing: +#define _Py_SET_OPCODE(word, opcode) (((unsigned char *)&(word))[0] = (opcode)) + +// To avoid repeating ourselves in deepfreeze.py, all PyCodeObject members are +// defined in this macro: +#define _PyCode_DEF(SIZE) { \ + PyObject_VAR_HEAD \ + \ + /* Note only the following fields are used in hash and/or comparisons \ + * \ + * - co_name \ + * - co_argcount \ + * - co_posonlyargcount \ + * - co_kwonlyargcount \ + * - co_nlocals \ + * - co_stacksize \ + * - co_flags \ + * - co_firstlineno \ + * - co_consts \ + * - co_names \ + * - co_localsplusnames \ + * This is done to preserve the name and line number for tracebacks \ + * and debuggers; otherwise, constant de-duplication would collapse \ + * identical functions/lambdas defined on different lines. \ + */ \ + \ + /* These fields are set with provided values on new code objects. */ \ + \ + /* The hottest fields (in the eval loop) are grouped here at the top. */ \ + PyObject *co_consts; /* list (constants used) */ \ + PyObject *co_names; /* list of strings (names used) */ \ + PyObject *co_exceptiontable; /* Byte string encoding exception handling \ + table */ \ + int co_flags; /* CO_..., see below */ \ + short co_warmup; /* Warmup counter for quickening */ \ + short _co_linearray_entry_size; /* Size of each entry in _co_linearray */ \ + \ + /* The rest are not so impactful on performance. */ \ + int co_argcount; /* #arguments, except *args */ \ + int co_posonlyargcount; /* #positional only arguments */ \ + int co_kwonlyargcount; /* #keyword only arguments */ \ + int co_stacksize; /* #entries needed for evaluation stack */ \ + int co_firstlineno; /* first source line number */ \ + \ + /* redundant values (derived from co_localsplusnames and \ + co_localspluskinds) */ \ + int co_nlocalsplus; /* number of local + cell + free variables \ + */ \ + int co_nlocals; /* number of local variables */ \ + int co_nplaincellvars; /* number of non-arg cell variables */ \ + int co_ncellvars; /* total number of cell variables */ \ + int co_nfreevars; /* number of free variables */ \ + \ + PyObject *co_localsplusnames; /* tuple mapping offsets to names */ \ + PyObject *co_localspluskinds; /* Bytes mapping to local kinds (one byte \ + per variable) */ \ + PyObject *co_filename; /* unicode (where it was loaded from) */ \ + PyObject *co_name; /* unicode (name, for reference) */ \ + PyObject *co_qualname; /* unicode (qualname, for reference) */ \ + PyObject *co_linetable; /* bytes object that holds location info */ \ + PyObject *co_weakreflist; /* to support weakrefs to code objects */ \ + PyObject *_co_code; /* cached co_code object/attribute */ \ + char *_co_linearray; /* array of line offsets */ \ + int _co_firsttraceable; /* index of first traceable instruction */ \ + /* Scratch space for extra data relating to the code object. \ + Type is a void* to keep the format private in codeobject.c to force \ + people to go through the proper APIs. */ \ + void *co_extra; \ + char co_code_adaptive[(SIZE)]; \ +} /* Bytecode object */ -struct PyCodeObject { - PyObject_HEAD - int co_argcount; /* #arguments, except *args */ - int co_posonlyargcount; /* #positional only arguments */ - int co_kwonlyargcount; /* #keyword only arguments */ - int co_nlocals; /* #local variables */ - int co_stacksize; /* #entries needed for evaluation stack */ - int co_flags; /* CO_..., see below */ - int co_firstlineno; /* first source line number */ - PyObject *co_code; /* instruction opcodes */ - PyObject *co_consts; /* list (constants used) */ - PyObject *co_names; /* list of strings (names used) */ - PyObject *co_varnames; /* tuple of strings (local variable names) */ - PyObject *co_freevars; /* tuple of strings (free variable names) */ - PyObject *co_cellvars; /* tuple of strings (cell variable names) */ - /* The rest aren't used in either hash or comparisons, except for co_name, - used in both. This is done to preserve the name and line number - for tracebacks and debuggers; otherwise, constant de-duplication - would collapse identical functions/lambdas defined on different lines. - */ - Py_ssize_t *co_cell2arg; /* Maps cell vars which are arguments. */ - PyObject *co_filename; /* unicode (where it was loaded from) */ - PyObject *co_name; /* unicode (name, for reference) */ - PyObject *co_linetable; /* string (encoding addr<->lineno mapping) See - Objects/lnotab_notes.txt for details. */ - void *co_zombieframe; /* for optimization only (see frameobject.c) */ - PyObject *co_weakreflist; /* to support weakrefs to code objects */ - /* Scratch space for extra data relating to the code object. - Type is a void* to keep the format private in codeobject.c to force - people to go through the proper APIs. */ - void *co_extra; - - /* Per opcodes just-in-time cache - * - * To reduce cache size, we use indirect mapping from opcode index to - * cache object: - * cache = co_opcache[co_opcache_map[next_instr - first_instr] - 1] - */ - - // co_opcache_map is indexed by (next_instr - first_instr). - // * 0 means there is no cache for this opcode. - // * n > 0 means there is cache in co_opcache[n-1]. - unsigned char *co_opcache_map; - _PyOpcache *co_opcache; - int co_opcache_flag; // used to determine when create a cache. - unsigned char co_opcache_size; // length of co_opcache. -}; +struct PyCodeObject _PyCode_DEF(1); /* Masks for co_flags above */ #define CO_OPTIMIZED 0x0001 @@ -70,12 +109,6 @@ struct PyCodeObject { #define CO_VARKEYWORDS 0x0008 #define CO_NESTED 0x0010 #define CO_GENERATOR 0x0020 -/* The CO_NOFREE flag is set if there are no free or cell variables. - This information is redundant, but it allows a single flag test - to determine whether there is any extra work to be done when the - call frame it setup. -*/ -#define CO_NOFREE 0x0040 /* The CO_COROUTINE flag is set for coroutine functions (defined with ``async def`` keywords) */ @@ -97,10 +130,6 @@ struct PyCodeObject { #define CO_FUTURE_GENERATOR_STOP 0x800000 #define CO_FUTURE_ANNOTATIONS 0x1000000 -/* This value is found in the co_cell2arg array when the associated cell - variable does not correspond to an argument. */ -#define CO_CELL_NOT_AN_ARG (-1) - /* This should be defined if a future statement modifies the syntax. For example, when a keyword is added. */ @@ -111,18 +140,22 @@ struct PyCodeObject { PyAPI_DATA(PyTypeObject) PyCode_Type; #define PyCode_Check(op) Py_IS_TYPE(op, &PyCode_Type) -#define PyCode_GetNumFree(op) (PyTuple_GET_SIZE((op)->co_freevars)) +#define PyCode_GetNumFree(op) ((op)->co_nfreevars) +#define _PyCode_CODE(CO) ((_Py_CODEUNIT *)(CO)->co_code_adaptive) +#define _PyCode_NBYTES(CO) (Py_SIZE(CO) * (Py_ssize_t)sizeof(_Py_CODEUNIT)) /* Public interface */ PyAPI_FUNC(PyCodeObject *) PyCode_New( int, int, int, int, int, PyObject *, PyObject *, PyObject *, PyObject *, PyObject *, PyObject *, - PyObject *, PyObject *, int, PyObject *); + PyObject *, PyObject *, PyObject *, int, PyObject *, + PyObject *); PyAPI_FUNC(PyCodeObject *) PyCode_NewWithPosOnlyArgs( int, int, int, int, int, int, PyObject *, PyObject *, PyObject *, PyObject *, PyObject *, PyObject *, - PyObject *, PyObject *, int, PyObject *); + PyObject *, PyObject *, PyObject *, int, PyObject *, + PyObject *); /* same as struct above */ /* Creates a new empty code object with the specified source location. */ @@ -134,11 +167,13 @@ PyCode_NewEmpty(const char *filename, const char *funcname, int firstlineno); use PyFrame_GetLineNumber() instead. */ PyAPI_FUNC(int) PyCode_Addr2Line(PyCodeObject *, int); +PyAPI_FUNC(int) PyCode_Addr2Location(PyCodeObject *, int, int *, int *, int *, int *); + /* for internal use only */ struct _opaque { int computed_line; - const char *lo_next; - const char *limit; + const uint8_t *lo_next; + const uint8_t *limit; }; typedef struct _line_offsets { @@ -171,14 +206,31 @@ PyAPI_FUNC(int) _PyCode_GetExtra(PyObject *code, Py_ssize_t index, PyAPI_FUNC(int) _PyCode_SetExtra(PyObject *code, Py_ssize_t index, void *extra); -/** API for initializing the line number table. */ -int _PyCode_InitAddressRange(PyCodeObject* co, PyCodeAddressRange *bounds); +/* Equivalent to getattr(code, 'co_code') in Python. + Returns a strong reference to a bytes object. */ +PyAPI_FUNC(PyObject *) PyCode_GetCode(PyCodeObject *code); +/* Equivalent to getattr(code, 'co_varnames') in Python. */ +PyAPI_FUNC(PyObject *) PyCode_GetVarnames(PyCodeObject *code); +/* Equivalent to getattr(code, 'co_cellvars') in Python. */ +PyAPI_FUNC(PyObject *) PyCode_GetCellvars(PyCodeObject *code); +/* Equivalent to getattr(code, 'co_freevars') in Python. */ +PyAPI_FUNC(PyObject *) PyCode_GetFreevars(PyCodeObject *code); -/** Out of process API for initializing the line number table. */ -void PyLineTable_InitAddressRange(const char *linetable, Py_ssize_t length, int firstlineno, PyCodeAddressRange *range); - -/** API for traversing the line number table. */ -int PyLineTable_NextAddressRange(PyCodeAddressRange *range); -int PyLineTable_PreviousAddressRange(PyCodeAddressRange *range); +typedef enum _PyCodeLocationInfoKind { + /* short forms are 0 to 9 */ + PY_CODE_LOCATION_INFO_SHORT0 = 0, + /* one lineforms are 10 to 12 */ + PY_CODE_LOCATION_INFO_ONE_LINE0 = 10, + PY_CODE_LOCATION_INFO_ONE_LINE1 = 11, + PY_CODE_LOCATION_INFO_ONE_LINE2 = 12, + PY_CODE_LOCATION_INFO_NO_COLUMNS = 13, + PY_CODE_LOCATION_INFO_LONG = 14, + PY_CODE_LOCATION_INFO_NONE = 15 +} _PyCodeLocationInfoKind; +#ifdef __cplusplus +} +#endif +#endif // !Py_CODE_H +#endif // !Py_LIMITED_API diff --git a/src/external/windows/include/python/cpython/complexobject.h b/src/external/windows/include/python/cpython/complexobject.h new file mode 100644 index 00000000..e1c2c2db --- /dev/null +++ b/src/external/windows/include/python/cpython/complexobject.h @@ -0,0 +1,44 @@ +#ifndef Py_CPYTHON_COMPLEXOBJECT_H +# error "this header file must not be included directly" +#endif + +typedef struct { + double real; + double imag; +} Py_complex; + +/* Operations on complex numbers from complexmodule.c */ + +PyAPI_FUNC(Py_complex) _Py_c_sum(Py_complex, Py_complex); +PyAPI_FUNC(Py_complex) _Py_c_diff(Py_complex, Py_complex); +PyAPI_FUNC(Py_complex) _Py_c_neg(Py_complex); +PyAPI_FUNC(Py_complex) _Py_c_prod(Py_complex, Py_complex); +PyAPI_FUNC(Py_complex) _Py_c_quot(Py_complex, Py_complex); +PyAPI_FUNC(Py_complex) _Py_c_pow(Py_complex, Py_complex); +PyAPI_FUNC(double) _Py_c_abs(Py_complex); + +/* Complex object interface */ + +/* +PyComplexObject represents a complex number with double-precision +real and imaginary parts. +*/ +typedef struct { + PyObject_HEAD + Py_complex cval; +} PyComplexObject; + +PyAPI_FUNC(PyObject *) PyComplex_FromCComplex(Py_complex); + +PyAPI_FUNC(Py_complex) PyComplex_AsCComplex(PyObject *op); + +#ifdef Py_BUILD_CORE +/* Format the object based on the format_spec, as defined in PEP 3101 + (Advanced String Formatting). */ +extern int _PyComplex_FormatAdvancedWriter( + _PyUnicodeWriter *writer, + PyObject *obj, + PyObject *format_spec, + Py_ssize_t start, + Py_ssize_t end); +#endif // Py_BUILD_CORE diff --git a/src/external/windows/include/python/context.h b/src/external/windows/include/python/cpython/context.h old mode 100755 new mode 100644 similarity index 95% rename from src/external/windows/include/python/context.h rename to src/external/windows/include/python/cpython/context.h index e1a0f0fb..aebba956 --- a/src/external/windows/include/python/context.h +++ b/src/external/windows/include/python/cpython/context.h @@ -1,12 +1,10 @@ +#ifndef Py_LIMITED_API #ifndef Py_CONTEXT_H #define Py_CONTEXT_H #ifdef __cplusplus extern "C" { #endif -#ifndef Py_LIMITED_API - - PyAPI_DATA(PyTypeObject) PyContext_Type; typedef struct _pycontextobject PyContext; @@ -73,9 +71,8 @@ PyAPI_FUNC(int) PyContextVar_Reset(PyObject *var, PyObject *token); PyAPI_FUNC(PyObject *) _PyContext_NewHamtForTests(void); -#endif /* !Py_LIMITED_API */ - #ifdef __cplusplus } #endif #endif /* !Py_CONTEXT_H */ +#endif /* !Py_LIMITED_API */ diff --git a/src/external/windows/include/python/cpython/descrobject.h b/src/external/windows/include/python/cpython/descrobject.h new file mode 100644 index 00000000..8f249d94 --- /dev/null +++ b/src/external/windows/include/python/cpython/descrobject.h @@ -0,0 +1,64 @@ +#ifndef Py_CPYTHON_DESCROBJECT_H +# error "this header file must not be included directly" +#endif + +typedef PyObject *(*wrapperfunc)(PyObject *self, PyObject *args, + void *wrapped); + +typedef PyObject *(*wrapperfunc_kwds)(PyObject *self, PyObject *args, + void *wrapped, PyObject *kwds); + +struct wrapperbase { + const char *name; + int offset; + void *function; + wrapperfunc wrapper; + const char *doc; + int flags; + PyObject *name_strobj; +}; + +/* Flags for above struct */ +#define PyWrapperFlag_KEYWORDS 1 /* wrapper function takes keyword args */ + +/* Various kinds of descriptor objects */ + +typedef struct { + PyObject_HEAD + PyTypeObject *d_type; + PyObject *d_name; + PyObject *d_qualname; +} PyDescrObject; + +#define PyDescr_COMMON PyDescrObject d_common + +#define PyDescr_TYPE(x) (((PyDescrObject *)(x))->d_type) +#define PyDescr_NAME(x) (((PyDescrObject *)(x))->d_name) + +typedef struct { + PyDescr_COMMON; + PyMethodDef *d_method; + vectorcallfunc vectorcall; +} PyMethodDescrObject; + +typedef struct { + PyDescr_COMMON; + PyMemberDef *d_member; +} PyMemberDescrObject; + +typedef struct { + PyDescr_COMMON; + PyGetSetDef *d_getset; +} PyGetSetDescrObject; + +typedef struct { + PyDescr_COMMON; + struct wrapperbase *d_base; + void *d_wrapped; /* This can be any function pointer */ +} PyWrapperDescrObject; + +PyAPI_DATA(PyTypeObject) _PyMethodWrapper_Type; + +PyAPI_FUNC(PyObject *) PyDescr_NewWrapper(PyTypeObject *, + struct wrapperbase *, void *); +PyAPI_FUNC(int) PyDescr_IsData(PyObject *); diff --git a/src/external/windows/include/python/cpython/dictobject.h b/src/external/windows/include/python/cpython/dictobject.h index 1888dd81..9aebdbee 100755 --- a/src/external/windows/include/python/cpython/dictobject.h +++ b/src/external/windows/include/python/cpython/dictobject.h @@ -3,6 +3,7 @@ #endif typedef struct _dictkeysobject PyDictKeysObject; +typedef struct _dictvalues PyDictValues; /* The ma_values pointer is NULL for a combined table * or points to an array of PyObject* for a split table @@ -24,13 +25,14 @@ typedef struct { If ma_values is not NULL, the table is split: keys are stored in ma_keys and values are stored in ma_values */ - PyObject **ma_values; + PyDictValues *ma_values; } PyDictObject; PyAPI_FUNC(PyObject *) _PyDict_GetItem_KnownHash(PyObject *mp, PyObject *key, Py_hash_t hash); +PyAPI_FUNC(PyObject *) _PyDict_GetItemWithError(PyObject *dp, PyObject *key); PyAPI_FUNC(PyObject *) _PyDict_GetItemIdWithError(PyObject *dp, - struct _Py_Identifier *key); + _Py_Identifier *key); PyAPI_FUNC(PyObject *) _PyDict_GetItemStringWithError(PyObject *, const char *); PyAPI_FUNC(PyObject *) PyDict_SetDefault( PyObject *mp, PyObject *key, PyObject *defaultobj); @@ -40,22 +42,18 @@ PyAPI_FUNC(int) _PyDict_DelItem_KnownHash(PyObject *mp, PyObject *key, Py_hash_t hash); PyAPI_FUNC(int) _PyDict_DelItemIf(PyObject *mp, PyObject *key, int (*predicate)(PyObject *value)); -PyDictKeysObject *_PyDict_NewKeysForClass(void); PyAPI_FUNC(int) _PyDict_Next( PyObject *mp, Py_ssize_t *pos, PyObject **key, PyObject **value, Py_hash_t *hash); /* Get the number of items of a dictionary. */ #define PyDict_GET_SIZE(mp) (assert(PyDict_Check(mp)),((PyDictObject *)mp)->ma_used) PyAPI_FUNC(int) _PyDict_Contains_KnownHash(PyObject *, PyObject *, Py_hash_t); -PyAPI_FUNC(int) _PyDict_ContainsId(PyObject *, struct _Py_Identifier *); +PyAPI_FUNC(int) _PyDict_ContainsId(PyObject *, _Py_Identifier *); PyAPI_FUNC(PyObject *) _PyDict_NewPresized(Py_ssize_t minused); PyAPI_FUNC(void) _PyDict_MaybeUntrack(PyObject *mp); PyAPI_FUNC(int) _PyDict_HasOnlyStringKeys(PyObject *mp); -Py_ssize_t _PyDict_KeysSize(PyDictKeysObject *keys); PyAPI_FUNC(Py_ssize_t) _PyDict_SizeOf(PyDictObject *); PyAPI_FUNC(PyObject *) _PyDict_Pop(PyObject *, PyObject *, PyObject *); -PyObject *_PyDict_Pop_KnownHash(PyObject *, PyObject *, Py_hash_t, PyObject *); -PyObject *_PyDict_FromKeys(PyObject *, PyObject *, PyObject *); #define _PyDict_HasSplitTable(d) ((d)->ma_values != NULL) /* Like PyDict_Merge, but override can be 0, 1 or 2. If override is 0, @@ -64,15 +62,11 @@ PyObject *_PyDict_FromKeys(PyObject *, PyObject *, PyObject *); argument is raised. */ PyAPI_FUNC(int) _PyDict_MergeEx(PyObject *mp, PyObject *other, int override); -PyAPI_FUNC(int) _PyDict_SetItemId(PyObject *dp, struct _Py_Identifier *key, PyObject *item); +PyAPI_FUNC(int) _PyDict_SetItemId(PyObject *dp, _Py_Identifier *key, PyObject *item); -PyAPI_FUNC(int) _PyDict_DelItemId(PyObject *mp, struct _Py_Identifier *key); +PyAPI_FUNC(int) _PyDict_DelItemId(PyObject *mp, _Py_Identifier *key); PyAPI_FUNC(void) _PyDict_DebugMallocStats(FILE *out); -int _PyObjectDict_SetItem(PyTypeObject *tp, PyObject **dictptr, PyObject *name, PyObject *value); -PyObject *_PyDict_LoadGlobal(PyDictObject *, PyDictObject *, PyObject *); -Py_ssize_t _PyDict_GetItemHint(PyDictObject *, PyObject *, Py_ssize_t, PyObject **); - /* _PyDictView */ typedef struct { diff --git a/src/external/windows/include/python/cpython/fileutils.h b/src/external/windows/include/python/cpython/fileutils.h index 7d10f615..57a09c90 100755 --- a/src/external/windows/include/python/cpython/fileutils.h +++ b/src/external/windows/include/python/cpython/fileutils.h @@ -2,171 +2,7 @@ # error "this header file must not be included directly" #endif -typedef enum { - _Py_ERROR_UNKNOWN=0, - _Py_ERROR_STRICT, - _Py_ERROR_SURROGATEESCAPE, - _Py_ERROR_REPLACE, - _Py_ERROR_IGNORE, - _Py_ERROR_BACKSLASHREPLACE, - _Py_ERROR_SURROGATEPASS, - _Py_ERROR_XMLCHARREFREPLACE, - _Py_ERROR_OTHER -} _Py_error_handler; - -PyAPI_FUNC(_Py_error_handler) _Py_GetErrorHandler(const char *errors); - -PyAPI_FUNC(int) _Py_DecodeLocaleEx( - const char *arg, - wchar_t **wstr, - size_t *wlen, - const char **reason, - int current_locale, - _Py_error_handler errors); - -PyAPI_FUNC(int) _Py_EncodeLocaleEx( - const wchar_t *text, - char **str, - size_t *error_pos, - const char **reason, - int current_locale, - _Py_error_handler errors); - -PyAPI_FUNC(char*) _Py_EncodeLocaleRaw( - const wchar_t *text, - size_t *error_pos); - -PyAPI_FUNC(PyObject *) _Py_device_encoding(int); - -#if defined(MS_WINDOWS) || defined(__APPLE__) - /* On Windows, the count parameter of read() is an int (bpo-9015, bpo-9611). - On macOS 10.13, read() and write() with more than INT_MAX bytes - fail with EINVAL (bpo-24658). */ -# define _PY_READ_MAX INT_MAX -# define _PY_WRITE_MAX INT_MAX -#else - /* write() should truncate the input to PY_SSIZE_T_MAX bytes, - but it's safer to do it ourself to have a portable behaviour */ -# define _PY_READ_MAX PY_SSIZE_T_MAX -# define _PY_WRITE_MAX PY_SSIZE_T_MAX -#endif - -#ifdef MS_WINDOWS -struct _Py_stat_struct { - unsigned long st_dev; - uint64_t st_ino; - unsigned short st_mode; - int st_nlink; - int st_uid; - int st_gid; - unsigned long st_rdev; - __int64 st_size; - time_t st_atime; - int st_atime_nsec; - time_t st_mtime; - int st_mtime_nsec; - time_t st_ctime; - int st_ctime_nsec; - unsigned long st_file_attributes; - unsigned long st_reparse_tag; -}; -#else -# define _Py_stat_struct stat -#endif - -PyAPI_FUNC(int) _Py_fstat( - int fd, - struct _Py_stat_struct *status); - -PyAPI_FUNC(int) _Py_fstat_noraise( - int fd, - struct _Py_stat_struct *status); - -PyAPI_FUNC(int) _Py_stat( - PyObject *path, - struct stat *status); - -PyAPI_FUNC(int) _Py_open( - const char *pathname, - int flags); - -PyAPI_FUNC(int) _Py_open_noraise( - const char *pathname, - int flags); - -PyAPI_FUNC(FILE *) _Py_wfopen( - const wchar_t *path, - const wchar_t *mode); - +// Used by _testcapi which must not use the internal C API PyAPI_FUNC(FILE*) _Py_fopen_obj( PyObject *path, const char *mode); - -PyAPI_FUNC(Py_ssize_t) _Py_read( - int fd, - void *buf, - size_t count); - -PyAPI_FUNC(Py_ssize_t) _Py_write( - int fd, - const void *buf, - size_t count); - -PyAPI_FUNC(Py_ssize_t) _Py_write_noraise( - int fd, - const void *buf, - size_t count); - -#ifdef HAVE_READLINK -PyAPI_FUNC(int) _Py_wreadlink( - const wchar_t *path, - wchar_t *buf, - /* Number of characters of 'buf' buffer - including the trailing NUL character */ - size_t buflen); -#endif - -#ifdef HAVE_REALPATH -PyAPI_FUNC(wchar_t*) _Py_wrealpath( - const wchar_t *path, - wchar_t *resolved_path, - /* Number of characters of 'resolved_path' buffer - including the trailing NUL character */ - size_t resolved_path_len); -#endif - -#ifndef MS_WINDOWS -PyAPI_FUNC(int) _Py_isabs(const wchar_t *path); -#endif - -PyAPI_FUNC(int) _Py_abspath(const wchar_t *path, wchar_t **abspath_p); - -PyAPI_FUNC(wchar_t*) _Py_wgetcwd( - wchar_t *buf, - /* Number of characters of 'buf' buffer - including the trailing NUL character */ - size_t buflen); - -PyAPI_FUNC(int) _Py_get_inheritable(int fd); - -PyAPI_FUNC(int) _Py_set_inheritable(int fd, int inheritable, - int *atomic_flag_works); - -PyAPI_FUNC(int) _Py_set_inheritable_async_safe(int fd, int inheritable, - int *atomic_flag_works); - -PyAPI_FUNC(int) _Py_dup(int fd); - -#ifndef MS_WINDOWS -PyAPI_FUNC(int) _Py_get_blocking(int fd); - -PyAPI_FUNC(int) _Py_set_blocking(int fd, int blocking); -#else /* MS_WINDOWS */ -PyAPI_FUNC(void*) _Py_get_osfhandle_noraise(int fd); - -PyAPI_FUNC(void*) _Py_get_osfhandle(int fd); - -PyAPI_FUNC(int) _Py_open_osfhandle_noraise(void *handle, int flags); - -PyAPI_FUNC(int) _Py_open_osfhandle(void *handle, int flags); -#endif /* MS_WINDOWS */ diff --git a/src/external/windows/include/python/cpython/floatobject.h b/src/external/windows/include/python/cpython/floatobject.h new file mode 100644 index 00000000..d9a34f7d --- /dev/null +++ b/src/external/windows/include/python/cpython/floatobject.h @@ -0,0 +1,21 @@ +#ifndef Py_CPYTHON_FLOATOBJECT_H +# error "this header file must not be included directly" +#endif + +typedef struct { + PyObject_HEAD + double ob_fval; +} PyFloatObject; + +// Macro version of PyFloat_AsDouble() trading safety for speed. +// It doesn't check if op is a double object. +#define PyFloat_AS_DOUBLE(op) (((PyFloatObject *)(op))->ob_fval) + + +PyAPI_FUNC(int) PyFloat_Pack2(double x, char *p, int le); +PyAPI_FUNC(int) PyFloat_Pack4(double x, char *p, int le); +PyAPI_FUNC(int) PyFloat_Pack8(double x, char *p, int le); + +PyAPI_FUNC(double) PyFloat_Unpack2(const char *p, int le); +PyAPI_FUNC(double) PyFloat_Unpack4(const char *p, int le); +PyAPI_FUNC(double) PyFloat_Unpack8(const char *p, int le); diff --git a/src/external/windows/include/python/cpython/frameobject.h b/src/external/windows/include/python/cpython/frameobject.h index 4e568da2..f4e1417c 100755 --- a/src/external/windows/include/python/cpython/frameobject.h +++ b/src/external/windows/include/python/cpython/frameobject.h @@ -4,91 +4,26 @@ # error "this header file must not be included directly" #endif -/* These values are chosen so that the inline functions below all - * compare f_state to zero. - */ -enum _framestate { - FRAME_CREATED = -2, - FRAME_SUSPENDED = -1, - FRAME_EXECUTING = 0, - FRAME_RETURNED = 1, - FRAME_UNWINDING = 2, - FRAME_RAISED = 3, - FRAME_CLEARED = 4 -}; - -typedef signed char PyFrameState; - -typedef struct { - int b_type; /* what kind of block this is */ - int b_handler; /* where to jump to find handler */ - int b_level; /* value stack level to pop to */ -} PyTryBlock; - -struct _frame { - PyObject_VAR_HEAD - struct _frame *f_back; /* previous frame, or NULL */ - PyCodeObject *f_code; /* code segment */ - PyObject *f_builtins; /* builtin symbol table (PyDictObject) */ - PyObject *f_globals; /* global symbol table (PyDictObject) */ - PyObject *f_locals; /* local symbol table (any mapping) */ - PyObject **f_valuestack; /* points after the last local */ - PyObject *f_trace; /* Trace function */ - int f_stackdepth; /* Depth of value stack */ - char f_trace_lines; /* Emit per-line trace events? */ - char f_trace_opcodes; /* Emit per-opcode trace events? */ - - /* Borrowed reference to a generator, or NULL */ - PyObject *f_gen; - - int f_lasti; /* Last instruction if called */ - int f_lineno; /* Current line number. Only valid if non-zero */ - int f_iblock; /* index in f_blockstack */ - PyFrameState f_state; /* What state the frame is in */ - PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */ - PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */ -}; - -static inline int _PyFrame_IsRunnable(struct _frame *f) { - return f->f_state < FRAME_EXECUTING; -} - -static inline int _PyFrame_IsExecuting(struct _frame *f) { - return f->f_state == FRAME_EXECUTING; -} - -static inline int _PyFrameHasCompleted(struct _frame *f) { - return f->f_state > FRAME_EXECUTING; -} - /* Standard object interface */ -PyAPI_DATA(PyTypeObject) PyFrame_Type; - -#define PyFrame_Check(op) Py_IS_TYPE(op, &PyFrame_Type) - PyAPI_FUNC(PyFrameObject *) PyFrame_New(PyThreadState *, PyCodeObject *, PyObject *, PyObject *); -/* only internal use */ -PyFrameObject* -_PyFrame_New_NoTrack(PyThreadState *, PyFrameConstructor *, PyObject *); - - /* The rest of the interface is specific for frame objects */ -/* Block management functions */ - -PyAPI_FUNC(void) PyFrame_BlockSetup(PyFrameObject *, int, int, int); -PyAPI_FUNC(PyTryBlock *) PyFrame_BlockPop(PyFrameObject *); - /* Conversions between "fast locals" and locals in dictionary */ PyAPI_FUNC(void) PyFrame_LocalsToFast(PyFrameObject *, int); +/* -- Caveat emptor -- + * The concept of entry frames is an implementation detail of the CPython + * interpreter. This API is considered unstable and is provided for the + * convenience of debuggers, profilers and state-inspecting tools. Notice that + * this API can be changed in future minor versions if the underlying frame + * mechanism change or the concept of an 'entry frame' or its semantics becomes + * obsolete or outdated. */ + +PyAPI_FUNC(int) _PyFrame_IsEntryFrame(PyFrameObject *frame); + PyAPI_FUNC(int) PyFrame_FastToLocalsWithError(PyFrameObject *f); PyAPI_FUNC(void) PyFrame_FastToLocals(PyFrameObject *); - -PyAPI_FUNC(void) _PyFrame_DebugMallocStats(FILE *out); - -PyAPI_FUNC(PyFrameObject *) PyFrame_GetBack(PyFrameObject *frame); diff --git a/src/external/windows/include/python/funcobject.h b/src/external/windows/include/python/cpython/funcobject.h old mode 100755 new mode 100644 similarity index 90% rename from src/external/windows/include/python/funcobject.h rename to src/external/windows/include/python/cpython/funcobject.h index ce70da02..517d72a1 --- a/src/external/windows/include/python/funcobject.h +++ b/src/external/windows/include/python/cpython/funcobject.h @@ -1,5 +1,5 @@ - /* Function object interface */ + #ifndef Py_LIMITED_API #ifndef Py_FUNCOBJECT_H #define Py_FUNCOBJECT_H @@ -42,6 +42,14 @@ typedef struct { PyObject *func_module; /* The __module__ attribute, can be anything */ PyObject *func_annotations; /* Annotations, a dict or NULL */ vectorcallfunc vectorcall; + /* Version number for use by specializer. + * Can set to non-zero when we want to specialize. + * Will be set to zero if any of these change: + * defaults + * kwdefaults (only if the object changes, not the contents of the dict) + * code + * annotations */ + uint32_t func_version; /* Invariant: * func_closure contains the bindings for func_code->co_freevars, so @@ -68,13 +76,11 @@ PyAPI_FUNC(int) PyFunction_SetClosure(PyObject *, PyObject *); PyAPI_FUNC(PyObject *) PyFunction_GetAnnotations(PyObject *); PyAPI_FUNC(int) PyFunction_SetAnnotations(PyObject *, PyObject *); -#ifndef Py_LIMITED_API PyAPI_FUNC(PyObject *) _PyFunction_Vectorcall( PyObject *func, PyObject *const *stack, size_t nargsf, PyObject *kwnames); -#endif /* Macros for direct access to these values. Type checks are *not* done, so use with care. */ @@ -93,9 +99,6 @@ PyAPI_FUNC(PyObject *) _PyFunction_Vectorcall( #define PyFunction_GET_ANNOTATIONS(func) \ (((PyFunctionObject *)func) -> func_annotations) -#define PyFunction_AS_FRAME_CONSTRUCTOR(func) \ - ((PyFrameConstructor *)&((PyFunctionObject *)(func))->func_globals) - /* The classmethod and staticmethod types lives here, too */ PyAPI_DATA(PyTypeObject) PyClassMethod_Type; PyAPI_DATA(PyTypeObject) PyStaticMethod_Type; diff --git a/src/external/windows/include/python/genobject.h b/src/external/windows/include/python/cpython/genobject.h old mode 100755 new mode 100644 similarity index 69% rename from src/external/windows/include/python/genobject.h rename to src/external/windows/include/python/cpython/genobject.h index e6d38d35..8c52b90f --- a/src/external/windows/include/python/genobject.h +++ b/src/external/windows/include/python/cpython/genobject.h @@ -1,4 +1,3 @@ - /* Generator object interface */ #ifndef Py_LIMITED_API @@ -8,24 +7,28 @@ extern "C" { #endif -#include "pystate.h" /* _PyErr_StackItem */ -#include "abstract.h" /* PySendResult */ +/* --- Generators --------------------------------------------------------- */ /* _PyGenObject_HEAD defines the initial segment of generator and coroutine objects. */ #define _PyGenObject_HEAD(prefix) \ PyObject_HEAD \ - /* Note: gi_frame can be NULL if the generator is "finished" */ \ - PyFrameObject *prefix##_frame; \ /* The code object backing the generator */ \ - PyObject *prefix##_code; \ + PyCodeObject *prefix##_code; \ /* List of weak reference. */ \ PyObject *prefix##_weakreflist; \ /* Name of the generator. */ \ PyObject *prefix##_name; \ /* Qualified name of the generator. */ \ PyObject *prefix##_qualname; \ - _PyErr_StackItem prefix##_exc_state; + _PyErr_StackItem prefix##_exc_state; \ + PyObject *prefix##_origin_or_finalizer; \ + char prefix##_hooks_inited; \ + char prefix##_closed; \ + char prefix##_running_async; \ + /* The frame */ \ + int8_t prefix##_frame_state; \ + PyObject *prefix##_iframe[1]; typedef struct { /* The gi_ prefix is intended to remind of generator-iterator. */ @@ -42,39 +45,27 @@ PyAPI_FUNC(PyObject *) PyGen_NewWithQualName(PyFrameObject *, PyObject *name, PyObject *qualname); PyAPI_FUNC(int) _PyGen_SetStopIterationValue(PyObject *); PyAPI_FUNC(int) _PyGen_FetchStopIterationValue(PyObject **); -PyObject *_PyGen_yf(PyGenObject *); PyAPI_FUNC(void) _PyGen_Finalize(PyObject *self); -#ifndef Py_LIMITED_API + +/* --- PyCoroObject ------------------------------------------------------- */ + typedef struct { _PyGenObject_HEAD(cr) - PyObject *cr_origin; } PyCoroObject; PyAPI_DATA(PyTypeObject) PyCoro_Type; PyAPI_DATA(PyTypeObject) _PyCoroWrapper_Type; #define PyCoro_CheckExact(op) Py_IS_TYPE(op, &PyCoro_Type) -PyObject *_PyCoro_GetAwaitableIter(PyObject *o); PyAPI_FUNC(PyObject *) PyCoro_New(PyFrameObject *, PyObject *name, PyObject *qualname); -/* Asynchronous Generators */ + +/* --- Asynchronous Generators -------------------------------------------- */ typedef struct { _PyGenObject_HEAD(ag) - PyObject *ag_finalizer; - - /* Flag is set to 1 when hooks set up by sys.set_asyncgen_hooks - were called on the generator, to avoid calling them more - than once. */ - int ag_hooks_inited; - - /* Flag is set to 1 when aclose() is called for the first time, or - when a StopAsyncIteration exception is raised. */ - int ag_closed; - - int ag_running_async; } PyAsyncGenObject; PyAPI_DATA(PyTypeObject) PyAsyncGen_Type; @@ -87,9 +78,6 @@ PyAPI_FUNC(PyObject *) PyAsyncGen_New(PyFrameObject *, #define PyAsyncGen_CheckExact(op) Py_IS_TYPE(op, &PyAsyncGen_Type) -PyObject *_PyAsyncGenValueWrapperNew(PyObject *); - -#endif #undef _PyGenObject_HEAD diff --git a/src/external/windows/include/python/cpython/import.h b/src/external/windows/include/python/cpython/import.h index 49d89473..31e1f17b 100755 --- a/src/external/windows/include/python/cpython/import.h +++ b/src/external/windows/include/python/cpython/import.h @@ -6,16 +6,13 @@ PyMODINIT_FUNC PyInit__imp(void); PyAPI_FUNC(int) _PyImport_IsInitialized(PyInterpreterState *); -PyAPI_FUNC(PyObject *) _PyImport_GetModuleId(struct _Py_Identifier *name); +PyAPI_FUNC(PyObject *) _PyImport_GetModuleId(_Py_Identifier *name); PyAPI_FUNC(int) _PyImport_SetModule(PyObject *name, PyObject *module); PyAPI_FUNC(int) _PyImport_SetModuleString(const char *name, PyObject* module); PyAPI_FUNC(void) _PyImport_AcquireLock(void); PyAPI_FUNC(int) _PyImport_ReleaseLock(void); -/* Obsolete since 3.5, will be removed in 3.11. */ -Py_DEPRECATED(3.10) PyAPI_FUNC(PyObject *) _PyImport_FindExtensionObject(PyObject *, PyObject *); - PyAPI_FUNC(int) _PyImport_FixupBuiltin( PyObject *mod, const char *name, /* UTF-8 encoded string */ @@ -35,9 +32,14 @@ struct _frozen { const char *name; /* ASCII encoded string */ const unsigned char *code; int size; + int is_package; + PyObject *(*get_code)(void); }; /* Embedding apps may change this pointer to point to their favorite collection of frozen modules: */ PyAPI_DATA(const struct _frozen *) PyImport_FrozenModules; + +PyAPI_DATA(PyObject *) _PyImport_GetModuleAttr(PyObject *, PyObject *); +PyAPI_DATA(PyObject *) _PyImport_GetModuleAttrString(const char *, const char *); diff --git a/src/external/windows/include/python/cpython/initconfig.h b/src/external/windows/include/python/cpython/initconfig.h index 06c12ca1..bec872bf 100755 --- a/src/external/windows/include/python/cpython/initconfig.h +++ b/src/external/windows/include/python/cpython/initconfig.h @@ -143,8 +143,10 @@ typedef struct PyConfig { int faulthandler; int tracemalloc; int import_time; + int code_debug_ranges; int show_ref_count; int dump_refs; + wchar_t *dump_refs_file; int malloc_stats; wchar_t *filesystem_encoding; wchar_t *filesystem_errors; @@ -173,6 +175,8 @@ typedef struct PyConfig { int legacy_windows_stdio; #endif wchar_t *check_hash_pycs_mode; + int use_frozen_modules; + int safe_path; /* --- Path configuration inputs ------------ */ int pathconfig_warnings; @@ -184,6 +188,7 @@ typedef struct PyConfig { /* --- Path configuration outputs ----------- */ int module_search_paths_set; PyWideStringList module_search_paths; + wchar_t *stdlib_dir; wchar_t *executable; wchar_t *base_executable; wchar_t *prefix; @@ -209,6 +214,9 @@ typedef struct PyConfig { // If non-zero, disallow threads, subprocesses, and fork. // Default: 0. int _isolated_interpreter; + + // If non-zero, we believe we're running from a source tree. + int _is_python_build; } PyConfig; PyAPI_FUNC(void) PyConfig_InitPythonConfig(PyConfig *config); diff --git a/src/external/windows/include/python/cpython/interpreteridobject.h b/src/external/windows/include/python/cpython/interpreteridobject.h deleted file mode 100755 index 8506ce01..00000000 --- a/src/external/windows/include/python/cpython/interpreteridobject.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef Py_CPYTHON_INTERPRETERIDOBJECT_H -# error "this header file must not be included directly" -#endif - -/* Interpreter ID Object */ - -PyAPI_DATA(PyTypeObject) _PyInterpreterID_Type; - -PyAPI_FUNC(PyObject *) _PyInterpreterID_New(int64_t); -PyAPI_FUNC(PyObject *) _PyInterpreterState_GetIDObject(PyInterpreterState *); -PyAPI_FUNC(PyInterpreterState *) _PyInterpreterID_LookUp(PyObject *); diff --git a/src/external/windows/include/python/cpython/listobject.h b/src/external/windows/include/python/cpython/listobject.h index ec1441b5..14681c0e 100755 --- a/src/external/windows/include/python/cpython/listobject.h +++ b/src/external/windows/include/python/cpython/listobject.h @@ -24,11 +24,28 @@ typedef struct { PyAPI_FUNC(PyObject *) _PyList_Extend(PyListObject *, PyObject *); PyAPI_FUNC(void) _PyList_DebugMallocStats(FILE *out); -/* Macro, trading safety for speed */ - /* Cast argument to PyListObject* type. */ -#define _PyList_CAST(op) (assert(PyList_Check(op)), (PyListObject *)(op)) +#define _PyList_CAST(op) \ + (assert(PyList_Check(op)), _Py_CAST(PyListObject*, (op))) -#define PyList_GET_ITEM(op, i) (_PyList_CAST(op)->ob_item[i]) -#define PyList_SET_ITEM(op, i, v) ((void)(_PyList_CAST(op)->ob_item[i] = (v))) -#define PyList_GET_SIZE(op) Py_SIZE(_PyList_CAST(op)) +// Macros and static inline functions, trading safety for speed + +static inline Py_ssize_t PyList_GET_SIZE(PyObject *op) { + PyListObject *list = _PyList_CAST(op); + return Py_SIZE(list); +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyList_GET_SIZE(op) PyList_GET_SIZE(_PyObject_CAST(op)) +#endif + +#define PyList_GET_ITEM(op, index) (_PyList_CAST(op)->ob_item[index]) + +static inline void +PyList_SET_ITEM(PyObject *op, Py_ssize_t index, PyObject *value) { + PyListObject *list = _PyList_CAST(op); + list->ob_item[index] = value; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +#define PyList_SET_ITEM(op, index, value) \ + PyList_SET_ITEM(_PyObject_CAST(op), index, _PyObject_CAST(value)) +#endif diff --git a/src/external/windows/include/python/longintrepr.h b/src/external/windows/include/python/cpython/longintrepr.h old mode 100755 new mode 100644 similarity index 92% rename from src/external/windows/include/python/longintrepr.h rename to src/external/windows/include/python/cpython/longintrepr.h index cc02f2b3..e89fdfa3 --- a/src/external/windows/include/python/longintrepr.h +++ b/src/external/windows/include/python/cpython/longintrepr.h @@ -21,8 +21,6 @@ extern "C" { PyLong_SHIFT. The majority of the code doesn't care about the precise value of PyLong_SHIFT, but there are some notable exceptions: - - long_pow() requires that PyLong_SHIFT be divisible by 5 - - PyLong_{As,From}ByteArray require that PyLong_SHIFT be at least 8 - long_hash() requires that PyLong_SHIFT is *strictly* less than the number @@ -63,10 +61,6 @@ typedef long stwodigits; /* signed variant of twodigits */ #define PyLong_BASE ((digit)1 << PyLong_SHIFT) #define PyLong_MASK ((digit)(PyLong_BASE - 1)) -#if PyLong_SHIFT % 5 != 0 -#error "longobject.c requires that PyLong_SHIFT be divisible by 5" -#endif - /* Long integer representation. The absolute value of a number is equal to SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2**(SHIFT*i) @@ -77,6 +71,9 @@ typedef long stwodigits; /* signed variant of twodigits */ 0 <= ob_digit[i] <= MASK. The allocation function takes care of allocating extra memory so that ob_digit[0] ... ob_digit[abs(ob_size)-1] are actually available. + We always allocate memory for at least one digit, so accessing ob_digit[0] + is always safe. However, in the case ob_size == 0, the contents of + ob_digit[0] may be undefined. CAUTION: Generic code manipulating subtypes of PyVarObject has to aware that ints abuse ob_size's sign bit. diff --git a/src/external/windows/include/python/cpython/longobject.h b/src/external/windows/include/python/cpython/longobject.h new file mode 100644 index 00000000..d5239a29 --- /dev/null +++ b/src/external/windows/include/python/cpython/longobject.h @@ -0,0 +1,95 @@ +#ifndef Py_CPYTHON_LONGOBJECT_H +# error "this header file must not be included directly" +#endif + +PyAPI_FUNC(int) _PyLong_AsInt(PyObject *); + +PyAPI_FUNC(int) _PyLong_UnsignedShort_Converter(PyObject *, void *); +PyAPI_FUNC(int) _PyLong_UnsignedInt_Converter(PyObject *, void *); +PyAPI_FUNC(int) _PyLong_UnsignedLong_Converter(PyObject *, void *); +PyAPI_FUNC(int) _PyLong_UnsignedLongLong_Converter(PyObject *, void *); +PyAPI_FUNC(int) _PyLong_Size_t_Converter(PyObject *, void *); + +/* _PyLong_Frexp returns a double x and an exponent e such that the + true value is approximately equal to x * 2**e. e is >= 0. x is + 0.0 if and only if the input is 0 (in which case, e and x are both + zeroes); otherwise, 0.5 <= abs(x) < 1.0. On overflow, which is + possible if the number of bits doesn't fit into a Py_ssize_t, sets + OverflowError and returns -1.0 for x, 0 for e. */ +PyAPI_FUNC(double) _PyLong_Frexp(PyLongObject *a, Py_ssize_t *e); + +PyAPI_FUNC(PyObject *) PyLong_FromUnicodeObject(PyObject *u, int base); +PyAPI_FUNC(PyObject *) _PyLong_FromBytes(const char *, Py_ssize_t, int); + +/* _PyLong_Sign. Return 0 if v is 0, -1 if v < 0, +1 if v > 0. + v must not be NULL, and must be a normalized long. + There are no error cases. +*/ +PyAPI_FUNC(int) _PyLong_Sign(PyObject *v); + +/* _PyLong_NumBits. Return the number of bits needed to represent the + absolute value of a long. For example, this returns 1 for 1 and -1, 2 + for 2 and -2, and 2 for 3 and -3. It returns 0 for 0. + v must not be NULL, and must be a normalized long. + (size_t)-1 is returned and OverflowError set if the true result doesn't + fit in a size_t. +*/ +PyAPI_FUNC(size_t) _PyLong_NumBits(PyObject *v); + +/* _PyLong_DivmodNear. Given integers a and b, compute the nearest + integer q to the exact quotient a / b, rounding to the nearest even integer + in the case of a tie. Return (q, r), where r = a - q*b. The remainder r + will satisfy abs(r) <= abs(b)/2, with equality possible only if q is + even. +*/ +PyAPI_FUNC(PyObject *) _PyLong_DivmodNear(PyObject *, PyObject *); + +/* _PyLong_FromByteArray: View the n unsigned bytes as a binary integer in + base 256, and return a Python int with the same numeric value. + If n is 0, the integer is 0. Else: + If little_endian is 1/true, bytes[n-1] is the MSB and bytes[0] the LSB; + else (little_endian is 0/false) bytes[0] is the MSB and bytes[n-1] the + LSB. + If is_signed is 0/false, view the bytes as a non-negative integer. + If is_signed is 1/true, view the bytes as a 2's-complement integer, + non-negative if bit 0x80 of the MSB is clear, negative if set. + Error returns: + + Return NULL with the appropriate exception set if there's not + enough memory to create the Python int. +*/ +PyAPI_FUNC(PyObject *) _PyLong_FromByteArray( + const unsigned char* bytes, size_t n, + int little_endian, int is_signed); + +/* _PyLong_AsByteArray: Convert the least-significant 8*n bits of long + v to a base-256 integer, stored in array bytes. Normally return 0, + return -1 on error. + If little_endian is 1/true, store the MSB at bytes[n-1] and the LSB at + bytes[0]; else (little_endian is 0/false) store the MSB at bytes[0] and + the LSB at bytes[n-1]. + If is_signed is 0/false, it's an error if v < 0; else (v >= 0) n bytes + are filled and there's nothing special about bit 0x80 of the MSB. + If is_signed is 1/true, bytes is filled with the 2's-complement + representation of v's value. Bit 0x80 of the MSB is the sign bit. + Error returns (-1): + + is_signed is 0 and v < 0. TypeError is set in this case, and bytes + isn't altered. + + n isn't big enough to hold the full mathematical value of v. For + example, if is_signed is 0 and there are more digits in the v than + fit in n; or if is_signed is 1, v < 0, and n is just 1 bit shy of + being large enough to hold a sign bit. OverflowError is set in this + case, but bytes holds the least-significant n bytes of the true value. +*/ +PyAPI_FUNC(int) _PyLong_AsByteArray(PyLongObject* v, + unsigned char* bytes, size_t n, + int little_endian, int is_signed); + +/* _PyLong_Format: Convert the long to a string object with given base, + appending a base prefix of 0[box] if base is 2, 8 or 16. */ +PyAPI_FUNC(PyObject *) _PyLong_Format(PyObject *obj, int base); + +/* For use by the gcd function in mathmodule.c */ +PyAPI_FUNC(PyObject *) _PyLong_GCD(PyObject *, PyObject *); + +PyAPI_FUNC(PyObject *) _PyLong_Rshift(PyObject *, size_t); +PyAPI_FUNC(PyObject *) _PyLong_Lshift(PyObject *, size_t); diff --git a/src/external/windows/include/python/cpython/methodobject.h b/src/external/windows/include/python/cpython/methodobject.h index a2c4857e..da902542 100755 --- a/src/external/windows/include/python/cpython/methodobject.h +++ b/src/external/windows/include/python/cpython/methodobject.h @@ -2,23 +2,7 @@ # error "this header file must not be included directly" #endif -PyAPI_DATA(PyTypeObject) PyCMethod_Type; - -#define PyCMethod_CheckExact(op) Py_IS_TYPE(op, &PyCMethod_Type) -#define PyCMethod_Check(op) PyObject_TypeCheck(op, &PyCMethod_Type) - -/* Macros for direct access to these values. Type checks are *not* - done, so use with care. */ -#define PyCFunction_GET_FUNCTION(func) \ - (((PyCFunctionObject *)func) -> m_ml -> ml_meth) -#define PyCFunction_GET_SELF(func) \ - (((PyCFunctionObject *)func) -> m_ml -> ml_flags & METH_STATIC ? \ - NULL : ((PyCFunctionObject *)func) -> m_self) -#define PyCFunction_GET_FLAGS(func) \ - (((PyCFunctionObject *)func) -> m_ml -> ml_flags) -#define PyCFunction_GET_CLASS(func) \ - (((PyCFunctionObject *)func) -> m_ml -> ml_flags & METH_METHOD ? \ - ((PyCMethodObject *)func) -> mm_class : NULL) +// PyCFunctionObject structure typedef struct { PyObject_HEAD @@ -29,7 +13,62 @@ typedef struct { vectorcallfunc vectorcall; } PyCFunctionObject; +#define _PyCFunctionObject_CAST(func) \ + (assert(PyCFunction_Check(func)), \ + _Py_CAST(PyCFunctionObject*, (func))) + + +// PyCMethodObject structure + typedef struct { PyCFunctionObject func; PyTypeObject *mm_class; /* Class that defines this method */ } PyCMethodObject; + +#define _PyCMethodObject_CAST(func) \ + (assert(PyCMethod_Check(func)), \ + _Py_CAST(PyCMethodObject*, (func))) + +PyAPI_DATA(PyTypeObject) PyCMethod_Type; + +#define PyCMethod_CheckExact(op) Py_IS_TYPE(op, &PyCMethod_Type) +#define PyCMethod_Check(op) PyObject_TypeCheck(op, &PyCMethod_Type) + + +/* Static inline functions for direct access to these values. + Type checks are *not* done, so use with care. */ +static inline PyCFunction PyCFunction_GET_FUNCTION(PyObject *func) { + return _PyCFunctionObject_CAST(func)->m_ml->ml_meth; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyCFunction_GET_FUNCTION(func) PyCFunction_GET_FUNCTION(_PyObject_CAST(func)) +#endif + +static inline PyObject* PyCFunction_GET_SELF(PyObject *func_obj) { + PyCFunctionObject *func = _PyCFunctionObject_CAST(func_obj); + if (func->m_ml->ml_flags & METH_STATIC) { + return _Py_NULL; + } + return func->m_self; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyCFunction_GET_SELF(func) PyCFunction_GET_SELF(_PyObject_CAST(func)) +#endif + +static inline int PyCFunction_GET_FLAGS(PyObject *func) { + return _PyCFunctionObject_CAST(func)->m_ml->ml_flags; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyCFunction_GET_FLAGS(func) PyCFunction_GET_FLAGS(_PyObject_CAST(func)) +#endif + +static inline PyTypeObject* PyCFunction_GET_CLASS(PyObject *func_obj) { + PyCFunctionObject *func = _PyCFunctionObject_CAST(func_obj); + if (func->m_ml->ml_flags & METH_METHOD) { + return _PyCMethodObject_CAST(func)->mm_class; + } + return _Py_NULL; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyCFunction_GET_CLASS(func) PyCFunction_GET_CLASS(_PyObject_CAST(func)) +#endif diff --git a/src/external/windows/include/python/cpython/modsupport.h b/src/external/windows/include/python/cpython/modsupport.h new file mode 100644 index 00000000..28ccc9a5 --- /dev/null +++ b/src/external/windows/include/python/cpython/modsupport.h @@ -0,0 +1,107 @@ +#ifndef Py_CPYTHON_MODSUPPORT_H +# error "this header file must not be included directly" +#endif + +/* If PY_SSIZE_T_CLEAN is defined, each functions treats #-specifier + to mean Py_ssize_t */ +#ifdef PY_SSIZE_T_CLEAN +#define _Py_VaBuildStack _Py_VaBuildStack_SizeT +#else +PyAPI_FUNC(PyObject *) _Py_VaBuildValue_SizeT(const char *, va_list); +PyAPI_FUNC(PyObject **) _Py_VaBuildStack_SizeT( + PyObject **small_stack, + Py_ssize_t small_stack_len, + const char *format, + va_list va, + Py_ssize_t *p_nargs); +#endif + +PyAPI_FUNC(int) _PyArg_UnpackStack( + PyObject *const *args, + Py_ssize_t nargs, + const char *name, + Py_ssize_t min, + Py_ssize_t max, + ...); + +PyAPI_FUNC(int) _PyArg_NoKeywords(const char *funcname, PyObject *kwargs); +PyAPI_FUNC(int) _PyArg_NoKwnames(const char *funcname, PyObject *kwnames); +PyAPI_FUNC(int) _PyArg_NoPositional(const char *funcname, PyObject *args); +#define _PyArg_NoKeywords(funcname, kwargs) \ + ((kwargs) == NULL || _PyArg_NoKeywords((funcname), (kwargs))) +#define _PyArg_NoKwnames(funcname, kwnames) \ + ((kwnames) == NULL || _PyArg_NoKwnames((funcname), (kwnames))) +#define _PyArg_NoPositional(funcname, args) \ + ((args) == NULL || _PyArg_NoPositional((funcname), (args))) + +PyAPI_FUNC(void) _PyArg_BadArgument(const char *, const char *, const char *, PyObject *); +PyAPI_FUNC(int) _PyArg_CheckPositional(const char *, Py_ssize_t, + Py_ssize_t, Py_ssize_t); +#define _PyArg_CheckPositional(funcname, nargs, min, max) \ + ((!ANY_VARARGS(max) && (min) <= (nargs) && (nargs) <= (max)) \ + || _PyArg_CheckPositional((funcname), (nargs), (min), (max))) + +PyAPI_FUNC(PyObject **) _Py_VaBuildStack( + PyObject **small_stack, + Py_ssize_t small_stack_len, + const char *format, + va_list va, + Py_ssize_t *p_nargs); + +typedef struct _PyArg_Parser { + const char *format; + const char * const *keywords; + const char *fname; + const char *custom_msg; + int pos; /* number of positional-only arguments */ + int min; /* minimal number of arguments */ + int max; /* maximal number of positional arguments */ + PyObject *kwtuple; /* tuple of keyword parameter names */ + struct _PyArg_Parser *next; +} _PyArg_Parser; + +#ifdef PY_SSIZE_T_CLEAN +#define _PyArg_ParseTupleAndKeywordsFast _PyArg_ParseTupleAndKeywordsFast_SizeT +#define _PyArg_ParseStack _PyArg_ParseStack_SizeT +#define _PyArg_ParseStackAndKeywords _PyArg_ParseStackAndKeywords_SizeT +#define _PyArg_VaParseTupleAndKeywordsFast _PyArg_VaParseTupleAndKeywordsFast_SizeT +#endif + +PyAPI_FUNC(int) _PyArg_ParseTupleAndKeywordsFast(PyObject *, PyObject *, + struct _PyArg_Parser *, ...); +PyAPI_FUNC(int) _PyArg_ParseStack( + PyObject *const *args, + Py_ssize_t nargs, + const char *format, + ...); +PyAPI_FUNC(int) _PyArg_ParseStackAndKeywords( + PyObject *const *args, + Py_ssize_t nargs, + PyObject *kwnames, + struct _PyArg_Parser *, + ...); +PyAPI_FUNC(int) _PyArg_VaParseTupleAndKeywordsFast(PyObject *, PyObject *, + struct _PyArg_Parser *, va_list); +PyAPI_FUNC(PyObject * const *) _PyArg_UnpackKeywords( + PyObject *const *args, Py_ssize_t nargs, + PyObject *kwargs, PyObject *kwnames, + struct _PyArg_Parser *parser, + int minpos, int maxpos, int minkw, + PyObject **buf); + +PyAPI_FUNC(PyObject * const *) _PyArg_UnpackKeywordsWithVararg( + PyObject *const *args, Py_ssize_t nargs, + PyObject *kwargs, PyObject *kwnames, + struct _PyArg_Parser *parser, + int minpos, int maxpos, int minkw, + int vararg, PyObject **buf); + +#define _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, parser, minpos, maxpos, minkw, buf) \ + (((minkw) == 0 && (kwargs) == NULL && (kwnames) == NULL && \ + (minpos) <= (nargs) && (nargs) <= (maxpos) && args != NULL) ? (args) : \ + _PyArg_UnpackKeywords((args), (nargs), (kwargs), (kwnames), (parser), \ + (minpos), (maxpos), (minkw), (buf))) + +PyAPI_FUNC(PyObject *) _PyModule_CreateInitialized(PyModuleDef*, int apiver); + +PyAPI_DATA(const char *) _Py_PackageContext; diff --git a/src/external/windows/include/python/cpython/object.h b/src/external/windows/include/python/cpython/object.h index 6e26be45..eb48ae62 100755 --- a/src/external/windows/include/python/cpython/object.h +++ b/src/external/windows/include/python/cpython/object.h @@ -41,27 +41,17 @@ typedef struct _Py_Identifier { Py_ssize_t index; } _Py_Identifier; +#if defined(NEEDS_PY_IDENTIFIER) || !defined(Py_BUILD_CORE) +// For now we are keeping _Py_IDENTIFIER for continued use +// in non-builtin extensions (and naughty PyPI modules). + // ericf tweak: (visual studio 2019 chokes on this) #define _Py_static_string_init(value) {value, -1 } // #define _Py_static_string_init(value) { .string = value, .index = -1 } #define _Py_static_string(varname, value) static _Py_Identifier varname = _Py_static_string_init(value) #define _Py_IDENTIFIER(varname) _Py_static_string(PyId_##varname, #varname) -/* buffer interface */ -typedef struct bufferinfo { - void *buf; - PyObject *obj; /* owned reference */ - Py_ssize_t len; - Py_ssize_t itemsize; /* This is Py_ssize_t so it can be - pointed to by strides in simple case.*/ - int readonly; - int ndim; - char *format; - Py_ssize_t *shape; - Py_ssize_t *strides; - Py_ssize_t *suboffsets; - void *internal; -} Py_buffer; +#endif /* NEEDS_PY_IDENTIFIER */ typedef int (*getbufferproc)(PyObject *, Py_buffer *, int); typedef void (*releasebufferproc)(PyObject *, Py_buffer *); @@ -69,39 +59,6 @@ typedef void (*releasebufferproc)(PyObject *, Py_buffer *); typedef PyObject *(*vectorcallfunc)(PyObject *callable, PyObject *const *args, size_t nargsf, PyObject *kwnames); -/* Maximum number of dimensions */ -#define PyBUF_MAX_NDIM 64 - -/* Flags for getting buffers */ -#define PyBUF_SIMPLE 0 -#define PyBUF_WRITABLE 0x0001 -/* we used to include an E, backwards compatible alias */ -#define PyBUF_WRITEABLE PyBUF_WRITABLE -#define PyBUF_FORMAT 0x0004 -#define PyBUF_ND 0x0008 -#define PyBUF_STRIDES (0x0010 | PyBUF_ND) -#define PyBUF_C_CONTIGUOUS (0x0020 | PyBUF_STRIDES) -#define PyBUF_F_CONTIGUOUS (0x0040 | PyBUF_STRIDES) -#define PyBUF_ANY_CONTIGUOUS (0x0080 | PyBUF_STRIDES) -#define PyBUF_INDIRECT (0x0100 | PyBUF_STRIDES) - -#define PyBUF_CONTIG (PyBUF_ND | PyBUF_WRITABLE) -#define PyBUF_CONTIG_RO (PyBUF_ND) - -#define PyBUF_STRIDED (PyBUF_STRIDES | PyBUF_WRITABLE) -#define PyBUF_STRIDED_RO (PyBUF_STRIDES) - -#define PyBUF_RECORDS (PyBUF_STRIDES | PyBUF_WRITABLE | PyBUF_FORMAT) -#define PyBUF_RECORDS_RO (PyBUF_STRIDES | PyBUF_FORMAT) - -#define PyBUF_FULL (PyBUF_INDIRECT | PyBUF_WRITABLE | PyBUF_FORMAT) -#define PyBUF_FULL_RO (PyBUF_INDIRECT | PyBUF_FORMAT) - - -#define PyBUF_READ 0x100 -#define PyBUF_WRITE 0x200 -/* End buffer interface */ - typedef struct { /* Number implementations must check *both* @@ -246,11 +203,11 @@ struct _typeobject { iternextfunc tp_iternext; /* Attribute descriptor and subclassing stuff */ - struct PyMethodDef *tp_methods; - struct PyMemberDef *tp_members; - struct PyGetSetDef *tp_getset; + PyMethodDef *tp_methods; + PyMemberDef *tp_members; + PyGetSetDef *tp_getset; // Strong reference on a heap type, borrowed reference on a static type - struct _typeobject *tp_base; + PyTypeObject *tp_base; PyObject *tp_dict; descrgetfunc tp_descr_get; descrsetfunc tp_descr_set; @@ -274,6 +231,13 @@ struct _typeobject { vectorcallfunc tp_vectorcall; }; +/* This struct is used by the specializer + * It should should be treated as an opaque blob + * by code other than the specializer and interpreter. */ +struct _specialization_cache { + PyObject *getitem; +}; + /* The *real* layout of a type object when allocated on the heap */ typedef struct _heaptypeobject { /* Note: there's a dependency on the order of these members @@ -291,32 +255,33 @@ typedef struct _heaptypeobject { PyObject *ht_name, *ht_slots, *ht_qualname; struct _dictkeysobject *ht_cached_keys; PyObject *ht_module; + char *_ht_tpname; // Storage for "tp_name"; see PyType_FromModuleAndSpec + struct _specialization_cache _spec_cache; // For use by the specializer. /* here are optional user slots, followed by the members. */ } PyHeapTypeObject; -/* access macro to the members which are floating "behind" the object */ -#define PyHeapType_GET_MEMBERS(etype) \ - ((PyMemberDef *)(((char *)etype) + Py_TYPE(etype)->tp_basicsize)) - PyAPI_FUNC(const char *) _PyType_Name(PyTypeObject *); PyAPI_FUNC(PyObject *) _PyType_Lookup(PyTypeObject *, PyObject *); PyAPI_FUNC(PyObject *) _PyType_LookupId(PyTypeObject *, _Py_Identifier *); -PyAPI_FUNC(PyObject *) _PyObject_LookupSpecial(PyObject *, _Py_Identifier *); +PyAPI_FUNC(PyObject *) _PyObject_LookupSpecialId(PyObject *, _Py_Identifier *); +#ifndef Py_BUILD_CORE +// Backward compatibility for 3rd-party extensions +// that may be using the old name. +#define _PyObject_LookupSpecial _PyObject_LookupSpecialId +#endif PyAPI_FUNC(PyTypeObject *) _PyType_CalculateMetaclass(PyTypeObject *, PyObject *); PyAPI_FUNC(PyObject *) _PyType_GetDocFromInternalDoc(const char *, const char *); PyAPI_FUNC(PyObject *) _PyType_GetTextSignatureFromInternalDoc(const char *, const char *); -struct PyModuleDef; -PyAPI_FUNC(PyObject *) _PyType_GetModuleByDef(PyTypeObject *, struct PyModuleDef *); +PyAPI_FUNC(PyObject *) PyType_GetModuleByDef(PyTypeObject *, PyModuleDef *); -struct _Py_Identifier; PyAPI_FUNC(int) PyObject_Print(PyObject *, FILE *, int); PyAPI_FUNC(void) _Py_BreakPoint(void); PyAPI_FUNC(void) _PyObject_Dump(PyObject *); PyAPI_FUNC(int) _PyObject_IsFreed(PyObject *); PyAPI_FUNC(int) _PyObject_IsAbstract(PyObject *); -PyAPI_FUNC(PyObject *) _PyObject_GetAttrId(PyObject *, struct _Py_Identifier *); -PyAPI_FUNC(int) _PyObject_SetAttrId(PyObject *, struct _Py_Identifier *, PyObject *); +PyAPI_FUNC(PyObject *) _PyObject_GetAttrId(PyObject *, _Py_Identifier *); +PyAPI_FUNC(int) _PyObject_SetAttrId(PyObject *, _Py_Identifier *, PyObject *); /* Replacements of PyObject_GetAttr() and _PyObject_GetAttrId() which don't raise AttributeError. @@ -327,7 +292,7 @@ PyAPI_FUNC(int) _PyObject_SetAttrId(PyObject *, struct _Py_Identifier *, PyObjec is raised. */ PyAPI_FUNC(int) _PyObject_LookupAttr(PyObject *, PyObject *, PyObject **); -PyAPI_FUNC(int) _PyObject_LookupAttrId(PyObject *, struct _Py_Identifier *, PyObject **); +PyAPI_FUNC(int) _PyObject_LookupAttrId(PyObject *, _Py_Identifier *, PyObject **); PyAPI_FUNC(int) _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method); @@ -495,8 +460,8 @@ without deallocating anything (and so unbounded call-stack depth is avoided). When the call stack finishes unwinding again, code generated by the END macro notices this, and calls another routine to deallocate all the objects that may have been added to the list of deferred deallocations. In effect, a -chain of N deallocations is broken into (N-1)/(PyTrash_UNWIND_LEVEL-1) pieces, -with the call stack never exceeding a depth of PyTrash_UNWIND_LEVEL. +chain of N deallocations is broken into (N-1)/(_PyTrash_UNWIND_LEVEL-1) pieces, +with the call stack never exceeding a depth of _PyTrash_UNWIND_LEVEL. Since the tp_dealloc of a subclass typically calls the tp_dealloc of the base class, we need to ensure that the trashcan is only triggered on the tp_dealloc @@ -505,27 +470,12 @@ partially-deallocated object. To check this, the tp_dealloc function must be passed as second argument to Py_TRASHCAN_BEGIN(). */ -/* This is the old private API, invoked by the macros before 3.2.4. - Kept for binary compatibility of extensions using the stable ABI. */ -PyAPI_FUNC(void) _PyTrash_deposit_object(PyObject*); -PyAPI_FUNC(void) _PyTrash_destroy_chain(void); - -/* This is the old private API, invoked by the macros before 3.9. - Kept for binary compatibility of extensions using the stable ABI. */ -PyAPI_FUNC(void) _PyTrash_thread_deposit_object(PyObject*); -PyAPI_FUNC(void) _PyTrash_thread_destroy_chain(void); - -/* Forward declarations for PyThreadState */ -struct _ts; - /* Python 3.9 private API, invoked by the macros below. */ -PyAPI_FUNC(int) _PyTrash_begin(struct _ts *tstate, PyObject *op); -PyAPI_FUNC(void) _PyTrash_end(struct _ts *tstate); +PyAPI_FUNC(int) _PyTrash_begin(PyThreadState *tstate, PyObject *op); +PyAPI_FUNC(void) _PyTrash_end(PyThreadState *tstate); /* Python 3.10 private API, invoked by the Py_TRASHCAN_BEGIN(). */ PyAPI_FUNC(int) _PyTrash_cond(PyObject *op, destructor dealloc); -#define PyTrash_UNWIND_LEVEL 50 - #define Py_TRASHCAN_BEGIN_CONDITION(op, cond) \ do { \ PyThreadState *_tstate = NULL; \ @@ -548,7 +498,16 @@ PyAPI_FUNC(int) _PyTrash_cond(PyObject *op, destructor dealloc); Py_TRASHCAN_BEGIN_CONDITION(op, \ _PyTrash_cond(_PyObject_CAST(op), (destructor)dealloc)) -/* For backwards compatibility, these macros enable the trashcan - * unconditionally */ -#define Py_TRASHCAN_SAFE_BEGIN(op) Py_TRASHCAN_BEGIN_CONDITION(op, 1) -#define Py_TRASHCAN_SAFE_END(op) Py_TRASHCAN_END +/* The following two macros, Py_TRASHCAN_SAFE_BEGIN and + * Py_TRASHCAN_SAFE_END, are deprecated since version 3.11 and + * will be removed in the future. + * Use Py_TRASHCAN_BEGIN and Py_TRASHCAN_END instead. + */ +Py_DEPRECATED(3.11) typedef int UsingDeprecatedTrashcanMacro; +#define Py_TRASHCAN_SAFE_BEGIN(op) \ + do { \ + UsingDeprecatedTrashcanMacro cond=1; \ + Py_TRASHCAN_BEGIN_CONDITION(op, cond); +#define Py_TRASHCAN_SAFE_END(op) \ + Py_TRASHCAN_END; \ + } while(0); diff --git a/src/external/windows/include/python/cpython/objimpl.h b/src/external/windows/include/python/cpython/objimpl.h index 5e9aa203..a2693329 100755 --- a/src/external/windows/include/python/cpython/objimpl.h +++ b/src/external/windows/include/python/cpython/objimpl.h @@ -52,14 +52,6 @@ the 1st step is performed automatically for you, so in a C++ class constructor you would start directly with PyObject_Init/InitVar. */ -/* This function returns the number of allocated memory blocks, regardless of size */ -PyAPI_FUNC(Py_ssize_t) _Py_GetAllocatedBlocks(void); - -/* Macros */ -#ifdef WITH_PYMALLOC -PyAPI_FUNC(int) _PyObject_DebugMallocStats(FILE *out); -#endif - typedef struct { /* user context passed as the first argument to the 2 functions */ @@ -90,11 +82,8 @@ PyAPI_FUNC(int) PyObject_IS_GC(PyObject *obj); # define _PyGC_FINALIZED(o) PyObject_GC_IsFinalized(o) #endif -PyAPI_FUNC(PyObject *) _PyObject_GC_Malloc(size_t size); -PyAPI_FUNC(PyObject *) _PyObject_GC_Calloc(size_t size); - -/* Test if a type supports weak references */ -#define PyType_SUPPORTS_WEAKREFS(t) ((t)->tp_weaklistoffset > 0) +// Test if a type supports weak references +PyAPI_FUNC(int) PyType_SUPPORTS_WEAKREFS(PyTypeObject *type); PyAPI_FUNC(PyObject **) PyObject_GET_WEAKREFS_LISTPTR(PyObject *op); diff --git a/src/external/windows/include/python/cpython/pthread_stubs.h b/src/external/windows/include/python/cpython/pthread_stubs.h new file mode 100644 index 00000000..3509f1f6 --- /dev/null +++ b/src/external/windows/include/python/cpython/pthread_stubs.h @@ -0,0 +1,88 @@ +#ifndef Py_CPYTHON_PTRHEAD_STUBS_H +#define Py_CPYTHON_PTRHEAD_STUBS_H + +#if !defined(HAVE_PTHREAD_STUBS) +# error "this header file requires stubbed pthreads." +#endif + +#ifndef _POSIX_THREADS +# define _POSIX_THREADS 1 +#endif + +/* Minimal pthread stubs for CPython. + * + * The stubs implement the minimum pthread API for CPython. + * - pthread_create() fails. + * - pthread_exit() calls exit(0). + * - pthread_key_*() functions implement minimal TSS without destructor. + * - all other functions do nothing and return 0. + */ + +#ifdef __wasi__ +// WASI's bits/alltypes.h provides type definitions when __NEED_ is set. +// The header file can be included multiple times. +# define __NEED_pthread_cond_t 1 +# define __NEED_pthread_condattr_t 1 +# define __NEED_pthread_mutex_t 1 +# define __NEED_pthread_mutexattr_t 1 +# define __NEED_pthread_key_t 1 +# define __NEED_pthread_t 1 +# define __NEED_pthread_attr_t 1 +# include +#else +typedef struct { void *__x; } pthread_cond_t; +typedef struct { unsigned __attr; } pthread_condattr_t; +typedef struct { void *__x; } pthread_mutex_t; +typedef struct { unsigned __attr; } pthread_mutexattr_t; +typedef unsigned pthread_key_t; +typedef unsigned pthread_t; +typedef struct { unsigned __attr; } pthread_attr_t; +#endif + +// mutex +PyAPI_FUNC(int) pthread_mutex_init(pthread_mutex_t *restrict mutex, + const pthread_mutexattr_t *restrict attr); +PyAPI_FUNC(int) pthread_mutex_destroy(pthread_mutex_t *mutex); +PyAPI_FUNC(int) pthread_mutex_trylock(pthread_mutex_t *mutex); +PyAPI_FUNC(int) pthread_mutex_lock(pthread_mutex_t *mutex); +PyAPI_FUNC(int) pthread_mutex_unlock(pthread_mutex_t *mutex); + +// condition +PyAPI_FUNC(int) pthread_cond_init(pthread_cond_t *restrict cond, + const pthread_condattr_t *restrict attr); +PyAPI_FUNC(int) pthread_cond_destroy(pthread_cond_t *cond); +PyAPI_FUNC(int) pthread_cond_wait(pthread_cond_t *restrict cond, + pthread_mutex_t *restrict mutex); +PyAPI_FUNC(int) pthread_cond_timedwait(pthread_cond_t *restrict cond, + pthread_mutex_t *restrict mutex, + const struct timespec *restrict abstime); +PyAPI_FUNC(int) pthread_cond_signal(pthread_cond_t *cond); +PyAPI_FUNC(int) pthread_condattr_init(pthread_condattr_t *attr); +PyAPI_FUNC(int) pthread_condattr_setclock( + pthread_condattr_t *attr, clockid_t clock_id); + +// pthread +PyAPI_FUNC(int) pthread_create(pthread_t *restrict thread, + const pthread_attr_t *restrict attr, + void *(*start_routine)(void *), + void *restrict arg); +PyAPI_FUNC(int) pthread_detach(pthread_t thread); +PyAPI_FUNC(pthread_t) pthread_self(void); +PyAPI_FUNC(int) pthread_exit(void *retval) __attribute__ ((__noreturn__)); +PyAPI_FUNC(int) pthread_attr_init(pthread_attr_t *attr); +PyAPI_FUNC(int) pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); +PyAPI_FUNC(int) pthread_attr_destroy(pthread_attr_t *attr); + + +// pthread_key +#ifndef PTHREAD_KEYS_MAX +# define PTHREAD_KEYS_MAX 128 +#endif + +PyAPI_FUNC(int) pthread_key_create(pthread_key_t *key, + void (*destr_function)(void *)); +PyAPI_FUNC(int) pthread_key_delete(pthread_key_t key); +PyAPI_FUNC(void *) pthread_getspecific(pthread_key_t key); +PyAPI_FUNC(int) pthread_setspecific(pthread_key_t key, const void *value); + +#endif // Py_CPYTHON_PTRHEAD_STUBS_H diff --git a/src/external/windows/include/python/cpython/pydebug.h b/src/external/windows/include/python/cpython/pydebug.h index e34cf907..5b26a6e2 100755 --- a/src/external/windows/include/python/cpython/pydebug.h +++ b/src/external/windows/include/python/cpython/pydebug.h @@ -29,7 +29,7 @@ PyAPI_DATA(int) Py_LegacyWindowsStdioFlag; /* this is a wrapper around getenv() that pays attention to Py_IgnoreEnvironmentFlag. It should be used for getting variables like PYTHONPATH and PYTHONHOME from the environment */ -#define Py_GETENV(s) (Py_IgnoreEnvironmentFlag ? NULL : getenv(s)) +PyAPI_DATA(char*) Py_GETENV(const char *name); #ifdef __cplusplus } diff --git a/src/external/windows/include/python/cpython/pyerrors.h b/src/external/windows/include/python/cpython/pyerrors.h index 1543cfc3..b9c6b328 100755 --- a/src/external/windows/include/python/cpython/pyerrors.h +++ b/src/external/windows/include/python/cpython/pyerrors.h @@ -6,7 +6,7 @@ /* PyException_HEAD defines the initial segment of every exception class. */ #define PyException_HEAD PyObject_HEAD PyObject *dict;\ - PyObject *args; PyObject *traceback;\ + PyObject *args; PyObject *notes; PyObject *traceback;\ PyObject *context; PyObject *cause;\ char suppress_context; @@ -14,6 +14,12 @@ typedef struct { PyException_HEAD } PyBaseExceptionObject; +typedef struct { + PyException_HEAD + PyObject *msg; + PyObject *excs; +} PyBaseExceptionGroupObject; + typedef struct { PyException_HEAD PyObject *msg; @@ -85,20 +91,14 @@ typedef PyOSErrorObject PyWindowsErrorObject; PyAPI_FUNC(void) _PyErr_SetKeyError(PyObject *); PyAPI_FUNC(_PyErr_StackItem*) _PyErr_GetTopmostException(PyThreadState *tstate); +PyAPI_FUNC(PyObject*) _PyErr_GetHandledException(PyThreadState *); +PyAPI_FUNC(void) _PyErr_SetHandledException(PyThreadState *, PyObject *); PyAPI_FUNC(void) _PyErr_GetExcInfo(PyThreadState *, PyObject **, PyObject **, PyObject **); /* Context manipulation (PEP 3134) */ PyAPI_FUNC(void) _PyErr_ChainExceptions(PyObject *, PyObject *, PyObject *); -/* Convenience functions */ - -#ifdef MS_WINDOWS -Py_DEPRECATED(3.3) -PyAPI_FUNC(PyObject *) PyErr_SetFromErrnoWithUnicodeFilename( - PyObject *, const Py_UNICODE *); -#endif /* MS_WINDOWS */ - /* Like PyErr_Format(), but saves current exception as __context__ and __cause__. */ @@ -108,16 +108,6 @@ PyAPI_FUNC(PyObject *) _PyErr_FormatFromCause( ... ); -#ifdef MS_WINDOWS -/* XXX redeclare to use WSTRING */ -Py_DEPRECATED(3.3) -PyAPI_FUNC(PyObject *) PyErr_SetFromWindowsErrWithUnicodeFilename( - int, const Py_UNICODE *); -Py_DEPRECATED(3.3) -PyAPI_FUNC(PyObject *) PyErr_SetExcFromWindowsErrWithUnicodeFilename( - PyObject *,int, const Py_UNICODE *); -#endif - /* In exceptions.c */ /* Helper that attempts to replace the current exception with one of the @@ -161,31 +151,6 @@ PyAPI_FUNC(PyObject *) PyErr_ProgramTextObject( PyObject *filename, int lineno); -/* Create a UnicodeEncodeError object. - * - * TODO: This API will be removed in Python 3.11. - */ -Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject *) PyUnicodeEncodeError_Create( - const char *encoding, /* UTF-8 encoded string */ - const Py_UNICODE *object, - Py_ssize_t length, - Py_ssize_t start, - Py_ssize_t end, - const char *reason /* UTF-8 encoded string */ - ); - -/* Create a UnicodeTranslateError object. - * - * TODO: This API will be removed in Python 3.11. - */ -Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject *) PyUnicodeTranslateError_Create( - const Py_UNICODE *object, - Py_ssize_t length, - Py_ssize_t start, - Py_ssize_t end, - const char *reason /* UTF-8 encoded string */ - ); - PyAPI_FUNC(PyObject *) _PyErr_ProgramDecodedTextObject( PyObject *filename, int lineno, diff --git a/src/external/windows/include/python/cpython/pyframe.h b/src/external/windows/include/python/cpython/pyframe.h new file mode 100644 index 00000000..54100e0b --- /dev/null +++ b/src/external/windows/include/python/cpython/pyframe.h @@ -0,0 +1,17 @@ +#ifndef Py_CPYTHON_PYFRAME_H +# error "this header file must not be included directly" +#endif + +PyAPI_DATA(PyTypeObject) PyFrame_Type; + +#define PyFrame_Check(op) Py_IS_TYPE((op), &PyFrame_Type) + +PyAPI_FUNC(PyFrameObject *) PyFrame_GetBack(PyFrameObject *frame); +PyAPI_FUNC(PyObject *) PyFrame_GetLocals(PyFrameObject *frame); + +PyAPI_FUNC(PyObject *) PyFrame_GetGlobals(PyFrameObject *frame); +PyAPI_FUNC(PyObject *) PyFrame_GetBuiltins(PyFrameObject *frame); + +PyAPI_FUNC(PyObject *) PyFrame_GetGenerator(PyFrameObject *frame); +PyAPI_FUNC(int) PyFrame_GetLasti(PyFrameObject *frame); + diff --git a/src/external/windows/include/python/cpython/pylifecycle.h b/src/external/windows/include/python/cpython/pylifecycle.h index 5993e6dd..139b955c 100755 --- a/src/external/windows/include/python/cpython/pylifecycle.h +++ b/src/external/windows/include/python/cpython/pylifecycle.h @@ -9,8 +9,9 @@ PyAPI_FUNC(int) Py_FrozenMain(int argc, char **argv); /* Only used by applications that embed the interpreter and need to * override the standard encoding determination mechanism */ -PyAPI_FUNC(int) Py_SetStandardStreamEncoding(const char *encoding, - const char *errors); +Py_DEPRECATED(3.11) PyAPI_FUNC(int) Py_SetStandardStreamEncoding( + const char *encoding, + const char *errors); /* PEP 432 Multi-phase initialization API (Private while provisional!) */ @@ -45,7 +46,7 @@ PyAPI_FUNC(void) _Py_RestoreSignals(void); PyAPI_FUNC(int) Py_FdIsInteractive(FILE *, const char *); PyAPI_FUNC(int) _Py_FdIsInteractive(FILE *fp, PyObject *filename); -PyAPI_FUNC(void) _Py_SetProgramFullPath(const wchar_t *); +Py_DEPRECATED(3.11) PyAPI_FUNC(void) _Py_SetProgramFullPath(const wchar_t *); PyAPI_FUNC(const char *) _Py_gitidentifier(void); PyAPI_FUNC(const char *) _Py_gitversion(void); diff --git a/src/external/windows/include/python/cpython/pystate.h b/src/external/windows/include/python/cpython/pystate.h index 06781a16..bd8519b0 100755 --- a/src/external/windows/include/python/cpython/pystate.h +++ b/src/external/windows/include/python/cpython/pystate.h @@ -2,6 +2,7 @@ # error "this header file must not be included directly" #endif + PyAPI_FUNC(int) _PyInterpreterState_RequiresIDRef(PyInterpreterState *); PyAPI_FUNC(void) _PyInterpreterState_RequireIDRef(PyInterpreterState *, int); @@ -27,7 +28,14 @@ typedef int (*Py_tracefunc)(PyObject *, PyFrameObject *, int, PyObject *); #define PyTrace_OPCODE 7 -typedef struct _cframe { +typedef struct { + PyCodeObject *code; // The code object for the bounds. May be NULL. + PyCodeAddressRange bounds; // Only valid if code != NULL. +} PyTraceInfo; + +// Internal structure: you should not use it directly, but use public functions +// like PyThreadState_EnterTracing() and PyThreadState_LeaveTracing(). +typedef struct _PyCFrame { /* This struct will be threaded through the C stack * allowing fast access to per-thread state that needs * to be accessed quickly by the interpreter, but can @@ -38,46 +46,68 @@ typedef struct _cframe { * discipline and make sure that instances of this struct cannot * accessed outside of their lifetime. */ - int use_tracing; - struct _cframe *previous; -} CFrame; + uint8_t use_tracing; // 0 or 255 (or'ed into opcode, hence 8-bit type) + /* Pointer to the currently executing frame (it can be NULL) */ + struct _PyInterpreterFrame *current_frame; + struct _PyCFrame *previous; +} _PyCFrame; typedef struct _err_stackitem { - /* This struct represents an entry on the exception stack, which is a - * per-coroutine state. (Coroutine in the computer science sense, - * including the thread and generators). - * This ensures that the exception state is not impacted by "yields" - * from an except handler. + /* This struct represents a single execution context where we might + * be currently handling an exception. It is a per-coroutine state + * (coroutine in the computer science sense, including the thread + * and generators). + * + * This is used as an entry on the exception stack, where each + * entry indicates if it is currently handling an exception. + * This ensures that the exception state is not impacted + * by "yields" from an except handler. The thread + * always has an entry (the bottom-most one). */ - PyObject *exc_type, *exc_value, *exc_traceback; + + /* The exception currently being handled in this context, if any. */ + PyObject *exc_value; struct _err_stackitem *previous_item; } _PyErr_StackItem; +typedef struct _stack_chunk { + struct _stack_chunk *previous; + size_t size; + size_t top; + PyObject * data[1]; /* Variable sized */ +} _PyStackChunk; -// The PyThreadState typedef is in Include/pystate.h. struct _ts { /* See Python/ceval.c for comments explaining most fields */ - struct _ts *prev; - struct _ts *next; + PyThreadState *prev; + PyThreadState *next; PyInterpreterState *interp; - /* Borrowed reference to the current frame (it can be NULL) */ - PyFrameObject *frame; - int recursion_depth; + /* Has been initialized to a safe state. + + In order to be effective, this must be set to 0 during or right + after allocation. */ + int _initialized; + + /* Was this thread state statically allocated? */ + int _static; + + int recursion_remaining; + int recursion_limit; int recursion_headroom; /* Allow 50 more calls to handle any errors. */ - int stackcheck_counter; /* 'tracing' keeps track of the execution depth when tracing/profiling. This is to prevent the actual trace/profile code from being recorded in the trace/profile. */ int tracing; + int tracing_what; /* The event currently being traced, if any. */ - /* Pointer to current CFrame in the C stack frame of the currently, + /* Pointer to current _PyCFrame in the C stack frame of the currently, * or most recently, executing _PyEval_EvalFrameDefault. */ - CFrame *cframe; + _PyCFrame *cframe; Py_tracefunc c_profilefunc; Py_tracefunc c_tracefunc; @@ -89,13 +119,9 @@ struct _ts { PyObject *curexc_value; PyObject *curexc_traceback; - /* The exception currently being handled, if no coroutines/generators - * are present. Always last element on the stack referred to be exc_info. - */ - _PyErr_StackItem exc_state; - - /* Pointer to the top of the stack of the exceptions currently - * being handled */ + /* Pointer to the top of the exception stack for the exceptions + * we may be currently handling. (See _PyErr_StackItem above.) + * This is never NULL. */ _PyErr_StackItem *exc_info; PyObject *dict; /* Stores per-thread state */ @@ -105,6 +131,12 @@ struct _ts { PyObject *async_exc; /* Asynchronous exception to raise */ unsigned long thread_id; /* Thread id where this tstate was created */ + /* Native thread id where this tstate was created. This will be 0 except on + * those platforms that have the notion of native thread id, for which the + * macro PY_HAVE_THREAD_NATIVE_ID is then defined. + */ + unsigned long native_thread_id; + int trash_delete_nesting; PyObject *trash_delete_later; @@ -145,12 +177,34 @@ struct _ts { /* Unique thread state id. */ uint64_t id; - CFrame root_cframe; + PyTraceInfo trace_info; + _PyStackChunk *datastack_chunk; + PyObject **datastack_top; + PyObject **datastack_limit; /* XXX signal handlers should also be here */ + /* The following fields are here to avoid allocation during init. + The data is exposed through PyThreadState pointer fields. + These fields should not be accessed directly outside of init. + This is indicated by an underscore prefix on the field names. + + All other PyInterpreterState pointer fields are populated when + needed and default to NULL. + */ + // Note some fields do not have a leading underscore for backward + // compatibility. See https://bugs.python.org/issue45953#msg412046. + + /* The thread's exception stack entry. (Always the last entry.) */ + _PyErr_StackItem exc_state; + + /* The bottom-most frame on the stack. */ + _PyCFrame root_cframe; }; + +/* other API */ + // Alias for backward compatibility with Python 3.8 #define _PyInterpreterState_Get PyInterpreterState_Get @@ -162,6 +216,13 @@ PyAPI_FUNC(PyThreadState *) _PyThreadState_UncheckedGet(void); PyAPI_FUNC(PyObject *) _PyThreadState_GetDict(PyThreadState *tstate); +// Disable tracing and profiling. +PyAPI_FUNC(void) PyThreadState_EnterTracing(PyThreadState *tstate); + +// Reset tracing and profiling: enable them if a trace function or a profile +// function is set, otherwise disable them. +PyAPI_FUNC(void) PyThreadState_LeaveTracing(PyThreadState *tstate); + /* PyGILState */ /* Helper/diagnostic function - return 1 if the current thread @@ -200,7 +261,7 @@ PyAPI_FUNC(void) PyThreadState_DeleteCurrent(void); /* Frame evaluation API */ -typedef PyObject* (*_PyFrameEvalFunction)(PyThreadState *tstate, PyFrameObject *, int); +typedef PyObject* (*_PyFrameEvalFunction)(PyThreadState *tstate, struct _PyInterpreterFrame *, int); PyAPI_FUNC(_PyFrameEvalFunction) _PyInterpreterState_GetEvalFrameFunc( PyInterpreterState *interp); @@ -247,12 +308,12 @@ PyAPI_FUNC(const PyConfig*) _Py_GetConfig(void); /* cross-interpreter data */ -struct _xid; - // _PyCrossInterpreterData is similar to Py_buffer as an effectively // opaque struct that holds data outside the object machinery. This // is necessary to pass safely between interpreters in the same process. -typedef struct _xid { +typedef struct _xid _PyCrossInterpreterData; + +struct _xid { // data is the cross-interpreter-safe derivation of a Python object // (see _PyObject_GetCrossInterpreterData). It will be NULL if the // new_object func (below) encodes the data. @@ -278,7 +339,7 @@ typedef struct _xid { // interpreter given the data. The resulting object (a new // reference) will be equivalent to the original object. This field // is required. - PyObject *(*new_object)(struct _xid *); + PyObject *(*new_object)(_PyCrossInterpreterData *); // free is called when the data is released. If it is NULL then // nothing will be done to free the data. For some types this is // okay (e.g. bytes) and for those types this field should be set @@ -289,7 +350,7 @@ typedef struct _xid { // to PyMem_RawFree (the default if not explicitly set to NULL). // The call will happen with the original interpreter activated. void (*free)(void *); -} _PyCrossInterpreterData; +}; PyAPI_FUNC(int) _PyObject_GetCrossInterpreterData(PyObject *, _PyCrossInterpreterData *); PyAPI_FUNC(PyObject *) _PyCrossInterpreterData_NewObject(_PyCrossInterpreterData *); @@ -299,7 +360,7 @@ PyAPI_FUNC(int) _PyObject_CheckCrossInterpreterData(PyObject *); /* cross-interpreter data registry */ -typedef int (*crossinterpdatafunc)(PyObject *, struct _xid *); +typedef int (*crossinterpdatafunc)(PyObject *, _PyCrossInterpreterData *); PyAPI_FUNC(int) _PyCrossInterpreterData_RegisterClass(PyTypeObject *, crossinterpdatafunc); PyAPI_FUNC(crossinterpdatafunc) _PyCrossInterpreterData_Lookup(PyObject *); diff --git a/src/external/windows/include/python/cpython/pythread.h b/src/external/windows/include/python/cpython/pythread.h new file mode 100644 index 00000000..7ebd5345 --- /dev/null +++ b/src/external/windows/include/python/cpython/pythread.h @@ -0,0 +1,42 @@ +#ifndef Py_CPYTHON_PYTHREAD_H +# error "this header file must not be included directly" +#endif + +#define PYTHREAD_INVALID_THREAD_ID ((unsigned long)-1) + +#ifdef HAVE_FORK +/* Private function to reinitialize a lock at fork in the child process. + Reset the lock to the unlocked state. + Return 0 on success, return -1 on error. */ +PyAPI_FUNC(int) _PyThread_at_fork_reinit(PyThread_type_lock *lock); +#endif /* HAVE_FORK */ + +#ifdef HAVE_PTHREAD_H + /* Darwin needs pthread.h to know type name the pthread_key_t. */ +# include +# define NATIVE_TSS_KEY_T pthread_key_t +#elif defined(NT_THREADS) + /* In Windows, native TSS key type is DWORD, + but hardcode the unsigned long to avoid errors for include directive. + */ +# define NATIVE_TSS_KEY_T unsigned long +#elif defined(HAVE_PTHREAD_STUBS) +# include "cpython/pthread_stubs.h" +# define NATIVE_TSS_KEY_T pthread_key_t +#else +# error "Require native threads. See https://bugs.python.org/issue31370" +#endif + +/* When Py_LIMITED_API is not defined, the type layout of Py_tss_t is + exposed to allow static allocation in the API clients. Even in this case, + you must handle TSS keys through API functions due to compatibility. +*/ +struct _Py_tss_t { + int _is_initialized; + NATIVE_TSS_KEY_T _key; +}; + +#undef NATIVE_TSS_KEY_T + +/* When static allocation, you must initialize with Py_tss_NEEDS_INIT. */ +#define Py_tss_NEEDS_INIT {0} diff --git a/src/external/windows/include/python/cpython/pytime.h b/src/external/windows/include/python/cpython/pytime.h index 656fe38f..23e0536a 100755 --- a/src/external/windows/include/python/cpython/pytime.h +++ b/src/external/windows/include/python/cpython/pytime.h @@ -1,3 +1,46 @@ +// The _PyTime_t API is written to use timestamp and timeout values stored in +// various formats and to read clocks. +// +// The _PyTime_t type is an integer to support directly common arithmetic +// operations like t1 + t2. +// +// The _PyTime_t API supports a resolution of 1 nanosecond. The _PyTime_t type +// is signed to support negative timestamps. The supported range is around +// [-292.3 years; +292.3 years]. Using the Unix epoch (January 1st, 1970), the +// supported date range is around [1677-09-21; 2262-04-11]. +// +// Formats: +// +// * seconds +// * seconds as a floating pointer number (C double) +// * milliseconds (10^-3 seconds) +// * microseconds (10^-6 seconds) +// * 100 nanoseconds (10^-7 seconds) +// * nanoseconds (10^-9 seconds) +// * timeval structure, 1 microsecond resolution (10^-6 seconds) +// * timespec structure, 1 nanosecond resolution (10^-9 seconds) +// +// Integer overflows are detected and raise OverflowError. Conversion to a +// resolution worse than 1 nanosecond is rounded correctly with the requested +// rounding mode. There are 4 rounding modes: floor (towards -inf), ceiling +// (towards +inf), half even and up (away from zero). +// +// Some functions clamp the result in the range [_PyTime_MIN; _PyTime_MAX], so +// the caller doesn't have to handle errors and doesn't need to hold the GIL. +// For example, _PyTime_Add(t1, t2) computes t1+t2 and clamp the result on +// overflow. +// +// Clocks: +// +// * System clock +// * Monotonic clock +// * Performance counter +// +// Operations like (t * k / q) with integers are implemented in a way to reduce +// the risk of integer overflow. Such operation is used to convert a clock +// value expressed in ticks with a frequency to _PyTime_t, like +// QueryPerformanceCounter() with QueryPerformanceFrequency(). + #ifndef Py_LIMITED_API #ifndef Py_PYTIME_H #define Py_PYTIME_H @@ -14,8 +57,11 @@ extern "C" { store a duration, and so indirectly a date (related to another date, like UNIX epoch). */ typedef int64_t _PyTime_t; +// _PyTime_MIN nanoseconds is around -292.3 years #define _PyTime_MIN INT64_MIN +// _PyTime_MAX nanoseconds is around +292.3 years #define _PyTime_MAX INT64_MAX +#define _SIZEOF_PYTIME_T 8 typedef enum { /* Round towards minus infinity (-inf). @@ -111,13 +157,24 @@ PyAPI_FUNC(_PyTime_t) _PyTime_AsMilliseconds(_PyTime_t t, PyAPI_FUNC(_PyTime_t) _PyTime_AsMicroseconds(_PyTime_t t, _PyTime_round_t round); +/* Convert timestamp to a number of nanoseconds (10^-9 seconds). */ +PyAPI_FUNC(_PyTime_t) _PyTime_AsNanoseconds(_PyTime_t t); + +#ifdef MS_WINDOWS +// Convert timestamp to a number of 100 nanoseconds (10^-7 seconds). +PyAPI_FUNC(_PyTime_t) _PyTime_As100Nanoseconds(_PyTime_t t, + _PyTime_round_t round); +#endif + /* Convert timestamp to a number of nanoseconds (10^-9 seconds) as a Python int object. */ PyAPI_FUNC(PyObject *) _PyTime_AsNanosecondsObject(_PyTime_t t); +#ifndef MS_WINDOWS /* Create a timestamp from a timeval structure. Raise an exception and return -1 on overflow, return 0 on success. */ PyAPI_FUNC(int) _PyTime_FromTimeval(_PyTime_t *tp, struct timeval *tv); +#endif /* Convert a timestamp to a timeval structure (microsecond resolution). tv_usec is always positive. @@ -127,8 +184,9 @@ PyAPI_FUNC(int) _PyTime_AsTimeval(_PyTime_t t, struct timeval *tv, _PyTime_round_t round); -/* Similar to _PyTime_AsTimeval(), but don't raise an exception on error. */ -PyAPI_FUNC(int) _PyTime_AsTimeval_noraise(_PyTime_t t, +/* Similar to _PyTime_AsTimeval() but don't raise an exception on overflow. + On overflow, clamp tv_sec to _PyTime_t min/max. */ +PyAPI_FUNC(void) _PyTime_AsTimeval_clamp(_PyTime_t t, struct timeval *tv, _PyTime_round_t round); @@ -153,9 +211,18 @@ PyAPI_FUNC(int) _PyTime_FromTimespec(_PyTime_t *tp, struct timespec *ts); tv_nsec is always positive. Raise an exception and return -1 on error, return 0 on success. */ PyAPI_FUNC(int) _PyTime_AsTimespec(_PyTime_t t, struct timespec *ts); + +/* Similar to _PyTime_AsTimespec() but don't raise an exception on overflow. + On overflow, clamp tv_sec to _PyTime_t min/max. */ +PyAPI_FUNC(void) _PyTime_AsTimespec_clamp(_PyTime_t t, struct timespec *ts); #endif + +// Compute t1 + t2. Clamp to [_PyTime_MIN; _PyTime_MAX] on overflow. +PyAPI_FUNC(_PyTime_t) _PyTime_Add(_PyTime_t t1, _PyTime_t t2); + /* Compute ticks * mul / div. + Clamp to [_PyTime_MIN; _PyTime_MAX] on overflow. The caller must ensure that ((div - 1) * mul) cannot overflow. */ PyAPI_FUNC(_PyTime_t) _PyTime_MulDiv(_PyTime_t ticks, _PyTime_t mul, @@ -172,8 +239,8 @@ typedef struct { /* Get the current time from the system clock. If the internal clock fails, silently ignore the error and return 0. - On integer overflow, silently ignore the overflow and truncated the clock to - _PyTime_MIN or _PyTime_MAX. + On integer overflow, silently ignore the overflow and clamp the clock to + [_PyTime_MIN; _PyTime_MAX]. Use _PyTime_GetSystemClockWithInfo() to check for failure. */ PyAPI_FUNC(_PyTime_t) _PyTime_GetSystemClock(void); @@ -192,8 +259,8 @@ PyAPI_FUNC(int) _PyTime_GetSystemClockWithInfo( results of consecutive calls is valid. If the internal clock fails, silently ignore the error and return 0. - On integer overflow, silently ignore the overflow and truncated the clock to - _PyTime_MIN or _PyTime_MAX. + On integer overflow, silently ignore the overflow and clamp the clock to + [_PyTime_MIN; _PyTime_MAX]. Use _PyTime_GetMonotonicClockWithInfo() to check for failure. */ PyAPI_FUNC(_PyTime_t) _PyTime_GetMonotonicClock(void); @@ -223,8 +290,8 @@ PyAPI_FUNC(int) _PyTime_gmtime(time_t t, struct tm *tm); measure a short duration. If the internal clock fails, silently ignore the error and return 0. - On integer overflow, silently ignore the overflow and truncated the clock to - _PyTime_MIN or _PyTime_MAX. + On integer overflow, silently ignore the overflow and clamp the clock to + [_PyTime_MIN; _PyTime_MAX]. Use _PyTime_GetPerfCounterWithInfo() to check for failure. */ PyAPI_FUNC(_PyTime_t) _PyTime_GetPerfCounter(void); @@ -239,6 +306,15 @@ PyAPI_FUNC(int) _PyTime_GetPerfCounterWithInfo( _PyTime_t *t, _Py_clock_info_t *info); + +// Create a deadline. +// Pseudo code: _PyTime_GetMonotonicClock() + timeout. +PyAPI_FUNC(_PyTime_t) _PyDeadline_Init(_PyTime_t timeout); + +// Get remaining time from a deadline. +// Pseudo code: deadline - _PyTime_GetMonotonicClock(). +PyAPI_FUNC(_PyTime_t) _PyDeadline_Get(_PyTime_t deadline); + #ifdef __cplusplus } #endif diff --git a/src/external/windows/include/python/cpython/setobject.h b/src/external/windows/include/python/cpython/setobject.h new file mode 100644 index 00000000..380500ab --- /dev/null +++ b/src/external/windows/include/python/cpython/setobject.h @@ -0,0 +1,67 @@ +#ifndef Py_CPYTHON_SETOBJECT_H +# error "this header file must not be included directly" +#endif + +/* There are three kinds of entries in the table: + +1. Unused: key == NULL and hash == 0 +2. Dummy: key == dummy and hash == -1 +3. Active: key != NULL and key != dummy and hash != -1 + +The hash field of Unused slots is always zero. + +The hash field of Dummy slots are set to -1 +meaning that dummy entries can be detected by +either entry->key==dummy or by entry->hash==-1. +*/ + +#define PySet_MINSIZE 8 + +typedef struct { + PyObject *key; + Py_hash_t hash; /* Cached hash code of the key */ +} setentry; + +/* The SetObject data structure is shared by set and frozenset objects. + +Invariant for sets: + - hash is -1 + +Invariants for frozensets: + - data is immutable. + - hash is the hash of the frozenset or -1 if not computed yet. + +*/ + +typedef struct { + PyObject_HEAD + + Py_ssize_t fill; /* Number active and dummy entries*/ + Py_ssize_t used; /* Number active entries */ + + /* The table contains mask + 1 slots, and that's a power of 2. + * We store the mask instead of the size because the mask is more + * frequently needed. + */ + Py_ssize_t mask; + + /* The table points to a fixed-size smalltable for small tables + * or to additional malloc'ed memory for bigger tables. + * The table pointer is never NULL which saves us from repeated + * runtime null-tests. + */ + setentry *table; + Py_hash_t hash; /* Only used by frozenset objects */ + Py_ssize_t finger; /* Search finger for pop() */ + + setentry smalltable[PySet_MINSIZE]; + PyObject *weakreflist; /* List of weak references */ +} PySetObject; + +#define PySet_GET_SIZE(so) \ + (assert(PyAnySet_Check(so)), (((PySetObject *)(so))->used)) + +PyAPI_DATA(PyObject *) _PySet_Dummy; + +PyAPI_FUNC(int) _PySet_NextEntry(PyObject *set, Py_ssize_t *pos, PyObject **key, Py_hash_t *hash); +PyAPI_FUNC(int) _PySet_Update(PyObject *set, PyObject *iterable); diff --git a/src/external/windows/include/python/cpython/sysmodule.h b/src/external/windows/include/python/cpython/sysmodule.h index 6aeba9cc..0f84620d 100755 --- a/src/external/windows/include/python/cpython/sysmodule.h +++ b/src/external/windows/include/python/cpython/sysmodule.h @@ -2,8 +2,8 @@ # error "this header file must not be included directly" #endif -PyAPI_FUNC(PyObject *) _PySys_GetObjectId(_Py_Identifier *key); -PyAPI_FUNC(int) _PySys_SetObjectId(_Py_Identifier *key, PyObject *); +PyAPI_FUNC(PyObject *) _PySys_GetAttr(PyThreadState *tstate, + PyObject *name); PyAPI_FUNC(size_t) _PySys_GetSizeOf(PyObject *); diff --git a/src/external/windows/include/python/cpython/traceback.h b/src/external/windows/include/python/cpython/traceback.h index 665587d1..a43129bb 100755 --- a/src/external/windows/include/python/cpython/traceback.h +++ b/src/external/windows/include/python/cpython/traceback.h @@ -2,13 +2,15 @@ # error "this header file must not be included directly" #endif -typedef struct _traceback { +typedef struct _traceback PyTracebackObject; + +struct _traceback { PyObject_HEAD - struct _traceback *tb_next; + PyTracebackObject *tb_next; PyFrameObject *tb_frame; int tb_lasti; int tb_lineno; -} PyTracebackObject; +}; -PyAPI_FUNC(int) _Py_DisplaySourceLine(PyObject *, PyObject *, int, int); +PyAPI_FUNC(int) _Py_DisplaySourceLine(PyObject *, PyObject *, int, int, int *, PyObject **); PyAPI_FUNC(void) _PyTraceback_Add(const char *, const char *, int); diff --git a/src/external/windows/include/python/cpython/tupleobject.h b/src/external/windows/include/python/cpython/tupleobject.h index 141aa0f6..6a6443bc 100755 --- a/src/external/windows/include/python/cpython/tupleobject.h +++ b/src/external/windows/include/python/cpython/tupleobject.h @@ -13,16 +13,31 @@ typedef struct { PyAPI_FUNC(int) _PyTuple_Resize(PyObject **, Py_ssize_t); PyAPI_FUNC(void) _PyTuple_MaybeUntrack(PyObject *); -/* Macros trading safety for speed */ - /* Cast argument to PyTupleObject* type. */ -#define _PyTuple_CAST(op) (assert(PyTuple_Check(op)), (PyTupleObject *)(op)) +#define _PyTuple_CAST(op) \ + (assert(PyTuple_Check(op)), _Py_CAST(PyTupleObject*, (op))) -#define PyTuple_GET_SIZE(op) Py_SIZE(_PyTuple_CAST(op)) +// Macros and static inline functions, trading safety for speed -#define PyTuple_GET_ITEM(op, i) (_PyTuple_CAST(op)->ob_item[i]) +static inline Py_ssize_t PyTuple_GET_SIZE(PyObject *op) { + PyTupleObject *tuple = _PyTuple_CAST(op); + return Py_SIZE(tuple); +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyTuple_GET_SIZE(op) PyTuple_GET_SIZE(_PyObject_CAST(op)) +#endif -/* Macro, *only* to be used to fill in brand new tuples */ -#define PyTuple_SET_ITEM(op, i, v) ((void)(_PyTuple_CAST(op)->ob_item[i] = v)) +#define PyTuple_GET_ITEM(op, index) (_PyTuple_CAST(op)->ob_item[index]) + +/* Function *only* to be used to fill in brand new tuples */ +static inline void +PyTuple_SET_ITEM(PyObject *op, Py_ssize_t index, PyObject *value) { + PyTupleObject *tuple = _PyTuple_CAST(op); + tuple->ob_item[index] = value; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +#define PyTuple_SET_ITEM(op, index, value) \ + PyTuple_SET_ITEM(_PyObject_CAST(op), index, _PyObject_CAST(value)) +#endif PyAPI_FUNC(void) _PyTuple_DebugMallocStats(FILE *out); diff --git a/src/external/windows/include/python/cpython/unicodeobject.h b/src/external/windows/include/python/cpython/unicodeobject.h index 7d00d861..ff98851d 100755 --- a/src/external/windows/include/python/cpython/unicodeobject.h +++ b/src/external/windows/include/python/cpython/unicodeobject.h @@ -45,24 +45,11 @@ #define Py_UNICODE_ISALPHA(ch) _PyUnicode_IsAlpha(ch) #define Py_UNICODE_ISALNUM(ch) \ - (Py_UNICODE_ISALPHA(ch) || \ + (Py_UNICODE_ISALPHA(ch) || \ Py_UNICODE_ISDECIMAL(ch) || \ Py_UNICODE_ISDIGIT(ch) || \ Py_UNICODE_ISNUMERIC(ch)) -Py_DEPRECATED(3.3) static inline void -Py_UNICODE_COPY(Py_UNICODE *target, const Py_UNICODE *source, Py_ssize_t length) { - memcpy(target, source, (size_t)(length) * sizeof(Py_UNICODE)); -} - -Py_DEPRECATED(3.3) static inline void -Py_UNICODE_FILL(Py_UNICODE *target, Py_UNICODE value, Py_ssize_t length) { - Py_ssize_t i; - for (i = 0; i < length; i++) { - target[i] = value; - } -} - /* macros to work with surrogates */ #define Py_UNICODE_IS_SURROGATE(ch) (0xD800 <= (ch) && (ch) <= 0xDFFF) #define Py_UNICODE_IS_HIGH_SURROGATE(ch) (0xD800 <= (ch) && (ch) <= 0xDBFF) @@ -247,40 +234,16 @@ PyAPI_FUNC(int) _PyUnicode_CheckConsistency( PyObject *op, int check_content); -/* Fast access macros */ -/* Returns the deprecated Py_UNICODE representation's size in code units - (this includes surrogate pairs as 2 units). - If the Py_UNICODE representation is not available, it will be computed - on request. Use PyUnicode_GET_LENGTH() for the length in code points. */ - -/* Py_DEPRECATED(3.3) */ -#define PyUnicode_GET_SIZE(op) \ - (assert(PyUnicode_Check(op)), \ - (((PyASCIIObject *)(op))->wstr) ? \ - PyUnicode_WSTR_LENGTH(op) : \ - ((void)PyUnicode_AsUnicode(_PyObject_CAST(op)),\ - assert(((PyASCIIObject *)(op))->wstr), \ - PyUnicode_WSTR_LENGTH(op))) - -/* Py_DEPRECATED(3.3) */ -#define PyUnicode_GET_DATA_SIZE(op) \ - (PyUnicode_GET_SIZE(op) * Py_UNICODE_SIZE) - -/* Alias for PyUnicode_AsUnicode(). This will create a wchar_t/Py_UNICODE - representation on demand. Using this macro is very inefficient now, - try to port your code to use the new PyUnicode_*BYTE_DATA() macros or - use PyUnicode_WRITE() and PyUnicode_READ(). */ - -/* Py_DEPRECATED(3.3) */ -#define PyUnicode_AS_UNICODE(op) \ +#define _PyASCIIObject_CAST(op) \ (assert(PyUnicode_Check(op)), \ - (((PyASCIIObject *)(op))->wstr) ? (((PyASCIIObject *)(op))->wstr) : \ - PyUnicode_AsUnicode(_PyObject_CAST(op))) - -/* Py_DEPRECATED(3.3) */ -#define PyUnicode_AS_DATA(op) \ - ((const char *)(PyUnicode_AS_UNICODE(op))) + _Py_CAST(PyASCIIObject*, (op))) +#define _PyCompactUnicodeObject_CAST(op) \ + (assert(PyUnicode_Check(op)), \ + _Py_CAST(PyCompactUnicodeObject*, (op))) +#define _PyUnicodeObject_CAST(op) \ + (assert(PyUnicode_Check(op)), \ + _Py_CAST(PyUnicodeObject*, (op))) /* --- Flexible String Representation Helper Macros (PEP 393) -------------- */ @@ -292,163 +255,212 @@ PyAPI_FUNC(int) _PyUnicode_CheckConsistency( #define SSTATE_INTERNED_MORTAL 1 #define SSTATE_INTERNED_IMMORTAL 2 +/* Use only if you know it's a string */ +static inline unsigned int PyUnicode_CHECK_INTERNED(PyObject *op) { + return _PyASCIIObject_CAST(op)->state.interned; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyUnicode_CHECK_INTERNED(op) PyUnicode_CHECK_INTERNED(_PyObject_CAST(op)) +#endif + +/* Fast check to determine whether an object is ready. Equivalent to: + PyUnicode_IS_COMPACT(op) || _PyUnicodeObject_CAST(op)->data.any */ +static inline unsigned int PyUnicode_IS_READY(PyObject *op) { + return _PyASCIIObject_CAST(op)->state.ready; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyUnicode_IS_READY(op) PyUnicode_IS_READY(_PyObject_CAST(op)) +#endif + /* Return true if the string contains only ASCII characters, or 0 if not. The string may be compact (PyUnicode_IS_COMPACT_ASCII) or not, but must be ready. */ -#define PyUnicode_IS_ASCII(op) \ - (assert(PyUnicode_Check(op)), \ - assert(PyUnicode_IS_READY(op)), \ - ((PyASCIIObject*)op)->state.ascii) +static inline unsigned int PyUnicode_IS_ASCII(PyObject *op) { + assert(PyUnicode_IS_READY(op)); + return _PyASCIIObject_CAST(op)->state.ascii; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyUnicode_IS_ASCII(op) PyUnicode_IS_ASCII(_PyObject_CAST(op)) +#endif /* Return true if the string is compact or 0 if not. No type checks or Ready calls are performed. */ -#define PyUnicode_IS_COMPACT(op) \ - (((PyASCIIObject*)(op))->state.compact) +static inline unsigned int PyUnicode_IS_COMPACT(PyObject *op) { + return _PyASCIIObject_CAST(op)->state.compact; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyUnicode_IS_COMPACT(op) PyUnicode_IS_COMPACT(_PyObject_CAST(op)) +#endif /* Return true if the string is a compact ASCII string (use PyASCIIObject structure), or 0 if not. No type checks or Ready calls are performed. */ -#define PyUnicode_IS_COMPACT_ASCII(op) \ - (((PyASCIIObject*)op)->state.ascii && PyUnicode_IS_COMPACT(op)) +static inline int PyUnicode_IS_COMPACT_ASCII(PyObject *op) { + return (_PyASCIIObject_CAST(op)->state.ascii && PyUnicode_IS_COMPACT(op)); +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyUnicode_IS_COMPACT_ASCII(op) PyUnicode_IS_COMPACT_ASCII(_PyObject_CAST(op)) +#endif enum PyUnicode_Kind { /* String contains only wstr byte characters. This is only possible when the string was created with a legacy API and _PyUnicode_Ready() has not been called yet. */ PyUnicode_WCHAR_KIND = 0, -/* Return values of the PyUnicode_KIND() macro: */ +/* Return values of the PyUnicode_KIND() function: */ PyUnicode_1BYTE_KIND = 1, PyUnicode_2BYTE_KIND = 2, PyUnicode_4BYTE_KIND = 4 }; +/* Return one of the PyUnicode_*_KIND values defined above. */ +#define PyUnicode_KIND(op) \ + (assert(PyUnicode_IS_READY(op)), \ + _PyASCIIObject_CAST(op)->state.kind) + +/* Return a void pointer to the raw unicode buffer. */ +static inline void* _PyUnicode_COMPACT_DATA(PyObject *op) { + if (PyUnicode_IS_ASCII(op)) { + return _Py_STATIC_CAST(void*, (_PyASCIIObject_CAST(op) + 1)); + } + return _Py_STATIC_CAST(void*, (_PyCompactUnicodeObject_CAST(op) + 1)); +} + +static inline void* _PyUnicode_NONCOMPACT_DATA(PyObject *op) { + void *data; + assert(!PyUnicode_IS_COMPACT(op)); + data = _PyUnicodeObject_CAST(op)->data.any; + assert(data != NULL); + return data; +} + +static inline void* PyUnicode_DATA(PyObject *op) { + if (PyUnicode_IS_COMPACT(op)) { + return _PyUnicode_COMPACT_DATA(op); + } + return _PyUnicode_NONCOMPACT_DATA(op); +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyUnicode_DATA(op) PyUnicode_DATA(_PyObject_CAST(op)) +#endif + /* Return pointers to the canonical representation cast to unsigned char, Py_UCS2, or Py_UCS4 for direct character access. No checks are performed, use PyUnicode_KIND() before to ensure these will work correctly. */ -#define PyUnicode_1BYTE_DATA(op) ((Py_UCS1*)PyUnicode_DATA(op)) -#define PyUnicode_2BYTE_DATA(op) ((Py_UCS2*)PyUnicode_DATA(op)) -#define PyUnicode_4BYTE_DATA(op) ((Py_UCS4*)PyUnicode_DATA(op)) +#define PyUnicode_1BYTE_DATA(op) _Py_STATIC_CAST(Py_UCS1*, PyUnicode_DATA(op)) +#define PyUnicode_2BYTE_DATA(op) _Py_STATIC_CAST(Py_UCS2*, PyUnicode_DATA(op)) +#define PyUnicode_4BYTE_DATA(op) _Py_STATIC_CAST(Py_UCS4*, PyUnicode_DATA(op)) -/* Return one of the PyUnicode_*_KIND values defined above. */ -#define PyUnicode_KIND(op) \ - (assert(PyUnicode_Check(op)), \ - assert(PyUnicode_IS_READY(op)), \ - ((PyASCIIObject *)(op))->state.kind) +/* Returns the length of the unicode string. The caller has to make sure that + the string has it's canonical representation set before calling + this function. Call PyUnicode_(FAST_)Ready to ensure that. */ +static inline Py_ssize_t PyUnicode_GET_LENGTH(PyObject *op) { + assert(PyUnicode_IS_READY(op)); + return _PyASCIIObject_CAST(op)->length; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyUnicode_GET_LENGTH(op) PyUnicode_GET_LENGTH(_PyObject_CAST(op)) +#endif -/* Return a void pointer to the raw unicode buffer. */ -#define _PyUnicode_COMPACT_DATA(op) \ - (PyUnicode_IS_ASCII(op) ? \ - ((void*)((PyASCIIObject*)(op) + 1)) : \ - ((void*)((PyCompactUnicodeObject*)(op) + 1))) - -#define _PyUnicode_NONCOMPACT_DATA(op) \ - (assert(((PyUnicodeObject*)(op))->data.any), \ - ((((PyUnicodeObject *)(op))->data.any))) - -#define PyUnicode_DATA(op) \ - (assert(PyUnicode_Check(op)), \ - PyUnicode_IS_COMPACT(op) ? _PyUnicode_COMPACT_DATA(op) : \ - _PyUnicode_NONCOMPACT_DATA(op)) - -/* In the access macros below, "kind" may be evaluated more than once. - All other macro parameters are evaluated exactly once, so it is safe - to put side effects into them (such as increasing the index). */ - -/* Write into the canonical representation, this macro does not do any sanity +/* Write into the canonical representation, this function does not do any sanity checks and is intended for usage in loops. The caller should cache the - kind and data pointers obtained from other macro calls. + kind and data pointers obtained from other function calls. index is the index in the string (starts at 0) and value is the new code point value which should be written to that location. */ +static inline void PyUnicode_WRITE(int kind, void *data, + Py_ssize_t index, Py_UCS4 value) +{ + if (kind == PyUnicode_1BYTE_KIND) { + assert(value <= 0xffU); + _Py_STATIC_CAST(Py_UCS1*, data)[index] = _Py_STATIC_CAST(Py_UCS1, value); + } + else if (kind == PyUnicode_2BYTE_KIND) { + assert(value <= 0xffffU); + _Py_STATIC_CAST(Py_UCS2*, data)[index] = _Py_STATIC_CAST(Py_UCS2, value); + } + else { + assert(kind == PyUnicode_4BYTE_KIND); + assert(value <= 0x10ffffU); + _Py_STATIC_CAST(Py_UCS4*, data)[index] = value; + } +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 #define PyUnicode_WRITE(kind, data, index, value) \ - do { \ - switch ((kind)) { \ - case PyUnicode_1BYTE_KIND: { \ - ((Py_UCS1 *)(data))[(index)] = (Py_UCS1)(value); \ - break; \ - } \ - case PyUnicode_2BYTE_KIND: { \ - ((Py_UCS2 *)(data))[(index)] = (Py_UCS2)(value); \ - break; \ - } \ - default: { \ - assert((kind) == PyUnicode_4BYTE_KIND); \ - ((Py_UCS4 *)(data))[(index)] = (Py_UCS4)(value); \ - } \ - } \ - } while (0) + PyUnicode_WRITE(_Py_STATIC_CAST(int, kind), _Py_CAST(void*, data), \ + (index), _Py_STATIC_CAST(Py_UCS4, value)) +#endif /* Read a code point from the string's canonical representation. No checks or ready calls are performed. */ +static inline Py_UCS4 PyUnicode_READ(int kind, + const void *data, Py_ssize_t index) +{ + if (kind == PyUnicode_1BYTE_KIND) { + return _Py_STATIC_CAST(const Py_UCS1*, data)[index]; + } + if (kind == PyUnicode_2BYTE_KIND) { + return _Py_STATIC_CAST(const Py_UCS2*, data)[index]; + } + assert(kind == PyUnicode_4BYTE_KIND); + return _Py_STATIC_CAST(const Py_UCS4*, data)[index]; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 #define PyUnicode_READ(kind, data, index) \ - ((Py_UCS4) \ - ((kind) == PyUnicode_1BYTE_KIND ? \ - ((const Py_UCS1 *)(data))[(index)] : \ - ((kind) == PyUnicode_2BYTE_KIND ? \ - ((const Py_UCS2 *)(data))[(index)] : \ - ((const Py_UCS4 *)(data))[(index)] \ - ) \ - )) + PyUnicode_READ(_Py_STATIC_CAST(int, kind), \ + _Py_STATIC_CAST(const void*, data), \ + (index)) +#endif /* PyUnicode_READ_CHAR() is less efficient than PyUnicode_READ() because it calls PyUnicode_KIND() and might call it twice. For single reads, use PyUnicode_READ_CHAR, for multiple consecutive reads callers should cache kind and use PyUnicode_READ instead. */ -#define PyUnicode_READ_CHAR(unicode, index) \ - (assert(PyUnicode_Check(unicode)), \ - assert(PyUnicode_IS_READY(unicode)), \ - (Py_UCS4) \ - (PyUnicode_KIND((unicode)) == PyUnicode_1BYTE_KIND ? \ - ((const Py_UCS1 *)(PyUnicode_DATA((unicode))))[(index)] : \ - (PyUnicode_KIND((unicode)) == PyUnicode_2BYTE_KIND ? \ - ((const Py_UCS2 *)(PyUnicode_DATA((unicode))))[(index)] : \ - ((const Py_UCS4 *)(PyUnicode_DATA((unicode))))[(index)] \ - ) \ - )) - -/* Returns the length of the unicode string. The caller has to make sure that - the string has it's canonical representation set before calling - this macro. Call PyUnicode_(FAST_)Ready to ensure that. */ -#define PyUnicode_GET_LENGTH(op) \ - (assert(PyUnicode_Check(op)), \ - assert(PyUnicode_IS_READY(op)), \ - ((PyASCIIObject *)(op))->length) - - -/* Fast check to determine whether an object is ready. Equivalent to - PyUnicode_IS_COMPACT(op) || ((PyUnicodeObject*)(op))->data.any */ - -#define PyUnicode_IS_READY(op) (((PyASCIIObject*)op)->state.ready) - -/* PyUnicode_READY() does less work than _PyUnicode_Ready() in the best - case. If the canonical representation is not yet set, it will still call - _PyUnicode_Ready(). - Returns 0 on success and -1 on errors. */ -#define PyUnicode_READY(op) \ - (assert(PyUnicode_Check(op)), \ - (PyUnicode_IS_READY(op) ? \ - 0 : _PyUnicode_Ready(_PyObject_CAST(op)))) +static inline Py_UCS4 PyUnicode_READ_CHAR(PyObject *unicode, Py_ssize_t index) +{ + int kind; + assert(PyUnicode_IS_READY(unicode)); + kind = PyUnicode_KIND(unicode); + if (kind == PyUnicode_1BYTE_KIND) { + return PyUnicode_1BYTE_DATA(unicode)[index]; + } + if (kind == PyUnicode_2BYTE_KIND) { + return PyUnicode_2BYTE_DATA(unicode)[index]; + } + assert(kind == PyUnicode_4BYTE_KIND); + return PyUnicode_4BYTE_DATA(unicode)[index]; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyUnicode_READ_CHAR(unicode, index) \ + PyUnicode_READ_CHAR(_PyObject_CAST(unicode), (index)) +#endif /* Return a maximum character value which is suitable for creating another string based on op. This is always an approximation but more efficient than iterating over the string. */ -#define PyUnicode_MAX_CHAR_VALUE(op) \ - (assert(PyUnicode_IS_READY(op)), \ - (PyUnicode_IS_ASCII(op) ? \ - (0x7f) : \ - (PyUnicode_KIND(op) == PyUnicode_1BYTE_KIND ? \ - (0xffU) : \ - (PyUnicode_KIND(op) == PyUnicode_2BYTE_KIND ? \ - (0xffffU) : \ - (0x10ffffU))))) +static inline Py_UCS4 PyUnicode_MAX_CHAR_VALUE(PyObject *op) +{ + int kind; -Py_DEPRECATED(3.3) -static inline Py_ssize_t _PyUnicode_get_wstr_length(PyObject *op) { - return PyUnicode_IS_COMPACT_ASCII(op) ? - ((PyASCIIObject*)op)->length : - ((PyCompactUnicodeObject*)op)->wstr_length; + assert(PyUnicode_IS_READY(op)); + if (PyUnicode_IS_ASCII(op)) { + return 0x7fU; + } + + kind = PyUnicode_KIND(op); + if (kind == PyUnicode_1BYTE_KIND) { + return 0xffU; + } + if (kind == PyUnicode_2BYTE_KIND) { + return 0xffffU; + } + assert(kind == PyUnicode_4BYTE_KIND); + return 0x10ffffU; } -#define PyUnicode_WSTR_LENGTH(op) _PyUnicode_get_wstr_length((PyObject*)op) +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyUnicode_MAX_CHAR_VALUE(op) \ + PyUnicode_MAX_CHAR_VALUE(_PyObject_CAST(op)) +#endif /* === Public API ========================================================= */ @@ -467,12 +479,27 @@ PyAPI_FUNC(PyObject*) PyUnicode_New( objects which were created using the old API to the new flexible format introduced with PEP 393. - Don't call this function directly, use the public PyUnicode_READY() macro + Don't call this function directly, use the public PyUnicode_READY() function instead. */ PyAPI_FUNC(int) _PyUnicode_Ready( PyObject *unicode /* Unicode object */ ); +/* PyUnicode_READY() does less work than _PyUnicode_Ready() in the best + case. If the canonical representation is not yet set, it will still call + _PyUnicode_Ready(). + Returns 0 on success and -1 on errors. */ +static inline int PyUnicode_READY(PyObject *op) +{ + if (PyUnicode_IS_READY(op)) { + return 0; + } + return _PyUnicode_Ready(op); +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyUnicode_READY(op) PyUnicode_READY(_PyObject_CAST(op)) +#endif + /* Get a copy of a Unicode string. */ PyAPI_FUNC(PyObject*) _PyUnicode_Copy( PyObject *unicode @@ -539,20 +566,6 @@ PyAPI_FUNC(void) _PyUnicode_FastFill( Py_UCS4 fill_char ); -/* Create a Unicode Object from the Py_UNICODE buffer u of the given - size. - - u may be NULL which causes the contents to be undefined. It is the - user's responsibility to fill in the needed data afterwards. Note - that modifying the Unicode object contents after construction is - only allowed if u was set to NULL. - - The buffer is copied into the new object. */ -Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject*) PyUnicode_FromUnicode( - const Py_UNICODE *u, /* Unicode buffer */ - Py_ssize_t size /* size of buffer */ - ); - /* Create a new string from a buffer of Py_UCS1, Py_UCS2 or Py_UCS4 characters. Scan the string to find the maximum character. */ PyAPI_FUNC(PyObject*) PyUnicode_FromKindAndData( @@ -573,6 +586,22 @@ PyAPI_FUNC(Py_UCS4) _PyUnicode_FindMaxChar ( Py_ssize_t start, Py_ssize_t end); +/* --- Legacy deprecated API ---------------------------------------------- */ + +/* Create a Unicode Object from the Py_UNICODE buffer u of the given + size. + + u may be NULL which causes the contents to be undefined. It is the + user's responsibility to fill in the needed data afterwards. Note + that modifying the Unicode object contents after construction is + only allowed if u was set to NULL. + + The buffer is copied into the new object. */ +Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject*) PyUnicode_FromUnicode( + const Py_UNICODE *u, /* Unicode buffer */ + Py_ssize_t size /* size of buffer */ + ); + /* Return a read-only pointer to the Unicode object's internal Py_UNICODE buffer. If the wchar_t/Py_UNICODE representation is not yet available, this @@ -598,6 +627,92 @@ Py_DEPRECATED(3.3) PyAPI_FUNC(Py_UNICODE *) PyUnicode_AsUnicodeAndSize( ); +/* Fast access macros */ + +Py_DEPRECATED(3.3) +static inline Py_ssize_t PyUnicode_WSTR_LENGTH(PyObject *op) +{ + if (PyUnicode_IS_COMPACT_ASCII(op)) { + return _PyASCIIObject_CAST(op)->length; + } + else { + return _PyCompactUnicodeObject_CAST(op)->wstr_length; + } +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyUnicode_WSTR_LENGTH(op) PyUnicode_WSTR_LENGTH(_PyObject_CAST(op)) +#endif + +/* Returns the deprecated Py_UNICODE representation's size in code units + (this includes surrogate pairs as 2 units). + If the Py_UNICODE representation is not available, it will be computed + on request. Use PyUnicode_GET_LENGTH() for the length in code points. */ + +Py_DEPRECATED(3.3) +static inline Py_ssize_t PyUnicode_GET_SIZE(PyObject *op) +{ + _Py_COMP_DIAG_PUSH + _Py_COMP_DIAG_IGNORE_DEPR_DECLS + if (_PyASCIIObject_CAST(op)->wstr == _Py_NULL) { + (void)PyUnicode_AsUnicode(op); + assert(_PyASCIIObject_CAST(op)->wstr != _Py_NULL); + } + return PyUnicode_WSTR_LENGTH(op); + _Py_COMP_DIAG_POP +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyUnicode_GET_SIZE(op) PyUnicode_GET_SIZE(_PyObject_CAST(op)) +#endif + +Py_DEPRECATED(3.3) +static inline Py_ssize_t PyUnicode_GET_DATA_SIZE(PyObject *op) +{ + _Py_COMP_DIAG_PUSH + _Py_COMP_DIAG_IGNORE_DEPR_DECLS + return PyUnicode_GET_SIZE(op) * Py_UNICODE_SIZE; + _Py_COMP_DIAG_POP +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyUnicode_GET_DATA_SIZE(op) PyUnicode_GET_DATA_SIZE(_PyObject_CAST(op)) +#endif + +/* Alias for PyUnicode_AsUnicode(). This will create a wchar_t/Py_UNICODE + representation on demand. Using this macro is very inefficient now, + try to port your code to use the new PyUnicode_*BYTE_DATA() macros or + use PyUnicode_WRITE() and PyUnicode_READ(). */ + +Py_DEPRECATED(3.3) +static inline Py_UNICODE* PyUnicode_AS_UNICODE(PyObject *op) +{ + wchar_t *wstr = _PyASCIIObject_CAST(op)->wstr; + if (wstr != _Py_NULL) { + return wstr; + } + + _Py_COMP_DIAG_PUSH + _Py_COMP_DIAG_IGNORE_DEPR_DECLS + return PyUnicode_AsUnicode(op); + _Py_COMP_DIAG_POP +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyUnicode_AS_UNICODE(op) PyUnicode_AS_UNICODE(_PyObject_CAST(op)) +#endif + +Py_DEPRECATED(3.3) +static inline const char* PyUnicode_AS_DATA(PyObject *op) +{ + _Py_COMP_DIAG_PUSH + _Py_COMP_DIAG_IGNORE_DEPR_DECLS + Py_UNICODE *data = PyUnicode_AS_UNICODE(op); + // In C++, casting directly PyUnicode* to const char* is not valid + return _Py_STATIC_CAST(const char*, _Py_STATIC_CAST(const void*, data)); + _Py_COMP_DIAG_POP +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyUnicode_AS_DATA(op) PyUnicode_AS_DATA(_PyObject_CAST(op)) +#endif + + /* --- _PyUnicodeWriter API ----------------------------------------------- */ typedef struct { @@ -743,27 +858,8 @@ PyAPI_FUNC(const char *) PyUnicode_AsUTF8(PyObject *unicode); #define _PyUnicode_AsString PyUnicode_AsUTF8 -/* --- Generic Codecs ----------------------------------------------------- */ - -/* Encodes a Py_UNICODE buffer of the given size and returns a - Python string object. */ -Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject*) PyUnicode_Encode( - const Py_UNICODE *s, /* Unicode char buffer */ - Py_ssize_t size, /* number of Py_UNICODE chars to encode */ - const char *encoding, /* encoding */ - const char *errors /* error handling */ - ); - /* --- UTF-7 Codecs ------------------------------------------------------- */ -Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject*) PyUnicode_EncodeUTF7( - const Py_UNICODE *data, /* Unicode char buffer */ - Py_ssize_t length, /* number of Py_UNICODE chars to encode */ - int base64SetO, /* Encode RFC2152 Set O characters in base64 */ - int base64WhiteSpace, /* Encode whitespace (sp, ht, nl, cr) in base64 */ - const char *errors /* error handling */ - ); - PyAPI_FUNC(PyObject*) _PyUnicode_EncodeUTF7( PyObject *unicode, /* Unicode object */ int base64SetO, /* Encode RFC2152 Set O characters in base64 */ @@ -777,21 +873,8 @@ PyAPI_FUNC(PyObject*) _PyUnicode_AsUTF8String( PyObject *unicode, const char *errors); -Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject*) PyUnicode_EncodeUTF8( - const Py_UNICODE *data, /* Unicode char buffer */ - Py_ssize_t length, /* number of Py_UNICODE chars to encode */ - const char *errors /* error handling */ - ); - /* --- UTF-32 Codecs ------------------------------------------------------ */ -Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject*) PyUnicode_EncodeUTF32( - const Py_UNICODE *data, /* Unicode char buffer */ - Py_ssize_t length, /* number of Py_UNICODE chars to encode */ - const char *errors, /* error handling */ - int byteorder /* byteorder to use 0=BOM+native;-1=LE,1=BE */ - ); - PyAPI_FUNC(PyObject*) _PyUnicode_EncodeUTF32( PyObject *object, /* Unicode object */ const char *errors, /* error handling */ @@ -813,19 +896,7 @@ PyAPI_FUNC(PyObject*) _PyUnicode_EncodeUTF32( If byteorder is 0, the output string will always start with the Unicode BOM mark (U+FEFF). In the other two modes, no BOM mark is prepended. - - Note that Py_UNICODE data is being interpreted as UTF-16 reduced to - UCS-2. This trick makes it possible to add full UTF-16 capabilities - at a later point without compromising the APIs. - */ -Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject*) PyUnicode_EncodeUTF16( - const Py_UNICODE *data, /* Unicode char buffer */ - Py_ssize_t length, /* number of Py_UNICODE chars to encode */ - const char *errors, /* error handling */ - int byteorder /* byteorder to use 0=BOM+native;-1=LE,1=BE */ - ); - PyAPI_FUNC(PyObject*) _PyUnicode_EncodeUTF16( PyObject* unicode, /* Unicode object */ const char *errors, /* error handling */ @@ -841,7 +912,6 @@ PyAPI_FUNC(PyObject*) _PyUnicode_DecodeUnicodeEscapeStateful( const char *errors, /* error handling */ Py_ssize_t *consumed /* bytes consumed */ ); - /* Helper for PyUnicode_DecodeUnicodeEscape that detects invalid escape chars. */ PyAPI_FUNC(PyObject*) _PyUnicode_DecodeUnicodeEscapeInternal( @@ -854,17 +924,7 @@ PyAPI_FUNC(PyObject*) _PyUnicode_DecodeUnicodeEscapeInternal( string. */ ); -Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject*) PyUnicode_EncodeUnicodeEscape( - const Py_UNICODE *data, /* Unicode char buffer */ - Py_ssize_t length /* Number of Py_UNICODE chars to encode */ - ); - -/* --- Raw-Unicode-Escape Codecs ------------------------------------------ */ - -Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject*) PyUnicode_EncodeRawUnicodeEscape( - const Py_UNICODE *data, /* Unicode char buffer */ - Py_ssize_t length /* Number of Py_UNICODE chars to encode */ - ); +/* --- Raw-Unicode-Escape Codecs ---------------------------------------------- */ /* Variant of PyUnicode_DecodeRawUnicodeEscape that supports partial decoding. */ PyAPI_FUNC(PyObject*) _PyUnicode_DecodeRawUnicodeEscapeStateful( @@ -880,42 +940,16 @@ PyAPI_FUNC(PyObject*) _PyUnicode_AsLatin1String( PyObject* unicode, const char* errors); -Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject*) PyUnicode_EncodeLatin1( - const Py_UNICODE *data, /* Unicode char buffer */ - Py_ssize_t length, /* Number of Py_UNICODE chars to encode */ - const char *errors /* error handling */ - ); - /* --- ASCII Codecs ------------------------------------------------------- */ PyAPI_FUNC(PyObject*) _PyUnicode_AsASCIIString( PyObject* unicode, const char* errors); -Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject*) PyUnicode_EncodeASCII( - const Py_UNICODE *data, /* Unicode char buffer */ - Py_ssize_t length, /* Number of Py_UNICODE chars to encode */ - const char *errors /* error handling */ - ); - /* --- Character Map Codecs ----------------------------------------------- */ -Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject*) PyUnicode_EncodeCharmap( - const Py_UNICODE *data, /* Unicode char buffer */ - Py_ssize_t length, /* Number of Py_UNICODE chars to encode */ - PyObject *mapping, /* encoding mapping */ - const char *errors /* error handling */ - ); - -PyAPI_FUNC(PyObject*) _PyUnicode_EncodeCharmap( - PyObject *unicode, /* Unicode object */ - PyObject *mapping, /* encoding mapping */ - const char *errors /* error handling */ - ); - -/* Translate a Py_UNICODE buffer of the given length by applying a - character mapping table to it and return the resulting Unicode - object. +/* Translate an Unicode object by applying a character mapping table to + it and return the resulting Unicode object. The mapping table must map Unicode ordinal integers to Unicode strings, Unicode ordinal integers or None (causing deletion of the character). @@ -923,68 +957,15 @@ PyAPI_FUNC(PyObject*) _PyUnicode_EncodeCharmap( Mapping tables may be dictionaries or sequences. Unmapped character ordinals (ones which cause a LookupError) are left untouched and are copied as-is. - */ -Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject *) PyUnicode_TranslateCharmap( - const Py_UNICODE *data, /* Unicode char buffer */ - Py_ssize_t length, /* Number of Py_UNICODE chars to encode */ - PyObject *table, /* Translate table */ +PyAPI_FUNC(PyObject*) _PyUnicode_EncodeCharmap( + PyObject *unicode, /* Unicode object */ + PyObject *mapping, /* encoding mapping */ const char *errors /* error handling */ ); -/* --- MBCS codecs for Windows -------------------------------------------- */ - -#ifdef MS_WINDOWS -Py_DEPRECATED(3.3) PyAPI_FUNC(PyObject*) PyUnicode_EncodeMBCS( - const Py_UNICODE *data, /* Unicode char buffer */ - Py_ssize_t length, /* number of Py_UNICODE chars to encode */ - const char *errors /* error handling */ - ); -#endif - /* --- Decimal Encoder ---------------------------------------------------- */ -/* Takes a Unicode string holding a decimal value and writes it into - an output buffer using standard ASCII digit codes. - - The output buffer has to provide at least length+1 bytes of storage - area. The output string is 0-terminated. - - The encoder converts whitespace to ' ', decimal characters to their - corresponding ASCII digit and all other Latin-1 characters except - \0 as-is. Characters outside this range (Unicode ordinals 1-256) - are treated as errors. This includes embedded NULL bytes. - - Error handling is defined by the errors argument: - - NULL or "strict": raise a ValueError - "ignore": ignore the wrong characters (these are not copied to the - output buffer) - "replace": replaces illegal characters with '?' - - Returns 0 on success, -1 on failure. - -*/ - -Py_DEPRECATED(3.3) PyAPI_FUNC(int) PyUnicode_EncodeDecimal( - Py_UNICODE *s, /* Unicode buffer */ - Py_ssize_t length, /* Number of Py_UNICODE chars to encode */ - char *output, /* Output buffer; must have size >= length */ - const char *errors /* error handling */ - ); - -/* Transforms code points that have decimal digit property to the - corresponding ASCII digit code points. - - Returns a new Unicode string on success, NULL on failure. -*/ - -Py_DEPRECATED(3.3) -PyAPI_FUNC(PyObject*) PyUnicode_TransformDecimalToASCII( - Py_UNICODE *s, /* Unicode buffer */ - Py_ssize_t length /* Number of Py_UNICODE chars to transform */ - ); - /* Coverts a Unicode object holding a decimal value to an ASCII string for using in int, float and complex parsers. Transforms code points that have decimal digit property to the @@ -1163,6 +1144,9 @@ PyAPI_FUNC(PyObject*) _PyUnicode_FromId(_Py_Identifier*); and where the hash values are equal (i.e. a very probable match) */ PyAPI_FUNC(int) _PyUnicode_EQ(PyObject *, PyObject *); +/* Equality check. Returns -1 on failure. */ +PyAPI_FUNC(int) _PyUnicode_Equal(PyObject *, PyObject *); + PyAPI_FUNC(int) _PyUnicode_WideCharString_Converter(PyObject *, void *); PyAPI_FUNC(int) _PyUnicode_WideCharString_Opt_Converter(PyObject *, void *); diff --git a/src/external/windows/include/python/cpython/warnings.h b/src/external/windows/include/python/cpython/warnings.h new file mode 100644 index 00000000..0e9d4d81 --- /dev/null +++ b/src/external/windows/include/python/cpython/warnings.h @@ -0,0 +1,20 @@ +#ifndef Py_CPYTHON_WARNINGS_H +# error "this header file must not be included directly" +#endif + +PyAPI_FUNC(int) PyErr_WarnExplicitObject( + PyObject *category, + PyObject *message, + PyObject *filename, + int lineno, + PyObject *module, + PyObject *registry); + +PyAPI_FUNC(int) PyErr_WarnExplicitFormat( + PyObject *category, + const char *filename, int lineno, + const char *module, PyObject *registry, + const char *format, ...); + +// DEPRECATED: Use PyErr_WarnEx() instead. +#define PyErr_Warn(category, msg) PyErr_WarnEx(category, msg, 1) diff --git a/src/external/windows/include/python/cpython/weakrefobject.h b/src/external/windows/include/python/cpython/weakrefobject.h new file mode 100644 index 00000000..1b56d0ce --- /dev/null +++ b/src/external/windows/include/python/cpython/weakrefobject.h @@ -0,0 +1,58 @@ +#ifndef Py_CPYTHON_WEAKREFOBJECT_H +# error "this header file must not be included directly" +#endif + +/* PyWeakReference is the base struct for the Python ReferenceType, ProxyType, + * and CallableProxyType. + */ +struct _PyWeakReference { + PyObject_HEAD + + /* The object to which this is a weak reference, or Py_None if none. + * Note that this is a stealth reference: wr_object's refcount is + * not incremented to reflect this pointer. + */ + PyObject *wr_object; + + /* A callable to invoke when wr_object dies, or NULL if none. */ + PyObject *wr_callback; + + /* A cache for wr_object's hash code. As usual for hashes, this is -1 + * if the hash code isn't known yet. + */ + Py_hash_t hash; + + /* If wr_object is weakly referenced, wr_object has a doubly-linked NULL- + * terminated list of weak references to it. These are the list pointers. + * If wr_object goes away, wr_object is set to Py_None, and these pointers + * have no meaning then. + */ + PyWeakReference *wr_prev; + PyWeakReference *wr_next; + vectorcallfunc vectorcall; +}; + +PyAPI_FUNC(Py_ssize_t) _PyWeakref_GetWeakrefCount(PyWeakReference *head); + +PyAPI_FUNC(void) _PyWeakref_ClearRef(PyWeakReference *self); + +static inline PyObject* PyWeakref_GET_OBJECT(PyObject *ref_obj) { + PyWeakReference *ref; + PyObject *obj; + assert(PyWeakref_Check(ref_obj)); + ref = _Py_CAST(PyWeakReference*, ref_obj); + obj = ref->wr_object; + // Explanation for the Py_REFCNT() check: when a weakref's target is part + // of a long chain of deallocations which triggers the trashcan mechanism, + // clearing the weakrefs can be delayed long after the target's refcount + // has dropped to zero. In the meantime, code accessing the weakref will + // be able to "see" the target object even though it is supposed to be + // unreachable. See issue gh-60806. + if (Py_REFCNT(obj) > 0) { + return obj; + } + return Py_None; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyWeakref_GET_OBJECT(ref) PyWeakref_GET_OBJECT(_PyObject_CAST(ref)) +#endif diff --git a/src/external/windows/include/python/descrobject.h b/src/external/windows/include/python/descrobject.h index d52b9f5c..f4fc9ef2 100755 --- a/src/external/windows/include/python/descrobject.h +++ b/src/external/windows/include/python/descrobject.h @@ -8,101 +8,37 @@ extern "C" { typedef PyObject *(*getter)(PyObject *, void *); typedef int (*setter)(PyObject *, PyObject *, void *); -typedef struct PyGetSetDef { +struct PyGetSetDef { const char *name; getter get; setter set; const char *doc; void *closure; -} PyGetSetDef; - -#ifndef Py_LIMITED_API -typedef PyObject *(*wrapperfunc)(PyObject *self, PyObject *args, - void *wrapped); - -typedef PyObject *(*wrapperfunc_kwds)(PyObject *self, PyObject *args, - void *wrapped, PyObject *kwds); - -struct wrapperbase { - const char *name; - int offset; - void *function; - wrapperfunc wrapper; - const char *doc; - int flags; - PyObject *name_strobj; }; -/* Flags for above struct */ -#define PyWrapperFlag_KEYWORDS 1 /* wrapper function takes keyword args */ - -/* Various kinds of descriptor objects */ - -typedef struct { - PyObject_HEAD - PyTypeObject *d_type; - PyObject *d_name; - PyObject *d_qualname; -} PyDescrObject; - -#define PyDescr_COMMON PyDescrObject d_common - -#define PyDescr_TYPE(x) (((PyDescrObject *)(x))->d_type) -#define PyDescr_NAME(x) (((PyDescrObject *)(x))->d_name) - -typedef struct { - PyDescr_COMMON; - PyMethodDef *d_method; - vectorcallfunc vectorcall; -} PyMethodDescrObject; - -typedef struct { - PyDescr_COMMON; - struct PyMemberDef *d_member; -} PyMemberDescrObject; - -typedef struct { - PyDescr_COMMON; - PyGetSetDef *d_getset; -} PyGetSetDescrObject; - -typedef struct { - PyDescr_COMMON; - struct wrapperbase *d_base; - void *d_wrapped; /* This can be any function pointer */ -} PyWrapperDescrObject; -#endif /* Py_LIMITED_API */ - PyAPI_DATA(PyTypeObject) PyClassMethodDescr_Type; PyAPI_DATA(PyTypeObject) PyGetSetDescr_Type; PyAPI_DATA(PyTypeObject) PyMemberDescr_Type; PyAPI_DATA(PyTypeObject) PyMethodDescr_Type; PyAPI_DATA(PyTypeObject) PyWrapperDescr_Type; PyAPI_DATA(PyTypeObject) PyDictProxy_Type; -#ifndef Py_LIMITED_API -PyAPI_DATA(PyTypeObject) _PyMethodWrapper_Type; -#endif /* Py_LIMITED_API */ +PyAPI_DATA(PyTypeObject) PyProperty_Type; PyAPI_FUNC(PyObject *) PyDescr_NewMethod(PyTypeObject *, PyMethodDef *); PyAPI_FUNC(PyObject *) PyDescr_NewClassMethod(PyTypeObject *, PyMethodDef *); -struct PyMemberDef; /* forward declaration for following prototype */ -PyAPI_FUNC(PyObject *) PyDescr_NewMember(PyTypeObject *, - struct PyMemberDef *); -PyAPI_FUNC(PyObject *) PyDescr_NewGetSet(PyTypeObject *, - struct PyGetSetDef *); -#ifndef Py_LIMITED_API -PyAPI_FUNC(PyObject *) PyDescr_NewWrapper(PyTypeObject *, - struct wrapperbase *, void *); -PyAPI_FUNC(int) PyDescr_IsData(PyObject *); -#endif +PyAPI_FUNC(PyObject *) PyDescr_NewMember(PyTypeObject *, PyMemberDef *); +PyAPI_FUNC(PyObject *) PyDescr_NewGetSet(PyTypeObject *, PyGetSetDef *); PyAPI_FUNC(PyObject *) PyDictProxy_New(PyObject *); PyAPI_FUNC(PyObject *) PyWrapper_New(PyObject *, PyObject *); +#ifndef Py_LIMITED_API +# define Py_CPYTHON_DESCROBJECT_H +# include "cpython/descrobject.h" +# undef Py_CPYTHON_DESCROBJECT_H +#endif -PyAPI_DATA(PyTypeObject) PyProperty_Type; #ifdef __cplusplus } #endif #endif /* !Py_DESCROBJECT_H */ - diff --git a/src/external/windows/include/python/dictobject.h b/src/external/windows/include/python/dictobject.h index 82ec40e6..d7555257 100755 --- a/src/external/windows/include/python/dictobject.h +++ b/src/external/windows/include/python/dictobject.h @@ -87,7 +87,7 @@ PyAPI_DATA(PyTypeObject) PyDictRevIterValue_Type; #ifndef Py_LIMITED_API # define Py_CPYTHON_DICTOBJECT_H -# include "cpython/dictobject.h" +# include "cpython/dictobject.h" # undef Py_CPYTHON_DICTOBJECT_H #endif diff --git a/src/external/windows/include/python/dynamic_annotations.h b/src/external/windows/include/python/dynamic_annotations.h index 03448a2d..3c08818b 100755 --- a/src/external/windows/include/python/dynamic_annotations.h +++ b/src/external/windows/include/python/dynamic_annotations.h @@ -44,7 +44,7 @@ Actual implementation of these macros may differ depending on the dynamic analysis tool being used. - See http://code.google.com/p/data-race-test/ for more information. + See https://code.google.com/p/data-race-test/ for more information. This file supports the following dynamic analysis tools: - None (DYNAMIC_ANNOTATIONS_ENABLED is not defined or zero). @@ -140,7 +140,7 @@ of the mutex's critical sections individually using the annotations above. This annotation makes sense only for hybrid race detectors. For pure happens-before detectors this is a no-op. For more details see - http://code.google.com/p/data-race-test/wiki/PureHappensBeforeVsHybrid . */ + https://code.google.com/p/data-race-test/wiki/PureHappensBeforeVsHybrid . */ #define _Py_ANNOTATE_PURE_HAPPENS_BEFORE_MUTEX(mu) \ AnnotateMutexIsUsedAsCondVar(__FILE__, __LINE__, mu) diff --git a/src/external/windows/include/python/eval.h b/src/external/windows/include/python/eval.h deleted file mode 100755 index 13eaddb5..00000000 --- a/src/external/windows/include/python/eval.h +++ /dev/null @@ -1,27 +0,0 @@ - -/* Interface to execute compiled code */ - -#ifndef Py_EVAL_H -#define Py_EVAL_H -#ifdef __cplusplus -extern "C" { -#endif - -PyAPI_FUNC(PyObject *) PyEval_EvalCode(PyObject *, PyObject *, PyObject *); - -PyAPI_FUNC(PyObject *) PyEval_EvalCodeEx(PyObject *co, - PyObject *globals, - PyObject *locals, - PyObject *const *args, int argc, - PyObject *const *kwds, int kwdc, - PyObject *const *defs, int defc, - PyObject *kwdefs, PyObject *closure); - -#ifndef Py_LIMITED_API -PyAPI_FUNC(PyObject *) _PyEval_CallTracing(PyObject *func, PyObject *args); -#endif - -#ifdef __cplusplus -} -#endif -#endif /* !Py_EVAL_H */ diff --git a/src/external/windows/include/python/fileobject.h b/src/external/windows/include/python/fileobject.h index cf7ba8df..cc880d5b 100755 --- a/src/external/windows/include/python/fileobject.h +++ b/src/external/windows/include/python/fileobject.h @@ -39,7 +39,7 @@ PyAPI_DATA(int) Py_UTF8Mode; #ifndef Py_LIMITED_API # define Py_CPYTHON_FILEOBJECT_H -# include "cpython/fileobject.h" +# include "cpython/fileobject.h" # undef Py_CPYTHON_FILEOBJECT_H #endif diff --git a/src/external/windows/include/python/fileutils.h b/src/external/windows/include/python/fileutils.h index 9483bd01..3c70e1c9 100755 --- a/src/external/windows/include/python/fileutils.h +++ b/src/external/windows/include/python/fileutils.h @@ -16,7 +16,7 @@ PyAPI_FUNC(char*) Py_EncodeLocale( #ifndef Py_LIMITED_API # define Py_CPYTHON_FILEUTILS_H -# include "cpython/fileutils.h" +# include "cpython/fileutils.h" # undef Py_CPYTHON_FILEUTILS_H #endif diff --git a/src/external/windows/include/python/floatobject.h b/src/external/windows/include/python/floatobject.h index 475f6add..08ebb7b4 100755 --- a/src/external/windows/include/python/floatobject.h +++ b/src/external/windows/include/python/floatobject.h @@ -11,107 +11,43 @@ PyFloatObject represents a (double precision) floating point number. extern "C" { #endif -#ifndef Py_LIMITED_API -typedef struct { - PyObject_HEAD - double ob_fval; -} PyFloatObject; -#endif - PyAPI_DATA(PyTypeObject) PyFloat_Type; #define PyFloat_Check(op) PyObject_TypeCheck(op, &PyFloat_Type) #define PyFloat_CheckExact(op) Py_IS_TYPE(op, &PyFloat_Type) -#ifdef Py_NAN #define Py_RETURN_NAN return PyFloat_FromDouble(Py_NAN) -#endif -#define Py_RETURN_INF(sign) do \ - if (copysign(1., sign) == 1.) { \ - return PyFloat_FromDouble(Py_HUGE_VAL); \ - } else { \ - return PyFloat_FromDouble(-Py_HUGE_VAL); \ +#define Py_RETURN_INF(sign) \ + do { \ + if (copysign(1., sign) == 1.) { \ + return PyFloat_FromDouble(Py_HUGE_VAL); \ + } \ + else { \ + return PyFloat_FromDouble(-Py_HUGE_VAL); \ + } \ } while(0) PyAPI_FUNC(double) PyFloat_GetMax(void); PyAPI_FUNC(double) PyFloat_GetMin(void); -PyAPI_FUNC(PyObject *) PyFloat_GetInfo(void); +PyAPI_FUNC(PyObject*) PyFloat_GetInfo(void); /* Return Python float from string PyObject. */ -PyAPI_FUNC(PyObject *) PyFloat_FromString(PyObject*); +PyAPI_FUNC(PyObject*) PyFloat_FromString(PyObject*); /* Return Python float from C double. */ -PyAPI_FUNC(PyObject *) PyFloat_FromDouble(double); +PyAPI_FUNC(PyObject*) PyFloat_FromDouble(double); /* Extract C double from Python float. The macro version trades safety for speed. */ -PyAPI_FUNC(double) PyFloat_AsDouble(PyObject *); +PyAPI_FUNC(double) PyFloat_AsDouble(PyObject*); + #ifndef Py_LIMITED_API -#define PyFloat_AS_DOUBLE(op) (((PyFloatObject *)(op))->ob_fval) +# define Py_CPYTHON_FLOATOBJECT_H +# include "cpython/floatobject.h" +# undef Py_CPYTHON_FLOATOBJECT_H #endif -#ifndef Py_LIMITED_API -/* _PyFloat_{Pack,Unpack}{4,8} - * - * The struct and pickle (at least) modules need an efficient platform- - * independent way to store floating-point values as byte strings. - * The Pack routines produce a string from a C double, and the Unpack - * routines produce a C double from such a string. The suffix (4 or 8) - * specifies the number of bytes in the string. - * - * On platforms that appear to use (see _PyFloat_Init()) IEEE-754 formats - * these functions work by copying bits. On other platforms, the formats the - * 4- byte format is identical to the IEEE-754 single precision format, and - * the 8-byte format to the IEEE-754 double precision format, although the - * packing of INFs and NaNs (if such things exist on the platform) isn't - * handled correctly, and attempting to unpack a string containing an IEEE - * INF or NaN will raise an exception. - * - * On non-IEEE platforms with more precision, or larger dynamic range, than - * 754 supports, not all values can be packed; on non-IEEE platforms with less - * precision, or smaller dynamic range, not all values can be unpacked. What - * happens in such cases is partly accidental (alas). - */ - -/* The pack routines write 2, 4 or 8 bytes, starting at p. le is a bool - * argument, true if you want the string in little-endian format (exponent - * last, at p+1, p+3 or p+7), false if you want big-endian format (exponent - * first, at p). - * Return value: 0 if all is OK, -1 if error (and an exception is - * set, most likely OverflowError). - * There are two problems on non-IEEE platforms: - * 1): What this does is undefined if x is a NaN or infinity. - * 2): -0.0 and +0.0 produce the same string. - */ -PyAPI_FUNC(int) _PyFloat_Pack2(double x, unsigned char *p, int le); -PyAPI_FUNC(int) _PyFloat_Pack4(double x, unsigned char *p, int le); -PyAPI_FUNC(int) _PyFloat_Pack8(double x, unsigned char *p, int le); - -/* The unpack routines read 2, 4 or 8 bytes, starting at p. le is a bool - * argument, true if the string is in little-endian format (exponent - * last, at p+1, p+3 or p+7), false if big-endian (exponent first, at p). - * Return value: The unpacked double. On error, this is -1.0 and - * PyErr_Occurred() is true (and an exception is set, most likely - * OverflowError). Note that on a non-IEEE platform this will refuse - * to unpack a string that represents a NaN or infinity. - */ -PyAPI_FUNC(double) _PyFloat_Unpack2(const unsigned char *p, int le); -PyAPI_FUNC(double) _PyFloat_Unpack4(const unsigned char *p, int le); -PyAPI_FUNC(double) _PyFloat_Unpack8(const unsigned char *p, int le); - -PyAPI_FUNC(void) _PyFloat_DebugMallocStats(FILE* out); - -/* Format the object based on the format_spec, as defined in PEP 3101 - (Advanced String Formatting). */ -PyAPI_FUNC(int) _PyFloat_FormatAdvancedWriter( - _PyUnicodeWriter *writer, - PyObject *obj, - PyObject *format_spec, - Py_ssize_t start, - Py_ssize_t end); -#endif /* Py_LIMITED_API */ - #ifdef __cplusplus } #endif diff --git a/src/external/windows/include/python/frameobject.h b/src/external/windows/include/python/frameobject.h index 36e2ab33..ad186c7d 100755 --- a/src/external/windows/include/python/frameobject.h +++ b/src/external/windows/include/python/frameobject.h @@ -10,7 +10,7 @@ extern "C" { #ifndef Py_LIMITED_API # define Py_CPYTHON_FRAMEOBJECT_H -# include "cpython/frameobject.h" +# include "cpython/frameobject.h" # undef Py_CPYTHON_FRAMEOBJECT_H #endif diff --git a/src/external/windows/include/python/import.h b/src/external/windows/include/python/import.h index 468a6734..50142586 100755 --- a/src/external/windows/include/python/import.h +++ b/src/external/windows/include/python/import.h @@ -88,7 +88,7 @@ PyAPI_FUNC(int) PyImport_AppendInittab( #ifndef Py_LIMITED_API # define Py_CPYTHON_IMPORT_H -# include "cpython/import.h" +# include "cpython/import.h" # undef Py_CPYTHON_IMPORT_H #endif diff --git a/src/external/windows/include/python/internal/pycore_abstract.h b/src/external/windows/include/python/internal/pycore_abstract.h index 15aa7f58..96e82efd 100755 --- a/src/external/windows/include/python/internal/pycore_abstract.h +++ b/src/external/windows/include/python/internal/pycore_abstract.h @@ -16,6 +16,9 @@ _PyIndex_Check(PyObject *obj) return (tp_as_number != NULL && tp_as_number->nb_index != NULL); } +PyObject *_PyNumber_PowerNoMod(PyObject *lhs, PyObject *rhs); +PyObject *_PyNumber_InPlacePowerNoMod(PyObject *lhs, PyObject *rhs); + #ifdef __cplusplus } #endif diff --git a/src/external/windows/include/python/internal/pycore_asdl.h b/src/external/windows/include/python/internal/pycore_asdl.h index a2cb8c08..39420236 100755 --- a/src/external/windows/include/python/internal/pycore_asdl.h +++ b/src/external/windows/include/python/internal/pycore_asdl.h @@ -78,9 +78,9 @@ asdl_ ## NAME ## _seq *_Py_asdl_ ## NAME ## _seq_new(Py_ssize_t size, PyArena *a return seq; \ } -#define asdl_seq_GET_UNTYPED(S, I) (S)->elements[(I)] -#define asdl_seq_GET(S, I) (S)->typed_elements[(I)] -#define asdl_seq_LEN(S) ((S) == NULL ? 0 : (S)->size) +#define asdl_seq_GET_UNTYPED(S, I) _Py_RVALUE((S)->elements[(I)]) +#define asdl_seq_GET(S, I) _Py_RVALUE((S)->typed_elements[(I)]) +#define asdl_seq_LEN(S) _Py_RVALUE(((S) == NULL ? 0 : (S)->size)) #ifdef Py_DEBUG # define asdl_seq_SET(S, I, V) \ @@ -91,7 +91,7 @@ asdl_ ## NAME ## _seq *_Py_asdl_ ## NAME ## _seq_new(Py_ssize_t size, PyArena *a (S)->typed_elements[_asdl_i] = (V); \ } while (0) #else -# define asdl_seq_SET(S, I, V) (S)->typed_elements[I] = (V) +# define asdl_seq_SET(S, I, V) _Py_RVALUE((S)->typed_elements[I] = (V)) #endif #ifdef Py_DEBUG @@ -103,7 +103,7 @@ asdl_ ## NAME ## _seq *_Py_asdl_ ## NAME ## _seq_new(Py_ssize_t size, PyArena *a (S)->elements[_asdl_i] = (V); \ } while (0) #else -# define asdl_seq_SET_UNTYPED(S, I, V) (S)->elements[I] = (V) +# define asdl_seq_SET_UNTYPED(S, I, V) _Py_RVALUE((S)->elements[I] = (V)) #endif #ifdef __cplusplus diff --git a/src/external/windows/include/python/internal/pycore_ast.h b/src/external/windows/include/python/internal/pycore_ast.h index 4e5a5c7a..1e03bf99 100755 --- a/src/external/windows/include/python/internal/pycore_ast.h +++ b/src/external/windows/include/python/internal/pycore_ast.h @@ -179,9 +179,9 @@ enum _stmt_kind {FunctionDef_kind=1, AsyncFunctionDef_kind=2, ClassDef_kind=3, AugAssign_kind=7, AnnAssign_kind=8, For_kind=9, AsyncFor_kind=10, While_kind=11, If_kind=12, With_kind=13, AsyncWith_kind=14, Match_kind=15, Raise_kind=16, Try_kind=17, - Assert_kind=18, Import_kind=19, ImportFrom_kind=20, - Global_kind=21, Nonlocal_kind=22, Expr_kind=23, Pass_kind=24, - Break_kind=25, Continue_kind=26}; + TryStar_kind=18, Assert_kind=19, Import_kind=20, + ImportFrom_kind=21, Global_kind=22, Nonlocal_kind=23, + Expr_kind=24, Pass_kind=25, Break_kind=26, Continue_kind=27}; struct _stmt { enum _stmt_kind kind; union { @@ -295,6 +295,13 @@ struct _stmt { asdl_stmt_seq *finalbody; } Try; + struct { + asdl_stmt_seq *body; + asdl_excepthandler_seq *handlers; + asdl_stmt_seq *orelse; + asdl_stmt_seq *finalbody; + } TryStar; + struct { expr_ty test; expr_ty msg; @@ -688,6 +695,10 @@ stmt_ty _PyAST_Try(asdl_stmt_seq * body, asdl_excepthandler_seq * handlers, asdl_stmt_seq * orelse, asdl_stmt_seq * finalbody, int lineno, int col_offset, int end_lineno, int end_col_offset, PyArena *arena); +stmt_ty _PyAST_TryStar(asdl_stmt_seq * body, asdl_excepthandler_seq * handlers, + asdl_stmt_seq * orelse, asdl_stmt_seq * finalbody, int + lineno, int col_offset, int end_lineno, int + end_col_offset, PyArena *arena); stmt_ty _PyAST_Assert(expr_ty test, expr_ty msg, int lineno, int col_offset, int end_lineno, int end_col_offset, PyArena *arena); stmt_ty _PyAST_Import(asdl_alias_seq * names, int lineno, int col_offset, int diff --git a/src/external/windows/include/python/internal/pycore_ast_state.h b/src/external/windows/include/python/internal/pycore_ast_state.h index 9080f0f8..7ec15d84 100755 --- a/src/external/windows/include/python/internal/pycore_ast_state.h +++ b/src/external/windows/include/python/internal/pycore_ast_state.h @@ -12,6 +12,8 @@ extern "C" { struct ast_state { int initialized; + int recursion_depth; + int recursion_limit; PyObject *AST_type; PyObject *Add_singleton; PyObject *Add_type; @@ -132,6 +134,7 @@ struct ast_state { PyObject *Sub_singleton; PyObject *Sub_type; PyObject *Subscript_type; + PyObject *TryStar_type; PyObject *Try_type; PyObject *Tuple_type; PyObject *TypeIgnore_type; diff --git a/src/external/windows/include/python/internal/pycore_atomic.h b/src/external/windows/include/python/internal/pycore_atomic.h index f0983f34..34536f5f 100755 --- a/src/external/windows/include/python/internal/pycore_atomic.h +++ b/src/external/windows/include/python/internal/pycore_atomic.h @@ -236,7 +236,7 @@ _Py_ANNOTATE_MEMORY_ORDER(const volatile void *address, _Py_memory_order order) in hardware they will fall back to a full memory barrier as well. This might affect performance but likely only in some very specific and - hard to meassure scenario. + hard to measure scenario. */ #if defined(_M_IX86) || defined(_M_X64) typedef enum _Py_memory_order { diff --git a/src/external/windows/include/python/internal/pycore_bitutils.h b/src/external/windows/include/python/internal/pycore_bitutils.h index 41bd024e..040dbc55 100755 --- a/src/external/windows/include/python/internal/pycore_bitutils.h +++ b/src/external/windows/include/python/internal/pycore_bitutils.h @@ -115,8 +115,6 @@ _Py_popcount32(uint32_t x) const uint32_t M2 = 0x33333333; // Binary: 0000 1111 0000 1111 ... const uint32_t M4 = 0x0F0F0F0F; - // 256**4 + 256**3 + 256**2 + 256**1 - const uint32_t SUM = 0x01010101; // Put count of each 2 bits into those 2 bits x = x - ((x >> 1) & M1); @@ -124,8 +122,20 @@ _Py_popcount32(uint32_t x) x = (x & M2) + ((x >> 2) & M2); // Put count of each 8 bits into those 8 bits x = (x + (x >> 4)) & M4; - // Sum of the 4 byte counts - return (uint32_t)((uint64_t)x * (uint64_t)SUM) >> 24; + // Sum of the 4 byte counts. + // Take care when considering changes to the next line. Portability and + // correctness are delicate here, thanks to C's "integer promotions" (C99 + // §6.3.1.1p2). On machines where the `int` type has width greater than 32 + // bits, `x` will be promoted to an `int`, and following C's "usual + // arithmetic conversions" (C99 §6.3.1.8), the multiplication will be + // performed as a multiplication of two `unsigned int` operands. In this + // case it's critical that we cast back to `uint32_t` in order to keep only + // the least significant 32 bits. On machines where the `int` type has + // width no greater than 32, the multiplication is of two 32-bit unsigned + // integer types, and the (uint32_t) cast is a no-op. In both cases, we + // avoid the risk of undefined behaviour due to overflow of a + // multiplication of signed integer types. + return (uint32_t)(x * 0x01010101U) >> 24; #endif } diff --git a/src/external/windows/include/python/internal/pycore_bytesobject.h b/src/external/windows/include/python/internal/pycore_bytesobject.h new file mode 100644 index 00000000..905a541c --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_bytesobject.h @@ -0,0 +1,52 @@ +#ifndef Py_INTERNAL_BYTESOBJECT_H +#define Py_INTERNAL_BYTESOBJECT_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + + +/* runtime lifecycle */ + +extern PyStatus _PyBytes_InitTypes(PyInterpreterState *); + + +/* Substring Search. + + Returns the index of the first occurrence of + a substring ("needle") in a larger text ("haystack"). + If the needle is not found, return -1. + If the needle is found, add offset to the index. +*/ + +PyAPI_FUNC(Py_ssize_t) +_PyBytes_Find(const char *haystack, Py_ssize_t len_haystack, + const char *needle, Py_ssize_t len_needle, + Py_ssize_t offset); + +/* Same as above, but search right-to-left */ +PyAPI_FUNC(Py_ssize_t) +_PyBytes_ReverseFind(const char *haystack, Py_ssize_t len_haystack, + const char *needle, Py_ssize_t len_needle, + Py_ssize_t offset); + + +/** Helper function to implement the repeat and inplace repeat methods on a buffer + * + * len_dest is assumed to be an integer multiple of len_src. + * If src equals dest, then assume the operation is inplace. + * + * This method repeately doubles the number of bytes copied to reduce + * the number of invocations of memcpy. + */ +PyAPI_FUNC(void) +_PyBytes_Repeat(char* dest, Py_ssize_t len_dest, + const char* src, Py_ssize_t len_src); + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_BYTESOBJECT_H */ diff --git a/src/external/windows/include/python/internal/pycore_call.h b/src/external/windows/include/python/internal/pycore_call.h index 6b301264..51c25628 100755 --- a/src/external/windows/include/python/internal/pycore_call.h +++ b/src/external/windows/include/python/internal/pycore_call.h @@ -8,6 +8,8 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif +#include "pycore_pystate.h" // _PyThreadState_GET() + PyAPI_FUNC(PyObject *) _PyObject_Call_Prepend( PyThreadState *tstate, PyObject *callable, @@ -28,11 +30,91 @@ PyAPI_FUNC(PyObject *) _PyObject_Call( PyObject *args, PyObject *kwargs); +extern PyObject * _PyObject_CallMethodFormat( + PyThreadState *tstate, PyObject *callable, const char *format, ...); + + +// Static inline variant of public PyVectorcall_Function(). +static inline vectorcallfunc +_PyVectorcall_FunctionInline(PyObject *callable) +{ + assert(callable != NULL); + + PyTypeObject *tp = Py_TYPE(callable); + if (!PyType_HasFeature(tp, Py_TPFLAGS_HAVE_VECTORCALL)) { + return NULL; + } + assert(PyCallable_Check(callable)); + + Py_ssize_t offset = tp->tp_vectorcall_offset; + assert(offset > 0); + + vectorcallfunc ptr; + memcpy(&ptr, (char *) callable + offset, sizeof(ptr)); + return ptr; +} + + +/* Call the callable object 'callable' with the "vectorcall" calling + convention. + + args is a C array for positional arguments. + + nargsf is the number of positional arguments plus optionally the flag + PY_VECTORCALL_ARGUMENTS_OFFSET which means that the caller is allowed to + modify args[-1]. + + kwnames is a tuple of keyword names. The values of the keyword arguments + are stored in "args" after the positional arguments (note that the number + of keyword arguments does not change nargsf). kwnames can also be NULL if + there are no keyword arguments. + + keywords must only contain strings and all keys must be unique. + + Return the result on success. Raise an exception and return NULL on + error. */ static inline PyObject * -_PyObject_CallNoArgTstate(PyThreadState *tstate, PyObject *func) { +_PyObject_VectorcallTstate(PyThreadState *tstate, PyObject *callable, + PyObject *const *args, size_t nargsf, + PyObject *kwnames) +{ + vectorcallfunc func; + PyObject *res; + + assert(kwnames == NULL || PyTuple_Check(kwnames)); + assert(args != NULL || PyVectorcall_NARGS(nargsf) == 0); + + func = _PyVectorcall_FunctionInline(callable); + if (func == NULL) { + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + return _PyObject_MakeTpCall(tstate, callable, args, nargs, kwnames); + } + res = func(callable, args, nargsf, kwnames); + return _Py_CheckFunctionResult(tstate, callable, res, NULL); +} + + +static inline PyObject * +_PyObject_CallNoArgsTstate(PyThreadState *tstate, PyObject *func) { return _PyObject_VectorcallTstate(tstate, func, NULL, 0, NULL); } + +// Private static inline function variant of public PyObject_CallNoArgs() +static inline PyObject * +_PyObject_CallNoArgs(PyObject *func) { + PyThreadState *tstate = _PyThreadState_GET(); + return _PyObject_VectorcallTstate(tstate, func, NULL, 0, NULL); +} + + +static inline PyObject * +_PyObject_FastCallTstate(PyThreadState *tstate, PyObject *func, PyObject *const *args, Py_ssize_t nargs) +{ + return _PyObject_VectorcallTstate(tstate, func, args, (size_t)nargs, NULL); +} + + #ifdef __cplusplus } #endif diff --git a/src/external/windows/include/python/internal/pycore_ceval.h b/src/external/windows/include/python/internal/pycore_ceval.h index be534a3c..6b1adb50 100755 --- a/src/external/windows/include/python/internal/pycore_ceval.h +++ b/src/external/windows/include/python/internal/pycore_ceval.h @@ -12,11 +12,24 @@ extern "C" { struct pyruntimestate; struct _ceval_runtime_state; -#include "pycore_interp.h" /* PyInterpreterState.eval_frame */ +/* WASI has limited call stack. Python's recursion limit depends on code + layout, optimization, and WASI runtime. Wasmtime can handle about 700-750 + recursions, sometimes less. 600 is a more conservative limit. */ +#ifndef Py_DEFAULT_RECURSION_LIMIT +# ifdef __wasi__ +# define Py_DEFAULT_RECURSION_LIMIT 600 +# else +# define Py_DEFAULT_RECURSION_LIMIT 1000 +# endif +#endif + +#include "pycore_interp.h" // PyInterpreterState.eval_frame +#include "pycore_pystate.h" // _PyThreadState_GET() + extern void _Py_FinishPendingCalls(PyThreadState *tstate); extern void _PyEval_InitRuntimeState(struct _ceval_runtime_state *); -extern int _PyEval_InitState(struct _ceval_state *ceval); +extern void _PyEval_InitState(struct _ceval_state *, PyThread_type_lock); extern void _PyEval_FiniState(struct _ceval_state *ceval); PyAPI_FUNC(void) _PyEval_SignalReceived(PyInterpreterState *interp); PyAPI_FUNC(int) _PyEval_AddPendingCall( @@ -27,36 +40,48 @@ PyAPI_FUNC(void) _PyEval_SignalAsyncExc(PyInterpreterState *interp); #ifdef HAVE_FORK extern PyStatus _PyEval_ReInitThreads(PyThreadState *tstate); #endif -PyAPI_FUNC(void) _PyEval_SetCoroutineOriginTrackingDepth( - PyThreadState *tstate, - int new_depth); -void _PyEval_Fini(void); +// Used by sys.call_tracing() +extern PyObject* _PyEval_CallTracing(PyObject *func, PyObject *args); + +// Used by sys.get_asyncgen_hooks() +extern PyObject* _PyEval_GetAsyncGenFirstiter(void); +extern PyObject* _PyEval_GetAsyncGenFinalizer(void); + +// Used by sys.set_asyncgen_hooks() +extern int _PyEval_SetAsyncGenFirstiter(PyObject *); +extern int _PyEval_SetAsyncGenFinalizer(PyObject *); + +// Used by sys.get_coroutine_origin_tracking_depth() +// and sys.set_coroutine_origin_tracking_depth() +extern int _PyEval_GetCoroutineOriginTrackingDepth(void); +extern int _PyEval_SetCoroutineOriginTrackingDepth(int depth); + +extern void _PyEval_Fini(void); extern PyObject* _PyEval_GetBuiltins(PyThreadState *tstate); -extern PyObject *_PyEval_BuiltinsFromGlobals( +extern PyObject* _PyEval_BuiltinsFromGlobals( PyThreadState *tstate, PyObject *globals); static inline PyObject* -_PyEval_EvalFrame(PyThreadState *tstate, PyFrameObject *f, int throwflag) +_PyEval_EvalFrame(PyThreadState *tstate, struct _PyInterpreterFrame *frame, int throwflag) { - return tstate->interp->eval_frame(tstate, f, throwflag); + if (tstate->interp->eval_frame == NULL) { + return _PyEval_EvalFrameDefault(tstate, frame, throwflag); + } + return tstate->interp->eval_frame(tstate, frame, throwflag); } -extern PyObject * +extern PyObject* _PyEval_Vector(PyThreadState *tstate, - PyFrameConstructor *desc, PyObject *locals, + PyFunctionObject *func, PyObject *locals, PyObject* const* args, size_t argcount, PyObject *kwnames); -#ifdef EXPERIMENTAL_ISOLATED_SUBINTERPRETERS -extern int _PyEval_ThreadsInitialized(PyInterpreterState *interp); -#else extern int _PyEval_ThreadsInitialized(struct pyruntimestate *runtime); -#endif extern PyStatus _PyEval_InitGIL(PyThreadState *tstate); extern void _PyEval_FiniGIL(PyInterpreterState *interp); @@ -69,14 +94,14 @@ extern void _PyEval_DeactivateOpCache(void); #ifdef USE_STACKCHECK /* With USE_STACKCHECK macro defined, trigger stack checks in - _Py_CheckRecursiveCall() on every 64th call to Py_EnterRecursiveCall. */ + _Py_CheckRecursiveCall() on every 64th call to _Py_EnterRecursiveCall. */ static inline int _Py_MakeRecCheck(PyThreadState *tstate) { - return (++tstate->recursion_depth > tstate->interp->ceval.recursion_limit - || ++tstate->stackcheck_counter > 64); + return (tstate->recursion_remaining-- <= 0 + || (tstate->recursion_remaining & 63) == 0); } #else static inline int _Py_MakeRecCheck(PyThreadState *tstate) { - return (++tstate->recursion_depth > tstate->interp->ceval.recursion_limit); + return tstate->recursion_remaining-- <= 0; } #endif @@ -84,29 +109,28 @@ PyAPI_FUNC(int) _Py_CheckRecursiveCall( PyThreadState *tstate, const char *where); -static inline int _Py_EnterRecursiveCall(PyThreadState *tstate, - const char *where) { +static inline int _Py_EnterRecursiveCallTstate(PyThreadState *tstate, + const char *where) { return (_Py_MakeRecCheck(tstate) && _Py_CheckRecursiveCall(tstate, where)); } -static inline int _Py_EnterRecursiveCall_inline(const char *where) { - PyThreadState *tstate = PyThreadState_GET(); - return _Py_EnterRecursiveCall(tstate, where); +static inline int _Py_EnterRecursiveCall(const char *where) { + PyThreadState *tstate = _PyThreadState_GET(); + return _Py_EnterRecursiveCallTstate(tstate, where); } -#define Py_EnterRecursiveCall(where) _Py_EnterRecursiveCall_inline(where) - -static inline void _Py_LeaveRecursiveCall(PyThreadState *tstate) { - tstate->recursion_depth--; +static inline void _Py_LeaveRecursiveCallTstate(PyThreadState *tstate) { + tstate->recursion_remaining++; } -static inline void _Py_LeaveRecursiveCall_inline(void) { - PyThreadState *tstate = PyThreadState_GET(); - _Py_LeaveRecursiveCall(tstate); +static inline void _Py_LeaveRecursiveCall(void) { + PyThreadState *tstate = _PyThreadState_GET(); + _Py_LeaveRecursiveCallTstate(tstate); } -#define Py_LeaveRecursiveCall() _Py_LeaveRecursiveCall_inline() +extern struct _PyInterpreterFrame* _PyEval_GetFrame(void); +extern PyObject* _Py_MakeCoro(PyFunctionObject *func); #ifdef __cplusplus } diff --git a/src/external/windows/include/python/internal/pycore_code.h b/src/external/windows/include/python/internal/pycore_code.h index 69baba09..c35a4377 100755 --- a/src/external/windows/include/python/internal/pycore_code.h +++ b/src/external/windows/include/python/internal/pycore_code.h @@ -4,28 +4,558 @@ extern "C" { #endif -typedef struct { - PyObject *ptr; /* Cached pointer (borrowed reference) */ - uint64_t globals_ver; /* ma_version of global dict */ - uint64_t builtins_ver; /* ma_version of builtin dict */ -} _PyOpcache_LoadGlobal; +/* PEP 659 + * Specialization and quickening structs and helper functions + */ + + +// Inline caches. If you change the number of cache entries for an instruction, +// you must *also* update the number of cache entries in Lib/opcode.py and bump +// the magic number in Lib/importlib/_bootstrap_external.py! + +#define CACHE_ENTRIES(cache) (sizeof(cache)/sizeof(_Py_CODEUNIT)) typedef struct { - PyTypeObject *type; - Py_ssize_t hint; - unsigned int tp_version_tag; -} _PyOpCodeOpt_LoadAttr; + _Py_CODEUNIT counter; + _Py_CODEUNIT index; + _Py_CODEUNIT module_keys_version[2]; + _Py_CODEUNIT builtin_keys_version; +} _PyLoadGlobalCache; -struct _PyOpcache { - union { - _PyOpcache_LoadGlobal lg; - _PyOpCodeOpt_LoadAttr la; - } u; - char optimized; +#define INLINE_CACHE_ENTRIES_LOAD_GLOBAL CACHE_ENTRIES(_PyLoadGlobalCache) + +typedef struct { + _Py_CODEUNIT counter; +} _PyBinaryOpCache; + +#define INLINE_CACHE_ENTRIES_BINARY_OP CACHE_ENTRIES(_PyBinaryOpCache) + +typedef struct { + _Py_CODEUNIT counter; +} _PyUnpackSequenceCache; + +#define INLINE_CACHE_ENTRIES_UNPACK_SEQUENCE \ + CACHE_ENTRIES(_PyUnpackSequenceCache) + +typedef struct { + _Py_CODEUNIT counter; + _Py_CODEUNIT mask; +} _PyCompareOpCache; + +#define INLINE_CACHE_ENTRIES_COMPARE_OP CACHE_ENTRIES(_PyCompareOpCache) + +typedef struct { + _Py_CODEUNIT counter; + _Py_CODEUNIT type_version[2]; + _Py_CODEUNIT func_version; +} _PyBinarySubscrCache; + +#define INLINE_CACHE_ENTRIES_BINARY_SUBSCR CACHE_ENTRIES(_PyBinarySubscrCache) + +typedef struct { + _Py_CODEUNIT counter; + _Py_CODEUNIT version[2]; + _Py_CODEUNIT index; +} _PyAttrCache; + +#define INLINE_CACHE_ENTRIES_LOAD_ATTR CACHE_ENTRIES(_PyAttrCache) + +#define INLINE_CACHE_ENTRIES_STORE_ATTR CACHE_ENTRIES(_PyAttrCache) + +typedef struct { + _Py_CODEUNIT counter; + _Py_CODEUNIT type_version[2]; + _Py_CODEUNIT dict_offset; + _Py_CODEUNIT keys_version[2]; + _Py_CODEUNIT descr[4]; +} _PyLoadMethodCache; + +#define INLINE_CACHE_ENTRIES_LOAD_METHOD CACHE_ENTRIES(_PyLoadMethodCache) + +typedef struct { + _Py_CODEUNIT counter; + _Py_CODEUNIT func_version[2]; + _Py_CODEUNIT min_args; +} _PyCallCache; + +#define INLINE_CACHE_ENTRIES_CALL CACHE_ENTRIES(_PyCallCache) + +typedef struct { + _Py_CODEUNIT counter; +} _PyPrecallCache; + +#define INLINE_CACHE_ENTRIES_PRECALL CACHE_ENTRIES(_PyPrecallCache) + +typedef struct { + _Py_CODEUNIT counter; +} _PyStoreSubscrCache; + +#define INLINE_CACHE_ENTRIES_STORE_SUBSCR CACHE_ENTRIES(_PyStoreSubscrCache) + +#define QUICKENING_WARMUP_DELAY 8 + +/* We want to compare to zero for efficiency, so we offset values accordingly */ +#define QUICKENING_INITIAL_WARMUP_VALUE (-QUICKENING_WARMUP_DELAY) + +void _PyCode_Quicken(PyCodeObject *code); + +static inline void +_PyCode_Warmup(PyCodeObject *code) +{ + if (code->co_warmup != 0) { + code->co_warmup++; + if (code->co_warmup == 0) { + _PyCode_Quicken(code); + } + } +} + +extern uint8_t _PyOpcode_Adaptive[256]; + +extern Py_ssize_t _Py_QuickenedCount; + +// Borrowed references to common callables: +struct callable_cache { + PyObject *isinstance; + PyObject *len; + PyObject *list_append; }; +/* "Locals plus" for a code object is the set of locals + cell vars + + * free vars. This relates to variable names as well as offsets into + * the "fast locals" storage array of execution frames. The compiler + * builds the list of names, their offsets, and the corresponding + * kind of local. + * + * Those kinds represent the source of the initial value and the + * variable's scope (as related to closures). A "local" is an + * argument or other variable defined in the current scope. A "free" + * variable is one that is defined in an outer scope and comes from + * the function's closure. A "cell" variable is a local that escapes + * into an inner function as part of a closure, and thus must be + * wrapped in a cell. Any "local" can also be a "cell", but the + * "free" kind is mutually exclusive with both. + */ + +// Note that these all fit within a byte, as do combinations. +// Later, we will use the smaller numbers to differentiate the different +// kinds of locals (e.g. pos-only arg, varkwargs, local-only). +#define CO_FAST_LOCAL 0x20 +#define CO_FAST_CELL 0x40 +#define CO_FAST_FREE 0x80 + +typedef unsigned char _PyLocals_Kind; + +static inline _PyLocals_Kind +_PyLocals_GetKind(PyObject *kinds, int i) +{ + assert(PyBytes_Check(kinds)); + assert(0 <= i && i < PyBytes_GET_SIZE(kinds)); + char *ptr = PyBytes_AS_STRING(kinds); + return (_PyLocals_Kind)(ptr[i]); +} + +static inline void +_PyLocals_SetKind(PyObject *kinds, int i, _PyLocals_Kind kind) +{ + assert(PyBytes_Check(kinds)); + assert(0 <= i && i < PyBytes_GET_SIZE(kinds)); + char *ptr = PyBytes_AS_STRING(kinds); + ptr[i] = (char) kind; +} + + +struct _PyCodeConstructor { + /* metadata */ + PyObject *filename; + PyObject *name; + PyObject *qualname; + int flags; + + /* the code */ + PyObject *code; + int firstlineno; + PyObject *linetable; + + /* used by the code */ + PyObject *consts; + PyObject *names; + + /* mapping frame offsets to information */ + PyObject *localsplusnames; // Tuple of strings + PyObject *localspluskinds; // Bytes object, one byte per variable + + /* args (within varnames) */ + int argcount; + int posonlyargcount; + // XXX Replace argcount with posorkwargcount (argcount - posonlyargcount). + int kwonlyargcount; + + /* needed to create the frame */ + int stacksize; + + /* used by the eval loop */ + PyObject *exceptiontable; +}; + +// Using an "arguments struct" like this is helpful for maintainability +// in a case such as this with many parameters. It does bear a risk: +// if the struct changes and callers are not updated properly then the +// compiler will not catch problems (like a missing argument). This can +// cause hard-to-debug problems. The risk is mitigated by the use of +// check_code() in codeobject.c. However, we may decide to switch +// back to a regular function signature. Regardless, this approach +// wouldn't be appropriate if this weren't a strictly internal API. +// (See the comments in https://github.com/python/cpython/pull/26258.) +PyAPI_FUNC(int) _PyCode_Validate(struct _PyCodeConstructor *); +PyAPI_FUNC(PyCodeObject *) _PyCode_New(struct _PyCodeConstructor *); + + /* Private API */ -int _PyCode_InitOpcache(PyCodeObject *co); + +/* Getters for internal PyCodeObject data. */ +extern PyObject* _PyCode_GetVarnames(PyCodeObject *); +extern PyObject* _PyCode_GetCellvars(PyCodeObject *); +extern PyObject* _PyCode_GetFreevars(PyCodeObject *); +extern PyObject* _PyCode_GetCode(PyCodeObject *); + +/** API for initializing the line number tables. */ +extern int _PyCode_InitAddressRange(PyCodeObject* co, PyCodeAddressRange *bounds); + +/** Out of process API for initializing the location table. */ +extern void _PyLineTable_InitAddressRange( + const char *linetable, + Py_ssize_t length, + int firstlineno, + PyCodeAddressRange *range); + +/** API for traversing the line number table. */ +extern int _PyLineTable_NextAddressRange(PyCodeAddressRange *range); +extern int _PyLineTable_PreviousAddressRange(PyCodeAddressRange *range); + +/* Specialization functions */ + +extern int _Py_Specialize_LoadAttr(PyObject *owner, _Py_CODEUNIT *instr, + PyObject *name); +extern int _Py_Specialize_StoreAttr(PyObject *owner, _Py_CODEUNIT *instr, + PyObject *name); +extern int _Py_Specialize_LoadGlobal(PyObject *globals, PyObject *builtins, _Py_CODEUNIT *instr, PyObject *name); +extern int _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, + PyObject *name); +extern int _Py_Specialize_BinarySubscr(PyObject *sub, PyObject *container, _Py_CODEUNIT *instr); +extern int _Py_Specialize_StoreSubscr(PyObject *container, PyObject *sub, _Py_CODEUNIT *instr); +extern int _Py_Specialize_Call(PyObject *callable, _Py_CODEUNIT *instr, + int nargs, PyObject *kwnames); +extern int _Py_Specialize_Precall(PyObject *callable, _Py_CODEUNIT *instr, + int nargs, PyObject *kwnames, int oparg); +extern void _Py_Specialize_BinaryOp(PyObject *lhs, PyObject *rhs, _Py_CODEUNIT *instr, + int oparg, PyObject **locals); +extern void _Py_Specialize_CompareOp(PyObject *lhs, PyObject *rhs, + _Py_CODEUNIT *instr, int oparg); +extern void _Py_Specialize_UnpackSequence(PyObject *seq, _Py_CODEUNIT *instr, + int oparg); + +/* Deallocator function for static codeobjects used in deepfreeze.py */ +extern void _PyStaticCode_Dealloc(PyCodeObject *co); +/* Function to intern strings of codeobjects */ +extern int _PyStaticCode_InternStrings(PyCodeObject *co); + +#ifdef Py_STATS + +#define SPECIALIZATION_FAILURE_KINDS 30 + +typedef struct _specialization_stats { + uint64_t success; + uint64_t failure; + uint64_t hit; + uint64_t deferred; + uint64_t miss; + uint64_t deopt; + uint64_t failure_kinds[SPECIALIZATION_FAILURE_KINDS]; +} SpecializationStats; + +typedef struct _opcode_stats { + SpecializationStats specialization; + uint64_t execution_count; + uint64_t pair_count[256]; +} OpcodeStats; + +typedef struct _call_stats { + uint64_t inlined_py_calls; + uint64_t pyeval_calls; + uint64_t frames_pushed; + uint64_t frame_objects_created; +} CallStats; + +typedef struct _object_stats { + uint64_t allocations; + uint64_t allocations512; + uint64_t allocations4k; + uint64_t allocations_big; + uint64_t frees; + uint64_t to_freelist; + uint64_t from_freelist; + uint64_t new_values; + uint64_t dict_materialized_on_request; + uint64_t dict_materialized_new_key; + uint64_t dict_materialized_too_big; + uint64_t dict_materialized_str_subclass; +} ObjectStats; + +typedef struct _stats { + OpcodeStats opcode_stats[256]; + CallStats call_stats; + ObjectStats object_stats; +} PyStats; + +extern PyStats _py_stats; + +#define STAT_INC(opname, name) _py_stats.opcode_stats[opname].specialization.name++ +#define STAT_DEC(opname, name) _py_stats.opcode_stats[opname].specialization.name-- +#define OPCODE_EXE_INC(opname) _py_stats.opcode_stats[opname].execution_count++ +#define CALL_STAT_INC(name) _py_stats.call_stats.name++ +#define OBJECT_STAT_INC(name) _py_stats.object_stats.name++ +#define OBJECT_STAT_INC_COND(name, cond) \ + do { if (cond) _py_stats.object_stats.name++; } while (0) + +extern void _Py_PrintSpecializationStats(int to_file); + +// Used by the _opcode extension which is built as a shared library +PyAPI_FUNC(PyObject*) _Py_GetSpecializationStats(void); + +#else +#define STAT_INC(opname, name) ((void)0) +#define STAT_DEC(opname, name) ((void)0) +#define OPCODE_EXE_INC(opname) ((void)0) +#define CALL_STAT_INC(name) ((void)0) +#define OBJECT_STAT_INC(name) ((void)0) +#define OBJECT_STAT_INC_COND(name, cond) ((void)0) +#endif // !Py_STATS + +// Cache values are only valid in memory, so use native endianness. +#ifdef WORDS_BIGENDIAN + +static inline void +write_u32(uint16_t *p, uint32_t val) +{ + p[0] = (uint16_t)(val >> 16); + p[1] = (uint16_t)(val >> 0); +} + +static inline void +write_u64(uint16_t *p, uint64_t val) +{ + p[0] = (uint16_t)(val >> 48); + p[1] = (uint16_t)(val >> 32); + p[2] = (uint16_t)(val >> 16); + p[3] = (uint16_t)(val >> 0); +} + +static inline uint32_t +read_u32(uint16_t *p) +{ + uint32_t val = 0; + val |= (uint32_t)p[0] << 16; + val |= (uint32_t)p[1] << 0; + return val; +} + +static inline uint64_t +read_u64(uint16_t *p) +{ + uint64_t val = 0; + val |= (uint64_t)p[0] << 48; + val |= (uint64_t)p[1] << 32; + val |= (uint64_t)p[2] << 16; + val |= (uint64_t)p[3] << 0; + return val; +} + +#else + +static inline void +write_u32(uint16_t *p, uint32_t val) +{ + p[0] = (uint16_t)(val >> 0); + p[1] = (uint16_t)(val >> 16); +} + +static inline void +write_u64(uint16_t *p, uint64_t val) +{ + p[0] = (uint16_t)(val >> 0); + p[1] = (uint16_t)(val >> 16); + p[2] = (uint16_t)(val >> 32); + p[3] = (uint16_t)(val >> 48); +} + +static inline uint32_t +read_u32(uint16_t *p) +{ + uint32_t val = 0; + val |= (uint32_t)p[0] << 0; + val |= (uint32_t)p[1] << 16; + return val; +} + +static inline uint64_t +read_u64(uint16_t *p) +{ + uint64_t val = 0; + val |= (uint64_t)p[0] << 0; + val |= (uint64_t)p[1] << 16; + val |= (uint64_t)p[2] << 32; + val |= (uint64_t)p[3] << 48; + return val; +} + +#endif + +static inline void +write_obj(uint16_t *p, PyObject *obj) +{ + uintptr_t val = (uintptr_t)obj; +#if SIZEOF_VOID_P == 8 + write_u64(p, val); +#elif SIZEOF_VOID_P == 4 + write_u32(p, val); +#else + #error "SIZEOF_VOID_P must be 4 or 8" +#endif +} + +static inline PyObject * +read_obj(uint16_t *p) +{ + uintptr_t val; +#if SIZEOF_VOID_P == 8 + val = read_u64(p); +#elif SIZEOF_VOID_P == 4 + val = read_u32(p); +#else + #error "SIZEOF_VOID_P must be 4 or 8" +#endif + return (PyObject *)val; +} + +/* See Objects/exception_handling_notes.txt for details. + */ +static inline unsigned char * +parse_varint(unsigned char *p, int *result) { + int val = p[0] & 63; + while (p[0] & 64) { + p++; + val = (val << 6) | (p[0] & 63); + } + *result = val; + return p+1; +} + +static inline int +write_varint(uint8_t *ptr, unsigned int val) +{ + int written = 1; + while (val >= 64) { + *ptr++ = 64 | (val & 63); + val >>= 6; + written++; + } + *ptr = val; + return written; +} + +static inline int +write_signed_varint(uint8_t *ptr, int val) +{ + if (val < 0) { + val = ((-val)<<1) | 1; + } + else { + val = val << 1; + } + return write_varint(ptr, val); +} + +static inline int +write_location_entry_start(uint8_t *ptr, int code, int length) +{ + assert((code & 15) == code); + *ptr = 128 | (code << 3) | (length - 1); + return 1; +} + + +/** Counters + * The first 16-bit value in each inline cache is a counter. + * When counting misses, the counter is treated as a simple unsigned value. + * + * When counting executions until the next specialization attempt, + * exponential backoff is used to reduce the number of specialization failures. + * The high 12 bits store the counter, the low 4 bits store the backoff exponent. + * On a specialization failure, the backoff exponent is incremented and the + * counter set to (2**backoff - 1). + * Backoff == 6 -> starting counter == 63, backoff == 10 -> starting counter == 1023. + */ + +/* With a 16-bit counter, we have 12 bits for the counter value, and 4 bits for the backoff */ +#define ADAPTIVE_BACKOFF_BITS 4 +/* The initial counter value is 31 == 2**ADAPTIVE_BACKOFF_START - 1 */ +#define ADAPTIVE_BACKOFF_START 5 + +#define MAX_BACKOFF_VALUE (16 - ADAPTIVE_BACKOFF_BITS) + + +static inline uint16_t +adaptive_counter_bits(int value, int backoff) { + return (value << ADAPTIVE_BACKOFF_BITS) | + (backoff & ((1< MAX_BACKOFF_VALUE) { + backoff = MAX_BACKOFF_VALUE; + } + unsigned int value = (1 << backoff) - 1; + return adaptive_counter_bits(value, backoff); +} + + +/* Line array cache for tracing */ + +extern int _PyCode_CreateLineArray(PyCodeObject *co); + +static inline int +_PyCode_InitLineArray(PyCodeObject *co) +{ + if (co->_co_linearray) { + return 0; + } + return _PyCode_CreateLineArray(co); +} + +static inline int +_PyCode_LineNumberFromArray(PyCodeObject *co, int index) +{ + assert(co->_co_linearray != NULL); + assert(index >= 0); + assert(index < Py_SIZE(co)); + if (co->_co_linearray_entry_size == 2) { + return ((int16_t *)co->_co_linearray)[index]; + } + else { + assert(co->_co_linearray_entry_size == 4); + return ((int32_t *)co->_co_linearray)[index]; + } +} #ifdef __cplusplus diff --git a/src/external/windows/include/python/internal/pycore_condvar.h b/src/external/windows/include/python/internal/pycore_condvar.h index 6e98673d..b744557d 100755 --- a/src/external/windows/include/python/internal/pycore_condvar.h +++ b/src/external/windows/include/python/internal/pycore_condvar.h @@ -20,7 +20,9 @@ */ #define Py_HAVE_CONDVAR -#include +#ifdef HAVE_PTHREAD_H +# include +#endif #define PyMUTEX_T pthread_mutex_t #define PyCOND_T pthread_cond_t @@ -60,7 +62,7 @@ typedef CRITICAL_SECTION PyMUTEX_T; with a Semaphore. Semaphores are available on Windows XP (2003 server) and later. We use a Semaphore rather than an auto-reset event, because although - an auto-resent event might appear to solve the lost-wakeup bug (race + an auto-reset event might appear to solve the lost-wakeup bug (race condition between releasing the outer lock and waiting) because it maintains state even though a wait hasn't happened, there is still a lost wakeup problem if more than one thread are interrupted in the diff --git a/src/external/windows/include/python/internal/pycore_context.h b/src/external/windows/include/python/internal/pycore_context.h index 4236f58e..4a87c5a7 100755 --- a/src/external/windows/include/python/internal/pycore_context.h +++ b/src/external/windows/include/python/internal/pycore_context.h @@ -7,6 +7,34 @@ #include "pycore_hamt.h" /* PyHamtObject */ + +extern PyTypeObject _PyContextTokenMissing_Type; + +/* runtime lifecycle */ + +PyStatus _PyContext_Init(PyInterpreterState *); +void _PyContext_Fini(PyInterpreterState *); + + +/* other API */ + +#ifndef WITH_FREELISTS +// without freelists +# define PyContext_MAXFREELIST 0 +#endif + +#ifndef PyContext_MAXFREELIST +# define PyContext_MAXFREELIST 255 +#endif + +struct _Py_context_state { +#if PyContext_MAXFREELIST > 0 + // List of free PyContext objects + PyContext *freelist; + int numfree; +#endif +}; + struct _pycontextobject { PyObject_HEAD PyContext *ctx_prev; @@ -36,7 +64,4 @@ struct _pycontexttokenobject { }; -int _PyContext_Init(void); -void _PyContext_Fini(PyInterpreterState *interp); - #endif /* !Py_INTERNAL_CONTEXT_H */ diff --git a/src/external/windows/include/python/internal/pycore_dict.h b/src/external/windows/include/python/internal/pycore_dict.h new file mode 100644 index 00000000..93344874 --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_dict.h @@ -0,0 +1,178 @@ + +#ifndef Py_INTERNAL_DICT_H +#define Py_INTERNAL_DICT_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + + +/* runtime lifecycle */ + +extern void _PyDict_Fini(PyInterpreterState *interp); + + +/* other API */ + +#ifndef WITH_FREELISTS +// without freelists +# define PyDict_MAXFREELIST 0 +#endif + +#ifndef PyDict_MAXFREELIST +# define PyDict_MAXFREELIST 80 +#endif + +struct _Py_dict_state { +#if PyDict_MAXFREELIST > 0 + /* Dictionary reuse scheme to save calls to malloc and free */ + PyDictObject *free_list[PyDict_MAXFREELIST]; + int numfree; + PyDictKeysObject *keys_free_list[PyDict_MAXFREELIST]; + int keys_numfree; +#endif +}; + +typedef struct { + /* Cached hash code of me_key. */ + Py_hash_t me_hash; + PyObject *me_key; + PyObject *me_value; /* This field is only meaningful for combined tables */ +} PyDictKeyEntry; + +typedef struct { + PyObject *me_key; /* The key must be Unicode and have hash. */ + PyObject *me_value; /* This field is only meaningful for combined tables */ +} PyDictUnicodeEntry; + +extern PyDictKeysObject *_PyDict_NewKeysForClass(void); +extern PyObject *_PyDict_FromKeys(PyObject *, PyObject *, PyObject *); + +/* Gets a version number unique to the current state of the keys of dict, if possible. + * Returns the version number, or zero if it was not possible to get a version number. */ +extern uint32_t _PyDictKeys_GetVersionForCurrentState(PyDictKeysObject *dictkeys); + +extern Py_ssize_t _PyDict_KeysSize(PyDictKeysObject *keys); + +/* _Py_dict_lookup() returns index of entry which can be used like DK_ENTRIES(dk)[index]. + * -1 when no entry found, -3 when compare raises error. + */ +extern Py_ssize_t _Py_dict_lookup(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject **value_addr); + +extern Py_ssize_t _PyDict_GetItemHint(PyDictObject *, PyObject *, Py_ssize_t, PyObject **); +extern Py_ssize_t _PyDictKeys_StringLookup(PyDictKeysObject* dictkeys, PyObject *key); +extern PyObject *_PyDict_LoadGlobal(PyDictObject *, PyDictObject *, PyObject *); + +/* Consumes references to key and value */ +extern int _PyDict_SetItem_Take2(PyDictObject *op, PyObject *key, PyObject *value); +extern int _PyObjectDict_SetItem(PyTypeObject *tp, PyObject **dictptr, PyObject *name, PyObject *value); + +extern PyObject *_PyDict_Pop_KnownHash(PyObject *, PyObject *, Py_hash_t, PyObject *); + +#define DKIX_EMPTY (-1) +#define DKIX_DUMMY (-2) /* Used internally */ +#define DKIX_ERROR (-3) +#define DKIX_KEY_CHANGED (-4) /* Used internally */ + +typedef enum { + DICT_KEYS_GENERAL = 0, + DICT_KEYS_UNICODE = 1, + DICT_KEYS_SPLIT = 2 +} DictKeysKind; + +/* See dictobject.c for actual layout of DictKeysObject */ +struct _dictkeysobject { + Py_ssize_t dk_refcnt; + + /* Size of the hash table (dk_indices). It must be a power of 2. */ + uint8_t dk_log2_size; + + /* Size of the hash table (dk_indices) by bytes. */ + uint8_t dk_log2_index_bytes; + + /* Kind of keys */ + uint8_t dk_kind; + + /* Version number -- Reset to 0 by any modification to keys */ + uint32_t dk_version; + + /* Number of usable entries in dk_entries. */ + Py_ssize_t dk_usable; + + /* Number of used entries in dk_entries. */ + Py_ssize_t dk_nentries; + + /* Actual hash table of dk_size entries. It holds indices in dk_entries, + or DKIX_EMPTY(-1) or DKIX_DUMMY(-2). + + Indices must be: 0 <= indice < USABLE_FRACTION(dk_size). + + The size in bytes of an indice depends on dk_size: + + - 1 byte if dk_size <= 0xff (char*) + - 2 bytes if dk_size <= 0xffff (int16_t*) + - 4 bytes if dk_size <= 0xffffffff (int32_t*) + - 8 bytes otherwise (int64_t*) + + Dynamically sized, SIZEOF_VOID_P is minimum. */ + char dk_indices[]; /* char is required to avoid strict aliasing. */ + + /* "PyDictKeyEntry or PyDictUnicodeEntry dk_entries[USABLE_FRACTION(DK_SIZE(dk))];" array follows: + see the DK_ENTRIES() macro */ +}; + +/* This must be no more than 250, for the prefix size to fit in one byte. */ +#define SHARED_KEYS_MAX_SIZE 30 +#define NEXT_LOG2_SHARED_KEYS_MAX_SIZE 6 + +/* Layout of dict values: + * + * The PyObject *values are preceded by an array of bytes holding + * the insertion order and size. + * [-1] = prefix size. [-2] = used size. size[-2-n...] = insertion order. + */ +struct _dictvalues { + PyObject *values[1]; +}; + +#define DK_LOG_SIZE(dk) ((dk)->dk_log2_size) +#if SIZEOF_VOID_P > 4 +#define DK_SIZE(dk) (((int64_t)1)<dk_kind == DICT_KEYS_GENERAL), (PyDictKeyEntry*)(&((int8_t*)((dk)->dk_indices))[(size_t)1 << (dk)->dk_log2_index_bytes])) +#define DK_UNICODE_ENTRIES(dk) \ + (assert(dk->dk_kind != DICT_KEYS_GENERAL), (PyDictUnicodeEntry*)(&((int8_t*)((dk)->dk_indices))[(size_t)1 << (dk)->dk_log2_index_bytes])) +#define DK_IS_UNICODE(dk) ((dk)->dk_kind != DICT_KEYS_GENERAL) + +extern uint64_t _pydict_global_version; + +#define DICT_NEXT_VERSION() (++_pydict_global_version) + +extern PyObject *_PyObject_MakeDictFromInstanceAttributes(PyObject *obj, PyDictValues *values); +extern PyObject *_PyDict_FromItems( + PyObject *const *keys, Py_ssize_t keys_offset, + PyObject *const *values, Py_ssize_t values_offset, + Py_ssize_t length); + +static inline void +_PyDictValues_AddToInsertionOrder(PyDictValues *values, Py_ssize_t ix) +{ + assert(ix < SHARED_KEYS_MAX_SIZE); + uint8_t *size_ptr = ((uint8_t *)values)-2; + int size = *size_ptr; + assert(size+2 < ((uint8_t *)values)[-1]); + size++; + size_ptr[-size] = (uint8_t)ix; + *size_ptr = size; +} + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_DICT_H */ diff --git a/src/external/windows/include/python/internal/pycore_dtoa.h b/src/external/windows/include/python/internal/pycore_dtoa.h index 4cf08f61..06361655 100755 --- a/src/external/windows/include/python/internal/pycore_dtoa.h +++ b/src/external/windows/include/python/internal/pycore_dtoa.h @@ -1,4 +1,3 @@ -#ifndef PY_NO_SHORT_FLOAT_REPR #ifdef __cplusplus extern "C" { #endif @@ -7,6 +6,11 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif +#include "pycore_pymath.h" // _PY_SHORT_FLOAT_REPR + + +#if _PY_SHORT_FLOAT_REPR == 1 + /* These functions are used by modules compiled as C extension like math: they must be exported. */ @@ -17,7 +21,8 @@ PyAPI_FUNC(void) _Py_dg_freedtoa(char *s); PyAPI_FUNC(double) _Py_dg_stdnan(int sign); PyAPI_FUNC(double) _Py_dg_infinity(int sign); +#endif // _PY_SHORT_FLOAT_REPR == 1 + #ifdef __cplusplus } #endif -#endif /* !PY_NO_SHORT_FLOAT_REPR */ diff --git a/src/external/windows/include/python/internal/pycore_emscripten_signal.h b/src/external/windows/include/python/internal/pycore_emscripten_signal.h new file mode 100644 index 00000000..cbb8941c --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_emscripten_signal.h @@ -0,0 +1,25 @@ +#ifndef Py_EMSCRIPTEN_SIGNAL_H +#define Py_EMSCRIPTEN_SIGNAL_H + +#if defined(__EMSCRIPTEN__) + +void +_Py_CheckEmscriptenSignals(void); + +void +_Py_CheckEmscriptenSignalsPeriodically(void); + +#define _Py_CHECK_EMSCRIPTEN_SIGNALS() _Py_CheckEmscriptenSignals() + +#define _Py_CHECK_EMSCRIPTEN_SIGNALS_PERIODICALLY() _Py_CheckEmscriptenSignalsPeriodically() + +extern int Py_EMSCRIPTEN_SIGNAL_HANDLING; + +#else + +#define _Py_CHECK_EMSCRIPTEN_SIGNALS() +#define _Py_CHECK_EMSCRIPTEN_SIGNALS_PERIODICALLY() + +#endif // defined(__EMSCRIPTEN__) + +#endif // ndef Py_EMSCRIPTEN_SIGNAL_H diff --git a/src/external/windows/include/python/internal/pycore_exceptions.h b/src/external/windows/include/python/internal/pycore_exceptions.h new file mode 100644 index 00000000..a66af16e --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_exceptions.h @@ -0,0 +1,37 @@ +#ifndef Py_INTERNAL_EXCEPTIONS_H +#define Py_INTERNAL_EXCEPTIONS_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + + +/* runtime lifecycle */ + +extern PyStatus _PyExc_InitState(PyInterpreterState *); +extern PyStatus _PyExc_InitGlobalObjects(PyInterpreterState *); +extern int _PyExc_InitTypes(PyInterpreterState *); +extern void _PyExc_Fini(PyInterpreterState *); + + +/* other API */ + +struct _Py_exc_state { + // The dict mapping from errno codes to OSError subclasses + PyObject *errnomap; + PyBaseExceptionObject *memerrors_freelist; + int memerrors_numfree; + // The ExceptionGroup type + PyObject *PyExc_ExceptionGroup; +}; + +extern void _PyExc_ClearExceptionGroupType(PyInterpreterState *); + + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_EXCEPTIONS_H */ diff --git a/src/external/windows/include/python/internal/pycore_fileutils.h b/src/external/windows/include/python/internal/pycore_fileutils.h index 8282908e..8252b75e 100755 --- a/src/external/windows/include/python/internal/pycore_fileutils.h +++ b/src/external/windows/include/python/internal/pycore_fileutils.h @@ -10,6 +10,171 @@ extern "C" { #include /* struct lconv */ +typedef enum { + _Py_ERROR_UNKNOWN=0, + _Py_ERROR_STRICT, + _Py_ERROR_SURROGATEESCAPE, + _Py_ERROR_REPLACE, + _Py_ERROR_IGNORE, + _Py_ERROR_BACKSLASHREPLACE, + _Py_ERROR_SURROGATEPASS, + _Py_ERROR_XMLCHARREFREPLACE, + _Py_ERROR_OTHER +} _Py_error_handler; + +PyAPI_FUNC(_Py_error_handler) _Py_GetErrorHandler(const char *errors); + +PyAPI_FUNC(int) _Py_DecodeLocaleEx( + const char *arg, + wchar_t **wstr, + size_t *wlen, + const char **reason, + int current_locale, + _Py_error_handler errors); + +PyAPI_FUNC(int) _Py_EncodeLocaleEx( + const wchar_t *text, + char **str, + size_t *error_pos, + const char **reason, + int current_locale, + _Py_error_handler errors); + +PyAPI_FUNC(char*) _Py_EncodeLocaleRaw( + const wchar_t *text, + size_t *error_pos); + +PyAPI_FUNC(PyObject *) _Py_device_encoding(int); + +#if defined(MS_WINDOWS) || defined(__APPLE__) + /* On Windows, the count parameter of read() is an int (bpo-9015, bpo-9611). + On macOS 10.13, read() and write() with more than INT_MAX bytes + fail with EINVAL (bpo-24658). */ +# define _PY_READ_MAX INT_MAX +# define _PY_WRITE_MAX INT_MAX +#else + /* write() should truncate the input to PY_SSIZE_T_MAX bytes, + but it's safer to do it ourself to have a portable behaviour */ +# define _PY_READ_MAX PY_SSIZE_T_MAX +# define _PY_WRITE_MAX PY_SSIZE_T_MAX +#endif + +#ifdef MS_WINDOWS +struct _Py_stat_struct { + unsigned long st_dev; + uint64_t st_ino; + unsigned short st_mode; + int st_nlink; + int st_uid; + int st_gid; + unsigned long st_rdev; + __int64 st_size; + time_t st_atime; + int st_atime_nsec; + time_t st_mtime; + int st_mtime_nsec; + time_t st_ctime; + int st_ctime_nsec; + unsigned long st_file_attributes; + unsigned long st_reparse_tag; +}; +#else +# define _Py_stat_struct stat +#endif + +PyAPI_FUNC(int) _Py_fstat( + int fd, + struct _Py_stat_struct *status); + +PyAPI_FUNC(int) _Py_fstat_noraise( + int fd, + struct _Py_stat_struct *status); + +PyAPI_FUNC(int) _Py_stat( + PyObject *path, + struct stat *status); + +PyAPI_FUNC(int) _Py_open( + const char *pathname, + int flags); + +PyAPI_FUNC(int) _Py_open_noraise( + const char *pathname, + int flags); + +PyAPI_FUNC(FILE *) _Py_wfopen( + const wchar_t *path, + const wchar_t *mode); + +PyAPI_FUNC(Py_ssize_t) _Py_read( + int fd, + void *buf, + size_t count); + +PyAPI_FUNC(Py_ssize_t) _Py_write( + int fd, + const void *buf, + size_t count); + +PyAPI_FUNC(Py_ssize_t) _Py_write_noraise( + int fd, + const void *buf, + size_t count); + +#ifdef HAVE_READLINK +PyAPI_FUNC(int) _Py_wreadlink( + const wchar_t *path, + wchar_t *buf, + /* Number of characters of 'buf' buffer + including the trailing NUL character */ + size_t buflen); +#endif + +#ifdef HAVE_REALPATH +PyAPI_FUNC(wchar_t*) _Py_wrealpath( + const wchar_t *path, + wchar_t *resolved_path, + /* Number of characters of 'resolved_path' buffer + including the trailing NUL character */ + size_t resolved_path_len); +#endif + +PyAPI_FUNC(wchar_t*) _Py_wgetcwd( + wchar_t *buf, + /* Number of characters of 'buf' buffer + including the trailing NUL character */ + size_t buflen); + +PyAPI_FUNC(int) _Py_get_inheritable(int fd); + +PyAPI_FUNC(int) _Py_set_inheritable(int fd, int inheritable, + int *atomic_flag_works); + +PyAPI_FUNC(int) _Py_set_inheritable_async_safe(int fd, int inheritable, + int *atomic_flag_works); + +PyAPI_FUNC(int) _Py_dup(int fd); + +#ifndef MS_WINDOWS +PyAPI_FUNC(int) _Py_get_blocking(int fd); + +PyAPI_FUNC(int) _Py_set_blocking(int fd, int blocking); +#else /* MS_WINDOWS */ +PyAPI_FUNC(void*) _Py_get_osfhandle_noraise(int fd); + +PyAPI_FUNC(void*) _Py_get_osfhandle(int fd); + +PyAPI_FUNC(int) _Py_open_osfhandle_noraise(void *handle, int flags); + +PyAPI_FUNC(int) _Py_open_osfhandle(void *handle, int flags); +#endif /* MS_WINDOWS */ + +// This is used after getting NULL back from Py_DecodeLocale(). +#define DECODE_LOCALE_ERR(NAME, LEN) \ + ((LEN) == (size_t)-2) \ + ? _PyStatus_ERR("cannot decode " NAME) \ + : _PyStatus_NO_MEMORY() + PyAPI_DATA(int) _Py_HasFileSystemDefaultEncodeErrors; PyAPI_FUNC(int) _Py_DecodeUTF8Ex( @@ -33,6 +198,9 @@ PyAPI_FUNC(wchar_t*) _Py_DecodeUTF8_surrogateescape( Py_ssize_t arglen, size_t *wlen); +extern int +_Py_wstat(const wchar_t *, struct stat *); + PyAPI_FUNC(int) _Py_GetForceASCII(void); /* Reset "force ASCII" mode (if it was initialized). @@ -65,6 +233,42 @@ extern int _Py_EncodeNonUnicodeWchar_InPlace( Py_ssize_t size); #endif +extern int _Py_isabs(const wchar_t *path); +extern int _Py_abspath(const wchar_t *path, wchar_t **abspath_p); +#ifdef MS_WINDOWS +extern int _PyOS_getfullpathname(const wchar_t *path, wchar_t **abspath_p); +#endif +extern wchar_t * _Py_join_relfile(const wchar_t *dirname, + const wchar_t *relfile); +extern int _Py_add_relfile(wchar_t *dirname, + const wchar_t *relfile, + size_t bufsize); +extern size_t _Py_find_basename(const wchar_t *filename); +PyAPI_FUNC(wchar_t *) _Py_normpath(wchar_t *path, Py_ssize_t size); + + +// Macros to protect CRT calls against instant termination when passed an +// invalid parameter (bpo-23524). IPH stands for Invalid Parameter Handler. +// Usage: +// +// _Py_BEGIN_SUPPRESS_IPH +// ... +// _Py_END_SUPPRESS_IPH +#if defined _MSC_VER && _MSC_VER >= 1900 + +# include // _set_thread_local_invalid_parameter_handler() + + extern _invalid_parameter_handler _Py_silent_invalid_parameter_handler; +# define _Py_BEGIN_SUPPRESS_IPH \ + { _invalid_parameter_handler _Py_old_handler = \ + _set_thread_local_invalid_parameter_handler(_Py_silent_invalid_parameter_handler); +# define _Py_END_SUPPRESS_IPH \ + _set_thread_local_invalid_parameter_handler(_Py_old_handler); } +#else +# define _Py_BEGIN_SUPPRESS_IPH +# define _Py_END_SUPPRESS_IPH +#endif /* _MSC_VER >= 1900 */ + #ifdef __cplusplus } #endif diff --git a/src/external/windows/include/python/internal/pycore_floatobject.h b/src/external/windows/include/python/internal/pycore_floatobject.h new file mode 100644 index 00000000..a4467b4c --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_floatobject.h @@ -0,0 +1,59 @@ +#ifndef Py_INTERNAL_FLOATOBJECT_H +#define Py_INTERNAL_FLOATOBJECT_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + + +/* runtime lifecycle */ + +extern void _PyFloat_InitState(PyInterpreterState *); +extern PyStatus _PyFloat_InitTypes(PyInterpreterState *); +extern void _PyFloat_Fini(PyInterpreterState *); +extern void _PyFloat_FiniType(PyInterpreterState *); + + +/* other API */ + +#ifndef WITH_FREELISTS +// without freelists +# define PyFloat_MAXFREELIST 0 +#endif + +#ifndef PyFloat_MAXFREELIST +# define PyFloat_MAXFREELIST 100 +#endif + +struct _Py_float_state { +#if PyFloat_MAXFREELIST > 0 + /* Special free list + free_list is a singly-linked list of available PyFloatObjects, + linked via abuse of their ob_type members. */ + int numfree; + PyFloatObject *free_list; +#endif +}; + +void _PyFloat_ExactDealloc(PyObject *op); + + +PyAPI_FUNC(void) _PyFloat_DebugMallocStats(FILE* out); + + +/* Format the object based on the format_spec, as defined in PEP 3101 + (Advanced String Formatting). */ +PyAPI_FUNC(int) _PyFloat_FormatAdvancedWriter( + _PyUnicodeWriter *writer, + PyObject *obj, + PyObject *format_spec, + Py_ssize_t start, + Py_ssize_t end); + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_FLOATOBJECT_H */ diff --git a/src/external/windows/include/python/internal/pycore_format.h b/src/external/windows/include/python/internal/pycore_format.h index 49c937a0..0cdf3ac0 100755 --- a/src/external/windows/include/python/internal/pycore_format.h +++ b/src/external/windows/include/python/internal/pycore_format.h @@ -14,12 +14,14 @@ extern "C" { * F_BLANK ' ' * F_ALT '#' * F_ZERO '0' + * F_NO_NEG_0 'z' */ #define F_LJUST (1<<0) #define F_SIGN (1<<1) #define F_BLANK (1<<2) #define F_ALT (1<<3) #define F_ZERO (1<<4) +#define F_NO_NEG_0 (1<<5) #ifdef __cplusplus } diff --git a/src/external/windows/include/python/internal/pycore_frame.h b/src/external/windows/include/python/internal/pycore_frame.h new file mode 100644 index 00000000..b1fbd516 --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_frame.h @@ -0,0 +1,240 @@ +#ifndef Py_INTERNAL_FRAME_H +#define Py_INTERNAL_FRAME_H +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/* See Objects/frame_layout.md for an explanation of the frame stack + * including explanation of the PyFrameObject and _PyInterpreterFrame + * structs. */ + + +struct _frame { + PyObject_HEAD + PyFrameObject *f_back; /* previous frame, or NULL */ + struct _PyInterpreterFrame *f_frame; /* points to the frame data */ + PyObject *f_trace; /* Trace function */ + int f_lineno; /* Current line number. Only valid if non-zero */ + char f_trace_lines; /* Emit per-line trace events? */ + char f_trace_opcodes; /* Emit per-opcode trace events? */ + char f_fast_as_locals; /* Have the fast locals of this frame been converted to a dict? */ + /* The frame data, if this frame object owns the frame */ + PyObject *_f_frame_data[1]; +}; + +extern PyFrameObject* _PyFrame_New_NoTrack(PyCodeObject *code); + + +/* other API */ + +typedef enum _framestate { + FRAME_CREATED = -2, + FRAME_SUSPENDED = -1, + FRAME_EXECUTING = 0, + FRAME_COMPLETED = 1, + FRAME_CLEARED = 4 +} PyFrameState; + +enum _frameowner { + FRAME_OWNED_BY_THREAD = 0, + FRAME_OWNED_BY_GENERATOR = 1, + FRAME_OWNED_BY_FRAME_OBJECT = 2 +}; + +typedef struct _PyInterpreterFrame { + /* "Specials" section */ + PyFunctionObject *f_func; /* Strong reference */ + PyObject *f_globals; /* Borrowed reference */ + PyObject *f_builtins; /* Borrowed reference */ + PyObject *f_locals; /* Strong reference, may be NULL */ + PyCodeObject *f_code; /* Strong reference */ + PyFrameObject *frame_obj; /* Strong reference, may be NULL */ + /* Linkage section */ + struct _PyInterpreterFrame *previous; + // NOTE: This is not necessarily the last instruction started in the given + // frame. Rather, it is the code unit *prior to* the *next* instruction. For + // example, it may be an inline CACHE entry, an instruction we just jumped + // over, or (in the case of a newly-created frame) a totally invalid value: + _Py_CODEUNIT *prev_instr; + int stacktop; /* Offset of TOS from localsplus */ + bool is_entry; // Whether this is the "root" frame for the current _PyCFrame. + char owner; + /* Locals and stack */ + PyObject *localsplus[1]; +} _PyInterpreterFrame; + +#define _PyInterpreterFrame_LASTI(IF) \ + ((int)((IF)->prev_instr - _PyCode_CODE((IF)->f_code))) + +static inline PyObject **_PyFrame_Stackbase(_PyInterpreterFrame *f) { + return f->localsplus + f->f_code->co_nlocalsplus; +} + +static inline PyObject *_PyFrame_StackPeek(_PyInterpreterFrame *f) { + assert(f->stacktop > f->f_code->co_nlocalsplus); + assert(f->localsplus[f->stacktop-1] != NULL); + return f->localsplus[f->stacktop-1]; +} + +static inline PyObject *_PyFrame_StackPop(_PyInterpreterFrame *f) { + assert(f->stacktop > f->f_code->co_nlocalsplus); + f->stacktop--; + return f->localsplus[f->stacktop]; +} + +static inline void _PyFrame_StackPush(_PyInterpreterFrame *f, PyObject *value) { + f->localsplus[f->stacktop] = value; + f->stacktop++; +} + +#define FRAME_SPECIALS_SIZE ((sizeof(_PyInterpreterFrame)-1)/sizeof(PyObject *)) + +void _PyFrame_Copy(_PyInterpreterFrame *src, _PyInterpreterFrame *dest); + +/* Consumes reference to func and locals. + Does not initialize frame->previous, which happens + when frame is linked into the frame stack. + */ +static inline void +_PyFrame_InitializeSpecials( + _PyInterpreterFrame *frame, PyFunctionObject *func, + PyObject *locals, int nlocalsplus) +{ + frame->f_func = func; + frame->f_code = (PyCodeObject *)Py_NewRef(func->func_code); + frame->f_builtins = func->func_builtins; + frame->f_globals = func->func_globals; + frame->f_locals = Py_XNewRef(locals); + frame->stacktop = nlocalsplus; + frame->frame_obj = NULL; + frame->prev_instr = _PyCode_CODE(frame->f_code) - 1; + frame->is_entry = false; + frame->owner = FRAME_OWNED_BY_THREAD; +} + +/* Gets the pointer to the locals array + * that precedes this frame. + */ +static inline PyObject** +_PyFrame_GetLocalsArray(_PyInterpreterFrame *frame) +{ + return frame->localsplus; +} + +static inline PyObject** +_PyFrame_GetStackPointer(_PyInterpreterFrame *frame) +{ + return frame->localsplus+frame->stacktop; +} + +static inline void +_PyFrame_SetStackPointer(_PyInterpreterFrame *frame, PyObject **stack_pointer) +{ + frame->stacktop = (int)(stack_pointer - frame->localsplus); +} + +/* Determine whether a frame is incomplete. + * A frame is incomplete if it is part way through + * creating cell objects or a generator or coroutine. + * + * Frames on the frame stack are incomplete until the + * first RESUME instruction. + * Frames owned by a generator are always complete. + */ +static inline bool +_PyFrame_IsIncomplete(_PyInterpreterFrame *frame) +{ + return frame->owner != FRAME_OWNED_BY_GENERATOR && + frame->prev_instr < _PyCode_CODE(frame->f_code) + frame->f_code->_co_firsttraceable; +} + +/* For use by _PyFrame_GetFrameObject + Do not call directly. */ +PyFrameObject * +_PyFrame_MakeAndSetFrameObject(_PyInterpreterFrame *frame); + +/* Gets the PyFrameObject for this frame, lazily + * creating it if necessary. + * Returns a borrowed referennce */ +static inline PyFrameObject * +_PyFrame_GetFrameObject(_PyInterpreterFrame *frame) +{ + + assert(!_PyFrame_IsIncomplete(frame)); + PyFrameObject *res = frame->frame_obj; + if (res != NULL) { + return res; + } + return _PyFrame_MakeAndSetFrameObject(frame); +} + +/* Clears all references in the frame. + * If take is non-zero, then the _PyInterpreterFrame frame + * may be transferred to the frame object it references + * instead of being cleared. Either way + * the caller no longer owns the references + * in the frame. + * take should be set to 1 for heap allocated + * frames like the ones in generators and coroutines. + */ +void +_PyFrame_Clear(_PyInterpreterFrame * frame); + +int +_PyFrame_Traverse(_PyInterpreterFrame *frame, visitproc visit, void *arg); + +int +_PyFrame_FastToLocalsWithError(_PyInterpreterFrame *frame); + +void +_PyFrame_LocalsToFast(_PyInterpreterFrame *frame, int clear); + +extern _PyInterpreterFrame * +_PyThreadState_BumpFramePointerSlow(PyThreadState *tstate, size_t size); + +static inline bool +_PyThreadState_HasStackSpace(PyThreadState *tstate, size_t size) +{ + assert( + (tstate->datastack_top == NULL && tstate->datastack_limit == NULL) + || + (tstate->datastack_top != NULL && tstate->datastack_limit != NULL) + ); + return tstate->datastack_top != NULL && + size < (size_t)(tstate->datastack_limit - tstate->datastack_top); +} + +static inline _PyInterpreterFrame * +_PyThreadState_BumpFramePointer(PyThreadState *tstate, size_t size) +{ + if (_PyThreadState_HasStackSpace(tstate, size)) { + _PyInterpreterFrame *res = (_PyInterpreterFrame *)tstate->datastack_top; + tstate->datastack_top += size; + return res; + } + return _PyThreadState_BumpFramePointerSlow(tstate, size); +} + +void _PyThreadState_PopFrame(PyThreadState *tstate, _PyInterpreterFrame *frame); + +/* Consume reference to func */ +_PyInterpreterFrame * +_PyFrame_Push(PyThreadState *tstate, PyFunctionObject *func); + +int _PyInterpreterFrame_GetLine(_PyInterpreterFrame *frame); + +static inline +PyGenObject *_PyFrame_GetGenerator(_PyInterpreterFrame *frame) +{ + assert(frame->owner == FRAME_OWNED_BY_GENERATOR); + size_t offset_in_gen = offsetof(PyGenObject, gi_iframe); + return (PyGenObject *)(((char *)frame) - offset_in_gen); +} + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_FRAME_H */ diff --git a/src/external/windows/include/python/internal/pycore_function.h b/src/external/windows/include/python/internal/pycore_function.h new file mode 100644 index 00000000..85118bee --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_function.h @@ -0,0 +1,18 @@ +#ifndef Py_INTERNAL_FUNCTION_H +#define Py_INTERNAL_FUNCTION_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +extern PyFunctionObject* _PyFunction_FromConstructor(PyFrameConstructor *constr); + +extern uint32_t _PyFunction_GetVersionForCurrentState(PyFunctionObject *func); + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_FUNCTION_H */ diff --git a/src/external/windows/include/python/internal/pycore_gc.h b/src/external/windows/include/python/internal/pycore_gc.h index 85c94cc6..7d15c05d 100755 --- a/src/external/windows/include/python/internal/pycore_gc.h +++ b/src/external/windows/include/python/internal/pycore_gc.h @@ -20,6 +20,7 @@ typedef struct { } PyGC_Head; #define _Py_AS_GC(o) ((PyGC_Head *)(o)-1) +#define _PyGC_Head_UNUSED PyGC_Head /* True if the object is currently tracked by the GC. */ #define _PyObject_GC_IS_TRACKED(o) (_Py_AS_GC(o)->_gc_next != 0) @@ -43,7 +44,7 @@ typedef struct { // Lowest bit of _gc_next is used for flags only in GC. // But it is always 0 for normal code. #define _PyGCHead_NEXT(g) ((PyGC_Head*)(g)->_gc_next) -#define _PyGCHead_SET_NEXT(g, p) ((g)->_gc_next = (uintptr_t)(p)) +#define _PyGCHead_SET_NEXT(g, p) _Py_RVALUE((g)->_gc_next = (uintptr_t)(p)) // Lowest two bits of _gc_prev is used for _PyGC_PREV_MASK_* flags. #define _PyGCHead_PREV(g) ((PyGC_Head*)((g)->_gc_prev & _PyGC_PREV_MASK)) @@ -56,7 +57,7 @@ typedef struct { #define _PyGCHead_FINALIZED(g) \ (((g)->_gc_prev & _PyGC_PREV_MASK_FINALIZED) != 0) #define _PyGCHead_SET_FINALIZED(g) \ - ((g)->_gc_prev |= _PyGC_PREV_MASK_FINALIZED) + _Py_RVALUE((g)->_gc_prev |= _PyGC_PREV_MASK_FINALIZED) #define _PyGC_FINALIZED(o) \ _PyGCHead_FINALIZED(_Py_AS_GC(o)) @@ -134,6 +135,7 @@ struct _gc_runtime_state { /* Current call-stack depth of tp_dealloc calls. */ int trash_delete_nesting; + /* Is automatic collection enabled? */ int enabled; int debug; /* linked lists of container objects */ @@ -161,13 +163,13 @@ struct _gc_runtime_state { Py_ssize_t long_lived_pending; }; + extern void _PyGC_InitState(struct _gc_runtime_state *); extern Py_ssize_t _PyGC_CollectNoFail(PyThreadState *tstate); // Functions to clear types free lists -extern void _PyFrame_ClearFreeList(PyInterpreterState *interp); extern void _PyTuple_ClearFreeList(PyInterpreterState *interp); extern void _PyFloat_ClearFreeList(PyInterpreterState *interp); extern void _PyList_ClearFreeList(PyInterpreterState *interp); diff --git a/src/external/windows/include/python/internal/pycore_genobject.h b/src/external/windows/include/python/internal/pycore_genobject.h new file mode 100644 index 00000000..e8f56a6b --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_genobject.h @@ -0,0 +1,49 @@ +#ifndef Py_INTERNAL_GENOBJECT_H +#define Py_INTERNAL_GENOBJECT_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +extern PyObject *_PyGen_yf(PyGenObject *); +extern PyObject *_PyCoro_GetAwaitableIter(PyObject *o); +extern PyObject *_PyAsyncGenValueWrapperNew(PyObject *); + +/* runtime lifecycle */ + +extern void _PyAsyncGen_Fini(PyInterpreterState *); + + +/* other API */ + +#ifndef WITH_FREELISTS +// without freelists +# define _PyAsyncGen_MAXFREELIST 0 +#endif + +#ifndef _PyAsyncGen_MAXFREELIST +# define _PyAsyncGen_MAXFREELIST 80 +#endif + +struct _Py_async_gen_state { +#if _PyAsyncGen_MAXFREELIST > 0 + /* Freelists boost performance 6-10%; they also reduce memory + fragmentation, as _PyAsyncGenWrappedValue and PyAsyncGenASend + are short-living objects that are instantiated for every + __anext__() call. */ + struct _PyAsyncGenWrappedValue* value_freelist[_PyAsyncGen_MAXFREELIST]; + int value_numfree; + + struct PyAsyncGenASend* asend_freelist[_PyAsyncGen_MAXFREELIST]; + int asend_numfree; +#endif +}; + + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_GENOBJECT_H */ diff --git a/src/external/windows/include/python/internal/pycore_global_objects.h b/src/external/windows/include/python/internal/pycore_global_objects.h new file mode 100644 index 00000000..4b6eff67 --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_global_objects.h @@ -0,0 +1,54 @@ +#ifndef Py_INTERNAL_GLOBAL_OBJECTS_H +#define Py_INTERNAL_GLOBAL_OBJECTS_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +#include "pycore_gc.h" // PyGC_Head +#include "pycore_global_strings.h" // struct _Py_global_strings + + +// These would be in pycore_long.h if it weren't for an include cycle. +#define _PY_NSMALLPOSINTS 257 +#define _PY_NSMALLNEGINTS 5 + + +// Only immutable objects should be considered runtime-global. +// All others must be per-interpreter. + +#define _Py_GLOBAL_OBJECT(NAME) \ + _PyRuntime.global_objects.NAME +#define _Py_SINGLETON(NAME) \ + _Py_GLOBAL_OBJECT(singletons.NAME) + +struct _Py_global_objects { + struct { + /* Small integers are preallocated in this array so that they + * can be shared. + * The integers that are preallocated are those in the range + * -_PY_NSMALLNEGINTS (inclusive) to _PY_NSMALLPOSINTS (exclusive). + */ + PyLongObject small_ints[_PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS]; + + PyBytesObject bytes_empty; + struct { + PyBytesObject ob; + char eos; + } bytes_characters[256]; + + struct _Py_global_strings strings; + + _PyGC_Head_UNUSED _tuple_empty_gc_not_used; + PyTupleObject tuple_empty; + } singletons; +}; + + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_GLOBAL_OBJECTS_H */ diff --git a/src/external/windows/include/python/internal/pycore_global_strings.h b/src/external/windows/include/python/internal/pycore_global_strings.h new file mode 100644 index 00000000..1679d458 --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_global_strings.h @@ -0,0 +1,395 @@ +#ifndef Py_INTERNAL_GLOBAL_STRINGS_H +#define Py_INTERNAL_GLOBAL_STRINGS_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +// The data structure & init here are inspired by Tools/scripts/deepfreeze.py. + +// All field names generated by ASCII_STR() have a common prefix, +// to help avoid collisions with keywords, etc. + +#define STRUCT_FOR_ASCII_STR(LITERAL) \ + struct { \ + PyASCIIObject _ascii; \ + uint8_t _data[sizeof(LITERAL)]; \ + } +#define STRUCT_FOR_STR(NAME, LITERAL) \ + STRUCT_FOR_ASCII_STR(LITERAL) _ ## NAME; +#define STRUCT_FOR_ID(NAME) \ + STRUCT_FOR_ASCII_STR(#NAME) _ ## NAME; + +// XXX Order by frequency of use? + +/* The following is auto-generated by Tools/scripts/generate_global_objects.py. */ +struct _Py_global_strings { + struct { + STRUCT_FOR_STR(anon_dictcomp, "") + STRUCT_FOR_STR(anon_genexpr, "") + STRUCT_FOR_STR(anon_lambda, "") + STRUCT_FOR_STR(anon_listcomp, "") + STRUCT_FOR_STR(anon_module, "") + STRUCT_FOR_STR(anon_setcomp, "") + STRUCT_FOR_STR(anon_string, "") + STRUCT_FOR_STR(anon_unknown, "") + STRUCT_FOR_STR(close_br, "}") + STRUCT_FOR_STR(comma_sep, ", ") + STRUCT_FOR_STR(dbl_close_br, "}}") + STRUCT_FOR_STR(dbl_open_br, "{{") + STRUCT_FOR_STR(dbl_percent, "%%") + STRUCT_FOR_STR(dot, ".") + STRUCT_FOR_STR(dot_locals, ".") + STRUCT_FOR_STR(empty, "") + STRUCT_FOR_STR(list_err, "list index out of range") + STRUCT_FOR_STR(newline, "\n") + STRUCT_FOR_STR(open_br, "{") + STRUCT_FOR_STR(percent, "%") + STRUCT_FOR_STR(utf_8, "utf-8") + } literals; + + struct { + STRUCT_FOR_ID(False) + STRUCT_FOR_ID(Py_Repr) + STRUCT_FOR_ID(TextIOWrapper) + STRUCT_FOR_ID(True) + STRUCT_FOR_ID(WarningMessage) + STRUCT_FOR_ID(_) + STRUCT_FOR_ID(__IOBase_closed) + STRUCT_FOR_ID(__abc_tpflags__) + STRUCT_FOR_ID(__abs__) + STRUCT_FOR_ID(__abstractmethods__) + STRUCT_FOR_ID(__add__) + STRUCT_FOR_ID(__aenter__) + STRUCT_FOR_ID(__aexit__) + STRUCT_FOR_ID(__aiter__) + STRUCT_FOR_ID(__all__) + STRUCT_FOR_ID(__and__) + STRUCT_FOR_ID(__anext__) + STRUCT_FOR_ID(__annotations__) + STRUCT_FOR_ID(__args__) + STRUCT_FOR_ID(__await__) + STRUCT_FOR_ID(__bases__) + STRUCT_FOR_ID(__bool__) + STRUCT_FOR_ID(__build_class__) + STRUCT_FOR_ID(__builtins__) + STRUCT_FOR_ID(__bytes__) + STRUCT_FOR_ID(__call__) + STRUCT_FOR_ID(__cantrace__) + STRUCT_FOR_ID(__class__) + STRUCT_FOR_ID(__class_getitem__) + STRUCT_FOR_ID(__classcell__) + STRUCT_FOR_ID(__complex__) + STRUCT_FOR_ID(__contains__) + STRUCT_FOR_ID(__copy__) + STRUCT_FOR_ID(__del__) + STRUCT_FOR_ID(__delattr__) + STRUCT_FOR_ID(__delete__) + STRUCT_FOR_ID(__delitem__) + STRUCT_FOR_ID(__dict__) + STRUCT_FOR_ID(__dir__) + STRUCT_FOR_ID(__divmod__) + STRUCT_FOR_ID(__doc__) + STRUCT_FOR_ID(__enter__) + STRUCT_FOR_ID(__eq__) + STRUCT_FOR_ID(__exit__) + STRUCT_FOR_ID(__file__) + STRUCT_FOR_ID(__float__) + STRUCT_FOR_ID(__floordiv__) + STRUCT_FOR_ID(__format__) + STRUCT_FOR_ID(__fspath__) + STRUCT_FOR_ID(__ge__) + STRUCT_FOR_ID(__get__) + STRUCT_FOR_ID(__getattr__) + STRUCT_FOR_ID(__getattribute__) + STRUCT_FOR_ID(__getinitargs__) + STRUCT_FOR_ID(__getitem__) + STRUCT_FOR_ID(__getnewargs__) + STRUCT_FOR_ID(__getnewargs_ex__) + STRUCT_FOR_ID(__getstate__) + STRUCT_FOR_ID(__gt__) + STRUCT_FOR_ID(__hash__) + STRUCT_FOR_ID(__iadd__) + STRUCT_FOR_ID(__iand__) + STRUCT_FOR_ID(__ifloordiv__) + STRUCT_FOR_ID(__ilshift__) + STRUCT_FOR_ID(__imatmul__) + STRUCT_FOR_ID(__imod__) + STRUCT_FOR_ID(__import__) + STRUCT_FOR_ID(__imul__) + STRUCT_FOR_ID(__index__) + STRUCT_FOR_ID(__init__) + STRUCT_FOR_ID(__init_subclass__) + STRUCT_FOR_ID(__instancecheck__) + STRUCT_FOR_ID(__int__) + STRUCT_FOR_ID(__invert__) + STRUCT_FOR_ID(__ior__) + STRUCT_FOR_ID(__ipow__) + STRUCT_FOR_ID(__irshift__) + STRUCT_FOR_ID(__isabstractmethod__) + STRUCT_FOR_ID(__isub__) + STRUCT_FOR_ID(__iter__) + STRUCT_FOR_ID(__itruediv__) + STRUCT_FOR_ID(__ixor__) + STRUCT_FOR_ID(__le__) + STRUCT_FOR_ID(__len__) + STRUCT_FOR_ID(__length_hint__) + STRUCT_FOR_ID(__lltrace__) + STRUCT_FOR_ID(__loader__) + STRUCT_FOR_ID(__lshift__) + STRUCT_FOR_ID(__lt__) + STRUCT_FOR_ID(__main__) + STRUCT_FOR_ID(__matmul__) + STRUCT_FOR_ID(__missing__) + STRUCT_FOR_ID(__mod__) + STRUCT_FOR_ID(__module__) + STRUCT_FOR_ID(__mro_entries__) + STRUCT_FOR_ID(__mul__) + STRUCT_FOR_ID(__name__) + STRUCT_FOR_ID(__ne__) + STRUCT_FOR_ID(__neg__) + STRUCT_FOR_ID(__new__) + STRUCT_FOR_ID(__newobj__) + STRUCT_FOR_ID(__newobj_ex__) + STRUCT_FOR_ID(__next__) + STRUCT_FOR_ID(__notes__) + STRUCT_FOR_ID(__or__) + STRUCT_FOR_ID(__orig_class__) + STRUCT_FOR_ID(__origin__) + STRUCT_FOR_ID(__package__) + STRUCT_FOR_ID(__parameters__) + STRUCT_FOR_ID(__path__) + STRUCT_FOR_ID(__pos__) + STRUCT_FOR_ID(__pow__) + STRUCT_FOR_ID(__prepare__) + STRUCT_FOR_ID(__qualname__) + STRUCT_FOR_ID(__radd__) + STRUCT_FOR_ID(__rand__) + STRUCT_FOR_ID(__rdivmod__) + STRUCT_FOR_ID(__reduce__) + STRUCT_FOR_ID(__reduce_ex__) + STRUCT_FOR_ID(__repr__) + STRUCT_FOR_ID(__reversed__) + STRUCT_FOR_ID(__rfloordiv__) + STRUCT_FOR_ID(__rlshift__) + STRUCT_FOR_ID(__rmatmul__) + STRUCT_FOR_ID(__rmod__) + STRUCT_FOR_ID(__rmul__) + STRUCT_FOR_ID(__ror__) + STRUCT_FOR_ID(__round__) + STRUCT_FOR_ID(__rpow__) + STRUCT_FOR_ID(__rrshift__) + STRUCT_FOR_ID(__rshift__) + STRUCT_FOR_ID(__rsub__) + STRUCT_FOR_ID(__rtruediv__) + STRUCT_FOR_ID(__rxor__) + STRUCT_FOR_ID(__set__) + STRUCT_FOR_ID(__set_name__) + STRUCT_FOR_ID(__setattr__) + STRUCT_FOR_ID(__setitem__) + STRUCT_FOR_ID(__setstate__) + STRUCT_FOR_ID(__sizeof__) + STRUCT_FOR_ID(__slotnames__) + STRUCT_FOR_ID(__slots__) + STRUCT_FOR_ID(__spec__) + STRUCT_FOR_ID(__str__) + STRUCT_FOR_ID(__sub__) + STRUCT_FOR_ID(__subclasscheck__) + STRUCT_FOR_ID(__subclasshook__) + STRUCT_FOR_ID(__truediv__) + STRUCT_FOR_ID(__trunc__) + STRUCT_FOR_ID(__typing_is_unpacked_typevartuple__) + STRUCT_FOR_ID(__typing_prepare_subst__) + STRUCT_FOR_ID(__typing_subst__) + STRUCT_FOR_ID(__typing_unpacked_tuple_args__) + STRUCT_FOR_ID(__warningregistry__) + STRUCT_FOR_ID(__weakref__) + STRUCT_FOR_ID(__xor__) + STRUCT_FOR_ID(_abc_impl) + STRUCT_FOR_ID(_annotation) + STRUCT_FOR_ID(_blksize) + STRUCT_FOR_ID(_bootstrap) + STRUCT_FOR_ID(_dealloc_warn) + STRUCT_FOR_ID(_finalizing) + STRUCT_FOR_ID(_find_and_load) + STRUCT_FOR_ID(_fix_up_module) + STRUCT_FOR_ID(_get_sourcefile) + STRUCT_FOR_ID(_handle_fromlist) + STRUCT_FOR_ID(_initializing) + STRUCT_FOR_ID(_is_text_encoding) + STRUCT_FOR_ID(_lock_unlock_module) + STRUCT_FOR_ID(_showwarnmsg) + STRUCT_FOR_ID(_shutdown) + STRUCT_FOR_ID(_slotnames) + STRUCT_FOR_ID(_strptime_time) + STRUCT_FOR_ID(_uninitialized_submodules) + STRUCT_FOR_ID(_warn_unawaited_coroutine) + STRUCT_FOR_ID(_xoptions) + STRUCT_FOR_ID(add) + STRUCT_FOR_ID(append) + STRUCT_FOR_ID(big) + STRUCT_FOR_ID(buffer) + STRUCT_FOR_ID(builtins) + STRUCT_FOR_ID(c_call) + STRUCT_FOR_ID(c_exception) + STRUCT_FOR_ID(c_return) + STRUCT_FOR_ID(call) + STRUCT_FOR_ID(clear) + STRUCT_FOR_ID(close) + STRUCT_FOR_ID(closed) + STRUCT_FOR_ID(code) + STRUCT_FOR_ID(copy) + STRUCT_FOR_ID(copyreg) + STRUCT_FOR_ID(decode) + STRUCT_FOR_ID(default) + STRUCT_FOR_ID(defaultaction) + STRUCT_FOR_ID(dictcomp) + STRUCT_FOR_ID(difference_update) + STRUCT_FOR_ID(dispatch_table) + STRUCT_FOR_ID(displayhook) + STRUCT_FOR_ID(enable) + STRUCT_FOR_ID(encode) + STRUCT_FOR_ID(encoding) + STRUCT_FOR_ID(end_lineno) + STRUCT_FOR_ID(end_offset) + STRUCT_FOR_ID(errors) + STRUCT_FOR_ID(excepthook) + STRUCT_FOR_ID(exception) + STRUCT_FOR_ID(extend) + STRUCT_FOR_ID(filename) + STRUCT_FOR_ID(fileno) + STRUCT_FOR_ID(fillvalue) + STRUCT_FOR_ID(filters) + STRUCT_FOR_ID(find_class) + STRUCT_FOR_ID(flush) + STRUCT_FOR_ID(genexpr) + STRUCT_FOR_ID(get) + STRUCT_FOR_ID(get_source) + STRUCT_FOR_ID(getattr) + STRUCT_FOR_ID(getstate) + STRUCT_FOR_ID(ignore) + STRUCT_FOR_ID(importlib) + STRUCT_FOR_ID(inf) + STRUCT_FOR_ID(intersection) + STRUCT_FOR_ID(isatty) + STRUCT_FOR_ID(isinstance) + STRUCT_FOR_ID(items) + STRUCT_FOR_ID(iter) + STRUCT_FOR_ID(join) + STRUCT_FOR_ID(keys) + STRUCT_FOR_ID(lambda) + STRUCT_FOR_ID(last_traceback) + STRUCT_FOR_ID(last_type) + STRUCT_FOR_ID(last_value) + STRUCT_FOR_ID(latin1) + STRUCT_FOR_ID(len) + STRUCT_FOR_ID(line) + STRUCT_FOR_ID(lineno) + STRUCT_FOR_ID(listcomp) + STRUCT_FOR_ID(little) + STRUCT_FOR_ID(locale) + STRUCT_FOR_ID(match) + STRUCT_FOR_ID(metaclass) + STRUCT_FOR_ID(mode) + STRUCT_FOR_ID(modules) + STRUCT_FOR_ID(mro) + STRUCT_FOR_ID(msg) + STRUCT_FOR_ID(n_fields) + STRUCT_FOR_ID(n_sequence_fields) + STRUCT_FOR_ID(n_unnamed_fields) + STRUCT_FOR_ID(name) + STRUCT_FOR_ID(newlines) + STRUCT_FOR_ID(next) + STRUCT_FOR_ID(obj) + STRUCT_FOR_ID(offset) + STRUCT_FOR_ID(onceregistry) + STRUCT_FOR_ID(opcode) + STRUCT_FOR_ID(open) + STRUCT_FOR_ID(parent) + STRUCT_FOR_ID(partial) + STRUCT_FOR_ID(path) + STRUCT_FOR_ID(peek) + STRUCT_FOR_ID(persistent_id) + STRUCT_FOR_ID(persistent_load) + STRUCT_FOR_ID(print_file_and_line) + STRUCT_FOR_ID(ps1) + STRUCT_FOR_ID(ps2) + STRUCT_FOR_ID(raw) + STRUCT_FOR_ID(read) + STRUCT_FOR_ID(read1) + STRUCT_FOR_ID(readable) + STRUCT_FOR_ID(readall) + STRUCT_FOR_ID(readinto) + STRUCT_FOR_ID(readinto1) + STRUCT_FOR_ID(readline) + STRUCT_FOR_ID(reducer_override) + STRUCT_FOR_ID(reload) + STRUCT_FOR_ID(replace) + STRUCT_FOR_ID(reset) + STRUCT_FOR_ID(return) + STRUCT_FOR_ID(reversed) + STRUCT_FOR_ID(seek) + STRUCT_FOR_ID(seekable) + STRUCT_FOR_ID(send) + STRUCT_FOR_ID(setcomp) + STRUCT_FOR_ID(setstate) + STRUCT_FOR_ID(sort) + STRUCT_FOR_ID(stderr) + STRUCT_FOR_ID(stdin) + STRUCT_FOR_ID(stdout) + STRUCT_FOR_ID(strict) + STRUCT_FOR_ID(symmetric_difference_update) + STRUCT_FOR_ID(tell) + STRUCT_FOR_ID(text) + STRUCT_FOR_ID(threading) + STRUCT_FOR_ID(throw) + STRUCT_FOR_ID(top) + STRUCT_FOR_ID(truncate) + STRUCT_FOR_ID(unraisablehook) + STRUCT_FOR_ID(values) + STRUCT_FOR_ID(version) + STRUCT_FOR_ID(warnings) + STRUCT_FOR_ID(warnoptions) + STRUCT_FOR_ID(writable) + STRUCT_FOR_ID(write) + STRUCT_FOR_ID(zipimporter) + } identifiers; + struct { + PyASCIIObject _ascii; + uint8_t _data[2]; + } ascii[128]; + struct { + PyCompactUnicodeObject _latin1; + uint8_t _data[2]; + } latin1[128]; +}; +/* End auto-generated code */ + +#undef ID +#undef STR + + +#define _Py_ID(NAME) \ + (_Py_SINGLETON(strings.identifiers._ ## NAME._ascii.ob_base)) +#define _Py_STR(NAME) \ + (_Py_SINGLETON(strings.literals._ ## NAME._ascii.ob_base)) + +/* _Py_DECLARE_STR() should precede all uses of _Py_STR() in a function. + + This is true even if the same string has already been declared + elsewhere, even in the same file. Mismatched duplicates are detected + by Tools/scripts/generate-global-objects.py. + + Pairing _Py_DECLARE_STR() with every use of _Py_STR() makes sure the + string keeps working even if the declaration is removed somewhere + else. It also makes it clear what the actual string is at every + place it is being used. */ +#define _Py_DECLARE_STR(name, str) + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_GLOBAL_STRINGS_H */ diff --git a/src/external/windows/include/python/internal/pycore_hamt.h b/src/external/windows/include/python/internal/pycore_hamt.h index f6abb113..0cb2b672 100755 --- a/src/external/windows/include/python/internal/pycore_hamt.h +++ b/src/external/windows/include/python/internal/pycore_hamt.h @@ -5,8 +5,35 @@ # error "this header requires Py_BUILD_CORE define" #endif -#define _Py_HAMT_MAX_TREE_DEPTH 7 +/* +HAMT tree is shaped by hashes of keys. Every group of 5 bits of a hash denotes +the exact position of the key in one level of the tree. Since we're using +32 bit hashes, we can have at most 7 such levels. Although if there are +two distinct keys with equal hashes, they will have to occupy the same +cell in the 7th level of the tree -- so we'd put them in a "collision" node. +Which brings the total possible tree depth to 8. Read more about the actual +layout of the HAMT tree in `hamt.c`. + +This constant is used to define a datastucture for storing iteration state. +*/ +#define _Py_HAMT_MAX_TREE_DEPTH 8 + + +extern PyTypeObject _PyHamt_Type; +extern PyTypeObject _PyHamt_ArrayNode_Type; +extern PyTypeObject _PyHamt_BitmapNode_Type; +extern PyTypeObject _PyHamt_CollisionNode_Type; +extern PyTypeObject _PyHamtKeys_Type; +extern PyTypeObject _PyHamtValues_Type; +extern PyTypeObject _PyHamtItems_Type; + +/* runtime lifecycle */ + +void _PyHamt_Fini(PyInterpreterState *); + + +/* other API */ #define PyHamt_Check(o) Py_IS_TYPE(o, &_PyHamt_Type) @@ -61,15 +88,6 @@ typedef struct { } PyHamtIterator; -PyAPI_DATA(PyTypeObject) _PyHamt_Type; -PyAPI_DATA(PyTypeObject) _PyHamt_ArrayNode_Type; -PyAPI_DATA(PyTypeObject) _PyHamt_BitmapNode_Type; -PyAPI_DATA(PyTypeObject) _PyHamt_CollisionNode_Type; -PyAPI_DATA(PyTypeObject) _PyHamtKeys_Type; -PyAPI_DATA(PyTypeObject) _PyHamtValues_Type; -PyAPI_DATA(PyTypeObject) _PyHamtItems_Type; - - /* Create a new HAMT immutable mapping. */ PyHamtObject * _PyHamt_New(void); @@ -110,7 +128,4 @@ PyObject * _PyHamt_NewIterValues(PyHamtObject *o); /* Return a Items iterator over "o". */ PyObject * _PyHamt_NewIterItems(PyHamtObject *o); -int _PyHamt_Init(void); -void _PyHamt_Fini(void); - #endif /* !Py_INTERNAL_HAMT_H */ diff --git a/src/external/windows/include/python/internal/pycore_import.h b/src/external/windows/include/python/internal/pycore_import.h index e629c0b7..8b9bd663 100755 --- a/src/external/windows/include/python/internal/pycore_import.h +++ b/src/external/windows/include/python/internal/pycore_import.h @@ -10,6 +10,16 @@ extern PyStatus _PyImport_ReInitLock(void); #endif extern PyObject* _PyImport_BootstrapImp(PyThreadState *tstate); +struct _module_alias { + const char *name; /* ASCII encoded string */ + const char *orig; /* ASCII encoded string */ +}; + +PyAPI_DATA(const struct _frozen *) _PyImport_FrozenBootstrap; +PyAPI_DATA(const struct _frozen *) _PyImport_FrozenStdlib; +PyAPI_DATA(const struct _frozen *) _PyImport_FrozenTest; +extern const struct _module_alias * _PyImport_FrozenAliases; + #ifdef __cplusplus } #endif diff --git a/src/external/windows/include/python/internal/pycore_initconfig.h b/src/external/windows/include/python/internal/pycore_initconfig.h index 79dcaf8e..5e0c2453 100755 --- a/src/external/windows/include/python/internal/pycore_initconfig.h +++ b/src/external/windows/include/python/internal/pycore_initconfig.h @@ -155,6 +155,7 @@ extern PyStatus _PyConfig_Copy( extern PyStatus _PyConfig_InitPathConfig( PyConfig *config, int compute_path_config); +extern PyStatus _PyConfig_InitImportConfig(PyConfig *config); extern PyStatus _PyConfig_Read(PyConfig *config, int compute_path_config); extern PyStatus _PyConfig_Write(const PyConfig *config, struct pyruntimestate *runtime); @@ -165,6 +166,12 @@ extern PyStatus _PyConfig_SetPyArgv( PyAPI_FUNC(PyObject*) _PyConfig_AsDict(const PyConfig *config); PyAPI_FUNC(int) _PyConfig_FromDict(PyConfig *config, PyObject *dict); +extern void _Py_DumpPathConfig(PyThreadState *tstate); + +PyAPI_FUNC(PyObject*) _Py_Get_Getpath_CodeObject(void); + +extern int _Py_global_config_int_max_str_digits; + /* --- Function used for testing ---------------------------------- */ diff --git a/src/external/windows/include/python/internal/pycore_interp.h b/src/external/windows/include/python/internal/pycore_interp.h index 877a9d37..6e072fee 100755 --- a/src/external/windows/include/python/internal/pycore_interp.h +++ b/src/external/windows/include/python/internal/pycore_interp.h @@ -8,10 +8,22 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif +#include + #include "pycore_atomic.h" // _Py_atomic_address #include "pycore_ast_state.h" // struct ast_state +#include "pycore_code.h" // struct callable_cache +#include "pycore_context.h" // struct _Py_context_state +#include "pycore_dict.h" // struct _Py_dict_state +#include "pycore_exceptions.h" // struct _Py_exc_state +#include "pycore_floatobject.h" // struct _Py_float_state +#include "pycore_genobject.h" // struct _Py_async_gen_state #include "pycore_gil.h" // struct _gil_runtime_state #include "pycore_gc.h" // struct _gc_runtime_state +#include "pycore_list.h" // struct _Py_list_state +#include "pycore_tuple.h" // struct _Py_tuple_state +#include "pycore_typeobject.h" // struct type_cache +#include "pycore_unicodeobject.h" // struct _Py_unicode_state #include "pycore_warnings.h" // struct _warnings_runtime_state struct _pending_calls { @@ -39,129 +51,6 @@ struct _ceval_state { /* Request for dropping the GIL */ _Py_atomic_int gil_drop_request; struct _pending_calls pending; -#ifdef EXPERIMENTAL_ISOLATED_SUBINTERPRETERS - struct _gil_runtime_state gil; -#endif -}; - -/* fs_codec.encoding is initialized to NULL. - Later, it is set to a non-NULL string by _PyUnicode_InitEncodings(). */ -struct _Py_unicode_fs_codec { - char *encoding; // Filesystem encoding (encoded to UTF-8) - int utf8; // encoding=="utf-8"? - char *errors; // Filesystem errors (encoded to UTF-8) - _Py_error_handler error_handler; -}; - -struct _Py_bytes_state { - PyObject *empty_string; - PyBytesObject *characters[256]; -}; - -struct _Py_unicode_ids { - Py_ssize_t size; - PyObject **array; -}; - -struct _Py_unicode_state { - // The empty Unicode object is a singleton to improve performance. - PyObject *empty_string; - /* Single character Unicode strings in the Latin-1 range are being - shared as well. */ - PyObject *latin1[256]; - struct _Py_unicode_fs_codec fs_codec; - - // Unused member kept for ABI backward compatibility with Python 3.10.0: - // see bpo-46006. - PyObject *unused_interned; - - // Unicode identifiers (_Py_Identifier): see _PyUnicode_FromId() - struct _Py_unicode_ids ids; -}; - -struct _Py_float_state { - /* Special free list - free_list is a singly-linked list of available PyFloatObjects, - linked via abuse of their ob_type members. */ - int numfree; - PyFloatObject *free_list; -}; - -/* Speed optimization to avoid frequent malloc/free of small tuples */ -#ifndef PyTuple_MAXSAVESIZE - // Largest tuple to save on free list -# define PyTuple_MAXSAVESIZE 20 -#endif -#ifndef PyTuple_MAXFREELIST - // Maximum number of tuples of each size to save -# define PyTuple_MAXFREELIST 2000 -#endif - -struct _Py_tuple_state { -#if PyTuple_MAXSAVESIZE > 0 - /* Entries 1 up to PyTuple_MAXSAVESIZE are free lists, - entry 0 is the empty tuple () of which at most one instance - will be allocated. */ - PyTupleObject *free_list[PyTuple_MAXSAVESIZE]; - int numfree[PyTuple_MAXSAVESIZE]; -#endif -}; - -/* Empty list reuse scheme to save calls to malloc and free */ -#ifndef PyList_MAXFREELIST -# define PyList_MAXFREELIST 80 -#endif - -struct _Py_list_state { - PyListObject *free_list[PyList_MAXFREELIST]; - int numfree; -}; - -#ifndef PyDict_MAXFREELIST -# define PyDict_MAXFREELIST 80 -#endif - -struct _Py_dict_state { - /* Dictionary reuse scheme to save calls to malloc and free */ - PyDictObject *free_list[PyDict_MAXFREELIST]; - int numfree; - PyDictKeysObject *keys_free_list[PyDict_MAXFREELIST]; - int keys_numfree; -}; - -struct _Py_frame_state { - PyFrameObject *free_list; - /* number of frames currently in free_list */ - int numfree; -}; - -#ifndef _PyAsyncGen_MAXFREELIST -# define _PyAsyncGen_MAXFREELIST 80 -#endif - -struct _Py_async_gen_state { - /* Freelists boost performance 6-10%; they also reduce memory - fragmentation, as _PyAsyncGenWrappedValue and PyAsyncGenASend - are short-living objects that are instantiated for every - __anext__() call. */ - struct _PyAsyncGenWrappedValue* value_freelist[_PyAsyncGen_MAXFREELIST]; - int value_numfree; - - struct PyAsyncGenASend* asend_freelist[_PyAsyncGen_MAXFREELIST]; - int asend_numfree; -}; - -struct _Py_context_state { - // List of free PyContext objects - PyContext *freelist; - int numfree; -}; - -struct _Py_exc_state { - // The dict mapping from errno codes to OSError subclasses - PyObject *errnomap; - PyBaseExceptionObject *memerrors_freelist; - int memerrors_numfree; }; @@ -179,42 +68,29 @@ struct atexit_state { }; -// Type attribute lookup cache: speed up attribute and method lookups, -// see _PyType_Lookup(). -struct type_cache_entry { - unsigned int version; // initialized from type->tp_version_tag - PyObject *name; // reference to exactly a str or None - PyObject *value; // borrowed reference or NULL -}; - -#define MCACHE_SIZE_EXP 12 -#define MCACHE_STATS 0 - -struct type_cache { - struct type_cache_entry hashtable[1 << MCACHE_SIZE_EXP]; -#if MCACHE_STATS - size_t hits; - size_t misses; - size_t collisions; -#endif -}; - - /* interpreter state */ -#define _PY_NSMALLPOSINTS 257 -#define _PY_NSMALLNEGINTS 5 +/* PyInterpreterState holds the global state for one of the runtime's + interpreters. Typically the initial (main) interpreter is the only one. -// _PyLong_GetZero() and _PyLong_GetOne() must always be available -#if _PY_NSMALLPOSINTS < 2 -# error "_PY_NSMALLPOSINTS must be greater than 1" -#endif - -// The PyInterpreterState typedef is in Include/pystate.h. + The PyInterpreterState typedef is in Include/pystate.h. + */ struct _is { - struct _is *next; - struct _ts *tstate_head; + PyInterpreterState *next; + + struct pythreads { + uint64_t next_unique_id; + /* The linked list of threads, newest first. */ + PyThreadState *head; + /* Used in Modules/_threadmodule.c. */ + long count; + /* Support for runtime thread stack size tuning. + A value of 0 means using the platform's default stack size + or the size specified by the THREAD_STACK_SIZE macro. */ + /* Used in Python/thread.c. */ + size_t stacksize; + } threads; /* Reference to the _PyRuntime global variable. This field exists to not have to pass runtime in addition to tstate to a function. @@ -226,8 +102,16 @@ struct _is { int requires_idref; PyThread_type_lock id_mutex; + /* Has been initialized to a safe state. + + In order to be effective, this must be set to 0 during or right + after allocation. */ + int _initialized; int finalizing; + /* Was this interpreter statically allocated? */ + bool _static; + struct _ceval_state ceval; struct _gc_runtime_state gc; @@ -240,14 +124,9 @@ struct _is { PyObject *builtins; // importlib module PyObject *importlib; - - /* Used in Modules/_threadmodule.c. */ - long num_threads; - /* Support for runtime thread stack size tuning. - A value of 0 means using the platform's default stack size - or the size specified by the THREAD_STACK_SIZE macro. */ - /* Used in Python/thread.c. */ - size_t pythread_stacksize; + // override for config->use_frozen_modules (for tests) + // (-1: "off", 1: "on", 0: no override) + int override_frozen_modules; PyObject *codec_search_path; PyObject *codec_search_cache; @@ -275,20 +154,11 @@ struct _is { PyObject *after_forkers_child; #endif - uint64_t tstate_next_unique_id; - struct _warnings_runtime_state warnings; struct atexit_state atexit; PyObject *audit_hooks; - /* Small integers are preallocated in this array so that they - can be shared. - The integers that are preallocated are those in the range - -_PY_NSMALLNEGINTS (inclusive) to _PY_NSMALLPOSINTS (not inclusive). - */ - PyLongObject* small_ints[_PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS]; - struct _Py_bytes_state bytes; struct _Py_unicode_state unicode; struct _Py_float_state float_state; /* Using a cache is very effective since typically only a single slice is @@ -298,15 +168,35 @@ struct _is { struct _Py_tuple_state tuple; struct _Py_list_state list; struct _Py_dict_state dict_state; - struct _Py_frame_state frame; struct _Py_async_gen_state async_gen; struct _Py_context_state context; struct _Py_exc_state exc_state; struct ast_state ast; struct type_cache type_cache; + struct callable_cache callable_cache; + + int int_max_str_digits; + + /* The following fields are here to avoid allocation during init. + The data is exposed through PyInterpreterState pointer fields. + These fields should not be accessed directly outside of init. + + All other PyInterpreterState pointer fields are populated when + needed and default to NULL. + + For now there are some exceptions to that rule, which require + allocation during init. These will be addressed on a case-by-case + basis. Also see _PyRuntimeState regarding the various mutex fields. + */ + + /* the initial PyInterpreterState.threads.head */ + PyThreadState _initial_thread; }; + +/* other API */ + extern void _PyInterpreterState_ClearModules(PyInterpreterState *interp); extern void _PyInterpreterState_Clear(PyThreadState *tstate); @@ -325,11 +215,11 @@ struct _xidregitem { struct _xidregitem *next; }; -PyAPI_FUNC(struct _is*) _PyInterpreterState_LookUpID(int64_t); +PyAPI_FUNC(PyInterpreterState*) _PyInterpreterState_LookUpID(int64_t); -PyAPI_FUNC(int) _PyInterpreterState_IDInitref(struct _is *); -PyAPI_FUNC(int) _PyInterpreterState_IDIncref(struct _is *); -PyAPI_FUNC(void) _PyInterpreterState_IDDecref(struct _is *); +PyAPI_FUNC(int) _PyInterpreterState_IDInitref(PyInterpreterState *); +PyAPI_FUNC(int) _PyInterpreterState_IDIncref(PyInterpreterState *); +PyAPI_FUNC(void) _PyInterpreterState_IDDecref(PyInterpreterState *); #ifdef __cplusplus } diff --git a/src/external/windows/include/python/internal/pycore_interpreteridobject.h b/src/external/windows/include/python/internal/pycore_interpreteridobject.h new file mode 100644 index 00000000..3c477a3d --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_interpreteridobject.h @@ -0,0 +1,22 @@ +/* Interpreter ID Object */ + +#ifndef Py_INTERNAL_INTERPRETERIDOBJECT_H +#define Py_INTERNAL_INTERPRETERIDOBJECT_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +PyAPI_DATA(PyTypeObject) _PyInterpreterID_Type; + +PyAPI_FUNC(PyObject *) _PyInterpreterID_New(int64_t); +PyAPI_FUNC(PyObject *) _PyInterpreterState_GetIDObject(PyInterpreterState *); +PyAPI_FUNC(PyInterpreterState *) _PyInterpreterID_LookUp(PyObject *); + +#ifdef __cplusplus +} +#endif +#endif // !Py_INTERNAL_INTERPRETERIDOBJECT_H diff --git a/src/external/windows/include/python/internal/pycore_list.h b/src/external/windows/include/python/internal/pycore_list.h index 9704e00d..8fd6ec71 100755 --- a/src/external/windows/include/python/internal/pycore_list.h +++ b/src/external/windows/include/python/internal/pycore_list.h @@ -11,8 +11,50 @@ extern "C" { #include "listobject.h" // _PyList_CAST() +/* runtime lifecycle */ + +extern void _PyList_Fini(PyInterpreterState *); + + +/* other API */ + +#ifndef WITH_FREELISTS +// without freelists +# define PyList_MAXFREELIST 0 +#endif + +/* Empty list reuse scheme to save calls to malloc and free */ +#ifndef PyList_MAXFREELIST +# define PyList_MAXFREELIST 80 +#endif + +struct _Py_list_state { +#if PyList_MAXFREELIST > 0 + PyListObject *free_list[PyList_MAXFREELIST]; + int numfree; +#endif +}; + #define _PyList_ITEMS(op) (_PyList_CAST(op)->ob_item) +extern int +_PyList_AppendTakeRefListResize(PyListObject *self, PyObject *newitem); + +static inline int +_PyList_AppendTakeRef(PyListObject *self, PyObject *newitem) +{ + assert(self != NULL && newitem != NULL); + assert(PyList_Check(self)); + Py_ssize_t len = PyList_GET_SIZE(self); + Py_ssize_t allocated = self->allocated; + assert((size_t)len + 1 < PY_SSIZE_T_MAX); + if (allocated > len) { + PyList_SET_ITEM(self, len, newitem); + Py_SET_SIZE(self, len + 1); + return 0; + } + return _PyList_AppendTakeRefListResize(self, newitem); +} #ifdef __cplusplus } diff --git a/src/external/windows/include/python/internal/pycore_long.h b/src/external/windows/include/python/internal/pycore_long.h index c92a7b59..3a800011 100755 --- a/src/external/windows/include/python/internal/pycore_long.h +++ b/src/external/windows/include/python/internal/pycore_long.h @@ -8,31 +8,105 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif -#include "pycore_interp.h" // PyInterpreterState.small_ints -#include "pycore_pystate.h" // _PyThreadState_GET() +#include "pycore_global_objects.h" // _PY_NSMALLNEGINTS +#include "pycore_runtime.h" // _PyRuntime -// Don't call this function but _PyLong_GetZero() and _PyLong_GetOne() -static inline PyObject* __PyLong_GetSmallInt_internal(int value) -{ - PyInterpreterState *interp = _PyInterpreterState_GET(); - assert(-_PY_NSMALLNEGINTS <= value && value < _PY_NSMALLPOSINTS); - size_t index = _PY_NSMALLNEGINTS + value; - PyObject *obj = (PyObject*)interp->small_ints[index]; - // _PyLong_GetZero(), _PyLong_GetOne() and get_small_int() must not be - // called before _PyLong_Init() nor after _PyLong_Fini(). - assert(obj != NULL); - return obj; -} +/* + * Default int base conversion size limitation: Denial of Service prevention. + * + * Chosen such that this isn't wildly slow on modern hardware and so that + * everyone's existing deployed numpy test suite passes before + * https://github.com/numpy/numpy/issues/22098 is widely available. + * + * $ python -m timeit -s 's = "1"*4300' 'int(s)' + * 2000 loops, best of 5: 125 usec per loop + * $ python -m timeit -s 's = "1"*4300; v = int(s)' 'str(v)' + * 1000 loops, best of 5: 311 usec per loop + * (zen2 cloud VM) + * + * 4300 decimal digits fits a ~14284 bit number. + */ +#define _PY_LONG_DEFAULT_MAX_STR_DIGITS 4300 +/* + * Threshold for max digits check. For performance reasons int() and + * int.__str__() don't checks values that are smaller than this + * threshold. Acts as a guaranteed minimum size limit for bignums that + * applications can expect from CPython. + * + * % python -m timeit -s 's = "1"*640; v = int(s)' 'str(int(s))' + * 20000 loops, best of 5: 12 usec per loop + * + * "640 digits should be enough for anyone." - gps + * fits a ~2126 bit decimal number. + */ +#define _PY_LONG_MAX_STR_DIGITS_THRESHOLD 640 + +#if ((_PY_LONG_DEFAULT_MAX_STR_DIGITS != 0) && \ + (_PY_LONG_DEFAULT_MAX_STR_DIGITS < _PY_LONG_MAX_STR_DIGITS_THRESHOLD)) +# error "_PY_LONG_DEFAULT_MAX_STR_DIGITS smaller than threshold." +#endif + + +/* runtime lifecycle */ + +extern PyStatus _PyLong_InitTypes(PyInterpreterState *); +extern void _PyLong_FiniTypes(PyInterpreterState *interp); + + +/* other API */ + +#define _PyLong_SMALL_INTS _Py_SINGLETON(small_ints) + +// _PyLong_GetZero() and _PyLong_GetOne() must always be available +// _PyLong_FromUnsignedChar must always be available +#if _PY_NSMALLPOSINTS < 257 +# error "_PY_NSMALLPOSINTS must be greater than or equal to 257" +#endif // Return a borrowed reference to the zero singleton. // The function cannot return NULL. static inline PyObject* _PyLong_GetZero(void) -{ return __PyLong_GetSmallInt_internal(0); } +{ return (PyObject *)&_PyLong_SMALL_INTS[_PY_NSMALLNEGINTS]; } // Return a borrowed reference to the one singleton. // The function cannot return NULL. static inline PyObject* _PyLong_GetOne(void) -{ return __PyLong_GetSmallInt_internal(1); } +{ return (PyObject *)&_PyLong_SMALL_INTS[_PY_NSMALLNEGINTS+1]; } + +static inline PyObject* _PyLong_FromUnsignedChar(unsigned char i) +{ + return Py_NewRef((PyObject *)&_PyLong_SMALL_INTS[_PY_NSMALLNEGINTS+i]); +} + +PyObject *_PyLong_Add(PyLongObject *left, PyLongObject *right); +PyObject *_PyLong_Multiply(PyLongObject *left, PyLongObject *right); +PyObject *_PyLong_Subtract(PyLongObject *left, PyLongObject *right); + +/* Used by Python/mystrtoul.c, _PyBytes_FromHex(), + _PyBytes_DecodeEscape(), etc. */ +PyAPI_DATA(unsigned char) _PyLong_DigitValue[256]; + +/* Format the object based on the format_spec, as defined in PEP 3101 + (Advanced String Formatting). */ +PyAPI_FUNC(int) _PyLong_FormatAdvancedWriter( + _PyUnicodeWriter *writer, + PyObject *obj, + PyObject *format_spec, + Py_ssize_t start, + Py_ssize_t end); + +PyAPI_FUNC(int) _PyLong_FormatWriter( + _PyUnicodeWriter *writer, + PyObject *obj, + int base, + int alternate); + +PyAPI_FUNC(char*) _PyLong_FormatBytesWriter( + _PyBytesWriter *writer, + char *str, + PyObject *obj, + int base, + int alternate); #ifdef __cplusplus } diff --git a/src/external/windows/include/python/internal/pycore_moduleobject.h b/src/external/windows/include/python/internal/pycore_moduleobject.h index 6db5b40d..85d466c5 100755 --- a/src/external/windows/include/python/internal/pycore_moduleobject.h +++ b/src/external/windows/include/python/internal/pycore_moduleobject.h @@ -11,7 +11,7 @@ extern "C" { typedef struct { PyObject_HEAD PyObject *md_dict; - struct PyModuleDef *md_def; + PyModuleDef *md_def; void *md_state; PyObject *md_weaklist; // for logging purposes after md_dict is cleared diff --git a/src/external/windows/include/python/internal/pycore_namespace.h b/src/external/windows/include/python/internal/pycore_namespace.h new file mode 100644 index 00000000..9c1d334a --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_namespace.h @@ -0,0 +1,20 @@ +// Simple namespace object interface + +#ifndef Py_INTERNAL_NAMESPACE_H +#define Py_INTERNAL_NAMESPACE_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +PyAPI_DATA(PyTypeObject) _PyNamespace_Type; + +PyAPI_FUNC(PyObject *) _PyNamespace_New(PyObject *kwds); + +#ifdef __cplusplus +} +#endif +#endif // !Py_INTERNAL_NAMESPACE_H diff --git a/src/external/windows/include/python/internal/pycore_object.h b/src/external/windows/include/python/internal/pycore_object.h index 5c1d8177..72182af6 100755 --- a/src/external/windows/include/python/internal/pycore_object.h +++ b/src/external/windows/include/python/internal/pycore_object.h @@ -8,9 +8,59 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif +#include #include "pycore_gc.h" // _PyObject_GC_IS_TRACKED() #include "pycore_interp.h" // PyInterpreterState.gc #include "pycore_pystate.h" // _PyInterpreterState_GET() +#include "pycore_runtime.h" // _PyRuntime + +#define _PyObject_IMMORTAL_INIT(type) \ + { \ + .ob_refcnt = 999999999, \ + .ob_type = type, \ + } +#define _PyVarObject_IMMORTAL_INIT(type, size) \ + { \ + .ob_base = _PyObject_IMMORTAL_INIT(type), \ + .ob_size = size, \ + } + +PyAPI_FUNC(void) _Py_NO_RETURN _Py_FatalRefcountErrorFunc( + const char *func, + const char *message); + +#define _Py_FatalRefcountError(message) _Py_FatalRefcountErrorFunc(__func__, message) + +static inline void +_Py_DECREF_SPECIALIZED(PyObject *op, const destructor destruct) +{ +#ifdef Py_REF_DEBUG + _Py_RefTotal--; +#endif + if (--op->ob_refcnt != 0) { + assert(op->ob_refcnt > 0); + } + else { +#ifdef Py_TRACE_REFS + _Py_ForgetReference(op); +#endif + destruct(op); + } +} + +static inline void +_Py_DECREF_NO_DEALLOC(PyObject *op) +{ +#ifdef Py_REF_DEBUG + _Py_RefTotal--; +#endif + op->ob_refcnt--; +#ifdef Py_DEBUG + if (op->ob_refcnt <= 0) { + _Py_FatalRefcountError("Expected a positive remaining refcount"); + } +#endif +} PyAPI_FUNC(int) _PyType_CheckConsistency(PyTypeObject *type); PyAPI_FUNC(int) _PyDict_CheckConsistency(PyObject *mp, int check_content); @@ -29,8 +79,6 @@ _PyType_HasFeature(PyTypeObject *type, unsigned long feature) { extern void _PyType_InitCache(PyInterpreterState *interp); -/* Only private in Python 3.10 and 3.9.8+; public in 3.11 */ -extern PyObject *_PyType_GetQualName(PyTypeObject *type); /* Inline functions trading binary compatibility for speed: _PyObject_Init() is the fast version of PyObject_Init(), and @@ -170,6 +218,15 @@ _PyObject_IS_GC(PyObject *obj) // Fast inlined version of PyType_IS_GC() #define _PyType_IS_GC(t) _PyType_HasFeature((t), Py_TPFLAGS_HAVE_GC) +static inline size_t +_PyType_PreHeaderSize(PyTypeObject *tp) +{ + return _PyType_IS_GC(tp) * sizeof(PyGC_Head) + + _PyType_HasFeature(tp, Py_TPFLAGS_MANAGED_DICT) * 2 * sizeof(PyObject *); +} + +void _PyObject_GC_Link(PyObject *op); + // Usage: assert(_Py_CheckSlotResult(obj, "__getitem__", result != NULL)); extern int _Py_CheckSlotResult( PyObject *obj, @@ -180,6 +237,73 @@ extern int _Py_CheckSlotResult( // See also the Py_TPFLAGS_READY flag. #define _PyType_IsReady(type) ((type)->tp_dict != NULL) +// Test if a type supports weak references +static inline int _PyType_SUPPORTS_WEAKREFS(PyTypeObject *type) { + return (type->tp_weaklistoffset > 0); +} + +extern PyObject* _PyType_AllocNoTrack(PyTypeObject *type, Py_ssize_t nitems); + +extern int _PyObject_InitializeDict(PyObject *obj); +extern int _PyObject_StoreInstanceAttribute(PyObject *obj, PyDictValues *values, + PyObject *name, PyObject *value); +PyObject * _PyObject_GetInstanceAttribute(PyObject *obj, PyDictValues *values, + PyObject *name); + +static inline PyDictValues **_PyObject_ValuesPointer(PyObject *obj) +{ + assert(Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT); + return ((PyDictValues **)obj)-4; +} + +static inline PyObject **_PyObject_ManagedDictPointer(PyObject *obj) +{ + assert(Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT); + return ((PyObject **)obj)-3; +} + +#define MANAGED_DICT_OFFSET (((int)sizeof(PyObject *))*-3) + +extern PyObject ** _PyObject_DictPointer(PyObject *); +extern int _PyObject_VisitInstanceAttributes(PyObject *self, visitproc visit, void *arg); +extern void _PyObject_ClearInstanceAttributes(PyObject *self); +extern void _PyObject_FreeInstanceAttributes(PyObject *self); +extern int _PyObject_IsInstanceDictEmpty(PyObject *); +extern PyObject* _PyType_GetSubclasses(PyTypeObject *); + +// Access macro to the members which are floating "behind" the object +#define _PyHeapType_GET_MEMBERS(etype) \ + ((PyMemberDef *)(((char *)etype) + Py_TYPE(etype)->tp_basicsize)) + +PyAPI_FUNC(PyObject *) _PyObject_LookupSpecial(PyObject *, PyObject *); + +/* C function call trampolines to mitigate bad function pointer casts. + * + * Typical native ABIs ignore additional arguments or fill in missing + * values with 0/NULL in function pointer cast. Compilers do not show + * warnings when a function pointer is explicitly casted to an + * incompatible type. + * + * Bad fpcasts are an issue in WebAssembly. WASM's indirect_call has strict + * function signature checks. Argument count, types, and return type must + * match. + * + * Third party code unintentionally rely on problematic fpcasts. The call + * trampoline mitigates common occurences of bad fpcasts on Emscripten. + */ +#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE) +#define _PyCFunction_TrampolineCall(meth, self, args) \ + _PyCFunctionWithKeywords_TrampolineCall( \ + (*(PyCFunctionWithKeywords)(void(*)(void))meth), self, args, NULL) +extern PyObject* _PyCFunctionWithKeywords_TrampolineCall( + PyCFunctionWithKeywords meth, PyObject *, PyObject *, PyObject *); +#else +#define _PyCFunction_TrampolineCall(meth, self, args) \ + (meth)((self), (args)) +#define _PyCFunctionWithKeywords_TrampolineCall(meth, self, args, kw) \ + (meth)((self), (args), (kw)) +#endif // __EMSCRIPTEN__ && PY_CALL_TRAMPOLINE + #ifdef __cplusplus } #endif diff --git a/src/external/windows/include/python/internal/pycore_opcode.h b/src/external/windows/include/python/internal/pycore_opcode.h new file mode 100644 index 00000000..8073de44 --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_opcode.h @@ -0,0 +1,581 @@ +// Auto-generated by Tools/scripts/generate_opcode_h.py from Lib/opcode.py + +#ifndef Py_INTERNAL_OPCODE_H +#define Py_INTERNAL_OPCODE_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +#include "opcode.h" + +extern const uint8_t _PyOpcode_Caches[256]; + +extern const uint8_t _PyOpcode_Deopt[256]; + +#ifdef NEED_OPCODE_TABLES +static const uint32_t _PyOpcode_RelativeJump[8] = { + 0U, + 0U, + 536870912U, + 135118848U, + 4163U, + 122880U, + 0U, + 0U, +}; +static const uint32_t _PyOpcode_Jump[8] = { + 0U, + 0U, + 536870912U, + 135118848U, + 4163U, + 122880U, + 0U, + 0U, +}; + +const uint8_t _PyOpcode_Caches[256] = { + [BINARY_SUBSCR] = 4, + [STORE_SUBSCR] = 1, + [UNPACK_SEQUENCE] = 1, + [STORE_ATTR] = 4, + [LOAD_ATTR] = 4, + [COMPARE_OP] = 2, + [LOAD_GLOBAL] = 5, + [BINARY_OP] = 1, + [LOAD_METHOD] = 10, + [PRECALL] = 1, + [CALL] = 4, +}; + +const uint8_t _PyOpcode_Deopt[256] = { + [ASYNC_GEN_WRAP] = ASYNC_GEN_WRAP, + [BEFORE_ASYNC_WITH] = BEFORE_ASYNC_WITH, + [BEFORE_WITH] = BEFORE_WITH, + [BINARY_OP] = BINARY_OP, + [BINARY_OP_ADAPTIVE] = BINARY_OP, + [BINARY_OP_ADD_FLOAT] = BINARY_OP, + [BINARY_OP_ADD_INT] = BINARY_OP, + [BINARY_OP_ADD_UNICODE] = BINARY_OP, + [BINARY_OP_INPLACE_ADD_UNICODE] = BINARY_OP, + [BINARY_OP_MULTIPLY_FLOAT] = BINARY_OP, + [BINARY_OP_MULTIPLY_INT] = BINARY_OP, + [BINARY_OP_SUBTRACT_FLOAT] = BINARY_OP, + [BINARY_OP_SUBTRACT_INT] = BINARY_OP, + [BINARY_SUBSCR] = BINARY_SUBSCR, + [BINARY_SUBSCR_ADAPTIVE] = BINARY_SUBSCR, + [BINARY_SUBSCR_DICT] = BINARY_SUBSCR, + [BINARY_SUBSCR_GETITEM] = BINARY_SUBSCR, + [BINARY_SUBSCR_LIST_INT] = BINARY_SUBSCR, + [BINARY_SUBSCR_TUPLE_INT] = BINARY_SUBSCR, + [BUILD_CONST_KEY_MAP] = BUILD_CONST_KEY_MAP, + [BUILD_LIST] = BUILD_LIST, + [BUILD_MAP] = BUILD_MAP, + [BUILD_SET] = BUILD_SET, + [BUILD_SLICE] = BUILD_SLICE, + [BUILD_STRING] = BUILD_STRING, + [BUILD_TUPLE] = BUILD_TUPLE, + [CACHE] = CACHE, + [CALL] = CALL, + [CALL_ADAPTIVE] = CALL, + [CALL_FUNCTION_EX] = CALL_FUNCTION_EX, + [CALL_PY_EXACT_ARGS] = CALL, + [CALL_PY_WITH_DEFAULTS] = CALL, + [CHECK_EG_MATCH] = CHECK_EG_MATCH, + [CHECK_EXC_MATCH] = CHECK_EXC_MATCH, + [COMPARE_OP] = COMPARE_OP, + [COMPARE_OP_ADAPTIVE] = COMPARE_OP, + [COMPARE_OP_FLOAT_JUMP] = COMPARE_OP, + [COMPARE_OP_INT_JUMP] = COMPARE_OP, + [COMPARE_OP_STR_JUMP] = COMPARE_OP, + [CONTAINS_OP] = CONTAINS_OP, + [COPY] = COPY, + [COPY_FREE_VARS] = COPY_FREE_VARS, + [DELETE_ATTR] = DELETE_ATTR, + [DELETE_DEREF] = DELETE_DEREF, + [DELETE_FAST] = DELETE_FAST, + [DELETE_GLOBAL] = DELETE_GLOBAL, + [DELETE_NAME] = DELETE_NAME, + [DELETE_SUBSCR] = DELETE_SUBSCR, + [DICT_MERGE] = DICT_MERGE, + [DICT_UPDATE] = DICT_UPDATE, + [END_ASYNC_FOR] = END_ASYNC_FOR, + [EXTENDED_ARG] = EXTENDED_ARG, + [EXTENDED_ARG_QUICK] = EXTENDED_ARG, + [FORMAT_VALUE] = FORMAT_VALUE, + [FOR_ITER] = FOR_ITER, + [GET_AITER] = GET_AITER, + [GET_ANEXT] = GET_ANEXT, + [GET_AWAITABLE] = GET_AWAITABLE, + [GET_ITER] = GET_ITER, + [GET_LEN] = GET_LEN, + [GET_YIELD_FROM_ITER] = GET_YIELD_FROM_ITER, + [IMPORT_FROM] = IMPORT_FROM, + [IMPORT_NAME] = IMPORT_NAME, + [IMPORT_STAR] = IMPORT_STAR, + [IS_OP] = IS_OP, + [JUMP_BACKWARD] = JUMP_BACKWARD, + [JUMP_BACKWARD_NO_INTERRUPT] = JUMP_BACKWARD_NO_INTERRUPT, + [JUMP_BACKWARD_QUICK] = JUMP_BACKWARD, + [JUMP_FORWARD] = JUMP_FORWARD, + [JUMP_IF_FALSE_OR_POP] = JUMP_IF_FALSE_OR_POP, + [JUMP_IF_TRUE_OR_POP] = JUMP_IF_TRUE_OR_POP, + [KW_NAMES] = KW_NAMES, + [LIST_APPEND] = LIST_APPEND, + [LIST_EXTEND] = LIST_EXTEND, + [LIST_TO_TUPLE] = LIST_TO_TUPLE, + [LOAD_ASSERTION_ERROR] = LOAD_ASSERTION_ERROR, + [LOAD_ATTR] = LOAD_ATTR, + [LOAD_ATTR_ADAPTIVE] = LOAD_ATTR, + [LOAD_ATTR_INSTANCE_VALUE] = LOAD_ATTR, + [LOAD_ATTR_MODULE] = LOAD_ATTR, + [LOAD_ATTR_SLOT] = LOAD_ATTR, + [LOAD_ATTR_WITH_HINT] = LOAD_ATTR, + [LOAD_BUILD_CLASS] = LOAD_BUILD_CLASS, + [LOAD_CLASSDEREF] = LOAD_CLASSDEREF, + [LOAD_CLOSURE] = LOAD_CLOSURE, + [LOAD_CONST] = LOAD_CONST, + [LOAD_CONST__LOAD_FAST] = LOAD_CONST, + [LOAD_DEREF] = LOAD_DEREF, + [LOAD_FAST] = LOAD_FAST, + [LOAD_FAST__LOAD_CONST] = LOAD_FAST, + [LOAD_FAST__LOAD_FAST] = LOAD_FAST, + [LOAD_GLOBAL] = LOAD_GLOBAL, + [LOAD_GLOBAL_ADAPTIVE] = LOAD_GLOBAL, + [LOAD_GLOBAL_BUILTIN] = LOAD_GLOBAL, + [LOAD_GLOBAL_MODULE] = LOAD_GLOBAL, + [LOAD_METHOD] = LOAD_METHOD, + [LOAD_METHOD_ADAPTIVE] = LOAD_METHOD, + [LOAD_METHOD_CLASS] = LOAD_METHOD, + [LOAD_METHOD_MODULE] = LOAD_METHOD, + [LOAD_METHOD_NO_DICT] = LOAD_METHOD, + [LOAD_METHOD_WITH_DICT] = LOAD_METHOD, + [LOAD_METHOD_WITH_VALUES] = LOAD_METHOD, + [LOAD_NAME] = LOAD_NAME, + [MAKE_CELL] = MAKE_CELL, + [MAKE_FUNCTION] = MAKE_FUNCTION, + [MAP_ADD] = MAP_ADD, + [MATCH_CLASS] = MATCH_CLASS, + [MATCH_KEYS] = MATCH_KEYS, + [MATCH_MAPPING] = MATCH_MAPPING, + [MATCH_SEQUENCE] = MATCH_SEQUENCE, + [NOP] = NOP, + [POP_EXCEPT] = POP_EXCEPT, + [POP_JUMP_BACKWARD_IF_FALSE] = POP_JUMP_BACKWARD_IF_FALSE, + [POP_JUMP_BACKWARD_IF_NONE] = POP_JUMP_BACKWARD_IF_NONE, + [POP_JUMP_BACKWARD_IF_NOT_NONE] = POP_JUMP_BACKWARD_IF_NOT_NONE, + [POP_JUMP_BACKWARD_IF_TRUE] = POP_JUMP_BACKWARD_IF_TRUE, + [POP_JUMP_FORWARD_IF_FALSE] = POP_JUMP_FORWARD_IF_FALSE, + [POP_JUMP_FORWARD_IF_NONE] = POP_JUMP_FORWARD_IF_NONE, + [POP_JUMP_FORWARD_IF_NOT_NONE] = POP_JUMP_FORWARD_IF_NOT_NONE, + [POP_JUMP_FORWARD_IF_TRUE] = POP_JUMP_FORWARD_IF_TRUE, + [POP_TOP] = POP_TOP, + [PRECALL] = PRECALL, + [PRECALL_ADAPTIVE] = PRECALL, + [PRECALL_BOUND_METHOD] = PRECALL, + [PRECALL_BUILTIN_CLASS] = PRECALL, + [PRECALL_BUILTIN_FAST_WITH_KEYWORDS] = PRECALL, + [PRECALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = PRECALL, + [PRECALL_NO_KW_BUILTIN_FAST] = PRECALL, + [PRECALL_NO_KW_BUILTIN_O] = PRECALL, + [PRECALL_NO_KW_ISINSTANCE] = PRECALL, + [PRECALL_NO_KW_LEN] = PRECALL, + [PRECALL_NO_KW_LIST_APPEND] = PRECALL, + [PRECALL_NO_KW_METHOD_DESCRIPTOR_FAST] = PRECALL, + [PRECALL_NO_KW_METHOD_DESCRIPTOR_NOARGS] = PRECALL, + [PRECALL_NO_KW_METHOD_DESCRIPTOR_O] = PRECALL, + [PRECALL_NO_KW_STR_1] = PRECALL, + [PRECALL_NO_KW_TUPLE_1] = PRECALL, + [PRECALL_NO_KW_TYPE_1] = PRECALL, + [PRECALL_PYFUNC] = PRECALL, + [PREP_RERAISE_STAR] = PREP_RERAISE_STAR, + [PRINT_EXPR] = PRINT_EXPR, + [PUSH_EXC_INFO] = PUSH_EXC_INFO, + [PUSH_NULL] = PUSH_NULL, + [RAISE_VARARGS] = RAISE_VARARGS, + [RERAISE] = RERAISE, + [RESUME] = RESUME, + [RESUME_QUICK] = RESUME, + [RETURN_GENERATOR] = RETURN_GENERATOR, + [RETURN_VALUE] = RETURN_VALUE, + [SEND] = SEND, + [SETUP_ANNOTATIONS] = SETUP_ANNOTATIONS, + [SET_ADD] = SET_ADD, + [SET_UPDATE] = SET_UPDATE, + [STORE_ATTR] = STORE_ATTR, + [STORE_ATTR_ADAPTIVE] = STORE_ATTR, + [STORE_ATTR_INSTANCE_VALUE] = STORE_ATTR, + [STORE_ATTR_SLOT] = STORE_ATTR, + [STORE_ATTR_WITH_HINT] = STORE_ATTR, + [STORE_DEREF] = STORE_DEREF, + [STORE_FAST] = STORE_FAST, + [STORE_FAST__LOAD_FAST] = STORE_FAST, + [STORE_FAST__STORE_FAST] = STORE_FAST, + [STORE_GLOBAL] = STORE_GLOBAL, + [STORE_NAME] = STORE_NAME, + [STORE_SUBSCR] = STORE_SUBSCR, + [STORE_SUBSCR_ADAPTIVE] = STORE_SUBSCR, + [STORE_SUBSCR_DICT] = STORE_SUBSCR, + [STORE_SUBSCR_LIST_INT] = STORE_SUBSCR, + [SWAP] = SWAP, + [UNARY_INVERT] = UNARY_INVERT, + [UNARY_NEGATIVE] = UNARY_NEGATIVE, + [UNARY_NOT] = UNARY_NOT, + [UNARY_POSITIVE] = UNARY_POSITIVE, + [UNPACK_EX] = UNPACK_EX, + [UNPACK_SEQUENCE] = UNPACK_SEQUENCE, + [UNPACK_SEQUENCE_ADAPTIVE] = UNPACK_SEQUENCE, + [UNPACK_SEQUENCE_LIST] = UNPACK_SEQUENCE, + [UNPACK_SEQUENCE_TUPLE] = UNPACK_SEQUENCE, + [UNPACK_SEQUENCE_TWO_TUPLE] = UNPACK_SEQUENCE, + [WITH_EXCEPT_START] = WITH_EXCEPT_START, + [YIELD_VALUE] = YIELD_VALUE, +}; +#endif // NEED_OPCODE_TABLES + +#ifdef Py_DEBUG +static const char *const _PyOpcode_OpName[256] = { + [CACHE] = "CACHE", + [POP_TOP] = "POP_TOP", + [PUSH_NULL] = "PUSH_NULL", + [BINARY_OP_ADAPTIVE] = "BINARY_OP_ADAPTIVE", + [BINARY_OP_ADD_FLOAT] = "BINARY_OP_ADD_FLOAT", + [BINARY_OP_ADD_INT] = "BINARY_OP_ADD_INT", + [BINARY_OP_ADD_UNICODE] = "BINARY_OP_ADD_UNICODE", + [BINARY_OP_INPLACE_ADD_UNICODE] = "BINARY_OP_INPLACE_ADD_UNICODE", + [BINARY_OP_MULTIPLY_FLOAT] = "BINARY_OP_MULTIPLY_FLOAT", + [NOP] = "NOP", + [UNARY_POSITIVE] = "UNARY_POSITIVE", + [UNARY_NEGATIVE] = "UNARY_NEGATIVE", + [UNARY_NOT] = "UNARY_NOT", + [BINARY_OP_MULTIPLY_INT] = "BINARY_OP_MULTIPLY_INT", + [BINARY_OP_SUBTRACT_FLOAT] = "BINARY_OP_SUBTRACT_FLOAT", + [UNARY_INVERT] = "UNARY_INVERT", + [BINARY_OP_SUBTRACT_INT] = "BINARY_OP_SUBTRACT_INT", + [BINARY_SUBSCR_ADAPTIVE] = "BINARY_SUBSCR_ADAPTIVE", + [BINARY_SUBSCR_DICT] = "BINARY_SUBSCR_DICT", + [BINARY_SUBSCR_GETITEM] = "BINARY_SUBSCR_GETITEM", + [BINARY_SUBSCR_LIST_INT] = "BINARY_SUBSCR_LIST_INT", + [BINARY_SUBSCR_TUPLE_INT] = "BINARY_SUBSCR_TUPLE_INT", + [CALL_ADAPTIVE] = "CALL_ADAPTIVE", + [CALL_PY_EXACT_ARGS] = "CALL_PY_EXACT_ARGS", + [CALL_PY_WITH_DEFAULTS] = "CALL_PY_WITH_DEFAULTS", + [BINARY_SUBSCR] = "BINARY_SUBSCR", + [COMPARE_OP_ADAPTIVE] = "COMPARE_OP_ADAPTIVE", + [COMPARE_OP_FLOAT_JUMP] = "COMPARE_OP_FLOAT_JUMP", + [COMPARE_OP_INT_JUMP] = "COMPARE_OP_INT_JUMP", + [COMPARE_OP_STR_JUMP] = "COMPARE_OP_STR_JUMP", + [GET_LEN] = "GET_LEN", + [MATCH_MAPPING] = "MATCH_MAPPING", + [MATCH_SEQUENCE] = "MATCH_SEQUENCE", + [MATCH_KEYS] = "MATCH_KEYS", + [EXTENDED_ARG_QUICK] = "EXTENDED_ARG_QUICK", + [PUSH_EXC_INFO] = "PUSH_EXC_INFO", + [CHECK_EXC_MATCH] = "CHECK_EXC_MATCH", + [CHECK_EG_MATCH] = "CHECK_EG_MATCH", + [JUMP_BACKWARD_QUICK] = "JUMP_BACKWARD_QUICK", + [LOAD_ATTR_ADAPTIVE] = "LOAD_ATTR_ADAPTIVE", + [LOAD_ATTR_INSTANCE_VALUE] = "LOAD_ATTR_INSTANCE_VALUE", + [LOAD_ATTR_MODULE] = "LOAD_ATTR_MODULE", + [LOAD_ATTR_SLOT] = "LOAD_ATTR_SLOT", + [LOAD_ATTR_WITH_HINT] = "LOAD_ATTR_WITH_HINT", + [LOAD_CONST__LOAD_FAST] = "LOAD_CONST__LOAD_FAST", + [LOAD_FAST__LOAD_CONST] = "LOAD_FAST__LOAD_CONST", + [LOAD_FAST__LOAD_FAST] = "LOAD_FAST__LOAD_FAST", + [LOAD_GLOBAL_ADAPTIVE] = "LOAD_GLOBAL_ADAPTIVE", + [LOAD_GLOBAL_BUILTIN] = "LOAD_GLOBAL_BUILTIN", + [WITH_EXCEPT_START] = "WITH_EXCEPT_START", + [GET_AITER] = "GET_AITER", + [GET_ANEXT] = "GET_ANEXT", + [BEFORE_ASYNC_WITH] = "BEFORE_ASYNC_WITH", + [BEFORE_WITH] = "BEFORE_WITH", + [END_ASYNC_FOR] = "END_ASYNC_FOR", + [LOAD_GLOBAL_MODULE] = "LOAD_GLOBAL_MODULE", + [LOAD_METHOD_ADAPTIVE] = "LOAD_METHOD_ADAPTIVE", + [LOAD_METHOD_CLASS] = "LOAD_METHOD_CLASS", + [LOAD_METHOD_MODULE] = "LOAD_METHOD_MODULE", + [LOAD_METHOD_NO_DICT] = "LOAD_METHOD_NO_DICT", + [STORE_SUBSCR] = "STORE_SUBSCR", + [DELETE_SUBSCR] = "DELETE_SUBSCR", + [LOAD_METHOD_WITH_DICT] = "LOAD_METHOD_WITH_DICT", + [LOAD_METHOD_WITH_VALUES] = "LOAD_METHOD_WITH_VALUES", + [PRECALL_ADAPTIVE] = "PRECALL_ADAPTIVE", + [PRECALL_BOUND_METHOD] = "PRECALL_BOUND_METHOD", + [PRECALL_BUILTIN_CLASS] = "PRECALL_BUILTIN_CLASS", + [PRECALL_BUILTIN_FAST_WITH_KEYWORDS] = "PRECALL_BUILTIN_FAST_WITH_KEYWORDS", + [GET_ITER] = "GET_ITER", + [GET_YIELD_FROM_ITER] = "GET_YIELD_FROM_ITER", + [PRINT_EXPR] = "PRINT_EXPR", + [LOAD_BUILD_CLASS] = "LOAD_BUILD_CLASS", + [PRECALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = "PRECALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS", + [PRECALL_NO_KW_BUILTIN_FAST] = "PRECALL_NO_KW_BUILTIN_FAST", + [LOAD_ASSERTION_ERROR] = "LOAD_ASSERTION_ERROR", + [RETURN_GENERATOR] = "RETURN_GENERATOR", + [PRECALL_NO_KW_BUILTIN_O] = "PRECALL_NO_KW_BUILTIN_O", + [PRECALL_NO_KW_ISINSTANCE] = "PRECALL_NO_KW_ISINSTANCE", + [PRECALL_NO_KW_LEN] = "PRECALL_NO_KW_LEN", + [PRECALL_NO_KW_LIST_APPEND] = "PRECALL_NO_KW_LIST_APPEND", + [PRECALL_NO_KW_METHOD_DESCRIPTOR_FAST] = "PRECALL_NO_KW_METHOD_DESCRIPTOR_FAST", + [PRECALL_NO_KW_METHOD_DESCRIPTOR_NOARGS] = "PRECALL_NO_KW_METHOD_DESCRIPTOR_NOARGS", + [LIST_TO_TUPLE] = "LIST_TO_TUPLE", + [RETURN_VALUE] = "RETURN_VALUE", + [IMPORT_STAR] = "IMPORT_STAR", + [SETUP_ANNOTATIONS] = "SETUP_ANNOTATIONS", + [YIELD_VALUE] = "YIELD_VALUE", + [ASYNC_GEN_WRAP] = "ASYNC_GEN_WRAP", + [PREP_RERAISE_STAR] = "PREP_RERAISE_STAR", + [POP_EXCEPT] = "POP_EXCEPT", + [STORE_NAME] = "STORE_NAME", + [DELETE_NAME] = "DELETE_NAME", + [UNPACK_SEQUENCE] = "UNPACK_SEQUENCE", + [FOR_ITER] = "FOR_ITER", + [UNPACK_EX] = "UNPACK_EX", + [STORE_ATTR] = "STORE_ATTR", + [DELETE_ATTR] = "DELETE_ATTR", + [STORE_GLOBAL] = "STORE_GLOBAL", + [DELETE_GLOBAL] = "DELETE_GLOBAL", + [SWAP] = "SWAP", + [LOAD_CONST] = "LOAD_CONST", + [LOAD_NAME] = "LOAD_NAME", + [BUILD_TUPLE] = "BUILD_TUPLE", + [BUILD_LIST] = "BUILD_LIST", + [BUILD_SET] = "BUILD_SET", + [BUILD_MAP] = "BUILD_MAP", + [LOAD_ATTR] = "LOAD_ATTR", + [COMPARE_OP] = "COMPARE_OP", + [IMPORT_NAME] = "IMPORT_NAME", + [IMPORT_FROM] = "IMPORT_FROM", + [JUMP_FORWARD] = "JUMP_FORWARD", + [JUMP_IF_FALSE_OR_POP] = "JUMP_IF_FALSE_OR_POP", + [JUMP_IF_TRUE_OR_POP] = "JUMP_IF_TRUE_OR_POP", + [PRECALL_NO_KW_METHOD_DESCRIPTOR_O] = "PRECALL_NO_KW_METHOD_DESCRIPTOR_O", + [POP_JUMP_FORWARD_IF_FALSE] = "POP_JUMP_FORWARD_IF_FALSE", + [POP_JUMP_FORWARD_IF_TRUE] = "POP_JUMP_FORWARD_IF_TRUE", + [LOAD_GLOBAL] = "LOAD_GLOBAL", + [IS_OP] = "IS_OP", + [CONTAINS_OP] = "CONTAINS_OP", + [RERAISE] = "RERAISE", + [COPY] = "COPY", + [PRECALL_NO_KW_STR_1] = "PRECALL_NO_KW_STR_1", + [BINARY_OP] = "BINARY_OP", + [SEND] = "SEND", + [LOAD_FAST] = "LOAD_FAST", + [STORE_FAST] = "STORE_FAST", + [DELETE_FAST] = "DELETE_FAST", + [PRECALL_NO_KW_TUPLE_1] = "PRECALL_NO_KW_TUPLE_1", + [POP_JUMP_FORWARD_IF_NOT_NONE] = "POP_JUMP_FORWARD_IF_NOT_NONE", + [POP_JUMP_FORWARD_IF_NONE] = "POP_JUMP_FORWARD_IF_NONE", + [RAISE_VARARGS] = "RAISE_VARARGS", + [GET_AWAITABLE] = "GET_AWAITABLE", + [MAKE_FUNCTION] = "MAKE_FUNCTION", + [BUILD_SLICE] = "BUILD_SLICE", + [JUMP_BACKWARD_NO_INTERRUPT] = "JUMP_BACKWARD_NO_INTERRUPT", + [MAKE_CELL] = "MAKE_CELL", + [LOAD_CLOSURE] = "LOAD_CLOSURE", + [LOAD_DEREF] = "LOAD_DEREF", + [STORE_DEREF] = "STORE_DEREF", + [DELETE_DEREF] = "DELETE_DEREF", + [JUMP_BACKWARD] = "JUMP_BACKWARD", + [PRECALL_NO_KW_TYPE_1] = "PRECALL_NO_KW_TYPE_1", + [CALL_FUNCTION_EX] = "CALL_FUNCTION_EX", + [PRECALL_PYFUNC] = "PRECALL_PYFUNC", + [EXTENDED_ARG] = "EXTENDED_ARG", + [LIST_APPEND] = "LIST_APPEND", + [SET_ADD] = "SET_ADD", + [MAP_ADD] = "MAP_ADD", + [LOAD_CLASSDEREF] = "LOAD_CLASSDEREF", + [COPY_FREE_VARS] = "COPY_FREE_VARS", + [RESUME_QUICK] = "RESUME_QUICK", + [RESUME] = "RESUME", + [MATCH_CLASS] = "MATCH_CLASS", + [STORE_ATTR_ADAPTIVE] = "STORE_ATTR_ADAPTIVE", + [STORE_ATTR_INSTANCE_VALUE] = "STORE_ATTR_INSTANCE_VALUE", + [FORMAT_VALUE] = "FORMAT_VALUE", + [BUILD_CONST_KEY_MAP] = "BUILD_CONST_KEY_MAP", + [BUILD_STRING] = "BUILD_STRING", + [STORE_ATTR_SLOT] = "STORE_ATTR_SLOT", + [STORE_ATTR_WITH_HINT] = "STORE_ATTR_WITH_HINT", + [LOAD_METHOD] = "LOAD_METHOD", + [STORE_FAST__LOAD_FAST] = "STORE_FAST__LOAD_FAST", + [LIST_EXTEND] = "LIST_EXTEND", + [SET_UPDATE] = "SET_UPDATE", + [DICT_MERGE] = "DICT_MERGE", + [DICT_UPDATE] = "DICT_UPDATE", + [PRECALL] = "PRECALL", + [STORE_FAST__STORE_FAST] = "STORE_FAST__STORE_FAST", + [STORE_SUBSCR_ADAPTIVE] = "STORE_SUBSCR_ADAPTIVE", + [STORE_SUBSCR_DICT] = "STORE_SUBSCR_DICT", + [STORE_SUBSCR_LIST_INT] = "STORE_SUBSCR_LIST_INT", + [CALL] = "CALL", + [KW_NAMES] = "KW_NAMES", + [POP_JUMP_BACKWARD_IF_NOT_NONE] = "POP_JUMP_BACKWARD_IF_NOT_NONE", + [POP_JUMP_BACKWARD_IF_NONE] = "POP_JUMP_BACKWARD_IF_NONE", + [POP_JUMP_BACKWARD_IF_FALSE] = "POP_JUMP_BACKWARD_IF_FALSE", + [POP_JUMP_BACKWARD_IF_TRUE] = "POP_JUMP_BACKWARD_IF_TRUE", + [UNPACK_SEQUENCE_ADAPTIVE] = "UNPACK_SEQUENCE_ADAPTIVE", + [UNPACK_SEQUENCE_LIST] = "UNPACK_SEQUENCE_LIST", + [UNPACK_SEQUENCE_TUPLE] = "UNPACK_SEQUENCE_TUPLE", + [UNPACK_SEQUENCE_TWO_TUPLE] = "UNPACK_SEQUENCE_TWO_TUPLE", + [181] = "<181>", + [182] = "<182>", + [183] = "<183>", + [184] = "<184>", + [185] = "<185>", + [186] = "<186>", + [187] = "<187>", + [188] = "<188>", + [189] = "<189>", + [190] = "<190>", + [191] = "<191>", + [192] = "<192>", + [193] = "<193>", + [194] = "<194>", + [195] = "<195>", + [196] = "<196>", + [197] = "<197>", + [198] = "<198>", + [199] = "<199>", + [200] = "<200>", + [201] = "<201>", + [202] = "<202>", + [203] = "<203>", + [204] = "<204>", + [205] = "<205>", + [206] = "<206>", + [207] = "<207>", + [208] = "<208>", + [209] = "<209>", + [210] = "<210>", + [211] = "<211>", + [212] = "<212>", + [213] = "<213>", + [214] = "<214>", + [215] = "<215>", + [216] = "<216>", + [217] = "<217>", + [218] = "<218>", + [219] = "<219>", + [220] = "<220>", + [221] = "<221>", + [222] = "<222>", + [223] = "<223>", + [224] = "<224>", + [225] = "<225>", + [226] = "<226>", + [227] = "<227>", + [228] = "<228>", + [229] = "<229>", + [230] = "<230>", + [231] = "<231>", + [232] = "<232>", + [233] = "<233>", + [234] = "<234>", + [235] = "<235>", + [236] = "<236>", + [237] = "<237>", + [238] = "<238>", + [239] = "<239>", + [240] = "<240>", + [241] = "<241>", + [242] = "<242>", + [243] = "<243>", + [244] = "<244>", + [245] = "<245>", + [246] = "<246>", + [247] = "<247>", + [248] = "<248>", + [249] = "<249>", + [250] = "<250>", + [251] = "<251>", + [252] = "<252>", + [253] = "<253>", + [254] = "<254>", + [DO_TRACING] = "DO_TRACING", +}; +#endif + +#define EXTRA_CASES \ + case 181: \ + case 182: \ + case 183: \ + case 184: \ + case 185: \ + case 186: \ + case 187: \ + case 188: \ + case 189: \ + case 190: \ + case 191: \ + case 192: \ + case 193: \ + case 194: \ + case 195: \ + case 196: \ + case 197: \ + case 198: \ + case 199: \ + case 200: \ + case 201: \ + case 202: \ + case 203: \ + case 204: \ + case 205: \ + case 206: \ + case 207: \ + case 208: \ + case 209: \ + case 210: \ + case 211: \ + case 212: \ + case 213: \ + case 214: \ + case 215: \ + case 216: \ + case 217: \ + case 218: \ + case 219: \ + case 220: \ + case 221: \ + case 222: \ + case 223: \ + case 224: \ + case 225: \ + case 226: \ + case 227: \ + case 228: \ + case 229: \ + case 230: \ + case 231: \ + case 232: \ + case 233: \ + case 234: \ + case 235: \ + case 236: \ + case 237: \ + case 238: \ + case 239: \ + case 240: \ + case 241: \ + case 242: \ + case 243: \ + case 244: \ + case 245: \ + case 246: \ + case 247: \ + case 248: \ + case 249: \ + case 250: \ + case 251: \ + case 252: \ + case 253: \ + case 254: \ + ; + +#ifdef __cplusplus +} +#endif +#endif // !Py_INTERNAL_OPCODE_H diff --git a/src/external/windows/include/python/internal/pycore_pathconfig.h b/src/external/windows/include/python/internal/pycore_pathconfig.h index 72a6ba18..d08d046f 100755 --- a/src/external/windows/include/python/internal/pycore_pathconfig.h +++ b/src/external/windows/include/python/internal/pycore_pathconfig.h @@ -8,64 +8,15 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif -typedef struct _PyPathConfig { - /* Full path to the Python program */ - wchar_t *program_full_path; - wchar_t *prefix; - wchar_t *exec_prefix; - /* Set by Py_SetPath(), or computed by _PyConfig_InitPathConfig() */ - wchar_t *module_search_path; - /* Python program name */ - wchar_t *program_name; - /* Set by Py_SetPythonHome() or PYTHONHOME environment variable */ - wchar_t *home; -#ifdef MS_WINDOWS - /* isolated and site_import are used to set Py_IsolatedFlag and - Py_NoSiteFlag flags on Windows in read_pth_file(). These fields - are ignored when their value are equal to -1 (unset). */ - int isolated; - int site_import; - /* Set when a venv is detected */ - wchar_t *base_executable; -#endif -} _PyPathConfig; +PyAPI_FUNC(void) _PyPathConfig_ClearGlobal(void); +extern PyStatus _PyPathConfig_ReadGlobal(PyConfig *config); +extern PyStatus _PyPathConfig_UpdateGlobal(const PyConfig *config); +extern const wchar_t * _PyPathConfig_GetGlobalModuleSearchPath(void); -#ifdef MS_WINDOWS -# define _PyPathConfig_INIT \ - {.module_search_path = NULL, \ - .isolated = -1, \ - .site_import = -1} -#else -# define _PyPathConfig_INIT \ - {.module_search_path = NULL} -#endif -/* Note: _PyPathConfig_INIT sets other fields to 0/NULL */ - -PyAPI_DATA(_PyPathConfig) _Py_path_config; -#ifdef MS_WINDOWS -PyAPI_DATA(wchar_t*) _Py_dll_path; -#endif - -extern void _PyPathConfig_ClearGlobal(void); - -extern PyStatus _PyPathConfig_Calculate( - _PyPathConfig *pathconfig, - const PyConfig *config); extern int _PyPathConfig_ComputeSysPath0( const PyWideStringList *argv, PyObject **path0); -extern PyStatus _Py_FindEnvConfigValue( - FILE *env_file, - const wchar_t *key, - wchar_t **value_p); -#ifdef MS_WINDOWS -extern wchar_t* _Py_GetDLLPath(void); -#endif - -extern PyStatus _PyConfig_WritePathConfig(const PyConfig *config); -extern void _Py_DumpPathConfig(PyThreadState *tstate); -extern PyObject* _PyPathConfig_AsDict(void); #ifdef __cplusplus } diff --git a/src/external/windows/include/python/internal/pycore_pyerrors.h b/src/external/windows/include/python/internal/pycore_pyerrors.h index 99c5fd46..5bb24c5c 100755 --- a/src/external/windows/include/python/internal/pycore_pyerrors.h +++ b/src/external/windows/include/python/internal/pycore_pyerrors.h @@ -8,6 +8,15 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif + +/* runtime lifecycle */ + +extern PyStatus _PyErr_InitTypes(PyInterpreterState *); +extern void _PyErr_FiniTypes(PyInterpreterState *); + + +/* other API */ + static inline PyObject* _PyErr_Occurred(PyThreadState *tstate) { assert(tstate != NULL); @@ -16,18 +25,11 @@ static inline PyObject* _PyErr_Occurred(PyThreadState *tstate) static inline void _PyErr_ClearExcState(_PyErr_StackItem *exc_state) { - PyObject *t, *v, *tb; - t = exc_state->exc_type; - v = exc_state->exc_value; - tb = exc_state->exc_traceback; - exc_state->exc_type = NULL; - exc_state->exc_value = NULL; - exc_state->exc_traceback = NULL; - Py_XDECREF(t); - Py_XDECREF(v); - Py_XDECREF(tb); + Py_CLEAR(exc_state->exc_value); } +PyAPI_FUNC(PyObject*) _PyErr_StackItemToExcInfoTuple( + _PyErr_StackItem *err_info); PyAPI_FUNC(void) _PyErr_Fetch( PyThreadState *tstate, @@ -82,6 +84,14 @@ PyAPI_FUNC(PyObject *) _PyErr_FormatFromCauseTstate( const char *format, ...); +PyAPI_FUNC(PyObject *) _PyExc_CreateExceptionGroup( + const char *msg, + PyObject *excs); + +PyAPI_FUNC(PyObject *) _PyExc_PrepReraiseStar( + PyObject *orig, + PyObject *excs); + PyAPI_FUNC(int) _PyErr_CheckSignalsTstate(PyThreadState *tstate); PyAPI_FUNC(void) _Py_DumpExtensionModules(int fd, PyInterpreterState *interp); diff --git a/src/external/windows/include/python/internal/pycore_pylifecycle.h b/src/external/windows/include/python/internal/pycore_pylifecycle.h index 98bdba2f..f08ae599 100755 --- a/src/external/windows/include/python/internal/pycore_pylifecycle.h +++ b/src/external/windows/include/python/internal/pycore_pylifecycle.h @@ -8,24 +8,8 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif -#ifdef HAVE_SIGNAL_H -#include -#endif - #include "pycore_runtime.h" // _PyRuntimeState -#ifndef NSIG -# if defined(_NSIG) -# define NSIG _NSIG /* For BSD/SysV */ -# elif defined(_SIGMAX) -# define NSIG (_SIGMAX + 1) /* For QNX */ -# elif defined(SIGMAX) -# define NSIG (SIGMAX + 1) /* For djgpp */ -# else -# define NSIG 64 /* Use a reasonable default value */ -# endif -#endif - /* Forward declarations */ struct _PyArgv; struct pyruntimestate; @@ -49,13 +33,6 @@ PyAPI_FUNC(int) _Py_IsLocaleCoercionTarget(const char *ctype_loc); /* Various one-time initializers */ -extern PyStatus _PyUnicode_Init(PyInterpreterState *interp); -extern PyStatus _PyUnicode_InitTypes(void); -extern PyStatus _PyBytes_Init(PyInterpreterState *interp); -extern int _PyStructSequence_Init(void); -extern int _PyLong_Init(PyInterpreterState *interp); -extern int _PyLong_InitTypes(void); -extern PyStatus _PyTuple_Init(PyInterpreterState *interp); extern PyStatus _PyFaulthandler_Init(int enable); extern int _PyTraceMalloc_Init(int enable); extern PyObject * _PyBuiltin_Init(PyInterpreterState *interp); @@ -65,49 +42,33 @@ extern PyStatus _PySys_Create( extern PyStatus _PySys_ReadPreinitWarnOptions(PyWideStringList *options); extern PyStatus _PySys_ReadPreinitXOptions(PyConfig *config); extern int _PySys_UpdateConfig(PyThreadState *tstate); -extern PyStatus _PyExc_Init(PyInterpreterState *interp); -extern PyStatus _PyErr_InitTypes(void); -extern PyStatus _PyBuiltins_AddExceptions(PyObject * bltinmod); -extern void _PyFloat_Init(void); -extern int _PyFloat_InitTypes(void); +extern void _PySys_Fini(PyInterpreterState *interp); +extern int _PyBuiltins_AddExceptions(PyObject * bltinmod); extern PyStatus _Py_HashRandomization_Init(const PyConfig *); -extern PyStatus _PyTypes_Init(void); -extern PyStatus _PyTypes_InitSlotDefs(void); extern PyStatus _PyImportZip_Init(PyThreadState *tstate); extern PyStatus _PyGC_Init(PyInterpreterState *interp); extern PyStatus _PyAtExit_Init(PyInterpreterState *interp); - +extern int _Py_Deepfreeze_Init(void); /* Various internal finalizers */ -extern void _PyFrame_Fini(PyInterpreterState *interp); -extern void _PyDict_Fini(PyInterpreterState *interp); -extern void _PyTuple_Fini(PyInterpreterState *interp); -extern void _PyList_Fini(PyInterpreterState *interp); -extern void _PyBytes_Fini(PyInterpreterState *interp); -extern void _PyFloat_Fini(PyInterpreterState *interp); -extern void _PySlice_Fini(PyInterpreterState *interp); -extern void _PyAsyncGen_Fini(PyInterpreterState *interp); - extern int _PySignal_Init(int install_signal_handlers); extern void _PySignal_Fini(void); -extern void _PyExc_Fini(PyInterpreterState *interp); extern void _PyImport_Fini(void); extern void _PyImport_Fini2(void); extern void _PyGC_Fini(PyInterpreterState *interp); -extern void _PyType_Fini(PyInterpreterState *interp); extern void _Py_HashRandomization_Fini(void); -extern void _PyUnicode_Fini(PyInterpreterState *interp); -extern void _PyUnicode_ClearInterned(PyInterpreterState *interp); -extern void _PyLong_Fini(PyInterpreterState *interp); extern void _PyFaulthandler_Fini(void); extern void _PyHash_Fini(void); extern void _PyTraceMalloc_Fini(void); extern void _PyWarnings_Fini(PyInterpreterState *interp); extern void _PyAST_Fini(PyInterpreterState *interp); extern void _PyAtExit_Fini(PyInterpreterState *interp); +extern void _PyThread_FiniType(PyInterpreterState *interp); +extern void _Py_Deepfreeze_Fini(void); +extern void _PyArg_Fini(void); extern PyStatus _PyGILState_Init(_PyRuntimeState *runtime); extern PyStatus _PyGILState_SetTstate(PyThreadState *tstate); @@ -122,6 +83,7 @@ PyAPI_FUNC(PyStatus) _Py_PreInitializeFromConfig( const PyConfig *config, const struct _PyArgv *args); +PyAPI_FUNC(wchar_t *) _Py_GetStdlibDir(void); PyAPI_FUNC(int) _Py_HandleSystemExit(int *exitcode_p); diff --git a/src/external/windows/include/python/internal/pycore_pymath.h b/src/external/windows/include/python/internal/pycore_pymath.h new file mode 100644 index 00000000..0d2104f3 --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_pymath.h @@ -0,0 +1,224 @@ +#ifndef Py_INTERNAL_PYMATH_H +#define Py_INTERNAL_PYMATH_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + + +/* _Py_ADJUST_ERANGE1(x) + * _Py_ADJUST_ERANGE2(x, y) + * Set errno to 0 before calling a libm function, and invoke one of these + * macros after, passing the function result(s) (_Py_ADJUST_ERANGE2 is useful + * for functions returning complex results). This makes two kinds of + * adjustments to errno: (A) If it looks like the platform libm set + * errno=ERANGE due to underflow, clear errno. (B) If it looks like the + * platform libm overflowed but didn't set errno, force errno to ERANGE. In + * effect, we're trying to force a useful implementation of C89 errno + * behavior. + * Caution: + * This isn't reliable. C99 no longer requires libm to set errno under + * any exceptional condition, but does require +- HUGE_VAL return + * values on overflow. A 754 box *probably* maps HUGE_VAL to a + * double infinity, and we're cool if that's so, unless the input + * was an infinity and an infinity is the expected result. A C89 + * system sets errno to ERANGE, so we check for that too. We're + * out of luck if a C99 754 box doesn't map HUGE_VAL to +Inf, or + * if the returned result is a NaN, or if a C89 box returns HUGE_VAL + * in non-overflow cases. + */ +static inline void _Py_ADJUST_ERANGE1(double x) +{ + if (errno == 0) { + if (x == Py_HUGE_VAL || x == -Py_HUGE_VAL) { + errno = ERANGE; + } + } + else if (errno == ERANGE && x == 0.0) { + errno = 0; + } +} + +static inline void _Py_ADJUST_ERANGE2(double x, double y) +{ + if (x == Py_HUGE_VAL || x == -Py_HUGE_VAL || + y == Py_HUGE_VAL || y == -Py_HUGE_VAL) + { + if (errno == 0) { + errno = ERANGE; + } + } + else if (errno == ERANGE) { + errno = 0; + } +} + +// Return whether integral type *type* is signed or not. +#define _Py_IntegralTypeSigned(type) \ + ((type)(-1) < 0) + +// Return the maximum value of integral type *type*. +#define _Py_IntegralTypeMax(type) \ + ((_Py_IntegralTypeSigned(type)) ? (((((type)1 << (sizeof(type)*CHAR_BIT - 2)) - 1) << 1) + 1) : ~(type)0) + +// Return the minimum value of integral type *type*. +#define _Py_IntegralTypeMin(type) \ + ((_Py_IntegralTypeSigned(type)) ? -_Py_IntegralTypeMax(type) - 1 : 0) + +// Check whether *v* is in the range of integral type *type*. This is most +// useful if *v* is floating-point, since demoting a floating-point *v* to an +// integral type that cannot represent *v*'s integral part is undefined +// behavior. +#define _Py_InIntegralTypeRange(type, v) \ + (_Py_IntegralTypeMin(type) <= v && v <= _Py_IntegralTypeMax(type)) + + +//--- HAVE_PY_SET_53BIT_PRECISION macro ------------------------------------ +// +// The functions _Py_dg_strtod() and _Py_dg_dtoa() in Python/dtoa.c (which are +// required to support the short float repr introduced in Python 3.1) require +// that the floating-point unit that's being used for arithmetic operations on +// C doubles is set to use 53-bit precision. It also requires that the FPU +// rounding mode is round-half-to-even, but that's less often an issue. +// +// If your FPU isn't already set to 53-bit precision/round-half-to-even, and +// you want to make use of _Py_dg_strtod() and _Py_dg_dtoa(), then you should: +// +// #define HAVE_PY_SET_53BIT_PRECISION 1 +// +// and also give appropriate definitions for the following three macros: +// +// * _Py_SET_53BIT_PRECISION_HEADER: any variable declarations needed to +// use the two macros below. +// * _Py_SET_53BIT_PRECISION_START: store original FPU settings, and +// set FPU to 53-bit precision/round-half-to-even +// * _Py_SET_53BIT_PRECISION_END: restore original FPU settings +// +// The macros are designed to be used within a single C function: see +// Python/pystrtod.c for an example of their use. + + +// Get and set x87 control word for gcc/x86 +#ifdef HAVE_GCC_ASM_FOR_X87 +#define HAVE_PY_SET_53BIT_PRECISION 1 + +// Functions defined in Python/pymath.c +extern unsigned short _Py_get_387controlword(void); +extern void _Py_set_387controlword(unsigned short); + +#define _Py_SET_53BIT_PRECISION_HEADER \ + unsigned short old_387controlword, new_387controlword +#define _Py_SET_53BIT_PRECISION_START \ + do { \ + old_387controlword = _Py_get_387controlword(); \ + new_387controlword = (old_387controlword & ~0x0f00) | 0x0200; \ + if (new_387controlword != old_387controlword) { \ + _Py_set_387controlword(new_387controlword); \ + } \ + } while (0) +#define _Py_SET_53BIT_PRECISION_END \ + do { \ + if (new_387controlword != old_387controlword) { \ + _Py_set_387controlword(old_387controlword); \ + } \ + } while (0) +#endif + +// Get and set x87 control word for VisualStudio/x86. +// x87 is not supported in 64-bit or ARM. +#if defined(_MSC_VER) && !defined(_WIN64) && !defined(_M_ARM) +#define HAVE_PY_SET_53BIT_PRECISION 1 + +#include // __control87_2() + +#define _Py_SET_53BIT_PRECISION_HEADER \ + unsigned int old_387controlword, new_387controlword, out_387controlword + // We use the __control87_2 function to set only the x87 control word. + // The SSE control word is unaffected. +#define _Py_SET_53BIT_PRECISION_START \ + do { \ + __control87_2(0, 0, &old_387controlword, NULL); \ + new_387controlword = \ + (old_387controlword & ~(_MCW_PC | _MCW_RC)) | (_PC_53 | _RC_NEAR); \ + if (new_387controlword != old_387controlword) { \ + __control87_2(new_387controlword, _MCW_PC | _MCW_RC, \ + &out_387controlword, NULL); \ + } \ + } while (0) +#define _Py_SET_53BIT_PRECISION_END \ + do { \ + if (new_387controlword != old_387controlword) { \ + __control87_2(old_387controlword, _MCW_PC | _MCW_RC, \ + &out_387controlword, NULL); \ + } \ + } while (0) +#endif + + +// MC68881 +#ifdef HAVE_GCC_ASM_FOR_MC68881 +#define HAVE_PY_SET_53BIT_PRECISION 1 +#define _Py_SET_53BIT_PRECISION_HEADER \ + unsigned int old_fpcr, new_fpcr +#define _Py_SET_53BIT_PRECISION_START \ + do { \ + __asm__ ("fmove.l %%fpcr,%0" : "=g" (old_fpcr)); \ + /* Set double precision / round to nearest. */ \ + new_fpcr = (old_fpcr & ~0xf0) | 0x80; \ + if (new_fpcr != old_fpcr) { \ + __asm__ volatile ("fmove.l %0,%%fpcr" : : "g" (new_fpcr));\ + } \ + } while (0) +#define _Py_SET_53BIT_PRECISION_END \ + do { \ + if (new_fpcr != old_fpcr) { \ + __asm__ volatile ("fmove.l %0,%%fpcr" : : "g" (old_fpcr)); \ + } \ + } while (0) +#endif + +// Default definitions are empty +#ifndef _Py_SET_53BIT_PRECISION_HEADER +# define _Py_SET_53BIT_PRECISION_HEADER +# define _Py_SET_53BIT_PRECISION_START +# define _Py_SET_53BIT_PRECISION_END +#endif + + +//--- _PY_SHORT_FLOAT_REPR macro ------------------------------------------- + +// If we can't guarantee 53-bit precision, don't use the code +// in Python/dtoa.c, but fall back to standard code. This +// means that repr of a float will be long (17 significant digits). +// +// Realistically, there are two things that could go wrong: +// +// (1) doubles aren't IEEE 754 doubles, or +// (2) we're on x86 with the rounding precision set to 64-bits +// (extended precision), and we don't know how to change +// the rounding precision. +#if !defined(DOUBLE_IS_LITTLE_ENDIAN_IEEE754) && \ + !defined(DOUBLE_IS_BIG_ENDIAN_IEEE754) && \ + !defined(DOUBLE_IS_ARM_MIXED_ENDIAN_IEEE754) +# define _PY_SHORT_FLOAT_REPR 0 +#endif + +// Double rounding is symptomatic of use of extended precision on x86. +// If we're seeing double rounding, and we don't have any mechanism available +// for changing the FPU rounding precision, then don't use Python/dtoa.c. +#if defined(X87_DOUBLE_ROUNDING) && !defined(HAVE_PY_SET_53BIT_PRECISION) +# define _PY_SHORT_FLOAT_REPR 0 +#endif + +#ifndef _PY_SHORT_FLOAT_REPR +# define _PY_SHORT_FLOAT_REPR 1 +#endif + + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_PYMATH_H */ diff --git a/src/external/windows/include/python/internal/pycore_pymem.h b/src/external/windows/include/python/internal/pycore_pymem.h index fb83264d..73fbb4f7 100755 --- a/src/external/windows/include/python/internal/pycore_pymem.h +++ b/src/external/windows/include/python/internal/pycore_pymem.h @@ -94,8 +94,21 @@ struct _PyTraceMalloc_Config { PyAPI_DATA(struct _PyTraceMalloc_Config) _Py_tracemalloc_config; +/* Allocate memory directly from the O/S virtual memory system, + * where supported. Otherwise fallback on malloc */ +void *_PyObject_VirtualAlloc(size_t size); +void _PyObject_VirtualFree(void *, size_t size); + +/* This function returns the number of allocated memory blocks, regardless of size */ +PyAPI_FUNC(Py_ssize_t) _Py_GetAllocatedBlocks(void); + +/* Macros */ +#ifdef WITH_PYMALLOC +// Export the symbol for the 3rd party guppy3 project +PyAPI_FUNC(int) _PyObject_DebugMallocStats(FILE *out); +#endif #ifdef __cplusplus } #endif -#endif /* !Py_INTERNAL_PYMEM_H */ +#endif // !Py_INTERNAL_PYMEM_H diff --git a/src/external/windows/include/python/internal/pycore_pystate.h b/src/external/windows/include/python/internal/pycore_pystate.h index 5ef5e590..6f4361b5 100755 --- a/src/external/windows/include/python/internal/pycore_pystate.h +++ b/src/external/windows/include/python/internal/pycore_pystate.h @@ -21,12 +21,27 @@ _Py_IsMainThread(void) } +static inline PyInterpreterState * +_PyInterpreterState_Main(void) +{ + return _PyRuntime.interpreters.main; +} + static inline int _Py_IsMainInterpreter(PyInterpreterState *interp) { - /* Use directly _PyRuntime rather than tstate->interp->runtime, since - this function is used in performance critical code path (ceval) */ - return (interp == _PyRuntime.interpreters.main); + return (interp == _PyInterpreterState_Main()); +} + + +static inline const PyConfig * +_Py_GetMainConfig(void) +{ + PyInterpreterState *interp = _PyInterpreterState_Main(); + if (interp == NULL) { + return NULL; + } + return _PyInterpreterState_GetConfig(interp); } @@ -34,7 +49,7 @@ _Py_IsMainInterpreter(PyInterpreterState *interp) static inline int _Py_ThreadCanHandleSignals(PyInterpreterState *interp) { - return (_Py_IsMainThread() && interp == _PyRuntime.interpreters.main); + return (_Py_IsMainThread() && _Py_IsMainInterpreter(interp)); } @@ -49,18 +64,10 @@ _Py_ThreadCanHandlePendingCalls(void) /* Variable and macro for in-line access to current thread and interpreter state */ -#ifdef EXPERIMENTAL_ISOLATED_SUBINTERPRETERS -PyAPI_FUNC(PyThreadState*) _PyThreadState_GetTSS(void); -#endif - static inline PyThreadState* _PyRuntimeState_GetThreadState(_PyRuntimeState *runtime) { -#ifdef EXPERIMENTAL_ISOLATED_SUBINTERPRETERS - return _PyThreadState_GetTSS(); -#else return (PyThreadState*)_Py_atomic_load_relaxed(&runtime->gilstate.tstate_current); -#endif } /* Get the current Python thread state. @@ -71,21 +78,13 @@ _PyRuntimeState_GetThreadState(_PyRuntimeState *runtime) The caller must hold the GIL. - See also PyThreadState_Get() and PyThreadState_GET(). */ + See also PyThreadState_Get() and _PyThreadState_UncheckedGet(). */ static inline PyThreadState* _PyThreadState_GET(void) { -#ifdef EXPERIMENTAL_ISOLATED_SUBINTERPRETERS - return _PyThreadState_GetTSS(); -#else return _PyRuntimeState_GetThreadState(&_PyRuntime); -#endif } -/* Redefine PyThreadState_GET() as an alias to _PyThreadState_GET() */ -#undef PyThreadState_GET -#define PyThreadState_GET() _PyThreadState_GET() - PyAPI_FUNC(void) _Py_NO_RETURN _Py_FatalError_TstateNULL(const char *func); static inline void @@ -118,14 +117,29 @@ static inline PyInterpreterState* _PyInterpreterState_GET(void) { } -/* Other */ +// PyThreadState functions +PyAPI_FUNC(void) _PyThreadState_SetCurrent(PyThreadState *tstate); +// We keep this around exclusively for stable ABI compatibility. PyAPI_FUNC(void) _PyThreadState_Init( PyThreadState *tstate); PyAPI_FUNC(void) _PyThreadState_DeleteExcept( _PyRuntimeState *runtime, PyThreadState *tstate); + +static inline void +_PyThreadState_UpdateTracingState(PyThreadState *tstate) +{ + bool use_tracing = + (tstate->tracing == 0) && + (tstate->c_tracefunc != NULL || tstate->c_profilefunc != NULL); + tstate->cframe->use_tracing = (use_tracing ? 255 : 0); +} + + +/* Other */ + PyAPI_FUNC(PyThreadState *) _PyThreadState_Swap( struct _gilstate_runtime_state *gilstate, PyThreadState *newts); @@ -142,7 +156,7 @@ extern void _PySignal_AfterFork(void); PyAPI_FUNC(int) _PyState_AddModule( PyThreadState *tstate, PyObject* module, - struct PyModuleDef* def); + PyModuleDef* def); PyAPI_FUNC(int) _PyOS_InterruptOccurred(PyThreadState *tstate); diff --git a/src/external/windows/include/python/internal/pycore_runtime.h b/src/external/windows/include/python/internal/pycore_runtime.h index ad747219..6cab63be 100755 --- a/src/external/windows/include/python/internal/pycore_runtime.h +++ b/src/external/windows/include/python/internal/pycore_runtime.h @@ -8,8 +8,12 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif -#include "pycore_atomic.h" /* _Py_atomic_address */ -#include "pycore_gil.h" // struct _gil_runtime_state +#include "pycore_atomic.h" /* _Py_atomic_address */ +#include "pycore_gil.h" // struct _gil_runtime_state +#include "pycore_global_objects.h" // struct _Py_global_objects +#include "pycore_interp.h" // PyInterpreterState +#include "pycore_unicodeobject.h" // struct _Py_unicode_runtime_ids + /* ceval state */ @@ -19,9 +23,7 @@ struct _ceval_runtime_state { the main thread of the main interpreter can handle signals: see _Py_ThreadCanHandleSignals(). */ _Py_atomic_int signals_pending; -#ifndef EXPERIMENTAL_ISOLATED_SUBINTERPRETERS struct _gil_runtime_state gil; -#endif }; /* GIL state */ @@ -49,16 +51,18 @@ typedef struct _Py_AuditHookEntry { void *userData; } _Py_AuditHookEntry; -struct _Py_unicode_runtime_ids { - PyThread_type_lock lock; - // next_index value must be preserved when Py_Initialize()/Py_Finalize() - // is called multiple times: see _PyUnicode_FromId() implementation. - Py_ssize_t next_index; -}; - /* Full Python runtime state */ +/* _PyRuntimeState holds the global state for the CPython runtime. + That data is exposed in the internal API as a static variable (_PyRuntime). + */ typedef struct pyruntimestate { + /* Has been initialized to a safe state. + + In order to be effective, this must be set to 0 during or right + after allocation. */ + int _initialized; + /* Is running Py_PreInitialize()? */ int preinitializing; @@ -80,7 +84,11 @@ typedef struct pyruntimestate { struct pyinterpreters { PyThread_type_lock mutex; + /* The linked list of interpreters, newest first. */ PyInterpreterState *head; + /* The runtime's initial interpreter, which has a special role + in the operation of the runtime. It is also often the only + interpreter. */ PyInterpreterState *main; /* _next_interp_id is an auto-numbered sequence of small integers. It gets initialized in _PyInterpreterState_Init(), @@ -117,13 +125,29 @@ typedef struct pyruntimestate { struct _Py_unicode_runtime_ids unicode_ids; - // XXX Consolidate globals found via the check-c-globals script. + /* All the objects that are shared by the runtime's interpreters. */ + struct _Py_global_objects global_objects; + + /* The following fields are here to avoid allocation during init. + The data is exposed through _PyRuntimeState pointer fields. + These fields should not be accessed directly outside of init. + + All other _PyRuntimeState pointer fields are populated when + needed and default to NULL. + + For now there are some exceptions to that rule, which require + allocation during init. These will be addressed on a case-by-case + basis. Most notably, we don't pre-allocated the several mutex + (PyThread_type_lock) fields, because on Windows we only ever get + a pointer type. + */ + + /* PyInterpreterState.interpreters.main */ + PyInterpreterState _main_interpreter; } _PyRuntimeState; -#define _PyRuntimeState_INIT \ - {.preinitialized = 0, .core_initialized = 0, .initialized = 0} -/* Note: _PyRuntimeState_INIT sets other fields to 0/NULL */ +/* other API */ PyAPI_DATA(_PyRuntimeState) _PyRuntime; diff --git a/src/external/windows/include/python/internal/pycore_runtime_init.h b/src/external/windows/include/python/internal/pycore_runtime_init.h new file mode 100644 index 00000000..276ed934 --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_runtime_init.h @@ -0,0 +1,1256 @@ +#ifndef Py_INTERNAL_RUNTIME_INIT_H +#define Py_INTERNAL_RUNTIME_INIT_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +#include "pycore_object.h" + + +/* The static initializers defined here should only be used + in the runtime init code (in pystate.c and pylifecycle.c). */ + + +#define _PyRuntimeState_INIT \ + { \ + .gilstate = { \ + .check_enabled = 1, \ + /* A TSS key must be initialized with Py_tss_NEEDS_INIT \ + in accordance with the specification. */ \ + .autoTSSkey = Py_tss_NEEDS_INIT, \ + }, \ + .interpreters = { \ + /* This prevents interpreters from getting created \ + until _PyInterpreterState_Enable() is called. */ \ + .next_id = -1, \ + }, \ + .global_objects = _Py_global_objects_INIT, \ + ._main_interpreter = _PyInterpreterState_INIT, \ + } + +#ifdef HAVE_DLOPEN +# include +# if HAVE_DECL_RTLD_NOW +# define _Py_DLOPEN_FLAGS RTLD_NOW +# else +# define _Py_DLOPEN_FLAGS RTLD_LAZY +# endif +# define DLOPENFLAGS_INIT .dlopenflags = _Py_DLOPEN_FLAGS, +#else +# define _Py_DLOPEN_FLAGS 0 +# define DLOPENFLAGS_INIT +#endif + +#define _PyInterpreterState_INIT \ + { \ + ._static = 1, \ + .id_refcount = -1, \ + DLOPENFLAGS_INIT \ + .ceval = { \ + .recursion_limit = Py_DEFAULT_RECURSION_LIMIT, \ + }, \ + .gc = { \ + .enabled = 1, \ + .generations = { \ + /* .head is set in _PyGC_InitState(). */ \ + { .threshold = 700, }, \ + { .threshold = 10, }, \ + { .threshold = 10, }, \ + }, \ + }, \ + ._initial_thread = _PyThreadState_INIT, \ + } + +#define _PyThreadState_INIT \ + { \ + ._static = 1, \ + .recursion_limit = Py_DEFAULT_RECURSION_LIMIT, \ + .context_ver = 1, \ + } + + +// global objects + +#define _PyLong_DIGIT_INIT(val) \ + { \ + _PyVarObject_IMMORTAL_INIT(&PyLong_Type, \ + ((val) == 0 ? 0 : ((val) > 0 ? 1 : -1))), \ + .ob_digit = { ((val) >= 0 ? (val) : -(val)) }, \ + } + +#define _PyBytes_SIMPLE_INIT(CH, LEN) \ + { \ + _PyVarObject_IMMORTAL_INIT(&PyBytes_Type, LEN), \ + .ob_shash = -1, \ + .ob_sval = { CH }, \ + } +#define _PyBytes_CHAR_INIT(CH) \ + { \ + _PyBytes_SIMPLE_INIT(CH, 1) \ + } + +#define _PyUnicode_ASCII_BASE_INIT(LITERAL, ASCII) \ + { \ + .ob_base = _PyObject_IMMORTAL_INIT(&PyUnicode_Type), \ + .length = sizeof(LITERAL) - 1, \ + .hash = -1, \ + .state = { \ + .kind = 1, \ + .compact = 1, \ + .ascii = ASCII, \ + .ready = 1, \ + }, \ + } +#define _PyASCIIObject_INIT(LITERAL) \ + { \ + ._ascii = _PyUnicode_ASCII_BASE_INIT(LITERAL, 1), \ + ._data = LITERAL \ + } +#define INIT_STR(NAME, LITERAL) \ + ._ ## NAME = _PyASCIIObject_INIT(LITERAL) +#define INIT_ID(NAME) \ + ._ ## NAME = _PyASCIIObject_INIT(#NAME) +#define _PyUnicode_LATIN1_INIT(LITERAL) \ + { \ + ._latin1 = { \ + ._base = _PyUnicode_ASCII_BASE_INIT(LITERAL, 0), \ + }, \ + ._data = LITERAL, \ + } + +/* The following is auto-generated by Tools/scripts/generate_global_objects.py. */ +#define _Py_global_objects_INIT { \ + .singletons = { \ + .small_ints = { \ + _PyLong_DIGIT_INIT(-5), \ + _PyLong_DIGIT_INIT(-4), \ + _PyLong_DIGIT_INIT(-3), \ + _PyLong_DIGIT_INIT(-2), \ + _PyLong_DIGIT_INIT(-1), \ + _PyLong_DIGIT_INIT(0), \ + _PyLong_DIGIT_INIT(1), \ + _PyLong_DIGIT_INIT(2), \ + _PyLong_DIGIT_INIT(3), \ + _PyLong_DIGIT_INIT(4), \ + _PyLong_DIGIT_INIT(5), \ + _PyLong_DIGIT_INIT(6), \ + _PyLong_DIGIT_INIT(7), \ + _PyLong_DIGIT_INIT(8), \ + _PyLong_DIGIT_INIT(9), \ + _PyLong_DIGIT_INIT(10), \ + _PyLong_DIGIT_INIT(11), \ + _PyLong_DIGIT_INIT(12), \ + _PyLong_DIGIT_INIT(13), \ + _PyLong_DIGIT_INIT(14), \ + _PyLong_DIGIT_INIT(15), \ + _PyLong_DIGIT_INIT(16), \ + _PyLong_DIGIT_INIT(17), \ + _PyLong_DIGIT_INIT(18), \ + _PyLong_DIGIT_INIT(19), \ + _PyLong_DIGIT_INIT(20), \ + _PyLong_DIGIT_INIT(21), \ + _PyLong_DIGIT_INIT(22), \ + _PyLong_DIGIT_INIT(23), \ + _PyLong_DIGIT_INIT(24), \ + _PyLong_DIGIT_INIT(25), \ + _PyLong_DIGIT_INIT(26), \ + _PyLong_DIGIT_INIT(27), \ + _PyLong_DIGIT_INIT(28), \ + _PyLong_DIGIT_INIT(29), \ + _PyLong_DIGIT_INIT(30), \ + _PyLong_DIGIT_INIT(31), \ + _PyLong_DIGIT_INIT(32), \ + _PyLong_DIGIT_INIT(33), \ + _PyLong_DIGIT_INIT(34), \ + _PyLong_DIGIT_INIT(35), \ + _PyLong_DIGIT_INIT(36), \ + _PyLong_DIGIT_INIT(37), \ + _PyLong_DIGIT_INIT(38), \ + _PyLong_DIGIT_INIT(39), \ + _PyLong_DIGIT_INIT(40), \ + _PyLong_DIGIT_INIT(41), \ + _PyLong_DIGIT_INIT(42), \ + _PyLong_DIGIT_INIT(43), \ + _PyLong_DIGIT_INIT(44), \ + _PyLong_DIGIT_INIT(45), \ + _PyLong_DIGIT_INIT(46), \ + _PyLong_DIGIT_INIT(47), \ + _PyLong_DIGIT_INIT(48), \ + _PyLong_DIGIT_INIT(49), \ + _PyLong_DIGIT_INIT(50), \ + _PyLong_DIGIT_INIT(51), \ + _PyLong_DIGIT_INIT(52), \ + _PyLong_DIGIT_INIT(53), \ + _PyLong_DIGIT_INIT(54), \ + _PyLong_DIGIT_INIT(55), \ + _PyLong_DIGIT_INIT(56), \ + _PyLong_DIGIT_INIT(57), \ + _PyLong_DIGIT_INIT(58), \ + _PyLong_DIGIT_INIT(59), \ + _PyLong_DIGIT_INIT(60), \ + _PyLong_DIGIT_INIT(61), \ + _PyLong_DIGIT_INIT(62), \ + _PyLong_DIGIT_INIT(63), \ + _PyLong_DIGIT_INIT(64), \ + _PyLong_DIGIT_INIT(65), \ + _PyLong_DIGIT_INIT(66), \ + _PyLong_DIGIT_INIT(67), \ + _PyLong_DIGIT_INIT(68), \ + _PyLong_DIGIT_INIT(69), \ + _PyLong_DIGIT_INIT(70), \ + _PyLong_DIGIT_INIT(71), \ + _PyLong_DIGIT_INIT(72), \ + _PyLong_DIGIT_INIT(73), \ + _PyLong_DIGIT_INIT(74), \ + _PyLong_DIGIT_INIT(75), \ + _PyLong_DIGIT_INIT(76), \ + _PyLong_DIGIT_INIT(77), \ + _PyLong_DIGIT_INIT(78), \ + _PyLong_DIGIT_INIT(79), \ + _PyLong_DIGIT_INIT(80), \ + _PyLong_DIGIT_INIT(81), \ + _PyLong_DIGIT_INIT(82), \ + _PyLong_DIGIT_INIT(83), \ + _PyLong_DIGIT_INIT(84), \ + _PyLong_DIGIT_INIT(85), \ + _PyLong_DIGIT_INIT(86), \ + _PyLong_DIGIT_INIT(87), \ + _PyLong_DIGIT_INIT(88), \ + _PyLong_DIGIT_INIT(89), \ + _PyLong_DIGIT_INIT(90), \ + _PyLong_DIGIT_INIT(91), \ + _PyLong_DIGIT_INIT(92), \ + _PyLong_DIGIT_INIT(93), \ + _PyLong_DIGIT_INIT(94), \ + _PyLong_DIGIT_INIT(95), \ + _PyLong_DIGIT_INIT(96), \ + _PyLong_DIGIT_INIT(97), \ + _PyLong_DIGIT_INIT(98), \ + _PyLong_DIGIT_INIT(99), \ + _PyLong_DIGIT_INIT(100), \ + _PyLong_DIGIT_INIT(101), \ + _PyLong_DIGIT_INIT(102), \ + _PyLong_DIGIT_INIT(103), \ + _PyLong_DIGIT_INIT(104), \ + _PyLong_DIGIT_INIT(105), \ + _PyLong_DIGIT_INIT(106), \ + _PyLong_DIGIT_INIT(107), \ + _PyLong_DIGIT_INIT(108), \ + _PyLong_DIGIT_INIT(109), \ + _PyLong_DIGIT_INIT(110), \ + _PyLong_DIGIT_INIT(111), \ + _PyLong_DIGIT_INIT(112), \ + _PyLong_DIGIT_INIT(113), \ + _PyLong_DIGIT_INIT(114), \ + _PyLong_DIGIT_INIT(115), \ + _PyLong_DIGIT_INIT(116), \ + _PyLong_DIGIT_INIT(117), \ + _PyLong_DIGIT_INIT(118), \ + _PyLong_DIGIT_INIT(119), \ + _PyLong_DIGIT_INIT(120), \ + _PyLong_DIGIT_INIT(121), \ + _PyLong_DIGIT_INIT(122), \ + _PyLong_DIGIT_INIT(123), \ + _PyLong_DIGIT_INIT(124), \ + _PyLong_DIGIT_INIT(125), \ + _PyLong_DIGIT_INIT(126), \ + _PyLong_DIGIT_INIT(127), \ + _PyLong_DIGIT_INIT(128), \ + _PyLong_DIGIT_INIT(129), \ + _PyLong_DIGIT_INIT(130), \ + _PyLong_DIGIT_INIT(131), \ + _PyLong_DIGIT_INIT(132), \ + _PyLong_DIGIT_INIT(133), \ + _PyLong_DIGIT_INIT(134), \ + _PyLong_DIGIT_INIT(135), \ + _PyLong_DIGIT_INIT(136), \ + _PyLong_DIGIT_INIT(137), \ + _PyLong_DIGIT_INIT(138), \ + _PyLong_DIGIT_INIT(139), \ + _PyLong_DIGIT_INIT(140), \ + _PyLong_DIGIT_INIT(141), \ + _PyLong_DIGIT_INIT(142), \ + _PyLong_DIGIT_INIT(143), \ + _PyLong_DIGIT_INIT(144), \ + _PyLong_DIGIT_INIT(145), \ + _PyLong_DIGIT_INIT(146), \ + _PyLong_DIGIT_INIT(147), \ + _PyLong_DIGIT_INIT(148), \ + _PyLong_DIGIT_INIT(149), \ + _PyLong_DIGIT_INIT(150), \ + _PyLong_DIGIT_INIT(151), \ + _PyLong_DIGIT_INIT(152), \ + _PyLong_DIGIT_INIT(153), \ + _PyLong_DIGIT_INIT(154), \ + _PyLong_DIGIT_INIT(155), \ + _PyLong_DIGIT_INIT(156), \ + _PyLong_DIGIT_INIT(157), \ + _PyLong_DIGIT_INIT(158), \ + _PyLong_DIGIT_INIT(159), \ + _PyLong_DIGIT_INIT(160), \ + _PyLong_DIGIT_INIT(161), \ + _PyLong_DIGIT_INIT(162), \ + _PyLong_DIGIT_INIT(163), \ + _PyLong_DIGIT_INIT(164), \ + _PyLong_DIGIT_INIT(165), \ + _PyLong_DIGIT_INIT(166), \ + _PyLong_DIGIT_INIT(167), \ + _PyLong_DIGIT_INIT(168), \ + _PyLong_DIGIT_INIT(169), \ + _PyLong_DIGIT_INIT(170), \ + _PyLong_DIGIT_INIT(171), \ + _PyLong_DIGIT_INIT(172), \ + _PyLong_DIGIT_INIT(173), \ + _PyLong_DIGIT_INIT(174), \ + _PyLong_DIGIT_INIT(175), \ + _PyLong_DIGIT_INIT(176), \ + _PyLong_DIGIT_INIT(177), \ + _PyLong_DIGIT_INIT(178), \ + _PyLong_DIGIT_INIT(179), \ + _PyLong_DIGIT_INIT(180), \ + _PyLong_DIGIT_INIT(181), \ + _PyLong_DIGIT_INIT(182), \ + _PyLong_DIGIT_INIT(183), \ + _PyLong_DIGIT_INIT(184), \ + _PyLong_DIGIT_INIT(185), \ + _PyLong_DIGIT_INIT(186), \ + _PyLong_DIGIT_INIT(187), \ + _PyLong_DIGIT_INIT(188), \ + _PyLong_DIGIT_INIT(189), \ + _PyLong_DIGIT_INIT(190), \ + _PyLong_DIGIT_INIT(191), \ + _PyLong_DIGIT_INIT(192), \ + _PyLong_DIGIT_INIT(193), \ + _PyLong_DIGIT_INIT(194), \ + _PyLong_DIGIT_INIT(195), \ + _PyLong_DIGIT_INIT(196), \ + _PyLong_DIGIT_INIT(197), \ + _PyLong_DIGIT_INIT(198), \ + _PyLong_DIGIT_INIT(199), \ + _PyLong_DIGIT_INIT(200), \ + _PyLong_DIGIT_INIT(201), \ + _PyLong_DIGIT_INIT(202), \ + _PyLong_DIGIT_INIT(203), \ + _PyLong_DIGIT_INIT(204), \ + _PyLong_DIGIT_INIT(205), \ + _PyLong_DIGIT_INIT(206), \ + _PyLong_DIGIT_INIT(207), \ + _PyLong_DIGIT_INIT(208), \ + _PyLong_DIGIT_INIT(209), \ + _PyLong_DIGIT_INIT(210), \ + _PyLong_DIGIT_INIT(211), \ + _PyLong_DIGIT_INIT(212), \ + _PyLong_DIGIT_INIT(213), \ + _PyLong_DIGIT_INIT(214), \ + _PyLong_DIGIT_INIT(215), \ + _PyLong_DIGIT_INIT(216), \ + _PyLong_DIGIT_INIT(217), \ + _PyLong_DIGIT_INIT(218), \ + _PyLong_DIGIT_INIT(219), \ + _PyLong_DIGIT_INIT(220), \ + _PyLong_DIGIT_INIT(221), \ + _PyLong_DIGIT_INIT(222), \ + _PyLong_DIGIT_INIT(223), \ + _PyLong_DIGIT_INIT(224), \ + _PyLong_DIGIT_INIT(225), \ + _PyLong_DIGIT_INIT(226), \ + _PyLong_DIGIT_INIT(227), \ + _PyLong_DIGIT_INIT(228), \ + _PyLong_DIGIT_INIT(229), \ + _PyLong_DIGIT_INIT(230), \ + _PyLong_DIGIT_INIT(231), \ + _PyLong_DIGIT_INIT(232), \ + _PyLong_DIGIT_INIT(233), \ + _PyLong_DIGIT_INIT(234), \ + _PyLong_DIGIT_INIT(235), \ + _PyLong_DIGIT_INIT(236), \ + _PyLong_DIGIT_INIT(237), \ + _PyLong_DIGIT_INIT(238), \ + _PyLong_DIGIT_INIT(239), \ + _PyLong_DIGIT_INIT(240), \ + _PyLong_DIGIT_INIT(241), \ + _PyLong_DIGIT_INIT(242), \ + _PyLong_DIGIT_INIT(243), \ + _PyLong_DIGIT_INIT(244), \ + _PyLong_DIGIT_INIT(245), \ + _PyLong_DIGIT_INIT(246), \ + _PyLong_DIGIT_INIT(247), \ + _PyLong_DIGIT_INIT(248), \ + _PyLong_DIGIT_INIT(249), \ + _PyLong_DIGIT_INIT(250), \ + _PyLong_DIGIT_INIT(251), \ + _PyLong_DIGIT_INIT(252), \ + _PyLong_DIGIT_INIT(253), \ + _PyLong_DIGIT_INIT(254), \ + _PyLong_DIGIT_INIT(255), \ + _PyLong_DIGIT_INIT(256), \ + }, \ + \ + .bytes_empty = _PyBytes_SIMPLE_INIT(0, 0), \ + .bytes_characters = { \ + _PyBytes_CHAR_INIT(0), \ + _PyBytes_CHAR_INIT(1), \ + _PyBytes_CHAR_INIT(2), \ + _PyBytes_CHAR_INIT(3), \ + _PyBytes_CHAR_INIT(4), \ + _PyBytes_CHAR_INIT(5), \ + _PyBytes_CHAR_INIT(6), \ + _PyBytes_CHAR_INIT(7), \ + _PyBytes_CHAR_INIT(8), \ + _PyBytes_CHAR_INIT(9), \ + _PyBytes_CHAR_INIT(10), \ + _PyBytes_CHAR_INIT(11), \ + _PyBytes_CHAR_INIT(12), \ + _PyBytes_CHAR_INIT(13), \ + _PyBytes_CHAR_INIT(14), \ + _PyBytes_CHAR_INIT(15), \ + _PyBytes_CHAR_INIT(16), \ + _PyBytes_CHAR_INIT(17), \ + _PyBytes_CHAR_INIT(18), \ + _PyBytes_CHAR_INIT(19), \ + _PyBytes_CHAR_INIT(20), \ + _PyBytes_CHAR_INIT(21), \ + _PyBytes_CHAR_INIT(22), \ + _PyBytes_CHAR_INIT(23), \ + _PyBytes_CHAR_INIT(24), \ + _PyBytes_CHAR_INIT(25), \ + _PyBytes_CHAR_INIT(26), \ + _PyBytes_CHAR_INIT(27), \ + _PyBytes_CHAR_INIT(28), \ + _PyBytes_CHAR_INIT(29), \ + _PyBytes_CHAR_INIT(30), \ + _PyBytes_CHAR_INIT(31), \ + _PyBytes_CHAR_INIT(32), \ + _PyBytes_CHAR_INIT(33), \ + _PyBytes_CHAR_INIT(34), \ + _PyBytes_CHAR_INIT(35), \ + _PyBytes_CHAR_INIT(36), \ + _PyBytes_CHAR_INIT(37), \ + _PyBytes_CHAR_INIT(38), \ + _PyBytes_CHAR_INIT(39), \ + _PyBytes_CHAR_INIT(40), \ + _PyBytes_CHAR_INIT(41), \ + _PyBytes_CHAR_INIT(42), \ + _PyBytes_CHAR_INIT(43), \ + _PyBytes_CHAR_INIT(44), \ + _PyBytes_CHAR_INIT(45), \ + _PyBytes_CHAR_INIT(46), \ + _PyBytes_CHAR_INIT(47), \ + _PyBytes_CHAR_INIT(48), \ + _PyBytes_CHAR_INIT(49), \ + _PyBytes_CHAR_INIT(50), \ + _PyBytes_CHAR_INIT(51), \ + _PyBytes_CHAR_INIT(52), \ + _PyBytes_CHAR_INIT(53), \ + _PyBytes_CHAR_INIT(54), \ + _PyBytes_CHAR_INIT(55), \ + _PyBytes_CHAR_INIT(56), \ + _PyBytes_CHAR_INIT(57), \ + _PyBytes_CHAR_INIT(58), \ + _PyBytes_CHAR_INIT(59), \ + _PyBytes_CHAR_INIT(60), \ + _PyBytes_CHAR_INIT(61), \ + _PyBytes_CHAR_INIT(62), \ + _PyBytes_CHAR_INIT(63), \ + _PyBytes_CHAR_INIT(64), \ + _PyBytes_CHAR_INIT(65), \ + _PyBytes_CHAR_INIT(66), \ + _PyBytes_CHAR_INIT(67), \ + _PyBytes_CHAR_INIT(68), \ + _PyBytes_CHAR_INIT(69), \ + _PyBytes_CHAR_INIT(70), \ + _PyBytes_CHAR_INIT(71), \ + _PyBytes_CHAR_INIT(72), \ + _PyBytes_CHAR_INIT(73), \ + _PyBytes_CHAR_INIT(74), \ + _PyBytes_CHAR_INIT(75), \ + _PyBytes_CHAR_INIT(76), \ + _PyBytes_CHAR_INIT(77), \ + _PyBytes_CHAR_INIT(78), \ + _PyBytes_CHAR_INIT(79), \ + _PyBytes_CHAR_INIT(80), \ + _PyBytes_CHAR_INIT(81), \ + _PyBytes_CHAR_INIT(82), \ + _PyBytes_CHAR_INIT(83), \ + _PyBytes_CHAR_INIT(84), \ + _PyBytes_CHAR_INIT(85), \ + _PyBytes_CHAR_INIT(86), \ + _PyBytes_CHAR_INIT(87), \ + _PyBytes_CHAR_INIT(88), \ + _PyBytes_CHAR_INIT(89), \ + _PyBytes_CHAR_INIT(90), \ + _PyBytes_CHAR_INIT(91), \ + _PyBytes_CHAR_INIT(92), \ + _PyBytes_CHAR_INIT(93), \ + _PyBytes_CHAR_INIT(94), \ + _PyBytes_CHAR_INIT(95), \ + _PyBytes_CHAR_INIT(96), \ + _PyBytes_CHAR_INIT(97), \ + _PyBytes_CHAR_INIT(98), \ + _PyBytes_CHAR_INIT(99), \ + _PyBytes_CHAR_INIT(100), \ + _PyBytes_CHAR_INIT(101), \ + _PyBytes_CHAR_INIT(102), \ + _PyBytes_CHAR_INIT(103), \ + _PyBytes_CHAR_INIT(104), \ + _PyBytes_CHAR_INIT(105), \ + _PyBytes_CHAR_INIT(106), \ + _PyBytes_CHAR_INIT(107), \ + _PyBytes_CHAR_INIT(108), \ + _PyBytes_CHAR_INIT(109), \ + _PyBytes_CHAR_INIT(110), \ + _PyBytes_CHAR_INIT(111), \ + _PyBytes_CHAR_INIT(112), \ + _PyBytes_CHAR_INIT(113), \ + _PyBytes_CHAR_INIT(114), \ + _PyBytes_CHAR_INIT(115), \ + _PyBytes_CHAR_INIT(116), \ + _PyBytes_CHAR_INIT(117), \ + _PyBytes_CHAR_INIT(118), \ + _PyBytes_CHAR_INIT(119), \ + _PyBytes_CHAR_INIT(120), \ + _PyBytes_CHAR_INIT(121), \ + _PyBytes_CHAR_INIT(122), \ + _PyBytes_CHAR_INIT(123), \ + _PyBytes_CHAR_INIT(124), \ + _PyBytes_CHAR_INIT(125), \ + _PyBytes_CHAR_INIT(126), \ + _PyBytes_CHAR_INIT(127), \ + _PyBytes_CHAR_INIT(128), \ + _PyBytes_CHAR_INIT(129), \ + _PyBytes_CHAR_INIT(130), \ + _PyBytes_CHAR_INIT(131), \ + _PyBytes_CHAR_INIT(132), \ + _PyBytes_CHAR_INIT(133), \ + _PyBytes_CHAR_INIT(134), \ + _PyBytes_CHAR_INIT(135), \ + _PyBytes_CHAR_INIT(136), \ + _PyBytes_CHAR_INIT(137), \ + _PyBytes_CHAR_INIT(138), \ + _PyBytes_CHAR_INIT(139), \ + _PyBytes_CHAR_INIT(140), \ + _PyBytes_CHAR_INIT(141), \ + _PyBytes_CHAR_INIT(142), \ + _PyBytes_CHAR_INIT(143), \ + _PyBytes_CHAR_INIT(144), \ + _PyBytes_CHAR_INIT(145), \ + _PyBytes_CHAR_INIT(146), \ + _PyBytes_CHAR_INIT(147), \ + _PyBytes_CHAR_INIT(148), \ + _PyBytes_CHAR_INIT(149), \ + _PyBytes_CHAR_INIT(150), \ + _PyBytes_CHAR_INIT(151), \ + _PyBytes_CHAR_INIT(152), \ + _PyBytes_CHAR_INIT(153), \ + _PyBytes_CHAR_INIT(154), \ + _PyBytes_CHAR_INIT(155), \ + _PyBytes_CHAR_INIT(156), \ + _PyBytes_CHAR_INIT(157), \ + _PyBytes_CHAR_INIT(158), \ + _PyBytes_CHAR_INIT(159), \ + _PyBytes_CHAR_INIT(160), \ + _PyBytes_CHAR_INIT(161), \ + _PyBytes_CHAR_INIT(162), \ + _PyBytes_CHAR_INIT(163), \ + _PyBytes_CHAR_INIT(164), \ + _PyBytes_CHAR_INIT(165), \ + _PyBytes_CHAR_INIT(166), \ + _PyBytes_CHAR_INIT(167), \ + _PyBytes_CHAR_INIT(168), \ + _PyBytes_CHAR_INIT(169), \ + _PyBytes_CHAR_INIT(170), \ + _PyBytes_CHAR_INIT(171), \ + _PyBytes_CHAR_INIT(172), \ + _PyBytes_CHAR_INIT(173), \ + _PyBytes_CHAR_INIT(174), \ + _PyBytes_CHAR_INIT(175), \ + _PyBytes_CHAR_INIT(176), \ + _PyBytes_CHAR_INIT(177), \ + _PyBytes_CHAR_INIT(178), \ + _PyBytes_CHAR_INIT(179), \ + _PyBytes_CHAR_INIT(180), \ + _PyBytes_CHAR_INIT(181), \ + _PyBytes_CHAR_INIT(182), \ + _PyBytes_CHAR_INIT(183), \ + _PyBytes_CHAR_INIT(184), \ + _PyBytes_CHAR_INIT(185), \ + _PyBytes_CHAR_INIT(186), \ + _PyBytes_CHAR_INIT(187), \ + _PyBytes_CHAR_INIT(188), \ + _PyBytes_CHAR_INIT(189), \ + _PyBytes_CHAR_INIT(190), \ + _PyBytes_CHAR_INIT(191), \ + _PyBytes_CHAR_INIT(192), \ + _PyBytes_CHAR_INIT(193), \ + _PyBytes_CHAR_INIT(194), \ + _PyBytes_CHAR_INIT(195), \ + _PyBytes_CHAR_INIT(196), \ + _PyBytes_CHAR_INIT(197), \ + _PyBytes_CHAR_INIT(198), \ + _PyBytes_CHAR_INIT(199), \ + _PyBytes_CHAR_INIT(200), \ + _PyBytes_CHAR_INIT(201), \ + _PyBytes_CHAR_INIT(202), \ + _PyBytes_CHAR_INIT(203), \ + _PyBytes_CHAR_INIT(204), \ + _PyBytes_CHAR_INIT(205), \ + _PyBytes_CHAR_INIT(206), \ + _PyBytes_CHAR_INIT(207), \ + _PyBytes_CHAR_INIT(208), \ + _PyBytes_CHAR_INIT(209), \ + _PyBytes_CHAR_INIT(210), \ + _PyBytes_CHAR_INIT(211), \ + _PyBytes_CHAR_INIT(212), \ + _PyBytes_CHAR_INIT(213), \ + _PyBytes_CHAR_INIT(214), \ + _PyBytes_CHAR_INIT(215), \ + _PyBytes_CHAR_INIT(216), \ + _PyBytes_CHAR_INIT(217), \ + _PyBytes_CHAR_INIT(218), \ + _PyBytes_CHAR_INIT(219), \ + _PyBytes_CHAR_INIT(220), \ + _PyBytes_CHAR_INIT(221), \ + _PyBytes_CHAR_INIT(222), \ + _PyBytes_CHAR_INIT(223), \ + _PyBytes_CHAR_INIT(224), \ + _PyBytes_CHAR_INIT(225), \ + _PyBytes_CHAR_INIT(226), \ + _PyBytes_CHAR_INIT(227), \ + _PyBytes_CHAR_INIT(228), \ + _PyBytes_CHAR_INIT(229), \ + _PyBytes_CHAR_INIT(230), \ + _PyBytes_CHAR_INIT(231), \ + _PyBytes_CHAR_INIT(232), \ + _PyBytes_CHAR_INIT(233), \ + _PyBytes_CHAR_INIT(234), \ + _PyBytes_CHAR_INIT(235), \ + _PyBytes_CHAR_INIT(236), \ + _PyBytes_CHAR_INIT(237), \ + _PyBytes_CHAR_INIT(238), \ + _PyBytes_CHAR_INIT(239), \ + _PyBytes_CHAR_INIT(240), \ + _PyBytes_CHAR_INIT(241), \ + _PyBytes_CHAR_INIT(242), \ + _PyBytes_CHAR_INIT(243), \ + _PyBytes_CHAR_INIT(244), \ + _PyBytes_CHAR_INIT(245), \ + _PyBytes_CHAR_INIT(246), \ + _PyBytes_CHAR_INIT(247), \ + _PyBytes_CHAR_INIT(248), \ + _PyBytes_CHAR_INIT(249), \ + _PyBytes_CHAR_INIT(250), \ + _PyBytes_CHAR_INIT(251), \ + _PyBytes_CHAR_INIT(252), \ + _PyBytes_CHAR_INIT(253), \ + _PyBytes_CHAR_INIT(254), \ + _PyBytes_CHAR_INIT(255), \ + }, \ + \ + .strings = { \ + .literals = { \ + INIT_STR(anon_dictcomp, ""), \ + INIT_STR(anon_genexpr, ""), \ + INIT_STR(anon_lambda, ""), \ + INIT_STR(anon_listcomp, ""), \ + INIT_STR(anon_module, ""), \ + INIT_STR(anon_setcomp, ""), \ + INIT_STR(anon_string, ""), \ + INIT_STR(anon_unknown, ""), \ + INIT_STR(close_br, "}"), \ + INIT_STR(comma_sep, ", "), \ + INIT_STR(dbl_close_br, "}}"), \ + INIT_STR(dbl_open_br, "{{"), \ + INIT_STR(dbl_percent, "%%"), \ + INIT_STR(dot, "."), \ + INIT_STR(dot_locals, "."), \ + INIT_STR(empty, ""), \ + INIT_STR(list_err, "list index out of range"), \ + INIT_STR(newline, "\n"), \ + INIT_STR(open_br, "{"), \ + INIT_STR(percent, "%"), \ + INIT_STR(utf_8, "utf-8"), \ + }, \ + .identifiers = { \ + INIT_ID(False), \ + INIT_ID(Py_Repr), \ + INIT_ID(TextIOWrapper), \ + INIT_ID(True), \ + INIT_ID(WarningMessage), \ + INIT_ID(_), \ + INIT_ID(__IOBase_closed), \ + INIT_ID(__abc_tpflags__), \ + INIT_ID(__abs__), \ + INIT_ID(__abstractmethods__), \ + INIT_ID(__add__), \ + INIT_ID(__aenter__), \ + INIT_ID(__aexit__), \ + INIT_ID(__aiter__), \ + INIT_ID(__all__), \ + INIT_ID(__and__), \ + INIT_ID(__anext__), \ + INIT_ID(__annotations__), \ + INIT_ID(__args__), \ + INIT_ID(__await__), \ + INIT_ID(__bases__), \ + INIT_ID(__bool__), \ + INIT_ID(__build_class__), \ + INIT_ID(__builtins__), \ + INIT_ID(__bytes__), \ + INIT_ID(__call__), \ + INIT_ID(__cantrace__), \ + INIT_ID(__class__), \ + INIT_ID(__class_getitem__), \ + INIT_ID(__classcell__), \ + INIT_ID(__complex__), \ + INIT_ID(__contains__), \ + INIT_ID(__copy__), \ + INIT_ID(__del__), \ + INIT_ID(__delattr__), \ + INIT_ID(__delete__), \ + INIT_ID(__delitem__), \ + INIT_ID(__dict__), \ + INIT_ID(__dir__), \ + INIT_ID(__divmod__), \ + INIT_ID(__doc__), \ + INIT_ID(__enter__), \ + INIT_ID(__eq__), \ + INIT_ID(__exit__), \ + INIT_ID(__file__), \ + INIT_ID(__float__), \ + INIT_ID(__floordiv__), \ + INIT_ID(__format__), \ + INIT_ID(__fspath__), \ + INIT_ID(__ge__), \ + INIT_ID(__get__), \ + INIT_ID(__getattr__), \ + INIT_ID(__getattribute__), \ + INIT_ID(__getinitargs__), \ + INIT_ID(__getitem__), \ + INIT_ID(__getnewargs__), \ + INIT_ID(__getnewargs_ex__), \ + INIT_ID(__getstate__), \ + INIT_ID(__gt__), \ + INIT_ID(__hash__), \ + INIT_ID(__iadd__), \ + INIT_ID(__iand__), \ + INIT_ID(__ifloordiv__), \ + INIT_ID(__ilshift__), \ + INIT_ID(__imatmul__), \ + INIT_ID(__imod__), \ + INIT_ID(__import__), \ + INIT_ID(__imul__), \ + INIT_ID(__index__), \ + INIT_ID(__init__), \ + INIT_ID(__init_subclass__), \ + INIT_ID(__instancecheck__), \ + INIT_ID(__int__), \ + INIT_ID(__invert__), \ + INIT_ID(__ior__), \ + INIT_ID(__ipow__), \ + INIT_ID(__irshift__), \ + INIT_ID(__isabstractmethod__), \ + INIT_ID(__isub__), \ + INIT_ID(__iter__), \ + INIT_ID(__itruediv__), \ + INIT_ID(__ixor__), \ + INIT_ID(__le__), \ + INIT_ID(__len__), \ + INIT_ID(__length_hint__), \ + INIT_ID(__lltrace__), \ + INIT_ID(__loader__), \ + INIT_ID(__lshift__), \ + INIT_ID(__lt__), \ + INIT_ID(__main__), \ + INIT_ID(__matmul__), \ + INIT_ID(__missing__), \ + INIT_ID(__mod__), \ + INIT_ID(__module__), \ + INIT_ID(__mro_entries__), \ + INIT_ID(__mul__), \ + INIT_ID(__name__), \ + INIT_ID(__ne__), \ + INIT_ID(__neg__), \ + INIT_ID(__new__), \ + INIT_ID(__newobj__), \ + INIT_ID(__newobj_ex__), \ + INIT_ID(__next__), \ + INIT_ID(__notes__), \ + INIT_ID(__or__), \ + INIT_ID(__orig_class__), \ + INIT_ID(__origin__), \ + INIT_ID(__package__), \ + INIT_ID(__parameters__), \ + INIT_ID(__path__), \ + INIT_ID(__pos__), \ + INIT_ID(__pow__), \ + INIT_ID(__prepare__), \ + INIT_ID(__qualname__), \ + INIT_ID(__radd__), \ + INIT_ID(__rand__), \ + INIT_ID(__rdivmod__), \ + INIT_ID(__reduce__), \ + INIT_ID(__reduce_ex__), \ + INIT_ID(__repr__), \ + INIT_ID(__reversed__), \ + INIT_ID(__rfloordiv__), \ + INIT_ID(__rlshift__), \ + INIT_ID(__rmatmul__), \ + INIT_ID(__rmod__), \ + INIT_ID(__rmul__), \ + INIT_ID(__ror__), \ + INIT_ID(__round__), \ + INIT_ID(__rpow__), \ + INIT_ID(__rrshift__), \ + INIT_ID(__rshift__), \ + INIT_ID(__rsub__), \ + INIT_ID(__rtruediv__), \ + INIT_ID(__rxor__), \ + INIT_ID(__set__), \ + INIT_ID(__set_name__), \ + INIT_ID(__setattr__), \ + INIT_ID(__setitem__), \ + INIT_ID(__setstate__), \ + INIT_ID(__sizeof__), \ + INIT_ID(__slotnames__), \ + INIT_ID(__slots__), \ + INIT_ID(__spec__), \ + INIT_ID(__str__), \ + INIT_ID(__sub__), \ + INIT_ID(__subclasscheck__), \ + INIT_ID(__subclasshook__), \ + INIT_ID(__truediv__), \ + INIT_ID(__trunc__), \ + INIT_ID(__typing_is_unpacked_typevartuple__), \ + INIT_ID(__typing_prepare_subst__), \ + INIT_ID(__typing_subst__), \ + INIT_ID(__typing_unpacked_tuple_args__), \ + INIT_ID(__warningregistry__), \ + INIT_ID(__weakref__), \ + INIT_ID(__xor__), \ + INIT_ID(_abc_impl), \ + INIT_ID(_annotation), \ + INIT_ID(_blksize), \ + INIT_ID(_bootstrap), \ + INIT_ID(_dealloc_warn), \ + INIT_ID(_finalizing), \ + INIT_ID(_find_and_load), \ + INIT_ID(_fix_up_module), \ + INIT_ID(_get_sourcefile), \ + INIT_ID(_handle_fromlist), \ + INIT_ID(_initializing), \ + INIT_ID(_is_text_encoding), \ + INIT_ID(_lock_unlock_module), \ + INIT_ID(_showwarnmsg), \ + INIT_ID(_shutdown), \ + INIT_ID(_slotnames), \ + INIT_ID(_strptime_time), \ + INIT_ID(_uninitialized_submodules), \ + INIT_ID(_warn_unawaited_coroutine), \ + INIT_ID(_xoptions), \ + INIT_ID(add), \ + INIT_ID(append), \ + INIT_ID(big), \ + INIT_ID(buffer), \ + INIT_ID(builtins), \ + INIT_ID(c_call), \ + INIT_ID(c_exception), \ + INIT_ID(c_return), \ + INIT_ID(call), \ + INIT_ID(clear), \ + INIT_ID(close), \ + INIT_ID(closed), \ + INIT_ID(code), \ + INIT_ID(copy), \ + INIT_ID(copyreg), \ + INIT_ID(decode), \ + INIT_ID(default), \ + INIT_ID(defaultaction), \ + INIT_ID(dictcomp), \ + INIT_ID(difference_update), \ + INIT_ID(dispatch_table), \ + INIT_ID(displayhook), \ + INIT_ID(enable), \ + INIT_ID(encode), \ + INIT_ID(encoding), \ + INIT_ID(end_lineno), \ + INIT_ID(end_offset), \ + INIT_ID(errors), \ + INIT_ID(excepthook), \ + INIT_ID(exception), \ + INIT_ID(extend), \ + INIT_ID(filename), \ + INIT_ID(fileno), \ + INIT_ID(fillvalue), \ + INIT_ID(filters), \ + INIT_ID(find_class), \ + INIT_ID(flush), \ + INIT_ID(genexpr), \ + INIT_ID(get), \ + INIT_ID(get_source), \ + INIT_ID(getattr), \ + INIT_ID(getstate), \ + INIT_ID(ignore), \ + INIT_ID(importlib), \ + INIT_ID(inf), \ + INIT_ID(intersection), \ + INIT_ID(isatty), \ + INIT_ID(isinstance), \ + INIT_ID(items), \ + INIT_ID(iter), \ + INIT_ID(join), \ + INIT_ID(keys), \ + INIT_ID(lambda), \ + INIT_ID(last_traceback), \ + INIT_ID(last_type), \ + INIT_ID(last_value), \ + INIT_ID(latin1), \ + INIT_ID(len), \ + INIT_ID(line), \ + INIT_ID(lineno), \ + INIT_ID(listcomp), \ + INIT_ID(little), \ + INIT_ID(locale), \ + INIT_ID(match), \ + INIT_ID(metaclass), \ + INIT_ID(mode), \ + INIT_ID(modules), \ + INIT_ID(mro), \ + INIT_ID(msg), \ + INIT_ID(n_fields), \ + INIT_ID(n_sequence_fields), \ + INIT_ID(n_unnamed_fields), \ + INIT_ID(name), \ + INIT_ID(newlines), \ + INIT_ID(next), \ + INIT_ID(obj), \ + INIT_ID(offset), \ + INIT_ID(onceregistry), \ + INIT_ID(opcode), \ + INIT_ID(open), \ + INIT_ID(parent), \ + INIT_ID(partial), \ + INIT_ID(path), \ + INIT_ID(peek), \ + INIT_ID(persistent_id), \ + INIT_ID(persistent_load), \ + INIT_ID(print_file_and_line), \ + INIT_ID(ps1), \ + INIT_ID(ps2), \ + INIT_ID(raw), \ + INIT_ID(read), \ + INIT_ID(read1), \ + INIT_ID(readable), \ + INIT_ID(readall), \ + INIT_ID(readinto), \ + INIT_ID(readinto1), \ + INIT_ID(readline), \ + INIT_ID(reducer_override), \ + INIT_ID(reload), \ + INIT_ID(replace), \ + INIT_ID(reset), \ + INIT_ID(return), \ + INIT_ID(reversed), \ + INIT_ID(seek), \ + INIT_ID(seekable), \ + INIT_ID(send), \ + INIT_ID(setcomp), \ + INIT_ID(setstate), \ + INIT_ID(sort), \ + INIT_ID(stderr), \ + INIT_ID(stdin), \ + INIT_ID(stdout), \ + INIT_ID(strict), \ + INIT_ID(symmetric_difference_update), \ + INIT_ID(tell), \ + INIT_ID(text), \ + INIT_ID(threading), \ + INIT_ID(throw), \ + INIT_ID(top), \ + INIT_ID(truncate), \ + INIT_ID(unraisablehook), \ + INIT_ID(values), \ + INIT_ID(version), \ + INIT_ID(warnings), \ + INIT_ID(warnoptions), \ + INIT_ID(writable), \ + INIT_ID(write), \ + INIT_ID(zipimporter), \ + }, \ + .ascii = { \ + _PyASCIIObject_INIT("\x00"), \ + _PyASCIIObject_INIT("\x01"), \ + _PyASCIIObject_INIT("\x02"), \ + _PyASCIIObject_INIT("\x03"), \ + _PyASCIIObject_INIT("\x04"), \ + _PyASCIIObject_INIT("\x05"), \ + _PyASCIIObject_INIT("\x06"), \ + _PyASCIIObject_INIT("\x07"), \ + _PyASCIIObject_INIT("\x08"), \ + _PyASCIIObject_INIT("\x09"), \ + _PyASCIIObject_INIT("\x0a"), \ + _PyASCIIObject_INIT("\x0b"), \ + _PyASCIIObject_INIT("\x0c"), \ + _PyASCIIObject_INIT("\x0d"), \ + _PyASCIIObject_INIT("\x0e"), \ + _PyASCIIObject_INIT("\x0f"), \ + _PyASCIIObject_INIT("\x10"), \ + _PyASCIIObject_INIT("\x11"), \ + _PyASCIIObject_INIT("\x12"), \ + _PyASCIIObject_INIT("\x13"), \ + _PyASCIIObject_INIT("\x14"), \ + _PyASCIIObject_INIT("\x15"), \ + _PyASCIIObject_INIT("\x16"), \ + _PyASCIIObject_INIT("\x17"), \ + _PyASCIIObject_INIT("\x18"), \ + _PyASCIIObject_INIT("\x19"), \ + _PyASCIIObject_INIT("\x1a"), \ + _PyASCIIObject_INIT("\x1b"), \ + _PyASCIIObject_INIT("\x1c"), \ + _PyASCIIObject_INIT("\x1d"), \ + _PyASCIIObject_INIT("\x1e"), \ + _PyASCIIObject_INIT("\x1f"), \ + _PyASCIIObject_INIT("\x20"), \ + _PyASCIIObject_INIT("\x21"), \ + _PyASCIIObject_INIT("\x22"), \ + _PyASCIIObject_INIT("\x23"), \ + _PyASCIIObject_INIT("\x24"), \ + _PyASCIIObject_INIT("\x25"), \ + _PyASCIIObject_INIT("\x26"), \ + _PyASCIIObject_INIT("\x27"), \ + _PyASCIIObject_INIT("\x28"), \ + _PyASCIIObject_INIT("\x29"), \ + _PyASCIIObject_INIT("\x2a"), \ + _PyASCIIObject_INIT("\x2b"), \ + _PyASCIIObject_INIT("\x2c"), \ + _PyASCIIObject_INIT("\x2d"), \ + _PyASCIIObject_INIT("\x2e"), \ + _PyASCIIObject_INIT("\x2f"), \ + _PyASCIIObject_INIT("\x30"), \ + _PyASCIIObject_INIT("\x31"), \ + _PyASCIIObject_INIT("\x32"), \ + _PyASCIIObject_INIT("\x33"), \ + _PyASCIIObject_INIT("\x34"), \ + _PyASCIIObject_INIT("\x35"), \ + _PyASCIIObject_INIT("\x36"), \ + _PyASCIIObject_INIT("\x37"), \ + _PyASCIIObject_INIT("\x38"), \ + _PyASCIIObject_INIT("\x39"), \ + _PyASCIIObject_INIT("\x3a"), \ + _PyASCIIObject_INIT("\x3b"), \ + _PyASCIIObject_INIT("\x3c"), \ + _PyASCIIObject_INIT("\x3d"), \ + _PyASCIIObject_INIT("\x3e"), \ + _PyASCIIObject_INIT("\x3f"), \ + _PyASCIIObject_INIT("\x40"), \ + _PyASCIIObject_INIT("\x41"), \ + _PyASCIIObject_INIT("\x42"), \ + _PyASCIIObject_INIT("\x43"), \ + _PyASCIIObject_INIT("\x44"), \ + _PyASCIIObject_INIT("\x45"), \ + _PyASCIIObject_INIT("\x46"), \ + _PyASCIIObject_INIT("\x47"), \ + _PyASCIIObject_INIT("\x48"), \ + _PyASCIIObject_INIT("\x49"), \ + _PyASCIIObject_INIT("\x4a"), \ + _PyASCIIObject_INIT("\x4b"), \ + _PyASCIIObject_INIT("\x4c"), \ + _PyASCIIObject_INIT("\x4d"), \ + _PyASCIIObject_INIT("\x4e"), \ + _PyASCIIObject_INIT("\x4f"), \ + _PyASCIIObject_INIT("\x50"), \ + _PyASCIIObject_INIT("\x51"), \ + _PyASCIIObject_INIT("\x52"), \ + _PyASCIIObject_INIT("\x53"), \ + _PyASCIIObject_INIT("\x54"), \ + _PyASCIIObject_INIT("\x55"), \ + _PyASCIIObject_INIT("\x56"), \ + _PyASCIIObject_INIT("\x57"), \ + _PyASCIIObject_INIT("\x58"), \ + _PyASCIIObject_INIT("\x59"), \ + _PyASCIIObject_INIT("\x5a"), \ + _PyASCIIObject_INIT("\x5b"), \ + _PyASCIIObject_INIT("\x5c"), \ + _PyASCIIObject_INIT("\x5d"), \ + _PyASCIIObject_INIT("\x5e"), \ + _PyASCIIObject_INIT("\x5f"), \ + _PyASCIIObject_INIT("\x60"), \ + _PyASCIIObject_INIT("\x61"), \ + _PyASCIIObject_INIT("\x62"), \ + _PyASCIIObject_INIT("\x63"), \ + _PyASCIIObject_INIT("\x64"), \ + _PyASCIIObject_INIT("\x65"), \ + _PyASCIIObject_INIT("\x66"), \ + _PyASCIIObject_INIT("\x67"), \ + _PyASCIIObject_INIT("\x68"), \ + _PyASCIIObject_INIT("\x69"), \ + _PyASCIIObject_INIT("\x6a"), \ + _PyASCIIObject_INIT("\x6b"), \ + _PyASCIIObject_INIT("\x6c"), \ + _PyASCIIObject_INIT("\x6d"), \ + _PyASCIIObject_INIT("\x6e"), \ + _PyASCIIObject_INIT("\x6f"), \ + _PyASCIIObject_INIT("\x70"), \ + _PyASCIIObject_INIT("\x71"), \ + _PyASCIIObject_INIT("\x72"), \ + _PyASCIIObject_INIT("\x73"), \ + _PyASCIIObject_INIT("\x74"), \ + _PyASCIIObject_INIT("\x75"), \ + _PyASCIIObject_INIT("\x76"), \ + _PyASCIIObject_INIT("\x77"), \ + _PyASCIIObject_INIT("\x78"), \ + _PyASCIIObject_INIT("\x79"), \ + _PyASCIIObject_INIT("\x7a"), \ + _PyASCIIObject_INIT("\x7b"), \ + _PyASCIIObject_INIT("\x7c"), \ + _PyASCIIObject_INIT("\x7d"), \ + _PyASCIIObject_INIT("\x7e"), \ + _PyASCIIObject_INIT("\x7f"), \ + }, \ + .latin1 = { \ + _PyUnicode_LATIN1_INIT("\x80"), \ + _PyUnicode_LATIN1_INIT("\x81"), \ + _PyUnicode_LATIN1_INIT("\x82"), \ + _PyUnicode_LATIN1_INIT("\x83"), \ + _PyUnicode_LATIN1_INIT("\x84"), \ + _PyUnicode_LATIN1_INIT("\x85"), \ + _PyUnicode_LATIN1_INIT("\x86"), \ + _PyUnicode_LATIN1_INIT("\x87"), \ + _PyUnicode_LATIN1_INIT("\x88"), \ + _PyUnicode_LATIN1_INIT("\x89"), \ + _PyUnicode_LATIN1_INIT("\x8a"), \ + _PyUnicode_LATIN1_INIT("\x8b"), \ + _PyUnicode_LATIN1_INIT("\x8c"), \ + _PyUnicode_LATIN1_INIT("\x8d"), \ + _PyUnicode_LATIN1_INIT("\x8e"), \ + _PyUnicode_LATIN1_INIT("\x8f"), \ + _PyUnicode_LATIN1_INIT("\x90"), \ + _PyUnicode_LATIN1_INIT("\x91"), \ + _PyUnicode_LATIN1_INIT("\x92"), \ + _PyUnicode_LATIN1_INIT("\x93"), \ + _PyUnicode_LATIN1_INIT("\x94"), \ + _PyUnicode_LATIN1_INIT("\x95"), \ + _PyUnicode_LATIN1_INIT("\x96"), \ + _PyUnicode_LATIN1_INIT("\x97"), \ + _PyUnicode_LATIN1_INIT("\x98"), \ + _PyUnicode_LATIN1_INIT("\x99"), \ + _PyUnicode_LATIN1_INIT("\x9a"), \ + _PyUnicode_LATIN1_INIT("\x9b"), \ + _PyUnicode_LATIN1_INIT("\x9c"), \ + _PyUnicode_LATIN1_INIT("\x9d"), \ + _PyUnicode_LATIN1_INIT("\x9e"), \ + _PyUnicode_LATIN1_INIT("\x9f"), \ + _PyUnicode_LATIN1_INIT("\xa0"), \ + _PyUnicode_LATIN1_INIT("\xa1"), \ + _PyUnicode_LATIN1_INIT("\xa2"), \ + _PyUnicode_LATIN1_INIT("\xa3"), \ + _PyUnicode_LATIN1_INIT("\xa4"), \ + _PyUnicode_LATIN1_INIT("\xa5"), \ + _PyUnicode_LATIN1_INIT("\xa6"), \ + _PyUnicode_LATIN1_INIT("\xa7"), \ + _PyUnicode_LATIN1_INIT("\xa8"), \ + _PyUnicode_LATIN1_INIT("\xa9"), \ + _PyUnicode_LATIN1_INIT("\xaa"), \ + _PyUnicode_LATIN1_INIT("\xab"), \ + _PyUnicode_LATIN1_INIT("\xac"), \ + _PyUnicode_LATIN1_INIT("\xad"), \ + _PyUnicode_LATIN1_INIT("\xae"), \ + _PyUnicode_LATIN1_INIT("\xaf"), \ + _PyUnicode_LATIN1_INIT("\xb0"), \ + _PyUnicode_LATIN1_INIT("\xb1"), \ + _PyUnicode_LATIN1_INIT("\xb2"), \ + _PyUnicode_LATIN1_INIT("\xb3"), \ + _PyUnicode_LATIN1_INIT("\xb4"), \ + _PyUnicode_LATIN1_INIT("\xb5"), \ + _PyUnicode_LATIN1_INIT("\xb6"), \ + _PyUnicode_LATIN1_INIT("\xb7"), \ + _PyUnicode_LATIN1_INIT("\xb8"), \ + _PyUnicode_LATIN1_INIT("\xb9"), \ + _PyUnicode_LATIN1_INIT("\xba"), \ + _PyUnicode_LATIN1_INIT("\xbb"), \ + _PyUnicode_LATIN1_INIT("\xbc"), \ + _PyUnicode_LATIN1_INIT("\xbd"), \ + _PyUnicode_LATIN1_INIT("\xbe"), \ + _PyUnicode_LATIN1_INIT("\xbf"), \ + _PyUnicode_LATIN1_INIT("\xc0"), \ + _PyUnicode_LATIN1_INIT("\xc1"), \ + _PyUnicode_LATIN1_INIT("\xc2"), \ + _PyUnicode_LATIN1_INIT("\xc3"), \ + _PyUnicode_LATIN1_INIT("\xc4"), \ + _PyUnicode_LATIN1_INIT("\xc5"), \ + _PyUnicode_LATIN1_INIT("\xc6"), \ + _PyUnicode_LATIN1_INIT("\xc7"), \ + _PyUnicode_LATIN1_INIT("\xc8"), \ + _PyUnicode_LATIN1_INIT("\xc9"), \ + _PyUnicode_LATIN1_INIT("\xca"), \ + _PyUnicode_LATIN1_INIT("\xcb"), \ + _PyUnicode_LATIN1_INIT("\xcc"), \ + _PyUnicode_LATIN1_INIT("\xcd"), \ + _PyUnicode_LATIN1_INIT("\xce"), \ + _PyUnicode_LATIN1_INIT("\xcf"), \ + _PyUnicode_LATIN1_INIT("\xd0"), \ + _PyUnicode_LATIN1_INIT("\xd1"), \ + _PyUnicode_LATIN1_INIT("\xd2"), \ + _PyUnicode_LATIN1_INIT("\xd3"), \ + _PyUnicode_LATIN1_INIT("\xd4"), \ + _PyUnicode_LATIN1_INIT("\xd5"), \ + _PyUnicode_LATIN1_INIT("\xd6"), \ + _PyUnicode_LATIN1_INIT("\xd7"), \ + _PyUnicode_LATIN1_INIT("\xd8"), \ + _PyUnicode_LATIN1_INIT("\xd9"), \ + _PyUnicode_LATIN1_INIT("\xda"), \ + _PyUnicode_LATIN1_INIT("\xdb"), \ + _PyUnicode_LATIN1_INIT("\xdc"), \ + _PyUnicode_LATIN1_INIT("\xdd"), \ + _PyUnicode_LATIN1_INIT("\xde"), \ + _PyUnicode_LATIN1_INIT("\xdf"), \ + _PyUnicode_LATIN1_INIT("\xe0"), \ + _PyUnicode_LATIN1_INIT("\xe1"), \ + _PyUnicode_LATIN1_INIT("\xe2"), \ + _PyUnicode_LATIN1_INIT("\xe3"), \ + _PyUnicode_LATIN1_INIT("\xe4"), \ + _PyUnicode_LATIN1_INIT("\xe5"), \ + _PyUnicode_LATIN1_INIT("\xe6"), \ + _PyUnicode_LATIN1_INIT("\xe7"), \ + _PyUnicode_LATIN1_INIT("\xe8"), \ + _PyUnicode_LATIN1_INIT("\xe9"), \ + _PyUnicode_LATIN1_INIT("\xea"), \ + _PyUnicode_LATIN1_INIT("\xeb"), \ + _PyUnicode_LATIN1_INIT("\xec"), \ + _PyUnicode_LATIN1_INIT("\xed"), \ + _PyUnicode_LATIN1_INIT("\xee"), \ + _PyUnicode_LATIN1_INIT("\xef"), \ + _PyUnicode_LATIN1_INIT("\xf0"), \ + _PyUnicode_LATIN1_INIT("\xf1"), \ + _PyUnicode_LATIN1_INIT("\xf2"), \ + _PyUnicode_LATIN1_INIT("\xf3"), \ + _PyUnicode_LATIN1_INIT("\xf4"), \ + _PyUnicode_LATIN1_INIT("\xf5"), \ + _PyUnicode_LATIN1_INIT("\xf6"), \ + _PyUnicode_LATIN1_INIT("\xf7"), \ + _PyUnicode_LATIN1_INIT("\xf8"), \ + _PyUnicode_LATIN1_INIT("\xf9"), \ + _PyUnicode_LATIN1_INIT("\xfa"), \ + _PyUnicode_LATIN1_INIT("\xfb"), \ + _PyUnicode_LATIN1_INIT("\xfc"), \ + _PyUnicode_LATIN1_INIT("\xfd"), \ + _PyUnicode_LATIN1_INIT("\xfe"), \ + _PyUnicode_LATIN1_INIT("\xff"), \ + }, \ + }, \ + \ + .tuple_empty = { \ + .ob_base = _PyVarObject_IMMORTAL_INIT(&PyTuple_Type, 0) \ + }, \ + }, \ +} +/* End auto-generated code */ + + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_RUNTIME_INIT_H */ diff --git a/src/external/windows/include/python/internal/pycore_signal.h b/src/external/windows/include/python/internal/pycore_signal.h new file mode 100644 index 00000000..2f1f778f --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_signal.h @@ -0,0 +1,35 @@ +// Define Py_NSIG constant for signal handling. + +#ifndef Py_INTERNAL_SIGNAL_H +#define Py_INTERNAL_SIGNAL_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +#include // NSIG + +#ifdef _SIG_MAXSIG + // gh-91145: On FreeBSD, defines NSIG as 32: it doesn't include + // realtime signals: [SIGRTMIN,SIGRTMAX]. Use _SIG_MAXSIG instead. For + // example on x86-64 FreeBSD 13, SIGRTMAX is 126 and _SIG_MAXSIG is 128. +# define Py_NSIG _SIG_MAXSIG +#elif defined(NSIG) +# define Py_NSIG NSIG +#elif defined(_NSIG) +# define Py_NSIG _NSIG // BSD/SysV +#elif defined(_SIGMAX) +# define Py_NSIG (_SIGMAX + 1) // QNX +#elif defined(SIGMAX) +# define Py_NSIG (SIGMAX + 1) // djgpp +#else +# define Py_NSIG 64 // Use a reasonable default value +#endif + +#ifdef __cplusplus +} +#endif +#endif // !Py_INTERNAL_SIGNAL_H diff --git a/src/external/windows/include/python/internal/pycore_sliceobject.h b/src/external/windows/include/python/internal/pycore_sliceobject.h new file mode 100644 index 00000000..b943594f --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_sliceobject.h @@ -0,0 +1,20 @@ +#ifndef Py_INTERNAL_SLICEOBJECT_H +#define Py_INTERNAL_SLICEOBJECT_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + + +/* runtime lifecycle */ + +extern void _PySlice_Fini(PyInterpreterState *); + + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_SLICEOBJECT_H */ diff --git a/src/external/windows/include/python/internal/pycore_strhex.h b/src/external/windows/include/python/internal/pycore_strhex.h new file mode 100644 index 00000000..053e84d9 --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_strhex.h @@ -0,0 +1,36 @@ +#ifndef Py_INTERNAL_STRHEX_H +#define Py_INTERNAL_STRHEX_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +// Returns a str() containing the hex representation of argbuf. +PyAPI_FUNC(PyObject*) _Py_strhex(const + char* argbuf, + const Py_ssize_t arglen); + +// Returns a bytes() containing the ASCII hex representation of argbuf. +PyAPI_FUNC(PyObject*) _Py_strhex_bytes( + const char* argbuf, + const Py_ssize_t arglen); + +// These variants include support for a separator between every N bytes: +PyAPI_FUNC(PyObject*) _Py_strhex_with_sep( + const char* argbuf, + const Py_ssize_t arglen, + PyObject* sep, + const int bytes_per_group); +PyAPI_FUNC(PyObject*) _Py_strhex_bytes_with_sep( + const char* argbuf, + const Py_ssize_t arglen, + PyObject* sep, + const int bytes_per_group); + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_STRHEX_H */ diff --git a/src/external/windows/include/python/internal/pycore_structseq.h b/src/external/windows/include/python/internal/pycore_structseq.h index 2792ec17..9c1f38e4 100755 --- a/src/external/windows/include/python/internal/pycore_structseq.h +++ b/src/external/windows/include/python/internal/pycore_structseq.h @@ -9,11 +9,18 @@ extern "C" { #endif +/* other API */ + +PyAPI_FUNC(PyTypeObject *) _PyStructSequence_NewType( + PyStructSequence_Desc *desc, + unsigned long tp_flags); + PyAPI_FUNC(int) _PyStructSequence_InitType( PyTypeObject *type, PyStructSequence_Desc *desc, unsigned long tp_flags); +extern void _PyStructSequence_FiniType(PyTypeObject *type); #ifdef __cplusplus } diff --git a/src/external/windows/include/python/internal/pycore_symtable.h b/src/external/windows/include/python/internal/pycore_symtable.h index 4e73fe37..0fd6ce1e 100755 --- a/src/external/windows/include/python/internal/pycore_symtable.h +++ b/src/external/windows/include/python/internal/pycore_symtable.h @@ -79,6 +79,7 @@ extern PyTypeObject PySTEntry_Type; #define PySTEntry_Check(op) Py_IS_TYPE(op, &PySTEntry_Type) +extern long _PyST_GetSymbol(PySTEntryObject *, PyObject *); extern int _PyST_GetScope(PySTEntryObject *, PyObject *); extern struct symtable* _PySymtable_Build( diff --git a/src/external/windows/include/python/internal/pycore_sysmodule.h b/src/external/windows/include/python/internal/pycore_sysmodule.h index 363eb873..282e863a 100755 --- a/src/external/windows/include/python/internal/pycore_sysmodule.h +++ b/src/external/windows/include/python/internal/pycore_sysmodule.h @@ -18,6 +18,8 @@ PyAPI_FUNC(int) _PySys_Audit( PyAPI_FUNC() to not export the symbol. */ extern void _PySys_ClearAuditHooks(PyThreadState *tstate); +PyAPI_FUNC(int) _PySys_SetAttr(PyObject *, PyObject *); + #ifdef __cplusplus } #endif diff --git a/src/external/windows/include/python/internal/pycore_traceback.h b/src/external/windows/include/python/internal/pycore_traceback.h index 025ddb1d..2312642c 100755 --- a/src/external/windows/include/python/internal/pycore_traceback.h +++ b/src/external/windows/include/python/internal/pycore_traceback.h @@ -8,9 +8,6 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif -/* Forward declaration */ -struct _is; - /* Write the Python traceback into the file 'fd'. For example: Traceback (most recent call first): @@ -57,7 +54,7 @@ PyAPI_FUNC(void) _Py_DumpTraceback( PyAPI_FUNC(const char*) _Py_DumpTracebackThreads( int fd, - struct _is *interp, + PyInterpreterState *interp, PyThreadState *current_tstate); /* Write a Unicode object into the file descriptor fd. Encode the string to @@ -87,6 +84,17 @@ PyAPI_FUNC(PyObject*) _PyTraceBack_FromFrame( PyObject *tb_next, PyFrameObject *frame); +#define EXCEPTION_TB_HEADER "Traceback (most recent call last):\n" +#define EXCEPTION_GROUP_TB_HEADER "Exception Group Traceback (most recent call last):\n" + +/* Write the traceback tb to file f. Prefix each line with + indent spaces followed by the margin (if it is not NULL). */ +PyAPI_FUNC(int) _PyTraceBack_Print_Indented( + PyObject *tb, int indent, const char* margin, + const char *header_margin, const char *header, PyObject *f); +PyAPI_FUNC(int) _Py_WriteIndentedMargin(int, const char*, PyObject *); +PyAPI_FUNC(int) _Py_WriteIndent(int, PyObject *); + #ifdef __cplusplus } #endif diff --git a/src/external/windows/include/python/internal/pycore_tuple.h b/src/external/windows/include/python/internal/pycore_tuple.h index 28f3e0c6..3e4a7f13 100755 --- a/src/external/windows/include/python/internal/pycore_tuple.h +++ b/src/external/windows/include/python/internal/pycore_tuple.h @@ -10,9 +10,62 @@ extern "C" { #include "tupleobject.h" /* _PyTuple_CAST() */ + +/* runtime lifecycle */ + +extern PyStatus _PyTuple_InitGlobalObjects(PyInterpreterState *); +extern PyStatus _PyTuple_InitTypes(PyInterpreterState *); +extern void _PyTuple_Fini(PyInterpreterState *); + + +/* other API */ + +// PyTuple_MAXSAVESIZE - largest tuple to save on free list +// PyTuple_MAXFREELIST - maximum number of tuples of each size to save + +#if defined(PyTuple_MAXSAVESIZE) && PyTuple_MAXSAVESIZE <= 0 + // A build indicated that tuple freelists should not be used. +# define PyTuple_NFREELISTS 0 +# undef PyTuple_MAXSAVESIZE +# undef PyTuple_MAXFREELIST + +#elif !defined(WITH_FREELISTS) +# define PyTuple_NFREELISTS 0 +# undef PyTuple_MAXSAVESIZE +# undef PyTuple_MAXFREELIST + +#else + // We are using a freelist for tuples. +# ifndef PyTuple_MAXSAVESIZE +# define PyTuple_MAXSAVESIZE 20 +# endif +# define PyTuple_NFREELISTS PyTuple_MAXSAVESIZE +# ifndef PyTuple_MAXFREELIST +# define PyTuple_MAXFREELIST 2000 +# endif +#endif + +struct _Py_tuple_state { +#if PyTuple_NFREELISTS > 0 + /* There is one freelist for each size from 1 to PyTuple_MAXSAVESIZE. + The empty tuple is handled separately. + + Each tuple stored in the array is the head of the linked list + (and the next available tuple) for that size. The actual tuple + object is used as the linked list node, with its first item + (ob_item[0]) pointing to the next node (i.e. the previous head). + Each linked list is initially NULL. */ + PyTupleObject *free_list[PyTuple_NFREELISTS]; + int numfree[PyTuple_NFREELISTS]; +#else + char _unused; // Empty structs are not allowed. +#endif +}; + #define _PyTuple_ITEMS(op) (_PyTuple_CAST(op)->ob_item) -PyAPI_FUNC(PyObject *) _PyTuple_FromArray(PyObject *const *, Py_ssize_t); +extern PyObject *_PyTuple_FromArray(PyObject *const *, Py_ssize_t); +extern PyObject *_PyTuple_FromArraySteal(PyObject *const *, Py_ssize_t); #ifdef __cplusplus } diff --git a/src/external/windows/include/python/internal/pycore_typeobject.h b/src/external/windows/include/python/internal/pycore_typeobject.h new file mode 100644 index 00000000..6bfe176b --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_typeobject.h @@ -0,0 +1,50 @@ +#ifndef Py_INTERNAL_TYPEOBJECT_H +#define Py_INTERNAL_TYPEOBJECT_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + + +/* runtime lifecycle */ + +extern PyStatus _PyTypes_InitState(PyInterpreterState *); +extern PyStatus _PyTypes_InitTypes(PyInterpreterState *); +extern void _PyTypes_FiniTypes(PyInterpreterState *); +extern void _PyTypes_Fini(PyInterpreterState *); + + +/* other API */ + +// Type attribute lookup cache: speed up attribute and method lookups, +// see _PyType_Lookup(). +struct type_cache_entry { + unsigned int version; // initialized from type->tp_version_tag + PyObject *name; // reference to exactly a str or None + PyObject *value; // borrowed reference or NULL +}; + +#define MCACHE_SIZE_EXP 12 +#define MCACHE_STATS 0 + +struct type_cache { + struct type_cache_entry hashtable[1 << MCACHE_SIZE_EXP]; +#if MCACHE_STATS + size_t hits; + size_t misses; + size_t collisions; +#endif +}; + +extern PyStatus _PyTypes_InitSlotDefs(void); + +extern void _PyStaticType_Dealloc(PyTypeObject *type); + + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_TYPEOBJECT_H */ diff --git a/src/external/windows/include/python/internal/pycore_unicodeobject.h b/src/external/windows/include/python/internal/pycore_unicodeobject.h new file mode 100644 index 00000000..4a64c9d4 --- /dev/null +++ b/src/external/windows/include/python/internal/pycore_unicodeobject.h @@ -0,0 +1,62 @@ +#ifndef Py_INTERNAL_UNICODEOBJECT_H +#define Py_INTERNAL_UNICODEOBJECT_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +#include "pycore_fileutils.h" // _Py_error_handler + +void _PyUnicode_ExactDealloc(PyObject *op); + +/* runtime lifecycle */ + +extern void _PyUnicode_InitState(PyInterpreterState *); +extern PyStatus _PyUnicode_InitGlobalObjects(PyInterpreterState *); +extern PyStatus _PyUnicode_InitTypes(PyInterpreterState *); +extern void _PyUnicode_Fini(PyInterpreterState *); +extern void _PyUnicode_FiniTypes(PyInterpreterState *); +extern void _PyStaticUnicode_Dealloc(PyObject *); + +extern PyTypeObject _PyUnicodeASCIIIter_Type; + +/* other API */ + +struct _Py_unicode_runtime_ids { + PyThread_type_lock lock; + // next_index value must be preserved when Py_Initialize()/Py_Finalize() + // is called multiple times: see _PyUnicode_FromId() implementation. + Py_ssize_t next_index; +}; + +/* fs_codec.encoding is initialized to NULL. + Later, it is set to a non-NULL string by _PyUnicode_InitEncodings(). */ +struct _Py_unicode_fs_codec { + char *encoding; // Filesystem encoding (encoded to UTF-8) + int utf8; // encoding=="utf-8"? + char *errors; // Filesystem errors (encoded to UTF-8) + _Py_error_handler error_handler; +}; + +struct _Py_unicode_ids { + Py_ssize_t size; + PyObject **array; +}; + +struct _Py_unicode_state { + struct _Py_unicode_fs_codec fs_codec; + + // Unicode identifiers (_Py_Identifier): see _PyUnicode_FromId() + struct _Py_unicode_ids ids; +}; + +extern void _PyUnicode_ClearInterned(PyInterpreterState *interp); + + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_UNICODEOBJECT_H */ diff --git a/src/external/windows/include/python/internal/pycore_unionobject.h b/src/external/windows/include/python/internal/pycore_unionobject.h index 469b9c44..4908b0d8 100755 --- a/src/external/windows/include/python/internal/pycore_unionobject.h +++ b/src/external/windows/include/python/internal/pycore_unionobject.h @@ -15,6 +15,7 @@ extern PyObject *_Py_union_type_or(PyObject *, PyObject *); #define _PyGenericAlias_Check(op) PyObject_TypeCheck(op, &Py_GenericAliasType) extern PyObject *_Py_subs_parameters(PyObject *, PyObject *, PyObject *, PyObject *); extern PyObject *_Py_make_parameters(PyObject *); +extern PyObject *_Py_union_args(PyObject *self); #ifdef __cplusplus } diff --git a/src/external/windows/include/python/internal/pycore_warnings.h b/src/external/windows/include/python/internal/pycore_warnings.h index e7521c10..adaaf807 100755 --- a/src/external/windows/include/python/internal/pycore_warnings.h +++ b/src/external/windows/include/python/internal/pycore_warnings.h @@ -19,6 +19,10 @@ struct _warnings_runtime_state { extern int _PyWarnings_InitState(PyInterpreterState *interp); +PyAPI_FUNC(PyObject*) _PyWarnings_Init(void); + +extern void _PyErr_WarnUnawaitedCoroutine(PyObject *coro); + #ifdef __cplusplus } #endif diff --git a/src/external/windows/include/python/interpreteridobject.h b/src/external/windows/include/python/interpreteridobject.h deleted file mode 100755 index c8b5f953..00000000 --- a/src/external/windows/include/python/interpreteridobject.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef Py_INTERPRETERIDOBJECT_H -#define Py_INTERPRETERIDOBJECT_H - -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef Py_LIMITED_API -# define Py_CPYTHON_INTERPRETERIDOBJECT_H -# include "cpython/interpreteridobject.h" -# undef Py_CPYTHON_INTERPRETERIDOBJECT_H -#endif - -#ifdef __cplusplus -} -#endif -#endif /* !Py_INTERPRETERIDOBJECT_H */ diff --git a/src/external/windows/include/python/listobject.h b/src/external/windows/include/python/listobject.h index 28273493..6cfdb80d 100755 --- a/src/external/windows/include/python/listobject.h +++ b/src/external/windows/include/python/listobject.h @@ -42,7 +42,7 @@ PyAPI_FUNC(PyObject *) PyList_AsTuple(PyObject *); #ifndef Py_LIMITED_API # define Py_CPYTHON_LISTOBJECT_H -# include "cpython/listobject.h" +# include "cpython/listobject.h" # undef Py_CPYTHON_LISTOBJECT_H #endif diff --git a/src/external/windows/include/python/longobject.h b/src/external/windows/include/python/longobject.h index 8b85ae97..0748a955 100755 --- a/src/external/windows/include/python/longobject.h +++ b/src/external/windows/include/python/longobject.h @@ -7,8 +7,6 @@ extern "C" { /* Long (arbitrary precision) integer object interface */ -typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */ - PyAPI_DATA(PyTypeObject) PyLong_Type; #define PyLong_Check(op) \ @@ -26,9 +24,6 @@ PyAPI_FUNC(Py_ssize_t) PyLong_AsSsize_t(PyObject *); PyAPI_FUNC(size_t) PyLong_AsSize_t(PyObject *); PyAPI_FUNC(unsigned long) PyLong_AsUnsignedLong(PyObject *); PyAPI_FUNC(unsigned long) PyLong_AsUnsignedLongMask(PyObject *); -#ifndef Py_LIMITED_API -PyAPI_FUNC(int) _PyLong_AsInt(PyObject *); -#endif PyAPI_FUNC(PyObject *) PyLong_GetInfo(void); /* It may be useful in the future. I've added it in the PyInt -> PyLong @@ -65,30 +60,6 @@ PyAPI_FUNC(PyObject *) PyLong_GetInfo(void); # error "void* different in size from int, long and long long" #endif /* SIZEOF_VOID_P */ -#ifndef Py_LIMITED_API -PyAPI_FUNC(int) _PyLong_UnsignedShort_Converter(PyObject *, void *); -PyAPI_FUNC(int) _PyLong_UnsignedInt_Converter(PyObject *, void *); -PyAPI_FUNC(int) _PyLong_UnsignedLong_Converter(PyObject *, void *); -PyAPI_FUNC(int) _PyLong_UnsignedLongLong_Converter(PyObject *, void *); -PyAPI_FUNC(int) _PyLong_Size_t_Converter(PyObject *, void *); -#endif - -/* Used by Python/mystrtoul.c, _PyBytes_FromHex(), - _PyBytes_DecodeEscape(), etc. */ -#ifndef Py_LIMITED_API -PyAPI_DATA(unsigned char) _PyLong_DigitValue[256]; -#endif - -/* _PyLong_Frexp returns a double x and an exponent e such that the - true value is approximately equal to x * 2**e. e is >= 0. x is - 0.0 if and only if the input is 0 (in which case, e and x are both - zeroes); otherwise, 0.5 <= abs(x) < 1.0. On overflow, which is - possible if the number of bits doesn't fit into a Py_ssize_t, sets - OverflowError and returns -1.0 for x, 0 for e. */ -#ifndef Py_LIMITED_API -PyAPI_FUNC(double) _PyLong_Frexp(PyLongObject *a, Py_ssize_t *e); -#endif - PyAPI_FUNC(double) PyLong_AsDouble(PyObject *); PyAPI_FUNC(PyObject *) PyLong_FromVoidPtr(void *); PyAPI_FUNC(void *) PyLong_AsVoidPtr(PyObject *); @@ -101,102 +72,6 @@ PyAPI_FUNC(unsigned long long) PyLong_AsUnsignedLongLongMask(PyObject *); PyAPI_FUNC(long long) PyLong_AsLongLongAndOverflow(PyObject *, int *); PyAPI_FUNC(PyObject *) PyLong_FromString(const char *, char **, int); -#ifndef Py_LIMITED_API -PyAPI_FUNC(PyObject *) PyLong_FromUnicodeObject(PyObject *u, int base); -PyAPI_FUNC(PyObject *) _PyLong_FromBytes(const char *, Py_ssize_t, int); -#endif - -#ifndef Py_LIMITED_API -/* _PyLong_Sign. Return 0 if v is 0, -1 if v < 0, +1 if v > 0. - v must not be NULL, and must be a normalized long. - There are no error cases. -*/ -PyAPI_FUNC(int) _PyLong_Sign(PyObject *v); - - -/* _PyLong_NumBits. Return the number of bits needed to represent the - absolute value of a long. For example, this returns 1 for 1 and -1, 2 - for 2 and -2, and 2 for 3 and -3. It returns 0 for 0. - v must not be NULL, and must be a normalized long. - (size_t)-1 is returned and OverflowError set if the true result doesn't - fit in a size_t. -*/ -PyAPI_FUNC(size_t) _PyLong_NumBits(PyObject *v); - -/* _PyLong_DivmodNear. Given integers a and b, compute the nearest - integer q to the exact quotient a / b, rounding to the nearest even integer - in the case of a tie. Return (q, r), where r = a - q*b. The remainder r - will satisfy abs(r) <= abs(b)/2, with equality possible only if q is - even. -*/ -PyAPI_FUNC(PyObject *) _PyLong_DivmodNear(PyObject *, PyObject *); - -/* _PyLong_FromByteArray: View the n unsigned bytes as a binary integer in - base 256, and return a Python int with the same numeric value. - If n is 0, the integer is 0. Else: - If little_endian is 1/true, bytes[n-1] is the MSB and bytes[0] the LSB; - else (little_endian is 0/false) bytes[0] is the MSB and bytes[n-1] the - LSB. - If is_signed is 0/false, view the bytes as a non-negative integer. - If is_signed is 1/true, view the bytes as a 2's-complement integer, - non-negative if bit 0x80 of the MSB is clear, negative if set. - Error returns: - + Return NULL with the appropriate exception set if there's not - enough memory to create the Python int. -*/ -PyAPI_FUNC(PyObject *) _PyLong_FromByteArray( - const unsigned char* bytes, size_t n, - int little_endian, int is_signed); - -/* _PyLong_AsByteArray: Convert the least-significant 8*n bits of long - v to a base-256 integer, stored in array bytes. Normally return 0, - return -1 on error. - If little_endian is 1/true, store the MSB at bytes[n-1] and the LSB at - bytes[0]; else (little_endian is 0/false) store the MSB at bytes[0] and - the LSB at bytes[n-1]. - If is_signed is 0/false, it's an error if v < 0; else (v >= 0) n bytes - are filled and there's nothing special about bit 0x80 of the MSB. - If is_signed is 1/true, bytes is filled with the 2's-complement - representation of v's value. Bit 0x80 of the MSB is the sign bit. - Error returns (-1): - + is_signed is 0 and v < 0. TypeError is set in this case, and bytes - isn't altered. - + n isn't big enough to hold the full mathematical value of v. For - example, if is_signed is 0 and there are more digits in the v than - fit in n; or if is_signed is 1, v < 0, and n is just 1 bit shy of - being large enough to hold a sign bit. OverflowError is set in this - case, but bytes holds the least-significant n bytes of the true value. -*/ -PyAPI_FUNC(int) _PyLong_AsByteArray(PyLongObject* v, - unsigned char* bytes, size_t n, - int little_endian, int is_signed); - -/* _PyLong_Format: Convert the long to a string object with given base, - appending a base prefix of 0[box] if base is 2, 8 or 16. */ -PyAPI_FUNC(PyObject *) _PyLong_Format(PyObject *obj, int base); - -PyAPI_FUNC(int) _PyLong_FormatWriter( - _PyUnicodeWriter *writer, - PyObject *obj, - int base, - int alternate); - -PyAPI_FUNC(char*) _PyLong_FormatBytesWriter( - _PyBytesWriter *writer, - char *str, - PyObject *obj, - int base, - int alternate); - -/* Format the object based on the format_spec, as defined in PEP 3101 - (Advanced String Formatting). */ -PyAPI_FUNC(int) _PyLong_FormatAdvancedWriter( - _PyUnicodeWriter *writer, - PyObject *obj, - PyObject *format_spec, - Py_ssize_t start, - Py_ssize_t end); -#endif /* Py_LIMITED_API */ /* These aren't really part of the int object, but they're handy. The functions are in Python/mystrtoul.c. @@ -205,13 +80,9 @@ PyAPI_FUNC(unsigned long) PyOS_strtoul(const char *, char **, int); PyAPI_FUNC(long) PyOS_strtol(const char *, char **, int); #ifndef Py_LIMITED_API -/* For use by the gcd function in mathmodule.c */ -PyAPI_FUNC(PyObject *) _PyLong_GCD(PyObject *, PyObject *); -#endif /* !Py_LIMITED_API */ - -#ifndef Py_LIMITED_API -PyAPI_FUNC(PyObject *) _PyLong_Rshift(PyObject *, size_t); -PyAPI_FUNC(PyObject *) _PyLong_Lshift(PyObject *, size_t); +# define Py_CPYTHON_LONGOBJECT_H +# include "cpython/longobject.h" +# undef Py_CPYTHON_LONGOBJECT_H #endif #ifdef __cplusplus diff --git a/src/external/windows/include/python/marshal.h b/src/external/windows/include/python/marshal.h index de9dfbbf..9f4961af 100755 --- a/src/external/windows/include/python/marshal.h +++ b/src/external/windows/include/python/marshal.h @@ -3,26 +3,29 @@ #ifndef Py_MARSHAL_H #define Py_MARSHAL_H +#ifndef Py_LIMITED_API + #ifdef __cplusplus extern "C" { #endif -#define Py_MARSHAL_VERSION 4 - -PyAPI_FUNC(void) PyMarshal_WriteLongToFile(long, FILE *, int); -PyAPI_FUNC(void) PyMarshal_WriteObjectToFile(PyObject *, FILE *, int); +PyAPI_FUNC(PyObject *) PyMarshal_ReadObjectFromString(const char *, + Py_ssize_t); PyAPI_FUNC(PyObject *) PyMarshal_WriteObjectToString(PyObject *, int); -#ifndef Py_LIMITED_API +#define Py_MARSHAL_VERSION 4 + PyAPI_FUNC(long) PyMarshal_ReadLongFromFile(FILE *); PyAPI_FUNC(int) PyMarshal_ReadShortFromFile(FILE *); PyAPI_FUNC(PyObject *) PyMarshal_ReadObjectFromFile(FILE *); PyAPI_FUNC(PyObject *) PyMarshal_ReadLastObjectFromFile(FILE *); -#endif -PyAPI_FUNC(PyObject *) PyMarshal_ReadObjectFromString(const char *, - Py_ssize_t); + +PyAPI_FUNC(void) PyMarshal_WriteLongToFile(long, FILE *, int); +PyAPI_FUNC(void) PyMarshal_WriteObjectToFile(PyObject *, FILE *, int); #ifdef __cplusplus } #endif + +#endif /* Py_LIMITED_API */ #endif /* !Py_MARSHAL_H */ diff --git a/src/external/windows/include/python/memoryobject.h b/src/external/windows/include/python/memoryobject.h index 3a7f2c20..16129f46 100755 --- a/src/external/windows/include/python/memoryobject.h +++ b/src/external/windows/include/python/memoryobject.h @@ -25,8 +25,8 @@ PyAPI_FUNC(PyObject *) PyMemoryView_FromObject(PyObject *base); PyAPI_FUNC(PyObject *) PyMemoryView_FromMemory(char *mem, Py_ssize_t size, int flags); #endif -#ifndef Py_LIMITED_API -PyAPI_FUNC(PyObject *) PyMemoryView_FromBuffer(Py_buffer *info); +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030b0000 +PyAPI_FUNC(PyObject *) PyMemoryView_FromBuffer(const Py_buffer *info); #endif PyAPI_FUNC(PyObject *) PyMemoryView_GetContiguous(PyObject *base, int buffertype, diff --git a/src/external/windows/include/python/methodobject.h b/src/external/windows/include/python/methodobject.h index 2456329c..56816e4f 100755 --- a/src/external/windows/include/python/methodobject.h +++ b/src/external/windows/include/python/methodobject.h @@ -26,6 +26,25 @@ typedef PyObject *(*_PyCFunctionFastWithKeywords) (PyObject *, typedef PyObject *(*PyCMethod)(PyObject *, PyTypeObject *, PyObject *const *, size_t, PyObject *); +// Cast an function to the PyCFunction type to use it with PyMethodDef. +// +// This macro can be used to prevent compiler warnings if the first parameter +// uses a different pointer type than PyObject* (ex: METH_VARARGS and METH_O +// calling conventions). +// +// The macro can also be used for METH_FASTCALL and METH_VARARGS|METH_KEYWORDS +// calling conventions to avoid compiler warnings because the function has more +// than 2 parameters. The macro first casts the function to the +// "void func(void)" type to prevent compiler warnings. +// +// If a function is declared with the METH_NOARGS calling convention, it must +// have 2 parameters. Since the second parameter is unused, Py_UNUSED() can be +// used to prevent a compiler warning. If the function has a single parameter, +// it triggers an undefined behavior when Python calls it with 2 parameters +// (bpo-33012). +#define _PyCFunction_CAST(func) \ + _Py_CAST(PyCFunction, _Py_CAST(void(*)(void), (func))) + PyAPI_FUNC(PyCFunction) PyCFunction_GetFunction(PyObject *); PyAPI_FUNC(PyObject *) PyCFunction_GetSelf(PyObject *); PyAPI_FUNC(int) PyCFunction_GetFlags(PyObject *); @@ -39,7 +58,6 @@ struct PyMethodDef { describe the args expected by the C func */ const char *ml_doc; /* The __doc__ attribute, or NULL */ }; -typedef struct PyMethodDef PyMethodDef; /* PyCFunction_New is declared as a function for stable ABI (declaration is * needed for e.g. GCC with -fvisibility=hidden), but redefined as a macro @@ -103,11 +121,9 @@ PyAPI_FUNC(PyObject *) PyCMethod_New(PyMethodDef *, PyObject *, #ifndef Py_LIMITED_API - -#define Py_CPYTHON_METHODOBJECT_H -#include "cpython/methodobject.h" -#undef Py_CPYTHON_METHODOBJECT_H - +# define Py_CPYTHON_METHODOBJECT_H +# include "cpython/methodobject.h" +# undef Py_CPYTHON_METHODOBJECT_H #endif #ifdef __cplusplus diff --git a/src/external/windows/include/python/modsupport.h b/src/external/windows/include/python/modsupport.h index 2c18c207..174503b8 100755 --- a/src/external/windows/include/python/modsupport.h +++ b/src/external/windows/include/python/modsupport.h @@ -7,7 +7,7 @@ extern "C" { /* Module support interface */ -#include +#include // va_list /* If PY_SSIZE_T_CLEAN is defined, each functions treats #-specifier to mean Py_ssize_t */ @@ -19,19 +19,6 @@ extern "C" { #define PyArg_VaParseTupleAndKeywords _PyArg_VaParseTupleAndKeywords_SizeT #define Py_BuildValue _Py_BuildValue_SizeT #define Py_VaBuildValue _Py_VaBuildValue_SizeT -#ifndef Py_LIMITED_API -#define _Py_VaBuildStack _Py_VaBuildStack_SizeT -#endif -#else -#ifndef Py_LIMITED_API -PyAPI_FUNC(PyObject *) _Py_VaBuildValue_SizeT(const char *, va_list); -PyAPI_FUNC(PyObject **) _Py_VaBuildStack_SizeT( - PyObject **small_stack, - Py_ssize_t small_stack_len, - const char *format, - va_list va, - Py_ssize_t *p_nargs); -#endif /* !Py_LIMITED_API */ #endif /* Due to a glitch in 3.2, the _SizeT versions weren't exported from the DLL. */ @@ -50,91 +37,9 @@ PyAPI_FUNC(PyObject *) Py_BuildValue(const char *, ...); PyAPI_FUNC(PyObject *) _Py_BuildValue_SizeT(const char *, ...); -#ifndef Py_LIMITED_API -PyAPI_FUNC(int) _PyArg_UnpackStack( - PyObject *const *args, - Py_ssize_t nargs, - const char *name, - Py_ssize_t min, - Py_ssize_t max, - ...); - -PyAPI_FUNC(int) _PyArg_NoKeywords(const char *funcname, PyObject *kwargs); -PyAPI_FUNC(int) _PyArg_NoKwnames(const char *funcname, PyObject *kwnames); -PyAPI_FUNC(int) _PyArg_NoPositional(const char *funcname, PyObject *args); -#define _PyArg_NoKeywords(funcname, kwargs) \ - ((kwargs) == NULL || _PyArg_NoKeywords((funcname), (kwargs))) -#define _PyArg_NoKwnames(funcname, kwnames) \ - ((kwnames) == NULL || _PyArg_NoKwnames((funcname), (kwnames))) -#define _PyArg_NoPositional(funcname, args) \ - ((args) == NULL || _PyArg_NoPositional((funcname), (args))) - -PyAPI_FUNC(void) _PyArg_BadArgument(const char *, const char *, const char *, PyObject *); -PyAPI_FUNC(int) _PyArg_CheckPositional(const char *, Py_ssize_t, - Py_ssize_t, Py_ssize_t); -#define _PyArg_CheckPositional(funcname, nargs, min, max) \ - (((min) <= (nargs) && (nargs) <= (max)) \ - || _PyArg_CheckPositional((funcname), (nargs), (min), (max))) - -#endif +#define ANY_VARARGS(n) (n == PY_SSIZE_T_MAX) PyAPI_FUNC(PyObject *) Py_VaBuildValue(const char *, va_list); -#ifndef Py_LIMITED_API -PyAPI_FUNC(PyObject **) _Py_VaBuildStack( - PyObject **small_stack, - Py_ssize_t small_stack_len, - const char *format, - va_list va, - Py_ssize_t *p_nargs); -#endif - -#ifndef Py_LIMITED_API -typedef struct _PyArg_Parser { - const char *format; - const char * const *keywords; - const char *fname; - const char *custom_msg; - int pos; /* number of positional-only arguments */ - int min; /* minimal number of arguments */ - int max; /* maximal number of positional arguments */ - PyObject *kwtuple; /* tuple of keyword parameter names */ - struct _PyArg_Parser *next; -} _PyArg_Parser; -#ifdef PY_SSIZE_T_CLEAN -#define _PyArg_ParseTupleAndKeywordsFast _PyArg_ParseTupleAndKeywordsFast_SizeT -#define _PyArg_ParseStack _PyArg_ParseStack_SizeT -#define _PyArg_ParseStackAndKeywords _PyArg_ParseStackAndKeywords_SizeT -#define _PyArg_VaParseTupleAndKeywordsFast _PyArg_VaParseTupleAndKeywordsFast_SizeT -#endif -PyAPI_FUNC(int) _PyArg_ParseTupleAndKeywordsFast(PyObject *, PyObject *, - struct _PyArg_Parser *, ...); -PyAPI_FUNC(int) _PyArg_ParseStack( - PyObject *const *args, - Py_ssize_t nargs, - const char *format, - ...); -PyAPI_FUNC(int) _PyArg_ParseStackAndKeywords( - PyObject *const *args, - Py_ssize_t nargs, - PyObject *kwnames, - struct _PyArg_Parser *, - ...); -PyAPI_FUNC(int) _PyArg_VaParseTupleAndKeywordsFast(PyObject *, PyObject *, - struct _PyArg_Parser *, va_list); -PyAPI_FUNC(PyObject * const *) _PyArg_UnpackKeywords( - PyObject *const *args, Py_ssize_t nargs, - PyObject *kwargs, PyObject *kwnames, - struct _PyArg_Parser *parser, - int minpos, int maxpos, int minkw, - PyObject **buf); -#define _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, parser, minpos, maxpos, minkw, buf) \ - (((minkw) == 0 && (kwargs) == NULL && (kwnames) == NULL && \ - (minpos) <= (nargs) && (nargs) <= (maxpos) && args != NULL) ? (args) : \ - _PyArg_UnpackKeywords((args), (nargs), (kwargs), (kwnames), (parser), \ - (minpos), (maxpos), (minkw), (buf))) - -void _PyArg_Fini(void); -#endif /* Py_LIMITED_API */ // Add an attribute with name 'name' and value 'obj' to the module 'mod. // On success, return 0 on success. @@ -147,10 +52,12 @@ PyAPI_FUNC(int) PyModule_AddObject(PyObject *mod, const char *, PyObject *value) PyAPI_FUNC(int) PyModule_AddIntConstant(PyObject *, const char *, long); PyAPI_FUNC(int) PyModule_AddStringConstant(PyObject *, const char *, const char *); + #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03090000 /* New in 3.9 */ PyAPI_FUNC(int) PyModule_AddType(PyObject *module, PyTypeObject *type); #endif /* Py_LIMITED_API */ + #define PyModule_AddIntMacro(m, c) PyModule_AddIntConstant(m, #c, c) #define PyModule_AddStringMacro(m, c) PyModule_AddStringConstant(m, #c, c) @@ -223,12 +130,7 @@ PyAPI_FUNC(int) PyModule_ExecDef(PyObject *module, PyModuleDef *def); #define PyModule_FromDefAndSpec2 PyModule_FromDefAndSpec2TraceRefs #endif -PyAPI_FUNC(PyObject *) PyModule_Create2(struct PyModuleDef*, - int apiver); -#ifndef Py_LIMITED_API -PyAPI_FUNC(PyObject *) _PyModule_CreateInitialized(struct PyModuleDef*, - int apiver); -#endif +PyAPI_FUNC(PyObject *) PyModule_Create2(PyModuleDef*, int apiver); #ifdef Py_LIMITED_API #define PyModule_Create(module) \ @@ -251,10 +153,13 @@ PyAPI_FUNC(PyObject *) PyModule_FromDefAndSpec2(PyModuleDef *def, #define PyModule_FromDefAndSpec(module, spec) \ PyModule_FromDefAndSpec2(module, spec, PYTHON_API_VERSION) #endif /* Py_LIMITED_API */ + #endif /* New in 3.5 */ #ifndef Py_LIMITED_API -PyAPI_DATA(const char *) _Py_PackageContext; +# define Py_CPYTHON_MODSUPPORT_H +# include "cpython/modsupport.h" +# undef Py_CPYTHON_MODSUPPORT_H #endif #ifdef __cplusplus diff --git a/src/external/windows/include/python/moduleobject.h b/src/external/windows/include/python/moduleobject.h index 872622b3..cf0d5413 100755 --- a/src/external/windows/include/python/moduleobject.h +++ b/src/external/windows/include/python/moduleobject.h @@ -32,12 +32,12 @@ PyAPI_FUNC(void) _PyModule_Clear(PyObject *); PyAPI_FUNC(void) _PyModule_ClearDict(PyObject *); PyAPI_FUNC(int) _PyModuleSpec_IsInitializing(PyObject *); #endif -PyAPI_FUNC(struct PyModuleDef*) PyModule_GetDef(PyObject*); +PyAPI_FUNC(PyModuleDef*) PyModule_GetDef(PyObject*); PyAPI_FUNC(void*) PyModule_GetState(PyObject*); #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03050000 /* New in 3.5 */ -PyAPI_FUNC(PyObject *) PyModuleDef_Init(struct PyModuleDef*); +PyAPI_FUNC(PyObject *) PyModuleDef_Init(PyModuleDef*); PyAPI_DATA(PyTypeObject) PyModuleDef_Type; #endif @@ -48,20 +48,19 @@ typedef struct PyModuleDef_Base { PyObject* m_copy; } PyModuleDef_Base; -#define PyModuleDef_HEAD_INIT { \ - PyObject_HEAD_INIT(NULL) \ - NULL, /* m_init */ \ - 0, /* m_index */ \ - NULL, /* m_copy */ \ +#define PyModuleDef_HEAD_INIT { \ + PyObject_HEAD_INIT(_Py_NULL) \ + _Py_NULL, /* m_init */ \ + 0, /* m_index */ \ + _Py_NULL, /* m_copy */ \ } -struct PyModuleDef_Slot; #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03050000 /* New in 3.5 */ -typedef struct PyModuleDef_Slot{ +struct PyModuleDef_Slot { int slot; void *value; -} PyModuleDef_Slot; +}; #define Py_mod_create 1 #define Py_mod_exec 2 @@ -72,17 +71,17 @@ typedef struct PyModuleDef_Slot{ #endif /* New in 3.5 */ -typedef struct PyModuleDef{ +struct PyModuleDef { PyModuleDef_Base m_base; const char* m_name; const char* m_doc; Py_ssize_t m_size; PyMethodDef *m_methods; - struct PyModuleDef_Slot* m_slots; + PyModuleDef_Slot *m_slots; traverseproc m_traverse; inquiry m_clear; freefunc m_free; -} PyModuleDef; +}; // Internal C API diff --git a/src/external/windows/include/python/namespaceobject.h b/src/external/windows/include/python/namespaceobject.h deleted file mode 100755 index 35146e5c..00000000 --- a/src/external/windows/include/python/namespaceobject.h +++ /dev/null @@ -1,19 +0,0 @@ - -/* simple namespace object interface */ - -#ifndef NAMESPACEOBJECT_H -#define NAMESPACEOBJECT_H -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef Py_LIMITED_API -PyAPI_DATA(PyTypeObject) _PyNamespace_Type; - -PyAPI_FUNC(PyObject *) _PyNamespace_New(PyObject *kwds); -#endif /* !Py_LIMITED_API */ - -#ifdef __cplusplus -} -#endif -#endif /* !NAMESPACEOBJECT_H */ diff --git a/src/external/windows/include/python/object.h b/src/external/windows/include/python/object.h index 19cb3c5e..59f85820 100755 --- a/src/external/windows/include/python/object.h +++ b/src/external/windows/include/python/object.h @@ -1,6 +1,5 @@ #ifndef Py_OBJECT_H #define Py_OBJECT_H - #ifdef __cplusplus extern "C" { #endif @@ -61,17 +60,13 @@ whose size is determined when the object is allocated. # error Py_LIMITED_API is incompatible with Py_TRACE_REFS #endif -/* PyTypeObject structure is defined in cpython/object.h. - In Py_LIMITED_API, PyTypeObject is an opaque structure. */ -typedef struct _typeobject PyTypeObject; - #ifdef Py_TRACE_REFS /* Define pointers to support a doubly-linked list of all live heap objects. */ #define _PyObject_HEAD_EXTRA \ - struct _object *_ob_next; \ - struct _object *_ob_prev; + PyObject *_ob_next; \ + PyObject *_ob_prev; -#define _PyObject_EXTRA_INIT 0, 0, +#define _PyObject_EXTRA_INIT _Py_NULL, _Py_NULL, #else # define _PyObject_HEAD_EXTRA @@ -102,15 +97,14 @@ typedef struct _typeobject PyTypeObject; * by hand. Similarly every pointer to a variable-size Python object can, * in addition, be cast to PyVarObject*. */ -typedef struct _object { +struct _object { _PyObject_HEAD_EXTRA Py_ssize_t ob_refcnt; PyTypeObject *ob_type; -} PyObject; +}; /* Cast argument to PyObject* type. */ -#define _PyObject_CAST(op) ((PyObject*)(op)) -#define _PyObject_CAST_CONST(op) ((const PyObject*)(op)) +#define _PyObject_CAST(op) _Py_CAST(PyObject*, (op)) typedef struct { PyObject ob_base; @@ -118,8 +112,7 @@ typedef struct { } PyVarObject; /* Cast argument to PyVarObject* type. */ -#define _PyVarObject_CAST(op) ((PyVarObject*)(op)) -#define _PyVarObject_CAST_CONST(op) ((const PyVarObject*)(op)) +#define _PyVarObject_CAST(op) _Py_CAST(PyVarObject*, (op)) // Test if the 'x' object is the 'y' object, the same as "x is y" in Python. @@ -127,43 +120,62 @@ PyAPI_FUNC(int) Py_Is(PyObject *x, PyObject *y); #define Py_Is(x, y) ((x) == (y)) -static inline Py_ssize_t _Py_REFCNT(const PyObject *ob) { +static inline Py_ssize_t Py_REFCNT(PyObject *ob) { return ob->ob_refcnt; } -#define Py_REFCNT(ob) _Py_REFCNT(_PyObject_CAST_CONST(ob)) +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define Py_REFCNT(ob) Py_REFCNT(_PyObject_CAST(ob)) +#endif // bpo-39573: The Py_SET_TYPE() function must be used to set an object type. -#define Py_TYPE(ob) (_PyObject_CAST(ob)->ob_type) +static inline PyTypeObject* Py_TYPE(PyObject *ob) { + return ob->ob_type; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define Py_TYPE(ob) Py_TYPE(_PyObject_CAST(ob)) +#endif // bpo-39573: The Py_SET_SIZE() function must be used to set an object size. -#define Py_SIZE(ob) (_PyVarObject_CAST(ob)->ob_size) - - -static inline int _Py_IS_TYPE(const PyObject *ob, const PyTypeObject *type) { - // bpo-44378: Don't use Py_TYPE() since Py_TYPE() requires a non-const - // object. - return ob->ob_type == type; +static inline Py_ssize_t Py_SIZE(PyObject *ob) { + PyVarObject *var_ob = _PyVarObject_CAST(ob); + return var_ob->ob_size; } -#define Py_IS_TYPE(ob, type) _Py_IS_TYPE(_PyObject_CAST_CONST(ob), type) +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define Py_SIZE(ob) Py_SIZE(_PyObject_CAST(ob)) +#endif -static inline void _Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) { +static inline int Py_IS_TYPE(PyObject *ob, PyTypeObject *type) { + return Py_TYPE(ob) == type; +} +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define Py_IS_TYPE(ob, type) Py_IS_TYPE(_PyObject_CAST(ob), type) +#endif + + +static inline void Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) { ob->ob_refcnt = refcnt; } -#define Py_SET_REFCNT(ob, refcnt) _Py_SET_REFCNT(_PyObject_CAST(ob), refcnt) +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define Py_SET_REFCNT(ob, refcnt) Py_SET_REFCNT(_PyObject_CAST(ob), refcnt) +#endif -static inline void _Py_SET_TYPE(PyObject *ob, PyTypeObject *type) { +static inline void Py_SET_TYPE(PyObject *ob, PyTypeObject *type) { ob->ob_type = type; } -#define Py_SET_TYPE(ob, type) _Py_SET_TYPE(_PyObject_CAST(ob), type) +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define Py_SET_TYPE(ob, type) Py_SET_TYPE(_PyObject_CAST(ob), type) +#endif -static inline void _Py_SET_SIZE(PyVarObject *ob, Py_ssize_t size) { +static inline void Py_SET_SIZE(PyVarObject *ob, Py_ssize_t size) { ob->ob_size = size; } -#define Py_SET_SIZE(ob, size) _Py_SET_SIZE(_PyVarObject_CAST(ob), size) +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define Py_SET_SIZE(ob, size) Py_SET_SIZE(_PyVarObject_CAST(ob), size) +#endif /* @@ -236,17 +248,23 @@ PyAPI_FUNC(void*) PyType_GetSlot(PyTypeObject*, int); #endif #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03090000 PyAPI_FUNC(PyObject*) PyType_FromModuleAndSpec(PyObject *, PyType_Spec *, PyObject *); -PyAPI_FUNC(PyObject *) PyType_GetModule(struct _typeobject *); -PyAPI_FUNC(void *) PyType_GetModuleState(struct _typeobject *); +PyAPI_FUNC(PyObject *) PyType_GetModule(PyTypeObject *); +PyAPI_FUNC(void *) PyType_GetModuleState(PyTypeObject *); +#endif +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030B0000 +PyAPI_FUNC(PyObject *) PyType_GetName(PyTypeObject *); +PyAPI_FUNC(PyObject *) PyType_GetQualName(PyTypeObject *); #endif /* Generic type check */ PyAPI_FUNC(int) PyType_IsSubtype(PyTypeObject *, PyTypeObject *); -static inline int _PyObject_TypeCheck(PyObject *ob, PyTypeObject *type) { +static inline int PyObject_TypeCheck(PyObject *ob, PyTypeObject *type) { return Py_IS_TYPE(ob, type) || PyType_IsSubtype(Py_TYPE(ob), type); } -#define PyObject_TypeCheck(ob, type) _PyObject_TypeCheck(_PyObject_CAST(ob), type) +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyObject_TypeCheck(ob, type) PyObject_TypeCheck(_PyObject_CAST(ob), type) +#endif PyAPI_DATA(PyTypeObject) PyType_Type; /* built-in 'type' */ PyAPI_DATA(PyTypeObject) PyBaseObject_Type; /* built-in 'object' */ @@ -294,6 +312,11 @@ PyAPI_FUNC(void) PyObject_ClearWeakRefs(PyObject *); */ PyAPI_FUNC(PyObject *) PyObject_Dir(PyObject *); +/* Pickle support. */ +#ifndef Py_LIMITED_API +PyAPI_FUNC(PyObject *) _PyObject_GetState(PyObject *); +#endif + /* Helpers for printing recursive container types */ PyAPI_FUNC(int) Py_ReprEnter(PyObject *); @@ -323,6 +346,13 @@ given type object has a specified feature. */ #ifndef Py_LIMITED_API + +/* Placement of dict (and values) pointers are managed by the VM, not by the type. + * The VM will automatically set tp_dictoffset. Should not be used for variable sized + * classes, such as classes that extend tuple. + */ +#define Py_TPFLAGS_MANAGED_DICT (1 << 4) + /* Set if instances of the type object are treated as sequences for pattern matching */ #define Py_TPFLAGS_SEQUENCE (1 << 5) /* Set if instances of the type object are treated as mappings for pattern matching */ @@ -458,7 +488,7 @@ PyAPI_FUNC(void) Py_DecRef(PyObject *); PyAPI_FUNC(void) _Py_IncRef(PyObject *); PyAPI_FUNC(void) _Py_DecRef(PyObject *); -static inline void _Py_INCREF(PyObject *op) +static inline void Py_INCREF(PyObject *op) { #if defined(Py_REF_DEBUG) && defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030A0000 // Stable ABI for Python 3.10 built in debug mode. @@ -472,39 +502,43 @@ static inline void _Py_INCREF(PyObject *op) op->ob_refcnt++; #endif } -#define Py_INCREF(op) _Py_INCREF(_PyObject_CAST(op)) +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define Py_INCREF(op) Py_INCREF(_PyObject_CAST(op)) +#endif + -static inline void _Py_DECREF( -#if defined(Py_REF_DEBUG) && !(defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030A0000) - const char *filename, int lineno, -#endif - PyObject *op) -{ #if defined(Py_REF_DEBUG) && defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030A0000 - // Stable ABI for Python 3.10 built in debug mode. +// Stable ABI for limited C API version 3.10 of Python debug build +static inline void Py_DECREF(PyObject *op) { _Py_DecRef(op); -#else - // Non-limited C API and limited C API for Python 3.9 and older access - // directly PyObject.ob_refcnt. -#ifdef Py_REF_DEBUG +} +#define Py_DECREF(op) Py_DECREF(_PyObject_CAST(op)) + +#elif defined(Py_REF_DEBUG) +static inline void Py_DECREF(const char *filename, int lineno, PyObject *op) +{ _Py_RefTotal--; -#endif if (--op->ob_refcnt != 0) { -#ifdef Py_REF_DEBUG if (op->ob_refcnt < 0) { _Py_NegativeRefcount(filename, lineno, op); } -#endif } else { _Py_Dealloc(op); } -#endif } -#if defined(Py_REF_DEBUG) && !(defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030A0000) -# define Py_DECREF(op) _Py_DECREF(__FILE__, __LINE__, _PyObject_CAST(op)) +#define Py_DECREF(op) Py_DECREF(__FILE__, __LINE__, _PyObject_CAST(op)) + #else -# define Py_DECREF(op) _Py_DECREF(_PyObject_CAST(op)) +static inline void Py_DECREF(PyObject *op) +{ + // Non-limited C API and limited C API for Python 3.9 and older access + // directly PyObject.ob_refcnt. + if (--op->ob_refcnt == 0) { + _Py_Dealloc(op); + } +} +#define Py_DECREF(op) Py_DECREF(_PyObject_CAST(op)) #endif @@ -552,23 +586,25 @@ static inline void _Py_DECREF( } while (0) /* Function to use in case the object pointer can be NULL: */ -static inline void _Py_XINCREF(PyObject *op) +static inline void Py_XINCREF(PyObject *op) { - if (op != NULL) { + if (op != _Py_NULL) { Py_INCREF(op); } } +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define Py_XINCREF(op) Py_XINCREF(_PyObject_CAST(op)) +#endif -#define Py_XINCREF(op) _Py_XINCREF(_PyObject_CAST(op)) - -static inline void _Py_XDECREF(PyObject *op) +static inline void Py_XDECREF(PyObject *op) { - if (op != NULL) { + if (op != _Py_NULL) { Py_DECREF(op); } } - -#define Py_XDECREF(op) _Py_XDECREF(_PyObject_CAST(op)) +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define Py_XDECREF(op) Py_XDECREF(_PyObject_CAST(op)) +#endif // Create a new strong reference to an object: // increment the reference count of the object and return the object. @@ -592,8 +628,13 @@ static inline PyObject* _Py_XNewRef(PyObject *obj) // Py_NewRef() and Py_XNewRef() are exported as functions for the stable ABI. // Names overridden with macros by static inline functions for best // performances. -#define Py_NewRef(obj) _Py_NewRef(_PyObject_CAST(obj)) -#define Py_XNewRef(obj) _Py_XNewRef(_PyObject_CAST(obj)) +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define Py_NewRef(obj) _Py_NewRef(_PyObject_CAST(obj)) +# define Py_XNewRef(obj) _Py_XNewRef(_PyObject_CAST(obj)) +#else +# define Py_NewRef(obj) _Py_NewRef(obj) +# define Py_XNewRef(obj) _Py_XNewRef(obj) +#endif /* @@ -713,7 +754,7 @@ times. #ifndef Py_LIMITED_API # define Py_CPYTHON_OBJECT_H -# include "cpython/object.h" +# include "cpython/object.h" # undef Py_CPYTHON_OBJECT_H #endif @@ -733,17 +774,24 @@ PyType_HasFeature(PyTypeObject *type, unsigned long feature) #define PyType_FastSubclass(type, flag) PyType_HasFeature(type, flag) -static inline int _PyType_Check(PyObject *op) { +static inline int PyType_Check(PyObject *op) { return PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_TYPE_SUBCLASS); } -#define PyType_Check(op) _PyType_Check(_PyObject_CAST(op)) +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyType_Check(op) PyType_Check(_PyObject_CAST(op)) +#endif -static inline int _PyType_CheckExact(PyObject *op) { +#define _PyType_CAST(op) \ + (assert(PyType_Check(op)), _Py_CAST(PyTypeObject*, (op))) + +static inline int PyType_CheckExact(PyObject *op) { return Py_IS_TYPE(op, &PyType_Type); } -#define PyType_CheckExact(op) _PyType_CheckExact(_PyObject_CAST(op)) +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define PyType_CheckExact(op) PyType_CheckExact(_PyObject_CAST(op)) +#endif #ifdef __cplusplus } #endif -#endif /* !Py_OBJECT_H */ +#endif // !Py_OBJECT_H diff --git a/src/external/windows/include/python/objimpl.h b/src/external/windows/include/python/objimpl.h index 2c9f308c..11b5c7af 100755 --- a/src/external/windows/include/python/objimpl.h +++ b/src/external/windows/include/python/objimpl.h @@ -182,9 +182,9 @@ PyAPI_FUNC(void) PyObject_GC_UnTrack(void *); PyAPI_FUNC(void) PyObject_GC_Del(void *); #define PyObject_GC_New(type, typeobj) \ - ( (type *) _PyObject_GC_New(typeobj) ) + _Py_CAST(type*, _PyObject_GC_New(typeobj)) #define PyObject_GC_NewVar(type, typeobj, n) \ - ( (type *) _PyObject_GC_NewVar((typeobj), (n)) ) + _Py_CAST(type*, _PyObject_GC_NewVar((typeobj), (n))) PyAPI_FUNC(int) PyObject_GC_IsTracked(PyObject *); PyAPI_FUNC(int) PyObject_GC_IsFinalized(PyObject *); @@ -205,7 +205,7 @@ PyAPI_FUNC(int) PyObject_GC_IsFinalized(PyObject *); #ifndef Py_LIMITED_API # define Py_CPYTHON_OBJIMPL_H -# include "cpython/objimpl.h" +# include "cpython/objimpl.h" # undef Py_CPYTHON_OBJIMPL_H #endif diff --git a/src/external/windows/include/python/opcode.h b/src/external/windows/include/python/opcode.h index 8c62f466..27cd9368 100755 --- a/src/external/windows/include/python/opcode.h +++ b/src/external/windows/include/python/opcode.h @@ -1,4 +1,5 @@ -/* Auto-generated by Tools/scripts/generate_opcode_h.py from Lib/opcode.py */ +// Auto-generated by Tools/scripts/generate_opcode_h.py from Lib/opcode.py + #ifndef Py_OPCODE_H #define Py_OPCODE_H #ifdef __cplusplus @@ -6,166 +7,229 @@ extern "C" { #endif - /* Instruction opcodes for compiled code */ -#define POP_TOP 1 -#define ROT_TWO 2 -#define ROT_THREE 3 -#define DUP_TOP 4 -#define DUP_TOP_TWO 5 -#define ROT_FOUR 6 -#define NOP 9 -#define UNARY_POSITIVE 10 -#define UNARY_NEGATIVE 11 -#define UNARY_NOT 12 -#define UNARY_INVERT 15 -#define BINARY_MATRIX_MULTIPLY 16 -#define INPLACE_MATRIX_MULTIPLY 17 -#define BINARY_POWER 19 -#define BINARY_MULTIPLY 20 -#define BINARY_MODULO 22 -#define BINARY_ADD 23 -#define BINARY_SUBTRACT 24 -#define BINARY_SUBSCR 25 -#define BINARY_FLOOR_DIVIDE 26 -#define BINARY_TRUE_DIVIDE 27 -#define INPLACE_FLOOR_DIVIDE 28 -#define INPLACE_TRUE_DIVIDE 29 -#define GET_LEN 30 -#define MATCH_MAPPING 31 -#define MATCH_SEQUENCE 32 -#define MATCH_KEYS 33 -#define COPY_DICT_WITHOUT_KEYS 34 -#define WITH_EXCEPT_START 49 -#define GET_AITER 50 -#define GET_ANEXT 51 -#define BEFORE_ASYNC_WITH 52 -#define END_ASYNC_FOR 54 -#define INPLACE_ADD 55 -#define INPLACE_SUBTRACT 56 -#define INPLACE_MULTIPLY 57 -#define INPLACE_MODULO 59 -#define STORE_SUBSCR 60 -#define DELETE_SUBSCR 61 -#define BINARY_LSHIFT 62 -#define BINARY_RSHIFT 63 -#define BINARY_AND 64 -#define BINARY_XOR 65 -#define BINARY_OR 66 -#define INPLACE_POWER 67 -#define GET_ITER 68 -#define GET_YIELD_FROM_ITER 69 -#define PRINT_EXPR 70 -#define LOAD_BUILD_CLASS 71 -#define YIELD_FROM 72 -#define GET_AWAITABLE 73 -#define LOAD_ASSERTION_ERROR 74 -#define INPLACE_LSHIFT 75 -#define INPLACE_RSHIFT 76 -#define INPLACE_AND 77 -#define INPLACE_XOR 78 -#define INPLACE_OR 79 -#define LIST_TO_TUPLE 82 -#define RETURN_VALUE 83 -#define IMPORT_STAR 84 -#define SETUP_ANNOTATIONS 85 -#define YIELD_VALUE 86 -#define POP_BLOCK 87 -#define POP_EXCEPT 89 -#define HAVE_ARGUMENT 90 -#define STORE_NAME 90 -#define DELETE_NAME 91 -#define UNPACK_SEQUENCE 92 -#define FOR_ITER 93 -#define UNPACK_EX 94 -#define STORE_ATTR 95 -#define DELETE_ATTR 96 -#define STORE_GLOBAL 97 -#define DELETE_GLOBAL 98 -#define ROT_N 99 -#define LOAD_CONST 100 -#define LOAD_NAME 101 -#define BUILD_TUPLE 102 -#define BUILD_LIST 103 -#define BUILD_SET 104 -#define BUILD_MAP 105 -#define LOAD_ATTR 106 -#define COMPARE_OP 107 -#define IMPORT_NAME 108 -#define IMPORT_FROM 109 -#define JUMP_FORWARD 110 -#define JUMP_IF_FALSE_OR_POP 111 -#define JUMP_IF_TRUE_OR_POP 112 -#define JUMP_ABSOLUTE 113 -#define POP_JUMP_IF_FALSE 114 -#define POP_JUMP_IF_TRUE 115 -#define LOAD_GLOBAL 116 -#define IS_OP 117 -#define CONTAINS_OP 118 -#define RERAISE 119 -#define JUMP_IF_NOT_EXC_MATCH 121 -#define SETUP_FINALLY 122 -#define LOAD_FAST 124 -#define STORE_FAST 125 -#define DELETE_FAST 126 -#define GEN_START 129 -#define RAISE_VARARGS 130 -#define CALL_FUNCTION 131 -#define MAKE_FUNCTION 132 -#define BUILD_SLICE 133 -#define LOAD_CLOSURE 135 -#define LOAD_DEREF 136 -#define STORE_DEREF 137 -#define DELETE_DEREF 138 -#define CALL_FUNCTION_KW 141 -#define CALL_FUNCTION_EX 142 -#define SETUP_WITH 143 -#define EXTENDED_ARG 144 -#define LIST_APPEND 145 -#define SET_ADD 146 -#define MAP_ADD 147 -#define LOAD_CLASSDEREF 148 -#define MATCH_CLASS 152 -#define SETUP_ASYNC_WITH 154 -#define FORMAT_VALUE 155 -#define BUILD_CONST_KEY_MAP 156 -#define BUILD_STRING 157 -#define LOAD_METHOD 160 -#define CALL_METHOD 161 -#define LIST_EXTEND 162 -#define SET_UPDATE 163 -#define DICT_MERGE 164 -#define DICT_UPDATE 165 -#ifdef NEED_OPCODE_JUMP_TABLES -static uint32_t _PyOpcode_RelativeJump[8] = { - 0U, - 0U, - 536870912U, - 67125248U, - 67141632U, - 0U, - 0U, - 0U, -}; -static uint32_t _PyOpcode_Jump[8] = { - 0U, - 0U, - 536870912U, - 101695488U, - 67141632U, - 0U, - 0U, - 0U, -}; -#endif /* OPCODE_TABLES */ +/* Instruction opcodes for compiled code */ +#define CACHE 0 +#define POP_TOP 1 +#define PUSH_NULL 2 +#define NOP 9 +#define UNARY_POSITIVE 10 +#define UNARY_NEGATIVE 11 +#define UNARY_NOT 12 +#define UNARY_INVERT 15 +#define BINARY_SUBSCR 25 +#define GET_LEN 30 +#define MATCH_MAPPING 31 +#define MATCH_SEQUENCE 32 +#define MATCH_KEYS 33 +#define PUSH_EXC_INFO 35 +#define CHECK_EXC_MATCH 36 +#define CHECK_EG_MATCH 37 +#define WITH_EXCEPT_START 49 +#define GET_AITER 50 +#define GET_ANEXT 51 +#define BEFORE_ASYNC_WITH 52 +#define BEFORE_WITH 53 +#define END_ASYNC_FOR 54 +#define STORE_SUBSCR 60 +#define DELETE_SUBSCR 61 +#define GET_ITER 68 +#define GET_YIELD_FROM_ITER 69 +#define PRINT_EXPR 70 +#define LOAD_BUILD_CLASS 71 +#define LOAD_ASSERTION_ERROR 74 +#define RETURN_GENERATOR 75 +#define LIST_TO_TUPLE 82 +#define RETURN_VALUE 83 +#define IMPORT_STAR 84 +#define SETUP_ANNOTATIONS 85 +#define YIELD_VALUE 86 +#define ASYNC_GEN_WRAP 87 +#define PREP_RERAISE_STAR 88 +#define POP_EXCEPT 89 +#define HAVE_ARGUMENT 90 +#define STORE_NAME 90 +#define DELETE_NAME 91 +#define UNPACK_SEQUENCE 92 +#define FOR_ITER 93 +#define UNPACK_EX 94 +#define STORE_ATTR 95 +#define DELETE_ATTR 96 +#define STORE_GLOBAL 97 +#define DELETE_GLOBAL 98 +#define SWAP 99 +#define LOAD_CONST 100 +#define LOAD_NAME 101 +#define BUILD_TUPLE 102 +#define BUILD_LIST 103 +#define BUILD_SET 104 +#define BUILD_MAP 105 +#define LOAD_ATTR 106 +#define COMPARE_OP 107 +#define IMPORT_NAME 108 +#define IMPORT_FROM 109 +#define JUMP_FORWARD 110 +#define JUMP_IF_FALSE_OR_POP 111 +#define JUMP_IF_TRUE_OR_POP 112 +#define POP_JUMP_FORWARD_IF_FALSE 114 +#define POP_JUMP_FORWARD_IF_TRUE 115 +#define LOAD_GLOBAL 116 +#define IS_OP 117 +#define CONTAINS_OP 118 +#define RERAISE 119 +#define COPY 120 +#define BINARY_OP 122 +#define SEND 123 +#define LOAD_FAST 124 +#define STORE_FAST 125 +#define DELETE_FAST 126 +#define POP_JUMP_FORWARD_IF_NOT_NONE 128 +#define POP_JUMP_FORWARD_IF_NONE 129 +#define RAISE_VARARGS 130 +#define GET_AWAITABLE 131 +#define MAKE_FUNCTION 132 +#define BUILD_SLICE 133 +#define JUMP_BACKWARD_NO_INTERRUPT 134 +#define MAKE_CELL 135 +#define LOAD_CLOSURE 136 +#define LOAD_DEREF 137 +#define STORE_DEREF 138 +#define DELETE_DEREF 139 +#define JUMP_BACKWARD 140 +#define CALL_FUNCTION_EX 142 +#define EXTENDED_ARG 144 +#define LIST_APPEND 145 +#define SET_ADD 146 +#define MAP_ADD 147 +#define LOAD_CLASSDEREF 148 +#define COPY_FREE_VARS 149 +#define RESUME 151 +#define MATCH_CLASS 152 +#define FORMAT_VALUE 155 +#define BUILD_CONST_KEY_MAP 156 +#define BUILD_STRING 157 +#define LOAD_METHOD 160 +#define LIST_EXTEND 162 +#define SET_UPDATE 163 +#define DICT_MERGE 164 +#define DICT_UPDATE 165 +#define PRECALL 166 +#define CALL 171 +#define KW_NAMES 172 +#define POP_JUMP_BACKWARD_IF_NOT_NONE 173 +#define POP_JUMP_BACKWARD_IF_NONE 174 +#define POP_JUMP_BACKWARD_IF_FALSE 175 +#define POP_JUMP_BACKWARD_IF_TRUE 176 +#define BINARY_OP_ADAPTIVE 3 +#define BINARY_OP_ADD_FLOAT 4 +#define BINARY_OP_ADD_INT 5 +#define BINARY_OP_ADD_UNICODE 6 +#define BINARY_OP_INPLACE_ADD_UNICODE 7 +#define BINARY_OP_MULTIPLY_FLOAT 8 +#define BINARY_OP_MULTIPLY_INT 13 +#define BINARY_OP_SUBTRACT_FLOAT 14 +#define BINARY_OP_SUBTRACT_INT 16 +#define BINARY_SUBSCR_ADAPTIVE 17 +#define BINARY_SUBSCR_DICT 18 +#define BINARY_SUBSCR_GETITEM 19 +#define BINARY_SUBSCR_LIST_INT 20 +#define BINARY_SUBSCR_TUPLE_INT 21 +#define CALL_ADAPTIVE 22 +#define CALL_PY_EXACT_ARGS 23 +#define CALL_PY_WITH_DEFAULTS 24 +#define COMPARE_OP_ADAPTIVE 26 +#define COMPARE_OP_FLOAT_JUMP 27 +#define COMPARE_OP_INT_JUMP 28 +#define COMPARE_OP_STR_JUMP 29 +#define EXTENDED_ARG_QUICK 34 +#define JUMP_BACKWARD_QUICK 38 +#define LOAD_ATTR_ADAPTIVE 39 +#define LOAD_ATTR_INSTANCE_VALUE 40 +#define LOAD_ATTR_MODULE 41 +#define LOAD_ATTR_SLOT 42 +#define LOAD_ATTR_WITH_HINT 43 +#define LOAD_CONST__LOAD_FAST 44 +#define LOAD_FAST__LOAD_CONST 45 +#define LOAD_FAST__LOAD_FAST 46 +#define LOAD_GLOBAL_ADAPTIVE 47 +#define LOAD_GLOBAL_BUILTIN 48 +#define LOAD_GLOBAL_MODULE 55 +#define LOAD_METHOD_ADAPTIVE 56 +#define LOAD_METHOD_CLASS 57 +#define LOAD_METHOD_MODULE 58 +#define LOAD_METHOD_NO_DICT 59 +#define LOAD_METHOD_WITH_DICT 62 +#define LOAD_METHOD_WITH_VALUES 63 +#define PRECALL_ADAPTIVE 64 +#define PRECALL_BOUND_METHOD 65 +#define PRECALL_BUILTIN_CLASS 66 +#define PRECALL_BUILTIN_FAST_WITH_KEYWORDS 67 +#define PRECALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS 72 +#define PRECALL_NO_KW_BUILTIN_FAST 73 +#define PRECALL_NO_KW_BUILTIN_O 76 +#define PRECALL_NO_KW_ISINSTANCE 77 +#define PRECALL_NO_KW_LEN 78 +#define PRECALL_NO_KW_LIST_APPEND 79 +#define PRECALL_NO_KW_METHOD_DESCRIPTOR_FAST 80 +#define PRECALL_NO_KW_METHOD_DESCRIPTOR_NOARGS 81 +#define PRECALL_NO_KW_METHOD_DESCRIPTOR_O 113 +#define PRECALL_NO_KW_STR_1 121 +#define PRECALL_NO_KW_TUPLE_1 127 +#define PRECALL_NO_KW_TYPE_1 141 +#define PRECALL_PYFUNC 143 +#define RESUME_QUICK 150 +#define STORE_ATTR_ADAPTIVE 153 +#define STORE_ATTR_INSTANCE_VALUE 154 +#define STORE_ATTR_SLOT 158 +#define STORE_ATTR_WITH_HINT 159 +#define STORE_FAST__LOAD_FAST 161 +#define STORE_FAST__STORE_FAST 167 +#define STORE_SUBSCR_ADAPTIVE 168 +#define STORE_SUBSCR_DICT 169 +#define STORE_SUBSCR_LIST_INT 170 +#define UNPACK_SEQUENCE_ADAPTIVE 177 +#define UNPACK_SEQUENCE_LIST 178 +#define UNPACK_SEQUENCE_TUPLE 179 +#define UNPACK_SEQUENCE_TWO_TUPLE 180 +#define DO_TRACING 255 -/* EXCEPT_HANDLER is a special, implicit block type which is created when - entering an except handler. It is not an opcode but we define it here - as we want it to be available to both frameobject.c and ceval.c, while - remaining private.*/ -#define EXCEPT_HANDLER 257 +#define HAS_CONST(op) (false\ + || ((op) == 100) \ + || ((op) == 172) \ + ) + +#define NB_ADD 0 +#define NB_AND 1 +#define NB_FLOOR_DIVIDE 2 +#define NB_LSHIFT 3 +#define NB_MATRIX_MULTIPLY 4 +#define NB_MULTIPLY 5 +#define NB_REMAINDER 6 +#define NB_OR 7 +#define NB_POWER 8 +#define NB_RSHIFT 9 +#define NB_SUBTRACT 10 +#define NB_TRUE_DIVIDE 11 +#define NB_XOR 12 +#define NB_INPLACE_ADD 13 +#define NB_INPLACE_AND 14 +#define NB_INPLACE_FLOOR_DIVIDE 15 +#define NB_INPLACE_LSHIFT 16 +#define NB_INPLACE_MATRIX_MULTIPLY 17 +#define NB_INPLACE_MULTIPLY 18 +#define NB_INPLACE_REMAINDER 19 +#define NB_INPLACE_OR 20 +#define NB_INPLACE_POWER 21 +#define NB_INPLACE_RSHIFT 22 +#define NB_INPLACE_SUBTRACT 23 +#define NB_INPLACE_TRUE_DIVIDE 24 +#define NB_INPLACE_XOR 25 #define HAS_ARG(op) ((op) >= HAVE_ARGUMENT) +/* Reserve some bytecodes for internal use in the compiler. + * The value of 240 is arbitrary. */ +#define IS_ARTIFICIAL(op) ((op) > 240) + #ifdef __cplusplus } #endif diff --git a/src/external/windows/include/python/patchlevel.h b/src/external/windows/include/python/patchlevel.h index 68439b81..240ca8d2 100755 --- a/src/external/windows/include/python/patchlevel.h +++ b/src/external/windows/include/python/patchlevel.h @@ -17,13 +17,13 @@ /* Version parsed out into numeric values */ /*--start constants--*/ #define PY_MAJOR_VERSION 3 -#define PY_MINOR_VERSION 10 -#define PY_MICRO_VERSION 4 +#define PY_MINOR_VERSION 11 +#define PY_MICRO_VERSION 3 #define PY_RELEASE_LEVEL PY_RELEASE_LEVEL_FINAL #define PY_RELEASE_SERIAL 0 /* Version as a string */ -#define PY_VERSION "3.10.4" +#define PY_VERSION "3.11.3" /*--end constants--*/ /* Version as a single 4-byte hex number, e.g. 0x010502B2 == 1.5.2b2. diff --git a/src/external/windows/include/python/py_curses.h b/src/external/windows/include/python/py_curses.h index b65aaa7b..68b847ed 100755 --- a/src/external/windows/include/python/py_curses.h +++ b/src/external/windows/include/python/py_curses.h @@ -77,7 +77,7 @@ typedef struct { static void **PyCurses_API; -#define PyCursesWindow_Type (*(PyTypeObject *) PyCurses_API[0]) +#define PyCursesWindow_Type (*_PyType_CAST(PyCurses_API[0])) #define PyCursesSetupTermCalled {if (! ((int (*)(void))PyCurses_API[1]) () ) return NULL;} #define PyCursesInitialised {if (! ((int (*)(void))PyCurses_API[2]) () ) return NULL;} #define PyCursesInitialisedColor {if (! ((int (*)(void))PyCurses_API[3]) () ) return NULL;} diff --git a/src/external/windows/include/python/pybuffer.h b/src/external/windows/include/python/pybuffer.h new file mode 100644 index 00000000..ec802d88 --- /dev/null +++ b/src/external/windows/include/python/pybuffer.h @@ -0,0 +1,142 @@ +/* Public Py_buffer API */ + +#ifndef Py_BUFFER_H +#define Py_BUFFER_H +#ifdef __cplusplus +extern "C" { +#endif + +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030b0000 + +/* === New Buffer API ============================================ + * Limited API and stable ABI since Python 3.11 + * + * Py_buffer struct layout and size is now part of the stable abi3. The + * struct layout and size must not be changed in any way, as it would + * break the ABI. + * + */ + +typedef struct { + void *buf; + PyObject *obj; /* owned reference */ + Py_ssize_t len; + Py_ssize_t itemsize; /* This is Py_ssize_t so it can be + pointed to by strides in simple case.*/ + int readonly; + int ndim; + char *format; + Py_ssize_t *shape; + Py_ssize_t *strides; + Py_ssize_t *suboffsets; + void *internal; +} Py_buffer; + +/* Return 1 if the getbuffer function is available, otherwise return 0. */ +PyAPI_FUNC(int) PyObject_CheckBuffer(PyObject *obj); + +/* This is a C-API version of the getbuffer function call. It checks + to make sure object has the required function pointer and issues the + call. + + Returns -1 and raises an error on failure and returns 0 on success. */ +PyAPI_FUNC(int) PyObject_GetBuffer(PyObject *obj, Py_buffer *view, + int flags); + +/* Get the memory area pointed to by the indices for the buffer given. + Note that view->ndim is the assumed size of indices. */ +PyAPI_FUNC(void *) PyBuffer_GetPointer(const Py_buffer *view, const Py_ssize_t *indices); + +/* Return the implied itemsize of the data-format area from a + struct-style description. */ +PyAPI_FUNC(Py_ssize_t) PyBuffer_SizeFromFormat(const char *format); + +/* Implementation in memoryobject.c */ +PyAPI_FUNC(int) PyBuffer_ToContiguous(void *buf, const Py_buffer *view, + Py_ssize_t len, char order); + +PyAPI_FUNC(int) PyBuffer_FromContiguous(const Py_buffer *view, const void *buf, + Py_ssize_t len, char order); + +/* Copy len bytes of data from the contiguous chunk of memory + pointed to by buf into the buffer exported by obj. Return + 0 on success and return -1 and raise a PyBuffer_Error on + error (i.e. the object does not have a buffer interface or + it is not working). + + If fort is 'F', then if the object is multi-dimensional, + then the data will be copied into the array in + Fortran-style (first dimension varies the fastest). If + fort is 'C', then the data will be copied into the array + in C-style (last dimension varies the fastest). If fort + is 'A', then it does not matter and the copy will be made + in whatever way is more efficient. */ +PyAPI_FUNC(int) PyObject_CopyData(PyObject *dest, PyObject *src); + +/* Copy the data from the src buffer to the buffer of destination. */ +PyAPI_FUNC(int) PyBuffer_IsContiguous(const Py_buffer *view, char fort); + +/*Fill the strides array with byte-strides of a contiguous + (Fortran-style if fort is 'F' or C-style otherwise) + array of the given shape with the given number of bytes + per element. */ +PyAPI_FUNC(void) PyBuffer_FillContiguousStrides(int ndims, + Py_ssize_t *shape, + Py_ssize_t *strides, + int itemsize, + char fort); + +/* Fills in a buffer-info structure correctly for an exporter + that can only share a contiguous chunk of memory of + "unsigned bytes" of the given length. + + Returns 0 on success and -1 (with raising an error) on error. */ +PyAPI_FUNC(int) PyBuffer_FillInfo(Py_buffer *view, PyObject *o, void *buf, + Py_ssize_t len, int readonly, + int flags); + +/* Releases a Py_buffer obtained from getbuffer ParseTuple's "s*". */ +PyAPI_FUNC(void) PyBuffer_Release(Py_buffer *view); + +/* Maximum number of dimensions */ +#define PyBUF_MAX_NDIM 64 + +/* Flags for getting buffers */ +#define PyBUF_SIMPLE 0 +#define PyBUF_WRITABLE 0x0001 + +#ifndef Py_LIMITED_API +/* we used to include an E, backwards compatible alias */ +#define PyBUF_WRITEABLE PyBUF_WRITABLE +#endif + +#define PyBUF_FORMAT 0x0004 +#define PyBUF_ND 0x0008 +#define PyBUF_STRIDES (0x0010 | PyBUF_ND) +#define PyBUF_C_CONTIGUOUS (0x0020 | PyBUF_STRIDES) +#define PyBUF_F_CONTIGUOUS (0x0040 | PyBUF_STRIDES) +#define PyBUF_ANY_CONTIGUOUS (0x0080 | PyBUF_STRIDES) +#define PyBUF_INDIRECT (0x0100 | PyBUF_STRIDES) + +#define PyBUF_CONTIG (PyBUF_ND | PyBUF_WRITABLE) +#define PyBUF_CONTIG_RO (PyBUF_ND) + +#define PyBUF_STRIDED (PyBUF_STRIDES | PyBUF_WRITABLE) +#define PyBUF_STRIDED_RO (PyBUF_STRIDES) + +#define PyBUF_RECORDS (PyBUF_STRIDES | PyBUF_WRITABLE | PyBUF_FORMAT) +#define PyBUF_RECORDS_RO (PyBUF_STRIDES | PyBUF_FORMAT) + +#define PyBUF_FULL (PyBUF_INDIRECT | PyBUF_WRITABLE | PyBUF_FORMAT) +#define PyBUF_FULL_RO (PyBUF_INDIRECT | PyBUF_FORMAT) + + +#define PyBUF_READ 0x100 +#define PyBUF_WRITE 0x200 + +#endif /* !Py_LIMITED_API || Py_LIMITED_API >= 3.11 */ + +#ifdef __cplusplus +} +#endif +#endif /* Py_BUFFER_H */ diff --git a/src/external/windows/include/python/pyconfig.h b/src/external/windows/include/python/pyconfig.h index 651f6ecf..b8615a7a 100755 --- a/src/external/windows/include/python/pyconfig.h +++ b/src/external/windows/include/python/pyconfig.h @@ -58,7 +58,6 @@ WIN32 is still required for the locale module. #include -#define HAVE_HYPOT #define HAVE_STRFTIME #define DONT_HAVE_SIG_ALARM #define DONT_HAVE_SIG_PAUSE @@ -67,9 +66,6 @@ WIN32 is still required for the locale module. #define MS_WIN32 /* only support win32 and greater. */ #define MS_WINDOWS -#ifndef PYTHONPATH -# define PYTHONPATH L".\\DLLs;.\\lib" -#endif #define NT_THREADS #define WITH_THREAD #ifndef NETSCAPE_PI @@ -117,20 +113,30 @@ WIN32 is still required for the locale module. #define MS_WIN64 #endif -/* set the COMPILER */ +/* set the COMPILER and support tier + * + * win_amd64 MSVC (x86_64-pc-windows-msvc): 1 + * win32 MSVC (i686-pc-windows-msvc): 1 + * win_arm64 MSVC (aarch64-pc-windows-msvc): 3 + * other archs and ICC: 0 + */ #ifdef MS_WIN64 #if defined(_M_X64) || defined(_M_AMD64) #if defined(__INTEL_COMPILER) #define COMPILER ("[ICC v." _Py_STRINGIZE(__INTEL_COMPILER) " 64 bit (amd64) with MSC v." _Py_STRINGIZE(_MSC_VER) " CRT]") +#define PY_SUPPORT_TIER 0 #else #define COMPILER _Py_PASTE_VERSION("64 bit (AMD64)") +#define PY_SUPPORT_TIER 1 #endif /* __INTEL_COMPILER */ #define PYD_PLATFORM_TAG "win_amd64" #elif defined(_M_ARM64) #define COMPILER _Py_PASTE_VERSION("64 bit (ARM64)") +#define PY_SUPPORT_TIER 3 #define PYD_PLATFORM_TAG "win_arm64" #else #define COMPILER _Py_PASTE_VERSION("64 bit (Unknown)") +#define PY_SUPPORT_TIER 0 #endif #endif /* MS_WIN64 */ @@ -166,8 +172,10 @@ WIN32 is still required for the locale module. /* Define like size_t, omitting the "unsigned" */ #ifdef MS_WIN64 typedef __int64 Py_ssize_t; +# define PY_SSIZE_T_MAX LLONG_MAX #else typedef _W64 int Py_ssize_t; +# define PY_SSIZE_T_MAX INT_MAX #endif #define HAVE_PY_SSIZE_T 1 @@ -175,25 +183,24 @@ typedef _W64 int Py_ssize_t; #if defined(_M_IX86) #if defined(__INTEL_COMPILER) #define COMPILER ("[ICC v." _Py_STRINGIZE(__INTEL_COMPILER) " 32 bit (Intel) with MSC v." _Py_STRINGIZE(_MSC_VER) " CRT]") +#define PY_SUPPORT_TIER 0 #else #define COMPILER _Py_PASTE_VERSION("32 bit (Intel)") +#define PY_SUPPORT_TIER 1 #endif /* __INTEL_COMPILER */ #define PYD_PLATFORM_TAG "win32" #elif defined(_M_ARM) #define COMPILER _Py_PASTE_VERSION("32 bit (ARM)") #define PYD_PLATFORM_TAG "win_arm32" +#define PY_SUPPORT_TIER 0 #else #define COMPILER _Py_PASTE_VERSION("32 bit (Unknown)") +#define PY_SUPPORT_TIER 0 #endif #endif /* MS_WIN32 && !MS_WIN64 */ typedef int pid_t; -#include -#define Py_IS_NAN _isnan -#define Py_IS_INFINITY(X) (!_finite(X) && !_isnan(X)) -#define Py_IS_FINITE(X) _finite(X) - /* define some ANSI types that are not defined in earlier Win headers */ #if _MSC_VER >= 1200 /* This file only exists in VC 6.0 or higher */ @@ -268,11 +275,11 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ file in their Makefile (other compilers are generally taken care of by distutils.) */ # if defined(_DEBUG) -# pragma comment(lib,"python310_d.lib") +# pragma comment(lib,"python311_d.lib") # elif defined(Py_LIMITED_API) # pragma comment(lib,"python3.lib") # else -# pragma comment(lib,"python310.lib") +# pragma comment(lib,"python311.lib") # endif /* _DEBUG */ # endif /* _MSC_VER */ # endif /* Py_BUILD_CORE */ @@ -353,20 +360,6 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ /* Fairly standard from here! */ -/* Define to 1 if you have the `copysign' function. */ -#define HAVE_COPYSIGN 1 - -/* Define to 1 if you have the `round' function. */ -#if _MSC_VER >= 1800 -#define HAVE_ROUND 1 -#endif - -/* Define to 1 if you have the `isinf' macro. */ -#define HAVE_DECL_ISINF 1 - -/* Define to 1 if you have the `isnan' function. */ -#define HAVE_DECL_ISNAN 1 - /* Define if on AIX 3. System headers sometimes define this. We just want to avoid a redefinition error message. */ @@ -483,6 +476,9 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ /* Use Python's own small-block memory-allocator. */ #define WITH_PYMALLOC 1 +/* Define if you want to compile in object freelists optimization */ +#define WITH_FREELISTS 1 + /* Define if you have clock. */ /* #define HAVE_CLOCK */ @@ -534,6 +530,9 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ /* Define if you have siginterrupt. */ /* #undef HAVE_SIGINTERRUPT */ +/* Define to 1 if you have the `shutdown' function. */ +#define HAVE_SHUTDOWN 1 + /* Define if you have symlink. */ /* #undef HAVE_SYMLINK */ @@ -546,6 +545,9 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ /* Define if you have times. */ /* #undef HAVE_TIMES */ +/* Define to 1 if you have the `umask' function. */ +#define HAVE_UMASK 1 + /* Define if you have uname. */ /* #undef HAVE_UNAME */ @@ -676,8 +678,28 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ /* Define to 1 if you have the `erfc' function. */ #define HAVE_ERFC 1 -/* Define if you have the 'inet_pton' function. */ +// netdb.h functions (provided by winsock.h) +#define HAVE_GETHOSTNAME 1 +#define HAVE_GETHOSTBYADDR 1 +#define HAVE_GETHOSTBYNAME 1 +#define HAVE_GETPROTOBYNAME 1 +#define HAVE_GETSERVBYNAME 1 +#define HAVE_GETSERVBYPORT 1 +// sys/socket.h functions (provided by winsock.h) #define HAVE_INET_PTON 1 +#define HAVE_INET_NTOA 1 +#define HAVE_ACCEPT 1 +#define HAVE_BIND 1 +#define HAVE_CONNECT 1 +#define HAVE_GETSOCKNAME 1 +#define HAVE_LISTEN 1 +#define HAVE_RECVFROM 1 +#define HAVE_SENDTO 1 +#define HAVE_SETSOCKOPT 1 +#define HAVE_SOCKET 1 + +/* Define to 1 if you have the `dup' function. */ +#define HAVE_DUP 1 /* framework name */ #define _PYTHONFRAMEWORK "" @@ -685,6 +707,4 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ /* Define if libssl has X509_VERIFY_PARAM_set1_host and related function */ #define HAVE_X509_VERIFY_PARAM_SET1_HOST 1 -#define PLATLIBDIR "lib" - #endif /* !Py_CONFIG_H */ diff --git a/src/external/windows/include/python/pyerrors.h b/src/external/windows/include/python/pyerrors.h index e39ee5a8..5ce49e48 100755 --- a/src/external/windows/include/python/pyerrors.h +++ b/src/external/windows/include/python/pyerrors.h @@ -18,6 +18,10 @@ PyAPI_FUNC(PyObject *) PyErr_Occurred(void); PyAPI_FUNC(void) PyErr_Clear(void); PyAPI_FUNC(void) PyErr_Fetch(PyObject **, PyObject **, PyObject **); PyAPI_FUNC(void) PyErr_Restore(PyObject *, PyObject *, PyObject *); +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030b0000 +PyAPI_FUNC(PyObject*) PyErr_GetHandledException(void); +PyAPI_FUNC(void) PyErr_SetHandledException(PyObject *); +#endif #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03030000 PyAPI_FUNC(void) PyErr_GetExcInfo(PyObject **, PyObject **, PyObject **); PyAPI_FUNC(void) PyErr_SetExcInfo(PyObject *, PyObject *, PyObject *); @@ -60,11 +64,14 @@ PyAPI_FUNC(const char *) PyExceptionClass_Name(PyObject *); #define PyExceptionInstance_Class(x) ((PyObject*)Py_TYPE(x)) +#define _PyBaseExceptionGroup_Check(x) \ + PyObject_TypeCheck(x, (PyTypeObject *)PyExc_BaseExceptionGroup) /* Predefined exceptions */ PyAPI_DATA(PyObject *) PyExc_BaseException; PyAPI_DATA(PyObject *) PyExc_Exception; +PyAPI_DATA(PyObject *) PyExc_BaseExceptionGroup; #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03050000 PyAPI_DATA(PyObject *) PyExc_StopAsyncIteration; #endif @@ -314,7 +321,7 @@ PyAPI_FUNC(int) PyOS_vsnprintf(char *str, size_t size, const char *format, va_l #ifndef Py_LIMITED_API # define Py_CPYTHON_ERRORS_H -# include "cpython/pyerrors.h" +# include "cpython/pyerrors.h" # undef Py_CPYTHON_ERRORS_H #endif diff --git a/src/external/windows/include/python/pyframe.h b/src/external/windows/include/python/pyframe.h index a2576902..9d9c1699 100755 --- a/src/external/windows/include/python/pyframe.h +++ b/src/external/windows/include/python/pyframe.h @@ -9,13 +9,17 @@ extern "C" { #endif -typedef struct _frame PyFrameObject; - /* Return the line of code the frame is currently executing. */ PyAPI_FUNC(int) PyFrame_GetLineNumber(PyFrameObject *); PyAPI_FUNC(PyCodeObject *) PyFrame_GetCode(PyFrameObject *frame); +#ifndef Py_LIMITED_API +# define Py_CPYTHON_PYFRAME_H +# include "cpython/pyframe.h" +# undef Py_CPYTHON_PYFRAME_H +#endif + #ifdef __cplusplus } #endif diff --git a/src/external/windows/include/python/pyhash.h b/src/external/windows/include/python/pyhash.h index 4878b75b..f47706a4 100755 --- a/src/external/windows/include/python/pyhash.h +++ b/src/external/windows/include/python/pyhash.h @@ -114,11 +114,10 @@ PyAPI_FUNC(PyHash_FuncDef*) PyHash_GetFuncDef(void); /* hash algorithm selection * - * The values for Py_HASH_SIPHASH24 and Py_HASH_FNV are hard-coded in the + * The values for Py_HASH_* are hard-coded in the * configure script. * - * - FNV is available on all platforms and architectures. - * - SIPHASH24 only works on platforms that don't require aligned memory for integers. + * - FNV and SIPHASH* are available on all platforms and architectures. * - With EXTERNAL embedders can provide an alternative implementation with:: * * PyHash_FuncDef PyHash_Func = {...}; @@ -128,10 +127,11 @@ PyAPI_FUNC(PyHash_FuncDef*) PyHash_GetFuncDef(void); #define Py_HASH_EXTERNAL 0 #define Py_HASH_SIPHASH24 1 #define Py_HASH_FNV 2 +#define Py_HASH_SIPHASH13 3 #ifndef Py_HASH_ALGORITHM # ifndef HAVE_ALIGNED_REQUIRED -# define Py_HASH_ALGORITHM Py_HASH_SIPHASH24 +# define Py_HASH_ALGORITHM Py_HASH_SIPHASH13 # else # define Py_HASH_ALGORITHM Py_HASH_FNV # endif /* uint64_t && uint32_t && aligned */ diff --git a/src/external/windows/include/python/pylifecycle.h b/src/external/windows/include/python/pylifecycle.h index 0601eb5e..b1bdcaf1 100755 --- a/src/external/windows/include/python/pylifecycle.h +++ b/src/external/windows/include/python/pylifecycle.h @@ -34,10 +34,10 @@ PyAPI_FUNC(int) Py_Main(int argc, wchar_t **argv); PyAPI_FUNC(int) Py_BytesMain(int argc, char **argv); /* In pathconfig.c */ -PyAPI_FUNC(void) Py_SetProgramName(const wchar_t *); +Py_DEPRECATED(3.11) PyAPI_FUNC(void) Py_SetProgramName(const wchar_t *); PyAPI_FUNC(wchar_t *) Py_GetProgramName(void); -PyAPI_FUNC(void) Py_SetPythonHome(const wchar_t *); +Py_DEPRECATED(3.11) PyAPI_FUNC(void) Py_SetPythonHome(const wchar_t *); PyAPI_FUNC(wchar_t *) Py_GetPythonHome(void); PyAPI_FUNC(wchar_t *) Py_GetProgramFullPath(void); @@ -45,7 +45,7 @@ PyAPI_FUNC(wchar_t *) Py_GetProgramFullPath(void); PyAPI_FUNC(wchar_t *) Py_GetPrefix(void); PyAPI_FUNC(wchar_t *) Py_GetExecPrefix(void); PyAPI_FUNC(wchar_t *) Py_GetPath(void); -PyAPI_FUNC(void) Py_SetPath(const wchar_t *); +Py_DEPRECATED(3.11) PyAPI_FUNC(void) Py_SetPath(const wchar_t *); #ifdef MS_WINDOWS int _Py_CheckPython3(void); #endif @@ -62,9 +62,13 @@ typedef void (*PyOS_sighandler_t)(int); PyAPI_FUNC(PyOS_sighandler_t) PyOS_getsig(int); PyAPI_FUNC(PyOS_sighandler_t) PyOS_setsig(int, PyOS_sighandler_t); +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030B0000 +PyAPI_DATA(const unsigned long) Py_Version; +#endif + #ifndef Py_LIMITED_API # define Py_CPYTHON_PYLIFECYCLE_H -# include "cpython/pylifecycle.h" +# include "cpython/pylifecycle.h" # undef Py_CPYTHON_PYLIFECYCLE_H #endif diff --git a/src/external/windows/include/python/pymacro.h b/src/external/windows/include/python/pymacro.h index 7bfee70e..c7b79218 100755 --- a/src/external/windows/include/python/pymacro.h +++ b/src/external/windows/include/python/pymacro.h @@ -1,6 +1,25 @@ #ifndef Py_PYMACRO_H #define Py_PYMACRO_H +// gh-91782: On FreeBSD 12, if the _POSIX_C_SOURCE and _XOPEN_SOURCE macros are +// defined, disables C11 support and does not define +// the static_assert() macro. Define the static_assert() macro in Python until +// suports C11: +// https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=255290 +#if defined(__FreeBSD__) && !defined(static_assert) +# define static_assert _Static_assert +#endif + +// static_assert is defined in glibc from version 2.16. Before it requires +// compiler support (gcc >= 4.6) and is called _Static_assert. +// In C++ 11 static_assert is a keyword, redefining is undefined behaviour. +#if (defined(__GLIBC__) \ + && (__GLIBC__ < 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ <= 16)) \ + && !(defined(__cplusplus) && __cplusplus >= 201103L) \ + && !defined(static_assert)) +# define static_assert _Static_assert +#endif + /* Minimum value between x and y */ #define Py_MIN(x, y) (((x) > (y)) ? (y) : (x)) @@ -129,4 +148,8 @@ Py_FatalError("Unreachable C code path reached") #endif +// Prevent using an expression as a l-value. +// For example, "int x; _Py_RVALUE(x) = 1;" fails with a compiler error. +#define _Py_RVALUE(EXPR) ((void)0, (EXPR)) + #endif /* Py_PYMACRO_H */ diff --git a/src/external/windows/include/python/pymath.h b/src/external/windows/include/python/pymath.h index abe3e9c3..b0ec907d 100755 --- a/src/external/windows/include/python/pymath.h +++ b/src/external/windows/include/python/pymath.h @@ -1,42 +1,9 @@ +// Symbols and macros to supply platform-independent interfaces to mathematical +// functions and constants. + #ifndef Py_PYMATH_H #define Py_PYMATH_H -#include "pyconfig.h" /* include for defines */ - -/************************************************************************** -Symbols and macros to supply platform-independent interfaces to mathematical -functions and constants -**************************************************************************/ - -/* Python provides implementations for copysign, round and hypot in - * Python/pymath.c just in case your math library doesn't provide the - * functions. - * - *Note: PC/pyconfig.h defines copysign as _copysign - */ -#ifndef HAVE_COPYSIGN -extern double copysign(double, double); -#endif - -#ifndef HAVE_ROUND -extern double round(double); -#endif - -#ifndef HAVE_HYPOT -extern double hypot(double, double); -#endif - -/* extra declarations */ -#ifndef _MSC_VER -#ifndef __STDC__ -extern double fmod (double, double); -extern double frexp (double, int *); -extern double ldexp (double, int); -extern double modf (double, double *); -extern double pow(double, double); -#endif /* __STDC__ */ -#endif /* _MSC_VER */ - /* High precision definition of pi and e (Euler) * The values are taken from libc6's math.h. */ @@ -60,84 +27,17 @@ extern double pow(double, double); #define Py_MATH_TAU 6.2831853071795864769252867665590057683943L #endif - -/* On x86, Py_FORCE_DOUBLE forces a floating-point number out of an x87 FPU - register and into a 64-bit memory location, rounding from extended - precision to double precision in the process. On other platforms it does - nothing. */ - -/* we take double rounding as evidence of x87 usage */ -#ifndef Py_LIMITED_API -#ifndef Py_FORCE_DOUBLE -# ifdef X87_DOUBLE_ROUNDING -PyAPI_FUNC(double) _Py_force_double(double); -# define Py_FORCE_DOUBLE(X) (_Py_force_double(X)) -# else -# define Py_FORCE_DOUBLE(X) (X) -# endif -#endif -#endif - -#ifndef Py_LIMITED_API -#ifdef HAVE_GCC_ASM_FOR_X87 -PyAPI_FUNC(unsigned short) _Py_get_387controlword(void); -PyAPI_FUNC(void) _Py_set_387controlword(unsigned short); -#endif -#endif - -/* Py_IS_NAN(X) - * Return 1 if float or double arg is a NaN, else 0. - * Caution: - * X is evaluated more than once. - * This may not work on all platforms. Each platform has *some* - * way to spell this, though -- override in pyconfig.h if you have - * a platform where it doesn't work. - * Note: PC/pyconfig.h defines Py_IS_NAN as _isnan - */ -#ifndef Py_IS_NAN -#if defined HAVE_DECL_ISNAN && HAVE_DECL_ISNAN == 1 +// Py_IS_NAN(X) +// Return 1 if float or double arg is a NaN, else 0. #define Py_IS_NAN(X) isnan(X) -#else -#define Py_IS_NAN(X) ((X) != (X)) -#endif -#endif -/* Py_IS_INFINITY(X) - * Return 1 if float or double arg is an infinity, else 0. - * Caution: - * X is evaluated more than once. - * This implementation may set the underflow flag if |X| is very small; - * it really can't be implemented correctly (& easily) before C99. - * Override in pyconfig.h if you have a better spelling on your platform. - * Py_FORCE_DOUBLE is used to avoid getting false negatives from a - * non-infinite value v sitting in an 80-bit x87 register such that - * v becomes infinite when spilled from the register to 64-bit memory. - * Note: PC/pyconfig.h defines Py_IS_INFINITY as _isinf - */ -#ifndef Py_IS_INFINITY -# if defined HAVE_DECL_ISINF && HAVE_DECL_ISINF == 1 -# define Py_IS_INFINITY(X) isinf(X) -# else -# define Py_IS_INFINITY(X) ((X) && \ - (Py_FORCE_DOUBLE(X)*0.5 == Py_FORCE_DOUBLE(X))) -# endif -#endif +// Py_IS_INFINITY(X) +// Return 1 if float or double arg is an infinity, else 0. +#define Py_IS_INFINITY(X) isinf(X) -/* Py_IS_FINITE(X) - * Return 1 if float or double arg is neither infinite nor NAN, else 0. - * Some compilers (e.g. VisualStudio) have intrinsics for this, so a special - * macro for this particular test is useful - * Note: PC/pyconfig.h defines Py_IS_FINITE as _finite - */ -#ifndef Py_IS_FINITE -#if defined HAVE_DECL_ISFINITE && HAVE_DECL_ISFINITE == 1 +// Py_IS_FINITE(X) +// Return 1 if float or double arg is neither infinite nor NAN, else 0. #define Py_IS_FINITE(X) isfinite(X) -#elif defined HAVE_FINITE -#define Py_IS_FINITE(X) finite(X) -#else -#define Py_IS_FINITE(X) (!Py_IS_INFINITY(X) && !Py_IS_NAN(X)) -#endif -#endif /* HUGE_VAL is supposed to expand to a positive double infinity. Python * uses Py_HUGE_VAL instead because some platforms are broken in this @@ -147,84 +47,19 @@ PyAPI_FUNC(void) _Py_set_387controlword(unsigned short); * config to #define Py_HUGE_VAL to something that works on your platform. */ #ifndef Py_HUGE_VAL -#define Py_HUGE_VAL HUGE_VAL +# define Py_HUGE_VAL HUGE_VAL #endif -/* Py_NAN - * A value that evaluates to a NaN. On IEEE 754 platforms INF*0 or - * INF/INF works. Define Py_NO_NAN in pyconfig.h if your platform - * doesn't support NaNs. - */ -#if !defined(Py_NAN) && !defined(Py_NO_NAN) -#if !defined(__INTEL_COMPILER) - #define Py_NAN (Py_HUGE_VAL * 0.) -#else /* __INTEL_COMPILER */ - #if defined(ICC_NAN_STRICT) - #pragma float_control(push) - #pragma float_control(precise, on) - #pragma float_control(except, on) - #if defined(_MSC_VER) - __declspec(noinline) - #else /* Linux */ - __attribute__((noinline)) - #endif /* _MSC_VER */ - static double __icc_nan() - { - return sqrt(-1.0); - } - #pragma float_control (pop) - #define Py_NAN __icc_nan() - #else /* ICC_NAN_RELAXED as default for Intel Compiler */ - static const union { unsigned char buf[8]; double __icc_nan; } __nan_store = {0,0,0,0,0,0,0xf8,0x7f}; - #define Py_NAN (__nan_store.__icc_nan) - #endif /* ICC_NAN_STRICT */ -#endif /* __INTEL_COMPILER */ -#endif - -/* Py_OVERFLOWED(X) - * Return 1 iff a libm function overflowed. Set errno to 0 before calling - * a libm function, and invoke this macro after, passing the function - * result. - * Caution: - * This isn't reliable. C99 no longer requires libm to set errno under - * any exceptional condition, but does require +- HUGE_VAL return - * values on overflow. A 754 box *probably* maps HUGE_VAL to a - * double infinity, and we're cool if that's so, unless the input - * was an infinity and an infinity is the expected result. A C89 - * system sets errno to ERANGE, so we check for that too. We're - * out of luck if a C99 754 box doesn't map HUGE_VAL to +Inf, or - * if the returned result is a NaN, or if a C89 box returns HUGE_VAL - * in non-overflow cases. - * X is evaluated more than once. - * Some platforms have better way to spell this, so expect some #ifdef'ery. - * - * OpenBSD uses 'isinf()' because a compiler bug on that platform causes - * the longer macro version to be mis-compiled. This isn't optimal, and - * should be removed once a newer compiler is available on that platform. - * The system that had the failure was running OpenBSD 3.2 on Intel, with - * gcc 2.95.3. - * - * According to Tim's checkin, the FreeBSD systems use isinf() to work - * around a FPE bug on that platform. - */ -#if defined(__FreeBSD__) || defined(__OpenBSD__) -#define Py_OVERFLOWED(X) isinf(X) +// Py_NAN: Value that evaluates to a quiet Not-a-Number (NaN). +#if !defined(Py_NAN) +# if _Py__has_builtin(__builtin_nan) + // Built-in implementation of the ISO C99 function nan(): quiet NaN. +# define Py_NAN (__builtin_nan("")) #else -#define Py_OVERFLOWED(X) ((X) != 0.0 && (errno == ERANGE || \ - (X) == Py_HUGE_VAL || \ - (X) == -Py_HUGE_VAL)) + // Use C99 NAN constant: quiet Not-A-Number. + // NAN is a float, Py_NAN is a double: cast to double. +# define Py_NAN ((double)NAN) +# endif #endif -/* Return whether integral type *type* is signed or not. */ -#define _Py_IntegralTypeSigned(type) ((type)(-1) < 0) -/* Return the maximum value of integral type *type*. */ -#define _Py_IntegralTypeMax(type) ((_Py_IntegralTypeSigned(type)) ? (((((type)1 << (sizeof(type)*CHAR_BIT - 2)) - 1) << 1) + 1) : ~(type)0) -/* Return the minimum value of integral type *type*. */ -#define _Py_IntegralTypeMin(type) ((_Py_IntegralTypeSigned(type)) ? -_Py_IntegralTypeMax(type) - 1 : 0) -/* Check whether *v* is in the range of integral type *type*. This is most - * useful if *v* is floating-point, since demoting a floating-point *v* to an - * integral type that cannot represent *v*'s integral part is undefined - * behavior. */ -#define _Py_InIntegralTypeRange(type, v) (_Py_IntegralTypeMin(type) <= v && v <= _Py_IntegralTypeMax(type)) - #endif /* Py_PYMATH_H */ diff --git a/src/external/windows/include/python/pymem.h b/src/external/windows/include/python/pymem.h index a7c88086..10b2538d 100755 --- a/src/external/windows/include/python/pymem.h +++ b/src/external/windows/include/python/pymem.h @@ -93,7 +93,7 @@ PyAPI_FUNC(void) PyMem_Free(void *ptr); #ifndef Py_LIMITED_API # define Py_CPYTHON_PYMEM_H -# include "cpython/pymem.h" +# include "cpython/pymem.h" # undef Py_CPYTHON_PYMEM_H #endif diff --git a/src/external/windows/include/python/pyport.h b/src/external/windows/include/python/pyport.h index f17729bc..2bd3d64e 100755 --- a/src/external/windows/include/python/pyport.h +++ b/src/external/windows/include/python/pyport.h @@ -5,6 +5,33 @@ #include +#include +#ifndef UCHAR_MAX +# error "limits.h must define UCHAR_MAX" +#endif +#if UCHAR_MAX != 255 +# error "Python's source code assumes C's unsigned char is an 8-bit type" +#endif + + +// Macro to use C++ static_cast<> in the Python C API. +#ifdef __cplusplus +# define _Py_STATIC_CAST(type, expr) static_cast(expr) +#else +# define _Py_STATIC_CAST(type, expr) ((type)(expr)) +#endif +// Macro to use the more powerful/dangerous C-style cast even in C++. +#define _Py_CAST(type, expr) ((type)(expr)) + +// Static inline functions should use _Py_NULL rather than using directly NULL +// to prevent C++ compiler warnings. On C++11 and newer, _Py_NULL is defined as +// nullptr. +#if defined(__cplusplus) && __cplusplus >= 201103 +# define _Py_NULL nullptr +#else +# define _Py_NULL NULL +#endif + /* Defines to build Python and its standard library: * @@ -77,16 +104,12 @@ Used in: Py_SAFE_DOWNCAST #define PY_INT32_T int32_t #define PY_INT64_T int64_t -/* If PYLONG_BITS_IN_DIGIT is not defined then we'll use 30-bit digits if all - the necessary integer types are available, and we're on a 64-bit platform - (as determined by SIZEOF_VOID_P); otherwise we use 15-bit digits. */ - +/* PYLONG_BITS_IN_DIGIT describes the number of bits per "digit" (limb) in the + * PyLongObject implementation (longintrepr.h). It's currently either 30 or 15, + * defaulting to 30. The 15-bit digit option may be removed in the future. + */ #ifndef PYLONG_BITS_IN_DIGIT -#if SIZEOF_VOID_P >= 8 #define PYLONG_BITS_IN_DIGIT 30 -#else -#define PYLONG_BITS_IN_DIGIT 15 -#endif #endif /* uintptr_t is the C9X name for an unsigned integral type such that a @@ -100,17 +123,23 @@ typedef intptr_t Py_intptr_t; /* Py_ssize_t is a signed integral type such that sizeof(Py_ssize_t) == * sizeof(size_t). C99 doesn't define such a thing directly (size_t is an * unsigned integral type). See PEP 353 for details. + * PY_SSIZE_T_MAX is the largest positive value of type Py_ssize_t. */ #ifdef HAVE_PY_SSIZE_T #elif HAVE_SSIZE_T typedef ssize_t Py_ssize_t; +# define PY_SSIZE_T_MAX SSIZE_MAX #elif SIZEOF_VOID_P == SIZEOF_SIZE_T typedef Py_intptr_t Py_ssize_t; +# define PY_SSIZE_T_MAX INTPTR_MAX #else # error "Python needs a typedef for Py_ssize_t in pyport.h." #endif +/* Smallest negative value of type Py_ssize_t. */ +#define PY_SSIZE_T_MIN (-PY_SSIZE_T_MAX-1) + /* Py_hash_t is the same size as a pointer. */ #define SIZEOF_PY_HASH_T SIZEOF_SIZE_T typedef Py_ssize_t Py_hash_t; @@ -118,21 +147,12 @@ typedef Py_ssize_t Py_hash_t; #define SIZEOF_PY_UHASH_T SIZEOF_SIZE_T typedef size_t Py_uhash_t; -/* Only used for compatibility with code that may not be PY_SSIZE_T_CLEAN. */ -#ifdef PY_SSIZE_T_CLEAN +/* Now PY_SSIZE_T_CLEAN is mandatory. This is just for backward compatibility. */ typedef Py_ssize_t Py_ssize_clean_t; -#else -typedef int Py_ssize_clean_t; -#endif /* Largest possible value of size_t. */ #define PY_SIZE_MAX SIZE_MAX -/* Largest positive value of type Py_ssize_t. */ -#define PY_SSIZE_T_MAX ((Py_ssize_t)(((size_t)-1)>>1)) -/* Smallest negative value of type Py_ssize_t. */ -#define PY_SSIZE_T_MIN (-PY_SSIZE_T_MAX-1) - /* Macro kept for backward compatibility: use "z" in new code. * * PY_FORMAT_SIZE_T is a platform-specific modifier for use in a printf @@ -170,23 +190,12 @@ typedef int Py_ssize_clean_t; * Py_LOCAL_INLINE does the same thing, and also explicitly requests inlining, * for platforms that support that. * - * If PY_LOCAL_AGGRESSIVE is defined before python.h is included, more - * "aggressive" inlining/optimization is enabled for the entire module. This - * may lead to code bloat, and may slow things down for those reasons. It may - * also lead to errors, if the code relies on pointer aliasing. Use with - * care. - * * NOTE: You can only use this for functions that are entirely local to a * module; functions that are exported via method tables, callbacks, etc, * should keep using static. */ #if defined(_MSC_VER) -# if defined(PY_LOCAL_AGGRESSIVE) - /* enable more aggressive optimization for MSVC */ - /* active in both release and debug builds - see bpo-43271 */ -# pragma optimize("gt", on) -#endif /* ignore warnings if the compiler decides not to inline a function */ # pragma warning(disable: 4710) /* fastest possible local call under MSVC */ @@ -197,11 +206,10 @@ typedef int Py_ssize_clean_t; # define Py_LOCAL_INLINE(type) static inline type #endif -/* Py_MEMCPY is kept for backwards compatibility, - * see https://bugs.python.org/issue28126 */ -#define Py_MEMCPY memcpy - -#include +// bpo-28126: Py_MEMCPY is kept for backwards compatibility, +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 +# define Py_MEMCPY memcpy +#endif #ifdef HAVE_IEEEFP_H #include /* needed for 'finite' declaration on some platforms */ @@ -213,17 +221,10 @@ typedef int Py_ssize_clean_t; * WRAPPER FOR and/or * ********************************************/ -#ifdef TIME_WITH_SYS_TIME -#include -#include -#else /* !TIME_WITH_SYS_TIME */ #ifdef HAVE_SYS_TIME_H #include -#else /* !HAVE_SYS_TIME_H */ +#endif #include -#endif /* !HAVE_SYS_TIME_H */ -#endif /* !TIME_WITH_SYS_TIME */ - /****************************** * WRAPPER FOR * @@ -314,189 +315,11 @@ extern "C" { * VALUE may be evaluated more than once. */ #ifdef Py_DEBUG -#define Py_SAFE_DOWNCAST(VALUE, WIDE, NARROW) \ - (assert((WIDE)(NARROW)(VALUE) == (VALUE)), (NARROW)(VALUE)) +# define Py_SAFE_DOWNCAST(VALUE, WIDE, NARROW) \ + (assert(_Py_STATIC_CAST(WIDE, _Py_STATIC_CAST(NARROW, (VALUE))) == (VALUE)), \ + _Py_STATIC_CAST(NARROW, (VALUE))) #else -#define Py_SAFE_DOWNCAST(VALUE, WIDE, NARROW) (NARROW)(VALUE) -#endif - -/* Py_SET_ERRNO_ON_MATH_ERROR(x) - * If a libm function did not set errno, but it looks like the result - * overflowed or not-a-number, set errno to ERANGE or EDOM. Set errno - * to 0 before calling a libm function, and invoke this macro after, - * passing the function result. - * Caution: - * This isn't reliable. See Py_OVERFLOWED comments. - * X is evaluated more than once. - */ -#if defined(__FreeBSD__) || defined(__OpenBSD__) || (defined(__hpux) && defined(__ia64)) -#define _Py_SET_EDOM_FOR_NAN(X) if (isnan(X)) errno = EDOM; -#else -#define _Py_SET_EDOM_FOR_NAN(X) ; -#endif -#define Py_SET_ERRNO_ON_MATH_ERROR(X) \ - do { \ - if (errno == 0) { \ - if ((X) == Py_HUGE_VAL || (X) == -Py_HUGE_VAL) \ - errno = ERANGE; \ - else _Py_SET_EDOM_FOR_NAN(X) \ - } \ - } while(0) - -/* Py_SET_ERANGE_IF_OVERFLOW(x) - * An alias of Py_SET_ERRNO_ON_MATH_ERROR for backward-compatibility. - */ -#define Py_SET_ERANGE_IF_OVERFLOW(X) Py_SET_ERRNO_ON_MATH_ERROR(X) - -/* Py_ADJUST_ERANGE1(x) - * Py_ADJUST_ERANGE2(x, y) - * Set errno to 0 before calling a libm function, and invoke one of these - * macros after, passing the function result(s) (Py_ADJUST_ERANGE2 is useful - * for functions returning complex results). This makes two kinds of - * adjustments to errno: (A) If it looks like the platform libm set - * errno=ERANGE due to underflow, clear errno. (B) If it looks like the - * platform libm overflowed but didn't set errno, force errno to ERANGE. In - * effect, we're trying to force a useful implementation of C89 errno - * behavior. - * Caution: - * This isn't reliable. See Py_OVERFLOWED comments. - * X and Y may be evaluated more than once. - */ -#define Py_ADJUST_ERANGE1(X) \ - do { \ - if (errno == 0) { \ - if ((X) == Py_HUGE_VAL || (X) == -Py_HUGE_VAL) \ - errno = ERANGE; \ - } \ - else if (errno == ERANGE && (X) == 0.0) \ - errno = 0; \ - } while(0) - -#define Py_ADJUST_ERANGE2(X, Y) \ - do { \ - if ((X) == Py_HUGE_VAL || (X) == -Py_HUGE_VAL || \ - (Y) == Py_HUGE_VAL || (Y) == -Py_HUGE_VAL) { \ - if (errno == 0) \ - errno = ERANGE; \ - } \ - else if (errno == ERANGE) \ - errno = 0; \ - } while(0) - -/* The functions _Py_dg_strtod and _Py_dg_dtoa in Python/dtoa.c (which are - * required to support the short float repr introduced in Python 3.1) require - * that the floating-point unit that's being used for arithmetic operations - * on C doubles is set to use 53-bit precision. It also requires that the - * FPU rounding mode is round-half-to-even, but that's less often an issue. - * - * If your FPU isn't already set to 53-bit precision/round-half-to-even, and - * you want to make use of _Py_dg_strtod and _Py_dg_dtoa, then you should - * - * #define HAVE_PY_SET_53BIT_PRECISION 1 - * - * and also give appropriate definitions for the following three macros: - * - * _PY_SET_53BIT_PRECISION_START : store original FPU settings, and - * set FPU to 53-bit precision/round-half-to-even - * _PY_SET_53BIT_PRECISION_END : restore original FPU settings - * _PY_SET_53BIT_PRECISION_HEADER : any variable declarations needed to - * use the two macros above. - * - * The macros are designed to be used within a single C function: see - * Python/pystrtod.c for an example of their use. - */ - -/* get and set x87 control word for gcc/x86 */ -#ifdef HAVE_GCC_ASM_FOR_X87 -#define HAVE_PY_SET_53BIT_PRECISION 1 -/* _Py_get/set_387controlword functions are defined in Python/pymath.c */ -#define _Py_SET_53BIT_PRECISION_HEADER \ - unsigned short old_387controlword, new_387controlword -#define _Py_SET_53BIT_PRECISION_START \ - do { \ - old_387controlword = _Py_get_387controlword(); \ - new_387controlword = (old_387controlword & ~0x0f00) | 0x0200; \ - if (new_387controlword != old_387controlword) \ - _Py_set_387controlword(new_387controlword); \ - } while (0) -#define _Py_SET_53BIT_PRECISION_END \ - if (new_387controlword != old_387controlword) \ - _Py_set_387controlword(old_387controlword) -#endif - -/* get and set x87 control word for VisualStudio/x86 */ -#if defined(_MSC_VER) && !defined(_WIN64) && !defined(_M_ARM) /* x87 not supported in 64-bit or ARM */ -#define HAVE_PY_SET_53BIT_PRECISION 1 -#define _Py_SET_53BIT_PRECISION_HEADER \ - unsigned int old_387controlword, new_387controlword, out_387controlword -/* We use the __control87_2 function to set only the x87 control word. - The SSE control word is unaffected. */ -#define _Py_SET_53BIT_PRECISION_START \ - do { \ - __control87_2(0, 0, &old_387controlword, NULL); \ - new_387controlword = \ - (old_387controlword & ~(_MCW_PC | _MCW_RC)) | (_PC_53 | _RC_NEAR); \ - if (new_387controlword != old_387controlword) \ - __control87_2(new_387controlword, _MCW_PC | _MCW_RC, \ - &out_387controlword, NULL); \ - } while (0) -#define _Py_SET_53BIT_PRECISION_END \ - do { \ - if (new_387controlword != old_387controlword) \ - __control87_2(old_387controlword, _MCW_PC | _MCW_RC, \ - &out_387controlword, NULL); \ - } while (0) -#endif - -#ifdef HAVE_GCC_ASM_FOR_MC68881 -#define HAVE_PY_SET_53BIT_PRECISION 1 -#define _Py_SET_53BIT_PRECISION_HEADER \ - unsigned int old_fpcr, new_fpcr -#define _Py_SET_53BIT_PRECISION_START \ - do { \ - __asm__ ("fmove.l %%fpcr,%0" : "=g" (old_fpcr)); \ - /* Set double precision / round to nearest. */ \ - new_fpcr = (old_fpcr & ~0xf0) | 0x80; \ - if (new_fpcr != old_fpcr) \ - __asm__ volatile ("fmove.l %0,%%fpcr" : : "g" (new_fpcr)); \ - } while (0) -#define _Py_SET_53BIT_PRECISION_END \ - do { \ - if (new_fpcr != old_fpcr) \ - __asm__ volatile ("fmove.l %0,%%fpcr" : : "g" (old_fpcr)); \ - } while (0) -#endif - -/* default definitions are empty */ -#ifndef HAVE_PY_SET_53BIT_PRECISION -#define _Py_SET_53BIT_PRECISION_HEADER -#define _Py_SET_53BIT_PRECISION_START -#define _Py_SET_53BIT_PRECISION_END -#endif - -/* If we can't guarantee 53-bit precision, don't use the code - in Python/dtoa.c, but fall back to standard code. This - means that repr of a float will be long (17 sig digits). - - Realistically, there are two things that could go wrong: - - (1) doubles aren't IEEE 754 doubles, or - (2) we're on x86 with the rounding precision set to 64-bits - (extended precision), and we don't know how to change - the rounding precision. - */ - -#if !defined(DOUBLE_IS_LITTLE_ENDIAN_IEEE754) && \ - !defined(DOUBLE_IS_BIG_ENDIAN_IEEE754) && \ - !defined(DOUBLE_IS_ARM_MIXED_ENDIAN_IEEE754) -#define PY_NO_SHORT_FLOAT_REPR -#endif - -/* double rounding is symptomatic of use of extended precision on x86. If - we're seeing double rounding, and we don't have any mechanism available for - changing the FPU rounding precision, then don't use Python/dtoa.c. */ -#if defined(X87_DOUBLE_ROUNDING) && !defined(HAVE_PY_SET_53BIT_PRECISION) -#define PY_NO_SHORT_FLOAT_REPR +# define Py_SAFE_DOWNCAST(VALUE, WIDE, NARROW) _Py_STATIC_CAST(NARROW, (VALUE)) #endif @@ -561,19 +384,51 @@ extern "C" { #define _Py_HOT_FUNCTION #endif -/* _Py_NO_INLINE - * Disable inlining on a function. For example, it helps to reduce the C stack - * consumption. - * - * Usage: - * int _Py_NO_INLINE x(void) { return 3; } - */ -#if defined(_MSC_VER) -# define _Py_NO_INLINE __declspec(noinline) -#elif defined(__GNUC__) || defined(__clang__) -# define _Py_NO_INLINE __attribute__ ((noinline)) +// Ask the compiler to always inline a static inline function. The compiler can +// ignore it and decides to not inline the function. +// +// It can be used to inline performance critical static inline functions when +// building Python in debug mode with function inlining disabled. For example, +// MSC disables function inlining when building in debug mode. +// +// Marking blindly a static inline function with Py_ALWAYS_INLINE can result in +// worse performances (due to increased code size for example). The compiler is +// usually smarter than the developer for the cost/benefit analysis. +// +// If Python is built in debug mode (if the Py_DEBUG macro is defined), the +// Py_ALWAYS_INLINE macro does nothing. +// +// It must be specified before the function return type. Usage: +// +// static inline Py_ALWAYS_INLINE int random(void) { return 4; } +#if defined(Py_DEBUG) + // If Python is built in debug mode, usually compiler optimizations are + // disabled. In this case, Py_ALWAYS_INLINE can increase a lot the stack + // memory usage. For example, forcing inlining using gcc -O0 increases the + // stack usage from 6 KB to 15 KB per Python function call. +# define Py_ALWAYS_INLINE +#elif defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER) +# define Py_ALWAYS_INLINE __attribute__((always_inline)) +#elif defined(_MSC_VER) +# define Py_ALWAYS_INLINE __forceinline #else -# define _Py_NO_INLINE +# define Py_ALWAYS_INLINE +#endif + +// Py_NO_INLINE +// Disable inlining on a function. For example, it reduces the C stack +// consumption: useful on LTO+PGO builds which heavily inline code (see +// bpo-33720). +// +// Usage: +// +// Py_NO_INLINE static int random(void) { return 4; } +#if defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER) +# define Py_NO_INLINE __attribute__ ((noinline)) +#elif defined(_MSC_VER) +# define Py_NO_INLINE __declspec(noinline) +#else +# define Py_NO_INLINE #endif /************************************************************************** @@ -800,26 +655,6 @@ extern char * _getpty(int *, int, mode_t, int); # define PY_LITTLE_ENDIAN 1 #endif -#ifdef Py_BUILD_CORE -/* - * Macros to protect CRT calls against instant termination when passed an - * invalid parameter (issue23524). - */ -#if defined _MSC_VER && _MSC_VER >= 1900 - -extern _invalid_parameter_handler _Py_silent_invalid_parameter_handler; -#define _Py_BEGIN_SUPPRESS_IPH { _invalid_parameter_handler _Py_old_handler = \ - _set_thread_local_invalid_parameter_handler(_Py_silent_invalid_parameter_handler); -#define _Py_END_SUPPRESS_IPH _set_thread_local_invalid_parameter_handler(_Py_old_handler); } - -#else - -#define _Py_BEGIN_SUPPRESS_IPH -#define _Py_END_SUPPRESS_IPH - -#endif /* _MSC_VER >= 1900 */ -#endif /* Py_BUILD_CORE */ - #ifdef __ANDROID__ /* The Android langinfo.h header is not used. */ # undef HAVE_LANGINFO_H @@ -886,4 +721,22 @@ extern _invalid_parameter_handler _Py_silent_invalid_parameter_handler; #endif +/* A convenient way for code to know if sanitizers are enabled. */ +#if defined(__has_feature) +# if __has_feature(memory_sanitizer) +# if !defined(_Py_MEMORY_SANITIZER) +# define _Py_MEMORY_SANITIZER +# endif +# endif +# if __has_feature(address_sanitizer) +# if !defined(_Py_ADDRESS_SANITIZER) +# define _Py_ADDRESS_SANITIZER +# endif +# endif +#elif defined(__GNUC__) +# if defined(__SANITIZE_ADDRESS__) +# define _Py_ADDRESS_SANITIZER +# endif +#endif + #endif /* Py_PYPORT_H */ diff --git a/src/external/windows/include/python/pystate.h b/src/external/windows/include/python/pystate.h index d9f31e36..1af98ba5 100755 --- a/src/external/windows/include/python/pystate.h +++ b/src/external/windows/include/python/pystate.h @@ -11,16 +11,6 @@ extern "C" { removed (with effort). */ #define MAX_CO_EXTRA_USERS 255 -/* Forward declarations for PyFrameObject, PyThreadState - and PyInterpreterState */ -struct _ts; -struct _is; - -/* struct _ts is defined in cpython/pystate.h */ -typedef struct _ts PyThreadState; -/* struct _is is defined in internal/pycore_interp.h */ -typedef struct _is PyInterpreterState; - PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_New(void); PyAPI_FUNC(void) PyInterpreterState_Clear(PyInterpreterState *); PyAPI_FUNC(void) PyInterpreterState_Delete(PyInterpreterState *); @@ -50,10 +40,10 @@ PyAPI_FUNC(int64_t) PyInterpreterState_GetID(PyInterpreterState *); /* State unique per thread */ /* New in 3.3 */ -PyAPI_FUNC(int) PyState_AddModule(PyObject*, struct PyModuleDef*); -PyAPI_FUNC(int) PyState_RemoveModule(struct PyModuleDef*); +PyAPI_FUNC(int) PyState_AddModule(PyObject*, PyModuleDef*); +PyAPI_FUNC(int) PyState_RemoveModule(PyModuleDef*); #endif -PyAPI_FUNC(PyObject*) PyState_FindModule(struct PyModuleDef*); +PyAPI_FUNC(PyObject*) PyState_FindModule(PyModuleDef*); PyAPI_FUNC(PyThreadState *) PyThreadState_New(PyInterpreterState *); PyAPI_FUNC(void) PyThreadState_Clear(PyThreadState *); @@ -66,18 +56,10 @@ PyAPI_FUNC(void) PyThreadState_Delete(PyThreadState *); The caller must hold the GIL. - See also PyThreadState_GET() and _PyThreadState_GET(). */ + See also _PyThreadState_UncheckedGet() and _PyThreadState_GET(). */ PyAPI_FUNC(PyThreadState *) PyThreadState_Get(void); -/* Get the current Python thread state. - - Macro using PyThreadState_Get() or _PyThreadState_GET() depending if - pycore_pystate.h is included or not (this header redefines the macro). - - If PyThreadState_Get() is used, issue a fatal error if the current thread - state is NULL. - - See also PyThreadState_Get() and _PyThreadState_GET(). */ +// Alias to PyThreadState_Get() #define PyThreadState_GET() PyThreadState_Get() PyAPI_FUNC(PyThreadState *) PyThreadState_Swap(PyThreadState *); @@ -140,7 +122,7 @@ PyAPI_FUNC(PyThreadState *) PyGILState_GetThisThreadState(void); #ifndef Py_LIMITED_API # define Py_CPYTHON_PYSTATE_H -# include "cpython/pystate.h" +# include "cpython/pystate.h" # undef Py_CPYTHON_PYSTATE_H #endif diff --git a/src/external/windows/include/python/pystrhex.h b/src/external/windows/include/python/pystrhex.h deleted file mode 100755 index bccae77a..00000000 --- a/src/external/windows/include/python/pystrhex.h +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef Py_STRHEX_H -#define Py_STRHEX_H - -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef Py_LIMITED_API -/* Returns a str() containing the hex representation of argbuf. */ -PyAPI_FUNC(PyObject*) _Py_strhex(const char* argbuf, const Py_ssize_t arglen); -/* Returns a bytes() containing the ASCII hex representation of argbuf. */ -PyAPI_FUNC(PyObject*) _Py_strhex_bytes(const char* argbuf, const Py_ssize_t arglen); -/* These variants include support for a separator between every N bytes: */ -PyAPI_FUNC(PyObject*) _Py_strhex_with_sep(const char* argbuf, const Py_ssize_t arglen, const PyObject* sep, const int bytes_per_group); -PyAPI_FUNC(PyObject*) _Py_strhex_bytes_with_sep(const char* argbuf, const Py_ssize_t arglen, const PyObject* sep, const int bytes_per_group); -#endif /* !Py_LIMITED_API */ - -#ifdef __cplusplus -} -#endif - -#endif /* !Py_STRHEX_H */ diff --git a/src/external/windows/include/python/pystrtod.h b/src/external/windows/include/python/pystrtod.h index ba2e8d00..52c292df 100755 --- a/src/external/windows/include/python/pystrtod.h +++ b/src/external/windows/include/python/pystrtod.h @@ -32,6 +32,7 @@ PyAPI_FUNC(double) _Py_parse_inf_or_nan(const char *p, char **endptr); #define Py_DTSF_ADD_DOT_0 0x02 /* if the result is an integer add ".0" */ #define Py_DTSF_ALT 0x04 /* "alternate" formatting. it's format_code specific */ +#define Py_DTSF_NO_NEG_0 0x08 /* negative zero result is coerced to 0 */ /* PyOS_double_to_string's "type", if non-NULL, will be set to one of: */ #define Py_DTST_FINITE 0 diff --git a/src/external/windows/include/python/pythonrun.h b/src/external/windows/include/python/pythonrun.h index bade2cc3..0a896db4 100755 --- a/src/external/windows/include/python/pythonrun.h +++ b/src/external/windows/include/python/pythonrun.h @@ -24,6 +24,7 @@ PyAPI_DATA(int) (*PyOS_InputHook)(void); #if defined(WIN32) && !defined(MS_WIN64) && !defined(_M_ARM) && defined(_MSC_VER) && _MSC_VER >= 1300 /* Enable stack checking under Microsoft C */ +// When changing the platforms, ensure PyOS_CheckStack() docs are still correct #define USE_STACKCHECK #endif @@ -34,7 +35,7 @@ PyAPI_FUNC(int) PyOS_CheckStack(void); #ifndef Py_LIMITED_API # define Py_CPYTHON_PYTHONRUN_H -# include "cpython/pythonrun.h" +# include "cpython/pythonrun.h" # undef Py_CPYTHON_PYTHONRUN_H #endif diff --git a/src/external/windows/include/python/pythread.h b/src/external/windows/include/python/pythread.h index 739594a8..42ca5b0a 100755 --- a/src/external/windows/include/python/pythread.h +++ b/src/external/windows/include/python/pythread.h @@ -1,4 +1,3 @@ - #ifndef Py_PYTHREAD_H #define Py_PYTHREAD_H @@ -16,10 +15,6 @@ typedef enum PyLockStatus { PY_LOCK_INTR } PyLockStatus; -#ifndef Py_LIMITED_API -#define PYTHREAD_INVALID_THREAD_ID ((unsigned long)-1) -#endif - PyAPI_FUNC(void) PyThread_init_thread(void); PyAPI_FUNC(unsigned long) PyThread_start_new_thread(void (*)(void *), void *); PyAPI_FUNC(void) _Py_NO_RETURN PyThread_exit_thread(void); @@ -36,15 +31,6 @@ PyAPI_FUNC(int) PyThread_acquire_lock(PyThread_type_lock, int); #define WAIT_LOCK 1 #define NOWAIT_LOCK 0 -#ifndef Py_LIMITED_API -#ifdef HAVE_FORK -/* Private function to reinitialize a lock at fork in the child process. - Reset the lock to the unlocked state. - Return 0 on success, return -1 on error. */ -PyAPI_FUNC(int) _PyThread_at_fork_reinit(PyThread_type_lock *lock); -#endif /* HAVE_FORK */ -#endif /* !Py_LIMITED_API */ - /* PY_TIMEOUT_T is the integral type used to specify timeouts when waiting on a lock (see PyThread_acquire_lock_timed() below). PY_TIMEOUT_MAX is the highest usable value (in microseconds) of that @@ -61,9 +47,11 @@ PyAPI_FUNC(int) _PyThread_at_fork_reinit(PyThread_type_lock *lock); convert microseconds to nanoseconds. */ # define PY_TIMEOUT_MAX (LLONG_MAX / 1000) #elif defined (NT_THREADS) - /* In the NT API, the timeout is a DWORD and is expressed in milliseconds */ -# if 0xFFFFFFFFLL * 1000 < LLONG_MAX -# define PY_TIMEOUT_MAX (0xFFFFFFFFLL * 1000) + // WaitForSingleObject() accepts timeout in milliseconds in the range + // [0; 0xFFFFFFFE] (DWORD type). INFINITE value (0xFFFFFFFF) means no + // timeout. 0xFFFFFFFE milliseconds is around 49.7 days. +# if 0xFFFFFFFELL * 1000 < LLONG_MAX +# define PY_TIMEOUT_MAX (0xFFFFFFFELL * 1000) # else # define PY_TIMEOUT_MAX LLONG_MAX # endif @@ -122,35 +110,6 @@ Py_DEPRECATED(3.7) PyAPI_FUNC(void) PyThread_ReInitTLS(void); typedef struct _Py_tss_t Py_tss_t; /* opaque */ -#ifndef Py_LIMITED_API -#if defined(_POSIX_THREADS) - /* Darwin needs pthread.h to know type name the pthread_key_t. */ -# include -# define NATIVE_TSS_KEY_T pthread_key_t -#elif defined(NT_THREADS) - /* In Windows, native TSS key type is DWORD, - but hardcode the unsigned long to avoid errors for include directive. - */ -# define NATIVE_TSS_KEY_T unsigned long -#else -# error "Require native threads. See https://bugs.python.org/issue31370" -#endif - -/* When Py_LIMITED_API is not defined, the type layout of Py_tss_t is - exposed to allow static allocation in the API clients. Even in this case, - you must handle TSS keys through API functions due to compatibility. -*/ -struct _Py_tss_t { - int _is_initialized; - NATIVE_TSS_KEY_T _key; -}; - -#undef NATIVE_TSS_KEY_T - -/* When static allocation, you must initialize with Py_tss_NEEDS_INIT. */ -#define Py_tss_NEEDS_INIT {0} -#endif /* !Py_LIMITED_API */ - PyAPI_FUNC(Py_tss_t *) PyThread_tss_alloc(void); PyAPI_FUNC(void) PyThread_tss_free(Py_tss_t *key); @@ -162,8 +121,13 @@ PyAPI_FUNC(int) PyThread_tss_set(Py_tss_t *key, void *value); PyAPI_FUNC(void *) PyThread_tss_get(Py_tss_t *key); #endif /* New in 3.7 */ +#ifndef Py_LIMITED_API +# define Py_CPYTHON_PYTHREAD_H +# include "cpython/pythread.h" +# undef Py_CPYTHON_PYTHREAD_H +#endif + #ifdef __cplusplus } #endif - #endif /* !Py_PYTHREAD_H */ diff --git a/src/external/windows/include/python/pytypedefs.h b/src/external/windows/include/python/pytypedefs.h new file mode 100644 index 00000000..1cf6af94 --- /dev/null +++ b/src/external/windows/include/python/pytypedefs.h @@ -0,0 +1,30 @@ +// Forward declarations of types of the Python C API. +// Declare them at the same place since redefining typedef is a C11 feature. +// Only use a forward declaration if there is an interdependency between two +// header files. + +#ifndef Py_PYTYPEDEFS_H +#define Py_PYTYPEDEFS_H +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct PyModuleDef PyModuleDef; +typedef struct PyModuleDef_Slot PyModuleDef_Slot; +typedef struct PyMethodDef PyMethodDef; +typedef struct PyGetSetDef PyGetSetDef; +typedef struct PyMemberDef PyMemberDef; + +typedef struct _object PyObject; +typedef struct _longobject PyLongObject; +typedef struct _typeobject PyTypeObject; +typedef struct PyCodeObject PyCodeObject; +typedef struct _frame PyFrameObject; + +typedef struct _ts PyThreadState; +typedef struct _is PyInterpreterState; + +#ifdef __cplusplus +} +#endif +#endif // !Py_PYTYPEDEFS_H diff --git a/src/external/windows/include/python/setobject.h b/src/external/windows/include/python/setobject.h index 7f6d0f8e..52f95caa 100755 --- a/src/external/windows/include/python/setobject.h +++ b/src/external/windows/include/python/setobject.h @@ -6,73 +6,6 @@ extern "C" { #endif -#ifndef Py_LIMITED_API - -/* There are three kinds of entries in the table: - -1. Unused: key == NULL and hash == 0 -2. Dummy: key == dummy and hash == -1 -3. Active: key != NULL and key != dummy and hash != -1 - -The hash field of Unused slots is always zero. - -The hash field of Dummy slots are set to -1 -meaning that dummy entries can be detected by -either entry->key==dummy or by entry->hash==-1. -*/ - -#define PySet_MINSIZE 8 - -typedef struct { - PyObject *key; - Py_hash_t hash; /* Cached hash code of the key */ -} setentry; - -/* The SetObject data structure is shared by set and frozenset objects. - -Invariant for sets: - - hash is -1 - -Invariants for frozensets: - - data is immutable. - - hash is the hash of the frozenset or -1 if not computed yet. - -*/ - -typedef struct { - PyObject_HEAD - - Py_ssize_t fill; /* Number active and dummy entries*/ - Py_ssize_t used; /* Number active entries */ - - /* The table contains mask + 1 slots, and that's a power of 2. - * We store the mask instead of the size because the mask is more - * frequently needed. - */ - Py_ssize_t mask; - - /* The table points to a fixed-size smalltable for small tables - * or to additional malloc'ed memory for bigger tables. - * The table pointer is never NULL which saves us from repeated - * runtime null-tests. - */ - setentry *table; - Py_hash_t hash; /* Only used by frozenset objects */ - Py_ssize_t finger; /* Search finger for pop() */ - - setentry smalltable[PySet_MINSIZE]; - PyObject *weakreflist; /* List of weak references */ -} PySetObject; - -#define PySet_GET_SIZE(so) (assert(PyAnySet_Check(so)),(((PySetObject *)(so))->used)) - -PyAPI_DATA(PyObject *) _PySet_Dummy; - -PyAPI_FUNC(int) _PySet_NextEntry(PyObject *set, Py_ssize_t *pos, PyObject **key, Py_hash_t *hash); -PyAPI_FUNC(int) _PySet_Update(PyObject *set, PyObject *iterable); - -#endif /* Section excluded by Py_LIMITED_API */ - PyAPI_DATA(PyTypeObject) PySet_Type; PyAPI_DATA(PyTypeObject) PyFrozenSet_Type; PyAPI_DATA(PyTypeObject) PySetIter_Type; @@ -104,6 +37,12 @@ PyAPI_FUNC(Py_ssize_t) PySet_Size(PyObject *anyset); (Py_IS_TYPE(ob, &PySet_Type) || \ PyType_IsSubtype(Py_TYPE(ob), &PySet_Type)) +#ifndef Py_LIMITED_API +# define Py_CPYTHON_SETOBJECT_H +# include "cpython/setobject.h" +# undef Py_CPYTHON_SETOBJECT_H +#endif + #ifdef __cplusplus } #endif diff --git a/src/external/windows/include/python/structmember.h b/src/external/windows/include/python/structmember.h index 9c00e638..ea0645b5 100755 --- a/src/external/windows/include/python/structmember.h +++ b/src/external/windows/include/python/structmember.h @@ -15,13 +15,13 @@ extern "C" { flag is set). The array must be terminated with an entry whose name pointer is NULL. */ -typedef struct PyMemberDef { +struct PyMemberDef { const char *name; int type; Py_ssize_t offset; int flags; const char *doc; -} PyMemberDef; +}; /* Types */ #define T_SHORT 0 @@ -65,8 +65,8 @@ typedef struct PyMemberDef { #define PY_AUDIT_READ READ_RESTRICTED /* Current API, use this */ -PyAPI_FUNC(PyObject *) PyMember_GetOne(const char *, struct PyMemberDef *); -PyAPI_FUNC(int) PyMember_SetOne(char *, struct PyMemberDef *, PyObject *); +PyAPI_FUNC(PyObject *) PyMember_GetOne(const char *, PyMemberDef *); +PyAPI_FUNC(int) PyMember_SetOne(char *, PyMemberDef *, PyObject *); #ifdef __cplusplus diff --git a/src/external/windows/include/python/structseq.h b/src/external/windows/include/python/structseq.h index e67bfda7..034eb8f0 100755 --- a/src/external/windows/include/python/structseq.h +++ b/src/external/windows/include/python/structseq.h @@ -15,11 +15,11 @@ typedef struct PyStructSequence_Field { typedef struct PyStructSequence_Desc { const char *name; const char *doc; - struct PyStructSequence_Field *fields; + PyStructSequence_Field *fields; int n_in_sequence; } PyStructSequence_Desc; -extern const char * const PyStructSequence_UnnamedField; +PyAPI_DATA(const char * const) PyStructSequence_UnnamedField; #ifndef Py_LIMITED_API PyAPI_FUNC(void) PyStructSequence_InitType(PyTypeObject *type, diff --git a/src/external/windows/include/python/sysmodule.h b/src/external/windows/include/python/sysmodule.h index 38874425..8b42b047 100755 --- a/src/external/windows/include/python/sysmodule.h +++ b/src/external/windows/include/python/sysmodule.h @@ -10,9 +10,9 @@ extern "C" { PyAPI_FUNC(PyObject *) PySys_GetObject(const char *); PyAPI_FUNC(int) PySys_SetObject(const char *, PyObject *); -PyAPI_FUNC(void) PySys_SetArgv(int, wchar_t **); -PyAPI_FUNC(void) PySys_SetArgvEx(int, wchar_t **, int); -PyAPI_FUNC(void) PySys_SetPath(const wchar_t *); +Py_DEPRECATED(3.11) PyAPI_FUNC(void) PySys_SetArgv(int, wchar_t **); +Py_DEPRECATED(3.11) PyAPI_FUNC(void) PySys_SetArgvEx(int, wchar_t **, int); +Py_DEPRECATED(3.11) PyAPI_FUNC(void) PySys_SetPath(const wchar_t *); PyAPI_FUNC(void) PySys_WriteStdout(const char *format, ...) Py_GCC_ATTRIBUTE((format(printf, 1, 2))); @@ -22,16 +22,16 @@ PyAPI_FUNC(void) PySys_FormatStdout(const char *format, ...); PyAPI_FUNC(void) PySys_FormatStderr(const char *format, ...); PyAPI_FUNC(void) PySys_ResetWarnOptions(void); -PyAPI_FUNC(void) PySys_AddWarnOption(const wchar_t *); -PyAPI_FUNC(void) PySys_AddWarnOptionUnicode(PyObject *); -PyAPI_FUNC(int) PySys_HasWarnOptions(void); +Py_DEPRECATED(3.11) PyAPI_FUNC(void) PySys_AddWarnOption(const wchar_t *); +Py_DEPRECATED(3.11) PyAPI_FUNC(void) PySys_AddWarnOptionUnicode(PyObject *); +Py_DEPRECATED(3.11) PyAPI_FUNC(int) PySys_HasWarnOptions(void); -PyAPI_FUNC(void) PySys_AddXOption(const wchar_t *); +Py_DEPRECATED(3.11) PyAPI_FUNC(void) PySys_AddXOption(const wchar_t *); PyAPI_FUNC(PyObject *) PySys_GetXOptions(void); #ifndef Py_LIMITED_API # define Py_CPYTHON_SYSMODULE_H -# include "cpython/sysmodule.h" +# include "cpython/sysmodule.h" # undef Py_CPYTHON_SYSMODULE_H #endif diff --git a/src/external/windows/include/python/traceback.h b/src/external/windows/include/python/traceback.h index b4466f51..a7b6822f 100755 --- a/src/external/windows/include/python/traceback.h +++ b/src/external/windows/include/python/traceback.h @@ -16,7 +16,7 @@ PyAPI_DATA(PyTypeObject) PyTraceBack_Type; #ifndef Py_LIMITED_API # define Py_CPYTHON_TRACEBACK_H -# include "cpython/traceback.h" +# include "cpython/traceback.h" # undef Py_CPYTHON_TRACEBACK_H #endif diff --git a/src/external/windows/include/python/tupleobject.h b/src/external/windows/include/python/tupleobject.h index 19f9997e..fc6917dd 100755 --- a/src/external/windows/include/python/tupleobject.h +++ b/src/external/windows/include/python/tupleobject.h @@ -36,7 +36,7 @@ PyAPI_FUNC(PyObject *) PyTuple_Pack(Py_ssize_t, ...); #ifndef Py_LIMITED_API # define Py_CPYTHON_TUPLEOBJECT_H -# include "cpython/tupleobject.h" +# include "cpython/tupleobject.h" # undef Py_CPYTHON_TUPLEOBJECT_H #endif diff --git a/src/external/windows/include/python/typeslots.h b/src/external/windows/include/python/typeslots.h index b85dbcc4..d091cf00 100755 --- a/src/external/windows/include/python/typeslots.h +++ b/src/external/windows/include/python/typeslots.h @@ -1,12 +1,6 @@ /* Do not renumber the file; these numbers are part of the stable ABI. */ -#if defined(Py_LIMITED_API) -/* Disabled, see #10181 */ -#undef Py_bf_getbuffer -#undef Py_bf_releasebuffer -#else #define Py_bf_getbuffer 1 #define Py_bf_releasebuffer 2 -#endif #define Py_mp_ass_subscript 3 #define Py_mp_length 4 #define Py_mp_subscript 5 diff --git a/src/external/windows/include/python/unicodeobject.h b/src/external/windows/include/python/unicodeobject.h index eb87535c..b10ea826 100755 --- a/src/external/windows/include/python/unicodeobject.h +++ b/src/external/windows/include/python/unicodeobject.h @@ -1,7 +1,7 @@ #ifndef Py_UNICODEOBJECT_H #define Py_UNICODEOBJECT_H -#include +#include // va_list /* @@ -112,7 +112,7 @@ PyAPI_DATA(PyTypeObject) PyUnicode_Type; PyAPI_DATA(PyTypeObject) PyUnicodeIter_Type; #define PyUnicode_Check(op) \ - PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_UNICODE_SUBCLASS) + PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_UNICODE_SUBCLASS) #define PyUnicode_CheckExact(op) Py_IS_TYPE(op, &PyUnicode_Type) /* --- Constants ---------------------------------------------------------- */ @@ -269,10 +269,6 @@ PyAPI_FUNC(PyObject *) PyUnicode_InternFromString( // and will be removed in Python 3.12. Use PyUnicode_InternInPlace() instead. Py_DEPRECATED(3.10) PyAPI_FUNC(void) PyUnicode_InternImmortal(PyObject **); -/* Use only if you know it's a string */ -#define PyUnicode_CHECK_INTERNED(op) \ - (((PyASCIIObject *)(op))->state.interned) - /* --- wchar_t support for platforms which support it --------------------- */ #ifdef HAVE_WCHAR_H @@ -1043,7 +1039,7 @@ PyAPI_FUNC(int) PyUnicode_IsIdentifier(PyObject *s); #ifndef Py_LIMITED_API # define Py_CPYTHON_UNICODEOBJECT_H -# include "cpython/unicodeobject.h" +# include "cpython/unicodeobject.h" # undef Py_CPYTHON_UNICODEOBJECT_H #endif diff --git a/src/external/windows/include/python/warnings.h b/src/external/windows/include/python/warnings.h index a1ec4252..060ac1ef 100755 --- a/src/external/windows/include/python/warnings.h +++ b/src/external/windows/include/python/warnings.h @@ -4,14 +4,11 @@ extern "C" { #endif -#ifndef Py_LIMITED_API -PyAPI_FUNC(PyObject*) _PyWarnings_Init(void); -#endif - PyAPI_FUNC(int) PyErr_WarnEx( PyObject *category, const char *message, /* UTF-8 encoded string */ Py_ssize_t stack_level); + PyAPI_FUNC(int) PyErr_WarnFormat( PyObject *category, Py_ssize_t stack_level, @@ -26,15 +23,7 @@ PyAPI_FUNC(int) PyErr_ResourceWarning( const char *format, /* ASCII-encoded string */ ...); #endif -#ifndef Py_LIMITED_API -PyAPI_FUNC(int) PyErr_WarnExplicitObject( - PyObject *category, - PyObject *message, - PyObject *filename, - int lineno, - PyObject *module, - PyObject *registry); -#endif + PyAPI_FUNC(int) PyErr_WarnExplicit( PyObject *category, const char *message, /* UTF-8 encoded string */ @@ -44,20 +33,9 @@ PyAPI_FUNC(int) PyErr_WarnExplicit( PyObject *registry); #ifndef Py_LIMITED_API -PyAPI_FUNC(int) -PyErr_WarnExplicitFormat(PyObject *category, - const char *filename, int lineno, - const char *module, PyObject *registry, - const char *format, ...); -#endif - -/* DEPRECATED: Use PyErr_WarnEx() instead. */ -#ifndef Py_LIMITED_API -#define PyErr_Warn(category, msg) PyErr_WarnEx(category, msg, 1) -#endif - -#ifndef Py_LIMITED_API -void _PyErr_WarnUnawaitedCoroutine(PyObject *coro); +# define Py_CPYTHON_WARNINGS_H +# include "cpython/warnings.h" +# undef Py_CPYTHON_WARNINGS_H #endif #ifdef __cplusplus diff --git a/src/external/windows/include/python/weakrefobject.h b/src/external/windows/include/python/weakrefobject.h index 36890820..190a8181 100755 --- a/src/external/windows/include/python/weakrefobject.h +++ b/src/external/windows/include/python/weakrefobject.h @@ -6,40 +6,8 @@ extern "C" { #endif - typedef struct _PyWeakReference PyWeakReference; -/* PyWeakReference is the base struct for the Python ReferenceType, ProxyType, - * and CallableProxyType. - */ -#ifndef Py_LIMITED_API -struct _PyWeakReference { - PyObject_HEAD - - /* The object to which this is a weak reference, or Py_None if none. - * Note that this is a stealth reference: wr_object's refcount is - * not incremented to reflect this pointer. - */ - PyObject *wr_object; - - /* A callable to invoke when wr_object dies, or NULL if none. */ - PyObject *wr_callback; - - /* A cache for wr_object's hash code. As usual for hashes, this is -1 - * if the hash code isn't known yet. - */ - Py_hash_t hash; - - /* If wr_object is weakly referenced, wr_object has a doubly-linked NULL- - * terminated list of weak references to it. These are the list pointers. - * If wr_object goes away, wr_object is set to Py_None, and these pointers - * have no meaning then. - */ - PyWeakReference *wr_prev; - PyWeakReference *wr_next; -}; -#endif - PyAPI_DATA(PyTypeObject) _PyWeakref_RefType; PyAPI_DATA(PyTypeObject) _PyWeakref_ProxyType; PyAPI_DATA(PyTypeObject) _PyWeakref_CallableProxyType; @@ -56,30 +24,18 @@ PyAPI_DATA(PyTypeObject) _PyWeakref_CallableProxyType; PyAPI_FUNC(PyObject *) PyWeakref_NewRef(PyObject *ob, - PyObject *callback); + PyObject *callback); PyAPI_FUNC(PyObject *) PyWeakref_NewProxy(PyObject *ob, - PyObject *callback); + PyObject *callback); PyAPI_FUNC(PyObject *) PyWeakref_GetObject(PyObject *ref); + #ifndef Py_LIMITED_API -PyAPI_FUNC(Py_ssize_t) _PyWeakref_GetWeakrefCount(PyWeakReference *head); - -PyAPI_FUNC(void) _PyWeakref_ClearRef(PyWeakReference *self); +# define Py_CPYTHON_WEAKREFOBJECT_H +# include "cpython/weakrefobject.h" +# undef Py_CPYTHON_WEAKREFOBJECT_H #endif -/* Explanation for the Py_REFCNT() check: when a weakref's target is part - of a long chain of deallocations which triggers the trashcan mechanism, - clearing the weakrefs can be delayed long after the target's refcount - has dropped to zero. In the meantime, code accessing the weakref will - be able to "see" the target object even though it is supposed to be - unreachable. See issue #16602. */ - -#define PyWeakref_GET_OBJECT(ref) \ - (Py_REFCNT(((PyWeakReference *)(ref))->wr_object) > 0 \ - ? ((PyWeakReference *)(ref))->wr_object \ - : Py_None) - - #ifdef __cplusplus } #endif diff --git a/src/external/windows/lib/Win32/python310.lib b/src/external/windows/lib/Win32/python310.lib deleted file mode 100755 index fbee7df20fd0e7e9ce276280f27a0cf9fb9027b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 363654 zcmb4s4VYX-m3G$iO5SGLsMy)AY>rWID`r58XXU zCL+d&5s_sPS(arLF&kNyWm!awh%q8y#B4-FL>4h$L_~}svKTQU;`g39^?&PjvhzIe zbEnUF&#C{qb*t*uebZ^>#=!9D2fy=Z|L+|q`M>qA>FS@E?|Ju(nezV&4i@6oUlWDf z4i<&mPZNbNw?y)t849=H3fzCML^0VY7}IeD3imKx@t7zQ3f&UPgG+_P)~iJ#aWt;L zxrZ~BLQgn%t-@|xcZ>AG^BEt%OQeKP)g+QT9upE@eo-Vq`o{)i-xVSSlCRt$k?uPj zzYiD16;DVchwc*+uU#P$AieuYg{yGAN)-Nbr9|<#?ShaVxk2GXT!An2iDEB&eP|iu zwJSvtNNRf&*5SHN6zBqz;BUngkN9J*s~8j;5Yq@UC)6B?B1*}2UlRvdd4&OpYZHP z#&37PAMmS382@>qNC-EAPq?{{@r6@FLio3p66qf{3PQ5{O2+4Ri-hoRPcd#my^uiq zZvi&m!uZnBA|d?iEQNb;1->}Uxal&H5H|EMZaNgdfiEGwgbmvnUwi_5;NJ?2jVK4g ze~c(Rjw|q=H!yBQ-V**D?MMRg`FG%kWsDn<*MwU)D|F)u+kJPAKU{s6^EFGwU0y@YxX`~+bT z9z$LO=~V>$Xzr2mROjj$2@8c?_&@g&^yfWlH-f$wxO?mb2nfb?Heg*CXY zfedLO+=p~X?7XR8smC+ExgLJki}dBgB$98Tt`pvX@&nQzPLU{VJxUOg&8UAs`X}TU zVdxNv;;U{GgygYHB#Po`K}fzeBvD*^iXapoS|$qLKaKI=6{0}+0qUy6YFr7Z`-J~q zp|As2VEYmU|9z41?BSw7`0WD5vxo~}5AvGuEaFMn^919!_rUIQ zQFspRZ|~{2ZWM)=p#NPDuob!~2p9F`yU0@@{jXycuE+IyQFviUBK`8yf>8L=28r}* z7YIV(5A!6_+t&%kUR?Kz!tarXKzdiVMB&AIg~Yy-MS<|=^BMb4-h`JB55n)SkVwCZ z`wCF_Bl4N>6515ukE2dgp0^Pr7s=aDzQ;`y$$N0$n1*)x&Id&D{yXsdNRga? zzT&h=A~^}|^wdovnU1>u?kyrY`5}><4*cD*A~^%PV{orO6?Nn-x8pa$dH*cP4iU+l z(XXE}1N(y!Bd<9KT2;@2iRAy%)eY+V|<_BTVRL?m=9@pLUr@-hLN;uYnDC zZ{CS8;rp#eh-3!hKkWtRa4(s0F~Wzx8IuvO+eC8W3iyNW9f!d8BMAReq-!JOs88>M zj}uNn*ht&4OW^+ur0Hsr-ZxdEIK3bUsm5`eMM`)(+9}~(f^q!uA|;%74dbM>A|*^i z+a6mPGBs0UL=IYn;44_S3)oHp0EgYn{W=wfY7^x(T6++((&s6+>;3yE2f`+ zMih3V?%ayH@c9P%Z1hW;&{u34VeCd<3nb@V$M_)90i+M3juCz&7!O~C-@s3|iS%)l z1!2eOjGr!n9Ched=!3VR58sA96-b`m$Ea@;NegMJpC^*BTNsT8MM7w;mq>qs_5dVJ z=m{4=P8d6lanTF-4K(qaaM4r-?oA1yF({G#;uQEfMI>uGMf%IfMe@n`$SI7gxv*=F=dj=J7 zAKC*vtMFUMfn?S7jESd3A~Aw1uxc9PBNR4pG3p*+4a%4B(IJWSDfA(PUth}DeJtV+ zJc}{|k~Ii}uo^xjUci-ryadvxP=-LVa+bpFxB?$uz_@rL!UZnbrGPqf3Gm^2B+_3Z zjfCHzp8}H4;Q!y^IpQ0*R{_aYdlfFm75IlM7+0nEor=N(4~fF}Zo?SwO2&8Bh=N1` zSKxbRFdkS2F7REnA0YYsB_g@zVv+7m6`sSDfIbsQ{snO%{4>IlcnVkG`lSkp+x5V| zAl-z2ZZST85PSpI>}Fhdq(~$X@9VA*>F)<6l7G5YNPO;ikw{#ND{#$ZiS&gY@ODZR zho_44g?mMDgik;QB;REe&%Htr(ib0) zC{8<0NW5*GC=zhLkT@RCNQAddR@j3pFzp(N^i!xGgv+j#C<=jbGqC(-iR8Z3cn(9~ zuOR7HGBifBd8BRand0Y z$&YsmiS1}lK=Gh^6uNQk7Rfe*3lt|KErcR`5?(nZkvs}LVf#MDHq=YP_iGAI;0io~ zx+j6MeFWp!SM*CH-?#(MrNGyR89&)562gzrE`g$0&3NVG2oLD%lSm%kjQjw;nKCwC ziRY6mMe*RP6pq357|g|-%y@9CNPyzG`xt}B6Ckaj4ii3d9OGs5DM0b^Wg`8{dd6+H zi4-V)=pl*Z{yrh`Bh)>@J!q?hA0jV-^mj)y{ugyn;vQU)C!a#ULAc^6#-|$i4P1UV z{gijyGxB_V;{CJ4*y+=ht`1?hS%jdx!`1|`M(&ru)63^cvQo?iRF`l1} z-_tSwfVTTz4*?fo?hW~N+jAnh9c_^C<)aw4qf7{oKL+$O{s(=K#2Bu?6PwUSFT;2h z@$5l9Ket6Bdyf>w!&ZpmU*p*YNPdTS0mZ`}l}P^g1R?P}>?I1g0{@G26Mly<2@8=H z!t*;AFDwwn58zqg&)Y=t@M#js?`uM0?|Os_ynuWp?0re1_}6zL4tIj{B;#fHBj6c= zfjs;(;`KVz1t59pDaKz;6A9tv9*Ojg=s%7|y55fZ2c&P@$2jT{kpjiH4@#tedxnsB zBkB<0Z*ONDH4NS``h#5(g|D3j|96V?IMk1~JS9iFYyHaknS}>9MFwgg3(% z;aKz+gf}%9N8^4-c+-;-#S_jFg!Ijb3s8I&p1*04Ti zqY-bQc;a-%J5ED9fD`U#oPqo&bU-Gt30I(NJ);}p6VAr`DxvFT2F7|tLO1#s!rTiO zvo9A#!W`%%1g^lW4UDeCVGqngnG@z+&*(&bkl2bVFlQIz>_-qTFdH_6xsNi=f}U{E z@gl9D?|{w7<%9&@Fnmpe-g^Av`JwWuD~*s38DNPct7eQq5lE~jaz}@`;czJ znJ8-@{m}D_fq5b&ES|>byBmC9mXIi(F%NYF`UMz=5e7C(6z88J25k(Y#%pOGlO8|@QFyB}lB z+KG4qGm$2s`0ghp(uLE7#9YLk(1SWccrWS<;mi#Z#hE7xLb~WF#{0I5lyLSgiQ;=t zN4^6y(UyVqRQMpAbErh|)LW3gTSPh!Wh`+FuE5_N#5iX?cqpU4LpcL!7wV=2%C!qP zZJ4oW4B?N7;weic(z9y_A2{np#yKy+&r9%etweg(C6E&iW}J7sNC}ne85bb$2p>E_ z;YM6($jVzT;P3+7=O1G@g+RR=tcfY^y3Q5c|c(SuE5+ajLwr$Uch_qRX|?9 z2UxI)vG6981AuTPx{+SODSeE0KaTnW%tHDJOOPJIxw{xkmx&Z8E||lZe-G;2Jt93_ zqZ|JNOV%qOzm@=}qAn5Md#gn8JzIpt`(8jhcmci<7EpYDzrv%qK8mm&VSE5-Ae?t9 z;{#}Wgfij=6wka}BJH>g?ejADOeNA~7oh!LfUssUN>_^_Vc~6}c+OM+Asio|1^o>dhECqYe@Nh&Du6cNSypc99aki@HSEewD&exE>{vBQeg9xCvL_s1XM4(TT+6 zxFStkQU8GCZx$)wzW+BUi|?#pJg`Khgzr4fc;I3D2DYL+3Ex9~1(G)%E0JzPT?LY( zkp>{e_)FscOGFAJZ+KZEz5jXqeqJO;U@Rzsan2FI8?KZnE=HLG>8FontU-PdEY8yHXExsUMJt&E>xu9NUM<`xJ)#heC^-h8kE z^5kaV-%$oY;nC+9KY36T2tUKzFX4$t82@v$C`eq7EASJ9MR*MJs6hH}mnlrbbrQln zTq3<0c?}eHOlSNQ&r5_KU(5J^$B2T&4Y=Nbb*Ez_3co&46fhT3_zj+YpXpZEf-CSV z%)>}Lfh({JbHx&u;z~G#@#`B!fq->WLi!(fOC+y6L=e(@P|tv5(&rd|eWOSSuUyYK zdUk=}PD<~dPUlD!HTmnOg~HcOz?|y#}~okHTtPf%8sQxCvL3N&P`a4gDcdT#EP-Rtz&Pm;^guA$QM z64$>BIdJVQ3Mb+UTz4$vpEin=@VSE|3SS+A{TOuM1BE+LAA$6+V-rge!2#&5X6{ zz(ac;LY@B5?TibK5D8&m0b@DtjfBb!g3sP z_hJ15<$uLm%t4b#|qkc@do-)*o;3Z zihqSZdvu>jHa*PvD#`*Vp1(^X#U22Oi;fd1Va274i>}0P<|u5zb&E*XqV5C5$})vVa0L#%g7I1*iiEeo zFX6cJ7{^Q(MTwhm1>SU<0>XR~@MiR%635~Sym2SvsHLJvcq7t6_?s&kN2a1Eu?APd z3yh;41|K;3ZpPm}Ac_(_xB^FaGB9^mBpi!pZHe``uE)C82;(W_Ay7PaDq|V!fb?gm zmxL$oW;}TZ@(uVIeiLrJnem^vN}%lj6S(a}iDD7?1f+jKKM5q~9mn{0x0SBYbNUXpWcy%A+ zReMDW6sOLUNMD0^0>wAnB9Xpo9rWuE_EC)2Zx=-%ebpq1;_H#8Kzh*C3P<63lt}hX zX8Zx|4k*6vP>JLZ*W)+RF%|WR@P-)@>8l??d>#_XiU%ap9jI%BpU+T09{(Kpe`hFc z#ueC!vXDR@uyYgAj65N%Sj4E}J|uAgt^n4P20~G&iBjZq%Gmv~0eJo*9zeMpe zv}eMn1{s&F!EXZ610-KPOrnUsh>(2kR*B-Js8@tlrz)V%uL4#huO(29SX(TvM!f@) zpI^dwvL+HB9X*yYa+*kiGgGTQk$#?y!!L5xVG-&l#U`bvz??`2Fn5zk7fGv7jc z0g7exjX?6t4UDJmLOkyh#Y66uNS=g_@Qd>qKSy2z#Y51(fMmx`iQ;R}h6$s{A0YXM zBNZ%X z9~t{_1zwAI5dNygcrC&q{PlGTN8t**ZZ`wZy(!^uwgBJ(Ma-c|ycK;1kRI`bMDZhi zg0U6Xts-5yhEOVX&F@*bsIN3FDbarJjj}{VOU1t%G7<}e#V&DCrHKe>t`{Mra`_7?tCo}-QNB?H{5&$`E^~HyWTZY2B^#B?pP$dy zDnsw;MFD8nq160F87;2K?95PImCMg%D@D%)EN9n8Mo~S5|5!v#5&nmVAX2b;*?U4L2%f)0~*>YnWU^ z_>J+g*1~~-@kYbVXL#?5Xxp2qfTDIKw3}AFQ9+#1+M7DXFv)0oCgwI8rQS-*#6YuS znyhT@>U_S`vB~V=%D{!$=up^!D$DO{w#qG2<&cQbN)xggKW>?jsD)IxrIyaDwB*lz z^mEz_VdYbN-Qrq{dPr@Vqx8k(JIBIPAsVvXbS<1UJ~UKml=>#d-26DVS{tmdYR<0L zn)Q)N*FvpFad$Qc@i0YSXwpQO`t&bRY{#M>Y^DgMi|iq1~t(UZo}dSx*^-%OxK%s*)S( zqDpzBtJyo=KQK~mx{XVD&*%fot{N&dvRQ|P((o*;-YBxcMOBW_Itgy`T}`X%nV4T5 z8>`lqmzI=A#w#W*S-F)UH%22wCAPe?D-#hKOD$_&xe1ZZiL5-7^tKv^ZXRE2trc}_ zh_F`ZywJ%G)YcBYK|8xZZSA@%wdL+zNC%|lmf7o!c|xiE9aMAePKli++f^H^I9;)6 zrAqEGZT2}d8Yr5d=t^3+Njn7?QF;7vd%ujx9TBp%(OaQ6L3K3TfL*O))LCviJ%))3 ziydBfHOgbL!|NL#a|bcTp^`gH3|D(cs8kaN;P$zwGFBt#>SQ1JeqJtIc(r zH-)Sf4p%qN3|A{0uHSHtR~9?Gpy?W-X}b%?NBik!UY}KNnwvq-L?AYT&XKyCTZ*Qn zhSgnnyGKQ0q2Y8id+CX*|nl~LkMl>mvJLpJ%4dNY1wg6NPx{S~omAu|)Z|+e-dyLg+bRMfFvVg2Uv|Q92t`6neX>DlL-PTHLL#qyV z)Ewj~sY3qJx-(q$`BD|~7v|DQW7zSk9M)l{Zt6}`MZwV+pKjp>bXR5gUTFpfp3Yc= zRXs2Y)z;2b@zBC33^!Kk{o0N?I$F=R3@tXEY=sL#wF<-4l`LRtvEk`z74Wp!@M6_W z$_yh`vHThpsY+fQ3hsTXW|%vldfdTm%&2=y3&eQp<;UGcm63{fiwxz4*;ySK>8cIY z?Hxu@t-|ACDA1}dAFq!$=?P)beFAa}BP{czHM>4GF{j)r+YnVMtip!asE=A-)&2t;oRwc(G&^lIld>_m;a+Qt^&6MQY zxJqLq+k)#&H(OnOXu{0Z`<6L@Nv~RGEYveb_zuZA~D_DwNzPBKjuH9NQV(IRtn_YBPvE_ z#*?N)rLt5R9cxXLkj37#kkxYuA#h`Xe#d+4%xYY=S%FfEWtOcqE*sBJxiPUi*TS!5 z*1N7~Rzg|C-f_3iGmDVi&`8bgLN}BH(KfyEW{ajI=Dj) z2)@eHcU`qyJEQb%I)X(z*F@`li=IYhtlThe;8wO< zhzFz7)iRTK!-!aSnP=m8$W)K!t}Ks~Y?O6WBZ^1>mYsE^Rsd*|bw4aDu4^3_H|3t1Ennd#jOW|*PU8{-%UWW1oB+qD@L2IcY@QI`P5518lDd5h$= zQ=^2+W8hcRc+mdt8Xc>dp<{F>=+O-GcI{ z8RSJ2o+vYW;RIlG)&ftmx#@6qo`oAx{a~qsfsw2B?BeNw1uHzRiEAVPqq7!WO)ULa zO}89TSP^#D>6(%!tc>V~LonzE%G9wj5~W2F0F=$H)fETHi*b|2-wPmJu)q4Z-|aK zTAfuXIx9Et=RIi6dH5LECHQvf>;n-SR0w}`_cm-jABc{YTGl``FBTi_H6#w z4LPf$IIy&9w7eWc+xob-&oZvHR+uUr$+z;g){2JL9FlX-*Fkwo-zGmzWaMaZLm4QQ z`;B+H!)Ss0cdTyu6xOjo>Mk0uo9=;?O2U>6yV9aeSt7NyirFQ^N)|}nV0Fl}I#w!) z)HcV>ZVXoX0&aZgHn4Fjn3qBp(S|T4pf4Zrr>&?#(}I6YL-+ljMtwD=%$%)#JrgRJ zmM~E6`#e84ccp3kGPo>kQIx`zLDbTl-K-@FIQR!;>t0BHrv#6Y{@WBr%au=o6+f+m0G1y9Y9%83T?irk!?netOjpM zwX%vgMmL?U>u8fvr6YFgsmqpPrePXIZoAU+5pJc<=zJ6J5td@2`J_-(G)&5dsP|Ck zn`FH!zT+L)nCm^%Dep8?rTu(a_nszTm(H%$SJkk9;x0?bBx@^|5g6N2_ADUQDaJh6 zLbo55JeoZtsU}euH&k|>Q6dT@qD(P28egVo>O&}_$uXgetDw561Gzg}((#$MEt1%=njn(;#HsWe8b%^c<8W~C8L zZ69*9KHiE%MZIto(R8%1{@stgyAfUQ1e(C;+|_o60avlv!_|>NJhTi{n$3vWd6h<; z_I~C>U*5hWW*yl4Gdx;pRRP@Ewq4DRIaM?t zOufcxDu)`0&fMC{YNK8oMb8q`F6@j*JI7n&jd&aGZPmvxylco->~{|fdn2x9Va@$Q zlvK1?s;bsquU|Mm7BP0qXlNXrW`wh#-oo7WNQDv@CCsg%pb&#-u2OP3niI8woHW6E z+ia%?`>f-5n98KbIZ;+d_0rWD_w~58XCj);((DFaym8Ij(+C zHV`hcJo^~M*!zV}^W2!^Y`TMocy>5McyXhf?>O}ZSG@#4nV(Fa~~SjJ0`E@aSL_kD-l!jdfG#Z6I;ix|b&yRpo%k5P<$ zkP2kS0i;5+j2AbGS{Y<#jAHEFDCXG5D8@cW1u_Cp81DLQZfPi<@#ID`*G-&ce7I4~ z^${l-A3=(cDF>7y%QK$b$Xa##(g}0Ljcl%uILX8#nFO3T(7#YS!Lw*g3|kt!AqkUPzUU!IW0;d)XF9Qku`Z4CQE zXX?Zvze2e$IXQ<(a-V6Y&?9oq?1_f0r7kNQF0tHD>323vE!DpAW&%wlvjlCL-7>&g zW#yVF7Yc%x8<^{ATz0sH!LqeRvr+W^TYF43Vuv^1?h6m4?(ed57KU*)-;*}*v~2qw z`3nKgLXPX|0EfQV5d{5{<|(lzW;31}2bGL8$r7|i&KfrR>U_q9X;ae5*J%r4_SIPz zCc)RmEFu&C97WgEA!#3Ss>N9|%E~B+u15mT>55Vgb5( z#}Z+-57BBJD}-m|vCyp6x8fO~NH)Tw*7+7^j@0{|)7o;BzBuTH+-#QvD2MIUR8x_{ znup)i$j=$a=*8PT>srJaI6_Ts^XM3x2*m|SxiC{1rGQFwE-4o^dqBF}pI~-wZqS?9A@k0!HUA9)-Vf;w(A3^*&6Grd@;!$7xd zj?Q=5d<%gQ(^U3ibNfIla@3xJdgTh8@7Fa)4@9{k`}Q%K=LgYp%E>qOb@CJ-n%mdu zP&XRdrSFJQGqu2R97?S-kA(Atq$^KIuLY}pSV7RFiHN+|NKQvHI2_BD;@N{dsr6)C ztf5r>?)6>@kcLX62-t9pcBj1{OC?83wHMSv{|GMjWWNFIMBJCZnA&FM1Xnfkq0%wyC6)c70X|&VXbKLSL+L={oqUm&asUUU1j()Utcm#h{&pq`*h-FgnNmd&$t0 z8yxeXG6rwX>J10FG}?2FR>xF(nqO`@&%QF#JDPD)u8&L)JjZS;jiR{3Ew-6c%8EZ};^nUKIU3q$CNoAxbw=(rkD;#b#*T{)hI9xY# zB55G2EG5T2NPlQ+a*Gr>VJWc+z}@w%v)XKXCTcKr4n^|i80;~Ud;Wvr$oz7xyu6~$ zWKv@l93Y^M!miU|3ZLO+Qx#QuVPMn}GvqyaG8O^Dv!;9rZ~Km^tIpWziCWeP6>_(L{4# zvp7d|JY;GQrj3~}h_AJ-m7DQdxslK$VtpVSlB?+WN@IifUUjW^ z<~o&)uQWDuS62pNMPcNINqr5Nbbbozpjj+xqcFBwM*Bl@v1{GkM$BG9IU+#C-5kT3wr#Lhduz=~y6j zo}9|4v=r&Zu4hMKB+zD*T42_Y&z##=I(Kfz*^B0O_LjQ4di#3%7FlOptfd?h0I%K0f8r_im+bdtz6RlXTL9C1D5z53ceECRW8QFUUW z-I5Fg1I)tGSB=8c$wRsEg(j^-^kwE(SJQ8wDnKnZJj}w=o3V7~aPIuM>WG5gxX47t zx>f2NLt0pH1GYKtd$7FF5pLVK;iXi=t>8!B)9C${(Qud0uM zHe#idYU6HgsO>06wwz@r9+4Z7N=_^~uFCM0>loiv89rWF>2J2&RrDB|86=;nWyqQ<91EKdbUb)*+jB-(RV3c}nP2|llBy~wp&`u~A9Y81 zilwB6g@GJC!W9{`SOxA{v+y#|n3|)bVL*!jwJ>*q&rwyY-T^ha^LS;w;+o3AWVjAChD|~Gkc+;3( zGVUOs^o~?G`ig6jHtML@2=j6uw@2bx)jU}`xN{Vbs{b$-zQ zCeb?Xo{&9+*+3L6V}?nos=CC`G85fwL=aamE@eC^23annv*#nUV;CJ(%J$X~2FH;C)=k7#T}rpFVYaRPZVZX-YmD#lG1<;& zJGyB`c}f$+Z2E?1&Blx8!8i+5#YJ#AEXTV^mLR3SCIe7Cl%2iB8Lwhp;BmJU-4?fQph_yFV1d5za zQAVrC zk{%E-r_pdv@*s{QXCh{HZBY|L3Rs6vCJ&Swf!+w2L(5a)HyGuH0VP!||1cd2N1dg^ zdRj>Iqrnnv3^jS{)xbc^UOWb;)~UyG*Oga7$kx)_k&)^cjy>U9t&M}0YXgKpySu1& zr0Ri}((%jfjpAy@OqsaLKgPz#K9m=n?)x0V*MKlk~Kx= zR#vVh^tw>UQZh{mPDRpBB^gf!Y(Wn*ayGsAqzp_K#2%83yNOTsdQP#N0j7-i*F(mUc#jcd0oRV?b+XsI%A zXT=VWQ>fN9BL;!qv7``a&X@|;+)Uc&l?o3h1M*`@m{=|k1mE|T$%!$PF)}dNK%p~0 zi!lu3Pm^v15u0OY%DA zP6OBv-TH<3{=G#>IwAD2HjLLIhrDr}$?DBp>{!?$jV3#(o2-^skE+@jCrn=tF=~LO;@gOtv26Xt!w4RA#3GoT`M^` zoLe(ot!stdqO=Sd$&0S0)wve(^?)c}Yb?93u4Q35tj4l&f;4)?QM77$10f!SPq4P| z=c^(C`UGp+)m&N~q=&9*&F-vls(YvpEOAG*C#ZyGTIt%z`kYgjuue>EAyNZfc{Xvb z)(wkZx9@LYg&~@quGY0Wcesi*7ur1&HS^TguCH!uOg4sDU8&q0s8&m>GJ!cRy?dT& zI?j07GO^VccLX_>2;NmU4KBk7t-D5XE@R8ymh<%)O-+7ITIW`-+A8$tw)Egw$I{3{ zDm@}=|4uDqzET<+erE%dSY>yX+?r+dp{4oaUQm$A(5OQ@sqdx}(oVv2n7+P zM}cDelj|qW&GE=>XaC+DUg(LQ?{g!=N@G%5)H^m}=~=lW=|SI$owbqE-Aaobowaa; zVbgL{mY)+9CAK^)0gpKSVMJj?ZuODvC$6&6rBuD$6=mHUXoaO>t1+G0OT%nai*mJA z(K}GCWesfwErkSY%5P2LQ4CY!x`^|%p*)XI3v@b-xbu3SWew#XGng24xLRZBbI;Mi zJE4)#Qk=aHnwzJz^0^m{mq+^Qvb*sf>ve20gt%>9pH_11HOabAdo}18*YELys+Pyg zc*>?+nYuY88^j1k6V5ZWPOq!8-m$4WQeTwRq2cfnm+F)b9YB)d)3-pw7prtc9Gwlo z{i0|nwZHqY!4%)5A8@wYXs?dS@^J?obbIoc#+PNYnS8i;JR1j+nqJfzYq6E^`^bI6 z_G~3=9LiK(O zoq7~lT0_oz8ERe@E2XvJbmZ80N^67HpCeBfZS-wa!Ltp{1Mq!YQ%)Ug^OC2v){2v$ z%^v;>r^JAJZ}!WtykyiE^HW) zgjlw9ihS$qJn57+`Q@suohma@X7k5awl#9{!LzbPDj^&V91hf`lU7O5q^OTn(dBo@%DpC7TUMBy&*oQ#5ZdY-36so*T5GLL zTEm@&x)o*!)|P$dMbfYBTwd*RG-TLtaK@N?Mjp(MV?eOBtjNYu%Po`Ey*Nd=;Y29D zeO?5~Q0oGV20nCTmksiiR(@`kCkwdp;JEkrIas>*_CwG0o9Q*2jyCY=P_4U;rq9Zj zK$a1+ZfED#Fv?-G?iUR5T%8TWN|$5<@s!pEz4q+hlil-r;<`|p4S+pCZmF)@tl08s z$`hw?yLV)V?r9w(r*hK@Ts~d40|nN zYhBF3bl&-fhT_}n+LzFQEm}^cu9bRl?E3=bOZlxnrsGOYUo}6D5uv!}kqN+Q4 zT`zU`j>__peKKA;zA=p_b6&xW$gN1m-yu60;ixPhu~QM%>m|B!IPs4$kYl}aA1|$L z>GzZ63%RxsC8w)4Hr{f-9fBx1rhW1;ih2h}XtO0E1Ql!2SfDBO(HG}vDiC3^e3w=#gEnJHHEi)u1uk+bnQ zrck!to7>u<7Aj7V(VAt&J`dB6I^qG9HHxiI+~X>&nMR6Q!{4E8ddCTY7s&Cc8+^Bq zgmYJ8&%!_z9}UEckh4gar_WmIc*itZS=b1}aJ1gFjD4;MdLX7hgNNHe08JTg|8}`Z;(*tw<9UFe4j%a z;vfh23@2?LBV=rKBC|%a5>Px$^KF_fpURD=@n!C|@0do(YUq9MsxHV( zh!L#6%tsKU4WeZfmMb3xovuz(joeac^%A?$yz!i-JGzWP#Ji$HW7+ciBs$}>Vk^;~ z2EY)PPiD|YjF|EGE$rR;qVsU6Rv8&47-lB1^oLC&JHa`xQW8**{Kp01X zR2DX{%eRJAeEGdzd|BB}x;fF=58cLWKg2MS?djGn#m|hzvf`{H3`jY!M2wb(&_Z_WcQoIG7f++u9BF$3tu4BC1XQceGWNP- zY=}<{_BCm7Yr*2~ZYPU#3Cb@jckikyw#>=3U~~bFt&DAqpls31+S}$55b@#4>Op!% zMBa(1y${F_c&aVBGDS|ttr-()r!=0EQ>lwr+B1+D}{S zK&fryTVec3?M%XbowlHfsR5pp$XNI~ZNV=t%FNZ}993!IA{?IC#26!L5h}G2&ur8C zdpZm>buJ}OrG-4_9p!r}&F4fAU#k6kC5IEEXmdUBuKNc!W^@NK2PtFNlb*f=#RPpRcx(R9)4 z;LPjZnzp#sP%TMQV;U{f4J+hYIWuYP1*04Hh!Dy!P{p28 z-j>ovNn6*?a*ohDFe*N}*4u~grpQR?^dZ|KQgVp5y;jq?alx#FQmBbEXQI{Em>2vV z9~kA(B$%zD#=FuOYYGwft|(W^JtpdNlquyN)0?|?VkYH=S(>}r9kD8&k~%DU6)qDn z&w+41Cew$^9-B}nNoCod()uQGoa}mzCC41;%?ihm70m6OK=-_wW-jP8C+|54 zDamTtN}icC=xbw1ZO4m+AM+6pC1&j>*0+;Sc}S7Gvgf6RVbPR9B!n1p-9R(EXVitH6&O1z_J;!Ky9TPLdtC|RjicPJ( zVNyBx`h<$f0a�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ⅈGQe0cW%u5q-#m5ry0A#o0|0D7L$FxKu$#FRaW zSnymTCgcAVzfHvT(Cve)AKd34+q^pwbAa`b&4KMk_}Ky8Bfm<-iT?@tjfv>HIT5FP zArUM84f;PIZOf52pHIY7`2FH72p_m*Bl7P{iJ0}TiMR(C{$e67yD1Sp8xnCSuGd4i z9sfT8c>#7a!5{gLL_7}MfN=IAoEiTPKftmZ5O4h6d}|`QfqN1DHpmXUEfH5DteX*+ zr|v_(+>N~ZdO=)wS3w+oFT%tB=Y69fP9H0ZI{^WG3pnM!6LH!%60!Ae zl%F8pkAZVlDjL8};HrN>IesP;Cm_5dK8P|J zEsD)wD~NfU3*zw`QnC751u^}b1##_`g4hh%LHIot|8Icoj_(x2Gx&Y^hE&{nV=A_x zKHTudR2+`qr{nkW_3DTt*&C;S|9Z$YHc ztpTpZ{|AA)2e<&d8xKePzll73DHXf7AU#OKk$+Cbb-*;pZkU>g)sWx%CxnUr*Moc5 z%LwCJNbehv?>|h%QCpFpn-g(5IEM@&POmD8OW@~eQ4~Yqow6A9_#w2f?-#@s4;F;@ zK|!nrb^$B?yC8M|OSU7pGm~2)l|Ir zzXfslvjwr>xA2SWiaiJe*C%kj2eQkdpZ*-`*xrKJi0itS3ZmzC@C8hP?s52i>AQ(I z_P&W%i0->maqo)-aq_-`IRDScBV4Dz z=M~Ux{AwyD{}E-2->Y%myD1e*z`YFD+Y$cKJ5XnUec(O}-zOuCEni8+9AGoTIqgoA zE5bh<{ORzu3BPy4&%p@0hB%%2azR}EGqk;TB;ux160sK7?Z=|r-;#(Y-_Q zcKEv*zh}Yz5XeT}j5dhydcc3;U5U8%ttj{76H$9#A})AuA})dN^WmolVeXj;KH_p2 z;&9j76R`%rYp~e~ADsyE2-xnOfqD*o0lJIv|FsBzGOo8FUMo(74*m{7+8%-LOA-D? zr0a3WcOtwKkd_A!_7bG~4EVnqX_|UpDhkt!0P^uT)Wf%@Lc9y@<@i)wb7Csio`k-C z8p`h-saQ7y?c;5!m`ZZ+?|N%0jyVBx@NWkHuy?{2IMczo9{iWVIT^M$fHM=^4e)(% z2g09?bm02rER+MT+wpq^{%^dLh->De@9#v~fur&N3s{lMp7zj^`M zV>j{&Sa3GV7uXG10ob@C5zp^KS)pC7#dQzb)Yd-8dyt2VanCvjaUjkj)DQfggzJi4 zlnt)eK(_<`cY}Z3IQk@D8T!s=K9Bs`jdpM=`dj?J8twA&O$9LmoDTVQ=OJv!4tp47 z@gwxbxNiF?()T#}&mCwN_ch{G>&-~TA$i~QcPD-~z`26bRJ`lNC6mA^)R z@e$kualH)JY0scMfzyA5_VsKk2KS`mX#)NKTa^DQ^q~`|&%iYNKLs|^FGhZ>NyHGs zIP$4fTnb;u!heBmAfK`t@dMTaS3y5(CE63P;KRso*zURnbp*HvziWu=U5H=pGq^t@ zyoYfw+-uSOve9vfNLNxd<3Tqt5~^hV4N> zKgN)E{D1b|1I)6b=pR1ug5;dT5=2lySe7M-$n0zym<{aAE{MqG?#!Lp3p4i)_s(oU zMMOkE#Du7b;WZ*6CPYL;L{wBnMI=d*poj@kQ4!()tE%pEy8HCGy9n>|egEhC^ttSu zs_N?Q>Qr4_UHwhyJKsQi+zCDB7g%3WmV1zHa1i7j6gtPP z;EDR3H4J^Pi8{UseF*FDN#{d0&tXovG6{YLU2*Ux$PzpkLSK3C=NSKgO@hULNrD^j zyZ=12$pvVC{Cn;BkoP-KSA6ed*M^i>0X-#Kf-#qH}sX} z-_gIp<66LQ#eGQ;m;q@Bd~xu81$JdOa5n?U=cqwsf68eEOvIeo~p zA3D-8Y49xmo;(`+4Sb(GCk@W;#rJIVKm0y1D-E^+esBDH$^0}}I5!QxkKg0K-Hx5$coG&lvn zCcanT`>_L|3*mQl6>|uFx1NOYU4veDGGw%Kt%s=>j=BOmtrY8w* zIxG!N2kk~jpl1^893NrID+u740~-K$aed1>$@($62loQ^VG3wSUK-;m3Z3$Y#n zK63&79>&^<-^m9j!NLVeF#Yf}I2^o|^@8W2m;(UYX)$Q#qMhcUoPa-uvJ4-L{aQcf z5%4?$bob)>fJNXx2QmTu%MZic3;O*JMctR6KCedGAA`9VWxocncORYvn;nb3kNiG3 zA8Ryz>%S@q&H-%NtJ2_tKGYSz6-Qt`Li%MhAv>gh`sgHh1~eCg<_!G30e)XP0dhxM z>;>8F`?@6fIDWT+{(hugf_%>e-zV_9Yaj`h<9G9H)D^$$QPvr#*Numu-1uD$xqJq7 zxOFD-McoGR?>&&q1nRiKGL-)~%m>FqpF`W+0NLGeB;<|XIlw)Q-%hC06{z?1hor$% zXoCsRFMkdC41TvG-Tc==$3t0f!oM4!{l0`YIv4Gkfc9jx|16|?Z#50BT!gvqSjYgs z%ds~)a|HS+ej}rh>*6%{D1H}T1^wcyaq#Rv(Z2Zm!Hv-0woihw9g^UD{JYgK=Bg8+ z$1G2S=|kw-_}zrRj|A`L))?1YVeP}e@5aBC^)ZKRgE;}~>+R5AE^9z;Ph)KU3*+uA zlyd^I`vKbZ7VJgvo3Ru0Q~btup>dBj9>3+^N17cmN8oqL_t0K9W1Ypnx8dJAfLr)O ztfRYOp7{~_(lnX}G1uJ${cC&Z1o-#C9WX!PcOky7#C~SacVHZxj=pd<#@8w68=$%D z$LL49LZ?Cc>G=D2{C)Mds0Ye18~;9ya$JNwCQigyM|tNXud6Ra9`{5+`wr~;?#H@% zFZMupVLx~u_6~qQ_ABV$m!T~9E&m+)3FvOU6MIPDZwKwPyQAO|{7wSw2&6mrvser9 z+X`u)M4AJ@b2WHej6Klq4`9E8-|p{1zyBTSJW=qo-(yel81@19jr|t;7X1G9codwC z-zNBd9CXV;bM5D`w_6XgyCDfKt;E5{x5ONb?`_aN`{M6g(LNV`7yTf{+>76JA@tmYt-YzkRy0y_L2N82VFLJxQe_H`TJ8~;Wdq5U_;+Jt{s;@>Cn zZ!>}Iz~_x9*W;+`*^`(TYiY3A*_bo&`{x_* z{l+v{z6O2u9cge|3-j&;7zgb?0D>!|!aQJN)f{osPba z-=9u~?t$No6Vl)__}$t%ep04}tcp*h}KyL-Ft9DR)??Arz-t*0<&l!->nUK+2utwo~GyMG^{=Vs~G+2V)Wx$^W*kvQoOJ5KD8TDB> zgg!EgF;j<~0l$&u=u^O7Hw=9NxWn;pr3RWR+8Mu(o&4i8`s9>pBA0&-jedmnzc{|o!B ze?zx<8v9oKF2dga?^*a>4fwP8_t_O#U!haqcroU^ z_hV0jUqAl-O%uHFI}v@hiTd3*h<=E<>q+$Sr?0_2{_EJ&;ol4Kn?PTFat-Y%(AN0( zX7qv0@b97M2RELY2D8pb{qei&Jj}yw%wy;q!yWvMv9T5U`X|uW_r_Y$|0|3y{029M z4)YPn{XOVQ@5LOA-xlb9?J2AS=;sgN-@`XW-xy$u@ZJbsEM1%6TFF zJ^yvkk?>0ZKl-&8gP8j-!W=wv6#WeO_oH3@fVJh_Z-Q*`Yog628t8v$%NcLSIKuA_ z_@40)oy@5ybI)se=pn-<=P3l4}MSKcN^f>PQ#vLSL_M!n~T3E!S9(pp%>vd_BhJ; z2=+PndoKFYq4%L*-HY`HWxE!CUv>q4UqJow+y6@R8`@~P-UfIYZD5}bnH;D(r^UxKl`9rjZjV~*G!;}PGx zApI@dV&1{ui}CM!*9Q#0jsA&w^&hb5Y>hhO-|LZXI@0_Md~QAk-}gf&`!&jp-%$@> z4#Drx2hkt!n}xiWfab179Gr~b0KSp-?c(52Qxf5Y#L6nzfA zTPn~a6O6YIeHQ;F_;)pa>jT%^1~Nz56`Q9sNIXGRYM4+g&u9>PlfaPUa*tKiuw=L770H3gzRC1t&*YWK0m;G1UdgoN(Bx&w9?Aa6e%O2+lI)%w zp1dk~I(RntPw?;HnP7)>+jP5h({%H6i*)OBn{=~uR??dsmGmU@lDWy8WOgz?`CGVI zxK+4CxMjF?xOuo~cv$%I@WAkhaF4Jzd|B8N?iao?JSaRkoEIJv?i(H%zBD`_oDuFH zo|+t=>=tyAu(BqZN|q+aC8s5CNCuOEq>-#j-k8Ldq|%=poh(cqi{F-1lUg#AEKg2O zCX(S~Bw3M+CiUbbY|TzgPDoyt>>TV8yg4~5c~i1DS&%GBej7g;KN9~Y{#|@*vVF2` z@}uAf!L7lD$$H5K$xD)_c_?`}c{KTTa(wXn;19vw z>0Rla>8DSY%(|gjdrB|i<1}_ix2{w$Hg_{>4E7%>GX8p^t$NU=zGz(q8p;?qwhrDj=mY)6n#J1qOy5q zPBb@~9rZ?k4*nSYDHw`Qj1EZ;O%F?FrU$1dN3V}gik3zvM9ZS%qT{32MuSl;S{}VF zdTlTfOa)6}&p9m^2*!h2Fd7U6@E8hC4jRGhgOh@KFdQroRs_p}6N15DGH3>^;KX26 z@VelX;MAZKw1Xv>iB<+bi*Ad49&H=^JKZMOA-F4kB(10Srw^pV>8$iOX>U3^?MXip zy*;=#JSTWtaB1}O;M38kq8|r01UCma26qNO3BDGc7n~dXG=5X?j^O;@&C#0Rt5F5> zOB!qvY#f{xofAz9&yL<2?H7DEdUtejbawF0VE^Eq(fgwhL?4Vc3O*NI7EKQh49<$) z9b6b(9la&`WOP;ZvFMWML(%!sjnM_sMbQPpdxDQf?+tz#d^flw`ek%`bXsS7gu2*>y3r!du8!QZ76C4%H3+4o~ zf}?}J;K-mqI3{>?uqfya76kKyxxwOKcJS-yk?7&*q3E~K1JR?=AEQ4E4y;mBT8B zSB|J0QkhvfsPgK{t12@p`&C|3IkM7IIka+cW#7tCl~+^_tQ=5zd1e2~K9yHi?v3w> ze-+QI^j7u^_P|!;nJ9|?6FnD&@%Hhi@h0(R@iy_+@fPt$@rLon@mBG6@p|$5!pp;t zg_ngN3O^lQ8eSZJBD^U4eE9M3bKxc7XTpz!9|%7gzBl}6_}TD-;itk6hwl&nCp1wC@fGps;>+UIaWj5X{LT2<_-*m|@yFx;iQg4}H2zrpmiX)O)$v&T+W7c* zNjwn$AYLASDP9$i$DR1o@u%W9#Gi>j8=n@p<1^y(;&bB*<9Eg%h%b)c7r!??EB->f zGCn14#l!LC@#*pBF((!;buuAeNDP$@I?Bb^v~(D=@ro@lK)9s=@apa^ptcw9ZlP5 zC;ff6W7=2wL2`4_mmZV;FgZG%pUzF^rJE!_O1=COtiUL;A+(4bj%YqI5yJFx@!WCA}tmb=r!;0DI}=;^2M3MZx=n4+O7EU!R_s zo|Imnd^x!?xgz;Oa%1xK1Wf=rC*MA3wICahkJ+ngni+j;o;${!lT01gtNlA;hb=KI6FKvoEh#FzB+tG_}ZkE zG?QJScT6Tx<@97_vMd=(#-Sdcot%=aPSVOT$vMeek~5RHCTAq?4wodGCzWLVoD|lb@Khe9Q z3!^RL+k?+WpNYEeG-%5X+-jaSh{Z9Jj^egFm)AywprSDJQlfEl`cY1O9q4b04CFuv! z3(|L_=cVVT7pCt_Kc0Rhy)^xB`u6nPboXFRWo0xOogS@@rlK{`swj@1js6{N9`6+I z7;g~o5Wgf|Ki)QePx$Ki$aro%C!QT275B!kiD$(<@l?Dfeoy?7_($<|@tfnbV$IIg5;+x|W<8Q^6Cr8BJj&F#E;+^AN;$7o?3fq8CLc&HNtUJ$rw^qM zroT=fO)p9=Ox}~cJ1Oe7(AMJ*2OlhGxt9i}p8L_@Go`xjr!8%F9`xP$(0HE<%XHqm zmDc-cuwHm?c#om`J`wyk+V8(S`tOm6)_`Aa=)kXm7W^km58fu+F5Ds9CEOMo@k_#8 z!=1vN!|m5eH{Ox8n6?Vc_cuqJJzAijDJTrW4I1!#5HpBD7)52;v5-tmCVLj}wssBDU zb@gw1wDq4rU%#h|#(tMqXa7sMp+|53e5vMsU#7eNu8a1bL{a3|;N$47=c&bS5j`IN z_nQ3rp{dIs6>S`fKL0p0`o-(0(=U1vwfY}NKaGA8-4cm*f2-2(?}$mm|Kji!jA+m4gS0R!=Jk!`wDxGk*E>PNbQ}+is+JHv&v9v?mrHWi#w2NT1!uU5%wW-ygQLEMtUz2(|x@j zJontL@;!V0Y>(po8U(ShKGf^l9HUy$3^AU#*W^o^6S(eS(PvMIOLFyD#sF`uG=_2Y z=a~AMRju|gZmdqx<9fJ9iR7I%&7pbqCWTa|?^&(ZSe1g9QoK$E@SA@W7GVrwI42vU zQ>`hE6@b^I9lTuj01uS}T=FCa?L0(DZZ#;>g#qmAcxkhsh8bJB(@6!?k@7RdV9Viv z2rc})Hr{2>!3{v1K|wuK!!sE~!sQ19C39 zy1f+Nlx`?jdvU3ZGRz@pcy-m?g{m|K@`3sojPWM5>ZdtV0OS&N?}9pB1sv8rnX>1B zs8isZA`|5Uv$rk z@S@eu5)7&Ct$FKjX$NK?59#Ct@@){P$3z{z99xWLzuK6LATvc1_dx|$;C4*2wW`Tb z4qq=-9#R2wx$t>Ws6|SurK8>E#2Vu8C`pgui6-o+3GmP)DCy1yT8Pa|= zC_|CJB|E0XxRJ0A837>r!3qSkE`X^iWI0uNf!+tRAB$9sXkuDcYiFZarYeGD!;vR1 zf^aYdC!HSUk`Bwugc`G)iocC8mTD%%t1ej=5xP12=nYqmN|a~bZt1@{d#x@I2{1;B5MAHx+69t zcO|0FguowISFuoxXVmIDZy=^ZSuHB{?Wdvvkd0_gPHFS}p^D`n3+i~^Bg>t?Xjlx? zPn(`WRZ}x79lMu5AV2lASVjwwRZ{islvFR+%w_2WDTo;dtWr~=-k}nht23REC=`f8 z+caxqdYLD%#FlD|0?gH8m^_Fo>73X}SwQ9mwaK9sd5&}I_|$pjiXO3(FV zQ){$c8y{2(2QgM}+7T#MKM8>c%SZ9x_8Rox)%|#hgfuIMqTb;MUKAijO){Jix(lN8 z>GGftpv=^*)C1?B>4qagP^_jV*0Ob_v&-en(c~)BSvM&oSFvFHWii_w45)gjSF>f5 zID?sauMJU1{PU)$%)L{3^t-miSyJ%RGyQlppc2Eu+%XiCj&Y#wR}jz^h@6) zEY1A-jW(S^!$j3q(1oq0**OYQ%V!);V0GfG2`s3=-h$h#sjKId@-;Em7^1j5QVkPk z&mShH$km~3Hzw;)m}-rV#54tSghr|MNVE@(M+6A5VG^JsgVN-vH$)aNU(=W)(Mf5j zJ=I2C#s+0bG^FZfEHpq#LDOVG%$X_uham_wU+D2o=O2Snx@qBPY{a3DpJYo!)u(<9^2WLka?;Nza?7*b7Tu$hh{hjI#63# z0AWYeZjGqekDLp-IS0(hs;5dwg_Q^3WF}udROfJ8tiV*W%4;xHx3@MWrk!j}U}(Of zwBT8o4$e8_gYtOz5Y)#4Y!$6B>XX#cs=E)bgQe6W1d5IR&uPxBdIL5(*N z`8Pe*hazEBtygD_VO%bz=&2S2RiG$z8a5X%au2rhzA9-=D2+tSTQYMB8fBr4YtDt* zUyaL*g2cX%L+0jmk`ErfMy@$jDRq#<>GdYcr}bz(V^BxxY2r;UUiNwTm?%83C2x43 zPTJ^V7RZ1p6|um=7``TpV^)ijj63EzMyOBSsPzE=rrCL>BSaBK6$ugPl;p}+YF)~NwFU%DZFrEpbdMa zVO3Rsx?(|ay%p5PBpy_fI)*IGWO~UZ?ZWc$rZLzH(IcFVNkm*UxX`1_)IAHVY=kTi zikFQlWM!kNLKL5nv_$S(8IwtGflybWkSRz}1G=C#+8D|Qje{7hu_@Ys^5gGDO^F4= zXg6cPB}oAwBd#D8p*E3)`U^=(N&)xRSBjxlmDfzdxUNhWws`(Bii|GRs+rvZA-GrP zR?I$r&3>q^40w8|F#{W=1}f`!^5_%nIfw|BSxE{Q?1b?Uhp>WDXeNTh%iviXkkaU+ z>6`6MrJsi>l%VvznX=00&2fQ~tngVzD4HQd`pQz$#{l{-m6V=&uJ-Ww%0D~VVGN_G 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≪_Rl3g)LA6Pj%5MQ1(dk}FMRZT@g~K@nPuE!;{g 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®SX}BfH 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 Q?G$^MBU`Xyn(B7{2gezW=Kufz diff --git a/src/external/windows/lib/Win32/python311.lib b/src/external/windows/lib/Win32/python311.lib new file mode 100644 index 0000000000000000000000000000000000000000..ca3fb02e995a91049ef7be4af1e6ea063aaba605 GIT binary patch literal 368002 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

@bUN)-XR@Oyg@!`LniB}UpZn`qo?cl} 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%0D>$3?ihN2(^sIP>XAPLMOW#Q=q6|S(9n-Vo8D@e! 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

eN`>~5 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%0D0@PMB1ka#CYSlXT%z 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 literal 0 HcmV?d00001 diff --git a/src/external/windows/lib/x64/python310.lib b/src/external/windows/lib/x64/python310.lib deleted file mode 100755 index 8fc361b733cebc8d136fb921e46488455cb1d54e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 355924 zcmb4s4VYX-m3GyBL<8C5s}Hykby}u&P+m#NT+9}C(~r6d+hE> zGFcYch!L|a>tdEgL_~}c5s{w|BO;3|vXRAz7!eUMMvRCNSr!p7;`g39^?&Pj_~v=u z=T4vVo>TvI>sHmR`?sf+8-pXKPkH+^|L<)l`oHzB8S0-?X3jiimi&MD6d_)9yC_`I zDGFDv5rt3PERq`(zI?1m2sg}@NcXH0j6-k*et9k9i91C~_~i)_$!&KDi5sVggz)97 z81LC962f~=XPiDF5{aF-?iA^w8e_$!A|;H?lt`|;STGL5^)Qh>Ho*AF1tJBKPn|1~ z{^S7sK0p+^Uz8}kFfJIoaosJ_&s?nVJg(1+!mo~(D86z)5YksHV~8h3N?7-jD9$=s z6yJNfMEcxmf{-lUtU&*tCJLWESR(0vP)OW_E8&)P66w?T3Pv}s-6FYtt-^u00xPd( zR7XW3@hGmq$a;n6aRtub#uzySzk&1TGgh7edtk+NjE9zrgz(@##&@3(31P<)iS)T` zNC$jiTo!#xIy7?T!HWJQh@FEfgd1$2;Xfn z9@>uh06Sk~d^d$0_$1;(SiggD*<~UjeDos5zuYPkAbs>oiDcnYA#pL{M!0Mmk985{PBgz(SH86Uq`B!mqG#>Z|J3E@(tiEznA#-*?){PQD>j~@&>;F6;m|8fT6 z18hL~5&rE;#^ncygz(9G86QKvApGly!sECC7hlglHPlS&?%lH!f0)@{{Wqb~8mvHTNiR6o$1)=b{t&GpF18GF%B+kVZ`0O}i^EQ!4%*7SBe!aqBxB_<`!MJ5A>OHV^yF~iy2LvJ6 zx{Prr>JQ?gX^b=bp3S3#pjBY@R0`Nqq9XSf%fuI z;3In&7e5FdaLJHF;Y;u%u^d+b{T(6w*d+=t;QE3n+=xDhaQ#yXYjFj>GQdDTTL9Aa zn;0KE9Cn98hH@l)`8dXn2f+{FLW%Sb%LO6%Jn|Gse}-^@WE1KX;VzUFknTzqF2WV~ z8S)iK#6gTrsKY?|_!Sa`zaSkz@_CdkknVa>BKg9QkPsKZ4t?;Jr4ogkP8EdYb6X_R z$B%;_;H;Avdl4p(d~=6H`U}(xAlZa^4HQqlK%%e>`EmPv#@3CZK=|5$3P|(U2v0L^ zL;gwN_iezIA%&f|0=J_c5Vj)k2zMMT3SZyE*!G|(5WbFh1IZ%v36n3PkGYqz2yK`! ziS{pX8Lq&ZTO|tLd`YAmj%R%Q4v`Wrc}Sx04fqApkI$DV+6Ul|S71BRBe4e8HKN$JjnR+1A}qdFVHvJK{|1F=xB^Q~kVqfC6zN7iXrWJd z6y?#qTO_mZ7Rk3LEoX>yLBB-d$9D*cosWwG;YVn5gdg6+c=#kyApEeK@$hx{4gBb0 z*g^KA=S1NrFNne;!=muSbo`IJ{>eK0Mq1E^Cts;aq?d0N5}!Omq(I?uM*8n)Pe9=p z$P*yFYKKJO@qOUz6X`~wfcCkuAqtP7e1Y`STO|s+$A!et_J{)Emj%YIUQr8nBBnppThV)$q-|HDyZW5^k`lTz8k3YX! zBK_ng$S+{yUWvjl9}~$vkBH=T(?#+|^tFfHA(DfR1TGcHYmOJmU*rCJ)J-CJ^)ith z_L4}By$8QhpI`g9Nd6AI!+S;Yw>w1g`caV_xl1H}^PEWj9(ChraNc}^NTw`={yg}D z?VGQKJ@~I2h8%w0*bn|HAQj2Muzd~6_n37enRWquUk*Ptgo!?A+Cd^Y1pUFQ9}>w_ zw5>N?C6Xh+ebr2aIRabsB}YsH=M3nM7Rei~NBB44dMe_DxV-K<_??2ZodsV{Bm75^ zu4mwTD&&aoYZ3RU2;)_-n~Lxc$Gv1T@)jsgT`!Ryio7Mf3Vl1_)hGwT>yej)gJDB> z?a_?Ga8Dro^+LvLZW1Zs^^g$`p3j(iy+{eKJBV@65|I)PL470~ejwx3Xk&!eUduRa z1N;MjjsA}?bvom9kP!|-zd?A-492S-M!bPTW=SN+p+6=ZznpO#%7^fly^P~y;Y$+Uo zD=-V;%|xA%xC~d6?QOdxl6PXPLU_jvg-y5uC&5PIE?j|kB3*=;`xx(mpP!xaN2anDTj!J@E()};a$5Jr(Fv@aLV(HQB{9!yhGpEjZ&Zi)0?@528`@9ZTQ^Gp?mYc?@Jb-@hR`fHso*`Yytq2DwZB#f0*JEIdz6MCn-NX1`FTw@N zPcQ~&B7C4iVFE)hF$Ve(AEb5r#f%;IBLD6c$!gSzhn~c<0q$u)vU&+))h>|`CZ1!g zLfV1C%}iF7+MfxM5@D#3u3nUWM zb%_t7?hr1TrEmkTz&|1V5*u*^K8!j;_z1X!f9PafI4%+i#QnluB7NjsiR40o-@rPw z6Nx>z0&9mQ(jTKQdIa$Uif5fB(jRXX#f97P{|$KVM87qJ@*z~Z8ADItH!!%F(Ktw? zgpr*R$&F|mK=EA~fQE!04ZRihxdWDC4>-L?VH>UWYjUb~$4&;s_*Lu2h(g>vT~(brz%ZdQk+@ z-b)zegGEaC`M5-J^dywWDC6=O7~9Se#WPZgF=+C{3?+=jPfS@ zXuZPWxB?H~#CYT$@PS!KC*enkC*db}7JBzf82i`Y_bG|w(F=sckFP+uz)$XDJc@9E z^!Zg1$)nHUH}DJO6=65xB!M*UMqa&ehD7r7-55^+Pn^Q|MK}1}m_OVh(iH6y<61&m zxRH^p7b#H0*p-kb&oTGzkh;pEatBymf;E zu247@SD+Jhh;RzzK=Dn8KjAHxOQf@p7ZPW-5HH|e4F$yiUBI#962+r82|{`j{1DFE zBT;-i;sm7ghZP>d6?nrFj1y5mf#Q+ZNTh#HFZ1fbt`}X&vL8 z$P=LW#!DsAx9t^-HMj!P9%szDTckko@CPK)w?7~lOK}BebxRbFLLCCq6CP6-!4)|D zX+{_P5awc>KzQ%@3gEvNdH06d66xO}k0kEH6?oGNjA`&iIOQ0|$;dmxTV^nh+lKN0 zjz?ZdT#YMmB-$F`Z{UmY4&;->i?{-RgK{FwxR-InPLWESiz{#}@_}$F;zc;^1cmEx zy-pNopx#QL&zW%u>e5M!qwhj_0jKU@ynD7t2}fP2a2&43iQ*CHHwe>~N~G`J3f@-q z3CMH8(K98|_Z%h|8*l|qU!#!X3d}oAqImoPXtxKTZN4B;d=vT(Af1hHf#MtBo6vDD zmHF@bvoA7o)+m(@SO4~+~*CFXO~Z&>vkQ3j1~`)Nlp< zyp*x8fZxFHE@S)|^QJ(u0{7Ju9uesR)U}_U&Ug&*A`BnR=-(nz0_MdfzWS(0B~ZV< zIwF$fN=Ax%JfXOQkzy<$u@_ggmoK5LfTXaEksJ@Z<3)Ns%AN4#O^h#{3LbFdG{z0! z5N<^M1d;=BuLjbaP&a|(z(XX`uOPmJFYc5`UNI~P>GhowNud+r0q8S zJbfJFPg6yK@VkQ9wdEK;hSSG4^Z~ z1t7ip7>U9kE)tCAa0Q-!UZI66@cWk-&!D{$o<%r>XP#vI?~9@!0p0%s|A)LI{2paZ z_&@k0JO{rL$b;vA-@q@BUe}N)?CC_j&=#(}g7G=DAHru)HwagwEP=xRAx{WTAx%K~ zxl1Gpze3pn=`}Tp!jp&}kbV|z7bxr=bg*=0Az%*sf_o5PZ)bb0prRsV8xw`58MYnFpBm{s9dOU7_Ptw!k0J( zSKxzd8P!8Yk#O!w3RmF@%sGNFd$TAKP8(O)jq7fa_C3t#eFo3N&)^yPT1Geedcy2k zjHM5XB9Q*e`HY+I$9ykvKk^JHPTL`ooH;DgHONE4B=VfFW*Xx{^tV9a>Kzj4>KlZ_ zKi(%&pm5Ex66uF=KPOyxBV#T4M8YKEOZW%GSpwzw55R}FN)$eGgCL|AoGVedx*y^6 zBaBlR7a@G0@R`#k(zUzb7nnr-AzbjdMB&q@J3#u6umuXAUMrC5F$TLukx*Wz@I0=-Ao7$j zv`GQwIs`1goG~yAd)#{#v_vxZQbs49bqQx3%~*iB1j5;vJ0x_kVRYf1Pw2+IiE#D? z##x_$9dI`8uY@xnW6Xn3Lgy%B9{flYa0TXE$C!I4;&G@*@90(7fh+J0^pQX^|2~Bt zTzf?NP2`^h>c=;MZ=fy_?wlo&blr$}q7K}t@O6|AVfj%QGh89k>S>t6+k)%$sKe*s zK8mn=j}+;mM@9OfYeafJcy)|B#%E)059Kp}dbaWz*u!?|7R>4FktjTHq>$KwvL-x$ z{#gR;{{fWYWlI_Vf_?}nd>4I?#BN-H|3M!nu?g2r;NCA$xF7d8AYCRTil>YS#v`~Q zUiEV&iZeTf#5->gMIb%*n9!I$lc42Ho_}Lu_3vmT@UCDU7 z1|IP90~vLcA7N~r!U4DfqX#KmiYrhP3K)OX2x}#ZXCEz+e?Z;eEX`7T27xaXu2$u%2=#MO_Bgzy=pmv9ZntUz(a zLXmzDZI*Bz>JZ_B$S1-FwlO}4{3e`xoJ4WKEmI7Ji*RrJk-j%5E03qsnDc1IW)k|=f|4M2L%!`&XTBiP|68K4 z9oOxmcp&r=&*BQC_cMxTh$109i;+Aeii87ZGKy#?1hIkf3L%OFfqNm4zI3|67F>b9 z9Ivnm*G=G}t`ZXD7omW(5MF_PnNWC=aUjx7_^T@vrs0Zm`|euC_Cv8ob133;E8~o{ z=x?CA8})+lpU4Zsw^26;ccWYgpP0e;w-ZE4_{2?&f4ddGfhTc~CH(v;iKGMVn{dXF zjQ8Fs62cj1%Rsti2jkBRMJj>({quH_{4e-~{(eRu+8d!4ZC>I6T!H_+jPX0PLy7sg z0?(k$6Q15Fk^U(agydQDDL}e!Kq7g12zu1PeLER1ZV)Mu{Prn{^u>em`(XUPN&!B8 z3;Y!Q3*oVu66qh14j_4KlSKL__ME~IxDqar zNbZ9l0RG9>tjcQBnA9uLACmzlH7Rk^ew( z?m-gC{f{7?z;~XJNKyX?$@U`^5TEV9f1eq@SAOnU!zVE_Mq$tzlLwZFJEHp zfgR!3FG{3;oGu8-FE3^MY8k=>euHub(m!4;kvxfV1JW1nkVu|ggLVY`d4RERok$3O zf-l00r!)SrN+g7RXt#tvKf>5|2z~>9x`y%Wlkf-p{yN4VaGxjqaXaJpGav_^yNmJ1 ze#n9ULtG_x;0ipCvLHM+pYi+=_zk=;#Q4Kr_yu0v!gyg9Me~=wnpl;cbqsw zlu8R1cXwwg;=_i+jQ;tH7oJ_3+tJ@4dL|bQoL?DimCl?~>S}gYYvs}E+R6}oRma9l zd0JoI-zX2R%(2lr*J5sEG~!=MU0QFYiKr;17Zt5>*$XSHmy{bZw$=pf9IEg_no}Mf ztq;biTH|_MkjqvYL+e87NFGzbQYQ91X zkovw-XJ1dbHDVlEiPUxswYp5U za^ak6ZK%GwIj3H0)<-K{i2p zxyvL65h9BXvA0ql?P~T-3=EEzn@+<~oBX&jmHpxd4D z-_V5Za%9HP)_J>NLcz z+EB%Ir>5<(*yEWDb4VDpnP2D`TDYCs!pdqq_Jp~^WyOvJ+1Ti-(2JP1b{o%*(lO~Q zH*F7KxKiq{x~nmklsc^biE(FuVVtqp;bF|!H(E7KFs5;2s5UulqjA{qO_eEI09xU2 zRfWuQwZh?|8s@~pXnb}&7OFP8b)l=)Gm6_?Z)LoKExNkXFuCWeoiLOFRcWw|ol-k? zc>yW8!_0G?^%*oq6M54bjl^q?zSIqw1|-?M&@FP|#Ml7cbnCOrP2G|AO!`vuyK}Ve zcVTNE?xsi0$R#h6A=qssV_0*fyR&`Y-SB=~-7gko3V@xAAOjT2R>Z_gGyGgLD z{j3WudMjh-C5LjYtUk0{P{xgG&4P*1R&{*T-tGKA15(Dveyp`RwCfnMBQ8Hc(uC~( z#zbXqbyamJljSPg971S|T3m0mHFg=HHAYZ0I*wEcStLds+VwU^s>69!N*UU8x3f{o z(5Ax~xd!leXmlZf6Qf!IkzESil<3_jO?w9R@_@!AU3?t>gZ@!ZMbgk z4T@;w9uxQLR&~WheWFPt^C9PX!!gT}%rniL`uODBa;t1YQ?WO48(O12W*r&64#w9S zD;g7YLKozbvI@0$iY^ZgmFhLzjO!)3J>W*Eg2Ih7s*H&{+u3xYZm94vWb3J8p2@ij zWi^J~MNj=Ix5M;%I+MHb&;ATuWv^8jZb$Q+MzvLm^L?dbgD2vlJQGLfSkPQUo`s_` z7P7KCu5{zp6@`X#j@q}22P9o8mEo&FeDHv&Lh+TMO_;m(c|e)#L4Oc;EjIr25I*2( zvElL9cRp%P7__S~bUc7VuBfQTDppEteyb665V(>WmN&}Q>7xf>!*C|70vRTMF!NcO zi|NTmT@RfQAz!L~W9gyVbe=XPNeewrN7H$T)b5z6Rf>wR zWo*1PSwfb&(=`%K9HT5J^uSfsv=wQaIp=Ac6 zvba`S=Sw^M1t`}*seFD zjbhWPg`H{BOfA(UuGOd^=zYCXZeop#D!fwp{3w%d&{TMsInSvYuEHhHH|T0^rGYD23a#hPx?oM~W9EPBcAoue|hGFTs~ z%xeyo&9a0J!dJzmUOmU1z;&iAc(7)cmuyzbfiC2Rh`bF)cBbHzBFkM8WC4{Blb3j& zBfS|{38ip7=l4&pi77G6N42mRgurHrT4xYv`#u8Y^@+)1KmK z7gVq-V5n=zcYwU*zOO`qgq(=dH*Z5QswG%nt~Umadz1Sr;H=)5x^a@Vu-`VENVGL2 zVy{)Y`em#&A!6I5pFHE4g*r2DRe99OK$F5y9&v8CnHV|y4%`Z$BN|DGt|N-##7k>T zXeMSxIZ zlIqHBg3Z%sEZYQ6Dy%ch+j`06IU=L)tBekt&=o<-3?mcAytvQ7@K2>3nMOa5+4I(5 zzF!4^&L|Ko0rEM1z-_2iMwl>eoJ2+@nKBZNSbN9T8En&N*{upX z!e~rJqsYkDdGvVgJ2g_v$`GhVQRaY4cP%V(Evk)9dfnmv^y)&(5z3md-S48Yl9iJ(Oer(X>Dj6&7s(Pvku19`4+ipArq^_ePXaw z9?+gqWcs)KZ@jKM2-e8oQuCpSx^C{QVgXw<%&LMilLD!$RrKBzR>|K|GgKYcZHZMZ zkhD!oa~jy7<4@-xM`*th>(L1hdXqiWL}^hvqM%!IPousDb4+%ZSkI&i zB4U8HhSnNUzzZUUc_hzKCPv1$*W>6UQ7_apX(L6y^_{?%f+dsL)%0fnJ)!1M3*tPp z&ZM!UaJZbrELF)JCTDAwspJmRDV{9dRXR4DcUgKUM-kG7j$potvWvD?`Q<@UnuH>Q ztcG`dW#wc6Us**~(py=T%T_uWO4gFha#YIDM%s)K&#crcjp`tZhEitoN{uc!QmgDU&c4A-DGgPyOUIRQU$(<3i8;1~>niBQ+^jstbG_kbE z=}TRqrciG+T(-{yS-Fx>M(EpvCsIGSb@(w-GR2_w=Ifmb;?8=3*Q8L#9-8ixR>7IbhWkM3A^tSNI z2(u)s*2Yq=)N`=jY@d>(xe_UYYH(d)1#sj()2U~CSJA;L3rFu8`Z=N4wia?Da;$yt zs?|AqAp(w(Nob-o4Z@5O>3j9UK!e3gey@0 z%f}9SLco4i?48$*T|3RHmEGLRz{CoE;Q+I{E#1LryBsxUzG@S}EgrYgN~2jB^6$O# z)(qlqID{8fuw|}Zb88ozK#3WZ`b1-}a!$EXb5{tZp`0m+X?S@&>y4LnBCg=BGEOlt zMNa`B!)mxoL)=Jj$~NmRMpQv$$z6-Gvcb!pB>b#w@i3WJqqGi$W0(x}sx z!Fasoy*6vqfn6sfW0h8QFsq|Ixbb?`TfwU_^a@MXs0WkKsOS5sJB!ekhDYnGv-Y;i zqUB)5!s)I#6R;Bsu|hWx$%4hT>L4wTWsLeJ(LmS4%1xQ5rLc^lQpYU2njLeiXdsx4 zjMPXDF&mS4wN=$dy*7p(Bcfc?nH6?Uv?dzS2HMxEk7L}`kWJKU_>20oer98VcqNJ^ z*6LKH>aN#UPK;+w-9j0jKqr``EUdRM!#i4`gvIppYA6|m9?MBeM@MtAHW*I~T(`?< zdSs5o2a~>8X54(u#l*C&d5#2Zg(XU$ae%~Yn2COVJ!ZNlbo)}86UV)?3~h#4?a7yDFE>1r$Fta8?LL461d1A%Ka zU%YqN-@wZj=YG2k}P+VBGI4N9L9Q(~KWChIvk+9OJ=_WuAv9$9M?R zg3Ne1X)!e8$Bn1a2Gu&sF&^A_=6Q&6jE8Jmc$(1+TjB+Sz!cAT3St^}6DJuTL0scL z;w0lE2tu-POAHf~M>BqcVDkLr(d4L@pZ(-ic*$EXHsg0k=UC7^1VayR5#E@%O4kNu zl5gWET^kC=x!*@su8`d1@Qupl!#`@+V8uC1AXn`w;H;fpRNdFYLzsMTfae0Rj|Bs% z(jx4?p*2NQ0eoq|3aXsPH8oyJku>dkKE~(1`%tMu!CZ=srElS@uxy5gM>8AcQD{c7 zg4?;GV&+Nm)jgB6gStd9l*>ldaf~o&!U4YUNTR~TxUN0TtVML-up8Hvi8N;h_UZ+C zl~TF)H<;U(+2$%-ZY<*^*Uxp7ytkY!eQKfdN2AzIJ%^Sfqk*!TG6F;XT3j2)?!!}T z76oFTH#5$I*yrg9=d4)sdSam}q07mHNE|0T`k4t$3ALY`i8T|sj9wdNmc2Q1j9ByJ zT)MyEotcipkzha`6I;;g|A8{abf0pg30>;q-SJ=Q~s_8}K4+J$E z8Eoo*rzeGT{oC(re5s~ovYGGtR1DG}qt^;K|JLlU%ku*CP=RecmF5$#zs{;4>Yggb z&(4l>K(3+XXb*Ba!!Dp?#GU}%-V_?}z4f}J(G!b1L z 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

Aaqzvdi5oWzgJA7I z{M&no;)3irAYX%C{5VIw$iGJf8_n3eK7`X^J5E!mP4VOBc>X8>G5#JAOubRk9U#HM z2KW&oKSf9IBZ4V6M)syuDCD>%6wE+DE;{tHlI=F6fW3gA)9PUinb*0-xLQfDR{A(lppYks&pWuip#uk+Ky2&tnW1 zQ3c@&iku|PbfQ6`Afwq|LkPY-{U`qs_aHb_zR{m3Sc3n{<^FjK7c3eq_styWUDUsD zaKWN-R%&3mJLE`$E*)mwEcqJtm}&b@lYNJJOcY%3&-4HIeJ&ssGYEV=W=Np2|BW8A zp8`^k`J#G}f2qg(FZF~qY2B?+j6#{dYPlV(zB%!wowsS}-rpkw(bRsqb@|fvSE8aX zmA*9d-x0wP>Pwrh;6(Qu^&hoK1O_KmC4RQw&rVx9SwBOcX%$$BeKCe>`yZhEHkXML7g;zCYcHyyk+iz=-)Bb93 zo}z2@{IjRtOTvrnoOqMc&LDWz-}V9O(>C(kY5e||c1M1(C%+yeu7lNLc0typrk96*@4UX%PhwcFzCb)Tl%y^du`_bzf&2i}dd>SH(L8r04=#_jsu z^w%`TttVj7xj|&Ct?eetWNcjN$d^|z-)t+on5j2pwD>5 zorA7>;}&Mj)pmc+JoUYO@A=)%$Zta{KVd+VwRNg#m0=B9bAQhWbl>1H zw{MSJH!^db4T~A}@yKn;%ENwJlN>#B3Z_~%f4s-mGaz7)KldNe-#=I z;3i-R^FJ63_RG&#{iSdIWjo~gMkh}Qmu_kNZ*x2_pW~+QYuvpPa@^d>k-CuFHu*1e z^=Tv@+2GHAB8}uto?L}=p_NcicB*U@Wlj38N5*@8^@N*9##=af!|0#S@YVMu&$#f` zQ}4K+#)U1N96c3$P4Zi~^oBbrzpd=A#W#VzCiyM<(oY_w{I<^WYrl41rbnKGLo5GA z`E8TQ6Za3kR$rxO&V1(Udp}3x?6#i#UYmaE(_P=zyzds$U8fkizDDDKXSBPp_g(kU zXt!N1H`EZ$`p$>=>idl+&-1_d)YFvb_C}uAw5Q-3xLFmh8rJ9j+KjNr?(?CW>FeLY z$g^;|eD$^T=spL3bGO?__u0{#;}WNmR+az55$?$sFSv#3xRaCPYcrnj?-^4M`0!2t zrW|*6a(r!O$^$&Bk<)wE`{l04aTjlnuhCcOnJ@RdZSRM+M83Nk`BKnSrx{;$&zbJY z>)uO0@OR29$6eqHC8Gg?3S=g(-)*vpfrJgw_=8LQ%J^5uKRnh(AF zKy7UJzs=F}9p1j>Ru7U@U>_$(ID!1Pxp`L0n|Z$wkSD}0AYq93=Sq0x;d;Lvh6`UsOCyv5;%vDxR*Pw1b{o;_F zZ+eCFi~U61WL~dH%{-R48xQ>Ajg;GTQ8VmTvWl4RCa-$r^jYGbe&6$dqF%YbsGG{o zQ@K|wNuQPcs-5?{oqEXuS>4Fv)NB5K^|kYiXE&|;o+oKMJ5c5~0T)a@xV{jVAlHN4T=~F|m4lu8 zn&jt^?4KUI?Z+h9L)`rQhN@TJnw~M{gvodPfqKuOR-PiBlwGO+pd~$H=y&FP`*$SM z!|eS24{zrkA64=FeL=9Gpr{}UYAmP-Djif5LJC9(1QNQiY?4c|knDynkfa8 zY8f@2d2(?Sqc#(%C1e@n_O<5w!)TGG$TH9hmGRibwX|k54|;Lk&jBYcZHM)N?v(C& zalu`T7Nbk3Wo1zzv0xBNNUiSw<}#6D!z8OYC?WOP-D@+E;=&|m47OzgS>Xo@Zd*d< zHSzjf(ddDRlD2WM5*@qm&=In?7CI@*>GSEa3$wR0*2aSFc$hfmUsvJ9&`R`rr3(9_ z7`3H7l3}WX=nU1L}iPk|MnXmIL^xaAZLwlnxy-+=f`-m#%T|1A7ukDTT|Yw& zKuwZJlhFBW&J7(e(D`gqNUk*2Ks&9EkJ(@*OTEzBfBbrkIH>Yk3id7Mzi*jOYfw7_ zUC&XPh5J9mW?(m+l+*+MOvw_UtDd0@&F{#NvT!rsJ)da0NS6V_@t}X_q z-dI9!tZrhMRrxB2$fqYoPdY?Cy{mypfp&53$Mh9f!9{EGad!a-A&BMZe4m@L9BoOzjJobKCy>^$<8V?XLfsb znZQzzB5k9{BX#LCU_LP&7A9}cVc$A|oaMB=SBTlu%w)~?)U22&a*ZDJ)h>!$odza* zcz|{DVb7k}ma-wx+Lyjqz3mA`Of?WYC$^?*tn4D9N*hHrtM~uaa5vSgrUmKz#Z*Vz z96HD37w+PE^`h%SCcQQPx(e^m2t*4v-TB56x&)D4Jefkz=c%iuy)QsIeY%o!6X};0 z`hqNW4JO(bL+=H#{}hn&b_7nvvKn+Kq>7bZCMKQXDx(4&yY|9s>&Q!GSg88VUooIC zjrCQ`Zv!CdjSqjlog~dPF%z<#8SF;2C%|&dB{9C)51iRgRF{d${33fVLJbwgqGP|B zI-g?E-ogEZ{+<=s%uDMKqG?B8<+MIQ8RhQ)ZDQ?LY$w~ZDml7_@`To=9+5MFBbsM>~wAz#r#Ykgc zeTG*bd5kQU<>I%!O@k;unwPaLC_+$&HaP}(lhKBiLQvgNdo@d-q8hZ8}Gq{(PcG5(9f-L$9h2mKE} zNaHPWQNm4nbir;hzIK_p6x#=iWTm4!))QDo)*0E!+piBlM!bF^Pes-ZXiu%m?Rgy7 zU*z$=3%2JFjOci6%3+c)S0pOPd83^0dzWK&M=@TWQDF=A>=PGDu zJoj(ty_;f=0cKh*Yx8A>NqZQyCVu@$#aXl_4m8oM#%DtRA0->|%4MFle8s4lNG;_h z1G~&G651T5O#YytWL^(A6p<%^D7@aGH?C0D`PHCYj8sClcAsnF0n&}vxJ>%dQi6|O zDYdH`DAI45Flimd+^-vGOf&e?rTXEPZ7Qe)xHDnRp(sWjY^JhPPpiX})k@?qYaCs- zmw0cOdF;88R7+w&1uwt%g6+f`BJ$`d6k$N6d0@pmb=kX;;-xpuyx^lN0#Mo>(3*bv zx@pU3#T&})ff~NEs;Z)g-Ap3=AJzm=Us!nY;ueaM-ZJvM#8hF2D4R6+!zJ5kOy4#U z+1*hDlpj;z&1SznY98e@-Z2sRdsAwdVBK3=WzIrc_udUL!w>P9r_uE5Dpm({Qusp8 z&6|ieOxII-io@&4%>zzbng*h>x@!k-BprP(B%4kHj$3~IK-OgU(b#k3ONWPe?X`YX zY#2#R<*8t08lC+3Mv9h3h|~zGDm}8G5+Y^8Z6|0~_`b-DsI1aduxcbHTsRTMs2_x= z92a~Y(nK)3ysOYwRGI#u+qHx$(?^E1RZXS_S;osZY~XhIp@GF}Dp}`vyZJqIhCX=s zmO_eVJ~Gl+KWm#~2u`v0&geLgqM47wq~I&x0!-c(fCruaeUIhjK|it3)MO4&ET7@c zVokP}Fb93Q&vc554 zDh;Y=4$^vq?4i`!;rq!R#)et63?tGai%V6g=GpptY!M^ z@)`nIl@1MDMy&B+mL3Y3|GN3tRg55tO!k#|dp^bN-}4bH>N>4t6v2EAT$5cu^Yw#H zV?78}kdRf6y5XlSQH=B>A49f^vD&^+PayNinSWi)I0DHQG@ZGO;`9kdUeRM8)GXdi zG5@+6Cddoik#!-T_OL&Nm@)KHPf>eevtlao%JV-ewa+1tnv-blm8ZG?u@F zsPU|WW~Nyp^RKIr55-z1AH9#qTEB)#{J|6RAD7SMpm`fNHDN8y+hl=d`#w6z01O(m z=`v*iehaZ!XwIe%v^bKUF?8O(aO1u!blyHC%yY5VdH;v9$XWx+R(*JDv-M=@zlX>y z> zi^$Xb6{7K1c?6URd^NKNBA0n1-rh;p|F@ZB{|+_C7ya;atF=*#^^bw&$?!U}=`A^% zvY~x)sm&W^&^~!O;=vXevLo6MWF3DW_hrG8jQVeg%3l9w@5v|UvGh?cJsRL=4aXr~ z-a}FNrw!Z>+z*05S4n4!Z zV)j*kg&JLJ3{ERrX-h9RVc#-G@>|64OxEtux0Kr9OGsG_L8r z_wY)zdBV4-h;vZ1@xo1?uA^vUzL7@;!7Rb;v8#pXE>_p`fA6QeSPLLgsSSWQa#Ew$ z&Qc5Sh5wVTM)wgt*aGQmglF9Cy+Rh`Py9`fM{q-zW>1_`)>13m*&!QlvX_T*K zTtQSkKI`heRA-hr zh0FnNFXGqecVQ%onjFMWBKaQ_OB{ksJ zn|ldUD$fMBS$Z~Yx{~7L^C2d^q7~^tS$br_Y;I~g>m1GIg)lLTZ9G_;q=dF;XjL$G zRUE+I>$wFj;G#fKQRUD6jVF z$2(?FUhP`Q_Oa7vY9wddFM+e(w$%?@q6|=(5IL5;Frn!s1d4PTRl9uJ{pdQXb}1X; z#A|qMZWO5FfI7xJr+?`pHPN&QI&U&QHJ(<1GAsY>v2xQG^1bg)&=Z-Md7ahw|`f1EM@{q8y zI)v8B4ap5>(OOw4L}e*w1d|_G5N!@zQEENOSlLKR^*Fuh{OSTY0GE{!-OU(UKaTf@ zxHCjm*JTy)KCcnGJc>zKCB)M!iqWvj%7^y!AGbZbn)dW}g?XNI_LPd!bqmA;H+^4y zKg9!ghq#m{RUsuhPt5IFbuq1yRl_uozjy$;V$CDD~(CAh(a#?*!7H@})9fItv zqLr&{hg5z8?4A&}ZL*TF1uMza*rbiLlH40+_0zn)vON7&5K*twMqe%1MZHe%3lWnv zcRCAs{^u4!c5G4ayMFhTT}iQJl!5NecKLh}QbW>A6y?SCSw;GLf0(InvLibqY5hg@ zRX@DenBUudAjEC2vD*JgqAFpwdhePUlm&ax!0e@YHFvPZy|7)7XM+90=8>g%<@iGe zrpn53RL$|UH~RWFHoZIOvmS0?qa3j5ti1`$&kH z?DynE`|{jr>>4qP-mM!zc`oouOX}AtAg}bOi5m$iZKEK9pEmE+4N;6(BTVGgRw}$8 zs&L4w(T~5W`k27Olc_?&Q8$lSNfOpH5LrKKDz&yGfuwmLc6T`zAE(&8mdLYi_oQ_O z#k1dh(ReS#vyX>(Db7?RoHPyD|D>I#)=<9oi7<^Bzb62DdGEy0&`NVT#vlhiDz#sgw%gN5)F1z~9tOi#tI6rjCK?qBi8gN0ou9%8xDJ z{$8!ZT;kRhx%#^kNL+a?$oju=xc_p}qIwpt3Q5t;(w|p#Sw%NX>x)Eo?C53>(%{1? zX^jHw*Vw|p_?wRnL^5`H^5YA<`VXD!EskQ+HViY_F(3v?%Lsg4bpMAAkk4xr;_-G0 zO7sWq6r6p}ucRDT;}B1gY(Q7%eR7V!$^BHA<#MNc`oom0{OkcARIgdz%jAQe4iUR* zUQev6pDR)bD?NuGn(WYQ?JC+KJX5NSenO?^xp^l`2Pg-UW&+#DZ2s>7itU=1nbb%* z#>vizY)8!?*VSAbUO>67X8}{uE3ClNKL7NVh2&|Uvr>8CLn?b0%eJ&&gd604yNpJ- zsla7-FiUKN-~aXEd>Y|qfLU@Qd`GjiV>H6iR;scQM!qO&%j{F+i<)yTo!B_&CSSbW zni3T2O>5eJJH>i2VHPtL74Q6^ee}VBlk-V;V#74n6d~Eq&dObMd&joEf@tsOhn}88 z(Oz7b*)~UWtEkZhKCjt`igU^5#an5xTwmJf0sk}cawPLZ(@K|)PA}Q(Ici>6LHn+oGQZwUquJWPBR5lW#O6O1>>*;JKvXk-s6PM5 ze>ZL<+iznaGWP~9VawVf@}=cR+{=AwTQe=uY5VOXM7vbF!hZ7HNdnC!781x?0`M^p z{CZ_4-S29rTLRZkC0%=gB~<7#Z8cd!dtEZRZs2H6wQ)%v@!=|I*^niiyF=SgdYLTp zm=_J$CxRI2Sg&EbXdO&35aXRbe~O1E1?*RKgG}J!cFQl)J(&&>m~>SJZLef41KDBE zi*fud(~brN}ZfCA8vuAO7nit@xeIGzy{G1XOgh2l~?_{@bmj zKV1w=U2{@Aq(^q+DAbW?vv%bIbtLL)U|JiA%FZ9UOM2>|;~UA>bkltew|^zR21JLq z-umb+(v=s2Qh8p~+|2AtsQ>x9^Cm}r@`1o{I(O}%OULg0&^ZHb zhNInXk6v;jIvd#;L}!DQ>kQ_jULtvnU%FqV5Z@;Ap)EmTE!ol=O?_Q4riFEcG%MZN)8>gl0B?jTEbS(wVS7g){M ziuJaV)%3Pd6vz#=edbusQd+IExRfno*foT#EbAk; z(mIP+59H3+ORVf77HcpI+5^b5fS;YXx8F93cics+I5$8^k08*K>F%uMzgmmrF>M&rzuYa=c)NStDTNw#9Tg}EuFqhipr@i2V0Q@MUfp#B;o4weO8l% zd7P)W26IHGr>lvhP_5~oXD9Eb)$c_}RFowKYkkdxulZTgOD1j(OZl?`X_w#cOcjP# zem=no>!D#4_tOdM%SLKku9ikw2RO*Gvx@e27n@XHNHN@i$W%+?3~4U#I-8%ax|n?I zz{p%U!LsZ|AUoJ@>yG)<`SF#=Z1PJ>myKY*5iks*+jKjL#;HUz1SrK&CPDMaCdD+eye!0 z67PsIZ;kd9ZlXBtZ7W~RDg-=o`x-y3rX0pQk(jNuewz50inR5hPV1lC{p)P%wEk|f zWH{LJd|l~{3<$?9J;oy%p()#)V;ws2GA}>+1_G6c6E=TIR<2K0(wmh=qi}nM}Zog?8?FT*# z%E?M8SR(`WZ!rG=9wm~t6Ev@b&s{l4^ZF6yvbQhUM+gQgEiZ`HhW8#ai=wrUF_%S9 z0#sgF;ETs)cvq4y{v;w*$qEm$Sf20o`5hIJoDgeQVsz)XT9r_b6Z*)Yf8^9 zSm$5*-MxyapJHkvdnQ&8R+cuAYKuzri~n@-8P|@%5!OYd1nS&QSVJ+A2U4Hf`+BXJ`-gb%+*xE97}W_SubwjX*wPF*1+WgtES~Bb7Eea>ACur6VuiRWnvo=0VOK?*&J7FNDSsM#vj8pmMbqQ{ir-^_ZrfV z2_~8n{Qx_hDQ%(;k$?QjKr5LyaQ}`Ox{yde8%V6lja@wiGQD4>&R!x-4EaXn6lFCY z)%K5T(wo0QGD#lmGbD zNNdfYHs=lPmTq0zpWhdsEV82A>1~63B?PSeh=Oye%HL&ej$$(YCUOhsR3wR=c=emAm{nZ-NoS|ZBV+^JodL;0FNOf(RfM;l3<3?V`@lTi+@w$lYxFX;F zV)~IYWS!H^JOvtrx=6H|{`4;Dlku;Wrh*8@^XWQoT!>=C|I9=+{s-)m{#-VhzpgMt z;3@JzK*H&FOyD&RGX)~+_FZlkNno|IjfqHMtb4MsYGF#-T z^R-}fSNy$%pK;BJOjV(|K$G77J$4=TMF5sj@XAT0%M*|?%iQYPY19{Vhd;}ROhuI)Z zYDxQUkTIP6@XZ&la~$5pl{x_dbBNC5<3Ad(lg{K<26srxB_PUR?9p9;&xlYoRA+ zvbee~kkXOC07ur^2euIk8%5 zAcH#Ah@puJDQBNLbBv_iqwlsfx}2}@mIv5%opDU$x6AhyNy&(G&!cULyy1Pa%*0J( z>HCZ{HjxTwKtJ#QByKmw&ijjKtiygsm)4*6s%p-kcoJNv^c(`}9hr%|+UEeG>Mi0+ zfLf4!$03sSAfm#=lB}#Kl3g?4$a;!o4~bN!LF}z3zbhl)35bRw9nYaVd+)VPS`@{| zg+-Q9G`A{h1A+f(I(04YC3QH+^JjX}6g?%F`B)N#Yip(K{9J)jehj$K9`q80dr zE*HNc!EVbrsOVWRdU-1VEB@qxq#QM-RQH#yI|zS zus7}fT3umamB=^HU4A9;E*g1s+tPY@R-Ol{;u>H4hrc^<$;jiUpIR?gq>!AWrP-Gg z#E-+@tyn-2-DM+}t^QUqW%U*L*Uzf_&bt#`5os#!gn^vZ(9-wspqmfZl&1c``t>A! z3!0^D=<23j%|SLfIpNN86jPOoz*OXcfQ>{g8+n>^wzNQ2(ccB#^P6+|N#671TEJ5z zC5WrseeUCDzGVa|zvLlIsI1PSxN3ikz+#H4${KkJ;wqFU8n~eJNm{k86IhBy6GQ}~ zW}ZDxtJd{KBHPi$deYS7_&`Ju_1j+lX3`C%>0Q~NRf2^>cJ|OdqgvfhwnZ`dZshXm z7keGisXk8@d+WIbv}N>ri3><6@U4uED`*wGRiJsi)So|HGy{1iI74W(u;XT`cB*J(Cb*Nb zoM{@{a0p_`jw!0Ia>cJZN5tC@(P9*`M06(ga&Ft*bV74GAhOAfFuby4wDK-nJ9RD9 z1>6D1t$mrUUbgk9A<(0m=Q6j_31B5aO?0xFtK7sumfw|=Wxo~zM9AOYwPOj@UR6eP zRa~%>JlQRdcka0paNF8GyFr=jsQT%AO|AlnNuK_eh8VIMg-$u&tUQpPa^3})?X_%n z*)ZKU7isdCnagIACf|*?N;DbO@im)ra0}^DRX|M+r24&1`#mbq-eBwKY71y@Pz`WX zyaBD`{PF0WJJ*tTya$jwTaF-QH43e9OMX3Yl-9U=%|v#=UU2^M>kZh)R-LezzjAt? zna1uhh^|mtHW0y%jh=Ima*|O-nu=OTAlb`PhRvYJqtSWF);(aP z73Hx1K4j#z(K6U0iT%WINS24{Jzrh3{UFtQ zRyXo2i-#-;?RO@$Y0G!V4;x8I9-m8vWMEZpzo+v8vhqjFJnJGQZ{=VuzPH{jduf0C zsF4udD-y*tSIYM6*htPj|MrY$)h#b}QiX%QAAX>9@0t*#b-k)*3>)`}eH!YLB|D;fu`Uqt9ZY$-7m#mx zwuqUb`Po%5nLefWFGwo^# z7)dSq;fpkqF$SjJBQ|W5o-^RfCajrxnpU4!6Dc5~S4rh1%0a$2tky=l zY0=Wmr27@TRL_-TEtZ$n86@lSEhkQptO;f=tr1FOMS0Ip4yG=kHTQWllV2-=Q0ZAk z-nz-?WBitDD<&2<*Pbl)skJcqWRhc$eFm z#V)?t&eS&qetYgWnfs_0Z<~lzy@Usq@JmY#{OaR#o;*yN+t$JlKFMNpq){yOR@L7N zDV9o##AL+^(cTQPlu%lK(HUPmU*>7rvxv;QaEPhI2R2 zIe14CEmKSDqsHT+lcpuNG@C^yO`R;vh|7z-r2v0;e(07tX9N>gMl5=97;#s+x5#2zM^^%ABm z3rTkQx%8RjYaB)vdrXX}i(SnGk%9NzAbxkLr$9^a6$K<>Qbk%u5Dy&Ra@%%_2b>ma z3Omy7V@c#lGl9*|x^!}J6l10eOir{rvS5{3zTxLWTBXtiUI(`dBI*;o{>jd#=stA1 zi5QpS=oA;-s(oC1vZHlEa!OK4d%MyCJ-V&jg>7^mtC`rETVpvER;C^8x3uDto>M?Q zksrmoR`kMD(BNAI^@(oNc*pAuKxO&r5|Z`(`Lp@EN|~6dLRMfk1^=BmAI0X?Wv2S0 zshg!uJwlZy`Ac$m*Y@5b&&vwo)!oAd5%k{Qd+>9rJ|fwwy3(s9+F8e}8+n8xpDe&r zvqq!(*yx5o<}U}qxH{>XL}OaN^E{qS5144yE1c46 z7O;xvhs7-z4jMBug)_QsX)@Me1JCUn}fV^@I@mHHUyKF4UFbvEgv{eqxq7R z){-6glz@DNWm*){zUO5lQArOIv{yavsI->$ssoHP=3fNC?7QMHE2|0X#Jk6tc^+%_% zE*d=b&T+EKp(dWSwW6$DB5!&y^Iu*|^_Gcc+44)<1(K_6+1=|%uD6XW6}K`$e&OhK zHJ4FV;~gWbBRkOcc+;3eRj}tq=gVJRS9KkoFTabaYR-^Agt4aW{x!rM#<@wZv_4sy zzAxmRjMSX2fc3HZr=!o1hkeh=WvL@YJ5#WedTmC#-Q)*`Te*tfsYhpn_Z(P0pCYRf zyvEBk$_lg?-G>`B_UvZ55BI(-ox>j4{L=wC zhy4K1lk!-%A-5Vf%B4S%S79l|lq2=A_Ih&IJ)1mK<|*q1k%zBSK7AE=_zyWv?VB&i z>kfME&fOHleiY^*(WJdD8p&(l8h?^T@?)K)KdY3EaIzzbeEgcTQ@4ld8)Z0#3xRr8#pNdQ+ zMAWgg-nmJqsAK7v5D`qCv|b_`xc5Rc-p639iDa2NX%@P(G-8mpFN#U^nZQEBC~E~s zGh56&x{NgQa}!Sq4V=k6^3#0YbL)!`ErH6b*$q>6*Q~Y2BNocaN{F)kN2<PL`pMJTvspU34V9^>j+FFtcp>7_Nye`J@K0eTo~9% zON>^;`!06ox&LoNAnLz1wH}n5Wc>l@hsXQjDtiKGm7N= z_ON!Dq~hZhy9yX=v<>`-?Kae`4f-cidbN~AwQDftl`*?_0Cfq z{Bx0H0gb&M51&|(=Yg3UH+0}F0#T%z6y7D{| zdzPZB-^|2#_B4lGodF|SZBwW1G@?_?G?w2`k^e(ldiRZ67to0QZXv1|QM7COtYP_m zv}^mrOiZx-{yWGS#h<#gnQ}&diY(r_iDeY5rZ4Yn0QYG3B^QZ16>{5aIjqyCpg!_k z-~mS8ehu%rKTYJS=u-lGYM(u``RT)7W}ce!Mv!}~e(|eqWQ~6dJocctFr3mJ97uPh z%naVY;2$#$PME{r_5sDy$#*(`xhHUqmzb zpFp&K;(&G*ci#3Suc)43rj^)h@UB2~A)VpQ3`S7ow$=17s1IhZqW|wbRTynnP&#&j z&UP<4bpLio=?z=yzT@l=Cx%MzZCV_PKzARm!tdYC36oggeh@B6Yd>0vvW|Rxm{y{> zAzG}&#K>xvMoo7hK zYC7%6*R_Sup^6V}DOx{i2zQ7<=2X_v;nqb!sP+oaLt?#ZxG1@Yn=4a<|Xo4Nj^2H7Lj%U5V%e5~Z*zx>*wB6DuCfx=jt&K;?J5gv|=vuqcGKwrW8hLF+ z_bEMZK(gvpW?m-AHU+INomW=k;9QyqG6286J9;H$05%6D^9IuVETgLDI2Gk{(CN{t zQxmpObjAL@_ZCkMF-MdYC4pG2%zV=v;1UWkCE8P z>2zM^h;{9@#NPdh`Ay_X3J@4unYh#qwl7>n$!ON2Fy##Cn;T3yFBpO7sLo5#@;> z`nYY(PYWnpcPK<;OBXBzkRDNB`G=qTtT2kv3XQa&uGrptgPm~V?UxqN%5gZvV;Azm zb|@m{M-=Q&|K9K?@9c9V#B8szMr;bHC_-MeVcJ^CbR7**=?1blqn};706H_N!F}^c zXO4+1OKL${X8>(z+B^Kd({UqBiH4$aTzdTn{C?vJBTYfqOtjyxTjN8%-#-~*@pf9! zy-1HFu)eytR9+j!=IxY)XLT;pJg^Ercj-5N6Yn(VrDS?FXF7lN%Aq2=j3UMEPxe1c z_I}1fWJlJTo|{i3hqFl+wngv%REW7`410Qzw1n7y2DGS13+B) zSp_Ti=)p^`kpH{hNKIqwH)HnE#OJbPM;LkBhqPCZ67dEjF`X~k3Wz9=c6Y|bE94Vy z#6(9wXO{i@`(RHJlXB$%#VF;BT#aRW9ey4m*|r&Ar9ZW6&3Cdl8JWFU-!e5C1a?6K z&poq|*6o{(#0=KI2Ht7Pi!WWGD5;W>>-YH_N_thG)#efR(0p2LDvLyYjk9Yn zU`2iU>(_Y~z&k~vzQ)-lf~bDQ2xMV@zwJm&QUoV>3G z_a<5mc3h#GD0hoIm(QV3qiw$jq^~G`E9Y zOQXK_0#1b`KJnAqIb@0V7-+Ox;ynA=QZSyGT{mqft-04o?C&A@N`%)V=8a{04uySd*_iT#)}eosO^vR41)3O`F$m>pZ8x&ZWKB@tB zbx~Q}FRG#LGk3=>vij<}jKt%cc)yli0@8K*=k84;-NR-YDiD|U+~@=^>T=8sI>CFy zz_XueLZ{l!8dDb2srI7=Qj%3gq@_Z0R_^6*&(NIJFtAv530&)uwR0rd#PY)mNwUX` zGzI$`#Ok{Ii!H>e8P@(1s}1QQj9Xvggc${53%x$6fg61uJ?fNpX-qyT*)j`sqIu@dR4}+|}U-f6_kk!{U^8709 z3Zv}q<^DC-lc%m{qA9AK1v~EFet(Of_|ykHev)T%22m|*rCDtbQ7vl&6Eiu>eosa; zqQ~M_Zln=y7^Wro=mlHmc?98<*!{A)cdsISX%yzM>2vjR+0Gs#&)wv+clo{f#$g&g z;~^$+r6ohFdSmZ@$7of5N?#?&bAzB$)|;VheCvUEb157DG?yw^+7zo%ffKLNb;R&Y zwJ?3sWRMMNSGLuaCmH#fNMv@>66a8y{F5YklVbUH_r0ih*sKi? z@1)*g&lW?5MH%vPgKXpUturnW{kdZ3OtF3TtItuM_4K&Mc%HRsF?8k)yWOUmxQ);I-9lN`);v-Q#xKh9Noe)%5tlrR#yZg`Elbe3ebn@`n4n3?>b-5rb?b=U zrWiVRY@%b4r-O5X4ilO!rZ+F!7Eg!HMS2AX{E^4gP7^<=cs^8hX+DTqYd5O2kk*uT z#q*J}%a1>Z-Fof4#C5NIF?{G`lz0#=$X@}_Kx+CDRbQ&we6}DyaR4~g!j!!*LXMvr} zR3CqC8aBg|wk&Wq_shNK4$^sfXMswGSJ}>-65|%My5}Tuy9iu;3!JXTlYvu^Ctq85 zjJkAo#au;qj8KJ5kDFe)L=txs$a+w&NMbOZ+jv%N?-em%TKfnw8qPB*yl@ zYaeVTDSH7P9bZ|G1f|CoSZl$lm-*Xs8Gs6hR^CT|)1*yD_?on-iyxcOX?(8GbJF!w$P3a^cfx?`Gou*UZ6f1G ztYqi4A+X!a?FzQhZp4FlbP^mnV3wX^wA#ITt|@<$Ge_Vn>Cy=*OWWUaWF=)<`ieXy z9p_N|`=irl773M5?ROcdnpwMW4X6HU^f}BQ%enRsC->t#W=7 zP2YdmUgdx{?9tdigKEA4fX7uw6zzvgQ3DkoueHFBK5=B}8Rcys~W9AfWTi)Fz+oqTKh(5vh2f zrL79=Kc+u&7vFyj0BqXp*&StcxAx^%-@HV3YX?SPDzhgr!}lM3dJpOKD}XKTGVRKa zs%7t5I=_%S=ODoEK>IHNDm%wWzp|P&UP$`&Y9wZa0heVXd0+2Z?jp(i8fMoM8&t_M zk-P=7?%hE0z8;CGN?u@3rB=4uO_BOw%x)x1ue8@f_H_5-Ef0}By%B*KL0oAz+L=uJ z?b973?-0zky~jrK4z7~1p5%Qq0#lW|NVi5e`f@Yr)=q;&*SH zPf^}mCbph)6>#!%3!=P5ugzOYw)-~Vl~k1X;K#={QIz)%V5*7o&@Q3b5YK+fvc8L` z!6?u62!oim{LC@iNZMh5%c6Hzwo?o$W%UTfl%L$cp2w8$0jjE)62)|z*MG2rqO0M6 zTXHeorIqjUNOA0`{azs`dEZp*Si z?54Hu6U0=x)`7L>T%g=`n%7Y#c6>5>L8c#jE7sC@N7gT*TgWfJagOJ+M+;0nk7D6aA@~=*kk^zVv#IXt%f7zJx0I#|Sh9l?Nc#_tG4F>-*Y~^w3BM}J~#8&btRSO;K&)rPSj-`v}1*h%ke6n@f^n-LhIl;u^ zuQI#ovZ@-}LHpF1LtooQ`_!KRIUzgSed>(ZJCD*n^=HI&^QZj;RZ?L) z{oU^ZF(-?47qM%Hpb}(E;}?$5 zi2fn)RP=8{kwLOh@Q;~D7i)U4ZD25^n&MHCy#^8w?%sbpjq!9VpDy;;l^ED&&Wht#C>Hq_6LHF1 z+V7!t^4eCtFVH&qpYhfdx?(O(L%EsTTYa{YMs|itPTrp#xLIlK1zz^_kk0F(7;z>i zcGS}R9xtl^$a2K6e{9Yql5kUUoy8>KEY9nsIs16EUesj>ZaJkT1S?n6J9)hC^K285 z#U4psPp9PE+hZrMliexu#%jJa_;8Rc5xvbY z^Tlc_D0W?-%c&E=BYx5>5J`R*yYUczlax!>)s>^h(*V0Kn0p`Z5Vwd+MNbnYvAbEU zfrJ9PM{f$3ZWmZW@5e3<^89i3Uz$U+Rk2yyb2M8^bPg}4?WmxqGU{_OwcQOTsYBJ$ zAS=(W`4ZWq!h+h%+c?nssB#xilHTWY&2Tg)7Cr95W{leY2uN97L_3W2&0p9+UaUZu zj)GEGma01`z@NlTd6?fRSO$1;xk`E)fo$1|uVcvwJW#OF|bVw;A78zl-#SelLu5?Lo47Wof^;1j7dV-HD3%hZ^CQn$Dq-_CJs+U?c?;=&UZqaQpo|dFMAL~$R z%=zoRC2aQ=L{)h=zw-QII&;`s9A7~nF4XJntz)N8P_MIX!O^DUGW$2}P!x5|cX#h0 z9olXt*?wsavEJP<{UEV+1V_~Cc~Nr%S1PTqV7=((Zn}v)=}uj)P*+HoQ&bjd8dwES z4t#Pp#caED$&54wJ?c=lqU-d)Ny=92E|QNP<4Xe%@=4nwT9-CFpE{dHb5DrI!)@^B zkL+k7*)rcBeuQM(8=?h6QrqVvNVffBUgVk5eFj=ip4XL;X?s@-G6s$BsCJZM?foHI zT&|Xu#8M-!T)SShKGD1M< ze>oANXSR2`(>>X)7g?VT4_b7keE{Ot?mlcbaZl>pgbcUGtHrtdxx5~CHap}7i@h`# z<(&L`Z(dEi&{O)%Cb$*sLXieFyQkp-(xB5t9CpyGA~Hkk((jIqe8+o6WZ90*P&bPX zKOdMuIo`7(jS4_mk!mlO-OLkOGo}_cUruYrxuCwdeXJe25-mY}@djMpy_ohe=Zk1; z|Ecd^l;|ahLiazLyWtK-ybv5!yRj!);U9@za+Fs1i$#ohXO_>t&jxEmx3>#;CBmg5 z61~Ex>%Z;JddRAq_8xJ8tom{huZ_no3MT6<5$A)QFU=*+l_E}x*Z#|VKpS$NzHpeL z=xfT*+bNC&pN9@n(}D8IN`<19gq#>&e_5&wy-Dh5tvUN?Dzh!959A9I*Uq3$jHPu6 zTWcB4w7gENwAIt!7oS{Ir3VC-UIkEfZNpma_$#c}0y=y2+T{UY*^#afa!jY&-g2CB zOl9E`kkCCS-r&HVJu;*i0{z!rTqw}Nl>t@cRy~9oVys^z}cww-+_d`+U z>-G1qq$u-dz@u=5WkE!9koPYj%k$xiy36VQQ27X4epCnGvU-Hhbb7bS*-rc23i^z; z3%E2TaS~#kI1t5Ob+|>Bv%Qw%jiVRB?an1)P03E(L~`94X7QEEHVfoJ`ae8qHbqJm zgRFoXxN2!lK{45+@*R&8={B9j+RuVCfiwyE=zEvC=0q{l?O~Fa=O#)hPw-#AZ+M>I zjxeu8^8}qIrJkfbL8TB+ZJq#S{yX&=Gn+F1l|xK+nBz`&ded38jpo#ELs>WO(({Qj z|I6OkvyU?Wcj|V|u9-Gw@3+{EdeNEHoUd|M(aKaMNOGqJdi7$j6fz-ks3a{P^4+&i z{c}C}?z{A{WfxW0^+%sGiv<@{DH@-l`EC1dw446xw!8Mzig>rq?dVijF9ObwD{c6g zXXmTxW6RP^sU9C%ex=70d7HyMrfec_Q%#qxGaE~5;3Q81(af+hb@!6DzDJiUQ_D?v zWw`9m7|IP?d3p3g$_?Dhq;e$XMJJ~?g2w=M*9Mdum~cnaxs)5YFGx#E3pk?LKiL6L z=qf#yNF$#()pQ$aWR!u(%G>;2j~Eh4#}7awZ$I%UuWr5HK#OPZY5T;oWqH}aN}Jcc z$9eMF4_Ii?KB`r;JA){aSTTCtdeY4Y4NQtd?Xpnzx=Y+mn@PThf-JA*cJj))JVjMK zpla%~YBg5VuBEz>8L!~2Ww3u(5g5kL#2z-X^mkW<;gr@*;8lM6bN*SHxkrpN=EMY) zyk>$rhWM)Qts^^sw1}1AXzomBL3@Dm)3%8~x}OG?UP9|^jW97LQ%mcUv=exu2rYClHTaDvmIj z(xZ#|miM^(=}pwP{K@~pcF>K~6x&tK_L4p|4ih^%x$i6yy8`iO^FC>N8nG{U>Z-m#&3rckCuo%xZkkz- zeaF-P^E=dh1BzXJmr5O?*!3C9Hz;BPzc%~5t=mZFnuM7B9MQf`Y_p?cH-IebyZ1gi zN;NyrvYjgJ9({T4G**@uoy_jOpsP00mIWfc1FsxjOOf7l0y8Dk>*&$a#**X3^2b*7OnoN46K_&U! z+iGp1*^Lf!eF<%w^8^+WR+9PNIJQ&??w|ujiMmN?dA5o$x2&VnW0-P;wDX& zi+phDJ&w~PYl4C6(3{Vx=_w0#X-}pfKO4oE&l{O~6EXXBQ(#3uweci>aiW!p#||M| zYWC}S!M>>0<`MjkYHK48G;@)jKj7V)yy#j=8OlT>ONo}En7>xbM(Zf%Z==&<*r|tu zCrVngbAdL2>{QDEq_m8{@AyAh&uE_5i-RIKIe>synzH~DYDd^+fk=VbW_*hz2M1K5J?J$vF z2$B6vm8juEOJ}y9O>hc3juQhTX)^K;8^#{ywS3+6{Y#9;>j^fvP_cVKm6`eXB=E}2 z9zj{X9-n@JQ@`y?eMcS8c%|h||2&#$-8t;-t%d1G*7%qDtWk@*~b-zadL=T8g)2>1ZeUN1MEZwA)G5>AWWZ z-?lng4I<0@DbRD^J10(=%Wr(A1*yzwb#!U{9lVaz$Lb$#`fGHqo;>^66SOX+3ry>Q zqV%|ebMnAA; zt27aL%W;kN?Iv%T0f>%PZcl$VyX0iQngERPoV^=)m(WZDnJch-8Gm0pQNO$_99807G7d$sK$x}T73C9~2#ma9}p2N844ybtHmc)P81b>j`V zT@U%T5Z7biGFxry&dSrJW*c3*D;_#&sPozHdnjX?W8fw_^HQ}AZnoF89GXkdF!KI? zKXK<;^8S5|OwF62+0XufUDw2hHF$qSuYtza8J0h!RG#RDWo-vlIW7BV`ZlVS@tKI0 zEv58WqFU4+_7?1=T2#M*mE^IzyF*pX3F%#CL@`ppKw>!xC7u>}zVXwi<&#D9v+|T# zBC2UjYVPH|nEM;KOjFv5?p=ERP|WxC3voQdovYKBSEX7f9(%EVBB0@p66&{6doEF= znHQ$Aj+QQWs1=UrYUHUP(rmCg<~-$^UlgePP@D8f2#I8E5$)o>&0D;aaxO0!nXIxa z*_FY@R}@p0i1apZOKpDM_A(}>u(YM^uB^!GI2zVCOsosqvb% zg|Pg10twr+n16-pMqf3s&=WGUav_hi=%ep;lI_1HviL1>XTY+xm6i}y${mZJUqC0| zuScXp!;og9l`HnoQoPP`aG1@L`8?%q)90XF*bPs#*-EkV8zx?gCnwwEV>x|A-NsO4 z_0mU=@s3JE!sIrdM2$VtAR4BuCLvFEPd)7zt&P;P zhPg>>V<09fq;&^ngck13+D{pww~Q=)c}qVWNvF)VlAIXwv(<0Dj>jTzhw1EWk7a}G zpL>J6vNvYgjr3L#e+SMksVt4uNzkRe~e`w&cWwWSCk5w6Q zM{hg6MC?w#cRfMuk0PP}#e!P;_^cm8kuZ`3!R~l<^Vs#qQM5IMk zo~q1G3BG~EHXc6+VWqtlh_zhbrEezf{Z{A3XL+1{RXM$aJLk5-=T(L}r#p8fd*@g`V#w#O5Bf!?QM){!;- z6y#AruG$)rm%TfoI=`*_bC4Twvj*=TwmTj*?fQt%rzw}a++3Z($ytcO#ti`i&xn;~zI(K@r20Fpq1EA|lAmOrBixG(|?ghlvh; z4XX{+(1Ql_@^XT6g*OjqpZ!z`6#q_h9-6 zE=YQ0K^*qn{0sRMhy7(@vQ$hu+aznOn9bvc=}cKRT6H!iPCi5${I|f)4deuKBZ9!v zGm2_r&#r2GnmqnLR%-DgOViQqs5T8tsT>XWb_GCy=o z5zn?Lw#OfMt;0WGIYpD0VbjW+|i>n~D!9;F?^ z;t2eNSawFuUR<#~@__y0s09~@y(A)=-y^io23FT>{(VQt>XsJ6cCjv?B3#<)fZPWY z^0?gj5!iG;r$bIctnD!edx#rbjyp!LtrbLI)0qj2JriOTbe2T={>c3+mXN+LE1vH| zA0R#UVBWpy%NEnTFOR@yk%JO%i@e?8CQEmaw_6dJt<>A0zN|NvI?Q`&uhhSRKPde_ zlE2cc9LkCIt-bL8Whqw~xIFl_pUnreT+y9>hAK8!2WcKR-K7k*EfEY$R`> zvYe{#)|iMabg{`onT{HP%X2ByvDU;&ak0#yh?Ac?;4`W{yLBJ!h}Ri-ARv%tp{z>A zu7{3MR%N{&`?aQ-i>2PPYBF-b8!ztAX+04)7^I7J^~?68w^2~}in`COtLeK!K6s;n zNUuimN6%e3tTc;tz;&~J$qK3ykS#uzHFE>m;wB>>m5NHwD9S;uSloCKt@4{Kv|=w+ z%d^2**Qms=Ptsm(i-m1y>tokK!LI4r^t7F{Yuakz!VBcmvLX*uuzm4X@<7{+Os|$} zdshfz=E_q}&Y&IIb`#5!t;)lq3fv0g`tOTk%pHJfw+0`1qR%##TSk6ir@&L>XThk} z`25dxbPs2jf!HEO3Z8yFO?>Ers28!Um zqvuHB!!?l#NIB%tamQ1ol)NAX$ zQk6GRudNGaB76GCZ+O{FOwh=Zkrygo{@ab@6)x&z^0YqI4qKz1Otx(k)S_gZd1qS` z<6Y7H<1p5H$CJVn~ql;yAX=zSYmzaML{thBy>m3nh8$81`uOX;+>$$DoYZy?9tmOb9X zmQLGR3VhFJW!^YQJHXOms*et1Sf6LjDFWq3732+Gd3XtbZ|2&vyqh(XvpplxgIOv3 z7_ua6qod}_k781m2}#Lz6)v|wuAlXx6xq2%E7l8*Qx?;TRW`)ypubrz2q()zQRR<6 zmbpwT)^#F_)mEpnuF7`nYfz2Lr3d!&_tvftGE;lA{#Pvf%-#d_$2(b?&$i~Ey=aU0 zwVP=#dV`6oqW1&J>5a@>bDnZ~H(F?PO3MpL)w~!2{GI=fuH4_1v#=9+Kw>GVk+w{< zGtKMt4?j1$DI%2}5i58T6pi**XV3BT!kZ(q)pYbn@$|30IwvTeE+2udhK%f}Mfvi) z%YFq5*}?kmz{Loc&A8I@}?cy$;5m$=s4|Us)$4tI~lM` zbieb|d6Muh!0Tvf)T3fdV;lc+l!$kWL?s>K(fR0a2c~YO^Un=MA2CrAoqe>F92L7ulp(k{>e5Ea5IkyPS{GDl{Q)-lUH7yC zTEA#O37SA2)G>3%JEdrL_ro+%?b7J44p; zgr2|SZy&Ow5lQb`3G25up;}QSkX$2MXe)U}L5t6sI z&Tb!YJEGlrRLcen=A~y9SQi#+pMg&!3})jqc!7?3!9gKDcg} zo$R7)dqk0CN4Y-Gs=dEPt-p&=>*-p>M%mA5J?-ie^7glv9>n_=)ep%>B>e^qJqcG5 z%#Wtr$Ze(pV3G>kW`bQ*^G~x@l4odWVmgZYQ3*)dIYgQ06Fm!d(f+%UnHW046sJ>` zi9Ex*_b%N>o}sbMOr~~7%*8u+?PH3Eb?f!!@cYAd|Y!;%i<`>cF z>F|1&wAP@U?y2{`IY&9&=pv#wkme700&+j#2wa+s>SybWoqw8SZ5|}ERt#)5^|jd& ze903*EMKo_o&98yF(x7%L@DUI4rX&)lkxjW!q_m6c^p*|qS)th-A)TA_KDMp`a!h5 zJjb$Y)_yHmMkY9P>Cr{iB7bXpXHd0Bd?dEsu2(=yTMbxCdf#-sn{{GO>!YQ!iM6Roi0s;XU<;938_ABK(S+Jfi_4p=%xHBh@4kz# zZi$G@+pyZE0?WDOk*Vj%a@qu`bVV%Mm*`4kX ziyA7L+v^rh*h6!hWTLilx{E}G+d>s{d)Lh2%gJurnaIhysIi`awQ+zn6=Yh|zsNWg z#i;E~RJxsFzr!lh;4@`g@)s165f5I9k(Cm4+c-Xc;3~Som4c`aw}<*e+ul2YJ@=&g z&z_?y?G6Sm1;B}}uogpwlD8nR672ha%`VcYjso4F&33&b*cDg3&~rEK7du6!LXlBGp`rByNiwj8R9%g!S9(yn{8%?w{70KLX>a~R=vm+u^mCUdb z@v-uksivl85z#q9jiRCmPLuIJ}WgG*Tl_ zee68WwzObf_;le5dud%ri@>$N@}n7DG`Y(nn$dIvH6xoc!?tAvan095#?FsoG|fO` zF44(4=UafXD-Vhr#|*2>G6>#$@lazQ(19^C(n^aZ{Z10Y2Sl#+|#G_+Ce$)EF)D# zH7tmN7w@>Rh*pwpBg@aPKRAjVUTI0u`NapP$6us))D7v8hgnvmP?Yv+?8*X)4n0A3 zfF1HF@5>D8G!skb@1%3093!`w$5dryMLF;xEBf!D9C%+NQ%!Xr$kLwu^Mx6dk@f~@ ztiG2DLem0S>@0)TUOCm&1AtXKsm1Cuv}*f|WW|-;NRPkBIJ1cK*l*xc;?k3*AE~g= zm%W3-ehThFdTxP_+BV0zhI~}O%+?Pan&+{Lu>w@ua?#5m$8Wx8J-rOl&rGFv&iJF5 z{(x6gD?3{69_jGoUW!}#8@@uX46`dUTFvi$=>FAo`jBg(*+1<->%fk>h%y#Zd-SsmCmm9?B^v60(HR?FXZbHuP5we43)(d~de*N)jvcKecv zu6NR5J{cv(rKLt0uh_#kucv7HWyGa&2%mU}Ep4%21-Sdt#hJ7M3_w%`Cl9FNx?05w zN2ua?AY#&O8z*ZCNLzH~r|jAe>Wnw3<8520Gu|u3@LRDC)vCS$TU}-%^olkbYw_qW4K6x!7xixzf z!CeQ6F8>7}7q_@^)nycIy&mxk7+Vlsddz{XjT<-i68W{kk+}gXmbNw+kfkHP*1ydo zSIDouVWP7Gn#hW#WVDy<{%$di_7IU<%rr~O3wCKOs@Fb5{`5_e+aV>sF;p&TD%itJ z`^vR~Y7~cBsQM}~*KasQXHjpNnK8u&o6^z(OX~Jam&Fvzzb%jz^lt|C zlQ{VDjK+-ij+v%EYs>n|1OtUQUz$_D#i#WOhzp3U)QvzTh9mEx%?;Rhyo}=C6orCn&n?xFZkt}lLs3v zu-p_L(sIRK2@$Z;GNP<+>7E}K(ynF%;xcbir(sD@e`?Zo4^iKbK&@BTHWk=d|JNP6 zC?5PE0=1C!t3R}7KyD{3#L?--x3=JDWSICczQee;%_WePk+su z#uQ2zTzM+k7d@E2?S$H+=HUJ z&H!$-YP4cvBlwC9G;Mt~ZJHQ~wWL8Fqn0SGC{Ai~v+P}9b=gby?(*og#-!F%+t`Vn zju{(_%~X?EHIlYkjaY>k1V!-$iZ29KMMO{(6MWF`JA3za??-3OoXZ~!fByD6_nhy2 zzC%z)cOAZh)Qx&-n%(WNnXi5g~cx5#PgvMc@2%bRAmsO#~NWESIoWBF@d> z;)(1D2=~(^{!^r_*=qxbFCKV zOc}<)*)#2ib}in0L~)CwUcZmrt>N4>rI#&D&d}BMXlXt%a80o^#5uh_ZP!Y1{8DCxiSOlE0%Ykl)4%IvHDo1~huzQk9p88M$()$MvJvv0j6>#>U)Sbm?~aCF@mct^of`gXn}Y8cgti_TS$PAk@aHtuSPJ9T%v3ET#`DSnT;u&D)ugxX zh;MtR=p8oI=L>B&K?YaJS0UOv|9^=#3- zW4L=OL#aQv!IX0q=G&MtbBlUi>&eJDRnb5_=%=Exhk3?U_|sq9;FXfIY)-_)FsEI*+Y7ZX5i9tsKB&*SI^J_iAmgTlD_o6MU4w9hs>s!O5 z0%4m%tYXu_ac3~+bJS0?tGNzK;atUopAho2Iw~RZX@yRP={nGKwgTuv{~`OD>`1s@3F6wzw@0)-juG z%A1?i?3E!U?>6GTIb~#;@rpXJrhi;BxC%3Orwv5OH`IFeyp$Bt)g^Y%l)lZ4xO?jS zTp4E>LYdw-4o;LvXuOo?N^aoVp79f5vMJWfQ_ac#68`LlEq|&q78G4m;*9kF{p<&LM$YS5YP@R3d>6__=0(l8 zi8k^=keTGvyE^o|+dsa`S#t-yfGZ(17QTv~NWyAth?>tjRGN)6qm#oeAGMk*BbJ=R z`24Eo<-nZrY?!9uG7MJYrxAPeEqnb1)TS+(d^4TB6>%)S)V~$&@3jEQ?uOadFy#&3 z=F)9TZrP~Wonr@FV%n|MPlW6W-iPsX^-VY2I))rYrFJ@rd0z97rPr%3+f}AvqD@%OmP_flA@9QkKYS+l2QFtPZw1b{Y{Q`4KO!>&k z%lcK~KDwcCVZJzX9CP7>7*qg1k@%(qOWtq6Z+bIGgs9vG@4kx9-KI1^{0m2qp$6#C zGypt$Mu$24(h*j1N8O;y7$e^b&_d^#li?LVm(Z*&37c5${VBf>CGpAy7xYDR28#CH2X z=s1h9-Q6G)YEM}%OAc1hQ!Q@&6;o5r%&>xDmbJU*r?TW(yy`0oiJlqCkGzq%y8`{l z``RhR(UQ3~d5&nUmePCcC)Smt-g}_2(p(<1^d7NK?>79o6YWU1ww8FAO-hns@JNCp zUR9&!JC$upx`IKcutdluM#JuH09+gJRyAvo=LSIg8FjHJT||)UpUpoiW5~K$m;v} z1FR5yO`}>epkvdZ0=WH4LcSSer?OW{`};|#9c1*8`kfMYIY)t=!QI zv3qglh6Fh-ML5!+NM}dQX0dTZfb^P25!TJr+O=NmdmPtpXn>@8azu9>;p-p2dGZ!o z*hl;v2MpPTX1FYU@s0Z|QMKYdNv9jJR{W@+25C09StKdB_|mY&fl5nNAps!i7nS#f zQF$Ww@lzd`CmN<{533s|7EzZKxM@UR+rZ+A+tEP#MmSAzV1usUm!vYqf2jFcCGsBA z@jQ4*L6k@6#qt%+`!N?jT>F-?Ef3$8fDqV>T0dn|%f|fmHrmukKQT*^-HQHF827g$ zYG9|gEnsuBqJpGMeDWPOov79hHzUQD&sLHva|s;)e|8+yE7ZE#`Tu&pE>32lenVMAw)&Z^aXcu21*N3+Y3?&}4X)~wej23h&AP)xWgi+M zX0YtTw}U+JRC!8s}G%mXHKJL=*XtZpo^056WS+Rpphve0mRA>6H^6I*=GyS_p zqLPDy!X(-5f~>s`%`YucXD&);yn=P+aR#<*Q*rF74$3PpRsE^RSW$`0@dhT`{IFRJ z51DZ@39Gbp%-h>z;}OgmS?CeVM6;z^mtD|eG3bwfR>t z*ZIQ`P7j8WpGU|w{N1*5`0btv<@MwOa1*Jz@9$ztDzNVREJ-v?Rw666XxY_D>}Hv) zk->DRHb_2ZN{UM|1{U1XQnCE~h=)H%&*M2Q+QsCm1YJAgU2W7Z-SbXeGw#wUnuK62 zg!4<3r<(jyawqx#u^I>T3aq19o+?#=i2ZaYn+yItO_VHZq0=gbG(#8|?k zIl1{_(_~oLp9G1}uY;S$>B-5kxqQ-kpupl%t>8WCmz&o%QeMu&u6^ZLFMCnbac+Fj z0;cppqcE-NOxeBft=ZCvyEk5wJfEdFM0XRF&l?}Ny&Lm+)BPlHiLLp1#mc*qR3@eW iu((#tq$K!B5c=yC8ZU>6P>U~=bw&dxF_hDT>HQxTk@`FU diff --git a/src/external/windows/lib/x64/python310_d.lib b/src/external/windows/lib/x64/python310_d.lib deleted file mode 100755 index 10a40261cec0f3bc7da514afdf1cd0582849c9cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 360248 zcmb4s4}4ukmH+teeTc{+R;-AqSP@yoTCmoN6-k?hrlf65(o)&V<0W}%UYooZ-b>n~ zBC;&Yb`cSgtt^XJ5o<-Ph!twBh=_<-5wRj7B34AiS`k@fYsKI9%$fgpZpyEp&$l=4 zobNgFf9~9wxpUtzr`+fpJoS*b9pnFd>q-7^{cDE$WA@Be<~b(%!_^bR5M=LbYeczS_Ea&y06Jb)|km4g)a;!0S< z*nXo(fb`G3jHmXBlyL6VjQ1ZVQo`C563MmC3C4O{*NeiF3j2=~1&K>>y;P)|FOVpH zpiK~xPw$dQe|NEvcw$7Pgw4+|eupp!FF9FZ6Rw*?aq3Ko^rIJxV(V;?+<6}3I~zqp z_+}5|)@MXQ_|A03m*A|aeLMWXPj zBLpEi>jH&6xB{0#Pq^d`#($qC62gWP82^PbBz&}&arHEj5UyImxb#BU1D}L1!iH-Y zpInFEz$KlGD^Z?=|C-CV3h5$zWCr8v4e$$m@-RmC`63~F0%;_CD=uMNK2s!wk8NdKcDqOjpFrLbK7N$~((rNMV{;f+AP)$abud100rTNwY1 zJRs~s8VC;~ZiI)B9>OlvQNk~dW&G@7kx1;r75D|xMtA__OL+JY#sk;kH}LRY#zTlV z;b&7BKiwe`i3PX<57rnzN8S-0e3a+%xv1NO z`k{>B8(w<52A!u4H@&utL}!e<|nNVdQq zP`Lgs#&u7K0^zfRjL*#he~u`=Xof_(Z?lm2+cc2^MR7A@(fy)G=tH|A^zM*IAHP;e zeE4jU5^E)v2GTO`saw+M+n8$}8vH=tb; zzKFU86qCmo^RE&`Abk#D1Ie9FN~GP}1tIxvuSEKX6NSVclt1AQ*GnWR+5jN|PXcv3 z!F}V8M>GDkPo#t=Pn9Ta-y;g!HZyKrEDD5M`V}6=75Lg-#kZY!roW+^w(zS766XiR8nNiKGW@^TP)-&fg#s!iU!|deBc3y3sa)!tK|I z!fl8P0r}2YfGcp@Dv9(HqbPgA9>$d@Uy1v1MZNps@uE0zJ){3pQIvQNSD>{AE53^pbq>1*m;w}Y+QkRb}_z*e&TfWPwU2!9+b;6zJfM^DA|KIn0;IQ(dlykfUVrlN0n9qzqH;+}N$K9Rh7r$}Bi9f17k^&)xgs7PK9 z{xtY_!wKNx9&jw~ov-c@$#LMl0sX|A76J(K&FEL&e4$8=-wVL*&G7fnXyYf`C6W`5 z63NTZm%MC~NM2q5o&(m2WD3HavIp_K32`}2BuAYJ+=6h?_KrCPfc!Yb<%qixM;S=G%Vi0lLj`%!)bVB#4Bf)tTVLpQNT?QNUb<>dMw;|4iw?37w}h3nY@?qdeY*vM0=FW1M`6NF<)e71)F_0@8I)NhC8+ zHiVN-RJaCL;AE6NkpA>?iDVZ1yyFJOtfNFic*iM>nRkgq0_8Ikcst4rNO$0#Lipyv z3gfsEP{#?kou#l7SKu2PC6ad^B9eFAtS}c>;FN0_?_4Dk!mQ^QryLJCFdOxU@Xkjh z(r@8j10=KWXUs*v@J-a0-y#j~MfwP*9m;smLXi+ooyBE)v42H!@nD z!f#;CE=CLLKVi;UjMLV`?|OuHkwo%a_yy9w6(a4$eF8{MM;U$_GNACY^F(^}`J(Wd z6GZwE}ILxCFm} z(pJVfm%|P?8~H+5af3v9{~96j4!HNC+!?8NG`| zBC!Kkpnrn`%BUZx9LyM4BN8C}3F;M)tbSS|eWXK3JdE~A_$BI^1j^u-z{8!4pP@|v zNf~vBu=;$)0Az$gMIvnXcT%-@~V*FyaNF@rm z0zW;P@gT;?g!><7Jb?B`_}MxIfh+Ly?TlYEU=QH_&Dek|u0r%J60K1=+Nd6sVNLYU)q-Abo7PL~_xikUuKY-=a@`40#U}SM-bGS%(4VL%t2qC&%G= z1NlFEoka32q=9hfVT{jRDH6ixkw(Jp0}2o03fzjiN4Vn_#@82$gmB9S#vRCS!nOw) z-(DsX!dLHRY~6-23GlTUjIZATdtf_$6LvhtxDE9QD0~fLF(AQn3L#x{fx=!~_ln|j zJd+Vle_A44^}LW+F-xRCap^OR1!s#QkT%c{6VAhZhEPM@1B&mzhS7D7C<4j1rb-lB z;TuT*bP3~GX2(G}ZFs3FP`4r<-SK#** zc#b|=0rmA8NYnouBx{~#j9o7h!o)F*HB%r5 z#!qFeMg9@Sw=mY;hu`;!^j9c9!lP)fgxymZkKTyiH^K+{6~e@2jCBQ!@zGygvri(q z4S7Yl5q&!0TM9Q_g?Xo|L~(JOM7kGw1SGc{#rQVr9+3X}b&OxlMLd9CUoDYb3*P^% z0iFO-kz9i|_$j1C;s#uSYwl%yZ-Gd@i*`V`3-ww8>AMT~{x!e}=t~Dh@kOY=kKQSg zcf;o|QKt%171rarUL?P|i1F@maDZRJp76*i3Y%~Reua7{u@_h1(d81wh0{f{8_xkx zAdKCYOB4&w!Z(l%NhH69Jz+P}NBGSu#;<2U4m^pvAh7{g;17tG1j_Ocz#im3;rG`` z6w|u}A^oT0MS2MOke7}y4&5$N!Xdbi6AnfFAsm9XNO`};If`+37v#W{{fvLY zeS$FMQi#!!>pOsxw=qtcE{cRX7ct(AHcfcfEsS>`jo-kWLmBTn6>?z4LyVJ_ zi6UWkr@}qB0;ggn$y^IB|2p@PGID|JIp>P?lz_EuYT!bqDb(3)7 zrHps35vjx;T!GoE6i^Rm1OI%p!YQ}{vo2J44p-p#PR2VB7GYM(c=LnEAK+bQGv0C( z;t#yzP{wgX$VcF0)NR6>)-h(BDpJCTTT=bZ+cV|f%LskDG^>RTdo-l`T{7zAn zK$}0l6=gffIQ1~_4ny8-XS@mR9!TeGmM9*(2l3q_(o+^I+==U*qWI5uNTgk7p^pIG za~0#=x1e4D@9UQ+&e@MX3YfK*@s{flf8a#)UxZW9uMyscG9gUAi19Y$58R~3_u_u}rN<@GrAG*fo{b`vKpW^m{++gp@!n^_e^w-) zMqGclPb7GDOg45%r27vS=|LAWUc5@AgoB=yC@wu%6uXhngq~v+kcJ)r{TU%yaJ)p3 z$^=ifx=&=NF*=9J)9uGm%und0B>*L+3;SGo{4(4bR27WsJDdQcS=5Is*FCK1}Vf!hJn=i(10M8u~w?2gUJS2*3hch}J6Gb3B4|5K89mBXA zbGtzL$(a&`J9h|)J8Ggp`1WNI=_jv;9@ucZLIae@7~efv6bN@?%tZJO=6VU=!!s-4`#p?r9|pg`cQ-L^hYjKT zyBPO$iUN>+=vaxu0~kv_s79$@@ww5)L{H|swfbiJ4E4fT!Ft{!q|g$O8DdPjD3hN;s35-JcE27{4e4_cm{q6e|eVi zx2r`#;#gdPC#OiHpFc_v3VSb7cn;U+&_>W+fx`2h3U}cO{265fq+j02_|m~5CEU1y z@%by@?+U~TX#vtNZj>nOy9)6Io`sz7k5d^>BaISA;)=dt3+^d|n-68&aHmKmP#mh}(4!33 zZ(&@xPNWiha0UJieJbH!FJoL-fE@Vu+Zg|LKlH#w=zj?RIgPP?Hv9qqfjU9>_cq2w zcYp`{2kHmmV)R#pOKwq^fh%zFX^j6w{U!9H4iPHHFa}VE2-Tw*cRnG~@0;cJqYv#Z3=U71@1;(626amL-_78#@)zs z!gqHv?pi2P36#}ccOVVVN~G%!6)EPn(+@s``LQXOFd7-!9UTjUqkw7?IGt0^#x3F`nqcZ{T<6%OsFzza#8sJdUvn zkS@Jg;dxwfZ@U+Lp#=Kvdx0OK-y+<%Q=tb};9lYaKYBy~_nsdCKR!(&?Yj@_2>|AX z2*tSz74F3qnAfIo8?FG2_X%wnPXg(GBVL57kUqkHBX5A>64WI^+dhe8bcSHufGaR| z9HViHNC?fXjK(ea4V=4;(VPkSOw6;QPg!|Bqx_&KN}Pl1IU-$-aDn3dX%guNSD~EH zZ=ADNBDwG?!8jII;9qZK{2S_^#3i@_pB-R)4jjVuh=;^NT#+y1NZa^K#!A!=!Z_*_ zVGwe{O0;dlz$1*x2 zY(l-kSc0}l7)F|bbU*T+@UO=(E|@D)iJNc*KD<++xDsuCum(IT(&tBzH|R(1dyY{; zT7Y5)Y} zwlm&;yeJaH9EtQF$bX>NzLjz24%h?#@`S<=u0vQCLHLB9fDaU30X^Y}hZGR6BY>A( z%Q$SaC=yr z;yGM_!!KpL{2);zyb@(6(Skw|pl3Ot8$Bs_zDk?_B(7|-HJ#Bd&oF+tPNYEb;ENb9K^-N$_$rB{^$I~qZ%6$AiiZp`Ua|+gJxKeB zj5D_)eZc#VR=5&Z;Q!D!6aM#6iF7CGG~vgH1K|hwTjFV4pT>OnZ4&7-sK_rL^s5{G$u?6L|h%iQ>VBBfY?jk7gXaAHTu9 z2lhZQ?O_~*dI%)fp&yYr3fH61Z=f9zK6o7CJk&|T2a#_;^4VRC&&)>qoDJQ@jJ4Nb z%m%DRy(3ibVyrq@6bY;0gRlzy5RktADT%^Uxc?CTJe%={K~Vsb`=?8!e@DE5J zdtf8+1g^jjP%jBT-p;shg-9eIyAQbcDaH?$qj65goy`AwC+A2^iwlWSn0rdqaZj(sv3H#lKp2fp1%Ql;BN?10`dAA?tw2lPa^rp20=&*xHkjI zz6%-q?}t6`_cn?2MQ9^H@{iXs{*H1Y?0ZHcO%OLix{Xo1Mx=xUWkE<=83!F9QbO?r z#=*CX^u-S|4qhx$pxAyYW9m(!2&Da|Nfhh5ai0Z7(6&Z;N~QKiolClUO0Dy|=XJGr z_AKctjZBOV)@yG+X-27EluC=2c64M(;=>M$IXw%OEq~wX@kNLH7?@+~6D~Z&$R-2((i=@82$!vk!i46M$#Xsg!BL&&zcjnx{f_HN{L7#SrrB$URY^3YJdFP3jwPzEny=LY1m9h1>+UR%4jQjkBDF08qm@Qmz2OvTcgYGnC+0UA zrFnyuzSZ5;mB`1Yag^;H{SVos%8f`=Lzz-(j@BC$ zKh_|J3YiNg#!LG8#u^Q`s&tnkN;Z;Z(lgkoly%jjb_N4aAw`HrwKh6`O$CE+P9{@5 zc{Ytb&C&9x)84~8*ZN5lp(3RV#U!zFg4$Ar>uSkwt67cDURoRV`T>`3biT>yl~Kv+ zwfzTT<{HKsvZ}W)nL9QxP-&EUCPtk6II~*oudi*+tJj+Kp-TG_RwFmK$8^ZP%>3%M z-Kj4#zd9adphG;w*M|v^SDu{Hg+w|d?QciSe)6Yj#lE~v@~6%u|e-OT-MDh zl$zgMLi#S35xasGWPOat^3DlTbDwCk&k=P_NM1m2Wy-_i%D9cSbRZ>4+g+*YUd5Gk zPAnc9?#+bn2@R*E-?~r?5*dB>ST8kW8w*7;a>HCSHZ)ot8JaK&R!k$e%rffixRL8V zUGsgdv1%Kt*Bh}iZu;&ex2*j<%j+*tdaLRHbR3d4oh;d&<9l+>^~ z>ua;#SZFwlD=W*R)is#}8M$G0)|=HB)5s08r57FDn7Jin)h?tD`ws z9mueh)UdjuEG0Fp7N^3h{3VgmFUQ1KP#s8Q^h+{oNKp4taN|L1a%IL|3d0asXgDp+ zZW=+CGD)@y!|kX$U9U=zloI+~De8eouL%EYKcyS!$n z?2I~N=QqoEsEd43=wp&5tb%tLp|#U&rWQMC14j1M*u`n8k4F@@FqjA_hV9bQMZX}eQHlTwG( zR*tcx)M3r5kC`W=jMx*@MsZ8GPopMW)C<-)>~{N5ZFrD*4BTY0Ij-*>j+N}q^D3We zM@sGamv(toQQ4Uqxu`rcf`Na~xO{2ng#PU{El+4ULBrB4)2_5&LAiNGWg>X)h$^jb zwlTTqvysbM0LQk#JfY<*D-VrTvMeRGJdBrzvow}kmTI>-$?>(;O0~%xD_?7#8`_)g zH9W1OOEejw9s(VW!^SZd~X)tW7F?8fGN=iAV=Xrh#Rbum-OLGBc8p1d@8pD>4 zZSf~9S7rFft~lE&IwzP>f~d2OcsPUPtj4gZ0=G5l!@-lBvIw-siY%zLS67bJ$80_- zt3Yc~F|R%{(OMoYhdE^A#(z7_E97|hbjG5sIy7W$sQ&ajby-I&O7eWP*9Ph)evX-x z8)jE!sN&wD714;@?gs_YU7^EeY#p+Fnxiz?ga`K_r{eN`%htS59oX3_ET^4>NGbo($Z*@^|L07O;jTs%;>c>Dit#849qeLaLfJurFsny0QFK;K3KHb9EmDp zHhOXxQOnvuX$Dd5BS}Vzi2}?vVtTQvn=2@eNc|wm@Ys+y4=7o#LUA#fU2P7wG@VT*4>D~4M&UI`jY>?jg$g&mBQunU1eFsvDReSxnxxznx#ysD3SJMvfR%CBi1~O z`Ru$0l~@xi6cA~Br2aN_-Ss)BUl_&*F=un7()`v+U%kIFzu8wdlj%{F7V1dHYH+iZ z(P}fVf~gipscXtfcg^BLFv~xNXXS>8X0pHx(=AbpH7}ApL-uvrVj;#VD&q{3Wr`su zo*O}5ElbPO_-x&g>U*He*E-*#6?a{4NzpYiO5dg>-$JQ;erKaHQf}x`s!py-hEe)9 zy3ArJ(K_FPXX>ds2l=W3a`c@O^V-I0eWTTStwc+qrUr-N&I!X?h6V9@qtCLDj+oMn zOUUu!dP`%aj!#VE$xJlVU|UAbV+5U$!!x~*Bh%4~`87Jv|Dp=EFW9MQ$CuW4bnQfT zO+7Z-9lMa?*|XD0tUZEfs{JCUyetkWo;`b>#5y2gNH~gRM07o^Rd;aVOEK7x4>CGv z)Kn~E*o=v5O~e+JS66yQoEnKbrt;EJ8JVq;PMq?zofDSsSNc#M@xYl8)+1}E^~RLa zz`}N2oorfsWz^Iy#7%gSGWWziqpdaU2i2 zkSxuEgFWc+xRMn~&#n?ydp(8{u zS{_0DuIM#z{nPFOvJ{fpUy+A(ZS1H#zI-UaxE|{_dVJ%UI9ktYad}wZcd~L%LQm1| z@~Gont1Q30iRq!LuGv|+m0<2BoR-j@9hJv-(h{Z=CXUv#il!8NDfgH8aGDTJu?|SXg9ioZL~ZIgN~hb;OD&Z;M3aHB03ceVBBc z(P}xi*E;1p5r&TL>YC2B&q;?72W9CK3Mt}=F| z>|9;v1nBUE*&#L0#1TQ8XB(5fp}cd#NaopuXX&oevEfu_=}PV}IpeZSC3l!AeOanj zXl{Gcn=j{tC?Pl|6WyNAmv&C*%yc<18QGba~MpR16OMYn)msgIma=BC# z6*u50u8_XsDsq?AbXC^SELqf{(oJgU2FO{xtXf&iJ5rl=OFH@%nN(lLLhptoGmpwZ z=};>ud|L-7%eBzw(2Wb*Pn`jubf(BwtE|v^v!1n3arAn&zSl~JTBtblu*PA>n3BOF zrS~?`spMf>ozha3@DnE`O3PKl*DIM~P!a+MEK+)J5>SS&W@R~|SZeaU(;}0O+tQJK zJEyPwDVh4UsM1(zZ{^Y@i(I{&JFc92vLBG-8I|IzvI=I2rC(pA_^P;Kd|##bs_0^T zUu7O@POsD|jcOl4r&yRIsOOnEdhMy0|VgML_)X;k^|>XoI+X;GUKzjEG)YQYWw^%tkF#oX+})s_)vLl<2rp(|6~@WfsU*O;Q~Wce0tRQnka$ zDP4wPJ~?vbv$fJYwvyioz--#6?)`L-SvA|q`;xu8E4q~`@AA3BTIxHQH^3@uNT{X zvnpQn&&ua7Y0C=b)>N!S$~lm%dP#Ry##0Mf`J#G1=9(7QN87Mtt3RukE8kf$no(5P zTQ4{I&5Jf(p<#?$Z$zwUDkbY{32Mc_P3#x z*Q52Ixz)!a>6~9%Q*G31)T*+X5Gz&no>!K9X1P(LT^jlkLe@I;qcM3ameniRGmH|B zsXH;54_60Zaf^@HtNtaXY8x9JYgA$y?p|XW)plYkUKEa`d_FZ(q#D5g69-i}RUas0zVB9{Lou>}$=9SS#++tHvfeou%KH`_hH!7D;PJ#slR17Bs zrF3n`QLc@nbZq2gP05?;H^Df%&IwhPPB&Sa*T9~1n*sR@V=W7pyHI*g(PeG&P`SvX z89#0;Ch?O;Gk$`A_?hhhu~8)BBM2n!BTh0t{JRTfP25c`&v*(_gyFBV))LQN<`@q_ zRHGiE9OEGy%czq)n(^bN1+_QoB+4-!+<4}Bh;odFAd-1b@@U478&6qd^;67b45M@7 z7`KivjCByXxPv&!_;BNoOqlPyCl{$zieaqX*u||Q3}YQcE$$#rGCth+BLlEx&~%=W zB1y0$y#yW^o+(8cM1vx?VRlUgZBByX@D-Z_t z@?8U-3&2h=jFshP)Hd5wxH82+1@ab4B}EGrNA6P#ZYYxHDqL)CunDQ$yVnA=~nrb3{ zpuiWE?Ym#hP(p1$Kg|x&K8zH_N>Fq~pfrTN6`sk16v@R*zv)7o|}v%_^RD~y!p6bx0UxLzwz5_9dj zGPzQ{&DdG*$NmMoAn#8mW6+ElTwd8Xl|sAl%Fh)daOWpw-22ARVY^%3atV$tQsmbn z?Nlp#?&(AIUiWk+69nj5=Uea$t}PbFzf$>pwREKy1ag#)7}Yo5##K5ti0v6`^R0Zf zV+VJW+4*Y6jxuwo-o)%oG$XXmv4ATrHEl>|ouhLs5JpR_R_E#9c=ow^#}fWrO)NZD z?^k8Fg1S|lYVZmR7sp3LN6Tncm5yl8t*4YzbYmQ9(V zjYp)kyQoojn5~)U_}o zMA^tS9L%#~Y}Vdt%G;OPP%;Km<$0m{SfN>+_Ev9<>6=Dao)^T0W~rCz+o^9IYULZJ zwC3kl7Gx2&Z(FG)+4T4_$HJu;H?EN0*z`jqi49HXI!V>eAzc%tX~q``Fe)aua!-ou zfF(*xS?lfrk|tWHJ;XSujnYtYu)Wqja7emQGR0`YK0IgcLo$@i#>GFNLPv(BHZ~C! zH4b?ohi17urKO*4k|?cOJ~n-pBxs@Hv^4!AFgU_ms7TnmY7>QOB}rPSe&!ErBs)h9 zV|w&)dN^`VqE?(H$Q#=zIr8}kj|%jMO^+vaSWB_?zOq)qN??rbX&o!7Le8N{CQK!F znaw<^QaIe;;l_A1&o)wm*O@bMRWjv^f8p&!z545CLso6oycMqaz^i;OH4LC59*JqOWTin5PP`AO+=4?lOcqf5lulW+G##1O zBt4Dx?;h)^|t<^yU=pqLTaZ)U%r-q3RS= z%(&L`Lsm>vIg_W&$a&LRo|V#iR=9D|>U?n2j}u9sfOpZ=al!S7*rZi{YyqrjZi~P)uF%55~_E04!0M5c#isoco>Qph^-4s2kUCr)-BX`)W~}ULBW-QYyJ4!SVN_&Ib!zvQ-(r>UrGPqm*w)W7w(%N7#^4_my!Q z9&6=3uG6GD2UcDAnbW!?q@3 zPhLc8tjaRu&I^0(B;V_=OVs9XV}QyQhuPz}qT zpoDB1sM4vcnA`C`u;z*Q2X!+}??EO?#Cq)O%@DMV8FY&=Q6qaPP}b$O`a5p20))M8W!#M;k+EXqEfXd9Vi{A=na`tdl+{LCC~@@ushpHBoi`;Tmt=zJ9&PlS z-DM~RvO-8&%mV#OHy|QunK3!DuNm{C#7skcOl-aU_7p16JT7(8I?1YrKr}WYyGq(i zs(Vr6$JgN0Ic5wW(RI}6x0Jb1yDne;zwQu#1`YfA{gD0cW9M=KkDNm16lw5D9GQoO4 zjykY2=iIB#*|Q3jUFF(JZvv`wLW;6wL+eUW6-pm@SHjcFus*hcCEh{-@?l-2Mnq4{ zxvuvUu^o-B@eJJ1-c$gNn0ekoF^XgkilLC)YM_5(7omxXtM{cG0OmhUp^-iPW{o@L z>`K(%SdsD0W+G!+Z;?Xg;8VAKc(vd2uVu_|cP^6y`pp_zX7>yo0rH_Ve`u&Wf|DeI zdM9@Q`Pr|Ty6Q5;%@;?4z-b?mT*t+W{P@%903s^QM@6Q}lVcKS9V6c$qR5Jzg&U3# zyel_QX+H0Zw>cy##&wX&IbZWJ1{$yWl{`nEYk9JbTioT zp-mR3R>$n}LZf<+;iD>gy=M>&t(Xs#!Wm^!%vut+GS2U?pp;DGTF_#hOQNqy!um zX#(D7x7`CWi~VS+{_P*^qldA~^>x}JGx#y%YPQPJ!yJ$Deqqo-G-_PrSJ^R~A*j0| zX{Kf~6^N2~h3ars(UOGj%CuhO`!rJJPyeQA4T`hw?sC;89wZ9 z%ikEw_v;(DGGvkZA@88`%6)@DaE@Ftk*?}M)y(N+q)OlqVr3lk$W{K}xkQyVY8R9f zTIl_BCn6n|HwDRtRE4lUY@MoF9cYzzJnr<#@6}-PvfSr<<4VUSMo_jWUo7lWg~Ud| zN&{~Q;HM2&Ml*+FJ@jB;MVp_>bn3IF8b_Szhf& zi!x^;cqUdCSkSnzS~F`;p^;U)c4rP&@dXK5zP5G1W`nDBt>}Hd-UikvVn&YA+R$rm zx%urWT^lD$^_@jWuBYxOT^lD$vo?;>wUKda8#7Dsi_hx#knS`m+&tF>16cp55P`^s9qL5qp8QVIy6t$vx;TXm4Qa+j$^fJ^b_u9Ujwl*Ejc?VLbsmI zT2K$V zfi$YJLfqx}E0P)4R^zd~#|j-|aMiPgme6pCzTTH{YZTV2+_W1dTa9JwhXM82Vgr;< z4wgrm&L>^;2U7{!B+KxvCoxkhpU?Y@?I+3HaI`LFk;x;97#rXTR(s*0llrD?#>Cf# zR!#-fV`<;5LkMl#hq1@lZWYT_TLn2g6CcTp7OAJSHZ9Fu3r7{><9XU{aGZo-RTPCK z4IHUv2ETEgm7;#Dsc2cFV&O$7)*kl;_sk8Q_eyMec#%Xh^J;9Uk60?-nF?Y|HNmx= zyq&Z(eSO3-w-A)2(nl;n>xUMEY{0Pf>;do%-*mIVXc^R7Z)6xu;xe>}CT3;Ne z4jkT5J1Hk7N@;EIj;Q44hU%%U9f##_Blh!O?9~qkU)6(PsavGDP zv^IR_(M~hz4b;|7)iXL$xsul>#yD{gnQ&cGr^n&wbjVOMHlEUk)7Fir8}G!dxXzVw z@Y!K`TesF&HcLq3f8IdX`B0y0Ie9jk05r||;e;DPPR3ZhwG2)r&`~xA6ZiszjJ5HM zduA)8_1!%(WF{U$Y4CdplU8=(T?Z;1u*&i~G53ZScCu$MmaN9IyR0y5O^(Xp$0Oav zV#~w6H$K%nlNnZH+0GYs1EKxdgFTJIWt?J?^=LGfjm@We8YmF@KdFbyI3#1Ku-tjh zHu6wve`DkNke!aq%B>`P9YhzYmS>o>{Ni^0VQDGDkY@d;2f@%T`+V4Jr{n2qwIQ1` zcpJ=7TIbp6>Tv!t!?4EgQkBjJDYFz_HQ~5Bb9hQ@dCr7|Lz7aUM8#X~!y-bqRT0}& zTSGdQEYybSjBp$WRWlPB3Z35w1+V$(Zp29k`IZ`ojAu}lXW44(=zNPaP{FA?Dv#>< zxk>cCiU%y2lAvc{rE>TP2B#-FZOl!KqqHVw+?h#@qsgZ`{w+U3*SCnUH5em9Fvk2! zxcdg84b0Pp7W66Da-*V`tW?4>#u-|UA1b>S=*y>u{_Lb!Sehz14k2obVPjrmq^Z=$ zErU7BOlGjXWktPYyTiC2j~Dgi4c{lr4WAhL!hNG^-q6a#MQcN=%*#irJ*!gCYGX#n z#o*aI)<8&Xc|7XL@RT^@!Dn_ZgSo7l#K5Mt6u00gd(Rn;ji-$SlTE7$C7WKJEeIR7 zF`!w7NDyZ7Ax9Z8iOd3+P)IkKNuJW$@Ss>#1%8ANo_ic0laeR(ncI`-xfl2O-jJW4Gn8~%ah5i_ho&~hs^rY3s9UJv17utuY6{%wLj@vLqSFq#D}6X|mRhL%=^ZN>3Kd7E-#iLPOC$AJ7(v)v+eOA>I=n~_qBr{*0 zh(!BwWgA(FmxB81`cv6~%w^!9hF*Ff&(;_;KV@xnNaC9E5LF3UfyP3Wt?LSHQoeLpT(oj+VDa-n2XA*D-P8Loz;MicLmH> zH#@4s&K!9}1wQgAkDpb6FOL>c7HcYg+#@8MaIci7-`}v6t|`C+%bH`5>;3?t%yR2)a!p)uB-6PV_BnuR4U(h zjIX@=eAIP}Dh6;|Ly+%|T)ECr(amt#S+14ffx_&I&PX!?J^s`Fl(pia714R`W9W%0 zOB&crS|dGuWEJ1T9MualIywhr+tBqX@om*c6N^^vs8YuyLb0)mD7};*DiKO7eE!;i zGoP6a-%w)V8)bg@hGOki_wGJ<$Z!#Ft5S6Nd2d?fL0xJ1@197Tm|mHZUUnG%3al=$ ztXkQj#&xf)V#!TT{J@E-quMZ{w`rTy5(y0T%Cf6+?ihL)T5rcW>5XzbK9)Qlgqy?B zv8Fc}V`lOlGNs6HaoCM>h>}U6@ul_3kn)V;=(k!U4Op=bm{UmO2eOh!Q&6k&o_Vtx z9dCMpWe9hq3O@OzyKL=BzTM&|l}+?^t!+Z37mjR0_;_hh3>=PCYMcMB-U4-K^1cPP zU%7S|yZs{eQ6sPOIufahnsV1FKtFNueDKJ|(;yNwm`7%>C*{c$24ko$W`pXNB*CLnNKecsDMr(QFPo#&$J|O{dq>zz@f&HRK1_@g9dvkkf_B zpe7^c`{J0t0AHG@6QO_WLAaY-dY{SKg@-d5lS{X+z6Gdr;sDV&a=b-94Ci zmBHDrCo3dxc$0KIIFCaKasHJ*E1=|RW{PGstpj%xi*`yf%W(Fl1VaC(I={)uHjbj) zdBSHqIbP)hYlh2G>v#UR^AxS4CxwPXsf{E;i#;AX-Y`hwk=Wzm<{smb*yG{BG0s#9 zL#7xmC=ZWfPNxT>MBV!2-H3B3Bw$d2nJm)WCSe%A%V5J$MY0fF7;ZNXq0Ea$E%fb8=<}sf4b_wFz)26 z+?2U-LyM(y4rWswk*l{auUWz}C{mnQ2imKaBCtWdA0 z46MqHM!d{kh{^wt|4bRafB@)$MCHw->@XW9C+GV-pdTBb_Tdq{;fKi88+i59J=?*! zk5OXEFjU7*g@<;ox1)G7xRf^;9S4yS>!+@m4V&O@f{f!{&x@0%$a1MJ*|RC~^{kQO zV>}*~t4-$>>$r}k-4l2nd7LKfy`A2&V)&{U-<^)2X69M&oG`?gmt2YO>N*c(?8}oY z_xQkSg{q|)&(pjDw_vMa5$-`f^^xIvlkPlT&MKQI!Pvscxvg0fBWEUt+f{qR(hW_~ zL#2tU*D0$tb={47#qW0iRMr_TyX4N2yv{3+xunvj98~65QXMh#WV4YEb~t;% zS?i>9g*s(1BR8(vT$1@nzuq2vmzom2LuVDU6i$nvFuGt<{22A)4s1THovml zcRjddz3rjDvW5;ytkf_cV4s*M%!~%I0h*+>nWcYgO^J?!sk&V#GproUzy<$=ypAu+ zIki7k>JBSHkyB_xYe5jgK`&|%?p!96!(Zev~=>_vyg3qN;~Gn1d>NUD89-G zCRBiXI-V};jlPPz&6M&#kVAqVJBORmus!z?eR!`kbJ7pFkEmhShmYM?@Z#P|x0GC= z>svN?zg-*0Ru{7fZ#FhU+DdK8|ZB6c!G=nom)MCx+ zq;G6s-pyTrXR51Vc6Rf2$iBG1<0TyGoOG7xM3kOL4rztMm1);PsFS3H>Pjp|vPeRIE_u>8 zy2KX{PlwKTRkgsS?}LF*{~VdXj;%r6JVPn}?i?zQ4q(FF+!yGUY=q2NhIggiQw50P z%g`8UZN^NO1J* zs^v)8Al6gm_*ec_8gbugjVe8xKHS#vZf~g{dk@V@Ct~1Oh7?a(!wRA4{T(AI51A#L z8s%)k^65(H+4N(H4BuIbhG2A_MW6q`?gXJU9vfArg2RiW{*BJFkZ0?~63hypYnBB4 z48=%Tj?!((t;}Icx$kon-`R=A7h8Lb#*DFOqbVu}MptUkg*v~eyt3MdL3OXQIU=sj zSoM_kY*18Xg!C0zT3%O&S#y5nX>;V}Sw3H%qoMiCPCQh)W0My|%^~T*yzJs8=NK*J zGk+!8{fM)Z%~qMUQRN#^7!kI+vHy7FLUq2@V@|Yr7?t6(B%Vy0m)JX9FRLO8xt=cY>pmn+ zmY@i45>H_aI1R238*@JUM|UJaQx23i$jEkibb=fX&i^KNW;4B!n?~dACssC|&RBHo zW~NeP!*DcNww_YegBBHrvC(>CncbalL-KUS!rh8*?iVT?PiHLX(4P6@qZOQ}Uaxs& zZNrLk91r7TBWmAuCPUh)uF|(TaImlqNydd#U@?<#H9-xDD@$|$1bP-dPlJ1${PF~C z3pbUsgwa_qCUkt^`^Ku6N%DAd9TkV+U!-GWmX+dNs}0 za8vPRYO7ssO|?<4(e@2r-HMG`a@vQy(hf6Uy|}&Ds#WywxNm8!~;m1s@-1 zmX_F8RjkBlu~=eOQv;#-X;(qi%~km^a>MjCfGL*R|D}Gl(^iaZh+1qsLEvsCv(_Xv zEVCYEs-p>1FZ$wbMjY0FB(vJ|^KzicAz0|L4=+_&JS&kMm5f+O>-T1>x~!X{ z31Koyrb5YyFXVjo%N&06c!ta)W_|h3Q=tRB78=TN0-1_C?p;x3M$9*gTq~{4*x~FF zw7pzAtwuJ>+8PuFq8G|dfzut0rYOqz-)U+vQPnz2Fe;ofZ3q6=LW>O#H)_tli7NV z9P6N$@WR&iieo)c$3PLMtc*rIL-1|82eBJ$Qe&UtTXVD~H4iAheFvuO0)p=9b@FBW zvozn2^^uc_M)hBIqX$!MRsoVe)Y(^j!m$dme|(R!Cfq09n!as=}gu* z3yznSX{g#S=efeU$*9uWpwjlO&a-iqFtLG{s_V%-O2((PRw^3_8FA~=O^(bZC6Ljv z-IGoki&0c^B7j~w=Q6NIWW;q@sfj}93WFId!WbDv*hHV8%P3Fl%DKu+Vi*uyTW6v) zBbFx$G;1T+i9Tx2G{H#%MkX0+%uikQL9s&iGTIoeJEzf;6D6{GJ!O`aP!*>g2=>$5 zbgxyrr08W5lf9uFhUk2w%s>t%`IMu!1@+LS6)TTcpi?aavSKe}`Zy8IcG-B^(2Y)8 zR0gX0T~trRKWN$xr#OZjN34u8!I1zuN~NbW0;S{~nI>ACJ}Ls6=YbMB>btuqFu#d& zt>gjB+2K)~621~eXPd;Cv<(19z%;0vn{<>t2_>`-M$)=0eGef?uD7hg35jQ-68i%i z8bW#sWDQ8v`Jxp$J&Y>^XC*e2jB{7*kUS|^s5J9ydc;g6q6NxBrM9LtU>_lgK`yK~ zOn!Xi4_|#+!j?t3q!b4*_yMqcJuj@3!*C z2o3;Sz6OihJrsr5Mt2mbAw#L6O&c|Yl_4(r&lWrfeBS6tkhXG7w>D%2;~sXiOJ zBl8%}oWElSDFsWPsd3-mvl2(^de9bZA51!`2h?CL5}vRz-pNiI=RgkTe07d37mj5H zaxh!4Ror=hP5&kzo?K06;1-6q)pQ>2t%UDj8Eum#@Q4=XWUuaZ0&&QrVXA!KMkix8 zq4-8VC+6V*#;k@RK4yo7Hyca+NKcI+t0SSAE~ZF$wLe$uDHdTfy;G~LKdAtfa-N#k z7$%4G{Eo@Hm=ZQ^{ZyyWE_v$FR)CBmmrN;+vDALJM=po1eJk*ugI#v4;qZU*M|yxte*Qsr<`qJ#dOv}xA83B zY7iHE*51xv^PpH)bIPf>8VdSSX&=6SA+r+G@;&7STBZ#wmjGL@AyS^veaB2nQLwp; zn9i&9QESc6+XWG15AHPXLm6)pbQFr)(u^{#7^l~fE52Ly)I)Yu8L?SomZ*_}jFHqv ztKDLs}DCe5TNXkb`Q~3^$5|m+Qw8qMH8DuU#Oyfj~o`03EMb*AWPWFhU`ILIx|CM)P^=c`0tm=aC}=!vOX5oYxU7O&cV(JI@IS`THjuoAp*V! z%Tu0^;xf((bce<=>W*A7R}M#LT4>|dS!oQ&gG$}ux6Tbu8MUzy3=`tm8WL7RX(+&B zK^~~%KBrnqjxr+lmsv6{^@2-Afv@)9R-PZ6(nmr>!Q`8fGH&BcT2DfVtJ+&94H0Ty zhO5xD7~idp0cWk1Yi1W-g zA}ZgzTVO7k%gU8>uXR4AmQiH9dPVBe;aTc@3k>?~(OI&I{Pp~rgP%-_6{!S`L*U)!?vYR?l9Fx4wC{y)e47;ME49VCFGkhJ4oN6B!YNb<(a&0wRa7yeDVH=jm(whJNn5iw3 zaCK};>QEh7wcU}eOlPPKt;{oRuBF3>6tDC6oTrV*7r2Y{VP=%1He$6{t}?0qBx5DD z5i2h5$wPEFI)il2ZVq^hD0~t3*5MIHZgmS7k;tOF>0m@zmi~NOCcRl|#2L`dLyBePR*QoXI>mXGZ1p2keD;cZr%b6RAm&nnJ3jBj(l|t0a~Zv2Sms$viAt+1$z0Rfm(~0cS8C$N^KL_IUUGK=ztq zX0EvBp}5*Aq<#$%Td$*HfjnC$ei|+(B5ZB6Q_}0!~Gq{biCX#dW zYyNxNnS#>%&-WY~WFNSbzB?l-l1>5)09r z!3_>#wV7!IMpx>^c2K7&DuxzY-da4^<0W<-BjmBBT8))SuO+L|3D-7(UZw|;#dI^R zC-&FJ@Z}N}?YK@#nJ2e@_=n12y&|jQ*r_0D@&y!*;TVocZZ!L_CJWxyZ?9TFMRp9rpq%0V%yHnndj2Zo-b^TpQ%4g_V!ha9o10Gq1mFiIX{piS0 zSjG%Q8qIms*(paM87WA#K1z$Zy64o9{#=XW+sl`~eT2skay3)pRnm@6Z{w$4VFR5%%45K-&E#MP-8sPCTN zK*e!zgjAFA45W@NWW>ViqGHN5%5$Pb);61sL{U-{HGk`ir_`OLcB%zgmD5$KP{A`j zC9P>>JIX)^zh0m1$1kdb3B3$cn%`fgFQd6znL`D((N*ZRO?s$t3&fWi7M}GYEG5N| zYGRn*8%BAo9dU-eP8Fp%zz^BU`l)_<+s#}V z`;nWM@#YFX{b4E+v0S;&tTgmqovfTCytO6b)Kdi9(5>N;g1Pb_9D3V`UQohTO)tV3 zCEH-!Yb`tjUlU=gBWLoBG=XigbL*4@l(xUWg?>68t&1r1!wE)t zXzinN^3J&AhZ0X^_|4HWwo=zBYjcb;dSh9^&Xyb_S7rDsv8#_GR>Ip&&X`g~FQboH z*62Hea&3I2v9V#tgUaZQWtZHf*3;1)wo$7v z+|g#Uoxt1(7T|?fYwn9(p8x1;?Uft@EcGDE1OED!25d^&&U3OjZO?D6V>6f}H?NA{)FfvUR~A(S%{FU>gT)KC;*u7z(q#{c)$ zllC%R+_i~VPk6E*j@(}m zmjV}TPDR@XielHN6LInHQZe#GDxQJtF#LV;OH#23m^rm5F8*jLX15l_d3Ppa<98C# z^UXv&b88}|aer$3O0L!iG7*S)xI1#c>JJAR7t z0csB-em_sd!?-TP^^{@w89`d=iMX{ohE$$PVPuv#Gcf zdHd{*h}W%&=(@fjwtNnC;IjpB_LfB4h2KwHR}h2vJqPj`FDi=7`%*FOZ>hLh6vh3E ziehG8QS9g~ifbQF#n~TD#g!jQ#S`bJVjMUaI1l!hoKX~)l#1d|{Qck;3gVhg1+o6~ z1#u|Kc|WeBn+sw9@+tWHY5aZsO$D*=hJv_%3&o=#p8QfltN{wZ_AeL24(K{?-SowR zn0h1X7BC%ur=LK)KAwtWuSmrlTsPo)&c{;m8ee zFCy)mQAWsz?O#Co+yFe8ij8|xF#~^3gZ{CkD6X21y89gbp-&zp;w-m&~z}~MF#4O0xKzHfa5qDgt z;(8}I8+R1M-QOsP6~IjV{n&?5#yyER7}pKwqut>;fZuyi7q7b={%)gm6vP5x75*MY zKI{R`xiS^^1IPccAg&)Mic9;8;yGYvr6^|OcQ3Bf@O$A83gQmnrkw>b8`yOZ%I2E| zvGw#sjITrbDa>W5Solk{sokmAemCmk&kJJtg9R}c*ZXii?jh8bpP}wNjPe5ayx*a` z9z}fcd)luG;%ewG_(efH0~`v>d<5kPe>dRT_*5z$eGF;;HGc0dh!Y+!h(}<1CG0wW zTM(E3rXZF*0YA7N4!;fPp1^fKey@VvOxRovo5ip{Z7cHVI`l=KNySuLx50kKrc@k* z-&;1O;<4+|2Ynj(axMBF{C&x1Q*k(O12|ouOND^!HeBa`dk1(=Aa2XSo%#EMIOHC* zy~7f5+AEPCZ%o8fz@@KB#99A@c6l_~BCc)FAANWtcE19B#8mVRuR}XM5^Wm#ov%il zehuWndi)-JE%>j8k7?+a-hj3VZvV09?}&FC`aZ}Oz6oIhQ{Iez3fOx*?0|jvyZ4{b zMo&n@Q7590dRZbi0fm<*;yGX)P@94}0^Ef79*4M`dKCJ0;4y@I$}x}w*CL#|k4S_- z95=ls5wn3F_#8z1wxlrw3kv{^dyI>k-JySEr)$m8qEiI{bYU{+^0H|Mke{qf>Fut5R{xF(?;sc7eO) zwW&DmHL184oUP!@1^+(S-UQnnkS_#(Ciqu=1#J%gr-OgN>x*K>?~&h#PumRi$0sM^ zd0-vx4^ORwf5=We34J^;1@g;(in5=DKJgvMKU`12^{$yn7x3uY(Rc4a|MgAuW55%@ zS+}KPC$RAwsW{}_=)d2EejB*<6x@H_i9Cmo_tR(5mTveh z!hSFM;nPs2?@7e0Q&A@PeH;EBZ^3;Ozn{W&*Bq1~u4m!89^8vyI}5fe`p~9(kzTaP zS>HzcLR&lk>QtQY8T1z)NyTNCqP_!zZHc%a*aXfkx1(-@w|!$lTzF3^4h8n@M816= z_uZc$F6e_U#`T)5XiMiLV)?(IE&zjP<6eZ{M|~g>ttHeoT<5Gnp5nR{zb}Vw7dSUS zw&s5Ht3O6xID~ueE|lL&@Kz;a+aT(66?GK97x$tL0~`90X5ioo>;{m=pQPgH)wnM{ zf;#Xp^7ofWAJB=vpZZxU9x0BX8e6E!Z;k^?Rqd3yMK|2 z0&q0S_H6uq9C4fSKq}V3mjJf^9QOg}k9!DxKX3}-yAxsG^dZFWJcI%4{2=b*_kVxzK*=U1?j=x4{pQQ;M*wI zucA!0qM!I0#zN4~*p7Z+2lDVXj1f=|wtqPh7p%cJ4!C)FQ9ONmQ9QpY6|+{Lt}HEz zvlkS_6AiSz^N_E#R9y4^qBy6kD5fGVH@D*cj(gqyXH#)8F!LGc|CWl{Uy*iTD_6V=^nagHUK6>L`6)9iin7am=G}`h8GbK z5Rss$2ndLXiin5^i17cNQ&qRZt=j|2`+VR3`M!OgyVG^fsZ&*_(y6Lbr!w&*!XL)p zt-lL7<2&j(jO+N0LYWq{&`*({b>D(KuSmpY6N#9z8hsqUk64w6lU8Cb$KN~f_uAj0 z&L2dZKwW$pf8Pq4^FSM)hBkf?bWX_PZ0G=Iqf8&a3Ux6r6X!pXiY+0_n<2w(AHrA! z9v9;8mKUQ9e=-q|;Y;xycM;P26#4|d+wpt$k5MN#LiRTyZG0Eqg8qPS7;X9r=utO3 zh`6su{_e*(8fW4hd}sX@V*y}y0Cv~|=)?Fn1MCTWOF+N(QJHw*VYE5Sg&)Me=k59=i={1G$wDGiA(Su|16E&nYemu=#bk0j_(ls-gker^8+(+!2ziEgV5&i z`)qvIOwGjJhe0NXqYhr5iEkZ>`p4gO5k3OH@7*^Oi}%aKf}dh<62e zZVP_PK=b7q%Cit<-5Pn@3wpqOw9CCyaqmp@F??q%z*w_0#;;vamf2|UeW|#4F6N?s z%tP}~mL9ax8L2p@7v;fs@lmO`0yI0~?`^XnpFPli_e8$|O)ui#wofWH*#UCJxAu;y zI1As4_|65*ii0tR<2(5f$P(YxyP-YeJM$p)X?zQIMZ5!3k>dBSke?F|g%%6*}8ZjZSUe=o$}%@064e0%PPz6$)|uLpd8(BPZ$2K3h>u_l{_@&Lap zc%t4;;Ek@ZNJqG-ahFkzYayrIGe8;0)heIA~LWZZ|+Yxfv z3^cdzj5@>Lp@X4E?vjazA=?Z0%)}i?Z}YuSN08BrklXWnL;pm0_uZjSAv({tVzlpT^ko2jt}v%yalQcmZQ%1^6{EC*bdz7+b&5pNSm@kS4yF_`U0Ftg#XI z+LzGJBlO|FV{L%Hy9$gW_}cqpjKO@ntWRKi5P>&P`|*n@m-N&+(4Xj*TFc0@41a9dy z7&Cxh@Ex?vZ=yYX8)f(g=IC!hwqJ*igx^;qZX3Vv0qvupISII{uf^Q;U5xK3<~yYK z;?-#LfSrxM*Mrx+;IqNZOgu6Nef$ix*PCMT;LXtKKZfz*hFCPOkHvXE#QFu{>n_0f zgs=Cb=%e@^0PaQLjs)EeKf*lyW2`UmeHc8Z-iWyxeC7eZCSU`%#9}?rUJm%w>#)W_ zcmdWqJH7>Vc26vxzZ>h3JAuRRk)LBtg>T_6ur9)P>0MYu;5!F2o8XHtgk1j}i!po; z;yeCH=udw_T91PUb#=jCkl$yJ?h~=N=g-g+{u+x*uzqOc?`HqRI^tQ>E57&Q_qB+# zE9f_Q9_!-gkdLRq;~${K-)F$vZ5Q$M17Md(DrbtKhr*X2|OnjO#a|EcksJ{vLG` z%8Bn8q_OU4DC?Wi4^PMZwgPj*DC9E&nT%sT@1T5df}XS#?crqfQ`G%+fUQEDwGqCy zg*JilWXh?K-(%2^9))g;zwsZCe|!()I}fn47hw*-ch>Fb6Zk%k{fOJ{K%d0-BEk>+ z41EsYk?4PS)KE_tua26YiQDnb?8(Fr@ZEv2_8g28XMX}|qV6}s-#zDN;(5UK9f#h9 zZ)fO!hqbXr=|B(0-)l~VE|1^O9tS-d?dVs7Xmj{3S%P|>fi{IQKXDTFbW90MbbHk@#jKy~u(%ArYGv0)5fpPwM)XzaE+rt>=ulx+= zol7ysqaM$nKpVjK^Tm)S>h>i3JqrBxMIKL_i{Hm$uAPPUx&(9137CK0l!?Oy=G~Vt zzxH8&5BSI4jJX-#%wMCNccblZ3>|C}$P?ei>tl{x5B(Q^H`)O4@q5m?nOL$8%CR=; z4u5Yz_{!I0;vjsh5clmHp`xfI`(&q4=&8hY*}B|7&1<(Th0OVf~E!J`UKPNdFs1Z|gUKAHFHz`5@96zYVg!6Egn^ z^cQ?Tn1`}IjdkTG!Sf>2#}nX>-%nwVSb=n&y&Z6TFCe{zkmr4<(+!aB)=2XO{C)P; zR1Bn$VFha_)Yq2yu0=c9`EF*Tri9|XYaev?*9#4>nA8b{!YIWZTe>@1HSRwu`dGHCU>KJKgHaB9Q21b zL$5{Ky&UmQyA^8_e77U+wTO589r%s!NyK{*^wV#H?D3t4-*+rTd%Y6n`U1uf@Oi!p z`C&bE{OO7K)8okVljwtg$2#aK=p*KC`%C!Ui{G16pd%rC-I^%V8mX9vzek`CT(wyy zjvPYT=)VIP$G^Xh5C@5?(yyggr{7Ayo_-_!X8K(8N#y4};$7mM;@#rC;w&*U{;arI zTp~UrE)$oEuVB~wtKus0HSs0!Mezl3h4{RL*ilap!kD$R6Hggi=GyL6Mq*^i8rLLPj^fAO?OTANe@YI^sdPMrhG>f7rj?yTJw#l~6w#uet+h^NlugkX0cF2xO z`_eh-(dmqIW;!dKo%W{BMjJ=#M;k^PMC(NxMVmysM6ZqZigu3nk9LUmi*}6mh<1zi ziS~`Qk9Lg?iFS&nMteqkN4rPIr6;GGi-qYwvL$JMdQv)=E=rG0N7B*s&FP!cW75B8 zPiLp5XQZd6e@q6_<>|6?D6OZf)5YoWX*E3|JuzL8*3zZvs~r(1|E#k_QWx*%<( zW9ez>ACf;Ok0*ah9!r{OmadWhT>L`ZEw)UzN>Qol-;-yO7n6S{PbdFK(sbjrlCGJ? zX_T&&u9GI|1<{A2^P>+%ABjF1eK7h)^v&qn=)2MPqVGrFioPA)7~K&4D7rqnF8X0~ zS9E9ei|FUkFQcDDw?}tJcSMiGk4BF~_eT##4@3_~zm0wq-532K`hE0J^gFbu3P#E` z#P!*CvhQZsW=}`|h<=~^IsH@mkMxoB_vs(fN7I+mC)0=1-=%*{A4~t1KArwOeLQ_4 z{cHM{^r>`?_^WtAd_KD(`$G1`>`U2?vzxNZvzxP9vIE7z;t;WRyneh!yiUAUoW<+K zYsTxwmH0LBF7X!emhqnPKJnJ^R`E9RUh&@Xw()N9=J6)+?(t^vYvX<6{o+mIJ>p&C zDRD=fE>?;a;xw^JOo%hYxL7R?&JM^9$_~j6%=XKsX8UK~h`$y8PyF@xn)urI+wphe zZ^qZfKa4l6Y*y)wd*bQwjQDZ!XYm(tLR^h|vgz5htT&sH4aO(MwfN0(f4n$8HeM7D z#K*@A<746z;~C;G(I@7KL&Y?)KpY{C5pNcY#GAx`=ocr5CE{3doLDFpi{r&yF;mPJ zv&0)P=k$o9MX#7Hjuf-S>&1!Ujp8Wri}>#N=kaUBW?~!ho8(W~itLf>ciGBpX7;+kk*v7Z==uNN1^pNek~H;Ws^E#imbN8*O~9bzAGS8}%4OYAAmjHim5 z;@61vF?y^c))ilkPm2$TPLI!s4-mJ+E8~xM#_f1@d~3Wi9*_6H+N>GxB7Q1Q~6i+Lbg_A?aK4nnw4j=wDM9W;;L9GnqpKe7Y#8YhD2So z#Hpe!#>AjFMbyMHF)U6JCyU?5kH&w9AB%q%|1thk{B-=c_^J4b`0w$Tvn#W&WPgk{ z&Nk0B%{I$kn{AS9lszB+BYq}+HvVV)T>RR~9+mAYJ5+Y6>|U8td0l1m%FdM?E8A7J ztZY@;qOx^m+sZbTtFo_UKgq7nzL9-9yC(Zq_SNjx?3>xwvqLIJRQ9hNP}#3?XyvfV z8!CHOrdAHG98}q-vTtS2%7K;FR}QZ1Re57&dZnjwWMxKWTBWygfAZVpfn-)?c4d39 zgAhraycj1*l)MygnyjC!o4h7jJ6R{$IN2cCFj+6zDA_F8B>7nMiRiNElhGHWi=)p+ zUx_{wT@`&i`fPMz^o8hC(dE&Xqf4T%MW2f7^26j^$q$m5$?f77$ zvrVIaCga%~vQ5NOSyb7$5?9WN-^C!3c&pWL3lJ08uN*%{eL{I&Sh?EUdo@e9eu;@PB~y)RysotCZ6-Wi`2 zZz}q;qqDi$bIDHGccM3D=f+2-=ZlYt3&cmoM?@_f%$8;+W#3J|mtK>8C%rcPLHa-G z4e5{4@29tEeG*QY;Bznxx}-kRQ={y04@`>wcFd`Da(z9+sfejvUjz9GITzAe5k z{vnDx#>OGUD+?PyR+-#?V{OHUvxzDhG|uReMj0(-<6)3zB~PD^wxCSbnA4Jbi;K0bffe&>1OF`(+$$~(kbcM z>89zr>Fd&M(#_K?(%Yi1q&G!Bj&6=_iEfR465SJBCw`RuGx|&PO!R#E0u-wk(|@MV zr2k5vO`l8u9sMhMFuOhbS$13YLiB^|-t5=eud-ie-_O36HL{;(8^#;N2gk3A-=DoP zJ}~}4bV&T=^sxBQc<1<_cxrrZ_WJk@@e%O>@!|3Q@lNsf@s9Bh@pkcT@pt3z#XpFD z68|**etcZKBz{x8G_J=V7Tbw!#p}f1lBbeO<6nuJ#2w-;ai{pPxJP_8{&{wD{F(T} z@h`_G0$W?7r+b*}Jo|v-8CJ#6$7J@n7R7<6Ff;*}vjlD!Wy7tvsAPko_>bKl`8T zx7l^sjoA&^$Fq-RpU5uAF3c{_gf4*(KR$vdgm1Wfy0k&OVi$mwhn% zK=#?}(ria@ROO2Ji}7Xg=i)EKm&c!vD@m55$yUkhlC6_%lWmgClP!|hCRaySB%e<{ zom`T9Ciz@)X>xh;+2pe1+T@z#E6Jyl4<;W;zMXs{`Fe6|^8MuMbl-`_NnB0>5I5{}kJJ~1MKRF=TIhm3im>iUBpKO=xlQ@ee#p!w&bSd(d3uOJ<0Cb&e<;6uGwzchvFl$pNQ?!J+ghX zGqbbf_r~u}`zq&T=cOM=N3v70?bEks9}qu|@5uJaPRMRgK9_zW{bKt0^aJq*l}D0? zl82MuCGUyflAWHtBYRKw&g@E`>XJrFfQrV<(WOjJ=`s|bGhr|bk zi2fyB5HE>;ix8D{h@#gIJ*`wKG*&nh$ zXBVUwrJqPIOe2A_$AXUgWVB}TN%8T5ru!*j>$(?-%S-j$&p8_JEa<$mq4mBLmFc~| zR+{hcq5VG={o2xhpA`Rz2K<~T>cBmz)`F*7dhj%8!cRE5@Rrdw(N@vc(dN*K*NI*i zZ5vIAws=|k@n)IXWe3Mx)V4)QZlH-W82SXGQOfPK~;2>wigX zef@s|8v9S7v;Ve>*8Z!Y-u`&BMnHG}YN_`AP_DoKsfz}m#AzJX;^X+fSE$Kv6hD#t z=i2<5k*&{9i`R;zPX9+}^$T96UO)d;)a>t!e-_^n-xW&@|8~~#f0dAyfA9Y_dj37E z>AxoWO?+>BboBBy{uiT{tMfk?yITJPx!(V{rTO3M>Hc4jZ0&!oD+g|)$- zx~vY4&DP7-&h#1~%TjBduzvPJvO%_CrdJDVWb0;?Os*M{!m2@Jui=$LjJ3lK>3_F+ z_*?Y%=&9(_VkhyRtt0*#{ok^Z_(ghm`t$UT^v?9I^k->!UGeZqczB~*sMZMi7&*y_;6;gMGi@dtC69so-KP7)+9#-S0z`+ zdR>B(@72@r;%!?#Rz#uvnRx3rKVfVm{Wpyj&VCzRMpth#D}2cd+BvrTLH5q-W^Goz z$cz5Hd2~- zs>{DVSTexd+LX8x3HMrkkVVt+6E^^tSqC?#tqkcPhA1vz7}K#TLZh0;kfM>ICb&)>rO7}pgipo-W-&cEocZ?y+E$B>i(pn~PyaUZB+ z_{9D7k!22yoR8XZ5$1c-J#*`LJZ?y}Q%aY6mm)`yT=L|dt!HqoGl4L&L#=~cvw^7r z_SQS&?Fo9V(1r8EDGsG+t~$DW4AY9&*I5h?|3el;=SpUxKa>dogP8!_%LE`+Ch8+2 zR4Pi0eyWt{C+G-D9V{lR<-44^z4< z`v9d(HoS{>gZ-`Ws`yAm|iC+9SCrqa~KIfT$qJ*=~pZ#)c}`4I5PSh z-p8XSt9a;>1(ra*({D;aJ+n6M335@h?WewE?wp?C!8SZ;7E@&JJJtDPHHf*NZspFv zE1Y;HoeExpqZ&U7-sTm}NPQK?upT1EPYX!nNeQ@@y)Vhy=V*BLcyKvhev@9AWa7ja zVieXPB6B(D?XLgBhOikW*+Lrsp_J zB%NxzrTRIBnpzu9|Jl{xg4HqzDr>Yr^c;)4s9A04AM}`)?Jk2Qp7xA zyj_$3o?FK&2|8I0P;2k6pE^|orC^zh^61IGAuRR8lO_bj&Yudp`BSqgb67Msg!}3` zHs+HFR{YS9u^dn;5rg=hOV|I43`uW!n8N1{ zj@O3sv}V>3Qjv4zf;huo4E4q-+Fv&r^b2cDV0LrTc$<;oB<1OOlc&KxI|N$jMM@l z3sxaj)my;+iKR7mR+`r;cUgWNs|aEhS23B_YFcf<7gaulFtyWIwa^`);p~7Nw$z|= zIvE%nY1DX1p=4~3Gb-4K0!Np!-59S!iy3Tm*movG0QVVH`we;*B;9FHk%*3lIlbtx zVfiGKQ^{;l)nQ~`*=plC6~%){NbmB)vgDKsQD`6R%rFNS8Vr5vrTUyPKu#4}w)kyR}R@VB-YP{n?L%=B{!^nQZVF&4aOig^D$csxGjG@`7w|qQjNo z0P|H^KGG9y?zn;y&FPVoo&pmB2AJqvllKcSp)pRblPR|xBC(Te4Hd-P1Rq&Ep|GmX zH1o&mc(j*5cx1m5k@H^VFP5CV@AWkzfv#Di+FqTD^&UDFs${ergocnt1Vrr)TOG zHU>|?@Dm6gR8^ebv;YgGt@-gyhqSpKy|RQ{JsV)9rZTTnLXc#2%*5b`>Ipet6s@Ej zzy!(xXebu|JK3AngB8Qbh+OO;0536WyhT8{C5E^E9;^jqh`K@U(4SRE8=#()ut*?0 z|2YT?Yj*9-o}&8Y2?Ea7hN&`1ASf6Tm6NCSbV?DFcOp%T^#(m=*cgXLK*NF_ex>G@ z*N-B1;XI}XF@7>`JSqse?<1X!8c@z+Jd_s{Q4;WAqG$n}JGi`2%lm@|9~h)f3Jb`O zEeAlfb}(QW!4J!>WLg&<1@Ghmws2zb(-8jx`@la$ki>A{W)KA-> z@bIm?D&NoR;}@W7b+xgjdlAKUF|q@)s%1ZEr1usI2rMq?{UPFm6}24&byNb@LJ!2~ z>kY*zdon^j;#f$!q!2IT4GEOPK6BuJjz)-nG*RS`9_X!3iUq&(}s4(%pL5|q)0?4lpW8-EN(T1 z7L3zMkx8AIKZn>!5e0>Pm|G&E^2)>@z0Z}iR?D2c!&-=<0EmTM6h9AMVW=sQn=$xz z#r_bvM%UAf4y(qND>fRAIRZ+&6;T2TbkxLXB-ZiNo{kRAsW+GNJKq|hT4jNcYL1$0 zvEGH53t#QS(8p z)XwfWlU{w4S6aLqgMAn%%5^Rtexi=1(IZEL^2~&=>a}Y2+l$?km9$99qatj@M#!h8 zO<;@M388}g!~mKY>#Z-HSWc=n*S6zR*!LdHVQeHVZou~_$xR*?Qf2W5Qx4Dfn>={l z7c4l0Kx+(n49cwdOv;*$oRb$7P-k3n&V;VR>pF_fvpOj}ngJIphhknpYH*y%8-Iq{ zkvN1DmP(0= z2>>-r(v}Q=3+R^yD5WbK1_TJu;b?>BZbc}kArtUppb254F>mcs`KnG^+G&kUFdq#W zqB&9rc*4|yju}lANRMSIh?g=)TUK;lgc)uKw%VY9S49KtYc8O(Gqi&u%uZd=b>lIo zGu&846Pn77ih@nvc4HOZ>>O{5jnIJVq012SX-0Bs7w~KGJ`_(CVmDu&ZMpOd!#TqC z*f_mEUEn{^o~V}yXbCSkbN0{=?by%-EN*H}QpCPk%H&a$(7gI`b_r}!vSorJQfz*# zuc9+7n@n2<9T3}cq>>;w zv!9kR@;B?Cv^i;Qtb>GRyb-xM5_%ei*(}#jxPyWppy5|!7-hXj!{Lra+G&o_(;qZq zA!3JB66*~Y4IOWhfq|qLZ%wGc3OhEt(5_DF@9FC!QWfmb>1aOa=TI7p_*APgCTFr7 zLuD<}^DOFv`3%vdr4C^fC*G@+ub}gqF9epush$9B3bl3MGS~A12-R`C6(!#TEx>~0 z^IAm~GNzj*&*YjVy;SH6fk(cO(0H+RY<=()%tth%X(l-0Q|FHgVl`Y@Y5q~4rfbkR zqyqW2vJOifMTKa~gV&@coIgI#r$siLb{Fvgqv4I%409lhnm%mCK=s%e(~e#^V`hJK zPGA4Pf`NrrjC!+<6MRf#)5ck&*D2<3H9xv=bkra{O?D}c`&U>;>;9aCsti#A0@*p9=Ne_wxfR*xwd zZS1%qoS<8Pj$ZuRFzet&WxTILBdDRt*8>VK!?Q6M8Jb*MRW!)4LF&^+g+m`unrseW zn<*V?)P`xtevl6di$JouGrUPzbj3_9qojHZjYa|d8UWa>OSRHbnE|=UfYJ;VYB6>a zTWuUl;05} z8jj*(0C@pykj+-Y^R{EyNsvRTcMzC}0~!r;32MJ;-yWffdb4IM^9(>6KH3m%6Jo(8 zxLf7`GiTT!y{nYN)R96SHlvvhDn#pRLTovO(w@;4rOl|1fZ-L^O>PXSmFATBSYsgA zd$XeynV8<1M*^sjSJj7e8ohr9T3SoijxH0_7lXa*|H5XHL?ZnOM*Q&e`#o5a%s(2?{)h-l#dNZK9j# zRQ17>4(d}wl^jt>V<|;|gAjF;WdIJrJY$lgDCUy_jZOgr(p>sh19K64h$~myUKEX) zTI8mr<#Ga04bC1TRFxXu!*3v=HJCSEkaIje0}vhFtCwy9FtsB{S(D`M^Eb>-A*Du! zL(_WxCQuHd2aF^O2q+(^)H5~t13Hodnt{m^w=ks4_HsoqimYt-h?fe<<|e~VTEMHN z4pFPaS1Y?3B6BMbwyCZ$Y$*#W8-^iBo*5K@#!}D5q395T2T}8>2i1lI9+=M+e3&_Z z@VLv?W0R{h02@RyBjbM@t9#M;-BX=k*}-}HR>nq+aKpvpA~}uymP{Dxzc9lZ6A2hw zfYh55Z~4Rry_8~LLoDtxn0gu|=Ma1wh~6ggQd*AT4axY?3EauwvB|j-cw9bNE*}>P zVa($}Iko`}RzG5t1KTK2{<=;M?SwN#;6R1|a^F?T`Iz@y9FUZ(kt&f%$+|ESl?y@H zT|n8(EyyYnwobuy5@;;6{?5ZxzYL*%ElCDewupoQlRc7xI7A|~@_dbE!xlBiX*EaO zC$O^MW-AY{^01ykB|vo&CSG92M8{C0sU%dBAPv28IOrLy4YQ00LMwT><*4>XDF+%s zd!>Guj*n@7hs#ZbI3K44Bd^{xv4W{QN~-)~1+$-pBV>Il>;ib;GK`=&@Y;om`Z~1T z1b!M19`v{iDFIAC>v`&XYWP<-PiTUy%1z=h4!NjROQfKmzL^3ghsMy@(7Qq96> zl$iJa0$5b$4BkTx(K^itLD2)PvB@GYfOT$C^nfB0KGVM#ljtN{h z#zkqM_cd|%W4s;~h==wYKoRYix=7*YuSnsQ-6QaeUqnxmLQx3j6kRni1;AhcHNmu4 zoc{6#rCdrma2f!+53ik!O-zI-v}F0X*%p=orWSQOduhJ39*}l?eFG(fjD!g z_w)zAIO$VtN0uh$&v)Lag=>m~=<;#WX>Sh31Avmc$B zjyQP(UbhR901aBCXXh~7)pmlm`h%U!=8!im#^y=DT+m z%$rYmSGQ?$WO0|MzERv~!fn+eLLU3u@J=7STq|E&G=e+|H|8xWxhT*%S^(7-XVA8G zTwNFh_Tx^6J-kBK@tcCWNopmA?H70cTxct`jge8h zf@Y=3YS}W}{8Al%pydxFx#H%>B6YMlzVP;rYW3zoLtkE}lgc)%a!}uSqtr0Sn(r|p z9SbDwY}|XGMiKPjxv8YO9;r6QX$NirtTEB%)bVk38m^9U4OLkkVDmF1T;!y5~^&Nn8#{Zi8W`p*Z_(EN?5Jt9NebYxmc{8Pem4R$6eJ1?X=0uHIxAp zl9DiC8hWSS~pC#$6)PfMD!zNNL##fVyRdU+QqQI;W0HK!tkL?QZViDRry8 zati1jURwY}b zH9!!j1^_750Dx$w%^@6!pl{)>zBMCMF;@-3J8)abXqL-D(q~6EsNWAlRaew_A9nT9IIaZs54w4NmKjiF6Jm!E%6(g?S)R z#t{a{wwJ_&Nz`d}9!4%D>2l|GG2@kat~70G(QK^1^IEX0l{e}PWF^`4bqq-{8s?&o zbV8u13@2=~5UBFYyh8*pgHIVr0Pce1VV7S_2}O&;mZi#F zi4PyU09{DjL`w*RL=JETHW!XXi`4Cn`y$Vu?W)|NL~dP2K<(zQroLJnuhUa>-#!W&Z)KPmMUSZE&T*ytB zDm$ZLqbQByjL1RS$pzW6(OHDWMMIY5d=(_XmKq$#rmZ5WZ0QsgN~)2i=m1aTo4f+a zi0A~l#Bd#8^-^5%Mzfb=8q0z2E6ZNUVF(W$p4JpbFnlRzvdgV~$fDE+8Mq3?n zN>PXNCM**bDz|FL!v#+p3L~xE@&N0+a@%2uMikR2Dc=}H(Uo$kpuW&XfPm??;s6lq zo+jvW0A4mO9bupiiyXfCA}@Wbh&)aij{+-><}$wDE{i~%9AvwL1vs`~teB(}V%uDHTTUA>dRlGR7hp!2kHtqr z8kHg|y{H$~_cUGq$_CcM+1P+nmz6Wn zX@>y1qyUv<78sJWYzHJU*^hFAgPa4qtARFNhl0%`f-DW033&y~K`_PC4Tx`7V35SE zAaC@Pn6jBoV(HX(D#_Ob4{(Ern4nbHEOH63B1nLY!k6BI`O;Uz%{~OBk>9{ZJF%8j zG#Ow>?F=qsNzzSf9y9vKTVoii+I$IqFd@Oz<}fpA zSQ6~(WxZk%Pbw~4&Epo3#jS72QwnCIL!!7BYyGAi%V$|7@u_|DfI~L@EWJP?eZ6K( z3?)PBUP}-@hrV8n&)BI5vhV98fd^th5_iBWLkN*Yo40t z8RXt3!Fpfs(QE#41j-XA8;~S3)&Q)vkr&Nq=90W!U&YN@g>c2OP(0EU^5s%a=M{2R z57#K8)lDg#ebq-8g@w@chSZQbtS&NXQX1j&43joiWrI_7W}&I{yr~-MHmbF*;+I zih17|44nIO2;*uMcQxnomx6a_ppm9|lnzKy%Z(=bAvvRykVWvDHq;t*LztkKj;OI) z4rSO^AeR9%vEMHzdIjeFV&Wukhfq-8b}}0i7E+r0$~hze28j0+U5+|Z7mh&`?M;%0 z5iXp{AYFy>R(v`ij0&tdt>~e#N=0WB8DY^}!_7n^$StQS5N!yNn5g#60zlDf*`ak= zQE;z`V&F=bVFQ@i7F4kTQ$5}R(5)LMqS-6abou2bx)DU{N!>PG0`!Ga0>(HMOVe$4 zae{&pBG(C42B9UevtSE`Wh)NzEI?le7k{Bf%O0e1i7j)ODcaR7HzoL*lS#k;o$N|5 z^7waF_he8cG0Ni zG|LAH{=$O`I*I1G(m4tviN|;+MF4Ej7ao}(lENwnWF>NSL2B`M~!JdGQ_Nrk- zqmDq3k=)*%XEpf#h6&49KBebaQjYsEQR7^ZBhfAtZGMfzv!ntjkDCoBa!A&~DSn zAy_7b?H+8IX+v}5w1||8eM-;9slMWabsgBGXd(GR)Zy+Dt}sMZli|o4vU5+y!KHt8ay)6D1zq=HZ`$nb1F)|WTL#M8CHZ= zi)uxwARLnUmx(HPwb>${gBJ;C-*FLL<<~>g;t5J#6ofwxDX4s>7g>` z1(Ky0XmNr@)Hb+7_R>15Ho~s6<)h=MqMQr@7{q3ag3zXeoYttH@g4+w{v)P@qX3}; zp>%&Y6C4}JV)i5^x2`eFF{Xwy1Vl+u=s)_b1N@$&)B!C z<4KJyLJO3Nk3855h+Q50IbHuZUF zVr8dF`VbF=I09;?%X+b(^}X(GiF9WFJY z+%(9vovb~3vC!m6!te=z=zw-9loj42G?a75NlgvB-PEx3rJFV`kdHTtIU8tUj!dJj3WSEX1mOnG=?zLV$)yl(3%_qfwqcUE0)CRJUUlpH7QhjT*$Gg`fGzY zSit&Qnh9E%fmWI9fbqo%mtd`tp*mboxebdPze0E=V87%8t$<8viihxuEP^r}Xt7L- z)Im}6#IVfR2Ex*#pLRF2ji4nCZSN_&o4XgJrR&k)I&7G32{GZ}g07;X(RX<5MD?QW z0(nP9?R{z(CQbE|6)q;ny#Sdb-l8;?J)YTMXqxs(^pT?vR|BsHHoKwK>C|QTQ5njT zWo~G8LFCY`w-caVkZnE@bH-dVF$Pm+ayz^nf<_*)sbYK!J*hqi4wh=zi-i)$z15!6 zX@pl2RfD~xljq6$l(nCo^3zDb`Rm1Q2^W>lF65$&7ZZ*pqv9Y3cgYkJ6UR4}iDvKC zGFQ~w_7Oy6TJ}EM$6M3&15%mW$}0a{Z`H85v*=1Ga^u1s+_A*-oBY zR|kiNs(7=2oOeMhA+NohLIFIJ!L=&ls$aN!%1eoAt=g=!B7t9HaOg^z#;-P9>B*m` zPT&%&{Mnh{&E{$i&c1O3ibp#nLY}=;TWsclBWGl%Lxv{uI0u)VSCAJA)R^f3L(8i} z<1JQKs`yQJpY3o1-4Lyer(eQ%#)q1Nyxfza_PENQItZvPYhkyT7K@ytWgTqfnqT%- zLKQ6$Sy@qCHUd}AR4xKEn#0gf;L2JJg#>GLmJM^?&Z%-pf#E7yc1ele;b;j}=u>3G zs%${2!|0=&6a=Y`v}~AkSX3Rtk)GTlwU9$-pvq>|f?`&#<7PCcZkX#XXCU;eIwsdK zR`jN{Np&IBWlBALxBrxIEbm;HptK`fFV-x~E9#L>592)5>EX2dE)Pum+B9itWy;Nn^LS?-Qy*i~ zF1{xujly};oGVWebA{n?pJa|u|`keFw zsXp8-Tj8-(pB(ybm+S%?%r4+%*>%OJUslbN)$gl10I)N1Bnp>_d3_aG#gmI=Sb5H_ zC2L}N5$GlYPAk;Yt$9WW>{(euRq}wW zRnig)YKQ#9tj)`NXx2y3^pig~8$!yB1-n@Uo}Tq1F2v#h`zi8WS|91PecFoA=2v+W zU?45(y%%Z?ubjg1PwL^34^o(=UW`$ilEMHZcLxe!-cjj>^Cn%xuoU5L5!zXy2}Akm zvb9$=mC#GJm|6V@1a`+Io_Gk%8yz8fXF!z`718^(K>1NeN=7g_FgJ(raPsD;C1H*s z_o+P6K)(G_SgNraM20Th{6xnMKABYtcj{Q9IfL_IkyGdy7uO!UnnQ?Pfa*n1q}c zoIRYpxn^Y$jTl0s4r5*gc7zuqAR*GdfF@MLS412r*JS)<6w~oZ7G-hNnP-=*6`B=T zQQ^TAFFxDCvJu_No5!|^!sYBjS}31GJ4v~Sw!HRStewkHc?}<;oFrj+X9=>SyN_<8 zvQWlY*CQeOJbo?OkE;=0$3d0IYjG~0u&}*hnVXICKD3)-2j!>hAOp$KiMba?g47$+ zcKK|Cw_`|ow9zFgg<{eEwYe#3inao$M9u{`EjkM!O;+7FwAG|f#)}wFGvf=56hmg1 z8V=@a~hplTY{85f=g%7x%?ckPmhU1UD>mG6U6``z=xpN8zVK;&5uGj zTP<8fAoTOOPL2TQqf0MYT0BYFdI(lqw8tKI>yFcLZU)-&Q;1N7Vce&&J%qAezIL*W z?u4mm8OCw9{D8v-g-(6#v{gyWF|S$HVFIa6Lkk=pt&cZq-f%!Tz88a2xc~}xDH==h zHdzrxrwD=h9Q@dn0LMOk^8>Nk8FXxo}P9-pi0LL;G`Vfd_sjPM$O za-pA#gs`n<6DNl#*R-YZe^wtVayBI0a5!hjd*NN9^V+h=12?O2c*%EUL;0UEe`YZb z?-8M-!3fP2jdrU!3frqdM03I3k`Ya|SZ%`XMKK29aK&RL><2%X72=846B2SRD zZ77%QI-2}hIICQ0g`pN<%|xa}6Yc02YAwVccOf7EmTECugj^K@tH~@>kFXi+w>+u`&o;+8S(2V_bKVMBYQYc2s^WrGrUGBcbxDps}u)2hexRH_(uwe&VPYeSUuc zhV%Jlc+a+oQJ%LZY0)s@A8o+qAvBh#j#?%gWzZFhnH#zkvsJ23L)z;8P9T!J>=SBR zbXA>w1YqeXFSk35(z(g*Vps_&rq$2zU{ZXaAryn|ITZTNz~Is%E!|{*ONRggTV_S* zqUKWS>pUBVu3aXg~Ji(+fXk5Ths#Nz0N4%AbH8HfDn zNc3QCUgM19(}GYed6U3BSVs)>Mt4s|ddw(np-V8Wsl2^)gYZ@!5&>~IYtI}C6L*ty zybnK4)i;-}+-Siu(Ahs?ZC0v5>`X{gvQG}yjvyPl%IzS?$ewJUVTWoIQpw6jk^1g7 zd`W|BO=n9whbf?Lt#KXzrOJm@rM{bw`F6eT0YXp}KOoao4Ek$Vus7eaZ zjHVC4+f@uRqf72chIcRXI?KT`noeEja8%jAaHAvFF|Yw8(=VNJ-y8+*0#-hdkzR5VOfV5ZRH-Pnd_Q)TBmAr{P{3@Dbj)rqQ^#LASRW z<1l*knUV)s>ZoDPF4kIDE7mSPoV-gzA|iX;COKy)W(JOT%5p0Ev&?59CZr~_WKAC` zk3h{GT;8a8gN}zCAY)Wl-V{;3GgF439eh8QNa| z5KNxNycSO!h3tl#6FN$3u+U7UX*AuHHf+=tez|2-)#Fkb-tKJr_P>hibRo-HUG`l0 zidkNPw1X&(*(F+DDYsJf@Mr^Fk!}m{xl^m`EssCjJ_9f0TpIoBCh4)t}Ilw9~ zmLr{;IH@`AxJl~}jA@qb6hr#?qOZyo-Q!`XQwIc9$CB>AC$IhRXp{>@T?KLtRvAdb zv(F7RP`+iQYm}kN(YOP%1;j+i5{!S%7|uYedB3?$;5-O0$Q6$v7{sz3>)XGWwJ@tpv$675|xTY_XOPZ`a40e?SjaUV&g)2 z@$qv9J7zeb2z<}B17AvYEEMT2m?D00t1-kLPwdi#JgJJ~b!3cN7 zkSDB`mfrYkp-n+uK@wA6R7HxQAgISQ@m`@*mrj$!`#Nry1b{BGdVOC*RTuOYE*cAU z1zS{kYA(1W4s|v&dFv?73BCrxY0YxFW}~WFgH?|$;qP=(9fjBIcB<{pFt-261ZfTy z8?uf_?zBR*612$B9=j5}3?7E0kav?1JMSt@iPxyqhOkNsWz|~Tz8nfaAFzj^(;9Mp zpoI>Q^rvnC&?=^BPht+GcB*92;#ymPC7=bafQ93e#hRa|5DX0?oGlGnrdkvwI0GRqWt&#j>EL2z$AaGXvh9!YZWyRiyRg~l6WBWA{R^EC;2 zzA?pc<1HYk2PGxK)8p{+GgOZ63^$gUUg|QY_g1BsxnQB_@s+NEUSe&V=bUlKR?I83 z;zA}I{Z2&>3%R>=%fzbhOZ&Mg%2_@PvZBz^gMu%Wxb`{f%Cbzx11&lx_w!aRa$DB( zX-Hy4%^kfp+}jXmFllfBYN>ez5ZYig&mnVh_S7TUJO?fV{e~h6XVXA00R-%bpdK(kd8QFA6FoPhUq!XzheWEA-1pQd)V&!0J!CN zK!=`*Fo}%fKn~Xx$kn9>gxet(+9^ctG{@ktXxz+4NP^hO+pjVsHecYzTjqM4q#kdP z8zPhViXvC(15`Oy(3A?;A^#4Z>LX_ftm$YbV`ZcE$yV|Brr#9Dv2`C)MaZSN!HCA+~K7}GnOqZzhT4hWn{}5A4 zi`SW;4Y;r{dDbki2;M{^cP`Mbie-b(6@KL~Z$GkTeLHcLi?F4*Mm7o!#ItHsgwFCo z!RY9~+VV8e+&>1nx^No|;?Of-IKK+w7!!!INWNiEz|0t^9y?>&(FmOJ! zu&_Ym_=_#aI(nfli;RmgQMzUI%93NlamV@kc`-#?x`C=$Ts!57mZZjDPvJ9h!VL}D zsXLs@wj#lWy2lPfsGtfQPMQu;{4x^EwMy|R`ueN0;Jd>WC>@!EV7R1H0C)7RbP-dq zb5%l8@6UOOdfOg~RzhcEp!fNEi)KZS()xt15xSy7pLZ75IoP_6tYOykmtLN&moe$q zzMnF!B2nuFp;0^T+Vn~qx&nvmAIMM(b2f%liNL_ms;gMY#%bQG2=yz2^#=F_joK;j z8L8Y6V09={%~N#n=m1`aanSzxql0wv439fl=F)9CYP}_Q^jxCgH9Z(j!QI=qcM+;& zJDb6-mZx^xhdik9u$;>Q)>7K*vUR}dKwVo{3spoo`(i{%0FSQ(h2s{Xe12k_KRfyM za9tzYyGE2TD=tuzMfDmFF))s&oje(^S8N+mOip6GK4p@Th(I(~zvT!Fa!GOPibeoO z!PhXS{pNnG-G#gXyB;oVxO;oe!mu|hHMHkoUp=uA?&(jCaV%4Es6VCIh!RG9#)>DW zR!e5xXX9Gk@37z3Kr*}{JBwKGa z37~XRoYxm_COWsyhyqgbYa4eE8=V>~W36(z20;|jHjARLm(Ou`xZ=VgJ10-zoU=J3 z9i;1wvFmiI{7Dd2FO>Z3T{Q`DdjK<*etb+>$Sx&d0U}Sa*(EA(wRsV+o6uU}+jdZ# zMosPJI+?K%Zk~Xd=u}_P zo^aNK(ec6<4acWgr{5~t5LaqNq>j_fh1WF_@3#n8A>VFhfFi z$j4>JcHE`%Nl2Mkm3<}pNm}?OW{VYY|5p%E1EXe1j@!~+GMMR3w@eeVG}Az|$Kft> zWl5^zolezV&Z^Q9Le`mIPQu7K3t>p`AvmNH0^>ryOr;7iqVy;^P zLn+S;HzS_%MzWIMe6hfE8;@+StFz&c)iBS&?vkHv|C!vGkQgj?5vuNfmyKDZU$@nH zrMDVbrFWU}6ArVyNsaYB>cFnHD8bI)!O`h;N>dsJVZd;vNg37A-XzG)4Zr$8+G7vg zb6OH2IPLbz!%=l+Ym(GxI>`+~$&8gwSggvRNPGjWQRBTdH=@Egg_SWpCv8*!5|zxF z&qRSq5%P+|(zSMdm``7a4v4_f;&?5>L%p;n#U||{BTtG2-Qe`qM4nC@uFl>chP*DR{n21GOke$b=vF5h2>ZzdRb|fNwYf5waZmvQee-gW{z~36i0a8XbH0fE91SiU4v zNSEJgDd$H|wsgbdrREUJLuewjl|@q%OIhvBmm`0Uk zjvPM>aV_IO9QL4vg7X`Imhz-ID>%*b&O1V05xwm?6WiOLG6?snn*uv~uTn>n-K3Cp z33gB9-6*R6mBgfLKMMwz%U25FZM=kRW8zk;UW zynNA}J9Weo-m;HEUn(!6-&0FCaj)x!!)V?u8N{cjl!8)Z+~o1`=2BVogC~z&?Dw*6 zWbf{tce|q&G9ky&5^%VM$|z+mRAE&MEn$qz(|buJ*t}hq;7b~$Ou3{%N@TFHNz|k* zNs@(IQjQC>k{nlT0XeEzy$aQ}?q>F55F-mZ>ws1dBP7EbD#2{;&AreyXp zsIt+EC{+4I!2zeIw*nHns2Y}8R+!yFB;NUtE-4}h=Xu`eYnogaSW1GvQFs&KS8%-g zSLo&fat>UK-08EP)ZVbsC%{TJ6hvRxB4ll?@i1OPvy=T!+wk zxgl0wz`St<=br3fT7qY+a?jXZY&V=&1JJXUPAqpbfN|dzx}%DjZ1&dR zxRqv)rgc7~G+mR8NQz|kpldzgZFuveT(YY#5L7i8GlNUu*y?s;`LG!h2}(CxU>dZnVNC1^?3{%$03!$uHb%Zg6^Phw zEr;6=^F)`H43jg?xvIjv`8}Q4E$ikBfpaHKbKtVy&GlmavXkbHW5aN@I`u_1fv{T| zYT*e0b^+p6igLVf{GNH}rrSN}1X(}TuJd7F)~TcQsvFI-P`&uQI}?WzMd%4G9b>of}C7EKUCZc20NgDc?mzcztav{uyJ z!B0LZr@S4_?AQ8;&ww5~R75y3ZnRvyJ21{2Jf&_8v{o@OORzZn!2@t-yqeeQZ;X4x z(YzLUbs~7~b$V+x z@?&k5y-6D;R<5wHrRR200?4lCb}xblx);D@-3us;v%zO9b&I97BhC}7h6iU~Z1h;i zJFkU$hQ5KdMh9+b>MKeAnYkP{8cgBrH}@=Py}cAK-nr6R;#WD|3mc-hUNt*d!XgQ7-bF(xZlRvuP6Q!jC6 zlx5DI!yLoj9Ew8Qe3u9Odo!)N(k<^EwamIjA?LMlW5@QyYTzrImTQ(AT*e4GcZYV6 z@)J81!@#lh38xh#(GM-=($ue>aT=vqKK!#%vP|{t428&EHqY*arPvTcT6cxTB5?tc zmOR&1ENU0RP-aTVyO0J@E3g}XEP4((#;_WwdK_*Vxec+cny6AV4hP5KW{;(_y!3Fnnr#hl%vh=BatqyvUgG3lNUncqp1h$kyRP-T9Bi$^bC{ng z+BTN{A_}hD5JzIw^=&k8lOe`Hm7B3@yp4B9bq8lK=9vJPo&A&cSNhbEN-#ty*B)B! z)r3k|mXG}$BO`<`j@?~OFQG)QH6FWA@+lrEe*aN=Q*<`ehaVVo2b|e6nAD0Of*Kbe zPtY}aqdm%)koeOh@|+;ZdND!-pa>j+aCj>(yo({~`JXoB><-DrWYhU=@GIz0t1 zO^!MU^P|Hm+q_mYPy}+ZNcskG^fI0uoin^V9Yi z#86ZS21D9U^95{~4eBBoyKN1@kP(!kf|r(qHa_QDB8I?9-aA=)46$> zVQxon{!vme5DjlNY9uYdvFBTJ6dsO7|FM;?duXd8qEfWp1^tEPY)z5O!69jPu-Tp; z|5#kqiwgE2reo72Gck^}-F$2@HRMfMWE38}b9YV_qyCi*>}d1IjjUTjA|WiSpE`kR zBOQVD1KhQ+l@?ABGM=nR4oSF3AbAD(1~nSpGHf1t83v+jfqJNnS?aa#Suwi?7Ha>k3Clzv#Dhs93G0KXT?Yt0uBPZj4 z0ZSUo+Jli|5ZVMa`BQ2p4k)y}vB9Cz9249C#vk;pX4Kcq%JU*^a73|O2DJB49rS+(J;d^3i&&+YRJzQlI`m)RC0idb}tK}_38S0 zF`eT`p-X~&y}T}*jHH1FWV_MOI&RTee)%ON1)7HNPDfv_*<$WO)|reBO>5E$0!@kF z+4X4opvP$LGS*bVekZ4OXitC#2qRU6K?5nk=#7WmGbqZ-q;7j*P%mw|dSjYGtQc(# z&4c;9$D594j7u0=P&xJ|IxRT^`Egl=En8A^x-y=%>k?!kw`zP;yS~cmvyvSa;$@N} z?T!;F@M(<(%J&1TV-CnbVb%4yE<`Za z!Vo@`KZu~>0(@+(Qvjn!DxErKX@pE^f?#4S$5}PS$U;Koi!rtSnzuq)$Dov3=`DnS+B2BHPi~RO>({o%5O#i&KvN<`@a0qy=qLlmv9?tRXu<4Gwje|72g16r0LGJMWWEM#g#e0< zIg5==Zlqg|o=oxv6)2uW(lCj^tdJU+5Dbh3yJ)B$0Si?kZAH4{S|K5fL?g7!=-{B- zUf8QzlxWWflqsRAUAuUEE(#w2bQ_e1Uy~&ItJECXj3j&rKrUFOHCL04C9`0n6N<-M zvf>Ohl0Me3mV>cXy@OQWgjB*y~<+Lg@fbDi?TyAZ*7615WoS_omwL%_AJX;Htg52qyU5neBDre{UfbGO zYMIkIa~m~`oiJ)_4uz1kRZ@bSH`vq>{rxD1A4V^09G)B~+x_1Q== z0PGUC*fN%nj-v^9Q6R!WxQlk+wDByp8yY`&AKH4ek0eMG1ftUii(wmQoYkdusMS7@ zx`z)Ns*klga1GI7U9yOUdeB;3tvcQ0b)fmUDEl5W9eI_I292YNn7K+4l)@6qH* zg!k@&nB*`!b;~Z$T{8i^yzm#yB4Ooyu|n>;SWKuW*vJ{P)F=&=C$s+DI95E&rA(`Q zl3vA@hE1iFs4RLM;&RS2jwzN(8vanl;&7x4ZQt0law_8jnP1rpA@!X-8+R(6W>nX7 z?Cz&UHp9Y~G$!11prg^ig<>lmqQO$Y(k65YbU1@YMuQEy+Wqh^XTf{1r1TcWMuU-k z^V7}~>Co1+q1nmHIs?OaoNb%mPo#qZ<=d%ATveh;O_W)5X#7-)>+ z(aaL2i{QwiG{s|wah%WFe4#{_G={Le#L<0zA;8Hl(CAsY;}i==c7xtZOp5^P-`uN= z6{Ej4*eqNJwD1G1?)3pY9Xk|DfrpP;BSW~qFcE6>^v*3Rq$@Hk*nw7QZIM!k3MpgS zrM3oITw7%^yHq7l#>>$El$c=q+)ul=?#(|-p!z1M985wx?z+Z2N?eTSM!kd>TEnKg z&eq`_3R6A$G9PWoP_yQ%exz#zNXYuhIcd|8p$(~@tlt--cHs(!UUG0MA%($5X1*D} zcaf0Y(V}w{cSL7Z5c|n1!r6Hy)E_XbQ*N{^$8;h>*}Gu@hhiq zy)O!t*7$TlJCWGX6(2@Ysk~cfIUIvR`4thSYGFjG%X>m|XklX2I_7V0@lP|lQ~hd+ zmoaTSLUQ>!HZr)nzcoSpF%DaTz)sH?OO%WcY?P&1m=%9a6SJU1IHV8pf)BMgdNt9v>C!If-D&d&5+ z#1$$hk6pu=abZ9v2Gq~-sJEr?z>g?pPOjUni=5bA{W7hefID^-yO- zmlzm?RjxIGR_*@Ti_O3qZUI_NPd|7B0oQ1E#X$uw1zo#ZZa3pVqrv!C(t?2@LsDs$XAddZWR)VO zT)6!NC}pKebG>z4kEA4rFKi2Xi^eK#eU86|>J0ej#Y}DAzo{;2 z4s+wDD_Bcg@Q%&vC^~MnB&aUkVHmf!-KsXYFqhaD^$N(rrBE?#W6$c~&`_05jK^D5 zEIaC}sy$Zv1>{T{*{L-e)s+FgwOXqpA&azDgVfG z)|5zcEf1RE>iX0uEz1mp-^_u6k!<;F|#( zQ)q*2=LU*JGK7%>d!JS=3}fi>bvP*v;!wgMoMKnU?0qCFv8H`f)P+}#%WI<`x?;vio#jR9oWk1N;I|FN?Twn(Gl&c=Wt#zZyVza z<@@iqZ*{0jx@vV`_M&-5S9_-oOcNGv@1c>AefHXKO7P#l3VPtahpJyEtvz&hBIX~l z@S%+(A=VV~-`+#yr)0-|6%;e@<6Iq3e~%O!Oy9dPgxhO7PSvPQ@Z(N=zgK_+e~%RV zUZ>~|Qed$Gek4dw(-HhgG3B}#e>W3rQBbV!6>1l;@dj%Eeg|WPSWj#tu9?_Ko*uTA zjZfKi;cipr^vye3;hjJ0B1D;?Pl%L%Q6@6}<)tK!tYL&fp0AmRqL*H>V}V>yCS#$C zexwkb?O_e!Ldvuh%>hJ%NFt{>&_D>jBmJlT5qBZpphEN~7RTZLYPE0fg82&vs=YJ% zdlvRB7?{7XYULYvZWIL~(Y4{MZLxS+EobV12Usm97W;m<=jrcrCMl~zgj&v!MCJcm zEoXlXq?Ypq9Z-L%<@~R;h?i5x+aM#Qa(%^;JJgn3BF;Z=>5UH|0a@W;$#v;-2Wnwa zpQS!$|2t9~sr%dk8cuY-)&cdG=>AtE{Boqb9Wv3ialTRsAE<}7DJQKh1a;h@vp-Rv z{)=t7w5h;igp5_>{)<0m`r%2(&;t(9L##>dtBbz-hpmxXgdv8PK2JDQNQAMQ79XSf35ZwR&kxd#)sp4b6BpmGCsIrTDNeI|Pk8m_XJ+!!2GV}; zmS5k$JD}NPi2KwKpH?>&aHQ)Ey>!%>SwNSgpKZe}(^%L~Mlh$rNL0UcM zb(_ET25P-)b9(fgf!uF%AEFYj!xE*pBH0rKpCEcA@~d>x5{5>;xBcsq zn`q=)SC){{<3oV|Hmy)=9l7yK4^wM>jVzmb_Yp_0v}M&;K7muser)hy4d@r`KWMFi zvFN$&2cM+cT+dIRt)%|z5(mbj&+mQIUw1`{>pLmR9XM6uLQ%bZOj@f421cbd{&Cg| zq?~SGr#lNfY&eA^qc1NFKRkJwfjRW}@BQN6J0r~v?KEi@R@beciXWal)xd1}*fv`| zOta}mcB?B%D5X73b&laD?1d*UX+XYLf8>rwNWPoc z>B7eMRg`aFhBxK{Qr%RJ5Bho>>tn%z;uXvIz(A@;ob=W^Db>wnS#_$gyrBUC z1`nP<|AX=2z~bzZe0!_=c0j7H^-_h1>BY(aK25!Nw`Q3BckceTU6AJHUYgXH)Y;B| zoo+~PnY+f8x02qnMIc=%W9Tl_std*TvNL&G4oLdBBX<59NqS2!bsQXol6>WDDlk%< z@b3K{qLE@NFU?>fUzHSxPk;I*N^xtq?}R7VS0%;YAJ)2!Qrsp_vGZ#7)PPhk=w1D5 zN^#p4q=!=CGD4wazVa3nNcHxApYS-1yxUu;!rFk`i{R~87^8UI|J50Je^p=oQyLF; zuu|qC^VRC52L$HRjo)zU6SV!etCcdjh%Hp-D{e!f z)IPY&7w@OkcC%7@b>@>$YHyjn$IX=5?q+oVUz|&BzHjf_X=LAn%hsc~@h5ui z&A+AbXYXRV{q>P$|9zT)etFe4Yd<;#Y3}2tN%uXyGWp@j%NSU3pYWZ^FKNZSZ!tYs zsr}a}2K159qu=}hwc7o-kC;~bDy>xl`p6@HeEiR}-#As4QC;EoA=- zzVI{BLiU&CQ>nd5r3<9@;IrpFOz9mUONUcZRwzRa<`s{q2L@W`IbWN1>lCDSpe&zG zFId1=EYCoD+3F|H-B0c1puBu!ds}kU@XAa0e|S6Z_^6BL|0|-1h>8Ue1P)QbhJpwv zqJ$KPBoIg_f_Pk#BRNR!!WEJrDu`eK5d{@N0Tr=#Y*-+agc^D;2^~TYgiyn8XWy5* zcX_>&?|yFc`-g|;A9=oKcXoDmW_A`>xh9XczCc#4ou8P&s!aJcCOXMe;g(J;aodWl zT+TgjA0R8&UQcCRW_Vd9YwxEA>wZ_RufPI4{mCCk$pSo~=kg0wI)eTSDkp;Ro%7VG zl{CIj>WOB-FY#FgdbF$BqRpg79W+#>$Ch3!@fk(2=}*46?;MTnQ(B^j^%-&`8<4t@ zu6$&HrJFtIg_UIKI%6bRG6Jl?kSzuh#CPittbOt4DVNv+h$OK6=uqo-E!I+?nq z#TuH;PI_*Yt&=MkJ99e}rYFihbt|zVgDkc*B*+CK&qIeyJ54#FC}qxQ z4B>={u6ndjO*#^@iq>Bjg++IU6(5JKza-Z3gYLuVIQrj}+cvOI4Nu72PwTI%GM+)M z98V6M%jZ4#Z!Vq7qxF(vA%(RD)gHWLS+kgSsb>`_sR<^#$L?;;FNWxcRhWp@-`ovf z?IS(y79>hp6UcY8y|8?xg|T8ZtVnwMK(8bE-<9eNigJuu{mMm(a>QzQ)bfD61wyUB zS@zv{Xw?mKrDP=O@UgF#lBC^(_CQI zZ|tq&`QIv|dsq^*rY-RQ8#$Y7e2`K5sHskNy@{%C*qvz}LN^L5tg(oF^w7#x#O{mP z%H&F5KMs6@PAAuHp{#U26`Q~Mz^N!ztFsxE+9Ooo(0Ang4HP?jPDNE3btsMMbqu{? zSIR`zzVDHl2gus@S24*suh=B%zNAtz+O-d@_=U%`2B^s8Z-wYZ`yXr)nAP|`9hTFq zJ`ad;Mi}YR;)h=^CF(#8wIYKrJi5SMEO}?sS+W-|sHiMAVz;IDVizMov5fGVMHLR- zRm+wT)oP&f)+aJnE0FcqF1PY(jzL-~Yi%fPM+Kg)z2fxm60H6 zTcE-B#VuO-C}Sq7nEhg7=>9Le$|!fuqMF;=9(y3@S;<<<+{!@tZf*1@5 zh-(wz^=`8GGI`2F{c_U1Ie$ZG-Smm?y2xhiExVc9j5IZmZf=*qILjiRbahV;*Zr^OdXO%fU1^ytvMRxU-AooX^|y&=nqe5v1iy;(#N5SN<9B4_-% zyKW%GXBBxhFXT_(LAD`7N90WvAXfJomCq-L)xZ5>wc`{=8-}PRr5~bPRBf`pmiHv> z^==*41Sz~u(fMa(+@aNU{_*IT+U79^brNQylSt9`EickZ#Ot?7JcY*_6XP(R@EC)$ zX5?Sv_K^)c2(^q#Q$pRJmt*~A z&)Ujw1-xkBm3LCsV|BkPwSFi*I<|AvX$zBbq=w2`ScBt+Fb|AsiF@ub8r7Gyye!s! zP50;jhfxLA{nw`+yhMBE%Nib9j5>VfqY0$T^juv=(!HYAQmmrFHWlRzzO6a8gmMO> z{L;ByFbN~h0-k$=$)9hg$o6QzY>ZW9Ywx?C>TaQNe3i?kB($PE^p30qEdl3)%+Bpf z$xk1n;xQfJ&&w)FH3?jhOWQSLei2!<*R))gcSVL&SVIAE(fDY7%XF-k$exu+l(FIg zvEk{zHeEz$o#jsYpWI?%qqc+VJqgW(OnI;cHA ztNto~hEFHd|6tibRb*4onLP5R-c%FaM3uBI!N?{Y`D_!7>{}WpyFrVQ;v)+pagNWg zUrh18@fs$7BTN<*M26VyC__^lKsFZ*yRku+CXaW2@mhShDm zkWQ78^juT56&+txopDVE%V|1QzN@EJptz!38G4$Pm3++Kw7v!jhq^%nw<+c9GW1(9tME z!XAcd9Xb`A=U&=7av7cHW(KL~s46TWn!8zLwU5)>%~G>KQN8Hgp6%?tG>pR-KhudF?a&M%L#Bo-8Ya=J3{KTX)eM&Q?*; zwQ$`CS307IRnvD*KC!+Ccox|G7Gyg~z%)%3+~(ewX(+ zkJ5g_N0RMwtR6AYSxG;7`rnn0Ch+c8Zugxh@BUjgvHXD-dLTeg(f_WT3eLe#KmIJQ z)A%kx zQ_aEekVz*BdhSgpo&I;FdW-z1W8?F8(`oIGAlb!&#V+>3sU9ajvmk?V>HN~%`x);~ z6_358#aZ| z(lSJJT-$=3>!-q)1+5tOzW|rL3!h?7j?ZQxsVsWH!ONPzn|y%qdODTz5B{Q8W(XT<7z^gr53MsRatNR)` z-VmxJv+j-BLmo+mXuaK?xXb+yz<*!q;2d~tBMFl2W zbiJV({u3w9AEg@pRp#6Z)lv8xfFDqFd%d+C*f-=?{0)^&gYb@pBG;XMZ?cgh*Q?dv zqO{k*X$N6yTd+?~K3sdj!;HGdgc|QlRGzig4fp?W-UajrTa+Fo3%FK8ry@4~JXPpS zkUeXh*Krw*^E$``uC?$uBhPD3*T!Y!d97Dd=?yf4nb!$j`MCk?^KEv%vDw0C8`Lxt zgDQj9ZAJ%vTYICLXA(dc=7Blv`ez!yd$TFX(|GQNK^31vU@dwMICz%gU7Jm)GIR&| z(KmeD`8fH}TTHkn6Duq;ihqR_&N)KyudM+#olj&)iaetH!V_o69&FRmGSVDLGAd2m z0uOz{>bff^aIgF(7;ScWp{2RM5WLzjcsjwWmHwTJ4mH0N_kI9gQ&XEu6%TX7d>Xy z4JRmGv`5P{DZCXWqMqIfkE~rrJ-zp8iONzTZ;@3o(4!!s`|BtJc-1|&trkY#7o{(IADw-UH1_PhQFX+bz$43F8^N>3hc!$*u);G5EZ>L)PaP&1 zi?lq}?v<+Y4eBZ`BVg`c=_n=U5d#ypcoSy=8}woCv~9FTj%t`)(p-tQG~-kXk1U8_ zJWWv45mn!2(0Zrm*;P= zFd0iU!~}W26_I~8GM%9==cds3hHRy{mxlTes!r*C6tw94P$qEQAI$aIu7%Qq#NaitpuE`~ts{tx&jpXx4lNm+pirTqX zP_*tENcXS|I5p`D-BzK9Qo*t3cr2@WfE~eJ2C*yU4AS%)RpF$JpR|FhaIOuIBYQ+i zk`!rH&09V@Nt$(CkmqEbrPP(Pym|2(sXBEU6N)Zq@d%Sk;DWl0PCnO66_ zwb#P9*9W;-N%jm{#0EtYmJ{qh)qQ*TOMEp0JW9PsX0di=vTPNob{c(R@@2A-H)yEc zUEU}cd)FQhuc)+W)p#B|bA{rtwSwF<7pnkrrtn7L7%Xldfp773qd__3Tih5V`@BB4 zU78L9=I@EU_Y<>rfZ4_Fw7YGr42Vr|=yhHw`}Nnf%?Bv^byI*T&5r>d#f6W*=5e!| zgS7Mvw$4SA3NKNtfnzJ3^A)rXu=N#PW7>V2|3B9A8QH==LDj6Bfae2T5$LWMhGa zwFzXTp0=%CN_*)Y0WJ$`W1R5#BH#4V6Ay5^TUX6XblKd={6Ypy#S6;`*6HTbA&bcK zuNUCT>~M?jx%@F}%_$<*4-l1_Xgs*0GNQ;p{k;cx9PQ2^(Ur`e!;!nIp=`z9dy7ge zOx6YgGDTq}NeZ;~mj^yNMZ0LjAkXEkY`KW*FYu1$mih81R(Y42%qnNncx!4i6Cpag zXkV-I-faFdR-*v9N4%U7M!U?bb-MGNyK#^<-0mKn=E{`AL|ta@Jp0^6>N49Tz>Kpy z$<6*h94v%K7xMFvV(mr)hi zThG6`l_G0*2gtqb>^6hBxKgNJq)z8AN;Oka2ix6tr~e@W!y!YQ3g&j~tj`K*Ztqc1 zCD*`%=+D;p2X|BS=iVUI#~yGuGmYYsg2?5Ter5cY@qK!xnS&@yMOs|+L3AEzaq|E* z-s{Q;^JF`d*oA85@uTCTFd12@F|*UQl0V!+LpFzsu35CVSMPWK2HM;24>Ea8m>k=P zq^jebg0Ugn7Qe{Xv!~XF;lJY?nll2&64|O!n{- z7E*K;k&hqS@)zDM_t5~aw=?&s&tyqYCwo}PEJY@& zTgYc|PM*Gxe3r-cY%@bBKFUbqtG?jxZnh0lSqjn&DJ&B#(|PAmD=b$MrUE}O{FAT)v{IhZQmL(VxQ*qd zB>34Nes*8;w!BY6M?jVKA}_H0%|^AkNEY~MBbS$AB(gWpBuEU#`R}kDCup3X(UIBx z(<&V2y89eO6sPP2s8u=6HQUx%P2(J9aBZJf) z8Fr_fDq~;?i=O^`0iD95j69Po2*Q>S*raruo4;ezB}iq9vs}=wE|(0;0QR_~$ln*x z9@jO<<h$=p#fcel@(ff%Rtz(*8kP>Htl{ql_#zW*eJj?f0wldj)UsC@l;2E9r zxz12dsGA}oWlz~lNYY-)0b_b{|C4)ZOk-3;GCftvoI5!7JTYT+Olk87&WVjybz5O! z%0J_R#WTZ8F}o0=)ExqP7w*PRDZ?3mb&j1-j2R!P0sa z-Dm5mtP8HGl6ohDbus0=&vR&9^irgwYZTVbRLK|TGCoQsEFU_1_O;eqL^>L;Co=2m zlfeYuR^q$;d8h9L6*J1_@g}%l6R>fB#kpXFfANjmPa~XY;L5t+h2qBlcxoJ^({z%7+AUL}yukY1 zao-`{)gakGj^#BY289wIW#ED3$4}vvyLK%V89-qks*iplXLl~uM-MjeXvgm9v!%iC zn>ZWT!&lB-yOFF#ik8hxVpUsKhk+_jlhGPlQ<89! z*3b|GSLzx9GrT0R6JeG14i;S3unoTK}7$ zLnkD+mdJdggj9z|xuD3!HV7;{qrg)++Wgf6+`OQcWZAd}t!w0FU0iuI4JrWB`g<4fHz0%JkajsD|tfBTv10LT0xE`O4AnG+Wo2vEQtiEy-r0 z8^v=UjO0BR-ZFlHtQ`tuh$cS#6z`Ef-i#V!A8t23h9hn{fL=d1?C??Qf;vFTYl15Q-$6Vm)3q+>6-=N+Ud&^u~* z*@?(yGjlja*EPDOwDZ-j`=}ORl0s#-M6*l|x}r4Xa}^$(wv~LYcl|Vv*T$}qC`|#a z^7;{i{WqpTC*Du#Jw%og&qQFqwH;2CTTm zz(x-b|2mH%)*qU3<@f;Tta$Qh+8LVXsiy4c2nkYy9r^07dhx#SA7OG=)XG$JmeE=9 z`&aiBlRrBRb7R?)!3Kdvxk$TaolE7tJ*H!>6#IeX-O+dTE|T|SGp>xccfbkt$MJg? z&4PLmXSMH&8rm9HM4NcPMit-FjjjS#C|mztZ1 zH7m%9XD8m!brzfuzAw9qSD}2$_nsKmo`vq%;h>=K8Ug;=8zT?pQnc)|0MGvxJVpvz zU*x?k{rTT*0PoV*woSzQ zBFLkNft**hfyQ+0^y^LwQoP%^v%B zk%h_kt%fH@Q;^-<*!IePvYX$jcvbZ#@`8m=9wgTHDi&)H6uM4=OwlUe^b^GTAz(AH zu@u#RbYmf{=aYF9f&5WnxtuBdT|4tlLEMG`o6>D>r}Y*_{Ye>3Ml~<~7M2p&ln3nf zE|5+6S7w&494{tOVmm_vHxjCl|u$+jD+#Yi|LHETfQck{^0&I@?Ha=dJG%FNu7vWrfcOF1jnmVr^?9s}6BQ{Mb!qlK}S@o{9V z47MgsLS>>>9YrHs{rbrnS|7_bJby^S`C=3d<~Bcs}UzIAIO#r z*x#LZNm;Go#n_!G-qg@%66x%U*v$2$vuiX&e{jTPDrJo9eZ4Oop$O1gEf)nXq>Su! z8JA0FjjYq~cn=y0bJ-TG5l8WkHB=$6-cNLUQrQ!Fy;$lehTXG-3UDEGF(_yz2^B#{mF&0S;Gme6TT78FNs|SMG7ZO27cN{b*f(^ska)* z%B0I;gVXZj(VdRbX?dGD+hoE;B}W;#v(tL)rHtHmg>Fx`(gTt%32((AAFBSX^K!|D z+7X~dx$P2c2&x%eeBjt2I#cZou$Vs+MzT@U`_Szc8qE)$b6ud(+!bKPN*Yac7XNy5 z(}fmBD+thf*)mP0PgGY>POkYwFXmGOwouK36DUjrK5*h~b9R$-yVW$4;S}b9J!k7v z%{J0LyGPAqo1UE2fW~uax8~bvJog5PCOBdFz`pjC<(5md9`*%jbOBP{dO%fD%Tr4E ziz55gOkYNXJ<*p!-F}P?j0{Q7UjB76NqRszsoAMMBHIGfG3?A~fZ|-uQzZP(~oX6fm=;N?w^3GqK`O{qnlPWrwh#PZk z`ZDtJix5|7<6k9Q+xagp5%&nJVhnn4^G6I~rB zUi8AGb%$w>KCQ^cZ-1~mf>tWuHkbkNH38OR#~U)w6Y-29Uw{aggg6mo3-5Nc;XO)A zR5H@t#PT9)zWfd*9)l@xWr*gNc&{bbqC>!`}%&UBE)B?emDsQ35lNQ*D%*pjpu z5a-`;^EM)0R1?G9c6QyqwPARr$Qaf*jN67wY9ie>MVE+$iKs^8!&k2G4y>2eM4o@O z4`yW(@fr3c8L|Rzv)lfkc?D>hn#?=|B&w+HBD-5JJdSs?yQ1gGxdjK}U=P>K=e>Nd zsz#lfm8)3%RyYd*(dE_9bqKA|1xW)>(i*+mgeuJ{ffe&@-zL0^Ry7@4PG1~U`#!H- zGQSso4IoOB6nJ2Z4yABEv$~GUFR>UVR#bnH2e$6^K0C<+yH-t<5t06`Jb=YAWOXJNdX)I9dgK)TB~ zg~A#NHQE`MW=pcOgNi{^;gJQ*ldh}Q5%X5W1WQDeiE?3Q!k<`2S)tnilTE7WAd9lm z-n_Kv?_*S_a64efcv2mMC1_Mzpiw)T6`ZI1;~jt-Yh$%xS+Tw}uOlN(LVX7Esb}wh zg7;ypi}=#W$R2+0`sWJS!+L<+Lqfg?#mCR($MRm6^#L=^l?j@Mi0T%~vtF}yLmA~+ z?*!Cd_H=gDGFbu{Y4w3eZazv{-2jngX*ICRk3>%3m4^)hH{O@%b=zcg^FSwx>femq zN+*iD06D?!vsa~^zUG!N{?2$K!0rc9prX2k?nnK6dL622xl)5bxZ4=IjFUcr9w4oElCbU8i=vw~};CH8i|XM5RMfz|;X> zmr~Z_ZZ)qGvoFd;o_)&Ob&iu~-%P_bGxsuxXa0Dxn27hNiQVlf?0L-LMz=|nh-y_E ze^q*!YE|!56T!wJ%0lCroO4qyNq3)`73cCeq)3O>{n`QVET?tfTuVe-nE1K}XY2*L zEoW)BZlUIxZ(ia|uo~yy`A(UIG4Izf<*Y_Di%TA~uAy0cK+QAVu7vdlNI1T6-JK-i zgKA=etFsMUZx!YNufFdy_{vi#|d|MP{qEq(xk4B!PQ})AZCfW|gbq2(`1}49K zgyyg%kC61BmLa_8x9W0a8>WrUqsaRs8lp*i0C5>XkXetD{FfLuVM@9DTwIZ+Tz|~+Akhc@leQ8 zl!f9X@4FmFNwzjBmcNqP;I@hK&>GCWd~zYJ!N-I1Cu`<__y6+;?Tct{YO6>WZw(vF z`ej79+{O!t!rF;;`dcQ%mC?+#E9XVUQ_sziM-uJ3`<{B~9Ld&R8OeB-3eWI)CB38u zYQuM2*l~eQa8H!W#uCJKx7(ND?UBS@tasau;UD2KMUm#Nj}N{`e)p5*Tq;LTWAD<0 zJgRMheHphdatqafcPJm#a$0#3KF%fUEkLmHJu73^P=5NUa#@pIzC`xOqSs~h$hn<| zw07Ct#)n92JC;*Z>|S;~F68k=TDxhMm*1gyx}24sO_C+0%Xq5_*zOm;TD#Q3WP3)# zb3&l7eFHQ2$GgAf(pYvX=XoTJB{-4Jdh3TRv@*g1EN2G2R2g~|1BzHJ_-FMA(#_5R z8rMkDO+`yX))%CkJGLBHK;s#%qIzA%2ARTh2P|BZ{O?ZF-V~u>`HV1ey9V~A?`~<$ z&rOl#wBa7J>660CJC({a6j2HNf7WXiGf&Xcp zkjEQ&UAN|}D^w(*oLi`f142 ziMX%bHk4J<%2=-;vUcURzGW0ydrrf3Wn@R#yta^6E2@Hg;!N#Qimdfl6U{*hE{e}ADz@+!$DY@49TN6C6n*OVb-RNWCgngijfH2@LD`J(3I$$7 z#Jdev(uwH>4G&#J6{Z3EGh)U8{+@$XO=C~SF~td4OHmH*!hg>lp&Z^I9WUBb?zR|1 zG{SNMpCQ7r>KOS9HUl?-rPqgoz>qK%Sb=ttDci{kBsLyPig4LP)2Zy0qD4PB#v-#f1Gy)EST4$%|cthit1on*isv)#&f@u7M)Qe)xW z6rIjDpD*Pv7^DHBtX&*cw0+fYb_r$4(sf)}ekk&hw(aoo?4wi1glRJI(FOAvw(Z@m zIiJhp_t=sOG$rD+iGlx81$csY4WZXGxD zTo$Ugz2odtOR3)0qo;Lu(Q9gH_H;W-Hc9YaKqTu;=hgctlI2yiqvNez!VLycd`7{@ zF6z^19gVC{!$a3tMRzo`DrMfOv$aer?F?N;z?(O=*%{)EQ1eXM ztO?5p#x(coUR!8PUo`T%vLsWL$k-#Nc@fDtQq7dq`30SJ-y31iqm0i>Y96yW2I0h4 zKCm>GSJl{0mgZ$OEsipF5_n*LeEhi`n`yUtMa^SDp{na^)2V(U?`tzkOUy8u8et6u z@wO#LVwO_8ZM25wNJ8r&p7={+gEP4g3G#t&dvv3hxj)R?n_7Z+*wuY8xo#HVzP_`C4F| zZhbjsAFb2ZbzDiFGw?y$54tXoe2_OZMAXPqbe$qA`)THvybH>k8X5>4h_4bL+am)f zohRAeQq!c|Ohu83#}WtfnA>tY`K#H-OXV=YIc!LGWsiWX;hlD(*^O(~ z$0J$!U!i*~C>OkX!r0T43w{sq!8i(IBocH~FKS;#liEA*h7JrWG$m@$gP-x17S;-hEJf6V% z4-C&;NY?*Du06ecPHUJmTT1s>q%E0wBUW1&ZK|FYX}t6%EFD;zNj0{vCy)0dBQGk= zCZ#?C)Wo)~7rvOHiPQ8{Nler)Hh$inCDbo=dVmQhQCLUOir914UA))B#~K!xLSY*6 z;(vU<*JkqKKhe?fU;<<;aTc{sOMHw>zJO;+g~O z!PrNtZ6xkImOLUVT8fcZr< zXSo5I49_2tRwR{jQeJ=*Cn3gzqJlYpzPydU#2Fyb)4T@d5S}f>ioI*s1!65LUt>6E zEzCl9M`xA3et{xp%N4oO9PA(ymORAgVK1mAvmX*XeXt)C{=I%D`Q0m2RB{eYkm8bp zNLf?wJzFVKwh}UBL{pGe*;)Pmm1I>`si;y`1IQKD-ZH&}q6+zn-tf7UbP5JG?CwO@ zMzUe6Rg(2~X0U4)p|1)Qz3w~z_8jtB)+jtL%LdY3pfrFsAcd_o(9-(dPj4eFU8^Fp z`z&TiDO$S1(!7+kbe)RHT8z1d+f4)&rvgduuXlO{NxD8jbtEZ?d>FJ5XM&wQV$|9~ z^3OIX+RElLz=TRGJflEow`I6bkj`!l$m#P~Bi(M$m{MFuFs`3RD(_)$((tm9>=|B% z%Xml>XM(-HL(@<9(q6w=%LI?r2#+X;+0^~v;uZ2hx2S2+9$8}weC%gt{*!BA#H~7F zmJA8OY3YvQ%X`S~Y*X{PyCl5>hW3mz7dsrJJ!89?*u!X6h35}Qcdp??-UV}qe-$S4 zs!%hRUX+Mp3C|QQ-A}QEoqp-uz9cV;*(xzqX%QlnwOV)Wjy;sM+J(sUoRbv0gEEA( zM^#%-8Nz~!$gK^Vn7BOveoxGsj~}NUr4X@^Du}ho;z5%b*fFfKz3=LB4>8G*G3JySeARJFZ4V& z%Fq?xyKOyr3H$v-5BXnVzGR22-fzH5NPB4cKJpR{sF{6jZf6g?g$i3Ju&0f_vb>O1 z>p{%y&3bsbq`Y$g*ggCGyo&bILuz(E6Jdqd3)*e|PHJ(McALXNB2B5m7>SN7(xC4e zJyk{;R1{z-SMKAztoyq$)FLb&Vy=I?cQG-K7@01gQA}|r@KFX#ZFm*B&NYa7@kfYmI~xJlIw4SoF#g+EtDRi0m?Aa9tXq;^PWVxXB%A^J>@= z0ctNhYjY=;3w-L*;uE>#Q=bfQ=}xvgWq9bT1ZdA&f5slLFxn|S4FoBKwFmI(tZTS} zc*Sa-ENum&dCd<)PS9wcR`cX^D+Oy~arEJRv^LHJXuL(4iOCTjPe9D+-s&U~OAJJ0 zaD<6qm#p(z+(FtU&vIfys@rZ$<}aIBCB?9T1-P7Z-x0C^r3NNTpWDIXSi%+n-PgVK zzNS~`zV0~_YB=?S1?2+5qYUP?#-s0WD|_C+X4XA)O91K_>qf+_qUh`eM3i#F1N9Eb z+0}m^-A1~Is8MbyFCn6ulat4uTS!`b32~z%LLOmM^Y3o?<{ql~zYLjFJeuKV$}Gb- z0UGtvl25LXr6@Dd(ZNhuhe1Rtqgq-Xc|%tKG0UjL;%gS{FKcgKbBWH~S6xHzYO1Xh z?V_G+XHsNVQpmH5e7~|Y)fSSpR~yNhHsc{lSX!8qZ9il^dGOU#OnOB$+Bw)|5K^27 z=5*it*KjGXQ4@RFc|;>7UIG(Xrh)fn6p>}Bu4dYjhWQ+ByEV-Pk^rLGi>!a0S0-&H z>wm2o)#`Pm%jnhse9xiLjaHIXyiUWW8_kr7fFg^}F4)`uu0N9B#Hyj@Cb1o!QHR=% zPojyAGqS|hN7YzQmiT%#GnsGeQkW<@w_(?UU1TL{Vy1PtEloy$1K`QLI(h{6WNuKC z?JS;b^>R;2!U_Vb_K!v*j*xAtrKS#M9pPl@642oVch=*b^lwx%Q|w-vRFeS~*I~e& z+o)^>t)1F>F6+#ZG?ZTd41J7|PVH~s`vmFKO?s{}u~zn6r37CQbx#}6a`X}Eo_4bd z*~uS$Vjo}-TzLHgjc&GfP(JzQx2Wk37U7ns(O^$l^va9tXiuo4rn;3|s#vLoWd*u* z-!EB5NVjfPQ&~`;s&)EG+Ijwx^=*1?73*|M`(LG`)3PZ5IPY=nzMFoEh9hmPCb$B2L_<CptVVpq~iK;TT*K1-VLfh$$MchuQGZ#NTdf$lp@M7XL5Xo-57F|qBrjN zmW383YfCK|0!8%(N&0ZBH%qC~<`E;Wufv;4@2JYykAa_@xqjqYiV{5VRtMPu- z@uhs)joSbs&mx9Clb{y&f!t%es22Be4K+SZN)JphuCtaszlHY3wn1LBhh8>j*2o~B z6iX1b>6si0Ben|?*)%!^J0wJ?kxg&>^y>9w)7uAm^bm_NL@X>F+WFt>XTL-{{}aj% zLN*;bslql6tolyNe^^g(|0lU*{sOEBJC$MaN@Hs{kB1wkQJ4+lOZPN8xU&^wcQ9kK z+)kvEeTu_re7I70q*2!6%RROoB>htrenIGG=e8)4*!iJ#)RV1a1#FX25pmf8|CgWY z?j`=y74Vsodt_A8qd0DAn-l9Pj{8gnd}brv#z2Cw?g71jZrJoAH1nOz=*%IFbXLvG ze?6n&Q5yNM3fNUS@|~^}FQk$0Tmir8M*jDSo})DK;c7Zx=2dTT+Rk`y6Iq-HGj^3* zob1*+c9Zmx6@Ee0=-s-k9xF)iqAFllrNtTl-BYWG-=zY6RqFkh&quBzz3*B9zv}dU z)^9(rrJ0X5qgSED8Tk8;1vK){R=}>xk$?Z{r`OQPcdLLeXXH_>%HNX~uB8}m4ELw} zK90or6x!WCI@x9kjd-kDVixvudZ^iIg|3y9-F>2S@*d)MuYk{uozc_?^UH3kQRR2 z@AjRf1-&Zx0u!?%%tv!DF0R!TnuGX?`BgXvmrIKe&>SRG`~{Nc09CDhnti#LPRG5~ zUtkT($gl(VQh3x+mFdET&lXbNz7HU>039nLHXP@KxhM;Hbxq42>iO4K%k}UF$xWIK z3a>Lz1=(r+v*#&q-%rP-gju>RqYBBjmmlL51JCKm$^klAl66D(9;YuWK20~N`eU-R zn^dUwXWL6J=hAo&(6JRKUYgWkrrUbjZYT2dm@I3iL5%DEvb0?^-UBgN-gtv}-lmiH zTJ`N%>^InTMPqk%S_)o$drubL4~PaK|4 zk`4w$N|>Qkr058v)q67h&rP&?QvlawfW@r_$X@NO`5=E&H&xFyx!x-(E7}j=_~e8A zWSU-&3v55epTO_iwZ$Zj-!#Bz}A#!wxJq$QUp3}U6+LIIKEK3~4Hjq;>U4cqK_GST@( z=g5Slg!OcWaUmjQqAPN+h36XC%sG);_-j@fx-XE^7Zt^97T6-zQOstTo+zo8AIkSf zygC02`B`oqGb5W;5qs*uE~Wks__cR^n89C__vpx|J+r7rq51sat~>aAdNn-d)Drq0 zi0abnUqA8^Wx{=c$ko??6_peDHm7!t-aj|6QGylgzJT`eVj6U!qhb^PI%# z=Sb!;W>k4HgP1_!l}pQMb-jk^?F`c_Y{}5-dU)ib1GKuvnovze7N#SMvj6n;XG!L9 zm@ea#4(?!O{gS(YWPaU*Do;^H8<3g5??|^99Z*q?LOFucITH?$|2YwG<@uk;`@7hA>N@hm-T~yQ^!{qJ z+nGt{hO~O>J&_s;Dew!uKvT60-EFZF;#*WfT(w@ZM=i1A7HACeHcWKPq(<^GWlB{ zBC6b-5bR6Sx_!Nj=69-w9u?2t02IsM6D&dDOmK`Upwf&$45G<5|NQ`vlZ+~ zi;q2Yfo=&*(=r`t#tlJy#|OMW=434<-gF&LN)-g~=h|(touLTg#|EC9m^kvIhCf+$ zlKiMov`iN(Lofzgghv%su+I$}f1IicXK0D+LYCZYJhBg`S0CXW^=BH`O7GFoJsMyDe<~v@{F#Al zQeGmgS7?-@U(LHhqx`v%>Sp&bBzTQT)}P`wa#?2^sM4}YAfq#;+K#idhQ2VNN{gO> z{pLW_;gdAZa|~>=k`hsEMn3ZK_tzdEU-e4^RbtK%*yM9pmt3GT_E#D%t3&k%q|1|c zhJ_@q)4;R%p=R~tw99_2r^|YQ4(Qg&Z}#k`Y~eS0rj)Ed%8j0@k$R4Dqu*+X{IzZ; zUDTDflE5B+_1l9tQU>iiz>ZE&ce%a5a0t&WI_LVnA6G`_-0u&KWk`l>@t`GKJOwtlOWRC@p3g@Me&P<*~=U%K98V^V$+4Tyes{$T55bo z8fX+RP6RgimY7aO6jU5tP4x%>k^FXd$A2plPCVW zjwq)i9`cx0pSGT%o%|0i(UFuYt2PAPp}+0l(v=oQ{Zm85>LX*7yfv)%0`f-xGV;bqh1y1h6LXw}7^Z_TIiU7(Sb_sxgqS6GvQ_5HA$V=r0X9M0@x zPx89ltQsNB8qWR+c}9^Ib=lvXYtceZ>}$6Tb=wD1em`q`&%SOOyEo;IuzQkB9)J>Ng39!= zXcqZq%%zH~3KKrpC`v*ks-_a083D-)`UE=1k1T(^vF~kyM@Nu zYe=yK=pEr#+x)zTUMybWCwe2Lye0@{ZT#7l2Wi$;DkNSE+($}hYt#{@a=hpyYWR|49P#^}^d}}S9^H~eHlBghqTEE+}(-b*a2dF07 zq_8d_&281{nZu;H>lIy!uqXPk5{c7*zw_~yo_lHiZ&0LU9xU1#h4mFwy#93|@2G{5 zHU>znup$O0-bHz+TDvp z^hlBv^=f_PzQhBxYiud!CG%*a$@49uV~ftxrI+jRE~Z<{i7fHj%bw(OdmO{ zoUrg50)M>o+J_HNoN1?@mK^QjYQyePut_v*S;8v=-IRH5U7d4Om9k4&lTox`WqVjp zSap6u>AoSMb0eyp+`3`%CdzOWRP+T>`kA55ajp)#&r|2PLjRakqE5=~Lf~%=T6N!f z(x%;7mW-G50PVfQM|k{skAGaz!Uwzm+(eO&{pB*!vx`aa=|$nSiFU1N-;d0pF+C9AaTgsv z-XuJxAhI)hO==OzcQC;7yRb4cGC*g-XSY8>@*Ps~GP2!{lvD{PV-Vp!l79PH^3M+k zc#&E5q&OC6ab$&VUC~K(U%w7p=%iW{Ai}35#kB>@-uR1057O)%39?xHk%FKSWCMHh zk@{Wp$tyk@=x{KVC?81H$#{Y)@F;lZ_1&Q&*=a^ zwU^DA>`HfxU>!+Zcv}{>3h2z0GHYrPk2Q~KW|~QQaw$AABSQ*^~3(# zL~-PEdLEUgup-;R4hhjLptG6xPFzI$$$7u#I6bUQy(E1>_t$^yv~?++elC>r*a=oS z`AE`HFqf2zH8@jKGynq%*!;X>{aH*UUWlQtOu;yU@xU;CiUfMq{m$T^Q zO=UGm=v#=^dHv|jWwg%A%8A`wPNQ(5Ruqwr9k_QVk*<`J65KLgVFY^d?|bb_ER1wj zb$a>68trjWDmNJ_qNrrZyBXm;&7(J0SEpBLtuc1naJ$^-4eE8c!BxyVURG12jIpQK zlCt}<;#^mzCn~2pF1s~xK(v! zykQzbl!LMmR@)u?mcq5=oS}AA`Y3d3gJO@*=S|1l-23nUj z0z@ehYp~P5S?lMmw9{X&XiRrkyu*vb#=^4)PB^tMgdL`QL(OtpZs$h;JWdP z$Sb`O5Xr4!F%{$D5VtuXZj`(K@nv*ZsD0s@^XSZclQOeCeGa=M zj|$N;ViuMW?Pf*RUiJ)kB)uXZ`kW$K{qdPsh;~bm#`iG^G*Ek5`g+@q5rriEQ0RZ87MR|tZGneI1 zo}r$y^4WFU_U!$g(EVg|S7mIYEBsz{eLu^Y=o>tky-UdiHFK(jWkgmz;mKA<$g1C| zj4->7!!Ce&Y-!8|p-R`N6uVb~RY#HX_g59}wlH!7h1|y`v6cm80_xxV({?%=H&jNL zg{~4^9=t&dk1Vo4?VrD=m@Lp;ihTXph$0tDoCUldckSsL$&NQtWJ|SYB|B0aQf3ZB z8GfBQD4#~MF_X+1mmL*LfzcBhS317^#rJbE0Qcb@Is zCWp>D_o}H;QeNRk=Z;DF`4&dIPfb(a-!%-XsGg!3?Dl>AT$;h=Y92Ff2AH^(g6fKi zw~x)CmEWSAmSXK}OJ=UVkK*7Gn823SJJ9hcognWIGQ(5tNkijY9)2xL#@#;TMgF$- z%1ZJg9|&^0ySmx4GhOav;}$GBm*@=H@7bDpBm}NAs4+oj-wws4KQ%ZOz8hNdw#{95`yw;Y8$gWwNPO9)2qptGy zR*iXA`9~_DTj_3YLg;G<GF3N(nLEm5k z7S#geFE09eR4(lrkL$Q5Na68D*?_{t%uQ75&^E~I?aW{n#&ogEkhCcEz1<{fJ3SQ} zH(^PUeTiF;SU{T9KFI82;}*4wRvGYKS2asp`2_ohC;m_0AZy;+f!tlI>SK01&c5Wy zO20(bS0GP!%%f#{$~13uv8n zWG7uZdwQ~+Nvx1CES}x)!B>ie*BS5$U;XDh?h`((qb8)fU6~jwyw;E>QLmZ%IC&D! zXt~M-v1ESlM;0BNEpED|=RwN#byDaQW)E|_ZP`|PdWJXK%H|n`-h{^++*Mg@`}r8n za#)b;iSE&vhs3bJ!c<^Y9?N-aJFTM5iasm%kK8PG4oPQMgAV{{LV!>Z-g7E z5q5TU)s+;wkA_xK_>+5zX^bO`T$8|q@EC&=!0NGcm(d9z(ny7Y0AVIry)m0&w$TVj z8JR3^mSJ-nL%QNK3$*yRFP`JQ8@m{}z3i|>gQ&D9L;v*5jf-g2b~RE1H+PK3J5e&y zsEI>c@>LtHB3qT_ezLmyg7e+)bxy6I^WC#*s?wTGLUk89H*f0Rc>|rByJ?6l1=`gv zp(Yu{l*ZKhuasg+F=`@ggCjhT!0uOTyY!@m(PGs!SsDu>W;dR_do`UCx+}bJmSeH< zkV_29Ky>$|CxY~+u$;gaeYC-~l59~AHP_|zuoRP*x$OqCE6fD`;>N7+i%56kR7`rI zHp}ZaHr5ljLV);5?mdf$*i%KM*HGmU(FvjF`S4YALg*DFCeZ~5YK_23O5Jt}-r3R= z%OD}M4Zop(PA+{ze8u0O+$xmt4XBdn`ntz?7mNheH~8&gf<4O^z7*CkpwnL$?#UsY z?yY7jQp#Zhd-1^h-u$L!A2l<^mZlJ5v8tkA>hJB4ZlCiu9E_627(-%%3 zd6}xx`<1iPS!5~8s_Zvm$-`~VBzrR7XN^iFJilNye|^V&YiTtcQ{Jwdy&LJ2*Iz@ViXS;l)GMdUfe!rL*8v(PIf%T=6#EyJPz%e8*t?r7yFz_> zo)56SOsS|bMps+9mrZm6O9L7mC}B3T7zwjhE~eExP&xO6yWB2+GY~na9#l~}cEo>q z^m_ISetF$4k8*`ox%o^TU#%vKSy*P&4XS9DcRh83vR1%ngKv_u6J1m7fCssD{*2vi z*%u6|;0t=$Q&?8i?ha40d5n`U`W5KDi?#Zj1!P%l6@EpU-3H3gMfvC)+R2%+o=!=L z3ZHjO;KyDYtD0o-L@7ECEb2$we_v1M&?G;X8L~bOd#06FzMc+~C+Ysx@nl?`a5Z=RCF>r_)!Y_7~oR$P`Q13Pjw z@jS2Mc4)|H(d=eE7-&(gLOJ8FUaPg3&b~tc(b$?xl!&4W^JbS6QFLLbkyydR3X_57 z)9Z1^McS9sRAf3(@X@9BL2;7@aeW4n!Ouq8_tGAjZe+7UL>2{=$p;?x#1Zp$(x^L) ze3_#T$a{Wycny(VDl%KUJ)CLTisbB|9QuBUavHBRkK!-xWvIxpw(LZEZznq|gVeq7 zOrw2a>c#vs7AEg7HP!A;vCD`hf%Dqub#LIWNV`=$wg<7eBn1eK=2>(lp&Fi6Gg~jB z8Xk{^2@a&fBZ{hMt>0XCma1sIDq5T?^phd7hlQ5HQnH6W6^q3>Bw1i&5C537;1sQo z;YOmwm5C|_vyKfoK^23UYBJNA9!A42JdemfY;?!d7sx-%Qh3bTQY{wu(pdi$*se$l zcjUQd2Z@^<uMAs%d0cO~m@qWB7 zs;R8{EZ&jAM%gH;C===Mf@XuZkq(c<%mfw?mCzFxS)$js*4jgs=p_}Iop6ky6Y&`Z zb<3{KSLe`*c{xanaClgEZV8?@s(<-pX^T>dYrhiY1(Y_Hx+{Y+JJI<>qx#Nco%pSu zQ9*8$9W`?nA5{>&Ki*(&5#=UEt7!NsAW_+n6-vBsQ3+XtSM@Z0v)<+dhht$WQ6|3l zx9b*CCVq@5*JPOp)6u>bk6g`&|XG0!K%c4^&$^O0- z|h+%jstP`qwN*UpzIGCEnsjAQ~wu|{Ebe@DyE^$P9jBkYgtqdk3!ip;jv z@|r?c1;w`j3En88hwpgs1kv9&qq{xq9UxHpAU@L|qV;%Uo269i_JNj8cQCyUCkR&x zQ`-Uk`n7kRE$tcgLoJo^Tt0@i_h5|#9Zu4=p-kN(Sesa5kXUrzMLT}i0smg6l|5Da z9r2kC<|gwaEmJ^)(g~Bn2(oM(f>~x|8Ub&ONc(*j4n;%k?uu%#vLQw`&dh_Qr$!On9mnCSxsKaC))2| z?{^GljeHGCC%o>EzB}Uv@F?yKEmN|-BmboL9iQ<|1v9l|TH+F9MJL1o*?D_u@0_J3 z^5_XybLP;8KE{aqX0KhVi2JF5%Q_|4*|mCOFjshPkv=tgrClNE(`RaKnj?udgh6IR zn2GeM;CkyNvY($rW_OntI=sRb4ESVACM?-Z2D$Ql{vYIN^-M^yro-i3$?&g-d zMP#|ZP?M{s?gzxK(zj>i(b;K^njXjY$<`9I8SwtC*POo?^`)NIrfPW8=FD41@_nV} zNqB!9*`@gQHyoi;%-3q3a$`%zh=TlO;gsu(iTI6?C}T_k@%X||cN6hjBT>SrBD?Tp z?E@QVb$;h3k_#_?bt3CKX3h9hWPQK)lYLIsfZye`2Hyb-U2jqCko%^r(^Nb3gTn0+ z9YX~McBRyndGAUQ75{&|ogUj@eMl%M|5{}pzpl}CKzy{-(Fs&Mm$ zwfwqJ=%awLrmg;2xy!=%KdJb#y7Hs5(|aRFUm;KO=OB-{u~i~1sPkwVcl<0xV}DVq z^r$S5@Tz{l7LDXYd`>$5e z8vZTFwDJpQ-RVXEd?G+tR+OiCH~Q19l&AS!#qI3yrrR>i!NRK+*ylBWwc11#Eq|!E z;jXka2ldo8M+?i0BG0KEN|#XN`A-#F&IqHMK|Af^c2h3(FJ-+(r)RK7vRMzGVLsNC zk-h0N)L@o`#}{~PwK`-jB9CpJhRVV^$?TYIXJvOTr%9kxl#X_!E@#$kAZ`9zN6+$Q z_@gWap@nA_)$=xOKZVyS{bS@-Cblpi&2|2lzm(Ek|EuA%xEkx7=1a4K(30qAqcuFW z{)`K>hX2!0=`kMjR?4DWB<<|iCgzc}^EKRFc3!6|MYBDqw;4gO1b{FZWSHL>_SQ>Y1CrHGV^F;Kw4;`g~u4|MGNoV$8S9>G03X;vyo29cNi2) zSmL(8P9N_SzMAZGt_hpj4wDNXqLQQg2P?L0JVo~p@+zZ;yD~!0KcgNJ&im%>r5+MX z&A%X!5Q$<38ws-j>YbKHyH`c8eCNY-NAaU8u6lQ z5S{0qyEdzsWY0ID)BU5~8PO3EMjo6yUbuH}K6Q;+Z9=E~4D(Z?Bc$~2M7n?NfEn7P4$G*nO%^f1!jd;5u` zm9OBBjaM347Ty6-)ky1tT>cK&1{Ik*>oRf%V73ppN-Lv0|3*L0<)k~Lfd(&zWhSa& zsE0w{50CGlI>AjECUb8>rvZ&}J==)C$+cNSOK`Awq!B1Qhv-B-WmEVOI#F*?5uwW< zOhfURu;d;qC_b}Q@e*Tb1|z8FudFB^*r6W2cF9FzZc|AZ;TWFoO71SDW*2qz8#a5# zS+dRBRZMyZjP>G8ayv3u@fhnp>|{Nwr0BYzr3XpZ$Jm#kbWGVLQ zh|=sY7~AC0J(kfuqkSr7mvE!YD7=>en~;6i(rsiD_N!Pio>a$R8G8$mZO)TR50h*M zR6G=b5S9&CfQEyfT4!ObgKCy9odv`t=n5Fin){COZt{mzyk4@#5_tPx%=u{r?bC-< zJi1+DFlxf~1ywgDPJa6kRW}xCXd{>xB6HtDQI+Qw{C<)=`XeebOF=S8?R0Ca0=%de zq5eB-@^_!3{yRqjotzc!ut?mwQ6&7w8-|^vtmrY5Z>V4h#OEHY+N-7&9i@oMalnsH zr3{Gq0Eo^3icR19_PH&z+ni8*#8{itmSRtq*+mgW;_EM&$ZtKJRHSD2HYgb$=p-TK zO&m1a`ExIxr`bNG;fDL%tnGg#c;VSbIof|*ukpJ0;vm15Et7@ClR+CKDZ~}k>+#NB zr!~aBNggRgbSo&e&(af=u{;wbvcQ19NINjap<7avPs@DwDqh!DqG9_o0$AhrBtFU@ z`%vql<44FtJgeg3NtLl?&yPQQh1P7Tf6dz2L(;6Dlr1BJbs?1#{RbzzZevgHrTE)9 zzl=On7th{~WzT2^GHB=mfcOXlkK^YLOYUvv+#||HmZ@OumnBpOc1dBA1@^Gk^)n&T*NgN_>d_}OZhF-E9EpVD``mM zeGTm zKw%p2fj`{v-vZjtuG7<4g?J+CZ5?{$5Aqc@?3uQiYHw=zsfj~aA2Al;W^ciIqiigE zC_{(9S$0H?>o-tb;Cc;LPHzkpE9@S>Y7y<*H4Qw<6Y~OCNw2)1QGTMvom*&>Z!pkf zx!VFteS}vkI@!MU)QOFBvaMyxWoc?D@4=!m|10#85ayggV&0j(?->l`*qg?C&Ew6iCJFw$! z95Q)n8^*o`utBFx!vKidP&DV!jsMQEL2BW{mq+r_p!PfYL{>;i3}c|5Y=YH{PIxJ4my3+(=#Q#T8K_Snf+(; z*i3yrQ_cwpoH0Ld*#8nqc_$$D0S%61jBL$4kE|eO13gn#?|yI+ns=$;0?N-e)H3Z( zyW7DY=JAGky>4ob2oKic>lS>$SCg*c8REOp7qIA*+%G^H{Yn0KUX9yG_XQjcoFLJ; zMioQ%E&XZ-RSY%OGsCN5S3><6Z+-9-e=Vnp{tIZTHQw@`K!Hls>$Qq%swbglO5uHWv))1pTVTm5r9{{vO79U#>wHbS* zJHOY|Oh={1YmKg*s1_qR-zux0M{?eyAu8R*Y26y8Q(R79G4g9KIzxW!y(Uz!WQDmX znsQ{_MgGpoeSk~BR)>V16KIr29XM1-qugA>jkOK6CuGPuo1!?w_Y;qoP@JKKhKe>^ zVNC*Sbk*OD4$&IDUr$4ut@vyLe=*^)f6tS@_<)X>#V)_A!-1jeF!IZ1AIm8tzx+WB zokgtDC}QtnPuR%luYkzy2hZgFwI2fHcwb@?Y>+QJ;>h;r9f;xH>cbkce~Sl;R#>k< zRIpRG)5mG;v{a&k{GCgdh;a?IJ9$)F!t4U?aq5+8iz&kKh$1OFU9*EOJ@irO6BsgN zUVq}XVb&qPR-ipWfI;>#0?c%J06g?T$6iV0%J_G&;!g zIkNvaMUn2-L3+G{V)_+`Ejre1fPST4|J(kCjQg0PUu>+stVT6-twQ_2Zeh)qQS2fg`-)`PR)W-pp)L8-*te{4jJ@=EJ*j7WemiP2BKt<;g)myCiZU{fa zx6?8M2~^{>ic(QDblQ$C$H-Q+SE%vSOo|zQE3el~aIs#=?hJm5I&_(VFZFHvJv=M_ zgpN*K2jW>(aFVfsm$0=!`Mwg*9NszjNoC!zO~4xAOHVgm&=RJh_(9K{-TWVMdnW=7)Y-O>4BHpGNJ2tkhkRr54$L zN{H|we>igJ)?%``PphdcW}hAOcG4R3$s!6@8Czr@pqv+ z``N5f1)Ei6_Xcj7I1~8)e>~LgEUnOR4U>{zQu^V8S*@F$!uxJT1c}TF$&(WKNd4CK z-$Oo9q{39v;mQUc!LFQp?f!B%gQ*uDV^qUZoZ!1mH7rqPbfsCc0WQ3Tz&>LcdCP7( zvv)DFnQqC6E}}EXw$C2EOnyXHBNsX^qOu~pI4SJO60(cYMkYH6R9$2~dZne9WPR4i zjnA|h(=NiYf|%3S|IRo^I@Qg{?CoS7@{@+zli37^tW!vzAKbX2h}bb|w$;Cc0*$|o zo1d`UV1J*ncJvjR<5bw25j7uP3xegq8N&MZhao*Hq{dg9?xqYyK%C9eWNlB}t34dqrl-Y9jYn zk=b4t>vFohE~g_YTuS{M$}YA!U$Ba@ivubmQ(Bjv^$vN|(SEV;{`$vhzj$8z1#x!w z;BXfUlF8_cj&6|@Ed6XF-69!i$~6^RcwHm8-%4J&l;nOv%a6}yPlTr92^Z!fxleTY zW*NzCHRVc_8&vz8YIr-Z_8Fw&$NE^=&2WQy3CoMd`S!Dm579W=R6J!VC%8;DEMYFn zo_uj@Oa2aQq6yc-@_?aBjCPE!GcWL3?j#ek4Br6twrGCziz8IWm8{~Xq*LTMbUBgt z(`xm9IpqD=RXk=WZR}kGfE3-8kdNs)^2&blF$epJ)Z1JsW}>(UZ*`uC9;_5)qsZi^ zGgk20gcKEO3$nE}6Eti#hxU#KD^xmNXD{PNY z9Nl+I$5OgClcwg%spJJ-^PYiY7t=12uBLhUr4DN)gDfm9%9ZTt9lnCRS106GG}ywr z1w6^vQ7e~`Kj-q(eJs6L1>Lbhb?N5Fb<3!_G(%0U@G)Icc~PAG;MfHREQ~r#O_f!P z2x7n`cRYBCV%}~)kJTqrA#0K^jpa>PeV9#BZ35cYhs+sph3YXpYPR$Wb)?hF+Z{Vc zI_*`FDJ1GjQW7}KRc7xPv7ZJ5B0R?+E}Q>Uoi*e~`LuK;wb417T@5y1g;z3q&E>5o zt&h`dF2l84dclrAmFo4m?b783yJ5}OoQr8U%v61cQok0u+-Sc)y&~cQf$C3NGPA zTC|SqKd|CBt>YIpe5C^sv(-5GE-pFnb9|r0o+i&@BqCFJhR2vn7hb_&Z`k)r^iJ9v zUP4?sISybq!asWA2-S_hjHq;*$i^Bj(lMXeuFzK!>bg4r+3+RQbMTc4zMvcH6)y2B zz^r%K(en&NT1KI-;A$5ZA08deE`Xrx_oDNUGR5_}zO^uKPo*)k%>38)bPq2Q(=k0y83PJpkfPS zzolbK>7or*(@i^53ZG=W@mikpv@z?%<98(@JxMm@-lBc5&$zXSO5P4shCKdM`to+{ zOD3Q%(R_x)y#;*72aTqb(RaLE>38rF5;PpbT8M4}+_mHAT8iLI)P6@~PlM8njz03> zzG(X3DY9zs=x9!IBxx&S@46Vk;&K9?=EC4Xi|H&h36Yt7Y1OuBxGnyU0_A+Iasp-leciM`~ZOw&+=he=3v`8+mO1=WSUNS%QkZ*i%?&iwobt}?0!{zUf;p0KoxR9omHjm~+4 z-aN`Hf@kQN;Wm#wI)#d5P(jlFtL@CAqpI#WZY>Sgg<^|D1Y=M^DT2ysi`GI&1dS#F zA?~k}nK#KCCiCLF8A6V`RC~nwM_p*G8?7sXS`e*bQT8QB1mY3|i6Mk7D6$g>roZ2v zH|zaQd+wQ=KXUTtXMXqH``hpDYLWM;=a;Tzd0#?oxsapGMBNh;iVlCnF~+P^CV7Ou zO3H}xv~+2ymCrBux`X?a)U&kzkjN0JEG^QNUAufB?zH|hm03swtjZ`~CJ$QD!hK-l ziBDNazbtXBYBT7jttBn7Iu}%uy7zxeykGxHDwk%W@#p~!RN&#=)%BbuEbXhQ)M5(K zLz>PH;10O#rJc-uEtNaT7xJ5$RvB=|tv`J+b6-#8mhe1EJzP|U4PWt@=#G6Om0Mcj z3q^((N*`GD)Xy%c(``Sk+YYW_%uS}!dLhndqJyr;v+k;1&S95ml zU#ZLj%kMEg1>sJL6harC`yK14d8yn&wn7lAzLSA|tRFC^jk)t7SC4)~zIyF4{|-L4 z@1}A))%wlu+tI<&E=Z*cE7W(_bmu58U0 z(`Ek+xq9Rs3FPhBeM-kD`o15eaJl&ErQp|5S)&7<@^{vEgQBFL#s zRjY-%42FJRx0WsRe@aur0<s)0J7w@*_Ze@>TNivldSkIS59=&^4WfOb!OPy6F{Sna};&s2# zHvh|fjZvPd=&1+TYDP(#BtVCFJUQQ4#I4WiO7YXYwj%Z&QxisNm11!(EHLOT=iI&NTk$5z~c&fNN1X^%6zV&qNKPgAFcGn)z^wlVqKJPQHIGZ zG|OG?nnJ$6p&|1!3}jwfvvcbBZMDxFZn`EvRTtif8axav1A)3OG zcje!Xv9;Y0XHfw>vXc4JGPikX0#e?lOKQ1twK2-{*;Rg5%|t8ApOP}s5>|Cpk?!3* zwoKUg`Y4rVLzQ0n}yz(4M~deLh3kG}*=YOTrmtD@TjCDoPNbBXztv$h2;zsenA(yVl^ySl|k|{G#8uK+|il=#-WXykAttHpx_){VfTqyn|3S^!5keUC%kmuO+4t z8F7A7NnxRcRz0bxi(u}<&0q39+8iZ&cv>YjEkuU=WzGaqN6m;G4SZgSunfc0s z6Z1)PiF7H{-4$BB-cpv&Jww`SM%pC_hC zZBIu{V?RygPxGX1tEN zbl@8no$-eq{0;Xc@I=6^Ln8nSHl)w!tnc_h=S5}hPtDoM z-pIZf%NL3W$M4%o$3uB-QB?M@Yh@!xWj{ojl$YY$RZ+6Mtg1Q|)exS$wW5V<2>WAH zXZS8kASla;_R#gSFKFdGbRb5In6W(0Bv2?g$rUWuvVXyOd1CVG4Qx>kI_$)}#aJ55 zNS(RzGAm1s@^Qnz$o`SzmO~EtCd!!b#B4GvZl08*VNU^_G~vA7yLcZTj!~cta~)O#B)~5de(wR9j!!<7N!e9l_f-eNzSNw zExe14#dwJ?38V>rmB}a$ef+MTBCF8mkc(-C2)!20O;2>CI`ch*Jit(|z*4@Qblmx# zbnP>)l=$+trq~6s?;Tjao!KYSv5RZ8WLnXU>Tlk-d==lQZgq| zjubaDKdSNFv;>)ES6HA}pZYw%FmD$`S>p$7q2)c1PczJYb`PaWE%U|5^(MH+DS z!oy;oa~Eet`9i3X@1e0jd6teEF`Bwom%nNi&%^ndBV$RRLSz;^H<&eL7S&yZP37m7 zHGENEQHvgI>tKtT<;XnQHHNEsRP_N5^Q!M#a9-}L_+Srb#<~KcuKFRWE*0;4ZXI(^ zOXg~=TcYYxx02izt}gxD;kwG`>2;tKls#^+wo@NkSjRG+9%Xqv2ET(IvJ5Imo70Ru z4Z8?%vkH5NZ08y2xCI)xK;!>?=p*f{@y~QlkIQ!Zd`5Np7IEbrJPg>=(!We-IghAk zInuff`lgbAs^1gEr8$E)h)id<7|G-FYM7^kq-s39%SM*!7co}A%sgw1NKfR{f61io zF;bP4c?AirpYi$IdpNE?J5CB(8lo`7x_R=vP0adbjCFU|(xA1`3QpSa&Ng1bU&UB< zxH97%h~l5%)F=0`4ek*o`Dtu6wS$(+g#B)=KVZP9lIT7f~8C{FkaQxBB`KBIoD!(p6LA#z_5@dCA`aRUaR`|Ngz*)82pSSMSIrQbgQVZ zUmE368ARV5K;%U|y?0j!dr<@8MDl95IZYB!%Z|Oc`B5>e`mG!gZNM_n0=1P?oT&)&temu9z`J zPzOm=x-Dh8z|)7yRA95_TzcAZ)(wLtDk^R%6G42xY30DLIj?ZJMC6@iRE1>6QGFi+ zNuPObg?Pv23W*yqGmjWimT1-uLr$=yS0bX}Mb}rgNV?^or-e=$B5^5KpZVSe`QsVC z>$8^g$3tZzg;x?%T}luMTsdOlHnyb0WM+sat4dJSy*muZ`Do?r6}^Z#++nigC4tnX z1G&_u(#n(UnT$vvr8S)DL?GMU^|dGX8;(pQrgsyRnW)Cz=)P_}*VwOuOpWq_eBb`; z(xsg5`;EgUvtpZ++VmP$&_Dr+vcxDSFreY{`dp&^)`^%oeQQvkn2xSWv}5OwoqL3L z>?lV{vLqUER>-sM@~TU`_jk2JB3(nPsn4@T6_5L0+W!?-Jg&h^!)4b5+=lJ)YS2Mw zbqCCSqlH)ZcTqkHjnwxIs9*K&F~6C;zH4KoU?dYSD|BTZ%0P}NSaFCmkiU=fDoh>s zJb$=fNETgQ3hO=a@T~U0}|KO9$I6fHzh()CQOnsGYJuqPR~qFrjwcOp}Qx^ zWD${NS(as4WHGXA#E3B>B1Vje7!eT>kwrvAjEE5!Z3E|d98T0QG z3880y#u>9jBC!eAO`=%8i!pw@C<5uwos18hB~rq=Y@GY(7dN{q_BVQ2bCqA`v@<#LJe71W3Pd2jj-$L<$sk&66k|+#?9-0p}}B!4q`ze^&0ZmnPlT#;vYZc~_# zE3kHyvHBE|NZf!cFoAeW+>I+hc|mCHV5~U=zX>lgS`Em79f%jn@PS_+#&{I}6COu=2#>B~{OVqj5FR^~@r(Ol5B%~f1=#-*_|;jA zpU*(Jz>f16kKY7;!1h}i8>fqe@SoQ*{u}WjT-nRG=0cGG>93bdBp-w?!pHVyT(b*$ z;FHfOOvM%W#5TtI2Sq}->H@~qGetu9ul0GB9TCvHvk`7%lJ>kf$*tY6z1Uy{5SHJ@G+DP z;nVvtK6yFP1zdS2A&A83O8LK3OAn2_~NyqK={JljGGRC9{A$nj2mwR57@dD@?9cDc}aW>?VIo)S2HfZ z34H)?1^NYvJ-7n@aX8}&*b%N;CsEh}e-d4|0yiU0K#HF-VxjBU6APcCHqcDqP{WM8x&!WPtVApPw*63M>%Lk@iD35oQn z69pl;`FO^@yP!w@-aaak+`Jj`&7$ztX%fjzsB=L21UQ60>?@J{0BsCN`)*+LA1YG9 zv$$6P$q&#k0O=prNhDuJy8w!_*GUwHxduw3fzW#mZ1NE zZ_HP?4Od_r>Jj0a$Ya7Cdw@$B-?~K<2zMYoKyo(n=wh_r4_?JMdkSR0KcoL3Tzn4W zpAmnc@V&!C`tb)P3U^Hr48->?;5*ka?s^)(pBCvSrZU#!o(B}ZGhHJ6INAzOxD)Xp zeD@xOb8rRjK2aiFzZG`CCoY#L{CHFpeuVZwc<=&+XK^JUE`%SgmnaV5K1isv7{gtn zNT@ujFa=j&=sL#m7U;K#^q2jj@NJ~|qpL;X*IgoAdcQ~(T_KVm&PMuCzGtG|0fomO zVf^9+Q6TJ?$#`ssC=h^utCh~G%d zuhGZewnZY{h;fU=W4Jyh3csBtk$z@|AQYZPo&f3R_me0*d8i76n=lXkoa9H3WR5-F@86W-@wyI1L29~66xpA-hsmJwln^)74Zh1LZ1Po*Kd?4 z{Qd~|Jp%uu-UEf-Tr81(2K^IZ6WR+03qe zCbaL@p`U-Rdu70HpeiDVk? z9dEl&BuDHJ$F#BQKi{DegISRV{ zMRGXW?mMr+bvojOxJnJ5av4&_JIqLu16t%5#b@ehav8VB8-Dz_uA_bMh)r0 zJ>wR%SD^T&gCx?!kZ**yqbvw-zM1h>lp|q!fid+|krEC^xf9+FIpNK-7;lAN!rM~D zn^0c}Z#jxFLx_|xZ6@Q0gGEZ1ej#Hj`gg+N*D>A(IpIxrFy68met^U7W=y*ger^=W zu?5DvdqhGw7G**>?h(eZtMMCnH_D7~+;YYrk&h=_qJVrn0XT6b6J>k9JN`UuX;AE5~kbV>S z1|+j)GTs9pgjpLI??K%loQ(Jr-m{xAXR1g}+eZQ6oCdrPX(ODrj?wXeNC>Ci%6Q+^ z(4%a>`J6;D`!pf(5BDI>z^SJ&W+T0XjBS!ubj}0Ma^i6l1tYB!m$mk^b-s zK}c30E`-XyjKMoZLa3mQ5{7SM3|#|1z>4*Z%Hz-@?LRz3B57VL(jTEbn=>WS2N6%g z!&@0YMO!31G9-~y=Ltgk&~Az3+@*q${sd_Sl5vy`kUq4J!uhxYKR%4{vr|P%*nXS> z!r2b|>=wq)AqSFGC?~>LFJlz#kgyVIA&j>e^(#d}sG%+q8n;WNKYS7C1AcN2AzfTs0ui>6|>t>82uVCDQ z`JH)G_fc4f zD*@>zTt39O4E2U^1-OJu4rct@X(Ew8-2V;t%U>NKkzD$KkoXU@6A9$qe*ph-qD1=m z{@`6JQ9KKAdi;D*Jm&#XJPYxB{3(&vP+o*O@=F5ug*q^X`#j;idl}6cA_bDKqr8D) z=e-i?6YDV6#yI))n1>-8ascD?C*t>sB3*-eNLYozjPD#R62do-zY-|FZ%h~Iu2UtF+fZ&m`p4TDyOAG+zg{GfeD5+rNdJT|f#jam zjBgz;5+L34fWq0hqW{_Uf<*eKB_jFo5sV9PC7gFF<9s0! z3B>h$;3HcZ=OZ5pAL(Lz4`o5vb|K?}E8qwC@a2q;7Vr#E5b2}HGs0sH#-k{I!Y`ha zNIu;V$#r)Ei!sl)4s#!mi{vxNZ^EW2jL%$$-@yMo3@m5d_oPT9#&HFHxCQruhcGss zCXzYNiR9OpiR6hLBK`YWq9{<$f#eC~7m)t_Zi(dA&w~g2=48gs`$Qr!fGhCCYQ~&P z;S>1HX2$Q3_w%P>EKJYZm`4SYCucH#i#AU99nwPhE!rYb6nIVok|#GYb`Oce7pT~Fu=6itjH4iWjyg{Tu z@sK?d>1z)LA9xMw7U8vMhlB%9lPGK+7Rhep*&81h>7g$&cCSa9T`!7<&0@R}eF~7i z9{m#GkaHNXUx(ko8&Fpz;PVZ@ArCTM*C|rMLH99U2Ody-!^0Bk8&@J+;Pq1&f7vEd zi7s4$L#Htg-XKzmBXKJpq(@vS()sB72$*MQpbt3% z=v*pMJZip>c+-`59s#EA$C$QT6bWyBj`0@6N1}x*Fl{XZ&r(Ig)IACp;Cg{b7b1@d z3(&Vnti~1SMVttWPG!6w{TWa^pI5C7op5ALcT9!yyH=k0>!rUZCfSMV^TpVzI~HKdJ66#5*OeK9D{ZX6lWYNk^aL@!5GICnE9|o z@vUbGi8D|Rgf7$v3ACZE14R11yA*nHMO&MJ{07ofA5vJ3E6}abhd2`!qE8@nBVP&g z7fKZ0`T)XvK%~d^DBO-KaLg{oO!y(pUaxRCuE2Yc|Abjv7$;8^sl-*d0!N+5cqe=j zI*wPk5m(@yCk-YLK!MGMz;D9|6>21g(AUWV7iS+9aAzX}G{=Ss)!c`(6ykfgV`n3z;??RF6 zcbfvn7W)CO7?mi_M!5p%LQ_m`b$`LFym`e;0O4|CW++L8->IH2O%Edf8%J0 zH(w|!1#esT!8x$klwn3aT~%Ud>C_=_wC2{9_BC!KfIpt zKf|Ix`2IG={dbB2;RjbU?!#Owkp5Rep@l1OO}|9p9z5$wV1D%;;Qob-|Je?|z`ZjS zZon0|r@{C>ctCpf1&mL;hn!obDJcR1Y>cD191iR*(#CVauwzzuM&m79LCs#{2=Te zmq>3K7KFm?M;U*_oD-1Vuw0_>_dSA;h!Kgxi_Z(jMYsabrwZre3j8n9AOZRR0xw*q zfONh9yeJs|cb+H^{z7p>9I@U-DEx6qBE9KB#0&V+JjNG#u#OMh@G#@6$a|ph0@6kJ z^E}4i;S)%|f^-3eKi?{mZaH2^d^XApPP+5`{f8k&nQ$C=(zlY?4U7Iur52v%t&ll}NWP77}0Cf%xnI=R}Eg z>m85-TW*pl{0(&fm0{44N`1e{TjNI!I^kT@Incc3^IX(F5r4&nW$F{+D25lFv@G9>)?Qic6exUVt3-Ms?t6qwkiQZr$4f2}h0jir zNH1L}B>weD*a8217=z}h2p6M{N*shMaOrM|!Y0%UApMs^B?_N88R4BQ(vLpN_&3A@ zC~P`HBK_xU;TO0F^@Q**8zc(X-75&`B?n3ru3L`Z%SBoNM*=n#U>JQA;XLT7mq--P zMg9@SuVXaN5k(2y*PB2CWh${2SD@9&n7~+^&_X#9&OM0HcoIB3U(;OFUHdb>gSrVM z??0Q-cZWy_OLsHQLVrV8c0c0-^F%^ex*y{M_ux0M>@3Ea%S9rw9ao_LM#f_JB=nua zSPUP8_dm$!ZD7nm*eQ{Id$wTQiYw+YmaJEphU+xUHJ`=!F3OZ}7wVJ5i@3gsxSc1F zezyfZaL4_Ol?{v!p2B<@%65ETTwg%_oiEaJM^Jz9`vZ4l{Q`BM1>U-&fc2P*y97Wz zSdDyJ3EP2vXY^9euM%y{@A@PUDL?Thcl~7*De0>(D~i09T19*H?q)DgnJKEb%;Qp~dg|0)zv5B{|wie)_45|&@X zC|`iz7odF5#)0DOeI(Ll&j>Iva*C=bGNlyP+r#z;q*~Q2+ptMe+xBzvD(1U(j;%;0Kr_wHo;=CR~ zNY9zcXrNsJ#SaWfq~&{&W?%q$3lz`9GbfO)f{ZYFq5|@JbQ9{q28k5=-y|k_MJjO* zuJ_>HfqN8?-hYTh@k1??0npjcST-(-5>Mg!6z0)k2NcglJ`X{)FGd50ET)gwgXf_J*|J zH;|&tfaHZkC5o7EAte91S>bqGfv4_e{O(DS0L7O*FOfX84Dw~7xDWE5P`sY856S^Z z{*FG6aL!p0>AlxuJrwvK)ME+cGga>a^n1?IyqhZDa&x-^oo`W$Fke-D+CVcQpiR8yuz#jM+ z;sX@^gmD^>ei3yWC_MWl_eFX$s?CBe!2n)5uC_4$*?IGJEC5j39E+F~yHpX92-h{uc zV?6r^!hZxDD?kF?1Qo(?E5q$?S~vl9%t;k7IL6)zQS%?f#S`KWE4E0hR!;hXmNk3b8g?9#ofLAix!v0CtD-++PjZER+LH$m-O^xDdHoB!;JoU zOBS9{>g?$65T)M9(&?2JVZ7WLDb1-5RTd4btPHl`t~xeeYD4IqWM=8~Ii>DqSG86i zt*)yKDZ@OiFYj-Z2Uq3TXq{`(Ss9J^mr|FuxY9&aY!YllwZ>&HtgKmDZp7GH6R>lr z!qB?=kSSqAS!g)OwIwx~#*pY}Ty`Fp<-LCqNzCsI-<9(&BzFxEj6Q-3&ZO3q{ z(&(x;R=EU6C`o?wLvF{&gRLZ*XATL1m6fCJB_cd8|f3sC?>53fjru11uFIHhF)Z8iy%A=$8L8^=L=Ky*!n@PEh-bq)r zq}HO|&jc3J`4+^8^IZ*6D%(W&qS+I}!<9yV#r6q~4O)e~#ZmD|JL4skyG(KrA+p#I7gx%o-Oav zkOgB|8cQwfjLKw|B!!kUuiS*hO#U9UOm_x$Q#r#6g(8BH1 z7FJf{u_w$OE-Q8<$i_xrgmh$0?-PFt14ucs}&9x)i5U(M&q;Nu~4ua3byGgLD{j3Wu7EFw`s^g>fZsi*pb!gWyWXC+d328$1 z;>sBM;-TEQS$$~P-VY|g2&|Q_`2c;dD1X^pA)5J{GGR#P$@>&z>@>o67 z?v&JcF|{g)pOG78yqbmeD_*g}8WpKZULA^59=fM^St4hWl84AWlnv3 zva{SOn@&!}-pFlejry2%WcWH5Uu&#rOwa{gkVncY)Z!_+JTz3Q*Kjkgm#E{$jZy`L z8);M-6L+<<=|V4{!pD%Uw~l!x=l+n@7j_?!rI&Gjx@`R$;gu%`+R- zRwd5&m5vRbh==k_9Gzo9a}9YGj?P%f%I>(*jayd~8qPUt-!2}Ibg5K^uLkkK1Evba zSB5rW?%L-8Wv&PPLEN?2_|rr9fTzWV$7A1ls5xQKuEx;u01mmLq8_VQDY5yjM$|#z zN@`f%C|jqG9)u0UnXn3EnEb)aXQ>m@la0C_dLcr-RQ<-1M78NWZAy|BdYq1?^AM@s zF;lA)pL~o>jC%8m(yyyf-0oUcXRr3@h|ntQ_0eE*uGEF;)(Wc7gcD ziYQzW9#`_>U{o&Ot4TH%Qta|5Ti7#htT{$mPUwNFs%bAaJTi?an{Ixyv!+uFN`e`6 z=h+&CO@5n3s(#C!wNc^@mI$^jN`6G|Tk2}B7xi2#t@AB9Xofo;j8ggh-bQ7-+|Umj zIw2}zM(Nvll_N0GI^Uub^Q*b>GD_dZX>>sttWj)Q+pzO-nyICl#C0F_2z|6y7n)do zqh_I0K0nH&+qf+3+zR9>T=G0K)n}qfR4LZH`4|@j#aEItO+dx74;rS{L1YQE7*|2{ z&?D!#O7mkxDbobqh-0m=SqinGRgj08ZgMfxu$x$HleZO4TxVqvtJ`y%gJrWEqJ!{N zajDmGa0haoX$u}Cnq@7MT}Ez*D6McrX{v%!WVuU&ETA%H@=}uvB#ZQ-O)TOVh3h%L zUvy1OiD4eBg+(Y7R%g|{MR`(l)1HII+BH!dY+;!fJ!D5?h3>f8Q#|d03U)XQbr1Ot zkhk3DmMD;r6H)r+Z3srS6iey##-MR;a$g0U)f-bcPO=vE+ol+awx&evwMtjNjI}02 zY`dLyo3SHh#HLL=QOD-4E{{4HZgM1)=R1ySJfW^II$@EN2NWk>T9b{L(L@6?d?%lb zg`ZC)x|!LP%N(T_3OkL3p)^hkSaN2yHG-wEHTA}jZ89d8W1QYe9p93zm?}ubF{bq6 zSi@-0uBLGivaA=ergw6VaYFUmBqtDiUJc3d7t}i&$2f=9dz|oWej9}eF_Z9^rgzfP zWco}TL%GM)X^*k3&IGuiysFYa9*wu7GISfX)34@gjY4Mu8HL*&RBlEn#+;(Ju%6c~ zYYiSi_Z9 zheJF+)$QoUF>$n>)xz?a9=m4co+O-bahz+F<>SdaFaD0o<2y0bPtrOFm^fO`DjI+A zrQBocNJgA%mF0Igu?wK8?;}~cm0+)&X&O4do~$yeo_1kF|D*h@ffgtoBJS%*pmZ6h zu0U^a-~oaE1m? z%!@m1<5X!!rqK^nLP~DSZm&Q;n$ai`GMiK;GLol-mP7l6R+k(0{-k+EVYyV|QLa%~ zE;eJ<&>FLTtwNDeh32lsI+2R0PLdT{r!gLq#9~Fml0oCRo;n`l?6$nd0 z^;KHgCwOyU(qAAZ@)&kqtTmm#`q|N(qcl#`ndzvkdG}a(1s*u+6YjoL?b}mY8_ejA z=Gu5lYeSDn9Ew}_IAX5Ox5!NknOG(6qJyRKfcA`R)xYI`<8|GAvPS-vnsX=Wx-(=I z3)re*R<)Fw6i8jIqDOPAlE0;9s5-2B8dkAD>Y5XJe+sL--`wAv*}w)de>xF4Li^oV zk6YBBH`z!X7;TD(C}`l)+o-R_9H-r-*gL6$h!~*FqIpLY@PbHT9@BG_iIMT`^~X9% zRLOfMZKPzSmk%JJ6u)IMyPMwXlPA<1YG<5hroJ=JQ8-*qVwS4p4wJJr%T#iQ=@d_v z?kXJ{&burL7}SQfBf>jlDxHG8e3+)yf*)t=+Vn_0ig5I!9{jqRS;I zlQ1VGx0eO8Kjbx3`3CMZfy;(m5z*o(?J>`R$~VY(R&47T8JFidP$|zenx@@}8P~2N z;Fiv))z{RpTIDPT$%JYfhY*;W67~4>1S!n0#y$MniQ(eZiVD8(pL>VurG*(pHnN8_HOI?!hL^5<3OU8%n zs6p8%nd?DLRaVYDF5=0mltFKOT+wuykVGlHE#xx7EXk_1vD7Q|9PD?n&ydnwi4;LK zxbCn5IC7uq)H8ni(!nYVNADZ@Iic9L7IGtUtbOmQ)j4|QT`7F;MyN<$JEZX<-n}r- z);a-sB;QB(&|d64f^WMQORH{gmaFv@PG0$WsS@hweM3Jl72A{)dTY!cHqmS7M{1pc zo<_M`P@Wv9_^*j*y021v^l&uL=Wg2R3$#&IuMF+2#TBUkIHMds636z+p zs!ucqD`(<)(p}k>hI0NXrr~WJS#P{z8gT`8m2vNhkpe)5)o^#mgJj15X1XdvS&w+- ztW_P0Mz0fVa}|CLjl~gVn{^i>svxrD?nPPIlG;FhqBcYiz@u3$#+3C&B-sA)K$eUM z4%EvHyt;=$6}_-#ngtgO-d>|u|FVX?mBv`L8P_doRvNKH_9GMP6RoT>^{P`=(b2*_ zg8}SJ&Z_z*QOn2Xt~FaXxGv2Z!HbA^#2BnJn^~i?DvdgAmyO3;-lMZd9oQ{3GFE9- z2eUfbJ07oBiz|4gi(b9T8uemP0`+_^b!QRU((q_~P1fF4S+pEXjX2#rX99NcAy()H zB3ZDcRvo0})r?W!BpT@2Sh*<^wUnAMRO*;zceA6jiUxu?-bjt)5VJ9vTU%Xi)N5nt zF(S%EU0Gq*L~EiEZJ>Rv`Z&gG4S%YLD)*wkte@G~vakw86Ki#S&_#px!uZVskjFGyPXAUsV+G?q#h>JHoQ&^7*ffTAJxk zs3{?HyQss>+Rbj%S5<1g)mZL&UBl^(`h*_;sy0e*l%TNXRDRa;g8C3vV}g)jiPvfS z8+Zq-B`2-QsA#Ik#_c+)?467zzcivQZ?HESqmjEp+V1ln{-Ynlc z&oJD0#TZXID5>mdHfhP}sMk&F*Fc5w$0hOu^I7q^ZujCBxy zWB>-`&I)Y2of?|)-EHN?Huf_;J%>v_VXw9OJ=_ zXP$>B$9Tx5h35{RtH>5B@c{JlE2qw=@ z9!=g3`N2%iZdyAPEr6ijs3So#*e3Tt6#cr@)$9))HU>#|)dDrOQRU)?)N+ww~kL%GIN z9mnvGW&_|0&kHI{jH=qx%mPCPPF8YVnMiYHU?XOrS1FYnuY(DDnQgAZ<;F5za{XLK z$WzfQGM5pCMGPAf{R+cTGVLU1rVDj|qm|KXMP~h)`9`dHa#GxXbAp+U!sP}l>6WV$ znu}ufUgTqX6%5yL&JqgJc)bhcB0 za`Z^9)*v_9S_XQ@5@F66tv4~tWov=<>gXH`_{z0#bdCl5oUul&fyR^Ns2w|aa}=iT z=;--s#}47O4b3PW8#puKSV%_c+wgQsHtXO{seFE>A8`_+b-sn)`W?DdI9#_rnu-zB zF#M#3_WlVBYS7tcf+*I!adNSiGCPh~y=NRFH_rVNh_E|WT4jhG7 zUlTOOdZX@2K%WDV-nQdx`Eec5`C_gW&M&Oge1VP^OKm*8Hfu(IEEbA%~*etFt|WKr>P~TZ)A%jB~p? zRX)>pS^PFqng(8?rz{cuxYmb5+PXWvL8@;ZVR>E=Osi%@B{m^WEG6AAheEL3XT<$Zd@6?e5|$@`GJxmD{-SC868dkbSqDYCk^tV)=*8w^n{Aj z=WfZD1ePc@_R69`*-9W8N~k@crg^7lu@@~>EEHvgWu%JLflY=cIA5lIVT-nVYLE;| z$rJnj`aihkUi8k&F64TIr#QPVf8Y) zDuK2XqtubqeioFQ_5n~PqoWz+(!xNeE-Iyb~Lrv@v220lbjlbaZ0TzIC@sbx1;H39M_K2t3l-5(J2c& zVa~3ac|YCGja0Gdd}5U8{e?5IG3sVW&sNA56V`9w@Vr%2-FM>g(Jh7*o!?Wa1l z)Hy`*csMLo&Ycaox(?|bxH8#xl%dsDBHysUW*E7(+#fV7DA&p>D(Z|S9#G)@e|0i+ zofa(k_$`x`n9lQ$p+&@u_lL;{_yo_E@*=oRJBE%nW2KH;(j7uzrL-9Xk|%mzNT&1 zDpJPG)%ff<6WO66juMp5jV76)++`w35gtih7A=U->x&VpQN{RStB|=!Prceo5nNku z630eeibGzme_}^`I~nb|l42vEskZuHFgR8b@pQ%luQ}>kZRY}&g{LzXbJtb|BU!J- zhDV(Tcyv?=YMNb>QO+}JW5`>UOuclsW-Hq)BQVZ{#umrfnh|L=+%Q!T_z^E5tI%O0 zk=Xd`Y*1wvff%iWO13$U)nC&Rt;EL1BYNV}LL6j>!u1X#BbnRaVP5K6LvI8QEpi^z zVC(Km%zAOP><^Cc9T_?R&9ogOkUBaB%~i?YQbUn)HS)LAxKflxX&F+8J+xM=1<-{m z_1mn)o<6s~bmrWSGZxS7>MQkh_x1PoFE)<2^h$9KoSVa`^q|v9OaQz~v8#+O#F=MN zo_(eKGDLHvG*~xtBq#~2*&CVuWhYS#L5j3~pn@-zsAL!h3lVKPC90=Vb`D*3L`sjf zC-bE1i%iPoGzZ;Psk_w;AEBwVg=%==%;&w%Hg>+)+E?vEg?0%MZf2OKqtDp{$5jVn z;|G;CEKm{|eRtEVgDNN`H7rcY(o2EV0ouD&o;D(zd zH@M%l3EgOO%#2$erDZ}__Lk=OX?d@ex~4{B*z}f`-@n)zqRJ#fG3GSXqDj;+kFDHL z5o5|u`_Sq|OsfnXMIOsOEjB#7i4)@)u_5xVY2vysJgPMM%7{%SSFDAt)22|>d(O(a z5gWfM6_JQI8pBrh5w@c-Z0zVCXttaazimD{DnnNR1_KWjb6;s}ROZHQe5J8bQH|U9 zN@D|8`aM%$=KYFR-*yb6TU;F+p~DYw*diVJ8CK#@9kXj=Lz9OpAN4p5sn1j-S7!`p z_C)vXfzbTy=$q5s9fr?B!?6K+BJ;yLxll!xWror@G3E?L6o-X|gTV$)Fv;F%jU0EO z+1Sp$O75k%S>OtdGr99EOcx6chweH!5K(?7ufgL{NX#jld*{U&VW?g(PI=;viW5|9 zI#jM`v2hMTjAVNZ8hlz$#!ibpK^Qq6xFTn<|I?1cQ0a^S|8~~`of0@K)mdq)(8&31 zqLY17Q4hsbPUI6x)HUH#3{+Y)+U)S;J{Zz&n!kdSnMVq!&bjVNq0xG*Oqs zCYaHI=_=jaC<7x(6Gf`&VNpn#Vc-RWSY6a&!=u_2udo0l~J=|<0%pu`10a|-;pR;sVOVEU*eU*vZ`6;-R?F7qoO+3 z);3GvG*FSun0!ocWthCx49I_(ia#1;^ zpjXM=M}FleBBD{Xewdd&(Lej&yz-^m6AGPTWhw%*v{bX~jVUJStq!glRi7Wwaqwi} z2P~q>-bqi48Wv1E=cofaXC_8e0Fva z?~r?1En@~PLj<3yldHQdR2=UV4+?<~@n9{j4tW7c0k+kdd~_?FFJ%v|p3|pCPtem| z7`G&lg{8Tpqt$U7roe*^6Sx*Dx;91yq#A&?WZ}x`SJ-u+b&QmOGt+M@tV;7chE`5A zv5CYDBd*g@-mGsT&%TvVa}c?>OIM-E)F6B|RX3S`u1d?iT$e0QBIhj47u=TAYM2Qf z>Z(>oW!>_%F!YvtFyg?3r!h8a1f8SN!^j+c#*oIq?SeG=T+8EZ&iirFrXb`RE88}N zj473v+dl^>)#{8L8bmugtPgD4-4mJ90!hyW1lROG#=S%OWEo$CTfO;Oue(-meC zx8@mzFr&#xWCV=wPB$-*+XTM1Ql&FJHn zDq6Ohdv>}`$PYlbP*eR>Sh0z{T#|5`tBSvo`%L+8;X74J2uh>qP7R{Ik=l$DssB;z z)#0j{WDBM0w;YkpDG!bW$#CV0iel);z*2q?o(^Z>!2upSFs+8AXq#)gc-phAR~YZr zSB#^RK%WuAcr<%TmW)VtYRSCkj5W@V(eV-I5M#BS)hb->9JG_}6}5VUmbGkhtxZ4( zZS{B>rRCc|VMTZ|tUfw~G1G)O=s#EMS}mcIVe^a}rEAk)ZH-p)Y#gO)(?j2D&9iZo z8Jj_5UpCk1)iyX}rE4RjoM+=Gtxb2+5$pT6&1pyJ+Bm^yZ5*X*BWIWMYJsD4ZLo2X zmL{_~(A_jDmoIml$Jk0?x&8GVu2EPn4$VdHIEEHWo7to8J-xMrGfxv{R;ag@-OXjy zA$nk}*32#fr%DIvzz+9vb5=%Zq;#%@thDi3gSBF+?%A5=h_i`ulx{fmCiXxBD-^LT zbd;{mnIl!K($KDun31csR{eETIkIufs7mGLV6|FW69(k?b8lFxZP;PF?I|RLw%ubmwXRjRt7N{VQD^MkE~859EG_G(D!Co`yfOP>4wOsy+HW(m$dhD^|& z&YH|UccMJnUzdG|`{1SH5$fZXIlWZLwN@PKKyApN2U)KR3#wEOE#oJXVrA&`NgSU@ zCQ>Fi`6ye)Ck#eN5Rku^fJzOb{i)*Qy0fNK0JCtA?Msp_7s2YWLJTsxmMj%ln6UueM{ zaI8G;wUah_tKy@&lzBGBzS^46Q7uFI_^$z@xD!_1_f$s$1x6x7~3^6C0SZ6Kpwq|zxny=2+groGpu{Gp4a;Q0xR7z{p(Zq%f z)phZwX>)5`4$=DJ@IzWoOtQ4SISQ(ZQ)uyk4c|JZw_Ev`X!#Z1+@!`>)(Z#cadHwF zaN4>s(dCwqNuKA+lH2;U!|t#WMbco%)+O?*zw6i&^Yh8oS~Kozq^ZgnCpE6pnyC1& zKODD_->s+^DNfWwUTKhb=PoK!_wM7`rOs+?Kqdk2#v*5!uddRXnADNKy`|3KL_OrE z4iT~zGIg`sCC(_@8aYMhyQHlI@^oyJ$SkwY4|*Vmz@&dx2}eCO>MM{3_1`$X-i{q)-PK zRaqexF4VRxS!#A1*p_0I;p9V1V`a3S8XfT(h2`-1K0T(1$Y>;N7wFp%JQdnZrXh3ta_R^R+5`YET5G*`^>OXbKK5IU%f^GW zcQ|NVXQk|&eDOBGob>0=a{XkoBXHp43u}7lLm}CbuBQw`V;h}k@doE}tcMO9a!8~3 zA-`6&Aw*QU9kTMR^wyH)Cbu)W6zW6EoLpivrL;CCjl5W#2`$vSmYnfzawi}}Y;!|Z zrb=%uS)Pfe5?db4;j0|Z2_wt!t=pJoVnx0dHSo;~Gcg+Y(mE?Xv&xT&T>dz;IC@Ns z?(EvLbG?Qw-MP019xc;4>uCCaq&4xpDrXxD)J>@ICiQ6&juO=+XTJ6qDmY+90g zbjT1Io4MR2^Hyu4v;iRcL4U4;wi?SumdO~IDyPXt;c@M2Wm&P6$aq_-lX|PMY(z^1 z>dyT-MsWgisZZ{0qzQQa&ZK;u(3F{^bl1iwTFxolM-wG>=t~i)61^GQR~f_s zp@zzKO9cm#e2Y(H) z>7K>`mM_Lv74UH-lFeO<_0z#BKKg{|bGv+yqt4oy{0%LTo1rLZT{&vkF5+GJ&Haud z(iQ5g$DC9KVoc||??>Rp3mnU?-yzbE2WM3^Nj8Bcc<*JXK87UGln1%VT$~gO#_1TaRI#v~AM1H$%T`935$HjL^k%Fr4foSwl!6~Bct)N_H5JWraT~x* zr933>QruuDvhEC5F2T2zO{bl$M^I6uByx-LQ{&1Vs?1k4brO^$S$OKu%*=d76rm^^ z8Lvcj?6NdU3q8;9ku|YSCPxVql%O)G*LtdB&TgHE3VhVvJbqRM{(|zViu%kbb(6Mw zXEk8sRX&GoQks#gtc%iheG8S!M(zV2MH(vjSdX){nXHBz#qStAcLJY2jYY^; z`To)Q=^JH4;rq^Vm1n6?T=}AzZ1%Pi$UJlvip!C;C;yp(YNJQ>=_+I6CPps!eo}vv zmIxLu>FKeVnx|JzQGff^RIx)uuB)LsSW6kth+bKu5w*3<(<90wm9;~(rJ2Ut)Q$(` z%MfZanoNq#tvtQ9r0>AsyQ0mJcIG)G?PJ-39vZ>pF{hoAJeKy-&eqpy3;BWqe*-g2 zu&2^|nx`1#C)$wjsWhM8g^?L*S}^CQtI_PmIN=Omb#BD?fliz71SY*ZVyg`Y;A%9R zXEtMOSEJb+MPf^>y-%X2a1-G%v`Wpa>QRszCsXG*&>&-!Q2Rl)R?1_%Z#7_gDxXhN zE6$TEht7PZ(tMoQ(vV*y#wLHc2gk04uvLT*vE^8|vDE}^8xKrNHA`Fh*=VGSMyKl2 zEz*gqq38jYOiVjszAqgqj)X4!h%Zglu|K0$UsIDYv2`a$r^Q_7!n&-anzW?WP%R~> z5spH0b-M|eM$9~#N5?S4dB>je%SwqQ9|s^Bga$*%!BumIhw*7|$8jE`jWun=oNv#7 zdPz)t{iUxThr`R@Y*uB3iB*#w4-FL#2)JNbK?42}&nQBsRQ~_wj1QVxhyK7cs&h`7zn?Bqvk(sFe)Y)mgtJ zjZz)2kwox&$<_@h^U7m1^U{yepT5P&CMUC-_#Od60nBdTX=nw$p++5XWI`pP^~|{0 z4rQ!qKxmz3S)Q7#O$In-QqD~N-aS3u$`73D356Ubi{;TFL%wAsiEU^x_Yn`#AY!&& zB7G~AHKknMtk4yet4-&vw;Wwmyr8KMs#j$YxFkIrqCS}Ky;5IdUoFkoXEk1$T$d`8 zU^0vnsYUEe|I&9Ghv5?JnwupoRbk97n-@;JnJr>wbnG-Ec7qRwT-*#GK_!9PUKy!9(u8gEVo_G89R^E z-bYSOMO|s`OYxYSCO2+gjx4O>@G*SY!LzhN4%Gg38>*2q3GKF{u>!CEI&UseCH3S? zwarmb(tAu)P^6}RjlKdn!@&YOs&@B@Xt9gMj3`W#{zYWzwxcBwQJ%k*~SFW zUx$*0+VYeWKc0Cc!WK2#@E8jXz$*9Wv4N4B$Ac-qM2NgYp`BEC<*&<)w;yxIbW=n-$V4Ql_aZH)~sjs zLRz202P&{eqbir@DyH6xp$a5o$%ZDN1@uaj^QIOx(5$kpkqRqg#YU%NniLC>DqXSQ z;taN4G>A2_hC6+S_!%K{Q0HjM#`z?Wx)o|^oTj2}tqsjtZ{8E%P=FR8i* zaRfG&0NWKfvo97*APo|Ac%v zP^jNp6DcdnuyQa@*R4fQ!0GmG$~Tu0vv#7Hv6Ct18AooQX8n=!H&g^7e}julCQU$A zu?VI3&C?Scyk+^)fab;0Ws(A1Ij@CbFH^H%#^dI;S=02&c-}xV+FqNbH_12s6GzfPIj;XFjvvE6KQ*R78<9X%Q$eGE9jmojQO)61cg-dlu`Vl#c zWkEYJjK!`kk>eGm_%X_% zIQB0!x1^_bi3w z{ai=15(jk=z6qY*?z=Sc^hQ^tM1(s=BeIW^bkN_ogE;azNJ^NJG{ z(3R4&LB>>YmZr)ZKLn%mELNE1LZ2<=9us5ps9!1fnJt`H7U#0YXWKPmKMGB$@VJBg z@uSiZ_Cc9-V&5vHc=lA5rE&KFFegs~r5PR3wkA{#>QB_D2(?EYYKyOP>B$Y{Y*B@km=AL@(UAe>D2;gT+7>?fmt*5AjSUB2E@()JFHb3D{2C&vSgq zj0o5jc%If6nGS_Myc1TVKxvHB#$Qaj=smkO98YH~+zq;6mB{LFH=IZNO66-b z)pc^)2)&MB;)hNAE6RhDXX0g7tgd(U$=M@JsraOY^*&5+`m*k39YZsm8|}GUSYLu! zXkvHDd$zlvS?a`IDmn8)Z*$QmFyGdTN?&WJTKDzx!w$Ykz80q5!tym3?hRl<+!yLS zYQB_CRY5J#Gn&Y=CAEPGY)!-ZMCAUF=is>~one5BVFSHbTg_ql2 z4>rx3_;F~onIj;$wwQxh9kGyNmnRD-hwSK#2@*f%r&2}7DhxNw55-hsLqkGjNM1LS zr*-AI21;SLG~Ck7fg=%SO@!fD_fED(>a~1ZU+H*CQB>yRI4t zxn`lz2EBr#>Sl!?Nf%b42gIRVGw~c-8^TwfkbBsVtUDSTWwy&oU9?YA15m+g0#)^-N%(K$kjG42SGMh41W^MfE z+8}~lUFhpQ<<>;gu2h?-WfX?n-SoDVDx#9QEN9I&BQ}DVFDWw&vl~WN3l*Y~8>YKT z90<+NMf%3U8lH`q!$6x*Rpet#|u^y-sX9v{z<+zBF z@_f*?Ia6Xf621pX@$GvsMQr1d>mx!7{9uiQQj+h%CHS^|BiM1+*4SrQYp)!lL1!6( zH7)MoGa0eWleQT>-JI!TPdIiH9dWCIjlFPLADX}lDu(;cQ4ATeE8rrBhbFpCdxN%) z0)lIcQA!&_SL<4N&kGSnR%Fjr$k8DymX_%u1|2`LB%EE0Dgzr;Whqx(S2SYR0xXeC zRPubg%D}|52G5c7EaU8&n&EaSqk>l}vp!uJD1&_b7Z&Ad`!@QBR=w1uX|IwlTuM

SMO+kwh!@JkqCLXfD9!q7BXH*n%F;afJSG)oz3*&yHAo&Ty2O(r1E` z0w^({Pzg0b+gcD&dM19}Shkcarp*L>w~}F6vEq3#Bv&eibVWO-F~7V;^E>jk!L-%G zH+5=#lXztgr%lT<=6L&nyffPMMS6&gGyT=~Tw3^oU{1Kw8)CHrdqCE5E3rxOrHWOl ztu77QCs1lXWNgI{pyEP$LVO zs-^n3^I>emVVP#&?TIivPgELg(YMFs`i&)OX?=peOBIoi(r2Hnd@)XNubM82N_jJHoz;)LUO)0?}PaD`+WHhbPohjzZIW{!{HvqJSBxoZ)f8^KS*cc%btNw@^KT9L(sNtBlpjQ zR-Ve|N5ZgydsI^<&Wqym7LG3*kDF;u9yaMtjY>SKhJ3F2a?3c*FEVu|D~ALXQ zXQs}m_%o5y3{3e_moY9YAvIczZ}nWDEz))zc!ve==%Zx&cetzy{5f@e8?VAMvEEid zifT*=ad(rMQjLLaf4PCa&qgtq09!YtPP8^0+~@9sjjCmkw$^BVRxBlRutuIGQ*yeS z8J(1>=I0M>x-kHftx}Cq)cwP>Je%CCrgu`yY(%nrSJ#Vg(3Y{cuO`1sYq`GEVUcl! z#$p`3gIir`#h8D`^Mr~sO54LDG)JzO7$Egl8pHCCJkv=cTcy`f85Ps2pYwK2+}4t; zj|KHwy;aA7({m85RhFUq3>}3$UY;kLfpHIXPth74H18z>QBu@LHfYDzgxi5n3XWA) zVR%xf6aURL>p}gms2YSaclA=uD5l-AgP-`gSj;*OXBv&ztSQw28gA>(Xr<#RU z5Ljfi5i44uYh%Xhh+U?!xq*>V?-ECPWW9G9C+`h+6pG6&$@MImR;=Q&U;@- zL^=R3$c$}XxXxpA#HZ9ouh0xwQoHB&!<(Eh}fT7;yecy5FnoO@du!1mo{X$_|DuqfutC*|p_WxkEoShftu z=7#5x>IyY3Gpc4ydkA;6qNmR$)Fu6+xHMx_WQ#gT>`y0(c%pyBuTjjDH zSH^)uCK|`a)LU9Tl(BMkh{HI2&I3!;!zG-BTr0b4H(3?(CHCPE>T6JrXMK6lAOEN&k4UkwKlZ!CQNdaZZ7&^=-jj3NfN7zp`4HS z>8v)#N6Wf}1+w6$9;;$Syrk`z(o0*zOn#D{C63l96Qd`YL$Nt+7_vP0xG!Fta5foG ziDXoqQ*Nut=2uqhT5(;?v+}f|nOBsEp3HLDfXPg0S?u&l=ti=k#q=>tFUU58jEZ`~ zw!u1tsIeEaN~{xWGWSw!T46TSP{rXXX)K&Tg^ED$)0E~8Rq4Bm=DiZCA%OxLMl1Bj z7%ft}UAr$eEQ~TEEG5M=+T<9&(v8%vny`14P)^LII;sV4_f#5sQ&2<^i0TuzVR=qS z!Cbi?IO5io`^?a(W?G*#>tpu}u(g(XznpeW(Ni2q4a?hDsW?)`3}ogqdPfR7j!h3j zELYA%bfA z$`x9qin|o*xdAF{XH`BUHiA~OS*kWmc=Dy!08RXOCZQB)Ju4c0#`se1G2Kaxh{A}P z*z?(>?d5!kfV?~AD2;eUI~!kVY$#Pw#Uf8)kB2zLcqI0CLvp?^MpYV*&DkAeD~(|< zmfK$Xd-_7MRTwUZR9aP;EMc{6B2zjUy|JW>$~E(q#)e8O*Tz>G8ykZ#IgZA#IWH)X zknLy;yMYqGmw8IGn%Q3Am*6r8wU+=;_6VLayVlk2iW8y zJS@+VDPQv{Yw>xO!75I#4ZL|8#m4kxgsG*-9+Oc2;UOc>Q1J*(g=Z==(i9ju42lgu z;_$;;54N|tvCryYA%{ac6uh#w!rRXp)%sRW2o9U_+7u)W6D-Xay9t_yH^LP!D|RKc ztHoVl(}Bs#WR=HE^Z(v;tp8j8nyLPob;7J;`TynnmG%+hs81HeIiD(s^&1P~;J%{R z{k24l+?t4saP9eHDz-n9ij{!)HhzCS5syN5-~2@E-;;=0XCz`1a96!3jvp_IJBLzn z)(29t#yc;ohEzwa+1L zz_vRRF&`LRn}}0ZC*lTR=|m##1}<5Xh#jp&9D?hMKm-3z*^!7Xk0s*jpC@9!M-#CV zVVv`eL_7`oVZX)|vQr*U#5!E>{Z%4P1@}JaulgnQz*&&ZfbDtU-vr*R+Y>Q;VI}SpQ#% z82Wf3`XQT&|F4JaG5p>Q{Q5B|utiFg>e3gJwFeAWhp4XlOlnF#lmPbFd=a4*8& z3E4iMPQ>L1>rTYwhFcK!=L_Pf>kHzh&lSXzTNAMX|Bu{I5ZeceqIa+;4ut+9;PG!J zV&fMRaTIXcjfr^vD~Y%mxaX^hSO%Q^eZ=#25r;1o#ByLBe9riCLF|CPJ0RbH|M!Q@ZeR%f%U)I#XKg{fdJ^^V%gC=EApO5X z-2xUuw*9w=^S&qpaL&R1`{Vy7zLba)pGw8?H=}&_%tIO}E?#k_A7#L?RdV%;|j;u&D*_JVi_SbiI>!2E9% z#BIQ};Or0mo;wQSQs5TYO~dtqvr+CBqi&$CO~L=W{~2+_?+bA~?0W_A;Kz|)cfmh! z&36joX@>DvXd`lE^H`gK9vzcdwBEK0=eAEG`ZPN$uj zibozVh!=i=GTVVNd8{CMe_0SKalHfA>mXYR{eizizw{gAC9d=Tfcm==?H_m+zi)xx zEw?3N<;GMz2F&^`%J(yf@6!db-{+CnPZq>opH9Vz*QH`NeC&qo^4}Lk`a9&=GX*h@ z>ke?2KT#0)246jh!xqHhd4ze{1L%jQCSvRKL~MQ&+Ry9Ieu0AzMPG4n zBCdHm^6QX9JoRR@?PJi#;P<_Ufp--8ptmIAwjq~f4A6-Ccs$kVr{;^sG_&A%1* zis`91bt=l@aQqMXEXb!qp1v&=U2noYSsa_px^)k8pkDxJ0bR^-la=j{m3s5%-D{5Px9hiHSG}*D3#qe&Rv&G3W;u zpM*3(zWMk>oD3Xz3i9nAaIe7id|>l?AqVb1IT7hM5vN(;ya)M->qWROhkQ4#Q|F*> zJ`Hxj^!K6tRc-kpjg??pd}|L1Q(guw4Beu#VF3e@Ea?)%`Z#`QMnt^sE~drw zL|*&|`G@O~l=ly#%zv7Sp+^v26?ynjDwdv$`{z$m@x(at62H#}4*N0c%Fi&K*bW=u z|FQQr@RnE8|M>nqol>cgBq2$XWUA>wl4kCmX>QF^bMH)3(&^4Q=gvKK=bYO)_s&C- zBuT<}N)kenrzA;8LXsp&LI?>VBuSF`z1P~$`?EiH=KK5ozOVo9|9k!B_4>>`d#$zC ze%^cSwbx$zYWn@NTx@kR`t2ID_XN^93H=;#-q}SxcF@mz=$lCM1n_zoY21!9ZUODt zh|>i9L-;#+8+akz-4ORG#K{qV5bDE>@k4c>wG6 zm8TWrvOhtV_z>3dC571Oi)btG*y;0-=}@No?#{(dzknQzZ}-=te$POkJPrN$bj)k` zu6iBviNEvl_c7%0Oq69KXx;|e^!3Qw&vVfOkKOV25%B1J81u|WFo)nf^%C@TglB&g z^9z1|1b^@RUM5b&ci%1OZ}_hGKE^l3%&xa#JPu$!{|&~|?=asyf%QxjVq5&a5x*Y= z{E**bPQMrPJHEAmp9}g+0o(Qum}`zK#CceEc7$v*>pt|4-y?m%ufyMufX^j>H6O&7 z_#5Vq6nq~*z2lp}@ALnPISO!r@4@YJ@xaq^aT@x=_%jP}+f#D!$)}?J5Z)2+RS2Jj zzlQ@h1{v>H;Caupuukrri}TSB4&J2@H$Mw~?-}TOJ3(&4-z%R9dVIGdJot2syBz?- z-#fwoB*fhYG}rzE^Ax^a!RMkKG3VgB47@fXd_3p}!Skk_A;03=eIMi*-}WzsoOxg& zE_hiXK7BCOl!LH0!ME)pg}7$FLfkW}5V!1Gh?V#r1#J_wXX5Yu2cSOoM_->^h`FHK z4e=*HyANOogU^i(lwld>ORO{39f>j>g)xC|#USRuy$kV9tY2F_2YaYp3-QQv!S8wK zbNJRHJcaN>z@35b_{F)nZxO}=zIQIgoV5h=@zLnthZbUwh4_tpJb*Rr41C)jfVmCd z^?MZJIiQ_-5%}VJ+ic8<_;UOfh`Z$g=H|UnFUb2v;PE@a?geZuzVQRG9=@ayXQBKn zk^i5+2;b-41A{|yz`#e>%AfullbnOh5h04Q5Vk8!gIfvxp-S`efey@8mZ>-gs5_bq#2FM#h()WwZxuZ!nH-Z%p3z8w7m-(IMT ztEoKmuy)|vxDWaqzUy|!-16c=>t z)5qkZhPnL0-y*M*DEA4uSotN)$=9N7@NKgVWEXsMx5eCvzb8H!WB(e=D_=w#Z;kyd zzK6C){=SU9fxmYk?usWMzdeR|@JX2WK=K?6J@NN8q`T@VnA;Z>;#Ww2KGK{0TCCl-q~dJsUoQK8 zD#pH-ifg|E`TADK!Muj^9a`UE73>(iTq|*#}IxDf1ir)DzwMDuZDc`59H?|>^mMrSuRK4 z#@{>snu-%YiFx!1?8OE4+K)q~!3QK>Ck<1~T!( z*w2C2#=jwLgl8d-rvi3K3cUh&-JD?Ex(a(5@Hr5$>+$mbD9U+E;UN?hi4>F-|Z4 z2J(n+_iy6w2xQSWpiS`IeJ1+_45d<@(!Z$#alg*oJG z^j~}r0>1H*RP6jF%yEeK@aw_jE#M8<2SGavv=;$)^GC2x1+NqFcUBAe!}q%}ginL~ zfN`+#6zq$kA3LFsIRRhZ!H1>gFo^-2pWB5$rgu+c%(Yk^ke5MqLkM?OlOB z1o*8hq2DpLh($A-+$etk*AttPlDH_seFzdPQEvGjhVfpK~+eha`C+<~!- z?+U>0f{tONiFIcHYd5|tHh|wNvF03(euTMl(LkSJ-80z@n0zOi5TDL2RY{K#iN)%yO7DyzaK+iZ(@yDa4Tdve2X7N{h@yy zdItI;`q~xvdoKRwx)R9l*^;G=50(0oHg^*Y9 zJ%I4S1&D`v{~FB4tB!>XgFMVe8|{1yexHnSGJ&$-y9o92E41s<>0I1|Z%2%=oo>ba ziZYE17GiQG3-&?kW4E0pV zUI?GP4a$vgM|_8Fg*-k9b%O6o{N7{hLR^fm_hh7pIAhx)|M-0m{%#4L8=qE)o$<}R z6Z61rSlhw#-YwBq=xcj?2lD7m7`w>trTD$$2T&&;L>PZBz~5Pa!`}8!&^axFOuG;= zKEC7e`=0BN|1B_QfzROrV|x_sd?NZv9sLU5m4J;ujxqQjj5GW_9e?*LATJ~s4~Vxd z!l&ZzH97KzHStjVz4YJcANX$mBjou%U`|gUcj5QO8u~Nh?u@^;VGTX*1n59+#{Bs` zm`o%t{AszJ7n=l{WfH`uXlM1o+WXOj2yA}R+?m+o}g#E#*AZs0mzVH)_rJtf~ zKStT_#Fzkn_a9`C$abl@JucVz)t46wuS z_na*tE93hK_~oFvXgj1agncr;-Vvy8jJLb-cW=ONe|jMvhn(A71z8aD%8ffBuiIj7 z2mIcxg?Nd$BfmZWF4RT0=HJP`o^O$UK%6dKE6xySi8qSV#OuY1^rPa#;!^Q3?AI<4 zUlCsxpGU9!s<>W!U0fx;A-*ZTD6SPh7Ozh0=?UqobTmCN-67qW{z3dv{7Kv|?iGI) ze-XbGzZ1U~_lduW2gL*8fh14TXu()RH3EBAJ`-mCw!($q&e1p1&kNIDc7w z7zFwk<#X~``QG_K`GNU9`QiDC^MQOY-#XbY**4iGd2;gnV%Oq%#ZJY}#j}d%6wfW5 zSsaxw%!l$L^QHNcd~v=gKRVwseO~f{?+R9-OY9hdKx@0#Bxz9;SwJLKEvPs^W*MtxGA~zN@N`jnbo$D4ae7#~Z~D@7X}TnxlkS-wCSEQM7B7QV;vjLTI7GZc93Wm+98w%y z98|o#ctvq&abR&vdUN`t^n2-T>83BMwu1&k?t3*$%6UT^CMPDozT`?-! zq6zuAB~BDDzYwd$YB44n;v}(B94}UgwPI47B2Ew+#H+=GSRBgLX(adA}fne@%#yU97?P2$t(PsA6}tI{8cZ;4yQ zx5cl;55=A0Tj^WGx#H*9>%?2ddE)KqYs5FxEkq$|;>ltg@y7K0bl>FN>ATXGiXWyI zr1WeVrk_YJOm9yw zN-s&@Cf+43Pv0$mCVnYyf}ZBL>AmTyqET#9+q(AL+E%qE)wZl{U)!SgMDgsJs6DH; zORZKbYR{}?wVi5t?J2dVr^8~Ym?P$jA#sEl6br>sSnU>zMdFp>XfaPL5l4yzVwspP z{+2$J{ylv#{b%~u^dISe(kIf#(jC*irK}MS&`MU zt+Fh8Qnq_-|JqAyvuZD?&91$mwny#2+TOK&YWvn+RNJd|K<&k~7uNQy{kXWZxTDxE z|3UG+;=9Go#Vy6Q`7etbitiNPFAlEFtqs+VtSzh^QJY^Is4c1;RU53mqPC>AxOQ}H zL2X{`(Av`4%WHFLhu03N9aei;?Ul7ZWWUeu%TB1RtnDFQAfA?Ompwh(K6`4mLpCeh zJ$rukg6t*P-r1hnbF*h>&&hVno|o;Ky*PVm_RQ>s*~gNvBv&L~NJ34D+YqLAE6SI1DLUviUD!V~i=&Gr#nR##`A_q&rmru~EZ$I@nZ7Q4 zTk-nzjP$W=N3m0WT5(qT#^UVaO~rZXTheDhL|joEQ|y?(u=q}LMDd1nUVedik2qhv zSG-S*730O~qFvmQUzdL+|5EjC`3?E^^Ka)j<~Qd*$bXn$pMNdCHveY+jr^wk zdwHw)wD_#}l(<5CMqDmFDXtS=5?>Qv5Lb&&h|h^@M3($pB*~VENS>6uB0n_WH{UOR zWxjv@>|&Qia#XS~IXYRIEK5d` zmB}&5K(ZnkN{&blN){)FCad!4d@6sQ*iF1He_HL0`Rns~-pl*>YxC3cx95}j8}c1% zBl$b>Q}grkx8!fj-<+?_KbpKM-#LFuzC~Wk_sAd5o}9lh-#ULz{w#w%uFQXz z{78Jg*gAc33K^t$H2-)0`}}vgD3T&8p2#21w@aUr{<*lS_gM;@;x7#qWyW z7xxsuDc)Rsp?E>MOFB0_GW~3^EPYXWb22;qe7;}0f4WaPkj_dkFZN1boDQdh>E7wQ z^nmo>^uY9>^kwN!(;ufdrQb>KN`I1mJ8h+_)3LOlZb*+6&lb-TJBxp3k7jR5e=5E& zZV}%VH;V6wo5k(oy7b!O`t+^o73p2#SK?FYR^mxwOYz?HJ?Tf%uN3c3FGzRJ-XT7k zelGn``oVOs>_yq#;*06k>HE_C#J=Lg>9Iw#*tPbY+RnAV6n`z=QM|L*OALqyQYfdh zf2TKyyNWHcJUghiU+qW54~tuiUley2KQC@8ZZE!3e6#pm@sZ-g#l^)(i%W`+6(1@- zP<*hswD?4ES@Fr@K2v65dkWZPzYWcz3PW&38kWY5a>$u3DovgO&Uv*WW@WyfU4Wh=5{vz6I7*;}#?WS`1@ zoP8&Id-ksEo!QCR>$3M`@5?@#oszA|I@yo1sqE(Ls(d)RCHr3XnrwDvVEagTU<{(@qU;w{D5>6g>X^NX_&WyjT)*N&;3TCAvz)P`%H z%s-WXFk4rwFJ1s8$|=Q*#5d9_i)R$0#ZR&;@=xTKP8H;C<%rzblkPf4DZ zJT=)dd1roP@wxob;@`!8ipPt`iVN}&=9lIl&+Ek{`T6;K^An1vil>R6<-f`A%J0s9 znctiLJpV=htNfn)*ZGC{hx3c`cjxcP|5bb-|7iY^{A2lv#XpLNiU*6o7ym3il)o>( zD1U#Rh%_P%*U}xc4~Pp(GVuq6D-~ZXE~}J`KkmuKuYz=ZJmll2RZ7UeQ8Myh#ZJis z$zP(9@}uH^MpoV`A}t@0Yk7ITBQY<4%$#~s^9z#~B`;21n(PVrdFNzS@{(kqWUtMV zr1xf7`qA|G|6}Fp4axhGspQn;;^ZyKhmz4`RkAi|CKo1eO-@h7k~bx9PtHyn$?K92 zCLd1DNj{LAl)N`NF*zZ5PxAictYmf4N#31IBp*rMmAoT)TXJsl+T^rkGC42lCa+2A zNjqsJ8aRT1_}TEsEl8vyCh{f|C&lk|F=fg|N9Km{!ZzRXtb_)SJTf_wgdwI}!u z?+Tus{676d`s!r!cLz`1?ES%?Qh$eVzu6-^lG zGk3?Z&1UQwcG`?x!{Y4!j(x+T($3*o$#au`WqSAUykmHd?VC3j^%GkcRg#pdl!{*}u8 z$@|5ocPPgfdXMtQ(k|uH^tI_>g}+xhExkxQuhuF29m~S3yk|M7m?%#EZ`-+iE!BG$ z*cWb?GX%F3^#StQMWNnmJl_LH9PV#^bh`wY)1l`A)^&Sud@Ca^?@ZF&plP^@P!Rn@ zxWuba_`JKJooF|4c}p=xPNwOFKxW?G&}kge>X5^DP|xjlC+c*Ugq(&_m|rX;cMlWr z_c_&GJ>8vVw+VRS+DG&4P8$I%Yy?Yr1pGU9;Yt9$oeeE{%+{*F@NlOY`h(I8$nmbvWL2l-vgdHu%dq|OB{o6~h5Qy1(!$AVUvxTVs`m|oSm zpfG>ST9e#XoPa9g069&i3*GerT>V2*7Jvqp*Z)JHiV+eIwxQ*`eMc9X~q2L3*Kmx-r$IvN>?|ijD)Z-BknS zE3Zzxq5cXyi_ml+I#)6i!?8>N7|jIWK_&okGSQltpi)s{^i!ooKS4)O^7ZQ16<#5w z%V@p*_--6XEC7z#sU0aE0javcYAvQI7evpPT8jC&HX2Z@z-|1|H9RFSAC)U&iJ$Y< zOv1&FVt_FDR$tofFX>Fsq-av2>-=lvpdu!XI$dn*qd4Q}gA=$l38x`lld*;x6^gG8 za(i-Y`R0&8%b-Q8_fmb9><^8m#?c8m<{bJCF8NAS$m*z0cU^~phJ>+i&FK#iIet1o znyf0o((Sp-G)Jostr=ZCN4rCpi4$W~!-NO6?^tn4Rj)1)2ZQsTN-`L#Vao&K4c&LG z2cYGnABR%atM0+cAbJGkzmO?;)AWTAriZ-muzB0*TbQ)U7vE&M`KXOT!*YW|IN6${52jZ#6=5=?^} zx-=>nrJ~MQEpj09T@!PKQ zx9=^=xO`qTI@K6AY0YmTq#_&1g*eFo$t)B?b%TMDClx@tfodAYniNg~ItJ*chT1e3 zMec&A;Rd@6jp?Ef(zD_+3yVpvqL+?B4oeyh@@h@90Oc&^1WRMOt9zqsMx^8+X${0k zn*_%*>Z|c2_y%;@^$XQ{>=U&5?5~X6%E%dnqd|g!`#_ZubOB_QI+S{J9V9cLsPN>m ziWcq7B=fQqOU~sfiFHSj)J}r}Kv_i|J6)T+0lAzufev#O~a zY^kpqIvJRpXgAyZ4N>nl%)SAwwnz8gB2}?S@4$G2LZV00NmYXOHur!#j=t zDv)25kQ!(z@qxpmYeNY3A-(PxrVU*NbYb>up~EU?lu7we9M6;M8!h(!#o|C+7g$4i z&oMg9Y7sNrGSKYXJg{-3L5UhB$jLy72>~NabgoxHbOjTd2jym(a?2sAc5)-4f><`- ze2wRN7Ll1|$z-de`i>Viot~jcOB1r&F2#|ha-WhxW+=~}=#EZ7MplpgAQ}T`f{K8G zXvn^rY-KEj7mbpbZG-T4qJ#9LG>IS*Nuk+Z=wK6)N+?S!!Y$aCD6D5B>cDe4cu?&l4#-@3FM5|N{p!{wM>LYv)#bIETi&piiegzc) ze}d!Gm{cJs7;+((c~YpDdJRElWNQR=F7*0mdkW4ZEem>5ntF1`$A|MmqZs*^Fc}|9 z1tBktB;!#BGAyQIc|j2+0nc!X7QjWLtJ@7TLIU{6D5*OfpnS)m45D3#rIZoEupCNZ zT}WCN7nWL5#N$cm!q!?=rmBp(5g0WS$Hm>b6H*jWvC(z9qfXPqRn>Zgz)gMpCzxyLlZPhL}nf z#bMNXt4H<2A(>zPxeQ%G9>s7F)04{vL642vOH0zOWQg+-cYRimtf^xYCj@=^5~MYT zm!c^jG8+9UlM*CKPU+)CHxqrh9{})96Sn59rq9!b&Wu?!+NU*_h)^guo{w44Z8w*~ z*QX-W265H~3u4q<+Yh$<)(gS5uXuvX4i&46_falos`3!B#I@%MOs0cf;5elhEo$Qc% zAv92!7(mmLgRNE5t4ZGH)|T$5kuh>K&AD*gQ*#)Krxk6uo}_w{C$5stg&X~p{9)+L zSa9G-i#9&;w3b=%(V4Rxc?U!)ppN3?nh9BlH=7ijS9KD2Gy^V{fnvQtYP9gt6lJMx z&J3vp(HlE1p={VFkC6?0;;EPp^vEkPZ9o)&BpxZ1DFD?V0Cb3nH#r?jREbAOVJVcT zm;g}6Bx%X=cYtAOfKsxuWk7%k9k1<{sa%FsECj+B=t59Tn%*vjulk%-{qDpx%K#eE zq%~3pc)`?xzMV}KNY7;|h^If-be-tD`Lo;*?DjwdC#e=V)LBZWaOekRn47wyn~BH5 z{&;(gdW*`AiUI|0ue~0RkWRHHCul+q&{c>fv?BSmOS|h>lJ%i@@(bqz@{}yV#B&6l z-4y(oma{t2n{HK9&`RFV8upM6-P}PIaJZ>8NfC$DQdJ-28d}_1%^q`YN^YItuoW7a zwN6_G)f1_gWkH3MYht837ygZHim@t@b+Lp`A?(SE49jsKf2*Yq%elGL6;eqaDnx1!-ly8|P+vC%&ABd|)S!3^(t|S0gDmfauz4f(W9H2{ zYT3N`!}Wzj!y`*amN_vxxkhaln<6F$<%m3!4`OP}@061Avo5Na%i+6(D2|FiGT!4TTnWNl`MvrXX(p z4-MCk7_g+I?D0c5pLYOw?^&t}T$po0q*5H})2wP~%pO7EWq1K5C`)4`SVe;zTckd* zR5*+QCEXeTn|tX{yD?5j3!{9{SO${0pyf>hq+iX{Gb)<5)M*qTYyp7%wp69q)lXVL)1#m$wTlWx};xAxy#Zoodl@}!Skf)K2$zU1Zb(}43Ykz8pK^r(kb@L zNuGNV3s&N~Cj%0KMcitPCCGbxSjKEOyj?lbP50~id`t)Rk*7+IDDcdN4dK8GPhFiPHk-u7+e^pAbGY> z23p%d7svL2Bv?voBo~ERK^X#wT51C*pRJaO`Dh{_<)uR&VFVEtw#ZB&nibjSTu$nQ z!39xG(+vn%5(J#R$J*tzz{+768#_+!?sx}D7q{*{wG)Si5Ad+;Ma`m)m=tdq8WZqj z)+sAklN+fDrruXFS!p>h?-<~idG5!6R%22`GGTNA_szps9`zJI=YxlNU?gn|Nf8vr zzT9KtwIK`Z7jg`+hXR@BkqopS&k%tl83M?Ew8!wFC$+R>t7?!yvSw|*2_|dShf!7e z5R}jdIFFQhsUoXH*gY9{Y9Qy(?%sqcCk&zWMv{!I>kQ>OC=5VL?F^Cf zeVu;O#O=bCccwIyt_pFtmB(Yeno?M=yAq%V2@{j)2l3+o28No0RahG4(}XQgDF;Ry z<6K7sAw7XqXe!Ms@zDs<#D-xyoF?rNmzxOLf}`D-qjn=4S;15uC6&2dq9~?hvYI~% zC;@X}hD^G~#Gvx6gM!;%HPBt12T{ z^p1I~@{QwWVvMK(V|kr|r55U-l8LyM<7>clM3MSu}TGI0DA$ zqGBVj@HMswvG!L1=l2_bIdawK2?H5eu5! zK>35p14bfVdUE%!31wGv2qZwO_`yyPKkE#g zDb7K$CdLZy8Og|1}90G z28Qd}UeH~uq5bPLgW+cmCJYA%bZX`+O|c~GMI%TcUE^`t9EIf2r_}L@i-~ex(reP8 zRKTP(D^MmRu-sn>r_u@7=t;%_L*h`wOV2Sr(rCPN&JiJ)H+g&7u{m-WNYv08+%3YL z(lSCGsk`w05N)kUtaMxSNL-qCsH8}&a|EvfQ#jvsMY;O2C~z2;INWIjYkJrf)O}AU zF>}>jr?<>I70}R-H;3s^)}28iK=;FCeq^*gv4$>+IcXa4!-26@Lv>uMuh&=59f&ql zL0me@>%o)-jb?hAhM^Mq8lgi5kTg#5uVGqACV(6JLUlV)X&p&lxfCXhSg_QYYkdyx zXK~fnc|BvBEhJp$BU)u(36p5PKWjR>516-J>!f{n9BPV7_oQW^)QxE~vB1!pW)3$D zBV|l4M%5y(f-Y;}(pw+ee_a?|6#7dO;xtr@>XHYQX+G|OPG~J;eSUKYhh5+*PY{?u z9Xq?fS=m)TV&2HS#WaK14PkqfIuNxWG-M5>C8Ho$q%zr~u&O;8HZnN5q1RqLPW_$2 zWGjTDma#UOV-P^8(D)~W@iE-_(-@De$M^Q;Pftv^F(g!NAeqMo7+G}|blHB00IJ%$ zorSp8u5+;h5``S8I1th8LaHQrVTLkbL$Xe6n5GzWYy{JIrtZ(j{UBP*0kEXBod7`H z9K$blj#y`nS*b;J4_q{Qa?9ztoB{?#&@0R^!39XxgW1e`0Lm{KjJcE$`l>WzG3)g7 zwPWbkCVQMsDg#brbmp+d&@Dhe{T3jI*8%{PTL3_G*iIAY1Q=zwH1ACMxCf6u-s|HM zL~9*QT=Q4M4utZsV$>hBD^&&H1>QNsBYHH_>%VhwKNMHk8q;g0X@6%xFy)x)b@6Vv zBEcHm#>uiDoYN-@V*{kd^p)HUN2#5)jsDobWpM*WX@Us_0Mv7dWWw2m-L}<}R`B*e(py!z5W51Pqky5kNU79NW;VwN36hEtF(k zC&cb@Fso{QthmJE<`%$$mO*1Cq7zXk+@~$G6Q&8HrBfj(U!Dv*_sT#l;mSr9eS{b= zjF+XU-lzenAh}y6Rdsw-T}M-*fe*vs z|H;#nHl7R7-m!iaE4<}YV+EOStE0t)lQIZhlB*s8HTH))dm3*`{2e_cGv#}x7Jkgu5?$>Po z&BKJO&`Chz>{H>{Y@1ipKHYtz*cz&up2D&Zp-%ebm8H|*4kHoj$a{!;wFj!R8yYrP zQf$$%utS%tf|m;**a?QiMMK7Po-|E_E%i8@DhxGgLX&cyPEnzx1Y3!&L_`upfyhyi zx;wlLZSQk$lk} za0%BIYJk>PZdEL#EF^;?fw5a@TmKZCbpz&EpU+qfF)S6ei;W0uq)!?m${O>)@fnX{ z3TXEm3TLEo?GZ!4r2`HfkiNsb5DS=mk2((avw+4%N-TjZazIM{5iDw?JGrLYr#SLL zxGsvZNKm8^?;8pu&%sQ91#_wXXwo!fM>i=>_g2t(p8nhB8Wzh^vEiuRKL8EStkgK`aB2n?fqa6Nf{vz0Q4Upool6_ zMxgFVN`*?A(J^v^V{pEC!Lmd!DzM?7L9Jlc2bsxw+kX<$x@R`D=2$gBEto|v5) z2V5R*ovTEO@XGIp?ZNWRUTiXjfp0V z;4aKF0blN%(!p8-q!IgM%tn?Up3Q9lK`zli~CglX2d zA@>Td?9-xXoIYW8Y03=?(F1TyqwfIFIn^R+7*J&pEl|^w2FijPgDpe8(@3T*uG)>x zuZe@a@F;Kd2Vs&*T^cB%rW_p__k1d49}a)ndnOTkm&p*uJtLmU$aj>v38mnDnrNiW z6Ga6nTDjf9I3$xT30V%OW6kawKZHef)r6Y6(+^rhH zK`NViO55HM$N@hGe2BQdE);4lc3E16>S4Uv>9bIvS0d(EOCIy&PrD!^H ztB0<_(4I{9O`iZ`p^|_xUK`SKJ9Eu}f)XM>0!{`YC9s2EAI3|#1Hx_)2XYQzsE>=W zv;~x1TQ)7OO~KI4ZEUHk;442iO$+o&Ai*@Wq1JYi^59+!(wdJ<22JTV3POI1&?Ypv zOJs9~bGZqUvij^z#L0t1n0rDthH504Ez**sXVZ-no`a;nAYy}Ipb#^!m)-CjXGLDMvQGbFOG2g4TFkF zBN-Sh$=<_Q?{S{Ukw}$98Y)Qo8le1P5}FP8X~o6Xf-eFXAi627S1z2kcw}Lurf?}= zVxMNkIt)$Mik=EU69FgeqtGD?1}5IL$SKKOS(94@gPhD|YS7a_^`-5yk=Zg*?yo6H z7pF#D2-eNulA@E0vK?l?0)4C^VK$ZysF!zOAWC->k+My-t~eMa0MCyyb&ccQnS@i% zKm~Jr{@veq)~v;&oi5a8-45L2Q=$^FsNKN469G?5w9DW)4+m=dvf@81Su6KzHeEui zU9_T95Dv+r%N#0r9bO>iI=~dv`4+)oLuWEbn9xqPOo0PpZy#x5ZYknL+~@GE~4Q)F=uU{*AFv%?`Jwy-)Ws_T%LP#r3vcGYL2 zWUNsQKAFT~J1V`AQm)9hn(stoad71!MxYGvMEqldGqRCWCUh;#9O`ugpnM5w3}>vXVxk3NQCns17qz`3PEbP>*YlkZCFu~S zPdeX}%P0ZaGF20Co?NrT5*wyq6P7s2vnY*|DB;h~9`m)fH8v(zt9XWhs7MSLY3SBd zyBU=f!KICxQ$5jMqa)~zxDHEAZDl)cTI=PmVDxN&ki&Q&WzX4sPM;7BY?`W5| zJpQ6aAZtt~QHem=_dPHQhS_0^`XCQ_3VbM*03%&)1liklZOkK5g+ZeRmCYLgcpad8 zDmzpsal%54;iZHg--Mgbpm!ve42?y|B$85a_(Pap6=4okkl8SCe87fLkUG)o^39<5&$kn2~N(-2oF; zC%kWUCz>sIdGb3Jxo}10Re-~ik8~q-N^3MKugoG^rz2gi(=v6ms(GatgBng16;>g` zRPAbLxfP|&Kjm=qHBGv90~*}kjMJ?oCOm)8ZBBIhJ{xXPa@xR<_g+e~s$p0z)lZg~ zm>lNtGDoZcwN^lZv7u2Xg>ZV^fRgq3SVd}mE{z9%Lx$)a3q-3ckrTPrFhFV`dvhXI zu2LF~1t5dz_z?;kX^>|i=)p1LD#mv)Oj--!sHg!oE<`LI814#A^SP=nHML7Rd7P_f zdl+Gt0Cl+2g*g~BX)XCk5+$e(MTgmWK1~Z;oYZ^?8H?s!O%e;^hk$ zGE3%wT5XVk&PSX!Qec@$l}3saT0)kZX~)sOwwmk=RTc?7855bZ+8`ns>f8M>0q1pC zUgw{5zABGkd7nTqlnNfjh6jd*IJ1DLIwH$dwUQ8>54GDDi)%n_?aVxcpl=eY5Deg^ z_RB_PlzEUF4K`(!!2Y<=x`U*T*CD3yq!j0ig8B_6O{~JY>6A4mVM)f6Y74epWDwps zBl%Jzc!h)J7>hPJVA3TW>dS%-&{#na9D%Kv+&#l?(+-?Lr~q&`MKXaRfn}pfwu#~F z0)!TC!6;C!KE9bIt?f?BLH4!zA{?0$;wn028}mS^jR2!#b$OsGy8;vgGT#(kw>W^x zM3W|RHrynZayXjj(FjE1W_?~0mxKpk9>57Ji%U!=ax+x?#Y;) z6K+9B>Z#pUeYDxEcRM)s=++@Ukt1IwFM;PXxKSTnB@5iB2g3+?;!^Ljn|vIWljkxrS&@fYxS_n3 z99Ez;?U-n;t~aN;ELPR=n=UN7;Wh>)IvcMGgzrx^JEOeAl%d{~%AYz8sgHG`9qB#Gf7MA+gMNY`=#a?D+WOc8ygyU+3DEA0Lym%XYc*g9*6ojtV>O6DN(|S_ zvP*s#B=!yjr_i1vJ39;rCK$(PR_+#z@)z-v`|l(SUJchIDKW|cV=x72P%DR^DeP$(hFXW~CAfs8I-vG3KfL;r zE$3CX5Avw7&+WI+USoA!KGuB##h2X`IV`nh0IQFZ!-TrFd!T#yvQ(&9Igm_nT$+!7 zq=%pNRrEL(m%#By30^+`4$-+$kIMZHc7Xqg0v_L@nwhx@jt zfteRROAuDZ*vOiY!4O!j-LPg43TYf%j;XAAJ#H12z5$IiA61EAl2@;Qn%uf?ftG-z zr7`^I?!H`w>pt9jKUDMJS4DlgncfvO(JYZvIjYKWd~#tO({65qSVv&_Q7?@K81KE& z0B6iR{iH}2PivbN!hEdv0;yrB&-Lj%BB&&df=Xpr?ff-IgF?y3OH+v*>mjuOREkBJ zlq_i*#FI;j1`Bu`Q9h-X%I!09uerKxLXIoW2inV)!MjTHemiV~?!_eIo5jOr@x^X-)&N~vP|euuEHoys#r)npco&Ma?v>GJw0Ww&Nh z^|0*Lxf~BNVuT)0+FX?~;PnCbDON)&Pj7>AK{?BP#Y;>BeV%}l^dR?}HV5T*Tuq7= z#kXtZYpxNj)GX^F*;wK95WMdia?-E7hH$@Zr59gAa)`wzjE|H-?m24!2uGUe^sHF2fcDdLE@9 zF%2tY27TX}p*SUEAgLV!Z>TmQL*Y3@4e-bZqY4wh~hrHAvK^TN4HjvrRm= zVG8ZQ=}n9>T)#7HQpXGc8D>o&vXKoCi8h1^%AhdVX2lVFC!mZ1eIyO2(9}c6ZiXb( zuR0`)SB#~UDO^R)7#zu)dalaN7@d_!r?&yr>339hQ0B{Lm_0HD3`QH%qj{;S zZF>i4Db-UxEw9#eM^(>DR;PT}# zhx-w@o@6ib#d3u9H#zoLy9nxzY&_$zwzuCrVCeyB-P@=X*5=!Zqb|pV5NAI}QI$@9 z1j~V!O$Q$Ahbb29#j75vcJqZr%6qChz{%@)P6pA*QE4=woM)@uNHGGcM7qO4GQC%X zO%X@RwRutO#6Wc+<9He=1*U@FCmyv9%3u&Ps}~6U83~c*g;v-<$#0)4SotcjH?krKSql37NE~3USGj1%FX3He(mSjLxt&j$VhVX z1#FL@1gQtK-SWAJK(|PFbkP+l#bS}3+ukX)Ia!ONCGSq5t~v)H53T(;G_z~VfHEc! z$mqHk`(%mNN{C+88TsTNRJ6!-H~(#1Tg|4!iS*H zqLFmymrx-*r3_~j2>lGNX;Hct!A0CC7B6_bA7y(K4Wy=DVBK* z!mAR@*C-FM{Lfo5zZ{1(J}7B4LT7Ee*X^u<-E$ zi?t){B?tNB8jnAgqhczJ0#|iMd(tx5UqjLLVmde)q}UiolaN+V)lETDVL1<=pDEu! zqYm|}N#*EEh9fYX`H$h*+%iUaDBDDj#{GjeZP-!7rW!R+tLnx$4lAc^3~OSj3h5WM zt*$ghBFXztvA#vl0oY>#HiFWffY&KK-vK^`Wt4JS!wiom#g{Q+G3b&;Y3z)Qt}4^g zH4MBs5kWu!R)#L`tfH~bi)9SUE{CN~x)Lol($#2R7)3KED^Io=B?oPlQDc>~97a1a z_zH6JQ05dPwSu@SB_m_UwR*6~UE7X!SLrT=)n9dA64BEvi8TY#af_YeWHd%ey`bAk zEYcV(M$lOPtJo5%lC2gSV_C7R;pQLGgg7>3MC`%IS*`)0hs!kb!0f5-GoU<>1k zjvh3Slo^No=t%S~uPG@bS@$1{#fV_cVk6uL+|$vO>cyOGOJLQH=O7pZQNef z6|7lNe}%cJt6H{E$htYqlq@}L?7=T!=c*1R6TcOD-BVi~^kDC30NqMKePU$`K%;(6 zr_+UT78XD!GY%ei?_f7=1%AE9lQV{M!s;TF1~bmFREth(N3EMNs=h<3D_Y0&D#rm* zXEE<}nC8(0sI3`G4}4|mfve6bkTVs;YYSYuALvft&WLSrfS~J?W|FF;0L|-!5OPgK zFgsBiFGOsRZ+egc&+B+?Y2fGwBjfG9+_AxYmW<(a%0mko0FxDHWXZf!BVY>Z);l>X zLO5FGg!4q%rsZnR-Nt5TD~HCYG*eIP^xQ?y3o{teyAYfr%@G-OIn ze`zj=bM~>03PG{<@$uw+8mb~Pbtc`U9U_JuyfanhRE}qv&r(d(n&jcEgo-Ipi$+(s z8^L50U`NPS*ROAi$bL#H5p=W=A|>BA)GflMDdN>B`NuQn;uf~nI+kIov?YQ~r?I%p z8iG=GcrqXhpF1uSc%5{TOj@9)9T7`E9oA&ykqIyHfG4z)sSKSHHElavm zc@IFMrMCbl)c}t^FeB3M0U>wlm4nss7r2M>rJT#F5mg!YD0qG z9;*yhUgkZRTR}{OERic(KV_^|beH9}7jQ2q-A}0;!k1 zKjC&8Up3WLEGY>FW6vdO$8S@$=&ER&h)TtxYZQKZ!~G~&JGzRc<>EF6zi6~?rvr+> z_ow@CxYfd*mtL|d<5zUsO?Hdp<~Fi_V0HvAXGBBem{@I4jA(r@!Z%Mg39Ef{Fuyuz zTTs95iRsK$NfgnFpb_J4i#Ro}OX77Pe@FsA_mP9Kuc2xP2754#g}4H3o5{Tou82c} z%`V6=3l<^zU0bdp8BD|Dp;L{_670KH-k zQIXQvLumK!>`?eg-8AQ=!%en67ZZrA{dp znlKWHpza<*B>^yvZ`vb;$vCKZO@P4DfLfLWu8hOtaG03aBV=U)6z73)0mGeeN|BMg z+LimTI=s?#CvcQ$($sKrW~I8329dL$0MrjxWJT)>u?i@CoQ6DDc^3h8d!%P}v^$E6 zdL15pFpQ)z<$Nj(lG+4U?V{8#riu=8s$P3NoI6dmCnxNR>*s3|=p}Zp_Tw!j-xU=l z!XxbPmD#Mu_s82~c9i;zVaYNj{SHP8Meps*SkWt;<;8Hud0{!Pkcvy0pk*?oj92o^ zwOduKi*en23Px{X8s&PHZ^4u)qZMCSOKLMP;wc;5ugvyH!~F*bN1M^M~b~dR1fW z5_WQW$~)d*AmaLdVr>kP5hvI)Mj58pu<+gycsAMapRcL=`vc%sSgR@24 zqac@e0TAw_d}zNEx!;+DYosZ=9w7;0C+`ExjJSM(o9fytgOYlxOYWs?;%m!XrQ=ZL zi9%B+Hxp2wty)w63#rP zlBWm;DVK0S90}N^Q@ZnL0tyFmm;qa<#)&S)r&wf(nIY=pZWU9>KgJY7Dk8-&kOSv3iaC^+4__ml~HT_*F~Ovt)~ertPYd;>PV7*|uwx0tJ?R8tQIW6pRiP z9A&758vi2BIf9F&lV3RK3~Lem7!!dn@5C|lM$9wk^@T&jBTGk?m12ADhO4oj#He#C z<6^dyPE!40!N+>-28n2{v}xi_@n!vTinu2PA#_e@v*$!)m#krGoQXgbv~l7c4RWhL z(aXLf!KJo`?jcrC1&$|8$17nO3Fe#LgcL)=^&{Z)#4jQpro>>lo>T(&wZ6KHDcSm} zN>Yyz28jj=5k)JZ3$O%){DbYZqDN^%LfCp0VpybaVxdw?B}v9_FcxR4KtA%&ISP$j<3LgW`7(i;0amiwzcqrR~Pa6D>JF zVtx!fJcdRvlst5JX6Yfl(!jwHw#MWPoBDOdg+rexdgl-}WAOAh72N8oP=PbpmkgDL zz8OHR&u!BtFc#B(xSc~u59(VAJE$_k)1?t50Rp}f6pvem@|lZu2kzxNz|9!h)ihDY zoVY-3Dyr*@hc*^$^_w1tRrE@Kf`V)aSku$<-wZ&RRQUjH?o z&Hc^41^b>tT)2M~&%wax(A1Cthk}cX@K1~k#-p-U-#|Rhz|j^S*Dj+MmU0%B8I7tXQpbw^kBH2j})|YSx~{3Sk-_vnhaXbe}AAf zMtw9bl|gxPf?g($ zfEBfeKwAZi9px2Jm7v|45*$LmErXBZt07fL)H}A+aQx_$TEW}_ixdEjp z7PO_JYPefc$3!id-&_n9DR{p$nJlA1R|YNxyEC92Ec8cF7O2xIxF_klOc95zHmf2! zHZnmpmUpDRPBiku*67;SOxbk@nakTVY;h6Pp5)~XGgLH1`H+X|05T%=GwW=R(eXeYP5LK0=kh9Ale@*tm^$v~ zk=Jd%cr8q`^{H`iF6D*1YDV+18tW!95obuqJ~=M!yKdH0J_#uktFx2KFgFgq!wPti zg*0u<%@sNBNlVUXru#Exnvj*52BJ3wKd0*|Ql0Ae>%Lx5m6i~)&B8JeMz&cDLlqx` zqjq9o_%M*El&Xr7LD6g^y75=X=0dv3*FAxulxK$9Ia4`sS;ub;Ibh5n@==flaI$Ne z8?di(pb;#|y=9NVQi~Dj+g!|Y{lKx#>ol%W@y5YtMqnPQu2C6Fmjv}?QVI4Z4~|Zk z1Z-&-gbBl2jAc|$MwB2wH~gRo$*2SHtLoGc!ATD-k8RayvQ1K>>5MrJB?DVJ`*9kB zA}JLh>+<}F3gZ=4#t2;MQ30r`WY$6^3OpCDecQ7LKMILrK~cfZJFV6cN`~tiF+Abu zuNXMgr$aF0&Cd~X*&b`V(jnJTD&vOp6;}n}t3T&cDIXBa&Q&rVa ziL6vZ0Y$uq0-|`f1ENi4J6=OB7f@_Esj3<(&ta+!F#EYr|8VV?hR>6uvS83SXUo|9nxYfQ#l13|BITHJ)ND9~Y`5Ts|;7J5o3Td%o0c#qj$< z)%-^0@4w$nSbKHOV|g$wm99AHW2-sp7n@4;qZrJB%g@e{CtKG4)(a0F}d}ltl^ltqF8G z5~s#rMNGP~v~+Z}d_NP8dh3cu6XaW*BGj|$@ z6};sbh167C#Biima8j>!MKh^J?q%B`V+U_4 zwmVQunUM3S?obtI(@`q$6`f%IFh&zur#VNQx|VdW>^%^!6Wmx@Ym>;%7*4Ne^$?(4!7vp^@K;*^4$w z5>++2h(e_wWvr*)5)uNHnl|+88zTwMNpwjOIl2z9Dyw62U1BK-hStF8$W){%6uPsN zJOY;^_lKMpL4sPON770$-2I-;`%-KP@U}>K?@(Vh18Nw4yQ~61^MXVd@0!XBaVbMk zy~fsW$AX{~C7v=v$!O1Rfpw;^Q|$W&c^Wf_S=n*z z;BpZi7AQ|No?%{n4xAb~UB-ED%~U|0?fbktkP@VvhR{*5C06dif~f*We(We(g2MxB zAL}1I`?#P5U`vL(Bz^`kmAjP=5}MkBRiQqJBS>k|2q#%Y;4_gka~KJm@R%ALcjB?C zC_g-FTYd%m)k`5-BB%T1b2sN*yMZ|-B@TOrO2Xiuu zp-(tZcgDQ=fu03j=i&%~jk~6WaK-QEdWC*BN^{5Q7#X{io7rnWGMs}6PqPcx{_H`+ zZzSbV-+Dj`&>erc&|#{6$6n{d!koi3wosCkw3I@+;`jAz9D2{1^_I4FdGttQoFErY ze}N?^T$0#@6;IFE3|PSnV9%*0!emtMB!*ip5QLk4N_2yXnZ*>dYw9{g;f zSKC^%R1(KBx@KgOs1(mMq?db*8MMQ;&+VA1p4vGY?iT=zC@m?!y_1+9`lLYA4H(yltA(#4YYevz@p9SXZWSZQ*?rP#m~$q@$ilJR ziKi7M(QiJQ+SD(raT=9aKC*IBa?IG=48_Pco9E1gmDm_UT0X_A#mA9S3|88s=4ym0 zj--lIe8t0Y))D(07O>n(Z;NL@9uY4cP)v_zROJCYaDW*p1Ey}El48K4He3*P}UoH$&b z{v_dIu;5HuO=-Z=+js^zBdt3Fm>DYxUn+lt;*)TzMF!1JJu`1a84Jhqhj)`h7S5jC zkb1V34%FEiV@6rb`$wbHp`A@+pK1Q+Qzr3Qb?qe_-%hVC?TDL{bHfcVC5fl3qzM`>Vzv7|vO2Ud_c?Fgu2jdWENv*_&;jr2AkdPV!O zBx4`FSk%|*Q_2tQKI6(-jU)=T;q}i`H5x6fT+KQ;BBmDM&VigBP{Zrm(75w}191>_ zS?iQ(v;&D0EKUrO7r}rA#YKybL0wfw%*Y%Oq1QJDDY_hTj0Oh4J`#KV7WUxIOvR;P z`@v`y>1qZeaP!jd2GV?w|da-Z%_qWelG?LkE4 zo3i}y3Zg6A{Pz0k$x;tTScZ4WhObKbZD>(}X2HGgq;!x+Fon{16%8!*FhXz@C zT&`_SD7J_7L@XcWOQsHvyO!_D*XJtUPEF*ImZyfcvX zH${i8waEqoO-04C=gjIkkI_7sof(!L8P4g`#+!8ywpJAe9i#-K*A|RpOO%&M{rL@T35_kU=|)6T7MTux)V#R)zF8xDL?BjWp~=oCK{yCC_gK zl{G}U!b}7SkaOl2XAtVUOKfH!pfg1BdSA{wxjMqoFKdD{dnSme%1W%N7cs?(38>#u zBRd01qo<8;FIxe0t`Kb)ji7-MZoU;m-ENo_N|}YrB5zVwQ6$*9t+5#aCRB{;IG&#( z(jxhT)hENWLY{hNAY*)_zmSl(Iqd*C%7FC??Gyr1FndES*9RZXy4q-!ZD_Y(*;k^+ zt7Bw31~Y;PijEu0T}>*F9XCZbUltW9w?xwXiCM0cnum?VrnCQ%$!HgfbpXI(txt+K ze?lx}1XI`q?N9nR;dVFDsvade@Ni^nKy?eZ0DOLlJL+`TkmqfiBpuL8nNwbTmd~g- zc+Z1PT)j?yN#=Amknkq6XrmK~CvxkuB}QVMp+vBBSJ&f60J*iGGph=eulhzlC{}13 zg<8`XyY^Xz##IJOawB{db25;V4GnF=hau7$Lq{-2Ths@q*Q{~W;sl~Tz-k3IsVV>A zY>LgkCrFaS>gc$;Fer>q61d7((rICd@@EfOWw0C!_gI+KTrUSy6aJbW5TIuVc=u74lW$8ytkRtAC^ga&oEF$$xPE9Pc7-gIpkKJ7pvZw73^cYQ2FYg^yx)Lo$sJHcq3*<^A~}Ndh9K23Q4lB?i9MWN z)>pN#xo|ScKpK=G%#)q@tYL<;PKl47`$=3VO47_s*6Ait*BSX9gyy6xKOD z$`6<0RE@lNL-mZHQxIZ7b%b?{75nrA?#$FuxC&TEpew0x>i(`>-6KHdyH|6)%i(An zMJ07NsL)_;_t7LaxChQY)?YJx9BqDJ?xJhOmM2O^_jWc#$~i7yCM*?+>73gSae~Or z)HtEHlkqdpDTVhrEEmWXGi4X(88ZRAyja%r^cE@$g!S%?I_a{RV0442Bl|i?4V6cd z;cY&aKHuhpALK-$aAmg0ltF zDUekao*Iqzb7kmNbzWy!u#%VL;z+`n(cG)CPL=TV0yF~bz|@41^VO`e5S{k^3qvVhLQFZ-kYpox*UE4DpNdWyp5ObNyQjTbY;7V zK|j^*n9BfOc7eu#GY&T#92xorn>Aep*g_j;B~Fat#%QN>+t0y|bZ2f4;N$IqH-owp zP263Wj&*u^pB4==BQjpGBi+i@B1sGlQpI$J)*9(@YgNUZp(%Nd9>e%k)dVx=VQN-2 zrIcG&Ulf&-Nle?XTP&c&rHGlRR}w>KHdR;K`Vjlv;i510kxqxYH7p+1Z}7LahJ?(8 zyi-1nfgGfMy7%nQ*=BeAhtJB*~Nh7Q}kNM_&|>(#eD zn5v73rX{cb;QW5nHDD>rg|vQqt*RDu8Pm0=(Rwt_ zhyi_*&=+IoZI(2>@cr3o>dTGJEm^8SX#Pn#T)z0|-)~i<E~Ewrq>?U-b{b0^n8AlLBz=N%5<=2M534u2&1Ck-VudELWd zI{GBp$2)0YpKA+CqH(D!mni6lm~r7sb%t5LqN4&sR-n=*&>b^SyUI$VB>mw4lrk|T z!Nj>&NJ+_w6>7&|Q&J~A?PROD#E}Ky@Nl|ZQM4zm&x5~QbZ*xet-ic7&K;YshOO$t zqct}Qol85_rEUjeadH)Vbr)P(A>A8=B_!{`q1X40s-*`632kAU)blh>_0eXtPKWPP z-8yzrt@ZT*Cx<2Ed>7eowA=M{5iz{psE@94Tc}Zo^FPOG3Iv^&v(rL&vg=O#2=}^f zzi!k|nKqsVsqq?EI32TY=y##9hPG$5yGqiegah|_m)#@7{uQO{_t|*GDM(|AG*j$* zxeV=2IPbA>!jif>O`8*kzS&xf)0R5MS2&9{SJ#_UU1yJ12ZTI^mZ5D-{g_Q035;m_ zQ_aq(qb8Ivyc^}#kY{01AH&lRoqBiGNlp&O`Z)Iv!}U(LHwp!~1IL7eVyvx?+3UPi za00hGkNu zATcXOb3na0j+lZv)4`yf;hxOA;7gT9eZ-;QE>rFfvL{8F*GEL!7fR253w>s+-tE=> z9*in=J1jmM=c4J~(G&jxd;gniCT*-F4TO?7M69-z)_^ zV3sC0ajWLpnRw~He);%zi4a=~`EQ>ld0*LmSOvuZew?QR>hIxV`?>qjWy8_k_tU5y z@Zx{vIx7U9ad4RA8|!ek4dw(-Hh|vFr6I{_Y^2L_zUnuTU=(PuqSAKyPE5 z0EgYgw@-VjJk;!tP3^kpvKQ^TaA@&S3h({d1|iA{eM02?YkT}(@GmbNad-n03-bHz z(~{(gC)_w77L>tM=wcr(1ZR0%L%4u4B}FrvXb?#lngc9^;JeX(>K}0z;^itte^Rjm z|JUn7i^zR$eQ@6Jz_OvGBTJUmO|F6E?oc8TGjy1(O!Up{G5Z~`pXxi*V^9VEyyBj( za0V%xLd1GZQ=-iOPLJ7N1F6S+TnE%&>M{RKJz;ZN_u0rsWu5+)>+NNF%*l~<_0Tc@ z{v{HSO&zbd8AjRxx~gcT(nxdv9WD;nBW<>Z6Wxz>K>a1U|AzYAoVtB3vM@up{IAyT z?1S|*cj8t;&~R;@edwE-zukpPnhR`1$QDN`nfzJO4{gSrIs3qJTH79W*LQYCS_vi< z-t^SjzRKtAhc+Xn1N7v)t1Z>1o|!#Nm6!S{@xia3fcP(u?F04DcG6q^tikW^h4ivW zdeG%!2P7SFcrE5!k#cTQmTn_rMfFnOjPzptW$nFBdzAW1VbW7x zBmC0%q0LC^z}e;ga@JOl-L*H;s)cEB;r{p4>wyQ9(>}WUjN4y=w6}25md*lHV`9(F z9$EVTgSPXIkFt3FxQHUyrKzY8P(V?Uj)-sZV8k2rL@J_IeN!XF_}9fS!D`uHd(l!_ zE$ix;RTDFeB1*58=v&tARlJwJWj#HQ+Fhx5=|8wIVhw*?jFdpLvEvROAkEg-Q(0@W zGR=Zj>dG6>vOrjn*DhdI7L+U{W5BPb15#|A`pRqN_fSWZr)40 zTaDg=7EmR;XD?a4jd-^OcwTj$3G}(FetaeI8VB^r9xqhom0->M;L-(iDb6+lJT-QT za@Q60#;&E@HB3iU6bC@Gi@9Y)IYqmsCK{8FU6T+TQQmF<^UZ;y7Z9_Vj>(!1Xex-- zzhH28H4(N!v$5yN`)}7VLoS!{Qi9ya^X%RQl>6KPi30`28btn&5pHJT{hLU}<^oYo zJnsl%>Yw!n?j&Lhfv9Gs>;!friQ{)1y^|4JLZTwC1ncF!L+6&>!nk+pxNLZzn8He1 zP6xnx>#Eu%#JWqzQn6+r)|}`aTZz>w7{jY=y*O3*dOpSPySZEuRZUvj2z`SC{0E;0 zapt}&*OXG6xkuo!4G0a+wZ5+dYsQ+ZJ}96y<6gjIZ9n`a2BWBkzb?k}L9FR~U#EQ( zYg(JB366|ZtFZ?7VZ#H7`BW9SPav`$5vw$?TfX`3Npr#&t&N$+dg2TkH2!rl`UIKA zqkEcdB};9~y&`C-D)tsyQ-0B2lg-akA{nT} z{hodG9-?*7Bx9$QJVi8{iFD*dmAET6EnZAk*-?|NiUx!;==DEZN#kq3T2EHeDIgp5 z^1|}JymTO*wZ3)SVv1*-{k$yJ+{&**(H@-Is0>2=hZqXt*`$O+ODUe+FEZJ*9NzQA zCgT~ghQBUGv%udg{_4o9@?AtKzoBIy80i?=D5kx7^@D3ErafffnH^u2 z)e(F@*YpJ|>HBpvuvq6_HG0O@m)_q(p7F5AtX9v+EN;Csj8PvEsnz5e<9<7FiZmN8 z@)UUnTA`jxYg0~ZMt6S{*Ww%q;?jQD0oYFYbqmUGX0!-RLPrK4aVa>264DDPx91Tl zGDtGJgA!8R;V%>tDJn=}&R|(4P!-<$-NZw5UK6c-E7BhLP|`jQR-!r6npKd$_0ULk zw^++vn7<{n86&I#QxH&^3ZmvcIe+g7W7M9SCWBOPm?+HyJL>VP#vGs>b&Mz_5>VP! z&{==q$>aIyMK4W4R)wKk=AL%^wunKb|F8}Le>?ct@uL(`dW$^Pv<|jM@-hOgzPR>y zK54a2kjXZI5zJb1Sbw2lHqt~C!%{nM-%Bwp)<|U9wMs*En8>!johJGE8fbKVM1_2a zxO;ESc|`1IAhNkbF8(x%MY}<9y57QNJ1C~b1&IoxC)!2S>v{cF+C}s?kfP{u4B_J$ z#Jq?+301Vt)@`ttDq8Ud8co1p56aL~cGf8&ny4%nNtt(a*b$O4A;_(I@yVi;=v;q5 z+sQjvh#3+vpirmy>Ld znrK#KLpzA826f+0tI`l1&Hv&Z-G#GkE1kd&aC-lj7u>~|L-kA-yDFB+Zr`xG_xwmi z6js`95c!Rp8_p;4Fih4ys%3IVkT3rF`_?nmMevx8%wOW*O!M)&bfg2-(eK|Hvy7^v zkL#G)1Q>0yzky*i<^LGd|Mm>bqnK{f5ov>Vq3~n)6&Jxpxm@a)n#-xC@Pv+A^~{S= zu>X+DfvLbu3*X6?9kJ28_C%+O(7TCX5y{bJ7KH546^bxd}~<*;YQ zvr8zZMkZ+=MG>ju%a8MjnPOt{J9^f=6Ue#yiZjcJ=`b?cC{8siCdwqw`rceXnPjSt z$sV;}Q?1y;K9;5I1gv)a_0Mi-&4_6_;^5dmbkmyMZ&m4|sMon`+asH)*E!u!=kFQY z`eo7iH^0Xjm6}R-%nWvG{Ocm3LuYVi)A7=~7SZJy=Y`3XdiJ70w$^?Dvgv6}*Hn^y z4G;W+40e4g+#5mjJ=wnsNO?a3r>TeEyQGvlokkd#boQ@|3T&+Z_RR|@N@bX++J$m4 zp)lzBBED}&AnC`~6`dzZGY!m`OnVx;^zY7QXA{+--;X9-vzVwZ1C_=<-#{UvA zt7_A2KZDH}|GEeZLVM9ctLLq!yu~AuCyY2k`O2YH(-%`D$QF2f*aUzYe!|7b2x_IhVpj4~yBx%X z%{kIkP-SaS&_0h=@{s}+0;P3}_Ay`GoqL@2F{1>capsWGZ2m*$3-%PxPu#tk_7u7P z_`?ri`J8JMgrv0#c8mWm`{o4Y_M@w0r6XTfaHHWUmL&wdx8EvXNW7;+o{Fj)u%5r- zQjU`KJT3D0IFIU(aN~C=8%e@vM52PT5_EH?_;XuH7~?&ww;B3^3Mye})!H?}bB0!} z=k)TiF(Yg`m%$+agGYdj$Mf#nWfbSem}pf4t&LE_Ul$YSKt@}TIrtRSh@Lkv*@Sb` z#zB^b^f~0U%Xk*>f{`|gO?ET<@8$K0*2D!ThMl4{ajb!6c0L38BJ_wP3;*gXeqU~! z-ZGI{N=I!}$$wzW%zaeJAMcmU;|7Om@+4rBQND3osOmhyFBcrsYtkP7lYRruJd|Mc!z8j!&oDyqA zHyREUtzAMl8r}<7tC~s;I)6R9DPkM>;Zz-q_2RMz;;h#9&>4D3%^CA3n|WVPGbQt~ z-wfr0kvm87Om|w44Z2Sw#`_0~1Y6q@jHxP^}{f~R-m#)3(EJc*B47}=!D7_jl%%_O*wTY%CqM&aVQ}-3#!}X1hX}&sd z(Co$N5@biGyZy$yioXry3@DYBCV`x&Nyf>;RAu=tDARx}HfB(CZ56C$Z)Fq{>-!)} zONET~XZ-6TG>9^j1z$E>NSVnDuEDB->PoZ-zSri-DTUIyHW)Sd3DQYYz1MA zG?VL)tzxXVFQsahlu>iWzb;0PK(e97qSjEJ{-d5(^(qfN-(aK||GF3^s0;kFxiy zM$H-jx(F7klG%Q>7tqe`*8nT<{$Z@$$6iYmQw1d}+=D;3cakoD3s9rk}^ zLOzshOEV}(_R%w30RDvR9Rm@=lsK3eOo~1{C@_yPWDE40EvxM zmW+C?4vu_=Pm1^}KxXM)fGdcqM8_}h-nfKx{5K@hj)WfiGstHA>tcKa+&}h=d5up~ z`6tMAvjVc)WBm*a(7wL^rBf}Ll>h2z?0GfzM1w<_cF}J3NaBQjlzaX!!0nfu%pQed z?>ZP&D*Y`{9vJt|t=lNV%?Z%>`&zs2k?bmt(h3KD8jocX;{hME<9;ENU9%97q4nI!kNULOqk-0kOzJ z9Vw$WoZUqoDfs~w+jKJDr`@~%sVkPW3Seo?b_QAF`z7u7)4=9M7bH`pr6+OdnwI5X z0U}`9CkjkopLVxQ&m$vb*5`6t{qpGERq4#A0=joq0EtTN4&;olJo(~08uMQG|I*di zTW7F>IDoy z_7|WWKGE^-JjyRu>VJXa93E9`GdHJ{Q5|lTo~sSrx``zrP@E7kU3?ft8ivOt#(y*%9QEue@J&n0C}#AQ29`mewlbcCImc z5plOdu9~lj@inZq8{3r~JBn?0?@_Man+i2tx^fjAT~^2!yohLoC!+T*j9nV|Qr z@b%Vt)cdwG$fWavs;QR((xByw(JRP5TYeS2EQ}F%8HunVRF;RPdH*=KK^{%>-fiS* z&O!MeJB?TJZVTAkeRX8u4qC_d1ldu{km!NfYS8TC%e${8&F(eROo3KbvnUFT8}r8w ziURwLyl7U962+5`cEBTN-T37ZitGCU&m^D{;@+k`3yD}3B(frZ_iSf|&)hsEPXre8 z(9F4e$YKr{h)AW_Y+3_SjBuZWYE4`?*}#Y>9GMvn%0bPKcU zy>f$@$gfjq7yIb`H@DC(_E?a}`;6#%G%MP9(AZ+b&$yVC14!DX<4220(&Is{|NWBc zT9H34er`Lh$R|uhMJqDM0**ZR(=lS6)HC^m(2AJAM~2lIy*`XF%Y)1WdyWGFrN1QL zWgV-XN4yFhk3V3Y%Irr7JJLK5Rg1bNucN4X%D_vcr%5f+kbjJg>9vde<8+Yb_r~C( z;o^~(w3HxAo?V)^nzH0GT7S!^WLr4PlG8ICK4&s2^GGv+wZ1t$iccgu8)SOf^VD=a zgOKtO4=`&Dw{Ip7ITvL5*rPW|**3t`^+DktfMOXWdUMDx-%n%T~tYSc)cp*M0MXW^Y>L!-S_e!)$MYzSurdmgeSQ@ zK1T{d%S#OG=-Bf6c%7gYU?Zrk9U*zM3hv!a@?HU$Y*0_OSCPD^p7+z~8kJPfs}0#+ zcBV^X;Vef95S?a>f2@q6Q=I@glD#wH&<^Gm*)-}QxwUlt0qP;SGQf@M7p+E0U{7~X z|FV?g(p5p4i%mdcu@H+WYfq>$JaX$N>!>n(wT_wN^tfF#Z%>V+AVd1`*|xhVL%JqN z9hL0J0;6uFWdwR{;QEwLWV<#%qtpJVQEZr&lDLFASt29Xo~CGcoq^fc?TdD^m+O&Y zr6mP^vbEE-733$^2dNou*1h6N!GhsVxZSlEZP+M9&Vzn=b}_ynDQ)e8_6Whfw9n3CJHGK{h62U(d}Y%xP0 zP}**QHlO(N-3roX1A(XL`+=UT#=l*^lHyLoAkmdVBRRi7HPNVoGGhGyG`|6))$k(ha2O7Ap2F($~B|ni`}$x zH4AY2B`DD?SV`J!YJZAWlG}r<9ET@0!#z?35lt=)%Pq>M$)$G$h;a@VCC&fu7C~0G zsCT#R71z$CoV~e@?#Xm|y&+0c(oB%$J@)0)0LRhXC|FJP znpQz(HhZ2O2SFv8{n&OBuch8C5^>Te&jglx?63NKAAe7Pnc#D0g?n>d$?Q@v%ifvS zSpK3QDjjN>bb_MNy#{V5q_mHM41QdT$^6Wrb&$xryi`QNjzGflcQ@`Q3GWkl_+zS& z@Yzj2>ho$exQr0!n{9kgtCnvtcpi z>m38M0j?w}h47ZGCK{-?EpsmkjXaA#ikTt2PPokgzpiUbl@ zo{Osf&7+plDssPxt3pzAU-S7e~0?v0C9-JL=i8jBoZXJN@F?MO5qR z4w#DG;sufR2YbtP6lo*ORNior#9q0wEG^J**;mPPDEdSSTz0du+BCerXX8Vp;V8hY zJ`L|^FyattIND5ArePF|Hhmw-rz`Z}TspC_(Otc0tGz5J*BdqR^Tm|w^$fC@t5nx( zkNaEPzJP2eCP-tW1`>SitlSC4G|R_6knQb>e}KOn)+@;Dm*sG&=nV!jukV|y`N?>1 zGYwu7kdApM{yepI0eMHCAd@*`m5@E%ZtHOXtYR%!cz0076&s}Tr--1K@?R1Vn~Z+* z7!mslM0jCInut8{>!dcNloj;j>n8I=wpCX$%LrsV(R-tg(k?Gfla5X=*;_b@UTOi_ z?D=WRX3}PV9go6H^%3h0>&d6_#tTF>-w)lU8**iB?Nv)1k%c#K&05wEQ7rY8$1S2* znrNiO+AY6iglJRBUsyu40RqiwxsHT9p=|Q`&EyFKb+kB^VTc0=rS%B1)kRq!EhW|< zfz?4sXH>8zU(t`x6d5eA?zhSUar(vHhtAX8yhpWY!aZEc9AmKRU4QTGN2$g!L?_n( zSC)b=hWxDXkK6egJJdw8`pppS%eChhkZi+DG^<|?)&6hZ)qgAHERSh4x^!cUwtIbv zZZ&re&|NCWylt!KF4f~9m~@>AZG~k$0%^Bho6GnOWSfpnmvPM7327Rr-p;5DPm4H-Zi+uv^@?VB{q*#*B{a(`UB@(!{+0DYbWeTE%u{(uymPiG!aF@%iwUvn%;$*)dI{%+|xtIH3rimMo;r6l?NWnm*nPAWIN|#+{ zXwTv@GtF0hZEDt_CS-->(9{b z0iTIRd0hW&dj=?m%acK_GxpbK=TL0OHj-KJtKQhKE;s)qtZkp*F8za-jSl|&TJp&aCSBinyNUI0pe z%?@CXd~WPB`)*>y7c`>7r6O|$-tg=2506vS9;@eEV2P5|CVFe+?#Di#M{kXc<8h2_ zi&1%9T8rqrwe2;9?~2F!N%Tyi_A(;~n({=@GjVLnqr1bHY!kRXd)mF}l$F!SAGRV1 zh9av=Bw?(x$y&0^7dcNGJ7^0}Nl}wWp?>m+uC@|d{U$=9qLU!7)*nxBl+cOSO9pNh zJJw^DAe=s*JxNfn{PzTP=|gjmEu>Dam-W=BQI2G)9YH0_epR%;dol5iQao`MV8F&+n3J%I zZ0uFzZ}2I!F+g2#I_)e`U*l2A@5Ag^j+HDl3$rdO*n_>+^Ej_@zpj^+#VU<&dR|qD zH?5r7Vk3Fe8v--dp5{zeQ^f<4wr|vI5A8PJMASrkn&r3wsutZ=<@1x)DI!zJ{ubSB zj>-7w1XLRMFzpbZgCM4iwV+88IoN(K?oz#E%j+v(S7pr8j z4m?(?WLq0Xe)qy;CD9R8-YQ@Ea5Lp;@0s~(Rv{3P_aDD~Db+BhhGO<{PG0=1xot#qaCpgIfeclV+oRTt&f@wX~1>K))KO z%5cCApmFg@K8N8$y{v3!$EMluLcLF=jXA zQXl(SUZ0&^r_x4Z+EmTr1glQcw7e6v>U@H@3aXAMO61k3y_cfIr<(1!46=ePkF0*t ze&CMg1v_a!@R?su)-J_{m9T&M>kr@=F=;6 zGtUUJ`dWWS3XxtqznH(u`a_VEz;>)w?G$AD!|!^JzbQJCuP=SrNFlmaipqD=UIDVO z&h;j3q1x7u0b0BxClpXR%A&j^;?<`MDKGhnYm(%%h4TuNMO~uv$Yy2D`2>-lbu4dU zHk*$iz~m(ZQT+Z-x^UVrK^pD#RlIf%>}1!yk^5;^_^W}Iz}^TDg_E@t^ws{|5>`N8 z?YAH;#zliN1w~4;KvwtMw%=D%?fUm1OF?u-_Hn++nF=~RniV85^LKcx-)BO!xOeK8 z5$%s4jn&Gl%RVlfn{b-!W43{G|d=VZgO;}nVi)X}Qv)#~t~l2Ri5r6aLHR#xp0 z$dtLQO$m|y4#Y+!Q5abKXJ70uq}c@O`;njRT5)U^m{ zp9XnQSa!^F?X47D=IPl9%;RlVJrUJwitBukPxYGl2AUV) z?4Z5&!ca^VTyP%sZPN*BXs?ie zA#SYQD_(Y!jS5KSw*Ma7N-{4B#a6&Y_H^Ug@H1pji}l=rZf8oI&-%1kaAJMWQ-dqQ z7;j0C7xGG&tX@%UpSIxY!{nVyjXVVwgeJulxA|okO^R7&rl}wTectHA2+B7w-}#9NL&Xw7^(aX7#AzTC(g;Nrtg)SLpK z3e<=k?-r_$tPr{CVlB|_S1G*@(;3&w&{P$c3oPm2^p{SOC9N`&6~!L3>-hP}p~q?0 zu{ubkU8yV8^82)?PV&OHGYTnZUK7YMfNgafooEas$Gdlmx8X9FPD={j)z zYKo?7xds_2i}@fdYDvd#kSX81Wzre)h;;^o?^GXafrcePOsYh%0ANR79-VM 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 literal 0 HcmV?d00001 diff --git a/src/external/windows/lib/x64/python311_d.lib b/src/external/windows/lib/x64/python311_d.lib new file mode 100644 index 0000000000000000000000000000000000000000..c501ba87bff408b67d15ac3b961cce620adeff4f GIT binary patch literal 364518 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 literal 0 HcmV?d00001 diff --git a/src/meta/.meta_manifest_public.json b/src/meta/.meta_manifest_public.json index a5a5e757..ff96115e 100644 --- a/src/meta/.meta_manifest_public.json +++ b/src/meta/.meta_manifest_public.json @@ -1,7 +1,12 @@ [ - "assets/src/ba_data/python/ba/_generated/__init__.py", - "assets/src/ba_data/python/ba/_generated/enums.py", - "src/ballistica/generated/python_embedded/binding.inc", - "src/ballistica/generated/python_embedded/bootstrap.inc", - "src/ballistica/generated/python_embedded/bootstrap_monolithic.inc" + "src/assets/ba_data/python/babase/_mgen/__init__.py", + "src/assets/ba_data/python/babase/_mgen/enums.py", + "src/ballistica/base/mgen/pyembed/binding_base.inc", + "src/ballistica/classic/mgen/pyembed/binding_classic.inc", + "src/ballistica/core/mgen/pyembed/binding_core.inc", + "src/ballistica/core/mgen/pyembed/env.inc", + "src/ballistica/core/mgen/python_modules_monolithic.h", + "src/ballistica/scene_v1/mgen/pyembed/binding_scene_v1.inc", + "src/ballistica/template_fs/mgen/pyembed/binding_template_fs.inc", + "src/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc" ] \ No newline at end of file diff --git a/src/meta/Makefile b/src/meta/Makefile index ce68dfee..7a511225 100644 --- a/src/meta/Makefile +++ b/src/meta/Makefile @@ -1,38 +1,66 @@ # Released under the MIT License. See LICENSE for details. -PCOMMAND = ../../tools/pcommand all: sources -clean: - rm -rf ../ballistica/generated \ - ../../assets/src/ba_data/python/ba/_generated \ - bametainternal/generated +PROJ_DIR = ../.. +TOOLS_DIR = $(PROJ_DIR)/tools +PROJ_SRC_DIR = $(PROJ_DIR)/src +PCOMMAND = $(TOOLS_DIR)/pcommand + +# Blow away 'mgen' dirs in Python, C++, and meta-source dirs. We use wildcards +# here instead of using our exact feature-set list so that we clean out +# orphaned generated files from featuresets that have since been removed or +# renamed; otherwise those files can hang around and break builds. +clean: + rm -rf $(PROJ_SRC_DIR)/ballistica/*/mgen + rm -rf $(PROJ_SRC_DIR)/meta/*/mgen + rm -rf $(PROJ_SRC_DIR)/assets/ba_data/python/*/_mgen # This section is generated by batools.metamakefile; do not edit by hand. # __AUTOGENERATED_PUBLIC_BEGIN__ sources: \ - ../../assets/src/ba_data/python/ba/_generated/__init__.py \ - ../../assets/src/ba_data/python/ba/_generated/enums.py \ - ../ballistica/generated/python_embedded/binding.inc \ - ../ballistica/generated/python_embedded/bootstrap.inc \ - ../ballistica/generated/python_embedded/bootstrap_monolithic.inc + $(PROJ_SRC_DIR)/assets/ba_data/python/babase/_mgen/__init__.py \ + $(PROJ_SRC_DIR)/assets/ba_data/python/babase/_mgen/enums.py \ + $(PROJ_SRC_DIR)/ballistica/base/mgen/pyembed/binding_base.inc \ + $(PROJ_SRC_DIR)/ballistica/classic/mgen/pyembed/binding_classic.inc \ + $(PROJ_SRC_DIR)/ballistica/core/mgen/pyembed/binding_core.inc \ + $(PROJ_SRC_DIR)/ballistica/core/mgen/pyembed/env.inc \ + $(PROJ_SRC_DIR)/ballistica/core/mgen/python_modules_monolithic.h \ + $(PROJ_SRC_DIR)/ballistica/scene_v1/mgen/pyembed/binding_scene_v1.inc \ + $(PROJ_SRC_DIR)/ballistica/template_fs/mgen/pyembed/binding_template_fs.inc \ + $(PROJ_SRC_DIR)/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc -../ballistica/generated/python_embedded/binding.inc : bameta/python_embedded/binding.py +$(PROJ_SRC_DIR)/ballistica/core/mgen/python_modules_monolithic.h : $(PROJ_DIR)/config/featuresets/featureset_base.py $(PROJ_DIR)/config/featuresets/featureset_classic.py $(PROJ_DIR)/config/featuresets/featureset_core.py $(PROJ_DIR)/config/featuresets/featureset_plus.py $(PROJ_DIR)/config/featuresets/featureset_scene_v1.py $(PROJ_DIR)/config/featuresets/featureset_template_fs.py $(PROJ_DIR)/config/featuresets/featureset_ui_v1.py + @$(PCOMMAND) gen_monolithic_register_modules $@ + +$(PROJ_SRC_DIR)/ballistica/base/mgen/pyembed/binding_base.inc : babasemeta/pyembed/binding_base.py @$(PCOMMAND) gen_binding_code $< $@ -../ballistica/generated/python_embedded/bootstrap.inc : bameta/python_embedded/bootstrap.py - @$(PCOMMAND) gen_flat_data_code $< $@ bootstrap_code +$(PROJ_SRC_DIR)/ballistica/classic/mgen/pyembed/binding_classic.inc : baclassicmeta/pyembed/binding_classic.py + @$(PCOMMAND) gen_binding_code $< $@ -../ballistica/generated/python_embedded/bootstrap_monolithic.inc : bameta/python_embedded/bootstrap_monolithic.py - @$(PCOMMAND) gen_flat_data_code $< $@ bootstrap_monolithic_code +$(PROJ_SRC_DIR)/ballistica/core/mgen/pyembed/binding_core.inc : bacoremeta/pyembed/binding_core.py + @$(PCOMMAND) gen_binding_code $< $@ -../../assets/src/ba_data/python/ba/_generated/__init__.py : ../../tools/batools/pcommand.py +$(PROJ_SRC_DIR)/ballistica/core/mgen/pyembed/env.inc : bacoremeta/pyembed/env.py + @$(PCOMMAND) gen_flat_data_code $< $@ env_code + +$(PROJ_SRC_DIR)/ballistica/scene_v1/mgen/pyembed/binding_scene_v1.inc : bascenev1meta/pyembed/binding_scene_v1.py + @$(PCOMMAND) gen_binding_code $< $@ + +$(PROJ_SRC_DIR)/ballistica/template_fs/mgen/pyembed/binding_template_fs.inc : batemplatefsmeta/pyembed/binding_template_fs.py + @$(PCOMMAND) gen_binding_code $< $@ + +$(PROJ_SRC_DIR)/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc : bauiv1meta/pyembed/binding_ui_v1.py + @$(PCOMMAND) gen_binding_code $< $@ + +$(PROJ_SRC_DIR)/assets/ba_data/python/babase/_mgen/__init__.py : $(TOOLS_DIR)/batools/pcommand.py @$(PCOMMAND) gen_python_init_module $@ -../../assets/src/ba_data/python/ba/_generated/enums.py : ../ballistica/core/types.h ../../tools/batools/pythonenumsmodule.py +$(PROJ_SRC_DIR)/assets/ba_data/python/babase/_mgen/enums.py : $(PROJ_DIR)/src/ballistica/shared/foundation/types.h $(TOOLS_DIR)/batools/pythonenumsmodule.py @$(PCOMMAND) gen_python_enums_module $< $@ # __AUTOGENERATED_PUBLIC_END__ @@ -46,11 +74,16 @@ sources: \ # we can't use our full Makefiles (like Windows CI). efrocache-list: - @echo "../../assets/src/ba_data/python/ba/_generated/__init__.py" \ - "../../assets/src/ba_data/python/ba/_generated/enums.py" \ - "../ballistica/generated/python_embedded/binding.inc" \ - "../ballistica/generated/python_embedded/bootstrap.inc" \ - "../ballistica/generated/python_embedded/bootstrap_monolithic.inc" + @echo "$(PROJ_SRC_DIR)/assets/ba_data/python/babase/_mgen/__init__.py" \ + "$(PROJ_SRC_DIR)/assets/ba_data/python/babase/_mgen/enums.py" \ + "$(PROJ_SRC_DIR)/ballistica/base/mgen/pyembed/binding_base.inc" \ + "$(PROJ_SRC_DIR)/ballistica/classic/mgen/pyembed/binding_classic.inc" \ + "$(PROJ_SRC_DIR)/ballistica/core/mgen/pyembed/binding_core.inc" \ + "$(PROJ_SRC_DIR)/ballistica/core/mgen/pyembed/env.inc" \ + "$(PROJ_SRC_DIR)/ballistica/core/mgen/python_modules_monolithic.h" \ + "$(PROJ_SRC_DIR)/ballistica/scene_v1/mgen/pyembed/binding_scene_v1.inc" \ + "$(PROJ_SRC_DIR)/ballistica/template_fs/mgen/pyembed/binding_template_fs.inc" \ + "$(PROJ_SRC_DIR)/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc" efrocache-build: sources diff --git a/src/meta/README.md b/src/meta/README.md index d336ad8e..947850e4 100644 --- a/src/meta/README.md +++ b/src/meta/README.md @@ -1,3 +1,6 @@ -# BallisticaCore Generated Files +# Ballistica Meta Source + +Code in this directory is used for Ballistica's 'meta' build targets. +These targets generate source code using other source code as inputs. + -This directory should contain files used to generated code diff --git a/src/meta/bameta/__init__.py b/src/meta/babasemeta/__init__.py similarity index 100% rename from src/meta/bameta/__init__.py rename to src/meta/babasemeta/__init__.py diff --git a/src/meta/babasemeta/pyembed/__init__.py b/src/meta/babasemeta/pyembed/__init__.py new file mode 100644 index 00000000..b39b3d0d --- /dev/null +++ b/src/meta/babasemeta/pyembed/__init__.py @@ -0,0 +1,3 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Meta source files.""" diff --git a/src/meta/babasemeta/pyembed/binding_base.py b/src/meta/babasemeta/pyembed/binding_base.py new file mode 100644 index 00000000..03f04a2c --- /dev/null +++ b/src/meta/babasemeta/pyembed/binding_base.py @@ -0,0 +1,88 @@ +# Released under the MIT License. See LICENSE for details. +# Where most of our python-c++ binding happens. +# Python objects should be added here along with their associated c++ enum. +# Run make update to update the project after editing this.. +# pylint: disable=missing-module-docstring, line-too-long +from __future__ import annotations + +import babase +from babase import _language +from babase import _apputils +from babase._mgen import enums +from babase import _hooks + +# The C++ layer looks for this variable: +values = [ + babase.app, # kApp + _hooks.reset_to_main_menu, # kResetToMainMenuCall + _hooks.set_config_fullscreen_on, # kSetConfigFullscreenOnCall + _hooks.set_config_fullscreen_off, # kSetConfigFullscreenOffCall + _hooks.not_signed_in_screen_message, # kNotSignedInScreenMessageCall + _hooks.connecting_to_party_message, # kConnectingToPartyMessageCall + _hooks.rejecting_invite_already_in_party_message, # kRejectingInviteAlreadyInPartyMessageCall + _hooks.connection_failed_message, # kConnectionFailedMessageCall + _hooks.temporarily_unavailable_message, # kTemporarilyUnavailableMessageCall + _hooks.in_progress_message, # kInProgressMessageCall + _hooks.error_message, # kErrorMessageCall + _hooks.purchase_not_valid_error, # kPurchaseNotValidErrorCall + _hooks.purchase_already_in_progress_error, # kPurchaseAlreadyInProgressErrorCall + _hooks.gear_vr_controller_warning, # kGearVRControllerWarningCall + _hooks.orientation_reset_cb_message, # kVROrientationResetCBMessageCall + _hooks.orientation_reset_message, # kVROrientationResetMessageCall + _apputils.handle_v1_cloud_log, # kHandleV1CloudLogCall + _hooks.language_test_toggle, # kLanguageTestToggleCall + _hooks.award_in_control_achievement, # kAwardInControlAchievementCall + _hooks.award_dual_wielding_achievement, # kAwardDualWieldingAchievementCall + _apputils.print_corrupt_file_error, # kPrintCorruptFileErrorCall + _hooks.play_gong_sound, # kPlayGongSoundCall + _hooks.launch_coop_game, # kLaunchCoopGameCall + _hooks.purchases_restored_message, # kPurchasesRestoredMessageCall + _hooks.dismiss_wii_remotes_window, # kDismissWiiRemotesWindowCall + _hooks.unavailable_message, # kUnavailableMessageCall + _hooks.set_last_ad_network, # kSetLastAdNetworkCall + _hooks.no_game_circle_message, # kNoGameCircleMessageCall + _hooks.google_play_purchases_not_available_message, # kGooglePlayPurchasesNotAvailableMessageCall + _hooks.google_play_services_not_available_message, # kGooglePlayServicesNotAvailableMessageCall + _hooks.empty_call, # kEmptyCall + _hooks.print_trace, # kPrintTraceCall + _hooks.toggle_fullscreen, # kToggleFullscreenCall + _hooks.read_config, # kReadConfigCall + _hooks.ui_remote_press, # kUIRemotePressCall + _hooks.remove_in_game_ads_message, # kRemoveInGameAdsMessageCall + _hooks.on_app_pause, # kOnAppPauseCall + _hooks.on_app_resume, # kOnAppResumeCall + _hooks.do_quit, # kQuitCall + _hooks.shutdown, # kShutdownCall + _hooks.show_post_purchase_message, # kShowPostPurchaseMessageCall + _hooks.on_app_bootstrapping_complete, # kOnAppBootstrappingCompleteCall + babase.app.handle_deep_link, # kDeepLinkCall + babase.app.lang.get_resource, # kGetResourceCall + babase.app.lang.translate, # kTranslateCall + babase.Lstr, # kLStrClass + babase.Call, # kCallClass + _apputils.garbage_collect_session_end, # kGarbageCollectSessionEndCall + babase.ContextError, # kContextError + babase.NotFoundError, # kNotFoundError + babase.NodeNotFoundError, # kNodeNotFoundError + babase.SessionTeamNotFoundError, # kSessionTeamNotFoundError + babase.InputDeviceNotFoundError, # kInputDeviceNotFoundError + babase.DelegateNotFoundError, # kDelegateNotFoundError + babase.SessionPlayerNotFoundError, # kSessionPlayerNotFoundError + babase.WidgetNotFoundError, # kWidgetNotFoundError + babase.ActivityNotFoundError, # kActivityNotFoundError + babase.SessionNotFoundError, # kSessionNotFoundError + enums.TimeFormat, # kTimeFormatClass + enums.TimeType, # kTimeTypeClass + enums.InputType, # kInputTypeClass + enums.Permission, # kPermissionClass + enums.SpecialChar, # kSpecialCharClass + _language.Lstr.from_json, # kLstrFromJsonCall + _hooks.uuid_str, # kUUIDStrCall + _hooks.hash_strings, # kHashStringsCall + _hooks.have_account_v2_credentials, # kHaveAccountV2CredentialsCall + _hooks.implicit_sign_in, # kImplicitSignInCall + _hooks.implicit_sign_out, # kImplicitSignOutCall + _hooks.login_adapter_get_sign_in_token_response, # kLoginAdapterGetSignInTokenResponseCall + _hooks.open_url_with_webbrowser_module, # kOpenURLWithWebBrowserModuleCall + _apputils.on_too_many_file_descriptors, # kOnTooManyFileDescriptorsCall +] diff --git a/src/meta/bameta/python_embedded/__init__.py b/src/meta/baclassicmeta/__init__.py similarity index 100% rename from src/meta/bameta/python_embedded/__init__.py rename to src/meta/baclassicmeta/__init__.py diff --git a/src/meta/baclassicmeta/pyembed/__init__.py b/src/meta/baclassicmeta/pyembed/__init__.py new file mode 100644 index 00000000..5463acf0 --- /dev/null +++ b/src/meta/baclassicmeta/pyembed/__init__.py @@ -0,0 +1,3 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Sources used to embed code in the c++ layer.""" diff --git a/src/meta/baclassicmeta/pyembed/binding_classic.py b/src/meta/baclassicmeta/pyembed/binding_classic.py new file mode 100644 index 00000000..5a348200 --- /dev/null +++ b/src/meta/baclassicmeta/pyembed/binding_classic.py @@ -0,0 +1,15 @@ +# Released under the MIT License. See LICENSE for details. +# Where most of our python-c++ binding happens. +# Python objects should be added here along with their associated c++ enum. +# Run make update to update the project after editing this.. +# pylint: disable=missing-module-docstring, line-too-long +from __future__ import annotations + +from baclassic._music import do_play_music +from baclassic._input import get_input_device_mapped_value + +# The C++ layer looks for this variable: +values = [ + do_play_music, # kDoPlayMusicCall + get_input_device_mapped_value, # kGetInputDeviceMappedValueCall +] diff --git a/src/meta/bacoremeta/__init__.py b/src/meta/bacoremeta/__init__.py new file mode 100644 index 00000000..b39b3d0d --- /dev/null +++ b/src/meta/bacoremeta/__init__.py @@ -0,0 +1,3 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Meta source files.""" diff --git a/src/meta/bacoremeta/pyembed/__init__.py b/src/meta/bacoremeta/pyembed/__init__.py new file mode 100644 index 00000000..5463acf0 --- /dev/null +++ b/src/meta/bacoremeta/pyembed/__init__.py @@ -0,0 +1,3 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Sources used to embed code in the c++ layer.""" diff --git a/src/meta/bacoremeta/pyembed/binding_core.py b/src/meta/bacoremeta/pyembed/binding_core.py new file mode 100644 index 00000000..a15e2dda --- /dev/null +++ b/src/meta/bacoremeta/pyembed/binding_core.py @@ -0,0 +1,26 @@ +# Released under the MIT License. See LICENSE for details. +# Where most of our python-c++ binding happens. +# Python objects should be added here along with their associated c++ enum. +# Run make update to update the project after editing this.. +# pylint: disable=missing-module-docstring, line-too-long +from __future__ import annotations + +import json +import copy +import logging +import sys + +# The C++ layer looks for this variable: +values = [ + sys.modules['__main__'].__dict__, # kMainDict + tuple(), # kEmptyTuple + copy.deepcopy, # kDeepCopyCall + copy.copy, # kShallowCopyCall + json.dumps, # kJsonDumpsCall + json.loads, # kJsonLoadsCall + logging.debug, # kLoggingDebugCall + logging.info, # kLoggingInfoCall + logging.warning, # kLoggingWarningCall + logging.error, # kLoggingErrorCall + logging.critical, # kLoggingCriticalCall +] diff --git a/src/meta/bacoremeta/pyembed/env.py b/src/meta/bacoremeta/pyembed/env.py new file mode 100644 index 00000000..2545df49 --- /dev/null +++ b/src/meta/bacoremeta/pyembed/env.py @@ -0,0 +1,56 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Various utility calls for interacting with baenv.""" + +# This code runs in the logic thread to bootstrap ballistica. + +from __future__ import annotations + +from typing import TYPE_CHECKING +import sys + +if TYPE_CHECKING: + import baenv + + +def prepend_sys_path(path: str) -> None: + """Simply add a path to sys paths at the beginning.""" + sys.path.insert(0, path) + + +def import_baenv_and_run_configure( + config_dir: str | None, + data_dir: str | None, + user_python_dir: str | None, + contains_python_dist: bool, +) -> None: + """Import baenv and run its configure method.""" + import baenv + + baenv.configure( + config_dir=config_dir, + data_dir=data_dir, + user_python_dir=user_python_dir, + contains_python_dist=contains_python_dist, + ) + + +def get_env_config() -> baenv.EnvConfig: + """Import baenv and get the config.""" + import baenv + + return baenv.get_config() + + +def on_babase_import() -> None: + """Called when _babase is execing.""" + import baenv + + baenv.on_babase_import() + + +def on_babase_start_app() -> None: + """Called when starting the app.""" + import baenv + + baenv.on_babase_start_app() diff --git a/src/meta/bameta/python_embedded/binding.py b/src/meta/bameta/python_embedded/binding.py deleted file mode 100644 index 00436446..00000000 --- a/src/meta/bameta/python_embedded/binding.py +++ /dev/null @@ -1,150 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# Where most of our python-c++ binding happens. -# Python objects should be added here along with their associated c++ enum. -# Run make update to update the project after editing this.. -# pylint: disable=missing-module-docstring, missing-function-docstring -# pylint: disable=line-too-long -from __future__ import annotations - -import json -import copy -import logging -from typing import TYPE_CHECKING - -import ba -from ba import _language -from ba import _music -from ba import _input -from ba import _apputils -from ba import _dependency -from ba._generated import enums -from ba import _player -from ba import _hooks -import _ba - -# FIXME: There should be no bastd in here; -# should pull in bases from ba which get overridden by bastd (or other). -from bastd.ui.onscreenkeyboard import OnScreenKeyboardWindow - -if TYPE_CHECKING: - from typing import Any - - -def get_binding_values() -> tuple[Any, ...]: - return ( - ba.app, # kApp - tuple(), # kEmptyTuple - _ba.client_info_query_response, # kClientInfoQueryResponseCall - _hooks.reset_to_main_menu, # kResetToMainMenuCall - _hooks.set_config_fullscreen_on, # kSetConfigFullscreenOnCall - _hooks.set_config_fullscreen_off, # kSetConfigFullscreenOffCall - _hooks.not_signed_in_screen_message, # kNotSignedInScreenMessageCall - _hooks.connecting_to_party_message, # kConnectingToPartyMessageCall - _hooks.rejecting_invite_already_in_party_message, # kRejectingInviteAlreadyInPartyMessageCall - _hooks.connection_failed_message, # kConnectionFailedMessageCall - _hooks.temporarily_unavailable_message, # kTemporarilyUnavailableMessageCall - _hooks.in_progress_message, # kInProgressMessageCall - _hooks.error_message, # kErrorMessageCall - _hooks.purchase_not_valid_error, # kPurchaseNotValidErrorCall - _hooks.purchase_already_in_progress_error, # kPurchaseAlreadyInProgressErrorCall - _hooks.gear_vr_controller_warning, # kGearVRControllerWarningCall - _hooks.orientation_reset_cb_message, # kVROrientationResetCBMessageCall - _hooks.orientation_reset_message, # kVROrientationResetMessageCall - _hooks.on_app_resume, # kHandleAppResumeCall - _apputils.handle_v1_cloud_log, # kHandleV1CloudLogCall - _hooks.launch_main_menu_session, # kLaunchMainMenuSessionCall - _hooks.language_test_toggle, # kLanguageTestToggleCall - _hooks.award_in_control_achievement, # kAwardInControlAchievementCall - _hooks.award_dual_wielding_achievement, # kAwardDualWieldingAchievementCall - _apputils.print_corrupt_file_error, # kPrintCorruptFileErrorCall - _hooks.play_gong_sound, # kPlayGongSoundCall - _hooks.launch_coop_game, # kLaunchCoopGameCall - _hooks.purchases_restored_message, # kPurchasesRestoredMessageCall - _hooks.dismiss_wii_remotes_window, # kDismissWiiRemotesWindowCall - _hooks.unavailable_message, # kUnavailableMessageCall - _hooks.submit_analytics_counts, # kSubmitAnalyticsCountsCall - _hooks.set_last_ad_network, # kSetLastAdNetworkCall - _hooks.no_game_circle_message, # kNoGameCircleMessageCall - _hooks.google_play_purchases_not_available_message, # kGooglePlayPurchasesNotAvailableMessageCall - _hooks.google_play_services_not_available_message, # kGooglePlayServicesNotAvailableMessageCall - _hooks.empty_call, # kEmptyCall - _hooks.level_icon_press, # kLevelIconPressCall - _hooks.trophy_icon_press, # kTrophyIconPressCall - _hooks.coin_icon_press, # kCoinIconPressCall - _hooks.ticket_icon_press, # kTicketIconPressCall - _hooks.back_button_press, # kBackButtonPressCall - _hooks.friends_button_press, # kFriendsButtonPressCall - _hooks.print_trace, # kPrintTraceCall - _hooks.toggle_fullscreen, # kToggleFullscreenCall - _hooks.party_icon_activate, # kPartyIconActivateCall - _hooks.read_config, # kReadConfigCall - _hooks.ui_remote_press, # kUIRemotePressCall - _hooks.quit_window, # kQuitWindowCall - _hooks.remove_in_game_ads_message, # kRemoveInGameAdsMessageCall - _hooks.telnet_access_request, # kTelnetAccessRequestCall - _hooks.on_app_pause, # kOnAppPauseCall - _hooks.do_quit, # kQuitCall - _hooks.shutdown, # kShutdownCall - _hooks.gc_disable, # kGCDisableCall - ba.app.accounts_v1.show_post_purchase_message, # kShowPostPurchaseMessageCall - _hooks.device_menu_press, # kDeviceMenuPressCall - _hooks.show_url_window, # kShowURLWindowCall - _hooks.party_invite_revoke, # kHandlePartyInviteRevokeCall - _hooks.filter_chat_message, # kFilterChatMessageCall - _hooks.local_chat_message, # kHandleLocalChatMessageCall - ba.ShouldShatterMessage, # kShouldShatterMessageClass - ba.ImpactDamageMessage, # kImpactDamageMessageClass - ba.PickedUpMessage, # kPickedUpMessageClass - ba.DroppedMessage, # kDroppedMessageClass - ba.OutOfBoundsMessage, # kOutOfBoundsMessageClass - ba.PickUpMessage, # kPickUpMessageClass - ba.DropMessage, # kDropMessageClass - _hooks.finish_bootstrapping, # kFinishBootstrappingCall - _input.get_device_value, # kGetDeviceValueCall - _input.get_last_player_name_from_input_device, # kGetLastPlayerNameFromInputDeviceCall - copy.deepcopy, # kDeepCopyCall - copy.copy, # kShallowCopyCall - ba.Activity, # kActivityClass - ba.Session, # kSessionClass - json.dumps, # kJsonDumpsCall - json.loads, # kJsonLoadsCall - OnScreenKeyboardWindow, # kOnScreenKeyboardClass - _music.do_play_music, # kDoPlayMusicCall - ba.app.handle_deep_link, # kDeepLinkCall - ba.app.lang.get_resource, # kGetResourceCall - ba.app.lang.translate, # kTranslateCall - ba.Lstr, # kLStrClass - ba.Call, # kCallClass - _apputils.garbage_collect_session_end, # kGarbageCollectSessionEndCall - ba.ContextError, # kContextError - ba.NotFoundError, # kNotFoundError - ba.NodeNotFoundError, # kNodeNotFoundError - ba.SessionTeamNotFoundError, # kSessionTeamNotFoundError - ba.InputDeviceNotFoundError, # kInputDeviceNotFoundError - ba.DelegateNotFoundError, # kDelegateNotFoundError - ba.SessionPlayerNotFoundError, # kSessionPlayerNotFoundError - ba.WidgetNotFoundError, # kWidgetNotFoundError - ba.ActivityNotFoundError, # kActivityNotFoundError - ba.SessionNotFoundError, # kSessionNotFoundError - _dependency.AssetPackage, # kAssetPackageClass - enums.TimeFormat, # kTimeFormatClass - enums.TimeType, # kTimeTypeClass - enums.InputType, # kInputTypeClass - enums.Permission, # kPermissionClass - enums.SpecialChar, # kSpecialCharClass - _player.Player, # kPlayerClass - _hooks.get_player_icon, # kGetPlayerIconCall - _language.Lstr.from_json, # kLstrFromJsonCall - _hooks.uuid_str, # kUUIDStrCall - _hooks.hash_strings, # kHashStringsCall - _hooks.have_account_v2_credentials, # kHaveAccountV2CredentialsCall - logging.debug, # kLoggingDebugCall - logging.info, # kLoggingInfoCall - logging.warning, # kLoggingWarningCall - logging.error, # kLoggingErrorCall - logging.critical, # kLoggingCriticalCall - _hooks.implicit_sign_in, # kImplicitSignInCall - _hooks.implicit_sign_out, # kImplicitSignOutCall - _hooks.login_adapter_get_sign_in_token_response, # kLoginAdapterGetSignInTokenResponseCall - _apputils.on_too_many_file_descriptors, # kOnTooManyFileDescriptorsCall - ) # yapf: disable diff --git a/src/meta/bameta/python_embedded/bootstrap.py b/src/meta/bameta/python_embedded/bootstrap.py deleted file mode 100644 index 85d06b15..00000000 --- a/src/meta/bameta/python_embedded/bootstrap.py +++ /dev/null @@ -1,32 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Ballistica bootstrapping.""" - -# This code runs in the logic thread to bootstrap ballistica. - -from __future__ import annotations - -import sys -from typing import TYPE_CHECKING - -import _ba - -if TYPE_CHECKING: - pass - -# All we do here is make our script files accessible and then hand it off -# to them. - -# Let's lookup mods first (so users can do whatever they want). -# and then our bundled scripts last (don't want bundled site-package -# stuff overwriting system versions) -sys.path.insert(0, _ba.env()['python_directory_user']) -sys.path.append(_ba.env()['python_directory_app']) -sys.path.append(_ba.env()['python_directory_app_site']) - -# The import is down here since it won't work until we muck with paths. -# noinspection PyProtectedMember -# pylint: disable=wrong-import-position -from ba._bootstrap import bootstrap - -bootstrap() diff --git a/src/meta/bameta/python_embedded/bootstrap_monolithic.py b/src/meta/bameta/python_embedded/bootstrap_monolithic.py deleted file mode 100644 index d6534a1a..00000000 --- a/src/meta/bameta/python_embedded/bootstrap_monolithic.py +++ /dev/null @@ -1,22 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""Main thread bootstrapping for Python monolithic builds.""" - -# This code runs in the main thread just after the interpreter comes up. -# It should *ONLY* do things that must be done in the main thread and -# should not import any ballistica stuff. - -from __future__ import annotations - -import signal -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - pass - -# Tell Python to not handle SIGINT itself (it normally generates -# KeyboardInterrupts which make a mess; we want to intercept them -# for simple clean exit). We have to do this part here because it must -# run in the main thread. We add our own handler later in the logic thread -# alongside our other ba bootstrapping. -signal.signal(signal.SIGINT, signal.SIG_DFL) # Do default handling. diff --git a/src/meta/bascenev1meta/__init__.py b/src/meta/bascenev1meta/__init__.py new file mode 100644 index 00000000..5463acf0 --- /dev/null +++ b/src/meta/bascenev1meta/__init__.py @@ -0,0 +1,3 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Sources used to embed code in the c++ layer.""" diff --git a/src/meta/bascenev1meta/pyembed/__init__.py b/src/meta/bascenev1meta/pyembed/__init__.py new file mode 100644 index 00000000..5463acf0 --- /dev/null +++ b/src/meta/bascenev1meta/pyembed/__init__.py @@ -0,0 +1,3 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Sources used to embed code in the c++ layer.""" diff --git a/src/meta/bascenev1meta/pyembed/binding_scene_v1.py b/src/meta/bascenev1meta/pyembed/binding_scene_v1.py new file mode 100644 index 00000000..a21f9574 --- /dev/null +++ b/src/meta/bascenev1meta/pyembed/binding_scene_v1.py @@ -0,0 +1,34 @@ +# Released under the MIT License. See LICENSE for details. +# Where most of our python-c++ binding happens. +# Python objects should be added here along with their associated c++ enum. +# Run make update to update the project after editing this.. +# pylint: disable=missing-module-docstring, line-too-long +from __future__ import annotations + +from bascenev1 import _messages +from bascenev1 import _hooks +from bascenev1._player import Player +from bascenev1._dependency import AssetPackage +from bascenev1._activity import Activity +from bascenev1._session import Session +import _bascenev1 + +# The C++ layer looks for this variable: +values = [ + _hooks.launch_main_menu_session, # kLaunchMainMenuSessionCall + _hooks.get_player_icon, # kGetPlayerIconCall + _hooks.filter_chat_message, # kFilterChatMessageCall + _hooks.local_chat_message, # kHandleLocalChatMessageCall + _bascenev1.client_info_query_response, # kClientInfoQueryResponseCall + _messages.ShouldShatterMessage, # kShouldShatterMessageClass + _messages.ImpactDamageMessage, # kImpactDamageMessageClass + _messages.PickedUpMessage, # kPickedUpMessageClass + _messages.DroppedMessage, # kDroppedMessageClass + _messages.OutOfBoundsMessage, # kOutOfBoundsMessageClass + _messages.PickUpMessage, # kPickUpMessageClass + _messages.DropMessage, # kDropMessageClass + Player, # kPlayerClass + AssetPackage, # kAssetPackageClass + Activity, # kActivityClass + Session, # kSceneV1SessionClass +] diff --git a/src/meta/batemplatefsmeta/__init__.py b/src/meta/batemplatefsmeta/__init__.py new file mode 100644 index 00000000..b39b3d0d --- /dev/null +++ b/src/meta/batemplatefsmeta/__init__.py @@ -0,0 +1,3 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Meta source files.""" diff --git a/src/meta/batemplatefsmeta/pyembed/__init__.py b/src/meta/batemplatefsmeta/pyembed/__init__.py new file mode 100644 index 00000000..5463acf0 --- /dev/null +++ b/src/meta/batemplatefsmeta/pyembed/__init__.py @@ -0,0 +1,3 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Sources used to embed code in the c++ layer.""" diff --git a/src/meta/batemplatefsmeta/pyembed/binding_template_fs.py b/src/meta/batemplatefsmeta/pyembed/binding_template_fs.py new file mode 100644 index 00000000..bd90cf3b --- /dev/null +++ b/src/meta/batemplatefsmeta/pyembed/binding_template_fs.py @@ -0,0 +1,14 @@ +# Released under the MIT License. See LICENSE for details. + +# Where most of our python-c++ binding happens. +# Python objects should be added here along with their associated c++ enum. +# Run make update to update the project after editing this.. +# pylint: disable=missing-module-docstring, line-too-long +from __future__ import annotations + +from batemplatefs import _hooks + +# The C++ layer looks for this variable: +values = [ + _hooks.hello_world, # kHelloWorldCall +] diff --git a/src/meta/bauiv1meta/__init__.py b/src/meta/bauiv1meta/__init__.py new file mode 100644 index 00000000..5463acf0 --- /dev/null +++ b/src/meta/bauiv1meta/__init__.py @@ -0,0 +1,3 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Sources used to embed code in the c++ layer.""" diff --git a/src/meta/bauiv1meta/pyembed/__init__.py b/src/meta/bauiv1meta/pyembed/__init__.py new file mode 100644 index 00000000..5463acf0 --- /dev/null +++ b/src/meta/bauiv1meta/pyembed/__init__.py @@ -0,0 +1,3 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Sources used to embed code in the c++ layer.""" diff --git a/src/meta/bauiv1meta/pyembed/binding_ui_v1.py b/src/meta/bauiv1meta/pyembed/binding_ui_v1.py new file mode 100644 index 00000000..120ecb07 --- /dev/null +++ b/src/meta/bauiv1meta/pyembed/binding_ui_v1.py @@ -0,0 +1,25 @@ +# Released under the MIT License. See LICENSE for details. +# This code is used to grab a bunch of Python objects for use in C++. +# Python objects should be added here along with their associated c++ enum. +# Run make update to update the project after editing this. +# pylint: disable=missing-module-docstring, line-too-long +from __future__ import annotations + +import bauiv1.onscreenkeyboard +from bauiv1 import _hooks + +# The C++ layer looks for this variable: +values = [ + bauiv1.onscreenkeyboard.OnScreenKeyboardWindow, # kOnScreenKeyboardClass + _hooks.ticket_icon_press, # kTicketIconPressCall + _hooks.trophy_icon_press, # kTrophyIconPressCall + _hooks.level_icon_press, # kLevelIconPressCall + _hooks.coin_icon_press, # kCoinIconPressCall + _hooks.empty_call, # kEmptyCall + _hooks.back_button_press, # kBackButtonPressCall + _hooks.friends_button_press, # kFriendsButtonPressCall + _hooks.party_icon_activate, # kPartyIconActivateCall + _hooks.quit_window, # kQuitWindowCall + _hooks.device_menu_press, # kDeviceMenuPressCall + _hooks.show_url_window, # kShowURLWindowCall +] diff --git a/src/resources/Makefile b/src/resources/Makefile new file mode 100644 index 00000000..3f4f1d57 --- /dev/null +++ b/src/resources/Makefile @@ -0,0 +1,48 @@ +# Released under the MIT License. See LICENSE for details. + +all: resources + +PROJ_DIR = ../.. +TOOLS_DIR = $(PROJ_DIR)/tools +BUILD_DIR = $(PROJ_DIR)/build/resources + +# In general our clean target here has to be more surgical than most; +# these resources generally get installed to exact platform-specific places +# so our clean can't simply blow away a dir and be done with it. +# (see the individual clean-XXX sub-targets). +# We *do* have it blow away BUILD_DIR for good measure, but that's only a +# bit of our overall output. +clean: + rm -rf $(BUILD_DIR) + +# This section is generated by batools.resourcesmakefile; do not edit by hand. +# __AUTOGENERATED_PUBLIC_BEGIN__ +# __AUTOGENERATED_PUBLIC_END__ + + +# This section is generated by batools.resourcesmakefile; do not edit by hand. +# __AUTOGENERATED_PRIVATE_BEGIN__ + +# Add this section's targets to the overall resources target. +resources: \ + $(PROJ_DIR)/ballisticakit-windows/Generic/BallisticaKit.ico + +clean-private: + rm -f "$(PROJ_DIR)/ballisticakit-windows/Generic/BallisticaKit.ico" + +# Include this section in an overall clean. +clean: clean-private + +$(PROJ_DIR)/ballisticakit-windows/Generic/BallisticaKit.ico : ../../.efrocachemap + @cd ../.. && tools/pcommand efrocache_get src/resources/$@ + +efrocache-list: + @echo "$(PROJ_DIR)/ballisticakit-windows/Generic/BallisticaKit.ico" + +efrocache-build: resources + +# __AUTOGENERATED_PRIVATE_END__ + +# These targets don't correspond to actual files; make sure make knows that. +.PHONY: resources clean clean-private clean-private-internal efrocache-list \ + efrocache-build diff --git a/resources/README.md b/src/resources/README.md similarity index 86% rename from resources/README.md rename to src/resources/README.md index 2d7fafc6..392ea3d7 100644 --- a/resources/README.md +++ b/src/resources/README.md @@ -1,4 +1,4 @@ -# BallisticaCore Resources +# BallisticaKit Resources This directory should contain sources for a common set of resources such as icons, launch screens, etc. Run 'make' to compile and push them to their platform-specific locations. diff --git a/tests/test_ba/__init__.py b/tests/test_babase/__init__.py similarity index 100% rename from tests/test_ba/__init__.py rename to tests/test_babase/__init__.py diff --git a/tests/test_ba/test_assetmanager.py b/tests/test_babase/test_assetmanager.py similarity index 91% rename from tests/test_ba/test_assetmanager.py rename to tests/test_babase/test_assetmanager.py index b9304901..ff1b6cf9 100644 --- a/tests/test_ba/test_assetmanager.py +++ b/tests/test_babase/test_assetmanager.py @@ -9,8 +9,6 @@ import weakref import tempfile from pathlib import Path -# noinspection PyProtectedMember -from ba._assetmanager import AssetManager from bacommon.assets import AssetPackageFlavor @@ -25,8 +23,10 @@ def test_assetmanager() -> None: # Disabling for now... if bool(False): - with tempfile.TemporaryDirectory() as tmpdir: + # noinspection PyProtectedMember + from babase._assetmanager import AssetManager + with tempfile.TemporaryDirectory() as tmpdir: manager = AssetManager(rootdir=Path(tmpdir)) wref = weakref.ref(manager) manager.start() diff --git a/tests/test_efro/test_message.py b/tests/test_efro/test_message.py index 1a944041..f52c24db 100644 --- a/tests/test_efro/test_message.py +++ b/tests/test_efro/test_message.py @@ -499,7 +499,6 @@ def test_protocol_creation() -> None: def test_sender_module_single_emb() -> None: - """Test generation of protocol-specific sender modules for typing/etc.""" # NOTE: Ideally we should be testing efro.message.create_sender_module() # here, but it requires us to pass code which imports this test module diff --git a/tests/test_efro/test_rpc.py b/tests/test_efro/test_rpc.py index aa5a795a..cf388c0b 100644 --- a/tests/test_efro/test_rpc.py +++ b/tests/test_efro/test_rpc.py @@ -244,7 +244,6 @@ class _Tester: raise RuntimeError(f'{name} did not go down cleanly') async def _run(self, testcall: Awaitable[None]) -> None: - # Give server a chance to spin up before kicking off client. await self.server.start() @@ -286,7 +285,6 @@ def test_keepalive_fail() -> None: tester = _Tester(keepalive_interval=kinterval, keepalive_timeout=ktimeout) async def _do_it() -> None: - # Tell our client to not send keepalives. tester.client.endpoint.test_suppress_keepalives = True @@ -311,7 +309,6 @@ def test_keepalive_success() -> None: tester = _Tester(keepalive_interval=kinterval, keepalive_timeout=ktimeout) async def _do_it() -> None: - # Sleep just past the keepalive timeout and make sure the endpoint # is NOT going down await asyncio.sleep(ktimeout * 1.25) @@ -325,7 +322,6 @@ def test_simple_messages() -> None: tester = _Tester() async def _do_it() -> None: - # Send some messages both directions and make sure we get the expected # response types. @@ -365,7 +361,6 @@ def test_simultaneous_messages() -> None: tester = _Tester() async def _do_it() -> None: - # Send a bunch of messages both ways at once and make sure # they all come through simultaneously-ish. starttime = time.monotonic() @@ -395,7 +390,6 @@ def test_message_timeout() -> None: tester = _Tester() async def _do_it() -> None: - # This message should return after a short wait. resp = await tester.server.send_message( _Message(_MessageType.TEST_SLOW) diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 00000000..72587db3 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,8 @@ +# Ballistica Tools + +Code in this directory is used to build or test Ballistica projects but +(in most cases) is not available *from* a running Ballistica app. + +Note that there are exceptions to this rule; some of the Python packages +here such as 'efro' or 'bacommon' are bundled with builds so can be used +from tools *and* app code. diff --git a/tools/bacloud b/tools/bacloud index 4eeab191..d074af6a 100755 --- a/tools/bacloud +++ b/tools/bacloud @@ -1,4 +1,4 @@ -#!/usr/bin/env python3.10 +#!/usr/bin/env python3.11 # Released under the MIT License. See LICENSE for details. # """A tool for interacting with ballistica's cloud services. @@ -256,7 +256,6 @@ class App: import base64 for fname, fdata in downloads_inline.items(): - # If there's a directory where we want our file to go, clear it # out first. File deletes should have run before this so # everything under it should be empty and thus killable via rmdir. diff --git a/tools/bacommon/transfer.py b/tools/bacommon/transfer.py index c0b44926..f6704b26 100644 --- a/tools/bacommon/transfer.py +++ b/tools/bacommon/transfer.py @@ -59,7 +59,7 @@ class DirectoryManifest: sha = hashlib.sha256() fullfilepath = os.path.join(pathstr, filepath) if not os.path.isfile(fullfilepath): - raise Exception(f'File not found: "{fullfilepath}"') + raise RuntimeError(f'File not found: "{fullfilepath}".') with open(fullfilepath, 'rb') as infile: filebytes = infile.read() filesize = len(filebytes) diff --git a/tools/batools/androidsdkutils.py b/tools/batools/androidsdkutils.py index 6f57b86f..fdf044bd 100755 --- a/tools/batools/androidsdkutils.py +++ b/tools/batools/androidsdkutils.py @@ -27,7 +27,6 @@ def _parse_lprop_file(local_properties_path: str) -> str: def _gen_lprop_file(local_properties_path: str) -> str: - os.makedirs(os.path.dirname(local_properties_path), exist_ok=True) # Ok, we've got no local.properties file; attempt to find @@ -101,7 +100,7 @@ def run(projroot: str, args: list[str]) -> None: # In all cases we make sure there's a local.properties in our android # dir that contains valid sdk path. If not, we attempt to create it. local_properties_path = os.path.join( - projroot, 'ballisticacore-android', 'local.properties' + projroot, 'ballisticakit-android', 'local.properties' ) if os.path.isfile(local_properties_path): sdk_dir = _parse_lprop_file(local_properties_path) @@ -138,7 +137,7 @@ def run(projroot: str, args: list[str]) -> None: # as external python builds still ask for this. So now we just pull it from # the project gradle file where we set it explicitly. if command == 'get-ndk-path': - gradlepath = Path(projroot, 'ballisticacore-android/build.gradle') + gradlepath = Path(projroot, 'ballisticakit-android/build.gradle') with gradlepath.open(encoding='utf-8') as infile: lines = [ l diff --git a/tools/batools/assetsmakefile.py b/tools/batools/assetsmakefile.py index 0be5a773..b4d65c92 100755 --- a/tools/batools/assetsmakefile.py +++ b/tools/batools/assetsmakefile.py @@ -1,24 +1,25 @@ # Released under the MIT License. See LICENSE for details. # -"""Updates assets/Makefile based on source assets present.""" +"""Updates src/assets/Makefile based on source assets present.""" from __future__ import annotations import json import os -import sys from typing import TYPE_CHECKING -from efro.terminal import Clr - if TYPE_CHECKING: pass -# Note: code below needs updating when Python version changes (currently 3.10) -PYC_SUFFIX = '.cpython-310.opt-1.pyc' +# Note: code below needs updating when Python version changes (currently 3.11) +PYC_SUFFIX = '.cpython-311.opt-1.pyc' + +ASSETS_SRC = 'src/assets' +BUILD_DIR = 'build/assets' def _get_targets( + projroot: str, varname: str, inext: str, outext: str, @@ -26,22 +27,24 @@ def _get_targets( limit_to_prefix: str | None = None, ) -> str: """Generic function to map source extension to dst files.""" + # pylint: disable=too-many-locals - src = 'assets/src' - dst = 'assets/build' + src = ASSETS_SRC + dst = BUILD_DIR targets = [] # Create outext targets for all inext files we find. - for root, _dname, fnames in os.walk(src): + for root, _dname, fnames in os.walk(os.path.join(projroot, src)): + src_abs = os.path.join(projroot, src) if limit_to_prefix is not None and not root.startswith( - os.path.join(src, limit_to_prefix) + os.path.join(src_abs, limit_to_prefix) ): continue - # Write the target to make sense from within assets/ - assert root.startswith(src) - dstrootvar = 'build' + root[len(src) :] - dstfin = dst + root[len(src) :] + # Write the target to make sense from within src/assets/ + assert root.startswith(src_abs) + dstrootvar = '$(BUILD_DIR)' + root.removeprefix(src_abs) + dstfin = dst + root.removeprefix(src_abs) for fname in fnames: outname = fname[: -len(inext)] + outext if fname.endswith(inext): @@ -52,6 +55,8 @@ def _get_targets( def _get_py_targets( + projroot: str, + meta_manifests: dict[str, str], src: str, dst: str, py_targets: list[str], @@ -60,47 +65,51 @@ def _get_py_targets( subset: str, ) -> None: # pylint: disable=too-many-branches + # pylint: disable=too-many-locals - py_generated_root = 'assets/src/ba_data/python/ba/_generated' + py_generated_root = f'{ASSETS_SRC}/ba_data/python/babase/_mgen' - def _do_get_targets(root: str, fnames: list[str]) -> None: + def _do_get_targets(proot: str, fnames: list[str]) -> None: # Special case: ignore temp py files in data src. - if root == 'assets/src/ba_data/data/maps': + if proot == f'{ASSETS_SRC}/ba_data/data/maps': return - assert root.startswith(src) - dstrootvar = dst[len('assets') + 1 :] + root[len(src) :] - dstfin = dst + root[len(src) :] + assert proot.startswith(src) + assert dst.startswith(BUILD_DIR) + dstrootvar = ( + '$(BUILD_DIR)' + + dst.removeprefix(BUILD_DIR) + + proot.removeprefix(src) + ) + dstfin = dst + proot[len(src) :] for fname in fnames: - - # Ignore flycheck temp files as well as our _ba dummy module. + # Ignore non-python files and flycheck/emacs temp files. if ( not fname.endswith('.py') or fname.startswith('flycheck_') or fname.startswith('.#') - or fname == '_ba.py' ): continue - if root.startswith('assets/src/ba_data/python-site-packages'): + if proot.startswith(f'{ASSETS_SRC}/ba_data/python-site-packages'): in_subset = 'private-common' - elif root.startswith('assets/src/ba_data') or root.startswith( - 'assets/src/server' + elif proot.startswith(f'{ASSETS_SRC}/ba_data') or proot.startswith( + f'{ASSETS_SRC}/server' ): in_subset = 'public' - elif root.startswith('tools/efro') and not root.startswith( + elif proot.startswith('tools/efro') and not proot.startswith( 'tools/efrotools' ): # We want to pull just 'efro' out of tools; not efrotools. in_subset = 'public_tools' - elif root.startswith('tools/bacommon'): + elif proot.startswith('tools/bacommon'): in_subset = 'public_tools' - elif root.startswith('assets/src/windows/x64'): + elif proot.startswith(f'{ASSETS_SRC}/windows/x64'): in_subset = 'private-windows-x64' - elif root.startswith('assets/src/windows/Win32'): + elif proot.startswith(f'{ASSETS_SRC}/windows/Win32'): in_subset = 'private-windows-Win32' - elif root.startswith('assets/src/pylib-apple'): + elif proot.startswith(f'{ASSETS_SRC}/pylib-apple'): in_subset = 'private-apple' - elif root.startswith('assets/src/pylib-android'): + elif proot.startswith(f'{ASSETS_SRC}/pylib-android'): in_subset = 'private-android' else: in_subset = 'private-common' @@ -125,25 +134,34 @@ def _get_py_targets( # Create py and pyc targets for all physical scripts in src, with # the exception of our dynamically generated stuff. - for physical_root, _dname, physical_fnames in os.walk(src): - + for physical_root, _dname, physical_fnames in os.walk( + os.path.join(projroot, src) + ): # Skip any generated files; we'll add those from the meta manifest. # (dont want our results to require a meta build beforehand) - if physical_root == py_generated_root or physical_root.startswith( - py_generated_root + '/' + if physical_root == os.path.join( + projroot, py_generated_root + ) or physical_root.startswith( + os.path.join(projroot, py_generated_root) + '/' ): continue - _do_get_targets(physical_root, physical_fnames) + _do_get_targets( + physical_root.removeprefix(projroot + '/'), physical_fnames + ) # Now create targets for any of our dynamically generated stuff that # lives under this dir. meta_targets: list[str] = [] - for mantype in ['public', 'private']: - with open( - f'src/meta/.meta_manifest_{mantype}.json', encoding='utf-8' - ) as infile: - meta_targets += json.loads(infile.read()) + for manifest in meta_manifests.values(): + # Sanity check; make sure meta system is giving actual paths; + # no accidental makefile vars. + if '$' in manifest: + raise RuntimeError( + 'meta-manifest value contains a $; probably a bug.' + ) + meta_targets += json.loads(manifest) + meta_targets = [ t for t in meta_targets @@ -152,28 +170,39 @@ def _get_py_targets( for target in meta_targets: _do_get_targets( - root=os.path.dirname(target), fnames=[os.path.basename(target)] + proot=os.path.dirname(target), fnames=[os.path.basename(target)] ) def _get_py_targets_subset( - all_targets: set[str], subset: str, suffix: str + projroot: str, + meta_manifests: dict[str, str], + all_targets: set[str], + subset: str, + suffix: str, ) -> str: if subset == 'public_tools': src = 'tools' - dst = 'assets/build/ba_data/python' - copyrule = 'build/ba_data/python/%.py : ../tools/%.py' + dst = f'{BUILD_DIR}/ba_data/python' + copyrule = '$(BUILD_DIR)/ba_data/python/%.py : $(TOOLS_DIR)/%.py' else: - src = 'assets/src' - dst = 'assets/build' - copyrule = 'build/%.py : src/%.py' + src = ASSETS_SRC + dst = BUILD_DIR + copyrule = '$(BUILD_DIR)/%.py : %.py' # Separate these into '1' and '2'. py_targets: list[str] = [] pyc_targets: list[str] = [] _get_py_targets( - src, dst, py_targets, pyc_targets, all_targets, subset=subset + projroot, + meta_manifests, + src, + dst, + py_targets, + pyc_targets, + all_targets, + subset=subset, ) # Need to sort these combined to keep pairs together. @@ -204,7 +233,7 @@ def _get_py_targets_subset( '# (and make non-writable so I\'m less likely to ' 'accidentally edit them there)\n' f'{efc}$(SCRIPT_TARGETS_PY{suffix}) : {copyrule}\n' - '\t@echo Copying script: $@\n' + '\t@echo Copying script: $(subst $(BUILD_DIR)/,,$@)\n' '\t@mkdir -p $(dir $@)\n' '\t@rm -f $@\n' '\t@cp $^ $@\n' @@ -238,7 +267,7 @@ def _get_py_targets_subset( + target + ': \\\n ' + py_targets[i] - + '\n\t@echo Compiling script: $^\n' + + '\n\t@echo Compiling script: $(subst $(BUILD_DIR),,$^)\n' '\t@rm -rf $@ && PYTHONHASHSEED=1 $(TOOLS_DIR)/pcommand' ' compile_python_files $^' ' && chmod 444 $@\n' @@ -247,15 +276,18 @@ def _get_py_targets_subset( return out -def _get_extras_targets_win(all_targets: set[str], platform: str) -> str: +def _get_extras_targets_win( + projroot: str, all_targets: set[str], platform: str +) -> str: targets: list[str] = [] - base = 'assets/src/windows' - dstbase = 'build/windows' - for root, _dnames, fnames in os.walk(base): + base = f'{ASSETS_SRC}/windows' + dstbase = 'windows' + for root, _dnames, fnames in os.walk(os.path.join(projroot, base)): for fname in fnames: - # Only include the platform we were passed. - if not root.startswith('assets/src/windows/' + platform): + if not root.startswith( + os.path.join(projroot, f'{ASSETS_SRC}/windows/{platform}') + ): continue ext = os.path.splitext(fname)[-1] @@ -295,9 +327,15 @@ def _get_extras_targets_win(all_targets: set[str], platform: str) -> str: 'command_template', 'fetch_macholib', ]: - targetpath = os.path.join(dstbase + root[len(base) :], fname) - targets.append(targetpath) - all_targets.add('assets/' + targetpath) + base_abs = os.path.join(projroot, base) + assert root.startswith(base_abs) + targetpath = os.path.join( + dstbase + root.removeprefix(base_abs), fname + ) + # print(f'DSTBASE {dstbase} ROOT {root} + # TARGETPATH {targetpath}') + targets.append('$(BUILD_DIR)/' + targetpath) + all_targets.add(BUILD_DIR + '/' + targetpath) continue # Complain if something new shows up instead of blindly @@ -314,9 +352,9 @@ def _get_extras_targets_win(all_targets: set[str], platform: str) -> str: out += ( '\n# Rule to copy src extras to build.\n' f'# __EFROCACHE_TARGET__\n' - f'$(EXTRAS_TARGETS_WIN_{p_up}) : build/% :' - ' src/%\n' - '\t@echo Copying file: $@\n' + f'$(EXTRAS_TARGETS_WIN_{p_up}) : $(BUILD_DIR)/% :' + ' %\n' + '\t@echo Copying file: $(subst $(BUILD_DIR)/,,$@)\n' '\t@mkdir -p $(dir $@)\n' '\t@rm -f $@\n' '\t@cp $^ $@\n' @@ -325,21 +363,23 @@ def _get_extras_targets_win(all_targets: set[str], platform: str) -> str: return out -def update_assets_makefile(projroot: str, check: bool) -> None: +def generate_assets_makefile( + projroot: str, + fname: str, + existing_data: str, + meta_manifests: dict[str, str], +) -> dict[str, str]: """Main script entry point.""" # pylint: disable=too-many-locals from efrotools import getconfig from pathlib import Path - # Always operate out of dist root dir. - os.chdir(projroot) - - public = getconfig(Path('.'))['public'] + public = getconfig(Path(projroot))['public'] assert isinstance(public, bool) - fname = 'assets/Makefile' - with open(fname, encoding='utf-8') as infile: - original = infile.read() + # with open(fname, encoding='utf-8') as infile: + # original = infile.read() + original = existing_data lines = original.splitlines() auto_start_public = lines.index('# __AUTOGENERATED_PUBLIC_BEGIN__') @@ -353,10 +393,18 @@ def update_assets_makefile(projroot: str, check: bool) -> None: # We always auto-generate the public section. our_lines_public = [ _get_py_targets_subset( - all_targets_public, subset='public', suffix='_PUBLIC' + projroot, + meta_manifests, + all_targets_public, + subset='public', + suffix='_PUBLIC', ), _get_py_targets_subset( - all_targets_public, subset='public_tools', suffix='_PUBLIC_TOOLS' + projroot, + meta_manifests, + all_targets_public, + subset='public_tools', + suffix='_PUBLIC_TOOLS', ), ] @@ -366,65 +414,105 @@ def update_assets_makefile(projroot: str, check: bool) -> None: else: our_lines_private = [ _get_py_targets_subset( + projroot, + meta_manifests, all_targets_private, subset='private-apple', suffix='_PRIVATE_APPLE', ), _get_py_targets_subset( + projroot, + meta_manifests, all_targets_private, subset='private-android', suffix='_PRIVATE_ANDROID', ), _get_py_targets_subset( + projroot, + meta_manifests, all_targets_private, subset='private-common', suffix='_PRIVATE_COMMON', ), _get_py_targets_subset( + projroot, + meta_manifests, all_targets_private, subset='private-windows-Win32', suffix='_PRIVATE_WIN_WIN32', ), _get_py_targets_subset( + projroot, + meta_manifests, all_targets_private, subset='private-windows-x64', suffix='_PRIVATE_WIN_X64', ), _get_targets( - 'COB_TARGETS', '.collidemodel.obj', '.cob', all_targets_private + projroot, + 'COB_TARGETS', + '.collisionmesh.obj', + '.cob', + all_targets_private, ), _get_targets( - 'BOB_TARGETS', '.model.obj', '.bob', all_targets_private + projroot, + 'BOB_TARGETS', + '.mesh.obj', + '.bob', + all_targets_private, ), _get_targets( - 'FONT_TARGETS', '.fdata', '.fdata', all_targets_private + projroot, + 'FONT_TARGETS', + '.fdata', + '.fdata', + all_targets_private, ), - _get_targets('PEM_TARGETS', '.pem', '.pem', all_targets_private), _get_targets( + projroot, 'PEM_TARGETS', '.pem', '.pem', all_targets_private + ), + _get_targets( + projroot, 'DATA_TARGETS', '.json', '.json', all_targets_private, limit_to_prefix='ba_data/data', ), - _get_targets('AUDIO_TARGETS', '.wav', '.ogg', all_targets_private), _get_targets( - 'TEX2D_DDS_TARGETS', '.tex2d.png', '.dds', all_targets_private + projroot, 'AUDIO_TARGETS', '.wav', '.ogg', all_targets_private ), _get_targets( - 'TEX2D_PVR_TARGETS', '.tex2d.png', '.pvr', all_targets_private + projroot, + 'TEX2D_DDS_TARGETS', + '.tex2d.png', + '.dds', + all_targets_private, ), _get_targets( - 'TEX2D_KTX_TARGETS', '.tex2d.png', '.ktx', all_targets_private + projroot, + 'TEX2D_PVR_TARGETS', + '.tex2d.png', + '.pvr', + all_targets_private, ), _get_targets( + projroot, + 'TEX2D_KTX_TARGETS', + '.tex2d.png', + '.ktx', + all_targets_private, + ), + _get_targets( + projroot, 'TEX2D_PREVIEW_PNG_TARGETS', '.tex2d.png', '_preview.png', all_targets_private, ), - _get_extras_targets_win(all_targets_private, 'Win32'), - _get_extras_targets_win(all_targets_private, 'x64'), + _get_extras_targets_win(projroot, all_targets_private, 'Win32'), + _get_extras_targets_win(projroot, all_targets_private, 'x64'), ] filtered = ( lines[: auto_start_public + 1] @@ -433,60 +521,27 @@ def update_assets_makefile(projroot: str, check: bool) -> None: + our_lines_private + lines[auto_end_private:] ) + out_files: dict[str, str] = {} + out = '\n'.join(filtered) + '\n' - if out == original: - print(f'{fname} is up to date.') - else: - if check: - print(f"{Clr.SRED}ERROR: file is out of date: '{fname}'.{Clr.RST}") + out_files[fname] = out - # Print exact contents if we need to debug: - if bool(False): - print( - f'EXPECTED ===========================================\n' - f'{out}\n' - f'FOUND ==============================================\n' - f'{original}\n' - f'END COMPARE ========================================' - ) - sys.exit(255) - print(f'{Clr.SBLU}Updating: {fname}{Clr.RST}') - with open(fname, 'w', encoding='utf-8') as outfile: - outfile.write(out) - - # Lastly, write a simple manifest of the things we expect to have - # in build. We can use this to clear out orphaned files as part of builds. - _write_manifest( - 'assets/.asset_manifest_public.json', all_targets_public, check + # Write a simple manifest of the things we expect to have in build. + # We can use this to clear out orphaned files as part of builds. + out_files['src/assets/.asset_manifest_public.json'] = _gen_manifest( + all_targets_public ) if not public: - _write_manifest( - 'assets/.asset_manifest_private.json', all_targets_private, check + out_files['src/assets/.asset_manifest_private.json'] = _gen_manifest( + all_targets_private ) + return out_files -def _write_manifest( - manifest_path: str, all_targets: set[str], check: bool -) -> None: +def _gen_manifest(all_targets: set[str]) -> str: # Lastly, write a simple manifest of the things we expect to have # in build. We can use this to clear out orphaned files as part of builds. - assert all(t.startswith('assets/build/') for t in all_targets) - if not os.path.exists(manifest_path): - existing_manifest = None - else: - with open(manifest_path, encoding='utf-8') as infile: - existing_manifest = json.loads(infile.read()) + assert all(t.startswith(BUILD_DIR) for t in all_targets) manifest = sorted(t[13:] for t in all_targets) - if manifest == existing_manifest: - print(f'{manifest_path} is up to date.') - else: - if check: - print( - f'{Clr.SRED}ERROR: file is out of date:' - f" '{manifest_path}'.{Clr.RST}" - ) - sys.exit(255) - print(f'{Clr.SBLU}Updating: {manifest_path}{Clr.RST}') - with open(manifest_path, 'w', encoding='utf-8') as outfile: - outfile.write(json.dumps(manifest, indent=1)) + return json.dumps(manifest, indent=1) diff --git a/tools/batools/assetstaging.py b/tools/batools/assetstaging.py index 7d6e542c..d68bb7b7 100755 --- a/tools/batools/assetstaging.py +++ b/tools/batools/assetstaging.py @@ -30,15 +30,15 @@ class Config: def __init__(self, projroot: str) -> None: self.projroot = projroot # We always calc src relative to this script. - self.src = self.projroot + '/assets/build' + self.src = self.projroot + '/build/assets' self.dst: str | None = None self.serverdst: str | None = None self.win_extras_src: str | None = None self.win_platform: str | None = None self.win_type: str | None = None self.include_audio = True - self.include_models = True - self.include_collide_models = True + self.include_meshes = True + self.include_collision_meshes = True self.include_scripts = True self.include_python = True self.include_textures = True @@ -61,8 +61,8 @@ class Config: self.include_payload_file = True self.tex_suffix = '.ktx' self.include_audio = False - self.include_models = False - self.include_collide_models = False + self.include_meshes = False + self.include_collision_meshes = False self.include_scripts = False self.include_python = False self.include_textures = False @@ -72,8 +72,8 @@ class Config: for arg in args: if arg == '-full': self.include_audio = True - self.include_models = True - self.include_collide_models = True + self.include_meshes = True + self.include_collision_meshes = True self.include_scripts = True self.include_python = True self.include_textures = True @@ -83,9 +83,9 @@ class Config: self.include_pylib = True elif arg == '-none': pass - elif arg == '-models': - self.include_models = True - self.include_collide_models = True + elif arg == '-meshes': + self.include_meshes = True + self.include_collision_meshes = True elif arg == '-python': self.include_python = True self.include_pylib = True @@ -113,14 +113,14 @@ class Config: self.serverdst = args[-1] self.include_textures = False self.include_audio = False - self.include_models = False + self.include_meshes = False else: raise RuntimeError(f'Invalid wintype: "{wintype}"') if winplt == 'Win32': - self.win_extras_src = self.projroot + '/assets/build/windows/Win32' + self.win_extras_src = self.projroot + '/build/assets/windows/Win32' elif winplt == 'x64': - self.win_extras_src = self.projroot + '/assets/build/windows/x64' + self.win_extras_src = self.projroot + '/build/assets/windows/x64' else: raise RuntimeError(f'Invalid winplt: "{winplt}"') @@ -148,7 +148,7 @@ class Config: self.serverdst = args[-1] self.include_textures = False self.include_audio = False - self.include_models = False + self.include_meshes = False # Require either -debug or -release in args. # FIXME: should require this for all platforms for consistency. @@ -162,7 +162,7 @@ class Config: "Expected either '-debug' or '-release' in args." ) elif '-xcode-mac' in args: - self.src = os.environ['SOURCE_ROOT'] + '/assets/build' + self.src = os.environ['SOURCE_ROOT'] + '/build/assets' self.dst = ( os.environ['TARGET_BUILD_DIR'] + '/' @@ -172,7 +172,7 @@ class Config: self.pylib_src_name = 'pylib-apple' self.tex_suffix = '.dds' elif '-xcode-ios' in args: - self.src = os.environ['SOURCE_ROOT'] + '/assets/build' + self.src = os.environ['SOURCE_ROOT'] + '/build/assets' self.dst = ( os.environ['TARGET_BUILD_DIR'] + '/' @@ -289,8 +289,8 @@ def _sync_windows_extras(cfg: Config) -> None: # We could technically copy everything over but this keeps staging # dirs a bit tidier. dbgsfx = '_d' if cfg.debug else '' - # Note: Below needs updating when Python version changes (currently 3.10) - toplevelfiles: list[str] = [f'python310{dbgsfx}.dll'] + # Note: Below needs updating when Python version changes (currently 3.11) + toplevelfiles: list[str] = [f'python311{dbgsfx}.dll'] if cfg.win_type == 'win': toplevelfiles += [ @@ -383,10 +383,10 @@ def _sync_standard_game_data(cfg: Config) -> None: if cfg.include_json: cmd += " --include '*.json'" - if cfg.include_models: + if cfg.include_meshes: cmd += " --include '*.bob'" - if cfg.include_collide_models: + if cfg.include_collision_meshes: cmd += " --include '*.cob'" cmd += ( @@ -408,32 +408,32 @@ def _sync_server_files(cfg: Config) -> None: stage_server_file( projroot=cfg.projroot, mode=modeval, - infilename=f'{cfg.src}/../src/server/ballisticacore_server.py', + infilename=f'{cfg.projroot}/src/assets/server/ballisticakit_server.py', outfilename=os.path.join( cfg.serverdst, - 'ballisticacore_server.py' + 'ballisticakit_server.py' if cfg.win_type is not None - else 'ballisticacore_server', + else 'ballisticakit_server', ), ) stage_server_file( projroot=cfg.projroot, mode=modeval, - infilename=f'{cfg.src}/../src/server/README.txt', + infilename=f'{cfg.projroot}/src/assets/server/README.txt', outfilename=os.path.join(cfg.serverdst, 'README.txt'), ) stage_server_file( projroot=cfg.projroot, mode=modeval, - infilename=f'{cfg.src}/../src/server/config_template.yaml', + infilename=f'{cfg.projroot}/src/assets/server/config_template.yaml', outfilename=os.path.join(cfg.serverdst, 'config_template.yaml'), ) if cfg.win_type is not None: - fname = 'launch_ballisticacore_server.bat' + fname = 'launch_ballisticakit_server.bat' stage_server_file( projroot=cfg.projroot, mode=modeval, - infilename=f'{cfg.src}/../src/server/{fname}', + infilename=f'{cfg.projroot}/src/assets/server/{fname}', outfilename=os.path.join(cfg.serverdst, fname), ) @@ -480,7 +480,7 @@ def stage_server_file( batools.build.filter_server_config(str(projroot), infilename), ) - elif basename == 'ballisticacore_server.py': + elif basename == 'ballisticakit_server.py': # Run Python in opt mode for release builds. with open(infilename, encoding='utf-8') as infile: lines = infile.read().splitlines() @@ -497,7 +497,7 @@ def stage_server_file( with open(infilename, encoding='utf-8') as infile: readme = infile.read() _write_if_changed(outfilename, readme) - elif basename == 'launch_ballisticacore_server.bat': + elif basename == 'launch_ballisticakit_server.bat': # Run Python in opt mode for release builds. with open(infilename, encoding='utf-8') as infile: lines = infile.read().splitlines() @@ -510,15 +510,15 @@ def stage_server_file( ) lines[2] = replace_exact( lines[2], - 'dist\\\\python.exe ballisticacore_server.py', - 'dist\\\\python.exe -O ballisticacore_server.py', + 'dist\\\\python.exe ballisticakit_server.py', + 'dist\\\\python.exe -O ballisticakit_server.py', ) else: # In debug mode we use the bundled debug interpreter. lines[2] = replace_exact( lines[2], - 'dist\\\\python.exe ballisticacore_server.py', - 'dist\\\\python_d.exe ballisticacore_server.py', + 'dist\\\\python.exe ballisticakit_server.py', + 'dist\\\\python_d.exe ballisticakit_server.py', ) _write_if_changed(outfilename, '\n'.join(lines) + '\n') diff --git a/tools/batools/build.py b/tools/batools/build.py index dd8e6ace..2163f87a 100644 --- a/tools/batools/build.py +++ b/tools/batools/build.py @@ -1,6 +1,9 @@ # Released under the MIT License. See LICENSE for details. # """General functionality related to running builds.""" + +# pylint: disable=too-many-lines + from __future__ import annotations import os @@ -9,12 +12,11 @@ import datetime import subprocess from enum import Enum from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, assert_never -from efro.util import assert_never from efro.error import CleanError from efro.terminal import Clr -from efrotools.build import Lazybuild +from efrotools.lazybuild import LazyBuildContext if TYPE_CHECKING: from typing import Sequence, Any @@ -40,23 +42,24 @@ class PyRequirement: # as manually-installed bits, pip itself must have some way to allow for # that, right?... PY_REQUIREMENTS = [ - PyRequirement(modulename='pylint', minversion=[2, 15, 9]), - PyRequirement(modulename='mypy', minversion=[0, 991]), + PyRequirement(modulename='pylint', minversion=[2, 17, 3]), + PyRequirement(modulename='mypy', minversion=[1, 2, 0]), PyRequirement(modulename='cpplint', minversion=[1, 6, 1]), - PyRequirement(modulename='pytest', minversion=[7, 2, 0]), + PyRequirement(modulename='pytest', minversion=[7, 3, 1]), PyRequirement(modulename='pytz'), PyRequirement(modulename='ansiwrap'), PyRequirement(modulename='yaml', pipname='PyYAML'), PyRequirement(modulename='requests'), PyRequirement(modulename='pdoc'), - PyRequirement(pipname='black', minversion=[22, 12, 0]), - PyRequirement(pipname='typing_extensions', minversion=[4, 4, 0]), + PyRequirement(pipname='black', minversion=[23, 3, 0]), + PyRequirement(pipname='typing_extensions', minversion=[4, 5, 0]), PyRequirement(pipname='types-filelock', minversion=[3, 2, 7]), - PyRequirement(pipname='types-requests', minversion=[2, 28, 11, 7]), - PyRequirement(pipname='types-pytz', minversion=[2022, 2, 1, 0]), - PyRequirement(pipname='types-PyYAML', minversion=[6, 0, 12, 2]), + PyRequirement(pipname='types-requests', minversion=[2, 28, 11, 17]), + PyRequirement(pipname='types-pytz', minversion=[2023, 3, 0, 0]), + PyRequirement(pipname='types-PyYAML', minversion=[6, 0, 12, 9]), PyRequirement(pipname='certifi', minversion=[2022, 12, 7]), PyRequirement(pipname='types-certifi', minversion=[2021, 10, 8, 3]), + PyRequirement(pipname='pbxproj', minversion=[3, 5, 0]), ] # Parts of full-tests suite we only run on particular days. @@ -80,7 +83,7 @@ SPARSE_TEST_BUILDS: list[list[str]] = [ # Currently only doing sparse-tests in core; not spinoffs. # (whole word will get subbed out in spinoffs so this will be false) -DO_SPARSE_TEST_BUILDS = 'ballistica' + 'core' == 'ballisticacore' +DO_SPARSE_TEST_BUILDS = 'ballistica' + 'kit' == 'ballisticakit' class PrefabTarget(Enum): @@ -96,6 +99,7 @@ class LazyBuildCategory(Enum): """Types of sources.""" RESOURCES = 'resources_src' + ASSETS = 'assets_src' META = 'meta_src' CMAKE = 'cmake_src' WIN = 'win_src' @@ -106,35 +110,52 @@ def lazybuild(target: str, category: LazyBuildCategory, command: str) -> None: # Everything possibly affecting meta builds. if category is LazyBuildCategory.META: - Lazybuild( + LazyBuildContext( target=target, + command=command, + # Since this category can kick off cleans and blow things away, + # its not safe to have multiple builds going with it at once. + buildlockname=category.value, + # Regular paths; changes to these will trigger meta build. srcpaths=[ 'Makefile', 'src/meta', - 'src/ballistica/core/types.h', + 'src/ballistica/shared/foundation/types.h', ], - command=command, # Our meta Makefile targets generally don't list tools scripts # that can affect their creation as sources, so let's set up - # a catch-all here: when any of our tools stuff changes let's - # blow away any existing meta builds. + # a catch-all here: when any of our tools stuff changes we'll + # blow away all existing meta builds. + # Update: also including featureset-defs here; any time we're + # mucking with those it's good to start fresh to be sure. srcpaths_fullclean=[ 'tools/efrotools', 'tools/efrotoolsinternal', 'tools/batools', 'tools/batoolsinternal', + 'config/featuresets', ], + # Also enable an option to maintain a hash of all file paths + # in the above sets and do a full-clean whenever that changes. + # Takes care of orphaned files if remove a featureset/etc. + manifest_file='.cache/lazybuild/meta_manifest', command_fullclean='make meta-clean', ).run() # Everything possibly affecting CMake builds. elif category is LazyBuildCategory.CMAKE: - Lazybuild( + LazyBuildContext( target=target, + # It should be safe to have multiple cmake build going at + # once I think; different targets should never stomp on each + # other. Actually if anything maybe we'd want to plug target + # path into this to watch for the same target getting built + # redundantly? + buildlockname=None, srcpaths=[ 'Makefile', 'src', - 'ballisticacore-cmake/CMakeLists.txt', + 'ballisticakit-cmake/CMakeLists.txt', ], dirfilter=( lambda root, dirname: not ( @@ -154,13 +175,18 @@ def lazybuild(target: str, category: LazyBuildCategory, command: str) -> None: return False return True - Lazybuild( + LazyBuildContext( target=target, + # It should be safe to have multiple of these build going at + # once I think; different targets should never stomp on each + # other. Actually if anything maybe we'd want to plug target + # path into this to watch for the same target getting built + # redundantly? + buildlockname=None, srcpaths=[ 'Makefile', 'src', - 'resources/src', - 'ballisticacore-windows', + 'ballisticakit-windows', ], dirfilter=_win_dirfilter, command=command, @@ -168,13 +194,32 @@ def lazybuild(target: str, category: LazyBuildCategory, command: str) -> None: # Everything possibly affecting resource builds. elif category is LazyBuildCategory.RESOURCES: - Lazybuild( + LazyBuildContext( target=target, + # Even though this category currently doesn't run any + # clean commands, going to restrict to one use at a time for + # now in case we want to add that. + buildlockname=category.value, srcpaths=[ 'Makefile', 'tools/pcommand', - 'resources/src', - 'resources/Makefile', + 'src/resources', + ], + command=command, + ).run() + + # Everything possibly affecting asset builds. + elif category is LazyBuildCategory.ASSETS: + LazyBuildContext( + target=target, + # Even though this category currently doesn't run any + # clean commands, going to restrict to one use at a time for + # now in case we want to add that. + buildlockname=category.value, + srcpaths=[ + 'Makefile', + 'tools', + 'src/assets', ], command=command, ).run() @@ -249,7 +294,7 @@ def gen_fulltest_buildfile_android() -> None: lines = [] for _i, flavor in enumerate( - sorted(os.listdir('ballisticacore-android/BallisticaCore/src')) + sorted(os.listdir('ballisticakit-android/BallisticaKit/src')) ): if flavor == 'main' or flavor.startswith('.'): continue @@ -271,7 +316,7 @@ def gen_fulltest_buildfile_android() -> None: if DO_SPARSE_TEST_BUILDS: extras = SPARSE_TEST_BUILDS[dayoffset % len(SPARSE_TEST_BUILDS)] extras = [e for e in extras if e.startswith('android.')] - cspre = 'tools/cloudshell linbeast --env android --' + cspre = 'tools/cloudshell linbeast --env ba-android-alldeps --' # This is currently broken; turning off. # Update: should be working again; hooray! @@ -324,7 +369,7 @@ def gen_fulltest_buildfile_android() -> None: f' python_build_android_debug x86_64' ) elif extra == 'android.package': - lines.append('make android-cloud-package') + lines.append('make android-package-cloud') else: raise RuntimeError(f'Unknown extra: {extra}') @@ -403,8 +448,8 @@ def gen_fulltest_buildfile_apple() -> None: ) # iOS stuff - lines.append('make ios-build') - lines.append('make ios-new-build') + lines.append('make ios-cloud-build') + lines.append('make ios-new-cloud-build') if DO_SPARSE_TEST_BUILDS: extras = SPARSE_TEST_BUILDS[dayoffset % len(SPARSE_TEST_BUILDS)] extras = [e for e in extras if e.startswith('ios.')] @@ -417,7 +462,7 @@ def gen_fulltest_buildfile_apple() -> None: raise RuntimeError(f'Unknown extra: {extra}') # tvOS stuff - lines.append('make tvos-build') + lines.append('make tvos-cloud-build') if DO_SPARSE_TEST_BUILDS: extras = SPARSE_TEST_BUILDS[dayoffset % len(SPARSE_TEST_BUILDS)] extras = [e for e in extras if e.startswith('tvos.')] @@ -430,21 +475,21 @@ def gen_fulltest_buildfile_apple() -> None: raise RuntimeError(f'Unknown extra: {extra}') # macOS stuff - lines.append('make mac-build') + lines.append('make mac-cloud-build') # (throw release build in the mix to hopefully catch opt-mode-only errors). - lines.append('make mac-appstore-release-build') - lines.append('make mac-new-build') - lines.append('make cmake-server-build') - lines.append('make cmake-build') + lines.append('MAC_CONFIGURATION=Release make mac-appstore-cloud-build') + lines.append('make mac-new-cloud-build') + lines.append('CMAKE_CLOUDSHELL_HOST=fromini make cmake-cloud-server-build') + lines.append('CMAKE_CLOUDSHELL_HOST=fromini make cmake-cloud-build') if DO_SPARSE_TEST_BUILDS: extras = SPARSE_TEST_BUILDS[dayoffset % len(SPARSE_TEST_BUILDS)] extras = [e for e in extras if e.startswith('mac.')] for extra in extras: if extra == 'mac.package': - # FIXME; Currently skipping notarization because it requires us - # to be logged in via the gui to succeed. lines.append( - 'BA_MAC_DISK_IMAGE_SKIP_NOTARIZATION=1 make mac-package' + 'BA_MAC_DISK_IMAGE_SKIP_NOTARIZATION=1' + ' make mac-package-cloud' + # 'make mac-package-cloud' ) elif extra == 'mac.package.server.x86_64': lines.append('make mac-server-package-x86-64') @@ -568,7 +613,28 @@ def checkenv() -> None: 'rsync is required; please install it via apt, brew, etc.' ) - # Make sure they've got our target python version. + # Make sure rsync is version 3.1.0 or newer. + # Macs come with ancient rsync versions with significant downsides such + # as single second file mod time resolution which has started to cause + # problems with build setups. So now am trying to make sure my Macs + # have an up-to-date one installed (via homebrew). + rsyncver = tuple( + int(s) + for s in subprocess.run( + ['rsync', '--version'], check=True, capture_output=True + ) + .stdout.decode() + .splitlines()[0] + .split()[2] + .split('.')[:2] + ) + if rsyncver < (3, 1): + raise CleanError( + 'rsync version 3.1 or greater not found;' + ' please install it via apt, brew, etc.' + ) + + # Make sure they've got our target Python version. if ( subprocess.run( ['which', PYTHON_BIN], check=False, capture_output=True diff --git a/tools/batools/docs.py b/tools/batools/docs.py index 2f52d755..c8104fbb 100755 --- a/tools/batools/docs.py +++ b/tools/batools/docs.py @@ -106,7 +106,14 @@ def generate(projroot: str) -> None: pdoc.render.configure( search=True, show_source=True, template_directory=templatesdir ) - pdoc.pdoc('ba', 'bastd', output_directory=outdirname) + pdoc.pdoc( + 'babase', + 'bastd', + 'baclassic', + 'bascenev1', + 'bauiv1', + output_directory=outdirname, + ) except Exception as exc: import traceback diff --git a/tools/batools/dummymodule.py b/tools/batools/dummymodule.py index 36148fbc..a1161f06 100755 --- a/tools/batools/dummymodule.py +++ b/tools/batools/dummymodule.py @@ -1,6 +1,7 @@ # Released under the MIT License. See LICENSE for details. # -"""Generates a dummy _ba.py and _bainternal.py based on binary modules. +# pylint: disable=too-many-lines +"""Generates dummy .py modules based on binary modules. This allows us to use code introspection tools such as pylint without spinning up the engine, and also allows external scripts to import game scripts @@ -10,14 +11,15 @@ successfully (albeit with limited functionality). from __future__ import annotations import os -import sys + +# import sys +import types import textwrap import subprocess from typing import TYPE_CHECKING from efro.error import CleanError from efro.terminal import Clr -from efrotools import get_files_hash if TYPE_CHECKING: from types import ModuleType @@ -54,80 +56,52 @@ def _get_varying_func_info(sig_in: str) -> tuple[str, str]: '# Show that our return type varies based on "doraise" value:\n' '@overload\n' 'def getinputdevice(name: str, unique_id: str,' - ' doraise: Literal[True] = True) -> ba.InputDevice:\n' + ' doraise: Literal[True] = True) -> bascenev1.InputDevice:\n' ' ...\n' '\n' '@overload\n' 'def getinputdevice(name: str, unique_id: str,' - ' doraise: Literal[False]) -> ba.InputDevice | None:\n' + ' doraise: Literal[False]) -> bascenev1.InputDevice | None:\n' ' ...\n' '\n' 'def getinputdevice(name: str, unique_id: str,' - ' doraise: bool=True) -> Any:' - ) - elif sig_in == ( - 'time(timetype: ba.TimeType = TimeType.SIM,' - ' timeformat: ba.TimeFormat = TimeFormat.SECONDS)' - ' -> ' - ): - sig = ( - '# Overloads to return a type based on requested format.\n' - '\n' - '@overload\n' - 'def time(timetype: ba.TimeType = TimeType.SIM,\n' - ' timeformat: Literal[TimeFormat.SECONDS]' - ' = TimeFormat.SECONDS) -> float:\n' - ' ...\n' - '\n' - '# This "*"' - ' keyword-only hack lets us accept 1 arg' - ' (timeformat=MILLISECS) forms.\n' - '@overload\n' - 'def time(timetype: ba.TimeType = TimeType.SIM, *,\n' - ' timeformat: Literal[TimeFormat.MILLISECONDS]) -> int:\n' - ' ...\n' - '\n' - '@overload\n' - 'def time(timetype: ba.TimeType,\n' - ' timeformat: Literal[TimeFormat.MILLISECONDS]) -> int:\n' - ' ...\n' - '\n' - '\n' - 'def time(timetype: ba.TimeType = TimeType.SIM,\n' - ' timeformat: ba.TimeFormat = TimeFormat.SECONDS)' - ' -> Any:\n' + ' doraise: bool=True) -> Any:\n' ) elif sig_in == 'getactivity(doraise: bool = True) -> ': sig = ( '# Show that our return type varies based on "doraise" value:\n' '@overload\n' - 'def getactivity(doraise: Literal[True] = True) -> ba.Activity:\n' + 'def getactivity(doraise: Literal[True] = True) ->' + ' bascenev1.Activity:\n' ' ...\n' '\n' '\n' '@overload\n' 'def getactivity(doraise: Literal[False])' - ' -> ba.Activity | None:\n' + ' -> bascenev1.Activity | None:\n' ' ...\n' '\n' '\n' - 'def getactivity(doraise: bool = True) -> ba.Activity | None:' + 'def getactivity(doraise: bool = True)' + ' -> bascenev1.Activity | None:\n' ) elif sig_in == 'getsession(doraise: bool = True) -> ': sig = ( '# Show that our return type varies based on "doraise" value:\n' '@overload\n' - 'def getsession(doraise: Literal[True] = True) -> ba.Session:\n' + 'def getsession(doraise: Literal[True] = True) ->' + ' bascenev1.Session:\n' ' ...\n' '\n' '\n' '@overload\n' 'def getsession(doraise: Literal[False])' - ' -> ba.Session | None:\n' + ' -> bascenev1.Session | None:\n' ' ...\n' '\n' '\n' - 'def getsession(doraise: bool = True) -> ba.Session | None:' + 'def getsession(doraise: bool = True)' + ' -> bascenev1.Session | None:\n' ) else: @@ -151,43 +125,56 @@ def _writefuncs( spcstr = '\n' * spacing indstr = ' ' * indent for funcname in funcnames: - # Skip some that are not in public builds. if funcname in {'master_hash_dump'}: continue func = getattr(parent, funcname) + + # Classmethods on classes have type BuiltinMethodType instead + # of the usual MethodDescriptorType; treat them special. + is_classmethod = isinstance( + getattr(parent, funcname), types.BuiltinMethodType + ) and isinstance(parent, type) + docstr = func.__doc__ # We expect an empty line and take everything before that to be # the function signature. if '\n\n' not in docstr: - raise Exception(f'docstr missing empty line: {func}') - sig = docstr.split('\n\n')[0].replace('\n', ' ').strip() + raise RuntimeError(f'docstr missing empty line: {func}') + sig = docstr.split('\n\n', maxsplit=1)[0].replace('\n', ' ').strip() - # Sanity check - make sure name is in the sig. - if funcname + '(' not in sig: - raise Exception(f'func name not found in sig for {funcname}') + # Make sure supplied signature matches the filtered function name. + if not sig.startswith(f'{funcname}('): + raise RuntimeError( + f'Expected signature for function {funcname} to start' + f" with '{funcname}'." + ) # If these are methods, add self. if as_method: + slf = 'cls' if is_classmethod else 'self' if funcname + '()' in sig: - sig = sig.replace(funcname + '()', funcname + '(self)') + sig = sig.replace(f'{funcname}()', f'{funcname}({slf})') else: - sig = sig.replace(funcname + '(', funcname + '(self, ') + sig = sig.replace(f'{funcname}(', f'{funcname}({slf}, ') # We expect sig to have a -> denoting return type. if ' -> ' not in sig: - raise Exception(f'no "->" found in docstr for {funcname}') + raise RuntimeError(f'no "->" found in docstr for {funcname}') returns = sig.split('->')[-1].strip() # Some functions don't have simple signatures; we need to hard-code # those here with overloads and whatnot. if '' in sig: overloadsigs, returnstr = _get_varying_func_info(sig) - defsline = textwrap.indent(overloadsigs, indstr) + defslines = textwrap.indent(overloadsigs, indstr) else: - defsline = f'{indstr}def {sig}:\n' + defslines = f'{indstr}def {sig}:\n' + + if is_classmethod: + defslines = f'{indstr}@classmethod\n{defslines}' # Types can be strings for forward-declaration cases. if (returns[0] == "'" and returns[-1] == "'") or ( @@ -196,43 +183,74 @@ def _writefuncs( returns = returns[1:-1] if returns == 'None': returnstr = 'return None' - elif returns == 'ba.Lstr': + elif returns == 'babase.Lstr': returnstr = ( - 'import ba # pylint: disable=cyclic-import\n' - "return ba.Lstr(value='')" + 'import babase # pylint: disable=cyclic-import\n' + "return babase.Lstr(value='')" ) - elif returns in {'ba.Activity', 'ba.Activity | None'}: + elif returns == 'babase.AppTime': returnstr = ( - 'import ba # pylint: disable=cyclic-import\nreturn ' - + 'ba.Activity(settings={})' + 'import babase # pylint: disable=cyclic-import\n' + 'return babase.AppTime(0.0)' ) - elif returns in {'ba.Session', 'ba.Session | None'}: + elif returns == 'bascenev1.BaseTime': returnstr = ( - 'import ba # pylint: disable=cyclic-import\nreturn ' - + 'ba.Session([])' + 'import bascenev1 # pylint: disable=cyclic-import\n' + 'return bascenev1.BaseTime(0.0)' ) - elif returns == 'ba.SessionPlayer | None': + elif returns == 'bascenev1.Time': returnstr = ( - 'import ba # pylint: disable=cyclic-import\n' - 'return ba.SessionPlayer()' + 'import bascenev1 # pylint: disable=cyclic-import\n' + 'return bascenev1.Time(0.0)' ) - elif returns == 'ba.Player | None': + elif returns == 'babase.DisplayTime': returnstr = ( - 'import ba # pylint: disable=cyclic-import\n' - 'return ba.Player()' + 'import babase # pylint: disable=cyclic-import\n' + 'return babase.DisplayTime(0.0)' ) - elif returns.startswith('ba.') and ' | None' not in returns: - - # We cant import ba at module level so let's + elif returns in {'bascenev1.Activity', 'bascenev1.Activity | None'}: + returnstr = ( + 'import bascenev1 # pylint: disable=cyclic-import\nreturn ' + + 'bascenev1.Activity(settings={})' + ) + elif returns in {'bascenev1.Session', 'bascenev1.Session | None'}: + returnstr = ( + 'import bascenev1 # pylint: disable=cyclic-import\nreturn ' + + 'bascenev1.Session([])' + ) + elif returns == 'bascenev1.SessionPlayer | None': + returnstr = ( + 'import bascenev1 # pylint: disable=cyclic-import\n' + 'return bascenev1.SessionPlayer()' + ) + elif returns == 'bascenev1.Player | None': + returnstr = ( + 'import bascenev1 # pylint: disable=cyclic-import\n' + 'return bascenev1.Player()' + ) + elif returns.startswith('babase.') and ' | None' not in returns: + # We cant import babase at module level so let's # do it within funcs as needed. returnstr = ( - 'import ba # pylint: disable=cyclic-import\nreturn ' - + returns - + '()' + f'import babase # pylint: disable=cyclic-import\n' + f'return {returns}()' + ) + elif returns.startswith('bascenev1.') and ' | None' not in returns: + # We cant import babase at module level so let's + # do it within funcs as needed. + returnstr = ( + f'import bascenev1 # pylint: disable=cyclic-import\n' + f'return {returns}()' + ) + elif returns.startswith('bauiv1.') and ' | None' not in returns: + # We cant import babase at module level so let's + # do it within funcs as needed. + returnstr = ( + f'import bauiv1 # pylint: disable=cyclic-import\n' + f'return {returns}()' ) elif returns in {'object', 'Any'}: - # We use 'object' when we mean "can vary" # don't want pylint making assumptions in this case. returnstr = 'return _uninferrable()' @@ -242,12 +260,12 @@ def _writefuncs( returnstr = "return ''" elif returns == 'tuple[float, float, float, float]': returnstr = 'return (0.0, 0.0, 0.0, 0.0)' - elif returns == 'ba.Widget | None': - returnstr = 'return Widget()' - elif returns == 'ba.InputDevice | None': + elif returns == 'bauiv1.Widget | None': + returnstr = 'import bauiv1\nreturn bauiv1.Widget()' + elif returns == 'bascenev1.InputDevice | None': returnstr = 'return InputDevice()' - elif returns == 'list[ba.Widget]': - returnstr = 'return [Widget()]' + elif returns == 'list[bauiv1.Widget]': + returnstr = 'import bauiv1\nreturn [bauiv1.Widget()]' elif returns == 'tuple[float, ...]': returnstr = 'return (0.0, 0.0, 0.0)' elif returns == 'list[str]': @@ -267,7 +285,7 @@ def _writefuncs( 'appconfig.AppConfig', }: returnstr = ( - 'from ba import ' + 'from babase import ' + returns.split('.')[0] + '; return ' + returns @@ -288,16 +306,18 @@ def _writefuncs( 'InputDevice', 'Sound', 'Texture', - 'Model', - 'CollideModel', + 'Mesh', + 'CollisionMesh', + 'SimpleSound', 'team.Team', 'Vec3', 'Widget', 'Node', + 'ContextRef', ]: returnstr = 'return ' + returns + '()' else: - raise Exception( + raise RuntimeError( f'unknown returns value: {returns} for {funcname}' ) returnspc = indstr + ' ' @@ -305,7 +325,7 @@ def _writefuncs( docstr_out = _formatdoc( _filterdoc(docstr, funcname=funcname), indent + 4 ) - out += spcstr + defsline + docstr_out + f'{returnspc}{returnstr}\n' + out += spcstr + defslines + docstr_out + f'{returnspc}{returnstr}\n' return out @@ -391,7 +411,7 @@ def _special_class_cases(classname: str) -> str: '\n' ' # Note attributes:\n' ' # NOTE: I\'m just adding *all* possible node attrs here\n' - ' # now now since we have a single ba.Node type; in the\n' + ' # now now since we have a single bascenev1.Node type; in the\n' ' # future I hope to create proper individual classes\n' ' # corresponding to different node types with correct\n' ' # attributes per node-type.\n' @@ -406,9 +426,9 @@ def _special_class_cases(classname: str) -> str: ' name_color: Sequence[float] = (0.0, 0.0, 0.0)\n' ' tint_color: Sequence[float] = (0.0, 0.0, 0.0)\n' ' tint2_color: Sequence[float] = (0.0, 0.0, 0.0)\n' - " text: ba.Lstr | str = ''\n" - ' texture: ba.Texture | None = None\n' - ' tint_texture: ba.Texture | None = None\n' + " text: babase.Lstr | str = ''\n" + ' texture: bascenev1.Texture | None = None\n' + ' tint_texture: bascenev1.Texture | None = None\n' ' times: Sequence[int] = (1,2,3,4,5)\n' ' values: Sequence[float] = (1.0, 2.0, 3.0, 4.0)\n' ' offset: float = 0.0\n' @@ -424,20 +444,20 @@ def _special_class_cases(classname: str) -> str: ' time2: int = 0\n' ' timemax: int = 0\n' ' client_only: bool = False\n' - ' materials: Sequence[Material] = ()\n' - ' roller_materials: Sequence[Material] = ()\n' + ' materials: Sequence[bascenev1.Material] = ()\n' + ' roller_materials: Sequence[bascenev1.Material] = ()\n' " name: str = ''\n" - ' punch_materials: Sequence[ba.Material] = ()\n' - ' pickup_materials: Sequence[ba.Material] = ()\n' - ' extras_material: Sequence[ba.Material] = ()\n' + ' punch_materials: Sequence[bascenev1.Material] = ()\n' + ' pickup_materials: Sequence[bascenev1.Material] = ()\n' + ' extras_material: Sequence[bascenev1.Material] = ()\n' ' rotate: float = 0.0\n' - ' hold_node: ba.Node | None = None\n' + ' hold_node: bascenev1.Node | None = None\n' ' hold_body: int = 0\n' ' host_only: bool = False\n' ' premultiplied: bool = False\n' - ' source_player: ba.Player | None = None\n' - ' model_opaque: ba.Model | None = None\n' - ' model_transparent: ba.Model | None = None\n' + ' source_player: bascenev1.Player | None = None\n' + ' mesh_opaque: bascenev1.Mesh | None = None\n' + ' mesh_transparent: bascenev1.Mesh | None = None\n' ' damage_smoothed: float = 0.0\n' ' gravity_scale: float = 1.0\n' ' punch_power: float = 0.0\n' @@ -469,13 +489,13 @@ def _special_class_cases(classname: str) -> str: ' music_count: int = 0\n' ' hurt: float = 0.0\n' ' always_show_health_bar: bool = False\n' - ' mini_billboard_1_texture: ba.Texture | None = None\n' + ' mini_billboard_1_texture: bascenev1.Texture | None = None\n' ' mini_billboard_1_start_time: int = 0\n' ' mini_billboard_1_end_time: int = 0\n' - ' mini_billboard_2_texture: ba.Texture | None = None\n' + ' mini_billboard_2_texture: bascenev1.Texture | None = None\n' ' mini_billboard_2_start_time: int = 0\n' ' mini_billboard_2_end_time: int = 0\n' - ' mini_billboard_3_texture: ba.Texture | None = None\n' + ' mini_billboard_3_texture: bascenev1.Texture | None = None\n' ' mini_billboard_3_start_time: int = 0\n' ' mini_billboard_3_end_time: int = 0\n' ' boxing_gloves_flashing: bool = False\n' @@ -496,9 +516,9 @@ def _special_class_cases(classname: str) -> str: ' = (-1, -1, -1, 1, 1, 1)\n' ' shadow_range: Sequence[float] = (0, 0, 0, 0)\n' " counter_text: str = ''\n" - ' counter_texture: ba.Texture | None = None\n' + ' counter_texture: bascenev1.Texture | None = None\n' ' shattered: int = 0\n' - ' billboard_texture: ba.Texture | None = None\n' + ' billboard_texture: bascenev1.Texture | None = None\n' ' billboard_cross_out: bool = False\n' ' billboard_opacity: float = 0.0\n' ' slow_motion: bool = False\n' @@ -511,9 +531,19 @@ def _special_class_cases(classname: str) -> str: ' tint: Sequence[float] = (1.0, 1.0, 1.0)\n' ) + # Special case: ContextCall needs to be callable. + if classname in ['ContextCall']: + out += ( + '\n' + ' def __call__(self) -> None:\n' + ' """Support for calling."""\n' + ' pass\n' + ) + # Special case: need to be able to use the 'with' statement # on some classes. - if classname in ['Context']: + # TODO - determine these cases programmatically if possible. + if classname in ['Context', 'ContextRef']: out += ( '\n' ' def __enter__(self) -> None:\n' @@ -525,6 +555,23 @@ def _special_class_cases(classname: str) -> str: ' """Support for "with" statement."""\n' ' pass\n' ) + + # Define bool functionality for classes that support it internally. + # (lets mypy know these are safe to use in bool evaluation) + # TODO - determine these cases programmatically if possible. + if classname in [ + 'Widget', + 'Node', + 'InputDevice', + 'SessionPlayer', + ]: + out += ( + '\n' + ' def __bool__(self) -> bool:\n' + ' """Support for bool evaluation."""\n' + ' return bool(True) # Slight obfuscation.\n' + ) + return out @@ -550,7 +597,7 @@ def _filterdoc(docstr: str, funcname: str | None = None) -> str: for i, line in enumerate(docslines): if line.strip() in ['Attrs:', 'Attributes:']: if attributes_line is not None: - raise Exception("Multiple 'Attributes:' lines found") + raise RuntimeError("Multiple 'Attributes:' lines found") attributes_line = i if not line.strip(): empty_lines_count += 1 @@ -581,14 +628,13 @@ def _formatdoc( docslines = docstr.splitlines() if len(docslines) == 1: - out += '\n' + indentstr + '"""' + docslines[0] + '"""\n' + out += indentstr + '"""' + docslines[0] + '"""\n' else: for i, line in enumerate(docslines): if i != 0 and line != '': docslines[i] = indentstr + inner_indent_str + line out += ( - '\n' - + indentstr + indentstr + '"""' + '\n'.join(docslines) + ('' if no_end_newline else '\n' + indentstr) @@ -599,14 +645,15 @@ def _formatdoc( def _writeclasses(module: ModuleType, classnames: Sequence[str]) -> str: # pylint: disable=too-many-branches - import types + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements from batools.docs import parse_docs_attrs out = '' for classname in classnames: cls = getattr(module, classname) if cls is None: - raise Exception('unexpected') + raise RuntimeError('unexpected') out += '\n' '\n' # Special case: @@ -629,22 +676,23 @@ def _writeclasses(module: ModuleType, classnames: Sequence[str]) -> str: and not docstr.splitlines()[0].endswith('.') and docstr != '(internal)' ): - # Ok.. looks like the first line is a signature. # Make sure we've got a signature followed by a blank line. if '\n\n' not in docstr: - raise Exception( + raise RuntimeError( f'Constructor docstr missing empty line for {cls}.' ) sig = docstr.split('\n\n')[0].replace('\n', ' ').strip() - # Sanity check - make sure name is in the sig. - if classname + '(' not in sig: - raise Exception( - f'Class name not found in constructor sig for {cls}.' + # Make sure supplied signature matches the filtered class name. + if not sig.startswith(classname + '('): + raise RuntimeError( + f'Expected constructor signature for class {classname}' + f" to start with '{classname}'." ) + sig = classname + sig.removeprefix(classname) sig = sig.replace(classname + '(', '__init__(self, ') - out += ' def ' + sig + ':\n pass\n' + out += ' def ' + sig + ' -> None:\n pass\n' has_constructor = True # Scan its doc-string for attribute info; drop in typed @@ -664,7 +712,7 @@ def _writeclasses(module: ModuleType, classnames: Sequence[str]) -> str: ) out += '\n' else: - raise Exception( + raise RuntimeError( f'Found untyped attr in' f' {classname} docs: {attr.name}' ) @@ -677,8 +725,14 @@ def _writeclasses(module: ModuleType, classnames: Sequence[str]) -> str: for entry in (e for e in dir(cls) if not e.startswith('__')): if isinstance(getattr(cls, entry), types.MethodDescriptorType): funcnames.append(entry) + elif isinstance(getattr(cls, entry), types.BuiltinMethodType): + # We get this for classmethods + funcnames.append(entry) else: - raise Exception(f'Unhandled obj {entry} in {cls}') + entrytype = type(getattr(cls, entry)) + raise RuntimeError( + f'Unhandled obj \'{entry}\' in {cls} (type {entrytype})' + ) funcnames.sort() functxt = _writefuncs( cls, funcnames, indent=4, spacing=1, as_method=True @@ -691,245 +745,221 @@ def _writeclasses(module: ModuleType, classnames: Sequence[str]) -> str: return out -def generate(mname: str, sources_hash: str, outfilename: str) -> None: - """Run the actual generation from within the game.""" - # pylint: disable=too-many-locals - import types +class Generator: + """Context for a module generation pass.""" - from efrotools import get_public_license - from efrotools.code import format_python_str + def __init__(self, modulename: str, outfilename: str): + self.mname = modulename + self.outfilename = outfilename - module = __import__(mname) + def run(self) -> None: + """Run the actual generation from within the app context.""" + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements - funcnames = [] - classnames = [] - for entry in (e for e in dir(module) if not e.startswith('__')): - if isinstance(getattr(module, entry), types.BuiltinFunctionType): - funcnames.append(entry) - elif isinstance(getattr(module, entry), type): - classnames.append(entry) - elif mname == '_ba' and entry == 'app': - # Ignore _ba.app. - continue - else: - raise Exception( - f'found unknown obj {entry}, {getattr(module, entry)}' + from efrotools import get_public_license + from efrotools.code import format_python_str + + module = __import__(self.mname) + + funcnames = [] + classnames = [] + for entry in (e for e in dir(module) if not e.startswith('__')): + if isinstance(getattr(module, entry), types.BuiltinFunctionType): + funcnames.append(entry) + elif isinstance(getattr(module, entry), type): + classnames.append(entry) + elif self.mname == '_babase' and entry == 'app': + # Ignore _babase.app. + continue + elif entry == '_ba_feature_set_data': + # Ignore the C++ data we stuff into our feature-set modules. + continue + elif entry == '_REACHED_END_OF_MODULE': + # Ignore this marker we use to debug import ordering. + continue + else: + raise RuntimeError( + f'found unknown obj {entry}, {getattr(module, entry)}' + ) + funcnames.sort() + classnames.sort() + typing_imports = ( + 'TYPE_CHECKING, overload, Sequence, TypeVar' + if self.mname == '_babase' + else 'TYPE_CHECKING, overload, TypeVar' + if self.mname == '_bascenev1' + else 'TYPE_CHECKING, TypeVar' + ) + typing_imports_tc = ( + 'Any, Callable, Literal' + if self.mname == '_babase' + else 'Any, Callable, Literal, Sequence' + if self.mname == '_bascenev1' + else 'Any, Callable, Literal, Sequence' + if self.mname == '_bauiv1' + else 'Any, Callable' + ) + tc_import_lines_extra = '' + if self.mname == '_babase': + tc_import_lines_extra += ( + ' from babase._app import App\n import babase\n' ) - funcnames.sort() - classnames.sort() - typing_imports = ( - 'TYPE_CHECKING, overload, Sequence, TypeVar' - if mname == '_ba' - else 'TYPE_CHECKING, TypeVar' - ) - typing_imports_tc = ( - 'Any, Callable, Literal' if mname == '_ba' else 'Any, Callable' - ) - tc_import_lines_extra = ( - ' from ba._app import App\n' ' import ba\n' - if mname == '_ba' - else '' - ) - app_declare_lines = 'app: App\n' '\n' if mname == '_ba' else '' - enum_import_lines = ( - 'from ba._generated.enums import TimeFormat, TimeType\n' '\n' - if mname == '_ba' - else '' - ) - out = ( - get_public_license('python') + '\n' - '#\n' - f'"""A dummy stub module for the real {mname}.\n' - '\n' - f'The real {mname} is a compiled extension module' - ' and only available\n' - 'in the live engine. This dummy-module allows Pylint/Mypy/etc. to\n' - 'function reasonably well outside of that environment.\n' - '\n' - 'Make sure this file is never included' - ' in dirs seen by the engine!\n' - '\n' - 'In the future perhaps this can be a stub (.pyi) file, but' - ' we will need\n' - 'to make sure that it works with all our tools' - ' (mypy, pylint, pycharm).\n' - '\n' - 'NOTE: This file was autogenerated by ' + __name__ + '; ' - 'do not edit by hand.\n' - '"""\n' - '\n' - # '# (hash we can use to see if this file is out of date)\n' - # '# SOURCES_HASH='+sources_hash+'\n' - # '\n' - '# I\'m sorry Pylint. I know this file saddens you. Be strong.\n' - '# pylint: disable=useless-suppression\n' - '# pylint: disable=unnecessary-pass\n' - '# pylint: disable=use-dict-literal\n' - '# pylint: disable=use-list-literal\n' - '# pylint: disable=unused-argument\n' - '# pylint: disable=missing-docstring\n' - '# pylint: disable=too-many-locals\n' - '# pylint: disable=redefined-builtin\n' - '# pylint: disable=too-many-lines\n' - '# pylint: disable=redefined-outer-name\n' - '# pylint: disable=invalid-name\n' - '# pylint: disable=no-value-for-parameter\n' - '\n' - 'from __future__ import annotations\n' - '\n' - f'from typing import {typing_imports}\n' - '\n' - f'{enum_import_lines}' - 'if TYPE_CHECKING:\n' - f' from typing import {typing_imports_tc}\n' - f'{tc_import_lines_extra}' - '\n' - '\n' - "_T = TypeVar('_T')\n" - '\n' - f'{app_declare_lines}' - 'def _uninferrable() -> Any:\n' - ' """Get an "Any" in mypy and "uninferrable" in Pylint."""\n' - ' # pylint: disable=undefined-variable\n' - ' return _not_a_real_variable # type: ignore' - '\n' - '\n' - ) + elif self.mname == '_bascenev1': + tc_import_lines_extra += ' import babase\n import bascenev1\n' + elif self.mname == '_bauiv1': + tc_import_lines_extra += ' import babase\n import bauiv1\n' + app_declare_lines = 'app: App\n\n' if self.mname == '_babase' else '' + enum_import_lines = ( + '' + if self.mname == '_babase' + else 'from babase._mgen.enums import TimeFormat, TimeType\n\n' + if self.mname == '_bascenev1' + else '' + ) + out = ( + get_public_license('python') + '\n' + '#\n' + f'"""A dummy stub module for the real {self.mname}.\n' + '\n' + f'The real {self.mname} is a compiled extension module' + ' and only available\n' + 'in the live engine. This dummy-module allows Pylint/Mypy/etc. to\n' + 'function reasonably well outside of that environment.\n' + '\n' + 'Make sure this file is never included' + ' in dirs seen by the engine!\n' + '\n' + 'In the future perhaps this can be a stub (.pyi) file, but' + ' we will need\n' + 'to make sure that it works with all our tools' + ' (mypy, pylint, pycharm).\n' + '\n' + 'NOTE: This file was autogenerated by ' + __name__ + '; ' + 'do not edit by hand.\n' + '"""\n' + '\n' + # '# (hash we can use to see if this file is out of date)\n' + # '# SOURCES_HASH='+sources_hash+'\n' + # '\n' + '# I\'m sorry Pylint. I know this file saddens you. Be strong.\n' + '# pylint: disable=useless-suppression\n' + '# pylint: disable=unnecessary-pass\n' + '# pylint: disable=use-dict-literal\n' + '# pylint: disable=use-list-literal\n' + '# pylint: disable=unused-argument\n' + '# pylint: disable=missing-docstring\n' + '# pylint: disable=too-many-locals\n' + '# pylint: disable=redefined-builtin\n' + '# pylint: disable=too-many-lines\n' + '# pylint: disable=redefined-outer-name\n' + '# pylint: disable=invalid-name\n' + '# pylint: disable=no-value-for-parameter\n' + '\n' + 'from __future__ import annotations\n' + '\n' + f'from typing import {typing_imports}\n' + '\n' + f'{enum_import_lines}' + 'if TYPE_CHECKING:\n' + f' from typing import {typing_imports_tc}\n' + f'{tc_import_lines_extra}' + '\n' + '\n' + "_T = TypeVar('_T')\n" + '\n' + f'{app_declare_lines}' + 'def _uninferrable() -> Any:\n' + ' """Get an "Any" in mypy and "uninferrable" in Pylint."""\n' + ' # pylint: disable=undefined-variable\n' + ' return _not_a_real_variable # type: ignore' + '\n' + '\n' + ) - out += _writeclasses(module, classnames) - out += _writefuncs(module, funcnames, indent=0, spacing=2, as_method=False) + out += _writeclasses(module, classnames) + out += _writefuncs( + module, funcnames, indent=0, spacing=2, as_method=False + ) - # Lastly format it. - out = format_python_str(out) + # Lastly format it. + out = format_python_str(out) - outhashpath = os.path.join( - os.path.dirname(outfilename), f'.{mname}_sources_hash' - ) - - with open(outfilename, 'w', encoding='utf-8') as outfile: - outfile.write(out) - - with open(outhashpath, 'w', encoding='utf-8') as outfile: - outfile.write(sources_hash) + os.makedirs(os.path.dirname(self.outfilename), exist_ok=True) + with open(self.outfilename, 'w', encoding='utf-8') as outfile: + outfile.write(out) -def _dummy_module_dirty(mname: str) -> tuple[bool, str]: - """Test hashes on the dummy-module to see if it needs updates.""" +def generate(projroot: str) -> None: + """Generate all dummy-modules.""" - # Let's generate a hash from all sources under the python source dir. - pysources = [] - exts = ['.cc', '.c', '.h'] - for root, _dirs, files in os.walk('src/ballistica/python'): - for fname in files: - if any(fname.endswith(ext) for ext in exts): - pysources.append(os.path.join(root, fname)) - - # Also lets add this script so we re-create when it changes. - pysources.append(__file__) - - outpath = f'assets/src/ba_data/python/.{mname}_sources_hash' - if not os.path.exists(outpath): - existing_hash = '' - else: - with open(outpath, encoding='utf-8') as infile: - existing_hash = infile.read() - - # Important to keep this deterministic... - pysources.sort() - - # Note: going with plain integers instead of hex so linters - # don't see words and whine about spelling errors. - pysources_hash = get_files_hash(pysources, int_only=True) - dirty = existing_hash != pysources_hash - return dirty, pysources_hash - - -def update(projroot: str, check: bool, force: bool) -> None: - """Update dummy-modules as needed.""" - from pathlib import Path - - from efrotools import getconfig + from batools.featureset import FeatureSet toolsdir = os.path.abspath(os.path.join(projroot, 'tools')) # Make sure we're running from the project root dir. - os.chdir(projroot) + if os.path.abspath(projroot) != os.getcwd(): + raise RuntimeError( + f"We expect to be running from '{projroot}'" + f" but cwd is '{os.getcwd()}'." + ) - public = getconfig(Path('.'))['public'] + # WARNING: this builds cmake-binary. This could cause problems in + # parallel builds containing checks plus actual builds of cmake-binary. + # The upside is during iteration this will often just use the same binary + # we are iterating with, reducing redundant compiles. + # TODO(ericf): for the ballistica public repo should make this use + # prefab builds so folks without compiler toolchains can still run + # code checks. + print(f'{Clr.SMAG}Building binary to generate dummy-modules...{Clr.RST}') - # Force makes no sense in check mode. - if force and check: - raise Exception('cannot specify both force and check mode') + subprocess.run(['make', 'cmake-binary'], check=True) + subprocess.run(['make', 'scripts-cmake'], cwd='src/assets', check=True) - for mname in ('_ba', '_bainternal'): - # Skip internal module in public since it might - # not exist and is read-only anyway. - if mname == '_bainternal' and public: - continue + pycmd = ( + f'import sys\n' + f'sys.path.append("build/assets/ba_data/python")\n' + f'sys.path.append("{toolsdir}")\n' + f'import _babase\n' + f'from batools import dummymodule\n' + ) + # Generate a dummy module for each featureset that has a binary module. + featuresets = FeatureSet.get_all_for_project(project_root=projroot) + featuresets = [f for f in featuresets if f.has_native_python_module] + mnames: list[str] = [fs.name_python_binary_module for fs in featuresets] + + gencount = 0 + for mname in mnames: + gencount += 1 outfilename = os.path.abspath( - os.path.join(projroot, f'assets/src/ba_data/python/{mname}.py') + os.path.join(projroot, f'build/dummymodules/{mname}.py') ) - - dirty, sources_hash = _dummy_module_dirty(mname) - - if dirty: - if check: - print( - f'{Clr.RED}ERROR: dummy {mname} module' - f' is out of date.{Clr.RST}' - ) - sys.exit(255) - elif not force: - # Dummy-module is clean and force is off; we're done here. - print(f'Dummy-module {Clr.BLD}{mname}.py{Clr.RST} is up to date.') - continue - - print( - f'{Clr.MAG}Updating {Clr.BLD}{mname}.py{Clr.RST}{Clr.MAG}' - f' dummy-module...{Clr.RST}' + pycmd += ( + f'dummymodule.Generator(modulename="{mname}",' + f' outfilename="{outfilename}").run()\n' ) + assert gencount - # Let's build the cmake version; no sandboxing issues to contend with - # there. Also going with the headless build; will need to revisit if - # there's ever any functionality not available in that build. - subprocess.run(['make', 'cmake-server-build'], check=True) - - # Launch ballisticacore and exec ourself from within it. - print( - f'Launching ballisticacore to generate' - f' {Clr.BLD}{mname}.py{Clr.RST} dummy-module...' + # Launch ballisticakit and exec ourself from within it. + print( + f'{Clr.SMAG}Launching ballisticakit to generate' + f' {gencount} dummy-modules...{Clr.RST}' + ) + try: + # Note: ask Python to not scatter __pycache__ files throughout + # our build output. + subprocess.run( + ['build/cmake/debug/ballisticakit', '--command', pycmd], + env=dict(os.environ, PYTHONDONTWRITEBYTECODE='1'), + check=True, ) - try: - subprocess.run( - [ - './ballisticacore', - '-exec', - f'try:\n' - f' import sys\n' - f' sys.path.append("{toolsdir}")\n' - f' from batools import dummymodule\n' - f' dummymodule.generate(mname="{mname}",\n' - f' sources_hash="{sources_hash}",\n' - f' outfilename="{outfilename}")\n' - f' ba.quit()\n' - f'except Exception as exc:\n' - f' import sys\n' - f' import traceback\n' - f' print("ERROR GENERATING {mname} DUMMY-MODULE")\n' - f' traceback.print_exc()\n' - f' sys.exit(255)\n', - ], - cwd='build/cmake/server-debug/dist', - check=True, - ) - print( - f'{Clr.BLU}{mname} dummy-module generation complete.{Clr.RST}' - ) + print(f'{Clr.BLU}Dummy-module generation complete.{Clr.RST}') - except Exception as exc2: - # Keep our error simple here; we want focus to be on what went - # wrong withing BallisticaCore. - raise CleanError( - 'BallisticaCore dummy-module generation failed.' - ) from exc2 + except Exception as exc2: + # Keep our error simple here; we want focus to be on what went + # wrong withing BallisticaKit. + raise CleanError( + 'BallisticaKit dummy-module generation failed.' + ) from exc2 diff --git a/tools/batools/featureset.py b/tools/batools/featureset.py new file mode 100644 index 00000000..dedeb8df --- /dev/null +++ b/tools/batools/featureset.py @@ -0,0 +1,268 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality for working with spinoff feature-sets. + +Feature-sets are logical groupings of functionality that can be stripped +out of or added in to spinoff dst projects. This allows for more high level +dependency management and organization than would be possible through +filtering alone. +""" + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + +from efro.util import snake_case_to_title +from efro.error import CleanError + +if TYPE_CHECKING: + pass + +# Cached feature-sets indexed by project root. +_g_feature_sets: dict[str, list[FeatureSet]] = {} + + +class FeatureSet: + """Defines a feature-set.""" + + _active_feature_set: FeatureSet | None = None + + @classmethod + def get_all_for_project(cls, project_root: str) -> list[FeatureSet]: + """Return all feature-sets for the current project.""" + project_root_abs = os.path.abspath(project_root) + + # Only do this once per project. + if project_root_abs not in _g_feature_sets: + _g_feature_sets[project_root_abs] = _build_feature_set_list( + project_root_abs + ) + return _g_feature_sets[project_root_abs] + + @classmethod + def resolve_requirements( + cls, featuresets: list[FeatureSet], reqs: set[str] + ) -> set[str]: + """Resolve all required feature-sets based on a given set of them. + + Throws descriptive CleanErrors if any are missing. + """ + fsets = {f.name: f for f in featuresets} + reqs_out = set[str]() + for req in reqs: + cls._resolve_requirements(fsets, reqs_out, req) + return reqs_out + + @classmethod + def _resolve_requirements( + cls, featuresets: dict[str, FeatureSet], reqs_out: set[str], req: str + ) -> None: + if req in reqs_out: + return + featureset = featuresets.get(req) + if featureset is None: + raise CleanError(f"Required featureset '{req}' not found.") + reqs_out.add(req) + for sub_req in featureset.requirements: + cls._resolve_requirements(featuresets, reqs_out, sub_req) + + def __init__(self, name: str): + self.requirements = set[str]() + self.internal = False + self.has_native_python_module = True + self.validate_name(name) + + # Paths of files we should disable c++ namespace checks for. + # (generally external-originating code that doesn't conform to our + # ballistica feature-set based namespace scheme) + self.cpp_namespace_check_disable_files = set[str]() + + # Our standard snake_case name. + self._name = name + + # Generate a default title form (foo_bar -> Foo Bar). The + # feature-set config can customize this; for example a word like + # base_sdk might look better as 'Base SDK' instead of the + # default 'Base Sdk'. + self._name_title = snake_case_to_title(self._name) + + @property + def name(self) -> str: + """Our base name.""" + return self._name + + @property + def name_compact(self) -> str: + """Compact name variation (foo_bar -> foobar). Used for Python bits.""" + return self._name.replace('_', '') + + @property + def name_title(self) -> str: + """Title name variation (foo_bar -> Foo Bar). For pretty stuff.""" + return self._name_title + + @name_title.setter + def name_title(self, val: str) -> None: + """Set custom title name.""" + + # Make sure they don't pass underscores; title versions are just + # words and spaces. + if '_' in val: + raise CleanError( + f"Custom FeatureSet name_title '{val}' contains" + ' underscores; it must contain only spaces.' + ) + + # Make sure the value they're providing still matches their base + # name. It could be easy to let this fall out of sync + # accidentally. + if val.lower().replace(' ', '_') != self._name: + raise CleanError( + f"Custom FeatureSet name_title '{val}' letters/spacing" + f" does not match base name '{self._name}'." + ) + + # Ok val; we will accept you. + self._name_title = val + + @property + def name_camel(self) -> str: + """Camel case name (foo_bar -> FooBar). Used for classes, etc.""" + # We want to use any of the customization applied to name_title + # so let's just give _name_title with spaces stripped out. + return self._name_title.replace(' ', '') + + @property + def name_python_package(self) -> str: + """Python package name (foo_bar -> bafoobar).""" + return f'ba{self.name_compact}' + + @property + def name_python_package_meta(self) -> str: + """The name of our meta python package.""" + return f'ba{self.name_compact}meta' + + @property + def name_python_binary_module(self) -> str: + """Python binary module name (foo_bar -> _bafoobar).""" + return f'_ba{self.name_compact}' + + @staticmethod + def validate_name(name: str) -> None: + """Validate a standard snake-case feature-set name. + + Throws descriptive CleanErrors if provided name is invalid. + """ + + # Disallow empty. + if not name: + raise CleanError('Feature set name cannot be empty.') + + # Require starting with a letter. + if not name[0].isalpha(): + raise CleanError( + f"Invalid feature set name '{name}'" + ' - names must start with a letter.' + ) + + # Require only letters, numbers, and underscores. + if not name.replace('_', '').isalnum(): + raise CleanError( + f"Invalid feature set name '{name}'" + ' - only letters, numbers, and underscores are allowed.' + ) + + # Require all lowercase. + if not name.islower(): + raise CleanError( + f"Invalid feature set name '{name}'" + ' - only lowercase letters are allowed.' + ) + + # Disallow leading, trailing, or consecutive underscores. + # (these will result in a '' in the split results which evals to False) + if not all(name.split('_')): + raise CleanError( + f"Invalid feature set name '{name}'" + ' - leading, trailing, and consecutive underscores are' + ' not allowed.' + ) + + @classmethod + def get_active(cls) -> FeatureSet: + """Return the FeatureSet currently being defined. + + For use by settings scripts. + """ + if cls._active_feature_set is None: + raise RuntimeError('No FeatureSet being actively defined.') + return cls._active_feature_set + + def apply_config(self, config_path: str) -> None: + """Apply a user config to this feature-set.""" + # pylint: disable=exec-used + try: + assert self._active_feature_set is None + type(self)._active_feature_set = self + + # Apply both src and dist spinoff configs. + exec_context: dict = {} + with open(config_path, encoding='utf-8') as infile: + config_contents = infile.read() + + # Use compile here so we can provide a nice file path for + # error tracebacks. + exec( + compile(config_contents, config_path, 'exec'), + exec_context, + exec_context, + ) + + finally: + assert type(self)._active_feature_set is self + type(self)._active_feature_set = None + + +def _build_feature_set_list(project_root: str) -> list[FeatureSet]: + featuresets: list[FeatureSet] = [] + fsdir = os.path.join(project_root, 'config', 'featuresets') + prefix = 'featureset_' + filenames = os.listdir(fsdir) + for filename in sorted(filenames): + if not filename.endswith('.py'): + continue + if not filename.startswith(prefix): + raise CleanError( + f"Found invalid featuresetdef filename: '{filename}'." + ) + basename, _ext = os.path.splitext(filename.removeprefix(prefix)) + featureset = FeatureSet(basename) + featureset.apply_config(os.path.join(fsdir, filename)) + featuresets.append(featureset) + + # Run some sanity checks to make sure our featuresets don't have + # clashing names/etc. (for instance, foo_v1 and foov_1 would resolve + # to the same foov1 py module name). + + fsnames = {f.name for f in featuresets} + assert len(fsnames) == len(featuresets) + + assert len({f.name_compact for f in featuresets}) == len(featuresets) + assert len({f.name_compact for f in featuresets}) == len(featuresets) + + for featureset in featuresets: + for req in featureset.requirements: + if req == featureset.name: + raise CleanError( + f"Feature-set '{featureset.name}'" + f' lists itself as a requirement; this is not allowed.' + ) + if req not in fsnames: + raise CleanError( + f"Undefined feature-set '{req}'" + f' listed as a requirement of feature-set' + f" '{featureset.name}'." + ) + + return featuresets diff --git a/tools/batools/meta.py b/tools/batools/metabuild.py similarity index 57% rename from tools/batools/meta.py rename to tools/batools/metabuild.py index aaeccdfa..46316e8c 100644 --- a/tools/batools/meta.py +++ b/tools/batools/metabuild.py @@ -5,12 +5,12 @@ from __future__ import annotations import os import json -from typing import TYPE_CHECKING from efro.terminal import Clr -if TYPE_CHECKING: - pass +# Can be plugged into hashes/etc to give us a convenient way to blow away +# all built meta output on CI/etc. (by incrementing this value). +META_BUILD_MAGIC_NUMBER = 1 def gen_flat_data_code( @@ -50,65 +50,75 @@ def gen_flat_data_code( def gen_binding_code(projroot: str, in_path: str, out_path: str) -> None: - """Generate binding.inc file.""" + """Generate binding_foo.inc file.""" out_dir = os.path.dirname(out_path) if not os.path.exists(out_dir): os.makedirs(out_dir, exist_ok=True) - # Pull all lines in the embedded list and split into py and c++ names. with open(in_path, encoding='utf-8') as infile: pycode = infile.read() # Double quotes cause errors. if '"' in pycode: - raise Exception('bindings file can\'t contain double quotes.') - lines = [ + raise RuntimeError('bindings file can\'t contain double quotes.') + + # Look for all lines associating some Python value with a constant. + entries = [ l.strip().split(', # ') for l in pycode.splitlines() - if l.startswith(' ') + if l.startswith(' ') ] - if not all(len(l) == 2 for l in lines): - raise Exception('malformatted data') + if not all(len(l) == 2 for l in entries): + raise RuntimeError('malformatted data.') # Our C++ code first execs our input as a string. - ccode = '{const char* bindcode = ' + repr(pycode).replace("'", '"') + ';' + ccode = '{\n' f'// Python code from {in_path}:\n' 'const char* bindcode =' + + for line in pycode.splitlines(): + ccode += f'\n "{line}\\n"' ccode += ( - '\nPyObject* result = PyRun_String(bindcode, Py_file_input,' - ' bootstrap_context.get(), bootstrap_context.get());\n' - 'if (result == nullptr) {\n' - ' PyErr_PrintEx(0);\n' - ' // Use a standard error to avoid a useless stack trace.\n' - ' throw std::logic_error("Error fetching required Python' - ' objects.");\n' + ';\n' + '\n' + '// Exec the Python code in an empty context.\n' + 'auto ctx = PythonRef::Stolen(PyDict_New());\n' + ) + + ccode += ( + 'bool success = PythonCommand(bindcode, "' + + os.path.basename(in_path) + + '").Exec(true,' + ' *ctx, *ctx);\n' + 'if (!success) {\n' + ' FatalError("Error fetching required Python objects.");\n' '}\n' ) - # Then it grabs the function that was defined and runs it. + # Then it grabs the 'values' var that should have been defined. ccode += ( - 'PyObject* bindvals = PythonCommand("get_binding_values()",' - ' "")' - '.RunReturnObj(true, bootstrap_context.get());\n' - 'if (bindvals == nullptr) {\n' - ' // Use a standard error to avoid a useless stack trace.\n' - ' throw std::logic_error("Error binding required Python' - ' objects.");\n' + '\n' + "// Grab the 'values' list that the binding code created.\n" + 'auto bindvals = ctx.DictGetItem("values");\n' + 'if (!bindvals.Exists() || !PyList_Check(*bindvals)) {\n' + ' FatalError("Error binding required Python objects.");\n' '}\n' + '\n' + '// Pull our various obj_ values from the values list.\n' ) # Then it pulls the individual values out of the returned tuple. - for i, line in enumerate(lines): + for i, entry in enumerate(entries): storecmd = ( - 'StoreObjCallable' - if line[1].endswith('Class') or line[1].endswith('Call') - else 'StoreObj' + 'objs_.StoreCallable' + if entry[1].endswith('Class') or entry[1].endswith('Call') + else 'objs_.Store' ) ccode += ( - f'{storecmd}(ObjID::{line[1]},' - f' PyTuple_GET_ITEM(bindvals, {i}), true);\n' + f'{storecmd}(ObjID::{entry[1]},' + f' PyList_GET_ITEM(bindvals.Get(), {i}));\n' ) - ccode += 'Py_DECREF(bindvals);\n}\n' + ccode += '}\n' pretty_path = os.path.abspath(out_path) if pretty_path.startswith(projroot + '/'): pretty_path = pretty_path[len(projroot) + 1 :] diff --git a/tools/batools/metamakefile.py b/tools/batools/metamakefile.py index 5e605e71..bc82015f 100755 --- a/tools/batools/metamakefile.py +++ b/tools/batools/metamakefile.py @@ -9,23 +9,27 @@ from __future__ import annotations import os import json -import subprocess from pathlib import Path from typing import TYPE_CHECKING from dataclasses import dataclass from efro.error import CleanError -from efro.terminal import Clr from efrotools import getconfig if TYPE_CHECKING: pass # These paths need to be relative to the dir we're writing the Makefile to. -TOOLS_DIR = '../../tools' -ROOT_DIR = '../..' -OUT_DIR_CPP = '../ballistica/generated' -OUT_DIR_PYTHON = '../../assets/src/ba_data/python/ba/_generated' +PROJ_DIR = '../..' +TOOLS_DIR = f'{PROJ_DIR}/tools' +PROJ_SRC_DIR = '..' + +# These should only be used for make targets since they use makefile vars. +# Our makefile vars have the same names as above. +# We could inline vars ourself but it's nice to build a makefile that feels +# like one we'd build by hand. +OUT_DIR_ROOT_CPP = '$(PROJ_SRC_DIR)/ballistica' +OUT_DIR_BASE_PYTHON = '$(PROJ_SRC_DIR)/assets/ba_data/python/babase/_mgen' @dataclass @@ -55,305 +59,364 @@ class Target: return out -def _emit_sources_lines(targets: list[Target]) -> list[str]: - """Gen lines to build provided targets.""" - out: list[str] = [] - if not targets: - return out - all_dsts = set() - for target in targets: - all_dsts.add(target.dst) - out.append( - 'sources: \\\n ' - + ' \\\n '.join(dst.replace(' ', '\\ ') for dst in sorted(all_dsts)) - + '\n' - ) - return out +def generate_meta_makefile(projroot: str, existing_data: str) -> dict[str, str]: + """Update the project meta Makefile. + + Returns file names and contents. + """ + return MetaMakefileGenerator(projroot, existing_data).run() -def _emit_efrocache_lines(targets: list[Target]) -> list[str]: - """Gen lines to cache provided targets.""" - out: list[str] = [] - if not targets: - return out - all_dsts = set() - for target in targets: +class MetaMakefileGenerator: + """Thing that does the thing.""" - # We may need to make pipeline adjustments if/when we get filenames - # with spaces in them. - if ' ' in target.dst: - raise CleanError( - 'FIXME: need to account for spaces in filename' - f' "{target.dst}".' - ) - all_dsts.add(target.dst) - out.append( - 'efrocache-list:\n\t@echo ' - + ' \\\n '.join('"' + dst + '"' for dst in sorted(all_dsts)) - + '\n' - ) - out.append('efrocache-build: sources\n') + def __init__(self, projroot: str, existing_data: str) -> None: + from batools.featureset import FeatureSet - return out + self._existing_data = existing_data + self._projroot = projroot + self._featuresets = FeatureSet.get_all_for_project(projroot) + def run(self) -> dict[str, str]: + """Do the thing.""" + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements -def _add_enums_module_target(targets: list[Target]) -> None: - targets.append( - Target( - src=[ - '../ballistica/core/types.h', - os.path.join(TOOLS_DIR, 'batools', 'pythonenumsmodule.py'), - ], - dst=os.path.join(OUT_DIR_PYTHON, 'enums.py'), - cmd='$(PCOMMAND) gen_python_enums_module $< $@', - ) - ) + public = getconfig(Path(self._projroot))['public'] + assert isinstance(public, bool) + fname = 'src/meta/Makefile' + fname_pub_man = 'src/meta/.meta_manifest_public.json' + fname_priv_man = 'src/meta/.meta_manifest_private.json' -def _add_init_module_target(targets: list[Target], moduledir: str) -> None: - targets.append( - Target( - src=[os.path.join(TOOLS_DIR, 'batools', 'pcommand.py')], - dst=os.path.join(moduledir, '__init__.py'), - cmd='$(PCOMMAND) gen_python_init_module $@', - ) - ) + original = self._existing_data + lines = original.splitlines() + # We'll generate manifests of all public/private files we generate + # (not private-internal though). + all_dsts_public: set[str] = set() + all_dsts_private: set[str] = set() -def _add_python_embedded_targets(targets: list[Target]) -> None: - pkg = 'bameta' - # Note: sort to keep things deterministic. - for fname in sorted(os.listdir(f'src/meta/{pkg}/python_embedded')): - if ( - not fname.endswith('.py') - or fname == '__init__.py' - or 'flycheck' in fname + # print('SRC', lines, '# __AUTOGENERATED_PUBLIC_BEGIN__' in lines) + + auto_start_public = lines.index('# __AUTOGENERATED_PUBLIC_BEGIN__') + auto_end_public = lines.index('# __AUTOGENERATED_PUBLIC_END__') + auto_start_private = lines.index('# __AUTOGENERATED_PRIVATE_BEGIN__') + auto_end_private = lines.index('# __AUTOGENERATED_PRIVATE_END__') + + # Public targets (stuff with full sources available in public repo). + targets: list[Target] = [] + pubtargets = targets + self._add_monolithic_register_modules_target(targets) + self._add_pyembed_targets(targets) + + # Base feature set bits. + if os.path.exists( + f'{self._projroot}/config/featuresets/featureset_base.py' ): - continue - name = os.path.splitext(fname)[0] - src = [ - f'{pkg}/python_embedded/{name}.py', - ] - dst = os.path.join(OUT_DIR_CPP, 'python_embedded', f'{name}.inc') - if name == 'binding': - targets.append( - Target( - src=src, dst=dst, cmd='$(PCOMMAND) gen_binding_code $< $@' - ) - ) + self._add_init_module_target(targets, moduledir=OUT_DIR_BASE_PYTHON) + self._add_base_enums_module_target(targets) + + our_lines_public = ( + _empty_line_if(bool(targets)) + + self._emit_sources_lines(targets) + + [t.emit() for t in targets] + ) + all_dsts_public.update(t.dst for t in targets) + + # Only rewrite the private section in the private repo; otherwise + # keep the existing one intact. + if public: + our_lines_private = lines[auto_start_private + 1 : auto_end_private] else: - targets.append( - Target( - src=src, - dst=dst, - cmd=f'$(PCOMMAND) gen_flat_data_code $< $@ {name}_code', + # Private targets (available in public through efrocache) + targets = [] + our_lines_private_1 = ( + _empty_line_if(bool(targets)) + + self._emit_sources_lines(targets) + + ['# __EFROCACHE_TARGET__\n' + t.emit() for t in targets] + + [ + '\n#' + ' Note: we include our public targets in efrocache even\n' + '# though they are buildable in public. This allows us to\n' + '# fetch them to bootstrap binary builds in cases where\n' + '# we can\'t use our full Makefiles (like Windows CI).\n' + ] + + self._emit_efrocache_lines(pubtargets + targets) + ) + all_dsts_private.update(t.dst for t in targets) + + # Private-internal targets (not available at all in public) + targets = [] + self._add_pyembed_targets_internal(targets) + self._add_extra_targets_internal(targets) + our_lines_private_2 = ( + ['# __PUBSYNC_STRIP_BEGIN__'] + + _empty_line_if(bool(targets)) + + self._emit_sources_lines(targets) + + [t.emit() for t in targets] + + ['# __PUBSYNC_STRIP_END__'] + ) + our_lines_private = our_lines_private_1 + our_lines_private_2 + + filtered = ( + lines[: auto_start_public + 1] + + our_lines_public + + lines[auto_end_public : auto_start_private + 1] + + our_lines_private + + lines[auto_end_private:] + ) + out = '\n'.join(filtered) + '\n' + + out_files: dict[str, str] = {} + + out_pub_man = json.dumps( + sorted(self._filter_manifest_path(p) for p in all_dsts_public), + indent=1, + ) + out_priv_man = json.dumps( + sorted(self._filter_manifest_path(p) for p in all_dsts_private), + indent=1, + ) + + out_files[fname] = out + out_files[fname_pub_man] = out_pub_man + out_files[fname_priv_man] = out_priv_man + + return out_files + + def _emit_sources_lines(self, targets: list[Target]) -> list[str]: + """Gen lines to build provided targets.""" + out: list[str] = [] + if not targets: + return out + all_dsts = set() + for target in targets: + all_dsts.add(target.dst) + out.append( + 'sources: \\\n ' + + ' \\\n '.join( + dst.replace(' ', '\\ ') for dst in sorted(all_dsts) + ) + + '\n' + ) + return out + + def _emit_efrocache_lines(self, targets: list[Target]) -> list[str]: + """Gen lines to cache provided targets.""" + out: list[str] = [] + if not targets: + return out + all_dsts = set() + for target in targets: + # We may need to make pipeline adjustments if/when we get filenames + # with spaces in them. + if ' ' in target.dst: + raise CleanError( + 'FIXME: need to account for spaces in filename' + f' "{target.dst}".' + ) + all_dsts.add(target.dst) + out.append( + 'efrocache-list:\n\t@echo ' + + ' \\\n '.join('"' + dst + '"' for dst in sorted(all_dsts)) + + '\n' + ) + out.append('efrocache-build: sources\n') + + return out + + def _add_base_enums_module_target(self, targets: list[Target]) -> None: + targets.append( + Target( + src=[ + '$(PROJ_DIR)/src/ballistica/shared/foundation/types.h', + '$(TOOLS_DIR)/batools/pythonenumsmodule.py', + ], + dst=os.path.join(OUT_DIR_BASE_PYTHON, 'enums.py'), + cmd='$(PCOMMAND) gen_python_enums_module $< $@', + ) + ) + + def _add_init_module_target( + self, targets: list[Target], moduledir: str + ) -> None: + targets.append( + Target( + src=['$(TOOLS_DIR)/batools/pcommand.py'], + dst=os.path.join(moduledir, '__init__.py'), + cmd='$(PCOMMAND) gen_python_init_module $@', + ) + ) + + def _add_monolithic_register_modules_target( + self, targets: list[Target] + ) -> None: + # When any of our featuresets configs changes, rebuild our snippet + # of code that registers them all. + featureset_fnames = [ + n + for n in os.listdir( + os.path.join(self._projroot, 'config/featuresets') + ) + if n.startswith('featureset_') and n.endswith('.py') + ] + targets.append( + Target( + src=[ + f'$(PROJ_DIR)/config/featuresets/{n}' + for n in sorted(featureset_fnames) + ], + dst=f'{OUT_DIR_ROOT_CPP}/core/mgen/python_modules_monolithic.h', + cmd='$(PCOMMAND) gen_monolithic_register_modules $@', + ) + ) + + def _add_featureset_entries( + self, entries: list[tuple[str, str]], internal: bool + ) -> None: + featuresets = [f for f in self._featuresets if internal == f.internal] + + # For featureset 'foo_bar', stuff under 'bafoobarmeta' goes + # into 'ballistica/foo_bar/mgen'. + for featureset in featuresets: + entries.append( + ( + featureset.name_python_package_meta, + os.path.join(OUT_DIR_ROOT_CPP, featureset.name, 'mgen'), ) ) + def _create_featureset_targets( + self, + entries: list[tuple[str, str]], + targets: list[Target], + internal: bool, + ) -> None: + for pkg, out_dir in entries: + base_src_dir = os.path.join(self._projroot, f'src/meta/{pkg}') + if not os.path.exists(base_src_dir): + continue -def _add_python_embedded_targets_internal(targets: list[Target]) -> None: - pkg = 'bametainternal' - # Note: sort to keep things deterministic. - for fname in sorted(os.listdir(f'src/meta/{pkg}/python_embedded')): - if ( - not fname.endswith('.py') - or fname == '__init__.py' - or 'flycheck' in fname + # Note: sort to keep things deterministic. + for fname in sorted(os.listdir(f'{base_src_dir}/pyembed')): + if ( + not fname.endswith('.py') + or fname == '__init__.py' + or 'flycheck' in fname + ): + continue + name = os.path.splitext(fname)[0] + src = [ + f'{pkg}/pyembed/{name}.py', + ] + + dst = os.path.join(out_dir, 'pyembed', f'{name}.inc') + if name.startswith('binding_'): + targets.append( + Target( + src=src, + dst=dst, + cmd='$(PCOMMAND) gen_binding_code $< $@', + ) + ) + else: + if internal: + targets.append( + Target( + src=src, + dst=dst, + cmd=( + '$(PCOMMAND) gen_encrypted_python_code' + ' $< $@' + ), + ) + ) + else: + targets.append( + Target( + src=src, + dst=dst, + cmd=f'$(PCOMMAND) gen_flat_data_code' + f' $< $@ {name}_code', + ) + ) + + def _add_pyembed_targets(self, targets: list[Target]) -> None: + entries: list[tuple[str, str]] = [] + + # Map stuff from other featureset meta packages to a + # mgen dir under their C++ root. + self._add_featureset_entries(entries, internal=False) + self._create_featureset_targets(entries, targets, internal=False) + + def _add_pyembed_targets_internal(self, targets: list[Target]) -> None: + entries: list[tuple[str, str]] = [] + self._add_featureset_entries(entries, internal=True) + self._create_featureset_targets(entries, targets, internal=True) + + def _add_extra_targets_internal(self, targets: list[Target]) -> None: + if os.path.exists( + f'{self._projroot}/config/featuresets/featureset_plus.py' ): - continue - name = os.path.splitext(fname)[0] - targets.append( - Target( - src=[f'{pkg}/python_embedded/{name}.py'], - dst=os.path.join(OUT_DIR_CPP, 'python_embedded', f'{name}.inc'), - cmd='$(PCOMMAND) gen_encrypted_python_code $< $@', - ) + # Add targets to generate message sender/receiver classes + # for our basn/client protocols. Their outputs go to 'mgen' + # so they don't get added to git. + self._add_init_module_target(targets, moduledir='baplusmeta/mgen') + for srcname, dstname, gencmd in [ + ('batocloud', 'basnmessagesender', 'gen_basn_msg_sender'), + ('cloudtoba', 'basnmessagereceiver', 'gen_basn_msg_receiver'), + ]: + targets.append( + Target( + src=[f'baplusmeta/pyembed/{srcname}.py'], + dst=f'baplusmeta/mgen/{dstname}.py', + cmd=f'$(PCOMMAND) {gencmd} $@', + ) + ) + + # Now add explicit targets to generate embedded code for the + # resulting classes. We can't simply place them in a scanned + # dir like pyembed because they might not exist yet at + # update time. + for name in ['basnmessagesender', 'basnmessagereceiver']: + targets.append( + Target( + src=[f'baplusmeta/mgen/{name}.py'], + dst=os.path.join( + OUT_DIR_ROOT_CPP, + 'plus', + 'mgen', + 'pyembed', + f'{name}.inc', + ), + cmd='$(PCOMMAND) gen_encrypted_python_code $< $@', + ) + ) + + def _filter_manifest_path(self, path: str) -> str: + """Given a path we dumped into our makefile, generate an abs one.""" + + # Our makefile paths contain vars to be subbed by the makefile. + # We need to do those same subs now. + for pair in [ + ('$(PROJ_DIR)', PROJ_DIR), + ('$(TOOLS_DIR)', TOOLS_DIR), + ('$(PROJ_SRC_DIR)', PROJ_SRC_DIR), + ]: + path = path.replace(pair[0], pair[1]) + + projpath = f'{self._projroot}/' + assert '\\' not in projpath # Don't expect to work on windows. + abspath = os.path.abspath( + os.path.join(self._projroot, 'src', 'meta', path) ) - - -def _add_extra_targets_internal(targets: list[Target]) -> None: - - # Add targets to generate message sender/receiver classes for - # our basn/client protocols. Their outputs go to 'generated' so they - # don't get added to git. - _add_init_module_target(targets, moduledir='bametainternal/generated') - for srcname, dstname, gencmd in [ - ('batocloud', 'basnmessagesender', 'gen_basn_msg_sender'), - ('cloudtoba', 'basnmessagereceiver', 'gen_basn_msg_receiver'), - ]: - targets.append( - Target( - src=[f'bametainternal/python_embedded/{srcname}.py'], - dst=f'bametainternal/generated/{dstname}.py', - cmd=f'$(PCOMMAND) {gencmd} $@', + if not abspath.startswith(projpath): + raise RuntimeError( + f'Path "{abspath}" is not under project root "{projpath}"' ) - ) - - # Now add explicit targets to generate embedded code for the resulting - # classes. We can't simply place them in a scanned dir like - # python_embedded because they might not exist yet at update time. - for name in ['basnmessagesender', 'basnmessagereceiver']: - targets.append( - Target( - src=[f'bametainternal/generated/{name}.py'], - dst=os.path.join(OUT_DIR_CPP, 'python_embedded', f'{name}.inc'), - cmd='$(PCOMMAND) gen_encrypted_python_code $< $@', - ) - ) + return abspath[len(projpath) :] def _empty_line_if(condition: bool) -> list[str]: return [''] if condition else [] - - -def _project_centric_path(path: str) -> str: - projpath = f'{os.getcwd()}/' - assert '\\' not in projpath # Don't expect to work on windows. - abspath = os.path.abspath(os.path.join('src/meta', path)) - if not abspath.startswith(projpath): - raise RuntimeError( - f'Path "{abspath}" is not under project root "{projpath}"' - ) - return abspath[len(projpath) :] - - -def update(projroot: str, check: bool) -> None: - """Update the project meta Makefile.""" - # pylint: disable=too-many-locals - # pylint: disable=too-many-statements - - # Operate out of root dist dir for consistency. - os.chdir(projroot) - - public = getconfig(Path('.'))['public'] - assert isinstance(public, bool) - - fname = 'src/meta/Makefile' - fname_pub_man = 'src/meta/.meta_manifest_public.json' - fname_priv_man = 'src/meta/.meta_manifest_private.json' - - with open(fname, encoding='utf-8') as infile: - original = infile.read() - lines = original.splitlines() - - with open(fname_pub_man, encoding='utf-8') as infile: - original_pub_man = infile.read() - - with open(fname_priv_man, encoding='utf-8') as infile: - original_priv_man = infile.read() - - # We'll generate manifests of all public/private files we generate - # (not private-internal though). - all_dsts_public: set[str] = set() - all_dsts_private: set[str] = set() - - auto_start_public = lines.index('# __AUTOGENERATED_PUBLIC_BEGIN__') - auto_end_public = lines.index('# __AUTOGENERATED_PUBLIC_END__') - auto_start_private = lines.index('# __AUTOGENERATED_PRIVATE_BEGIN__') - auto_end_private = lines.index('# __AUTOGENERATED_PRIVATE_END__') - - # Public targets (full sources available in public) - targets: list[Target] = [] - pubtargets = targets - _add_python_embedded_targets(targets) - _add_init_module_target(targets, moduledir=OUT_DIR_PYTHON) - _add_enums_module_target(targets) - our_lines_public = ( - _empty_line_if(bool(targets)) - + _emit_sources_lines(targets) - + [t.emit() for t in targets] - ) - all_dsts_public.update(t.dst for t in targets) - - # Only rewrite the private section in the private repo; otherwise - # keep the existing one intact. - if public: - our_lines_private = lines[auto_start_private + 1 : auto_end_private] - else: - # Private targets (available in public through efrocache) - targets = [] - our_lines_private_1 = ( - _empty_line_if(bool(targets)) - + _emit_sources_lines(targets) - + ['# __EFROCACHE_TARGET__\n' + t.emit() for t in targets] - + [ - '\n# Note: we include our public targets in efrocache even\n' - '# though they are buildable in public. This allows us to\n' - '# fetch them to bootstrap binary builds in cases where\n' - '# we can\'t use our full Makefiles (like Windows CI).\n' - ] - + _emit_efrocache_lines(pubtargets + targets) - ) - all_dsts_private.update(t.dst for t in targets) - - # Private-internal targets (not available at all in public) - targets = [] - _add_python_embedded_targets_internal(targets) - _add_extra_targets_internal(targets) - our_lines_private_2 = ( - ['# __PUBSYNC_STRIP_BEGIN__'] - + _empty_line_if(bool(targets)) - + _emit_sources_lines(targets) - + [t.emit() for t in targets] - + ['# __PUBSYNC_STRIP_END__'] - ) - our_lines_private = our_lines_private_1 + our_lines_private_2 - - filtered = ( - lines[: auto_start_public + 1] - + our_lines_public - + lines[auto_end_public : auto_start_private + 1] - + our_lines_private - + lines[auto_end_private:] - ) - out = '\n'.join(filtered) + '\n' - - out_pub_man = json.dumps( - sorted(_project_centric_path(p) for p in all_dsts_public), indent=1 - ) - out_priv_man = json.dumps( - sorted(_project_centric_path(p) for p in all_dsts_private), indent=1 - ) - - if ( - out == original - and out_pub_man == original_pub_man - and out_priv_man == original_priv_man - ): - print(f'{fname} (and manifests) are up to date.') - else: - if check: - errname = ( - fname - if out != original - else fname_pub_man - if out_pub_man != original_pub_man - else fname_priv_man - if out_priv_man != original_priv_man - else 'unknown' - ) - raise CleanError(f"ERROR: file is out of date: '{errname}'.") - print( - f'{Clr.SBLU}Updating {fname} (and cleaning existing output).' - f'{Clr.RST}' - ) - - if out != original: - with open(fname, 'w', encoding='utf-8') as outfile: - outfile.write(out) - - # Also write our output file manifests every time we write the - # Makefile (technically should check them individually in case - # they're out of date but the Makefile isn't, though that should not - # happen normally). - if out_pub_man != fname_pub_man: - with open(fname_pub_man, 'w', encoding='utf-8') as outfile: - outfile.write(out_pub_man) - if out_priv_man != fname_priv_man: - with open(fname_priv_man, 'w', encoding='utf-8') as outfile: - outfile.write(out_priv_man) - - # Also clean existing meta output every time the Makefile changes; - # this should minimize the chance of orphan outputs hanging around - # causing trouble. - subprocess.run(['make', 'clean'], cwd='src/meta', check=True) diff --git a/tools/batools/pcommand.py b/tools/batools/pcommand.py index 23191a88..8af73749 100644 --- a/tools/batools/pcommand.py +++ b/tools/batools/pcommand.py @@ -1,97 +1,27 @@ # Released under the MIT License. See LICENSE for details. # -# pylint: disable=too-many-lines """A nice collection of ready-to-use pcommands for this package.""" from __future__ import annotations # Note: import as little as possible here at the module level to # keep launch times fast for small snippets. import sys -from typing import TYPE_CHECKING from efrotools.pcommand import PROJROOT -if TYPE_CHECKING: - pass +def prune_includes() -> None: + """Check for unnecessary includes in C++ files.""" + from batools.pruneincludes import Pruner -def stage_server_file() -> None: - """Stage files for the server environment with some filtering.""" - from efro.error import CleanError - import batools.assetstaging + args = sys.argv.copy()[2:] + commit = False + if '--commit' in args: + args.remove('--commit') + commit = True - if len(sys.argv) != 5: - raise CleanError('Expected 3 args (mode, infile, outfile).') - mode, infilename, outfilename = sys.argv[2], sys.argv[3], sys.argv[4] - batools.assetstaging.stage_server_file( - str(PROJROOT), mode, infilename, outfilename - ) - - -def py_examine() -> None: - """Run a python examination at a given point in a given file.""" - import os - from pathlib import Path - import efrotools - - if len(sys.argv) != 7: - print('ERROR: expected 7 args') - sys.exit(255) - filename = Path(sys.argv[2]) - line = int(sys.argv[3]) - column = int(sys.argv[4]) - selection: str | None = None if sys.argv[5] == '' else sys.argv[5] - operation = sys.argv[6] - - # This stuff assumes it is being run from project root. - os.chdir(PROJROOT) - - # Set up pypaths so our main distro stuff works. - scriptsdir = os.path.abspath( - os.path.join( - os.path.dirname(sys.argv[0]), '../assets/src/ba_data/python' - ) - ) - toolsdir = os.path.abspath( - os.path.join(os.path.dirname(sys.argv[0]), '../tools') - ) - if scriptsdir not in sys.path: - sys.path.append(scriptsdir) - if toolsdir not in sys.path: - sys.path.append(toolsdir) - efrotools.py_examine(PROJROOT, filename, line, column, selection, operation) - - -def clean_orphaned_assets() -> None: - """Remove asset files that are no longer part of the build.""" - import os - import json - import subprocess - - # Operate from dist root.. - os.chdir(PROJROOT) - - # Our manifest is split into 2 files (public and private) - with open('assets/.asset_manifest_public.json', encoding='utf-8') as infile: - manifest = set(json.loads(infile.read())) - with open( - 'assets/.asset_manifest_private.json', encoding='utf-8' - ) as infile: - manifest.update(set(json.loads(infile.read()))) - for root, _dirs, fnames in os.walk('assets/build'): - for fname in fnames: - fpath = os.path.join(root, fname) - fpathrel = fpath[13:] # paths are relative to assets/build - if fpathrel not in manifest: - print(f'Removing orphaned asset file: {fpath}') - os.unlink(fpath) - - # Lastly, clear empty dirs. - subprocess.run( - 'find assets/build -depth -empty -type d -delete', - shell=True, - check=True, - ) + Pruner(commit=commit, paths=args).run() + print('Prune run complete!') def resize_image() -> None: @@ -103,7 +33,7 @@ def resize_image() -> None: import subprocess if len(sys.argv) != 6: - raise Exception('expected 5 args') + raise RuntimeError('Expected 5 args.') width = int(sys.argv[2]) height = int(sys.argv[3]) src = sys.argv[4] @@ -127,6 +57,9 @@ def check_clean_safety() -> None: adding something. """ import os + import subprocess + + from efro.error import CleanError from efrotools.pcommand import check_clean_safety as std_snippet # First do standard checks. @@ -136,9 +69,11 @@ def check_clean_safety() -> None: # (since we may be blowing core away here). spinoff_bin = os.path.join(str(PROJROOT), 'tools', 'spinoff') if os.path.exists(spinoff_bin): - status = os.system(spinoff_bin + ' cleancheck') - if status != 0: - sys.exit(255) + result = subprocess.run( + [spinoff_bin, 'cleancheck', '--soft'], check=False + ) + if result.returncode != 0: + raise CleanError() def archive_old_builds() -> None: @@ -149,7 +84,7 @@ def archive_old_builds() -> None: import batools.build if len(sys.argv) < 3: - raise Exception('invalid arguments') + raise RuntimeError('Invalid arguments.') ssh_server = sys.argv[2] builds_dir = sys.argv[3] ssh_args = sys.argv[4:] @@ -174,7 +109,7 @@ def lazy_increment_build() -> None: if sys.argv[2:] not in [[], ['--update-hash-only']]: raise CleanError('Invalid arguments') update_hash_only = '--update-hash-only' in sys.argv - codefiles = get_code_filenames(PROJROOT) + codefiles = get_code_filenames(PROJROOT, include_generated=False) codehash = get_files_hash(codefiles) hashfilename = '.cache/lazy_increment_build' try: @@ -183,7 +118,6 @@ def lazy_increment_build() -> None: except FileNotFoundError: lasthash = '' if codehash != lasthash: - if not update_hash_only: print( f'{Clr.SMAG}Source(s) changed; incrementing build...{Clr.RST}' @@ -206,12 +140,11 @@ def get_master_asset_src_dir() -> None: import subprocess import os - master_assets_dir = '/Users/ericf/Documents/ballisticacore_master_assets' + master_assets_dir = '/Users/ericf/Documents/ballisticakit_master_assets' dummy_dir = '/__DUMMY_MASTER_SRC_DISABLED_PATH__' - # Only apply this on my setup - if os.path.exists(master_assets_dir): - + # Only apply this on my primary setup. + if os.path.exists(master_assets_dir) and os.path.exists('.git'): # Ok, for now lets simply use our hard-coded master-src # path if we're on master in and not otherwise. Should # probably make this configurable. @@ -226,9 +159,8 @@ def get_master_asset_src_dir() -> None: # pylint: disable=condition-evals-to-constant if ( 'origin/master' in output.splitlines()[0] - and 'ballistica' + 'core' == 'ballisticacore' + and 'ballistica' + 'kit' == 'ballisticakit' ): - # We seem to be in master in core repo; lets do it. print(master_assets_dir) return @@ -259,14 +191,17 @@ def androidaddr() -> None: def push_ipa() -> None: """Construct and push ios IPA for testing.""" - from pathlib import Path + + from efrotools import extract_arg import efrotools.ios - root = Path(sys.argv[0], '../..').resolve() - if len(sys.argv) != 3: - raise Exception('expected 1 arg (debug or release)') - modename = sys.argv[2].lower() - efrotools.ios.push_ipa(root, modename) + args = sys.argv[2:] + signing_config = extract_arg(args, '--signing-config') + + if len(args) != 1: + raise RuntimeError('Expected 1 mode arg (debug or release).') + modename = args[0].lower() + efrotools.ios.push_ipa(PROJROOT, modename, signing_config=signing_config) def printcolors() -> None: @@ -368,17 +303,18 @@ def python_build_apple_debug() -> None: def _python_build_apple(debug: bool) -> None: """Build an embeddable python for macOS/iOS/tvOS.""" import os + from efro.error import CleanError from efrotools import pybuild os.chdir(PROJROOT) archs = ('mac', 'ios', 'tvos') if len(sys.argv) != 3: - print('ERROR: expected one arg: ' + ', '.join(archs)) - sys.exit(255) + raise CleanError('Error: expected one arg: ' + ', '.join(archs)) arch = sys.argv[2] if arch not in archs: - print('ERROR: invalid arch. valid values are: ' + ', '.join(archs)) - sys.exit(255) + raise CleanError( + 'Error: invalid arch. valid values are: ' + ', '.join(archs) + ) pybuild.build_apple(arch, debug=debug) @@ -394,17 +330,18 @@ def python_build_android_debug() -> None: def _python_build_android(debug: bool) -> None: import os + from efro.error import CleanError from efrotools import pybuild os.chdir(PROJROOT) archs = ('arm', 'arm64', 'x86', 'x86_64') if len(sys.argv) != 3: - print('ERROR: expected one arg: ' + ', '.join(archs)) - sys.exit(255) + raise CleanError('Error: Expected one arg: ' + ', '.join(archs)) arch = sys.argv[2] if arch not in archs: - print('ERROR: invalid arch. valid values are: ' + ', '.join(archs)) - sys.exit(255) + raise CleanError( + 'Error: invalid arch. valid values are: ' + ', '.join(archs) + ) pybuild.build_android(str(PROJROOT), arch, debug=debug) @@ -426,13 +363,19 @@ def python_android_patch_ssl() -> None: def python_apple_patch() -> None: """Patches Python to prep for building for Apple platforms.""" + from efro.error import CleanError from efrotools import pybuild - arch = sys.argv[2] - slc = sys.argv[3] - assert slc - assert ' ' not in slc - pybuild.apple_patch(arch, slc) + if len(sys.argv) != 3: + raise CleanError('Expected 1 arg.') + + pydir: str = sys.argv[2] + pybuild.apple_patch(pydir) + # arch = sys.argv[2] + # slc = sys.argv[3] + # assert slc + # assert ' ' not in slc + # pybuild.apple_patch(arch, slc) def python_gather() -> None: @@ -488,7 +431,7 @@ def efrocache_update() -> None: """Build & push files to efrocache for public access.""" from efrotools.efrocache import update_cache - makefile_dirs = ['', 'assets', 'resources', 'src/meta'] + makefile_dirs = ['', 'src/assets', 'src/resources', 'src/meta'] update_cache(makefile_dirs) @@ -811,7 +754,7 @@ def logcat() -> None: else: format_args = '-v color ' cmd = ( - f'{adb} logcat {format_args}SDL:V BallisticaCore:V VrLib:V' + f'{adb} logcat {format_args}SDL:V BallisticaKit:V VrLib:V' ' VrApi:V VrApp:V TimeWarp:V EyeBuf:V GlUtils:V DirectRender:V' ' HmdInfo:V IabHelper:V CrashAnrDetector:V DEBUG:V \'*:S\'' ) @@ -819,42 +762,6 @@ def logcat() -> None: subprocess.run(cmd, shell=True, check=True) -def android_archive_unstripped_libs() -> None: - """Copy libs to a build archive.""" - import subprocess - from pathlib import Path - from efro.error import CleanError - from efro.terminal import Clr - - if len(sys.argv) != 4: - raise CleanError('Expected 2 args; src-dir and dst-dir') - src = Path(sys.argv[2]) - dst = Path(sys.argv[3]) - if dst.exists(): - subprocess.run(['rm', '-rf', dst], check=True) - dst.mkdir(parents=True, exist_ok=True) - if not src.is_dir(): - raise CleanError(f"Source dir not found: '{src}'") - libname = 'libmain' - libext = '.so' - for abi, abishort in [ - ('armeabi-v7a', 'arm'), - ('arm64-v8a', 'arm64'), - ('x86', 'x86'), - ('x86_64', 'x86-64'), - ]: - srcpath = Path(src, abi, libname + libext) - dstname = f'{libname}_{abishort}{libext}' - dstpath = Path(dst, dstname) - if srcpath.exists(): - print(f'Archiving unstripped library: {Clr.BLD}{dstname}{Clr.RST}') - subprocess.run(['cp', srcpath, dstpath], check=True) - subprocess.run( - ['tar', '-zcf', dstname + '.tgz', dstname], cwd=dst, check=True - ) - subprocess.run(['rm', dstpath], check=True) - - def _camel_case_split(string: str) -> list[str]: words = [[string[0]]] for char in string[1:]: @@ -879,7 +786,7 @@ def efro_gradle() -> None: enabled_tags = {'google', 'crashlytics'} prev_suffix = 'efro_gradle_prev' - buildfilename = 'BallisticaCore/build.gradle' + buildfilename = 'BallisticaKit/build.gradle' # Move the original file out of the way and operate on a copy of it. subprocess.run( @@ -918,14 +825,6 @@ def stage_assets() -> None: sys.exit(1) -def update_assets_makefile() -> None: - """Update the assets makefile.""" - from batools.assetsmakefile import update_assets_makefile as uam - - check = '--check' in sys.argv - uam(projroot=str(PROJROOT), check=check) - - def update_project() -> None: """Update project files. @@ -942,59 +841,18 @@ def update_project() -> None: any files but instead fail if any modifications *would* have been made. (used in CI builds to make sure things are kosher). """ - from batools.project import Updater + import os + from batools.project import ProjectUpdater check = '--check' in sys.argv fix = '--fix' in sys.argv - Updater(check=check, fix=fix).run() + # ProjectUpdater is supposed to work from any dir, so let's keep + # ourself honest by forcing the issue. + cwd = os.getcwd() + os.chdir('/') - -def update_cmake_prefab_lib() -> None: - """Update prefab internal libs for builds.""" - import subprocess - import os - from efro.error import CleanError - import batools.build - - if len(sys.argv) != 5: - raise CleanError( - 'Expected 3 args (standard/server, debug/release, build-dir)' - ) - buildtype = sys.argv[2] - mode = sys.argv[3] - builddir = sys.argv[4] - if buildtype not in {'standard', 'server'}: - raise CleanError(f'Invalid buildtype: {buildtype}') - if mode not in {'debug', 'release'}: - raise CleanError(f'Invalid mode: {mode}') - platform = batools.build.get_current_prefab_platform( - wsl_gives_windows=False - ) - suffix = '_server' if buildtype == 'server' else '_gui' - target = ( - f'build/prefab/lib/{platform}{suffix}/{mode}/' - f'libballisticacore_internal.a' - ) - - # Build the target and then copy it to dst if it doesn't exist there yet - # or the existing one is older than our target. - subprocess.run(['make', target], check=True) - - libdir = os.path.join(builddir, 'prefablib') - libpath = os.path.join(libdir, 'libballisticacore_internal.a') - - update = True - time1 = os.path.getmtime(target) - if os.path.exists(libpath): - time2 = os.path.getmtime(libpath) - if time1 <= time2: - update = False - - if update: - if not os.path.exists(libdir): - os.makedirs(libdir, exist_ok=True) - subprocess.run(['cp', target, libdir], check=True) + ProjectUpdater(cwd, check=check, fix=fix).run() def cmake_prep_dir() -> None: @@ -1016,82 +874,29 @@ def cmake_prep_dir() -> None: def gen_binding_code() -> None: - """Generate binding.inc file.""" + """Generate a binding_foo.inc file.""" from efro.error import CleanError - import batools.meta + import batools.metabuild if len(sys.argv) != 4: raise CleanError('Expected 2 args (srcfile, dstfile)') inpath = sys.argv[2] outpath = sys.argv[3] - batools.meta.gen_binding_code(str(PROJROOT), inpath, outpath) + batools.metabuild.gen_binding_code(str(PROJROOT), inpath, outpath) def gen_flat_data_code() -> None: """Generate a C++ include file from a Python file.""" from efro.error import CleanError - import batools.meta + import batools.metabuild if len(sys.argv) != 5: raise CleanError('Expected 3 args (srcfile, dstfile, varname)') inpath = sys.argv[2] outpath = sys.argv[3] varname = sys.argv[4] - batools.meta.gen_flat_data_code(str(PROJROOT), inpath, outpath, varname) - - -def win_ci_install_prereqs() -> None: - """Install bits needed for basic win ci.""" - import json - from efrotools.efrocache import get_target - - # We'll need to pull a handful of things out of efrocache for the - # build to succeed. Normally this would happen through our Makefile - # targets but we can't use them under raw window so we need to just - # hard-code whatever we need here. - lib_dbg_win32 = 'build/prefab/lib/windows/Debug_Win32' - needed_targets: set[str] = { - f'{lib_dbg_win32}/BallisticaCoreGenericInternal.lib', - f'{lib_dbg_win32}/BallisticaCoreGenericInternal.pdb', - 'ballisticacore-windows/Generic/BallisticaCore.ico', - } - - # Look through everything that gets generated by our meta builds - # and pick out anything we need for our basic builds/tests. - with open( - 'src/meta/.meta_manifest_public.json', encoding='utf-8' - ) as infile: - meta_public: list[str] = json.loads(infile.read()) - with open( - 'src/meta/.meta_manifest_private.json', encoding='utf-8' - ) as infile: - meta_private: list[str] = json.loads(infile.read()) - for target in meta_public + meta_private: - if target.startswith('src/ballistica/generated/') or target.startswith( - 'assets/src/ba_data/python/ba/_generated/' - ): - needed_targets.add(target) - - for target in needed_targets: - get_target(target) - - -def win_ci_binary_build() -> None: - """Simple windows binary build for ci.""" - import subprocess - - # Do the thing. - subprocess.run( - [ - 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\' - 'Enterprise\\MSBuild\\Current\\Bin\\MSBuild.exe', - 'ballisticacore-windows\\Generic\\BallisticaCoreGeneric.vcxproj', - '-target:Build', - '-property:Configuration=Debug', - '-property:Platform=Win32', - '-property:VisualStudioVersion=16', - ], - check=True, + batools.metabuild.gen_flat_data_code( + str(PROJROOT), inpath, outpath, varname ) @@ -1109,26 +914,12 @@ def android_sdk_utils() -> None: run(projroot=str(PROJROOT), args=sys.argv[2:]) -def update_resources_makefile() -> None: - """Update the resources Makefile if needed.""" - from batools.resourcesmakefile import update - - update(projroot=str(PROJROOT), check='--check' in sys.argv) - - -def update_meta_makefile() -> None: - """Update the meta Makefile if needed.""" - from batools.metamakefile import update - - update(projroot=str(PROJROOT), check='--check' in sys.argv) - - def gen_python_enums_module() -> None: """Update our procedurally generated python enums.""" from batools.pythonenumsmodule import generate if len(sys.argv) != 4: - raise Exception('Expected infile and outfile args.') + raise RuntimeError('Expected infile and outfile args.') generate( projroot=str(PROJROOT), infilename=sys.argv[2], outfilename=sys.argv[3] ) @@ -1141,7 +932,7 @@ def gen_python_init_module() -> None: from batools.project import project_centric_path if len(sys.argv) != 3: - raise Exception('Expected an outfile arg.') + raise RuntimeError('Expected an outfile arg.') outfilename = sys.argv[2] os.makedirs(os.path.dirname(outfilename), exist_ok=True) prettypath = project_centric_path(projroot=str(PROJROOT), path=outfilename) @@ -1154,15 +945,15 @@ def gen_python_init_module() -> None: ) -def update_dummy_modules() -> None: - """Update our _ba.py and _bainternal.py dummy modules.""" - from batools.dummymodule import update +def gen_dummy_modules() -> None: + """Generate all dummy modules.""" + from efro.error import CleanError + from batools.dummymodule import generate - update( - projroot=str(PROJROOT), - check='--check' in sys.argv, - force='--force' in sys.argv, - ) + if len(sys.argv) != 2: + raise CleanError(f'Expected no args; got {len(sys.argv)-2}.') + + generate(projroot=str(PROJROOT)) def version() -> None: diff --git a/tools/batools/pcommand2.py b/tools/batools/pcommand2.py new file mode 100644 index 00000000..4af16aa5 --- /dev/null +++ b/tools/batools/pcommand2.py @@ -0,0 +1,304 @@ +# Released under the MIT License. See LICENSE for details. +# +# pylint: disable=too-many-lines +"""A nice collection of ready-to-use pcommands for this package.""" +from __future__ import annotations + +# Note: import as little as possible here at the module level to +# keep launch times fast for small snippets. +import sys + +from efrotools.pcommand import PROJROOT + + +def gen_monolithic_register_modules() -> None: + """Generate .h file for registering py modules in monolithic builds.""" + import os + import textwrap + + from efro.error import CleanError + from batools.featureset import FeatureSet + + if len(sys.argv) != 3: + raise CleanError('Expected 1 arg.') + outpath = sys.argv[2] + + featuresets = FeatureSet.get_all_for_project(str(PROJROOT)) + + # Filter out ones without native modules. + featuresets = [f for f in featuresets if f.has_native_python_module] + + pymodulenames = sorted(f.name_python_binary_module for f in featuresets) + + extern_def_code = '\n'.join( + f'auto PyInit_{n}() -> PyObject*;' for n in pymodulenames + ) + py_register_code = '\n'.join( + f'PyImport_AppendInittab("{n}", &PyInit_{n});' for n in pymodulenames + ) + base_code = """ + // Released under the MIT License. See LICENSE for details. + + #ifndef BALLISTICA_CORE_MGEN_PYTHON_MODULES_MONOLITHIC_H_ + #define BALLISTICA_CORE_MGEN_PYTHON_MODULES_MONOLITHIC_H_ + + // THIS CODE IS AUTOGENERATED BY META BUILD; DO NOT EDIT BY HAND. + + #include "ballistica/shared/python/python_sys.h" + + #if BA_MONOLITHIC_BUILD + extern "C" { + ${EXTERN_DEF_CODE} + } + #endif // BA_MONOLITHIC_BUILD + + namespace ballistica { + + /// Register init calls for all of our built-in Python modules. + /// Should only be used in monolithic builds. In modular builds + /// binary modules get located as .so files on disk as per regular + /// Python behavior. + void MonolithicRegisterPythonModules() { + #if BA_MONOLITHIC_BUILD + ${PY_REGISTER_CODE} + #else + FatalError( + "MonolithicRegisterPythonModules should not be called" + " in modular builds."); + #endif // BA_MONOLITHIC_BUILD + } + + } // namespace ballistica + + #endif // BALLISTICA_CORE_MGEN_PYTHON_MODULES_MONOLITHIC_H_ + """ + out = ( + textwrap.dedent(base_code) + .replace('${EXTERN_DEF_CODE}', extern_def_code) + .replace('${PY_REGISTER_CODE}', textwrap.indent(py_register_code, ' ')) + .strip() + + '\n' + ) + + os.makedirs(os.path.dirname(outpath), exist_ok=True) + with open(outpath, 'w', encoding='utf-8') as outfile: + outfile.write(out) + + +def stage_server_file() -> None: + """Stage files for the server environment with some filtering.""" + from efro.error import CleanError + import batools.assetstaging + + if len(sys.argv) != 5: + raise CleanError('Expected 3 args (mode, infile, outfile).') + mode, infilename, outfilename = sys.argv[2], sys.argv[3], sys.argv[4] + batools.assetstaging.stage_server_file( + str(PROJROOT), mode, infilename, outfilename + ) + + +def py_examine() -> None: + """Run a python examination at a given point in a given file.""" + import os + from pathlib import Path + import efrotools + + if len(sys.argv) != 7: + print('ERROR: expected 7 args') + sys.exit(255) + filename = Path(sys.argv[2]) + line = int(sys.argv[3]) + column = int(sys.argv[4]) + selection: str | None = None if sys.argv[5] == '' else sys.argv[5] + operation = sys.argv[6] + + # This stuff assumes it is being run from project root. + os.chdir(PROJROOT) + + # Set up pypaths so our main distro stuff works. + scriptsdir = os.path.abspath( + os.path.join( + os.path.dirname(sys.argv[0]), '../src/assets/ba_data/python' + ) + ) + toolsdir = os.path.abspath( + os.path.join(os.path.dirname(sys.argv[0]), '../tools') + ) + if scriptsdir not in sys.path: + sys.path.append(scriptsdir) + if toolsdir not in sys.path: + sys.path.append(toolsdir) + efrotools.py_examine(PROJROOT, filename, line, column, selection, operation) + + +def clean_orphaned_assets() -> None: + """Remove asset files that are no longer part of the build.""" + import os + import json + import subprocess + + # Operate from dist root.. + os.chdir(PROJROOT) + + # Our manifest is split into 2 files (public and private) + with open( + 'src/assets/.asset_manifest_public.json', encoding='utf-8' + ) as infile: + manifest = set(json.loads(infile.read())) + with open( + 'src/assets/.asset_manifest_private.json', encoding='utf-8' + ) as infile: + manifest.update(set(json.loads(infile.read()))) + for root, _dirs, fnames in os.walk('build/assets'): + for fname in fnames: + fpath = os.path.join(root, fname) + fpathrel = fpath[13:] # paths are relative to build/assets + if fpathrel not in manifest: + print(f'Removing orphaned asset file: {fpath}') + os.unlink(fpath) + + # Lastly, clear empty dirs. + subprocess.run( + 'find build/assets -depth -empty -type d -delete', + shell=True, + check=True, + ) + + +def win_ci_install_prereqs() -> None: + """Install bits needed for basic win ci.""" + import json + from efrotools.efrocache import get_target + + # We'll need to pull a handful of things out of efrocache for the + # build to succeed. Normally this would happen through our Makefile + # targets but we can't use them under raw window so we need to just + # hard-code whatever we need here. + lib_dbg_win32 = 'build/prefab/lib/windows/Debug_Win32' + needed_targets: set[str] = { + f'{lib_dbg_win32}/BallisticaKitGenericInternal.lib', + f'{lib_dbg_win32}/BallisticaKitGenericInternal.pdb', + 'ballisticakit-windows/Generic/BallisticaKit.ico', + } + + # Look through everything that gets generated by our meta builds + # and pick out anything we need for our basic builds/tests. + with open( + 'src/meta/.meta_manifest_public.json', encoding='utf-8' + ) as infile: + meta_public: list[str] = json.loads(infile.read()) + with open( + 'src/meta/.meta_manifest_private.json', encoding='utf-8' + ) as infile: + meta_private: list[str] = json.loads(infile.read()) + for target in meta_public + meta_private: + if target.startswith('src/ballistica/mgen/') or target.startswith( + 'src/assets/ba_data/python/babase/_mgen/' + ): + needed_targets.add(target) + + for target in needed_targets: + get_target(target) + + +def win_ci_binary_build() -> None: + """Simple windows binary build for ci.""" + import subprocess + + # Do the thing. + subprocess.run( + [ + 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\' + 'Enterprise\\MSBuild\\Current\\Bin\\MSBuild.exe', + 'ballisticakit-windows\\Generic\\BallisticaKitGeneric.vcxproj', + '-target:Build', + '-property:Configuration=Debug', + '-property:Platform=Win32', + '-property:VisualStudioVersion=16', + ], + check=True, + ) + + +def update_cmake_prefab_lib() -> None: + """Update prefab internal libs for builds.""" + import subprocess + import os + from efro.error import CleanError + import batools.build + + if len(sys.argv) != 5: + raise CleanError( + 'Expected 3 args (standard/server, debug/release, build-dir)' + ) + buildtype = sys.argv[2] + mode = sys.argv[3] + builddir = sys.argv[4] + if buildtype not in {'standard', 'server'}: + raise CleanError(f'Invalid buildtype: {buildtype}') + if mode not in {'debug', 'release'}: + raise CleanError(f'Invalid mode: {mode}') + platform = batools.build.get_current_prefab_platform( + wsl_gives_windows=False + ) + suffix = '_server' if buildtype == 'server' else '_gui' + target = ( + f'build/prefab/lib/{platform}{suffix}/{mode}/' + f'libballisticakit_internal.a' + ) + + # Build the target and then copy it to dst if it doesn't exist there yet + # or the existing one is older than our target. + subprocess.run(['make', target], check=True) + + libdir = os.path.join(builddir, 'prefablib') + libpath = os.path.join(libdir, 'libballisticakit_internal.a') + + update = True + time1 = os.path.getmtime(target) + if os.path.exists(libpath): + time2 = os.path.getmtime(libpath) + if time1 <= time2: + update = False + + if update: + if not os.path.exists(libdir): + os.makedirs(libdir, exist_ok=True) + subprocess.run(['cp', target, libdir], check=True) + + +def android_archive_unstripped_libs() -> None: + """Copy libs to a build archive.""" + import subprocess + from pathlib import Path + from efro.error import CleanError + from efro.terminal import Clr + + if len(sys.argv) != 4: + raise CleanError('Expected 2 args; src-dir and dst-dir') + src = Path(sys.argv[2]) + dst = Path(sys.argv[3]) + if dst.exists(): + subprocess.run(['rm', '-rf', dst], check=True) + dst.mkdir(parents=True, exist_ok=True) + if not src.is_dir(): + raise CleanError(f"Source dir not found: '{src}'") + libname = 'libmain' + libext = '.so' + for abi, abishort in [ + ('armeabi-v7a', 'arm'), + ('arm64-v8a', 'arm64'), + ('x86', 'x86'), + ('x86_64', 'x86-64'), + ]: + srcpath = Path(src, abi, libname + libext) + dstname = f'{libname}_{abishort}{libext}' + dstpath = Path(dst, dstname) + if srcpath.exists(): + print(f'Archiving unstripped library: {Clr.BLD}{dstname}{Clr.RST}') + subprocess.run(['cp', srcpath, dstpath], check=True) + subprocess.run( + ['tar', '-zcf', dstname + '.tgz', dstname], cwd=dst, check=True + ) + subprocess.run(['rm', dstpath], check=True) diff --git a/tools/batools/project.py b/tools/batools/project.py deleted file mode 100755 index f4d82d7f..00000000 --- a/tools/batools/project.py +++ /dev/null @@ -1,800 +0,0 @@ -# Released under the MIT License. See LICENSE for details. -# -"""General project related functionality.""" - -from __future__ import annotations - -import os -import sys -import subprocess -from dataclasses import dataclass -from typing import TYPE_CHECKING - -from efro.error import CleanError -from efro.terminal import Clr - -if TYPE_CHECKING: - pass - - -def project_centric_path(projroot: str, path: str) -> str: - """Convert a CWD-relative path to a project-relative one.""" - abspath = os.path.abspath(path) - if abspath == projroot: - return '.' - projprefix = f'{projroot}/' - if not abspath.startswith(projprefix): - raise RuntimeError( - f'Path "{abspath}" is not under project root "{projprefix}"' - ) - return abspath[len(projprefix) :] - - -def get_legal_notice_private() -> str: - """Return the one line legal notice we expect private files to have.""" - return 'Copyright (c) 2011-2022 Eric Froemling' - - -@dataclass -class LineChange: - """A change applying to a particular line in a file.""" - - line_number: int - expected: str - can_auto_update: bool - - -class Updater: - """Context for an app run.""" - - def __init__(self, check: bool, fix: bool) -> None: - from efrotools import getconfig, getlocalconfig - from pathlib import Path - - self._check = check - self._fix = fix - self._checkarg = ' --check' if self._check else '' - self._checkarglist: list[str] = ['--check'] if self._check else [] - - # We behave differently in the public repo - self._public = getconfig(Path('.'))['public'] - assert isinstance(self._public, bool) - - self._source_files: list[str] = [] - self._header_files: list[str] = [] - - self._line_corrections: dict[str, list[LineChange]] = {} - self._file_changes: dict[str, str] = {} - - self._license_line_checks = bool( - getlocalconfig(Path('.')).get('license_line_checks', True) - ) - - self._internal_source_dirs: set[str] | None = None - self._internal_source_files: set[str] | None = None - - def run(self) -> None: - """Do the thing.""" - - # Make sure we're operating from a project root. - if not os.path.isdir('config') or not os.path.isdir('tools'): - raise Exception('This must be run from a project root.') - - # Note: we need to update the meta Makefile first since its output - # manifest may be used when generating asset/resource targets. - self._update_meta_makefile() - self._update_resources_makefile() - self._update_assets_makefile() - - self._check_makefiles() - self._check_python_files() - self._check_sync_states() - self._check_misc() - - self._find_sources_and_headers('src/ballistica') - - # FIXME: It might make more sense to have some of these checks - # run via 'make check' rather than here through 'make update'. - self._check_source_files() - self._check_headers() - - self._update_cmake_files() - self._update_visual_studio_projects() - - # If we're all good to here, do the actual writes we set up above. - self._apply_line_changes() - self._apply_file_changes() - - self._update_dummy_modules() - - # Though not technically necessary, let's go ahead and update - # irony compile-commands, tool configs, etc. as part of the - # update process. This lessens the chance we'll have tools - # behave funny until the next time we run a build. - subprocess.run(['make', f'-j{os.cpu_count()}', 'prereqs'], check=True) - - if self._check: - print(f'{Clr.BLU}Check-Builds: Everything up to date.{Clr.RST}') - else: - print(f'{Clr.GRN}Update-Project: SUCCESS!{Clr.RST}') - - def _get_internal_source_files(self) -> set[str]: - from pathlib import Path - from efrotools import getconfig - - # Fetch/calc just once and cache results. - if self._internal_source_files is None: - sources: list[str] - if self._public: - sources = [] - else: - sources = getconfig(Path('.')).get('internal_source_files', []) - if not isinstance(sources, list): - raise CleanError( - f'Expected list for internal_source_files;' - f' got {type(sources)}' - ) - self._internal_source_files = set(sources) - return self._internal_source_files - - def _get_internal_source_dirs(self) -> set[str]: - from pathlib import Path - from efrotools import getconfig - - # Fetch/calc just once and cache results. - if self._internal_source_dirs is None: - sources: list[str] - if self._public: - sources = [] - else: - sources = getconfig(Path('.')).get('internal_source_dirs', []) - if not isinstance(sources, list): - raise CleanError( - f'Expected list for internal_source_dirs;' - f' got {type(sources)}' - ) - self._internal_source_dirs = set(sources) - return self._internal_source_dirs - - def _apply_file_changes(self) -> None: - # Now write out any project files that have changed - # (or error if we're in check mode). - unchanged_project_count = 0 - for fname, fcode in self._file_changes.items(): - f_orig: str | None - if os.path.exists(fname): - with open(fname, 'r', encoding='utf-8') as infile: - f_orig = infile.read() - else: - f_orig = None - if f_orig == fcode.replace('\r\n', '\n'): - unchanged_project_count += 1 - else: - if self._check: - print( - f'{Clr.RED}ERROR: found out-of-date' - f' project file: {fname}{Clr.RST}' - ) - sys.exit(255) - - print(f'{Clr.BLU}Writing project file: {fname}{Clr.RST}') - with open(fname, 'w', encoding='utf-8') as outfile: - outfile.write(fcode) - if unchanged_project_count > 0: - print( - f'All {unchanged_project_count} project files are up to date.' - ) - - def _apply_line_changes(self) -> None: - - # Build a flat list of entries that can and can-not be auto applied. - manual_changes: list[tuple[str, LineChange]] = [] - auto_changes: list[tuple[str, LineChange]] = [] - for fname, entries in self._line_corrections.items(): - for entry in entries: - if entry.can_auto_update: - auto_changes.append((fname, entry)) - else: - manual_changes.append((fname, entry)) - - # If there are any manual-only entries, list then and bail. - # (Don't wanna allow auto-apply unless it fixes everything) - if manual_changes: - print( - f'{Clr.RED}Found erroneous lines ' - f'requiring manual correction:{Clr.RST}' - ) - for change in manual_changes: - print( - f'{Clr.RED}{change[0]}:{change[1].line_number + 1}:' - f' Expected line to be:\n {change[1].expected}{Clr.RST}' - ) - - sys.exit(-1) - - # Now, if we've got auto entries, either list or auto-correct them. - if auto_changes: - if not self._fix: - for i, change in enumerate(auto_changes): - print( - f'{Clr.RED}#{i}:' - f' {change[0]}:{change[1].line_number+1}:' - f'{Clr.RST}' - ) - print( - f'{Clr.RED} Expected "{change[1].expected}"{Clr.RST}' - ) - with open(change[0], encoding='utf-8') as infile: - lines = infile.read().splitlines() - line = lines[change[1].line_number] - print(f'{Clr.RED} Found "{line}"{Clr.RST}') - print( - f'{Clr.RED}All {len(auto_changes)} errors are' - f' auto-fixable; run tools/pcommand update_project' - f' --fix to apply corrections. {Clr.RST}' - ) - sys.exit(255) - else: - for i, change in enumerate(auto_changes): - print(f'{Clr.BLU}Correcting file: {change[0]}{Clr.RST}') - with open(change[0], encoding='utf-8') as infile: - lines = infile.read().splitlines() - lines[change[1].line_number] = change[1].expected - with open(change[0], 'w', encoding='utf-8') as outfile: - outfile.write('\n'.join(lines) + '\n') - - # If there were no issues whatsoever, note that. - if not manual_changes and not auto_changes: - fcount = len(self._header_files) + len(self._source_files) - print(f'No issues found in {fcount} source files.') - - def _check_source_files(self) -> None: - - for fsrc in self._source_files: - if fsrc.endswith('.cpp') or fsrc.endswith('.cxx'): - raise Exception('please use .cc for c++ files; found ' + fsrc) - - # Watch out for in-progress emacs edits. - # Could just ignore these but it probably means I intended - # to save something and forgot. - if '/.#' in fsrc: - print( - f'{Clr.RED}' - f'ERROR: Found an unsaved emacs file: "{fsrc}"' - f'{Clr.RST}' - ) - sys.exit(255) - - fname = 'src/ballistica' + fsrc - self._check_source_file(fname) - - def _check_source_file(self, fname: str) -> None: - with open(fname, encoding='utf-8') as infile: - lines = infile.read().splitlines() - - if self._license_line_checks: - self._check_c_license(fname, lines) - - def _check_headers(self) -> None: - for header_file_raw in self._header_files: - assert header_file_raw[0] == '/' - header_file = 'src/ballistica' + header_file_raw - - if header_file.endswith('.h'): - self._check_header(header_file) - - def _add_line_correction( - self, - filename: str, - line_number: int, - expected: str, - can_auto_update: bool, - ) -> None: - # No longer allowing negatives here since they don't show up nicely - # in correction list. - assert line_number >= 0 - self._line_corrections.setdefault(filename, []).append( - LineChange( - line_number=line_number, - expected=expected, - can_auto_update=can_auto_update, - ) - ) - - def _check_c_license(self, fname: str, lines: list[str]) -> None: - from efrotools import get_public_license - - # Look for public license line (public or private repo) - # or private license line (private repo only) - line_private = '// ' + get_legal_notice_private() - line_public = get_public_license('c++') - lnum = 0 - - if self._public: - if lines[lnum] != line_public: - # Allow auto-correcting from private to public line - allow_auto = lines[lnum] == line_private - self._add_line_correction( - fname, - line_number=lnum, - expected=line_public, - can_auto_update=allow_auto, - ) - else: - if lines[lnum] not in [line_public, line_private]: - self._add_line_correction( - fname, - line_number=lnum, - expected=line_private, - can_auto_update=False, - ) - - def _check_header(self, fname: str) -> None: - - # Make sure its define guard is correct. - guard = fname[4:].upper().replace('/', '_').replace('.', '_') + '_' - with open(fname, encoding='utf-8') as fhdr: - lines = fhdr.read().splitlines() - - if self._license_line_checks: - self._check_c_license(fname, lines) - - # Check for header guard lines at top - line = '#ifndef ' + guard - lnum = 2 - if lines[lnum] != line: - # Allow auto-correcting if it looks close already - # (don't want to blow away an unrelated line) - allow_auto = lines[lnum].startswith('#ifndef BALLISTICA_') - self._add_line_correction( - fname, - line_number=lnum, - expected=line, - can_auto_update=allow_auto, - ) - line = '#define ' + guard - lnum = 3 - if lines[lnum] != line: - # Allow auto-correcting if it looks close already - # (don't want to blow away an unrelated line) - allow_auto = lines[lnum].startswith('#define BALLISTICA_') - self._add_line_correction( - fname, - line_number=lnum, - expected=line, - can_auto_update=allow_auto, - ) - - # Check for header guard at bottom - line = '#endif // ' + guard - lnum = len(lines) - 1 - if lines[lnum] != line: - # Allow auto-correcting if it looks close already - # (don't want to blow away an unrelated line) - allow_auto = lines[lnum].startswith('#endif // BALLISTICA_') - self._add_line_correction( - fname, - line_number=lnum, - expected=line, - can_auto_update=allow_auto, - ) - - def _check_makefiles(self) -> None: - from efrotools import get_public_license - - # Run a few sanity checks on whatever makefiles we come across. - fnames = ( - subprocess.run( - 'find . -maxdepth 3 -name Makefile', - shell=True, - capture_output=True, - check=True, - ) - .stdout.decode() - .split() - ) - fnames = [n for n in fnames if '/build/' not in n] - - for fname in fnames: - with open(fname, encoding='utf-8') as infile: - makefile = infile.read() - if self._public: - public_license = get_public_license('makefile') - if public_license not in makefile: - raise CleanError(f'Pub license not found in {fname}.') - else: - if ( - get_legal_notice_private() not in makefile - and get_public_license('makefile') not in makefile - ): - raise CleanError(f'Priv or pub legal not found in {fname}.') - - def _check_python_file(self, fname: str) -> None: - # pylint: disable=too-many-branches - from efrotools import get_public_license, PYVER - - with open(fname, encoding='utf-8') as infile: - contents = infile.read() - lines = contents.splitlines() - - # Make sure all standalone scripts are pointing to the right - # version of python (with a few exceptions where it needs to - # differ) - if contents.startswith('#!/'): - copyrightline = 1 - if fname not in ['tools/vmshell']: - if not contents.startswith(f'#!/usr/bin/env python{PYVER}'): - raise CleanError( - f'Incorrect shebang (first line) for ' f'{fname}.' - ) - else: - copyrightline = 0 - - # Special case: it there's spinoff autogenerate notice there, - # look below it. - if ( - lines[copyrightline] == '' - and 'THIS FILE IS AUTOGENERATED' in lines[copyrightline + 1] - ): - copyrightline += 2 - - if lines[copyrightline].startswith('# Synced from '): - copyrightline += 3 - - for i, line in enumerate(lines): - # stuff under the ba module. - if '/ba/' in fname: - # Don't allow importing ba at the top level from within ba. - if line == 'import ba': - raise CleanError( - f'{fname}:{i+1}: no top level ba imports allowed' - f' under ba module.' - ) - if '/bastd/' in fname: - # Don't allow importing _ba or _bainternal anywhere here. - # (any internal needs should be in ba.internal) - if 'import _ba' in line: - raise CleanError( - f'{fname}:{i+1}: _ba or _bainternal imports not' - f' allowed under bastd.' - ) - - # In all cases, look for our one-line legal notice. - # In the public case, look for the rest of our public license too. - if self._license_line_checks: - public_license = get_public_license('python') - private_license = '# ' + get_legal_notice_private() - lnum = copyrightline - if len(lines) < lnum + 1: - raise RuntimeError('Not enough lines in file:', fname) - - disable_note = ( - 'NOTE: You can disable license line' - ' checks by adding "license_line_checks": false\n' - 'to the root dict in config/localconfig.json.\n' - 'see https://ballistica.net/wiki' - '/Knowledge-Nuggets#' - 'hello-world-creating-a-new-game-type' - ) - - if self._public: - # Check for public license only. - if lines[lnum] != public_license: - raise CleanError( - f'License text not found' - f" at '{fname}' line {lnum+1};" - f' please correct.\n' - f'Expected text is: {public_license}\n' - f'{disable_note}' - ) - else: - # Check for public or private license. - if ( - lines[lnum] != public_license - and lines[lnum] != private_license - ): - raise CleanError( - f'License text not found' - f" at '{fname}' line {lnum+1};" - f' please correct.\n' - f'Expected text (for public files):' - f' {public_license}\n' - f'Expected text (for private files):' - f' {private_license}\n' - f'{disable_note}' - ) - - def _check_python_files(self) -> None: - from pathlib import Path - from efrotools.code import get_script_filenames - - scriptfiles = get_script_filenames(Path('.')) - for fname in scriptfiles: - self._check_python_file(fname) - - # Check our packages and make sure all subdirs contain and __init__.py - # (I tend to forget this sometimes) - packagedirs = ['tools/efrotools', 'tools/efro'] - - # (Assume all dirs under these dirs are packages) - dirs_of_packages = ['assets/src/ba_data/python', 'tests'] - for dir_of_packages in dirs_of_packages: - for name in os.listdir(dir_of_packages): - if not name.startswith('.') and os.path.isdir( - os.path.join(dir_of_packages, name) - ): - packagedirs.append(os.path.join(dir_of_packages, name)) - - for packagedir in packagedirs: - for root, _dirs, files in os.walk(packagedir): - if ( - '__pycache__' not in root - and os.path.basename(root) != '.vscode' - ): - if '__init__.py' not in files: - print( - Clr.RED - + 'Error: no __init__.py in package dir: ' - + root - + Clr.RST - ) - sys.exit(255) - - def _update_visual_studio_project(self, basename: str) -> None: - - fname = ( - f'ballisticacore-windows/{basename}/' - f'BallisticaCore{basename}.vcxproj' - ) - - # Currently just silently skipping if not found (for public repo). - if not os.path.exists(fname): - raise CleanError(f'Visual Studio project not found: {fname}') - - with open(fname, encoding='utf-8') as infile: - lines = infile.read().splitlines() - - src_root = '..\\..\\src' - - public_project = 'Internal' not in basename - - all_files = sorted( - [ - f - for f in (self._source_files + self._header_files) - if not f.endswith('.m') - and not f.endswith('.mm') - and not f.endswith('.c') - and self._is_public_source_file(f) == public_project - ] - ) - - # Find the ItemGroup containing stdafx.cpp. This is where we'll dump - # our stuff. - index = lines.index(' ') - begin_index = end_index = index - while lines[begin_index] != ' ': - begin_index -= 1 - while lines[end_index] != ' ': - end_index += 1 - group_lines = lines[begin_index + 1 : end_index] - - # Strip out any existing files from src/ballistica. - group_lines = [ - l for l in group_lines if src_root + '\\ballistica\\' not in l - ] - - # Now add in our own. - # Note: we can't use C files in this build at the moment; breaks - # precompiled header stuff. (shouldn't be a problem though). - group_lines = [ - ' <' - + ('ClInclude' if src.endswith('.h') else 'ClCompile') - + ' Include="' - + src_root - + '\\ballistica' - + src.replace('/', '\\') - + '" />' - for src in all_files - ] + group_lines - - filtered = lines[: begin_index + 1] + group_lines + lines[end_index:] - self._file_changes[fname] = '\r\n'.join(filtered) + '\r\n' - - self._update_visual_studio_project_filters(filtered, fname, src_root) - - def _update_visual_studio_project_filters( - self, lines_in: list[str], fname: str, src_root: str - ) -> None: - filterpaths: set[str] = set() - filterlines: list[str] = [ - '', - '', - ' ', - ] - sourcelines = [l for l in lines_in if 'Include="' + src_root in l] - for line in sourcelines: - entrytype = line.strip().split()[0][1:] - path = line.split('"')[1] - filterlines.append(' <' + entrytype + ' Include="' + path + '">') - - # If we have a dir foo/bar/eep we need to create filters for - # each of foo, foo/bar, and foo/bar/eep - splits = path[len(src_root) :].split('\\') - splits = [s for s in splits if s != ''] - splits = splits[:-1] - for i in range(len(splits)): - filterpaths.add('\\'.join(splits[: (i + 1)])) - filterlines.append( - ' ' + '\\'.join(splits) + '' - ) - filterlines.append(' ') - filterlines += [ - ' ', - ' ', - ] - for filterpath in sorted(filterpaths): - filterlines.append(' ') - filterlines += [ - ' ', - '', - ] - self._file_changes[fname + '.filters'] = ( - '\r\n'.join(filterlines) + '\r\n' - ) - - def _update_visual_studio_projects(self) -> None: - self._update_visual_studio_project('Generic') - self._update_visual_studio_project('Headless') - if not self._public: - self._update_visual_studio_project('GenericInternal') - self._update_visual_studio_project('HeadlessInternal') - self._update_visual_studio_project('Oculus') - self._update_visual_studio_project('OculusInternal') - - def _is_public_source_file(self, filename: str) -> bool: - assert filename.startswith('/') - filename = f'src/ballistica{filename}' - - # If its under any of our internal source dirs, make it internal. - for srcdir in self._get_internal_source_dirs(): - assert not srcdir.startswith('/') - assert not srcdir.endswith('/') - if filename.startswith(f'{srcdir}/'): - return False - - # If its specifically listed as an internal file, make it internal. - return filename not in self._get_internal_source_files() - - def _update_cmake_file(self, fname: str) -> None: - with open(fname, encoding='utf-8') as infile: - lines = infile.read().splitlines() - - for section in ['PUBLIC', 'PRIVATE']: - # Public repo has no private section. - if self._public and section == 'PRIVATE': - continue - - auto_start = lines.index( - f' # AUTOGENERATED_{section}_BEGIN (this section' - f' is managed by the "update_project" tool)' - ) - auto_end = lines.index(f' # AUTOGENERATED_{section}_END') - our_lines = [ - ' ${BA_SRC_ROOT}/ballistica' + f - for f in sorted(self._source_files + self._header_files) - if not f.endswith('.mm') - and not f.endswith('.m') - and self._is_public_source_file(f) == (section == 'PUBLIC') - ] - lines = lines[: auto_start + 1] + our_lines + lines[auto_end:] - - self._file_changes[fname] = '\n'.join(lines) + '\n' - - def _update_cmake_files(self) -> None: - # Note: currently not updating cmake files at all in public builds; - # will need to get this working at some point... - - # Top level cmake builds: - fname = 'ballisticacore-cmake/CMakeLists.txt' - if not self._public: - self._update_cmake_file(fname) - - # CMake android components: - fname = ( - 'ballisticacore-android/BallisticaCore/src/main/cpp/CMakeLists.txt' - ) - if not self._public: - self._update_cmake_file(fname) - - def _find_sources_and_headers(self, scan_dir: str) -> None: - src_files = set() - header_files = set() - exts = ['.c', '.cc', '.cpp', '.cxx', '.m', '.mm'] - header_exts = ['.h'] - - # Gather all sources and headers. - # HMMM: Ideally we should use efrotools.code.get_code_filenames() here. - # (though we return things relative to the scan-dir which could - # throw things off) - for root, _dirs, files in os.walk(scan_dir): - for ftst in files: - if any(ftst.endswith(ext) for ext in exts): - src_files.add(os.path.join(root, ftst)[len(scan_dir) :]) - if any(ftst.endswith(ext) for ext in header_exts): - header_files.add(os.path.join(root, ftst)[len(scan_dir) :]) - self._source_files = sorted(src_files) - self._header_files = sorted(header_files) - - def _check_misc(self) -> None: - # Misc sanity checks. - - # Make sure we're set to prod master server. - # (but ONLY when checking; still want to be able to run updates). - if not self._public and self._check: - with open( - 'src/ballistica/internal/master_server_config.h', - encoding='utf-8', - ) as infile: - msconfig = infile.read() - if ( - '// V2 Master Server:\n' '\n' '// PROD\n' '#if 1\n' - ) not in msconfig: - raise CleanError('Not using prod v2 master server.') - - def _check_sync_states(self) -> None: - # Make sure none of our sync targets have been mucked with since - # their last sync. - if ( - subprocess.run( - ['tools/pcommand', 'sync', 'check'], check=False - ).returncode - != 0 - ): - raise CleanError('Sync check failed; you may need to run "sync".') - - def _update_assets_makefile(self) -> None: - if ( - subprocess.run( - ['tools/pcommand', 'update_assets_makefile', self._checkarg], - check=False, - ).returncode - != 0 - ): - print(f'{Clr.RED}Error checking/updating assets Makefile.{Clr.RST}') - sys.exit(255) - - def _update_meta_makefile(self) -> None: - try: - subprocess.run( - ['tools/pcommand', 'update_meta_makefile'] + self._checkarglist, - check=True, - ) - except Exception as exc: - raise CleanError('Error checking/updating meta Makefile.') from exc - - def _update_resources_makefile(self) -> None: - try: - subprocess.run( - ['tools/pcommand', 'update_resources_makefile'] - + self._checkarglist, - check=True, - ) - except Exception as exc: - raise CleanError( - 'Error checking/updating resources Makefile.' - ) from exc - - def _update_dummy_modules(self) -> None: - # Update our dummy _ba module. - # Note: This should happen near the end because it may run the cmake - # build so its success may depend on the cmake build files having - # already been updated. - try: - subprocess.run( - ['tools/pcommand', 'update_dummy_modules'] + self._checkarglist, - check=True, - ) - except Exception as exc: - raise CleanError('Error checking/updating dummy module.') from exc diff --git a/tools/batools/project/__init__.py b/tools/batools/project/__init__.py new file mode 100644 index 00000000..edf09162 --- /dev/null +++ b/tools/batools/project/__init__.py @@ -0,0 +1,12 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality for updating/checking the project""" + +from __future__ import annotations + +from batools.project._updater import ProjectUpdater, project_centric_path + +__all__ = [ + 'ProjectUpdater', + 'project_centric_path', +] diff --git a/tools/batools/project/_checks.py b/tools/batools/project/_checks.py new file mode 100755 index 00000000..885999c3 --- /dev/null +++ b/tools/batools/project/_checks.py @@ -0,0 +1,487 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Checks we can run on the overall project state.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +from pathlib import Path +import subprocess +import os + +from efro.error import CleanError +from efro.terminal import Clr +from efrotools import get_public_license, PYVER + +if TYPE_CHECKING: + from batools.project._updater import ProjectUpdater + from batools.featureset import FeatureSet + + +def _get_legal_notice_private() -> str: + """Return the one line legal notice we expect private files to have.""" + return 'Copyright (c) 2011-2022 Eric Froemling' + + +def check_source_files(self: ProjectUpdater) -> None: + """Check project source files.""" + for fsrc in self.source_files: + if fsrc.endswith('.cpp') or fsrc.endswith('.cxx'): + raise RuntimeError('please use .cc for c++ files; found ' + fsrc) + + # Watch out for in-progress emacs edits. + # Could just ignore these but it probably means I intended + # to save something and forgot. + if '/.#' in fsrc: + raise CleanError(f'Found an unsaved emacs file: "{fsrc}".') + + fname = 'src/ballistica' + fsrc + _check_source_file(self, fname) + + +def _check_source_file(self: ProjectUpdater, fname: str) -> None: + with open(os.path.join(self.projroot, fname), encoding='utf-8') as infile: + lines = infile.read().splitlines() + + if self.license_line_checks: + _check_c_license(self, fname, lines) + + _source_file_feature_set_namespace_check(self, fname, lines) + + +def _source_file_feature_set_namespace_check( + self: ProjectUpdater, fname: str, lines: list[str] +) -> None: + """Make sure C++ code uses correct namespaces based on its location.""" + # pylint: disable=too-many-branches + # if bool(True): + # return + + # Extensions we know we're skipping. + if any(fname.endswith(x) for x in ['.c', '.swift']): + return + + if not any(fname.endswith(x) for x in ['.cc', '.h', '.mm']): + raise CleanError(f"Unrecognized source file type: '{fname}'.") + + splits = fname.split('/') + assert len(splits) >= 3 # should be at least src, ballistica, foo + toplevelname = splits[2] + + # Make sure FOO in src/ballistica/FOO corresponds to a feature-set. + # (or one of our reserved names). + reserved_names = {'shared'} + feature_set = self.feature_sets.get(toplevelname) + + if toplevelname not in reserved_names and feature_set is None: + raise CleanError( + f"{toplevelname} in path '{fname}' does not correspond" + ' to a feature-set.' + ) + + # If the feature-set lists these files as to-be-ignored, ignore. + if ( + feature_set is not None + and fname in feature_set.cpp_namespace_check_disable_files + ): + return + + # Ignore ballistica.h/cc for now + if len(splits) == 3: + return + + # Anything under shared should only use ballistica namespace. + if splits[2] == 'shared': + for i, line in enumerate(lines): + if line.startswith('namespace '): + namespace, predecs_only = _get_namespace_info(lines, i) + if namespace != 'ballistica' and not predecs_only: + raise CleanError( + f'Invalid line "{line}" at {fname} line {i+1}.\n' + f"Files under 'shared' should use only ballistica" + f' namespace.' + ) + return + + # Anything else should use only the featureset namespace. + for i, line in enumerate(lines): + if line.startswith('namespace '): + namespace, predecs_only = _get_namespace_info(lines, i) + if namespace != f'ballistica::{toplevelname}' and not predecs_only: + raise CleanError( + f'Invalid line "{line}" at {fname} line {i+1}.\n' + f"This file is associated with the '{toplevelname}'" + ' FeatureSet so should be using the' + f" 'ballistica::{toplevelname}' namespace." + ) + + +def _get_namespace_info(lines: list[str], index: int) -> tuple[str, bool]: + """Given a line no, return name of namespace declared and whether it + is only predeclares.""" + assert lines[index].startswith('namespace ') + # Special case: single-line empty declaration. + splits = lines[index].split() + assert splits[0] == 'namespace' + if '{}' in lines[index]: + assert splits[2] == '{}' + # Not considering this a predeclare statement since it doesn't need to + # be there. + return splits[1], False + assert splits[2] == '{' + name = splits[1] + # Now scan lines until we find the close or a non-predeclare statement + index += 1 + while True: + if lines[index].startswith('}'): + return name, True + if not ( + lines[index].startswith('class ') and lines[index].endswith(';') + ): + # Found a non-predeclare statement + return name, False + index += 1 + + +def check_headers(self: ProjectUpdater) -> None: + """Check all project headers.""" + for header_file_raw in self.header_files: + assert header_file_raw[0] == '/' + header_file = f'src/ballistica{header_file_raw}' + if header_file.endswith('.h'): + _check_header(self, header_file) + + +def _check_header(self: ProjectUpdater, fname: str) -> None: + # Make sure its define guard is correct. + guard = fname[4:].upper().replace('/', '_').replace('.', '_') + '_' + with open(os.path.join(self.projroot, fname), encoding='utf-8') as fhdr: + lines = fhdr.read().splitlines() + + if self.license_line_checks: + _check_c_license(self, fname, lines) + + _source_file_feature_set_namespace_check(self, fname, lines) + + # Check for header guard lines at top + line = f'#ifndef {guard}' + lnum = 2 + if lines[lnum] != line: + # Allow auto-correcting if it looks close already + # (don't want to blow away an unrelated line) + allow_auto = lines[lnum].startswith('#ifndef BALLISTICA_') + self.add_line_correction( + fname, + line_number=lnum, + expected=line, + can_auto_update=allow_auto, + ) + line = f'#define {guard}' + lnum = 3 + if lines[lnum] != line: + # Allow auto-correcting if it looks close already + # (don't want to blow away an unrelated line) + allow_auto = lines[lnum].startswith('#define BALLISTICA_') + self.add_line_correction( + fname, + line_number=lnum, + expected=line, + can_auto_update=allow_auto, + ) + + # Check for header guard at bottom + line = f'#endif // {guard}' + lnum = len(lines) - 1 + if lines[lnum] != line: + # Allow auto-correcting if it looks close already + # (don't want to blow away an unrelated line) + allow_auto = lines[lnum].startswith('#endif // BALLISTICA_') + self.add_line_correction( + fname, + line_number=lnum, + expected=line, + can_auto_update=allow_auto, + ) + + +def _check_c_license( + self: ProjectUpdater, fname: str, lines: list[str] +) -> None: + # Look for public license line (public or private repo) or private + # license line (private repo only) + line_private = '// ' + _get_legal_notice_private() + line_public = get_public_license('c++') + lnum = 0 + + if self.public: + if lines[lnum] != line_public: + # Allow auto-correcting from private to public line + allow_auto = lines[lnum] == line_private + self.add_line_correction( + fname, + line_number=lnum, + expected=line_public, + can_auto_update=allow_auto, + ) + else: + if lines[lnum] not in [line_public, line_private]: + self.add_line_correction( + fname, + line_number=lnum, + expected=line_private, + can_auto_update=False, + ) + + +def check_makefiles(self: ProjectUpdater) -> None: + """Check all project makefiles.""" + + # Run a few sanity checks on whatever makefiles we come across. + fpaths = ( + subprocess.run( + ['find', self.projroot, '-maxdepth', '3', '-name', 'Makefile'], + capture_output=True, + check=True, + ) + .stdout.decode() + .split() + ) + + # Ignore stuff in the build dir such as cmake-generated ones. + fpaths = [ + n + for n in fpaths + if not n.startswith(os.path.join(self.projroot, 'build')) + ] + + for fpath in fpaths: + with open(fpath, encoding='utf-8') as infile: + makefile = infile.read() + + # Make sure public repo is public-license only. + if self.public: + public_license = get_public_license('makefile') + if public_license not in makefile: + raise CleanError(f'Pub license not found in {fpath}.') + # Allow both public and private license in private repo. + else: + if ( + _get_legal_notice_private() not in makefile + and get_public_license('makefile') not in makefile + ): + raise CleanError(f'Priv or pub legal not found in {fpath}.') + + +def check_python_files(self: ProjectUpdater) -> None: + """Check all project python files.""" + from efrotools.code import get_script_filenames + + scriptfiles = get_script_filenames(Path(self.projroot)) + for fname in scriptfiles: + _check_python_file(self, fname) + + packagedirs: list[str] = [] + + # Assume all dirs under these dirs are Python packages... + dirs_of_packages = [ + 'tools', + 'tests', + 'src/assets/ba_data/python', + 'src/meta', + ] + # EXCEPT for the following specifics. + ignores: dict[str, set[str]] = { + 'tools': { + 'make_bob', + 'mali_texture_compression_tool', + 'nvidia_texture_tools', + 'powervr_tools', + } + } + for dir_of_packages in dirs_of_packages: + for name in os.listdir(os.path.join(self.projroot, dir_of_packages)): + if name in ignores.get(dir_of_packages, {}): + continue + fullpath = os.path.join(self.projroot, dir_of_packages, name) + if not name.startswith('.') and os.path.isdir(fullpath): + packagedirs.append(fullpath) + + for packagedir in packagedirs: + # Special case: if this dir contains ONLY __pycache__ dirs and + # hidden files like .DS_Store, blow it away. It probably is left + # over bits from a since-removed-or-renamed package. + if _contains_only_pycache_and_cruft(packagedir): + print( + f"{Clr.MAG}NOTE: Directory '{packagedir}' contains only" + ' __pycache__ and hidden files/dirs; assuming it is' + ' left over from a deleted package and blowing it away.' + f'{Clr.RST}' + ) + subprocess.run(['rm', '-rf', packagedir], check=True) + + for root, dirs, files in os.walk(packagedir, topdown=True): + # Skip over hidden and pycache dirs. + for dirname in dirs: + if dirname.startswith('.'): + dirs.remove(dirname) + if '__pycache__' in dirs: + dirs.remove('__pycache__') + + # Check our packages and make sure all subdirs contain an + # __init__.py (I tend to forget this sometimes). + if '__init__.py' not in files: + raise CleanError( + f'No __init__.py under (presumed)' + f" Python package dir: '{root}'." + ) + + +def _check_python_file(self: ProjectUpdater, fname: str) -> None: + # pylint: disable=too-many-branches + + with open(fname, encoding='utf-8') as infile: + contents = infile.read() + lines = contents.splitlines() + + # Make sure all standalone scripts are pointing to the right version + # of Python (with a few exceptions where it needs to differ) + if contents.startswith('#!/'): + copyrightline = 1 + if fname not in ['tools/vmshell']: + if not contents.startswith(f'#!/usr/bin/env python{PYVER}'): + raise CleanError( + f'Incorrect shebang (first line) for ' f'{fname}.' + ) + else: + copyrightline = 0 + + # Special case: it there's spinoff autogenerate notice there, + # look below it. + if ( + lines[copyrightline] == '' + and 'THIS FILE IS AUTOGENERATED' in lines[copyrightline + 1] + ): + copyrightline += 2 + + if lines[copyrightline].startswith('# Synced from '): + copyrightline += 3 + + for i, line in enumerate(lines): + # FIXME: update this for the new feature-set world. Perhaps we + # can screen for imports based on feature-set dependencies or + # perhaps we should not even try and just let feature-set test + # builds catch problems like that. + if bool(False): + # Stuff under the babase module. + if '/babase/' in fname: + # Don't allow importing ba at the top level from within babase. + if line == 'import babase': + raise CleanError( + f'{fname}:{i+1}: no top level babase imports allowed' + f' under babase module.' + ) + if '/bastd/' in fname: + # Don't allow importing _babase or _baplus anywhere here. + # (any internal needs should be in babase.internal) + if 'import _babase' in line: + raise CleanError( + f'{fname}:{i+1}: _babase or _baplus imports not' + f' allowed under bastd.' + ) + + # In all cases, look for our one-line legal notice. + # In the public case, look for the rest of our public license too. + if self.license_line_checks: + public_license = get_public_license('python') + private_license = '# ' + _get_legal_notice_private() + lnum = copyrightline + if len(lines) < lnum + 1: + raise RuntimeError('Not enough lines in file:', fname) + + disable_note = ( + 'NOTE: You can disable license line' + ' checks by adding "license_line_checks": false\n' + 'to the root dict in config/localconfig.json.\n' + 'see https://ballistica.net/wiki' + '/Knowledge-Nuggets#' + 'hello-world-creating-a-new-game-type' + ) + + if self.public: + if lines[lnum] != public_license: + raise CleanError( + f'License text not found' + f" at '{fname}' line {lnum+1};" + f' please correct.\n' + f'Expected text is: {public_license}\n' + f'{disable_note}' + ) + else: + if lines[lnum] != public_license and lines[lnum] != private_license: + raise CleanError( + f'License text not found' + f" at '{fname}' line {lnum+1};" + f' please correct.\n' + f'Expected text (for public files):' + f' {public_license}\n' + f'Expected text (for private files):' + f' {private_license}\n' + f'{disable_note}' + ) + + +def _contains_only_pycache_and_cruft(path: str) -> bool: + for _root, dirs, files in os.walk(path, topdown=True): + # Skip over hidden and pycache dirs. + for dirname in dirs: + if dirname.startswith('.'): + dirs.remove(dirname) + if '__pycache__' in dirs: + dirs.remove('__pycache__') + + # Return as soon as we find a single non-hidden file. + for fname in files: + if not fname.startswith('.'): + return False + + # Welp.. found no normal files; just pycache and cruft. + return True + + +def check_sync_states(self: ProjectUpdater) -> None: + """Make sure project sync states are ok.""" + + # Assuming nobody else will be using sync system. + if self.public: + return + + # Make sure none of our sync targets have been mucked with since + # their last sync. + if ( + subprocess.run( + [os.path.join(self.projroot, 'tools/pcommand'), 'sync', 'check'], + check=False, + ).returncode + != 0 + ): + raise CleanError('Sync check failed; you may need to run "sync".') + + +def check_misc(self: ProjectUpdater) -> None: + """Check misc project stuff.""" + + # Make sure we're set to prod master server. (but ONLY when + # checking; still want to be able to run updates). + if not self.public and self.check and 'plus' in self.feature_sets: + with open( + os.path.join( + self.projroot, + 'src/ballistica/plus/support/master_server_config.h', + ), + encoding='utf-8', + ) as infile: + msconfig = infile.read() + if ( + '// V2 Master Server:\n' '\n' '// PROD\n' '#if 1\n' + ) not in msconfig: + raise CleanError('Not using prod v2 master server.') diff --git a/tools/batools/project/_updater.py b/tools/batools/project/_updater.py new file mode 100755 index 00000000..0d024e08 --- /dev/null +++ b/tools/batools/project/_updater.py @@ -0,0 +1,689 @@ +# Released under the MIT License. See LICENSE for details. +# +"""General project related functionality.""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import TYPE_CHECKING +from dataclasses import dataclass + +from efrotools import getconfig, getlocalconfig +from efro.error import CleanError +from efro.terminal import Clr + +if TYPE_CHECKING: + from batools.featureset import FeatureSet + + +def project_centric_path(projroot: str, path: str) -> str: + """Convert a CWD-relative path to a project-relative one.""" + abspath = os.path.abspath(path) + if abspath == projroot: + return '.' + projprefix = f'{projroot}/' + if not abspath.startswith(projprefix): + raise RuntimeError( + f'Path "{abspath}" is not under project root "{projprefix}"' + ) + return abspath[len(projprefix) :] + + +@dataclass +class _LineChange: + """A change applying to a particular line in a file.""" + + line_number: int + expected: str + can_auto_update: bool + + +class ProjectUpdater: + """Context for an project-updater run.""" + + def __init__( + self, + projroot: str, + check: bool, + fix: bool, + empty: bool = False, + projname: str = 'BallisticaKit', + ) -> None: + self.projname = projname + self.projroot = os.path.abspath(projroot) + self.check = check + self.fix = fix + + # 'fix' implies making changes and 'check' implies no changes. + if fix and check: + raise RuntimeError('fix and check cannot both be enabled') + + # We behave a bit differently in the public repo. + self.public: bool = getconfig(Path(projroot)).get('public', False) + assert isinstance(self.public, bool) + + self._source_files: list[str] | None = None + self._header_files: list[str] | None = None + + # Set of files this updater instance will update. + # Add stuff here as desired before calling run(). + # The associated value can be input data for the file. + # If None, the existing file will be read from disk. + self._enqueued_updates: dict[str, str | None] = {} + + # Individual line corrections made in _fix mode. + self._line_corrections: dict[str, list[_LineChange]] = {} + + self._can_generate_files = False + + # All files generated by going through updates. Note that + # this can include files not explicitly requested in + # updates (manifest files or other side-effect files). + self._generated_files: dict[str, str] = {} + + # Cached feature-set list for any functionality/tools that might + # need it. + self._feature_sets: dict[str, FeatureSet] | None = None + + self.license_line_checks = bool( + getlocalconfig(Path(projroot)).get('license_line_checks', True) + ) + + self._internal_source_dirs: set[str] | None = None + self._internal_source_files: set[str] | None = None + + # Whether to run various checks across project files. This can + # be turned off to speed things up when updating a focused set + # of files. + self.run_file_checks = True + + # For 'empty' mode we disable all default stuff and only do + # exactly what is requested of us. + if empty: + self.run_file_checks = False + else: + # Schedule updates for all the things in normal mode. + self._update_meta_makefile() + self._update_resources_makefile() + self._update_assets_makefile() + self._update_top_level_makefile() + self._update_cmake_files() + self._update_visual_studio_projects() + self._update_xcode_projects() + + @property + def source_files(self) -> list[str]: + """Return project source files.""" + assert self._source_files is not None + return self._source_files + + @property + def header_files(self) -> list[str]: + """Return project header files.""" + assert self._header_files is not None + return self._header_files + + @property + def feature_sets(self) -> dict[str, FeatureSet]: + """Cached list of project feature-sets.""" + if self._feature_sets is None: + from batools.featureset import FeatureSet + + self._feature_sets = { + f.name: f for f in FeatureSet.get_all_for_project(self.projroot) + } + return self._feature_sets + + def enqueue_update(self, path: str, data: str | None = None) -> None: + """Add an update to the queue.""" + self._enqueued_updates[path] = data + + def run(self) -> None: + """Do the thing.""" + self.prepare_to_generate() + start_updates = self._enqueued_updates.copy() + + # Generate all files we've been asked to. + for path in self._enqueued_updates: + self.generate_file(path) + + # Run some lovely checks. + if self.run_file_checks: + from batools.project import _checks + + _checks.check_makefiles(self) + _checks.check_python_files(self) + _checks.check_sync_states(self) + _checks.check_misc(self) + _checks.check_source_files(self) + _checks.check_headers(self) + + # Make sure nobody is changing this while processing. + self._can_generate_files = False + assert start_updates == self._enqueued_updates + + # If we're all good to here, do the actual writes we set up above. + self._apply_line_changes() + self._apply_file_changes() + + def prepare_to_generate(self) -> None: + """Prepare""" + # Make sure we're operating from a project root. + if not os.path.isdir( + os.path.join(self.projroot, 'config') + ) or not os.path.isdir(os.path.join(self.projroot, 'tools')): + raise RuntimeError( + f"ProjectUpdater projroot '{self.projroot}' is not valid." + ) + + self._find_sources_and_headers( + os.path.join(self.projroot, 'src/ballistica') + ) + + self._can_generate_files = True + + def _get_internal_source_files(self) -> set[str]: + # Fetch/calc just once and cache results. + if self._internal_source_files is None: + sources: list[str] + if self.public: + sources = [] + else: + sources = getconfig(Path(self.projroot)).get( + 'internal_source_files', [] + ) + if not isinstance(sources, list): + raise CleanError( + f'Expected list for internal_source_files;' + f' got {type(sources)}' + ) + self._internal_source_files = set(sources) + return self._internal_source_files + + def _get_internal_source_dirs(self) -> set[str]: + # Fetch/calc just once and cache results. + if self._internal_source_dirs is None: + sources: list[str] + if self.public: + sources = [] + else: + sources = getconfig(Path(self.projroot)).get( + 'internal_source_dirs', [] + ) + if not isinstance(sources, list): + raise CleanError( + f'Expected list for internal_source_dirs;' + f' got {type(sources)}' + ) + self._internal_source_dirs = set(sources) + return self._internal_source_dirs + + def _apply_file_changes(self) -> None: + # Now write out any project files that have changed + # (or error if we're in check mode). + unchanged_file_count = 0 + for fname, fcode in self._generated_files.items(): + f_path_abs = os.path.join(self.projroot, fname) + # Allow for line ending changes by git?... + fcodefin = fcode.replace('\r\n', '\n') + f_orig: str | None + if os.path.exists(f_path_abs): + with open(f_path_abs, 'r', encoding='utf-8') as infile: + f_orig = infile.read() + else: + f_orig = None + if f_orig == fcodefin: + unchanged_file_count += 1 + else: + if self.check: + # Dump the generated and print a command to diff it + # against the original. This can be useful to + # diagnose non-deterministic generation issues. + errfile = os.path.join( + self.projroot, 'build/project_check_error_file' + ) + os.makedirs(os.path.dirname(errfile), exist_ok=True) + with open(errfile, 'w', encoding='utf-8') as outfile: + outfile.write(fcodefin) + path1 = f_path_abs + path2 = errfile + raise CleanError( + f"Found out-of-date project file: '{fname}'.\n" + 'To see what would change, run:\n' + f" diff '{path1}' '{path2}'\n" + ) + + print(f'{Clr.BLU}Writing project file: {fname}{Clr.RST}') + with open(f_path_abs, 'w', encoding='utf-8') as outfile: + outfile.write(fcode) + if unchanged_file_count > 0: + print(f'{unchanged_file_count} project files are up to date.') + + def _apply_line_changes(self) -> None: + # Build a flat list of entries that can and can-not be auto + # applied. + manual_changes: list[tuple[str, _LineChange]] = [] + auto_changes: list[tuple[str, _LineChange]] = [] + for fname, entries in self._line_corrections.items(): + for entry in entries: + if entry.can_auto_update: + auto_changes.append((fname, entry)) + else: + manual_changes.append((fname, entry)) + + # If there are any manual-only entries, list then and bail. + # (Don't wanna allow auto-apply unless it fixes everything) + if manual_changes: + print( + f'{Clr.RED}Found erroneous lines ' + f'requiring manual correction:{Clr.RST}' + ) + for change in manual_changes: + print( + f'{Clr.RED}{change[0]}:{change[1].line_number + 1}:' + f' Expected line to be:\n {change[1].expected}{Clr.RST}' + ) + + raise CleanError() + + # Now, if we've got auto entries, either list or auto-correct them. + if auto_changes: + if not self.fix: + for i, change in enumerate(auto_changes): + print( + f'{Clr.RED}#{i}:' + f' {change[0]}:{change[1].line_number+1}:' + f'{Clr.RST}' + ) + print( + f'{Clr.RED} Expected "{change[1].expected}"{Clr.RST}' + ) + with open( + os.path.join(self.projroot, change[0]), encoding='utf-8' + ) as infile: + lines = infile.read().splitlines() + line = lines[change[1].line_number] + print(f'{Clr.RED} Found "{line}"{Clr.RST}') + raise CleanError( + f'All {len(auto_changes)} errors are' + f' auto-fixable; run tools/pcommand update_project' + f' --fix to apply corrections.' + ) + + for i, change in enumerate(auto_changes): + print(f'{Clr.BLU}Correcting file: {change[0]}{Clr.RST}') + with open( + os.path.join(self.projroot, change[0]), encoding='utf-8' + ) as infile: + lines = infile.read().splitlines() + lines[change[1].line_number] = change[1].expected + with open( + os.path.join(self.projroot, change[0]), + 'w', + encoding='utf-8', + ) as outfile: + outfile.write('\n'.join(lines) + '\n') + + # If there were no issues whatsoever, note that. + if not manual_changes and not auto_changes: + fcount = len(self.header_files) + len(self.source_files) + print(f'No issues found in {fcount} source files.') + + def add_line_correction( + self, + filename: str, + line_number: int, + expected: str, + can_auto_update: bool, + ) -> None: + """Add a correction that the updater can optionally perform.""" + # No longer allowing negatives here since they don't show up + # nicely in correction list. + assert line_number >= 0 + self._line_corrections.setdefault(filename, []).append( + _LineChange( + line_number=line_number, + expected=expected, + can_auto_update=can_auto_update, + ) + ) + + def generate_file(self, path: str) -> str: + """Generate/return the contents for the file at the given path.""" + # pylint: disable=too-many-branches + + if not self._can_generate_files: + raise RuntimeError('Generate cannot be called right now.') + + if path not in self._generated_files: + # First we need input data. If the user provided it explicitly, + # go with theirs. Otherwise load the existing file from disk. + existing_data = self._enqueued_updates.get(path) + if existing_data is None: + with open( + os.path.join(self.projroot, path), encoding='utf-8' + ) as infile: + existing_data = infile.read() + # Dispatch to generator methods depending on extension/etc. + if path.endswith('/project.pbxproj'): + self._generate_xcode_project(path, existing_data) + elif path.endswith('.vcxproj.filters'): + self._generate_visual_studio_project_filters( + path, existing_data + ) + elif path.endswith('.vcxproj'): + self._generate_visual_studio_project(path, existing_data) + elif path.endswith('CMakeLists.txt'): + self._generate_cmake_file(path, existing_data) + elif path == 'Makefile': + self._generate_top_level_makefile(path, existing_data) + elif path == 'src/assets/Makefile': + self._generate_assets_makefile(path, existing_data) + elif path.startswith('src/assets/.asset_manifest_'): + # These are generated as a side-effect of the assets makefile. + self.generate_file('src/assets/Makefile') + elif path == 'src/resources/Makefile': + self._generate_resources_makefile(path, existing_data) + elif path == 'src/meta/Makefile': + self._generate_meta_makefile(existing_data) + elif path.startswith('src/meta/.meta_manifest_'): + # These are generated as a side-effect of the meta makefile. + self.generate_file('src/meta/Makefile') + assert path in self._generated_files + else: + raise RuntimeError( + f"No known formula to create project file: '{path}'." + ) + + assert path in self._generated_files + return self._generated_files[path] + + def _update_xcode_projects(self) -> None: + # from batools.xcode import update_xcode_project + + for projpath in [ + 'ballisticakit-mac.xcodeproj/project.pbxproj', + 'ballisticakit-ios.xcodeproj/project.pbxproj', + 'ballisticakit-xcode/BallisticaKit.xcodeproj/project.pbxproj', + ]: + # These currently aren't bundled in public. + if self.public: + assert not os.path.exists(projpath) + continue + + self.enqueue_update(projpath) + + def _generate_xcode_project(self, path: str, existing_data: str) -> None: + from batools.xcodeproject import update_xcode_project + + all_files = sorted( + [f'ballistica{p}' for p in (self.source_files + self.header_files)] + ) + + # We have .pbxproj; this wants .xcodeproj above it. + # Should probably change that as its confusing... + assert path.endswith('.pbxproj') + self._generated_files[path] = update_xcode_project( + self.projroot, + os.path.dirname(path), + existing_data, + all_files, + projname=self.projname, + ) + + def _update_visual_studio_project(self, basename: str) -> None: + fname = ( + f'ballisticakit-windows/{basename}/' + f'BallisticaKit{basename}.vcxproj' + ) + self.enqueue_update(f'{fname}.filters') + self.enqueue_update(fname) + + def _generate_visual_studio_project( + self, fname: str, existing_data: str + ) -> None: + lines = existing_data.splitlines() + + src_root = '..\\..\\src' + + public_project = 'Internal' not in os.path.basename(fname) + + all_files = sorted( + [ + f + for f in (self.source_files + self.header_files) + if not f.endswith('.m') + and not f.endswith('.mm') + and not f.endswith('.c') + and not f.endswith('.swift') + and self._is_public_source_file(f) == public_project + ] + ) + + # Find the ItemGroup containing stdafx.cpp. This is where we'll + # dump our stuff. + index = lines.index(' ') + begin_index = end_index = index + while lines[begin_index] != ' ': + begin_index -= 1 + while lines[end_index] != ' ': + end_index += 1 + group_lines = lines[begin_index + 1 : end_index] + + # Strip out any existing files from src/ballistica. + group_lines = [ + l for l in group_lines if src_root + '\\ballistica\\' not in l + ] + + # Now add in our own. + # Note: we can't use C files in this build at the moment; breaks + # precompiled header stuff. (shouldn't be a problem though). + group_lines = [ + ' <' + + ('ClInclude' if src.endswith('.h') else 'ClCompile') + + ' Include="' + + src_root + + '\\ballistica' + + src.replace('/', '\\') + + '" />' + for src in all_files + ] + group_lines + + filtered = lines[: begin_index + 1] + group_lines + lines[end_index:] + out = '\r\n'.join(filtered) + '\r\n' + self._generated_files[fname] = out + + def _generate_visual_studio_project_filters( + self, fname: str, existing_data: str + ) -> None: + del existing_data # Unused. + assert fname.endswith('.filters') + # We rely on the generated project file itself. + project = self.generate_file(fname.removesuffix('.filters')) + lines_in = project.splitlines() + src_root = '..\\..\\src' + filterpaths: set[str] = set() + filterlines: list[str] = [ + '', + '', + ' ', + ] + sourcelines = [l for l in lines_in if 'Include="' + src_root in l] + for line in sourcelines: + entrytype = line.strip().split()[0][1:] + path = line.split('"')[1] + filterlines.append(' <' + entrytype + ' Include="' + path + '">') + + # If we have a dir foo/bar/eep we need to create filters for + # each of foo, foo/bar, and foo/bar/eep + splits = path[len(src_root) :].split('\\') + splits = [s for s in splits if s != ''] + splits = splits[:-1] + for i in range(len(splits)): + filterpaths.add('\\'.join(splits[: (i + 1)])) + filterlines.append( + ' ' + '\\'.join(splits) + '' + ) + filterlines.append(' ') + filterlines += [ + ' ', + ' ', + ] + for filterpath in sorted(filterpaths): + filterlines.append(' ') + filterlines += [ + ' ', + '', + ] + self._generated_files[fname] = '\r\n'.join(filterlines) + '\r\n' + + def _update_visual_studio_projects(self) -> None: + self._update_visual_studio_project('Generic') + self._update_visual_studio_project('Headless') + if not self.public: + self._update_visual_studio_project('GenericInternal') + self._update_visual_studio_project('HeadlessInternal') + self._update_visual_studio_project('Oculus') + self._update_visual_studio_project('OculusInternal') + + def _is_public_source_file(self, filename: str) -> bool: + assert filename.startswith('/') + filename = f'src/ballistica{filename}' + + # If its under any of our internal source dirs, make it internal. + for srcdir in self._get_internal_source_dirs(): + assert not srcdir.startswith('/') + assert not srcdir.endswith('/') + if filename.startswith(f'{srcdir}/'): + return False + + # If its specifically listed as an internal file, make it internal. + return filename not in self._get_internal_source_files() + + def _generate_cmake_file(self, fname: str, existing_data: str) -> None: + lines = existing_data.splitlines() + + for section in ['PUBLIC', 'PRIVATE']: + # Public repo has no private section. + if self.public and section == 'PRIVATE': + continue + + auto_start = lines.index( + f' # AUTOGENERATED_{section}_BEGIN (this section' + f' is managed by the "update_project" tool)' + ) + auto_end = lines.index(f' # AUTOGENERATED_{section}_END') + our_lines = [ + ' ${BA_SRC_ROOT}/ballistica' + f + for f in sorted(self.source_files + self.header_files) + if not f.endswith('.mm') + and not f.endswith('.m') + and not f.endswith('.swift') + and self._is_public_source_file(f) == (section == 'PUBLIC') + ] + lines = lines[: auto_start + 1] + our_lines + lines[auto_end:] + + self._generated_files[fname] = '\n'.join(lines) + '\n' + + def _update_cmake_files(self) -> None: + # Note: currently not updating cmake files at all in public + # builds; will need to get this working at some point. + + # Top level cmake builds: + fname = 'ballisticakit-cmake/CMakeLists.txt' + if not self.public: + self.enqueue_update(fname) + + # CMake android components: + fname = ( + 'ballisticakit-android/BallisticaKit/src/main/cpp/CMakeLists.txt' + ) + if not self.public: + self.enqueue_update(fname) + + def _find_sources_and_headers(self, scan_dir: str) -> None: + src_files = set() + header_files = set() + exts = ['.c', '.cc', '.cpp', '.cxx', '.m', '.mm', '.swift'] + header_exts = ['.h'] + + # Gather all sources and headers. + # HMMM: Ideally we should use + # efrotools.code.get_code_filenames() here (though we return + # things relative to the scan-dir which could throw things off). + for root, _dirs, files in os.walk(scan_dir): + for ftst in files: + if any(ftst.endswith(ext) for ext in exts): + src_files.add(os.path.join(root, ftst)[len(scan_dir) :]) + if any(ftst.endswith(ext) for ext in header_exts): + header_files.add(os.path.join(root, ftst)[len(scan_dir) :]) + + # IMPORTANT - exclude generated files. + # For now these just consist of headers so its ok to completely + # ignore their existence here, but at some point if we start + # generating .cc files that need to be compiled we'll have to + # ask the meta system which files it *will* be generating and + # add THAT list (not what we see on disk) to projects. + self._source_files = sorted(s for s in src_files if '/mgen/' not in s) + self._header_files = sorted( + h for h in header_files if '/mgen/' not in h + ) + + def _update_assets_makefile(self) -> None: + self.enqueue_update('src/assets/Makefile') + + def _generate_assets_makefile(self, path: str, existing_data: str) -> None: + from batools.assetsmakefile import generate_assets_makefile + + # We need to now what files meta will be creating (since they + # can be asset sources). + meta_manifests: dict[str, str] = {} + for mantype in ['public', 'private']: + manifest_file_name = f'src/meta/.meta_manifest_{mantype}.json' + meta_manifests[manifest_file_name] = self.generate_file( + manifest_file_name + ) + + outfiles = generate_assets_makefile( + self.projroot, path, existing_data, meta_manifests + ) + for out_path, out_contents in outfiles.items(): + self._generated_files[out_path] = out_contents + + def _update_top_level_makefile(self) -> None: + self.enqueue_update('Makefile') + + def _generate_top_level_makefile( + self, path: str, existing_data: str + ) -> None: + from batools.toplevelmakefile import generate_top_level_makefile + + self._generated_files[path] = generate_top_level_makefile( + self.projroot, existing_data + ) + + def _update_meta_makefile(self) -> None: + self.enqueue_update('src/meta/Makefile') + + def _generate_meta_makefile(self, existing_data: str) -> None: + from batools.metamakefile import generate_meta_makefile + + outfiles = generate_meta_makefile(self.projroot, existing_data) + for out_path, out_contents in outfiles.items(): + self._generated_files[out_path] = out_contents + + def _update_resources_makefile(self) -> None: + self.enqueue_update('src/resources/Makefile') + + def _generate_resources_makefile( + self, path: str, existing_data: str + ) -> None: + from batools.resourcesmakefile import ResourcesMakefileGenerator + + self._generated_files[path] = ResourcesMakefileGenerator( + self.projroot, + existing_data, + projname=self.projname, + ).run() diff --git a/tools/batools/pruneincludes.py b/tools/batools/pruneincludes.py new file mode 100755 index 00000000..9b24405a --- /dev/null +++ b/tools/batools/pruneincludes.py @@ -0,0 +1,209 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Utility to scan for unnecessary includes in c++ files.""" + +from __future__ import annotations + +import os +import json +import tempfile +from typing import TYPE_CHECKING +from dataclasses import dataclass +import subprocess + +from efro.error import CleanError +from efro.terminal import Clr +from efro.dataclassio import dataclass_from_dict, ioprepped + +if TYPE_CHECKING: + pass + + +@ioprepped +@dataclass +class _CompileCommandsEntry: + directory: str + command: str + file: str + + +class Pruner: + """Wrangles a prune operation.""" + + def __init__(self, commit: bool, paths: list[str]) -> None: + self.commit = commit + self.paths = paths + + # Files we're ok checking despite them containing #ifs. + self.ifdef_check_whitelist = { + 'src/ballistica/shared/python/python.cc', + 'src/ballistica/base/assets/assets.cc', + 'src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc', + 'src/ballistica/scene_v1/support/scene.cc', + 'src/ballistica/scene_v1/support/scene_v1_app_mode.cc', + } + + # Exact lines we never flag as removable. + self.line_whitelist = { + '#include "ballistica/mgen/pyembed/binding_ba.inc"' + } + + def run(self) -> None: + """Do the thing.""" + + cwd = os.getcwd() + + if self.commit: + print(f'{Clr.MAG}{Clr.BLD}RUNNING IN COMMIT MODE!!!{Clr.RST}') + + self._prep_paths() + + entries = self._get_entries() + + with tempfile.TemporaryDirectory() as tempdir: + for entry in entries: + if not entry.file.startswith(cwd): + raise CleanError( + f'compile-commands file {entry.file}' + f' does not start with cwd "{cwd}".' + ) + relpath = entry.file.removeprefix(cwd + '/') + + # Only process our stuff under the ballistica dir. + if not relpath.startswith('src/ballistica/'): + continue + + # If we were given a list of paths, constrain to those. + if self.paths: + if os.path.abspath(entry.file) not in self.paths: + continue + + # See what file the command will write so we can prep its dir. + splits = entry.command.split(' ') + outpath = splits[splits.index('-o') + 1] + outdir = os.path.dirname(outpath) + cmd = ( + f'cd "{tempdir}" && mkdir -p "{outdir}" && {entry.command}' + ) + self._check_file(relpath, cmd) + + def _prep_paths(self) -> None: + # First off, make sure all our whitelist files still exist. + # This will be a nice reminder to keep the list updated with + # any changes. + for wpath in self.ifdef_check_whitelist: + if not os.path.isfile(wpath): + raise CleanError( + f"ifdef-check-whitelist entry does not exist: '{wpath}'." + ) + + # If we were given paths, make sure they exist and convert to absolute. + if self.paths: + for path in self.paths: + if not os.path.exists(path): + raise CleanError(f'path not found: "{path}"') + self.paths = [os.path.abspath(p) for p in self.paths] + + def _get_entries(self) -> list[_CompileCommandsEntry]: + cmdspath = '.cache/irony/compile_commands.json' + if not os.path.isfile(cmdspath): + raise CleanError( + f'Compile-commands not found at "{cmdspath}".' + f' do you have the irony build db enabled? (see Makefile)' + ) + with open(cmdspath, encoding='utf-8') as infile: + cmdsraw = json.loads(infile.read()) + if not isinstance(cmdsraw, list): + raise CleanError( + f'Expected list for compile-commands;' + f' found {type(cmdsraw)}.' + ) + return [dataclass_from_dict(_CompileCommandsEntry, e) for e in cmdsraw] + + def _check_file(self, path: str, cmd: str) -> None: + """Run all checks on an individual file.""" + # pylint: disable=too-many-locals + + with open(path, encoding='utf-8') as infile: + orig_contents = infile.read() + orig_lines = orig_contents.splitlines(keepends=True) + + # If there's any conditional compilation in there, skip. Code that + # isn't getting compiled by default could be using something from + # an include. + for i, line in enumerate(orig_lines): + if ( + line.startswith('#if') + and path not in self.ifdef_check_whitelist + ): + print( + f'Skipping {Clr.YLW}{path}{Clr.RST} due to line' + f' {i+1}: {line[:-1]}' + ) + return + includelines: list[int] = [] + for i, line in enumerate(orig_lines): + if line.startswith('#include "') and line.strip().endswith('.h"'): + includelines.append(i) + + # Remove any includes of our associated header file. + # (we want to leave those in even if its technically not necessary). + bpath = path.removeprefix('src/') + our_header = '#include "' + os.path.splitext(bpath)[0] + '.h"\n' + includelines = [h for h in includelines if orig_lines[h] != our_header] + + print(f'Processing {Clr.BLD}{Clr.BLU}{path}{Clr.RST}...') + working_lines = orig_lines + completed = False + + # First run the compile unmodified just to be sure it works. + success = ( + subprocess.run( + cmd, shell=True, check=False, capture_output=True + ).returncode + == 0 + ) + if not success: + print( + f' {Clr.RED}{Clr.BLD}Initial test compile failed;' + f' something is probably wrong.{Clr.RST}' + ) + + try: + # Go through backwards because then removing a line doesn't + # invalidate our next lines to check. + for i, lineno in enumerate(reversed(includelines)): + test_lines = working_lines.copy() + print(f' Checking include {i+1} of {len(includelines)}...') + removed_line = test_lines.pop(lineno).removesuffix('\n') + + with open(path, 'w', encoding='utf-8') as outfile: + outfile.write(''.join(test_lines)) + success = ( + subprocess.run( + cmd, shell=True, check=False, capture_output=True + ).returncode + == 0 + and removed_line not in self.line_whitelist + ) + if success: + working_lines = test_lines + print( + f' {Clr.GRN}{Clr.BLD}Line {lineno+1}' + f' seems to be removable:{Clr.RST} {removed_line}' + ) + completed = True + + finally: + if not completed: + print(f' {Clr.RED}{Clr.BLD}Error processing file.{Clr.RST}') + + # Restore original if we're not committing or something went wrong. + if not self.commit or not completed: + with open(path, 'w', encoding='utf-8') as outfile: + outfile.write(orig_contents) + + # Otherwise restore the latest working version if committing. + elif self.commit: + with open(path, 'w', encoding='utf-8') as outfile: + outfile.write(''.join(working_lines)) diff --git a/tools/batools/pythonenumsmodule.py b/tools/batools/pythonenumsmodule.py index bdc7c176..c2145eb0 100755 --- a/tools/batools/pythonenumsmodule.py +++ b/tools/batools/pythonenumsmodule.py @@ -37,7 +37,6 @@ def _gen_enums(infilename: str) -> str: # Now export each of them. for lnum in enum_lnums: - doclines, lnum = _parse_doc_lines(lines, lnum) enum_name = _parse_name(lines, lnum) @@ -130,7 +129,6 @@ def _find_enum_end(lines: list[str], lnum: int) -> int: def _parse_doc_lines(lines: list[str], lnum: int) -> tuple[list[str], int]: - # First parse the doc-string doclines: list[str] = [] lnumorig = lnum diff --git a/tools/batools/resourcesmakefile.py b/tools/batools/resourcesmakefile.py index 7df6ae8d..131d238a 100755 --- a/tools/batools/resourcesmakefile.py +++ b/tools/batools/resourcesmakefile.py @@ -9,14 +9,16 @@ from __future__ import annotations import os from pathlib import Path -from typing import TYPE_CHECKING from dataclasses import dataclass from efro.error import CleanError -from efro.terminal import Clr -if TYPE_CHECKING: - pass + +# These paths need to be relative to the dir we're writing the Makefile to. +ROOT_DIR = '$(PROJ_DIR)' +TOOLS_DIR = '$(TOOLS_DIR)' +BUILD_DIR = '$(BUILD_DIR)' +RESIZE_CMD = os.path.join(TOOLS_DIR, 'pcommand resize_image') @dataclass @@ -46,399 +48,411 @@ class Target: return out -def _emit_group_build_lines(targets: list[Target], basename: str) -> list[str]: - """Gen a group build target.""" - del basename # Unused. - out: list[str] = [] - if not targets: - return out - all_dsts = set() - for target in targets: - all_dsts.add(target.dst) - out.append( - 'resources: \\\n ' - + ' \\\n '.join(dst.replace(' ', '\\ ') for dst in sorted(all_dsts)) - + '\n' - ) - return out +class ResourcesMakefileGenerator: + """Does the thing.""" + def __init__( + self, + projroot: str, + existing_data: str, + projname: str, + ) -> None: + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements -def _emit_group_clean_lines(targets: list[Target], basename: str) -> list[str]: - """Gen a group clean target.""" - out: list[str] = [] - if not targets: - return out - out.append(f'clean: clean-{basename}\n') - all_dsts = set() - for target in targets: - all_dsts.add(target.dst) - out.append( - f'clean-{basename}:\n\trm -f ' - + ' \\\n '.join('"' + dst + '"' for dst in sorted(all_dsts)) - + '\n' - ) - return out + from efrotools import getconfig + self.public = getconfig(Path(projroot))['public'] + assert isinstance(self.public, bool) + self.existing_data = existing_data + self.projroot = projroot + self.targets: list[Target] = [] -def _emit_group_efrocache_lines(targets: list[Target]) -> list[str]: - """Gen a group clean target.""" - out: list[str] = [] - if not targets: - return out - all_dsts = set() - for target in targets: + # Regular and lowercase project name. + self.nameu = projname + self.namel = projname.lower() - # We may need to make pipeline adjustments if/when we get filenames - # with spaces in them. - if ' ' in target.dst: - raise CleanError( - 'FIXME: need to account for spaces in filename' - f' "{target.dst}".' + def run(self) -> str: + """Does the thing.""" + # fname = 'src/resources/Makefile' + # with open(fname, encoding='utf-8') as infile: + # original = infile.read() + original = self.existing_data + lines = original.splitlines() + + auto_start_public = lines.index('# __AUTOGENERATED_PUBLIC_BEGIN__') + auto_end_public = lines.index('# __AUTOGENERATED_PUBLIC_END__') + auto_start_private = lines.index('# __AUTOGENERATED_PRIVATE_BEGIN__') + auto_end_private = lines.index('# __AUTOGENERATED_PRIVATE_END__') + + # Public targets (full sources available in public) + + basename = 'public' + our_lines_public = ( + _empty_line_if(bool(self.targets)) + + self._emit_group_build_lines(basename) + + self._emit_group_clean_lines(basename) + + [t.emit() for t in self.targets] + ) + + # Only rewrite the private section in the private repo; otherwise + # keep the existing one intact. + if self.public: + our_lines_private = lines[auto_start_private + 1 : auto_end_private] + else: + # Private targets (available in public through efrocache) + self.targets = [] + basename = 'private' + self._add_windows_icon(generic=True, oculus=False, inputs=False) + our_lines_private_1 = ( + _empty_line_if(bool(self.targets)) + + self._emit_group_build_lines(basename) + + self._emit_group_clean_lines(basename) + + ['# __EFROCACHE_TARGET__\n' + t.emit() for t in self.targets] + + self._emit_group_efrocache_lines() ) - all_dsts.add(target.dst) - out.append( - 'efrocache-list:\n\t@echo ' - + ' \\\n '.join('"' + dst + '"' for dst in sorted(all_dsts)) - + '\n' - ) - out.append('efrocache-build: resources\n') - return out - - -# These paths need to be relative to the dir we're writing the Makefile to. -TOOLS_DIR = '../tools' -ROOT_DIR = '..' -RES_DIR = '.' -RESIZE_CMD = os.path.join(TOOLS_DIR, 'pcommand resize_image') - - -def _add_windows_icon( - targets: list[Target], generic: bool, oculus: bool, inputs: bool -) -> None: - - sizes = [256, 128, 96, 64, 48, 32, 16] - all_icons = [] - for size in sizes: - dst_base = 'build' - src = os.path.join('src', 'icon', 'icon_clipped.png') - dst = os.path.join(dst_base, 'win_icon_' + str(size) + '_tmp.png') - cmd = ' '.join( - [RESIZE_CMD, str(size), str(size), '"' + src + '"', '"' + dst + '"'] - ) - all_icons.append(dst) - if inputs: - targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) - - # Assemble all the bits we just made into .ico files. - for path, enable in [ - ( - ROOT_DIR + '/ballisticacore-windows/Generic/BallisticaCore.ico', - generic, - ), - ( - ROOT_DIR + '/ballisticacore-windows/Oculus/BallisticaCore.ico', - oculus, - ), - ]: - cmd = ( - 'convert ' - + ''.join([' "' + f + '"' for f in all_icons]) - + ' "' - + path - + '"' - ) - if enable: - targets.append(Target(src=all_icons, dst=path, cmd=cmd)) - - -def _add_ios_app_icon(targets: list[Target]) -> None: - sizes = [ - (20, 2), - (20, 3), - (29, 2), - (29, 3), - (40, 2), - (40, 3), - (60, 2), - (60, 3), - (20, 1), - (29, 1), - (40, 1), - (76, 1), - (76, 2), - (83.5, 2), - (1024, 1), - ] - for size in sizes: - res = int(size[0] * size[1]) - src = os.path.join('src', 'icon', 'icon_flat.png') - dst = os.path.join( - ROOT_DIR, - 'ballisticacore-xcode', - 'BallisticaCore Shared', - 'Assets.xcassets', - 'AppIcon iOS.appiconset', - 'icon_' + str(size[0]) + 'x' + str(size[1]) + '.png', - ) - cmd = ' '.join( - [RESIZE_CMD, str(res), str(res), '"' + src + '"', '"' + dst + '"'] - ) - targets.append(Target(src=[src], dst=dst, cmd=cmd)) - - -def _add_macos_app_icon(targets: list[Target]) -> None: - sizes = [ - (16, 1), - (16, 2), - (32, 1), - (32, 2), - (128, 1), - (128, 2), - (256, 1), - (256, 2), - (512, 1), - (512, 2), - ] - for size in sizes: - res = int(size[0] * size[1]) - src = os.path.join(RES_DIR, 'src', 'icon', 'icon_clipped.png') - dst = os.path.join( - ROOT_DIR, - 'ballisticacore-xcode', - 'BallisticaCore Shared', - 'Assets.xcassets', - 'AppIcon macOS.appiconset', - 'icon_' + str(size[0]) + 'x' + str(size[1]) + '.png', - ) - cmd = ' '.join( - [RESIZE_CMD, str(res), str(res), '"' + src + '"', '"' + dst + '"'] - ) - targets.append(Target(src=[src], dst=dst, cmd=cmd)) - - -def _add_android_app_icon( - targets: list[Target], - src_name: str = 'icon_clipped.png', - variant_name: str = 'main', -) -> None: - sizes = [ - ('mdpi', 48), - ('hdpi', 72), - ('xhdpi', 96), - ('xxhdpi', 144), - ('xxxhdpi', 192), - ] - for size in sizes: - res = size[1] - src = os.path.join(RES_DIR, 'src', 'icon', src_name) - dst = os.path.join( - ROOT_DIR, - 'ballisticacore-android', - 'BallisticaCore', - 'src', - variant_name, - 'res', - 'mipmap-' + size[0], - 'ic_launcher.png', - ) - cmd = ' '.join( - [RESIZE_CMD, str(res), str(res), '"' + src + '"', '"' + dst + '"'] - ) - targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) - - -def _add_android_app_icon_new( - targets: list[Target], - src_fg_name: str = 'icon_android_layered_fg.png', - src_bg_name: str = 'icon_android_layered_bg.png', - variant_name: str = 'main', -) -> None: - sizes = [ - ('mdpi', 108), - ('hdpi', 162), - ('xhdpi', 216), - ('xxhdpi', 324), - ('xxxhdpi', 432), - ] - for size in sizes: - res = size[1] - - # Foreground component. - src = os.path.join(RES_DIR, 'src', 'icon', src_fg_name) - dst = os.path.join( - ROOT_DIR, - 'ballisticacore-android', - 'BallisticaCore', - 'src', - variant_name, - 'res', - 'mipmap-' + size[0], - 'ic_launcher_foreground.png', - ) - cmd = ' '.join( - [RESIZE_CMD, str(res), str(res), '"' + src + '"', '"' + dst + '"'] - ) - targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) - - # Background component. - src = os.path.join(RES_DIR, 'src', 'icon', src_bg_name) - dst = os.path.join( - ROOT_DIR, - 'ballisticacore-android', - 'BallisticaCore', - 'src', - variant_name, - 'res', - 'mipmap-' + size[0], - 'ic_launcher_background.png', - ) - cmd = ' '.join( - [RESIZE_CMD, str(res), str(res), '"' + src + '"', '"' + dst + '"'] - ) - targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) - - -def _add_android_cardboard_app_icon(targets: list[Target]) -> None: - _add_android_app_icon( - targets=targets, - src_name='icon_clipped_vr.png', - variant_name='cardboard', - ) - - -def _add_android_cardboard_app_icon_new(targets: list[Target]) -> None: - _add_android_app_icon_new( - targets=targets, - src_fg_name='icon_android_layered_fg_vr.png', - variant_name='cardboard', - ) - - -def _add_android_tv_banner(targets: list[Target]) -> None: - res = (320, 180) - src = os.path.join(RES_DIR, 'src', 'banner', 'banner_16x9.png') - dst = os.path.join( - ROOT_DIR, - 'ballisticacore-android', - 'BallisticaCore', - 'src', - 'main', - 'res', - 'drawable-xhdpi', - 'banner.png', - ) - cmd = ' '.join( - [RESIZE_CMD, str(res[0]), str(res[1]), '"' + src + '"', '"' + dst + '"'] - ) - targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) - - -def _add_apple_tv_top_shelf(targets: list[Target]) -> None: - instances = [ - ('24x9', '', '', 1920, 720), - ('29x9', ' Wide', '_wide', 2320, 720), - ] - for instance in instances: - for scale in [1, 2]: - res = (instance[3] * scale, instance[4] * scale) - src = os.path.join( - RES_DIR, 'src', 'banner', 'banner_' + instance[0] + '.png' + # Private-internal targets (not available at all in public) + self.targets = [] + basename = 'private-internal' + self._add_windows_icon(generic=False, oculus=True, inputs=True) + self._add_ios_app_icon() + self._add_macos_app_icon() + self._add_android_app_icon() + self._add_android_app_icon_new() + self._add_android_cardboard_app_icon() + self._add_android_cardboard_app_icon_new() + self._add_android_tv_banner() + self._add_apple_tv_top_shelf() + self._add_apple_tv_3d_icon() + self._add_apple_tv_store_icon() + self._add_google_vr_icon() + our_lines_private_2 = ( + ['# __PUBSYNC_STRIP_BEGIN__'] + + _empty_line_if(bool(self.targets)) + + self._emit_group_build_lines(basename) + + self._emit_group_clean_lines(basename) + + [t.emit() for t in self.targets] + + ['# __PUBSYNC_STRIP_END__'] ) - dst = os.path.join( - ROOT_DIR, - 'ballisticacore-xcode', - 'BallisticaCore Shared', - 'Assets.xcassets', - 'tvOS App Icon & Top Shelf Image.brandassets', - 'Top Shelf Image' + instance[1] + '.imageset', - 'shelf' + instance[2] + '_' + str(scale) + 'x.png', + our_lines_private = our_lines_private_1 + our_lines_private_2 + + filtered = ( + lines[: auto_start_public + 1] + + our_lines_public + + lines[auto_end_public : auto_start_private + 1] + + our_lines_private + + lines[auto_end_private:] + ) + out = '\n'.join(filtered) + '\n' + + return out + + def _emit_group_build_lines(self, basename: str) -> list[str]: + """Gen a group build target.""" + del basename # Unused. + out: list[str] = [] + if not self.targets: + return out + all_dsts = set() + for target in self.targets: + all_dsts.add(target.dst) + out.append( + "# Add this section's targets to the overall resources target.\n" + 'resources: \\\n ' + + ' \\\n '.join( + dst.replace(' ', '\\ ') for dst in sorted(all_dsts) ) + + '\n' + ) + return out + + def _emit_group_clean_lines(self, basename: str) -> list[str]: + """Gen a group clean target.""" + out: list[str] = [] + if not self.targets: + return out + all_dsts = set() + for target in self.targets: + all_dsts.add(target.dst) + out.append( + f'clean-{basename}:\n\trm -f ' + + ' \\\n '.join('"' + dst + '"' for dst in sorted(all_dsts)) + + '\n' + ) + out.append( + '# Include this section in an overall clean.\n' + f'clean: clean-{basename}\n' + ) + + return out + + def _emit_group_efrocache_lines(self) -> list[str]: + """Gen a group clean target.""" + out: list[str] = [] + if not self.targets: + return out + all_dsts = set() + for target in self.targets: + # We may need to make pipeline adjustments if/when we get filenames + # with spaces in them. + if ' ' in target.dst: + raise CleanError( + 'FIXME: need to account for spaces in filename' + f' "{target.dst}".' + ) + all_dsts.add(target.dst) + out.append( + 'efrocache-list:\n\t@echo ' + + ' \\\n '.join('"' + dst + '"' for dst in sorted(all_dsts)) + + '\n' + ) + out.append('efrocache-build: resources\n') + + return out + + def _add_windows_icon( + self, generic: bool, oculus: bool, inputs: bool + ) -> None: + sizes = [256, 128, 96, 64, 48, 32, 16] + all_icons = [] + for size in sizes: + dst_base = BUILD_DIR + src = os.path.join('icon', 'icon_clipped.png') + dst = os.path.join(dst_base, 'win_icon_' + str(size) + '_tmp.png') cmd = ' '.join( [ RESIZE_CMD, - str(res[0]), - str(res[1]), + str(size), + str(size), '"' + src + '"', '"' + dst + '"', ] ) - targets.append(Target(src=[src], dst=dst, cmd=cmd)) + all_icons.append(dst) + if inputs: + self.targets.append( + Target(src=[src], dst=dst, cmd=cmd, mkdir=True) + ) - -def _add_apple_tv_3d_icon(targets: list[Target]) -> None: - res = (400, 240) - for layer in ['Layer1', 'Layer2', 'Layer3', 'Layer4', 'Layer5']: - for scale in [1, 2]: - src = os.path.join( - RES_DIR, 'src', 'icon_appletv', layer.lower() + '.png' + # Assemble all the bits we just made into .ico files. + for path, enable in [ + ( + f'{ROOT_DIR}/{self.namel}-windows/Generic/{self.nameu}.ico', + generic, + ), + ( + f'{ROOT_DIR}/{self.namel}-windows/Oculus/{self.nameu}.ico', + oculus, + ), + ]: + cmd = ( + 'convert ' + + ''.join([' "' + f + '"' for f in all_icons]) + + ' "' + + path + + '"' ) + if enable: + self.targets.append(Target(src=all_icons, dst=path, cmd=cmd)) + + def _add_ios_app_icon(self) -> None: + sizes = [ + (20, 2), + (20, 3), + (29, 2), + (29, 3), + (40, 2), + (40, 3), + (60, 2), + (60, 3), + (20, 1), + (29, 1), + (40, 1), + (76, 1), + (76, 2), + (83.5, 2), + (1024, 1), + ] + for size in sizes: + res = int(size[0] * size[1]) + src = os.path.join('icon', 'icon_flat.png') dst = os.path.join( ROOT_DIR, - 'ballisticacore-xcode', - 'BallisticaCore Shared', + f'{self.namel}-xcode', + f'{self.nameu} Shared', 'Assets.xcassets', - 'tvOS App Icon & Top Shelf Image.brandassets', - 'App Icon.imagestack', - layer + '.imagestacklayer', - 'Content.imageset', - layer.lower() + '_' + str(scale) + 'x.png', + 'AppIcon iOS.appiconset', + 'icon_' + str(size[0]) + 'x' + str(size[1]) + '.png', ) cmd = ' '.join( [ RESIZE_CMD, - str(res[0] * scale), - str(res[1] * scale), + str(res), + str(res), '"' + src + '"', '"' + dst + '"', ] ) - targets.append(Target(src=[src], dst=dst, cmd=cmd)) + self.targets.append(Target(src=[src], dst=dst, cmd=cmd)) - -def _add_apple_tv_store_icon(targets: list[Target]) -> None: - res = (1280, 768) - for layer in ['Layer1', 'Layer2', 'Layer3', 'Layer4', 'Layer5']: - for scale in [1]: - src = os.path.join( - RES_DIR, 'src', 'icon_appletv', layer.lower() + '.png' - ) + def _add_macos_app_icon(self) -> None: + sizes = [ + (16, 1), + (16, 2), + (32, 1), + (32, 2), + (128, 1), + (128, 2), + (256, 1), + (256, 2), + (512, 1), + (512, 2), + ] + for size in sizes: + res = int(size[0] * size[1]) + src = os.path.join('icon', 'icon_clipped.png') dst = os.path.join( ROOT_DIR, - 'ballisticacore-xcode', - 'BallisticaCore Shared', + f'{self.namel}-xcode', + f'{self.nameu} Shared', 'Assets.xcassets', - 'tvOS App Icon & Top Shelf Image.brandassets', - 'App Icon - App Store.imagestack', - layer + '.imagestacklayer', - 'Content.imageset', - layer.lower() + '_' + str(scale) + 'x.png', + 'AppIcon macOS.appiconset', + 'icon_' + str(size[0]) + 'x' + str(size[1]) + '.png', ) cmd = ' '.join( [ RESIZE_CMD, - str(res[0] * scale), - str(res[1] * scale), + str(res), + str(res), '"' + src + '"', '"' + dst + '"', ] ) - targets.append(Target(src=[src], dst=dst, cmd=cmd)) + self.targets.append(Target(src=[src], dst=dst, cmd=cmd)) + def _add_android_app_icon( + self, + src_name: str = 'icon_clipped.png', + variant_name: str = 'main', + ) -> None: + sizes = [ + ('mdpi', 48), + ('hdpi', 72), + ('xhdpi', 96), + ('xxhdpi', 144), + ('xxxhdpi', 192), + ] + for size in sizes: + res = size[1] + src = os.path.join('icon', src_name) + dst = os.path.join( + ROOT_DIR, + f'{self.namel}-android', + f'{self.nameu}', + 'src', + variant_name, + 'res', + 'mipmap-' + size[0], + 'ic_launcher.png', + ) + cmd = ' '.join( + [ + RESIZE_CMD, + str(res), + str(res), + '"' + src + '"', + '"' + dst + '"', + ] + ) + self.targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) -def _add_google_vr_icon(targets: list[Target]) -> None: - res = (512, 512) - for layer in ['vr_icon_background', 'vr_icon']: - src = os.path.join(RES_DIR, 'src', 'icon_googlevr', layer + '.png') + def _add_android_app_icon_new( + self, + src_fg_name: str = 'icon_android_layered_fg.png', + src_bg_name: str = 'icon_android_layered_bg.png', + variant_name: str = 'main', + ) -> None: + sizes = [ + ('mdpi', 108), + ('hdpi', 162), + ('xhdpi', 216), + ('xxhdpi', 324), + ('xxxhdpi', 432), + ] + for size in sizes: + res = size[1] + + # Foreground component. + src = os.path.join('icon', src_fg_name) + dst = os.path.join( + ROOT_DIR, + f'{self.namel}-android', + f'{self.nameu}', + 'src', + variant_name, + 'res', + 'mipmap-' + size[0], + 'ic_launcher_foreground.png', + ) + cmd = ' '.join( + [ + RESIZE_CMD, + str(res), + str(res), + '"' + src + '"', + '"' + dst + '"', + ] + ) + self.targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) + + # Background component. + src = os.path.join('icon', src_bg_name) + dst = os.path.join( + ROOT_DIR, + f'{self.namel}-android', + f'{self.nameu}', + 'src', + variant_name, + 'res', + 'mipmap-' + size[0], + 'ic_launcher_background.png', + ) + cmd = ' '.join( + [ + RESIZE_CMD, + str(res), + str(res), + '"' + src + '"', + '"' + dst + '"', + ] + ) + self.targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) + + def _add_android_cardboard_app_icon(self) -> None: + self._add_android_app_icon( + src_name='icon_clipped_vr.png', + variant_name='cardboard', + ) + + def _add_android_cardboard_app_icon_new(self) -> None: + self._add_android_app_icon_new( + src_fg_name='icon_android_layered_fg_vr.png', + variant_name='cardboard', + ) + + def _add_android_tv_banner(self) -> None: + res = (320, 180) + src = os.path.join('banner', 'banner_16x9.png') dst = os.path.join( ROOT_DIR, - 'ballisticacore-android', - 'BallisticaCore', + f'{self.namel}-android', + f'{self.nameu}', 'src', - 'cardboard', + 'main', 'res', - 'drawable-nodpi', - layer + '.png', + 'drawable-xhdpi', + 'banner.png', ) cmd = ' '.join( [ @@ -449,107 +463,116 @@ def _add_google_vr_icon(targets: list[Target]) -> None: '"' + dst + '"', ] ) - targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) + self.targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) + + def _add_apple_tv_top_shelf(self) -> None: + instances = [ + ('24x9', '', '', 1920, 720), + ('29x9', ' Wide', '_wide', 2320, 720), + ] + for instance in instances: + for scale in [1, 2]: + res = (instance[3] * scale, instance[4] * scale) + src = os.path.join('banner', 'banner_' + instance[0] + '.png') + dst = os.path.join( + ROOT_DIR, + f'{self.namel}-xcode', + f'{self.nameu} Shared', + 'Assets.xcassets', + 'tvOS App Icon & Top Shelf Image.brandassets', + 'Top Shelf Image' + instance[1] + '.imageset', + 'shelf' + instance[2] + '_' + str(scale) + 'x.png', + ) + cmd = ' '.join( + [ + RESIZE_CMD, + str(res[0]), + str(res[1]), + '"' + src + '"', + '"' + dst + '"', + ] + ) + self.targets.append(Target(src=[src], dst=dst, cmd=cmd)) + + def _add_apple_tv_3d_icon(self) -> None: + res = (400, 240) + for layer in ['Layer1', 'Layer2', 'Layer3', 'Layer4', 'Layer5']: + for scale in [1, 2]: + src = os.path.join('icon_appletv', layer.lower() + '.png') + dst = os.path.join( + ROOT_DIR, + f'{self.namel}-xcode', + f'{self.nameu} Shared', + 'Assets.xcassets', + 'tvOS App Icon & Top Shelf Image.brandassets', + 'App Icon.imagestack', + layer + '.imagestacklayer', + 'Content.imageset', + layer.lower() + '_' + str(scale) + 'x.png', + ) + cmd = ' '.join( + [ + RESIZE_CMD, + str(res[0] * scale), + str(res[1] * scale), + '"' + src + '"', + '"' + dst + '"', + ] + ) + self.targets.append(Target(src=[src], dst=dst, cmd=cmd)) + + def _add_apple_tv_store_icon(self) -> None: + res = (1280, 768) + for layer in ['Layer1', 'Layer2', 'Layer3', 'Layer4', 'Layer5']: + for scale in [1]: + src = os.path.join('icon_appletv', layer.lower() + '.png') + dst = os.path.join( + ROOT_DIR, + f'{self.namel}-xcode', + f'{self.nameu} Shared', + 'Assets.xcassets', + 'tvOS App Icon & Top Shelf Image.brandassets', + 'App Icon - App Store.imagestack', + layer + '.imagestacklayer', + 'Content.imageset', + layer.lower() + '_' + str(scale) + 'x.png', + ) + cmd = ' '.join( + [ + RESIZE_CMD, + str(res[0] * scale), + str(res[1] * scale), + '"' + src + '"', + '"' + dst + '"', + ] + ) + self.targets.append(Target(src=[src], dst=dst, cmd=cmd)) + + def _add_google_vr_icon(self) -> None: + res = (512, 512) + for layer in ['vr_icon_background', 'vr_icon']: + src = os.path.join('icon_googlevr', layer + '.png') + dst = os.path.join( + ROOT_DIR, + f'{self.namel}-android', + f'{self.nameu}', + 'src', + 'cardboard', + 'res', + 'drawable-nodpi', + layer + '.png', + ) + cmd = ' '.join( + [ + RESIZE_CMD, + str(res[0]), + str(res[1]), + '"' + src + '"', + '"' + dst + '"', + ] + ) + self.targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) def _empty_line_if(condition: bool) -> list[str]: return [''] if condition else [] - - -def update(projroot: str, check: bool) -> None: - """main script entry point""" - # pylint: disable=too-many-locals - # pylint: disable=too-many-statements - - from efrotools import getconfig - - # Operate out of root dist dir for consistency. - os.chdir(projroot) - - public = getconfig(Path('.'))['public'] - assert isinstance(public, bool) - - fname = 'resources/Makefile' - with open(fname, encoding='utf-8') as infile: - original = infile.read() - lines = original.splitlines() - - auto_start_public = lines.index('# __AUTOGENERATED_PUBLIC_BEGIN__') - auto_end_public = lines.index('# __AUTOGENERATED_PUBLIC_END__') - auto_start_private = lines.index('# __AUTOGENERATED_PRIVATE_BEGIN__') - auto_end_private = lines.index('# __AUTOGENERATED_PRIVATE_END__') - - # Public targets (full sources available in public) - targets: list[Target] = [] - basename = 'public' - our_lines_public = ( - _empty_line_if(bool(targets)) - + _emit_group_build_lines(targets, basename) - + _emit_group_clean_lines(targets, basename) - + [t.emit() for t in targets] - ) - - # Only rewrite the private section in the private repo; otherwise - # keep the existing one intact. - if public: - our_lines_private = lines[auto_start_private + 1 : auto_end_private] - else: - # Private targets (available in public through efrocache) - targets = [] - basename = 'private' - _add_windows_icon(targets, generic=True, oculus=False, inputs=False) - our_lines_private_1 = ( - _empty_line_if(bool(targets)) - + _emit_group_build_lines(targets, basename) - + _emit_group_clean_lines(targets, basename) - + ['# __EFROCACHE_TARGET__\n' + t.emit() for t in targets] - + _emit_group_efrocache_lines(targets) - ) - - # Private-internal targets (not available at all in public) - targets = [] - basename = 'private-internal' - _add_windows_icon(targets, generic=False, oculus=True, inputs=True) - _add_ios_app_icon(targets) - _add_macos_app_icon(targets) - _add_android_app_icon(targets) - _add_android_app_icon_new(targets) - _add_android_cardboard_app_icon(targets) - _add_android_cardboard_app_icon_new(targets) - _add_android_tv_banner(targets) - _add_apple_tv_top_shelf(targets) - _add_apple_tv_3d_icon(targets) - _add_apple_tv_store_icon(targets) - _add_google_vr_icon(targets) - our_lines_private_2 = ( - ['# __PUBSYNC_STRIP_BEGIN__'] - + _empty_line_if(bool(targets)) - + _emit_group_build_lines(targets, basename) - + _emit_group_clean_lines(targets, basename) - + [t.emit() for t in targets] - + ['# __PUBSYNC_STRIP_END__'] - ) - our_lines_private = our_lines_private_1 + our_lines_private_2 - - filtered = ( - lines[: auto_start_public + 1] - + our_lines_public - + lines[auto_end_public : auto_start_private + 1] - + our_lines_private - + lines[auto_end_private:] - ) - out = '\n'.join(filtered) + '\n' - - if out == original: - print(f'{fname} is up to date.') - else: - if check: - if bool(False): - print( - f'FOUND------\n{original}\nEND FOUND--------\n' - f'EXPECTED------\n{out}\nEND EXPECTED-------\n' - ) - raise CleanError(f"ERROR: file is out of date: '{fname}'.") - print(f'{Clr.SBLU}Updating: {fname}{Clr.RST}') - with open(fname, 'w', encoding='utf-8') as outfile: - outfile.write(out) diff --git a/tools/batools/spinoff/__init__.py b/tools/batools/spinoff/__init__.py new file mode 100644 index 00000000..f54e5402 --- /dev/null +++ b/tools/batools/spinoff/__init__.py @@ -0,0 +1,23 @@ +# Released under the MIT License. See LICENSE for details. +# +"""A system to wrangle projects spun off from a parent Ballistica project. + +Think of this as 'subclassing' the project. +Spinoff can arbitrarily filter/override/exclude files from +the source project such that only a minimal number of additions +and changes need to be included in the spinoff project itself. + +Spinoff operates by copying or hard-linking source project files in +from a git submodule, while also telling git to ignore those same files. +At any point, the submodule/core system can be jettisoned to leave +a 100% self contained standalone project. To do this, just kill the +submodule and remove the 'spinoff' section in .gitignore. +""" + +from batools.spinoff._context import SpinoffContext +from batools.spinoff._main import spinoff_main + +__all__ = [ + 'SpinoffContext', + 'spinoff_main', +] diff --git a/tools/batools/spinoff/_config_template.py b/tools/batools/spinoff/_config_template.py new file mode 100644 index 00000000..83913f82 --- /dev/null +++ b/tools/batools/spinoff/_config_template.py @@ -0,0 +1,59 @@ +# Released under the MIT License. See LICENSE for details. +# +# pylint: disable=missing-docstring, invalid-name + +# A TEMPLATE CONFIG FOR CREATED SPINOFF DST PROJECTS. +# THIS IS NOT USED AT RUNTIME; IT ONLY EXISTS FOR TYPE-CHECKING PURPOSES. + +# This file is exec'ed by tools/spinoff, allowing us to customize +# how the parent project is filtered into ours. + +from __future__ import annotations + +from batools.spinoff import SpinoffContext + +ctx = SpinoffContext.get_active() + +# BallisticaKit will get replaced with this in default filtering. +ctx.dst_name = 'SPINOFF_TEMPLATE_NAME' + +# Feature sets from the source project that we should include in dst. +# Set to None to include all feature sets. Check config/featuresets to +# see what is available. These will be names like 'base', 'scene_v1', +# etc. Note that the 'core' feature set as well as feature sets required +# by ones we pass will be implicitly included as well. +# __SRC_FEATURE_SETS__ + +# These paths in the src project will be ignored during updates and +# not synced into this dst project. We can use this to omit parts of +# the src project that we don't want or that we intend to 'override' +# with our own versions. +src_omit_paths: list[str] = [] + +# Add ours to the existing set. +ctx.src_omit_paths.update(src_omit_paths) + +# Use this to 'carve out' directories or exact file paths which will be +# git-managed on dst. By default, spinoff will consider dirs containing +# the files it generates as 'spinoff-managed'; it will set them as +# git-ignored and will complain if any files appear in them that it does +# not manage itself (to prevent accidentally working in such places). +src_write_paths: list[str] = [] + +# Add ours to the existing set. +ctx.src_write_paths.update(src_write_paths) + + +# Define and register a filter-file-call. +# This will get called for each filtered file. +# The default_filter_file() call replaces variations of 'BallisticaKit' +# with the dst_name declared above. +def filter_file(context: SpinoffContext, src_path: str, text: str) -> str: + text = context.default_filter_file(src_path, text) + + # Custom filtering would go here. + + return text + + +ctx.filter_file_call = filter_file diff --git a/tools/batools/spinoff/_context.py b/tools/batools/spinoff/_context.py new file mode 100644 index 00000000..d8e665e9 --- /dev/null +++ b/tools/batools/spinoff/_context.py @@ -0,0 +1,2031 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Spinoff system for spawning child projects from a ballistica project.""" +# pylint: disable=too-many-lines + +from __future__ import annotations + +import os +import sys +import fnmatch +import tempfile +import subprocess +from enum import Enum +from pathlib import Path +from typing import TYPE_CHECKING, assert_never + +from efrotools.code import format_python_str, format_cpp_str +from efro.error import CleanError +from efro.terminal import Clr +from efro.util import timedelta_str +from batools.featureset import FeatureSet +from batools.spinoff._state import ( + EntityType, + DstEntitySet, + SrcEntity, + DstEntity, +) + +if TYPE_CHECKING: + from typing import Callable, Any, Iterable + + from batools.project import ProjectUpdater + + +class SpinoffContext: + """Guts of the spinoff system.""" + + _active_context: SpinoffContext | None = None + + class BackportInProgressError(Exception): + """Error we can raise to bow out of processing during a backport.""" + + class Mode(Enum): + """Mode the context can operate in.""" + + STATUS = 'status' + UPDATE = 'update' + CHECK = 'check' + CLEAN_LIST = 'cleanlist' + CLEAN = 'clean' + CLEAN_CHECK = 'cleancheck' + OVERRIDE = 'override' + DIFF = 'diff' + BACKPORT = 'backport' + + def __init__( + self, + src_root: str, + dst_root: str, + mode: Mode, + force: bool = False, + print_full_lists: bool = False, + override_paths: list[str] | None = None, + backport_file: str | None = None, + auto_backport: bool = False, + ) -> None: + # pylint: disable=too-many-statements + + # By default, if dst files have their modtimes changed but + # still line up with src files, we can recover. But one may + # choose to error in that case to track down things mucking + # with dst files when they shouldnt' be. + self.strict = False + + self._mode = mode + self._force = force + self._print_full_lists = print_full_lists + self._override_paths = override_paths + self._backport_file = backport_file + self._auto_backport = auto_backport + + self._project_updater: ProjectUpdater | None = None + + if not os.path.isdir(src_root): + raise CleanError(f"Spinoff src dir not found: '{src_root}'.") + if not os.path.isdir(dst_root): + raise CleanError(f"Spinoff dst dir not found: '{dst_root}'.") + + # FeatureSet names we should include + self.src_feature_sets: set[str] | None = None + + # Just to be safe, make sure we're working with abs paths. + self._src_root = os.path.abspath(src_root) + self._dst_root = os.path.abspath(dst_root) + + self._data_file_path = os.path.join(self._dst_root, '.spinoffdata') + + self._built_parent_repo_tool_configs = False + + self._auto_backport_success_count = 0 + self._auto_backport_fail_count = 0 + + self._src_name = 'BallisticaKit' + + self._src_all_feature_sets = { + f.name: f for f in FeatureSet.get_all_for_project(self._src_root) + } + + self._src_git_files: set[str] | None = None + self._dst_git_files: set[str] | None = None + self._dst_git_file_dirs: set[str] | None = None + + self.filter_file_call: Callable[[SpinoffContext, str, str], str] = type( + self + ).default_filter_file + + self.filter_path_call: Callable[[SpinoffContext, str], str] = type( + self + ).default_filter_path + + self._execution_error = False + + self._project_file_names = { + 'Makefile', + 'CMakeLists.txt', + '.meta_manifest_public.json', + '.meta_manifest_private.json', + '.asset_manifest_public.json', + '.asset_manifest_private.json', + } + self._project_file_suffixes = { + '.vcxproj', + '.vcxproj.filters', + '.pbxproj', + } + # Set of files/symlinks in src. + self._src_entities: dict[str, SrcEntity] = {} + + # Set of files/symlinks in dst. + self._dst_entities: dict[str, DstEntity] = {} + + # Src entities for which errors have occurred + # (dst modified independently, etc). + self._src_error_entities: dict[str, str] = {} + + # Dst entries with errors + # (non-spinoff files in spinoff-owned dirs, etc). + self._dst_error_entities: dict[str, str] = {} + + # Entities in src we should filter/copy. + self._src_copy_entities = set[str]() + + # Entities in src we should simply re-cache modtimes/sizes for. + self._src_recache_entities = set[str]() + + # Dst entities still found in src. + self._dst_entities_claimed = set[str]() + + # Entities in dst we should kill. + self._dst_purge_entities = set[str]() + + # Normally spinoff errors if it finds any files in its managed dirs + # that it did not put there. This is to prevent accidentally working + # in these parts of a dst project; since these sections are git-ignored, + # git itself won't raise any warnings in such cases and it would be easy + # to accidentally lose work otherwise. + # This list can be used to suppress spinoff's errors for specific + # locations. This is generally used to allow build output or other + # dynamically generated files to exist within spinoff-managed + # directories. It is possible to use src_write_paths for such purposes, + # but this has the side-effect of greatly complicating the dst + # project's gitignore list; selectively marking a few dirs as + # unchecked makes for a cleaner setup. Just be careful to not set + # excessively broad regions as unchecked; you don't want to mask + # actual useful error messages. + self.src_unchecked_paths = set[str]() + + # TODO(ericf): describe this. + self.project_file_paths = set[str]() + + # Anything under these dirs WILL be filtered. + self.filter_dirs = set[str]() + + # ELSE anything under these dirs will NOT be filtered. + self.no_filter_dirs = set[str]() + + # ELSE files matching these exact base names WILL be filtered + # (so FOO matches a/b/FOO as well as just FOO). + self.filter_file_names = set[str]() + + # ELSE files matching these exact base names will NOT be filtered. + self.no_filter_file_names = set[str]() + + # ELSE files with these extensions WILL be filtered. + self.filter_file_extensions = set[str]() + + # ELSE files with these extensions will NOT be filtered. + self.no_filter_file_extensions = set[str]() + + self._spinoff_managed_dirs: set[str] | None = None + + # These paths in the src project will be skipped over during updates and + # not synced into the dst project. The dst project can use this to + # trim out parts of the src project that it doesn't want or that it + # intends to 'override' with its own versions. + self.src_omit_paths = set[str]() + + # Any files/dirs with these base names will be ignored by spinoff + # on both src and dst. + self.ignore_names = set[str]() + + # Use this to 'carve out' directories or exact file paths which will be + # git-managed on dst. By default, spinoff will consider dirs containing + # the files it generates as 'spinoff-managed'; it will set them as + # git-ignored and will complain if any files appear in them that it does + # not manage itself (to prevent accidentally working in such places). + self.src_write_paths = set[str]() + + # Paths wwhich will NOT be gitignored/etc. (in dst format) + self.dst_write_paths = set[str]() + + # Special set of paths managed by spinoff but ALSO stored in git in + # the dst project. This is for bare minimum stuff needed to be always + # present in dst for bootstrapping, indexing by github, etc). Changes + # to these files in dst will be silently and happily overwritten by + # spinoff, so tread carefully. + self.git_mirrored_paths = set[str]() + + # File names that can be quietly ignored or cleared out when found. + # This should encompass things like .DS_Store files created by the + # Mac Finder when browsing directories. This helps spinoff remove + # empty directories when doing a 'clean', etc. + self.cruft_file_names = set[str]() + + self.dst_name = 'Untitled' + + self._src_config_path = os.path.join( + self._src_root, 'config', 'spinoffconfig.py' + ) + if not os.path.exists(self._src_config_path): + raise CleanError( + f"Spinoff src config not found at '{self._src_config_path}'." + ) + self._dst_config_path = os.path.join( + self._dst_root, 'config', 'spinoffconfig.py' + ) + if not os.path.exists(self._dst_config_path): + raise CleanError( + f"Spinoff dst config not found at '{self._dst_config_path}'." + ) + + # Sets various stuff from user config .py files. + self._apply_project_configs() + + # Based on feature-sets they requested, calc which feature-sets + # from src we *exclude*. + self._src_omit_feature_sets = self._calc_src_omit_feature_sets() + + # Generate a version of src_omit_paths that includes our feature-set + # omissions. Basically, omitting a feature set simply omits + # particular names at a few particular places. + self._src_omit_paths_expanded = self.src_omit_paths.copy() + self._add_feature_set_omit_paths(self._src_omit_paths_expanded) + + # Create a version of dst-write-paths that also includes filtered + # src-write-paths as well as parents of everything. + # (so if a/b/c is added as a write path, stuff under a and a/b + # will also be covered). + # We also add git_mirrored_paths since that stuff is intended + # to be in git as well. + self._dst_write_paths_expanded = self._filter_paths( + self.src_write_paths + ) + self._dst_write_paths_expanded.update(self.dst_write_paths) + self._dst_write_paths_expanded.update( + self._filter_paths(self.git_mirrored_paths) + ) + for path in self._dst_write_paths_expanded.copy(): + for subpath in _get_dir_levels(path): + self._dst_write_paths_expanded.add(subpath) + + # Create a version of src_unchecked_paths for dst. + self._dst_unchecked_paths = self._filter_paths(self.src_unchecked_paths) + self._sanity_test_setup() + self._generate_env_hash() + + def _calc_src_omit_feature_sets(self) -> set[str]: + if self.src_feature_sets is None: + return set() + + # Based on the requested set, calc the total sets we'll need. + # Also always include 'core' since we'd be totally broken + # without it. + reqs = FeatureSet.resolve_requirements( + list(self._src_all_feature_sets.values()), + self.src_feature_sets | {'core'}, + ) + + # Now simply return any sets *not* included in our resolved set. + return {s for s in self._src_all_feature_sets.keys() if s not in reqs} + + def _add_feature_set_omit_paths(self, paths: set[str]) -> None: + for fsname in sorted(self._src_omit_feature_sets): + featureset = self._src_all_feature_sets.get(fsname) + if featureset is None: + raise CleanError( + f"src_omit_feature_sets entry '{featureset}' not found" + f' on src project.' + ) + + # Omit its config file. + # Make sure this featureset exists on src. + fsconfigpath = f'config/featuresets/featureset_{fsname}.py' + paths.add(fsconfigpath) + + # Omit its Python package. + fspackagename = featureset.name_python_package + paths.add(f'src/assets/ba_data/python/{fspackagename}') + + # Omit its C++ dir. + paths.add(f'src/ballistica/{fsname}') + + # Omits its meta dir. + fsmetapackagename = featureset.name_python_package_meta + paths.add(f'src/meta/{fsmetapackagename}') + + @classmethod + def get_active(cls) -> SpinoffContext: + """Return the context currently running.""" + if cls._active_context is None: + raise RuntimeError('No active context.') + return cls._active_context + + def run(self) -> None: + """Do the thing.""" + # pylint: disable=too-many-branches + + self._read_state() + + # Get the list of src files managed by git. + self._src_git_files = set[str]( + subprocess.run( + ['git', 'ls-files'], + check=True, + cwd=os.path.join(self._src_root), + capture_output=True, + ) + .stdout.decode() + .splitlines() + ) + + # Ignore anything under omitted paths/names. + self._filter_src_git_file_list() + + # Now map whatever is left to paths in dst. + self._dst_git_files = set( + self._filter_path(s) for s in self._src_git_files + ) + + # Build a set of all dirs on dst containing a mapped file + # (excluding root). + fdirs = self._dst_git_file_dirs = set[str]() + for dst_git_file in self._dst_git_files: + dname = os.path.dirname(dst_git_file) + if dname: + # Expand to include directories above these as well. + # We want this set to be 'everything that (even recursively) + # contains a synced dst file'. + for leveldir in _get_dir_levels(dname): + fdirs.add(leveldir) + + # Now take that list and filter out ones under our write paths + # to get our final list of spinoff-managed-dirs. + self._calc_spinoff_managed_dirs() + + # Check our spinoff-managed-dirs for any unrecognized files/etc. + # Since we git-ignore all of them, this is an important safety + # feature to avoid blowing away work. + self._check_spinoff_managed_dirs() + + if self._mode in { + self.Mode.CLEAN, + self.Mode.CLEAN_LIST, + self.Mode.CLEAN_CHECK, + }: + # For clean operations, simply stuff all dst entities + # into our purge list. + self._purge_all_dst_entities() + else: + # For normal operations, queue up our copy ops/etc. + self._register_sync_operations() + + # Tracked dst files that didn't get claimed can be killed. + for key in self._dst_entities: + if key not in self._dst_entities_claimed: + self._dst_purge_entities.add(key) + + # Special case: if we're doing an auto-backport, stop here. + # Otherwise we wind up showing all the errors we probably just fixed. + if self._mode is self.Mode.BACKPORT and self._auto_backport: + bpcolor = Clr.YLW if self._auto_backport_fail_count else Clr.GRN + print( + f'{bpcolor}Auto-backport complete; backported' + f' {self._auto_backport_success_count}; ' + f'skipped {self._auto_backport_fail_count}.{Clr.RST}' + ) + raise self.BackportInProgressError + + # If anything is off, print errors; otherwise actually do the deed. + if self._src_error_entities or self._dst_error_entities: + self._print_error_entities() + else: + if ( + self._mode is self.Mode.STATUS + or self._mode is self.Mode.CLEAN_LIST + ): + self._status() + elif self._mode is self.Mode.DIFF: + self._diff() + elif ( + self._mode is self.Mode.UPDATE or self._mode is self.Mode.CLEAN + ): + self._update() + elif self._mode is self.Mode.OVERRIDE: + self._override() + elif self._mode is self.Mode.BACKPORT: + # If backport gets here, the file they passed isn't erroring. + raise CleanError( + 'Nothing needs backporting.' + if self._backport_file is None + else 'Provided file does not need backporting.' + ) + elif ( + self._mode is self.Mode.CHECK + or self._mode is self.Mode.CLEAN_CHECK + ): + pass + else: + assert_never(self._mode) + + # Bail at this point if anything went wrong. We don't store state + # or update the .gitignore or anything in that case. + # Note: perhaps we should? If we wrote a bit of stuff and then + # failed we'll get a bunch of complaints about mod times changing + # under us next time we run, right? + if ( + self._src_error_entities + or self._dst_error_entities + or self._execution_error + ): + # Any of these have printed error info already so no need to + # do so ourself. + raise CleanError() + + # If we did anything that possibly deleted stuff, clean up any + # empty dirs that got left behind (hmm should we be more selective + # here to avoid dirs we didn't manage?..) + if self._mode is self.Mode.CLEAN or self._mode is self.Mode.UPDATE: + self._clean_cruft() + + # Update .gitignore to ignore everything spinoff-managed. + if self._mode is self.Mode.UPDATE or self._mode is self.Mode.OVERRIDE: + self._write_gitignore() + + # Only writing updated state in case of success. Is there a reason + # we would want to write it on errors also? + self._write_state() + + def _apply_project_configs(self) -> None: + # pylint: disable=exec-used + try: + assert self._active_context is None + type(self)._active_context = self + + # Apply both src and dist spinoff configs. + for config_path in (self._src_config_path, self._dst_config_path): + exec_context: dict = {} + with open(config_path, encoding='utf-8') as infile: + config_contents = infile.read() + + # Use compile here so we can provide a nice file path for + # error tracebacks. + exec( + compile(config_contents, config_path, 'exec'), + exec_context, + exec_context, + ) + + finally: + assert type(self)._active_context is self + type(self)._active_context = None + + def _calc_spinoff_managed_dirs(self) -> None: + assert self._dst_git_file_dirs is not None + # Take our list of dirs containing stuff synced in from src + # and strip out anything that has been explicitly been called + # out as a write-path. What's left will the set of dirs we consider + # spinoff-managed. + all_spinoff_managed_dirs = set[str]() + for gitfiledir in self._dst_git_file_dirs: + # If we see this exact dir in our expanded write-paths set, + # (which includes parents), pop it out. + if gitfiledir in self._dst_write_paths_expanded: + continue + all_spinoff_managed_dirs.add(gitfiledir) + + top_level_spinoff_managed_dirs = set[str]() + + # Now take this big soup of dirs and filter it down to top-level ones. + for rdir in all_spinoff_managed_dirs: + if any(rdir.startswith(f'{d}/') for d in all_spinoff_managed_dirs): + continue + top_level_spinoff_managed_dirs.add(rdir) + + self._spinoff_managed_dirs = top_level_spinoff_managed_dirs + + def _sanity_test_setup(self) -> None: + # Sanity tests: + # None of our names lists should ever end in a trailing backslash + # (currently breaks our logic). + for entitylist in [ + self.filter_dirs, + self.no_filter_dirs, + self.filter_file_names, + self.no_filter_file_names, + self.filter_file_extensions, + self.no_filter_file_extensions, + self.git_mirrored_paths, + self._src_omit_paths_expanded, + self.ignore_names, + self.src_unchecked_paths, + ]: + for ent in entitylist: + if ent.endswith('/'): + raise RuntimeError(f"list item {ent} ends in '/'") + + # Make sure nothing in a directory list refers to something that's a + # file. + for entitylist in [ + self.filter_dirs, + self.no_filter_dirs, + ]: + for ent in entitylist: + if os.path.exists(ent): + if not os.path.isdir(ent): + raise RuntimeError( + f'list item {ent} in a dir-list is not a dir' + ) + + # Likewise make sure nothing in a file list refers to a directory. + for ent in []: + if os.path.exists(ent): + if os.path.isdir(ent): + raise RuntimeError( + f'list item {ent} in a file-list is a dir' + ) + + def _generate_env_hash(self) -> None: + from efrotools import get_files_hash + import batools.spinoff + + # Generate an 'env' hash we can tag tracked files with, so + # that if spinoff scripts or config files change it will + # invalidate all tracked files. + hashfiles = set[str]() + + # Add all Python files under our spinoff subpackage. + for root, _subdirs, fnames in os.walk( + os.path.dirname(batools.spinoff.__file__) + ): + for fname in fnames: + if fname.endswith('.py') and not fname.startswith('flycheck_'): + hashfiles.add(os.path.join(root, fname)) + + # Hash config files too since they can affect anything. + hashfiles.add(self._src_config_path) + hashfiles.add(self._dst_config_path) + + self._envhash = get_files_hash(sorted(hashfiles)) + + def _read_state(self) -> None: + """Read persistent state from disk.""" + if os.path.exists(self._data_file_path): + self._dst_entities = DstEntitySet.read_from_file( + self._data_file_path + ).entities + + def _write_state(self) -> None: + """Write persistent state to disk.""" + DstEntitySet(entities=self._dst_entities).write_to_file( + self._data_file_path + ) + + def _write_gitignore(self) -> None: + """filter/write out a gitignore file.""" + assert self._dst_git_files is not None + assert self._spinoff_managed_dirs is not None + + # We've currently got a list of spinoff-managed-dirs which each + # results in a gitignore entry. On top of that we add entries + # for individual files that aren't covered by those dirs. + gitignore_entries = self._spinoff_managed_dirs.copy() + for gitpath in self._dst_git_files: + if self._should_add_gitignore_path(gitpath): + gitignore_entries.add(gitpath) + + # Pull in src .gitignore. + with open( + os.path.join(self._src_root, '.gitignore'), encoding='utf-8' + ) as infile: + gitignoreraw = infile.read() + + # Run standard filters on it. + gitignoreraw = self._filter_file('.gitignore', gitignoreraw) + gitignorelines = gitignoreraw.splitlines() + + # Now add our ignore entries at the bottom. + start_line = ( + '# Ignore everything managed by spinoff.\n' + '# To control this, modify src_write_paths in' + " 'config/spinoffconfig.py'.\n" + "# If you ever want to 'flatten' your project and remove it" + ' from spinoff\n' + '# control completely: simply delete this section, delete' + " the 'tools/spinoff'\n" + "# symlink, and delete 'config/spinoffconfig.py'. Then you can add" + ' everything\n' + '# in its current state to your git repo and forget that spinoff' + ' ever existed.' + ) + if gitignorelines and gitignorelines[-1] != '': + gitignorelines.append('') + gitignorelines.append(start_line) + for entry in sorted(gitignore_entries): + gitignorelines.append(f'/{entry}') + + # Add a blurb about this coming from spinoff. + blurb = ( + '# THIS FILE IS AUTOGENERATED BY SPINOFF;' + ' MAKE ANY EDITS IN SOURCE PROJECT' + ) + gitignorelines = [blurb, ''] + gitignorelines + with open( + os.path.join(self._dst_root, '.gitignore'), 'w', encoding='utf-8' + ) as outfile: + outfile.write('\n'.join(gitignorelines) + '\n') + + def _filter_path(self, path: str) -> str: + """Run filtering on a given path.""" + + return self.filter_path_call(self, path) + + def default_filter_path(self, text: str) -> str: + """Run default filtering on path text.""" + return self.default_filter_text(text) + + def replace_path_components( + self, path: str, replace_src: str, replace_dst: str + ) -> str: + """Replace a path hierarchy with another. + + Does the right thing for parents. For instance, src 'a/b/c' + and dst 'a2/b2/c2' will correctly filter 'a/foo' to 'a2/foo' + and 'a/b/foo' to 'a2/b2/foo'. + """ + pathsrc = replace_src.split('/') + pathdst = replace_dst.split('/') + assert len(pathsrc) == len(pathdst) + splits = path.split('/') + cmplen = min(len(splits), len(pathsrc)) + if splits[:cmplen] == pathsrc[:cmplen]: + return '/'.join(pathdst[:cmplen] + splits[cmplen:]) + return path + + def default_filter_text(self, text: str) -> str: + """Run default filtering on a piece of text.""" + + # Replace uppercase, lowercase, and mixed versions of our name. + return ( + text.replace(self._src_name.upper(), self.dst_name.upper()) + .replace(self._src_name.lower(), self.dst_name.lower()) + .replace(self._src_name, self.dst_name) + ) + + def default_filter_file(self, src_path: str, text: str) -> str: + """Run default filtering on a file.""" + + # Add warnings to some of the git-managed files that we write. + if src_path == 'README.md': + blurb = ( + '(this readme is autogenerated by spinoff; ' + 'make any edits in source project)' + ) + lines = self.default_filter_text(text).splitlines() + return '\n'.join(lines[:1] + ['', blurb] + lines[1:]) + if 'Jenkinsfile' in src_path: + blurb = ( + '// THIS FILE IS AUTOGENERATED BY SPINOFF;' + ' MAKE ANY EDITS IN SOURCE PROJECT' + ) + lines = self.default_filter_text(text).splitlines() + return '\n'.join([blurb, ''] + lines) + if src_path in ['.gitattributes']: + blurb = ( + '# THIS FILE IS AUTOGENERATED BY SPINOFF;' + ' MAKE ANY EDITS IN SOURCE PROJECT' + ) + lines = self.default_filter_text(text).splitlines() + return '\n'.join([blurb, ''] + lines) + + # Jetbrains dict files will get sorted differently after filtering + # words; go ahead and do that as we filter to avoid triggering + # difference errors next time the dst dict is saved. + # FIXME: generalize this for any jetbrains dict path; not just mine. + if src_path.endswith('/ericf.xml'): + from efrotools.code import sort_jetbrains_dict + + return sort_jetbrains_dict(self.default_filter_text(text)) + + return self.default_filter_text(text) + + def _encoding_for_file(self, path: str) -> str: + """Returns the text encoding a file requires.""" + + # Just make sure this path is valid; at some point we may want to + # crack the file. + if not os.path.isfile(path): + raise RuntimeError('invalid path passed to _encoding_for_file') + + # These files seem to get cranky if we try to convert them to utf-8. + # TODO: I think I read that MSVC 2017+ might be more lenient here; + # should check that out because this is annoying. + if path.endswith('BallisticaKit.rc') or path.endswith('Resource.rc'): + return 'utf-16le' + return 'utf-8' + + def _filter_file(self, src_path: str, text: str) -> str: + """Run filtering on a given file.""" + + # Run our registered filter call. + out = self.filter_file_call(self, src_path, text) + + # Run formatting on some files if they change. Otherwise, running + # a preflight in the dst project could change things, leading to + # 'spinoff-managed-file-changed' errors. + + # Note that we use our parent repo for these commands to pick up their + # tool configs, since those might not exist yet in our child repo. + # (This also means we need to make sure tool configs have been + # generated in the parent repo). + + # WARNING: hard-coding a few 'script' files that don't end in .py too. + # The proper way might be to ask the parent repo for its full list of + # script files but that would add more expense. + if ( + src_path.endswith('.py') or src_path in {'tools/cloudshell'} + ) and out != text: + self._ensure_parent_repo_tool_configs_exist() + out = format_python_str(out) + + # Ditto for .cc + if src_path.endswith('.cc') and out != text: + self._ensure_parent_repo_tool_configs_exist() + out = format_cpp_str( + projroot=Path(self._src_root), + text=out, + filename=os.path.basename(src_path), + ) + + return out + + def _ensure_parent_repo_tool_configs_exist(self) -> None: + if not self._built_parent_repo_tool_configs: + # Interestingly, seems we need to use shell command cd here + # instead of just passing cwd arg. + subprocess.run( + f'cd {self._src_root} && make prereqs', + shell=True, + check=True, + capture_output=True, + ) + self._built_parent_repo_tool_configs = True + + def _should_filter_src_file(self, path: str) -> bool: + """Return whether a given file should be filtered.""" + basename = os.path.basename(path) + ext = os.path.splitext(basename)[1] + if any(path.startswith(f'{p}/') for p in self.filter_dirs): + return True + if any(path.startswith(f'{p}/') for p in self.no_filter_dirs): + return False + if basename in self.filter_file_names: + return True + if basename in self.no_filter_file_names: + return False + if ext in self.filter_file_extensions: + return True + if ext in self.no_filter_file_extensions: + return False + raise RuntimeError(f"No filter rule for path '{path}'.") + + def _should_add_gitignore_path(self, path: str) -> bool: + """Return whether a file path should be added to gitignore.""" + assert self._spinoff_managed_dirs is not None + + # Special case: specific dirs/files we *always* want in git + # should never get added to gitignore. + if _any_path_contains(self.git_mirrored_paths, path): + return False + + # If there's a spinoff-managed dir above us, we're already covered. + if any(path.startswith(f'{d}/') for d in self._spinoff_managed_dirs): + return False + + # Go ahead and ignore. + return True + + def _print_error_entities(self) -> None: + """Print info about entity errors encountered.""" + print( + '\nSpinoff Error(s) Found:\n' + " Tip: to resolve 'spinoff-managed file modified' errors,\n" + " use the 'backport' subcommand.\n", + file=sys.stderr, + ) + for key, val in sorted(self._src_error_entities.items()): + dst = self._src_entities[key].dst + print( + f' {Clr.RED}Error: {dst}{Clr.RST} ({val})', + file=sys.stderr, + ) + for key, val in sorted(self._dst_error_entities.items()): + print( + f' {Clr.RED}Error: {key}{Clr.RST} ({val})', + file=sys.stderr, + ) + print('') + + def _validate_final_lists(self) -> None: + """Make sure we never delete the few files we're letting git store.""" + + for ent in self._dst_purge_entities.copy(): + if _any_path_contains(self.git_mirrored_paths, ent): + print( + 'WARNING; git-mirrored entity' + f" '{ent}' unexpectedly found on purge list. Ignoring.", + file=sys.stderr, + ) + self._dst_purge_entities.remove(ent) + + def _purge_all_dst_entities(self) -> None: + """Go through everything in dst and add it to our purge list. + + (or error if unsafe to do so) + """ + for key, val in list(self._dst_entities.items()): + # We never want to purge git-managed stuff. + if _any_path_contains(self.git_mirrored_paths, key): + continue + + dst_path = key + dst_path_full = os.path.join(self._dst_root, dst_path) + + # If dst doesnt exist we just ignore it. + if not os.path.exists(dst_path_full): + continue + + # For symlinks we just error if dst is no longer a symlink; + # otherwise kill it. + if val.entity_type is EntityType.SYMLINK: + if not os.path.islink(dst_path_full): + self._dst_error_entities[dst_path] = 'expected a symlink' + continue + self._dst_purge_entities.add(dst_path) + continue + + # Cor regular files we try to make sure nothing changed + # since we put it there. + src_path = val.src_path + assert src_path is not None + src_path_full = os.path.join(self._src_root, src_path) + dst_size = val.dst_size + dst_mtime = val.dst_mtime + + if (os.path.getsize(dst_path_full) == dst_size) and ( + os.path.getmtime(dst_path_full) == dst_mtime + ): + self._dst_purge_entities.add(key) + else: + self._attempt_purge_modified_dst( + src_path, src_path_full, dst_path, dst_path_full, key + ) + + def _attempt_purge_modified_dst( + self, + src_path: str, + src_path_full: str, + dst_path: str, + dst_path_full: str, + key: str, + ) -> None: + # Ick; dst changed. Now the only way we allow + # the delete is if we can re-filter its src + # and come up with the same dst again + # (meaning it probably just had its timestamp + # changed and nothing else). + if self._should_filter_src_file(src_path): + encoding = self._encoding_for_file(src_path_full) + with open(src_path_full, 'rb') as infile: + try: + src_data = self._filter_file( + src_path, infile.read().decode(encoding) + ) + except Exception: + print(f"Error decoding/filtering file: '{src_path}'.") + raise + with open(dst_path_full, 'rb') as infile: + try: + dst_data = infile.read().decode(encoding) + except Exception: + print(f"Error decoding file: '{dst_path}'.") + raise + still_same = src_data == dst_data + else: + with open(src_path_full, 'rb') as infile_b: + src_data_b = infile_b.read() + with open(dst_path_full, 'rb') as infile_b: + dst_data_b = infile_b.read() + still_same = src_data_b == dst_data_b + if still_same: + self._dst_purge_entities.add(key) + else: + self._dst_error_entities[dst_path] = 'spinoff-managed file modified' + + def _remove_empty_folders( + self, path: str, remove_root: bool = True + ) -> None: + """Remove empty folders.""" + if not os.path.isdir(path): + return + + # Ignore symlinks. + if os.path.islink(path): + return + + # Remove empty subdirs. + fnames = os.listdir(path) + if fnames: + for fname in fnames: + fullpath = os.path.join(path, fname) + if os.path.isdir(fullpath): + self._remove_empty_folders(fullpath) + + # If folder empty, delete it. + fnames = os.listdir(path) + if not fnames and remove_root: + os.rmdir(path) + + def _handle_recache_entities(self) -> None: + """Re-cache some special case entries. + + For these entries we simply re-cache modtimes/sizes + but don't touch any actual files. + """ + for src_path in self._src_recache_entities: + src_entity = self._src_entities[src_path] + dst_path = src_entity.dst + src_path_full = os.path.join(self._src_root, src_path) + dst_path_full = os.path.join(self._dst_root, dst_path) + self._dst_entities[dst_path] = DstEntity( + entity_type=src_entity.entity_type, + env_hash=self._envhash, + src_path=src_path, + src_mtime=os.path.getmtime(src_path_full), + src_size=os.path.getsize(src_path_full), + dst_mtime=os.path.getmtime(dst_path_full), + dst_size=os.path.getsize(dst_path_full), + ) + + def _status(self) -> None: + self._validate_final_lists() + self._handle_recache_entities() + max_print = 10 + + # FIXME: We should show .gitignore here in cases when it would change + # (we handle that specially). + if self._src_copy_entities: + print( + f'\n{len(self._src_copy_entities)}' + f' file(s) would be updated:\n', + file=sys.stderr, + ) + src_copy_entities_truncated = sorted(self._src_copy_entities) + if ( + not self._print_full_lists + and len(src_copy_entities_truncated) > max_print + ): + src_copy_entities_truncated = src_copy_entities_truncated[ + :max_print + ] + for ename in src_copy_entities_truncated: + dst_path_full = os.path.join( + self._dst_root, self._src_entities[ename].dst + ) + exists = os.path.exists(dst_path_full) + modstr = 'modified' if exists else 'new' + dstent = self._src_entities[ename].dst + print( + f' {Clr.GRN}{modstr}: {dstent}{Clr.RST}', + file=sys.stderr, + ) + if len(src_copy_entities_truncated) != len(self._src_copy_entities): + morecnt = len(self._src_copy_entities) - len( + src_copy_entities_truncated + ) + print( + f' {Clr.GRN}{Clr.BLD}(plus {morecnt} more;' + f' pass --full for complete list){Clr.RST}', + file=sys.stderr, + ) + dst_purge_entities_valid: set[str] = set() + if self._dst_purge_entities: + self._list_dst_purge_entities(dst_purge_entities_valid, max_print) + if not self._src_copy_entities and not dst_purge_entities_valid: + print(f'{Clr.GRN}Spinoff is up-to-date.{Clr.RST}', file=sys.stderr) + else: + print('') + + def _list_dst_purge_entities( + self, dst_purge_entities_valid: set[str], max_print: int + ) -> None: + for ent in self._dst_purge_entities: + dst_path_full = os.path.join(self._dst_root, ent) + + # Only make note of the deletion if it exists. + if ( + os.path.exists(dst_path_full) + # and ent not in self._dst_entities_delete_quietly + ): + dst_purge_entities_valid.add(ent) + if dst_purge_entities_valid: + print( + f'\n{len(dst_purge_entities_valid)} file(s)' + ' would be removed:\n', + file=sys.stderr, + ) + dst_purge_entities_truncated = sorted(dst_purge_entities_valid) + if ( + not self._print_full_lists + and len(dst_purge_entities_truncated) > max_print + ): + dst_purge_entities_truncated = dst_purge_entities_truncated[ + :max_print + ] + for ent in sorted(dst_purge_entities_truncated): + print(f' {Clr.GRN}{ent}{Clr.RST}', file=sys.stderr) + if len(dst_purge_entities_truncated) != len(dst_purge_entities_valid): + num_more = len(dst_purge_entities_valid) - len( + dst_purge_entities_truncated + ) + print( + f' {Clr.GRN}{Clr.BLD}(plus {num_more} more;' + f' pass --full for complete list){Clr.RST}', + file=sys.stderr, + ) + + def _override(self) -> None: + """Add one or more overrides.""" + # pylint: disable=eval-used + try: + override_paths, src_paths = self._check_override_paths() + + # To take an existing dst file out of spinoff management we need + # to do 3 things: + # - Add it to src_omit_paths to keep the src version from being + # synced in. + # - Add it to src_write_paths to ensure git has control over + # its location in dst. + # - Remove our dst entry for it to prevent spinoff from blowing + # it away when it sees the src entry no longer exists. + + if not os.path.exists(self._dst_config_path): + raise RuntimeError( + f"Config file not found: '{self._dst_config_path}'." + ) + with open(self._dst_config_path, encoding='utf-8') as infile: + config = infile.read() + + config = _add_config_list_entry(config, 'src_omit_paths', src_paths) + config = _add_config_list_entry( + config, 'src_write_paths', src_paths + ) + + # Ok, now we simply remove it from tracking while leaving the + # existing file in place. + for override_path in override_paths: + del self._dst_entities[override_path] + + with open(self._dst_config_path, 'w', encoding='utf-8') as outfile: + outfile.write(config) + + for override_path in override_paths: + print( + f"'{override_path}' overridden. It should now show" + ' up as untracked by git (you probably want to add it).' + ) + + except Exception as exc: + self._execution_error = True + print(f'{Clr.RED}Error{Clr.RST}: {exc}', file=sys.stderr) + + def _check_override_paths(self) -> tuple[set[str], set[str]]: + assert self._override_paths is not None + # Return the set of dst overridden paths and the src paths + # they came from. + src_paths = set[str]() + override_paths = set[str]() + for arg in self._override_paths: + override_path_full = os.path.abspath(arg) + if not override_path_full.startswith(self._dst_root): + raise CleanError( + f'Override-path {override_path_full} does not reside' + f' under dst ({self._dst_root}).' + ) + # TODO(ericf): generalize this now that we're no longer hard-coded + # to use submodules/ballistica. Should disallow any path under + # any submodule I suppose. + if override_path_full.startswith( + os.path.join(self._dst_root, 'submodules') + ): + raise RuntimeError('Path can not reside under submodules.') + override_path = override_path_full[len(self._dst_root) + 1 :] + if not os.path.exists(override_path_full): + raise RuntimeError(f"Path does not exist: '{override_path}'.") + + # For the time being we only support individual files here. + if not os.path.isfile(override_path_full): + raise RuntimeError( + f"path does not appear to be a file: '{override_path}'." + ) + + # Make sure this is a file we're tracking. + if override_path not in self._dst_entities: + raise RuntimeError( + f'Path does not appear to be' + f" tracked by spinoff: '{override_path}'." + ) + + # Disallow git-mirrored-paths. + # We would have to add special handling for this. + if _any_path_contains(self.git_mirrored_paths, override_path): + raise RuntimeError( + 'Not allowed to override special git-managed path:' + f" '{override_path}'." + ) + + src_path = self._dst_entities[override_path].src_path + assert src_path is not None + src_paths.add(src_path) + override_paths.add(override_path) + return override_paths, src_paths + + def _diff(self) -> None: + self._validate_final_lists() + self._handle_recache_entities() + + if os.system('which colordiff > /dev/null 2>&1') == 0: + display_diff_cmd = 'colordiff' + else: + print( + 'NOTE: for color-coded output, install "colordiff" via brew.', + file=sys.stderr, + ) + display_diff_cmd = 'diff' + + for src_path in sorted(self._src_copy_entities): + src_entity = self._src_entities[src_path] + dst_path = src_entity.dst + src_path_full = os.path.join(self._src_root, src_path) + dst_path_full = os.path.join(self._dst_root, dst_path) + try: + if src_entity.entity_type is EntityType.SYMLINK: + pass + + elif src_entity.entity_type is EntityType.FILE: + self._diff_file( + src_path, + src_path_full, + dst_path, + dst_path_full, + display_diff_cmd, + ) + else: + assert_never(src_entity.entity_type) + + except Exception as exc: + self._execution_error = True + print( + f"{Clr.RED}Error diffing file: '{src_path_full}'" + f'{Clr.RST}: {exc}', + file=sys.stderr, + ) + + def _diff_file( + self, + src_path: str, + src_path_full: str, + dst_path: str, + dst_path_full: str, + display_diff_cmd: str, + ) -> None: + if os.path.isfile(src_path_full) and os.path.isfile(dst_path_full): + # We want to show how this update would change the dst + # file, so we need to compare a filtered version of src + # to the existing dst. For non-filtered src files we + # can just do a direct compare + delete_file_name: str | None + if self._should_filter_src_file(src_path): + with tempfile.NamedTemporaryFile('wb', delete=False) as tmpf: + with open(src_path_full, 'rb') as infile: + encoding = self._encoding_for_file(src_path_full) + try: + contents_in = infile.read().decode(encoding) + except Exception: + print(f"Error decoding file: '{src_path}'.") + raise + contents_out = self._filter_file(src_path, contents_in) + tmpf.write(contents_out.encode(encoding)) + delete_file_name = tmpf.name + tmpf.close() + diff_src_path_full = delete_file_name + else: + diff_src_path_full = src_path_full + delete_file_name = None + result = os.system( + f'diff "{diff_src_path_full}" "{dst_path_full}"' + f' > /dev/null 2>&1' + ) + if result != 0: + print(f'\n{dst_path}:') + os.system( + f'{display_diff_cmd} "{dst_path_full}"' + f' "{diff_src_path_full}"' + ) + print('') + + if delete_file_name is not None: + os.remove(delete_file_name) + + def _is_project_file(self, path: str) -> bool: + if path.startswith('tools/') or path.startswith('src/external'): + return False + bname = os.path.basename(path) + return bname in self._project_file_names or any( + bname.endswith(s) for s in self._project_file_suffixes + ) + + def _update(self) -> None: + """Run a variation of the "update" command.""" + self._validate_final_lists() + self._handle_recache_entities() + + # Let's print individual updates only if there's few of them. + print_individual_updates = len(self._src_copy_entities) < 50 + + project_src_paths: list[str] = [] + + # Run all file updates except for project ones (Makefiles, etc.) + # Which we wait for until the end. + for src_path in sorted(self._src_copy_entities): + if self._is_project_file(src_path): + project_src_paths.append(src_path) + else: + self._handle_src_copy(src_path, print_individual_updates) + + # Now attempt to remove anything in our purge list. + removed_f_count = self._remove_purge_entities() + + # Update project files after all other copies and deletes are done. + # This is because these files take the state of the project on disk + # into account, so we need all files they're looking at to be final. + if project_src_paths: + from batools.project import ProjectUpdater + + assert self._project_updater is None + self._project_updater = ProjectUpdater( + self._dst_root, + check=False, + fix=False, + empty=True, + projname=self.default_filter_text(self._src_name), + ) + + # For project-updater to do its thing, we need to provide + # filtered source versions of *all* project files which + # might be changing. (Some project files may implicitly generate + # others as part of their own generation so we need all sources + # in place before any generation happens). + for src_path in project_src_paths: + self._handle_src_copy_project_updater_register(src_path) + + # Ok; everything is registered. Can now use the updater to + # filter dst versions of these. + self._project_updater.prepare_to_generate() + for src_path in project_src_paths: + self._handle_src_copy( + src_path, print_individual_updates, is_project_file=True + ) + + # Print some overall results. + if self._src_copy_entities: + print( + f'{len(self._src_copy_entities)} file(s) updated.', + file=sys.stderr, + ) + + if removed_f_count > 0: + print(f'{removed_f_count} file(s) removed.', file=sys.stderr) + + # If we didn't update any files or delete anything, say so. + if removed_f_count == 0 and not self._src_copy_entities: + print('Spinoff is up-to-date.', file=sys.stderr) + + def _handle_src_copy_project_updater_register(self, src_path: str) -> None: + src_entity = self._src_entities[src_path] + dst_path = src_entity.dst + src_path_full = os.path.join(self._src_root, src_path) + # dst_path_full = os.path.join(self._dst_root, dst_path) + + # Currently assuming these are filtered. + assert self._should_filter_src_file(src_path) + assert src_entity.entity_type is EntityType.FILE + encoding = self._encoding_for_file(src_path_full) + with open(src_path_full, 'rb') as infile: + try: + contents_in = infile.read().decode(encoding) + except Exception: + print(f"Error decoding file: '{src_path}'.") + raise + contents_out = self._filter_file(src_path, contents_in) + + # Take the filtered spinoff contents from src and plug that into + # the project updater as the 'current' version of the file. The + # updater will then update it based on the current state of the + # project. + assert self._project_updater is not None + self._project_updater.enqueue_update(dst_path, contents_out) + + def _handle_src_copy( + self, + src_path: str, + print_individual_updates: bool, + is_project_file: bool = False, + ) -> None: + src_entity = self._src_entities[src_path] + dst_path = src_entity.dst + src_path_full = os.path.join(self._src_root, src_path) + dst_path_full = os.path.join(self._dst_root, dst_path) + try: + # Create its containing dir if need be. + dirname = os.path.dirname(dst_path_full) + if not os.path.exists(dirname): + os.makedirs(dirname) + + mode = os.lstat(src_path_full).st_mode + + if src_entity.entity_type is EntityType.SYMLINK: + assert not is_project_file # Undefined. + linkto = os.readlink(src_path_full) + if os.path.islink(dst_path_full): + os.remove(dst_path_full) + os.symlink(linkto, dst_path_full) + dst_entity = DstEntity( + entity_type=src_entity.entity_type, + env_hash=None, + src_path=None, + src_mtime=None, + src_size=None, + dst_mtime=None, + dst_size=None, + ) + + elif src_entity.entity_type is EntityType.FILE: + dst_entity = self._handle_src_copy_file( + src_path, + src_path_full, + dst_path, + dst_path_full, + src_entity, + is_project_file, + ) + os.chmod(dst_path_full, mode) + else: + raise RuntimeError( + f"Invalid entity type: '{src_entity['type']}'." + ) + + # NOTE TO SELF - was using lchmod here but it doesn't exist + # on linux (apparently symlinks can't have perms modified). + # Now doing a chmod above only for the 'file' path. + # os.lchmod(dst_path_full, mode) + self._dst_entities[dst_path] = dst_entity + if print_individual_updates: + print( + f' updated: {Clr.GRN}{dst_path}{Clr.RST}', file=sys.stderr + ) + + except Exception as exc: + # Attempt to remove whatever we just put there so we avoid + # 'non-managed-file-found' errors in subsequent runs. + try: + if os.path.exists(dst_path_full): + os.unlink(dst_path_full) + except Exception as exc2: + print( + f'{Clr.RED}Error removing failed dst file: {exc2}{Clr.RST}' + ) + self._execution_error = True + print( + f'{Clr.RED}Error copying/filtering file:' + f" '{src_path_full}'{Clr.RST}: {exc}.", + file=sys.stderr, + ) + + def _handle_src_copy_file( + self, + src_path: str, + src_path_full: str, + dst_path: str, + dst_path_full: str, + src_entity: SrcEntity, + is_project_file: bool, + ) -> DstEntity: + # del dst_path # Unused. + + # If this is a project file, we already fed the filtered + # src into our ProjectUpdater instance, so all we do here is + # have the updater give us its output. + if is_project_file: + assert self._project_updater is not None + try: + pupdatedata = self._project_updater.generate_file(dst_path) + except Exception: + if bool(False): + print(f"ProjectUpdate error generating '{dst_path}'.") + import traceback + + traceback.print_exc() + raise + with open(dst_path_full, 'w', encoding='utf-8') as outfile: + outfile.write(pupdatedata) + else: + # Normal non-project file path. + if not self._should_filter_src_file(src_path): + with open(src_path_full, 'rb') as infile: + data = infile.read() + with open(dst_path_full, 'wb') as outfile: + outfile.write(data) + else: + with open(src_path_full, 'rb') as infile: + encoding = self._encoding_for_file(src_path_full) + try: + contents_in = infile.read().decode(encoding) + except Exception: + print(f"Error decoding file: '{src_path}'.") + raise + contents_out = self._filter_file(src_path, contents_in) + with open(dst_path_full, 'wb') as outfile: + outfile.write(contents_out.encode(encoding)) + + return DstEntity( + entity_type=src_entity.entity_type, + env_hash=self._envhash, + src_path=src_path, + src_mtime=os.path.getmtime(src_path_full), + src_size=os.path.getsize(src_path_full), + dst_mtime=os.path.getmtime(dst_path_full), + dst_size=os.path.getsize(dst_path_full), + ) + + def _remove_purge_entities(self) -> int: + removed_f_count = 0 + if self._dst_purge_entities: + for ent in sorted(self._dst_purge_entities): + dst_path_full = os.path.join(self._dst_root, ent) + try: + if os.path.isfile(dst_path_full) or os.path.islink( + dst_path_full + ): + os.remove(dst_path_full) + del self._dst_entities[ent] + removed_f_count += 1 + elif not os.path.exists(dst_path_full): + # It's already gone; no biggie. + del self._dst_entities[ent] + else: + print( + f"Anomaly removing file: '{dst_path_full}'.", + file=sys.stderr, + ) + except Exception: + self._execution_error = True + print( + (f"Error removing file: '{dst_path_full}'."), + file=sys.stderr, + ) + return removed_f_count + + def _clean_cruft(self) -> None: + """Clear out some known cruft-y files. + + Makes us more likely to be able to clear directories (.DS_Store, etc) + """ + + # Go through our list of dirs above files we've mapped to dst, + # cleaning out any 'cruft' files we find there. + assert self._dst_git_file_dirs is not None + for dstdir in self._dst_git_file_dirs: + dstdirfull = os.path.join(self._dst_root, dstdir) + if not os.path.isdir(dstdirfull): + continue + for fname in os.listdir(dstdirfull): + if fname in self.cruft_file_names: + cruftpath = os.path.join(dstdirfull, fname) + try: + os.remove(cruftpath) + except Exception: + print( + f"error removing cruft file: '{cruftpath}'.", + file=sys.stderr, + ) + self._remove_empty_folders(self._dst_root, False) + + def _check_spinoff_managed_dirs(self) -> None: + assert self._spinoff_managed_dirs is not None + # Spinoff-managed dirs are marked gitignore which means we are + # fully responsible for them. We thus want to be careful + # to avoid silently blowing away work that may have happened + # in one. So let's be rather strict about it and complain about + # any files we come across that aren't directly managed by us + # (or cruft). + dstrootsl = f'{self._dst_root}/' + for rdir in self._spinoff_managed_dirs: + for root, dirnames, fnames in os.walk( + os.path.join(self._dst_root, rdir), + topdown=True, + ): + # Completely ignore ignore-names in both dirs and files + # and cruft-file names in files. + for dirname in dirnames.copy(): + if dirname in self.ignore_names: + dirnames.remove(dirname) + for fname in fnames.copy(): + if ( + fname in self.ignore_names + or fname in self.cruft_file_names + ): + fnames.remove(fname) + + for fname in fnames: + dst_path_full = os.path.join(root, fname) + assert dst_path_full.startswith(dstrootsl) + dst_path = dst_path_full.removeprefix(dstrootsl) + + # If its not a mapped-in file from src and not + # covered by generated-paths or git-mirror-paths, + # complain. + if ( + dst_path not in self._dst_entities + and not _any_path_contains( + self._dst_unchecked_paths, dst_path + ) + and not _any_path_contains( + self.git_mirrored_paths, dst_path + ) + and not self._force + ): + self._dst_error_entities[dst_path] = ( + 'non-spinoff file in spinoff-managed dir;' + ' --force to ignore' + ) + + def _filter_src_git_file_list(self) -> None: + # Crate a filtered version of src git files based on our omit entries. + out = set[str]() + assert self._src_git_files is not None + for gitpath in self._src_git_files: + # If omit-path contians this one or any component is found + # in omit-names, pretend it doesn't exist. + if _any_path_contains(self._src_omit_paths_expanded, gitpath): + continue # Omitting + if any(name in gitpath.split('/') for name in self.ignore_names): + continue + out.add(gitpath) + self._src_git_files = out + + def _register_sync_operations(self) -> None: + assert self._src_git_files is not None + for src_path in self._src_git_files: + dst_path = self._filter_path(src_path) + + src_path_full = os.path.join(self._src_root, src_path) + dst_path_full = os.path.join(self._dst_root, dst_path) + + if os.path.islink(src_path_full): + self._do_copy_symlink( + src_path, src_path_full, dst_path, dst_path_full + ) + else: + assert os.path.isfile(src_path_full) + self._do_file_copy_and_filter( + src_path, src_path_full, dst_path, dst_path_full + ) + + def _do_copy_symlink( + self, + src_path: str, + src_path_full: str, + dst_path: str, + dst_path_full: str, + ) -> None: + self._src_entities[src_path] = SrcEntity( + entity_type=EntityType.SYMLINK, dst=dst_path + ) + if dst_path not in self._dst_entities: + self._src_copy_entities.add(src_path) + else: + dst_type = self._dst_entities[dst_path].entity_type + if dst_type is not EntityType.SYMLINK: + self._src_error_entities[ + src_path + ] = f'expected symlink; found {dst_type}' + else: + # Ok; looks like there's a symlink already there. + self._dst_entities_claimed.add(dst_path) + + # See if existing link is pointing to the right place & + # schedule a copy if not. + linkto = os.readlink(src_path_full) + if ( + not os.path.islink(dst_path_full) + or os.readlink(dst_path_full) != linkto + ): + self._src_copy_entities.add(src_path) + + def _do_file_copy_and_filter( + self, + src_path: str, + src_path_full: str, + dst_path: str, + dst_path_full: str, + ) -> None: + self._src_entities[src_path] = SrcEntity( + entity_type=EntityType.FILE, dst=dst_path + ) + if dst_path not in self._dst_entities: + # If we're unaware of dst, copy or error if something's + # there already (except for our git-managed files in which + # case we *expect* something to be there). + if ( + os.path.exists(dst_path_full) + and not _any_path_contains(self.git_mirrored_paths, src_path) + and not self._force + ): + self._src_error_entities[src_path] = ( + 'would overwrite non-spinoff file in dst;' + ' --force to override' + ) + else: + self._src_copy_entities.add(src_path) + else: + dst_type = self._dst_entities[dst_path].entity_type + if dst_type is not EntityType.FILE: + self._src_error_entities[ + src_path + ] = f'expected file; found {dst_type}' + else: + dst_exists = os.path.isfile(dst_path_full) + + # Ok; we know of a dst file and it seems to exist. If both + # src and dst data still lines up with our cache we can + # assume there's nothing to be done. + dst_entity = self._dst_entities[dst_path] + # pylint: disable=too-many-boolean-expressions + if ( + dst_exists + and dst_entity.env_hash == self._envhash + and os.path.getsize(dst_path_full) == dst_entity.dst_size + and os.path.getmtime(dst_path_full) == dst_entity.dst_mtime + and os.path.getsize(src_path_full) == dst_entity.src_size + and os.path.getmtime(src_path_full) == dst_entity.src_mtime + ): + pass + else: + # *Something* differs from our cache; we have work to do. + self._do_differing_file_copy_and_filter( + src_path, + src_path_full, + dst_path, + dst_path_full, + dst_entity, + dst_exists, + ) + + self._dst_entities_claimed.add(dst_path) + + def _do_differing_file_copy_and_filter( + self, + src_path: str, + src_path_full: str, + dst_path: str, + dst_path_full: str, + dst_entity: DstEntity, + dst_exists: bool, + ) -> None: + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + # pylint: disable=too-many-locals + + # Ok, *something* differs from our cache. Need to take a closer look. + if not dst_exists: + # With no dst gotta do a copy of course. + self._src_copy_entities.add(src_path) + return + + do_backport = False + + src_datab: bytes | None + dst_datab: bytes | None + src_data: str | None = None + dst_data: str | None = None + + # In strict mode we want it to always be an error if dst mod-time + # varies from the version we wrote (we want to track down anyone + # writing to our files who is not us). + # Note that we need to ignore git-mirrored-paths because git might + # be mucking with modtimes itself. + if ( + self.strict + and not self._force + and os.path.getmtime(dst_path_full) != dst_entity.dst_mtime + and not _any_path_contains(self.git_mirrored_paths, src_path) + ): + # Try to include when the dst file got modified in + # case its helpful. + sincestr = ( + '' + if dst_entity.dst_mtime is None + else ( + ' ' + + timedelta_str( + os.path.getmtime(dst_path_full) - dst_entity.dst_mtime, + maxparts=1, + decimals=2, + ) + ) + ) + self._src_error_entities[src_path] = ( + f'[STRICT] spinoff-managed file modified{sincestr}' + f' after spinoff wrote it;' + f' --force to overwrite from src' + ) + return + + # Let's filter the src file and if it matches dst we can just + # re-grab our cache info and call it a day. + if self._should_filter_src_file(src_path): + encoding = self._encoding_for_file(src_path_full) + with open(src_path_full, 'rb') as infile: + try: + src_data = self._filter_file( + src_path, infile.read().decode(encoding) + ) + except Exception: + print(f"Error decoding/filtering file: '{src_path}'.") + raise + with open(dst_path_full, 'rb') as infile: + try: + dst_data = infile.read().decode(encoding) + except Exception: + print(f"Error decoding file: '{dst_path}'.") + raise + results_are_same = src_data == dst_data + + # Bytes versions are only used very rarely by 'backport' + # command so let's lazy compute them here. + src_datab = dst_datab = None + else: + # Ok our src isn't filtered; can be a bit more streamlined. + with open(src_path_full, 'rb') as infile: + src_datab = infile.read() + with open(dst_path_full, 'rb') as infile: + dst_datab = infile.read() + results_are_same = src_datab == dst_datab + + # No string versions needed in this case. + src_data = dst_data = None + + if results_are_same: + # Things match; just update the times we've got recorded + # for these fellas. + self._src_recache_entities.add(src_path) + else: + # Ok, something legit changed. + if (os.path.getsize(dst_path_full) == dst_entity.dst_size) and ( + os.path.getmtime(dst_path_full) == dst_entity.dst_mtime + ): + # If it looks like dst did not change, we can go + # through with a standard update. + self._src_copy_entities.add(src_path) + elif _any_path_contains(self.git_mirrored_paths, src_path): + # Ok, dst changed but it is managed by git so this + # happens (switching git branches or whatever else...) + # in this case we just blindly replace it; no erroring. + self._src_copy_entities.add(src_path) + elif self._force: + # If the user is forcing the issue, do the overwrite. + self._src_copy_entities.add(src_path) + elif (os.path.getsize(src_path_full) == dst_entity.src_size) and ( + os.path.getmtime(src_path_full) == dst_entity.src_mtime + ): + # Ok, dst changed but src did not. This is an error. + + # Try to include when the dst file got modified in + # case its helpful. + sincestr = ( + '' + if dst_entity.dst_mtime is None + else ( + ' ' + + timedelta_str( + os.path.getmtime(dst_path_full) + - dst_entity.dst_mtime, + maxparts=1, + decimals=2, + ) + ) + ) + self._src_error_entities[src_path] = ( + f'spinoff-managed file modified{sincestr}' + f' after spinoff wrote it;' + f' --force to overwrite from src' + ) + + # Allow backport process here to correct this. + if self._mode is self.Mode.BACKPORT and ( + self._backport_file == dst_path + or self._backport_file is None + ): + do_backport = True + else: + # Ok, *nothing* matches (file contents don't match + # and both modtimes differ from cached ones). + # User needs to sort this mess out. + self._src_error_entities[src_path] = ( + 'src AND spinoff-managed file modified;' + ' --force to overwrite from src' + ) + + # Allow backport process here to correct this. + if self._mode is self.Mode.BACKPORT and ( + self._backport_file == dst_path + or self._backport_file is None + ): + do_backport = True + + if do_backport: + # Lazy compute string version if needed. + if src_data is None: + assert src_datab is not None + src_data = src_datab.decode() + if dst_data is None: + assert dst_datab is not None + dst_data = dst_datab.decode() + self._backport(src_path, dst_path, src_data, dst_data) + + def _backport( + self, src_path: str, dst_path: str, src_data: str, dst_data: str + ) -> None: + is_filtered = self._should_filter_src_file(src_path) + full_src_path = os.path.join(self._src_root, src_path) + + # If we're doing auto-backport, just do the thing (when we can) + # and keep on going. + if self._auto_backport: + if is_filtered: + print( + f"{Clr.YLW}Can't auto-backport filtered file:{Clr.RST}" + f' {Clr.BLD}{dst_path}{Clr.RST}' + ) + self._auto_backport_fail_count += 1 + else: + src_path_full = os.path.join(self._src_root, src_path) + dst_path_full = os.path.join(self._dst_root, dst_path) + assert os.path.isfile(src_path_full) + assert os.path.isfile(dst_path_full) + subprocess.run(['cp', dst_path_full, src_path_full], check=True) + print( + f'{Clr.BLU}Auto-backporting{Clr.RST}' + f' {Clr.BLD}{dst_path}{Clr.RST}' + ) + self._auto_backport_success_count += 1 + return + + # Ok NOT auto-backporting; we'll show a diff and stop after the + # first file. + + # If this isn't a filtered file, it makes things easier. + if not is_filtered: + print( + f'Backporting {Clr.BLD}{dst_path}{Clr.RST}:\n' + f'{Clr.GRN}This file is NOT filtered so backporting' + f' is simple.{Clr.RST}\n' + f'{Clr.BLU}{Clr.BLD}LEFT:{Clr.RST}' + f' src file\n' + f'{Clr.BLU}{Clr.BLD}RIGHT:{Clr.RST} dst file\n' + f'{Clr.BLU}{Clr.BLD}YOUR MISSION:{Clr.RST}' + f' move changes from dst back to src.\n' + f"{Clr.CYN}Or pass '--auto' to the backport subcommand" + f' to do this for you.{Clr.RST}' + ) + subprocess.run( + [ + 'opendiff', + os.path.join(self._src_root, src_path), + os.path.join(self._dst_root, dst_path), + ], + check=True, + capture_output=True, + ) + + else: + # It IS filtered. + + print( + f'Backporting {Clr.BLD}{dst_path}{Clr.RST}:\n' + f'{Clr.YLW}This file is filtered which complicates' + f' backporting a bit.{Clr.RST}\n' + f'{Clr.BLU}{Clr.BLD}LEFT:{Clr.RST}' + f' {Clr.CYN}{Clr.BLD}FILTERED{Clr.RST}' + ' src file\n' + f'{Clr.BLU}{Clr.BLD}RIGHT:{Clr.RST} dst file\n' + f'{Clr.BLU}{Clr.BLD}YOUR MISSION:{Clr.RST}' + f' modify {Clr.CYN}{Clr.BLD}ORIGINAL{Clr.RST}' + f' src file such that filtered src matches dst:\n' + f'{Clr.BLD}{full_src_path}{Clr.RST}' + ) + with tempfile.TemporaryDirectory() as tempdir: + srcname = os.path.basename(src_path) + dstname = os.path.basename(dst_path) + tsrcpath = os.path.join(tempdir, f'FILTERED-PARENT({srcname})') + tdstpath = os.path.join(tempdir, f'SPINOFF({dstname})') + with open(tsrcpath, 'w', encoding='utf-8') as outfile: + outfile.write(src_data) + with open(tdstpath, 'w', encoding='utf-8') as outfile: + outfile.write(dst_data) + subprocess.run( + ['opendiff', tsrcpath, tdstpath], + check=True, + capture_output=True, + ) + + # Bow out after this one single file. Otherwise we wind up showing + # all errors (one of which we might have just fixed) which is + # misleading. + raise self.BackportInProgressError() + + def _filter_paths(self, paths: Iterable[str]) -> set[str]: + return set(self._filter_path(p) for p in paths) + + +def _any_path_contains(paths: Iterable[str], path: str) -> bool: + assert not path.endswith('/') + + for tpath in paths: + # Use simple logic if there's no special chars used by fnmatch. + if not any(char in tpath for char in ('*', '?', '[')): + if tpath == path or path.startswith(f'{tpath}/'): + return True + else: + # Bust out the fancy logic. + # Split both paths into segments ('a/b/c' -> ['a','b','c']) + # and compare each using fnmatch. If all segments + # from tpath match corresponding ones in path then tpath + # is a parent. + pathsegs = path.split('/') + tpathsegs = tpath.split('/') + if len(tpathsegs) > len(pathsegs): + continue # tpath is deeper than path; can't contain it. + all_matches = True + for i in range(len(tpathsegs)): # pylint: disable=C0200 + seg_matches = fnmatch.fnmatchcase(pathsegs[i], tpathsegs[i]) + if not seg_matches: + all_matches = False + break + if all_matches: + return True + return False + + +def _get_dir_levels(dirpath: str) -> list[str]: + """For 'a/b/c' return ['a', 'a/b', 'a/b/c'].""" + splits = dirpath.split('/') + return ['/'.join(splits[: (i + 1)]) for i in range(len(splits))] + + +def _add_config_list_entry( + config: str, list_name: str, add_paths: set[str] +) -> str: + # pylint: disable=eval-used + splits = config.split(f'{list_name}: list[str] = [') + if len(splits) != 2: + raise RuntimeError('Parse error.') + splits2 = splits[1].split(']') + paths = eval(f'[{splits2[0]}]') + assert isinstance(paths, list) + for add_path in add_paths: + if add_path in paths: + raise RuntimeError( + f'Path already present in {list_name} in spinoffconfig:' + f" '{add_path}'." + ) + paths.append(add_path) + + config = ( + splits[0] + + f'{list_name}: list[str] = [\n' + + ''.join([f' {repr(p)},\n' for p in sorted(paths)]) + + ']'.join([''] + splits2[1:]) + ) + return config diff --git a/tools/batools/spinoff/_main.py b/tools/batools/spinoff/_main.py new file mode 100644 index 00000000..8d8dca0e --- /dev/null +++ b/tools/batools/spinoff/_main.py @@ -0,0 +1,338 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Spinoff system for spawning child projects from a ballistica project.""" + +from __future__ import annotations + +import os +import sys +import subprocess +from enum import Enum +from typing import assert_never + +from efro.error import CleanError +from efro.terminal import Clr +from efrotools import replace_exact +import batools.spinoff +from batools.spinoff._context import SpinoffContext + + +class Command(Enum): + """Our top level commands.""" + + STATUS = 'status' + UPDATE = 'update' + CHECK = 'check' + CLEAN_LIST = 'cleanlist' + CLEAN = 'clean' + CLEAN_CHECK = 'cleancheck' + OVERRIDE = 'override' + DIFF = 'diff' + BACKPORT = 'backport' + FEATURESETS = 'featuresets' + CREATE = 'create' + + +def spinoff_main() -> None: + """Main script entry point.""" + try: + _main() + except CleanError as exc: + exc.pretty_print() + sys.exit(1) + + +def _main() -> None: + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + if len(sys.argv) < 2: + print(f'{Clr.RED}Error: Expected a command argument.{Clr.RST}') + _print_available_commands() + raise CleanError() + + try: + cmd = Command(sys.argv[1]) + except ValueError: + print(f"{Clr.RED}Error: Invalid command '{sys.argv[1]}'.{Clr.RST}") + _print_available_commands() + return + + dst_root = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '..')) + + # Determine our src project based on our tools/spinoff symlink. + # If its not a link it means we ARE a src project. + dst_spinoff_path = os.path.join(dst_root, 'tools', 'spinoff') + if os.path.islink(dst_spinoff_path): + src_root = os.path.abspath( + os.path.join( + os.path.dirname(os.path.realpath(dst_spinoff_path)), '..' + ) + ) + else: + src_root = None + + # By default we assume our src project is a git submodule at + # submodules/ballistica, but this can be overridden to an arbitrary + # directory via the project localconfig. + # src_proj_val = getlocalconfig(Path(dst_root)).get('spinoff_src') + # if isinstance(src_proj_val, str): + # src_root = src_proj_val + # else: + # src_root = os.path.join(dst_root, 'submodules', 'ballistica') + + single_run_mode: SpinoffContext.Mode | None = None + + if cmd is Command.STATUS: + single_run_mode = SpinoffContext.Mode.STATUS + elif cmd is Command.UPDATE: + single_run_mode = SpinoffContext.Mode.UPDATE + elif cmd is Command.CHECK: + single_run_mode = SpinoffContext.Mode.CHECK + elif cmd is Command.CLEAN_LIST: + single_run_mode = SpinoffContext.Mode.CLEAN_LIST + elif cmd is Command.CLEAN: + single_run_mode = SpinoffContext.Mode.CLEAN + elif cmd is Command.CLEAN_CHECK: + single_run_mode = SpinoffContext.Mode.CLEAN_CHECK + elif cmd is Command.DIFF: + single_run_mode = SpinoffContext.Mode.DIFF + elif cmd is Command.OVERRIDE: + _do_override(src_root, dst_root) + elif cmd is Command.BACKPORT: + _do_backport(src_root, dst_root) + elif cmd is Command.FEATURESETS: + _do_featuresets(dst_root) + elif cmd is Command.CREATE: + _do_create(src_root, dst_root) + else: + assert_never(cmd) + + if single_run_mode is not None: + if src_root is None: + if '--soft' in sys.argv: + return + raise CleanError( + 'This only works on dst projects;' + ' you appear to be in a src project.' + " To silently no-op in this case, pass '--soft'." + ) + # SpinoffContext should never be relying on relative paths, so let's + # keep ourself honest by making sure. + os.chdir('/') + SpinoffContext( + src_root, + dst_root, + single_run_mode, + force='--force' in sys.argv, + print_full_lists='--full' in sys.argv, + ).run() + + +def _do_create(src_root: str | None, dst_root: str) -> None: + from efrotools.code import format_python_str + + if src_root is not None: + raise CleanError('This only works on src projects.') + + args = sys.argv[2:] + args2: list[str] = [] + i = 0 + + featuresets: set[str] | None = None + + while i < len(args): + if args[i] == '--featuresets': + if i >= len(args) - 1: + raise CleanError('--featuresets must be followed by an arg.') + fsarg = args[i + 1] + if fsarg in {'', 'none'}: + featuresets = set() + else: + featuresets = set(fsarg.split(',')) + i += 2 + else: + args2.append(args[i]) + i += 1 + + if len(args2) != 2: + raise CleanError(f'Expected a name and path arg; got {args2}.') + + name, path = args2 # pylint: disable=unbalanced-tuple-unpacking + + if not name: + raise CleanError('Name cannot be an empty string.') + if not name[0].isupper(): + raise CleanError('Name must start with a capital letter.') + + if os.path.exists(path): + raise CleanError(f"Target path '{path}' already exists.") + + # The components we need for a spinoff dst project are: + # - a tools/spinoff symlink pointing to our src project's tools/spinoff + # - a config/spinoffconfig.py + + subprocess.run(['mkdir', '-p', path], check=True) + subprocess.run(['mkdir', os.path.join(path, 'tools')], check=True) + subprocess.run(['mkdir', os.path.join(path, 'config')], check=True) + + subprocess.run( + [ + 'ln', + '-s', + os.path.join(dst_root, 'tools', 'spinoff'), + os.path.join(path, 'tools'), + ], + check=True, + ) + + # Read in the dummy module we use as a template. + template_path = os.path.join( + os.path.dirname(batools.spinoff.__file__), '_config_template.py' + ) + with open(template_path, encoding='utf-8') as infile: + template = infile.read() + template = replace_exact( + template, + '\n# A TEMPLATE CONFIG FOR CREATED SPINOFF DST PROJECTS.\n' + '# THIS IS NOT USED AT RUNTIME;' + ' IT ONLY EXISTS FOR TYPE-CHECKING PURPOSES.\n', + '', + ) + template = replace_exact(template, 'SPINOFF_TEMPLATE_NAME', name) + + template = replace_exact( + template, + '# __SRC_FEATURE_SETS__', + format_python_str(f'ctx.src_feature_sets = {featuresets!r}'), + ) + + with open( + os.path.join(path, 'config', 'spinoffconfig.py'), 'w', encoding='utf-8' + ) as outfile: + outfile.write(template) + + print( + f'{Clr.GRN}{Clr.BLD}Spinoff dst project created at' + f' {Clr.RST}{Clr.BLD}{path}{Clr.RST}{Clr.GRN}.{Clr.RST}\n\n' + 'Next, from dst project root, do:\n' + f' {Clr.BLD}{Clr.MAG}./tools/spinoff update{Clr.RST} ' + '- Syncs src project into dst.\n' + f' {Clr.BLD}{Clr.MAG}make update-check{Clr.RST} ' + '- Makes sure all project files are up-to-date (they should be).\n' + ' At that point you should have a functional dst project.\n' + ) + + +def _do_featuresets(dst_root: str) -> None: + from batools.featureset import FeatureSet + + featuresets = FeatureSet.get_all_for_project(dst_root) + print( + f'{Clr.BLD}{len(featuresets)}' + f' feature-sets present in this project:{Clr.RST}' + ) + for fset in featuresets: + print(f' {Clr.BLU}{fset.name}{Clr.RST}') + + +def _do_override(src_root: str | None, dst_root: str) -> None: + if src_root is None: + raise CleanError('This only works on dst projects.') + override_paths = [os.path.abspath(p) for p in sys.argv[2:]] + if not override_paths: + raise RuntimeError('Expected at least one path arg.') + + # SpinoffContext should never be relying on relative paths, so let's + # keep ourself honest by making sure. + os.chdir('/') + + # Do an initial update to make sure everything in the project is kosher. + # We expect to have a full set of src/dst entities/etc. + print(f'{Clr.BLU}Bringing project up-to-date before override...{Clr.RST}') + SpinoffContext(src_root, dst_root, SpinoffContext.Mode.UPDATE).run() + + # Now, in another pass, add filters to the spinoff config to ignore + # the overridden files and also purge them from the spinoff dst + # state cache so that they don't get blown away the next time we run + # spinoff update. + print(f'{Clr.BLU}Adding overrides...{Clr.RST}') + SpinoffContext( + src_root, + dst_root, + SpinoffContext.Mode.OVERRIDE, + override_paths=override_paths, + ).run() + + # Do one more update which will actually update our spinoff-managed dirs + # (and related things such as .gitignore) based on the changes we made in + # the OVERRIDE mode run. + print(f'{Clr.BLU}Updating state for config changes...{Clr.RST}') + SpinoffContext(src_root, dst_root, SpinoffContext.Mode.UPDATE).run() + + +def _do_backport(src_root: str | None, dst_root: str) -> None: + if src_root is None: + raise CleanError('This only works on dst projects.') + args = sys.argv[2:] + auto = '--auto' in args + args = [a for a in args if a != '--auto'] + + if len(args) not in {0, 1}: + raise CleanError('Expected zero or one file arg.') + + # None means 'backport first thing in list'. + backport_file = args[0] if args else None + + # SpinoffContext should never be relying on relative paths, so let's + # keep ourself honest by making sure. + os.chdir('/') + try: + SpinoffContext( + src_root, + dst_root, + SpinoffContext.Mode.BACKPORT, + backport_file=backport_file, + auto_backport=auto, + ).run() + except SpinoffContext.BackportInProgressError: + # We expect this to break us out of processing during backports. + pass + + +def _print_available_commands() -> None: + bgn = Clr.SBLU + end = Clr.RST + print( + ( + 'Available commands:\n' + f' {bgn}status{end} ' + 'Print list of files update would affect.\n' + f' {bgn}diff{end} ' + 'Print diffs for what update would do.\n' + f' {bgn}update{end} ' + 'Sync all spinoff files from src project.\n' + f' {bgn}check{end} ' + 'Make sure everything is kosher.\n' + f' {bgn}clean{end} ' + 'Remove all spinoff files' + ' (minus a few such as .gitignore).\n' + f' {bgn}cleanlist{end} ' + 'Shows what clean would do.\n' + f' {bgn}override [file...]{end} ' + 'Remove files from spinoff, leaving local copies in place.\n' + f' {bgn}backport [file]{end} ' + 'Help get changes to spinoff dst files back to src.\n' + f' {bgn}featuresets{end} ' + 'List featuresets present in the current project.\n' + f' {bgn}create [name, path]{end} ' + 'Create a new spinoff project based on this src one.\n' + ' Name should be in CamelCase form.\n' + ' Use --featuresets a,b to specify included' + ' feature-sets.\n' + " Pass 'none' or an empty string for no" + ' featuresets.\n' + ' If unspecified, all src feature-sets will be' + ' included.' + ), + file=sys.stderr, + ) diff --git a/tools/batools/spinoff/_state.py b/tools/batools/spinoff/_state.py new file mode 100644 index 00000000..45cf8ad3 --- /dev/null +++ b/tools/batools/spinoff/_state.py @@ -0,0 +1,68 @@ +# Released under the MIT License. See LICENSE for details. +# +"""State data for spinoff.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Annotated +from enum import Enum +from dataclasses import dataclass + +from efro.dataclassio import ( + ioprepped, + IOAttrs, + dataclass_from_json, + dataclass_to_json, +) + +if TYPE_CHECKING: + pass + + +class EntityType(Enum): + """Type of something in the dst project.""" + + FILE = 'f' + SYMLINK = 's' + + +@ioprepped +@dataclass +class SrcEntity: + """Data for a src entity.""" + + entity_type: Annotated[EntityType, IOAttrs('t')] + dst: Annotated[str, IOAttrs('d')] + + +@ioprepped +@dataclass +class DstEntity: + """Data for something in the dst project.""" + + entity_type: Annotated[EntityType, IOAttrs('t')] + env_hash: Annotated[str | None, IOAttrs('e')] + src_path: Annotated[str | None, IOAttrs('sp')] + src_mtime: Annotated[float | None, IOAttrs('sm')] + src_size: Annotated[int | None, IOAttrs('ss')] + dst_mtime: Annotated[float | None, IOAttrs('dm')] + dst_size: Annotated[int | None, IOAttrs('ds')] + + +@ioprepped +@dataclass +class DstEntitySet: + """All entities for a project.""" + + entities: Annotated[dict[str, DstEntity], IOAttrs('e')] + + @classmethod + def read_from_file(cls, path: str) -> DstEntitySet: + """Load from a file.""" + with open(path, 'r', encoding='utf-8') as infile: + return dataclass_from_json(cls, infile.read()) + + def write_to_file(self, path: str) -> None: + """Save to a file.""" + with open(path, 'w', encoding='utf-8') as outfile: + outfile.write(dataclass_to_json(self)) diff --git a/tools/batools/toplevelmakefile.py b/tools/batools/toplevelmakefile.py new file mode 100755 index 00000000..dcf0a77b --- /dev/null +++ b/tools/batools/toplevelmakefile.py @@ -0,0 +1,98 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Updates top level Makefile based on project elements present.""" + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING +import subprocess + +if TYPE_CHECKING: + pass + + +def generate_top_level_makefile(projroot: str, existing_data: str) -> str: + """Main script entry point.""" + from efrotools import getconfig + from pathlib import Path + + public = getconfig(Path(projroot))['public'] + assert isinstance(public, bool) + + original = existing_data + lines = original.splitlines() + + auto_start = lines.index('# __AUTOGENERATED_DUMMY_MODULES_BEGIN__') + auto_end = lines.index('# __AUTOGENERATED_DUMMY_MODULES_END__') + + our_lines = [ + _get_dummy_module_target(projroot), + ] + + filtered = lines[: auto_start + 1] + our_lines + lines[auto_end:] + out = '\n'.join(filtered) + '\n' + + return out + + +def _get_dummy_module_target(projroot: str) -> str: + lines = ( + subprocess.run( + [ + 'find', + os.path.join(projroot, 'src/ballistica'), + '-name', + 'python_*', + ], + check=True, + capture_output=True, + ) + .stdout.decode() + .splitlines() + ) + targets: list[str] = [] + for line in lines: + fname = os.path.split(line)[1] + if ( + fname.startswith('python_class_') + or fname.startswith('python_methods_') + ) and (fname.endswith('.cc')): + assert ' ' not in line + assert line.startswith(projroot + '/') + targets.append(line.removeprefix(projroot + '/')) + + # Keep our results deterministic. + targets.sort() + + # Also require a built binary and compiled scripts for it to use. + # UPDATE - scratch that. Now just trying to make it clear that + # dummy-modules should not be generated as part of regular dependency + # setups but rather in a dedicated pass. + # targets.append('assets-cmake-scripts') + # targets.append('cmake-binary') + + # Let's just use a single file for dependency tracking and regenerate + # all dummy modules when it is dirty. There's basically no speed difference + # regenerating all dummy-modules vs a single one so this keeps things + # simple. It also lets us blow away any orphaned modules. + dmstatepath = 'build/dummymodules/.dummy_modules_state' + dmstatedir = os.path.dirname(dmstatepath) + out = ( + '\n# Update dummy Python modules when source files contributing to' + ' them change.\n' + f'{dmstatepath}: \\\n' + ) + assert targets + out += ' \\\n'.join(f' {target}' for target in targets) + + assert ' ' not in dmstatedir + out += ( + '\n' + f'\t@tools/pcommand with_build_lock gen_dummy_modules_lock \\\n' + f' rm -rf {dmstatedir} \\&\\& mkdir -p {dmstatedir} \\\n' + ' \\&\\& ./tools/pcommand gen_dummy_modules \\\n' + f' \\&\\& touch {dmstatepath}' + ) + + return out diff --git a/tools/batools/version.py b/tools/batools/version.py index 0d608cf4..8de4babd 100755 --- a/tools/batools/version.py +++ b/tools/batools/version.py @@ -1,14 +1,12 @@ # Released under the MIT License. See LICENSE for details. # -"""Util to get ballisticacore versions.""" +"""Util to get ballisticakit versions.""" from __future__ import annotations import os from enum import Enum -from typing import TYPE_CHECKING - -from typing_extensions import assert_never +from typing import TYPE_CHECKING, assert_never from efro.error import CleanError @@ -46,28 +44,34 @@ def get_current_version() -> tuple[str, int]: """Pull current version and build_number from the project.""" version = None build_number = None - with open('src/ballistica/ballistica.cc', encoding='utf-8') as infile: + with open( + 'src/ballistica/shared/ballistica.cc', encoding='utf-8' + ) as infile: lines = infile.readlines() for line in lines: - if line.startswith('const char* kAppVersion = "'): + prefix = 'const char* kEngineVersion = "' + suffix = '";\n' + if line.startswith(prefix) and line.endswith(suffix): if version is not None: - raise Exception('found multiple version lines') - version = line[27:-3] - if line.startswith('const int kAppBuildNumber = '): + raise RuntimeError('Found multiple version lines.') + version = line.removeprefix(prefix).removesuffix(suffix) + prefix = 'const int kEngineBuildNumber = ' + suffix = ';\n' + if line.startswith(prefix) and line.endswith(suffix): if build_number is not None: - raise Exception('found multiple build number lines') - build_number = int(line[28:-2]) + raise RuntimeError('Found multiple build number lines.') + build_number = int(line.removeprefix(prefix).removesuffix(suffix)) if version is None: - raise Exception('version not found') + raise RuntimeError('Version not found.') if build_number is None: - raise Exception('build number not found') + raise RuntimeError('Build number not found.') return version, build_number def get_current_api_version() -> int: """Pull current api version from the project.""" with open( - 'assets/src/ba_data/python/ba/_meta.py', encoding='utf-8' + 'src/assets/ba_data/python/babase/_meta.py', encoding='utf-8' ) as infile: lines = infile.readlines() linestart = 'CURRENT_API_VERSION = ' diff --git a/tools/batools/xcodeproject.py b/tools/batools/xcodeproject.py new file mode 100644 index 00000000..c0290650 --- /dev/null +++ b/tools/batools/xcodeproject.py @@ -0,0 +1,428 @@ +# Released under the MIT License. See LICENSE for details. +# +"""XCode related functionality.""" +from __future__ import annotations + +import os +import hashlib +from typing import TYPE_CHECKING + +import openstep_parser as osp +from pbxproj import XcodeProject +from pbxproj.pbxextensions import TreeType, PBXGroup + +# Need to patch XcodeProject slightly to support .cc files. +xcft = XcodeProject._FILE_TYPES # pylint: disable=protected-access +if '.cc' not in xcft: + xcft['.cc'] = xcft['.cpp'] + +# Normally header files are added to a copy-headers build phase; +# we don't want that (its only really relevant for frameworks and +# its something else we'd need to worry about restoring uuids for). +xcft['.h'] = (xcft['.h'][0], None) + +if TYPE_CHECKING: + from typing import Any + + +def update_xcode_project( + projroot: str, + path: str, + existing_data: str, + all_source_files: list[str], + projname: str, + force: bool = False, +) -> str: + """Given an xcode project, update it for the current set of files.""" + + pbasename = os.path.basename(path) + if pbasename.endswith('-mac.xcodeproj') or pbasename.endswith( + '-ios.xcodeproj' + ): + suffixes = ['.cc', '.h', '.m', '.mm'] + updater = Updater( + projroot, + path, + existing_data, + sorted( + p + for p in all_source_files + if os.path.splitext(p)[1] in suffixes + ), + has_app_delegate_mm=True, + projname=projname, + ) + else: + suffixes = ['.cc', '.h', '.m', '.mm', '.swift'] + updater = Updater( + projroot, + path, + existing_data, + sorted( + p + for p in all_source_files + if os.path.splitext(p)[1] in suffixes + ), + has_app_delegate_mm=True, + projname=projname, + ) + + return updater.run(force=force) + + +class Updater: + """Does the thing.""" + + project: Any + + def __init__( + self, + projroot: str, + path: str, + existing_data: str, + sources: list[str], + projname: str, + has_app_delegate_mm: bool = False, + ) -> None: + if not path.endswith('.xcodeproj'): + raise RuntimeError(f"Path does not end in .xcodeproj: '{path}'.") + + self.projroot = projroot + self.path = path + self.existing_data = existing_data + self.sources = sources + self.project = None + self.has_app_delegate_mm = has_app_delegate_mm + + # Project name variations. + self.pnameu = projname + self.pnamel = projname.lower() + + # uuids associated with a given file path + # (grp/file obj and possibly build-files) + self.old_path_uuids: dict[str, list[str]] = {} + self.new_path_uuids: dict[str, list[str]] = {} + + self.print_test = False + + def run(self, force: bool = False) -> str: + """Do the thing.""" + # pylint: disable=too-many-locals + + projpath = os.path.join(self.projroot, self.path, 'project.pbxproj') + + # Make a hash out of all paths we'd add combined with the incoming + # state of this project. If this calced hash matches the current + # on-disk hash, we can assume our output would match the input + # and just deliver the input. + projsrc = self.existing_data + + fhash = self.hash_inputs( + self.sources, include_us=True, project_source=projsrc + ) + + # WARNING - this cache naming convention assumes all project + # basenames are unique regardless of dir (currently true but + # maybe won't always be). + _dirname, basename = os.path.split(self.path) + basebasename = os.path.splitext(basename)[0] + cachedir = os.path.join(self.projroot, '.cache') + os.makedirs(cachedir, exist_ok=True) + hashpath = os.path.join(cachedir, f'xcode_src_hash_{basebasename}') + + if os.path.exists(hashpath): + with open(hashpath, encoding='utf-8') as infile: + currenthash = infile.read() + else: + currenthash = None + + # If hash still matches, return incoming project state. + if currenthash == fhash and not force: + # with open(projpath, encoding='utf-8') as infile: + # existing_file = infile.read() + # FIXME: Its weird to be feeding our hash file back out to + # the project system; we should probably just manage it + # completely internally. + return self.existing_data + + tree = osp.OpenStepDecoder.ParseFromString(self.existing_data) + self.project = XcodeProject(tree, projpath) + # self.project = XcodeProject.load(projpath) + + bgrp = self._get_unique_group('ballistica') + assert isinstance(bgrp.get_id(), str) + + # Store uuids for all existing paths under ballistica and then + # blow it away. When we're done rebuilding ballistica we'll + # restore uuids on any paths that got remade. This keeps changes + # to the pbxproj file much more minimal which is good for git, + # and is simpler to accomplish than doing more selective + # adds/removes would be. + self._store_path_uuids(bgrp.get_id(), self.old_path_uuids, '') + self.project.remove_group_by_id(bgrp.get_id()) + + srcgrp = self._get_unique_group('Source') + self.add_paths(srcgrp) + + if self.has_app_delegate_mm: + self.mod_app_delegate_mm() + + # Groups we made should be sorted already since we sorted while + # building them, but let's sort the top level group we placed + # our stuff *into*. + srcgrp.children.sort( + key=lambda c: self.project.objects[c].get_name().lower() + ) + + # Now store uuids for the new stuff we made. + bgrp = self._get_unique_group('ballistica') + assert isinstance(bgrp.get_id(), str) + self._store_path_uuids(bgrp.get_id(), self.new_path_uuids, '') + + # Now filter the raw project file to replace new uuids with old + # ones when possible. + self._filter_uuids(projpath) + + # Now go through and, for every object with a path equal to its + # name, kill the name. This seems to match what xcode does so our + # project structure stays more similar to theirs. + bgrp = self._get_unique_group('ballistica') + assert isinstance(bgrp.get_id(), str) + self._trim_names(bgrp.get_id(), '') + + projsrc = repr(self.project) + '\n' + + # A few hacky last tweaks on the final project source to + # get ours matching xcode's 100% when possible. + projsrc = projsrc.replace( + '/* Build configuration list for PBXProject' + f' "{self.pnameu} macOS Legacy" */', + '/* Build configuration list for PBXProject' + f' "{self.pnamel}-mac" */', + ) + + # The hash we generated above used the project as it exists on disk + # for checking purposes, so for the one we return we need to + # regenerate it here to use the project source we just created. + fhash = self.hash_inputs( + self.sources, include_us=True, project_source=projsrc + ) + # Store the new hash. + if fhash != currenthash: + with open(hashpath, 'w', encoding='utf-8') as outfile: + outfile.write(fhash) + + return projsrc + + def _target_name_for_buildfile(self, buildfile: Any) -> str: + for target in self.project.objects.get_targets(): + for build_phase_id in target.buildPhases: + build_phase = self.project.objects[build_phase_id] + if build_phase.isa == 'PBXSourcesBuildPhase': + if buildfile.get_id() in build_phase.files: + assert isinstance(target.name, str) + return target.name + + raise RuntimeError('Could not deduce target name from build file.') + + def _filter_uuids(self, projpath: str) -> None: + projtxt = repr(self.project) + '\n' + + # For any path that used to exist in our project, if we + # find a new uuid for it, replace it with the old one. + for oldpath, olduuids in self.old_path_uuids.items(): + newuuids = self.new_path_uuids.get(oldpath) + if newuuids is not None: + if len(olduuids) != len(newuuids): + print( + f'uuids count changed for path {oldpath}; unexpected.' + ) + else: + for olduuid, newuuid in zip(olduuids, newuuids): + projtxt = projtxt.replace(newuuid, olduuid) + + # Now replace our existing project with this filtered one. + # This will properly update ordering for the id swaps we just made. + tree = osp.OpenStepDecoder.ParseFromString(projtxt) + self.project = XcodeProject(tree, projpath) + + def _trim_names(self, objid: str, parentpath: str) -> None: + obj = self.project.objects[objid] + assert hasattr(obj, 'name') + objpath = os.path.join(parentpath, obj.name) + if isinstance(obj, PBXGroup): + for childid in obj.children: + self._trim_names(childid, objpath) + + assert hasattr(obj, 'path') + if obj.path == obj.name: + delattr(obj, 'name') + + def _store_path_uuids( + self, objid: str, uuids: dict[str, list[str]], parentpath: str + ) -> None: + obj = self.project.objects[objid] + + # Hmmm - seems sometimes things have name but sometimes just path. + # (assuming maybe when they're identical or something?..) + if hasattr(obj, 'name'): + objpath = os.path.join(parentpath, obj.name) + else: + assert hasattr(obj, 'path') + assert '/' not in obj.path + objpath = os.path.join(parentpath, obj.path) + + # Store this uuid with this path. + uuidentry = uuids.setdefault(objpath, []) + uuidentry.append(objid) + + if isinstance(obj, PBXGroup): + for childid in obj.children: + self._store_path_uuids(childid, uuids, objpath) + else: + buildfiles = self.project.get_build_files_for_file(objid) + + # We'll replace the new buildfile ids with our previous ones; + # however can come out shuffled (the buildfile for target A + # might be given the UUID that was previously assigned to the + # buildfile for target B, etc.) We can fix this by sorting + # our buildfiles by the target they go with. + buildfiles.sort(key=self._target_name_for_buildfile) + for buildfile in buildfiles: + uuidentry.append(buildfile.get_id()) + + def _get_unique_group(self, name: str) -> Any: + grps = self.project.get_groups_by_name(name) + if len(grps) != 1: + raise RuntimeError( + f'Expected exactly 1 "{name}" group; found {len(grps)}.' + ) + return grps[0] + + def mod_app_delegate_mm(self) -> None: + """Set per-file compiler flags.""" + files = self.project.get_files_by_name('app_delegate.mm') + if len(files) != 1: + raise RuntimeError( + f'Expected to find exactly 1 app_delegate.mm;' + f' found {len(files)}.' + ) + bfiles = self.project.get_build_files_for_file(files[0].get_id()) + + for bfile in bfiles: + bfile.add_compiler_flags('-fobjc-arc') + + def hash_inputs( + self, sources: list[str], include_us: bool, project_source: str + ) -> str: + """Make a simple hash based on inputs to the project.""" + + if TYPE_CHECKING: + # Help Mypy infer the right type for this. + hashobj = hashlib.md5() + else: + hashobj = getattr(hashlib, 'md5')() + + # If they're not providing project-source, use what's on disk. + + # Hash our sorted list of sources; when sources are added, removed, + # or renamed we'll want to rebuild. + for source in sorted(sources): + hashobj.update(source.encode()) + + # Also hash the project-source we were passed. We do this so that + # we know to re-run our generation if xcode itself (or whatever else) + # modifies the project. We want to do that because our generated + # output might be slightly different than what xcode writes and + # we want our version to always win out and get stored in git; + # otherwise CI project checks would see mismatches and complain + # that the project is out of date. Ideally our output will exactly + # match xcode's so no rewrites will need to happen, but this way + # we'll behave even if they do need to. + hashobj.update(project_source.encode()) + + # Also include the full source of this module so we rebuild + # when logic here is updated. + if include_us: + with open(__file__, 'rb') as infile: + hashobj.update(infile.read()) + + return hashobj.hexdigest() + + @staticmethod + def _xcodesortkey(val: str) -> str: + # Yes this is super nitpicky, but I'd like to have things + # show up in xcode such that doing a 'sort by name' doesn't move + # anything around. The main funky bit of logic with xcode's + # sorting seems to be that foo.cc shows up *after* foobar.cc, + # whereas in vanilla Python sorts it comes before. A quick hack + # to fix this is to replace the . with something that comes after + # the alphabet instead of before. + return val.lower().replace('.', '~') + + def add_paths(self, parent_pbxgrp: Any) -> None: + """Do the thing.""" + + # PBXGroups we create for each dir we come across in paths. + dir_pbxgrps: dict[str, Any] = {} + + # For each path, create/fetch its chain of dirs as PBXGroups and + # drop the file in the bottom one. + for source in self.sources: + parts = source.split('/') + assert all(p for p in parts) + for i in range(len(parts) - 1): + thisname = parts[i] + thispath = '/'.join(parts[: i + 1]) + if thispath not in dir_pbxgrps: + if i == 0: + # Root; provide full path. + pbxgrp = self.project.get_or_create_group( + thisname, + os.path.abspath( + os.path.join(self.projroot, 'src', thispath) + ), + parent_pbxgrp, + make_relative=True, + ) + else: + # Non-root; provide relative path from parent. + parentpath = '/'.join(parts[:i]) + pbxgrp = self.project.get_or_create_group( + thisname, + thisname, + dir_pbxgrps[parentpath], + make_relative=True, + ) + dir_pbxgrps[thispath] = pbxgrp + + assert os.path.dirname(source) # All sources should be in a dir. + self.project.add_file( + os.path.basename(source), + dir_pbxgrps[os.path.dirname(source)], + force=False, + tree=TreeType.GROUP, + target_name=self._target_names_for_file( + os.path.basename(source) + ), + ) + + def _target_names_for_file(self, filename: str) -> list[str] | None: + # Cocoa stuff only applies to our macOS build. + if filename in { + 'CocoaAppDelegate.swift', + 'CocoaGLViewController.swift', + 'CocoaMetalViewController.swift', + }: + return [f'{self.pnameu} macOS'] + + # UIKit stuff applies to our iOS/tvOS builds. + if filename in { + 'UIKitAppDelegate.swift', + 'UIKitGLViewController.swift', + 'UIKitMetalViewController.swift', + }: + return [f'{self.pnameu} iOS', f'{self.pnameu} tvOS'] + + # Everything else applies to everything. + return None diff --git a/tools/efro/cloudshell.py b/tools/efro/cloudshell.py index b1e031e6..c443e8d9 100644 --- a/tools/efro/cloudshell.py +++ b/tools/efro/cloudshell.py @@ -40,7 +40,7 @@ class HostConfig: managed: bool = False idle_minutes: int = 5 can_sudo_reboot: bool = False - max_sessions: int = 3 + max_sessions: int = 4 reboot_wait_seconds: int = 20 reboot_attempts: int = 1 diff --git a/tools/efro/dataclassio/_base.py b/tools/efro/dataclassio/_base.py index 0402488b..6ef37105 100644 --- a/tools/efro/dataclassio/_base.py +++ b/tools/efro/dataclassio/_base.py @@ -152,7 +152,6 @@ class IOAttrs: soft_default: Any = MISSING, soft_default_factory: Callable[[], Any] | _MissingType = MISSING, ): - # Only store values that differ from class defaults to keep # our instances nice and lean. cls = type(self) @@ -167,7 +166,6 @@ class IOAttrs: if whole_minutes != cls.whole_minutes: self.whole_minutes = whole_minutes if soft_default is not cls.soft_default: - # Do what dataclasses does with its default types and # tell the user to use factory for mutable ones. if isinstance(soft_default, (list, dict, set)): diff --git a/tools/efro/dataclassio/_inputter.py b/tools/efro/dataclassio/_inputter.py index 9442a25d..d6650a65 100644 --- a/tools/efro/dataclassio/_inputter.py +++ b/tools/efro/dataclassio/_inputter.py @@ -96,6 +96,7 @@ class _Inputter(Generic[T]): ) return value + # noinspection PyPep8 if origin is typing.Union or origin is types.UnionType: # Currently, the only unions we support are None/Value # (translated from Optional), which we verified on prep. @@ -333,7 +334,8 @@ class _Inputter(Generic[T]): # We treat 'Any' dicts simply as json; we don't do any translating. if not childtypes or childtypes[0] is typing.Any: - if not isinstance(value, dict) or not _is_valid_for_codec( + value_any: Any = value + if not isinstance(value_any, dict) or not _is_valid_for_codec( value, self._codec ): raise TypeError( @@ -436,7 +438,6 @@ class _Inputter(Generic[T]): seqtype: type, ioattrs: IOAttrs | None, ) -> Any: - # Because we are json-centric, we expect a list for all sequences. if type(value) is not list: raise TypeError( @@ -468,7 +469,6 @@ class _Inputter(Generic[T]): def _datetime_from_input( self, cls: type, fieldpath: str, value: Any, ioattrs: IOAttrs | None ) -> Any: - # For firestore we expect a datetime object. if self._codec is Codec.FIRESTORE: # Don't compare exact type here, as firestore can give us @@ -510,7 +510,6 @@ class _Inputter(Generic[T]): value: Any, ioattrs: IOAttrs | None, ) -> Any: - out: list = [] # Because we are json-centric, we expect a list for all sequences. diff --git a/tools/efro/dataclassio/_outputter.py b/tools/efro/dataclassio/_outputter.py index 65c5f395..406265cc 100644 --- a/tools/efro/dataclassio/_outputter.py +++ b/tools/efro/dataclassio/_outputter.py @@ -166,6 +166,7 @@ class _Outputter: ) return value if self._create else None + # noinspection PyPep8 if origin is typing.Union or origin is types.UnionType: # Currently, the only unions we support are None/Value # (translated from Optional), which we verified on prep. @@ -387,8 +388,9 @@ class _Outputter: assert len(childtypes) in (0, 2) # We treat 'Any' dicts simply as json; we don't do any translating. + value_any: Any = value if not childtypes or childtypes[0] is typing.Any: - if not isinstance(value, dict) or not _is_valid_for_codec( + if not isinstance(value_any, dict) or not _is_valid_for_codec( value, self._codec ): raise TypeError( diff --git a/tools/efro/dataclassio/_pathcapture.py b/tools/efro/dataclassio/_pathcapture.py index 2fb0c8a3..6b9a61a6 100644 --- a/tools/efro/dataclassio/_pathcapture.py +++ b/tools/efro/dataclassio/_pathcapture.py @@ -27,7 +27,6 @@ class _PathCapture: self._pathparts = pathparts def __getattr__(self, name: str) -> _PathCapture: - # We only allow diving into sub-objects if we are a dataclass. if not self._is_dataclass: raise TypeError( diff --git a/tools/efro/dataclassio/_prep.py b/tools/efro/dataclassio/_prep.py index d01db203..0800b559 100644 --- a/tools/efro/dataclassio/_prep.py +++ b/tools/efro/dataclassio/_prep.py @@ -143,7 +143,8 @@ class PrepSession: raise RuntimeError('Max recursion exceeded.') # We should only be passed classes which are dataclasses. - if not isinstance(cls, type) or not dataclasses.is_dataclass(cls): + cls_any: Any = cls + if not isinstance(cls_any, type) or not dataclasses.is_dataclass(cls): raise TypeError(f'Passed arg {cls} is not a dataclass type.') # Add a pointer to the prep-session while doing the prep. @@ -175,7 +176,7 @@ class PrepSession: # which allows us to pick up nested classes, etc. resolved_annotations = get_type_hints( cls, - localns=vars(cls), + localns=vars(cls), # type: ignore[arg-type] globalns=self.globalns, include_extras=True, ) @@ -199,7 +200,6 @@ class PrepSession: # now recurse through them, verifying that we support all contained # types and prepping any contained dataclass types. for attrname, anntype in resolved_annotations.items(): - anntype, ioattrs = _parse_annotated(anntype) # If we found attached IOAttrs data, make sure it contains @@ -260,6 +260,7 @@ class PrepSession: origin = _get_origin(anntype) + # noinspection PyPep8 if origin is typing.Union or origin is types.UnionType: self.prep_union( cls, attrname, anntype, recursion_level=recursion_level + 1 diff --git a/tools/efro/debug.py b/tools/efro/debug.py index 8a78b545..438a84e6 100644 --- a/tools/efro/debug.py +++ b/tools/efro/debug.py @@ -259,8 +259,10 @@ def printsizes( def _desctype(obj: Any) -> str: cls = type(obj) + # noinspection PyPep8 if cls is types.ModuleType: return f'{type(obj).__name__} {obj.__name__}' + # noinspection PyPep8 if cls is types.MethodType: bnd = 'bound' if hasattr(obj, '__self__') else 'unbound' return f'{bnd} {type(obj).__name__} {obj.__name__}' @@ -321,7 +323,6 @@ def _printrefs( if level < max_level or (id(obj) in expand_ids and level < ABS_MAX_LEVEL): refs = getrefs(obj) for ref in refs: - # It seems we tend to get a transient cell object with contents # set to obj. Would be nice to understand why that happens # but just ignoring it for now. diff --git a/tools/efro/error.py b/tools/efro/error.py index 456fc6f9..ed731ae3 100644 --- a/tools/efro/error.py +++ b/tools/efro/error.py @@ -117,13 +117,11 @@ def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool: socket.timeout, ), ): - # Special case: although an HTTPError is a subclass of URLError, # we don't consider it a communication error. It generally means we # have successfully communicated with the server but what we are asking # for is not there/etc. if isinstance(exc, urllib.error.HTTPError): - # Special sub-case: appspot.com hosting seems to give 403 errors # (forbidden) to some countries. I'm assuming for legal reasons?.. # Let's consider that a communication error since its out of our diff --git a/tools/efro/log.py b/tools/efro/log.py index fa0dc1ce..080dd98d 100644 --- a/tools/efro/log.py +++ b/tools/efro/log.py @@ -133,7 +133,6 @@ class LogHandler(logging.Handler): # pylint: disable=consider-using-with self._file = None if path is None else open(path, 'w', encoding='utf-8') self._echofile = echofile - self._callbacks_lock = Lock() self._callbacks: list[Callable[[LogEntry], None]] = [] self._suppress_non_root_debug = suppress_non_root_debug self._file_chunks: dict[str, list[str]] = {'stdout': [], 'stderr': []} @@ -161,13 +160,42 @@ class LogHandler(logging.Handler): while not self._thread_bootstrapped: time.sleep(0.001) - def add_callback(self, call: Callable[[LogEntry], None]) -> None: + def add_callback( + self, call: Callable[[LogEntry], None], feed_existing_logs: bool = False + ) -> None: """Add a callback to be run for each LogEntry. Note that this callback will always run in a background thread. + Passing True for feed_existing_logs will cause all cached logs + in the handler to be fed to the callback (still in the + background thread though). """ - with self._callbacks_lock: - self._callbacks.append(call) + + # Kick this over to our bg thread to add the callback and + # process cached entries at the same time to ensure there are no + # race conditions that could cause entries to be skipped/etc. + self._event_loop.call_soon_threadsafe( + tpartial(self._add_callback_in_thread, call, feed_existing_logs) + ) + + def _add_callback_in_thread( + self, call: Callable[[LogEntry], None], feed_existing_logs: bool + ) -> None: + """Add a callback to be run for each LogEntry. + + Note that this callback will always run in a background thread. + Passing True for feed_existing_logs will cause all cached logs + in the handler to be fed to the callback (still in the + background thread though). + """ + assert current_thread() is self._thread + self._callbacks.append(call) + + # Run all of our cached entries through the new callback if desired. + if feed_existing_logs and self._cache_size_limit > 0: + with self._cache_lock: + for _id, entry in self._cache: + self._run_callback_on_entry(call, entry) def _log_thread_main(self) -> None: self._event_loop = asyncio.new_event_loop() @@ -183,12 +211,14 @@ class LogHandler(logging.Handler): self._thread_bootstrapped = True try: if self._cache_time_limit is not None: - self._event_loop.create_task(self._time_prune_cache()) + _prunetask = self._event_loop.create_task( + self._time_prune_cache() + ) self._event_loop.run_forever() except BaseException: - # If this ever goes down we're in trouble. - # We won't be able to log about it though... - # Try to make some noise however we can. + # If this ever goes down we're in trouble; we won't be able + # to log about it though. Try to make some noise however we + # can. print('LogHandler died!!!', file=sys.stderr) import traceback @@ -201,7 +231,6 @@ class LogHandler(logging.Handler): await asyncio.sleep(61.27) now = utc_now() with self._cache_lock: - # Prune the oldest entry as long as there is a first one that # is too old. while ( @@ -380,7 +409,6 @@ class LogHandler(logging.Handler): message: str | logging.LogRecord, ) -> None: try: - # If they passed a raw record here, bake it down to a string. if isinstance(message, logging.LogRecord): message = self.format(message) @@ -508,25 +536,30 @@ class LogHandler(logging.Handler): self._cache_index_offset += 1 # Pass to callbacks. - with self._callbacks_lock: - for call in self._callbacks: - try: - call(entry) - except Exception: - # Only print one callback error to avoid insanity. - if not self._printed_callback_error: - import traceback - - traceback.print_exc(file=self._echofile) - self._printed_callback_error = True + for call in self._callbacks: + self._run_callback_on_entry(call, entry) # Dump to our structured log file. - # TODO: set a timer for flushing; don't flush every line. + # TODO: should set a timer for flushing; don't flush every line. if self._file is not None: entry_s = dataclass_to_json(entry) assert '\n' not in entry_s # Make sure its a single line. print(entry_s, file=self._file, flush=True) + def _run_callback_on_entry( + self, callback: Callable[[LogEntry], None], entry: LogEntry + ) -> None: + """Run a callback and handle any errors.""" + try: + callback(entry) + except Exception: + # Only print the first callback error to avoid insanity. + if not self._printed_callback_error: + import traceback + + traceback.print_exc(file=self._echofile) + self._printed_callback_error = True + class FileLogEcho: """A file-like object for forwarding stdout/stderr to a LogHandler.""" @@ -611,7 +644,10 @@ def setup_logging( force=True, ) if had_previous_handlers: - logging.warning('setup_logging: force-replacing previous handlers.') + logging.warning( + 'setup_logging: Replacing existing handlers.' + ' Something may have logged before expected.' + ) # Optionally intercept Python's stdout/stderr output and generate # log entries from it. diff --git a/tools/efro/message/_protocol.py b/tools/efro/message/_protocol.py index ce86d39b..1276233b 100644 --- a/tools/efro/message/_protocol.py +++ b/tools/efro/message/_protocol.py @@ -96,7 +96,6 @@ class MessageProtocol: type[Response] | type[SysResponse], int ] = {} for m_id, m_type in message_types.items(): - # Make sure only valid message types were passed and each # id was assigned only once. assert isinstance(m_id, int) @@ -331,7 +330,7 @@ class MessageProtocol: for module, names in sorted(imports.items()): jnames = ', '.join(names) line = f'from {module} import {jnames}' - if len(line) > 79: + if len(line) > 80: # Recreate in a wrapping-friendly form. line = f'from {module} import ({jnames})' import_lines += f'{line}\n' @@ -485,7 +484,6 @@ class MessageProtocol: out += f' return cast({rtypevar}, out)\n' else: - for msgtype in msgtypes: msgtypevar = msgtype.__name__ rtypes = msgtype.get_response_types() diff --git a/tools/efro/message/_receiver.py b/tools/efro/message/_receiver.py index 3d8477c3..6659f76b 100644 --- a/tools/efro/message/_receiver.py +++ b/tools/efro/message/_receiver.py @@ -73,7 +73,7 @@ class MessageReceiver: """ # TODO: can use types.GenericAlias in 3.9. # (hmm though now that we're there, it seems a drop-in - # replace gives us errors. Should re-test in 3.10 as it seems + # replace gives us errors. Should re-test in 3.11 as it seems # that typing_extensions handles it differently in that case) from typing import _GenericAlias # type: ignore from typing import get_type_hints, get_args @@ -141,6 +141,7 @@ class MessageReceiver: # This will contain NoneType for empty return cases, but # we expect it to be None. + # noinspection PyPep8 responsetypes = tuple( None if r is type(None) else r for r in responsetypes ) diff --git a/tools/efro/message/_sender.py b/tools/efro/message/_sender.py index 63effe76..c1f9d00c 100644 --- a/tools/efro/message/_sender.py +++ b/tools/efro/message/_sender.py @@ -271,7 +271,6 @@ class MessageSender: async def _fetch_raw_response_awaitable( self, bound_obj: Any, message: Message, send_awaitable: Awaitable[str] ) -> Response | SysResponse: - try: response_encoded = await send_awaitable except Exception as exc: @@ -360,7 +359,6 @@ class MessageSender: # Some error occurred. Raise a local Exception for it. if isinstance(raw_response, ErrorSysResponse): - # Errors that happened locally can attach their exceptions # here for extra logging goodness. local_exception = raw_response.get_local_exception() diff --git a/tools/efro/rpc.py b/tools/efro/rpc.py index d7af09ff..11ac1535 100644 --- a/tools/efro/rpc.py +++ b/tools/efro/rpc.py @@ -12,9 +12,8 @@ from enum import Enum from collections import deque from dataclasses import dataclass from threading import current_thread -from typing import TYPE_CHECKING, Annotated +from typing import TYPE_CHECKING, Annotated, assert_never -from efro.util import assert_never from efro.error import ( CommunicationError, is_asyncio_streams_communication_error, @@ -51,7 +50,6 @@ _BYTE_ORDER: Literal['big'] = 'big' @ioprepped @dataclass class _PeerInfo: - # So we can gracefully evolve how we communicate in the future. protocol: Annotated[int, IOAttrs('p')] @@ -270,7 +268,6 @@ class RPCEndpoint: raise async def _do_run(self) -> None: - self._check_env() if self._run_called: @@ -430,7 +427,6 @@ class RPCEndpoint: bytes_awaitable: asyncio.Task[bytes], message_id: int, ) -> bytes: - # We need to know their protocol, so if we haven't gotten a handshake # from them yet, just wait. while self._peer_info is None: @@ -456,13 +452,11 @@ class RPCEndpoint: raise CommunicationError() from exc except Exception as exc: - # If our timer timed-out or anything else went wrong with # the stream, lump it in as a communication error. if isinstance( exc, asyncio.TimeoutError ) or is_asyncio_streams_communication_error(exc): - if self.debug_print: self.debug_print_call( f'{self._label}: got {type(exc)} sending message' @@ -754,7 +748,6 @@ class RPCEndpoint: # Now just write out-messages as they come in. while True: - # Wait until some data comes in. await self._have_out_packets.wait() diff --git a/tools/efro/util.py b/tools/efro/util.py index 2c0ab72a..a36c2d70 100644 --- a/tools/efro/util.py +++ b/tools/efro/util.py @@ -46,6 +46,19 @@ else: Call = functools.partial +def snake_case_to_title(val: str) -> str: + """Given a snake-case string 'foo_bar', returns 'Foo Bar'.""" + # Kill empty words resulting from leading/trailing/multiple underscores. + return ' '.join(w for w in val.split('_') if w).title() + + +def snake_case_to_camel_case(val: str) -> str: + """Given a snake-case string 'foo_bar', returns camel-case 'FooBar'.""" + # Replace underscores with spaces; capitalize words; kill spaces. + # Not sure about efficiency, but logically simple. + return val.replace('_', ' ').title().replace(' ', '') + + def enum_by_value(cls: type[EnumT], value: Any) -> EnumT: """Create an enum from a value. @@ -218,7 +231,6 @@ class DirtyBit: @dirty.setter def dirty(self, value: bool) -> None: - # If we're freshly clean, set our next auto-dirty time (if we have # one). if self._dirty and not value and self._auto_dirty_seconds is not None: @@ -668,16 +680,6 @@ def compact_id(num: int) -> str: ) -# NOTE: Even though this is available as part of typing_extensions, keeping -# it in here for now so we don't require typing_extensions as a dependency. -# Once 3.11 rolls around we can kill this and use typing.assert_never. -def assert_never(value: NoReturn) -> NoReturn: - """Trick for checking exhaustive handling of Enums, etc. - See https://github.com/python/typing/issues/735 - """ - assert False, f'Unhandled value: {value} ({type(value).__name__})' - - def unchanging_hostname() -> str: """Return an unchanging name for the local device. @@ -733,3 +735,99 @@ def set_canonical_module( type(obj), name, ) + + +def timedelta_str( + timeval: datetime.timedelta | float, maxparts: int = 2, decimals: int = 0 +) -> str: + """Return a simple human readable time string for a length of time. + + Time can be given as a timedelta or a float representing seconds. + Example output: + "23d 1h 2m 32s" (with maxparts == 4) + "23d 1h" (with maxparts == 2) + "23d 1.08h" (with maxparts == 2 and decimals == 2) + + Note that this is hard-coded in English and probably not especially + performant. + """ + # pylint: disable=too-many-locals + + if isinstance(timeval, float): + timevalfin = datetime.timedelta(seconds=timeval) + else: + timevalfin = timeval + + # Internally we only handle positive values. + if timevalfin.total_seconds() < 0: + return f'-{timedelta_str(timeval=-timeval, maxparts=maxparts)}' + + years = timevalfin.days // 365 + days = timevalfin.days % 365 + hours = timevalfin.seconds // 3600 + hour_remainder = timevalfin.seconds % 3600 + minutes = hour_remainder // 60 + seconds = hour_remainder % 60 + + # Now, if we want decimal places for our last value, + # calc fractional parts. + if decimals: + # Calc totals of each type. + t_seconds = timevalfin.total_seconds() + t_minutes = t_seconds / 60 + t_hours = t_minutes / 60 + t_days = t_hours / 24 + t_years = t_days / 365 + + # Calc fractional parts that exclude all whole values to their left. + years_covered = years + years_f = t_years - years_covered + days_covered = years_covered * 365 + days + days_f = t_days - days_covered + hours_covered = days_covered * 24 + hours + hours_f = t_hours - hours_covered + minutes_covered = hours_covered * 60 + minutes + minutes_f = t_minutes - minutes_covered + seconds_covered = minutes_covered * 60 + seconds + seconds_f = t_seconds - seconds_covered + else: + years_f = days_f = hours_f = minutes_f = seconds_f = 0.0 + + parts: list[str] = [] + for part, part_f, suffix in ( + (years, years_f, 'y'), + (days, days_f, 'd'), + (hours, hours_f, 'h'), + (minutes, minutes_f, 'm'), + (seconds, seconds_f, 's'), + ): + if part or parts or (not parts and suffix == 's'): + # Do decimal version only for the last part. + if decimals and (len(parts) >= maxparts - 1 or suffix == 's'): + parts.append(f'{part+part_f:.{decimals}f}{suffix}') + else: + parts.append(f'{part}{suffix}') + if len(parts) >= maxparts: + break + return ' '.join(parts) + + +def ago_str( + timeval: datetime.datetime, + maxparts: int = 1, + now: datetime.datetime | None = None, + decimals: int = 0, +) -> str: + """Given a datetime, return a clean human readable 'ago' str. + + Note that this is hard-coded in English so should not be used + for visible in-game elements; only tools/etc. + + If now is not passed, efro.util.utc_now() is used. + """ + if now is None: + now = utc_now() + return ( + timedelta_str(now - timeval, maxparts=maxparts, decimals=decimals) + + ' ago' + ) diff --git a/tools/efrotools/__init__.py b/tools/efrotools/__init__.py index c0452242..728069d7 100644 --- a/tools/efrotools/__init__.py +++ b/tools/efrotools/__init__.py @@ -12,19 +12,21 @@ live client or server code. from __future__ import annotations import os +import sys import json -import platform from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, overload if TYPE_CHECKING: from typing import Sequence, Any, Literal # Python major version we're using for all this stuff. -PYVER = '3.10' +PYVER = '3.11' # Python binary assumed by these tools. -PYTHON_BIN = f'python{PYVER}' if platform.system() != 'Windows' else 'python' +# PYTHON_BIN = f 'python{PYVER}' if platform.system() != 'Windows' else 'python' +# Update; just using the same executable used to launch us. +PYTHON_BIN = sys.executable def explicit_bool(value: bool) -> bool: @@ -35,10 +37,14 @@ def explicit_bool(value: bool) -> bool: def getlocalconfig(projroot: Path) -> dict[str, Any]: """Return a project's localconfig contents (or default if missing).""" localconfig: dict[str, Any] + + # Allow overriding path via env var. + path = os.environ.get('EFRO_LOCALCONFIG_PATH') + if path is None: + path = 'config/localconfig.json' + try: - with open( - Path(projroot, 'config/localconfig.json'), encoding='utf-8' - ) as infile: + with open(Path(projroot, path), encoding='utf-8') as infile: localconfig = json.loads(infile.read()) except FileNotFoundError: localconfig = {} @@ -46,11 +52,11 @@ def getlocalconfig(projroot: Path) -> dict[str, Any]: def getconfig(projroot: Path) -> dict[str, Any]: - """Return a project's config contents (or default if missing).""" + """Return a project's projectconfig contents (or default if missing).""" config: dict[str, Any] try: with open( - Path(projroot, 'config/config.json'), encoding='utf-8' + Path(projroot, 'config/projectconfig.json'), encoding='utf-8' ) as infile: config = json.loads(infile.read()) except FileNotFoundError: @@ -61,12 +67,53 @@ def getconfig(projroot: Path) -> dict[str, Any]: def setconfig(projroot: Path, config: dict[str, Any]) -> None: """Set the project config contents.""" os.makedirs(Path(projroot, 'config'), exist_ok=True) - with Path(projroot, 'config/config.json').open( + with Path(projroot, 'config/projectconfig.json').open( 'w', encoding='utf-8' ) as outfile: outfile.write(json.dumps(config, indent=2)) +@overload +def extract_arg( + args: list[str], name: str, required: Literal[False] = False +) -> str | None: + ... + + +@overload +def extract_arg(args: list[str], name: str, required: Literal[True]) -> str: + ... + + +def extract_arg( + args: list[str], name: str, required: bool = False +) -> str | None: + """Given a list of args and an arg name, returns a value. + + The arg flag and value are removed from the arg list. + raises CleanErrors on any problems. + """ + from efro.error import CleanError + + count = args.count(name) + if not count: + if required: + raise CleanError(f'Required argument {name} not passed.') + return None + + if count > 1: + raise CleanError(f'Arg {name} passed multiple times.') + + argindex = args.index(name) + if argindex + 1 >= len(args): + raise CleanError(f'No value passed after {name} arg.') + + val = args[argindex + 1] + del args[argindex : argindex + 2] + + return val + + def get_public_license(style: str) -> str: """Return the license notice as used for our public facing stuff. @@ -107,8 +154,8 @@ def replace_exact(opstr: str, old: str, new: str, count: int = 1) -> str: """ found = opstr.count(old) if found != count: - raise Exception( - f'expected {count} string occurrence(s);' + raise RuntimeError( + f'Expected {count} string occurrence(s);' f' found {found}. String = {old}' ) return opstr.replace(old, new) @@ -124,7 +171,7 @@ def get_files_hash( import hashlib if not isinstance(filenames, list): - raise Exception('expected a list') + raise RuntimeError(f'Expected a list; got a {type(filenames)}.') if TYPE_CHECKING: # Help Mypy infer the right type for this. hashobj = hashlib.md5() @@ -198,11 +245,10 @@ def py_examine( with open(filename, encoding='utf-8') as infile: fcontents = infile.read() if '#@' in fcontents: - raise Exception('#@ marker found in file; this breaks examinations.') + raise RuntimeError('#@ marker found in file; this breaks examinations.') flines = fcontents.splitlines() if operation == 'pylint_infer': - # See what asteroid can infer about the target symbol. symbol = ( selection @@ -220,7 +266,6 @@ def py_examine( inferred = list(node.infer()) print(symbol + ':', ', '.join([str(i) for i in inferred])) elif operation in ('mypy_infer', 'mypy_locals'): - # Ask mypy for the type of the target symbol. symbol = ( selection diff --git a/tools/efrotools/buildlock.py b/tools/efrotools/buildlock.py new file mode 100644 index 00000000..7930b041 --- /dev/null +++ b/tools/efrotools/buildlock.py @@ -0,0 +1,48 @@ +# Released under the MIT License. See LICENSE for details. +# +"""A system for sanity testing parallel build isolation.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +import os + + +if TYPE_CHECKING: + from typing import Any + +LOCK_DIR_PATH = '.cache/buildlocks' + + +class BuildLock: + """Tries to ensure a build is not getting stomped on/etc.""" + + def __init__(self, name: str) -> None: + self.name = name + if '/' in name or '\\' in name: + raise ValueError(f"Illegal BuildLock name: '{name}'.") + self.lockpath = os.path.join(LOCK_DIR_PATH, name) + + def __enter__(self) -> None: + if not os.path.exists(LOCK_DIR_PATH): + os.makedirs(LOCK_DIR_PATH, exist_ok=True) + + # Note: we aren't doing anything super accurate/atomic here. This + # is more intended as a gross check to make noise on clearly broken + # build logic; it isn't important that it catch every corner case + # perfectly. + if os.path.exists(self.lockpath): + raise RuntimeError( + f"Build-lock: lock '{self.name}' exists." + ' This probably means multiple builds' + ' are running at once that should not be.' + ) + with open(self.lockpath, 'w', encoding='utf-8') as outfile: + outfile.write('') + + def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any: + if not os.path.exists(self.lockpath): + raise RuntimeError( + f"Build-lock: lock '{self.name}' not found at tear-down." + ) + os.unlink(self.lockpath) diff --git a/tools/efrotools/code.py b/tools/efrotools/code.py index 291c8b58..bf4540d7 100644 --- a/tools/efrotools/code.py +++ b/tools/efrotools/code.py @@ -54,7 +54,9 @@ def format_project_cpp_files(projroot: Path, full: bool) -> None: cache = FileCache(cachepath) cfconfig = Path(projroot, '.clang-format') - filenames = get_code_filenames(projroot) + # Exclude generated files or else we could mess up dependencies + # by mucking with their modtimes. + filenames = get_code_filenames(projroot, include_generated=False) confighash = get_files_hash([cfconfig]) cache.update(filenames, confighash) @@ -67,7 +69,7 @@ def format_project_cpp_files(projroot: Path, full: bool) -> None: # make sure to use subprocess. result = subprocess.call(['clang-format', '-i', filename]) if result != 0: - raise Exception(f'Formatting failed for {filename}') + raise RuntimeError(f'Formatting failed for {filename}') duration = time.time() - start_time print(f'Formatted {filename} in {duration:.2f} seconds.') sys.stdout.flush() @@ -94,15 +96,16 @@ def check_cpplint(projroot: Path, full: bool) -> None: # pylint: disable=too-many-locals from concurrent.futures import ThreadPoolExecutor from multiprocessing import cpu_count + from efrotools import getconfig, PYVER from efro.terminal import Clr from efro.error import CleanError os.chdir(projroot) - filenames = get_code_filenames(projroot) + filenames = get_code_filenames(projroot, include_generated=True) for fpath in filenames: if ' ' in fpath: - raise Exception(f'Found space in path {fpath}; unexpected.') + raise RuntimeError(f'Found space in path {fpath}; unexpected.') # Check the config for a list of ones to ignore. code_blacklist: list[str] = getconfig(projroot).get('cpplint_blacklist', []) @@ -139,6 +142,10 @@ def check_cpplint(projroot: Path, full: bool) -> None: result = subprocess.call( [ f'python{PYVER}', + # Currently (May 2023) seeing a bunch of warnings + # about 'sre_compile deprecated'. Ignoring them. + '-W', + 'ignore::DeprecationWarning', '-m', 'cpplint', '--root=src', @@ -164,8 +171,14 @@ def check_cpplint(projroot: Path, full: bool) -> None: ) -def get_code_filenames(projroot: Path) -> list[str]: - """Return the list of files to lint-check or auto-formatting.""" +def get_code_filenames(projroot: Path, include_generated: bool) -> list[str]: + """Return the list of files to lint-check or auto-format. + + Be sure to pass False for include_generated if performing any + operation that can modify files (such as formatting). Otherwise it + could cause dirty generated files to not get updated properly when + their sources change). + """ from efrotools import getconfig exts = ('.h', '.c', '.cc', '.cpp', '.cxx', '.m', '.mm') @@ -177,7 +190,11 @@ def get_code_filenames(projroot: Path) -> list[str]: for root, _dirs, files in os.walk(place): for fname in files: if any(fname.endswith(ext) for ext in exts): - codefilenames.append(os.path.join(root, fname)) + path = os.path.join(root, fname) + if '/mgen/' in path and not include_generated: + pass + else: + codefilenames.append(path) codefilenames.sort() return codefilenames @@ -294,7 +311,7 @@ def runpylint(projroot: Path, filenames: list[str]) -> None: pylintrc = Path(projroot, '.pylintrc') if not os.path.isfile(pylintrc): - raise Exception('pylintrc not found where expected') + raise RuntimeError('pylintrc not found where expected') # Technically we could just run pylint standalone via command line here, # but let's go ahead and run it inline so we're consistent with our cached @@ -311,11 +328,11 @@ def pylint(projroot: Path, full: bool, fast: bool) -> None: pylintrc = Path(projroot, '.pylintrc') if not os.path.isfile(pylintrc): - raise Exception('pylintrc not found where expected') + raise RuntimeError('pylintrc not found where expected') filenames = get_script_filenames(projroot) if any(' ' in name for name in filenames): - raise Exception('found space in path; unexpected') + raise RuntimeError('found space in path; unexpected') script_blacklist: list[str] = [] filenames = [f for f in filenames if f not in script_blacklist] @@ -401,7 +418,7 @@ def _dirty_dep_check( recursion2 = recursion if fast: # Our one exception is top level ba which basically aggregates. - if not fname.endswith('/ba/__init__.py'): + if not fname.endswith('/babase/__init__.py'): recursion2 += 1 if recursion2 <= 1: deps = cacheentry.get('deps', []) @@ -609,7 +626,7 @@ def _apply_pylint_run_to_cache( dep for dep in untracked_deps if dep not in ignored_untracked_deps - and not dep.startswith('bametainternal') + and not dep.startswith('baplusmeta') ) if untracked_deps: raise CleanError( @@ -655,7 +672,7 @@ def _apply_pylint_run_to_cache( for fname in dirtyfiles: mname2 = paths_to_names.get(fname) if mname2 is None: - raise Exception('unable to get module name for "' + fname + '"') + raise RuntimeError('unable to get module name for "' + fname + '"') counts = stats_by_module.get(mname2) # 'statement' count seems to be new and always non-zero; ignore it @@ -686,10 +703,9 @@ def runmypy( projroot: Path, filenames: list[str], full: bool = False, check: bool = True ) -> None: """Run MyPy on provided filenames.""" - from efrotools import PYTHON_BIN args = [ - PYTHON_BIN, + sys.executable, '-m', 'mypy', '--pretty', @@ -824,7 +840,7 @@ def _run_idea_inspections( if not iprof.exists(): iprof = Path(projroot, '.idea/inspectionProfiles/Project_Default.xml') if not iprof.exists(): - raise Exception('No default inspection profile found.') + raise RuntimeError('No default inspection profile found.') cmd = [str(inspect), str(projroot), str(iprof), tmpdir.name, '-v2'] if inspectdir is not None: cmd += ['-d', str(inspectdir)] @@ -847,16 +863,8 @@ def _run_idea_inspections( if result.returncode != 0: # In verbose mode this stuff got printed already. if not verbose: - 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) - ) + stdout = result.stdout.decode() + stderr = result.stderr.decode() print( f'{displayname} inspection failure stdout:\n{stdout}' + f'{displayname} inspection failure stderr:\n{stderr}' @@ -979,7 +987,6 @@ def check_pycharm(projroot: Path, full: bool, verbose: bool) -> None: if bool(False): print('Launching GUI PyCharm to rebuild caches...', flush=True) with subprocess.Popen(str(pycharmbin)) as process: - # Wait a bit and ask it nicely to die. # We need to make sure it has enough time to do its # cache updating thing even if the system is fully under load. @@ -1011,7 +1018,7 @@ def check_clioncode(projroot: Path, full: bool, verbose: bool) -> None: import time cachepath = Path('.cache/check_clioncode') - filenames = get_code_filenames(projroot) + filenames = get_code_filenames(projroot, include_generated=True) clionroot = Path('/Applications/CLion.app') # clionbin = Path(clionroot, 'Contents/MacOS/clion') inspect = Path(clionroot, 'Contents/bin/inspect.sh') @@ -1038,7 +1045,7 @@ def check_clioncode(projroot: Path, full: bool, verbose: bool) -> None: print('Launching GUI CLion to rebuild caches...', flush=True) # process = subprocess.Popen(str(clionbin)) subprocess.run( - ['open', '-a', clionroot, Path(projroot, 'ballisticacore-cmake')], + ['open', '-a', clionroot, Path(projroot, 'ballisticakit-cmake')], check=True, ) @@ -1073,7 +1080,7 @@ def check_clioncode(projroot: Path, full: bool, verbose: bool) -> None: cachepath=cachepath, filenames=filenames, full=full, - projroot=Path(projroot, 'ballisticacore-cmake'), + projroot=Path(projroot, 'ballisticakit-cmake'), inspectdir=Path(projroot, 'src/ballistica'), displayname='CLion', inspect=inspect, @@ -1086,7 +1093,7 @@ def check_android_studio(projroot: Path, full: bool, verbose: bool) -> None: # import time cachepath = Path('.cache/check_android_studio') - filenames = get_code_filenames(projroot) + filenames = get_code_filenames(projroot, include_generated=True) clionroot = Path('/Applications/Android Studio.app') # clionbin = Path(clionroot, 'Contents/MacOS/studio') inspect = Path(clionroot, 'Contents/bin/inspect.sh') @@ -1126,10 +1133,10 @@ def check_android_studio(projroot: Path, full: bool, verbose: bool) -> None: cachepath=cachepath, filenames=filenames, full=full, - projroot=Path(projroot, 'ballisticacore-android'), + projroot=Path(projroot, 'ballisticakit-android'), inspectdir=Path( projroot, - 'ballisticacore-android/BallisticaCore/src/main/cpp/src/ballistica', + 'ballisticakit-android/BallisticaKit/src/main/cpp/src/ballistica', ), # inspectdir=None, displayname='Android Studio', diff --git a/tools/efrotools/efrocache.py b/tools/efrotools/efrocache.py index f26ab962..5cbb0a92 100644 --- a/tools/efrotools/efrocache.py +++ b/tools/efrotools/efrocache.py @@ -155,9 +155,9 @@ def filter_makefile(makefile_dir: str, contents: str) -> str: """Filter makefile contents to use efrocache lookups.""" if makefile_dir: - # Assuming just one level deep at the moment; can revisit later. - assert '/' not in makefile_dir - to_proj_root = '..' + # Assuming two levels deep at the moment; can revisit if needed. + assert len(makefile_dir.split('/')) == 2 + to_proj_root = '../..' else: to_proj_root = '' @@ -262,7 +262,6 @@ def _upload_cache( hashes_str: str, hashes_existing_str: str, ) -> None: - # First, if we've run before, print the files causing us to re-run: if hashes_existing_str != '': changed_files: set[str] = set() @@ -492,7 +491,6 @@ def warm_start_cache() -> None: cachemap_mtime = os.path.getmtime(CACHE_MAP_NAME) entries: list[tuple[str, str]] = [] for fname, url in cachemap.items(): - # File hasn't been pulled from cache yet = ignore. if not os.path.exists(fname): continue diff --git a/tools/efrotools/filecommand.py b/tools/efrotools/filecommand.py index 632ebba0..7a543fdd 100644 --- a/tools/efrotools/filecommand.py +++ b/tools/efrotools/filecommand.py @@ -77,12 +77,10 @@ class _FileBatchesRun: if os.path.isfile(path): self._possibly_add_to_pending_batch(path) elif os.path.isdir(path): - # From os.walk docs: we can prune dirs in-place when # running in top-down mode. We can use this to skip # diving into mac packages. for root, dirs, fnames in os.walk(path, topdown=True): - # If we find dirs that are actually mac packages, pull # them out of the dir list we'll dive into and pass # them directly to our batch for processing. diff --git a/tools/efrotools/ios.py b/tools/efrotools/ios.py index db724320..a49b7158 100644 --- a/tools/efrotools/ios.py +++ b/tools/efrotools/ios.py @@ -45,7 +45,9 @@ class LocalConfig: sftp_dir: str -def push_ipa(root: pathlib.Path, modename: str) -> None: +def push_ipa( + root: pathlib.Path, modename: str, signing_config: str | None +) -> None: """Construct ios IPA and push it to staging server for device testing. This takes some shortcuts to minimize turnaround time; @@ -54,21 +56,21 @@ def push_ipa(root: pathlib.Path, modename: str) -> None: The use case for this is quick build iteration on a device that is not physically near the build machine. """ - from efrotools.xcode import project_build_path + from efrotools.xcodebuild import project_build_path # Load both the local and project config data. cfg = Config(**getconfig(root)['push_ipa_config']) lcfg = LocalConfig(**getlocalconfig(root)['push_ipa_local_config']) if modename not in MODES: - raise Exception('invalid mode: "' + str(modename) + '"') + raise RuntimeError(f'invalid mode: "{modename}"') mode = MODES[modename] xcprojpath = pathlib.Path(root, cfg.projectpath) app_dir = project_build_path( projroot=str(root), project_path=str(xcprojpath), - scheme='BallisticaCore iOS Legacy', + scheme='BallisticaKit iOS Legacy', configuration=mode['configuration'], executable=False, ) @@ -84,12 +86,12 @@ def push_ipa(root: pathlib.Path, modename: str) -> None: # Inject our latest build into an existing xcarchive (creating if needed). archivepath = _add_build_to_xcarchive( - workdir, xcprojpath, built_app_path, cfg + workdir, xcprojpath, built_app_path, cfg, signing_config ) # Export an IPA from said xcarchive. ipa_path = _export_ipa_from_xcarchive( - archivepath, exportoptionspath, ipa_dir_path, cfg + archivepath, exportoptionspath, ipa_dir_path, cfg, signing_config ) # And lastly sync said IPA up to our staging server. @@ -115,6 +117,7 @@ def _add_build_to_xcarchive( xcprojpath: pathlib.Path, built_app_path: pathlib.Path, cfg: Config, + signing_config: str | None, ) -> pathlib.Path: archivepathbase = pathlib.Path(workdir, cfg.archive_name) archivepath = pathlib.Path(workdir, cfg.archive_name + '.xcarchive') @@ -124,6 +127,7 @@ def _add_build_to_xcarchive( print('Base archive not found; doing full build (can take a while)...') sys.stdout.flush() args = [ + 'tools/pcommand', 'xcodebuild', 'archive', '-project', @@ -136,6 +140,9 @@ def _add_build_to_xcarchive( str(archivepathbase), '-allowProvisioningUpdates', ] + if signing_config is not None: + args += ['-signingconfig', signing_config] + subprocess.run(args, check=True, capture_output=False) # Now copy our just-built app into the archive. @@ -154,6 +161,7 @@ def _export_ipa_from_xcarchive( exportoptionspath: pathlib.Path, ipa_dir_path: pathlib.Path, cfg: Config, + signing_config: str | None, ) -> pathlib.Path: import textwrap @@ -188,6 +196,7 @@ def _export_ipa_from_xcarchive( sys.stdout.flush() args = [ + 'tools/pcommand', 'xcodebuild', '-allowProvisioningUpdates', '-exportArchive', @@ -198,6 +207,8 @@ def _export_ipa_from_xcarchive( '-exportPath', str(ipa_dir_path), ] + if signing_config is not None: + args += ['-signingconfig', signing_config] try: subprocess.run(args, check=True, capture_output=True) except Exception: diff --git a/tools/efrotools/build.py b/tools/efrotools/lazybuild.py similarity index 51% rename from tools/efrotools/build.py rename to tools/efrotools/lazybuild.py index 6e2fd5c9..26998a43 100644 --- a/tools/efrotools/build.py +++ b/tools/efrotools/lazybuild.py @@ -5,17 +5,27 @@ from __future__ import annotations import os +import time import subprocess from pathlib import Path from typing import TYPE_CHECKING + +# Pylint's preferred import order here seems non-deterministic (as of 2.17.2). +# pylint: disable=useless-suppression +# pylint: disable=wrong-import-order from efro.terminal import Clr +from efrotools.buildlock import BuildLock +from efrotools import get_string_hash + +# pylint: enable=wrong-import-order +# pylint: enable=useless-suppression if TYPE_CHECKING: from typing import Callable -class Lazybuild: +class LazyBuildContext: """Run a build if anything in some category is newer than a target. This can be used as an optimization for build targets that *always* run. @@ -38,9 +48,11 @@ class Lazybuild: target: str, srcpaths: list[str], command: str, + buildlockname: str | None = None, dirfilter: Callable[[str, str], bool] | None = None, filefilter: Callable[[str, str], bool] | None = None, srcpaths_fullclean: list[str] | None = None, + manifest_file: str | None = None, command_fullclean: str | None = None, ) -> None: self.target = target @@ -48,6 +60,7 @@ class Lazybuild: self.command = command self.dirfilter = dirfilter self.filefilter = filefilter + self.buildlockname = buildlockname self.mtime = ( None if not os.path.exists(self.target) @@ -60,16 +73,17 @@ class Lazybuild: else: self.target_name_pretty = target - self.have_fullclean_changes = False self.have_changes = False + self.have_fullclean_changes = False self.total_unchanged_count = 0 + self.printed_trigger = False # We support a mechanism where some paths can be passed as 'fullclean' # paths - these will trigger a separate 'fullclean' command as well as # the regular command when any of them change. This is handy for 'meta' # type builds where a lot of tools scripts can conceivably influence # target creation, but where it would be unwieldy to list all of them - # as sources in a Makefile. + # as dependency relationships in a Makefile. self.srcpaths_fullclean = srcpaths_fullclean self.command_fullclean = command_fullclean if (self.srcpaths_fullclean is None) != ( @@ -80,72 +94,135 @@ class Lazybuild: ' command_fullclean together' ) + # We also support a 'manifest' file which is a hash of all filenames + # processed as part of srcpaths OR srcpaths_fullclean. If defined, + # any changes in this hash will result in the full-clean-command + # being run. This is useful for blowing away old output files when + # source files are removed. + self.manifest_file = manifest_file + def run(self) -> None: """Do the thing.""" + starttime = time.monotonic() - self._check_paths() + self._check_for_changes() + + if self.have_changes: + # If we were given a build-lock-name, surround our payload + # with a build-lock. This can be used as a sanity-check to make + # sure that only one build for some given purpose is being run at + # once. + if self.buildlockname is not None: + with BuildLock(self.buildlockname): + self._run_commands_and_update_target() + else: + self._run_commands_and_update_target() + + else: + duration = time.monotonic() - starttime + print( + f'{Clr.BLU}Lazybuild: skipping "{self.target_name_pretty}"' + f' (checked {self.total_unchanged_count} inputs' + f' in {duration:.2}s).{Clr.RST}' + ) + + def _run_commands_and_update_target(self) -> None: + assert self.have_changes if self.have_fullclean_changes: assert self.command_fullclean is not None print( - f'{Clr.MAG}Lazybuild: full-clean input changed;' - f' running {Clr.BLD}{self.command_fullclean}.{Clr.RST}' + f'{Clr.MAG}Lazybuild:' + f' {Clr.SCYN}{Clr.BLD}full-clean{Clr.RST}' + f'{Clr.MAG} input changed;' + f' running {Clr.BLD}{self.command_fullclean}.{Clr.RST}', + flush=True, ) subprocess.run(self.command_fullclean, shell=True, check=True) - if self.have_changes: - subprocess.run(self.command, shell=True, check=True) + subprocess.run(self.command, shell=True, check=True) - # Complain if the target path does not exist at this point. - # (otherwise we'd create an empty file there below which can - # cause problems). - # We make a special exception for files under .cache/lazybuild - # since those are not actually meaningful files; only used for - # dep tracking. - if not self.target.startswith( - '.cache/lazybuild' - ) and not os.path.isfile(self.target): - raise RuntimeError( - f'Expected output file \'{self.target}\' not found' - f' after running lazybuild command:' - f' \'{self.command}\'.' - ) - - # We also explicitly update the mod-time of the target; - # the command we (such as a VM build) may not have actually - # done anything but we still want to update our target to - # be newer than all the lazy sources. - os.makedirs(os.path.dirname(self.target), exist_ok=True) - Path(self.target).touch() - else: - print( - f'{Clr.BLU}Lazybuild: skipping "{self.target_name_pretty}"' - f' ({self.total_unchanged_count} inputs unchanged).{Clr.RST}' + # Complain if the target path does not exist at this point. + # (otherwise we'd create an empty file there below which can + # cause problems). + # We make a special exception for files under .cache/lazybuild + # since those are not actually meaningful files; only used for + # dep tracking. + if not self.target.startswith( + '.cache/lazybuild' + ) and not os.path.isfile(self.target): + raise RuntimeError( + f'Expected output file \'{self.target}\' not found' + f' after running lazybuild command:' + f' \'{self.command}\'.' ) - def _check_paths(self) -> None: + # We also explicitly update the mod-time of the target; + # the command we (such as a VM build) may not have actually + # done anything but we still want to update our target to + # be newer than all the lazy sources. + os.makedirs(os.path.dirname(self.target), exist_ok=True) + Path(self.target).touch() + + def _check_for_changes(self) -> None: + manfile = self.manifest_file + # If we're watching for file adds/removes/renames in addition + # to just modtimes, build a set of all files we come across. + man_input_paths = set[str]() if manfile is not None else None # First check our fullclean paths if we have them. # any changes here will kick off a full-clean and then a build. if self.srcpaths_fullclean is not None: for srcpath in self.srcpaths_fullclean: - src_did_change, src_unchanged_count = self._check_path(srcpath) + src_did_change, src_unchanged_count = self._check_path( + srcpath, man_input_paths + ) if src_did_change: - self.have_fullclean_changes = True self.have_changes = True - return # Can stop as soon as we find a change. + self.have_fullclean_changes = True + + # If we're *not* building a manifest + # we can bail on the first difference. + if manfile is None: + return self.total_unchanged_count += src_unchanged_count - # Ok; no fullclean changes found. Now check our regular paths. + # Now check our regular paths. # Any changes here just trigger a regular build. for srcpath in self.srcpaths: - src_did_change, src_unchanged_count = self._check_path(srcpath) + src_did_change, src_unchanged_count = self._check_path( + srcpath, man_input_paths + ) if src_did_change: self.have_changes = True - return # Can stop as soon as we find a change. + # If we're *not* building a manifest + # we can bail on the first difference. + if manfile is None: + return self.total_unchanged_count += src_unchanged_count - def _check_path(self, srcpath: str) -> tuple[bool, int]: + # If we built a manifest, check/write it and kick off + # a full-clean if anything differed. + if manfile is not None: + assert man_input_paths is not None + # Need to sort to keep this deterministic. + hashstr = get_string_hash('\n'.join(sorted(man_input_paths))) + try: + with open(manfile, encoding='utf-8') as infile: + existing_hash = infile.read() + except FileNotFoundError: + existing_hash = None + if hashstr != existing_hash: + # Manifest changed; write new one and mark us changed. + os.makedirs(os.path.dirname(manfile), exist_ok=True) + with open(manfile, 'w', encoding='utf-8') as outfile: + outfile.write(hashstr) + self.have_changes = True + self.have_fullclean_changes = True + + def _check_path( + self, srcpath: str, manifest_paths: set[str] | None + ) -> tuple[bool, int]: """Return whether path has changed and unchanged file count if not.""" unchanged_count = 0 @@ -155,6 +232,9 @@ class Lazybuild: return True, 0 unchanged_count += 1 return False, unchanged_count + + results: tuple[bool, int] | None = None + for root, dirnames, fnames in os.walk(srcpath, topdown=True): # In top-down mode we can modify dirnames in-place to # prevent recursing into them at all. @@ -178,9 +258,24 @@ class Lazybuild: raise RuntimeError(f'Invalid path with space: {fpath}') if self._test_path(fpath): - return True, 0 + results = (True, 0) + + # If we're not building a manifest we can bail + # immediately on a negative result. + if manifest_paths is None: + return results + + # Add files to any manifest set we're building. + if manifest_paths is not None: + manifest_paths.add(fpath) + unchanged_count += 1 - return False, unchanged_count + + # If we got here with no negatives, succeed. + if results is None: + results = (False, unchanged_count) + + return results def _default_dir_filter(self, root: str, dirname: str) -> bool: del root # Unused. @@ -205,14 +300,17 @@ class Lazybuild: return True def _test_path(self, path: str) -> bool: - # Now see this path is newer than our target.. - if self.mtime is None or os.path.getmtime(path) >= self.mtime: - print( - f'{Clr.MAG}Lazybuild: ' - f'{Clr.BLD}{self.target_name_pretty}{Clr.RST}{Clr.MAG}' - f' build' - f' triggered by change in {Clr.BLD}{path}{Clr.RST}{Clr.MAG}' - f'.{Clr.RST}' - ) + # Now see this path is newer than our target. + if self.mtime is None or os.path.getmtime(path) > self.mtime: + # Only announce trigger condition once. + if not self.printed_trigger: + self.printed_trigger = True + print( + f'{Clr.MAG}LazyBuildContext: ' + f'{Clr.BLD}{self.target_name_pretty}{Clr.RST}{Clr.MAG}' + f' build triggered by change in ' + f'{Clr.BLD}{path}{Clr.RST}{Clr.MAG}.{Clr.RST}', + flush=True, + ) return True return False diff --git a/tools/efrotools/makefile.py b/tools/efrotools/makefile.py index a7479a9f..dc839683 100644 --- a/tools/efrotools/makefile.py +++ b/tools/efrotools/makefile.py @@ -41,7 +41,6 @@ class Makefile: header_line_empty = '#' + ' ' * 78 + '#' def __init__(self, contents: str): - self.sections: list[Section] = [] self._original = copy.copy(contents) @@ -53,7 +52,6 @@ class Makefile: # First off, break into paragraphs (continuous sets of lines) plines: list[str] = [] for line in lines: - if line.strip() == '': if plines: paragraphs.append(Paragraph(contents='\n'.join(plines))) @@ -67,7 +65,6 @@ class Makefile: section = Section(name=None, paragraphs=[]) self.sections.append(section) for paragraph in paragraphs: - # Look for our very particular section headers and start # a new section whenever we come across one. plines = paragraph.contents.splitlines() diff --git a/tools/efrotools/pcommand.py b/tools/efrotools/pcommand.py index 7e8c2ede..9ddd0b47 100644 --- a/tools/efrotools/pcommand.py +++ b/tools/efrotools/pcommand.py @@ -139,7 +139,7 @@ def _spelling(words: list[str]) -> None: num_modded_dictionaries = 0 for fname in [ '.idea/dictionaries/ericf.xml', - 'ballisticacore-cmake/.idea/dictionaries/ericf.xml', + 'ballisticakit-cmake/.idea/dictionaries/ericf.xml', ]: if not os.path.exists(fname): continue @@ -201,7 +201,7 @@ def spelling() -> None: def xcodebuild() -> None: """Run xcodebuild with added smarts.""" - from efrotools.xcode import XCodeBuild + from efrotools.xcodebuild import XCodeBuild XCodeBuild(projroot=str(PROJROOT), args=sys.argv[2:]).run() @@ -211,7 +211,7 @@ def xcoderun() -> None: import os import subprocess from efro.error import CleanError - from efrotools.xcode import project_build_path + from efrotools.xcodebuild import project_build_path if len(sys.argv) != 5: raise CleanError( @@ -276,9 +276,10 @@ def check_clean_safety() -> None: """ import os import subprocess + from efro.error import CleanError if len(sys.argv) != 2: - raise Exception('invalid arguments') + raise CleanError('invalid arguments') # Make sure we wouldn't be deleting anything not tracked by git # or ignored. @@ -286,15 +287,13 @@ def check_clean_safety() -> None: ['git', 'status', '--porcelain=v2'] ).decode() if any(line.startswith('?') for line in output.splitlines()): - print( - 'ERROR: untracked file(s) found; aborting.' + raise CleanError( + 'Untracked file(s) found; aborting.' ' (see "git status" from "' + os.getcwd() + '") Either \'git add\' them, add them to .gitignore,' - ' or remove them and try again.', - file=sys.stderr, + ' or remove them and try again.' ) - sys.exit(255) def gen_empty_py_init() -> None: @@ -491,11 +490,21 @@ def tool_config_install() -> None: def _filter_tool_config(cfg: str) -> str: import textwrap + from efrotools import getconfig # Stick project-root wherever they want. cfg = cfg.replace('__EFRO_PROJECT_ROOT__', str(PROJROOT)) + # Stick a colon-separated list of project Python paths wherever they want. + name = '__EFRO_PYTHON_PATHS__' + if name in cfg: + pypaths = getconfig(PROJROOT).get('python_paths') + if pypaths is None: + raise RuntimeError('python_paths not set in project config') + assert not any(' ' in p for p in pypaths) + cfg = cfg.replace(name, ':'.join(f'{PROJROOT}/{p}' for p in pypaths)) + # Short project name. short_names = { 'ballistica-internal': 'ba-i', @@ -530,6 +539,9 @@ def _filter_tool_config(cfg: str) -> str: strict_equality = True local_partial_types = True no_implicit_reexport = True + + enable_error_code = redundant-expr, truthy-bool, \ +truthy-function, unused-awaitable """ ).strip() @@ -545,7 +557,7 @@ def _filter_tool_config(cfg: str) -> str: name, '(' + ' '.join(f'"{b}"' for b in bargs[3:]) + ')' ) - # Gen a pylint init to set up our python paths: + # Gen a pylint init hook which sets up our python paths. pylint_init_tag = '__EFRO_PYLINT_INIT__' if pylint_init_tag in cfg: pypaths = getconfig(PROJROOT).get('python_paths') @@ -644,14 +656,13 @@ def compile_python_files() -> None: the built-in scripts directly (or go through the asset build system which properly recreates the .pyc files). """ - import os import py_compile for arg in sys.argv[2:]: mode = py_compile.PycInvalidationMode.UNCHECKED_HASH py_compile.compile( arg, - dfile=os.path.basename(arg), + # dfile=os.path.basename(arg), doraise=True, optimize=1, invalidation_mode=mode, @@ -729,7 +740,6 @@ def makefile_target_list() -> None: entries: list[_Entry] = [] for i, line in enumerate(lines): - # Targets. if ( ':' in line @@ -818,3 +828,118 @@ def urandom_pretty() -> None: bstr = '\n'.join(str(l) for l in lines) print(f'({bstr})') + + +def tweak_empty_py_files() -> None: + """Find any zero-length Python files and make them length 1.""" + from efro.error import CleanError + import efrotools.pybuild + + if len(sys.argv) != 3: + raise CleanError('Expected exactly 1 path arg.') + efrotools.pybuild.tweak_empty_py_files(sys.argv[2]) + + +def make_ensure() -> None: + """Make sure a makefile target is up-to-date. + + This can technically be done by simply `make --question`, but this + has some extra bells and whistles such as printing some of the commands + that would run. + Can be useful to run after cloud-builds to ensure the local results + consider themselves up-to-date. + """ + # pylint: disable=too-many-locals + from efro.error import CleanError + from efro.terminal import Clr + import subprocess + + dirpath: str | None = None + args = sys.argv[2:] + if '--dir' in args: + argindex = args.index('--dir') + dirpath = args[argindex + 1] + del args[argindex : argindex + 2] + + if len(args) not in (0, 1): + raise CleanError('Expected zero or one target args.') + target = args[0] if args else None + + cmd = ['make', '--no-print-directory', '--dry-run'] + if target is not None: + cmd.append(target) + results = subprocess.run(cmd, check=False, capture_output=True, cwd=dirpath) + out = results.stdout.decode() + err = results.stderr.decode() + if results.returncode != 0: + print(f'Failed command stdout:\n{out}\nFailed command stderr:\n{err}') + raise CleanError(f"Command failed during make_ensure: '{cmd}'.") + + targetname: str = '' if target is None else target + lines = out.splitlines() + in_str = '' if dirpath is None else f"in directory '{dirpath}' " + if len(lines) == 1 and 'Nothing to be done for ' in lines[0]: + print(f"make_ensure: '{targetname}' target {in_str}is up to date.") + else: + maxlines = 20 + if len(lines) > maxlines: + outlines = '\n'.join( + lines[:maxlines] + [f'(plus {len(lines)-maxlines} more lines)'] + ) + else: + outlines = '\n'.join(lines) + + print( + f"make_ensure: '{targetname}' target {in_str}" + f'is out of date; would run:\n\n' + '-------------------------- MAKE-ENSURE COMMANDS BEGIN ' + f'--------------------------\n{Clr.YLW}' + f'{outlines}{Clr.RST}\n' + '--------------------------- MAKE-ENSURE COMMANDS END ' + '---------------------------\n' + ) + raise CleanError( + f"make_ensure: '{targetname}' target {in_str}is out of date." + ) + + +def make_target_debug() -> None: + """Debug makefile src/target mod times given src and dst path. + + Built to debug stubborn Makefile targets that insist on being + rebuilt just after being built via a cloud target. + """ + import os + import datetime + + from efro.error import CleanError + + # from efro.util import ago_str, utc_now + + args = sys.argv[2:] + if len(args) != 2: + raise CleanError('Expected 2 args.') + + def _utc_mod_time(path: str) -> datetime.datetime: + mtime = os.path.getmtime(path) + mdtime = datetime.datetime.fromtimestamp(mtime, datetime.timezone.utc) + # mdtime.replace(tzinfo=datetime.timezone.utc) + return mdtime + + # srcname = os.path.basename(args[0]) + # dstname = os.path.basename(args[1]) + srctime = _utc_mod_time(args[0]) + dsttime = _utc_mod_time(args[1]) + # now = utc_now() + # src_ago = ago_str(srctime, maxparts=3, decimals=2, now=now) + # dst_ago = ago_str(dsttime, maxparts=3, decimals=2, now=now) + srctimestr = ( + f'{srctime.hour}:{srctime.minute}:{srctime.second}:' + f'{srctime.microsecond}' + ) + dsttimestr = ( + f'{dsttime.hour}:{dsttime.minute}:{dsttime.second}:' + f'{dsttime.microsecond}' + ) + print(f'SRC modified at {srctimestr}.') + print(f'DST modified at {dsttimestr}.') diff --git a/tools/efrotools/pcommand2.py b/tools/efrotools/pcommand2.py new file mode 100644 index 00000000..ecb99ff6 --- /dev/null +++ b/tools/efrotools/pcommand2.py @@ -0,0 +1,31 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Standard snippets that can be pulled into project pcommand scripts. + +A snippet is a mini-program that directly takes input from stdin and does +some focused task. This module is a repository of common snippets that can +be imported into projects' pcommand script for easy reuse. +""" +from __future__ import annotations + +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + pass + + +def with_build_lock() -> None: + """Run a shell command wrapped in a build-lock.""" + from efro.error import CleanError + from efrotools.buildlock import BuildLock + + import subprocess + + args = sys.argv[2:] + if len(args) < 2: + raise CleanError( + 'Expected one lock-name arg and at least one command arg' + ) + with BuildLock(args[0]): + subprocess.run(' '.join(args[1:]), check=True, shell=True) diff --git a/tools/efrotools/pybuild.py b/tools/efrotools/pybuild.py index 3ac5a137..25171cfa 100644 --- a/tools/efrotools/pybuild.py +++ b/tools/efrotools/pybuild.py @@ -2,10 +2,13 @@ # """Functionality related to building python for ios, android, etc.""" +# pylint: disable=too-many-lines from __future__ import annotations import os import subprocess +from enum import Enum +from dataclasses import dataclass from typing import TYPE_CHECKING from efrotools import readfile, writefile, replace_exact @@ -15,14 +18,27 @@ if TYPE_CHECKING: # Python version we build here (not necessarily same as we use in repo). PY_VER_ANDROID = '3.11' -PY_VER_EXACT_ANDROID = '3.11.0' -PY_VER_APPLE = '3.10' -PY_VER_EXACT_APPLE = '3.10.5' +PY_VER_EXACT_ANDROID = '3.11.3' +PY_VER_APPLE = '3.11' +PY_VER_EXACT_APPLE = '3.11.3' + +# I occasionally run into openssl problems (particularly on arm systems) +# so keeping exact control of the versions we're building here to try +# and minimize it. +# Earlier I ran into an issue with android builds testing while OpenSSL +# was probing for ARMV7_TICK instruction presence (see android_patch_ssl here), +# and more recently I'm seeing a similar thing in 3.1.0 with arm_v8_sve_probe +# on mac. Ugh. +# See https://stackoverflow.com/questions/74059978/ +# why-is-lldb-generating-exc-bad-instruction-with-user-compiled-library-on-macos +OPENSSL_VER_APPLE = '3.0.8' +OPENSSL_VER_ANDROID = '3.0.8' -ANDROID_PYTHON_REPO = 'https://github.com/GRRedWings/python3-android' # Filenames we prune from Python lib dirs in source repo to cut down on size. PRUNE_LIB_NAMES = [ + 'msilib', + '__phello__', 'config-*', 'idlelib', 'lib-dynload', @@ -44,6 +60,8 @@ PRUNE_LIB_NAMES = [ 'ctypes/test', 'imaplib.py', '_sysconfigdata_*', + 'ctypes/macholib/fetch_macholib*', + 'ctypes/macholib/README.ctypes', ] # Same but for DLLs dir (windows only) @@ -61,17 +79,20 @@ def build_apple(arch: str, debug: bool = False) -> None: # (via brew remove gettext --ignore-dependencies) # NOTE: Should check to see if this is still necessary on Apple silicon # since homebrew stuff is no longer in /usr/local there. - if ( - 'MacBook-Fro' in platform.node() - and os.environ.get('SKIP_GETTEXT_WARNING') != '1' - ): + if bool(False): if ( - subprocess.run('which gettext', shell=True, check=False).returncode - == 0 + 'MacBook-Fro' in platform.node() + and os.environ.get('SKIP_GETTEXT_WARNING') != '1' ): - raise CleanError( - 'NEED TO TEMP-KILL GETTEXT (or set SKIP_GETTEXT_WARNING=1)' - ) + if ( + subprocess.run( + 'which gettext', shell=True, check=False + ).returncode + == 0 + ): + raise CleanError( + 'NEED TO TEMP-KILL GETTEXT (or set SKIP_GETTEXT_WARNING=1)' + ) builddir = 'build/python_apple_' + arch + ('_debug' if debug else '') subprocess.run(['rm', '-rf', builddir], check=True) @@ -92,14 +113,16 @@ def build_apple(arch: str, debug: bool = False) -> None: # broke in the underlying build even on old commits so keeping it # locked for now... # run('git checkout bf1ed73d0d5ff46862ba69dd5eb2ffaeff6f19b6') + + # Grab the branch corresponding to our target python version. subprocess.run(['git', 'checkout', PY_VER_APPLE], check=True) txt = readfile('Makefile') # Turn doc strings on; looks like it only adds a few hundred k. - txt = replace_exact( - txt, '--without-doc-strings', '--with-doc-strings', count=2 - ) + # txt = replace_exact( + # txt, '--without-doc-strings', '--with-doc-strings', count=2 + # ) # Customize our minimum version requirements txt = replace_exact( @@ -117,57 +140,88 @@ def build_apple(arch: str, debug: bool = False) -> None: 'VERSION_MIN-tvOS=9.0\n', 'VERSION_MIN-tvOS=9.0\n', ) + txt = replace_exact( + txt, + 'OPENSSL_VERSION=3.1.0\n', + f'OPENSSL_VERSION={OPENSSL_VER_APPLE}\n', + ) + + # Don't copy in lib-dynload; we don't make it so it errors if we try. + txt = replace_exact( + txt, + '\t$$(foreach sdk,$$(SDKS-$(os)),' + 'cp $$(PYTHON_FATSTDLIB-$$(sdk))/lib-dynload/*', + '\t# (ericf disabled) $$(foreach sdk,$$(SDKS-$(os)),' + 'cp $$(PYTHON_FATSTDLIB-$$(sdk))/lib-dynload/*', + count=1, + ) assert '--with-pydebug' not in txt if debug: - # Add debug build flag txt = replace_exact( txt, - '--enable-ipv6 --without-ensurepip ', - '--enable-ipv6 --with-pydebug --without-ensurepip ', + '\t\t\t--enable-ipv6 \\\n', + '\t\t\t--enable-ipv6 \\\n\t\t\t--with-pydebug \\\n', count=2, ) # Debug lib has a different name. txt = replace_exact( - txt, 'python$(PYTHON_VER).a', 'python$(PYTHON_VER)d.a', count=2 + txt, + '))/lib/libpython$(PYTHON_VER).a', + '))/lib/libpython$(PYTHON_VER)d.a', + count=2, ) txt = replace_exact( txt, '/include/python$(PYTHON_VER)', '/include/python$(PYTHON_VER)d', - count=4, + count=3, + ) + txt = replace_exact( + txt, '/config-$(PYTHON_VER)-', '/config-$(PYTHON_VER)d-', count=3 + ) + txt = replace_exact( + txt, '/_sysconfigdata__', '/_sysconfigdata_d_', count=6 ) - # Inject our custom modifications to fire right after their normal - # Setup.local filtering and right before building (and pass the same - # 'slice' value they use so we can use it too). - # txt = replace_exact( - # txt, '\t\t\tsed -e "s/{{slice}}/$$(SLICE-$$(SDK-$(target)))/g" \\\n' - # '\t\t\t> $$(PYTHON_DIR-$(target))/Modules/Setup.local\n', - # '\t\t\tsed -e "s/{{slice}}/$$(SLICE-$$(SDK-$(target)))/g" \\\n' - # '\t\t\t> $$(PYTHON_DIR-$(target))/Modules/Setup.local\n' - # '\tcd $$(PYTHON_DIR-$(target)) && ' - # f'../../../../../tools/pcommand python_apple_patch {arch} ' - # '"$$(SLICE-$$(SDK-$(target)))"\n') - # txt = replace_exact( - # txt, '\t\t\tsed -e "s/{{slice}}/$$(SLICE-macosx)/g" \\\n' - # '\t\t\t> $$(PYTHON_DIR-$(os))/Modules/Setup.local\n', - # '\t\t\tsed -e "s/{{slice}}/$$(SLICE-macosx)/g" \\\n' - # '\t\t\t> $$(PYTHON_DIR-$(os))/Modules/Setup.local\n' - # '\tcd $$(PYTHON_DIR-$(os)) && ' - # f'../../../../../tools/pcommand python_apple_patch {arch} ' - # '"$$(SLICE-macosx)"\n') - # txt = replace_exact( - # txt, - # ' # Configure target Python\n', - # ' # Configure target Python\n' - # f'\t../../../../../tools/pcommand python_apple_patch' - # f'{arch} wtfslice\n', - # count=2, - # ) + # Rename the patch files corresponding to these as well. + patchpaths = [ + os.path.join('patch/Python', n) + for n in os.listdir('patch/Python') + if n.startswith('_sysconfigdata__') + ] + for path in patchpaths: + subprocess.run( + [ + 'mv', + path, + path.replace('_sysconfigdata__', '_sysconfigdata_d_'), + ], + check=True, + ) + + # Add our bit of patching right after standard patching. + for tword in ('target', 'sdk'): + txt = replace_exact( + txt, + ( + '\t# Apply target Python patches\n' + f'\tcd $$(PYTHON_SRCDIR-$({tword})) && ' + 'patch -p1 < $(PROJECT_DIR)/patch/Python/Python.patch\n' + ), + ( + '\t# Apply target Python patches\n' + f'\tcd $$(PYTHON_SRCDIR-$({tword})) && ' + 'patch -p1 < $(PROJECT_DIR)/patch/Python/Python.patch\n' + f'\t/opt/homebrew/opt/python@3.11/bin/python3.11' + ' ../../tools/pcommand python_apple_patch' + f' $$(PYTHON_SRCDIR-$({tword}))\n' + ), + count=1, + ) writefile('Makefile', txt) # Ok; let 'er rip. @@ -197,7 +251,12 @@ def build_android(rootdir: str, arch: str, debug: bool = False) -> None: subprocess.run(['rm', '-rf', builddir], check=True) subprocess.run(['mkdir', '-p', 'build'], check=True) subprocess.run( - ['git', 'clone', ANDROID_PYTHON_REPO, builddir], + [ + 'git', + 'clone', + 'https://github.com/GRRedWings/python3-android', + builddir, + ], check=True, ) os.chdir(builddir) @@ -221,7 +280,16 @@ def build_android(rootdir: str, arch: str, debug: bool = False) -> None: ftxt, ' ' 'BZip2, GDBM, LibFFI, LibUUID, OpenSSL, Readline, SQLite, XZ, ZLib,\n', - ' BZip2, LibUUID, OpenSSL, SQLite, XZ, ZLib,\n', + ' BZip2, LibFFI, LibUUID, OpenSSL, SQLite, XZ, ZLib,\n', + ) + + # Set specific OpenSSL version. + ftxt = replace_exact( + ftxt, + "source = 'https://www.openssl.org/source/openssl-3.0.7.tar.gz'", + f"source = 'https://www.openssl.org/" + f"source/openssl-{OPENSSL_VER_ANDROID}.tar.gz'", + count=1, ) # Give ourselves a handle to patch the OpenSSL build. @@ -261,25 +329,325 @@ def build_android(rootdir: str, arch: str, debug: bool = False) -> None: print('python build complete! (android/' + arch + ')') -def apple_patch(arch: str, slc: str) -> None: - """Run necessary patches on an apple archive before building.""" +def apple_patch(python_dir: str) -> None: + """New test.""" + patch_modules_setup(python_dir, 'apple') - # Here's the deal: we want our custom static python libraries to - # be as similar as possible on apple platforms and android, so let's - # blow away all the tweaks that this setup does to Setup.local and - # instead apply our very similar ones directly to Setup, just as we - # do for android. - # with open('Modules/Setup.local', 'w', encoding='utf-8') as outfile: - # outfile.write('# cleared by efrotools build\n') - _patch_setup_file('apple', arch, slc) - _patch_py_ssl() +def patch_modules_setup(python_dir: str, baseplatform: str) -> None: + """Muck with the Setup.* files Python uses to build modules.""" + # pylint: disable=too-many-locals + + assert ' ' not in python_dir + + # Use the shiny new Setup.stdlib setup (Sounds like this will be default + # in the future?). It looks like by mucking with Setup.stdlib.in we can + # make pretty minimal changes to get the results we want without having + # to inject platform-specific linker flags and whatnot like we had to + # previously. + subprocess.run( + f'cd {python_dir}/Modules && ln -sf ./Setup.stdlib ./Setup.local', + shell=True, + check=True, + ) + + # Edit the inputs for that shiny new setup. + fname = os.path.join(python_dir, 'Modules', 'Setup.stdlib.in') + ftxt = readfile(fname) + + # Start by flipping everything to hard-coded static. + ftxt = replace_exact( + ftxt, + '*@MODULE_BUILDTYPE@*', + '*static*', + count=1, + ) + + # This list should contain all possible compiled modules to start. + # If any .so files are coming out of builds or anything unrecognized + # is showing up in the final Setup.local or the build, add it here. + # TODO(ericf): could automate a warning for at least the last part + # of that. + cmodules = { + '_asyncio', + '_bisect', + '_blake2', + '_codecs_cn', + '_codecs_hk', + '_codecs_iso2022', + '_codecs_jp', + '_codecs_kr', + '_codecs_tw', + '_contextvars', + '_crypt', + '_csv', + '_ctypes_test', + '_curses_panel', + '_curses', + '_datetime', + '_decimal', + '_gdbm', + '_dbm', + '_elementtree', + '_heapq', + '_json', + '_lsprof', + '_lzma', + '_md5', + '_multibytecodec', + '_multiprocessing', + '_opcode', + '_pickle', + '_posixsubprocess', + '_posixshmem', + '_queue', + '_random', + '_sha1', + '_sha3', + '_sha256', + '_sha512', + '_socket', + '_statistics', + '_struct', + '_testbuffer', + '_testcapi', + '_testimportmultiple', + '_testinternalcapi', + '_testmultiphase', + '_testclinic', + '_uuid', + '_xxsubinterpreters', + '_xxtestfuzz', + 'spwd', + '_zoneinfo', + 'array', + 'audioop', + 'binascii', + 'cmath', + 'fcntl', + 'grp', + 'math', + '_tkinter', + 'mmap', + 'ossaudiodev', + 'pyexpat', + 'resource', + 'select', + 'nis', + 'syslog', + 'termios', + 'unicodedata', + 'xxlimited', + 'xxlimited_35', + 'zlib', + 'readline', + } + + # The set of modules we want statically compiled into our Python lib. + enables = { + '_asyncio', + 'array', + 'cmath', + 'math', + '_contextvars', + '_struct', + '_random', + '_elementtree', + '_pickle', + '_datetime', + '_zoneinfo', + '_bisect', + '_heapq', + '_json', + '_ctypes', + '_statistics', + 'unicodedata', + 'fcntl', + 'select', + 'mmap', + '_csv', + '_socket', + '_sha1', + '_sha3', + '_sha256', + '_sha512', + '_blake2', + '_lzma', + 'binascii', + '_posixsubprocess', + 'zlib', + } + + # Muck with things in line form for a bit. + lines = ftxt.splitlines() + + disable_at_end = set[str]() + + for cmodule in cmodules: + linebegin = f'@MODULE_{cmodule.upper()}_TRUE@' + indices = [i for i, val in enumerate(lines) if linebegin in val] + if len(indices) != 1: + raise RuntimeError( + f'Expected to find exact 1 entry for {cmodule};' + f' found {len(indices)}.' + ) + line = lines[indices[0]] + + is_enabled = not line.startswith('#') + should_enable = cmodule in enables + + if not should_enable: + # If something is enabled but we don't want it, comment it out. + # Also stick all disabled stuff in a *disabled* section at the + # bottom so it won't get built even as shared. + if is_enabled: + lines[indices[0]] = f'#{line}' + disable_at_end.add(cmodule) + elif not is_enabled: + # Ok; its enabled and shouldn't be. What to do... + if bool(False): + # Uncomment the line to enable it. + # UPDATE: Seems this doesn't work; will have to figure out + # the right way to get things like _ctypes compiling + # statically. + lines[indices[0]] = replace_exact( + line, f'#{linebegin}', linebegin, count=1 + ) + else: + # Don't support this currently. + raise RuntimeError( + f'UNEXPECTED is_enabled=False' + f' should_enable=True for {cmodule}' + ) + + ftxt = '\n'.join(lines) + '\n' + + # ftxt = replace_exact( + # ftxt, + # '# Some testing modules MUST be built as shared libraries.\n' + # '*shared*\n', + # '# Some testing modules MUST be built as shared libraries.\n' + # '#*static*\n', + # ) + + # Now for the one remaining ULTRA-HACKY bit: getting _ctypes building + # statically. + # It seems that as of 3.11 it is not possible to do this through + # Setup.stdlib.in like we do with our other stuff, as the line is + # commented out and we get errors if we change that. Looks like + # its not commented out anymore in 3.12 so we'll have to revisit + # this next year, but for now... hacks! + + # To get this working for now we can dump an exact set of build flags + # for _ctypes into the Setup.stdlib.in. We just need to provide those + # per-platform. So, to get those: + # - Run 'vanilla' ios/android/etc Python builds which generate .so versions + # - Look for the flags in the compiler command lines for those .c files + # - Stuff those flags in here for the associated platform. + if baseplatform == 'android': + ftxt = replace_exact( + ftxt, + '#@MODULE__CTYPES_TRUE@_ctypes _ctypes/_ctypes.c' + ' _ctypes/callbacks.c' + ' _ctypes/callproc.c _ctypes/stgdict.c _ctypes/cfield.c\n', + '_ctypes _ctypes/_ctypes.c' + ' _ctypes/callbacks.c' + ' _ctypes/callproc.c _ctypes/stgdict.c _ctypes/cfield.c' + ' -I$(srcdir)/Android/sysroot/usr/include' + ' -L$(srcdir)/Android/sysroot/usr/lib' + ' -DHAVE_FFI_PREP_CIF_VAR=1 -DHAVE_FFI_PREP_CLOSURE_LOC=1' + ' -DHAVE_FFI_CLOSURE_ALLOC=1' + ' -lffi' + '\n', + ) + elif baseplatform == 'apple' and python_dir.split('/')[-2] == 'macosx': + ftxt = replace_exact( + ftxt, + '#@MODULE__CTYPES_TRUE@_ctypes _ctypes/_ctypes.c' + ' _ctypes/callbacks.c' + ' _ctypes/callproc.c _ctypes/stgdict.c _ctypes/cfield.c\n', + '_ctypes _ctypes/_ctypes.c _ctypes/callbacks.c' + ' _ctypes/callproc.c _ctypes/stgdict.c _ctypes/cfield.c' + ' _ctypes/malloc_closure.c' + ' -I_ctypes/darwin -I/Applications/Xcode.app/Contents/Developer' + '/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/' + 'usr/include/ffi' + ' -DUSING_MALLOC_CLOSURE_DOT_C=1 -DMACOSX' + ' -DUSING_APPLE_OS_LIBFFI=1 -DHAVE_FFI_PREP_CIF_VAR=1' + ' -DHAVE_FFI_PREP_CLOSURE_LOC=1 -DHAVE_FFI_CLOSURE_ALLOC=1' + ' -lffi' + '\n', + ) + elif baseplatform == 'apple': + arch = python_dir.split('/')[-2] + if '.' not in arch or arch.split('.')[0] not in { + 'iphoneos', + 'iphonesimulator', + 'appletvos', + 'appletvsimulator', + }: + raise RuntimeError( + f'Unable to determine apple platform; got {arch}' + ) + archbase = arch.split('.')[0] + archos = 'iOS' if 'iphone' in archbase else 'tvOS' + + ftxt = replace_exact( + ftxt, + '#@MODULE__CTYPES_TRUE@_ctypes _ctypes/_ctypes.c' + ' _ctypes/callbacks.c' + ' _ctypes/callproc.c _ctypes/stgdict.c _ctypes/cfield.c\n', + '_ctypes _ctypes/_ctypes.c _ctypes/callbacks.c' + ' _ctypes/callproc.c _ctypes/stgdict.c _ctypes/cfield.c' + ' _ctypes/malloc_closure.c' + ' -I_ctypes/darwin' + f' -I$(srcdir)/../../../../merge/{archos}' + f'/{archbase}/libffi-3.4.2/include' + f' -L$(srcdir)/../../../../merge/{archos}' + f'/{archbase}/libffi-3.4.2/lib' + ' -DUSING_MALLOC_CLOSURE_DOT_C' + ' -DHAVE_FFI_PREP_CIF_VAR=1' + ' -DHAVE_FFI_PREP_CLOSURE_LOC=1 -DHAVE_FFI_CLOSURE_ALLOC=1' + ' -lffi' + '\n', + ) + else: + raise RuntimeError(f'Invalid platform {baseplatform}') + + # There is one last hacky bit, which is a holdover from previous years. + # Seems makesetup still has a bug where *any* line containing an equals + # gets interpreted as a global DEF instead of a target, which means our + # custom _ctypes lines above get ignored. Ugh. + # To fix it we need to revert the *=* case to what it apparently used to + # be: [A-Z]*=*. I wonder why this got changed and how has it not broken + # tons of stuff? Maybe I'm missing something. + fname2 = os.path.join(python_dir, 'Modules', 'makesetup') + ftxt2 = readfile(fname2) + ftxt2 = replace_exact( + ftxt2, + ' *=*) DEFS="$line$NL$DEFS"; continue;;', + ' [A-Z]*=*) DEFS="$line$NL$DEFS"; continue;;', + ) + assert ftxt2.count('[A-Z]*=*') == 1 + writefile(fname2, ftxt2) + + # Explicitly mark the remaining ones as disabled + # (so Python won't try to build them as dynamic libs). + remaining_disabled = ' '.join(sorted(disable_at_end)) + ftxt += ( + '\n# Disabled by efrotools build:\n' + '*disabled*\n' + f'{remaining_disabled}\n' + ) + + writefile(fname, ftxt) def android_patch() -> None: """Run necessary patches on an android archive before building.""" - _patch_setup_file('android', '?', '?') - _patch_py_ssl() + patch_modules_setup('.', 'android') + # _patch_setup_file('android', '?', '?') + # _patch_py_ssl() def android_patch_ssl() -> None: @@ -307,7 +675,7 @@ def android_patch_ssl() -> None: writefile(fname, txt) # Update: looks like this might have been disabled by default for - # newer SSL builds used by 3.11+ + # newer SSL builds used by 3.11+; can remove this if it seems stable. if bool(False): # Getting a lot of crashes in _armv7_tick, which seems to be a # somewhat known issue with certain arm7 devices. Sounds like @@ -337,7 +705,6 @@ def android_patch_ssl() -> None: def _patch_py_ssl() -> None: - # UPDATE: this is now included in Python as of 3.10.6; woohoo! if bool(True): return @@ -353,8 +720,8 @@ def _patch_py_ssl() -> None: # so slow and if there's anything we can do about that. # UPDATE: This should be fixed in Python itself as of 3.10.6 # (see https://github.com/python/cpython/issues/94637) - # UPDATE 2: Have confirmed that call is expected to be slow in some - # situations. + # UPDATE 2: Have also confirmed that call is expected to be slow in + # some situations. fname = 'Modules/_ssl.c' txt = readfile(fname) txt = replace_exact( @@ -371,260 +738,18 @@ def _patch_py_ssl() -> None: writefile(fname, txt) -def _patch_setup_file(platform: str, arch: str, slc: str) -> None: - # pylint: disable=too-many-locals - # pylint: disable=too-many-statements - - fname = 'Modules/Setup' - ftxt = readfile(fname) - - if platform == 'android': - prefix = '$(srcdir)/Android/sysroot/usr' - uuid_ex = f' -L{prefix}/lib -luuid' - uuid_ex += f' -I{prefix}/include/uuid' # Testing - zlib_ex = f' -I{prefix}/include -L{prefix}/lib -lz' - bz2_ex = f' -I{prefix}/include -L{prefix}/lib -lbz2' - ssl_ex = f' -DUSE_SSL -I{prefix}/include -L{prefix}/lib -lssl -lcrypto' - sqlite_ex = f' -I{prefix}/include -L{prefix}/lib' - hash_ex = ' -DUSE_SSL -lssl -lcrypto' - lzma_ex = ' -llzma' - elif platform == 'apple': - # These should basically match what the Python-Apple-support dist - # does in its patch/Python/Setup.embedded. - # (We do our own thing and ignore the Setup.local it generates - # for the sake of cross-platform consistency, but still need to - # do what we do the same way they do what they do) - - def _slrp(val: str) -> str: - # In the distro, the Makefile does this filtering to Setup.local - return val.replace('{{slice}}', slc) - - uuid_ex = '' - zlib_ex = ' -I$(prefix)/include -lz' - bz2_ex = _slrp( - ' -I$(srcdir)/../Support/BZip2.xcframework/{{slice}}/Headers' - ' -L$(srcdir)/../Support/BZip2.xcframework/{{slice}} -lbzip2' - ) - ssl_ex = _slrp( - ' -I$(srcdir)/../Support/OpenSSL.xcframework/{{slice}}/Headers' - ' -L$(srcdir)/../Support/OpenSSL.xcframework/{{slice}}' - ' -lOpenSSL -DUSE_SSL' - ) - sqlite_ex = ' -I$(srcdir)/Modules/_sqlite' - hash_ex = _slrp( - ' -I$(srcdir)/../Support/OpenSSL.xcframework/{{slice}}/Headers' - ' -L$(srcdir)/../Support/OpenSSL.xcframework/{{slice}}' - ' -lOpenSSL -DUSE_SSL' - ) - lzma_ex = _slrp( - ' -I$(srcdir)/../Support/XZ.xcframework/{{slice}}/Headers' - ' -L$(srcdir)/../Support/XZ.xcframework/{{slice}} -lxz' - ) - else: - raise RuntimeError(f'Unknown platform {platform}') - - # This list should contain all possible compiled modules to start. - # If any .so files are coming out of builds, their names should be - # added here to stop that. - cmodules = [ - '_asyncio', - '_bisect', - '_blake2', - '_codecs_cn', - '_codecs_hk', - '_codecs_iso2022', - '_codecs_jp', - '_codecs_kr', - '_codecs_tw', - '_contextvars', - '_crypt', - '_csv', - '_ctypes_test', - '_ctypes', - '_curses_panel', - '_curses', - '_datetime', - '_decimal', - '_elementtree', - '_heapq', - '_json', - '_lsprof', - '_lzma', - '_md5', - '_multibytecodec', - '_multiprocessing', - '_opcode', - '_pickle', - '_posixsubprocess', - '_queue', - '_random', - '_sha1', - '_sha3', - '_sha256', - '_sha512', - '_socket', - '_statistics', - '_struct', - '_testbuffer', - '_testcapi', - '_testimportmultiple', - '_testinternalcapi', - '_testmultiphase', - '_uuid', - '_xxsubinterpreters', - '_xxtestfuzz', - '_zoneinfo', - 'array', - 'audioop', - 'binascii', - 'cmath', - 'fcntl', - 'grp', - 'math', - 'mmap', - 'ossaudiodev', - 'parser', - 'pyexpat', - 'resource', - 'select', - 'syslog', - 'termios', - 'unicodedata', - 'xxlimited', - 'zlib', - ] - - # Selectively uncomment some existing modules for static compilation. - enables = [ - '_asyncio', - 'array', - 'cmath', - 'math', - '_contextvars', - '_struct', - '_random', - '_elementtree', - '_pickle', - '_datetime', - '_zoneinfo', - '_bisect', - '_heapq', - '_json', - '_statistics', - 'unicodedata', - 'fcntl', - 'select', - 'mmap', - '_csv', - '_socket', - '_sha3', - '_blake2', - 'binascii', - '_posixsubprocess', - ] - # Note that the _md5 and _sha modules are normally only built if the - # system does not have the OpenSSL libs containing an optimized - # version. - if bool(False): - enables += ['_md5'] - - for enable in enables: - ftxt = replace_exact(ftxt, f'#{enable} ', f'{enable} ') - cmodules.remove(enable) - - # Disable ones that were enabled: - disables = ['xxsubtype'] - for disable in disables: - ftxt = replace_exact(ftxt, f'\n{disable} ', f'\n#{disable} ') - - # Additions: - ftxt += '\n# Additions by efrotools:\n' - - if bool(True): - ftxt += f'_uuid _uuidmodule.c{uuid_ex}\n' - cmodules.remove('_uuid') - - ftxt += f'zlib zlibmodule.c{zlib_ex}\n' - - cmodules.remove('zlib') - - # Why isn't this getting built as a shared lib by default? - # Do we need it for sure? - ftxt += f'_hashlib _hashopenssl.c{hash_ex}\n' - - ftxt += f'_lzma _lzmamodule.c{lzma_ex}\n' - cmodules.remove('_lzma') - - ftxt += f'_bz2 _bz2module.c{bz2_ex}\n' - - ftxt += f'_ssl _ssl.c{ssl_ex}\n' - - ftxt += ( - f'_sqlite3' - f' _sqlite/blob.c' - f' _sqlite/connection.c' - f' _sqlite/cursor.c' - f' _sqlite/microprotocols.c' - f' _sqlite/module.c' - f' _sqlite/prepare_protocol.c' - f' _sqlite/row.c' - f' _sqlite/statement.c' - f' _sqlite/util.c' - f'{sqlite_ex}' - f' -DMODULE_NAME=\'\\"sqlite3\\"\'' - f' -DSQLITE_OMIT_LOAD_EXTENSION' - f' -lsqlite3\n' - ) - - # Mac needs this: - if arch == 'mac': - ftxt += ( - '\n' - '# efrotools: mac urllib needs this:\n' - '_scproxy _scproxy.c ' - '-framework SystemConfiguration ' - '-framework CoreFoundation\n' - ) - - # Explicitly mark the remaining ones as disabled - # (so Python won't try to build them as dynamic libs). - remaining_disabled = ' '.join(cmodules) - ftxt += ( - '\n# Disabled by efrotools build:\n' - '*disabled*\n' - f'{remaining_disabled}\n' - ) - writefile(fname, ftxt) - - # Ok, this is weird. - # When applying the module Setup, python looks for any line containing *=* - # and interprets the whole thing a a global define?... - # This breaks things for our static sqlite compile above. - # The check used to look for [A-Z]*=* which didn't break, so let' just - # change it back to that for now. - # UPDATE: Currently this seems to only be necessary on Android; - # perhaps this broke between 3.9.6 and 3.9.7 or perhaps the apple - # bundle already patches it ¯\_(ツ)_/¯ - fname = 'Modules/makesetup' - txt = readfile(fname) - if platform == 'android': - txt = replace_exact( - txt, - ' *=*) DEFS="$line$NL$DEFS"; continue;;', - ' [A-Z]*=*) DEFS="$line$NL$DEFS"; continue;;', - ) - assert txt.count('[A-Z]*=*') == 1 - writefile(fname, txt) - - def winprune() -> None: - """Prune unneeded files from windows python dists.""" + """Prune unneeded files from windows python dists. + + Should run this after dropping updated windows libs/dlls/etc into + our src dirs. + """ for libdir in ( - 'assets/src/windows/Win32/Lib', - 'assets/src/windows/x64/Lib', + 'src/assets/windows/Win32/Lib', + 'src/assets/windows/x64/Lib', ): assert os.path.isdir(libdir) - assert (' ' not in name for name in PRUNE_LIB_NAMES) + assert all(' ' not in name for name in PRUNE_LIB_NAMES) subprocess.run( f'cd "{libdir}" && rm -rf ' + ' '.join(PRUNE_LIB_NAMES), shell=True, @@ -636,12 +761,14 @@ def winprune() -> None: shell=True, check=True, ) + tweak_empty_py_files(libdir) + for dlldir in ( - 'assets/src/windows/Win32/DLLs', - 'assets/src/windows/x64/DLLs', + 'src/assets/windows/Win32/DLLs', + 'src/assets/windows/x64/DLLs', ): assert os.path.isdir(dlldir) - assert (' ' not in name for name in PRUNE_DLL_NAMES) + assert all(' ' not in name for name in PRUNE_DLL_NAMES) subprocess.run( f'cd "{dlldir}" && rm -rf ' + ' '.join(PRUNE_DLL_NAMES), shell=True, @@ -652,234 +779,526 @@ def winprune() -> None: def gather(do_android: bool, do_apple: bool) -> None: - """Gather per-platform python headers, libs, and modules together. + """Gather per-platform python headers, libs, and modules into our src. This assumes all embeddable py builds have been run successfully, and that PROJROOT is the cwd. """ # pylint: disable=too-many-locals # pylint: disable=too-many-statements + # pylint: disable=too-many-branches + + class CompileArch(Enum): + """The exhaustive set of single architectures we build for. + + Basically if there is a unique pyconfig.h for it somewhere, it + should be listed here. This does not include debug/release though. + """ + + ANDROID_ARM = 'android_arm' + ANDROID_ARM64 = 'android_arm64' + ANDROID_X86 = 'android_x86' + ANDROID_X86_64 = 'android_x86_64' + IOS_ARM64 = 'ios_arm64' + IOS_SIM_ARM64 = 'ios_simulator_arm64' + IOS_SIM_X86_64 = 'ios_simulator_x86_64' + TVOS_ARM64 = 'tvos_arm64' + TVOS_SIM_ARM64 = 'tvos_simulator_arm64' + TVOS_SIM_X86_64 = 'tvos_simulator_x86_64' + MAC_ARM64 = 'mac_arm64' + MAC_X86_64 = 'mac_x86_64' + + @dataclass + class GroupDef: + """apple, android, etc.""" + + # Vanilla headers from the python version. + # The first dir will be actually used and any others will + # simply be checked to make sure they're identical. + baseheaders: list[str] + # Vanilla lib dir from the python version. + basepylib: list[str] + + @dataclass + class BuildDef: + """macos, etc.""" + + name: str + group: GroupDef + config_headers: dict[CompileArch, str] + libs: list[str] + libinst: str | None = None + sys_config_scripts: list[str] | None = None # First off, clear out any existing output. - existing_dirs = [ - os.path.join('src/external', d) - for d in os.listdir('src/external') - if d.startswith('python-') and d != 'python-notes.txt' - ] - existing_dirs += [ - os.path.join('assets/src', d) - for d in os.listdir('assets/src') - if d.startswith('pylib-') - ] - if not do_android: - existing_dirs = [d for d in existing_dirs if 'android' not in d] - - for existing_dir in existing_dirs: - subprocess.run(['rm', '-rf', existing_dir], check=True) + for platform, enabled in [('android', do_android), ('apple', do_apple)]: + if enabled: + subprocess.run( + [ + 'rm', + '-rf', + f'src/external/python-{platform}', + f'src/external/python-{platform}-debug', + f'src/assets/pylib-{platform}', + ], + check=True, + ) apost2 = f'src/Python-{PY_VER_EXACT_ANDROID}/Android/sysroot' for buildtype in ['debug', 'release']: debug = buildtype == 'debug' + debug_d = 'd' if debug else '' bsuffix = '_debug' if buildtype == 'debug' else '' bsuffix2 = '-debug' if buildtype == 'debug' else '' - alibname = 'python' + PY_VER_ANDROID + ('d' if debug else '') + alibname = 'python' + PY_VER_ANDROID + debug_d + # Where our base stuff got built to. bases = { - 'mac': f'build/python_apple_mac{bsuffix}/build/macOS', - 'ios': f'build/python_apple_ios{bsuffix}/build/iOS', - 'tvos': f'build/python_apple_tvos{bsuffix}/build/tvOS', + 'mac': f'build/python_apple_mac{bsuffix}', + 'ios': f'build/python_apple_ios{bsuffix}', + 'ios_simulator': f'build/python_apple_ios{bsuffix}', + 'tvos': f'build/python_apple_tvos{bsuffix}', + 'tvos_simulator': f'build/python_apple_tvos{bsuffix}', 'android_arm': f'build/python_android_arm{bsuffix}/build', 'android_arm64': f'build/python_android_arm64{bsuffix}/build', 'android_x86': f'build/python_android_x86{bsuffix}/build', 'android_x86_64': f'build/python_android_x86_64{bsuffix}/build', } + + # Where some support libraries got built to. bases2 = { + 'mac': f'{bases["mac"]}/merge/macOS/macosx', + 'ios': f'{bases["ios"]}/merge/iOS/iphoneos', + 'ios_simulator': ( + f'{bases["ios_simulator"]}/merge/iOS/iphonesimulator' + ), + 'tvos': f'{bases["tvos"]}/merge/tvOS/appletvos', + 'tvos_simulator': ( + f'{bases["tvos_simulator"]}/merge/tvOS/appletvsimulator' + ), 'android_arm': f'build/python_android_arm{bsuffix}/{apost2}', 'android_arm64': f'build/python_android_arm64{bsuffix}/{apost2}', 'android_x86': f'build/python_android_x86{bsuffix}/{apost2}', 'android_x86_64': f'build/python_android_x86_64{bsuffix}/{apost2}', } - # Note: only need pylib for the first in each group. - mac_arch_dir = 'macos-arm64_x86_64' - ios_arch_dir = 'ios-arm64' - tvos_arch_dir = 'tvos-arm64' - builds: list[dict[str, Any]] = [ - { - 'name': 'macos', - 'group': 'apple', - 'headers': bases['mac'] - + f'/Support/Python.xcframework/{mac_arch_dir}/Headers', - 'libs': [ - bases['mac'] - + f'/Support/Python.xcframework/{mac_arch_dir}/libPython.a', - bases['mac'] - + f'/Support/OpenSSL.xcframework/{mac_arch_dir}/' - f'libOpenSSL.a', - bases['mac'] - + f'/Support/XZ.xcframework/{mac_arch_dir}/libxz.a', - bases['mac'] - + f'/Support/BZip2.xcframework/{mac_arch_dir}/libbzip2.a', + # Groups should point to base sets of headers and pylibs that are + # used by all builds in the group. + # Note we point to a bunch of bases here but that is only for + # sanity check purposes (to make sure they are all identical); + # only the first actually gets used. + groups: dict[str, GroupDef] = { + 'apple': GroupDef( + baseheaders=[ + f'{bases["mac"]}/build/macOS/macosx/' + f'python-{PY_VER_EXACT_APPLE}/Include', + f'{bases["ios"]}/build/iOS/iphoneos.arm64/' + f'python-{PY_VER_EXACT_APPLE}/Include', + f'{bases["ios_simulator"]}' + f'/build/iOS/iphonesimulator.arm64/' + f'python-{PY_VER_EXACT_APPLE}/Include', + f'{bases["ios_simulator"]}' + f'/build/iOS/iphonesimulator.x86_64/' + f'python-{PY_VER_EXACT_APPLE}/Include', + f'{bases["tvos"]}/build/tvOS/appletvos.arm64/' + f'python-{PY_VER_EXACT_APPLE}/Include', + f'{bases["tvos_simulator"]}' + f'/build/tvOS/appletvsimulator.arm64/' + f'python-{PY_VER_EXACT_APPLE}/Include', + f'{bases["tvos_simulator"]}' + f'/build/tvOS/appletvsimulator.x86_64/' + f'python-{PY_VER_EXACT_APPLE}/Include', ], - 'pylib': ( - bases['mac'] + f'/Python-{PY_VER_EXACT_APPLE}-macOS/lib' + basepylib=[ + f'{bases["mac"]}/build/macOS/macosx/' + f'python-{PY_VER_EXACT_APPLE}/Lib', + f'{bases["ios"]}/build/iOS/iphoneos.arm64/' + f'python-{PY_VER_EXACT_APPLE}/Lib', + f'{bases["ios_simulator"]}' + f'/build/iOS/iphonesimulator.arm64/' + f'python-{PY_VER_EXACT_APPLE}/Lib', + f'{bases["ios_simulator"]}' + f'/build/iOS/iphonesimulator.x86_64/' + f'python-{PY_VER_EXACT_APPLE}/Lib', + f'{bases["tvos"]}/build/tvOS/appletvos.arm64/' + f'python-{PY_VER_EXACT_APPLE}/Lib', + f'{bases["tvos_simulator"]}' + f'/build/tvOS/appletvsimulator.arm64/' + f'python-{PY_VER_EXACT_APPLE}/Lib', + f'{bases["tvos_simulator"]}' + f'/build/tvOS/appletvsimulator.x86_64/' + f'python-{PY_VER_EXACT_APPLE}/Lib', + ], + ), + 'android': GroupDef( + baseheaders=[ + f'build/python_android_arm/src/' + f'Python-{PY_VER_EXACT_ANDROID}/Include', + f'build/python_android_arm64/src/' + f'Python-{PY_VER_EXACT_ANDROID}/Include', + f'build/python_android_x86/src/' + f'Python-{PY_VER_EXACT_ANDROID}/Include', + f'build/python_android_x86_64/src/' + f'Python-{PY_VER_EXACT_ANDROID}/Include', + ], + basepylib=[ + f'build/python_android_arm/src/' + f'Python-{PY_VER_EXACT_ANDROID}/Lib', + f'build/python_android_arm64/src/' + f'Python-{PY_VER_EXACT_ANDROID}/Lib', + f'build/python_android_x86/src/' + f'Python-{PY_VER_EXACT_ANDROID}/Lib', + f'build/python_android_x86_64/src/' + f'Python-{PY_VER_EXACT_ANDROID}/Lib', + ], + ), + } + + def _apple_libs(base: str) -> list[str]: + # pylint: disable=cell-var-from-loop + out = [ + ( + f'{bases2[base]}/python-{PY_VER_EXACT_APPLE}' + f'/libPython{PY_VER_APPLE}.a' ), - }, - { - 'name': 'ios', - 'group': 'apple', - 'headers': bases['ios'] - + f'/Support/Python.xcframework/{ios_arch_dir}/Headers', - 'libs': [ - bases['ios'] - + f'/Support/Python.xcframework/{ios_arch_dir}/libPython.a', - bases['ios'] - + f'/Support/OpenSSL.xcframework/{ios_arch_dir}/' - f'libOpenSSL.a', - bases['ios'] - + f'/Support/XZ.xcframework/{ios_arch_dir}/libxz.a', - bases['ios'] - + f'/Support/BZip2.xcframework/{ios_arch_dir}/libbzip2.a', + f'{bases2[base]}/openssl-{OPENSSL_VER_APPLE}/lib/libssl.a', + f'{bases2[base]}/openssl-{OPENSSL_VER_APPLE}/lib/libcrypto.a', + f'{bases2[base]}/xz-5.4.2/lib/liblzma.a', + f'{bases2[base]}/bzip2-1.0.8/lib/libbz2.a', + ] + if base != 'mac': + out.append(f'{bases2[base]}/libffi-3.4.2/lib/libffi.a') + return out + + def _android_libs(base: str) -> list[str]: + # pylint: disable=cell-var-from-loop + return [ + f'{bases[base]}/usr/lib/lib{alibname}.a', + f'{bases2[base]}/usr/lib/libssl.a', + f'{bases2[base]}/usr/lib/libcrypto.a', + f'{bases2[base]}/usr/lib/liblzma.a', + f'{bases2[base]}/usr/lib/libsqlite3.a', + f'{bases2[base]}/usr/lib/libffi.a', + f'{bases2[base]}/usr/lib/libbz2.a', + f'{bases2[base]}/usr/lib/libuuid.a', + ] + + builds: list[BuildDef] = [ + BuildDef( + name='macos', + group=groups['apple'], + # There's just a single config for the universal build; + # that seems odd but I guess it's right?... + config_headers={ + CompileArch.MAC_ARM64: bases2['mac'] + + f'/python-{PY_VER_EXACT_APPLE}/Headers/pyconfig.h', + CompileArch.MAC_X86_64: bases2['mac'] + + f'/python-{PY_VER_EXACT_APPLE}/Headers/pyconfig.h', + }, + sys_config_scripts=[ + bases2['mac'] + + f'/python-{PY_VER_EXACT_APPLE}/python-stdlib/' + f'_sysconfigdata_{debug_d}_darwin_darwin.py' ], - }, - { - 'name': 'tvos', - 'group': 'apple', - 'headers': bases['tvos'] - + f'/Support/Python.xcframework/{tvos_arch_dir}/Headers', - 'libs': [ - bases['tvos'] - + f'/Support/Python.xcframework/{tvos_arch_dir}/' - f'libPython.a', - bases['tvos'] - + f'/Support/OpenSSL.xcframework/{tvos_arch_dir}/' - f'libOpenSSL.a', - bases['tvos'] - + f'/Support/XZ.xcframework/{tvos_arch_dir}/libxz.a', - bases['tvos'] - + f'/Support/BZip2.xcframework/{tvos_arch_dir}/libbzip2.a', + libs=_apple_libs('mac'), + ), + BuildDef( + name='ios', + group=groups['apple'], + config_headers={ + CompileArch.IOS_ARM64: bases2['ios'] + + f'/python-{PY_VER_EXACT_APPLE}/Headers/pyconfig-arm64.h', + }, + sys_config_scripts=[ + bases2['ios'] + + f'/python-{PY_VER_EXACT_APPLE}/python-stdlib/' + f'_sysconfigdata_{debug_d}_ios_iphoneos.py', + bases2['ios'] + + f'/python-{PY_VER_EXACT_APPLE}/python-stdlib/' + f'_sysconfigdata_{debug_d}_ios_iphoneos_arm64.py', ], - }, - { - 'name': 'android_arm', - 'group': 'android', - 'headers': bases['android_arm'] + f'/usr/include/{alibname}', - 'libs': [ - bases['android_arm'] + f'/usr/lib/lib{alibname}.a', - bases2['android_arm'] + '/usr/lib/libssl.a', - bases2['android_arm'] + '/usr/lib/libcrypto.a', - bases2['android_arm'] + '/usr/lib/liblzma.a', - bases2['android_arm'] + '/usr/lib/libsqlite3.a', - bases2['android_arm'] + '/usr/lib/libbz2.a', - bases2['android_arm'] + '/usr/lib/libuuid.a', + libs=_apple_libs('ios'), + ), + BuildDef( + name='ios_simulator', + group=groups['apple'], + config_headers={ + CompileArch.IOS_SIM_ARM64: bases2['ios_simulator'] + + f'/python-{PY_VER_EXACT_APPLE}/Headers/pyconfig-arm64.h', + CompileArch.IOS_SIM_X86_64: bases2['ios_simulator'] + + f'/python-{PY_VER_EXACT_APPLE}/Headers/pyconfig-x86_64.h', + }, + sys_config_scripts=[ + bases2['ios_simulator'] + + f'/python-{PY_VER_EXACT_APPLE}/python-stdlib/' + f'_sysconfigdata_{debug_d}_ios_iphonesimulator.py', + bases2['ios_simulator'] + + f'/python-{PY_VER_EXACT_APPLE}/python-stdlib/' + f'_sysconfigdata_{debug_d}_ios_iphonesimulator_arm64.py', + bases2['ios_simulator'] + + f'/python-{PY_VER_EXACT_APPLE}/python-stdlib/' + f'_sysconfigdata_{debug_d}_ios_iphonesimulator_x86_64.py', ], - 'libinst': 'android_armeabi-v7a', - 'pylib': ( - bases['android_arm'] + '/usr/lib/python' + PY_VER_ANDROID - ), - }, - { - 'name': 'android_arm64', - 'group': 'android', - 'headers': bases['android_arm64'] + f'/usr/include/{alibname}', - 'libs': [ - bases['android_arm64'] + f'/usr/lib/lib{alibname}.a', - bases2['android_arm64'] + '/usr/lib/libssl.a', - bases2['android_arm64'] + '/usr/lib/libcrypto.a', - bases2['android_arm64'] + '/usr/lib/liblzma.a', - bases2['android_arm64'] + '/usr/lib/libsqlite3.a', - bases2['android_arm64'] + '/usr/lib/libbz2.a', - bases2['android_arm64'] + '/usr/lib/libuuid.a', + libs=_apple_libs('ios_simulator'), + ), + BuildDef( + name='tvos', + group=groups['apple'], + config_headers={ + CompileArch.TVOS_ARM64: bases2['tvos'] + + f'/python-{PY_VER_EXACT_APPLE}/Headers/pyconfig-arm64.h', + }, + sys_config_scripts=[ + bases2['tvos'] + + f'/python-{PY_VER_EXACT_APPLE}/python-stdlib/' + f'_sysconfigdata_{debug_d}_tvos_appletvos.py', + bases2['tvos'] + + f'/python-{PY_VER_EXACT_APPLE}/python-stdlib/' + f'_sysconfigdata_{debug_d}_tvos_appletvos_arm64.py', ], - 'libinst': 'android_arm64-v8a', - }, - { - 'name': 'android_x86', - 'group': 'android', - 'headers': bases['android_x86'] + f'/usr/include/{alibname}', - 'libs': [ - bases['android_x86'] + f'/usr/lib/lib{alibname}.a', - bases2['android_x86'] + '/usr/lib/libssl.a', - bases2['android_x86'] + '/usr/lib/libcrypto.a', - bases2['android_x86'] + '/usr/lib/liblzma.a', - bases2['android_x86'] + '/usr/lib/libsqlite3.a', - bases2['android_x86'] + '/usr/lib/libbz2.a', - bases2['android_x86'] + '/usr/lib/libuuid.a', + libs=_apple_libs('tvos'), + ), + BuildDef( + name='tvos_simulator', + group=groups['apple'], + config_headers={ + CompileArch.TVOS_SIM_ARM64: bases2['tvos_simulator'] + + f'/python-{PY_VER_EXACT_APPLE}/Headers/pyconfig-arm64.h', + CompileArch.TVOS_SIM_X86_64: bases2['ios_simulator'] + + f'/python-{PY_VER_EXACT_APPLE}/Headers/pyconfig-x86_64.h', + }, + sys_config_scripts=[ + bases2['tvos_simulator'] + + f'/python-{PY_VER_EXACT_APPLE}/python-stdlib/' + f'_sysconfigdata_{debug_d}_tvos_appletvsimulator.py', + bases2['tvos_simulator'] + + f'/python-{PY_VER_EXACT_APPLE}/python-stdlib/' + f'_sysconfigdata_{debug_d}_tvos_appletvsimulator_arm64.py', + bases2['tvos_simulator'] + + f'/python-{PY_VER_EXACT_APPLE}/python-stdlib/' + f'_sysconfigdata_{debug_d}_tvos_appletvsimulator_x86_64.py', ], - 'libinst': 'android_x86', - }, - { - 'name': 'android_x86_64', - 'group': 'android', - 'headers': bases['android_x86_64'] + f'/usr/include/{alibname}', - 'libs': [ - bases['android_x86_64'] + f'/usr/lib/lib{alibname}.a', - bases2['android_x86_64'] + '/usr/lib/libssl.a', - bases2['android_x86_64'] + '/usr/lib/libcrypto.a', - bases2['android_x86_64'] + '/usr/lib/liblzma.a', - bases2['android_x86_64'] + '/usr/lib/libsqlite3.a', - bases2['android_x86_64'] + '/usr/lib/libbz2.a', - bases2['android_x86_64'] + '/usr/lib/libuuid.a', + libs=_apple_libs('tvos_simulator'), + ), + BuildDef( + name='android_arm', + group=groups['android'], + config_headers={ + CompileArch.ANDROID_ARM: bases['android_arm'] + + f'/usr/include/{alibname}/pyconfig.h' + }, + sys_config_scripts=[ + bases['android_arm'] + f'/usr/lib/python{PY_VER_ANDROID}/' + f'_sysconfigdata_{debug_d}_linux_arm-linux-androideabi.py' ], - 'libinst': 'android_x86_64', - }, + libs=_android_libs('android_arm'), + libinst='android_armeabi-v7a', + ), + BuildDef( + name='android_arm64', + group=groups['android'], + config_headers={ + CompileArch.ANDROID_ARM64: bases['android_arm64'] + + f'/usr/include/{alibname}/pyconfig.h' + }, + sys_config_scripts=[ + bases['android_arm64'] + f'/usr/lib/python{PY_VER_ANDROID}/' + f'_sysconfigdata_{debug_d}' + f'_linux_aarch64-linux-android.py' + ], + libs=_android_libs('android_arm64'), + libinst='android_arm64-v8a', + ), + BuildDef( + name='android_x86', + group=groups['android'], + config_headers={ + CompileArch.ANDROID_X86: bases['android_x86'] + + f'/usr/include/{alibname}/pyconfig.h' + }, + sys_config_scripts=[ + bases['android_x86'] + f'/usr/lib/python{PY_VER_ANDROID}/' + f'_sysconfigdata_{debug_d}' + f'_linux_i686-linux-android.py' + ], + libs=_android_libs('android_x86'), + libinst='android_x86', + ), + BuildDef( + name='android_x86_64', + group=groups['android'], + config_headers={ + CompileArch.ANDROID_X86_64: bases['android_x86_64'] + + f'/usr/include/{alibname}/pyconfig.h' + }, + sys_config_scripts=[ + bases['android_x86_64'] + + f'/usr/lib/python{PY_VER_ANDROID}/' + f'_sysconfigdata_{debug_d}' + f'_linux_x86_64-linux-android.py' + ], + libs=_android_libs('android_x86_64'), + libinst='android_x86_64', + ), ] - for build in builds: - grp = build['group'] - if not do_android and grp == 'android': + # Assemble per-group stuff. + for grpname, grp in groups.items(): + if not do_android and grpname == 'android': continue - if not do_apple and grp == 'apple': + if not do_apple and grpname == 'apple': continue - builddir = f'src/external/python-{grp}{bsuffix2}' - header_dst = os.path.join(builddir, 'include') - lib_dst = os.path.join(builddir, 'lib') - assets_src_dst = f'assets/src/pylib-{grp}' - # Do some setup only once per group. - if not os.path.exists(builddir): - subprocess.run(['mkdir', '-p', builddir], check=True) - subprocess.run(['mkdir', '-p', lib_dst], check=True) + # Sanity check: if we have more than one set of base headers/libs + # for this group, make sure they're all identical. + for dirlist, dirdesc in [ + (grp.baseheaders, 'baseheaders'), + (grp.basepylib, 'basepylib'), + ]: + for i in range(len(dirlist) - 1): + returncode = subprocess.run( + ['diff', dirlist[i], dirlist[i + 1]], + check=False, + capture_output=True, + ).returncode + if returncode != 0: + raise RuntimeError( + f'Sanity check failed: the following {dirdesc}' + f' dirs differ:\n' + f'{dirlist[i]}\n' + f'{dirlist[i+1]}' + ) - # Only pull modules into game assets on release pass. - if not debug: - # Copy system modules into the src assets - # dir for this group. - subprocess.run(['mkdir', '-p', assets_src_dst], check=True) - subprocess.run( - [ - 'rsync', - '--recursive', - '--include', - '*.py', - '--exclude', - '__pycache__', - '--include', - '*/', - '--exclude', - '*', - build['pylib'] + '/', - assets_src_dst, - ], - check=True, - ) + pylib_dst = f'src/assets/pylib-{grpname}' + src_dst = f'src/external/python-{grpname}{bsuffix2}' + include_dst = os.path.join(src_dst, 'include') + lib_dst = os.path.join(src_dst, 'lib') - # Prune a bunch of modules we don't need to cut - # down on size. - subprocess.run( - 'cd "' - + assets_src_dst - + '" && rm -rf ' - + ' '.join(PRUNE_LIB_NAMES), - shell=True, - check=True, - ) + assert not os.path.exists(src_dst) + assert not os.path.exists(lib_dst) + subprocess.run(['mkdir', '-p', src_dst], check=True) + subprocess.run(['mkdir', '-p', lib_dst], check=True) + # Copy in the base 'include' dir for this group. + subprocess.run( + ['cp', '-r', grp.baseheaders[0], include_dst], + check=True, + ) + + # Write a master pyconfig.h that reroutes to each + # compile-arch's actual header (pyconfig-FOO_BAR.h). + # FIXME - we are using ballistica-specific values here; + # could be nice to generalize this so its usable elsewhere. + unified_pyconfig = ( + f'#if BA_XCODE_BUILD\n' + f'// Necessary to get the TARGET_OS_SIMULATOR define.\n' + f'#include \n' + f'#endif\n' + f'\n' + f'#if BA_OSTYPE_MACOS and defined(__aarch64__)\n' + f'#include "pyconfig_{CompileArch.MAC_ARM64.value}.h"\n' + f'\n' + f'#elif BA_OSTYPE_MACOS and defined(__x86_64__)\n' + f'#include "pyconfig_{CompileArch.MAC_X86_64.value}.h"\n' + f'\n' + f'#elif BA_OSTYPE_IOS and defined(__aarch64__)\n' + f'#if TARGET_OS_SIMULATOR\n' + f'#include "pyconfig_{CompileArch.IOS_SIM_ARM64.value}.h"\n' + f'#else\n' + f'#include "pyconfig_{CompileArch.IOS_ARM64.value}.h"\n' + f'#endif // TARGET_OS_SIMULATOR\n' + f'\n' + f'#elif BA_OSTYPE_IOS and defined(__x86_64__)\n' + f'#if TARGET_OS_SIMULATOR\n' + f'#include "pyconfig_{CompileArch.IOS_SIM_X86_64.value}.h"\n' + f'#else\n' + f'#error this platform combo should not be possible\n' + f'#endif // TARGET_OS_SIMULATOR\n' + f'\n' + f'#elif BA_OSTYPE_TVOS and defined(__aarch64__)\n' + f'#if TARGET_OS_SIMULATOR\n' + f'#include "pyconfig_{CompileArch.TVOS_SIM_ARM64.value}.h"\n' + f'#else\n' + f'#include "pyconfig_{CompileArch.TVOS_ARM64.value}.h"\n' + f'#endif // TARGET_OS_SIMULATOR\n' + f'\n' + f'#elif BA_OSTYPE_TVOS and defined(__x86_64__)\n' + f'#if TARGET_OS_SIMULATOR\n' + f'#include "pyconfig_{CompileArch.TVOS_SIM_X86_64.value}.h"\n' + f'#else\n' + f'#error this platform combo should not be possible\n' + f'#endif // TARGET_OS_SIMULATOR\n' + f'\n' + f'#elif BA_OSTYPE_ANDROID and defined(__arm__)\n' + f'#include "pyconfig_{CompileArch.ANDROID_ARM.value}.h"\n' + f'\n' + f'#elif BA_OSTYPE_ANDROID and defined(__aarch64__)\n' + f'#include "pyconfig_{CompileArch.ANDROID_ARM64.value}.h"\n' + f'\n' + f'#elif BA_OSTYPE_ANDROID and defined(__i386__)\n' + f'#include "pyconfig_{CompileArch.ANDROID_X86.value}.h"\n' + f'\n' + f'#elif BA_OSTYPE_ANDROID and defined(__x86_64__)\n' + f'#include "pyconfig_{CompileArch.ANDROID_X86_64.value}.h"\n' + f'\n' + f'#else\n' + f'#error unknown platform\n' + f'\n' + f'#endif\n' + ) + with open( + f'{include_dst}/pyconfig.h', 'w', encoding='utf-8' + ) as hfile: + hfile.write(unified_pyconfig) + + # Pylib is the same for debug and release, so we only need + # to assemble for one of them. + if not os.path.exists(pylib_dst): + assert not os.path.exists(pylib_dst) + subprocess.run(['mkdir', '-p', pylib_dst], check=True) + subprocess.run( + [ + 'rsync', + '--recursive', + '--include', + '*.py', + '--exclude', + '__pycache__', + '--include', + '*/', + '--exclude', + '*', + f'{grp.basepylib[0]}/', + pylib_dst, + ], + check=True, + ) + tweak_empty_py_files(pylib_dst) + + # Prune a bunch of modules we don't need to cut down on size. + # NOTE: allowing shell expansion in PRUNE_LIB_NAMES so need + # to run this as shell=True. + subprocess.run( + 'cd "' + + pylib_dst + + '" && rm -rf ' + + ' '.join(PRUNE_LIB_NAMES), + shell=True, + check=True, + ) + + # UPDATE: now bundling sysconfigdata scripts AND + # disabling site.py when initializing python for bundled + # builds so this should no longer be necessary. + if bool(False): # Some minor filtering to system scripts: # on iOS/tvOS, addusersitepackages() leads to a crash # due to _sysconfigdata_dm_ios_darwin module not existing, - # so let's skip that. - fname = f'{assets_src_dst}/site.py' + # so let's remove that logic in all cases. + # In general we *could* bundle _sysconfigdata everywhere but + # gonna try to just avoid anything that uses it for now + # and save a bit of memory. + fname = f'{pylib_dst}/site.py' txt = readfile(fname) txt = replace_exact( txt, @@ -890,88 +1309,67 @@ def gather(do_android: bool, do_apple: bool) -> None: ) writefile(fname, txt) - # Copy in a base set of headers (everything in a group should - # be using the same headers) - subprocess.run( - f'cp -r "{build["headers"]}" "{header_dst}"', - shell=True, - check=True, + # Pull stuff in from all builds in this group. + for build in builds: + if build.group is not grp: + continue + + # Copy the build's pyconfig.h in with a unique name + # (which the unified pyconfig.h we wrote above will route to). + for compilearch, pycfgpath in build.config_headers.items(): + dstpath = f'{include_dst}/pyconfig_{compilearch.value}.h' + assert not os.path.exists(dstpath), f'exists!: {dstpath}' + subprocess.run(['cp', pycfgpath, dstpath], check=True) + + # If the build points at any sysconfig scripts, pull those + # in (and ensure each has a unique name). + if build.sys_config_scripts is not None: + for script in build.sys_config_scripts: + scriptdst = os.path.join( + pylib_dst, os.path.basename(script) + ) + if os.path.exists(scriptdst): + raise RuntimeError( + 'Multiple sys-config-scripts trying to write' + f" to '{scriptdst}'." + ) + subprocess.run(['cp', script, pylib_dst], check=True) + + # Copy in this build's libs. + libinst = ( + build.libinst if build.libinst is not None else build.name ) - - # Clear whatever pyconfigs came across; we'll build our own - # universal one below. - subprocess.run( - 'rm ' + header_dst + '/pyconfig*', shell=True, check=True - ) - - # Write a master pyconfig header that reroutes to each - # platform's actual header. - with open( - header_dst + '/pyconfig.h', 'w', encoding='utf-8' - ) as hfile: - hfile.write( - '#if BA_OSTYPE_MACOS\n' - '#include "pyconfig-macos.h"\n\n' - '#elif BA_OSTYPE_IOS\n' - '#include "pyconfig-ios.h"\n\n' - '#elif BA_OSTYPE_TVOS\n' - '#include "pyconfig-tvos.h"\n\n' - '#elif BA_OSTYPE_ANDROID and defined(__arm__)\n' - '#include "pyconfig-android_arm.h"\n\n' - '#elif BA_OSTYPE_ANDROID and defined(__aarch64__)\n' - '#include "pyconfig-android_arm64.h"\n\n' - '#elif BA_OSTYPE_ANDROID and defined(__i386__)\n' - '#include "pyconfig-android_x86.h"\n\n' - '#elif BA_OSTYPE_ANDROID and defined(__x86_64__)\n' - '#include "pyconfig-android_x86_64.h"\n\n' - '#else\n' - '#error unknown platform\n\n' - '#endif\n' - ) - - # Now copy each build's config headers in with unique names. - cfgs = [ - f - for f in os.listdir(build['headers']) - if f.startswith('pyconfig') - ] - - # Copy config headers to their filtered names. - for cfg in cfgs: - out = cfg.replace('pyconfig', 'pyconfig-' + build['name']) - if cfg == 'pyconfig.h': - - # For platform's root pyconfig.h we need to filter - # contents too (those headers can themselves include - # others; ios for instance points to a arm64 and a - # x86_64 variant). - contents = readfile(build['headers'] + '/' + cfg) - contents = contents.replace( - 'pyconfig', 'pyconfig-' + build['name'] - ) - writefile(header_dst + '/' + out, contents) - else: - # other configs we just rename - subprocess.run( - 'cp "' - + build['headers'] - + '/' - + cfg - + '" "' - + header_dst - + '/' - + out - + '"', - shell=True, - check=True, - ) - - # Copy in libs. If the lib gave a specific install name, - # use that; otherwise use name. - targetdir = lib_dst + '/' + build.get('libinst', build['name']) - subprocess.run(['rm', '-rf', targetdir], check=True) - subprocess.run(['mkdir', '-p', targetdir], check=True) - for lib in build['libs']: - subprocess.run(['cp', lib, targetdir], check=True) + targetdir = f'{lib_dst}/{libinst}' + subprocess.run(['rm', '-rf', targetdir], check=True) + subprocess.run(['mkdir', '-p', targetdir], check=True) + for lib in build.libs: + finalpath = os.path.join(targetdir, os.path.basename(lib)) + assert not os.path.exists(finalpath) + subprocess.run(['cp', lib, targetdir], check=True) + assert os.path.exists(finalpath) print('Great success!') + + +def tweak_empty_py_files(dirpath: str) -> None: + """Find any zero-length Python files and make them length 1 + + I'm finding that my jenkins server updates modtimes on all empty files + when fetching updates regardless of whether anything has changed. + This leads to a decent number of assets getting rebuilt when not + necessary. + + As a slightly-hacky-but-effective workaround I'm sticking a newline + up in there. + """ + for root, _subdirs, fnames in os.walk(dirpath): + for fname in fnames: + if ( + fname.endswith('.py') or fname == 'py.typed' + ) and os.path.getsize(os.path.join(root, fname)) == 0: + if bool(False): + print('Tweaking empty py file:', os.path.join(root, fname)) + with open( + os.path.join(root, fname), 'w', encoding='utf-8' + ) as outfile: + outfile.write('\n') diff --git a/tools/efrotools/pylintplugins.py b/tools/efrotools/pylintplugins.py index 07b05a32..2818c7eb 100644 --- a/tools/efrotools/pylintplugins.py +++ b/tools/efrotools/pylintplugins.py @@ -44,7 +44,6 @@ def ignore_type_check_filter(if_node: nc.NodeNG) -> nc.NodeNG: and if_node.test.name == 'TYPE_CHECKING' and isinstance(if_node.parent, astroid.Module) ): - # Special case: some third party modules are starting to contain # code that we don't handle cleanly which results in pylint runs # breaking. For now just ignoring them as they pop up. @@ -147,7 +146,7 @@ def func_annotations_filter(node: nc.NodeNG) -> nc.NodeNG: # Special-case: certain function decorators *do* # evaluate annotations at runtime so we want to leave theirs intact. - # This includes functools.singledispatch, ba.dispatchmethod, and + # This includes functools.singledispatch, babase.dispatchmethod, and # efro.MessageReceiver. # Lets just look for a @XXX.register or @XXX.handler decorators for # now; can get more specific if we get false positives. @@ -186,7 +185,6 @@ def var_annotations_filter(node: nc.NodeNG) -> nc.NodeNG: # pylint: disable=too-many-nested-blocks if using_future_annotations(node): - # Future behavior: # Annotated assigns under functions are not evaluated. # Class and module vars are normally not either. However we @@ -201,7 +199,6 @@ def var_annotations_filter(node: nc.NodeNG) -> nc.NodeNG: if fnode.decorators is not None: found_ioprepped = False for dec in fnode.decorators.nodes: - # Look for dataclassio.ioprepped. if ( isinstance(dec, astroid.nodes.Attribute) diff --git a/tools/efrotools/sync.py b/tools/efrotools/sync.py index daab8e1a..28e2ecc6 100644 --- a/tools/efrotools/sync.py +++ b/tools/efrotools/sync.py @@ -187,7 +187,6 @@ def sync_paths(src_proj: str, src: Path, dst: Path, mode: Mode) -> int: outfile.write(add_marker(src_proj, srcdata)) continue if src_hash == marker_hash and dst_hash != marker_hash: - # Dst has changed; we only copy backwards to src # if we're in full mode. if mode == Mode.LIST: @@ -209,7 +208,6 @@ def sync_paths(src_proj: str, src: Path, dst: Path, mode: Mode) -> int: continue if marker_hash not in (src_hash, dst_hash): - # One more option: source and dst could have been changed in # identical ways (common when doing global search/replaces). # In this case the calced hash from src and dst will match diff --git a/tools/efrotools/xcode.py b/tools/efrotools/xcodebuild.py similarity index 50% rename from tools/efrotools/xcode.py rename to tools/efrotools/xcodebuild.py index 6cb7e3ee..a44da5ae 100644 --- a/tools/efrotools/xcode.py +++ b/tools/efrotools/xcodebuild.py @@ -1,33 +1,73 @@ # Released under the MIT License. See LICENSE for details. # """Functionality related to Xcode on Apple platforms.""" - +# pylint: disable=too-many-lines from __future__ import annotations import json import os -import subprocess import sys import time import shlex +import logging +import tempfile +import subprocess from enum import Enum -from typing import TYPE_CHECKING +from pathlib import Path +from typing import TYPE_CHECKING, assert_never +from dataclasses import dataclass + +from filelock import FileLock -from efro.util import assert_never from efro.terminal import Clr from efro.error import CleanError +from efro.dataclassio import ioprepped, dataclass_from_dict +from efrotools import getlocalconfig # pylint: disable=wrong-import-order if TYPE_CHECKING: from typing import Any +@ioprepped +@dataclass +class SigningConfig: + """Info about signing.""" + + certfile: str + certpass: str + + +# HOW THIS WORKS: + +# Basically we scan line beginnings for the following words followed by +# spaces. When we find one, we switch our section to that until a different +# section header is found. We then call parse/print functions with new lines +# depending on the current section. + +# Section line parsing/printing is generally blacklist based; if we recognize +# a line is not needed we can ignore it. We generally look for specific +# strings or string-endings in a line to do this. We want to be sure to print +# anything we don't recognize to avoid hiding important output. + +# I'm sure this system isn't 100% accurate with threads spitting out +# overlapping output and whatnot, but it hopefully works 'well enough'. + + class _Section(Enum): COMPILEC = 'CompileC' + SWIFTCOMPILE = 'SwiftCompile' + SWIFTGENERATEPCH = 'SwiftGeneratePch' + SWIFTDRIVER = 'SwiftDriver' + SWIFTDRIVERJOBDISCOVERY = 'SwiftDriverJobDiscovery' + SWIFTEMITMODULE = 'SwiftEmitModule' + COMPILESWIFT = 'CompileSwift' MKDIR = 'MkDir' LD = 'Ld' + CPRESOURCE = 'CpResource' COMPILEASSETCATALOG = 'CompileAssetCatalog' CODESIGN = 'CodeSign' COMPILESTORYBOARD = 'CompileStoryboard' + CONVERTICONSETFILE = 'ConvertIconsetFile' LINKSTORYBOARDS = 'LinkStoryboards' PROCESSINFOPLISTFILE = 'ProcessInfoPlistFile' COPYSWIFTLIBS = 'CopySwiftLibs' @@ -36,7 +76,6 @@ class _Section(Enum): TOUCH = 'Touch' REGISTERWITHLAUNCHSERVICES = 'RegisterWithLaunchServices' METALLINK = 'MetalLink' - COMPILESWIFT = 'CompileSwift' CREATEBUILDDIRECTORY = 'CreateBuildDirectory' COMPILEMETALFILE = 'CompileMetalFile' COPY = 'Copy' @@ -46,6 +85,12 @@ class _Section(Enum): PROCESSPCH = 'ProcessPCH' PROCESSPCHPLUSPLUS = 'ProcessPCH++' PHASESCRIPTEXECUTION = 'PhaseScriptExecution' + PROCESSPRODUCTPACKAGING = 'ProcessProductPackaging' + PROCESSPRODUCTPACKAGINGDER = 'ProcessProductPackagingDER' + CLANGSTATCACHE = 'ClangStatCache' + EXTRACTAPPINTENTSMETADATA = 'ExtractAppIntentsMetadata' + SWIFTMERGEGENERATEDHEADERS = 'SwiftMergeGeneratedHeaders' + GENERATEDSYMFILE = 'GenerateDSYMFile' class XCodeBuild: @@ -59,52 +104,285 @@ class XCodeBuild: self._section: _Section | None = None self._section_line_count = 0 self._returncode: int | None = None - self._project: str = self._argstr(args, '-project') - self._scheme: str = self._argstr(args, '-scheme') - self._configuration: str = self._argstr(args, '-configuration') + self._project: str | None = ( + self._argstr(args, '-project') if '-project' in args else None + ) + self._scheme: str | None = ( + self._argstr(args, '-scheme') if '-scheme' in args else None + ) + self._configuration: str | None = ( + self._argstr(args, '-configuration') + if '-configuration' in args + else None + ) + + # Use random name for temp keychains to hopefully avoid collisions + # and make snooping harder. + self._keychain_name = f'build{os.urandom(8).hex()}.keychain' + self._keychain_pass = os.urandom(16).hex() + + self._signingconfigname: str | None = None + self._signingconfig: SigningConfig | None = None + if '-signingconfig' in args: + self._signingconfigname = self._argstr( + args, '-signingconfig', remove=True + ) + + lconfig = getlocalconfig(projroot=Path(projroot)) + if self._signingconfigname not in lconfig.get( + 'apple_signing_configs', {} + ): + raise CleanError( + f"Error: Signing-config '{self._signingconfigname}'" + ' is not present in localconfig.' + ) + try: + self._signingconfig = dataclass_from_dict( + SigningConfig, + lconfig['apple_signing_configs'][self._signingconfigname], + ) + if not os.path.exists(self._signingconfig.certfile): + raise RuntimeError( + f'Certfile not found at' + f" '{self._signingconfig.certfile}'." + ) + except Exception: + logging.exception( + "Error loading signing-config '%s'.", + self._signingconfigname, + ) def run(self) -> None: """Do the thing.""" - self._run_cmd(self._build_cmd_args()) - assert self._returncode is not None - # In some failure cases we may want to run a clean and try again. - if self._returncode != 0: + # Ok here's the deal: + # First I tried creating file-locks only while creating or destroying + # temp keychains that we use for builds, but I'm still seeing random + # code signing failures when 2 builds overlap. + # So now I'm going with a single lock that is held throughout the + # entire build when code signing is involved. We'll see if that works. + # If that seems to slow things down too much we can try something in + # the middle such as enforcing a cooldown period after making keychain + # changes or something like that. - # Getting this error sometimes after xcode updates. - if 'error: PCH file built from a different branch' in '\n'.join( - self._output - ): + # Go with a lock for the full build duration only if there's signing + # involved. + if self._signingconfig is not None: + wait_start_time = time.monotonic() + print('Waiting for xcode build lock...', flush=True) + with self._get_build_file_lock(): + wait_time = time.monotonic() - wait_start_time print( - f'{Clr.MAG}WILL CLEAN AND' - f' RE-ATTEMPT XCODE BUILD{Clr.RST}' + f'Xcode build lock acquired in {wait_time:.2f} seconds.', + flush=True, ) - self._run_cmd( - [ - 'xcodebuild', - '-project', - self._project, - '-scheme', - self._scheme, - '-configuration', - self._configuration, - 'clean', - ] - ) - # Now re-run the original build. - print( - f'{Clr.MAG}RE-ATTEMPTING XCODE BUILD' - f' AFTER CLEAN{Clr.RST}' - ) - self._run_cmd(self._build_cmd_args()) + self._run() + else: + self._run() - if self._returncode != 0: - raise CleanError(f'Command failed with code {self._returncode}.') + def _run(self) -> None: + self._set_up_keychain() + + try: + self._run_cmd(self._build_cmd_args()) + assert self._returncode is not None + + # In some failure cases we may want to run a clean and try again. + if self._returncode != 0: + # Getting this error sometimes after xcode updates. + if ( + 'error: PCH file built from a different branch' + in '\n'.join(self._output) + ): + # Assume these were all passed for the build that just + # failed. + assert self._project is not None + assert self._scheme is not None + assert self._configuration is not None + print( + f'{Clr.MAG}WILL CLEAN AND' + f' RE-ATTEMPT XCODE BUILD{Clr.RST}' + ) + self._run_cmd( + [ + 'xcodebuild', + '-project', + self._project, + '-scheme', + self._scheme, + '-configuration', + self._configuration, + 'clean', + ] + ) + # Now re-run the original build. + print( + f'{Clr.MAG}RE-ATTEMPTING XCODE BUILD' + f' AFTER CLEAN{Clr.RST}' + ) + self._run_cmd(self._build_cmd_args()) + + if self._returncode != 0: + raise CleanError( + f'Command failed with code {self._returncode}.' + ) + finally: + self._tear_down_keychain() + + def _get_keychain_file_lock(self) -> FileLock: + """Return a lock that we hold while mucking with keychain stuff.""" + path = os.path.join(tempfile.gettempdir(), 'ba_xc_keychain_lock') + return FileLock(path) + + def _get_build_file_lock(self) -> FileLock: + """Return a lock that we hold for an entire build.""" + path = os.path.join(tempfile.gettempdir(), 'ba_xc_build_lock') + return FileLock(path) + + def _set_up_keychain(self) -> None: + # If we're specifying a signing configuration, this sets it up + # via a temporary keychain. + # As seen in https://github.com/Apple-Actions/import-codesign-certs + # And similarly https://xcodebuild.tips/pages/certificates-and-keys/ + if self._signingconfig is None: + return + + # We're mucking with keychain settings here which is a global thing. + # Let's try to at least avoid two of us mucking with it at once. + assert self._signingconfigname is not None + with self._get_keychain_file_lock(): + print(f"Setting up signing-config '{self._signingconfigname}'...") + + # Create a new temp keychain. + subprocess.run( + [ + 'security', + 'create-keychain', + '-p', + self._keychain_pass, + self._keychain_name, + ], + check=True, + capture_output=True, + ) + # Grab list of current keychains. + keychains = [ + line.strip().replace('"', '') + for line in subprocess.run( + ['security', 'list-keychains', '-d', 'user'], + check=True, + capture_output=True, + ) + .stdout.decode() + .splitlines() + ] + # Warn if we're seeing keychain leaks/etc. + if len(keychains) != 1: + print( + f'{Clr.RED}Expected to initially find 1 keychain;' + f' got {keychains}{Clr.RST}' + ) + assert all(os.path.exists(p) for p in keychains) + + keychains.insert(0, self._keychain_name) + subprocess.run( + ['security', 'list-keychains', '-d', 'user', '-s'] + keychains, + check=True, + ) + subprocess.run( + [ + 'security', + 'unlock-keychain', + '-p', + self._keychain_pass, + self._keychain_name, + ], + check=True, + capture_output=True, + ) + subprocess.run( + [ + 'security', + 'import', + self._signingconfig.certfile, + '-k', + self._keychain_name, + '-f', + 'pkcs12', + '-A', + '-T', + '/usr/bin/codesign', + '-T', + '/usr/bin/security', + '-P', + self._signingconfig.certpass, + ], + check=True, + capture_output=True, + ) + subprocess.run( + [ + 'security', + 'set-key-partition-list', + '-S', + 'apple-tool:,apple:', + '-k', + self._keychain_pass, + self._keychain_name, + ], + check=True, + capture_output=True, + ) + + def _tear_down_keychain(self) -> None: + if self._signingconfig is None: + return + + # We're mucking with keychain settings here which is a global thing. + # Let's try to at least avoid two of us mucking with it at once. + with self._get_keychain_file_lock(): + print('Tearing down signing-config...') + + # Grab list of current keychains. + keychains = [ + line.strip().replace('"', '') + for line in subprocess.run( + ['security', 'list-keychains', '-d', 'user'], + check=True, + capture_output=True, + ) + .stdout.decode() + .splitlines() + ] + + # Strip out ours. + keychains = [k for k in keychains if self._keychain_name not in k] + + # Warn if this doesn't put us back to the default 1. + if len(keychains) != 1: + print( + f'{Clr.RED}Expected to restore to 1 keychain;' + f' got {keychains}{Clr.RST}' + ) + subprocess.run( + ['security', 'list-keychains', '-d', 'user', '-s'] + keychains, + check=True, + capture_output=True, + ) + subprocess.run( + ['security', 'delete-keychain', self._keychain_name], + check=True, + capture_output=True, + ) @staticmethod - def _argstr(args: list[str], flag: str) -> str: + def _argstr(args: list[str], flag: str, remove: bool = False) -> str: try: - return args[args.index(flag) + 1] + flagindex = args.index(flag) + val = args[flagindex + 1] + if remove: + del args[flagindex : flagindex + 2] + return val except (ValueError, IndexError) as exc: raise RuntimeError(f'{flag} value not found') from exc @@ -116,7 +394,7 @@ class XCodeBuild: self._output = [] self._section = None self._returncode = 0 - print(f'{Clr.BLU}Running build: {Clr.BLD}{cmd}{Clr.RST}') + print(f'{Clr.BLU}Running build: {Clr.BLD}{cmd}{Clr.RST}', flush=True) with subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) as proc: @@ -132,9 +410,9 @@ class XCodeBuild: self._returncode = proc.returncode def _print_filtered_line(self, line: str) -> None: - # pylint: disable=too-many-branches # pylint: disable=too-many-statements + # pylint: disable=too-many-return-statements # NOTE: xcodebuild output can be coming from multiple tasks and # intermingled, so lets try to be as conservative as possible when @@ -143,6 +421,7 @@ class XCodeBuild: if self._verbose: sys.stdout.write(line) + sys.stdout.flush() return # Look for a few special cases regardless of the section we're in: @@ -150,16 +429,51 @@ class XCodeBuild: sys.stdout.write( f'{Clr.GRN}{Clr.BLD}XCODE BUILD SUCCEEDED{Clr.RST}\n' ) + sys.stdout.flush() return if line == '** CLEAN SUCCEEDED **\n': sys.stdout.write( f'{Clr.GRN}{Clr.BLD}XCODE CLEAN SUCCEEDED{Clr.RST}\n' ) + sys.stdout.flush() return - if 'warning: OpenGL is deprecated.' in line: - return # yes Apple, I know. + # Seeing these popping up in the middle of other stuff a lot. + if any( + line.startswith(x) + for x in [ + 'SwiftDriver\\ Compilation\\ Requirements ', + 'SwiftDriver\\ Compilation ', + ] + ): + return + lsplits = line.split() + if lsplits and lsplits[0] in ['builtin-Swift-Compilation']: + return + + # If they're warning us about build phases running every time, + # spit out a simplified warning. + before = "Run script build phase '" + after = "' will be run during every build because" + if before in line and after in line: + phasename = line.split(before)[-1].split(after)[0] + sys.stdout.write( + f"{Clr.WHT}Warning: build phase '{phasename}'" + f' is running every time (no deps set up).{Clr.RST}\n' + ) + return + + warnstr = 'warning: The Copy Bundle Resources build phase contains' + if warnstr in line: + warnstr2 = line[line.index(warnstr) :].replace( + 'warning: ', 'Warning: ' + ) + sys.stdout.write(f'{Clr.WHT}{warnstr2}{Clr.RST}') + return + + # if 'warning: OpenGL is deprecated.' in line: + # return # yes Apple, I know. # xcodebuild output generally consists of some high level command # ('CompileC blah blah blah') followed by a number of related lines. @@ -183,6 +497,49 @@ class XCodeBuild: return if self._section is _Section.COMPILEC: self._print_compilec_line(line) + elif self._section is _Section.SWIFTCOMPILE: + self._print_swift_compile_line(line) + elif self._section is _Section.SWIFTMERGEGENERATEDHEADERS: + self._print_simple_section_line( + line, ignore_line_starts=['builtin-swiftHeaderTool'] + ) + elif self._section is _Section.SWIFTDRIVER: + self._print_simple_section_line( + line, + ignore_line_starts=[ + 'builtin-SwiftDriver', + 'builtin-Swift-Compilation', + ], + ) + elif self._section is _Section.SWIFTEMITMODULE: + self._print_simple_section_line( + line, + ignore_line_starts=[ + 'builtin-SwiftDriver', + 'builtin-swiftTaskExecution', + ], + ) + elif self._section is _Section.SWIFTGENERATEPCH: + self._print_simple_section_line( + line, + ignore_line_starts=['builtin-swiftTaskExecution'], + prefix_unexpected=False, + ) + elif self._section is _Section.GENERATEDSYMFILE: + self._print_simple_section_line( + line, + prefix='Generating DSYM File', + ignore_line_start_tails=['/dsymutil'], + ) + elif self._section is _Section.SWIFTDRIVERJOBDISCOVERY: + self._print_simple_section_line( + line, + ignore_line_starts=[ + 'builtin-Swift-Compilation-Requirements', + 'builtin-Swift-Compilation', + 'builtin-swiftTaskExecution', + ], + ) elif self._section is _Section.MKDIR: self._print_mkdir_line(line) elif self._section is _Section.LD: @@ -197,11 +554,16 @@ class XCodeBuild: self._print_simple_section_line( line, ignore_line_start_tails=['/ibtool'] ) + elif self._section is _Section.CPRESOURCE: + self._print_simple_section_line( + line, ignore_line_starts=['builtin-copy'] + ) elif self._section is _Section.PROCESSINFOPLISTFILE: self._print_process_info_plist_file_line(line) elif self._section is _Section.COPYSWIFTLIBS: self._print_simple_section_line( - line, ignore_line_starts=['builtin-swiftStdLibTool'] + line, + ignore_line_starts=['builtin-swiftStdLibTool'], ) elif self._section is _Section.REGISTEREXECUTIONPOLICYEXCEPTION: self._print_simple_section_line( @@ -210,11 +572,20 @@ class XCodeBuild: ) elif self._section is _Section.VALIDATE: self._print_simple_section_line( - line, ignore_line_starts=['builtin-validationUtility'] + line, + ignore_line_starts=['builtin-validationUtility'], + ) + elif self._section is _Section.CONVERTICONSETFILE: + self._print_simple_section_line( + line, + prefix='Creating', + prefix_index=1, + ignore_line_start_tails=['/iconutil'], ) elif self._section is _Section.TOUCH: self._print_simple_section_line( - line, ignore_line_starts=['/usr/bin/touch'] + line, + ignore_line_starts=['/usr/bin/touch'], ) elif self._section is _Section.REGISTERWITHLAUNCHSERVICES: self._print_simple_section_line( @@ -222,36 +593,60 @@ class XCodeBuild: ) elif self._section is _Section.METALLINK: self._print_simple_section_line( - line, prefix='Linking', ignore_line_start_tails=['/metal'] + line, + prefix='Linking', + prefix_index=1, + ignore_line_start_tails=['/metal'], ) + # I think this is outdated and can go away?... elif self._section is _Section.COMPILESWIFT: self._print_simple_section_line( line, prefix='Compiling', prefix_index=3, - ignore_line_start_tails=['/swift-frontend', 'EmitSwiftModule'], + ignore_line_start_tails=[ + '/swift-frontend', + 'EmitSwiftModule', + ], ) elif self._section is _Section.CREATEBUILDDIRECTORY: self._print_simple_section_line( - line, ignore_line_starts=['builtin-create-build-directory'] + line, + ignore_line_starts=['builtin-create-build-directory'], + ignore_line_start_tails=['/clang-stat-cache'], ) elif self._section is _Section.COMPILEMETALFILE: self._print_simple_section_line( line, prefix='Metal-Compiling', + prefix_index=1, ignore_line_start_tails=['/metal'], ) elif self._section is _Section.COPY: self._print_simple_section_line( - line, ignore_line_starts=['builtin-copy'] + line, + ignore_line_starts=['builtin-copy'], ) + elif self._section is _Section.CLANGSTATCACHE: + self._print_simple_section_line( + line, + ignore_line_start_tails=['/clang-stat-cache'], + ) + elif self._section is _Section.EXTRACTAPPINTENTSMETADATA: + # Don't think we need to see this. + if 'note: Metadata extraction skipped' in line: + pass + else: + self._print_simple_section_line( + line, + ignore_line_start_tails=['/appintentsmetadataprocessor'], + ) elif self._section is _Section.COPYSTRINGSFILE: self._print_simple_section_line( line, ignore_line_starts=[ 'builtin-copyStrings', 'CopyPNGFile', - 'ConvertIconsetFile', ], ignore_line_start_tails=[ '/InfoPlist.strings:1:1:', @@ -259,18 +654,17 @@ class XCodeBuild: '/iconutil', ], ) - elif self._section is _Section.WRITEAUXILIARYFILE: - # EW: this spits out our full list of entitlements line by line. - # We should make this smart enough to ignore that whole section - # but just ignoring specific exact lines for now. + elif self._section is _Section.PROCESSPRODUCTPACKAGING: + if '.net.froemling.ballistica.ios"' in line: + return self._print_simple_section_line( line, ignore_line_starts=[ - 'PhaseScriptExecution', - '/bin/sh -c', - 'write-file', + '"application-identifier"', + '"com.apple.developer.ubiquity-kvstore-identifier"', + '"get-task-allow"', + '"keychain-access-groups"', 'builtin-productPackagingUtility', - 'ProcessProductPackaging', 'Entitlements:', '{', '}', @@ -286,12 +680,34 @@ class XCodeBuild: '"com.apple.security.device.bluetooth"', '"com.apple.security.device.usb"', '"com.apple.security.get-task-allow"', + '"com.apple.developer.game-center"', + '"com.apple.developer.team-identifier"', + '"com.apple.application-identifier"', '"com.apple.security.network.client"', '"com.apple.security.network.server"', '"com.apple.security.scripting-targets"', '"com.apple.Music.library.read",', ], ) + elif self._section is _Section.PROCESSPRODUCTPACKAGINGDER: + self._print_simple_section_line( + line, + ignore_line_start_tails=['/derq'], + ) + elif self._section is _Section.WRITEAUXILIARYFILE: + # EW: this spits out our full list of entitlements line by line. + # We should make this smart enough to ignore that whole section + # but just ignoring specific exact lines for now. + self._print_simple_section_line( + line, + ignore_line_starts=[ + 'PhaseScriptExecution', + '/bin/sh -c', + 'write-file', + 'builtin-productPackagingUtility', + 'ProcessProductPackaging', + ], + ) elif self._section is _Section.COMPILESWIFTSOURCES: self._print_simple_section_line( line, @@ -314,16 +730,26 @@ class XCodeBuild: ) elif self._section is _Section.PHASESCRIPTEXECUTION: self._print_simple_section_line( - line, prefix='Running Script', ignore_line_starts=['/bin/sh'] + line, + prefix='Running Script', + prefix_index=1, + ignore_line_starts=['/bin/sh'], ) + # elif self._section is _Section.NOTE: + # self._print_note_line(line) else: assert_never(self._section) + sys.stdout.flush() def _print_compilec_line(self, line: str) -> None: + # TEMP + # sys.stdout.write(line) + # return # First line of the section. if self._section_line_count == 0: - fname = os.path.basename(shlex.split(line)[2]) + # If the file path starts with cwd, strip that out. + fname = shlex.split(line)[2].removeprefix(f'{os.getcwd()}/') sys.stdout.write(f'{Clr.BLU}Compiling {Clr.BLD}{fname}{Clr.RST}\n') return @@ -339,8 +765,37 @@ class XCodeBuild: # Fall back on printing anything we don't recognize. sys.stdout.write(line) - def _print_mkdir_line(self, line: str) -> None: + def _print_swift_compile_line(self, line: str) -> None: + # First line of the section. + if self._section_line_count == 0: + # Currently seeing 2 mostly identical lines per compiled files. + # The first has a 'Compiling\ foo.swift /path/to/foo.swift' + # The second is just /path/to/foo.swift. + # Let's hide the first. + if line.split()[3] == 'Compiling\\': + return + # If the file path starts with cwd, strip that out. + fname = shlex.split(line)[3].removeprefix(f'{os.getcwd()}/') + sys.stdout.write(f'{Clr.BLU}Compiling {Clr.BLD}{fname}{Clr.RST}\n') + return + + # Ignore empty lines or things we expect to be there. + splits = line.split() + if not splits: + return + if splits[0] in [ + 'cd', + 'builtin-swiftTaskExecution', + ]: + return + if any(splits[0].endswith(s) for s in ['/clang', '/swift-frontend']): + return + + # Fall back on printing anything we don't recognize. + sys.stdout.write(line) + + def _print_mkdir_line(self, line: str) -> None: # First line of the section. if self._section_line_count == 0: return @@ -356,7 +811,6 @@ class XCodeBuild: sys.stdout.write(line) def _print_ld_line(self, line: str) -> None: - # First line of the section. if self._section_line_count == 0: name = os.path.basename(shlex.split(line)[1]) @@ -409,7 +863,6 @@ class XCodeBuild: sys.stdout.write(line) def _print_compile_storyboard_line(self, line: str) -> None: - # First line of the section. if self._section_line_count == 0: name = os.path.basename(shlex.split(line)[1]) @@ -431,7 +884,7 @@ class XCodeBuild: sys.stdout.write(line) def _print_code_sign_line(self, line: str) -> None: - + # pylint: disable=too-many-return-statements # First line of the section. if self._section_line_count == 0: name = os.path.basename(shlex.split(line)[1]) @@ -442,6 +895,21 @@ class XCodeBuild: splits = line.split() if not splits: return + if ( + len(splits) > 1 + and splits[0] == 'Provisioning' + and splits[1] == 'Profile:' + ): + return + # A uuid string (provisioning profile id or whatnot) + if ( + len(splits) == 1 + and splits[0].startswith('(') + and splits[0].endswith(')') + and len(splits[0].split('-')) == 5 + ): + return + if splits[0] in ['cd', 'export', '/usr/bin/codesign']: return if line.strip().startswith('Signing Identity:'): @@ -453,7 +921,6 @@ class XCodeBuild: sys.stdout.write(line) def _print_process_info_plist_file_line(self, line: str) -> None: - # First line of the section. if self._section_line_count == 0: name = os.path.basename(shlex.split(line)[1]) @@ -474,11 +941,11 @@ class XCodeBuild: self, line: str, prefix: str | None = None, - prefix_index: int | None = 1, + prefix_index: int | None = None, ignore_line_starts: list[str] | None = None, ignore_line_start_tails: list[str] | None = None, + prefix_unexpected: bool = True, ) -> None: - if ignore_line_starts is None: ignore_line_starts = [] if ignore_line_start_tails is None: @@ -510,10 +977,10 @@ class XCodeBuild: return # Fall back on printing anything we don't recognize. - if prefix is None: + if prefix is None and prefix_unexpected: # If a prefix was not supplied for this section, the user will # have no way to know what this output relates to. Tack a bit - # on to clarify in that case. + # on to clarify in that case (unless requested not to). assert self._section is not None sys.stdout.write( f'{Clr.YLW}Unexpected {self._section.value}' @@ -548,7 +1015,6 @@ def project_build_path( and configuration in config[project_path] and scheme in config[project_path][configuration] ): - # Ok we've found a build-dir entry for this project; now if it # exists on disk and all timestamps within it are decently # close to the one we've got recorded, lets use it. @@ -595,16 +1061,16 @@ def project_build_path( prefix = 'TARGET_BUILD_DIR = ' lines = [l for l in output.splitlines() if l.strip().startswith(prefix)] if len(lines) != 1: - raise Exception( - 'TARGET_BUILD_DIR not found in xcodebuild settings output' + raise RuntimeError( + 'TARGET_BUILD_DIR not found in xcodebuild settings output.' ) build_dir = lines[0].replace(prefix, '').strip() prefix = 'EXECUTABLE_PATH = ' lines = [l for l in output.splitlines() if l.strip().startswith(prefix)] if len(lines) != 1: - raise Exception( - 'EXECUTABLE_PATH not found in xcodebuild settings output' + raise RuntimeError( + 'EXECUTABLE_PATH not found in xcodebuild settings output.' ) executable_path = lines[0].replace(prefix, '').strip() diff --git a/tools/pcommand b/tools/pcommand index e372f9af..a8cd06be 100755 --- a/tools/pcommand +++ b/tools/pcommand @@ -1,4 +1,4 @@ -#!/usr/bin/env python3.10 +#!/usr/bin/env python3.11 # Released under the MIT License. See LICENSE for details. # """A collection of commands for use with this project. @@ -47,13 +47,16 @@ from efrotools.pcommand import ( try_repeat, xcodebuild, xcoderun, + tweak_empty_py_files, + make_ensure, + make_target_debug, +) +from efrotools.pcommand2 import ( + with_build_lock, ) from batools.pcommand import ( - stage_server_file, - py_examine, resize_image, check_clean_safety, - clean_orphaned_assets, archive_old_builds, lazy_increment_build, get_master_asset_src_dir, @@ -64,6 +67,7 @@ from batools.pcommand import ( gen_fulltest_buildfile_windows, gen_fulltest_buildfile_apple, gen_fulltest_buildfile_linux, + prune_includes, python_version_android, python_version_apple, python_build_apple, @@ -92,29 +96,32 @@ from batools.pcommand import ( prefab_run_var, make_prefab, lazybuild, - android_archive_unstripped_libs, efro_gradle, stage_assets, - update_assets_makefile, update_project, - update_cmake_prefab_lib, cmake_prep_dir, gen_binding_code, gen_flat_data_code, wsl_path_to_win, wsl_build_check_win_drive, - win_ci_binary_build, genchangelog, android_sdk_utils, logcat, - update_resources_makefile, - update_meta_makefile, gen_python_enums_module, gen_python_init_module, - update_dummy_modules, - win_ci_install_prereqs, + gen_dummy_modules, version, ) +from batools.pcommand2 import ( + gen_monolithic_register_modules, + stage_server_file, + py_examine, + clean_orphaned_assets, + win_ci_install_prereqs, + win_ci_binary_build, + update_cmake_prefab_lib, + android_archive_unstripped_libs, +) # pylint: enable=unused-import diff --git a/tools/spinoff b/tools/spinoff new file mode 100755 index 00000000..e50dfb26 --- /dev/null +++ b/tools/spinoff @@ -0,0 +1,13 @@ +#!/usr/bin/env python3.11 +# Released under the MIT License. See LICENSE for details. +# +"""Command line wrapper for the spinoff system.""" + +from __future__ import annotations + +from batools.spinoff import spinoff_main + +if __name__ == '__main__': + import batools.spinoff + + spinoff_main()